From 91990f524f6345c0a54b2489dc581abd44c6cb2d Mon Sep 17 00:00:00 2001 From: James Grugett Date: Fri, 6 May 2022 14:28:46 -0400 Subject: [PATCH] Outline of numeric bet panel --- web/components/amount-input.tsx | 55 ++++++ web/components/numeric-bet-panel.tsx | 222 ++++++++++++++++++++++++ web/pages/[username]/[contractSlug].tsx | 28 ++- 3 files changed, 299 insertions(+), 6 deletions(-) create mode 100644 web/components/numeric-bet-panel.tsx diff --git a/web/components/amount-input.tsx b/web/components/amount-input.tsx index 76111b6f..8097b527 100644 --- a/web/components/amount-input.tsx +++ b/web/components/amount-input.tsx @@ -139,6 +139,61 @@ export function BuyAmountInput(props: { ) } +export function BucketAmountInput(props: { + bucket: number | undefined + bucketCount: number + min: number + max: number + onChange: (newBucket: number | undefined) => void + error: string | undefined + setError: (error: string | undefined) => void + disabled?: boolean + className?: string + inputClassName?: string + // Needed to focus the amount input + inputRef?: React.MutableRefObject +}) { + const { + bucket, + bucketCount, + min, + max, + onChange, + error, + setError, + disabled, + className, + inputClassName, + inputRef, + } = props + + const onBucketChange = (bucket: number | undefined) => { + onChange(bucket) + + // Check for errors. + if (bucket !== undefined) { + if (bucket < 0 || bucket >= bucketCount) { + setError('Enter a number between 0 and ' + (bucketCount - 1)) + } else { + setError(undefined) + } + } + } + + return ( + + ) +} + export function SellAmountInput(props: { contract: FullContract amount: number | undefined diff --git a/web/components/numeric-bet-panel.tsx b/web/components/numeric-bet-panel.tsx new file mode 100644 index 00000000..74d11148 --- /dev/null +++ b/web/components/numeric-bet-panel.tsx @@ -0,0 +1,222 @@ +import clsx from 'clsx' +import { useState, useEffect } from 'react' +import { Bet } from '../../common/bet' +import { + getOutcomeProbabilityAfterBet, + calculateShares, + calculatePayoutAfterCorrectBet, +} from '../../common/calculate' +import { NumericContract } from '../../common/contract' +import { + formatPercent, + formatWithCommas, + formatMoney, +} from '../../common/util/format' +import { useFocus } from '../hooks/use-focus' +import { useUser } from '../hooks/use-user' +import { placeBet } from '../lib/firebase/api-call' +import { firebaseLogin, User } from '../lib/firebase/users' +import { BucketAmountInput, BuyAmountInput } from './amount-input' +import { InfoTooltip } from './info-tooltip' +import { Col } from './layout/col' +import { Row } from './layout/row' +import { Spacer } from './layout/spacer' + +export function NumericBetPanel(props: { + contract: NumericContract + className?: string +}) { + const { contract, className } = props + const user = useUser() + + return ( + +
Place your bet
+ + + + {user === null && ( + + )} + + ) +} + +function NumericBuyPanel(props: { + contract: NumericContract + user: User | null | undefined + onBuySuccess?: () => void +}) { + const { contract, user, onBuySuccess } = props + const { bucketCount, min, max } = contract + + const [bucketChoice, setBucketChoice] = useState( + undefined + ) + const [betAmount, setBetAmount] = useState(undefined) + const [error, setError] = useState() + const [isSubmitting, setIsSubmitting] = useState(false) + const [wasSubmitted, setWasSubmitted] = useState(false) + + const [inputRef, focusAmountInput] = useFocus() + + useEffect(() => { + focusAmountInput() + }, [focusAmountInput]) + + function onBetChange(newAmount: number | undefined) { + setWasSubmitted(false) + setBetAmount(newAmount) + } + + async function submitBet() { + if (!user || !betAmount) return + + setError(undefined) + setIsSubmitting(true) + + const result = await placeBet({ + amount: betAmount, + outcome: bucketChoice, + contractId: contract.id, + }).then((r) => r.data as any) + + console.log('placed bet. Result:', result) + + if (result?.status === 'success') { + setIsSubmitting(false) + setWasSubmitted(true) + setBetAmount(undefined) + if (onBuySuccess) onBuySuccess() + } else { + setError(result?.message || 'Error placing bet') + setIsSubmitting(false) + } + } + + const betDisabled = isSubmitting || !betAmount || error + + const initialProb = 0 + const outcomeProb = getOutcomeProbabilityAfterBet( + contract, + bucketChoice || 'YES', + betAmount ?? 0 + ) + const resultProb = bucketChoice === 'NO' ? 1 - outcomeProb : outcomeProb + + const shares = calculateShares( + contract, + betAmount ?? 0, + bucketChoice || 'YES' + ) + + const currentPayout = betAmount + ? calculatePayoutAfterCorrectBet(contract, { + outcome: bucketChoice, + amount: betAmount, + shares, + } as Bet) + : 0 + + const currentReturn = betAmount ? (currentPayout - betAmount) / betAmount : 0 + const currentReturnPercent = formatPercent(currentReturn) + + const dpmTooltip = + contract.mechanism === 'dpm-2' + ? `Current payout for ${formatWithCommas(shares)} / ${formatWithCommas( + shares + + contract.totalShares[bucketChoice ?? 'YES'] - + (contract.phantomShares + ? contract.phantomShares[bucketChoice ?? 'YES'] + : 0) + )} ${bucketChoice ?? 'YES'} shares` + : undefined + return ( + <> +
Numeric value
+ setBucketChoice(bucket ? `${bucket}` : undefined)} + error={error} + setError={setError} + disabled={isSubmitting} + inputRef={inputRef} + /> + +
Amount
+ + + + +
Probability
+ +
{formatPercent(initialProb)}
+
+
{formatPercent(resultProb)}
+
+
+ + + +
+ {contract.mechanism === 'dpm-2' ? ( + <> + Estimated +
payout if correct + + ) : ( + <>Payout if correct + )} +
+ + {dpmTooltip && } +
+ + + {formatMoney(currentPayout)} + + (+{currentReturnPercent}) + +
+ + + + + {user && ( + + )} + + {wasSubmitted &&
Bet submitted!
} + + ) +} diff --git a/web/pages/[username]/[contractSlug].tsx b/web/pages/[username]/[contractSlug].tsx index 34ed776a..c092adfa 100644 --- a/web/pages/[username]/[contractSlug].tsx +++ b/web/pages/[username]/[contractSlug].tsx @@ -1,5 +1,6 @@ import React, { useEffect, useState } from 'react' import { ArrowLeftIcon } from '@heroicons/react/outline' +import _ from 'lodash' import { useContractWithPreload } from '../../hooks/use-contract' import { ContractOverview } from '../../components/contract/contract-overview' @@ -24,17 +25,23 @@ import Custom404 from '../404' import { AnswersPanel } from '../../components/answers/answers-panel' import { fromPropz, usePropz } from '../../hooks/use-propz' import { Leaderboard } from '../../components/leaderboard' -import _ from 'lodash' import { resolvedPayout } from '../../../common/calculate' import { formatMoney } from '../../../common/util/format' import { FeedBet, FeedComment } from '../../components/feed/feed-items' import { useUserById } from '../../hooks/use-users' import { ContractTabs } from '../../components/contract/contract-tabs' import { FirstArgument } from '../../../common/util/types' -import { DPM, FreeResponse, FullContract } from '../../../common/contract' +import { + BinaryContract, + DPM, + FreeResponse, + FullContract, + NumericContract, +} from '../../../common/contract' import { contractTextDetails } from '../../components/contract/contract-details' import { useWindowSize } from '../../hooks/use-window-size' import Confetti from 'react-confetti' +import { NumericBetPanel } from '../../components/numeric-bet-panel' export const getStaticProps = fromPropz(getStaticPropz) export async function getStaticPropz(props: { @@ -116,18 +123,27 @@ export function ContractPageContent(props: FirstArgument) { const isCreator = user?.id === creatorId const isBinary = outcomeType === 'BINARY' + const isNumeric = outcomeType === 'NUMERIC' const allowTrade = tradingAllowed(contract) const allowResolve = !isResolved && isCreator && !!user - const hasSidePanel = isBinary && (allowTrade || allowResolve) + const hasSidePanel = (isBinary || isNumeric) && (allowTrade || allowResolve) const ogCardProps = getOpenGraphProps(contract) const rightSidebar = hasSidePanel ? ( - {allowTrade && ( - + {allowTrade && + (isNumeric ? ( + + ) : ( + + ))} + {allowResolve && ( + )} - {allowResolve && } ) : null