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", "main": "lib/functions/src/index.js",
"dependencies": { "dependencies": {
"@amplitude/node": "1.10.0", "@amplitude/node": "1.10.0",
"cors": "2.8.5",
"fetch": "1.1.0", "fetch": "1.1.0",
"firebase-admin": "10.0.0", "firebase-admin": "10.0.0",
"firebase-functions": "3.21.2", "firebase-functions": "3.21.2",

View File

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

View File

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