Merge branch 'main' into activity-feed

This commit is contained in:
Austin Chen 2022-01-11 11:37:42 -05:00
commit f500cafa46
60 changed files with 733 additions and 676 deletions

19
common/antes.ts Normal file
View File

@ -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 }
}

View File

@ -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

View File

@ -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
}

12
common/comment.ts Normal file
View File

@ -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
}

4
common/fees.ts Normal file
View File

@ -0,0 +1,4 @@
export const PLATFORM_FEE = 0.01 // == 1%
export const CREATOR_FEE = 0.01
export const FEES = PLATFORM_FEE + CREATOR_FEE

56
common/new-bet.ts Normal file
View File

@ -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 }
}

48
common/new-contract.ts Normal file
View File

@ -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
}

128
common/payouts.ts Normal file
View File

@ -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 = <T>(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 = <T>(array: T[], f: (t: T) => number) => {
const values = array.map(f)
return values.reduce((prev, cur) => prev + cur, 0)
}

108
common/sell-bet.ts Normal file
View File

@ -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,
}
}

View File

@ -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
}

View File

@ -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) {

View File

@ -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' }

View File

@ -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 }
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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,
}
}

View File

@ -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 <no-reply@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<string, string>
) => {
const data = {
from: 'Manifold Markets <no-reply@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)
})
}

View File

@ -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)
}

View File

@ -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()

View File

@ -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 <T>(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<User>('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)
}

View File

@ -9,7 +9,5 @@
"target": "es2017"
},
"compileOnSave": true,
"include": [
"src"
]
"include": ["src", "../common/**/*.ts"]
}

View File

@ -1,7 +0,0 @@
{
"tabWidth": 2,
"useTabs": false,
"semi": false,
"trailingComma": "es5",
"singleQuote": true
}

View File

@ -18,7 +18,7 @@ export function AddFundsButton(props: { className?: string }) {
<label
htmlFor="add-funds"
className={clsx(
'btn btn-sm normal-case modal-button bg-gradient-to-r from-teal-500 to-green-500 hover:from-teal-600 hover:to-green-600 font-normal border-none',
'btn btn-xs normal-case modal-button bg-gradient-to-r from-teal-500 to-green-500 hover:from-teal-600 hover:to-green-600 font-normal border-none',
className
)}
>

View File

@ -0,0 +1,79 @@
import clsx from 'clsx'
import { useUser } from '../hooks/use-user'
import { formatMoney } from '../lib/util/format'
import { AddFundsButton } from './add-funds-button'
import { Col } from './layout/col'
import { Row } from './layout/row'
export function AmountInput(props: {
amount: number | undefined
onChange: (newAmount: number | undefined) => void
error: string | undefined
setError: (error: string | undefined) => void
disabled?: boolean
className?: string
inputClassName?: string
}) {
const {
amount,
onChange,
error,
setError,
disabled,
className,
inputClassName,
} = props
const user = useUser()
const onAmountChange = (str: string) => {
const amount = parseInt(str.replace(/[^\d]/, ''))
if (str && isNaN(amount)) return
onChange(str ? amount : undefined)
if (user && user.balance < amount) setError('Insufficient balance')
else setError(undefined)
}
const remainingBalance = (user?.balance ?? 0) - (amount ?? 0)
return (
<Col className={className}>
<label className="input-group">
<span className="text-sm bg-gray-200">M$</span>
<input
className={clsx(
'input input-bordered',
error && 'input-error',
inputClassName
)}
type="text"
placeholder="0"
maxLength={9}
value={amount ?? ''}
disabled={disabled}
onChange={(e) => onAmountChange(e.target.value)}
/>
</label>
{user && (
<Row className="text-sm text-gray-500 justify-between mt-3 gap-4 items-end">
{error ? (
<div className="font-medium tracking-wide text-red-500 text-xs whitespace-nowrap mr-auto self-center">
{error}
</div>
) : (
<Col>
<div className="whitespace-nowrap">Remaining balance</div>
<div className="text-neutral mt-1">
{formatMoney(Math.floor(remainingBalance))}
</div>
</Col>
)}
{user.balance !== 1000 && <AddFundsButton className="mt-1" />}
</Row>
)}
</Col>
)
}

View File

@ -2,7 +2,7 @@ import clsx from 'clsx'
import React, { useEffect, useState } from 'react'
import { useUser } from '../hooks/use-user'
import { Contract } from '../lib/firebase/contracts'
import { Contract } from '../../common/contract'
import { Col } from './layout/col'
import { Row } from './layout/row'
import { Spacer } from './layout/spacer'
@ -18,13 +18,13 @@ import {
calculateShares,
getProbabilityAfterBet,
calculatePayoutAfterCorrectBet,
} from '../lib/calculate'
} from '../../common/calculate'
import { firebaseLogin } from '../lib/firebase/users'
import { AddFundsButton } from './add-funds-button'
import { OutcomeLabel } from './outcome-label'
import { AdvancedPanel } from './advanced-panel'
import { Bet } from '../lib/firebase/bets'
import { Bet } from '../../common/bet'
import { placeBet } from '../lib/firebase/api-call'
import { AmountInput } from './amount-input'
export function BetPanel(props: { contract: Contract; className?: string }) {
useEffect(() => {
@ -48,17 +48,9 @@ export function BetPanel(props: { contract: Contract; className?: string }) {
setWasSubmitted(false)
}
function onBetChange(str: string) {
function onBetChange(newAmount: number | undefined) {
setWasSubmitted(false)
const amount = parseInt(str.replace(/[^\d]/, ''))
if (str && isNaN(amount)) return
setBetAmount(str ? amount : undefined)
if (user && user.balance < amount) setError('Insufficient balance')
else setError(undefined)
setBetAmount(newAmount)
}
async function submitBet() {
@ -106,8 +98,6 @@ export function BetPanel(props: { contract: Contract; className?: string }) {
: 0
const estimatedReturnPercent = (estimatedReturn * 100).toFixed() + '%'
const remainingBalance = (user?.balance || 0) - (betAmount || 0)
return (
<Col
className={clsx('bg-gray-100 shadow-md px-8 py-6 rounded-md', className)}
@ -121,39 +111,17 @@ export function BetPanel(props: { contract: Contract; className?: string }) {
onSelect={(choice) => onBetChoice(choice)}
/>
<div className="mt-3 mb-1 text-sm text-gray-500">
Amount{' '}
{user && (
<span className="float-right">
{formatMoney(
remainingBalance > 0 ? Math.floor(remainingBalance) : 0
)}{' '}
left
</span>
)}
</div>
<Col className="my-2">
<label className="input-group">
<span className="text-sm bg-gray-200">M$</span>
<input
className={clsx(
'input input-bordered w-full',
error && 'input-error'
)}
type="text"
placeholder="0"
maxLength={9}
value={betAmount ?? ''}
onChange={(e) => onBetChange(e.target.value)}
/>
</label>
{error && (
<div className="font-medium tracking-wide text-red-500 text-xs mt-3">
{error}
</div>
)}
{user && <AddFundsButton className="self-end mt-3" />}
</Col>
<div className="my-3 text-sm text-gray-500">Amount </div>
<AmountInput
inputClassName="w-full"
amount={betAmount}
onChange={onBetChange}
error={error}
setError={setError}
disabled={isSubmitting}
/>
<Spacer h={4} />
<div className="mt-2 mb-1 text-sm text-gray-500">Implied probability</div>
<Row>
@ -214,7 +182,7 @@ export function BetPanel(props: { contract: Contract; className?: string }) {
</button>
) : (
<button
className="btn mt-4 border-none normal-case text-lg font-medium px-10 bg-gradient-to-r from-teal-500 to-green-500 hover:from-teal-600 hover:to-green-600"
className="btn mt-4 border-none normal-case text-lg font-medium whitespace-nowrap px-10 bg-gradient-to-r from-teal-500 to-green-500 hover:from-teal-600 hover:to-green-600"
onClick={firebaseLogin}
>
Sign in to trade!

View File

@ -21,7 +21,7 @@ import {
calculatePayout,
calculateSaleAmount,
resolvedPayout,
} from '../lib/calculate'
} from '../../common/calculate'
import { sellBet } from '../lib/firebase/api-call'
import { ConfirmationButton } from './confirmation-button'
import { OutcomeLabel, YesLabel, NoLabel, MarketLabel } from './outcome-label'
@ -308,7 +308,8 @@ function BetRow(props: { bet: Bet; contract: Contract; sale?: Bet }) {
shares,
isSold,
} = bet
const { isResolved } = contract
const { isResolved, closeTime } = contract
const isClosed = closeTime && Date.now() > closeTime
return (
<tr>
@ -333,7 +334,7 @@ function BetRow(props: { bet: Bet; contract: Contract; sale?: Bet }) {
)}
</td>
{!isResolved && !isSold && (
{!isResolved && !isClosed && !isSold && (
<td className="text-neutral">
<SellButton contract={contract} bet={bet} />
</td>

View File

@ -55,7 +55,7 @@ function FeedComment(props: { activityItem: any }) {
<a href={person.href} className="font-medium text-gray-900">
{person.name}
</a>{' '}
placed M$ {amount} on <OutcomeLabel outcome={outcome} />{' '}
placed {formatMoney(amount)} on <OutcomeLabel outcome={outcome} />{' '}
<Timestamp time={createdTime} />
</p>
</div>
@ -326,7 +326,7 @@ function toFeedBet(bet: Bet) {
contractId: bet.contractId,
userId: bet.userId,
type: 'bet',
amount: bet.amount,
amount: bet.sale ? -bet.sale.amount : bet.amount,
outcome: bet.outcome,
createdTime: bet.createdTime,
date: dayjs(bet.createdTime).fromNow(),
@ -339,7 +339,7 @@ function toFeedComment(bet: Bet, comment: Comment) {
contractId: bet.contractId,
userId: bet.userId,
type: 'comment',
amount: bet.amount,
amount: bet.sale ? -bet.sale.amount : bet.amount,
outcome: bet.outcome,
createdTime: bet.createdTime,
date: dayjs(bet.createdTime).fromNow(),

View File

@ -0,0 +1,37 @@
import Link from 'next/link'
import clsx from 'clsx'
export function ManifoldLogo(props: { darkBackground?: boolean }) {
const { darkBackground } = props
return (
<Link href="/">
<a className="flex flex-row gap-4">
<img
className="hover:rotate-12 transition-all"
src={darkBackground ? '/logo-white.svg' : '/logo.svg'}
width={45}
height={45}
/>
<div
className={clsx(
'sm:hidden font-major-mono lowercase mt-1 text-lg',
darkBackground && 'text-white'
)}
>
Manifold
<br />
Markets
</div>
<div
className={clsx(
'hidden sm:flex font-major-mono lowercase mt-1 sm:text-2xl md:whitespace-nowrap',
darkBackground && 'text-white'
)}
>
Manifold Markets
</div>
</a>
</Link>
)
}

View File

@ -1,26 +0,0 @@
import Link from 'next/link'
import clsx from 'clsx'
export function ManticLogo(props: { darkBackground?: boolean }) {
const { darkBackground } = props
return (
<Link href="/">
<a className="flex flex-row gap-3">
<img
className="sm:h-10 sm:w-10 hover:rotate-12 transition-all"
src="/logo-icon.svg"
width={40}
height={40}
/>
<div
className={clsx(
'hidden sm:flex font-major-mono lowercase mt-1 sm:text-2xl md:whitespace-nowrap',
darkBackground && 'text-white'
)}
>
Manifold Markets
</div>
</a>
</Link>
)
}

View File

@ -4,16 +4,17 @@ import Link from 'next/link'
import { useUser } from '../hooks/use-user'
import { Row } from './layout/row'
import { firebaseLogin, User } from '../lib/firebase/users'
import { ManticLogo } from './mantic-logo'
import { ManifoldLogo } from './manifold-logo'
import { ProfileMenu } from './profile-menu'
export function NavBar(props: {
darkBackground?: boolean
wide?: boolean
isLandingPage?: boolean
className?: string
children?: any
}) {
const { darkBackground, wide, className, children } = props
const { darkBackground, wide, isLandingPage, className, children } = props
const user = useUser()
@ -26,10 +27,10 @@ export function NavBar(props: {
<Row
className={clsx(
'justify-between items-center mx-auto sm:px-4',
wide ? 'max-w-7xl' : 'max-w-4xl'
isLandingPage ? 'max-w-7xl' : wide ? 'max-w-6xl' : 'max-w-4xl'
)}
>
<ManticLogo darkBackground={darkBackground} />
<ManifoldLogo darkBackground={darkBackground} />
<Row className="items-center gap-6 sm:gap-8 md:ml-16 lg:ml-40">
{children}

View File

@ -11,7 +11,7 @@ export function Page(props: { wide?: boolean; children?: any }) {
<div
className={clsx(
'w-full px-4 pb-8 mx-auto',
wide ? 'max-w-7xl' : 'max-w-4xl'
wide ? 'max-w-6xl' : 'max-w-4xl'
)}
>
{children}

View File

@ -1,7 +1,7 @@
import Image from 'next/image'
import { firebaseLogout, User } from '../lib/firebase/users'
import { formatMoney } from '../lib/util/format'
import { Row } from './layout/row'
import { Col } from './layout/col'
import { MenuButton } from './menu'
export function ProfileMenu(props: { user: User }) {
@ -67,16 +67,16 @@ function getNavigationOptions(user: User, options: { mobile: boolean }) {
function ProfileSummary(props: { user: User }) {
const { user } = props
return (
<Row className="avatar items-center">
<div className="rounded-full w-10 h-10 mr-4">
<Col className="avatar items-center sm:flex-row gap-2 sm:gap-0">
<div className="rounded-full w-10 h-10 sm:mr-4">
<Image src={user.avatarUrl} width={40} height={40} />
</div>
<div className="truncate text-left" style={{ maxWidth: 170 }}>
{user.name}
<div className="hidden sm:flex">{user.name}</div>
<div className="text-gray-700 text-sm">
{formatMoney(Math.floor(user.balance))}
</div>
</div>
</Row>
</Col>
)
}

View File

@ -6,30 +6,10 @@ import {
where,
} from 'firebase/firestore'
import _ from 'lodash'
import { db } from './init'
export type Bet = {
id: string
userId: string
contractId: string
amount: number // bet size; negative if SELL bet
outcome: 'YES' | 'NO'
shares: number // dynamic parimutuel pool weight; negative if SELL bet
probBefore: number
probAfter: number
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
createdTime: number
}
import { Bet } from '../../../common/bet'
export type { Bet }
function getBetsCollection(contractId: string) {
return collection(db, 'contracts', contractId, 'bets')

View File

@ -9,22 +9,11 @@ import {
where,
orderBy,
} from 'firebase/firestore'
import { db } from './init'
import { User } from './users'
import { listenForValues } from './utils'
// 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
}
import { db } from './init'
import { User } from '../../../common/user'
import { Comment } from '../../../common/comment'
export type { Comment }
export async function createComment(
contractId: string,

View File

@ -1,4 +1,4 @@
import { app } from './init'
import dayjs from 'dayjs'
import {
getFirestore,
doc,
@ -14,38 +14,11 @@ import {
updateDoc,
limit,
} from 'firebase/firestore'
import dayjs from 'dayjs'
import { app } from './init'
import { getValues, listenForValues } from './utils'
export type Contract = {
id: string
slug: string // auto-generated; must be unique
creatorId: string
creatorName: string
creatorUsername: string
question: string
description: string // More info about what the contract is about
outcomeType: 'BINARY' // | 'MULTI' | 'interval' | 'date'
// outcomes: ['YES', 'NO']
startPool: { YES: number; NO: number }
pool: { YES: number; NO: number }
totalShares: { YES: number; NO: number }
totalBets: { YES: number; NO: number }
createdTime: number // Milliseconds since epoch
lastUpdatedTime: number // If the question or description was changed
closeTime?: number // When no more trading is allowed
isResolved: boolean
resolutionTime?: number // When the contract creator resolved the market
resolution?: 'YES' | 'NO' | 'CANCEL' // Chosen by creator; must be one of outcomes
volume24Hours: number
volume7Days: number
}
import { Contract } from '../../../common/contract'
export type { Contract }
export function path(contract: Contract) {
// For now, derive username from creatorName

View File

@ -19,18 +19,10 @@ import {
signInWithPopup,
} from 'firebase/auth'
export const STARTING_BALANCE = 1000
import { User } from '../../../common/user'
export type { User }
export type User = {
id: string
email: string
name: string
username: string
avatarUrl: string
balance: number
createdTime: number
lastUpdatedTime: number
}
export const STARTING_BALANCE = 1000
const db = getFirestore(app)
export const auth = getAuth(app)

View File

@ -1,6 +1,7 @@
/** @type {import('next').NextConfig} */
module.exports = {
reactStrictMode: true,
experimental: { externalDir: true },
images: {
domains: ['lh3.googleusercontent.com'],
},

View File

@ -56,7 +56,7 @@ export default function ContractPage(props: {
const isCreator = user?.id === creatorId
const allowTrade =
!isResolved && (!contract.closeTime || contract.closeTime > Date.now())
const allowResolve = !isResolved && isCreator && user
const allowResolve = !isResolved && isCreator && !!user
const { probPercent } = compute(contract)
@ -73,7 +73,7 @@ export default function ContractPage(props: {
}
return (
<Page wide={allowTrade}>
<Page wide={allowTrade || allowResolve}>
<SEO
title={question}
description={description}

View File

@ -35,7 +35,7 @@ function MyApp({ Component, pageProps }: AppProps) {
/>
<meta
name="twitter:image"
content="https://manifold.markets/logo-bg.png"
content="https://manifold.markets/logo-bg-white.png"
key="image2"
/>
</Head>

View File

@ -222,20 +222,16 @@ function Contents() {
<h3 id="how-does-betting-work">How does betting work?</h3>
<ul>
<li>Markets are structured around a question with a binary outcome.</li>
<li>
Markets are structured around a question with a binary outcome (either
YES or NO)
</li>
<li>
Traders can place a bet on either YES or NO. The bet amount is added
to the corresponding bet pool for the outcome. The trader receives
some shares of the final pool. The number of shares depends on the
current implied probability.
Traders can place a bet on either YES or NO. The trader receives some
shares of the betting pool. The number of shares depends on the
current probability.
</li>
<li>
When the market is resolved, the traders who bet on the correct
outcome are paid out of the total pool (YES pool + NO pool) in
proportion to the amount of shares they own, minus any fees.
outcome are paid out of the final pool in proportion to the number of
shares they own.
</li>
</ul>
@ -286,36 +282,22 @@ function Contents() {
Office hours:{' '}
<a href="https://calendly.com/austinchen/mantic">Calendly</a>
</li>
<li>Discord:</li>
<li>
Chat:{' '}
<a href="https://discord.gg/eHQBNBqXuh">
Manifold Markets Discord server
</a>
</li>
</ul>
<p>
<a href="https://discord.gg/eHQBNBqXuh">
Join the Manifold Markets Discord Server!
</a>
</p>
<p></p>
<h1 id="further-reading">Further Reading</h1>
<hr />
<ul>
<li>
<a href="https://manifoldmarkets.notion.site/Technical-Overview-b9b48a09ea1f45b88d991231171730c5">
Technical Overview of Manifold Markets
</a>
</li>
<li>
<a href="https://en.wikipedia.org/wiki/Prediction_market">
Wikipedia: Prediction markets
</a>
</li>
<li>
<a href="https://www.gwern.net/Prediction-markets">
Gwern: Prediction markets
</a>
</li>
<li>
<a href="https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.91.7441&amp;rep=rep1&amp;type=pdf">
David Pennock: Dynamic parimutuel markets
<a href="https://manifoldmarkets.notion.site/Technical-Guide-to-Manifold-Markets-b9b48a09ea1f45b88d991231171730c5">
Technical Guide to Manifold Markets
</a>
</li>
<li>
@ -323,11 +305,6 @@ function Contents() {
Paul Christiano: Prediction markets for internet points
</a>
</li>
<li>
<a href="https://manifold.markets/simulator">
Dynamic parimutuel market simulator
</a>
</li>
<li>
<a href="https://thezvi.wordpress.com/2021/12/02/covid-prediction-markets-at-polymarket/">
Zvi Mowshowitz on resolving prediction markets

View File

@ -9,7 +9,7 @@ function SignInCard() {
<div className="card glass sm:card-side shadow-xl hover:shadow-xl text-neutral-content bg-green-600 hover:bg-green-600 transition-all max-w-sm mx-4 sm:mx-auto my-12">
<div className="p-4">
<img
src="/logo-icon-white-bg.png"
src="/logo-bg-white.png"
className="rounded-lg shadow-lg w-20 h-20"
/>
</div>

View File

@ -10,10 +10,10 @@ import { Title } from '../components/title'
import { useUser } from '../hooks/use-user'
import { Contract, path } from '../lib/firebase/contracts'
import { Page } from '../components/page'
import { formatMoney } from '../lib/util/format'
import { AdvancedPanel } from '../components/advanced-panel'
import { createContract } from '../lib/firebase/api-call'
import { Row } from '../components/layout/row'
import { AmountInput } from '../components/amount-input'
// Allow user to create a new contract
export default function NewContract() {
@ -33,7 +33,7 @@ export default function NewContract() {
const [description, setDescription] = useState('')
const [ante, setAnte] = useState<number | undefined>(0)
const [anteError, setAnteError] = useState('')
const [anteError, setAnteError] = useState<string | undefined>()
const [closeDate, setCloseDate] = useState('')
const [isSubmitting, setIsSubmitting] = useState(false)
@ -75,17 +75,6 @@ export default function NewContract() {
await router.push(path(result.contract as Contract))
}
function onAnteChange(str: string) {
const amount = parseInt(str.replace(/[^\d]/, ''))
if (str && isNaN(amount)) return
setAnte(str ? amount : undefined)
if (user && user.balance < amount) setAnteError('Insufficient balance')
else setAnteError('')
}
const descriptionPlaceholder = `e.g. This market will resolve to “Yes” if, by June 2, 2021, 11:59:59 PM ET, Paxlovid (also known under PF-07321332)...`
if (!creator) return <></>
@ -166,33 +155,19 @@ export default function NewContract() {
<label className="label">
<span className="mb-1">Subsidize your market</span>
</label>
<label className="input-group">
<span className="text-sm ">M$</span>
<input
className={clsx(
'input input-bordered',
anteError && 'input-error'
)}
type="text"
placeholder="0"
maxLength={9}
value={ante ?? ''}
disabled={isSubmitting}
onChange={(e) => onAnteChange(e.target.value)}
/>
</label>
<label>
<span className="label-text text-gray-500 ml-1">
Remaining balance:{' '}
{formatMoney(remainingBalance > 0 ? remainingBalance : 0)}
</span>
</label>
<AmountInput
className="items-start"
amount={ante}
onChange={setAnte}
error={anteError}
setError={setAnteError}
disabled={isSubmitting}
/>
</div>
<Spacer h={4} />
<div className="form-control">
<div className="form-control items-start mb-1">
<label className="label">
<span className="mb-1">Close date</span>
</label>
@ -208,8 +183,8 @@ export default function NewContract() {
</div>
<label>
<span className="label-text text-gray-500 ml-1">
No new trades will be allowed after{' '}
{closeDate ? formattedCloseTime : 'this time'}
No trades allowed after this date
{/* {closeDate ? formattedCloseTime : 'this date'} */}
</span>
</label>
</AdvancedPanel>

View File

@ -34,7 +34,7 @@ const scrollToAbout = () => {
function Hero() {
return (
<div className="overflow-hidden h-screen bg-world-trading bg-cover bg-gray-900 bg-center lg:bg-left">
<NavBar wide darkBackground>
<NavBar isLandingPage darkBackground>
<div
className="text-base font-medium text-white ml-8 cursor-pointer hover:underline hover:decoration-teal-500 hover:decoration-2"
onClick={scrollToAbout}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<svg viewBox="0 0 69.218 70.506" xmlns="http://www.w3.org/2000/svg" fill="none">
<g data-v-fde0c5aa="" id="5c4df6fb-50cb-4581-b5fd-961d2467c672" stroke="none" fill="#11b981" transform="matrix(0.801904, 0, 0, 0.801904, 7.879115, 8.276814)">
<path d="M29.347 10.688A16.012 16.012 0 0 1 34 10c1.604 0 3.168.238 4.652.688C41.374 3.578 47.976 0 58 0a2 2 0 1 1 0 4c-8.574 0-13.645 2.759-15.688 8.325a16.03 16.03 0 0 1 4.399 3.956C49.892 17.358 52 21.399 52 26c0 4.672-2.173 8.766-5.437 9.767C43.11 40.917 37.801 45 34 45s-9.11-4.083-12.563-9.233C18.173 34.767 16 30.672 16 26c0-4.6 2.108-8.642 5.29-9.72a16.03 16.03 0 0 1 4.398-3.955C23.645 6.76 18.573 4 9.999 4a2 2 0 1 1 0-4c10.024 0 16.627 3.578 19.348 10.688zM34 41c1.894 0 5.359-2.493 8.068-5.87C39.58 33.547 38 29.984 38 26c0-3.898 1.513-7.394 3.91-9.026A11.966 11.966 0 0 0 34 14c-2.972 0-5.76 1.087-7.91 2.974C28.488 18.606 30 22.102 30 26c0 3.983-1.58 7.546-4.068 9.13C28.642 38.508 32.106 41 34 41zm-11-9c1.408 0 3-2.547 3-6s-1.592-6-3-6-3 2.547-3 6 1.592 6 3 6zm22 0c1.408 0 3-2.547 3-6s-1.592-6-3-6-3 2.547-3 6 1.592 6 3 6zM6.883 66.673a2 2 0 0 1-3.297.741C1.13 64.96 0 60.813 0 55c0-6.052 3.982-12.206 11.734-18.548a2 2 0 0 1 3.029.604l15 28a2 2 0 1 1-3.526 1.888l-8.755-16.342c-4.18 2.733-7.74 8.065-10.599 16.07zm5.526-25.54C6.75 46.174 4 50.818 4 55c0 2.566.243 4.666.7 6.304 2.934-6.756 6.547-11.518 10.887-14.24l-3.178-5.932zm48.708 25.54c-2.86-8.006-6.418-13.338-10.599-16.071l-8.755 16.342a2 2 0 1 1-3.526-1.888l15-28a2 2 0 0 1 3.03-.604C64.018 42.794 68 48.948 68 55c0 5.813-1.13 9.96-3.585 12.414a2 2 0 0 1-3.298-.741zm-5.526-25.54l-3.178 5.932c4.34 2.721 7.954 7.483 10.887 14.24.457-1.639.7-3.739.7-6.305 0-4.18-2.75-8.825-8.409-13.868z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<svg id="master-artboard" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500" width="500px" height="500px"><rect id="ee-background" x="0" y="0" width="500" height="500" style="fill: white; fill-opacity: 0; pointer-events: none;"/><g transform="matrix(0.4310344457626343, 0, 0, 0.4310344457626343, -49.559356689453125, 43.23752975463867)"><g transform="matrix(-1, 0, 0, 1, 1398.2724346748023, 1.4210854715202004e-14)"><g transform="matrix(-0.8166666626930236, 0, 0, 0.8166666626930236, 1188.272278324478, 1.4210854715202004e-14)"><path d="m1200 723.75c-1.875-7.5-9.375-13.125-16.875-13.125l-266.25-18.75 196.88-444.38c3.75-7.5 1.875-16.875-5.625-22.5s-15-5.625-22.5 0l-401.25 288.75-60-371.25c-1.875-7.5-7.5-15-16.875-15-7.5 0-16.875 3.75-18.75 11.25l-157.5 384.38-243.75-285h-1.875-1.875c-1.875-1.875-1.875-1.875-3.75-1.875h-1.875-5.625-1.875c-1.875 0-3.75 0-5.625 1.875h-1.875c-1.875 0-3.75 1.875-3.75 3.75l-157.5 170.62c-5.625 3.75-5.625 11.25-1.875 18.75 3.75 5.625 9.375 9.375 16.875 9.375h1.875l146.25-22.5 30 459.38v1.875c0 1.875 0 3.75 1.875 5.625 0 1.875 1.875 3.75 3.75 3.75l1.875 1.875c1.875 1.875 1.875 1.875 3.75 1.875h1.875l521.25 178.12c1.875 0 3.75 1.875 5.625 1.875h5.625 1.875c1.875 0 1.875-1.875 3.75-1.875l446.25-326.25c5.625-5.625 9.375-13.125 7.5-20.625zm-1134.4-328.12 91.875-99.375 5.625 84.375zm1063.1 348.75-7.5 5.625-341.25 249.38 65.625-148.12 54.375-123.75zm-605.62 217.5 146.25-161.25c7.5-7.5 5.625-18.75-1.875-26.25s-18.75-5.625-26.25 1.875l-157.5 174.38-230.62-78.75 796.88-575.62-324.38 735zm75-748.12 52.5 324.38-129.38 93.75-63.75-75zm-106.88 438.75-118.12 86.25-142.5 103.12-33.75-525v-11.25z" style="fill-opacity: 1; fill: rgb(255, 255, 255);"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 98 KiB

2
web/public/logo.svg Normal file
View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<svg id="master-artboard" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500" width="500px" height="500px"><rect id="ee-background" x="0" y="0" width="500" height="500" style="fill: white; fill-opacity: 0; pointer-events: none;"/><g transform="matrix(0.4310344457626343, 0, 0, 0.4310344457626343, -49.559356689453125, 43.23752975463867)"><g transform="matrix(-1, 0, 0, 1, 1398.2724346748023, 1.4210854715202004e-14)"><g transform="matrix(-0.8166666626930236, 0, 0, 0.8166666626930236, 1188.272278324478, 1.4210854715202004e-14)"><path d="m1200 723.75c-1.875-7.5-9.375-13.125-16.875-13.125l-266.25-18.75 196.88-444.38c3.75-7.5 1.875-16.875-5.625-22.5s-15-5.625-22.5 0l-401.25 288.75-60-371.25c-1.875-7.5-7.5-15-16.875-15-7.5 0-16.875 3.75-18.75 11.25l-157.5 384.38-243.75-285h-1.875-1.875c-1.875-1.875-1.875-1.875-3.75-1.875h-1.875-5.625-1.875c-1.875 0-3.75 0-5.625 1.875h-1.875c-1.875 0-3.75 1.875-3.75 3.75l-157.5 170.62c-5.625 3.75-5.625 11.25-1.875 18.75 3.75 5.625 9.375 9.375 16.875 9.375h1.875l146.25-22.5 30 459.38v1.875c0 1.875 0 3.75 1.875 5.625 0 1.875 1.875 3.75 3.75 3.75l1.875 1.875c1.875 1.875 1.875 1.875 3.75 1.875h1.875l521.25 178.12c1.875 0 3.75 1.875 5.625 1.875h5.625 1.875c1.875 0 1.875-1.875 3.75-1.875l446.25-326.25c5.625-5.625 9.375-13.125 7.5-20.625zm-1134.4-328.12 91.875-99.375 5.625 84.375zm1063.1 348.75-7.5 5.625-341.25 249.38 65.625-148.12 54.375-123.75zm-605.62 217.5 146.25-161.25c7.5-7.5 5.625-18.75-1.875-26.25s-18.75-5.625-26.25 1.875l-157.5 174.38-230.62-78.75 796.88-575.62-324.38 735zm75-748.12 52.5 324.38-129.38 93.75-63.75-75zm-106.88 438.75-118.12 86.25-142.5 103.12-33.75-525v-11.25z" style="fill-opacity: 1; fill: rgb(67, 55, 201);"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -15,6 +15,6 @@
"jsx": "preserve",
"incremental": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "../common/**/*.ts"],
"exclude": ["node_modules"]
}