Compare commits

..

1 Commits

Author SHA1 Message Date
Marshall Polaris
9815d38f1f Destroy the blue bubble 2022-06-01 13:05:50 -07:00
675 changed files with 19512 additions and 57667 deletions

View File

@ -52,4 +52,4 @@ jobs:
- name: Run Typescript checker on cloud functions - name: Run Typescript checker on cloud functions
if: ${{ success() || failure() }} if: ${{ success() || failure() }}
working-directory: functions working-directory: functions
run: tsc -b -v --pretty run: tsc --pretty --project tsconfig.json --noEmit

View File

@ -1,43 +0,0 @@
name: Reformat main
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
branches: [main]
env:
FORCE_COLOR: 3
NEXT_TELEMETRY_DISABLED: 1
# mqp - i generated a personal token to use for these writes -- it's unclear
# why, but the default token didn't work, even when i gave it max permissions
jobs:
prettify:
name: Auto-prettify
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
token: ${{ secrets.FORMATTER_ACCESS_TOKEN }}
- name: Restore cached node_modules
uses: actions/cache@v2
with:
path: '**/node_modules'
key: ${{ runner.os }}-${{ matrix.node-version }}-nodemodules-${{ hashFiles('**/yarn.lock') }}
- name: Install missing dependencies
run: yarn install --prefer-offline --frozen-lockfile
- name: Run Prettier on web client
working-directory: web
run: yarn format
- name: Commit any Prettier changes
uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: Auto-prettification
branch: ${{ github.head_ref }}

View File

@ -1,43 +0,0 @@
name: Run linter (remove unused imports)
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
branches: [main]
env:
FORCE_COLOR: 3
NEXT_TELEMETRY_DISABLED: 1
# mqp - i generated a personal token to use for these writes -- it's unclear
# why, but the default token didn't work, even when i gave it max permissions
jobs:
lint:
name: Auto-lint
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
token: ${{ secrets.FORMATTER_ACCESS_TOKEN }}
- name: Restore cached node_modules
uses: actions/cache@v2
with:
path: '**/node_modules'
key: ${{ runner.os }}-${{ matrix.node-version }}-nodemodules-${{ hashFiles('**/yarn.lock') }}
- name: Install missing dependencies
run: yarn install --prefer-offline --frozen-lockfile
- name: Run lint script
run: yarn lint
- name: Commit any lint changes
if: always()
uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: Auto-remove unused imports
branch: ${{ github.head_ref }}

View File

@ -1,17 +0,0 @@
name: Merge main into main2 on every commit
on:
push:
branches:
- 'main'
jobs:
merge-branch:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Merge main -> main2
uses: devmasx/merge-branch@master
with:
type: now
target_branch: main2
github_token: ${{ github.token }}

2
.gitignore vendored
View File

@ -3,5 +3,3 @@
.vercel .vercel
node_modules node_modules
yarn-error.log yarn-error.log
firebase-debug.log

View File

@ -1,14 +0,0 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
// List of extensions which should be recommended for users of this workspace.
"recommendations": [
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"toba.vsfire",
"bradlc.vscode-tailwindcss"
],
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
"unwantedRecommendations": []
}

16
.vscode/launch.json vendored
View File

@ -1,16 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Attach to Chrome",
"type": "chrome",
"request": "attach",
"port": 9222, // chrome needs to be started with the parameter "--remote-debugging-port=9222"
"urlFilter": "http://localhost:3000/*",
"webRoot": "${workspaceFolder}/web"
}
]
}

11
.vscode/settings.json vendored
View File

@ -1,13 +1,4 @@
{ {
"javascript.preferences.importModuleSpecifier": "shortest", "javascript.preferences.importModuleSpecifier": "shortest",
"typescript.preferences.importModuleSpecifier": "shortest", "typescript.preferences.importModuleSpecifier": "shortest"
"files.eol": "\n",
"search.exclude": {
"**/node_modules": true,
"**/package-lock.json": true,
"**/yarn.lock": true
},
"editor.formatOnSave": true,
"editor.formatOnPaste": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
} }

View File

@ -1,7 +1,6 @@
module.exports = { module.exports = {
plugins: ['lodash', 'unused-imports'], plugins: ['lodash'],
extends: ['eslint:recommended'], extends: ['eslint:recommended'],
ignorePatterns: ['lib'],
env: { env: {
browser: true, browser: true,
node: true, node: true,
@ -18,22 +17,13 @@ module.exports = {
}, },
rules: { rules: {
'@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': [
'warn',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
},
],
'unused-imports/no-unused-imports': 'warn',
}, },
}, },
], ],
rules: { rules: {
'no-extra-semi': 'off', 'no-extra-semi': 'off',
'no-unused-vars': 'off',
'no-constant-condition': ['error', { checkLoops: false }], 'no-constant-condition': ['error', { checkLoops: false }],
'linebreak-style': ['error', 'unix'],
'lodash/import-scope': [2, 'member'], 'lodash/import-scope': [2, 'member'],
}, },
} }

3
common/.gitignore vendored
View File

@ -1,5 +1,6 @@
# Compiled JavaScript files # Compiled JavaScript files
lib/ lib/**/*.js
lib/**/*.js.map
# TypeScript v1 declaration files # TypeScript v1 declaration files
typings/ typings/

View File

@ -1 +0,0 @@
save-prefix ""

View File

@ -1,30 +1,33 @@
import { getCpmmLiquidity } from './calculate-cpmm' import { addCpmmLiquidity, getCpmmLiquidity } from './calculate-cpmm'
import { CPMMContract } from './contract' import { CPMMContract } from './contract'
import { LiquidityProvision } from './liquidity-provision' import { LiquidityProvision } from './liquidity-provision'
import { User } from './user'
export const getNewLiquidityProvision = ( export const getNewLiquidityProvision = (
userId: string, user: User,
amount: number, amount: number,
contract: CPMMContract, contract: CPMMContract,
newLiquidityProvisionId: string newLiquidityProvisionId: string
) => { ) => {
const { pool, p, totalLiquidity, subsidyPool } = contract const { pool, p, totalLiquidity } = contract
const liquidity = getCpmmLiquidity(pool, p) const { newPool, newP } = addCpmmLiquidity(pool, p, amount)
const liquidity =
getCpmmLiquidity(newPool, newP) - getCpmmLiquidity(pool, newP)
const newLiquidityProvision: LiquidityProvision = { const newLiquidityProvision: LiquidityProvision = {
id: newLiquidityProvisionId, id: newLiquidityProvisionId,
userId: userId, userId: user.id,
contractId: contract.id, contractId: contract.id,
amount, amount,
pool, pool: newPool,
p, p: newP,
liquidity, liquidity,
createdTime: Date.now(), createdTime: Date.now(),
} }
const newTotalLiquidity = (totalLiquidity ?? 0) + amount const newTotalLiquidity = (totalLiquidity ?? 0) + amount
const newSubsidyPool = (subsidyPool ?? 0) + amount
return { newLiquidityProvision, newTotalLiquidity, newSubsidyPool } return { newLiquidityProvision, newPool, newP, newTotalLiquidity }
} }

View File

@ -5,22 +5,19 @@ import {
CPMMBinaryContract, CPMMBinaryContract,
DPMBinaryContract, DPMBinaryContract,
FreeResponseContract, FreeResponseContract,
MultipleChoiceContract,
NumericContract, NumericContract,
} from './contract' } from './contract'
import { User } from './user' import { User } from './user'
import { LiquidityProvision } from './liquidity-provision' import { LiquidityProvision } from './liquidity-provision'
import { noFees } from './fees' import { noFees } from './fees'
import { Answer } from './answer'
export const FIXED_ANTE = 100
// deprecated
export const PHANTOM_ANTE = 0.001
export const MINIMUM_ANTE = 50
export const HOUSE_LIQUIDITY_PROVIDER_ID = 'IPTOzEqrpkWmEzh6hwvAyY9PqFb2' // @ManifoldMarkets' id export const HOUSE_LIQUIDITY_PROVIDER_ID = 'IPTOzEqrpkWmEzh6hwvAyY9PqFb2' // @ManifoldMarkets' id
export const DEV_HOUSE_LIQUIDITY_PROVIDER_ID = '94YYTk1AFWfbWMpfYcvnnwI1veP2' // @ManifoldMarkets' id
export const UNIQUE_BETTOR_LIQUIDITY_AMOUNT = 20
type NormalizedBet<T extends Bet = Bet> = Omit<
T,
'userAvatarUrl' | 'userName' | 'userUsername'
>
export function getCpmmInitialLiquidity( export function getCpmmInitialLiquidity(
providerId: string, providerId: string,
@ -57,7 +54,7 @@ export function getAnteBets(
const { createdTime } = contract const { createdTime } = contract
const yesBet: NormalizedBet = { const yesBet: Bet = {
id: yesAnteId, id: yesAnteId,
userId: creator.id, userId: creator.id,
contractId: contract.id, contractId: contract.id,
@ -71,7 +68,7 @@ export function getAnteBets(
fees: noFees, fees: noFees,
} }
const noBet: NormalizedBet = { const noBet: Bet = {
id: noAnteId, id: noAnteId,
userId: creator.id, userId: creator.id,
contractId: contract.id, contractId: contract.id,
@ -99,7 +96,7 @@ export function getFreeAnswerAnte(
const { createdTime } = contract const { createdTime } = contract
const anteBet: NormalizedBet = { const anteBet: Bet = {
id: anteBetId, id: anteBetId,
userId: anteBettorId, userId: anteBettorId,
contractId: contract.id, contractId: contract.id,
@ -116,50 +113,6 @@ export function getFreeAnswerAnte(
return anteBet return anteBet
} }
export function getMultipleChoiceAntes(
creator: User,
contract: MultipleChoiceContract,
answers: string[],
betDocIds: string[]
) {
const { totalBets, totalShares } = contract
const amount = totalBets['0']
const shares = totalShares['0']
const p = 1 / answers.length
const { createdTime } = contract
const bets: NormalizedBet[] = answers.map((answer, i) => ({
id: betDocIds[i],
userId: creator.id,
contractId: contract.id,
amount,
shares,
outcome: i.toString(),
probBefore: p,
probAfter: p,
createdTime,
isAnte: true,
fees: noFees,
}))
const { username, name, avatarUrl } = creator
const answerObjects: Answer[] = answers.map((answer, i) => ({
id: i.toString(),
number: i,
contractId: contract.id,
createdTime,
userId: creator.id,
username,
name,
avatarUrl,
text: answer,
}))
return { bets, answerObjects }
}
export function getNumericAnte( export function getNumericAnte(
anteBettorId: string, anteBettorId: string,
contract: NumericContract, contract: NumericContract,
@ -179,7 +132,7 @@ export function getNumericAnte(
range(0, bucketCount).map((_, i) => [i, betAnte]) range(0, bucketCount).map((_, i) => [i, betAnte])
) )
const anteBet: NormalizedBet<NumericBet> = { const anteBet: NumericBet = {
id: newBetId, id: newBetId,
userId: anteBettorId, userId: anteBettorId,
contractId: contract.id, contractId: contract.id,

View File

@ -1,24 +0,0 @@
import { ENV_CONFIG } from './envs/constants'
export class APIError extends Error {
code: number
details?: unknown
constructor(code: number, message: string, details?: unknown) {
super(message)
this.code = code
this.name = 'APIError'
this.details = details
}
}
export function getFunctionUrl(name: string) {
if (process.env.NEXT_PUBLIC_FUNCTIONS_URL) {
return `${process.env.NEXT_PUBLIC_FUNCTIONS_URL}/${name}`
} else if (process.env.NEXT_PUBLIC_FIREBASE_EMULATE) {
const { projectId, region } = ENV_CONFIG.firebaseConfig
return `http://localhost:5001/${projectId}/${region}/${name}`
} else {
const { cloudRunId, cloudRunRegion } = ENV_CONFIG
return `https://${name}-${cloudRunId}-${cloudRunRegion}.a.run.app`
}
}

View File

@ -1,123 +0,0 @@
import { User } from './user'
export type Badge = {
type: BadgeTypes
createdTime: number
data: { [key: string]: any }
name: 'Proven Correct' | 'Streaker' | 'Market Creator'
}
export type BadgeTypes = 'PROVEN_CORRECT' | 'STREAKER' | 'MARKET_CREATOR'
export type ProvenCorrectBadgeData = {
type: 'PROVEN_CORRECT'
data: {
contractSlug: string
contractCreatorUsername: string
contractTitle: string
commentId: string
betAmount: number
}
}
export type MarketCreatorBadgeData = {
type: 'MARKET_CREATOR'
data: {
totalContractsCreated: number
}
}
export type StreakerBadgeData = {
type: 'STREAKER'
data: {
totalBettingStreak: number
}
}
export type ProvenCorrectBadge = Badge & ProvenCorrectBadgeData
export type StreakerBadge = Badge & StreakerBadgeData
export type MarketCreatorBadge = Badge & MarketCreatorBadgeData
export const MINIMUM_UNIQUE_BETTORS_FOR_PROVEN_CORRECT_BADGE = 5
export const provenCorrectRarityThresholds = [1, 1000, 10000]
const calculateProvenCorrectBadgeRarity = (badge: ProvenCorrectBadge) => {
const { betAmount } = badge.data
const thresholdArray = provenCorrectRarityThresholds
let i = thresholdArray.length - 1
while (i >= 0) {
if (betAmount >= thresholdArray[i]) {
return i + 1
}
i--
}
return 1
}
export const streakerBadgeRarityThresholds = [1, 50, 250]
const calculateStreakerBadgeRarity = (badge: StreakerBadge) => {
const { totalBettingStreak } = badge.data
const thresholdArray = streakerBadgeRarityThresholds
let i = thresholdArray.length - 1
while (i >= 0) {
if (totalBettingStreak == thresholdArray[i]) {
return i + 1
}
i--
}
return 1
}
export const marketCreatorBadgeRarityThresholds = [1, 75, 300]
const calculateMarketCreatorBadgeRarity = (badge: MarketCreatorBadge) => {
const { totalContractsCreated } = badge.data
const thresholdArray = marketCreatorBadgeRarityThresholds
let i = thresholdArray.length - 1
while (i >= 0) {
if (totalContractsCreated == thresholdArray[i]) {
return i + 1
}
i--
}
return 1
}
export type rarities = 'bronze' | 'silver' | 'gold'
const rarityRanks: { [key: number]: rarities } = {
1: 'bronze',
2: 'silver',
3: 'gold',
}
export const calculateBadgeRarity = (badge: Badge) => {
switch (badge.type) {
case 'PROVEN_CORRECT':
return rarityRanks[
calculateProvenCorrectBadgeRarity(badge as ProvenCorrectBadge)
]
case 'MARKET_CREATOR':
return rarityRanks[
calculateMarketCreatorBadgeRarity(badge as MarketCreatorBadge)
]
case 'STREAKER':
return rarityRanks[calculateStreakerBadgeRarity(badge as StreakerBadge)]
default:
return rarityRanks[0]
}
}
export const getBadgesByRarity = (user: User | null | undefined) => {
const rarities: { [key in rarities]: number } = {
bronze: 0,
silver: 0,
gold: 0,
}
if (!user) return rarities
Object.values(user.achievements).map((value) => {
value.badges.map((badge) => {
rarities[calculateBadgeRarity(badge)] =
(rarities[calculateBadgeRarity(badge)] ?? 0) + 1
})
})
return rarities
}

View File

@ -3,14 +3,7 @@ import { Fees } from './fees'
export type Bet = { export type Bet = {
id: string id: string
userId: string userId: string
// denormalized for bet lists
userAvatarUrl?: string
userUsername: string
userName: string
contractId: string contractId: string
createdTime: number
amount: number // bet size; negative if SELL bet amount: number // bet size; negative if SELL bet
loanAmount?: number loanAmount?: number
@ -20,22 +13,21 @@ export type Bet = {
probBefore: number probBefore: number
probAfter: number probAfter: number
sale?: {
amount: number // amount user makes from sale
betId: string // id of bet being sold
// TODO: add sale time?
}
fees: Fees fees: Fees
isSold?: boolean // true if this BUY bet has been sold
isAnte?: boolean isAnte?: boolean
isLiquidityProvision?: boolean isLiquidityProvision?: boolean
isRedemption?: boolean isRedemption?: boolean
challengeSlug?: string
// Props for bets in DPM contract below. createdTime: number
// A bet is either a BUY or a SELL that sells all of a previous buy.
isSold?: boolean // true if this BUY bet has been sold
// This field marks a SELL bet.
sale?: {
amount: number // amount user makes from sale
betId: string // id of BUY bet being sold
} }
} & Partial<LimitProps>
export type NumericBet = Bet & { export type NumericBet = Bet & {
value: number value: number
@ -43,27 +35,4 @@ export type NumericBet = Bet & {
allBetAmounts: { [outcome: string]: number } allBetAmounts: { [outcome: string]: number }
} }
// Binary market limit order. export const MAX_LOAN_PER_CONTRACT = 20
export type LimitBet = Bet & LimitProps
type LimitProps = {
orderAmount: number // Amount of limit order.
limitProb: number // [0, 1]. Bet to this probability.
isFilled: boolean // Whether all of the bet amount has been filled.
isCancelled: boolean // Whether to prevent any further fills.
// A record of each transaction that partially (or fully) fills the orderAmount.
// I.e. A limit order could be filled by partially matching with several bets.
// Non-limit orders can also be filled by matching with multiple limit orders.
fills: fill[]
}
export type fill = {
// The id the bet matched against, or null if the bet was matched by the pool.
matchedBetId: string | null
amount: number
shares: number
timestamp: number
// If the fill is a sale, it means the matching bet has shares of the same outcome.
// I.e. -fill.shares === matchedBet.shares
isSale?: boolean
}

View File

@ -1,15 +1,9 @@
import { groupBy, mapValues, sumBy } from 'lodash' import { sum, groupBy, mapValues, sumBy } from 'lodash'
import { LimitBet } from './bet'
import { CREATOR_FEE, Fees, LIQUIDITY_FEE, PLATFORM_FEE } from './fees' import { CPMMContract } from './contract'
import { CREATOR_FEE, Fees, LIQUIDITY_FEE, noFees, PLATFORM_FEE } from './fees'
import { LiquidityProvision } from './liquidity-provision' import { LiquidityProvision } from './liquidity-provision'
import { computeFills } from './new-bet' import { addObjects } from './util/object'
import { binarySearch } from './util/algos'
export type CpmmState = {
pool: { [outcome: string]: number }
p: number
}
export function getCpmmProbability( export function getCpmmProbability(
pool: { [outcome: string]: number }, pool: { [outcome: string]: number },
@ -20,11 +14,11 @@ export function getCpmmProbability(
} }
export function getCpmmProbabilityAfterBetBeforeFees( export function getCpmmProbabilityAfterBetBeforeFees(
state: CpmmState, contract: CPMMContract,
outcome: string, outcome: string,
bet: number bet: number
) { ) {
const { pool, p } = state const { pool, p } = contract
const shares = calculateCpmmShares(pool, p, bet, outcome) const shares = calculateCpmmShares(pool, p, bet, outcome)
const { YES: y, NO: n } = pool const { YES: y, NO: n } = pool
@ -37,12 +31,12 @@ export function getCpmmProbabilityAfterBetBeforeFees(
} }
export function getCpmmOutcomeProbabilityAfterBet( export function getCpmmOutcomeProbabilityAfterBet(
state: CpmmState, contract: CPMMContract,
outcome: string, outcome: string,
bet: number bet: number
) { ) {
const { newPool } = calculateCpmmPurchase(state, bet, outcome) const { newPool } = calculateCpmmPurchase(contract, bet, outcome)
const p = getCpmmProbability(newPool, state.p) const p = getCpmmProbability(newPool, contract.p)
return outcome === 'NO' ? 1 - p : p return outcome === 'NO' ? 1 - p : p
} }
@ -64,8 +58,12 @@ function calculateCpmmShares(
: n + bet - (k * (bet + y) ** -p) ** (1 / (1 - p)) : n + bet - (k * (bet + y) ** -p) ** (1 / (1 - p))
} }
export function getCpmmFees(state: CpmmState, bet: number, outcome: string) { export function getCpmmLiquidityFee(
const prob = getCpmmProbabilityAfterBetBeforeFees(state, outcome, bet) contract: CPMMContract,
bet: number,
outcome: string
) {
const prob = getCpmmProbabilityAfterBetBeforeFees(contract, outcome, bet)
const betP = outcome === 'YES' ? 1 - prob : prob const betP = outcome === 'YES' ? 1 - prob : prob
const liquidityFee = LIQUIDITY_FEE * betP * bet const liquidityFee = LIQUIDITY_FEE * betP * bet
@ -80,23 +78,25 @@ export function getCpmmFees(state: CpmmState, bet: number, outcome: string) {
} }
export function calculateCpmmSharesAfterFee( export function calculateCpmmSharesAfterFee(
state: CpmmState, contract: CPMMContract,
bet: number, bet: number,
outcome: string outcome: string
) { ) {
const { pool, p } = state const { pool, p } = contract
const { remainingBet } = getCpmmFees(state, bet, outcome) const { remainingBet } = getCpmmLiquidityFee(contract, bet, outcome)
return calculateCpmmShares(pool, p, remainingBet, outcome) return calculateCpmmShares(pool, p, remainingBet, outcome)
} }
export function calculateCpmmPurchase( export function calculateCpmmPurchase(
state: CpmmState, contract: CPMMContract,
bet: number, bet: number,
outcome: string outcome: string
) { ) {
const { pool, p } = state const { pool, p } = contract
const { remainingBet, fees } = getCpmmFees(state, bet, outcome) const { remainingBet, fees } = getCpmmLiquidityFee(contract, bet, outcome)
// const remainingBet = bet
// const fees = noFees
const shares = calculateCpmmShares(pool, p, remainingBet, outcome) const shares = calculateCpmmShares(pool, p, remainingBet, outcome)
const { YES: y, NO: n } = pool const { YES: y, NO: n } = pool
@ -115,125 +115,119 @@ export function calculateCpmmPurchase(
return { shares, newPool, newP, fees } return { shares, newPool, newP, fees }
} }
// Note: there might be a closed form solution for this. function computeK(y: number, n: number, p: number) {
// If so, feel free to switch out this implementation. return y ** p * n ** (1 - p)
export function calculateCpmmAmountToProb(
state: CpmmState,
prob: number,
outcome: 'YES' | 'NO'
) {
if (prob <= 0 || prob >= 1 || isNaN(prob)) return Infinity
if (outcome === 'NO') prob = 1 - prob
// First, find an upper bound that leads to a more extreme probability than prob.
let maxGuess = 10
let newProb = 0
do {
maxGuess *= 10
newProb = getCpmmOutcomeProbabilityAfterBet(state, outcome, maxGuess)
} while (newProb < prob)
// Then, binary search for the amount that gets closest to prob.
const amount = binarySearch(0, maxGuess, (amount) => {
const newProb = getCpmmOutcomeProbabilityAfterBet(state, outcome, amount)
return newProb - prob
})
return amount
} }
function calculateAmountToBuyShares( function sellSharesK(
state: CpmmState, y: number,
shares: number, n: number,
p: number,
s: number,
outcome: 'YES' | 'NO', outcome: 'YES' | 'NO',
unfilledBets: LimitBet[], b: number
balanceByUserId: { [userId: string]: number }
) { ) {
// Search for amount between bounds (0, shares). return outcome === 'YES'
// Min share price is M$0, and max is M$1 each. ? computeK(y - b + s, n - b, p)
return binarySearch(0, shares, (amount) => { : computeK(y - b, n - b + s, p)
const { takers } = computeFills( }
outcome,
amount,
state,
undefined,
unfilledBets,
balanceByUserId
)
const totalShares = sumBy(takers, (taker) => taker.shares) function calculateCpmmShareValue(
return totalShares - shares contract: CPMMContract,
}) shares: number,
outcome: 'YES' | 'NO'
) {
const { pool, p } = contract
// Find bet amount that preserves k after selling shares.
const k = computeK(pool.YES, pool.NO, p)
const otherPool = outcome === 'YES' ? pool.NO : pool.YES
// Constrain the max sale value to the lessor of 1. shares and 2. the other pool.
// This is because 1. the max value per share is M$ 1,
// and 2. The other pool cannot go negative and the sale value is subtracted from it.
// (Without this, there are multiple solutions for the same k.)
let highAmount = Math.min(shares, otherPool)
let lowAmount = 0
let mid = 0
let kGuess = 0
while (true) {
mid = lowAmount + (highAmount - lowAmount) / 2
// Break once we've reached max precision.
if (mid === lowAmount || mid === highAmount) break
kGuess = sellSharesK(pool.YES, pool.NO, p, shares, outcome, mid)
if (kGuess < k) {
highAmount = mid
} else {
lowAmount = mid
}
}
return mid
} }
export function calculateCpmmSale( export function calculateCpmmSale(
state: CpmmState, contract: CPMMContract,
shares: number, shares: number,
outcome: 'YES' | 'NO', outcome: string
unfilledBets: LimitBet[],
balanceByUserId: { [userId: string]: number }
) { ) {
if (Math.round(shares) < 0) { if (Math.round(shares) < 0) {
throw new Error('Cannot sell non-positive shares') throw new Error('Cannot sell non-positive shares')
} }
const oppositeOutcome = outcome === 'YES' ? 'NO' : 'YES' const saleValue = calculateCpmmShareValue(
const buyAmount = calculateAmountToBuyShares( contract,
state,
shares, shares,
oppositeOutcome, outcome as 'YES' | 'NO'
unfilledBets,
balanceByUserId
) )
const { cpmmState, makers, takers, totalFees, ordersToCancel } = computeFills( const fees = noFees
oppositeOutcome,
buyAmount,
state,
undefined,
unfilledBets,
balanceByUserId
)
// Transform buys of opposite outcome into sells. // const { fees, remainingBet: saleValue } = getCpmmLiquidityFee(
const saleTakers = takers.map((taker) => ({ // contract,
...taker, // rawSaleValue,
// You bought opposite shares, which combine with existing shares, removing them. // outcome === 'YES' ? 'NO' : 'YES'
shares: -taker.shares, // )
// Opposite shares combine with shares you are selling for M$ of shares.
// You paid taker.amount for the opposite shares.
// Take the negative because this is money you gain.
amount: -(taker.shares - taker.amount),
isSale: true,
}))
const saleValue = -sumBy(saleTakers, (taker) => taker.amount) const { pool } = contract
const { YES: y, NO: n } = pool
return { const { liquidityFee: fee } = fees
const [newY, newN] =
outcome === 'YES'
? [y + shares - saleValue + fee, n - saleValue + fee]
: [y - saleValue + fee, n + shares - saleValue + fee]
if (newY < 0 || newN < 0) {
console.log('calculateCpmmSale', {
newY,
newN,
y,
n,
shares,
saleValue, saleValue,
cpmmState, fee,
fees: totalFees, outcome,
makers, })
takers: saleTakers, throw new Error('Cannot sell more than in pool')
ordersToCancel,
} }
const postBetPool = { YES: newY, NO: newN }
const { newPool, newP } = addCpmmLiquidity(postBetPool, contract.p, fee)
return { saleValue, newPool, newP, fees }
} }
export function getCpmmProbabilityAfterSale( export function getCpmmProbabilityAfterSale(
state: CpmmState, contract: CPMMContract,
shares: number, shares: number,
outcome: 'YES' | 'NO', outcome: 'YES' | 'NO'
unfilledBets: LimitBet[],
balanceByUserId: { [userId: string]: number }
) { ) {
const { cpmmState } = calculateCpmmSale( const { newPool } = calculateCpmmSale(contract, shares, outcome)
state, return getCpmmProbability(newPool, contract.p)
shares,
outcome,
unfilledBets,
balanceByUserId
)
return getCpmmProbability(cpmmState.pool, cpmmState.p)
} }
export function getCpmmLiquidity( export function getCpmmLiquidity(
@ -266,23 +260,52 @@ export function addCpmmLiquidity(
return { newPool, liquidity, newP } return { newPool, liquidity, newP }
} }
export function getCpmmLiquidityPoolWeights(liquidities: LiquidityProvision[]) { export function getCpmmLiquidityPoolWeights(
const userAmounts = groupBy(liquidities, (w) => w.userId) contract: CPMMContract,
const totalAmount = sumBy(liquidities, (w) => w.amount)
return mapValues(
userAmounts,
(amounts) => sumBy(amounts, (w) => w.amount) / totalAmount
)
}
export function getUserLiquidityShares(
userId: string,
state: CpmmState,
liquidities: LiquidityProvision[] liquidities: LiquidityProvision[]
) { ) {
const weights = getCpmmLiquidityPoolWeights(liquidities) const { p } = contract
const userWeight = weights[userId] ?? 0
return mapValues(state.pool, (shares) => userWeight * shares) const liquidityShares = liquidities.map((l) => {
const oldLiquidity = getCpmmLiquidity(l.pool, p)
const newPool = addObjects(l.pool, { YES: l.amount, NO: l.amount })
const newLiquidity = getCpmmLiquidity(newPool, p)
const liquidity = newLiquidity - oldLiquidity
return liquidity
})
const shareSum = sum(liquidityShares)
const weights = liquidityShares.map((s, i) => ({
weight: s / shareSum,
providerId: liquidities[i].userId,
}))
const userWeights = groupBy(weights, (w) => w.providerId)
const totalUserWeights = mapValues(userWeights, (userWeight) =>
sumBy(userWeight, (w) => w.weight)
)
return totalUserWeights
} }
// export function removeCpmmLiquidity(
// contract: CPMMContract,
// liquidity: number
// ) {
// const { YES, NO } = contract.pool
// const poolLiquidity = getCpmmLiquidity({ YES, NO })
// const p = getCpmmProbability({ YES, NO }, contract.p)
// const f = liquidity / poolLiquidity
// const [payoutYes, payoutNo] = [f * YES, f * NO]
// const betAmount = Math.abs(payoutYes - payoutNo)
// const betOutcome = p >= 0.5 ? 'NO' : 'YES' // opposite side as adding liquidity
// const payout = Math.min(payoutYes, payoutNo)
// const newPool = { YES: YES - payoutYes, NO: NO - payoutNo }
// return { newPool, payout, betAmount, betOutcome }
// }

View File

@ -2,7 +2,7 @@ import { cloneDeep, range, sum, sumBy, sortBy, mapValues } from 'lodash'
import { Bet, NumericBet } from './bet' import { Bet, NumericBet } from './bet'
import { DPMContract, DPMBinaryContract, NumericContract } from './contract' import { DPMContract, DPMBinaryContract, NumericContract } from './contract'
import { DPM_FEES } from './fees' import { DPM_FEES } from './fees'
import { normpdf } from './util/math' import { normpdf } from '../common/util/math'
import { addObjects } from './util/object' import { addObjects } from './util/object'
export function getDpmProbability(totalShares: { [outcome: string]: number }) { export function getDpmProbability(totalShares: { [outcome: string]: number }) {
@ -346,6 +346,10 @@ function calculateMktDpmPayout(contract: DPMContract, bet: Bet) {
probs = mapValues(totalShares, (shares) => shares ** 2 / squareSum) probs = mapValues(totalShares, (shares) => shares ** 2 / squareSum)
} }
const weightedShareTotal = sumBy(Object.keys(totalShares), (outcome) => {
return probs[outcome] * totalShares[outcome]
})
const { outcome, amount, shares } = bet const { outcome, amount, shares } = bet
const poolFrac = const poolFrac =
@ -355,11 +359,11 @@ function calculateMktDpmPayout(contract: DPMContract, bet: Bet) {
(outcome) => { (outcome) => {
return ( return (
(probs[outcome] * (bet as NumericBet).allOutcomeShares[outcome]) / (probs[outcome] * (bet as NumericBet).allOutcomeShares[outcome]) /
totalShares[outcome] weightedShareTotal
) )
} }
) )
: (probs[outcome] * shares) / totalShares[outcome] : (probs[outcome] * shares) / weightedShareTotal
const totalPool = sum(Object.values(pool)) const totalPool = sum(Object.values(pool))
const winnings = poolFrac * totalPool const winnings = poolFrac * totalPool

View File

@ -1,315 +0,0 @@
import { Dictionary, groupBy, last, partition, sum, sumBy, uniq } from 'lodash'
import { calculatePayout, getContractBetMetrics } from './calculate'
import { Bet, LimitBet } from './bet'
import {
Contract,
CPMMBinaryContract,
CPMMContract,
DPMContract,
} from './contract'
import { PortfolioMetrics, User } from './user'
import { DAY_MS } from './util/time'
import { getBinaryCpmmBetInfo, getNewMultiBetInfo } from './new-bet'
import { getCpmmProbability } from './calculate-cpmm'
import { removeUndefinedProps } from './util/object'
const computeInvestmentValue = (
bets: Bet[],
contractsDict: { [k: string]: Contract }
) => {
return sumBy(bets, (bet) => {
const contract = contractsDict[bet.contractId]
if (!contract || contract.isResolved) return 0
if (bet.sale || bet.isSold) return 0
const payout = calculatePayout(contract, bet, 'MKT')
const value = payout - (bet.loanAmount ?? 0)
if (isNaN(value)) return 0
return value
})
}
export const computeInvestmentValueCustomProb = (
bets: Bet[],
contract: Contract,
p: number
) => {
return sumBy(bets, (bet) => {
if (!contract || contract.isResolved) return 0
if (bet.sale || bet.isSold) return 0
const { outcome, shares } = bet
const betP = outcome === 'YES' ? p : 1 - p
const value = betP * shares
if (isNaN(value)) return 0
return value
})
}
export const computeElasticity = (
bets: Bet[],
contract: Contract,
betAmount = 50
) => {
const { mechanism, outcomeType } = contract
return mechanism === 'cpmm-1' &&
(outcomeType === 'BINARY' || outcomeType === 'PSEUDO_NUMERIC')
? computeBinaryCpmmElasticity(bets, contract, betAmount)
: computeDpmElasticity(contract, betAmount)
}
export const computeBinaryCpmmElasticity = (
bets: Bet[],
contract: CPMMContract,
betAmount: number
) => {
const limitBets = bets
.filter(
(b) =>
!b.isFilled &&
!b.isSold &&
!b.isRedemption &&
!b.sale &&
!b.isCancelled &&
b.limitProb !== undefined
)
.sort((a, b) => a.createdTime - b.createdTime) as LimitBet[]
const userIds = uniq(limitBets.map((b) => b.userId))
// Assume all limit orders are good.
const userBalances = Object.fromEntries(
userIds.map((id) => [id, Number.MAX_SAFE_INTEGER])
)
const { newPool: poolY, newP: pY } = getBinaryCpmmBetInfo(
'YES',
betAmount,
contract,
undefined,
limitBets,
userBalances
)
const resultYes = getCpmmProbability(poolY, pY)
const { newPool: poolN, newP: pN } = getBinaryCpmmBetInfo(
'NO',
betAmount,
contract,
undefined,
limitBets,
userBalances
)
const resultNo = getCpmmProbability(poolN, pN)
// handle AMM overflow
const safeYes = Number.isFinite(resultYes) ? resultYes : 1
const safeNo = Number.isFinite(resultNo) ? resultNo : 0
return safeYes - safeNo
}
export const computeDpmElasticity = (
contract: DPMContract,
betAmount: number
) => {
return getNewMultiBetInfo('', 2 * betAmount, contract).newBet.probAfter
}
const computeTotalPool = (userContracts: Contract[], startTime = 0) => {
const periodFilteredContracts = userContracts.filter(
(contract) => contract.createdTime >= startTime
)
return sum(
periodFilteredContracts.map((contract) => sum(Object.values(contract.pool)))
)
}
export const computeVolume = (contractBets: Bet[], since: number) => {
return sumBy(contractBets, (b) =>
b.createdTime > since && !b.isRedemption ? Math.abs(b.amount) : 0
)
}
const calculateProbChangeSince = (descendingBets: Bet[], since: number) => {
const newestBet = descendingBets[0]
if (!newestBet) return 0
const betBeforeSince = descendingBets.find((b) => b.createdTime < since)
if (!betBeforeSince) {
const oldestBet = last(descendingBets) ?? newestBet
return newestBet.probAfter - oldestBet.probBefore
}
return newestBet.probAfter - betBeforeSince.probAfter
}
export const calculateProbChanges = (descendingBets: Bet[]) => {
const now = Date.now()
const yesterday = now - DAY_MS
const weekAgo = now - 7 * DAY_MS
const monthAgo = now - 30 * DAY_MS
return {
day: calculateProbChangeSince(descendingBets, yesterday),
week: calculateProbChangeSince(descendingBets, weekAgo),
month: calculateProbChangeSince(descendingBets, monthAgo),
}
}
export const calculateCreatorVolume = (userContracts: Contract[]) => {
const allTimeCreatorVolume = computeTotalPool(userContracts, 0)
const monthlyCreatorVolume = computeTotalPool(
userContracts,
Date.now() - 30 * DAY_MS
)
const weeklyCreatorVolume = computeTotalPool(
userContracts,
Date.now() - 7 * DAY_MS
)
const dailyCreatorVolume = computeTotalPool(
userContracts,
Date.now() - 1 * DAY_MS
)
return {
daily: dailyCreatorVolume,
weekly: weeklyCreatorVolume,
monthly: monthlyCreatorVolume,
allTime: allTimeCreatorVolume,
}
}
export const calculateNewPortfolioMetrics = (
user: User,
contractsById: { [k: string]: Contract },
currentBets: Bet[]
) => {
const investmentValue = computeInvestmentValue(currentBets, contractsById)
const newPortfolio = {
investmentValue: investmentValue,
balance: user.balance,
totalDeposits: user.totalDeposits,
timestamp: Date.now(),
userId: user.id,
}
return newPortfolio
}
const calculateProfitForPeriod = (
startingPortfolio: PortfolioMetrics | undefined,
currentProfit: number
) => {
if (startingPortfolio === undefined) {
return currentProfit
}
const startingProfit = calculatePortfolioProfit(startingPortfolio)
return currentProfit - startingProfit
}
export const calculatePortfolioProfit = (portfolio: PortfolioMetrics) => {
return portfolio.investmentValue + portfolio.balance - portfolio.totalDeposits
}
export const calculateNewProfit = (
portfolioHistory: Record<
'current' | 'day' | 'week' | 'month',
PortfolioMetrics | undefined
>,
newPortfolio: PortfolioMetrics
) => {
const allTimeProfit = calculatePortfolioProfit(newPortfolio)
const newProfit = {
daily: calculateProfitForPeriod(portfolioHistory.day, allTimeProfit),
weekly: calculateProfitForPeriod(portfolioHistory.week, allTimeProfit),
monthly: calculateProfitForPeriod(portfolioHistory.month, allTimeProfit),
allTime: allTimeProfit,
}
return newProfit
}
export const calculateMetricsByContract = (
bets: Bet[],
contractsById: Dictionary<Contract>
) => {
const betsByContract = groupBy(bets, (bet) => bet.contractId)
const unresolvedContracts = Object.keys(betsByContract)
.map((cid) => contractsById[cid])
.filter((c) => c && !c.isResolved)
return unresolvedContracts.map((c) => {
const bets = betsByContract[c.id] ?? []
const current = getContractBetMetrics(c, bets)
let periodMetrics
if (c.mechanism === 'cpmm-1' && c.outcomeType === 'BINARY') {
const periods = ['day', 'week', 'month'] as const
periodMetrics = Object.fromEntries(
periods.map((period) => [
period,
calculatePeriodProfit(c, bets, period),
])
)
}
return removeUndefinedProps({
contractId: c.id,
...current,
from: periodMetrics,
})
})
}
export type ContractMetrics = ReturnType<
typeof calculateMetricsByContract
>[number]
const calculatePeriodProfit = (
contract: CPMMBinaryContract,
bets: Bet[],
period: 'day' | 'week' | 'month'
) => {
const days = period === 'day' ? 1 : period === 'week' ? 7 : 30
const fromTime = Date.now() - days * DAY_MS
const [previousBets, recentBets] = partition(
bets,
(b) => b.createdTime < fromTime
)
const prevProb = contract.prob - contract.probChanges[period]
const prob = contract.resolutionProbability
? contract.resolutionProbability
: contract.prob
const previousBetsValue = computeInvestmentValueCustomProb(
previousBets,
contract,
prevProb
)
const currentBetsValue = computeInvestmentValueCustomProb(
previousBets,
contract,
prob
)
const { profit: recentProfit, invested: recentInvested } =
getContractBetMetrics(contract, recentBets)
const profit = currentBetsValue - previousBetsValue + recentProfit
const invested = previousBetsValue + recentInvested
const profitPercent = invested === 0 ? 0 : 100 * (profit / invested)
return {
profit,
profitPercent,
invested,
prevValue: previousBetsValue,
value: currentBetsValue,
}
}

View File

@ -1,5 +1,5 @@
import { maxBy, partition, sortBy, sum, sumBy } from 'lodash' import { maxBy } from 'lodash'
import { Bet, LimitBet } from './bet' import { Bet } from './bet'
import { import {
calculateCpmmSale, calculateCpmmSale,
getCpmmProbability, getCpmmProbability,
@ -18,26 +18,15 @@ import {
getDpmProbabilityAfterSale, getDpmProbabilityAfterSale,
} from './calculate-dpm' } from './calculate-dpm'
import { calculateFixedPayout } from './calculate-fixed-payouts' import { calculateFixedPayout } from './calculate-fixed-payouts'
import { import { Contract, BinaryContract, FreeResponseContract } from './contract'
Contract,
BinaryContract,
FreeResponseContract,
PseudoNumericContract,
MultipleChoiceContract,
} from './contract'
import { floatingEqual } from './util/math'
export function getProbability( export function getProbability(contract: BinaryContract) {
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( export function getInitialProbability(contract: BinaryContract) {
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)
@ -75,22 +64,9 @@ export function calculateShares(
: calculateDpmShares(contract.totalShares, bet, betChoice) : calculateDpmShares(contract.totalShares, bet, betChoice)
} }
export function calculateSaleAmount( export function calculateSaleAmount(contract: Contract, bet: Bet) {
contract: Contract, return contract.mechanism === 'cpmm-1' && contract.outcomeType === 'BINARY'
bet: Bet, ? calculateCpmmSale(contract, Math.abs(bet.shares), bet.outcome).saleValue
unfilledBets: LimitBet[],
balanceByUserId: { [userId: string]: number }
) {
return contract.mechanism === 'cpmm-1' &&
(contract.outcomeType === 'BINARY' ||
contract.outcomeType === 'PSEUDO_NUMERIC')
? calculateCpmmSale(
contract,
Math.abs(bet.shares),
bet.outcome as 'YES' | 'NO',
unfilledBets,
balanceByUserId
).saleValue
: calculateDpmSaleAmount(contract, bet) : calculateDpmSaleAmount(contract, bet)
} }
@ -103,25 +79,15 @@ export function calculatePayoutAfterCorrectBet(contract: Contract, bet: Bet) {
export function getProbabilityAfterSale( export function getProbabilityAfterSale(
contract: Contract, contract: Contract,
outcome: string, outcome: string,
shares: number, shares: number
unfilledBets: LimitBet[],
balanceByUserId: { [userId: string]: number }
) { ) {
return contract.mechanism === 'cpmm-1' return contract.mechanism === 'cpmm-1'
? getCpmmProbabilityAfterSale( ? getCpmmProbabilityAfterSale(contract, shares, outcome as 'YES' | 'NO')
contract,
shares,
outcome as 'YES' | 'NO',
unfilledBets,
balanceByUserId
)
: getDpmProbabilityAfterSale(contract.totalShares, outcome, shares) : getDpmProbabilityAfterSale(contract.totalShares, outcome, shares)
} }
export function calculatePayout(contract: Contract, bet: Bet, outcome: string) { export function calculatePayout(contract: Contract, bet: Bet, outcome: string) {
return contract.mechanism === 'cpmm-1' && return contract.mechanism === 'cpmm-1' && contract.outcomeType === 'BINARY'
(contract.outcomeType === 'BINARY' ||
contract.outcomeType === 'PSEUDO_NUMERIC')
? calculateFixedPayout(contract, bet, outcome) ? calculateFixedPayout(contract, bet, outcome)
: calculateDpmPayout(contract, bet, outcome) : calculateDpmPayout(contract, bet, outcome)
} }
@ -130,60 +96,15 @@ 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' && return contract.mechanism === 'cpmm-1' && contract.outcomeType === 'BINARY'
(contract.outcomeType === 'BINARY' ||
contract.outcomeType === 'PSEUDO_NUMERIC')
? calculateFixedPayout(contract, bet, outcome) ? calculateFixedPayout(contract, bet, outcome)
: calculateDpmPayout(contract, bet, outcome) : calculateDpmPayout(contract, bet, outcome)
} }
function getCpmmInvested(yourBets: Bet[]) {
const totalShares: { [outcome: string]: number } = {}
const totalSpent: { [outcome: string]: number } = {}
const sortedBets = sortBy(yourBets, 'createdTime')
for (const bet of sortedBets) {
const { outcome, shares, amount } = bet
if (floatingEqual(shares, 0)) continue
const spent = totalSpent[outcome] ?? 0
const position = totalShares[outcome] ?? 0
if (amount > 0) {
totalShares[outcome] = position + shares
totalSpent[outcome] = spent + amount
} else if (amount < 0) {
const averagePrice = position === 0 ? 0 : spent / position
totalShares[outcome] = position + shares
totalSpent[outcome] = spent + averagePrice * shares
}
}
return sum([0, ...Object.values(totalSpent)])
}
function getDpmInvested(yourBets: Bet[]) {
const sortedBets = sortBy(yourBets, 'createdTime')
return sumBy(sortedBets, (bet) => {
const { amount, sale } = bet
if (sale) {
const originalBet = sortedBets.find((b) => b.id === sale.betId)
if (originalBet) return -originalBet.amount
return 0
}
return amount
})
}
export type ContractBetMetrics = ReturnType<typeof getContractBetMetrics>
export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) { export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) {
const { resolution } = contract const { resolution } = contract
const isCpmm = contract.mechanism === 'cpmm-1'
let currentInvested = 0
let totalInvested = 0 let totalInvested = 0
let payout = 0 let payout = 0
let loan = 0 let loan = 0
@ -209,6 +130,7 @@ export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) {
saleValue -= amount saleValue -= amount
} }
currentInvested += amount
loan += loanAmount ?? 0 loan += loanAmount ?? 0
payout += resolution payout += resolution
? calculatePayout(contract, bet, resolution) ? calculatePayout(contract, bet, resolution)
@ -216,40 +138,32 @@ export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) {
} }
} }
const netPayout = payout - loan
const profit = payout + saleValue + redeemed - totalInvested const profit = payout + saleValue + redeemed - totalInvested
const profitPercent = totalInvested === 0 ? 0 : (profit / totalInvested) * 100 const profitPercent = (profit / totalInvested) * 100
const invested = isCpmm ? getCpmmInvested(yourBets) : getDpmInvested(yourBets)
const hasShares = Object.values(totalShares).some(
(shares) => !floatingEqual(shares, 0)
)
return { return {
invested, invested: Math.max(0, currentInvested),
loan,
payout, payout,
netPayout,
profit, profit,
profitPercent, profitPercent,
totalShares, totalShares,
hasShares,
} }
} }
export function getContractBetNullMetrics() { export function getContractBetNullMetrics() {
return { return {
invested: 0, invested: 0,
loan: 0,
payout: 0, payout: 0,
netPayout: 0,
profit: 0, profit: 0,
profitPercent: 0, profitPercent: 0,
totalShares: {} as { [outcome: string]: number }, totalShares: {} as { [outcome: string]: number },
hasShares: false,
} }
} }
export function getTopAnswer( export function getTopAnswer(contract: FreeResponseContract) {
contract: FreeResponseContract | MultipleChoiceContract
) {
const { answers } = contract const { answers } = contract
const top = maxBy( const top = maxBy(
answers?.map((answer) => ({ answers?.map((answer) => ({
@ -260,43 +174,3 @@ export function getTopAnswer(
) )
return top?.answer return top?.answer
} }
export function getLargestPosition(contract: Contract, userBets: Bet[]) {
let yesFloorShares = 0,
yesShares = 0,
noShares = 0,
noFloorShares = 0
if (userBets.length === 0) {
return null
}
if (contract.outcomeType === 'FREE_RESPONSE') {
const answerCounts: { [outcome: string]: number } = {}
for (const bet of userBets) {
if (bet.outcome) {
if (!answerCounts[bet.outcome]) {
answerCounts[bet.outcome] = bet.amount
} else {
answerCounts[bet.outcome] += bet.amount
}
}
}
const majorityAnswer =
maxBy(Object.keys(answerCounts), (outcome) => answerCounts[outcome]) ?? ''
return {
prob: undefined,
shares: answerCounts[majorityAnswer] || 0,
outcome: majorityAnswer,
}
}
const [yesBets, noBets] = partition(userBets, (bet) => bet.outcome === 'YES')
yesShares = sumBy(yesBets, (bet) => bet.shares)
noShares = sumBy(noBets, (bet) => bet.shares)
yesFloorShares = Math.floor(yesShares)
noFloorShares = Math.floor(noShares)
const shares = yesFloorShares || noFloorShares
const outcome = yesFloorShares > noFloorShares ? 'YES' : 'NO'
return { shares, outcome }
}

View File

@ -1,7 +1,3 @@
import { difference } from 'lodash'
export const CATEGORIES_GROUP_SLUG_POSTFIX = '-default'
export const CATEGORIES = { export const CATEGORIES = {
politics: 'Politics', politics: 'Politics',
technology: 'Technology', technology: 'Technology',
@ -16,28 +12,10 @@ export const CATEGORIES = {
crypto: 'Crypto', crypto: 'Crypto',
gaming: 'Gaming', gaming: 'Gaming',
fun: 'Fun', fun: 'Fun',
} } as { [category: string]: string }
export type category = keyof typeof CATEGORIES
export const TO_CATEGORY = Object.fromEntries( export const TO_CATEGORY = Object.fromEntries(
Object.entries(CATEGORIES).map(([k, v]) => [v, k]) Object.entries(CATEGORIES).map(([k, v]) => [v, k])
) )
export const CATEGORY_LIST = Object.keys(CATEGORIES) export const CATEGORY_LIST = Object.keys(CATEGORIES)
export const EXCLUDED_CATEGORIES: category[] = [
'fun',
'manifold',
'personal',
'covid',
'gaming',
'crypto',
]
export const DEFAULT_CATEGORIES = difference(CATEGORY_LIST, EXCLUDED_CATEGORIES)
export const DEFAULT_CATEGORY_GROUPS = DEFAULT_CATEGORIES.map((c) => ({
slug: c.toLowerCase() + CATEGORIES_GROUP_SLUG_POSTFIX,
name: CATEGORIES[c as category],
}))

View File

@ -1,65 +0,0 @@
import { IS_PRIVATE_MANIFOLD } from './envs/constants'
export type Challenge = {
// The link to send: https://manifold.markets/challenges/username/market-slug/{slug}
// Also functions as the unique id for the link.
slug: string
// The user that created the challenge.
creatorId: string
creatorUsername: string
creatorName: string
creatorAvatarUrl?: string
// Displayed to people claiming the challenge
message: string
// How much to put up
creatorAmount: number
// YES or NO for now
creatorOutcome: string
// Different than the creator
acceptorOutcome: string
acceptorAmount: number
// The probability the challenger thinks
creatorOutcomeProb: number
contractId: string
contractSlug: string
contractQuestion: string
contractCreatorUsername: string
createdTime: number
// If null, the link is valid forever
expiresTime: number | null
// How many times the challenge can be used
maxUses: number
// Used for simpler caching
acceptedByUserIds: string[]
// Successful redemptions of the link
acceptances: Acceptance[]
// TODO: will have to fill this on resolve contract
isResolved: boolean
resolutionOutcome?: string
}
export type Acceptance = {
// User that accepted the challenge
userId: string
userUsername: string
userName: string
userAvatarUrl: string
// The ID of the successful bet that tracks the money moved
betId: string
createdTime: number
}
export const CHALLENGES_ENABLED = !IS_PRIVATE_MANIFOLD

View File

@ -58,19 +58,6 @@ export const charities: Charity[] = [
- Promoting long-term thinking`, - Promoting long-term thinking`,
tags: ['Featured'] as CharityTag[], tags: ['Featured'] as CharityTag[],
}, },
{
name: 'New Science',
website: 'https://newscience.org/',
photo: 'https://i.imgur.com/C7PoR4q.png',
preview:
'Facilitating scientific breakthroughs by empowering the next generation of scientists and building the 21st century institutions of basic science.',
description: `As its first major project, in the summer of 2022, New Science will run an in-person research fellowship in Boston for young life scientists, during which they will independently explore an ambitious high-risk scientific idea they couldnt work on otherwise and start building the foundations for a bigger research project, while having much more freedom than they could expect in their normal research environment but also much more support from us. This is inspired by Cold Spring Harbor Laboratory, which started as a place where leading molecular biologists came for the summer to hang out and work on random projects together, and which eventually housed 8 Nobel Prize winners.
As its second major project, in the fall of 2022, New Science will run an in-person 12-month-long fellowship for young scientists starting to directly attack the biggest structural issues of the established institutions of science. We will double down on things that worked well during the summer fellowship, while extending the fellowship to one year, thus allowing researchers to make much more progress and will strive to provide them as much scientific leverage as possible.
In several years, New Science will start funding entire labs outside of academia and then will be creating an entire network of scientific organizations, while supporting the broader scientific ecosystem that will constitute the 21st century institutions of basic science.`,
tags: ['Featured'] as CharityTag[],
},
{ {
name: 'Global Health and Development Fund', name: 'Global Health and Development Fund',
website: 'https://funds.effectivealtruism.org/funds/global-development', website: 'https://funds.effectivealtruism.org/funds/global-development',
@ -169,7 +156,7 @@ export const charities: Charity[] = [
{ {
name: "Founder's Pledge Climate Change Fund", name: "Founder's Pledge Climate Change Fund",
website: 'https://founderspledge.com/funds/climate-change-fund', website: 'https://founderspledge.com/funds/climate-change-fund',
photo: 'https://i.imgur.com/9turaJW.png', photo: 'https://i.imgur.com/ZAhzHu4.png',
preview: preview:
'The Climate Change Fund aims to sustainably reach net-zero emissions globally, while still allowing growth to free millions from energy poverty.', 'The Climate Change Fund aims to sustainably reach net-zero emissions globally, while still allowing growth to free millions from energy poverty.',
description: `The Climate Change Fund aims to sustainably reach net-zero emissions globally. description: `The Climate Change Fund aims to sustainably reach net-zero emissions globally.
@ -183,7 +170,7 @@ export const charities: Charity[] = [
{ {
name: "Founder's Pledge Patient Philanthropy Fund", name: "Founder's Pledge Patient Philanthropy Fund",
website: 'https://founderspledge.com/funds/patient-philanthropy-fund', website: 'https://founderspledge.com/funds/patient-philanthropy-fund',
photo: 'https://i.imgur.com/LLR6CI6.png', photo: 'https://i.imgur.com/ZAhzHu4.png',
preview: preview:
'The Patient Philanthropy Project aims to safeguard and benefit the long-term future of humanity', 'The Patient Philanthropy Project aims to safeguard and benefit the long-term future of humanity',
description: `The Patient Philanthropy Project focuses on how we can collectively grow our resources to support the long-term flourishing of humanity. It addresses a crucial gap: as a society, we spend much too little on safeguarding and benefiting future generations. In fact, we spend more money on ice cream each year than we do on preventing our own extinction. However, people in the future - who do not have a voice in their future survival or environment - matter. Lots of them may yet come into existence and we have the ability to positively affect their lives now, if only by making sure we avoid major catastrophes that could destroy our common future. description: `The Patient Philanthropy Project focuses on how we can collectively grow our resources to support the long-term flourishing of humanity. It addresses a crucial gap: as a society, we spend much too little on safeguarding and benefiting future generations. In fact, we spend more money on ice cream each year than we do on preventing our own extinction. However, people in the future - who do not have a voice in their future survival or environment - matter. Lots of them may yet come into existence and we have the ability to positively affect their lives now, if only by making sure we avoid major catastrophes that could destroy our common future.
@ -300,29 +287,10 @@ Future plans: We expect to focus on similar theoretical problems in alignment un
name: 'Wild Animal Initiative', name: 'Wild Animal Initiative',
website: 'https://www.wildanimalinitiative.org/', website: 'https://www.wildanimalinitiative.org/',
ein: '82-2281466', ein: '82-2281466',
tags: ['Featured'] as CharityTag[],
photo: 'https://i.imgur.com/bOVUnDm.png', photo: 'https://i.imgur.com/bOVUnDm.png',
preview: preview: 'We want to make life better for wild animals.',
'Our mission is to understand and improve the lives of wild animals.', description:
description: `Although the natural world is a source of great beauty and happiness, vast numbers of animals routinely face serious challenges such as disease, hunger, or natural disasters. There is no “one-size-fits-all” solution to these threats. However, even as we recognize that improving the welfare of free-ranging wild animals is difficult, we believe that humans have a responsibility to help whenever we can. 'Wild Animal Initiative (WAI) currently operates in the U.S., where they work to strengthen the animal advocacy movement through creating an academic field dedicated to wild animal welfare. They compile literature reviews, write theoretical and opinion articles, and publish research results on their website and/or in peer-reviewed journals. WAI focuses on identifying and sharing possible research avenues and connecting with more established fields. They also work with researchers from various academic and non-academic institutions to identify potential collaborators, and they recently launched a grant assistance program.',
Our staff explores how humans can beneficially coexist with animals through the lens of wild animal welfare.
We respect wild animals as individuals with their own needs and preferences, rather than seeing them as mere parts of ecosystems. But this approach demands a richer understanding of wild animals lives.
We want to take a proactive approach to managing the welfare benefits, threats, and uncertainties that are inherent to complex natural and urban environments. Yet, to take action safely, we must conduct research to understand the impacts of our actions. The transdisciplinary perspective of wild animal welfare draws upon ethics, ecology, and animal welfare science to gather the knowledge we need, facilitating evidence-based improvements to wild animals quality of life.
Without sufficient public interest or research activity, solutions to the problems wild animals face will go undiscovered.
Wild Animal Initiative currently focuses on helping scientists, grantors, and decision-makers investigate important and understudied questions about wild animal welfare. Our work catalyzes research and applied projects that will open the door to a clearer picture of wild animals needs and how to enhance their well-being. Ultimately, we envision a world in which people actively choose to help wild animals and have the knowledge they need to do so responsibly.`,
},
{
name: 'FYXX Foundation',
website: 'https://www.fyxxfoundation.org/',
photo: 'https://i.imgur.com/ROmWO7m.png',
preview:
'FYXX Foundation: wildlife population management, without killing.',
description: `The future of our planet depends on the innovations of today, and the health of our wildlife are the first indication of our successful stewardship, which we believe can be improved by safe population management utilizing fertility control instead of poison and culling.`,
}, },
{ {
name: 'New Incentives', name: 'New Incentives',
@ -504,9 +472,9 @@ Wild Animal Initiative currently focuses on helping scientists, grantors, and de
name: 'The Trevor Project', name: 'The Trevor Project',
website: 'https://www.thetrevorproject.org/', website: 'https://www.thetrevorproject.org/',
photo: 'https://i.imgur.com/QN4mVNn.jpeg', photo: 'https://i.imgur.com/QN4mVNn.jpeg',
preview: preview: 'The Trevor Project is the worlds largest suicide prevention and crisis intervention organization for LGBTQ (lesbian, gay, bisexual, transgender, queer, and questioning) young people.',
'The Trevor Project is the worlds largest suicide prevention and crisis intervention organization for LGBTQ (lesbian, gay, bisexual, transgender, queer, and questioning) young people.', description:
description: `Two decades ago, we responded to a health crisis. Now were building a safer, more-inclusive world. LGBTQ young people are four times more likely to attempt suicide, and suicide remains the second leading cause of death among all young people in the U.S. `Two decades ago, we responded to a health crisis. Now were building a safer, more-inclusive world. LGBTQ young people are four times more likely to attempt suicide, and suicide remains the second leading cause of death among all young people in the U.S.
Our Mission Our Mission
To end suicide among lesbian, gay, bisexual, transgender, queer & questioning young people. To end suicide among lesbian, gay, bisexual, transgender, queer & questioning young people.
@ -517,87 +485,6 @@ Wild Animal Initiative currently focuses on helping scientists, grantors, and de
Our Goal Our Goal
To serve 1.8 million crisis contacts annually, by the end of our 25th year, while continuing to innovate on our core services.`, To serve 1.8 million crisis contacts annually, by the end of our 25th year, while continuing to innovate on our core services.`,
}, },
{
name: 'ACLU',
website: 'https://www.aclu.org/',
photo: 'https://i.imgur.com/nbSYuDC.png',
preview:
'The ACLU works in the courts, legislatures, and communities to defend and preserve the individual rights and liberties guaranteed to all people in this country by the Constitution and laws of the United States.',
description: `
THREE THINGS TO KNOW ABOUT THE ACLU
We protect American values. In many ways, the ACLU is the nation's most conservative organization. Our job is to conserve America's original civic values - the Constitution and the Bill of Rights - and defend the rights of every man, woman and child in this country.
We're not anti-anything. The only things we fight are attempts to take away or limit your civil liberties, like your right to practice any religion you want (or none at all); or to decide in private whether or not to have a child; or to speak out - for or against - anything at all; or to be treated with equality and fairness, no matter who you are.
We're there for you. Rich or poor, straight or gay, black or white or brown, urban or rural, pious or atheist, American-born or foreign-born, able-bodied or living with a disability. Every person in this country should have the same basic rights. And since our founding in 1920, we've been working hard to make sure no one takes them away.
The American Civil Liberties Union is our nation's guardian of liberty, working daily in courts, legislatures and communities to defend and preserve the individual rights and liberties that the Constitution and laws of the United States guarantee everyone in this country.
"So long as we have enough people in this country willing to fight for their rights, we'll be called a democracy," ACLU Founder Roger Baldwin said.
The U.S. Constitution and the Bill of Rights trumpet our aspirations for the kind of society that we want to be. But for much of our history, our nation failed to fulfill the promise of liberty for whole groups of people.`,
},
{
name: 'The Center for Election Science',
website: 'https://electionscience.org/',
photo: 'https://i.imgur.com/WvdHHZa.png',
preview:
'The Center for Election Science is a nonpartisan nonprofit dedicated to empowering voters with voting methods that strengthen democracy. We believe you deserve a vote that empowers you to impact the world you live in.',
description: `Founded in 2011, The Center for Election Science is a national, nonpartisan nonprofit focused on voting reform.
Our Mission To empower people with voting methods that strengthen democracy.
Our Vision A world where democracies thrive because voters voices are heard.
With an emphasis on approval voting, we bring better elections to people across the country through both advocacy and research.
The movement for a better way to vote is rapidly gaining momentum as voters grow tired of election results that dont represent the will of the people. In 2018, we worked with locals in Fargo, ND to help them become the first city in the U.S. to adopt approval voting. And in 2020, we helped grassroots activists empower the 300k people of St. Louis, MO with stronger democracy through approval voting.`,
},
{
name: 'Founders Pledge Global Health and Development Fund',
website: 'https://founderspledge.com/funds/global-health-and-development',
photo: 'https://i.imgur.com/EXbxH7T.png',
preview:
'Tackling the vast global inequalities in health, wealth and opportunity',
description: `Nearly half the world lives on less than $2.50 a day, yet giving by the worlds richest often overlooks the worlds poorest and most vulnerable. Despite the average American household being richer than 90% of the rest of the world, only 6% of US charitable giving goes to charities which work internationally.
This Fund is focused on helping those who need it most, wherever that help can make the biggest difference. By building a mixed portfolio of direct and indirect interventions, such as policy work, we aim to:
Improve the lives of the world's most vulnerable people.
Reduce the number of easily preventable deaths worldwide.
Work towards sustainable, systemic change.`,
},
{
name: 'YIMBY Law',
website: 'https://www.yimbylaw.org/',
photo: 'https://i.imgur.com/zlzp21Z.png',
preview:
'YIMBY Law works to make housing in California more accessible and affordable, by enforcing state housing laws.',
description: `
YIMBY Law works to make housing in California more accessible and affordable. Our method is to enforce state housing laws, and some examples are outlined below. We send letters to cities considering zoning or general plan compliant housing developments informing them of their duties under state law, and sue them when they do not comply.
If you would like to support our work, you can do so by getting involved or by donating.`,
},
{
name: 'CaRLA',
website: 'https://carlaef.org/',
photo: 'https://i.imgur.com/IsNVTOY.png',
preview:
'The California Renters Legal Advocacy and Education Funds core mission is to make lasting impacts to improve the affordability and accessibility of housing to current and future Californians, especially low- and moderate-income people and communities of color.',
description: `
The California Renters Legal Advocacy and Education Funds core mission is to make lasting impacts to improve the affordability and accessibility of housing to current and future Californians, especially low- and moderate-income people and communities of color.
CaRLA uses legal advocacy and education to ensure all cities comply with their own zoning and state housing laws and do their part to help solve the states housing shortage.
In addition to housing impact litigation, we provide free legal aid, education and workshops, counseling and advocacy to advocates, homeowners, small developers, and city and state government officials.`,
},
{
name: 'Mriya',
website: 'https://mriya-ua.org/',
photo:
'https://firebasestorage.googleapis.com/v0/b/mantic-markets.appspot.com/o/user-images%2Fdefault%2Fci2h3hStFM.47?alt=media&token=0d2cdc3d-e4d8-4f5e-8f23-4a586b6ff637',
preview: 'Donate supplies to soldiers in Ukraine',
description:
'Donate supplies to soldiers in Ukraine, including tourniquets and plate carriers.',
},
].map((charity) => { ].map((charity) => {
const slug = charity.name.toLowerCase().replace(/\s/g, '-') const slug = charity.name.toLowerCase().replace(/\s/g, '-')
return { return {

View File

@ -1,56 +1,18 @@
import type { JSONContent } from '@tiptap/core'
export type AnyCommentType = OnContract | OnGroup | OnPost
// Currently, comments are created after the bet, not atomically with the bet. // Currently, comments are created after the bet, not atomically with the bet.
// They're uniquely identified by the pair contractId/betId. // They're uniquely identified by the pair contractId/betId.
export type Comment<T extends AnyCommentType = AnyCommentType> = { export type Comment = {
id: string id: string
contractId: string
betId?: string
answerOutcome?: string
replyToCommentId?: string replyToCommentId?: string
userId: string userId: string
/** @deprecated - content now stored as JSON in content*/ text: string
text?: string
content: JSONContent
createdTime: number createdTime: number
// Denormalized, for rendering comments // Denormalized, for rendering comments
userName: string userName: string
userUsername: string userUsername: string
userAvatarUrl?: string userAvatarUrl?: string
bountiesAwarded?: number
} & T
export type OnContract = {
commentType: 'contract'
contractId: string
answerOutcome?: string
betId?: string
// denormalized from contract
contractSlug: string
contractQuestion: string
// denormalized from bet
betAmount?: number
betOutcome?: string
// denormalized based on betting history
commenterPositionProb?: number // binary only
commenterPositionShares?: number
commenterPositionOutcome?: string
} }
export type OnGroup = {
commentType: 'group'
groupId: string
}
export type OnPost = {
commentType: 'post'
postId: string
}
export type ContractComment = Comment<OnContract>
export type GroupComment = Comment<OnGroup>
export type PostComment = Comment<OnPost>

View File

@ -1,168 +0,0 @@
import { Challenge } from './challenge'
import { BinaryContract, Contract } from './contract'
import { getFormattedMappedValue } from './pseudo-numeric'
import { getProbability } from './calculate'
import { richTextToString } from './util/parse'
import { getCpmmProbability } from './calculate-cpmm'
import { getDpmProbability } from './calculate-dpm'
import { formatMoney, formatPercent } from './util/format'
export function contractMetrics(contract: Contract) {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const dayjs = require('dayjs')
const { createdTime, resolutionTime, isResolved } = contract
const createdDate = dayjs(createdTime).format('MMM D')
const resolvedDate = isResolved
? dayjs(resolutionTime).format('MMM D')
: undefined
const volumeLabel = `${formatMoney(contract.volume)} bet`
return { volumeLabel, createdDate, resolvedDate }
}
// String version of the above, to send to the OpenGraph image generator
export function contractTextDetails(contract: Contract) {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const dayjs = require('dayjs')
const { closeTime, groupLinks } = contract
const { createdDate, resolvedDate, volumeLabel } = contractMetrics(contract)
const groupHashtags = groupLinks?.map((g) => `#${g.name.replace(/ /g, '')}`)
return (
`${resolvedDate ? `${createdDate} - ${resolvedDate}` : createdDate}` +
(closeTime
? `${closeTime > Date.now() ? 'Closes' : 'Closed'} ${dayjs(
closeTime
).format('MMM D, h:mma')}`
: '') +
`${volumeLabel}` +
(groupHashtags ? `${groupHashtags.join(' ')}` : '')
)
}
export function getBinaryProb(contract: BinaryContract) {
const { pool, resolutionProbability, mechanism } = contract
return (
resolutionProbability ??
(mechanism === 'cpmm-1'
? getCpmmProbability(pool, contract.p)
: getDpmProbability(contract.totalShares))
)
}
export const getOpenGraphProps = (contract: Contract) => {
const {
resolution,
question,
creatorName,
creatorUsername,
outcomeType,
creatorAvatarUrl,
description: desc,
} = contract
const probPercent =
outcomeType === 'BINARY'
? formatPercent(getBinaryProb(contract))
: undefined
const numericValue =
outcomeType === 'PSEUDO_NUMERIC'
? getFormattedMappedValue(contract)(getProbability(contract))
: undefined
const stringDesc = typeof desc === 'string' ? desc : richTextToString(desc)
const description = resolution
? `Resolved ${resolution}. ${stringDesc}`
: probPercent
? `${probPercent} chance. ${stringDesc}`
: stringDesc
return {
question,
probability: probPercent,
metadata: contractTextDetails(contract),
creatorName,
creatorUsername,
creatorAvatarUrl,
description,
numericValue,
resolution,
}
}
export type OgCardProps = {
question: string
probability?: string
metadata: string
creatorName: string
creatorUsername: string
creatorAvatarUrl?: string
numericValue?: string
resolution?: string
}
export function buildCardUrl(props: OgCardProps, challenge?: Challenge) {
const {
creatorAmount,
acceptances,
acceptorAmount,
creatorOutcome,
acceptorOutcome,
} = challenge || {}
const {
probability,
numericValue,
resolution,
creatorAvatarUrl,
question,
metadata,
creatorUsername,
creatorName,
} = props
const { userName, userAvatarUrl } = acceptances?.[0] ?? {}
const probabilityParam =
probability === undefined
? ''
: `&probability=${encodeURIComponent(probability ?? '')}`
const numericValueParam =
numericValue === undefined
? ''
: `&numericValue=${encodeURIComponent(numericValue ?? '')}`
const creatorAvatarUrlParam =
creatorAvatarUrl === undefined
? ''
: `&creatorAvatarUrl=${encodeURIComponent(creatorAvatarUrl ?? '')}`
const challengeUrlParams = challenge
? `&creatorAmount=${creatorAmount}&creatorOutcome=${creatorOutcome}` +
`&challengerAmount=${acceptorAmount}&challengerOutcome=${acceptorOutcome}` +
`&acceptedName=${userName ?? ''}&acceptedAvatarUrl=${userAvatarUrl ?? ''}`
: ''
const resolutionUrlParam = resolution
? `&resolution=${encodeURIComponent(resolution)}`
: ''
// URL encode each of the props, then add them as query params
return (
`https://manifold-og-image.vercel.app/m.png` +
`?question=${encodeURIComponent(question)}` +
probabilityParam +
numericValueParam +
`&metadata=${encodeURIComponent(metadata)}` +
`&creatorName=${encodeURIComponent(creatorName)}` +
creatorAvatarUrlParam +
`&creatorUsername=${encodeURIComponent(creatorUsername)}` +
challengeUrlParams +
resolutionUrlParam
)
}

View File

@ -1,23 +1,13 @@
import { Answer } from './answer' import { Answer } from './answer'
import { Fees } from './fees' import { Fees } from './fees'
import { JSONContent } from '@tiptap/core'
import { GroupLink } from 'common/group'
export type AnyMechanism = DPM | CPMM export type AnyMechanism = DPM | CPMM
export type AnyOutcomeType = export type AnyOutcomeType = Binary | FreeResponse | Numeric
| Binary
| MultipleChoice
| 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)
| (DPM & MultipleChoice)
export type Contract<T extends AnyContractType = AnyContractType> = { export type Contract<T extends AnyContractType = AnyContractType> = {
id: string id: string
@ -29,10 +19,10 @@ export type Contract<T extends AnyContractType = AnyContractType> = {
creatorAvatarUrl?: string creatorAvatarUrl?: string
question: string question: string
description: string | JSONContent // More info about what the contract is about description: string // More info about what the contract is about
tags: string[] tags: string[]
lowercaseTags: string[] lowercaseTags: string[]
visibility: visibility visibility: 'public' | 'unlisted'
createdTime: number // Milliseconds since epoch createdTime: number // Milliseconds since epoch
lastUpdatedTime?: number // Updated on new bet or comment lastUpdatedTime?: number // Updated on new bet or comment
@ -43,37 +33,19 @@ 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
closeEmailsSent?: number closeEmailsSent?: number
volume: number volume: number
volume24Hours: number volume24Hours: number
volume7Days: number volume7Days: number
elasticity: number
collectedFees: Fees collectedFees: Fees
groupSlugs?: string[]
groupLinks?: GroupLink[]
uniqueBettorIds?: string[]
uniqueBettorCount?: number
popularityScore?: number
dailyScore?: number
followerCount?: number
featuredOnHomeRank?: number
likedByUserIds?: string[]
likedByUserCount?: number
flaggedByUsernames?: string[]
openCommentBounties?: number
unlistedById?: string
} & 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 MultipleChoiceContract = Contract & MultipleChoice
export type DPMContract = Contract & DPM export type DPMContract = Contract & DPM
export type CPMMContract = Contract & CPMM export type CPMMContract = Contract & CPMM
export type DPMBinaryContract = BinaryContract & DPM export type DPMBinaryContract = BinaryContract & DPM
@ -92,33 +64,14 @@ export type CPMM = {
mechanism: 'cpmm-1' mechanism: 'cpmm-1'
pool: { [outcome: string]: number } pool: { [outcome: string]: number }
p: number // probability constant in y^p * n^(1-p) = k p: number // probability constant in y^p * n^(1-p) = k
totalLiquidity: number // for historical reasons, this the total subsidy amount added in M$ totalLiquidity: number // in M$
subsidyPool: number // current value of subsidy pool in M$
prob: number
probChanges: {
day: number
week: number
month: number
}
} }
export type Binary = { export type Binary = {
outcomeType: 'BINARY' outcomeType: 'BINARY'
initialProbability: number initialProbability: number
resolutionProbability?: number // Used for BINARY markets resolved to MKT resolutionProbability?: number // Used for BINARY markets resolved to MKT
resolution?: resolution resolution?: 'YES' | 'NO' | 'MKT' | 'CANCEL'
}
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 = {
@ -128,13 +81,6 @@ export type FreeResponse = {
resolutions?: { [outcome: string]: number } // Used for MKT resolution. resolutions?: { [outcome: string]: number } // Used for MKT resolution.
} }
export type MultipleChoice = {
outcomeType: 'MULTIPLE_CHOICE'
answers: Answer[]
resolution?: string | 'MKT' | 'CANCEL'
resolutions?: { [outcome: string]: number } // Used for MKT resolution.
}
export type Numeric = { export type Numeric = {
outcomeType: 'NUMERIC' outcomeType: 'NUMERIC'
bucketCount: number bucketCount: number
@ -145,21 +91,7 @@ export type Numeric = {
} }
export type outcomeType = AnyOutcomeType['outcomeType'] export type outcomeType = AnyOutcomeType['outcomeType']
export type resolution = 'YES' | 'NO' | 'MKT' | 'CANCEL' export const OUTCOME_TYPES = ['BINARY', 'FREE_RESPONSE', 'NUMERIC'] as const
export const RESOLUTIONS = ['YES', 'NO', 'MKT', 'CANCEL'] as const export const MAX_QUESTION_LENGTH = 480
export const OUTCOME_TYPES = [ export const MAX_DESCRIPTION_LENGTH = 10000
'BINARY',
'MULTIPLE_CHOICE',
'FREE_RESPONSE',
'PSEUDO_NUMERIC',
'NUMERIC',
] as const
export const MAX_QUESTION_LENGTH = 240
export const MAX_DESCRIPTION_LENGTH = 16000
export const MAX_TAG_LENGTH = 60 export const MAX_TAG_LENGTH = 60
export const CPMM_MIN_POOL_QTY = 0.01
export type visibility = 'public' | 'unlisted'
export const VISIBILITIES = ['public', 'unlisted'] as const

View File

@ -1,20 +0,0 @@
import { ENV_CONFIG } from './envs/constants'
const econ = ENV_CONFIG.economy
export const FIXED_ANTE = econ?.FIXED_ANTE ?? 100
export const STARTING_BALANCE = econ?.STARTING_BALANCE ?? 1000
// for sus users, i.e. multiple sign ups for same person
export const SUS_STARTING_BALANCE = econ?.SUS_STARTING_BALANCE ?? 10
export const REFERRAL_AMOUNT = econ?.REFERRAL_AMOUNT ?? 250
export const UNIQUE_BETTOR_BONUS_AMOUNT = econ?.UNIQUE_BETTOR_BONUS_AMOUNT ?? 10
export const BETTING_STREAK_BONUS_AMOUNT =
econ?.BETTING_STREAK_BONUS_AMOUNT ?? 5
export const BETTING_STREAK_BONUS_MAX = econ?.BETTING_STREAK_BONUS_MAX ?? 25
export const BETTING_STREAK_RESET_HOUR = econ?.BETTING_STREAK_RESET_HOUR ?? 7
export const FREE_MARKETS_PER_USER_MAX = econ?.FREE_MARKETS_PER_USER_MAX ?? 5
export const COMMENT_BOUNTY_AMOUNT = econ?.COMMENT_BOUNTY_AMOUNT ?? 250
export const UNIQUE_BETTOR_LIQUIDITY = 20

View File

@ -21,38 +21,18 @@ export function isWhitelisted(email?: string) {
} }
// TODO: Before open sourcing, we should turn these into env vars // TODO: Before open sourcing, we should turn these into env vars
export function isAdmin(email?: string) { export function isAdmin(email: string) {
if (!email) {
return false
}
return ENV_CONFIG.adminEmails.includes(email) return ENV_CONFIG.adminEmails.includes(email)
} }
export function isManifoldId(userId: string) {
return userId === 'IPTOzEqrpkWmEzh6hwvAyY9PqFb2'
}
export const DOMAIN = ENV_CONFIG.domain export const DOMAIN = ENV_CONFIG.domain
export const FIREBASE_CONFIG = ENV_CONFIG.firebaseConfig export const FIREBASE_CONFIG = ENV_CONFIG.firebaseConfig
export const PROJECT_ID = ENV_CONFIG.firebaseConfig.projectId export const PROJECT_ID = ENV_CONFIG.firebaseConfig.projectId
export const IS_PRIVATE_MANIFOLD = ENV_CONFIG.visibility === 'PRIVATE' export const IS_PRIVATE_MANIFOLD = ENV_CONFIG.visibility === 'PRIVATE'
export const AUTH_COOKIE_NAME = `FBUSER_${PROJECT_ID.toUpperCase().replace(
/-/g,
'_'
)}`
// Manifold's domain or any subdomains thereof // Manifold's domain or any subdomains thereof
export const CORS_ORIGIN_MANIFOLD = new RegExp( export const CORS_ORIGIN_MANIFOLD = new RegExp(
'^https?://(?:[a-zA-Z0-9\\-]+\\.)*' + escapeRegExp(ENV_CONFIG.domain) + '$' '^https?://(?:[a-zA-Z0-9\\-]+\\.)*' + escapeRegExp(ENV_CONFIG.domain) + '$'
) )
// Vercel deployments, used for testing.
export const CORS_ORIGIN_VERCEL = new RegExp(
'^https?://[a-zA-Z0-9\\-]+' + escapeRegExp('mantic.vercel.app') + '$'
)
// Any localhost server on any port // Any localhost server on any port
export const CORS_ORIGIN_LOCALHOST = /^http:\/\/localhost:\d+$/ export const CORS_ORIGIN_LOCALHOST = /^http:\/\/localhost:\d+$/
export function firestoreConsolePath(contractId: string) {
return `https://console.firebase.google.com/project/${PROJECT_ID}/firestore/data/~2Fcontracts~2F${contractId}`
}

View File

@ -2,7 +2,6 @@ import { EnvConfig, PROD_CONFIG } from './prod'
export const DEV_CONFIG: EnvConfig = { export const DEV_CONFIG: EnvConfig = {
...PROD_CONFIG, ...PROD_CONFIG,
domain: 'dev.manifold.markets',
firebaseConfig: { firebaseConfig: {
apiKey: 'AIzaSyBoq3rzUa8Ekyo3ZaTnlycQYPRCA26VpOw', apiKey: 'AIzaSyBoq3rzUa8Ekyo3ZaTnlycQYPRCA26VpOw',
authDomain: 'dev-mantic-markets.firebaseapp.com', authDomain: 'dev-mantic-markets.firebaseapp.com',
@ -13,9 +12,4 @@ export const DEV_CONFIG: EnvConfig = {
appId: '1:134303100058:web:27f9ea8b83347251f80323', appId: '1:134303100058:web:27f9ea8b83347251f80323',
measurementId: 'G-YJC9E37P37', measurementId: 'G-YJC9E37P37',
}, },
cloudRunId: 'w3txbmd3ba',
cloudRunRegion: 'uc',
amplitudeApiKey: 'fd8cbfd964b9a205b8678a39faae71b3',
twitchBotEndpoint: 'https://dev-twitch-bot.manifold.markets',
sprigEnvironmentId: 'Tu7kRZPm7daP',
} }

View File

@ -1,14 +1,6 @@
export type EnvConfig = { export type EnvConfig = {
domain: string domain: string
firebaseConfig: FirebaseConfig firebaseConfig: FirebaseConfig
amplitudeApiKey?: string
twitchBotEndpoint?: string
sprigEnvironmentId?: string
// IDs for v2 cloud functions -- find these by deploying a cloud function and
// examining the URL, https://[name]-[cloudRunId]-[cloudRunRegion].a.run.app
cloudRunId: string
cloudRunRegion: string
// Access controls // Access controls
adminEmails: string[] adminEmails: string[]
@ -17,38 +9,16 @@ export type EnvConfig = {
// Branding // Branding
moneyMoniker: string // e.g. 'M$' moneyMoniker: string // e.g. 'M$'
bettor?: string // e.g. 'bettor' or 'predictor'
presentBet?: string // e.g. 'bet' or 'predict'
pastBet?: string // e.g. 'bet' or 'prediction'
faviconPath?: string // Should be a file in /public faviconPath?: string // Should be a file in /public
navbarLogoPath?: string navbarLogoPath?: string
newQuestionPlaceholders: string[] newQuestionPlaceholders: string[]
economy?: Economy
}
export type Economy = {
FIXED_ANTE?: number
STARTING_BALANCE?: number
SUS_STARTING_BALANCE?: number
REFERRAL_AMOUNT?: number
UNIQUE_BETTOR_BONUS_AMOUNT?: number
BETTING_STREAK_BONUS_AMOUNT?: number
BETTING_STREAK_BONUS_MAX?: number
BETTING_STREAK_RESET_HOUR?: number
FREE_MARKETS_PER_USER_MAX?: number
COMMENT_BOUNTY_AMOUNT?: number
} }
type FirebaseConfig = { type FirebaseConfig = {
apiKey: string apiKey: string
authDomain: string authDomain: string
projectId: string projectId: string
region?: string region: string
storageBucket: string storageBucket: string
messagingSenderId: string messagingSenderId: string
appId: string appId: string
@ -57,9 +27,6 @@ type FirebaseConfig = {
export const PROD_CONFIG: EnvConfig = { export const PROD_CONFIG: EnvConfig = {
domain: 'manifold.markets', domain: 'manifold.markets',
amplitudeApiKey: '2d6509fd4185ebb8be29709842752a15',
sprigEnvironmentId: 'sQcrq9TDqkib',
firebaseConfig: { firebaseConfig: {
apiKey: 'AIzaSyDp3J57vLeAZCzxLD-vcPaGIkAmBoGOSYw', apiKey: 'AIzaSyDp3J57vLeAZCzxLD-vcPaGIkAmBoGOSYw',
authDomain: 'mantic-markets.firebaseapp.com', authDomain: 'mantic-markets.firebaseapp.com',
@ -70,26 +37,16 @@ export const PROD_CONFIG: EnvConfig = {
appId: '1:128925704902:web:f61f86944d8ffa2a642dc7', appId: '1:128925704902:web:f61f86944d8ffa2a642dc7',
measurementId: 'G-SSFK1Q138D', measurementId: 'G-SSFK1Q138D',
}, },
twitchBotEndpoint: 'https://twitch-bot.manifold.markets',
cloudRunId: 'nggbo3neva',
cloudRunRegion: 'uc',
adminEmails: [ adminEmails: [
'akrolsmir@gmail.com', // Austin 'akrolsmir@gmail.com', // Austin
'jahooma@gmail.com', // James 'jahooma@gmail.com', // James
'taowell@gmail.com', // Stephen 'taowell@gmail.com', // Stephen
'abc.sinclair@gmail.com', // Sinclair 'abc.sinclair@gmail.com', // Sinclair
'manticmarkets@gmail.com', // Manifold 'manticmarkets@gmail.com', // Manifold
'iansphilips@gmail.com', // Ian
'd4vidchee@gmail.com', // D4vid
'federicoruizcassarino@gmail.com', // Fede
'ingawei@gmail.com', //Inga
], ],
visibility: 'PUBLIC', visibility: 'PUBLIC',
moneyMoniker: 'M$', moneyMoniker: 'M$',
bettor: 'trader',
pastBet: 'trade',
presentBet: 'trade',
navbarLogoPath: '', navbarLogoPath: '',
faviconPath: '/favicon.ico', faviconPath: '/favicon.ico',
newQuestionPlaceholders: [ newQuestionPlaceholders: [

View File

@ -12,8 +12,6 @@ export const THEOREMONE_CONFIG: EnvConfig = {
appId: '1:698012149198:web:b342af75662831aa84b79f', appId: '1:698012149198:web:b342af75662831aa84b79f',
measurementId: 'G-Y3EZ1WNT6E', measurementId: 'G-Y3EZ1WNT6E',
}, },
cloudRunId: 'nggbo3neva', // TODO: fill in real ID for T1
cloudRunRegion: 'uc',
adminEmails: [...PROD_CONFIG.adminEmails, 'david.glidden@theoremone.co'], adminEmails: [...PROD_CONFIG.adminEmails, 'david.glidden@theoremone.co'],
whitelistEmail: '@theoremone.co', whitelistEmail: '@theoremone.co',
moneyMoniker: 'T$', moneyMoniker: 'T$',

View File

@ -1,11 +1,9 @@
export const FLAT_TRADE_FEE = 0.1 // M$0.1 export const PLATFORM_FEE = 0.01
export const CREATOR_FEE = 0.06
export const LIQUIDITY_FEE = 0.06
export const PLATFORM_FEE = 0 export const DPM_PLATFORM_FEE = 0.01
export const CREATOR_FEE = 0 export const DPM_CREATOR_FEE = 0.04
export const LIQUIDITY_FEE = 0
export const DPM_PLATFORM_FEE = 0.0
export const DPM_CREATOR_FEE = 0.0
export const DPM_FEES = DPM_PLATFORM_FEE + DPM_CREATOR_FEE export const DPM_FEES = DPM_PLATFORM_FEE + DPM_CREATOR_FEE
export type Fees = { export type Fees = {

23
common/fold.ts Normal file
View File

@ -0,0 +1,23 @@
export type Fold = {
id: string
slug: string
name: string
about: string
curatorId: string // User id
createdTime: number
tags: string[]
lowercaseTags: string[]
contractIds: string[]
excludedContractIds: string[]
// Invariant: exactly one of the following is defined.
// Default: creatorIds: undefined, excludedCreatorIds: []
creatorIds?: string[]
excludedCreatorIds?: string[]
followCount: number
disallowMarketCreation?: boolean
}

View File

@ -1,9 +0,0 @@
export type Follow = {
userId: string
timestamp: number
}
export type ContractFollow = {
id: string // user id
createdTime: number
}

View File

@ -1,3 +0,0 @@
export type GlobalConfig = {
pinnedItems: { itemId: string; type: 'post' | 'contract' }[]
}

View File

@ -1,42 +0,0 @@
export type Group = {
id: string
slug: string
name: string
about: string
creatorId: string // User id
createdTime: number
mostRecentActivityTime: number
anyoneCanJoin: boolean
totalContracts: number
totalMembers: number
aboutPostId?: string
postIds: string[]
chatDisabled?: boolean
mostRecentContractAddedTime?: number
cachedLeaderboard?: {
topTraders: {
userId: string
score: number
}[]
topCreators: {
userId: string
score: number
}[]
}
pinnedItems: { itemId: string; type: 'post' | 'contract' }[]
}
export const MAX_GROUP_NAME_LENGTH = 75
export const MAX_ABOUT_LENGTH = 140
export const MAX_ID_LENGTH = 60
export const NEW_USER_GROUP_SLUGS = ['updates', 'bugs', 'welcome']
export const GROUP_CHAT_SLUG = 'chat'
export type GroupLink = {
slug: string
name: string
groupId: string
createdTime: number
userId?: string
}
export type GroupContractDoc = { contractId: string; createdTime: number }

View File

@ -1,9 +0,0 @@
export type Like = {
id: string // will be id of the object liked, i.e. contract.id
userId: string
type: 'contract' | 'post'
createdTime: number
tipTxnId?: string // only holds most recent tip txn id
}
export const LIKE_TIP_AMOUNT = 10
export const TIP_UNDO_DURATION = 2000

View File

@ -1,138 +0,0 @@
import { Dictionary, groupBy, sumBy, minBy } from 'lodash'
import { Bet } from './bet'
import { getContractBetMetrics } from './calculate'
import {
Contract,
CPMMContract,
FreeResponseContract,
MultipleChoiceContract,
} from './contract'
import { PortfolioMetrics, User } from './user'
import { filterDefined } from './util/array'
const LOAN_DAILY_RATE = 0.02
const calculateNewLoan = (investedValue: number, loanTotal: number) => {
const netValue = investedValue - loanTotal
return netValue * LOAN_DAILY_RATE
}
export const getLoanUpdates = (
users: User[],
contractsById: { [contractId: string]: Contract },
portfolioByUser: { [userId: string]: PortfolioMetrics | undefined },
betsByUser: { [userId: string]: Bet[] }
) => {
const eligibleUsers = filterDefined(
users.map((user) =>
isUserEligibleForLoan(portfolioByUser[user.id]) ? user : undefined
)
)
const betUpdates = eligibleUsers
.map((user) => {
const updates = calculateLoanBetUpdates(
betsByUser[user.id] ?? [],
contractsById
).betUpdates
return updates.map((update) => ({ ...update, user }))
})
.flat()
const updatesByUser = groupBy(betUpdates, (update) => update.userId)
const userPayouts = Object.values(updatesByUser).map((updates) => {
return {
user: updates[0].user,
payout: sumBy(updates, (update) => update.newLoan),
}
})
return {
betUpdates,
userPayouts,
}
}
const isUserEligibleForLoan = (portfolio: PortfolioMetrics | undefined) => {
if (!portfolio) return true
const { balance, investmentValue } = portfolio
return balance + investmentValue > 0
}
const calculateLoanBetUpdates = (
bets: Bet[],
contractsById: Dictionary<Contract>
) => {
const betsByContract = groupBy(bets, (bet) => bet.contractId)
const contracts = filterDefined(
Object.keys(betsByContract).map((contractId) => contractsById[contractId])
).filter((c) => !c.isResolved)
const betUpdates = filterDefined(
contracts
.map((c) => {
if (c.mechanism === 'cpmm-1') {
return getBinaryContractLoanUpdate(c, betsByContract[c.id])
} else if (
c.outcomeType === 'FREE_RESPONSE' ||
c.outcomeType === 'MULTIPLE_CHOICE'
)
return getFreeResponseContractLoanUpdate(c, betsByContract[c.id])
else {
// Unsupported contract / mechanism for loans.
return []
}
})
.flat()
)
const totalNewLoan = sumBy(betUpdates, (loanUpdate) => loanUpdate.loanTotal)
return {
totalNewLoan,
betUpdates,
}
}
const getBinaryContractLoanUpdate = (contract: CPMMContract, bets: Bet[]) => {
const { invested } = getContractBetMetrics(contract, bets)
const loanAmount = sumBy(bets, (bet) => bet.loanAmount ?? 0)
const oldestBet = minBy(bets, (bet) => bet.createdTime)
const newLoan = calculateNewLoan(invested, loanAmount)
if (!isFinite(newLoan) || newLoan <= 0 || !oldestBet) return undefined
const loanTotal = (oldestBet.loanAmount ?? 0) + newLoan
return {
userId: oldestBet.userId,
contractId: contract.id,
betId: oldestBet.id,
newLoan,
loanTotal,
}
}
const getFreeResponseContractLoanUpdate = (
contract: FreeResponseContract | MultipleChoiceContract,
bets: Bet[]
) => {
const openBets = bets.filter((bet) => !bet.isSold && !bet.sale)
return openBets.map((bet) => {
const loanAmount = bet.loanAmount ?? 0
const newLoan = calculateNewLoan(bet.amount, loanAmount)
const loanTotal = loanAmount + newLoan
if (!isFinite(newLoan) || newLoan <= 0) return undefined
return {
userId: bet.userId,
contractId: contract.id,
betId: bet.id,
newLoan,
loanTotal,
}
})
}

View File

@ -1,35 +0,0 @@
export type Manalink = {
// The link to send: https://manifold.markets/send/{slug}
// Also functions as the unique id for the link.
slug: string
// Note: we assume both fromId and toId are of SourceType 'USER'
fromId: string
// Displayed to people claiming the link
message: string
// How much to send with the link
amount: number
token: 'M$' // TODO: could send eg YES shares too??
createdTime: number
// If null, the link is valid forever
expiresTime: number | null
// If null, the link can be used infinitely
maxUses: number | null
// Used for simpler caching
claimedUserIds: string[]
// Successful redemptions of the link
claims: Claim[]
}
export type Claim = {
toId: string
// The ID of the successful txn that tracks the money moved
txnId: string
claimedTime: number
}

View File

@ -1,6 +1,6 @@
import { sortBy, sum, sumBy } from 'lodash' import { sumBy } from 'lodash'
import { Bet, fill, LimitBet, NumericBet } from './bet' import { Bet, MAX_LOAN_PER_CONTRACT, NumericBet } from './bet'
import { import {
calculateDpmShares, calculateDpmShares,
getDpmProbability, getDpmProbability,
@ -8,34 +8,20 @@ import {
getNumericBets, getNumericBets,
calculateNumericDpmShares, calculateNumericDpmShares,
} from './calculate-dpm' } from './calculate-dpm'
import { import { calculateCpmmPurchase, getCpmmProbability } from './calculate-cpmm'
calculateCpmmAmountToProb,
calculateCpmmPurchase,
CpmmState,
getCpmmProbability,
} from './calculate-cpmm'
import { import {
CPMMBinaryContract, CPMMBinaryContract,
DPMBinaryContract, DPMBinaryContract,
DPMContract, FreeResponseContract,
NumericContract, NumericContract,
PseudoNumericContract,
} from './contract' } from './contract'
import { noFees } from './fees' import { noFees } from './fees'
import { addObjects, removeUndefinedProps } from './util/object' import { addObjects } from './util/object'
import { NUMERIC_FIXED_VAR } from './numeric-constants' import { NUMERIC_FIXED_VAR } from './numeric-constants'
import {
floatingEqual,
floatingGreaterEqual,
floatingLesserEqual,
} from './util/math'
export type CandidateBet<T extends Bet = Bet> = Omit< export type CandidateBet<T extends Bet> = Omit<T, 'id' | 'userId'>
T,
'id' | 'userId' | 'userAvatarUrl' | 'userName' | 'userUsername'
>
export type BetInfo = { export type BetInfo = {
newBet: CandidateBet newBet: CandidateBet<Bet>
newPool?: { [outcome: string]: number } newPool?: { [outcome: string]: number }
newTotalShares?: { [outcome: string]: number } newTotalShares?: { [outcome: string]: number }
newTotalBets?: { [outcome: string]: number } newTotalBets?: { [outcome: string]: number }
@ -43,261 +29,45 @@ export type BetInfo = {
newP?: number newP?: number
} }
const computeFill = ( export const getNewBinaryCpmmBetInfo = (
amount: number,
outcome: 'YES' | 'NO', outcome: 'YES' | 'NO',
limitProb: number | undefined, amount: number,
cpmmState: CpmmState, contract: CPMMBinaryContract,
matchedBet: LimitBet | undefined loanAmount: number
) => { ) => {
const prob = getCpmmProbability(cpmmState.pool, cpmmState.p)
if (
limitProb !== undefined &&
(outcome === 'YES'
? floatingGreaterEqual(prob, limitProb) &&
(matchedBet?.limitProb ?? 1) > limitProb
: floatingLesserEqual(prob, limitProb) &&
(matchedBet?.limitProb ?? 0) < limitProb)
) {
// No fill.
return undefined
}
const timestamp = Date.now()
if (
!matchedBet ||
(outcome === 'YES'
? !floatingGreaterEqual(prob, matchedBet.limitProb)
: !floatingLesserEqual(prob, matchedBet.limitProb))
) {
// Fill from pool.
const limit = !matchedBet
? limitProb
: outcome === 'YES'
? Math.min(matchedBet.limitProb, limitProb ?? 1)
: Math.max(matchedBet.limitProb, limitProb ?? 0)
const buyAmount =
limit === undefined
? amount
: Math.min(amount, calculateCpmmAmountToProb(cpmmState, limit, outcome))
const { shares, newPool, newP, fees } = calculateCpmmPurchase( const { shares, newPool, newP, fees } = calculateCpmmPurchase(
cpmmState, contract,
buyAmount, amount,
outcome outcome
) )
const newState = { pool: newPool, p: newP }
return { const { pool, p, totalLiquidity } = contract
maker: { const probBefore = getCpmmProbability(pool, p)
matchedBetId: null, const probAfter = getCpmmProbability(newPool, newP)
shares,
amount: buyAmount,
state: newState,
fees,
timestamp,
},
taker: {
matchedBetId: null,
shares,
amount: buyAmount,
timestamp,
},
}
}
// Fill from matchedBet. const newBet: CandidateBet<Bet> = {
const matchRemaining = matchedBet.orderAmount - matchedBet.amount
const shares = Math.min(
amount /
(outcome === 'YES' ? matchedBet.limitProb : 1 - matchedBet.limitProb),
matchRemaining /
(outcome === 'YES' ? 1 - matchedBet.limitProb : matchedBet.limitProb)
)
const maker = {
bet: matchedBet,
matchedBetId: 'taker',
amount:
shares *
(outcome === 'YES' ? 1 - matchedBet.limitProb : matchedBet.limitProb),
shares,
timestamp,
}
const taker = {
matchedBetId: matchedBet.id,
amount:
shares *
(outcome === 'YES' ? matchedBet.limitProb : 1 - matchedBet.limitProb),
shares,
timestamp,
}
return { maker, taker }
}
export const computeFills = (
outcome: 'YES' | 'NO',
betAmount: number,
state: CpmmState,
limitProb: number | undefined,
unfilledBets: LimitBet[],
balanceByUserId: { [userId: string]: number }
) => {
if (isNaN(betAmount)) {
throw new Error('Invalid bet amount: ${betAmount}')
}
if (isNaN(limitProb ?? 0)) {
throw new Error('Invalid limitProb: ${limitProb}')
}
const sortedBets = sortBy(
unfilledBets.filter((bet) => bet.outcome !== outcome),
(bet) => (outcome === 'YES' ? bet.limitProb : -bet.limitProb),
(bet) => bet.createdTime
)
const takers: fill[] = []
const makers: {
bet: LimitBet
amount: number
shares: number
timestamp: number
}[] = []
const ordersToCancel: LimitBet[] = []
let amount = betAmount
let cpmmState = { pool: state.pool, p: state.p }
let totalFees = noFees
const currentBalanceByUserId = { ...balanceByUserId }
let i = 0
while (true) {
const matchedBet: LimitBet | undefined = sortedBets[i]
const fill = computeFill(amount, outcome, limitProb, cpmmState, matchedBet)
if (!fill) break
const { taker, maker } = fill
if (maker.matchedBetId === null) {
// Matched against pool.
cpmmState = maker.state
totalFees = addObjects(totalFees, maker.fees)
takers.push(taker)
} else {
// Matched against bet.
i++
const { userId } = maker.bet
const makerBalance = currentBalanceByUserId[userId]
if (floatingGreaterEqual(makerBalance, maker.amount)) {
currentBalanceByUserId[userId] = makerBalance - maker.amount
} else {
// Insufficient balance. Cancel maker bet.
ordersToCancel.push(maker.bet)
continue
}
takers.push(taker)
makers.push(maker)
}
amount -= taker.amount
if (floatingEqual(amount, 0)) break
}
return { takers, makers, totalFees, cpmmState, ordersToCancel }
}
export const getBinaryCpmmBetInfo = (
outcome: 'YES' | 'NO',
betAmount: number,
contract: CPMMBinaryContract | PseudoNumericContract,
limitProb: number | undefined,
unfilledBets: LimitBet[],
balanceByUserId: { [userId: string]: number }
) => {
const { pool, p } = contract
const { takers, makers, cpmmState, totalFees, ordersToCancel } = computeFills(
outcome,
betAmount,
{ pool, p },
limitProb,
unfilledBets,
balanceByUserId
)
const probBefore = getCpmmProbability(contract.pool, contract.p)
const probAfter = getCpmmProbability(cpmmState.pool, cpmmState.p)
const takerAmount = sumBy(takers, 'amount')
const takerShares = sumBy(takers, 'shares')
const isFilled = floatingEqual(betAmount, takerAmount)
const newBet: CandidateBet = removeUndefinedProps({
orderAmount: betAmount,
amount: takerAmount,
shares: takerShares,
limitProb,
isFilled,
isCancelled: false,
fills: takers,
contractId: contract.id, contractId: contract.id,
amount,
shares,
outcome, outcome,
fees,
loanAmount,
probBefore, probBefore,
probAfter, probAfter,
loanAmount: 0,
createdTime: Date.now(), createdTime: Date.now(),
fees: totalFees,
})
const { liquidityFee } = totalFees
const newTotalLiquidity = (contract.totalLiquidity ?? 0) + liquidityFee
return {
newBet,
newPool: cpmmState.pool,
newP: cpmmState.p,
newTotalLiquidity,
makers,
ordersToCancel,
}
} }
export const getBinaryBetStats = ( const { liquidityFee } = fees
outcome: 'YES' | 'NO', const newTotalLiquidity = (totalLiquidity ?? 0) + liquidityFee
betAmount: number,
contract: CPMMBinaryContract | PseudoNumericContract,
limitProb: number,
unfilledBets: LimitBet[],
balanceByUserId: { [userId: string]: number }
) => {
const { newBet } = getBinaryCpmmBetInfo(
outcome,
betAmount ?? 0,
contract,
limitProb,
unfilledBets,
balanceByUserId
)
const remainingMatched =
((newBet.orderAmount ?? 0) - newBet.amount) /
(outcome === 'YES' ? limitProb : 1 - limitProb)
const currentPayout = newBet.shares + remainingMatched
const currentReturn = betAmount ? (currentPayout - betAmount) / betAmount : 0 return { newBet, newPool, newP, newTotalLiquidity }
const totalFees = sum(Object.values(newBet.fees))
return { currentPayout, currentReturn, totalFees, newBet }
} }
export const getNewBinaryDpmBetInfo = ( export const getNewBinaryDpmBetInfo = (
outcome: 'YES' | 'NO', outcome: 'YES' | 'NO',
amount: number, amount: number,
contract: DPMBinaryContract contract: DPMBinaryContract,
loanAmount: number
) => { ) => {
const { YES: yesPool, NO: noPool } = contract.pool const { YES: yesPool, NO: noPool } = contract.pool
@ -325,10 +95,10 @@ export const getNewBinaryDpmBetInfo = (
const probBefore = getDpmProbability(contract.totalShares) const probBefore = getDpmProbability(contract.totalShares)
const probAfter = getDpmProbability(newTotalShares) const probAfter = getDpmProbability(newTotalShares)
const newBet: CandidateBet = { const newBet: CandidateBet<Bet> = {
contractId: contract.id, contractId: contract.id,
amount, amount,
loanAmount: 0, loanAmount,
shares, shares,
outcome, outcome,
probBefore, probBefore,
@ -343,7 +113,8 @@ export const getNewBinaryDpmBetInfo = (
export const getNewMultiBetInfo = ( export const getNewMultiBetInfo = (
outcome: string, outcome: string,
amount: number, amount: number,
contract: DPMContract contract: FreeResponseContract,
loanAmount: number
) => { ) => {
const { pool, totalShares, totalBets } = contract const { pool, totalShares, totalBets } = contract
@ -361,10 +132,10 @@ export const getNewMultiBetInfo = (
const probBefore = getDpmOutcomeProbability(totalShares, outcome) const probBefore = getDpmOutcomeProbability(totalShares, outcome)
const probAfter = getDpmOutcomeProbability(newTotalShares, outcome) const probAfter = getDpmOutcomeProbability(newTotalShares, outcome)
const newBet: CandidateBet = { const newBet: CandidateBet<Bet> = {
contractId: contract.id, contractId: contract.id,
amount, amount,
loanAmount: 0, loanAmount,
shares, shares,
outcome, outcome,
probBefore, probBefore,
@ -418,3 +189,13 @@ export const getNumericBetsInfo = (
return { newBet, newPool, newTotalShares, newTotalBets } return { newBet, newPool, newTotalShares, newTotalBets }
} }
export const getLoanAmount = (yourBets: Bet[], newBetAmount: number) => {
const openBets = yourBets.filter((bet) => !bet.isSold && !bet.sale)
const prevLoanAmount = sumBy(openBets, (bet) => bet.loanAmount ?? 0)
const loanAmount = Math.min(
newBetAmount,
MAX_LOAN_PER_CONTRACT - prevLoanAmount
)
return loanAmount
}

View File

@ -5,15 +5,12 @@ import {
CPMM, CPMM,
DPM, DPM,
FreeResponse, FreeResponse,
MultipleChoice,
Numeric, Numeric,
outcomeType, outcomeType,
PseudoNumeric,
visibility,
} from './contract' } from './contract'
import { User } from './user' import { User } from './user'
import { parseTags } from './util/parse'
import { removeUndefinedProps } from './util/object' import { removeUndefinedProps } from './util/object'
import { JSONContent } from '@tiptap/core'
export function getNewContract( export function getNewContract(
id: string, id: string,
@ -21,7 +18,7 @@ export function getNewContract(
creator: User, creator: User,
question: string, question: string,
outcomeType: outcomeType, outcomeType: outcomeType,
description: JSONContent, description: string,
initialProb: number, initialProb: number,
ante: number, ante: number,
closeTime: number, closeTime: number,
@ -30,22 +27,18 @@ 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,
// for multiple choice
answers: string[],
visibility: visibility
) { ) {
const tags = parseTags(
`${question} ${description} ${extraTags.map((tag) => `#${tag}`).join(' ')}`
)
const lowercaseTags = tags.map((tag) => tag.toLowerCase())
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)
: outcomeType === 'MULTIPLE_CHOICE'
? getMultipleChoiceProps(ante, answers)
: getFreeAnswerProps(ante) : getFreeAnswerProps(ante)
const contract: Contract = removeUndefinedProps({ const contract: Contract = removeUndefinedProps({
@ -59,11 +52,10 @@ export function getNewContract(
creatorAvatarUrl: creator.avatarUrl, creatorAvatarUrl: creator.avatarUrl,
question: question.trim(), question: question.trim(),
description, description: description.trim(),
tags: [], tags,
lowercaseTags: [], lowercaseTags,
visibility, visibility: 'public',
unlistedById: visibility === 'unlisted' ? creator.id : undefined,
isResolved: false, isResolved: false,
createdTime: Date.now(), createdTime: Date.now(),
closeTime, closeTime,
@ -71,7 +63,6 @@ export function getNewContract(
volume: 0, volume: 0,
volume24Hours: 0, volume24Hours: 0,
volume7Days: 0, volume7Days: 0,
elasticity: propsByOutcomeType.mechanism === 'cpmm-1' ? 0.38 : 0.75,
collectedFees: { collectedFees: {
creatorFee: 0, creatorFee: 0,
@ -112,30 +103,9 @@ const getBinaryCpmmProps = (initialProb: number, ante: number) => {
mechanism: 'cpmm-1', mechanism: 'cpmm-1',
outcomeType: 'BINARY', outcomeType: 'BINARY',
totalLiquidity: ante, totalLiquidity: ante,
subsidyPool: 0,
initialProbability: p, initialProbability: p,
p, p,
pool: pool, pool: pool,
prob: initialProb,
probChanges: { day: 0, week: 0, month: 0 },
}
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 return system
@ -154,26 +124,6 @@ const getFreeAnswerProps = (ante: number) => {
return system return system
} }
const getMultipleChoiceProps = (ante: number, answers: string[]) => {
const numAnswers = answers.length
const betAnte = ante / numAnswers
const betShares = Math.sqrt(ante ** 2 / numAnswers)
const defaultValues = (x: any) =>
Object.fromEntries(range(0, numAnswers).map((k) => [k, x]))
const system: DPM & MultipleChoice = {
mechanism: 'dpm-2',
outcomeType: 'MULTIPLE_CHOICE',
pool: defaultValues(betAnte),
totalShares: defaultValues(betShares),
totalBets: defaultValues(betAnte),
answers: [],
}
return system
}
const getNumericProps = ( const getNumericProps = (
ante: number, ante: number,
bucketCount: number, bucketCount: number,

View File

@ -1,270 +1,29 @@
import { notification_preference } from './user-notification-preferences'
export type Notification = { export type Notification = {
id: string id: string
userId: string userId: string
reasonText?: string reasonText?: string
reason?: notification_reason_types | notification_preference reason?: notification_reason_types
createdTime: number createdTime: number
viewTime?: number viewTime?: number
isSeen: boolean isSeen: boolean
sourceId?: string sourceId?: string
sourceType?: notification_source_types sourceType?: notification_source_types
sourceUpdateType?: notification_source_update_types
sourceContractId?: string sourceContractId?: string
sourceUserName?: string sourceUserName?: string
sourceUserUsername?: string sourceUserUsername?: string
sourceUserAvatarUrl?: string sourceUserAvatarUrl?: string
sourceText?: string
data?: { [key: string]: any }
sourceContractTitle?: string
sourceContractCreatorUsername?: string
sourceContractSlug?: string
sourceSlug?: string
sourceTitle?: string
isSeenOnHref?: string
} }
export type notification_source_types = export type notification_source_types =
| 'contract' | 'contract'
| 'comment' | 'comment'
| 'bet' | 'bet'
| 'answer' | 'answer'
| 'liquidity' | 'liquidity'
| 'follow'
| 'tip'
| 'admin_message'
| 'group'
| 'user'
| 'bonus'
| 'challenge'
| 'betting_streak_bonus'
| 'loan'
| 'like'
| 'tip_and_like'
| 'badge'
export type notification_source_update_types = export type notification_reason_types =
| 'created' | 'created'
| 'updated' | 'updated'
| 'resolved' | 'resolved'
| 'deleted' | 'tagged'
| 'closed' | 'replied'
/* Optional - if possible use a notification_preference */
export type notification_reason_types =
| 'tagged_user'
| 'on_new_follow'
| 'contract_from_followed_user'
| 'you_referred_user'
| 'user_joined_to_bet_on_your_market'
| 'unique_bettors_on_your_contract'
| 'tip_received'
| 'bet_fill'
| 'user_joined_from_your_group_invite'
| 'challenge_accepted'
| 'betting_streak_incremented'
| 'loan_income'
| 'liked_and_tipped_your_contract'
| 'comment_on_your_contract'
| 'answer_on_your_contract'
| 'comment_on_contract_you_follow'
| 'answer_on_contract_you_follow'
| 'update_on_contract_you_follow'
| 'resolution_on_contract_you_follow'
| 'comment_on_contract_with_users_shares_in'
| 'answer_on_contract_with_users_shares_in'
| 'update_on_contract_with_users_shares_in'
| 'resolution_on_contract_with_users_shares_in'
| 'comment_on_contract_with_users_answer'
| 'update_on_contract_with_users_answer'
| 'resolution_on_contract_with_users_answer'
| 'answer_on_contract_with_users_answer'
| 'comment_on_contract_with_users_comment'
| 'answer_on_contract_with_users_comment'
| 'update_on_contract_with_users_comment'
| 'resolution_on_contract_with_users_comment'
| 'reply_to_users_answer'
| 'reply_to_users_comment'
| 'your_contract_closed'
| 'subsidized_your_market'
type notification_descriptions = {
[key in notification_preference]: {
simple: string
detailed: string
necessary?: boolean
}
}
export const NOTIFICATION_DESCRIPTIONS: notification_descriptions = {
all_answers_on_my_markets: {
simple: 'Answers on your markets',
detailed: 'Answers on your own markets',
},
all_comments_on_my_markets: {
simple: 'Comments on your markets',
detailed: 'Comments on your own markets',
},
answers_by_followed_users_on_watched_markets: {
simple: 'Only answers by users you follow',
detailed: "Only answers by users you follow on markets you're watching",
},
answers_by_market_creator_on_watched_markets: {
simple: 'Only answers by market creator',
detailed: "Only answers by market creator on markets you're watching",
},
betting_streaks: {
simple: `For prediction streaks`,
detailed: `Bonuses for predictions made over consecutive days (Prediction streaks)})`,
},
comments_by_followed_users_on_watched_markets: {
simple: 'Only comments by users you follow',
detailed:
'Only comments by users that you follow on markets that you watch',
},
contract_from_followed_user: {
simple: 'New markets from users you follow',
detailed: 'New markets from users you follow',
},
limit_order_fills: {
simple: 'Limit order fills',
detailed: 'When your limit order is filled by another user',
},
loan_income: {
simple: 'Automatic loans from your predictions in unresolved markets',
detailed:
'Automatic loans from your predictions that are locked in unresolved markets',
},
market_updates_on_watched_markets: {
simple: 'All creator updates',
detailed: 'All market updates made by the creator',
},
market_updates_on_watched_markets_with_shares_in: {
simple: "Only creator updates on markets that you're invested in",
detailed:
"Only updates made by the creator on markets that you're invested in",
},
on_new_follow: {
simple: 'A user followed you',
detailed: 'A user followed you',
},
onboarding_flow: {
simple: 'Emails to help you get started using Manifold',
detailed: 'Emails to help you learn how to use Manifold',
},
probability_updates_on_watched_markets: {
simple: 'Large changes in probability on markets that you watch',
detailed: 'Large changes in probability on markets that you watch',
},
profit_loss_updates: {
simple: 'Weekly portfolio updates',
detailed: 'Weekly portfolio updates',
},
referral_bonuses: {
simple: 'For referring new users',
detailed: 'Bonuses you receive from referring a new user',
},
resolutions_on_watched_markets: {
simple: 'All market resolutions',
detailed: "All resolutions on markets that you're watching",
},
resolutions_on_watched_markets_with_shares_in: {
simple: "Only market resolutions that you're invested in",
detailed:
"Only resolutions of markets you're watching and that you're invested in",
},
subsidized_your_market: {
simple: 'Your market was subsidized',
detailed: 'When someone subsidizes your market',
},
tagged_user: {
simple: 'A user tagged you',
detailed: 'When another use tags you',
},
thank_you_for_purchases: {
simple: 'Thank you notes for your purchases',
detailed: 'Thank you notes for your purchases',
},
tipped_comments_on_watched_markets: {
simple: 'Only highly tipped comments on markets that you watch',
detailed: 'Only highly tipped comments on markets that you watch',
},
tips_on_your_comments: {
simple: 'Tips on your comments',
detailed: 'Tips on your comments',
},
tips_on_your_markets: {
simple: 'Tips/Likes on your markets',
detailed: 'Tips/Likes on your markets',
},
trending_markets: {
simple: 'Weekly interesting markets',
detailed: 'Weekly interesting markets',
},
unique_bettors_on_your_contract: {
simple: 'For unique predictors on your markets',
detailed: 'Bonuses for unique predictors on your markets',
},
your_contract_closed: {
simple: 'Your market has closed and you need to resolve it (necessary)',
detailed: 'Your market has closed and you need to resolve it (necessary)',
necessary: true,
},
all_comments_on_watched_markets: {
simple: 'All new comments',
detailed: 'All new comments on markets you follow',
},
all_comments_on_contracts_with_shares_in_on_watched_markets: {
simple: `Only on markets you're invested in`,
detailed: `Comments on markets that you're watching and you're invested in`,
},
all_replies_to_my_comments_on_watched_markets: {
simple: 'Only replies to your comments',
detailed: "Only replies to your comments on markets you're watching",
},
all_replies_to_my_answers_on_watched_markets: {
simple: 'Only replies to your answers',
detailed: "Only replies to your answers on markets you're watching",
},
all_answers_on_watched_markets: {
simple: 'All new answers',
detailed: "All new answers on markets you're watching",
},
all_answers_on_contracts_with_shares_in_on_watched_markets: {
simple: `Only on markets you're invested in`,
detailed: `Answers on markets that you're watching and that you're invested in`,
},
badges_awarded: {
simple: 'New badges awarded',
detailed: 'New badges you have earned',
},
opt_out_all: {
simple: 'Opt out of all notifications (excludes when your markets close)',
detailed:
'Opt out of all notifications excluding your own market closure notifications',
},
}
export type BettingStreakData = {
streak: number
bonusAmount: number
}
export type BetFillData = {
betOutcome: string
creatorOutcome: string
probability: number
fillAmount: number
limitOrderTotal?: number
limitOrderRemaining?: number
}
export type ContractResolutionData = {
outcome: string
userPayout: number
userInvestment: number
}

View File

@ -2,19 +2,9 @@
"name": "common", "name": "common",
"version": "1.0.0", "version": "1.0.0",
"private": true, "private": true,
"scripts": { "scripts": {},
"verify": "(cd .. && yarn verify)",
"verify:dir": "npx eslint . --max-warnings 0"
},
"sideEffects": false, "sideEffects": false,
"dependencies": { "dependencies": {
"@tiptap/core": "2.0.0-beta.199",
"@tiptap/extension-image": "2.0.0-beta.199",
"@tiptap/extension-link": "2.0.0-beta.199",
"@tiptap/extension-mention": "2.0.0-beta.199",
"@tiptap/html": "2.0.0-beta.199",
"@tiptap/starter-kit": "2.0.0-beta.199",
"@tiptap/suggestion": "2.0.0-beta.199",
"lodash": "4.17.21" "lodash": "4.17.21"
}, },
"devDependencies": { "devDependencies": {

View File

@ -2,17 +2,14 @@ import { sum, groupBy, sumBy, mapValues } from 'lodash'
import { Bet, NumericBet } from './bet' import { Bet, NumericBet } from './bet'
import { deductDpmFees, getDpmProbability } from './calculate-dpm' import { deductDpmFees, getDpmProbability } from './calculate-dpm'
import { import { DPMContract, FreeResponseContract } from './contract'
DPMContract,
FreeResponseContract,
MultipleChoiceContract,
} from './contract'
import { DPM_CREATOR_FEE, DPM_FEES, DPM_PLATFORM_FEE } from './fees' import { DPM_CREATOR_FEE, DPM_FEES, DPM_PLATFORM_FEE } from './fees'
import { addObjects } from './util/object' import { addObjects } from './util/object'
export const getDpmCancelPayouts = (contract: DPMContract, bets: Bet[]) => { export const getDpmCancelPayouts = (contract: DPMContract, bets: Bet[]) => {
const { pool } = contract const { pool } = contract
const poolTotal = sum(Object.values(pool)) const poolTotal = sum(Object.values(pool))
console.log('resolved N/A, pool M$', poolTotal)
const betSum = sumBy(bets, (b) => b.amount) const betSum = sumBy(bets, (b) => b.amount)
@ -57,6 +54,17 @@ export const getDpmStandardPayouts = (
liquidityFee: 0, liquidityFee: 0,
}) })
console.log(
'resolved',
outcome,
'pool',
poolTotal,
'profits',
profits,
'creator fee',
creatorFee
)
return { return {
payouts: payouts.map(({ userId, payout }) => ({ userId, payout })), payouts: payouts.map(({ userId, payout }) => ({ userId, payout })),
creatorPayout: creatorFee, creatorPayout: creatorFee,
@ -98,6 +106,17 @@ export const getNumericDpmPayouts = (
liquidityFee: 0, liquidityFee: 0,
}) })
console.log(
'resolved numeric bucket: ',
outcome,
'pool',
poolTotal,
'profits',
profits,
'creator fee',
creatorFee
)
return { return {
payouts: payouts.map(({ userId, payout }) => ({ userId, payout })), payouts: payouts.map(({ userId, payout }) => ({ userId, payout })),
creatorPayout: creatorFee, creatorPayout: creatorFee,
@ -140,6 +159,17 @@ export const getDpmMktPayouts = (
liquidityFee: 0, liquidityFee: 0,
}) })
console.log(
'resolved MKT',
p,
'pool',
pool,
'profits',
profits,
'creator fee',
creatorFee
)
return { return {
payouts: payouts.map(({ userId, payout }) => ({ userId, payout })), payouts: payouts.map(({ userId, payout }) => ({ userId, payout })),
creatorPayout: creatorFee, creatorPayout: creatorFee,
@ -150,7 +180,7 @@ export const getDpmMktPayouts = (
export const getPayoutsMultiOutcome = ( export const getPayoutsMultiOutcome = (
resolutions: { [outcome: string]: number }, resolutions: { [outcome: string]: number },
contract: FreeResponseContract | MultipleChoiceContract, contract: FreeResponseContract,
bets: Bet[] bets: Bet[]
) => { ) => {
const poolTotal = sum(Object.values(contract.pool)) const poolTotal = sum(Object.values(contract.pool))
@ -168,7 +198,7 @@ export const getPayoutsMultiOutcome = (
const winnings = (shares / sharesByOutcome[outcome]) * prob * poolTotal const winnings = (shares / sharesByOutcome[outcome]) * prob * poolTotal
const profit = winnings - amount const profit = winnings - amount
const payout = amount + (1 - DPM_FEES) * profit const payout = amount + (1 - DPM_FEES) * Math.max(0, profit)
return { userId, profit, payout } return { userId, profit, payout }
}) })
@ -182,6 +212,16 @@ export const getPayoutsMultiOutcome = (
liquidityFee: 0, liquidityFee: 0,
}) })
console.log(
'resolved',
resolutions,
'pool',
poolTotal,
'profits',
profits,
'creator fee',
creatorFee
)
return { return {
payouts: payouts.map(({ userId, payout }) => ({ userId, payout })), payouts: payouts.map(({ userId, payout }) => ({ userId, payout })),
creatorPayout: creatorFee, creatorPayout: creatorFee,

View File

@ -1,3 +1,5 @@
import { sum } from 'lodash'
import { Bet } from './bet' import { Bet } from './bet'
import { getProbability } from './calculate' import { getProbability } from './calculate'
import { getCpmmLiquidityPoolWeights } from './calculate-cpmm' import { getCpmmLiquidityPoolWeights } from './calculate-cpmm'
@ -41,6 +43,18 @@ export const getStandardFixedPayouts = (
const { collectedFees } = contract const { collectedFees } = contract
const creatorPayout = collectedFees.creatorFee const creatorPayout = collectedFees.creatorFee
console.log(
'resolved',
outcome,
'pool',
contract.pool[outcome],
'payouts',
sum(payouts),
'creator fee',
creatorPayout
)
const liquidityPayouts = getLiquidityPoolPayouts( const liquidityPayouts = getLiquidityPoolPayouts(
contract, contract,
outcome, outcome,
@ -55,11 +69,10 @@ export const getLiquidityPoolPayouts = (
outcome: string, outcome: string,
liquidities: LiquidityProvision[] liquidities: LiquidityProvision[]
) => { ) => {
const { pool, subsidyPool } = contract const { pool } = contract
const finalPool = pool[outcome] + (subsidyPool ?? 0) const finalPool = pool[outcome]
if (finalPool < 1e-3) return []
const weights = getCpmmLiquidityPoolWeights(liquidities) const weights = getCpmmLiquidityPoolWeights(contract, liquidities)
return Object.entries(weights).map(([providerId, weight]) => ({ return Object.entries(weights).map(([providerId, weight]) => ({
userId: providerId, userId: providerId,
@ -85,6 +98,18 @@ export const getMktFixedPayouts = (
const { collectedFees } = contract const { collectedFees } = contract
const creatorPayout = collectedFees.creatorFee const creatorPayout = collectedFees.creatorFee
console.log(
'resolved PROB',
p,
'pool',
p * contract.pool.YES + (1 - p) * contract.pool.NO,
'payouts',
sum(payouts),
'creator fee',
creatorPayout
)
const liquidityPayouts = getLiquidityPoolProbPayouts(contract, p, liquidities) const liquidityPayouts = getLiquidityPoolProbPayouts(contract, p, liquidities)
return { payouts, creatorPayout, liquidityPayouts, collectedFees } return { payouts, creatorPayout, liquidityPayouts, collectedFees }
@ -95,11 +120,10 @@ export const getLiquidityPoolProbPayouts = (
p: number, p: number,
liquidities: LiquidityProvision[] liquidities: LiquidityProvision[]
) => { ) => {
const { pool, subsidyPool } = contract const { pool } = contract
const finalPool = p * pool.YES + (1 - p) * pool.NO + (subsidyPool ?? 0) const finalPool = p * pool.YES + (1 - p) * pool.NO
if (finalPool < 1e-3) return []
const weights = getCpmmLiquidityPoolWeights(liquidities) const weights = getCpmmLiquidityPoolWeights(contract, liquidities)
return Object.entries(weights).map(([providerId, weight]) => ({ return Object.entries(weights).map(([providerId, weight]) => ({
userId: providerId, userId: providerId,

View File

@ -1,12 +1,7 @@
import { sumBy, groupBy, mapValues } from 'lodash' import { sumBy, groupBy, mapValues } from 'lodash'
import { Bet, NumericBet } from './bet' import { Bet, NumericBet } from './bet'
import { import { Contract, CPMMBinaryContract, DPMContract } from './contract'
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 {
@ -53,19 +48,15 @@ export type PayoutInfo = {
export const getPayouts = ( export const getPayouts = (
outcome: string | undefined, outcome: string | undefined,
resolutions: {
[outcome: string]: number
},
contract: Contract, contract: Contract,
bets: Bet[], bets: Bet[],
liquidities: LiquidityProvision[], liquidities: LiquidityProvision[],
resolutions?: {
[outcome: string]: number
},
resolutionProbability?: number resolutionProbability?: number
): PayoutInfo => { ): PayoutInfo => {
if ( if (contract.mechanism === 'cpmm-1' && contract.outcomeType === 'BINARY') {
contract.mechanism === 'cpmm-1' &&
(contract.outcomeType === 'BINARY' ||
contract.outcomeType === 'PSEUDO_NUMERIC')
) {
return getFixedPayouts( return getFixedPayouts(
outcome, outcome,
contract, contract,
@ -76,16 +67,16 @@ export const getPayouts = (
} }
return getDpmPayouts( return getDpmPayouts(
outcome, outcome,
resolutions,
contract, contract,
bets, bets,
resolutions,
resolutionProbability resolutionProbability
) )
} }
export const getFixedPayouts = ( export const getFixedPayouts = (
outcome: string | undefined, outcome: string | undefined,
contract: CPMMBinaryContract | PseudoNumericContract, contract: CPMMBinaryContract,
bets: Bet[], bets: Bet[],
liquidities: LiquidityProvision[], liquidities: LiquidityProvision[],
resolutionProbability?: number resolutionProbability?: number
@ -109,15 +100,14 @@ export const getFixedPayouts = (
export const getDpmPayouts = ( export const getDpmPayouts = (
outcome: string | undefined, outcome: string | undefined,
contract: DPMContract, resolutions: {
bets: Bet[],
resolutions?: {
[outcome: string]: number [outcome: string]: number
}, },
contract: DPMContract,
bets: Bet[],
resolutionProbability?: number resolutionProbability?: number
): PayoutInfo => { ): PayoutInfo => {
const openBets = bets.filter((b) => !b.isSold && !b.sale) const openBets = bets.filter((b) => !b.isSold && !b.sale)
const { outcomeType } = contract
switch (outcome) { switch (outcome) {
case 'YES': case 'YES':
@ -125,16 +115,15 @@ export const getDpmPayouts = (
return getDpmStandardPayouts(outcome, contract, openBets) return getDpmStandardPayouts(outcome, contract, openBets)
case 'MKT': case 'MKT':
return outcomeType === 'FREE_RESPONSE' || return contract.outcomeType === 'FREE_RESPONSE'
outcomeType === 'MULTIPLE_CHOICE' // eslint-disable-next-line @typescript-eslint/no-non-null-assertion ? getPayoutsMultiOutcome(resolutions, contract, openBets)
? getPayoutsMultiOutcome(resolutions!, contract, openBets)
: getDpmMktPayouts(contract, openBets, resolutionProbability) : getDpmMktPayouts(contract, openBets, resolutionProbability)
case 'CANCEL': case 'CANCEL':
case undefined: case undefined:
return getDpmCancelPayouts(contract, openBets) return getDpmCancelPayouts(contract, openBets)
default: default:
if (outcomeType === 'NUMERIC') if (contract.outcomeType === 'NUMERIC')
return getNumericDpmPayouts(outcome, contract, openBets as NumericBet[]) return getNumericDpmPayouts(outcome, contract, openBets as NumericBet[])
// Outcome is a free response answer id. // Outcome is a free response answer id.

View File

@ -1,29 +0,0 @@
import { JSONContent } from '@tiptap/core'
export type Post = {
id: string
title: string
subtitle: string
content: JSONContent
creatorId: string // User id
createdTime: number
slug: string
// denormalized user fields
creatorName: string
creatorUsername: string
creatorAvatarUrl?: string
likedByUserIds?: string[]
likedByUserCount?: number
}
export type DateDoc = Post & {
bounty: number
birthday: number
type: 'date-doc'
contractSlug: string
}
export const MAX_POST_TITLE_LENGTH = 480
export const MAX_POST_SUBTITLE_LENGTH = 480

View File

@ -1,48 +0,0 @@
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 + 1)
return 10 ** logValue + min - 1
}
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 (value < min) return 0
if (value > max) return 1
if (isLogScale) {
return Math.log10(value - min + 1) / Math.log10(max - min + 1)
}
return (value - min) / (max - min)
}

View File

@ -1,27 +0,0 @@
import { groupBy, mapValues, sum, sumBy } from 'lodash'
import { Txn } from './txn'
// Returns a map of charity ids to the amount of M$ matched
export function quadraticMatches(
allCharityTxns: Txn[],
matchingPool: number
): Record<string, number> {
// For each charity, group the donations by each individual donor
const donationsByCharity = groupBy(allCharityTxns, 'toId')
const donationsByDonors = mapValues(donationsByCharity, (txns) =>
groupBy(txns, 'fromId')
)
// Weight for each charity = [sum of sqrt(individual donor)] ^ 2
const weights = mapValues(donationsByDonors, (byDonor) => {
const sumByDonor = Object.values(byDonor).map((txns) =>
sumBy(txns, 'amount')
)
const sumOfRoots = sumBy(sumByDonor, Math.sqrt)
return sumOfRoots ** 2
})
// Then distribute the matching pool based on the individual weights
const totalWeight = sum(Object.values(weights))
return mapValues(weights, (weight) => matchingPool * (weight / totalWeight))
}

View File

@ -0,0 +1,187 @@
import { union, sum, sumBy, sortBy, groupBy, mapValues } from 'lodash'
import { Bet } from './bet'
import { Contract } from './contract'
import { ClickEvent } from './tracking'
import { filterDefined } from './util/array'
import { addObjects } from './util/object'
export const MAX_FEED_CONTRACTS = 75
export const getRecommendedContracts = (
contractsById: { [contractId: string]: Contract },
yourBetOnContractIds: string[]
) => {
const contracts = Object.values(contractsById)
const yourContracts = filterDefined(
yourBetOnContractIds.map((contractId) => contractsById[contractId])
)
const yourContractIds = new Set(yourContracts.map((c) => c.id))
const notYourContracts = contracts.filter((c) => !yourContractIds.has(c.id))
const yourWordFrequency = contractsToWordFrequency(yourContracts)
const otherWordFrequency = contractsToWordFrequency(notYourContracts)
const words = union(
Object.keys(yourWordFrequency),
Object.keys(otherWordFrequency)
)
const yourWeightedFrequency = Object.fromEntries(
words.map((word) => {
const [yourFreq, otherFreq] = [
yourWordFrequency[word] ?? 0,
otherWordFrequency[word] ?? 0,
]
const score = yourFreq / (yourFreq + otherFreq + 0.0001)
return [word, score]
})
)
// console.log(
// 'your weighted frequency',
// _.sortBy(_.toPairs(yourWeightedFrequency), ([, freq]) => -freq)
// )
const scoredContracts = contracts.map((contract) => {
const wordFrequency = contractToWordFrequency(contract)
const score = sumBy(Object.keys(wordFrequency), (word) => {
const wordFreq = wordFrequency[word] ?? 0
const weight = yourWeightedFrequency[word] ?? 0
return wordFreq * weight
})
return {
contract,
score,
}
})
return sortBy(scoredContracts, (scored) => -scored.score).map(
(scored) => scored.contract
)
}
const contractToText = (contract: Contract) => {
const { description, question, tags, creatorUsername } = contract
return `${creatorUsername} ${question} ${tags.join(' ')} ${description}`
}
const MAX_CHARS_IN_WORD = 100
const getWordsCount = (text: string) => {
const normalizedText = text.replace(/[^a-zA-Z]/g, ' ').toLowerCase()
const words = normalizedText
.split(' ')
.filter((word) => word)
.filter((word) => word.length <= MAX_CHARS_IN_WORD)
const counts: { [word: string]: number } = {}
for (const word of words) {
if (counts[word]) counts[word]++
else counts[word] = 1
}
return counts
}
const toFrequency = (counts: { [word: string]: number }) => {
const total = sum(Object.values(counts))
return mapValues(counts, (count) => count / total)
}
const contractToWordFrequency = (contract: Contract) =>
toFrequency(getWordsCount(contractToText(contract)))
const contractsToWordFrequency = (contracts: Contract[]) => {
const frequencySum = contracts
.map(contractToWordFrequency)
.reduce(addObjects, {})
return toFrequency(frequencySum)
}
export const getWordScores = (
contracts: Contract[],
contractViewCounts: { [contractId: string]: number },
clicks: ClickEvent[],
bets: Bet[]
) => {
const contractClicks = groupBy(clicks, (click) => click.contractId)
const contractBets = groupBy(bets, (bet) => bet.contractId)
const yourContracts = contracts.filter(
(c) =>
contractViewCounts[c.id] || contractClicks[c.id] || contractBets[c.id]
)
const yourTfIdf = calculateContractTfIdf(yourContracts)
const contractWordScores = mapValues(yourTfIdf, (wordsTfIdf, contractId) => {
const viewCount = contractViewCounts[contractId] ?? 0
const clickCount = contractClicks[contractId]?.length ?? 0
const betCount = contractBets[contractId]?.length ?? 0
const factor =
-1 * Math.log(viewCount + 1) +
10 * Math.log(betCount + clickCount / 4 + 1)
return mapValues(wordsTfIdf, (tfIdf) => tfIdf * factor)
})
const wordScores = Object.values(contractWordScores).reduce(addObjects, {})
const minScore = Math.min(...Object.values(wordScores))
const maxScore = Math.max(...Object.values(wordScores))
const normalizedWordScores = mapValues(
wordScores,
(score) => (score - minScore) / (maxScore - minScore)
)
// console.log(
// 'your word scores',
// _.sortBy(_.toPairs(normalizedWordScores), ([, score]) => -score).slice(0, 100),
// _.sortBy(_.toPairs(normalizedWordScores), ([, score]) => -score).slice(-100)
// )
return normalizedWordScores
}
export function getContractScore(
contract: Contract,
wordScores: { [word: string]: number }
) {
if (Object.keys(wordScores).length === 0) return 1
const wordFrequency = contractToWordFrequency(contract)
const score = sumBy(Object.keys(wordFrequency), (word) => {
const wordFreq = wordFrequency[word] ?? 0
const weight = wordScores[word] ?? 0
return wordFreq * weight
})
return score
}
// Caluculate Term Frequency-Inverse Document Frequency (TF-IDF):
// https://medium.datadriveninvestor.com/tf-idf-in-natural-language-processing-8db8ef4a7736
function calculateContractTfIdf(contracts: Contract[]) {
const contractFreq = contracts.map((c) => contractToWordFrequency(c))
const contractWords = contractFreq.map((freq) => Object.keys(freq))
const wordsCount: { [word: string]: number } = {}
for (const words of contractWords) {
for (const word of words) {
wordsCount[word] = (wordsCount[word] ?? 0) + 1
}
}
const wordIdf = mapValues(wordsCount, (count) =>
Math.log(contracts.length / count)
)
const contractWordsTfIdf = contractFreq.map((wordFreq) =>
mapValues(wordFreq, (freq, word) => freq * wordIdf[word])
)
return Object.fromEntries(
contracts.map((c, i) => [c.id, contractWordsTfIdf[i]])
)
}

View File

@ -1,58 +0,0 @@
import { partition, sumBy } from 'lodash'
import { Bet } from './bet'
import { getProbability } from './calculate'
import { CPMMContract } from './contract'
import { noFees } from './fees'
import { CandidateBet } from './new-bet'
type RedeemableBet = Pick<Bet, 'outcome' | 'shares' | 'loanAmount'>
export const getRedeemableAmount = (bets: RedeemableBet[]) => {
const [yesBets, noBets] = partition(bets, (b) => b.outcome === 'YES')
const yesShares = sumBy(yesBets, (b) => b.shares)
const noShares = sumBy(noBets, (b) => b.shares)
const shares = Math.max(Math.min(yesShares, noShares), 0)
const soldFrac =
shares > 0
? Math.min(yesShares, noShares) / Math.max(yesShares, noShares)
: 0
const loanAmount = sumBy(bets, (bet) => bet.loanAmount ?? 0)
const loanPayment = loanAmount * soldFrac
const netAmount = shares - loanPayment
return { shares, loanPayment, netAmount }
}
export const getRedemptionBets = (
shares: number,
loanPayment: number,
contract: CPMMContract
) => {
const p = getProbability(contract)
const createdTime = Date.now()
const yesBet: CandidateBet = {
contractId: contract.id,
amount: p * -shares,
shares: -shares,
loanAmount: loanPayment ? -loanPayment / 2 : 0,
outcome: 'YES',
probBefore: p,
probAfter: p,
createdTime,
isRedemption: true,
fees: noFees,
}
const noBet: CandidateBet = {
contractId: contract.id,
amount: (1 - p) * -shares,
shares: -shares,
loanAmount: loanPayment ? -loanPayment / 2 : 0,
outcome: 'NO',
probBefore: p,
probAfter: p,
createdTime,
isRedemption: true,
fees: noFees,
}
return [yesBet, noBet]
}

View File

@ -1,19 +1,13 @@
import { groupBy, sumBy, mapValues, keyBy, sortBy } from 'lodash' import { groupBy, sumBy, mapValues, partition } from 'lodash'
import { Bet } from './bet' import { Bet } from './bet'
import { getContractBetMetrics, resolvedPayout } from './calculate'
import { Contract } from './contract' import { Contract } from './contract'
import { ContractComment } from './comment' import { getPayouts } from './payouts'
export function scoreCreators(contracts: Contract[]) { export function scoreCreators(contracts: Contract[]) {
const creatorScore = mapValues( const creatorScore = mapValues(
groupBy(contracts, ({ creatorId }) => creatorId), groupBy(contracts, ({ creatorId }) => creatorId),
(contracts) => (contracts) => sumBy(contracts, ({ pool }) => pool.YES + pool.NO)
sumBy(
contracts.map((contract) => {
return contract.volume
})
)
) )
return creatorScore return creatorScore
@ -31,11 +25,46 @@ export function scoreTraders(contracts: Contract[], bets: Bet[][]) {
} }
export function scoreUsersByContract(contract: Contract, bets: Bet[]) { export function scoreUsersByContract(contract: Contract, bets: Bet[]) {
const betsByUser = groupBy(bets, (bet) => bet.userId) const { resolution } = contract
return mapValues( const resolutionProb =
betsByUser, contract.outcomeType == 'BINARY'
(bets) => getContractBetMetrics(contract, bets).profit ? contract.resolutionProbability
: undefined
const [closedBets, openBets] = partition(
bets,
(bet) => bet.isSold || bet.sale
) )
const { payouts: resolvePayouts } = getPayouts(
resolution as string,
{},
contract,
openBets,
[],
resolutionProb
)
const salePayouts = closedBets.map((bet) => {
const { userId, sale } = bet
return { userId, payout: sale ? sale.amount : 0 }
})
const investments = bets
.filter((bet) => !bet.sale)
.map((bet) => {
const { userId, amount, loanAmount } = bet
const payout = -amount - (loanAmount ?? 0)
return { userId, payout }
})
const netPayouts = [...resolvePayouts, ...salePayouts, ...investments]
const userScore = mapValues(
groupBy(netPayouts, (payout) => payout.userId),
(payouts) => sumBy(payouts, ({ payout }) => payout)
)
return userScore
} }
export function addUserScores( export function addUserScores(
@ -47,47 +76,3 @@ export function addUserScores(
dest[userId] += score dest[userId] += score
} }
} }
export function scoreCommentorsAndBettors(
contract: Contract,
bets: Bet[],
comments: ContractComment[]
) {
const commentsById = keyBy(comments, 'id')
const betsById = keyBy(bets, 'id')
// If 'id2' is the sale of 'id1', both are logged with (id2 - id1) of profit
// Otherwise, we record the profit at resolution time
const profitById: Record<string, number> = {}
for (const bet of bets) {
if (bet.sale) {
const originalBet = betsById[bet.sale.betId]
const profit = bet.sale.amount - originalBet.amount
profitById[bet.id] = profit
profitById[originalBet.id] = profit
} else {
profitById[bet.id] = resolvedPayout(contract, bet) - bet.amount
}
}
// Now find the betId with the highest profit
const topBetId = sortBy(bets, (b) => -profitById[b.id])[0]?.id
const topBettor = betsById[topBetId]?.userName
// And also the commentId of the comment with the highest profit
const topCommentId = sortBy(
comments,
(c) => c.betId && -profitById[c.betId]
)[0]?.id
const topCommentBetId = commentsById[topCommentId]?.betId
return {
topCommentId,
topBetId,
topBettor,
profitById,
commentsById,
betsById,
topCommentBetId,
}
}

View File

@ -1,20 +1,20 @@
import { Bet, LimitBet } from './bet' import { Bet } from './bet'
import { import {
getDpmProbability,
calculateDpmShareValue, calculateDpmShareValue,
deductDpmFees, deductDpmFees,
getDpmOutcomeProbability,
} from './calculate-dpm' } from './calculate-dpm'
import { calculateCpmmSale, getCpmmProbability } from './calculate-cpmm' import { calculateCpmmSale, getCpmmProbability } from './calculate-cpmm'
import { CPMMContract, DPMContract } from './contract' import { CPMMContract, DPMContract } from './contract'
import { DPM_CREATOR_FEE, DPM_PLATFORM_FEE, Fees } from './fees' import { DPM_CREATOR_FEE, DPM_PLATFORM_FEE, Fees } from './fees'
import { sumBy } from 'lodash' import { User } from './user'
export type CandidateBet<T extends Bet> = Omit< export const getSellBetInfo = (
T, user: User,
'id' | 'userId' | 'userAvatarUrl' | 'userName' | 'userUsername' bet: Bet,
> contract: DPMContract,
newBetId: string
export const getSellBetInfo = (bet: Bet, contract: DPMContract) => { ) => {
const { pool, totalShares, totalBets } = contract const { pool, totalShares, totalBets } = contract
const { id: betId, amount, shares, outcome, loanAmount } = bet const { id: betId, amount, shares, outcome, loanAmount } = bet
@ -29,8 +29,8 @@ export const getSellBetInfo = (bet: Bet, contract: DPMContract) => {
const newTotalBets = { ...totalBets, [outcome]: totalBets[outcome] - amount } const newTotalBets = { ...totalBets, [outcome]: totalBets[outcome] - amount }
const probBefore = getDpmOutcomeProbability(totalShares, outcome) const probBefore = getDpmProbability(totalShares)
const probAfter = getDpmOutcomeProbability(newTotalShares, outcome) const probAfter = getDpmProbability(newTotalShares)
const profit = adjShareValue - amount const profit = adjShareValue - amount
@ -54,7 +54,9 @@ export const getSellBetInfo = (bet: Bet, contract: DPMContract) => {
creatorFee creatorFee
) )
const newBet: CandidateBet<Bet> = { const newBet: Bet = {
id: newBetId,
userId: user.id,
contractId: contract.id, contractId: contract.id,
amount: -adjShareValue, amount: -adjShareValue,
shares: -shares, shares: -shares,
@ -67,41 +69,41 @@ export const getSellBetInfo = (bet: Bet, contract: DPMContract) => {
betId, betId,
}, },
fees, fees,
loanAmount: -(loanAmount ?? 0),
} }
const newBalance = user.balance + saleAmount - (loanAmount ?? 0)
return { return {
newBet, newBet,
newPool, newPool,
newTotalShares, newTotalShares,
newTotalBets, newTotalBets,
newBalance,
fees, fees,
} }
} }
export const getCpmmSellBetInfo = ( export const getCpmmSellBetInfo = (
user: User,
shares: number, shares: number,
outcome: 'YES' | 'NO', outcome: 'YES' | 'NO',
contract: CPMMContract, contract: CPMMContract,
unfilledBets: LimitBet[], prevLoanAmount: number,
balanceByUserId: { [userId: string]: number }, newBetId: string
loanPaid: number
) => { ) => {
const { pool, p } = contract const { pool, p } = contract
const { saleValue, cpmmState, fees, makers, takers, ordersToCancel } = calculateCpmmSale( const { saleValue, newPool, newP, fees } = calculateCpmmSale(
contract, contract,
shares, shares,
outcome, outcome
unfilledBets,
balanceByUserId,
) )
const probBefore = getCpmmProbability(pool, p) const loanPaid = Math.min(prevLoanAmount, saleValue)
const probAfter = getCpmmProbability(cpmmState.pool, cpmmState.p) const netAmount = saleValue - loanPaid
const takerAmount = sumBy(takers, 'amount') const probBefore = getCpmmProbability(pool, p)
const takerShares = sumBy(takers, 'shares') const probAfter = getCpmmProbability(newPool, p)
console.log( console.log(
'SELL M$', 'SELL M$',
@ -113,29 +115,27 @@ export const getCpmmSellBetInfo = (
fees.creatorFee fees.creatorFee
) )
const newBet: CandidateBet<Bet> = { const newBet: Bet = {
id: newBetId,
userId: user.id,
contractId: contract.id, contractId: contract.id,
amount: takerAmount, amount: -saleValue,
shares: takerShares, shares: -shares,
outcome, outcome,
probBefore, probBefore,
probAfter, probAfter,
createdTime: Date.now(), createdTime: Date.now(),
loanAmount: -loanPaid, loanAmount: -loanPaid,
fees, fees,
fills: takers,
isFilled: true,
isCancelled: false,
orderAmount: takerAmount,
} }
const newBalance = user.balance + netAmount
return { return {
newBet, newBet,
newPool: cpmmState.pool, newPool,
newP: cpmmState.p, newP,
newBalance,
fees, fees,
makers,
takers,
ordersToCancel
} }
} }

View File

@ -1,25 +0,0 @@
export type Stats = {
startDate: number
dailyActiveUsers: number[]
dailyActiveUsersWeeklyAvg: number[]
weeklyActiveUsers: number[]
monthlyActiveUsers: number[]
d1: number[]
d1WeeklyAvg: number[]
nd1: number[]
nd1WeeklyAvg: number[]
nw1: number[]
dailyBetCounts: number[]
dailyContractCounts: number[]
dailyCommentCounts: number[]
dailySignups: number[]
weekOnWeekRetention: number[]
monthlyRetention: number[]
dailyActivationRate: number[]
dailyActivationRateWeeklyAvg: number[]
manaBet: {
daily: number[]
weekly: number[]
monthly: number[]
}
}

View File

@ -1,8 +1,6 @@
{ {
"compilerOptions": { "compilerOptions": {
"baseUrl": "../", "baseUrl": "../",
"composite": true,
"module": "commonjs",
"moduleResolution": "node", "moduleResolution": "node",
"noImplicitReturns": true, "noImplicitReturns": true,
"outDir": "lib", "outDir": "lib",

View File

@ -1,17 +1,6 @@
// A txn (pronounced "texan") respresents a payment between two ids on Manifold // A txn (pronounced "texan") respresents a payment between two ids on Manifold
// Shortened from "transaction" to distinguish from Firebase transactions (and save chars) // Shortened from "transaction" to distinguish from Firebase transactions (and save chars)
type AnyTxnType = export type Txn = {
| Donation
| Tip
| Manalink
| Referral
| UniqueBettorBonus
| BettingStreakBonus
| CancelUniqueBettorBonus
| CommentBountyRefund
type SourceType = 'USER' | 'CONTRACT' | 'CHARITY' | 'BANK'
export type Txn<T extends AnyTxnType = AnyTxnType> = {
id: string id: string
createdTime: number createdTime: number
@ -24,117 +13,9 @@ export type Txn<T extends AnyTxnType = AnyTxnType> = {
amount: number amount: number
token: 'M$' // | 'USD' | MarketOutcome token: 'M$' // | 'USD' | MarketOutcome
category: category: 'CHARITY' // | 'BET' | 'TIP'
| 'CHARITY'
| 'MANALINK'
| 'TIP'
| 'REFERRAL'
| 'UNIQUE_BETTOR_BONUS'
| 'BETTING_STREAK_BONUS'
| 'CANCEL_UNIQUE_BETTOR_BONUS'
| 'COMMENT_BOUNTY'
| 'REFUND_COMMENT_BOUNTY'
// Any extra data
data?: { [key: string]: any }
// Human-readable description // Human-readable description
description?: string description?: string
} & T
type Donation = {
fromType: 'USER'
toType: 'CHARITY'
category: 'CHARITY'
} }
type Tip = { export type SourceType = 'USER' | 'CONTRACT' | 'CHARITY' | 'BANK'
fromType: 'USER'
toType: 'USER'
category: 'TIP'
data: {
commentId: string
contractId?: string
groupId?: string
}
}
type Manalink = {
fromType: 'USER'
toType: 'USER'
category: 'MANALINK'
}
type Referral = {
fromType: 'BANK'
toType: 'USER'
category: 'REFERRAL'
}
type UniqueBettorBonus = {
fromType: 'BANK'
toType: 'USER'
category: 'UNIQUE_BETTOR_BONUS'
data: {
contractId: string
uniqueNewBettorId?: string
// Old unique bettor bonus txns stored all unique bettor ids
uniqueBettorIds?: string[]
}
}
type BettingStreakBonus = {
fromType: 'BANK'
toType: 'USER'
category: 'BETTING_STREAK_BONUS'
data: {
currentBettingStreak?: number
}
}
type CancelUniqueBettorBonus = {
fromType: 'USER'
toType: 'BANK'
category: 'CANCEL_UNIQUE_BETTOR_BONUS'
data: {
contractId: string
}
}
type CommentBountyDeposit = {
fromType: 'USER'
toType: 'BANK'
category: 'COMMENT_BOUNTY'
data: {
contractId: string
}
}
type CommentBountyWithdrawal = {
fromType: 'BANK'
toType: 'USER'
category: 'COMMENT_BOUNTY'
data: {
contractId: string
commentId: string
}
}
type CommentBountyRefund = {
fromType: 'BANK'
toType: 'USER'
category: 'REFUND_COMMENT_BOUNTY'
data: {
contractId: string
}
}
export type DonationTxn = Txn & Donation
export type TipTxn = Txn & Tip
export type ManalinkTxn = Txn & Manalink
export type ReferralTxn = Txn & Referral
export type BettingStreakBonusTxn = Txn & BettingStreakBonus
export type UniqueBettorBonusTxn = Txn & UniqueBettorBonus
export type CancelUniqueBettorBonusTxn = Txn & CancelUniqueBettorBonus
export type CommentBountyDepositTxn = Txn & CommentBountyDeposit
export type CommentBountyWithdrawalTxn = Txn & CommentBountyWithdrawal

View File

@ -1,222 +0,0 @@
import { filterDefined } from './util/array'
import { notification_reason_types } from './notification'
import { getFunctionUrl } from './api'
import { DOMAIN } from './envs/constants'
import { PrivateUser } from './user'
export type notification_destination_types = 'email' | 'browser'
export type notification_preference = keyof notification_preferences
export type notification_preferences = {
// Watched Markets
all_comments_on_watched_markets: notification_destination_types[]
all_answers_on_watched_markets: notification_destination_types[]
// Comments
tipped_comments_on_watched_markets: notification_destination_types[]
comments_by_followed_users_on_watched_markets: notification_destination_types[]
all_replies_to_my_comments_on_watched_markets: notification_destination_types[]
all_replies_to_my_answers_on_watched_markets: notification_destination_types[]
all_comments_on_contracts_with_shares_in_on_watched_markets: notification_destination_types[]
// Answers
answers_by_followed_users_on_watched_markets: notification_destination_types[]
answers_by_market_creator_on_watched_markets: notification_destination_types[]
all_answers_on_contracts_with_shares_in_on_watched_markets: notification_destination_types[]
// On users' markets
your_contract_closed: notification_destination_types[]
all_comments_on_my_markets: notification_destination_types[]
all_answers_on_my_markets: notification_destination_types[]
subsidized_your_market: notification_destination_types[]
// Market updates
resolutions_on_watched_markets: notification_destination_types[]
resolutions_on_watched_markets_with_shares_in: notification_destination_types[]
market_updates_on_watched_markets: notification_destination_types[]
market_updates_on_watched_markets_with_shares_in: notification_destination_types[]
probability_updates_on_watched_markets: notification_destination_types[]
// Balance Changes
loan_income: notification_destination_types[]
betting_streaks: notification_destination_types[]
referral_bonuses: notification_destination_types[]
unique_bettors_on_your_contract: notification_destination_types[]
tips_on_your_comments: notification_destination_types[]
tips_on_your_markets: notification_destination_types[]
limit_order_fills: notification_destination_types[]
// General
tagged_user: notification_destination_types[]
on_new_follow: notification_destination_types[]
contract_from_followed_user: notification_destination_types[]
trending_markets: notification_destination_types[]
profit_loss_updates: notification_destination_types[]
onboarding_flow: notification_destination_types[]
thank_you_for_purchases: notification_destination_types[]
badges_awarded: notification_destination_types[]
opt_out_all: notification_destination_types[]
// When adding a new notification preference, use add-new-notification-preference.ts to existing users
}
export const getDefaultNotificationPreferences = (
userId: string,
privateUser?: PrivateUser,
noEmails?: boolean
) => {
const constructPref = (browserIf: boolean, emailIf: boolean) => {
const browser = browserIf ? 'browser' : undefined
const email = noEmails ? undefined : emailIf ? 'email' : undefined
return filterDefined([browser, email]) as notification_destination_types[]
}
const defaults: notification_preferences = {
// Watched Markets
all_comments_on_watched_markets: constructPref(true, false),
all_answers_on_watched_markets: constructPref(true, false),
// Comments
tips_on_your_comments: constructPref(true, true),
comments_by_followed_users_on_watched_markets: constructPref(true, true),
all_replies_to_my_comments_on_watched_markets: constructPref(true, true),
all_replies_to_my_answers_on_watched_markets: constructPref(true, true),
all_comments_on_contracts_with_shares_in_on_watched_markets: constructPref(
true,
false
),
// Answers
answers_by_followed_users_on_watched_markets: constructPref(true, true),
answers_by_market_creator_on_watched_markets: constructPref(true, true),
all_answers_on_contracts_with_shares_in_on_watched_markets: constructPref(
true,
true
),
// On users' markets
your_contract_closed: constructPref(true, true), // High priority
all_comments_on_my_markets: constructPref(true, true),
all_answers_on_my_markets: constructPref(true, true),
subsidized_your_market: constructPref(true, true),
// Market updates
resolutions_on_watched_markets: constructPref(true, false),
market_updates_on_watched_markets: constructPref(true, false),
market_updates_on_watched_markets_with_shares_in: constructPref(
true,
false
),
resolutions_on_watched_markets_with_shares_in: constructPref(true, true),
//Balance Changes
loan_income: constructPref(true, false),
betting_streaks: constructPref(true, false),
referral_bonuses: constructPref(true, true),
unique_bettors_on_your_contract: constructPref(true, true),
tipped_comments_on_watched_markets: constructPref(true, true),
tips_on_your_markets: constructPref(true, true),
limit_order_fills: constructPref(true, false),
// General
tagged_user: constructPref(true, true),
on_new_follow: constructPref(true, true),
contract_from_followed_user: constructPref(true, true),
trending_markets: constructPref(false, true),
profit_loss_updates: constructPref(false, true),
probability_updates_on_watched_markets: constructPref(true, false),
thank_you_for_purchases: constructPref(false, false),
onboarding_flow: constructPref(false, false),
opt_out_all: [],
badges_awarded: constructPref(true, false),
}
return defaults
}
// Adding a new key:value here is optional, you can just use a key of notification_subscription_types
// You might want to add a key:value here if there will be multiple notification reasons that map to the same
// subscription type, i.e. 'comment_on_contract_you_follow' and 'comment_on_contract_with_users_answer' both map to
// 'all_comments_on_watched_markets' subscription type
// TODO: perhaps better would be to map notification_subscription_types to arrays of notification_reason_types
const notificationReasonToSubscriptionType: Partial<
Record<notification_reason_types, notification_preference>
> = {
you_referred_user: 'referral_bonuses',
user_joined_to_bet_on_your_market: 'referral_bonuses',
tip_received: 'tips_on_your_comments',
bet_fill: 'limit_order_fills',
user_joined_from_your_group_invite: 'referral_bonuses',
challenge_accepted: 'limit_order_fills',
betting_streak_incremented: 'betting_streaks',
liked_and_tipped_your_contract: 'tips_on_your_markets',
comment_on_your_contract: 'all_comments_on_my_markets',
answer_on_your_contract: 'all_answers_on_my_markets',
comment_on_contract_you_follow: 'all_comments_on_watched_markets',
answer_on_contract_you_follow: 'all_answers_on_watched_markets',
update_on_contract_you_follow: 'market_updates_on_watched_markets',
resolution_on_contract_you_follow: 'resolutions_on_watched_markets',
comment_on_contract_with_users_shares_in:
'all_comments_on_contracts_with_shares_in_on_watched_markets',
answer_on_contract_with_users_shares_in:
'all_answers_on_contracts_with_shares_in_on_watched_markets',
update_on_contract_with_users_shares_in:
'market_updates_on_watched_markets_with_shares_in',
resolution_on_contract_with_users_shares_in:
'resolutions_on_watched_markets_with_shares_in',
comment_on_contract_with_users_answer: 'all_comments_on_watched_markets',
update_on_contract_with_users_answer: 'market_updates_on_watched_markets',
resolution_on_contract_with_users_answer: 'resolutions_on_watched_markets',
answer_on_contract_with_users_answer: 'all_answers_on_watched_markets',
comment_on_contract_with_users_comment: 'all_comments_on_watched_markets',
answer_on_contract_with_users_comment: 'all_answers_on_watched_markets',
update_on_contract_with_users_comment: 'market_updates_on_watched_markets',
resolution_on_contract_with_users_comment: 'resolutions_on_watched_markets',
reply_to_users_answer: 'all_replies_to_my_answers_on_watched_markets',
reply_to_users_comment: 'all_replies_to_my_comments_on_watched_markets',
}
export const getNotificationDestinationsForUser = (
privateUser: PrivateUser,
// TODO: accept reasons array from most to least important and work backwards
reason: notification_reason_types | notification_preference
) => {
const notificationSettings = privateUser.notificationPreferences
const unsubscribeEndpoint = getFunctionUrl('unsubscribe')
try {
let destinations
let subscriptionType: notification_preference | undefined
if (Object.keys(notificationSettings).includes(reason)) {
subscriptionType = reason as notification_preference
destinations = notificationSettings[subscriptionType]
} else {
const key = reason as notification_reason_types
subscriptionType = notificationReasonToSubscriptionType[key]
destinations = subscriptionType
? notificationSettings[subscriptionType]
: []
}
const optOutOfAllSettings = notificationSettings['opt_out_all']
// Your market closure notifications are high priority, opt-out doesn't affect their delivery
const optedOutOfEmail =
optOutOfAllSettings.includes('email') &&
subscriptionType !== 'your_contract_closed'
const optedOutOfBrowser =
optOutOfAllSettings.includes('browser') &&
subscriptionType !== 'your_contract_closed'
return {
sendToEmail: destinations.includes('email') && !optedOutOfEmail,
sendToBrowser: destinations.includes('browser') && !optedOutOfBrowser,
unsubscribeUrl: `${unsubscribeEndpoint}?id=${privateUser.id}&type=${subscriptionType}`,
urlToManageThisNotification: `${DOMAIN}/notifications?tab=settings&section=${subscriptionType}`,
}
} catch (e) {
// Fail safely
console.log(
`couldn't get notification destinations for type ${reason} for user ${privateUser.id}`
)
return {
sendToEmail: false,
sendToBrowser: false,
unsubscribeUrl: '',
urlToManageThisNotification: '',
}
}
}

View File

@ -1,7 +1,3 @@
import { notification_preferences } from './user-notification-preferences'
import { ENV_CONFIG } from './envs/constants'
import { MarketCreatorBadge, ProvenCorrectBadge, StreakerBadge } from './badge'
export type User = { export type User = {
id: string id: string
createdTime: number createdTime: number
@ -12,96 +8,32 @@ export type User = {
// For their user page // For their user page
bio?: string bio?: string
bannerUrl?: string
website?: string website?: string
twitterHandle?: string twitterHandle?: string
discordHandle?: string discordHandle?: string
balance: number balance: number
totalDeposits: number totalDeposits: number
totalPnLCached: number
profitCached: { creatorVolumeCached: number
daily: number
weekly: number
monthly: number
allTime: number
}
creatorVolumeCached: {
daily: number
weekly: number
monthly: number
allTime: number
}
fractionResolvedCorrectly: number
nextLoanCached: number
followerCountCached: number
followedCategories?: string[] followedCategories?: string[]
homeSections?: string[] }
referredByUserId?: string export const STARTING_BALANCE = 1000
referredByContractId?: string export const SUS_STARTING_BALANCE = 10 // for sus users, i.e. multiple sign ups for same person
referredByGroupId?: string
lastPingTime?: number
shouldShowWelcome?: boolean
lastBetTime?: number
currentBettingStreak?: number
hasSeenContractFollowModal?: boolean
freeMarketsCreated?: number
isBannedFromPosting?: boolean
achievements: {
provenCorrect?: {
badges: ProvenCorrectBadge[]
}
marketCreator?: {
badges: MarketCreatorBadge[]
}
streaker?: {
badges: StreakerBadge[]
}
}
}
export type PrivateUser = { export type PrivateUser = {
id: string // same as User.id id: string // same as User.id
username: string // denormalized from User username: string // denormalized from User
email?: string email?: string
weeklyTrendingEmailSent?: boolean unsubscribedFromResolutionEmails?: boolean
weeklyPortfolioUpdateEmailSent?: boolean unsubscribedFromCommentEmails?: boolean
manaBonusEmailSent?: boolean unsubscribedFromAnswerEmails?: boolean
unsubscribedFromGenericEmails?: boolean
initialDeviceToken?: string initialDeviceToken?: string
initialIpAddress?: string initialIpAddress?: string
apiKey?: string apiKey?: string
notificationPreferences: notification_preferences
twitchInfo?: {
twitchName: string
controlToken: string
botEnabled?: boolean
needsRelinking?: boolean
} }
}
export type PortfolioMetrics = {
investmentValue: number
balance: number
totalDeposits: number
timestamp: number
userId: string
}
export const MANIFOLD_USER_USERNAME = 'ManifoldMarkets'
export const MANIFOLD_USER_NAME = 'ManifoldMarkets'
export const MANIFOLD_AVATAR_URL = 'https://manifold.markets/logo-bg-white.png'
// TODO: remove. Hardcoding the strings would be better.
// Different views require different language.
export const BETTOR = ENV_CONFIG.bettor ?? 'trader'
export const BETTORS = ENV_CONFIG.bettor + 's' ?? 'traders'
export const PRESENT_BET = ENV_CONFIG.presentBet ?? 'trade'
export const PRESENT_BETS = ENV_CONFIG.presentBet + 's' ?? 'trades'
export const PAST_BET = ENV_CONFIG.pastBet ?? 'trade'
export const PAST_BETS = ENV_CONFIG.pastBet + 's' ?? 'trades'

View File

@ -1,22 +0,0 @@
export function binarySearch(
min: number,
max: number,
comparator: (x: number) => number
) {
let mid = 0
while (true) {
mid = min + (max - min) / 2
// Break once we've reached max precision.
if (mid === min || mid === max) break
const comparison = comparator(mid)
if (comparison === 0) break
else if (comparison > 0) {
max = mid
} else {
min = mid
}
}
return mid
}

View File

@ -1,40 +1,3 @@
import { isEqual } from 'lodash'
export function filterDefined<T>(array: (T | null | undefined)[]) { export function filterDefined<T>(array: (T | null | undefined)[]) {
return array.filter((item) => item !== null && item !== undefined) as T[] return array.filter((item) => item !== null && item !== undefined) as T[]
} }
export function buildArray<T>(
...params: (T | T[] | false | undefined | null)[]
) {
const array: T[] = []
for (const el of params) {
if (Array.isArray(el)) {
array.push(...el)
} else if (el) {
array.push(el)
}
}
return array
}
export function groupConsecutive<T, U>(xs: T[], key: (x: T) => U) {
if (!xs.length) {
return []
}
const result = []
let curr = { key: key(xs[0]), items: [xs[0]] }
for (const x of xs.slice(1)) {
const k = key(x)
if (!isEqual(key, curr.key)) {
result.push(curr)
curr = { key: k, items: [x] }
} else {
curr.items.push(x)
}
}
result.push(curr)
return result
}

View File

@ -7,6 +7,6 @@ export const cleanUsername = (name: string, maxLength = 25) => {
.substring(0, maxLength) .substring(0, maxLength)
} }
export const cleanDisplayName = (displayName: string, maxLength = 30) => { export const cleanDisplayName = (displayName: string, maxLength = 25) => {
return displayName.replace(/\s+/g, ' ').substring(0, maxLength).trim() return displayName.replace(/\s+/g, ' ').substring(0, maxLength).trim()
} }

View File

@ -1,24 +0,0 @@
export const interpolateColor = (color1: string, color2: string, p: number) => {
const rgb1 = parseInt(color1.replace('#', ''), 16)
const rgb2 = parseInt(color2.replace('#', ''), 16)
const [r1, g1, b1] = toArray(rgb1)
const [r2, g2, b2] = toArray(rgb2)
const q = 1 - p
const rr = Math.round(r1 * q + r2 * p)
const rg = Math.round(g1 * q + g2 * p)
const rb = Math.round(b1 * q + b2 * p)
const hexString = Number((rr << 16) + (rg << 8) + rb).toString(16)
const hex = `#${'0'.repeat(6 - hexString.length)}${hexString}`
return hex
}
function toArray(rgb: number) {
const r = rgb >> 16
const g = (rgb >> 8) % 256
const b = rgb % 256
return [r, g, b]
}

View File

@ -8,21 +8,10 @@ const formatter = new Intl.NumberFormat('en-US', {
}) })
export function formatMoney(amount: number) { export function formatMoney(amount: number) {
const newAmount = const newAmount = Math.round(amount) === 0 ? 0 : Math.floor(amount) // handle -0 case
// handle -0 case
Math.round(amount) === 0
? 0
: // Handle 499.9999999999999 case
(amount > 0 ? Math.floor : Math.ceil)(
amount + 0.00000000001 * Math.sign(amount)
)
return ENV_CONFIG.moneyMoniker + formatter.format(newAmount).replace('$', '') return ENV_CONFIG.moneyMoniker + formatter.format(newAmount).replace('$', '')
} }
export function formatMoneyWithDecimals(amount: number) {
return ENV_CONFIG.moneyMoniker + amount.toFixed(2)
}
export function formatWithCommas(amount: number) { export function formatWithCommas(amount: number) {
return formatter.format(Math.floor(amount)).replace('$', '') return formatter.format(Math.floor(amount)).replace('$', '')
} }
@ -40,34 +29,18 @@ export function formatPercent(zeroToOne: number) {
return (zeroToOne * 100).toFixed(decimalPlaces) + '%' return (zeroToOne * 100).toFixed(decimalPlaces) + '%'
} }
const showPrecision = (x: number, sigfigs: number) =>
// convert back to number for weird formatting reason
`${Number(x.toPrecision(sigfigs))}`
// Eg 1234567.89 => 1.23M; 5678 => 5.68K // Eg 1234567.89 => 1.23M; 5678 => 5.68K
export function formatLargeNumber(num: number, sigfigs = 2): string { export function formatLargeNumber(num: number, sigfigs = 2): string {
const absNum = Math.abs(num) const absNum = Math.abs(num)
if (absNum < 1) return showPrecision(num, sigfigs) if (absNum < 1000) {
return '' + Number(num.toPrecision(sigfigs))
if (absNum < 100) return showPrecision(num, 2)
if (absNum < 1000) return showPrecision(num, 3)
if (absNum < 10000) return showPrecision(num, 4)
const suffix = ['', 'K', 'M', 'B', 'T', 'Q']
const i = Math.floor(Math.log10(absNum) / 3)
const numStr = showPrecision(num / Math.pow(10, 3 * i), sigfigs)
return `${numStr}${suffix[i] ?? ''}`
} }
export function shortFormatNumber(num: number): string {
if (num < 1000) return showPrecision(num, 3)
const suffix = ['', 'K', 'M', 'B', 'T', 'Q'] const suffix = ['', 'K', 'M', 'B', 'T', 'Q']
const i = Math.floor(Math.log10(num) / 3) const suffixIdx = Math.floor(Math.log10(absNum) / 3)
const suffixStr = suffix[suffixIdx]
const numStr = showPrecision(num / Math.pow(10, 3 * i), 2) const numStr = (num / Math.pow(10, 3 * suffixIdx)).toPrecision(sigfigs)
return `${numStr}${suffix[i] ?? ''}` return `${Number(numStr)}${suffixStr}`
} }
export function toCamelCase(words: string) { export function toCamelCase(words: string) {

View File

@ -1,5 +1,3 @@
import { sortBy, sum } from 'lodash'
export const logInterpolation = (min: number, max: number, value: number) => { export const logInterpolation = (min: number, max: number, value: number) => {
if (value <= min) return 0 if (value <= min) return 0
if (value >= max) return 1 if (value >= max) return 1
@ -18,33 +16,4 @@ export function normpdf(x: number, mean = 0, variance = 1) {
) )
} }
export const TAU = Math.PI * 2 const TAU = Math.PI * 2
export function median(xs: number[]) {
if (xs.length === 0) return NaN
const sorted = sortBy(xs, (x) => x)
const mid = Math.floor(sorted.length / 2)
if (sorted.length % 2 === 0) {
return (sorted[mid - 1] + sorted[mid]) / 2
}
return sorted[mid]
}
export function average(xs: number[]) {
return sum(xs) / xs.length
}
const EPSILON = 0.00000001
export function floatingEqual(a: number, b: number, epsilon = EPSILON) {
return Math.abs(a - b) < epsilon
}
export function floatingGreaterEqual(a: number, b: number, epsilon = EPSILON) {
return a + epsilon >= b
}
export function floatingLesserEqual(a: number, b: number, epsilon = EPSILON) {
return a - epsilon <= b
}

View File

@ -1,6 +1,6 @@
import { union } from 'lodash' import { union } from 'lodash'
export const removeUndefinedProps = <T extends object>(obj: T): T => { export const removeUndefinedProps = <T>(obj: T): T => {
const newObj: any = {} const newObj: any = {}
for (const key of Object.keys(obj)) { for (const key of Object.keys(obj)) {
@ -23,17 +23,3 @@ export const addObjects = <T extends { [key: string]: number }>(
return newObj as T return newObj as T
} }
export const subtractObjects = <T extends { [key: string]: number }>(
obj1: T,
obj2: T
) => {
const keys = union(Object.keys(obj1), Object.keys(obj2))
const newObj = {} as any
for (const key of keys) {
newObj[key] = (obj1[key] ?? 0) - (obj2[key] ?? 0)
}
return newObj as T
}

View File

@ -1,115 +1,29 @@
import { generateText, JSONContent, Node } from '@tiptap/core' import { MAX_TAG_LENGTH } from '../contract'
import { generateJSON } from '@tiptap/html'
// Tiptap starter extensions
import { Blockquote } from '@tiptap/extension-blockquote'
import { Bold } from '@tiptap/extension-bold'
import { BulletList } from '@tiptap/extension-bullet-list'
import { Code } from '@tiptap/extension-code'
import { CodeBlock } from '@tiptap/extension-code-block'
import { Document } from '@tiptap/extension-document'
import { HardBreak } from '@tiptap/extension-hard-break'
import { Heading } from '@tiptap/extension-heading'
import { History } from '@tiptap/extension-history'
import { HorizontalRule } from '@tiptap/extension-horizontal-rule'
import { Italic } from '@tiptap/extension-italic'
import { ListItem } from '@tiptap/extension-list-item'
import { OrderedList } from '@tiptap/extension-ordered-list'
import { Paragraph } from '@tiptap/extension-paragraph'
import { Strike } from '@tiptap/extension-strike'
import { Text } from '@tiptap/extension-text'
// other tiptap extensions
import { Image } from '@tiptap/extension-image'
import { Link } from '@tiptap/extension-link'
import { Mention } from '@tiptap/extension-mention'
import Iframe from './tiptap-iframe'
import TiptapTweet from './tiptap-tweet-type'
import { find } from 'linkifyjs'
import { uniq } from 'lodash'
import { TiptapSpoiler } from './tiptap-spoiler'
/** get first url in text. like "notion.so " -> "http://notion.so"; "notion" -> null */ export function parseTags(text: string) {
export function getUrl(text: string) { const regex = /(?:^|\s)(?:[#][a-z0-9_]+)/gi
const results = find(text, 'url') const matches = (text.match(regex) || []).map((match) =>
return results.length ? results[0].href : null match.trim().substring(1).substring(0, MAX_TAG_LENGTH)
)
const tagSet = new Set()
const uniqueTags: string[] = []
// Keep casing of last tag.
matches.reverse()
for (const tag of matches) {
const lowercase = tag.toLowerCase()
if (!tagSet.has(lowercase)) {
tagSet.add(lowercase)
uniqueTags.push(tag)
}
}
uniqueTags.reverse()
return uniqueTags
} }
// TODO: fuzzy matching export function parseWordsAsTags(text: string) {
export const wordIn = (word: string, corpus: string) => const taggedText = text
corpus.toLocaleLowerCase().includes(word.toLocaleLowerCase()) .split(/\s+/)
.map((tag) => (tag.startsWith('#') ? tag : `#${tag}`))
const checkAgainstQuery = (query: string, corpus: string) => .join(' ')
query.split(' ').every((word) => wordIn(word, corpus)) return parseTags(taggedText)
export const searchInAny = (query: string, ...fields: string[]) =>
fields.some((field) => checkAgainstQuery(query, field))
/** @return user ids of all \@mentions */
export function parseMentions(data: JSONContent): string[] {
const mentions = data.content?.flatMap(parseMentions) ?? [] //dfs
if (data.type === 'mention' && data.attrs) {
mentions.push(data.attrs.id as string)
}
return uniq(mentions)
}
// TODO: this is a hack to get around the fact that tiptap doesn't have a
// way to add a node view without bundling in tsx
function skippableComponent(name: string): Node<any, any> {
return Node.create({
name,
group: 'block',
content: 'inline*',
parseHTML() {
return [
{
tag: 'grid-cards-component',
},
]
},
})
}
const stringParseExts = [
// StarterKit extensions
Blockquote,
Bold,
BulletList,
Code,
CodeBlock,
Document,
HardBreak,
Heading,
History,
HorizontalRule,
Italic,
ListItem,
OrderedList,
Paragraph,
Strike,
Text,
// other extensions
Link,
Image.extend({ renderText: () => '[image]' }),
Mention, // user @mention
Mention.extend({ name: 'contract-mention' }), // market %mention
Iframe.extend({
renderText: ({ node }) =>
'[embed]' + node.attrs.src ? `(${node.attrs.src})` : '',
}),
skippableComponent('gridCardsComponent'),
skippableComponent('staticReactEmbedComponent'),
TiptapTweet.extend({ renderText: () => '[tweet]' }),
TiptapSpoiler.extend({ renderHTML: () => ['span', '[spoiler]', 0] }),
]
export function richTextToString(text?: JSONContent) {
if (!text) return ''
return generateText(text, stringParseExts)
}
export function htmlToRichText(html: string) {
return generateJSON(html, stringParseExts)
} }

View File

@ -46,10 +46,3 @@ export const shuffle = (array: unknown[], rand: () => number) => {
;[array[i], array[swapIndex]] = [array[swapIndex], array[i]] ;[array[i], array[swapIndex]] = [array[swapIndex], array[i]]
} }
} }
export function chooseRandomSubset<T>(items: T[], count: number) {
const fiveMinutes = 5 * 60 * 1000
const seed = Math.round(Date.now() / fiveMinutes).toString()
shuffle(items, createRNG(seed))
return items.slice(0, count)
}

View File

@ -1,6 +1,2 @@
export const MINUTE_MS = 60 * 1000 export const HOUR_MS = 60 * 60 * 1000
export const HOUR_MS = 60 * MINUTE_MS
export const DAY_MS = 24 * HOUR_MS export const DAY_MS = 24 * HOUR_MS
export const sleep = (ms: number) =>
new Promise((resolve) => setTimeout(resolve, ms))

View File

@ -1,100 +0,0 @@
// Adopted from https://github.com/ueberdosis/tiptap/blob/main/demos/src/Experiments/Embeds/Vue/iframe.ts
import { Node } from '@tiptap/core'
export interface IframeOptions {
allowFullscreen: boolean
HTMLAttributes: {
[key: string]: any
}
}
declare module '@tiptap/core' {
interface Commands<ReturnType> {
iframe: {
setIframe: (options: { src: string }) => ReturnType
}
}
}
// These classes style the outer wrapper and the inner iframe;
// Adopted from css in https://github.com/ueberdosis/tiptap/blob/main/demos/src/Experiments/Embeds/Vue/index.vue
const wrapperClasses = 'relative h-auto w-full overflow-hidden'
const iframeClasses = 'absolute top-0 left-0 h-full w-full'
export default Node.create<IframeOptions>({
name: 'iframe',
group: 'block',
atom: true,
addOptions() {
return {
allowFullscreen: true,
HTMLAttributes: {
class: 'iframe-wrapper' + ' ' + wrapperClasses,
// Tailwind JIT doesn't seem to pick up `pb-[20rem]`, so we hack this in:
style: 'padding-bottom: 20rem; ',
},
}
},
addAttributes() {
return {
src: {
default: null,
},
frameborder: {
default: 0,
},
height: {
default: 0,
},
allowfullscreen: {
default: this.options.allowFullscreen,
parseHTML: () => this.options.allowFullscreen,
},
}
},
parseHTML() {
return [{ tag: 'iframe' }]
},
renderHTML({ HTMLAttributes }) {
this.options.HTMLAttributes.style =
this.options.HTMLAttributes.style +
' height: ' +
HTMLAttributes.height +
';'
return [
'div',
this.options.HTMLAttributes,
[
'iframe',
{
...HTMLAttributes,
class: HTMLAttributes.class + ' ' + iframeClasses,
},
],
]
},
addCommands() {
return {
setIframe:
(options: { src: string }) =>
({ tr, dispatch }) => {
const { selection } = tr
const node = this.type.create(options)
if (dispatch) {
tr.replaceRangeWith(selection.from, selection.to, node)
}
return true
},
}
},
})

View File

@ -1,116 +0,0 @@
// adapted from @n8body/tiptap-spoiler
import {
Mark,
markInputRule,
markPasteRule,
mergeAttributes,
} from '@tiptap/core'
import type { ElementType } from 'react'
declare module '@tiptap/core' {
interface Commands<ReturnType> {
spoilerEditor: {
setSpoiler: () => ReturnType
toggleSpoiler: () => ReturnType
unsetSpoiler: () => ReturnType
}
}
}
export type SpoilerOptions = {
HTMLAttributes: Record<string, any>
spoilerOpenClass: string
spoilerCloseClass?: string
inputRegex: RegExp
pasteRegex: RegExp
as: ElementType
}
const spoilerInputRegex = /(?:^|\s)((?:\|\|)((?:[^||]+))(?:\|\|))$/
const spoilerPasteRegex = /(?:^|\s)((?:\|\|)((?:[^||]+))(?:\|\|))/g
export const TiptapSpoiler = Mark.create<SpoilerOptions>({
name: 'spoiler',
inline: true,
group: 'inline',
inclusive: false,
exitable: true,
content: 'inline*',
priority: 1001, // higher priority than other formatting so they go inside
addOptions() {
return {
HTMLAttributes: { 'aria-label': 'spoiler' },
spoilerOpenClass: '',
spoilerCloseClass: undefined,
inputRegex: spoilerInputRegex,
pasteRegex: spoilerPasteRegex,
as: 'span',
editing: false,
}
},
addCommands() {
return {
setSpoiler:
() =>
({ commands }) =>
commands.setMark(this.name),
toggleSpoiler:
() =>
({ commands }) =>
commands.toggleMark(this.name),
unsetSpoiler:
() =>
({ commands }) =>
commands.unsetMark(this.name),
}
},
addInputRules() {
return [
markInputRule({
find: this.options.inputRegex,
type: this.type,
}),
]
},
addPasteRules() {
return [
markPasteRule({
find: this.options.pasteRegex,
type: this.type,
}),
]
},
parseHTML() {
return [
{
tag: 'span',
getAttrs: (node) =>
(node as HTMLElement).ariaLabel?.toLowerCase() === 'spoiler' && null,
},
]
},
renderHTML({ HTMLAttributes }) {
const elem = document.createElement(this.options.as as string)
Object.entries(
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
class: this.options.spoilerCloseClass ?? this.options.spoilerOpenClass,
})
).forEach(([attr, val]) => elem.setAttribute(attr, val))
elem.addEventListener('click', () => {
elem.setAttribute('class', this.options.spoilerOpenClass)
})
return elem
},
})

View File

@ -1,37 +0,0 @@
import { Node, mergeAttributes } from '@tiptap/core'
export interface TweetOptions {
tweetId: string
}
// This is a version of the Tiptap Node config without addNodeView,
// since that would require bundling in tsx
export const TiptapTweetNode = {
name: 'tiptapTweet',
group: 'block',
atom: true,
addAttributes() {
return {
tweetId: {
default: null,
},
}
},
parseHTML() {
return [
{
tag: 'tiptap-tweet',
},
]
},
renderHTML(props: { HTMLAttributes: Record<string, any> }) {
return ['tiptap-tweet', mergeAttributes(props.HTMLAttributes)]
},
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
export default Node.create<TweetOptions>(TiptapTweetNode)

5
common/util/types.ts Normal file
View File

@ -0,0 +1,5 @@
export type FirstArgument<T> = T extends (arg1: infer U, ...args: any[]) => any
? U
: any
export type Truthy<T> = Exclude<T, undefined | null | false | 0 | ''>

43
dev.sh
View File

@ -1,43 +0,0 @@
#!/bin/bash
ENV=${1:-dev}
case $ENV in
dev)
FIREBASE_PROJECT=dev
NEXT_ENV=DEV ;;
prod)
FIREBASE_PROJECT=prod
NEXT_ENV=PROD ;;
localdb)
FIREBASE_PROJECT=dev
NEXT_ENV=DEV
EMULATOR=true ;;
*)
echo "Invalid environment; must be dev, prod, or localdb."
exit 1
esac
firebase use $FIREBASE_PROJECT
if [ ! -z $EMULATOR ]
then
npx concurrently \
-n FIRESTORE,FUNCTIONS,NEXT,TS \
-c green,white,magenta,cyan \
"yarn --cwd=functions localDbScript" \
"cross-env FIRESTORE_EMULATOR_HOST=localhost:8080 yarn --cwd=functions dev" \
"cross-env NEXT_PUBLIC_FUNCTIONS_URL=http://localhost:8088 \
NEXT_PUBLIC_FIREBASE_EMULATE=TRUE \
NEXT_PUBLIC_FIREBASE_ENV=${NEXT_ENV} \
yarn --cwd=web serve" \
"cross-env yarn --cwd=web ts-watch"
else
npx concurrently \
-n FUNCTIONS,NEXT,TS \
-c white,magenta,cyan \
"yarn --cwd=functions dev" \
"cross-env NEXT_PUBLIC_FUNCTIONS_URL=http://localhost:8088 \
NEXT_PUBLIC_FIREBASE_ENV=${NEXT_ENV} \
yarn --cwd=web serve" \
"cross-env yarn --cwd=web ts-watch"
fi

View File

@ -1,11 +1,3 @@
# docs # docs
Manifold Markets Docs Manifold Markets Docs
## Getting started
0. Make sure you have [Yarn 1.x][yarn]
1. `$ cd docs`
2. `$ yarn`
3. `$ yarn start`
4. The docs site will be available on http://localhost:3000

View File

@ -1,61 +0,0 @@
# How to Manifold
Manifold Markets is a novel site where users can bet against each other to predict the outcomes of all types of questions. Engage in intense discussion, or joke with friends, whilst putting play-money where your mouth is.
## Mana
Mana (M$) is our virtual play currency that cannot be converted to real money.
- **Its Value**
You can redeem your Mana and we will [donate to a charity](https://manifold.markets/charity) on your behalf. Redeeming and purchasing Mana occurs at a rate of M$100 to $1. You will be able to redeem it for merch and other cool items soon too!
- **It sets us apart**
Using play-money sets us apart from other similar sites as we dont want our users to solely focus on monetary gains. Instead we prioritize providing value in the form of an enjoyable experience and facilitating a more informed world through the power of prediction markets.
## How probabilities work
The probability of a market represents what the collective bets of users predict the chances of an outcome occurring is. How this is calculated depends on the type of market - see below!
## Types of markets
There are currently 3 types of markets: Yes/No (binary), Free response, and Numerical.
- **Yes/No (Binary)**
The creator asks a question where traders can bet yes or no.
Check out [Maniswap](https://www.notion.so/Maniswap-ce406e1e897d417cbd491071ea8a0c39) for more info on its automated market maker.
- **Free Response**
The creator asks an open ended question. Both the creator and users can propose answers which can be bet on. Dont be intimidated to add new answers! The payout system and initial liquidity rewards users who bet on new answers early. The algorithm used to determine the probability and payout is complicated but if you want to learn more check out [DPM](https://www.notion.so/DPM-b9b48a09ea1f45b88d991231171730c5).
- **Numerical**
Retracted whilst we make improvements. You still may see some old ones floating around though. Questions which can be answered by a number within a given range. Betting on a value will cause you to buy shares from buckets surrounding the number you choose.
## Compete and build your portfolio
Generate profits to prove your expertise and shine above your friends.
To the moon 🚀
- **Find inaccurate probabilities**
Use your superior knowledge on topics to identify markets which have inaccurate probabilities. This gives you favorable odds, so bet accordingly to shift the probability to what you think it should be.
- **React to news**
Markets are dynamic and ongoing events can drastically affect what the probability should look like. Be the keenest to react and there is a lot of Mana to be made.
- **Buy low, sell high**
Similar to a stock market, probabilities can be overvalued and undervalued. If you bet (buy shares) at one end of the spectrum and subsequently other users buy even more shares of that same type, the value of your own shares will increase. Sometimes it will be most profitable to wait for the market to resolve but often it can be wise to sell your shares and take the immediate profits. This can also be a great way to free up Mana if you are lacking funds.
- **Create innovative answers**
Certain free response markets provide room for creativity! The answers themselves can often affect the outcome based on how compelling they are.
More questions? Check out **[this community-driven FAQ](https://docs.manifold.markets/faq)**!

View File

@ -5,7 +5,7 @@ slug: /
# About Manifold Markets # About Manifold Markets
Manifold Markets lets anyone create a prediction market on any topic. Win virtual play money betting on what you know, from **[chess tournaments](https://manifold.markets/SG/will-magnus-carlsen-lose-any-regula)** to **[lunar collisions](https://manifold.markets/Duncan/will-the-wayward-falcon-9-booster-h)** to **[newsletter subscriber rates](https://manifold.markets/Nu%C3%B1oSempere/how-many-additional-subscribers-wil)** - or learn about the future by creating your own market! Manifold Markets lets anyone create a prediction market on any topic. Win virtual money betting on what you know, from **[chess tournaments](https://manifold.markets/SG/will-magnus-carlsen-lose-any-regula)** to **[lunar collisions](https://manifold.markets/Duncan/will-the-wayward-falcon-9-booster-h)** to **[newsletter subscriber rates](https://manifold.markets/Nu%C3%B1oSempere/how-many-additional-subscribers-wil)** - or learn about the future by creating your own market!
### **What are prediction markets?** ### **What are prediction markets?**
@ -17,13 +17,27 @@ If I think the Democrats are very likely to win, and you disagree, I might offer
Now, you or I could be mistaken and overshooting the true probability one way or another. If so, there's an incentive for someone else to bet and correct it! Over time, the implied probability will converge to the **[market's best estimate](https://en.wikipedia.org/wiki/Efficient-market_hypothesis)**. Since these probabilities are public, anyone can use them to make better decisions! Now, you or I could be mistaken and overshooting the true probability one way or another. If so, there's an incentive for someone else to bet and correct it! Over time, the implied probability will converge to the **[market's best estimate](https://en.wikipedia.org/wiki/Efficient-market_hypothesis)**. Since these probabilities are public, anyone can use them to make better decisions!
### **How does Manifold Markets work?**
1. **Anyone can create a market for any yes-or-no question.**
You can ask questions about the future like "Will Taiwan remove its 14-day COVID quarantine by Jun 01, 2022?" If the market thinks this is very likely, you can plan more activities for your trip.
You can also ask subjective, personal questions like "Will I enjoy my 2022 Taiwan trip?". Then share the market with your family and friends and get their takes!
2. **Anyone can bet on a market using Manifold Dollars (M$), our platform currency.**
You get M$ 1,000 just for signing up, so you can start betting immediately! When a market creator decides an outcome in your favor, you'll win Manifold Dollars from people who bet against you.
More questions? Check out **[this community-driven FAQ](https://outsidetheasylum.blog/manifold-markets-faq/)**!
### **Can prediction markets work without real money?** ### **Can prediction markets work without real money?**
Yes! There is substantial evidence that play-money prediction markets provide real predictive power. Examples include **[sports betting](http://www.electronicmarkets.org/fileadmin/user_upload/doc/Issues/Volume_16/Issue_01/V16I1_Statistical_Tests_of_Real-Money_versus_Play-Money_Prediction_Markets.pdf)** and internal prediction markets at firms like **[Google](https://www.networkworld.com/article/2284098/google-bets-on-value-of-prediction-markets.html)**. Yes! There is substantial evidence that play-money prediction markets provide real predictive power. Examples include **[sports betting](http://www.electronicmarkets.org/fileadmin/user_upload/doc/Issues/Volume_16/Issue_01/V16I1_Statistical_Tests_of_Real-Money_versus_Play-Money_Prediction_Markets.pdf)** and internal prediction markets at firms like **[Google](https://www.networkworld.com/article/2284098/google-bets-on-value-of-prediction-markets.html)**.
Our overall design also ensures that good forecasting will come out on top in the long term. In the competitive environment of the marketplace, bettors that are correct more often will gain influence, leading to better-calibrated forecasts over time. Our overall design also ensures that good forecasting will come out on top in the long term. In the competitive environment of the marketplace, bettors that are correct more often will gain influence, leading to better-calibrated forecasts over time.
Since our launch, we've seen hundreds of users trade each day, on over a thousand different markets! You can track the popularity of our platform at **[https://manifold.markets/stats](https://manifold.markets/stats)**. Since our launch, we've seen hundreds of users trade each day, on over a thousand different markets! You can track the popularity of our platform at **[http://manifold.markets/analytics](http://manifold.markets/analytics)**.
### **Why is this important?** ### **Why is this important?**
@ -53,7 +67,7 @@ Manifold Markets is currently a team of three:
- Stephen Grugett - Stephen Grugett
- Austin Chen - Austin Chen
We've previously launched consumer-facing startups (**[Throne](https://throne.live/)**, **[One Word](https://oneword.games/platform)**), and worked at top tech and trading firms (Google, Susquehanna). We've previously launched consumer-facing startups (**[Throne](https://throne.live/)**, **[One Word](http://oneword.games/platform)**), and worked at top tech and trading firms (Google, Susquehanna).
## **Talk to us!** ## **Talk to us!**

View File

@ -4,132 +4,42 @@
Our API is still in alpha — things may change or break at any time! Our API is still in alpha — things may change or break at any time!
If you have questions, come chat with us on [Discord](https://discord.com/invite/eHQBNBqXuh). Wed love to hear about what you build!
::: :::
## General notes Manifold currently supports a basic, read-only API for getting information about our markets.
Some APIs are not associated with any particular user. Other APIs require authentication. If you have questions, come chat with us on [Discord](https://discord.com/invite/eHQBNBqXuh). Wed love to hear about what you build!
APIs that require authentication accept an `Authorization` header in one of two formats: ## List out all markets
- `Authorization: Key {key}`. A Manifold API key associated with a user ### `/v0/markets`
account. Each account may have zero or one API keys. To generate an API key
for your account, visit your user profile, click "edit", and click the
"refresh" button next to the API key field at the bottom. You can click it
again any time to invalidate your existing key and generate a new one.
- `Authorization: Bearer {jwt}`. A signed JWT from Firebase asserting your
identity. This is what our web client uses. It will probably be annoying for
you to generate and we will not document it further here.
API requests that accept parameters should either have the parameters in the
query string if they are GET requests, or have a body with a JSON object with
one property per parameter if they are POST requests.
API responses should always either have a body with a JSON result object (if
the response was a 200) or with a JSON object representing an error (if the
response was a 4xx or 5xx.)
## Endpoints
### `GET /v0/user/[username]`
Gets a user by their username. Remember that usernames may change.
Requires no authorization.
### `GET /v0/user/by-id/[id]`
Gets a user by their unique ID. Many other API endpoints return this as the `userId`.
Requires no authorization.
### GET /v0/me
Returns the authenticated user.
### `GET /v0/groups`
Gets all groups, in no particular order.
Parameters:
- `availableToUserId`: Optional. if specified, only groups that the user can
join and groups they've already joined will be returned.
Requires no authorization.
### `GET /v0/group/[slug]`
Gets a group by its slug.
Requires no authorization.
Note: group is singular in the URL.
### `GET /v0/group/by-id/[id]`
Gets a group by its unique ID.
Requires no authorization.
Note: group is singular in the URL.
### `GET /v0/group/by-id/[id]/markets`
Gets a group's markets by its unique ID.
Requires no authorization.
Note: group is singular in the URL.
### `GET /v0/markets`
Lists all markets, ordered by creation date descending.
Parameters:
- `limit`: Optional. How many markets to return. The maximum and the default is 1000.
- `before`: Optional. The ID of the market before which the list will start. For
example, if you ask for the most recent 10 markets, and then perform a second
query for 10 more markets with `before=[the id of the 10th market]`, you will
get markets 11 through 20.
Requires no authorization.
- Example request - Example request
``` ```
https://manifold.markets/api/v0/markets?limit=1 http://manifold.markets/api/v0/markets
``` ```
- Example response - Example response
```json ```json
[ [
{ {
"id":"EvIhzcJXwhL0HavaszD7", "id":"FKtYX3t8ZfIp5gytJWAI",
"creatorUsername":"Austin", "creatorUsername":"JamesGrugett",
"creatorName":"Austin", "creatorName":"James Grugett",
"createdTime":1653850472294, "createdTime":1645139406452,
"creatorAvatarUrl":"https://lh3.googleusercontent.com/a-/AOh14GiZyl1lBehuBMGyJYJhZd-N-mstaUtgE4xdI22lLw=s96-c", "closeTime":1647406740000,
"closeTime":1653893940000, "question":"What will be the best assessment of the Free response feature on March 15th?",
"question":"Will I write a new blog post today?", "description":"Hey guys, let's try this out!\nWe will see how people use the new Free response market type over the next month. Then I will pick the answer that I think best describes the consensus view of this feature on March 15th. Cheers.",
"tags":[ "tags":[
"personal", "ManifoldMarkets"
"commitments"
], ],
"url":"https://manifold.markets/Austin/will-i-write-a-new-blog-post-today", "url":"https://manifold.markets/JamesGrugett/what-will-be-the-best-assessment-of",
"pool":146.73022894879944, "pool":null,
"probability":0.8958175225896258, "probability":0,
"p":0.08281474972181882, "volume7Days":100,
"totalLiquidity":102.65696071594805, "volume24Hours":100,
"outcomeType":"BINARY", "isResolved":false,
"mechanism":"cpmm-1",
"volume":241,
"volume7Days":0,
"volume24Hours":0,
"isResolved":true,
"resolution":"YES",
"resolutionTime":1653924077078
},
... ...
}
``` ```
- Response type: Array of `LiteMarket` - Response type: Array of `LiteMarket`
@ -142,51 +52,29 @@ Requires no authorization.
// Attributes about the creator // Attributes about the creator
creatorUsername: string creatorUsername: string
creatorName: string creatorName: string
createdTime: number // milliseconds since epoch createdTime: number
creatorAvatarUrl?: string creatorAvatarUrl?: string
// Market attributes. All times are in milliseconds since epoch // Market attributes. All times are in milliseconds since epoch
closeTime?: number // Min of creator's chosen date, and resolutionTime closeTime?: number // Min of creator's chosen date, and resolutionTime
question: string question: string
description: string
// A list of tags on each market. Any user can add tags to any market.
// This list also includes the predefined categories shown as filters on the home page.
tags: string[] tags: string[]
// Note: This url always points to https://manifold.markets, regardless of what instance the api is running on.
// This url includes the creator's username, but this doesn't need to be correct when constructing valid URLs.
// i.e. https://manifold.markets/Austin/test-market is the same as https://manifold.markets/foo/test-market
url: string url: string
outcomeType: string // BINARY, FREE_RESPONSE, MULTIPLE_CHOICE, NUMERIC, or PSEUDO_NUMERIC pool: number
mechanism: string // dpm-2 or cpmm-1
probability: number probability: number
pool: { outcome: number } // For CPMM markets, the number of shares in the liquidity pool. For DPM markets, the amount of mana invested in each answer.
p?: number // CPMM markets only, probability constant in y^p * n^(1-p) = k
totalLiquidity?: number // CPMM markets only, the amount of mana deposited into the liquidity pool
min?: number // PSEUDO_NUMERIC markets only, the minimum resolvable value
max?: number // PSEUDO_NUMERIC markets only, the maximum resolvable value
isLogScale?: bool // PSEUDO_NUMERIC markets only, if true `number = (max - min + 1)^probability + minstart - 1`, otherwise `number = min + (max - min) * probability`
volume: number
volume7Days: number volume7Days: number
volume24Hours: number volume24Hours: number
isResolved: boolean isResolved: boolean
resolutionTime?: number resolutionTime?: number
resolution?: string resolution?: string
resolutionProbability?: number // Used for BINARY markets resolved to MKT
lastUpdatedTime?: number
} }
``` ```
### `GET /v0/market/[marketId]` ## Get information about one market
Gets information about a single market by ID. Includes comments, bets, and answers. ### `/v0/market/[marketId]`
Requires no authorization.
- Example request - Example request
@ -198,204 +86,227 @@ Requires no authorization.
```json ```json
{ {
"id": "lEoqtnDgJzft6apSKzYK", "id": "3zspH9sSzMlbFQLn9GKR",
"creatorUsername": "Angela", "creatorUsername": "Austin",
"creatorName": "Angela", "creatorName": "Austin Chen",
"createdTime": 1655258914863, "createdTime": 1644103005345,
"creatorAvatarUrl": "https://firebasestorage.googleapis.com/v0/b/mantic-markets.appspot.com/o/user-images%2FAngela%2F50463444807_edfd4598d6_o.jpeg?alt=media&token=ef44e13b-2e6c-4498-b9c4-8e38bdaf1476", "closeTime": 1667894340000,
"closeTime": 1655265001448, "question": "Will Carrick Flynn win the general election for Oregon's 6th District?",
"question": "What is good?", "description": "The Effective Altruism movement usually stays out of politics, but here is a recent, highly-upvoted endorsement of donating to Carrick Flynn as a high-impact area: https://forum.effectivealtruism.org/posts/Qi9nnrmjwNbBqWbNT/the-best-usd5-800-i-ve-ever-donated-to-pandemic-prevention\nFurther reading: https://ballotpedia.org/Oregon%27s_6th_Congressional_District_election,_2022\n\n#EffectiveAltruism #Politics",
"description": "Resolves proportionally to the answer(s) which I find most compelling. (Obviously Ill refrain from giving my own answers)\n\n(Please have at it with philosophy, ethics, etc etc)\n\n\nContract resolved automatically.", "tags": ["EffectiveAltruism", "Politics"],
"tags": [], "url": "https://manifold.markets/Austin/will-carrick-flynn-win-the-general",
"url": "https://manifold.markets/Angela/what-is-good", "pool": 400.0916328426886,
"pool": null, "probability": 0.34455568984059187,
"outcomeType": "FREE_RESPONSE", "volume7Days": 326.9083671573114,
"mechanism": "dpm-2",
"volume": 112,
"volume7Days": 212,
"volume24Hours": 0, "volume24Hours": 0,
"isResolved": true, "isResolved": false,
"resolution": "MKT", "bets": [
"resolutionTime": 1655265001448,
"answers": [
{ {
"createdTime": 1655258941573, "createdTime": 1644103005345,
"avatarUrl": "https://firebasestorage.googleapis.com/v0/b/mantic-markets.appspot.com/o/user-images%2FAngela%2F50463444807_edfd4598d6_o.jpeg?alt=media&token=ef44e13b-2e6c-4498-b9c4-8e38bdaf1476", "isAnte": true,
"id": "1", "shares": 83.66600265340756,
"username": "Angela", "userId": "igi2zGXsfxYPgB0DJTXVJVmwCOr2",
"number": 1, "amount": 70,
"name": "Angela", "probAfter": 0.3,
"contractId": "lEoqtnDgJzft6apSKzYK", "probBefore": 0.3,
"text": "ANTE", "id": "E1MjiVYBM0GkqRXhv5cR",
"userId": "qe2QqIlOkeWsbljfeF3MsxpSJ9i2", "outcome": "NO",
"probability": 0.66749733001068 "contractId": "3zspH9sSzMlbFQLn9GKR"
}, },
{ {
"name": "Isaac King", "contractId": "3zspH9sSzMlbFQLn9GKR",
"username": "IsaacKing", "probAfter": 0.3,
"text": "This answer", "shares": 54.77225575051661,
"userId": "y1hb6k7txdZPV5mgyxPFApZ7nQl2", "userId": "igi2zGXsfxYPgB0DJTXVJVmwCOr2",
"id": "2", "isAnte": true,
"number": 2, "createdTime": 1644103005345,
"avatarUrl": "https://lh3.googleusercontent.com/a-/AOh14GhNVriOvxK2VUAmE-jvYZwC-XIymatzVirT0Bqb2g=s96-c", "id": "jn3iIGwD5f0vxOHxo62o",
"contractId": "lEoqtnDgJzft6apSKzYK", "amount": 30,
"createdTime": 1655261198074, "probBefore": 0.3,
"probability": 0.008922214311142757 "outcome": "YES"
}, },
{ {
"createdTime": 1655263226587, "shares": 11.832723364874056,
"userId": "jbgplxty4kUKIa1MmgZk22byJq03", "probAfter": 0.272108843537415,
"id": "3", "userId": "PkBnU8cAZiOLa0fjxiUzMKsFMYZ2",
"avatarUrl": "https://firebasestorage.googleapis.com/v0/b/mantic-markets.appspot.com/o/user-images%2FMartin%2Fgiphy.gif?alt=media&token=422ef610-553f-47e3-bf6f-c0c5cc16c70a", "contractId": "3zspH9sSzMlbFQLn9GKR",
"text": "Toyota Camry", "outcome": "NO",
"contractId": "lEoqtnDgJzft6apSKzYK", "amount": 10,
"name": "Undox", "id": "f6sHBab6lbGw9PsnVXdc",
"username": "Undox", "probBefore": 0.3,
"number": 3, "createdTime": 1644203305863
"probability": 0.008966714133143469
}, },
{ {
"number": 4, "userId": "BTksWMdCeHfDitWVaAZdjLSdu3o1",
"name": "James Grugett", "amount": 10,
"userId": "5LZ4LgYuySdL1huCWe7bti02ghx2", "id": "Vfui2KOQwy7gkRPP7xc6",
"text": "Utility (Defined by your personal utility function.)", "shares": 18.12694184700382,
"createdTime": 1655264793224, "outcome": "YES",
"contractId": "lEoqtnDgJzft6apSKzYK", "createdTime": 1644212358699,
"username": "JamesGrugett", "contractId": "3zspH9sSzMlbFQLn9GKR",
"id": "4", "probBefore": 0.272108843537415,
"avatarUrl": "https://lh3.googleusercontent.com/a-/AOh14GjC83uMe-fEfzd6QvxiK6ZqZdlMytuHxevgMYIkpAI=s96-c", "probAfter": 0.3367768595041322
"probability": 0.09211463154147384 },
{
"contractId": "3zspH9sSzMlbFQLn9GKR",
"probAfter": 0.3659259259259259,
"userId": "BTksWMdCeHfDitWVaAZdjLSdu3o1",
"probBefore": 0.3367768595041322,
"amount": 5,
"outcome": "YES",
"createdTime": 1644433184238,
"id": "eGI1VwAWF822LkcmOUot",
"shares": 8.435122540124937
},
{
"userId": "NHA7Gv9nNpb7b60GpLD3oFkBvPa2",
"shares": 59.79133423528123,
"amount": 50,
"probAfter": 0.24495867768595042,
"contractId": "3zspH9sSzMlbFQLn9GKR",
"createdTime": 1644693685223,
"probBefore": 0.3659259259259259,
"id": "fbU0DbmDWMnubggpQotw",
"outcome": "NO"
},
{
"amount": 25,
"userId": "iXw2OSyhs0c4QW2fAfK3yqmaYDv1",
"probAfter": 0.20583333333333328,
"outcome": "NO",
"shares": 28.3920247989266,
"contractId": "3zspH9sSzMlbFQLn9GKR",
"createdTime": 1644695698202,
"id": "k9hyljJD3MMXK2OYxTsR",
"probBefore": 0.24495867768595042
},
{
"createdTime": 1644716782308,
"shares": 11.17480183821209,
"probBefore": 0.20583333333333328,
"userId": "clvYFhVDzccYu20OUc5NBKJyDxj2",
"probAfter": 0.1927679500520291,
"id": "yYkZ4JpLgZHrRQUugpCD",
"outcome": "NO",
"contractId": "3zspH9sSzMlbFQLn9GKR",
"amount": 10
},
{
"contractId": "3zspH9sSzMlbFQLn9GKR",
"outcome": "YES",
"amount": 30,
"id": "IU2Hb1DesgKIN140BkhE",
"shares": 58.893424111838016,
"createdTime": 1644736846538,
"probBefore": 0.1927679500520291,
"userId": "BTksWMdCeHfDitWVaAZdjLSdu3o1",
"probAfter": 0.3289359861591695
},
{
"isSold": true,
"userId": "5zeWhzi9nlNNf5C9TVjshAN7QOd2",
"createdTime": 1644751343436,
"outcome": "NO",
"contractId": "3zspH9sSzMlbFQLn9GKR",
"amount": 25,
"probBefore": 0.3289359861591695,
"id": "fkCxVH7THaDbEhyJjXVk",
"probAfter": 0.2854194032651529,
"shares": 30.022082866721178
},
{
"probAfter": 0.2838618650900295,
"id": "Ao05LRRMXVWw8d7LtwhL",
"outcome": "NO",
"probBefore": 0.2854194032651529,
"shares": 1.1823269994736165,
"userId": "pUF3dMs9oLNpgU2LYtFmodaoDow1",
"amount": 1,
"contractId": "3zspH9sSzMlbFQLn9GKR",
"createdTime": 1644768321860
},
{
"id": "LJ8H8DTuK7CH9vN3u0Sd",
"createdTime": 1644771352663,
"shares": 113.5114039238785,
"probAfter": 0.17510453314667793,
"outcome": "NO",
"amount": 100,
"probBefore": 0.2838618650900295,
"contractId": "3zspH9sSzMlbFQLn9GKR",
"userId": "ebX5nzwrs8V0M5UynWvbtcj7KAI2"
},
{
"outcome": "YES",
"amount": 20,
"probBefore": 0.17510453314667793,
"id": "TECEF9I5FqTqt6uTIsJX",
"contractId": "3zspH9sSzMlbFQLn9GKR",
"createdTime": 1644805061501,
"shares": 43.88281646028875,
"userId": "lHxg3179e4amWm5LJhJoJrcWK482",
"probAfter": 0.24160019644701852
},
{
"amount": -25.908367157311375,
"id": "G3u2EzETWOyrGo15wtiQ",
"outcome": "NO",
"createdTime": 1644847494264,
"sale": {
"betId": "fkCxVH7THaDbEhyJjXVk",
"amount": 25.862948799445807
},
"probAfter": 0.26957595409437557,
"shares": -30.022082866721178,
"probBefore": 0.24160019644701852,
"contractId": "3zspH9sSzMlbFQLn9GKR",
"userId": "5zeWhzi9nlNNf5C9TVjshAN7QOd2"
},
{
"createdTime": 1644853733891,
"userId": "lbTXACtCnIacKDloKfXxYkDn0zM2",
"amount": 10,
"id": "z443uCkbYRLZW9QdXu1u",
"probAfter": 0.25822886066938844,
"contractId": "3zspH9sSzMlbFQLn9GKR",
"outcome": "NO",
"shares": 11.655141043149968,
"probBefore": 0.26957595409437557
},
{
"userId": "BTksWMdCeHfDitWVaAZdjLSdu3o1",
"amount": 15,
"shares": 28.311399392675895,
"id": "axoryV664uzHZ0jzWSXR",
"outcome": "YES",
"probBefore": 0.25822886066938844,
"contractId": "3zspH9sSzMlbFQLn9GKR",
"createdTime": 1644863335939,
"probAfter": 0.3033936853512369
},
{
"createdTime": 1644987330420,
"id": "jHAYDdZRkDw3lFoDXdmm",
"shares": 26.353902809992064,
"userId": "BTksWMdCeHfDitWVaAZdjLSdu3o1",
"contractId": "3zspH9sSzMlbFQLn9GKR",
"probAfter": 0.34455568984059187,
"probBefore": 0.3033936853512369,
"amount": 15,
"outcome": "YES"
} }
], ],
"comments": [ "comments": [
{ {
"id": "ZdHIyfQazHyl8nI0ENS7", "contractId": "3zspH9sSzMlbFQLn9GKR",
"userId": "qe2QqIlOkeWsbljfeF3MsxpSJ9i2", "userUsername": "Celer",
"createdTime": 1655265807433, "userAvatarUrl": "https://lh3.googleusercontent.com/a/AATXAJwp0vAolZgOmT7GbzFq7mOf8lr0BFEB_LqWWfZk=s96-c",
"text": "ok what\ni did not resolve this intentionally", "userId": "NHA7Gv9nNpb7b60GpLD3oFkBvPa2",
"contractId": "lEoqtnDgJzft6apSKzYK", "text": "It's a D+3 district, and the person we're pushing is functionally an outsider. I maxed my donation, but 25%, what I bought down to, implying even odds on both the general and the primary, seems if anything optimistic.",
"userName": "Angela", "createdTime": 1644693740967,
"userAvatarUrl": "https://firebasestorage.googleapis.com/v0/b/mantic-markets.appspot.com/o/user-images%2FAngela%2F50463444807_edfd4598d6_o.jpeg?alt=media&token=ef44e13b-2e6c-4498-b9c4-8e38bdaf1476", "id": "fbU0DbmDWMnubggpQotw",
"userUsername": "Angela" "betId": "fbU0DbmDWMnubggpQotw",
}, "userName": "Celer"
{
"userName": "James Grugett",
"userUsername": "JamesGrugett",
"id": "F7fvHGhTiFal8uTsUc9P",
"userAvatarUrl": "https://lh3.googleusercontent.com/a-/AOh14GjC83uMe-fEfzd6QvxiK6ZqZdlMytuHxevgMYIkpAI=s96-c",
"replyToCommentId": "ZdHIyfQazHyl8nI0ENS7",
"text": "@Angela Sorry! There was an error that automatically resolved several markets that were created in the last few hours.",
"createdTime": 1655266286514,
"userId": "5LZ4LgYuySdL1huCWe7bti02ghx2",
"contractId": "lEoqtnDgJzft6apSKzYK"
},
{
"userId": "qe2QqIlOkeWsbljfeF3MsxpSJ9i2",
"contractId": "lEoqtnDgJzft6apSKzYK",
"id": "PIHhXy5hLHSgW8uoUD0Q",
"userName": "Angela",
"text": "lmk if anyone lost manna from this situation and i'll try to fix it",
"userUsername": "Angela",
"createdTime": 1655277581308,
"userAvatarUrl": "https://firebasestorage.googleapis.com/v0/b/mantic-markets.appspot.com/o/user-images%2FAngela%2F50463444807_edfd4598d6_o.jpeg?alt=media&token=ef44e13b-2e6c-4498-b9c4-8e38bdaf1476"
},
{
"userAvatarUrl": "https://firebasestorage.googleapis.com/v0/b/mantic-markets.appspot.com/o/user-images%2FAngela%2F50463444807_edfd4598d6_o.jpeg?alt=media&token=ef44e13b-2e6c-4498-b9c4-8e38bdaf1476",
"userName": "Angela",
"text": "from my end it looks like no one did",
"replyToCommentId": "PIHhXy5hLHSgW8uoUD0Q",
"createdTime": 1655287149528,
"userUsername": "Angela",
"id": "5slnWEQWwm6dHjDi6oiH",
"contractId": "lEoqtnDgJzft6apSKzYK",
"userId": "qe2QqIlOkeWsbljfeF3MsxpSJ9i2"
}
],
"bets": [
{
"outcome": "0",
"contractId": "lEoqtnDgJzft6apSKzYK",
"fees": {
"liquidityFee": 0,
"creatorFee": 0,
"platformFee": 0
},
"isAnte": true,
"shares": 100,
"probAfter": 1,
"amount": 100,
"userId": "IPTOzEqrpkWmEzh6hwvAyY9PqFb2",
"createdTime": 1655258914863,
"probBefore": 0,
"id": "2jNZqnwoEQL7WDTTAWDP"
},
{
"shares": 173.20508075688772,
"fees": {
"platformFee": 0,
"liquidityFee": 0,
"creatorFee": 0
},
"contractId": "lEoqtnDgJzft6apSKzYK",
"probBefore": 0,
"createdTime": 1655258941573,
"loanAmount": 0,
"userId": "qe2QqIlOkeWsbljfeF3MsxpSJ9i2",
"amount": 100,
"outcome": "1",
"probAfter": 0.75,
"id": "xuc3JoiNkE8lXPh15mUb"
},
{
"userId": "y1hb6k7txdZPV5mgyxPFApZ7nQl2",
"contractId": "lEoqtnDgJzft6apSKzYK",
"loanAmount": 0,
"probAfter": 0.009925496893641248,
"id": "8TBlzPtOdO0q5BgSyRbi",
"createdTime": 1655261198074,
"shares": 20.024984394500787,
"amount": 1,
"outcome": "2",
"probBefore": 0,
"fees": {
"liquidityFee": 0,
"creatorFee": 0,
"platformFee": 0
}
},
{
"probAfter": 0.00987648269777473,
"outcome": "3",
"id": "9vdwes6s9QxbYZUBhHs4",
"createdTime": 1655263226587,
"shares": 20.074859899884732,
"amount": 1,
"loanAmount": 0,
"fees": {
"liquidityFee": 0,
"platformFee": 0,
"creatorFee": 0
},
"userId": "jbgplxty4kUKIa1MmgZk22byJq03",
"contractId": "lEoqtnDgJzft6apSKzYK",
"probBefore": 0
},
{
"createdTime": 1655264793224,
"fees": {
"creatorFee": 0,
"liquidityFee": 0,
"platformFee": 0
},
"probAfter": 0.09211463154147384,
"amount": 10,
"id": "BehiSGgk1wAkIWz1a8L4",
"userId": "5LZ4LgYuySdL1huCWe7bti02ghx2",
"contractId": "lEoqtnDgJzft6apSKzYK",
"loanAmount": 0,
"probBefore": 0,
"outcome": "4",
"shares": 64.34283176858165
} }
] ]
} }
@ -407,13 +318,10 @@ Requires no authorization.
- Response type: A `FullMarket` - Response type: A `FullMarket`
```tsx ```tsx
// A complete market, along with bets, comments, and answers (for free response markets) // A complete market, along with bets and comments
type FullMarket = LiteMarket & { type FullMarket = LiteMarket & {
bets: Bet[] bets: Bet[]
comments: Comment[] comments: Comment[]
answers?: Answer[] // dpm-2 markets only
description: JSONContent // Rich text content. See https://tiptap.dev/guide/output#option-1-json
textDescription: string // string description without formatting, images, or embeds
} }
type Bet = { type Bet = {
@ -439,11 +347,9 @@ Requires no authorization.
} }
``` ```
### `GET /v0/slug/[marketSlug]` ### `/v0/slug/[marketSlug]`
Gets information about a single market by slug (the portion of the URL path after the username). This is a convenience endpoint for getting info about a market from it slug (everything after the last slash in a markets URL).
Requires no authorization.
- Example request - Example request
``` ```
@ -451,339 +357,13 @@ Requires no authorization.
``` ```
- Response type: A `FullMarket` ; same as above. - Response type: A `FullMarket` ; same as above.
### `GET /v0/users` ## Deprecated
Lists all users. - Our old Markets API was available at [https://us-central1-mantic-markets.cloudfunctions.net/markets](https://us-central1-mantic-markets.cloudfunctions.net/markets)
- We dont plan on continuing to change this, but well support this endpoint until 2022-03-30
Requires no authorization.
- Example request
```
https://manifold.markets/api/v0/users
```
- Example response
```json
[
{
"id":"igi2zGXsfxYPgB0DJTXVJVmwCOr2",
"createdTime":1639011767273,
"name":"Austin",
"username":"Austin",
"url":"https://manifold.markets/Austin",
"avatarUrl":"https://lh3.googleusercontent.com/a-/AOh14GiZyl1lBehuBMGyJYJhZd-N-mstaUtgE4xdI22lLw=s96-c",
"bio":"I build Manifold! Always happy to chat; reach out on Discord or find a time on https://calendly.com/austinchen/manifold!",
"bannerUrl":"https://images.unsplash.com/photo-1501523460185-2aa5d2a0f981?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1531&q=80",
"website":"https://blog.austn.io",
"twitterHandle":"akrolsmir",
"discordHandle":"akrolsmir#4125",
"balance":9122.607163564959,
"totalDeposits":10339.004780544328,
"totalPnLCached":9376.601262721899,
"creatorVolumeCached":76078.46984199001
}
```
- Response type: Array of `LiteUser`
```tsx
// Basic information about a user
type LiteUser = {
id: string // user's unique id
createdTime: number
name: string // display name, may contain spaces
username: string // username, used in urls
url: string // link to user's profile
avatarUrl?: string
bio?: string
bannerUrl?: string
website?: string
twitterHandle?: string
discordHandle?: string
// Note: the following are here for convenience only and may be removed in the future.
balance: number
totalDeposits: number
totalPnLCached: number
creatorVolumeCached: number
}
```
### `POST /v0/bet`
Places a new bet on behalf of the authorized user.
Parameters:
- `amount`: Required. The amount to bet, in M$, before fees.
- `contractId`: Required. The ID of the contract (market) to bet on.
- `outcome`: Required. The outcome to bet on. For binary markets, this is `YES`
or `NO`. For free response markets, this is the ID of the free response
answer. For numeric markets, this is a string representing the target bucket,
and an additional `value` parameter is required which is a number representing
the target value. (Bet on numeric markets at your own peril.)
- `limitProb`: Optional. A number between `0.001` and `0.999` inclusive representing
the limit probability for your bet (i.e. 0.1% to 99.9% — multiply by 100 for the
probability percentage).
The bet will execute immediately in the direction of `outcome`, but not beyond this
specified limit. If not all the bet is filled, the bet will remain as an open offer
that can later be matched against an opposite direction bet.
- For example, if the current market probability is `50%`:
- A `M$10` bet on `YES` with `limitProb=0.4` would not be filled until the market
probability moves down to `40%` and someone bets `M$15` of `NO` to match your
bet odds.
- A `M$100` bet on `YES` with `limitProb=0.6` would fill partially or completely
depending on current unfilled limit bets and the AMM's liquidity. Any remaining
portion of the bet not filled would remain to be matched against in the future.
- An unfilled limit order bet can be cancelled using the cancel API.
Example request:
```
$ curl https://manifold.markets/api/v0/bet -X POST -H 'Content-Type: application/json' \
-H 'Authorization: Key {...}' \
--data-raw '{"amount":1, \
"outcome":"YES", \
"contractId":"{...}"}'
```
### `POST /v0/bet/cancel/[id]`
Cancel the limit order of a bet with the specified id. If the bet was unfilled, it will be cancelled so that no other bets will match with it. This is action irreversable.
### `POST /v0/market`
Creates a new market on behalf of the authorized user.
Parameters:
- `outcomeType`: Required. One of `BINARY`, `FREE_RESPONSE`, `MULTIPLE_CHOICE`, or `PSEUDO_NUMERIC`.
- `question`: Required. The headline question for the market.
- `description`: Required. A long description describing the rules for the market.
- Note: string descriptions do **not** turn into links, mentions, formatted text. Instead, rich text descriptions must be in [TipTap json](https://tiptap.dev/guide/output#option-1-json).
- `closeTime`: Required. The time at which the market will close, represented as milliseconds since the epoch.
- `tags`: Optional. An array of string tags for the market.
For binary markets, you must also provide:
- `initialProb`: An initial probability for the market, between 1 and 99.
For numeric markets, you must also provide:
- `min`: The minimum value that the market may resolve to.
- `max`: The maximum value that the market may resolve to.
- `isLogScale`: If true, your numeric market will increase exponentially from min to max.
- `initialValue`: An initial value for the market, between min and max, exclusive.
For multiple choice markets, you must also provide:
- `answers`: An array of strings, each of which will be a valid answer for the market.
Example request:
```
$ curl https://manifold.markets/api/v0/market -X POST -H 'Content-Type: application/json' \
-H 'Authorization: Key {...}'
--data-raw '{"outcomeType":"BINARY", \
"question":"Is there life on Mars?", \
"description":"I'm not going to type some long ass example description.", \
"closeTime":1700000000000, \
"initialProb":25}'
```
### `POST /v0/market/[marketId]/add-liquidity`
Adds a specified amount of liquidity into the market.
- `amount`: Required. The amount of liquidity to add, in M$.
### `POST /v0/market/[marketId]/close`
Closes a market on behalf of the authorized user.
- `closeTime`: Optional. Milliseconds since the epoch to close the market at. If not provided, the market will be closed immediately. Cannot provide close time in past.
### `POST /v0/market/[marketId]/resolve`
Resolves a market on behalf of the authorized user.
Parameters:
For binary markets:
- `outcome`: Required. One of `YES`, `NO`, `MKT`, or `CANCEL`.
- `probabilityInt`: Optional. The probability to use for `MKT` resolution.
For free response or multiple choice markets:
- `outcome`: Required. One of `MKT`, `CANCEL`, or a `number` indicating the answer index.
- `resolutions`: An array of `{ answer, pct }` objects to use as the weights for resolving in favor of multiple free response options. Can only be set with `MKT` outcome. Note that the total weights must add to 100.
For numeric markets:
- `outcome`: Required. One of `CANCEL`, or a `number` indicating the selected numeric bucket ID.
- `value`: The value that the market may resolves to.
- `probabilityInt`: Required if `value` is present. Should be equal to
- If log scale: `log10(value - min + 1) / log10(max - min + 1)`
- Otherwise: `(value - min) / (max - min)`
Example request:
```
# Resolve a binary market
$ curl https://manifold.markets/api/v0/market/{marketId}/resolve -X POST \
-H 'Content-Type: application/json' \
-H 'Authorization: Key {...}' \
--data-raw '{"outcome": "YES"}'
# Resolve a binary market with a specified probability
$ curl https://manifold.markets/api/v0/market/{marketId}/resolve -X POST \
-H 'Content-Type: application/json' \
-H 'Authorization: Key {...}' \
--data-raw '{"outcome": "MKT", \
"probabilityInt": 75}'
# Resolve a free response market with a single answer chosen
$ curl https://manifold.markets/api/v0/market/{marketId}/resolve -X POST \
-H 'Content-Type: application/json' \
-H 'Authorization: Key {...}' \
--data-raw '{"outcome": 2}'
# Resolve a free response market with multiple answers chosen
$ curl https://manifold.markets/api/v0/market/{marketId}/resolve -X POST \
-H 'Content-Type: application/json' \
-H 'Authorization: Key {...}' \
--data-raw '{"outcome": "MKT", \
"resolutions": [ \
{"answer": 0, "pct": 50}, \
{"answer": 2, "pct": 50} \
]}'
```
### `POST /v0/market/[marketId]/sell`
Sells some quantity of shares in a binary market on behalf of the authorized user.
Parameters:
- `outcome`: Optional. One of `YES`, or `NO`. If you leave it off, and you only
own one kind of shares, you will sell that kind of shares.
- `shares`: Optional. The amount of shares to sell of the outcome given
above. If not provided, all the shares you own will be sold.
Example request:
```
$ curl https://manifold.markets/api/v0/market/{marketId}/sell -X POST \
-H 'Content-Type: application/json' \
-H 'Authorization: Key {...}' \
--data-raw '{"outcome": "YES", "shares": 10}'
```
### `POST /v0/comment`
Creates a comment in the specified market. Only supports top-level comments for now.
Parameters:
- `contractId`: Required. The ID of the market to comment on.
- `content`: The comment to post, formatted as [TipTap json](https://tiptap.dev/guide/output#option-1-json), OR
- `html`: The comment to post, formatted as an HTML string, OR
- `markdown`: The comment to post, formatted as a markdown string.
### `GET /v0/bets`
Gets a list of bets, ordered by creation date descending.
Parameters:
- `username`: Optional. If set, the response will include only bets created by this user.
- `market`: Optional. The slug of a market. If set, the response will only include bets on this market.
- `limit`: Optional. How many bets to return. The maximum and the default is 1000.
- `before`: Optional. The ID of the bet before which the list will start. For
example, if you ask for the most recent 10 bets, and then perform a second
query for 10 more bets with `before=[the id of the 10th bet]`, you will
get bets 11 through 20.
Requires no authorization.
- Example request
```
https://manifold.markets/api/v0/bets?username=ManifoldMarkets&market=will-i-be-able-to-place-a-limit-ord
```
- Response type: A `Bet[]`.
- <details><summary>Example response</summary><p>
```json
[
// Limit bet, partially filled.
{
"isFilled": false,
"amount": 15.596681605353808,
"userId": "IPTOzEqrpkWmEzh6hwvAyY9PqFb2",
"contractId": "Tz5dA01GkK5QKiQfZeDL",
"probBefore": 0.5730753474948571,
"isCancelled": false,
"outcome": "YES",
"fees": { "creatorFee": 0, "liquidityFee": 0, "platformFee": 0 },
"shares": 31.193363210707616,
"limitProb": 0.5,
"id": "yXB8lVbs86TKkhWA1FVi",
"loanAmount": 0,
"orderAmount": 100,
"probAfter": 0.5730753474948571,
"createdTime": 1659482775970,
"fills": [
{
"timestamp": 1659483249648,
"matchedBetId": "MfrMd5HTiGASDXzqibr7",
"amount": 15.596681605353808,
"shares": 31.193363210707616
}
]
},
// Normal bet (no limitProb specified).
{
"shares": 17.350459904608414,
"probBefore": 0.5304358279113885,
"isFilled": true,
"probAfter": 0.5730753474948571,
"userId": "IPTOzEqrpkWmEzh6hwvAyY9PqFb2",
"amount": 10,
"contractId": "Tz5dA01GkK5QKiQfZeDL",
"id": "1LPJHNz5oAX4K6YtJlP1",
"fees": {
"platformFee": 0,
"liquidityFee": 0,
"creatorFee": 0.4251333951457593
},
"isCancelled": false,
"loanAmount": 0,
"orderAmount": 10,
"fills": [
{
"amount": 10,
"matchedBetId": null,
"shares": 17.350459904608414,
"timestamp": 1659482757271
}
],
"createdTime": 1659482757271,
"outcome": "YES"
}
]
```
</p>
</details>
## Changelog ## Changelog
- 2022-09-24: Expand market POST docs to include new market types (`PSEUDO_NUMERIC`, `MULTIPLE_CHOICE`)
- 2022-07-15: Add user by username and user by ID APIs
- 2022-06-08: Add paging to markets endpoint
- 2022-06-05: Add new authorized write endpoints
- 2022-02-28: Add `resolutionTime` to markets, change `closeTime` definition - 2022-02-28: Add `resolutionTime` to markets, change `closeTime` definition
- 2022-02-19: Removed user IDs from bets - 2022-02-19: Removed user IDs from bets
- 2022-02-17: Released our v0 API, with `/markets`, `/market/[marketId]`, and `/slug/[slugId]` - 2022-02-17: Released our v0 API, with `/markets`, `/market/[marketId]`, and `/slug/[slugId]`

View File

@ -1,47 +0,0 @@
# Awesome Manifold 😎
A list of community-created projects built on, or related to, Manifold Markets.
## Data
- [Manifold Market Stats](https://wasabipesto.com/jupyter/manifold/)
## Sites using Manifold
- [WagerWith.me](https://www.wagerwith.me/) — Bet with your friends, with full Manifold integration to bet with M$.
- [Alignment Markets](https://alignmentmarkets.com/) - Bet on the progress of benchmarks in ML safety!
## API / Dev
- [PyManifold](https://github.com/bcongdon/PyManifold) - Python client for the Manifold API
- [PyManifold fork](https://github.com/gappleto97/PyManifold/) - Fork maintained by [@LivInTheLookingGlass](https://manifold.markets/LivInTheLookingGlass)
- [manifold-markets-python](https://github.com/vluzko/manifold-markets-python) - Python tools for working with Manifold Markets (including various accuracy metrics)
- [ManifoldMarketManager](https://github.com/gappleto97/ManifoldMarketManager) - Python script and library to automatically manage markets
- [manifeed](https://github.com/joy-void-joy/manifeed) - Tool that creates an RSS feed for new Manifold markets
- [manifold-sdk](https://github.com/keriwarr/manifold-sdk) - TypeScript/JavaScript client for the Manifold API
## Bots
- [@manifold@botsin.space](https://botsin.space/@manifold) - Posts new Manifold markets to Mastodon
- [James' Bot](https://github.com/manifoldmarkets/market-maker) — Simple trading bot that makes markets
- [mana](https://github.com/AnnikaCodes/mana) - A Discord bot for Manifold by [@arae](https://manifold.markets/arae)
## Writeups
- [Information Markets, Decision Markets, Attention Markets, Action Markets](https://astralcodexten.substack.com/p/information-markets-decision-markets) by Scott Alexander
- [Mismatched Monetary Motivation in Manifold Markets](https://kevin.zielnicki.com/2022/02/17/manifold/) by Kevin Zielnicki
- [Introducing the Salem/CSPI Forecasting Tournament](https://www.cspicenter.com/p/introducing-the-salemcspi-forecasting) by Richard Hanania
- [What I learned about running a betting market game night contest](https://shakeddown.wordpress.com/2022/08/04/what-i-learned-about-running-a-betting-market-game-night-contest/) by shakeddown
- [Free-riding on prediction markets](https://pedunculate.substack.com/p/free-riding-on-prediction-markets) by John Roxton
## Art
- Folded origami and doodles by [@hamnox](https://manifold.markets/hamnox) ![](https://i.imgur.com/nVGY4pL.png)
- Laser-cut Foldy by [@wasabipesto](https://manifold.markets/wasabipesto) ![](https://i.imgur.com/g9S6v3P.jpg)
## Alumni
_These projects are no longer active, but were really really cool!_
- [Research.Bet](https://research.bet/) - Prediction market for scientific papers, using Manifold
- [CivicDashboard](https://civicdash.org/dashboard) - Uses Manifold to for tracked solutions for the SF city government

View File

@ -0,0 +1,52 @@
# Guide to YES/NO markets
# Overview
Historically, Manifold used a special type of automated market marker based on a dynamic pari-mutuel (DPM) betting
system. Free response and numeric markets still use this system. Binary markets created prior to March 15, 2022 used
this system.
Binary markets created after March 15 use a constant-function market maker which holds constant the weighted geometric
mean, with weights equal to the probabilities chosen by the market creator at creation. This design was inspired by
Uniswap's CPMM and a suggestion from Manifold user Pepe.
# Basic facts
- Markets are structured around a question with a binary outcome.
- Traders can place a bet on either YES or NO and receive shares in the outcome in return.
- 1 YES share = M$1 if the event happens. 1 NO share = M$1 if the event does not happen.
- Notice that 1 YES share + 1 NO share = M$1. If you ever get multiple YES and NO shares, they will cancel out and you will be left with cash.
- When the market is resolved, you will be paid out according to your shares. If you own 100 YES shares, if the event resolves YES, you will earn M$100. (If the event resolves NO, you will earn M$0).
- The creator of each market is responsible for resolving each market YES or NO.
- Creators can also resolve N/A to cancel all transactions and return the money, or resolve to a particular probability (say 50%).
# Betting
- Betting on YES will increase the markets implied probability; betting on NO will decrease the probability.
- Manifold's automated market automatically adjusts the market probability after each trade and determines how many shares a user will get for their bet.
- You can sell back your shares for cash. If you sell YES shares, the market probability will go down. If you sell NO shares, the probability will go up.
- Manifold charges fees on each trade. They are baked into the number of shares you receive.
- If you place a M$100 bet on YES when the probability is 50%, you may end up with 150 YES shares. These shares already include our fees. Notice also that when you buy, the probability goes up, so you are not getting in exactly at 200 shares or 50%.
- Our fee schedule is currently: 13% _ (1 - post-bet probability) _ bet amount
- The post-trade probability is what the market probability would be after your bet if there were no fees.
- Example:
- If you bet M$100 on NO and the resulting probability without fees would be 10%, then you pay M$100 _ 13% _ 10% = M$1.3.
- If you bet M$100 on YES and the resulting probability without fees would be 90%, then you pay `M$100 * 13% * 10% = M$1.3`.
- The fees are used to provide a commission to the market creator and to subsidize trading within the market.
- The market creators commission is paid out only after the market is resolved.
- No fees are levied on sales.
# Market creation
- Users can create a market on any question they want.
- When you create a market, you must choose an initial probability and a close date (after which trading will halt).
- You must also pay a M$ 50 market creation fee, which is used to subsidize trading on your market.
- You will earn a commission on all bets placed in your market.
- You are responsible for resolving your market in a timely manner. All the fees you earned as a commission will be paid out after resolution.
# Liquidity
- The liquidity in a market is the amount of capital available for traders to trade against.
- The more liquidity, the greater incentive there is for traders to bet, the more accurate the market will be.
- You can add liquidity to a market you are interested in to increase the incentives for traders to participate. You can think of added liquidity as a subsidy for getting your question answered.
- You can add liquidity to any market by opening up the market info popup window located in the (...) section of the header on the market page.

View File

@ -15,85 +15,7 @@ Our community is the beating heart of Manifold; your individual contributions ar
## Awarded bounties ## Awarded bounties
💥 *Awarded on 2022-10-07* 🥧 *Awarded 2022-03-14*
**[Pepe](https://manifold.markets/Pepe): M$10,000**
**[Jack](https://manifold.markets/jack): M$2,000**
**[Martin](https://manifold.markets/MartinRandall): M$2,000**
**[Yev](https://manifold.markets/Yev): M$2,000**
**[Michael](https://manifold.markets/MichaelWheatley): M$2,000**
- For discovering an infinite mana exploit using limit orders, and informing the Manifold team of it privately.
**[Matt](https://manifold.markets/MattP): M$5,000**
**[Adrian](https://manifold.markets/ahalekelly): M$5,000**
**[Yev](https://manifold.markets/Yev): M$5,000**
- For discovering an AMM liquidity exploit and informing the Manifold team of it privately.
🎈 *Awarded on 2022-06-14*
**[Wasabipesto](https://manifold.markets/wasabipesto): M$20,000**
- For creating an awesome stats page which features and analyses various data sets! This can be found on the second tab of our [analytics page](https://manifold.markets/stats).
**[Jack](https://manifold.markets/jack): M$10,000**
- For adding a bunch of charities to [Manifold for Good](https://manifold.markets/charity), working out market math with Austin, and excellent comment activity.
**[Forrest](https://manifold.markets/Forrest): M$10,000**
- For a variety of [open source code contributions](https://github.com/manifoldmarkets/manifold/commits?author=ForrestWeiswolf), making our code base easier to use and maintain.
**[IsaacKing](https://manifold.markets/IsaacKing): M$10,000**
- For responsible disclosure of an exploit involving liquidity withdrawal, which has [now been fixed](https://github.com/manifoldmarkets/manifold/pull/472)! Removing one infinite money glitch at a time.
**[Sjlver](https://manifold.markets/Sjlver): M$5,000**
- For responsible disclosure of a potential exploit. We would say what it is, but it isnt quite fixed yet! 🤫
_🌿 Announced on 2022-05-02_
**[Marshall Polaris](https://manifold.markets/mqp): M$200K**
- For spearheading the effort to [open-source Manifold](https://github.com/manifoldmarkets/manifold), by documenting our processes, triaging bugs, and improving the new contributor experience.
- Marshall contributed over 2 weeks of part-time volunteer work; as such, we are awarding an amount that reflects the extraordinary amount of effort hes put in.
**[Vincent Luczkow](https://manifold.markets/VincentLuczkow): M$10,000**
- For building and releasing https://github.com/vluzko/manifold-markets-python, a super cool Python visualization of the calibration accuracy of all Manifold markets. Turns out were doing okay!
**[Akhil Wable](https://manifold.markets/AkhilWable): M$10,000**
- For writing up [Akhils Product Suggestions](https://www.notion.so/Akhil-s-Product-Suggestions-672e1cba393d4242852ff95ae79528df), an extensive, thoughtful list of improvements we could make to our platform.
**[Alex K. Chen](https://manifold.markets/AlexKChen): M$6,000**
- For the creation of a metric ton of innovative, long term questions. At the time of award, Alex was singlehandedly responsible for 20% of all markets posted in April.
**[ZorbaTHut](https://manifold.markets/ZorbaTHut): M$5,000**
- For [testing out futarchy](https://manifold.markets/tag/themotte_leaving) on an important problem for the community of The Motte.
**[Tetraspace](https://manifold.markets/Tetraspace): M$3,500**
- For the creation of [a focused set of questions on UK politics](https://twitter.com/TetraspaceWest/status/1516824123149848579), with relevant real-world predictions.
- For the idea and execution of using FR bounded buckets for mapping out a scalar range ([example market](https://manifold.markets/Tetraspace/if-ron-desantis-is-elected-presiden), [discussion here](https://manifold.markets/StephenMalina/how-many-daily-active-users-will-ma)).
**[tcheasdfjkl](https://manifold.markets/tcheasdfjkl): M$2,500**
- For calling out numerous areas of improvement, e.g. around our profit numbers being wonky, and problems with the DPM ⇒ CFMM market conversions.
**[Jack](https://manifold.markets/JackC): M$500**
- For recommending we list the Long-Term Future Fund as a supported charity.
**[N.C. Young](https://manifold.markets/NcyRocks): M$500**
- For recommending we list the Givewell Maximum Impact Fund as a supported charity.
\**🥧 *Awarded 2022-03-14\*
**[Kevin Zielnicki](https://manifold.markets/kjz): M$ 10,000** **[Kevin Zielnicki](https://manifold.markets/kjz): M$ 10,000**
@ -101,11 +23,11 @@ _🌿 Announced on 2022-05-02_
**[Pepe](https://manifold.markets/Pepe): M$ 10,000** **[Pepe](https://manifold.markets/Pepe): M$ 10,000**
- For developing the function used in our Constant Function Market Maker, making it easier for us to provision liquidity compared to a CPMM. - For developing the function used in our Constant Function Market Maker and working with us to polish it on Discord, making it easier for us to provision liquidity compared to a CPMM.
**[Gurkenglas](https://manifold.markets/Gurkenglas): M$ 5,000** **[Gurkenglas](https://manifold.markets/Gurkenglas): M$ 5,000**
- For concrete suggestions around improving our market maker algorithms, and creating useful graphs to make our different market makers more legible. - For concrete suggestions on Discord around improving our market maker algorithms, and creating useful graphs to make our different market makers more legible.
**[Scott Alexander](https://manifold.markets/ScottAlexander): M$ 5,000** **[Scott Alexander](https://manifold.markets/ScottAlexander): M$ 5,000**
@ -117,11 +39,11 @@ _🌿 Announced on 2022-05-02_
**[Isaac King](https://manifold.markets/IsaacKing): M$ 5,000** **[Isaac King](https://manifold.markets/IsaacKing): M$ 5,000**
- For [compiling an FAQ](https://outsidetheasylum.blog/manifold-markets-faq/) that answers a variety of questions that new users commonly face, and also inspiring us to move to [this open-source docs platform](https://docs.manifold.markets/). - For [compiling a comprehensive FAQ](https://outsidetheasylum.blog/manifold-markets-faq/) that answers a variety of questions that new users commonly face, and also inspiring us to move to [this open-source docs platform](http://docs.manifold.markets/).
**[Blazer](https://manifold.markets/BlazingDarkness/was-it-an-unpleasant-surprise-when): M$2,500** **[Blazer](https://manifold.markets/BlazingDarkness): M$ 2,500**
- For [calling out our mistake](https://manifold.markets/BlazingDarkness/was-it-an-unpleasant-surprise-when) in retroactively publicizing the market creators trades, leading us to revert this feature entirely. - For [calling out our mistake](https://manifold.markets/BlazingDarkness/was-it-an-unpleasant-surprise-when) in retroactively publicizing all market creators trades, leading us to revert this feature entirely.
⛑️ _Awarded 2022-01-09_ ⛑️ _Awarded 2022-01-09_

View File

@ -4,17 +4,19 @@
### Do I have to pay real money in order to participate? ### Do I have to pay real money in order to participate?
Nope! Each account starts with a free 1000 mana (or M$1000 for short). If you invest it wisely, you can increase your total without ever needing to put any real money into the site. Nope! Each account starts with a free M$ 1000. If you invest it wisely, you can increase your total without ever needing to put any real money into the site.
### What is the name for the currency Manifold uses, represented by M$?
Manifold Dollars, or mana for short.
### Can M$ be sold for real money? ### Can M$ be sold for real money?
No. Gambling laws put many restrictions on real-money prediction markets, so Manifold uses play money instead. No. Gambling laws put many restrictions on real-money prediction markets, so Manifold uses play money instead.
You can instead redeem your Mana and we will [donate to a charity](http://manifold.markets/charity) on your behalf. Redeeming and purchasing Mana occurs at a rate of M$100 to $1.
### How do the free response markets work? ### How do the free response markets work?
Any user can enter a response and bet on it, or they can bet on other people's responses. The response probabilities are weighted proportionally to how many people have bet on them. The market creator's ante goes into a "none of the above" pseudo-option that can't be bet on and can't be chosen as a correct answer when the market is resolved. (This means that free response markets tend to lose their creator almost their entire ante. It also means that if there are only a finite number of options that could win, traders can make guaranteed money by investing in them all equally.) See [here](https://manifoldmarkets.substack.com/p/above-the-fold-milestones-and-new) for more information. Any user can enter a response and bet on it, or they can bet on on other people's responses. The response probabilities are weighted proportionally to how many people have bet on them. The market creator's ante goes into a "none of the above" pseudo-option that can't be bet on and can't be chosen as a correct answer when the market is resolved. (This means that free response markets tend to lose their creator almost their entire ante, whereas normal markets only lose them a small fraction that's proportional to how well they chose their starting odds. It also means that if there are only a finite number of options that could win, traders can make guaranteed money by investing in them all equally.) See [here](https://manifoldmarkets.substack.com/p/above-the-fold-milestones-and-new) for more information.
### How accurate are the market probabilities? ### How accurate are the market probabilities?
@ -43,7 +45,7 @@ It's not only allowed, but encouraged. The whole point of a prediction market is
### Can I see who is buying/selling in a market? ### Can I see who is buying/selling in a market?
All trades before June 1, 2022 are anonymous by default. Trades after that date can be viewed in the Bets tab of any market, and also on that user's profile. Trading is anonymous by default. You'll only see their username if they leave a comment. As an exception, trading from the market's creator has their name attached.
## Creating and resolving markets ## Creating and resolving markets
@ -61,14 +63,12 @@ A market being "closed" means that people can no longer place or sell bets, "loc
### What does "PROB" mean? ### What does "PROB" mean?
Resolving a market as "PROB" means that it's resolved at a certain probability, chosen by the market creator. PROB 100% is the same as "yes", and PROB 0% is the same as "no". For example, if a market is resolved at PROB 75%, anyone who bought "yes" at less than 75% will (usually) make a profit, and anyone who bought "yes" at greater than 75% will (usually) take a loss. Vice versa for "no". This is also shown as "MKT" in the interface and API. Resolving a market as "PROB" means that it's resolved at a certain probability, chosen by the market creator. PROB 100% is the same as "yes", and PROB 0% is the same as "no". For example, if a market is resolved at PROB 75%, anyone who bought "yes" at less than 75% will (usually) make a profit, and anyone who bought "yes" at greater than 75% will (usually) take a loss. Vice versa for "no".
### What happens if a market creator resolves a market incorrectly, or doesn't resolve it at all? ### What happens if a market creator resolves a market incorrectly, or doesn't resolve it at all?
Nothing. The idea is for Manifold Markets to function with similar freedom and versatility to a Twitter poll, but with more accurate results due to the dynamics of prediction markets. Individual market resolution is not enforced by the site, so if you don't trust a certain user to judge their markets fairly, you probably shouldn't participate in their markets. Nothing. The idea is for Manifold Markets to function with similar freedom and versatility to a Twitter poll, but with more accurate results due to the dynamics of prediction markets. Individual market resolution is not enforced by the site, so if you don't trust a certain user to judge their markets fairly, you probably shouldn't participate in their markets.
That being said, manifold staff may manually send reminder emails to the creators of large markets if they have not been resolved in some time. There are also some projects in the works to enable automated market resolution after some time has passed.
### How do I tell if a certain market creator is trustworthy? ### How do I tell if a certain market creator is trustworthy?
Look at their market resolution history on their profile page. If their past markets have all been resolved correctly, their future ones probably will be too. You can also look at the comments on those markets to see if any traders noticed anything suspicious. You can also ask about that person in the [Manifold Markets Discord](https://discord.gg/eHQBNBqXuh). And if their profile links to their website or social media pages, you can take that into account too. Look at their market resolution history on their profile page. If their past markets have all been resolved correctly, their future ones probably will be too. You can also look at the comments on those markets to see if any traders noticed anything suspicious. You can also ask about that person in the [Manifold Markets Discord](https://discord.gg/eHQBNBqXuh). And if their profile links to their website or social media pages, you can take that into account too.
@ -87,15 +87,15 @@ You'll get an automated email when they close. You can also go to your profile p
### When do market creators get their commission fees? ### When do market creators get their commission fees?
When the creator resolves their market, they get the commission from all the trades that were executed in the market. When the creator resolves their market, they get the commission from all the trades that were exectuted in the market.
### How do I see markets that are currently open? ### How do I see markets that are currently open?
You can see the top markets in various categories [here](https://manifold.markets/markets). You can see the top 99 markets in various categories [here](https://manifold.markets/markets).
### Can I bet in a market I created? ### Can I bet in a market I created?
Yes. However if you're doing things that the community would perceive as "shady", such as putting all your money on the correct resolution immediately before closing the market, people may be more reluctant to participate in your markets in the future. Betting "normally" in your own market is fine though. Yes. However if you're doing things that the community would perceive as "shady", such as put all your money on the correct resolution immediately before closing the market, people may be more reluctant to participate in your markets in the future. Betting "normally" in your own market is fine though.
## Miscellaneous ## Miscellaneous
@ -103,8 +103,6 @@ Yes. However if you're doing things that the community would perceive as "shady"
Contact them via [email](mailto:info@manifold.markets), post in their [Discord](https://discord.gg/eHQBNBqXuh), or create a market about that bug/feature in order to draw more attention to it and get community input. Contact them via [email](mailto:info@manifold.markets), post in their [Discord](https://discord.gg/eHQBNBqXuh), or create a market about that bug/feature in order to draw more attention to it and get community input.
If you don't mind putting in a little work, fork the code and open a [pull request](https://github.com/manifoldmarkets/manifold/pulls) on GitHub.
### How can I get notified of new developments? ### How can I get notified of new developments?
Being a very recent project, Manifold is adding new features and tweaking existing ones quite frequently. You can keep up with changes by subscribing to their [Substack](https://manifoldmarkets.substack.com/), or joining their [Discord server](https://discord.gg/eHQBNBqXuh). Being a very recent project, Manifold is adding new features and tweaking existing ones quite frequently. You can keep up with changes by subscribing to their [Substack](https://manifoldmarkets.substack.com/), or joining their [Discord server](https://discord.gg/eHQBNBqXuh).
@ -115,7 +113,7 @@ No, but the website is designed responsively and looks great on mobile.
### Does Manifold have an API for programmers? ### Does Manifold have an API for programmers?
Yep. Documentation is [here](https://docs.manifold.markets/api). Yep. Documentation is [here](https://www.notion.so/Manifold-Markets-API-5e7d0aef4dcf452bb04b319e178fabc5).
### If I have a question that isn't answered here, where can I ask it? ### If I have a question that isn't answered here, where can I ask it?

View File

@ -1,109 +0,0 @@
# Guide to Market Types
# Market Mechanisms
Historically, Manifold used a special type of automated market maker based on a dynamic pari-mutuel (DPM) betting
system. Free response and numeric markets still use this system. Binary markets created prior to March 15, 2022 used
this system, but all of those markets have since closed.
Binary markets created after March 15 use a constant-function market maker which holds constant the weighted geometric
mean, with weights equal to the probabilities chosen by the market creator at creation. This design was inspired by
Uniswap's CPMM and a suggestion from Manifold user Pepe. The benefit of this approach is that the payout for any bet
is fixed at purchase time - 100 shares of YES will always return M$100 if YES is chosen.
Free response markets (and the depreciated numeric markets) still use the DPM system, as they have discrete "buckets"
for the pool to be sorted into.
## Market Creation
- Users can create a market on any question they want.
- When a user creates a market, they must choose a close date, after which trading will halt.
- They must also pay a M$100 market creation fee, which is used as liquidity to subsidize trading on the market.
- The market creator will earn a commission on all bets placed in the market.
- The market creator is responsible for resolving each market in a timely manner. All fees earned as a commission will be paid out after resolution.
- Creators can also resolve N/A to cancel all transactions and reverse all transactions made on the market - this includes profits from selling shares.
# Binary Markets
## Binary Markets: Overview
- Binary markets are structured around a question with a binary outcome, such as:
- [Will Bitcoin be worth more than $60,000 on Jan 1, 2022 at 12 am ET?](https://manifold.markets/SG/will-bitcoin-be-worth-more-than-600)
- [Will Manifold Markets have over $1M in revenue by Jan 1st, 2023?](https://manifold.markets/ManifoldMarkets/will-mantic-markets-have-over-1m)
- [Will we discover life on Mars before 2024?](https://manifold.markets/LarsDoucet/will-we-discover-life-on-mars-befor)
- Some binary markets are used as quasi-numeric markets, such as:
- [How many additional subscribers will my newsletter have by the end of February?](https://manifold.markets/Nu%C3%B1oSempere/how-many-additional-subscribers-wil)
- [How many new signups will Manifold have at the end of launch day?](https://manifold.markets/ManifoldMarkets/how-many-new-signups-will-manifold)
- [What day will US Covid deaths peak in February?](https://manifold.markets/JamesGrugett/what-day-will-us-covid-deaths-peak)
- These markets are made possible by the MKT option described below.
## Binary Markets: Betting & Payouts
- Traders can place a bet on either YES or NO and receive shares in the outcome in return.
- Betting on YES will increase the markets implied probability; betting on NO will decrease the probability.
- Manifold's automated market automatically adjusts the market probability after each trade and determines how many shares a user will get for their bet.
- You can sell back your shares for cash. If you sell YES shares, the market probability will go down. If you sell NO shares, the probability will go up.
- 1 YES share = M$1 if the event happens. 1 NO share = M$1 if the event does not happen.
- Notice that 1 YES share + 1 NO share = M$1. If you ever get multiple YES and NO shares, they will cancel out and you will be left with cash.
- When the market is resolved, you will be paid out according to your shares. If you own 100 YES shares, if the event resolves YES, you will earn M$100. (If the event resolves NO, you will earn M$0).
- The creator of each market is responsible for resolving each market. They can resolve to YES, NO, MKT, or N/A.
- Resolving to MKT allows the creator to choose a percentage. The payout for any YES share is multiplied by this percentage, and vice versa for NO.
- For example, if a market resolves to MKT at 30%, if you have 100 shares of YES you will receive `M$100 * 30% = M$30`.
- In the same situation as above, if you have 100 shares of NO you will receive `M$100 * (100% - 30%) = M$70`.
- Note that even in this instance, 1 YES share plus 1 NO share still equals M$1.
## Binary Markets: Liquidity
- The liquidity in a market is the amount of capital available for traders to trade against. The more liquidity, the greater incentive there is for traders to bet, and the more accurate the market should be.
- When a market is created, the creation fee (also called the ante or subsidy) is used to fill the liquiity pool. This happens whether the creation fee is paid by the user or by Manifold for the daily free market.
- Behind the scenes, when a bet is placed the CPMM mechanism does [a bunch of math](http://bit.ly/maniswap). The end result is that for each M$1 bet, 1 YES share and 1 NO share is created. Some amount of shares are then given to the user who made the bet, and the rest are stored in the liquidity pool.
Due to this mechansim, the number of YES shares in the whole market always equals the number of NO shares.
- You can manually add liquidity to any market to increase the incentives for traders to participate. You can think of added liquidity as a subsidy for getting your question answered. You can do this by opening up the market info popup window located in the (...) section of the header on the market page.
- Adding liquidity provides you with a number of YES and NO shares, which can be withdrawn from the same interface. These shares resolve to M$ like normal when the market resolves, which will return you some amount of your investment.
- If the market moves significantly in either direction, your liquidity will become significantly less valuable. You are currently very unlikely to make money by investing liquidity in a market, it is a way to subsidize a market and encourage more people to bet, to achieve a more accurate answer.
- Adding liquidity to a market also makes it require more capital to move the market, so if you want to subsidize a market, first make sure the market price is roughly where you think it should be.
# Free-Response Markets
## Free-Response Markets: Overview
- Free-response markets are structured around a question with a multiple outcomes, such as:
- [Which team will win the NBA Finals 2022?](https://manifold.markets/howtodowtle/which-team-will-win-the-nba-finals)
- [Who will win "Top Streaming Songs Artist" at the 2022 Billboard Music Awards?](https://manifold.markets/Predictor/who-will-win-top-streaming-songs-ar)
- [What life improvement intervention suggested would I found most useful?](https://manifold.markets/vlad/what-life-improvement-intervention)
- Some free-response markets are used as quasi-numeric markets, such as:
- [What day will Russia invade Ukraine?](https://manifold.markets/Duncan/what-day-will-russia-invade-ukraine)
- [What will inflation be in March?](https://manifold.markets/ManifoldMarkets/what-will-inflation-be-in-march)
- [How many Manifold team members in the Bahamas will test positive for COVID?](https://manifold.markets/Sinclair/how-many-manifold-team-members-in-t)
## Free-Response Markets: Betting & Payouts
- Markets are structured around a list of answers, any of which can be bet on.
- When a Free Response market is created, the market creation fee goes into a hidden answer called the Ante and gets paid to the winner(s), to subsidize the market and create an incentive to bet. This happens whether the creation fee is paid by the user or by Manifold for the daily free market.
- This hidden answer is why a market's probabilities will not add up to 100%.
- If you want to further subsidize a market, it's customary to create an ANTE answer and put money in that.
- Anyone can add answers to a market as long as they stake some amount of M$ on it. Traders can place a bet on any answer and receive shares in the outcome in return.
- When a user places a bet, their M$ goes into the market's pool and they receive a certain amount of shares of the selected answer.
- When the market is resolved, you will be paid out according to your shares. If the creator resolves to answer #1, the entire pool is divided up amongst the users who bet on answer #1 proportional to their shares.
- The creator of each market is responsible for resolving each market. They can resolve to any single answer, or even multiple answers.
- Resolving to multiple answers allows the creator to choose a percentage for each selected answer (or distribute equally). The payout for any answer is taken from the amount of the total pool allocated to that answer.
- For example, let's take a free-response market with many answers. The pool for this market is $500, and you own 100 out of 500 total shares of answer #1.
- If the creator resolves to answer #1 only, you will receive `M$500 * (100 / 500) = M$100`.
- If the creator resolves 50% to answer #1 and 50% to answer #2, you will receive `(M$500 * 50%) * (100 / 500) = M$50`.
- Note that your payout is dependent on the total number of shares, and thus may decrease if more people buy shares in that answer.
# Fees
- Manifold charges fees on each trade. They are automatically calculated and baked into the number of shares you receive when you place a bet.
- Our CPMM fee schedule is currently: `10% * (1 - post-bet probability) * bet amount`
- Note that all current binary markets use this fee schedule.
- The post-bet probability is what the market probability would be after your bet if there were no fees.
- Example:
- If you bet M$100 on NO and the resulting probability without fees would be 10%, then you pay `M$100 * 10% * 10% = M$1.0`.
- If you bet M$100 on YES and the resulting probability without fees would be 50%, then you pay `M$100 * 10% * 50% = M$5.0`.
- 100% of this fee is used to provide a commission to the market creator, which is paid out after the market is resolved.
- Our DPM fee schedule is currently: `5% * (1 - post-bet probability) * bet amount`
- Note that all free-response markets use this fee schedule. The calculation for this is the same as above.
- 4% is used to provide a commission to the market creator, which is paid out after the market is resolved. 1% is "burnt" to prevent inflation.
- No fees are levied on sales. If you have existing shares in a binary market and buy shares on the opposite side, that is equivalent to selling your shares and you do not pay fees.

View File

@ -26,7 +26,8 @@ const config = {
docs: { docs: {
routeBasePath: '/', routeBasePath: '/',
sidebarPath: require.resolve('./sidebars.js'), sidebarPath: require.resolve('./sidebars.js'),
editUrl: 'https://github.com/manifoldmarkets/manifold/tree/main/docs', // Please change this to your repo.
editUrl: 'https://github.com/manifoldmarkets/docs/tree/main/',
remarkPlugins: [math], remarkPlugins: [math],
rehypePlugins: [katex], rehypePlugins: [katex],
}, },
@ -71,7 +72,7 @@ const config = {
label: 'Docs', label: 'Docs',
}, },
{ {
href: 'https://github.com/manifoldmarkets/manifold/tree/main/docs/docs', href: 'https://github.com/manifoldmarkets/docs',
label: 'GitHub', label: 'GitHub',
position: 'right', position: 'right',
}, },
@ -115,7 +116,7 @@ const config = {
}, },
{ {
label: 'GitHub', label: 'GitHub',
href: 'https://github.com/manifoldmarkets/manifold/', href: 'https://github.com/manifoldmarkets/docs',
}, },
], ],
}, },

View File

@ -30,8 +30,7 @@
}, },
"devDependencies": { "devDependencies": {
"@docusaurus/module-type-aliases": "2.0.0-beta.17", "@docusaurus/module-type-aliases": "2.0.0-beta.17",
"@tsconfig/docusaurus": "^1.0.4", "@tsconfig/docusaurus": "^1.0.4"
"@types/react": "^17.0.2"
}, },
"browserslist": { "browserslist": {
"production": [ "production": [

View File

@ -1,31 +1,11 @@
{ {
"functions": { "functions": {
"predeploy": "cd functions && yarn build", "predeploy": "npm --prefix \"$RESOURCE_DIR\" run build",
"runtime": "nodejs16", "runtime": "nodejs12",
"source": "functions/dist", "source": "functions"
"ignore": [
"node_modules",
".git",
"firebase-debug.log",
"firebase-debug.*.log"
]
}, },
"firestore": { "firestore": {
"rules": "firestore.rules", "rules": "firestore.rules",
"indexes": "firestore.indexes.json" "indexes": "firestore.indexes.json"
},
"emulators": {
"functions": {
"port": 5001
},
"firestore": {
"port": 8080
},
"pubsub": {
"port": 8085
},
"ui": {
"enabled": true
}
} }
} }

View File

@ -1,161 +1,5 @@
{ {
"indexes": [ "indexes": [
{
"collectionGroup": "bets",
"queryScope": "COLLECTION_GROUP",
"fields": [
{
"fieldPath": "isAnte",
"order": "ASCENDING"
},
{
"fieldPath": "isRedemption",
"order": "ASCENDING"
},
{
"fieldPath": "userId",
"order": "ASCENDING"
},
{
"fieldPath": "createdTime",
"order": "ASCENDING"
}
]
},
{
"collectionGroup": "bets",
"queryScope": "COLLECTION_GROUP",
"fields": [
{
"fieldPath": "isFilled",
"order": "ASCENDING"
},
{
"fieldPath": "userId",
"order": "ASCENDING"
}
]
},
{
"collectionGroup": "bets",
"queryScope": "COLLECTION_GROUP",
"fields": [
{
"fieldPath": "userId",
"order": "ASCENDING"
},
{
"fieldPath": "createdTime",
"order": "DESCENDING"
}
]
},
{
"collectionGroup": "bets",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "isCancelled",
"order": "ASCENDING"
},
{
"fieldPath": "isFilled",
"order": "ASCENDING"
},
{
"fieldPath": "createdTime",
"order": "DESCENDING"
}
]
},
{
"collectionGroup": "challenges",
"queryScope": "COLLECTION_GROUP",
"fields": [
{
"fieldPath": "creatorId",
"order": "ASCENDING"
},
{
"fieldPath": "createdTime",
"order": "DESCENDING"
}
]
},
{
"collectionGroup": "comments",
"queryScope": "COLLECTION_GROUP",
"fields": [
{
"fieldPath": "commentType",
"order": "ASCENDING"
},
{
"fieldPath": "userId",
"order": "ASCENDING"
},
{
"fieldPath": "createdTime",
"order": "DESCENDING"
}
]
},
{
"collectionGroup": "comments",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "userId",
"order": "ASCENDING"
},
{
"fieldPath": "createdTime",
"order": "ASCENDING"
}
]
},
{
"collectionGroup": "comments",
"queryScope": "COLLECTION_GROUP",
"fields": [
{
"fieldPath": "userId",
"order": "ASCENDING"
},
{
"fieldPath": "createdTime",
"order": "ASCENDING"
}
]
},
{
"collectionGroup": "comments",
"queryScope": "COLLECTION_GROUP",
"fields": [
{
"fieldPath": "userId",
"order": "ASCENDING"
},
{
"fieldPath": "createdTime",
"order": "DESCENDING"
}
]
},
{
"collectionGroup": "contracts",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "creatorId",
"order": "ASCENDING"
},
{
"fieldPath": "createdTime",
"order": "ASCENDING"
}
]
},
{ {
"collectionGroup": "contracts", "collectionGroup": "contracts",
"queryScope": "COLLECTION", "queryScope": "COLLECTION",
@ -170,56 +14,6 @@
} }
] ]
}, },
{
"collectionGroup": "contracts",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "creatorId",
"order": "ASCENDING"
},
{
"fieldPath": "isResolved",
"order": "ASCENDING"
},
{
"fieldPath": "popularityScore",
"order": "DESCENDING"
}
]
},
{
"collectionGroup": "contracts",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "groupSlugs",
"arrayConfig": "CONTAINS"
},
{
"fieldPath": "isResolved",
"order": "ASCENDING"
},
{
"fieldPath": "popularityScore",
"order": "DESCENDING"
}
]
},
{
"collectionGroup": "contracts",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "isResolved",
"order": "ASCENDING"
},
{
"fieldPath": "autoResolutionTime",
"order": "ASCENDING"
}
]
},
{ {
"collectionGroup": "contracts", "collectionGroup": "contracts",
"queryScope": "COLLECTION", "queryScope": "COLLECTION",
@ -252,46 +46,6 @@
} }
] ]
}, },
{
"collectionGroup": "contracts",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "isResolved",
"order": "ASCENDING"
},
{
"fieldPath": "visibility",
"order": "ASCENDING"
},
{
"fieldPath": "closeTime",
"order": "ASCENDING"
},
{
"fieldPath": "popularityScore",
"order": "DESCENDING"
}
]
},
{
"collectionGroup": "contracts",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "isResolved",
"order": "ASCENDING"
},
{
"fieldPath": "visibility",
"order": "ASCENDING"
},
{
"fieldPath": "popularityScore",
"order": "DESCENDING"
}
]
},
{ {
"collectionGroup": "contracts", "collectionGroup": "contracts",
"queryScope": "COLLECTION", "queryScope": "COLLECTION",
@ -350,24 +104,6 @@
} }
] ]
}, },
{
"collectionGroup": "contracts",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "isResolved",
"order": "ASCENDING"
},
{
"fieldPath": "visibility",
"order": "ASCENDING"
},
{
"fieldPath": "volume7Days",
"order": "ASCENDING"
}
]
},
{ {
"collectionGroup": "contracts", "collectionGroup": "contracts",
"queryScope": "COLLECTION", "queryScope": "COLLECTION",
@ -382,84 +118,6 @@
} }
] ]
}, },
{
"collectionGroup": "contracts",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "isResolved",
"order": "ASCENDING"
},
{
"fieldPath": "volume7Days",
"order": "ASCENDING"
}
]
},
{
"collectionGroup": "contracts",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "isResolved",
"order": "ASCENDING"
},
{
"fieldPath": "volume7Days",
"order": "ASCENDING"
},
{
"fieldPath": "closeTime",
"order": "ASCENDING"
}
]
},
{
"collectionGroup": "contracts",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "isResolved",
"order": "ASCENDING"
},
{
"fieldPath": "volume7Days",
"order": "ASCENDING"
},
{
"fieldPath": "createdTime",
"order": "ASCENDING"
}
]
},
{
"collectionGroup": "contracts",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "isResolved",
"order": "ASCENDING"
},
{
"fieldPath": "volume7Days",
"order": "DESCENDING"
}
]
},
{
"collectionGroup": "contracts",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "lowercaseTags",
"arrayConfig": "CONTAINS"
},
{
"fieldPath": "createdTime",
"order": "DESCENDING"
}
]
},
{ {
"collectionGroup": "contracts", "collectionGroup": "contracts",
"queryScope": "COLLECTION", "queryScope": "COLLECTION",
@ -473,80 +131,6 @@
"order": "DESCENDING" "order": "DESCENDING"
} }
] ]
},
{
"collectionGroup": "manalinks",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "fromId",
"order": "ASCENDING"
},
{
"fieldPath": "createdTime",
"order": "DESCENDING"
}
]
},
{
"collectionGroup": "notifications",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "isSeen",
"order": "ASCENDING"
},
{
"fieldPath": "createdTime",
"order": "DESCENDING"
}
]
},
{
"collectionGroup": "portfolioHistory",
"queryScope": "COLLECTION_GROUP",
"fields": [
{
"fieldPath": "userId",
"order": "ASCENDING"
},
{
"fieldPath": "timestamp",
"order": "ASCENDING"
}
]
},
{
"collectionGroup": "portfolioHistory",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "userId",
"order": "ASCENDING"
},
{
"fieldPath": "timestamp",
"order": "ASCENDING"
}
]
},
{
"collectionGroup": "txns",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "toId",
"order": "ASCENDING"
},
{
"fieldPath": "toType",
"order": "ASCENDING"
},
{
"fieldPath": "createdTime",
"order": "DESCENDING"
}
]
} }
], ],
"fieldOverrides": [ "fieldOverrides": [
@ -620,28 +204,6 @@
} }
] ]
}, },
{
"collectionGroup": "bets",
"fieldPath": "id",
"indexes": [
{
"order": "ASCENDING",
"queryScope": "COLLECTION"
},
{
"order": "DESCENDING",
"queryScope": "COLLECTION"
},
{
"arrayConfig": "CONTAINS",
"queryScope": "COLLECTION"
},
{
"order": "ASCENDING",
"queryScope": "COLLECTION_GROUP"
}
]
},
{ {
"collectionGroup": "bets", "collectionGroup": "bets",
"fieldPath": "userId", "fieldPath": "userId",
@ -664,28 +226,6 @@
} }
] ]
}, },
{
"collectionGroup": "comments",
"fieldPath": "contractId",
"indexes": [
{
"order": "ASCENDING",
"queryScope": "COLLECTION"
},
{
"order": "DESCENDING",
"queryScope": "COLLECTION"
},
{
"arrayConfig": "CONTAINS",
"queryScope": "COLLECTION"
},
{
"order": "ASCENDING",
"queryScope": "COLLECTION_GROUP"
}
]
},
{ {
"collectionGroup": "comments", "collectionGroup": "comments",
"fieldPath": "createdTime", "fieldPath": "createdTime",
@ -755,50 +295,6 @@
"queryScope": "COLLECTION_GROUP" "queryScope": "COLLECTION_GROUP"
} }
] ]
},
{
"collectionGroup": "follows",
"fieldPath": "userId",
"indexes": [
{
"order": "ASCENDING",
"queryScope": "COLLECTION"
},
{
"order": "DESCENDING",
"queryScope": "COLLECTION"
},
{
"arrayConfig": "CONTAINS",
"queryScope": "COLLECTION"
},
{
"order": "ASCENDING",
"queryScope": "COLLECTION_GROUP"
}
]
},
{
"collectionGroup": "portfolioHistory",
"fieldPath": "timestamp",
"indexes": [
{
"order": "ASCENDING",
"queryScope": "COLLECTION"
},
{
"order": "DESCENDING",
"queryScope": "COLLECTION"
},
{
"arrayConfig": "CONTAINS",
"queryScope": "COLLECTION"
},
{
"order": "ASCENDING",
"queryScope": "COLLECTION_GROUP"
}
]
} }
] ]
} }

View File

@ -6,89 +6,24 @@ service cloud.firestore {
match /databases/{database}/documents { match /databases/{database}/documents {
function isAdmin() { function isAdmin() {
return request.auth.token.email in [ return request.auth.uid == 'igi2zGXsfxYPgB0DJTXVJVmwCOr2' // Austin
'akrolsmir@gmail.com', || request.auth.uid == '5LZ4LgYuySdL1huCWe7bti02ghx2' // James
'jahooma@gmail.com', || request.auth.uid == 'tlmGNz9kjXc2EteizMORes4qvWl2' // Stephen
'taowell@gmail.com', || request.auth.uid == 'IPTOzEqrpkWmEzh6hwvAyY9PqFb2' // Manifold
'abc.sinclair@gmail.com',
'manticmarkets@gmail.com',
'iansphilips@gmail.com',
'd4vidchee@gmail.com',
'federicoruizcassarino@gmail.com',
'ingawei@gmail.com'
]
}
match /stats/stats {
allow read;
}
match /globalConfig/globalConfig {
allow read;
allow update: if isAdmin()
allow create: if isAdmin()
} }
match /users/{userId} { match /users/{userId} {
allow read; allow read;
allow update: if userId == 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', 'website', 'twitterHandle', 'discordHandle', 'followedCategories', 'lastPingTime','shouldShowWelcome', 'hasSeenContractFollowModal', 'homeSections']); .hasOnly(['bio', 'bannerUrl', 'website', 'twitterHandle', 'discordHandle', 'followedCategories']);
// User referral rules
allow update: if userId == request.auth.uid
&& request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['referredByUserId', 'referredByContractId', 'referredByGroupId'])
// only one referral allowed per user
&& !("referredByUserId" in resource.data)
// user can't refer themselves
&& !(userId == request.resource.data.referredByUserId);
// quid pro quos enabled (only once though so nbd) - bc I can't make this work:
// && (get(/databases/$(database)/documents/users/$(request.resource.data.referredByUserId)).referredByUserId == resource.data.id);
}
match /{somePath=**}/portfolioHistory/{portfolioHistoryId} {
allow read;
}
match /{somePath=**}/contract-metrics/{contractId} {
allow read;
}
match /{somePath=**}/challenges/{challengeId}{
allow read;
}
match /contracts/{contractId}/follows/{userId} {
allow read;
allow create, delete: if userId == request.auth.uid;
}
match /contracts/{contractId}/challenges/{challengeId}{
allow read;
allow create: if request.auth.uid == request.resource.data.creatorId;
// allow update if there have been no claims yet and if the challenge is still open
allow update: if request.auth.uid == resource.data.creatorId;
}
match /users/{userId}/follows/{followUserId} {
allow read;
allow write: if request.auth.uid == userId;
}
match /users/{userId}/likes/{likeId} {
allow read;
allow write: if request.auth.uid == userId;
}
match /{somePath=**}/follows/{followUserId} {
allow read;
} }
match /private-users/{userId} { match /private-users/{userId} {
allow read: if userId == request.auth.uid || isAdmin(); allow read: if resource.data.id == request.auth.uid || isAdmin();
allow update: if (userId == request.auth.uid || isAdmin()) allow update: if (resource.data.id == request.auth.uid || isAdmin())
&& request.resource.data.diff(resource.data).affectedKeys() && request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['apiKey', 'notificationPreferences', 'twitchInfo']); .hasOnly(['apiKey']);
} }
match /private-users/{userId}/views/{viewId} { match /private-users/{userId}/views/{viewId} {
@ -110,24 +45,14 @@ service cloud.firestore {
match /contracts/{contractId} { match /contracts/{contractId} {
allow read; allow read;
allow update: if request.resource.data.diff(resource.data).affectedKeys() allow update: if request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['tags', 'lowercaseTags', 'groupSlugs', 'groupLinks', 'flaggedByUsernames']); .hasOnly(['description', 'closeTime', 'tags', 'lowercaseTags']);
allow update: if request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['description', 'closeTime', 'question', 'visibility', 'unlistedById'])
&& resource.data.creatorId == request.auth.uid;
allow update: if isAdmin(); allow update: if isAdmin();
match /comments/{commentId} {
allow create: if request.auth != null && commentMatchesUser(request.auth.uid, request.resource.data);
}
} }
match /{somePath=**}/bets/{betId} { match /{somePath=**}/bets/{betId} {
allow read; allow read;
} }
match /{somePath=**}/liquidity/{liquidityId} {
allow read;
}
function commentMatchesUser(userId, comment) { function commentMatchesUser(userId, comment) {
// it's a bad look if someone can impersonate other ids/names/avatars so check everything // it's a bad look if someone can impersonate other ids/names/avatars so check everything
let user = get(/databases/$(database)/documents/users/$(userId)); let user = get(/databases/$(database)/documents/users/$(userId));
@ -139,12 +64,20 @@ service cloud.firestore {
match /{somePath=**}/comments/{commentId} { match /{somePath=**}/comments/{commentId} {
allow read; allow read;
allow create: if request.auth != null && commentMatchesUser(request.auth.uid, request.resource.data);
} }
match /{somePath=**}/answers/{answerId} { match /{somePath=**}/answers/{answerId} {
allow read; allow read;
} }
match /folds/{foldId} {
allow read;
allow update: if request.auth.uid == resource.data.curatorId
&& request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['name', 'about', 'tags', 'lowercaseTags']);
allow delete: if request.auth.uid == resource.data.curatorId;
}
match /{somePath=**}/followers/{userId} { match /{somePath=**}/followers/{userId} {
allow read; allow read;
@ -155,70 +88,5 @@ service cloud.firestore {
match /txns/{txnId} { match /txns/{txnId} {
allow read; allow read;
} }
// Note: `resource` = existing doc, `request.resource` = incoming doc
match /manalinks/{slug} {
// Anyone can view any manalink
allow get;
// Only you can create a manalink with your fromId
allow create: if request.auth.uid == request.resource.data.fromId;
// Only you can list and change your own manalinks
allow list, update: if request.auth.uid == resource.data.fromId;
}
match /users/{userId}/notifications/{notificationId} {
allow read;
allow update: if resource.data.userId == request.auth.uid
&& request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['isSeen', 'viewTime']);
}
match /{somePath=**}/groupMembers/{memberId} {
allow read;
}
match /{somePath=**}/groupContracts/{contractId} {
allow read;
}
match /groups/{groupId} {
allow read;
allow update: if (request.auth.uid == resource.data.creatorId || isAdmin())
&& request.resource.data.diff(resource.data)
.affectedKeys()
.hasOnly(['name', 'about', 'anyoneCanJoin', 'aboutPostId', 'pinnedItems' ]);
allow delete: if request.auth.uid == resource.data.creatorId;
match /groupContracts/{contractId} {
allow write: if isGroupMember() || request.auth.uid == get(/databases/$(database)/documents/groups/$(groupId)).data.creatorId
}
match /groupMembers/{memberId}{
allow create: if request.auth.uid == get(/databases/$(database)/documents/groups/$(groupId)).data.creatorId || (request.auth.uid == request.resource.data.userId && get(/databases/$(database)/documents/groups/$(groupId)).data.anyoneCanJoin);
allow delete: if request.auth.uid == resource.data.userId;
}
function isGroupMember() {
return exists(/databases/$(database)/documents/groups/$(groupId)/groupMembers/$(request.auth.uid));
}
match /comments/{commentId} {
allow read;
allow create: if request.auth != null && commentMatchesUser(request.auth.uid, request.resource.data) && isGroupMember();
}
}
match /posts/{postId} {
allow read;
allow update: if isAdmin() || request.auth.uid == resource.data.creatorId
&& request.resource.data.diff(resource.data)
.affectedKeys()
.hasOnly(['name', 'content']);
allow delete: if isAdmin() || request.auth.uid == resource.data.creatorId;
match /comments/{commentId} {
allow read;
allow create: if request.auth != null && commentMatchesUser(request.auth.uid, request.resource.data) ;
}
}
} }
} }

View File

@ -1,3 +0,0 @@
# This sets which EnvConfig is deployed to Firebase Cloud Functions
NEXT_PUBLIC_FIREBASE_ENV=DEV

View File

@ -1,3 +0,0 @@
# This sets which EnvConfig is deployed to Firebase Cloud Functions
NEXT_PUBLIC_FIREBASE_ENV=PROD

View File

@ -1,7 +1,7 @@
module.exports = { module.exports = {
plugins: ['lodash', 'unused-imports'], plugins: ['lodash'],
extends: ['eslint:recommended'], extends: ['eslint:recommended'],
ignorePatterns: ['dist', 'lib'], ignorePatterns: ['lib'],
env: { env: {
node: true, node: true,
}, },
@ -9,29 +9,17 @@ module.exports = {
{ {
files: ['**/*.ts'], files: ['**/*.ts'],
plugins: ['@typescript-eslint'], plugins: ['@typescript-eslint'],
extends: ['plugin:@typescript-eslint/recommended'],
parser: '@typescript-eslint/parser', parser: '@typescript-eslint/parser',
parserOptions: { parserOptions: {
tsconfigRootDir: __dirname, tsconfigRootDir: __dirname,
project: ['./tsconfig.json'], project: ['./tsconfig.json'],
}, },
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-extra-semi': 'off',
'@typescript-eslint/no-unused-vars': [
'warn',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
},
],
'unused-imports/no-unused-imports': 'warn',
},
}, },
], ],
rules: { rules: {
'linebreak-style': ['error', 'unix'], 'no-extra-semi': 'off',
'no-unused-vars': 'off',
'no-constant-condition': ['error', { checkLoops: false }],
'lodash/import-scope': [2, 'member'], 'lodash/import-scope': [2, 'member'],
}, },
} }

View File

@ -1,11 +1,10 @@
# Secrets # Secrets
.env*
.runtimeconfig.json .runtimeconfig.json
# GCP deployment artifact
dist/
# Compiled JavaScript files # Compiled JavaScript files
lib/ lib/**/*.js
lib/**/*.js.map
# TypeScript v1 declaration files # TypeScript v1 declaration files
typings/ typings/
@ -17,5 +16,4 @@ package-lock.json
ui-debug.log ui-debug.log
firebase-debug.log firebase-debug.log
firestore-debug.log firestore-debug.log
pubsub-debug.log
firestore_export/ firestore_export/

View File

@ -1 +0,0 @@
save-prefix ""

View File

@ -20,25 +20,27 @@ Adapted from https://firebase.google.com/docs/functions/get-started
3. `$ firebase login` to authenticate the CLI tools to Firebase 3. `$ firebase login` to authenticate the CLI tools to Firebase
4. `$ firebase use dev` to choose the dev project 4. `$ firebase use dev` to choose the dev project
#### (Installing) For local development ### For local development
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. `$ firebase functions:config:get > .runtimeconfig.json` to cache secrets for local dev
1. [Install](https://cloud.google.com/sdk/docs/install) gcloud CLI
2. If you don't have java (or see the error `Error: Process java -version has exited with code 1. Please make sure Java is installed and on your system PATH.`):
1. `$ brew install java` 1. `$ brew install java`
2. `$ sudo ln -sfn /opt/homebrew/opt/openjdk/libexec/openjdk.jdk /Library/Java/JavaVirtualMachines/openjdk.jdk` 2. `$ sudo ln -sfn /opt/homebrew/opt/openjdk/libexec/openjdk.jdk /Library/Java/JavaVirtualMachines/openjdk.jdk`
3. `$ gcloud auth login` to authenticate the CLI tools to Google Cloud
2. `$ gcloud auth login` to authenticate the CLI tools to Google Cloud 4. `$ gcloud config set project <project-id>` to choose the project (`$ gcloud projects list` to see options)
3. `$ gcloud config set project <project-id>` to choose the project (`$ gcloud projects list` to see options) 5. `$ mkdir firestore_export` to create a folder to store the exported database
4. `$ mkdir firestore_export` to create a folder to store the exported database 6. `$ yarn db:update-local-from-remote` to pull the remote db from Firestore to local
5. `$ yarn db:update-local-from-remote` to pull the remote db from Firestore to local 0. TODO: this won't work when open source, we'll have to point to the public db 1. TODO: this won't work when open source, we'll have to point to the public db
## Developing locally ## Developing locally
0. `$ ./dev.sh localdb` to start the local emulator and front end 0. `$ firebase use dev` if you haven't already
1. If you change db trigger code, you have to start (doesn't have to complete) the deploy of it to dev to cause a hard emulator code refresh `$ firebase deploy --only functions:dbTriggerNameHere` 1. `$ yarn serve` to spin up the emulators
- There's surely a better way to cause/react to a db trigger update but just adding this here for now as it works 1. The Emulator UI is at http://localhost:4000; the functions are hosted on :5001.
2. If you want to test a scheduled function replace your function in `test-scheduled-function.ts` and send a GET to `http://localhost:8088/testscheduledfunction` (Best user experience is via [Postman](https://www.postman.com/downloads/)!) Note: You have to kill and restart emulators when you change code; no hot reload =(
2. `$ yarn dev:emulate` in `/web` to connect to emulators with the frontend
1. Note: emulated database is cleared after every shutdown
## Firestore Commands ## Firestore Commands
@ -54,17 +56,14 @@ Adapted from https://firebase.google.com/docs/functions/get-started
## Deploying ## Deploying
0. After merging, you need to manually deploy to backend: 0. `$ firebase use prod` to switch to prod
1. `git checkout main` 1. `$ yarn deploy` to push your changes live!
1. `git pull origin main`
1. `$ firebase use prod` to switch to prod
1. `$ firebase deploy --only functions` to push your changes live!
(Future TODO: auto-deploy functions on Git push) (Future TODO: auto-deploy functions on Git push)
## Secrets management ## Secrets management
Secrets are strings that shouldn't be checked into Git (eg API keys, passwords). We store these using [Google Secret Manager](https://console.cloud.google.com/security/secret-manager), which provides them as environment variables to functions that require them. Some useful workflows: Secrets are strings that shouldn't be checked into Git (eg API keys, passwords). We store these using [environment config on Firebase Functions](https://firebase.google.com/docs/functions/config-env). Some useful workflows:
- Set a secret: `$ firebase functions:secrets:set STRIPE_APIKEY` - Set a secret: `$ firebase functions:config:set stripe.test_secret="THE-API-KEY"`
- Then, enter the secret in the prompt. - Preview all secrets: `$ firebase functions:config:get`
- Read a secret: `$ firebase functions:secrets:access STRIPE_APIKEY` - Cache for local dev:`$ firebase functions:config:get > .runtimeconfig.json`

View File

@ -5,54 +5,36 @@
"firestore": "dev-mantic-markets.appspot.com" "firestore": "dev-mantic-markets.appspot.com"
}, },
"scripts": { "scripts": {
"build": "yarn compile && rm -rf dist && mkdir -p dist/functions && cp -R ../common/lib dist/common && cp -R lib/src dist/functions/src && cp ../yarn.lock dist && cp package.json dist && cp .env.prod dist && cp .env.dev dist", "build": "tsc",
"compile": "tsc -b",
"watch": "tsc -w", "watch": "tsc -w",
"shell": "yarn build && firebase functions:shell", "shell": "yarn build && firebase functions:shell",
"start": "yarn shell", "start": "yarn shell",
"deploy": "firebase deploy --only functions", "deploy": "firebase deploy --only functions",
"logs": "firebase functions:log", "logs": "firebase functions:log",
"dev": "nodemon src/serve.ts", "serve": "yarn build && firebase emulators:start --only functions,firestore --import=./firestore_export",
"localDbScript": "firebase emulators:start --only functions,firestore,pubsub --import=./firestore_export", "db:update-local-from-remote": "yarn db:backup-remote && gsutil rsync -r gs://$npm_package_config_firestore/firestore_export ./firestore_export",
"serve": "firebase use dev && yarn build && firebase emulators:start --only functions,firestore,pubsub --import=./firestore_export",
"db:update-local-from-remote": "yarn db:backup-remote && gsutil -m 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 -m 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)",
"db:backup-remote": "yarn db:rename-remote-backup-folder && gcloud firestore export gs://$npm_package_config_firestore/firestore_export/", "db:backup-remote": "yarn db:rename-remote-backup-folder && gcloud firestore export gs://$npm_package_config_firestore/firestore_export/"
"verify": "(cd .. && yarn verify)",
"verify:dir": "npx eslint . --max-warnings 0; tsc -b -v --pretty"
}, },
"main": "functions/src/index.js", "main": "lib/functions/src/index.js",
"dependencies": { "dependencies": {
"@amplitude/node": "1.10.0", "@react-query-firebase/firestore": "0.4.2",
"@google-cloud/functions-framework": "3.1.2",
"@tiptap/core": "2.0.0-beta.199",
"@tiptap/extension-image": "2.0.0-beta.199",
"@tiptap/extension-link": "2.0.0-beta.199",
"@tiptap/extension-mention": "2.0.0-beta.199",
"@tiptap/html": "2.0.0-beta.199",
"@tiptap/starter-kit": "2.0.0-beta.199",
"@tiptap/suggestion": "2.0.0-beta.199",
"cors": "2.8.5", "cors": "2.8.5",
"dayjs": "1.11.4", "fetch": "1.1.0",
"express": "4.18.1",
"firebase-admin": "10.0.0", "firebase-admin": "10.0.0",
"firebase-functions": "3.21.2", "firebase-functions": "3.16.0",
"lodash": "4.17.21", "lodash": "4.17.21",
"mailgun-js": "0.22.0", "mailgun-js": "0.22.0",
"marked": "4.1.1",
"module-alias": "2.2.2", "module-alias": "2.2.2",
"node-fetch": "2", "react-query": "3.39.0",
"stripe": "8.194.0", "stripe": "8.194.0",
"zod": "3.17.2" "zod": "3.17.2"
}, },
"devDependencies": { "devDependencies": {
"@types/mailgun-js": "0.22.12", "@types/mailgun-js": "0.22.12",
"@types/marked": "4.0.7",
"@types/module-alias": "2.0.1", "@types/module-alias": "2.0.1",
"@types/node-fetch": "2.6.2", "firebase-functions-test": "0.3.3"
"firebase-functions-test": "0.3.3",
"puppeteer": "18.0.5"
}, },
"private": true "private": true
} }

View File

@ -1,170 +0,0 @@
import { z } from 'zod'
import { APIError, newEndpoint, validate } from './api'
import { log } from './utils'
import { Contract, CPMMBinaryContract } from '../../common/contract'
import { User } from '../../common/user'
import * as admin from 'firebase-admin'
import { FieldValue } from 'firebase-admin/firestore'
import { removeUndefinedProps } from '../../common/util/object'
import { Acceptance, Challenge } from '../../common/challenge'
import { CandidateBet } from '../../common/new-bet'
import { createChallengeAcceptedNotification } from './create-notification'
import { noFees } from '../../common/fees'
import { formatMoney, formatPercent } from '../../common/util/format'
import { redeemShares } from './redeem-shares'
const bodySchema = z.object({
contractId: z.string(),
challengeSlug: z.string(),
outcomeType: z.literal('BINARY'),
closeTime: z.number().gte(Date.now()),
})
const firestore = admin.firestore()
export const acceptchallenge = newEndpoint({}, async (req, auth) => {
const { challengeSlug, contractId } = validate(bodySchema, req.body)
const result = await firestore.runTransaction(async (trans) => {
const contractDoc = firestore.doc(`contracts/${contractId}`)
const userDoc = firestore.doc(`users/${auth.uid}`)
const challengeDoc = firestore.doc(
`contracts/${contractId}/challenges/${challengeSlug}`
)
const [contractSnap, userSnap, challengeSnap] = await trans.getAll(
contractDoc,
userDoc,
challengeDoc
)
if (!contractSnap.exists) throw new APIError(400, 'Contract not found.')
if (!userSnap.exists) throw new APIError(400, 'User not found.')
if (!challengeSnap.exists) throw new APIError(400, 'Challenge not found.')
const anyContract = contractSnap.data() as Contract
const user = userSnap.data() as User
const challenge = challengeSnap.data() as Challenge
if (challenge.acceptances.length > 0)
throw new APIError(400, 'Challenge already accepted.')
const creatorDoc = firestore.doc(`users/${challenge.creatorId}`)
const creatorSnap = await trans.get(creatorDoc)
if (!creatorSnap.exists) throw new APIError(400, 'Creator not found.')
const creator = creatorSnap.data() as User
const {
creatorAmount,
acceptorOutcome,
creatorOutcome,
creatorOutcomeProb,
acceptorAmount,
} = challenge
if (user.balance < acceptorAmount)
throw new APIError(400, 'Insufficient balance.')
if (creator.balance < creatorAmount)
throw new APIError(400, 'Creator has insufficient balance.')
const contract = anyContract as CPMMBinaryContract
const shares = (1 / creatorOutcomeProb) * creatorAmount
const createdTime = Date.now()
const probOfYes =
creatorOutcome === 'YES' ? creatorOutcomeProb : 1 - creatorOutcomeProb
log(
'Creating challenge bet for',
user.username,
shares,
acceptorOutcome,
'shares',
'at',
formatPercent(creatorOutcomeProb),
'for',
formatMoney(acceptorAmount)
)
const yourNewBet: CandidateBet = removeUndefinedProps({
orderAmount: acceptorAmount,
amount: acceptorAmount,
shares,
isCancelled: false,
contractId: contract.id,
outcome: acceptorOutcome,
probBefore: probOfYes,
probAfter: probOfYes,
loanAmount: 0,
createdTime,
fees: noFees,
challengeSlug: challenge.slug,
})
const yourNewBetDoc = contractDoc.collection('bets').doc()
trans.create(yourNewBetDoc, {
id: yourNewBetDoc.id,
userId: user.id,
...yourNewBet,
})
trans.update(userDoc, { balance: FieldValue.increment(-yourNewBet.amount) })
const creatorNewBet: CandidateBet = removeUndefinedProps({
orderAmount: creatorAmount,
amount: creatorAmount,
shares,
isCancelled: false,
contractId: contract.id,
outcome: creatorOutcome,
probBefore: probOfYes,
probAfter: probOfYes,
loanAmount: 0,
createdTime,
fees: noFees,
challengeSlug: challenge.slug,
})
const creatorBetDoc = contractDoc.collection('bets').doc()
trans.create(creatorBetDoc, {
id: creatorBetDoc.id,
userId: creator.id,
...creatorNewBet,
})
trans.update(creatorDoc, {
balance: FieldValue.increment(-creatorNewBet.amount),
})
const volume = contract.volume + yourNewBet.amount + creatorNewBet.amount
trans.update(contractDoc, { volume })
trans.update(
challengeDoc,
removeUndefinedProps({
acceptedByUserIds: [user.id],
acceptances: [
{
userId: user.id,
betId: yourNewBetDoc.id,
createdTime,
amount: acceptorAmount,
userUsername: user.username,
userName: user.name,
userAvatarUrl: user.avatarUrl,
} as Acceptance,
],
})
)
await createChallengeAcceptedNotification(
user,
creator,
challenge,
acceptorAmount,
contract
)
log('Done, sent notification.')
return yourNewBetDoc
})
await redeemShares(auth.uid, contractId)
return { betId: result.id }
})

View File

@ -0,0 +1,104 @@
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'
import { Contract } from '../../common/contract'
import { User } from '../../common/user'
import { removeUndefinedProps } from '../../common/util/object'
import { redeemShares } from './redeem-shares'
import { getNewLiquidityProvision } from '../../common/add-liquidity'
export const addLiquidity = functions.runWith({ minInstances: 1 }).https.onCall(
async (
data: {
amount: number
contractId: string
},
context
) => {
const userId = context?.auth?.uid
if (!userId) return { status: 'error', message: 'Not authorized' }
const { amount, contractId } = data
if (amount <= 0 || isNaN(amount) || !isFinite(amount))
return { status: 'error', message: 'Invalid amount' }
// run as transaction to prevent race conditions
return await firestore
.runTransaction(async (transaction) => {
const userDoc = firestore.doc(`users/${userId}`)
const userSnap = await transaction.get(userDoc)
if (!userSnap.exists)
return { status: 'error', message: 'User not found' }
const user = userSnap.data() as User
const contractDoc = firestore.doc(`contracts/${contractId}`)
const contractSnap = await transaction.get(contractDoc)
if (!contractSnap.exists)
return { status: 'error', message: 'Invalid contract' }
const contract = contractSnap.data() as Contract
if (
contract.mechanism !== 'cpmm-1' ||
contract.outcomeType !== 'BINARY'
)
return { status: 'error', message: 'Invalid contract' }
const { closeTime } = contract
if (closeTime && Date.now() > closeTime)
return { status: 'error', message: 'Trading is closed' }
if (user.balance < amount)
return { status: 'error', message: 'Insufficient balance' }
const newLiquidityProvisionDoc = firestore
.collection(`contracts/${contractId}/liquidity`)
.doc()
const { newLiquidityProvision, newPool, newP, newTotalLiquidity } =
getNewLiquidityProvision(
user,
amount,
contract,
newLiquidityProvisionDoc.id
)
if (newP !== undefined && !isFinite(newP)) {
return {
status: 'error',
message: 'Liquidity injection rejected due to overflow error.',
}
}
transaction.update(
contractDoc,
removeUndefinedProps({
pool: newPool,
p: newP,
totalLiquidity: newTotalLiquidity,
})
)
const newBalance = user.balance - amount
const newTotalDeposits = user.totalDeposits - amount
if (!isFinite(newBalance)) {
throw new Error('Invalid user balance for ' + user.username)
}
transaction.update(userDoc, {
balance: newBalance,
totalDeposits: newTotalDeposits,
})
transaction.create(newLiquidityProvisionDoc, newLiquidityProvision)
return { status: 'success', newLiquidityProvision }
})
.then(async (result) => {
await redeemShares(userId, contractId)
return result
})
}
)
const firestore = admin.firestore()

View File

@ -1,78 +0,0 @@
import * as admin from 'firebase-admin'
import { z } from 'zod'
import { Contract, CPMMContract } from '../../common/contract'
import { User } from '../../common/user'
import { getNewLiquidityProvision } from '../../common/add-liquidity'
import { APIError, newEndpoint, validate } from './api'
const bodySchema = z.object({
contractId: z.string(),
amount: z.number().gt(0),
})
export const addsubsidy = newEndpoint({}, async (req, auth) => {
const { amount, contractId } = validate(bodySchema, req.body)
if (!isFinite(amount) || amount < 1) throw new APIError(400, 'Invalid amount')
// run as transaction to prevent race conditions
return await firestore.runTransaction(async (transaction) => {
const userDoc = firestore.doc(`users/${auth.uid}`)
const userSnap = await transaction.get(userDoc)
if (!userSnap.exists) throw new APIError(400, 'User not found')
const user = userSnap.data() as User
const contractDoc = firestore.doc(`contracts/${contractId}`)
const contractSnap = await transaction.get(contractDoc)
if (!contractSnap.exists) throw new APIError(400, 'Invalid contract')
const contract = contractSnap.data() as Contract
if (
contract.mechanism !== 'cpmm-1' ||
(contract.outcomeType !== 'BINARY' &&
contract.outcomeType !== 'PSEUDO_NUMERIC')
)
throw new APIError(400, 'Invalid contract')
const { closeTime } = contract
if (closeTime && Date.now() > closeTime)
throw new APIError(400, 'Trading is closed')
if (user.balance < amount) throw new APIError(400, 'Insufficient balance')
const newLiquidityProvisionDoc = firestore
.collection(`contracts/${contractId}/liquidity`)
.doc()
const { newLiquidityProvision, newTotalLiquidity, newSubsidyPool } =
getNewLiquidityProvision(
user.id,
amount,
contract,
newLiquidityProvisionDoc.id
)
transaction.update(contractDoc, {
subsidyPool: newSubsidyPool,
totalLiquidity: newTotalLiquidity,
} as Partial<CPMMContract>)
const newBalance = user.balance - amount
const newTotalDeposits = user.totalDeposits - amount
if (!isFinite(newBalance)) {
throw new APIError(500, 'Invalid user balance for ' + user.username)
}
transaction.update(userDoc, {
balance: newBalance,
totalDeposits: newTotalDeposits,
})
transaction.create(newLiquidityProvisionDoc, newLiquidityProvision)
return newLiquidityProvision
})
})
const firestore = admin.firestore()

View File

@ -1,26 +0,0 @@
import * as Amplitude from '@amplitude/node'
import { DEV_CONFIG } from '../../common/envs/dev'
import { PROD_CONFIG } from '../../common/envs/prod'
import { isProd, tryOrLogError } from './utils'
const key = isProd() ? PROD_CONFIG.amplitudeApiKey : DEV_CONFIG.amplitudeApiKey
const amp = Amplitude.init(key ?? '')
export const track = async (
userId: string,
eventName: string,
eventProperties?: any,
amplitudeProperties?: Partial<Amplitude.Event>
) => {
return await tryOrLogError(
amp.logEvent({
event_type: eventName,
user_id: userId,
event_properties: eventProperties,
...amplitudeProperties,
})
)
}

View File

@ -1,30 +1,36 @@
import * as admin from 'firebase-admin' import * as admin from 'firebase-admin'
import { Request, RequestHandler, Response } from 'express' import * as functions from 'firebase-functions'
import { error } from 'firebase-functions/logger' import * as Cors from 'cors'
import { HttpsOptions } from 'firebase-functions/v2/https'
import { log } from './utils'
import { z } from 'zod' import { z } from 'zod'
import { APIError } from '../../common/api'
import { PrivateUser } from '../../common/user' import { User, PrivateUser } from '../../common/user'
import { import {
CORS_ORIGIN_MANIFOLD, CORS_ORIGIN_MANIFOLD,
CORS_ORIGIN_LOCALHOST, CORS_ORIGIN_LOCALHOST,
CORS_ORIGIN_VERCEL,
} from '../../common/envs/constants' } from '../../common/envs/constants'
export { APIError } from '../../common/api'
type Output = Record<string, unknown> type Output = Record<string, unknown>
export type AuthedUser = { type Request = functions.https.Request
uid: string type Response = functions.Response
creds: JwtCredentials | (KeyCredentials & { privateUser: PrivateUser }) type AuthedUser = [User, PrivateUser]
}
type Handler = (req: Request, user: AuthedUser) => Promise<Output> type Handler = (req: Request, user: AuthedUser) => Promise<Output>
type JwtCredentials = { kind: 'jwt'; data: admin.auth.DecodedIdToken } type JwtCredentials = { kind: 'jwt'; data: admin.auth.DecodedIdToken }
type KeyCredentials = { kind: 'key'; data: string } type KeyCredentials = { kind: 'key'; data: string }
type Credentials = JwtCredentials | KeyCredentials type Credentials = JwtCredentials | KeyCredentials
export class APIError {
code: number
msg: string
details: unknown
constructor(code: number, msg: string, details?: unknown) {
this.code = code
this.msg = msg
this.details = details
}
toJson() {}
}
export const parseCredentials = async (req: Request): Promise<Credentials> => { export const parseCredentials = async (req: Request): Promise<Credentials> => {
const auth = admin.auth()
const authHeader = req.get('Authorization') const authHeader = req.get('Authorization')
if (!authHeader) { if (!authHeader) {
throw new APIError(403, 'Missing Authorization header.') throw new APIError(403, 'Missing Authorization header.')
@ -38,10 +44,11 @@ export const parseCredentials = async (req: Request): Promise<Credentials> => {
switch (scheme) { switch (scheme) {
case 'Bearer': case 'Bearer':
try { try {
return { kind: 'jwt', data: await auth.verifyIdToken(payload) } const jwt = await admin.auth().verifyIdToken(payload)
return { kind: 'jwt', data: jwt }
} catch (err) { } catch (err) {
// This is somewhat suspicious, so get it into the firebase console // This is somewhat suspicious, so get it into the firebase console
error('Error verifying Firebase JWT: ', err) functions.logger.error('Error verifying Firebase JWT: ', err)
throw new APIError(403, 'Error validating token.') throw new APIError(403, 'Error validating token.')
} }
case 'Key': case 'Key':
@ -53,13 +60,24 @@ export const parseCredentials = async (req: Request): Promise<Credentials> => {
export const lookupUser = async (creds: Credentials): Promise<AuthedUser> => { export const lookupUser = async (creds: Credentials): Promise<AuthedUser> => {
const firestore = admin.firestore() const firestore = admin.firestore()
const users = firestore.collection('users')
const privateUsers = firestore.collection('private-users') const privateUsers = firestore.collection('private-users')
switch (creds.kind) { switch (creds.kind) {
case 'jwt': { case 'jwt': {
if (typeof creds.data.user_id !== 'string') { const { user_id } = creds.data
if (typeof user_id !== 'string') {
throw new APIError(403, 'JWT must contain Manifold user ID.') throw new APIError(403, 'JWT must contain Manifold user ID.')
} }
return { uid: creds.data.user_id, creds } const [userSnap, privateUserSnap] = await Promise.all([
users.doc(user_id).get(),
privateUsers.doc(user_id).get(),
])
if (!userSnap.exists || !privateUserSnap.exists) {
throw new APIError(403, 'No user exists with the provided ID.')
}
const user = userSnap.data() as User
const privateUser = privateUserSnap.data() as PrivateUser
return [user, privateUser]
} }
case 'key': { case 'key': {
const key = creds.data const key = creds.data
@ -67,25 +85,33 @@ export const lookupUser = async (creds: Credentials): Promise<AuthedUser> => {
if (privateUserQ.empty) { if (privateUserQ.empty) {
throw new APIError(403, `No private user exists with API key ${key}.`) throw new APIError(403, `No private user exists with API key ${key}.`)
} }
const privateUser = privateUserQ.docs[0].data() as PrivateUser const privateUserSnap = privateUserQ.docs[0]
return { uid: privateUser.id, creds: { privateUser, ...creds } } const userSnap = await users.doc(privateUserSnap.id).get()
if (!userSnap.exists) {
throw new APIError(403, `No user exists with ID ${privateUserSnap.id}.`)
}
const user = userSnap.data() as User
const privateUser = privateUserSnap.data() as PrivateUser
return [user, privateUser]
} }
default: default:
throw new APIError(500, 'Invalid credential type.') throw new APIError(500, 'Invalid credential type.')
} }
} }
export const writeResponseError = (e: unknown, res: Response) => { export const applyCors = (
if (e instanceof APIError) { req: Request,
const output: { [k: string]: unknown } = { message: e.message } res: Response,
if (e.details != null) { params: Cors.CorsOptions
output.details = e.details ) => {
} return new Promise((resolve, reject) => {
res.status(e.code).json(output) Cors(params)(req, res, (result) => {
} else { if (result instanceof Error) {
error(e) return reject(result)
res.status(500).json({ message: 'An unknown error occurred.' })
} }
return resolve(result)
})
})
} }
export const zTimestamp = () => { export const zTimestamp = () => {
@ -94,16 +120,10 @@ export const zTimestamp = () => {
}, z.date()) }, z.date())
} }
export type EndpointDefinition = {
opts: EndpointOptions & { method: string }
handler: RequestHandler
}
export const validate = <T extends z.ZodTypeAny>(schema: T, val: unknown) => { export const validate = <T extends z.ZodTypeAny>(schema: T, val: unknown) => {
const result = schema.safeParse(val) const result = schema.safeParse(val)
if (!result.success) { if (!result.success) {
const issues = result.error.issues.map((i) => { const issues = result.error.issues.map((i) => {
// TODO: export this type for the front-end to parse
return { return {
field: i.path.join('.') || null, field: i.path.join('.') || null,
error: i.message, error: i.message,
@ -115,55 +135,29 @@ export const validate = <T extends z.ZodTypeAny>(schema: T, val: unknown) => {
} }
} }
export interface EndpointOptions extends HttpsOptions { export const newEndpoint = (methods: [string], fn: Handler) =>
method?: string functions.runWith({ minInstances: 1 }).https.onRequest(async (req, res) => {
} await applyCors(req, res, {
origin: [CORS_ORIGIN_MANIFOLD, CORS_ORIGIN_LOCALHOST],
const DEFAULT_OPTS = { methods: methods,
method: 'POST', })
minInstances: 1,
concurrency: 100,
memory: '2GiB',
cpu: 1,
cors: [CORS_ORIGIN_MANIFOLD, CORS_ORIGIN_VERCEL, CORS_ORIGIN_LOCALHOST],
}
export const newEndpoint = (endpointOpts: EndpointOptions, fn: Handler) => {
const opts = Object.assign({}, DEFAULT_OPTS, endpointOpts)
return {
opts,
handler: async (req: Request, res: Response) => {
log(`${req.method} ${req.url} ${JSON.stringify(req.body)}`)
try { try {
if (opts.method !== req.method) { if (!methods.includes(req.method)) {
throw new APIError(405, `This endpoint supports only ${opts.method}.`) const allowed = methods.join(', ')
throw new APIError(405, `This endpoint supports only ${allowed}.`)
} }
const authedUser = await lookupUser(await parseCredentials(req)) const authedUser = await lookupUser(await parseCredentials(req))
res.status(200).json(await fn(req, authedUser)) res.status(200).json(await fn(req, authedUser))
} catch (e) { } catch (e) {
writeResponseError(e, res) if (e instanceof APIError) {
const output: { [k: string]: unknown } = { message: e.msg }
if (e.details != null) {
output.details = e.details
} }
}, res.status(e.code).json(output)
} as EndpointDefinition } else {
functions.logger.error(e)
res.status(500).json({ message: 'An unknown error occurred.' })
} }
export const newEndpointNoAuth = (
endpointOpts: EndpointOptions,
fn: (req: Request) => Promise<Output>
) => {
const opts = Object.assign({}, DEFAULT_OPTS, endpointOpts)
return {
opts,
handler: async (req: Request, res: Response) => {
log(`${req.method} ${req.url} ${JSON.stringify(req.body)}`)
try {
if (opts.method !== req.method) {
throw new APIError(405, `This endpoint supports only ${opts.method}.`)
}
res.status(200).json(await fn(req))
} catch (e) {
writeResponseError(e, res)
}
},
} as EndpointDefinition
} }
})

View File

@ -18,63 +18,43 @@
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'
import { FirestoreAdminClient } from '@google-cloud/firestore/types/v1/firestore_admin_client' const client = new firestore.v1.FirestoreAdminClient()
export const backupDbCore = async ( const bucket = 'gs://manifold-firestore-backup'
client: FirestoreAdminClient,
project: string, export const backupDb = functions.pubsub
bucket: string .schedule('every 24 hours')
) => { .onRun((context) => {
const name = client.databasePath(project, '(default)') const projectId = process.env.GCP_PROJECT || process.env.GCLOUD_PROJECT
const outputUriPrefix = `gs://${bucket}` 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
const collectionIds = [ collectionIds: [
'contracts', 'contracts',
'groups', 'folds',
'private-users', 'private-users',
'stripe-transactions', 'stripe-transactions',
'transactions',
'users', 'users',
'bets', 'bets',
'comments', 'comments',
'follows',
'followers', 'followers',
'answers', 'answers',
'txns', 'txns',
'manalinks', ],
'liquidity', })
'stats', .then((responses) => {
'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')
} })
}) })

Some files were not shown because too many files have changed in this diff Show More