2022-01-05 05:51:26 +00:00
|
|
|
import * as admin from 'firebase-admin'
|
2022-05-12 15:07:10 +00:00
|
|
|
|
2022-03-09 17:08:57 +00:00
|
|
|
import {
|
2022-03-15 22:27:51 +00:00
|
|
|
Binary,
|
2022-03-09 17:08:57 +00:00
|
|
|
Contract,
|
2022-03-15 22:27:51 +00:00
|
|
|
CPMM,
|
|
|
|
DPM,
|
|
|
|
FreeResponse,
|
|
|
|
FullContract,
|
2022-03-09 17:08:57 +00:00
|
|
|
MAX_DESCRIPTION_LENGTH,
|
|
|
|
MAX_QUESTION_LENGTH,
|
|
|
|
MAX_TAG_LENGTH,
|
2022-05-19 17:42:03 +00:00
|
|
|
Numeric,
|
|
|
|
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'
|
|
|
|
import { APIError, newEndpoint, parseCredentials, lookupUser } from './api'
|
|
|
|
|
2022-02-17 23:00:19 +00:00
|
|
|
import {
|
2022-04-09 18:51:22 +00:00
|
|
|
FIXED_ANTE,
|
2022-02-17 23:00:19 +00:00
|
|
|
getAnteBets,
|
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-04-29 19:58:01 +00:00
|
|
|
HOUSE_LIQUIDITY_PROVIDER_ID,
|
2022-02-17 23:00:19 +00:00
|
|
|
MINIMUM_ANTE,
|
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-01-05 05:51:26 +00:00
|
|
|
|
2022-05-17 04:43:40 +00:00
|
|
|
export const createContract = newEndpoint(['POST'], async (req, _res) => {
|
|
|
|
const [creator, _privateUser] = await lookupUser(await parseCredentials(req))
|
|
|
|
let {
|
|
|
|
question,
|
|
|
|
outcomeType,
|
|
|
|
description,
|
|
|
|
initialProb,
|
|
|
|
closeTime,
|
|
|
|
tags,
|
2022-05-19 17:42:03 +00:00
|
|
|
min,
|
|
|
|
max,
|
2022-05-17 04:43:40 +00:00
|
|
|
manaLimitPerUser,
|
2022-05-20 21:58:14 +00:00
|
|
|
} = req.body || {}
|
2022-05-17 04:43:40 +00:00
|
|
|
|
|
|
|
if (!question || typeof question != 'string')
|
|
|
|
throw new APIError(400, 'Missing or invalid question field')
|
|
|
|
|
|
|
|
question = question.slice(0, MAX_QUESTION_LENGTH)
|
|
|
|
|
|
|
|
if (typeof description !== 'string')
|
|
|
|
throw new APIError(400, 'Invalid description field')
|
|
|
|
|
|
|
|
description = description.slice(0, MAX_DESCRIPTION_LENGTH)
|
|
|
|
|
|
|
|
if (tags !== undefined && !Array.isArray(tags))
|
|
|
|
throw new APIError(400, 'Invalid tags field')
|
|
|
|
|
|
|
|
tags = (tags || []).map((tag: string) =>
|
|
|
|
tag.toString().slice(0, MAX_TAG_LENGTH)
|
|
|
|
)
|
|
|
|
|
|
|
|
outcomeType = outcomeType ?? 'BINARY'
|
2022-05-19 17:42:03 +00:00
|
|
|
|
|
|
|
if (!OUTCOME_TYPES.includes(outcomeType))
|
2022-05-17 04:43:40 +00:00
|
|
|
throw new APIError(400, 'Invalid outcomeType')
|
|
|
|
|
2022-05-19 17:42:03 +00:00
|
|
|
if (
|
|
|
|
outcomeType === 'NUMERIC' &&
|
|
|
|
!(
|
|
|
|
min !== undefined &&
|
|
|
|
max !== undefined &&
|
|
|
|
isFinite(min) &&
|
|
|
|
isFinite(max) &&
|
|
|
|
min < max &&
|
|
|
|
max - min > 0.01
|
|
|
|
)
|
|
|
|
)
|
|
|
|
throw new APIError(400, 'Invalid range')
|
|
|
|
|
2022-05-17 04:43:40 +00:00
|
|
|
if (
|
|
|
|
outcomeType === 'BINARY' &&
|
|
|
|
(!initialProb || initialProb < 1 || initialProb > 99)
|
|
|
|
)
|
|
|
|
throw new APIError(400, 'Invalid initial probability')
|
|
|
|
|
2022-05-23 14:43:11 +00:00
|
|
|
// Uses utc time on server:
|
|
|
|
const yesterday = new Date()
|
|
|
|
yesterday.setUTCDate(yesterday.getUTCDate() - 1)
|
|
|
|
const freeMarketResetTime = yesterday.setUTCHours(16, 0, 0, 0)
|
|
|
|
|
2022-05-17 04:43:40 +00:00
|
|
|
const userContractsCreatedTodaySnapshot = await firestore
|
|
|
|
.collection(`contracts`)
|
|
|
|
.where('creatorId', '==', creator.id)
|
2022-05-23 14:43:11 +00:00
|
|
|
.where('createdTime', '>=', freeMarketResetTime)
|
2022-05-17 04:43:40 +00:00
|
|
|
.get()
|
2022-05-23 14:43:11 +00:00
|
|
|
console.log('free market reset time: ', freeMarketResetTime)
|
2022-05-17 04:43:40 +00:00
|
|
|
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 ?? [],
|
2022-05-19 17:42:03 +00:00
|
|
|
NUMERIC_BUCKET_COUNT,
|
|
|
|
min ?? 0,
|
|
|
|
max ?? 0,
|
2022-05-17 04:43:40 +00:00
|
|
|
manaLimitPerUser ?? 0
|
|
|
|
)
|
|
|
|
|
|
|
|
if (!isFree && ante) await chargeUser(creator.id, ante, true)
|
|
|
|
|
|
|
|
await contractRef.create(contract)
|
|
|
|
|
2022-05-20 02:06:50 +00:00
|
|
|
const providerId = isFree ? HOUSE_LIQUIDITY_PROVIDER_ID : creator.id
|
|
|
|
|
|
|
|
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 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(
|
|
|
|
providerId,
|
|
|
|
contract as FullContract<DPM, FreeResponse>,
|
|
|
|
anteBetDoc.id
|
|
|
|
)
|
|
|
|
await anteBetDoc.set(anteBet)
|
|
|
|
} else if (outcomeType === 'NUMERIC') {
|
|
|
|
const anteBetDoc = firestore
|
|
|
|
.collection(`contracts/${contract.id}/bets`)
|
|
|
|
.doc()
|
|
|
|
|
|
|
|
const anteBet = getNumericAnte(
|
|
|
|
creator,
|
|
|
|
contract as FullContract<DPM, Numeric>,
|
|
|
|
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)
|
|
|
|
}
|