From 7b0be014eb29448992bd1d4112182a0757d21d3b Mon Sep 17 00:00:00 2001 From: mantikoros Date: Thu, 26 May 2022 09:39:06 -0500 Subject: [PATCH 01/11] show resolved n/a for numeric markets --- web/components/contract/contract-card.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/web/components/contract/contract-card.tsx b/web/components/contract/contract-card.tsx index 6f83ccb9..84e2db8f 100644 --- a/web/components/contract/contract-card.tsx +++ b/web/components/contract/contract-card.tsx @@ -20,6 +20,7 @@ import { import { AnswerLabel, BinaryContractOutcomeLabel, + CancelLabel, FreeResponseOutcomeLabel, } from '../outcome-label' import { getOutcomeProbability, getTopAnswer } from 'common/calculate' @@ -240,7 +241,12 @@ export function NumericResolutionOrExpectation(props: { {resolution ? ( <>
Resolved
-
{resolutionValue}
+ + {resolution === 'CANCEL' ? ( + + ) : ( +
{resolutionValue}
+ )} ) : ( <> From 09e93779fbd1ad34ca06efed9dabe38d49a84ec5 Mon Sep 17 00:00:00 2001 From: Ian Philips Date: Thu, 26 May 2022 10:29:46 -0600 Subject: [PATCH 02/11] Use today's 4pm utc if past already --- functions/src/create-contract.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/functions/src/create-contract.ts b/functions/src/create-contract.ts index 485e3bb1..952d396c 100644 --- a/functions/src/create-contract.ts +++ b/functions/src/create-contract.ts @@ -88,9 +88,11 @@ export const createContract = newEndpoint(['POST'], async (req, _res) => { throw new APIError(400, 'Invalid initial probability') // Uses utc time on server: - const yesterday = new Date() - yesterday.setUTCDate(yesterday.getUTCDate() - 1) - const freeMarketResetTime = yesterday.setUTCHours(16, 0, 0, 0) + const today = new Date() + let freeMarketResetTime = today.setUTCHours(16, 0, 0, 0) + if (today.getTime() < freeMarketResetTime) { + freeMarketResetTime = freeMarketResetTime - 24 * 60 * 60 * 1000 + } const userContractsCreatedTodaySnapshot = await firestore .collection(`contracts`) From 52172700734e0bd5610adc3819ece894968233ae Mon Sep 17 00:00:00 2001 From: Marshall Polaris Date: Thu, 26 May 2022 14:37:51 -0700 Subject: [PATCH 03/11] Serious business API validation & big cleanup of `createContract`, `placeBet` (#302) * Add the great Zod as a dependency to help us * Tweak eslint * Rewrite a ton of stuff in createContract and placeBet * Clean up error reporting in API * Make sure the UI is enforcing validated limits on lengths * Remove unnecessary Math.abs * Better type on `BetInfo` * Kill `manaLimitPerUser` * Clean up hacky parameters on bet info functions * Validate `closeTime` as a valid timestamp in the future --- common/.eslintrc.js | 2 + common/contract.ts | 10 +- common/new-bet.ts | 59 +++----- common/new-contract.ts | 4 +- functions/.eslintrc.js | 1 + functions/package.json | 3 +- functions/src/api.ts | 49 +++++-- functions/src/create-answer.ts | 31 ++--- functions/src/create-contract.ts | 116 ++++++---------- functions/src/place-bet.ts | 200 +++++++++++---------------- web/components/numeric-bet-panel.tsx | 4 +- web/components/tags-input.tsx | 2 + web/pages/create.tsx | 7 +- yarn.lock | 5 + 14 files changed, 221 insertions(+), 272 deletions(-) diff --git a/common/.eslintrc.js b/common/.eslintrc.js index 6e7b62cd..e03e5ef5 100644 --- a/common/.eslintrc.js +++ b/common/.eslintrc.js @@ -21,6 +21,8 @@ module.exports = { }, ], rules: { + 'no-extra-semi': 'off', + 'no-unused-vars': 'off', 'no-constant-condition': ['error', { checkLoops: false }], 'lodash/import-scope': [2, 'member'], }, diff --git a/common/contract.ts b/common/contract.ts index a8f2bd42..e9b768ea 100644 --- a/common/contract.ts +++ b/common/contract.ts @@ -31,8 +31,6 @@ export type FullContract< closeEmailsSent?: number - manaLimitPerUser?: number - volume: number volume24Hours: number volume7Days: number @@ -97,8 +95,12 @@ export type Numeric = { } export type outcomeType = 'BINARY' | 'MULTI' | 'FREE_RESPONSE' | 'NUMERIC' -export const OUTCOME_TYPES = ['BINARY', 'MULTI', 'FREE_RESPONSE', 'NUMERIC'] - +export const OUTCOME_TYPES = [ + 'BINARY', + 'MULTI', + 'FREE_RESPONSE', + 'NUMERIC', +] as const export const MAX_QUESTION_LENGTH = 480 export const MAX_DESCRIPTION_LENGTH = 10000 export const MAX_TAG_LENGTH = 60 diff --git a/common/new-bet.ts b/common/new-bet.ts index b640c0b5..1053ec58 100644 --- a/common/new-bet.ts +++ b/common/new-bet.ts @@ -18,18 +18,25 @@ import { Multi, NumericContract, } from './contract' -import { User } from './user' import { noFees } from './fees' import { addObjects } from './util/object' import { NUMERIC_FIXED_VAR } from './numeric-constants' +export type CandidateBet = Omit +export type BetInfo = { + newBet: CandidateBet + newPool?: { [outcome: string]: number } + newTotalShares?: { [outcome: string]: number } + newTotalBets?: { [outcome: string]: number } + newTotalLiquidity?: number + newP?: number +} + export const getNewBinaryCpmmBetInfo = ( - user: User, outcome: 'YES' | 'NO', amount: number, contract: FullContract, - loanAmount: number, - newBetId: string + loanAmount: number ) => { const { shares, newPool, newP, fees } = calculateCpmmPurchase( contract, @@ -37,15 +44,11 @@ export const getNewBinaryCpmmBetInfo = ( outcome ) - const newBalance = user.balance - (amount - loanAmount) - const { pool, p, totalLiquidity } = contract const probBefore = getCpmmProbability(pool, p) const probAfter = getCpmmProbability(newPool, newP) - const newBet: Bet = { - id: newBetId, - userId: user.id, + const newBet: CandidateBet = { contractId: contract.id, amount, shares, @@ -60,16 +63,14 @@ export const getNewBinaryCpmmBetInfo = ( const { liquidityFee } = fees const newTotalLiquidity = (totalLiquidity ?? 0) + liquidityFee - return { newBet, newPool, newP, newBalance, newTotalLiquidity, fees } + return { newBet, newPool, newP, newTotalLiquidity } } export const getNewBinaryDpmBetInfo = ( - user: User, outcome: 'YES' | 'NO', amount: number, contract: FullContract, - loanAmount: number, - newBetId: string + loanAmount: number ) => { const { YES: yesPool, NO: noPool } = contract.pool @@ -97,9 +98,7 @@ export const getNewBinaryDpmBetInfo = ( const probBefore = getDpmProbability(contract.totalShares) const probAfter = getDpmProbability(newTotalShares) - const newBet: Bet = { - id: newBetId, - userId: user.id, + const newBet: CandidateBet = { contractId: contract.id, amount, loanAmount, @@ -111,18 +110,14 @@ export const getNewBinaryDpmBetInfo = ( fees: noFees, } - const newBalance = user.balance - (amount - loanAmount) - - return { newBet, newPool, newTotalShares, newTotalBets, newBalance } + return { newBet, newPool, newTotalShares, newTotalBets } } export const getNewMultiBetInfo = ( - user: User, outcome: string, amount: number, contract: FullContract, - loanAmount: number, - newBetId: string + loanAmount: number ) => { const { pool, totalShares, totalBets } = contract @@ -140,9 +135,7 @@ export const getNewMultiBetInfo = ( const probBefore = getDpmOutcomeProbability(totalShares, outcome) const probAfter = getDpmOutcomeProbability(newTotalShares, outcome) - const newBet: Bet = { - id: newBetId, - userId: user.id, + const newBet: CandidateBet = { contractId: contract.id, amount, loanAmount, @@ -154,18 +147,14 @@ export const getNewMultiBetInfo = ( fees: noFees, } - const newBalance = user.balance - (amount - loanAmount) - - return { newBet, newPool, newTotalShares, newTotalBets, newBalance } + return { newBet, newPool, newTotalShares, newTotalBets } } export const getNumericBetsInfo = ( - user: User, value: number, outcome: string, amount: number, - contract: NumericContract, - newBetId: string + contract: NumericContract ) => { const { pool, totalShares, totalBets } = contract @@ -187,9 +176,7 @@ export const getNumericBetsInfo = ( const probBefore = getDpmOutcomeProbability(totalShares, outcome) const probAfter = getDpmOutcomeProbability(newTotalShares, outcome) - const newBet: NumericBet = { - id: newBetId, - userId: user.id, + const newBet: CandidateBet = { contractId: contract.id, value, amount, @@ -203,9 +190,7 @@ export const getNumericBetsInfo = ( fees: noFees, } - const newBalance = user.balance - amount - - return { newBet, newPool, newTotalShares, newTotalBets, newBalance } + return { newBet, newPool, newTotalShares, newTotalBets } } export const getLoanAmount = (yourBets: Bet[], newBetAmount: number) => { diff --git a/common/new-contract.ts b/common/new-contract.ts index b70dee37..0b7d294a 100644 --- a/common/new-contract.ts +++ b/common/new-contract.ts @@ -27,8 +27,7 @@ export function getNewContract( // used for numeric markets bucketCount: number, min: number, - max: number, - manaLimitPerUser: number + max: number ) { const tags = parseTags( `${question} ${description} ${extraTags.map((tag) => `#${tag}`).join(' ')}` @@ -70,7 +69,6 @@ export function getNewContract( liquidityFee: 0, platformFee: 0, }, - manaLimitPerUser, }) return contract as Contract diff --git a/functions/.eslintrc.js b/functions/.eslintrc.js index c5b8e16f..5d66d2c6 100644 --- a/functions/.eslintrc.js +++ b/functions/.eslintrc.js @@ -17,6 +17,7 @@ module.exports = { }, ], rules: { + 'no-extra-semi': 'off', 'no-unused-vars': 'off', 'no-constant-condition': ['error', { checkLoops: false }], 'lodash/import-scope': [2, 'member'], diff --git a/functions/package.json b/functions/package.json index d3ee68e2..49352969 100644 --- a/functions/package.json +++ b/functions/package.json @@ -28,7 +28,8 @@ "mailgun-js": "0.22.0", "module-alias": "2.2.2", "react-query": "3.39.0", - "stripe": "8.194.0" + "stripe": "8.194.0", + "zod": "3.17.2" }, "devDependencies": { "@types/mailgun-js": "0.22.12", diff --git a/functions/src/api.ts b/functions/src/api.ts index fa3a9aa6..31f14257 100644 --- a/functions/src/api.ts +++ b/functions/src/api.ts @@ -1,6 +1,7 @@ import * as admin from 'firebase-admin' import * as functions from 'firebase-functions' import * as Cors from 'cors' +import { z } from 'zod' import { User, PrivateUser } from '../../common/user' import { @@ -8,10 +9,11 @@ import { CORS_ORIGIN_LOCALHOST, } from '../../common/envs/constants' +type Output = Record type Request = functions.https.Request type Response = functions.Response -type Handler = (req: Request, res: Response) => Promise type AuthedUser = [User, PrivateUser] +type Handler = (req: Request, user: AuthedUser) => Promise type JwtCredentials = { kind: 'jwt'; data: admin.auth.DecodedIdToken } type KeyCredentials = { kind: 'key'; data: string } type Credentials = JwtCredentials | KeyCredentials @@ -19,10 +21,13 @@ type Credentials = JwtCredentials | KeyCredentials export class APIError { code: number msg: string - constructor(code: number, msg: string) { + details: unknown + constructor(code: number, msg: string, details?: unknown) { this.code = code this.msg = msg + this.details = details } + toJson() {} } export const parseCredentials = async (req: Request): Promise => { @@ -40,14 +45,11 @@ export const parseCredentials = async (req: Request): Promise => { case 'Bearer': try { const jwt = await admin.auth().verifyIdToken(payload) - if (!jwt.user_id) { - throw new APIError(403, 'JWT must contain Manifold user ID.') - } return { kind: 'jwt', data: jwt } } catch (err) { // This is somewhat suspicious, so get it into the firebase console functions.logger.error('Error verifying Firebase JWT: ', err) - throw new APIError(403, `Error validating token: ${err}.`) + throw new APIError(403, 'Error validating token.') } case 'Key': return { kind: 'key', data: payload } @@ -63,6 +65,9 @@ export const lookupUser = async (creds: Credentials): Promise => { switch (creds.kind) { case 'jwt': { const { user_id } = creds.data + if (typeof user_id !== 'string') { + throw new APIError(403, 'JWT must contain Manifold user ID.') + } const [userSnap, privateUserSnap] = await Promise.all([ users.doc(user_id).get(), privateUsers.doc(user_id).get(), @@ -109,6 +114,27 @@ export const applyCors = ( }) } +export const zTimestamp = () => { + return z.preprocess((arg) => { + return typeof arg == 'number' ? new Date(arg) : undefined + }, z.date()) +} + +export const validate = (schema: T, val: unknown) => { + const result = schema.safeParse(val) + if (!result.success) { + const issues = result.error.issues.map((i) => { + return { + field: i.path.join('.') || null, + error: i.message, + } + }) + throw new APIError(400, 'Error validating request.', issues) + } else { + return result.data as z.infer + } +} + export const newEndpoint = (methods: [string], fn: Handler) => functions.runWith({ minInstances: 1 }).https.onRequest(async (req, res) => { await applyCors(req, res, { @@ -120,12 +146,17 @@ export const newEndpoint = (methods: [string], fn: Handler) => const allowed = methods.join(', ') throw new APIError(405, `This endpoint supports only ${allowed}.`) } - res.status(200).json(await fn(req, res)) + const authedUser = await lookupUser(await parseCredentials(req)) + res.status(200).json(await fn(req, authedUser)) } catch (e) { if (e instanceof APIError) { - // Emit a 200 anyway here for now, for backwards compatibility - res.status(e.code).json({ message: e.msg }) + const output: { [k: string]: unknown } = { message: e.msg } + if (e.details != null) { + output.details = e.details + } + res.status(e.code).json(output) } else { + functions.logger.error(e) res.status(500).json({ message: 'An unknown error occurred.' }) } } diff --git a/functions/src/create-answer.ts b/functions/src/create-answer.ts index bc74dc05..7fbe5416 100644 --- a/functions/src/create-answer.ts +++ b/functions/src/create-answer.ts @@ -12,7 +12,6 @@ import { getNewMultiBetInfo } from '../../common/new-bet' import { Answer, MAX_ANSWER_LENGTH } from '../../common/answer' import { getContract, getValues } from './utils' import { sendNewAnswerEmail } from './emails' -import { Bet } from '../../common/bet' export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall( async ( @@ -61,11 +60,6 @@ export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall( if (closeTime && Date.now() > closeTime) return { status: 'error', message: 'Trading is closed' } - const yourBetsSnap = await transaction.get( - contractDoc.collection('bets').where('userId', '==', userId) - ) - const yourBets = yourBetsSnap.docs.map((doc) => doc.data() as Bet) - const [lastAnswer] = await getValues( firestore .collection(`contracts/${contractId}/answers`) @@ -99,23 +93,20 @@ export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall( } transaction.create(newAnswerDoc, answer) - const newBetDoc = firestore - .collection(`contracts/${contractId}/bets`) - .doc() + const loanAmount = 0 - const loanAmount = 0 // getLoanAmount(yourBets, amount) - - const { newBet, newPool, newTotalShares, newTotalBets, newBalance } = + const { newBet, newPool, newTotalShares, newTotalBets } = getNewMultiBetInfo( - user, answerId, amount, contract as FullContract, - loanAmount, - newBetDoc.id + loanAmount ) - transaction.create(newBetDoc, newBet) + const newBalance = user.balance - amount + const betDoc = firestore.collection(`contracts/${contractId}/bets`).doc() + transaction.create(betDoc, { id: betDoc.id, userId: user.id, ...newBet }) + transaction.update(userDoc, { balance: newBalance }) transaction.update(contractDoc, { pool: newPool, totalShares: newTotalShares, @@ -124,13 +115,7 @@ export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall( volume: volume + amount, }) - if (!isFinite(newBalance)) { - throw new Error('Invalid user balance for ' + user.username) - } - - transaction.update(userDoc, { balance: newBalance }) - - return { status: 'success', answerId, betId: newBetDoc.id, answer } + return { status: 'success', answerId, betId: betDoc.id, answer } }) const { answer } = result diff --git a/functions/src/create-contract.ts b/functions/src/create-contract.ts index 952d396c..42a1c376 100644 --- a/functions/src/create-contract.ts +++ b/functions/src/create-contract.ts @@ -1,4 +1,5 @@ import * as admin from 'firebase-admin' +import { z } from 'zod' import { Binary, @@ -17,7 +18,7 @@ import { slugify } from '../../common/util/slugify' import { randomString } from '../../common/util/random' import { chargeUser } from './utils' -import { APIError, newEndpoint, parseCredentials, lookupUser } from './api' +import { APIError, newEndpoint, validate, zTimestamp } from './api' import { FIXED_ANTE, @@ -26,66 +27,45 @@ import { getFreeAnswerAnte, getNumericAnte, HOUSE_LIQUIDITY_PROVIDER_ID, - MINIMUM_ANTE, } from '../../common/antes' import { getNoneAnswer } from '../../common/answer' import { getNewContract } from '../../common/new-contract' import { NUMERIC_BUCKET_COUNT } from '../../common/numeric-constants' -export const createContract = newEndpoint(['POST'], async (req, _res) => { - const [creator, _privateUser] = await lookupUser(await parseCredentials(req)) - let { - question, - outcomeType, - description, - initialProb, - closeTime, - tags, - min, - max, - manaLimitPerUser, - } = req.body || {} +const bodySchema = z.object({ + question: z.string().min(1).max(MAX_QUESTION_LENGTH), + description: z.string().max(MAX_DESCRIPTION_LENGTH), + tags: z.array(z.string().min(1).max(MAX_TAG_LENGTH)).optional(), + closeTime: zTimestamp().refine( + (date) => date.getTime() > new Date().getTime(), + 'Close time must be in the future.' + ), + outcomeType: z.enum(OUTCOME_TYPES), +}) - if (!question || typeof question != 'string') - throw new APIError(400, 'Missing or invalid question field') +const binarySchema = z.object({ + initialProb: z.number().min(1).max(99), +}) - question = question.slice(0, MAX_QUESTION_LENGTH) +const numericSchema = z.object({ + min: z.number(), + max: z.number(), +}) - if (typeof description !== 'string') - throw new APIError(400, 'Invalid description field') - - description = description.slice(0, MAX_DESCRIPTION_LENGTH) - - if (tags !== undefined && !Array.isArray(tags)) - throw new APIError(400, 'Invalid tags field') - - tags = (tags || []).map((tag: string) => - tag.toString().slice(0, MAX_TAG_LENGTH) +export const createContract = newEndpoint(['POST'], async (req, [user, _]) => { + const { question, description, tags, closeTime, outcomeType } = validate( + bodySchema, + req.body ) - outcomeType = outcomeType ?? 'BINARY' - - if (!OUTCOME_TYPES.includes(outcomeType)) - throw new APIError(400, 'Invalid outcomeType') - - if ( - outcomeType === 'NUMERIC' && - !( - min !== undefined && - max !== undefined && - isFinite(min) && - isFinite(max) && - min < max && - max - min > 0.01 - ) - ) - throw new APIError(400, 'Invalid range') - - if ( - outcomeType === 'BINARY' && - (!initialProb || initialProb < 1 || initialProb > 99) - ) - throw new APIError(400, 'Invalid initial probability') + let min, max, initialProb + if (outcomeType === 'NUMERIC') { + ;({ min, max } = validate(numericSchema, req.body)) + if (max - min <= 0.01) throw new APIError(400, 'Invalid range.') + } + if (outcomeType === 'BINARY') { + ;({ initialProb } = validate(binarySchema, req.body)) + } // Uses utc time on server: const today = new Date() @@ -96,7 +76,7 @@ export const createContract = newEndpoint(['POST'], async (req, _res) => { const userContractsCreatedTodaySnapshot = await firestore .collection(`contracts`) - .where('creatorId', '==', creator.id) + .where('creatorId', '==', user.id) .where('createdTime', '>=', freeMarketResetTime) .get() console.log('free market reset time: ', freeMarketResetTime) @@ -104,18 +84,9 @@ export const createContract = newEndpoint(['POST'], async (req, _res) => { const ante = FIXED_ANTE - if ( - ante === undefined || - ante < MINIMUM_ANTE || - (ante > creator.balance && !isFree) || - isNaN(ante) || - !isFinite(ante) - ) - throw new APIError(400, 'Invalid ante') - console.log( 'creating contract for', - creator.username, + user.username, 'on', question, 'ante:', @@ -123,31 +94,28 @@ export const createContract = newEndpoint(['POST'], async (req, _res) => { ) const slug = await getSlug(question) - const contractRef = firestore.collection('contracts').doc() - const contract = getNewContract( contractRef.id, slug, - creator, + user, question, outcomeType, description, - initialProb, + initialProb ?? 0, ante, - closeTime, + closeTime.getTime(), tags ?? [], NUMERIC_BUCKET_COUNT, min ?? 0, - max ?? 0, - manaLimitPerUser ?? 0 + max ?? 0 ) - if (!isFree && ante) await chargeUser(creator.id, ante, true) + if (!isFree && ante) await chargeUser(user.id, ante, true) await contractRef.create(contract) - const providerId = isFree ? HOUSE_LIQUIDITY_PROVIDER_ID : creator.id + const providerId = isFree ? HOUSE_LIQUIDITY_PROVIDER_ID : user.id if (outcomeType === 'BINARY' && contract.mechanism === 'dpm-2') { const yesBetDoc = firestore @@ -157,7 +125,7 @@ export const createContract = newEndpoint(['POST'], async (req, _res) => { const noBetDoc = firestore.collection(`contracts/${contract.id}/bets`).doc() const { yesBet, noBet } = getAnteBets( - creator, + user, contract as FullContract, yesBetDoc.id, noBetDoc.id @@ -183,7 +151,7 @@ export const createContract = newEndpoint(['POST'], async (req, _res) => { .collection(`contracts/${contract.id}/answers`) .doc('0') - const noneAnswer = getNoneAnswer(contract.id, creator) + const noneAnswer = getNoneAnswer(contract.id, user) await noneAnswerDoc.set(noneAnswer) const anteBetDoc = firestore @@ -202,7 +170,7 @@ export const createContract = newEndpoint(['POST'], async (req, _res) => { .doc() const anteBet = getNumericAnte( - creator, + user, contract as FullContract, ante, anteBetDoc.id diff --git a/functions/src/place-bet.ts b/functions/src/place-bet.ts index d97effe4..184ee2df 100644 --- a/functions/src/place-bet.ts +++ b/functions/src/place-bet.ts @@ -1,146 +1,112 @@ import * as admin from 'firebase-admin' +import { z } from 'zod' -import { APIError, newEndpoint, parseCredentials, lookupUser } from './api' +import { APIError, newEndpoint, validate } from './api' import { Contract } from '../../common/contract' import { User } from '../../common/user' import { + BetInfo, getNewBinaryCpmmBetInfo, getNewBinaryDpmBetInfo, getNewMultiBetInfo, getNumericBetsInfo, } from '../../common/new-bet' import { addObjects, removeUndefinedProps } from '../../common/util/object' -import { Bet } from '../../common/bet' import { redeemShares } from './redeem-shares' -import { Fees } from '../../common/fees' -export const placeBet = newEndpoint(['POST'], async (req, _res) => { - const [bettor, _privateUser] = await lookupUser(await parseCredentials(req)) - const { amount, outcome, contractId, value } = req.body || {} +const bodySchema = z.object({ + contractId: z.string(), + amount: z.number().gte(1), +}) - if (amount < 1 || isNaN(amount) || !isFinite(amount)) - throw new APIError(400, 'Invalid amount') +const binarySchema = z.object({ + outcome: z.enum(['YES', 'NO']), +}) - if (outcome !== 'YES' && outcome !== 'NO' && isNaN(+outcome)) - throw new APIError(400, 'Invalid outcome') +const freeResponseSchema = z.object({ + outcome: z.string(), +}) - if (value !== undefined && !isFinite(value)) - throw new APIError(400, 'Invalid value') +const numericSchema = z.object({ + outcome: z.string(), + value: z.number(), +}) - // run as transaction to prevent race conditions - return await firestore - .runTransaction(async (transaction) => { - const userDoc = firestore.doc(`users/${bettor.id}`) - const userSnap = await transaction.get(userDoc) - if (!userSnap.exists) throw new APIError(400, 'User not found') - const user = userSnap.data() as User +export const placeBet = newEndpoint(['POST'], async (req, [bettor, _]) => { + const { amount, contractId } = validate(bodySchema, req.body) - const contractDoc = firestore.doc(`contracts/${contractId}`) - const contractSnap = await transaction.get(contractDoc) - if (!contractSnap.exists) throw new APIError(400, 'Invalid contract') - const contract = contractSnap.data() as Contract + const result = await firestore.runTransaction(async (trans) => { + const userDoc = firestore.doc(`users/${bettor.id}`) + const userSnap = await trans.get(userDoc) + if (!userSnap.exists) throw new APIError(400, 'User not found.') + const user = userSnap.data() as User + if (user.balance < amount) throw new APIError(400, 'Insufficient balance.') - const { closeTime, outcomeType, mechanism, collectedFees, volume } = - contract - if (closeTime && Date.now() > closeTime) - throw new APIError(400, 'Trading is closed') + const contractDoc = firestore.doc(`contracts/${contractId}`) + const contractSnap = await trans.get(contractDoc) + if (!contractSnap.exists) throw new APIError(400, 'Contract not found.') + const contract = contractSnap.data() as Contract - const yourBetsSnap = await transaction.get( - contractDoc.collection('bets').where('userId', '==', bettor.id) - ) - const yourBets = yourBetsSnap.docs.map((doc) => doc.data() as Bet) + const loanAmount = 0 + const { closeTime, outcomeType, mechanism, collectedFees, volume } = + contract + if (closeTime && Date.now() > closeTime) + throw new APIError(400, 'Trading is closed.') - const loanAmount = 0 // getLoanAmount(yourBets, amount) - if (user.balance < amount) throw new APIError(400, 'Insufficient balance') - - if (outcomeType === 'FREE_RESPONSE') { - const answerSnap = await transaction.get( - contractDoc.collection('answers').doc(outcome) - ) - if (!answerSnap.exists) throw new APIError(400, 'Invalid contract') + const { + newBet, + newPool, + newTotalShares, + newTotalBets, + newTotalLiquidity, + newP, + } = await (async (): Promise => { + if (outcomeType == 'BINARY' && mechanism == 'dpm-2') { + const { outcome } = validate(binarySchema, req.body) + return getNewBinaryDpmBetInfo(outcome, amount, contract, loanAmount) + } else if (outcomeType == 'BINARY' && mechanism == 'cpmm-1') { + const { outcome } = validate(binarySchema, req.body) + return getNewBinaryCpmmBetInfo(outcome, amount, contract, loanAmount) + } else if (outcomeType == 'FREE_RESPONSE' && mechanism == 'dpm-2') { + const { outcome } = validate(freeResponseSchema, req.body) + const answerDoc = contractDoc.collection('answers').doc(outcome) + const answerSnap = await trans.get(answerDoc) + if (!answerSnap.exists) throw new APIError(400, 'Invalid answer') + return getNewMultiBetInfo(outcome, amount, contract, loanAmount) + } else if (outcomeType == 'NUMERIC' && mechanism == 'dpm-2') { + const { outcome, value } = validate(numericSchema, req.body) + return getNumericBetsInfo(value, outcome, amount, contract) + } else { + throw new APIError(500, 'Contract has invalid type/mechanism.') } + })() - const newBetDoc = firestore - .collection(`contracts/${contractId}/bets`) - .doc() + if (newP != null && !isFinite(newP)) { + throw new APIError(400, 'Trade rejected due to overflow error.') + } - const { - newBet, - newPool, - newTotalShares, - newTotalBets, - newBalance, - newTotalLiquidity, - fees, - newP, - } = - outcomeType === 'BINARY' - ? mechanism === 'dpm-2' - ? getNewBinaryDpmBetInfo( - user, - outcome as 'YES' | 'NO', - amount, - contract, - loanAmount, - newBetDoc.id - ) - : (getNewBinaryCpmmBetInfo( - user, - outcome as 'YES' | 'NO', - amount, - contract, - loanAmount, - newBetDoc.id - ) as any) - : outcomeType === 'NUMERIC' && mechanism === 'dpm-2' - ? getNumericBetsInfo( - user, - value, - outcome, - amount, - contract, - newBetDoc.id - ) - : getNewMultiBetInfo( - user, - outcome, - amount, - contract as any, - loanAmount, - newBetDoc.id - ) + const newBalance = user.balance - amount - loanAmount + const betDoc = contractDoc.collection('bets').doc() + trans.create(betDoc, { id: betDoc.id, userId: user.id, ...newBet }) + trans.update(userDoc, { balance: newBalance }) + trans.update( + contractDoc, + removeUndefinedProps({ + pool: newPool, + p: newP, + totalShares: newTotalShares, + totalBets: newTotalBets, + totalLiquidity: newTotalLiquidity, + collectedFees: addObjects(newBet.fees, collectedFees), + volume: volume + amount, + }) + ) - if (newP !== undefined && !isFinite(newP)) { - throw new APIError(400, 'Trade rejected due to overflow error.') - } + return { betId: betDoc.id } + }) - transaction.create(newBetDoc, newBet) - - transaction.update( - contractDoc, - removeUndefinedProps({ - pool: newPool, - p: newP, - totalShares: newTotalShares, - totalBets: newTotalBets, - totalLiquidity: newTotalLiquidity, - collectedFees: addObjects(fees ?? {}, collectedFees ?? {}), - volume: volume + Math.abs(amount), - }) - ) - - if (!isFinite(newBalance)) { - throw new APIError(500, 'Invalid user balance for ' + user.username) - } - - transaction.update(userDoc, { balance: newBalance }) - - return { betId: newBetDoc.id } - }) - .then(async (result) => { - await redeemShares(bettor.id, contractId) - return result - }) + await redeemShares(bettor.id, contractId) + return result }) const firestore = admin.firestore() diff --git a/web/components/numeric-bet-panel.tsx b/web/components/numeric-bet-panel.tsx index f249e3c3..ebb80dd0 100644 --- a/web/components/numeric-bet-panel.tsx +++ b/web/components/numeric-bet-panel.tsx @@ -102,12 +102,10 @@ function NumericBuyPanel(props: { const betDisabled = isSubmitting || !betAmount || !bucketChoice || error const { newBet, newPool, newTotalShares, newTotalBets } = getNumericBetsInfo( - { id: 'dummy', balance: 0 } as User, // a little hackish value ?? 0, bucketChoice ?? 'NaN', betAmount ?? 0, - contract, - 'dummy id' + contract ) const { probAfter: outcomeProb, shares } = newBet diff --git a/web/components/tags-input.tsx b/web/components/tags-input.tsx index ff9029c9..6a0cdcda 100644 --- a/web/components/tags-input.tsx +++ b/web/components/tags-input.tsx @@ -5,6 +5,7 @@ import { Contract, updateContract } from 'web/lib/firebase/contracts' import { Col } from './layout/col' import { Row } from './layout/row' import { TagsList } from './tags-list' +import { MAX_TAG_LENGTH } from 'common/contract' export function TagsInput(props: { contract: Contract; className?: string }) { const { contract, className } = props @@ -36,6 +37,7 @@ export function TagsInput(props: { contract: Contract; className?: string }) { className="input input-sm input-bordered resize-none" disabled={isSubmitting} value={tagText} + maxLength={MAX_TAG_LENGTH} onChange={(e) => setTagText(e.target.value || '')} onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) { diff --git a/web/pages/create.tsx b/web/pages/create.tsx index bf035330..342eafeb 100644 --- a/web/pages/create.tsx +++ b/web/pages/create.tsx @@ -11,7 +11,11 @@ import { FIXED_ANTE, MINIMUM_ANTE } from 'common/antes' import { InfoTooltip } from 'web/components/info-tooltip' import { Page } from 'web/components/page' import { Row } from 'web/components/layout/row' -import { MAX_DESCRIPTION_LENGTH, outcomeType } from 'common/contract' +import { + MAX_DESCRIPTION_LENGTH, + MAX_QUESTION_LENGTH, + outcomeType, +} from 'common/contract' import { formatMoney } from 'common/util/format' import { useHasCreatedContractToday } from 'web/hooks/use-has-created-contract-today' import { removeUndefinedProps } from 'common/util/object' @@ -37,6 +41,7 @@ export default function Create() { placeholder="e.g. Will the Democrats win the 2024 US presidential election?" className="input input-bordered resize-none" autoFocus + maxLength={MAX_QUESTION_LENGTH} value={question} onChange={(e) => setQuestion(e.target.value || '')} /> diff --git a/yarn.lock b/yarn.lock index 7eecfe4d..982cd49a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5410,3 +5410,8 @@ yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +zod@3.17.2: + version "3.17.2" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.17.2.tgz#d20b32146a3b5068f8f71768b4f9a4bfe52cddb0" + integrity sha512-L8UPS2J/F3dIA8gsPTvGjd8wSRuwR1Td4AqR2Nw8r8BgcLIbZZ5/tCII7hbTLXTQDhxUnnsFdHwpETGajt5i3A== From 420ea9e90e31c2080a516e4dcf9c1a4119acce42 Mon Sep 17 00:00:00 2001 From: Marshall Polaris Date: Thu, 26 May 2022 14:41:24 -0700 Subject: [PATCH 04/11] Add more linting to `web` package (#343) * Import React a lot * Fix misc. linting concerns * Turn on many recommended lints for `web` package --- web/.eslintrc.js | 13 ++++- web/components/amount-input.tsx | 1 + web/components/client-render.tsx | 2 +- web/components/contract/quick-bet.tsx | 3 +- web/components/datetime-tooltip.tsx | 1 + web/components/feed/activity-items.ts | 7 +-- web/components/feed/copy-link-date-time.tsx | 2 +- web/components/join-spans.tsx | 4 +- web/components/layout/modal.tsx | 4 +- web/components/layout/tabs.tsx | 6 +-- web/components/linkify.tsx | 4 +- web/components/nav/nav-bar.tsx | 2 +- web/components/nav/sidebar.tsx | 4 +- web/components/number-input.tsx | 1 + web/components/outcome-label.tsx | 3 +- web/components/page.tsx | 3 +- web/components/site-link.tsx | 3 +- web/lib/api/proxy.ts | 6 +-- web/lib/firebase/init.ts | 12 +++-- web/lib/util/copy.ts | 2 +- web/pages/_app.tsx | 12 ++--- web/pages/admin.tsx | 58 ++++++++++----------- web/pages/charity/[charitySlug].tsx | 2 +- web/pages/folds.tsx | 2 +- web/pages/profile.tsx | 25 ++++----- 25 files changed, 99 insertions(+), 83 deletions(-) diff --git a/web/.eslintrc.js b/web/.eslintrc.js index f864ffa2..6c34b521 100644 --- a/web/.eslintrc.js +++ b/web/.eslintrc.js @@ -1,10 +1,21 @@ module.exports = { parser: '@typescript-eslint/parser', plugins: ['lodash'], - extends: ['plugin:react-hooks/recommended', 'plugin:@next/next/recommended'], + extends: [ + 'plugin:@typescript-eslint/recommended', + 'plugin:react-hooks/recommended', + 'plugin:@next/next/recommended', + ], rules: { + '@typescript-eslint/no-empty-function': 'off', + '@typescript-eslint/no-unused-vars': 'off', + '@typescript-eslint/no-explicit-any': 'off', '@next/next/no-img-element': 'off', '@next/next/no-typos': 'off', 'lodash/import-scope': [2, 'member'], }, + env: { + browser: true, + node: true, + }, } diff --git a/web/components/amount-input.tsx b/web/components/amount-input.tsx index 84a29b6d..a31957cb 100644 --- a/web/components/amount-input.tsx +++ b/web/components/amount-input.tsx @@ -1,4 +1,5 @@ import clsx from 'clsx' +import React from 'react' import { useUser } from 'web/hooks/use-user' import { formatMoney } from 'common/util/format' import { Col } from './layout/col' diff --git a/web/components/client-render.tsx b/web/components/client-render.tsx index a58c90ff..d26d2301 100644 --- a/web/components/client-render.tsx +++ b/web/components/client-render.tsx @@ -1,5 +1,5 @@ // Adapted from https://stackoverflow.com/a/50884055/1222351 -import { useEffect, useState } from 'react' +import React, { useEffect, useState } from 'react' export function ClientRender(props: { children: React.ReactNode }) { const { children } = props diff --git a/web/components/contract/quick-bet.tsx b/web/components/contract/quick-bet.tsx index d7b37834..c4b1ec16 100644 --- a/web/components/contract/quick-bet.tsx +++ b/web/components/contract/quick-bet.tsx @@ -229,12 +229,13 @@ function QuickOutcomeView(props: { case 'NUMERIC': display = formatLargeNumber(getExpectedValue(contract as NumericContract)) break - case 'FREE_RESPONSE': + case 'FREE_RESPONSE': { const topAnswer = getTopAnswer(contract as FreeResponseContract) display = topAnswer && formatPercent(getOutcomeProbability(contract, topAnswer.id)) break + } } return ( diff --git a/web/components/datetime-tooltip.tsx b/web/components/datetime-tooltip.tsx index 69c4521e..7f7a9b45 100644 --- a/web/components/datetime-tooltip.tsx +++ b/web/components/datetime-tooltip.tsx @@ -1,3 +1,4 @@ +import React from 'react' import dayjs from 'dayjs' import utc from 'dayjs/plugin/utc' import timezone from 'dayjs/plugin/timezone' diff --git a/web/components/feed/activity-items.ts b/web/components/feed/activity-items.ts index c958a892..b8d99598 100644 --- a/web/components/feed/activity-items.ts +++ b/web/components/feed/activity-items.ts @@ -343,7 +343,7 @@ function groupBetsAndComments( // iterate through the bets and comment activity items and add them to the items in order of comment creation time: const unorderedBetsAndComments = [...commentsWithoutBets, ...groupedBets] - let sortedBetsAndComments = sortBy(unorderedBetsAndComments, (item) => { + const sortedBetsAndComments = sortBy(unorderedBetsAndComments, (item) => { if (item.type === 'comment') { return item.comment.createdTime } else if (item.type === 'bet') { @@ -540,7 +540,7 @@ export function getSpecificContractActivityItems( } ) { const { mode } = options - let items = [] as ActivityItem[] + const items = [] as ActivityItem[] switch (mode) { case 'bets': @@ -559,7 +559,7 @@ export function getSpecificContractActivityItems( ) break - case 'comments': + case 'comments': { const nonFreeResponseComments = comments.filter((comment) => commentIsGeneralComment(comment, contract) ) @@ -585,6 +585,7 @@ export function getSpecificContractActivityItems( ), }) break + } case 'free-response-comment-answer-groups': items.push( ...getAnswerAndCommentInputGroups( diff --git a/web/components/feed/copy-link-date-time.tsx b/web/components/feed/copy-link-date-time.tsx index 354996a4..60395801 100644 --- a/web/components/feed/copy-link-date-time.tsx +++ b/web/components/feed/copy-link-date-time.tsx @@ -21,7 +21,7 @@ export function CopyLinkDateTimeComponent(props: { event: React.MouseEvent ) { event.preventDefault() - let elementLocation = `https://${ENV_CONFIG.domain}${contractPath( + const elementLocation = `https://${ENV_CONFIG.domain}${contractPath( contract )}#${elementId}` diff --git a/web/components/join-spans.tsx b/web/components/join-spans.tsx index e6947ee8..c11b4eed 100644 --- a/web/components/join-spans.tsx +++ b/web/components/join-spans.tsx @@ -1,6 +1,8 @@ +import { ReactNode } from 'react' + export const JoinSpans = (props: { children: any[] - separator?: JSX.Element | string + separator?: ReactNode }) => { const { separator } = props const children = props.children.filter((x) => !!x) diff --git a/web/components/layout/modal.tsx b/web/components/layout/modal.tsx index 6c6b1af4..d61a38dd 100644 --- a/web/components/layout/modal.tsx +++ b/web/components/layout/modal.tsx @@ -1,9 +1,9 @@ -import { Fragment } from 'react' +import { Fragment, ReactNode } from 'react' import { Dialog, Transition } from '@headlessui/react' // From https://tailwindui.com/components/application-ui/overlays/modals export function Modal(props: { - children: React.ReactNode + children: ReactNode open: boolean setOpen: (open: boolean) => void }) { diff --git a/web/components/layout/tabs.tsx b/web/components/layout/tabs.tsx index fc9dd775..eeab17f9 100644 --- a/web/components/layout/tabs.tsx +++ b/web/components/layout/tabs.tsx @@ -1,12 +1,12 @@ import clsx from 'clsx' import Link from 'next/link' -import { useState } from 'react' +import { ReactNode, useState } from 'react' import { Row } from './row' type Tab = { title: string - tabIcon?: JSX.Element - content: JSX.Element + tabIcon?: ReactNode + content: ReactNode // If set, change the url to this href when the tab is selected href?: string } diff --git a/web/components/linkify.tsx b/web/components/linkify.tsx index 3a5f2a18..f33b2bf5 100644 --- a/web/components/linkify.tsx +++ b/web/components/linkify.tsx @@ -4,14 +4,14 @@ import { SiteLink } from './site-link' // Return a JSX span, linkifying @username, #hashtags, and https://... // TODO: Use a markdown parser instead of rolling our own here. export function Linkify(props: { text: string; gray?: boolean }) { - let { text, gray } = props + const { text, gray } = props // Replace "m1234" with "ϻ1234" // const mRegex = /(\W|^)m(\d+)/g // text = text.replace(mRegex, (_, pre, num) => `${pre}ϻ${num}`) // Find instances of @username, #hashtag, and https://... const regex = - /(?:^|\s)(?:[@#][a-z0-9_]+|https?:\/\/[-A-Za-z0-9+&@#\/%?=~_()|!:,.;]*[-A-Za-z0-9+&@#\/%=~_|])/gi + /(?:^|\s)(?:[@#][a-z0-9_]+|https?:\/\/[-A-Za-z0-9+&@#/%?=~_()|!:,.;]*[-A-Za-z0-9+&@#/%=~_|])/gi const matches = text.match(regex) || [] const links = matches.map((match) => { // Matches are in the form: " @username" or "https://example.com" diff --git a/web/components/nav/nav-bar.tsx b/web/components/nav/nav-bar.tsx index 0cb885f5..40a5aacd 100644 --- a/web/components/nav/nav-bar.tsx +++ b/web/components/nav/nav-bar.tsx @@ -17,7 +17,7 @@ import { Avatar } from '../avatar' import clsx from 'clsx' import { useRouter } from 'next/router' -function getNavigation(username: String) { +function getNavigation(username: string) { return [ { name: 'Home', href: '/home', icon: HomeIcon }, { name: 'Activity', href: '/activity', icon: ChatAltIcon }, diff --git a/web/components/nav/sidebar.tsx b/web/components/nav/sidebar.tsx index 1948aae2..962593ed 100644 --- a/web/components/nav/sidebar.tsx +++ b/web/components/nav/sidebar.tsx @@ -25,7 +25,7 @@ import { useHasCreatedContractToday, } from 'web/hooks/use-has-created-contract-today' import { Row } from '../layout/row' -import { useEffect, useState } from 'react' +import React, { useEffect, useState } from 'react' // Create an icon from the url of an image function IconFromUrl(url: string): React.ComponentType<{ className?: string }> { @@ -130,7 +130,7 @@ export default function Sidebar(props: { className?: string }) { const nextUtcResetTime = getUtcFreeMarketResetTime(false) const interval = setInterval(() => { const now = new Date().getTime() - let timeUntil = nextUtcResetTime - now + const timeUntil = nextUtcResetTime - now const hoursUntil = timeUntil / 1000 / 60 / 60 const minutesUntil = Math.floor((hoursUntil * 60) % 60) const secondsUntil = Math.floor((hoursUntil * 60 * 60) % 60) diff --git a/web/components/number-input.tsx b/web/components/number-input.tsx index a5adb3f8..c40fadeb 100644 --- a/web/components/number-input.tsx +++ b/web/components/number-input.tsx @@ -1,5 +1,6 @@ import clsx from 'clsx' +import React from 'react' import { Col } from './layout/col' import { Spacer } from './layout/spacer' diff --git a/web/components/outcome-label.tsx b/web/components/outcome-label.tsx index 394fb6ae..7cbfa144 100644 --- a/web/components/outcome-label.tsx +++ b/web/components/outcome-label.tsx @@ -1,4 +1,5 @@ import clsx from 'clsx' +import { ReactNode } from 'react' import { Answer } from 'common/answer' import { getProbability } from 'common/calculate' import { getValueFromBucket } from 'common/calculate-dpm' @@ -156,7 +157,7 @@ export function AnswerLabel(props: { function FreeResponseAnswerToolTip(props: { text: string - children?: React.ReactNode + children?: ReactNode }) { const { text } = props return ( diff --git a/web/components/page.tsx b/web/components/page.tsx index faefb718..64830181 100644 --- a/web/components/page.tsx +++ b/web/components/page.tsx @@ -1,4 +1,5 @@ import clsx from 'clsx' +import { ReactNode } from 'react' import { BottomNavBar } from './nav/nav-bar' import Sidebar from './nav/sidebar' import { Toaster } from 'react-hot-toast' @@ -6,7 +7,7 @@ import { Toaster } from 'react-hot-toast' export function Page(props: { margin?: boolean assertUser?: 'signed-in' | 'signed-out' - rightSidebar?: React.ReactNode + rightSidebar?: ReactNode suspend?: boolean children?: any }) { diff --git a/web/components/site-link.tsx b/web/components/site-link.tsx index 86fc6c5c..cff47ea5 100644 --- a/web/components/site-link.tsx +++ b/web/components/site-link.tsx @@ -1,4 +1,5 @@ import clsx from 'clsx' +import { ReactNode } from 'react' import Link from 'next/link' export const SiteLink = (props: { @@ -30,7 +31,7 @@ export const SiteLink = (props: { ) } -function MaybeLink(props: { href: string; children: React.ReactNode }) { +function MaybeLink(props: { href: string; children: ReactNode }) { const { href, children } = props return href.startsWith('http') ? ( <>{children} diff --git a/web/lib/api/proxy.ts b/web/lib/api/proxy.ts index 6fa66873..0a386730 100644 --- a/web/lib/api/proxy.ts +++ b/web/lib/api/proxy.ts @@ -6,10 +6,10 @@ import fetch, { Headers, Response } from 'node-fetch' function getProxiedRequestHeaders(req: NextApiRequest, whitelist: string[]) { const result = new Headers() - for (let name of whitelist) { + for (const name of whitelist) { const v = req.headers[name.toLowerCase()] if (Array.isArray(v)) { - for (let vv of v) { + for (const vv of v) { result.append(name, vv) } } else if (v != null) { @@ -23,7 +23,7 @@ function getProxiedRequestHeaders(req: NextApiRequest, whitelist: string[]) { function getProxiedResponseHeaders(res: Response, whitelist: string[]) { const result: { [k: string]: string } = {} - for (let name of whitelist) { + for (const name of whitelist) { const v = res.headers.get(name) if (v != null) { result[name] = v diff --git a/web/lib/firebase/init.ts b/web/lib/firebase/init.ts index 379f7cb6..12f3d832 100644 --- a/web/lib/firebase/init.ts +++ b/web/lib/firebase/init.ts @@ -9,13 +9,15 @@ export const app = getApps().length ? getApp() : initializeApp(FIREBASE_CONFIG) export const db = getFirestore() export const functions = getFunctions() -const EMULATORS_STARTED = 'EMULATORS_STARTED' +declare global { + /* eslint-disable-next-line no-var */ + var EMULATORS_STARTED: boolean +} + function startEmulators() { // I don't like this but this is the only way to reconnect to the emulators without error, see: https://stackoverflow.com/questions/65066963/firebase-firestore-emulator-error-host-has-been-set-in-both-settings-and-usee - // @ts-ignore - if (!global[EMULATORS_STARTED]) { - // @ts-ignore - global[EMULATORS_STARTED] = true + if (!global.EMULATORS_STARTED) { + global.EMULATORS_STARTED = true connectFirestoreEmulator(db, 'localhost', 8080) connectFunctionsEmulator(functions, 'localhost', 5001) } diff --git a/web/lib/util/copy.ts b/web/lib/util/copy.ts index 47dc4dab..66314612 100644 --- a/web/lib/util/copy.ts +++ b/web/lib/util/copy.ts @@ -21,7 +21,7 @@ export function copyToClipboard(text: string) { document.queryCommandSupported('copy') ) { console.log('copy 3') - var textarea = document.createElement('textarea') + const textarea = document.createElement('textarea') textarea.textContent = text textarea.style.position = 'fixed' // Prevent scrolling to bottom of page in Microsoft Edge. document.body.appendChild(textarea) diff --git a/web/pages/_app.tsx b/web/pages/_app.tsx index 55618e4c..a833c933 100644 --- a/web/pages/_app.tsx +++ b/web/pages/_app.tsx @@ -13,12 +13,12 @@ function firstLine(msg: string) { function printBuildInfo() { // These are undefined if e.g. dev server if (process.env.NEXT_PUBLIC_VERCEL_ENV) { - let env = process.env.NEXT_PUBLIC_VERCEL_ENV - let msg = process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_MESSAGE - let owner = process.env.NEXT_PUBLIC_VERCEL_GIT_REPO_OWNER - let repo = process.env.NEXT_PUBLIC_VERCEL_GIT_REPO_SLUG - let sha = process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA - let url = `https://github.com/${owner}/${repo}/commit/${sha}` + const env = process.env.NEXT_PUBLIC_VERCEL_ENV + const msg = process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_MESSAGE + const owner = process.env.NEXT_PUBLIC_VERCEL_GIT_REPO_OWNER + const repo = process.env.NEXT_PUBLIC_VERCEL_GIT_REPO_SLUG + const sha = process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA + const url = `https://github.com/${owner}/${repo}/commit/${sha}` console.info(`Build: ${env} / ${firstLine(msg || '???')} / ${url}`) } } diff --git a/web/pages/admin.tsx b/web/pages/admin.tsx index db230e74..e916eb6f 100644 --- a/web/pages/admin.tsx +++ b/web/pages/admin.tsx @@ -19,28 +19,22 @@ function avatarHtml(avatarUrl: string) { } function UsersTable() { - let users = useUsers() - let privateUsers = usePrivateUsers() + const users = useUsers() + const privateUsers = usePrivateUsers() // Map private users by user id const privateUsersById = mapKeys(privateUsers, 'id') console.log('private users by id', privateUsersById) // For each user, set their email from the PrivateUser - users = users.map((user) => { - // @ts-ignore - user.email = privateUsersById[user.id]?.email - return user - }) - - // Sort users by createdTime descending, by default - users = users.sort((a, b) => b.createdTime - a.createdTime) + const fullUsers = users + .map((user) => { + return { email: privateUsersById[user.id]?.email, ...user } + }) + .sort((a, b) => b.createdTime - a.createdTime) function exportCsv() { - const csv = users - // @ts-ignore - .map((u) => [u.email, u.name].join(', ')) - .join('\n') + const csv = fullUsers.map((u) => [u.email, u.name].join(', ')).join('\n') const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' }) const url = URL.createObjectURL(blob) const a = document.createElement('a') @@ -108,27 +102,29 @@ function UsersTable() { } function ContractsTable() { - let contracts = useContracts() ?? [] + const contracts = useContracts() ?? [] + // Sort users by createdTime descending, by default - contracts.sort((a, b) => b.createdTime - a.createdTime) - // Render a clickable question. See https://gridjs.io/docs/examples/react-cells for docs - contracts.map((contract) => { - // @ts-ignore - contract.questionLink = r( - - ) - }) + const displayContracts = contracts + .sort((a, b) => b.createdTime - a.createdTime) + .map((contract) => { + // Render a clickable question. See https://gridjs.io/docs/examples/react-cells for docs + const questionLink = r( + + ) + return { questionLink, ...contract } + }) return ( corpus.toLowerCase().includes(word)) } diff --git a/web/pages/profile.tsx b/web/pages/profile.tsx index ea206f99..ac06eaf2 100644 --- a/web/pages/profile.tsx +++ b/web/pages/profile.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react' +import React, { useEffect, useState } from 'react' import { RefreshIcon } from '@heroicons/react/outline' import Router from 'next/router' @@ -21,7 +21,7 @@ import Textarea from 'react-expanding-textarea' function EditUserField(props: { user: User - field: 'bio' | 'bannerUrl' | 'twitterHandle' | 'discordHandle' + field: 'bio' | 'website' | 'bannerUrl' | 'twitterHandle' | 'discordHandle' label: string }) { const { user, field, label } = props @@ -220,18 +220,15 @@ export default function ProfilePage() { }} /> - {[ - ['bio', 'Bio'], - ['website', 'Website URL'], - ['twitterHandle', 'Twitter'], - ['discordHandle', 'Discord'], - ].map(([field, label]) => ( - + {( + [ + ['bio', 'Bio'], + ['website', 'Website URL'], + ['twitterHandle', 'Twitter'], + ['discordHandle', 'Discord'], + ] as const + ).map(([field, label]) => ( + ))} )} From 1e0845f4b99af6c071c0b5312894b01bb15d22bc Mon Sep 17 00:00:00 2001 From: Forrest Wolf Date: Thu, 26 May 2022 17:22:44 -0500 Subject: [PATCH 05/11] Replace some uses of `any` with more specific types (#344) * Add tsconfig.json for common * Prefer `const` over `let` over `var` * Kill dead code * Fix some trivial Typescript issues * Turn on Typescript linting in common except for no-explicit-any * Correctly specify tsconfig dir name in functions eslintrc * Give react children explicit types * Add explicit types to removeUndefinedProps * Create StripeSession type * Give event in Dropdown an explicit type * Revert "Give event in Dropdown an explicit type" This reverts commit 80604310f2ff320a62aa16dddfdb12546179f7e6. * Give bids in NewBidTable an explicit type * Cast results of removeUndefinedProps when neccessary * Fix type of JoinSpans * Revert "Cast results of removeUndefinedProps when neccessary" This reverts commit 5541617bc80b017a8f7ff0e399be7c2e909ba2f5. * Revert "Add explicit types to removeUndefinedProps" This reverts commit ccf8ffb0b534fbda2ed4bc567b1e43c7285692b0. * Give React children types everywhere * Give event a type * Give event correct type * Lint * Standardize React import Co-authored-by: Marshall Polaris --- functions/src/stripe.ts | 8 +++++--- web/components/SEO.tsx | 3 ++- web/components/advanced-panel.tsx | 4 ++-- web/components/avatar.tsx | 3 ++- web/components/confirmation-button.tsx | 4 ++-- web/components/join-spans.tsx | 4 ++-- web/components/layout/col.tsx | 4 ++-- web/components/layout/row.tsx | 3 ++- web/components/number-input.tsx | 3 ++- web/components/page.tsx | 2 +- web/components/site-link.tsx | 2 +- web/components/yes-no-selector.tsx | 4 ++-- web/pages/simulator.tsx | 2 +- 13 files changed, 26 insertions(+), 20 deletions(-) diff --git a/functions/src/stripe.ts b/functions/src/stripe.ts index a52b0fae..d32010a6 100644 --- a/functions/src/stripe.ts +++ b/functions/src/stripe.ts @@ -5,11 +5,13 @@ import Stripe from 'stripe' import { getPrivateUser, getUser, isProd, payUser } from './utils' import { sendThankYouEmail } from './emails' +export type StripeSession = Stripe.Event.Data.Object & { id: any, metadata: any} + export type StripeTransaction = { userId: string manticDollarQuantity: number sessionId: string - session: any + session: StripeSession timestamp: number } @@ -96,14 +98,14 @@ export const stripeWebhook = functions } if (event.type === 'checkout.session.completed') { - const session = event.data.object as any + const session = event.data.object as StripeSession await issueMoneys(session) } res.status(200).send('success') }) -const issueMoneys = async (session: any) => { +const issueMoneys = async (session: StripeSession) => { const { id: sessionId } = session const query = await firestore diff --git a/web/components/SEO.tsx b/web/components/SEO.tsx index 8987d671..11e24c99 100644 --- a/web/components/SEO.tsx +++ b/web/components/SEO.tsx @@ -1,3 +1,4 @@ +import { ReactNode } from 'react' import Head from 'next/head' export type OgCardProps = { @@ -35,7 +36,7 @@ export function SEO(props: { title: string description: string url?: string - children?: any[] + children?: ReactNode ogCardProps?: OgCardProps }) { const { title, description, url, children, ogCardProps } = props diff --git a/web/components/advanced-panel.tsx b/web/components/advanced-panel.tsx index 1a8cf389..51caba67 100644 --- a/web/components/advanced-panel.tsx +++ b/web/components/advanced-panel.tsx @@ -1,7 +1,7 @@ import clsx from 'clsx' -import { useState } from 'react' +import { useState, ReactNode } from 'react' -export function AdvancedPanel(props: { children: any }) { +export function AdvancedPanel(props: { children: ReactNode }) { const { children } = props const [collapsed, setCollapsed] = useState(true) diff --git a/web/components/avatar.tsx b/web/components/avatar.tsx index 706b61f3..8ae2a40d 100644 --- a/web/components/avatar.tsx +++ b/web/components/avatar.tsx @@ -1,5 +1,6 @@ import Router from 'next/router' import clsx from 'clsx' +import { MouseEvent } from 'react' import { UserCircleIcon } from '@heroicons/react/solid' export function Avatar(props: { @@ -15,7 +16,7 @@ export function Avatar(props: { const onClick = noLink && username ? undefined - : (e: any) => { + : (e: MouseEvent) => { e.stopPropagation() Router.push(`/${username}`) } diff --git a/web/components/confirmation-button.tsx b/web/components/confirmation-button.tsx index c23ec87c..e07b6dab 100644 --- a/web/components/confirmation-button.tsx +++ b/web/components/confirmation-button.tsx @@ -1,5 +1,5 @@ import clsx from 'clsx' -import { useState } from 'react' +import { ReactNode, useState } from 'react' import { Col } from './layout/col' import { Modal } from './layout/modal' import { Row } from './layout/row' @@ -20,7 +20,7 @@ export function ConfirmationButton(props: { className?: string } onSubmit: () => void - children: any + children: ReactNode }) { const { id, openModalBtn, cancelBtn, submitBtn, onSubmit, children } = props diff --git a/web/components/join-spans.tsx b/web/components/join-spans.tsx index c11b4eed..2f071c82 100644 --- a/web/components/join-spans.tsx +++ b/web/components/join-spans.tsx @@ -1,14 +1,14 @@ import { ReactNode } from 'react' export const JoinSpans = (props: { - children: any[] + children: ReactNode[] separator?: ReactNode }) => { const { separator } = props const children = props.children.filter((x) => !!x) if (children.length === 0) return <> - if (children.length === 1) return children[0] + if (children.length === 1) return <>{children[0]} if (children.length === 2) return ( <> diff --git a/web/components/layout/col.tsx b/web/components/layout/col.tsx index 00e85532..74fad186 100644 --- a/web/components/layout/col.tsx +++ b/web/components/layout/col.tsx @@ -1,8 +1,8 @@ import clsx from 'clsx' -import { CSSProperties, Ref } from 'react' +import { CSSProperties, Ref, ReactNode } from 'react' export function Col(props: { - children?: any + children?: ReactNode className?: string style?: CSSProperties ref?: Ref diff --git a/web/components/layout/row.tsx b/web/components/layout/row.tsx index 5ccccd7b..1c69c252 100644 --- a/web/components/layout/row.tsx +++ b/web/components/layout/row.tsx @@ -1,7 +1,8 @@ import clsx from 'clsx' +import { ReactNode } from 'react' export function Row(props: { - children?: any + children?: ReactNode className?: string id?: string }) { diff --git a/web/components/number-input.tsx b/web/components/number-input.tsx index c40fadeb..d7159fab 100644 --- a/web/components/number-input.tsx +++ b/web/components/number-input.tsx @@ -1,4 +1,5 @@ import clsx from 'clsx' +import { ReactNode } from 'react' import React from 'react' import { Col } from './layout/col' @@ -14,7 +15,7 @@ export function NumberInput(props: { inputClassName?: string // Needed to focus the amount input inputRef?: React.MutableRefObject - children?: any + children?: ReactNode }) { const { numberString, diff --git a/web/components/page.tsx b/web/components/page.tsx index 64830181..421722a3 100644 --- a/web/components/page.tsx +++ b/web/components/page.tsx @@ -9,7 +9,7 @@ export function Page(props: { assertUser?: 'signed-in' | 'signed-out' rightSidebar?: ReactNode suspend?: boolean - children?: any + children?: ReactNode }) { const { margin, assertUser, children, rightSidebar, suspend } = props diff --git a/web/components/site-link.tsx b/web/components/site-link.tsx index cff47ea5..8137eb08 100644 --- a/web/components/site-link.tsx +++ b/web/components/site-link.tsx @@ -4,7 +4,7 @@ import Link from 'next/link' export const SiteLink = (props: { href: string - children?: any + children?: ReactNode onClick?: () => void className?: string }) => { diff --git a/web/components/yes-no-selector.tsx b/web/components/yes-no-selector.tsx index 38723060..25bdaaeb 100644 --- a/web/components/yes-no-selector.tsx +++ b/web/components/yes-no-selector.tsx @@ -1,5 +1,5 @@ import clsx from 'clsx' -import React from 'react' +import React, { ReactNode } from 'react' import { formatMoney } from 'common/util/format' import { Col } from './layout/col' import { Row } from './layout/row' @@ -230,7 +230,7 @@ function Button(props: { className?: string onClick?: () => void color: 'green' | 'red' | 'blue' | 'yellow' | 'gray' - children?: any + children?: ReactNode }) { const { className, onClick, children, color } = props diff --git a/web/pages/simulator.tsx b/web/pages/simulator.tsx index bbf7408e..dcf44478 100644 --- a/web/pages/simulator.tsx +++ b/web/pages/simulator.tsx @@ -112,7 +112,7 @@ function TableRowEnd(props: { entry: Entry | null; isNew?: boolean }) { function NewBidTable(props: { steps: number - bids: any[] + bids: Array<{ yesBid: number; noBid: number }> setSteps: (steps: number) => void setBids: (bids: any[]) => void }) { From 3e25faf74c8418b740adc7d03ebb009e4f12a614 Mon Sep 17 00:00:00 2001 From: Marshall Polaris Date: Thu, 26 May 2022 22:07:56 -0700 Subject: [PATCH 06/11] Add script to backfill contract `collectedFees` --- functions/src/scripts/backfill-fees.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 functions/src/scripts/backfill-fees.ts diff --git a/functions/src/scripts/backfill-fees.ts b/functions/src/scripts/backfill-fees.ts new file mode 100644 index 00000000..9ca068d2 --- /dev/null +++ b/functions/src/scripts/backfill-fees.ts @@ -0,0 +1,25 @@ +// We have many old contracts without a collectedFees data structure. Let's fill them in. + +import * as admin from 'firebase-admin' +import { initAdmin } from './script-init' +import { noFees } from '../../../common/fees' + +initAdmin() +const firestore = admin.firestore() + +if (require.main === module) { + const contractsRef = firestore.collection('contracts') + contractsRef.get().then((contractsSnaps) => { + let n = 0 + console.log(`Loaded ${contractsSnaps.size} contracts.`) + contractsSnaps.forEach((ct) => { + const data = ct.data() + if (!('collectedFees' in data)) { + n += 1 + console.log(`Filling in missing fees on contract ${data.id}...`) + ct.ref.update({ collectedFees: noFees }) + } + }) + console.log(`Updated ${n} contracts.`) + }) +} From e5ce17c2ade2a1b36d69a7b049a8a249b5b09c81 Mon Sep 17 00:00:00 2001 From: Marshall Polaris Date: Fri, 27 May 2022 13:11:32 -0700 Subject: [PATCH 07/11] Fix up backfill script (and I ran it) --- functions/src/scripts/backfill-fees.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/functions/src/scripts/backfill-fees.ts b/functions/src/scripts/backfill-fees.ts index 9ca068d2..66bbc0c3 100644 --- a/functions/src/scripts/backfill-fees.ts +++ b/functions/src/scripts/backfill-fees.ts @@ -9,17 +9,15 @@ const firestore = admin.firestore() if (require.main === module) { const contractsRef = firestore.collection('contracts') - contractsRef.get().then((contractsSnaps) => { - let n = 0 + contractsRef.get().then(async (contractsSnaps) => { console.log(`Loaded ${contractsSnaps.size} contracts.`) - contractsSnaps.forEach((ct) => { - const data = ct.data() - if (!('collectedFees' in data)) { - n += 1 - console.log(`Filling in missing fees on contract ${data.id}...`) - ct.ref.update({ collectedFees: noFees }) - } + const needsFilling = contractsSnaps.docs.filter((ct) => { + return !('collectedFees' in ct.data()) }) - console.log(`Updated ${n} contracts.`) + console.log(`Found ${needsFilling.length} contracts to update.`) + await Promise.all( + needsFilling.map((ct) => ct.ref.update({ collectedFees: noFees })) + ) + console.log(`Updated all contracts.`) }) } From 15d203977a7938eba1bb85ebf7ace4a5af8dd894 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Fri, 27 May 2022 15:40:47 -0500 Subject: [PATCH 08/11] Portfolio: Consistently filter out contracts you have sold out of. --- web/components/bets-list.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx index 400a5f7f..e5165c3a 100644 --- a/web/components/bets-list.tsx +++ b/web/components/bets-list.tsx @@ -122,8 +122,7 @@ export function BetsList(props: { user: User }) { .reverse() .filter(FILTERS[filter]) .filter((c) => { - if (sort === 'profit') return true - + // TODO: Expose a user setting to toggle whether to show contracts you sold out of. // Filter out contracts where you don't have shares anymore. const metrics = contractsMetrics[c.id] return metrics.payout > 0 From 279b139556eb7d6b516f4aa1d7be01726e7856c6 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Fri, 27 May 2022 15:51:55 -0500 Subject: [PATCH 09/11] Add 'sold' filter option in portfolio page --- web/components/bets-list.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx index e5165c3a..c979899f 100644 --- a/web/components/bets-list.tsx +++ b/web/components/bets-list.tsx @@ -50,7 +50,7 @@ import { trackLatency } from 'web/lib/firebase/tracking' import { NumericContract } from 'common/contract' type BetSort = 'newest' | 'profit' | 'closeTime' | 'value' -type BetFilter = 'open' | 'closed' | 'resolved' | 'all' +type BetFilter = 'open' | 'sold' | 'closed' | 'resolved' | 'all' export function BetsList(props: { user: User }) { const { user } = props @@ -107,6 +107,7 @@ export function BetsList(props: { user: User }) { !FILTERS.resolved(c) && (c.closeTime ?? Infinity) < Date.now(), open: (c) => !(FILTERS.closed(c) || FILTERS.resolved(c)), all: () => true, + sold: () => true, } const SORTS: Record number> = { profit: (c) => contractsMetrics[c.id].profit, @@ -122,9 +123,14 @@ export function BetsList(props: { user: User }) { .reverse() .filter(FILTERS[filter]) .filter((c) => { - // TODO: Expose a user setting to toggle whether to show contracts you sold out of. - // Filter out contracts where you don't have shares anymore. + if (filter === 'all') return true + const metrics = contractsMetrics[c.id] + + // Filter for contracts you sold out of. + if (filter === 'sold') return metrics.payout === 0 + + // Filter for contracts where you currently have shares. return metrics.payout > 0 }) @@ -180,6 +186,7 @@ export function BetsList(props: { user: User }) { onChange={(e) => setFilter(e.target.value as BetFilter)} > + From 86625798cdd6992fe4951f7ed132fa98691ff91c Mon Sep 17 00:00:00 2001 From: Marshall Polaris Date: Fri, 27 May 2022 14:02:02 -0700 Subject: [PATCH 10/11] Clean up some mess related to nullable `collectedFees` (#352) * contract.collectedFees is no longer sometimes nonexistent * Fix typing issues around payouts code --- common/contract.ts | 1 + common/payouts-dpm.ts | 60 +++++-------------- common/payouts.ts | 21 ++++--- functions/src/sell-bet.ts | 2 +- functions/src/sell-shares.ts | 2 +- .../contract/contract-info-dialog.tsx | 2 +- 6 files changed, 31 insertions(+), 57 deletions(-) diff --git a/common/contract.ts b/common/contract.ts index e9b768ea..ac4848ec 100644 --- a/common/contract.ts +++ b/common/contract.ts @@ -46,6 +46,7 @@ export type Contract = FullContract< export type BinaryContract = FullContract export type FreeResponseContract = FullContract export type NumericContract = FullContract +export type AnyOutcome = Binary | Multi | FreeResponse | Numeric export type DPM = { mechanism: 'dpm-2' diff --git a/common/payouts-dpm.ts b/common/payouts-dpm.ts index c138d302..a3208ab3 100644 --- a/common/payouts-dpm.ts +++ b/common/payouts-dpm.ts @@ -2,18 +2,12 @@ import { sum, groupBy, sumBy, mapValues } from 'lodash' import { Bet, NumericBet } from './bet' import { deductDpmFees, getDpmProbability } from './calculate-dpm' -import { DPM, FreeResponse, FullContract, Multi } from './contract' -import { - DPM_CREATOR_FEE, - DPM_FEES, - DPM_PLATFORM_FEE, - Fees, - noFees, -} from './fees' +import { DPM, FreeResponse, FullContract, Multi, AnyOutcome } from './contract' +import { DPM_CREATOR_FEE, DPM_FEES, DPM_PLATFORM_FEE } from './fees' import { addObjects } from './util/object' export const getDpmCancelPayouts = ( - contract: FullContract, + contract: FullContract, bets: Bet[] ) => { const { pool } = contract @@ -31,13 +25,13 @@ export const getDpmCancelPayouts = ( payouts, creatorPayout: 0, liquidityPayouts: [], - collectedFees: contract.collectedFees ?? noFees, + collectedFees: contract.collectedFees, } } export const getDpmStandardPayouts = ( outcome: string, - contract: FullContract, + contract: FullContract, bets: Bet[] ) => { const winningBets = bets.filter((bet) => bet.outcome === outcome) @@ -57,17 +51,11 @@ export const getDpmStandardPayouts = ( const profits = sumBy(payouts, (po) => Math.max(0, po.profit)) const creatorFee = DPM_CREATOR_FEE * profits const platformFee = DPM_PLATFORM_FEE * profits - - const finalFees: Fees = { + const collectedFees = addObjects(contract.collectedFees, { creatorFee, platformFee, liquidityFee: 0, - } - - const collectedFees = addObjects( - finalFees, - contract.collectedFees ?? {} - ) + }) console.log( 'resolved', @@ -90,7 +78,7 @@ export const getDpmStandardPayouts = ( export const getNumericDpmPayouts = ( outcome: string, - contract: FullContract, + contract: FullContract, bets: NumericBet[] ) => { const totalShares = sumBy(bets, (bet) => bet.allOutcomeShares[outcome] ?? 0) @@ -115,17 +103,11 @@ export const getNumericDpmPayouts = ( const profits = sumBy(payouts, (po) => Math.max(0, po.profit)) const creatorFee = DPM_CREATOR_FEE * profits const platformFee = DPM_PLATFORM_FEE * profits - - const finalFees: Fees = { + const collectedFees = addObjects(contract.collectedFees, { creatorFee, platformFee, liquidityFee: 0, - } - - const collectedFees = addObjects( - finalFees, - contract.collectedFees ?? {} - ) + }) console.log( 'resolved numeric bucket: ', @@ -147,7 +129,7 @@ export const getNumericDpmPayouts = ( } export const getDpmMktPayouts = ( - contract: FullContract, + contract: FullContract, bets: Bet[], resolutionProbability?: number ) => { @@ -174,17 +156,11 @@ export const getDpmMktPayouts = ( const creatorFee = DPM_CREATOR_FEE * profits const platformFee = DPM_PLATFORM_FEE * profits - - const finalFees: Fees = { + const collectedFees = addObjects(contract.collectedFees, { creatorFee, platformFee, liquidityFee: 0, - } - - const collectedFees = addObjects( - finalFees, - contract.collectedFees ?? {} - ) + }) console.log( 'resolved MKT', @@ -233,17 +209,11 @@ export const getPayoutsMultiOutcome = ( const creatorFee = DPM_CREATOR_FEE * profits const platformFee = DPM_PLATFORM_FEE * profits - - const finalFees: Fees = { + const collectedFees = addObjects(contract.collectedFees, { creatorFee, platformFee, liquidityFee: 0, - } - - const collectedFees = addObjects( - finalFees, - contract.collectedFees ?? noFees - ) + }) console.log( 'resolved', diff --git a/common/payouts.ts b/common/payouts.ts index 68fb8694..54c850f0 100644 --- a/common/payouts.ts +++ b/common/payouts.ts @@ -9,6 +9,7 @@ import { FreeResponse, FullContract, Multi, + AnyOutcome, } from './contract' import { Fees } from './fees' import { LiquidityProvision } from './liquidity-provision' @@ -72,15 +73,17 @@ export const getPayouts = ( liquidities, resolutionProbability ) + } else if (contract.mechanism === 'dpm-2') { + return getDpmPayouts( + outcome, + resolutions, + contract, + bets, + resolutionProbability + ) + } else { + throw new Error('Unknown contract mechanism.') } - - return getDpmPayouts( - outcome, - resolutions, - contract, - bets, - resolutionProbability - ) } export const getFixedPayouts = ( @@ -112,7 +115,7 @@ export const getDpmPayouts = ( resolutions: { [outcome: string]: number }, - contract: Contract, + contract: FullContract, bets: Bet[], resolutionProbability?: number ): PayoutInfo => { diff --git a/functions/src/sell-bet.ts b/functions/src/sell-bet.ts index fff88716..39ed8017 100644 --- a/functions/src/sell-bet.ts +++ b/functions/src/sell-bet.ts @@ -78,7 +78,7 @@ export const sellBet = functions.runWith({ minInstances: 1 }).https.onCall( pool: newPool, totalShares: newTotalShares, totalBets: newTotalBets, - collectedFees: addObjects(fees ?? {}, collectedFees ?? {}), + collectedFees: addObjects(fees, collectedFees), volume: volume + Math.abs(newBet.amount), }) ) diff --git a/functions/src/sell-shares.ts b/functions/src/sell-shares.ts index 08e7d7c5..c4166b8b 100644 --- a/functions/src/sell-shares.ts +++ b/functions/src/sell-shares.ts @@ -101,7 +101,7 @@ export const sellShares = functions.runWith({ minInstances: 1 }).https.onCall( removeUndefinedProps({ pool: newPool, p: newP, - collectedFees: addObjects(fees ?? {}, collectedFees ?? {}), + collectedFees: addObjects(fees, collectedFees), volume: volume + Math.abs(newBet.amount), }) ) diff --git a/web/components/contract/contract-info-dialog.tsx b/web/components/contract/contract-info-dialog.tsx index 8a238727..28cd91f4 100644 --- a/web/components/contract/contract-info-dialog.tsx +++ b/web/components/contract/contract-info-dialog.tsx @@ -95,7 +95,7 @@ export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) { Creator earnings - {formatMoney(contract.collectedFees?.creatorFee ?? 0)} + {formatMoney(contract.collectedFees.creatorFee)} From 80d5607984c281c6676aa2b410b647cc6333dca8 Mon Sep 17 00:00:00 2001 From: Marshall Polaris Date: Fri, 27 May 2022 14:23:57 -0700 Subject: [PATCH 11/11] Revert fishy change regarding payouts --- common/contract.ts | 1 - common/payouts-dpm.ts | 10 +++++----- common/payouts.ts | 21 +++++++++------------ 3 files changed, 14 insertions(+), 18 deletions(-) diff --git a/common/contract.ts b/common/contract.ts index ac4848ec..e9b768ea 100644 --- a/common/contract.ts +++ b/common/contract.ts @@ -46,7 +46,6 @@ export type Contract = FullContract< export type BinaryContract = FullContract export type FreeResponseContract = FullContract export type NumericContract = FullContract -export type AnyOutcome = Binary | Multi | FreeResponse | Numeric export type DPM = { mechanism: 'dpm-2' diff --git a/common/payouts-dpm.ts b/common/payouts-dpm.ts index a3208ab3..300a906f 100644 --- a/common/payouts-dpm.ts +++ b/common/payouts-dpm.ts @@ -2,12 +2,12 @@ import { sum, groupBy, sumBy, mapValues } from 'lodash' import { Bet, NumericBet } from './bet' import { deductDpmFees, getDpmProbability } from './calculate-dpm' -import { DPM, FreeResponse, FullContract, Multi, AnyOutcome } from './contract' +import { DPM, FreeResponse, FullContract, Multi } from './contract' import { DPM_CREATOR_FEE, DPM_FEES, DPM_PLATFORM_FEE } from './fees' import { addObjects } from './util/object' export const getDpmCancelPayouts = ( - contract: FullContract, + contract: FullContract, bets: Bet[] ) => { const { pool } = contract @@ -31,7 +31,7 @@ export const getDpmCancelPayouts = ( export const getDpmStandardPayouts = ( outcome: string, - contract: FullContract, + contract: FullContract, bets: Bet[] ) => { const winningBets = bets.filter((bet) => bet.outcome === outcome) @@ -78,7 +78,7 @@ export const getDpmStandardPayouts = ( export const getNumericDpmPayouts = ( outcome: string, - contract: FullContract, + contract: FullContract, bets: NumericBet[] ) => { const totalShares = sumBy(bets, (bet) => bet.allOutcomeShares[outcome] ?? 0) @@ -129,7 +129,7 @@ export const getNumericDpmPayouts = ( } export const getDpmMktPayouts = ( - contract: FullContract, + contract: FullContract, bets: Bet[], resolutionProbability?: number ) => { diff --git a/common/payouts.ts b/common/payouts.ts index 54c850f0..68fb8694 100644 --- a/common/payouts.ts +++ b/common/payouts.ts @@ -9,7 +9,6 @@ import { FreeResponse, FullContract, Multi, - AnyOutcome, } from './contract' import { Fees } from './fees' import { LiquidityProvision } from './liquidity-provision' @@ -73,17 +72,15 @@ export const getPayouts = ( liquidities, resolutionProbability ) - } else if (contract.mechanism === 'dpm-2') { - return getDpmPayouts( - outcome, - resolutions, - contract, - bets, - resolutionProbability - ) - } else { - throw new Error('Unknown contract mechanism.') } + + return getDpmPayouts( + outcome, + resolutions, + contract, + bets, + resolutionProbability + ) } export const getFixedPayouts = ( @@ -115,7 +112,7 @@ export const getDpmPayouts = ( resolutions: { [outcome: string]: number }, - contract: FullContract, + contract: Contract, bets: Bet[], resolutionProbability?: number ): PayoutInfo => {