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: { 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',
createmarket: 'https://createmarket-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 = { export type EnvConfig = {
domain: string domain: string
@ -42,6 +42,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',
createmarket: 'https://createmarket-nggbo3neva-uc.a.run.app', createmarket: 'https://createmarket-nggbo3neva-uc.a.run.app',
}, },
adminEmails: [ adminEmails: [

View File

@ -15,6 +15,7 @@ export const THEOREMONE_CONFIG: EnvConfig = {
// TODO: fill in real endpoints for T1 // TODO: fill in real endpoints for T1
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',
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'],

View File

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

View File

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

View File

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

View File

@ -50,3 +50,7 @@ export function createMarket(params: any) {
export function placeBet(params: any) { export function placeBet(params: any) {
return call(getFunctionUrl('placebet'), 'POST', params) 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 sellBet = cloudFunction('sellBet')
export const sellShares = cloudFunction<
{ contractId: string; shares: number; outcome: 'YES' | 'NO' },
{ status: 'error' | 'success'; message?: string }
>('sellShares')
export const createAnswer = cloudFunction< export const createAnswer = cloudFunction<
{ contractId: string; text: string; amount: number }, { contractId: string; text: string; amount: number },
{ {