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?: { sale?: {
amount: number // amount user makes from sale amount: number // amount user makes from sale
betId: string // id of bet being sold betId: string // id of bet being sold
// TODO: add sale time?
} }
isSold?: boolean // true if this BUY bet has been sold isSold?: boolean // true if this BUY bet has been sold

View File

@ -1,9 +1,10 @@
import { Bet } from './firebase/bets' import { Bet } from './bet'
import { Contract } from './firebase/contracts' 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 [yesPool, noPool] = [pool.YES, pool.NO]
const numerator = Math.pow(yesPool, 2) const numerator = Math.pow(yesPool, 2)
const denominator = Math.pow(yesPool, 2) + Math.pow(noPool, 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 total = totalShares[outcome] - totalBets[outcome]
const winningsPool = truePool - 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) { export function calculatePayoutAfterCorrectBet(contract: Contract, bet: Bet) {
@ -78,7 +79,7 @@ export function calculatePayoutAfterCorrectBet(contract: Contract, bet: Bet) {
const total = totalSharesOutcome - totalBetsOutcome const total = totalSharesOutcome - totalBetsOutcome
const winningsPool = truePool - 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) { function calculateMktPayout(contract: Contract, bet: Bet) {
@ -103,7 +104,7 @@ function calculateMktPayout(contract: Contract, bet: Bet) {
(1 - p) * (contract.totalShares.NO - contract.totalBets.NO) (1 - p) * (contract.totalShares.NO - contract.totalBets.NO)
return ( return (
(1 - fees) * (1 - FEES) *
(betP * bet.amount + (betP * bet.amount +
((betP * (bet.shares - bet.amount)) / weightedShareTotal) * winningsPool) ((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 adjShareValue = Math.min(Math.min(1, f) * shareValue, myPool)
const saleAmount = (1 - fees) * adjShareValue const saleAmount = (1 - FEES) * adjShareValue
return saleAmount 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

@ -7,4 +7,4 @@ export type User = {
balance: number balance: number
createdTime: number createdTime: number
lastUpdatedTime: number lastUpdatedTime: number
} }

View File

@ -1,4 +1,8 @@
export const slugify = (text: any, separator = '-'): string => { export const slugify = (
text: string,
separator = '-',
maxLength = 35
): string => {
return text return text
.toString() .toString()
.normalize('NFD') // split an accented letter in the base letter and the acent .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() .trim()
.replace(/[^a-z0-9 ]/g, '') // remove all chars not letters, numbers and spaces (to be replaced) .replace(/[^a-z0-9 ]/g, '') // remove all chars not letters, numbers and spaces (to be replaced)
.replace(/\s+/g, separator) .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 functions from 'firebase-functions'
import * as admin from 'firebase-admin' import * as admin from 'firebase-admin'
import { randomString } from './util/random-string' import { chargeUser, getUser } from './utils'
import { slugify } from './util/slugify' import { Contract } from '../../common/contract'
import { Contract } from './types/contract' import { slugify } from '../../common/util/slugify'
import { getUser } from './utils' import { randomString } from '../../common/util/random-string'
import { payUser } from '.' import { getNewContract } from '../../common/new-contract'
import { User } from './types/user'
export const createContract = functions export const createContract = functions
.runWith({ minInstances: 1 }) .runWith({ minInstances: 1 })
@ -62,7 +61,7 @@ export const createContract = functions
closeTime closeTime
) )
if (ante) await payUser([creator.id, -ante]) if (ante) await chargeUser(creator.id, ante)
await contractRef.create(contract) await contractRef.create(contract)
return { status: 'success', contract } return { status: 'success', contract }
@ -70,7 +69,7 @@ export const createContract = functions
) )
const getSlug = async (question: string) => { const getSlug = async (question: string) => {
const proposedSlug = slugify(question).substring(0, 35) const proposedSlug = slugify(question)
const preexistingContract = await getContractFromSlug(proposedSlug) const preexistingContract = await getContractFromSlug(proposedSlug)
@ -79,73 +78,6 @@ const getSlug = async (question: string) => {
: proposedSlug : 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() const firestore = admin.firestore()
export async function getContractFromSlug(slug: string) { export async function getContractFromSlug(slug: string) {

View File

@ -1,8 +1,17 @@
import { sendEmail } from './send-email' import { Contract } from '../../common/contract'
import { Contract } from './types/contract' import { User } from '../../common/user'
import { User } from './types/user' import { sendTemplateEmail } from './send-email'
import { getUser } from './utils' import { getUser } from './utils'
type market_resolved_template = {
name: string
creatorName: string
question: string
outcome: string
payout: string
url: string
}
export const sendMarketResolutionEmail = async ( export const sendMarketResolutionEmail = async (
userId: string, userId: string,
payout: number, payout: number,
@ -13,22 +22,24 @@ export const sendMarketResolutionEmail = async (
const user = await getUser(userId) const user = await getUser(userId)
if (!user) return 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} // Modify template here:
Question: ${contract.question} // https://app.mailgun.com/app/sending/domains/mg.manifold.markets/templates/edit/market-resolved/initial
Resolution: ${toDisplayResolution[resolution]} // Mailgun username: james@mantic.markets
Your payout is M$ ${Math.round(payout)} await sendTemplateEmail(user.email, subject, 'market-resolved', templateData)
View the market here:
https://manifold.markets/${creator.username}/${contract.slug}
`
await sendEmail(user.email, subject, body)
} }
const toDisplayResolution = { YES: 'YES', NO: 'NO', CANCEL: 'N/A', MKT: 'MKT' } 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 functions from 'firebase-functions'
import * as admin from 'firebase-admin' import * as admin from 'firebase-admin'
import { Contract } from './types/contract' import { Contract } from '../../common/contract'
import { User } from './types/user' import { User } from '../../common/user'
import { Bet } from './types/bet' import { getNewBetInfo } from '../../common/new-bet'
export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall( export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
async ( async (
@ -42,6 +42,10 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
return { status: 'error', message: 'Invalid contract' } return { status: 'error', message: 'Invalid contract' }
const contract = contractSnap.data() as 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 const newBetDoc = firestore
.collection(`contracts/${contractId}/bets`) .collection(`contracts/${contractId}/bets`)
.doc() .doc()
@ -63,56 +67,3 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
) )
const firestore = admin.firestore() 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 admin from 'firebase-admin'
import * as _ from 'lodash' import * as _ from 'lodash'
import { Contract } from './types/contract' import { Contract } from '../../common/contract'
import { User } from './types/user' import { User } from '../../common/user'
import { Bet } from './types/bet' import { Bet } from '../../common/bet'
import { getUser } from './utils' import { getUser, payUser } from './utils'
import { sendMarketResolutionEmail } from './emails' import { sendMarketResolutionEmail } from './emails'
import {
export const PLATFORM_FEE = 0.01 // 1% getCancelPayouts,
export const CREATOR_FEE = 0.01 // 1% getMktPayouts,
getStandardPayouts,
} from '../../common/payouts'
export const resolveMarket = functions export const resolveMarket = functions
.runWith({ minInstances: 1 }) .runWith({ minInstances: 1 })
@ -76,7 +78,9 @@ export const resolveMarket = functions
_.sumBy(group, (g) => g.payout) _.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) const result = await Promise.all(payoutPromises)
.catch((e) => ({ status: 'error', message: e })) .catch((e) => ({ status: 'error', message: e }))
@ -117,121 +121,3 @@ const sendResolutionEmails = async (
} }
const firestore = admin.firestore() 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 admin from 'firebase-admin'
import * as _ from 'lodash' 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 type DocRef = admin.firestore.DocumentReference

View File

@ -1,7 +1,8 @@
import * as admin from 'firebase-admin' import * as admin from 'firebase-admin'
import * as _ from 'lodash' 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 type DocRef = admin.firestore.DocumentReference

View File

@ -1,6 +1,7 @@
import * as admin from 'firebase-admin' import * as admin from 'firebase-admin'
import * as _ from 'lodash' import * as _ from 'lodash'
import { Contract } from '../types/contract'
import { Contract } from '../../../common/contract'
import { getValues } from '../utils' import { getValues } from '../utils'
// Generate your own private key, and set the path below: // Generate your own private key, and set the path below:

View File

@ -1,10 +1,10 @@
import * as admin from 'firebase-admin' import * as admin from 'firebase-admin'
import * as functions from 'firebase-functions' import * as functions from 'firebase-functions'
import { CREATOR_FEE, PLATFORM_FEE } from './resolve-market' import { Contract } from '../../common/contract'
import { Bet } from './types/bet' import { User } from '../../common/user'
import { Contract } from './types/contract' import { Bet } from '../../common/bet'
import { User } from './types/user' import { getSellBetInfo } from '../../common/sell-bet'
export const sellBet = functions.runWith({ minInstances: 1 }).https.onCall( export const sellBet = functions.runWith({ minInstances: 1 }).https.onCall(
async ( async (
@ -33,6 +33,10 @@ export const sellBet = functions.runWith({ minInstances: 1 }).https.onCall(
return { status: 'error', message: 'Invalid contract' } return { status: 'error', message: 'Invalid contract' }
const contract = contractSnap.data() as 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 betDoc = firestore.doc(`contracts/${contractId}/bets/${betId}`)
const betSnap = await transaction.get(betDoc) const betSnap = await transaction.get(betDoc)
if (!betSnap.exists) return { status: 'error', message: 'Invalid bet' } 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 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 DOMAIN = 'mg.manifold.markets'
const mg = mailgun({ apiKey: functions.config().mailgun.key, domain: DOMAIN }) 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 = { const data = {
from: 'Manifold Markets <no-reply@manifold.markets>', from: 'Manifold Markets <no-reply@manifold.markets>',
to, to,
@ -12,7 +12,27 @@ export const sendEmail = (to: string, subject: string, text: string) => {
text, text,
} }
return mg.messages().send(data, (error, body) => { return mg.messages().send(data, (error) => {
console.log('Sent email', error, body) 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 * as admin from 'firebase-admin'
import Stripe from 'stripe' import Stripe from 'stripe'
import { payUser } from './resolve-market' import { payUser } from './utils'
const stripe = new Stripe(functions.config().stripe.apikey, { const stripe = new Stripe(functions.config().stripe.apikey, {
apiVersion: '2020-08-27', apiVersion: '2020-08-27',
@ -118,7 +118,7 @@ const issueMoneys = async (session: any) => {
session, session,
}) })
await payUser([userId, payout]) await payUser(userId, payout)
console.log('user', userId, 'paid M$', payout) console.log('user', userId, 'paid M$', payout)
} }

View File

@ -1,9 +1,10 @@
import * as functions from 'firebase-functions' import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin' import * as admin from 'firebase-admin'
import * as _ from 'lodash' import * as _ from 'lodash'
import { Contract } from './types/contract'
import { getValues } from './utils' import { getValues } from './utils'
import { Bet } from './types/bet' import { Contract } from '../../common/contract'
import { Bet } from '../../common/bet'
const firestore = admin.firestore() const firestore = admin.firestore()

View File

@ -1,7 +1,7 @@
import * as admin from 'firebase-admin' import * as admin from 'firebase-admin'
import { Contract } from './types/contract' import { Contract } from '../../common/contract'
import { User } from './types/user' import { User } from '../../common/user'
export const getValue = async <T>(collection: string, doc: string) => { export const getValue = async <T>(collection: string, doc: string) => {
const snap = await admin.firestore().collection(collection).doc(doc).get() const snap = await admin.firestore().collection(collection).doc(doc).get()
@ -21,3 +21,37 @@ export const getContract = (contractId: string) => {
export const getUser = (userId: string) => { export const getUser = (userId: string) => {
return getValue<User>('users', userId) 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" "target": "es2017"
}, },
"compileOnSave": true, "compileOnSave": true,
"include": [ "include": ["src", "../common/**/*.ts"]
"src"
]
} }

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 <label
htmlFor="add-funds" htmlFor="add-funds"
className={clsx( 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 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 React, { useEffect, useState } from 'react'
import { useUser } from '../hooks/use-user' import { useUser } from '../hooks/use-user'
import { Contract } from '../lib/firebase/contracts' import { Contract } from '../../common/contract'
import { Col } from './layout/col' import { Col } from './layout/col'
import { Row } from './layout/row' import { Row } from './layout/row'
import { Spacer } from './layout/spacer' import { Spacer } from './layout/spacer'
@ -18,13 +18,13 @@ import {
calculateShares, calculateShares,
getProbabilityAfterBet, getProbabilityAfterBet,
calculatePayoutAfterCorrectBet, calculatePayoutAfterCorrectBet,
} from '../lib/calculate' } from '../../common/calculate'
import { firebaseLogin } from '../lib/firebase/users' import { firebaseLogin } from '../lib/firebase/users'
import { AddFundsButton } from './add-funds-button'
import { OutcomeLabel } from './outcome-label' import { OutcomeLabel } from './outcome-label'
import { AdvancedPanel } from './advanced-panel' import { AdvancedPanel } from './advanced-panel'
import { Bet } from '../lib/firebase/bets' import { Bet } from '../../common/bet'
import { placeBet } from '../lib/firebase/api-call' import { placeBet } from '../lib/firebase/api-call'
import { AmountInput } from './amount-input'
export function BetPanel(props: { contract: Contract; className?: string }) { export function BetPanel(props: { contract: Contract; className?: string }) {
useEffect(() => { useEffect(() => {
@ -48,17 +48,9 @@ export function BetPanel(props: { contract: Contract; className?: string }) {
setWasSubmitted(false) setWasSubmitted(false)
} }
function onBetChange(str: string) { function onBetChange(newAmount: number | undefined) {
setWasSubmitted(false) setWasSubmitted(false)
setBetAmount(newAmount)
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)
} }
async function submitBet() { async function submitBet() {
@ -106,8 +98,6 @@ export function BetPanel(props: { contract: Contract; className?: string }) {
: 0 : 0
const estimatedReturnPercent = (estimatedReturn * 100).toFixed() + '%' const estimatedReturnPercent = (estimatedReturn * 100).toFixed() + '%'
const remainingBalance = (user?.balance || 0) - (betAmount || 0)
return ( return (
<Col <Col
className={clsx('bg-gray-100 shadow-md px-8 py-6 rounded-md', className)} 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)} onSelect={(choice) => onBetChoice(choice)}
/> />
<div className="mt-3 mb-1 text-sm text-gray-500"> <div className="my-3 text-sm text-gray-500">Amount </div>
Amount{' '} <AmountInput
{user && ( inputClassName="w-full"
<span className="float-right"> amount={betAmount}
{formatMoney( onChange={onBetChange}
remainingBalance > 0 ? Math.floor(remainingBalance) : 0 error={error}
)}{' '} setError={setError}
left disabled={isSubmitting}
</span> />
)}
</div> <Spacer h={4} />
<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="mt-2 mb-1 text-sm text-gray-500">Implied probability</div> <div className="mt-2 mb-1 text-sm text-gray-500">Implied probability</div>
<Row> <Row>
@ -214,7 +182,7 @@ export function BetPanel(props: { contract: Contract; className?: string }) {
</button> </button>
) : ( ) : (
<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} onClick={firebaseLogin}
> >
Sign in to trade! Sign in to trade!

View File

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

View File

@ -55,7 +55,7 @@ function FeedComment(props: { activityItem: any }) {
<a href={person.href} className="font-medium text-gray-900"> <a href={person.href} className="font-medium text-gray-900">
{person.name} {person.name}
</a>{' '} </a>{' '}
placed M$ {amount} on <OutcomeLabel outcome={outcome} />{' '} placed {formatMoney(amount)} on <OutcomeLabel outcome={outcome} />{' '}
<Timestamp time={createdTime} /> <Timestamp time={createdTime} />
</p> </p>
</div> </div>
@ -326,7 +326,7 @@ function toFeedBet(bet: Bet) {
contractId: bet.contractId, contractId: bet.contractId,
userId: bet.userId, userId: bet.userId,
type: 'bet', type: 'bet',
amount: bet.amount, amount: bet.sale ? -bet.sale.amount : bet.amount,
outcome: bet.outcome, outcome: bet.outcome,
createdTime: bet.createdTime, createdTime: bet.createdTime,
date: dayjs(bet.createdTime).fromNow(), date: dayjs(bet.createdTime).fromNow(),
@ -339,7 +339,7 @@ function toFeedComment(bet: Bet, comment: Comment) {
contractId: bet.contractId, contractId: bet.contractId,
userId: bet.userId, userId: bet.userId,
type: 'comment', type: 'comment',
amount: bet.amount, amount: bet.sale ? -bet.sale.amount : bet.amount,
outcome: bet.outcome, outcome: bet.outcome,
createdTime: bet.createdTime, createdTime: bet.createdTime,
date: dayjs(bet.createdTime).fromNow(), 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 { useUser } from '../hooks/use-user'
import { Row } from './layout/row' import { Row } from './layout/row'
import { firebaseLogin, User } from '../lib/firebase/users' import { firebaseLogin, User } from '../lib/firebase/users'
import { ManticLogo } from './mantic-logo' import { ManifoldLogo } from './manifold-logo'
import { ProfileMenu } from './profile-menu' import { ProfileMenu } from './profile-menu'
export function NavBar(props: { export function NavBar(props: {
darkBackground?: boolean darkBackground?: boolean
wide?: boolean wide?: boolean
isLandingPage?: boolean
className?: string className?: string
children?: any children?: any
}) { }) {
const { darkBackground, wide, className, children } = props const { darkBackground, wide, isLandingPage, className, children } = props
const user = useUser() const user = useUser()
@ -26,10 +27,10 @@ export function NavBar(props: {
<Row <Row
className={clsx( className={clsx(
'justify-between items-center mx-auto sm:px-4', '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"> <Row className="items-center gap-6 sm:gap-8 md:ml-16 lg:ml-40">
{children} {children}

View File

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

View File

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

View File

@ -6,30 +6,10 @@ import {
where, where,
} from 'firebase/firestore' } from 'firebase/firestore'
import _ from 'lodash' import _ from 'lodash'
import { db } from './init' import { db } from './init'
import { Bet } from '../../../common/bet'
export type Bet = { 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
}
function getBetsCollection(contractId: string) { function getBetsCollection(contractId: string) {
return collection(db, 'contracts', contractId, 'bets') return collection(db, 'contracts', contractId, 'bets')

View File

@ -9,22 +9,11 @@ import {
where, where,
orderBy, orderBy,
} from 'firebase/firestore' } from 'firebase/firestore'
import { db } from './init'
import { User } from './users'
import { listenForValues } from './utils' import { listenForValues } from './utils'
import { db } from './init'
// Currently, comments are created after the bet, not atomically with the bet. import { User } from '../../../common/user'
// They're uniquely identified by the pair contractId/betId. import { Comment } from '../../../common/comment'
export type Comment = { export type { Comment }
contractId: string
betId: string
text: string
createdTime: number
// Denormalized, for rendering comments
userName?: string
userUsername?: string
userAvatarUrl?: string
}
export async function createComment( export async function createComment(
contractId: string, contractId: string,

View File

@ -1,4 +1,4 @@
import { app } from './init' import dayjs from 'dayjs'
import { import {
getFirestore, getFirestore,
doc, doc,
@ -14,38 +14,11 @@ import {
updateDoc, updateDoc,
limit, limit,
} from 'firebase/firestore' } from 'firebase/firestore'
import dayjs from 'dayjs'
import { app } from './init'
import { getValues, listenForValues } from './utils' import { getValues, listenForValues } from './utils'
import { Contract } from '../../../common/contract'
export type Contract = { 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
}
export function path(contract: Contract) { export function path(contract: Contract) {
// For now, derive username from creatorName // For now, derive username from creatorName

View File

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

View File

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

View File

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

View File

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

View File

@ -222,20 +222,16 @@ function Contents() {
<h3 id="how-does-betting-work">How does betting work?</h3> <h3 id="how-does-betting-work">How does betting work?</h3>
<ul> <ul>
<li>Markets are structured around a question with a binary outcome.</li>
<li> <li>
Markets are structured around a question with a binary outcome (either Traders can place a bet on either YES or NO. The trader receives some
YES or NO) shares of the betting pool. The number of shares depends on the
</li> current probability.
<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.
</li> </li>
<li> <li>
When the market is resolved, the traders who bet on the correct 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 outcome are paid out of the final pool in proportion to the number of
proportion to the amount of shares they own, minus any fees. shares they own.
</li> </li>
</ul> </ul>
@ -286,36 +282,22 @@ function Contents() {
Office hours:{' '} Office hours:{' '}
<a href="https://calendly.com/austinchen/mantic">Calendly</a> <a href="https://calendly.com/austinchen/mantic">Calendly</a>
</li> </li>
<li>Discord:</li> <li>
Chat:{' '}
<a href="https://discord.gg/eHQBNBqXuh">
Manifold Markets Discord server
</a>
</li>
</ul> </ul>
<p> <p></p>
<a href="https://discord.gg/eHQBNBqXuh">
Join the Manifold Markets Discord Server!
</a>
</p>
<h1 id="further-reading">Further Reading</h1> <h1 id="further-reading">Further Reading</h1>
<hr /> <hr />
<ul> <ul>
<li> <li>
<a href="https://manifoldmarkets.notion.site/Technical-Overview-b9b48a09ea1f45b88d991231171730c5"> <a href="https://manifoldmarkets.notion.site/Technical-Guide-to-Manifold-Markets-b9b48a09ea1f45b88d991231171730c5">
Technical Overview of Manifold Markets Technical Guide to 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> </a>
</li> </li>
<li> <li>
@ -323,11 +305,6 @@ function Contents() {
Paul Christiano: Prediction markets for internet points Paul Christiano: Prediction markets for internet points
</a> </a>
</li> </li>
<li>
<a href="https://manifold.markets/simulator">
Dynamic parimutuel market simulator
</a>
</li>
<li> <li>
<a href="https://thezvi.wordpress.com/2021/12/02/covid-prediction-markets-at-polymarket/"> <a href="https://thezvi.wordpress.com/2021/12/02/covid-prediction-markets-at-polymarket/">
Zvi Mowshowitz on resolving prediction markets 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="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"> <div className="p-4">
<img <img
src="/logo-icon-white-bg.png" src="/logo-bg-white.png"
className="rounded-lg shadow-lg w-20 h-20" className="rounded-lg shadow-lg w-20 h-20"
/> />
</div> </div>

View File

@ -10,10 +10,10 @@ import { Title } from '../components/title'
import { useUser } from '../hooks/use-user' import { useUser } from '../hooks/use-user'
import { Contract, path } from '../lib/firebase/contracts' import { Contract, path } from '../lib/firebase/contracts'
import { Page } from '../components/page' import { Page } from '../components/page'
import { formatMoney } from '../lib/util/format'
import { AdvancedPanel } from '../components/advanced-panel' import { AdvancedPanel } from '../components/advanced-panel'
import { createContract } from '../lib/firebase/api-call' import { createContract } from '../lib/firebase/api-call'
import { Row } from '../components/layout/row' import { Row } from '../components/layout/row'
import { AmountInput } from '../components/amount-input'
// Allow user to create a new contract // Allow user to create a new contract
export default function NewContract() { export default function NewContract() {
@ -33,7 +33,7 @@ export default function NewContract() {
const [description, setDescription] = useState('') const [description, setDescription] = useState('')
const [ante, setAnte] = useState<number | undefined>(0) const [ante, setAnte] = useState<number | undefined>(0)
const [anteError, setAnteError] = useState('') const [anteError, setAnteError] = useState<string | undefined>()
const [closeDate, setCloseDate] = useState('') const [closeDate, setCloseDate] = useState('')
const [isSubmitting, setIsSubmitting] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false)
@ -75,17 +75,6 @@ export default function NewContract() {
await router.push(path(result.contract as Contract)) 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)...` 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 <></> if (!creator) return <></>
@ -166,33 +155,19 @@ export default function NewContract() {
<label className="label"> <label className="label">
<span className="mb-1">Subsidize your market</span> <span className="mb-1">Subsidize your market</span>
</label> </label>
<AmountInput
<label className="input-group"> className="items-start"
<span className="text-sm ">M$</span> amount={ante}
<input onChange={setAnte}
className={clsx( error={anteError}
'input input-bordered', setError={setAnteError}
anteError && 'input-error' disabled={isSubmitting}
)} />
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>
</div> </div>
<Spacer h={4} /> <Spacer h={4} />
<div className="form-control"> <div className="form-control items-start mb-1">
<label className="label"> <label className="label">
<span className="mb-1">Close date</span> <span className="mb-1">Close date</span>
</label> </label>
@ -208,8 +183,8 @@ export default function NewContract() {
</div> </div>
<label> <label>
<span className="label-text text-gray-500 ml-1"> <span className="label-text text-gray-500 ml-1">
No new trades will be allowed after{' '} No trades allowed after this date
{closeDate ? formattedCloseTime : 'this time'} {/* {closeDate ? formattedCloseTime : 'this date'} */}
</span> </span>
</label> </label>
</AdvancedPanel> </AdvancedPanel>

View File

@ -34,7 +34,7 @@ const scrollToAbout = () => {
function Hero() { function Hero() {
return ( return (
<div className="overflow-hidden h-screen bg-world-trading bg-cover bg-gray-900 bg-center lg:bg-left"> <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 <div
className="text-base font-medium text-white ml-8 cursor-pointer hover:underline hover:decoration-teal-500 hover:decoration-2" className="text-base font-medium text-white ml-8 cursor-pointer hover:underline hover:decoration-teal-500 hover:decoration-2"
onClick={scrollToAbout} 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", "jsx": "preserve",
"incremental": true "incremental": true
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "../common/**/*.ts"],
"exclude": ["node_modules"] "exclude": ["node_modules"]
} }