Implement really obvious optimizations on placebet
, sellbet
, sellshares
(#452)
* Change authed endpoints to not look up users unnecessarily * Parallelize some extremely parallelizable DB requests * Clean up overcomplicated sellshares logic
This commit is contained in:
parent
ee816d6552
commit
6956f0d730
|
@ -6,14 +6,17 @@ import { onRequest, Request } from 'firebase-functions/v2/https'
|
|||
import * as Cors from 'cors'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { User, PrivateUser } from '../../common/user'
|
||||
import { PrivateUser } from '../../common/user'
|
||||
import {
|
||||
CORS_ORIGIN_MANIFOLD,
|
||||
CORS_ORIGIN_LOCALHOST,
|
||||
} from '../../common/envs/constants'
|
||||
|
||||
type Output = Record<string, unknown>
|
||||
type AuthedUser = [User, PrivateUser]
|
||||
type AuthedUser = {
|
||||
uid: string
|
||||
creds: JwtCredentials | (KeyCredentials & { privateUser: PrivateUser })
|
||||
}
|
||||
type Handler = (req: Request, user: AuthedUser) => Promise<Output>
|
||||
type JwtCredentials = { kind: 'jwt'; data: admin.auth.DecodedIdToken }
|
||||
type KeyCredentials = { kind: 'key'; data: string }
|
||||
|
@ -60,24 +63,13 @@ export const parseCredentials = async (req: Request): Promise<Credentials> => {
|
|||
|
||||
export const lookupUser = async (creds: Credentials): Promise<AuthedUser> => {
|
||||
const firestore = admin.firestore()
|
||||
const users = firestore.collection('users')
|
||||
const privateUsers = firestore.collection('private-users')
|
||||
switch (creds.kind) {
|
||||
case 'jwt': {
|
||||
const { user_id } = creds.data
|
||||
if (typeof user_id !== 'string') {
|
||||
if (typeof creds.data.user_id !== 'string') {
|
||||
throw new APIError(403, 'JWT must contain Manifold user ID.')
|
||||
}
|
||||
const [userSnap, privateUserSnap] = await Promise.all([
|
||||
users.doc(user_id).get(),
|
||||
privateUsers.doc(user_id).get(),
|
||||
])
|
||||
if (!userSnap.exists || !privateUserSnap.exists) {
|
||||
throw new APIError(403, 'No user exists with the provided ID.')
|
||||
}
|
||||
const user = userSnap.data() as User
|
||||
const privateUser = privateUserSnap.data() as PrivateUser
|
||||
return [user, privateUser]
|
||||
return { uid: creds.data.user_id, creds }
|
||||
}
|
||||
case 'key': {
|
||||
const key = creds.data
|
||||
|
@ -86,13 +78,8 @@ export const lookupUser = async (creds: Credentials): Promise<AuthedUser> => {
|
|||
throw new APIError(403, `No private user exists with API key ${key}.`)
|
||||
}
|
||||
const privateUserSnap = privateUserQ.docs[0]
|
||||
const userSnap = await users.doc(privateUserSnap.id).get()
|
||||
if (!userSnap.exists) {
|
||||
throw new APIError(403, `No user exists with ID ${privateUserSnap.id}.`)
|
||||
}
|
||||
const user = userSnap.data() as User
|
||||
const privateUser = privateUserSnap.data() as PrivateUser
|
||||
return [user, privateUser]
|
||||
return { uid: privateUser.id, creds: { privateUser, ...creds } }
|
||||
}
|
||||
default:
|
||||
throw new APIError(500, 'Invalid credential type.')
|
||||
|
|
|
@ -27,6 +27,7 @@ import {
|
|||
import { getNoneAnswer } from '../../common/answer'
|
||||
import { getNewContract } from '../../common/new-contract'
|
||||
import { NUMERIC_BUCKET_COUNT } from '../../common/numeric-constants'
|
||||
import { User } from '../../common/user'
|
||||
|
||||
const bodySchema = z.object({
|
||||
question: z.string().min(1).max(MAX_QUESTION_LENGTH),
|
||||
|
@ -48,7 +49,7 @@ const numericSchema = z.object({
|
|||
max: z.number(),
|
||||
})
|
||||
|
||||
export const createmarket = newEndpoint(['POST'], async (req, [user, _]) => {
|
||||
export const createmarket = newEndpoint(['POST'], async (req, auth) => {
|
||||
const { question, description, tags, closeTime, outcomeType } = validate(
|
||||
bodySchema,
|
||||
req.body
|
||||
|
@ -70,9 +71,15 @@ export const createmarket = newEndpoint(['POST'], async (req, [user, _]) => {
|
|||
freeMarketResetTime = freeMarketResetTime - 24 * 60 * 60 * 1000
|
||||
}
|
||||
|
||||
const userDoc = await firestore.collection('users').doc(auth.uid).get()
|
||||
if (!userDoc.exists) {
|
||||
throw new APIError(400, 'No user exists with the authenticated user ID.')
|
||||
}
|
||||
const user = userDoc.data() as User
|
||||
|
||||
const userContractsCreatedTodaySnapshot = await firestore
|
||||
.collection(`contracts`)
|
||||
.where('creatorId', '==', user.id)
|
||||
.where('creatorId', '==', auth.uid)
|
||||
.where('createdTime', '>=', freeMarketResetTime)
|
||||
.get()
|
||||
console.log('free market reset time: ', freeMarketResetTime)
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
import { newEndpoint } from './api'
|
||||
|
||||
export const health = newEndpoint(['GET'], async (_req, [user, _]) => {
|
||||
export const health = newEndpoint(['GET'], async (_req, auth) => {
|
||||
return {
|
||||
message: 'Server is working.',
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
},
|
||||
uid: auth.uid,
|
||||
}
|
||||
})
|
||||
|
|
|
@ -32,21 +32,23 @@ const numericSchema = z.object({
|
|||
value: z.number(),
|
||||
})
|
||||
|
||||
export const placebet = newEndpoint(['POST'], async (req, [bettor, _]) => {
|
||||
export const placebet = newEndpoint(['POST'], async (req, auth) => {
|
||||
const { amount, contractId } = validate(bodySchema, req.body)
|
||||
|
||||
const result = await firestore.runTransaction(async (trans) => {
|
||||
const userDoc = firestore.doc(`users/${bettor.id}`)
|
||||
const userSnap = await trans.get(userDoc)
|
||||
const contractDoc = firestore.doc(`contracts/${contractId}`)
|
||||
const userDoc = firestore.doc(`users/${auth.uid}`)
|
||||
const [contractSnap, userSnap] = await Promise.all([
|
||||
trans.get(contractDoc),
|
||||
trans.get(userDoc),
|
||||
])
|
||||
if (!contractSnap.exists) throw new APIError(400, 'Contract not found.')
|
||||
if (!userSnap.exists) throw new APIError(400, 'User not found.')
|
||||
|
||||
const contract = contractSnap.data() as Contract
|
||||
const user = userSnap.data() as User
|
||||
if (user.balance < amount) throw new APIError(400, 'Insufficient balance.')
|
||||
|
||||
const contractDoc = firestore.doc(`contracts/${contractId}`)
|
||||
const contractSnap = await trans.get(contractDoc)
|
||||
if (!contractSnap.exists) throw new APIError(400, 'Contract not found.')
|
||||
const contract = contractSnap.data() as Contract
|
||||
|
||||
const loanAmount = 0
|
||||
const { closeTime, outcomeType, mechanism, collectedFees, volume } =
|
||||
contract
|
||||
|
@ -110,7 +112,7 @@ export const placebet = newEndpoint(['POST'], async (req, [bettor, _]) => {
|
|||
return { betId: betDoc.id }
|
||||
})
|
||||
|
||||
await redeemShares(bettor.id, contractId)
|
||||
await redeemShares(auth.uid, contractId)
|
||||
return result
|
||||
})
|
||||
|
||||
|
|
|
@ -13,20 +13,26 @@ const bodySchema = z.object({
|
|||
betId: z.string(),
|
||||
})
|
||||
|
||||
export const sellbet = newEndpoint(['POST'], async (req, [bettor, _]) => {
|
||||
export const sellbet = newEndpoint(['POST'], async (req, auth) => {
|
||||
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/${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)
|
||||
const userDoc = firestore.doc(`users/${auth.uid}`)
|
||||
const betDoc = firestore.doc(`contracts/${contractId}/bets/${betId}`)
|
||||
const [contractSnap, userSnap, betSnap] = await Promise.all([
|
||||
transaction.get(contractDoc),
|
||||
transaction.get(userDoc),
|
||||
transaction.get(betDoc),
|
||||
])
|
||||
if (!contractSnap.exists) throw new APIError(400, 'Contract not found.')
|
||||
if (!userSnap.exists) throw new APIError(400, 'User not found.')
|
||||
if (!betSnap.exists) throw new APIError(400, 'Bet not found.')
|
||||
|
||||
const contract = contractSnap.data() as Contract
|
||||
const user = userSnap.data() as User
|
||||
const bet = betSnap.data() as Bet
|
||||
|
||||
const { closeTime, mechanism, collectedFees, volume } = contract
|
||||
if (mechanism !== 'dpm-2')
|
||||
|
@ -34,12 +40,7 @@ export const sellbet = newEndpoint(['POST'], async (req, [bettor, _]) => {
|
|||
if (closeTime && Date.now() > closeTime)
|
||||
throw new APIError(400, '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
|
||||
|
||||
if (bettor.id !== bet.userId)
|
||||
if (auth.uid !== 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.')
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { partition, sumBy } from 'lodash'
|
||||
import { sumBy } from 'lodash'
|
||||
import * as admin from 'firebase-admin'
|
||||
import { z } from 'zod'
|
||||
|
||||
|
@ -16,20 +16,25 @@ const bodySchema = z.object({
|
|||
outcome: z.enum(['YES', 'NO']),
|
||||
})
|
||||
|
||||
export const sellshares = newEndpoint(['POST'], async (req, [bettor, _]) => {
|
||||
export const sellshares = newEndpoint(['POST'], async (req, auth) => {
|
||||
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/${bettor.id}`)
|
||||
const userSnap = await transaction.get(userDoc)
|
||||
const contractDoc = firestore.doc(`contracts/${contractId}`)
|
||||
const userDoc = firestore.doc(`users/${auth.uid}`)
|
||||
const betsQ = contractDoc.collection('bets').where('userId', '==', auth.uid)
|
||||
const [contractSnap, userSnap, userBets] = await Promise.all([
|
||||
transaction.get(contractDoc),
|
||||
transaction.get(userDoc),
|
||||
getValues<Bet>(betsQ), // TODO: why is this not in the transaction??
|
||||
])
|
||||
if (!contractSnap.exists) throw new APIError(400, 'Contract not found.')
|
||||
if (!userSnap.exists) throw new APIError(400, 'User not found.')
|
||||
|
||||
const contract = contractSnap.data() as Contract
|
||||
const user = userSnap.data() as User
|
||||
|
||||
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')
|
||||
|
@ -37,22 +42,11 @@ export const sellshares = newEndpoint(['POST'], async (req, [bettor, _]) => {
|
|||
if (closeTime && Date.now() > closeTime)
|
||||
throw new APIError(400, 'Trading is closed.')
|
||||
|
||||
const userBets = await getValues<Bet>(
|
||||
contractDoc.collection('bets').where('userId', '==', bettor.id)
|
||||
)
|
||||
|
||||
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 outcomeBets = userBets.filter((bet) => bet.outcome == outcome)
|
||||
const maxShares = sumBy(outcomeBets, (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.`)
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user