Clean some API stuff up and instrument placeBet with a bunch of logging (#521)

* Hoist some variables out of functions

* Use built in CORS processing machinery

* Instrument `placebet` with a bunch of logging
This commit is contained in:
Marshall Polaris 2022-06-16 20:57:03 -07:00 committed by GitHub
parent a8ae724159
commit 0820cc8f4d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 28 additions and 31 deletions

View File

@ -21,7 +21,6 @@
"main": "lib/functions/src/index.js",
"dependencies": {
"@amplitude/node": "1.10.0",
"cors": "2.8.5",
"fetch": "1.1.0",
"firebase-admin": "10.0.0",
"firebase-functions": "3.21.2",

View File

@ -1,9 +1,7 @@
import * as admin from 'firebase-admin'
import { Response } from 'express'
import { logger } from 'firebase-functions/v2'
import { onRequest, Request } from 'firebase-functions/v2/https'
import * as Cors from 'cors'
import { HttpsOptions, onRequest, Request } from 'firebase-functions/v2/https'
import { log } from './utils'
import { z } from 'zod'
import { PrivateUser } from '../../common/user'
@ -33,6 +31,12 @@ export class APIError {
}
}
const auth = admin.auth()
const firestore = admin.firestore()
const privateUsers = firestore.collection(
'private-users'
) as admin.firestore.CollectionReference<PrivateUser>
export const parseCredentials = async (req: Request): Promise<Credentials> => {
const authHeader = req.get('Authorization')
if (!authHeader) {
@ -47,8 +51,7 @@ export const parseCredentials = async (req: Request): Promise<Credentials> => {
switch (scheme) {
case 'Bearer':
try {
const jwt = await admin.auth().verifyIdToken(payload)
return { kind: 'jwt', data: jwt }
return { kind: 'jwt', data: await auth.verifyIdToken(payload) }
} catch (err) {
// This is somewhat suspicious, so get it into the firebase console
logger.error('Error verifying Firebase JWT: ', err)
@ -62,8 +65,6 @@ export const parseCredentials = async (req: Request): Promise<Credentials> => {
}
export const lookupUser = async (creds: Credentials): Promise<AuthedUser> => {
const firestore = admin.firestore()
const privateUsers = firestore.collection('private-users')
switch (creds.kind) {
case 'jwt': {
if (typeof creds.data.user_id !== 'string') {
@ -77,8 +78,7 @@ export const lookupUser = async (creds: Credentials): Promise<AuthedUser> => {
if (privateUserQ.empty) {
throw new APIError(403, `No private user exists with API key ${key}.`)
}
const privateUserSnap = privateUserQ.docs[0]
const privateUser = privateUserSnap.data() as PrivateUser
const privateUser = privateUserQ.docs[0].data()
return { uid: privateUser.id, creds: { privateUser, ...creds } }
}
default:
@ -86,21 +86,6 @@ export const lookupUser = async (creds: Credentials): Promise<AuthedUser> => {
}
}
export const applyCors = (
req: Request,
res: Response,
params: Cors.CorsOptions
) => {
return new Promise((resolve, reject) => {
Cors(params)(req, res, (result) => {
if (result instanceof Error) {
return reject(result)
}
return resolve(result)
})
})
}
export const zTimestamp = () => {
return z.preprocess((arg) => {
return typeof arg == 'number' ? new Date(arg) : undefined
@ -122,18 +107,21 @@ export const validate = <T extends z.ZodTypeAny>(schema: T, val: unknown) => {
}
}
const DEFAULT_OPTS: HttpsOptions = {
minInstances: 1,
cors: [CORS_ORIGIN_MANIFOLD, CORS_ORIGIN_LOCALHOST],
}
export const newEndpoint = (methods: [string], fn: Handler) =>
onRequest({ minInstances: 1 }, async (req, res) => {
onRequest(DEFAULT_OPTS, async (req, res) => {
log('Request processing started.')
try {
await applyCors(req, res, {
origin: [CORS_ORIGIN_MANIFOLD, CORS_ORIGIN_LOCALHOST],
methods: methods,
})
if (!methods.includes(req.method)) {
const allowed = methods.join(', ')
throw new APIError(405, `This endpoint supports only ${allowed}.`)
}
const authedUser = await lookupUser(await parseCredentials(req))
log('User credentials processed.')
res.status(200).json(await fn(req, authedUser))
} catch (e) {
if (e instanceof APIError) {

View File

@ -13,6 +13,7 @@ import {
} from '../../common/new-bet'
import { addObjects, removeUndefinedProps } from '../../common/util/object'
import { redeemShares } from './redeem-shares'
import { log } from './utils'
const bodySchema = z.object({
contractId: z.string(),
@ -33,9 +34,11 @@ const numericSchema = z.object({
})
export const placebet = newEndpoint(['POST'], async (req, auth) => {
log('Inside endpoint handler.')
const { amount, contractId } = validate(bodySchema, req.body)
const result = await firestore.runTransaction(async (trans) => {
log('Inside main transaction.')
const contractDoc = firestore.doc(`contracts/${contractId}`)
const userDoc = firestore.doc(`users/${auth.uid}`)
const [contractSnap, userSnap] = await Promise.all([
@ -44,6 +47,7 @@ export const placebet = newEndpoint(['POST'], async (req, auth) => {
])
if (!contractSnap.exists) throw new APIError(400, 'Contract not found.')
if (!userSnap.exists) throw new APIError(400, 'User not found.')
log('Loaded user and contract snapshots.')
const contract = contractSnap.data() as Contract
const user = userSnap.data() as User
@ -82,6 +86,7 @@ export const placebet = newEndpoint(['POST'], async (req, auth) => {
throw new APIError(500, 'Contract has invalid type/mechanism.')
}
})()
log('Calculated new bet information.')
if (
mechanism == 'cpmm-1' &&
@ -95,7 +100,9 @@ export const placebet = newEndpoint(['POST'], async (req, auth) => {
const newBalance = user.balance - amount - loanAmount
const betDoc = contractDoc.collection('bets').doc()
trans.create(betDoc, { id: betDoc.id, userId: user.id, ...newBet })
log('Created new bet document.')
trans.update(userDoc, { balance: newBalance })
log('Updated user balance.')
trans.update(
contractDoc,
removeUndefinedProps({
@ -108,11 +115,14 @@ export const placebet = newEndpoint(['POST'], async (req, auth) => {
volume: volume + amount,
})
)
log('Updated contract properties.')
return { betId: betDoc.id }
})
log('Main transaction finished.')
await redeemShares(auth.uid, contractId)
log('Share redemption transaction finished.')
return result
})