From 244bbc51b2e04cd83f435e4d7a646413c9ce1401 Mon Sep 17 00:00:00 2001 From: Marshall Polaris Date: Tue, 7 Jun 2022 14:08:56 -0700 Subject: [PATCH] Migrate `sellBet` cloud function to v2 `sellbet` (#438) * Migrate sellBet to v2 * Kill sellBet warmup requests * Point client at new v2 sellbet function * Clean up `getSellBetInfo` * Fix up functions index.ts --- common/envs/dev.ts | 1 + common/envs/prod.ts | 7 +- common/envs/theoremone.ts | 1 + common/sell-bet.ts | 17 +-- functions/src/index.ts | 4 +- functions/src/scripts/migrate-to-dpm-2.ts | 10 +- functions/src/sell-bet.ts | 125 +++++++++------------- web/components/bets-list.tsx | 18 +--- web/lib/firebase/api-call.ts | 4 + web/lib/firebase/fn-call.ts | 2 - 10 files changed, 77 insertions(+), 112 deletions(-) diff --git a/common/envs/dev.ts b/common/envs/dev.ts index 2c110861..151de075 100644 --- a/common/envs/dev.ts +++ b/common/envs/dev.ts @@ -15,6 +15,7 @@ export const DEV_CONFIG: EnvConfig = { functionEndpoints: { placebet: 'https://placebet-w3txbmd3ba-uc.a.run.app', sellshares: 'https://sellshares-w3txbmd3ba-uc.a.run.app', + sellbet: 'https://sellbet-w3txbmd3ba-uc.a.run.app', createmarket: 'https://createmarket-w3txbmd3ba-uc.a.run.app', }, } diff --git a/common/envs/prod.ts b/common/envs/prod.ts index 6538aa42..4352af64 100644 --- a/common/envs/prod.ts +++ b/common/envs/prod.ts @@ -1,4 +1,8 @@ -export type V2CloudFunction = 'placebet' | 'sellshares' | 'createmarket' +export type V2CloudFunction = + | 'placebet' + | 'sellbet' + | 'sellshares' + | 'createmarket' export type EnvConfig = { domain: string @@ -43,6 +47,7 @@ export const PROD_CONFIG: EnvConfig = { functionEndpoints: { placebet: 'https://placebet-nggbo3neva-uc.a.run.app', sellshares: 'https://sellshares-nggbo3neva-uc.a.run.app', + sellbet: 'https://sellbet-nggbo3neva-uc.a.run.app', createmarket: 'https://createmarket-nggbo3neva-uc.a.run.app', }, adminEmails: [ diff --git a/common/envs/theoremone.ts b/common/envs/theoremone.ts index a2ddba51..54ced3f4 100644 --- a/common/envs/theoremone.ts +++ b/common/envs/theoremone.ts @@ -16,6 +16,7 @@ export const THEOREMONE_CONFIG: EnvConfig = { functionEndpoints: { placebet: 'https://placebet-nggbo3neva-uc.a.run.app', sellshares: 'https://sellshares-nggbo3neva-uc.a.run.app', + sellbet: 'https://sellbet-nggbo3neva-uc.a.run.app', createmarket: 'https://createmarket-nggbo3neva-uc.a.run.app', }, adminEmails: [...PROD_CONFIG.adminEmails, 'david.glidden@theoremone.co'], diff --git a/common/sell-bet.ts b/common/sell-bet.ts index 3070399b..2e75e4e7 100644 --- a/common/sell-bet.ts +++ b/common/sell-bet.ts @@ -7,18 +7,12 @@ import { import { calculateCpmmSale, getCpmmProbability } from './calculate-cpmm' import { CPMMContract, DPMContract } from './contract' import { DPM_CREATOR_FEE, DPM_PLATFORM_FEE, Fees } from './fees' -import { User } from './user' export type CandidateBet = Omit -export const getSellBetInfo = ( - user: User, - bet: Bet, - contract: DPMContract, - newBetId: string -) => { +export const getSellBetInfo = (bet: Bet, contract: DPMContract) => { const { pool, totalShares, totalBets } = contract - const { id: betId, amount, shares, outcome, loanAmount } = bet + const { id: betId, amount, shares, outcome } = bet const adjShareValue = calculateDpmShareValue(contract, bet) @@ -56,9 +50,7 @@ export const getSellBetInfo = ( creatorFee ) - const newBet: Bet = { - id: newBetId, - userId: user.id, + const newBet: CandidateBet = { contractId: contract.id, amount: -adjShareValue, shares: -shares, @@ -73,14 +65,11 @@ export const getSellBetInfo = ( fees, } - const newBalance = user.balance + saleAmount - (loanAmount ?? 0) - return { newBet, newPool, newTotalShares, newTotalBets, - newBalance, fees, } } diff --git a/functions/src/index.ts b/functions/src/index.ts index 043b860f..f3099fec 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -7,8 +7,6 @@ admin.initializeApp() export * from './transact' export * from './resolve-market' export * from './stripe' -export * from './sell-bet' -export * from './sell-shares' export * from './create-user' export * from './create-fold' export * from './create-answer' @@ -33,4 +31,6 @@ export * from './on-follow-user' // v2 export * from './health' export * from './place-bet' +export * from './sell-bet' +export * from './sell-shares' export * from './create-contract' diff --git a/functions/src/scripts/migrate-to-dpm-2.ts b/functions/src/scripts/migrate-to-dpm-2.ts index 98e2f9fc..d62862e0 100644 --- a/functions/src/scripts/migrate-to-dpm-2.ts +++ b/functions/src/scripts/migrate-to-dpm-2.ts @@ -11,7 +11,6 @@ import { getDpmProbability, } from '../../../common/calculate-dpm' import { getSellBetInfo } from '../../../common/sell-bet' -import { User } from '../../../common/user' type DocRef = admin.firestore.DocumentReference @@ -105,8 +104,6 @@ async function recalculateContract( const soldBet = bets.find((b) => b.id === bet.sale?.betId) if (!soldBet) throw new Error('invalid sold bet' + bet.sale.betId) - const fakeUser = { id: soldBet.userId, balance: 0 } as User - const fakeContract: Contract = { ...contract, totalBets, @@ -116,11 +113,14 @@ async function recalculateContract( } const { newBet, newPool, newTotalShares, newTotalBets } = - getSellBetInfo(fakeUser, soldBet, fakeContract, bet.id) + getSellBetInfo(soldBet, fakeContract) + const betDoc = betsRef.doc(bet.id) + const userId = soldBet.userId newBet.createdTime = bet.createdTime console.log('sale bet', newBet) - if (isCommit) transaction.update(betsRef.doc(bet.id), newBet) + if (isCommit) + transaction.update(betDoc, { id: bet.id, userId, ...newBet }) pool = newPool totalShares = newTotalShares diff --git a/functions/src/sell-bet.ts b/functions/src/sell-bet.ts index 93fcb6a2..4bad754d 100644 --- a/functions/src/sell-bet.ts +++ b/functions/src/sell-bet.ts @@ -1,92 +1,73 @@ import * as admin from 'firebase-admin' -import * as functions from 'firebase-functions' +import { z } from 'zod' +import { APIError, newEndpoint, validate } from './api' import { Contract } from '../../common/contract' import { User } from '../../common/user' import { Bet } from '../../common/bet' import { getSellBetInfo } from '../../common/sell-bet' import { addObjects, removeUndefinedProps } from '../../common/util/object' -export const sellBet = functions.runWith({ minInstances: 1 }).https.onCall( - async ( - data: { - contractId: string - betId: string - }, - context - ) => { - const userId = context?.auth?.uid - if (!userId) return { status: 'error', message: 'Not authorized' } +const bodySchema = z.object({ + contractId: z.string(), + betId: z.string(), +}) - const { contractId, betId } = data +export const sellbet = newEndpoint(['POST'], async (req, [bettor, _]) => { + const { contractId, betId } = validate(bodySchema, req.body) - // run as transaction to prevent race conditions - return await firestore.runTransaction(async (transaction) => { - const userDoc = firestore.doc(`users/${userId}`) - const userSnap = await transaction.get(userDoc) - if (!userSnap.exists) - return { status: 'error', message: 'User not found' } - const user = userSnap.data() as User + // 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 - const contractDoc = firestore.doc(`contracts/${contractId}`) - const contractSnap = await transaction.get(contractDoc) - if (!contractSnap.exists) - return { status: 'error', message: 'Invalid contract' } - const contract = contractSnap.data() as Contract - const { closeTime, mechanism, collectedFees, volume } = contract + const contractDoc = firestore.doc(`contracts/${contractId}`) + const contractSnap = await transaction.get(contractDoc) + if (!contractSnap.exists) throw new APIError(400, 'Contract not found.') + const contract = contractSnap.data() as Contract - if (mechanism !== 'dpm-2') - return { - status: 'error', - message: 'Sell shares only works with mechanism dpm-2', - } + const { closeTime, mechanism, collectedFees, volume } = contract + if (mechanism !== 'dpm-2') + throw new APIError(400, 'You can only sell bets on DPM-2 contracts.') + if (closeTime && Date.now() > closeTime) + throw new APIError(400, 'Trading is closed.') - if (closeTime && Date.now() > closeTime) - return { status: 'error', message: 'Trading is closed' } + const betDoc = firestore.doc(`contracts/${contractId}/bets/${betId}`) + const betSnap = await transaction.get(betDoc) + if (!betSnap.exists) throw new APIError(400, 'Bet not found.') + const bet = betSnap.data() as Bet - const betDoc = firestore.doc(`contracts/${contractId}/bets/${betId}`) - const betSnap = await transaction.get(betDoc) - if (!betSnap.exists) return { status: 'error', message: 'Invalid bet' } - const bet = betSnap.data() as Bet + if (bettor.id !== bet.userId) + throw new APIError(400, 'The specified bet does not belong to you.') + if (bet.isSold) + throw new APIError(400, 'The specified bet is already sold.') - if (userId !== bet.userId) - return { status: 'error', message: 'Not authorized' } - if (bet.isSold) return { status: 'error', message: 'Bet already sold' } + const { newBet, newPool, newTotalShares, newTotalBets, fees } = + getSellBetInfo(bet, contract) - const newBetDoc = firestore - .collection(`contracts/${contractId}/bets`) - .doc() + /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ + const saleAmount = newBet.sale!.amount + const newBalance = user.balance + saleAmount - (bet.loanAmount ?? 0) + const newBetDoc = firestore.collection(`contracts/${contractId}/bets`).doc() - const { - newBet, - newPool, - newTotalShares, - newTotalBets, - newBalance, - fees, - } = getSellBetInfo(user, bet, contract, newBetDoc.id) + transaction.update(userDoc, { balance: newBalance }) + transaction.update(betDoc, { isSold: true }) + transaction.create(newBetDoc, { id: betDoc.id, userId: user.id, ...newBet }) + transaction.update( + contractDoc, + removeUndefinedProps({ + pool: newPool, + totalShares: newTotalShares, + totalBets: newTotalBets, + collectedFees: addObjects(fees, collectedFees), + volume: volume + Math.abs(newBet.amount), + }) + ) - if (!isFinite(newBalance)) { - throw new Error('Invalid user balance for ' + user.username) - } - transaction.update(userDoc, { balance: newBalance }) - - transaction.update(betDoc, { isSold: true }) - transaction.create(newBetDoc, newBet) - transaction.update( - contractDoc, - removeUndefinedProps({ - pool: newPool, - totalShares: newTotalShares, - totalBets: newTotalBets, - collectedFees: addObjects(fees, collectedFees), - volume: volume + Math.abs(newBet.amount), - }) - ) - - return { status: 'success' } - }) - } -) + return {} + }) +}) const firestore = admin.firestore() diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx index 506da013..a4e90c35 100644 --- a/web/components/bets-list.tsx +++ b/web/components/bets-list.tsx @@ -1,13 +1,5 @@ import Link from 'next/link' -import { - uniq, - groupBy, - mapValues, - sortBy, - partition, - sumBy, - throttle, -} from 'lodash' +import { uniq, groupBy, mapValues, sortBy, partition, sumBy } from 'lodash' import dayjs from 'dayjs' import { useEffect, useState } from 'react' import clsx from 'clsx' @@ -30,7 +22,7 @@ import { } from 'web/lib/firebase/contracts' import { Row } from './layout/row' import { UserLink } from './user-page' -import { sellBet } from 'web/lib/firebase/fn-call' +import { sellBet } from 'web/lib/firebase/api-call' import { ConfirmationButton } from './confirmation-button' import { OutcomeLabel, YesLabel, NoLabel } from './outcome-label' import { filterDefined } from 'common/util/array' @@ -647,13 +639,7 @@ function BetRow(props: { ) } -const warmUpSellBet = throttle(() => sellBet({}).catch(() => {}), 5000 /* ms */) - function SellButton(props: { contract: Contract; bet: Bet }) { - useEffect(() => { - warmUpSellBet() - }, []) - const { contract, bet } = props const { outcome, shares, loanAmount } = bet diff --git a/web/lib/firebase/api-call.ts b/web/lib/firebase/api-call.ts index 898ca3e7..4df1fccf 100644 --- a/web/lib/firebase/api-call.ts +++ b/web/lib/firebase/api-call.ts @@ -54,3 +54,7 @@ export function placeBet(params: any) { export function sellShares(params: any) { return call(getFunctionUrl('sellshares'), 'POST', params) } + +export function sellBet(params: any) { + return call(getFunctionUrl('sellbet'), 'POST', params) +} diff --git a/web/lib/firebase/fn-call.ts b/web/lib/firebase/fn-call.ts index 1cb259b1..198e59f8 100644 --- a/web/lib/firebase/fn-call.ts +++ b/web/lib/firebase/fn-call.ts @@ -20,8 +20,6 @@ export const transact = cloudFunction< { status: 'error' | 'success'; message?: string; txn?: Txn } >('transact') -export const sellBet = cloudFunction('sellBet') - export const createAnswer = cloudFunction< { contractId: string; text: string; amount: number }, {