diff --git a/common/calculate.ts b/common/calculate.ts index e4c9ed07..5edf1211 100644 --- a/common/calculate.ts +++ b/common/calculate.ts @@ -1,4 +1,4 @@ -import { maxBy, sortBy, sum, sumBy } from 'lodash' +import { maxBy, partition, sortBy, sum, sumBy } from 'lodash' import { Bet, LimitBet } from './bet' import { calculateCpmmSale, @@ -255,3 +255,43 @@ export function getTopAnswer( ) return top?.answer } + +export function getLargestPosition(contract: Contract, userBets: Bet[]) { + let yesFloorShares = 0, + yesShares = 0, + noShares = 0, + noFloorShares = 0 + + if (userBets.length === 0) { + return null + } + if (contract.outcomeType === 'FREE_RESPONSE') { + const answerCounts: { [outcome: string]: number } = {} + for (const bet of userBets) { + if (bet.outcome) { + if (!answerCounts[bet.outcome]) { + answerCounts[bet.outcome] = bet.amount + } else { + answerCounts[bet.outcome] += bet.amount + } + } + } + const majorityAnswer = + maxBy(Object.keys(answerCounts), (outcome) => answerCounts[outcome]) ?? '' + return { + prob: undefined, + shares: answerCounts[majorityAnswer] || 0, + outcome: majorityAnswer, + } + } + + const [yesBets, noBets] = partition(userBets, (bet) => bet.outcome === 'YES') + yesShares = sumBy(yesBets, (bet) => bet.shares) + noShares = sumBy(noBets, (bet) => bet.shares) + yesFloorShares = Math.floor(yesShares) + noFloorShares = Math.floor(noShares) + + const shares = yesFloorShares || noFloorShares + const outcome = yesFloorShares > noFloorShares ? 'YES' : 'NO' + return { shares, outcome } +} diff --git a/common/comment.ts b/common/comment.ts index 7ecbb6d4..cdb62fd3 100644 --- a/common/comment.ts +++ b/common/comment.ts @@ -33,6 +33,11 @@ export type OnContract = { // denormalized from bet betAmount?: number betOutcome?: string + + // denormalized based on betting history + commenterPositionProb?: number // binary only + commenterPositionShares?: number + commenterPositionOutcome?: string } export type OnGroup = { diff --git a/common/contract.ts b/common/contract.ts index 0d2a38ca..2f71bab7 100644 --- a/common/contract.ts +++ b/common/contract.ts @@ -148,7 +148,7 @@ export const OUTCOME_TYPES = [ 'NUMERIC', ] as const -export const MAX_QUESTION_LENGTH = 480 +export const MAX_QUESTION_LENGTH = 240 export const MAX_DESCRIPTION_LENGTH = 16000 export const MAX_TAG_LENGTH = 60 diff --git a/common/economy.ts b/common/economy.ts index a412d4de..7ec52b30 100644 --- a/common/economy.ts +++ b/common/economy.ts @@ -7,7 +7,7 @@ export const FIXED_ANTE = econ?.FIXED_ANTE ?? 100 export const STARTING_BALANCE = econ?.STARTING_BALANCE ?? 1000 // for sus users, i.e. multiple sign ups for same person export const SUS_STARTING_BALANCE = econ?.SUS_STARTING_BALANCE ?? 10 -export const REFERRAL_AMOUNT = econ?.REFERRAL_AMOUNT ?? 500 +export const REFERRAL_AMOUNT = econ?.REFERRAL_AMOUNT ?? 250 export const UNIQUE_BETTOR_BONUS_AMOUNT = econ?.UNIQUE_BETTOR_BONUS_AMOUNT ?? 10 export const BETTING_STREAK_BONUS_AMOUNT = diff --git a/common/envs/dev.ts b/common/envs/dev.ts index 719de36e..96ec4dc2 100644 --- a/common/envs/dev.ts +++ b/common/envs/dev.ts @@ -16,4 +16,6 @@ export const DEV_CONFIG: EnvConfig = { cloudRunId: 'w3txbmd3ba', cloudRunRegion: 'uc', amplitudeApiKey: 'fd8cbfd964b9a205b8678a39faae71b3', + // this is Phil's deployment + twitchBotEndpoint: 'https://king-prawn-app-5btyw.ondigitalocean.app', } diff --git a/common/envs/prod.ts b/common/envs/prod.ts index a9d1ffc3..3014f4e3 100644 --- a/common/envs/prod.ts +++ b/common/envs/prod.ts @@ -2,6 +2,7 @@ export type EnvConfig = { domain: string firebaseConfig: FirebaseConfig amplitudeApiKey?: string + twitchBotEndpoint?: string // IDs for v2 cloud functions -- find these by deploying a cloud function and // examining the URL, https://[name]-[cloudRunId]-[cloudRunRegion].a.run.app @@ -66,6 +67,7 @@ export const PROD_CONFIG: EnvConfig = { appId: '1:128925704902:web:f61f86944d8ffa2a642dc7', measurementId: 'G-SSFK1Q138D', }, + twitchBotEndpoint: 'https://twitch-bot-nggbo3neva-uc.a.run.app', cloudRunId: 'nggbo3neva', cloudRunRegion: 'uc', adminEmails: [ @@ -82,9 +84,9 @@ export const PROD_CONFIG: EnvConfig = { visibility: 'PUBLIC', moneyMoniker: 'M$', - bettor: 'predictor', - pastBet: 'prediction', - presentBet: 'predict', + bettor: 'trader', + pastBet: 'trade', + presentBet: 'trade', navbarLogoPath: '', faviconPath: '/favicon.ico', newQuestionPlaceholders: [ diff --git a/common/stats.ts b/common/stats.ts index 152a6eae..8ddf3466 100644 --- a/common/stats.ts +++ b/common/stats.ts @@ -1,20 +1,22 @@ export type Stats = { startDate: number dailyActiveUsers: number[] + dailyActiveUsersWeeklyAvg: number[] weeklyActiveUsers: number[] monthlyActiveUsers: number[] + d1: number[] + d1WeeklyAvg: number[] + nd1: number[] + nd1WeeklyAvg: number[] + nw1: number[] dailyBetCounts: number[] dailyContractCounts: number[] dailyCommentCounts: number[] dailySignups: number[] weekOnWeekRetention: number[] monthlyRetention: number[] - weeklyActivationRate: number[] - topTenthActions: { - daily: number[] - weekly: number[] - monthly: number[] - } + dailyActivationRate: number[] + dailyActivationRateWeeklyAvg: number[] manaBet: { daily: number[] weekly: number[] diff --git a/common/user.ts b/common/user.ts index 5ab07d35..0372d99b 100644 --- a/common/user.ts +++ b/common/user.ts @@ -56,11 +56,6 @@ export type PrivateUser = { username: string // denormalized from User email?: string - unsubscribedFromResolutionEmails?: boolean - unsubscribedFromCommentEmails?: boolean - unsubscribedFromAnswerEmails?: boolean - unsubscribedFromGenericEmails?: boolean - unsubscribedFromWeeklyTrendingEmails?: boolean weeklyTrendingEmailSent?: boolean manaBonusEmailSent?: boolean initialDeviceToken?: string @@ -86,9 +81,11 @@ export type PortfolioMetrics = { export const MANIFOLD_USERNAME = 'ManifoldMarkets' export const MANIFOLD_AVATAR_URL = 'https://manifold.markets/logo-bg-white.png' -export const BETTOR = ENV_CONFIG.bettor ?? 'bettor' // aka predictor -export const BETTORS = ENV_CONFIG.bettor + 's' ?? 'bettors' -export const PRESENT_BET = ENV_CONFIG.presentBet ?? 'bet' // aka predict -export const PRESENT_BETS = ENV_CONFIG.presentBet + 's' ?? 'bets' -export const PAST_BET = ENV_CONFIG.pastBet ?? 'bet' // aka prediction -export const PAST_BETS = ENV_CONFIG.pastBet + 's' ?? 'bets' // aka predictions +// TODO: remove. Hardcoding the strings would be better. +// Different views require different language. +export const BETTOR = ENV_CONFIG.bettor ?? 'trader' +export const BETTORS = ENV_CONFIG.bettor + 's' ?? 'traders' +export const PRESENT_BET = ENV_CONFIG.presentBet ?? 'trade' +export const PRESENT_BETS = ENV_CONFIG.presentBet + 's' ?? 'trades' +export const PAST_BET = ENV_CONFIG.pastBet ?? 'trade' +export const PAST_BETS = ENV_CONFIG.pastBet + 's' ?? 'trades' diff --git a/firestore.rules b/firestore.rules index 08214b10..26649fa6 100644 --- a/firestore.rules +++ b/firestore.rules @@ -27,7 +27,7 @@ service cloud.firestore { allow read; allow update: if userId == request.auth.uid && request.resource.data.diff(resource.data).affectedKeys() - .hasOnly(['bio', 'bannerUrl', 'website', 'twitterHandle', 'discordHandle', 'followedCategories', 'lastPingTime','shouldShowWelcome', 'hasSeenContractFollowModal']); + .hasOnly(['bio', 'bannerUrl', 'website', 'twitterHandle', 'discordHandle', 'followedCategories', 'lastPingTime','shouldShowWelcome', 'hasSeenContractFollowModal', 'homeSections']); // User referral rules allow update: if userId == request.auth.uid && request.resource.data.diff(resource.data).affectedKeys() @@ -78,7 +78,7 @@ service cloud.firestore { allow read: if userId == request.auth.uid || isAdmin(); allow update: if (userId == request.auth.uid || isAdmin()) && request.resource.data.diff(resource.data).affectedKeys() - .hasOnly(['apiKey', 'unsubscribedFromResolutionEmails', 'unsubscribedFromCommentEmails', 'unsubscribedFromAnswerEmails', 'unsubscribedFromWeeklyTrendingEmails', 'notificationPreferences', 'twitchInfo']); + .hasOnly(['apiKey', 'notificationPreferences', 'twitchInfo']); } match /private-users/{userId}/views/{viewId} { @@ -171,33 +171,32 @@ service cloud.firestore { allow read; } - match /groups/{groupId} { - allow read; - allow update: if (request.auth.uid == resource.data.creatorId || isAdmin()) - && request.resource.data.diff(resource.data) - .affectedKeys() - .hasOnly(['name', 'about', 'anyoneCanJoin', 'aboutPostId' ]); - allow delete: if request.auth.uid == resource.data.creatorId; + match /groups/{groupId} { + allow read; + allow update: if (request.auth.uid == resource.data.creatorId || isAdmin()) + && request.resource.data.diff(resource.data) + .affectedKeys() + .hasOnly(['name', 'about', 'anyoneCanJoin', 'aboutPostId' ]); + allow delete: if request.auth.uid == resource.data.creatorId; - match /groupContracts/{contractId} { - allow write: if isGroupMember() || request.auth.uid == get(/databases/$(database)/documents/groups/$(groupId)).data.creatorId - } + match /groupContracts/{contractId} { + allow write: if isGroupMember() || request.auth.uid == get(/databases/$(database)/documents/groups/$(groupId)).data.creatorId + } - match /groupMembers/{memberId}{ - allow create: if request.auth.uid == get(/databases/$(database)/documents/groups/$(groupId)).data.creatorId || (request.auth.uid == request.resource.data.userId && get(/databases/$(database)/documents/groups/$(groupId)).data.anyoneCanJoin); - allow delete: if request.auth.uid == resource.data.userId; - } + match /groupMembers/{memberId}{ + allow create: if request.auth.uid == get(/databases/$(database)/documents/groups/$(groupId)).data.creatorId || (request.auth.uid == request.resource.data.userId && get(/databases/$(database)/documents/groups/$(groupId)).data.anyoneCanJoin); + allow delete: if request.auth.uid == resource.data.userId; + } - function isGroupMember() { - return exists(/databases/$(database)/documents/groups/$(groupId)/groupMembers/$(request.auth.uid)); - } + function isGroupMember() { + return exists(/databases/$(database)/documents/groups/$(groupId)/groupMembers/$(request.auth.uid)); + } - match /comments/{commentId} { - allow read; - allow create: if request.auth != null && commentMatchesUser(request.auth.uid, request.resource.data) && isGroupMember(); - } - - } + match /comments/{commentId} { + allow read; + allow create: if request.auth != null && commentMatchesUser(request.auth.uid, request.resource.data) && isGroupMember(); + } + } match /posts/{postId} { allow read; diff --git a/functions/README.md b/functions/README.md index 97a7a33b..02477588 100644 --- a/functions/README.md +++ b/functions/README.md @@ -65,5 +65,6 @@ Adapted from https://firebase.google.com/docs/functions/get-started Secrets are strings that shouldn't be checked into Git (eg API keys, passwords). We store these using [Google Secret Manager](https://console.cloud.google.com/security/secret-manager), which provides them as environment variables to functions that require them. Some useful workflows: -- Set a secret: `$ firebase functions:secrets:set stripe.test_secret="THE-API-KEY"` +- Set a secret: `$ firebase functions:secrets:set STRIPE_APIKEY` + - Then, enter the secret in the prompt. - Read a secret: `$ firebase functions:secrets:access STRIPE_APIKEY` diff --git a/functions/package.json b/functions/package.json index d5a578de..ba59f090 100644 --- a/functions/package.json +++ b/functions/package.json @@ -39,6 +39,7 @@ "lodash": "4.17.21", "mailgun-js": "0.22.0", "module-alias": "2.2.2", + "node-fetch": "2", "react-masonry-css": "1.0.16", "stripe": "8.194.0", "zod": "3.17.2" @@ -46,6 +47,7 @@ "devDependencies": { "@types/mailgun-js": "0.22.12", "@types/module-alias": "2.0.1", + "@types/node-fetch": "2.6.2", "firebase-functions-test": "0.3.3" }, "private": true diff --git a/functions/src/email-templates/new-unique-bettor.html b/functions/src/email-templates/new-unique-bettor.html index 51026121..ffd507f5 100644 --- a/functions/src/email-templates/new-unique-bettor.html +++ b/functions/src/email-templates/new-unique-bettor.html @@ -9,7 +9,7 @@ - New unique predictors on your market + New unique traders on your market