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
This commit is contained in:
		
							parent
							
								
									60e830974e
								
							
						
					
					
						commit
						244bbc51b2
					
				|  | @ -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', | ||||
|   }, | ||||
| } | ||||
|  |  | |||
|  | @ -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: [ | ||||
|  |  | |||
|  | @ -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'], | ||||
|  |  | |||
|  | @ -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<T extends Bet> = Omit<T, 'id' | 'userId'> | ||||
| 
 | ||||
| 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<Bet> = { | ||||
|     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, | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -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' | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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() | ||||
|  |  | |||
|  | @ -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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -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) | ||||
| } | ||||
|  |  | |||
|  | @ -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 }, | ||||
|   { | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	Block a user