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

View File

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

View File

@ -6,7 +6,7 @@ import { findBestMatch } from 'string-similarity'
import { FreeResponseContract } from 'common/contract'
import { BuyAmountInput } from '../amount-input'
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 {
formatMoney,
@ -46,20 +46,23 @@ export function CreateAnswerPanel(props: { contract: FreeResponseContract }) {
if (canSubmit) {
setIsSubmitting(true)
const result = await createAnswer({
contractId: contract.id,
text,
amount: betAmount,
}).then((r) => r.data)
setIsSubmitting(false)
if (result.status === 'success') {
try {
await createAnswer({
contractId: contract.id,
text,
amount: betAmount,
})
setText('')
setBetAmount(10)
setAmountError(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.details = details
}
toString() {
return this.name
}
}
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) {
return call(getFunctionUrl('changeuserinfo'), 'POST', params)
}

View File

@ -14,16 +14,6 @@ export const transact = cloudFunction<
{ status: 'error' | 'success'; message?: string; txn?: Txn }
>('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> = () => {
const local = safeLocalStorage()
let deviceToken = local?.getItem('device-token')