contract creation: use slug, calculate seed amounts from given probability
This commit is contained in:
parent
571c1307fa
commit
5ffe266cf7
|
@ -1,5 +1,7 @@
|
||||||
import Link from 'next/link'
|
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 }) {
|
function ContractCard(props: { contract: Contract }) {
|
||||||
const { contract } = props
|
const { contract } = props
|
||||||
|
@ -49,8 +51,17 @@ function ContractCard(props: { contract: Contract }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ContractsList(props: { contracts: Contract[] }) {
|
export function ContractsList(props: {}) {
|
||||||
const { contracts } = props
|
const creator = useUser()
|
||||||
|
|
||||||
|
const [contracts, setContracts] = useState<Contract[]>([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (creator?.id) {
|
||||||
|
listContracts(creator.id).then(setContracts)
|
||||||
|
}
|
||||||
|
}, [creator])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-gray-200 shadow-xl overflow-hidden sm:rounded-md max-w-4xl w-full">
|
<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">
|
<ul role="list" className="divide-y divide-gray-300">
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
getDocs,
|
getDocs,
|
||||||
onSnapshot,
|
onSnapshot,
|
||||||
orderBy,
|
orderBy,
|
||||||
|
getDoc,
|
||||||
} from 'firebase/firestore'
|
} from 'firebase/firestore'
|
||||||
|
|
||||||
export type Contract = {
|
export type Contract = {
|
||||||
|
@ -42,6 +43,15 @@ export async function setContract(contract: Contract) {
|
||||||
await setDoc(docRef, 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) {
|
export async function deleteContract(contractId: string) {
|
||||||
const docRef = doc(db, 'contracts', contractId)
|
const docRef = doc(db, 'contracts', contractId)
|
||||||
await deleteDoc(docRef)
|
await deleteDoc(docRef)
|
||||||
|
|
51
web/lib/service/create-contract.ts
Normal file
51
web/lib/service/create-contract.ts
Normal 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 }
|
||||||
|
}
|
2
web/lib/util/random-string.ts
Normal file
2
web/lib/util/random-string.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
|
||||||
|
export const randomString = () => Math.random().toString(16).substr(2, 14)
|
11
web/lib/util/slugify.ts
Normal file
11
web/lib/util/slugify.ts
Normal 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)
|
||||||
|
}
|
|
@ -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 { ContractsList } from '../../components/contracts-list'
|
||||||
import { Header } from '../../components/header'
|
import { Header } from '../../components/header'
|
||||||
import { Spacer } from '../../components/layout/spacer'
|
import { Spacer } from '../../components/layout/spacer'
|
||||||
import { Title } from '../../components/title'
|
import { Title } from '../../components/title'
|
||||||
import { useUser } from '../../hooks/use-user'
|
import { useUser } from '../../hooks/use-user'
|
||||||
import {
|
import { createContract } from '../../lib/service/create-contract'
|
||||||
Contract,
|
|
||||||
listContracts,
|
|
||||||
setContract as pushContract,
|
|
||||||
} from '../../lib/firebase/contracts'
|
|
||||||
|
|
||||||
// Allow user to create a new contract
|
// Allow user to create a new contract
|
||||||
// TODO: Extract to a reusable UI, for listing contracts too?
|
|
||||||
export default function NewContract() {
|
export default function NewContract() {
|
||||||
const creator = useUser()
|
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
|
const [initialProb, setInitialProb] = useState(50)
|
||||||
createdTime: Date.now(),
|
const [question, setQuestion] = useState('')
|
||||||
lastUpdatedTime: Date.now(),
|
const [description, setDescription] = useState('')
|
||||||
} as Contract)
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
|
||||||
const [contracts, setContracts] = useState<Contract[]>([])
|
async function submit() {
|
||||||
useEffect(() => {
|
// TODO: add more rigorous error handling for question, description
|
||||||
if (creator?.id) {
|
if (!creator || !question || !description) return
|
||||||
setContract((contract) => ({
|
|
||||||
...contract,
|
|
||||||
creatorId: creator.id,
|
|
||||||
creatorName: creator.name,
|
|
||||||
}))
|
|
||||||
listContracts(creator.id).then(setContracts)
|
|
||||||
}
|
|
||||||
}, [creator])
|
|
||||||
|
|
||||||
async function saveContract() {
|
setIsSubmitting(true)
|
||||||
await pushContract(contract)
|
|
||||||
// Update local contract list
|
|
||||||
setContracts([{ ...contract }, ...contracts])
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveField(field: keyof Contract) {
|
const contract = await createContract(question, description, initialProb, creator)
|
||||||
return (changeEvent: React.ChangeEvent<any>) =>
|
await router.push(`contract/${contract.id}`)
|
||||||
setContract((c) => ({ ...c, [field]: changeEvent.target.value }))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)...`
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Header />
|
<Header />
|
||||||
|
|
||||||
<div className="max-w-4xl py-12 lg:mx-auto px-4">
|
<div className="max-w-4xl py-12 lg:mx-auto px-4">
|
||||||
<Title text="Create a new prediction market" />
|
<Title text="Create a new prediction market" />
|
||||||
|
|
||||||
<div className="w-full bg-gray-200 rounded-lg shadow-xl p-6">
|
<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 */}
|
{/* 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 */}
|
{/* When the form is submitted, create a new contract in the database */}
|
||||||
<form>
|
<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">
|
<div className="form-control">
|
||||||
<label className="label">
|
<label className="label">
|
||||||
<span className="label-text">Question</span>
|
<span className="label-text">Question</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="e.g. Will the FDA approve Paxlovid before Jun 2nd, 2022?"
|
placeholder="e.g. Will the FDA approve Paxlovid before Jun 2nd, 2022?"
|
||||||
className="input"
|
className="input"
|
||||||
value={contract.question}
|
value={question}
|
||||||
onChange={saveField('question')}
|
onChange={e => setQuestion(e.target.value || '')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -91,79 +59,38 @@ export default function NewContract() {
|
||||||
<label className="label">
|
<label className="label">
|
||||||
<span className="label-text">Description</span>
|
<span className="label-text">Description</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<textarea
|
<textarea
|
||||||
className="textarea h-24 textarea-bordered"
|
className="textarea h-24 textarea-bordered"
|
||||||
placeholder={descriptionPlaceholder}
|
placeholder={descriptionPlaceholder}
|
||||||
value={contract.description}
|
value={description}
|
||||||
onChange={saveField('description')}
|
onChange={e => setDescription(e.target.value || '')}
|
||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-6 grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">
|
<div className="form-control">
|
||||||
<div className="sm:col-span-3">
|
<label className="label">
|
||||||
<div className="form-control">
|
<span className="label-text">Initial probability: {initialProb}%</span>
|
||||||
<label className="label">
|
</label>
|
||||||
<span className="label-text">Yes Seed</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),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="sm:col-span-3">
|
<input
|
||||||
<div className="form-control">
|
type="range"
|
||||||
<label className="label">
|
className="range-primary"
|
||||||
<span className="label-text">No Seed</span>
|
min="1"
|
||||||
</label>
|
max={99}
|
||||||
<input
|
value={initialProb}
|
||||||
type="number"
|
onChange={e => setInitialProb(parseInt(e.target.value))}
|
||||||
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>
|
</div>
|
||||||
|
|
||||||
{/* TODO: Show a preview of the created market here? */}
|
|
||||||
|
|
||||||
<div className="flex justify-end mt-6">
|
<div className="flex justify-end mt-6">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="btn btn-primary"
|
className="btn btn-primary"
|
||||||
|
disabled={isSubmitting}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
saveContract()
|
submit()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Create market
|
Create market
|
||||||
|
@ -175,7 +102,8 @@ export default function NewContract() {
|
||||||
<Spacer h={10} />
|
<Spacer h={10} />
|
||||||
|
|
||||||
<Title text="Your markets" />
|
<Title text="Your markets" />
|
||||||
<ContractsList contracts={contracts} />
|
|
||||||
|
<ContractsList />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user