Implement onRequest
versions of createContract
, placeBet
functions (#227)
* Reimplement createContract and placeBet cloud functions * Fix broken warmup function error handling
This commit is contained in:
parent
aafd2a226f
commit
cd7efb03ca
|
@ -20,6 +20,7 @@
|
|||
"main": "lib/functions/src/index.js",
|
||||
"dependencies": {
|
||||
"@react-query-firebase/firestore": "0.4.2",
|
||||
"cors": "2.8.5",
|
||||
"fetch": "1.1.0",
|
||||
"firebase-admin": "10.0.0",
|
||||
"firebase-functions": "3.16.0",
|
||||
|
|
129
functions/src/api.ts
Normal file
129
functions/src/api.ts
Normal file
|
@ -0,0 +1,129 @@
|
|||
import * as admin from 'firebase-admin'
|
||||
import * as functions from 'firebase-functions'
|
||||
import * as Cors from 'cors'
|
||||
|
||||
import { User, PrivateUser } from 'common/user'
|
||||
|
||||
type Request = functions.https.Request
|
||||
type Response = functions.Response
|
||||
type Handler = (req: Request, res: Response) => Promise<any>
|
||||
type AuthedUser = [User, PrivateUser]
|
||||
type JwtCredentials = { kind: 'jwt'; data: admin.auth.DecodedIdToken }
|
||||
type KeyCredentials = { kind: 'key'; data: string }
|
||||
type Credentials = JwtCredentials | KeyCredentials
|
||||
|
||||
export class APIError {
|
||||
code: number
|
||||
msg: string
|
||||
constructor(code: number, msg: string) {
|
||||
this.code = code
|
||||
this.msg = msg
|
||||
}
|
||||
}
|
||||
|
||||
export const parseCredentials = async (req: Request): Promise<Credentials> => {
|
||||
const authHeader = req.get('Authorization')
|
||||
if (!authHeader) {
|
||||
throw new APIError(403, 'Missing Authorization header.')
|
||||
}
|
||||
const authParts = authHeader.split(' ')
|
||||
if (authParts.length !== 2) {
|
||||
throw new APIError(403, 'Invalid Authorization header.')
|
||||
}
|
||||
|
||||
const [scheme, payload] = authParts
|
||||
switch (scheme) {
|
||||
case 'Bearer':
|
||||
try {
|
||||
const jwt = await admin.auth().verifyIdToken(payload)
|
||||
if (!jwt.user_id) {
|
||||
throw new APIError(403, 'JWT must contain Manifold user ID.')
|
||||
}
|
||||
return { kind: 'jwt', data: jwt }
|
||||
} catch (err) {
|
||||
// This is somewhat suspicious, so get it into the firebase console
|
||||
functions.logger.error('Error verifying Firebase JWT: ', err)
|
||||
throw new APIError(403, `Error validating token: ${err}.`)
|
||||
}
|
||||
case 'Key':
|
||||
return { kind: 'key', data: payload }
|
||||
default:
|
||||
throw new APIError(403, 'Invalid auth scheme; must be "Key" or "Bearer".')
|
||||
}
|
||||
}
|
||||
|
||||
export const lookupUser = async (creds: Credentials): Promise<AuthedUser> => {
|
||||
const firestore = admin.firestore()
|
||||
const users = firestore.collection('users')
|
||||
const privateUsers = firestore.collection('private-users')
|
||||
switch (creds.kind) {
|
||||
case 'jwt': {
|
||||
const { user_id } = creds.data
|
||||
const [userSnap, privateUserSnap] = await Promise.all([
|
||||
users.doc(user_id).get(),
|
||||
privateUsers.doc(user_id).get(),
|
||||
])
|
||||
if (!userSnap.exists || !privateUserSnap.exists) {
|
||||
throw new APIError(403, 'No user exists with the provided ID.')
|
||||
}
|
||||
const user = userSnap.data() as User
|
||||
const privateUser = privateUserSnap.data() as PrivateUser
|
||||
return [user, privateUser]
|
||||
}
|
||||
case 'key': {
|
||||
const key = creds.data
|
||||
const privateUserQ = await privateUsers.where('apiKey', '==', key).get()
|
||||
if (privateUserQ.empty) {
|
||||
throw new APIError(403, `No private user exists with API key ${key}.`)
|
||||
}
|
||||
const privateUserSnap = privateUserQ.docs[0]
|
||||
const userSnap = await users.doc(privateUserSnap.id).get()
|
||||
if (!userSnap.exists) {
|
||||
throw new APIError(403, `No user exists with ID ${privateUserSnap.id}.`)
|
||||
}
|
||||
const user = userSnap.data() as User
|
||||
const privateUser = privateUserSnap.data() as PrivateUser
|
||||
return [user, privateUser]
|
||||
}
|
||||
default:
|
||||
throw new APIError(500, 'Invalid credential type.')
|
||||
}
|
||||
}
|
||||
|
||||
export const CORS_ORIGIN_MANIFOLD = /^https?:\/\/.+\.manifold\.markets$/
|
||||
export const CORS_ORIGIN_LOCALHOST = /^http:\/\/localhost:\d+$/
|
||||
|
||||
export const applyCors = (req: any, res: any, params: object) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
Cors(params)(req, res, (result) => {
|
||||
if (result instanceof Error) {
|
||||
return reject(result)
|
||||
}
|
||||
return resolve(result)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export const newEndpoint = (methods: [string], fn: Handler) =>
|
||||
functions.runWith({ minInstances: 1 }).https.onRequest(async (req, res) => {
|
||||
await applyCors(req, res, {
|
||||
origins: [CORS_ORIGIN_MANIFOLD, CORS_ORIGIN_LOCALHOST],
|
||||
methods: methods,
|
||||
})
|
||||
try {
|
||||
if (!methods.includes(req.method)) {
|
||||
const allowed = methods.join(', ')
|
||||
throw new APIError(405, `This endpoint supports only ${allowed}.`)
|
||||
}
|
||||
const data = await fn(req, res)
|
||||
data.status = 'success'
|
||||
res.status(200).json({ data: data })
|
||||
} catch (e) {
|
||||
if (e instanceof APIError) {
|
||||
// Emit a 200 anyway here for now, for backwards compatibility
|
||||
res.status(200).json({ data: { status: 'error', message: e.msg } })
|
||||
} else {
|
||||
res.status(500).json({ data: { status: 'error', message: '???' } })
|
||||
}
|
||||
}
|
||||
})
|
|
@ -1,8 +1,7 @@
|
|||
import * as functions from 'firebase-functions'
|
||||
import * as admin from 'firebase-admin'
|
||||
import * as _ from 'lodash'
|
||||
|
||||
import { chargeUser, getUser } from './utils'
|
||||
import { chargeUser } from './utils'
|
||||
import { APIError, newEndpoint, parseCredentials, lookupUser } from './api'
|
||||
import {
|
||||
Binary,
|
||||
Contract,
|
||||
|
@ -13,7 +12,6 @@ import {
|
|||
MAX_DESCRIPTION_LENGTH,
|
||||
MAX_QUESTION_LENGTH,
|
||||
MAX_TAG_LENGTH,
|
||||
outcomeType,
|
||||
} from '../../common/contract'
|
||||
import { slugify } from '../../common/util/slugify'
|
||||
import { randomString } from '../../common/util/random'
|
||||
|
@ -28,169 +26,154 @@ import {
|
|||
} from '../../common/antes'
|
||||
import { getNoneAnswer } from '../../common/answer'
|
||||
|
||||
export const createContract = functions
|
||||
.runWith({ minInstances: 1 })
|
||||
.https.onCall(
|
||||
async (
|
||||
data: {
|
||||
question: string
|
||||
outcomeType: outcomeType
|
||||
description: string
|
||||
initialProb: number
|
||||
ante: number
|
||||
closeTime: number
|
||||
tags?: string[]
|
||||
manaLimitPerUser?: number
|
||||
},
|
||||
context
|
||||
) => {
|
||||
const userId = context?.auth?.uid
|
||||
if (!userId) return { status: 'error', message: 'Not authorized' }
|
||||
export const createContract = newEndpoint(['POST'], async (req, _res) => {
|
||||
const [creator, _privateUser] = await lookupUser(await parseCredentials(req))
|
||||
let {
|
||||
question,
|
||||
outcomeType,
|
||||
description,
|
||||
initialProb,
|
||||
closeTime,
|
||||
tags,
|
||||
manaLimitPerUser,
|
||||
} = req.body.data || {}
|
||||
|
||||
const creator = await getUser(userId)
|
||||
if (!creator) return { status: 'error', message: 'User not found' }
|
||||
if (!question || typeof question != 'string')
|
||||
throw new APIError(400, 'Missing or invalid question field')
|
||||
|
||||
let {
|
||||
question,
|
||||
description,
|
||||
initialProb,
|
||||
closeTime,
|
||||
tags,
|
||||
manaLimitPerUser,
|
||||
} = data
|
||||
question = question.slice(0, MAX_QUESTION_LENGTH)
|
||||
|
||||
if (!question || typeof question != 'string')
|
||||
return { status: 'error', message: 'Missing or invalid question field' }
|
||||
question = question.slice(0, MAX_QUESTION_LENGTH)
|
||||
if (typeof description !== 'string')
|
||||
throw new APIError(400, 'Invalid description field')
|
||||
|
||||
if (typeof description !== 'string')
|
||||
return { status: 'error', message: 'Invalid description field' }
|
||||
description = description.slice(0, MAX_DESCRIPTION_LENGTH)
|
||||
description = description.slice(0, MAX_DESCRIPTION_LENGTH)
|
||||
|
||||
if (tags !== undefined && !_.isArray(tags))
|
||||
return { status: 'error', message: 'Invalid tags field' }
|
||||
tags = tags?.map((tag) => tag.toString().slice(0, MAX_TAG_LENGTH))
|
||||
if (tags !== undefined && !Array.isArray(tags))
|
||||
throw new APIError(400, 'Invalid tags field')
|
||||
|
||||
let outcomeType = data.outcomeType ?? 'BINARY'
|
||||
if (!['BINARY', 'MULTI', 'FREE_RESPONSE'].includes(outcomeType))
|
||||
return { status: 'error', message: 'Invalid outcomeType' }
|
||||
|
||||
if (
|
||||
outcomeType === 'BINARY' &&
|
||||
(!initialProb || initialProb < 1 || initialProb > 99)
|
||||
)
|
||||
return { status: 'error', message: 'Invalid initial probability' }
|
||||
|
||||
// uses utc time on server:
|
||||
const today = new Date().setHours(0, 0, 0, 0)
|
||||
const userContractsCreatedTodaySnapshot = await firestore
|
||||
.collection(`contracts`)
|
||||
.where('creatorId', '==', userId)
|
||||
.where('createdTime', '>=', today)
|
||||
.get()
|
||||
const isFree = userContractsCreatedTodaySnapshot.size === 0
|
||||
|
||||
const ante = FIXED_ANTE // data.ante
|
||||
|
||||
if (
|
||||
ante === undefined ||
|
||||
ante < MINIMUM_ANTE ||
|
||||
(ante > creator.balance && !isFree) ||
|
||||
isNaN(ante) ||
|
||||
!isFinite(ante)
|
||||
)
|
||||
return { status: 'error', message: 'Invalid ante' }
|
||||
|
||||
console.log(
|
||||
'creating contract for',
|
||||
creator.username,
|
||||
'on',
|
||||
question,
|
||||
'ante:',
|
||||
ante || 0
|
||||
)
|
||||
|
||||
const slug = await getSlug(question)
|
||||
|
||||
const contractRef = firestore.collection('contracts').doc()
|
||||
|
||||
const contract = getNewContract(
|
||||
contractRef.id,
|
||||
slug,
|
||||
creator,
|
||||
question,
|
||||
outcomeType,
|
||||
description,
|
||||
initialProb,
|
||||
ante,
|
||||
closeTime,
|
||||
tags ?? [],
|
||||
manaLimitPerUser ?? 0
|
||||
)
|
||||
|
||||
if (!isFree && ante) await chargeUser(creator.id, ante, true)
|
||||
|
||||
await contractRef.create(contract)
|
||||
|
||||
if (ante) {
|
||||
if (outcomeType === 'BINARY' && contract.mechanism === 'dpm-2') {
|
||||
const yesBetDoc = firestore
|
||||
.collection(`contracts/${contract.id}/bets`)
|
||||
.doc()
|
||||
|
||||
const noBetDoc = firestore
|
||||
.collection(`contracts/${contract.id}/bets`)
|
||||
.doc()
|
||||
|
||||
const { yesBet, noBet } = getAnteBets(
|
||||
creator,
|
||||
contract as FullContract<DPM, Binary>,
|
||||
yesBetDoc.id,
|
||||
noBetDoc.id
|
||||
)
|
||||
|
||||
await yesBetDoc.set(yesBet)
|
||||
await noBetDoc.set(noBet)
|
||||
} else if (outcomeType === 'BINARY') {
|
||||
const liquidityDoc = firestore
|
||||
.collection(`contracts/${contract.id}/liquidity`)
|
||||
.doc()
|
||||
|
||||
const providerId = isFree ? HOUSE_LIQUIDITY_PROVIDER_ID : creator.id
|
||||
|
||||
const lp = getCpmmInitialLiquidity(
|
||||
providerId,
|
||||
contract as FullContract<CPMM, Binary>,
|
||||
liquidityDoc.id,
|
||||
ante
|
||||
)
|
||||
|
||||
await liquidityDoc.set(lp)
|
||||
} else if (outcomeType === 'FREE_RESPONSE') {
|
||||
const noneAnswerDoc = firestore
|
||||
.collection(`contracts/${contract.id}/answers`)
|
||||
.doc('0')
|
||||
|
||||
const noneAnswer = getNoneAnswer(contract.id, creator)
|
||||
await noneAnswerDoc.set(noneAnswer)
|
||||
|
||||
const anteBetDoc = firestore
|
||||
.collection(`contracts/${contract.id}/bets`)
|
||||
.doc()
|
||||
|
||||
const anteBet = getFreeAnswerAnte(
|
||||
creator,
|
||||
contract as FullContract<DPM, FreeResponse>,
|
||||
anteBetDoc.id
|
||||
)
|
||||
await anteBetDoc.set(anteBet)
|
||||
}
|
||||
}
|
||||
|
||||
return { status: 'success', contract }
|
||||
}
|
||||
tags = (tags || []).map((tag: string) =>
|
||||
tag.toString().slice(0, MAX_TAG_LENGTH)
|
||||
)
|
||||
|
||||
outcomeType = outcomeType ?? 'BINARY'
|
||||
if (!['BINARY', 'MULTI', 'FREE_RESPONSE'].includes(outcomeType))
|
||||
throw new APIError(400, 'Invalid outcomeType')
|
||||
|
||||
if (
|
||||
outcomeType === 'BINARY' &&
|
||||
(!initialProb || initialProb < 1 || initialProb > 99)
|
||||
)
|
||||
throw new APIError(400, 'Invalid initial probability')
|
||||
|
||||
// uses utc time on server:
|
||||
const today = new Date().setHours(0, 0, 0, 0)
|
||||
const userContractsCreatedTodaySnapshot = await firestore
|
||||
.collection(`contracts`)
|
||||
.where('creatorId', '==', creator.id)
|
||||
.where('createdTime', '>=', today)
|
||||
.get()
|
||||
const isFree = userContractsCreatedTodaySnapshot.size === 0
|
||||
|
||||
const ante = FIXED_ANTE
|
||||
|
||||
if (
|
||||
ante === undefined ||
|
||||
ante < MINIMUM_ANTE ||
|
||||
(ante > creator.balance && !isFree) ||
|
||||
isNaN(ante) ||
|
||||
!isFinite(ante)
|
||||
)
|
||||
throw new APIError(400, 'Invalid ante')
|
||||
|
||||
console.log(
|
||||
'creating contract for',
|
||||
creator.username,
|
||||
'on',
|
||||
question,
|
||||
'ante:',
|
||||
ante || 0
|
||||
)
|
||||
|
||||
const slug = await getSlug(question)
|
||||
|
||||
const contractRef = firestore.collection('contracts').doc()
|
||||
|
||||
const contract = getNewContract(
|
||||
contractRef.id,
|
||||
slug,
|
||||
creator,
|
||||
question,
|
||||
outcomeType,
|
||||
description,
|
||||
initialProb,
|
||||
ante,
|
||||
closeTime,
|
||||
tags ?? [],
|
||||
manaLimitPerUser ?? 0
|
||||
)
|
||||
|
||||
if (!isFree && ante) await chargeUser(creator.id, ante, true)
|
||||
|
||||
await contractRef.create(contract)
|
||||
|
||||
if (ante) {
|
||||
if (outcomeType === 'BINARY' && contract.mechanism === 'dpm-2') {
|
||||
const yesBetDoc = firestore
|
||||
.collection(`contracts/${contract.id}/bets`)
|
||||
.doc()
|
||||
|
||||
const noBetDoc = firestore
|
||||
.collection(`contracts/${contract.id}/bets`)
|
||||
.doc()
|
||||
|
||||
const { yesBet, noBet } = getAnteBets(
|
||||
creator,
|
||||
contract as FullContract<DPM, Binary>,
|
||||
yesBetDoc.id,
|
||||
noBetDoc.id
|
||||
)
|
||||
|
||||
await yesBetDoc.set(yesBet)
|
||||
await noBetDoc.set(noBet)
|
||||
} else if (outcomeType === 'BINARY') {
|
||||
const liquidityDoc = firestore
|
||||
.collection(`contracts/${contract.id}/liquidity`)
|
||||
.doc()
|
||||
|
||||
const providerId = isFree ? HOUSE_LIQUIDITY_PROVIDER_ID : creator.id
|
||||
|
||||
const lp = getCpmmInitialLiquidity(
|
||||
providerId,
|
||||
contract as FullContract<CPMM, Binary>,
|
||||
liquidityDoc.id,
|
||||
ante
|
||||
)
|
||||
|
||||
await liquidityDoc.set(lp)
|
||||
} else if (outcomeType === 'FREE_RESPONSE') {
|
||||
const noneAnswerDoc = firestore
|
||||
.collection(`contracts/${contract.id}/answers`)
|
||||
.doc('0')
|
||||
|
||||
const noneAnswer = getNoneAnswer(contract.id, creator)
|
||||
await noneAnswerDoc.set(noneAnswer)
|
||||
|
||||
const anteBetDoc = firestore
|
||||
.collection(`contracts/${contract.id}/bets`)
|
||||
.doc()
|
||||
|
||||
const anteBet = getFreeAnswerAnte(
|
||||
creator,
|
||||
contract as FullContract<DPM, FreeResponse>,
|
||||
anteBetDoc.id
|
||||
)
|
||||
await anteBetDoc.set(anteBet)
|
||||
}
|
||||
}
|
||||
|
||||
return { contract: contract }
|
||||
})
|
||||
|
||||
const getSlug = async (question: string) => {
|
||||
const proposedSlug = slugify(question)
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as functions from 'firebase-functions'
|
||||
import * as admin from 'firebase-admin'
|
||||
|
||||
import { APIError, newEndpoint, parseCredentials, lookupUser } from './api'
|
||||
import { Contract } from '../../common/contract'
|
||||
import { User } from '../../common/user'
|
||||
import {
|
||||
|
@ -14,146 +14,128 @@ import { redeemShares } from './redeem-shares'
|
|||
import { Fees } from '../../common/fees'
|
||||
import { hasUserHitManaLimit } from '../../common/calculate'
|
||||
|
||||
export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||
async (
|
||||
data: {
|
||||
amount: number
|
||||
outcome: string
|
||||
contractId: string
|
||||
},
|
||||
context
|
||||
) => {
|
||||
const userId = context?.auth?.uid
|
||||
if (!userId) return { status: 'error', message: 'Not authorized' }
|
||||
export const placeBet = newEndpoint(['POST'], async (req, _res) => {
|
||||
const [bettor, _privateUser] = await lookupUser(await parseCredentials(req))
|
||||
const { amount, outcome, contractId } = req.body.data || {}
|
||||
|
||||
const { amount, outcome, contractId } = data
|
||||
if (amount <= 0 || isNaN(amount) || !isFinite(amount))
|
||||
throw new APIError(400, 'Invalid amount')
|
||||
|
||||
if (amount <= 0 || isNaN(amount) || !isFinite(amount))
|
||||
return { status: 'error', message: 'Invalid amount' }
|
||||
if (outcome !== 'YES' && outcome !== 'NO' && isNaN(+outcome))
|
||||
throw new APIError(400, 'Invalid outcome')
|
||||
|
||||
if (outcome !== 'YES' && outcome !== 'NO' && isNaN(+outcome))
|
||||
return { status: 'error', message: 'Invalid outcome' }
|
||||
// run as transaction to prevent race conditions
|
||||
return await firestore
|
||||
.runTransaction(async (transaction) => {
|
||||
const userDoc = firestore.doc(`users/${bettor.id}`)
|
||||
const userSnap = await transaction.get(userDoc)
|
||||
if (!userSnap.exists) throw new APIError(400, 'User not found')
|
||||
const user = userSnap.data() as User
|
||||
|
||||
// run as transaction to prevent race conditions
|
||||
return 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
|
||||
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
|
||||
|
||||
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 { closeTime, outcomeType, mechanism, collectedFees, volume } =
|
||||
contract
|
||||
if (closeTime && Date.now() > closeTime)
|
||||
throw new APIError(400, 'Trading is closed')
|
||||
|
||||
const { closeTime, outcomeType, mechanism, collectedFees, volume } =
|
||||
contract
|
||||
if (closeTime && Date.now() > closeTime)
|
||||
return { status: 'error', message: 'Trading is closed' }
|
||||
const yourBetsSnap = await transaction.get(
|
||||
contractDoc.collection('bets').where('userId', '==', bettor.id)
|
||||
)
|
||||
const yourBets = yourBetsSnap.docs.map((doc) => doc.data() as Bet)
|
||||
|
||||
const yourBetsSnap = await transaction.get(
|
||||
contractDoc.collection('bets').where('userId', '==', userId)
|
||||
const loanAmount = 0 // getLoanAmount(yourBets, amount)
|
||||
if (user.balance < amount) throw new APIError(400, 'Insufficient balance')
|
||||
|
||||
if (outcomeType === 'FREE_RESPONSE') {
|
||||
const answerSnap = await transaction.get(
|
||||
contractDoc.collection('answers').doc(outcome)
|
||||
)
|
||||
const yourBets = yourBetsSnap.docs.map((doc) => doc.data() as Bet)
|
||||
if (!answerSnap.exists) throw new APIError(400, 'Invalid contract')
|
||||
|
||||
const loanAmount = 0 // getLoanAmount(yourBets, amount)
|
||||
if (user.balance < amount)
|
||||
return { status: 'error', message: 'Insufficient balance' }
|
||||
const { status, message } = hasUserHitManaLimit(
|
||||
contract,
|
||||
yourBets,
|
||||
amount
|
||||
)
|
||||
if (status === 'error') throw new APIError(400, message)
|
||||
}
|
||||
|
||||
if (outcomeType === 'FREE_RESPONSE') {
|
||||
const answerSnap = await transaction.get(
|
||||
contractDoc.collection('answers').doc(outcome)
|
||||
)
|
||||
if (!answerSnap.exists)
|
||||
return { status: 'error', message: 'Invalid contract' }
|
||||
const newBetDoc = firestore
|
||||
.collection(`contracts/${contractId}/bets`)
|
||||
.doc()
|
||||
|
||||
const { status, message } = hasUserHitManaLimit(
|
||||
contract,
|
||||
yourBets,
|
||||
amount
|
||||
)
|
||||
if (status === 'error') return { status, message: message }
|
||||
}
|
||||
|
||||
const newBetDoc = firestore
|
||||
.collection(`contracts/${contractId}/bets`)
|
||||
.doc()
|
||||
|
||||
const {
|
||||
newBet,
|
||||
newPool,
|
||||
newTotalShares,
|
||||
newTotalBets,
|
||||
newBalance,
|
||||
newTotalLiquidity,
|
||||
fees,
|
||||
newP,
|
||||
} =
|
||||
outcomeType === 'BINARY'
|
||||
? mechanism === 'dpm-2'
|
||||
? getNewBinaryDpmBetInfo(
|
||||
user,
|
||||
outcome as 'YES' | 'NO',
|
||||
amount,
|
||||
contract,
|
||||
loanAmount,
|
||||
newBetDoc.id
|
||||
)
|
||||
: (getNewBinaryCpmmBetInfo(
|
||||
user,
|
||||
outcome as 'YES' | 'NO',
|
||||
amount,
|
||||
contract,
|
||||
loanAmount,
|
||||
newBetDoc.id
|
||||
) as any)
|
||||
: getNewMultiBetInfo(
|
||||
const {
|
||||
newBet,
|
||||
newPool,
|
||||
newTotalShares,
|
||||
newTotalBets,
|
||||
newBalance,
|
||||
newTotalLiquidity,
|
||||
fees,
|
||||
newP,
|
||||
} =
|
||||
outcomeType === 'BINARY'
|
||||
? mechanism === 'dpm-2'
|
||||
? getNewBinaryDpmBetInfo(
|
||||
user,
|
||||
outcome,
|
||||
outcome as 'YES' | 'NO',
|
||||
amount,
|
||||
contract as any,
|
||||
contract,
|
||||
loanAmount,
|
||||
newBetDoc.id
|
||||
)
|
||||
: (getNewBinaryCpmmBetInfo(
|
||||
user,
|
||||
outcome as 'YES' | 'NO',
|
||||
amount,
|
||||
contract,
|
||||
loanAmount,
|
||||
newBetDoc.id
|
||||
) as any)
|
||||
: getNewMultiBetInfo(
|
||||
user,
|
||||
outcome,
|
||||
amount,
|
||||
contract as any,
|
||||
loanAmount,
|
||||
newBetDoc.id
|
||||
)
|
||||
|
||||
if (newP !== undefined && !isFinite(newP)) {
|
||||
return {
|
||||
status: 'error',
|
||||
message: 'Trade rejected due to overflow error.',
|
||||
}
|
||||
}
|
||||
if (newP !== undefined && !isFinite(newP)) {
|
||||
throw new APIError(400, 'Trade rejected due to overflow error.')
|
||||
}
|
||||
|
||||
transaction.create(newBetDoc, newBet)
|
||||
transaction.create(newBetDoc, newBet)
|
||||
|
||||
transaction.update(
|
||||
contractDoc,
|
||||
removeUndefinedProps({
|
||||
pool: newPool,
|
||||
p: newP,
|
||||
totalShares: newTotalShares,
|
||||
totalBets: newTotalBets,
|
||||
totalLiquidity: newTotalLiquidity,
|
||||
collectedFees: addObjects<Fees>(fees ?? {}, collectedFees ?? {}),
|
||||
volume: volume + Math.abs(amount),
|
||||
})
|
||||
)
|
||||
transaction.update(
|
||||
contractDoc,
|
||||
removeUndefinedProps({
|
||||
pool: newPool,
|
||||
p: newP,
|
||||
totalShares: newTotalShares,
|
||||
totalBets: newTotalBets,
|
||||
totalLiquidity: newTotalLiquidity,
|
||||
collectedFees: addObjects<Fees>(fees ?? {}, collectedFees ?? {}),
|
||||
volume: volume + Math.abs(amount),
|
||||
})
|
||||
)
|
||||
|
||||
if (!isFinite(newBalance)) {
|
||||
throw new Error('Invalid user balance for ' + user.username)
|
||||
}
|
||||
if (!isFinite(newBalance)) {
|
||||
throw new APIError(500, 'Invalid user balance for ' + user.username)
|
||||
}
|
||||
|
||||
transaction.update(userDoc, { balance: newBalance })
|
||||
transaction.update(userDoc, { balance: newBalance })
|
||||
|
||||
return { status: 'success', betId: newBetDoc.id }
|
||||
})
|
||||
.then(async (result) => {
|
||||
await redeemShares(userId, contractId)
|
||||
return result
|
||||
})
|
||||
}
|
||||
)
|
||||
return { betId: newBetDoc.id }
|
||||
})
|
||||
.then(async (result) => {
|
||||
await redeemShares(bettor.id, contractId)
|
||||
return result
|
||||
})
|
||||
})
|
||||
|
||||
const firestore = admin.firestore()
|
||||
|
|
|
@ -214,7 +214,7 @@ function BuyPanel(props: {
|
|||
|
||||
useEffect(() => {
|
||||
// warm up cloud function
|
||||
placeBet({}).catch()
|
||||
placeBet({}).catch(() => {})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -553,7 +553,10 @@ function BetRow(props: { bet: Bet; contract: Contract; saleBet?: Bet }) {
|
|||
)
|
||||
}
|
||||
|
||||
const warmUpSellBet = _.throttle(() => sellBet({}).catch(), 5000 /* ms */)
|
||||
const warmUpSellBet = _.throttle(
|
||||
() => sellBet({}).catch(() => {}),
|
||||
5000 /* ms */
|
||||
)
|
||||
|
||||
function SellButton(props: { contract: Contract; bet: Bet }) {
|
||||
useEffect(() => {
|
||||
|
|
|
@ -20,7 +20,7 @@ export function ResolutionPanel(props: {
|
|||
}) {
|
||||
useEffect(() => {
|
||||
// warm up cloud function
|
||||
resolveMarket({} as any).catch()
|
||||
resolveMarket({} as any).catch(() => {})
|
||||
}, [])
|
||||
|
||||
const { contract, className } = props
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
"@nivo/line": "0.74.0",
|
||||
"algoliasearch": "4.13.0",
|
||||
"clsx": "1.1.1",
|
||||
"cors": "^2.8.5",
|
||||
"cors": "2.8.5",
|
||||
"daisyui": "1.16.4",
|
||||
"dayjs": "1.10.7",
|
||||
"firebase": "9.6.0",
|
||||
|
|
|
@ -61,7 +61,7 @@ export function NewContract(props: { question: string; tag?: string }) {
|
|||
}, [creator])
|
||||
|
||||
useEffect(() => {
|
||||
createContract({}).catch() // warm up function
|
||||
createContract({}).catch(() => {}) // warm up function
|
||||
}, [])
|
||||
|
||||
const [outcomeType, setOutcomeType] = useState<outcomeType>('BINARY')
|
||||
|
|
|
@ -1874,7 +1874,7 @@ core-util-is@~1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
|
||||
integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
|
||||
|
||||
cors@^2.8.5:
|
||||
cors@2.8.5, cors@^2.8.5:
|
||||
version "2.8.5"
|
||||
resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29"
|
||||
integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==
|
||||
|
|
Loading…
Reference in New Issue
Block a user