Market Resolution API (#600)
* Add market resolution API * Add additional free market resolution validation * Address review comments * Refactor resolution validation code somewhat Co-authored-by: Marshall Polaris <marshall@pol.rs>
This commit is contained in:
parent
a5a0a1370a
commit
c5efd5b7d0
|
@ -456,7 +456,6 @@ Requires no authorization.
|
|||
}
|
||||
```
|
||||
|
||||
|
||||
### `POST /v0/bet`
|
||||
|
||||
Places a new bet on behalf of the authorized user.
|
||||
|
@ -514,6 +513,60 @@ $ curl https://manifold.markets/api/v0/market -X POST -H 'Content-Type: applicat
|
|||
"initialProb":25}'
|
||||
```
|
||||
|
||||
### `POST /v0/market/[marketId]/resolve`
|
||||
|
||||
Resolves a market on behalf of the authorized user.
|
||||
|
||||
Parameters:
|
||||
|
||||
For binary markets:
|
||||
|
||||
- `outcome`: Required. One of `YES`, `NO`, `MKT`, or `CANCEL`.
|
||||
- `probabilityInt`: Optional. The probability to use for `MKT` resolution.
|
||||
|
||||
For free response markets:
|
||||
|
||||
- `outcome`: Required. One of `MKT`, `CANCEL`, or a `number` indicating the Free Response outcome ID.
|
||||
- `resolutions`: A map of from outcome => number to use as the weights for resolving in favor of multiple free response options. Can only be set with `MKT` outcome.
|
||||
|
||||
For numeric markets:
|
||||
|
||||
- `outcome`: Required. One of `CANCEL`, or a `number` indicating the selected numeric bucket ID.
|
||||
- `value`: The value that the market may resolves to.
|
||||
|
||||
Example request:
|
||||
|
||||
```
|
||||
# Resolve a binary market
|
||||
$ curl https://manifold.markets/api/v0/market -X POST -H 'Content-Type: application/json' \
|
||||
-H 'Authorization: Key {...}'
|
||||
--data-raw '{"contractId":"{...}", \
|
||||
"outcome":"YES"}'
|
||||
|
||||
# Resolve a binary market with a specified probability
|
||||
$ curl https://manifold.markets/api/v0/market -X POST -H 'Content-Type: application/json' \
|
||||
-H 'Authorization: Key {...}'
|
||||
--data-raw '{"contractId":"{...}", \
|
||||
"outcome":"MKT",
|
||||
"probabilityInt": 75}'
|
||||
|
||||
# Resolve a free response market with a single answer chosen
|
||||
$ curl https://manifold.markets/api/v0/market -X POST -H 'Content-Type: application/json' \
|
||||
-H 'Authorization: Key {...}'
|
||||
--data-raw '{"contractId":"{...}", \
|
||||
"outcome":"{...}"}'
|
||||
|
||||
# Resolve a free response market with multiple answers chosen
|
||||
$ curl https://manifold.markets/api/v0/market -X POST -H 'Content-Type: application/json' \
|
||||
-H 'Authorization: Key {...}'
|
||||
--data-raw '{"contractId":"{...}", \
|
||||
"outcome":"MKT",
|
||||
"resolutions": {
|
||||
"{...}": 1,
|
||||
"{...}": 2,
|
||||
}}'
|
||||
```
|
||||
|
||||
## Changelog
|
||||
|
||||
- 2022-06-08: Add paging to markets endpoint
|
||||
|
|
|
@ -2,7 +2,11 @@ import * as admin from 'firebase-admin'
|
|||
import { z } from 'zod'
|
||||
import { difference, uniq, mapValues, groupBy, sumBy } from 'lodash'
|
||||
|
||||
import { Contract, RESOLUTIONS } from '../../common/contract'
|
||||
import {
|
||||
Contract,
|
||||
FreeResponseContract,
|
||||
RESOLUTIONS,
|
||||
} from '../../common/contract'
|
||||
import { User } from '../../common/user'
|
||||
import { Bet } from '../../common/bet'
|
||||
import { getUser, isProd, payUser } from './utils'
|
||||
|
@ -59,10 +63,10 @@ export const resolvemarket = newEndpoint(opts, async (req, auth) => {
|
|||
if (!contractSnap.exists)
|
||||
throw new APIError(404, 'No contract exists with the provided ID')
|
||||
const contract = contractSnap.data() as Contract
|
||||
const { creatorId, outcomeType, closeTime } = contract
|
||||
const { creatorId, closeTime } = contract
|
||||
|
||||
const { value, resolutions, probabilityInt, outcome } = getResolutionParams(
|
||||
outcomeType,
|
||||
contract,
|
||||
req.body
|
||||
)
|
||||
|
||||
|
@ -215,7 +219,8 @@ const sendResolutionEmails = async (
|
|||
)
|
||||
}
|
||||
|
||||
function getResolutionParams(outcomeType: string, body: string) {
|
||||
function getResolutionParams(contract: Contract, body: string) {
|
||||
const { outcomeType } = contract
|
||||
if (outcomeType === 'NUMERIC') {
|
||||
return {
|
||||
...validate(numericSchema, body),
|
||||
|
@ -225,20 +230,40 @@ function getResolutionParams(outcomeType: string, body: string) {
|
|||
} 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
|
||||
switch (outcome) {
|
||||
case 'CANCEL':
|
||||
return {
|
||||
// Free Response outcome IDs are numbers by convention,
|
||||
// but treated as strings everywhere else.
|
||||
outcome: outcome.toString(),
|
||||
resolutions,
|
||||
resolutions: undefined,
|
||||
value: undefined,
|
||||
probabilityInt: undefined,
|
||||
}
|
||||
case 'MKT': {
|
||||
const { resolutions } = freeResponseParams
|
||||
resolutions.forEach(({ answer }) => validateAnswer(contract, answer))
|
||||
const pctSum = sumBy(resolutions, ({ pct }) => pct)
|
||||
if (Math.abs(pctSum - 100) > 0.1) {
|
||||
throw new APIError(400, 'Resolution percentages must sum to 100')
|
||||
}
|
||||
return {
|
||||
outcome: outcome.toString(),
|
||||
resolutions: Object.fromEntries(
|
||||
resolutions.map((r) => [r.answer, r.pct])
|
||||
),
|
||||
value: undefined,
|
||||
probabilityInt: undefined,
|
||||
}
|
||||
}
|
||||
default: {
|
||||
validateAnswer(contract, outcome)
|
||||
return {
|
||||
outcome: outcome.toString(),
|
||||
resolutions: undefined,
|
||||
value: undefined,
|
||||
probabilityInt: undefined,
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (outcomeType === 'BINARY') {
|
||||
return {
|
||||
...validate(binarySchema, body),
|
||||
|
@ -249,4 +274,11 @@ function getResolutionParams(outcomeType: string, body: string) {
|
|||
throw new APIError(500, `Invalid outcome type: ${outcomeType}`)
|
||||
}
|
||||
|
||||
function validateAnswer(contract: FreeResponseContract, answer: number) {
|
||||
const validIds = contract.answers.map((a) => a.id)
|
||||
if (!validIds.includes(answer.toString())) {
|
||||
throw new APIError(400, `${answer} is not a valid answer ID`)
|
||||
}
|
||||
}
|
||||
|
||||
const firestore = admin.firestore()
|
||||
|
|
|
@ -41,7 +41,12 @@ export const fetchBackend = (req: NextApiRequest, name: string) => {
|
|||
'Origin',
|
||||
])
|
||||
const hasBody = req.method != 'HEAD' && req.method != 'GET'
|
||||
const opts = { headers, method: req.method, body: hasBody ? req : undefined }
|
||||
const body = req.body ? JSON.stringify(req.body) : req
|
||||
const opts = {
|
||||
headers,
|
||||
method: req.method,
|
||||
body: hasBody ? body : undefined,
|
||||
}
|
||||
return fetch(url, opts)
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import { listAllBets } from 'web/lib/firebase/bets'
|
|||
import { listAllComments } from 'web/lib/firebase/comments'
|
||||
import { getContractFromId } from 'web/lib/firebase/contracts'
|
||||
import { applyCorsHeaders, CORS_UNRESTRICTED } from 'web/lib/api/cors'
|
||||
import { FullMarket, ApiError, toFullMarket } from '../_types'
|
||||
import { FullMarket, ApiError, toFullMarket } from '../../_types'
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
28
web/pages/api/v0/market/[id]/resolve.ts
Normal file
28
web/pages/api/v0/market/[id]/resolve.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import {
|
||||
CORS_ORIGIN_MANIFOLD,
|
||||
CORS_ORIGIN_LOCALHOST,
|
||||
} from 'common/envs/constants'
|
||||
import { applyCorsHeaders } from 'web/lib/api/cors'
|
||||
import { fetchBackend, forwardResponse } from 'web/lib/api/proxy'
|
||||
|
||||
export const config = { api: { bodyParser: true } }
|
||||
|
||||
export default async function route(req: NextApiRequest, res: NextApiResponse) {
|
||||
await applyCorsHeaders(req, res, {
|
||||
origin: [CORS_ORIGIN_MANIFOLD, CORS_ORIGIN_LOCALHOST],
|
||||
methods: 'POST',
|
||||
})
|
||||
|
||||
const { id } = req.query
|
||||
const contractId = id as string
|
||||
|
||||
if (req.body) req.body.contractId = contractId
|
||||
try {
|
||||
const backendRes = await fetchBackend(req, 'resolvemarket')
|
||||
await forwardResponse(res, backendRes)
|
||||
} catch (err) {
|
||||
console.error('Error talking to cloud function: ', err)
|
||||
res.status(500).json({ message: 'Error communicating with backend.' })
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user