2021-12-11 00:06:17 +00:00
|
|
|
import * as admin from 'firebase-admin'
|
2022-05-26 21:37:51 +00:00
|
|
|
import { z } from 'zod'
|
2021-12-11 00:06:17 +00:00
|
|
|
|
2022-05-26 21:37:51 +00:00
|
|
|
import { APIError, newEndpoint, validate } from './api'
|
2022-06-10 01:25:05 +00:00
|
|
|
import { Contract, CPMM_MIN_POOL_QTY } from '../../common/contract'
|
2022-05-15 17:39:42 +00:00
|
|
|
import { User } from '../../common/user'
|
2022-03-02 03:31:48 +00:00
|
|
|
import {
|
2022-05-26 21:37:51 +00:00
|
|
|
BetInfo,
|
2022-03-15 22:27:51 +00:00
|
|
|
getNewBinaryCpmmBetInfo,
|
|
|
|
getNewBinaryDpmBetInfo,
|
2022-03-02 03:31:48 +00:00
|
|
|
getNewMultiBetInfo,
|
2022-05-19 17:42:03 +00:00
|
|
|
getNumericBetsInfo,
|
2022-05-15 17:39:42 +00:00
|
|
|
} from '../../common/new-bet'
|
|
|
|
import { addObjects, removeUndefinedProps } from '../../common/util/object'
|
2022-03-15 22:27:51 +00:00
|
|
|
import { redeemShares } from './redeem-shares'
|
2022-06-17 03:57:03 +00:00
|
|
|
import { log } from './utils'
|
2021-12-11 00:06:17 +00:00
|
|
|
|
2022-05-26 21:37:51 +00:00
|
|
|
const bodySchema = z.object({
|
|
|
|
contractId: z.string(),
|
|
|
|
amount: z.number().gte(1),
|
|
|
|
})
|
2022-05-17 04:43:40 +00:00
|
|
|
|
2022-05-26 21:37:51 +00:00
|
|
|
const binarySchema = z.object({
|
|
|
|
outcome: z.enum(['YES', 'NO']),
|
|
|
|
})
|
2022-05-17 04:43:40 +00:00
|
|
|
|
2022-05-26 21:37:51 +00:00
|
|
|
const freeResponseSchema = z.object({
|
|
|
|
outcome: z.string(),
|
|
|
|
})
|
2022-05-17 04:43:40 +00:00
|
|
|
|
2022-05-26 21:37:51 +00:00
|
|
|
const numericSchema = z.object({
|
|
|
|
outcome: z.string(),
|
|
|
|
value: z.number(),
|
|
|
|
})
|
2022-05-17 04:43:40 +00:00
|
|
|
|
2022-06-29 23:47:06 +00:00
|
|
|
export const placebet = newEndpoint({}, async (req, auth) => {
|
2022-06-17 03:57:03 +00:00
|
|
|
log('Inside endpoint handler.')
|
2022-05-26 21:37:51 +00:00
|
|
|
const { amount, contractId } = validate(bodySchema, req.body)
|
|
|
|
|
|
|
|
const result = await firestore.runTransaction(async (trans) => {
|
2022-06-17 03:57:03 +00:00
|
|
|
log('Inside main transaction.')
|
2022-05-26 21:37:51 +00:00
|
|
|
const contractDoc = firestore.doc(`contracts/${contractId}`)
|
2022-06-11 00:51:55 +00:00
|
|
|
const userDoc = firestore.doc(`users/${auth.uid}`)
|
|
|
|
const [contractSnap, userSnap] = await Promise.all([
|
|
|
|
trans.get(contractDoc),
|
|
|
|
trans.get(userDoc),
|
|
|
|
])
|
2022-05-26 21:37:51 +00:00
|
|
|
if (!contractSnap.exists) throw new APIError(400, 'Contract not found.')
|
2022-06-11 00:51:55 +00:00
|
|
|
if (!userSnap.exists) throw new APIError(400, 'User not found.')
|
2022-06-17 03:57:03 +00:00
|
|
|
log('Loaded user and contract snapshots.')
|
2022-06-11 00:51:55 +00:00
|
|
|
|
2022-05-26 21:37:51 +00:00
|
|
|
const contract = contractSnap.data() as Contract
|
2022-06-11 00:51:55 +00:00
|
|
|
const user = userSnap.data() as User
|
|
|
|
if (user.balance < amount) throw new APIError(400, 'Insufficient balance.')
|
2022-05-26 21:37:51 +00:00
|
|
|
|
|
|
|
const loanAmount = 0
|
|
|
|
const { closeTime, outcomeType, mechanism, collectedFees, volume } =
|
|
|
|
contract
|
|
|
|
if (closeTime && Date.now() > closeTime)
|
|
|
|
throw new APIError(400, 'Trading is closed.')
|
|
|
|
|
|
|
|
const {
|
|
|
|
newBet,
|
|
|
|
newPool,
|
|
|
|
newTotalShares,
|
|
|
|
newTotalBets,
|
|
|
|
newTotalLiquidity,
|
|
|
|
newP,
|
|
|
|
} = await (async (): Promise<BetInfo> => {
|
|
|
|
if (outcomeType == 'BINARY' && mechanism == 'dpm-2') {
|
|
|
|
const { outcome } = validate(binarySchema, req.body)
|
|
|
|
return getNewBinaryDpmBetInfo(outcome, amount, contract, loanAmount)
|
|
|
|
} else if (outcomeType == 'BINARY' && mechanism == 'cpmm-1') {
|
|
|
|
const { outcome } = validate(binarySchema, req.body)
|
|
|
|
return getNewBinaryCpmmBetInfo(outcome, amount, contract, loanAmount)
|
|
|
|
} else if (outcomeType == 'FREE_RESPONSE' && mechanism == 'dpm-2') {
|
|
|
|
const { outcome } = validate(freeResponseSchema, req.body)
|
|
|
|
const answerDoc = contractDoc.collection('answers').doc(outcome)
|
|
|
|
const answerSnap = await trans.get(answerDoc)
|
|
|
|
if (!answerSnap.exists) throw new APIError(400, 'Invalid answer')
|
|
|
|
return getNewMultiBetInfo(outcome, amount, contract, loanAmount)
|
|
|
|
} else if (outcomeType == 'NUMERIC' && mechanism == 'dpm-2') {
|
|
|
|
const { outcome, value } = validate(numericSchema, req.body)
|
|
|
|
return getNumericBetsInfo(value, outcome, amount, contract)
|
|
|
|
} else {
|
|
|
|
throw new APIError(500, 'Contract has invalid type/mechanism.')
|
2022-05-17 04:43:40 +00:00
|
|
|
}
|
2022-05-26 21:37:51 +00:00
|
|
|
})()
|
2022-06-17 03:57:03 +00:00
|
|
|
log('Calculated new bet information.')
|
2022-05-26 21:37:51 +00:00
|
|
|
|
2022-06-10 01:25:05 +00:00
|
|
|
if (
|
|
|
|
mechanism == 'cpmm-1' &&
|
|
|
|
(!newP ||
|
|
|
|
!isFinite(newP) ||
|
|
|
|
Math.min(...Object.values(newPool ?? {})) < CPMM_MIN_POOL_QTY)
|
|
|
|
) {
|
|
|
|
throw new APIError(400, 'Bet too large for current liquidity pool.')
|
2022-05-26 21:37:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const newBalance = user.balance - amount - loanAmount
|
|
|
|
const betDoc = contractDoc.collection('bets').doc()
|
|
|
|
trans.create(betDoc, { id: betDoc.id, userId: user.id, ...newBet })
|
2022-06-17 03:57:03 +00:00
|
|
|
log('Created new bet document.')
|
2022-05-26 21:37:51 +00:00
|
|
|
trans.update(userDoc, { balance: newBalance })
|
2022-06-17 03:57:03 +00:00
|
|
|
log('Updated user balance.')
|
2022-05-26 21:37:51 +00:00
|
|
|
trans.update(
|
|
|
|
contractDoc,
|
|
|
|
removeUndefinedProps({
|
|
|
|
pool: newPool,
|
|
|
|
p: newP,
|
|
|
|
totalShares: newTotalShares,
|
|
|
|
totalBets: newTotalBets,
|
|
|
|
totalLiquidity: newTotalLiquidity,
|
|
|
|
collectedFees: addObjects(newBet.fees, collectedFees),
|
|
|
|
volume: volume + amount,
|
|
|
|
})
|
|
|
|
)
|
2022-06-17 03:57:03 +00:00
|
|
|
log('Updated contract properties.')
|
2022-05-26 21:37:51 +00:00
|
|
|
|
|
|
|
return { betId: betDoc.id }
|
|
|
|
})
|
|
|
|
|
2022-06-17 03:57:03 +00:00
|
|
|
log('Main transaction finished.')
|
2022-06-11 00:51:55 +00:00
|
|
|
await redeemShares(auth.uid, contractId)
|
2022-06-17 03:57:03 +00:00
|
|
|
log('Share redemption transaction finished.')
|
2022-05-26 21:37:51 +00:00
|
|
|
return result
|
2022-05-17 04:43:40 +00:00
|
|
|
})
|
2021-12-11 00:06:17 +00:00
|
|
|
|
|
|
|
const firestore = admin.firestore()
|