Update to 1.3.1, merge branch 'main' into link-summoner
This commit is contained in:
commit
7caaa69b6f
55
.github/workflows/check.yml
vendored
Normal file
55
.github/workflows/check.yml
vendored
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
name: Check PRs
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_COLOR: 3
|
||||||
|
NEXT_TELEMETRY_DISABLED: 1
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
name: Static analysis
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Restore cached node_modules
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: '**/node_modules'
|
||||||
|
key: ${{ runner.os }}-${{ matrix.node-version }}-nodemodules-${{ hashFiles('**/yarn.lock') }}
|
||||||
|
- name: Install missing dependencies
|
||||||
|
run: yarn install --prefer-offline --frozen-lockfile
|
||||||
|
- name: Run Prettier on web client
|
||||||
|
working-directory: web
|
||||||
|
run: npx prettier --check .
|
||||||
|
- name: Run ESLint on common
|
||||||
|
if: ${{ success() || failure() }}
|
||||||
|
working-directory: common
|
||||||
|
run: npx eslint . --max-warnings 0
|
||||||
|
- name: Run ESLint on web client
|
||||||
|
if: ${{ success() || failure() }}
|
||||||
|
working-directory: web
|
||||||
|
run: yarn lint --max-warnings 0
|
||||||
|
- name: Run ESLint on cloud functions
|
||||||
|
if: ${{ success() || failure() }}
|
||||||
|
working-directory: functions
|
||||||
|
run: npx eslint . --max-warnings 0
|
||||||
|
- name: Run Typescript checker on web client
|
||||||
|
if: ${{ success() || failure() }}
|
||||||
|
working-directory: web
|
||||||
|
run: tsc --pretty --project tsconfig.json --noEmit
|
||||||
|
- name: Run Typescript checker on cloud functions
|
||||||
|
if: ${{ success() || failure() }}
|
||||||
|
working-directory: functions
|
||||||
|
run: tsc --pretty --project tsconfig.json --noEmit
|
29
common/.eslintrc.js
Normal file
29
common/.eslintrc.js
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
module.exports = {
|
||||||
|
plugins: ['lodash'],
|
||||||
|
extends: ['eslint:recommended'],
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ['**/*.ts'],
|
||||||
|
plugins: ['@typescript-eslint'],
|
||||||
|
extends: ['plugin:@typescript-eslint/recommended'],
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
parserOptions: {
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
project: ['./tsconfig.json'],
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
'no-extra-semi': 'off',
|
||||||
|
'no-unused-vars': 'off',
|
||||||
|
'no-constant-condition': ['error', { checkLoops: false }],
|
||||||
|
'lodash/import-scope': [2, 'member'],
|
||||||
|
},
|
||||||
|
}
|
|
@ -1,6 +1,14 @@
|
||||||
import { Bet } from './bet'
|
import { range } from 'lodash'
|
||||||
import { getDpmProbability } from './calculate-dpm'
|
import { Bet, NumericBet } from './bet'
|
||||||
import { Binary, CPMM, DPM, FreeResponse, FullContract } from './contract'
|
import { getDpmProbability, getValueFromBucket } from './calculate-dpm'
|
||||||
|
import {
|
||||||
|
Binary,
|
||||||
|
CPMM,
|
||||||
|
DPM,
|
||||||
|
FreeResponse,
|
||||||
|
FullContract,
|
||||||
|
Numeric,
|
||||||
|
} from './contract'
|
||||||
import { User } from './user'
|
import { User } from './user'
|
||||||
import { LiquidityProvision } from './liquidity-provision'
|
import { LiquidityProvision } from './liquidity-provision'
|
||||||
import { noFees } from './fees'
|
import { noFees } from './fees'
|
||||||
|
@ -80,7 +88,7 @@ export function getAnteBets(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getFreeAnswerAnte(
|
export function getFreeAnswerAnte(
|
||||||
creator: User,
|
anteBettorId: string,
|
||||||
contract: FullContract<DPM, FreeResponse>,
|
contract: FullContract<DPM, FreeResponse>,
|
||||||
anteBetId: string
|
anteBetId: string
|
||||||
) {
|
) {
|
||||||
|
@ -92,7 +100,7 @@ export function getFreeAnswerAnte(
|
||||||
|
|
||||||
const anteBet: Bet = {
|
const anteBet: Bet = {
|
||||||
id: anteBetId,
|
id: anteBetId,
|
||||||
userId: creator.id,
|
userId: anteBettorId,
|
||||||
contractId: contract.id,
|
contractId: contract.id,
|
||||||
amount,
|
amount,
|
||||||
shares,
|
shares,
|
||||||
|
@ -106,3 +114,42 @@ export function getFreeAnswerAnte(
|
||||||
|
|
||||||
return anteBet
|
return anteBet
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getNumericAnte(
|
||||||
|
creator: User,
|
||||||
|
contract: FullContract<DPM, Numeric>,
|
||||||
|
ante: number,
|
||||||
|
newBetId: string
|
||||||
|
) {
|
||||||
|
const { bucketCount, createdTime } = contract
|
||||||
|
|
||||||
|
const betAnte = ante / bucketCount
|
||||||
|
const betShares = Math.sqrt(ante ** 2 / bucketCount)
|
||||||
|
|
||||||
|
const allOutcomeShares = Object.fromEntries(
|
||||||
|
range(0, bucketCount).map((_, i) => [i, betShares])
|
||||||
|
)
|
||||||
|
|
||||||
|
const allBetAmounts = Object.fromEntries(
|
||||||
|
range(0, bucketCount).map((_, i) => [i, betAnte])
|
||||||
|
)
|
||||||
|
|
||||||
|
const anteBet: NumericBet = {
|
||||||
|
id: newBetId,
|
||||||
|
userId: creator.id,
|
||||||
|
contractId: contract.id,
|
||||||
|
amount: ante,
|
||||||
|
allBetAmounts,
|
||||||
|
outcome: '0',
|
||||||
|
value: getValueFromBucket('0', contract),
|
||||||
|
shares: betShares,
|
||||||
|
allOutcomeShares,
|
||||||
|
probBefore: 0,
|
||||||
|
probAfter: 1 / bucketCount,
|
||||||
|
createdTime,
|
||||||
|
isAnte: true,
|
||||||
|
fees: noFees,
|
||||||
|
}
|
||||||
|
|
||||||
|
return anteBet
|
||||||
|
}
|
||||||
|
|
|
@ -29,4 +29,10 @@ export type Bet = {
|
||||||
createdTime: number
|
createdTime: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type NumericBet = Bet & {
|
||||||
|
value: number
|
||||||
|
allOutcomeShares: { [outcome: string]: number }
|
||||||
|
allBetAmounts: { [outcome: string]: number }
|
||||||
|
}
|
||||||
|
|
||||||
export const MAX_LOAN_PER_CONTRACT = 20
|
export const MAX_LOAN_PER_CONTRACT = 20
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import * as _ from 'lodash'
|
import { sum, groupBy, mapValues, sumBy } from 'lodash'
|
||||||
|
|
||||||
import { Binary, CPMM, FullContract } from './contract'
|
import { Binary, CPMM, FullContract } from './contract'
|
||||||
import { CREATOR_FEE, Fees, LIQUIDITY_FEE, noFees, PLATFORM_FEE } from './fees'
|
import { CREATOR_FEE, Fees, LIQUIDITY_FEE, noFees, PLATFORM_FEE } from './fees'
|
||||||
|
@ -63,10 +63,8 @@ export function getCpmmLiquidityFee(
|
||||||
bet: number,
|
bet: number,
|
||||||
outcome: string
|
outcome: string
|
||||||
) {
|
) {
|
||||||
const probBefore = getCpmmProbability(contract.pool, contract.p)
|
const prob = getCpmmProbabilityAfterBetBeforeFees(contract, outcome, bet)
|
||||||
const probAfter = getCpmmProbabilityAfterBetBeforeFees(contract, outcome, bet)
|
const betP = outcome === 'YES' ? 1 - prob : prob
|
||||||
const probMid = Math.sqrt(probBefore * probAfter)
|
|
||||||
const betP = outcome === 'YES' ? 1 - probMid : probMid
|
|
||||||
|
|
||||||
const liquidityFee = LIQUIDITY_FEE * betP * bet
|
const liquidityFee = LIQUIDITY_FEE * betP * bet
|
||||||
const platformFee = PLATFORM_FEE * betP * bet
|
const platformFee = PLATFORM_FEE * betP * bet
|
||||||
|
@ -278,16 +276,16 @@ export function getCpmmLiquidityPoolWeights(
|
||||||
return liquidity
|
return liquidity
|
||||||
})
|
})
|
||||||
|
|
||||||
const shareSum = _.sum(liquidityShares)
|
const shareSum = sum(liquidityShares)
|
||||||
|
|
||||||
const weights = liquidityShares.map((s, i) => ({
|
const weights = liquidityShares.map((s, i) => ({
|
||||||
weight: s / shareSum,
|
weight: s / shareSum,
|
||||||
providerId: liquidities[i].userId,
|
providerId: liquidities[i].userId,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const userWeights = _.groupBy(weights, (w) => w.providerId)
|
const userWeights = groupBy(weights, (w) => w.providerId)
|
||||||
const totalUserWeights = _.mapValues(userWeights, (userWeight) =>
|
const totalUserWeights = mapValues(userWeights, (userWeight) =>
|
||||||
_.sumBy(userWeight, (w) => w.weight)
|
sumBy(userWeight, (w) => w.weight)
|
||||||
)
|
)
|
||||||
return totalUserWeights
|
return totalUserWeights
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,16 @@
|
||||||
import * as _ from 'lodash'
|
import { cloneDeep, range, sum, sumBy, sortBy, mapValues } from 'lodash'
|
||||||
import { Bet } from './bet'
|
import { Bet, NumericBet } from './bet'
|
||||||
import { Binary, DPM, FreeResponse, FullContract } from './contract'
|
import {
|
||||||
|
Binary,
|
||||||
|
DPM,
|
||||||
|
FreeResponse,
|
||||||
|
FullContract,
|
||||||
|
Numeric,
|
||||||
|
NumericContract,
|
||||||
|
} from './contract'
|
||||||
import { DPM_FEES } from './fees'
|
import { DPM_FEES } from './fees'
|
||||||
|
import { normpdf } from '../common/util/math'
|
||||||
|
import { addObjects } from './util/object'
|
||||||
|
|
||||||
export function getDpmProbability(totalShares: { [outcome: string]: number }) {
|
export function getDpmProbability(totalShares: { [outcome: string]: number }) {
|
||||||
// For binary contracts only.
|
// For binary contracts only.
|
||||||
|
@ -14,11 +23,94 @@ export function getDpmOutcomeProbability(
|
||||||
},
|
},
|
||||||
outcome: string
|
outcome: string
|
||||||
) {
|
) {
|
||||||
const squareSum = _.sumBy(Object.values(totalShares), (shares) => shares ** 2)
|
const squareSum = sumBy(Object.values(totalShares), (shares) => shares ** 2)
|
||||||
const shares = totalShares[outcome] ?? 0
|
const shares = totalShares[outcome] ?? 0
|
||||||
return shares ** 2 / squareSum
|
return shares ** 2 / squareSum
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getDpmOutcomeProbabilities(totalShares: {
|
||||||
|
[outcome: string]: number
|
||||||
|
}) {
|
||||||
|
const squareSum = sumBy(Object.values(totalShares), (shares) => shares ** 2)
|
||||||
|
return mapValues(totalShares, (shares) => shares ** 2 / squareSum)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getNumericBets(
|
||||||
|
contract: NumericContract,
|
||||||
|
bucket: string,
|
||||||
|
betAmount: number,
|
||||||
|
variance: number
|
||||||
|
) {
|
||||||
|
const { bucketCount } = contract
|
||||||
|
const bucketNumber = parseInt(bucket)
|
||||||
|
const buckets = range(0, bucketCount)
|
||||||
|
|
||||||
|
const mean = bucketNumber / bucketCount
|
||||||
|
|
||||||
|
const allDensities = buckets.map((i) =>
|
||||||
|
normpdf(i / bucketCount, mean, variance)
|
||||||
|
)
|
||||||
|
const densitySum = sum(allDensities)
|
||||||
|
|
||||||
|
const rawBetAmounts = allDensities
|
||||||
|
.map((d) => (d / densitySum) * betAmount)
|
||||||
|
.map((x) => (x >= 1 / bucketCount ? x : 0))
|
||||||
|
|
||||||
|
const rawSum = sum(rawBetAmounts)
|
||||||
|
const scaledBetAmounts = rawBetAmounts.map((x) => (x / rawSum) * betAmount)
|
||||||
|
|
||||||
|
const bets = scaledBetAmounts
|
||||||
|
.map((x, i) => (x > 0 ? [i.toString(), x] : undefined))
|
||||||
|
.filter((x) => x != undefined) as [string, number][]
|
||||||
|
|
||||||
|
return bets
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getMappedBucket = (value: number, contract: NumericContract) => {
|
||||||
|
const { bucketCount, min, max } = contract
|
||||||
|
|
||||||
|
const index = Math.floor(((value - min) / (max - min)) * bucketCount)
|
||||||
|
const bucket = Math.max(Math.min(index, bucketCount - 1), 0)
|
||||||
|
|
||||||
|
return `${bucket}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getValueFromBucket = (
|
||||||
|
bucket: string,
|
||||||
|
contract: NumericContract
|
||||||
|
) => {
|
||||||
|
const { bucketCount, min, max } = contract
|
||||||
|
const index = parseInt(bucket)
|
||||||
|
const value = min + (index / bucketCount) * (max - min)
|
||||||
|
const rounded = Math.round(value * 1e4) / 1e4
|
||||||
|
return rounded
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getExpectedValue = (contract: NumericContract) => {
|
||||||
|
const { bucketCount, min, max, totalShares } = contract
|
||||||
|
|
||||||
|
const totalShareSum = sumBy(
|
||||||
|
Object.values(totalShares),
|
||||||
|
(shares) => shares ** 2
|
||||||
|
)
|
||||||
|
const probs = range(0, bucketCount).map(
|
||||||
|
(i) => totalShares[i] ** 2 / totalShareSum
|
||||||
|
)
|
||||||
|
|
||||||
|
const values = range(0, bucketCount).map(
|
||||||
|
(i) =>
|
||||||
|
// use mid point within bucket
|
||||||
|
0.5 * (min + (i / bucketCount) * (max - min)) +
|
||||||
|
0.5 * (min + ((i + 1) / bucketCount) * (max - min))
|
||||||
|
)
|
||||||
|
|
||||||
|
const weightedValues = range(0, bucketCount).map((i) => probs[i] * values[i])
|
||||||
|
|
||||||
|
const expectation = sum(weightedValues)
|
||||||
|
const rounded = Math.round(expectation * 1e2) / 1e2
|
||||||
|
return rounded
|
||||||
|
}
|
||||||
|
|
||||||
export function getDpmOutcomeProbabilityAfterBet(
|
export function getDpmOutcomeProbabilityAfterBet(
|
||||||
totalShares: {
|
totalShares: {
|
||||||
[outcome: string]: number
|
[outcome: string]: number
|
||||||
|
@ -55,7 +147,7 @@ export function calculateDpmShares(
|
||||||
bet: number,
|
bet: number,
|
||||||
betChoice: string
|
betChoice: string
|
||||||
) {
|
) {
|
||||||
const squareSum = _.sumBy(Object.values(totalShares), (shares) => shares ** 2)
|
const squareSum = sumBy(Object.values(totalShares), (shares) => shares ** 2)
|
||||||
const shares = totalShares[betChoice] ?? 0
|
const shares = totalShares[betChoice] ?? 0
|
||||||
|
|
||||||
const c = 2 * bet * Math.sqrt(squareSum)
|
const c = 2 * bet * Math.sqrt(squareSum)
|
||||||
|
@ -63,6 +155,30 @@ export function calculateDpmShares(
|
||||||
return Math.sqrt(bet ** 2 + shares ** 2 + c) - shares
|
return Math.sqrt(bet ** 2 + shares ** 2 + c) - shares
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function calculateNumericDpmShares(
|
||||||
|
totalShares: {
|
||||||
|
[outcome: string]: number
|
||||||
|
},
|
||||||
|
bets: [string, number][]
|
||||||
|
) {
|
||||||
|
const shares: number[] = []
|
||||||
|
|
||||||
|
totalShares = cloneDeep(totalShares)
|
||||||
|
|
||||||
|
const order = sortBy(
|
||||||
|
bets.map(([, amount], i) => [amount, i]),
|
||||||
|
([amount]) => amount
|
||||||
|
).map(([, i]) => i)
|
||||||
|
|
||||||
|
for (const i of order) {
|
||||||
|
const [bucket, bet] = bets[i]
|
||||||
|
shares[i] = calculateDpmShares(totalShares, bet, bucket)
|
||||||
|
totalShares = addObjects(totalShares, { [bucket]: shares[i] })
|
||||||
|
}
|
||||||
|
|
||||||
|
return { shares, totalShares }
|
||||||
|
}
|
||||||
|
|
||||||
export function calculateDpmRawShareValue(
|
export function calculateDpmRawShareValue(
|
||||||
totalShares: {
|
totalShares: {
|
||||||
[outcome: string]: number
|
[outcome: string]: number
|
||||||
|
@ -71,11 +187,11 @@ export function calculateDpmRawShareValue(
|
||||||
betChoice: string
|
betChoice: string
|
||||||
) {
|
) {
|
||||||
const currentValue = Math.sqrt(
|
const currentValue = Math.sqrt(
|
||||||
_.sumBy(Object.values(totalShares), (shares) => shares ** 2)
|
sumBy(Object.values(totalShares), (shares) => shares ** 2)
|
||||||
)
|
)
|
||||||
|
|
||||||
const postSaleValue = Math.sqrt(
|
const postSaleValue = Math.sqrt(
|
||||||
_.sumBy(Object.keys(totalShares), (outcome) =>
|
sumBy(Object.keys(totalShares), (outcome) =>
|
||||||
outcome === betChoice
|
outcome === betChoice
|
||||||
? Math.max(0, totalShares[outcome] - shares) ** 2
|
? Math.max(0, totalShares[outcome] - shares) ** 2
|
||||||
: totalShares[outcome] ** 2
|
: totalShares[outcome] ** 2
|
||||||
|
@ -95,12 +211,12 @@ export function calculateDpmMoneyRatio(
|
||||||
|
|
||||||
const p = getDpmOutcomeProbability(totalShares, outcome)
|
const p = getDpmOutcomeProbability(totalShares, outcome)
|
||||||
|
|
||||||
const actual = _.sum(Object.values(pool)) - shareValue
|
const actual = sum(Object.values(pool)) - shareValue
|
||||||
|
|
||||||
const betAmount = p * amount
|
const betAmount = p * amount
|
||||||
|
|
||||||
const expected =
|
const expected =
|
||||||
_.sumBy(
|
sumBy(
|
||||||
Object.keys(totalBets),
|
Object.keys(totalBets),
|
||||||
(outcome) =>
|
(outcome) =>
|
||||||
getDpmOutcomeProbability(totalShares, outcome) *
|
getDpmOutcomeProbability(totalShares, outcome) *
|
||||||
|
@ -152,8 +268,8 @@ export function calculateDpmCancelPayout(
|
||||||
bet: Bet
|
bet: Bet
|
||||||
) {
|
) {
|
||||||
const { totalBets, pool } = contract
|
const { totalBets, pool } = contract
|
||||||
const betTotal = _.sum(Object.values(totalBets))
|
const betTotal = sum(Object.values(totalBets))
|
||||||
const poolTotal = _.sum(Object.values(pool))
|
const poolTotal = sum(Object.values(pool))
|
||||||
|
|
||||||
return (bet.amount / betTotal) * poolTotal
|
return (bet.amount / betTotal) * poolTotal
|
||||||
}
|
}
|
||||||
|
@ -163,27 +279,39 @@ export function calculateStandardDpmPayout(
|
||||||
bet: Bet,
|
bet: Bet,
|
||||||
outcome: string
|
outcome: string
|
||||||
) {
|
) {
|
||||||
const { amount, outcome: betOutcome, shares } = bet
|
const { outcome: betOutcome } = bet
|
||||||
if (betOutcome !== outcome) return 0
|
const isNumeric = contract.outcomeType === 'NUMERIC'
|
||||||
|
if (!isNumeric && betOutcome !== outcome) return 0
|
||||||
|
|
||||||
|
const shares = isNumeric
|
||||||
|
? ((bet as NumericBet).allOutcomeShares ?? {})[outcome]
|
||||||
|
: bet.shares
|
||||||
|
|
||||||
|
if (!shares) return 0
|
||||||
|
|
||||||
const { totalShares, phantomShares, pool } = contract
|
const { totalShares, phantomShares, pool } = contract
|
||||||
if (!totalShares[outcome]) return 0
|
if (!totalShares[outcome]) return 0
|
||||||
|
|
||||||
const poolTotal = _.sum(Object.values(pool))
|
const poolTotal = sum(Object.values(pool))
|
||||||
|
|
||||||
const total =
|
const total =
|
||||||
totalShares[outcome] - (phantomShares ? phantomShares[outcome] : 0)
|
totalShares[outcome] - (phantomShares ? phantomShares[outcome] : 0)
|
||||||
|
|
||||||
const winnings = (shares / total) * poolTotal
|
const winnings = (shares / total) * poolTotal
|
||||||
// profit can be negative if using phantom shares
|
|
||||||
return amount + (1 - DPM_FEES) * Math.max(0, winnings - amount)
|
const amount = isNumeric
|
||||||
|
? (bet as NumericBet).allBetAmounts[outcome]
|
||||||
|
: bet.amount
|
||||||
|
|
||||||
|
const payout = amount + (1 - DPM_FEES) * Math.max(0, winnings - amount)
|
||||||
|
return payout
|
||||||
}
|
}
|
||||||
|
|
||||||
export function calculateDpmPayoutAfterCorrectBet(
|
export function calculateDpmPayoutAfterCorrectBet(
|
||||||
contract: FullContract<DPM, any>,
|
contract: FullContract<DPM, any>,
|
||||||
bet: Bet
|
bet: Bet
|
||||||
) {
|
) {
|
||||||
const { totalShares, pool, totalBets } = contract
|
const { totalShares, pool, totalBets, outcomeType } = contract
|
||||||
const { shares, amount, outcome } = bet
|
const { shares, amount, outcome } = bet
|
||||||
|
|
||||||
const prevShares = totalShares[outcome] ?? 0
|
const prevShares = totalShares[outcome] ?? 0
|
||||||
|
@ -204,45 +332,60 @@ export function calculateDpmPayoutAfterCorrectBet(
|
||||||
...totalBets,
|
...totalBets,
|
||||||
[outcome]: prevTotalBet + amount,
|
[outcome]: prevTotalBet + amount,
|
||||||
},
|
},
|
||||||
|
outcomeType:
|
||||||
|
outcomeType === 'NUMERIC'
|
||||||
|
? 'FREE_RESPONSE' // hack to show payout at particular bet point estimate
|
||||||
|
: outcomeType,
|
||||||
}
|
}
|
||||||
|
|
||||||
return calculateStandardDpmPayout(newContract, bet, outcome)
|
return calculateStandardDpmPayout(newContract, bet, outcome)
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculateMktDpmPayout(contract: FullContract<DPM, any>, bet: Bet) {
|
function calculateMktDpmPayout(
|
||||||
|
contract: FullContract<DPM, Binary | FreeResponse | Numeric>,
|
||||||
|
bet: Bet
|
||||||
|
) {
|
||||||
if (contract.outcomeType === 'BINARY')
|
if (contract.outcomeType === 'BINARY')
|
||||||
return calculateBinaryMktDpmPayout(contract, bet)
|
return calculateBinaryMktDpmPayout(contract, bet)
|
||||||
|
|
||||||
const { totalShares, pool, resolutions } = contract as FullContract<
|
const { totalShares, pool, resolutions, outcomeType } = contract
|
||||||
DPM,
|
|
||||||
FreeResponse
|
|
||||||
>
|
|
||||||
|
|
||||||
let probs: { [outcome: string]: number }
|
let probs: { [outcome: string]: number }
|
||||||
|
|
||||||
if (resolutions) {
|
if (resolutions) {
|
||||||
const probTotal = _.sum(Object.values(resolutions))
|
const probTotal = sum(Object.values(resolutions))
|
||||||
probs = _.mapValues(
|
probs = mapValues(
|
||||||
totalShares,
|
totalShares,
|
||||||
(_, outcome) => (resolutions[outcome] ?? 0) / probTotal
|
(_, outcome) => (resolutions[outcome] ?? 0) / probTotal
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
const squareSum = _.sum(
|
const squareSum = sum(
|
||||||
Object.values(totalShares).map((shares) => shares ** 2)
|
Object.values(totalShares).map((shares) => shares ** 2)
|
||||||
)
|
)
|
||||||
probs = _.mapValues(totalShares, (shares) => shares ** 2 / squareSum)
|
probs = mapValues(totalShares, (shares) => shares ** 2 / squareSum)
|
||||||
}
|
}
|
||||||
|
|
||||||
const weightedShareTotal = _.sumBy(Object.keys(totalShares), (outcome) => {
|
const weightedShareTotal = sumBy(Object.keys(totalShares), (outcome) => {
|
||||||
return probs[outcome] * totalShares[outcome]
|
return probs[outcome] * totalShares[outcome]
|
||||||
})
|
})
|
||||||
|
|
||||||
const { outcome, amount, shares } = bet
|
const { outcome, amount, shares } = bet
|
||||||
|
|
||||||
const totalPool = _.sum(Object.values(pool))
|
const poolFrac =
|
||||||
const poolFrac = (probs[outcome] * shares) / weightedShareTotal
|
outcomeType === 'NUMERIC'
|
||||||
const winnings = poolFrac * totalPool
|
? sumBy(
|
||||||
|
Object.keys((bet as NumericBet).allOutcomeShares ?? {}),
|
||||||
|
(outcome) => {
|
||||||
|
return (
|
||||||
|
(probs[outcome] * (bet as NumericBet).allOutcomeShares[outcome]) /
|
||||||
|
weightedShareTotal
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
: (probs[outcome] * shares) / weightedShareTotal
|
||||||
|
|
||||||
|
const totalPool = sum(Object.values(pool))
|
||||||
|
const winnings = poolFrac * totalPool
|
||||||
return deductDpmFees(amount, winnings)
|
return deductDpmFees(amount, winnings)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import * as _ from 'lodash'
|
import { maxBy } from 'lodash'
|
||||||
import { Bet } from './bet'
|
import { Bet } from './bet'
|
||||||
import {
|
import {
|
||||||
calculateCpmmSale,
|
calculateCpmmSale,
|
||||||
|
@ -161,7 +161,6 @@ export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
invested: Math.max(0, currentInvested),
|
invested: Math.max(0, currentInvested),
|
||||||
currentInvested,
|
|
||||||
payout,
|
payout,
|
||||||
netPayout,
|
netPayout,
|
||||||
profit,
|
profit,
|
||||||
|
@ -181,7 +180,7 @@ export function getContractBetNullMetrics() {
|
||||||
|
|
||||||
export function getTopAnswer(contract: FreeResponseContract) {
|
export function getTopAnswer(contract: FreeResponseContract) {
|
||||||
const { answers } = contract
|
const { answers } = contract
|
||||||
const top = _.maxBy(
|
const top = maxBy(
|
||||||
answers?.map((answer) => ({
|
answers?.map((answer) => ({
|
||||||
answer,
|
answer,
|
||||||
prob: getOutcomeProbability(contract, answer.id),
|
prob: getOutcomeProbability(contract, answer.id),
|
||||||
|
@ -190,29 +189,3 @@ export function getTopAnswer(contract: FreeResponseContract) {
|
||||||
)
|
)
|
||||||
return top?.answer
|
return top?.answer
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hasUserHitManaLimit(
|
|
||||||
contract: FreeResponseContract,
|
|
||||||
bets: Bet[],
|
|
||||||
amount: number
|
|
||||||
) {
|
|
||||||
const { manaLimitPerUser } = contract
|
|
||||||
if (manaLimitPerUser) {
|
|
||||||
const contractMetrics = getContractBetMetrics(contract, bets)
|
|
||||||
const currentInvested = contractMetrics.currentInvested
|
|
||||||
console.log('user current invested amount', currentInvested)
|
|
||||||
console.log('mana limit:', manaLimitPerUser)
|
|
||||||
|
|
||||||
if (currentInvested + amount > manaLimitPerUser) {
|
|
||||||
const manaAllowed = manaLimitPerUser - currentInvested
|
|
||||||
return {
|
|
||||||
status: 'error',
|
|
||||||
message: `Market bet cap is M$${manaLimitPerUser}, you've M$${manaAllowed} left`,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
status: 'success',
|
|
||||||
message: '',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -393,6 +393,23 @@ Future plans: We expect to focus on similar theoretical problems in alignment un
|
||||||
description:
|
description:
|
||||||
'The mission of the Alliance to Feed the Earth in Disasters is to help create resilience to global food shocks. We seek to identify various resilient food solutions and to help governments implement these solutions, to increase the chances that people have enough to eat in the event of a global catastrophe. We focus on events that could deplete food supplies or access to 5% of the global population or more.Our ultimate goal is to feed everyone, no matter what. An important aspect of this goal is that we need to establish equitable solutions so that all people can access the nutrition they need, regardless of wealth or location.ALLFED is inspired by effective altruism, using reason and evidence to identify how to do the most good. Our solutions are backed by science and research, and we also identify the most cost-effective solutions, to be able to provide more nutrition in catastrophes.',
|
'The mission of the Alliance to Feed the Earth in Disasters is to help create resilience to global food shocks. We seek to identify various resilient food solutions and to help governments implement these solutions, to increase the chances that people have enough to eat in the event of a global catastrophe. We focus on events that could deplete food supplies or access to 5% of the global population or more.Our ultimate goal is to feed everyone, no matter what. An important aspect of this goal is that we need to establish equitable solutions so that all people can access the nutrition they need, regardless of wealth or location.ALLFED is inspired by effective altruism, using reason and evidence to identify how to do the most good. Our solutions are backed by science and research, and we also identify the most cost-effective solutions, to be able to provide more nutrition in catastrophes.',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'The Trevor Project',
|
||||||
|
website: 'https://www.thetrevorproject.org/',
|
||||||
|
photo: 'https://i.imgur.com/QN4mVNn.jpeg',
|
||||||
|
preview: 'The Trevor Project is the world’s largest suicide prevention and crisis intervention organization for LGBTQ (lesbian, gay, bisexual, transgender, queer, and questioning) young people.',
|
||||||
|
description:
|
||||||
|
`Two decades ago, we responded to a health crisis. Now we’re building a safer, more-inclusive world. LGBTQ young people are four times more likely to attempt suicide, and suicide remains the second leading cause of death among all young people in the U.S.
|
||||||
|
|
||||||
|
Our Mission
|
||||||
|
To end suicide among lesbian, gay, bisexual, transgender, queer & questioning young people.
|
||||||
|
|
||||||
|
Our Vision
|
||||||
|
A world where all LGBTQ young people see a bright future for themselves.
|
||||||
|
|
||||||
|
Our Goal
|
||||||
|
To serve 1.8 million crisis contacts annually, by the end of our 25th year, while continuing to innovate on our core services.`,
|
||||||
|
},
|
||||||
].map((charity) => {
|
].map((charity) => {
|
||||||
const slug = charity.name.toLowerCase().replace(/\s/g, '-')
|
const slug = charity.name.toLowerCase().replace(/\s/g, '-')
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { Fees } from './fees'
|
||||||
|
|
||||||
export type FullContract<
|
export type FullContract<
|
||||||
M extends DPM | CPMM,
|
M extends DPM | CPMM,
|
||||||
T extends Binary | Multi | FreeResponse
|
T extends Binary | Multi | FreeResponse | Numeric
|
||||||
> = {
|
> = {
|
||||||
id: string
|
id: string
|
||||||
slug: string // auto-generated; must be unique
|
slug: string // auto-generated; must be unique
|
||||||
|
@ -11,7 +11,7 @@ export type FullContract<
|
||||||
creatorId: string
|
creatorId: string
|
||||||
creatorName: string
|
creatorName: string
|
||||||
creatorUsername: string
|
creatorUsername: string
|
||||||
creatorAvatarUrl?: string // Start requiring after 2022-03-01
|
creatorAvatarUrl?: string
|
||||||
|
|
||||||
question: string
|
question: string
|
||||||
description: string // More info about what the contract is about
|
description: string // More info about what the contract is about
|
||||||
|
@ -31,8 +31,6 @@ export type FullContract<
|
||||||
|
|
||||||
closeEmailsSent?: number
|
closeEmailsSent?: number
|
||||||
|
|
||||||
manaLimitPerUser?: number
|
|
||||||
|
|
||||||
volume: number
|
volume: number
|
||||||
volume24Hours: number
|
volume24Hours: number
|
||||||
volume7Days: number
|
volume7Days: number
|
||||||
|
@ -41,9 +39,13 @@ export type FullContract<
|
||||||
} & M &
|
} & M &
|
||||||
T
|
T
|
||||||
|
|
||||||
export type Contract = FullContract<DPM | CPMM, Binary | Multi | FreeResponse>
|
export type Contract = FullContract<
|
||||||
|
DPM | CPMM,
|
||||||
|
Binary | Multi | FreeResponse | Numeric
|
||||||
|
>
|
||||||
export type BinaryContract = FullContract<DPM | CPMM, Binary>
|
export type BinaryContract = FullContract<DPM | CPMM, Binary>
|
||||||
export type FreeResponseContract = FullContract<DPM | CPMM, FreeResponse>
|
export type FreeResponseContract = FullContract<DPM | CPMM, FreeResponse>
|
||||||
|
export type NumericContract = FullContract<DPM, Numeric>
|
||||||
|
|
||||||
export type DPM = {
|
export type DPM = {
|
||||||
mechanism: 'dpm-2'
|
mechanism: 'dpm-2'
|
||||||
|
@ -83,8 +85,22 @@ export type FreeResponse = {
|
||||||
resolutions?: { [outcome: string]: number } // Used for MKT resolution.
|
resolutions?: { [outcome: string]: number } // Used for MKT resolution.
|
||||||
}
|
}
|
||||||
|
|
||||||
export type outcomeType = 'BINARY' | 'MULTI' | 'FREE_RESPONSE'
|
export type Numeric = {
|
||||||
|
outcomeType: 'NUMERIC'
|
||||||
|
bucketCount: number
|
||||||
|
min: number
|
||||||
|
max: number
|
||||||
|
resolutions?: { [outcome: string]: number } // Used for MKT resolution.
|
||||||
|
resolutionValue?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type outcomeType = 'BINARY' | 'MULTI' | 'FREE_RESPONSE' | 'NUMERIC'
|
||||||
|
export const OUTCOME_TYPES = [
|
||||||
|
'BINARY',
|
||||||
|
'MULTI',
|
||||||
|
'FREE_RESPONSE',
|
||||||
|
'NUMERIC',
|
||||||
|
] as const
|
||||||
export const MAX_QUESTION_LENGTH = 480
|
export const MAX_QUESTION_LENGTH = 480
|
||||||
export const MAX_DESCRIPTION_LENGTH = 10000
|
export const MAX_DESCRIPTION_LENGTH = 10000
|
||||||
export const MAX_TAG_LENGTH = 60
|
export const MAX_TAG_LENGTH = 60
|
||||||
|
|
|
@ -1,16 +1,17 @@
|
||||||
|
import { escapeRegExp } from 'lodash'
|
||||||
import { DEV_CONFIG } from './dev'
|
import { DEV_CONFIG } from './dev'
|
||||||
import { EnvConfig, PROD_CONFIG } from './prod'
|
import { EnvConfig, PROD_CONFIG } from './prod'
|
||||||
import { THEOREMONE_CONFIG } from './theoremone'
|
import { THEOREMONE_CONFIG } from './theoremone'
|
||||||
|
|
||||||
export const ENV = process.env.NEXT_PUBLIC_FIREBASE_ENV ?? 'DEV'
|
export const ENV = process.env.NEXT_PUBLIC_FIREBASE_ENV ?? 'DEV'
|
||||||
|
|
||||||
const CONFIGS = {
|
const CONFIGS: { [env: string]: EnvConfig } = {
|
||||||
PROD: PROD_CONFIG,
|
PROD: PROD_CONFIG,
|
||||||
DEV: DEV_CONFIG,
|
DEV: DEV_CONFIG,
|
||||||
THEOREMONE: THEOREMONE_CONFIG,
|
THEOREMONE: THEOREMONE_CONFIG,
|
||||||
}
|
}
|
||||||
// @ts-ignore
|
|
||||||
export const ENV_CONFIG: EnvConfig = CONFIGS[ENV]
|
export const ENV_CONFIG = CONFIGS[ENV]
|
||||||
|
|
||||||
export function isWhitelisted(email?: string) {
|
export function isWhitelisted(email?: string) {
|
||||||
if (!ENV_CONFIG.whitelistEmail) {
|
if (!ENV_CONFIG.whitelistEmail) {
|
||||||
|
@ -28,3 +29,10 @@ export const DOMAIN = ENV_CONFIG.domain
|
||||||
export const FIREBASE_CONFIG = ENV_CONFIG.firebaseConfig
|
export const FIREBASE_CONFIG = ENV_CONFIG.firebaseConfig
|
||||||
export const PROJECT_ID = ENV_CONFIG.firebaseConfig.projectId
|
export const PROJECT_ID = ENV_CONFIG.firebaseConfig.projectId
|
||||||
export const IS_PRIVATE_MANIFOLD = ENV_CONFIG.visibility === 'PRIVATE'
|
export const IS_PRIVATE_MANIFOLD = ENV_CONFIG.visibility === 'PRIVATE'
|
||||||
|
|
||||||
|
// Manifold's domain or any subdomains thereof
|
||||||
|
export const CORS_ORIGIN_MANIFOLD = new RegExp(
|
||||||
|
'^https?://(?:[a-zA-Z0-9\\-]+\\.)*' + escapeRegExp(ENV_CONFIG.domain) + '$'
|
||||||
|
)
|
||||||
|
// Any localhost server on any port
|
||||||
|
export const CORS_ORIGIN_LOCALHOST = /^http:\/\/localhost:\d+$/
|
||||||
|
|
|
@ -6,6 +6,7 @@ export const DEV_CONFIG: EnvConfig = {
|
||||||
apiKey: 'AIzaSyBoq3rzUa8Ekyo3ZaTnlycQYPRCA26VpOw',
|
apiKey: 'AIzaSyBoq3rzUa8Ekyo3ZaTnlycQYPRCA26VpOw',
|
||||||
authDomain: 'dev-mantic-markets.firebaseapp.com',
|
authDomain: 'dev-mantic-markets.firebaseapp.com',
|
||||||
projectId: 'dev-mantic-markets',
|
projectId: 'dev-mantic-markets',
|
||||||
|
region: 'us-central1',
|
||||||
storageBucket: 'dev-mantic-markets.appspot.com',
|
storageBucket: 'dev-mantic-markets.appspot.com',
|
||||||
messagingSenderId: '134303100058',
|
messagingSenderId: '134303100058',
|
||||||
appId: '1:134303100058:web:27f9ea8b83347251f80323',
|
appId: '1:134303100058:web:27f9ea8b83347251f80323',
|
||||||
|
|
|
@ -18,6 +18,7 @@ type FirebaseConfig = {
|
||||||
apiKey: string
|
apiKey: string
|
||||||
authDomain: string
|
authDomain: string
|
||||||
projectId: string
|
projectId: string
|
||||||
|
region: string
|
||||||
storageBucket: string
|
storageBucket: string
|
||||||
messagingSenderId: string
|
messagingSenderId: string
|
||||||
appId: string
|
appId: string
|
||||||
|
@ -30,6 +31,7 @@ export const PROD_CONFIG: EnvConfig = {
|
||||||
apiKey: 'AIzaSyDp3J57vLeAZCzxLD-vcPaGIkAmBoGOSYw',
|
apiKey: 'AIzaSyDp3J57vLeAZCzxLD-vcPaGIkAmBoGOSYw',
|
||||||
authDomain: 'mantic-markets.firebaseapp.com',
|
authDomain: 'mantic-markets.firebaseapp.com',
|
||||||
projectId: 'mantic-markets',
|
projectId: 'mantic-markets',
|
||||||
|
region: 'us-central1',
|
||||||
storageBucket: 'mantic-markets.appspot.com',
|
storageBucket: 'mantic-markets.appspot.com',
|
||||||
messagingSenderId: '128925704902',
|
messagingSenderId: '128925704902',
|
||||||
appId: '1:128925704902:web:f61f86944d8ffa2a642dc7',
|
appId: '1:128925704902:web:f61f86944d8ffa2a642dc7',
|
||||||
|
@ -39,6 +41,7 @@ export const PROD_CONFIG: EnvConfig = {
|
||||||
'akrolsmir@gmail.com', // Austin
|
'akrolsmir@gmail.com', // Austin
|
||||||
'jahooma@gmail.com', // James
|
'jahooma@gmail.com', // James
|
||||||
'taowell@gmail.com', // Stephen
|
'taowell@gmail.com', // Stephen
|
||||||
|
'abc.sinclair@gmail.com', // Sinclair
|
||||||
'manticmarkets@gmail.com', // Manifold
|
'manticmarkets@gmail.com', // Manifold
|
||||||
],
|
],
|
||||||
visibility: 'PUBLIC',
|
visibility: 'PUBLIC',
|
||||||
|
|
|
@ -6,6 +6,7 @@ export const THEOREMONE_CONFIG: EnvConfig = {
|
||||||
apiKey: 'AIzaSyBSXL6Ys7InNHnCKSy-_E_luhh4Fkj4Z6M',
|
apiKey: 'AIzaSyBSXL6Ys7InNHnCKSy-_E_luhh4Fkj4Z6M',
|
||||||
authDomain: 'theoremone-manifold.firebaseapp.com',
|
authDomain: 'theoremone-manifold.firebaseapp.com',
|
||||||
projectId: 'theoremone-manifold',
|
projectId: 'theoremone-manifold',
|
||||||
|
region: 'us-central1',
|
||||||
storageBucket: 'theoremone-manifold.appspot.com',
|
storageBucket: 'theoremone-manifold.appspot.com',
|
||||||
messagingSenderId: '698012149198',
|
messagingSenderId: '698012149198',
|
||||||
appId: '1:698012149198:web:b342af75662831aa84b79f',
|
appId: '1:698012149198:web:b342af75662831aa84b79f',
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import * as _ from 'lodash'
|
import { sumBy } from 'lodash'
|
||||||
|
|
||||||
import { Bet, MAX_LOAN_PER_CONTRACT } from './bet'
|
import { Bet, MAX_LOAN_PER_CONTRACT, NumericBet } from './bet'
|
||||||
import {
|
import {
|
||||||
calculateDpmShares,
|
calculateDpmShares,
|
||||||
getDpmProbability,
|
getDpmProbability,
|
||||||
getDpmOutcomeProbability,
|
getDpmOutcomeProbability,
|
||||||
|
getNumericBets,
|
||||||
|
calculateNumericDpmShares,
|
||||||
} from './calculate-dpm'
|
} from './calculate-dpm'
|
||||||
import { calculateCpmmPurchase, getCpmmProbability } from './calculate-cpmm'
|
import { calculateCpmmPurchase, getCpmmProbability } from './calculate-cpmm'
|
||||||
import {
|
import {
|
||||||
|
@ -14,17 +16,27 @@ import {
|
||||||
FreeResponse,
|
FreeResponse,
|
||||||
FullContract,
|
FullContract,
|
||||||
Multi,
|
Multi,
|
||||||
|
NumericContract,
|
||||||
} from './contract'
|
} from './contract'
|
||||||
import { User } from './user'
|
|
||||||
import { noFees } from './fees'
|
import { noFees } from './fees'
|
||||||
|
import { addObjects } from './util/object'
|
||||||
|
import { NUMERIC_FIXED_VAR } from './numeric-constants'
|
||||||
|
|
||||||
|
export type CandidateBet<T extends Bet> = Omit<T, 'id' | 'userId'>
|
||||||
|
export type BetInfo = {
|
||||||
|
newBet: CandidateBet<Bet>
|
||||||
|
newPool?: { [outcome: string]: number }
|
||||||
|
newTotalShares?: { [outcome: string]: number }
|
||||||
|
newTotalBets?: { [outcome: string]: number }
|
||||||
|
newTotalLiquidity?: number
|
||||||
|
newP?: number
|
||||||
|
}
|
||||||
|
|
||||||
export const getNewBinaryCpmmBetInfo = (
|
export const getNewBinaryCpmmBetInfo = (
|
||||||
user: User,
|
|
||||||
outcome: 'YES' | 'NO',
|
outcome: 'YES' | 'NO',
|
||||||
amount: number,
|
amount: number,
|
||||||
contract: FullContract<CPMM, Binary>,
|
contract: FullContract<CPMM, Binary>,
|
||||||
loanAmount: number,
|
loanAmount: number
|
||||||
newBetId: string
|
|
||||||
) => {
|
) => {
|
||||||
const { shares, newPool, newP, fees } = calculateCpmmPurchase(
|
const { shares, newPool, newP, fees } = calculateCpmmPurchase(
|
||||||
contract,
|
contract,
|
||||||
|
@ -32,15 +44,11 @@ export const getNewBinaryCpmmBetInfo = (
|
||||||
outcome
|
outcome
|
||||||
)
|
)
|
||||||
|
|
||||||
const newBalance = user.balance - (amount - loanAmount)
|
|
||||||
|
|
||||||
const { pool, p, totalLiquidity } = contract
|
const { pool, p, totalLiquidity } = contract
|
||||||
const probBefore = getCpmmProbability(pool, p)
|
const probBefore = getCpmmProbability(pool, p)
|
||||||
const probAfter = getCpmmProbability(newPool, newP)
|
const probAfter = getCpmmProbability(newPool, newP)
|
||||||
|
|
||||||
const newBet: Bet = {
|
const newBet: CandidateBet<Bet> = {
|
||||||
id: newBetId,
|
|
||||||
userId: user.id,
|
|
||||||
contractId: contract.id,
|
contractId: contract.id,
|
||||||
amount,
|
amount,
|
||||||
shares,
|
shares,
|
||||||
|
@ -55,16 +63,14 @@ export const getNewBinaryCpmmBetInfo = (
|
||||||
const { liquidityFee } = fees
|
const { liquidityFee } = fees
|
||||||
const newTotalLiquidity = (totalLiquidity ?? 0) + liquidityFee
|
const newTotalLiquidity = (totalLiquidity ?? 0) + liquidityFee
|
||||||
|
|
||||||
return { newBet, newPool, newP, newBalance, newTotalLiquidity, fees }
|
return { newBet, newPool, newP, newTotalLiquidity }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getNewBinaryDpmBetInfo = (
|
export const getNewBinaryDpmBetInfo = (
|
||||||
user: User,
|
|
||||||
outcome: 'YES' | 'NO',
|
outcome: 'YES' | 'NO',
|
||||||
amount: number,
|
amount: number,
|
||||||
contract: FullContract<DPM, Binary>,
|
contract: FullContract<DPM, Binary>,
|
||||||
loanAmount: number,
|
loanAmount: number
|
||||||
newBetId: string
|
|
||||||
) => {
|
) => {
|
||||||
const { YES: yesPool, NO: noPool } = contract.pool
|
const { YES: yesPool, NO: noPool } = contract.pool
|
||||||
|
|
||||||
|
@ -92,9 +98,7 @@ export const getNewBinaryDpmBetInfo = (
|
||||||
const probBefore = getDpmProbability(contract.totalShares)
|
const probBefore = getDpmProbability(contract.totalShares)
|
||||||
const probAfter = getDpmProbability(newTotalShares)
|
const probAfter = getDpmProbability(newTotalShares)
|
||||||
|
|
||||||
const newBet: Bet = {
|
const newBet: CandidateBet<Bet> = {
|
||||||
id: newBetId,
|
|
||||||
userId: user.id,
|
|
||||||
contractId: contract.id,
|
contractId: contract.id,
|
||||||
amount,
|
amount,
|
||||||
loanAmount,
|
loanAmount,
|
||||||
|
@ -106,18 +110,14 @@ export const getNewBinaryDpmBetInfo = (
|
||||||
fees: noFees,
|
fees: noFees,
|
||||||
}
|
}
|
||||||
|
|
||||||
const newBalance = user.balance - (amount - loanAmount)
|
return { newBet, newPool, newTotalShares, newTotalBets }
|
||||||
|
|
||||||
return { newBet, newPool, newTotalShares, newTotalBets, newBalance }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getNewMultiBetInfo = (
|
export const getNewMultiBetInfo = (
|
||||||
user: User,
|
|
||||||
outcome: string,
|
outcome: string,
|
||||||
amount: number,
|
amount: number,
|
||||||
contract: FullContract<DPM, Multi | FreeResponse>,
|
contract: FullContract<DPM, Multi | FreeResponse>,
|
||||||
loanAmount: number,
|
loanAmount: number
|
||||||
newBetId: string
|
|
||||||
) => {
|
) => {
|
||||||
const { pool, totalShares, totalBets } = contract
|
const { pool, totalShares, totalBets } = contract
|
||||||
|
|
||||||
|
@ -135,9 +135,7 @@ export const getNewMultiBetInfo = (
|
||||||
const probBefore = getDpmOutcomeProbability(totalShares, outcome)
|
const probBefore = getDpmOutcomeProbability(totalShares, outcome)
|
||||||
const probAfter = getDpmOutcomeProbability(newTotalShares, outcome)
|
const probAfter = getDpmOutcomeProbability(newTotalShares, outcome)
|
||||||
|
|
||||||
const newBet: Bet = {
|
const newBet: CandidateBet<Bet> = {
|
||||||
id: newBetId,
|
|
||||||
userId: user.id,
|
|
||||||
contractId: contract.id,
|
contractId: contract.id,
|
||||||
amount,
|
amount,
|
||||||
loanAmount,
|
loanAmount,
|
||||||
|
@ -149,14 +147,55 @@ export const getNewMultiBetInfo = (
|
||||||
fees: noFees,
|
fees: noFees,
|
||||||
}
|
}
|
||||||
|
|
||||||
const newBalance = user.balance - (amount - loanAmount)
|
return { newBet, newPool, newTotalShares, newTotalBets }
|
||||||
|
}
|
||||||
|
|
||||||
return { newBet, newPool, newTotalShares, newTotalBets, newBalance }
|
export const getNumericBetsInfo = (
|
||||||
|
value: number,
|
||||||
|
outcome: string,
|
||||||
|
amount: number,
|
||||||
|
contract: NumericContract
|
||||||
|
) => {
|
||||||
|
const { pool, totalShares, totalBets } = contract
|
||||||
|
|
||||||
|
const bets = getNumericBets(contract, outcome, amount, NUMERIC_FIXED_VAR)
|
||||||
|
|
||||||
|
const allBetAmounts = Object.fromEntries(bets)
|
||||||
|
const newTotalBets = addObjects(totalBets, allBetAmounts)
|
||||||
|
const newPool = addObjects(pool, allBetAmounts)
|
||||||
|
|
||||||
|
const { shares, totalShares: newTotalShares } = calculateNumericDpmShares(
|
||||||
|
contract.totalShares,
|
||||||
|
bets
|
||||||
|
)
|
||||||
|
|
||||||
|
const allOutcomeShares = Object.fromEntries(
|
||||||
|
bets.map(([outcome], i) => [outcome, shares[i]])
|
||||||
|
)
|
||||||
|
|
||||||
|
const probBefore = getDpmOutcomeProbability(totalShares, outcome)
|
||||||
|
const probAfter = getDpmOutcomeProbability(newTotalShares, outcome)
|
||||||
|
|
||||||
|
const newBet: CandidateBet<NumericBet> = {
|
||||||
|
contractId: contract.id,
|
||||||
|
value,
|
||||||
|
amount,
|
||||||
|
allBetAmounts,
|
||||||
|
shares: shares.find((s, i) => bets[i][0] === outcome) ?? 0,
|
||||||
|
allOutcomeShares,
|
||||||
|
outcome,
|
||||||
|
probBefore,
|
||||||
|
probAfter,
|
||||||
|
createdTime: Date.now(),
|
||||||
|
fees: noFees,
|
||||||
|
}
|
||||||
|
|
||||||
|
return { newBet, newPool, newTotalShares, newTotalBets }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getLoanAmount = (yourBets: Bet[], newBetAmount: number) => {
|
export const getLoanAmount = (yourBets: Bet[], newBetAmount: number) => {
|
||||||
const openBets = yourBets.filter((bet) => !bet.isSold && !bet.sale)
|
const openBets = yourBets.filter((bet) => !bet.isSold && !bet.sale)
|
||||||
const prevLoanAmount = _.sumBy(openBets, (bet) => bet.loanAmount ?? 0)
|
const prevLoanAmount = sumBy(openBets, (bet) => bet.loanAmount ?? 0)
|
||||||
const loanAmount = Math.min(
|
const loanAmount = Math.min(
|
||||||
newBetAmount,
|
newBetAmount,
|
||||||
MAX_LOAN_PER_CONTRACT - prevLoanAmount
|
MAX_LOAN_PER_CONTRACT - prevLoanAmount
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
import { PHANTOM_ANTE } from './antes'
|
import { range } from 'lodash'
|
||||||
import {
|
import {
|
||||||
Binary,
|
Binary,
|
||||||
Contract,
|
Contract,
|
||||||
CPMM,
|
CPMM,
|
||||||
DPM,
|
DPM,
|
||||||
FreeResponse,
|
FreeResponse,
|
||||||
|
Numeric,
|
||||||
outcomeType,
|
outcomeType,
|
||||||
} from './contract'
|
} from './contract'
|
||||||
import { User } from './user'
|
import { User } from './user'
|
||||||
import { parseTags } from './util/parse'
|
import { parseTags } from './util/parse'
|
||||||
import { removeUndefinedProps } from './util/object'
|
import { removeUndefinedProps } from './util/object'
|
||||||
import { calcDpmInitialPool } from './calculate-dpm'
|
|
||||||
|
|
||||||
export function getNewContract(
|
export function getNewContract(
|
||||||
id: string,
|
id: string,
|
||||||
|
@ -23,7 +23,11 @@ export function getNewContract(
|
||||||
ante: number,
|
ante: number,
|
||||||
closeTime: number,
|
closeTime: number,
|
||||||
extraTags: string[],
|
extraTags: string[],
|
||||||
manaLimitPerUser: number
|
|
||||||
|
// used for numeric markets
|
||||||
|
bucketCount: number,
|
||||||
|
min: number,
|
||||||
|
max: number
|
||||||
) {
|
) {
|
||||||
const tags = parseTags(
|
const tags = parseTags(
|
||||||
`${question} ${description} ${extraTags.map((tag) => `#${tag}`).join(' ')}`
|
`${question} ${description} ${extraTags.map((tag) => `#${tag}`).join(' ')}`
|
||||||
|
@ -33,6 +37,8 @@ export function getNewContract(
|
||||||
const propsByOutcomeType =
|
const propsByOutcomeType =
|
||||||
outcomeType === 'BINARY'
|
outcomeType === 'BINARY'
|
||||||
? getBinaryCpmmProps(initialProb, ante) // getBinaryDpmProps(initialProb, ante)
|
? getBinaryCpmmProps(initialProb, ante) // getBinaryDpmProps(initialProb, ante)
|
||||||
|
: outcomeType === 'NUMERIC'
|
||||||
|
? getNumericProps(ante, bucketCount, min, max)
|
||||||
: getFreeAnswerProps(ante)
|
: getFreeAnswerProps(ante)
|
||||||
|
|
||||||
const contract: Contract = removeUndefinedProps({
|
const contract: Contract = removeUndefinedProps({
|
||||||
|
@ -63,12 +69,14 @@ export function getNewContract(
|
||||||
liquidityFee: 0,
|
liquidityFee: 0,
|
||||||
platformFee: 0,
|
platformFee: 0,
|
||||||
},
|
},
|
||||||
manaLimitPerUser,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return contract as Contract
|
return contract as Contract
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
import { PHANTOM_ANTE } from './antes'
|
||||||
|
import { calcDpmInitialPool } from './calculate-dpm'
|
||||||
const getBinaryDpmProps = (initialProb: number, ante: number) => {
|
const getBinaryDpmProps = (initialProb: number, ante: number) => {
|
||||||
const { sharesYes, sharesNo, poolYes, poolNo, phantomYes, phantomNo } =
|
const { sharesYes, sharesNo, poolYes, poolNo, phantomYes, phantomNo } =
|
||||||
calcDpmInitialPool(initialProb, ante, PHANTOM_ANTE)
|
calcDpmInitialPool(initialProb, ante, PHANTOM_ANTE)
|
||||||
|
@ -85,6 +93,7 @@ const getBinaryDpmProps = (initialProb: number, ante: number) => {
|
||||||
|
|
||||||
return system
|
return system
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
const getBinaryCpmmProps = (initialProb: number, ante: number) => {
|
const getBinaryCpmmProps = (initialProb: number, ante: number) => {
|
||||||
const pool = { YES: ante, NO: ante }
|
const pool = { YES: ante, NO: ante }
|
||||||
|
@ -115,10 +124,33 @@ const getFreeAnswerProps = (ante: number) => {
|
||||||
return system
|
return system
|
||||||
}
|
}
|
||||||
|
|
||||||
const getMultiProps = (
|
const getNumericProps = (
|
||||||
outcomes: string[],
|
ante: number,
|
||||||
initialProbs: number[],
|
bucketCount: number,
|
||||||
ante: number
|
min: number,
|
||||||
|
max: number
|
||||||
) => {
|
) => {
|
||||||
// Not implemented.
|
const buckets = range(0, bucketCount).map((i) => i.toString())
|
||||||
|
|
||||||
|
const betAnte = ante / bucketCount
|
||||||
|
const pool = Object.fromEntries(buckets.map((answer) => [answer, betAnte]))
|
||||||
|
const totalBets = pool
|
||||||
|
|
||||||
|
const betShares = Math.sqrt(ante ** 2 / bucketCount)
|
||||||
|
const totalShares = Object.fromEntries(
|
||||||
|
buckets.map((answer) => [answer, betShares])
|
||||||
|
)
|
||||||
|
|
||||||
|
const system: DPM & Numeric = {
|
||||||
|
mechanism: 'dpm-2',
|
||||||
|
outcomeType: 'NUMERIC',
|
||||||
|
pool,
|
||||||
|
totalBets,
|
||||||
|
totalShares,
|
||||||
|
bucketCount,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
}
|
||||||
|
|
||||||
|
return system
|
||||||
}
|
}
|
||||||
|
|
5
common/numeric-constants.ts
Normal file
5
common/numeric-constants.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export const NUMERIC_BUCKET_COUNT = 200
|
||||||
|
export const NUMERIC_FIXED_VAR = 0.005
|
||||||
|
|
||||||
|
export const NUMERIC_GRAPH_COLOR = '#5fa5f9'
|
||||||
|
export const NUMERIC_TEXT_COLOR = 'text-blue-500'
|
|
@ -3,6 +3,7 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {},
|
"scripts": {},
|
||||||
|
"sideEffects": false,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lodash": "4.17.21"
|
"lodash": "4.17.21"
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import * as _ from 'lodash'
|
import { sum, groupBy, sumBy, mapValues } from 'lodash'
|
||||||
|
|
||||||
import { Bet } from './bet'
|
import { Bet, NumericBet } from './bet'
|
||||||
import { deductDpmFees, getDpmProbability } from './calculate-dpm'
|
import { deductDpmFees, getDpmProbability } from './calculate-dpm'
|
||||||
import { DPM, FreeResponse, FullContract, Multi } from './contract'
|
import { DPM, FreeResponse, FullContract, Multi } from './contract'
|
||||||
import {
|
import {
|
||||||
|
@ -17,10 +17,10 @@ export const getDpmCancelPayouts = (
|
||||||
bets: Bet[]
|
bets: Bet[]
|
||||||
) => {
|
) => {
|
||||||
const { pool } = contract
|
const { pool } = contract
|
||||||
const poolTotal = _.sum(Object.values(pool))
|
const poolTotal = sum(Object.values(pool))
|
||||||
console.log('resolved N/A, pool M$', poolTotal)
|
console.log('resolved N/A, pool M$', poolTotal)
|
||||||
|
|
||||||
const betSum = _.sumBy(bets, (b) => b.amount)
|
const betSum = sumBy(bets, (b) => b.amount)
|
||||||
|
|
||||||
const payouts = bets.map((bet) => ({
|
const payouts = bets.map((bet) => ({
|
||||||
userId: bet.userId,
|
userId: bet.userId,
|
||||||
|
@ -42,8 +42,8 @@ export const getDpmStandardPayouts = (
|
||||||
) => {
|
) => {
|
||||||
const winningBets = bets.filter((bet) => bet.outcome === outcome)
|
const winningBets = bets.filter((bet) => bet.outcome === outcome)
|
||||||
|
|
||||||
const poolTotal = _.sum(Object.values(contract.pool))
|
const poolTotal = sum(Object.values(contract.pool))
|
||||||
const totalShares = _.sumBy(winningBets, (b) => b.shares)
|
const totalShares = sumBy(winningBets, (b) => b.shares)
|
||||||
|
|
||||||
const payouts = winningBets.map(({ userId, amount, shares }) => {
|
const payouts = winningBets.map(({ userId, amount, shares }) => {
|
||||||
const winnings = (shares / totalShares) * poolTotal
|
const winnings = (shares / totalShares) * poolTotal
|
||||||
|
@ -54,7 +54,7 @@ export const getDpmStandardPayouts = (
|
||||||
return { userId, profit, payout }
|
return { userId, profit, payout }
|
||||||
})
|
})
|
||||||
|
|
||||||
const profits = _.sumBy(payouts, (po) => Math.max(0, po.profit))
|
const profits = sumBy(payouts, (po) => Math.max(0, po.profit))
|
||||||
const creatorFee = DPM_CREATOR_FEE * profits
|
const creatorFee = DPM_CREATOR_FEE * profits
|
||||||
const platformFee = DPM_PLATFORM_FEE * profits
|
const platformFee = DPM_PLATFORM_FEE * profits
|
||||||
|
|
||||||
|
@ -88,6 +88,64 @@ export const getDpmStandardPayouts = (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getNumericDpmPayouts = (
|
||||||
|
outcome: string,
|
||||||
|
contract: FullContract<DPM, any>,
|
||||||
|
bets: NumericBet[]
|
||||||
|
) => {
|
||||||
|
const totalShares = sumBy(bets, (bet) => bet.allOutcomeShares[outcome] ?? 0)
|
||||||
|
const winningBets = bets.filter((bet) => !!bet.allOutcomeShares[outcome])
|
||||||
|
|
||||||
|
const poolTotal = sum(Object.values(contract.pool))
|
||||||
|
|
||||||
|
const payouts = winningBets.map(
|
||||||
|
({ userId, allBetAmounts, allOutcomeShares }) => {
|
||||||
|
const shares = allOutcomeShares[outcome] ?? 0
|
||||||
|
const winnings = (shares / totalShares) * poolTotal
|
||||||
|
|
||||||
|
const amount = allBetAmounts[outcome] ?? 0
|
||||||
|
const profit = winnings - amount
|
||||||
|
|
||||||
|
// profit can be negative if using phantom shares
|
||||||
|
const payout = amount + (1 - DPM_FEES) * Math.max(0, profit)
|
||||||
|
return { userId, profit, payout }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const profits = sumBy(payouts, (po) => Math.max(0, po.profit))
|
||||||
|
const creatorFee = DPM_CREATOR_FEE * profits
|
||||||
|
const platformFee = DPM_PLATFORM_FEE * profits
|
||||||
|
|
||||||
|
const finalFees: Fees = {
|
||||||
|
creatorFee,
|
||||||
|
platformFee,
|
||||||
|
liquidityFee: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
const collectedFees = addObjects<Fees>(
|
||||||
|
finalFees,
|
||||||
|
contract.collectedFees ?? {}
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
'resolved numeric bucket: ',
|
||||||
|
outcome,
|
||||||
|
'pool',
|
||||||
|
poolTotal,
|
||||||
|
'profits',
|
||||||
|
profits,
|
||||||
|
'creator fee',
|
||||||
|
creatorFee
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
payouts: payouts.map(({ userId, payout }) => ({ userId, payout })),
|
||||||
|
creatorPayout: creatorFee,
|
||||||
|
liquidityPayouts: [],
|
||||||
|
collectedFees,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const getDpmMktPayouts = (
|
export const getDpmMktPayouts = (
|
||||||
contract: FullContract<DPM, any>,
|
contract: FullContract<DPM, any>,
|
||||||
bets: Bet[],
|
bets: Bet[],
|
||||||
|
@ -98,7 +156,7 @@ export const getDpmMktPayouts = (
|
||||||
? getDpmProbability(contract.totalShares)
|
? getDpmProbability(contract.totalShares)
|
||||||
: resolutionProbability
|
: resolutionProbability
|
||||||
|
|
||||||
const weightedShareTotal = _.sumBy(bets, (b) =>
|
const weightedShareTotal = sumBy(bets, (b) =>
|
||||||
b.outcome === 'YES' ? p * b.shares : (1 - p) * b.shares
|
b.outcome === 'YES' ? p * b.shares : (1 - p) * b.shares
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -112,7 +170,7 @@ export const getDpmMktPayouts = (
|
||||||
return { userId, profit, payout }
|
return { userId, profit, payout }
|
||||||
})
|
})
|
||||||
|
|
||||||
const profits = _.sumBy(payouts, (po) => Math.max(0, po.profit))
|
const profits = sumBy(payouts, (po) => Math.max(0, po.profit))
|
||||||
|
|
||||||
const creatorFee = DPM_CREATOR_FEE * profits
|
const creatorFee = DPM_CREATOR_FEE * profits
|
||||||
const platformFee = DPM_PLATFORM_FEE * profits
|
const platformFee = DPM_PLATFORM_FEE * profits
|
||||||
|
@ -152,15 +210,15 @@ export const getPayoutsMultiOutcome = (
|
||||||
contract: FullContract<DPM, Multi | FreeResponse>,
|
contract: FullContract<DPM, Multi | FreeResponse>,
|
||||||
bets: Bet[]
|
bets: Bet[]
|
||||||
) => {
|
) => {
|
||||||
const poolTotal = _.sum(Object.values(contract.pool))
|
const poolTotal = sum(Object.values(contract.pool))
|
||||||
const winningBets = bets.filter((bet) => resolutions[bet.outcome])
|
const winningBets = bets.filter((bet) => resolutions[bet.outcome])
|
||||||
|
|
||||||
const betsByOutcome = _.groupBy(winningBets, (bet) => bet.outcome)
|
const betsByOutcome = groupBy(winningBets, (bet) => bet.outcome)
|
||||||
const sharesByOutcome = _.mapValues(betsByOutcome, (bets) =>
|
const sharesByOutcome = mapValues(betsByOutcome, (bets) =>
|
||||||
_.sumBy(bets, (bet) => bet.shares)
|
sumBy(bets, (bet) => bet.shares)
|
||||||
)
|
)
|
||||||
|
|
||||||
const probTotal = _.sum(Object.values(resolutions))
|
const probTotal = sum(Object.values(resolutions))
|
||||||
|
|
||||||
const payouts = winningBets.map(({ userId, outcome, amount, shares }) => {
|
const payouts = winningBets.map(({ userId, outcome, amount, shares }) => {
|
||||||
const prob = resolutions[outcome] / probTotal
|
const prob = resolutions[outcome] / probTotal
|
||||||
|
@ -171,7 +229,7 @@ export const getPayoutsMultiOutcome = (
|
||||||
return { userId, profit, payout }
|
return { userId, profit, payout }
|
||||||
})
|
})
|
||||||
|
|
||||||
const profits = _.sumBy(payouts, (po) => po.profit)
|
const profits = sumBy(payouts, (po) => po.profit)
|
||||||
|
|
||||||
const creatorFee = DPM_CREATOR_FEE * profits
|
const creatorFee = DPM_CREATOR_FEE * profits
|
||||||
const platformFee = DPM_PLATFORM_FEE * profits
|
const platformFee = DPM_PLATFORM_FEE * profits
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import * as _ from 'lodash'
|
import { sum } from 'lodash'
|
||||||
|
|
||||||
import { Bet } from './bet'
|
import { Bet } from './bet'
|
||||||
import { getProbability } from './calculate'
|
import { getProbability } from './calculate'
|
||||||
|
@ -50,7 +50,7 @@ export const getStandardFixedPayouts = (
|
||||||
'pool',
|
'pool',
|
||||||
contract.pool[outcome],
|
contract.pool[outcome],
|
||||||
'payouts',
|
'payouts',
|
||||||
_.sum(payouts),
|
sum(payouts),
|
||||||
'creator fee',
|
'creator fee',
|
||||||
creatorPayout
|
creatorPayout
|
||||||
)
|
)
|
||||||
|
@ -105,7 +105,7 @@ export const getMktFixedPayouts = (
|
||||||
'pool',
|
'pool',
|
||||||
p * contract.pool.YES + (1 - p) * contract.pool.NO,
|
p * contract.pool.YES + (1 - p) * contract.pool.NO,
|
||||||
'payouts',
|
'payouts',
|
||||||
_.sum(payouts),
|
sum(payouts),
|
||||||
'creator fee',
|
'creator fee',
|
||||||
creatorPayout
|
creatorPayout
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import * as _ from 'lodash'
|
import { sumBy, groupBy, mapValues } from 'lodash'
|
||||||
|
|
||||||
import { Bet } from './bet'
|
import { Bet, NumericBet } from './bet'
|
||||||
import {
|
import {
|
||||||
Binary,
|
Binary,
|
||||||
Contract,
|
Contract,
|
||||||
|
@ -16,6 +16,7 @@ import {
|
||||||
getDpmCancelPayouts,
|
getDpmCancelPayouts,
|
||||||
getDpmMktPayouts,
|
getDpmMktPayouts,
|
||||||
getDpmStandardPayouts,
|
getDpmStandardPayouts,
|
||||||
|
getNumericDpmPayouts,
|
||||||
getPayoutsMultiOutcome,
|
getPayoutsMultiOutcome,
|
||||||
} from './payouts-dpm'
|
} from './payouts-dpm'
|
||||||
import {
|
import {
|
||||||
|
@ -31,16 +32,19 @@ export type Payout = {
|
||||||
|
|
||||||
export const getLoanPayouts = (bets: Bet[]): Payout[] => {
|
export const getLoanPayouts = (bets: Bet[]): Payout[] => {
|
||||||
const betsWithLoans = bets.filter((bet) => bet.loanAmount)
|
const betsWithLoans = bets.filter((bet) => bet.loanAmount)
|
||||||
const betsByUser = _.groupBy(betsWithLoans, (bet) => bet.userId)
|
const betsByUser = groupBy(betsWithLoans, (bet) => bet.userId)
|
||||||
const loansByUser = _.mapValues(betsByUser, (bets) =>
|
const loansByUser = mapValues(betsByUser, (bets) =>
|
||||||
_.sumBy(bets, (bet) => -(bet.loanAmount ?? 0))
|
sumBy(bets, (bet) => -(bet.loanAmount ?? 0))
|
||||||
)
|
)
|
||||||
return _.toPairs(loansByUser).map(([userId, payout]) => ({ userId, payout }))
|
return Object.entries(loansByUser).map(([userId, payout]) => ({
|
||||||
|
userId,
|
||||||
|
payout,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
export const groupPayoutsByUser = (payouts: Payout[]) => {
|
export const groupPayoutsByUser = (payouts: Payout[]) => {
|
||||||
const groups = _.groupBy(payouts, (payout) => payout.userId)
|
const groups = groupBy(payouts, (payout) => payout.userId)
|
||||||
return _.mapValues(groups, (group) => _.sumBy(group, (g) => g.payout))
|
return mapValues(groups, (group) => sumBy(group, (g) => g.payout))
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PayoutInfo = {
|
export type PayoutInfo = {
|
||||||
|
@ -131,6 +135,9 @@ export const getDpmPayouts = (
|
||||||
return getDpmCancelPayouts(contract, openBets)
|
return getDpmCancelPayouts(contract, openBets)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
if (contract.outcomeType === 'NUMERIC')
|
||||||
|
return getNumericDpmPayouts(outcome, contract, openBets as NumericBet[])
|
||||||
|
|
||||||
// Outcome is a free response answer id.
|
// Outcome is a free response answer id.
|
||||||
return getDpmStandardPayouts(outcome, contract, openBets)
|
return getDpmStandardPayouts(outcome, contract, openBets)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import * as _ from 'lodash'
|
import { union, sum, sumBy, sortBy, groupBy, mapValues } from 'lodash'
|
||||||
import { Bet } from './bet'
|
import { Bet } from './bet'
|
||||||
import { Contract } from './contract'
|
import { Contract } from './contract'
|
||||||
import { ClickEvent } from './tracking'
|
import { ClickEvent } from './tracking'
|
||||||
|
@ -21,13 +21,13 @@ export const getRecommendedContracts = (
|
||||||
|
|
||||||
const yourWordFrequency = contractsToWordFrequency(yourContracts)
|
const yourWordFrequency = contractsToWordFrequency(yourContracts)
|
||||||
const otherWordFrequency = contractsToWordFrequency(notYourContracts)
|
const otherWordFrequency = contractsToWordFrequency(notYourContracts)
|
||||||
const words = _.union(
|
const words = union(
|
||||||
Object.keys(yourWordFrequency),
|
Object.keys(yourWordFrequency),
|
||||||
Object.keys(otherWordFrequency)
|
Object.keys(otherWordFrequency)
|
||||||
)
|
)
|
||||||
|
|
||||||
const yourWeightedFrequency = _.fromPairs(
|
const yourWeightedFrequency = Object.fromEntries(
|
||||||
_.map(words, (word) => {
|
words.map((word) => {
|
||||||
const [yourFreq, otherFreq] = [
|
const [yourFreq, otherFreq] = [
|
||||||
yourWordFrequency[word] ?? 0,
|
yourWordFrequency[word] ?? 0,
|
||||||
otherWordFrequency[word] ?? 0,
|
otherWordFrequency[word] ?? 0,
|
||||||
|
@ -47,7 +47,7 @@ export const getRecommendedContracts = (
|
||||||
const scoredContracts = contracts.map((contract) => {
|
const scoredContracts = contracts.map((contract) => {
|
||||||
const wordFrequency = contractToWordFrequency(contract)
|
const wordFrequency = contractToWordFrequency(contract)
|
||||||
|
|
||||||
const score = _.sumBy(Object.keys(wordFrequency), (word) => {
|
const score = sumBy(Object.keys(wordFrequency), (word) => {
|
||||||
const wordFreq = wordFrequency[word] ?? 0
|
const wordFreq = wordFrequency[word] ?? 0
|
||||||
const weight = yourWeightedFrequency[word] ?? 0
|
const weight = yourWeightedFrequency[word] ?? 0
|
||||||
return wordFreq * weight
|
return wordFreq * weight
|
||||||
|
@ -59,7 +59,7 @@ export const getRecommendedContracts = (
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return _.sortBy(scoredContracts, (scored) => -scored.score).map(
|
return sortBy(scoredContracts, (scored) => -scored.score).map(
|
||||||
(scored) => scored.contract
|
(scored) => scored.contract
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -87,8 +87,8 @@ const getWordsCount = (text: string) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const toFrequency = (counts: { [word: string]: number }) => {
|
const toFrequency = (counts: { [word: string]: number }) => {
|
||||||
const total = _.sum(Object.values(counts))
|
const total = sum(Object.values(counts))
|
||||||
return _.mapValues(counts, (count) => count / total)
|
return mapValues(counts, (count) => count / total)
|
||||||
}
|
}
|
||||||
|
|
||||||
const contractToWordFrequency = (contract: Contract) =>
|
const contractToWordFrequency = (contract: Contract) =>
|
||||||
|
@ -108,8 +108,8 @@ export const getWordScores = (
|
||||||
clicks: ClickEvent[],
|
clicks: ClickEvent[],
|
||||||
bets: Bet[]
|
bets: Bet[]
|
||||||
) => {
|
) => {
|
||||||
const contractClicks = _.groupBy(clicks, (click) => click.contractId)
|
const contractClicks = groupBy(clicks, (click) => click.contractId)
|
||||||
const contractBets = _.groupBy(bets, (bet) => bet.contractId)
|
const contractBets = groupBy(bets, (bet) => bet.contractId)
|
||||||
|
|
||||||
const yourContracts = contracts.filter(
|
const yourContracts = contracts.filter(
|
||||||
(c) =>
|
(c) =>
|
||||||
|
@ -117,25 +117,22 @@ export const getWordScores = (
|
||||||
)
|
)
|
||||||
const yourTfIdf = calculateContractTfIdf(yourContracts)
|
const yourTfIdf = calculateContractTfIdf(yourContracts)
|
||||||
|
|
||||||
const contractWordScores = _.mapValues(
|
const contractWordScores = mapValues(yourTfIdf, (wordsTfIdf, contractId) => {
|
||||||
yourTfIdf,
|
const viewCount = contractViewCounts[contractId] ?? 0
|
||||||
(wordsTfIdf, contractId) => {
|
const clickCount = contractClicks[contractId]?.length ?? 0
|
||||||
const viewCount = contractViewCounts[contractId] ?? 0
|
const betCount = contractBets[contractId]?.length ?? 0
|
||||||
const clickCount = contractClicks[contractId]?.length ?? 0
|
|
||||||
const betCount = contractBets[contractId]?.length ?? 0
|
|
||||||
|
|
||||||
const factor =
|
const factor =
|
||||||
-1 * Math.log(viewCount + 1) +
|
-1 * Math.log(viewCount + 1) +
|
||||||
10 * Math.log(betCount + clickCount / 4 + 1)
|
10 * Math.log(betCount + clickCount / 4 + 1)
|
||||||
|
|
||||||
return _.mapValues(wordsTfIdf, (tfIdf) => tfIdf * factor)
|
return mapValues(wordsTfIdf, (tfIdf) => tfIdf * factor)
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
const wordScores = Object.values(contractWordScores).reduce(addObjects, {})
|
const wordScores = Object.values(contractWordScores).reduce(addObjects, {})
|
||||||
const minScore = Math.min(...Object.values(wordScores))
|
const minScore = Math.min(...Object.values(wordScores))
|
||||||
const maxScore = Math.max(...Object.values(wordScores))
|
const maxScore = Math.max(...Object.values(wordScores))
|
||||||
const normalizedWordScores = _.mapValues(
|
const normalizedWordScores = mapValues(
|
||||||
wordScores,
|
wordScores,
|
||||||
(score) => (score - minScore) / (maxScore - minScore)
|
(score) => (score - minScore) / (maxScore - minScore)
|
||||||
)
|
)
|
||||||
|
@ -156,7 +153,7 @@ export function getContractScore(
|
||||||
if (Object.keys(wordScores).length === 0) return 1
|
if (Object.keys(wordScores).length === 0) return 1
|
||||||
|
|
||||||
const wordFrequency = contractToWordFrequency(contract)
|
const wordFrequency = contractToWordFrequency(contract)
|
||||||
const score = _.sumBy(Object.keys(wordFrequency), (word) => {
|
const score = sumBy(Object.keys(wordFrequency), (word) => {
|
||||||
const wordFreq = wordFrequency[word] ?? 0
|
const wordFreq = wordFrequency[word] ?? 0
|
||||||
const weight = wordScores[word] ?? 0
|
const weight = wordScores[word] ?? 0
|
||||||
return wordFreq * weight
|
return wordFreq * weight
|
||||||
|
@ -178,11 +175,13 @@ function calculateContractTfIdf(contracts: Contract[]) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const wordIdf = _.mapValues(wordsCount, (count) =>
|
const wordIdf = mapValues(wordsCount, (count) =>
|
||||||
Math.log(contracts.length / count)
|
Math.log(contracts.length / count)
|
||||||
)
|
)
|
||||||
const contractWordsTfIdf = _.map(contractFreq, (wordFreq) =>
|
const contractWordsTfIdf = contractFreq.map((wordFreq) =>
|
||||||
_.mapValues(wordFreq, (freq, word) => freq * wordIdf[word])
|
mapValues(wordFreq, (freq, word) => freq * wordIdf[word])
|
||||||
|
)
|
||||||
|
return Object.fromEntries(
|
||||||
|
contracts.map((c, i) => [c.id, contractWordsTfIdf[i]])
|
||||||
)
|
)
|
||||||
return _.fromPairs(contracts.map((c, i) => [c.id, contractWordsTfIdf[i]]))
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import * as _ from 'lodash'
|
import { groupBy, sumBy, mapValues, partition } from 'lodash'
|
||||||
|
|
||||||
import { Bet } from './bet'
|
import { Bet } from './bet'
|
||||||
import { Binary, Contract, FullContract } from './contract'
|
import { Binary, Contract, FullContract } from './contract'
|
||||||
import { getPayouts } from './payouts'
|
import { getPayouts } from './payouts'
|
||||||
|
|
||||||
export function scoreCreators(contracts: Contract[], bets: Bet[][]) {
|
export function scoreCreators(contracts: Contract[]) {
|
||||||
const creatorScore = _.mapValues(
|
const creatorScore = mapValues(
|
||||||
_.groupBy(contracts, ({ creatorId }) => creatorId),
|
groupBy(contracts, ({ creatorId }) => creatorId),
|
||||||
(contracts) => _.sumBy(contracts, ({ pool }) => pool.YES + pool.NO)
|
(contracts) => sumBy(contracts, ({ pool }) => pool.YES + pool.NO)
|
||||||
)
|
)
|
||||||
|
|
||||||
return creatorScore
|
return creatorScore
|
||||||
|
@ -30,7 +30,7 @@ export function scoreUsersByContract(
|
||||||
) {
|
) {
|
||||||
const { resolution, resolutionProbability } = contract
|
const { resolution, resolutionProbability } = contract
|
||||||
|
|
||||||
const [closedBets, openBets] = _.partition(
|
const [closedBets, openBets] = partition(
|
||||||
bets,
|
bets,
|
||||||
(bet) => bet.isSold || bet.sale
|
(bet) => bet.isSold || bet.sale
|
||||||
)
|
)
|
||||||
|
@ -58,9 +58,9 @@ export function scoreUsersByContract(
|
||||||
|
|
||||||
const netPayouts = [...resolvePayouts, ...salePayouts, ...investments]
|
const netPayouts = [...resolvePayouts, ...salePayouts, ...investments]
|
||||||
|
|
||||||
const userScore = _.mapValues(
|
const userScore = mapValues(
|
||||||
_.groupBy(netPayouts, (payout) => payout.userId),
|
groupBy(netPayouts, (payout) => payout.userId),
|
||||||
(payouts) => _.sumBy(payouts, ({ payout }) => payout)
|
(payouts) => sumBy(payouts, ({ payout }) => payout)
|
||||||
)
|
)
|
||||||
|
|
||||||
return userScore
|
return userScore
|
||||||
|
|
12
common/tsconfig.json
Normal file
12
common/tsconfig.json
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": "../",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"outDir": "lib",
|
||||||
|
"sourceMap": true,
|
||||||
|
"strict": true,
|
||||||
|
"target": "es2017"
|
||||||
|
},
|
||||||
|
"include": ["**/*.ts"]
|
||||||
|
}
|
|
@ -29,6 +29,20 @@ export function formatPercent(zeroToOne: number) {
|
||||||
return (zeroToOne * 100).toFixed(decimalPlaces) + '%'
|
return (zeroToOne * 100).toFixed(decimalPlaces) + '%'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Eg 1234567.89 => 1.23M; 5678 => 5.68K
|
||||||
|
export function formatLargeNumber(num: number, sigfigs = 2): string {
|
||||||
|
const absNum = Math.abs(num)
|
||||||
|
if (absNum < 1000) {
|
||||||
|
return '' + Number(num.toPrecision(sigfigs))
|
||||||
|
}
|
||||||
|
|
||||||
|
const suffix = ['', 'K', 'M', 'B', 'T', 'Q']
|
||||||
|
const suffixIdx = Math.floor(Math.log10(absNum) / 3)
|
||||||
|
const suffixStr = suffix[suffixIdx]
|
||||||
|
const numStr = (num / Math.pow(10, 3 * suffixIdx)).toPrecision(sigfigs)
|
||||||
|
return `${Number(numStr)}${suffixStr}`
|
||||||
|
}
|
||||||
|
|
||||||
export function toCamelCase(words: string) {
|
export function toCamelCase(words: string) {
|
||||||
const camelCase = words
|
const camelCase = words
|
||||||
.split(' ')
|
.split(' ')
|
||||||
|
|
|
@ -4,3 +4,16 @@ export const logInterpolation = (min: number, max: number, value: number) => {
|
||||||
|
|
||||||
return Math.log(value - min + 1) / Math.log(max - min + 1)
|
return Math.log(value - min + 1) / Math.log(max - min + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function normpdf(x: number, mean = 0, variance = 1) {
|
||||||
|
if (variance === 0) {
|
||||||
|
return x === mean ? Infinity : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
Math.exp((-0.5 * Math.pow(x - mean, 2)) / variance) /
|
||||||
|
Math.sqrt(TAU * variance)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const TAU = Math.PI * 2
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import * as _ from 'lodash'
|
import { union } from 'lodash'
|
||||||
|
|
||||||
export const removeUndefinedProps = <T>(obj: T): T => {
|
export const removeUndefinedProps = <T>(obj: T): T => {
|
||||||
let newObj: any = {}
|
const newObj: any = {}
|
||||||
|
|
||||||
for (let key of Object.keys(obj)) {
|
for (const key of Object.keys(obj)) {
|
||||||
if ((obj as any)[key] !== undefined) newObj[key] = (obj as any)[key]
|
if ((obj as any)[key] !== undefined) newObj[key] = (obj as any)[key]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,10 +14,10 @@ export const addObjects = <T extends { [key: string]: number }>(
|
||||||
obj1: T,
|
obj1: T,
|
||||||
obj2: T
|
obj2: T
|
||||||
) => {
|
) => {
|
||||||
const keys = _.union(Object.keys(obj1), Object.keys(obj2))
|
const keys = union(Object.keys(obj1), Object.keys(obj2))
|
||||||
const newObj = {} as any
|
const newObj = {} as any
|
||||||
|
|
||||||
for (let key of keys) {
|
for (const key of keys) {
|
||||||
newObj[key] = (obj1[key] ?? 0) + (obj2[key] ?? 0)
|
newObj[key] = (obj1[key] ?? 0) + (obj2[key] ?? 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,8 @@ export const randomString = (length = 12) =>
|
||||||
|
|
||||||
export function genHash(str: string) {
|
export function genHash(str: string) {
|
||||||
// xmur3
|
// xmur3
|
||||||
for (var i = 0, h = 1779033703 ^ str.length; i < str.length; i++) {
|
let h: number
|
||||||
|
for (let i = 0, h = 1779033703 ^ str.length; i < str.length; i++) {
|
||||||
h = Math.imul(h ^ str.charCodeAt(i), 3432918353)
|
h = Math.imul(h ^ str.charCodeAt(i), 3432918353)
|
||||||
h = (h << 13) | (h >>> 19)
|
h = (h << 13) | (h >>> 19)
|
||||||
}
|
}
|
||||||
|
@ -28,7 +29,7 @@ export function createRNG(seed: string) {
|
||||||
b >>>= 0
|
b >>>= 0
|
||||||
c >>>= 0
|
c >>>= 0
|
||||||
d >>>= 0
|
d >>>= 0
|
||||||
var t = (a + b) | 0
|
let t = (a + b) | 0
|
||||||
a = b ^ (b >>> 9)
|
a = b ^ (b >>> 9)
|
||||||
b = (c + (c << 3)) | 0
|
b = (c + (c << 3)) | 0
|
||||||
c = (c << 21) | (c >>> 11)
|
c = (c << 21) | (c >>> 11)
|
||||||
|
@ -39,7 +40,7 @@ export function createRNG(seed: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const shuffle = (array: any[], rand: () => number) => {
|
export const shuffle = (array: unknown[], rand: () => number) => {
|
||||||
for (let i = 0; i < array.length; i++) {
|
for (let i = 0; i < array.length; i++) {
|
||||||
const swapIndex = Math.floor(rand() * (array.length - i))
|
const swapIndex = Math.floor(rand() * (array.length - i))
|
||||||
;[array[i], array[swapIndex]] = [array[swapIndex], array[i]]
|
;[array[i], array[swapIndex]] = [array[swapIndex], array[i]]
|
||||||
|
|
25
functions/.eslintrc.js
Normal file
25
functions/.eslintrc.js
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
module.exports = {
|
||||||
|
plugins: ['lodash'],
|
||||||
|
extends: ['eslint:recommended'],
|
||||||
|
ignorePatterns: ['lib'],
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ['**/*.ts'],
|
||||||
|
plugins: ['@typescript-eslint'],
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
parserOptions: {
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
project: ['./tsconfig.json'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
'no-extra-semi': 'off',
|
||||||
|
'no-unused-vars': 'off',
|
||||||
|
'no-constant-condition': ['error', { checkLoops: false }],
|
||||||
|
'lodash/import-scope': [2, 'member'],
|
||||||
|
},
|
||||||
|
}
|
|
@ -24,8 +24,9 @@ Adapted from https://firebase.google.com/docs/functions/get-started
|
||||||
|
|
||||||
0. `$ firebase functions:config:get > .runtimeconfig.json` to cache secrets for local dev
|
0. `$ firebase functions:config:get > .runtimeconfig.json` to cache secrets for local dev
|
||||||
1. [Install](https://cloud.google.com/sdk/docs/install) gcloud CLI
|
1. [Install](https://cloud.google.com/sdk/docs/install) gcloud CLI
|
||||||
2. `$ brew install java` to install java if you don't already have it
|
2. If you don't have java (or see the error `Error: Process java -version has exited with code 1. Please make sure Java is installed and on your system PATH.`):
|
||||||
1. `$ echo 'export PATH="/usr/local/opt/openjdk/bin:$PATH"' >> ~/.zshrc` to add java to your path
|
1. `$ brew install java`
|
||||||
|
2. `$ sudo ln -sfn /opt/homebrew/opt/openjdk/libexec/openjdk.jdk /Library/Java/JavaVirtualMachines/openjdk.jdk`
|
||||||
3. `$ gcloud auth login` to authenticate the CLI tools to Google Cloud
|
3. `$ gcloud auth login` to authenticate the CLI tools to Google Cloud
|
||||||
4. `$ gcloud config set project <project-id>` to choose the project (`$ gcloud projects list` to see options)
|
4. `$ gcloud config set project <project-id>` to choose the project (`$ gcloud projects list` to see options)
|
||||||
5. `$ mkdir firestore_export` to create a folder to store the exported database
|
5. `$ mkdir firestore_export` to create a folder to store the exported database
|
||||||
|
|
|
@ -26,15 +26,15 @@
|
||||||
"firebase-functions": "3.16.0",
|
"firebase-functions": "3.16.0",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"mailgun-js": "0.22.0",
|
"mailgun-js": "0.22.0",
|
||||||
"react-query": "3.39.0",
|
|
||||||
"module-alias": "2.2.2",
|
"module-alias": "2.2.2",
|
||||||
"stripe": "8.194.0"
|
"react-query": "3.39.0",
|
||||||
|
"stripe": "8.194.0",
|
||||||
|
"zod": "3.17.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/module-alias": "2.0.1",
|
|
||||||
"@types/mailgun-js": "0.22.12",
|
"@types/mailgun-js": "0.22.12",
|
||||||
"firebase-functions-test": "0.3.3",
|
"@types/module-alias": "2.0.1",
|
||||||
"typescript": "4.5.3"
|
"firebase-functions-test": "0.3.3"
|
||||||
},
|
},
|
||||||
"private": true
|
"private": true
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,19 @@
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
import * as functions from 'firebase-functions'
|
import * as functions from 'firebase-functions'
|
||||||
import * as Cors from 'cors'
|
import * as Cors from 'cors'
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
import { User, PrivateUser } from 'common/user'
|
import { User, PrivateUser } from '../../common/user'
|
||||||
|
import {
|
||||||
|
CORS_ORIGIN_MANIFOLD,
|
||||||
|
CORS_ORIGIN_LOCALHOST,
|
||||||
|
} from '../../common/envs/constants'
|
||||||
|
|
||||||
|
type Output = Record<string, unknown>
|
||||||
type Request = functions.https.Request
|
type Request = functions.https.Request
|
||||||
type Response = functions.Response
|
type Response = functions.Response
|
||||||
type Handler = (req: Request, res: Response) => Promise<any>
|
|
||||||
type AuthedUser = [User, PrivateUser]
|
type AuthedUser = [User, PrivateUser]
|
||||||
|
type Handler = (req: Request, user: AuthedUser) => Promise<Output>
|
||||||
type JwtCredentials = { kind: 'jwt'; data: admin.auth.DecodedIdToken }
|
type JwtCredentials = { kind: 'jwt'; data: admin.auth.DecodedIdToken }
|
||||||
type KeyCredentials = { kind: 'key'; data: string }
|
type KeyCredentials = { kind: 'key'; data: string }
|
||||||
type Credentials = JwtCredentials | KeyCredentials
|
type Credentials = JwtCredentials | KeyCredentials
|
||||||
|
@ -15,10 +21,13 @@ type Credentials = JwtCredentials | KeyCredentials
|
||||||
export class APIError {
|
export class APIError {
|
||||||
code: number
|
code: number
|
||||||
msg: string
|
msg: string
|
||||||
constructor(code: number, msg: string) {
|
details: unknown
|
||||||
|
constructor(code: number, msg: string, details?: unknown) {
|
||||||
this.code = code
|
this.code = code
|
||||||
this.msg = msg
|
this.msg = msg
|
||||||
|
this.details = details
|
||||||
}
|
}
|
||||||
|
toJson() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const parseCredentials = async (req: Request): Promise<Credentials> => {
|
export const parseCredentials = async (req: Request): Promise<Credentials> => {
|
||||||
|
@ -36,14 +45,11 @@ export const parseCredentials = async (req: Request): Promise<Credentials> => {
|
||||||
case 'Bearer':
|
case 'Bearer':
|
||||||
try {
|
try {
|
||||||
const jwt = await admin.auth().verifyIdToken(payload)
|
const jwt = await admin.auth().verifyIdToken(payload)
|
||||||
if (!jwt.user_id) {
|
|
||||||
throw new APIError(403, 'JWT must contain Manifold user ID.')
|
|
||||||
}
|
|
||||||
return { kind: 'jwt', data: jwt }
|
return { kind: 'jwt', data: jwt }
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// This is somewhat suspicious, so get it into the firebase console
|
// This is somewhat suspicious, so get it into the firebase console
|
||||||
functions.logger.error('Error verifying Firebase JWT: ', err)
|
functions.logger.error('Error verifying Firebase JWT: ', err)
|
||||||
throw new APIError(403, `Error validating token: ${err}.`)
|
throw new APIError(403, 'Error validating token.')
|
||||||
}
|
}
|
||||||
case 'Key':
|
case 'Key':
|
||||||
return { kind: 'key', data: payload }
|
return { kind: 'key', data: payload }
|
||||||
|
@ -59,6 +65,9 @@ export const lookupUser = async (creds: Credentials): Promise<AuthedUser> => {
|
||||||
switch (creds.kind) {
|
switch (creds.kind) {
|
||||||
case 'jwt': {
|
case 'jwt': {
|
||||||
const { user_id } = creds.data
|
const { user_id } = creds.data
|
||||||
|
if (typeof user_id !== 'string') {
|
||||||
|
throw new APIError(403, 'JWT must contain Manifold user ID.')
|
||||||
|
}
|
||||||
const [userSnap, privateUserSnap] = await Promise.all([
|
const [userSnap, privateUserSnap] = await Promise.all([
|
||||||
users.doc(user_id).get(),
|
users.doc(user_id).get(),
|
||||||
privateUsers.doc(user_id).get(),
|
privateUsers.doc(user_id).get(),
|
||||||
|
@ -90,10 +99,11 @@ export const lookupUser = async (creds: Credentials): Promise<AuthedUser> => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CORS_ORIGIN_MANIFOLD = /^https?:\/\/.+\.manifold\.markets$/
|
export const applyCors = (
|
||||||
export const CORS_ORIGIN_LOCALHOST = /^http:\/\/localhost:\d+$/
|
req: Request,
|
||||||
|
res: Response,
|
||||||
export const applyCors = (req: any, res: any, params: object) => {
|
params: Cors.CorsOptions
|
||||||
|
) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
Cors(params)(req, res, (result) => {
|
Cors(params)(req, res, (result) => {
|
||||||
if (result instanceof Error) {
|
if (result instanceof Error) {
|
||||||
|
@ -104,10 +114,31 @@ export const applyCors = (req: any, res: any, params: object) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const zTimestamp = () => {
|
||||||
|
return z.preprocess((arg) => {
|
||||||
|
return typeof arg == 'number' ? new Date(arg) : undefined
|
||||||
|
}, z.date())
|
||||||
|
}
|
||||||
|
|
||||||
|
export const validate = <T extends z.ZodTypeAny>(schema: T, val: unknown) => {
|
||||||
|
const result = schema.safeParse(val)
|
||||||
|
if (!result.success) {
|
||||||
|
const issues = result.error.issues.map((i) => {
|
||||||
|
return {
|
||||||
|
field: i.path.join('.') || null,
|
||||||
|
error: i.message,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
throw new APIError(400, 'Error validating request.', issues)
|
||||||
|
} else {
|
||||||
|
return result.data as z.infer<T>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const newEndpoint = (methods: [string], fn: Handler) =>
|
export const newEndpoint = (methods: [string], fn: Handler) =>
|
||||||
functions.runWith({ minInstances: 1 }).https.onRequest(async (req, res) => {
|
functions.runWith({ minInstances: 1 }).https.onRequest(async (req, res) => {
|
||||||
await applyCors(req, res, {
|
await applyCors(req, res, {
|
||||||
origins: [CORS_ORIGIN_MANIFOLD, CORS_ORIGIN_LOCALHOST],
|
origin: [CORS_ORIGIN_MANIFOLD, CORS_ORIGIN_LOCALHOST],
|
||||||
methods: methods,
|
methods: methods,
|
||||||
})
|
})
|
||||||
try {
|
try {
|
||||||
|
@ -115,15 +146,18 @@ export const newEndpoint = (methods: [string], fn: Handler) =>
|
||||||
const allowed = methods.join(', ')
|
const allowed = methods.join(', ')
|
||||||
throw new APIError(405, `This endpoint supports only ${allowed}.`)
|
throw new APIError(405, `This endpoint supports only ${allowed}.`)
|
||||||
}
|
}
|
||||||
const data = await fn(req, res)
|
const authedUser = await lookupUser(await parseCredentials(req))
|
||||||
data.status = 'success'
|
res.status(200).json(await fn(req, authedUser))
|
||||||
res.status(200).json({ data: data })
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof APIError) {
|
if (e instanceof APIError) {
|
||||||
// Emit a 200 anyway here for now, for backwards compatibility
|
const output: { [k: string]: unknown } = { message: e.msg }
|
||||||
res.status(200).json({ data: { status: 'error', message: e.msg } })
|
if (e.details != null) {
|
||||||
|
output.details = e.details
|
||||||
|
}
|
||||||
|
res.status(e.code).json(output)
|
||||||
} else {
|
} else {
|
||||||
res.status(500).json({ data: { status: 'error', message: '???' } })
|
functions.logger.error(e)
|
||||||
|
res.status(500).json({ message: 'An unknown error occurred.' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -12,8 +12,6 @@ import { getNewMultiBetInfo } from '../../common/new-bet'
|
||||||
import { Answer, MAX_ANSWER_LENGTH } from '../../common/answer'
|
import { Answer, MAX_ANSWER_LENGTH } from '../../common/answer'
|
||||||
import { getContract, getValues } from './utils'
|
import { getContract, getValues } from './utils'
|
||||||
import { sendNewAnswerEmail } from './emails'
|
import { sendNewAnswerEmail } from './emails'
|
||||||
import { Bet } from '../../common/bet'
|
|
||||||
import { hasUserHitManaLimit } from '../../common/calculate'
|
|
||||||
|
|
||||||
export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
|
export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
async (
|
async (
|
||||||
|
@ -62,18 +60,6 @@ export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
if (closeTime && Date.now() > closeTime)
|
if (closeTime && Date.now() > closeTime)
|
||||||
return { status: 'error', message: 'Trading is closed' }
|
return { status: 'error', message: 'Trading is closed' }
|
||||||
|
|
||||||
const yourBetsSnap = await transaction.get(
|
|
||||||
contractDoc.collection('bets').where('userId', '==', userId)
|
|
||||||
)
|
|
||||||
const yourBets = yourBetsSnap.docs.map((doc) => doc.data() as Bet)
|
|
||||||
|
|
||||||
const { status, message } = hasUserHitManaLimit(
|
|
||||||
contract,
|
|
||||||
yourBets,
|
|
||||||
amount
|
|
||||||
)
|
|
||||||
if (status === 'error') return { status, message: message }
|
|
||||||
|
|
||||||
const [lastAnswer] = await getValues<Answer>(
|
const [lastAnswer] = await getValues<Answer>(
|
||||||
firestore
|
firestore
|
||||||
.collection(`contracts/${contractId}/answers`)
|
.collection(`contracts/${contractId}/answers`)
|
||||||
|
@ -107,23 +93,20 @@ export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
}
|
}
|
||||||
transaction.create(newAnswerDoc, answer)
|
transaction.create(newAnswerDoc, answer)
|
||||||
|
|
||||||
const newBetDoc = firestore
|
const loanAmount = 0
|
||||||
.collection(`contracts/${contractId}/bets`)
|
|
||||||
.doc()
|
|
||||||
|
|
||||||
const loanAmount = 0 // getLoanAmount(yourBets, amount)
|
const { newBet, newPool, newTotalShares, newTotalBets } =
|
||||||
|
|
||||||
const { newBet, newPool, newTotalShares, newTotalBets, newBalance } =
|
|
||||||
getNewMultiBetInfo(
|
getNewMultiBetInfo(
|
||||||
user,
|
|
||||||
answerId,
|
answerId,
|
||||||
amount,
|
amount,
|
||||||
contract as FullContract<DPM, FreeResponse>,
|
contract as FullContract<DPM, FreeResponse>,
|
||||||
loanAmount,
|
loanAmount
|
||||||
newBetDoc.id
|
|
||||||
)
|
)
|
||||||
|
|
||||||
transaction.create(newBetDoc, newBet)
|
const newBalance = user.balance - amount
|
||||||
|
const betDoc = firestore.collection(`contracts/${contractId}/bets`).doc()
|
||||||
|
transaction.create(betDoc, { id: betDoc.id, userId: user.id, ...newBet })
|
||||||
|
transaction.update(userDoc, { balance: newBalance })
|
||||||
transaction.update(contractDoc, {
|
transaction.update(contractDoc, {
|
||||||
pool: newPool,
|
pool: newPool,
|
||||||
totalShares: newTotalShares,
|
totalShares: newTotalShares,
|
||||||
|
@ -132,13 +115,7 @@ export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
volume: volume + amount,
|
volume: volume + amount,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!isFinite(newBalance)) {
|
return { status: 'success', answerId, betId: betDoc.id, answer }
|
||||||
throw new Error('Invalid user balance for ' + user.username)
|
|
||||||
}
|
|
||||||
|
|
||||||
transaction.update(userDoc, { balance: newBalance })
|
|
||||||
|
|
||||||
return { status: 'success', answerId, betId: newBetDoc.id, answer }
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const { answer } = result
|
const { answer } = result
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
import { chargeUser } from './utils'
|
|
||||||
import { APIError, newEndpoint, parseCredentials, lookupUser } from './api'
|
|
||||||
import {
|
import {
|
||||||
Binary,
|
Binary,
|
||||||
Contract,
|
Contract,
|
||||||
|
@ -12,82 +11,82 @@ import {
|
||||||
MAX_DESCRIPTION_LENGTH,
|
MAX_DESCRIPTION_LENGTH,
|
||||||
MAX_QUESTION_LENGTH,
|
MAX_QUESTION_LENGTH,
|
||||||
MAX_TAG_LENGTH,
|
MAX_TAG_LENGTH,
|
||||||
|
Numeric,
|
||||||
|
OUTCOME_TYPES,
|
||||||
} from '../../common/contract'
|
} from '../../common/contract'
|
||||||
import { slugify } from '../../common/util/slugify'
|
import { slugify } from '../../common/util/slugify'
|
||||||
import { randomString } from '../../common/util/random'
|
import { randomString } from '../../common/util/random'
|
||||||
import { getNewContract } from '../../common/new-contract'
|
|
||||||
|
import { chargeUser } from './utils'
|
||||||
|
import { APIError, newEndpoint, validate, zTimestamp } from './api'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
FIXED_ANTE,
|
FIXED_ANTE,
|
||||||
getAnteBets,
|
getAnteBets,
|
||||||
getCpmmInitialLiquidity,
|
getCpmmInitialLiquidity,
|
||||||
getFreeAnswerAnte,
|
getFreeAnswerAnte,
|
||||||
|
getNumericAnte,
|
||||||
HOUSE_LIQUIDITY_PROVIDER_ID,
|
HOUSE_LIQUIDITY_PROVIDER_ID,
|
||||||
MINIMUM_ANTE,
|
|
||||||
} from '../../common/antes'
|
} from '../../common/antes'
|
||||||
import { getNoneAnswer } from '../../common/answer'
|
import { getNoneAnswer } from '../../common/answer'
|
||||||
|
import { getNewContract } from '../../common/new-contract'
|
||||||
|
import { NUMERIC_BUCKET_COUNT } from '../../common/numeric-constants'
|
||||||
|
|
||||||
export const createContract = newEndpoint(['POST'], async (req, _res) => {
|
const bodySchema = z.object({
|
||||||
const [creator, _privateUser] = await lookupUser(await parseCredentials(req))
|
question: z.string().min(1).max(MAX_QUESTION_LENGTH),
|
||||||
let {
|
description: z.string().max(MAX_DESCRIPTION_LENGTH),
|
||||||
question,
|
tags: z.array(z.string().min(1).max(MAX_TAG_LENGTH)).optional(),
|
||||||
outcomeType,
|
closeTime: zTimestamp().refine(
|
||||||
description,
|
(date) => date.getTime() > new Date().getTime(),
|
||||||
initialProb,
|
'Close time must be in the future.'
|
||||||
closeTime,
|
),
|
||||||
tags,
|
outcomeType: z.enum(OUTCOME_TYPES),
|
||||||
manaLimitPerUser,
|
})
|
||||||
} = req.body.data || {}
|
|
||||||
|
|
||||||
if (!question || typeof question != 'string')
|
const binarySchema = z.object({
|
||||||
throw new APIError(400, 'Missing or invalid question field')
|
initialProb: z.number().min(1).max(99),
|
||||||
|
})
|
||||||
|
|
||||||
question = question.slice(0, MAX_QUESTION_LENGTH)
|
const numericSchema = z.object({
|
||||||
|
min: z.number(),
|
||||||
|
max: z.number(),
|
||||||
|
})
|
||||||
|
|
||||||
if (typeof description !== 'string')
|
export const createContract = newEndpoint(['POST'], async (req, [user, _]) => {
|
||||||
throw new APIError(400, 'Invalid description field')
|
const { question, description, tags, closeTime, outcomeType } = validate(
|
||||||
|
bodySchema,
|
||||||
description = description.slice(0, MAX_DESCRIPTION_LENGTH)
|
req.body
|
||||||
|
|
||||||
if (tags !== undefined && !Array.isArray(tags))
|
|
||||||
throw new APIError(400, 'Invalid tags field')
|
|
||||||
|
|
||||||
tags = (tags || []).map((tag: string) =>
|
|
||||||
tag.toString().slice(0, MAX_TAG_LENGTH)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
outcomeType = outcomeType ?? 'BINARY'
|
let min, max, initialProb
|
||||||
if (!['BINARY', 'MULTI', 'FREE_RESPONSE'].includes(outcomeType))
|
if (outcomeType === 'NUMERIC') {
|
||||||
throw new APIError(400, 'Invalid outcomeType')
|
;({ min, max } = validate(numericSchema, req.body))
|
||||||
|
if (max - min <= 0.01) throw new APIError(400, 'Invalid range.')
|
||||||
|
}
|
||||||
|
if (outcomeType === 'BINARY') {
|
||||||
|
;({ initialProb } = validate(binarySchema, req.body))
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
// Uses utc time on server:
|
||||||
outcomeType === 'BINARY' &&
|
const today = new Date()
|
||||||
(!initialProb || initialProb < 1 || initialProb > 99)
|
let freeMarketResetTime = today.setUTCHours(16, 0, 0, 0)
|
||||||
)
|
if (today.getTime() < freeMarketResetTime) {
|
||||||
throw new APIError(400, 'Invalid initial probability')
|
freeMarketResetTime = freeMarketResetTime - 24 * 60 * 60 * 1000
|
||||||
|
}
|
||||||
|
|
||||||
// uses utc time on server:
|
|
||||||
const today = new Date().setHours(0, 0, 0, 0)
|
|
||||||
const userContractsCreatedTodaySnapshot = await firestore
|
const userContractsCreatedTodaySnapshot = await firestore
|
||||||
.collection(`contracts`)
|
.collection(`contracts`)
|
||||||
.where('creatorId', '==', creator.id)
|
.where('creatorId', '==', user.id)
|
||||||
.where('createdTime', '>=', today)
|
.where('createdTime', '>=', freeMarketResetTime)
|
||||||
.get()
|
.get()
|
||||||
|
console.log('free market reset time: ', freeMarketResetTime)
|
||||||
const isFree = userContractsCreatedTodaySnapshot.size === 0
|
const isFree = userContractsCreatedTodaySnapshot.size === 0
|
||||||
|
|
||||||
const ante = FIXED_ANTE
|
const ante = FIXED_ANTE
|
||||||
|
|
||||||
if (
|
|
||||||
ante === undefined ||
|
|
||||||
ante < MINIMUM_ANTE ||
|
|
||||||
(ante > creator.balance && !isFree) ||
|
|
||||||
isNaN(ante) ||
|
|
||||||
!isFinite(ante)
|
|
||||||
)
|
|
||||||
throw new APIError(400, 'Invalid ante')
|
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
'creating contract for',
|
'creating contract for',
|
||||||
creator.username,
|
user.username,
|
||||||
'on',
|
'on',
|
||||||
question,
|
question,
|
||||||
'ante:',
|
'ante:',
|
||||||
|
@ -95,83 +94,92 @@ export const createContract = newEndpoint(['POST'], async (req, _res) => {
|
||||||
)
|
)
|
||||||
|
|
||||||
const slug = await getSlug(question)
|
const slug = await getSlug(question)
|
||||||
|
|
||||||
const contractRef = firestore.collection('contracts').doc()
|
const contractRef = firestore.collection('contracts').doc()
|
||||||
|
|
||||||
const contract = getNewContract(
|
const contract = getNewContract(
|
||||||
contractRef.id,
|
contractRef.id,
|
||||||
slug,
|
slug,
|
||||||
creator,
|
user,
|
||||||
question,
|
question,
|
||||||
outcomeType,
|
outcomeType,
|
||||||
description,
|
description,
|
||||||
initialProb,
|
initialProb ?? 0,
|
||||||
ante,
|
ante,
|
||||||
closeTime,
|
closeTime.getTime(),
|
||||||
tags ?? [],
|
tags ?? [],
|
||||||
manaLimitPerUser ?? 0
|
NUMERIC_BUCKET_COUNT,
|
||||||
|
min ?? 0,
|
||||||
|
max ?? 0
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!isFree && ante) await chargeUser(creator.id, ante, true)
|
if (!isFree && ante) await chargeUser(user.id, ante, true)
|
||||||
|
|
||||||
await contractRef.create(contract)
|
await contractRef.create(contract)
|
||||||
|
|
||||||
if (ante) {
|
const providerId = isFree ? HOUSE_LIQUIDITY_PROVIDER_ID : user.id
|
||||||
if (outcomeType === 'BINARY' && contract.mechanism === 'dpm-2') {
|
|
||||||
const yesBetDoc = firestore
|
|
||||||
.collection(`contracts/${contract.id}/bets`)
|
|
||||||
.doc()
|
|
||||||
|
|
||||||
const noBetDoc = firestore
|
if (outcomeType === 'BINARY' && contract.mechanism === 'dpm-2') {
|
||||||
.collection(`contracts/${contract.id}/bets`)
|
const yesBetDoc = firestore
|
||||||
.doc()
|
.collection(`contracts/${contract.id}/bets`)
|
||||||
|
.doc()
|
||||||
|
|
||||||
const { yesBet, noBet } = getAnteBets(
|
const noBetDoc = firestore.collection(`contracts/${contract.id}/bets`).doc()
|
||||||
creator,
|
|
||||||
contract as FullContract<DPM, Binary>,
|
|
||||||
yesBetDoc.id,
|
|
||||||
noBetDoc.id
|
|
||||||
)
|
|
||||||
|
|
||||||
await yesBetDoc.set(yesBet)
|
const { yesBet, noBet } = getAnteBets(
|
||||||
await noBetDoc.set(noBet)
|
user,
|
||||||
} else if (outcomeType === 'BINARY') {
|
contract as FullContract<DPM, Binary>,
|
||||||
const liquidityDoc = firestore
|
yesBetDoc.id,
|
||||||
.collection(`contracts/${contract.id}/liquidity`)
|
noBetDoc.id
|
||||||
.doc()
|
)
|
||||||
|
|
||||||
const providerId = isFree ? HOUSE_LIQUIDITY_PROVIDER_ID : creator.id
|
await yesBetDoc.set(yesBet)
|
||||||
|
await noBetDoc.set(noBet)
|
||||||
|
} else if (outcomeType === 'BINARY') {
|
||||||
|
const liquidityDoc = firestore
|
||||||
|
.collection(`contracts/${contract.id}/liquidity`)
|
||||||
|
.doc()
|
||||||
|
|
||||||
const lp = getCpmmInitialLiquidity(
|
const lp = getCpmmInitialLiquidity(
|
||||||
providerId,
|
providerId,
|
||||||
contract as FullContract<CPMM, Binary>,
|
contract as FullContract<CPMM, Binary>,
|
||||||
liquidityDoc.id,
|
liquidityDoc.id,
|
||||||
ante
|
ante
|
||||||
)
|
)
|
||||||
|
|
||||||
await liquidityDoc.set(lp)
|
await liquidityDoc.set(lp)
|
||||||
} else if (outcomeType === 'FREE_RESPONSE') {
|
} else if (outcomeType === 'FREE_RESPONSE') {
|
||||||
const noneAnswerDoc = firestore
|
const noneAnswerDoc = firestore
|
||||||
.collection(`contracts/${contract.id}/answers`)
|
.collection(`contracts/${contract.id}/answers`)
|
||||||
.doc('0')
|
.doc('0')
|
||||||
|
|
||||||
const noneAnswer = getNoneAnswer(contract.id, creator)
|
const noneAnswer = getNoneAnswer(contract.id, user)
|
||||||
await noneAnswerDoc.set(noneAnswer)
|
await noneAnswerDoc.set(noneAnswer)
|
||||||
|
|
||||||
const anteBetDoc = firestore
|
const anteBetDoc = firestore
|
||||||
.collection(`contracts/${contract.id}/bets`)
|
.collection(`contracts/${contract.id}/bets`)
|
||||||
.doc()
|
.doc()
|
||||||
|
|
||||||
const anteBet = getFreeAnswerAnte(
|
const anteBet = getFreeAnswerAnte(
|
||||||
creator,
|
providerId,
|
||||||
contract as FullContract<DPM, FreeResponse>,
|
contract as FullContract<DPM, FreeResponse>,
|
||||||
anteBetDoc.id
|
anteBetDoc.id
|
||||||
)
|
)
|
||||||
await anteBetDoc.set(anteBet)
|
await anteBetDoc.set(anteBet)
|
||||||
}
|
} else if (outcomeType === 'NUMERIC') {
|
||||||
|
const anteBetDoc = firestore
|
||||||
|
.collection(`contracts/${contract.id}/bets`)
|
||||||
|
.doc()
|
||||||
|
|
||||||
|
const anteBet = getNumericAnte(
|
||||||
|
user,
|
||||||
|
contract as FullContract<DPM, Numeric>,
|
||||||
|
ante,
|
||||||
|
anteBetDoc.id
|
||||||
|
)
|
||||||
|
|
||||||
|
await anteBetDoc.set(anteBet)
|
||||||
}
|
}
|
||||||
|
|
||||||
return { contract: contract }
|
return contract
|
||||||
})
|
})
|
||||||
|
|
||||||
const getSlug = async (question: string) => {
|
const getSlug = async (question: string) => {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
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 * as _ from 'lodash'
|
|
||||||
|
|
||||||
import { getUser } from './utils'
|
import { getUser } from './utils'
|
||||||
import { Contract } from '../../common/contract'
|
import { Contract } from '../../common/contract'
|
||||||
|
@ -34,7 +33,7 @@ export const createFold = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
return { status: 'error', message: 'About must be a string' }
|
return { status: 'error', message: 'About must be a string' }
|
||||||
about = about.trim().slice(0, 140)
|
about = about.trim().slice(0, 140)
|
||||||
|
|
||||||
if (!_.isArray(tags))
|
if (!Array.isArray(tags))
|
||||||
return { status: 'error', message: 'Tags must be an array of strings' }
|
return { status: 'error', message: 'Tags must be an array of strings' }
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import * as _ from 'lodash'
|
|
||||||
|
|
||||||
import { DOMAIN, PROJECT_ID } from '../../common/envs/constants'
|
import { DOMAIN, PROJECT_ID } from '../../common/envs/constants'
|
||||||
import { Answer } from '../../common/answer'
|
import { Answer } from '../../common/answer'
|
||||||
import { Bet } from '../../common/bet'
|
import { Bet } from '../../common/bet'
|
||||||
|
@ -9,6 +7,8 @@ import { Contract, FreeResponseContract } from '../../common/contract'
|
||||||
import { DPM_CREATOR_FEE } from '../../common/fees'
|
import { DPM_CREATOR_FEE } from '../../common/fees'
|
||||||
import { PrivateUser, User } from '../../common/user'
|
import { PrivateUser, User } from '../../common/user'
|
||||||
import { formatMoney, formatPercent } from '../../common/util/format'
|
import { formatMoney, formatPercent } from '../../common/util/format'
|
||||||
|
import { getValueFromBucket } from '../../common/calculate-dpm'
|
||||||
|
|
||||||
import { sendTemplateEmail } from './send-email'
|
import { sendTemplateEmail } from './send-email'
|
||||||
import { getPrivateUser, getUser } from './utils'
|
import { getPrivateUser, getUser } from './utils'
|
||||||
|
|
||||||
|
@ -104,6 +104,12 @@ const toDisplayResolution = (
|
||||||
if (resolution === 'MKT' && resolutions) return 'MULTI'
|
if (resolution === 'MKT' && resolutions) return 'MULTI'
|
||||||
if (resolution === 'CANCEL') return 'N/A'
|
if (resolution === 'CANCEL') return 'N/A'
|
||||||
|
|
||||||
|
if (contract.outcomeType === 'NUMERIC' && contract.mechanism === 'dpm-2')
|
||||||
|
return (
|
||||||
|
contract.resolutionValue?.toString() ??
|
||||||
|
getValueFromBucket(resolution, contract).toString()
|
||||||
|
)
|
||||||
|
|
||||||
const answer = (contract as FreeResponseContract).answers?.find(
|
const answer = (contract as FreeResponseContract).answers?.find(
|
||||||
(a) => a.id === resolution
|
(a) => a.id === resolution
|
||||||
)
|
)
|
||||||
|
@ -244,7 +250,8 @@ export const sendNewCommentEmail = async (
|
||||||
contract: Contract,
|
contract: Contract,
|
||||||
comment: Comment,
|
comment: Comment,
|
||||||
bet?: Bet,
|
bet?: Bet,
|
||||||
answer?: Answer
|
answerText?: string,
|
||||||
|
answerId?: string
|
||||||
) => {
|
) => {
|
||||||
const privateUser = await getPrivateUser(userId)
|
const privateUser = await getPrivateUser(userId)
|
||||||
if (
|
if (
|
||||||
|
@ -255,7 +262,7 @@ export const sendNewCommentEmail = async (
|
||||||
return
|
return
|
||||||
|
|
||||||
const { question, creatorUsername, slug } = contract
|
const { question, creatorUsername, slug } = contract
|
||||||
const marketUrl = `https://${DOMAIN}/${creatorUsername}/${slug}`
|
const marketUrl = `https://${DOMAIN}/${creatorUsername}/${slug}#${comment.id}`
|
||||||
|
|
||||||
const unsubscribeUrl = `https://us-central1-${PROJECT_ID}.cloudfunctions.net/unsubscribe?id=${userId}&type=market-comment`
|
const unsubscribeUrl = `https://us-central1-${PROJECT_ID}.cloudfunctions.net/unsubscribe?id=${userId}&type=market-comment`
|
||||||
|
|
||||||
|
@ -273,9 +280,8 @@ export const sendNewCommentEmail = async (
|
||||||
const subject = `Comment on ${question}`
|
const subject = `Comment on ${question}`
|
||||||
const from = `${commentorName} <info@manifold.markets>`
|
const from = `${commentorName} <info@manifold.markets>`
|
||||||
|
|
||||||
if (contract.outcomeType === 'FREE_RESPONSE') {
|
if (contract.outcomeType === 'FREE_RESPONSE' && answerId && answerText) {
|
||||||
const answerText = answer?.text ?? ''
|
const answerNumber = `#${answerId}`
|
||||||
const answerNumber = `#${answer?.id ?? ''}`
|
|
||||||
|
|
||||||
await sendTemplateEmail(
|
await sendTemplateEmail(
|
||||||
privateUser.email,
|
privateUser.email,
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
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 * as _ from 'lodash'
|
|
||||||
|
|
||||||
import { getContract } from './utils'
|
import { getContract } from './utils'
|
||||||
import { Bet } from '../../common/bet'
|
import { Bet } from '../../common/bet'
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
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 * as _ from 'lodash'
|
import { uniq } from 'lodash'
|
||||||
|
|
||||||
import { getContract, getUser, getValues } from './utils'
|
import { getContract, getUser, getValues } from './utils'
|
||||||
import { Comment } from '../../common/comment'
|
import { Comment } from '../../common/comment'
|
||||||
|
@ -34,7 +34,14 @@ export const onCreateComment = functions.firestore
|
||||||
|
|
||||||
let bet: Bet | undefined
|
let bet: Bet | undefined
|
||||||
let answer: Answer | undefined
|
let answer: Answer | undefined
|
||||||
if (comment.betId) {
|
if (comment.answerOutcome) {
|
||||||
|
answer =
|
||||||
|
contract.outcomeType === 'FREE_RESPONSE' && contract.answers
|
||||||
|
? contract.answers?.find(
|
||||||
|
(answer) => answer.id === comment.answerOutcome
|
||||||
|
)
|
||||||
|
: undefined
|
||||||
|
} else if (comment.betId) {
|
||||||
const betSnapshot = await firestore
|
const betSnapshot = await firestore
|
||||||
.collection('contracts')
|
.collection('contracts')
|
||||||
.doc(contractId)
|
.doc(contractId)
|
||||||
|
@ -53,7 +60,7 @@ export const onCreateComment = functions.firestore
|
||||||
firestore.collection('contracts').doc(contractId).collection('comments')
|
firestore.collection('contracts').doc(contractId).collection('comments')
|
||||||
)
|
)
|
||||||
|
|
||||||
const recipientUserIds = _.uniq([
|
const recipientUserIds = uniq([
|
||||||
contract.creatorId,
|
contract.creatorId,
|
||||||
...comments.map((comment) => comment.userId),
|
...comments.map((comment) => comment.userId),
|
||||||
]).filter((id) => id !== comment.userId)
|
]).filter((id) => id !== comment.userId)
|
||||||
|
@ -66,7 +73,8 @@ export const onCreateComment = functions.firestore
|
||||||
contract,
|
contract,
|
||||||
comment,
|
comment,
|
||||||
bet,
|
bet,
|
||||||
answer
|
answer?.text,
|
||||||
|
answer?.id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,141 +1,112 @@
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
import { APIError, newEndpoint, parseCredentials, lookupUser } from './api'
|
import { APIError, newEndpoint, validate } from './api'
|
||||||
import { Contract } from '../../common/contract'
|
import { Contract } from '../../common/contract'
|
||||||
import { User } from '../../common/user'
|
import { User } from '../../common/user'
|
||||||
import {
|
import {
|
||||||
|
BetInfo,
|
||||||
getNewBinaryCpmmBetInfo,
|
getNewBinaryCpmmBetInfo,
|
||||||
getNewBinaryDpmBetInfo,
|
getNewBinaryDpmBetInfo,
|
||||||
getNewMultiBetInfo,
|
getNewMultiBetInfo,
|
||||||
|
getNumericBetsInfo,
|
||||||
} from '../../common/new-bet'
|
} from '../../common/new-bet'
|
||||||
import { addObjects, removeUndefinedProps } from '../../common/util/object'
|
import { addObjects, removeUndefinedProps } from '../../common/util/object'
|
||||||
import { Bet } from '../../common/bet'
|
|
||||||
import { redeemShares } from './redeem-shares'
|
import { redeemShares } from './redeem-shares'
|
||||||
import { Fees } from '../../common/fees'
|
|
||||||
import { hasUserHitManaLimit } from '../../common/calculate'
|
|
||||||
|
|
||||||
export const placeBet = newEndpoint(['POST'], async (req, _res) => {
|
const bodySchema = z.object({
|
||||||
const [bettor, _privateUser] = await lookupUser(await parseCredentials(req))
|
contractId: z.string(),
|
||||||
const { amount, outcome, contractId } = req.body.data || {}
|
amount: z.number().gte(1),
|
||||||
|
})
|
||||||
|
|
||||||
if (amount <= 0 || isNaN(amount) || !isFinite(amount))
|
const binarySchema = z.object({
|
||||||
throw new APIError(400, 'Invalid amount')
|
outcome: z.enum(['YES', 'NO']),
|
||||||
|
})
|
||||||
|
|
||||||
if (outcome !== 'YES' && outcome !== 'NO' && isNaN(+outcome))
|
const freeResponseSchema = z.object({
|
||||||
throw new APIError(400, 'Invalid outcome')
|
outcome: z.string(),
|
||||||
|
})
|
||||||
|
|
||||||
// run as transaction to prevent race conditions
|
const numericSchema = z.object({
|
||||||
return await firestore
|
outcome: z.string(),
|
||||||
.runTransaction(async (transaction) => {
|
value: z.number(),
|
||||||
const userDoc = firestore.doc(`users/${bettor.id}`)
|
})
|
||||||
const userSnap = await transaction.get(userDoc)
|
|
||||||
if (!userSnap.exists) throw new APIError(400, 'User not found')
|
|
||||||
const user = userSnap.data() as User
|
|
||||||
|
|
||||||
const contractDoc = firestore.doc(`contracts/${contractId}`)
|
export const placeBet = newEndpoint(['POST'], async (req, [bettor, _]) => {
|
||||||
const contractSnap = await transaction.get(contractDoc)
|
const { amount, contractId } = validate(bodySchema, req.body)
|
||||||
if (!contractSnap.exists) throw new APIError(400, 'Invalid contract')
|
|
||||||
const contract = contractSnap.data() as Contract
|
|
||||||
|
|
||||||
const { closeTime, outcomeType, mechanism, collectedFees, volume } =
|
const result = await firestore.runTransaction(async (trans) => {
|
||||||
contract
|
const userDoc = firestore.doc(`users/${bettor.id}`)
|
||||||
if (closeTime && Date.now() > closeTime)
|
const userSnap = await trans.get(userDoc)
|
||||||
throw new APIError(400, 'Trading is closed')
|
if (!userSnap.exists) throw new APIError(400, 'User not found.')
|
||||||
|
const user = userSnap.data() as User
|
||||||
|
if (user.balance < amount) throw new APIError(400, 'Insufficient balance.')
|
||||||
|
|
||||||
const yourBetsSnap = await transaction.get(
|
const contractDoc = firestore.doc(`contracts/${contractId}`)
|
||||||
contractDoc.collection('bets').where('userId', '==', bettor.id)
|
const contractSnap = await trans.get(contractDoc)
|
||||||
)
|
if (!contractSnap.exists) throw new APIError(400, 'Contract not found.')
|
||||||
const yourBets = yourBetsSnap.docs.map((doc) => doc.data() as Bet)
|
const contract = contractSnap.data() as Contract
|
||||||
|
|
||||||
const loanAmount = 0 // getLoanAmount(yourBets, amount)
|
const loanAmount = 0
|
||||||
if (user.balance < amount) throw new APIError(400, 'Insufficient balance')
|
const { closeTime, outcomeType, mechanism, collectedFees, volume } =
|
||||||
|
contract
|
||||||
|
if (closeTime && Date.now() > closeTime)
|
||||||
|
throw new APIError(400, 'Trading is closed.')
|
||||||
|
|
||||||
if (outcomeType === 'FREE_RESPONSE') {
|
const {
|
||||||
const answerSnap = await transaction.get(
|
newBet,
|
||||||
contractDoc.collection('answers').doc(outcome)
|
newPool,
|
||||||
)
|
newTotalShares,
|
||||||
if (!answerSnap.exists) throw new APIError(400, 'Invalid contract')
|
newTotalBets,
|
||||||
|
newTotalLiquidity,
|
||||||
const { status, message } = hasUserHitManaLimit(
|
newP,
|
||||||
contract,
|
} = await (async (): Promise<BetInfo> => {
|
||||||
yourBets,
|
if (outcomeType == 'BINARY' && mechanism == 'dpm-2') {
|
||||||
amount
|
const { outcome } = validate(binarySchema, req.body)
|
||||||
)
|
return getNewBinaryDpmBetInfo(outcome, amount, contract, loanAmount)
|
||||||
if (status === 'error') throw new APIError(400, message)
|
} else if (outcomeType == 'BINARY' && mechanism == 'cpmm-1') {
|
||||||
|
const { outcome } = validate(binarySchema, req.body)
|
||||||
|
return getNewBinaryCpmmBetInfo(outcome, amount, contract, loanAmount)
|
||||||
|
} else if (outcomeType == 'FREE_RESPONSE' && mechanism == 'dpm-2') {
|
||||||
|
const { outcome } = validate(freeResponseSchema, req.body)
|
||||||
|
const answerDoc = contractDoc.collection('answers').doc(outcome)
|
||||||
|
const answerSnap = await trans.get(answerDoc)
|
||||||
|
if (!answerSnap.exists) throw new APIError(400, 'Invalid answer')
|
||||||
|
return getNewMultiBetInfo(outcome, amount, contract, loanAmount)
|
||||||
|
} else if (outcomeType == 'NUMERIC' && mechanism == 'dpm-2') {
|
||||||
|
const { outcome, value } = validate(numericSchema, req.body)
|
||||||
|
return getNumericBetsInfo(value, outcome, amount, contract)
|
||||||
|
} else {
|
||||||
|
throw new APIError(500, 'Contract has invalid type/mechanism.')
|
||||||
}
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
const newBetDoc = firestore
|
if (newP != null && !isFinite(newP)) {
|
||||||
.collection(`contracts/${contractId}/bets`)
|
throw new APIError(400, 'Trade rejected due to overflow error.')
|
||||||
.doc()
|
}
|
||||||
|
|
||||||
const {
|
const newBalance = user.balance - amount - loanAmount
|
||||||
newBet,
|
const betDoc = contractDoc.collection('bets').doc()
|
||||||
newPool,
|
trans.create(betDoc, { id: betDoc.id, userId: user.id, ...newBet })
|
||||||
newTotalShares,
|
trans.update(userDoc, { balance: newBalance })
|
||||||
newTotalBets,
|
trans.update(
|
||||||
newBalance,
|
contractDoc,
|
||||||
newTotalLiquidity,
|
removeUndefinedProps({
|
||||||
fees,
|
pool: newPool,
|
||||||
newP,
|
p: newP,
|
||||||
} =
|
totalShares: newTotalShares,
|
||||||
outcomeType === 'BINARY'
|
totalBets: newTotalBets,
|
||||||
? mechanism === 'dpm-2'
|
totalLiquidity: newTotalLiquidity,
|
||||||
? getNewBinaryDpmBetInfo(
|
collectedFees: addObjects(newBet.fees, collectedFees),
|
||||||
user,
|
volume: volume + amount,
|
||||||
outcome as 'YES' | 'NO',
|
})
|
||||||
amount,
|
)
|
||||||
contract,
|
|
||||||
loanAmount,
|
|
||||||
newBetDoc.id
|
|
||||||
)
|
|
||||||
: (getNewBinaryCpmmBetInfo(
|
|
||||||
user,
|
|
||||||
outcome as 'YES' | 'NO',
|
|
||||||
amount,
|
|
||||||
contract,
|
|
||||||
loanAmount,
|
|
||||||
newBetDoc.id
|
|
||||||
) as any)
|
|
||||||
: getNewMultiBetInfo(
|
|
||||||
user,
|
|
||||||
outcome,
|
|
||||||
amount,
|
|
||||||
contract as any,
|
|
||||||
loanAmount,
|
|
||||||
newBetDoc.id
|
|
||||||
)
|
|
||||||
|
|
||||||
if (newP !== undefined && !isFinite(newP)) {
|
return { betId: betDoc.id }
|
||||||
throw new APIError(400, 'Trade rejected due to overflow error.')
|
})
|
||||||
}
|
|
||||||
|
|
||||||
transaction.create(newBetDoc, newBet)
|
await redeemShares(bettor.id, contractId)
|
||||||
|
return result
|
||||||
transaction.update(
|
|
||||||
contractDoc,
|
|
||||||
removeUndefinedProps({
|
|
||||||
pool: newPool,
|
|
||||||
p: newP,
|
|
||||||
totalShares: newTotalShares,
|
|
||||||
totalBets: newTotalBets,
|
|
||||||
totalLiquidity: newTotalLiquidity,
|
|
||||||
collectedFees: addObjects<Fees>(fees ?? {}, collectedFees ?? {}),
|
|
||||||
volume: volume + Math.abs(amount),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!isFinite(newBalance)) {
|
|
||||||
throw new APIError(500, 'Invalid user balance for ' + user.username)
|
|
||||||
}
|
|
||||||
|
|
||||||
transaction.update(userDoc, { balance: newBalance })
|
|
||||||
|
|
||||||
return { betId: newBetDoc.id }
|
|
||||||
})
|
|
||||||
.then(async (result) => {
|
|
||||||
await redeemShares(bettor.id, contractId)
|
|
||||||
return result
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const firestore = admin.firestore()
|
const firestore = admin.firestore()
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
import * as _ from 'lodash'
|
import { partition, sumBy } from 'lodash'
|
||||||
|
|
||||||
import { Bet } from '../../common/bet'
|
import { Bet } from '../../common/bet'
|
||||||
import { getProbability } from '../../common/calculate'
|
import { getProbability } from '../../common/calculate'
|
||||||
|
@ -25,14 +25,14 @@ export const redeemShares = async (userId: string, contractId: string) => {
|
||||||
.where('userId', '==', userId)
|
.where('userId', '==', userId)
|
||||||
)
|
)
|
||||||
const bets = betsSnap.docs.map((doc) => doc.data() as Bet)
|
const bets = betsSnap.docs.map((doc) => doc.data() as Bet)
|
||||||
const [yesBets, noBets] = _.partition(bets, (b) => b.outcome === 'YES')
|
const [yesBets, noBets] = partition(bets, (b) => b.outcome === 'YES')
|
||||||
const yesShares = _.sumBy(yesBets, (b) => b.shares)
|
const yesShares = sumBy(yesBets, (b) => b.shares)
|
||||||
const noShares = _.sumBy(noBets, (b) => b.shares)
|
const noShares = sumBy(noBets, (b) => b.shares)
|
||||||
|
|
||||||
const amount = Math.min(yesShares, noShares)
|
const amount = Math.min(yesShares, noShares)
|
||||||
if (amount <= 0) return
|
if (amount <= 0) return
|
||||||
|
|
||||||
const prevLoanAmount = _.sumBy(bets, (bet) => bet.loanAmount ?? 0)
|
const prevLoanAmount = sumBy(bets, (bet) => bet.loanAmount ?? 0)
|
||||||
const loanPaid = Math.min(prevLoanAmount, amount)
|
const loanPaid = Math.min(prevLoanAmount, amount)
|
||||||
const netAmount = amount - loanPaid
|
const netAmount = amount - loanPaid
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
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 * as _ from 'lodash'
|
import { difference, uniq, mapValues, groupBy, sumBy } from 'lodash'
|
||||||
|
|
||||||
import { Contract } from '../../common/contract'
|
import { Contract } from '../../common/contract'
|
||||||
import { User } from '../../common/user'
|
import { User } from '../../common/user'
|
||||||
|
@ -22,6 +22,7 @@ export const resolveMarket = functions
|
||||||
async (
|
async (
|
||||||
data: {
|
data: {
|
||||||
outcome: string
|
outcome: string
|
||||||
|
value?: number
|
||||||
contractId: string
|
contractId: string
|
||||||
probabilityInt?: number
|
probabilityInt?: number
|
||||||
resolutions?: { [outcome: string]: number }
|
resolutions?: { [outcome: string]: number }
|
||||||
|
@ -31,7 +32,7 @@ export const resolveMarket = functions
|
||||||
const userId = context?.auth?.uid
|
const userId = context?.auth?.uid
|
||||||
if (!userId) return { status: 'error', message: 'Not authorized' }
|
if (!userId) return { status: 'error', message: 'Not authorized' }
|
||||||
|
|
||||||
const { outcome, contractId, probabilityInt, resolutions } = data
|
const { outcome, contractId, probabilityInt, resolutions, value } = data
|
||||||
|
|
||||||
const contractDoc = firestore.doc(`contracts/${contractId}`)
|
const contractDoc = firestore.doc(`contracts/${contractId}`)
|
||||||
const contractSnap = await contractDoc.get()
|
const contractSnap = await contractDoc.get()
|
||||||
|
@ -50,10 +51,16 @@ export const resolveMarket = functions
|
||||||
outcome !== 'CANCEL'
|
outcome !== 'CANCEL'
|
||||||
)
|
)
|
||||||
return { status: 'error', message: 'Invalid outcome' }
|
return { status: 'error', message: 'Invalid outcome' }
|
||||||
|
} else if (outcomeType === 'NUMERIC') {
|
||||||
|
if (isNaN(+outcome) && outcome !== 'CANCEL')
|
||||||
|
return { status: 'error', message: 'Invalid outcome' }
|
||||||
} else {
|
} else {
|
||||||
return { status: 'error', message: 'Invalid contract outcomeType' }
|
return { status: 'error', message: 'Invalid contract outcomeType' }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (value !== undefined && !isFinite(value))
|
||||||
|
return { status: 'error', message: 'Invalid value' }
|
||||||
|
|
||||||
if (
|
if (
|
||||||
outcomeType === 'BINARY' &&
|
outcomeType === 'BINARY' &&
|
||||||
probabilityInt !== undefined &&
|
probabilityInt !== undefined &&
|
||||||
|
@ -108,6 +115,7 @@ export const resolveMarket = functions
|
||||||
removeUndefinedProps({
|
removeUndefinedProps({
|
||||||
isResolved: true,
|
isResolved: true,
|
||||||
resolution: outcome,
|
resolution: outcome,
|
||||||
|
resolutionValue: value,
|
||||||
resolutionTime,
|
resolutionTime,
|
||||||
closeTime: newCloseTime,
|
closeTime: newCloseTime,
|
||||||
resolutionProbability,
|
resolutionProbability,
|
||||||
|
@ -179,13 +187,13 @@ const sendResolutionEmails = async (
|
||||||
resolutionProbability?: number,
|
resolutionProbability?: number,
|
||||||
resolutions?: { [outcome: string]: number }
|
resolutions?: { [outcome: string]: number }
|
||||||
) => {
|
) => {
|
||||||
const nonWinners = _.difference(
|
const nonWinners = difference(
|
||||||
_.uniq(openBets.map(({ userId }) => userId)),
|
uniq(openBets.map(({ userId }) => userId)),
|
||||||
Object.keys(userPayouts)
|
Object.keys(userPayouts)
|
||||||
)
|
)
|
||||||
const investedByUser = _.mapValues(
|
const investedByUser = mapValues(
|
||||||
_.groupBy(openBets, (bet) => bet.userId),
|
groupBy(openBets, (bet) => bet.userId),
|
||||||
(bets) => _.sumBy(bets, (bet) => bet.amount)
|
(bets) => sumBy(bets, (bet) => bet.amount)
|
||||||
)
|
)
|
||||||
const emailPayouts = [
|
const emailPayouts = [
|
||||||
...Object.entries(userPayouts),
|
...Object.entries(userPayouts),
|
||||||
|
|
25
functions/src/scripts/backfill-fees.ts
Normal file
25
functions/src/scripts/backfill-fees.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
// We have many old contracts without a collectedFees data structure. Let's fill them in.
|
||||||
|
|
||||||
|
import * as admin from 'firebase-admin'
|
||||||
|
import { initAdmin } from './script-init'
|
||||||
|
import { noFees } from '../../../common/fees'
|
||||||
|
|
||||||
|
initAdmin()
|
||||||
|
const firestore = admin.firestore()
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
const contractsRef = firestore.collection('contracts')
|
||||||
|
contractsRef.get().then((contractsSnaps) => {
|
||||||
|
let n = 0
|
||||||
|
console.log(`Loaded ${contractsSnaps.size} contracts.`)
|
||||||
|
contractsSnaps.forEach((ct) => {
|
||||||
|
const data = ct.data()
|
||||||
|
if (!('collectedFees' in data)) {
|
||||||
|
n += 1
|
||||||
|
console.log(`Filling in missing fees on contract ${data.id}...`)
|
||||||
|
ct.ref.update({ collectedFees: noFees })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
console.log(`Updated ${n} contracts.`)
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,5 +1,4 @@
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
import * as _ from 'lodash'
|
|
||||||
|
|
||||||
import { initAdmin } from './script-init'
|
import { initAdmin } from './script-init'
|
||||||
initAdmin()
|
initAdmin()
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
import * as _ from 'lodash'
|
|
||||||
|
|
||||||
import { initAdmin } from './script-init'
|
import { initAdmin } from './script-init'
|
||||||
initAdmin()
|
initAdmin()
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
import * as _ from 'lodash'
|
import { sortBy } from 'lodash'
|
||||||
|
|
||||||
import { initAdmin } from './script-init'
|
import { initAdmin } from './script-init'
|
||||||
initAdmin()
|
initAdmin()
|
||||||
|
@ -20,7 +20,7 @@ async function migrateContract(
|
||||||
.get()
|
.get()
|
||||||
.then((snap) => snap.docs.map((bet) => bet.data() as Bet))
|
.then((snap) => snap.docs.map((bet) => bet.data() as Bet))
|
||||||
|
|
||||||
const lastBet = _.sortBy(bets, (bet) => -bet.createdTime)[0]
|
const lastBet = sortBy(bets, (bet) => -bet.createdTime)[0]
|
||||||
if (lastBet) {
|
if (lastBet) {
|
||||||
const probAfter = getDpmProbability(contract.totalShares)
|
const probAfter = getDpmProbability(contract.totalShares)
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
import * as _ from 'lodash'
|
|
||||||
|
|
||||||
import { initAdmin } from './script-init'
|
import { initAdmin } from './script-init'
|
||||||
initAdmin()
|
initAdmin()
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
import * as _ from 'lodash'
|
|
||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
|
|
||||||
import { initAdmin } from './script-init'
|
import { initAdmin } from './script-init'
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
import * as _ from 'lodash'
|
import { uniq } from 'lodash'
|
||||||
|
|
||||||
import { initAdmin } from './script-init'
|
import { initAdmin } from './script-init'
|
||||||
initAdmin()
|
initAdmin()
|
||||||
|
@ -19,7 +19,7 @@ async function lowercaseFoldTags() {
|
||||||
const foldRef = firestore.doc(`folds/${fold.id}`)
|
const foldRef = firestore.doc(`folds/${fold.id}`)
|
||||||
|
|
||||||
const { tags } = fold
|
const { tags } = fold
|
||||||
const lowercaseTags = _.uniq(tags.map((tag) => tag.toLowerCase()))
|
const lowercaseTags = uniq(tags.map((tag) => tag.toLowerCase()))
|
||||||
|
|
||||||
console.log('Adding lowercase tags', fold.slug, lowercaseTags)
|
console.log('Adding lowercase tags', fold.slug, lowercaseTags)
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
import * as _ from 'lodash'
|
|
||||||
|
|
||||||
import { initAdmin } from './script-init'
|
import { initAdmin } from './script-init'
|
||||||
initAdmin()
|
initAdmin()
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
import * as _ from 'lodash'
|
import { sumBy } from 'lodash'
|
||||||
|
|
||||||
import { initAdmin } from './script-init'
|
import { initAdmin } from './script-init'
|
||||||
initAdmin()
|
initAdmin()
|
||||||
|
@ -25,8 +25,8 @@ async function migrateContract(contractRef: DocRef, contract: Contract) {
|
||||||
.then((snap) => snap.docs.map((bet) => bet.data() as Bet))
|
.then((snap) => snap.docs.map((bet) => bet.data() as Bet))
|
||||||
|
|
||||||
const totalShares = {
|
const totalShares = {
|
||||||
YES: _.sumBy(bets, (bet) => (bet.outcome === 'YES' ? bet.shares : 0)),
|
YES: sumBy(bets, (bet) => (bet.outcome === 'YES' ? bet.shares : 0)),
|
||||||
NO: _.sumBy(bets, (bet) => (bet.outcome === 'NO' ? bet.shares : 0)),
|
NO: sumBy(bets, (bet) => (bet.outcome === 'NO' ? bet.shares : 0)),
|
||||||
}
|
}
|
||||||
|
|
||||||
await contractRef.update({ totalShares })
|
await contractRef.update({ totalShares })
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
import * as _ from 'lodash'
|
import { sortBy } from 'lodash'
|
||||||
|
|
||||||
import { initAdmin } from './script-init'
|
import { initAdmin } from './script-init'
|
||||||
initAdmin()
|
initAdmin()
|
||||||
|
@ -48,7 +48,7 @@ async function recalculateContract(contractRef: DocRef, isCommit = false) {
|
||||||
|
|
||||||
const betsRef = contractRef.collection('bets')
|
const betsRef = contractRef.collection('bets')
|
||||||
const betDocs = await transaction.get(betsRef)
|
const betDocs = await transaction.get(betsRef)
|
||||||
const bets = _.sortBy(
|
const bets = sortBy(
|
||||||
betDocs.docs.map((d) => d.data() as Bet),
|
betDocs.docs.map((d) => d.data() as Bet),
|
||||||
(b) => b.createdTime
|
(b) => b.createdTime
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
import * as _ from 'lodash'
|
import { sortBy, sumBy } from 'lodash'
|
||||||
|
|
||||||
import { initAdmin } from './script-init'
|
import { initAdmin } from './script-init'
|
||||||
initAdmin()
|
initAdmin()
|
||||||
|
@ -35,7 +35,7 @@ async function recalculateContract(
|
||||||
const contract = contractDoc.data() as FullContract<DPM, Binary>
|
const contract = contractDoc.data() as FullContract<DPM, Binary>
|
||||||
|
|
||||||
const betDocs = await transaction.get(contractRef.collection('bets'))
|
const betDocs = await transaction.get(contractRef.collection('bets'))
|
||||||
const bets = _.sortBy(
|
const bets = sortBy(
|
||||||
betDocs.docs.map((d) => d.data() as Bet),
|
betDocs.docs.map((d) => d.data() as Bet),
|
||||||
(b) => b.createdTime
|
(b) => b.createdTime
|
||||||
)
|
)
|
||||||
|
@ -43,8 +43,8 @@ async function recalculateContract(
|
||||||
const phantomAnte = startPool.YES + startPool.NO
|
const phantomAnte = startPool.YES + startPool.NO
|
||||||
|
|
||||||
const leftovers =
|
const leftovers =
|
||||||
_.sumBy(bets, (b) => b.amount) -
|
sumBy(bets, (b) => b.amount) -
|
||||||
_.sumBy(bets, (b) => {
|
sumBy(bets, (b) => {
|
||||||
if (!b.sale) return b.amount
|
if (!b.sale) return b.amount
|
||||||
const soldBet = bets.find((bet) => bet.id === b.sale?.betId)
|
const soldBet = bets.find((bet) => bet.id === b.sale?.betId)
|
||||||
return soldBet?.amount || 0
|
return soldBet?.amount || 0
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
import * as _ from 'lodash'
|
import { flatten, groupBy, sumBy, mapValues } from 'lodash'
|
||||||
|
|
||||||
import { initAdmin } from './script-init'
|
import { initAdmin } from './script-init'
|
||||||
initAdmin()
|
initAdmin()
|
||||||
|
@ -35,12 +35,12 @@ async function checkIfPayOutAgain(contractRef: DocRef, contract: Contract) {
|
||||||
)
|
)
|
||||||
|
|
||||||
const loanPayouts = getLoanPayouts(openBets)
|
const loanPayouts = getLoanPayouts(openBets)
|
||||||
const groups = _.groupBy(
|
const groups = groupBy(
|
||||||
[...payouts, ...loanPayouts],
|
[...payouts, ...loanPayouts],
|
||||||
(payout) => payout.userId
|
(payout) => payout.userId
|
||||||
)
|
)
|
||||||
const userPayouts = _.mapValues(groups, (group) =>
|
const userPayouts = mapValues(groups, (group) =>
|
||||||
_.sumBy(group, (g) => g.payout)
|
sumBy(group, (g) => g.payout)
|
||||||
)
|
)
|
||||||
|
|
||||||
const entries = Object.entries(userPayouts)
|
const entries = Object.entries(userPayouts)
|
||||||
|
@ -93,7 +93,7 @@ async function payOutContractAgain() {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
const flattened = _.flatten(toPayOutAgain.map((d) => d.toBePaidOut))
|
const flattened = flatten(toPayOutAgain.map((d) => d.toBePaidOut))
|
||||||
|
|
||||||
for (const [userId, payout] of flattened) {
|
for (const [userId, payout] of flattened) {
|
||||||
console.log('Paying out', userId, payout)
|
console.log('Paying out', userId, payout)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
import * as _ from 'lodash'
|
import { sumBy } from 'lodash'
|
||||||
|
|
||||||
import { initAdmin } from './script-init'
|
import { initAdmin } from './script-init'
|
||||||
initAdmin()
|
initAdmin()
|
||||||
|
@ -20,13 +20,13 @@ async function recalculateContract(contractRef: DocRef, contract: Contract) {
|
||||||
const openBets = bets.filter((b) => !b.isSold && !b.sale)
|
const openBets = bets.filter((b) => !b.isSold && !b.sale)
|
||||||
|
|
||||||
const totalShares = {
|
const totalShares = {
|
||||||
YES: _.sumBy(openBets, (bet) => (bet.outcome === 'YES' ? bet.shares : 0)),
|
YES: sumBy(openBets, (bet) => (bet.outcome === 'YES' ? bet.shares : 0)),
|
||||||
NO: _.sumBy(openBets, (bet) => (bet.outcome === 'NO' ? bet.shares : 0)),
|
NO: sumBy(openBets, (bet) => (bet.outcome === 'NO' ? bet.shares : 0)),
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalBets = {
|
const totalBets = {
|
||||||
YES: _.sumBy(openBets, (bet) => (bet.outcome === 'YES' ? bet.amount : 0)),
|
YES: sumBy(openBets, (bet) => (bet.outcome === 'YES' ? bet.amount : 0)),
|
||||||
NO: _.sumBy(openBets, (bet) => (bet.outcome === 'NO' ? bet.amount : 0)),
|
NO: sumBy(openBets, (bet) => (bet.outcome === 'NO' ? bet.amount : 0)),
|
||||||
}
|
}
|
||||||
|
|
||||||
await contractRef.update({ totalShares, totalBets })
|
await contractRef.update({ totalShares, totalBets })
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
import * as _ from 'lodash'
|
|
||||||
|
|
||||||
import { initAdmin } from './script-init'
|
import { initAdmin } from './script-init'
|
||||||
initAdmin()
|
initAdmin()
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
import * as _ from 'lodash'
|
|
||||||
|
|
||||||
import { initAdmin } from './script-init'
|
import { initAdmin } from './script-init'
|
||||||
initAdmin()
|
initAdmin()
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
import * as _ from 'lodash'
|
import { uniq } from 'lodash'
|
||||||
|
|
||||||
import { initAdmin } from './script-init'
|
import { initAdmin } from './script-init'
|
||||||
initAdmin()
|
initAdmin()
|
||||||
|
@ -19,7 +19,7 @@ async function updateContractTags() {
|
||||||
for (const contract of contracts) {
|
for (const contract of contracts) {
|
||||||
const contractRef = firestore.doc(`contracts/${contract.id}`)
|
const contractRef = firestore.doc(`contracts/${contract.id}`)
|
||||||
|
|
||||||
const tags = _.uniq([
|
const tags = uniq([
|
||||||
...parseTags(contract.question + contract.description),
|
...parseTags(contract.question + contract.description),
|
||||||
...(contract.tags ?? []),
|
...(contract.tags ?? []),
|
||||||
])
|
])
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
import * as _ from 'lodash'
|
|
||||||
|
|
||||||
import { initAdmin } from './script-init'
|
import { initAdmin } from './script-init'
|
||||||
initAdmin()
|
initAdmin()
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
import * as _ from 'lodash'
|
|
||||||
|
|
||||||
import { initAdmin } from './script-init'
|
import { initAdmin } from './script-init'
|
||||||
initAdmin()
|
initAdmin()
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import * as _ from 'lodash'
|
import { partition, sumBy } from 'lodash'
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
import * as functions from 'firebase-functions'
|
import * as functions from 'firebase-functions'
|
||||||
|
|
||||||
|
@ -51,15 +51,15 @@ export const sellShares = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
contractDoc.collection('bets').where('userId', '==', userId)
|
contractDoc.collection('bets').where('userId', '==', userId)
|
||||||
)
|
)
|
||||||
|
|
||||||
const prevLoanAmount = _.sumBy(userBets, (bet) => bet.loanAmount ?? 0)
|
const prevLoanAmount = sumBy(userBets, (bet) => bet.loanAmount ?? 0)
|
||||||
|
|
||||||
const [yesBets, noBets] = _.partition(
|
const [yesBets, noBets] = partition(
|
||||||
userBets ?? [],
|
userBets ?? [],
|
||||||
(bet) => bet.outcome === 'YES'
|
(bet) => bet.outcome === 'YES'
|
||||||
)
|
)
|
||||||
const [yesShares, noShares] = [
|
const [yesShares, noShares] = [
|
||||||
_.sumBy(yesBets, (bet) => bet.shares),
|
sumBy(yesBets, (bet) => bet.shares),
|
||||||
_.sumBy(noBets, (bet) => bet.shares),
|
sumBy(noBets, (bet) => bet.shares),
|
||||||
]
|
]
|
||||||
|
|
||||||
const maxShares = outcome === 'YES' ? yesShares : noShares
|
const maxShares = outcome === 'YES' ? yesShares : noShares
|
||||||
|
|
|
@ -5,11 +5,13 @@ import Stripe from 'stripe'
|
||||||
import { getPrivateUser, getUser, isProd, payUser } from './utils'
|
import { getPrivateUser, getUser, isProd, payUser } from './utils'
|
||||||
import { sendThankYouEmail } from './emails'
|
import { sendThankYouEmail } from './emails'
|
||||||
|
|
||||||
|
export type StripeSession = Stripe.Event.Data.Object & { id: any, metadata: any}
|
||||||
|
|
||||||
export type StripeTransaction = {
|
export type StripeTransaction = {
|
||||||
userId: string
|
userId: string
|
||||||
manticDollarQuantity: number
|
manticDollarQuantity: number
|
||||||
sessionId: string
|
sessionId: string
|
||||||
session: any
|
session: StripeSession
|
||||||
timestamp: number
|
timestamp: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,14 +98,14 @@ export const stripeWebhook = functions
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.type === 'checkout.session.completed') {
|
if (event.type === 'checkout.session.completed') {
|
||||||
const session = event.data.object as any
|
const session = event.data.object as StripeSession
|
||||||
await issueMoneys(session)
|
await issueMoneys(session)
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(200).send('success')
|
res.status(200).send('success')
|
||||||
})
|
})
|
||||||
|
|
||||||
const issueMoneys = async (session: any) => {
|
const issueMoneys = async (session: StripeSession) => {
|
||||||
const { id: sessionId } = session
|
const { id: sessionId } = session
|
||||||
|
|
||||||
const query = await firestore
|
const query = await firestore
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
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 * as _ from 'lodash'
|
|
||||||
import { getUser } from './utils'
|
import { getUser } from './utils'
|
||||||
import { PrivateUser } from '../../common/user'
|
import { PrivateUser } from '../../common/user'
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
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 * as _ from 'lodash'
|
import { sumBy } from 'lodash'
|
||||||
|
|
||||||
import { getValues } from './utils'
|
import { getValues } from './utils'
|
||||||
import { Contract } from '../../common/contract'
|
import { Contract } from '../../common/contract'
|
||||||
|
@ -39,5 +39,5 @@ const computeVolumeFrom = async (contract: Contract, timeAgoMs: number) => {
|
||||||
.where('createdTime', '>', Date.now() - timeAgoMs)
|
.where('createdTime', '>', Date.now() - timeAgoMs)
|
||||||
)
|
)
|
||||||
|
|
||||||
return _.sumBy(bets, (bet) => (bet.isRedemption ? 0 : Math.abs(bet.amount)))
|
return sumBy(bets, (bet) => (bet.isRedemption ? 0 : Math.abs(bet.amount)))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import * as _ from 'lodash'
|
|
||||||
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 { shuffle, sortBy } from 'lodash'
|
||||||
|
|
||||||
import { getValue, getValues } from './utils'
|
import { getValue, getValues } from './utils'
|
||||||
import { Contract } from '../../common/contract'
|
import { Contract } from '../../common/contract'
|
||||||
|
@ -30,7 +30,7 @@ const BATCH_SIZE = 30
|
||||||
const MAX_BATCHES = 50
|
const MAX_BATCHES = 50
|
||||||
|
|
||||||
const getUserBatches = async () => {
|
const getUserBatches = async () => {
|
||||||
const users = _.shuffle(await getValues<User>(firestore.collection('users')))
|
const users = shuffle(await getValues<User>(firestore.collection('users')))
|
||||||
let userBatches: User[][] = []
|
let userBatches: User[][] = []
|
||||||
for (let i = 0; i < users.length; i += BATCH_SIZE) {
|
for (let i = 0; i < users.length; i += BATCH_SIZE) {
|
||||||
userBatches.push(users.slice(i, i + BATCH_SIZE))
|
userBatches.push(users.slice(i, i + BATCH_SIZE))
|
||||||
|
@ -42,7 +42,7 @@ const getUserBatches = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const updateFeed = functions.pubsub
|
export const updateFeed = functions.pubsub
|
||||||
.schedule('every 15 minutes')
|
.schedule('every 60 minutes')
|
||||||
.onRun(async () => {
|
.onRun(async () => {
|
||||||
const userBatches = await getUserBatches()
|
const userBatches = await getUserBatches()
|
||||||
|
|
||||||
|
@ -128,7 +128,7 @@ export const computeFeed = async (user: User, contracts: Contract[]) => {
|
||||||
return [contract, score] as [Contract, number]
|
return [contract, score] as [Contract, number]
|
||||||
})
|
})
|
||||||
|
|
||||||
const sortedContracts = _.sortBy(
|
const sortedContracts = sortBy(
|
||||||
scoredContracts,
|
scoredContracts,
|
||||||
([_, score]) => score
|
([_, score]) => score
|
||||||
).reverse()
|
).reverse()
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
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 * as _ from 'lodash'
|
|
||||||
|
|
||||||
import { getValue, getValues } from './utils'
|
import { getValue, getValues } from './utils'
|
||||||
import { Contract } from '../../common/contract'
|
import { Contract } from '../../common/contract'
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
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 * as _ from 'lodash'
|
import { sum, sumBy } from 'lodash'
|
||||||
|
|
||||||
import { getValues } from './utils'
|
import { getValues } from './utils'
|
||||||
import { Contract } from '../../common/contract'
|
import { Contract } from '../../common/contract'
|
||||||
|
@ -19,7 +19,7 @@ export const updateUserMetrics = functions.pubsub
|
||||||
getValues<Contract>(firestore.collection('contracts')),
|
getValues<Contract>(firestore.collection('contracts')),
|
||||||
])
|
])
|
||||||
|
|
||||||
const contractsDict = _.fromPairs(
|
const contractsDict = Object.fromEntries(
|
||||||
contracts.map((contract) => [contract.id, contract])
|
contracts.map((contract) => [contract.id, contract])
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -43,12 +43,12 @@ export const updateUserMetrics = functions.pubsub
|
||||||
|
|
||||||
const computeInvestmentValue = async (
|
const computeInvestmentValue = async (
|
||||||
user: User,
|
user: User,
|
||||||
contractsDict: _.Dictionary<Contract>
|
contractsDict: { [k: string]: Contract }
|
||||||
) => {
|
) => {
|
||||||
const query = firestore.collectionGroup('bets').where('userId', '==', user.id)
|
const query = firestore.collectionGroup('bets').where('userId', '==', user.id)
|
||||||
const bets = await getValues<Bet>(query)
|
const bets = await getValues<Bet>(query)
|
||||||
|
|
||||||
return _.sumBy(bets, (bet) => {
|
return sumBy(bets, (bet) => {
|
||||||
const contract = contractsDict[bet.contractId]
|
const contract = contractsDict[bet.contractId]
|
||||||
if (!contract || contract.isResolved) return 0
|
if (!contract || contract.isResolved) return 0
|
||||||
if (bet.sale || bet.isSold) return 0
|
if (bet.sale || bet.isSold) return 0
|
||||||
|
@ -60,20 +60,20 @@ const computeInvestmentValue = async (
|
||||||
|
|
||||||
const computeTotalPool = async (
|
const computeTotalPool = async (
|
||||||
user: User,
|
user: User,
|
||||||
contractsDict: _.Dictionary<Contract>
|
contractsDict: { [k: string]: Contract }
|
||||||
) => {
|
) => {
|
||||||
const creatorContracts = Object.values(contractsDict).filter(
|
const creatorContracts = Object.values(contractsDict).filter(
|
||||||
(contract) => contract.creatorId === user.id
|
(contract) => contract.creatorId === user.id
|
||||||
)
|
)
|
||||||
const pools = creatorContracts.map((contract) =>
|
const pools = creatorContracts.map((contract) =>
|
||||||
_.sum(Object.values(contract.pool))
|
sum(Object.values(contract.pool))
|
||||||
)
|
)
|
||||||
return _.sum(pools)
|
return sum(pools)
|
||||||
}
|
}
|
||||||
|
|
||||||
const computeVolume = async (contract: Contract) => {
|
const computeVolume = async (contract: Contract) => {
|
||||||
const bets = await getValues<Bet>(
|
const bets = await getValues<Bet>(
|
||||||
firestore.collection(`contracts/${contract.id}/bets`)
|
firestore.collection(`contracts/${contract.id}/bets`)
|
||||||
)
|
)
|
||||||
return _.sumBy(bets, (bet) => Math.abs(bet.amount))
|
return sumBy(bets, (bet) => Math.abs(bet.amount))
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,5 +8,12 @@
|
||||||
],
|
],
|
||||||
"scripts": {},
|
"scripts": {},
|
||||||
"dependencies": {},
|
"dependencies": {},
|
||||||
"devDependencies": {}
|
"devDependencies": {
|
||||||
|
"@typescript-eslint/eslint-plugin": "5.25.0",
|
||||||
|
"@typescript-eslint/parser": "5.25.0",
|
||||||
|
"eslint": "8.15.0",
|
||||||
|
"eslint-plugin-lodash": "^7.4.0",
|
||||||
|
"prettier": "2.5.0",
|
||||||
|
"typescript": "4.6.4"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,21 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
parser: '@typescript-eslint/parser',
|
parser: '@typescript-eslint/parser',
|
||||||
extends: ['plugin:react-hooks/recommended', 'plugin:@next/next/recommended'],
|
plugins: ['lodash'],
|
||||||
|
extends: [
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:react-hooks/recommended',
|
||||||
|
'plugin:@next/next/recommended',
|
||||||
|
],
|
||||||
rules: {
|
rules: {
|
||||||
// Add or disable rules here.
|
'@typescript-eslint/no-empty-function': 'off',
|
||||||
|
'@typescript-eslint/no-unused-vars': 'off',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
'@next/next/no-img-element': 'off',
|
'@next/next/no-img-element': 'off',
|
||||||
'@next/next/no-typos': 'off',
|
'@next/next/no-typos': 'off',
|
||||||
|
'lodash/import-scope': [2, 'member'],
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
node: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
. "$(dirname "$0")/_/husky.sh"
|
|
||||||
|
|
||||||
npx pretty-quick --staged
|
|
||||||
# Disable tsc lint for now, cuz it's been annoying
|
|
||||||
# cd web
|
|
||||||
# npx lint-staged
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { ReactNode } from 'react'
|
||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
|
|
||||||
export type OgCardProps = {
|
export type OgCardProps = {
|
||||||
|
@ -35,7 +36,7 @@ export function SEO(props: {
|
||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
url?: string
|
url?: string
|
||||||
children?: any[]
|
children?: ReactNode
|
||||||
ogCardProps?: OgCardProps
|
ogCardProps?: OgCardProps
|
||||||
}) {
|
}) {
|
||||||
const { title, description, url, children, ogCardProps } = props
|
const { title, description, url, children, ogCardProps } = props
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { useState } from 'react'
|
||||||
import { Contract } from 'common/contract'
|
import { Contract } from 'common/contract'
|
||||||
import { formatMoney } from 'common/util/format'
|
import { formatMoney } from 'common/util/format'
|
||||||
import { useUser } from 'web/hooks/use-user'
|
import { useUser } from 'web/hooks/use-user'
|
||||||
import { addLiquidity } from 'web/lib/firebase/api-call'
|
import { addLiquidity } from 'web/lib/firebase/fn-call'
|
||||||
import { AmountInput } from './amount-input'
|
import { AmountInput } from './amount-input'
|
||||||
import { Row } from './layout/row'
|
import { Row } from './layout/row'
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { useState } from 'react'
|
import { useState, ReactNode } from 'react'
|
||||||
|
|
||||||
export function AdvancedPanel(props: { children: any }) {
|
export function AdvancedPanel(props: { children: ReactNode }) {
|
||||||
const { children } = props
|
const { children } = props
|
||||||
const [collapsed, setCollapsed] = useState(true)
|
const [collapsed, setCollapsed] = useState(true)
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
|
import React from 'react'
|
||||||
import { useUser } from 'web/hooks/use-user'
|
import { useUser } from 'web/hooks/use-user'
|
||||||
import { formatMoney } from 'common/util/format'
|
import { formatMoney } from 'common/util/format'
|
||||||
import { Col } from './layout/col'
|
import { Col } from './layout/col'
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { ResponsiveLine } from '@nivo/line'
|
import { Point, ResponsiveLine } from '@nivo/line'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import _ from 'lodash'
|
import { zip } from 'lodash'
|
||||||
import { useWindowSize } from 'web/hooks/use-window-size'
|
import { useWindowSize } from 'web/hooks/use-window-size'
|
||||||
|
import { Col } from '../layout/col'
|
||||||
|
|
||||||
export function DailyCountChart(props: {
|
export function DailyCountChart(props: {
|
||||||
startDate: number
|
startDate: number
|
||||||
|
@ -15,7 +16,7 @@ export function DailyCountChart(props: {
|
||||||
dayjs(startDate).add(i, 'day').toDate()
|
dayjs(startDate).add(i, 'day').toDate()
|
||||||
)
|
)
|
||||||
|
|
||||||
const points = _.zip(dates, dailyCounts).map(([date, betCount]) => ({
|
const points = zip(dates, dailyCounts).map(([date, betCount]) => ({
|
||||||
x: date,
|
x: date,
|
||||||
y: betCount,
|
y: betCount,
|
||||||
}))
|
}))
|
||||||
|
@ -46,6 +47,10 @@ export function DailyCountChart(props: {
|
||||||
enableGridX={!!width && width >= 800}
|
enableGridX={!!width && width >= 800}
|
||||||
enableArea
|
enableArea
|
||||||
margin={{ top: 20, right: 28, bottom: 22, left: 40 }}
|
margin={{ top: 20, right: 28, bottom: 22, left: 40 }}
|
||||||
|
sliceTooltip={({ slice }) => {
|
||||||
|
const point = slice.points[0]
|
||||||
|
return <Tooltip point={point} />
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -63,7 +68,7 @@ export function DailyPercentChart(props: {
|
||||||
dayjs(startDate).add(i, 'day').toDate()
|
dayjs(startDate).add(i, 'day').toDate()
|
||||||
)
|
)
|
||||||
|
|
||||||
const points = _.zip(dates, dailyPercent).map(([date, betCount]) => ({
|
const points = zip(dates, dailyPercent).map(([date, betCount]) => ({
|
||||||
x: date,
|
x: date,
|
||||||
y: betCount,
|
y: betCount,
|
||||||
}))
|
}))
|
||||||
|
@ -97,7 +102,28 @@ export function DailyPercentChart(props: {
|
||||||
enableGridX={!!width && width >= 800}
|
enableGridX={!!width && width >= 800}
|
||||||
enableArea
|
enableArea
|
||||||
margin={{ top: 20, right: 28, bottom: 22, left: 40 }}
|
margin={{ top: 20, right: 28, bottom: 22, left: 40 }}
|
||||||
|
sliceTooltip={({ slice }) => {
|
||||||
|
const point = slice.points[0]
|
||||||
|
return <Tooltip point={point} />
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Tooltip(props: { point: Point }) {
|
||||||
|
const { point } = props
|
||||||
|
return (
|
||||||
|
<Col className="border border-gray-300 bg-white py-2 px-3">
|
||||||
|
<div
|
||||||
|
className="pb-1"
|
||||||
|
style={{
|
||||||
|
color: point.serieColor,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<strong>{point.serieId}</strong> {point.data.yFormatted}
|
||||||
|
</div>
|
||||||
|
<div>{dayjs(point.data.x).format('MMM DD')}</div>
|
||||||
|
</Col>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { Answer } from 'common/answer'
|
||||||
import { DPM, FreeResponse, FullContract } from 'common/contract'
|
import { DPM, FreeResponse, FullContract } from 'common/contract'
|
||||||
import { BuyAmountInput } from '../amount-input'
|
import { BuyAmountInput } from '../amount-input'
|
||||||
import { Col } from '../layout/col'
|
import { Col } from '../layout/col'
|
||||||
import { placeBet } from 'web/lib/firebase/api-call'
|
import { APIError, placeBet } from 'web/lib/firebase/api-call'
|
||||||
import { Row } from '../layout/row'
|
import { Row } from '../layout/row'
|
||||||
import { Spacer } from '../layout/spacer'
|
import { Spacer } from '../layout/spacer'
|
||||||
import {
|
import {
|
||||||
|
@ -52,22 +52,26 @@ export function AnswerBetPanel(props: {
|
||||||
setError(undefined)
|
setError(undefined)
|
||||||
setIsSubmitting(true)
|
setIsSubmitting(true)
|
||||||
|
|
||||||
const result = await placeBet({
|
placeBet({
|
||||||
amount: betAmount,
|
amount: betAmount,
|
||||||
outcome: answerId,
|
outcome: answerId,
|
||||||
contractId: contract.id,
|
contractId: contract.id,
|
||||||
}).then((r) => r.data as any)
|
})
|
||||||
|
.then((r) => {
|
||||||
console.log('placed bet. Result:', result)
|
console.log('placed bet. Result:', r)
|
||||||
|
setIsSubmitting(false)
|
||||||
if (result?.status === 'success') {
|
setBetAmount(undefined)
|
||||||
setIsSubmitting(false)
|
props.closePanel()
|
||||||
setBetAmount(undefined)
|
})
|
||||||
props.closePanel()
|
.catch((e) => {
|
||||||
} else {
|
if (e instanceof APIError) {
|
||||||
setError(result?.message || 'Error placing bet')
|
setError(e.toString())
|
||||||
setIsSubmitting(false)
|
} else {
|
||||||
}
|
console.error(e)
|
||||||
|
setError('Error placing bet')
|
||||||
|
}
|
||||||
|
setIsSubmitting(false)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const betDisabled = isSubmitting || !betAmount || error
|
const betDisabled = isSubmitting || !betAmount || error
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import _ from 'lodash'
|
import { sum, mapValues } from 'lodash'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
|
||||||
import { DPM, FreeResponse, FullContract } from 'common/contract'
|
import { DPM, FreeResponse, FullContract } from 'common/contract'
|
||||||
import { Col } from '../layout/col'
|
import { Col } from '../layout/col'
|
||||||
import { resolveMarket } from 'web/lib/firebase/api-call'
|
import { resolveMarket } from 'web/lib/firebase/fn-call'
|
||||||
import { Row } from '../layout/row'
|
import { Row } from '../layout/row'
|
||||||
import { ChooseCancelSelector } from '../yes-no-selector'
|
import { ChooseCancelSelector } from '../yes-no-selector'
|
||||||
import { ResolveConfirmationButton } from '../confirmation-button'
|
import { ResolveConfirmationButton } from '../confirmation-button'
|
||||||
|
@ -30,8 +30,8 @@ export function AnswerResolvePanel(props: {
|
||||||
|
|
||||||
setIsSubmitting(true)
|
setIsSubmitting(true)
|
||||||
|
|
||||||
const totalProb = _.sum(Object.values(chosenAnswers))
|
const totalProb = sum(Object.values(chosenAnswers))
|
||||||
const normalizedProbs = _.mapValues(
|
const normalizedProbs = mapValues(
|
||||||
chosenAnswers,
|
chosenAnswers,
|
||||||
(prob) => (100 * prob) / totalProb
|
(prob) => (100 * prob) / totalProb
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { DatumValue } from '@nivo/core'
|
import { DatumValue } from '@nivo/core'
|
||||||
import { ResponsiveLine } from '@nivo/line'
|
import { ResponsiveLine } from '@nivo/line'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import _ from 'lodash'
|
import { groupBy, sortBy, sumBy } from 'lodash'
|
||||||
import { memo } from 'react'
|
import { memo } from 'react'
|
||||||
|
|
||||||
import { Bet } from 'common/bet'
|
import { Bet } from 'common/bet'
|
||||||
|
@ -48,7 +48,7 @@ export const AnswersGraph = memo(function AnswersGraph(props: {
|
||||||
// to the right.
|
// to the right.
|
||||||
latestTime.add(1, 'month').valueOf()
|
latestTime.add(1, 'month').valueOf()
|
||||||
|
|
||||||
const times = _.sortBy([
|
const times = sortBy([
|
||||||
createdTime,
|
createdTime,
|
||||||
...bets.map((bet) => bet.createdTime),
|
...bets.map((bet) => bet.createdTime),
|
||||||
endTime,
|
endTime,
|
||||||
|
@ -167,7 +167,7 @@ const computeProbsByOutcome = (
|
||||||
) => {
|
) => {
|
||||||
const { totalBets } = contract
|
const { totalBets } = contract
|
||||||
|
|
||||||
const betsByOutcome = _.groupBy(bets, (bet) => bet.outcome)
|
const betsByOutcome = groupBy(bets, (bet) => bet.outcome)
|
||||||
const outcomes = Object.keys(betsByOutcome).filter((outcome) => {
|
const outcomes = Object.keys(betsByOutcome).filter((outcome) => {
|
||||||
const maxProb = Math.max(
|
const maxProb = Math.max(
|
||||||
...betsByOutcome[outcome].map((bet) => bet.probAfter)
|
...betsByOutcome[outcome].map((bet) => bet.probAfter)
|
||||||
|
@ -175,15 +175,15 @@ const computeProbsByOutcome = (
|
||||||
return outcome !== '0' && maxProb > 0.02 && totalBets[outcome] > 0.000000001
|
return outcome !== '0' && maxProb > 0.02 && totalBets[outcome] > 0.000000001
|
||||||
})
|
})
|
||||||
|
|
||||||
const trackedOutcomes = _.sortBy(
|
const trackedOutcomes = sortBy(
|
||||||
outcomes,
|
outcomes,
|
||||||
(outcome) => -1 * getOutcomeProbability(contract, outcome)
|
(outcome) => -1 * getOutcomeProbability(contract, outcome)
|
||||||
).slice(0, NUM_LINES)
|
).slice(0, NUM_LINES)
|
||||||
|
|
||||||
const probsByOutcome = _.fromPairs(
|
const probsByOutcome = Object.fromEntries(
|
||||||
trackedOutcomes.map((outcome) => [outcome, [] as number[]])
|
trackedOutcomes.map((outcome) => [outcome, [] as number[]])
|
||||||
)
|
)
|
||||||
const sharesByOutcome = _.fromPairs(
|
const sharesByOutcome = Object.fromEntries(
|
||||||
Object.keys(betsByOutcome).map((outcome) => [outcome, 0])
|
Object.keys(betsByOutcome).map((outcome) => [outcome, 0])
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -191,7 +191,7 @@ const computeProbsByOutcome = (
|
||||||
const { outcome, shares } = bet
|
const { outcome, shares } = bet
|
||||||
sharesByOutcome[outcome] += shares
|
sharesByOutcome[outcome] += shares
|
||||||
|
|
||||||
const sharesSquared = _.sumBy(
|
const sharesSquared = sumBy(
|
||||||
Object.values(sharesByOutcome).map((shares) => shares ** 2)
|
Object.values(sharesByOutcome).map((shares) => shares ** 2)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import _ from 'lodash'
|
import { sortBy, partition, sum, uniq } from 'lodash'
|
||||||
import React, { useLayoutEffect, useState } from 'react'
|
import { useLayoutEffect, useState } from 'react'
|
||||||
|
|
||||||
import { DPM, FreeResponse, FullContract } from 'common/contract'
|
import { DPM, FreeResponse, FullContract } from 'common/contract'
|
||||||
import { Col } from '../layout/col'
|
import { Col } from '../layout/col'
|
||||||
|
@ -32,7 +32,7 @@ export function AnswersPanel(props: {
|
||||||
const { creatorId, resolution, resolutions, totalBets } = contract
|
const { creatorId, resolution, resolutions, totalBets } = contract
|
||||||
|
|
||||||
const answers = useAnswers(contract.id) ?? contract.answers
|
const answers = useAnswers(contract.id) ?? contract.answers
|
||||||
const [winningAnswers, losingAnswers] = _.partition(
|
const [winningAnswers, losingAnswers] = partition(
|
||||||
answers.filter(
|
answers.filter(
|
||||||
(answer) => answer.id !== '0' && totalBets[answer.id] > 0.000000001
|
(answer) => answer.id !== '0' && totalBets[answer.id] > 0.000000001
|
||||||
),
|
),
|
||||||
|
@ -40,10 +40,10 @@ export function AnswersPanel(props: {
|
||||||
answer.id === resolution || (resolutions && resolutions[answer.id])
|
answer.id === resolution || (resolutions && resolutions[answer.id])
|
||||||
)
|
)
|
||||||
const sortedAnswers = [
|
const sortedAnswers = [
|
||||||
..._.sortBy(winningAnswers, (answer) =>
|
...sortBy(winningAnswers, (answer) =>
|
||||||
resolutions ? -1 * resolutions[answer.id] : 0
|
resolutions ? -1 * resolutions[answer.id] : 0
|
||||||
),
|
),
|
||||||
..._.sortBy(
|
...sortBy(
|
||||||
resolution ? [] : losingAnswers,
|
resolution ? [] : losingAnswers,
|
||||||
(answer) => -1 * getDpmOutcomeProbability(contract.totalShares, answer.id)
|
(answer) => -1 * getDpmOutcomeProbability(contract.totalShares, answer.id)
|
||||||
),
|
),
|
||||||
|
@ -58,7 +58,7 @@ export function AnswersPanel(props: {
|
||||||
[answerId: string]: number
|
[answerId: string]: number
|
||||||
}>({})
|
}>({})
|
||||||
|
|
||||||
const chosenTotal = _.sum(Object.values(chosenAnswers))
|
const chosenTotal = sum(Object.values(chosenAnswers))
|
||||||
|
|
||||||
const answerItems = getAnswerItems(
|
const answerItems = getAnswerItems(
|
||||||
contract,
|
contract,
|
||||||
|
@ -158,10 +158,10 @@ function getAnswerItems(
|
||||||
answers: Answer[],
|
answers: Answer[],
|
||||||
user: User | undefined | null
|
user: User | undefined | null
|
||||||
) {
|
) {
|
||||||
let outcomes = _.uniq(
|
let outcomes = uniq(answers.map((answer) => answer.number.toString())).filter(
|
||||||
answers.map((answer) => answer.number.toString())
|
(outcome) => getOutcomeProbability(contract, outcome) > 0.0001
|
||||||
).filter((outcome) => getOutcomeProbability(contract, outcome) > 0.0001)
|
)
|
||||||
outcomes = _.sortBy(outcomes, (outcome) =>
|
outcomes = sortBy(outcomes, (outcome) =>
|
||||||
getOutcomeProbability(contract, outcome)
|
getOutcomeProbability(contract, outcome)
|
||||||
).reverse()
|
).reverse()
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import Textarea from 'react-expanding-textarea'
|
||||||
import { DPM, FreeResponse, FullContract } from 'common/contract'
|
import { DPM, FreeResponse, FullContract } from 'common/contract'
|
||||||
import { BuyAmountInput } from '../amount-input'
|
import { BuyAmountInput } from '../amount-input'
|
||||||
import { Col } from '../layout/col'
|
import { Col } from '../layout/col'
|
||||||
import { createAnswer } from 'web/lib/firebase/api-call'
|
import { createAnswer } from 'web/lib/firebase/fn-call'
|
||||||
import { Row } from '../layout/row'
|
import { Row } from '../layout/row'
|
||||||
import {
|
import {
|
||||||
formatMoney,
|
formatMoney,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import Router from 'next/router'
|
import Router from 'next/router'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
|
import { MouseEvent } from 'react'
|
||||||
import { UserCircleIcon } from '@heroicons/react/solid'
|
import { UserCircleIcon } from '@heroicons/react/solid'
|
||||||
|
|
||||||
export function Avatar(props: {
|
export function Avatar(props: {
|
||||||
|
@ -15,7 +16,7 @@ export function Avatar(props: {
|
||||||
const onClick =
|
const onClick =
|
||||||
noLink && username
|
noLink && username
|
||||||
? undefined
|
? undefined
|
||||||
: (e: any) => {
|
: (e: MouseEvent) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
Router.push(`/${username}`)
|
Router.push(`/${username}`)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import _ from 'lodash'
|
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import { partition, sumBy } from 'lodash'
|
||||||
|
|
||||||
import { useUser } from 'web/hooks/use-user'
|
import { useUser } from 'web/hooks/use-user'
|
||||||
import { Binary, CPMM, DPM, FullContract } from 'common/contract'
|
import { Binary, CPMM, DPM, FullContract } from 'common/contract'
|
||||||
|
@ -14,9 +14,10 @@ import {
|
||||||
formatWithCommas,
|
formatWithCommas,
|
||||||
} from 'common/util/format'
|
} from 'common/util/format'
|
||||||
import { Title } from './title'
|
import { Title } from './title'
|
||||||
import { firebaseLogin, User } from 'web/lib/firebase/users'
|
import { User } from 'web/lib/firebase/users'
|
||||||
import { Bet } from 'common/bet'
|
import { Bet } from 'common/bet'
|
||||||
import { placeBet, sellShares } from 'web/lib/firebase/api-call'
|
import { APIError, placeBet } from 'web/lib/firebase/api-call'
|
||||||
|
import { sellShares } from 'web/lib/firebase/fn-call'
|
||||||
import { AmountInput, BuyAmountInput } from './amount-input'
|
import { AmountInput, BuyAmountInput } from './amount-input'
|
||||||
import { InfoTooltip } from './info-tooltip'
|
import { InfoTooltip } from './info-tooltip'
|
||||||
import { BinaryOutcomeLabel } from './outcome-label'
|
import { BinaryOutcomeLabel } from './outcome-label'
|
||||||
|
@ -35,6 +36,7 @@ import {
|
||||||
} from 'common/calculate-cpmm'
|
} from 'common/calculate-cpmm'
|
||||||
import { SellRow } from './sell-row'
|
import { SellRow } from './sell-row'
|
||||||
import { useSaveShares } from './use-save-shares'
|
import { useSaveShares } from './use-save-shares'
|
||||||
|
import { SignUpPrompt } from './sign-up-prompt'
|
||||||
|
|
||||||
export function BetPanel(props: {
|
export function BetPanel(props: {
|
||||||
contract: FullContract<DPM | CPMM, Binary>
|
contract: FullContract<DPM | CPMM, Binary>
|
||||||
|
@ -69,14 +71,7 @@ export function BetPanel(props: {
|
||||||
|
|
||||||
<BuyPanel contract={contract} user={user} />
|
<BuyPanel contract={contract} user={user} />
|
||||||
|
|
||||||
{user === null && (
|
<SignUpPrompt />
|
||||||
<button
|
|
||||||
className="btn flex-1 whitespace-nowrap border-none bg-gradient-to-r from-teal-500 to-green-500 px-10 text-lg font-medium normal-case hover:from-teal-600 hover:to-green-600"
|
|
||||||
onClick={firebaseLogin}
|
|
||||||
>
|
|
||||||
Sign up to bet!
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</Col>
|
</Col>
|
||||||
</Col>
|
</Col>
|
||||||
)
|
)
|
||||||
|
@ -182,14 +177,7 @@ export function BetPanelSwitcher(props: {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{user === null && (
|
<SignUpPrompt />
|
||||||
<button
|
|
||||||
className="btn flex-1 whitespace-nowrap border-none bg-gradient-to-r from-teal-500 to-green-500 px-10 text-lg font-medium normal-case hover:from-teal-600 hover:to-green-600"
|
|
||||||
onClick={firebaseLogin}
|
|
||||||
>
|
|
||||||
Sign up to bet!
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</Col>
|
</Col>
|
||||||
</Col>
|
</Col>
|
||||||
)
|
)
|
||||||
|
@ -240,23 +228,27 @@ function BuyPanel(props: {
|
||||||
setError(undefined)
|
setError(undefined)
|
||||||
setIsSubmitting(true)
|
setIsSubmitting(true)
|
||||||
|
|
||||||
const result = await placeBet({
|
placeBet({
|
||||||
amount: betAmount,
|
amount: betAmount,
|
||||||
outcome: betChoice,
|
outcome: betChoice,
|
||||||
contractId: contract.id,
|
contractId: contract.id,
|
||||||
}).then((r) => r.data as any)
|
})
|
||||||
|
.then((r) => {
|
||||||
console.log('placed bet. Result:', result)
|
console.log('placed bet. Result:', r)
|
||||||
|
setIsSubmitting(false)
|
||||||
if (result?.status === 'success') {
|
setWasSubmitted(true)
|
||||||
setIsSubmitting(false)
|
setBetAmount(undefined)
|
||||||
setWasSubmitted(true)
|
if (onBuySuccess) onBuySuccess()
|
||||||
setBetAmount(undefined)
|
})
|
||||||
if (onBuySuccess) onBuySuccess()
|
.catch((e) => {
|
||||||
} else {
|
if (e instanceof APIError) {
|
||||||
setError(result?.message || 'Error placing bet')
|
setError(e.toString())
|
||||||
setIsSubmitting(false)
|
} else {
|
||||||
}
|
console.error(e)
|
||||||
|
setError('Error placing bet')
|
||||||
|
}
|
||||||
|
setIsSubmitting(false)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const betDisabled = isSubmitting || !betAmount || error
|
const betDisabled = isSubmitting || !betAmount || error
|
||||||
|
@ -436,13 +428,13 @@ export function SellPanel(props: {
|
||||||
const resultProb = getCpmmProbability(newPool, contract.p)
|
const resultProb = getCpmmProbability(newPool, contract.p)
|
||||||
|
|
||||||
const openUserBets = userBets.filter((bet) => !bet.isSold && !bet.sale)
|
const openUserBets = userBets.filter((bet) => !bet.isSold && !bet.sale)
|
||||||
const [yesBets, noBets] = _.partition(
|
const [yesBets, noBets] = partition(
|
||||||
openUserBets,
|
openUserBets,
|
||||||
(bet) => bet.outcome === 'YES'
|
(bet) => bet.outcome === 'YES'
|
||||||
)
|
)
|
||||||
const [yesShares, noShares] = [
|
const [yesShares, noShares] = [
|
||||||
_.sumBy(yesBets, (bet) => bet.shares),
|
sumBy(yesBets, (bet) => bet.shares),
|
||||||
_.sumBy(noBets, (bet) => bet.shares),
|
sumBy(noBets, (bet) => bet.shares),
|
||||||
]
|
]
|
||||||
|
|
||||||
const sellOutcome = yesShares ? 'YES' : noShares ? 'NO' : undefined
|
const sellOutcome = yesShares ? 'YES' : noShares ? 'NO' : undefined
|
||||||
|
|
|
@ -1,5 +1,13 @@
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import _ from 'lodash'
|
import {
|
||||||
|
uniq,
|
||||||
|
groupBy,
|
||||||
|
mapValues,
|
||||||
|
sortBy,
|
||||||
|
partition,
|
||||||
|
sumBy,
|
||||||
|
throttle,
|
||||||
|
} from 'lodash'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
|
@ -22,7 +30,7 @@ import {
|
||||||
} from 'web/lib/firebase/contracts'
|
} from 'web/lib/firebase/contracts'
|
||||||
import { Row } from './layout/row'
|
import { Row } from './layout/row'
|
||||||
import { UserLink } from './user-page'
|
import { UserLink } from './user-page'
|
||||||
import { sellBet } from 'web/lib/firebase/api-call'
|
import { sellBet } from 'web/lib/firebase/fn-call'
|
||||||
import { ConfirmationButton } from './confirmation-button'
|
import { ConfirmationButton } from './confirmation-button'
|
||||||
import { OutcomeLabel, YesLabel, NoLabel } from './outcome-label'
|
import { OutcomeLabel, YesLabel, NoLabel } from './outcome-label'
|
||||||
import { filterDefined } from 'common/util/array'
|
import { filterDefined } from 'common/util/array'
|
||||||
|
@ -39,14 +47,14 @@ import {
|
||||||
} from 'common/calculate'
|
} from 'common/calculate'
|
||||||
import { useTimeSinceFirstRender } from 'web/hooks/use-time-since-first-render'
|
import { useTimeSinceFirstRender } from 'web/hooks/use-time-since-first-render'
|
||||||
import { trackLatency } from 'web/lib/firebase/tracking'
|
import { trackLatency } from 'web/lib/firebase/tracking'
|
||||||
|
import { NumericContract } from 'common/contract'
|
||||||
|
|
||||||
type BetSort = 'newest' | 'profit' | 'closeTime' | 'value'
|
type BetSort = 'newest' | 'profit' | 'closeTime' | 'value'
|
||||||
type BetFilter = 'open' | 'closed' | 'resolved' | 'all'
|
type BetFilter = 'open' | 'closed' | 'resolved' | 'all'
|
||||||
|
|
||||||
export function BetsList(props: { user: User }) {
|
export function BetsList(props: { user: User }) {
|
||||||
const { user } = props
|
const { user } = props
|
||||||
const bets = useUserBets(user.id)
|
const bets = useUserBets(user.id, { includeRedemptions: true })
|
||||||
|
|
||||||
const [contracts, setContracts] = useState<Contract[] | undefined>()
|
const [contracts, setContracts] = useState<Contract[] | undefined>()
|
||||||
|
|
||||||
const [sort, setSort] = useState<BetSort>('newest')
|
const [sort, setSort] = useState<BetSort>('newest')
|
||||||
|
@ -54,7 +62,7 @@ export function BetsList(props: { user: User }) {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (bets) {
|
if (bets) {
|
||||||
const contractIds = _.uniq(bets.map((bet) => bet.contractId))
|
const contractIds = uniq(bets.map((bet) => bet.contractId))
|
||||||
|
|
||||||
let disposed = false
|
let disposed = false
|
||||||
Promise.all(contractIds.map((id) => getContractFromId(id))).then(
|
Promise.all(contractIds.map((id) => getContractFromId(id))).then(
|
||||||
|
@ -84,10 +92,10 @@ export function BetsList(props: { user: User }) {
|
||||||
if (bets.length === 0) return <NoBets />
|
if (bets.length === 0) return <NoBets />
|
||||||
// Decending creation time.
|
// Decending creation time.
|
||||||
bets.sort((bet1, bet2) => bet2.createdTime - bet1.createdTime)
|
bets.sort((bet1, bet2) => bet2.createdTime - bet1.createdTime)
|
||||||
const contractBets = _.groupBy(bets, 'contractId')
|
const contractBets = groupBy(bets, 'contractId')
|
||||||
const contractsById = _.fromPairs(contracts.map((c) => [c.id, c]))
|
const contractsById = Object.fromEntries(contracts.map((c) => [c.id, c]))
|
||||||
|
|
||||||
const contractsMetrics = _.mapValues(contractBets, (bets, contractId) => {
|
const contractsMetrics = mapValues(contractBets, (bets, contractId) => {
|
||||||
const contract = contractsById[contractId]
|
const contract = contractsById[contractId]
|
||||||
if (!contract) return getContractBetNullMetrics()
|
if (!contract) return getContractBetNullMetrics()
|
||||||
return getContractBetMetrics(contract, bets)
|
return getContractBetMetrics(contract, bets)
|
||||||
|
@ -110,7 +118,7 @@ export function BetsList(props: { user: User }) {
|
||||||
(filter === 'open' ? -1 : 1) *
|
(filter === 'open' ? -1 : 1) *
|
||||||
(c.resolutionTime ?? c.closeTime ?? Infinity),
|
(c.resolutionTime ?? c.closeTime ?? Infinity),
|
||||||
}
|
}
|
||||||
const displayedContracts = _.sortBy(contracts, SORTS[sort])
|
const displayedContracts = sortBy(contracts, SORTS[sort])
|
||||||
.reverse()
|
.reverse()
|
||||||
.filter(FILTERS[filter])
|
.filter(FILTERS[filter])
|
||||||
.filter((c) => {
|
.filter((c) => {
|
||||||
|
@ -121,20 +129,20 @@ export function BetsList(props: { user: User }) {
|
||||||
return metrics.payout > 0
|
return metrics.payout > 0
|
||||||
})
|
})
|
||||||
|
|
||||||
const [settled, unsettled] = _.partition(
|
const [settled, unsettled] = partition(
|
||||||
contracts,
|
contracts,
|
||||||
(c) => c.isResolved || contractsMetrics[c.id].invested === 0
|
(c) => c.isResolved || contractsMetrics[c.id].invested === 0
|
||||||
)
|
)
|
||||||
|
|
||||||
const currentInvested = _.sumBy(
|
const currentInvested = sumBy(
|
||||||
unsettled,
|
unsettled,
|
||||||
(c) => contractsMetrics[c.id].invested
|
(c) => contractsMetrics[c.id].invested
|
||||||
)
|
)
|
||||||
const currentBetsValue = _.sumBy(
|
const currentBetsValue = sumBy(
|
||||||
unsettled,
|
unsettled,
|
||||||
(c) => contractsMetrics[c.id].payout
|
(c) => contractsMetrics[c.id].payout
|
||||||
)
|
)
|
||||||
const currentNetInvestment = _.sumBy(
|
const currentNetInvestment = sumBy(
|
||||||
unsettled,
|
unsettled,
|
||||||
(c) => contractsMetrics[c.id].netPayout
|
(c) => contractsMetrics[c.id].netPayout
|
||||||
)
|
)
|
||||||
|
@ -228,6 +236,8 @@ function MyContractBets(props: {
|
||||||
const { bets, contract, metric } = props
|
const { bets, contract, metric } = props
|
||||||
const { resolution, outcomeType } = contract
|
const { resolution, outcomeType } = contract
|
||||||
|
|
||||||
|
const resolutionValue = (contract as NumericContract).resolutionValue
|
||||||
|
|
||||||
const [collapsed, setCollapsed] = useState(true)
|
const [collapsed, setCollapsed] = useState(true)
|
||||||
|
|
||||||
const isBinary = outcomeType === 'BINARY'
|
const isBinary = outcomeType === 'BINARY'
|
||||||
|
@ -273,6 +283,7 @@ function MyContractBets(props: {
|
||||||
Resolved{' '}
|
Resolved{' '}
|
||||||
<OutcomeLabel
|
<OutcomeLabel
|
||||||
outcome={resolution}
|
outcome={resolution}
|
||||||
|
value={resolutionValue}
|
||||||
contract={contract}
|
contract={contract}
|
||||||
truncate="short"
|
truncate="short"
|
||||||
/>
|
/>
|
||||||
|
@ -327,16 +338,20 @@ export function MyBetsSummary(props: {
|
||||||
bets: Bet[]
|
bets: Bet[]
|
||||||
className?: string
|
className?: string
|
||||||
}) {
|
}) {
|
||||||
const { bets, contract, className } = props
|
const { contract, className } = props
|
||||||
const { resolution, outcomeType, mechanism } = contract
|
const { resolution, outcomeType, mechanism } = contract
|
||||||
const isBinary = outcomeType === 'BINARY'
|
const isBinary = outcomeType === 'BINARY'
|
||||||
const isCpmm = mechanism === 'cpmm-1'
|
const isCpmm = mechanism === 'cpmm-1'
|
||||||
|
|
||||||
const excludeSales = bets.filter((b) => !b.isSold && !b.sale)
|
const bets = props.bets.filter((b) => !b.isAnte)
|
||||||
const yesWinnings = _.sumBy(excludeSales, (bet) =>
|
|
||||||
|
const excludeSalesAndAntes = bets.filter(
|
||||||
|
(b) => !b.isAnte && !b.isSold && !b.sale
|
||||||
|
)
|
||||||
|
const yesWinnings = sumBy(excludeSalesAndAntes, (bet) =>
|
||||||
calculatePayout(contract, bet, 'YES')
|
calculatePayout(contract, bet, 'YES')
|
||||||
)
|
)
|
||||||
const noWinnings = _.sumBy(excludeSales, (bet) =>
|
const noWinnings = sumBy(excludeSalesAndAntes, (bet) =>
|
||||||
calculatePayout(contract, bet, 'NO')
|
calculatePayout(contract, bet, 'NO')
|
||||||
)
|
)
|
||||||
const { invested, profitPercent, payout, profit } = getContractBetMetrics(
|
const { invested, profitPercent, payout, profit } = getContractBetMetrics(
|
||||||
|
@ -410,29 +425,30 @@ export function ContractBetsTable(props: {
|
||||||
bets: Bet[]
|
bets: Bet[]
|
||||||
className?: string
|
className?: string
|
||||||
}) {
|
}) {
|
||||||
const { contract, bets, className } = props
|
const { contract, className } = props
|
||||||
|
|
||||||
const [sales, buys] = _.partition(bets, (bet) => bet.sale)
|
const bets = props.bets.filter((b) => !b.isAnte)
|
||||||
|
|
||||||
const salesDict = _.fromPairs(
|
const [sales, buys] = partition(bets, (bet) => bet.sale)
|
||||||
|
|
||||||
|
const salesDict = Object.fromEntries(
|
||||||
sales.map((sale) => [sale.sale?.betId ?? '', sale])
|
sales.map((sale) => [sale.sale?.betId ?? '', sale])
|
||||||
)
|
)
|
||||||
|
|
||||||
const [redemptions, normalBets] = _.partition(
|
const [redemptions, normalBets] = partition(
|
||||||
contract.mechanism === 'cpmm-1' ? bets : buys,
|
contract.mechanism === 'cpmm-1' ? bets : buys,
|
||||||
(b) => b.isRedemption
|
(b) => b.isRedemption
|
||||||
)
|
)
|
||||||
const amountRedeemed = Math.floor(
|
const amountRedeemed = Math.floor(-0.5 * sumBy(redemptions, (b) => b.shares))
|
||||||
-0.5 * _.sumBy(redemptions, (b) => b.shares)
|
|
||||||
)
|
|
||||||
|
|
||||||
const amountLoaned = _.sumBy(
|
const amountLoaned = sumBy(
|
||||||
bets.filter((bet) => !bet.isSold && !bet.sale),
|
bets.filter((bet) => !bet.isSold && !bet.sale),
|
||||||
(bet) => bet.loanAmount ?? 0
|
(bet) => bet.loanAmount ?? 0
|
||||||
)
|
)
|
||||||
|
|
||||||
const { isResolved, mechanism } = contract
|
const { isResolved, mechanism, outcomeType } = contract
|
||||||
const isCPMM = mechanism === 'cpmm-1'
|
const isCPMM = mechanism === 'cpmm-1'
|
||||||
|
const isNumeric = outcomeType === 'NUMERIC'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx('overflow-x-auto', className)}>
|
<div className={clsx('overflow-x-auto', className)}>
|
||||||
|
@ -462,7 +478,9 @@ export function ContractBetsTable(props: {
|
||||||
{isCPMM && <th>Type</th>}
|
{isCPMM && <th>Type</th>}
|
||||||
<th>Outcome</th>
|
<th>Outcome</th>
|
||||||
<th>Amount</th>
|
<th>Amount</th>
|
||||||
{!isCPMM && <th>{isResolved ? <>Payout</> : <>Sale price</>}</th>}
|
{!isCPMM && !isNumeric && (
|
||||||
|
<th>{isResolved ? <>Payout</> : <>Sale price</>}</th>
|
||||||
|
)}
|
||||||
{!isCPMM && !isResolved && <th>Payout if chosen</th>}
|
{!isCPMM && !isResolved && <th>Payout if chosen</th>}
|
||||||
<th>Shares</th>
|
<th>Shares</th>
|
||||||
<th>Probability</th>
|
<th>Probability</th>
|
||||||
|
@ -497,11 +515,12 @@ function BetRow(props: { bet: Bet; contract: Contract; saleBet?: Bet }) {
|
||||||
isAnte,
|
isAnte,
|
||||||
} = bet
|
} = bet
|
||||||
|
|
||||||
const { isResolved, closeTime, mechanism } = contract
|
const { isResolved, closeTime, mechanism, outcomeType } = contract
|
||||||
|
|
||||||
const isClosed = closeTime && Date.now() > closeTime
|
const isClosed = closeTime && Date.now() > closeTime
|
||||||
|
|
||||||
const isCPMM = mechanism === 'cpmm-1'
|
const isCPMM = mechanism === 'cpmm-1'
|
||||||
|
const isNumeric = outcomeType === 'NUMERIC'
|
||||||
|
|
||||||
const saleAmount = saleBet?.sale?.amount
|
const saleAmount = saleBet?.sale?.amount
|
||||||
|
|
||||||
|
@ -518,31 +537,35 @@ function BetRow(props: { bet: Bet; contract: Contract; saleBet?: Bet }) {
|
||||||
)
|
)
|
||||||
|
|
||||||
const payoutIfChosenDisplay =
|
const payoutIfChosenDisplay =
|
||||||
bet.outcome === '0' && bet.isAnte
|
bet.isAnte && outcomeType === 'FREE_RESPONSE' && bet.outcome === '0'
|
||||||
? 'N/A'
|
? 'N/A'
|
||||||
: formatMoney(calculatePayout(contract, bet, bet.outcome))
|
: formatMoney(calculatePayout(contract, bet, bet.outcome))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr>
|
<tr>
|
||||||
<td className="text-neutral">
|
<td className="text-neutral">
|
||||||
{!isCPMM && !isResolved && !isClosed && !isSold && !isAnte && (
|
{!isCPMM &&
|
||||||
<SellButton contract={contract} bet={bet} />
|
!isResolved &&
|
||||||
)}
|
!isClosed &&
|
||||||
|
!isSold &&
|
||||||
|
!isAnte &&
|
||||||
|
!isNumeric && <SellButton contract={contract} bet={bet} />}
|
||||||
</td>
|
</td>
|
||||||
{isCPMM && <td>{shares >= 0 ? 'BUY' : 'SELL'}</td>}
|
{isCPMM && <td>{shares >= 0 ? 'BUY' : 'SELL'}</td>}
|
||||||
<td>
|
<td>
|
||||||
{outcome === '0' ? (
|
{bet.isAnte ? (
|
||||||
'ANTE'
|
'ANTE'
|
||||||
) : (
|
) : (
|
||||||
<OutcomeLabel
|
<OutcomeLabel
|
||||||
outcome={outcome}
|
outcome={outcome}
|
||||||
|
value={(bet as any).value}
|
||||||
contract={contract}
|
contract={contract}
|
||||||
truncate="short"
|
truncate="short"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td>{formatMoney(Math.abs(amount))}</td>
|
<td>{formatMoney(Math.abs(amount))}</td>
|
||||||
{!isCPMM && <td>{saleDisplay}</td>}
|
{!isCPMM && !isNumeric && <td>{saleDisplay}</td>}
|
||||||
{!isCPMM && !isResolved && <td>{payoutIfChosenDisplay}</td>}
|
{!isCPMM && !isResolved && <td>{payoutIfChosenDisplay}</td>}
|
||||||
<td>{formatWithCommas(Math.abs(shares))}</td>
|
<td>{formatWithCommas(Math.abs(shares))}</td>
|
||||||
<td>
|
<td>
|
||||||
|
@ -553,10 +576,7 @@ function BetRow(props: { bet: Bet; contract: Contract; saleBet?: Bet }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const warmUpSellBet = _.throttle(
|
const warmUpSellBet = throttle(() => sellBet({}).catch(() => {}), 5000 /* ms */)
|
||||||
() => sellBet({}).catch(() => {}),
|
|
||||||
5000 /* ms */
|
|
||||||
)
|
|
||||||
|
|
||||||
function SellButton(props: { contract: Contract; bet: Bet }) {
|
function SellButton(props: { contract: Contract; bet: Bet }) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
42
web/components/bucket-input.tsx
Normal file
42
web/components/bucket-input.tsx
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
import { NumericContract } from 'common/contract'
|
||||||
|
import { getMappedBucket } from 'common/calculate-dpm'
|
||||||
|
|
||||||
|
import { NumberInput } from './number-input'
|
||||||
|
|
||||||
|
export function BucketInput(props: {
|
||||||
|
contract: NumericContract
|
||||||
|
isSubmitting?: boolean
|
||||||
|
onBucketChange: (value?: number, bucket?: string) => void
|
||||||
|
}) {
|
||||||
|
const { contract, isSubmitting, onBucketChange } = props
|
||||||
|
|
||||||
|
const [numberString, setNumberString] = useState('')
|
||||||
|
|
||||||
|
const onChange = (s: string) => {
|
||||||
|
setNumberString(s)
|
||||||
|
|
||||||
|
const value = parseFloat(s)
|
||||||
|
|
||||||
|
if (!isFinite(value)) {
|
||||||
|
onBucketChange(undefined, undefined)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const bucket = getMappedBucket(value, contract)
|
||||||
|
|
||||||
|
onBucketChange(value, bucket)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NumberInput
|
||||||
|
inputClassName="w-full max-w-none"
|
||||||
|
onChange={onChange}
|
||||||
|
error={undefined}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
numberString={numberString}
|
||||||
|
label="Value"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import { StarIcon } from '@heroicons/react/solid'
|
import { StarIcon } from '@heroicons/react/solid'
|
||||||
import _ from 'lodash'
|
import { sumBy } from 'lodash'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import { Charity } from 'common/charity'
|
import { Charity } from 'common/charity'
|
||||||
|
@ -11,7 +11,7 @@ export function CharityCard(props: { charity: Charity }) {
|
||||||
const { name, slug, photo, preview, id, tags } = props.charity
|
const { name, slug, photo, preview, id, tags } = props.charity
|
||||||
|
|
||||||
const txns = useCharityTxns(id)
|
const txns = useCharityTxns(id)
|
||||||
const raised = _.sumBy(txns, (txn) => txn.amount)
|
const raised = sumBy(txns, (txn) => txn.amount)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link href={`/charity/${slug}`} passHref>
|
<Link href={`/charity/${slug}`} passHref>
|
||||||
|
|
56
web/components/choices-toggle-group.tsx
Normal file
56
web/components/choices-toggle-group.tsx
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import { Row } from './layout/row'
|
||||||
|
import { RadioGroup } from '@headlessui/react'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export function ChoicesToggleGroup(props: {
|
||||||
|
currentChoice: number | string
|
||||||
|
choicesMap: { [key: string]: string | number }
|
||||||
|
isSubmitting?: boolean
|
||||||
|
setChoice: (p: number | string) => void
|
||||||
|
className?: string
|
||||||
|
children?: React.ReactNode
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
currentChoice,
|
||||||
|
setChoice,
|
||||||
|
isSubmitting,
|
||||||
|
choicesMap,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
} = props
|
||||||
|
return (
|
||||||
|
<Row className={'mt-2 items-center gap-2'}>
|
||||||
|
<RadioGroup
|
||||||
|
value={currentChoice.toString()}
|
||||||
|
onChange={(str) => null}
|
||||||
|
className="mt-2"
|
||||||
|
>
|
||||||
|
<div className={`grid grid-cols-12 gap-3`}>
|
||||||
|
{Object.keys(choicesMap).map((choiceKey) => (
|
||||||
|
<RadioGroup.Option
|
||||||
|
key={choiceKey}
|
||||||
|
value={choicesMap[choiceKey]}
|
||||||
|
onClick={() => setChoice(choicesMap[choiceKey])}
|
||||||
|
className={({ active }) =>
|
||||||
|
clsx(
|
||||||
|
active ? 'ring-2 ring-indigo-500 ring-offset-2' : '',
|
||||||
|
currentChoice === choicesMap[choiceKey]
|
||||||
|
? 'border-transparent bg-indigo-500 text-white hover:bg-indigo-600'
|
||||||
|
: 'border-gray-200 bg-white text-gray-900 hover:bg-gray-50',
|
||||||
|
'flex cursor-pointer items-center justify-center rounded-md border py-3 px-3 text-sm font-medium normal-case',
|
||||||
|
"hover:ring-offset-2' hover:ring-2 hover:ring-indigo-500",
|
||||||
|
className
|
||||||
|
)
|
||||||
|
}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
<RadioGroup.Label as="span">{choiceKey}</RadioGroup.Label>
|
||||||
|
</RadioGroup.Option>
|
||||||
|
))}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
</Row>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
// Adapted from https://stackoverflow.com/a/50884055/1222351
|
// Adapted from https://stackoverflow.com/a/50884055/1222351
|
||||||
import { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
|
|
||||||
export function ClientRender(props: { children: React.ReactNode }) {
|
export function ClientRender(props: { children: React.ReactNode }) {
|
||||||
const { children } = props
|
const { children } = props
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { useState } from 'react'
|
import { ReactNode, useState } from 'react'
|
||||||
import { Col } from './layout/col'
|
import { Col } from './layout/col'
|
||||||
import { Modal } from './layout/modal'
|
import { Modal } from './layout/modal'
|
||||||
import { Row } from './layout/row'
|
import { Row } from './layout/row'
|
||||||
|
@ -20,7 +20,7 @@ export function ConfirmationButton(props: {
|
||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
onSubmit: () => void
|
onSubmit: () => void
|
||||||
children: any
|
children: ReactNode
|
||||||
}) {
|
}) {
|
||||||
const { id, openModalBtn, cancelBtn, submitBtn, onSubmit, children } = props
|
const { id, openModalBtn, cancelBtn, submitBtn, onSubmit, children } = props
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import algoliasearch from 'algoliasearch/lite'
|
import algoliasearch from 'algoliasearch/lite'
|
||||||
import {
|
import {
|
||||||
InstantSearch,
|
InstantSearch,
|
||||||
|
@ -8,7 +9,6 @@ import {
|
||||||
useRange,
|
useRange,
|
||||||
useRefinementList,
|
useRefinementList,
|
||||||
useSortBy,
|
useSortBy,
|
||||||
useToggleRefinement,
|
|
||||||
} from 'react-instantsearch-hooks-web'
|
} from 'react-instantsearch-hooks-web'
|
||||||
import { Contract } from '../../common/contract'
|
import { Contract } from '../../common/contract'
|
||||||
import {
|
import {
|
||||||
|
@ -37,6 +37,7 @@ const sortIndexes = [
|
||||||
{ label: 'Oldest', value: indexPrefix + 'contracts-oldest' },
|
{ label: 'Oldest', value: indexPrefix + 'contracts-oldest' },
|
||||||
{ label: 'Most traded', value: indexPrefix + 'contracts-most-traded' },
|
{ label: 'Most traded', value: indexPrefix + 'contracts-most-traded' },
|
||||||
{ label: '24h volume', value: indexPrefix + 'contracts-24-hour-vol' },
|
{ label: '24h volume', value: indexPrefix + 'contracts-24-hour-vol' },
|
||||||
|
{ label: 'Last updated', value: indexPrefix + 'contracts-last-updated' },
|
||||||
{ label: 'Close date', value: indexPrefix + 'contracts-close-date' },
|
{ label: 'Close date', value: indexPrefix + 'contracts-close-date' },
|
||||||
{ label: 'Resolve date', value: indexPrefix + 'contracts-resolve-date' },
|
{ label: 'Resolve date', value: indexPrefix + 'contracts-resolve-date' },
|
||||||
]
|
]
|
||||||
|
@ -82,54 +83,49 @@ export function ContractSearch(props: {
|
||||||
additionalFilter?.tag ?? additionalFilter?.creatorId ?? ''
|
additionalFilter?.tag ?? additionalFilter?.creatorId ?? ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Row className="flex-wrap gap-2">
|
<Row className="gap-1 sm:gap-2">
|
||||||
<SearchBox
|
<SearchBox
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
classNames={{
|
classNames={{
|
||||||
form: 'before:top-6',
|
form: 'before:top-6',
|
||||||
input: '!pl-10 !input !input-bordered shadow-none',
|
input: '!pl-10 !input !input-bordered shadow-none w-[100px]',
|
||||||
resetIcon: 'mt-2',
|
resetIcon: 'mt-2 hidden sm:flex',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
className="!select !select-bordered"
|
||||||
|
value={filter}
|
||||||
|
onChange={(e) => setFilter(e.target.value as filter)}
|
||||||
|
>
|
||||||
|
<option value="open">Open</option>
|
||||||
|
<option value="closed">Closed</option>
|
||||||
|
<option value="resolved">Resolved</option>
|
||||||
|
<option value="all">All</option>
|
||||||
|
</select>
|
||||||
|
<SortBy
|
||||||
|
items={sortIndexes}
|
||||||
|
classNames={{
|
||||||
|
select: '!select !select-bordered',
|
||||||
}}
|
}}
|
||||||
placeholder="Search markets"
|
|
||||||
/>
|
/>
|
||||||
<Row className="mt-2 gap-2 sm:mt-0">
|
|
||||||
<select
|
|
||||||
className="!select !select-bordered"
|
|
||||||
value={filter}
|
|
||||||
onChange={(e) => setFilter(e.target.value as filter)}
|
|
||||||
>
|
|
||||||
<option value="open">Open</option>
|
|
||||||
<option value="closed">Closed</option>
|
|
||||||
<option value="resolved">Resolved</option>
|
|
||||||
<option value="all">All</option>
|
|
||||||
</select>
|
|
||||||
<SortBy
|
|
||||||
items={sortIndexes}
|
|
||||||
classNames={{
|
|
||||||
select: '!select !select-bordered',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Row>
|
|
||||||
</Row>
|
</Row>
|
||||||
<div>
|
|
||||||
{showCategorySelector && (
|
|
||||||
<>
|
|
||||||
<Spacer h={4} />
|
|
||||||
<CategorySelector
|
|
||||||
user={user}
|
|
||||||
category={category}
|
|
||||||
setCategory={setCategory}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<Spacer h={4} />
|
|
||||||
|
|
||||||
<ContractSearchInner
|
<Spacer h={3} />
|
||||||
querySortOptions={querySortOptions}
|
|
||||||
filter={filter}
|
{showCategorySelector && (
|
||||||
additionalFilter={{ category, ...additionalFilter }}
|
<CategorySelector
|
||||||
|
className="mb-2"
|
||||||
|
user={user}
|
||||||
|
category={category}
|
||||||
|
setCategory={setCategory}
|
||||||
/>
|
/>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
|
<ContractSearchInner
|
||||||
|
querySortOptions={querySortOptions}
|
||||||
|
filter={filter}
|
||||||
|
additionalFilter={{ category, ...additionalFilter }}
|
||||||
|
/>
|
||||||
</InstantSearch>
|
</InstantSearch>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -195,20 +191,23 @@ export function ContractSearchInner(props: {
|
||||||
filter === 'resolved' ? true : filter === 'all' ? undefined : false
|
filter === 'resolved' ? true : filter === 'all' ? undefined : false
|
||||||
)
|
)
|
||||||
|
|
||||||
const { showMore, hits, isLastPage, results } = useInfiniteHits()
|
const [isInitialLoad, setIsInitialLoad] = useState(true)
|
||||||
|
useEffect(() => {
|
||||||
|
const id = setTimeout(() => setIsInitialLoad(false), 1000)
|
||||||
|
return () => clearTimeout(id)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const { showMore, hits, isLastPage } = useInfiniteHits()
|
||||||
const contracts = hits as any as Contract[]
|
const contracts = hits as any as Contract[]
|
||||||
|
|
||||||
const router = useRouter()
|
if (isInitialLoad && contracts.length === 0) return <></>
|
||||||
const hasLoaded = contracts.length > 0 || router.isReady
|
|
||||||
|
|
||||||
if (!hasLoaded || !results) return <></>
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ContractsGrid
|
<ContractsGrid
|
||||||
contracts={contracts}
|
contracts={contracts}
|
||||||
loadMore={showMore}
|
loadMore={showMore}
|
||||||
hasMore={!isLastPage}
|
hasMore={!isLastPage}
|
||||||
showCloseTime={index === 'contracts-closing-soon'}
|
showCloseTime={index.endsWith('close-date')}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -242,13 +241,16 @@ const useFilterClosed = (value: boolean | undefined) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const useFilterResolved = (value: boolean | undefined) => {
|
const useFilterResolved = (value: boolean | undefined) => {
|
||||||
// Note (James): I don't know why this works.
|
const { items, refine: deleteRefinement } = useCurrentRefinements({
|
||||||
const { refine: refineResolved } = useToggleRefinement({
|
includedAttributes: ['isResolved'],
|
||||||
attribute: value === undefined ? 'non-existant-field' : 'isResolved',
|
|
||||||
on: true,
|
|
||||||
off: value === undefined ? undefined : false,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const { refine } = useRefinementList({ attribute: 'isResolved' })
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refineResolved({ isRefined: !value })
|
const refinements = items[0]?.refinements ?? []
|
||||||
|
|
||||||
|
if (value !== undefined) refine(`${value}`)
|
||||||
|
refinements.forEach((refinement) => deleteRefinement(refinement))
|
||||||
}, [value])
|
}, [value])
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { Row } from '../layout/row'
|
import { Row } from '../layout/row'
|
||||||
import { formatPercent } from 'common/util/format'
|
import { formatLargeNumber, formatPercent } from 'common/util/format'
|
||||||
import {
|
import {
|
||||||
Contract,
|
Contract,
|
||||||
contractPath,
|
contractPath,
|
||||||
getBinaryProbPercent,
|
getBinaryProbPercent,
|
||||||
getBinaryProb,
|
|
||||||
} from 'web/lib/firebase/contracts'
|
} from 'web/lib/firebase/contracts'
|
||||||
import { Col } from '../layout/col'
|
import { Col } from '../layout/col'
|
||||||
import {
|
import {
|
||||||
|
@ -16,47 +15,19 @@ import {
|
||||||
FreeResponse,
|
FreeResponse,
|
||||||
FreeResponseContract,
|
FreeResponseContract,
|
||||||
FullContract,
|
FullContract,
|
||||||
|
NumericContract,
|
||||||
} from 'common/contract'
|
} from 'common/contract'
|
||||||
import {
|
import {
|
||||||
AnswerLabel,
|
AnswerLabel,
|
||||||
BinaryContractOutcomeLabel,
|
BinaryContractOutcomeLabel,
|
||||||
|
CancelLabel,
|
||||||
FreeResponseOutcomeLabel,
|
FreeResponseOutcomeLabel,
|
||||||
OUTCOME_TO_COLOR,
|
|
||||||
} from '../outcome-label'
|
} from '../outcome-label'
|
||||||
import { getOutcomeProbability, getTopAnswer } from 'common/calculate'
|
import { getOutcomeProbability, getTopAnswer } from 'common/calculate'
|
||||||
import { AbbrContractDetails } from './contract-details'
|
import { AvatarDetails, MiscDetails } from './contract-details'
|
||||||
|
import { getExpectedValue, getValueFromBucket } from 'common/calculate-dpm'
|
||||||
// Return a number from 0 to 1 for this contract
|
import { QuickBet, ProbBar, getColor } from './quick-bet'
|
||||||
// Resolved contracts are set to 1, for coloring purposes (even if NO)
|
import { useContractWithPreload } from 'web/hooks/use-contract'
|
||||||
function getProb(contract: Contract) {
|
|
||||||
const { outcomeType, resolution } = contract
|
|
||||||
return resolution
|
|
||||||
? 1
|
|
||||||
: outcomeType === 'BINARY'
|
|
||||||
? getBinaryProb(contract)
|
|
||||||
: outcomeType === 'FREE_RESPONSE'
|
|
||||||
? getOutcomeProbability(contract, getTopAnswer(contract)?.id || '')
|
|
||||||
: 1 // Should not happen
|
|
||||||
}
|
|
||||||
|
|
||||||
function getColor(contract: Contract) {
|
|
||||||
const { resolution } = contract
|
|
||||||
if (resolution) {
|
|
||||||
return (
|
|
||||||
// @ts-ignore; TODO: Have better typing for contract.resolution?
|
|
||||||
OUTCOME_TO_COLOR[resolution] ||
|
|
||||||
// If resolved to a FR answer, use 'primary'
|
|
||||||
'primary'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const marketClosed = (contract.closeTime || Infinity) < Date.now()
|
|
||||||
return marketClosed
|
|
||||||
? 'gray-400'
|
|
||||||
: getProb(contract) >= 0.5
|
|
||||||
? 'primary'
|
|
||||||
: 'red-400'
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ContractCard(props: {
|
export function ContractCard(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
|
@ -64,73 +35,95 @@ export function ContractCard(props: {
|
||||||
showCloseTime?: boolean
|
showCloseTime?: boolean
|
||||||
className?: string
|
className?: string
|
||||||
}) {
|
}) {
|
||||||
const { contract, showHotVolume, showCloseTime, className } = props
|
const { showHotVolume, showCloseTime, className } = props
|
||||||
|
const contract = useContractWithPreload(props.contract) ?? props.contract
|
||||||
const { question, outcomeType } = contract
|
const { question, outcomeType } = contract
|
||||||
|
const { resolution } = contract
|
||||||
|
|
||||||
const prob = getProb(contract)
|
|
||||||
const color = getColor(contract)
|
|
||||||
const marketClosed = (contract.closeTime || Infinity) < Date.now()
|
const marketClosed = (contract.closeTime || Infinity) < Date.now()
|
||||||
const showTopBar = prob >= 0.5 || marketClosed
|
const showQuickBet = !(
|
||||||
|
marketClosed ||
|
||||||
|
(outcomeType === 'FREE_RESPONSE' && getTopAnswer(contract) === undefined)
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Col
|
<Col
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'relative gap-3 rounded-lg bg-white p-6 pr-7 shadow-md hover:bg-gray-100',
|
'relative gap-3 rounded-lg bg-white py-4 pl-6 pr-5 shadow-md hover:cursor-pointer hover:bg-gray-100',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Link href={contractPath(contract)}>
|
<Row>
|
||||||
<a className="absolute left-0 right-0 top-0 bottom-0" />
|
<Col className="relative flex-1 gap-3 pr-1">
|
||||||
</Link>
|
<div
|
||||||
|
className={clsx(
|
||||||
<AbbrContractDetails
|
'peer absolute -left-6 -top-4 -bottom-4 z-10',
|
||||||
contract={contract}
|
// Hack: Extend the clickable area for closed markets
|
||||||
showHotVolume={showHotVolume}
|
showQuickBet ? 'right-0' : 'right-[-6.5rem]'
|
||||||
showCloseTime={showCloseTime}
|
)}
|
||||||
/>
|
>
|
||||||
|
<Link href={contractPath(contract)}>
|
||||||
<Row className={clsx('justify-between gap-4')}>
|
<a className="absolute top-0 left-0 right-0 bottom-0" />
|
||||||
<Col className="gap-3">
|
</Link>
|
||||||
|
</div>
|
||||||
|
<AvatarDetails contract={contract} />
|
||||||
<p
|
<p
|
||||||
className="break-words font-medium text-indigo-700"
|
className="break-words font-semibold text-indigo-700 peer-hover:underline peer-hover:decoration-indigo-400 peer-hover:decoration-2"
|
||||||
style={{ /* For iOS safari */ wordBreak: 'break-word' }}
|
style={{ /* For iOS safari */ wordBreak: 'break-word' }}
|
||||||
>
|
>
|
||||||
{question}
|
{question}
|
||||||
</p>
|
</p>
|
||||||
</Col>
|
|
||||||
{outcomeType === 'BINARY' && (
|
{outcomeType === 'FREE_RESPONSE' &&
|
||||||
<BinaryResolutionOrChance
|
(resolution ? (
|
||||||
className="items-center"
|
<FreeResponseOutcomeLabel
|
||||||
|
contract={contract as FreeResponseContract}
|
||||||
|
resolution={resolution}
|
||||||
|
truncate={'long'}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FreeResponseTopAnswer
|
||||||
|
contract={contract as FullContract<DPM, FreeResponse>}
|
||||||
|
truncate="long"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<MiscDetails
|
||||||
contract={contract}
|
contract={contract}
|
||||||
|
showHotVolume={showHotVolume}
|
||||||
|
showCloseTime={showCloseTime}
|
||||||
/>
|
/>
|
||||||
|
</Col>
|
||||||
|
{showQuickBet ? (
|
||||||
|
<QuickBet contract={contract} />
|
||||||
|
) : (
|
||||||
|
<Col className="m-auto pl-2">
|
||||||
|
{outcomeType === 'BINARY' && (
|
||||||
|
<BinaryResolutionOrChance
|
||||||
|
className="items-center"
|
||||||
|
contract={contract}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{outcomeType === 'NUMERIC' && (
|
||||||
|
<NumericResolutionOrExpectation
|
||||||
|
className="items-center"
|
||||||
|
contract={contract as NumericContract}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{outcomeType === 'FREE_RESPONSE' && (
|
||||||
|
<FreeResponseResolutionOrChance
|
||||||
|
className="self-end text-gray-600"
|
||||||
|
contract={contract as FullContract<DPM, FreeResponse>}
|
||||||
|
truncate="long"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<ProbBar contract={contract} />
|
||||||
|
</Col>
|
||||||
)}
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
{outcomeType === 'FREE_RESPONSE' && (
|
|
||||||
<FreeResponseResolutionOrChance
|
|
||||||
className="self-end text-gray-600"
|
|
||||||
contract={contract as FullContract<DPM, FreeResponse>}
|
|
||||||
truncate="long"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={clsx(
|
|
||||||
'absolute right-0 top-0 w-2 rounded-tr-md',
|
|
||||||
'bg-gray-200'
|
|
||||||
)}
|
|
||||||
style={{ height: `${100 * (1 - prob)}%` }}
|
|
||||||
></div>
|
|
||||||
<div
|
|
||||||
className={clsx(
|
|
||||||
'absolute right-0 bottom-0 w-2 rounded-br-md',
|
|
||||||
`bg-${color}`,
|
|
||||||
// If we're showing the full bar, also round the top
|
|
||||||
prob === 1 ? 'rounded-tr-md' : ''
|
|
||||||
)}
|
|
||||||
style={{ height: `${100 * prob}%` }}
|
|
||||||
></div>
|
|
||||||
</Col>
|
</Col>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -171,6 +164,24 @@ export function BinaryResolutionOrChance(props: {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function FreeResponseTopAnswer(props: {
|
||||||
|
contract: FreeResponseContract
|
||||||
|
truncate: 'short' | 'long' | 'none'
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
const { contract, truncate } = props
|
||||||
|
|
||||||
|
const topAnswer = getTopAnswer(contract)
|
||||||
|
|
||||||
|
return topAnswer ? (
|
||||||
|
<AnswerLabel
|
||||||
|
className="!text-gray-600"
|
||||||
|
answer={topAnswer}
|
||||||
|
truncate={truncate}
|
||||||
|
/>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
|
||||||
export function FreeResponseResolutionOrChance(props: {
|
export function FreeResponseResolutionOrChance(props: {
|
||||||
contract: FreeResponseContract
|
contract: FreeResponseContract
|
||||||
truncate: 'short' | 'long' | 'none'
|
truncate: 'short' | 'long' | 'none'
|
||||||
|
@ -186,22 +197,21 @@ export function FreeResponseResolutionOrChance(props: {
|
||||||
<Col className={clsx(resolution ? 'text-3xl' : 'text-xl', className)}>
|
<Col className={clsx(resolution ? 'text-3xl' : 'text-xl', className)}>
|
||||||
{resolution ? (
|
{resolution ? (
|
||||||
<>
|
<>
|
||||||
<div className={clsx('text-base text-gray-500')}>Resolved</div>
|
<div className={clsx('text-base text-gray-500 sm:hidden')}>
|
||||||
<FreeResponseOutcomeLabel
|
Resolved
|
||||||
contract={contract}
|
</div>
|
||||||
resolution={resolution}
|
{(resolution === 'CANCEL' || resolution === 'MKT') && (
|
||||||
truncate={truncate}
|
<FreeResponseOutcomeLabel
|
||||||
answerClassName="text-xl"
|
contract={contract}
|
||||||
/>
|
resolution={resolution}
|
||||||
|
truncate={truncate}
|
||||||
|
answerClassName="text-3xl uppercase text-blue-500"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
topAnswer && (
|
topAnswer && (
|
||||||
<Row className="items-center gap-6">
|
<Row className="items-center gap-6">
|
||||||
<AnswerLabel
|
|
||||||
className="!text-gray-600"
|
|
||||||
answer={topAnswer}
|
|
||||||
truncate={truncate}
|
|
||||||
/>
|
|
||||||
<Col className={clsx('text-3xl', textColor)}>
|
<Col className={clsx('text-3xl', textColor)}>
|
||||||
<div>
|
<div>
|
||||||
{formatPercent(getOutcomeProbability(contract, topAnswer.id))}
|
{formatPercent(getOutcomeProbability(contract, topAnswer.id))}
|
||||||
|
@ -214,3 +224,38 @@ export function FreeResponseResolutionOrChance(props: {
|
||||||
</Col>
|
</Col>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function NumericResolutionOrExpectation(props: {
|
||||||
|
contract: NumericContract
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
const { contract, className } = props
|
||||||
|
const { resolution } = contract
|
||||||
|
const textColor = `text-${getColor(contract)}`
|
||||||
|
|
||||||
|
const resolutionValue =
|
||||||
|
contract.resolutionValue ?? getValueFromBucket(resolution ?? '', contract)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col className={clsx(resolution ? 'text-3xl' : 'text-xl', className)}>
|
||||||
|
{resolution ? (
|
||||||
|
<>
|
||||||
|
<div className={clsx('text-base text-gray-500')}>Resolved</div>
|
||||||
|
|
||||||
|
{resolution === 'CANCEL' ? (
|
||||||
|
<CancelLabel />
|
||||||
|
) : (
|
||||||
|
<div className="text-blue-400">{resolutionValue}</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className={clsx('text-3xl', textColor)}>
|
||||||
|
{formatLargeNumber(getExpectedValue(contract))}
|
||||||
|
</div>
|
||||||
|
<div className={clsx('text-base', textColor)}>expected</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Col>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -5,13 +5,16 @@ import {
|
||||||
PencilIcon,
|
PencilIcon,
|
||||||
CurrencyDollarIcon,
|
CurrencyDollarIcon,
|
||||||
TrendingUpIcon,
|
TrendingUpIcon,
|
||||||
|
StarIcon,
|
||||||
} from '@heroicons/react/outline'
|
} from '@heroicons/react/outline'
|
||||||
|
import { StarIcon as SolidStarIcon } from '@heroicons/react/solid'
|
||||||
import { Row } from '../layout/row'
|
import { Row } from '../layout/row'
|
||||||
import { formatMoney } from 'common/util/format'
|
import { formatMoney } from 'common/util/format'
|
||||||
import { UserLink } from '../user-page'
|
import { UserLink } from '../user-page'
|
||||||
import {
|
import {
|
||||||
Contract,
|
Contract,
|
||||||
contractMetrics,
|
contractMetrics,
|
||||||
|
contractPool,
|
||||||
updateContract,
|
updateContract,
|
||||||
} from 'web/lib/firebase/contracts'
|
} from 'web/lib/firebase/contracts'
|
||||||
import { Col } from '../layout/col'
|
import { Col } from '../layout/col'
|
||||||
|
@ -26,20 +29,13 @@ import NewContractBadge from '../new-contract-badge'
|
||||||
import { CATEGORY_LIST } from 'common/categories'
|
import { CATEGORY_LIST } from 'common/categories'
|
||||||
import { TagsList } from '../tags-list'
|
import { TagsList } from '../tags-list'
|
||||||
|
|
||||||
export function AbbrContractDetails(props: {
|
export function MiscDetails(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
showHotVolume?: boolean
|
showHotVolume?: boolean
|
||||||
showCloseTime?: boolean
|
showCloseTime?: boolean
|
||||||
}) {
|
}) {
|
||||||
const { contract, showHotVolume, showCloseTime } = props
|
const { contract, showHotVolume, showCloseTime } = props
|
||||||
const {
|
const { volume, volume24Hours, closeTime, tags } = contract
|
||||||
volume,
|
|
||||||
volume24Hours,
|
|
||||||
creatorName,
|
|
||||||
creatorUsername,
|
|
||||||
closeTime,
|
|
||||||
tags,
|
|
||||||
} = contract
|
|
||||||
const { volumeLabel } = contractMetrics(contract)
|
const { volumeLabel } = contractMetrics(contract)
|
||||||
// Show at most one category that this contract is tagged by
|
// Show at most one category that this contract is tagged by
|
||||||
const categories = CATEGORY_LIST.filter((category) =>
|
const categories = CATEGORY_LIST.filter((category) =>
|
||||||
|
@ -47,45 +43,62 @@ export function AbbrContractDetails(props: {
|
||||||
).slice(0, 1)
|
).slice(0, 1)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col className={clsx('gap-2 text-sm text-gray-500')}>
|
<Row className="items-center gap-3 text-sm text-gray-400">
|
||||||
<Row className="items-center justify-between">
|
{showHotVolume ? (
|
||||||
<Row className="items-center gap-2">
|
<Row className="gap-0.5">
|
||||||
<Avatar
|
<TrendingUpIcon className="h-5 w-5" /> {formatMoney(volume24Hours)}
|
||||||
username={creatorUsername}
|
|
||||||
avatarUrl={contract.creatorAvatarUrl}
|
|
||||||
size={6}
|
|
||||||
/>
|
|
||||||
<UserLink
|
|
||||||
className="whitespace-nowrap"
|
|
||||||
name={creatorName}
|
|
||||||
username={creatorUsername}
|
|
||||||
/>
|
|
||||||
</Row>
|
</Row>
|
||||||
|
) : showCloseTime ? (
|
||||||
<Row className="gap-3 text-gray-400">
|
<Row className="gap-0.5">
|
||||||
{categories.length > 0 && (
|
<ClockIcon className="h-5 w-5" />
|
||||||
<TagsList className="text-gray-400" tags={categories} noLabel />
|
{(closeTime || 0) < Date.now() ? 'Closed' : 'Closes'}{' '}
|
||||||
)}
|
{fromNow(closeTime || 0)}
|
||||||
|
|
||||||
{showHotVolume ? (
|
|
||||||
<Row className="gap-0.5">
|
|
||||||
<TrendingUpIcon className="h-5 w-5" />{' '}
|
|
||||||
{formatMoney(volume24Hours)}
|
|
||||||
</Row>
|
|
||||||
) : showCloseTime ? (
|
|
||||||
<Row className="gap-0.5">
|
|
||||||
<ClockIcon className="h-5 w-5" />
|
|
||||||
{(closeTime || 0) < Date.now() ? 'Closed' : 'Closes'}{' '}
|
|
||||||
{fromNow(closeTime || 0)}
|
|
||||||
</Row>
|
|
||||||
) : volume > 0 ? (
|
|
||||||
<Row>{volumeLabel}</Row>
|
|
||||||
) : (
|
|
||||||
<NewContractBadge />
|
|
||||||
)}
|
|
||||||
</Row>
|
</Row>
|
||||||
</Row>
|
) : volume > 0 ? (
|
||||||
</Col>
|
<Row>{contractPool(contract)} pool</Row>
|
||||||
|
) : (
|
||||||
|
<NewContractBadge />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{categories.length > 0 && (
|
||||||
|
<TagsList className="text-gray-400" tags={categories} noLabel />
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AvatarDetails(props: { contract: Contract }) {
|
||||||
|
const { contract } = props
|
||||||
|
const { creatorName, creatorUsername } = contract
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Row className="items-center gap-2 text-sm text-gray-400">
|
||||||
|
<Avatar
|
||||||
|
username={creatorUsername}
|
||||||
|
avatarUrl={contract.creatorAvatarUrl}
|
||||||
|
size={6}
|
||||||
|
/>
|
||||||
|
<UserLink name={creatorName} username={creatorUsername} />
|
||||||
|
</Row>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AbbrContractDetails(props: {
|
||||||
|
contract: Contract
|
||||||
|
showHotVolume?: boolean
|
||||||
|
showCloseTime?: boolean
|
||||||
|
}) {
|
||||||
|
const { contract, showHotVolume, showCloseTime } = props
|
||||||
|
return (
|
||||||
|
<Row className="items-center justify-between">
|
||||||
|
<AvatarDetails contract={contract} />
|
||||||
|
|
||||||
|
<MiscDetails
|
||||||
|
contract={contract}
|
||||||
|
showHotVolume={showHotVolume}
|
||||||
|
showCloseTime={showCloseTime}
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,7 +110,7 @@ export function ContractDetails(props: {
|
||||||
}) {
|
}) {
|
||||||
const { contract, bets, isCreator, disabled } = props
|
const { contract, bets, isCreator, disabled } = props
|
||||||
const { closeTime, creatorName, creatorUsername } = contract
|
const { closeTime, creatorName, creatorUsername } = contract
|
||||||
const { volumeLabel, createdDate, resolvedDate } = contractMetrics(contract)
|
const { volumeLabel, resolvedDate } = contractMetrics(contract)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row className="flex-1 flex-wrap items-center gap-x-4 gap-y-2 text-sm text-gray-500">
|
<Row className="flex-1 flex-wrap items-center gap-x-4 gap-y-2 text-sm text-gray-500">
|
||||||
|
@ -191,7 +204,7 @@ function EditableCloseDate(props: {
|
||||||
|
|
||||||
const [isEditingCloseTime, setIsEditingCloseTime] = useState(false)
|
const [isEditingCloseTime, setIsEditingCloseTime] = useState(false)
|
||||||
const [closeDate, setCloseDate] = useState(
|
const [closeDate, setCloseDate] = useState(
|
||||||
closeTime && dayjs(closeTime).format('YYYY-MM-DDT23:59')
|
closeTime && dayjs(closeTime).format('YYYY-MM-DDTHH:mm')
|
||||||
)
|
)
|
||||||
|
|
||||||
const isSameYear = dayjs(closeTime).isSame(dayjs(), 'year')
|
const isSameYear = dayjs(closeTime).isSame(dayjs(), 'year')
|
||||||
|
|
|
@ -1,13 +1,18 @@
|
||||||
import { DotsHorizontalIcon } from '@heroicons/react/outline'
|
import { DotsHorizontalIcon } from '@heroicons/react/outline'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import _ from 'lodash'
|
import { uniqBy, sum } from 'lodash'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { Bet } from 'common/bet'
|
import { Bet } from 'common/bet'
|
||||||
|
|
||||||
import { Contract } from 'common/contract'
|
import { Contract } from 'common/contract'
|
||||||
import { formatMoney } from 'common/util/format'
|
import { formatMoney } from 'common/util/format'
|
||||||
import { contractPath, getBinaryProbPercent } from 'web/lib/firebase/contracts'
|
import {
|
||||||
|
contractMetrics,
|
||||||
|
contractPath,
|
||||||
|
contractPool,
|
||||||
|
getBinaryProbPercent,
|
||||||
|
} from 'web/lib/firebase/contracts'
|
||||||
import { AddLiquidityPanel } from '../add-liquidity-panel'
|
import { AddLiquidityPanel } from '../add-liquidity-panel'
|
||||||
import { CopyLinkButton } from '../copy-link-button'
|
import { CopyLinkButton } from '../copy-link-button'
|
||||||
import { Col } from '../layout/col'
|
import { Col } from '../layout/col'
|
||||||
|
@ -26,7 +31,7 @@ export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) {
|
||||||
const formatTime = (dt: number) => dayjs(dt).format('MMM DD, YYYY hh:mm a z')
|
const formatTime = (dt: number) => dayjs(dt).format('MMM DD, YYYY hh:mm a z')
|
||||||
|
|
||||||
const { createdTime, closeTime, resolutionTime } = contract
|
const { createdTime, closeTime, resolutionTime } = contract
|
||||||
const tradersCount = _.uniqBy(bets, 'userId').length
|
const tradersCount = uniqBy(bets, 'userId').length
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -98,19 +103,10 @@ export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) {
|
||||||
<td>{tradersCount}</td>
|
<td>{tradersCount}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
{contract.mechanism === 'cpmm-1' && (
|
<tr>
|
||||||
<tr>
|
<td>Pool</td>
|
||||||
<td>Liquidity</td>
|
<td>{contractPool(contract)}</td>
|
||||||
<td>{formatMoney(contract.totalLiquidity)}</td>
|
</tr>
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{contract.mechanism === 'dpm-2' && (
|
|
||||||
<tr>
|
|
||||||
<td>Pool</td>
|
|
||||||
<td>{formatMoney(_.sum(Object.values(contract.pool)))}</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
|
|
@ -6,18 +6,26 @@ import { useUser } from 'web/hooks/use-user'
|
||||||
import { Row } from '../layout/row'
|
import { Row } from '../layout/row'
|
||||||
import { Linkify } from '../linkify'
|
import { Linkify } from '../linkify'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
FreeResponseResolutionOrChance,
|
FreeResponseResolutionOrChance,
|
||||||
BinaryResolutionOrChance,
|
BinaryResolutionOrChance,
|
||||||
|
NumericResolutionOrExpectation,
|
||||||
} from './contract-card'
|
} from './contract-card'
|
||||||
import { Bet } from 'common/bet'
|
import { Bet } from 'common/bet'
|
||||||
import { Comment } from 'common/comment'
|
import { Comment } from 'common/comment'
|
||||||
import BetRow from '../bet-row'
|
import BetRow from '../bet-row'
|
||||||
import { AnswersGraph } from '../answers/answers-graph'
|
import { AnswersGraph } from '../answers/answers-graph'
|
||||||
import { DPM, FreeResponse, FullContract } from 'common/contract'
|
import {
|
||||||
|
DPM,
|
||||||
|
FreeResponse,
|
||||||
|
FullContract,
|
||||||
|
NumericContract,
|
||||||
|
} from 'common/contract'
|
||||||
import { ContractDescription } from './contract-description'
|
import { ContractDescription } from './contract-description'
|
||||||
import { ContractDetails } from './contract-details'
|
import { ContractDetails } from './contract-details'
|
||||||
import { ShareMarket } from '../share-market'
|
import { ShareMarket } from '../share-market'
|
||||||
|
import { NumericGraph } from './numeric-graph'
|
||||||
|
|
||||||
export const ContractOverview = (props: {
|
export const ContractOverview = (props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
|
@ -47,6 +55,13 @@ export const ContractOverview = (props: {
|
||||||
large
|
large
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{outcomeType === 'NUMERIC' && (
|
||||||
|
<NumericResolutionOrExpectation
|
||||||
|
contract={contract as NumericContract}
|
||||||
|
className="hidden items-end xl:flex"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
{isBinary ? (
|
{isBinary ? (
|
||||||
|
@ -65,28 +80,33 @@ export const ContractOverview = (props: {
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{outcomeType === 'NUMERIC' && (
|
||||||
|
<Row className="items-center justify-between gap-4 xl:hidden">
|
||||||
|
<NumericResolutionOrExpectation
|
||||||
|
contract={contract as NumericContract}
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
|
||||||
<ContractDetails
|
<ContractDetails
|
||||||
contract={contract}
|
contract={contract}
|
||||||
bets={bets}
|
bets={bets}
|
||||||
isCreator={isCreator}
|
isCreator={isCreator}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Spacer h={4} />
|
<Spacer h={4} />
|
||||||
|
{isBinary && <ContractProbGraph contract={contract} bets={bets} />}{' '}
|
||||||
{isBinary ? (
|
{outcomeType === 'FREE_RESPONSE' && (
|
||||||
<ContractProbGraph contract={contract} bets={bets} />
|
|
||||||
) : (
|
|
||||||
<AnswersGraph
|
<AnswersGraph
|
||||||
contract={contract as FullContract<DPM, FreeResponse>}
|
contract={contract as FullContract<DPM, FreeResponse>}
|
||||||
bets={bets}
|
bets={bets}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{outcomeType === 'NUMERIC' && (
|
||||||
|
<NumericGraph contract={contract as NumericContract} />
|
||||||
|
)}
|
||||||
{(contract.description || isCreator) && <Spacer h={6} />}
|
{(contract.description || isCreator) && <Spacer h={6} />}
|
||||||
|
|
||||||
{isCreator && <ShareMarket className="px-2" contract={contract} />}
|
{isCreator && <ShareMarket className="px-2" contract={contract} />}
|
||||||
|
|
||||||
<ContractDescription
|
<ContractDescription
|
||||||
className="px-2"
|
className="px-2"
|
||||||
contract={contract}
|
contract={contract}
|
||||||
|
|
|
@ -16,6 +16,7 @@ export function ContractTabs(props: {
|
||||||
comments: Comment[]
|
comments: Comment[]
|
||||||
}) {
|
}) {
|
||||||
const { contract, user, comments } = props
|
const { contract, user, comments } = props
|
||||||
|
const { outcomeType } = contract
|
||||||
|
|
||||||
const bets = useBets(contract.id) ?? props.bets
|
const bets = useBets(contract.id) ?? props.bets
|
||||||
// Decending creation time.
|
// Decending creation time.
|
||||||
|
@ -47,7 +48,7 @@ export function ContractTabs(props: {
|
||||||
}
|
}
|
||||||
betRowClassName="!mt-0 xl:hidden"
|
betRowClassName="!mt-0 xl:hidden"
|
||||||
/>
|
/>
|
||||||
{contract.outcomeType === 'FREE_RESPONSE' && (
|
{outcomeType === 'FREE_RESPONSE' && (
|
||||||
<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'} />
|
||||||
|
|
|
@ -19,7 +19,6 @@ export function ContractsGrid(props: {
|
||||||
const isBottomVisible = useIsVisible(elem)
|
const isBottomVisible = useIsVisible(elem)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log({ isBottomVisible, hasMore })
|
|
||||||
if (isBottomVisible) {
|
if (isBottomVisible) {
|
||||||
loadMore()
|
loadMore()
|
||||||
}
|
}
|
||||||
|
@ -29,7 +28,7 @@ export function ContractsGrid(props: {
|
||||||
return (
|
return (
|
||||||
<p className="mx-2 text-gray-500">
|
<p className="mx-2 text-gray-500">
|
||||||
No markets found. Why not{' '}
|
No markets found. Why not{' '}
|
||||||
<SiteLink href="/home" className="font-bold text-gray-700">
|
<SiteLink href="/create" className="font-bold text-gray-700">
|
||||||
create one?
|
create one?
|
||||||
</SiteLink>
|
</SiteLink>
|
||||||
</p>
|
</p>
|
||||||
|
@ -38,7 +37,7 @@ export function ContractsGrid(props: {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col className="gap-8">
|
<Col className="gap-8">
|
||||||
<ul className="grid w-full grid-cols-1 gap-6 md:grid-cols-2">
|
<ul className="grid w-full grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
{contracts.map((contract) => (
|
{contracts.map((contract) => (
|
||||||
<ContractCard
|
<ContractCard
|
||||||
contract={contract}
|
contract={contract}
|
||||||
|
|
99
web/components/contract/numeric-graph.tsx
Normal file
99
web/components/contract/numeric-graph.tsx
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
import { DatumValue } from '@nivo/core'
|
||||||
|
import { Point, ResponsiveLine } from '@nivo/line'
|
||||||
|
import { NUMERIC_GRAPH_COLOR } from 'common/numeric-constants'
|
||||||
|
import { memo } from 'react'
|
||||||
|
import { range } from 'lodash'
|
||||||
|
import { getDpmOutcomeProbabilities } from '../../../common/calculate-dpm'
|
||||||
|
import { NumericContract } from '../../../common/contract'
|
||||||
|
import { useWindowSize } from '../../hooks/use-window-size'
|
||||||
|
import { Col } from '../layout/col'
|
||||||
|
import { formatLargeNumber } from 'common/util/format'
|
||||||
|
|
||||||
|
export const NumericGraph = memo(function NumericGraph(props: {
|
||||||
|
contract: NumericContract
|
||||||
|
height?: number
|
||||||
|
}) {
|
||||||
|
const { contract, height } = props
|
||||||
|
const { totalShares, bucketCount, min, max } = contract
|
||||||
|
|
||||||
|
const bucketProbs = getDpmOutcomeProbabilities(totalShares)
|
||||||
|
|
||||||
|
const xs = range(bucketCount).map(
|
||||||
|
(i) => min + ((max - min) * i) / bucketCount
|
||||||
|
)
|
||||||
|
const probs = range(bucketCount).map((i) => bucketProbs[`${i}`] * 100)
|
||||||
|
const points = probs.map((prob, i) => ({ x: xs[i], y: prob }))
|
||||||
|
const maxProb = Math.max(...probs)
|
||||||
|
const data = [{ id: 'Probability', data: points, color: NUMERIC_GRAPH_COLOR }]
|
||||||
|
|
||||||
|
const yTickValues = [
|
||||||
|
0,
|
||||||
|
0.25 * maxProb,
|
||||||
|
0.5 & maxProb,
|
||||||
|
0.75 * maxProb,
|
||||||
|
maxProb,
|
||||||
|
]
|
||||||
|
|
||||||
|
const { width } = useWindowSize()
|
||||||
|
|
||||||
|
const numXTickValues = !width || width < 800 ? 2 : 5
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="w-full overflow-hidden"
|
||||||
|
style={{ height: height ?? (!width || width >= 800 ? 350 : 250) }}
|
||||||
|
>
|
||||||
|
<ResponsiveLine
|
||||||
|
data={data}
|
||||||
|
yScale={{ min: 0, max: maxProb, type: 'linear' }}
|
||||||
|
yFormat={formatPercent}
|
||||||
|
axisLeft={{
|
||||||
|
tickValues: yTickValues,
|
||||||
|
format: formatPercent,
|
||||||
|
}}
|
||||||
|
xScale={{
|
||||||
|
type: 'linear',
|
||||||
|
min: min,
|
||||||
|
max: max,
|
||||||
|
}}
|
||||||
|
xFormat={(d) => `${formatLargeNumber(+d, 3)}`}
|
||||||
|
axisBottom={{
|
||||||
|
tickValues: numXTickValues,
|
||||||
|
format: (d) => `${formatLargeNumber(+d, 3)}`,
|
||||||
|
}}
|
||||||
|
colors={{ datum: 'color' }}
|
||||||
|
pointSize={0}
|
||||||
|
enableSlices="x"
|
||||||
|
sliceTooltip={({ slice }) => {
|
||||||
|
const point = slice.points[0]
|
||||||
|
return <Tooltip point={point} />
|
||||||
|
}}
|
||||||
|
enableGridX={!!width && width >= 800}
|
||||||
|
enableArea
|
||||||
|
margin={{ top: 20, right: 28, bottom: 22, left: 50 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
function formatPercent(y: DatumValue) {
|
||||||
|
const p = Math.round(+y * 100) / 100
|
||||||
|
return `${p}%`
|
||||||
|
}
|
||||||
|
|
||||||
|
function Tooltip(props: { point: Point }) {
|
||||||
|
const { point } = props
|
||||||
|
return (
|
||||||
|
<Col className="border border-gray-300 bg-white py-2 px-3">
|
||||||
|
<div
|
||||||
|
className="pb-1"
|
||||||
|
style={{
|
||||||
|
color: point.serieColor,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<strong>{point.serieId}</strong> {point.data.yFormatted}
|
||||||
|
</div>
|
||||||
|
<div>{formatLargeNumber(+point.data.x)}</div>
|
||||||
|
</Col>
|
||||||
|
)
|
||||||
|
}
|
290
web/components/contract/quick-bet.tsx
Normal file
290
web/components/contract/quick-bet.tsx
Normal file
|
@ -0,0 +1,290 @@
|
||||||
|
import clsx from 'clsx'
|
||||||
|
import {
|
||||||
|
getOutcomeProbability,
|
||||||
|
getOutcomeProbabilityAfterBet,
|
||||||
|
getTopAnswer,
|
||||||
|
} from 'common/calculate'
|
||||||
|
import { getExpectedValue } from 'common/calculate-dpm'
|
||||||
|
import {
|
||||||
|
Contract,
|
||||||
|
FullContract,
|
||||||
|
CPMM,
|
||||||
|
DPM,
|
||||||
|
Binary,
|
||||||
|
NumericContract,
|
||||||
|
FreeResponseContract,
|
||||||
|
} from 'common/contract'
|
||||||
|
import {
|
||||||
|
formatLargeNumber,
|
||||||
|
formatMoney,
|
||||||
|
formatPercent,
|
||||||
|
} from 'common/util/format'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import toast from 'react-hot-toast'
|
||||||
|
import { useUser } from 'web/hooks/use-user'
|
||||||
|
import { useUserContractBets } from 'web/hooks/use-user-bets'
|
||||||
|
import { placeBet } from 'web/lib/firebase/api-call'
|
||||||
|
import { getBinaryProb, getBinaryProbPercent } from 'web/lib/firebase/contracts'
|
||||||
|
import TriangleDownFillIcon from 'web/lib/icons/triangle-down-fill-icon'
|
||||||
|
import TriangleFillIcon from 'web/lib/icons/triangle-fill-icon'
|
||||||
|
import { Col } from '../layout/col'
|
||||||
|
import { OUTCOME_TO_COLOR } from '../outcome-label'
|
||||||
|
import { useSaveShares } from '../use-save-shares'
|
||||||
|
|
||||||
|
const BET_SIZE = 10
|
||||||
|
|
||||||
|
export function QuickBet(props: { contract: Contract }) {
|
||||||
|
const { contract } = props
|
||||||
|
|
||||||
|
const user = useUser()
|
||||||
|
const userBets = useUserContractBets(user?.id, contract.id)
|
||||||
|
const topAnswer =
|
||||||
|
contract.outcomeType === 'FREE_RESPONSE'
|
||||||
|
? getTopAnswer(contract as FreeResponseContract)
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
// TODO: yes/no from useSaveShares doesn't work on numeric contracts
|
||||||
|
const { yesFloorShares, noFloorShares } = useSaveShares(
|
||||||
|
contract as FullContract<DPM | CPMM, Binary | FreeResponseContract>,
|
||||||
|
userBets,
|
||||||
|
topAnswer?.number.toString() || undefined
|
||||||
|
)
|
||||||
|
const hasUpShares =
|
||||||
|
yesFloorShares || (noFloorShares && contract.outcomeType === 'NUMERIC')
|
||||||
|
const hasDownShares =
|
||||||
|
noFloorShares && yesFloorShares <= 0 && contract.outcomeType !== 'NUMERIC'
|
||||||
|
|
||||||
|
const [upHover, setUpHover] = useState(false)
|
||||||
|
const [downHover, setDownHover] = useState(false)
|
||||||
|
|
||||||
|
let previewProb = undefined
|
||||||
|
try {
|
||||||
|
previewProb = upHover
|
||||||
|
? getOutcomeProbabilityAfterBet(
|
||||||
|
contract,
|
||||||
|
quickOutcome(contract, 'UP') || '',
|
||||||
|
BET_SIZE
|
||||||
|
)
|
||||||
|
: downHover
|
||||||
|
? 1 -
|
||||||
|
getOutcomeProbabilityAfterBet(
|
||||||
|
contract,
|
||||||
|
quickOutcome(contract, 'DOWN') || '',
|
||||||
|
BET_SIZE
|
||||||
|
)
|
||||||
|
: undefined
|
||||||
|
} catch (e) {
|
||||||
|
// Catch any errors from hovering on an invalid option
|
||||||
|
}
|
||||||
|
|
||||||
|
const color = getColor(contract, previewProb)
|
||||||
|
|
||||||
|
async function placeQuickBet(direction: 'UP' | 'DOWN') {
|
||||||
|
const betPromise = async () => {
|
||||||
|
const outcome = quickOutcome(contract, direction)
|
||||||
|
return await placeBet({
|
||||||
|
amount: BET_SIZE,
|
||||||
|
outcome,
|
||||||
|
contractId: contract.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const shortQ = contract.question.slice(0, 20)
|
||||||
|
toast.promise(betPromise(), {
|
||||||
|
loading: `${formatMoney(BET_SIZE)} on "${shortQ}"...`,
|
||||||
|
success: `${formatMoney(BET_SIZE)} on "${shortQ}"...`,
|
||||||
|
error: (err) => `${err.message}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function quickOutcome(contract: Contract, direction: 'UP' | 'DOWN') {
|
||||||
|
if (contract.outcomeType === 'BINARY') {
|
||||||
|
return direction === 'UP' ? 'YES' : 'NO'
|
||||||
|
}
|
||||||
|
if (contract.outcomeType === 'FREE_RESPONSE') {
|
||||||
|
// TODO: Implement shorting of free response answers
|
||||||
|
if (direction === 'DOWN') {
|
||||||
|
throw new Error("Can't bet against free response answers")
|
||||||
|
}
|
||||||
|
return getTopAnswer(contract)?.id
|
||||||
|
}
|
||||||
|
if (contract.outcomeType === 'NUMERIC') {
|
||||||
|
// TODO: Ideally an 'UP' bet would be a uniform bet between [current, max]
|
||||||
|
throw new Error("Can't quick bet on numeric markets")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col
|
||||||
|
className={clsx(
|
||||||
|
'relative -my-4 -mr-5 min-w-[5.5rem] justify-center gap-2 pr-5 pl-1 align-middle'
|
||||||
|
// Use this for colored QuickBet panes
|
||||||
|
// `bg-opacity-10 bg-${color}`
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Up bet triangle */}
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className="peer absolute top-0 left-0 right-0 h-[50%]"
|
||||||
|
onMouseEnter={() => setUpHover(true)}
|
||||||
|
onMouseLeave={() => setUpHover(false)}
|
||||||
|
onClick={() => placeQuickBet('UP')}
|
||||||
|
/>
|
||||||
|
<div className="mt-2 text-center text-xs text-transparent peer-hover:text-gray-400">
|
||||||
|
{formatMoney(10)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{hasUpShares > 0 ? (
|
||||||
|
<TriangleFillIcon
|
||||||
|
className={clsx(
|
||||||
|
'mx-auto h-5 w-5',
|
||||||
|
upHover ? `text-${color}` : 'text-gray-400'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<TriangleFillIcon
|
||||||
|
className={clsx(
|
||||||
|
'mx-auto h-5 w-5',
|
||||||
|
upHover ? `text-${color}` : 'text-gray-200'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<QuickOutcomeView contract={contract} previewProb={previewProb} />
|
||||||
|
|
||||||
|
{/* Down bet triangle */}
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className="peer absolute bottom-0 left-0 right-0 h-[50%]"
|
||||||
|
onMouseEnter={() => setDownHover(true)}
|
||||||
|
onMouseLeave={() => setDownHover(false)}
|
||||||
|
onClick={() => placeQuickBet('DOWN')}
|
||||||
|
></div>
|
||||||
|
{hasDownShares > 0 ? (
|
||||||
|
<TriangleDownFillIcon
|
||||||
|
className={clsx(
|
||||||
|
'mx-auto h-5 w-5',
|
||||||
|
downHover ? `text-${color}` : 'text-gray-400'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<TriangleDownFillIcon
|
||||||
|
className={clsx(
|
||||||
|
'mx-auto h-5 w-5',
|
||||||
|
downHover ? `text-${color}` : 'text-gray-200'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="mb-2 text-center text-xs text-transparent peer-hover:text-gray-400">
|
||||||
|
{formatMoney(10)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProbBar(props: { contract: Contract; previewProb?: number }) {
|
||||||
|
const { contract, previewProb } = props
|
||||||
|
const color = getColor(contract, previewProb)
|
||||||
|
const prob = previewProb ?? getProb(contract)
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'absolute right-0 top-0 w-2 rounded-tr-md transition-all',
|
||||||
|
'bg-gray-200'
|
||||||
|
)}
|
||||||
|
style={{ height: `${100 * (1 - prob)}%` }}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'absolute right-0 bottom-0 w-2 rounded-br-md transition-all',
|
||||||
|
`bg-${color}`,
|
||||||
|
// If we're showing the full bar, also round the top
|
||||||
|
prob === 1 ? 'rounded-tr-md' : ''
|
||||||
|
)}
|
||||||
|
style={{ height: `${100 * prob}%` }}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function QuickOutcomeView(props: {
|
||||||
|
contract: Contract
|
||||||
|
previewProb?: number
|
||||||
|
caption?: 'chance' | 'expected'
|
||||||
|
}) {
|
||||||
|
const { contract, previewProb, caption } = props
|
||||||
|
const { outcomeType } = contract
|
||||||
|
// If there's a preview prob, display that instead of the current prob
|
||||||
|
const override =
|
||||||
|
previewProb === undefined ? undefined : formatPercent(previewProb)
|
||||||
|
const textColor = `text-${getColor(contract, previewProb)}`
|
||||||
|
|
||||||
|
let display: string | undefined
|
||||||
|
switch (outcomeType) {
|
||||||
|
case 'BINARY':
|
||||||
|
display = getBinaryProbPercent(contract)
|
||||||
|
break
|
||||||
|
case 'NUMERIC':
|
||||||
|
display = formatLargeNumber(getExpectedValue(contract as NumericContract))
|
||||||
|
break
|
||||||
|
case 'FREE_RESPONSE': {
|
||||||
|
const topAnswer = getTopAnswer(contract as FreeResponseContract)
|
||||||
|
display =
|
||||||
|
topAnswer &&
|
||||||
|
formatPercent(getOutcomeProbability(contract, topAnswer.id))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col className={clsx('items-center text-3xl', textColor)}>
|
||||||
|
{override ?? display}
|
||||||
|
{caption && <div className="text-base">{caption}</div>}
|
||||||
|
<ProbBar contract={contract} previewProb={previewProb} />
|
||||||
|
</Col>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a number from 0 to 1 for this contract
|
||||||
|
// Resolved contracts are set to 1, for coloring purposes (even if NO)
|
||||||
|
function getProb(contract: Contract) {
|
||||||
|
const { outcomeType, resolution } = contract
|
||||||
|
return resolution
|
||||||
|
? 1
|
||||||
|
: outcomeType === 'BINARY'
|
||||||
|
? getBinaryProb(contract)
|
||||||
|
: outcomeType === 'FREE_RESPONSE'
|
||||||
|
? getOutcomeProbability(contract, getTopAnswer(contract)?.id || '')
|
||||||
|
: outcomeType === 'NUMERIC'
|
||||||
|
? getNumericScale(contract as NumericContract)
|
||||||
|
: 1 // Should not happen
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNumericScale(contract: NumericContract) {
|
||||||
|
const { min, max } = contract
|
||||||
|
const ev = getExpectedValue(contract)
|
||||||
|
return (ev - min) / (max - min)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getColor(contract: Contract, previewProb?: number) {
|
||||||
|
// TODO: Not sure why eg green-400 doesn't work here; try upgrading Tailwind
|
||||||
|
// TODO: Try injecting a gradient here
|
||||||
|
// return 'primary'
|
||||||
|
const { resolution } = contract
|
||||||
|
if (resolution) {
|
||||||
|
return (
|
||||||
|
OUTCOME_TO_COLOR[resolution as 'YES' | 'NO' | 'CANCEL' | 'MKT'] ??
|
||||||
|
// If resolved to a FR answer, use 'primary'
|
||||||
|
'primary'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (contract.outcomeType === 'NUMERIC') {
|
||||||
|
return 'blue-400'
|
||||||
|
}
|
||||||
|
|
||||||
|
const marketClosed = (contract.closeTime || Infinity) < Date.now()
|
||||||
|
const prob = previewProb ?? getProb(contract)
|
||||||
|
return marketClosed ? 'gray-400' : prob >= 0.5 ? 'primary' : 'red-400'
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
import React from 'react'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import utc from 'dayjs/plugin/utc'
|
import utc from 'dayjs/plugin/utc'
|
||||||
import timezone from 'dayjs/plugin/timezone'
|
import timezone from 'dayjs/plugin/timezone'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import _ from 'lodash'
|
import { sample } from 'lodash'
|
||||||
import { SparklesIcon, XIcon } from '@heroicons/react/solid'
|
import { SparklesIcon, XIcon } from '@heroicons/react/solid'
|
||||||
import { Avatar } from './avatar'
|
import { Avatar } from './avatar'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
@ -30,7 +30,7 @@ export function FeedPromo(props: { hotContracts: Contract[] }) {
|
||||||
<div className="font-semibold sm:mb-2">
|
<div className="font-semibold sm:mb-2">
|
||||||
Bet on{' '}
|
Bet on{' '}
|
||||||
<span className="bg-gradient-to-r from-teal-400 to-green-400 bg-clip-text font-bold text-transparent">
|
<span className="bg-gradient-to-r from-teal-400 to-green-400 bg-clip-text font-bold text-transparent">
|
||||||
any question!
|
anything!
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</h1>
|
</h1>
|
||||||
|
@ -86,9 +86,7 @@ export default function FeedCreate(props: {
|
||||||
// Take care not to produce a different placeholder on the server and client
|
// Take care not to produce a different placeholder on the server and client
|
||||||
const [defaultPlaceholder, setDefaultPlaceholder] = useState('')
|
const [defaultPlaceholder, setDefaultPlaceholder] = useState('')
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setDefaultPlaceholder(
|
setDefaultPlaceholder(`e.g. ${sample(ENV_CONFIG.newQuestionPlaceholders)}`)
|
||||||
`e.g. ${_.sample(ENV_CONFIG.newQuestionPlaceholders)}`
|
|
||||||
)
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const placeholder = props.placeholder ?? defaultPlaceholder
|
const placeholder = props.placeholder ?? defaultPlaceholder
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import _, { Dictionary } from 'lodash'
|
import { last, findLastIndex, uniq, sortBy } from 'lodash'
|
||||||
|
|
||||||
import { Answer } from 'common/answer'
|
import { Answer } from 'common/answer'
|
||||||
import { Bet } from 'common/bet'
|
import { Bet } from 'common/bet'
|
||||||
|
@ -28,7 +28,7 @@ type BaseActivityItem = {
|
||||||
export type CommentInputItem = BaseActivityItem & {
|
export type CommentInputItem = BaseActivityItem & {
|
||||||
type: 'commentInput'
|
type: 'commentInput'
|
||||||
betsByCurrentUser: Bet[]
|
betsByCurrentUser: Bet[]
|
||||||
comments: Comment[]
|
commentsByCurrentUser: Comment[]
|
||||||
answerOutcome?: string
|
answerOutcome?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,6 +54,7 @@ export type CommentItem = BaseActivityItem & {
|
||||||
type: 'comment'
|
type: 'comment'
|
||||||
comment: Comment
|
comment: Comment
|
||||||
betsBySameUser: Bet[]
|
betsBySameUser: Bet[]
|
||||||
|
probAtCreatedTime?: number
|
||||||
truncate?: boolean
|
truncate?: boolean
|
||||||
smallAvatar?: boolean
|
smallAvatar?: boolean
|
||||||
}
|
}
|
||||||
|
@ -62,7 +63,7 @@ export type CommentThreadItem = BaseActivityItem & {
|
||||||
type: 'commentThread'
|
type: 'commentThread'
|
||||||
parentComment: Comment
|
parentComment: Comment
|
||||||
comments: Comment[]
|
comments: Comment[]
|
||||||
betsByUserId: Dictionary<[Bet, ...Bet[]]>
|
bets: Bet[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BetGroupItem = BaseActivityItem & {
|
export type BetGroupItem = BaseActivityItem & {
|
||||||
|
@ -76,7 +77,7 @@ export type AnswerGroupItem = BaseActivityItem & {
|
||||||
answer: Answer
|
answer: Answer
|
||||||
items: ActivityItem[]
|
items: ActivityItem[]
|
||||||
betsByCurrentUser?: Bet[]
|
betsByCurrentUser?: Bet[]
|
||||||
comments?: Comment[]
|
commentsByCurrentUser?: Comment[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CloseItem = BaseActivityItem & {
|
export type CloseItem = BaseActivityItem & {
|
||||||
|
@ -87,7 +88,6 @@ export type ResolveItem = BaseActivityItem & {
|
||||||
type: 'resolve'
|
type: 'resolve'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GENERAL_COMMENTS_OUTCOME_ID = 'General Comments'
|
|
||||||
const DAY_IN_MS = 24 * 60 * 60 * 1000
|
const DAY_IN_MS = 24 * 60 * 60 * 1000
|
||||||
const ABBREVIATED_NUM_COMMENTS_OR_BETS_TO_SHOW = 3
|
const ABBREVIATED_NUM_COMMENTS_OR_BETS_TO_SHOW = 3
|
||||||
|
|
||||||
|
@ -200,17 +200,17 @@ function getAnswerGroups(
|
||||||
) {
|
) {
|
||||||
const { sortByProb, abbreviated, reversed } = options
|
const { sortByProb, abbreviated, reversed } = options
|
||||||
|
|
||||||
let outcomes = _.uniq(bets.map((bet) => bet.outcome)).filter(
|
let outcomes = uniq(bets.map((bet) => bet.outcome)).filter(
|
||||||
(outcome) => getOutcomeProbability(contract, outcome) > 0.0001
|
(outcome) => getOutcomeProbability(contract, outcome) > 0.0001
|
||||||
)
|
)
|
||||||
if (abbreviated) {
|
if (abbreviated) {
|
||||||
const lastComment = _.last(comments)
|
const lastComment = last(comments)
|
||||||
const lastCommentOutcome = bets.find(
|
const lastCommentOutcome = bets.find(
|
||||||
(bet) => bet.id === lastComment?.betId
|
(bet) => bet.id === lastComment?.betId
|
||||||
)?.outcome
|
)?.outcome
|
||||||
const lastBetOutcome = _.last(bets)?.outcome
|
const lastBetOutcome = last(bets)?.outcome
|
||||||
if (lastCommentOutcome && lastBetOutcome) {
|
if (lastCommentOutcome && lastBetOutcome) {
|
||||||
outcomes = _.uniq([
|
outcomes = uniq([
|
||||||
...outcomes.filter(
|
...outcomes.filter(
|
||||||
(outcome) =>
|
(outcome) =>
|
||||||
outcome !== lastCommentOutcome && outcome !== lastBetOutcome
|
outcome !== lastCommentOutcome && outcome !== lastBetOutcome
|
||||||
|
@ -222,13 +222,13 @@ function getAnswerGroups(
|
||||||
outcomes = outcomes.slice(-2)
|
outcomes = outcomes.slice(-2)
|
||||||
}
|
}
|
||||||
if (sortByProb) {
|
if (sortByProb) {
|
||||||
outcomes = _.sortBy(outcomes, (outcome) =>
|
outcomes = sortBy(outcomes, (outcome) =>
|
||||||
getOutcomeProbability(contract, outcome)
|
getOutcomeProbability(contract, outcome)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// Sort by recent bet.
|
// Sort by recent bet.
|
||||||
outcomes = _.sortBy(outcomes, (outcome) =>
|
outcomes = sortBy(outcomes, (outcome) =>
|
||||||
_.findLastIndex(bets, (bet) => bet.outcome === outcome)
|
findLastIndex(bets, (bet) => bet.outcome === outcome)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -274,12 +274,13 @@ function getAnswerAndCommentInputGroups(
|
||||||
comments: Comment[],
|
comments: Comment[],
|
||||||
user: User | undefined | null
|
user: User | undefined | null
|
||||||
) {
|
) {
|
||||||
let outcomes = _.uniq(bets.map((bet) => bet.outcome)).filter(
|
let outcomes = uniq(bets.map((bet) => bet.outcome)).filter(
|
||||||
(outcome) => getOutcomeProbability(contract, outcome) > 0.0001
|
(outcome) => getOutcomeProbability(contract, outcome) > 0.0001
|
||||||
)
|
)
|
||||||
outcomes = _.sortBy(outcomes, (outcome) =>
|
outcomes = sortBy(outcomes, (outcome) =>
|
||||||
getOutcomeProbability(contract, outcome)
|
getOutcomeProbability(contract, outcome)
|
||||||
)
|
)
|
||||||
|
const betsByCurrentUser = bets.filter((bet) => bet.userId === user?.id)
|
||||||
|
|
||||||
const answerGroups = outcomes
|
const answerGroups = outcomes
|
||||||
.map((outcome) => {
|
.map((outcome) => {
|
||||||
|
@ -293,9 +294,7 @@ function getAnswerAndCommentInputGroups(
|
||||||
comment.answerOutcome === outcome ||
|
comment.answerOutcome === outcome ||
|
||||||
answerBets.some((bet) => bet.id === comment.betId)
|
answerBets.some((bet) => bet.id === comment.betId)
|
||||||
)
|
)
|
||||||
const items = getCommentThreads(answerBets, answerComments, contract)
|
const items = getCommentThreads(bets, answerComments, contract)
|
||||||
|
|
||||||
if (outcome === GENERAL_COMMENTS_OUTCOME_ID) items.reverse()
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: outcome,
|
id: outcome,
|
||||||
|
@ -304,8 +303,10 @@ function getAnswerAndCommentInputGroups(
|
||||||
answer,
|
answer,
|
||||||
items,
|
items,
|
||||||
user,
|
user,
|
||||||
betsByCurrentUser: answerBets.filter((bet) => bet.userId === user?.id),
|
betsByCurrentUser,
|
||||||
comments: answerComments,
|
commentsByCurrentUser: answerComments.filter(
|
||||||
|
(comment) => comment.userId === user?.id
|
||||||
|
),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.filter((group) => group.answer) as ActivityItem[]
|
.filter((group) => group.answer) as ActivityItem[]
|
||||||
|
@ -325,6 +326,7 @@ function groupBetsAndComments(
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const { smallAvatar, abbreviated, reversed } = options
|
const { smallAvatar, abbreviated, reversed } = options
|
||||||
|
// Comments in feed don't show user's position?
|
||||||
const commentsWithoutBets = comments
|
const commentsWithoutBets = comments
|
||||||
.filter((comment) => !comment.betId)
|
.filter((comment) => !comment.betId)
|
||||||
.map((comment) => ({
|
.map((comment) => ({
|
||||||
|
@ -341,7 +343,7 @@ function groupBetsAndComments(
|
||||||
|
|
||||||
// iterate through the bets and comment activity items and add them to the items in order of comment creation time:
|
// iterate through the bets and comment activity items and add them to the items in order of comment creation time:
|
||||||
const unorderedBetsAndComments = [...commentsWithoutBets, ...groupedBets]
|
const unorderedBetsAndComments = [...commentsWithoutBets, ...groupedBets]
|
||||||
let sortedBetsAndComments = _.sortBy(unorderedBetsAndComments, (item) => {
|
const sortedBetsAndComments = sortBy(unorderedBetsAndComments, (item) => {
|
||||||
if (item.type === 'comment') {
|
if (item.type === 'comment') {
|
||||||
return item.comment.createdTime
|
return item.comment.createdTime
|
||||||
} else if (item.type === 'bet') {
|
} else if (item.type === 'bet') {
|
||||||
|
@ -364,7 +366,6 @@ function getCommentThreads(
|
||||||
comments: Comment[],
|
comments: Comment[],
|
||||||
contract: Contract
|
contract: Contract
|
||||||
) {
|
) {
|
||||||
const betsByUserId = _.groupBy(bets, (bet) => bet.userId)
|
|
||||||
const parentComments = comments.filter((comment) => !comment.replyToCommentId)
|
const parentComments = comments.filter((comment) => !comment.replyToCommentId)
|
||||||
|
|
||||||
const items = parentComments.map((comment) => ({
|
const items = parentComments.map((comment) => ({
|
||||||
|
@ -373,7 +374,7 @@ function getCommentThreads(
|
||||||
contract: contract,
|
contract: contract,
|
||||||
comments: comments,
|
comments: comments,
|
||||||
parentComment: comment,
|
parentComment: comment,
|
||||||
betsByUserId: betsByUserId,
|
bets: bets,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return items
|
return items
|
||||||
|
@ -433,7 +434,7 @@ export function getAllContractActivityItems(
|
||||||
id: 'commentInput',
|
id: 'commentInput',
|
||||||
contract,
|
contract,
|
||||||
betsByCurrentUser: [],
|
betsByCurrentUser: [],
|
||||||
comments: [],
|
commentsByCurrentUser: [],
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
items.push(
|
items.push(
|
||||||
|
@ -459,7 +460,7 @@ export function getAllContractActivityItems(
|
||||||
id: 'commentInput',
|
id: 'commentInput',
|
||||||
contract,
|
contract,
|
||||||
betsByCurrentUser: [],
|
betsByCurrentUser: [],
|
||||||
comments: [],
|
commentsByCurrentUser: [],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -520,6 +521,15 @@ export function getRecentContractActivityItems(
|
||||||
return [questionItem, ...items]
|
return [questionItem, ...items]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function commentIsGeneralComment(comment: Comment, contract: Contract) {
|
||||||
|
return (
|
||||||
|
comment.answerOutcome === undefined &&
|
||||||
|
(contract.outcomeType === 'FREE_RESPONSE'
|
||||||
|
? comment.betId === undefined
|
||||||
|
: true)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function getSpecificContractActivityItems(
|
export function getSpecificContractActivityItems(
|
||||||
contract: Contract,
|
contract: Contract,
|
||||||
bets: Bet[],
|
bets: Bet[],
|
||||||
|
@ -530,7 +540,7 @@ export function getSpecificContractActivityItems(
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const { mode } = options
|
const { mode } = options
|
||||||
let items = [] as ActivityItem[]
|
const items = [] as ActivityItem[]
|
||||||
|
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case 'bets':
|
case 'bets':
|
||||||
|
@ -549,9 +559,9 @@ export function getSpecificContractActivityItems(
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'comments':
|
case 'comments': {
|
||||||
const nonFreeResponseComments = comments.filter(
|
const nonFreeResponseComments = comments.filter((comment) =>
|
||||||
(comment) => comment.answerOutcome === undefined
|
commentIsGeneralComment(comment, contract)
|
||||||
)
|
)
|
||||||
const nonFreeResponseBets =
|
const nonFreeResponseBets =
|
||||||
contract.outcomeType === 'FREE_RESPONSE' ? [] : bets
|
contract.outcomeType === 'FREE_RESPONSE' ? [] : bets
|
||||||
|
@ -567,12 +577,15 @@ export function getSpecificContractActivityItems(
|
||||||
type: 'commentInput',
|
type: 'commentInput',
|
||||||
id: 'commentInput',
|
id: 'commentInput',
|
||||||
contract,
|
contract,
|
||||||
betsByCurrentUser: user
|
betsByCurrentUser: nonFreeResponseBets.filter(
|
||||||
? nonFreeResponseBets.filter((bet) => bet.userId === user.id)
|
(bet) => bet.userId === user?.id
|
||||||
: [],
|
),
|
||||||
comments: nonFreeResponseComments,
|
commentsByCurrentUser: nonFreeResponseComments.filter(
|
||||||
|
(comment) => comment.userId === user?.id
|
||||||
|
),
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
|
}
|
||||||
case 'free-response-comment-answer-groups':
|
case 'free-response-comment-answer-groups':
|
||||||
items.push(
|
items.push(
|
||||||
...getAnswerAndCommentInputGroups(
|
...getAnswerAndCommentInputGroups(
|
||||||
|
|
|
@ -21,14 +21,11 @@ export function CopyLinkDateTimeComponent(props: {
|
||||||
event: React.MouseEvent<HTMLAnchorElement, MouseEvent>
|
event: React.MouseEvent<HTMLAnchorElement, MouseEvent>
|
||||||
) {
|
) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
const elementLocation = `https://${ENV_CONFIG.domain}${contractPath(
|
||||||
|
contract
|
||||||
|
)}#${elementId}`
|
||||||
|
|
||||||
let currentLocation = window.location.href.includes('/home')
|
copyToClipboard(elementLocation)
|
||||||
? `https://${ENV_CONFIG.domain}${contractPath(contract)}#${elementId}`
|
|
||||||
: window.location.href
|
|
||||||
if (currentLocation.includes('#')) {
|
|
||||||
currentLocation = currentLocation.split('#')[0]
|
|
||||||
}
|
|
||||||
copyToClipboard(`${currentLocation}#${elementId}`)
|
|
||||||
setShowToast(true)
|
setShowToast(true)
|
||||||
setTimeout(() => setShowToast(false), 2000)
|
setTimeout(() => setShowToast(false), 2000)
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,8 +17,11 @@ import { Linkify } from 'web/components/linkify'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { tradingAllowed } from 'web/lib/firebase/contracts'
|
import { tradingAllowed } from 'web/lib/firebase/contracts'
|
||||||
import { BuyButton } from 'web/components/yes-no-selector'
|
import { BuyButton } from 'web/components/yes-no-selector'
|
||||||
import { CommentInput, FeedItem } from 'web/components/feed/feed-items'
|
import { FeedItem } from 'web/components/feed/feed-items'
|
||||||
import { getMostRecentCommentableBet } from 'web/components/feed/feed-comments'
|
import {
|
||||||
|
CommentInput,
|
||||||
|
getMostRecentCommentableBet,
|
||||||
|
} from 'web/components/feed/feed-comments'
|
||||||
import { CopyLinkDateTimeComponent } from 'web/components/feed/copy-link-date-time'
|
import { CopyLinkDateTimeComponent } from 'web/components/feed/copy-link-date-time'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
|
|
||||||
|
@ -28,15 +31,16 @@ export function FeedAnswerCommentGroup(props: {
|
||||||
items: ActivityItem[]
|
items: ActivityItem[]
|
||||||
type: string
|
type: string
|
||||||
betsByCurrentUser?: Bet[]
|
betsByCurrentUser?: Bet[]
|
||||||
comments?: Comment[]
|
commentsByCurrentUser?: Comment[]
|
||||||
}) {
|
}) {
|
||||||
const { answer, items, contract, betsByCurrentUser, comments } = props
|
const { answer, items, contract, betsByCurrentUser, commentsByCurrentUser } =
|
||||||
|
props
|
||||||
const { username, avatarUrl, name, text } = answer
|
const { username, avatarUrl, name, text } = answer
|
||||||
const answerElementId = `answer-${answer.id}`
|
const answerElementId = `answer-${answer.id}`
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
const mostRecentCommentableBet = getMostRecentCommentableBet(
|
const mostRecentCommentableBet = getMostRecentCommentableBet(
|
||||||
betsByCurrentUser ?? [],
|
betsByCurrentUser ?? [],
|
||||||
comments ?? [],
|
commentsByCurrentUser ?? [],
|
||||||
user,
|
user,
|
||||||
answer.number + ''
|
answer.number + ''
|
||||||
)
|
)
|
||||||
|
@ -44,7 +48,7 @@ export function FeedAnswerCommentGroup(props: {
|
||||||
const probPercent = formatPercent(prob)
|
const probPercent = formatPercent(prob)
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const [showReply, setShowReply] = useState(false)
|
const [showReply, setShowReply] = useState(false)
|
||||||
const isFreeResponseContractPage = comments
|
const isFreeResponseContractPage = !!commentsByCurrentUser
|
||||||
if (mostRecentCommentableBet && !showReply) setShowReplyAndFocus(true)
|
if (mostRecentCommentableBet && !showReply) setShowReplyAndFocus(true)
|
||||||
const [inputRef, setInputRef] = useState<HTMLTextAreaElement | null>(null)
|
const [inputRef, setInputRef] = useState<HTMLTextAreaElement | null>(null)
|
||||||
|
|
||||||
|
@ -64,7 +68,7 @@ export function FeedAnswerCommentGroup(props: {
|
||||||
if (router.asPath.endsWith(`#${answerElementId}`)) {
|
if (router.asPath.endsWith(`#${answerElementId}`)) {
|
||||||
setHighlighted(true)
|
setHighlighted(true)
|
||||||
}
|
}
|
||||||
}, [router.asPath])
|
}, [answerElementId, router.asPath])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col className={'flex-1 gap-2'}>
|
<Col className={'flex-1 gap-2'}>
|
||||||
|
@ -174,7 +178,7 @@ export function FeedAnswerCommentGroup(props: {
|
||||||
<CommentInput
|
<CommentInput
|
||||||
contract={contract}
|
contract={contract}
|
||||||
betsByCurrentUser={betsByCurrentUser ?? []}
|
betsByCurrentUser={betsByCurrentUser ?? []}
|
||||||
comments={comments ?? []}
|
commentsByCurrentUser={commentsByCurrentUser ?? []}
|
||||||
answerOutcome={answer.number + ''}
|
answerOutcome={answer.number + ''}
|
||||||
replyToUsername={answer.username}
|
replyToUsername={answer.username}
|
||||||
setRef={setInputRef}
|
setRef={setInputRef}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user