diff --git a/functions/.prettierrc b/.prettierrc similarity index 100% rename from functions/.prettierrc rename to .prettierrc diff --git a/common/antes.ts b/common/antes.ts new file mode 100644 index 00000000..cfe2ede8 --- /dev/null +++ b/common/antes.ts @@ -0,0 +1,19 @@ +export const PHANTOM_ANTE = 200 + +export const calcStartPool = (initialProbInt: number, ante?: number) => { + const p = initialProbInt / 100.0 + const totalAnte = PHANTOM_ANTE + (ante || 0) + + const poolYes = + p === 0.5 + ? p * totalAnte + : -(totalAnte * (-p + Math.sqrt((-1 + p) * -p))) / (-1 + 2 * p) + + const poolNo = totalAnte - poolYes + + const f = PHANTOM_ANTE / totalAnte + const startYes = f * poolYes + const startNo = f * poolNo + + return { startYes, startNo, poolYes, poolNo } +} diff --git a/functions/src/types/bet.ts b/common/bet.ts similarity index 94% rename from functions/src/types/bet.ts rename to common/bet.ts index 8b540165..9cbecc9d 100644 --- a/functions/src/types/bet.ts +++ b/common/bet.ts @@ -13,6 +13,7 @@ export type Bet = { sale?: { amount: number // amount user makes from sale betId: string // id of bet being sold + // TODO: add sale time? } isSold?: boolean // true if this BUY bet has been sold diff --git a/web/lib/calculate.ts b/common/calculate.ts similarity index 92% rename from web/lib/calculate.ts rename to common/calculate.ts index 6e446155..5cf08088 100644 --- a/web/lib/calculate.ts +++ b/common/calculate.ts @@ -1,9 +1,10 @@ -import { Bet } from './firebase/bets' -import { Contract } from './firebase/contracts' +import { Bet } from './bet' +import { Contract } from './contract' +import { FEES } from './fees' -const fees = 0.02 +export const blah = () => 999 -export function getProbability(pool: { YES: number; NO: number }) { +export const getProbability = (pool: { YES: number; NO: number }) => { const [yesPool, noPool] = [pool.YES, pool.NO] const numerator = Math.pow(yesPool, 2) const denominator = Math.pow(yesPool, 2) + Math.pow(noPool, 2) @@ -59,7 +60,7 @@ export function calculatePayout( const total = totalShares[outcome] - totalBets[outcome] const winningsPool = truePool - totalBets[outcome] - return (1 - fees) * (amount + ((shares - amount) / total) * winningsPool) + return (1 - FEES) * (amount + ((shares - amount) / total) * winningsPool) } export function calculatePayoutAfterCorrectBet(contract: Contract, bet: Bet) { @@ -78,7 +79,7 @@ export function calculatePayoutAfterCorrectBet(contract: Contract, bet: Bet) { const total = totalSharesOutcome - totalBetsOutcome const winningsPool = truePool - totalBetsOutcome - return (1 - fees) * (amount + ((shares - amount) / total) * winningsPool) + return (1 - FEES) * (amount + ((shares - amount) / total) * winningsPool) } function calculateMktPayout(contract: Contract, bet: Bet) { @@ -103,7 +104,7 @@ function calculateMktPayout(contract: Contract, bet: Bet) { (1 - p) * (contract.totalShares.NO - contract.totalBets.NO) return ( - (1 - fees) * + (1 - FEES) * (betP * bet.amount + ((betP * (bet.shares - bet.amount)) / weightedShareTotal) * winningsPool) ) @@ -160,6 +161,6 @@ export function calculateSaleAmount(contract: Contract, bet: Bet) { const adjShareValue = Math.min(Math.min(1, f) * shareValue, myPool) - const saleAmount = (1 - fees) * adjShareValue + const saleAmount = (1 - FEES) * adjShareValue return saleAmount } diff --git a/common/comment.ts b/common/comment.ts new file mode 100644 index 00000000..8fe25688 --- /dev/null +++ b/common/comment.ts @@ -0,0 +1,12 @@ +// Currently, comments are created after the bet, not atomically with the bet. +// They're uniquely identified by the pair contractId/betId. +export type Comment = { + contractId: string + betId: string + text: string + createdTime: number + // Denormalized, for rendering comments + userName?: string + userUsername?: string + userAvatarUrl?: string +} diff --git a/functions/src/types/contract.ts b/common/contract.ts similarity index 100% rename from functions/src/types/contract.ts rename to common/contract.ts diff --git a/common/fees.ts b/common/fees.ts new file mode 100644 index 00000000..718cdbcb --- /dev/null +++ b/common/fees.ts @@ -0,0 +1,4 @@ +export const PLATFORM_FEE = 0.01 // == 1% +export const CREATOR_FEE = 0.01 + +export const FEES = PLATFORM_FEE + CREATOR_FEE diff --git a/common/new-bet.ts b/common/new-bet.ts new file mode 100644 index 00000000..fe441aa4 --- /dev/null +++ b/common/new-bet.ts @@ -0,0 +1,56 @@ +import { Bet } from './bet' +import { Contract } from './contract' +import { User } from './user' + +export const getNewBetInfo = ( + user: User, + outcome: 'YES' | 'NO', + amount: number, + contract: Contract, + newBetId: string +) => { + const { YES: yesPool, NO: noPool } = contract.pool + + const newPool = + outcome === 'YES' + ? { YES: yesPool + amount, NO: noPool } + : { YES: yesPool, NO: noPool + amount } + + const shares = + outcome === 'YES' + ? amount + (amount * noPool ** 2) / (yesPool ** 2 + amount * yesPool) + : amount + (amount * yesPool ** 2) / (noPool ** 2 + amount * noPool) + + const { YES: yesShares, NO: noShares } = contract.totalShares + + const newTotalShares = + outcome === 'YES' + ? { YES: yesShares + shares, NO: noShares } + : { YES: yesShares, NO: noShares + shares } + + const { YES: yesBets, NO: noBets } = contract.totalBets + + const newTotalBets = + outcome === 'YES' + ? { YES: yesBets + amount, NO: noBets } + : { YES: yesBets, NO: noBets + amount } + + const probBefore = yesPool ** 2 / (yesPool ** 2 + noPool ** 2) + const probAfter = newPool.YES ** 2 / (newPool.YES ** 2 + newPool.NO ** 2) + + const newBet: Bet = { + id: newBetId, + userId: user.id, + contractId: contract.id, + amount, + shares, + outcome, + probBefore, + probAfter, + createdTime: Date.now(), + } + + const newBalance = user.balance - amount + + return { newBet, newPool, newTotalShares, newTotalBets, newBalance } +} diff --git a/common/new-contract.ts b/common/new-contract.ts new file mode 100644 index 00000000..b8abdba7 --- /dev/null +++ b/common/new-contract.ts @@ -0,0 +1,48 @@ +import { calcStartPool } from './antes' +import { Contract } from './contract' +import { User } from './user' + +export function getNewContract( + id: string, + slug: string, + creator: User, + question: string, + description: string, + initialProb: number, + ante?: number, + closeTime?: number +) { + const { startYes, startNo, poolYes, poolNo } = calcStartPool( + initialProb, + ante + ) + + const contract: Contract = { + id, + slug, + outcomeType: 'BINARY', + + creatorId: creator.id, + creatorName: creator.name, + creatorUsername: creator.username, + + question: question.trim(), + description: description.trim(), + + startPool: { YES: startYes, NO: startNo }, + pool: { YES: poolYes, NO: poolNo }, + totalShares: { YES: 0, NO: 0 }, + totalBets: { YES: 0, NO: 0 }, + isResolved: false, + + createdTime: Date.now(), + lastUpdatedTime: Date.now(), + + volume24Hours: 0, + volume7Days: 0, + } + + if (closeTime) contract.closeTime = closeTime + + return contract +} diff --git a/common/payouts.ts b/common/payouts.ts new file mode 100644 index 00000000..e0225a27 --- /dev/null +++ b/common/payouts.ts @@ -0,0 +1,128 @@ +import { Bet } from './bet' +import { Contract } from './contract' +import { CREATOR_FEE, FEES } from './fees' + +export const getCancelPayouts = (truePool: number, bets: Bet[]) => { + console.log('resolved N/A, pool M$', truePool) + + const betSum = sumBy(bets, (b) => b.amount) + + return bets.map((bet) => ({ + userId: bet.userId, + payout: (bet.amount / betSum) * truePool, + })) +} + +export const getStandardPayouts = ( + outcome: string, + truePool: number, + contract: Contract, + bets: Bet[] +) => { + const [yesBets, noBets] = partition(bets, (bet) => bet.outcome === 'YES') + const winningBets = outcome === 'YES' ? yesBets : noBets + + const betSum = sumBy(winningBets, (b) => b.amount) + + if (betSum >= truePool) return getCancelPayouts(truePool, winningBets) + + const creatorPayout = CREATOR_FEE * truePool + console.log( + 'resolved', + outcome, + 'pool: M$', + truePool, + 'creator fee: M$', + creatorPayout + ) + + const shareDifferenceSum = sumBy(winningBets, (b) => b.shares - b.amount) + + const winningsPool = truePool - betSum + + const winnerPayouts = winningBets.map((bet) => ({ + userId: bet.userId, + payout: + (1 - FEES) * + (bet.amount + + ((bet.shares - bet.amount) / shareDifferenceSum) * winningsPool), + })) + + return winnerPayouts.concat([ + { userId: contract.creatorId, payout: creatorPayout }, + ]) // add creator fee +} + +export const getMktPayouts = ( + truePool: number, + contract: Contract, + bets: Bet[] +) => { + const p = + contract.pool.YES ** 2 / (contract.pool.YES ** 2 + contract.pool.NO ** 2) + console.log('Resolved MKT at p=', p, 'pool: $M', truePool) + + const [yesBets, noBets] = partition(bets, (bet) => bet.outcome === 'YES') + + const weightedBetTotal = + p * sumBy(yesBets, (b) => b.amount) + + (1 - p) * sumBy(noBets, (b) => b.amount) + + if (weightedBetTotal >= truePool) { + return bets.map((bet) => ({ + userId: bet.userId, + payout: + (((bet.outcome === 'YES' ? p : 1 - p) * bet.amount) / + weightedBetTotal) * + truePool, + })) + } + + const winningsPool = truePool - weightedBetTotal + + const weightedShareTotal = + p * sumBy(yesBets, (b) => b.shares - b.amount) + + (1 - p) * sumBy(noBets, (b) => b.shares - b.amount) + + const yesPayouts = yesBets.map((bet) => ({ + userId: bet.userId, + payout: + (1 - FEES) * + (p * bet.amount + + ((p * (bet.shares - bet.amount)) / weightedShareTotal) * winningsPool), + })) + + const noPayouts = noBets.map((bet) => ({ + userId: bet.userId, + payout: + (1 - FEES) * + ((1 - p) * bet.amount + + (((1 - p) * (bet.shares - bet.amount)) / weightedShareTotal) * + winningsPool), + })) + + const creatorPayout = CREATOR_FEE * truePool + + return [ + ...yesPayouts, + ...noPayouts, + { userId: contract.creatorId, payout: creatorPayout }, + ] +} + +const partition = (array: T[], f: (t: T) => boolean) => { + const yes = [] + const no = [] + + for (let t of array) { + if (f(t)) yes.push(t) + else no.push(t) + } + + return [yes, no] as [T[], T[]] +} + +const sumBy = (array: T[], f: (t: T) => number) => { + const values = array.map(f) + return values.reduce((prev, cur) => prev + cur, 0) +} diff --git a/common/sell-bet.ts b/common/sell-bet.ts new file mode 100644 index 00000000..8aa2a709 --- /dev/null +++ b/common/sell-bet.ts @@ -0,0 +1,108 @@ +import { Bet } from './bet' +import { Contract } from './contract' +import { CREATOR_FEE, PLATFORM_FEE } from './fees' +import { User } from './user' + +export const getSellBetInfo = ( + user: User, + bet: Bet, + contract: Contract, + newBetId: string +) => { + const { id: betId, amount, shares, outcome } = bet + + const { YES: yesPool, NO: noPool } = contract.pool + const { YES: yesStart, NO: noStart } = contract.startPool + const { YES: yesShares, NO: noShares } = contract.totalShares + const { YES: yesBets, NO: noBets } = contract.totalBets + + const [y, n, s] = [yesPool, noPool, shares] + + const shareValue = + outcome === 'YES' + ? // https://www.wolframalpha.com/input/?i=b+%2B+%28b+n%5E2%29%2F%28y+%28-b+%2B+y%29%29+%3D+c+solve+b + (n ** 2 + + s * y + + y ** 2 - + Math.sqrt( + n ** 4 + (s - y) ** 2 * y ** 2 + 2 * n ** 2 * y * (s + y) + )) / + (2 * y) + : (y ** 2 + + s * n + + n ** 2 - + Math.sqrt( + y ** 4 + (s - n) ** 2 * n ** 2 + 2 * y ** 2 * n * (s + n) + )) / + (2 * n) + + const startPool = yesStart + noStart + const pool = yesPool + noPool - startPool + + const probBefore = yesPool ** 2 / (yesPool ** 2 + noPool ** 2) + + const f = pool / (probBefore * yesShares + (1 - probBefore) * noShares) + + const myPool = outcome === 'YES' ? yesPool - yesStart : noPool - noStart + + const adjShareValue = Math.min(Math.min(1, f) * shareValue, myPool) + + const newPool = + outcome === 'YES' + ? { YES: yesPool - adjShareValue, NO: noPool } + : { YES: yesPool, NO: noPool - adjShareValue } + + const newTotalShares = + outcome === 'YES' + ? { YES: yesShares - shares, NO: noShares } + : { YES: yesShares, NO: noShares - shares } + + const newTotalBets = + outcome === 'YES' + ? { YES: yesBets - amount, NO: noBets } + : { YES: yesBets, NO: noBets - amount } + + const probAfter = newPool.YES ** 2 / (newPool.YES ** 2 + newPool.NO ** 2) + + const creatorFee = CREATOR_FEE * adjShareValue + const saleAmount = (1 - CREATOR_FEE - PLATFORM_FEE) * adjShareValue + + console.log( + 'SELL M$', + amount, + outcome, + 'for M$', + saleAmount, + 'M$/share:', + f, + 'creator fee: M$', + creatorFee + ) + + const newBet: Bet = { + id: newBetId, + userId: user.id, + contractId: contract.id, + amount: -adjShareValue, + shares: -shares, + outcome, + probBefore, + probAfter, + createdTime: Date.now(), + sale: { + amount: saleAmount, + betId, + }, + } + + const newBalance = user.balance + saleAmount + + return { + newBet, + newPool, + newTotalShares, + newTotalBets, + newBalance, + creatorFee, + } +} diff --git a/functions/src/types/user.ts b/common/user.ts similarity index 98% rename from functions/src/types/user.ts rename to common/user.ts index 8fae1c0b..97e349ed 100644 --- a/functions/src/types/user.ts +++ b/common/user.ts @@ -7,4 +7,4 @@ export type User = { balance: number createdTime: number lastUpdatedTime: number -} \ No newline at end of file +} diff --git a/functions/src/util/random-string.ts b/common/util/random-string.ts similarity index 100% rename from functions/src/util/random-string.ts rename to common/util/random-string.ts diff --git a/functions/src/util/slugify.ts b/common/util/slugify.ts similarity index 63% rename from functions/src/util/slugify.ts rename to common/util/slugify.ts index 82172c3a..114ae19f 100644 --- a/functions/src/util/slugify.ts +++ b/common/util/slugify.ts @@ -1,4 +1,8 @@ -export const slugify = (text: any, separator = '-'): string => { +export const slugify = ( + text: string, + separator = '-', + maxLength = 35 +): string => { return text .toString() .normalize('NFD') // split an accented letter in the base letter and the acent @@ -7,4 +11,6 @@ export const slugify = (text: any, separator = '-'): string => { .trim() .replace(/[^a-z0-9 ]/g, '') // remove all chars not letters, numbers and spaces (to be replaced) .replace(/\s+/g, separator) + .substring(0, maxLength) + .replace(new RegExp(separator + '+$', 'g'), '') // remove terminal separators } diff --git a/functions/src/create-contract.ts b/functions/src/create-contract.ts index e40a8db9..dbe3806b 100644 --- a/functions/src/create-contract.ts +++ b/functions/src/create-contract.ts @@ -1,12 +1,11 @@ import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' -import { randomString } from './util/random-string' -import { slugify } from './util/slugify' -import { Contract } from './types/contract' -import { getUser } from './utils' -import { payUser } from '.' -import { User } from './types/user' +import { chargeUser, getUser } from './utils' +import { Contract } from '../../common/contract' +import { slugify } from '../../common/util/slugify' +import { randomString } from '../../common/util/random-string' +import { getNewContract } from '../../common/new-contract' export const createContract = functions .runWith({ minInstances: 1 }) @@ -62,7 +61,7 @@ export const createContract = functions closeTime ) - if (ante) await payUser([creator.id, -ante]) + if (ante) await chargeUser(creator.id, ante) await contractRef.create(contract) return { status: 'success', contract } @@ -70,7 +69,7 @@ export const createContract = functions ) const getSlug = async (question: string) => { - const proposedSlug = slugify(question).substring(0, 35) + const proposedSlug = slugify(question) const preexistingContract = await getContractFromSlug(proposedSlug) @@ -79,73 +78,6 @@ const getSlug = async (question: string) => { : proposedSlug } -function getNewContract( - id: string, - slug: string, - creator: User, - question: string, - description: string, - initialProb: number, - ante?: number, - closeTime?: number -) { - const { startYes, startNo, poolYes, poolNo } = calcStartPool( - initialProb, - ante - ) - - const contract: Contract = { - id, - slug, - outcomeType: 'BINARY', - - creatorId: creator.id, - creatorName: creator.name, - creatorUsername: creator.username, - - question: question.trim(), - description: description.trim(), - - startPool: { YES: startYes, NO: startNo }, - pool: { YES: poolYes, NO: poolNo }, - totalShares: { YES: 0, NO: 0 }, - totalBets: { YES: 0, NO: 0 }, - isResolved: false, - - createdTime: Date.now(), - lastUpdatedTime: Date.now(), - - volume24Hours: 0, - volume7Days: 0, - } - - if (closeTime) contract.closeTime = closeTime - - return contract -} - -const calcStartPool = ( - initialProbInt: number, - ante?: number, - phantomAnte = 200 -) => { - const p = initialProbInt / 100.0 - const totalAnte = phantomAnte + (ante || 0) - - const poolYes = - p === 0.5 - ? p * totalAnte - : -(totalAnte * (-p + Math.sqrt((-1 + p) * -p))) / (-1 + 2 * p) - - const poolNo = totalAnte - poolYes - - const f = phantomAnte / totalAnte - const startYes = f * poolYes - const startNo = f * poolNo - - return { startYes, startNo, poolYes, poolNo } -} - const firestore = admin.firestore() export async function getContractFromSlug(slug: string) { diff --git a/functions/src/emails.ts b/functions/src/emails.ts index 0f85638f..d7318caa 100644 --- a/functions/src/emails.ts +++ b/functions/src/emails.ts @@ -1,8 +1,17 @@ -import { sendEmail } from './send-email' -import { Contract } from './types/contract' -import { User } from './types/user' +import { Contract } from '../../common/contract' +import { User } from '../../common/user' +import { sendTemplateEmail } from './send-email' import { getUser } from './utils' +type market_resolved_template = { + name: string + creatorName: string + question: string + outcome: string + payout: string + url: string +} + export const sendMarketResolutionEmail = async ( userId: string, payout: number, @@ -13,22 +22,24 @@ export const sendMarketResolutionEmail = async ( const user = await getUser(userId) if (!user) return - const subject = `Resolved ${toDisplayResolution[resolution]}: ${contract.question}` + const outcome = toDisplayResolution[resolution] - const body = `Dear ${user.name}, + const subject = `Resolved ${outcome}: ${contract.question}` -A market you bet in has been resolved! + const templateData: market_resolved_template = { + name: user.name, + creatorName: creator.name, + question: contract.question, + outcome, + payout: `${Math.round(payout)}`, + url: `https://manifold.markets/${creator.username}/${contract.slug}`, + } -Creator: ${contract.creatorName} -Question: ${contract.question} -Resolution: ${toDisplayResolution[resolution]} + // Modify template here: + // https://app.mailgun.com/app/sending/domains/mg.manifold.markets/templates/edit/market-resolved/initial + // Mailgun username: james@mantic.markets -Your payout is M$ ${Math.round(payout)} - -View the market here: -https://manifold.markets/${creator.username}/${contract.slug} -` - await sendEmail(user.email, subject, body) + await sendTemplateEmail(user.email, subject, 'market-resolved', templateData) } const toDisplayResolution = { YES: 'YES', NO: 'NO', CANCEL: 'N/A', MKT: 'MKT' } diff --git a/functions/src/place-bet.ts b/functions/src/place-bet.ts index 11ae425d..dee470d6 100644 --- a/functions/src/place-bet.ts +++ b/functions/src/place-bet.ts @@ -1,9 +1,9 @@ import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' -import { Contract } from './types/contract' -import { User } from './types/user' -import { Bet } from './types/bet' +import { Contract } from '../../common/contract' +import { User } from '../../common/user' +import { getNewBetInfo } from '../../common/new-bet' export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall( async ( @@ -42,6 +42,10 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall( return { status: 'error', message: 'Invalid contract' } const contract = contractSnap.data() as Contract + const { closeTime } = contract + if (closeTime && Date.now() > closeTime) + return { status: 'error', message: 'Trading is closed' } + const newBetDoc = firestore .collection(`contracts/${contractId}/bets`) .doc() @@ -63,56 +67,3 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall( ) const firestore = admin.firestore() - -const getNewBetInfo = ( - user: User, - outcome: 'YES' | 'NO', - amount: number, - contract: Contract, - newBetId: string -) => { - const { YES: yesPool, NO: noPool } = contract.pool - - const newPool = - outcome === 'YES' - ? { YES: yesPool + amount, NO: noPool } - : { YES: yesPool, NO: noPool + amount } - - const shares = - outcome === 'YES' - ? amount + (amount * noPool ** 2) / (yesPool ** 2 + amount * yesPool) - : amount + (amount * yesPool ** 2) / (noPool ** 2 + amount * noPool) - - const { YES: yesShares, NO: noShares } = contract.totalShares - - const newTotalShares = - outcome === 'YES' - ? { YES: yesShares + shares, NO: noShares } - : { YES: yesShares, NO: noShares + shares } - - const { YES: yesBets, NO: noBets } = contract.totalBets - - const newTotalBets = - outcome === 'YES' - ? { YES: yesBets + amount, NO: noBets } - : { YES: yesBets, NO: noBets + amount } - - const probBefore = yesPool ** 2 / (yesPool ** 2 + noPool ** 2) - const probAfter = newPool.YES ** 2 / (newPool.YES ** 2 + newPool.NO ** 2) - - const newBet: Bet = { - id: newBetId, - userId: user.id, - contractId: contract.id, - amount, - shares, - outcome, - probBefore, - probAfter, - createdTime: Date.now(), - } - - const newBalance = user.balance - amount - - return { newBet, newPool, newTotalShares, newTotalBets, newBalance } -} diff --git a/functions/src/resolve-market.ts b/functions/src/resolve-market.ts index aad9d32c..8fdbe470 100644 --- a/functions/src/resolve-market.ts +++ b/functions/src/resolve-market.ts @@ -2,14 +2,16 @@ import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' import * as _ from 'lodash' -import { Contract } from './types/contract' -import { User } from './types/user' -import { Bet } from './types/bet' -import { getUser } from './utils' +import { Contract } from '../../common/contract' +import { User } from '../../common/user' +import { Bet } from '../../common/bet' +import { getUser, payUser } from './utils' import { sendMarketResolutionEmail } from './emails' - -export const PLATFORM_FEE = 0.01 // 1% -export const CREATOR_FEE = 0.01 // 1% +import { + getCancelPayouts, + getMktPayouts, + getStandardPayouts, +} from '../../common/payouts' export const resolveMarket = functions .runWith({ minInstances: 1 }) @@ -76,7 +78,9 @@ export const resolveMarket = functions _.sumBy(group, (g) => g.payout) ) - const payoutPromises = Object.entries(userPayouts).map(payUser) + const payoutPromises = Object.entries(userPayouts).map( + ([userId, payout]) => payUser(userId, payout) + ) const result = await Promise.all(payoutPromises) .catch((e) => ({ status: 'error', message: e })) @@ -117,121 +121,3 @@ const sendResolutionEmails = async ( } const firestore = admin.firestore() - -const getCancelPayouts = (truePool: number, bets: Bet[]) => { - console.log('resolved N/A, pool M$', truePool) - - const betSum = _.sumBy(bets, (b) => b.amount) - - return bets.map((bet) => ({ - userId: bet.userId, - payout: (bet.amount / betSum) * truePool, - })) -} - -const getStandardPayouts = ( - outcome: string, - truePool: number, - contract: Contract, - bets: Bet[] -) => { - const [yesBets, noBets] = _.partition(bets, (bet) => bet.outcome === 'YES') - const winningBets = outcome === 'YES' ? yesBets : noBets - - const betSum = _.sumBy(winningBets, (b) => b.amount) - - if (betSum >= truePool) return getCancelPayouts(truePool, winningBets) - - const creatorPayout = CREATOR_FEE * truePool - console.log( - 'resolved', - outcome, - 'pool: M$', - truePool, - 'creator fee: M$', - creatorPayout - ) - - const shareDifferenceSum = _.sumBy(winningBets, (b) => b.shares - b.amount) - - const winningsPool = truePool - betSum - - const winnerPayouts = winningBets.map((bet) => ({ - userId: bet.userId, - payout: - (1 - fees) * - (bet.amount + - ((bet.shares - bet.amount) / shareDifferenceSum) * winningsPool), - })) - - return winnerPayouts.concat([ - { userId: contract.creatorId, payout: creatorPayout }, - ]) // add creator fee -} - -const getMktPayouts = (truePool: number, contract: Contract, bets: Bet[]) => { - const p = - contract.pool.YES ** 2 / (contract.pool.YES ** 2 + contract.pool.NO ** 2) - console.log('Resolved MKT at p=', p, 'pool: $M', truePool) - - const [yesBets, noBets] = _.partition(bets, (bet) => bet.outcome === 'YES') - - const weightedBetTotal = - p * _.sumBy(yesBets, (b) => b.amount) + - (1 - p) * _.sumBy(noBets, (b) => b.amount) - - if (weightedBetTotal >= truePool) { - return bets.map((bet) => ({ - userId: bet.userId, - payout: - (((bet.outcome === 'YES' ? p : 1 - p) * bet.amount) / - weightedBetTotal) * - truePool, - })) - } - - const winningsPool = truePool - weightedBetTotal - - const weightedShareTotal = - p * _.sumBy(yesBets, (b) => b.shares - b.amount) + - (1 - p) * _.sumBy(noBets, (b) => b.shares - b.amount) - - const yesPayouts = yesBets.map((bet) => ({ - userId: bet.userId, - payout: - (1 - fees) * - (p * bet.amount + - ((p * (bet.shares - bet.amount)) / weightedShareTotal) * winningsPool), - })) - - const noPayouts = noBets.map((bet) => ({ - userId: bet.userId, - payout: - (1 - fees) * - ((1 - p) * bet.amount + - (((1 - p) * (bet.shares - bet.amount)) / weightedShareTotal) * - winningsPool), - })) - - const creatorPayout = CREATOR_FEE * truePool - - return [ - ...yesPayouts, - ...noPayouts, - { userId: contract.creatorId, payout: creatorPayout }, - ] -} - -export const payUser = ([userId, payout]: [string, number]) => { - return firestore.runTransaction(async (transaction) => { - const userDoc = firestore.doc(`users/${userId}`) - const userSnap = await transaction.get(userDoc) - if (!userSnap.exists) return - const user = userSnap.data() as User - - const newUserBalance = user.balance + payout - transaction.update(userDoc, { balance: newUserBalance }) - }) -} - -const fees = PLATFORM_FEE + CREATOR_FEE diff --git a/functions/src/scripts/migrate-contract.ts b/functions/src/scripts/migrate-contract.ts index eb55783e..118c9159 100644 --- a/functions/src/scripts/migrate-contract.ts +++ b/functions/src/scripts/migrate-contract.ts @@ -1,7 +1,8 @@ import * as admin from 'firebase-admin' import * as _ from 'lodash' -import { Bet } from '../types/bet' -import { Contract } from '../types/contract' + +import { Bet } from '../../../common/bet' +import { Contract } from '../../../common/contract' type DocRef = admin.firestore.DocumentReference diff --git a/functions/src/scripts/recalculate-contract-totals.ts b/functions/src/scripts/recalculate-contract-totals.ts index d691db36..3a368fbb 100644 --- a/functions/src/scripts/recalculate-contract-totals.ts +++ b/functions/src/scripts/recalculate-contract-totals.ts @@ -1,7 +1,8 @@ import * as admin from 'firebase-admin' import * as _ from 'lodash' -import { Bet } from '../types/bet' -import { Contract } from '../types/contract' + +import { Bet } from '../../../common/bet' +import { Contract } from '../../../common/contract' type DocRef = admin.firestore.DocumentReference diff --git a/functions/src/scripts/rename-user-contracts.ts b/functions/src/scripts/rename-user-contracts.ts index 2debd9e7..8639a590 100644 --- a/functions/src/scripts/rename-user-contracts.ts +++ b/functions/src/scripts/rename-user-contracts.ts @@ -1,6 +1,7 @@ import * as admin from 'firebase-admin' import * as _ from 'lodash' -import { Contract } from '../types/contract' + +import { Contract } from '../../../common/contract' import { getValues } from '../utils' // Generate your own private key, and set the path below: diff --git a/functions/src/sell-bet.ts b/functions/src/sell-bet.ts index 3d37d3c3..840435f1 100644 --- a/functions/src/sell-bet.ts +++ b/functions/src/sell-bet.ts @@ -1,10 +1,10 @@ import * as admin from 'firebase-admin' import * as functions from 'firebase-functions' -import { CREATOR_FEE, PLATFORM_FEE } from './resolve-market' -import { Bet } from './types/bet' -import { Contract } from './types/contract' -import { User } from './types/user' +import { Contract } from '../../common/contract' +import { User } from '../../common/user' +import { Bet } from '../../common/bet' +import { getSellBetInfo } from '../../common/sell-bet' export const sellBet = functions.runWith({ minInstances: 1 }).https.onCall( async ( @@ -33,6 +33,10 @@ export const sellBet = functions.runWith({ minInstances: 1 }).https.onCall( return { status: 'error', message: 'Invalid contract' } const contract = contractSnap.data() as Contract + const { closeTime } = contract + if (closeTime && Date.now() > closeTime) + return { status: 'error', message: 'Trading is closed' } + const betDoc = firestore.doc(`contracts/${contractId}/bets/${betId}`) const betSnap = await transaction.get(betDoc) if (!betSnap.exists) return { status: 'error', message: 'Invalid bet' } @@ -76,107 +80,3 @@ export const sellBet = functions.runWith({ minInstances: 1 }).https.onCall( ) const firestore = admin.firestore() - -const getSellBetInfo = ( - user: User, - bet: Bet, - contract: Contract, - newBetId: string -) => { - const { id: betId, amount, shares, outcome } = bet - - const { YES: yesPool, NO: noPool } = contract.pool - const { YES: yesStart, NO: noStart } = contract.startPool - const { YES: yesShares, NO: noShares } = contract.totalShares - const { YES: yesBets, NO: noBets } = contract.totalBets - - const [y, n, s] = [yesPool, noPool, shares] - - const shareValue = - outcome === 'YES' - ? // https://www.wolframalpha.com/input/?i=b+%2B+%28b+n%5E2%29%2F%28y+%28-b+%2B+y%29%29+%3D+c+solve+b - (n ** 2 + - s * y + - y ** 2 - - Math.sqrt( - n ** 4 + (s - y) ** 2 * y ** 2 + 2 * n ** 2 * y * (s + y) - )) / - (2 * y) - : (y ** 2 + - s * n + - n ** 2 - - Math.sqrt( - y ** 4 + (s - n) ** 2 * n ** 2 + 2 * y ** 2 * n * (s + n) - )) / - (2 * n) - - const startPool = yesStart + noStart - const pool = yesPool + noPool - startPool - - const probBefore = yesPool ** 2 / (yesPool ** 2 + noPool ** 2) - - const f = pool / (probBefore * yesShares + (1 - probBefore) * noShares) - - const myPool = outcome === 'YES' ? yesPool - yesStart : noPool - noStart - - const adjShareValue = Math.min(Math.min(1, f) * shareValue, myPool) - - const newPool = - outcome === 'YES' - ? { YES: yesPool - adjShareValue, NO: noPool } - : { YES: yesPool, NO: noPool - adjShareValue } - - const newTotalShares = - outcome === 'YES' - ? { YES: yesShares - shares, NO: noShares } - : { YES: yesShares, NO: noShares - shares } - - const newTotalBets = - outcome === 'YES' - ? { YES: yesBets - amount, NO: noBets } - : { YES: yesBets, NO: noBets - amount } - - const probAfter = newPool.YES ** 2 / (newPool.YES ** 2 + newPool.NO ** 2) - - const creatorFee = CREATOR_FEE * adjShareValue - const saleAmount = (1 - CREATOR_FEE - PLATFORM_FEE) * adjShareValue - - console.log( - 'SELL M$', - amount, - outcome, - 'for M$', - saleAmount, - 'M$/share:', - f, - 'creator fee: M$', - creatorFee - ) - - const newBet: Bet = { - id: newBetId, - userId: user.id, - contractId: contract.id, - amount: -adjShareValue, - shares: -shares, - outcome, - probBefore, - probAfter, - createdTime: Date.now(), - sale: { - amount: saleAmount, - betId, - }, - } - - const newBalance = user.balance + saleAmount - - return { - newBet, - newPool, - newTotalShares, - newTotalBets, - newBalance, - creatorFee, - } -} diff --git a/functions/src/send-email.ts b/functions/src/send-email.ts index fd995e59..1afc1526 100644 --- a/functions/src/send-email.ts +++ b/functions/src/send-email.ts @@ -4,7 +4,7 @@ import * as functions from 'firebase-functions' const DOMAIN = 'mg.manifold.markets' const mg = mailgun({ apiKey: functions.config().mailgun.key, domain: DOMAIN }) -export const sendEmail = (to: string, subject: string, text: string) => { +export const sendTextEmail = (to: string, subject: string, text: string) => { const data = { from: 'Manifold Markets ', to, @@ -12,7 +12,27 @@ export const sendEmail = (to: string, subject: string, text: string) => { text, } - return mg.messages().send(data, (error, body) => { - console.log('Sent email', error, body) + return mg.messages().send(data, (error) => { + if (error) console.log('Error sending email', error) + else console.log('Sent text email', to, subject) + }) +} + +export const sendTemplateEmail = ( + to: string, + subject: string, + templateId: string, + templateData: Record +) => { + const data = { + from: 'Manifold Markets ', + to, + subject, + template: templateId, + 'h:X-Mailgun-Variables': JSON.stringify(templateData), + } + return mg.messages().send(data, (error) => { + if (error) console.log('Error sending email', error) + else console.log('Sent template email', templateId, to, subject) }) } diff --git a/functions/src/stripe.ts b/functions/src/stripe.ts index aac128cb..71ec4a71 100644 --- a/functions/src/stripe.ts +++ b/functions/src/stripe.ts @@ -2,7 +2,7 @@ import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' import Stripe from 'stripe' -import { payUser } from './resolve-market' +import { payUser } from './utils' const stripe = new Stripe(functions.config().stripe.apikey, { apiVersion: '2020-08-27', @@ -118,7 +118,7 @@ const issueMoneys = async (session: any) => { session, }) - await payUser([userId, payout]) + await payUser(userId, payout) console.log('user', userId, 'paid M$', payout) } diff --git a/functions/src/update-contract-metrics.ts b/functions/src/update-contract-metrics.ts index 8e37b88a..5ebb1d7c 100644 --- a/functions/src/update-contract-metrics.ts +++ b/functions/src/update-contract-metrics.ts @@ -1,9 +1,10 @@ import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' import * as _ from 'lodash' -import { Contract } from './types/contract' + import { getValues } from './utils' -import { Bet } from './types/bet' +import { Contract } from '../../common/contract' +import { Bet } from '../../common/bet' const firestore = admin.firestore() diff --git a/functions/src/utils.ts b/functions/src/utils.ts index 85f5b3fa..887d602c 100644 --- a/functions/src/utils.ts +++ b/functions/src/utils.ts @@ -1,7 +1,7 @@ import * as admin from 'firebase-admin' -import { Contract } from './types/contract' -import { User } from './types/user' +import { Contract } from '../../common/contract' +import { User } from '../../common/user' export const getValue = async (collection: string, doc: string) => { const snap = await admin.firestore().collection(collection).doc(doc).get() @@ -21,3 +21,37 @@ export const getContract = (contractId: string) => { export const getUser = (userId: string) => { return getValue('users', userId) } + +const firestore = admin.firestore() + +const updateUserBalance = (userId: string, delta: number) => { + return firestore.runTransaction(async (transaction) => { + const userDoc = firestore.doc(`users/${userId}`) + const userSnap = await transaction.get(userDoc) + if (!userSnap.exists) return + const user = userSnap.data() as User + + const newUserBalance = user.balance + delta + + if (newUserBalance < 0) + throw new Error( + `User (${userId}) balance cannot be negative: ${newUserBalance}` + ) + + transaction.update(userDoc, { balance: newUserBalance }) + }) +} + +export const payUser = (userId: string, payout: number) => { + if (!isFinite(payout) || payout <= 0) + throw new Error('Payout is not positive: ' + payout) + + return updateUserBalance(userId, payout) +} + +export const chargeUser = (userId: string, charge: number) => { + if (!isFinite(charge) || charge <= 0) + throw new Error('User charge is not positive: ' + charge) + + return updateUserBalance(userId, -charge) +} diff --git a/functions/tsconfig.json b/functions/tsconfig.json index 7ce05d03..6a0ed692 100644 --- a/functions/tsconfig.json +++ b/functions/tsconfig.json @@ -9,7 +9,5 @@ "target": "es2017" }, "compileOnSave": true, - "include": [ - "src" - ] + "include": ["src", "../common/**/*.ts"] } diff --git a/web/.prettierrc b/web/.prettierrc deleted file mode 100644 index bd18729f..00000000 --- a/web/.prettierrc +++ /dev/null @@ -1,7 +0,0 @@ -{ - "tabWidth": 2, - "useTabs": false, - "semi": false, - "trailingComma": "es5", - "singleQuote": true -} diff --git a/web/components/add-funds-button.tsx b/web/components/add-funds-button.tsx index 3c70604e..5fecde73 100644 --- a/web/components/add-funds-button.tsx +++ b/web/components/add-funds-button.tsx @@ -18,7 +18,7 @@ export function AddFundsButton(props: { className?: string }) {