diff --git a/common/bet.ts b/common/bet.ts index 49fdf67e..8851e797 100644 --- a/common/bet.ts +++ b/common/bet.ts @@ -1,4 +1,3 @@ -import { Truthy } from 'lodash' import { Fees } from './fees' export type Bet = { @@ -27,17 +26,10 @@ export type Bet = { isAnte?: boolean isLiquidityProvision?: boolean isRedemption?: boolean - // A record of each transaction that partially (or fully) fills the bet amount. // I.e. A limit order could be filled by partially matching with several bets. // Non-limit orders can also be filled by matching with multiple limit orders. - fills?: { - // The id the bet matched against, or null if the bet was matched by - // the pool. - matchedBetId: string | null - amount: number - shares: number - }[] + fills?: fill[] } export type NumericBet = Bet & { @@ -48,10 +40,19 @@ export type NumericBet = Bet & { // Binary market limit order. export type LimitBet = Bet & { + orderAmount: number // Amount of limit order. limitProb: number // [0, 1]. Bet to this probability. isFilled: boolean // Whether all of the bet amount has been filled. isCancelled: boolean // Whether to prevent any further fills. - fills: Truthy + fills: fill[] +} + +export type fill = { + // The id the bet matched against, or null if the bet was matched by the pool. + matchedBetId: string | null + amount: number + shares: number + timestamp: number } export const MAX_LOAN_PER_CONTRACT = 20 diff --git a/common/new-bet.ts b/common/new-bet.ts index c35d5bed..c4d946bf 100644 --- a/common/new-bet.ts +++ b/common/new-bet.ts @@ -1,6 +1,6 @@ import { sortBy, sumBy } from 'lodash' -import { Bet, LimitBet, MAX_LOAN_PER_CONTRACT, NumericBet } from './bet' +import { Bet, fill, LimitBet, MAX_LOAN_PER_CONTRACT, NumericBet } from './bet' import { calculateDpmShares, getDpmProbability, @@ -62,6 +62,8 @@ const computeFill = ( return undefined } + const timestamp = Date.now() + if ( !matchedBet || (outcome === 'YES' @@ -103,18 +105,19 @@ const computeFill = ( amount: poolAmount, state: newState, fees, + timestamp, }, taker: { matchedBetId: null, shares, amount: poolAmount, + timestamp, }, } } // Fill from matchedBet. - const matchRemaining = - matchedBet.amount - sumBy(matchedBet.fills, (fill) => fill.amount) + const matchRemaining = matchedBet.orderAmount - matchedBet.amount const shares = Math.min( amount / (outcome === 'YES' ? matchedBet.limitProb : 1 - matchedBet.limitProb), @@ -138,6 +141,7 @@ const computeFill = ( shares * (outcome === 'YES' ? 1 - matchedBet.limitProb : matchedBet.limitProb), shares, + timestamp, } const taker = { matchedBetId: matchedBet.id, @@ -145,6 +149,7 @@ const computeFill = ( shares * (outcome === 'YES' ? matchedBet.limitProb : 1 - matchedBet.limitProb), shares, + timestamp, } return { maker, taker } } @@ -164,12 +169,13 @@ export const getBinaryCpmmBetInfo = ( console.log({ outcome, betAmount, limitProb, sortedBets }) - const takers: { - matchedBetId: string | null + const takers: fill[] = [] + const makers: { + bet: LimitBet amount: number shares: number + timestamp: number }[] = [] - const makers: { bet: LimitBet; amount: number; shares: number }[] = [] let amount = betAmount let cpmmState = { pool: contract.pool, p: contract.p } @@ -210,13 +216,14 @@ export const getBinaryCpmmBetInfo = ( const isFilled = floatingEqual(betAmount, takerAmount) const newBet: CandidateBet = removeUndefinedProps({ - amount: betAmount, + orderAmount: betAmount, + amount: takerAmount, + shares: takerShares, limitProb, isFilled, isCancelled: false, fills: takers, contractId: contract.id, - shares: takerShares, outcome, probBefore, probAfter, diff --git a/functions/src/place-bet.ts b/functions/src/place-bet.ts index 88601a0d..9cc47dda 100644 --- a/functions/src/place-bet.ts +++ b/functions/src/place-bet.ts @@ -1,7 +1,7 @@ import * as admin from 'firebase-admin' import { z } from 'zod' -import { Query } from 'firebase-admin/firestore' -import { sumBy } from 'lodash' +import { FieldValue, Query } from 'firebase-admin/firestore' +import { groupBy, mapValues, sumBy } from 'lodash' import { APIError, newEndpoint, validate } from './api' import { Contract, CPMM_MIN_POOL_QTY } from '../../common/contract' @@ -74,6 +74,7 @@ export const placebet = newEndpoint({}, async (req, auth) => { bet: LimitBet amount: number shares: number + timestamp: number }[] } > => { @@ -128,29 +129,44 @@ export const placebet = newEndpoint({}, async (req, auth) => { throw new APIError(400, 'Bet too large for current liquidity pool.') } - const newBalance = user.balance - amount - loanAmount const betDoc = contractDoc.collection('bets').doc() trans.create(betDoc, { id: betDoc.id, userId: user.id, ...newBet }) log('Created new bet document.') if (makers) { - for (const maker of makers) { - const { bet, amount, shares } = maker - const newFill = { amount, shares, matchedBetId: betDoc.id } - const fills = [...bet.fills, newFill] + const makersByBet = groupBy(makers, (maker) => maker.bet.id) + for (const makers of Object.values(makersByBet)) { + const bet = makers[0].bet + const newFills = makers.map((maker) => { + const { amount, shares, timestamp } = maker + return { amount, shares, matchedBetId: betDoc.id, timestamp } + }) + const fills = [...bet.fills, ...newFills] const totalShares = sumBy(fills, 'shares') - const isFilled = floatingEqual(sumBy(fills, 'amount'), bet.amount) + const totalAmount = sumBy(fills, 'amount') + const isFilled = floatingEqual(totalAmount, bet.orderAmount) log('Updated a matched limit bet.') trans.update(contractDoc.collection('bets').doc(bet.id), { fills, isFilled, + amount: totalAmount, shares: totalShares, }) } + + // Deduct balance of makers. + const spentByUser = mapValues( + groupBy(makers, (maker) => maker.bet.userId), + (makers) => sumBy(makers, (maker) => maker.amount) + ) + for (const [userId, spent] of Object.entries(spentByUser)) { + const userDoc = firestore.collection('users').doc(userId) + trans.update(userDoc, { balance: FieldValue.increment(-spent) }) + } } - trans.update(userDoc, { balance: newBalance }) + trans.update(userDoc, { balance: FieldValue.increment(-amount) }) log('Updated user balance.') trans.update( contractDoc, diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index 50fcbf8d..5179e6ed 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -24,10 +24,7 @@ import { sellShares } from 'web/lib/firebase/api-call' import { AmountInput, BuyAmountInput } from './amount-input' import { InfoTooltip } from './info-tooltip' import { BinaryOutcomeLabel, PseudoNumericOutcomeLabel } from './outcome-label' -import { - calculatePayoutAfterCorrectBet, - getProbability, -} from 'common/calculate' +import { getProbability } from 'common/calculate' import { useFocus } from 'web/hooks/use-focus' import { useUserContractBets } from 'web/hooks/use-user-bets' import { diff --git a/web/components/bet-row.tsx b/web/components/bet-row.tsx index fc6ab43b..fe7a2843 100644 --- a/web/components/bet-row.tsx +++ b/web/components/bet-row.tsx @@ -28,7 +28,6 @@ export default function BetRow(props: { contract, userBets ) - return ( <> diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx index b8fb7d31..c4dcda88 100644 --- a/web/components/bets-list.tsx +++ b/web/components/bets-list.tsx @@ -505,7 +505,7 @@ export function ContractBetsTable(props: { const { contract, className, isYourBets } = props const bets = sortBy( - props.bets.filter((b) => !b.isAnte), + props.bets.filter((b) => !b.isAnte && b.amount !== 0), (bet) => bet.createdTime ).reverse() diff --git a/web/components/feed/contract-activity.tsx b/web/components/feed/contract-activity.tsx index 8f728d39..c60afa70 100644 --- a/web/components/feed/contract-activity.tsx +++ b/web/components/feed/contract-activity.tsx @@ -31,7 +31,9 @@ export function ContractActivity(props: { const comments = updatedComments ?? props.comments const updatedBets = useBets(contract.id) - const bets = (updatedBets ?? props.bets).filter((bet) => !bet.isRedemption) + const bets = (updatedBets ?? props.bets).filter( + (bet) => !bet.isRedemption && bet.amount !== 0 + ) const items = getSpecificContractActivityItems( contract, bets, diff --git a/web/components/limit-bets.tsx b/web/components/limit-bets.tsx index 6b9eba96..3538cc4d 100644 --- a/web/components/limit-bets.tsx +++ b/web/components/limit-bets.tsx @@ -28,7 +28,7 @@ export function LimitBets(props: { bets: LimitBet[]; className?: string }) { function LimitBet(props: { bet: LimitBet }) { const { bet } = props - const filledAmount = sumBy(bet.fills, (fill) => fill.amount) + const { orderAmount, amount, limitProb, outcome } = bet const [isCancelling, setIsCancelling] = useState(false) const onCancel = () => { @@ -40,11 +40,11 @@ function LimitBet(props: { bet: LimitBet }) {
- +
- {formatMoney(bet.amount - filledAmount)} - {formatPercent(bet.limitProb)} + {formatMoney(orderAmount - amount)} + {formatPercent(limitProb)} {isCancelling ? (