refactor data structures, calculations to common directory

This commit is contained in:
mantikoros 2022-01-10 15:07:57 -06:00
parent dd6edc3b7a
commit b97a65cf2c
32 changed files with 306 additions and 352 deletions

22
common/bet.ts Normal file
View File

@ -0,0 +1,22 @@
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;
};

166
common/calculate.ts Normal file
View File

@ -0,0 +1,166 @@
import { Bet } from "./bet";
import { Contract } from "./contract";
import { FEES } from "./fees";
export const blah = () => 999;
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);
return numerator / denominator;
};
export function getProbabilityAfterBet(
pool: { YES: number; NO: number },
outcome: "YES" | "NO",
bet: number
) {
const [YES, NO] = [
pool.YES + (outcome === "YES" ? bet : 0),
pool.NO + (outcome === "NO" ? bet : 0),
];
return getProbability({ YES, NO });
}
export function calculateShares(
pool: { YES: number; NO: number },
bet: number,
betChoice: "YES" | "NO"
) {
const [yesPool, noPool] = [pool.YES, pool.NO];
return betChoice === "YES"
? bet + (bet * noPool ** 2) / (yesPool ** 2 + bet * yesPool)
: bet + (bet * yesPool ** 2) / (noPool ** 2 + bet * noPool);
}
export function calculatePayout(
contract: Contract,
bet: Bet,
outcome: "YES" | "NO" | "CANCEL" | "MKT"
) {
const { amount, outcome: betOutcome, shares } = bet;
if (outcome === "CANCEL") return amount;
if (outcome === "MKT") return calculateMktPayout(contract, bet);
if (betOutcome !== outcome) return 0;
const { totalShares, totalBets } = contract;
if (totalShares[outcome] === 0) return 0;
const startPool = contract.startPool.YES + contract.startPool.NO;
const truePool = contract.pool.YES + contract.pool.NO - startPool;
if (totalBets[outcome] >= truePool)
return (amount / totalBets[outcome]) * truePool;
const total = totalShares[outcome] - totalBets[outcome];
const winningsPool = truePool - totalBets[outcome];
return (1 - FEES) * (amount + ((shares - amount) / total) * winningsPool);
}
export function calculatePayoutAfterCorrectBet(contract: Contract, bet: Bet) {
const { amount, outcome, shares } = bet;
const { totalShares, totalBets } = contract;
const startPool = contract.startPool.YES + contract.startPool.NO;
const truePool = amount + contract.pool.YES + contract.pool.NO - startPool;
const totalBetsOutcome = totalBets[outcome] + amount;
const totalSharesOutcome = totalShares[outcome] + shares;
if (totalBetsOutcome >= truePool)
return (amount / totalBetsOutcome) * truePool;
const total = totalSharesOutcome - totalBetsOutcome;
const winningsPool = truePool - totalBetsOutcome;
return (1 - FEES) * (amount + ((shares - amount) / total) * winningsPool);
}
function calculateMktPayout(contract: Contract, bet: Bet) {
const p =
contract.pool.YES ** 2 / (contract.pool.YES ** 2 + contract.pool.NO ** 2);
const weightedTotal =
p * contract.totalBets.YES + (1 - p) * contract.totalBets.NO;
const startPool = contract.startPool.YES + contract.startPool.NO;
const truePool = contract.pool.YES + contract.pool.NO - startPool;
const betP = bet.outcome === "YES" ? p : 1 - p;
if (weightedTotal >= truePool) {
return ((betP * bet.amount) / weightedTotal) * truePool;
}
const winningsPool = truePool - weightedTotal;
const weightedShareTotal =
p * (contract.totalShares.YES - contract.totalBets.YES) +
(1 - p) * (contract.totalShares.NO - contract.totalBets.NO);
return (
(1 - FEES) *
(betP * bet.amount +
((betP * (bet.shares - bet.amount)) / weightedShareTotal) * winningsPool)
);
}
export function resolvedPayout(contract: Contract, bet: Bet) {
if (contract.resolution)
return calculatePayout(contract, bet, contract.resolution);
throw new Error("Contract was not resolved");
}
export function currentValue(contract: Contract, bet: Bet) {
const prob = getProbability(contract.pool);
const yesPayout = calculatePayout(contract, bet, "YES");
const noPayout = calculatePayout(contract, bet, "NO");
return prob * yesPayout + (1 - prob) * noPayout;
}
export function calculateSaleAmount(contract: Contract, bet: Bet) {
const { 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 [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 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;
};

29
common/contract.ts Normal file
View File

@ -0,0 +1,29 @@
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;
};

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;

10
common/user.ts Normal file
View File

@ -0,0 +1,10 @@
export type User = {
id: string;
email: string;
name: string;
username: string;
avatarUrl: string;
balance: number;
createdTime: number;
lastUpdatedTime: number;
};

View File

@ -1 +1 @@
export const randomString = () => Math.random().toString(16).substr(2, 14) export const randomString = () => Math.random().toString(16).substr(2, 14);

10
common/util/slugify.ts Normal file
View File

@ -0,0 +1,10 @@
export const slugify = (text: any, separator = "-"): string => {
return text
.toString()
.normalize("NFD") // split an accented letter in the base letter and the acent
.replace(/[\u0300-\u036f]/g, "") // remove all previously split accents
.toLowerCase()
.trim()
.replace(/[^a-z0-9 ]/g, "") // remove all chars not letters, numbers and spaces (to be replaced)
.replace(/\s+/g, separator);
};

View File

@ -1,12 +1,12 @@
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 { slugify } from './util/slugify'
import { Contract } from './types/contract'
import { getUser } from './utils' import { getUser } from './utils'
import { payUser } from '.' import { payUser } from '.'
import { User } from './types/user' import { Contract } from '../../common/contract'
import { User } from '../../common/user'
import { slugify } from '../../common/util/slugify'
import { randomString } from '../../common/util/random-string'
export const createContract = functions export const createContract = functions
.runWith({ minInstances: 1 }) .runWith({ minInstances: 1 })

View File

@ -1,6 +1,6 @@
import { sendEmail } from './send-email' import { sendEmail } from './send-email'
import { Contract } from './types/contract' import { Contract } from '../../common/contract'
import { User } from './types/user' import { User } from '../../common/user'
import { getUser } from './utils' import { getUser } from './utils'
export const sendMarketResolutionEmail = async ( export const sendMarketResolutionEmail = async (

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 { Bet } from '../../common/bet'
export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall( export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
async ( async (

View File

@ -2,15 +2,13 @@ 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 { CREATOR_FEE, PLATFORM_FEE } from '../../common/fees'
import { getUser } from './utils' import { getUser } from './utils'
import { sendMarketResolutionEmail } from './emails' import { sendMarketResolutionEmail } from './emails'
export const PLATFORM_FEE = 0.01 // 1%
export const CREATOR_FEE = 0.01 // 1%
export const resolveMarket = functions export const resolveMarket = functions
.runWith({ minInstances: 1 }) .runWith({ minInstances: 1 })
.https.onCall( .https.onCall(

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 { CREATOR_FEE, PLATFORM_FEE } from '../../common/fees'
import { Bet } from './types/bet' import { Contract } from '../../common/contract'
import { Contract } from './types/contract' import { User } from '../../common/user'
import { User } from './types/user' import { Bet } from '../../common/bet'
export const sellBet = functions.runWith({ minInstances: 1 }).https.onCall( export const sellBet = functions.runWith({ minInstances: 1 }).https.onCall(
async ( async (

View File

@ -1,21 +0,0 @@
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
}
isSold?: boolean // true if this BUY bet has been sold
createdTime: number
}

View File

@ -1,29 +0,0 @@
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
}

View File

@ -1,10 +0,0 @@
export type User = {
id: string
email: string
name: string
username: string
avatarUrl: string
balance: number
createdTime: number
lastUpdatedTime: number
}

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,10 +0,0 @@
export const slugify = (text: any, separator = '-'): string => {
return text
.toString()
.normalize('NFD') // split an accented letter in the base letter and the acent
.replace(/[\u0300-\u036f]/g, '') // remove all previously split accents
.toLowerCase()
.trim()
.replace(/[^a-z0-9 ]/g, '') // remove all chars not letters, numbers and spaces (to be replaced)
.replace(/\s+/g, separator)
}

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

View File

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

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,12 +18,12 @@ 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 { 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'
export function BetPanel(props: { contract: Contract; className?: string }) { export function BetPanel(props: { contract: Contract; className?: string }) {

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'

View File

@ -1,165 +0,0 @@
import { Bet } from './firebase/bets'
import { Contract } from './firebase/contracts'
const fees = 0.02
export function 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)
return numerator / denominator
}
export function getProbabilityAfterBet(
pool: { YES: number; NO: number },
outcome: 'YES' | 'NO',
bet: number
) {
const [YES, NO] = [
pool.YES + (outcome === 'YES' ? bet : 0),
pool.NO + (outcome === 'NO' ? bet : 0),
]
return getProbability({ YES, NO })
}
export function calculateShares(
pool: { YES: number; NO: number },
bet: number,
betChoice: 'YES' | 'NO'
) {
const [yesPool, noPool] = [pool.YES, pool.NO]
return betChoice === 'YES'
? bet + (bet * noPool ** 2) / (yesPool ** 2 + bet * yesPool)
: bet + (bet * yesPool ** 2) / (noPool ** 2 + bet * noPool)
}
export function calculatePayout(
contract: Contract,
bet: Bet,
outcome: 'YES' | 'NO' | 'CANCEL' | 'MKT'
) {
const { amount, outcome: betOutcome, shares } = bet
if (outcome === 'CANCEL') return amount
if (outcome === 'MKT') return calculateMktPayout(contract, bet)
if (betOutcome !== outcome) return 0
const { totalShares, totalBets } = contract
if (totalShares[outcome] === 0) return 0
const startPool = contract.startPool.YES + contract.startPool.NO
const truePool = contract.pool.YES + contract.pool.NO - startPool
if (totalBets[outcome] >= truePool)
return (amount / totalBets[outcome]) * truePool
const total = totalShares[outcome] - totalBets[outcome]
const winningsPool = truePool - totalBets[outcome]
return (1 - fees) * (amount + ((shares - amount) / total) * winningsPool)
}
export function calculatePayoutAfterCorrectBet(contract: Contract, bet: Bet) {
const { amount, outcome, shares } = bet
const { totalShares, totalBets } = contract
const startPool = contract.startPool.YES + contract.startPool.NO
const truePool = amount + contract.pool.YES + contract.pool.NO - startPool
const totalBetsOutcome = totalBets[outcome] + amount
const totalSharesOutcome = totalShares[outcome] + shares
if (totalBetsOutcome >= truePool)
return (amount / totalBetsOutcome) * truePool
const total = totalSharesOutcome - totalBetsOutcome
const winningsPool = truePool - totalBetsOutcome
return (1 - fees) * (amount + ((shares - amount) / total) * winningsPool)
}
function calculateMktPayout(contract: Contract, bet: Bet) {
const p =
contract.pool.YES ** 2 / (contract.pool.YES ** 2 + contract.pool.NO ** 2)
const weightedTotal =
p * contract.totalBets.YES + (1 - p) * contract.totalBets.NO
const startPool = contract.startPool.YES + contract.startPool.NO
const truePool = contract.pool.YES + contract.pool.NO - startPool
const betP = bet.outcome === 'YES' ? p : 1 - p
if (weightedTotal >= truePool) {
return ((betP * bet.amount) / weightedTotal) * truePool
}
const winningsPool = truePool - weightedTotal
const weightedShareTotal =
p * (contract.totalShares.YES - contract.totalBets.YES) +
(1 - p) * (contract.totalShares.NO - contract.totalBets.NO)
return (
(1 - fees) *
(betP * bet.amount +
((betP * (bet.shares - bet.amount)) / weightedShareTotal) * winningsPool)
)
}
export function resolvedPayout(contract: Contract, bet: Bet) {
if (contract.resolution)
return calculatePayout(contract, bet, contract.resolution)
throw new Error('Contract was not resolved')
}
export function currentValue(contract: Contract, bet: Bet) {
const prob = getProbability(contract.pool)
const yesPayout = calculatePayout(contract, bet, 'YES')
const noPayout = calculatePayout(contract, bet, 'NO')
return prob * yesPayout + (1 - prob) * noPayout
}
export function calculateSaleAmount(contract: Contract, bet: Bet) {
const { 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 [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 saleAmount = (1 - fees) * adjShareValue
return saleAmount
}

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

@ -1,19 +1,9 @@
import { doc, collection, onSnapshot, setDoc } from 'firebase/firestore' import { doc, collection, onSnapshot, setDoc } from 'firebase/firestore'
import { db } from './init'
import { User } from './users'
// Currently, comments are created after the bet, not atomically with the bet. import { db } from './init'
// They're uniquely identified by the pair contractId/betId. import { User } from '../../../common/user'
export type Comment = { import { Comment } from '../../../common/comment'
contractId: string export type { Comment }
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

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