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:
parent
a8ae724159
commit
0820cc8f4d
|
@ -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",
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user