Migrate sellShares cloud function to v2 sellshares (#440)

* Migrate `sellShares` to v2 `sellshares`

* Point client at new v2 sellshares function

* Clean up `getCpmmSellBetInfo`
This commit is contained in:
Marshall Polaris 2022-06-07 13:54:58 -07:00 committed by GitHub
parent 0f0390cb6a
commit 60e830974e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 98 additions and 123 deletions

View File

@ -14,6 +14,7 @@ export const DEV_CONFIG: EnvConfig = {
},
functionEndpoints: {
placebet: 'https://placebet-w3txbmd3ba-uc.a.run.app',
sellshares: 'https://sellshares-w3txbmd3ba-uc.a.run.app',
createmarket: 'https://createmarket-w3txbmd3ba-uc.a.run.app',
},
}

View File

@ -1,4 +1,4 @@
export type V2CloudFunction = 'placebet' | 'createmarket'
export type V2CloudFunction = 'placebet' | 'sellshares' | 'createmarket'
export type EnvConfig = {
domain: string
@ -42,6 +42,7 @@ export const PROD_CONFIG: EnvConfig = {
},
functionEndpoints: {
placebet: 'https://placebet-nggbo3neva-uc.a.run.app',
sellshares: 'https://sellshares-nggbo3neva-uc.a.run.app',
createmarket: 'https://createmarket-nggbo3neva-uc.a.run.app',
},
adminEmails: [

View File

@ -15,6 +15,7 @@ export const THEOREMONE_CONFIG: EnvConfig = {
// TODO: fill in real endpoints for T1
functionEndpoints: {
placebet: 'https://placebet-nggbo3neva-uc.a.run.app',
sellshares: 'https://sellshares-nggbo3neva-uc.a.run.app',
createmarket: 'https://createmarket-nggbo3neva-uc.a.run.app',
},
adminEmails: [...PROD_CONFIG.adminEmails, 'david.glidden@theoremone.co'],

View File

@ -9,6 +9,8 @@ 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,
@ -84,12 +86,10 @@ export const getSellBetInfo = (
}
export const getCpmmSellBetInfo = (
user: User,
shares: number,
outcome: 'YES' | 'NO',
contract: CPMMContract,
prevLoanAmount: number,
newBetId: string
prevLoanAmount: number
) => {
const { pool, p } = contract
@ -100,8 +100,6 @@ export const getCpmmSellBetInfo = (
)
const loanPaid = Math.min(prevLoanAmount, saleValue)
const netAmount = saleValue - loanPaid
const probBefore = getCpmmProbability(pool, p)
const probAfter = getCpmmProbability(newPool, p)
@ -115,9 +113,7 @@ export const getCpmmSellBetInfo = (
fees.creatorFee
)
const newBet: Bet = {
id: newBetId,
userId: user.id,
const newBet: CandidateBet<Bet> = {
contractId: contract.id,
amount: -saleValue,
shares: -shares,
@ -129,13 +125,10 @@ export const getCpmmSellBetInfo = (
fees,
}
const newBalance = user.balance + netAmount
return {
newBet,
newPool,
newP,
newBalance,
fees,
}
}

View File

@ -1,114 +1,90 @@
import { partition, sumBy } from 'lodash'
import * as admin from 'firebase-admin'
import * as functions from 'firebase-functions'
import { z } from 'zod'
import { BinaryContract } from '../../common/contract'
import { APIError, newEndpoint, validate } from './api'
import { Contract } from '../../common/contract'
import { User } from '../../common/user'
import { getCpmmSellBetInfo } from '../../common/sell-bet'
import { addObjects, removeUndefinedProps } from '../../common/util/object'
import { getValues } from './utils'
import { Bet } from '../../common/bet'
export const sellShares = functions.runWith({ minInstances: 1 }).https.onCall(
async (
data: {
contractId: string
shares: number
outcome: 'YES' | 'NO'
},
context
) => {
const userId = context?.auth?.uid
if (!userId) return { status: 'error', message: 'Not authorized' }
const bodySchema = z.object({
contractId: z.string(),
shares: z.number(),
outcome: z.enum(['YES', 'NO']),
})
const { contractId, shares, outcome } = data
export const sellshares = newEndpoint(['POST'], async (req, [bettor, _]) => {
const { contractId, shares, outcome } = 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 BinaryContract
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
const { closeTime, mechanism, collectedFees, volume } = contract
if (mechanism !== 'cpmm-1')
return {
status: 'error',
message: 'Sell shares only works with mechanism cpmm-1',
}
if (mechanism !== 'cpmm-1')
throw new APIError(400, 'You can only sell shares on CPMM-1 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 userBets = await getValues<Bet>(
contractDoc.collection('bets').where('userId', '==', bettor.id)
)
const userBets = await getValues<Bet>(
contractDoc.collection('bets').where('userId', '==', userId)
)
const prevLoanAmount = sumBy(userBets, (bet) => bet.loanAmount ?? 0)
const prevLoanAmount = sumBy(userBets, (bet) => bet.loanAmount ?? 0)
const [yesBets, noBets] = partition(
userBets ?? [],
(bet) => bet.outcome === 'YES'
)
const [yesShares, noShares] = [
sumBy(yesBets, (bet) => bet.shares),
sumBy(noBets, (bet) => bet.shares),
]
const [yesBets, noBets] = partition(
userBets ?? [],
(bet) => bet.outcome === 'YES'
)
const [yesShares, noShares] = [
sumBy(yesBets, (bet) => bet.shares),
sumBy(noBets, (bet) => bet.shares),
]
const maxShares = outcome === 'YES' ? yesShares : noShares
if (shares > maxShares + 0.000000000001)
throw new APIError(400, `You can only sell up to ${maxShares} shares.`)
const maxShares = outcome === 'YES' ? yesShares : noShares
if (shares > maxShares + 0.000000000001) {
return {
status: 'error',
message: `You can only sell ${maxShares} shares`,
}
}
const { newBet, newPool, newP, fees } = getCpmmSellBetInfo(
shares,
outcome,
contract,
prevLoanAmount
)
const newBetDoc = firestore
.collection(`contracts/${contractId}/bets`)
.doc()
if (!isFinite(newP)) {
throw new APIError(500, 'Trade rejected due to overflow error.')
}
const { newBet, newPool, newP, newBalance, fees } = getCpmmSellBetInfo(
user,
shares,
outcome,
contract,
prevLoanAmount,
newBetDoc.id
)
const newBetDoc = firestore.collection(`contracts/${contractId}/bets`).doc()
const newBalance = user.balance - newBet.amount + (newBet.loanAmount ?? 0)
const userId = user.id
if (!isFinite(newP)) {
return {
status: 'error',
message: 'Trade rejected due to overflow error.',
}
}
transaction.update(userDoc, { balance: newBalance })
transaction.create(newBetDoc, { id: newBetDoc.id, userId, ...newBet })
transaction.update(
contractDoc,
removeUndefinedProps({
pool: newPool,
p: newP,
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.create(newBetDoc, newBet)
transaction.update(
contractDoc,
removeUndefinedProps({
pool: newPool,
p: newP,
collectedFees: addObjects(fees, collectedFees),
volume: volume + Math.abs(newBet.amount),
})
)
return { status: 'success' }
})
}
)
return { status: 'success' }
})
})
const firestore = admin.firestore()

View File

@ -17,7 +17,7 @@ import { Title } from './title'
import { User } from 'web/lib/firebase/users'
import { Bet } from 'common/bet'
import { APIError, placeBet } from 'web/lib/firebase/api-call'
import { sellShares } from 'web/lib/firebase/fn-call'
import { sellShares } from 'web/lib/firebase/api-call'
import { AmountInput, BuyAmountInput } from './amount-input'
import { InfoTooltip } from './info-tooltip'
import { BinaryOutcomeLabel } from './outcome-label'
@ -398,23 +398,27 @@ export function SellPanel(props: {
// Sell all shares if remaining shares would be < 1
const sellAmount = amount === Math.floor(shares) ? shares : amount
const result = await sellShares({
await sellShares({
shares: sellAmount,
outcome: sharesOutcome,
contractId: contract.id,
}).then((r) => r.data)
console.log('Sold shares. Result:', result)
if (result?.status === 'success') {
setIsSubmitting(false)
setWasSubmitted(true)
setAmount(undefined)
if (onSellSuccess) onSellSuccess()
} else {
setError(result?.message || 'Error selling')
setIsSubmitting(false)
}
})
.then((r) => {
console.log('Sold shares. Result:', r)
setIsSubmitting(false)
setWasSubmitted(true)
setAmount(undefined)
if (onSellSuccess) onSellSuccess()
})
.catch((e) => {
if (e instanceof APIError) {
setError(e.toString())
} else {
console.error(e)
setError('Error selling')
}
setIsSubmitting(false)
})
}
const initialProb = getProbability(contract)

View File

@ -22,7 +22,7 @@ import TriangleFillIcon from 'web/lib/icons/triangle-fill-icon'
import { Col } from '../layout/col'
import { OUTCOME_TO_COLOR } from '../outcome-label'
import { useSaveShares } from '../use-save-shares'
import { sellShares } from 'web/lib/firebase/fn-call'
import { sellShares } from 'web/lib/firebase/api-call'
import { calculateCpmmSale, getCpmmProbability } from 'common/calculate-cpmm'
const BET_SIZE = 10

View File

@ -50,3 +50,7 @@ export function createMarket(params: any) {
export function placeBet(params: any) {
return call(getFunctionUrl('placebet'), 'POST', params)
}
export function sellShares(params: any) {
return call(getFunctionUrl('sellshares'), 'POST', params)
}

View File

@ -22,11 +22,6 @@ export const transact = cloudFunction<
export const sellBet = cloudFunction('sellBet')
export const sellShares = cloudFunction<
{ contractId: string; shares: number; outcome: 'YES' | 'NO' },
{ status: 'error' | 'success'; message?: string }
>('sellShares')
export const createAnswer = cloudFunction<
{ contractId: string; text: string; amount: number },
{