Finalize v2 resolvemarket migration (#598)
* Update resolve-market to be a v2 function * Cleanup API error responses * Update frontend to use v2 version of resolvemarket * Appease ESLint * Address review comments * Appease ESLint * Remove unnecessary auth check * Fix logic bug in FR market validation * Make it so you can specify runtime opts for v2 functions * Cleanup to resolve market API resolutions input, fixes * Fix up tiny lint * Last minute cleanup to resolvemarket FR API input validation Co-authored-by: Benjamin <ben@congdon.dev>
This commit is contained in:
parent
2fbbc66029
commit
fc7f19e785
|
@ -48,12 +48,12 @@ export type PayoutInfo = {
|
||||||
|
|
||||||
export const getPayouts = (
|
export const getPayouts = (
|
||||||
outcome: string | undefined,
|
outcome: string | undefined,
|
||||||
resolutions: {
|
|
||||||
[outcome: string]: number
|
|
||||||
},
|
|
||||||
contract: Contract,
|
contract: Contract,
|
||||||
bets: Bet[],
|
bets: Bet[],
|
||||||
liquidities: LiquidityProvision[],
|
liquidities: LiquidityProvision[],
|
||||||
|
resolutions?: {
|
||||||
|
[outcome: string]: number
|
||||||
|
},
|
||||||
resolutionProbability?: number
|
resolutionProbability?: number
|
||||||
): PayoutInfo => {
|
): PayoutInfo => {
|
||||||
if (contract.mechanism === 'cpmm-1' && contract.outcomeType === 'BINARY') {
|
if (contract.mechanism === 'cpmm-1' && contract.outcomeType === 'BINARY') {
|
||||||
|
@ -67,9 +67,9 @@ export const getPayouts = (
|
||||||
}
|
}
|
||||||
return getDpmPayouts(
|
return getDpmPayouts(
|
||||||
outcome,
|
outcome,
|
||||||
resolutions,
|
|
||||||
contract,
|
contract,
|
||||||
bets,
|
bets,
|
||||||
|
resolutions,
|
||||||
resolutionProbability
|
resolutionProbability
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -100,11 +100,11 @@ export const getFixedPayouts = (
|
||||||
|
|
||||||
export const getDpmPayouts = (
|
export const getDpmPayouts = (
|
||||||
outcome: string | undefined,
|
outcome: string | undefined,
|
||||||
resolutions: {
|
|
||||||
[outcome: string]: number
|
|
||||||
},
|
|
||||||
contract: DPMContract,
|
contract: DPMContract,
|
||||||
bets: Bet[],
|
bets: Bet[],
|
||||||
|
resolutions?: {
|
||||||
|
[outcome: string]: number
|
||||||
|
},
|
||||||
resolutionProbability?: number
|
resolutionProbability?: number
|
||||||
): PayoutInfo => {
|
): PayoutInfo => {
|
||||||
const openBets = bets.filter((b) => !b.isSold && !b.sale)
|
const openBets = bets.filter((b) => !b.isSold && !b.sale)
|
||||||
|
@ -115,8 +115,8 @@ export const getDpmPayouts = (
|
||||||
return getDpmStandardPayouts(outcome, contract, openBets)
|
return getDpmStandardPayouts(outcome, contract, openBets)
|
||||||
|
|
||||||
case 'MKT':
|
case 'MKT':
|
||||||
return contract.outcomeType === 'FREE_RESPONSE'
|
return contract.outcomeType === 'FREE_RESPONSE' // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
? getPayoutsMultiOutcome(resolutions, contract, openBets)
|
? getPayoutsMultiOutcome(resolutions!, contract, openBets)
|
||||||
: getDpmMktPayouts(contract, openBets, resolutionProbability)
|
: getDpmMktPayouts(contract, openBets, resolutionProbability)
|
||||||
case 'CANCEL':
|
case 'CANCEL':
|
||||||
case undefined:
|
case undefined:
|
||||||
|
|
|
@ -42,10 +42,10 @@ export function scoreUsersByContract(contract: Contract, bets: Bet[]) {
|
||||||
)
|
)
|
||||||
const { payouts: resolvePayouts } = getPayouts(
|
const { payouts: resolvePayouts } = getPayouts(
|
||||||
resolution as string,
|
resolution as string,
|
||||||
{},
|
|
||||||
contract,
|
contract,
|
||||||
openBets,
|
openBets,
|
||||||
[],
|
[],
|
||||||
|
{},
|
||||||
resolutionProb
|
resolutionProb
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -108,7 +108,12 @@ export const validate = <T extends z.ZodTypeAny>(schema: T, val: unknown) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_OPTS: HttpsOptions = {
|
interface EndpointOptions extends HttpsOptions {
|
||||||
|
methods?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_OPTS = {
|
||||||
|
methods: ['POST'],
|
||||||
minInstances: 1,
|
minInstances: 1,
|
||||||
concurrency: 100,
|
concurrency: 100,
|
||||||
memory: '2GiB',
|
memory: '2GiB',
|
||||||
|
@ -116,12 +121,13 @@ const DEFAULT_OPTS: HttpsOptions = {
|
||||||
cors: [CORS_ORIGIN_MANIFOLD, CORS_ORIGIN_LOCALHOST],
|
cors: [CORS_ORIGIN_MANIFOLD, CORS_ORIGIN_LOCALHOST],
|
||||||
}
|
}
|
||||||
|
|
||||||
export const newEndpoint = (methods: [string], fn: Handler) =>
|
export const newEndpoint = (endpointOpts: EndpointOptions, fn: Handler) => {
|
||||||
onRequest(DEFAULT_OPTS, async (req, res) => {
|
const opts = Object.assign(endpointOpts, DEFAULT_OPTS)
|
||||||
|
return onRequest(opts, async (req, res) => {
|
||||||
log('Request processing started.')
|
log('Request processing started.')
|
||||||
try {
|
try {
|
||||||
if (!methods.includes(req.method)) {
|
if (!opts.methods.includes(req.method)) {
|
||||||
const allowed = methods.join(', ')
|
const allowed = opts.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))
|
||||||
|
@ -140,3 +146,4 @@ export const newEndpoint = (methods: [string], fn: Handler) =>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -50,7 +50,7 @@ const numericSchema = z.object({
|
||||||
max: z.number(),
|
max: z.number(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const createmarket = newEndpoint(['POST'], async (req, auth) => {
|
export const createmarket = newEndpoint({}, async (req, auth) => {
|
||||||
const { question, description, tags, closeTime, outcomeType, groupId } =
|
const { question, description, tags, closeTime, outcomeType, groupId } =
|
||||||
validate(bodySchema, req.body)
|
validate(bodySchema, req.body)
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ const bodySchema = z.object({
|
||||||
about: z.string().min(1).max(MAX_ABOUT_LENGTH).optional(),
|
about: z.string().min(1).max(MAX_ABOUT_LENGTH).optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const creategroup = newEndpoint(['POST'], async (req, auth) => {
|
export const creategroup = newEndpoint({}, async (req, auth) => {
|
||||||
const { name, about, memberIds, anyoneCanJoin } = validate(
|
const { name, about, memberIds, anyoneCanJoin } = validate(
|
||||||
bodySchema,
|
bodySchema,
|
||||||
req.body
|
req.body
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { newEndpoint } from './api'
|
import { newEndpoint } from './api'
|
||||||
|
|
||||||
export const health = newEndpoint(['GET'], async (_req, auth) => {
|
export const health = newEndpoint({ methods: ['GET'] }, async (_req, auth) => {
|
||||||
return {
|
return {
|
||||||
message: 'Server is working.',
|
message: 'Server is working.',
|
||||||
uid: auth.uid,
|
uid: auth.uid,
|
||||||
|
|
|
@ -6,7 +6,6 @@ admin.initializeApp()
|
||||||
// export * from './keep-awake'
|
// export * from './keep-awake'
|
||||||
export * from './claim-manalink'
|
export * from './claim-manalink'
|
||||||
export * from './transact'
|
export * from './transact'
|
||||||
export * from './resolve-market'
|
|
||||||
export * from './stripe'
|
export * from './stripe'
|
||||||
export * from './create-user'
|
export * from './create-user'
|
||||||
export * from './create-answer'
|
export * from './create-answer'
|
||||||
|
@ -37,3 +36,4 @@ export * from './sell-shares'
|
||||||
export * from './create-contract'
|
export * from './create-contract'
|
||||||
export * from './withdraw-liquidity'
|
export * from './withdraw-liquidity'
|
||||||
export * from './create-group'
|
export * from './create-group'
|
||||||
|
export * from './resolve-market'
|
||||||
|
|
|
@ -33,7 +33,7 @@ const numericSchema = z.object({
|
||||||
value: z.number(),
|
value: z.number(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const placebet = newEndpoint(['POST'], async (req, auth) => {
|
export const placebet = newEndpoint({}, async (req, auth) => {
|
||||||
log('Inside endpoint handler.')
|
log('Inside endpoint handler.')
|
||||||
const { amount, contractId } = validate(bodySchema, req.body)
|
const { amount, contractId } = validate(bodySchema, req.body)
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import * as functions from 'firebase-functions'
|
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
|
import { z } from 'zod'
|
||||||
import { difference, uniq, mapValues, groupBy, sumBy } from 'lodash'
|
import { difference, uniq, mapValues, groupBy, sumBy } from 'lodash'
|
||||||
|
|
||||||
import { Contract, resolution, RESOLUTIONS } from '../../common/contract'
|
import { Contract, RESOLUTIONS } from '../../common/contract'
|
||||||
import { User } from '../../common/user'
|
import { User } from '../../common/user'
|
||||||
import { Bet } from '../../common/bet'
|
import { Bet } from '../../common/bet'
|
||||||
import { getUser, isProd, payUser } from './utils'
|
import { getUser, isProd, payUser } from './utils'
|
||||||
|
@ -15,156 +15,150 @@ import {
|
||||||
} from '../../common/payouts'
|
} from '../../common/payouts'
|
||||||
import { removeUndefinedProps } from '../../common/util/object'
|
import { removeUndefinedProps } from '../../common/util/object'
|
||||||
import { LiquidityProvision } from '../../common/liquidity-provision'
|
import { LiquidityProvision } from '../../common/liquidity-provision'
|
||||||
|
import { APIError, newEndpoint, validate } from './api'
|
||||||
|
|
||||||
export const resolveMarket = functions
|
const bodySchema = z.object({
|
||||||
.runWith({ minInstances: 1, secrets: ['MAILGUN_KEY'] })
|
contractId: z.string(),
|
||||||
.https.onCall(
|
})
|
||||||
async (
|
|
||||||
data: {
|
|
||||||
outcome: resolution
|
|
||||||
value?: number
|
|
||||||
contractId: string
|
|
||||||
probabilityInt?: number
|
|
||||||
resolutions?: { [outcome: string]: number }
|
|
||||||
},
|
|
||||||
context
|
|
||||||
) => {
|
|
||||||
const userId = context?.auth?.uid
|
|
||||||
if (!userId) return { status: 'error', message: 'Not authorized' }
|
|
||||||
|
|
||||||
const { outcome, contractId, probabilityInt, resolutions, value } = data
|
const binarySchema = z.object({
|
||||||
|
outcome: z.enum(RESOLUTIONS),
|
||||||
|
probabilityInt: z.number().gte(0).lt(100).optional(),
|
||||||
|
})
|
||||||
|
|
||||||
const contractDoc = firestore.doc(`contracts/${contractId}`)
|
const freeResponseSchema = z.union([
|
||||||
const contractSnap = await contractDoc.get()
|
z.object({
|
||||||
if (!contractSnap.exists)
|
outcome: z.literal('CANCEL'),
|
||||||
return { status: 'error', message: 'Invalid contract' }
|
}),
|
||||||
const contract = contractSnap.data() as Contract
|
z.object({
|
||||||
const { creatorId, outcomeType, closeTime } = contract
|
outcome: z.literal('MKT'),
|
||||||
|
resolutions: z.array(
|
||||||
|
z.object({
|
||||||
|
answer: z.number().int().nonnegative(),
|
||||||
|
pct: z.number().gte(0).lt(100),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
outcome: z.number().int().nonnegative(),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
|
||||||
if (outcomeType === 'BINARY') {
|
const numericSchema = z.object({
|
||||||
if (!RESOLUTIONS.includes(outcome))
|
outcome: z.union([z.literal('CANCEL'), z.string()]),
|
||||||
return { status: 'error', message: 'Invalid outcome' }
|
value: z.number().optional(),
|
||||||
} else if (outcomeType === 'FREE_RESPONSE') {
|
})
|
||||||
if (
|
|
||||||
isNaN(+outcome) &&
|
|
||||||
!(outcome === 'MKT' && resolutions) &&
|
|
||||||
outcome !== 'CANCEL'
|
|
||||||
)
|
|
||||||
return { status: 'error', message: 'Invalid outcome' }
|
|
||||||
} else if (outcomeType === 'NUMERIC') {
|
|
||||||
if (isNaN(+outcome) && outcome !== 'CANCEL')
|
|
||||||
return { status: 'error', message: 'Invalid outcome' }
|
|
||||||
} else {
|
|
||||||
return { status: 'error', message: 'Invalid contract outcomeType' }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value !== undefined && !isFinite(value))
|
const opts = { secrets: ['MAILGUN_KEY'] }
|
||||||
return { status: 'error', message: 'Invalid value' }
|
export const resolvemarket = newEndpoint(opts, async (req, auth) => {
|
||||||
|
const { contractId } = validate(bodySchema, req.body)
|
||||||
|
const userId = auth.uid
|
||||||
|
|
||||||
if (
|
const contractDoc = firestore.doc(`contracts/${contractId}`)
|
||||||
outcomeType === 'BINARY' &&
|
const contractSnap = await contractDoc.get()
|
||||||
probabilityInt !== undefined &&
|
if (!contractSnap.exists)
|
||||||
(probabilityInt < 0 ||
|
throw new APIError(404, 'No contract exists with the provided ID')
|
||||||
probabilityInt > 100 ||
|
const contract = contractSnap.data() as Contract
|
||||||
!isFinite(probabilityInt))
|
const { creatorId, outcomeType, closeTime } = contract
|
||||||
)
|
|
||||||
return { status: 'error', message: 'Invalid probability' }
|
|
||||||
|
|
||||||
if (creatorId !== userId)
|
const { value, resolutions, probabilityInt, outcome } = getResolutionParams(
|
||||||
return { status: 'error', message: 'User not creator of contract' }
|
outcomeType,
|
||||||
|
req.body
|
||||||
if (contract.resolution)
|
|
||||||
return { status: 'error', message: 'Contract already resolved' }
|
|
||||||
|
|
||||||
const creator = await getUser(creatorId)
|
|
||||||
if (!creator) return { status: 'error', message: 'Creator not found' }
|
|
||||||
|
|
||||||
const resolutionProbability =
|
|
||||||
probabilityInt !== undefined ? probabilityInt / 100 : undefined
|
|
||||||
|
|
||||||
const resolutionTime = Date.now()
|
|
||||||
const newCloseTime = closeTime
|
|
||||||
? Math.min(closeTime, resolutionTime)
|
|
||||||
: closeTime
|
|
||||||
|
|
||||||
const betsSnap = await firestore
|
|
||||||
.collection(`contracts/${contractId}/bets`)
|
|
||||||
.get()
|
|
||||||
|
|
||||||
const bets = betsSnap.docs.map((doc) => doc.data() as Bet)
|
|
||||||
|
|
||||||
const liquiditiesSnap = await firestore
|
|
||||||
.collection(`contracts/${contractId}/liquidity`)
|
|
||||||
.get()
|
|
||||||
|
|
||||||
const liquidities = liquiditiesSnap.docs.map(
|
|
||||||
(doc) => doc.data() as LiquidityProvision
|
|
||||||
)
|
|
||||||
|
|
||||||
const { payouts, creatorPayout, liquidityPayouts, collectedFees } =
|
|
||||||
getPayouts(
|
|
||||||
outcome,
|
|
||||||
resolutions ?? {},
|
|
||||||
contract,
|
|
||||||
bets,
|
|
||||||
liquidities,
|
|
||||||
resolutionProbability
|
|
||||||
)
|
|
||||||
|
|
||||||
await contractDoc.update(
|
|
||||||
removeUndefinedProps({
|
|
||||||
isResolved: true,
|
|
||||||
resolution: outcome,
|
|
||||||
resolutionValue: value,
|
|
||||||
resolutionTime,
|
|
||||||
closeTime: newCloseTime,
|
|
||||||
resolutionProbability,
|
|
||||||
resolutions,
|
|
||||||
collectedFees,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
console.log('contract ', contractId, 'resolved to:', outcome)
|
|
||||||
|
|
||||||
const openBets = bets.filter((b) => !b.isSold && !b.sale)
|
|
||||||
const loanPayouts = getLoanPayouts(openBets)
|
|
||||||
|
|
||||||
if (!isProd())
|
|
||||||
console.log(
|
|
||||||
'payouts:',
|
|
||||||
payouts,
|
|
||||||
'creator payout:',
|
|
||||||
creatorPayout,
|
|
||||||
'liquidity payout:'
|
|
||||||
)
|
|
||||||
|
|
||||||
if (creatorPayout)
|
|
||||||
await processPayouts(
|
|
||||||
[{ userId: creatorId, payout: creatorPayout }],
|
|
||||||
true
|
|
||||||
)
|
|
||||||
|
|
||||||
await processPayouts(liquidityPayouts, true)
|
|
||||||
|
|
||||||
const result = await processPayouts([...payouts, ...loanPayouts])
|
|
||||||
|
|
||||||
const userPayoutsWithoutLoans = groupPayoutsByUser(payouts)
|
|
||||||
|
|
||||||
await sendResolutionEmails(
|
|
||||||
openBets,
|
|
||||||
userPayoutsWithoutLoans,
|
|
||||||
creator,
|
|
||||||
creatorPayout,
|
|
||||||
contract,
|
|
||||||
outcome,
|
|
||||||
resolutionProbability,
|
|
||||||
resolutions
|
|
||||||
)
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (creatorId !== userId)
|
||||||
|
throw new APIError(403, 'User is not creator of contract')
|
||||||
|
|
||||||
|
if (contract.resolution) throw new APIError(400, 'Contract already resolved')
|
||||||
|
|
||||||
|
const creator = await getUser(creatorId)
|
||||||
|
if (!creator) throw new APIError(500, 'Creator not found')
|
||||||
|
|
||||||
|
const resolutionProbability =
|
||||||
|
probabilityInt !== undefined ? probabilityInt / 100 : undefined
|
||||||
|
|
||||||
|
const resolutionTime = Date.now()
|
||||||
|
const newCloseTime = closeTime
|
||||||
|
? Math.min(closeTime, resolutionTime)
|
||||||
|
: closeTime
|
||||||
|
|
||||||
|
const betsSnap = await firestore
|
||||||
|
.collection(`contracts/${contractId}/bets`)
|
||||||
|
.get()
|
||||||
|
|
||||||
|
const bets = betsSnap.docs.map((doc) => doc.data() as Bet)
|
||||||
|
|
||||||
|
const liquiditiesSnap = await firestore
|
||||||
|
.collection(`contracts/${contractId}/liquidity`)
|
||||||
|
.get()
|
||||||
|
|
||||||
|
const liquidities = liquiditiesSnap.docs.map(
|
||||||
|
(doc) => doc.data() as LiquidityProvision
|
||||||
|
)
|
||||||
|
|
||||||
|
const { payouts, creatorPayout, liquidityPayouts, collectedFees } =
|
||||||
|
getPayouts(
|
||||||
|
outcome,
|
||||||
|
contract,
|
||||||
|
bets,
|
||||||
|
liquidities,
|
||||||
|
resolutions,
|
||||||
|
resolutionProbability
|
||||||
|
)
|
||||||
|
|
||||||
|
const updatedContract = {
|
||||||
|
...contract,
|
||||||
|
...removeUndefinedProps({
|
||||||
|
isResolved: true,
|
||||||
|
resolution: outcome,
|
||||||
|
resolutionValue: value,
|
||||||
|
resolutionTime,
|
||||||
|
closeTime: newCloseTime,
|
||||||
|
resolutionProbability,
|
||||||
|
resolutions,
|
||||||
|
collectedFees,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
await contractDoc.update(updatedContract)
|
||||||
|
|
||||||
|
console.log('contract ', contractId, 'resolved to:', outcome)
|
||||||
|
|
||||||
|
const openBets = bets.filter((b) => !b.isSold && !b.sale)
|
||||||
|
const loanPayouts = getLoanPayouts(openBets)
|
||||||
|
|
||||||
|
if (!isProd())
|
||||||
|
console.log(
|
||||||
|
'payouts:',
|
||||||
|
payouts,
|
||||||
|
'creator payout:',
|
||||||
|
creatorPayout,
|
||||||
|
'liquidity payout:'
|
||||||
|
)
|
||||||
|
|
||||||
|
if (creatorPayout)
|
||||||
|
await processPayouts([{ userId: creatorId, payout: creatorPayout }], true)
|
||||||
|
|
||||||
|
await processPayouts(liquidityPayouts, true)
|
||||||
|
|
||||||
|
await processPayouts([...payouts, ...loanPayouts])
|
||||||
|
|
||||||
|
const userPayoutsWithoutLoans = groupPayoutsByUser(payouts)
|
||||||
|
|
||||||
|
await sendResolutionEmails(
|
||||||
|
openBets,
|
||||||
|
userPayoutsWithoutLoans,
|
||||||
|
creator,
|
||||||
|
creatorPayout,
|
||||||
|
contract,
|
||||||
|
outcome,
|
||||||
|
resolutionProbability,
|
||||||
|
resolutions
|
||||||
|
)
|
||||||
|
|
||||||
|
return updatedContract
|
||||||
|
})
|
||||||
|
|
||||||
const processPayouts = async (payouts: Payout[], isDeposit = false) => {
|
const processPayouts = async (payouts: Payout[], isDeposit = false) => {
|
||||||
const userPayouts = groupPayoutsByUser(payouts)
|
const userPayouts = groupPayoutsByUser(payouts)
|
||||||
|
|
||||||
|
@ -221,4 +215,38 @@ const sendResolutionEmails = async (
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getResolutionParams(outcomeType: string, body: string) {
|
||||||
|
if (outcomeType === 'NUMERIC') {
|
||||||
|
return {
|
||||||
|
...validate(numericSchema, body),
|
||||||
|
resolutions: undefined,
|
||||||
|
probabilityInt: undefined,
|
||||||
|
}
|
||||||
|
} else if (outcomeType === 'FREE_RESPONSE') {
|
||||||
|
const freeResponseParams = validate(freeResponseSchema, body)
|
||||||
|
const { outcome } = freeResponseParams
|
||||||
|
const resolutions =
|
||||||
|
'resolutions' in freeResponseParams
|
||||||
|
? Object.fromEntries(
|
||||||
|
freeResponseParams.resolutions.map((r) => [r.answer, r.pct])
|
||||||
|
)
|
||||||
|
: undefined
|
||||||
|
return {
|
||||||
|
// Free Response outcome IDs are numbers by convention,
|
||||||
|
// but treated as strings everywhere else.
|
||||||
|
outcome: outcome.toString(),
|
||||||
|
resolutions,
|
||||||
|
value: undefined,
|
||||||
|
probabilityInt: undefined,
|
||||||
|
}
|
||||||
|
} else if (outcomeType === 'BINARY') {
|
||||||
|
return {
|
||||||
|
...validate(binarySchema, body),
|
||||||
|
value: undefined,
|
||||||
|
resolutions: undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new APIError(500, `Invalid outcome type: ${outcomeType}`)
|
||||||
|
}
|
||||||
|
|
||||||
const firestore = admin.firestore()
|
const firestore = admin.firestore()
|
||||||
|
|
|
@ -27,10 +27,10 @@ async function checkIfPayOutAgain(contractRef: DocRef, contract: Contract) {
|
||||||
|
|
||||||
const { payouts } = getPayouts(
|
const { payouts } = getPayouts(
|
||||||
resolution,
|
resolution,
|
||||||
resolutions,
|
|
||||||
contract,
|
contract,
|
||||||
openBets,
|
openBets,
|
||||||
[],
|
[],
|
||||||
|
resolutions,
|
||||||
resolutionProbability
|
resolutionProbability
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ const bodySchema = z.object({
|
||||||
betId: z.string(),
|
betId: z.string(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const sellbet = newEndpoint(['POST'], async (req, auth) => {
|
export const sellbet = newEndpoint({}, async (req, auth) => {
|
||||||
const { contractId, betId } = validate(bodySchema, req.body)
|
const { contractId, betId } = validate(bodySchema, req.body)
|
||||||
|
|
||||||
// run as transaction to prevent race conditions
|
// run as transaction to prevent race conditions
|
||||||
|
|
|
@ -16,7 +16,7 @@ const bodySchema = z.object({
|
||||||
outcome: z.enum(['YES', 'NO']),
|
outcome: z.enum(['YES', 'NO']),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const sellshares = newEndpoint(['POST'], async (req, auth) => {
|
export const sellshares = newEndpoint({}, async (req, auth) => {
|
||||||
const { contractId, shares, outcome } = validate(bodySchema, req.body)
|
const { contractId, shares, outcome } = validate(bodySchema, req.body)
|
||||||
|
|
||||||
// Run as transaction to prevent race conditions.
|
// Run as transaction to prevent race conditions.
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { sum, mapValues } from 'lodash'
|
import { sum } from 'lodash'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
|
||||||
import { Contract, FreeResponse } from 'common/contract'
|
import { Contract, FreeResponse } from 'common/contract'
|
||||||
import { Col } from '../layout/col'
|
import { Col } from '../layout/col'
|
||||||
import { resolveMarket } from 'web/lib/firebase/fn-call'
|
import { APIError, resolveMarket } from 'web/lib/firebase/api-call'
|
||||||
import { Row } from '../layout/row'
|
import { Row } from '../layout/row'
|
||||||
import { ChooseCancelSelector } from '../yes-no-selector'
|
import { ChooseCancelSelector } from '../yes-no-selector'
|
||||||
import { ResolveConfirmationButton } from '../confirmation-button'
|
import { ResolveConfirmationButton } from '../confirmation-button'
|
||||||
|
@ -31,30 +31,34 @@ export function AnswerResolvePanel(props: {
|
||||||
setIsSubmitting(true)
|
setIsSubmitting(true)
|
||||||
|
|
||||||
const totalProb = sum(Object.values(chosenAnswers))
|
const totalProb = sum(Object.values(chosenAnswers))
|
||||||
const normalizedProbs = mapValues(
|
const resolutions = Object.entries(chosenAnswers).map(([i, p]) => {
|
||||||
chosenAnswers,
|
return { answer: parseInt(i), pct: (100 * p) / totalProb }
|
||||||
(prob) => (100 * prob) / totalProb
|
})
|
||||||
)
|
|
||||||
|
|
||||||
const resolutionProps = removeUndefinedProps({
|
const resolutionProps = removeUndefinedProps({
|
||||||
outcome:
|
outcome:
|
||||||
resolveOption === 'CHOOSE'
|
resolveOption === 'CHOOSE'
|
||||||
? answers[0]
|
? parseInt(answers[0])
|
||||||
: resolveOption === 'CHOOSE_MULTIPLE'
|
: resolveOption === 'CHOOSE_MULTIPLE'
|
||||||
? 'MKT'
|
? 'MKT'
|
||||||
: 'CANCEL',
|
: 'CANCEL',
|
||||||
resolutions:
|
resolutions:
|
||||||
resolveOption === 'CHOOSE_MULTIPLE' ? normalizedProbs : undefined,
|
resolveOption === 'CHOOSE_MULTIPLE' ? resolutions : undefined,
|
||||||
contractId: contract.id,
|
contractId: contract.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
const result = await resolveMarket(resolutionProps).then((r) => r.data)
|
try {
|
||||||
|
const result = await resolveMarket(resolutionProps)
|
||||||
console.log('resolved', resolutionProps, 'result:', result)
|
console.log('resolved', resolutionProps, 'result:', result)
|
||||||
|
} catch (e) {
|
||||||
if (result?.status !== 'success') {
|
if (e instanceof APIError) {
|
||||||
setError(result?.message || 'Error resolving market')
|
setError(e.toString())
|
||||||
|
} else {
|
||||||
|
console.error(e)
|
||||||
|
setError('Error resolving market')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setResolveOption(undefined)
|
setResolveOption(undefined)
|
||||||
setIsSubmitting(false)
|
setIsSubmitting(false)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { User } from 'web/lib/firebase/users'
|
||||||
import { NumberCancelSelector } from './yes-no-selector'
|
import { NumberCancelSelector } from './yes-no-selector'
|
||||||
import { Spacer } from './layout/spacer'
|
import { Spacer } from './layout/spacer'
|
||||||
import { ResolveConfirmationButton } from './confirmation-button'
|
import { ResolveConfirmationButton } from './confirmation-button'
|
||||||
import { resolveMarket } from 'web/lib/firebase/fn-call'
|
import { APIError, resolveMarket } from 'web/lib/firebase/api-call'
|
||||||
import { NumericContract } from 'common/contract'
|
import { NumericContract } from 'common/contract'
|
||||||
import { BucketInput } from './bucket-input'
|
import { BucketInput } from './bucket-input'
|
||||||
|
|
||||||
|
@ -37,17 +37,22 @@ export function NumericResolutionPanel(props: {
|
||||||
|
|
||||||
setIsSubmitting(true)
|
setIsSubmitting(true)
|
||||||
|
|
||||||
const result = await resolveMarket({
|
try {
|
||||||
outcome: finalOutcome,
|
const result = await resolveMarket({
|
||||||
value,
|
outcome: finalOutcome,
|
||||||
contractId: contract.id,
|
value,
|
||||||
}).then((r) => r.data)
|
contractId: contract.id,
|
||||||
|
})
|
||||||
console.log('resolved', outcome, 'result:', result)
|
console.log('resolved', outcome, 'result:', result)
|
||||||
|
} catch (e) {
|
||||||
if (result?.status !== 'success') {
|
if (e instanceof APIError) {
|
||||||
setError(result?.message || 'Error resolving market')
|
setError(e.toString())
|
||||||
|
} else {
|
||||||
|
console.error(e)
|
||||||
|
setError('Error resolving market')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsSubmitting(false)
|
setIsSubmitting(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { User } from 'web/lib/firebase/users'
|
||||||
import { YesNoCancelSelector } from './yes-no-selector'
|
import { YesNoCancelSelector } from './yes-no-selector'
|
||||||
import { Spacer } from './layout/spacer'
|
import { Spacer } from './layout/spacer'
|
||||||
import { ResolveConfirmationButton } from './confirmation-button'
|
import { ResolveConfirmationButton } from './confirmation-button'
|
||||||
import { resolveMarket } from 'web/lib/firebase/fn-call'
|
import { APIError, resolveMarket } from 'web/lib/firebase/api-call'
|
||||||
import { ProbabilitySelector } from './probability-selector'
|
import { ProbabilitySelector } from './probability-selector'
|
||||||
import { DPM_CREATOR_FEE } from 'common/fees'
|
import { DPM_CREATOR_FEE } from 'common/fees'
|
||||||
import { getProbability } from 'common/calculate'
|
import { getProbability } from 'common/calculate'
|
||||||
|
@ -42,17 +42,22 @@ export function ResolutionPanel(props: {
|
||||||
|
|
||||||
setIsSubmitting(true)
|
setIsSubmitting(true)
|
||||||
|
|
||||||
const result = await resolveMarket({
|
try {
|
||||||
outcome,
|
const result = await resolveMarket({
|
||||||
contractId: contract.id,
|
outcome,
|
||||||
probabilityInt: prob,
|
contractId: contract.id,
|
||||||
}).then((r) => r.data)
|
probabilityInt: prob,
|
||||||
|
})
|
||||||
console.log('resolved', outcome, 'result:', result)
|
console.log('resolved', outcome, 'result:', result)
|
||||||
|
} catch (e) {
|
||||||
if (result?.status !== 'success') {
|
if (e instanceof APIError) {
|
||||||
setError(result?.message || 'Error resolving market')
|
setError(e.toString())
|
||||||
|
} else {
|
||||||
|
console.error(e)
|
||||||
|
setError('Error resolving market')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsSubmitting(false)
|
setIsSubmitting(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -54,6 +54,10 @@ export function createMarket(params: any) {
|
||||||
return call(getFunctionUrl('createmarket'), 'POST', params)
|
return call(getFunctionUrl('createmarket'), 'POST', params)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resolveMarket(params: any) {
|
||||||
|
return call(getFunctionUrl('resolvemarket'), 'POST', params)
|
||||||
|
}
|
||||||
|
|
||||||
export function placeBet(params: any) {
|
export function placeBet(params: any) {
|
||||||
return call(getFunctionUrl('placebet'), 'POST', params)
|
return call(getFunctionUrl('placebet'), 'POST', params)
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,17 +29,6 @@ export const createAnswer = cloudFunction<
|
||||||
}
|
}
|
||||||
>('createAnswer')
|
>('createAnswer')
|
||||||
|
|
||||||
export const resolveMarket = cloudFunction<
|
|
||||||
{
|
|
||||||
outcome: string
|
|
||||||
value?: number
|
|
||||||
contractId: string
|
|
||||||
probabilityInt?: number
|
|
||||||
resolutions?: { [outcome: string]: number }
|
|
||||||
},
|
|
||||||
{ status: 'error' | 'success'; message?: string }
|
|
||||||
>('resolveMarket')
|
|
||||||
|
|
||||||
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')
|
||||||
|
|
Loading…
Reference in New Issue
Block a user