import clsx from 'clsx'
import React, { useEffect, useState } from 'react'
import { partition, sumBy } from 'lodash'
import { useUser } from 'web/hooks/use-user'
import { BinaryContract, CPMMBinaryContract } from 'common/contract'
import { Col } from './layout/col'
import { Row } from './layout/row'
import { Spacer } from './layout/spacer'
import { YesNoSelector } from './yes-no-selector'
import {
formatMoney,
formatPercent,
formatWithCommas,
} from 'common/util/format'
import { Title } from './title'
import { User } from 'web/lib/firebase/users'
import { Bet } from 'common/bet'
import { APIError, placeBet } from 'web/lib/firebase/api-call'
import { sellShares } from 'web/lib/firebase/api-call'
import { AmountInput, BuyAmountInput } from './amount-input'
import { InfoTooltip } from './info-tooltip'
import { BinaryOutcomeLabel } from './outcome-label'
import {
calculatePayoutAfterCorrectBet,
calculateShares,
getProbability,
getOutcomeProbabilityAfterBet,
} from 'common/calculate'
import { useFocus } from 'web/hooks/use-focus'
import { useUserContractBets } from 'web/hooks/use-user-bets'
import {
calculateCpmmSale,
getCpmmProbability,
getCpmmLiquidityFee,
} from 'common/calculate-cpmm'
import { SellRow } from './sell-row'
import { useSaveShares } from './use-save-shares'
import { SignUpPrompt } from './sign-up-prompt'
export function BetPanel(props: {
contract: BinaryContract
className?: string
}) {
const { contract, className } = props
const user = useUser()
const userBets = useUserContractBets(user?.id, contract.id)
const { yesFloorShares, noFloorShares } = useSaveShares(contract, userBets)
const sharesOutcome = yesFloorShares
? 'YES'
: noFloorShares
? 'NO'
: undefined
return (
Place your bet
{/* */}
)
}
export function BetPanelSwitcher(props: {
contract: BinaryContract
className?: string
title?: string // Set if BetPanel is on a feed modal
selected?: 'YES' | 'NO'
onBetSuccess?: () => void
}) {
const { contract, className, title, selected, onBetSuccess } = props
const { mechanism } = contract
const user = useUser()
const userBets = useUserContractBets(user?.id, contract.id)
const [tradeType, setTradeType] = useState<'BUY' | 'SELL'>('BUY')
const { yesFloorShares, noFloorShares, yesShares, noShares } = useSaveShares(
contract,
userBets
)
const floorShares = yesFloorShares || noFloorShares
const sharesOutcome = yesFloorShares
? 'YES'
: noFloorShares
? 'NO'
: undefined
useEffect(() => {
// Switch back to BUY if the user has sold all their shares.
if (tradeType === 'SELL' && sharesOutcome === undefined) {
setTradeType('BUY')
}
}, [tradeType, sharesOutcome])
return (
{sharesOutcome && mechanism === 'cpmm-1' && (
You have {formatWithCommas(floorShares)}{' '}
shares
{tradeType === 'BUY' && (
)}
)}
{tradeType === 'SELL' &&
mechanism == 'cpmm-1' &&
user &&
sharesOutcome && (
)}
{tradeType === 'BUY' && (
)}
)
}
function BuyPanel(props: {
contract: BinaryContract
user: User | null | undefined
selected?: 'YES' | 'NO'
onBuySuccess?: () => void
}) {
const { contract, user, selected, onBuySuccess } = props
const [betChoice, setBetChoice] = useState<'YES' | 'NO' | undefined>(selected)
const [betAmount, setBetAmount] = useState(undefined)
const [error, setError] = useState()
const [isSubmitting, setIsSubmitting] = useState(false)
const [wasSubmitted, setWasSubmitted] = useState(false)
const [inputRef, focusAmountInput] = useFocus()
useEffect(() => {
if (selected) focusAmountInput()
}, [selected, focusAmountInput])
function onBetChoice(choice: 'YES' | 'NO') {
setBetChoice(choice)
setWasSubmitted(false)
focusAmountInput()
}
function onBetChange(newAmount: number | undefined) {
setWasSubmitted(false)
setBetAmount(newAmount)
if (!betChoice) {
setBetChoice('YES')
}
}
async function submitBet() {
if (!user || !betAmount) return
setError(undefined)
setIsSubmitting(true)
placeBet({
amount: betAmount,
outcome: betChoice,
contractId: contract.id,
})
.then((r) => {
console.log('placed bet. Result:', r)
setIsSubmitting(false)
setWasSubmitted(true)
setBetAmount(undefined)
if (onBuySuccess) onBuySuccess()
})
.catch((e) => {
if (e instanceof APIError) {
setError(e.toString())
} else {
console.error(e)
setError('Error placing bet')
}
setIsSubmitting(false)
})
}
const betDisabled = isSubmitting || !betAmount || error
const initialProb = getProbability(contract)
const outcomeProb = getOutcomeProbabilityAfterBet(
contract,
betChoice || 'YES',
betAmount ?? 0
)
const resultProb = betChoice === 'NO' ? 1 - outcomeProb : outcomeProb
const shares = calculateShares(contract, betAmount ?? 0, betChoice || 'YES')
const currentPayout = betAmount
? calculatePayoutAfterCorrectBet(contract, {
outcome: betChoice,
amount: betAmount,
shares,
} as Bet)
: 0
const currentReturn = betAmount ? (currentPayout - betAmount) / betAmount : 0
const currentReturnPercent = formatPercent(currentReturn)
const cpmmFees =
contract.mechanism === 'cpmm-1' &&
getCpmmLiquidityFee(contract, betAmount ?? 0, betChoice ?? 'YES').totalFees
const dpmTooltip =
contract.mechanism === 'dpm-2'
? `Current payout for ${formatWithCommas(shares)} / ${formatWithCommas(
shares +
contract.totalShares[betChoice ?? 'YES'] -
(contract.phantomShares
? contract.phantomShares[betChoice ?? 'YES']
: 0)
)} ${betChoice ?? 'YES'} shares`
: undefined
return (
<>
onBetChoice(choice)}
/>
Amount
Probability
{formatPercent(initialProb)}
→
{formatPercent(resultProb)}
{contract.mechanism === 'dpm-2' ? (
<>
Estimated
payout if{' '}
>
) : (
<>
Payout if
>
)}
{cpmmFees !== false && (
)}
{dpmTooltip && }
{formatMoney(currentPayout)}
(+{currentReturnPercent})
{user && (
)}
{wasSubmitted && Bet submitted!
}
>
)
}
export function SellPanel(props: {
contract: CPMMBinaryContract
userBets: Bet[]
shares: number
sharesOutcome: 'YES' | 'NO'
user: User
onSellSuccess?: () => void
}) {
const { contract, shares, sharesOutcome, userBets, user, onSellSuccess } =
props
const [amount, setAmount] = useState(shares)
const [error, setError] = useState()
const [isSubmitting, setIsSubmitting] = useState(false)
const [wasSubmitted, setWasSubmitted] = useState(false)
const betDisabled = isSubmitting || !amount || error
async function submitSell() {
if (!user || !amount) return
setError(undefined)
setIsSubmitting(true)
// Sell all shares if remaining shares would be < 1
const sellAmount = amount === Math.floor(shares) ? shares : amount
await sellShares({
shares: sellAmount,
outcome: sharesOutcome,
contractId: contract.id,
})
.then((r) => {
console.log('Sold shares. Result:', r)
setIsSubmitting(false)
setWasSubmitted(true)
setAmount(undefined)
if (onSellSuccess) onSellSuccess()
})
.catch((e) => {
if (e instanceof APIError) {
setError(e.toString())
} else {
console.error(e)
setError('Error selling')
}
setIsSubmitting(false)
})
}
const initialProb = getProbability(contract)
const { newPool } = calculateCpmmSale(
contract,
Math.min(amount ?? 0, shares),
sharesOutcome
)
const resultProb = getCpmmProbability(newPool, contract.p)
const openUserBets = userBets.filter((bet) => !bet.isSold && !bet.sale)
const [yesBets, noBets] = partition(
openUserBets,
(bet) => bet.outcome === 'YES'
)
const [yesShares, noShares] = [
sumBy(yesBets, (bet) => bet.shares),
sumBy(noBets, (bet) => bet.shares),
]
const sellOutcome = yesShares ? 'YES' : noShares ? 'NO' : undefined
const ownedShares = Math.round(yesShares) || Math.round(noShares)
const sharesSold = Math.min(amount ?? 0, ownedShares)
const { saleValue } = calculateCpmmSale(
contract,
sharesSold,
sellOutcome as 'YES' | 'NO'
)
const onAmountChange = (amount: number | undefined) => {
setAmount(amount)
// Check for errors.
if (amount !== undefined) {
if (amount > ownedShares) {
setError(`Maximum ${formatWithCommas(Math.floor(ownedShares))} shares`)
} else {
setError(undefined)
}
}
}
return (
<>
Sale proceeds
{formatMoney(saleValue)}
Probability
{formatPercent(initialProb)}
→
{formatPercent(resultProb)}
{wasSubmitted && Sell submitted!
}
>
)
}