diff --git a/common/antes.ts b/common/antes.ts index 3604d941..17c58e96 100644 --- a/common/antes.ts +++ b/common/antes.ts @@ -61,3 +61,28 @@ export function getAnteBets( return { yesBet, noBet } } + +export function getFreeAnswerAnte( + creator: User, + contract: Contract<'MULTI'>, + anteBetId: string +) { + const ante = contract.totalBets.YES + contract.totalBets.NO + + const { createdTime } = contract + + const anteBet: Bet<'MULTI'> = { + id: anteBetId, + userId: creator.id, + contractId: contract.id, + amount: ante, + shares: 0, + outcome: 'NONE', + probBefore: 0, + probAfter: 0, + createdTime, + isAnte: true, + } + + return anteBet +} diff --git a/common/bet.ts b/common/bet.ts index a875102c..96429fa2 100644 --- a/common/bet.ts +++ b/common/bet.ts @@ -1,10 +1,13 @@ -export type Bet = { +export type Bet = { id: string userId: string contractId: string amount: number // bet size; negative if SELL bet - outcome: 'YES' | 'NO' + outcome: { + BINARY: 'YES' | 'NO' + MULTI: string + }[outcomeType] shares: number // dynamic parimutuel pool weight; negative if SELL bet probBefore: number diff --git a/common/contract.ts b/common/contract.ts index 3b57b766..b77f7606 100644 --- a/common/contract.ts +++ b/common/contract.ts @@ -11,13 +11,18 @@ export type Contract = { description: string // More info about what the contract is about tags: string[] lowercaseTags: string[] - outcomeType: outcomeType visibility: 'public' | 'unlisted' + outcomeType: outcomeType + outcomes: { + BINARY: undefined + MULTI: 'FREE_ANSWER' | string[] + }[outcomeType] + mechanism: 'dpm-2' phantomShares: { BINARY: { YES: number; NO: number } - MULTI: { [answerId: string]: number } + MULTI: undefined }[outcomeType] pool: { BINARY: { YES: number; NO: number } diff --git a/common/new-contract.ts b/common/new-contract.ts index 99f27874..edb38d72 100644 --- a/common/new-contract.ts +++ b/common/new-contract.ts @@ -1,32 +1,37 @@ import { calcStartPool } from './antes' - import { Contract } from './contract' import { User } from './user' import { parseTags } from './util/parse' +import { removeUndefinedProps } from './util/object' export function getNewContract( id: string, slug: string, creator: User, question: string, + outcomeType: 'BINARY' | 'MULTI', description: string, initialProb: number, ante: number, closeTime: number, extraTags: string[] ) { - const { sharesYes, sharesNo, poolYes, poolNo, phantomYes, phantomNo } = - calcStartPool(initialProb, ante) - const tags = parseTags( `${question} ${description} ${extraTags.map((tag) => `#${tag}`).join(' ')}` ) const lowercaseTags = tags.map((tag) => tag.toLowerCase()) - const contract: Contract = { + const propsByOutcomeType = + outcomeType === 'BINARY' + ? getBinaryProps(initialProb, ante) + : getFreeAnswerProps() + + const contract: Contract<'BINARY' | 'MULTI'> = removeUndefinedProps({ id, slug, - outcomeType: 'BINARY', + mechanism: 'dpm-2', + outcomeType, + ...propsByOutcomeType, creatorId: creator.id, creatorName: creator.name, @@ -38,22 +43,45 @@ export function getNewContract( tags, lowercaseTags, visibility: 'public', + isResolved: false, + createdTime: Date.now(), + lastUpdatedTime: Date.now(), + closeTime, - mechanism: 'dpm-2', + volume24Hours: 0, + volume7Days: 0, + }) + + return contract +} + +const getBinaryProps = (initialProb: number, ante: number) => { + const { sharesYes, sharesNo, poolYes, poolNo, phantomYes, phantomNo } = + calcStartPool(initialProb, ante) + + return { phantomShares: { YES: phantomYes, NO: phantomNo }, pool: { YES: poolYes, NO: poolNo }, totalShares: { YES: sharesYes, NO: sharesNo }, totalBets: { YES: poolYes, NO: poolNo }, - isResolved: false, - - createdTime: Date.now(), - lastUpdatedTime: Date.now(), - - volume24Hours: 0, - volume7Days: 0, + outcomes: undefined, } - - if (closeTime) contract.closeTime = closeTime - - return contract +} + +const getFreeAnswerProps = () => { + return { + pool: {}, + totalShares: {}, + totalBets: {}, + phantomShares: undefined, + outcomes: 'FREE_ANSWER' as const, + } +} + +const getMultiProps = ( + outcomes: string[], + initialProbs: number[], + ante: number +) => { + // Not implemented. } diff --git a/common/util/object.ts b/common/util/object.ts new file mode 100644 index 00000000..4148b057 --- /dev/null +++ b/common/util/object.ts @@ -0,0 +1,9 @@ +export const removeUndefinedProps = (obj: T): T => { + let newObj: any = {} + + for (let key of Object.keys(obj)) { + if ((obj as any)[key] !== undefined) newObj[key] = (obj as any)[key] + } + + return newObj +} diff --git a/functions/src/change-user-info.ts b/functions/src/change-user-info.ts index de8a8d6a..99c1ef5e 100644 --- a/functions/src/change-user-info.ts +++ b/functions/src/change-user-info.ts @@ -1,11 +1,12 @@ import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' -import { getUser, removeUndefinedProps } from './utils' +import { getUser } from './utils' import { Contract } from '../../common/contract' import { Comment } from '../../common/comment' import { User } from '../../common/user' import { cleanUsername } from '../../common/util/clean-username' +import { removeUndefinedProps } from '../../common/util/object' export const changeUserInfo = functions .runWith({ minInstances: 1 }) diff --git a/functions/src/create-contract.ts b/functions/src/create-contract.ts index 8ab38b88..2c3c1153 100644 --- a/functions/src/create-contract.ts +++ b/functions/src/create-contract.ts @@ -6,7 +6,11 @@ import { Contract } from '../../common/contract' import { slugify } from '../../common/util/slugify' import { randomString } from '../../common/util/random' import { getNewContract } from '../../common/new-contract' -import { getAnteBets, MINIMUM_ANTE } from '../../common/antes' +import { + getAnteBets, + getFreeAnswerAnte, + MINIMUM_ANTE, +} from '../../common/antes' export const createContract = functions .runWith({ minInstances: 1 }) @@ -14,6 +18,7 @@ export const createContract = functions async ( data: { question: string + outcomeType: 'BINARY' | 'MULTI' description: string initialProb: number ante: number @@ -28,12 +33,26 @@ export const createContract = functions const creator = await getUser(userId) if (!creator) return { status: 'error', message: 'User not found' } - const { question, description, initialProb, ante, closeTime, tags } = data + const { + question, + outcomeType, + description, + initialProb, + ante, + closeTime, + tags, + } = data - if (!question || !initialProb) - return { status: 'error', message: 'Missing contract attributes' } + if (!question) + return { status: 'error', message: 'Missing question field' } - if (initialProb < 1 || initialProb > 99) + if (outcomeType !== 'BINARY' && outcomeType !== 'MULTI') + return { status: 'error', message: 'Invalid outcomeType' } + + if ( + outcomeType === 'BINARY' && + (!initialProb || initialProb < 1 || initialProb > 99) + ) return { status: 'error', message: 'Invalid initial probability' } if ( @@ -63,6 +82,7 @@ export const createContract = functions slug, creator, question, + outcomeType, description, initialProb, ante, @@ -75,22 +95,35 @@ export const createContract = functions await contractRef.create(contract) if (ante) { - const yesBetDoc = firestore - .collection(`contracts/${contract.id}/bets`) - .doc() + if (outcomeType === 'BINARY') { + const yesBetDoc = firestore + .collection(`contracts/${contract.id}/bets`) + .doc() - const noBetDoc = firestore - .collection(`contracts/${contract.id}/bets`) - .doc() + const noBetDoc = firestore + .collection(`contracts/${contract.id}/bets`) + .doc() - const { yesBet, noBet } = getAnteBets( - creator, - contract, - yesBetDoc.id, - noBetDoc.id - ) - await yesBetDoc.set(yesBet) - await noBetDoc.set(noBet) + const { yesBet, noBet } = getAnteBets( + creator, + contract as Contract<'BINARY'>, + yesBetDoc.id, + noBetDoc.id + ) + await yesBetDoc.set(yesBet) + await noBetDoc.set(noBet) + } else if (outcomeType === 'MULTI') { + const anteBetDoc = firestore + .collection(`contracts/${contract.id}/bets`) + .doc() + getFreeAnswerAnte( + creator, + contract as Contract<'MULTI'>, + anteBetDoc.id + ) + // Disable until we figure out how this should work. + // await anteBetDoc.set(anteBetDoc) + } } return { status: 'success', contract } diff --git a/functions/src/utils.ts b/functions/src/utils.ts index 95881870..9f3777e8 100644 --- a/functions/src/utils.ts +++ b/functions/src/utils.ts @@ -77,13 +77,3 @@ export const chargeUser = (userId: string, charge: number) => { return updateUserBalance(userId, -charge) } - -export const removeUndefinedProps = (obj: T): T => { - let newObj: any = {} - - for (let key of Object.keys(obj)) { - if ((obj as any)[key] !== undefined) newObj[key] = (obj as any)[key] - } - - return newObj -} diff --git a/web/pages/create.tsx b/web/pages/create.tsx index 016906fc..f1745269 100644 --- a/web/pages/create.tsx +++ b/web/pages/create.tsx @@ -17,6 +17,7 @@ import { Title } from '../components/title' import { ProbabilitySelector } from '../components/probability-selector' import { parseWordsAsTags } from '../../common/util/parse' import { TagsList } from '../components/tags-list' +import { Row } from '../components/layout/row' export default function Create() { const [question, setQuestion] = useState('') @@ -61,6 +62,7 @@ export function NewContract(props: { question: string; tag?: string }) { createContract({}).catch() // warm up function }, []) + const [outcomeType, setOutcomeType] = useState<'BINARY' | 'MULTI'>('BINARY') const [initialProb, setInitialProb] = useState(50) const [description, setDescription] = useState('') const [tagText, setTagText] = useState(tag ?? '') @@ -105,6 +107,7 @@ export function NewContract(props: { question: string; tag?: string }) { const result: any = await createContract({ question, + outcomeType, description, initialProb, ante, @@ -126,18 +129,48 @@ export function NewContract(props: { question: string; tag?: string }) { return (
- - -
-
+ + + + + {outcomeType === 'BINARY' && ( +
+ + + +
+ )}