Compare commits
14 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
35410e25d2 | ||
|
5d5a1860b5 | ||
|
45d322cfd3 | ||
|
5bf4654bf4 | ||
|
2692a98151 | ||
|
bfee2ea954 | ||
|
45fe430a5a | ||
|
4da8000363 | ||
|
eb7bf72f0c | ||
|
1d73f6e863 | ||
|
1da30812ad | ||
|
d5ae4a2377 | ||
|
42533c296a | ||
|
659f848bec |
|
@ -4,6 +4,7 @@ export type Bet = {
|
||||||
contractId: string
|
contractId: string
|
||||||
|
|
||||||
amount: number // bet size; negative if SELL bet
|
amount: number // bet size; negative if SELL bet
|
||||||
|
loanAmount?: number
|
||||||
outcome: string
|
outcome: string
|
||||||
shares: number // dynamic parimutuel pool weight; negative if SELL bet
|
shares: number // dynamic parimutuel pool weight; negative if SELL bet
|
||||||
|
|
||||||
|
@ -21,3 +22,5 @@ export type Bet = {
|
||||||
|
|
||||||
createdTime: number
|
createdTime: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const MAX_LOAN_PER_CONTRACT = 20
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { Bet } from './bet'
|
import * as _ from 'lodash'
|
||||||
|
import { Bet, MAX_LOAN_PER_CONTRACT } from './bet'
|
||||||
import {
|
import {
|
||||||
calculateShares,
|
calculateShares,
|
||||||
getProbability,
|
getProbability,
|
||||||
|
@ -11,6 +12,7 @@ export const getNewBinaryBetInfo = (
|
||||||
user: User,
|
user: User,
|
||||||
outcome: 'YES' | 'NO',
|
outcome: 'YES' | 'NO',
|
||||||
amount: number,
|
amount: number,
|
||||||
|
loanAmount: number,
|
||||||
contract: Contract,
|
contract: Contract,
|
||||||
newBetId: string
|
newBetId: string
|
||||||
) => {
|
) => {
|
||||||
|
@ -45,6 +47,7 @@ export const getNewBinaryBetInfo = (
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
contractId: contract.id,
|
contractId: contract.id,
|
||||||
amount,
|
amount,
|
||||||
|
loanAmount,
|
||||||
shares,
|
shares,
|
||||||
outcome,
|
outcome,
|
||||||
probBefore,
|
probBefore,
|
||||||
|
@ -52,7 +55,7 @@ export const getNewBinaryBetInfo = (
|
||||||
createdTime: Date.now(),
|
createdTime: Date.now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
const newBalance = user.balance - amount
|
const newBalance = user.balance - (amount - loanAmount)
|
||||||
|
|
||||||
return { newBet, newPool, newTotalShares, newTotalBets, newBalance }
|
return { newBet, newPool, newTotalShares, newTotalBets, newBalance }
|
||||||
}
|
}
|
||||||
|
@ -61,6 +64,7 @@ export const getNewMultiBetInfo = (
|
||||||
user: User,
|
user: User,
|
||||||
outcome: string,
|
outcome: string,
|
||||||
amount: number,
|
amount: number,
|
||||||
|
loanAmount: number,
|
||||||
contract: Contract,
|
contract: Contract,
|
||||||
newBetId: string
|
newBetId: string
|
||||||
) => {
|
) => {
|
||||||
|
@ -85,6 +89,7 @@ export const getNewMultiBetInfo = (
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
contractId: contract.id,
|
contractId: contract.id,
|
||||||
amount,
|
amount,
|
||||||
|
loanAmount,
|
||||||
shares,
|
shares,
|
||||||
outcome,
|
outcome,
|
||||||
probBefore,
|
probBefore,
|
||||||
|
@ -92,7 +97,17 @@ export const getNewMultiBetInfo = (
|
||||||
createdTime: Date.now(),
|
createdTime: Date.now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
const newBalance = user.balance - amount
|
const newBalance = user.balance - (amount - loanAmount)
|
||||||
|
|
||||||
return { newBet, newPool, newTotalShares, newTotalBets, newBalance }
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -161,3 +161,12 @@ export const getPayoutsMultiOutcome = (
|
||||||
.map(({ userId, payout }) => ({ userId, payout }))
|
.map(({ userId, payout }) => ({ userId, payout }))
|
||||||
.concat([{ userId: contract.creatorId, payout: creatorPayout }]) // add creator fee
|
.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 }))
|
||||||
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ export const getSellBetInfo = (
|
||||||
newBetId: string
|
newBetId: string
|
||||||
) => {
|
) => {
|
||||||
const { pool, totalShares, totalBets } = contract
|
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)
|
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 {
|
return {
|
||||||
newBet,
|
newBet,
|
||||||
|
|
|
@ -3,10 +3,11 @@ import * as admin from 'firebase-admin'
|
||||||
|
|
||||||
import { Contract } from '../../common/contract'
|
import { Contract } from '../../common/contract'
|
||||||
import { User } from '../../common/user'
|
import { User } from '../../common/user'
|
||||||
import { getNewMultiBetInfo } from '../../common/new-bet'
|
import { getLoanAmount, getNewMultiBetInfo } from '../../common/new-bet'
|
||||||
import { Answer } from '../../common/answer'
|
import { Answer } from '../../common/answer'
|
||||||
import { getContract, getValues } from './utils'
|
import { getContract, getValues } from './utils'
|
||||||
import { sendNewAnswerEmail } from './emails'
|
import { sendNewAnswerEmail } from './emails'
|
||||||
|
import { Bet } from '../../common/bet'
|
||||||
|
|
||||||
export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
|
export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
async (
|
async (
|
||||||
|
@ -55,6 +56,11 @@ export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
if (closeTime && Date.now() > closeTime)
|
if (closeTime && Date.now() > closeTime)
|
||||||
return { status: 'error', message: 'Trading is closed' }
|
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>(
|
const [lastAnswer] = await getValues<Answer>(
|
||||||
firestore
|
firestore
|
||||||
.collection(`contracts/${contractId}/answers`)
|
.collection(`contracts/${contractId}/answers`)
|
||||||
|
@ -92,8 +98,17 @@ export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
.collection(`contracts/${contractId}/bets`)
|
.collection(`contracts/${contractId}/bets`)
|
||||||
.doc()
|
.doc()
|
||||||
|
|
||||||
|
const loanAmount = getLoanAmount(yourBets, amount)
|
||||||
|
|
||||||
const { newBet, newPool, newTotalShares, newTotalBets, newBalance } =
|
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.create(newBetDoc, newBet)
|
||||||
transaction.update(contractDoc, {
|
transaction.update(contractDoc, {
|
||||||
|
|
|
@ -3,7 +3,13 @@ import * as admin from 'firebase-admin'
|
||||||
|
|
||||||
import { Contract } from '../../common/contract'
|
import { Contract } from '../../common/contract'
|
||||||
import { User } from '../../common/user'
|
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(
|
export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
async (
|
async (
|
||||||
|
@ -46,6 +52,11 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
if (closeTime && Date.now() > closeTime)
|
if (closeTime && Date.now() > closeTime)
|
||||||
return { status: 'error', message: 'Trading is closed' }
|
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') {
|
if (outcomeType === 'FREE_RESPONSE') {
|
||||||
const answerSnap = await transaction.get(
|
const answerSnap = await transaction.get(
|
||||||
contractDoc.collection('answers').doc(outcome)
|
contractDoc.collection('answers').doc(outcome)
|
||||||
|
@ -58,16 +69,26 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
.collection(`contracts/${contractId}/bets`)
|
.collection(`contracts/${contractId}/bets`)
|
||||||
.doc()
|
.doc()
|
||||||
|
|
||||||
|
const loanAmount = getLoanAmount(yourBets, amount)
|
||||||
|
|
||||||
const { newBet, newPool, newTotalShares, newTotalBets, newBalance } =
|
const { newBet, newPool, newTotalShares, newTotalBets, newBalance } =
|
||||||
outcomeType === 'BINARY'
|
outcomeType === 'BINARY'
|
||||||
? getNewBinaryBetInfo(
|
? getNewBinaryBetInfo(
|
||||||
user,
|
user,
|
||||||
outcome as 'YES' | 'NO',
|
outcome as 'YES' | 'NO',
|
||||||
amount,
|
amount,
|
||||||
|
loanAmount,
|
||||||
|
contract,
|
||||||
|
newBetDoc.id
|
||||||
|
)
|
||||||
|
: getNewMultiBetInfo(
|
||||||
|
user,
|
||||||
|
outcome,
|
||||||
|
amount,
|
||||||
|
loanAmount,
|
||||||
contract,
|
contract,
|
||||||
newBetDoc.id
|
newBetDoc.id
|
||||||
)
|
)
|
||||||
: getNewMultiBetInfo(user, outcome, amount, contract, newBetDoc.id)
|
|
||||||
|
|
||||||
transaction.create(newBetDoc, newBet)
|
transaction.create(newBetDoc, newBet)
|
||||||
transaction.update(contractDoc, {
|
transaction.update(contractDoc, {
|
||||||
|
|
|
@ -7,7 +7,11 @@ import { User } from '../../common/user'
|
||||||
import { Bet } from '../../common/bet'
|
import { Bet } from '../../common/bet'
|
||||||
import { getUser, payUser } from './utils'
|
import { getUser, payUser } from './utils'
|
||||||
import { sendMarketResolutionEmail } from './emails'
|
import { sendMarketResolutionEmail } from './emails'
|
||||||
import { getPayouts, getPayoutsMultiOutcome } from '../../common/payouts'
|
import {
|
||||||
|
getLoanPayouts,
|
||||||
|
getPayouts,
|
||||||
|
getPayoutsMultiOutcome,
|
||||||
|
} from '../../common/payouts'
|
||||||
import { removeUndefinedProps } from '../../common/util/object'
|
import { removeUndefinedProps } from '../../common/util/object'
|
||||||
|
|
||||||
export const resolveMarket = functions
|
export const resolveMarket = functions
|
||||||
|
@ -99,13 +103,23 @@ export const resolveMarket = functions
|
||||||
? getPayoutsMultiOutcome(resolutions, contract, openBets)
|
? getPayoutsMultiOutcome(resolutions, contract, openBets)
|
||||||
: getPayouts(outcome, contract, openBets, resolutionProbability)
|
: getPayouts(outcome, contract, openBets, resolutionProbability)
|
||||||
|
|
||||||
|
const loanPayouts = getLoanPayouts(openBets)
|
||||||
|
|
||||||
console.log('payouts:', payouts)
|
console.log('payouts:', payouts)
|
||||||
|
|
||||||
const groups = _.groupBy(payouts, (payout) => payout.userId)
|
const groups = _.groupBy(
|
||||||
|
[...payouts, ...loanPayouts],
|
||||||
|
(payout) => payout.userId
|
||||||
|
)
|
||||||
const userPayouts = _.mapValues(groups, (group) =>
|
const userPayouts = _.mapValues(groups, (group) =>
|
||||||
_.sumBy(group, (g) => g.payout)
|
_.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(
|
const payoutPromises = Object.entries(userPayouts).map(
|
||||||
([userId, payout]) => payUser(userId, payout)
|
([userId, payout]) => payUser(userId, payout)
|
||||||
)
|
)
|
||||||
|
@ -116,7 +130,7 @@ export const resolveMarket = functions
|
||||||
|
|
||||||
await sendResolutionEmails(
|
await sendResolutionEmails(
|
||||||
openBets,
|
openBets,
|
||||||
userPayouts,
|
userPayoutsWithoutLoans,
|
||||||
creator,
|
creator,
|
||||||
contract,
|
contract,
|
||||||
outcome,
|
outcome,
|
||||||
|
|
|
@ -1,15 +1,20 @@
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
|
import _ from 'lodash'
|
||||||
import { useUser } from '../hooks/use-user'
|
import { useUser } from '../hooks/use-user'
|
||||||
import { formatMoney } from '../../common/util/format'
|
import { formatMoney } from '../../common/util/format'
|
||||||
import { AddFundsButton } from './add-funds-button'
|
|
||||||
import { Col } from './layout/col'
|
import { Col } from './layout/col'
|
||||||
import { Row } from './layout/row'
|
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: {
|
export function AmountInput(props: {
|
||||||
amount: number | undefined
|
amount: number | undefined
|
||||||
onChange: (newAmount: number | undefined) => void
|
onChange: (newAmount: number | undefined) => void
|
||||||
error: string | undefined
|
error: string | undefined
|
||||||
setError: (error: string | undefined) => void
|
setError: (error: string | undefined) => void
|
||||||
|
contractId: string | undefined
|
||||||
minimumAmount?: number
|
minimumAmount?: number
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
className?: string
|
className?: string
|
||||||
|
@ -22,6 +27,7 @@ export function AmountInput(props: {
|
||||||
onChange,
|
onChange,
|
||||||
error,
|
error,
|
||||||
setError,
|
setError,
|
||||||
|
contractId,
|
||||||
disabled,
|
disabled,
|
||||||
className,
|
className,
|
||||||
inputClassName,
|
inputClassName,
|
||||||
|
@ -31,10 +37,24 @@ export function AmountInput(props: {
|
||||||
|
|
||||||
const user = useUser()
|
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) => {
|
const onAmountChange = (str: string) => {
|
||||||
|
if (str.includes('-')) {
|
||||||
|
onChange(undefined)
|
||||||
|
return
|
||||||
|
}
|
||||||
const amount = parseInt(str.replace(/[^\d]/, ''))
|
const amount = parseInt(str.replace(/[^\d]/, ''))
|
||||||
|
|
||||||
if (str && isNaN(amount)) return
|
if (str && isNaN(amount)) return
|
||||||
|
if (amount >= 10 ** 9) return
|
||||||
|
|
||||||
onChange(str ? amount : undefined)
|
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 (
|
return (
|
||||||
<Col className={className}>
|
<Col className={className}>
|
||||||
|
@ -68,19 +89,34 @@ export function AmountInput(props: {
|
||||||
onChange={(e) => onAmountChange(e.target.value)}
|
onChange={(e) => onAmountChange(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<Spacer h={4} />
|
||||||
|
|
||||||
{error && (
|
{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}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{user && (
|
{user && (
|
||||||
<Col className="mt-3 text-sm">
|
<Col className="gap-3 text-sm">
|
||||||
<div className="mb-2 whitespace-nowrap text-gray-500">
|
{contractId && (
|
||||||
Remaining balance
|
<Row className="items-center justify-between gap-2 text-gray-500">
|
||||||
</div>
|
<Row className="items-center gap-2">
|
||||||
<Row className="gap-4">
|
Amount loaned{' '}
|
||||||
<div>{formatMoney(Math.floor(remainingBalance))}</div>
|
<InfoTooltip
|
||||||
{user.balance !== 1000 && <AddFundsButton />}
|
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>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -30,8 +30,9 @@ export function AnswerBetPanel(props: {
|
||||||
answer: Answer
|
answer: Answer
|
||||||
contract: Contract
|
contract: Contract
|
||||||
closePanel: () => void
|
closePanel: () => void
|
||||||
|
className?: string
|
||||||
}) {
|
}) {
|
||||||
const { answer, contract, closePanel } = props
|
const { answer, contract, closePanel, className } = props
|
||||||
const { id: answerId } = answer
|
const { id: answerId } = answer
|
||||||
|
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
@ -97,7 +98,7 @@ export function AnswerBetPanel(props: {
|
||||||
const currentReturnPercent = (currentReturn * 100).toFixed() + '%'
|
const currentReturnPercent = (currentReturn * 100).toFixed() + '%'
|
||||||
|
|
||||||
return (
|
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">
|
<Row className="self-stretch items-center justify-between">
|
||||||
<div className="text-xl">Buy this answer</div>
|
<div className="text-xl">Buy this answer</div>
|
||||||
|
|
||||||
|
@ -114,40 +115,44 @@ export function AnswerBetPanel(props: {
|
||||||
setError={setError}
|
setError={setError}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
inputRef={inputRef}
|
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} />
|
<Row className="justify-between items-start text-sm gap-2">
|
||||||
|
<Row className="flex-nowrap whitespace-nowrap items-center gap-2 text-gray-500">
|
||||||
<div className="mt-2 mb-1 text-sm text-gray-500">Implied probability</div>
|
<div>Payout if chosen</div>
|
||||||
<Row>
|
<InfoTooltip
|
||||||
<div>{formatPercent(initialProb)}</div>
|
text={`Current payout for ${formatWithCommas(
|
||||||
<div className="mx-2">→</div>
|
shares
|
||||||
<div>{formatPercent(resultProb)}</div>
|
)} / ${formatWithCommas(
|
||||||
</Row>
|
shares + contract.totalShares[answerId]
|
||||||
|
)} shares`}
|
||||||
<Spacer h={4} />
|
/>
|
||||||
|
</Row>
|
||||||
<Row className="mt-2 mb-1 items-center gap-2 text-sm text-gray-500">
|
<Row className="flex-wrap justify-end items-end gap-2">
|
||||||
Payout if chosen
|
<span className="whitespace-nowrap">
|
||||||
<InfoTooltip
|
{formatMoney(currentPayout)}
|
||||||
text={`Current payout for ${formatWithCommas(
|
</span>
|
||||||
shares
|
<span>(+{currentReturnPercent})</span>
|
||||||
)} / ${formatWithCommas(
|
</Row>
|
||||||
shares + contract.totalShares[answerId]
|
</Row>
|
||||||
)} shares`}
|
</Col>
|
||||||
/>
|
|
||||||
</Row>
|
|
||||||
<div>
|
|
||||||
{formatMoney(currentPayout)}
|
|
||||||
<span>(+{currentReturnPercent})</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Spacer h={6} />
|
<Spacer h={6} />
|
||||||
|
|
||||||
{user ? (
|
{user ? (
|
||||||
<button
|
<button
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'btn',
|
'btn self-stretch',
|
||||||
betDisabled ? 'btn-disabled' : 'btn-primary',
|
betDisabled ? 'btn-disabled' : 'btn-primary',
|
||||||
isSubmitting ? 'loading' : ''
|
isSubmitting ? 'loading' : ''
|
||||||
)}
|
)}
|
||||||
|
@ -157,7 +162,7 @@ export function AnswerBetPanel(props: {
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<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}
|
onClick={firebaseLogin}
|
||||||
>
|
>
|
||||||
Sign in to trade!
|
Sign in to trade!
|
||||||
|
|
|
@ -97,6 +97,7 @@ export function AnswerItem(props: {
|
||||||
answer={answer}
|
answer={answer}
|
||||||
contract={contract}
|
contract={contract}
|
||||||
closePanel={() => setIsBetting(false)}
|
closePanel={() => setIsBetting(false)}
|
||||||
|
className="sm:w-72"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Row className="items-center justify-end gap-4 self-end sm:self-start">
|
<Row className="items-center justify-end gap-4 self-end sm:self-start">
|
||||||
|
|
|
@ -86,7 +86,7 @@ export function CreateAnswerPanel(props: { contract: Contract }) {
|
||||||
<div />
|
<div />
|
||||||
<Col
|
<Col
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'sm:flex-row gap-4',
|
'sm:flex-row sm:items-end gap-4',
|
||||||
text ? 'justify-between' : 'self-end'
|
text ? 'justify-between' : 'self-end'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
@ -101,34 +101,42 @@ export function CreateAnswerPanel(props: { contract: Contract }) {
|
||||||
setError={setAmountError}
|
setError={setAmountError}
|
||||||
minimumAmount={1}
|
minimumAmount={1}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
|
contractId={contract.id}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col className="gap-2 mt-1">
|
<Col className="gap-3">
|
||||||
<div className="text-sm text-gray-500">Implied probability</div>
|
<Row className="justify-between items-center text-sm">
|
||||||
<Row>
|
<div className="text-gray-500">Probability</div>
|
||||||
<div>{formatPercent(0)}</div>
|
<Row>
|
||||||
<div className="mx-2">→</div>
|
<div>{formatPercent(0)}</div>
|
||||||
<div>{formatPercent(resultProb)}</div>
|
<div className="mx-2">→</div>
|
||||||
|
<div>{formatPercent(resultProb)}</div>
|
||||||
|
</Row>
|
||||||
</Row>
|
</Row>
|
||||||
<Row className="mt-2 mb-1 items-center gap-2 text-sm text-gray-500">
|
|
||||||
Payout if chosen
|
<Row className="justify-between text-sm gap-2">
|
||||||
<InfoTooltip
|
<Row className="flex-nowrap whitespace-nowrap items-center gap-2 text-gray-500">
|
||||||
text={`Current payout for ${formatWithCommas(
|
<div>Payout if chosen</div>
|
||||||
shares
|
<InfoTooltip
|
||||||
)} / ${formatWithCommas(shares)} shares`}
|
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>
|
</Row>
|
||||||
<div>
|
|
||||||
{formatMoney(currentPayout)}
|
|
||||||
<span>(+{currentReturnPercent})</span>
|
|
||||||
</div>
|
|
||||||
</Col>
|
</Col>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{user ? (
|
{user ? (
|
||||||
<button
|
<button
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'btn self-end mt-2',
|
'btn mt-2',
|
||||||
canSubmit ? 'btn-outline' : 'btn-disabled',
|
canSubmit ? 'btn-outline' : 'btn-disabled',
|
||||||
isSubmitting && 'loading'
|
isSubmitting && 'loading'
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -144,7 +144,6 @@ export function BetPanel(props: {
|
||||||
text={panelTitle}
|
text={panelTitle}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* <div className="mt-2 mb-1 text-sm text-gray-500">Outcome</div> */}
|
|
||||||
<YesNoSelector
|
<YesNoSelector
|
||||||
className="mb-4"
|
className="mb-4"
|
||||||
selected={betChoice}
|
selected={betChoice}
|
||||||
|
@ -160,45 +159,49 @@ export function BetPanel(props: {
|
||||||
setError={setError}
|
setError={setError}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
inputRef={inputRef}
|
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 className="justify-between items-start text-sm gap-2">
|
||||||
<Row>
|
<Row className="flex-nowrap whitespace-nowrap items-center gap-2 text-gray-500">
|
||||||
<div>{formatPercent(initialProb)}</div>
|
<div>
|
||||||
<div className="mx-2">→</div>
|
Payout if <OutcomeLabel outcome={betChoice ?? 'YES'} />
|
||||||
<div>{formatPercent(resultProb)}</div>
|
</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} />
|
|
||||||
<InfoTooltip
|
<InfoTooltip
|
||||||
text={`Current payout for ${formatWithCommas(
|
text={`Current payout for ${formatWithCommas(
|
||||||
shares
|
shares
|
||||||
)} / ${formatWithCommas(
|
)} / ${formatWithCommas(
|
||||||
shares +
|
shares +
|
||||||
totalShares[betChoice] -
|
totalShares[betChoice ?? 'YES'] -
|
||||||
(phantomShares ? phantomShares[betChoice] : 0)
|
(phantomShares ? phantomShares[betChoice ?? 'YES'] : 0)
|
||||||
)} ${betChoice} shares`}
|
)} ${betChoice} shares`}
|
||||||
/>
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
<div>
|
<Row className="flex-wrap justify-end items-end gap-2">
|
||||||
{formatMoney(currentPayout)}
|
<span className="whitespace-nowrap">
|
||||||
<span>(+{currentReturnPercent})</span>
|
{formatMoney(currentPayout)}
|
||||||
</div>
|
</span>
|
||||||
</>
|
<span>(+{currentReturnPercent})</span>
|
||||||
)}
|
</Row>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
|
||||||
<Spacer h={6} />
|
<Spacer h={8} />
|
||||||
|
|
||||||
{user && (
|
{user && (
|
||||||
<button
|
<button
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'btn',
|
'btn flex-1',
|
||||||
betDisabled
|
betDisabled
|
||||||
? 'btn-disabled'
|
? 'btn-disabled'
|
||||||
: betChoice === 'YES'
|
: betChoice === 'YES'
|
||||||
|
@ -213,7 +216,7 @@ export function BetPanel(props: {
|
||||||
)}
|
)}
|
||||||
{user === null && (
|
{user === null && (
|
||||||
<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 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}
|
onClick={firebaseLogin}
|
||||||
>
|
>
|
||||||
Sign in to trade!
|
Sign in to trade!
|
||||||
|
|
|
@ -82,7 +82,8 @@ export function BetsList(props: { user: User }) {
|
||||||
if (bet.isSold || bet.sale) return 0
|
if (bet.isSold || bet.sale) return 0
|
||||||
|
|
||||||
const contract = contracts.find((c) => c.id === contractId)
|
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>
|
<Col>
|
||||||
<div className="text-sm text-gray-500">Invested</div>
|
<div className="text-sm text-gray-500">Invested</div>
|
||||||
<div className="text-lg">
|
<div className="text-lg">
|
||||||
{formatMoney(currentBetsValue)}{' '}
|
{formatMoney(currentInvestment)}{' '}
|
||||||
<ProfitBadge profitPercent={investedProfit} />
|
<ProfitBadge profitPercent={investedProfit} />
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
|
@ -457,6 +458,7 @@ function BetRow(props: { bet: Bet; contract: Contract; saleBet?: Bet }) {
|
||||||
shares,
|
shares,
|
||||||
isSold,
|
isSold,
|
||||||
isAnte,
|
isAnte,
|
||||||
|
loanAmount,
|
||||||
} = bet
|
} = bet
|
||||||
|
|
||||||
const { isResolved, closeTime } = contract
|
const { isResolved, closeTime } = contract
|
||||||
|
@ -464,7 +466,7 @@ function BetRow(props: { bet: Bet; contract: Contract; saleBet?: Bet }) {
|
||||||
|
|
||||||
const saleAmount = saleBet?.sale?.amount
|
const saleAmount = saleBet?.sale?.amount
|
||||||
|
|
||||||
const saleDisplay = bet.isAnte ? (
|
const saleDisplay = isAnte ? (
|
||||||
'ANTE'
|
'ANTE'
|
||||||
) : saleAmount !== undefined ? (
|
) : saleAmount !== undefined ? (
|
||||||
<>{formatMoney(saleAmount)} (sold)</>
|
<>{formatMoney(saleAmount)} (sold)</>
|
||||||
|
@ -491,7 +493,10 @@ function BetRow(props: { bet: Bet; contract: Contract; saleBet?: Bet }) {
|
||||||
<td>
|
<td>
|
||||||
<OutcomeLabel outcome={outcome} />
|
<OutcomeLabel outcome={outcome} />
|
||||||
</td>
|
</td>
|
||||||
<td>{formatMoney(amount)}</td>
|
<td>
|
||||||
|
{formatMoney(amount)}
|
||||||
|
{loanAmount ? ` (${formatMoney(loanAmount ?? 0)} loan)` : ''}
|
||||||
|
</td>
|
||||||
<td>{saleDisplay}</td>
|
<td>{saleDisplay}</td>
|
||||||
{!isResolved && <td>{payoutIfChosenDisplay}</td>}
|
{!isResolved && <td>{payoutIfChosenDisplay}</td>}
|
||||||
<td>
|
<td>
|
||||||
|
@ -510,18 +515,19 @@ function SellButton(props: { contract: Contract; bet: Bet }) {
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const { contract, bet } = props
|
const { contract, bet } = props
|
||||||
const isBinary = contract.outcomeType === 'BINARY'
|
const { outcome, shares, loanAmount } = bet
|
||||||
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
|
||||||
const initialProb = getOutcomeProbability(
|
const initialProb = getOutcomeProbability(
|
||||||
contract.totalShares,
|
contract.totalShares,
|
||||||
bet.outcome === 'NO' ? 'YES' : bet.outcome
|
outcome === 'NO' ? 'YES' : outcome
|
||||||
)
|
)
|
||||||
|
|
||||||
const outcomeProb = getProbabilityAfterSale(
|
const outcomeProb = getProbabilityAfterSale(
|
||||||
contract.totalShares,
|
contract.totalShares,
|
||||||
bet.outcome,
|
outcome,
|
||||||
bet.shares
|
shares
|
||||||
)
|
)
|
||||||
|
|
||||||
const saleAmount = calculateSaleAmount(contract, bet)
|
const saleAmount = calculateSaleAmount(contract, bet)
|
||||||
|
@ -533,7 +539,7 @@ function SellButton(props: { contract: Contract; bet: Bet }) {
|
||||||
className: clsx('btn-sm', isSubmitting && 'btn-disabled loading'),
|
className: clsx('btn-sm', isSubmitting && 'btn-disabled loading'),
|
||||||
label: 'Sell',
|
label: 'Sell',
|
||||||
}}
|
}}
|
||||||
submitBtn={{ className: 'btn-primary' }}
|
submitBtn={{ className: 'btn-primary', label: 'Sell' }}
|
||||||
onSubmit={async () => {
|
onSubmit={async () => {
|
||||||
setIsSubmitting(true)
|
setIsSubmitting(true)
|
||||||
await sellBet({ contractId: contract.id, betId: bet.id })
|
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">
|
<div className="mb-4 text-2xl">
|
||||||
Sell <OutcomeLabel outcome={bet.outcome} />
|
Sell {formatWithCommas(shares)} shares of{' '}
|
||||||
</div>
|
<OutcomeLabel outcome={outcome} /> for {formatMoney(saleAmount)}?
|
||||||
<div>
|
|
||||||
Do you want to sell {formatWithCommas(bet.shares)} shares of{' '}
|
|
||||||
<OutcomeLabel outcome={bet.outcome} /> for {formatMoney(saleAmount)}?
|
|
||||||
</div>
|
</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">
|
<div className="mt-2 mb-1 text-sm">
|
||||||
({isBinary ? 'Updated' : <OutcomeLabel outcome={bet.outcome} />}{' '}
|
Market probability: {formatPercent(initialProb)} →{' '}
|
||||||
probability: {formatPercent(initialProb)} → {formatPercent(outcomeProb)}
|
{formatPercent(outcomeProb)}
|
||||||
)
|
|
||||||
</div>
|
</div>
|
||||||
</ConfirmationButton>
|
</ConfirmationButton>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import { useEffect, useState } from 'react'
|
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) => {
|
export const useUserBets = (userId: string | undefined) => {
|
||||||
const [bets, setBets] = useState<Bet[] | undefined>(undefined)
|
const [bets, setBets] = useState<Bet[] | undefined>(undefined)
|
||||||
|
@ -12,6 +16,20 @@ export const useUserBets = (userId: string | undefined) => {
|
||||||
return bets
|
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) => {
|
export const useUserBetContracts = (userId: string | undefined) => {
|
||||||
const [contractIds, setContractIds] = useState<string[] | undefined>()
|
const [contractIds, setContractIds] = useState<string[] | undefined>()
|
||||||
|
|
||||||
|
|
|
@ -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[]) {
|
export function withoutAnteBets(contract: Contract, bets?: Bet[]) {
|
||||||
const { createdTime } = contract
|
const { createdTime } = contract
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { db } from './init'
|
import { db } from './init'
|
||||||
import {
|
import {
|
||||||
doc,
|
|
||||||
getDoc,
|
getDoc,
|
||||||
getDocs,
|
getDocs,
|
||||||
onSnapshot,
|
onSnapshot,
|
||||||
|
@ -22,7 +21,9 @@ export function listenForValue<T>(
|
||||||
docRef: DocumentReference,
|
docRef: DocumentReference,
|
||||||
setValue: (value: T | null) => void
|
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
|
if (snapshot.metadata.fromCache) return
|
||||||
|
|
||||||
const value = snapshot.exists() ? (snapshot.data() as T) : null
|
const value = snapshot.exists() ? (snapshot.data() as T) : null
|
||||||
|
@ -34,7 +35,9 @@ export function listenForValues<T>(
|
||||||
query: Query,
|
query: Query,
|
||||||
setValues: (values: T[]) => void
|
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
|
if (snapshot.metadata.fromCache) return
|
||||||
|
|
||||||
const values = snapshot.docs.map((doc) => doc.data() as T)
|
const values = snapshot.docs.map((doc) => doc.data() as T)
|
||||||
|
|
|
@ -145,7 +145,7 @@ export default function ContractPage(props: {
|
||||||
|
|
||||||
<Col className="flex-1">
|
<Col className="flex-1">
|
||||||
{allowTrade && (
|
{allowTrade && (
|
||||||
<BetPanel className="hidden lg:inline" contract={contract} />
|
<BetPanel className="hidden lg:flex" contract={contract} />
|
||||||
)}
|
)}
|
||||||
{allowResolve && (
|
{allowResolve && (
|
||||||
<ResolutionPanel creator={user} contract={contract} />
|
<ResolutionPanel creator={user} contract={contract} />
|
||||||
|
|
|
@ -248,6 +248,7 @@ export function NewContract(props: { question: string; tag?: string }) {
|
||||||
error={anteError}
|
error={anteError}
|
||||||
setError={setAnteError}
|
setError={setAnteError}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
|
contractId={undefined}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -245,6 +245,7 @@ ${TEST_VALUE}
|
||||||
error={anteError}
|
error={anteError}
|
||||||
setError={setAnteError}
|
setError={setAnteError}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
|
contractId={undefined}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user