Merge branch 'main' into atlas2
This commit is contained in:
commit
e5c3c782e6
|
@ -18,15 +18,24 @@ import {
|
||||||
getDpmProbabilityAfterSale,
|
getDpmProbabilityAfterSale,
|
||||||
} from './calculate-dpm'
|
} from './calculate-dpm'
|
||||||
import { calculateFixedPayout } from './calculate-fixed-payouts'
|
import { calculateFixedPayout } from './calculate-fixed-payouts'
|
||||||
import { Contract, BinaryContract, FreeResponseContract } from './contract'
|
import {
|
||||||
|
Contract,
|
||||||
|
BinaryContract,
|
||||||
|
FreeResponseContract,
|
||||||
|
PseudoNumericContract,
|
||||||
|
} from './contract'
|
||||||
|
|
||||||
export function getProbability(contract: BinaryContract) {
|
export function getProbability(
|
||||||
|
contract: BinaryContract | PseudoNumericContract
|
||||||
|
) {
|
||||||
return contract.mechanism === 'cpmm-1'
|
return contract.mechanism === 'cpmm-1'
|
||||||
? getCpmmProbability(contract.pool, contract.p)
|
? getCpmmProbability(contract.pool, contract.p)
|
||||||
: getDpmProbability(contract.totalShares)
|
: getDpmProbability(contract.totalShares)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getInitialProbability(contract: BinaryContract) {
|
export function getInitialProbability(
|
||||||
|
contract: BinaryContract | PseudoNumericContract
|
||||||
|
) {
|
||||||
if (contract.initialProbability) return contract.initialProbability
|
if (contract.initialProbability) return contract.initialProbability
|
||||||
|
|
||||||
if (contract.mechanism === 'dpm-2' || (contract as any).totalShares)
|
if (contract.mechanism === 'dpm-2' || (contract as any).totalShares)
|
||||||
|
@ -65,7 +74,9 @@ export function calculateShares(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function calculateSaleAmount(contract: Contract, bet: Bet) {
|
export function calculateSaleAmount(contract: Contract, bet: Bet) {
|
||||||
return contract.mechanism === 'cpmm-1' && contract.outcomeType === 'BINARY'
|
return contract.mechanism === 'cpmm-1' &&
|
||||||
|
(contract.outcomeType === 'BINARY' ||
|
||||||
|
contract.outcomeType === 'PSEUDO_NUMERIC')
|
||||||
? calculateCpmmSale(contract, Math.abs(bet.shares), bet.outcome).saleValue
|
? calculateCpmmSale(contract, Math.abs(bet.shares), bet.outcome).saleValue
|
||||||
: calculateDpmSaleAmount(contract, bet)
|
: calculateDpmSaleAmount(contract, bet)
|
||||||
}
|
}
|
||||||
|
@ -87,7 +98,9 @@ export function getProbabilityAfterSale(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function calculatePayout(contract: Contract, bet: Bet, outcome: string) {
|
export function calculatePayout(contract: Contract, bet: Bet, outcome: string) {
|
||||||
return contract.mechanism === 'cpmm-1' && contract.outcomeType === 'BINARY'
|
return contract.mechanism === 'cpmm-1' &&
|
||||||
|
(contract.outcomeType === 'BINARY' ||
|
||||||
|
contract.outcomeType === 'PSEUDO_NUMERIC')
|
||||||
? calculateFixedPayout(contract, bet, outcome)
|
? calculateFixedPayout(contract, bet, outcome)
|
||||||
: calculateDpmPayout(contract, bet, outcome)
|
: calculateDpmPayout(contract, bet, outcome)
|
||||||
}
|
}
|
||||||
|
@ -96,7 +109,9 @@ export function resolvedPayout(contract: Contract, bet: Bet) {
|
||||||
const outcome = contract.resolution
|
const outcome = contract.resolution
|
||||||
if (!outcome) throw new Error('Contract not resolved')
|
if (!outcome) throw new Error('Contract not resolved')
|
||||||
|
|
||||||
return contract.mechanism === 'cpmm-1' && contract.outcomeType === 'BINARY'
|
return contract.mechanism === 'cpmm-1' &&
|
||||||
|
(contract.outcomeType === 'BINARY' ||
|
||||||
|
contract.outcomeType === 'PSEUDO_NUMERIC')
|
||||||
? calculateFixedPayout(contract, bet, outcome)
|
? calculateFixedPayout(contract, bet, outcome)
|
||||||
: calculateDpmPayout(contract, bet, outcome)
|
: calculateDpmPayout(contract, bet, outcome)
|
||||||
}
|
}
|
||||||
|
@ -142,9 +157,7 @@ export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) {
|
||||||
const profit = payout + saleValue + redeemed - totalInvested
|
const profit = payout + saleValue + redeemed - totalInvested
|
||||||
const profitPercent = (profit / totalInvested) * 100
|
const profitPercent = (profit / totalInvested) * 100
|
||||||
|
|
||||||
const hasShares = Object.values(totalShares).some(
|
const hasShares = Object.values(totalShares).some((shares) => shares > 0)
|
||||||
(shares) => shares > 0
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
invested: Math.max(0, currentInvested),
|
invested: Math.max(0, currentInvested),
|
||||||
|
|
|
@ -2,9 +2,10 @@ import { Answer } from './answer'
|
||||||
import { Fees } from './fees'
|
import { Fees } from './fees'
|
||||||
|
|
||||||
export type AnyMechanism = DPM | CPMM
|
export type AnyMechanism = DPM | CPMM
|
||||||
export type AnyOutcomeType = Binary | FreeResponse | Numeric
|
export type AnyOutcomeType = Binary | PseudoNumeric | FreeResponse | Numeric
|
||||||
export type AnyContractType =
|
export type AnyContractType =
|
||||||
| (CPMM & Binary)
|
| (CPMM & Binary)
|
||||||
|
| (CPMM & PseudoNumeric)
|
||||||
| (DPM & Binary)
|
| (DPM & Binary)
|
||||||
| (DPM & FreeResponse)
|
| (DPM & FreeResponse)
|
||||||
| (DPM & Numeric)
|
| (DPM & Numeric)
|
||||||
|
@ -33,7 +34,7 @@ export type Contract<T extends AnyContractType = AnyContractType> = {
|
||||||
isResolved: boolean
|
isResolved: boolean
|
||||||
resolutionTime?: number // When the contract creator resolved the market
|
resolutionTime?: number // When the contract creator resolved the market
|
||||||
resolution?: string
|
resolution?: string
|
||||||
resolutionProbability?: number,
|
resolutionProbability?: number
|
||||||
|
|
||||||
closeEmailsSent?: number
|
closeEmailsSent?: number
|
||||||
|
|
||||||
|
@ -45,6 +46,7 @@ export type Contract<T extends AnyContractType = AnyContractType> = {
|
||||||
} & T
|
} & T
|
||||||
|
|
||||||
export type BinaryContract = Contract & Binary
|
export type BinaryContract = Contract & Binary
|
||||||
|
export type PseudoNumericContract = Contract & PseudoNumeric
|
||||||
export type NumericContract = Contract & Numeric
|
export type NumericContract = Contract & Numeric
|
||||||
export type FreeResponseContract = Contract & FreeResponse
|
export type FreeResponseContract = Contract & FreeResponse
|
||||||
export type DPMContract = Contract & DPM
|
export type DPMContract = Contract & DPM
|
||||||
|
@ -75,6 +77,18 @@ export type Binary = {
|
||||||
resolution?: resolution
|
resolution?: resolution
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PseudoNumeric = {
|
||||||
|
outcomeType: 'PSEUDO_NUMERIC'
|
||||||
|
min: number
|
||||||
|
max: number
|
||||||
|
isLogScale: boolean
|
||||||
|
resolutionValue?: number
|
||||||
|
|
||||||
|
// same as binary market; map everything to probability
|
||||||
|
initialProbability: number
|
||||||
|
resolutionProbability?: number
|
||||||
|
}
|
||||||
|
|
||||||
export type FreeResponse = {
|
export type FreeResponse = {
|
||||||
outcomeType: 'FREE_RESPONSE'
|
outcomeType: 'FREE_RESPONSE'
|
||||||
answers: Answer[] // Used for outcomeType 'FREE_RESPONSE'.
|
answers: Answer[] // Used for outcomeType 'FREE_RESPONSE'.
|
||||||
|
@ -94,7 +108,7 @@ export type Numeric = {
|
||||||
export type outcomeType = AnyOutcomeType['outcomeType']
|
export type outcomeType = AnyOutcomeType['outcomeType']
|
||||||
export type resolution = 'YES' | 'NO' | 'MKT' | 'CANCEL'
|
export type resolution = 'YES' | 'NO' | 'MKT' | 'CANCEL'
|
||||||
export const RESOLUTIONS = ['YES', 'NO', 'MKT', 'CANCEL'] as const
|
export const RESOLUTIONS = ['YES', 'NO', 'MKT', 'CANCEL'] as const
|
||||||
export const OUTCOME_TYPES = ['BINARY', 'FREE_RESPONSE', 'NUMERIC'] as const
|
export const OUTCOME_TYPES = ['BINARY', 'FREE_RESPONSE', 'PSEUDO_NUMERIC', '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
|
||||||
|
|
|
@ -14,6 +14,7 @@ import {
|
||||||
DPMBinaryContract,
|
DPMBinaryContract,
|
||||||
FreeResponseContract,
|
FreeResponseContract,
|
||||||
NumericContract,
|
NumericContract,
|
||||||
|
PseudoNumericContract,
|
||||||
} from './contract'
|
} from './contract'
|
||||||
import { noFees } from './fees'
|
import { noFees } from './fees'
|
||||||
import { addObjects } from './util/object'
|
import { addObjects } from './util/object'
|
||||||
|
@ -32,7 +33,7 @@ export type BetInfo = {
|
||||||
export const getNewBinaryCpmmBetInfo = (
|
export const getNewBinaryCpmmBetInfo = (
|
||||||
outcome: 'YES' | 'NO',
|
outcome: 'YES' | 'NO',
|
||||||
amount: number,
|
amount: number,
|
||||||
contract: CPMMBinaryContract,
|
contract: CPMMBinaryContract | PseudoNumericContract,
|
||||||
loanAmount: number
|
loanAmount: number
|
||||||
) => {
|
) => {
|
||||||
const { shares, newPool, newP, fees } = calculateCpmmPurchase(
|
const { shares, newPool, newP, fees } = calculateCpmmPurchase(
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
FreeResponse,
|
FreeResponse,
|
||||||
Numeric,
|
Numeric,
|
||||||
outcomeType,
|
outcomeType,
|
||||||
|
PseudoNumeric,
|
||||||
} from './contract'
|
} from './contract'
|
||||||
import { User } from './user'
|
import { User } from './user'
|
||||||
import { parseTags } from './util/parse'
|
import { parseTags } from './util/parse'
|
||||||
|
@ -27,7 +28,8 @@ export function getNewContract(
|
||||||
// used for numeric markets
|
// used for numeric markets
|
||||||
bucketCount: number,
|
bucketCount: number,
|
||||||
min: number,
|
min: number,
|
||||||
max: number
|
max: number,
|
||||||
|
isLogScale: boolean
|
||||||
) {
|
) {
|
||||||
const tags = parseTags(
|
const tags = parseTags(
|
||||||
`${question} ${description} ${extraTags.map((tag) => `#${tag}`).join(' ')}`
|
`${question} ${description} ${extraTags.map((tag) => `#${tag}`).join(' ')}`
|
||||||
|
@ -37,6 +39,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 === 'PSEUDO_NUMERIC'
|
||||||
|
? getPseudoNumericCpmmProps(initialProb, ante, min, max, isLogScale)
|
||||||
: outcomeType === 'NUMERIC'
|
: outcomeType === 'NUMERIC'
|
||||||
? getNumericProps(ante, bucketCount, min, max)
|
? getNumericProps(ante, bucketCount, min, max)
|
||||||
: getFreeAnswerProps(ante)
|
: getFreeAnswerProps(ante)
|
||||||
|
@ -111,6 +115,24 @@ const getBinaryCpmmProps = (initialProb: number, ante: number) => {
|
||||||
return system
|
return system
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getPseudoNumericCpmmProps = (
|
||||||
|
initialProb: number,
|
||||||
|
ante: number,
|
||||||
|
min: number,
|
||||||
|
max: number,
|
||||||
|
isLogScale: boolean
|
||||||
|
) => {
|
||||||
|
const system: CPMM & PseudoNumeric = {
|
||||||
|
...getBinaryCpmmProps(initialProb, ante),
|
||||||
|
outcomeType: 'PSEUDO_NUMERIC',
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
isLogScale,
|
||||||
|
}
|
||||||
|
|
||||||
|
return system
|
||||||
|
}
|
||||||
|
|
||||||
const getFreeAnswerProps = (ante: number) => {
|
const getFreeAnswerProps = (ante: number) => {
|
||||||
const system: DPM & FreeResponse = {
|
const system: DPM & FreeResponse = {
|
||||||
mechanism: 'dpm-2',
|
mechanism: 'dpm-2',
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
import { sumBy, groupBy, mapValues } from 'lodash'
|
import { sumBy, groupBy, mapValues } from 'lodash'
|
||||||
|
|
||||||
import { Bet, NumericBet } from './bet'
|
import { Bet, NumericBet } from './bet'
|
||||||
import { Contract, CPMMBinaryContract, DPMContract } from './contract'
|
import {
|
||||||
|
Contract,
|
||||||
|
CPMMBinaryContract,
|
||||||
|
DPMContract,
|
||||||
|
PseudoNumericContract,
|
||||||
|
} from './contract'
|
||||||
import { Fees } from './fees'
|
import { Fees } from './fees'
|
||||||
import { LiquidityProvision } from './liquidity-provision'
|
import { LiquidityProvision } from './liquidity-provision'
|
||||||
import {
|
import {
|
||||||
|
@ -56,7 +61,11 @@ export const getPayouts = (
|
||||||
},
|
},
|
||||||
resolutionProbability?: number
|
resolutionProbability?: number
|
||||||
): PayoutInfo => {
|
): PayoutInfo => {
|
||||||
if (contract.mechanism === 'cpmm-1' && contract.outcomeType === 'BINARY') {
|
if (
|
||||||
|
contract.mechanism === 'cpmm-1' &&
|
||||||
|
(contract.outcomeType === 'BINARY' ||
|
||||||
|
contract.outcomeType === 'PSEUDO_NUMERIC')
|
||||||
|
) {
|
||||||
return getFixedPayouts(
|
return getFixedPayouts(
|
||||||
outcome,
|
outcome,
|
||||||
contract,
|
contract,
|
||||||
|
@ -76,7 +85,7 @@ export const getPayouts = (
|
||||||
|
|
||||||
export const getFixedPayouts = (
|
export const getFixedPayouts = (
|
||||||
outcome: string | undefined,
|
outcome: string | undefined,
|
||||||
contract: CPMMBinaryContract,
|
contract: CPMMBinaryContract | PseudoNumericContract,
|
||||||
bets: Bet[],
|
bets: Bet[],
|
||||||
liquidities: LiquidityProvision[],
|
liquidities: LiquidityProvision[],
|
||||||
resolutionProbability?: number
|
resolutionProbability?: number
|
||||||
|
|
45
common/pseudo-numeric.ts
Normal file
45
common/pseudo-numeric.ts
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import { BinaryContract, PseudoNumericContract } from './contract'
|
||||||
|
import { formatLargeNumber, formatPercent } from './util/format'
|
||||||
|
|
||||||
|
export function formatNumericProbability(
|
||||||
|
p: number,
|
||||||
|
contract: PseudoNumericContract
|
||||||
|
) {
|
||||||
|
const value = getMappedValue(contract)(p)
|
||||||
|
return formatLargeNumber(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getMappedValue =
|
||||||
|
(contract: PseudoNumericContract | BinaryContract) => (p: number) => {
|
||||||
|
if (contract.outcomeType === 'BINARY') return p
|
||||||
|
|
||||||
|
const { min, max, isLogScale } = contract
|
||||||
|
|
||||||
|
if (isLogScale) {
|
||||||
|
const logValue = p * Math.log10(max - min)
|
||||||
|
return 10 ** logValue + min
|
||||||
|
}
|
||||||
|
|
||||||
|
return p * (max - min) + min
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getFormattedMappedValue =
|
||||||
|
(contract: PseudoNumericContract | BinaryContract) => (p: number) => {
|
||||||
|
if (contract.outcomeType === 'BINARY') return formatPercent(p)
|
||||||
|
|
||||||
|
const value = getMappedValue(contract)(p)
|
||||||
|
return formatLargeNumber(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getPseudoProbability = (
|
||||||
|
value: number,
|
||||||
|
min: number,
|
||||||
|
max: number,
|
||||||
|
isLogScale = false
|
||||||
|
) => {
|
||||||
|
if (isLogScale) {
|
||||||
|
return Math.log10(value - min) / Math.log10(max - min)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (value - min) / (max - min)
|
||||||
|
}
|
|
@ -21,11 +21,16 @@ service cloud.firestore {
|
||||||
allow update: if resource.data.id == request.auth.uid
|
allow update: if resource.data.id == request.auth.uid
|
||||||
&& request.resource.data.diff(resource.data).affectedKeys()
|
&& request.resource.data.diff(resource.data).affectedKeys()
|
||||||
.hasOnly(['bio', 'bannerUrl', 'website', 'twitterHandle', 'discordHandle', 'followedCategories', 'referredByContractId']);
|
.hasOnly(['bio', 'bannerUrl', 'website', 'twitterHandle', 'discordHandle', 'followedCategories', 'referredByContractId']);
|
||||||
// only one referral allowed per user
|
|
||||||
allow update: if resource.data.id == request.auth.uid
|
allow update: if resource.data.id == request.auth.uid
|
||||||
&& request.resource.data.diff(resource.data).affectedKeys()
|
&& request.resource.data.diff(resource.data).affectedKeys()
|
||||||
.hasOnly(['referredByUserId'])
|
.hasOnly(['referredByUserId'])
|
||||||
&& !("referredByUserId" in resource.data);
|
// only one referral allowed per user
|
||||||
|
&& !("referredByUserId" in resource.data)
|
||||||
|
// user can't refer themselves
|
||||||
|
&& (resource.data.id != request.resource.data.referredByUserId)
|
||||||
|
// user can't refer someone who referred them quid pro quo
|
||||||
|
&& get(/databases/$(database)/documents/users/$(request.resource.data.referredByUserId)).referredByUserId != resource.data.id;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
match /{somePath=**}/portfolioHistory/{portfolioHistoryId} {
|
match /{somePath=**}/portfolioHistory/{portfolioHistoryId} {
|
||||||
|
|
|
@ -23,8 +23,10 @@ Adapted from https://firebase.google.com/docs/functions/get-started
|
||||||
### For local development
|
### For local development
|
||||||
|
|
||||||
0. [Install](https://cloud.google.com/sdk/docs/install) gcloud CLI
|
0. [Install](https://cloud.google.com/sdk/docs/install) gcloud CLI
|
||||||
1. 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.`): 0. `$ brew install java`
|
1. 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. `$ sudo ln -sfn /opt/homebrew/opt/openjdk/libexec/openjdk.jdk /Library/Java/JavaVirtualMachines/openjdk.jdk`
|
|
||||||
|
1. `$ brew install java`
|
||||||
|
2. `$ sudo ln -sfn /opt/homebrew/opt/openjdk/libexec/openjdk.jdk /Library/Java/JavaVirtualMachines/openjdk.jdk`
|
||||||
2. `$ gcloud auth login` to authenticate the CLI tools to Google Cloud
|
2. `$ gcloud auth login` to authenticate the CLI tools to Google Cloud
|
||||||
3. `$ gcloud config set project <project-id>` to choose the project (`$ gcloud projects list` to see options)
|
3. `$ gcloud config set project <project-id>` to choose the project (`$ gcloud projects list` to see options)
|
||||||
4. `$ mkdir firestore_export` to create a folder to store the exported database
|
4. `$ mkdir firestore_export` to create a folder to store the exported database
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
"start": "yarn shell",
|
"start": "yarn shell",
|
||||||
"deploy": "firebase deploy --only functions",
|
"deploy": "firebase deploy --only functions",
|
||||||
"logs": "firebase functions:log",
|
"logs": "firebase functions:log",
|
||||||
"serve": "yarn build && firebase emulators:start --only functions,firestore --import=./firestore_export",
|
"serve": "firebase use dev && yarn build && firebase emulators:start --only functions,firestore --import=./firestore_export",
|
||||||
"db:update-local-from-remote": "yarn db:backup-remote && gsutil rsync -r gs://$npm_package_config_firestore/firestore_export ./firestore_export",
|
"db:update-local-from-remote": "yarn db:backup-remote && gsutil rsync -r gs://$npm_package_config_firestore/firestore_export ./firestore_export",
|
||||||
"db:backup-local": "firebase emulators:export --force ./firestore_export",
|
"db:backup-local": "firebase emulators:export --force ./firestore_export",
|
||||||
"db:rename-remote-backup-folder": "gsutil mv gs://$npm_package_config_firestore/firestore_export gs://$npm_package_config_firestore/firestore_export_$(date +%d-%m-%Y-%H-%M)",
|
"db:rename-remote-backup-folder": "gsutil mv gs://$npm_package_config_firestore/firestore_export gs://$npm_package_config_firestore/firestore_export_$(date +%d-%m-%Y-%H-%M)",
|
||||||
|
@ -23,7 +23,6 @@
|
||||||
"main": "functions/src/index.js",
|
"main": "functions/src/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@amplitude/node": "1.10.0",
|
"@amplitude/node": "1.10.0",
|
||||||
"fetch": "1.1.0",
|
|
||||||
"firebase-admin": "10.0.0",
|
"firebase-admin": "10.0.0",
|
||||||
"firebase-functions": "3.21.2",
|
"firebase-functions": "3.21.2",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
|
|
|
@ -39,7 +39,8 @@ export const addLiquidity = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
const contract = contractSnap.data() as Contract
|
const contract = contractSnap.data() as Contract
|
||||||
if (
|
if (
|
||||||
contract.mechanism !== 'cpmm-1' ||
|
contract.mechanism !== 'cpmm-1' ||
|
||||||
contract.outcomeType !== 'BINARY'
|
(contract.outcomeType !== 'BINARY' &&
|
||||||
|
contract.outcomeType !== 'PSEUDO_NUMERIC')
|
||||||
)
|
)
|
||||||
return { status: 'error', message: 'Invalid contract' }
|
return { status: 'error', message: 'Invalid contract' }
|
||||||
|
|
||||||
|
|
|
@ -18,46 +18,63 @@
|
||||||
|
|
||||||
import * as functions from 'firebase-functions'
|
import * as functions from 'firebase-functions'
|
||||||
import * as firestore from '@google-cloud/firestore'
|
import * as firestore from '@google-cloud/firestore'
|
||||||
const client = new firestore.v1.FirestoreAdminClient()
|
import { FirestoreAdminClient } from '@google-cloud/firestore/types/v1/firestore_admin_client'
|
||||||
|
|
||||||
const bucket = 'gs://manifold-firestore-backup'
|
export const backupDbCore = async (
|
||||||
|
client: FirestoreAdminClient,
|
||||||
export const backupDb = functions.pubsub
|
project: string,
|
||||||
.schedule('every 24 hours')
|
bucket: string
|
||||||
.onRun((_context) => {
|
) => {
|
||||||
const projectId = process.env.GCP_PROJECT || process.env.GCLOUD_PROJECT
|
const name = client.databasePath(project, '(default)')
|
||||||
if (projectId == null) {
|
const outputUriPrefix = `gs://${bucket}`
|
||||||
throw new Error('No project ID environment variable set.')
|
|
||||||
}
|
|
||||||
const databaseName = client.databasePath(projectId, '(default)')
|
|
||||||
|
|
||||||
return client
|
|
||||||
.exportDocuments({
|
|
||||||
name: databaseName,
|
|
||||||
outputUriPrefix: bucket,
|
|
||||||
// Leave collectionIds empty to export all collections
|
// Leave collectionIds empty to export all collections
|
||||||
// or set to a list of collection IDs to export,
|
// or set to a list of collection IDs to export,
|
||||||
// collectionIds: ['users', 'posts']
|
// collectionIds: ['users', 'posts']
|
||||||
// NOTE: Subcollections are not backed up by default
|
// NOTE: Subcollections are not backed up by default
|
||||||
collectionIds: [
|
const collectionIds = [
|
||||||
'contracts',
|
'contracts',
|
||||||
'groups',
|
'groups',
|
||||||
'private-users',
|
'private-users',
|
||||||
'stripe-transactions',
|
'stripe-transactions',
|
||||||
|
'transactions',
|
||||||
'users',
|
'users',
|
||||||
'bets',
|
'bets',
|
||||||
'comments',
|
'comments',
|
||||||
|
'follows',
|
||||||
'followers',
|
'followers',
|
||||||
'answers',
|
'answers',
|
||||||
'txns',
|
'txns',
|
||||||
],
|
'manalinks',
|
||||||
})
|
'liquidity',
|
||||||
.then((responses) => {
|
'stats',
|
||||||
|
'cache',
|
||||||
|
'latency',
|
||||||
|
'views',
|
||||||
|
'notifications',
|
||||||
|
'portfolioHistory',
|
||||||
|
'folds',
|
||||||
|
]
|
||||||
|
return await client.exportDocuments({ name, outputUriPrefix, collectionIds })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const backupDb = functions.pubsub
|
||||||
|
.schedule('every 24 hours')
|
||||||
|
.onRun(async (_context) => {
|
||||||
|
try {
|
||||||
|
const client = new firestore.v1.FirestoreAdminClient()
|
||||||
|
const project = process.env.GCP_PROJECT || process.env.GCLOUD_PROJECT
|
||||||
|
if (project == null) {
|
||||||
|
throw new Error('No project ID environment variable set.')
|
||||||
|
}
|
||||||
|
const responses = await backupDbCore(
|
||||||
|
client,
|
||||||
|
project,
|
||||||
|
'manifold-firestore-backup'
|
||||||
|
)
|
||||||
const response = responses[0]
|
const response = responses[0]
|
||||||
console.log(`Operation Name: ${response['name']}`)
|
console.log(`Operation Name: ${response['name']}`)
|
||||||
})
|
} catch (err) {
|
||||||
.catch((err) => {
|
|
||||||
console.error(err)
|
console.error(err)
|
||||||
throw new Error('Export operation failed')
|
throw new Error('Export operation failed')
|
||||||
})
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
import * as admin from 'firebase-admin'
|
|
||||||
|
|
||||||
import fetch from './fetch'
|
|
||||||
|
|
||||||
export const callCloudFunction = (functionName: string, data: unknown = {}) => {
|
|
||||||
const projectId = admin.instanceId().app.options.projectId
|
|
||||||
|
|
||||||
const url = `https://us-central1-${projectId}.cloudfunctions.net/${functionName}`
|
|
||||||
|
|
||||||
return fetch(url, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ data }),
|
|
||||||
}).then((response) => response.json())
|
|
||||||
}
|
|
|
@ -28,6 +28,7 @@ import { getNewContract } from '../../common/new-contract'
|
||||||
import { NUMERIC_BUCKET_COUNT } from '../../common/numeric-constants'
|
import { NUMERIC_BUCKET_COUNT } from '../../common/numeric-constants'
|
||||||
import { User } from '../../common/user'
|
import { User } from '../../common/user'
|
||||||
import { Group, MAX_ID_LENGTH } from '../../common/group'
|
import { Group, MAX_ID_LENGTH } from '../../common/group'
|
||||||
|
import { getPseudoProbability } from '../../common/pseudo-numeric'
|
||||||
|
|
||||||
const bodySchema = z.object({
|
const bodySchema = z.object({
|
||||||
question: z.string().min(1).max(MAX_QUESTION_LENGTH),
|
question: z.string().min(1).max(MAX_QUESTION_LENGTH),
|
||||||
|
@ -45,19 +46,31 @@ const binarySchema = z.object({
|
||||||
initialProb: z.number().min(1).max(99),
|
initialProb: z.number().min(1).max(99),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const finite = () => z.number().gte(Number.MIN_SAFE_INTEGER).lte(Number.MAX_SAFE_INTEGER)
|
||||||
|
|
||||||
const numericSchema = z.object({
|
const numericSchema = z.object({
|
||||||
min: z.number(),
|
min: finite(),
|
||||||
max: z.number(),
|
max: finite(),
|
||||||
|
initialValue: finite(),
|
||||||
|
isLogScale: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const createmarket = newEndpoint({}, async (req, auth) => {
|
export const createmarket = newEndpoint({}, async (req, auth) => {
|
||||||
const { question, description, tags, closeTime, outcomeType, groupId } =
|
const { question, description, tags, closeTime, outcomeType, groupId } =
|
||||||
validate(bodySchema, req.body)
|
validate(bodySchema, req.body)
|
||||||
|
|
||||||
let min, max, initialProb
|
let min, max, initialProb, isLogScale
|
||||||
if (outcomeType === 'NUMERIC') {
|
|
||||||
;({ min, max } = validate(numericSchema, req.body))
|
if (outcomeType === 'PSEUDO_NUMERIC' || outcomeType === 'NUMERIC') {
|
||||||
if (max - min <= 0.01) throw new APIError(400, 'Invalid range.')
|
let initialValue
|
||||||
|
;({ min, max, initialValue, isLogScale } = validate(
|
||||||
|
numericSchema,
|
||||||
|
req.body
|
||||||
|
))
|
||||||
|
if (max - min <= 0.01 || initialValue < min || initialValue > max)
|
||||||
|
throw new APIError(400, 'Invalid range.')
|
||||||
|
|
||||||
|
initialProb = getPseudoProbability(initialValue, min, max, isLogScale) * 100
|
||||||
}
|
}
|
||||||
if (outcomeType === 'BINARY') {
|
if (outcomeType === 'BINARY') {
|
||||||
;({ initialProb } = validate(binarySchema, req.body))
|
;({ initialProb } = validate(binarySchema, req.body))
|
||||||
|
@ -121,7 +134,8 @@ export const createmarket = newEndpoint({}, async (req, auth) => {
|
||||||
tags ?? [],
|
tags ?? [],
|
||||||
NUMERIC_BUCKET_COUNT,
|
NUMERIC_BUCKET_COUNT,
|
||||||
min ?? 0,
|
min ?? 0,
|
||||||
max ?? 0
|
max ?? 0,
|
||||||
|
isLogScale ?? false
|
||||||
)
|
)
|
||||||
|
|
||||||
if (ante) await chargeUser(user.id, ante, true)
|
if (ante) await chargeUser(user.id, ante, true)
|
||||||
|
@ -130,7 +144,7 @@ export const createmarket = newEndpoint({}, async (req, auth) => {
|
||||||
|
|
||||||
const providerId = user.id
|
const providerId = user.id
|
||||||
|
|
||||||
if (outcomeType === 'BINARY') {
|
if (outcomeType === 'BINARY' || outcomeType === 'PSEUDO_NUMERIC') {
|
||||||
const liquidityDoc = firestore
|
const liquidityDoc = firestore
|
||||||
.collection(`contracts/${contract.id}/liquidity`)
|
.collection(`contracts/${contract.id}/liquidity`)
|
||||||
.doc()
|
.doc()
|
||||||
|
|
|
@ -6,8 +6,13 @@ import { Comment } from '../../common/comment'
|
||||||
import { Contract } from '../../common/contract'
|
import { Contract } 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 {
|
||||||
|
formatLargeNumber,
|
||||||
|
formatMoney,
|
||||||
|
formatPercent,
|
||||||
|
} from '../../common/util/format'
|
||||||
import { getValueFromBucket } from '../../common/calculate-dpm'
|
import { getValueFromBucket } from '../../common/calculate-dpm'
|
||||||
|
import { formatNumericProbability } from '../../common/pseudo-numeric'
|
||||||
|
|
||||||
import { sendTemplateEmail } from './send-email'
|
import { sendTemplateEmail } from './send-email'
|
||||||
import { getPrivateUser, getUser } from './utils'
|
import { getPrivateUser, getUser } from './utils'
|
||||||
|
@ -101,6 +106,17 @@ const toDisplayResolution = (
|
||||||
return display || resolution
|
return display || resolution
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (contract.outcomeType === 'PSEUDO_NUMERIC') {
|
||||||
|
const { resolutionValue } = contract
|
||||||
|
|
||||||
|
return resolutionValue
|
||||||
|
? formatLargeNumber(resolutionValue)
|
||||||
|
: formatNumericProbability(
|
||||||
|
resolutionProbability ?? getProbability(contract),
|
||||||
|
contract
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (resolution === 'MKT' && resolutions) return 'MULTI'
|
if (resolution === 'MKT' && resolutions) return 'MULTI'
|
||||||
if (resolution === 'CANCEL') return 'N/A'
|
if (resolution === 'CANCEL') return 'N/A'
|
||||||
|
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
let fetchRequest: typeof fetch
|
|
||||||
|
|
||||||
try {
|
|
||||||
fetchRequest = fetch
|
|
||||||
} catch {
|
|
||||||
fetchRequest = require('node-fetch')
|
|
||||||
}
|
|
||||||
|
|
||||||
export default fetchRequest
|
|
|
@ -1,25 +0,0 @@
|
||||||
import * as functions from 'firebase-functions'
|
|
||||||
|
|
||||||
import { callCloudFunction } from './call-cloud-function'
|
|
||||||
|
|
||||||
export const keepAwake = functions.pubsub
|
|
||||||
.schedule('every 1 minutes')
|
|
||||||
.onRun(async () => {
|
|
||||||
await Promise.all([
|
|
||||||
callCloudFunction('placeBet'),
|
|
||||||
callCloudFunction('resolveMarket'),
|
|
||||||
callCloudFunction('sellBet'),
|
|
||||||
])
|
|
||||||
|
|
||||||
await sleep(30)
|
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
callCloudFunction('placeBet'),
|
|
||||||
callCloudFunction('resolveMarket'),
|
|
||||||
callCloudFunction('sellBet'),
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
const sleep = (seconds: number) => {
|
|
||||||
return new Promise((resolve) => setTimeout(resolve, seconds * 1000))
|
|
||||||
}
|
|
|
@ -41,10 +41,7 @@ export const placebet = newEndpoint({}, async (req, auth) => {
|
||||||
log('Inside main transaction.')
|
log('Inside main transaction.')
|
||||||
const contractDoc = firestore.doc(`contracts/${contractId}`)
|
const contractDoc = firestore.doc(`contracts/${contractId}`)
|
||||||
const userDoc = firestore.doc(`users/${auth.uid}`)
|
const userDoc = firestore.doc(`users/${auth.uid}`)
|
||||||
const [contractSnap, userSnap] = await Promise.all([
|
const [contractSnap, userSnap] = await trans.getAll(contractDoc, userDoc)
|
||||||
trans.get(contractDoc),
|
|
||||||
trans.get(userDoc),
|
|
||||||
])
|
|
||||||
if (!contractSnap.exists) throw new APIError(400, 'Contract not found.')
|
if (!contractSnap.exists) throw new APIError(400, 'Contract not found.')
|
||||||
if (!userSnap.exists) throw new APIError(400, 'User not found.')
|
if (!userSnap.exists) throw new APIError(400, 'User not found.')
|
||||||
log('Loaded user and contract snapshots.')
|
log('Loaded user and contract snapshots.')
|
||||||
|
@ -70,7 +67,10 @@ export const placebet = newEndpoint({}, async (req, auth) => {
|
||||||
if (outcomeType == 'BINARY' && mechanism == 'dpm-2') {
|
if (outcomeType == 'BINARY' && mechanism == 'dpm-2') {
|
||||||
const { outcome } = validate(binarySchema, req.body)
|
const { outcome } = validate(binarySchema, req.body)
|
||||||
return getNewBinaryDpmBetInfo(outcome, amount, contract, loanAmount)
|
return getNewBinaryDpmBetInfo(outcome, amount, contract, loanAmount)
|
||||||
} else if (outcomeType == 'BINARY' && mechanism == 'cpmm-1') {
|
} else if (
|
||||||
|
(outcomeType == 'BINARY' || outcomeType == 'PSEUDO_NUMERIC') &&
|
||||||
|
mechanism == 'cpmm-1'
|
||||||
|
) {
|
||||||
const { outcome } = validate(binarySchema, req.body)
|
const { outcome } = validate(binarySchema, req.body)
|
||||||
return getNewBinaryCpmmBetInfo(outcome, amount, contract, loanAmount)
|
return getNewBinaryCpmmBetInfo(outcome, amount, contract, loanAmount)
|
||||||
} else if (outcomeType == 'FREE_RESPONSE' && mechanism == 'dpm-2') {
|
} else if (outcomeType == 'FREE_RESPONSE' && mechanism == 'dpm-2') {
|
||||||
|
|
|
@ -16,7 +16,11 @@ export const redeemShares = async (userId: string, contractId: string) => {
|
||||||
return { status: 'error', message: 'Invalid contract' }
|
return { status: 'error', message: 'Invalid contract' }
|
||||||
|
|
||||||
const contract = contractSnap.data() as Contract
|
const contract = contractSnap.data() as Contract
|
||||||
if (contract.outcomeType !== 'BINARY' || contract.mechanism !== 'cpmm-1')
|
const { mechanism, outcomeType } = contract
|
||||||
|
if (
|
||||||
|
!(outcomeType === 'BINARY' || outcomeType === 'PSEUDO_NUMERIC') ||
|
||||||
|
mechanism !== 'cpmm-1'
|
||||||
|
)
|
||||||
return { status: 'success' }
|
return { status: 'success' }
|
||||||
|
|
||||||
const betsSnap = await transaction.get(
|
const betsSnap = await transaction.get(
|
||||||
|
|
|
@ -27,7 +27,7 @@ const bodySchema = z.object({
|
||||||
|
|
||||||
const binarySchema = z.object({
|
const binarySchema = z.object({
|
||||||
outcome: z.enum(RESOLUTIONS),
|
outcome: z.enum(RESOLUTIONS),
|
||||||
probabilityInt: z.number().gte(0).lt(100).optional(),
|
probabilityInt: z.number().gte(0).lte(100).optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const freeResponseSchema = z.union([
|
const freeResponseSchema = z.union([
|
||||||
|
@ -39,7 +39,7 @@ const freeResponseSchema = z.union([
|
||||||
resolutions: z.array(
|
resolutions: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
answer: z.number().int().nonnegative(),
|
answer: z.number().int().nonnegative(),
|
||||||
pct: z.number().gte(0).lt(100),
|
pct: z.number().gte(0).lte(100),
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
|
@ -53,7 +53,19 @@ const numericSchema = z.object({
|
||||||
value: z.number().optional(),
|
value: z.number().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const pseudoNumericSchema = z.union([
|
||||||
|
z.object({
|
||||||
|
outcome: z.literal('CANCEL'),
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
outcome: z.literal('MKT'),
|
||||||
|
value: z.number(),
|
||||||
|
probabilityInt: z.number().gte(0).lte(100),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
|
||||||
const opts = { secrets: ['MAILGUN_KEY'] }
|
const opts = { secrets: ['MAILGUN_KEY'] }
|
||||||
|
|
||||||
export const resolvemarket = newEndpoint(opts, async (req, auth) => {
|
export const resolvemarket = newEndpoint(opts, async (req, auth) => {
|
||||||
const { contractId } = validate(bodySchema, req.body)
|
const { contractId } = validate(bodySchema, req.body)
|
||||||
const userId = auth.uid
|
const userId = auth.uid
|
||||||
|
@ -221,12 +233,18 @@ const sendResolutionEmails = async (
|
||||||
|
|
||||||
function getResolutionParams(contract: Contract, body: string) {
|
function getResolutionParams(contract: Contract, body: string) {
|
||||||
const { outcomeType } = contract
|
const { outcomeType } = contract
|
||||||
|
|
||||||
if (outcomeType === 'NUMERIC') {
|
if (outcomeType === 'NUMERIC') {
|
||||||
return {
|
return {
|
||||||
...validate(numericSchema, body),
|
...validate(numericSchema, body),
|
||||||
resolutions: undefined,
|
resolutions: undefined,
|
||||||
probabilityInt: undefined,
|
probabilityInt: undefined,
|
||||||
}
|
}
|
||||||
|
} else if (outcomeType === 'PSEUDO_NUMERIC') {
|
||||||
|
return {
|
||||||
|
...validate(pseudoNumericSchema, body),
|
||||||
|
resolutions: undefined,
|
||||||
|
}
|
||||||
} else if (outcomeType === 'FREE_RESPONSE') {
|
} else if (outcomeType === 'FREE_RESPONSE') {
|
||||||
const freeResponseParams = validate(freeResponseSchema, body)
|
const freeResponseParams = validate(freeResponseSchema, body)
|
||||||
const { outcome } = freeResponseParams
|
const { outcome } = freeResponseParams
|
||||||
|
|
16
functions/src/scripts/backup-db.ts
Normal file
16
functions/src/scripts/backup-db.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import * as firestore from '@google-cloud/firestore'
|
||||||
|
import { getServiceAccountCredentials } from './script-init'
|
||||||
|
import { backupDbCore } from '../backup-db'
|
||||||
|
|
||||||
|
async function backupDb() {
|
||||||
|
const credentials = getServiceAccountCredentials()
|
||||||
|
const projectId = credentials.project_id
|
||||||
|
const client = new firestore.v1.FirestoreAdminClient({ credentials })
|
||||||
|
const bucket = 'manifold-firestore-backup'
|
||||||
|
const resp = await backupDbCore(client, projectId, bucket)
|
||||||
|
console.log(`Operation: ${resp[0]['name']}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
backupDb().then(() => process.exit())
|
||||||
|
}
|
|
@ -47,26 +47,29 @@ const getFirebaseActiveProject = (cwd: string) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const initAdmin = (env?: string) => {
|
export const getServiceAccountCredentials = (env?: string) => {
|
||||||
env = env || getFirebaseActiveProject(process.cwd())
|
env = env || getFirebaseActiveProject(process.cwd())
|
||||||
if (env == null) {
|
if (env == null) {
|
||||||
console.error(
|
throw new Error(
|
||||||
"Couldn't find active Firebase project; did you do `firebase use <alias>?`"
|
"Couldn't find active Firebase project; did you do `firebase use <alias>?`"
|
||||||
)
|
)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
const envVar = `GOOGLE_APPLICATION_CREDENTIALS_${env.toUpperCase()}`
|
const envVar = `GOOGLE_APPLICATION_CREDENTIALS_${env.toUpperCase()}`
|
||||||
const keyPath = process.env[envVar]
|
const keyPath = process.env[envVar]
|
||||||
if (keyPath == null) {
|
if (keyPath == null) {
|
||||||
console.error(
|
throw new Error(
|
||||||
`Please set the ${envVar} environment variable to contain the path to your ${env} environment key file.`
|
`Please set the ${envVar} environment variable to contain the path to your ${env} environment key file.`
|
||||||
)
|
)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
console.log(`Initializing connection to ${env} Firebase...`)
|
|
||||||
/* eslint-disable-next-line @typescript-eslint/no-var-requires */
|
/* eslint-disable-next-line @typescript-eslint/no-var-requires */
|
||||||
const serviceAccount = require(keyPath)
|
return require(keyPath)
|
||||||
admin.initializeApp({
|
}
|
||||||
|
|
||||||
|
export const initAdmin = (env?: string) => {
|
||||||
|
const serviceAccount = getServiceAccountCredentials(env)
|
||||||
|
console.log(`Initializing connection to ${serviceAccount.project_id}...`)
|
||||||
|
return admin.initializeApp({
|
||||||
|
projectId: serviceAccount.project_id,
|
||||||
credential: admin.credential.cert(serviceAccount),
|
credential: admin.credential.cert(serviceAccount),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,53 +0,0 @@
|
||||||
import * as admin from 'firebase-admin'
|
|
||||||
|
|
||||||
import { initAdmin } from './script-init'
|
|
||||||
initAdmin()
|
|
||||||
|
|
||||||
import { getValues } from '../utils'
|
|
||||||
import { User } from '../../../common/user'
|
|
||||||
import { batchedWaitAll } from '../../../common/util/promise'
|
|
||||||
import { Contract } from '../../../common/contract'
|
|
||||||
import { updateWordScores } from '../update-recommendations'
|
|
||||||
import { computeFeed } from '../update-feed'
|
|
||||||
import { getFeedContracts, getTaggedContracts } from '../get-feed-data'
|
|
||||||
import { CATEGORY_LIST } from '../../../common/categories'
|
|
||||||
|
|
||||||
const firestore = admin.firestore()
|
|
||||||
|
|
||||||
async function updateFeed() {
|
|
||||||
console.log('Updating feed')
|
|
||||||
|
|
||||||
const contracts = await getValues<Contract>(firestore.collection('contracts'))
|
|
||||||
const feedContracts = await getFeedContracts()
|
|
||||||
const users = await getValues<User>(
|
|
||||||
firestore.collection('users').where('username', '==', 'JamesGrugett')
|
|
||||||
)
|
|
||||||
|
|
||||||
await batchedWaitAll(
|
|
||||||
users.map((user) => async () => {
|
|
||||||
console.log('Updating recs for', user.username)
|
|
||||||
await updateWordScores(user, contracts)
|
|
||||||
console.log('Updating feed for', user.username)
|
|
||||||
await computeFeed(user, feedContracts)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
console.log('Updating feed categories!')
|
|
||||||
|
|
||||||
await batchedWaitAll(
|
|
||||||
users.map((user) => async () => {
|
|
||||||
for (const category of CATEGORY_LIST) {
|
|
||||||
const contracts = await getTaggedContracts(category)
|
|
||||||
const feed = await computeFeed(user, contracts)
|
|
||||||
await firestore
|
|
||||||
.collection(`private-users/${user.id}/cache`)
|
|
||||||
.doc(`feed-${category}`)
|
|
||||||
.set({ feed })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (require.main === module) {
|
|
||||||
updateFeed().then(() => process.exit())
|
|
||||||
}
|
|
|
@ -21,11 +21,11 @@ export const sellbet = newEndpoint({}, async (req, auth) => {
|
||||||
const contractDoc = firestore.doc(`contracts/${contractId}`)
|
const contractDoc = firestore.doc(`contracts/${contractId}`)
|
||||||
const userDoc = firestore.doc(`users/${auth.uid}`)
|
const userDoc = firestore.doc(`users/${auth.uid}`)
|
||||||
const betDoc = firestore.doc(`contracts/${contractId}/bets/${betId}`)
|
const betDoc = firestore.doc(`contracts/${contractId}/bets/${betId}`)
|
||||||
const [contractSnap, userSnap, betSnap] = await Promise.all([
|
const [contractSnap, userSnap, betSnap] = await transaction.getAll(
|
||||||
transaction.get(contractDoc),
|
contractDoc,
|
||||||
transaction.get(userDoc),
|
userDoc,
|
||||||
transaction.get(betDoc),
|
betDoc
|
||||||
])
|
)
|
||||||
if (!contractSnap.exists) throw new APIError(400, 'Contract not found.')
|
if (!contractSnap.exists) throw new APIError(400, 'Contract not found.')
|
||||||
if (!userSnap.exists) throw new APIError(400, 'User not found.')
|
if (!userSnap.exists) throw new APIError(400, 'User not found.')
|
||||||
if (!betSnap.exists) throw new APIError(400, 'Bet not found.')
|
if (!betSnap.exists) throw new APIError(400, 'Bet not found.')
|
||||||
|
|
|
@ -24,9 +24,8 @@ export const sellshares = newEndpoint({}, async (req, auth) => {
|
||||||
const contractDoc = firestore.doc(`contracts/${contractId}`)
|
const contractDoc = firestore.doc(`contracts/${contractId}`)
|
||||||
const userDoc = firestore.doc(`users/${auth.uid}`)
|
const userDoc = firestore.doc(`users/${auth.uid}`)
|
||||||
const betsQ = contractDoc.collection('bets').where('userId', '==', auth.uid)
|
const betsQ = contractDoc.collection('bets').where('userId', '==', auth.uid)
|
||||||
const [contractSnap, userSnap, userBets] = await Promise.all([
|
const [[contractSnap, userSnap], userBets] = await Promise.all([
|
||||||
transaction.get(contractDoc),
|
transaction.getAll(contractDoc, userDoc),
|
||||||
transaction.get(userDoc),
|
|
||||||
getValues<Bet>(betsQ), // TODO: why is this not in the transaction??
|
getValues<Bet>(betsQ), // TODO: why is this not in the transaction??
|
||||||
])
|
])
|
||||||
if (!contractSnap.exists) throw new APIError(400, 'Contract not found.')
|
if (!contractSnap.exists) throw new APIError(400, 'Contract not found.')
|
||||||
|
|
|
@ -1,220 +0,0 @@
|
||||||
import * as functions from 'firebase-functions'
|
|
||||||
import * as admin from 'firebase-admin'
|
|
||||||
import { flatten, shuffle, sortBy, uniq, zip, zipObject } from 'lodash'
|
|
||||||
|
|
||||||
import { getValue, getValues } from './utils'
|
|
||||||
import { Contract } from '../../common/contract'
|
|
||||||
import { logInterpolation } from '../../common/util/math'
|
|
||||||
import { DAY_MS } from '../../common/util/time'
|
|
||||||
import {
|
|
||||||
getProbability,
|
|
||||||
getOutcomeProbability,
|
|
||||||
getTopAnswer,
|
|
||||||
} from '../../common/calculate'
|
|
||||||
import { User } from '../../common/user'
|
|
||||||
import {
|
|
||||||
getContractScore,
|
|
||||||
MAX_FEED_CONTRACTS,
|
|
||||||
} from '../../common/recommended-contracts'
|
|
||||||
import { callCloudFunction } from './call-cloud-function'
|
|
||||||
import {
|
|
||||||
getFeedContracts,
|
|
||||||
getRecentBetsAndComments,
|
|
||||||
getTaggedContracts,
|
|
||||||
} from './get-feed-data'
|
|
||||||
import { CATEGORY_LIST } from '../../common/categories'
|
|
||||||
|
|
||||||
const firestore = admin.firestore()
|
|
||||||
|
|
||||||
const BATCH_SIZE = 30
|
|
||||||
const MAX_BATCHES = 50
|
|
||||||
|
|
||||||
const getUserBatches = async () => {
|
|
||||||
const users = shuffle(await getValues<User>(firestore.collection('users')))
|
|
||||||
const userBatches: User[][] = []
|
|
||||||
for (let i = 0; i < users.length; i += BATCH_SIZE) {
|
|
||||||
userBatches.push(users.slice(i, i + BATCH_SIZE))
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('updating feed batches', MAX_BATCHES, 'of', userBatches.length)
|
|
||||||
|
|
||||||
return userBatches.slice(0, MAX_BATCHES)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const updateFeed = functions.pubsub
|
|
||||||
.schedule('every 60 minutes')
|
|
||||||
.onRun(async () => {
|
|
||||||
const userBatches = await getUserBatches()
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
userBatches.map((users) =>
|
|
||||||
callCloudFunction('updateFeedBatch', { users })
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
console.log('updating category feed')
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
CATEGORY_LIST.map((category) =>
|
|
||||||
callCloudFunction('updateCategoryFeed', {
|
|
||||||
category,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
export const updateFeedBatch = functions.https.onCall(
|
|
||||||
async (data: { users: User[] }) => {
|
|
||||||
const { users } = data
|
|
||||||
const contracts = await getFeedContracts()
|
|
||||||
const feeds = await getNewFeeds(users, contracts)
|
|
||||||
await Promise.all(
|
|
||||||
zip(users, feeds).map(([user, feed]) =>
|
|
||||||
/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */
|
|
||||||
getUserCacheCollection(user!).doc('feed').set({ feed })
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
export const updateCategoryFeed = functions.https.onCall(
|
|
||||||
async (data: { category: string }) => {
|
|
||||||
const { category } = data
|
|
||||||
const userBatches = await getUserBatches()
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
userBatches.map(async (users) => {
|
|
||||||
await callCloudFunction('updateCategoryFeedBatch', {
|
|
||||||
users,
|
|
||||||
category,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
export const updateCategoryFeedBatch = functions.https.onCall(
|
|
||||||
async (data: { users: User[]; category: string }) => {
|
|
||||||
const { users, category } = data
|
|
||||||
const contracts = await getTaggedContracts(category)
|
|
||||||
const feeds = await getNewFeeds(users, contracts)
|
|
||||||
await Promise.all(
|
|
||||||
zip(users, feeds).map(([user, feed]) =>
|
|
||||||
/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */
|
|
||||||
getUserCacheCollection(user!).doc(`feed-${category}`).set({ feed })
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const getNewFeeds = async (users: User[], contracts: Contract[]) => {
|
|
||||||
const feeds = await Promise.all(users.map((u) => computeFeed(u, contracts)))
|
|
||||||
const contractIds = uniq(flatten(feeds).map((c) => c.id))
|
|
||||||
const data = await Promise.all(contractIds.map(getRecentBetsAndComments))
|
|
||||||
const dataByContractId = zipObject(contractIds, data)
|
|
||||||
return feeds.map((feed) =>
|
|
||||||
feed.map((contract) => {
|
|
||||||
return { contract, ...dataByContractId[contract.id] }
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getUserCacheCollection = (user: User) =>
|
|
||||||
firestore.collection(`private-users/${user.id}/cache`)
|
|
||||||
|
|
||||||
export const computeFeed = async (user: User, contracts: Contract[]) => {
|
|
||||||
const userCacheCollection = getUserCacheCollection(user)
|
|
||||||
|
|
||||||
const [wordScores, lastViewedTime] = await Promise.all([
|
|
||||||
getValue<{ [word: string]: number }>(userCacheCollection.doc('wordScores')),
|
|
||||||
getValue<{ [contractId: string]: number }>(
|
|
||||||
userCacheCollection.doc('lastViewTime')
|
|
||||||
),
|
|
||||||
]).then((dicts) => dicts.map((dict) => dict ?? {}))
|
|
||||||
|
|
||||||
const scoredContracts = contracts.map((contract) => {
|
|
||||||
const score = scoreContract(
|
|
||||||
contract,
|
|
||||||
wordScores,
|
|
||||||
lastViewedTime[contract.id]
|
|
||||||
)
|
|
||||||
return [contract, score] as [Contract, number]
|
|
||||||
})
|
|
||||||
|
|
||||||
const sortedContracts = sortBy(
|
|
||||||
scoredContracts,
|
|
||||||
([_, score]) => score
|
|
||||||
).reverse()
|
|
||||||
|
|
||||||
// console.log(sortedContracts.map(([c, score]) => c.question + ': ' + score))
|
|
||||||
|
|
||||||
return sortedContracts.slice(0, MAX_FEED_CONTRACTS).map(([c]) => c)
|
|
||||||
}
|
|
||||||
|
|
||||||
function scoreContract(
|
|
||||||
contract: Contract,
|
|
||||||
wordScores: { [word: string]: number },
|
|
||||||
viewTime: number | undefined
|
|
||||||
) {
|
|
||||||
const recommendationScore = getContractScore(contract, wordScores)
|
|
||||||
const activityScore = getActivityScore(contract, viewTime)
|
|
||||||
// const lastViewedScore = getLastViewedScore(viewTime)
|
|
||||||
return recommendationScore * activityScore
|
|
||||||
}
|
|
||||||
|
|
||||||
function getActivityScore(contract: Contract, viewTime: number | undefined) {
|
|
||||||
const { createdTime, lastBetTime, lastCommentTime, outcomeType } = contract
|
|
||||||
const hasNewComments =
|
|
||||||
lastCommentTime && (!viewTime || lastCommentTime > viewTime)
|
|
||||||
const newCommentScore = hasNewComments ? 1 : 0.5
|
|
||||||
|
|
||||||
const timeSinceLastComment = Date.now() - (lastCommentTime ?? createdTime)
|
|
||||||
const commentDaysAgo = timeSinceLastComment / DAY_MS
|
|
||||||
const commentTimeScore =
|
|
||||||
0.25 + 0.75 * (1 - logInterpolation(0, 3, commentDaysAgo))
|
|
||||||
|
|
||||||
const timeSinceLastBet = Date.now() - (lastBetTime ?? createdTime)
|
|
||||||
const betDaysAgo = timeSinceLastBet / DAY_MS
|
|
||||||
const betTimeScore = 0.5 + 0.5 * (1 - logInterpolation(0, 3, betDaysAgo))
|
|
||||||
|
|
||||||
let prob = 0.5
|
|
||||||
if (outcomeType === 'BINARY') {
|
|
||||||
prob = getProbability(contract)
|
|
||||||
} else if (outcomeType === 'FREE_RESPONSE') {
|
|
||||||
const topAnswer = getTopAnswer(contract)
|
|
||||||
if (topAnswer)
|
|
||||||
prob = Math.max(0.5, getOutcomeProbability(contract, topAnswer.id))
|
|
||||||
}
|
|
||||||
const frac = 1 - Math.abs(prob - 0.5) ** 2 / 0.25
|
|
||||||
const probScore = 0.5 + frac * 0.5
|
|
||||||
|
|
||||||
const { volume24Hours, volume7Days } = contract
|
|
||||||
const combinedVolume = Math.log(volume24Hours + 1) + Math.log(volume7Days + 1)
|
|
||||||
const volumeScore = 0.5 + 0.5 * logInterpolation(4, 20, combinedVolume)
|
|
||||||
|
|
||||||
const score =
|
|
||||||
newCommentScore * commentTimeScore * betTimeScore * probScore * volumeScore
|
|
||||||
|
|
||||||
// Map score to [0.5, 1] since no recent activty is not a deal breaker.
|
|
||||||
const mappedScore = 0.5 + 0.5 * score
|
|
||||||
const newMappedScore = 0.7 + 0.3 * score
|
|
||||||
|
|
||||||
const isNew = Date.now() < contract.createdTime + DAY_MS
|
|
||||||
return isNew ? newMappedScore : mappedScore
|
|
||||||
}
|
|
||||||
|
|
||||||
// function getLastViewedScore(viewTime: number | undefined) {
|
|
||||||
// if (viewTime === undefined) {
|
|
||||||
// return 1
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const daysAgo = (Date.now() - viewTime) / DAY_MS
|
|
||||||
|
|
||||||
// if (daysAgo < 0.5) {
|
|
||||||
// const frac = logInterpolation(0, 0.5, daysAgo)
|
|
||||||
// return 0.5 + 0.25 * frac
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const frac = logInterpolation(0.5, 14, daysAgo)
|
|
||||||
// return 0.75 + 0.25 * frac
|
|
||||||
// }
|
|
|
@ -1,70 +0,0 @@
|
||||||
import * as functions from 'firebase-functions'
|
|
||||||
import * as admin from 'firebase-admin'
|
|
||||||
|
|
||||||
import { getValue, getValues } from './utils'
|
|
||||||
import { Contract } from '../../common/contract'
|
|
||||||
import { Bet } from '../../common/bet'
|
|
||||||
import { User } from '../../common/user'
|
|
||||||
import { ClickEvent } from '../../common/tracking'
|
|
||||||
import { getWordScores } from '../../common/recommended-contracts'
|
|
||||||
import { batchedWaitAll } from '../../common/util/promise'
|
|
||||||
import { callCloudFunction } from './call-cloud-function'
|
|
||||||
|
|
||||||
const firestore = admin.firestore()
|
|
||||||
|
|
||||||
export const updateRecommendations = functions.pubsub
|
|
||||||
.schedule('every 24 hours')
|
|
||||||
.onRun(async () => {
|
|
||||||
const users = await getValues<User>(firestore.collection('users'))
|
|
||||||
|
|
||||||
const batchSize = 100
|
|
||||||
const userBatches: User[][] = []
|
|
||||||
for (let i = 0; i < users.length; i += batchSize) {
|
|
||||||
userBatches.push(users.slice(i, i + batchSize))
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
userBatches.map((batch) =>
|
|
||||||
callCloudFunction('updateRecommendationsBatch', { users: batch })
|
|
||||||
)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
export const updateRecommendationsBatch = functions.https.onCall(
|
|
||||||
async (data: { users: User[] }) => {
|
|
||||||
const { users } = data
|
|
||||||
|
|
||||||
const contracts = await getValues<Contract>(
|
|
||||||
firestore.collection('contracts')
|
|
||||||
)
|
|
||||||
|
|
||||||
await batchedWaitAll(
|
|
||||||
users.map((user) => () => updateWordScores(user, contracts))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
export const updateWordScores = async (user: User, contracts: Contract[]) => {
|
|
||||||
const [bets, viewCounts, clicks] = await Promise.all([
|
|
||||||
getValues<Bet>(
|
|
||||||
firestore.collectionGroup('bets').where('userId', '==', user.id)
|
|
||||||
),
|
|
||||||
|
|
||||||
getValue<{ [contractId: string]: number }>(
|
|
||||||
firestore.doc(`private-users/${user.id}/cache/viewCounts`)
|
|
||||||
),
|
|
||||||
|
|
||||||
getValues<ClickEvent>(
|
|
||||||
firestore
|
|
||||||
.collection(`private-users/${user.id}/events`)
|
|
||||||
.where('type', '==', 'click')
|
|
||||||
),
|
|
||||||
])
|
|
||||||
|
|
||||||
const wordScores = getWordScores(contracts, viewCounts ?? {}, clicks, bets)
|
|
||||||
|
|
||||||
const cachedCollection = firestore.collection(
|
|
||||||
`private-users/${user.id}/cache`
|
|
||||||
)
|
|
||||||
await cachedCollection.doc('wordScores').set(wordScores)
|
|
||||||
}
|
|
|
@ -3,7 +3,11 @@ import React, { useEffect, useState } from 'react'
|
||||||
import { partition, sumBy } from 'lodash'
|
import { partition, sumBy } from 'lodash'
|
||||||
|
|
||||||
import { useUser } from 'web/hooks/use-user'
|
import { useUser } from 'web/hooks/use-user'
|
||||||
import { BinaryContract, CPMMBinaryContract } from 'common/contract'
|
import {
|
||||||
|
BinaryContract,
|
||||||
|
CPMMBinaryContract,
|
||||||
|
PseudoNumericContract,
|
||||||
|
} from 'common/contract'
|
||||||
import { Col } from './layout/col'
|
import { Col } from './layout/col'
|
||||||
import { Row } from './layout/row'
|
import { Row } from './layout/row'
|
||||||
import { Spacer } from './layout/spacer'
|
import { Spacer } from './layout/spacer'
|
||||||
|
@ -21,7 +25,7 @@ import { APIError, placeBet } from 'web/lib/firebase/api-call'
|
||||||
import { sellShares } from 'web/lib/firebase/api-call'
|
import { sellShares } from 'web/lib/firebase/api-call'
|
||||||
import { AmountInput, BuyAmountInput } from './amount-input'
|
import { AmountInput, BuyAmountInput } from './amount-input'
|
||||||
import { InfoTooltip } from './info-tooltip'
|
import { InfoTooltip } from './info-tooltip'
|
||||||
import { BinaryOutcomeLabel } from './outcome-label'
|
import { BinaryOutcomeLabel, PseudoNumericOutcomeLabel } from './outcome-label'
|
||||||
import {
|
import {
|
||||||
calculatePayoutAfterCorrectBet,
|
calculatePayoutAfterCorrectBet,
|
||||||
calculateShares,
|
calculateShares,
|
||||||
|
@ -35,6 +39,7 @@ import {
|
||||||
getCpmmProbability,
|
getCpmmProbability,
|
||||||
getCpmmLiquidityFee,
|
getCpmmLiquidityFee,
|
||||||
} from 'common/calculate-cpmm'
|
} from 'common/calculate-cpmm'
|
||||||
|
import { getFormattedMappedValue } from 'common/pseudo-numeric'
|
||||||
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'
|
import { SignUpPrompt } from './sign-up-prompt'
|
||||||
|
@ -42,7 +47,7 @@ import { isIOS } from 'web/lib/util/device'
|
||||||
import { track } from 'web/lib/service/analytics'
|
import { track } from 'web/lib/service/analytics'
|
||||||
|
|
||||||
export function BetPanel(props: {
|
export function BetPanel(props: {
|
||||||
contract: BinaryContract
|
contract: BinaryContract | PseudoNumericContract
|
||||||
className?: string
|
className?: string
|
||||||
}) {
|
}) {
|
||||||
const { contract, className } = props
|
const { contract, className } = props
|
||||||
|
@ -81,7 +86,7 @@ export function BetPanel(props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BetPanelSwitcher(props: {
|
export function BetPanelSwitcher(props: {
|
||||||
contract: BinaryContract
|
contract: BinaryContract | PseudoNumericContract
|
||||||
className?: string
|
className?: string
|
||||||
title?: string // Set if BetPanel is on a feed modal
|
title?: string // Set if BetPanel is on a feed modal
|
||||||
selected?: 'YES' | 'NO'
|
selected?: 'YES' | 'NO'
|
||||||
|
@ -89,7 +94,8 @@ export function BetPanelSwitcher(props: {
|
||||||
}) {
|
}) {
|
||||||
const { contract, className, title, selected, onBetSuccess } = props
|
const { contract, className, title, selected, onBetSuccess } = props
|
||||||
|
|
||||||
const { mechanism } = contract
|
const { mechanism, outcomeType } = contract
|
||||||
|
const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC'
|
||||||
|
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
const userBets = useUserContractBets(user?.id, contract.id)
|
const userBets = useUserContractBets(user?.id, contract.id)
|
||||||
|
@ -122,7 +128,12 @@ export function BetPanelSwitcher(props: {
|
||||||
<Row className="items-center justify-between gap-2">
|
<Row className="items-center justify-between gap-2">
|
||||||
<div>
|
<div>
|
||||||
You have {formatWithCommas(floorShares)}{' '}
|
You have {formatWithCommas(floorShares)}{' '}
|
||||||
<BinaryOutcomeLabel outcome={sharesOutcome} /> shares
|
{isPseudoNumeric ? (
|
||||||
|
<PseudoNumericOutcomeLabel outcome={sharesOutcome} />
|
||||||
|
) : (
|
||||||
|
<BinaryOutcomeLabel outcome={sharesOutcome} />
|
||||||
|
)}{' '}
|
||||||
|
shares
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{tradeType === 'BUY' && (
|
{tradeType === 'BUY' && (
|
||||||
|
@ -190,12 +201,13 @@ export function BetPanelSwitcher(props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
function BuyPanel(props: {
|
function BuyPanel(props: {
|
||||||
contract: BinaryContract
|
contract: BinaryContract | PseudoNumericContract
|
||||||
user: User | null | undefined
|
user: User | null | undefined
|
||||||
selected?: 'YES' | 'NO'
|
selected?: 'YES' | 'NO'
|
||||||
onBuySuccess?: () => void
|
onBuySuccess?: () => void
|
||||||
}) {
|
}) {
|
||||||
const { contract, user, selected, onBuySuccess } = props
|
const { contract, user, selected, onBuySuccess } = props
|
||||||
|
const isPseudoNumeric = contract.outcomeType === 'PSEUDO_NUMERIC'
|
||||||
|
|
||||||
const [betChoice, setBetChoice] = useState<'YES' | 'NO' | undefined>(selected)
|
const [betChoice, setBetChoice] = useState<'YES' | 'NO' | undefined>(selected)
|
||||||
const [betAmount, setBetAmount] = useState<number | undefined>(undefined)
|
const [betAmount, setBetAmount] = useState<number | undefined>(undefined)
|
||||||
|
@ -302,6 +314,9 @@ function BuyPanel(props: {
|
||||||
: 0)
|
: 0)
|
||||||
)} ${betChoice ?? 'YES'} shares`
|
)} ${betChoice ?? 'YES'} shares`
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
|
const format = getFormattedMappedValue(contract)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<YesNoSelector
|
<YesNoSelector
|
||||||
|
@ -309,6 +324,7 @@ function BuyPanel(props: {
|
||||||
btnClassName="flex-1"
|
btnClassName="flex-1"
|
||||||
selected={betChoice}
|
selected={betChoice}
|
||||||
onSelect={(choice) => onBetChoice(choice)}
|
onSelect={(choice) => onBetChoice(choice)}
|
||||||
|
isPseudoNumeric={isPseudoNumeric}
|
||||||
/>
|
/>
|
||||||
<div className="my-3 text-left text-sm text-gray-500">Amount</div>
|
<div className="my-3 text-left text-sm text-gray-500">Amount</div>
|
||||||
<BuyAmountInput
|
<BuyAmountInput
|
||||||
|
@ -323,11 +339,13 @@ function BuyPanel(props: {
|
||||||
|
|
||||||
<Col className="mt-3 w-full gap-3">
|
<Col className="mt-3 w-full gap-3">
|
||||||
<Row className="items-center justify-between text-sm">
|
<Row className="items-center justify-between text-sm">
|
||||||
<div className="text-gray-500">Probability</div>
|
<div className="text-gray-500">
|
||||||
|
{isPseudoNumeric ? 'Estimated value' : 'Probability'}
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{formatPercent(initialProb)}
|
{format(initialProb)}
|
||||||
<span className="mx-2">→</span>
|
<span className="mx-2">→</span>
|
||||||
{formatPercent(resultProb)}
|
{format(resultProb)}
|
||||||
</div>
|
</div>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
|
@ -340,6 +358,8 @@ function BuyPanel(props: {
|
||||||
<br /> payout if{' '}
|
<br /> payout if{' '}
|
||||||
<BinaryOutcomeLabel outcome={betChoice ?? 'YES'} />
|
<BinaryOutcomeLabel outcome={betChoice ?? 'YES'} />
|
||||||
</>
|
</>
|
||||||
|
) : isPseudoNumeric ? (
|
||||||
|
'Max payout'
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
Payout if <BinaryOutcomeLabel outcome={betChoice ?? 'YES'} />
|
Payout if <BinaryOutcomeLabel outcome={betChoice ?? 'YES'} />
|
||||||
|
@ -389,7 +409,7 @@ function BuyPanel(props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SellPanel(props: {
|
export function SellPanel(props: {
|
||||||
contract: CPMMBinaryContract
|
contract: CPMMBinaryContract | PseudoNumericContract
|
||||||
userBets: Bet[]
|
userBets: Bet[]
|
||||||
shares: number
|
shares: number
|
||||||
sharesOutcome: 'YES' | 'NO'
|
sharesOutcome: 'YES' | 'NO'
|
||||||
|
@ -488,6 +508,10 @@ export function SellPanel(props: {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { outcomeType } = contract
|
||||||
|
const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC'
|
||||||
|
const format = getFormattedMappedValue(contract)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AmountInput
|
<AmountInput
|
||||||
|
@ -511,11 +535,13 @@ export function SellPanel(props: {
|
||||||
<span className="text-neutral">{formatMoney(saleValue)}</span>
|
<span className="text-neutral">{formatMoney(saleValue)}</span>
|
||||||
</Row>
|
</Row>
|
||||||
<Row className="items-center justify-between">
|
<Row className="items-center justify-between">
|
||||||
<div className="text-gray-500">Probability</div>
|
<div className="text-gray-500">
|
||||||
|
{isPseudoNumeric ? 'Estimated value' : 'Probability'}
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{formatPercent(initialProb)}
|
{format(initialProb)}
|
||||||
<span className="mx-2">→</span>
|
<span className="mx-2">→</span>
|
||||||
{formatPercent(resultProb)}
|
{format(resultProb)}
|
||||||
</div>
|
</div>
|
||||||
</Row>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
|
@ -3,7 +3,7 @@ import clsx from 'clsx'
|
||||||
|
|
||||||
import { BetPanelSwitcher } from './bet-panel'
|
import { BetPanelSwitcher } from './bet-panel'
|
||||||
import { YesNoSelector } from './yes-no-selector'
|
import { YesNoSelector } from './yes-no-selector'
|
||||||
import { BinaryContract } from 'common/contract'
|
import { BinaryContract, PseudoNumericContract } from 'common/contract'
|
||||||
import { Modal } from './layout/modal'
|
import { Modal } from './layout/modal'
|
||||||
import { SellButton } from './sell-button'
|
import { SellButton } from './sell-button'
|
||||||
import { useUser } from 'web/hooks/use-user'
|
import { useUser } from 'web/hooks/use-user'
|
||||||
|
@ -12,7 +12,7 @@ import { useSaveShares } from './use-save-shares'
|
||||||
|
|
||||||
// Inline version of a bet panel. Opens BetPanel in a new modal.
|
// Inline version of a bet panel. Opens BetPanel in a new modal.
|
||||||
export default function BetRow(props: {
|
export default function BetRow(props: {
|
||||||
contract: BinaryContract
|
contract: BinaryContract | PseudoNumericContract
|
||||||
className?: string
|
className?: string
|
||||||
btnClassName?: string
|
btnClassName?: string
|
||||||
betPanelClassName?: string
|
betPanelClassName?: string
|
||||||
|
@ -32,6 +32,7 @@ export default function BetRow(props: {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<YesNoSelector
|
<YesNoSelector
|
||||||
|
isPseudoNumeric={contract.outcomeType === 'PSEUDO_NUMERIC'}
|
||||||
className={clsx('justify-end', className)}
|
className={clsx('justify-end', className)}
|
||||||
btnClassName={clsx('btn-sm w-24', btnClassName)}
|
btnClassName={clsx('btn-sm w-24', btnClassName)}
|
||||||
onSelect={(choice) => {
|
onSelect={(choice) => {
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { useUserBets } from 'web/hooks/use-user-bets'
|
||||||
import { Bet } from 'web/lib/firebase/bets'
|
import { Bet } from 'web/lib/firebase/bets'
|
||||||
import { User } from 'web/lib/firebase/users'
|
import { User } from 'web/lib/firebase/users'
|
||||||
import {
|
import {
|
||||||
|
formatLargeNumber,
|
||||||
formatMoney,
|
formatMoney,
|
||||||
formatPercent,
|
formatPercent,
|
||||||
formatWithCommas,
|
formatWithCommas,
|
||||||
|
@ -40,6 +41,7 @@ import {
|
||||||
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'
|
import { NumericContract } from 'common/contract'
|
||||||
|
import { formatNumericProbability } from 'common/pseudo-numeric'
|
||||||
import { useUser } from 'web/hooks/use-user'
|
import { useUser } from 'web/hooks/use-user'
|
||||||
import { SellSharesModal } from './sell-modal'
|
import { SellSharesModal } from './sell-modal'
|
||||||
|
|
||||||
|
@ -366,6 +368,7 @@ export function BetsSummary(props: {
|
||||||
const { contract, isYourBets, className } = props
|
const { contract, isYourBets, className } = props
|
||||||
const { resolution, closeTime, outcomeType, mechanism } = contract
|
const { resolution, closeTime, outcomeType, mechanism } = contract
|
||||||
const isBinary = outcomeType === 'BINARY'
|
const isBinary = outcomeType === 'BINARY'
|
||||||
|
const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC'
|
||||||
const isCpmm = mechanism === 'cpmm-1'
|
const isCpmm = mechanism === 'cpmm-1'
|
||||||
const isClosed = closeTime && Date.now() > closeTime
|
const isClosed = closeTime && Date.now() > closeTime
|
||||||
|
|
||||||
|
@ -427,6 +430,25 @@ export function BetsSummary(props: {
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
</>
|
</>
|
||||||
|
) : isPseudoNumeric ? (
|
||||||
|
<>
|
||||||
|
<Col>
|
||||||
|
<div className="whitespace-nowrap text-sm text-gray-500">
|
||||||
|
Payout if {'>='} {formatLargeNumber(contract.max)}
|
||||||
|
</div>
|
||||||
|
<div className="whitespace-nowrap">
|
||||||
|
{formatMoney(yesWinnings)}
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
<Col>
|
||||||
|
<div className="whitespace-nowrap text-sm text-gray-500">
|
||||||
|
Payout if {'<='} {formatLargeNumber(contract.min)}
|
||||||
|
</div>
|
||||||
|
<div className="whitespace-nowrap">
|
||||||
|
{formatMoney(noWinnings)}
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Col>
|
<Col>
|
||||||
<div className="whitespace-nowrap text-sm text-gray-500">
|
<div className="whitespace-nowrap text-sm text-gray-500">
|
||||||
|
@ -507,13 +529,15 @@ export function ContractBetsTable(props: {
|
||||||
const { isResolved, mechanism, outcomeType } = contract
|
const { isResolved, mechanism, outcomeType } = contract
|
||||||
const isCPMM = mechanism === 'cpmm-1'
|
const isCPMM = mechanism === 'cpmm-1'
|
||||||
const isNumeric = outcomeType === 'NUMERIC'
|
const isNumeric = outcomeType === 'NUMERIC'
|
||||||
|
const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx('overflow-x-auto', className)}>
|
<div className={clsx('overflow-x-auto', className)}>
|
||||||
{amountRedeemed > 0 && (
|
{amountRedeemed > 0 && (
|
||||||
<>
|
<>
|
||||||
<div className="pl-2 text-sm text-gray-500">
|
<div className="pl-2 text-sm text-gray-500">
|
||||||
{amountRedeemed} YES shares and {amountRedeemed} NO shares
|
{amountRedeemed} {isPseudoNumeric ? 'HIGHER' : 'YES'} shares and{' '}
|
||||||
|
{amountRedeemed} {isPseudoNumeric ? 'LOWER' : 'NO'} shares
|
||||||
automatically redeemed for {formatMoney(amountRedeemed)}.
|
automatically redeemed for {formatMoney(amountRedeemed)}.
|
||||||
</div>
|
</div>
|
||||||
<Spacer h={4} />
|
<Spacer h={4} />
|
||||||
|
@ -541,7 +565,7 @@ export function ContractBetsTable(props: {
|
||||||
)}
|
)}
|
||||||
{!isCPMM && !isResolved && <th>Payout if chosen</th>}
|
{!isCPMM && !isResolved && <th>Payout if chosen</th>}
|
||||||
<th>Shares</th>
|
<th>Shares</th>
|
||||||
<th>Probability</th>
|
{!isPseudoNumeric && <th>Probability</th>}
|
||||||
<th>Date</th>
|
<th>Date</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -585,6 +609,7 @@ function BetRow(props: {
|
||||||
|
|
||||||
const isCPMM = mechanism === 'cpmm-1'
|
const isCPMM = mechanism === 'cpmm-1'
|
||||||
const isNumeric = outcomeType === 'NUMERIC'
|
const isNumeric = outcomeType === 'NUMERIC'
|
||||||
|
const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC'
|
||||||
|
|
||||||
const saleAmount = saleBet?.sale?.amount
|
const saleAmount = saleBet?.sale?.amount
|
||||||
|
|
||||||
|
@ -628,14 +653,18 @@ function BetRow(props: {
|
||||||
truncate="short"
|
truncate="short"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{isPseudoNumeric &&
|
||||||
|
' than ' + formatNumericProbability(bet.probAfter, contract)}
|
||||||
</td>
|
</td>
|
||||||
<td>{formatMoney(Math.abs(amount))}</td>
|
<td>{formatMoney(Math.abs(amount))}</td>
|
||||||
{!isCPMM && !isNumeric && <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>
|
||||||
|
{!isPseudoNumeric && (
|
||||||
<td>
|
<td>
|
||||||
{formatPercent(probBefore)} → {formatPercent(probAfter)}
|
{formatPercent(probBefore)} → {formatPercent(probAfter)}
|
||||||
</td>
|
</td>
|
||||||
|
)}
|
||||||
<td>{dayjs(createdTime).format('MMM D, h:mma')}</td>
|
<td>{dayjs(createdTime).format('MMM D, h:mma')}</td>
|
||||||
</tr>
|
</tr>
|
||||||
)
|
)
|
||||||
|
|
|
@ -9,7 +9,7 @@ import {
|
||||||
useSortBy,
|
useSortBy,
|
||||||
} from 'react-instantsearch-hooks-web'
|
} from 'react-instantsearch-hooks-web'
|
||||||
|
|
||||||
import { Contract } from '../../common/contract'
|
import { Contract } from 'common/contract'
|
||||||
import {
|
import {
|
||||||
Sort,
|
Sort,
|
||||||
useInitialQueryAndSort,
|
useInitialQueryAndSort,
|
||||||
|
@ -58,15 +58,24 @@ export function ContractSearch(props: {
|
||||||
additionalFilter?: {
|
additionalFilter?: {
|
||||||
creatorId?: string
|
creatorId?: string
|
||||||
tag?: string
|
tag?: string
|
||||||
|
excludeContractIds?: string[]
|
||||||
}
|
}
|
||||||
showCategorySelector: boolean
|
showCategorySelector: boolean
|
||||||
onContractClick?: (contract: Contract) => void
|
onContractClick?: (contract: Contract) => void
|
||||||
|
showPlaceHolder?: boolean
|
||||||
|
hideOrderSelector?: boolean
|
||||||
|
overrideGridClassName?: string
|
||||||
|
hideQuickBet?: boolean
|
||||||
}) {
|
}) {
|
||||||
const {
|
const {
|
||||||
querySortOptions,
|
querySortOptions,
|
||||||
additionalFilter,
|
additionalFilter,
|
||||||
showCategorySelector,
|
showCategorySelector,
|
||||||
onContractClick,
|
onContractClick,
|
||||||
|
overrideGridClassName,
|
||||||
|
hideOrderSelector,
|
||||||
|
showPlaceHolder,
|
||||||
|
hideQuickBet,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
@ -136,6 +145,7 @@ export function ContractSearch(props: {
|
||||||
<Row className="gap-1 sm:gap-2">
|
<Row className="gap-1 sm:gap-2">
|
||||||
<SearchBox
|
<SearchBox
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
|
placeholder={showPlaceHolder ? `Search ${filter} contracts` : ''}
|
||||||
classNames={{
|
classNames={{
|
||||||
form: 'before:top-6',
|
form: 'before:top-6',
|
||||||
input: '!pl-10 !input !input-bordered shadow-none w-[100px]',
|
input: '!pl-10 !input !input-bordered shadow-none w-[100px]',
|
||||||
|
@ -153,6 +163,7 @@ export function ContractSearch(props: {
|
||||||
<option value="resolved">Resolved</option>
|
<option value="resolved">Resolved</option>
|
||||||
<option value="all">All</option>
|
<option value="all">All</option>
|
||||||
</select>
|
</select>
|
||||||
|
{!hideOrderSelector && (
|
||||||
<SortBy
|
<SortBy
|
||||||
items={sortIndexes}
|
items={sortIndexes}
|
||||||
classNames={{
|
classNames={{
|
||||||
|
@ -160,6 +171,7 @@ export function ContractSearch(props: {
|
||||||
}}
|
}}
|
||||||
onBlur={trackCallback('select search sort')}
|
onBlur={trackCallback('select search sort')}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
<Configure
|
<Configure
|
||||||
facetFilters={filters}
|
facetFilters={filters}
|
||||||
numericFilters={numericFilters}
|
numericFilters={numericFilters}
|
||||||
|
@ -187,6 +199,9 @@ export function ContractSearch(props: {
|
||||||
<ContractSearchInner
|
<ContractSearchInner
|
||||||
querySortOptions={querySortOptions}
|
querySortOptions={querySortOptions}
|
||||||
onContractClick={onContractClick}
|
onContractClick={onContractClick}
|
||||||
|
overrideGridClassName={overrideGridClassName}
|
||||||
|
hideQuickBet={hideQuickBet}
|
||||||
|
excludeContractIds={additionalFilter?.excludeContractIds}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</InstantSearch>
|
</InstantSearch>
|
||||||
|
@ -199,8 +214,17 @@ export function ContractSearchInner(props: {
|
||||||
shouldLoadFromStorage?: boolean
|
shouldLoadFromStorage?: boolean
|
||||||
}
|
}
|
||||||
onContractClick?: (contract: Contract) => void
|
onContractClick?: (contract: Contract) => void
|
||||||
|
overrideGridClassName?: string
|
||||||
|
hideQuickBet?: boolean
|
||||||
|
excludeContractIds?: string[]
|
||||||
}) {
|
}) {
|
||||||
const { querySortOptions, onContractClick } = props
|
const {
|
||||||
|
querySortOptions,
|
||||||
|
onContractClick,
|
||||||
|
overrideGridClassName,
|
||||||
|
hideQuickBet,
|
||||||
|
excludeContractIds,
|
||||||
|
} = props
|
||||||
const { initialQuery } = useInitialQueryAndSort(querySortOptions)
|
const { initialQuery } = useInitialQueryAndSort(querySortOptions)
|
||||||
|
|
||||||
const { query, setQuery, setSort } = useUpdateQueryAndSort({
|
const { query, setQuery, setSort } = useUpdateQueryAndSort({
|
||||||
|
@ -239,7 +263,7 @@ export function ContractSearchInner(props: {
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const { showMore, hits, isLastPage } = useInfiniteHits()
|
const { showMore, hits, isLastPage } = useInfiniteHits()
|
||||||
const contracts = hits as any as Contract[]
|
let contracts = hits as any as Contract[]
|
||||||
|
|
||||||
if (isInitialLoad && contracts.length === 0) return <></>
|
if (isInitialLoad && contracts.length === 0) return <></>
|
||||||
|
|
||||||
|
@ -249,6 +273,9 @@ export function ContractSearchInner(props: {
|
||||||
? 'resolve-date'
|
? 'resolve-date'
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
|
if (excludeContractIds)
|
||||||
|
contracts = contracts.filter((c) => !excludeContractIds.includes(c.id))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ContractsGrid
|
<ContractsGrid
|
||||||
contracts={contracts}
|
contracts={contracts}
|
||||||
|
@ -256,6 +283,8 @@ export function ContractSearchInner(props: {
|
||||||
hasMore={!isLastPage}
|
hasMore={!isLastPage}
|
||||||
showTime={showTime}
|
showTime={showTime}
|
||||||
onContractClick={onContractClick}
|
onContractClick={onContractClick}
|
||||||
|
overrideGridClassName={overrideGridClassName}
|
||||||
|
hideQuickBet={hideQuickBet}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {
|
||||||
BinaryContract,
|
BinaryContract,
|
||||||
FreeResponseContract,
|
FreeResponseContract,
|
||||||
NumericContract,
|
NumericContract,
|
||||||
|
PseudoNumericContract,
|
||||||
} from 'common/contract'
|
} from 'common/contract'
|
||||||
import {
|
import {
|
||||||
AnswerLabel,
|
AnswerLabel,
|
||||||
|
@ -16,7 +17,11 @@ import {
|
||||||
CancelLabel,
|
CancelLabel,
|
||||||
FreeResponseOutcomeLabel,
|
FreeResponseOutcomeLabel,
|
||||||
} from '../outcome-label'
|
} from '../outcome-label'
|
||||||
import { getOutcomeProbability, getTopAnswer } from 'common/calculate'
|
import {
|
||||||
|
getOutcomeProbability,
|
||||||
|
getProbability,
|
||||||
|
getTopAnswer,
|
||||||
|
} from 'common/calculate'
|
||||||
import { AvatarDetails, MiscDetails, ShowTime } from './contract-details'
|
import { AvatarDetails, MiscDetails, ShowTime } from './contract-details'
|
||||||
import { getExpectedValue, getValueFromBucket } from 'common/calculate-dpm'
|
import { getExpectedValue, getValueFromBucket } from 'common/calculate-dpm'
|
||||||
import { QuickBet, ProbBar, getColor } from './quick-bet'
|
import { QuickBet, ProbBar, getColor } from './quick-bet'
|
||||||
|
@ -24,6 +29,7 @@ import { useContractWithPreload } from 'web/hooks/use-contract'
|
||||||
import { useUser } from 'web/hooks/use-user'
|
import { useUser } from 'web/hooks/use-user'
|
||||||
import { track } from '@amplitude/analytics-browser'
|
import { track } from '@amplitude/analytics-browser'
|
||||||
import { trackCallback } from 'web/lib/service/analytics'
|
import { trackCallback } from 'web/lib/service/analytics'
|
||||||
|
import { formatNumericProbability } from 'common/pseudo-numeric'
|
||||||
|
|
||||||
export function ContractCard(props: {
|
export function ContractCard(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
|
@ -131,6 +137,13 @@ export function ContractCard(props: {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{outcomeType === 'PSEUDO_NUMERIC' && (
|
||||||
|
<PseudoNumericResolutionOrExpectation
|
||||||
|
className="items-center"
|
||||||
|
contract={contract}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{outcomeType === 'NUMERIC' && (
|
{outcomeType === 'NUMERIC' && (
|
||||||
<NumericResolutionOrExpectation
|
<NumericResolutionOrExpectation
|
||||||
className="items-center"
|
className="items-center"
|
||||||
|
@ -270,7 +283,9 @@ export function NumericResolutionOrExpectation(props: {
|
||||||
{resolution === 'CANCEL' ? (
|
{resolution === 'CANCEL' ? (
|
||||||
<CancelLabel />
|
<CancelLabel />
|
||||||
) : (
|
) : (
|
||||||
<div className="text-blue-400">{resolutionValue}</div>
|
<div className="text-blue-400">
|
||||||
|
{formatLargeNumber(resolutionValue)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
@ -284,3 +299,42 @@ export function NumericResolutionOrExpectation(props: {
|
||||||
</Col>
|
</Col>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function PseudoNumericResolutionOrExpectation(props: {
|
||||||
|
contract: PseudoNumericContract
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
const { contract, className } = props
|
||||||
|
const { resolution, resolutionValue, resolutionProbability } = contract
|
||||||
|
const textColor = `text-blue-400`
|
||||||
|
|
||||||
|
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
|
||||||
|
? formatLargeNumber(resolutionValue)
|
||||||
|
: formatNumericProbability(
|
||||||
|
resolutionProbability ?? 0,
|
||||||
|
contract
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className={clsx('text-3xl', textColor)}>
|
||||||
|
{formatNumericProbability(getProbability(contract), contract)}
|
||||||
|
</div>
|
||||||
|
<div className={clsx('text-base', textColor)}>expected</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Col>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -130,9 +130,32 @@ export function ContractDetails(props: {
|
||||||
const { contract, bets, isCreator, disabled } = props
|
const { contract, bets, isCreator, disabled } = props
|
||||||
const { closeTime, creatorName, creatorUsername, creatorId } = contract
|
const { closeTime, creatorName, creatorUsername, creatorId } = contract
|
||||||
const { volumeLabel, resolvedDate } = contractMetrics(contract)
|
const { volumeLabel, resolvedDate } = contractMetrics(contract)
|
||||||
// Find a group that this contract id is in
|
|
||||||
const groups = useGroupsWithContract(contract.id)
|
const groups = (useGroupsWithContract(contract.id) ?? []).sort((g1, g2) => {
|
||||||
|
return g2.createdTime - g1.createdTime
|
||||||
|
})
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
|
||||||
|
const groupsUserIsMemberOf = groups
|
||||||
|
? groups.filter((g) => g.memberIds.includes(contract.creatorId))
|
||||||
|
: []
|
||||||
|
const groupsUserIsCreatorOf = groups
|
||||||
|
? groups.filter((g) => g.creatorId === contract.creatorId)
|
||||||
|
: []
|
||||||
|
|
||||||
|
// Priorities for which group the contract belongs to:
|
||||||
|
// In order of created most recently
|
||||||
|
// Group that the contract owner created
|
||||||
|
// Group the contract owner is a member of
|
||||||
|
// Any group the contract is in
|
||||||
|
const groupToDisplay =
|
||||||
|
groupsUserIsCreatorOf.length > 0
|
||||||
|
? groupsUserIsCreatorOf[0]
|
||||||
|
: groupsUserIsMemberOf.length > 0
|
||||||
|
? groupsUserIsMemberOf[0]
|
||||||
|
: groups
|
||||||
|
? groups[0]
|
||||||
|
: undefined
|
||||||
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">
|
||||||
<Row className="items-center gap-2">
|
<Row className="items-center gap-2">
|
||||||
|
@ -153,14 +176,15 @@ export function ContractDetails(props: {
|
||||||
)}
|
)}
|
||||||
{!disabled && <UserFollowButton userId={creatorId} small />}
|
{!disabled && <UserFollowButton userId={creatorId} small />}
|
||||||
</Row>
|
</Row>
|
||||||
{/*// TODO: we can add contracts to multiple groups but only show the first it was added to*/}
|
{groupToDisplay ? (
|
||||||
{groups && groups.length > 0 && (
|
|
||||||
<Row className={'line-clamp-1 mt-1 max-w-[200px]'}>
|
<Row className={'line-clamp-1 mt-1 max-w-[200px]'}>
|
||||||
<SiteLink href={`${groupPath(groups[0].slug)}`}>
|
<SiteLink href={`${groupPath(groupToDisplay.slug)}`}>
|
||||||
<UserGroupIcon className="mx-1 mb-1 inline h-5 w-5" />
|
<UserGroupIcon className="mx-1 mb-1 inline h-5 w-5" />
|
||||||
<span>{groups[0].name}</span>
|
<span>{groupToDisplay.name}</span>
|
||||||
</SiteLink>
|
</SiteLink>
|
||||||
</Row>
|
</Row>
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(!!closeTime || !!resolvedDate) && (
|
{(!!closeTime || !!resolvedDate) && (
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
FreeResponseResolutionOrChance,
|
FreeResponseResolutionOrChance,
|
||||||
BinaryResolutionOrChance,
|
BinaryResolutionOrChance,
|
||||||
NumericResolutionOrExpectation,
|
NumericResolutionOrExpectation,
|
||||||
|
PseudoNumericResolutionOrExpectation,
|
||||||
} from './contract-card'
|
} from './contract-card'
|
||||||
import { Bet } from 'common/bet'
|
import { Bet } from 'common/bet'
|
||||||
import BetRow from '../bet-row'
|
import BetRow from '../bet-row'
|
||||||
|
@ -32,6 +33,7 @@ export const ContractOverview = (props: {
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
const isCreator = user?.id === creatorId
|
const isCreator = user?.id === creatorId
|
||||||
const isBinary = outcomeType === 'BINARY'
|
const isBinary = outcomeType === 'BINARY'
|
||||||
|
const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col className={clsx('mb-6', className)}>
|
<Col className={clsx('mb-6', className)}>
|
||||||
|
@ -49,6 +51,13 @@ export const ContractOverview = (props: {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{isPseudoNumeric && (
|
||||||
|
<PseudoNumericResolutionOrExpectation
|
||||||
|
contract={contract}
|
||||||
|
className="hidden items-end xl:flex"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{outcomeType === 'NUMERIC' && (
|
{outcomeType === 'NUMERIC' && (
|
||||||
<NumericResolutionOrExpectation
|
<NumericResolutionOrExpectation
|
||||||
contract={contract}
|
contract={contract}
|
||||||
|
@ -61,6 +70,11 @@ export const ContractOverview = (props: {
|
||||||
<Row className="items-center justify-between gap-4 xl:hidden">
|
<Row className="items-center justify-between gap-4 xl:hidden">
|
||||||
<BinaryResolutionOrChance contract={contract} />
|
<BinaryResolutionOrChance contract={contract} />
|
||||||
|
|
||||||
|
{tradingAllowed(contract) && <BetRow contract={contract} />}
|
||||||
|
</Row>
|
||||||
|
) : isPseudoNumeric ? (
|
||||||
|
<Row className="items-center justify-between gap-4 xl:hidden">
|
||||||
|
<PseudoNumericResolutionOrExpectation contract={contract} />
|
||||||
{tradingAllowed(contract) && <BetRow contract={contract} />}
|
{tradingAllowed(contract) && <BetRow contract={contract} />}
|
||||||
</Row>
|
</Row>
|
||||||
) : (
|
) : (
|
||||||
|
@ -86,7 +100,9 @@ export const ContractOverview = (props: {
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Spacer h={4} />
|
<Spacer h={4} />
|
||||||
{isBinary && <ContractProbGraph contract={contract} bets={bets} />}{' '}
|
{(isBinary || isPseudoNumeric) && (
|
||||||
|
<ContractProbGraph contract={contract} bets={bets} />
|
||||||
|
)}{' '}
|
||||||
{outcomeType === 'FREE_RESPONSE' && (
|
{outcomeType === 'FREE_RESPONSE' && (
|
||||||
<AnswersGraph contract={contract} bets={bets} />
|
<AnswersGraph contract={contract} bets={bets} />
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -5,16 +5,20 @@ import dayjs from 'dayjs'
|
||||||
import { memo } from 'react'
|
import { memo } from 'react'
|
||||||
import { Bet } from 'common/bet'
|
import { Bet } from 'common/bet'
|
||||||
import { getInitialProbability } from 'common/calculate'
|
import { getInitialProbability } from 'common/calculate'
|
||||||
import { BinaryContract } from 'common/contract'
|
import { BinaryContract, PseudoNumericContract } from 'common/contract'
|
||||||
import { useWindowSize } from 'web/hooks/use-window-size'
|
import { useWindowSize } from 'web/hooks/use-window-size'
|
||||||
|
import { getMappedValue } from 'common/pseudo-numeric'
|
||||||
|
import { formatLargeNumber } from 'common/util/format'
|
||||||
|
|
||||||
export const ContractProbGraph = memo(function ContractProbGraph(props: {
|
export const ContractProbGraph = memo(function ContractProbGraph(props: {
|
||||||
contract: BinaryContract
|
contract: BinaryContract | PseudoNumericContract
|
||||||
bets: Bet[]
|
bets: Bet[]
|
||||||
height?: number
|
height?: number
|
||||||
}) {
|
}) {
|
||||||
const { contract, height } = props
|
const { contract, height } = props
|
||||||
const { resolutionTime, closeTime } = contract
|
const { resolutionTime, closeTime, outcomeType } = contract
|
||||||
|
const isBinary = outcomeType === 'BINARY'
|
||||||
|
const isLogScale = outcomeType === 'PSEUDO_NUMERIC' && contract.isLogScale
|
||||||
|
|
||||||
const bets = props.bets.filter((bet) => !bet.isAnte && !bet.isRedemption)
|
const bets = props.bets.filter((bet) => !bet.isAnte && !bet.isRedemption)
|
||||||
|
|
||||||
|
@ -24,7 +28,10 @@ export const ContractProbGraph = memo(function ContractProbGraph(props: {
|
||||||
contract.createdTime,
|
contract.createdTime,
|
||||||
...bets.map((bet) => bet.createdTime),
|
...bets.map((bet) => bet.createdTime),
|
||||||
].map((time) => new Date(time))
|
].map((time) => new Date(time))
|
||||||
const probs = [startProb, ...bets.map((bet) => bet.probAfter)]
|
|
||||||
|
const f = getMappedValue(contract)
|
||||||
|
|
||||||
|
const probs = [startProb, ...bets.map((bet) => bet.probAfter)].map(f)
|
||||||
|
|
||||||
const isClosed = !!closeTime && Date.now() > closeTime
|
const isClosed = !!closeTime && Date.now() > closeTime
|
||||||
const latestTime = dayjs(
|
const latestTime = dayjs(
|
||||||
|
@ -39,7 +46,11 @@ export const ContractProbGraph = memo(function ContractProbGraph(props: {
|
||||||
times.push(latestTime.toDate())
|
times.push(latestTime.toDate())
|
||||||
probs.push(probs[probs.length - 1])
|
probs.push(probs[probs.length - 1])
|
||||||
|
|
||||||
const yTickValues = [0, 25, 50, 75, 100]
|
const quartiles = [0, 25, 50, 75, 100]
|
||||||
|
|
||||||
|
const yTickValues = isBinary
|
||||||
|
? quartiles
|
||||||
|
: quartiles.map((x) => x / 100).map(f)
|
||||||
|
|
||||||
const { width } = useWindowSize()
|
const { width } = useWindowSize()
|
||||||
|
|
||||||
|
@ -55,9 +66,13 @@ export const ContractProbGraph = memo(function ContractProbGraph(props: {
|
||||||
const totalPoints = width ? (width > 800 ? 300 : 50) : 1
|
const totalPoints = width ? (width > 800 ? 300 : 50) : 1
|
||||||
|
|
||||||
const timeStep: number = latestTime.diff(startDate, 'ms') / totalPoints
|
const timeStep: number = latestTime.diff(startDate, 'ms') / totalPoints
|
||||||
|
|
||||||
const points: { x: Date; y: number }[] = []
|
const points: { x: Date; y: number }[] = []
|
||||||
|
const s = isBinary ? 100 : 1
|
||||||
|
const c = isLogScale && contract.min === 0 ? 1 : 0
|
||||||
|
|
||||||
for (let i = 0; i < times.length - 1; i++) {
|
for (let i = 0; i < times.length - 1; i++) {
|
||||||
points[points.length] = { x: times[i], y: probs[i] * 100 }
|
points[points.length] = { x: times[i], y: s * probs[i] + c }
|
||||||
const numPoints: number = Math.floor(
|
const numPoints: number = Math.floor(
|
||||||
dayjs(times[i + 1]).diff(dayjs(times[i]), 'ms') / timeStep
|
dayjs(times[i + 1]).diff(dayjs(times[i]), 'ms') / timeStep
|
||||||
)
|
)
|
||||||
|
@ -69,17 +84,23 @@ export const ContractProbGraph = memo(function ContractProbGraph(props: {
|
||||||
x: dayjs(times[i])
|
x: dayjs(times[i])
|
||||||
.add(thisTimeStep * n, 'ms')
|
.add(thisTimeStep * n, 'ms')
|
||||||
.toDate(),
|
.toDate(),
|
||||||
y: probs[i] * 100,
|
y: s * probs[i] + c,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = [{ id: 'Yes', data: points, color: '#11b981' }]
|
const data = [
|
||||||
|
{ id: 'Yes', data: points, color: isBinary ? '#11b981' : '#5fa5f9' },
|
||||||
|
]
|
||||||
|
|
||||||
const multiYear = !dayjs(startDate).isSame(latestTime, 'year')
|
const multiYear = !dayjs(startDate).isSame(latestTime, 'year')
|
||||||
const lessThanAWeek = dayjs(startDate).add(8, 'day').isAfter(latestTime)
|
const lessThanAWeek = dayjs(startDate).add(8, 'day').isAfter(latestTime)
|
||||||
|
|
||||||
|
const formatter = isBinary
|
||||||
|
? formatPercent
|
||||||
|
: (x: DatumValue) => formatLargeNumber(+x.valueOf())
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="w-full overflow-visible"
|
className="w-full overflow-visible"
|
||||||
|
@ -87,12 +108,20 @@ export const ContractProbGraph = memo(function ContractProbGraph(props: {
|
||||||
>
|
>
|
||||||
<ResponsiveLine
|
<ResponsiveLine
|
||||||
data={data}
|
data={data}
|
||||||
yScale={{ min: 0, max: 100, type: 'linear' }}
|
yScale={
|
||||||
yFormat={formatPercent}
|
isBinary
|
||||||
|
? { min: 0, max: 100, type: 'linear' }
|
||||||
|
: {
|
||||||
|
min: contract.min + c,
|
||||||
|
max: contract.max + c,
|
||||||
|
type: contract.isLogScale ? 'log' : 'linear',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
yFormat={formatter}
|
||||||
gridYValues={yTickValues}
|
gridYValues={yTickValues}
|
||||||
axisLeft={{
|
axisLeft={{
|
||||||
tickValues: yTickValues,
|
tickValues: yTickValues,
|
||||||
format: formatPercent,
|
format: formatter,
|
||||||
}}
|
}}
|
||||||
xScale={{
|
xScale={{
|
||||||
type: 'time',
|
type: 'time',
|
||||||
|
|
|
@ -2,6 +2,7 @@ import clsx from 'clsx'
|
||||||
import {
|
import {
|
||||||
getOutcomeProbability,
|
getOutcomeProbability,
|
||||||
getOutcomeProbabilityAfterBet,
|
getOutcomeProbabilityAfterBet,
|
||||||
|
getProbability,
|
||||||
getTopAnswer,
|
getTopAnswer,
|
||||||
} from 'common/calculate'
|
} from 'common/calculate'
|
||||||
import { getExpectedValue } from 'common/calculate-dpm'
|
import { getExpectedValue } from 'common/calculate-dpm'
|
||||||
|
@ -25,18 +26,18 @@ import { useSaveShares } from '../use-save-shares'
|
||||||
import { sellShares } from 'web/lib/firebase/api-call'
|
import { sellShares } from 'web/lib/firebase/api-call'
|
||||||
import { calculateCpmmSale, getCpmmProbability } from 'common/calculate-cpmm'
|
import { calculateCpmmSale, getCpmmProbability } from 'common/calculate-cpmm'
|
||||||
import { track } from 'web/lib/service/analytics'
|
import { track } from 'web/lib/service/analytics'
|
||||||
|
import { formatNumericProbability } from 'common/pseudo-numeric'
|
||||||
|
|
||||||
const BET_SIZE = 10
|
const BET_SIZE = 10
|
||||||
|
|
||||||
export function QuickBet(props: { contract: Contract; user: User }) {
|
export function QuickBet(props: { contract: Contract; user: User }) {
|
||||||
const { contract, user } = props
|
const { contract, user } = props
|
||||||
const isCpmm = contract.mechanism === 'cpmm-1'
|
const { mechanism, outcomeType } = contract
|
||||||
|
const isCpmm = mechanism === 'cpmm-1'
|
||||||
|
|
||||||
const userBets = useUserContractBets(user.id, contract.id)
|
const userBets = useUserContractBets(user.id, contract.id)
|
||||||
const topAnswer =
|
const topAnswer =
|
||||||
contract.outcomeType === 'FREE_RESPONSE'
|
outcomeType === 'FREE_RESPONSE' ? getTopAnswer(contract) : undefined
|
||||||
? getTopAnswer(contract)
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
// TODO: yes/no from useSaveShares doesn't work on numeric contracts
|
// TODO: yes/no from useSaveShares doesn't work on numeric contracts
|
||||||
const { yesFloorShares, noFloorShares, yesShares, noShares } = useSaveShares(
|
const { yesFloorShares, noFloorShares, yesShares, noShares } = useSaveShares(
|
||||||
|
@ -45,9 +46,9 @@ export function QuickBet(props: { contract: Contract; user: User }) {
|
||||||
topAnswer?.number.toString() || undefined
|
topAnswer?.number.toString() || undefined
|
||||||
)
|
)
|
||||||
const hasUpShares =
|
const hasUpShares =
|
||||||
yesFloorShares || (noFloorShares && contract.outcomeType === 'NUMERIC')
|
yesFloorShares || (noFloorShares && outcomeType === 'NUMERIC')
|
||||||
const hasDownShares =
|
const hasDownShares =
|
||||||
noFloorShares && yesFloorShares <= 0 && contract.outcomeType !== 'NUMERIC'
|
noFloorShares && yesFloorShares <= 0 && outcomeType !== 'NUMERIC'
|
||||||
|
|
||||||
const [upHover, setUpHover] = useState(false)
|
const [upHover, setUpHover] = useState(false)
|
||||||
const [downHover, setDownHover] = useState(false)
|
const [downHover, setDownHover] = useState(false)
|
||||||
|
@ -130,25 +131,6 @@ export function QuickBet(props: { contract: Contract; user: User }) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const textColor = `text-${getColor(contract)}`
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col
|
<Col
|
||||||
className={clsx(
|
className={clsx(
|
||||||
|
@ -173,14 +155,14 @@ export function QuickBet(props: { contract: Contract; user: User }) {
|
||||||
<TriangleFillIcon
|
<TriangleFillIcon
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'mx-auto h-5 w-5',
|
'mx-auto h-5 w-5',
|
||||||
upHover ? textColor : 'text-gray-400'
|
upHover ? 'text-green-500' : 'text-gray-400'
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<TriangleFillIcon
|
<TriangleFillIcon
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'mx-auto h-5 w-5',
|
'mx-auto h-5 w-5',
|
||||||
upHover ? textColor : 'text-gray-200'
|
upHover ? 'text-green-500' : 'text-gray-200'
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -189,7 +171,7 @@ export function QuickBet(props: { contract: Contract; user: User }) {
|
||||||
<QuickOutcomeView contract={contract} previewProb={previewProb} />
|
<QuickOutcomeView contract={contract} previewProb={previewProb} />
|
||||||
|
|
||||||
{/* Down bet triangle */}
|
{/* Down bet triangle */}
|
||||||
{contract.outcomeType !== 'BINARY' ? (
|
{outcomeType !== 'BINARY' && outcomeType !== 'PSEUDO_NUMERIC' ? (
|
||||||
<div>
|
<div>
|
||||||
<div className="peer absolute bottom-0 left-0 right-0 h-[50%] cursor-default"></div>
|
<div className="peer absolute bottom-0 left-0 right-0 h-[50%] cursor-default"></div>
|
||||||
<TriangleDownFillIcon
|
<TriangleDownFillIcon
|
||||||
|
@ -254,6 +236,25 @@ export function ProbBar(props: { contract: Contract; previewProb?: number }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function quickOutcome(contract: Contract, direction: 'UP' | 'DOWN') {
|
||||||
|
const { outcomeType } = contract
|
||||||
|
|
||||||
|
if (outcomeType === 'BINARY' || outcomeType === 'PSEUDO_NUMERIC') {
|
||||||
|
return direction === 'UP' ? 'YES' : 'NO'
|
||||||
|
}
|
||||||
|
if (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 (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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function QuickOutcomeView(props: {
|
function QuickOutcomeView(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
previewProb?: number
|
previewProb?: number
|
||||||
|
@ -261,9 +262,16 @@ function QuickOutcomeView(props: {
|
||||||
}) {
|
}) {
|
||||||
const { contract, previewProb, caption } = props
|
const { contract, previewProb, caption } = props
|
||||||
const { outcomeType } = contract
|
const { outcomeType } = contract
|
||||||
|
const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC'
|
||||||
|
|
||||||
// If there's a preview prob, display that instead of the current prob
|
// If there's a preview prob, display that instead of the current prob
|
||||||
const override =
|
const override =
|
||||||
previewProb === undefined ? undefined : formatPercent(previewProb)
|
previewProb === undefined
|
||||||
|
? undefined
|
||||||
|
: isPseudoNumeric
|
||||||
|
? formatNumericProbability(previewProb, contract)
|
||||||
|
: formatPercent(previewProb)
|
||||||
|
|
||||||
const textColor = `text-${getColor(contract)}`
|
const textColor = `text-${getColor(contract)}`
|
||||||
|
|
||||||
let display: string | undefined
|
let display: string | undefined
|
||||||
|
@ -271,6 +279,9 @@ function QuickOutcomeView(props: {
|
||||||
case 'BINARY':
|
case 'BINARY':
|
||||||
display = getBinaryProbPercent(contract)
|
display = getBinaryProbPercent(contract)
|
||||||
break
|
break
|
||||||
|
case 'PSEUDO_NUMERIC':
|
||||||
|
display = formatNumericProbability(getProbability(contract), contract)
|
||||||
|
break
|
||||||
case 'NUMERIC':
|
case 'NUMERIC':
|
||||||
display = formatLargeNumber(getExpectedValue(contract))
|
display = formatLargeNumber(getExpectedValue(contract))
|
||||||
break
|
break
|
||||||
|
@ -295,11 +306,15 @@ function QuickOutcomeView(props: {
|
||||||
// Return a number from 0 to 1 for this contract
|
// Return a number from 0 to 1 for this contract
|
||||||
// Resolved contracts are set to 1, for coloring purposes (even if NO)
|
// Resolved contracts are set to 1, for coloring purposes (even if NO)
|
||||||
function getProb(contract: Contract) {
|
function getProb(contract: Contract) {
|
||||||
const { outcomeType, resolution } = contract
|
const { outcomeType, resolution, resolutionProbability } = contract
|
||||||
return resolution
|
return resolutionProbability
|
||||||
|
? resolutionProbability
|
||||||
|
: resolution
|
||||||
? 1
|
? 1
|
||||||
: outcomeType === 'BINARY'
|
: outcomeType === 'BINARY'
|
||||||
? getBinaryProb(contract)
|
? getBinaryProb(contract)
|
||||||
|
: outcomeType === 'PSEUDO_NUMERIC'
|
||||||
|
? getProbability(contract)
|
||||||
: outcomeType === 'FREE_RESPONSE'
|
: outcomeType === 'FREE_RESPONSE'
|
||||||
? getOutcomeProbability(contract, getTopAnswer(contract)?.id || '')
|
? getOutcomeProbability(contract, getTopAnswer(contract)?.id || '')
|
||||||
: outcomeType === 'NUMERIC'
|
: outcomeType === 'NUMERIC'
|
||||||
|
@ -316,7 +331,8 @@ function getNumericScale(contract: NumericContract) {
|
||||||
export function getColor(contract: Contract) {
|
export function getColor(contract: Contract) {
|
||||||
// TODO: Try injecting a gradient here
|
// TODO: Try injecting a gradient here
|
||||||
// return 'primary'
|
// return 'primary'
|
||||||
const { resolution } = contract
|
const { resolution, outcomeType } = contract
|
||||||
|
|
||||||
if (resolution) {
|
if (resolution) {
|
||||||
return (
|
return (
|
||||||
OUTCOME_TO_COLOR[resolution as resolution] ??
|
OUTCOME_TO_COLOR[resolution as resolution] ??
|
||||||
|
@ -325,6 +341,8 @@ export function getColor(contract: Contract) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (outcomeType === 'PSEUDO_NUMERIC') return 'blue-400'
|
||||||
|
|
||||||
if ((contract.closeTime ?? Infinity) < Date.now()) {
|
if ((contract.closeTime ?? Infinity) < Date.now()) {
|
||||||
return 'gray-400'
|
return 'gray-400'
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,13 +7,14 @@ import { Row } from 'web/components/layout/row'
|
||||||
import { Avatar, EmptyAvatar } from 'web/components/avatar'
|
import { Avatar, EmptyAvatar } from 'web/components/avatar'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { UsersIcon } from '@heroicons/react/solid'
|
import { UsersIcon } from '@heroicons/react/solid'
|
||||||
import { formatMoney } from 'common/util/format'
|
import { formatMoney, formatPercent } from 'common/util/format'
|
||||||
import { OutcomeLabel } from 'web/components/outcome-label'
|
import { OutcomeLabel } from 'web/components/outcome-label'
|
||||||
import { RelativeTimestamp } from 'web/components/relative-timestamp'
|
import { RelativeTimestamp } from 'web/components/relative-timestamp'
|
||||||
import React, { Fragment } from 'react'
|
import React, { Fragment } from 'react'
|
||||||
import { uniqBy, partition, sumBy, groupBy } from 'lodash'
|
import { uniqBy, partition, sumBy, groupBy } from 'lodash'
|
||||||
import { JoinSpans } from 'web/components/join-spans'
|
import { JoinSpans } from 'web/components/join-spans'
|
||||||
import { UserLink } from '../user-page'
|
import { UserLink } from '../user-page'
|
||||||
|
import { formatNumericProbability } from 'common/pseudo-numeric'
|
||||||
|
|
||||||
export function FeedBet(props: {
|
export function FeedBet(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
|
@ -75,6 +76,8 @@ export function BetStatusText(props: {
|
||||||
hideOutcome?: boolean
|
hideOutcome?: boolean
|
||||||
}) {
|
}) {
|
||||||
const { bet, contract, bettor, isSelf, hideOutcome } = props
|
const { bet, contract, bettor, isSelf, hideOutcome } = props
|
||||||
|
const { outcomeType } = contract
|
||||||
|
const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC'
|
||||||
const { amount, outcome, createdTime } = bet
|
const { amount, outcome, createdTime } = bet
|
||||||
|
|
||||||
const bought = amount >= 0 ? 'bought' : 'sold'
|
const bought = amount >= 0 ? 'bought' : 'sold'
|
||||||
|
@ -97,7 +100,10 @@ export function BetStatusText(props: {
|
||||||
value={(bet as any).value}
|
value={(bet as any).value}
|
||||||
contract={contract}
|
contract={contract}
|
||||||
truncate="short"
|
truncate="short"
|
||||||
/>
|
/>{' '}
|
||||||
|
{isPseudoNumeric
|
||||||
|
? ' than ' + formatNumericProbability(bet.probAfter, contract)
|
||||||
|
: ' at ' + formatPercent(bet.probAfter)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<RelativeTimestamp time={createdTime} />
|
<RelativeTimestamp time={createdTime} />
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { useRouter } from 'next/router'
|
||||||
import { Modal } from 'web/components/layout/modal'
|
import { Modal } from 'web/components/layout/modal'
|
||||||
import { FilterSelectUsers } from 'web/components/filter-select-users'
|
import { FilterSelectUsers } from 'web/components/filter-select-users'
|
||||||
import { User } from 'common/user'
|
import { User } from 'common/user'
|
||||||
|
import { uniq } from 'lodash'
|
||||||
|
|
||||||
export function EditGroupButton(props: { group: Group; className?: string }) {
|
export function EditGroupButton(props: { group: Group; className?: string }) {
|
||||||
const { group, className } = props
|
const { group, className } = props
|
||||||
|
@ -35,7 +36,7 @@ export function EditGroupButton(props: { group: Group; className?: string }) {
|
||||||
await updateGroup(group, {
|
await updateGroup(group, {
|
||||||
name,
|
name,
|
||||||
about,
|
about,
|
||||||
memberIds: [...memberIds, ...addMemberUsers.map((user) => user.id)],
|
memberIds: uniq([...memberIds, ...addMemberUsers.map((user) => user.id)]),
|
||||||
})
|
})
|
||||||
|
|
||||||
setIsSubmitting(false)
|
setIsSubmitting(false)
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { TextButton } from 'web/components/text-button'
|
||||||
import { Group } from 'common/group'
|
import { Group } from 'common/group'
|
||||||
import { Modal } from 'web/components/layout/modal'
|
import { Modal } from 'web/components/layout/modal'
|
||||||
import { Col } from 'web/components/layout/col'
|
import { Col } from 'web/components/layout/col'
|
||||||
import { joinGroup, leaveGroup } from 'web/lib/firebase/groups'
|
import { addUserToGroup, leaveGroup } from 'web/lib/firebase/groups'
|
||||||
import { firebaseLogin } from 'web/lib/firebase/users'
|
import { firebaseLogin } from 'web/lib/firebase/users'
|
||||||
import { GroupLink } from 'web/pages/groups'
|
import { GroupLink } from 'web/pages/groups'
|
||||||
|
|
||||||
|
@ -93,7 +93,7 @@ export function JoinOrLeaveGroupButton(props: {
|
||||||
: false
|
: false
|
||||||
const onJoinGroup = () => {
|
const onJoinGroup = () => {
|
||||||
if (!currentUser) return
|
if (!currentUser) return
|
||||||
joinGroup(group, currentUser.id)
|
addUserToGroup(group, currentUser.id)
|
||||||
}
|
}
|
||||||
const onLeaveGroup = () => {
|
const onLeaveGroup = () => {
|
||||||
if (!currentUser) return
|
if (!currentUser) return
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
import { Fragment, ReactNode } from 'react'
|
import { Fragment, ReactNode } from 'react'
|
||||||
import { Dialog, Transition } from '@headlessui/react'
|
import { Dialog, Transition } from '@headlessui/react'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
|
||||||
// From https://tailwindui.com/components/application-ui/overlays/modals
|
// From https://tailwindui.com/components/application-ui/overlays/modals
|
||||||
export function Modal(props: {
|
export function Modal(props: {
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
open: boolean
|
open: boolean
|
||||||
setOpen: (open: boolean) => void
|
setOpen: (open: boolean) => void
|
||||||
|
className?: string
|
||||||
}) {
|
}) {
|
||||||
const { children, open, setOpen } = props
|
const { children, open, setOpen, className } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Transition.Root show={open} as={Fragment}>
|
<Transition.Root show={open} as={Fragment}>
|
||||||
|
@ -45,7 +47,12 @@ export function Modal(props: {
|
||||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
>
|
>
|
||||||
<div className="inline-block transform overflow-hidden text-left align-bottom transition-all sm:my-8 sm:w-full sm:max-w-md sm:p-6 sm:align-middle">
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'inline-block transform overflow-hidden text-left align-bottom transition-all sm:my-8 sm:w-full sm:max-w-md sm:p-6 sm:align-middle',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
|
|
|
@ -14,16 +14,16 @@ type Tab = {
|
||||||
export function Tabs(props: {
|
export function Tabs(props: {
|
||||||
tabs: Tab[]
|
tabs: Tab[]
|
||||||
defaultIndex?: number
|
defaultIndex?: number
|
||||||
className?: string
|
labelClassName?: string
|
||||||
onClick?: (tabTitle: string, index: number) => void
|
onClick?: (tabTitle: string, index: number) => void
|
||||||
}) {
|
}) {
|
||||||
const { tabs, defaultIndex, className, onClick } = props
|
const { tabs, defaultIndex, labelClassName, onClick } = props
|
||||||
const [activeIndex, setActiveIndex] = useState(defaultIndex ?? 0)
|
const [activeIndex, setActiveIndex] = useState(defaultIndex ?? 0)
|
||||||
const activeTab = tabs[activeIndex] as Tab | undefined // can be undefined in weird case
|
const activeTab = tabs[activeIndex] as Tab | undefined // can be undefined in weird case
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
<div className="border-b border-gray-200">
|
<div className="mb-4 border-b border-gray-200">
|
||||||
<nav className="-mb-px flex space-x-8" aria-label="Tabs">
|
<nav className="-mb-px flex space-x-8" aria-label="Tabs">
|
||||||
{tabs.map((tab, i) => (
|
{tabs.map((tab, i) => (
|
||||||
<Link href={tab.href ?? '#'} key={tab.title} shallow={!!tab.href}>
|
<Link href={tab.href ?? '#'} key={tab.title} shallow={!!tab.href}>
|
||||||
|
@ -42,7 +42,7 @@ export function Tabs(props: {
|
||||||
? 'border-indigo-500 text-indigo-600'
|
? 'border-indigo-500 text-indigo-600'
|
||||||
: 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700',
|
: 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700',
|
||||||
'cursor-pointer whitespace-nowrap border-b-2 py-3 px-1 text-sm font-medium',
|
'cursor-pointer whitespace-nowrap border-b-2 py-3 px-1 text-sm font-medium',
|
||||||
className
|
labelClassName
|
||||||
)}
|
)}
|
||||||
aria-current={activeIndex === i ? 'page' : undefined}
|
aria-current={activeIndex === i ? 'page' : undefined}
|
||||||
>
|
>
|
||||||
|
@ -56,7 +56,7 @@ export function Tabs(props: {
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4">{activeTab?.content}</div>
|
{activeTab?.content}
|
||||||
</div>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -254,7 +254,7 @@ function GroupsList(props: { currentPage: string; memberItems: Item[] }) {
|
||||||
<div className="mt-1 space-y-0.5">
|
<div className="mt-1 space-y-0.5">
|
||||||
{memberItems.map((item) => (
|
{memberItems.map((item) => (
|
||||||
<a
|
<a
|
||||||
key={item.name}
|
key={item.href}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
className="group flex items-center rounded-md px-3 py-2 text-sm font-medium text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
className="group flex items-center rounded-md px-3 py-2 text-sm font-medium text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
||||||
>
|
>
|
||||||
|
|
|
@ -6,13 +6,14 @@ import { User } from 'web/lib/firebase/users'
|
||||||
import { NumberCancelSelector } from './yes-no-selector'
|
import { NumberCancelSelector } from './yes-no-selector'
|
||||||
import { Spacer } from './layout/spacer'
|
import { Spacer } from './layout/spacer'
|
||||||
import { ResolveConfirmationButton } from './confirmation-button'
|
import { ResolveConfirmationButton } from './confirmation-button'
|
||||||
|
import { NumericContract, PseudoNumericContract } from 'common/contract'
|
||||||
import { APIError, resolveMarket } from 'web/lib/firebase/api-call'
|
import { APIError, resolveMarket } from 'web/lib/firebase/api-call'
|
||||||
import { NumericContract } from 'common/contract'
|
|
||||||
import { BucketInput } from './bucket-input'
|
import { BucketInput } from './bucket-input'
|
||||||
|
import { getPseudoProbability } from 'common/pseudo-numeric'
|
||||||
|
|
||||||
export function NumericResolutionPanel(props: {
|
export function NumericResolutionPanel(props: {
|
||||||
creator: User
|
creator: User
|
||||||
contract: NumericContract
|
contract: NumericContract | PseudoNumericContract
|
||||||
className?: string
|
className?: string
|
||||||
}) {
|
}) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -21,6 +22,7 @@ export function NumericResolutionPanel(props: {
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const { contract, className } = props
|
const { contract, className } = props
|
||||||
|
const { min, max, outcomeType } = contract
|
||||||
|
|
||||||
const [outcomeMode, setOutcomeMode] = useState<
|
const [outcomeMode, setOutcomeMode] = useState<
|
||||||
'NUMBER' | 'CANCEL' | undefined
|
'NUMBER' | 'CANCEL' | undefined
|
||||||
|
@ -32,15 +34,32 @@ export function NumericResolutionPanel(props: {
|
||||||
const [error, setError] = useState<string | undefined>(undefined)
|
const [error, setError] = useState<string | undefined>(undefined)
|
||||||
|
|
||||||
const resolve = async () => {
|
const resolve = async () => {
|
||||||
const finalOutcome = outcomeMode === 'NUMBER' ? outcome : 'CANCEL'
|
const finalOutcome =
|
||||||
|
outcomeMode === 'CANCEL'
|
||||||
|
? 'CANCEL'
|
||||||
|
: outcomeType === 'PSEUDO_NUMERIC'
|
||||||
|
? 'MKT'
|
||||||
|
: 'NUMBER'
|
||||||
if (outcomeMode === undefined || finalOutcome === undefined) return
|
if (outcomeMode === undefined || finalOutcome === undefined) return
|
||||||
|
|
||||||
setIsSubmitting(true)
|
setIsSubmitting(true)
|
||||||
|
|
||||||
|
const boundedValue = Math.max(Math.min(max, value ?? 0), min)
|
||||||
|
|
||||||
|
const probabilityInt =
|
||||||
|
100 *
|
||||||
|
getPseudoProbability(
|
||||||
|
boundedValue,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
outcomeType === 'PSEUDO_NUMERIC' && contract.isLogScale
|
||||||
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await resolveMarket({
|
const result = await resolveMarket({
|
||||||
outcome: finalOutcome,
|
outcome: finalOutcome,
|
||||||
value,
|
value,
|
||||||
|
probabilityInt,
|
||||||
contractId: contract.id,
|
contractId: contract.id,
|
||||||
})
|
})
|
||||||
console.log('resolved', outcome, 'result:', result)
|
console.log('resolved', outcome, 'result:', result)
|
||||||
|
@ -77,7 +96,7 @@ export function NumericResolutionPanel(props: {
|
||||||
|
|
||||||
{outcomeMode === 'NUMBER' && (
|
{outcomeMode === 'NUMBER' && (
|
||||||
<BucketInput
|
<BucketInput
|
||||||
contract={contract}
|
contract={contract as any}
|
||||||
isSubmitting={isSubmitting}
|
isSubmitting={isSubmitting}
|
||||||
onBucketChange={(v, o) => (setValue(v), setOutcome(o))}
|
onBucketChange={(v, o) => (setValue(v), setOutcome(o))}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -19,11 +19,15 @@ export function OutcomeLabel(props: {
|
||||||
value?: number
|
value?: number
|
||||||
}) {
|
}) {
|
||||||
const { outcome, contract, truncate, value } = props
|
const { outcome, contract, truncate, value } = props
|
||||||
|
const { outcomeType } = contract
|
||||||
|
|
||||||
if (contract.outcomeType === 'BINARY')
|
if (outcomeType === 'PSEUDO_NUMERIC')
|
||||||
|
return <PseudoNumericOutcomeLabel outcome={outcome as any} />
|
||||||
|
|
||||||
|
if (outcomeType === 'BINARY')
|
||||||
return <BinaryOutcomeLabel outcome={outcome as any} />
|
return <BinaryOutcomeLabel outcome={outcome as any} />
|
||||||
|
|
||||||
if (contract.outcomeType === 'NUMERIC')
|
if (outcomeType === 'NUMERIC')
|
||||||
return (
|
return (
|
||||||
<span className="text-blue-500">
|
<span className="text-blue-500">
|
||||||
{value ?? getValueFromBucket(outcome, contract)}
|
{value ?? getValueFromBucket(outcome, contract)}
|
||||||
|
@ -49,6 +53,15 @@ export function BinaryOutcomeLabel(props: { outcome: resolution }) {
|
||||||
return <CancelLabel />
|
return <CancelLabel />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function PseudoNumericOutcomeLabel(props: { outcome: resolution }) {
|
||||||
|
const { outcome } = props
|
||||||
|
|
||||||
|
if (outcome === 'YES') return <HigherLabel />
|
||||||
|
if (outcome === 'NO') return <LowerLabel />
|
||||||
|
if (outcome === 'MKT') return <ProbLabel />
|
||||||
|
return <CancelLabel />
|
||||||
|
}
|
||||||
|
|
||||||
export function BinaryContractOutcomeLabel(props: {
|
export function BinaryContractOutcomeLabel(props: {
|
||||||
contract: BinaryContract
|
contract: BinaryContract
|
||||||
resolution: resolution
|
resolution: resolution
|
||||||
|
@ -98,6 +111,14 @@ export function YesLabel() {
|
||||||
return <span className="text-primary">YES</span>
|
return <span className="text-primary">YES</span>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function HigherLabel() {
|
||||||
|
return <span className="text-primary">HIGHER</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LowerLabel() {
|
||||||
|
return <span className="text-red-400">LOWER</span>
|
||||||
|
}
|
||||||
|
|
||||||
export function NoLabel() {
|
export function NoLabel() {
|
||||||
return <span className="text-red-400">NO</span>
|
return <span className="text-red-400">NO</span>
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,7 +52,7 @@ export const PortfolioValueGraph = memo(function PortfolioValueGraph(props: {
|
||||||
margin={{ top: 20, right: 28, bottom: 22, left: 60 }}
|
margin={{ top: 20, right: 28, bottom: 22, left: 60 }}
|
||||||
xScale={{
|
xScale={{
|
||||||
type: 'time',
|
type: 'time',
|
||||||
min: points[0].x,
|
min: points[0]?.x,
|
||||||
max: endDate,
|
max: endDate,
|
||||||
}}
|
}}
|
||||||
yScale={{
|
yScale={{
|
||||||
|
@ -77,6 +77,7 @@ export const PortfolioValueGraph = memo(function PortfolioValueGraph(props: {
|
||||||
enableGridY={true}
|
enableGridY={true}
|
||||||
enableSlices="x"
|
enableSlices="x"
|
||||||
animate={false}
|
animate={false}
|
||||||
|
yFormat={(value) => formatMoney(+value)}
|
||||||
></ResponsiveLine>
|
></ResponsiveLine>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -13,7 +13,7 @@ export const PortfolioValueSection = memo(
|
||||||
}) {
|
}) {
|
||||||
const { portfolioHistory } = props
|
const { portfolioHistory } = props
|
||||||
const lastPortfolioMetrics = last(portfolioHistory)
|
const lastPortfolioMetrics = last(portfolioHistory)
|
||||||
const [portfolioPeriod] = useState<Period>('allTime')
|
const [portfolioPeriod, setPortfolioPeriod] = useState<Period>('allTime')
|
||||||
|
|
||||||
if (portfolioHistory.length === 0 || !lastPortfolioMetrics) {
|
if (portfolioHistory.length === 0 || !lastPortfolioMetrics) {
|
||||||
return <div> No portfolio history data yet </div>
|
return <div> No portfolio history data yet </div>
|
||||||
|
@ -33,9 +33,16 @@ export const PortfolioValueSection = memo(
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
</div>
|
</div>
|
||||||
{
|
<select
|
||||||
//TODO: enable day/week/monthly as data becomes available
|
className="select select-bordered self-start"
|
||||||
}
|
onChange={(e) => {
|
||||||
|
setPortfolioPeriod(e.target.value as Period)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="allTime">All time</option>
|
||||||
|
<option value="weekly">Weekly</option>
|
||||||
|
<option value="daily">Daily</option>
|
||||||
|
</select>
|
||||||
</Row>
|
</Row>
|
||||||
<PortfolioValueGraph
|
<PortfolioValueGraph
|
||||||
portfolioHistory={portfolioHistory}
|
portfolioHistory={portfolioHistory}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { BinaryContract } from 'common/contract'
|
import { BinaryContract, PseudoNumericContract } from 'common/contract'
|
||||||
import { User } from 'common/user'
|
import { User } from 'common/user'
|
||||||
import { useUserContractBets } from 'web/hooks/use-user-bets'
|
import { useUserContractBets } from 'web/hooks/use-user-bets'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
@ -7,7 +7,7 @@ import clsx from 'clsx'
|
||||||
import { SellSharesModal } from './sell-modal'
|
import { SellSharesModal } from './sell-modal'
|
||||||
|
|
||||||
export function SellButton(props: {
|
export function SellButton(props: {
|
||||||
contract: BinaryContract
|
contract: BinaryContract | PseudoNumericContract
|
||||||
user: User | null | undefined
|
user: User | null | undefined
|
||||||
sharesOutcome: 'YES' | 'NO' | undefined
|
sharesOutcome: 'YES' | 'NO' | undefined
|
||||||
shares: number
|
shares: number
|
||||||
|
@ -16,7 +16,8 @@ export function SellButton(props: {
|
||||||
const { contract, user, sharesOutcome, shares, panelClassName } = props
|
const { contract, user, sharesOutcome, shares, panelClassName } = props
|
||||||
const userBets = useUserContractBets(user?.id, contract.id)
|
const userBets = useUserContractBets(user?.id, contract.id)
|
||||||
const [showSellModal, setShowSellModal] = useState(false)
|
const [showSellModal, setShowSellModal] = useState(false)
|
||||||
const { mechanism } = contract
|
const { mechanism, outcomeType } = contract
|
||||||
|
const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC'
|
||||||
|
|
||||||
if (sharesOutcome && user && mechanism === 'cpmm-1') {
|
if (sharesOutcome && user && mechanism === 'cpmm-1') {
|
||||||
return (
|
return (
|
||||||
|
@ -32,7 +33,10 @@ export function SellButton(props: {
|
||||||
)}
|
)}
|
||||||
onClick={() => setShowSellModal(true)}
|
onClick={() => setShowSellModal(true)}
|
||||||
>
|
>
|
||||||
{'Sell ' + sharesOutcome}
|
Sell{' '}
|
||||||
|
{isPseudoNumeric
|
||||||
|
? { YES: 'HIGH', NO: 'LOW' }[sharesOutcome]
|
||||||
|
: sharesOutcome}
|
||||||
</button>
|
</button>
|
||||||
<div className={'mt-1 w-24 text-center text-sm text-gray-500'}>
|
<div className={'mt-1 w-24 text-center text-sm text-gray-500'}>
|
||||||
{'(' + Math.floor(shares) + ' shares)'}
|
{'(' + Math.floor(shares) + ' shares)'}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { CPMMBinaryContract } from 'common/contract'
|
import { CPMMBinaryContract, PseudoNumericContract } from 'common/contract'
|
||||||
import { Bet } from 'common/bet'
|
import { Bet } from 'common/bet'
|
||||||
import { User } from 'common/user'
|
import { User } from 'common/user'
|
||||||
import { Modal } from './layout/modal'
|
import { Modal } from './layout/modal'
|
||||||
|
@ -11,7 +11,7 @@ import clsx from 'clsx'
|
||||||
|
|
||||||
export function SellSharesModal(props: {
|
export function SellSharesModal(props: {
|
||||||
className?: string
|
className?: string
|
||||||
contract: CPMMBinaryContract
|
contract: CPMMBinaryContract | PseudoNumericContract
|
||||||
userBets: Bet[]
|
userBets: Bet[]
|
||||||
shares: number
|
shares: number
|
||||||
sharesOutcome: 'YES' | 'NO'
|
sharesOutcome: 'YES' | 'NO'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { BinaryContract } from 'common/contract'
|
import { BinaryContract, PseudoNumericContract } from 'common/contract'
|
||||||
import { User } from 'common/user'
|
import { User } from 'common/user'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { Col } from './layout/col'
|
import { Col } from './layout/col'
|
||||||
|
@ -10,7 +10,7 @@ import { useSaveShares } from './use-save-shares'
|
||||||
import { SellSharesModal } from './sell-modal'
|
import { SellSharesModal } from './sell-modal'
|
||||||
|
|
||||||
export function SellRow(props: {
|
export function SellRow(props: {
|
||||||
contract: BinaryContract
|
contract: BinaryContract | PseudoNumericContract
|
||||||
user: User | null | undefined
|
user: User | null | undefined
|
||||||
className?: string
|
className?: string
|
||||||
}) {
|
}) {
|
||||||
|
|
|
@ -38,6 +38,7 @@ import { FollowButton } from './follow-button'
|
||||||
import { PortfolioMetrics } from 'common/user'
|
import { PortfolioMetrics } from 'common/user'
|
||||||
import { ReferralsButton } from 'web/components/referrals-button'
|
import { ReferralsButton } from 'web/components/referrals-button'
|
||||||
import { GroupsButton } from 'web/components/groups/groups-button'
|
import { GroupsButton } from 'web/components/groups/groups-button'
|
||||||
|
import { PortfolioValueSection } from './portfolio/portfolio-value-section'
|
||||||
|
|
||||||
export function UserLink(props: {
|
export function UserLink(props: {
|
||||||
name: string
|
name: string
|
||||||
|
@ -75,7 +76,9 @@ export function UserPage(props: {
|
||||||
'loading'
|
'loading'
|
||||||
)
|
)
|
||||||
const [usersBets, setUsersBets] = useState<Bet[] | 'loading'>('loading')
|
const [usersBets, setUsersBets] = useState<Bet[] | 'loading'>('loading')
|
||||||
const [, setUsersPortfolioHistory] = useState<PortfolioMetrics[]>([])
|
const [portfolioHistory, setUsersPortfolioHistory] = useState<
|
||||||
|
PortfolioMetrics[]
|
||||||
|
>([])
|
||||||
const [commentsByContract, setCommentsByContract] = useState<
|
const [commentsByContract, setCommentsByContract] = useState<
|
||||||
Map<Contract, Comment[]> | 'loading'
|
Map<Contract, Comment[]> | 'loading'
|
||||||
>('loading')
|
>('loading')
|
||||||
|
@ -258,7 +261,7 @@ export function UserPage(props: {
|
||||||
|
|
||||||
{usersContracts !== 'loading' && commentsByContract != 'loading' ? (
|
{usersContracts !== 'loading' && commentsByContract != 'loading' ? (
|
||||||
<Tabs
|
<Tabs
|
||||||
className={'pb-2 pt-1 '}
|
labelClassName={'pb-2 pt-1 '}
|
||||||
defaultIndex={
|
defaultIndex={
|
||||||
defaultTabTitle ? TAB_IDS.indexOf(defaultTabTitle) : 0
|
defaultTabTitle ? TAB_IDS.indexOf(defaultTabTitle) : 0
|
||||||
}
|
}
|
||||||
|
@ -297,9 +300,9 @@ export function UserPage(props: {
|
||||||
title: 'Bets',
|
title: 'Bets',
|
||||||
content: (
|
content: (
|
||||||
<div>
|
<div>
|
||||||
{
|
<PortfolioValueSection
|
||||||
// TODO: add portfolio-value-section here
|
portfolioHistory={portfolioHistory}
|
||||||
}
|
/>
|
||||||
<BetsList
|
<BetsList
|
||||||
user={user}
|
user={user}
|
||||||
hideBetsBefore={isCurrentUser ? 0 : JUNE_1_2022}
|
hideBetsBefore={isCurrentUser ? 0 : JUNE_1_2022}
|
||||||
|
|
|
@ -12,6 +12,7 @@ export function YesNoSelector(props: {
|
||||||
btnClassName?: string
|
btnClassName?: string
|
||||||
replaceYesButton?: React.ReactNode
|
replaceYesButton?: React.ReactNode
|
||||||
replaceNoButton?: React.ReactNode
|
replaceNoButton?: React.ReactNode
|
||||||
|
isPseudoNumeric?: boolean
|
||||||
}) {
|
}) {
|
||||||
const {
|
const {
|
||||||
selected,
|
selected,
|
||||||
|
@ -20,6 +21,7 @@ export function YesNoSelector(props: {
|
||||||
btnClassName,
|
btnClassName,
|
||||||
replaceNoButton,
|
replaceNoButton,
|
||||||
replaceYesButton,
|
replaceYesButton,
|
||||||
|
isPseudoNumeric,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const commonClassNames =
|
const commonClassNames =
|
||||||
|
@ -41,7 +43,7 @@ export function YesNoSelector(props: {
|
||||||
)}
|
)}
|
||||||
onClick={() => onSelect('YES')}
|
onClick={() => onSelect('YES')}
|
||||||
>
|
>
|
||||||
Bet YES
|
{isPseudoNumeric ? 'HIGHER' : 'Bet YES'}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{replaceNoButton ? (
|
{replaceNoButton ? (
|
||||||
|
@ -58,7 +60,7 @@ export function YesNoSelector(props: {
|
||||||
)}
|
)}
|
||||||
onClick={() => onSelect('NO')}
|
onClick={() => onSelect('NO')}
|
||||||
>
|
>
|
||||||
Bet NO
|
{isPseudoNumeric ? 'LOWER' : 'Bet NO'}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
|
|
|
@ -74,9 +74,7 @@ export function useMembers(group: Group) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listMembers(group: Group) {
|
export async function listMembers(group: Group) {
|
||||||
return (await Promise.all(group.memberIds.map(getUser))).filter(
|
return await Promise.all(group.memberIds.map(getUser))
|
||||||
(user) => user
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useGroupsWithContract = (contractId: string | undefined) => {
|
export const useGroupsWithContract = (contractId: string | undefined) => {
|
||||||
|
|
|
@ -102,10 +102,13 @@ export async function addUserToGroupViaSlug(groupSlug: string, userId: string) {
|
||||||
console.error(`Group not found: ${groupSlug}`)
|
console.error(`Group not found: ${groupSlug}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return await joinGroup(group, userId)
|
return await addUserToGroup(group, userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function joinGroup(group: Group, userId: string): Promise<Group> {
|
export async function addUserToGroup(
|
||||||
|
group: Group,
|
||||||
|
userId: string
|
||||||
|
): Promise<Group> {
|
||||||
const { memberIds } = group
|
const { memberIds } = group
|
||||||
if (memberIds.includes(userId)) {
|
if (memberIds.includes(userId)) {
|
||||||
return group
|
return group
|
||||||
|
@ -125,3 +128,14 @@ export async function leaveGroup(group: Group, userId: string): Promise<Group> {
|
||||||
await updateGroup(newGroup, { memberIds: uniq(newMemberIds) })
|
await updateGroup(newGroup, { memberIds: uniq(newMemberIds) })
|
||||||
return newGroup
|
return newGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function addContractToGroup(group: Group, contractId: string) {
|
||||||
|
return await updateGroup(group, {
|
||||||
|
contractIds: uniq([...group.contractIds, contractId]),
|
||||||
|
})
|
||||||
|
.then(() => group)
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('error adding contract to group', err)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -144,10 +144,12 @@ export function ContractPageContent(
|
||||||
|
|
||||||
const isCreator = user?.id === creatorId
|
const isCreator = user?.id === creatorId
|
||||||
const isBinary = outcomeType === 'BINARY'
|
const isBinary = outcomeType === 'BINARY'
|
||||||
|
const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC'
|
||||||
const isNumeric = outcomeType === 'NUMERIC'
|
const isNumeric = outcomeType === 'NUMERIC'
|
||||||
const allowTrade = tradingAllowed(contract)
|
const allowTrade = tradingAllowed(contract)
|
||||||
const allowResolve = !isResolved && isCreator && !!user
|
const allowResolve = !isResolved && isCreator && !!user
|
||||||
const hasSidePanel = (isBinary || isNumeric) && (allowTrade || allowResolve)
|
const hasSidePanel =
|
||||||
|
(isBinary || isNumeric || isPseudoNumeric) && (allowTrade || allowResolve)
|
||||||
|
|
||||||
const ogCardProps = getOpenGraphProps(contract)
|
const ogCardProps = getOpenGraphProps(contract)
|
||||||
|
|
||||||
|
@ -170,7 +172,7 @@ export function ContractPageContent(
|
||||||
<BetPanel className="hidden xl:flex" contract={contract} />
|
<BetPanel className="hidden xl:flex" contract={contract} />
|
||||||
))}
|
))}
|
||||||
{allowResolve &&
|
{allowResolve &&
|
||||||
(isNumeric ? (
|
(isNumeric || isPseudoNumeric ? (
|
||||||
<NumericResolutionPanel creator={user} contract={contract} />
|
<NumericResolutionPanel creator={user} contract={contract} />
|
||||||
) : (
|
) : (
|
||||||
<ResolutionPanel creator={user} contract={contract} />
|
<ResolutionPanel creator={user} contract={contract} />
|
||||||
|
@ -210,10 +212,11 @@ export function ContractPageContent(
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ContractOverview contract={contract} bets={bets} />
|
<ContractOverview contract={contract} bets={bets} />
|
||||||
|
|
||||||
{isNumeric && (
|
{isNumeric && (
|
||||||
<AlertBox
|
<AlertBox
|
||||||
title="Warning"
|
title="Warning"
|
||||||
text="Numeric markets were introduced as an experimental feature and are now deprecated."
|
text="Distributional numeric markets were introduced as an experimental feature and are now deprecated."
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ import {
|
||||||
import { formatMoney } from 'common/util/format'
|
import { formatMoney } from 'common/util/format'
|
||||||
import { removeUndefinedProps } from 'common/util/object'
|
import { removeUndefinedProps } from 'common/util/object'
|
||||||
import { ChoicesToggleGroup } from 'web/components/choices-toggle-group'
|
import { ChoicesToggleGroup } from 'web/components/choices-toggle-group'
|
||||||
import { getGroup, updateGroup } from 'web/lib/firebase/groups'
|
import { addContractToGroup, getGroup } from 'web/lib/firebase/groups'
|
||||||
import { Group } from 'common/group'
|
import { Group } from 'common/group'
|
||||||
import { useTracking } from 'web/hooks/use-tracking'
|
import { useTracking } from 'web/hooks/use-tracking'
|
||||||
import { useWarnUnsavedChanges } from 'web/hooks/use-warn-unsaved-changes'
|
import { useWarnUnsavedChanges } from 'web/hooks/use-warn-unsaved-changes'
|
||||||
|
@ -85,8 +85,12 @@ export function NewContract(props: {
|
||||||
const { creator, question, groupId } = props
|
const { creator, question, groupId } = props
|
||||||
const [outcomeType, setOutcomeType] = useState<outcomeType>('BINARY')
|
const [outcomeType, setOutcomeType] = useState<outcomeType>('BINARY')
|
||||||
const [initialProb] = useState(50)
|
const [initialProb] = useState(50)
|
||||||
|
|
||||||
const [minString, setMinString] = useState('')
|
const [minString, setMinString] = useState('')
|
||||||
const [maxString, setMaxString] = useState('')
|
const [maxString, setMaxString] = useState('')
|
||||||
|
const [isLogScale, setIsLogScale] = useState(false)
|
||||||
|
const [initialValueString, setInitialValueString] = useState('')
|
||||||
|
|
||||||
const [description, setDescription] = useState('')
|
const [description, setDescription] = useState('')
|
||||||
// const [tagText, setTagText] = useState<string>(tag ?? '')
|
// const [tagText, setTagText] = useState<string>(tag ?? '')
|
||||||
// const tags = parseWordsAsTags(tagText)
|
// const tags = parseWordsAsTags(tagText)
|
||||||
|
@ -129,6 +133,18 @@ export function NewContract(props: {
|
||||||
|
|
||||||
const min = minString ? parseFloat(minString) : undefined
|
const min = minString ? parseFloat(minString) : undefined
|
||||||
const max = maxString ? parseFloat(maxString) : undefined
|
const max = maxString ? parseFloat(maxString) : undefined
|
||||||
|
const initialValue = initialValueString
|
||||||
|
? parseFloat(initialValueString)
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
const adjustIsLog = () => {
|
||||||
|
if (min === undefined || max === undefined) return
|
||||||
|
const lengthDiff = Math.log10(max - min)
|
||||||
|
if (lengthDiff > 2) {
|
||||||
|
setIsLogScale(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// get days from today until the end of this year:
|
// get days from today until the end of this year:
|
||||||
const daysLeftInTheYear = dayjs().endOf('year').diff(dayjs(), 'day')
|
const daysLeftInTheYear = dayjs().endOf('year').diff(dayjs(), 'day')
|
||||||
|
|
||||||
|
@ -145,13 +161,16 @@ export function NewContract(props: {
|
||||||
// closeTime must be in the future
|
// closeTime must be in the future
|
||||||
closeTime &&
|
closeTime &&
|
||||||
closeTime > Date.now() &&
|
closeTime > Date.now() &&
|
||||||
(outcomeType !== 'NUMERIC' ||
|
(outcomeType !== 'PSEUDO_NUMERIC' ||
|
||||||
(min !== undefined &&
|
(min !== undefined &&
|
||||||
max !== undefined &&
|
max !== undefined &&
|
||||||
|
initialValue !== undefined &&
|
||||||
isFinite(min) &&
|
isFinite(min) &&
|
||||||
isFinite(max) &&
|
isFinite(max) &&
|
||||||
min < max &&
|
min < max &&
|
||||||
max - min > 0.01))
|
max - min > 0.01 &&
|
||||||
|
min < initialValue &&
|
||||||
|
initialValue < max))
|
||||||
|
|
||||||
function setCloseDateInDays(days: number) {
|
function setCloseDateInDays(days: number) {
|
||||||
const newCloseDate = dayjs().add(days, 'day').format('YYYY-MM-DD')
|
const newCloseDate = dayjs().add(days, 'day').format('YYYY-MM-DD')
|
||||||
|
@ -175,6 +194,8 @@ export function NewContract(props: {
|
||||||
closeTime,
|
closeTime,
|
||||||
min,
|
min,
|
||||||
max,
|
max,
|
||||||
|
initialValue,
|
||||||
|
isLogScale: (min ?? 0) < 0 ? false : isLogScale,
|
||||||
groupId: selectedGroup?.id,
|
groupId: selectedGroup?.id,
|
||||||
tags: category ? [category] : undefined,
|
tags: category ? [category] : undefined,
|
||||||
})
|
})
|
||||||
|
@ -186,9 +207,7 @@ export function NewContract(props: {
|
||||||
isFree: false,
|
isFree: false,
|
||||||
})
|
})
|
||||||
if (result && selectedGroup) {
|
if (result && selectedGroup) {
|
||||||
await updateGroup(selectedGroup, {
|
await addContractToGroup(selectedGroup, result.id)
|
||||||
contractIds: [...selectedGroup.contractIds, result.id],
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await router.push(contractPath(result as Contract))
|
await router.push(contractPath(result as Contract))
|
||||||
|
@ -222,6 +241,7 @@ export function NewContract(props: {
|
||||||
choicesMap={{
|
choicesMap={{
|
||||||
'Yes / No': 'BINARY',
|
'Yes / No': 'BINARY',
|
||||||
'Free response': 'FREE_RESPONSE',
|
'Free response': 'FREE_RESPONSE',
|
||||||
|
Numeric: 'PSEUDO_NUMERIC',
|
||||||
}}
|
}}
|
||||||
isSubmitting={isSubmitting}
|
isSubmitting={isSubmitting}
|
||||||
className={'col-span-4'}
|
className={'col-span-4'}
|
||||||
|
@ -234,8 +254,9 @@ export function NewContract(props: {
|
||||||
|
|
||||||
<Spacer h={6} />
|
<Spacer h={6} />
|
||||||
|
|
||||||
{outcomeType === 'NUMERIC' && (
|
{outcomeType === 'PSEUDO_NUMERIC' && (
|
||||||
<div className="form-control items-start">
|
<>
|
||||||
|
<div className="form-control mb-2 items-start">
|
||||||
<label className="label gap-2">
|
<label className="label gap-2">
|
||||||
<span className="mb-1">Range</span>
|
<span className="mb-1">Range</span>
|
||||||
<InfoTooltip text="The minimum and maximum numbers across the numeric range." />
|
<InfoTooltip text="The minimum and maximum numbers across the numeric range." />
|
||||||
|
@ -248,6 +269,7 @@ export function NewContract(props: {
|
||||||
placeholder="MIN"
|
placeholder="MIN"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
onChange={(e) => setMinString(e.target.value)}
|
onChange={(e) => setMinString(e.target.value)}
|
||||||
|
onBlur={adjustIsLog}
|
||||||
min={Number.MIN_SAFE_INTEGER}
|
min={Number.MIN_SAFE_INTEGER}
|
||||||
max={Number.MAX_SAFE_INTEGER}
|
max={Number.MAX_SAFE_INTEGER}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
|
@ -259,14 +281,63 @@ export function NewContract(props: {
|
||||||
placeholder="MAX"
|
placeholder="MAX"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
onChange={(e) => setMaxString(e.target.value)}
|
onChange={(e) => setMaxString(e.target.value)}
|
||||||
|
onBlur={adjustIsLog}
|
||||||
min={Number.MIN_SAFE_INTEGER}
|
min={Number.MIN_SAFE_INTEGER}
|
||||||
max={Number.MAX_SAFE_INTEGER}
|
max={Number.MAX_SAFE_INTEGER}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
value={maxString}
|
value={maxString}
|
||||||
/>
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
|
{!(min !== undefined && min < 0) && (
|
||||||
|
<Row className="mt-1 ml-2 mb-2 items-center">
|
||||||
|
<span className="mr-2 text-sm">Log scale</span>{' '}
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={isLogScale}
|
||||||
|
onChange={() => setIsLogScale(!isLogScale)}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{min !== undefined && max !== undefined && min >= max && (
|
||||||
|
<div className="mt-2 mb-2 text-sm text-red-500">
|
||||||
|
The maximum value must be greater than the minimum.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="form-control mb-2 items-start">
|
||||||
|
<label className="label gap-2">
|
||||||
|
<span className="mb-1">Initial value</span>
|
||||||
|
<InfoTooltip text="The starting value for this market. Should be in between min and max values." />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<Row className="gap-2">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="input input-bordered"
|
||||||
|
placeholder="Initial value"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
onChange={(e) => setInitialValueString(e.target.value)}
|
||||||
|
max={Number.MAX_SAFE_INTEGER}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
value={initialValueString ?? ''}
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
{initialValue !== undefined &&
|
||||||
|
min !== undefined &&
|
||||||
|
max !== undefined &&
|
||||||
|
min < max &&
|
||||||
|
(initialValue <= min || initialValue >= max) && (
|
||||||
|
<div className="mt-2 mb-2 text-sm text-red-500">
|
||||||
|
Initial value must be in between {min} and {max}.{' '}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="form-control max-w-[265px] items-start">
|
<div className="form-control max-w-[265px] items-start">
|
||||||
<label className="label gap-2">
|
<label className="label gap-2">
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
BinaryResolutionOrChance,
|
BinaryResolutionOrChance,
|
||||||
FreeResponseResolutionOrChance,
|
FreeResponseResolutionOrChance,
|
||||||
NumericResolutionOrExpectation,
|
NumericResolutionOrExpectation,
|
||||||
|
PseudoNumericResolutionOrExpectation,
|
||||||
} from 'web/components/contract/contract-card'
|
} from 'web/components/contract/contract-card'
|
||||||
import { ContractDetails } from 'web/components/contract/contract-details'
|
import { ContractDetails } from 'web/components/contract/contract-details'
|
||||||
import { ContractProbGraph } from 'web/components/contract/contract-prob-graph'
|
import { ContractProbGraph } from 'web/components/contract/contract-prob-graph'
|
||||||
|
@ -79,6 +80,7 @@ function ContractEmbed(props: { contract: Contract; bets: Bet[] }) {
|
||||||
const { question, outcomeType } = contract
|
const { question, outcomeType } = contract
|
||||||
|
|
||||||
const isBinary = outcomeType === 'BINARY'
|
const isBinary = outcomeType === 'BINARY'
|
||||||
|
const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC'
|
||||||
|
|
||||||
const href = `https://${DOMAIN}${contractPath(contract)}`
|
const href = `https://${DOMAIN}${contractPath(contract)}`
|
||||||
|
|
||||||
|
@ -110,13 +112,18 @@ function ContractEmbed(props: { contract: Contract; bets: Bet[] }) {
|
||||||
|
|
||||||
{isBinary && (
|
{isBinary && (
|
||||||
<Row className="items-center gap-4">
|
<Row className="items-center gap-4">
|
||||||
{/* this fails typechecking, but it doesn't explode because we will
|
<BetRow contract={contract} betPanelClassName="scale-75" />
|
||||||
never */}
|
|
||||||
<BetRow contract={contract as any} betPanelClassName="scale-75" />
|
|
||||||
<BinaryResolutionOrChance contract={contract} />
|
<BinaryResolutionOrChance contract={contract} />
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{isPseudoNumeric && (
|
||||||
|
<Row className="items-center gap-4">
|
||||||
|
<BetRow contract={contract} betPanelClassName="scale-75" />
|
||||||
|
<PseudoNumericResolutionOrExpectation contract={contract} />
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
|
||||||
{outcomeType === 'FREE_RESPONSE' && (
|
{outcomeType === 'FREE_RESPONSE' && (
|
||||||
<FreeResponseResolutionOrChance
|
<FreeResponseResolutionOrChance
|
||||||
contract={contract}
|
contract={contract}
|
||||||
|
@ -133,7 +140,7 @@ function ContractEmbed(props: { contract: Contract; bets: Bet[] }) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mx-1" style={{ paddingBottom }}>
|
<div className="mx-1" style={{ paddingBottom }}>
|
||||||
{isBinary && (
|
{(isBinary || isPseudoNumeric) && (
|
||||||
<ContractProbGraph
|
<ContractProbGraph
|
||||||
contract={contract}
|
contract={contract}
|
||||||
bets={bets}
|
bets={bets}
|
||||||
|
|
|
@ -4,12 +4,14 @@ import { Group } from 'common/group'
|
||||||
import { Page } from 'web/components/page'
|
import { Page } from 'web/components/page'
|
||||||
import { Title } from 'web/components/title'
|
import { Title } from 'web/components/title'
|
||||||
import { listAllBets } from 'web/lib/firebase/bets'
|
import { listAllBets } from 'web/lib/firebase/bets'
|
||||||
import { Contract, listenForUserContracts } from 'web/lib/firebase/contracts'
|
import { Contract } from 'web/lib/firebase/contracts'
|
||||||
import {
|
import {
|
||||||
groupPath,
|
groupPath,
|
||||||
getGroupBySlug,
|
getGroupBySlug,
|
||||||
getGroupContracts,
|
getGroupContracts,
|
||||||
updateGroup,
|
updateGroup,
|
||||||
|
addContractToGroup,
|
||||||
|
addUserToGroup,
|
||||||
} from 'web/lib/firebase/groups'
|
} from 'web/lib/firebase/groups'
|
||||||
import { Row } from 'web/components/layout/row'
|
import { Row } from 'web/components/layout/row'
|
||||||
import { UserLink } from 'web/components/user-page'
|
import { UserLink } from 'web/components/user-page'
|
||||||
|
@ -39,7 +41,6 @@ import React, { useEffect, useState } from 'react'
|
||||||
import { GroupChat } from 'web/components/groups/group-chat'
|
import { GroupChat } from 'web/components/groups/group-chat'
|
||||||
import { LoadingIndicator } from 'web/components/loading-indicator'
|
import { LoadingIndicator } from 'web/components/loading-indicator'
|
||||||
import { Modal } from 'web/components/layout/modal'
|
import { Modal } from 'web/components/layout/modal'
|
||||||
import { PlusIcon } from '@heroicons/react/outline'
|
|
||||||
import { checkAgainstQuery } from 'web/hooks/use-sort-and-query-params'
|
import { checkAgainstQuery } from 'web/hooks/use-sort-and-query-params'
|
||||||
import { ChoicesToggleGroup } from 'web/components/choices-toggle-group'
|
import { ChoicesToggleGroup } from 'web/components/choices-toggle-group'
|
||||||
import { toast } from 'react-hot-toast'
|
import { toast } from 'react-hot-toast'
|
||||||
|
@ -48,6 +49,7 @@ import ShortToggle from 'web/components/widgets/short-toggle'
|
||||||
import { ShareIconButton } from 'web/components/share-icon-button'
|
import { ShareIconButton } from 'web/components/share-icon-button'
|
||||||
import { REFERRAL_AMOUNT } from 'common/user'
|
import { REFERRAL_AMOUNT } from 'common/user'
|
||||||
import { SiteLink } from 'web/components/site-link'
|
import { SiteLink } from 'web/components/site-link'
|
||||||
|
import { ContractSearch } from 'web/components/contract-search'
|
||||||
|
|
||||||
export const getStaticProps = fromPropz(getStaticPropz)
|
export const getStaticProps = fromPropz(getStaticPropz)
|
||||||
export async function getStaticPropz(props: { params: { slugs: string[] } }) {
|
export async function getStaticPropz(props: { params: { slugs: string[] } }) {
|
||||||
|
@ -509,75 +511,46 @@ function GroupLeaderboards(props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
function AddContractButton(props: { group: Group; user: User }) {
|
function AddContractButton(props: { group: Group; user: User }) {
|
||||||
const { group, user } = props
|
const { group } = props
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const [contracts, setContracts] = useState<Contract[] | undefined>(undefined)
|
|
||||||
const [query, setQuery] = useState('')
|
|
||||||
|
|
||||||
useEffect(() => {
|
async function addContractToCurrentGroup(contract: Contract) {
|
||||||
return listenForUserContracts(user.id, (contracts) => {
|
await addContractToGroup(group, contract.id)
|
||||||
setContracts(contracts.filter((c) => !group.contractIds.includes(c.id)))
|
|
||||||
})
|
|
||||||
}, [group.contractIds, user.id])
|
|
||||||
|
|
||||||
async function addContractToGroup(contract: Contract) {
|
|
||||||
await updateGroup(group, {
|
|
||||||
...group,
|
|
||||||
contractIds: [...group.contractIds, contract.id],
|
|
||||||
})
|
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO use find-active-contracts to sort by?
|
|
||||||
const matches = sortBy(contracts, [
|
|
||||||
(contract) => -1 * contract.createdTime,
|
|
||||||
]).filter(
|
|
||||||
(c) =>
|
|
||||||
checkAgainstQuery(query, c.question) ||
|
|
||||||
checkAgainstQuery(query, c.description) ||
|
|
||||||
checkAgainstQuery(query, c.tags.flat().join(' '))
|
|
||||||
)
|
|
||||||
const debouncedQuery = debounce(setQuery, 50)
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Modal open={open} setOpen={setOpen}>
|
<Modal open={open} setOpen={setOpen} className={'sm:p-0'}>
|
||||||
<Col className={'max-h-[60vh] w-full gap-4 rounded-md bg-white p-8'}>
|
<Col
|
||||||
|
className={
|
||||||
|
'max-h-[60vh] min-h-[60vh] w-full gap-4 rounded-md bg-white p-8'
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className={'text-lg text-indigo-700'}>
|
<div className={'text-lg text-indigo-700'}>
|
||||||
Add a question to your group
|
Add a question to your group
|
||||||
</div>
|
</div>
|
||||||
<input
|
<div className={'overflow-y-scroll p-1'}>
|
||||||
type="text"
|
<ContractSearch
|
||||||
onChange={(e) => debouncedQuery(e.target.value)}
|
hideOrderSelector={true}
|
||||||
placeholder="Search your questions"
|
onContractClick={addContractToCurrentGroup}
|
||||||
className="input input-bordered mb-4 w-full"
|
showCategorySelector={false}
|
||||||
/>
|
|
||||||
<div className={'overflow-y-scroll'}>
|
|
||||||
{contracts ? (
|
|
||||||
<ContractsGrid
|
|
||||||
contracts={matches}
|
|
||||||
loadMore={() => {}}
|
|
||||||
hasMore={false}
|
|
||||||
onContractClick={(contract) => {
|
|
||||||
addContractToGroup(contract)
|
|
||||||
}}
|
|
||||||
overrideGridClassName={'flex grid-cols-1 flex-col gap-3 p-1'}
|
overrideGridClassName={'flex grid-cols-1 flex-col gap-3 p-1'}
|
||||||
|
showPlaceHolder={true}
|
||||||
hideQuickBet={true}
|
hideQuickBet={true}
|
||||||
|
additionalFilter={{ excludeContractIds: group.contractIds }}
|
||||||
/>
|
/>
|
||||||
) : (
|
|
||||||
<LoadingIndicator />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
</Modal>
|
</Modal>
|
||||||
<Row className={'items-center justify-center'}>
|
<Row className={'items-center justify-center'}>
|
||||||
<button
|
<button
|
||||||
className={
|
className={
|
||||||
'btn btn-sm btn-outline cursor-pointer gap-2 whitespace-nowrap text-sm normal-case'
|
'btn btn-md btn-outline cursor-pointer gap-2 whitespace-nowrap text-sm normal-case'
|
||||||
}
|
}
|
||||||
onClick={() => setOpen(true)}
|
onClick={() => setOpen(true)}
|
||||||
>
|
>
|
||||||
<PlusIcon className="mr-1 h-5 w-5" />
|
Add an old question
|
||||||
Add old questions to this group
|
|
||||||
</button>
|
</button>
|
||||||
</Row>
|
</Row>
|
||||||
</>
|
</>
|
||||||
|
@ -591,17 +564,11 @@ function JoinGroupButton(props: {
|
||||||
const { group, user } = props
|
const { group, user } = props
|
||||||
function joinGroup() {
|
function joinGroup() {
|
||||||
if (user && !group.memberIds.includes(user.id)) {
|
if (user && !group.memberIds.includes(user.id)) {
|
||||||
toast.promise(
|
toast.promise(addUserToGroup(group, user.id), {
|
||||||
updateGroup(group, {
|
|
||||||
...group,
|
|
||||||
memberIds: [...group.memberIds, user.id],
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
loading: 'Joining group...',
|
loading: 'Joining group...',
|
||||||
success: 'Joined group!',
|
success: 'Joined group!',
|
||||||
error: "Couldn't join group",
|
error: "Couldn't join group",
|
||||||
}
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -67,7 +67,9 @@ export default function Leaderboards(props: {
|
||||||
<Col className="mx-4 items-center gap-10 lg:flex-row">
|
<Col className="mx-4 items-center gap-10 lg:flex-row">
|
||||||
{!isLoading ? (
|
{!isLoading ? (
|
||||||
<>
|
<>
|
||||||
{period === 'allTime' || period === 'daily' ? ( //TODO: show other periods once they're available
|
{period === 'allTime' ||
|
||||||
|
period == 'weekly' ||
|
||||||
|
period === 'daily' ? ( //TODO: show other periods once they're available
|
||||||
<Leaderboard
|
<Leaderboard
|
||||||
title="🏅 Top bettors"
|
title="🏅 Top bettors"
|
||||||
users={topTradersState}
|
users={topTradersState}
|
||||||
|
|
|
@ -64,7 +64,7 @@ export default function LinkPage() {
|
||||||
<Col className="w-full px-8">
|
<Col className="w-full px-8">
|
||||||
<Title text="Manalinks" />
|
<Title text="Manalinks" />
|
||||||
<Tabs
|
<Tabs
|
||||||
className={'pb-2 pt-1 '}
|
labelClassName={'pb-2 pt-1 '}
|
||||||
defaultIndex={0}
|
defaultIndex={0}
|
||||||
tabs={[
|
tabs={[
|
||||||
{
|
{
|
||||||
|
|
|
@ -86,7 +86,7 @@ export default function Notifications() {
|
||||||
<div className={'p-2 sm:p-4'}>
|
<div className={'p-2 sm:p-4'}>
|
||||||
<Title text={'Notifications'} className={'hidden md:block'} />
|
<Title text={'Notifications'} className={'hidden md:block'} />
|
||||||
<Tabs
|
<Tabs
|
||||||
className={'pb-2 pt-1 '}
|
labelClassName={'pb-2 pt-1 '}
|
||||||
defaultIndex={0}
|
defaultIndex={0}
|
||||||
tabs={[
|
tabs={[
|
||||||
{
|
{
|
||||||
|
|
29
yarn.lock
29
yarn.lock
|
@ -3875,13 +3875,6 @@ binary-extensions@^2.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
|
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
|
||||||
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
|
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
|
||||||
|
|
||||||
biskviit@1.0.1:
|
|
||||||
version "1.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/biskviit/-/biskviit-1.0.1.tgz#037a0cd4b71b9e331fd90a1122de17dc49e420a7"
|
|
||||||
integrity sha512-VGCXdHbdbpEkFgtjkeoBN8vRlbj1ZRX2/mxhE8asCCRalUx2nBzOomLJv8Aw/nRt5+ccDb+tPKidg4XxcfGW4w==
|
|
||||||
dependencies:
|
|
||||||
psl "^1.1.7"
|
|
||||||
|
|
||||||
bluebird@^3.7.1:
|
bluebird@^3.7.1:
|
||||||
version "3.7.2"
|
version "3.7.2"
|
||||||
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
|
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
|
||||||
|
@ -5237,13 +5230,6 @@ encodeurl@~1.0.2:
|
||||||
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
|
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
|
||||||
integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
|
integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
|
||||||
|
|
||||||
encoding@0.1.12:
|
|
||||||
version "0.1.12"
|
|
||||||
resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb"
|
|
||||||
integrity sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=
|
|
||||||
dependencies:
|
|
||||||
iconv-lite "~0.4.13"
|
|
||||||
|
|
||||||
end-of-stream@^1.1.0, end-of-stream@^1.4.1:
|
end-of-stream@^1.1.0, end-of-stream@^1.4.1:
|
||||||
version "1.4.4"
|
version "1.4.4"
|
||||||
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
|
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
|
||||||
|
@ -5817,14 +5803,6 @@ fetch-blob@^3.1.2, fetch-blob@^3.1.4:
|
||||||
node-domexception "^1.0.0"
|
node-domexception "^1.0.0"
|
||||||
web-streams-polyfill "^3.0.3"
|
web-streams-polyfill "^3.0.3"
|
||||||
|
|
||||||
fetch@1.1.0:
|
|
||||||
version "1.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/fetch/-/fetch-1.1.0.tgz#0a8279f06be37f9f0ebb567560a30a480da59a2e"
|
|
||||||
integrity sha1-CoJ58Gvjf58Ou1Z1YKMKSA2lmi4=
|
|
||||||
dependencies:
|
|
||||||
biskviit "1.0.1"
|
|
||||||
encoding "0.1.12"
|
|
||||||
|
|
||||||
file-entry-cache@^6.0.1:
|
file-entry-cache@^6.0.1:
|
||||||
version "6.0.1"
|
version "6.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
|
resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
|
||||||
|
@ -6782,7 +6760,7 @@ human-signals@^2.1.0:
|
||||||
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
|
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
|
||||||
integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
|
integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
|
||||||
|
|
||||||
iconv-lite@0.4.24, iconv-lite@~0.4.13:
|
iconv-lite@0.4.24:
|
||||||
version "0.4.24"
|
version "0.4.24"
|
||||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
|
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
|
||||||
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
|
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
|
||||||
|
@ -9151,11 +9129,6 @@ pseudomap@^1.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
|
resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
|
||||||
integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM=
|
integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM=
|
||||||
|
|
||||||
psl@^1.1.7:
|
|
||||||
version "1.8.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24"
|
|
||||||
integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==
|
|
||||||
|
|
||||||
pump@^3.0.0:
|
pump@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
|
resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user