From 5ffe266cf7369441bef2dde7a0a93c4b4c57fd5e Mon Sep 17 00:00:00 2001 From: mantikoros Date: Mon, 13 Dec 2021 19:07:28 -0600 Subject: [PATCH] contract creation: use slug, calculate seed amounts from given probability --- web/components/contracts-list.tsx | 17 +++- web/lib/firebase/contracts.ts | 10 ++ web/lib/service/create-contract.ts | 51 ++++++++++ web/lib/util/random-string.ts | 2 + web/lib/util/slugify.ts | 11 +++ web/pages/contract/index.tsx | 150 ++++++++--------------------- 6 files changed, 127 insertions(+), 114 deletions(-) create mode 100644 web/lib/service/create-contract.ts create mode 100644 web/lib/util/random-string.ts create mode 100644 web/lib/util/slugify.ts diff --git a/web/components/contracts-list.tsx b/web/components/contracts-list.tsx index fa1bedd2..97e2c06a 100644 --- a/web/components/contracts-list.tsx +++ b/web/components/contracts-list.tsx @@ -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([]) + + useEffect(() => { + if (creator?.id) { + listContracts(creator.id).then(setContracts) + } + }, [creator]) + return (
    diff --git a/web/lib/firebase/contracts.ts b/web/lib/firebase/contracts.ts index 1273c07b..f99d9806 100644 --- a/web/lib/firebase/contracts.ts +++ b/web/lib/firebase/contracts.ts @@ -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) diff --git a/web/lib/service/create-contract.ts b/web/lib/service/create-contract.ts new file mode 100644 index 00000000..55221e61 --- /dev/null +++ b/web/lib/service/create-contract.ts @@ -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 } +} diff --git a/web/lib/util/random-string.ts b/web/lib/util/random-string.ts new file mode 100644 index 00000000..c9dfc768 --- /dev/null +++ b/web/lib/util/random-string.ts @@ -0,0 +1,2 @@ + +export const randomString = () => Math.random().toString(16).substr(2, 14) \ No newline at end of file diff --git a/web/lib/util/slugify.ts b/web/lib/util/slugify.ts new file mode 100644 index 00000000..f3437959 --- /dev/null +++ b/web/lib/util/slugify.ts @@ -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) +} \ No newline at end of file diff --git a/web/pages/contract/index.tsx b/web/pages/contract/index.tsx index 058a98f6..6dadf0ee 100644 --- a/web/pages/contract/index.tsx +++ b/web/pages/contract/index.tsx @@ -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({ - 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([]) - 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) => - 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 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> - </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="form-control"> + <label className="label"> + <span className="label-text">Initial probability: {initialProb}%</span> + </label> - <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> + <input + type="range" + className="range-primary" + min="1" + max={99} + value={initialProb} + onChange={e => setInitialProb(parseInt(e.target.value))} + /> </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> )