contract creation: use slug, calculate seed amounts from given probability

This commit is contained in:
mantikoros 2021-12-13 19:07:28 -06:00
parent 571c1307fa
commit 5ffe266cf7
6 changed files with 127 additions and 114 deletions

View File

@ -1,5 +1,7 @@
import Link from 'next/link'
import { Contract, deleteContract } from '../lib/firebase/contracts'
import { useEffect, useState } from 'react'
import { useUser } from '../hooks/use-user'
import { Contract, deleteContract, listContracts } from '../lib/firebase/contracts'
function ContractCard(props: { contract: Contract }) {
const { contract } = props
@ -49,8 +51,17 @@ function ContractCard(props: { contract: Contract }) {
)
}
export function ContractsList(props: { contracts: Contract[] }) {
const { contracts } = props
export function ContractsList(props: {}) {
const creator = useUser()
const [contracts, setContracts] = useState<Contract[]>([])
useEffect(() => {
if (creator?.id) {
listContracts(creator.id).then(setContracts)
}
}, [creator])
return (
<div className="bg-gray-200 shadow-xl overflow-hidden sm:rounded-md max-w-4xl w-full">
<ul role="list" className="divide-y divide-gray-300">

View File

@ -10,6 +10,7 @@ import {
getDocs,
onSnapshot,
orderBy,
getDoc,
} from 'firebase/firestore'
export type Contract = {
@ -42,6 +43,15 @@ export async function setContract(contract: Contract) {
await setDoc(docRef, contract)
}
export async function getContract(contractId: string) {
const docRef = doc(db, 'contracts', contractId)
const result = await getDoc(docRef)
return result.exists()
? result.data() as Contract
: undefined
}
export async function deleteContract(contractId: string) {
const docRef = doc(db, 'contracts', contractId)
await deleteDoc(docRef)

View File

@ -0,0 +1,51 @@
import { Contract, getContract, setContract } from '../firebase/contracts'
import { User } from '../firebase/users'
import { randomString } from '../util/random-string'
import { slugify } from '../util/slugify'
// consider moving to cloud function for security
export async function createContract(question: string, description: string, initialProb: number, creator: User) {
const slug = slugify(question).substr(0, 35)
const preexistingContract = await getContract(slug)
const contractId = preexistingContract
? slug + '-' + randomString()
: slug
const { seedYes, seedNo } = calcSeedBets(initialProb)
const contract: Contract = {
id: contractId,
outcomeType: 'BINARY',
creatorId: creator.id,
creatorName: creator.name,
question: question.trim(),
description: description.trim(),
seedAmounts: { YES: seedYes, NO: seedNo },
pot: { YES: seedYes, NO: seedNo },
// TODO: Set create time to Firestore timestamp
createdTime: Date.now(),
lastUpdatedTime: Date.now(),
}
await setContract(contract)
return contract
}
export function calcSeedBets(initialProb: number, initialCapital = 1000) {
const p = initialProb / 100.0
const seedYes = p === 0.5
? p * initialCapital
: -(initialCapital * (-p + Math.sqrt((-1 + p) * -p))) / (-1 + 2 * p)
const seedNo = initialCapital - seedYes
return { seedYes, seedNo }
}

View File

@ -0,0 +1,2 @@
export const randomString = () => Math.random().toString(16).substr(2, 14)

11
web/lib/util/slugify.ts Normal file
View File

@ -0,0 +1,11 @@
export const slugify = (text: any, separator = '-'): string => {
return text
.toString()
.normalize('NFD') // split an accented letter in the base letter and the acent
.replace(/[\u0300-\u036f]/g, '') // remove all previously split accents
.toLowerCase()
.trim()
.replace(/[^a-z0-9 ]/g, '') // remove all chars not letters, numbers and spaces (to be replaced)
.replace(/\s+/g, separator)
}

View File

@ -1,53 +1,31 @@
import { useEffect, useState } from 'react'
import router from 'next/router'
import { useState } from 'react'
import { ContractsList } from '../../components/contracts-list'
import { Header } from '../../components/header'
import { Spacer } from '../../components/layout/spacer'
import { Title } from '../../components/title'
import { useUser } from '../../hooks/use-user'
import {
Contract,
listContracts,
setContract as pushContract,
} from '../../lib/firebase/contracts'
import { createContract } from '../../lib/service/create-contract'
// Allow user to create a new contract
// TODO: Extract to a reusable UI, for listing contracts too?
export default function NewContract() {
const creator = useUser()
const [contract, setContract] = useState<Contract>({
id: '',
creatorId: '',
question: '',
description: '',
seedAmounts: { YES: 100, NO: 100 },
pot: { YES: 100, NO: 100 },
// TODO: Set create time to Firestore timestamp
createdTime: Date.now(),
lastUpdatedTime: Date.now(),
} as Contract)
const [initialProb, setInitialProb] = useState(50)
const [question, setQuestion] = useState('')
const [description, setDescription] = useState('')
const [isSubmitting, setIsSubmitting] = useState(false)
const [contracts, setContracts] = useState<Contract[]>([])
useEffect(() => {
if (creator?.id) {
setContract((contract) => ({
...contract,
creatorId: creator.id,
creatorName: creator.name,
}))
listContracts(creator.id).then(setContracts)
}
}, [creator])
async function submit() {
// TODO: add more rigorous error handling for question, description
if (!creator || !question || !description) return
async function saveContract() {
await pushContract(contract)
// Update local contract list
setContracts([{ ...contract }, ...contracts])
}
setIsSubmitting(true)
function saveField(field: keyof Contract) {
return (changeEvent: React.ChangeEvent<any>) =>
setContract((c) => ({ ...c, [field]: changeEvent.target.value }))
const contract = await createContract(question, description, initialProb, creator)
await router.push(`contract/${contract.id}`)
}
const descriptionPlaceholder = `e.g. This market will resolve to “Yes” if, by June 2, 2021, 11:59:59 PM ET, Paxlovid (also known under PF-07321332)...`
@ -55,35 +33,25 @@ export default function NewContract() {
return (
<div>
<Header />
<div className="max-w-4xl py-12 lg:mx-auto px-4">
<Title text="Create a new prediction market" />
<div className="w-full bg-gray-200 rounded-lg shadow-xl p-6">
{/* Create a Tailwind form that takes in all the fields needed for a new contract */}
{/* When the form is submitted, create a new contract in the database */}
<form>
<div className="form-control">
<label className="label">
<span className="label-text">Contract ID</span>
</label>
<input
type="text"
placeholder="e.g. COVID-123"
className="input"
value={contract.id}
onChange={saveField('id')}
/>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">Question</span>
</label>
<input
type="text"
placeholder="e.g. Will the FDA approve Paxlovid before Jun 2nd, 2022?"
className="input"
value={contract.question}
onChange={saveField('question')}
value={question}
onChange={e => setQuestion(e.target.value || '')}
/>
</div>
@ -91,79 +59,38 @@ export default function NewContract() {
<label className="label">
<span className="label-text">Description</span>
</label>
<textarea
className="textarea h-24 textarea-bordered"
placeholder={descriptionPlaceholder}
value={contract.description}
onChange={saveField('description')}
value={description}
onChange={e => setDescription(e.target.value || '')}
></textarea>
</div>
<div className="mt-6 grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">
<div className="sm:col-span-3">
<div className="form-control">
<label className="label">
<span className="label-text">Yes Seed</span>
<span className="label-text">Initial probability: {initialProb}%</span>
</label>
<input
type="number"
placeholder="100"
className="input"
value={contract.seedAmounts.YES}
onChange={(e) => {
setContract({
...contract,
seedAmounts: {
...contract.seedAmounts,
YES: parseInt(e.target.value),
},
pot: {
...contract.pot,
YES: parseInt(e.target.value),
},
})
}}
type="range"
className="range-primary"
min="1"
max={99}
value={initialProb}
onChange={e => setInitialProb(parseInt(e.target.value))}
/>
</div>
</div>
<div className="sm:col-span-3">
<div className="form-control">
<label className="label">
<span className="label-text">No Seed</span>
</label>
<input
type="number"
placeholder="100"
className="input"
value={contract.seedAmounts.NO}
onChange={(e) => {
setContract({
...contract,
seedAmounts: {
...contract.seedAmounts,
NO: parseInt(e.target.value),
},
pot: {
...contract.pot,
NO: parseInt(e.target.value),
},
})
}}
/>
</div>
</div>
</div>
{/* TODO: Show a preview of the created market here? */}
<div className="flex justify-end mt-6">
<button
type="submit"
className="btn btn-primary"
disabled={isSubmitting}
onClick={(e) => {
e.preventDefault()
saveContract()
submit()
}}
>
Create market
@ -175,7 +102,8 @@ export default function NewContract() {
<Spacer h={10} />
<Title text="Your markets" />
<ContractsList contracts={contracts} />
<ContractsList />
</div>
</div>
)