Migrate createAnswer function to v2 (#634)

* Migrate createAnswer function to v2

* Remove unhelpful toString on APIError
This commit is contained in:
Marshall Polaris 2022-07-09 00:26:56 -07:00 committed by GitHub
parent fdde73710e
commit c1ca1471a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 101 additions and 127 deletions

View File

@ -1,5 +1,5 @@
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin' import * as admin from 'firebase-admin'
import { z } from 'zod'
import { Contract } from '../../common/contract' import { Contract } from '../../common/contract'
import { User } from '../../common/user' import { User } from '../../common/user'
@ -7,55 +7,41 @@ import { getNewMultiBetInfo } from '../../common/new-bet'
import { Answer, MAX_ANSWER_LENGTH } from '../../common/answer' import { Answer, MAX_ANSWER_LENGTH } from '../../common/answer'
import { getContract, getValues } from './utils' import { getContract, getValues } from './utils'
import { sendNewAnswerEmail } from './emails' import { sendNewAnswerEmail } from './emails'
import { APIError, newEndpoint, validate } from './api'
export const createAnswer = functions const bodySchema = z.object({
.runWith({ minInstances: 1, secrets: ['MAILGUN_KEY'] }) contractId: z.string().max(MAX_ANSWER_LENGTH),
.https.onCall( amount: z.number().gt(0),
async ( text: z.string(),
data: { })
contractId: string
amount: number
text: string
},
context
) => {
const userId = context?.auth?.uid
if (!userId) return { status: 'error', message: 'Not authorized' }
const { contractId, amount, text } = data const opts = { secrets: ['MAILGUN_KEY'] }
if (amount <= 0 || isNaN(amount) || !isFinite(amount)) export const createanswer = newEndpoint(opts, async (req, auth) => {
return { status: 'error', message: 'Invalid amount' } const { contractId, amount, text } = validate(bodySchema, req.body)
if (!text || typeof text !== 'string' || text.length > MAX_ANSWER_LENGTH) if (!isFinite(amount)) throw new APIError(400, 'Invalid amount')
return { status: 'error', message: 'Invalid text' }
// Run as transaction to prevent race conditions. // Run as transaction to prevent race conditions.
const result = await firestore.runTransaction(async (transaction) => { const answer = await firestore.runTransaction(async (transaction) => {
const userDoc = firestore.doc(`users/${userId}`) const userDoc = firestore.doc(`users/${auth.uid}`)
const userSnap = await transaction.get(userDoc) const userSnap = await transaction.get(userDoc)
if (!userSnap.exists) if (!userSnap.exists) throw new APIError(400, 'User not found')
return { status: 'error', message: 'User not found' }
const user = userSnap.data() as User const user = userSnap.data() as User
if (user.balance < amount) if (user.balance < amount) throw new APIError(400, 'Insufficient balance')
return { status: 'error', message: 'Insufficient balance' }
const contractDoc = firestore.doc(`contracts/${contractId}`) const contractDoc = firestore.doc(`contracts/${contractId}`)
const contractSnap = await transaction.get(contractDoc) const contractSnap = await transaction.get(contractDoc)
if (!contractSnap.exists) if (!contractSnap.exists) throw new APIError(400, 'Invalid contract')
return { status: 'error', message: 'Invalid contract' }
const contract = contractSnap.data() as Contract const contract = contractSnap.data() as Contract
if (contract.outcomeType !== 'FREE_RESPONSE') if (contract.outcomeType !== 'FREE_RESPONSE')
return { throw new APIError(400, 'Requires a free response contract')
status: 'error',
message: 'Requires a free response contract',
}
const { closeTime, volume } = contract const { closeTime, volume } = contract
if (closeTime && Date.now() > closeTime) if (closeTime && Date.now() > closeTime)
return { status: 'error', message: 'Trading is closed' } throw new APIError(400, 'Trading is closed')
const [lastAnswer] = await getValues<Answer>( const [lastAnswer] = await getValues<Answer>(
firestore firestore
@ -64,8 +50,7 @@ export const createAnswer = functions
.limit(1) .limit(1)
) )
if (!lastAnswer) if (!lastAnswer) throw new APIError(500, 'Could not fetch last answer')
return { status: 'error', message: 'Could not fetch last answer' }
const number = lastAnswer.number + 1 const number = lastAnswer.number + 1
const id = `${number}` const id = `${number}`
@ -96,9 +81,7 @@ export const createAnswer = functions
getNewMultiBetInfo(answerId, amount, contract, loanAmount) getNewMultiBetInfo(answerId, amount, contract, loanAmount)
const newBalance = user.balance - amount const newBalance = user.balance - amount
const betDoc = firestore const betDoc = firestore.collection(`contracts/${contractId}/bets`).doc()
.collection(`contracts/${contractId}/bets`)
.doc()
transaction.create(betDoc, { transaction.create(betDoc, {
id: betDoc.id, id: betDoc.id,
userId: user.id, userId: user.id,
@ -113,16 +96,14 @@ export const createAnswer = functions
volume: volume + amount, volume: volume + amount,
}) })
return { status: 'success', answerId, betId: betDoc.id, answer } return answer
}) })
const { answer } = result
const contract = await getContract(contractId) const contract = await getContract(contractId)
if (answer && contract) await sendNewAnswerEmail(answer, contract) if (answer && contract) await sendNewAnswerEmail(answer, contract)
return result return answer
} })
)
const firestore = admin.firestore() const firestore = admin.firestore()

View File

@ -6,7 +6,6 @@ admin.initializeApp()
export * from './transact' export * from './transact'
export * from './stripe' export * from './stripe'
export * from './create-user' export * from './create-user'
export * from './create-answer'
export * from './on-create-bet' export * from './on-create-bet'
export * from './on-create-comment-on-contract' export * from './on-create-comment-on-contract'
export * from './on-view' export * from './on-view'
@ -30,6 +29,7 @@ export * from './on-create-txn'
// v2 // v2
export * from './health' export * from './health'
export * from './change-user-info' export * from './change-user-info'
export * from './create-answer'
export * from './place-bet' export * from './place-bet'
export * from './sell-bet' export * from './sell-bet'
export * from './sell-shares' export * from './sell-shares'

View File

@ -6,7 +6,7 @@ import { findBestMatch } from 'string-similarity'
import { FreeResponseContract } from 'common/contract' import { FreeResponseContract } from 'common/contract'
import { BuyAmountInput } from '../amount-input' import { BuyAmountInput } from '../amount-input'
import { Col } from '../layout/col' import { Col } from '../layout/col'
import { createAnswer } from 'web/lib/firebase/fn-call' import { APIError, createAnswer } from 'web/lib/firebase/api-call'
import { Row } from '../layout/row' import { Row } from '../layout/row'
import { import {
formatMoney, formatMoney,
@ -46,20 +46,23 @@ export function CreateAnswerPanel(props: { contract: FreeResponseContract }) {
if (canSubmit) { if (canSubmit) {
setIsSubmitting(true) setIsSubmitting(true)
const result = await createAnswer({ try {
await createAnswer({
contractId: contract.id, contractId: contract.id,
text, text,
amount: betAmount, amount: betAmount,
}).then((r) => r.data) })
setIsSubmitting(false)
if (result.status === 'success') {
setText('') setText('')
setBetAmount(10) setBetAmount(10)
setAmountError(undefined) setAmountError(undefined)
setPossibleDuplicateAnswer(undefined) setPossibleDuplicateAnswer(undefined)
} else setAmountError(result.message) } catch (e) {
if (e instanceof APIError) {
setAmountError(e.toString())
}
}
setIsSubmitting(false)
} }
} }

View File

@ -10,9 +10,6 @@ export class APIError extends Error {
this.name = 'APIError' this.name = 'APIError'
this.details = details this.details = details
} }
toString() {
return this.name
}
} }
export async function call(url: string, method: string, params: any) { export async function call(url: string, method: string, params: any) {
@ -53,6 +50,9 @@ export function getFunctionUrl(name: string) {
} }
} }
export function createAnswer(params: any) {
return call(getFunctionUrl('createanswer'), 'POST', params)
}
export function changeUserInfo(params: any) { export function changeUserInfo(params: any) {
return call(getFunctionUrl('changeuserinfo'), 'POST', params) return call(getFunctionUrl('changeuserinfo'), 'POST', params)
} }

View File

@ -14,16 +14,6 @@ export const transact = cloudFunction<
{ status: 'error' | 'success'; message?: string; txn?: Txn } { status: 'error' | 'success'; message?: string; txn?: Txn }
>('transact') >('transact')
export const createAnswer = cloudFunction<
{ contractId: string; text: string; amount: number },
{
status: 'error' | 'success'
message?: string
answerId?: string
betId?: string
}
>('createAnswer')
export const createUser: () => Promise<User | null> = () => { export const createUser: () => Promise<User | null> = () => {
const local = safeLocalStorage() const local = safeLocalStorage()
let deviceToken = local?.getItem('device-token') let deviceToken = local?.getItem('device-token')