Compare commits

...

14 Commits
main ... loans

Author SHA1 Message Date
James Grugett
35410e25d2 Fix to actually show investment instead of current value. And subtract bet amount from current value. 2022-03-01 21:07:22 -08:00
James Grugett
5d5a1860b5 Add comment for includeMetadataChanges. 2022-03-01 19:14:43 -08:00
James Grugett
45d322cfd3 Fix loan calc on front end 2022-03-01 19:07:32 -08:00
James Grugett
5bf4654bf4 Adjust bet panel width 2022-03-01 19:07:32 -08:00
Austin Chen
2692a98151 Reword loan copy 2022-03-01 18:52:34 -08:00
James Grugett
bfee2ea954 Fix bug where listen query was not updating data. 2022-03-01 18:45:10 -08:00
Austin Chen
45fe430a5a Clean up Sell popup UI 2022-03-01 18:29:13 -08:00
James Grugett
4da8000363 Fix layout of create answer bet info 2022-03-01 18:17:16 -08:00
James Grugett
eb7bf72f0c Floor remaining balance 2022-03-01 18:12:55 -08:00
James Grugett
1d73f6e863 Handle bets table for loans. Sell dialog explains how you will repay your loan. 2022-03-01 17:12:29 -08:00
James Grugett
1da30812ad Exclude sold bets from current loan amount 2022-03-01 16:59:46 -08:00
James Grugett
d5ae4a2377 Resolve emails include full payout not subtracting loan 2022-03-01 16:34:08 -08:00
James Grugett
42533c296a Loan frontend: show your loan amount in bet panel, answer bet panel 2022-03-01 16:26:37 -08:00
James Grugett
659f848bec Loan backend: Add loanAmount field to Bet, manage loans up to max loan amount per market -- buy, sell, and resolve. 2022-03-01 16:23:00 -08:00
19 changed files with 294 additions and 118 deletions

View File

@ -4,6 +4,7 @@ export type Bet = {
contractId: string
amount: number // bet size; negative if SELL bet
loanAmount?: number
outcome: string
shares: number // dynamic parimutuel pool weight; negative if SELL bet
@ -21,3 +22,5 @@ export type Bet = {
createdTime: number
}
export const MAX_LOAN_PER_CONTRACT = 20

View File

@ -1,4 +1,5 @@
import { Bet } from './bet'
import * as _ from 'lodash'
import { Bet, MAX_LOAN_PER_CONTRACT } from './bet'
import {
calculateShares,
getProbability,
@ -11,6 +12,7 @@ export const getNewBinaryBetInfo = (
user: User,
outcome: 'YES' | 'NO',
amount: number,
loanAmount: number,
contract: Contract,
newBetId: string
) => {
@ -45,6 +47,7 @@ export const getNewBinaryBetInfo = (
userId: user.id,
contractId: contract.id,
amount,
loanAmount,
shares,
outcome,
probBefore,
@ -52,7 +55,7 @@ export const getNewBinaryBetInfo = (
createdTime: Date.now(),
}
const newBalance = user.balance - amount
const newBalance = user.balance - (amount - loanAmount)
return { newBet, newPool, newTotalShares, newTotalBets, newBalance }
}
@ -61,6 +64,7 @@ export const getNewMultiBetInfo = (
user: User,
outcome: string,
amount: number,
loanAmount: number,
contract: Contract,
newBetId: string
) => {
@ -85,6 +89,7 @@ export const getNewMultiBetInfo = (
userId: user.id,
contractId: contract.id,
amount,
loanAmount,
shares,
outcome,
probBefore,
@ -92,7 +97,17 @@ export const getNewMultiBetInfo = (
createdTime: Date.now(),
}
const newBalance = user.balance - amount
const newBalance = user.balance - (amount - loanAmount)
return { newBet, newPool, newTotalShares, newTotalBets, newBalance }
}
export const getLoanAmount = (yourBets: Bet[], newBetAmount: number) => {
const openBets = yourBets.filter((bet) => !bet.isSold && !bet.sale)
const prevLoanAmount = _.sumBy(openBets, (bet) => bet.loanAmount ?? 0)
const loanAmount = Math.min(
newBetAmount,
MAX_LOAN_PER_CONTRACT - prevLoanAmount
)
return loanAmount
}

View File

@ -161,3 +161,12 @@ export const getPayoutsMultiOutcome = (
.map(({ userId, payout }) => ({ userId, payout }))
.concat([{ userId: contract.creatorId, payout: creatorPayout }]) // add creator fee
}
export const getLoanPayouts = (bets: Bet[]) => {
const betsWithLoans = bets.filter((bet) => bet.loanAmount)
const betsByUser = _.groupBy(betsWithLoans, (bet) => bet.userId)
const loansByUser = _.mapValues(betsByUser, (bets) =>
_.sumBy(bets, (bet) => -(bet.loanAmount ?? 0))
)
return _.toPairs(loansByUser).map(([userId, payout]) => ({ userId, payout }))
}

View File

@ -11,7 +11,7 @@ export const getSellBetInfo = (
newBetId: string
) => {
const { pool, totalShares, totalBets } = contract
const { id: betId, amount, shares, outcome } = bet
const { id: betId, amount, shares, outcome, loanAmount } = bet
const adjShareValue = calculateShareValue(contract, bet)
@ -57,7 +57,7 @@ export const getSellBetInfo = (
},
}
const newBalance = user.balance + saleAmount
const newBalance = user.balance + saleAmount - (loanAmount ?? 0)
return {
newBet,

View File

@ -3,10 +3,11 @@ import * as admin from 'firebase-admin'
import { Contract } from '../../common/contract'
import { User } from '../../common/user'
import { getNewMultiBetInfo } from '../../common/new-bet'
import { getLoanAmount, getNewMultiBetInfo } from '../../common/new-bet'
import { Answer } from '../../common/answer'
import { getContract, getValues } from './utils'
import { sendNewAnswerEmail } from './emails'
import { Bet } from '../../common/bet'
export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
async (
@ -55,6 +56,11 @@ export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
if (closeTime && Date.now() > closeTime)
return { status: 'error', message: 'Trading is closed' }
const yourBetsSnap = await transaction.get(
contractDoc.collection('bets').where('userId', '==', userId)
)
const yourBets = yourBetsSnap.docs.map((doc) => doc.data() as Bet)
const [lastAnswer] = await getValues<Answer>(
firestore
.collection(`contracts/${contractId}/answers`)
@ -92,8 +98,17 @@ export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
.collection(`contracts/${contractId}/bets`)
.doc()
const loanAmount = getLoanAmount(yourBets, amount)
const { newBet, newPool, newTotalShares, newTotalBets, newBalance } =
getNewMultiBetInfo(user, answerId, amount, contract, newBetDoc.id)
getNewMultiBetInfo(
user,
answerId,
amount,
loanAmount,
contract,
newBetDoc.id
)
transaction.create(newBetDoc, newBet)
transaction.update(contractDoc, {

View File

@ -3,7 +3,13 @@ import * as admin from 'firebase-admin'
import { Contract } from '../../common/contract'
import { User } from '../../common/user'
import { getNewBinaryBetInfo, getNewMultiBetInfo } from '../../common/new-bet'
import {
getLoanAmount,
getNewBinaryBetInfo,
getNewMultiBetInfo,
} from '../../common/new-bet'
import { Bet } from '../../common/bet'
import { getValues } from './utils'
export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
async (
@ -46,6 +52,11 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
if (closeTime && Date.now() > closeTime)
return { status: 'error', message: 'Trading is closed' }
const yourBetsSnap = await transaction.get(
contractDoc.collection('bets').where('userId', '==', userId)
)
const yourBets = yourBetsSnap.docs.map((doc) => doc.data() as Bet)
if (outcomeType === 'FREE_RESPONSE') {
const answerSnap = await transaction.get(
contractDoc.collection('answers').doc(outcome)
@ -58,16 +69,26 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
.collection(`contracts/${contractId}/bets`)
.doc()
const loanAmount = getLoanAmount(yourBets, amount)
const { newBet, newPool, newTotalShares, newTotalBets, newBalance } =
outcomeType === 'BINARY'
? getNewBinaryBetInfo(
user,
outcome as 'YES' | 'NO',
amount,
loanAmount,
contract,
newBetDoc.id
)
: getNewMultiBetInfo(
user,
outcome,
amount,
loanAmount,
contract,
newBetDoc.id
)
: getNewMultiBetInfo(user, outcome, amount, contract, newBetDoc.id)
transaction.create(newBetDoc, newBet)
transaction.update(contractDoc, {

View File

@ -7,7 +7,11 @@ import { User } from '../../common/user'
import { Bet } from '../../common/bet'
import { getUser, payUser } from './utils'
import { sendMarketResolutionEmail } from './emails'
import { getPayouts, getPayoutsMultiOutcome } from '../../common/payouts'
import {
getLoanPayouts,
getPayouts,
getPayoutsMultiOutcome,
} from '../../common/payouts'
import { removeUndefinedProps } from '../../common/util/object'
export const resolveMarket = functions
@ -99,13 +103,23 @@ export const resolveMarket = functions
? getPayoutsMultiOutcome(resolutions, contract, openBets)
: getPayouts(outcome, contract, openBets, resolutionProbability)
const loanPayouts = getLoanPayouts(openBets)
console.log('payouts:', payouts)
const groups = _.groupBy(payouts, (payout) => payout.userId)
const groups = _.groupBy(
[...payouts, ...loanPayouts],
(payout) => payout.userId
)
const userPayouts = _.mapValues(groups, (group) =>
_.sumBy(group, (g) => g.payout)
)
const groupsWithoutLoans = _.groupBy(payouts, (payout) => payout.userId)
const userPayoutsWithoutLoans = _.mapValues(groupsWithoutLoans, (group) =>
_.sumBy(group, (g) => g.payout)
)
const payoutPromises = Object.entries(userPayouts).map(
([userId, payout]) => payUser(userId, payout)
)
@ -116,7 +130,7 @@ export const resolveMarket = functions
await sendResolutionEmails(
openBets,
userPayouts,
userPayoutsWithoutLoans,
creator,
contract,
outcome,

View File

@ -1,15 +1,20 @@
import clsx from 'clsx'
import _ from 'lodash'
import { useUser } from '../hooks/use-user'
import { formatMoney } from '../../common/util/format'
import { AddFundsButton } from './add-funds-button'
import { Col } from './layout/col'
import { Row } from './layout/row'
import { useUserContractBets } from '../hooks/use-user-bets'
import { MAX_LOAN_PER_CONTRACT } from '../../common/bet'
import { InfoTooltip } from './info-tooltip'
import { Spacer } from './layout/spacer'
export function AmountInput(props: {
amount: number | undefined
onChange: (newAmount: number | undefined) => void
error: string | undefined
setError: (error: string | undefined) => void
contractId: string | undefined
minimumAmount?: number
disabled?: boolean
className?: string
@ -22,6 +27,7 @@ export function AmountInput(props: {
onChange,
error,
setError,
contractId,
disabled,
className,
inputClassName,
@ -31,10 +37,24 @@ export function AmountInput(props: {
const user = useUser()
const userBets = useUserContractBets(user?.id, contractId) ?? []
const openUserBets = userBets.filter((bet) => !bet.isSold && !bet.sale)
const prevLoanAmount = _.sumBy(openUserBets, (bet) => bet.loanAmount ?? 0)
const loanAmount = Math.min(
amount ?? 0,
MAX_LOAN_PER_CONTRACT - prevLoanAmount
)
const onAmountChange = (str: string) => {
if (str.includes('-')) {
onChange(undefined)
return
}
const amount = parseInt(str.replace(/[^\d]/, ''))
if (str && isNaN(amount)) return
if (amount >= 10 ** 9) return
onChange(str ? amount : undefined)
@ -47,7 +67,8 @@ export function AmountInput(props: {
}
}
const remainingBalance = Math.max(0, (user?.balance ?? 0) - (amount ?? 0))
const amountNetLoan = (amount ?? 0) - loanAmount
const remainingBalance = Math.max(0, (user?.balance ?? 0) - amountNetLoan)
return (
<Col className={className}>
@ -68,19 +89,34 @@ export function AmountInput(props: {
onChange={(e) => onAmountChange(e.target.value)}
/>
</label>
<Spacer h={4} />
{error && (
<div className="mr-auto mt-4 self-center whitespace-nowrap text-xs font-medium tracking-wide text-red-500">
<div className="mb-2 mr-auto self-center whitespace-nowrap text-xs font-medium tracking-wide text-red-500">
{error}
</div>
)}
{user && (
<Col className="mt-3 text-sm">
<div className="mb-2 whitespace-nowrap text-gray-500">
Remaining balance
</div>
<Row className="gap-4">
<div>{formatMoney(Math.floor(remainingBalance))}</div>
{user.balance !== 1000 && <AddFundsButton />}
<Col className="gap-3 text-sm">
{contractId && (
<Row className="items-center justify-between gap-2 text-gray-500">
<Row className="items-center gap-2">
Amount loaned{' '}
<InfoTooltip
text={`In every market, you get an interest-free loan on the first ${formatMoney(
MAX_LOAN_PER_CONTRACT
)}.`}
/>
</Row>
<span className="text-neutral">{formatMoney(loanAmount)}</span>{' '}
</Row>
)}
<Row className="items-center justify-between gap-2 text-gray-500">
Remaining balance{' '}
<span className="text-neutral">
{formatMoney(Math.floor(remainingBalance))}
</span>
</Row>
</Col>
)}

View File

@ -30,8 +30,9 @@ export function AnswerBetPanel(props: {
answer: Answer
contract: Contract
closePanel: () => void
className?: string
}) {
const { answer, contract, closePanel } = props
const { answer, contract, closePanel, className } = props
const { id: answerId } = answer
const user = useUser()
@ -97,7 +98,7 @@ export function AnswerBetPanel(props: {
const currentReturnPercent = (currentReturn * 100).toFixed() + '%'
return (
<Col className="items-start px-2 pb-2 pt-4 sm:pt-0">
<Col className={clsx('px-2 pb-2 pt-4 sm:pt-0', className)}>
<Row className="self-stretch items-center justify-between">
<div className="text-xl">Buy this answer</div>
@ -114,40 +115,44 @@ export function AnswerBetPanel(props: {
setError={setError}
disabled={isSubmitting}
inputRef={inputRef}
contractId={contract.id}
/>
<Col className="gap-3 mt-3 w-full">
<Row className="justify-between items-center text-sm">
<div className="text-gray-500">Probability</div>
<Row>
<div>{formatPercent(initialProb)}</div>
<div className="mx-2"></div>
<div>{formatPercent(resultProb)}</div>
</Row>
</Row>
<Spacer h={4} />
<div className="mt-2 mb-1 text-sm text-gray-500">Implied probability</div>
<Row>
<div>{formatPercent(initialProb)}</div>
<div className="mx-2"></div>
<div>{formatPercent(resultProb)}</div>
</Row>
<Spacer h={4} />
<Row className="mt-2 mb-1 items-center gap-2 text-sm text-gray-500">
Payout if chosen
<InfoTooltip
text={`Current payout for ${formatWithCommas(
shares
)} / ${formatWithCommas(
shares + contract.totalShares[answerId]
)} shares`}
/>
</Row>
<div>
{formatMoney(currentPayout)}
&nbsp; <span>(+{currentReturnPercent})</span>
</div>
<Row className="justify-between items-start text-sm gap-2">
<Row className="flex-nowrap whitespace-nowrap items-center gap-2 text-gray-500">
<div>Payout if chosen</div>
<InfoTooltip
text={`Current payout for ${formatWithCommas(
shares
)} / ${formatWithCommas(
shares + contract.totalShares[answerId]
)} shares`}
/>
</Row>
<Row className="flex-wrap justify-end items-end gap-2">
<span className="whitespace-nowrap">
{formatMoney(currentPayout)}
</span>
<span>(+{currentReturnPercent})</span>
</Row>
</Row>
</Col>
<Spacer h={6} />
{user ? (
<button
className={clsx(
'btn',
'btn self-stretch',
betDisabled ? 'btn-disabled' : 'btn-primary',
isSubmitting ? 'loading' : ''
)}
@ -157,7 +162,7 @@ export function AnswerBetPanel(props: {
</button>
) : (
<button
className="btn mt-4 whitespace-nowrap border-none bg-gradient-to-r from-teal-500 to-green-500 px-10 text-lg font-medium normal-case hover:from-teal-600 hover:to-green-600"
className="btn self-stretch whitespace-nowrap border-none bg-gradient-to-r from-teal-500 to-green-500 px-10 text-lg font-medium normal-case hover:from-teal-600 hover:to-green-600"
onClick={firebaseLogin}
>
Sign in to trade!

View File

@ -97,6 +97,7 @@ export function AnswerItem(props: {
answer={answer}
contract={contract}
closePanel={() => setIsBetting(false)}
className="sm:w-72"
/>
) : (
<Row className="items-center justify-end gap-4 self-end sm:self-start">

View File

@ -86,7 +86,7 @@ export function CreateAnswerPanel(props: { contract: Contract }) {
<div />
<Col
className={clsx(
'sm:flex-row gap-4',
'sm:flex-row sm:items-end gap-4',
text ? 'justify-between' : 'self-end'
)}
>
@ -101,34 +101,42 @@ export function CreateAnswerPanel(props: { contract: Contract }) {
setError={setAmountError}
minimumAmount={1}
disabled={isSubmitting}
contractId={contract.id}
/>
</Col>
<Col className="gap-2 mt-1">
<div className="text-sm text-gray-500">Implied probability</div>
<Row>
<div>{formatPercent(0)}</div>
<div className="mx-2"></div>
<div>{formatPercent(resultProb)}</div>
<Col className="gap-3">
<Row className="justify-between items-center text-sm">
<div className="text-gray-500">Probability</div>
<Row>
<div>{formatPercent(0)}</div>
<div className="mx-2"></div>
<div>{formatPercent(resultProb)}</div>
</Row>
</Row>
<Row className="mt-2 mb-1 items-center gap-2 text-sm text-gray-500">
Payout if chosen
<InfoTooltip
text={`Current payout for ${formatWithCommas(
shares
)} / ${formatWithCommas(shares)} shares`}
/>
<Row className="justify-between text-sm gap-2">
<Row className="flex-nowrap whitespace-nowrap items-center gap-2 text-gray-500">
<div>Payout if chosen</div>
<InfoTooltip
text={`Current payout for ${formatWithCommas(
shares
)} / ${formatWithCommas(shares)} shares`}
/>
</Row>
<Row className="flex-wrap justify-end items-end gap-2">
<span className="whitespace-nowrap">
{formatMoney(currentPayout)}
</span>
<span>(+{currentReturnPercent})</span>
</Row>
</Row>
<div>
{formatMoney(currentPayout)}
&nbsp; <span>(+{currentReturnPercent})</span>
</div>
</Col>
</>
)}
{user ? (
<button
className={clsx(
'btn self-end mt-2',
'btn mt-2',
canSubmit ? 'btn-outline' : 'btn-disabled',
isSubmitting && 'loading'
)}

View File

@ -144,7 +144,6 @@ export function BetPanel(props: {
text={panelTitle}
/>
{/* <div className="mt-2 mb-1 text-sm text-gray-500">Outcome</div> */}
<YesNoSelector
className="mb-4"
selected={betChoice}
@ -160,45 +159,49 @@ export function BetPanel(props: {
setError={setError}
disabled={isSubmitting}
inputRef={inputRef}
contractId={contract.id}
/>
<Spacer h={4} />
<Col className="gap-3 mt-3 w-full">
<Row className="justify-between items-center text-sm">
<div className="text-gray-500">Probability</div>
<Row>
<div>{formatPercent(initialProb)}</div>
<div className="mx-2"></div>
<div>{formatPercent(resultProb)}</div>
</Row>
</Row>
<div className="mt-2 mb-1 text-sm text-gray-500">Implied probability</div>
<Row>
<div>{formatPercent(initialProb)}</div>
<div className="mx-2"></div>
<div>{formatPercent(resultProb)}</div>
</Row>
{betChoice && (
<>
<Spacer h={4} />
<Row className="mt-2 mb-1 items-center gap-2 text-sm text-gray-500">
Payout if <OutcomeLabel outcome={betChoice} />
<Row className="justify-between items-start text-sm gap-2">
<Row className="flex-nowrap whitespace-nowrap items-center gap-2 text-gray-500">
<div>
Payout if <OutcomeLabel outcome={betChoice ?? 'YES'} />
</div>
<InfoTooltip
text={`Current payout for ${formatWithCommas(
shares
)} / ${formatWithCommas(
shares +
totalShares[betChoice] -
(phantomShares ? phantomShares[betChoice] : 0)
totalShares[betChoice ?? 'YES'] -
(phantomShares ? phantomShares[betChoice ?? 'YES'] : 0)
)} ${betChoice} shares`}
/>
</Row>
<div>
{formatMoney(currentPayout)}
&nbsp; <span>(+{currentReturnPercent})</span>
</div>
</>
)}
<Row className="flex-wrap justify-end items-end gap-2">
<span className="whitespace-nowrap">
{formatMoney(currentPayout)}
</span>
<span>(+{currentReturnPercent})</span>
</Row>
</Row>
</Col>
<Spacer h={6} />
<Spacer h={8} />
{user && (
<button
className={clsx(
'btn',
'btn flex-1',
betDisabled
? 'btn-disabled'
: betChoice === 'YES'
@ -213,7 +216,7 @@ export function BetPanel(props: {
)}
{user === null && (
<button
className="btn mt-4 whitespace-nowrap border-none bg-gradient-to-r from-teal-500 to-green-500 px-10 text-lg font-medium normal-case hover:from-teal-600 hover:to-green-600"
className="btn flex-1 whitespace-nowrap border-none bg-gradient-to-r from-teal-500 to-green-500 px-10 text-lg font-medium normal-case hover:from-teal-600 hover:to-green-600"
onClick={firebaseLogin}
>
Sign in to trade!

View File

@ -82,7 +82,8 @@ export function BetsList(props: { user: User }) {
if (bet.isSold || bet.sale) return 0
const contract = contracts.find((c) => c.id === contractId)
return contract ? calculatePayout(contract, bet, 'MKT') : 0
const payout = contract ? calculatePayout(contract, bet, 'MKT') : 0
return payout - (bet.loanAmount ?? 0)
})
}
)
@ -137,7 +138,7 @@ export function BetsList(props: { user: User }) {
<Col>
<div className="text-sm text-gray-500">Invested</div>
<div className="text-lg">
{formatMoney(currentBetsValue)}{' '}
{formatMoney(currentInvestment)}{' '}
<ProfitBadge profitPercent={investedProfit} />
</div>
</Col>
@ -457,6 +458,7 @@ function BetRow(props: { bet: Bet; contract: Contract; saleBet?: Bet }) {
shares,
isSold,
isAnte,
loanAmount,
} = bet
const { isResolved, closeTime } = contract
@ -464,7 +466,7 @@ function BetRow(props: { bet: Bet; contract: Contract; saleBet?: Bet }) {
const saleAmount = saleBet?.sale?.amount
const saleDisplay = bet.isAnte ? (
const saleDisplay = isAnte ? (
'ANTE'
) : saleAmount !== undefined ? (
<>{formatMoney(saleAmount)} (sold)</>
@ -491,7 +493,10 @@ function BetRow(props: { bet: Bet; contract: Contract; saleBet?: Bet }) {
<td>
<OutcomeLabel outcome={outcome} />
</td>
<td>{formatMoney(amount)}</td>
<td>
{formatMoney(amount)}
{loanAmount ? ` (${formatMoney(loanAmount ?? 0)} loan)` : ''}
</td>
<td>{saleDisplay}</td>
{!isResolved && <td>{payoutIfChosenDisplay}</td>}
<td>
@ -510,18 +515,19 @@ function SellButton(props: { contract: Contract; bet: Bet }) {
}, [])
const { contract, bet } = props
const isBinary = contract.outcomeType === 'BINARY'
const { outcome, shares, loanAmount } = bet
const [isSubmitting, setIsSubmitting] = useState(false)
const initialProb = getOutcomeProbability(
contract.totalShares,
bet.outcome === 'NO' ? 'YES' : bet.outcome
outcome === 'NO' ? 'YES' : outcome
)
const outcomeProb = getProbabilityAfterSale(
contract.totalShares,
bet.outcome,
bet.shares
outcome,
shares
)
const saleAmount = calculateSaleAmount(contract, bet)
@ -533,7 +539,7 @@ function SellButton(props: { contract: Contract; bet: Bet }) {
className: clsx('btn-sm', isSubmitting && 'btn-disabled loading'),
label: 'Sell',
}}
submitBtn={{ className: 'btn-primary' }}
submitBtn={{ className: 'btn-primary', label: 'Sell' }}
onSubmit={async () => {
setIsSubmitting(true)
await sellBet({ contractId: contract.id, betId: bet.id })
@ -541,17 +547,19 @@ function SellButton(props: { contract: Contract; bet: Bet }) {
}}
>
<div className="mb-4 text-2xl">
Sell <OutcomeLabel outcome={bet.outcome} />
</div>
<div>
Do you want to sell {formatWithCommas(bet.shares)} shares of{' '}
<OutcomeLabel outcome={bet.outcome} /> for {formatMoney(saleAmount)}?
Sell {formatWithCommas(shares)} shares of{' '}
<OutcomeLabel outcome={outcome} /> for {formatMoney(saleAmount)}?
</div>
{!!loanAmount && (
<div className="mt-2">
You will also pay back {formatMoney(loanAmount)} of your loan, for a
net of {formatMoney(saleAmount - loanAmount)}.
</div>
)}
<div className="mt-2 mb-1 text-sm text-gray-500">
({isBinary ? 'Updated' : <OutcomeLabel outcome={bet.outcome} />}{' '}
probability: {formatPercent(initialProb)} {formatPercent(outcomeProb)}
)
<div className="mt-2 mb-1 text-sm">
Market probability: {formatPercent(initialProb)} {' '}
{formatPercent(outcomeProb)}
</div>
</ConfirmationButton>
)

View File

@ -1,6 +1,10 @@
import _ from 'lodash'
import { useEffect, useState } from 'react'
import { Bet, listenForUserBets } from '../lib/firebase/bets'
import {
Bet,
listenForUserBets,
listenForUserContractBets,
} from '../lib/firebase/bets'
export const useUserBets = (userId: string | undefined) => {
const [bets, setBets] = useState<Bet[] | undefined>(undefined)
@ -12,6 +16,20 @@ export const useUserBets = (userId: string | undefined) => {
return bets
}
export const useUserContractBets = (
userId: string | undefined,
contractId: string | undefined
) => {
const [bets, setBets] = useState<Bet[] | undefined>(undefined)
useEffect(() => {
if (userId && contractId)
return listenForUserContractBets(userId, contractId, setBets)
}, [userId, contractId])
return bets
}
export const useUserBetContracts = (userId: string | undefined) => {
const [contractIds, setContractIds] = useState<string[] | undefined>()

View File

@ -74,6 +74,21 @@ export function listenForUserBets(
})
}
export function listenForUserContractBets(
userId: string,
contractId: string,
setBets: (bets: Bet[]) => void
) {
const betsQuery = query(
collection(db, 'contracts', contractId, 'bets'),
where('userId', '==', userId)
)
return listenForValues<Bet>(betsQuery, (bets) => {
bets.sort((bet1, bet2) => bet1.createdTime - bet2.createdTime)
setBets(bets)
})
}
export function withoutAnteBets(contract: Contract, bets?: Bet[]) {
const { createdTime } = contract

View File

@ -1,6 +1,5 @@
import { db } from './init'
import {
doc,
getDoc,
getDocs,
onSnapshot,
@ -22,7 +21,9 @@ export function listenForValue<T>(
docRef: DocumentReference,
setValue: (value: T | null) => void
) {
return onSnapshot(docRef, (snapshot) => {
// Exclude cached snapshots so we only trigger on fresh data.
// includeMetadataChanges ensures listener is called even when server data is the same as cached data.
return onSnapshot(docRef, { includeMetadataChanges: true }, (snapshot) => {
if (snapshot.metadata.fromCache) return
const value = snapshot.exists() ? (snapshot.data() as T) : null
@ -34,7 +35,9 @@ export function listenForValues<T>(
query: Query,
setValues: (values: T[]) => void
) {
return onSnapshot(query, (snapshot) => {
// Exclude cached snapshots so we only trigger on fresh data.
// includeMetadataChanges ensures listener is called even when server data is the same as cached data.
return onSnapshot(query, { includeMetadataChanges: true }, (snapshot) => {
if (snapshot.metadata.fromCache) return
const values = snapshot.docs.map((doc) => doc.data() as T)

View File

@ -145,7 +145,7 @@ export default function ContractPage(props: {
<Col className="flex-1">
{allowTrade && (
<BetPanel className="hidden lg:inline" contract={contract} />
<BetPanel className="hidden lg:flex" contract={contract} />
)}
{allowResolve && (
<ResolutionPanel creator={user} contract={contract} />

View File

@ -248,6 +248,7 @@ export function NewContract(props: { question: string; tag?: string }) {
error={anteError}
setError={setAnteError}
disabled={isSubmitting}
contractId={undefined}
/>
</div>

View File

@ -245,6 +245,7 @@ ${TEST_VALUE}
error={anteError}
setError={setAnteError}
disabled={isSubmitting}
contractId={undefined}
/>
</div>