Merge branch 'main' into austin/dc-hackathon
This commit is contained in:
commit
69a785fd74
17
.github/workflows/merge-main-into-main2.yml
vendored
Normal file
17
.github/workflows/merge-main-into-main2.yml
vendored
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
name: Merge main into main2 on every commit
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'main'
|
||||||
|
jobs:
|
||||||
|
merge-branch:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@master
|
||||||
|
|
||||||
|
- name: Merge main -> main2
|
||||||
|
uses: devmasx/merge-branch@master
|
||||||
|
with:
|
||||||
|
type: now
|
||||||
|
target_branch: main2
|
||||||
|
github_token: ${{ github.token }}
|
|
@ -147,7 +147,8 @@ function calculateAmountToBuyShares(
|
||||||
state: CpmmState,
|
state: CpmmState,
|
||||||
shares: number,
|
shares: number,
|
||||||
outcome: 'YES' | 'NO',
|
outcome: 'YES' | 'NO',
|
||||||
unfilledBets: LimitBet[]
|
unfilledBets: LimitBet[],
|
||||||
|
balanceByUserId: { [userId: string]: number }
|
||||||
) {
|
) {
|
||||||
// Search for amount between bounds (0, shares).
|
// Search for amount between bounds (0, shares).
|
||||||
// Min share price is M$0, and max is M$1 each.
|
// Min share price is M$0, and max is M$1 each.
|
||||||
|
@ -157,7 +158,8 @@ function calculateAmountToBuyShares(
|
||||||
amount,
|
amount,
|
||||||
state,
|
state,
|
||||||
undefined,
|
undefined,
|
||||||
unfilledBets
|
unfilledBets,
|
||||||
|
balanceByUserId
|
||||||
)
|
)
|
||||||
|
|
||||||
const totalShares = sumBy(takers, (taker) => taker.shares)
|
const totalShares = sumBy(takers, (taker) => taker.shares)
|
||||||
|
@ -169,7 +171,8 @@ export function calculateCpmmSale(
|
||||||
state: CpmmState,
|
state: CpmmState,
|
||||||
shares: number,
|
shares: number,
|
||||||
outcome: 'YES' | 'NO',
|
outcome: 'YES' | 'NO',
|
||||||
unfilledBets: LimitBet[]
|
unfilledBets: LimitBet[],
|
||||||
|
balanceByUserId: { [userId: string]: number }
|
||||||
) {
|
) {
|
||||||
if (Math.round(shares) < 0) {
|
if (Math.round(shares) < 0) {
|
||||||
throw new Error('Cannot sell non-positive shares')
|
throw new Error('Cannot sell non-positive shares')
|
||||||
|
@ -180,15 +183,17 @@ export function calculateCpmmSale(
|
||||||
state,
|
state,
|
||||||
shares,
|
shares,
|
||||||
oppositeOutcome,
|
oppositeOutcome,
|
||||||
unfilledBets
|
unfilledBets,
|
||||||
|
balanceByUserId
|
||||||
)
|
)
|
||||||
|
|
||||||
const { cpmmState, makers, takers, totalFees } = computeFills(
|
const { cpmmState, makers, takers, totalFees, ordersToCancel } = computeFills(
|
||||||
oppositeOutcome,
|
oppositeOutcome,
|
||||||
buyAmount,
|
buyAmount,
|
||||||
state,
|
state,
|
||||||
undefined,
|
undefined,
|
||||||
unfilledBets
|
unfilledBets,
|
||||||
|
balanceByUserId
|
||||||
)
|
)
|
||||||
|
|
||||||
// Transform buys of opposite outcome into sells.
|
// Transform buys of opposite outcome into sells.
|
||||||
|
@ -211,6 +216,7 @@ export function calculateCpmmSale(
|
||||||
fees: totalFees,
|
fees: totalFees,
|
||||||
makers,
|
makers,
|
||||||
takers: saleTakers,
|
takers: saleTakers,
|
||||||
|
ordersToCancel,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -218,9 +224,16 @@ export function getCpmmProbabilityAfterSale(
|
||||||
state: CpmmState,
|
state: CpmmState,
|
||||||
shares: number,
|
shares: number,
|
||||||
outcome: 'YES' | 'NO',
|
outcome: 'YES' | 'NO',
|
||||||
unfilledBets: LimitBet[]
|
unfilledBets: LimitBet[],
|
||||||
|
balanceByUserId: { [userId: string]: number }
|
||||||
) {
|
) {
|
||||||
const { cpmmState } = calculateCpmmSale(state, shares, outcome, unfilledBets)
|
const { cpmmState } = calculateCpmmSale(
|
||||||
|
state,
|
||||||
|
shares,
|
||||||
|
outcome,
|
||||||
|
unfilledBets,
|
||||||
|
balanceByUserId
|
||||||
|
)
|
||||||
return getCpmmProbability(cpmmState.pool, cpmmState.p)
|
return getCpmmProbability(cpmmState.pool, cpmmState.p)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { last, sortBy, sum, sumBy } from 'lodash'
|
import { last, sortBy, sum, sumBy, uniq } from 'lodash'
|
||||||
import { calculatePayout } from './calculate'
|
import { calculatePayout } from './calculate'
|
||||||
import { Bet, LimitBet } from './bet'
|
import { Bet, LimitBet } from './bet'
|
||||||
import { Contract, CPMMContract, DPMContract } from './contract'
|
import { Contract, CPMMContract, DPMContract } from './contract'
|
||||||
|
@ -62,16 +62,28 @@ export const computeBinaryCpmmElasticity = (
|
||||||
const limitBets = bets
|
const limitBets = bets
|
||||||
.filter(
|
.filter(
|
||||||
(b) =>
|
(b) =>
|
||||||
!b.isFilled && !b.isSold && !b.isRedemption && !b.sale && !b.isCancelled
|
!b.isFilled &&
|
||||||
|
!b.isSold &&
|
||||||
|
!b.isRedemption &&
|
||||||
|
!b.sale &&
|
||||||
|
!b.isCancelled &&
|
||||||
|
b.limitProb !== undefined
|
||||||
|
)
|
||||||
|
.sort((a, b) => a.createdTime - b.createdTime) as LimitBet[]
|
||||||
|
|
||||||
|
const userIds = uniq(limitBets.map((b) => b.userId))
|
||||||
|
// Assume all limit orders are good.
|
||||||
|
const userBalances = Object.fromEntries(
|
||||||
|
userIds.map((id) => [id, Number.MAX_SAFE_INTEGER])
|
||||||
)
|
)
|
||||||
.sort((a, b) => a.createdTime - b.createdTime)
|
|
||||||
|
|
||||||
const { newPool: poolY, newP: pY } = getBinaryCpmmBetInfo(
|
const { newPool: poolY, newP: pY } = getBinaryCpmmBetInfo(
|
||||||
'YES',
|
'YES',
|
||||||
betAmount,
|
betAmount,
|
||||||
contract,
|
contract,
|
||||||
undefined,
|
undefined,
|
||||||
limitBets as LimitBet[]
|
limitBets,
|
||||||
|
userBalances
|
||||||
)
|
)
|
||||||
const resultYes = getCpmmProbability(poolY, pY)
|
const resultYes = getCpmmProbability(poolY, pY)
|
||||||
|
|
||||||
|
@ -80,7 +92,8 @@ export const computeBinaryCpmmElasticity = (
|
||||||
betAmount,
|
betAmount,
|
||||||
contract,
|
contract,
|
||||||
undefined,
|
undefined,
|
||||||
limitBets as LimitBet[]
|
limitBets,
|
||||||
|
userBalances
|
||||||
)
|
)
|
||||||
const resultNo = getCpmmProbability(poolN, pN)
|
const resultNo = getCpmmProbability(poolN, pN)
|
||||||
|
|
||||||
|
|
|
@ -78,7 +78,8 @@ export function calculateShares(
|
||||||
export function calculateSaleAmount(
|
export function calculateSaleAmount(
|
||||||
contract: Contract,
|
contract: Contract,
|
||||||
bet: Bet,
|
bet: Bet,
|
||||||
unfilledBets: LimitBet[]
|
unfilledBets: LimitBet[],
|
||||||
|
balanceByUserId: { [userId: string]: number }
|
||||||
) {
|
) {
|
||||||
return contract.mechanism === 'cpmm-1' &&
|
return contract.mechanism === 'cpmm-1' &&
|
||||||
(contract.outcomeType === 'BINARY' ||
|
(contract.outcomeType === 'BINARY' ||
|
||||||
|
@ -87,7 +88,8 @@ export function calculateSaleAmount(
|
||||||
contract,
|
contract,
|
||||||
Math.abs(bet.shares),
|
Math.abs(bet.shares),
|
||||||
bet.outcome as 'YES' | 'NO',
|
bet.outcome as 'YES' | 'NO',
|
||||||
unfilledBets
|
unfilledBets,
|
||||||
|
balanceByUserId
|
||||||
).saleValue
|
).saleValue
|
||||||
: calculateDpmSaleAmount(contract, bet)
|
: calculateDpmSaleAmount(contract, bet)
|
||||||
}
|
}
|
||||||
|
@ -102,14 +104,16 @@ export function getProbabilityAfterSale(
|
||||||
contract: Contract,
|
contract: Contract,
|
||||||
outcome: string,
|
outcome: string,
|
||||||
shares: number,
|
shares: number,
|
||||||
unfilledBets: LimitBet[]
|
unfilledBets: LimitBet[],
|
||||||
|
balanceByUserId: { [userId: string]: number }
|
||||||
) {
|
) {
|
||||||
return contract.mechanism === 'cpmm-1'
|
return contract.mechanism === 'cpmm-1'
|
||||||
? getCpmmProbabilityAfterSale(
|
? getCpmmProbabilityAfterSale(
|
||||||
contract,
|
contract,
|
||||||
shares,
|
shares,
|
||||||
outcome as 'YES' | 'NO',
|
outcome as 'YES' | 'NO',
|
||||||
unfilledBets
|
unfilledBets,
|
||||||
|
balanceByUserId
|
||||||
)
|
)
|
||||||
: getDpmProbabilityAfterSale(contract.totalShares, outcome, shares)
|
: getDpmProbabilityAfterSale(contract.totalShares, outcome, shares)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
export const FLAT_TRADE_FEE = 0.1 // M$0.1
|
||||||
|
|
||||||
export const PLATFORM_FEE = 0
|
export const PLATFORM_FEE = 0
|
||||||
export const CREATOR_FEE = 0
|
export const CREATOR_FEE = 0
|
||||||
export const LIQUIDITY_FEE = 0
|
export const LIQUIDITY_FEE = 0
|
||||||
|
|
|
@ -143,7 +143,8 @@ export const computeFills = (
|
||||||
betAmount: number,
|
betAmount: number,
|
||||||
state: CpmmState,
|
state: CpmmState,
|
||||||
limitProb: number | undefined,
|
limitProb: number | undefined,
|
||||||
unfilledBets: LimitBet[]
|
unfilledBets: LimitBet[],
|
||||||
|
balanceByUserId: { [userId: string]: number }
|
||||||
) => {
|
) => {
|
||||||
if (isNaN(betAmount)) {
|
if (isNaN(betAmount)) {
|
||||||
throw new Error('Invalid bet amount: ${betAmount}')
|
throw new Error('Invalid bet amount: ${betAmount}')
|
||||||
|
@ -165,10 +166,12 @@ export const computeFills = (
|
||||||
shares: number
|
shares: number
|
||||||
timestamp: number
|
timestamp: number
|
||||||
}[] = []
|
}[] = []
|
||||||
|
const ordersToCancel: LimitBet[] = []
|
||||||
|
|
||||||
let amount = betAmount
|
let amount = betAmount
|
||||||
let cpmmState = { pool: state.pool, p: state.p }
|
let cpmmState = { pool: state.pool, p: state.p }
|
||||||
let totalFees = noFees
|
let totalFees = noFees
|
||||||
|
const currentBalanceByUserId = { ...balanceByUserId }
|
||||||
|
|
||||||
let i = 0
|
let i = 0
|
||||||
while (true) {
|
while (true) {
|
||||||
|
@ -185,9 +188,20 @@ export const computeFills = (
|
||||||
takers.push(taker)
|
takers.push(taker)
|
||||||
} else {
|
} else {
|
||||||
// Matched against bet.
|
// Matched against bet.
|
||||||
|
i++
|
||||||
|
const { userId } = maker.bet
|
||||||
|
const makerBalance = currentBalanceByUserId[userId]
|
||||||
|
|
||||||
|
if (floatingGreaterEqual(makerBalance, maker.amount)) {
|
||||||
|
currentBalanceByUserId[userId] = makerBalance - maker.amount
|
||||||
|
} else {
|
||||||
|
// Insufficient balance. Cancel maker bet.
|
||||||
|
ordersToCancel.push(maker.bet)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
takers.push(taker)
|
takers.push(taker)
|
||||||
makers.push(maker)
|
makers.push(maker)
|
||||||
i++
|
|
||||||
}
|
}
|
||||||
|
|
||||||
amount -= taker.amount
|
amount -= taker.amount
|
||||||
|
@ -195,7 +209,7 @@ export const computeFills = (
|
||||||
if (floatingEqual(amount, 0)) break
|
if (floatingEqual(amount, 0)) break
|
||||||
}
|
}
|
||||||
|
|
||||||
return { takers, makers, totalFees, cpmmState }
|
return { takers, makers, totalFees, cpmmState, ordersToCancel }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getBinaryCpmmBetInfo = (
|
export const getBinaryCpmmBetInfo = (
|
||||||
|
@ -203,15 +217,17 @@ export const getBinaryCpmmBetInfo = (
|
||||||
betAmount: number,
|
betAmount: number,
|
||||||
contract: CPMMBinaryContract | PseudoNumericContract,
|
contract: CPMMBinaryContract | PseudoNumericContract,
|
||||||
limitProb: number | undefined,
|
limitProb: number | undefined,
|
||||||
unfilledBets: LimitBet[]
|
unfilledBets: LimitBet[],
|
||||||
|
balanceByUserId: { [userId: string]: number }
|
||||||
) => {
|
) => {
|
||||||
const { pool, p } = contract
|
const { pool, p } = contract
|
||||||
const { takers, makers, cpmmState, totalFees } = computeFills(
|
const { takers, makers, cpmmState, totalFees, ordersToCancel } = computeFills(
|
||||||
outcome,
|
outcome,
|
||||||
betAmount,
|
betAmount,
|
||||||
{ pool, p },
|
{ pool, p },
|
||||||
limitProb,
|
limitProb,
|
||||||
unfilledBets
|
unfilledBets,
|
||||||
|
balanceByUserId
|
||||||
)
|
)
|
||||||
const probBefore = getCpmmProbability(contract.pool, contract.p)
|
const probBefore = getCpmmProbability(contract.pool, contract.p)
|
||||||
const probAfter = getCpmmProbability(cpmmState.pool, cpmmState.p)
|
const probAfter = getCpmmProbability(cpmmState.pool, cpmmState.p)
|
||||||
|
@ -246,6 +262,7 @@ export const getBinaryCpmmBetInfo = (
|
||||||
newP: cpmmState.p,
|
newP: cpmmState.p,
|
||||||
newTotalLiquidity,
|
newTotalLiquidity,
|
||||||
makers,
|
makers,
|
||||||
|
ordersToCancel,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -254,14 +271,16 @@ export const getBinaryBetStats = (
|
||||||
betAmount: number,
|
betAmount: number,
|
||||||
contract: CPMMBinaryContract | PseudoNumericContract,
|
contract: CPMMBinaryContract | PseudoNumericContract,
|
||||||
limitProb: number,
|
limitProb: number,
|
||||||
unfilledBets: LimitBet[]
|
unfilledBets: LimitBet[],
|
||||||
|
balanceByUserId: { [userId: string]: number }
|
||||||
) => {
|
) => {
|
||||||
const { newBet } = getBinaryCpmmBetInfo(
|
const { newBet } = getBinaryCpmmBetInfo(
|
||||||
outcome,
|
outcome,
|
||||||
betAmount ?? 0,
|
betAmount ?? 0,
|
||||||
contract,
|
contract,
|
||||||
limitProb,
|
limitProb,
|
||||||
unfilledBets as LimitBet[]
|
unfilledBets,
|
||||||
|
balanceByUserId
|
||||||
)
|
)
|
||||||
const remainingMatched =
|
const remainingMatched =
|
||||||
((newBet.orderAmount ?? 0) - newBet.amount) /
|
((newBet.orderAmount ?? 0) - newBet.amount) /
|
||||||
|
|
|
@ -84,15 +84,17 @@ export const getCpmmSellBetInfo = (
|
||||||
outcome: 'YES' | 'NO',
|
outcome: 'YES' | 'NO',
|
||||||
contract: CPMMContract,
|
contract: CPMMContract,
|
||||||
unfilledBets: LimitBet[],
|
unfilledBets: LimitBet[],
|
||||||
|
balanceByUserId: { [userId: string]: number },
|
||||||
loanPaid: number
|
loanPaid: number
|
||||||
) => {
|
) => {
|
||||||
const { pool, p } = contract
|
const { pool, p } = contract
|
||||||
|
|
||||||
const { saleValue, cpmmState, fees, makers, takers } = calculateCpmmSale(
|
const { saleValue, cpmmState, fees, makers, takers, ordersToCancel } = calculateCpmmSale(
|
||||||
contract,
|
contract,
|
||||||
shares,
|
shares,
|
||||||
outcome,
|
outcome,
|
||||||
unfilledBets
|
unfilledBets,
|
||||||
|
balanceByUserId,
|
||||||
)
|
)
|
||||||
|
|
||||||
const probBefore = getCpmmProbability(pool, p)
|
const probBefore = getCpmmProbability(pool, p)
|
||||||
|
@ -134,5 +136,6 @@ export const getCpmmSellBetInfo = (
|
||||||
fees,
|
fees,
|
||||||
makers,
|
makers,
|
||||||
takers,
|
takers,
|
||||||
|
ordersToCancel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,22 @@ Our community is the beating heart of Manifold; your individual contributions ar
|
||||||
|
|
||||||
## Awarded bounties
|
## Awarded bounties
|
||||||
|
|
||||||
|
💥 *Awarded on 2022-10-07*
|
||||||
|
|
||||||
|
**[Pepe](https://manifold.markets/Pepe): M$10,000**
|
||||||
|
**[Jack](https://manifold.markets/jack): M$2,000**
|
||||||
|
**[Martin](https://manifold.markets/MartinRandall): M$2,000**
|
||||||
|
**[Yev](https://manifold.markets/Yev): M$2,000**
|
||||||
|
**[Michael](https://manifold.markets/MichaelWheatley): M$2,000**
|
||||||
|
|
||||||
|
- For discovering an infinite mana exploit using limit orders, and informing the Manifold team of it privately.
|
||||||
|
|
||||||
|
**[Matt](https://manifold.markets/MattP): M$5,000**
|
||||||
|
**[Adrian](https://manifold.markets/ahalekelly): M$5,000**
|
||||||
|
**[Yev](https://manifold.markets/Yev): M$5,000**
|
||||||
|
|
||||||
|
- For discovering an AMM liquidity exploit and informing the Manifold team of it privately.
|
||||||
|
|
||||||
🎈 *Awarded on 2022-06-14*
|
🎈 *Awarded on 2022-06-14*
|
||||||
|
|
||||||
**[Wasabipesto](https://manifold.markets/wasabipesto): M$20,000**
|
**[Wasabipesto](https://manifold.markets/wasabipesto): M$20,000**
|
||||||
|
|
|
@ -146,3 +146,24 @@ export const newEndpoint = (endpointOpts: EndpointOptions, fn: Handler) => {
|
||||||
},
|
},
|
||||||
} as EndpointDefinition
|
} as EndpointDefinition
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const newEndpointNoAuth = (
|
||||||
|
endpointOpts: EndpointOptions,
|
||||||
|
fn: (req: Request) => Promise<Output>
|
||||||
|
) => {
|
||||||
|
const opts = Object.assign({}, DEFAULT_OPTS, endpointOpts)
|
||||||
|
return {
|
||||||
|
opts,
|
||||||
|
handler: async (req: Request, res: Response) => {
|
||||||
|
log(`${req.method} ${req.url} ${JSON.stringify(req.body)}`)
|
||||||
|
try {
|
||||||
|
if (opts.method !== req.method) {
|
||||||
|
throw new APIError(405, `This endpoint supports only ${opts.method}.`)
|
||||||
|
}
|
||||||
|
res.status(200).json(await fn(req))
|
||||||
|
} catch (e) {
|
||||||
|
writeResponseError(e, res)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
} as EndpointDefinition
|
||||||
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ export * from './on-create-user'
|
||||||
export * from './on-create-bet'
|
export * from './on-create-bet'
|
||||||
export * from './on-create-comment-on-contract'
|
export * from './on-create-comment-on-contract'
|
||||||
export * from './on-view'
|
export * from './on-view'
|
||||||
export * from './update-metrics'
|
export { updateMetrics } from './update-metrics'
|
||||||
export * from './update-stats'
|
export * from './update-stats'
|
||||||
export * from './update-loans'
|
export * from './update-loans'
|
||||||
export * from './backup-db'
|
export * from './backup-db'
|
||||||
|
@ -78,6 +78,7 @@ import { getcurrentuser } from './get-current-user'
|
||||||
import { acceptchallenge } from './accept-challenge'
|
import { acceptchallenge } from './accept-challenge'
|
||||||
import { createpost } from './create-post'
|
import { createpost } from './create-post'
|
||||||
import { savetwitchcredentials } from './save-twitch-credentials'
|
import { savetwitchcredentials } from './save-twitch-credentials'
|
||||||
|
import { updatemetrics } from './update-metrics'
|
||||||
|
|
||||||
const toCloudFunction = ({ opts, handler }: EndpointDefinition) => {
|
const toCloudFunction = ({ opts, handler }: EndpointDefinition) => {
|
||||||
return onRequest(opts, handler as any)
|
return onRequest(opts, handler as any)
|
||||||
|
@ -108,6 +109,7 @@ const getCurrentUserFunction = toCloudFunction(getcurrentuser)
|
||||||
const acceptChallenge = toCloudFunction(acceptchallenge)
|
const acceptChallenge = toCloudFunction(acceptchallenge)
|
||||||
const createPostFunction = toCloudFunction(createpost)
|
const createPostFunction = toCloudFunction(createpost)
|
||||||
const saveTwitchCredentials = toCloudFunction(savetwitchcredentials)
|
const saveTwitchCredentials = toCloudFunction(savetwitchcredentials)
|
||||||
|
const updateMetricsFunction = toCloudFunction(updatemetrics)
|
||||||
|
|
||||||
export {
|
export {
|
||||||
healthFunction as health,
|
healthFunction as health,
|
||||||
|
@ -136,4 +138,5 @@ export {
|
||||||
createCommentFunction as createcomment,
|
createCommentFunction as createcomment,
|
||||||
addCommentBounty as addcommentbounty,
|
addCommentBounty as addcommentbounty,
|
||||||
awardCommentBounty as awardcommentbounty,
|
awardCommentBounty as awardcommentbounty,
|
||||||
|
updateMetricsFunction as updatemetrics,
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,6 @@ import { HOUSE_LIQUIDITY_PROVIDER_ID } from '../../common/antes'
|
||||||
import { createReferralNotification } from './create-notification'
|
import { createReferralNotification } from './create-notification'
|
||||||
import { ReferralTxn } from '../../common/txn'
|
import { ReferralTxn } from '../../common/txn'
|
||||||
import { Contract } from '../../common/contract'
|
import { Contract } from '../../common/contract'
|
||||||
import { LimitBet } from '../../common/bet'
|
|
||||||
import { QuerySnapshot } from 'firebase-admin/firestore'
|
|
||||||
import { Group } from '../../common/group'
|
import { Group } from '../../common/group'
|
||||||
import { REFERRAL_AMOUNT } from '../../common/economy'
|
import { REFERRAL_AMOUNT } from '../../common/economy'
|
||||||
const firestore = admin.firestore()
|
const firestore = admin.firestore()
|
||||||
|
@ -21,10 +19,6 @@ export const onUpdateUser = functions.firestore
|
||||||
if (prevUser.referredByUserId !== user.referredByUserId) {
|
if (prevUser.referredByUserId !== user.referredByUserId) {
|
||||||
await handleUserUpdatedReferral(user, eventId)
|
await handleUserUpdatedReferral(user, eventId)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.balance <= 0) {
|
|
||||||
await cancelLimitOrders(user.id)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
async function handleUserUpdatedReferral(user: User, eventId: string) {
|
async function handleUserUpdatedReferral(user: User, eventId: string) {
|
||||||
|
@ -123,15 +117,3 @@ async function handleUserUpdatedReferral(user: User, eventId: string) {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cancelLimitOrders(userId: string) {
|
|
||||||
const snapshot = (await firestore
|
|
||||||
.collectionGroup('bets')
|
|
||||||
.where('userId', '==', userId)
|
|
||||||
.where('isFilled', '==', false)
|
|
||||||
.get()) as QuerySnapshot<LimitBet>
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
snapshot.docs.map((doc) => doc.ref.update({ isCancelled: true }))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { groupBy, mapValues, sumBy, uniq } 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 { FLAT_TRADE_FEE } from '../../common/fees'
|
||||||
import {
|
import {
|
||||||
BetInfo,
|
BetInfo,
|
||||||
getBinaryCpmmBetInfo,
|
getBinaryCpmmBetInfo,
|
||||||
|
@ -23,6 +24,7 @@ import { floatingEqual } from '../../common/util/math'
|
||||||
import { redeemShares } from './redeem-shares'
|
import { redeemShares } from './redeem-shares'
|
||||||
import { log } from './utils'
|
import { log } from './utils'
|
||||||
import { addUserToContractFollowers } from './follow-market'
|
import { addUserToContractFollowers } from './follow-market'
|
||||||
|
import { filterDefined } from '../../common/util/array'
|
||||||
|
|
||||||
const bodySchema = z.object({
|
const bodySchema = z.object({
|
||||||
contractId: z.string(),
|
contractId: z.string(),
|
||||||
|
@ -73,9 +75,11 @@ export const placebet = newEndpoint({}, async (req, auth) => {
|
||||||
newTotalLiquidity,
|
newTotalLiquidity,
|
||||||
newP,
|
newP,
|
||||||
makers,
|
makers,
|
||||||
|
ordersToCancel,
|
||||||
} = await (async (): Promise<
|
} = await (async (): Promise<
|
||||||
BetInfo & {
|
BetInfo & {
|
||||||
makers?: maker[]
|
makers?: maker[]
|
||||||
|
ordersToCancel?: LimitBet[]
|
||||||
}
|
}
|
||||||
> => {
|
> => {
|
||||||
if (
|
if (
|
||||||
|
@ -99,17 +103,16 @@ export const placebet = newEndpoint({}, async (req, auth) => {
|
||||||
limitProb = Math.round(limitProb * 100) / 100
|
limitProb = Math.round(limitProb * 100) / 100
|
||||||
}
|
}
|
||||||
|
|
||||||
const unfilledBetsSnap = await trans.get(
|
const { unfilledBets, balanceByUserId } =
|
||||||
getUnfilledBetsQuery(contractDoc)
|
await getUnfilledBetsAndUserBalances(trans, contractDoc)
|
||||||
)
|
|
||||||
const unfilledBets = unfilledBetsSnap.docs.map((doc) => doc.data())
|
|
||||||
|
|
||||||
return getBinaryCpmmBetInfo(
|
return getBinaryCpmmBetInfo(
|
||||||
outcome,
|
outcome,
|
||||||
amount,
|
amount,
|
||||||
contract,
|
contract,
|
||||||
limitProb,
|
limitProb,
|
||||||
unfilledBets
|
unfilledBets,
|
||||||
|
balanceByUserId
|
||||||
)
|
)
|
||||||
} else if (
|
} else if (
|
||||||
(outcomeType == 'FREE_RESPONSE' || outcomeType === 'MULTIPLE_CHOICE') &&
|
(outcomeType == 'FREE_RESPONSE' || outcomeType === 'MULTIPLE_CHOICE') &&
|
||||||
|
@ -152,11 +155,25 @@ export const placebet = newEndpoint({}, async (req, auth) => {
|
||||||
if (makers) {
|
if (makers) {
|
||||||
updateMakers(makers, betDoc.id, contractDoc, trans)
|
updateMakers(makers, betDoc.id, contractDoc, trans)
|
||||||
}
|
}
|
||||||
|
if (ordersToCancel) {
|
||||||
|
for (const bet of ordersToCancel) {
|
||||||
|
trans.update(contractDoc.collection('bets').doc(bet.id), {
|
||||||
|
isCancelled: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (newBet.amount !== 0) {
|
const balanceChange =
|
||||||
trans.update(userDoc, { balance: FieldValue.increment(-newBet.amount) })
|
newBet.amount !== 0
|
||||||
|
? // quick bet
|
||||||
|
newBet.amount + FLAT_TRADE_FEE
|
||||||
|
: // limit order
|
||||||
|
FLAT_TRADE_FEE
|
||||||
|
|
||||||
|
trans.update(userDoc, { balance: FieldValue.increment(-balanceChange) })
|
||||||
log('Updated user balance.')
|
log('Updated user balance.')
|
||||||
|
|
||||||
|
if (newBet.amount !== 0) {
|
||||||
trans.update(
|
trans.update(
|
||||||
contractDoc,
|
contractDoc,
|
||||||
removeUndefinedProps({
|
removeUndefinedProps({
|
||||||
|
@ -193,13 +210,36 @@ export const placebet = newEndpoint({}, async (req, auth) => {
|
||||||
|
|
||||||
const firestore = admin.firestore()
|
const firestore = admin.firestore()
|
||||||
|
|
||||||
export const getUnfilledBetsQuery = (contractDoc: DocumentReference) => {
|
const getUnfilledBetsQuery = (contractDoc: DocumentReference) => {
|
||||||
return contractDoc
|
return contractDoc
|
||||||
.collection('bets')
|
.collection('bets')
|
||||||
.where('isFilled', '==', false)
|
.where('isFilled', '==', false)
|
||||||
.where('isCancelled', '==', false) as Query<LimitBet>
|
.where('isCancelled', '==', false) as Query<LimitBet>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getUnfilledBetsAndUserBalances = async (
|
||||||
|
trans: Transaction,
|
||||||
|
contractDoc: DocumentReference
|
||||||
|
) => {
|
||||||
|
const unfilledBetsSnap = await trans.get(getUnfilledBetsQuery(contractDoc))
|
||||||
|
const unfilledBets = unfilledBetsSnap.docs.map((doc) => doc.data())
|
||||||
|
|
||||||
|
// Get balance of all users with open limit orders.
|
||||||
|
const userIds = uniq(unfilledBets.map((bet) => bet.userId))
|
||||||
|
const userDocs =
|
||||||
|
userIds.length === 0
|
||||||
|
? []
|
||||||
|
: await trans.getAll(
|
||||||
|
...userIds.map((userId) => firestore.doc(`users/${userId}`))
|
||||||
|
)
|
||||||
|
const users = filterDefined(userDocs.map((doc) => doc.data() as User))
|
||||||
|
const balanceByUserId = Object.fromEntries(
|
||||||
|
users.map((user) => [user.id, user.balance])
|
||||||
|
)
|
||||||
|
|
||||||
|
return { unfilledBets, balanceByUserId }
|
||||||
|
}
|
||||||
|
|
||||||
type maker = {
|
type maker = {
|
||||||
bet: LimitBet
|
bet: LimitBet
|
||||||
amount: number
|
amount: number
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { mapValues, groupBy, sumBy, uniq } from 'lodash'
|
import { mapValues, groupBy, sumBy, uniq } from 'lodash'
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
import { FieldValue } from 'firebase-admin/firestore'
|
||||||
|
|
||||||
import { APIError, newEndpoint, validate } from './api'
|
import { APIError, newEndpoint, validate } from './api'
|
||||||
import { Contract, CPMM_MIN_POOL_QTY } from '../../common/contract'
|
import { Contract, CPMM_MIN_POOL_QTY } from '../../common/contract'
|
||||||
|
@ -10,8 +11,7 @@ import { addObjects, removeUndefinedProps } from '../../common/util/object'
|
||||||
import { log } from './utils'
|
import { log } from './utils'
|
||||||
import { Bet } from '../../common/bet'
|
import { Bet } from '../../common/bet'
|
||||||
import { floatingEqual, floatingLesserEqual } from '../../common/util/math'
|
import { floatingEqual, floatingLesserEqual } from '../../common/util/math'
|
||||||
import { getUnfilledBetsQuery, updateMakers } from './place-bet'
|
import { getUnfilledBetsAndUserBalances, updateMakers } from './place-bet'
|
||||||
import { FieldValue } from 'firebase-admin/firestore'
|
|
||||||
import { redeemShares } from './redeem-shares'
|
import { redeemShares } from './redeem-shares'
|
||||||
import { removeUserFromContractFollowers } from './follow-market'
|
import { removeUserFromContractFollowers } from './follow-market'
|
||||||
|
|
||||||
|
@ -29,16 +29,18 @@ export const sellshares = newEndpoint({}, async (req, auth) => {
|
||||||
const contractDoc = firestore.doc(`contracts/${contractId}`)
|
const contractDoc = firestore.doc(`contracts/${contractId}`)
|
||||||
const userDoc = firestore.doc(`users/${auth.uid}`)
|
const userDoc = firestore.doc(`users/${auth.uid}`)
|
||||||
const betsQ = contractDoc.collection('bets').where('userId', '==', auth.uid)
|
const betsQ = contractDoc.collection('bets').where('userId', '==', auth.uid)
|
||||||
const [[contractSnap, userSnap], userBetsSnap, unfilledBetsSnap] =
|
const [
|
||||||
await Promise.all([
|
[contractSnap, userSnap],
|
||||||
|
userBetsSnap,
|
||||||
|
{ unfilledBets, balanceByUserId },
|
||||||
|
] = await Promise.all([
|
||||||
transaction.getAll(contractDoc, userDoc),
|
transaction.getAll(contractDoc, userDoc),
|
||||||
transaction.get(betsQ),
|
transaction.get(betsQ),
|
||||||
transaction.get(getUnfilledBetsQuery(contractDoc)),
|
getUnfilledBetsAndUserBalances(transaction, contractDoc),
|
||||||
])
|
])
|
||||||
if (!contractSnap.exists) throw new APIError(400, 'Contract not found.')
|
if (!contractSnap.exists) throw new APIError(400, 'Contract not found.')
|
||||||
if (!userSnap.exists) throw new APIError(400, 'User not found.')
|
if (!userSnap.exists) throw new APIError(400, 'User not found.')
|
||||||
const userBets = userBetsSnap.docs.map((doc) => doc.data() as Bet)
|
const userBets = userBetsSnap.docs.map((doc) => doc.data() as Bet)
|
||||||
const unfilledBets = unfilledBetsSnap.docs.map((doc) => doc.data())
|
|
||||||
|
|
||||||
const contract = contractSnap.data() as Contract
|
const contract = contractSnap.data() as Contract
|
||||||
const user = userSnap.data() as User
|
const user = userSnap.data() as User
|
||||||
|
@ -86,11 +88,13 @@ export const sellshares = newEndpoint({}, async (req, auth) => {
|
||||||
let loanPaid = saleFrac * loanAmount
|
let loanPaid = saleFrac * loanAmount
|
||||||
if (!isFinite(loanPaid)) loanPaid = 0
|
if (!isFinite(loanPaid)) loanPaid = 0
|
||||||
|
|
||||||
const { newBet, newPool, newP, fees, makers } = getCpmmSellBetInfo(
|
const { newBet, newPool, newP, fees, makers, ordersToCancel } =
|
||||||
|
getCpmmSellBetInfo(
|
||||||
soldShares,
|
soldShares,
|
||||||
chosenOutcome,
|
chosenOutcome,
|
||||||
contract,
|
contract,
|
||||||
unfilledBets,
|
unfilledBets,
|
||||||
|
balanceByUserId,
|
||||||
loanPaid
|
loanPaid
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -127,6 +131,12 @@ export const sellshares = newEndpoint({}, async (req, auth) => {
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
for (const bet of ordersToCancel) {
|
||||||
|
transaction.update(contractDoc.collection('bets').doc(bet.id), {
|
||||||
|
isCancelled: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return { newBet, makers, maxShares, soldShares }
|
return { newBet, makers, maxShares, soldShares }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,10 +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 { groupBy, isEmpty, keyBy, last, sortBy } from 'lodash'
|
import { groupBy, isEmpty, keyBy, last, sortBy } from 'lodash'
|
||||||
|
import fetch from 'node-fetch'
|
||||||
|
|
||||||
import { getValues, log, logMemory, writeAsync } from './utils'
|
import { getValues, log, logMemory, writeAsync } from './utils'
|
||||||
import { Bet } from '../../common/bet'
|
import { Bet } from '../../common/bet'
|
||||||
import { Contract, CPMM } from '../../common/contract'
|
import { Contract, CPMM } from '../../common/contract'
|
||||||
|
|
||||||
import { PortfolioMetrics, User } from '../../common/user'
|
import { PortfolioMetrics, User } from '../../common/user'
|
||||||
import { DAY_MS } from '../../common/util/time'
|
import { DAY_MS } from '../../common/util/time'
|
||||||
import { getLoanUpdates } from '../../common/loans'
|
import { getLoanUpdates } from '../../common/loans'
|
||||||
|
@ -20,13 +21,35 @@ import {
|
||||||
import { getProbability } from '../../common/calculate'
|
import { getProbability } from '../../common/calculate'
|
||||||
import { Group } from '../../common/group'
|
import { Group } from '../../common/group'
|
||||||
import { batchedWaitAll } from '../../common/util/promise'
|
import { batchedWaitAll } from '../../common/util/promise'
|
||||||
|
import { newEndpointNoAuth } from './api'
|
||||||
|
import { getFunctionUrl } from '../../common/api'
|
||||||
|
|
||||||
const firestore = admin.firestore()
|
const firestore = admin.firestore()
|
||||||
|
|
||||||
export const updateMetrics = functions
|
export const updateMetrics = functions.pubsub
|
||||||
.runWith({ memory: '8GB', timeoutSeconds: 540 })
|
.schedule('every 15 minutes')
|
||||||
.pubsub.schedule('every 15 minutes')
|
.onRun(async () => {
|
||||||
.onRun(updateMetricsCore)
|
const response = await fetch(getFunctionUrl('updatemetrics'), {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const json = await response.json()
|
||||||
|
|
||||||
|
if (response.ok) console.log(json)
|
||||||
|
else console.error(json)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const updatemetrics = newEndpointNoAuth(
|
||||||
|
{ timeoutSeconds: 2000, memory: '8GiB', minInstances: 0 },
|
||||||
|
async (_req) => {
|
||||||
|
await updateMetricsCore()
|
||||||
|
return { success: true }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
export async function updateMetricsCore() {
|
export async function updateMetricsCore() {
|
||||||
console.log('Loading users')
|
console.log('Loading users')
|
||||||
|
|
|
@ -2,12 +2,12 @@ import clsx from 'clsx'
|
||||||
import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd'
|
import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd'
|
||||||
import { MenuIcon } from '@heroicons/react/solid'
|
import { MenuIcon } from '@heroicons/react/solid'
|
||||||
import { toast } from 'react-hot-toast'
|
import { toast } from 'react-hot-toast'
|
||||||
|
import { XCircleIcon } from '@heroicons/react/outline'
|
||||||
|
|
||||||
import { Col } from 'web/components/layout/col'
|
import { Col } from 'web/components/layout/col'
|
||||||
import { Row } from 'web/components/layout/row'
|
import { Row } from 'web/components/layout/row'
|
||||||
import { Subtitle } from 'web/components/subtitle'
|
import { Subtitle } from 'web/components/subtitle'
|
||||||
import { keyBy } from 'lodash'
|
import { keyBy } from 'lodash'
|
||||||
import { XCircleIcon } from '@heroicons/react/outline'
|
|
||||||
import { Button } from './button'
|
import { Button } from './button'
|
||||||
import { updateUser } from 'web/lib/firebase/users'
|
import { updateUser } from 'web/lib/firebase/users'
|
||||||
import { leaveGroup } from 'web/lib/firebase/groups'
|
import { leaveGroup } from 'web/lib/firebase/groups'
|
||||||
|
|
|
@ -16,7 +16,7 @@ import { Button } from 'web/components/button'
|
||||||
import { BetSignUpPrompt } from './sign-up-prompt'
|
import { BetSignUpPrompt } from './sign-up-prompt'
|
||||||
import { User } from 'web/lib/firebase/users'
|
import { User } from 'web/lib/firebase/users'
|
||||||
import { SellRow } from './sell-row'
|
import { SellRow } from './sell-row'
|
||||||
import { useUnfilledBets } from 'web/hooks/use-bets'
|
import { useUnfilledBetsAndBalanceByUserId } from 'web/hooks/use-bets'
|
||||||
import { PlayMoneyDisclaimer } from './play-money-disclaimer'
|
import { PlayMoneyDisclaimer } from './play-money-disclaimer'
|
||||||
|
|
||||||
/** Button that opens BetPanel in a new modal */
|
/** Button that opens BetPanel in a new modal */
|
||||||
|
@ -100,7 +100,9 @@ export function SignedInBinaryMobileBetting(props: {
|
||||||
user: User
|
user: User
|
||||||
}) {
|
}) {
|
||||||
const { contract, user } = props
|
const { contract, user } = props
|
||||||
const unfilledBets = useUnfilledBets(contract.id) ?? []
|
const { unfilledBets, balanceByUserId } = useUnfilledBetsAndBalanceByUserId(
|
||||||
|
contract.id
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -111,6 +113,7 @@ export function SignedInBinaryMobileBetting(props: {
|
||||||
contract={contract as CPMMBinaryContract}
|
contract={contract as CPMMBinaryContract}
|
||||||
user={user}
|
user={user}
|
||||||
unfilledBets={unfilledBets}
|
unfilledBets={unfilledBets}
|
||||||
|
balanceByUserId={balanceByUserId}
|
||||||
mobileView={true}
|
mobileView={true}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { BuyAmountInput } from './amount-input'
|
||||||
import { Button } from './button'
|
import { Button } from './button'
|
||||||
import { Row } from './layout/row'
|
import { Row } from './layout/row'
|
||||||
import { YesNoSelector } from './yes-no-selector'
|
import { YesNoSelector } from './yes-no-selector'
|
||||||
import { useUnfilledBets } from 'web/hooks/use-bets'
|
import { useUnfilledBetsAndBalanceByUserId } from 'web/hooks/use-bets'
|
||||||
import { useUser } from 'web/hooks/use-user'
|
import { useUser } from 'web/hooks/use-user'
|
||||||
import { BetSignUpPrompt } from './sign-up-prompt'
|
import { BetSignUpPrompt } from './sign-up-prompt'
|
||||||
import { getCpmmProbability } from 'common/calculate-cpmm'
|
import { getCpmmProbability } from 'common/calculate-cpmm'
|
||||||
|
@ -34,14 +34,17 @@ export function BetInline(props: {
|
||||||
const [error, setError] = useState<string>()
|
const [error, setError] = useState<string>()
|
||||||
|
|
||||||
const isPseudoNumeric = contract.outcomeType === 'PSEUDO_NUMERIC'
|
const isPseudoNumeric = contract.outcomeType === 'PSEUDO_NUMERIC'
|
||||||
const unfilledBets = useUnfilledBets(contract.id) ?? []
|
const { unfilledBets, balanceByUserId } = useUnfilledBetsAndBalanceByUserId(
|
||||||
|
contract.id
|
||||||
|
)
|
||||||
|
|
||||||
const { newPool, newP } = getBinaryCpmmBetInfo(
|
const { newPool, newP } = getBinaryCpmmBetInfo(
|
||||||
outcome ?? 'YES',
|
outcome ?? 'YES',
|
||||||
amount ?? 0,
|
amount ?? 0,
|
||||||
contract,
|
contract,
|
||||||
undefined,
|
undefined,
|
||||||
unfilledBets
|
unfilledBets,
|
||||||
|
balanceByUserId
|
||||||
)
|
)
|
||||||
const resultProb = getCpmmProbability(newPool, newP)
|
const resultProb = getCpmmProbability(newPool, newP)
|
||||||
useEffect(() => setProbAfter(resultProb), [setProbAfter, resultProb])
|
useEffect(() => setProbAfter(resultProb), [setProbAfter, resultProb])
|
||||||
|
|
|
@ -35,7 +35,7 @@ import { useSaveBinaryShares } from './use-save-binary-shares'
|
||||||
import { BetSignUpPrompt } from './sign-up-prompt'
|
import { BetSignUpPrompt } from './sign-up-prompt'
|
||||||
import { ProbabilityOrNumericInput } from './probability-input'
|
import { ProbabilityOrNumericInput } from './probability-input'
|
||||||
import { track } from 'web/lib/service/analytics'
|
import { track } from 'web/lib/service/analytics'
|
||||||
import { useUnfilledBets } from 'web/hooks/use-bets'
|
import { useUnfilledBetsAndBalanceByUserId } from 'web/hooks/use-bets'
|
||||||
import { LimitBets } from './limit-bets'
|
import { LimitBets } from './limit-bets'
|
||||||
import { PillButton } from './buttons/pill-button'
|
import { PillButton } from './buttons/pill-button'
|
||||||
import { YesNoSelector } from './yes-no-selector'
|
import { YesNoSelector } from './yes-no-selector'
|
||||||
|
@ -55,7 +55,9 @@ export function BetPanel(props: {
|
||||||
const { contract, className } = props
|
const { contract, className } = props
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
const userBets = useUserContractBets(user?.id, contract.id)
|
const userBets = useUserContractBets(user?.id, contract.id)
|
||||||
const unfilledBets = useUnfilledBets(contract.id) ?? []
|
const { unfilledBets, balanceByUserId } = useUnfilledBetsAndBalanceByUserId(
|
||||||
|
contract.id
|
||||||
|
)
|
||||||
const { sharesOutcome } = useSaveBinaryShares(contract, userBets)
|
const { sharesOutcome } = useSaveBinaryShares(contract, userBets)
|
||||||
|
|
||||||
const [isLimitOrder, setIsLimitOrder] = useState(false)
|
const [isLimitOrder, setIsLimitOrder] = useState(false)
|
||||||
|
@ -86,12 +88,14 @@ export function BetPanel(props: {
|
||||||
contract={contract}
|
contract={contract}
|
||||||
user={user}
|
user={user}
|
||||||
unfilledBets={unfilledBets}
|
unfilledBets={unfilledBets}
|
||||||
|
balanceByUserId={balanceByUserId}
|
||||||
/>
|
/>
|
||||||
<LimitOrderPanel
|
<LimitOrderPanel
|
||||||
hidden={!isLimitOrder}
|
hidden={!isLimitOrder}
|
||||||
contract={contract}
|
contract={contract}
|
||||||
user={user}
|
user={user}
|
||||||
unfilledBets={unfilledBets}
|
unfilledBets={unfilledBets}
|
||||||
|
balanceByUserId={balanceByUserId}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
@ -117,7 +121,9 @@ export function SimpleBetPanel(props: {
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
const [isLimitOrder, setIsLimitOrder] = useState(false)
|
const [isLimitOrder, setIsLimitOrder] = useState(false)
|
||||||
|
|
||||||
const unfilledBets = useUnfilledBets(contract.id) ?? []
|
const { unfilledBets, balanceByUserId } = useUnfilledBetsAndBalanceByUserId(
|
||||||
|
contract.id
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col className={className}>
|
<Col className={className}>
|
||||||
|
@ -142,6 +148,7 @@ export function SimpleBetPanel(props: {
|
||||||
contract={contract}
|
contract={contract}
|
||||||
user={user}
|
user={user}
|
||||||
unfilledBets={unfilledBets}
|
unfilledBets={unfilledBets}
|
||||||
|
balanceByUserId={balanceByUserId}
|
||||||
onBuySuccess={onBetSuccess}
|
onBuySuccess={onBetSuccess}
|
||||||
/>
|
/>
|
||||||
<LimitOrderPanel
|
<LimitOrderPanel
|
||||||
|
@ -149,6 +156,7 @@ export function SimpleBetPanel(props: {
|
||||||
contract={contract}
|
contract={contract}
|
||||||
user={user}
|
user={user}
|
||||||
unfilledBets={unfilledBets}
|
unfilledBets={unfilledBets}
|
||||||
|
balanceByUserId={balanceByUserId}
|
||||||
onBuySuccess={onBetSuccess}
|
onBuySuccess={onBetSuccess}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -167,13 +175,21 @@ export function SimpleBetPanel(props: {
|
||||||
export function BuyPanel(props: {
|
export function BuyPanel(props: {
|
||||||
contract: CPMMBinaryContract | PseudoNumericContract
|
contract: CPMMBinaryContract | PseudoNumericContract
|
||||||
user: User | null | undefined
|
user: User | null | undefined
|
||||||
unfilledBets: Bet[]
|
unfilledBets: LimitBet[]
|
||||||
|
balanceByUserId: { [userId: string]: number }
|
||||||
hidden: boolean
|
hidden: boolean
|
||||||
onBuySuccess?: () => void
|
onBuySuccess?: () => void
|
||||||
mobileView?: boolean
|
mobileView?: boolean
|
||||||
}) {
|
}) {
|
||||||
const { contract, user, unfilledBets, hidden, onBuySuccess, mobileView } =
|
const {
|
||||||
props
|
contract,
|
||||||
|
user,
|
||||||
|
unfilledBets,
|
||||||
|
balanceByUserId,
|
||||||
|
hidden,
|
||||||
|
onBuySuccess,
|
||||||
|
mobileView,
|
||||||
|
} = props
|
||||||
|
|
||||||
const initialProb = getProbability(contract)
|
const initialProb = getProbability(contract)
|
||||||
const isPseudoNumeric = contract.outcomeType === 'PSEUDO_NUMERIC'
|
const isPseudoNumeric = contract.outcomeType === 'PSEUDO_NUMERIC'
|
||||||
|
@ -261,7 +277,8 @@ export function BuyPanel(props: {
|
||||||
betAmount ?? 0,
|
betAmount ?? 0,
|
||||||
contract,
|
contract,
|
||||||
undefined,
|
undefined,
|
||||||
unfilledBets as LimitBet[]
|
unfilledBets,
|
||||||
|
balanceByUserId
|
||||||
)
|
)
|
||||||
|
|
||||||
const [seeLimit, setSeeLimit] = useState(false)
|
const [seeLimit, setSeeLimit] = useState(false)
|
||||||
|
@ -416,6 +433,7 @@ export function BuyPanel(props: {
|
||||||
contract={contract}
|
contract={contract}
|
||||||
user={user}
|
user={user}
|
||||||
unfilledBets={unfilledBets}
|
unfilledBets={unfilledBets}
|
||||||
|
balanceByUserId={balanceByUserId}
|
||||||
/>
|
/>
|
||||||
<LimitBets
|
<LimitBets
|
||||||
contract={contract}
|
contract={contract}
|
||||||
|
@ -431,11 +449,19 @@ export function BuyPanel(props: {
|
||||||
function LimitOrderPanel(props: {
|
function LimitOrderPanel(props: {
|
||||||
contract: CPMMBinaryContract | PseudoNumericContract
|
contract: CPMMBinaryContract | PseudoNumericContract
|
||||||
user: User | null | undefined
|
user: User | null | undefined
|
||||||
unfilledBets: Bet[]
|
unfilledBets: LimitBet[]
|
||||||
|
balanceByUserId: { [userId: string]: number }
|
||||||
hidden: boolean
|
hidden: boolean
|
||||||
onBuySuccess?: () => void
|
onBuySuccess?: () => void
|
||||||
}) {
|
}) {
|
||||||
const { contract, user, unfilledBets, hidden, onBuySuccess } = props
|
const {
|
||||||
|
contract,
|
||||||
|
user,
|
||||||
|
unfilledBets,
|
||||||
|
balanceByUserId,
|
||||||
|
hidden,
|
||||||
|
onBuySuccess,
|
||||||
|
} = props
|
||||||
|
|
||||||
const initialProb = getProbability(contract)
|
const initialProb = getProbability(contract)
|
||||||
const isPseudoNumeric = contract.outcomeType === 'PSEUDO_NUMERIC'
|
const isPseudoNumeric = contract.outcomeType === 'PSEUDO_NUMERIC'
|
||||||
|
@ -581,7 +607,8 @@ function LimitOrderPanel(props: {
|
||||||
yesAmount,
|
yesAmount,
|
||||||
contract,
|
contract,
|
||||||
yesLimitProb ?? initialProb,
|
yesLimitProb ?? initialProb,
|
||||||
unfilledBets as LimitBet[]
|
unfilledBets,
|
||||||
|
balanceByUserId
|
||||||
)
|
)
|
||||||
const yesReturnPercent = formatPercent(yesReturn)
|
const yesReturnPercent = formatPercent(yesReturn)
|
||||||
|
|
||||||
|
@ -595,7 +622,8 @@ function LimitOrderPanel(props: {
|
||||||
noAmount,
|
noAmount,
|
||||||
contract,
|
contract,
|
||||||
noLimitProb ?? initialProb,
|
noLimitProb ?? initialProb,
|
||||||
unfilledBets as LimitBet[]
|
unfilledBets,
|
||||||
|
balanceByUserId
|
||||||
)
|
)
|
||||||
const noReturnPercent = formatPercent(noReturn)
|
const noReturnPercent = formatPercent(noReturn)
|
||||||
|
|
||||||
|
@ -830,7 +858,9 @@ export function SellPanel(props: {
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
const [wasSubmitted, setWasSubmitted] = useState(false)
|
const [wasSubmitted, setWasSubmitted] = useState(false)
|
||||||
|
|
||||||
const unfilledBets = useUnfilledBets(contract.id) ?? []
|
const { unfilledBets, balanceByUserId } = useUnfilledBetsAndBalanceByUserId(
|
||||||
|
contract.id
|
||||||
|
)
|
||||||
|
|
||||||
const betDisabled = isSubmitting || !amount || error !== undefined
|
const betDisabled = isSubmitting || !amount || error !== undefined
|
||||||
|
|
||||||
|
@ -889,7 +919,8 @@ export function SellPanel(props: {
|
||||||
contract,
|
contract,
|
||||||
sellQuantity ?? 0,
|
sellQuantity ?? 0,
|
||||||
sharesOutcome,
|
sharesOutcome,
|
||||||
unfilledBets
|
unfilledBets,
|
||||||
|
balanceByUserId
|
||||||
)
|
)
|
||||||
const netProceeds = saleValue - loanPaid
|
const netProceeds = saleValue - loanPaid
|
||||||
const profit = saleValue - costBasis
|
const profit = saleValue - costBasis
|
||||||
|
|
|
@ -4,7 +4,7 @@ import dayjs from 'dayjs'
|
||||||
import { useMemo, useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/solid'
|
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/solid'
|
||||||
|
|
||||||
import { Bet } from 'web/lib/firebase/bets'
|
import { Bet, MAX_USER_BETS_LOADED } from 'web/lib/firebase/bets'
|
||||||
import { User } from 'web/lib/firebase/users'
|
import { User } from 'web/lib/firebase/users'
|
||||||
import {
|
import {
|
||||||
formatMoney,
|
formatMoney,
|
||||||
|
@ -17,6 +17,7 @@ import {
|
||||||
Contract,
|
Contract,
|
||||||
contractPath,
|
contractPath,
|
||||||
getBinaryProbPercent,
|
getBinaryProbPercent,
|
||||||
|
MAX_USER_BET_CONTRACTS_LOADED,
|
||||||
} from 'web/lib/firebase/contracts'
|
} from 'web/lib/firebase/contracts'
|
||||||
import { Row } from './layout/row'
|
import { Row } from './layout/row'
|
||||||
import { sellBet } from 'web/lib/firebase/api'
|
import { sellBet } from 'web/lib/firebase/api'
|
||||||
|
@ -37,7 +38,7 @@ import { NumericContract } from 'common/contract'
|
||||||
import { formatNumericProbability } from 'common/pseudo-numeric'
|
import { formatNumericProbability } from 'common/pseudo-numeric'
|
||||||
import { useUser } from 'web/hooks/use-user'
|
import { useUser } from 'web/hooks/use-user'
|
||||||
import { useUserBets } from 'web/hooks/use-user-bets'
|
import { useUserBets } from 'web/hooks/use-user-bets'
|
||||||
import { useUnfilledBets } from 'web/hooks/use-bets'
|
import { useUnfilledBetsAndBalanceByUserId } from 'web/hooks/use-bets'
|
||||||
import { LimitBet } from 'common/bet'
|
import { LimitBet } from 'common/bet'
|
||||||
import { Pagination } from './pagination'
|
import { Pagination } from './pagination'
|
||||||
import { LimitOrderTable } from './limit-bets'
|
import { LimitOrderTable } from './limit-bets'
|
||||||
|
@ -50,6 +51,7 @@ import {
|
||||||
usePersistentState,
|
usePersistentState,
|
||||||
} from 'web/hooks/use-persistent-state'
|
} from 'web/hooks/use-persistent-state'
|
||||||
import { safeLocalStorage } from 'web/lib/util/local'
|
import { safeLocalStorage } from 'web/lib/util/local'
|
||||||
|
import { ExclamationIcon } from '@heroicons/react/outline'
|
||||||
|
|
||||||
type BetSort = 'newest' | 'profit' | 'closeTime' | 'value'
|
type BetSort = 'newest' | 'profit' | 'closeTime' | 'value'
|
||||||
type BetFilter = 'open' | 'limit_bet' | 'sold' | 'closed' | 'resolved' | 'all'
|
type BetFilter = 'open' | 'limit_bet' | 'sold' | 'closed' | 'resolved' | 'all'
|
||||||
|
@ -80,6 +82,10 @@ export function BetsList(props: { user: User }) {
|
||||||
return contractList ? keyBy(contractList, 'id') : undefined
|
return contractList ? keyBy(contractList, 'id') : undefined
|
||||||
}, [contractList])
|
}, [contractList])
|
||||||
|
|
||||||
|
const loadedPartialData =
|
||||||
|
userBets?.length === MAX_USER_BETS_LOADED ||
|
||||||
|
contractList?.length === MAX_USER_BET_CONTRACTS_LOADED
|
||||||
|
|
||||||
const [sort, setSort] = usePersistentState<BetSort>('newest', {
|
const [sort, setSort] = usePersistentState<BetSort>('newest', {
|
||||||
key: 'bets-list-sort',
|
key: 'bets-list-sort',
|
||||||
store: storageStore(safeLocalStorage()),
|
store: storageStore(safeLocalStorage()),
|
||||||
|
@ -167,6 +173,13 @@ export function BetsList(props: { user: User }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col>
|
<Col>
|
||||||
|
{loadedPartialData && (
|
||||||
|
<Row className="my-4 items-center gap-2 self-start rounded bg-yellow-50 p-4">
|
||||||
|
<ExclamationIcon className="h-5 w-5" />
|
||||||
|
<div>Partial trade data only</div>
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
|
||||||
<Col className="justify-between gap-4 sm:flex-row">
|
<Col className="justify-between gap-4 sm:flex-row">
|
||||||
<Row className="gap-4">
|
<Row className="gap-4">
|
||||||
<Col>
|
<Col>
|
||||||
|
@ -412,7 +425,9 @@ export function ContractBetsTable(props: {
|
||||||
const isNumeric = outcomeType === 'NUMERIC'
|
const isNumeric = outcomeType === 'NUMERIC'
|
||||||
const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC'
|
const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC'
|
||||||
|
|
||||||
const unfilledBets = useUnfilledBets(contract.id) ?? []
|
const { unfilledBets, balanceByUserId } = useUnfilledBetsAndBalanceByUserId(
|
||||||
|
contract.id
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
|
@ -461,6 +476,7 @@ export function ContractBetsTable(props: {
|
||||||
contract={contract}
|
contract={contract}
|
||||||
isYourBet={isYourBets}
|
isYourBet={isYourBets}
|
||||||
unfilledBets={unfilledBets}
|
unfilledBets={unfilledBets}
|
||||||
|
balanceByUserId={balanceByUserId}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -475,8 +491,10 @@ function BetRow(props: {
|
||||||
saleBet?: Bet
|
saleBet?: Bet
|
||||||
isYourBet: boolean
|
isYourBet: boolean
|
||||||
unfilledBets: LimitBet[]
|
unfilledBets: LimitBet[]
|
||||||
|
balanceByUserId: { [userId: string]: number }
|
||||||
}) {
|
}) {
|
||||||
const { bet, saleBet, contract, isYourBet, unfilledBets } = props
|
const { bet, saleBet, contract, isYourBet, unfilledBets, balanceByUserId } =
|
||||||
|
props
|
||||||
const {
|
const {
|
||||||
amount,
|
amount,
|
||||||
outcome,
|
outcome,
|
||||||
|
@ -504,9 +522,9 @@ function BetRow(props: {
|
||||||
} else if (contract.isResolved) {
|
} else if (contract.isResolved) {
|
||||||
return resolvedPayout(contract, bet)
|
return resolvedPayout(contract, bet)
|
||||||
} else {
|
} else {
|
||||||
return calculateSaleAmount(contract, bet, unfilledBets)
|
return calculateSaleAmount(contract, bet, unfilledBets, balanceByUserId)
|
||||||
}
|
}
|
||||||
}, [contract, bet, saleBet, unfilledBets])
|
}, [contract, bet, saleBet, unfilledBets, balanceByUserId])
|
||||||
|
|
||||||
const saleDisplay = isAnte ? (
|
const saleDisplay = isAnte ? (
|
||||||
'ANTE'
|
'ANTE'
|
||||||
|
@ -545,6 +563,7 @@ function BetRow(props: {
|
||||||
contract={contract}
|
contract={contract}
|
||||||
bet={bet}
|
bet={bet}
|
||||||
unfilledBets={unfilledBets}
|
unfilledBets={unfilledBets}
|
||||||
|
balanceByUserId={balanceByUserId}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
|
@ -590,8 +609,9 @@ function SellButton(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
bet: Bet
|
bet: Bet
|
||||||
unfilledBets: LimitBet[]
|
unfilledBets: LimitBet[]
|
||||||
|
balanceByUserId: { [userId: string]: number }
|
||||||
}) {
|
}) {
|
||||||
const { contract, bet, unfilledBets } = props
|
const { contract, bet, unfilledBets, balanceByUserId } = props
|
||||||
const { outcome, shares, loanAmount } = bet
|
const { outcome, shares, loanAmount } = bet
|
||||||
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
@ -605,10 +625,16 @@ function SellButton(props: {
|
||||||
contract,
|
contract,
|
||||||
outcome,
|
outcome,
|
||||||
shares,
|
shares,
|
||||||
unfilledBets
|
unfilledBets,
|
||||||
|
balanceByUserId
|
||||||
)
|
)
|
||||||
|
|
||||||
const saleAmount = calculateSaleAmount(contract, bet, unfilledBets)
|
const saleAmount = calculateSaleAmount(
|
||||||
|
contract,
|
||||||
|
bet,
|
||||||
|
unfilledBets,
|
||||||
|
balanceByUserId
|
||||||
|
)
|
||||||
const profit = saleAmount - bet.amount
|
const profit = saleAmount - bet.amount
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
|
import clsx from 'clsx'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
import { CurrencyDollarIcon } from '@heroicons/react/outline'
|
import { CurrencyDollarIcon } from '@heroicons/react/outline'
|
||||||
import { Contract } from 'common/contract'
|
import { Contract } from 'common/contract'
|
||||||
import { Tooltip } from 'web/components/tooltip'
|
import { Tooltip } from 'web/components/tooltip'
|
||||||
import { formatMoney } from 'common/util/format'
|
import { formatMoney } from 'common/util/format'
|
||||||
import { COMMENT_BOUNTY_AMOUNT } from 'common/economy'
|
import { COMMENT_BOUNTY_AMOUNT } from 'common/economy'
|
||||||
|
import { CommentBountyDialog } from './comment-bounty-dialog'
|
||||||
|
|
||||||
export function BountiedContractBadge() {
|
export function BountiedContractBadge() {
|
||||||
return (
|
return (
|
||||||
<span className="inline-flex items-center gap-1 rounded-full bg-blue-100 px-3 py-0.5 text-sm font-medium text-blue-800">
|
<span className="inline-flex items-center gap-1 rounded-full bg-indigo-300 px-3 py-0.5 text-sm font-medium text-white">
|
||||||
<CurrencyDollarIcon className={'h4 w-4'} /> Bounty
|
<CurrencyDollarIcon className={'h4 w-4'} /> Bounty
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
|
@ -18,30 +22,50 @@ export function BountiedContractSmallBadge(props: {
|
||||||
}) {
|
}) {
|
||||||
const { contract, showAmount } = props
|
const { contract, showAmount } = props
|
||||||
const { openCommentBounties } = contract
|
const { openCommentBounties } = contract
|
||||||
if (!openCommentBounties) return <div />
|
|
||||||
|
|
||||||
return (
|
const [open, setOpen] = useState(false)
|
||||||
<Tooltip
|
|
||||||
text={CommentBountiesTooltipText(
|
if (!openCommentBounties && !showAmount) return <></>
|
||||||
contract.creatorName,
|
|
||||||
openCommentBounties
|
const modal = (
|
||||||
)}
|
<CommentBountyDialog open={open} setOpen={setOpen} contract={contract} />
|
||||||
placement="bottom"
|
)
|
||||||
>
|
if (!openCommentBounties)
|
||||||
<span className="inline-flex items-center gap-1 whitespace-nowrap rounded-full bg-indigo-300 px-2 py-0.5 text-xs font-medium text-white">
|
return (
|
||||||
<CurrencyDollarIcon className={'h3 w-3'} />
|
<>
|
||||||
{showAmount && formatMoney(openCommentBounties)} Bounty
|
{modal}
|
||||||
</span>
|
<SmallBadge text="Add bounty" onClick={() => setOpen(true)} />
|
||||||
</Tooltip>
|
</>
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
export const CommentBountiesTooltipText = (
|
const tooltip = `${contract.creatorName} may award ${formatMoney(
|
||||||
creator: string,
|
|
||||||
openCommentBounties: number
|
|
||||||
) =>
|
|
||||||
`${creator} may award ${formatMoney(
|
|
||||||
COMMENT_BOUNTY_AMOUNT
|
COMMENT_BOUNTY_AMOUNT
|
||||||
)} for good comments. ${formatMoney(
|
)} for good comments. ${formatMoney(
|
||||||
openCommentBounties
|
openCommentBounties
|
||||||
)} currently available.`
|
)} currently available.`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip text={tooltip} placement="bottom">
|
||||||
|
{modal}
|
||||||
|
<SmallBadge
|
||||||
|
text={`${formatMoney(openCommentBounties)} bounty`}
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SmallBadge(props: { text: string; onClick?: () => void }) {
|
||||||
|
const { text, onClick } = props
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={onClick}
|
||||||
|
className={clsx(
|
||||||
|
'inline-flex items-center gap-1 whitespace-nowrap rounded-full bg-indigo-300 px-2 py-0.5 text-xs font-medium text-white'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CurrencyDollarIcon className={'h4 w-4'} />
|
||||||
|
{text}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -8,9 +8,16 @@ import clsx from 'clsx'
|
||||||
import { formatMoney } from 'common/util/format'
|
import { formatMoney } from 'common/util/format'
|
||||||
import { COMMENT_BOUNTY_AMOUNT } from 'common/economy'
|
import { COMMENT_BOUNTY_AMOUNT } from 'common/economy'
|
||||||
import { Button } from 'web/components/button'
|
import { Button } from 'web/components/button'
|
||||||
|
import { Title } from '../title'
|
||||||
|
import { Col } from '../layout/col'
|
||||||
|
import { Modal } from '../layout/modal'
|
||||||
|
|
||||||
export function AddCommentBountyPanel(props: { contract: Contract }) {
|
export function CommentBountyDialog(props: {
|
||||||
const { contract } = props
|
contract: Contract
|
||||||
|
open: boolean
|
||||||
|
setOpen: (open: boolean) => void
|
||||||
|
}) {
|
||||||
|
const { contract, open, setOpen } = props
|
||||||
const { id: contractId, slug } = contract
|
const { id: contractId, slug } = contract
|
||||||
|
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
@ -45,7 +52,10 @@ export function AddCommentBountyPanel(props: { contract: Contract }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Modal open={open} setOpen={setOpen}>
|
||||||
|
<Col className="gap-4 rounded bg-white p-6">
|
||||||
|
<Title className="!mt-0 !mb-0" text="Comment bounty" />
|
||||||
|
|
||||||
<div className="mb-4 text-gray-500">
|
<div className="mb-4 text-gray-500">
|
||||||
Add a {formatMoney(amount)} bounty for good comments that the creator
|
Add a {formatMoney(amount)} bounty for good comments that the creator
|
||||||
can award.{' '}
|
can award.{' '}
|
||||||
|
@ -69,6 +79,7 @@ export function AddCommentBountyPanel(props: { contract: Contract }) {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isLoading && <div>Processing...</div>}
|
{isLoading && <div>Processing...</div>}
|
||||||
</>
|
</Col>
|
||||||
|
</Modal>
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -5,7 +5,7 @@ import { useState } from 'react'
|
||||||
import { capitalize } from 'lodash'
|
import { capitalize } from 'lodash'
|
||||||
|
|
||||||
import { Contract } from 'common/contract'
|
import { Contract } from 'common/contract'
|
||||||
import { formatMoney } from 'common/util/format'
|
import { formatMoney, formatPercent } from 'common/util/format'
|
||||||
import { contractPool, updateContract } from 'web/lib/firebase/contracts'
|
import { contractPool, updateContract } from 'web/lib/firebase/contracts'
|
||||||
import { LiquidityBountyPanel } from 'web/components/contract/liquidity-bounty-panel'
|
import { LiquidityBountyPanel } from 'web/components/contract/liquidity-bounty-panel'
|
||||||
import { Col } from '../layout/col'
|
import { Col } from '../layout/col'
|
||||||
|
@ -54,6 +54,7 @@ export function ContractInfoDialog(props: {
|
||||||
mechanism,
|
mechanism,
|
||||||
outcomeType,
|
outcomeType,
|
||||||
id,
|
id,
|
||||||
|
elasticity,
|
||||||
} = contract
|
} = contract
|
||||||
|
|
||||||
const typeDisplay =
|
const typeDisplay =
|
||||||
|
@ -142,7 +143,10 @@ export function ContractInfoDialog(props: {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>Volume</td>
|
<td>
|
||||||
|
<span className="mr-1">Volume</span>
|
||||||
|
<InfoTooltip text="Total amount bought or sold" />
|
||||||
|
</td>
|
||||||
<td>{formatMoney(contract.volume)}</td>
|
<td>{formatMoney(contract.volume)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
@ -151,6 +155,22 @@ export function ContractInfoDialog(props: {
|
||||||
<td>{uniqueBettorCount ?? '0'}</td>
|
<td>{uniqueBettorCount ?? '0'}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<Row>
|
||||||
|
<span className="mr-1">Elasticity</span>
|
||||||
|
<InfoTooltip
|
||||||
|
text={
|
||||||
|
mechanism === 'cpmm-1'
|
||||||
|
? 'Probability change between a M$50 bet on YES and NO'
|
||||||
|
: 'Probability change from a M$100 bet'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
</td>
|
||||||
|
<td>{formatPercent(elasticity)}</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
{mechanism === 'cpmm-1' ? 'Liquidity pool' : 'Betting pool'}
|
{mechanism === 'cpmm-1' ? 'Liquidity pool' : 'Betting pool'}
|
||||||
|
|
|
@ -80,7 +80,7 @@ const CommentsTabContent = memo(function CommentsTabContent(props: {
|
||||||
const { contract } = props
|
const { contract } = props
|
||||||
const tips = useTipTxns({ contractId: contract.id })
|
const tips = useTipTxns({ contractId: contract.id })
|
||||||
const comments = useComments(contract.id) ?? props.comments
|
const comments = useComments(contract.id) ?? props.comments
|
||||||
const [sort, setSort] = usePersistentState<'Newest' | 'Best'>('Best', {
|
const [sort, setSort] = usePersistentState<'Newest' | 'Best'>('Newest', {
|
||||||
key: `contract-comments-sort`,
|
key: `contract-comments-sort`,
|
||||||
store: storageStore(safeLocalStorage()),
|
store: storageStore(safeLocalStorage()),
|
||||||
})
|
})
|
||||||
|
@ -177,8 +177,9 @@ const CommentsTabContent = memo(function CommentsTabContent(props: {
|
||||||
<Col className="mt-8 flex w-full">
|
<Col className="mt-8 flex w-full">
|
||||||
<div className="text-md mt-8 mb-2 text-left">General Comments</div>
|
<div className="text-md mt-8 mb-2 text-left">General Comments</div>
|
||||||
<div className="mb-4 w-full border-b border-gray-200" />
|
<div className="mb-4 w-full border-b border-gray-200" />
|
||||||
{sortRow}
|
|
||||||
<ContractCommentInput className="mb-5" contract={contract} />
|
<ContractCommentInput className="mb-5" contract={contract} />
|
||||||
|
{sortRow}
|
||||||
|
|
||||||
{generalTopLevelComments.map((comment) => (
|
{generalTopLevelComments.map((comment) => (
|
||||||
<FeedCommentThread
|
<FeedCommentThread
|
||||||
key={comment.id}
|
key={comment.id}
|
||||||
|
@ -194,8 +195,9 @@ const CommentsTabContent = memo(function CommentsTabContent(props: {
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{sortRow}
|
|
||||||
<ContractCommentInput className="mb-5" contract={contract} />
|
<ContractCommentInput className="mb-5" contract={contract} />
|
||||||
|
{sortRow}
|
||||||
|
|
||||||
{topLevelComments.map((parent) => (
|
{topLevelComments.map((parent) => (
|
||||||
<FeedCommentThread
|
<FeedCommentThread
|
||||||
key={parent.id}
|
key={parent.id}
|
||||||
|
|
|
@ -16,7 +16,6 @@ import { InfoTooltip } from 'web/components/info-tooltip'
|
||||||
import { BETTORS, PRESENT_BET } from 'common/user'
|
import { BETTORS, PRESENT_BET } from 'common/user'
|
||||||
import { buildArray } from 'common/util/array'
|
import { buildArray } from 'common/util/array'
|
||||||
import { useAdmin } from 'web/hooks/use-admin'
|
import { useAdmin } from 'web/hooks/use-admin'
|
||||||
import { AddCommentBountyPanel } from 'web/components/contract/add-comment-bounty'
|
|
||||||
|
|
||||||
export function LiquidityBountyPanel(props: { contract: Contract }) {
|
export function LiquidityBountyPanel(props: { contract: Contract }) {
|
||||||
const { contract } = props
|
const { contract } = props
|
||||||
|
@ -36,13 +35,11 @@ export function LiquidityBountyPanel(props: { contract: Contract }) {
|
||||||
const isCreator = user?.id === contract.creatorId
|
const isCreator = user?.id === contract.creatorId
|
||||||
const isAdmin = useAdmin()
|
const isAdmin = useAdmin()
|
||||||
|
|
||||||
|
if (!isCreator && !isAdmin && !showWithdrawal) return <></>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs
|
<Tabs
|
||||||
tabs={buildArray(
|
tabs={buildArray(
|
||||||
{
|
|
||||||
title: 'Bounty Comments',
|
|
||||||
content: <AddCommentBountyPanel contract={contract} />,
|
|
||||||
},
|
|
||||||
(isCreator || isAdmin) &&
|
(isCreator || isAdmin) &&
|
||||||
isCPMM && {
|
isCPMM && {
|
||||||
title: (isAdmin ? '[Admin] ' : '') + 'Subsidize',
|
title: (isAdmin ? '[Admin] ' : '') + 'Subsidize',
|
||||||
|
|
|
@ -33,7 +33,7 @@ import { sellShares } from 'web/lib/firebase/api'
|
||||||
import { calculateCpmmSale, getCpmmProbability } from 'common/calculate-cpmm'
|
import { calculateCpmmSale, getCpmmProbability } from 'common/calculate-cpmm'
|
||||||
import { track } from 'web/lib/service/analytics'
|
import { track } from 'web/lib/service/analytics'
|
||||||
import { formatNumericProbability } from 'common/pseudo-numeric'
|
import { formatNumericProbability } from 'common/pseudo-numeric'
|
||||||
import { useUnfilledBets } from 'web/hooks/use-bets'
|
import { useUnfilledBetsAndBalanceByUserId } from 'web/hooks/use-bets'
|
||||||
import { getBinaryProb } from 'common/contract-details'
|
import { getBinaryProb } from 'common/contract-details'
|
||||||
|
|
||||||
const BET_SIZE = 10
|
const BET_SIZE = 10
|
||||||
|
@ -48,7 +48,10 @@ export function QuickBet(props: {
|
||||||
const isCpmm = mechanism === 'cpmm-1'
|
const isCpmm = mechanism === 'cpmm-1'
|
||||||
|
|
||||||
const userBets = useUserContractBets(user.id, contract.id)
|
const userBets = useUserContractBets(user.id, contract.id)
|
||||||
const unfilledBets = useUnfilledBets(contract.id) ?? []
|
// TODO: Below hook fetches a decent amount of data. Maybe not worth it to show prob change on hover?
|
||||||
|
const { unfilledBets, balanceByUserId } = useUnfilledBetsAndBalanceByUserId(
|
||||||
|
contract.id
|
||||||
|
)
|
||||||
|
|
||||||
const { hasYesShares, hasNoShares, yesShares, noShares } =
|
const { hasYesShares, hasNoShares, yesShares, noShares } =
|
||||||
useSaveBinaryShares(contract, userBets)
|
useSaveBinaryShares(contract, userBets)
|
||||||
|
@ -94,7 +97,8 @@ export function QuickBet(props: {
|
||||||
contract,
|
contract,
|
||||||
sharesSold,
|
sharesSold,
|
||||||
sellOutcome,
|
sellOutcome,
|
||||||
unfilledBets
|
unfilledBets,
|
||||||
|
balanceByUserId
|
||||||
)
|
)
|
||||||
saleAmount = saleValue
|
saleAmount = saleValue
|
||||||
previewProb = getCpmmProbability(cpmmState.pool, cpmmState.p)
|
previewProb = getCpmmProbability(cpmmState.pool, cpmmState.p)
|
||||||
|
|
|
@ -109,12 +109,18 @@ export const FeedComment = memo(function FeedComment(props: {
|
||||||
}
|
}
|
||||||
const totalAwarded = bountiesAwarded ?? 0
|
const totalAwarded = bountiesAwarded ?? 0
|
||||||
|
|
||||||
const router = useRouter()
|
const { isReady, asPath } = useRouter()
|
||||||
const highlighted = router.asPath.endsWith(`#${comment.id}`)
|
const [highlighted, setHighlighted] = useState(false)
|
||||||
const commentRef = useRef<HTMLDivElement>(null)
|
const commentRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (highlighted && commentRef.current != null) {
|
if (isReady && asPath.endsWith(`#${comment.id}`)) {
|
||||||
|
setHighlighted(true)
|
||||||
|
}
|
||||||
|
}, [isReady, asPath, comment.id])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (highlighted && commentRef.current) {
|
||||||
commentRef.current.scrollIntoView(true)
|
commentRef.current.scrollIntoView(true)
|
||||||
}
|
}
|
||||||
}, [highlighted])
|
}, [highlighted])
|
||||||
|
@ -126,7 +132,7 @@ export const FeedComment = memo(function FeedComment(props: {
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'relative',
|
'relative',
|
||||||
indent ? 'ml-6' : '',
|
indent ? 'ml-6' : '',
|
||||||
highlighted ? `-m-1.5 rounded bg-indigo-500/[0.2] p-1.5` : ''
|
highlighted ? `-m-1.5 rounded bg-indigo-500/[0.2] px-2 py-4` : ''
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/*draw a gray line from the comment to the left:*/}
|
{/*draw a gray line from the comment to the left:*/}
|
||||||
|
|
|
@ -11,13 +11,13 @@ function SidebarButton(props: {
|
||||||
}) {
|
}) {
|
||||||
const { text, children } = props
|
const { text, children } = props
|
||||||
return (
|
return (
|
||||||
<button className="group flex w-full items-center rounded-md px-3 py-2 text-sm font-medium text-gray-600 hover:cursor-pointer hover:bg-gray-100">
|
<div className="group flex w-full items-center rounded-md px-3 py-2 text-sm font-medium text-gray-600 hover:cursor-pointer hover:bg-gray-100">
|
||||||
<props.icon
|
<props.icon
|
||||||
className="-ml-1 mr-3 h-6 w-6 flex-shrink-0 text-gray-400 group-hover:text-gray-500"
|
className="-ml-1 mr-3 h-6 w-6 flex-shrink-0 text-gray-400 group-hover:text-gray-500"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
<span className="truncate">{text}</span>
|
<span className="truncate">{text}</span>
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
withoutAnteBets,
|
withoutAnteBets,
|
||||||
} from 'web/lib/firebase/bets'
|
} from 'web/lib/firebase/bets'
|
||||||
import { LimitBet } from 'common/bet'
|
import { LimitBet } from 'common/bet'
|
||||||
|
import { getUser } from 'web/lib/firebase/users'
|
||||||
|
|
||||||
export const useBets = (
|
export const useBets = (
|
||||||
contractId: string,
|
contractId: string,
|
||||||
|
@ -62,3 +63,31 @@ export const useUnfilledBets = (contractId: string) => {
|
||||||
)
|
)
|
||||||
return unfilledBets
|
return unfilledBets
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useUnfilledBetsAndBalanceByUserId = (contractId: string) => {
|
||||||
|
const [data, setData] = useState<{
|
||||||
|
unfilledBets: LimitBet[]
|
||||||
|
balanceByUserId: { [userId: string]: number }
|
||||||
|
}>({ unfilledBets: [], balanceByUserId: {} })
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let requestCount = 0
|
||||||
|
|
||||||
|
return listenForUnfilledBets(contractId, (unfilledBets) => {
|
||||||
|
requestCount++
|
||||||
|
const count = requestCount
|
||||||
|
|
||||||
|
Promise.all(unfilledBets.map((bet) => getUser(bet.userId))).then(
|
||||||
|
(users) => {
|
||||||
|
if (count === requestCount) {
|
||||||
|
const balanceByUserId = Object.fromEntries(
|
||||||
|
users.map((user) => [user.id, user.balance])
|
||||||
|
)
|
||||||
|
setData({ unfilledBets, balanceByUserId })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}, [contractId])
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
|
@ -74,11 +74,13 @@ export async function getUserBets(userId: string) {
|
||||||
return getValues<Bet>(getUserBetsQuery(userId))
|
return getValues<Bet>(getUserBetsQuery(userId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const MAX_USER_BETS_LOADED = 10000
|
||||||
export function getUserBetsQuery(userId: string) {
|
export function getUserBetsQuery(userId: string) {
|
||||||
return query(
|
return query(
|
||||||
collectionGroup(db, 'bets'),
|
collectionGroup(db, 'bets'),
|
||||||
where('userId', '==', userId),
|
where('userId', '==', userId),
|
||||||
orderBy('createdTime', 'desc')
|
orderBy('createdTime', 'desc'),
|
||||||
|
limit(MAX_USER_BETS_LOADED)
|
||||||
) as Query<Bet>
|
) as Query<Bet>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -168,10 +168,12 @@ export function getUserBetContracts(userId: string) {
|
||||||
return getValues<Contract>(getUserBetContractsQuery(userId))
|
return getValues<Contract>(getUserBetContractsQuery(userId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const MAX_USER_BET_CONTRACTS_LOADED = 1000
|
||||||
export function getUserBetContractsQuery(userId: string) {
|
export function getUserBetContractsQuery(userId: string) {
|
||||||
return query(
|
return query(
|
||||||
contracts,
|
contracts,
|
||||||
where('uniqueBettorIds', 'array-contains', userId)
|
where('uniqueBettorIds', 'array-contains', userId),
|
||||||
|
limit(MAX_USER_BET_CONTRACTS_LOADED)
|
||||||
) as Query<Contract>
|
) as Query<Contract>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user