Verify balance of limit order "makers" (#1007)

* Fetch balance of users with open limit orders & cancel orders with insufficient balance

* Fix imports

* Fix bugs

* Fix a bug

* Remove redundant cast

* buttons overlaying content fix (#1005)

* buttons overlaying content fix

* stats: round DAU number

* made set width for portfolio/profit fields (#1006)

* tournaments: included resolved markets

* made delete red, moved button for regular posts (#1008)

* Fix localstorage saved user being overwritten on every page load

* Market page: Show no right panel while user loading

* Don't flash sign in button if user is loading

* election map coloring

* market group modal scroll fix (#1009)

* midterms: posititoning, make mobile friendly

* Un-daisy share buttons (#1010)

* Make embed and challenge buttons non-daisyui

* Allow link Buttons. Change tweet, dupe buttons.

* lint

* don't insert extra lines when upload photos

* Map fixes (#1011)

* usa map: fix sizing

* useSetIframeBackbroundColor

* preload contracts

* seo

* remove hook

* turn off sprig on dev

* Render timestamp only on client to prevent error of server not matching client

* Make sized container have default height so graph doesn't jump

* midterms: use null in static props

* Create common card component (#1012)

* Create common card component

* lint

* add key prop to pills

* redirect to /home after login

* create market: use transaction

* card: reduce border size

* Update groupContracts in db trigger

* Default sort to best

* Save comment sort per user rather than per contract

* Refactor Pinned Items into a reusable component

* Revert "create market: use transaction"

This reverts commit e1f24f24a9.

* Mark @v with a (Bot) label

* fix padding on daily movers

* fix type errors

* Wrap sprig init in check for window

* unindex date-docs from search engines

* Auto-prettification

* compute elasticity

* change dpm elasticity

* Fix google lighthouse issues (#1013)

* don't hide free response panel on open resolve

* liquidity sort

* Limit order trade log: '/' to 'of'. Remove 'of' in 'of YES'.

* Date doc: Toggle to disable creating a prediction market

* Listen for date doc changes

* Fix merge error

* Don't cancel all a users limit orders if they go negative

Co-authored-by: ingawei <46611122+ingawei@users.noreply.github.com>
Co-authored-by: mantikoros <sgrugett@gmail.com>
Co-authored-by: Sinclair Chen <abc.sinclair@gmail.com>
Co-authored-by: mantikoros <95266179+mantikoros@users.noreply.github.com>
Co-authored-by: Ian Philips <iansphilips@gmail.com>
Co-authored-by: Pico2x <pico2x@gmail.com>
Co-authored-by: Austin Chen <akrolsmir@gmail.com>
Co-authored-by: sipec <sipec@users.noreply.github.com>
This commit is contained in:
James Grugett 2022-10-06 22:16:49 -05:00 committed by GitHub
parent 42a7d04b4d
commit f533d9bfcb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 256 additions and 97 deletions

View File

@ -147,7 +147,8 @@ function calculateAmountToBuyShares(
state: CpmmState, state: CpmmState,
shares: number, shares: number,
outcome: 'YES' | 'NO', outcome: 'YES' | 'NO',
unfilledBets: LimitBet[] unfilledBets: LimitBet[],
balanceByUserId: { [userId: string]: number }
) { ) {
// Search for amount between bounds (0, shares). // Search for amount between bounds (0, shares).
// Min share price is M$0, and max is M$1 each. // Min share price is M$0, and max is M$1 each.
@ -157,7 +158,8 @@ function calculateAmountToBuyShares(
amount, amount,
state, state,
undefined, undefined,
unfilledBets unfilledBets,
balanceByUserId
) )
const totalShares = sumBy(takers, (taker) => taker.shares) const totalShares = sumBy(takers, (taker) => taker.shares)
@ -169,7 +171,8 @@ export function calculateCpmmSale(
state: CpmmState, state: CpmmState,
shares: number, shares: number,
outcome: 'YES' | 'NO', outcome: 'YES' | 'NO',
unfilledBets: LimitBet[] unfilledBets: LimitBet[],
balanceByUserId: { [userId: string]: number }
) { ) {
if (Math.round(shares) < 0) { if (Math.round(shares) < 0) {
throw new Error('Cannot sell non-positive shares') throw new Error('Cannot sell non-positive shares')
@ -180,15 +183,17 @@ export function calculateCpmmSale(
state, state,
shares, shares,
oppositeOutcome, oppositeOutcome,
unfilledBets unfilledBets,
balanceByUserId
) )
const { cpmmState, makers, takers, totalFees } = computeFills( const { cpmmState, makers, takers, totalFees, ordersToCancel } = computeFills(
oppositeOutcome, oppositeOutcome,
buyAmount, buyAmount,
state, state,
undefined, undefined,
unfilledBets unfilledBets,
balanceByUserId
) )
// Transform buys of opposite outcome into sells. // Transform buys of opposite outcome into sells.
@ -211,6 +216,7 @@ export function calculateCpmmSale(
fees: totalFees, fees: totalFees,
makers, makers,
takers: saleTakers, takers: saleTakers,
ordersToCancel,
} }
} }
@ -218,9 +224,16 @@ export function getCpmmProbabilityAfterSale(
state: CpmmState, state: CpmmState,
shares: number, shares: number,
outcome: 'YES' | 'NO', outcome: 'YES' | 'NO',
unfilledBets: LimitBet[] unfilledBets: LimitBet[],
balanceByUserId: { [userId: string]: number }
) { ) {
const { cpmmState } = calculateCpmmSale(state, shares, outcome, unfilledBets) const { cpmmState } = calculateCpmmSale(
state,
shares,
outcome,
unfilledBets,
balanceByUserId
)
return getCpmmProbability(cpmmState.pool, cpmmState.p) return getCpmmProbability(cpmmState.pool, cpmmState.p)
} }

View File

@ -1,4 +1,4 @@
import { last, sortBy, sum, sumBy } from 'lodash' import { last, sortBy, sum, sumBy, uniq } from 'lodash'
import { calculatePayout } from './calculate' import { calculatePayout } from './calculate'
import { Bet, LimitBet } from './bet' import { Bet, LimitBet } from './bet'
import { Contract, CPMMContract, DPMContract } from './contract' import { Contract, CPMMContract, DPMContract } from './contract'
@ -62,16 +62,28 @@ export const computeBinaryCpmmElasticity = (
const limitBets = bets const limitBets = bets
.filter( .filter(
(b) => (b) =>
!b.isFilled && !b.isSold && !b.isRedemption && !b.sale && !b.isCancelled !b.isFilled &&
!b.isSold &&
!b.isRedemption &&
!b.sale &&
!b.isCancelled &&
b.limitProb !== undefined
) )
.sort((a, b) => a.createdTime - b.createdTime) .sort((a, b) => a.createdTime - b.createdTime) as LimitBet[]
const userIds = uniq(limitBets.map((b) => b.userId))
// Assume all limit orders are good.
const userBalances = Object.fromEntries(
userIds.map((id) => [id, Number.MAX_SAFE_INTEGER])
)
const { newPool: poolY, newP: pY } = getBinaryCpmmBetInfo( const { newPool: poolY, newP: pY } = getBinaryCpmmBetInfo(
'YES', 'YES',
betAmount, betAmount,
contract, contract,
undefined, undefined,
limitBets as LimitBet[] limitBets,
userBalances
) )
const resultYes = getCpmmProbability(poolY, pY) const resultYes = getCpmmProbability(poolY, pY)
@ -80,7 +92,8 @@ export const computeBinaryCpmmElasticity = (
betAmount, betAmount,
contract, contract,
undefined, undefined,
limitBets as LimitBet[] limitBets,
userBalances
) )
const resultNo = getCpmmProbability(poolN, pN) const resultNo = getCpmmProbability(poolN, pN)

View File

@ -78,7 +78,8 @@ export function calculateShares(
export function calculateSaleAmount( export function calculateSaleAmount(
contract: Contract, contract: Contract,
bet: Bet, bet: Bet,
unfilledBets: LimitBet[] unfilledBets: LimitBet[],
balanceByUserId: { [userId: string]: number }
) { ) {
return contract.mechanism === 'cpmm-1' && return contract.mechanism === 'cpmm-1' &&
(contract.outcomeType === 'BINARY' || (contract.outcomeType === 'BINARY' ||
@ -87,7 +88,8 @@ export function calculateSaleAmount(
contract, contract,
Math.abs(bet.shares), Math.abs(bet.shares),
bet.outcome as 'YES' | 'NO', bet.outcome as 'YES' | 'NO',
unfilledBets unfilledBets,
balanceByUserId
).saleValue ).saleValue
: calculateDpmSaleAmount(contract, bet) : calculateDpmSaleAmount(contract, bet)
} }
@ -102,14 +104,16 @@ export function getProbabilityAfterSale(
contract: Contract, contract: Contract,
outcome: string, outcome: string,
shares: number, shares: number,
unfilledBets: LimitBet[] unfilledBets: LimitBet[],
balanceByUserId: { [userId: string]: number }
) { ) {
return contract.mechanism === 'cpmm-1' return contract.mechanism === 'cpmm-1'
? getCpmmProbabilityAfterSale( ? getCpmmProbabilityAfterSale(
contract, contract,
shares, shares,
outcome as 'YES' | 'NO', outcome as 'YES' | 'NO',
unfilledBets unfilledBets,
balanceByUserId
) )
: getDpmProbabilityAfterSale(contract.totalShares, outcome, shares) : getDpmProbabilityAfterSale(contract.totalShares, outcome, shares)
} }

View File

@ -143,7 +143,8 @@ export const computeFills = (
betAmount: number, betAmount: number,
state: CpmmState, state: CpmmState,
limitProb: number | undefined, limitProb: number | undefined,
unfilledBets: LimitBet[] unfilledBets: LimitBet[],
balanceByUserId: { [userId: string]: number }
) => { ) => {
if (isNaN(betAmount)) { if (isNaN(betAmount)) {
throw new Error('Invalid bet amount: ${betAmount}') throw new Error('Invalid bet amount: ${betAmount}')
@ -165,10 +166,12 @@ export const computeFills = (
shares: number shares: number
timestamp: number timestamp: number
}[] = [] }[] = []
const ordersToCancel: LimitBet[] = []
let amount = betAmount let amount = betAmount
let cpmmState = { pool: state.pool, p: state.p } let cpmmState = { pool: state.pool, p: state.p }
let totalFees = noFees let totalFees = noFees
const currentBalanceByUserId = { ...balanceByUserId }
let i = 0 let i = 0
while (true) { while (true) {
@ -185,9 +188,20 @@ export const computeFills = (
takers.push(taker) takers.push(taker)
} else { } else {
// Matched against bet. // Matched against bet.
i++
const { userId } = maker.bet
const makerBalance = currentBalanceByUserId[userId]
if (floatingGreaterEqual(makerBalance, maker.amount)) {
currentBalanceByUserId[userId] = makerBalance - maker.amount
} else {
// Insufficient balance. Cancel maker bet.
ordersToCancel.push(maker.bet)
continue
}
takers.push(taker) takers.push(taker)
makers.push(maker) makers.push(maker)
i++
} }
amount -= taker.amount amount -= taker.amount
@ -195,7 +209,7 @@ export const computeFills = (
if (floatingEqual(amount, 0)) break if (floatingEqual(amount, 0)) break
} }
return { takers, makers, totalFees, cpmmState } return { takers, makers, totalFees, cpmmState, ordersToCancel }
} }
export const getBinaryCpmmBetInfo = ( export const getBinaryCpmmBetInfo = (
@ -203,15 +217,17 @@ export const getBinaryCpmmBetInfo = (
betAmount: number, betAmount: number,
contract: CPMMBinaryContract | PseudoNumericContract, contract: CPMMBinaryContract | PseudoNumericContract,
limitProb: number | undefined, limitProb: number | undefined,
unfilledBets: LimitBet[] unfilledBets: LimitBet[],
balanceByUserId: { [userId: string]: number }
) => { ) => {
const { pool, p } = contract const { pool, p } = contract
const { takers, makers, cpmmState, totalFees } = computeFills( const { takers, makers, cpmmState, totalFees, ordersToCancel } = computeFills(
outcome, outcome,
betAmount, betAmount,
{ pool, p }, { pool, p },
limitProb, limitProb,
unfilledBets unfilledBets,
balanceByUserId
) )
const probBefore = getCpmmProbability(contract.pool, contract.p) const probBefore = getCpmmProbability(contract.pool, contract.p)
const probAfter = getCpmmProbability(cpmmState.pool, cpmmState.p) const probAfter = getCpmmProbability(cpmmState.pool, cpmmState.p)
@ -246,6 +262,7 @@ export const getBinaryCpmmBetInfo = (
newP: cpmmState.p, newP: cpmmState.p,
newTotalLiquidity, newTotalLiquidity,
makers, makers,
ordersToCancel,
} }
} }
@ -254,14 +271,16 @@ export const getBinaryBetStats = (
betAmount: number, betAmount: number,
contract: CPMMBinaryContract | PseudoNumericContract, contract: CPMMBinaryContract | PseudoNumericContract,
limitProb: number, limitProb: number,
unfilledBets: LimitBet[] unfilledBets: LimitBet[],
balanceByUserId: { [userId: string]: number }
) => { ) => {
const { newBet } = getBinaryCpmmBetInfo( const { newBet } = getBinaryCpmmBetInfo(
outcome, outcome,
betAmount ?? 0, betAmount ?? 0,
contract, contract,
limitProb, limitProb,
unfilledBets as LimitBet[] unfilledBets,
balanceByUserId
) )
const remainingMatched = const remainingMatched =
((newBet.orderAmount ?? 0) - newBet.amount) / ((newBet.orderAmount ?? 0) - newBet.amount) /

View File

@ -84,15 +84,17 @@ export const getCpmmSellBetInfo = (
outcome: 'YES' | 'NO', outcome: 'YES' | 'NO',
contract: CPMMContract, contract: CPMMContract,
unfilledBets: LimitBet[], unfilledBets: LimitBet[],
balanceByUserId: { [userId: string]: number },
loanPaid: number loanPaid: number
) => { ) => {
const { pool, p } = contract const { pool, p } = contract
const { saleValue, cpmmState, fees, makers, takers } = calculateCpmmSale( const { saleValue, cpmmState, fees, makers, takers, ordersToCancel } = calculateCpmmSale(
contract, contract,
shares, shares,
outcome, outcome,
unfilledBets unfilledBets,
balanceByUserId,
) )
const probBefore = getCpmmProbability(pool, p) const probBefore = getCpmmProbability(pool, p)
@ -134,5 +136,6 @@ export const getCpmmSellBetInfo = (
fees, fees,
makers, makers,
takers, takers,
ordersToCancel
} }
} }

View File

@ -5,8 +5,6 @@ import { HOUSE_LIQUIDITY_PROVIDER_ID } from '../../common/antes'
import { createReferralNotification } from './create-notification' import { createReferralNotification } from './create-notification'
import { ReferralTxn } from '../../common/txn' import { ReferralTxn } from '../../common/txn'
import { Contract } from '../../common/contract' import { Contract } from '../../common/contract'
import { LimitBet } from '../../common/bet'
import { QuerySnapshot } from 'firebase-admin/firestore'
import { Group } from '../../common/group' import { Group } from '../../common/group'
import { REFERRAL_AMOUNT } from '../../common/economy' import { REFERRAL_AMOUNT } from '../../common/economy'
const firestore = admin.firestore() const firestore = admin.firestore()
@ -21,10 +19,6 @@ export const onUpdateUser = functions.firestore
if (prevUser.referredByUserId !== user.referredByUserId) { if (prevUser.referredByUserId !== user.referredByUserId) {
await handleUserUpdatedReferral(user, eventId) await handleUserUpdatedReferral(user, eventId)
} }
if (user.balance <= 0) {
await cancelLimitOrders(user.id)
}
}) })
async function handleUserUpdatedReferral(user: User, eventId: string) { async function handleUserUpdatedReferral(user: User, eventId: string) {
@ -123,15 +117,3 @@ async function handleUserUpdatedReferral(user: User, eventId: string) {
) )
}) })
} }
async function cancelLimitOrders(userId: string) {
const snapshot = (await firestore
.collectionGroup('bets')
.where('userId', '==', userId)
.where('isFilled', '==', false)
.get()) as QuerySnapshot<LimitBet>
await Promise.all(
snapshot.docs.map((doc) => doc.ref.update({ isCancelled: true }))
)
}

View File

@ -23,6 +23,7 @@ import { floatingEqual } from '../../common/util/math'
import { redeemShares } from './redeem-shares' import { redeemShares } from './redeem-shares'
import { log } from './utils' import { log } from './utils'
import { addUserToContractFollowers } from './follow-market' import { addUserToContractFollowers } from './follow-market'
import { filterDefined } from '../../common/util/array'
const bodySchema = z.object({ const bodySchema = z.object({
contractId: z.string(), contractId: z.string(),
@ -73,9 +74,11 @@ export const placebet = newEndpoint({}, async (req, auth) => {
newTotalLiquidity, newTotalLiquidity,
newP, newP,
makers, makers,
ordersToCancel,
} = await (async (): Promise< } = await (async (): Promise<
BetInfo & { BetInfo & {
makers?: maker[] makers?: maker[]
ordersToCancel?: LimitBet[]
} }
> => { > => {
if ( if (
@ -99,17 +102,16 @@ export const placebet = newEndpoint({}, async (req, auth) => {
limitProb = Math.round(limitProb * 100) / 100 limitProb = Math.round(limitProb * 100) / 100
} }
const unfilledBetsSnap = await trans.get( const { unfilledBets, balanceByUserId } =
getUnfilledBetsQuery(contractDoc) await getUnfilledBetsAndUserBalances(trans, contractDoc)
)
const unfilledBets = unfilledBetsSnap.docs.map((doc) => doc.data())
return getBinaryCpmmBetInfo( return getBinaryCpmmBetInfo(
outcome, outcome,
amount, amount,
contract, contract,
limitProb, limitProb,
unfilledBets unfilledBets,
balanceByUserId
) )
} else if ( } else if (
(outcomeType == 'FREE_RESPONSE' || outcomeType === 'MULTIPLE_CHOICE') && (outcomeType == 'FREE_RESPONSE' || outcomeType === 'MULTIPLE_CHOICE') &&
@ -152,6 +154,13 @@ export const placebet = newEndpoint({}, async (req, auth) => {
if (makers) { if (makers) {
updateMakers(makers, betDoc.id, contractDoc, trans) updateMakers(makers, betDoc.id, contractDoc, trans)
} }
if (ordersToCancel) {
for (const bet of ordersToCancel) {
trans.update(contractDoc.collection('bets').doc(bet.id), {
isCancelled: true,
})
}
}
if (newBet.amount !== 0) { if (newBet.amount !== 0) {
trans.update(userDoc, { balance: FieldValue.increment(-newBet.amount) }) trans.update(userDoc, { balance: FieldValue.increment(-newBet.amount) })
@ -193,13 +202,36 @@ export const placebet = newEndpoint({}, async (req, auth) => {
const firestore = admin.firestore() const firestore = admin.firestore()
export const getUnfilledBetsQuery = (contractDoc: DocumentReference) => { const getUnfilledBetsQuery = (contractDoc: DocumentReference) => {
return contractDoc return contractDoc
.collection('bets') .collection('bets')
.where('isFilled', '==', false) .where('isFilled', '==', false)
.where('isCancelled', '==', false) as Query<LimitBet> .where('isCancelled', '==', false) as Query<LimitBet>
} }
export const getUnfilledBetsAndUserBalances = async (
trans: Transaction,
contractDoc: DocumentReference
) => {
const unfilledBetsSnap = await trans.get(getUnfilledBetsQuery(contractDoc))
const unfilledBets = unfilledBetsSnap.docs.map((doc) => doc.data())
// Get balance of all users with open limit orders.
const userIds = uniq(unfilledBets.map((bet) => bet.userId))
const userDocs =
userIds.length === 0
? []
: await trans.getAll(
...userIds.map((userId) => firestore.doc(`users/${userId}`))
)
const users = filterDefined(userDocs.map((doc) => doc.data() as User))
const balanceByUserId = Object.fromEntries(
users.map((user) => [user.id, user.balance])
)
return { unfilledBets, balanceByUserId }
}
type maker = { type maker = {
bet: LimitBet bet: LimitBet
amount: number amount: number

View File

@ -1,6 +1,7 @@
import { mapValues, groupBy, sumBy, uniq } from 'lodash' import { mapValues, groupBy, sumBy, uniq } from 'lodash'
import * as admin from 'firebase-admin' import * as admin from 'firebase-admin'
import { z } from 'zod' import { z } from 'zod'
import { FieldValue } from 'firebase-admin/firestore'
import { APIError, newEndpoint, validate } from './api' import { APIError, newEndpoint, validate } from './api'
import { Contract, CPMM_MIN_POOL_QTY } from '../../common/contract' import { Contract, CPMM_MIN_POOL_QTY } from '../../common/contract'
@ -10,8 +11,7 @@ import { addObjects, removeUndefinedProps } from '../../common/util/object'
import { log } from './utils' import { log } from './utils'
import { Bet } from '../../common/bet' import { Bet } from '../../common/bet'
import { floatingEqual, floatingLesserEqual } from '../../common/util/math' import { floatingEqual, floatingLesserEqual } from '../../common/util/math'
import { getUnfilledBetsQuery, updateMakers } from './place-bet' import { getUnfilledBetsAndUserBalances, updateMakers } from './place-bet'
import { FieldValue } from 'firebase-admin/firestore'
import { redeemShares } from './redeem-shares' import { redeemShares } from './redeem-shares'
import { removeUserFromContractFollowers } from './follow-market' import { removeUserFromContractFollowers } from './follow-market'
@ -29,16 +29,18 @@ export const sellshares = newEndpoint({}, async (req, auth) => {
const contractDoc = firestore.doc(`contracts/${contractId}`) const contractDoc = firestore.doc(`contracts/${contractId}`)
const userDoc = firestore.doc(`users/${auth.uid}`) const userDoc = firestore.doc(`users/${auth.uid}`)
const betsQ = contractDoc.collection('bets').where('userId', '==', auth.uid) const betsQ = contractDoc.collection('bets').where('userId', '==', auth.uid)
const [[contractSnap, userSnap], userBetsSnap, unfilledBetsSnap] = const [
await Promise.all([ [contractSnap, userSnap],
transaction.getAll(contractDoc, userDoc), userBetsSnap,
transaction.get(betsQ), { unfilledBets, balanceByUserId },
transaction.get(getUnfilledBetsQuery(contractDoc)), ] = await Promise.all([
]) transaction.getAll(contractDoc, userDoc),
transaction.get(betsQ),
getUnfilledBetsAndUserBalances(transaction, contractDoc),
])
if (!contractSnap.exists) throw new APIError(400, 'Contract not found.') if (!contractSnap.exists) throw new APIError(400, 'Contract not found.')
if (!userSnap.exists) throw new APIError(400, 'User not found.') if (!userSnap.exists) throw new APIError(400, 'User not found.')
const userBets = userBetsSnap.docs.map((doc) => doc.data() as Bet) const userBets = userBetsSnap.docs.map((doc) => doc.data() as Bet)
const unfilledBets = unfilledBetsSnap.docs.map((doc) => doc.data())
const contract = contractSnap.data() as Contract const contract = contractSnap.data() as Contract
const user = userSnap.data() as User const user = userSnap.data() as User
@ -86,13 +88,15 @@ export const sellshares = newEndpoint({}, async (req, auth) => {
let loanPaid = saleFrac * loanAmount let loanPaid = saleFrac * loanAmount
if (!isFinite(loanPaid)) loanPaid = 0 if (!isFinite(loanPaid)) loanPaid = 0
const { newBet, newPool, newP, fees, makers } = getCpmmSellBetInfo( const { newBet, newPool, newP, fees, makers, ordersToCancel } =
soldShares, getCpmmSellBetInfo(
chosenOutcome, soldShares,
contract, chosenOutcome,
unfilledBets, contract,
loanPaid unfilledBets,
) balanceByUserId,
loanPaid
)
if ( if (
!newP || !newP ||
@ -127,6 +131,12 @@ export const sellshares = newEndpoint({}, async (req, auth) => {
}) })
) )
for (const bet of ordersToCancel) {
transaction.update(contractDoc.collection('bets').doc(bet.id), {
isCancelled: true,
})
}
return { newBet, makers, maxShares, soldShares } return { newBet, makers, maxShares, soldShares }
}) })

View File

@ -2,12 +2,12 @@ import clsx from 'clsx'
import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd' import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd'
import { MenuIcon } from '@heroicons/react/solid' import { MenuIcon } from '@heroicons/react/solid'
import { toast } from 'react-hot-toast' import { toast } from 'react-hot-toast'
import { XCircleIcon } from '@heroicons/react/outline'
import { Col } from 'web/components/layout/col' import { Col } from 'web/components/layout/col'
import { Row } from 'web/components/layout/row' import { Row } from 'web/components/layout/row'
import { Subtitle } from 'web/components/subtitle' import { Subtitle } from 'web/components/subtitle'
import { keyBy } from 'lodash' import { keyBy } from 'lodash'
import { XCircleIcon } from '@heroicons/react/outline'
import { Button } from './button' import { Button } from './button'
import { updateUser } from 'web/lib/firebase/users' import { updateUser } from 'web/lib/firebase/users'
import { leaveGroup } from 'web/lib/firebase/groups' import { leaveGroup } from 'web/lib/firebase/groups'

View File

@ -16,7 +16,7 @@ import { Button } from 'web/components/button'
import { BetSignUpPrompt } from './sign-up-prompt' import { BetSignUpPrompt } from './sign-up-prompt'
import { User } from 'web/lib/firebase/users' import { User } from 'web/lib/firebase/users'
import { SellRow } from './sell-row' import { SellRow } from './sell-row'
import { useUnfilledBets } from 'web/hooks/use-bets' import { useUnfilledBetsAndBalanceByUserId } from 'web/hooks/use-bets'
import { PlayMoneyDisclaimer } from './play-money-disclaimer' import { PlayMoneyDisclaimer } from './play-money-disclaimer'
/** Button that opens BetPanel in a new modal */ /** Button that opens BetPanel in a new modal */
@ -100,7 +100,9 @@ export function SignedInBinaryMobileBetting(props: {
user: User user: User
}) { }) {
const { contract, user } = props const { contract, user } = props
const unfilledBets = useUnfilledBets(contract.id) ?? [] const { unfilledBets, balanceByUserId } = useUnfilledBetsAndBalanceByUserId(
contract.id
)
return ( return (
<> <>
@ -111,6 +113,7 @@ export function SignedInBinaryMobileBetting(props: {
contract={contract as CPMMBinaryContract} contract={contract as CPMMBinaryContract}
user={user} user={user}
unfilledBets={unfilledBets} unfilledBets={unfilledBets}
balanceByUserId={balanceByUserId}
mobileView={true} mobileView={true}
/> />
</Col> </Col>

View File

@ -10,7 +10,7 @@ import { BuyAmountInput } from './amount-input'
import { Button } from './button' import { Button } from './button'
import { Row } from './layout/row' import { Row } from './layout/row'
import { YesNoSelector } from './yes-no-selector' import { YesNoSelector } from './yes-no-selector'
import { useUnfilledBets } from 'web/hooks/use-bets' import { useUnfilledBetsAndBalanceByUserId } from 'web/hooks/use-bets'
import { useUser } from 'web/hooks/use-user' import { useUser } from 'web/hooks/use-user'
import { BetSignUpPrompt } from './sign-up-prompt' import { BetSignUpPrompt } from './sign-up-prompt'
import { getCpmmProbability } from 'common/calculate-cpmm' import { getCpmmProbability } from 'common/calculate-cpmm'
@ -34,14 +34,17 @@ export function BetInline(props: {
const [error, setError] = useState<string>() const [error, setError] = useState<string>()
const isPseudoNumeric = contract.outcomeType === 'PSEUDO_NUMERIC' const isPseudoNumeric = contract.outcomeType === 'PSEUDO_NUMERIC'
const unfilledBets = useUnfilledBets(contract.id) ?? [] const { unfilledBets, balanceByUserId } = useUnfilledBetsAndBalanceByUserId(
contract.id
)
const { newPool, newP } = getBinaryCpmmBetInfo( const { newPool, newP } = getBinaryCpmmBetInfo(
outcome ?? 'YES', outcome ?? 'YES',
amount ?? 0, amount ?? 0,
contract, contract,
undefined, undefined,
unfilledBets unfilledBets,
balanceByUserId
) )
const resultProb = getCpmmProbability(newPool, newP) const resultProb = getCpmmProbability(newPool, newP)
useEffect(() => setProbAfter(resultProb), [setProbAfter, resultProb]) useEffect(() => setProbAfter(resultProb), [setProbAfter, resultProb])

View File

@ -35,7 +35,7 @@ import { useSaveBinaryShares } from './use-save-binary-shares'
import { BetSignUpPrompt } from './sign-up-prompt' import { BetSignUpPrompt } from './sign-up-prompt'
import { ProbabilityOrNumericInput } from './probability-input' import { ProbabilityOrNumericInput } from './probability-input'
import { track } from 'web/lib/service/analytics' import { track } from 'web/lib/service/analytics'
import { useUnfilledBets } from 'web/hooks/use-bets' import { useUnfilledBetsAndBalanceByUserId } from 'web/hooks/use-bets'
import { LimitBets } from './limit-bets' import { LimitBets } from './limit-bets'
import { PillButton } from './buttons/pill-button' import { PillButton } from './buttons/pill-button'
import { YesNoSelector } from './yes-no-selector' import { YesNoSelector } from './yes-no-selector'
@ -55,7 +55,9 @@ export function BetPanel(props: {
const { contract, className } = props const { contract, className } = props
const user = useUser() const user = useUser()
const userBets = useUserContractBets(user?.id, contract.id) const userBets = useUserContractBets(user?.id, contract.id)
const unfilledBets = useUnfilledBets(contract.id) ?? [] const { unfilledBets, balanceByUserId } = useUnfilledBetsAndBalanceByUserId(
contract.id
)
const { sharesOutcome } = useSaveBinaryShares(contract, userBets) const { sharesOutcome } = useSaveBinaryShares(contract, userBets)
const [isLimitOrder, setIsLimitOrder] = useState(false) const [isLimitOrder, setIsLimitOrder] = useState(false)
@ -86,12 +88,14 @@ export function BetPanel(props: {
contract={contract} contract={contract}
user={user} user={user}
unfilledBets={unfilledBets} unfilledBets={unfilledBets}
balanceByUserId={balanceByUserId}
/> />
<LimitOrderPanel <LimitOrderPanel
hidden={!isLimitOrder} hidden={!isLimitOrder}
contract={contract} contract={contract}
user={user} user={user}
unfilledBets={unfilledBets} unfilledBets={unfilledBets}
balanceByUserId={balanceByUserId}
/> />
</> </>
) : ( ) : (
@ -117,7 +121,9 @@ export function SimpleBetPanel(props: {
const user = useUser() const user = useUser()
const [isLimitOrder, setIsLimitOrder] = useState(false) const [isLimitOrder, setIsLimitOrder] = useState(false)
const unfilledBets = useUnfilledBets(contract.id) ?? [] const { unfilledBets, balanceByUserId } = useUnfilledBetsAndBalanceByUserId(
contract.id
)
return ( return (
<Col className={className}> <Col className={className}>
@ -142,6 +148,7 @@ export function SimpleBetPanel(props: {
contract={contract} contract={contract}
user={user} user={user}
unfilledBets={unfilledBets} unfilledBets={unfilledBets}
balanceByUserId={balanceByUserId}
onBuySuccess={onBetSuccess} onBuySuccess={onBetSuccess}
/> />
<LimitOrderPanel <LimitOrderPanel
@ -149,6 +156,7 @@ export function SimpleBetPanel(props: {
contract={contract} contract={contract}
user={user} user={user}
unfilledBets={unfilledBets} unfilledBets={unfilledBets}
balanceByUserId={balanceByUserId}
onBuySuccess={onBetSuccess} onBuySuccess={onBetSuccess}
/> />
@ -167,13 +175,21 @@ export function SimpleBetPanel(props: {
export function BuyPanel(props: { export function BuyPanel(props: {
contract: CPMMBinaryContract | PseudoNumericContract contract: CPMMBinaryContract | PseudoNumericContract
user: User | null | undefined user: User | null | undefined
unfilledBets: Bet[] unfilledBets: LimitBet[]
balanceByUserId: { [userId: string]: number }
hidden: boolean hidden: boolean
onBuySuccess?: () => void onBuySuccess?: () => void
mobileView?: boolean mobileView?: boolean
}) { }) {
const { contract, user, unfilledBets, hidden, onBuySuccess, mobileView } = const {
props contract,
user,
unfilledBets,
balanceByUserId,
hidden,
onBuySuccess,
mobileView,
} = props
const initialProb = getProbability(contract) const initialProb = getProbability(contract)
const isPseudoNumeric = contract.outcomeType === 'PSEUDO_NUMERIC' const isPseudoNumeric = contract.outcomeType === 'PSEUDO_NUMERIC'
@ -261,7 +277,8 @@ export function BuyPanel(props: {
betAmount ?? 0, betAmount ?? 0,
contract, contract,
undefined, undefined,
unfilledBets as LimitBet[] unfilledBets,
balanceByUserId
) )
const [seeLimit, setSeeLimit] = useState(false) const [seeLimit, setSeeLimit] = useState(false)
@ -416,6 +433,7 @@ export function BuyPanel(props: {
contract={contract} contract={contract}
user={user} user={user}
unfilledBets={unfilledBets} unfilledBets={unfilledBets}
balanceByUserId={balanceByUserId}
/> />
<LimitBets <LimitBets
contract={contract} contract={contract}
@ -431,11 +449,19 @@ export function BuyPanel(props: {
function LimitOrderPanel(props: { function LimitOrderPanel(props: {
contract: CPMMBinaryContract | PseudoNumericContract contract: CPMMBinaryContract | PseudoNumericContract
user: User | null | undefined user: User | null | undefined
unfilledBets: Bet[] unfilledBets: LimitBet[]
balanceByUserId: { [userId: string]: number }
hidden: boolean hidden: boolean
onBuySuccess?: () => void onBuySuccess?: () => void
}) { }) {
const { contract, user, unfilledBets, hidden, onBuySuccess } = props const {
contract,
user,
unfilledBets,
balanceByUserId,
hidden,
onBuySuccess,
} = props
const initialProb = getProbability(contract) const initialProb = getProbability(contract)
const isPseudoNumeric = contract.outcomeType === 'PSEUDO_NUMERIC' const isPseudoNumeric = contract.outcomeType === 'PSEUDO_NUMERIC'
@ -581,7 +607,8 @@ function LimitOrderPanel(props: {
yesAmount, yesAmount,
contract, contract,
yesLimitProb ?? initialProb, yesLimitProb ?? initialProb,
unfilledBets as LimitBet[] unfilledBets,
balanceByUserId
) )
const yesReturnPercent = formatPercent(yesReturn) const yesReturnPercent = formatPercent(yesReturn)
@ -595,7 +622,8 @@ function LimitOrderPanel(props: {
noAmount, noAmount,
contract, contract,
noLimitProb ?? initialProb, noLimitProb ?? initialProb,
unfilledBets as LimitBet[] unfilledBets,
balanceByUserId
) )
const noReturnPercent = formatPercent(noReturn) const noReturnPercent = formatPercent(noReturn)
@ -830,7 +858,9 @@ export function SellPanel(props: {
const [isSubmitting, setIsSubmitting] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false)
const [wasSubmitted, setWasSubmitted] = useState(false) const [wasSubmitted, setWasSubmitted] = useState(false)
const unfilledBets = useUnfilledBets(contract.id) ?? [] const { unfilledBets, balanceByUserId } = useUnfilledBetsAndBalanceByUserId(
contract.id
)
const betDisabled = isSubmitting || !amount || error !== undefined const betDisabled = isSubmitting || !amount || error !== undefined
@ -889,7 +919,8 @@ export function SellPanel(props: {
contract, contract,
sellQuantity ?? 0, sellQuantity ?? 0,
sharesOutcome, sharesOutcome,
unfilledBets unfilledBets,
balanceByUserId
) )
const netProceeds = saleValue - loanPaid const netProceeds = saleValue - loanPaid
const profit = saleValue - costBasis const profit = saleValue - costBasis

View File

@ -37,7 +37,7 @@ import { NumericContract } from 'common/contract'
import { formatNumericProbability } from 'common/pseudo-numeric' import { formatNumericProbability } from 'common/pseudo-numeric'
import { useUser } from 'web/hooks/use-user' import { useUser } from 'web/hooks/use-user'
import { useUserBets } from 'web/hooks/use-user-bets' import { useUserBets } from 'web/hooks/use-user-bets'
import { useUnfilledBets } from 'web/hooks/use-bets' import { useUnfilledBetsAndBalanceByUserId } from 'web/hooks/use-bets'
import { LimitBet } from 'common/bet' import { LimitBet } from 'common/bet'
import { Pagination } from './pagination' import { Pagination } from './pagination'
import { LimitOrderTable } from './limit-bets' import { LimitOrderTable } from './limit-bets'
@ -412,7 +412,9 @@ export function ContractBetsTable(props: {
const isNumeric = outcomeType === 'NUMERIC' const isNumeric = outcomeType === 'NUMERIC'
const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC' const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC'
const unfilledBets = useUnfilledBets(contract.id) ?? [] const { unfilledBets, balanceByUserId } = useUnfilledBetsAndBalanceByUserId(
contract.id
)
return ( return (
<div className="overflow-x-auto"> <div className="overflow-x-auto">
@ -461,6 +463,7 @@ export function ContractBetsTable(props: {
contract={contract} contract={contract}
isYourBet={isYourBets} isYourBet={isYourBets}
unfilledBets={unfilledBets} unfilledBets={unfilledBets}
balanceByUserId={balanceByUserId}
/> />
))} ))}
</tbody> </tbody>
@ -475,8 +478,10 @@ function BetRow(props: {
saleBet?: Bet saleBet?: Bet
isYourBet: boolean isYourBet: boolean
unfilledBets: LimitBet[] unfilledBets: LimitBet[]
balanceByUserId: { [userId: string]: number }
}) { }) {
const { bet, saleBet, contract, isYourBet, unfilledBets } = props const { bet, saleBet, contract, isYourBet, unfilledBets, balanceByUserId } =
props
const { const {
amount, amount,
outcome, outcome,
@ -504,9 +509,9 @@ function BetRow(props: {
} else if (contract.isResolved) { } else if (contract.isResolved) {
return resolvedPayout(contract, bet) return resolvedPayout(contract, bet)
} else { } else {
return calculateSaleAmount(contract, bet, unfilledBets) return calculateSaleAmount(contract, bet, unfilledBets, balanceByUserId)
} }
}, [contract, bet, saleBet, unfilledBets]) }, [contract, bet, saleBet, unfilledBets, balanceByUserId])
const saleDisplay = isAnte ? ( const saleDisplay = isAnte ? (
'ANTE' 'ANTE'
@ -545,6 +550,7 @@ function BetRow(props: {
contract={contract} contract={contract}
bet={bet} bet={bet}
unfilledBets={unfilledBets} unfilledBets={unfilledBets}
balanceByUserId={balanceByUserId}
/> />
)} )}
</td> </td>
@ -590,8 +596,9 @@ function SellButton(props: {
contract: Contract contract: Contract
bet: Bet bet: Bet
unfilledBets: LimitBet[] unfilledBets: LimitBet[]
balanceByUserId: { [userId: string]: number }
}) { }) {
const { contract, bet, unfilledBets } = props const { contract, bet, unfilledBets, balanceByUserId } = props
const { outcome, shares, loanAmount } = bet const { outcome, shares, loanAmount } = bet
const [isSubmitting, setIsSubmitting] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false)
@ -605,10 +612,16 @@ function SellButton(props: {
contract, contract,
outcome, outcome,
shares, shares,
unfilledBets unfilledBets,
balanceByUserId
) )
const saleAmount = calculateSaleAmount(contract, bet, unfilledBets) const saleAmount = calculateSaleAmount(
contract,
bet,
unfilledBets,
balanceByUserId
)
const profit = saleAmount - bet.amount const profit = saleAmount - bet.amount
return ( return (

View File

@ -33,7 +33,7 @@ import { sellShares } from 'web/lib/firebase/api'
import { calculateCpmmSale, getCpmmProbability } from 'common/calculate-cpmm' import { calculateCpmmSale, getCpmmProbability } from 'common/calculate-cpmm'
import { track } from 'web/lib/service/analytics' import { track } from 'web/lib/service/analytics'
import { formatNumericProbability } from 'common/pseudo-numeric' import { formatNumericProbability } from 'common/pseudo-numeric'
import { useUnfilledBets } from 'web/hooks/use-bets' import { useUnfilledBetsAndBalanceByUserId } from 'web/hooks/use-bets'
import { getBinaryProb } from 'common/contract-details' import { getBinaryProb } from 'common/contract-details'
const BET_SIZE = 10 const BET_SIZE = 10
@ -48,7 +48,10 @@ export function QuickBet(props: {
const isCpmm = mechanism === 'cpmm-1' const isCpmm = mechanism === 'cpmm-1'
const userBets = useUserContractBets(user.id, contract.id) const userBets = useUserContractBets(user.id, contract.id)
const unfilledBets = useUnfilledBets(contract.id) ?? [] // TODO: Below hook fetches a decent amount of data. Maybe not worth it to show prob change on hover?
const { unfilledBets, balanceByUserId } = useUnfilledBetsAndBalanceByUserId(
contract.id
)
const { hasYesShares, hasNoShares, yesShares, noShares } = const { hasYesShares, hasNoShares, yesShares, noShares } =
useSaveBinaryShares(contract, userBets) useSaveBinaryShares(contract, userBets)
@ -94,7 +97,8 @@ export function QuickBet(props: {
contract, contract,
sharesSold, sharesSold,
sellOutcome, sellOutcome,
unfilledBets unfilledBets,
balanceByUserId
) )
saleAmount = saleValue saleAmount = saleValue
previewProb = getCpmmProbability(cpmmState.pool, cpmmState.p) previewProb = getCpmmProbability(cpmmState.pool, cpmmState.p)

View File

@ -8,6 +8,7 @@ import {
withoutAnteBets, withoutAnteBets,
} from 'web/lib/firebase/bets' } from 'web/lib/firebase/bets'
import { LimitBet } from 'common/bet' import { LimitBet } from 'common/bet'
import { getUser } from 'web/lib/firebase/users'
export const useBets = ( export const useBets = (
contractId: string, contractId: string,
@ -62,3 +63,31 @@ export const useUnfilledBets = (contractId: string) => {
) )
return unfilledBets return unfilledBets
} }
export const useUnfilledBetsAndBalanceByUserId = (contractId: string) => {
const [data, setData] = useState<{
unfilledBets: LimitBet[]
balanceByUserId: { [userId: string]: number }
}>({ unfilledBets: [], balanceByUserId: {} })
useEffect(() => {
let requestCount = 0
return listenForUnfilledBets(contractId, (unfilledBets) => {
requestCount++
const count = requestCount
Promise.all(unfilledBets.map((bet) => getUser(bet.userId))).then(
(users) => {
if (count === requestCount) {
const balanceByUserId = Object.fromEntries(
users.map((user) => [user.id, user.balance])
)
setData({ unfilledBets, balanceByUserId })
}
}
)
})
}, [contractId])
return data
}