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: { |   functionEndpoints: { | ||||||
|     placebet: 'https://placebet-w3txbmd3ba-uc.a.run.app', |     placebet: 'https://placebet-w3txbmd3ba-uc.a.run.app', | ||||||
|     sellshares: 'https://sellshares-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', |     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 = { | export type EnvConfig = { | ||||||
|   domain: string |   domain: string | ||||||
|  | @ -43,6 +47,7 @@ export const PROD_CONFIG: EnvConfig = { | ||||||
|   functionEndpoints: { |   functionEndpoints: { | ||||||
|     placebet: 'https://placebet-nggbo3neva-uc.a.run.app', |     placebet: 'https://placebet-nggbo3neva-uc.a.run.app', | ||||||
|     sellshares: 'https://sellshares-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', |     createmarket: 'https://createmarket-nggbo3neva-uc.a.run.app', | ||||||
|   }, |   }, | ||||||
|   adminEmails: [ |   adminEmails: [ | ||||||
|  |  | ||||||
|  | @ -16,6 +16,7 @@ export const THEOREMONE_CONFIG: EnvConfig = { | ||||||
|   functionEndpoints: { |   functionEndpoints: { | ||||||
|     placebet: 'https://placebet-nggbo3neva-uc.a.run.app', |     placebet: 'https://placebet-nggbo3neva-uc.a.run.app', | ||||||
|     sellshares: 'https://sellshares-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', |     createmarket: 'https://createmarket-nggbo3neva-uc.a.run.app', | ||||||
|   }, |   }, | ||||||
|   adminEmails: [...PROD_CONFIG.adminEmails, 'david.glidden@theoremone.co'], |   adminEmails: [...PROD_CONFIG.adminEmails, 'david.glidden@theoremone.co'], | ||||||
|  |  | ||||||
|  | @ -7,18 +7,12 @@ import { | ||||||
| import { calculateCpmmSale, getCpmmProbability } from './calculate-cpmm' | import { calculateCpmmSale, getCpmmProbability } from './calculate-cpmm' | ||||||
| import { CPMMContract, DPMContract } from './contract' | import { CPMMContract, DPMContract } from './contract' | ||||||
| import { DPM_CREATOR_FEE, DPM_PLATFORM_FEE, Fees } from './fees' | 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 type CandidateBet<T extends Bet> = Omit<T, 'id' | 'userId'> | ||||||
| 
 | 
 | ||||||
| export const getSellBetInfo = ( | export const getSellBetInfo = (bet: Bet, contract: DPMContract) => { | ||||||
|   user: User, |  | ||||||
|   bet: Bet, |  | ||||||
|   contract: DPMContract, |  | ||||||
|   newBetId: string |  | ||||||
| ) => { |  | ||||||
|   const { pool, totalShares, totalBets } = contract |   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) |   const adjShareValue = calculateDpmShareValue(contract, bet) | ||||||
| 
 | 
 | ||||||
|  | @ -56,9 +50,7 @@ export const getSellBetInfo = ( | ||||||
|     creatorFee |     creatorFee | ||||||
|   ) |   ) | ||||||
| 
 | 
 | ||||||
|   const newBet: Bet = { |   const newBet: CandidateBet<Bet> = { | ||||||
|     id: newBetId, |  | ||||||
|     userId: user.id, |  | ||||||
|     contractId: contract.id, |     contractId: contract.id, | ||||||
|     amount: -adjShareValue, |     amount: -adjShareValue, | ||||||
|     shares: -shares, |     shares: -shares, | ||||||
|  | @ -73,14 +65,11 @@ export const getSellBetInfo = ( | ||||||
|     fees, |     fees, | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   const newBalance = user.balance + saleAmount - (loanAmount ?? 0) |  | ||||||
| 
 |  | ||||||
|   return { |   return { | ||||||
|     newBet, |     newBet, | ||||||
|     newPool, |     newPool, | ||||||
|     newTotalShares, |     newTotalShares, | ||||||
|     newTotalBets, |     newTotalBets, | ||||||
|     newBalance, |  | ||||||
|     fees, |     fees, | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -7,8 +7,6 @@ admin.initializeApp() | ||||||
| export * from './transact' | export * from './transact' | ||||||
| export * from './resolve-market' | export * from './resolve-market' | ||||||
| export * from './stripe' | export * from './stripe' | ||||||
| export * from './sell-bet' |  | ||||||
| export * from './sell-shares' |  | ||||||
| export * from './create-user' | export * from './create-user' | ||||||
| export * from './create-fold' | export * from './create-fold' | ||||||
| export * from './create-answer' | export * from './create-answer' | ||||||
|  | @ -33,4 +31,6 @@ export * from './on-follow-user' | ||||||
| // v2
 | // v2
 | ||||||
| export * from './health' | export * from './health' | ||||||
| export * from './place-bet' | export * from './place-bet' | ||||||
|  | export * from './sell-bet' | ||||||
|  | export * from './sell-shares' | ||||||
| export * from './create-contract' | export * from './create-contract' | ||||||
|  |  | ||||||
|  | @ -11,7 +11,6 @@ import { | ||||||
|   getDpmProbability, |   getDpmProbability, | ||||||
| } from '../../../common/calculate-dpm' | } from '../../../common/calculate-dpm' | ||||||
| import { getSellBetInfo } from '../../../common/sell-bet' | import { getSellBetInfo } from '../../../common/sell-bet' | ||||||
| import { User } from '../../../common/user' |  | ||||||
| 
 | 
 | ||||||
| type DocRef = admin.firestore.DocumentReference | type DocRef = admin.firestore.DocumentReference | ||||||
| 
 | 
 | ||||||
|  | @ -105,8 +104,6 @@ async function recalculateContract( | ||||||
|         const soldBet = bets.find((b) => b.id === bet.sale?.betId) |         const soldBet = bets.find((b) => b.id === bet.sale?.betId) | ||||||
|         if (!soldBet) throw new Error('invalid sold bet' + 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 = { |         const fakeContract: Contract = { | ||||||
|           ...contract, |           ...contract, | ||||||
|           totalBets, |           totalBets, | ||||||
|  | @ -116,11 +113,14 @@ async function recalculateContract( | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const { newBet, newPool, newTotalShares, newTotalBets } = |         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 |         newBet.createdTime = bet.createdTime | ||||||
|         console.log('sale bet', newBet) |         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 |         pool = newPool | ||||||
|         totalShares = newTotalShares |         totalShares = newTotalShares | ||||||
|  |  | ||||||
|  | @ -1,78 +1,60 @@ | ||||||
| import * as admin from 'firebase-admin' | 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 { Contract } from '../../common/contract' | ||||||
| import { User } from '../../common/user' | import { User } from '../../common/user' | ||||||
| import { Bet } from '../../common/bet' | import { Bet } from '../../common/bet' | ||||||
| import { getSellBetInfo } from '../../common/sell-bet' | import { getSellBetInfo } from '../../common/sell-bet' | ||||||
| import { addObjects, removeUndefinedProps } from '../../common/util/object' | import { addObjects, removeUndefinedProps } from '../../common/util/object' | ||||||
| 
 | 
 | ||||||
| export const sellBet = functions.runWith({ minInstances: 1 }).https.onCall( | const bodySchema = z.object({ | ||||||
|   async ( |   contractId: z.string(), | ||||||
|     data: { |   betId: z.string(), | ||||||
|       contractId: string | }) | ||||||
|       betId: string |  | ||||||
|     }, |  | ||||||
|     context |  | ||||||
|   ) => { |  | ||||||
|     const userId = context?.auth?.uid |  | ||||||
|     if (!userId) return { status: 'error', message: 'Not authorized' } |  | ||||||
| 
 | 
 | ||||||
|     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
 |   // run as transaction to prevent race conditions
 | ||||||
|   return await firestore.runTransaction(async (transaction) => { |   return await firestore.runTransaction(async (transaction) => { | ||||||
|       const userDoc = firestore.doc(`users/${userId}`) |     const userDoc = firestore.doc(`users/${bettor.id}`) | ||||||
|     const userSnap = await transaction.get(userDoc) |     const userSnap = await transaction.get(userDoc) | ||||||
|       if (!userSnap.exists) |     if (!userSnap.exists) throw new APIError(400, 'User not found.') | ||||||
|         return { status: 'error', message: 'User not found' } |  | ||||||
|     const user = userSnap.data() as User |     const user = userSnap.data() as User | ||||||
| 
 | 
 | ||||||
|     const contractDoc = firestore.doc(`contracts/${contractId}`) |     const contractDoc = firestore.doc(`contracts/${contractId}`) | ||||||
|     const contractSnap = await transaction.get(contractDoc) |     const contractSnap = await transaction.get(contractDoc) | ||||||
|       if (!contractSnap.exists) |     if (!contractSnap.exists) throw new APIError(400, 'Contract not found.') | ||||||
|         return { status: 'error', message: 'Invalid contract' } |  | ||||||
|     const contract = contractSnap.data() as Contract |     const contract = contractSnap.data() as Contract | ||||||
|  | 
 | ||||||
|     const { closeTime, mechanism, collectedFees, volume } = contract |     const { closeTime, mechanism, collectedFees, volume } = contract | ||||||
| 
 |  | ||||||
|     if (mechanism !== 'dpm-2') |     if (mechanism !== 'dpm-2') | ||||||
|         return { |       throw new APIError(400, 'You can only sell bets on DPM-2 contracts.') | ||||||
|           status: 'error', |  | ||||||
|           message: 'Sell shares only works with mechanism dpm-2', |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|     if (closeTime && Date.now() > closeTime) |     if (closeTime && Date.now() > closeTime) | ||||||
|         return { status: 'error', message: 'Trading is closed' } |       throw new APIError(400, 'Trading is closed.') | ||||||
| 
 | 
 | ||||||
|     const betDoc = firestore.doc(`contracts/${contractId}/bets/${betId}`) |     const betDoc = firestore.doc(`contracts/${contractId}/bets/${betId}`) | ||||||
|     const betSnap = await transaction.get(betDoc) |     const betSnap = await transaction.get(betDoc) | ||||||
|       if (!betSnap.exists) return { status: 'error', message: 'Invalid bet' } |     if (!betSnap.exists) throw new APIError(400, 'Bet not found.') | ||||||
|     const bet = betSnap.data() as Bet |     const bet = betSnap.data() as Bet | ||||||
| 
 | 
 | ||||||
|       if (userId !== bet.userId) |     if (bettor.id !== bet.userId) | ||||||
|         return { status: 'error', message: 'Not authorized' } |       throw new APIError(400, 'The specified bet does not belong to you.') | ||||||
|       if (bet.isSold) return { status: 'error', message: 'Bet already sold' } |     if (bet.isSold) | ||||||
|  |       throw new APIError(400, 'The specified bet is already sold.') | ||||||
| 
 | 
 | ||||||
|       const newBetDoc = firestore |     const { newBet, newPool, newTotalShares, newTotalBets, fees } = | ||||||
|         .collection(`contracts/${contractId}/bets`) |       getSellBetInfo(bet, contract) | ||||||
|         .doc() |  | ||||||
| 
 | 
 | ||||||
|       const { |     /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ | ||||||
|         newBet, |     const saleAmount = newBet.sale!.amount | ||||||
|         newPool, |     const newBalance = user.balance + saleAmount - (bet.loanAmount ?? 0) | ||||||
|         newTotalShares, |     const newBetDoc = firestore.collection(`contracts/${contractId}/bets`).doc() | ||||||
|         newTotalBets, |  | ||||||
|         newBalance, |  | ||||||
|         fees, |  | ||||||
|       } = getSellBetInfo(user, bet, contract, newBetDoc.id) |  | ||||||
| 
 | 
 | ||||||
|       if (!isFinite(newBalance)) { |  | ||||||
|         throw new Error('Invalid user balance for ' + user.username) |  | ||||||
|       } |  | ||||||
|     transaction.update(userDoc, { balance: newBalance }) |     transaction.update(userDoc, { balance: newBalance }) | ||||||
| 
 |  | ||||||
|     transaction.update(betDoc, { isSold: true }) |     transaction.update(betDoc, { isSold: true }) | ||||||
|       transaction.create(newBetDoc, newBet) |     transaction.create(newBetDoc, { id: betDoc.id, userId: user.id, ...newBet }) | ||||||
|     transaction.update( |     transaction.update( | ||||||
|       contractDoc, |       contractDoc, | ||||||
|       removeUndefinedProps({ |       removeUndefinedProps({ | ||||||
|  | @ -84,9 +66,8 @@ export const sellBet = functions.runWith({ minInstances: 1 }).https.onCall( | ||||||
|       }) |       }) | ||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
|       return { status: 'success' } |     return {} | ||||||
|   }) |   }) | ||||||
|   } | }) | ||||||
| ) |  | ||||||
| 
 | 
 | ||||||
| const firestore = admin.firestore() | const firestore = admin.firestore() | ||||||
|  |  | ||||||
|  | @ -1,13 +1,5 @@ | ||||||
| import Link from 'next/link' | import Link from 'next/link' | ||||||
| import { | import { uniq, groupBy, mapValues, sortBy, partition, sumBy } from 'lodash' | ||||||
|   uniq, |  | ||||||
|   groupBy, |  | ||||||
|   mapValues, |  | ||||||
|   sortBy, |  | ||||||
|   partition, |  | ||||||
|   sumBy, |  | ||||||
|   throttle, |  | ||||||
| } from 'lodash' |  | ||||||
| import dayjs from 'dayjs' | import dayjs from 'dayjs' | ||||||
| import { useEffect, useState } from 'react' | import { useEffect, useState } from 'react' | ||||||
| import clsx from 'clsx' | import clsx from 'clsx' | ||||||
|  | @ -30,7 +22,7 @@ import { | ||||||
| } from 'web/lib/firebase/contracts' | } from 'web/lib/firebase/contracts' | ||||||
| import { Row } from './layout/row' | import { Row } from './layout/row' | ||||||
| import { UserLink } from './user-page' | 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 { ConfirmationButton } from './confirmation-button' | ||||||
| import { OutcomeLabel, YesLabel, NoLabel } from './outcome-label' | import { OutcomeLabel, YesLabel, NoLabel } from './outcome-label' | ||||||
| import { filterDefined } from 'common/util/array' | 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 }) { | function SellButton(props: { contract: Contract; bet: Bet }) { | ||||||
|   useEffect(() => { |  | ||||||
|     warmUpSellBet() |  | ||||||
|   }, []) |  | ||||||
| 
 |  | ||||||
|   const { contract, bet } = props |   const { contract, bet } = props | ||||||
|   const { outcome, shares, loanAmount } = bet |   const { outcome, shares, loanAmount } = bet | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -54,3 +54,7 @@ export function placeBet(params: any) { | ||||||
| export function sellShares(params: any) { | export function sellShares(params: any) { | ||||||
|   return call(getFunctionUrl('sellshares'), 'POST', params) |   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 } |   { status: 'error' | 'success'; message?: string; txn?: Txn } | ||||||
| >('transact') | >('transact') | ||||||
| 
 | 
 | ||||||
| export const sellBet = cloudFunction('sellBet') |  | ||||||
| 
 |  | ||||||
| export const createAnswer = cloudFunction< | export const createAnswer = cloudFunction< | ||||||
|   { contractId: string; text: string; amount: number }, |   { contractId: string; text: string; amount: number }, | ||||||
|   { |   { | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user