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 { 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">
|
||||
|
|
|
@ -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)
|
||||
|
|
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 { 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>
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue
Block a user