🧾 Limit orders! (#495)
* Simple limit order UI * Update bet schema * Restrict bet panel / bet row to only CPMMBinaryContracts (all binary DPM are resolved) * Limit orders partway implemented * Update follow leaderboard copy * Change cpmm code to take some state instead of whole contract * Write more of matching algorithm * Fill in more of placebet * Use client side contract search for emulator * More correct matching * Merge branch 'main' into limit-orders * Some cleanup * Listen for unfilled bets in bet panel. Calculate how the probability moves based on open limit orders. * Simpler switching between bet & limit bet. * Render your open bets (unfilled limit orders) * Cancel bet endpoint. * Fix build error * Rename open bets to limit bets. Tweak payout calculation * Limit probability selector to 1-99 * Deduct user balance only on each fill. Store orderAmount of bet. Timestamp of fills. * Use floating equal to check if have shares * Add limit order switcher to mobile bet dialog * Support limit orders on numeric markets * Allow CORS exception for Vercel deployments * Remove console.logs * Update user balance by new bet amount * Tweak vercel cors * Try another regexp for vercel cors * Test another vercel regex * Slight notifications refactor * Fix docs edit link (#624) * Fix docs edit link * Update github links * Small groups UX changes * Groups UX on mobile * Leaderboards => Rankings on groups * Unused vars * create: remove automatic setting of log scale * Use react-query to cache notifications (#625) * Use react-query to cache notifications * Fix imports * Cleanup * Limit unseen notifs query * Catch the bounced query * Don't use interval * Unused var * Avoid flash of page nav * Give notification question priority & 2 lines * Right justify timestamps * Rewording * Margin * Simplify error msg * Be explicit about limit for unseen notifs * Pass limit > 0 * Remove category filters * Remove category selector references * Track notification clicks * Analyze tab usage * Bold more on new group chats * Add API route for listing a bets by user (#567) * Add API route for getting a user's bets * Refactor bets API to use /bets * Update /markets to use zod validation * Update docs * Clone missing indexes from firestore * Minor notif spacing adjustments * Enable tipping on group chats w/ notif (#629) * Tweak cors regex for vercel * Your limit bets * Implement selling shares * Merge branch 'main' into limit-orders * Fix lint * Move binary search to util file * Add note that there might be closed form * Add tooltip to explain limit probability * Tweak * Cancel your limit orders if you run out of money * Don't show amount error in probability input * Require limit prob to be >= .1% and <= 99.9% * Fix focus input bug * Simplify mobile betting dialog * Move mobile limit bets list into bet dialog. * Small fixes to existing sell shares client * Lint * Refactor useSaveShares to actually read from localStorage, use less bug-prone interface. * Fix NaN error * Remove TODO * Simple bet fill notification * Tweak wording * Sort limit bets by limit prob * Padding on limit bets * Match header size Co-authored-by: Ian Philips <iansphilips@gmail.com> Co-authored-by: ahalekelly <ahalekelly@gmail.com> Co-authored-by: mantikoros <sgrugett@gmail.com> Co-authored-by: Ben Congdon <ben@congdon.dev> Co-authored-by: Austin Chen <akrolsmir@gmail.com>
This commit is contained in:
parent
fc06b03af8
commit
80ae551ca9
|
@ -4,6 +4,7 @@ export type Bet = {
|
||||||
id: string
|
id: string
|
||||||
userId: string
|
userId: string
|
||||||
contractId: string
|
contractId: string
|
||||||
|
createdTime: number
|
||||||
|
|
||||||
amount: number // bet size; negative if SELL bet
|
amount: number // bet size; negative if SELL bet
|
||||||
loanAmount?: number
|
loanAmount?: number
|
||||||
|
@ -25,9 +26,7 @@ export type Bet = {
|
||||||
isAnte?: boolean
|
isAnte?: boolean
|
||||||
isLiquidityProvision?: boolean
|
isLiquidityProvision?: boolean
|
||||||
isRedemption?: boolean
|
isRedemption?: boolean
|
||||||
|
} & Partial<LimitProps>
|
||||||
createdTime: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export type NumericBet = Bet & {
|
export type NumericBet = Bet & {
|
||||||
value: number
|
value: number
|
||||||
|
@ -35,4 +34,29 @@ export type NumericBet = Bet & {
|
||||||
allBetAmounts: { [outcome: string]: number }
|
allBetAmounts: { [outcome: string]: number }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Binary market limit order.
|
||||||
|
export type LimitBet = Bet & LimitProps
|
||||||
|
|
||||||
|
type LimitProps = {
|
||||||
|
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.
|
||||||
|
// A record of each transaction that partially (or fully) fills the orderAmount.
|
||||||
|
// 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: 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
|
||||||
|
// If the fill is a sale, it means the matching bet has shares of the same outcome.
|
||||||
|
// I.e. -fill.shares === matchedBet.shares
|
||||||
|
isSale?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export const MAX_LOAN_PER_CONTRACT = 20
|
export const MAX_LOAN_PER_CONTRACT = 20
|
||||||
|
|
|
@ -1,10 +1,17 @@
|
||||||
import { sum, groupBy, mapValues, sumBy, zip } from 'lodash'
|
import { sum, groupBy, mapValues, sumBy } from 'lodash'
|
||||||
|
import { LimitBet } from './bet'
|
||||||
|
|
||||||
import { CPMMContract } from './contract'
|
|
||||||
import { CREATOR_FEE, Fees, LIQUIDITY_FEE, PLATFORM_FEE } from './fees'
|
import { CREATOR_FEE, Fees, LIQUIDITY_FEE, PLATFORM_FEE } from './fees'
|
||||||
import { LiquidityProvision } from './liquidity-provision'
|
import { LiquidityProvision } from './liquidity-provision'
|
||||||
|
import { computeFills } from './new-bet'
|
||||||
|
import { binarySearch } from './util/algos'
|
||||||
import { addObjects } from './util/object'
|
import { addObjects } from './util/object'
|
||||||
|
|
||||||
|
export type CpmmState = {
|
||||||
|
pool: { [outcome: string]: number }
|
||||||
|
p: number
|
||||||
|
}
|
||||||
|
|
||||||
export function getCpmmProbability(
|
export function getCpmmProbability(
|
||||||
pool: { [outcome: string]: number },
|
pool: { [outcome: string]: number },
|
||||||
p: number
|
p: number
|
||||||
|
@ -14,11 +21,11 @@ export function getCpmmProbability(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCpmmProbabilityAfterBetBeforeFees(
|
export function getCpmmProbabilityAfterBetBeforeFees(
|
||||||
contract: CPMMContract,
|
state: CpmmState,
|
||||||
outcome: string,
|
outcome: string,
|
||||||
bet: number
|
bet: number
|
||||||
) {
|
) {
|
||||||
const { pool, p } = contract
|
const { pool, p } = state
|
||||||
const shares = calculateCpmmShares(pool, p, bet, outcome)
|
const shares = calculateCpmmShares(pool, p, bet, outcome)
|
||||||
const { YES: y, NO: n } = pool
|
const { YES: y, NO: n } = pool
|
||||||
|
|
||||||
|
@ -31,12 +38,12 @@ export function getCpmmProbabilityAfterBetBeforeFees(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCpmmOutcomeProbabilityAfterBet(
|
export function getCpmmOutcomeProbabilityAfterBet(
|
||||||
contract: CPMMContract,
|
state: CpmmState,
|
||||||
outcome: string,
|
outcome: string,
|
||||||
bet: number
|
bet: number
|
||||||
) {
|
) {
|
||||||
const { newPool } = calculateCpmmPurchase(contract, bet, outcome)
|
const { newPool } = calculateCpmmPurchase(state, bet, outcome)
|
||||||
const p = getCpmmProbability(newPool, contract.p)
|
const p = getCpmmProbability(newPool, state.p)
|
||||||
return outcome === 'NO' ? 1 - p : p
|
return outcome === 'NO' ? 1 - p : p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,12 +65,8 @@ function calculateCpmmShares(
|
||||||
: n + bet - (k * (bet + y) ** -p) ** (1 / (1 - p))
|
: n + bet - (k * (bet + y) ** -p) ** (1 / (1 - p))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCpmmFees(
|
export function getCpmmFees(state: CpmmState, bet: number, outcome: string) {
|
||||||
contract: CPMMContract,
|
const prob = getCpmmProbabilityAfterBetBeforeFees(state, outcome, bet)
|
||||||
bet: number,
|
|
||||||
outcome: string
|
|
||||||
) {
|
|
||||||
const prob = getCpmmProbabilityAfterBetBeforeFees(contract, outcome, bet)
|
|
||||||
const betP = outcome === 'YES' ? 1 - prob : prob
|
const betP = outcome === 'YES' ? 1 - prob : prob
|
||||||
|
|
||||||
const liquidityFee = LIQUIDITY_FEE * betP * bet
|
const liquidityFee = LIQUIDITY_FEE * betP * bet
|
||||||
|
@ -78,23 +81,23 @@ export function getCpmmFees(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function calculateCpmmSharesAfterFee(
|
export function calculateCpmmSharesAfterFee(
|
||||||
contract: CPMMContract,
|
state: CpmmState,
|
||||||
bet: number,
|
bet: number,
|
||||||
outcome: string
|
outcome: string
|
||||||
) {
|
) {
|
||||||
const { pool, p } = contract
|
const { pool, p } = state
|
||||||
const { remainingBet } = getCpmmFees(contract, bet, outcome)
|
const { remainingBet } = getCpmmFees(state, bet, outcome)
|
||||||
|
|
||||||
return calculateCpmmShares(pool, p, remainingBet, outcome)
|
return calculateCpmmShares(pool, p, remainingBet, outcome)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function calculateCpmmPurchase(
|
export function calculateCpmmPurchase(
|
||||||
contract: CPMMContract,
|
state: CpmmState,
|
||||||
bet: number,
|
bet: number,
|
||||||
outcome: string
|
outcome: string
|
||||||
) {
|
) {
|
||||||
const { pool, p } = contract
|
const { pool, p } = state
|
||||||
const { remainingBet, fees } = getCpmmFees(contract, bet, outcome)
|
const { remainingBet, fees } = getCpmmFees(state, bet, outcome)
|
||||||
|
|
||||||
const shares = calculateCpmmShares(pool, p, remainingBet, outcome)
|
const shares = calculateCpmmShares(pool, p, remainingBet, outcome)
|
||||||
const { YES: y, NO: n } = pool
|
const { YES: y, NO: n } = pool
|
||||||
|
@ -113,117 +116,111 @@ export function calculateCpmmPurchase(
|
||||||
return { shares, newPool, newP, fees }
|
return { shares, newPool, newP, fees }
|
||||||
}
|
}
|
||||||
|
|
||||||
function computeK(y: number, n: number, p: number) {
|
// Note: there might be a closed form solution for this.
|
||||||
return y ** p * n ** (1 - p)
|
// If so, feel free to switch out this implementation.
|
||||||
}
|
export function calculateCpmmAmountToProb(
|
||||||
|
state: CpmmState,
|
||||||
function sellSharesK(
|
prob: number,
|
||||||
y: number,
|
|
||||||
n: number,
|
|
||||||
p: number,
|
|
||||||
s: number,
|
|
||||||
outcome: 'YES' | 'NO',
|
|
||||||
b: number
|
|
||||||
) {
|
|
||||||
return outcome === 'YES'
|
|
||||||
? computeK(y - b + s, n - b, p)
|
|
||||||
: computeK(y - b, n - b + s, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
function calculateCpmmShareValue(
|
|
||||||
contract: CPMMContract,
|
|
||||||
shares: number,
|
|
||||||
outcome: 'YES' | 'NO'
|
outcome: 'YES' | 'NO'
|
||||||
) {
|
) {
|
||||||
const { pool, p } = contract
|
if (outcome === 'NO') prob = 1 - prob
|
||||||
|
|
||||||
// Find bet amount that preserves k after selling shares.
|
// First, find an upper bound that leads to a more extreme probability than prob.
|
||||||
const k = computeK(pool.YES, pool.NO, p)
|
let maxGuess = 10
|
||||||
const otherPool = outcome === 'YES' ? pool.NO : pool.YES
|
let newProb = 0
|
||||||
|
do {
|
||||||
|
maxGuess *= 10
|
||||||
|
newProb = getCpmmOutcomeProbabilityAfterBet(state, outcome, maxGuess)
|
||||||
|
} while (newProb < prob)
|
||||||
|
|
||||||
// Constrain the max sale value to the lessor of 1. shares and 2. the other pool.
|
// Then, binary search for the amount that gets closest to prob.
|
||||||
// This is because 1. the max value per share is M$ 1,
|
const amount = binarySearch(0, maxGuess, (amount) => {
|
||||||
// and 2. The other pool cannot go negative and the sale value is subtracted from it.
|
const newProb = getCpmmOutcomeProbabilityAfterBet(state, outcome, amount)
|
||||||
// (Without this, there are multiple solutions for the same k.)
|
return newProb - prob
|
||||||
let highAmount = Math.min(shares, otherPool)
|
})
|
||||||
let lowAmount = 0
|
|
||||||
let mid = 0
|
|
||||||
let kGuess = 0
|
|
||||||
while (true) {
|
|
||||||
mid = lowAmount + (highAmount - lowAmount) / 2
|
|
||||||
|
|
||||||
// Break once we've reached max precision.
|
return amount
|
||||||
if (mid === lowAmount || mid === highAmount) break
|
}
|
||||||
|
|
||||||
kGuess = sellSharesK(pool.YES, pool.NO, p, shares, outcome, mid)
|
function calculateAmountToBuyShares(
|
||||||
if (kGuess < k) {
|
state: CpmmState,
|
||||||
highAmount = mid
|
shares: number,
|
||||||
} else {
|
outcome: 'YES' | 'NO',
|
||||||
lowAmount = mid
|
unfilledBets: LimitBet[]
|
||||||
}
|
) {
|
||||||
}
|
// Search for amount between bounds (0, shares).
|
||||||
return mid
|
// Min share price is M$0, and max is M$1 each.
|
||||||
|
return binarySearch(0, shares, (amount) => {
|
||||||
|
const { takers } = computeFills(
|
||||||
|
outcome,
|
||||||
|
amount,
|
||||||
|
state,
|
||||||
|
undefined,
|
||||||
|
unfilledBets
|
||||||
|
)
|
||||||
|
|
||||||
|
const totalShares = sumBy(takers, (taker) => taker.shares)
|
||||||
|
return totalShares - shares
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function calculateCpmmSale(
|
export function calculateCpmmSale(
|
||||||
contract: CPMMContract,
|
state: CpmmState,
|
||||||
shares: number,
|
shares: number,
|
||||||
outcome: string
|
outcome: 'YES' | 'NO',
|
||||||
|
unfilledBets: LimitBet[]
|
||||||
) {
|
) {
|
||||||
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')
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawSaleValue = calculateCpmmShareValue(
|
const oppositeOutcome = outcome === 'YES' ? 'NO' : 'YES'
|
||||||
contract,
|
const buyAmount = calculateAmountToBuyShares(
|
||||||
|
state,
|
||||||
shares,
|
shares,
|
||||||
outcome as 'YES' | 'NO'
|
oppositeOutcome,
|
||||||
|
unfilledBets
|
||||||
)
|
)
|
||||||
|
|
||||||
const { fees, remainingBet: saleValue } = getCpmmFees(
|
const { cpmmState, makers, takers, totalFees } = computeFills(
|
||||||
contract,
|
oppositeOutcome,
|
||||||
rawSaleValue,
|
buyAmount,
|
||||||
outcome === 'YES' ? 'NO' : 'YES'
|
state,
|
||||||
|
undefined,
|
||||||
|
unfilledBets
|
||||||
)
|
)
|
||||||
|
|
||||||
const { pool } = contract
|
// Transform buys of opposite outcome into sells.
|
||||||
const { YES: y, NO: n } = pool
|
const saleTakers = takers.map((taker) => ({
|
||||||
|
...taker,
|
||||||
|
// You bought opposite shares, which combine with existing shares, removing them.
|
||||||
|
shares: -taker.shares,
|
||||||
|
// Opposite shares combine with shares you are selling for M$ of shares.
|
||||||
|
// You paid taker.amount for the opposite shares.
|
||||||
|
// Take the negative because this is money you gain.
|
||||||
|
amount: -(taker.shares - taker.amount),
|
||||||
|
isSale: true,
|
||||||
|
}))
|
||||||
|
|
||||||
const { liquidityFee: fee } = fees
|
const saleValue = -sumBy(saleTakers, (taker) => taker.amount)
|
||||||
|
|
||||||
const [newY, newN] =
|
return {
|
||||||
outcome === 'YES'
|
saleValue,
|
||||||
? [y + shares - saleValue + fee, n - saleValue + fee]
|
cpmmState,
|
||||||
: [y - saleValue + fee, n + shares - saleValue + fee]
|
fees: totalFees,
|
||||||
|
makers,
|
||||||
if (newY < 0 || newN < 0) {
|
takers: saleTakers,
|
||||||
console.log('calculateCpmmSale', {
|
|
||||||
newY,
|
|
||||||
newN,
|
|
||||||
y,
|
|
||||||
n,
|
|
||||||
shares,
|
|
||||||
saleValue,
|
|
||||||
fee,
|
|
||||||
outcome,
|
|
||||||
})
|
|
||||||
throw new Error('Cannot sell more than in pool')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const postBetPool = { YES: newY, NO: newN }
|
|
||||||
|
|
||||||
const { newPool, newP } = addCpmmLiquidity(postBetPool, contract.p, fee)
|
|
||||||
|
|
||||||
return { saleValue, newPool, newP, fees }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCpmmProbabilityAfterSale(
|
export function getCpmmProbabilityAfterSale(
|
||||||
contract: CPMMContract,
|
state: CpmmState,
|
||||||
shares: number,
|
shares: number,
|
||||||
outcome: 'YES' | 'NO'
|
outcome: 'YES' | 'NO',
|
||||||
|
unfilledBets: LimitBet[]
|
||||||
) {
|
) {
|
||||||
const { newPool } = calculateCpmmSale(contract, shares, outcome)
|
const { cpmmState } = calculateCpmmSale(state, shares, outcome, unfilledBets)
|
||||||
return getCpmmProbability(newPool, contract.p)
|
return getCpmmProbability(cpmmState.pool, cpmmState.p)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCpmmLiquidity(
|
export function getCpmmLiquidity(
|
||||||
|
@ -267,11 +264,11 @@ const calculateLiquidityDelta = (p: number) => (l: LiquidityProvision) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCpmmLiquidityPoolWeights(
|
export function getCpmmLiquidityPoolWeights(
|
||||||
contract: CPMMContract,
|
state: CpmmState,
|
||||||
liquidities: LiquidityProvision[],
|
liquidities: LiquidityProvision[],
|
||||||
excludeAntes: boolean
|
excludeAntes: boolean
|
||||||
) {
|
) {
|
||||||
const calcLiqudity = calculateLiquidityDelta(contract.p)
|
const calcLiqudity = calculateLiquidityDelta(state.p)
|
||||||
const liquidityShares = liquidities.map(calcLiqudity)
|
const liquidityShares = liquidities.map(calcLiqudity)
|
||||||
const shareSum = sum(liquidityShares)
|
const shareSum = sum(liquidityShares)
|
||||||
|
|
||||||
|
@ -293,16 +290,12 @@ export function getCpmmLiquidityPoolWeights(
|
||||||
|
|
||||||
export function getUserLiquidityShares(
|
export function getUserLiquidityShares(
|
||||||
userId: string,
|
userId: string,
|
||||||
contract: CPMMContract,
|
state: CpmmState,
|
||||||
liquidities: LiquidityProvision[],
|
liquidities: LiquidityProvision[],
|
||||||
excludeAntes: boolean
|
excludeAntes: boolean
|
||||||
) {
|
) {
|
||||||
const weights = getCpmmLiquidityPoolWeights(
|
const weights = getCpmmLiquidityPoolWeights(state, liquidities, excludeAntes)
|
||||||
contract,
|
|
||||||
liquidities,
|
|
||||||
excludeAntes
|
|
||||||
)
|
|
||||||
const userWeight = weights[userId] ?? 0
|
const userWeight = weights[userId] ?? 0
|
||||||
|
|
||||||
return mapValues(contract.pool, (shares) => userWeight * shares)
|
return mapValues(state.pool, (shares) => userWeight * shares)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { maxBy } from 'lodash'
|
import { maxBy } from 'lodash'
|
||||||
import { Bet } from './bet'
|
import { Bet, LimitBet } from './bet'
|
||||||
import {
|
import {
|
||||||
calculateCpmmSale,
|
calculateCpmmSale,
|
||||||
getCpmmProbability,
|
getCpmmProbability,
|
||||||
|
@ -24,6 +24,7 @@ import {
|
||||||
FreeResponseContract,
|
FreeResponseContract,
|
||||||
PseudoNumericContract,
|
PseudoNumericContract,
|
||||||
} from './contract'
|
} from './contract'
|
||||||
|
import { floatingEqual } from './util/math'
|
||||||
|
|
||||||
export function getProbability(
|
export function getProbability(
|
||||||
contract: BinaryContract | PseudoNumericContract
|
contract: BinaryContract | PseudoNumericContract
|
||||||
|
@ -73,11 +74,20 @@ export function calculateShares(
|
||||||
: calculateDpmShares(contract.totalShares, bet, betChoice)
|
: calculateDpmShares(contract.totalShares, bet, betChoice)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function calculateSaleAmount(contract: Contract, bet: Bet) {
|
export function calculateSaleAmount(
|
||||||
|
contract: Contract,
|
||||||
|
bet: Bet,
|
||||||
|
unfilledBets: LimitBet[]
|
||||||
|
) {
|
||||||
return contract.mechanism === 'cpmm-1' &&
|
return contract.mechanism === 'cpmm-1' &&
|
||||||
(contract.outcomeType === 'BINARY' ||
|
(contract.outcomeType === 'BINARY' ||
|
||||||
contract.outcomeType === 'PSEUDO_NUMERIC')
|
contract.outcomeType === 'PSEUDO_NUMERIC')
|
||||||
? calculateCpmmSale(contract, Math.abs(bet.shares), bet.outcome).saleValue
|
? calculateCpmmSale(
|
||||||
|
contract,
|
||||||
|
Math.abs(bet.shares),
|
||||||
|
bet.outcome as 'YES' | 'NO',
|
||||||
|
unfilledBets
|
||||||
|
).saleValue
|
||||||
: calculateDpmSaleAmount(contract, bet)
|
: calculateDpmSaleAmount(contract, bet)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,10 +100,16 @@ export function calculatePayoutAfterCorrectBet(contract: Contract, bet: Bet) {
|
||||||
export function getProbabilityAfterSale(
|
export function getProbabilityAfterSale(
|
||||||
contract: Contract,
|
contract: Contract,
|
||||||
outcome: string,
|
outcome: string,
|
||||||
shares: number
|
shares: number,
|
||||||
|
unfilledBets: LimitBet[]
|
||||||
) {
|
) {
|
||||||
return contract.mechanism === 'cpmm-1'
|
return contract.mechanism === 'cpmm-1'
|
||||||
? getCpmmProbabilityAfterSale(contract, shares, outcome as 'YES' | 'NO')
|
? getCpmmProbabilityAfterSale(
|
||||||
|
contract,
|
||||||
|
shares,
|
||||||
|
outcome as 'YES' | 'NO',
|
||||||
|
unfilledBets
|
||||||
|
)
|
||||||
: getDpmProbabilityAfterSale(contract.totalShares, outcome, shares)
|
: getDpmProbabilityAfterSale(contract.totalShares, outcome, shares)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -157,7 +173,9 @@ export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) {
|
||||||
const profit = payout + saleValue + redeemed - totalInvested
|
const profit = payout + saleValue + redeemed - totalInvested
|
||||||
const profitPercent = (profit / totalInvested) * 100
|
const profitPercent = (profit / totalInvested) * 100
|
||||||
|
|
||||||
const hasShares = Object.values(totalShares).some((shares) => shares > 0)
|
const hasShares = Object.values(totalShares).some(
|
||||||
|
(shares) => !floatingEqual(shares, 0)
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
invested: Math.max(0, currentInvested),
|
invested: Math.max(0, currentInvested),
|
||||||
|
|
|
@ -34,5 +34,9 @@ export const IS_PRIVATE_MANIFOLD = ENV_CONFIG.visibility === 'PRIVATE'
|
||||||
export const CORS_ORIGIN_MANIFOLD = new RegExp(
|
export const CORS_ORIGIN_MANIFOLD = new RegExp(
|
||||||
'^https?://(?:[a-zA-Z0-9\\-]+\\.)*' + escapeRegExp(ENV_CONFIG.domain) + '$'
|
'^https?://(?:[a-zA-Z0-9\\-]+\\.)*' + escapeRegExp(ENV_CONFIG.domain) + '$'
|
||||||
)
|
)
|
||||||
|
// Vercel deployments, used for testing.
|
||||||
|
export const CORS_ORIGIN_VERCEL = new RegExp(
|
||||||
|
'^https?://[a-zA-Z0-9\\-]+' + escapeRegExp('mantic.vercel.app') + '$'
|
||||||
|
)
|
||||||
// Any localhost server on any port
|
// Any localhost server on any port
|
||||||
export const CORS_ORIGIN_LOCALHOST = /^http:\/\/localhost:\d+$/
|
export const CORS_ORIGIN_LOCALHOST = /^http:\/\/localhost:\d+$/
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { sumBy } from 'lodash'
|
import { sortBy, sumBy } from 'lodash'
|
||||||
|
|
||||||
import { Bet, MAX_LOAN_PER_CONTRACT, NumericBet } from './bet'
|
import { Bet, fill, LimitBet, MAX_LOAN_PER_CONTRACT, NumericBet } from './bet'
|
||||||
import {
|
import {
|
||||||
calculateDpmShares,
|
calculateDpmShares,
|
||||||
getDpmProbability,
|
getDpmProbability,
|
||||||
|
@ -8,7 +8,12 @@ import {
|
||||||
getNumericBets,
|
getNumericBets,
|
||||||
calculateNumericDpmShares,
|
calculateNumericDpmShares,
|
||||||
} from './calculate-dpm'
|
} from './calculate-dpm'
|
||||||
import { calculateCpmmPurchase, getCpmmProbability } from './calculate-cpmm'
|
import {
|
||||||
|
calculateCpmmAmountToProb,
|
||||||
|
calculateCpmmPurchase,
|
||||||
|
CpmmState,
|
||||||
|
getCpmmProbability,
|
||||||
|
} from './calculate-cpmm'
|
||||||
import {
|
import {
|
||||||
CPMMBinaryContract,
|
CPMMBinaryContract,
|
||||||
DPMBinaryContract,
|
DPMBinaryContract,
|
||||||
|
@ -17,8 +22,13 @@ import {
|
||||||
PseudoNumericContract,
|
PseudoNumericContract,
|
||||||
} from './contract'
|
} from './contract'
|
||||||
import { noFees } from './fees'
|
import { noFees } from './fees'
|
||||||
import { addObjects } from './util/object'
|
import { addObjects, removeUndefinedProps } from './util/object'
|
||||||
import { NUMERIC_FIXED_VAR } from './numeric-constants'
|
import { NUMERIC_FIXED_VAR } from './numeric-constants'
|
||||||
|
import {
|
||||||
|
floatingEqual,
|
||||||
|
floatingGreaterEqual,
|
||||||
|
floatingLesserEqual,
|
||||||
|
} from './util/math'
|
||||||
|
|
||||||
export type CandidateBet<T extends Bet = Bet> = Omit<T, 'id' | 'userId'>
|
export type CandidateBet<T extends Bet = Bet> = Omit<T, 'id' | 'userId'>
|
||||||
export type BetInfo = {
|
export type BetInfo = {
|
||||||
|
@ -30,38 +40,203 @@ export type BetInfo = {
|
||||||
newP?: number
|
newP?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getNewBinaryCpmmBetInfo = (
|
const computeFill = (
|
||||||
outcome: 'YES' | 'NO',
|
|
||||||
amount: number,
|
amount: number,
|
||||||
contract: CPMMBinaryContract | PseudoNumericContract,
|
outcome: 'YES' | 'NO',
|
||||||
loanAmount: number
|
limitProb: number | undefined,
|
||||||
|
cpmmState: CpmmState,
|
||||||
|
matchedBet: LimitBet | undefined
|
||||||
) => {
|
) => {
|
||||||
const { shares, newPool, newP, fees } = calculateCpmmPurchase(
|
const prob = getCpmmProbability(cpmmState.pool, cpmmState.p)
|
||||||
contract,
|
|
||||||
amount,
|
|
||||||
outcome
|
|
||||||
)
|
|
||||||
|
|
||||||
const { pool, p, totalLiquidity } = contract
|
if (
|
||||||
const probBefore = getCpmmProbability(pool, p)
|
limitProb !== undefined &&
|
||||||
const probAfter = getCpmmProbability(newPool, newP)
|
(outcome === 'YES'
|
||||||
|
? floatingGreaterEqual(prob, limitProb) &&
|
||||||
const newBet: CandidateBet = {
|
(matchedBet?.limitProb ?? 1) > limitProb
|
||||||
contractId: contract.id,
|
: floatingLesserEqual(prob, limitProb) &&
|
||||||
amount,
|
(matchedBet?.limitProb ?? 0) < limitProb)
|
||||||
shares,
|
) {
|
||||||
outcome,
|
// No fill.
|
||||||
fees,
|
return undefined
|
||||||
loanAmount,
|
|
||||||
probBefore,
|
|
||||||
probAfter,
|
|
||||||
createdTime: Date.now(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { liquidityFee } = fees
|
const timestamp = Date.now()
|
||||||
const newTotalLiquidity = (totalLiquidity ?? 0) + liquidityFee
|
|
||||||
|
|
||||||
return { newBet, newPool, newP, newTotalLiquidity }
|
if (
|
||||||
|
!matchedBet ||
|
||||||
|
(outcome === 'YES'
|
||||||
|
? prob < matchedBet.limitProb
|
||||||
|
: prob > matchedBet.limitProb)
|
||||||
|
) {
|
||||||
|
// Fill from pool.
|
||||||
|
const limit = !matchedBet
|
||||||
|
? limitProb
|
||||||
|
: outcome === 'YES'
|
||||||
|
? Math.min(matchedBet.limitProb, limitProb ?? 1)
|
||||||
|
: Math.max(matchedBet.limitProb, limitProb ?? 0)
|
||||||
|
|
||||||
|
const buyAmount =
|
||||||
|
limit === undefined
|
||||||
|
? amount
|
||||||
|
: Math.min(amount, calculateCpmmAmountToProb(cpmmState, limit, outcome))
|
||||||
|
|
||||||
|
const { shares, newPool, newP, fees } = calculateCpmmPurchase(
|
||||||
|
cpmmState,
|
||||||
|
buyAmount,
|
||||||
|
outcome
|
||||||
|
)
|
||||||
|
const newState = { pool: newPool, p: newP }
|
||||||
|
|
||||||
|
return {
|
||||||
|
maker: {
|
||||||
|
matchedBetId: null,
|
||||||
|
shares,
|
||||||
|
amount: buyAmount,
|
||||||
|
state: newState,
|
||||||
|
fees,
|
||||||
|
timestamp,
|
||||||
|
},
|
||||||
|
taker: {
|
||||||
|
matchedBetId: null,
|
||||||
|
shares,
|
||||||
|
amount: buyAmount,
|
||||||
|
timestamp,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill from matchedBet.
|
||||||
|
const matchRemaining = matchedBet.orderAmount - matchedBet.amount
|
||||||
|
const shares = Math.min(
|
||||||
|
amount /
|
||||||
|
(outcome === 'YES' ? matchedBet.limitProb : 1 - matchedBet.limitProb),
|
||||||
|
matchRemaining /
|
||||||
|
(outcome === 'YES' ? 1 - matchedBet.limitProb : matchedBet.limitProb)
|
||||||
|
)
|
||||||
|
|
||||||
|
const maker = {
|
||||||
|
bet: matchedBet,
|
||||||
|
matchedBetId: 'taker',
|
||||||
|
amount:
|
||||||
|
shares *
|
||||||
|
(outcome === 'YES' ? 1 - matchedBet.limitProb : matchedBet.limitProb),
|
||||||
|
shares,
|
||||||
|
timestamp,
|
||||||
|
}
|
||||||
|
const taker = {
|
||||||
|
matchedBetId: matchedBet.id,
|
||||||
|
amount:
|
||||||
|
shares *
|
||||||
|
(outcome === 'YES' ? matchedBet.limitProb : 1 - matchedBet.limitProb),
|
||||||
|
shares,
|
||||||
|
timestamp,
|
||||||
|
}
|
||||||
|
return { maker, taker }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const computeFills = (
|
||||||
|
outcome: 'YES' | 'NO',
|
||||||
|
betAmount: number,
|
||||||
|
state: CpmmState,
|
||||||
|
limitProb: number | undefined,
|
||||||
|
unfilledBets: LimitBet[]
|
||||||
|
) => {
|
||||||
|
const sortedBets = sortBy(
|
||||||
|
unfilledBets.filter((bet) => bet.outcome !== outcome),
|
||||||
|
(bet) => (outcome === 'YES' ? bet.limitProb : -bet.limitProb),
|
||||||
|
(bet) => bet.createdTime
|
||||||
|
)
|
||||||
|
|
||||||
|
const takers: fill[] = []
|
||||||
|
const makers: {
|
||||||
|
bet: LimitBet
|
||||||
|
amount: number
|
||||||
|
shares: number
|
||||||
|
timestamp: number
|
||||||
|
}[] = []
|
||||||
|
|
||||||
|
let amount = betAmount
|
||||||
|
let cpmmState = { pool: state.pool, p: state.p }
|
||||||
|
let totalFees = noFees
|
||||||
|
|
||||||
|
let i = 0
|
||||||
|
while (true) {
|
||||||
|
const matchedBet: LimitBet | undefined = sortedBets[i]
|
||||||
|
const fill = computeFill(amount, outcome, limitProb, cpmmState, matchedBet)
|
||||||
|
if (!fill) break
|
||||||
|
|
||||||
|
const { taker, maker } = fill
|
||||||
|
|
||||||
|
if (maker.matchedBetId === null) {
|
||||||
|
// Matched against pool.
|
||||||
|
cpmmState = maker.state
|
||||||
|
totalFees = addObjects(totalFees, maker.fees)
|
||||||
|
takers.push(taker)
|
||||||
|
} else {
|
||||||
|
// Matched against bet.
|
||||||
|
takers.push(taker)
|
||||||
|
makers.push(maker)
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
amount -= taker.amount
|
||||||
|
|
||||||
|
if (floatingEqual(amount, 0)) break
|
||||||
|
}
|
||||||
|
|
||||||
|
return { takers, makers, totalFees, cpmmState }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getBinaryCpmmBetInfo = (
|
||||||
|
outcome: 'YES' | 'NO',
|
||||||
|
betAmount: number,
|
||||||
|
contract: CPMMBinaryContract | PseudoNumericContract,
|
||||||
|
limitProb: number | undefined,
|
||||||
|
unfilledBets: LimitBet[]
|
||||||
|
) => {
|
||||||
|
const { pool, p } = contract
|
||||||
|
const { takers, makers, cpmmState, totalFees } = computeFills(
|
||||||
|
outcome,
|
||||||
|
betAmount,
|
||||||
|
{ pool, p },
|
||||||
|
limitProb,
|
||||||
|
unfilledBets
|
||||||
|
)
|
||||||
|
const probBefore = getCpmmProbability(contract.pool, contract.p)
|
||||||
|
const probAfter = getCpmmProbability(cpmmState.pool, cpmmState.p)
|
||||||
|
|
||||||
|
const takerAmount = sumBy(takers, 'amount')
|
||||||
|
const takerShares = sumBy(takers, 'shares')
|
||||||
|
const isFilled = floatingEqual(betAmount, takerAmount)
|
||||||
|
|
||||||
|
const newBet: CandidateBet = removeUndefinedProps({
|
||||||
|
orderAmount: betAmount,
|
||||||
|
amount: takerAmount,
|
||||||
|
shares: takerShares,
|
||||||
|
limitProb,
|
||||||
|
isFilled,
|
||||||
|
isCancelled: false,
|
||||||
|
fills: takers,
|
||||||
|
contractId: contract.id,
|
||||||
|
outcome,
|
||||||
|
probBefore,
|
||||||
|
probAfter,
|
||||||
|
loanAmount: 0,
|
||||||
|
createdTime: Date.now(),
|
||||||
|
fees: totalFees,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { liquidityFee } = totalFees
|
||||||
|
const newTotalLiquidity = (contract.totalLiquidity ?? 0) + liquidityFee
|
||||||
|
|
||||||
|
return {
|
||||||
|
newBet,
|
||||||
|
newPool: cpmmState.pool,
|
||||||
|
newP: cpmmState.p,
|
||||||
|
newTotalLiquidity,
|
||||||
|
makers,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getNewBinaryDpmBetInfo = (
|
export const getNewBinaryDpmBetInfo = (
|
||||||
|
|
|
@ -62,3 +62,4 @@ export type notification_reason_types =
|
||||||
| 'unique_bettors_on_your_contract'
|
| 'unique_bettors_on_your_contract'
|
||||||
| 'on_group_you_are_member_of'
|
| 'on_group_you_are_member_of'
|
||||||
| 'tip_received'
|
| 'tip_received'
|
||||||
|
| 'bet_fill'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Bet } from './bet'
|
import { Bet, LimitBet } from './bet'
|
||||||
import {
|
import {
|
||||||
calculateDpmShareValue,
|
calculateDpmShareValue,
|
||||||
deductDpmFees,
|
deductDpmFees,
|
||||||
|
@ -7,6 +7,7 @@ import {
|
||||||
import { calculateCpmmSale, getCpmmProbability } from './calculate-cpmm'
|
import { calculateCpmmSale, getCpmmProbability } from './calculate-cpmm'
|
||||||
import { CPMMContract, DPMContract } from './contract'
|
import { CPMMContract, DPMContract } from './contract'
|
||||||
import { DPM_CREATOR_FEE, DPM_PLATFORM_FEE, Fees } from './fees'
|
import { DPM_CREATOR_FEE, DPM_PLATFORM_FEE, Fees } from './fees'
|
||||||
|
import { sumBy } from 'lodash'
|
||||||
|
|
||||||
export type CandidateBet<T extends Bet> = Omit<T, 'id' | 'userId'>
|
export type CandidateBet<T extends Bet> = Omit<T, 'id' | 'userId'>
|
||||||
|
|
||||||
|
@ -78,19 +79,24 @@ export const getCpmmSellBetInfo = (
|
||||||
shares: number,
|
shares: number,
|
||||||
outcome: 'YES' | 'NO',
|
outcome: 'YES' | 'NO',
|
||||||
contract: CPMMContract,
|
contract: CPMMContract,
|
||||||
prevLoanAmount: number
|
prevLoanAmount: number,
|
||||||
|
unfilledBets: LimitBet[]
|
||||||
) => {
|
) => {
|
||||||
const { pool, p } = contract
|
const { pool, p } = contract
|
||||||
|
|
||||||
const { saleValue, newPool, newP, fees } = calculateCpmmSale(
|
const { saleValue, cpmmState, fees, makers, takers } = calculateCpmmSale(
|
||||||
contract,
|
contract,
|
||||||
shares,
|
shares,
|
||||||
outcome
|
outcome,
|
||||||
|
unfilledBets
|
||||||
)
|
)
|
||||||
|
|
||||||
const loanPaid = Math.min(prevLoanAmount, saleValue)
|
const loanPaid = Math.min(prevLoanAmount, saleValue)
|
||||||
const probBefore = getCpmmProbability(pool, p)
|
const probBefore = getCpmmProbability(pool, p)
|
||||||
const probAfter = getCpmmProbability(newPool, p)
|
const probAfter = getCpmmProbability(cpmmState.pool, cpmmState.p)
|
||||||
|
|
||||||
|
const takerAmount = sumBy(takers, 'amount')
|
||||||
|
const takerShares = sumBy(takers, 'shares')
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
'SELL M$',
|
'SELL M$',
|
||||||
|
@ -104,20 +110,26 @@ export const getCpmmSellBetInfo = (
|
||||||
|
|
||||||
const newBet: CandidateBet<Bet> = {
|
const newBet: CandidateBet<Bet> = {
|
||||||
contractId: contract.id,
|
contractId: contract.id,
|
||||||
amount: -saleValue,
|
amount: takerAmount,
|
||||||
shares: -shares,
|
shares: takerShares,
|
||||||
outcome,
|
outcome,
|
||||||
probBefore,
|
probBefore,
|
||||||
probAfter,
|
probAfter,
|
||||||
createdTime: Date.now(),
|
createdTime: Date.now(),
|
||||||
loanAmount: -loanPaid,
|
loanAmount: -loanPaid,
|
||||||
fees,
|
fees,
|
||||||
|
fills: takers,
|
||||||
|
isFilled: true,
|
||||||
|
isCancelled: false,
|
||||||
|
orderAmount: takerAmount,
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
newBet,
|
newBet,
|
||||||
newPool,
|
newPool: cpmmState.pool,
|
||||||
newP,
|
newP: cpmmState.p,
|
||||||
fees,
|
fees,
|
||||||
|
makers,
|
||||||
|
takers,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
22
common/util/algos.ts
Normal file
22
common/util/algos.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
export function binarySearch(
|
||||||
|
min: number,
|
||||||
|
max: number,
|
||||||
|
comparator: (x: number) => number
|
||||||
|
) {
|
||||||
|
let mid = 0
|
||||||
|
while (true) {
|
||||||
|
mid = min + (max - min) / 2
|
||||||
|
|
||||||
|
// Break once we've reached max precision.
|
||||||
|
if (mid === min || mid === max) break
|
||||||
|
|
||||||
|
const comparison = comparator(mid)
|
||||||
|
if (comparison === 0) break
|
||||||
|
else if (comparison > 0) {
|
||||||
|
max = mid
|
||||||
|
} else {
|
||||||
|
min = mid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mid
|
||||||
|
}
|
|
@ -34,3 +34,17 @@ export function median(xs: number[]) {
|
||||||
export function average(xs: number[]) {
|
export function average(xs: number[]) {
|
||||||
return sum(xs) / xs.length
|
return sum(xs) / xs.length
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const EPSILON = 0.00000001
|
||||||
|
|
||||||
|
export function floatingEqual(a: number, b: number, epsilon = EPSILON) {
|
||||||
|
return Math.abs(a - b) < epsilon
|
||||||
|
}
|
||||||
|
|
||||||
|
export function floatingGreaterEqual(a: number, b: number, epsilon = EPSILON) {
|
||||||
|
return a + epsilon >= b
|
||||||
|
}
|
||||||
|
|
||||||
|
export function floatingLesserEqual(a: number, b: number, epsilon = EPSILON) {
|
||||||
|
return a - epsilon <= b
|
||||||
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { PrivateUser } from '../../common/user'
|
||||||
import {
|
import {
|
||||||
CORS_ORIGIN_MANIFOLD,
|
CORS_ORIGIN_MANIFOLD,
|
||||||
CORS_ORIGIN_LOCALHOST,
|
CORS_ORIGIN_LOCALHOST,
|
||||||
|
CORS_ORIGIN_VERCEL,
|
||||||
} from '../../common/envs/constants'
|
} from '../../common/envs/constants'
|
||||||
|
|
||||||
type Output = Record<string, unknown>
|
type Output = Record<string, unknown>
|
||||||
|
@ -118,7 +119,7 @@ const DEFAULT_OPTS = {
|
||||||
concurrency: 100,
|
concurrency: 100,
|
||||||
memory: '2GiB',
|
memory: '2GiB',
|
||||||
cpu: 1,
|
cpu: 1,
|
||||||
cors: [CORS_ORIGIN_MANIFOLD, CORS_ORIGIN_LOCALHOST],
|
cors: [CORS_ORIGIN_MANIFOLD, CORS_ORIGIN_VERCEL, CORS_ORIGIN_LOCALHOST],
|
||||||
}
|
}
|
||||||
|
|
||||||
export const newEndpoint = (endpointOpts: EndpointOptions, fn: Handler) => {
|
export const newEndpoint = (endpointOpts: EndpointOptions, fn: Handler) => {
|
||||||
|
|
35
functions/src/cancel-bet.ts
Normal file
35
functions/src/cancel-bet.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import * as admin from 'firebase-admin'
|
||||||
|
import { z } from 'zod'
|
||||||
|
import { APIError, newEndpoint, validate } from './api'
|
||||||
|
import { LimitBet } from '../../common/bet'
|
||||||
|
|
||||||
|
const bodySchema = z.object({
|
||||||
|
betId: z.string(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const cancelbet = newEndpoint({}, async (req, auth) => {
|
||||||
|
const { betId } = validate(bodySchema, req.body)
|
||||||
|
|
||||||
|
const result = await firestore.runTransaction(async (trans) => {
|
||||||
|
const snap = await trans.get(
|
||||||
|
firestore.collectionGroup('bets').where('id', '==', betId)
|
||||||
|
)
|
||||||
|
const betDoc = snap.docs[0]
|
||||||
|
if (!betDoc?.exists) throw new APIError(400, 'Bet not found.')
|
||||||
|
|
||||||
|
const bet = betDoc.data() as LimitBet
|
||||||
|
if (bet.userId !== auth.uid)
|
||||||
|
throw new APIError(400, 'Not authorized to cancel bet.')
|
||||||
|
if (bet.limitProb === undefined)
|
||||||
|
throw new APIError(400, 'Not a limit bet: Cannot cancel.')
|
||||||
|
if (bet.isCancelled) throw new APIError(400, 'Bet already cancelled.')
|
||||||
|
|
||||||
|
trans.update(betDoc.ref, { isCancelled: true })
|
||||||
|
|
||||||
|
return { ...bet, isCancelled: true }
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
|
||||||
|
const firestore = admin.firestore()
|
|
@ -10,7 +10,7 @@ import { Contract } from '../../common/contract'
|
||||||
import { getUserByUsername, getValues } from './utils'
|
import { getUserByUsername, getValues } from './utils'
|
||||||
import { Comment } from '../../common/comment'
|
import { Comment } from '../../common/comment'
|
||||||
import { uniq } from 'lodash'
|
import { uniq } from 'lodash'
|
||||||
import { Bet } from '../../common/bet'
|
import { Bet, LimitBet } from '../../common/bet'
|
||||||
import { Answer } from '../../common/answer'
|
import { Answer } from '../../common/answer'
|
||||||
import { getContractBetMetrics } from '../../common/calculate'
|
import { getContractBetMetrics } from '../../common/calculate'
|
||||||
import { removeUndefinedProps } from '../../common/util/object'
|
import { removeUndefinedProps } from '../../common/util/object'
|
||||||
|
@ -382,3 +382,37 @@ export const createTipNotification = async (
|
||||||
}
|
}
|
||||||
return await notificationRef.set(removeUndefinedProps(notification))
|
return await notificationRef.set(removeUndefinedProps(notification))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const createBetFillNotification = async (
|
||||||
|
fromUser: User,
|
||||||
|
toUser: User,
|
||||||
|
bet: Bet,
|
||||||
|
userBet: LimitBet,
|
||||||
|
contract: Contract,
|
||||||
|
idempotencyKey: string
|
||||||
|
) => {
|
||||||
|
const fill = userBet.fills.find((fill) => fill.matchedBetId === bet.id)
|
||||||
|
const fillAmount = fill?.amount ?? 0
|
||||||
|
|
||||||
|
const notificationRef = firestore
|
||||||
|
.collection(`/users/${toUser.id}/notifications`)
|
||||||
|
.doc(idempotencyKey)
|
||||||
|
const notification: Notification = {
|
||||||
|
id: idempotencyKey,
|
||||||
|
userId: toUser.id,
|
||||||
|
reason: 'bet_fill',
|
||||||
|
createdTime: Date.now(),
|
||||||
|
isSeen: false,
|
||||||
|
sourceId: userBet.id,
|
||||||
|
sourceType: 'bet',
|
||||||
|
sourceUpdateType: 'updated',
|
||||||
|
sourceUserName: fromUser.name,
|
||||||
|
sourceUserUsername: fromUser.username,
|
||||||
|
sourceUserAvatarUrl: fromUser.avatarUrl,
|
||||||
|
sourceText: fillAmount.toString(),
|
||||||
|
sourceContractCreatorUsername: contract.creatorUsername,
|
||||||
|
sourceContractTitle: contract.question,
|
||||||
|
sourceContractSlug: contract.slug,
|
||||||
|
}
|
||||||
|
return await notificationRef.set(removeUndefinedProps(notification))
|
||||||
|
}
|
||||||
|
|
|
@ -31,6 +31,7 @@ export * from './transact'
|
||||||
export * from './change-user-info'
|
export * from './change-user-info'
|
||||||
export * from './create-answer'
|
export * from './create-answer'
|
||||||
export * from './place-bet'
|
export * from './place-bet'
|
||||||
|
export * from './cancel-bet'
|
||||||
export * from './sell-bet'
|
export * from './sell-bet'
|
||||||
export * from './sell-shares'
|
export * from './sell-shares'
|
||||||
export * from './claim-manalink'
|
export * from './claim-manalink'
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
import * as functions from 'firebase-functions'
|
import * as functions from 'firebase-functions'
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
|
import { keyBy } from 'lodash'
|
||||||
|
|
||||||
import { Bet } from '../../common/bet'
|
import { Bet, LimitBet } from '../../common/bet'
|
||||||
|
import { getContract, getUser, getValues } from './utils'
|
||||||
|
import { createBetFillNotification } from './create-notification'
|
||||||
|
import { filterDefined } from '../../common/util/array'
|
||||||
|
|
||||||
const firestore = admin.firestore()
|
const firestore = admin.firestore()
|
||||||
|
|
||||||
|
@ -11,6 +15,8 @@ export const onCreateBet = functions.firestore
|
||||||
const { contractId } = context.params as {
|
const { contractId } = context.params as {
|
||||||
contractId: string
|
contractId: string
|
||||||
}
|
}
|
||||||
|
const { eventId } = context
|
||||||
|
|
||||||
const bet = change.data() as Bet
|
const bet = change.data() as Bet
|
||||||
const lastBetTime = bet.createdTime
|
const lastBetTime = bet.createdTime
|
||||||
|
|
||||||
|
@ -18,4 +24,47 @@ export const onCreateBet = functions.firestore
|
||||||
.collection('contracts')
|
.collection('contracts')
|
||||||
.doc(contractId)
|
.doc(contractId)
|
||||||
.update({ lastBetTime, lastUpdatedTime: Date.now() })
|
.update({ lastBetTime, lastUpdatedTime: Date.now() })
|
||||||
|
|
||||||
|
await notifyFills(bet, contractId, eventId)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const notifyFills = async (bet: Bet, contractId: string, eventId: string) => {
|
||||||
|
if (!bet.fills) return
|
||||||
|
|
||||||
|
const user = await getUser(bet.userId)
|
||||||
|
if (!user) return
|
||||||
|
const contract = await getContract(contractId)
|
||||||
|
if (!contract) return
|
||||||
|
|
||||||
|
const matchedFills = bet.fills.filter((fill) => fill.matchedBetId !== null)
|
||||||
|
const matchedBets = (
|
||||||
|
await Promise.all(
|
||||||
|
matchedFills.map((fill) =>
|
||||||
|
getValues<LimitBet>(
|
||||||
|
firestore.collectionGroup('bets').where('id', '==', fill.matchedBetId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).flat()
|
||||||
|
|
||||||
|
const betUsers = await Promise.all(
|
||||||
|
matchedBets.map((bet) => getUser(bet.userId))
|
||||||
|
)
|
||||||
|
const betUsersById = keyBy(filterDefined(betUsers), 'id')
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
matchedBets.map((matchedBet) => {
|
||||||
|
const matchedUser = betUsersById[matchedBet.userId]
|
||||||
|
if (!matchedUser) return
|
||||||
|
|
||||||
|
return createBetFillNotification(
|
||||||
|
user,
|
||||||
|
matchedUser,
|
||||||
|
bet,
|
||||||
|
matchedBet,
|
||||||
|
contract,
|
||||||
|
eventId
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -5,6 +5,8 @@ import { HOUSE_LIQUIDITY_PROVIDER_ID } from '../../common/antes'
|
||||||
import { createNotification } from './create-notification'
|
import { createNotification } 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'
|
||||||
const firestore = admin.firestore()
|
const firestore = admin.firestore()
|
||||||
|
|
||||||
export const onUpdateUser = functions.firestore
|
export const onUpdateUser = functions.firestore
|
||||||
|
@ -17,6 +19,10 @@ 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) {
|
||||||
|
@ -109,3 +115,15 @@ 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 }))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -1,17 +1,25 @@
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
import {
|
||||||
|
DocumentReference,
|
||||||
|
FieldValue,
|
||||||
|
Query,
|
||||||
|
Transaction,
|
||||||
|
} from 'firebase-admin/firestore'
|
||||||
|
import { groupBy, mapValues, sumBy } from 'lodash'
|
||||||
|
|
||||||
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'
|
||||||
import { User } from '../../common/user'
|
import { User } from '../../common/user'
|
||||||
import {
|
import {
|
||||||
BetInfo,
|
BetInfo,
|
||||||
getNewBinaryCpmmBetInfo,
|
getBinaryCpmmBetInfo,
|
||||||
getNewBinaryDpmBetInfo,
|
|
||||||
getNewMultiBetInfo,
|
getNewMultiBetInfo,
|
||||||
getNumericBetsInfo,
|
getNumericBetsInfo,
|
||||||
} from '../../common/new-bet'
|
} from '../../common/new-bet'
|
||||||
import { addObjects, removeUndefinedProps } from '../../common/util/object'
|
import { addObjects, removeUndefinedProps } from '../../common/util/object'
|
||||||
|
import { LimitBet } from '../../common/bet'
|
||||||
|
import { floatingEqual } from '../../common/util/math'
|
||||||
import { redeemShares } from './redeem-shares'
|
import { redeemShares } from './redeem-shares'
|
||||||
import { log } from './utils'
|
import { log } from './utils'
|
||||||
|
|
||||||
|
@ -22,6 +30,7 @@ const bodySchema = z.object({
|
||||||
|
|
||||||
const binarySchema = z.object({
|
const binarySchema = z.object({
|
||||||
outcome: z.enum(['YES', 'NO']),
|
outcome: z.enum(['YES', 'NO']),
|
||||||
|
limitProb: z.number().gte(0.001).lte(0.999).optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const freeResponseSchema = z.object({
|
const freeResponseSchema = z.object({
|
||||||
|
@ -63,16 +72,30 @@ export const placebet = newEndpoint({}, async (req, auth) => {
|
||||||
newTotalBets,
|
newTotalBets,
|
||||||
newTotalLiquidity,
|
newTotalLiquidity,
|
||||||
newP,
|
newP,
|
||||||
} = await (async (): Promise<BetInfo> => {
|
makers,
|
||||||
if (outcomeType == 'BINARY' && mechanism == 'dpm-2') {
|
} = await (async (): Promise<
|
||||||
const { outcome } = validate(binarySchema, req.body)
|
BetInfo & {
|
||||||
return getNewBinaryDpmBetInfo(outcome, amount, contract, loanAmount)
|
makers?: maker[]
|
||||||
} else if (
|
}
|
||||||
(outcomeType == 'BINARY' || outcomeType == 'PSEUDO_NUMERIC') &&
|
> => {
|
||||||
|
if (
|
||||||
|
(outcomeType == 'BINARY' || outcomeType === 'PSEUDO_NUMERIC') &&
|
||||||
mechanism == 'cpmm-1'
|
mechanism == 'cpmm-1'
|
||||||
) {
|
) {
|
||||||
const { outcome } = validate(binarySchema, req.body)
|
const { outcome, limitProb } = validate(binarySchema, req.body)
|
||||||
return getNewBinaryCpmmBetInfo(outcome, amount, contract, loanAmount)
|
|
||||||
|
const unfilledBetsSnap = await trans.get(
|
||||||
|
getUnfilledBetsQuery(contractDoc)
|
||||||
|
)
|
||||||
|
const unfilledBets = unfilledBetsSnap.docs.map((doc) => doc.data())
|
||||||
|
|
||||||
|
return getBinaryCpmmBetInfo(
|
||||||
|
outcome,
|
||||||
|
amount,
|
||||||
|
contract,
|
||||||
|
limitProb,
|
||||||
|
unfilledBets
|
||||||
|
)
|
||||||
} else if (outcomeType == 'FREE_RESPONSE' && mechanism == 'dpm-2') {
|
} else if (outcomeType == 'FREE_RESPONSE' && mechanism == 'dpm-2') {
|
||||||
const { outcome } = validate(freeResponseSchema, req.body)
|
const { outcome } = validate(freeResponseSchema, req.body)
|
||||||
const answerDoc = contractDoc.collection('answers').doc(outcome)
|
const answerDoc = contractDoc.collection('answers').doc(outcome)
|
||||||
|
@ -97,11 +120,15 @@ export const placebet = newEndpoint({}, async (req, auth) => {
|
||||||
throw new APIError(400, 'Bet too large for current liquidity pool.')
|
throw new APIError(400, 'Bet too large for current liquidity pool.')
|
||||||
}
|
}
|
||||||
|
|
||||||
const newBalance = user.balance - amount - loanAmount
|
|
||||||
const betDoc = contractDoc.collection('bets').doc()
|
const betDoc = contractDoc.collection('bets').doc()
|
||||||
trans.create(betDoc, { id: betDoc.id, userId: user.id, ...newBet })
|
trans.create(betDoc, { id: betDoc.id, userId: user.id, ...newBet })
|
||||||
log('Created new bet document.')
|
log('Created new bet document.')
|
||||||
trans.update(userDoc, { balance: newBalance })
|
|
||||||
|
if (makers) {
|
||||||
|
updateMakers(makers, betDoc.id, contractDoc, trans)
|
||||||
|
}
|
||||||
|
|
||||||
|
trans.update(userDoc, { balance: FieldValue.increment(-newBet.amount) })
|
||||||
log('Updated user balance.')
|
log('Updated user balance.')
|
||||||
trans.update(
|
trans.update(
|
||||||
contractDoc,
|
contractDoc,
|
||||||
|
@ -112,7 +139,7 @@ export const placebet = newEndpoint({}, async (req, auth) => {
|
||||||
totalBets: newTotalBets,
|
totalBets: newTotalBets,
|
||||||
totalLiquidity: newTotalLiquidity,
|
totalLiquidity: newTotalLiquidity,
|
||||||
collectedFees: addObjects(newBet.fees, collectedFees),
|
collectedFees: addObjects(newBet.fees, collectedFees),
|
||||||
volume: volume + amount,
|
volume: volume + newBet.amount,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
log('Updated contract properties.')
|
log('Updated contract properties.')
|
||||||
|
@ -127,3 +154,54 @@ export const placebet = newEndpoint({}, async (req, auth) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
const firestore = admin.firestore()
|
const firestore = admin.firestore()
|
||||||
|
|
||||||
|
export const getUnfilledBetsQuery = (contractDoc: DocumentReference) => {
|
||||||
|
return contractDoc
|
||||||
|
.collection('bets')
|
||||||
|
.where('isFilled', '==', false)
|
||||||
|
.where('isCancelled', '==', false) as Query<LimitBet>
|
||||||
|
}
|
||||||
|
|
||||||
|
type maker = {
|
||||||
|
bet: LimitBet
|
||||||
|
amount: number
|
||||||
|
shares: number
|
||||||
|
timestamp: number
|
||||||
|
}
|
||||||
|
export const updateMakers = (
|
||||||
|
makers: maker[],
|
||||||
|
takerBetId: string,
|
||||||
|
contractDoc: DocumentReference,
|
||||||
|
trans: Transaction
|
||||||
|
) => {
|
||||||
|
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: takerBetId, timestamp }
|
||||||
|
})
|
||||||
|
const fills = [...bet.fills, ...newFills]
|
||||||
|
const totalShares = sumBy(fills, 'shares')
|
||||||
|
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) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -9,6 +9,9 @@ import { getCpmmSellBetInfo } from '../../common/sell-bet'
|
||||||
import { addObjects, removeUndefinedProps } from '../../common/util/object'
|
import { addObjects, removeUndefinedProps } from '../../common/util/object'
|
||||||
import { getValues } from './utils'
|
import { getValues } from './utils'
|
||||||
import { Bet } from '../../common/bet'
|
import { Bet } from '../../common/bet'
|
||||||
|
import { floatingLesserEqual } from '../../common/util/math'
|
||||||
|
import { getUnfilledBetsQuery, updateMakers } from './place-bet'
|
||||||
|
import { FieldValue } from 'firebase-admin/firestore'
|
||||||
|
|
||||||
const bodySchema = z.object({
|
const bodySchema = z.object({
|
||||||
contractId: z.string(),
|
contractId: z.string(),
|
||||||
|
@ -46,14 +49,22 @@ export const sellshares = newEndpoint({}, async (req, auth) => {
|
||||||
const outcomeBets = userBets.filter((bet) => bet.outcome == outcome)
|
const outcomeBets = userBets.filter((bet) => bet.outcome == outcome)
|
||||||
const maxShares = sumBy(outcomeBets, (bet) => bet.shares)
|
const maxShares = sumBy(outcomeBets, (bet) => bet.shares)
|
||||||
|
|
||||||
if (shares > maxShares)
|
if (!floatingLesserEqual(shares, maxShares))
|
||||||
throw new APIError(400, `You can only sell up to ${maxShares} shares.`)
|
throw new APIError(400, `You can only sell up to ${maxShares} shares.`)
|
||||||
|
|
||||||
const { newBet, newPool, newP, fees } = getCpmmSellBetInfo(
|
const soldShares = Math.min(shares, maxShares)
|
||||||
shares,
|
|
||||||
|
const unfilledBetsSnap = await transaction.get(
|
||||||
|
getUnfilledBetsQuery(contractDoc)
|
||||||
|
)
|
||||||
|
const unfilledBets = unfilledBetsSnap.docs.map((doc) => doc.data())
|
||||||
|
|
||||||
|
const { newBet, newPool, newP, fees, makers } = getCpmmSellBetInfo(
|
||||||
|
soldShares,
|
||||||
outcome,
|
outcome,
|
||||||
contract,
|
contract,
|
||||||
prevLoanAmount
|
prevLoanAmount,
|
||||||
|
unfilledBets
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
@ -65,11 +76,17 @@ export const sellshares = newEndpoint({}, async (req, auth) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const newBetDoc = firestore.collection(`contracts/${contractId}/bets`).doc()
|
const newBetDoc = firestore.collection(`contracts/${contractId}/bets`).doc()
|
||||||
const newBalance = user.balance - newBet.amount + (newBet.loanAmount ?? 0)
|
|
||||||
const userId = user.id
|
|
||||||
|
|
||||||
transaction.update(userDoc, { balance: newBalance })
|
updateMakers(makers, newBetDoc.id, contractDoc, transaction)
|
||||||
transaction.create(newBetDoc, { id: newBetDoc.id, userId, ...newBet })
|
|
||||||
|
transaction.update(userDoc, {
|
||||||
|
balance: FieldValue.increment(-newBet.amount),
|
||||||
|
})
|
||||||
|
transaction.create(newBetDoc, {
|
||||||
|
id: newBetDoc.id,
|
||||||
|
userId: user.id,
|
||||||
|
...newBet,
|
||||||
|
})
|
||||||
transaction.update(
|
transaction.update(
|
||||||
contractDoc,
|
contractDoc,
|
||||||
removeUndefinedProps({
|
removeUndefinedProps({
|
||||||
|
|
|
@ -1,13 +1,10 @@
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { partition, sumBy } from 'lodash'
|
import { partition, sumBy } from 'lodash'
|
||||||
|
import { SwitchHorizontalIcon } from '@heroicons/react/solid'
|
||||||
|
|
||||||
import { useUser } from 'web/hooks/use-user'
|
import { useUser } from 'web/hooks/use-user'
|
||||||
import {
|
import { CPMMBinaryContract, PseudoNumericContract } from 'common/contract'
|
||||||
BinaryContract,
|
|
||||||
CPMMBinaryContract,
|
|
||||||
PseudoNumericContract,
|
|
||||||
} from 'common/contract'
|
|
||||||
import { Col } from './layout/col'
|
import { Col } from './layout/col'
|
||||||
import { Row } from './layout/row'
|
import { Row } from './layout/row'
|
||||||
import { Spacer } from './layout/spacer'
|
import { Spacer } from './layout/spacer'
|
||||||
|
@ -18,20 +15,16 @@ import {
|
||||||
formatPercent,
|
formatPercent,
|
||||||
formatWithCommas,
|
formatWithCommas,
|
||||||
} from 'common/util/format'
|
} from 'common/util/format'
|
||||||
|
import { getBinaryCpmmBetInfo } from 'common/new-bet'
|
||||||
import { Title } from './title'
|
import { Title } from './title'
|
||||||
import { User } from 'web/lib/firebase/users'
|
import { User } from 'web/lib/firebase/users'
|
||||||
import { Bet } from 'common/bet'
|
import { Bet, LimitBet } from 'common/bet'
|
||||||
import { APIError, placeBet } from 'web/lib/firebase/api-call'
|
import { APIError, placeBet } from 'web/lib/firebase/api-call'
|
||||||
import { sellShares } from 'web/lib/firebase/api-call'
|
import { sellShares } from 'web/lib/firebase/api-call'
|
||||||
import { AmountInput, BuyAmountInput } from './amount-input'
|
import { AmountInput, BuyAmountInput } from './amount-input'
|
||||||
import { InfoTooltip } from './info-tooltip'
|
import { InfoTooltip } from './info-tooltip'
|
||||||
import { BinaryOutcomeLabel, PseudoNumericOutcomeLabel } from './outcome-label'
|
import { BinaryOutcomeLabel } from './outcome-label'
|
||||||
import {
|
import { getProbability } from 'common/calculate'
|
||||||
calculatePayoutAfterCorrectBet,
|
|
||||||
calculateShares,
|
|
||||||
getProbability,
|
|
||||||
getOutcomeProbabilityAfterBet,
|
|
||||||
} from 'common/calculate'
|
|
||||||
import { useFocus } from 'web/hooks/use-focus'
|
import { useFocus } from 'web/hooks/use-focus'
|
||||||
import { useUserContractBets } from 'web/hooks/use-user-bets'
|
import { useUserContractBets } from 'web/hooks/use-user-bets'
|
||||||
import {
|
import {
|
||||||
|
@ -39,178 +32,153 @@ import {
|
||||||
getCpmmProbability,
|
getCpmmProbability,
|
||||||
getCpmmFees,
|
getCpmmFees,
|
||||||
} from 'common/calculate-cpmm'
|
} from 'common/calculate-cpmm'
|
||||||
import { getFormattedMappedValue } from 'common/pseudo-numeric'
|
import {
|
||||||
|
getFormattedMappedValue,
|
||||||
|
getPseudoProbability,
|
||||||
|
} from 'common/pseudo-numeric'
|
||||||
import { SellRow } from './sell-row'
|
import { SellRow } from './sell-row'
|
||||||
import { useSaveShares } from './use-save-shares'
|
import { useSaveBinaryShares } from './use-save-binary-shares'
|
||||||
import { SignUpPrompt } from './sign-up-prompt'
|
import { SignUpPrompt } from './sign-up-prompt'
|
||||||
import { isIOS } from 'web/lib/util/device'
|
import { isIOS } from 'web/lib/util/device'
|
||||||
|
import { ProbabilityInput } from './probability-input'
|
||||||
import { track } from 'web/lib/service/analytics'
|
import { track } from 'web/lib/service/analytics'
|
||||||
|
import { removeUndefinedProps } from 'common/util/object'
|
||||||
|
import { useUnfilledBets } from 'web/hooks/use-bets'
|
||||||
|
import { LimitBets } from './limit-bets'
|
||||||
|
import { BucketInput } from './bucket-input'
|
||||||
|
|
||||||
export function BetPanel(props: {
|
export function BetPanel(props: {
|
||||||
contract: BinaryContract | PseudoNumericContract
|
contract: CPMMBinaryContract | PseudoNumericContract
|
||||||
className?: string
|
className?: string
|
||||||
}) {
|
}) {
|
||||||
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 { yesFloorShares, noFloorShares } = useSaveShares(contract, userBets)
|
const unfilledBets = useUnfilledBets(contract.id) ?? []
|
||||||
const sharesOutcome = yesFloorShares
|
const yourUnfilledBets = unfilledBets.filter((bet) => bet.userId === user?.id)
|
||||||
? 'YES'
|
const { sharesOutcome } = useSaveBinaryShares(contract, userBets)
|
||||||
: noFloorShares
|
|
||||||
? 'NO'
|
const [isLimitOrder, setIsLimitOrder] = useState(false)
|
||||||
: undefined
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col className={className}>
|
<Col className={className}>
|
||||||
<SellRow
|
<SellRow
|
||||||
contract={contract}
|
contract={contract}
|
||||||
user={user}
|
user={user}
|
||||||
className={'rounded-t-md bg-gray-100 px-6 py-6'}
|
className={'rounded-t-md bg-gray-100 px-4 py-5'}
|
||||||
/>
|
/>
|
||||||
<Col
|
<Col
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'rounded-b-md bg-white px-8 py-6',
|
'relative rounded-b-md bg-white px-8 py-6',
|
||||||
!sharesOutcome && 'rounded-t-md',
|
!sharesOutcome && 'rounded-t-md',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="mb-6 text-2xl">Place your bet</div>
|
<Row className="align-center justify-between">
|
||||||
{/* <Title className={clsx('!mt-0 text-neutral')} text="Place a trade" /> */}
|
<div className="mb-6 text-2xl">
|
||||||
|
{isLimitOrder ? <>Limit bet</> : <>Place your bet</>}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="btn btn-ghost btn-sm text-sm normal-case"
|
||||||
|
onClick={() => setIsLimitOrder(!isLimitOrder)}
|
||||||
|
>
|
||||||
|
<SwitchHorizontalIcon className="inline h-6 w-6" />
|
||||||
|
</button>
|
||||||
|
</Row>
|
||||||
|
|
||||||
<BuyPanel contract={contract} user={user} />
|
<BuyPanel
|
||||||
|
contract={contract}
|
||||||
|
user={user}
|
||||||
|
isLimitOrder={isLimitOrder}
|
||||||
|
unfilledBets={unfilledBets}
|
||||||
|
/>
|
||||||
|
|
||||||
<SignUpPrompt />
|
<SignUpPrompt />
|
||||||
</Col>
|
</Col>
|
||||||
|
{yourUnfilledBets.length > 0 && (
|
||||||
|
<LimitBets
|
||||||
|
className="mt-4"
|
||||||
|
contract={contract}
|
||||||
|
bets={yourUnfilledBets}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BetPanelSwitcher(props: {
|
export function SimpleBetPanel(props: {
|
||||||
contract: BinaryContract | PseudoNumericContract
|
contract: CPMMBinaryContract | PseudoNumericContract
|
||||||
className?: string
|
className?: string
|
||||||
title?: string // Set if BetPanel is on a feed modal
|
|
||||||
selected?: 'YES' | 'NO'
|
selected?: 'YES' | 'NO'
|
||||||
onBetSuccess?: () => void
|
onBetSuccess?: () => void
|
||||||
}) {
|
}) {
|
||||||
const { contract, className, title, selected, onBetSuccess } = props
|
const { contract, className, selected, onBetSuccess } = props
|
||||||
|
|
||||||
const { mechanism, outcomeType } = contract
|
|
||||||
const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC'
|
|
||||||
|
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
const userBets = useUserContractBets(user?.id, contract.id)
|
const [isLimitOrder, setIsLimitOrder] = useState(false)
|
||||||
|
|
||||||
const [tradeType, setTradeType] = useState<'BUY' | 'SELL'>('BUY')
|
const unfilledBets = useUnfilledBets(contract.id) ?? []
|
||||||
|
const yourUnfilledBets = unfilledBets.filter((bet) => bet.userId === user?.id)
|
||||||
const { yesFloorShares, noFloorShares, yesShares, noShares } = useSaveShares(
|
|
||||||
contract,
|
|
||||||
userBets
|
|
||||||
)
|
|
||||||
|
|
||||||
const floorShares = yesFloorShares || noFloorShares
|
|
||||||
const sharesOutcome = yesFloorShares
|
|
||||||
? 'YES'
|
|
||||||
: noFloorShares
|
|
||||||
? 'NO'
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Switch back to BUY if the user has sold all their shares.
|
|
||||||
if (tradeType === 'SELL' && sharesOutcome === undefined) {
|
|
||||||
setTradeType('BUY')
|
|
||||||
}
|
|
||||||
}, [tradeType, sharesOutcome])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col className={className}>
|
<Col className={className}>
|
||||||
{sharesOutcome && mechanism === 'cpmm-1' && (
|
<Col className={clsx('rounded-b-md rounded-t-md bg-white px-8 py-6')}>
|
||||||
<Col className="rounded-t-md bg-gray-100 px-6 py-6">
|
<Row className="justify-between">
|
||||||
<Row className="items-center justify-between gap-2">
|
<Title
|
||||||
<div>
|
className={clsx('!mt-0')}
|
||||||
You have {formatWithCommas(floorShares)}{' '}
|
text={isLimitOrder ? 'Limit bet' : 'Place a trade'}
|
||||||
{isPseudoNumeric ? (
|
|
||||||
<PseudoNumericOutcomeLabel outcome={sharesOutcome} />
|
|
||||||
) : (
|
|
||||||
<BinaryOutcomeLabel outcome={sharesOutcome} />
|
|
||||||
)}{' '}
|
|
||||||
shares
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{tradeType === 'BUY' && (
|
|
||||||
<button
|
|
||||||
className="btn btn-sm"
|
|
||||||
style={{
|
|
||||||
backgroundColor: 'white',
|
|
||||||
border: '2px solid',
|
|
||||||
color: '#3D4451',
|
|
||||||
}}
|
|
||||||
onClick={() =>
|
|
||||||
tradeType === 'BUY'
|
|
||||||
? setTradeType('SELL')
|
|
||||||
: setTradeType('BUY')
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{tradeType === 'BUY' ? 'Sell' : 'Bet'}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</Row>
|
|
||||||
</Col>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Col
|
|
||||||
className={clsx(
|
|
||||||
'rounded-b-md bg-white px-8 py-6',
|
|
||||||
!sharesOutcome && 'rounded-t-md'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Title
|
|
||||||
className={clsx(
|
|
||||||
'!mt-0',
|
|
||||||
tradeType === 'BUY' && title ? '!text-xl' : ''
|
|
||||||
)}
|
|
||||||
text={tradeType === 'BUY' ? title ?? 'Place a trade' : 'Sell shares'}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{tradeType === 'SELL' &&
|
|
||||||
mechanism == 'cpmm-1' &&
|
|
||||||
user &&
|
|
||||||
sharesOutcome && (
|
|
||||||
<SellPanel
|
|
||||||
contract={contract}
|
|
||||||
shares={yesShares || noShares}
|
|
||||||
sharesOutcome={sharesOutcome}
|
|
||||||
user={user}
|
|
||||||
userBets={userBets ?? []}
|
|
||||||
onSellSuccess={onBetSuccess}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{tradeType === 'BUY' && (
|
|
||||||
<BuyPanel
|
|
||||||
contract={contract}
|
|
||||||
user={user}
|
|
||||||
selected={selected}
|
|
||||||
onBuySuccess={onBetSuccess}
|
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
|
<button
|
||||||
|
className="btn btn-ghost btn-sm text-sm normal-case"
|
||||||
|
onClick={() => setIsLimitOrder(!isLimitOrder)}
|
||||||
|
>
|
||||||
|
<SwitchHorizontalIcon className="inline h-6 w-6" />
|
||||||
|
</button>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<BuyPanel
|
||||||
|
contract={contract}
|
||||||
|
user={user}
|
||||||
|
unfilledBets={unfilledBets}
|
||||||
|
selected={selected}
|
||||||
|
onBuySuccess={onBetSuccess}
|
||||||
|
isLimitOrder={isLimitOrder}
|
||||||
|
/>
|
||||||
|
|
||||||
<SignUpPrompt />
|
<SignUpPrompt />
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
|
{yourUnfilledBets.length > 0 && (
|
||||||
|
<LimitBets
|
||||||
|
className="mt-4"
|
||||||
|
contract={contract}
|
||||||
|
bets={yourUnfilledBets}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function BuyPanel(props: {
|
function BuyPanel(props: {
|
||||||
contract: BinaryContract | PseudoNumericContract
|
contract: CPMMBinaryContract | PseudoNumericContract
|
||||||
user: User | null | undefined
|
user: User | null | undefined
|
||||||
|
unfilledBets: Bet[]
|
||||||
|
isLimitOrder?: boolean
|
||||||
selected?: 'YES' | 'NO'
|
selected?: 'YES' | 'NO'
|
||||||
onBuySuccess?: () => void
|
onBuySuccess?: () => void
|
||||||
}) {
|
}) {
|
||||||
const { contract, user, selected, onBuySuccess } = props
|
const { contract, user, unfilledBets, isLimitOrder, selected, onBuySuccess } =
|
||||||
|
props
|
||||||
|
|
||||||
|
const initialProb = getProbability(contract)
|
||||||
const isPseudoNumeric = contract.outcomeType === 'PSEUDO_NUMERIC'
|
const isPseudoNumeric = contract.outcomeType === 'PSEUDO_NUMERIC'
|
||||||
|
|
||||||
const [betChoice, setBetChoice] = useState<'YES' | 'NO' | undefined>(selected)
|
const [betChoice, setBetChoice] = useState<'YES' | 'NO' | undefined>(selected)
|
||||||
const [betAmount, setBetAmount] = useState<number | undefined>(undefined)
|
const [betAmount, setBetAmount] = useState<number | undefined>(undefined)
|
||||||
|
const [limitProb, setLimitProb] = useState<number | undefined>(
|
||||||
|
Math.round(100 * initialProb)
|
||||||
|
)
|
||||||
const [error, setError] = useState<string | undefined>()
|
const [error, setError] = useState<string | undefined>()
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
const [wasSubmitted, setWasSubmitted] = useState(false)
|
const [wasSubmitted, setWasSubmitted] = useState(false)
|
||||||
|
@ -240,15 +208,22 @@ function BuyPanel(props: {
|
||||||
|
|
||||||
async function submitBet() {
|
async function submitBet() {
|
||||||
if (!user || !betAmount) return
|
if (!user || !betAmount) return
|
||||||
|
if (isLimitOrder && limitProb === undefined) return
|
||||||
|
|
||||||
|
const limitProbScaled =
|
||||||
|
isLimitOrder && limitProb !== undefined ? limitProb / 100 : undefined
|
||||||
|
|
||||||
setError(undefined)
|
setError(undefined)
|
||||||
setIsSubmitting(true)
|
setIsSubmitting(true)
|
||||||
|
|
||||||
placeBet({
|
placeBet(
|
||||||
amount: betAmount,
|
removeUndefinedProps({
|
||||||
outcome: betChoice,
|
amount: betAmount,
|
||||||
contractId: contract.id,
|
outcome: betChoice,
|
||||||
})
|
contractId: contract.id,
|
||||||
|
limitProb: limitProbScaled,
|
||||||
|
})
|
||||||
|
)
|
||||||
.then((r) => {
|
.then((r) => {
|
||||||
console.log('placed bet. Result:', r)
|
console.log('placed bet. Result:', r)
|
||||||
setIsSubmitting(false)
|
setIsSubmitting(false)
|
||||||
|
@ -278,42 +253,31 @@ function BuyPanel(props: {
|
||||||
|
|
||||||
const betDisabled = isSubmitting || !betAmount || error
|
const betDisabled = isSubmitting || !betAmount || error
|
||||||
|
|
||||||
const initialProb = getProbability(contract)
|
const limitProbFrac = (limitProb ?? 0) / 100
|
||||||
|
|
||||||
const outcomeProb = getOutcomeProbabilityAfterBet(
|
const { newPool, newP, newBet } = getBinaryCpmmBetInfo(
|
||||||
|
betChoice ?? 'YES',
|
||||||
|
betAmount ?? 0,
|
||||||
contract,
|
contract,
|
||||||
betChoice || 'YES',
|
isLimitOrder ? limitProbFrac : undefined,
|
||||||
betAmount ?? 0
|
unfilledBets as LimitBet[]
|
||||||
)
|
)
|
||||||
const resultProb = betChoice === 'NO' ? 1 - outcomeProb : outcomeProb
|
|
||||||
|
|
||||||
const shares = calculateShares(contract, betAmount ?? 0, betChoice || 'YES')
|
const resultProb = getCpmmProbability(newPool, newP)
|
||||||
|
const remainingMatched = isLimitOrder
|
||||||
const currentPayout = betAmount
|
? ((newBet.orderAmount ?? 0) - newBet.amount) /
|
||||||
? calculatePayoutAfterCorrectBet(contract, {
|
(betChoice === 'YES' ? limitProbFrac : 1 - limitProbFrac)
|
||||||
outcome: betChoice,
|
|
||||||
amount: betAmount,
|
|
||||||
shares,
|
|
||||||
} as Bet)
|
|
||||||
: 0
|
: 0
|
||||||
|
const currentPayout = newBet.shares + remainingMatched
|
||||||
|
|
||||||
const currentReturn = betAmount ? (currentPayout - betAmount) / betAmount : 0
|
const currentReturn = betAmount ? (currentPayout - betAmount) / betAmount : 0
|
||||||
const currentReturnPercent = formatPercent(currentReturn)
|
const currentReturnPercent = formatPercent(currentReturn)
|
||||||
|
|
||||||
const cpmmFees =
|
const cpmmFees = getCpmmFees(
|
||||||
contract.mechanism === 'cpmm-1' &&
|
contract,
|
||||||
getCpmmFees(contract, betAmount ?? 0, betChoice ?? 'YES').totalFees
|
betAmount ?? 0,
|
||||||
|
betChoice ?? 'YES'
|
||||||
const dpmTooltip =
|
).totalFees
|
||||||
contract.mechanism === 'dpm-2'
|
|
||||||
? `Current payout for ${formatWithCommas(shares)} / ${formatWithCommas(
|
|
||||||
shares +
|
|
||||||
contract.totalShares[betChoice ?? 'YES'] -
|
|
||||||
(contract.phantomShares
|
|
||||||
? contract.phantomShares[betChoice ?? 'YES']
|
|
||||||
: 0)
|
|
||||||
)} ${betChoice ?? 'YES'} shares`
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
const format = getFormattedMappedValue(contract)
|
const format = getFormattedMappedValue(contract)
|
||||||
|
|
||||||
|
@ -336,29 +300,62 @@ function BuyPanel(props: {
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
inputRef={inputRef}
|
inputRef={inputRef}
|
||||||
/>
|
/>
|
||||||
|
{isLimitOrder && (
|
||||||
|
<>
|
||||||
|
<Row className="my-3 items-center gap-2 text-left text-sm text-gray-500">
|
||||||
|
Limit {isPseudoNumeric ? 'value' : 'probability'}
|
||||||
|
<InfoTooltip
|
||||||
|
text={`Bet ${betChoice === 'NO' ? 'down' : 'up'} to this ${
|
||||||
|
isPseudoNumeric ? 'value' : 'probability'
|
||||||
|
} and wait to match other bets.`}
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
{isPseudoNumeric ? (
|
||||||
|
<BucketInput
|
||||||
|
contract={contract}
|
||||||
|
onBucketChange={(value) =>
|
||||||
|
setLimitProb(
|
||||||
|
value === undefined
|
||||||
|
? undefined
|
||||||
|
: 100 *
|
||||||
|
getPseudoProbability(
|
||||||
|
value,
|
||||||
|
contract.min,
|
||||||
|
contract.max,
|
||||||
|
contract.isLogScale
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
isSubmitting={isSubmitting}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ProbabilityInput
|
||||||
|
inputClassName="w-full max-w-none"
|
||||||
|
prob={limitProb}
|
||||||
|
onChange={setLimitProb}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<Col className="mt-3 w-full gap-3">
|
<Col className="mt-3 w-full gap-3">
|
||||||
<Row className="items-center justify-between text-sm">
|
{!isLimitOrder && (
|
||||||
<div className="text-gray-500">
|
<Row className="items-center justify-between text-sm">
|
||||||
{isPseudoNumeric ? 'Estimated value' : 'Probability'}
|
<div className="text-gray-500">
|
||||||
</div>
|
{isPseudoNumeric ? 'Estimated value' : 'Probability'}
|
||||||
<div>
|
</div>
|
||||||
{format(initialProb)}
|
<div>
|
||||||
<span className="mx-2">→</span>
|
{format(initialProb)}
|
||||||
{format(resultProb)}
|
<span className="mx-2">→</span>
|
||||||
</div>
|
{format(resultProb)}
|
||||||
</Row>
|
</div>
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
|
||||||
<Row className="items-center justify-between gap-2 text-sm">
|
<Row className="items-center justify-between gap-2 text-sm">
|
||||||
<Row className="flex-nowrap items-center gap-2 whitespace-nowrap text-gray-500">
|
<Row className="flex-nowrap items-center gap-2 whitespace-nowrap text-gray-500">
|
||||||
<div>
|
<div>
|
||||||
{contract.mechanism === 'dpm-2' ? (
|
{isPseudoNumeric ? (
|
||||||
<>
|
|
||||||
Estimated
|
|
||||||
<br /> payout if{' '}
|
|
||||||
<BinaryOutcomeLabel outcome={betChoice ?? 'YES'} />
|
|
||||||
</>
|
|
||||||
) : isPseudoNumeric ? (
|
|
||||||
'Max payout'
|
'Max payout'
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
@ -366,14 +363,9 @@ function BuyPanel(props: {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<InfoTooltip
|
||||||
{cpmmFees !== false && (
|
text={`Includes ${formatMoneyWithDecimals(cpmmFees)} in fees`}
|
||||||
<InfoTooltip
|
/>
|
||||||
text={`Includes ${formatMoneyWithDecimals(cpmmFees)} in fees`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{dpmTooltip && <InfoTooltip text={dpmTooltip} />}
|
|
||||||
</Row>
|
</Row>
|
||||||
<div>
|
<div>
|
||||||
<span className="mr-2 whitespace-nowrap">
|
<span className="mr-2 whitespace-nowrap">
|
||||||
|
@ -424,19 +416,21 @@ 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 betDisabled = isSubmitting || !amount || error
|
const betDisabled = isSubmitting || !amount || error
|
||||||
|
|
||||||
|
// Sell all shares if remaining shares would be < 1
|
||||||
|
const sellQuantity = amount === Math.floor(shares) ? shares : amount
|
||||||
|
|
||||||
async function submitSell() {
|
async function submitSell() {
|
||||||
if (!user || !amount) return
|
if (!user || !amount) return
|
||||||
|
|
||||||
setError(undefined)
|
setError(undefined)
|
||||||
setIsSubmitting(true)
|
setIsSubmitting(true)
|
||||||
|
|
||||||
// Sell all shares if remaining shares would be < 1
|
|
||||||
const sellAmount = amount === Math.floor(shares) ? shares : amount
|
|
||||||
|
|
||||||
await sellShares({
|
await sellShares({
|
||||||
shares: sellAmount,
|
shares: sellQuantity,
|
||||||
outcome: sharesOutcome,
|
outcome: sharesOutcome,
|
||||||
contractId: contract.id,
|
contractId: contract.id,
|
||||||
})
|
})
|
||||||
|
@ -461,18 +455,19 @@ export function SellPanel(props: {
|
||||||
outcomeType: contract.outcomeType,
|
outcomeType: contract.outcomeType,
|
||||||
slug: contract.slug,
|
slug: contract.slug,
|
||||||
contractId: contract.id,
|
contractId: contract.id,
|
||||||
shares: sellAmount,
|
shares: sellQuantity,
|
||||||
outcome: sharesOutcome,
|
outcome: sharesOutcome,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialProb = getProbability(contract)
|
const initialProb = getProbability(contract)
|
||||||
const { newPool } = calculateCpmmSale(
|
const { cpmmState, saleValue } = calculateCpmmSale(
|
||||||
contract,
|
contract,
|
||||||
Math.min(amount ?? 0, shares),
|
sellQuantity ?? 0,
|
||||||
sharesOutcome
|
sharesOutcome,
|
||||||
|
unfilledBets
|
||||||
)
|
)
|
||||||
const resultProb = getCpmmProbability(newPool, contract.p)
|
const resultProb = getCpmmProbability(cpmmState.pool, cpmmState.p)
|
||||||
|
|
||||||
const openUserBets = userBets.filter((bet) => !bet.isSold && !bet.sale)
|
const openUserBets = userBets.filter((bet) => !bet.isSold && !bet.sale)
|
||||||
const [yesBets, noBets] = partition(
|
const [yesBets, noBets] = partition(
|
||||||
|
@ -484,17 +479,8 @@ export function SellPanel(props: {
|
||||||
sumBy(noBets, (bet) => bet.shares),
|
sumBy(noBets, (bet) => bet.shares),
|
||||||
]
|
]
|
||||||
|
|
||||||
const sellOutcome = yesShares ? 'YES' : noShares ? 'NO' : undefined
|
|
||||||
const ownedShares = Math.round(yesShares) || Math.round(noShares)
|
const ownedShares = Math.round(yesShares) || Math.round(noShares)
|
||||||
|
|
||||||
const sharesSold = Math.min(amount ?? 0, ownedShares)
|
|
||||||
|
|
||||||
const { saleValue } = calculateCpmmSale(
|
|
||||||
contract,
|
|
||||||
sharesSold,
|
|
||||||
sellOutcome as 'YES' | 'NO'
|
|
||||||
)
|
|
||||||
|
|
||||||
const onAmountChange = (amount: number | undefined) => {
|
const onAmountChange = (amount: number | undefined) => {
|
||||||
setAmount(amount)
|
setAmount(amount)
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
|
|
||||||
import { BetPanelSwitcher } from './bet-panel'
|
import { SimpleBetPanel } from './bet-panel'
|
||||||
import { YesNoSelector } from './yes-no-selector'
|
import { YesNoSelector } from './yes-no-selector'
|
||||||
import { BinaryContract, PseudoNumericContract } from 'common/contract'
|
import { CPMMBinaryContract, PseudoNumericContract } from 'common/contract'
|
||||||
import { Modal } from './layout/modal'
|
import { Modal } from './layout/modal'
|
||||||
import { SellButton } from './sell-button'
|
import { SellButton } from './sell-button'
|
||||||
import { useUser } from 'web/hooks/use-user'
|
import { useUser } from 'web/hooks/use-user'
|
||||||
import { useUserContractBets } from 'web/hooks/use-user-bets'
|
import { useUserContractBets } from 'web/hooks/use-user-bets'
|
||||||
import { useSaveShares } from './use-save-shares'
|
import { useSaveBinaryShares } from './use-save-binary-shares'
|
||||||
|
|
||||||
// Inline version of a bet panel. Opens BetPanel in a new modal.
|
// Inline version of a bet panel. Opens BetPanel in a new modal.
|
||||||
export default function BetRow(props: {
|
export default function BetRow(props: {
|
||||||
contract: BinaryContract | PseudoNumericContract
|
contract: CPMMBinaryContract | PseudoNumericContract
|
||||||
className?: string
|
className?: string
|
||||||
btnClassName?: string
|
btnClassName?: string
|
||||||
betPanelClassName?: string
|
betPanelClassName?: string
|
||||||
|
@ -24,10 +24,8 @@ export default function BetRow(props: {
|
||||||
)
|
)
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
const userBets = useUserContractBets(user?.id, contract.id)
|
const userBets = useUserContractBets(user?.id, contract.id)
|
||||||
const { yesFloorShares, noFloorShares, yesShares, noShares } = useSaveShares(
|
const { yesShares, noShares, hasYesShares, hasNoShares } =
|
||||||
contract,
|
useSaveBinaryShares(contract, userBets)
|
||||||
userBets
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -40,7 +38,7 @@ export default function BetRow(props: {
|
||||||
setBetChoice(choice)
|
setBetChoice(choice)
|
||||||
}}
|
}}
|
||||||
replaceNoButton={
|
replaceNoButton={
|
||||||
yesFloorShares > 0 ? (
|
hasYesShares ? (
|
||||||
<SellButton
|
<SellButton
|
||||||
panelClassName={betPanelClassName}
|
panelClassName={betPanelClassName}
|
||||||
contract={contract}
|
contract={contract}
|
||||||
|
@ -51,7 +49,7 @@ export default function BetRow(props: {
|
||||||
) : undefined
|
) : undefined
|
||||||
}
|
}
|
||||||
replaceYesButton={
|
replaceYesButton={
|
||||||
noFloorShares > 0 ? (
|
hasNoShares ? (
|
||||||
<SellButton
|
<SellButton
|
||||||
panelClassName={betPanelClassName}
|
panelClassName={betPanelClassName}
|
||||||
contract={contract}
|
contract={contract}
|
||||||
|
@ -63,10 +61,9 @@ export default function BetRow(props: {
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Modal open={open} setOpen={setOpen}>
|
<Modal open={open} setOpen={setOpen}>
|
||||||
<BetPanelSwitcher
|
<SimpleBetPanel
|
||||||
className={betPanelClassName}
|
className={betPanelClassName}
|
||||||
contract={contract}
|
contract={contract}
|
||||||
title={contract.question}
|
|
||||||
selected={betChoice}
|
selected={betChoice}
|
||||||
onBetSuccess={() => setOpen(false)}
|
onBetSuccess={() => setOpen(false)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -44,6 +44,9 @@ 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 { SellSharesModal } from './sell-modal'
|
import { SellSharesModal } from './sell-modal'
|
||||||
|
import { useUnfilledBets } from 'web/hooks/use-bets'
|
||||||
|
import { LimitBet } from 'common/bet'
|
||||||
|
import { floatingEqual } from 'common/util/math'
|
||||||
|
|
||||||
type BetSort = 'newest' | 'profit' | 'closeTime' | 'value'
|
type BetSort = 'newest' | 'profit' | 'closeTime' | 'value'
|
||||||
type BetFilter = 'open' | 'sold' | 'closed' | 'resolved' | 'all'
|
type BetFilter = 'open' | 'sold' | 'closed' | 'resolved' | 'all'
|
||||||
|
@ -390,6 +393,12 @@ export function BetsSummary(props: {
|
||||||
const [showSellModal, setShowSellModal] = useState(false)
|
const [showSellModal, setShowSellModal] = useState(false)
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
|
||||||
|
const sharesOutcome = floatingEqual(totalShares.YES, 0)
|
||||||
|
? floatingEqual(totalShares.NO, 0)
|
||||||
|
? undefined
|
||||||
|
: 'NO'
|
||||||
|
: 'YES'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row className={clsx('flex-wrap gap-4 sm:flex-nowrap sm:gap-6', className)}>
|
<Row className={clsx('flex-wrap gap-4 sm:flex-nowrap sm:gap-6', className)}>
|
||||||
<Row className="flex-wrap gap-4 sm:gap-6">
|
<Row className="flex-wrap gap-4 sm:gap-6">
|
||||||
|
@ -469,6 +478,7 @@ export function BetsSummary(props: {
|
||||||
!isClosed &&
|
!isClosed &&
|
||||||
!resolution &&
|
!resolution &&
|
||||||
hasShares &&
|
hasShares &&
|
||||||
|
sharesOutcome &&
|
||||||
user && (
|
user && (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
|
@ -482,8 +492,8 @@ export function BetsSummary(props: {
|
||||||
contract={contract}
|
contract={contract}
|
||||||
user={user}
|
user={user}
|
||||||
userBets={bets}
|
userBets={bets}
|
||||||
shares={totalShares.YES || totalShares.NO}
|
shares={totalShares[sharesOutcome]}
|
||||||
sharesOutcome={totalShares.YES ? 'YES' : 'NO'}
|
sharesOutcome={sharesOutcome}
|
||||||
setOpen={setShowSellModal}
|
setOpen={setShowSellModal}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -505,7 +515,7 @@ export function ContractBetsTable(props: {
|
||||||
const { contract, className, isYourBets } = props
|
const { contract, className, isYourBets } = props
|
||||||
|
|
||||||
const bets = sortBy(
|
const bets = sortBy(
|
||||||
props.bets.filter((b) => !b.isAnte),
|
props.bets.filter((b) => !b.isAnte && b.amount !== 0),
|
||||||
(bet) => bet.createdTime
|
(bet) => bet.createdTime
|
||||||
).reverse()
|
).reverse()
|
||||||
|
|
||||||
|
@ -531,6 +541,8 @@ 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) ?? []
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx('overflow-x-auto', className)}>
|
<div className={clsx('overflow-x-auto', className)}>
|
||||||
{amountRedeemed > 0 && (
|
{amountRedeemed > 0 && (
|
||||||
|
@ -577,6 +589,7 @@ export function ContractBetsTable(props: {
|
||||||
saleBet={salesDict[bet.id]}
|
saleBet={salesDict[bet.id]}
|
||||||
contract={contract}
|
contract={contract}
|
||||||
isYourBet={isYourBets}
|
isYourBet={isYourBets}
|
||||||
|
unfilledBets={unfilledBets}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -590,8 +603,9 @@ function BetRow(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
saleBet?: Bet
|
saleBet?: Bet
|
||||||
isYourBet: boolean
|
isYourBet: boolean
|
||||||
|
unfilledBets: LimitBet[]
|
||||||
}) {
|
}) {
|
||||||
const { bet, saleBet, contract, isYourBet } = props
|
const { bet, saleBet, contract, isYourBet, unfilledBets } = props
|
||||||
const {
|
const {
|
||||||
amount,
|
amount,
|
||||||
outcome,
|
outcome,
|
||||||
|
@ -621,7 +635,7 @@ function BetRow(props: {
|
||||||
formatMoney(
|
formatMoney(
|
||||||
isResolved
|
isResolved
|
||||||
? resolvedPayout(contract, bet)
|
? resolvedPayout(contract, bet)
|
||||||
: calculateSaleAmount(contract, bet)
|
: calculateSaleAmount(contract, bet, unfilledBets)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -681,9 +695,16 @@ function SellButton(props: { contract: Contract; bet: Bet }) {
|
||||||
outcome === 'NO' ? 'YES' : outcome
|
outcome === 'NO' ? 'YES' : outcome
|
||||||
)
|
)
|
||||||
|
|
||||||
const outcomeProb = getProbabilityAfterSale(contract, outcome, shares)
|
const unfilledBets = useUnfilledBets(contract.id) ?? []
|
||||||
|
|
||||||
const saleAmount = calculateSaleAmount(contract, bet)
|
const outcomeProb = getProbabilityAfterSale(
|
||||||
|
contract,
|
||||||
|
outcome,
|
||||||
|
shares,
|
||||||
|
unfilledBets
|
||||||
|
)
|
||||||
|
|
||||||
|
const saleAmount = calculateSaleAmount(contract, bet, unfilledBets)
|
||||||
const profit = saleAmount - bet.amount
|
const profit = saleAmount - bet.amount
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
|
||||||
import { NumericContract } from 'common/contract'
|
import { NumericContract, PseudoNumericContract } from 'common/contract'
|
||||||
import { getMappedBucket } from 'common/calculate-dpm'
|
import { getMappedBucket } from 'common/calculate-dpm'
|
||||||
|
|
||||||
import { NumberInput } from './number-input'
|
import { NumberInput } from './number-input'
|
||||||
|
|
||||||
export function BucketInput(props: {
|
export function BucketInput(props: {
|
||||||
contract: NumericContract
|
contract: NumericContract | PseudoNumericContract
|
||||||
isSubmitting?: boolean
|
isSubmitting?: boolean
|
||||||
onBucketChange: (value?: number, bucket?: string) => void
|
onBucketChange: (value?: number, bucket?: string) => void
|
||||||
}) {
|
}) {
|
||||||
|
@ -24,7 +24,10 @@ export function BucketInput(props: {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const bucket = getMappedBucket(value, contract)
|
const bucket =
|
||||||
|
contract.outcomeType === 'PSEUDO_NUMERIC'
|
||||||
|
? ''
|
||||||
|
: getMappedBucket(value, contract)
|
||||||
|
|
||||||
onBucketChange(value, bucket)
|
onBucketChange(value, bucket)
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,10 +52,7 @@ export function ContractCard(props: {
|
||||||
const showQuickBet =
|
const showQuickBet =
|
||||||
user &&
|
user &&
|
||||||
!marketClosed &&
|
!marketClosed &&
|
||||||
!(
|
(outcomeType === 'BINARY' || outcomeType === 'PSEUDO_NUMERIC') &&
|
||||||
outcomeType === 'FREE_RESPONSE' && getTopAnswer(contract) === undefined
|
|
||||||
) &&
|
|
||||||
outcomeType !== 'NUMERIC' &&
|
|
||||||
!hideQuickBet
|
!hideQuickBet
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -16,7 +16,7 @@ import {
|
||||||
import { Bet } from 'common/bet'
|
import { Bet } from 'common/bet'
|
||||||
import BetRow from '../bet-row'
|
import BetRow from '../bet-row'
|
||||||
import { AnswersGraph } from '../answers/answers-graph'
|
import { AnswersGraph } from '../answers/answers-graph'
|
||||||
import { Contract } from 'common/contract'
|
import { Contract, CPMMBinaryContract } from 'common/contract'
|
||||||
import { ContractDescription } from './contract-description'
|
import { ContractDescription } from './contract-description'
|
||||||
import { ContractDetails } from './contract-details'
|
import { ContractDetails } from './contract-details'
|
||||||
import { ShareMarket } from '../share-market'
|
import { ShareMarket } from '../share-market'
|
||||||
|
@ -70,6 +70,13 @@ export const ContractOverview = (props: {
|
||||||
<Row className="items-center justify-between gap-4 xl:hidden">
|
<Row className="items-center justify-between gap-4 xl:hidden">
|
||||||
<BinaryResolutionOrChance contract={contract} />
|
<BinaryResolutionOrChance contract={contract} />
|
||||||
|
|
||||||
|
{tradingAllowed(contract) && (
|
||||||
|
<BetRow contract={contract as CPMMBinaryContract} />
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
) : isPseudoNumeric ? (
|
||||||
|
<Row className="items-center justify-between gap-4 xl:hidden">
|
||||||
|
<PseudoNumericResolutionOrExpectation contract={contract} />
|
||||||
{tradingAllowed(contract) && <BetRow contract={contract} />}
|
{tradingAllowed(contract) && <BetRow contract={contract} />}
|
||||||
</Row>
|
</Row>
|
||||||
) : isPseudoNumeric ? (
|
) : isPseudoNumeric ? (
|
||||||
|
|
|
@ -7,7 +7,13 @@ import {
|
||||||
} from 'common/calculate'
|
} from 'common/calculate'
|
||||||
import { getExpectedValue } from 'common/calculate-dpm'
|
import { getExpectedValue } from 'common/calculate-dpm'
|
||||||
import { User } from 'common/user'
|
import { User } from 'common/user'
|
||||||
import { Contract, NumericContract, resolution } from 'common/contract'
|
import {
|
||||||
|
BinaryContract,
|
||||||
|
Contract,
|
||||||
|
NumericContract,
|
||||||
|
PseudoNumericContract,
|
||||||
|
resolution,
|
||||||
|
} from 'common/contract'
|
||||||
import {
|
import {
|
||||||
formatLargeNumber,
|
formatLargeNumber,
|
||||||
formatMoney,
|
formatMoney,
|
||||||
|
@ -22,33 +28,30 @@ import TriangleDownFillIcon from 'web/lib/icons/triangle-down-fill-icon'
|
||||||
import TriangleFillIcon from 'web/lib/icons/triangle-fill-icon'
|
import TriangleFillIcon from 'web/lib/icons/triangle-fill-icon'
|
||||||
import { Col } from '../layout/col'
|
import { Col } from '../layout/col'
|
||||||
import { OUTCOME_TO_COLOR } from '../outcome-label'
|
import { OUTCOME_TO_COLOR } from '../outcome-label'
|
||||||
import { useSaveShares } from '../use-save-shares'
|
import { useSaveBinaryShares } from '../use-save-binary-shares'
|
||||||
import { sellShares } from 'web/lib/firebase/api-call'
|
import { sellShares } from 'web/lib/firebase/api-call'
|
||||||
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'
|
||||||
|
|
||||||
const BET_SIZE = 10
|
const BET_SIZE = 10
|
||||||
|
|
||||||
export function QuickBet(props: { contract: Contract; user: User }) {
|
export function QuickBet(props: {
|
||||||
|
contract: BinaryContract | PseudoNumericContract
|
||||||
|
user: User
|
||||||
|
}) {
|
||||||
const { contract, user } = props
|
const { contract, user } = props
|
||||||
const { mechanism, outcomeType } = contract
|
const { mechanism, outcomeType } = contract
|
||||||
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 topAnswer =
|
const unfilledBets = useUnfilledBets(contract.id) ?? []
|
||||||
outcomeType === 'FREE_RESPONSE' ? getTopAnswer(contract) : undefined
|
|
||||||
|
|
||||||
// TODO: yes/no from useSaveShares doesn't work on numeric contracts
|
const { hasYesShares, hasNoShares, yesShares, noShares } =
|
||||||
const { yesFloorShares, noFloorShares, yesShares, noShares } = useSaveShares(
|
useSaveBinaryShares(contract, userBets)
|
||||||
contract,
|
const hasUpShares = hasYesShares
|
||||||
userBets,
|
const hasDownShares = hasNoShares && !hasUpShares
|
||||||
topAnswer?.number.toString() || undefined
|
|
||||||
)
|
|
||||||
const hasUpShares =
|
|
||||||
yesFloorShares || (noFloorShares && outcomeType === 'NUMERIC')
|
|
||||||
const hasDownShares =
|
|
||||||
noFloorShares && yesFloorShares <= 0 && outcomeType !== 'NUMERIC'
|
|
||||||
|
|
||||||
const [upHover, setUpHover] = useState(false)
|
const [upHover, setUpHover] = useState(false)
|
||||||
const [downHover, setDownHover] = useState(false)
|
const [downHover, setDownHover] = useState(false)
|
||||||
|
@ -85,13 +88,14 @@ export function QuickBet(props: { contract: Contract; user: User }) {
|
||||||
const maxSharesSold = BET_SIZE / (sellOutcome === 'YES' ? prob : 1 - prob)
|
const maxSharesSold = BET_SIZE / (sellOutcome === 'YES' ? prob : 1 - prob)
|
||||||
sharesSold = Math.min(oppositeShares, maxSharesSold)
|
sharesSold = Math.min(oppositeShares, maxSharesSold)
|
||||||
|
|
||||||
const { newPool, saleValue } = calculateCpmmSale(
|
const { cpmmState, saleValue } = calculateCpmmSale(
|
||||||
contract,
|
contract,
|
||||||
sharesSold,
|
sharesSold,
|
||||||
sellOutcome
|
sellOutcome,
|
||||||
|
unfilledBets
|
||||||
)
|
)
|
||||||
saleAmount = saleValue
|
saleAmount = saleValue
|
||||||
previewProb = getCpmmProbability(newPool, contract.p)
|
previewProb = getCpmmProbability(cpmmState.pool, cpmmState.p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,13 +135,6 @@ export function QuickBet(props: { contract: Contract; user: User }) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (outcomeType === 'FREE_RESPONSE')
|
|
||||||
return (
|
|
||||||
<Col className="relative -my-4 -mr-5 min-w-[5.5rem] justify-center gap-2 pr-5 pl-1 align-middle">
|
|
||||||
<QuickOutcomeView contract={contract} previewProb={previewProb} />
|
|
||||||
</Col>
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col
|
<Col
|
||||||
className={clsx(
|
className={clsx(
|
||||||
|
@ -158,7 +155,7 @@ export function QuickBet(props: { contract: Contract; user: User }) {
|
||||||
{formatMoney(10)}
|
{formatMoney(10)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{hasUpShares > 0 ? (
|
{hasUpShares ? (
|
||||||
<TriangleFillIcon
|
<TriangleFillIcon
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'mx-auto h-5 w-5',
|
'mx-auto h-5 w-5',
|
||||||
|
@ -193,7 +190,7 @@ export function QuickBet(props: { contract: Contract; user: User }) {
|
||||||
onMouseLeave={() => setDownHover(false)}
|
onMouseLeave={() => setDownHover(false)}
|
||||||
onClick={() => placeQuickBet('DOWN')}
|
onClick={() => placeQuickBet('DOWN')}
|
||||||
></div>
|
></div>
|
||||||
{hasDownShares > 0 ? (
|
{hasDownShares ? (
|
||||||
<TriangleDownFillIcon
|
<TriangleDownFillIcon
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'mx-auto h-5 w-5',
|
'mx-auto h-5 w-5',
|
||||||
|
|
|
@ -31,7 +31,9 @@ export function ContractActivity(props: {
|
||||||
const comments = updatedComments ?? props.comments
|
const comments = updatedComments ?? props.comments
|
||||||
|
|
||||||
const updatedBets = useBets(contract.id)
|
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(
|
const items = getSpecificContractActivityItems(
|
||||||
contract,
|
contract,
|
||||||
bets,
|
bets,
|
||||||
|
|
|
@ -34,7 +34,7 @@ import {
|
||||||
TruncatedComment,
|
TruncatedComment,
|
||||||
} from 'web/components/feed/feed-comments'
|
} from 'web/components/feed/feed-comments'
|
||||||
import { FeedBet } from 'web/components/feed/feed-bets'
|
import { FeedBet } from 'web/components/feed/feed-bets'
|
||||||
import { NumericContract } from 'common/contract'
|
import { CPMMBinaryContract, NumericContract } from 'common/contract'
|
||||||
import { FeedLiquidity } from './feed-liquidity'
|
import { FeedLiquidity } from './feed-liquidity'
|
||||||
|
|
||||||
export function FeedItems(props: {
|
export function FeedItems(props: {
|
||||||
|
@ -68,7 +68,10 @@ export function FeedItems(props: {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{outcomeType === 'BINARY' && tradingAllowed(contract) && (
|
{outcomeType === 'BINARY' && tradingAllowed(contract) && (
|
||||||
<BetRow contract={contract} className={clsx('mb-2', betRowClassName)} />
|
<BetRow
|
||||||
|
contract={contract as CPMMBinaryContract}
|
||||||
|
className={clsx('mb-2', betRowClassName)}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
89
web/components/limit-bets.tsx
Normal file
89
web/components/limit-bets.tsx
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
import clsx from 'clsx'
|
||||||
|
import { LimitBet } from 'common/bet'
|
||||||
|
import { CPMMBinaryContract, PseudoNumericContract } from 'common/contract'
|
||||||
|
import { getFormattedMappedValue } from 'common/pseudo-numeric'
|
||||||
|
import { formatMoney, formatPercent } from 'common/util/format'
|
||||||
|
import { sortBy } from 'lodash'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { cancelBet } from 'web/lib/firebase/api-call'
|
||||||
|
import { Col } from './layout/col'
|
||||||
|
import { LoadingIndicator } from './loading-indicator'
|
||||||
|
import { BinaryOutcomeLabel, PseudoNumericOutcomeLabel } from './outcome-label'
|
||||||
|
|
||||||
|
export function LimitBets(props: {
|
||||||
|
contract: CPMMBinaryContract | PseudoNumericContract
|
||||||
|
bets: LimitBet[]
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
const { contract, bets, className } = props
|
||||||
|
const recentBets = sortBy(
|
||||||
|
bets,
|
||||||
|
(bet) => -1 * bet.limitProb,
|
||||||
|
(bet) => -1 * bet.createdTime
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col
|
||||||
|
className={clsx(className, 'gap-2 overflow-hidden rounded bg-white py-3')}
|
||||||
|
>
|
||||||
|
<div className="px-6 py-3 text-2xl">Your limit bets</div>
|
||||||
|
<div className="px-4">
|
||||||
|
<table className="table-compact table w-full rounded text-gray-500">
|
||||||
|
<tbody>
|
||||||
|
{recentBets.map((bet) => (
|
||||||
|
<LimitBet key={bet.id} bet={bet} contract={contract} />
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function LimitBet(props: {
|
||||||
|
contract: CPMMBinaryContract | PseudoNumericContract
|
||||||
|
bet: LimitBet
|
||||||
|
}) {
|
||||||
|
const { contract, bet } = props
|
||||||
|
const { orderAmount, amount, limitProb, outcome } = bet
|
||||||
|
const isPseudoNumeric = contract.outcomeType === 'PSEUDO_NUMERIC'
|
||||||
|
|
||||||
|
const [isCancelling, setIsCancelling] = useState(false)
|
||||||
|
|
||||||
|
const onCancel = () => {
|
||||||
|
cancelBet({ betId: bet.id })
|
||||||
|
setIsCancelling(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div className="pl-2">
|
||||||
|
{isPseudoNumeric ? (
|
||||||
|
<PseudoNumericOutcomeLabel outcome={outcome as 'YES' | 'NO'} />
|
||||||
|
) : (
|
||||||
|
<BinaryOutcomeLabel outcome={outcome as 'YES' | 'NO'} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>{formatMoney(orderAmount - amount)}</td>
|
||||||
|
<td>
|
||||||
|
{isPseudoNumeric
|
||||||
|
? getFormattedMappedValue(contract)(limitProb)
|
||||||
|
: formatPercent(limitProb)}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{isCancelling ? (
|
||||||
|
<LoadingIndicator />
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
className="btn btn-xs btn-outline my-auto normal-case"
|
||||||
|
onClick={onCancel}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
}
|
|
@ -96,7 +96,7 @@ export function NumericResolutionPanel(props: {
|
||||||
|
|
||||||
{outcomeMode === 'NUMBER' && (
|
{outcomeMode === 'NUMBER' && (
|
||||||
<BucketInput
|
<BucketInput
|
||||||
contract={contract as any}
|
contract={contract}
|
||||||
isSubmitting={isSubmitting}
|
isSubmitting={isSubmitting}
|
||||||
onBucketChange={(v, o) => (setValue(v), setOutcome(o))}
|
onBucketChange={(v, o) => (setValue(v), setOutcome(o))}
|
||||||
/>
|
/>
|
||||||
|
|
49
web/components/probability-input.tsx
Normal file
49
web/components/probability-input.tsx
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import clsx from 'clsx'
|
||||||
|
import { Col } from './layout/col'
|
||||||
|
import { Spacer } from './layout/spacer'
|
||||||
|
|
||||||
|
export function ProbabilityInput(props: {
|
||||||
|
prob: number | undefined
|
||||||
|
onChange: (newProb: number | undefined) => void
|
||||||
|
disabled?: boolean
|
||||||
|
className?: string
|
||||||
|
inputClassName?: string
|
||||||
|
}) {
|
||||||
|
const { prob, onChange, disabled, className, inputClassName } = props
|
||||||
|
|
||||||
|
const onProbChange = (str: string) => {
|
||||||
|
let prob = parseInt(str.replace(/\D/g, ''))
|
||||||
|
const isInvalid = !str || isNaN(prob)
|
||||||
|
if (prob.toString().length > 2) {
|
||||||
|
if (prob === 100) prob = 99
|
||||||
|
else if (prob < 1) prob = 1
|
||||||
|
else prob = +prob.toString().slice(-2)
|
||||||
|
}
|
||||||
|
onChange(isInvalid ? undefined : prob)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col className={className}>
|
||||||
|
<label className="input-group">
|
||||||
|
<input
|
||||||
|
className={clsx(
|
||||||
|
'input input-bordered max-w-[200px] text-lg',
|
||||||
|
inputClassName
|
||||||
|
)}
|
||||||
|
type="number"
|
||||||
|
max={99}
|
||||||
|
min={1}
|
||||||
|
pattern="[0-9]*"
|
||||||
|
inputMode="numeric"
|
||||||
|
placeholder="0"
|
||||||
|
maxLength={2}
|
||||||
|
value={prob ?? ''}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={(e) => onProbChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
<span className="bg-gray-200 text-sm">%</span>
|
||||||
|
</label>
|
||||||
|
<Spacer h={4} />
|
||||||
|
</Col>
|
||||||
|
)
|
||||||
|
}
|
|
@ -6,7 +6,7 @@ import { Row } from './layout/row'
|
||||||
import { formatWithCommas } from 'common/util/format'
|
import { formatWithCommas } from 'common/util/format'
|
||||||
import { OutcomeLabel } from './outcome-label'
|
import { OutcomeLabel } from './outcome-label'
|
||||||
import { useUserContractBets } from 'web/hooks/use-user-bets'
|
import { useUserContractBets } from 'web/hooks/use-user-bets'
|
||||||
import { useSaveShares } from './use-save-shares'
|
import { useSaveBinaryShares } from './use-save-binary-shares'
|
||||||
import { SellSharesModal } from './sell-modal'
|
import { SellSharesModal } from './sell-modal'
|
||||||
|
|
||||||
export function SellRow(props: {
|
export function SellRow(props: {
|
||||||
|
@ -20,16 +20,7 @@ export function SellRow(props: {
|
||||||
const [showSellModal, setShowSellModal] = useState(false)
|
const [showSellModal, setShowSellModal] = useState(false)
|
||||||
|
|
||||||
const { mechanism } = contract
|
const { mechanism } = contract
|
||||||
const { yesFloorShares, noFloorShares, yesShares, noShares } = useSaveShares(
|
const { sharesOutcome, shares } = useSaveBinaryShares(contract, userBets)
|
||||||
contract,
|
|
||||||
userBets
|
|
||||||
)
|
|
||||||
const floorShares = yesFloorShares || noFloorShares
|
|
||||||
const sharesOutcome = yesFloorShares
|
|
||||||
? 'YES'
|
|
||||||
: noFloorShares
|
|
||||||
? 'NO'
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
if (sharesOutcome && user && mechanism === 'cpmm-1') {
|
if (sharesOutcome && user && mechanism === 'cpmm-1') {
|
||||||
return (
|
return (
|
||||||
|
@ -37,7 +28,7 @@ export function SellRow(props: {
|
||||||
<Col className={className}>
|
<Col className={className}>
|
||||||
<Row className="items-center justify-between gap-2 ">
|
<Row className="items-center justify-between gap-2 ">
|
||||||
<div>
|
<div>
|
||||||
You have {formatWithCommas(floorShares)}{' '}
|
You have {formatWithCommas(shares)}{' '}
|
||||||
<OutcomeLabel
|
<OutcomeLabel
|
||||||
outcome={sharesOutcome}
|
outcome={sharesOutcome}
|
||||||
contract={contract}
|
contract={contract}
|
||||||
|
@ -64,7 +55,7 @@ export function SellRow(props: {
|
||||||
contract={contract}
|
contract={contract}
|
||||||
user={user}
|
user={user}
|
||||||
userBets={userBets ?? []}
|
userBets={userBets ?? []}
|
||||||
shares={yesShares || noShares}
|
shares={shares}
|
||||||
sharesOutcome={sharesOutcome}
|
sharesOutcome={sharesOutcome}
|
||||||
setOpen={setShowSellModal}
|
setOpen={setShowSellModal}
|
||||||
/>
|
/>
|
||||||
|
|
56
web/components/use-save-binary-shares.ts
Normal file
56
web/components/use-save-binary-shares.ts
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import { BinaryContract, PseudoNumericContract } from 'common/contract'
|
||||||
|
import { Bet } from 'common/bet'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { partition, sumBy } from 'lodash'
|
||||||
|
import { safeLocalStorage } from 'web/lib/util/local'
|
||||||
|
|
||||||
|
export const useSaveBinaryShares = (
|
||||||
|
contract: BinaryContract | PseudoNumericContract,
|
||||||
|
userBets: Bet[] | undefined
|
||||||
|
) => {
|
||||||
|
const [savedShares, setSavedShares] = useState({ yesShares: 0, noShares: 0 })
|
||||||
|
|
||||||
|
const [yesBets, noBets] = partition(
|
||||||
|
userBets ?? [],
|
||||||
|
(bet) => bet.outcome === 'YES'
|
||||||
|
)
|
||||||
|
const [yesShares, noShares] = userBets
|
||||||
|
? [sumBy(yesBets, (bet) => bet.shares), sumBy(noBets, (bet) => bet.shares)]
|
||||||
|
: [savedShares.yesShares, savedShares.noShares]
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const local = safeLocalStorage()
|
||||||
|
|
||||||
|
// Read shares from local storage.
|
||||||
|
const savedShares = local?.getItem(`${contract.id}-shares`)
|
||||||
|
if (savedShares) {
|
||||||
|
setSavedShares(JSON.parse(savedShares))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userBets) {
|
||||||
|
// Save shares to local storage.
|
||||||
|
const sharesData = JSON.stringify({ yesShares, noShares })
|
||||||
|
local?.setItem(`${contract.id}-shares`, sharesData)
|
||||||
|
}
|
||||||
|
}, [contract.id, userBets, noShares, yesShares])
|
||||||
|
|
||||||
|
const hasYesShares = yesShares >= 1
|
||||||
|
const hasNoShares = noShares >= 1
|
||||||
|
|
||||||
|
const sharesOutcome = hasYesShares
|
||||||
|
? ('YES' as const)
|
||||||
|
: hasNoShares
|
||||||
|
? ('NO' as const)
|
||||||
|
: undefined
|
||||||
|
const shares =
|
||||||
|
sharesOutcome === 'YES' ? yesShares : sharesOutcome === 'NO' ? noShares : 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
yesShares,
|
||||||
|
noShares,
|
||||||
|
shares,
|
||||||
|
sharesOutcome,
|
||||||
|
hasYesShares,
|
||||||
|
hasNoShares,
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,59 +0,0 @@
|
||||||
import { Contract } from 'common/contract'
|
|
||||||
import { Bet } from 'common/bet'
|
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
import { partition, sumBy } from 'lodash'
|
|
||||||
import { safeLocalStorage } from 'web/lib/util/local'
|
|
||||||
|
|
||||||
export const useSaveShares = (
|
|
||||||
contract: Contract,
|
|
||||||
userBets: Bet[] | undefined,
|
|
||||||
freeResponseAnswerOutcome?: string
|
|
||||||
) => {
|
|
||||||
const [savedShares, setSavedShares] = useState<
|
|
||||||
| {
|
|
||||||
yesShares: number
|
|
||||||
noShares: number
|
|
||||||
yesFloorShares: number
|
|
||||||
noFloorShares: number
|
|
||||||
}
|
|
||||||
| undefined
|
|
||||||
>()
|
|
||||||
|
|
||||||
// TODO: How do we handle numeric yes / no bets? - maybe bet amounts above vs below the highest peak
|
|
||||||
const [yesBets, noBets] = partition(userBets ?? [], (bet) =>
|
|
||||||
freeResponseAnswerOutcome
|
|
||||||
? bet.outcome === freeResponseAnswerOutcome
|
|
||||||
: bet.outcome === 'YES'
|
|
||||||
)
|
|
||||||
const [yesShares, noShares] = [
|
|
||||||
sumBy(yesBets, (bet) => bet.shares),
|
|
||||||
sumBy(noBets, (bet) => bet.shares),
|
|
||||||
]
|
|
||||||
|
|
||||||
const yesFloorShares = Math.round(yesShares) === 0 ? 0 : Math.floor(yesShares)
|
|
||||||
const noFloorShares = Math.round(noShares) === 0 ? 0 : Math.floor(noShares)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const local = safeLocalStorage()
|
|
||||||
// Save yes and no shares to local storage.
|
|
||||||
const savedShares = local?.getItem(`${contract.id}-shares`)
|
|
||||||
if (!userBets && savedShares) {
|
|
||||||
setSavedShares(JSON.parse(savedShares))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userBets) {
|
|
||||||
const updatedShares = { yesShares, noShares }
|
|
||||||
local?.setItem(`${contract.id}-shares`, JSON.stringify(updatedShares))
|
|
||||||
}
|
|
||||||
}, [contract.id, userBets, noShares, yesShares])
|
|
||||||
|
|
||||||
if (userBets) return { yesShares, noShares, yesFloorShares, noFloorShares }
|
|
||||||
return (
|
|
||||||
savedShares ?? {
|
|
||||||
yesShares: 0,
|
|
||||||
noShares: 0,
|
|
||||||
yesFloorShares: 0,
|
|
||||||
noFloorShares: 0,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -4,8 +4,10 @@ import {
|
||||||
Bet,
|
Bet,
|
||||||
listenForBets,
|
listenForBets,
|
||||||
listenForRecentBets,
|
listenForRecentBets,
|
||||||
|
listenForUnfilledBets,
|
||||||
withoutAnteBets,
|
withoutAnteBets,
|
||||||
} from 'web/lib/firebase/bets'
|
} from 'web/lib/firebase/bets'
|
||||||
|
import { LimitBet } from 'common/bet'
|
||||||
|
|
||||||
export const useBets = (contractId: string) => {
|
export const useBets = (contractId: string) => {
|
||||||
const [bets, setBets] = useState<Bet[] | undefined>()
|
const [bets, setBets] = useState<Bet[] | undefined>()
|
||||||
|
@ -36,3 +38,12 @@ export const useRecentBets = () => {
|
||||||
useEffect(() => listenForRecentBets(setRecentBets), [])
|
useEffect(() => listenForRecentBets(setRecentBets), [])
|
||||||
return recentBets
|
return recentBets
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useUnfilledBets = (contractId: string) => {
|
||||||
|
const [unfilledBets, setUnfilledBets] = useState<LimitBet[] | undefined>()
|
||||||
|
useEffect(
|
||||||
|
() => listenForUnfilledBets(contractId, setUnfilledBets),
|
||||||
|
[contractId]
|
||||||
|
)
|
||||||
|
return unfilledBets
|
||||||
|
}
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import { useRef } from 'react'
|
import { useRef } from 'react'
|
||||||
|
import { useEvent } from './use-event'
|
||||||
|
|
||||||
// Focus helper from https://stackoverflow.com/a/54159564/1222351
|
// Focus helper from https://stackoverflow.com/a/54159564/1222351
|
||||||
export function useFocus(): [React.RefObject<HTMLElement>, () => void] {
|
export function useFocus(): [React.RefObject<HTMLElement>, () => void] {
|
||||||
const htmlElRef = useRef<HTMLElement>(null)
|
const htmlElRef = useRef<HTMLElement>(null)
|
||||||
const setFocus = () => {
|
const setFocus = useEvent(() => {
|
||||||
htmlElRef.current && htmlElRef.current.focus()
|
htmlElRef.current && htmlElRef.current.focus()
|
||||||
}
|
})
|
||||||
|
|
||||||
return [htmlElRef, setFocus]
|
return [htmlElRef, setFocus]
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,6 +82,10 @@ export function placeBet(params: any) {
|
||||||
return call(getFunctionUrl('placebet'), 'POST', params)
|
return call(getFunctionUrl('placebet'), 'POST', params)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function cancelBet(params: { betId: string }) {
|
||||||
|
return call(getFunctionUrl('cancelbet'), 'POST', params)
|
||||||
|
}
|
||||||
|
|
||||||
export function sellShares(params: any) {
|
export function sellShares(params: any) {
|
||||||
return call(getFunctionUrl('sellshares'), 'POST', params)
|
return call(getFunctionUrl('sellshares'), 'POST', params)
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ import {
|
||||||
import { uniq } from 'lodash'
|
import { uniq } from 'lodash'
|
||||||
|
|
||||||
import { db } from './init'
|
import { db } from './init'
|
||||||
import { Bet } from 'common/bet'
|
import { Bet, LimitBet } from 'common/bet'
|
||||||
import { Contract } from 'common/contract'
|
import { Contract } from 'common/contract'
|
||||||
import { getValues, listenForValues } from './utils'
|
import { getValues, listenForValues } from './utils'
|
||||||
import { getContractFromId } from './contracts'
|
import { getContractFromId } from './contracts'
|
||||||
|
@ -166,6 +166,21 @@ export function listenForUserContractBets(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function listenForUnfilledBets(
|
||||||
|
contractId: string,
|
||||||
|
setBets: (bets: LimitBet[]) => void
|
||||||
|
) {
|
||||||
|
const betsQuery = query(
|
||||||
|
collection(db, 'contracts', contractId, 'bets'),
|
||||||
|
where('isFilled', '==', false),
|
||||||
|
where('isCancelled', '==', false)
|
||||||
|
)
|
||||||
|
return listenForValues<LimitBet>(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
|
||||||
|
|
||||||
|
|
|
@ -39,6 +39,7 @@ import { FeedBet } from 'web/components/feed/feed-bets'
|
||||||
import { useIsIframe } from 'web/hooks/use-is-iframe'
|
import { useIsIframe } from 'web/hooks/use-is-iframe'
|
||||||
import ContractEmbedPage from '../embed/[username]/[contractSlug]'
|
import ContractEmbedPage from '../embed/[username]/[contractSlug]'
|
||||||
import { useBets } from 'web/hooks/use-bets'
|
import { useBets } from 'web/hooks/use-bets'
|
||||||
|
import { CPMMBinaryContract } from 'common/contract'
|
||||||
import { AlertBox } from 'web/components/alert-box'
|
import { AlertBox } from 'web/components/alert-box'
|
||||||
import { useTracking } from 'web/hooks/use-tracking'
|
import { useTracking } from 'web/hooks/use-tracking'
|
||||||
import { CommentTipMap, useTipTxns } from 'web/hooks/use-tip-txns'
|
import { CommentTipMap, useTipTxns } from 'web/hooks/use-tip-txns'
|
||||||
|
@ -127,6 +128,7 @@ export function ContractPageContent(
|
||||||
const tips = useTipTxns({ contractId: contract.id })
|
const tips = useTipTxns({ contractId: contract.id })
|
||||||
|
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
|
||||||
const { width, height } = useWindowSize()
|
const { width, height } = useWindowSize()
|
||||||
|
|
||||||
const [showConfetti, setShowConfetti] = useState(false)
|
const [showConfetti, setShowConfetti] = useState(false)
|
||||||
|
@ -169,7 +171,10 @@ export function ContractPageContent(
|
||||||
(isNumeric ? (
|
(isNumeric ? (
|
||||||
<NumericBetPanel className="hidden xl:flex" contract={contract} />
|
<NumericBetPanel className="hidden xl:flex" contract={contract} />
|
||||||
) : (
|
) : (
|
||||||
<BetPanel className="hidden xl:flex" contract={contract} />
|
<BetPanel
|
||||||
|
className="hidden xl:flex"
|
||||||
|
contract={contract as CPMMBinaryContract}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
{allowResolve &&
|
{allowResolve &&
|
||||||
(isNumeric || isPseudoNumeric ? (
|
(isNumeric || isPseudoNumeric ? (
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Bet } from 'common/bet'
|
import { Bet } from 'common/bet'
|
||||||
import { Contract } from 'common/contract'
|
import { Contract, CPMMBinaryContract } from 'common/contract'
|
||||||
import { DOMAIN } from 'common/envs/constants'
|
import { DOMAIN } from 'common/envs/constants'
|
||||||
import { AnswersGraph } from 'web/components/answers/answers-graph'
|
import { AnswersGraph } from 'web/components/answers/answers-graph'
|
||||||
import BetRow from 'web/components/bet-row'
|
import BetRow from 'web/components/bet-row'
|
||||||
|
@ -112,7 +112,10 @@ function ContractEmbed(props: { contract: Contract; bets: Bet[] }) {
|
||||||
|
|
||||||
{isBinary && (
|
{isBinary && (
|
||||||
<Row className="items-center gap-4">
|
<Row className="items-center gap-4">
|
||||||
<BetRow contract={contract} betPanelClassName="scale-75" />
|
<BetRow
|
||||||
|
contract={contract as CPMMBinaryContract}
|
||||||
|
betPanelClassName="scale-75"
|
||||||
|
/>
|
||||||
<BinaryResolutionOrChance contract={contract} />
|
<BinaryResolutionOrChance contract={contract} />
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -795,6 +795,8 @@ function getSourceIdForLinkComponent(
|
||||||
return sourceId
|
return sourceId
|
||||||
case 'contract':
|
case 'contract':
|
||||||
return ''
|
return ''
|
||||||
|
case 'bet':
|
||||||
|
return ''
|
||||||
default:
|
default:
|
||||||
return sourceId
|
return sourceId
|
||||||
}
|
}
|
||||||
|
@ -861,8 +863,16 @@ function NotificationTextLabel(props: {
|
||||||
{'+' + formatMoney(parseInt(sourceText))}
|
{'+' + formatMoney(parseInt(sourceText))}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
|
} else if (sourceType === 'bet' && sourceText) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span className="text-primary">
|
||||||
|
{formatMoney(parseInt(sourceText))}
|
||||||
|
</span>{' '}
|
||||||
|
<span>of your limit bet was filled</span>
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
// return default text
|
|
||||||
return (
|
return (
|
||||||
<div className={className ? className : 'line-clamp-4 whitespace-pre-line'}>
|
<div className={className ? className : 'line-clamp-4 whitespace-pre-line'}>
|
||||||
<Linkify text={defaultText} />
|
<Linkify text={defaultText} />
|
||||||
|
@ -913,6 +923,9 @@ function getReasonForShowingNotification(
|
||||||
else if (sourceSlug) reasonText = 'joined because you shared'
|
else if (sourceSlug) reasonText = 'joined because you shared'
|
||||||
else reasonText = 'joined because of you'
|
else reasonText = 'joined because of you'
|
||||||
break
|
break
|
||||||
|
case 'bet':
|
||||||
|
reasonText = 'bet against you'
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
reasonText = ''
|
reasonText = ''
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user