Create free answer contract

This commit is contained in:
James Grugett 2022-02-11 22:53:13 -06:00
parent 34607262f3
commit 05540c9cc1
9 changed files with 189 additions and 62 deletions

View File

@ -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
}

View File

@ -1,10 +1,13 @@
export type Bet = {
export type Bet<outcomeType extends 'BINARY' | 'MULTI' = 'BINARY'> = {
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

View File

@ -11,13 +11,18 @@ export type Contract<outcomeType extends 'BINARY' | 'MULTI' = 'BINARY'> = {
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 }

View File

@ -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.
}

9
common/util/object.ts Normal file
View File

@ -0,0 +1,9 @@
export const removeUndefinedProps = <T>(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
}

View File

@ -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 })

View File

@ -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 }

View File

@ -77,13 +77,3 @@ export const chargeUser = (userId: string, charge: number) => {
return updateUserBalance(userId, -charge)
}
export const removeUndefinedProps = <T>(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
}

View File

@ -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<string>(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 (
<div>
<Spacer h={4} />
<div className="form-control">
<label className="label">
<span className="mb-1">Initial probability</span>
<label className="label">
<span className="mb-1">Answer type</span>
</label>
<Row className="form-control gap-2">
<label className="cursor-pointer label gap-2">
<input
className="radio"
type="radio"
name="opt"
checked={outcomeType === 'BINARY'}
value="BINARY"
onChange={(e) => setOutcomeType(e.target.value as 'BINARY')}
/>
<span className="label-text">Yes / No</span>
</label>
<ProbabilitySelector
probabilityInt={initialProb}
setProbabilityInt={setInitialProb}
/>
</div>
<label className="cursor-pointer label gap-2">
<input
className="radio"
type="radio"
name="opt"
checked={outcomeType === 'MULTI'}
value="MULTI"
onChange={(e) => setOutcomeType(e.target.value as 'MULTI')}
/>
<span className="label-text">Free response</span>
</label>
</Row>
<Spacer h={4} />
{outcomeType === 'BINARY' && (
<div className="form-control">
<label className="label">
<span className="mb-1">Initial probability</span>
</label>
<ProbabilitySelector
probabilityInt={initialProb}
setProbabilityInt={setInitialProb}
/>
</div>
)}
<Spacer h={4} />