2022-01-05 05:51:26 +00:00
|
|
|
import * as admin from 'firebase-admin'
|
2022-05-26 21:37:51 +00:00
|
|
|
import { z } from 'zod'
|
2022-05-12 15:07:10 +00:00
|
|
|
|
2022-03-09 17:08:57 +00:00
|
|
|
import {
|
2022-06-01 02:42:35 +00:00
|
|
|
CPMMBinaryContract,
|
2022-03-09 17:08:57 +00:00
|
|
|
Contract,
|
2022-06-01 02:42:35 +00:00
|
|
|
FreeResponseContract,
|
2022-03-09 17:08:57 +00:00
|
|
|
MAX_DESCRIPTION_LENGTH,
|
|
|
|
MAX_QUESTION_LENGTH,
|
|
|
|
MAX_TAG_LENGTH,
|
2022-06-01 02:42:35 +00:00
|
|
|
NumericContract,
|
2022-05-19 17:42:03 +00:00
|
|
|
OUTCOME_TYPES,
|
2022-05-15 17:39:42 +00:00
|
|
|
} from '../../common/contract'
|
|
|
|
import { slugify } from '../../common/util/slugify'
|
|
|
|
import { randomString } from '../../common/util/random'
|
2022-05-19 17:42:03 +00:00
|
|
|
|
|
|
|
import { chargeUser } from './utils'
|
2022-05-26 21:37:51 +00:00
|
|
|
import { APIError, newEndpoint, validate, zTimestamp } from './api'
|
2022-05-19 17:42:03 +00:00
|
|
|
|
2022-02-17 23:00:19 +00:00
|
|
|
import {
|
2022-04-09 18:51:22 +00:00
|
|
|
FIXED_ANTE,
|
2022-03-15 22:27:51 +00:00
|
|
|
getCpmmInitialLiquidity,
|
2022-02-17 23:00:19 +00:00
|
|
|
getFreeAnswerAnte,
|
2022-05-19 17:42:03 +00:00
|
|
|
getNumericAnte,
|
2022-05-15 17:39:42 +00:00
|
|
|
} from '../../common/antes'
|
|
|
|
import { getNoneAnswer } from '../../common/answer'
|
2022-05-19 17:42:03 +00:00
|
|
|
import { getNewContract } from '../../common/new-contract'
|
|
|
|
import { NUMERIC_BUCKET_COUNT } from '../../common/numeric-constants'
|
2022-06-11 00:51:55 +00:00
|
|
|
import { User } from '../../common/user'
|
2022-06-22 16:35:50 +00:00
|
|
|
import { Group, MAX_ID_LENGTH } from '../../common/group'
|
2022-01-05 05:51:26 +00:00
|
|
|
|
2022-05-26 21:37:51 +00:00
|
|
|
const bodySchema = z.object({
|
|
|
|
question: z.string().min(1).max(MAX_QUESTION_LENGTH),
|
|
|
|
description: z.string().max(MAX_DESCRIPTION_LENGTH),
|
|
|
|
tags: z.array(z.string().min(1).max(MAX_TAG_LENGTH)).optional(),
|
|
|
|
closeTime: zTimestamp().refine(
|
|
|
|
(date) => date.getTime() > new Date().getTime(),
|
|
|
|
'Close time must be in the future.'
|
|
|
|
),
|
|
|
|
outcomeType: z.enum(OUTCOME_TYPES),
|
2022-06-22 16:35:50 +00:00
|
|
|
groupId: z.string().min(1).max(MAX_ID_LENGTH).optional(),
|
2022-05-26 21:37:51 +00:00
|
|
|
})
|
2022-05-17 04:43:40 +00:00
|
|
|
|
2022-05-26 21:37:51 +00:00
|
|
|
const binarySchema = z.object({
|
|
|
|
initialProb: z.number().min(1).max(99),
|
|
|
|
})
|
2022-05-19 17:42:03 +00:00
|
|
|
|
2022-05-26 21:37:51 +00:00
|
|
|
const numericSchema = z.object({
|
|
|
|
min: z.number(),
|
|
|
|
max: z.number(),
|
|
|
|
})
|
2022-05-17 04:43:40 +00:00
|
|
|
|
2022-06-11 00:51:55 +00:00
|
|
|
export const createmarket = newEndpoint(['POST'], async (req, auth) => {
|
2022-06-22 16:35:50 +00:00
|
|
|
const { question, description, tags, closeTime, outcomeType, groupId } =
|
|
|
|
validate(bodySchema, req.body)
|
2022-05-19 17:42:03 +00:00
|
|
|
|
2022-05-26 21:37:51 +00:00
|
|
|
let min, max, initialProb
|
|
|
|
if (outcomeType === 'NUMERIC') {
|
|
|
|
;({ min, max } = validate(numericSchema, req.body))
|
|
|
|
if (max - min <= 0.01) throw new APIError(400, 'Invalid range.')
|
|
|
|
}
|
|
|
|
if (outcomeType === 'BINARY') {
|
|
|
|
;({ initialProb } = validate(binarySchema, req.body))
|
|
|
|
}
|
2022-05-17 04:43:40 +00:00
|
|
|
|
2022-06-11 00:51:55 +00:00
|
|
|
const userDoc = await firestore.collection('users').doc(auth.uid).get()
|
|
|
|
if (!userDoc.exists) {
|
|
|
|
throw new APIError(400, 'No user exists with the authenticated user ID.')
|
|
|
|
}
|
|
|
|
const user = userDoc.data() as User
|
|
|
|
|
2022-05-17 04:43:40 +00:00
|
|
|
const ante = FIXED_ANTE
|
|
|
|
|
2022-06-12 21:23:23 +00:00
|
|
|
// TODO: this is broken because it's not in a transaction
|
2022-06-23 18:00:14 +00:00
|
|
|
if (ante > user.balance)
|
2022-06-12 21:23:23 +00:00
|
|
|
throw new APIError(400, `Balance must be at least ${ante}.`)
|
|
|
|
|
2022-06-22 22:19:17 +00:00
|
|
|
const slug = await getSlug(question)
|
|
|
|
const contractRef = firestore.collection('contracts').doc()
|
|
|
|
|
|
|
|
let group = null
|
|
|
|
if (groupId) {
|
|
|
|
const groupDocRef = await firestore.collection('groups').doc(groupId)
|
|
|
|
const groupDoc = await groupDocRef.get()
|
|
|
|
if (!groupDoc.exists) {
|
|
|
|
throw new APIError(400, 'No group exists with the given group ID.')
|
|
|
|
}
|
|
|
|
|
|
|
|
group = groupDoc.data() as Group
|
|
|
|
if (!group.memberIds.includes(user.id)) {
|
|
|
|
throw new APIError(
|
|
|
|
400,
|
|
|
|
'User must be a member of the group to add markets to it.'
|
|
|
|
)
|
|
|
|
}
|
|
|
|
if (!group.contractIds.includes(contractRef.id))
|
|
|
|
await groupDocRef.update({
|
|
|
|
contractIds: [...group.contractIds, contractRef.id],
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-05-17 04:43:40 +00:00
|
|
|
console.log(
|
|
|
|
'creating contract for',
|
2022-05-26 21:37:51 +00:00
|
|
|
user.username,
|
2022-05-17 04:43:40 +00:00
|
|
|
'on',
|
|
|
|
question,
|
|
|
|
'ante:',
|
|
|
|
ante || 0
|
|
|
|
)
|
|
|
|
|
|
|
|
const contract = getNewContract(
|
|
|
|
contractRef.id,
|
|
|
|
slug,
|
2022-05-26 21:37:51 +00:00
|
|
|
user,
|
2022-05-17 04:43:40 +00:00
|
|
|
question,
|
|
|
|
outcomeType,
|
|
|
|
description,
|
2022-05-26 21:37:51 +00:00
|
|
|
initialProb ?? 0,
|
2022-05-17 04:43:40 +00:00
|
|
|
ante,
|
2022-05-26 21:37:51 +00:00
|
|
|
closeTime.getTime(),
|
2022-05-17 04:43:40 +00:00
|
|
|
tags ?? [],
|
2022-05-19 17:42:03 +00:00
|
|
|
NUMERIC_BUCKET_COUNT,
|
|
|
|
min ?? 0,
|
2022-06-22 22:19:17 +00:00
|
|
|
max ?? 0
|
2022-05-17 04:43:40 +00:00
|
|
|
)
|
|
|
|
|
2022-06-23 18:00:14 +00:00
|
|
|
if (ante) await chargeUser(user.id, ante, true)
|
2022-05-17 04:43:40 +00:00
|
|
|
|
|
|
|
await contractRef.create(contract)
|
|
|
|
|
2022-06-23 18:00:14 +00:00
|
|
|
const providerId = user.id
|
2022-05-20 02:06:50 +00:00
|
|
|
|
2022-06-01 02:42:35 +00:00
|
|
|
if (outcomeType === 'BINARY') {
|
2022-05-20 02:06:50 +00:00
|
|
|
const liquidityDoc = firestore
|
|
|
|
.collection(`contracts/${contract.id}/liquidity`)
|
|
|
|
.doc()
|
|
|
|
|
|
|
|
const lp = getCpmmInitialLiquidity(
|
|
|
|
providerId,
|
2022-06-01 02:42:35 +00:00
|
|
|
contract as CPMMBinaryContract,
|
2022-05-20 02:06:50 +00:00
|
|
|
liquidityDoc.id,
|
|
|
|
ante
|
|
|
|
)
|
|
|
|
|
|
|
|
await liquidityDoc.set(lp)
|
|
|
|
} else if (outcomeType === 'FREE_RESPONSE') {
|
|
|
|
const noneAnswerDoc = firestore
|
|
|
|
.collection(`contracts/${contract.id}/answers`)
|
|
|
|
.doc('0')
|
|
|
|
|
2022-05-26 21:37:51 +00:00
|
|
|
const noneAnswer = getNoneAnswer(contract.id, user)
|
2022-05-20 02:06:50 +00:00
|
|
|
await noneAnswerDoc.set(noneAnswer)
|
|
|
|
|
|
|
|
const anteBetDoc = firestore
|
|
|
|
.collection(`contracts/${contract.id}/bets`)
|
|
|
|
.doc()
|
|
|
|
|
|
|
|
const anteBet = getFreeAnswerAnte(
|
|
|
|
providerId,
|
2022-06-01 02:42:35 +00:00
|
|
|
contract as FreeResponseContract,
|
2022-05-20 02:06:50 +00:00
|
|
|
anteBetDoc.id
|
|
|
|
)
|
|
|
|
await anteBetDoc.set(anteBet)
|
|
|
|
} else if (outcomeType === 'NUMERIC') {
|
|
|
|
const anteBetDoc = firestore
|
|
|
|
.collection(`contracts/${contract.id}/bets`)
|
|
|
|
.doc()
|
|
|
|
|
|
|
|
const anteBet = getNumericAnte(
|
2022-05-28 18:43:57 +00:00
|
|
|
providerId,
|
2022-06-01 02:42:35 +00:00
|
|
|
contract as NumericContract,
|
2022-05-20 02:06:50 +00:00
|
|
|
ante,
|
|
|
|
anteBetDoc.id
|
|
|
|
)
|
|
|
|
|
|
|
|
await anteBetDoc.set(anteBet)
|
2022-05-17 04:43:40 +00:00
|
|
|
}
|
|
|
|
|
2022-05-20 21:58:14 +00:00
|
|
|
return contract
|
2022-05-17 04:43:40 +00:00
|
|
|
})
|
2022-01-05 05:51:26 +00:00
|
|
|
|
|
|
|
const getSlug = async (question: string) => {
|
2022-01-10 22:49:04 +00:00
|
|
|
const proposedSlug = slugify(question)
|
2022-01-05 05:51:26 +00:00
|
|
|
|
|
|
|
const preexistingContract = await getContractFromSlug(proposedSlug)
|
|
|
|
|
|
|
|
return preexistingContract
|
|
|
|
? proposedSlug + '-' + randomString()
|
|
|
|
: proposedSlug
|
|
|
|
}
|
|
|
|
|
|
|
|
const firestore = admin.firestore()
|
|
|
|
|
|
|
|
export async function getContractFromSlug(slug: string) {
|
|
|
|
const snap = await firestore
|
|
|
|
.collection('contracts')
|
|
|
|
.where('slug', '==', slug)
|
|
|
|
.get()
|
|
|
|
|
|
|
|
return snap.empty ? undefined : (snap.docs[0].data() as Contract)
|
|
|
|
}
|