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 } 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 id: string
userId: string userId: string
contractId: string contractId: string
amount: number // bet size; negative if SELL bet 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 shares: number // dynamic parimutuel pool weight; negative if SELL bet
probBefore: number 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 description: string // More info about what the contract is about
tags: string[] tags: string[]
lowercaseTags: string[] lowercaseTags: string[]
outcomeType: outcomeType
visibility: 'public' | 'unlisted' visibility: 'public' | 'unlisted'
outcomeType: outcomeType
outcomes: {
BINARY: undefined
MULTI: 'FREE_ANSWER' | string[]
}[outcomeType]
mechanism: 'dpm-2' mechanism: 'dpm-2'
phantomShares: { phantomShares: {
BINARY: { YES: number; NO: number } BINARY: { YES: number; NO: number }
MULTI: { [answerId: string]: number } MULTI: undefined
}[outcomeType] }[outcomeType]
pool: { pool: {
BINARY: { YES: number; NO: number } BINARY: { YES: number; NO: number }

View File

@ -1,32 +1,37 @@
import { calcStartPool } from './antes' import { calcStartPool } from './antes'
import { Contract } from './contract' import { Contract } from './contract'
import { User } from './user' import { User } from './user'
import { parseTags } from './util/parse' import { parseTags } from './util/parse'
import { removeUndefinedProps } from './util/object'
export function getNewContract( export function getNewContract(
id: string, id: string,
slug: string, slug: string,
creator: User, creator: User,
question: string, question: string,
outcomeType: 'BINARY' | 'MULTI',
description: string, description: string,
initialProb: number, initialProb: number,
ante: number, ante: number,
closeTime: number, closeTime: number,
extraTags: string[] extraTags: string[]
) { ) {
const { sharesYes, sharesNo, poolYes, poolNo, phantomYes, phantomNo } =
calcStartPool(initialProb, ante)
const tags = parseTags( const tags = parseTags(
`${question} ${description} ${extraTags.map((tag) => `#${tag}`).join(' ')}` `${question} ${description} ${extraTags.map((tag) => `#${tag}`).join(' ')}`
) )
const lowercaseTags = tags.map((tag) => tag.toLowerCase()) 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, id,
slug, slug,
outcomeType: 'BINARY', mechanism: 'dpm-2',
outcomeType,
...propsByOutcomeType,
creatorId: creator.id, creatorId: creator.id,
creatorName: creator.name, creatorName: creator.name,
@ -38,22 +43,45 @@ export function getNewContract(
tags, tags,
lowercaseTags, lowercaseTags,
visibility: 'public', 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 }, phantomShares: { YES: phantomYes, NO: phantomNo },
pool: { YES: poolYes, NO: poolNo }, pool: { YES: poolYes, NO: poolNo },
totalShares: { YES: sharesYes, NO: sharesNo }, totalShares: { YES: sharesYes, NO: sharesNo },
totalBets: { YES: poolYes, NO: poolNo }, totalBets: { YES: poolYes, NO: poolNo },
isResolved: false, outcomes: undefined,
createdTime: Date.now(),
lastUpdatedTime: Date.now(),
volume24Hours: 0,
volume7Days: 0,
} }
}
if (closeTime) contract.closeTime = closeTime
const getFreeAnswerProps = () => {
return contract 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 functions from 'firebase-functions'
import * as admin from 'firebase-admin' import * as admin from 'firebase-admin'
import { getUser, removeUndefinedProps } from './utils' import { getUser } from './utils'
import { Contract } from '../../common/contract' import { Contract } from '../../common/contract'
import { Comment } from '../../common/comment' import { Comment } from '../../common/comment'
import { User } from '../../common/user' import { User } from '../../common/user'
import { cleanUsername } from '../../common/util/clean-username' import { cleanUsername } from '../../common/util/clean-username'
import { removeUndefinedProps } from '../../common/util/object'
export const changeUserInfo = functions export const changeUserInfo = functions
.runWith({ minInstances: 1 }) .runWith({ minInstances: 1 })

View File

@ -6,7 +6,11 @@ import { Contract } from '../../common/contract'
import { slugify } from '../../common/util/slugify' import { slugify } from '../../common/util/slugify'
import { randomString } from '../../common/util/random' import { randomString } from '../../common/util/random'
import { getNewContract } from '../../common/new-contract' 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 export const createContract = functions
.runWith({ minInstances: 1 }) .runWith({ minInstances: 1 })
@ -14,6 +18,7 @@ export const createContract = functions
async ( async (
data: { data: {
question: string question: string
outcomeType: 'BINARY' | 'MULTI'
description: string description: string
initialProb: number initialProb: number
ante: number ante: number
@ -28,12 +33,26 @@ export const createContract = functions
const creator = await getUser(userId) const creator = await getUser(userId)
if (!creator) return { status: 'error', message: 'User not found' } 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) if (!question)
return { status: 'error', message: 'Missing contract attributes' } 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' } return { status: 'error', message: 'Invalid initial probability' }
if ( if (
@ -63,6 +82,7 @@ export const createContract = functions
slug, slug,
creator, creator,
question, question,
outcomeType,
description, description,
initialProb, initialProb,
ante, ante,
@ -75,22 +95,35 @@ export const createContract = functions
await contractRef.create(contract) await contractRef.create(contract)
if (ante) { if (ante) {
const yesBetDoc = firestore if (outcomeType === 'BINARY') {
.collection(`contracts/${contract.id}/bets`) const yesBetDoc = firestore
.doc() .collection(`contracts/${contract.id}/bets`)
.doc()
const noBetDoc = firestore const noBetDoc = firestore
.collection(`contracts/${contract.id}/bets`) .collection(`contracts/${contract.id}/bets`)
.doc() .doc()
const { yesBet, noBet } = getAnteBets( const { yesBet, noBet } = getAnteBets(
creator, creator,
contract, contract as Contract<'BINARY'>,
yesBetDoc.id, yesBetDoc.id,
noBetDoc.id noBetDoc.id
) )
await yesBetDoc.set(yesBet) await yesBetDoc.set(yesBet)
await noBetDoc.set(noBet) 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 } return { status: 'success', contract }

View File

@ -77,13 +77,3 @@ export const chargeUser = (userId: string, charge: number) => {
return updateUserBalance(userId, -charge) 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 { ProbabilitySelector } from '../components/probability-selector'
import { parseWordsAsTags } from '../../common/util/parse' import { parseWordsAsTags } from '../../common/util/parse'
import { TagsList } from '../components/tags-list' import { TagsList } from '../components/tags-list'
import { Row } from '../components/layout/row'
export default function Create() { export default function Create() {
const [question, setQuestion] = useState('') const [question, setQuestion] = useState('')
@ -61,6 +62,7 @@ export function NewContract(props: { question: string; tag?: string }) {
createContract({}).catch() // warm up function createContract({}).catch() // warm up function
}, []) }, [])
const [outcomeType, setOutcomeType] = useState<'BINARY' | 'MULTI'>('BINARY')
const [initialProb, setInitialProb] = useState(50) const [initialProb, setInitialProb] = useState(50)
const [description, setDescription] = useState('') const [description, setDescription] = useState('')
const [tagText, setTagText] = useState<string>(tag ?? '') const [tagText, setTagText] = useState<string>(tag ?? '')
@ -105,6 +107,7 @@ export function NewContract(props: { question: string; tag?: string }) {
const result: any = await createContract({ const result: any = await createContract({
question, question,
outcomeType,
description, description,
initialProb, initialProb,
ante, ante,
@ -126,18 +129,48 @@ export function NewContract(props: { question: string; tag?: string }) {
return ( return (
<div> <div>
<Spacer h={4} /> <label className="label">
<span className="mb-1">Answer type</span>
<div className="form-control"> </label>
<label className="label"> <Row className="form-control gap-2">
<span className="mb-1">Initial probability</span> <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> </label>
<ProbabilitySelector <label className="cursor-pointer label gap-2">
probabilityInt={initialProb} <input
setProbabilityInt={setInitialProb} className="radio"
/> type="radio"
</div> 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} /> <Spacer h={4} />