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