Compare commits
2 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
a78c32714c | ||
|
7d29b742d0 |
2
.github/workflows/check.yml
vendored
2
.github/workflows/check.yml
vendored
|
@ -48,7 +48,7 @@ jobs:
|
|||
- name: Run Typescript checker on web client
|
||||
if: ${{ success() || failure() }}
|
||||
working-directory: web
|
||||
run: tsc --pretty --project tsconfig.json --noEmit
|
||||
run: tsc -b -v --pretty
|
||||
- name: Run Typescript checker on cloud functions
|
||||
if: ${{ success() || failure() }}
|
||||
working-directory: functions
|
||||
|
|
43
.github/workflows/format.yml
vendored
43
.github/workflows/format.yml
vendored
|
@ -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 }}
|
43
.github/workflows/lint.yml
vendored
43
.github/workflows/lint.yml
vendored
|
@ -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 }}
|
17
.github/workflows/merge-main-into-main2.yml
vendored
17
.github/workflows/merge-main-into-main2.yml
vendored
|
@ -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 }}
|
|
@ -1,7 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: ['lodash', 'unused-imports'],
|
||||
plugins: ['lodash'],
|
||||
extends: ['eslint:recommended'],
|
||||
ignorePatterns: ['lib'],
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
|
@ -26,14 +25,12 @@ module.exports = {
|
|||
caughtErrorsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
'unused-imports/no-unused-imports': 'warn',
|
||||
},
|
||||
},
|
||||
],
|
||||
rules: {
|
||||
'no-extra-semi': 'off',
|
||||
'no-constant-condition': ['error', { checkLoops: false }],
|
||||
'linebreak-style': ['error', 'unix'],
|
||||
'lodash/import-scope': [2, 'member'],
|
||||
},
|
||||
}
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
save-prefix ""
|
|
@ -1,30 +1,33 @@
|
|||
import { getCpmmLiquidity } from './calculate-cpmm'
|
||||
import { addCpmmLiquidity, getCpmmLiquidity } from './calculate-cpmm'
|
||||
import { CPMMContract } from './contract'
|
||||
import { LiquidityProvision } from './liquidity-provision'
|
||||
import { User } from './user'
|
||||
|
||||
export const getNewLiquidityProvision = (
|
||||
userId: string,
|
||||
user: User,
|
||||
amount: number,
|
||||
contract: CPMMContract,
|
||||
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 = {
|
||||
id: newLiquidityProvisionId,
|
||||
userId: userId,
|
||||
userId: user.id,
|
||||
contractId: contract.id,
|
||||
amount,
|
||||
pool,
|
||||
p,
|
||||
pool: newPool,
|
||||
p: newP,
|
||||
liquidity,
|
||||
createdTime: Date.now(),
|
||||
}
|
||||
|
||||
const newTotalLiquidity = (totalLiquidity ?? 0) + amount
|
||||
const newSubsidyPool = (subsidyPool ?? 0) + amount
|
||||
|
||||
return { newLiquidityProvision, newTotalLiquidity, newSubsidyPool }
|
||||
return { newLiquidityProvision, newPool, newP, newTotalLiquidity }
|
||||
}
|
||||
|
|
|
@ -5,22 +5,19 @@ import {
|
|||
CPMMBinaryContract,
|
||||
DPMBinaryContract,
|
||||
FreeResponseContract,
|
||||
MultipleChoiceContract,
|
||||
NumericContract,
|
||||
} from './contract'
|
||||
import { User } from './user'
|
||||
import { LiquidityProvision } from './liquidity-provision'
|
||||
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 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(
|
||||
providerId: string,
|
||||
|
@ -57,7 +54,7 @@ export function getAnteBets(
|
|||
|
||||
const { createdTime } = contract
|
||||
|
||||
const yesBet: NormalizedBet = {
|
||||
const yesBet: Bet = {
|
||||
id: yesAnteId,
|
||||
userId: creator.id,
|
||||
contractId: contract.id,
|
||||
|
@ -71,7 +68,7 @@ export function getAnteBets(
|
|||
fees: noFees,
|
||||
}
|
||||
|
||||
const noBet: NormalizedBet = {
|
||||
const noBet: Bet = {
|
||||
id: noAnteId,
|
||||
userId: creator.id,
|
||||
contractId: contract.id,
|
||||
|
@ -99,7 +96,7 @@ export function getFreeAnswerAnte(
|
|||
|
||||
const { createdTime } = contract
|
||||
|
||||
const anteBet: NormalizedBet = {
|
||||
const anteBet: Bet = {
|
||||
id: anteBetId,
|
||||
userId: anteBettorId,
|
||||
contractId: contract.id,
|
||||
|
@ -116,50 +113,6 @@ export function getFreeAnswerAnte(
|
|||
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(
|
||||
anteBettorId: string,
|
||||
contract: NumericContract,
|
||||
|
@ -179,7 +132,7 @@ export function getNumericAnte(
|
|||
range(0, bucketCount).map((_, i) => [i, betAnte])
|
||||
)
|
||||
|
||||
const anteBet: NormalizedBet<NumericBet> = {
|
||||
const anteBet: NumericBet = {
|
||||
id: newBetId,
|
||||
userId: anteBettorId,
|
||||
contractId: contract.id,
|
||||
|
|
|
@ -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`
|
||||
}
|
||||
}
|
123
common/badge.ts
123
common/badge.ts
|
@ -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
|
||||
}
|
|
@ -3,14 +3,7 @@ import { Fees } from './fees'
|
|||
export type Bet = {
|
||||
id: string
|
||||
userId: string
|
||||
|
||||
// denormalized for bet lists
|
||||
userAvatarUrl?: string
|
||||
userUsername: string
|
||||
userName: string
|
||||
|
||||
contractId: string
|
||||
createdTime: number
|
||||
|
||||
amount: number // bet size; negative if SELL bet
|
||||
loanAmount?: number
|
||||
|
@ -20,22 +13,21 @@ export type Bet = {
|
|||
probBefore: number
|
||||
probAfter: number
|
||||
|
||||
sale?: {
|
||||
amount: number // amount user makes from sale
|
||||
betId: string // id of bet being sold
|
||||
// TODO: add sale time?
|
||||
}
|
||||
|
||||
fees: Fees
|
||||
|
||||
isSold?: boolean // true if this BUY bet has been sold
|
||||
isAnte?: boolean
|
||||
isLiquidityProvision?: boolean
|
||||
isRedemption?: boolean
|
||||
challengeSlug?: string
|
||||
|
||||
// Props for bets in DPM contract below.
|
||||
// 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>
|
||||
createdTime: number
|
||||
}
|
||||
|
||||
export type NumericBet = Bet & {
|
||||
value: number
|
||||
|
@ -43,27 +35,4 @@ export type NumericBet = Bet & {
|
|||
allBetAmounts: { [outcome: string]: number }
|
||||
}
|
||||
|
||||
// Binary market limit order.
|
||||
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
|
||||
}
|
||||
export const MAX_LOAN_PER_CONTRACT = 20
|
||||
|
|
|
@ -1,15 +1,9 @@
|
|||
import { groupBy, mapValues, sumBy } from 'lodash'
|
||||
import { LimitBet } from './bet'
|
||||
import { sum, groupBy, mapValues, sumBy, partition } from 'lodash'
|
||||
|
||||
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 { computeFills } from './new-bet'
|
||||
import { binarySearch } from './util/algos'
|
||||
|
||||
export type CpmmState = {
|
||||
pool: { [outcome: string]: number }
|
||||
p: number
|
||||
}
|
||||
import { addObjects } from './util/object'
|
||||
|
||||
export function getCpmmProbability(
|
||||
pool: { [outcome: string]: number },
|
||||
|
@ -20,11 +14,11 @@ export function getCpmmProbability(
|
|||
}
|
||||
|
||||
export function getCpmmProbabilityAfterBetBeforeFees(
|
||||
state: CpmmState,
|
||||
contract: CPMMContract,
|
||||
outcome: string,
|
||||
bet: number
|
||||
) {
|
||||
const { pool, p } = state
|
||||
const { pool, p } = contract
|
||||
const shares = calculateCpmmShares(pool, p, bet, outcome)
|
||||
const { YES: y, NO: n } = pool
|
||||
|
||||
|
@ -37,12 +31,12 @@ export function getCpmmProbabilityAfterBetBeforeFees(
|
|||
}
|
||||
|
||||
export function getCpmmOutcomeProbabilityAfterBet(
|
||||
state: CpmmState,
|
||||
contract: CPMMContract,
|
||||
outcome: string,
|
||||
bet: number
|
||||
) {
|
||||
const { newPool } = calculateCpmmPurchase(state, bet, outcome)
|
||||
const p = getCpmmProbability(newPool, state.p)
|
||||
const { newPool } = calculateCpmmPurchase(contract, bet, outcome)
|
||||
const p = getCpmmProbability(newPool, contract.p)
|
||||
return outcome === 'NO' ? 1 - p : p
|
||||
}
|
||||
|
||||
|
@ -64,8 +58,12 @@ function calculateCpmmShares(
|
|||
: n + bet - (k * (bet + y) ** -p) ** (1 / (1 - p))
|
||||
}
|
||||
|
||||
export function getCpmmFees(state: CpmmState, bet: number, outcome: string) {
|
||||
const prob = getCpmmProbabilityAfterBetBeforeFees(state, outcome, bet)
|
||||
export function getCpmmLiquidityFee(
|
||||
contract: CPMMContract,
|
||||
bet: number,
|
||||
outcome: string
|
||||
) {
|
||||
const prob = getCpmmProbabilityAfterBetBeforeFees(contract, outcome, bet)
|
||||
const betP = outcome === 'YES' ? 1 - prob : prob
|
||||
|
||||
const liquidityFee = LIQUIDITY_FEE * betP * bet
|
||||
|
@ -80,23 +78,25 @@ export function getCpmmFees(state: CpmmState, bet: number, outcome: string) {
|
|||
}
|
||||
|
||||
export function calculateCpmmSharesAfterFee(
|
||||
state: CpmmState,
|
||||
contract: CPMMContract,
|
||||
bet: number,
|
||||
outcome: string
|
||||
) {
|
||||
const { pool, p } = state
|
||||
const { remainingBet } = getCpmmFees(state, bet, outcome)
|
||||
const { pool, p } = contract
|
||||
const { remainingBet } = getCpmmLiquidityFee(contract, bet, outcome)
|
||||
|
||||
return calculateCpmmShares(pool, p, remainingBet, outcome)
|
||||
}
|
||||
|
||||
export function calculateCpmmPurchase(
|
||||
state: CpmmState,
|
||||
contract: CPMMContract,
|
||||
bet: number,
|
||||
outcome: string
|
||||
) {
|
||||
const { pool, p } = state
|
||||
const { remainingBet, fees } = getCpmmFees(state, bet, outcome)
|
||||
const { pool, p } = contract
|
||||
const { remainingBet, fees } = getCpmmLiquidityFee(contract, bet, outcome)
|
||||
// const remainingBet = bet
|
||||
// const fees = noFees
|
||||
|
||||
const shares = calculateCpmmShares(pool, p, remainingBet, outcome)
|
||||
const { YES: y, NO: n } = pool
|
||||
|
@ -115,125 +115,119 @@ export function calculateCpmmPurchase(
|
|||
return { shares, newPool, newP, fees }
|
||||
}
|
||||
|
||||
// Note: there might be a closed form solution for this.
|
||||
// If so, feel free to switch out this implementation.
|
||||
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 computeK(y: number, n: number, p: number) {
|
||||
return y ** p * n ** (1 - p)
|
||||
}
|
||||
|
||||
function calculateAmountToBuyShares(
|
||||
state: CpmmState,
|
||||
shares: number,
|
||||
function sellSharesK(
|
||||
y: number,
|
||||
n: number,
|
||||
p: number,
|
||||
s: number,
|
||||
outcome: 'YES' | 'NO',
|
||||
unfilledBets: LimitBet[],
|
||||
balanceByUserId: { [userId: string]: number }
|
||||
b: number
|
||||
) {
|
||||
// Search for amount between bounds (0, shares).
|
||||
// Min share price is M$0, and max is M$1 each.
|
||||
return binarySearch(0, shares, (amount) => {
|
||||
const { takers } = computeFills(
|
||||
outcome,
|
||||
amount,
|
||||
state,
|
||||
undefined,
|
||||
unfilledBets,
|
||||
balanceByUserId
|
||||
)
|
||||
return outcome === 'YES'
|
||||
? computeK(y - b + s, n - b, p)
|
||||
: computeK(y - b, n - b + s, p)
|
||||
}
|
||||
|
||||
const totalShares = sumBy(takers, (taker) => taker.shares)
|
||||
return totalShares - shares
|
||||
})
|
||||
function calculateCpmmShareValue(
|
||||
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(
|
||||
state: CpmmState,
|
||||
contract: CPMMContract,
|
||||
shares: number,
|
||||
outcome: 'YES' | 'NO',
|
||||
unfilledBets: LimitBet[],
|
||||
balanceByUserId: { [userId: string]: number }
|
||||
outcome: string
|
||||
) {
|
||||
if (Math.round(shares) < 0) {
|
||||
throw new Error('Cannot sell non-positive shares')
|
||||
}
|
||||
|
||||
const oppositeOutcome = outcome === 'YES' ? 'NO' : 'YES'
|
||||
const buyAmount = calculateAmountToBuyShares(
|
||||
state,
|
||||
const saleValue = calculateCpmmShareValue(
|
||||
contract,
|
||||
shares,
|
||||
oppositeOutcome,
|
||||
unfilledBets,
|
||||
balanceByUserId
|
||||
outcome as 'YES' | 'NO'
|
||||
)
|
||||
|
||||
const { cpmmState, makers, takers, totalFees, ordersToCancel } = computeFills(
|
||||
oppositeOutcome,
|
||||
buyAmount,
|
||||
state,
|
||||
undefined,
|
||||
unfilledBets,
|
||||
balanceByUserId
|
||||
)
|
||||
const fees = noFees
|
||||
|
||||
// Transform buys of opposite outcome into sells.
|
||||
const saleTakers = takers.map((taker) => ({
|
||||
...taker,
|
||||
// You bought opposite shares, which combine with existing shares, removing them.
|
||||
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 { fees, remainingBet: saleValue } = getCpmmLiquidityFee(
|
||||
// contract,
|
||||
// rawSaleValue,
|
||||
// outcome === 'YES' ? 'NO' : 'YES'
|
||||
// )
|
||||
|
||||
const saleValue = -sumBy(saleTakers, (taker) => taker.amount)
|
||||
const { pool } = contract
|
||||
const { YES: y, NO: n } = pool
|
||||
|
||||
return {
|
||||
saleValue,
|
||||
cpmmState,
|
||||
fees: totalFees,
|
||||
makers,
|
||||
takers: saleTakers,
|
||||
ordersToCancel,
|
||||
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,
|
||||
fee,
|
||||
outcome,
|
||||
})
|
||||
throw new Error('Cannot sell more than in pool')
|
||||
}
|
||||
|
||||
const postBetPool = { YES: newY, NO: newN }
|
||||
|
||||
const { newPool, newP } = addCpmmLiquidity(postBetPool, contract.p, fee)
|
||||
|
||||
return { saleValue, newPool, newP, fees }
|
||||
}
|
||||
|
||||
export function getCpmmProbabilityAfterSale(
|
||||
state: CpmmState,
|
||||
contract: CPMMContract,
|
||||
shares: number,
|
||||
outcome: 'YES' | 'NO',
|
||||
unfilledBets: LimitBet[],
|
||||
balanceByUserId: { [userId: string]: number }
|
||||
outcome: 'YES' | 'NO'
|
||||
) {
|
||||
const { cpmmState } = calculateCpmmSale(
|
||||
state,
|
||||
shares,
|
||||
outcome,
|
||||
unfilledBets,
|
||||
balanceByUserId
|
||||
)
|
||||
return getCpmmProbability(cpmmState.pool, cpmmState.p)
|
||||
const { newPool } = calculateCpmmSale(contract, shares, outcome)
|
||||
return getCpmmProbability(newPool, contract.p)
|
||||
}
|
||||
|
||||
export function getCpmmLiquidity(
|
||||
|
@ -266,23 +260,46 @@ export function addCpmmLiquidity(
|
|||
return { newPool, liquidity, newP }
|
||||
}
|
||||
|
||||
export function getCpmmLiquidityPoolWeights(liquidities: LiquidityProvision[]) {
|
||||
const userAmounts = groupBy(liquidities, (w) => w.userId)
|
||||
const totalAmount = sumBy(liquidities, (w) => w.amount)
|
||||
const calculateLiquidityDelta = (p: number) => (l: LiquidityProvision) => {
|
||||
const oldLiquidity = getCpmmLiquidity(l.pool, p)
|
||||
|
||||
return mapValues(
|
||||
userAmounts,
|
||||
(amounts) => sumBy(amounts, (w) => w.amount) / totalAmount
|
||||
const newPool = addObjects(l.pool, { YES: l.amount, NO: l.amount })
|
||||
const newLiquidity = getCpmmLiquidity(newPool, p)
|
||||
|
||||
const liquidity = newLiquidity - oldLiquidity
|
||||
return liquidity
|
||||
}
|
||||
|
||||
export function getCpmmLiquidityPoolWeights(
|
||||
contract: CPMMContract,
|
||||
liquidities: LiquidityProvision[]
|
||||
) {
|
||||
const [antes, nonAntes] = partition(liquidities, (l) => !!l.isAnte)
|
||||
|
||||
const calcLiqudity = calculateLiquidityDelta(contract.p)
|
||||
const liquidityShares = nonAntes.map(calcLiqudity)
|
||||
|
||||
const shareSum = sum(liquidityShares) + sum(antes.map(calcLiqudity))
|
||||
|
||||
const weights = liquidityShares.map((s, i) => ({
|
||||
weight: s / shareSum,
|
||||
providerId: nonAntes[i].userId,
|
||||
}))
|
||||
|
||||
const userWeights = groupBy(weights, (w) => w.providerId)
|
||||
const totalUserWeights = mapValues(userWeights, (userWeight) =>
|
||||
sumBy(userWeight, (w) => w.weight)
|
||||
)
|
||||
return totalUserWeights
|
||||
}
|
||||
|
||||
export function getUserLiquidityShares(
|
||||
userId: string,
|
||||
state: CpmmState,
|
||||
contract: CPMMContract,
|
||||
liquidities: LiquidityProvision[]
|
||||
) {
|
||||
const weights = getCpmmLiquidityPoolWeights(liquidities)
|
||||
const weights = getCpmmLiquidityPoolWeights(contract, liquidities)
|
||||
const userWeight = weights[userId] ?? 0
|
||||
|
||||
return mapValues(state.pool, (shares) => userWeight * shares)
|
||||
return mapValues(contract.pool, (shares) => userWeight * shares)
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import { cloneDeep, range, sum, sumBy, sortBy, mapValues } from 'lodash'
|
|||
import { Bet, NumericBet } from './bet'
|
||||
import { DPMContract, DPMBinaryContract, NumericContract } from './contract'
|
||||
import { DPM_FEES } from './fees'
|
||||
import { normpdf } from './util/math'
|
||||
import { normpdf } from '../common/util/math'
|
||||
import { addObjects } from './util/object'
|
||||
|
||||
export function getDpmProbability(totalShares: { [outcome: string]: number }) {
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import { maxBy, partition, sortBy, sum, sumBy } from 'lodash'
|
||||
import { Bet, LimitBet } from './bet'
|
||||
import { maxBy } from 'lodash'
|
||||
import { Bet } from './bet'
|
||||
import {
|
||||
calculateCpmmSale,
|
||||
getCpmmProbability,
|
||||
|
@ -18,26 +18,15 @@ import {
|
|||
getDpmProbabilityAfterSale,
|
||||
} from './calculate-dpm'
|
||||
import { calculateFixedPayout } from './calculate-fixed-payouts'
|
||||
import {
|
||||
Contract,
|
||||
BinaryContract,
|
||||
FreeResponseContract,
|
||||
PseudoNumericContract,
|
||||
MultipleChoiceContract,
|
||||
} from './contract'
|
||||
import { floatingEqual } from './util/math'
|
||||
import { Contract, BinaryContract, FreeResponseContract } from './contract'
|
||||
|
||||
export function getProbability(
|
||||
contract: BinaryContract | PseudoNumericContract
|
||||
) {
|
||||
export function getProbability(contract: BinaryContract) {
|
||||
return contract.mechanism === 'cpmm-1'
|
||||
? getCpmmProbability(contract.pool, contract.p)
|
||||
: getDpmProbability(contract.totalShares)
|
||||
}
|
||||
|
||||
export function getInitialProbability(
|
||||
contract: BinaryContract | PseudoNumericContract
|
||||
) {
|
||||
export function getInitialProbability(contract: BinaryContract) {
|
||||
if (contract.initialProbability) return contract.initialProbability
|
||||
|
||||
if (contract.mechanism === 'dpm-2' || (contract as any).totalShares)
|
||||
|
@ -75,22 +64,9 @@ export function calculateShares(
|
|||
: calculateDpmShares(contract.totalShares, bet, betChoice)
|
||||
}
|
||||
|
||||
export function calculateSaleAmount(
|
||||
contract: Contract,
|
||||
bet: Bet,
|
||||
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
|
||||
export function calculateSaleAmount(contract: Contract, bet: Bet) {
|
||||
return contract.mechanism === 'cpmm-1' && contract.outcomeType === 'BINARY'
|
||||
? calculateCpmmSale(contract, Math.abs(bet.shares), bet.outcome).saleValue
|
||||
: calculateDpmSaleAmount(contract, bet)
|
||||
}
|
||||
|
||||
|
@ -103,25 +79,15 @@ export function calculatePayoutAfterCorrectBet(contract: Contract, bet: Bet) {
|
|||
export function getProbabilityAfterSale(
|
||||
contract: Contract,
|
||||
outcome: string,
|
||||
shares: number,
|
||||
unfilledBets: LimitBet[],
|
||||
balanceByUserId: { [userId: string]: number }
|
||||
shares: number
|
||||
) {
|
||||
return contract.mechanism === 'cpmm-1'
|
||||
? getCpmmProbabilityAfterSale(
|
||||
contract,
|
||||
shares,
|
||||
outcome as 'YES' | 'NO',
|
||||
unfilledBets,
|
||||
balanceByUserId
|
||||
)
|
||||
? getCpmmProbabilityAfterSale(contract, shares, outcome as 'YES' | 'NO')
|
||||
: getDpmProbabilityAfterSale(contract.totalShares, outcome, shares)
|
||||
}
|
||||
|
||||
export function calculatePayout(contract: Contract, bet: Bet, outcome: string) {
|
||||
return contract.mechanism === 'cpmm-1' &&
|
||||
(contract.outcomeType === 'BINARY' ||
|
||||
contract.outcomeType === 'PSEUDO_NUMERIC')
|
||||
return contract.mechanism === 'cpmm-1' && contract.outcomeType === 'BINARY'
|
||||
? calculateFixedPayout(contract, bet, outcome)
|
||||
: calculateDpmPayout(contract, bet, outcome)
|
||||
}
|
||||
|
@ -130,60 +96,15 @@ export function resolvedPayout(contract: Contract, bet: Bet) {
|
|||
const outcome = contract.resolution
|
||||
if (!outcome) throw new Error('Contract not resolved')
|
||||
|
||||
return contract.mechanism === 'cpmm-1' &&
|
||||
(contract.outcomeType === 'BINARY' ||
|
||||
contract.outcomeType === 'PSEUDO_NUMERIC')
|
||||
return contract.mechanism === 'cpmm-1' && contract.outcomeType === 'BINARY'
|
||||
? calculateFixedPayout(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[]) {
|
||||
const { resolution } = contract
|
||||
const isCpmm = contract.mechanism === 'cpmm-1'
|
||||
|
||||
let currentInvested = 0
|
||||
let totalInvested = 0
|
||||
let payout = 0
|
||||
let loan = 0
|
||||
|
@ -209,6 +130,7 @@ export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) {
|
|||
saleValue -= amount
|
||||
}
|
||||
|
||||
currentInvested += amount
|
||||
loan += loanAmount ?? 0
|
||||
payout += resolution
|
||||
? calculatePayout(contract, bet, resolution)
|
||||
|
@ -216,18 +138,18 @@ export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) {
|
|||
}
|
||||
}
|
||||
|
||||
const netPayout = payout - loan
|
||||
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)
|
||||
(shares) => shares > 0
|
||||
)
|
||||
|
||||
return {
|
||||
invested,
|
||||
loan,
|
||||
invested: Math.max(0, currentInvested),
|
||||
payout,
|
||||
netPayout,
|
||||
profit,
|
||||
profitPercent,
|
||||
totalShares,
|
||||
|
@ -238,8 +160,8 @@ export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) {
|
|||
export function getContractBetNullMetrics() {
|
||||
return {
|
||||
invested: 0,
|
||||
loan: 0,
|
||||
payout: 0,
|
||||
netPayout: 0,
|
||||
profit: 0,
|
||||
profitPercent: 0,
|
||||
totalShares: {} as { [outcome: string]: number },
|
||||
|
@ -247,9 +169,7 @@ export function getContractBetNullMetrics() {
|
|||
}
|
||||
}
|
||||
|
||||
export function getTopAnswer(
|
||||
contract: FreeResponseContract | MultipleChoiceContract
|
||||
) {
|
||||
export function getTopAnswer(contract: FreeResponseContract) {
|
||||
const { answers } = contract
|
||||
const top = maxBy(
|
||||
answers?.map((answer) => ({
|
||||
|
@ -260,43 +180,3 @@ export function getTopAnswer(
|
|||
)
|
||||
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 }
|
||||
}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import { difference } from 'lodash'
|
||||
|
||||
export const CATEGORIES_GROUP_SLUG_POSTFIX = '-default'
|
||||
|
||||
export const CATEGORIES = {
|
||||
politics: 'Politics',
|
||||
technology: 'Technology',
|
||||
|
@ -26,18 +24,9 @@ export const TO_CATEGORY = Object.fromEntries(
|
|||
|
||||
export const CATEGORY_LIST = Object.keys(CATEGORIES)
|
||||
|
||||
export const EXCLUDED_CATEGORIES: category[] = [
|
||||
'fun',
|
||||
'manifold',
|
||||
'personal',
|
||||
'covid',
|
||||
'gaming',
|
||||
'crypto',
|
||||
]
|
||||
export const EXCLUDED_CATEGORIES: category[] = ['fun', 'manifold', 'personal']
|
||||
|
||||
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],
|
||||
}))
|
||||
export const DEFAULT_CATEGORIES = difference(
|
||||
CATEGORY_LIST,
|
||||
EXCLUDED_CATEGORIES
|
||||
)
|
||||
|
|
|
@ -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
|
|
@ -169,7 +169,7 @@ export const charities: Charity[] = [
|
|||
{
|
||||
name: "Founder's Pledge 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:
|
||||
'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.
|
||||
|
@ -183,7 +183,7 @@ export const charities: Charity[] = [
|
|||
{
|
||||
name: "Founder's Pledge 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:
|
||||
'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.
|
||||
|
@ -300,29 +300,10 @@ Future plans: We expect to focus on similar theoretical problems in alignment un
|
|||
name: 'Wild Animal Initiative',
|
||||
website: 'https://www.wildanimalinitiative.org/',
|
||||
ein: '82-2281466',
|
||||
tags: ['Featured'] as CharityTag[],
|
||||
photo: 'https://i.imgur.com/bOVUnDm.png',
|
||||
preview:
|
||||
'Our mission is to understand and improve the lives of wild animals.',
|
||||
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.
|
||||
|
||||
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.`,
|
||||
preview: 'We want to make life better for wild animals.',
|
||||
description:
|
||||
'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.',
|
||||
},
|
||||
{
|
||||
name: 'New Incentives',
|
||||
|
@ -535,69 +516,6 @@ The American Civil Liberties Union is our nation's guardian of liberty, working
|
|||
|
||||
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 don’t 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 world’s richest often overlooks the world’s 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 Fund’s 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 Fund’s 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 state’s 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) => {
|
||||
const slug = charity.name.toLowerCase().replace(/\s/g, '-')
|
||||
return {
|
||||
|
|
|
@ -1,56 +1,19 @@
|
|||
import type { JSONContent } from '@tiptap/core'
|
||||
|
||||
export type AnyCommentType = OnContract | OnGroup | OnPost
|
||||
|
||||
// Currently, comments are created after the bet, not atomically with the bet.
|
||||
// They're uniquely identified by the pair contractId/betId.
|
||||
export type Comment<T extends AnyCommentType = AnyCommentType> = {
|
||||
export type Comment = {
|
||||
id: string
|
||||
contractId?: string
|
||||
groupId?: string
|
||||
betId?: string
|
||||
answerOutcome?: string
|
||||
replyToCommentId?: string
|
||||
userId: string
|
||||
|
||||
/** @deprecated - content now stored as JSON in content*/
|
||||
text?: string
|
||||
content: JSONContent
|
||||
text: string
|
||||
createdTime: number
|
||||
|
||||
// Denormalized, for rendering comments
|
||||
userName: string
|
||||
userUsername: 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>
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -1,23 +1,13 @@
|
|||
import { Answer } from './answer'
|
||||
import { Fees } from './fees'
|
||||
import { JSONContent } from '@tiptap/core'
|
||||
import { GroupLink } from 'common/group'
|
||||
|
||||
export type AnyMechanism = DPM | CPMM
|
||||
export type AnyOutcomeType =
|
||||
| Binary
|
||||
| MultipleChoice
|
||||
| PseudoNumeric
|
||||
| FreeResponse
|
||||
| Numeric
|
||||
|
||||
export type AnyOutcomeType = Binary | FreeResponse | Numeric
|
||||
export type AnyContractType =
|
||||
| (CPMM & Binary)
|
||||
| (CPMM & PseudoNumeric)
|
||||
| (DPM & Binary)
|
||||
| (DPM & FreeResponse)
|
||||
| (DPM & Numeric)
|
||||
| (DPM & MultipleChoice)
|
||||
|
||||
export type Contract<T extends AnyContractType = AnyContractType> = {
|
||||
id: string
|
||||
|
@ -29,10 +19,10 @@ export type Contract<T extends AnyContractType = AnyContractType> = {
|
|||
creatorAvatarUrl?: 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[]
|
||||
lowercaseTags: string[]
|
||||
visibility: visibility
|
||||
visibility: 'public' | 'unlisted'
|
||||
|
||||
createdTime: number // Milliseconds since epoch
|
||||
lastUpdatedTime?: number // Updated on new bet or comment
|
||||
|
@ -43,37 +33,20 @@ export type Contract<T extends AnyContractType = AnyContractType> = {
|
|||
isResolved: boolean
|
||||
resolutionTime?: number // When the contract creator resolved the market
|
||||
resolution?: string
|
||||
resolutionProbability?: number
|
||||
resolutionProbability?: number,
|
||||
|
||||
closeEmailsSent?: number
|
||||
|
||||
volume: number
|
||||
volume24Hours: number
|
||||
volume7Days: number
|
||||
elasticity: number
|
||||
|
||||
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
|
||||
|
||||
export type BinaryContract = Contract & Binary
|
||||
export type PseudoNumericContract = Contract & PseudoNumeric
|
||||
export type NumericContract = Contract & Numeric
|
||||
export type FreeResponseContract = Contract & FreeResponse
|
||||
export type MultipleChoiceContract = Contract & MultipleChoice
|
||||
export type DPMContract = Contract & DPM
|
||||
export type CPMMContract = Contract & CPMM
|
||||
export type DPMBinaryContract = BinaryContract & DPM
|
||||
|
@ -92,14 +65,7 @@ export type CPMM = {
|
|||
mechanism: 'cpmm-1'
|
||||
pool: { [outcome: string]: number }
|
||||
p: number // probability constant in y^p * n^(1-p) = k
|
||||
totalLiquidity: number // for historical reasons, this the total subsidy amount added in M$
|
||||
subsidyPool: number // current value of subsidy pool in M$
|
||||
prob: number
|
||||
probChanges: {
|
||||
day: number
|
||||
week: number
|
||||
month: number
|
||||
}
|
||||
totalLiquidity: number // in M$
|
||||
}
|
||||
|
||||
export type Binary = {
|
||||
|
@ -109,18 +75,6 @@ export type Binary = {
|
|||
resolution?: resolution
|
||||
}
|
||||
|
||||
export type PseudoNumeric = {
|
||||
outcomeType: 'PSEUDO_NUMERIC'
|
||||
min: number
|
||||
max: number
|
||||
isLogScale: boolean
|
||||
resolutionValue?: number
|
||||
|
||||
// same as binary market; map everything to probability
|
||||
initialProbability: number
|
||||
resolutionProbability?: number
|
||||
}
|
||||
|
||||
export type FreeResponse = {
|
||||
outcomeType: 'FREE_RESPONSE'
|
||||
answers: Answer[] // Used for outcomeType 'FREE_RESPONSE'.
|
||||
|
@ -128,13 +82,6 @@ export type FreeResponse = {
|
|||
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 = {
|
||||
outcomeType: 'NUMERIC'
|
||||
bucketCount: number
|
||||
|
@ -147,19 +94,10 @@ export type Numeric = {
|
|||
export type outcomeType = AnyOutcomeType['outcomeType']
|
||||
export type resolution = 'YES' | 'NO' | 'MKT' | 'CANCEL'
|
||||
export const RESOLUTIONS = ['YES', 'NO', 'MKT', 'CANCEL'] as const
|
||||
export const OUTCOME_TYPES = [
|
||||
'BINARY',
|
||||
'MULTIPLE_CHOICE',
|
||||
'FREE_RESPONSE',
|
||||
'PSEUDO_NUMERIC',
|
||||
'NUMERIC',
|
||||
] as const
|
||||
export const OUTCOME_TYPES = ['BINARY', 'FREE_RESPONSE', 'NUMERIC'] as const
|
||||
|
||||
export const MAX_QUESTION_LENGTH = 240
|
||||
export const MAX_DESCRIPTION_LENGTH = 16000
|
||||
export const MAX_QUESTION_LENGTH = 480
|
||||
export const MAX_DESCRIPTION_LENGTH = 10000
|
||||
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
|
||||
|
|
|
@ -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
|
|
@ -21,38 +21,18 @@ export function isWhitelisted(email?: string) {
|
|||
}
|
||||
|
||||
// TODO: Before open sourcing, we should turn these into env vars
|
||||
export function isAdmin(email?: string) {
|
||||
if (!email) {
|
||||
return false
|
||||
}
|
||||
export function isAdmin(email: string) {
|
||||
return ENV_CONFIG.adminEmails.includes(email)
|
||||
}
|
||||
|
||||
export function isManifoldId(userId: string) {
|
||||
return userId === 'IPTOzEqrpkWmEzh6hwvAyY9PqFb2'
|
||||
}
|
||||
|
||||
export const DOMAIN = ENV_CONFIG.domain
|
||||
export const FIREBASE_CONFIG = ENV_CONFIG.firebaseConfig
|
||||
export const PROJECT_ID = ENV_CONFIG.firebaseConfig.projectId
|
||||
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
|
||||
export const CORS_ORIGIN_MANIFOLD = new RegExp(
|
||||
'^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
|
||||
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}`
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ import { EnvConfig, PROD_CONFIG } from './prod'
|
|||
|
||||
export const DEV_CONFIG: EnvConfig = {
|
||||
...PROD_CONFIG,
|
||||
domain: 'dev.manifold.markets',
|
||||
firebaseConfig: {
|
||||
apiKey: 'AIzaSyBoq3rzUa8Ekyo3ZaTnlycQYPRCA26VpOw',
|
||||
authDomain: 'dev-mantic-markets.firebaseapp.com',
|
||||
|
@ -16,6 +15,4 @@ export const DEV_CONFIG: EnvConfig = {
|
|||
cloudRunId: 'w3txbmd3ba',
|
||||
cloudRunRegion: 'uc',
|
||||
amplitudeApiKey: 'fd8cbfd964b9a205b8678a39faae71b3',
|
||||
twitchBotEndpoint: 'https://dev-twitch-bot.manifold.markets',
|
||||
sprigEnvironmentId: 'Tu7kRZPm7daP',
|
||||
}
|
||||
|
|
|
@ -2,8 +2,6 @@ export type EnvConfig = {
|
|||
domain: string
|
||||
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
|
||||
|
@ -17,38 +15,16 @@ export type EnvConfig = {
|
|||
|
||||
// Branding
|
||||
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
|
||||
navbarLogoPath?: 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 = {
|
||||
apiKey: string
|
||||
authDomain: string
|
||||
projectId: string
|
||||
region?: string
|
||||
region: string
|
||||
storageBucket: string
|
||||
messagingSenderId: string
|
||||
appId: string
|
||||
|
@ -58,7 +34,6 @@ type FirebaseConfig = {
|
|||
export const PROD_CONFIG: EnvConfig = {
|
||||
domain: 'manifold.markets',
|
||||
amplitudeApiKey: '2d6509fd4185ebb8be29709842752a15',
|
||||
sprigEnvironmentId: 'sQcrq9TDqkib',
|
||||
|
||||
firebaseConfig: {
|
||||
apiKey: 'AIzaSyDp3J57vLeAZCzxLD-vcPaGIkAmBoGOSYw',
|
||||
|
@ -70,7 +45,6 @@ export const PROD_CONFIG: EnvConfig = {
|
|||
appId: '1:128925704902:web:f61f86944d8ffa2a642dc7',
|
||||
measurementId: 'G-SSFK1Q138D',
|
||||
},
|
||||
twitchBotEndpoint: 'https://twitch-bot.manifold.markets',
|
||||
cloudRunId: 'nggbo3neva',
|
||||
cloudRunRegion: 'uc',
|
||||
adminEmails: [
|
||||
|
@ -79,17 +53,10 @@ export const PROD_CONFIG: EnvConfig = {
|
|||
'taowell@gmail.com', // Stephen
|
||||
'abc.sinclair@gmail.com', // Sinclair
|
||||
'manticmarkets@gmail.com', // Manifold
|
||||
'iansphilips@gmail.com', // Ian
|
||||
'd4vidchee@gmail.com', // D4vid
|
||||
'federicoruizcassarino@gmail.com', // Fede
|
||||
'ingawei@gmail.com', //Inga
|
||||
],
|
||||
visibility: 'PUBLIC',
|
||||
|
||||
moneyMoniker: 'M$',
|
||||
bettor: 'trader',
|
||||
pastBet: 'trade',
|
||||
presentBet: 'trade',
|
||||
navbarLogoPath: '',
|
||||
faviconPath: '/favicon.ico',
|
||||
newQuestionPlaceholders: [
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
export const FLAT_TRADE_FEE = 0.1 // M$0.1
|
||||
|
||||
export const PLATFORM_FEE = 0
|
||||
export const CREATOR_FEE = 0
|
||||
export const CREATOR_FEE = 0.1
|
||||
export const LIQUIDITY_FEE = 0
|
||||
|
||||
export const DPM_PLATFORM_FEE = 0.0
|
||||
export const DPM_CREATOR_FEE = 0.0
|
||||
export const DPM_PLATFORM_FEE = 0.01
|
||||
export const DPM_CREATOR_FEE = 0.04
|
||||
export const DPM_FEES = DPM_PLATFORM_FEE + DPM_CREATOR_FEE
|
||||
|
||||
export type Fees = {
|
||||
|
|
|
@ -2,8 +2,3 @@ export type Follow = {
|
|||
userId: string
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
export type ContractFollow = {
|
||||
id: string // user id
|
||||
createdTime: number
|
||||
}
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
export type GlobalConfig = {
|
||||
pinnedItems: { itemId: string; type: 'post' | 'contract' }[]
|
||||
}
|
|
@ -6,37 +6,10 @@ export type Group = {
|
|||
creatorId: string // User id
|
||||
createdTime: number
|
||||
mostRecentActivityTime: number
|
||||
memberIds: string[] // User ids
|
||||
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' }[]
|
||||
contractIds: string[]
|
||||
}
|
||||
|
||||
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 }
|
||||
|
|
|
@ -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
|
138
common/loans.ts
138
common/loans.ts
|
@ -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,
|
||||
}
|
||||
})
|
||||
}
|
|
@ -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 {
|
||||
calculateDpmShares,
|
||||
getDpmProbability,
|
||||
|
@ -8,34 +8,20 @@ import {
|
|||
getNumericBets,
|
||||
calculateNumericDpmShares,
|
||||
} from './calculate-dpm'
|
||||
import {
|
||||
calculateCpmmAmountToProb,
|
||||
calculateCpmmPurchase,
|
||||
CpmmState,
|
||||
getCpmmProbability,
|
||||
} from './calculate-cpmm'
|
||||
import { calculateCpmmPurchase, getCpmmProbability } from './calculate-cpmm'
|
||||
import {
|
||||
CPMMBinaryContract,
|
||||
DPMBinaryContract,
|
||||
DPMContract,
|
||||
FreeResponseContract,
|
||||
NumericContract,
|
||||
PseudoNumericContract,
|
||||
} from './contract'
|
||||
import { noFees } from './fees'
|
||||
import { addObjects, removeUndefinedProps } from './util/object'
|
||||
import { addObjects } from './util/object'
|
||||
import { NUMERIC_FIXED_VAR } from './numeric-constants'
|
||||
import {
|
||||
floatingEqual,
|
||||
floatingGreaterEqual,
|
||||
floatingLesserEqual,
|
||||
} from './util/math'
|
||||
|
||||
export type CandidateBet<T extends Bet = Bet> = Omit<
|
||||
T,
|
||||
'id' | 'userId' | 'userAvatarUrl' | 'userName' | 'userUsername'
|
||||
>
|
||||
export type CandidateBet<T extends Bet> = Omit<T, 'id' | 'userId'>
|
||||
export type BetInfo = {
|
||||
newBet: CandidateBet
|
||||
newBet: CandidateBet<Bet>
|
||||
newPool?: { [outcome: string]: number }
|
||||
newTotalShares?: { [outcome: string]: number }
|
||||
newTotalBets?: { [outcome: string]: number }
|
||||
|
@ -43,261 +29,45 @@ export type BetInfo = {
|
|||
newP?: number
|
||||
}
|
||||
|
||||
const computeFill = (
|
||||
export const getNewBinaryCpmmBetInfo = (
|
||||
outcome: 'YES' | 'NO',
|
||||
amount: number,
|
||||
outcome: 'YES' | 'NO',
|
||||
limitProb: number | undefined,
|
||||
cpmmState: CpmmState,
|
||||
matchedBet: LimitBet | undefined
|
||||
contract: CPMMBinaryContract,
|
||||
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(
|
||||
cpmmState,
|
||||
buyAmount,
|
||||
outcome
|
||||
)
|
||||
const newState = { pool: newPool, p: newP }
|
||||
|
||||
return {
|
||||
maker: {
|
||||
matchedBetId: null,
|
||||
shares,
|
||||
amount: buyAmount,
|
||||
state: newState,
|
||||
fees,
|
||||
timestamp,
|
||||
},
|
||||
taker: {
|
||||
matchedBetId: null,
|
||||
shares,
|
||||
amount: buyAmount,
|
||||
timestamp,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Fill from matchedBet.
|
||||
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 { shares, newPool, newP, fees } = calculateCpmmPurchase(
|
||||
contract,
|
||||
amount,
|
||||
outcome
|
||||
)
|
||||
|
||||
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 }
|
||||
}
|
||||
const { pool, p, totalLiquidity } = contract
|
||||
const probBefore = getCpmmProbability(pool, p)
|
||||
const probAfter = getCpmmProbability(newPool, newP)
|
||||
|
||||
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,
|
||||
const newBet: CandidateBet<Bet> = {
|
||||
contractId: contract.id,
|
||||
amount,
|
||||
shares,
|
||||
outcome,
|
||||
fees,
|
||||
loanAmount,
|
||||
probBefore,
|
||||
probAfter,
|
||||
loanAmount: 0,
|
||||
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 = (
|
||||
outcome: 'YES' | 'NO',
|
||||
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 { liquidityFee } = fees
|
||||
const newTotalLiquidity = (totalLiquidity ?? 0) + liquidityFee
|
||||
|
||||
const currentReturn = betAmount ? (currentPayout - betAmount) / betAmount : 0
|
||||
|
||||
const totalFees = sum(Object.values(newBet.fees))
|
||||
|
||||
return { currentPayout, currentReturn, totalFees, newBet }
|
||||
return { newBet, newPool, newP, newTotalLiquidity }
|
||||
}
|
||||
|
||||
export const getNewBinaryDpmBetInfo = (
|
||||
outcome: 'YES' | 'NO',
|
||||
amount: number,
|
||||
contract: DPMBinaryContract
|
||||
contract: DPMBinaryContract,
|
||||
loanAmount: number
|
||||
) => {
|
||||
const { YES: yesPool, NO: noPool } = contract.pool
|
||||
|
||||
|
@ -325,10 +95,10 @@ export const getNewBinaryDpmBetInfo = (
|
|||
const probBefore = getDpmProbability(contract.totalShares)
|
||||
const probAfter = getDpmProbability(newTotalShares)
|
||||
|
||||
const newBet: CandidateBet = {
|
||||
const newBet: CandidateBet<Bet> = {
|
||||
contractId: contract.id,
|
||||
amount,
|
||||
loanAmount: 0,
|
||||
loanAmount,
|
||||
shares,
|
||||
outcome,
|
||||
probBefore,
|
||||
|
@ -343,7 +113,8 @@ export const getNewBinaryDpmBetInfo = (
|
|||
export const getNewMultiBetInfo = (
|
||||
outcome: string,
|
||||
amount: number,
|
||||
contract: DPMContract
|
||||
contract: FreeResponseContract,
|
||||
loanAmount: number
|
||||
) => {
|
||||
const { pool, totalShares, totalBets } = contract
|
||||
|
||||
|
@ -361,10 +132,10 @@ export const getNewMultiBetInfo = (
|
|||
const probBefore = getDpmOutcomeProbability(totalShares, outcome)
|
||||
const probAfter = getDpmOutcomeProbability(newTotalShares, outcome)
|
||||
|
||||
const newBet: CandidateBet = {
|
||||
const newBet: CandidateBet<Bet> = {
|
||||
contractId: contract.id,
|
||||
amount,
|
||||
loanAmount: 0,
|
||||
loanAmount,
|
||||
shares,
|
||||
outcome,
|
||||
probBefore,
|
||||
|
@ -418,3 +189,13 @@ export const getNumericBetsInfo = (
|
|||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -5,15 +5,12 @@ import {
|
|||
CPMM,
|
||||
DPM,
|
||||
FreeResponse,
|
||||
MultipleChoice,
|
||||
Numeric,
|
||||
outcomeType,
|
||||
PseudoNumeric,
|
||||
visibility,
|
||||
} from './contract'
|
||||
import { User } from './user'
|
||||
import { parseTags } from './util/parse'
|
||||
import { removeUndefinedProps } from './util/object'
|
||||
import { JSONContent } from '@tiptap/core'
|
||||
|
||||
export function getNewContract(
|
||||
id: string,
|
||||
|
@ -21,7 +18,7 @@ export function getNewContract(
|
|||
creator: User,
|
||||
question: string,
|
||||
outcomeType: outcomeType,
|
||||
description: JSONContent,
|
||||
description: string,
|
||||
initialProb: number,
|
||||
ante: number,
|
||||
closeTime: number,
|
||||
|
@ -30,22 +27,18 @@ export function getNewContract(
|
|||
// used for numeric markets
|
||||
bucketCount: number,
|
||||
min: number,
|
||||
max: number,
|
||||
isLogScale: boolean,
|
||||
|
||||
// for multiple choice
|
||||
answers: string[],
|
||||
visibility: visibility
|
||||
max: number
|
||||
) {
|
||||
const tags = parseTags(
|
||||
`${question} ${description} ${extraTags.map((tag) => `#${tag}`).join(' ')}`
|
||||
)
|
||||
const lowercaseTags = tags.map((tag) => tag.toLowerCase())
|
||||
|
||||
const propsByOutcomeType =
|
||||
outcomeType === 'BINARY'
|
||||
? getBinaryCpmmProps(initialProb, ante) // getBinaryDpmProps(initialProb, ante)
|
||||
: outcomeType === 'PSEUDO_NUMERIC'
|
||||
? getPseudoNumericCpmmProps(initialProb, ante, min, max, isLogScale)
|
||||
: outcomeType === 'NUMERIC'
|
||||
? getNumericProps(ante, bucketCount, min, max)
|
||||
: outcomeType === 'MULTIPLE_CHOICE'
|
||||
? getMultipleChoiceProps(ante, answers)
|
||||
: getFreeAnswerProps(ante)
|
||||
|
||||
const contract: Contract = removeUndefinedProps({
|
||||
|
@ -59,11 +52,10 @@ export function getNewContract(
|
|||
creatorAvatarUrl: creator.avatarUrl,
|
||||
|
||||
question: question.trim(),
|
||||
description,
|
||||
tags: [],
|
||||
lowercaseTags: [],
|
||||
visibility,
|
||||
unlistedById: visibility === 'unlisted' ? creator.id : undefined,
|
||||
description: description.trim(),
|
||||
tags,
|
||||
lowercaseTags,
|
||||
visibility: 'public',
|
||||
isResolved: false,
|
||||
createdTime: Date.now(),
|
||||
closeTime,
|
||||
|
@ -71,7 +63,6 @@ export function getNewContract(
|
|||
volume: 0,
|
||||
volume24Hours: 0,
|
||||
volume7Days: 0,
|
||||
elasticity: propsByOutcomeType.mechanism === 'cpmm-1' ? 0.38 : 0.75,
|
||||
|
||||
collectedFees: {
|
||||
creatorFee: 0,
|
||||
|
@ -112,30 +103,9 @@ const getBinaryCpmmProps = (initialProb: number, ante: number) => {
|
|||
mechanism: 'cpmm-1',
|
||||
outcomeType: 'BINARY',
|
||||
totalLiquidity: ante,
|
||||
subsidyPool: 0,
|
||||
initialProbability: p,
|
||||
p,
|
||||
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
|
||||
|
@ -154,26 +124,6 @@ const getFreeAnswerProps = (ante: number) => {
|
|||
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 = (
|
||||
ante: number,
|
||||
bucketCount: number,
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
import { notification_preference } from './user-notification-preferences'
|
||||
|
||||
export type Notification = {
|
||||
id: string
|
||||
userId: string
|
||||
reasonText?: string
|
||||
reason?: notification_reason_types | notification_preference
|
||||
reason?: notification_reason_types
|
||||
createdTime: number
|
||||
viewTime?: number
|
||||
isSeen: boolean
|
||||
|
@ -17,7 +15,6 @@ export type Notification = {
|
|||
sourceUserUsername?: string
|
||||
sourceUserAvatarUrl?: string
|
||||
sourceText?: string
|
||||
data?: { [key: string]: any }
|
||||
|
||||
sourceContractTitle?: string
|
||||
sourceContractCreatorUsername?: string
|
||||
|
@ -25,10 +22,7 @@ export type Notification = {
|
|||
|
||||
sourceSlug?: string
|
||||
sourceTitle?: string
|
||||
|
||||
isSeenOnHref?: string
|
||||
}
|
||||
|
||||
export type notification_source_types =
|
||||
| 'contract'
|
||||
| 'comment'
|
||||
|
@ -39,14 +33,6 @@ export type notification_source_types =
|
|||
| 'tip'
|
||||
| 'admin_message'
|
||||
| 'group'
|
||||
| 'user'
|
||||
| 'bonus'
|
||||
| 'challenge'
|
||||
| 'betting_streak_bonus'
|
||||
| 'loan'
|
||||
| 'like'
|
||||
| 'tip_and_like'
|
||||
| 'badge'
|
||||
|
||||
export type notification_source_update_types =
|
||||
| 'created'
|
||||
|
@ -55,216 +41,15 @@ export type notification_source_update_types =
|
|||
| 'deleted'
|
||||
| 'closed'
|
||||
|
||||
/* 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'
|
||||
| 'on_users_contract'
|
||||
| 'on_contract_with_users_shares_in'
|
||||
| 'on_contract_with_users_shares_out'
|
||||
| 'on_contract_with_users_answer'
|
||||
| '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
|
||||
}
|
||||
| 'on_new_follow'
|
||||
| 'you_follow_user'
|
||||
| 'added_you_to_group'
|
||||
|
|
|
@ -3,18 +3,10 @@
|
|||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"verify": "(cd .. && yarn verify)",
|
||||
"verify:dir": "npx eslint . --max-warnings 0"
|
||||
"verify": "(cd .. && yarn verify)"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"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"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -2,17 +2,14 @@ import { sum, groupBy, sumBy, mapValues } from 'lodash'
|
|||
|
||||
import { Bet, NumericBet } from './bet'
|
||||
import { deductDpmFees, getDpmProbability } from './calculate-dpm'
|
||||
import {
|
||||
DPMContract,
|
||||
FreeResponseContract,
|
||||
MultipleChoiceContract,
|
||||
} from './contract'
|
||||
import { DPMContract, FreeResponseContract } from './contract'
|
||||
import { DPM_CREATOR_FEE, DPM_FEES, DPM_PLATFORM_FEE } from './fees'
|
||||
import { addObjects } from './util/object'
|
||||
|
||||
export const getDpmCancelPayouts = (contract: DPMContract, bets: Bet[]) => {
|
||||
const { pool } = contract
|
||||
const poolTotal = sum(Object.values(pool))
|
||||
console.log('resolved N/A, pool M$', poolTotal)
|
||||
|
||||
const betSum = sumBy(bets, (b) => b.amount)
|
||||
|
||||
|
@ -57,6 +54,17 @@ export const getDpmStandardPayouts = (
|
|||
liquidityFee: 0,
|
||||
})
|
||||
|
||||
console.log(
|
||||
'resolved',
|
||||
outcome,
|
||||
'pool',
|
||||
poolTotal,
|
||||
'profits',
|
||||
profits,
|
||||
'creator fee',
|
||||
creatorFee
|
||||
)
|
||||
|
||||
return {
|
||||
payouts: payouts.map(({ userId, payout }) => ({ userId, payout })),
|
||||
creatorPayout: creatorFee,
|
||||
|
@ -98,6 +106,17 @@ export const getNumericDpmPayouts = (
|
|||
liquidityFee: 0,
|
||||
})
|
||||
|
||||
console.log(
|
||||
'resolved numeric bucket: ',
|
||||
outcome,
|
||||
'pool',
|
||||
poolTotal,
|
||||
'profits',
|
||||
profits,
|
||||
'creator fee',
|
||||
creatorFee
|
||||
)
|
||||
|
||||
return {
|
||||
payouts: payouts.map(({ userId, payout }) => ({ userId, payout })),
|
||||
creatorPayout: creatorFee,
|
||||
|
@ -140,6 +159,17 @@ export const getDpmMktPayouts = (
|
|||
liquidityFee: 0,
|
||||
})
|
||||
|
||||
console.log(
|
||||
'resolved MKT',
|
||||
p,
|
||||
'pool',
|
||||
pool,
|
||||
'profits',
|
||||
profits,
|
||||
'creator fee',
|
||||
creatorFee
|
||||
)
|
||||
|
||||
return {
|
||||
payouts: payouts.map(({ userId, payout }) => ({ userId, payout })),
|
||||
creatorPayout: creatorFee,
|
||||
|
@ -150,7 +180,7 @@ export const getDpmMktPayouts = (
|
|||
|
||||
export const getPayoutsMultiOutcome = (
|
||||
resolutions: { [outcome: string]: number },
|
||||
contract: FreeResponseContract | MultipleChoiceContract,
|
||||
contract: FreeResponseContract,
|
||||
bets: Bet[]
|
||||
) => {
|
||||
const poolTotal = sum(Object.values(contract.pool))
|
||||
|
@ -168,7 +198,7 @@ export const getPayoutsMultiOutcome = (
|
|||
const winnings = (shares / sharesByOutcome[outcome]) * prob * poolTotal
|
||||
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 }
|
||||
})
|
||||
|
||||
|
@ -182,6 +212,16 @@ export const getPayoutsMultiOutcome = (
|
|||
liquidityFee: 0,
|
||||
})
|
||||
|
||||
console.log(
|
||||
'resolved',
|
||||
resolutions,
|
||||
'pool',
|
||||
poolTotal,
|
||||
'profits',
|
||||
profits,
|
||||
'creator fee',
|
||||
creatorFee
|
||||
)
|
||||
return {
|
||||
payouts: payouts.map(({ userId, payout }) => ({ userId, payout })),
|
||||
creatorPayout: creatorFee,
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { sum } from 'lodash'
|
||||
|
||||
import { Bet } from './bet'
|
||||
import { getProbability } from './calculate'
|
||||
import { getCpmmLiquidityPoolWeights } from './calculate-cpmm'
|
||||
|
@ -41,6 +43,18 @@ export const getStandardFixedPayouts = (
|
|||
|
||||
const { collectedFees } = contract
|
||||
const creatorPayout = collectedFees.creatorFee
|
||||
|
||||
console.log(
|
||||
'resolved',
|
||||
outcome,
|
||||
'pool',
|
||||
contract.pool[outcome],
|
||||
'payouts',
|
||||
sum(payouts),
|
||||
'creator fee',
|
||||
creatorPayout
|
||||
)
|
||||
|
||||
const liquidityPayouts = getLiquidityPoolPayouts(
|
||||
contract,
|
||||
outcome,
|
||||
|
@ -55,11 +69,10 @@ export const getLiquidityPoolPayouts = (
|
|||
outcome: string,
|
||||
liquidities: LiquidityProvision[]
|
||||
) => {
|
||||
const { pool, subsidyPool } = contract
|
||||
const finalPool = pool[outcome] + (subsidyPool ?? 0)
|
||||
if (finalPool < 1e-3) return []
|
||||
const { pool } = contract
|
||||
const finalPool = pool[outcome]
|
||||
|
||||
const weights = getCpmmLiquidityPoolWeights(liquidities)
|
||||
const weights = getCpmmLiquidityPoolWeights(contract, liquidities)
|
||||
|
||||
return Object.entries(weights).map(([providerId, weight]) => ({
|
||||
userId: providerId,
|
||||
|
@ -85,6 +98,18 @@ export const getMktFixedPayouts = (
|
|||
|
||||
const { collectedFees } = contract
|
||||
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)
|
||||
|
||||
return { payouts, creatorPayout, liquidityPayouts, collectedFees }
|
||||
|
@ -95,11 +120,10 @@ export const getLiquidityPoolProbPayouts = (
|
|||
p: number,
|
||||
liquidities: LiquidityProvision[]
|
||||
) => {
|
||||
const { pool, subsidyPool } = contract
|
||||
const finalPool = p * pool.YES + (1 - p) * pool.NO + (subsidyPool ?? 0)
|
||||
if (finalPool < 1e-3) return []
|
||||
const { pool } = contract
|
||||
const finalPool = p * pool.YES + (1 - p) * pool.NO
|
||||
|
||||
const weights = getCpmmLiquidityPoolWeights(liquidities)
|
||||
const weights = getCpmmLiquidityPoolWeights(contract, liquidities)
|
||||
|
||||
return Object.entries(weights).map(([providerId, weight]) => ({
|
||||
userId: providerId,
|
||||
|
|
|
@ -1,12 +1,7 @@
|
|||
import { sumBy, groupBy, mapValues } from 'lodash'
|
||||
|
||||
import { Bet, NumericBet } from './bet'
|
||||
import {
|
||||
Contract,
|
||||
CPMMBinaryContract,
|
||||
DPMContract,
|
||||
PseudoNumericContract,
|
||||
} from './contract'
|
||||
import { Contract, CPMMBinaryContract, DPMContract } from './contract'
|
||||
import { Fees } from './fees'
|
||||
import { LiquidityProvision } from './liquidity-provision'
|
||||
import {
|
||||
|
@ -53,19 +48,15 @@ export type PayoutInfo = {
|
|||
|
||||
export const getPayouts = (
|
||||
outcome: string | undefined,
|
||||
resolutions: {
|
||||
[outcome: string]: number
|
||||
},
|
||||
contract: Contract,
|
||||
bets: Bet[],
|
||||
liquidities: LiquidityProvision[],
|
||||
resolutions?: {
|
||||
[outcome: string]: number
|
||||
},
|
||||
resolutionProbability?: number
|
||||
): PayoutInfo => {
|
||||
if (
|
||||
contract.mechanism === 'cpmm-1' &&
|
||||
(contract.outcomeType === 'BINARY' ||
|
||||
contract.outcomeType === 'PSEUDO_NUMERIC')
|
||||
) {
|
||||
if (contract.mechanism === 'cpmm-1' && contract.outcomeType === 'BINARY') {
|
||||
return getFixedPayouts(
|
||||
outcome,
|
||||
contract,
|
||||
|
@ -76,16 +67,16 @@ export const getPayouts = (
|
|||
}
|
||||
return getDpmPayouts(
|
||||
outcome,
|
||||
resolutions,
|
||||
contract,
|
||||
bets,
|
||||
resolutions,
|
||||
resolutionProbability
|
||||
)
|
||||
}
|
||||
|
||||
export const getFixedPayouts = (
|
||||
outcome: string | undefined,
|
||||
contract: CPMMBinaryContract | PseudoNumericContract,
|
||||
contract: CPMMBinaryContract,
|
||||
bets: Bet[],
|
||||
liquidities: LiquidityProvision[],
|
||||
resolutionProbability?: number
|
||||
|
@ -109,15 +100,14 @@ export const getFixedPayouts = (
|
|||
|
||||
export const getDpmPayouts = (
|
||||
outcome: string | undefined,
|
||||
contract: DPMContract,
|
||||
bets: Bet[],
|
||||
resolutions?: {
|
||||
resolutions: {
|
||||
[outcome: string]: number
|
||||
},
|
||||
contract: DPMContract,
|
||||
bets: Bet[],
|
||||
resolutionProbability?: number
|
||||
): PayoutInfo => {
|
||||
const openBets = bets.filter((b) => !b.isSold && !b.sale)
|
||||
const { outcomeType } = contract
|
||||
|
||||
switch (outcome) {
|
||||
case 'YES':
|
||||
|
@ -125,16 +115,15 @@ export const getDpmPayouts = (
|
|||
return getDpmStandardPayouts(outcome, contract, openBets)
|
||||
|
||||
case 'MKT':
|
||||
return outcomeType === 'FREE_RESPONSE' ||
|
||||
outcomeType === 'MULTIPLE_CHOICE' // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
? getPayoutsMultiOutcome(resolutions!, contract, openBets)
|
||||
return contract.outcomeType === 'FREE_RESPONSE'
|
||||
? getPayoutsMultiOutcome(resolutions, contract, openBets)
|
||||
: getDpmMktPayouts(contract, openBets, resolutionProbability)
|
||||
case 'CANCEL':
|
||||
case undefined:
|
||||
return getDpmCancelPayouts(contract, openBets)
|
||||
|
||||
default:
|
||||
if (outcomeType === 'NUMERIC')
|
||||
if (contract.outcomeType === 'NUMERIC')
|
||||
return getNumericDpmPayouts(outcome, contract, openBets as NumericBet[])
|
||||
|
||||
// Outcome is a free response answer id.
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
}
|
187
common/recommended-contracts.ts
Normal file
187
common/recommended-contracts.ts
Normal 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]])
|
||||
)
|
||||
}
|
|
@ -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]
|
||||
}
|
|
@ -1,9 +1,8 @@
|
|||
import { groupBy, sumBy, mapValues, keyBy, sortBy } from 'lodash'
|
||||
import { groupBy, sumBy, mapValues, partition } from 'lodash'
|
||||
|
||||
import { Bet } from './bet'
|
||||
import { getContractBetMetrics, resolvedPayout } from './calculate'
|
||||
import { Contract } from './contract'
|
||||
import { ContractComment } from './comment'
|
||||
import { getPayouts } from './payouts'
|
||||
|
||||
export function scoreCreators(contracts: Contract[]) {
|
||||
const creatorScore = mapValues(
|
||||
|
@ -31,11 +30,46 @@ export function scoreTraders(contracts: Contract[], bets: Bet[][]) {
|
|||
}
|
||||
|
||||
export function scoreUsersByContract(contract: Contract, bets: Bet[]) {
|
||||
const betsByUser = groupBy(bets, (bet) => bet.userId)
|
||||
return mapValues(
|
||||
betsByUser,
|
||||
(bets) => getContractBetMetrics(contract, bets).profit
|
||||
const { resolution } = contract
|
||||
const resolutionProb =
|
||||
contract.outcomeType == 'BINARY'
|
||||
? 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(
|
||||
|
@ -47,47 +81,3 @@ export function addUserScores(
|
|||
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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Bet, LimitBet } from './bet'
|
||||
import { Bet } from './bet'
|
||||
import {
|
||||
calculateDpmShareValue,
|
||||
deductDpmFees,
|
||||
|
@ -7,16 +7,12 @@ import {
|
|||
import { calculateCpmmSale, getCpmmProbability } from './calculate-cpmm'
|
||||
import { CPMMContract, DPMContract } from './contract'
|
||||
import { DPM_CREATOR_FEE, DPM_PLATFORM_FEE, Fees } from './fees'
|
||||
import { sumBy } from 'lodash'
|
||||
|
||||
export type CandidateBet<T extends Bet> = Omit<
|
||||
T,
|
||||
'id' | 'userId' | 'userAvatarUrl' | 'userName' | 'userUsername'
|
||||
>
|
||||
export type CandidateBet<T extends Bet> = Omit<T, 'id' | 'userId'>
|
||||
|
||||
export const getSellBetInfo = (bet: Bet, contract: DPMContract) => {
|
||||
const { pool, totalShares, totalBets } = contract
|
||||
const { id: betId, amount, shares, outcome, loanAmount } = bet
|
||||
const { id: betId, amount, shares, outcome } = bet
|
||||
|
||||
const adjShareValue = calculateDpmShareValue(contract, bet)
|
||||
|
||||
|
@ -67,7 +63,6 @@ export const getSellBetInfo = (bet: Bet, contract: DPMContract) => {
|
|||
betId,
|
||||
},
|
||||
fees,
|
||||
loanAmount: -(loanAmount ?? 0),
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -83,25 +78,19 @@ export const getCpmmSellBetInfo = (
|
|||
shares: number,
|
||||
outcome: 'YES' | 'NO',
|
||||
contract: CPMMContract,
|
||||
unfilledBets: LimitBet[],
|
||||
balanceByUserId: { [userId: string]: number },
|
||||
loanPaid: number
|
||||
prevLoanAmount: number
|
||||
) => {
|
||||
const { pool, p } = contract
|
||||
|
||||
const { saleValue, cpmmState, fees, makers, takers, ordersToCancel } = calculateCpmmSale(
|
||||
const { saleValue, newPool, newP, fees } = calculateCpmmSale(
|
||||
contract,
|
||||
shares,
|
||||
outcome,
|
||||
unfilledBets,
|
||||
balanceByUserId,
|
||||
outcome
|
||||
)
|
||||
|
||||
const loanPaid = Math.min(prevLoanAmount, saleValue)
|
||||
const probBefore = getCpmmProbability(pool, p)
|
||||
const probAfter = getCpmmProbability(cpmmState.pool, cpmmState.p)
|
||||
|
||||
const takerAmount = sumBy(takers, 'amount')
|
||||
const takerShares = sumBy(takers, 'shares')
|
||||
const probAfter = getCpmmProbability(newPool, p)
|
||||
|
||||
console.log(
|
||||
'SELL M$',
|
||||
|
@ -115,27 +104,20 @@ export const getCpmmSellBetInfo = (
|
|||
|
||||
const newBet: CandidateBet<Bet> = {
|
||||
contractId: contract.id,
|
||||
amount: takerAmount,
|
||||
shares: takerShares,
|
||||
amount: -saleValue,
|
||||
shares: -shares,
|
||||
outcome,
|
||||
probBefore,
|
||||
probAfter,
|
||||
createdTime: Date.now(),
|
||||
loanAmount: -loanPaid,
|
||||
fees,
|
||||
fills: takers,
|
||||
isFilled: true,
|
||||
isCancelled: false,
|
||||
orderAmount: takerAmount,
|
||||
}
|
||||
|
||||
return {
|
||||
newBet,
|
||||
newPool: cpmmState.pool,
|
||||
newP: cpmmState.p,
|
||||
newPool,
|
||||
newP,
|
||||
fees,
|
||||
makers,
|
||||
takers,
|
||||
ordersToCancel
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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[]
|
||||
}
|
||||
}
|
|
@ -1,14 +1,6 @@
|
|||
// A txn (pronounced "texan") respresents a payment between two ids on Manifold
|
||||
// Shortened from "transaction" to distinguish from Firebase transactions (and save chars)
|
||||
type AnyTxnType =
|
||||
| Donation
|
||||
| Tip
|
||||
| Manalink
|
||||
| Referral
|
||||
| UniqueBettorBonus
|
||||
| BettingStreakBonus
|
||||
| CancelUniqueBettorBonus
|
||||
| CommentBountyRefund
|
||||
type AnyTxnType = Donation | Tip | Manalink
|
||||
type SourceType = 'USER' | 'CONTRACT' | 'CHARITY' | 'BANK'
|
||||
|
||||
export type Txn<T extends AnyTxnType = AnyTxnType> = {
|
||||
|
@ -24,17 +16,7 @@ export type Txn<T extends AnyTxnType = AnyTxnType> = {
|
|||
amount: number
|
||||
token: 'M$' // | 'USD' | MarketOutcome
|
||||
|
||||
category:
|
||||
| 'CHARITY'
|
||||
| 'MANALINK'
|
||||
| 'TIP'
|
||||
| 'REFERRAL'
|
||||
| 'UNIQUE_BETTOR_BONUS'
|
||||
| 'BETTING_STREAK_BONUS'
|
||||
| 'CANCEL_UNIQUE_BETTOR_BONUS'
|
||||
| 'COMMENT_BOUNTY'
|
||||
| 'REFUND_COMMENT_BOUNTY'
|
||||
|
||||
category: 'CHARITY' | 'MANALINK' | 'TIP' // | 'BET'
|
||||
// Any extra data
|
||||
data?: { [key: string]: any }
|
||||
|
||||
|
@ -53,9 +35,8 @@ type Tip = {
|
|||
toType: 'USER'
|
||||
category: 'TIP'
|
||||
data: {
|
||||
contractId: string
|
||||
commentId: string
|
||||
contractId?: string
|
||||
groupId?: string
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -65,76 +46,6 @@ type Manalink = {
|
|||
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
|
||||
|
|
|
@ -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§ion=${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: '',
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 = {
|
||||
id: string
|
||||
createdTime: number
|
||||
|
@ -12,6 +8,7 @@ export type User = {
|
|||
|
||||
// For their user page
|
||||
bio?: string
|
||||
bannerUrl?: string
|
||||
website?: string
|
||||
twitterHandle?: string
|
||||
discordHandle?: string
|
||||
|
@ -33,58 +30,31 @@ export type User = {
|
|||
allTime: number
|
||||
}
|
||||
|
||||
fractionResolvedCorrectly: number
|
||||
|
||||
nextLoanCached: number
|
||||
followerCountCached: number
|
||||
|
||||
followedCategories?: string[]
|
||||
homeSections?: string[]
|
||||
|
||||
referredByUserId?: string
|
||||
referredByContractId?: string
|
||||
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 const STARTING_BALANCE = 1000
|
||||
export const SUS_STARTING_BALANCE = 10 // for sus users, i.e. multiple sign ups for same person
|
||||
|
||||
export type PrivateUser = {
|
||||
id: string // same as User.id
|
||||
username: string // denormalized from User
|
||||
|
||||
email?: string
|
||||
weeklyTrendingEmailSent?: boolean
|
||||
weeklyPortfolioUpdateEmailSent?: boolean
|
||||
manaBonusEmailSent?: boolean
|
||||
unsubscribedFromResolutionEmails?: boolean
|
||||
unsubscribedFromCommentEmails?: boolean
|
||||
unsubscribedFromAnswerEmails?: boolean
|
||||
unsubscribedFromGenericEmails?: boolean
|
||||
initialDeviceToken?: string
|
||||
initialIpAddress?: string
|
||||
apiKey?: string
|
||||
notificationPreferences: notification_preferences
|
||||
twitchInfo?: {
|
||||
twitchName: string
|
||||
controlToken: string
|
||||
botEnabled?: boolean
|
||||
needsRelinking?: boolean
|
||||
}
|
||||
notificationPreferences?: notification_subscribe_types
|
||||
}
|
||||
|
||||
export type notification_subscribe_types = 'all' | 'less' | 'none'
|
||||
|
||||
export type PortfolioMetrics = {
|
||||
investmentValue: number
|
||||
balance: number
|
||||
|
@ -92,16 +62,3 @@ export type PortfolioMetrics = {
|
|||
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'
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -1,40 +1,3 @@
|
|||
import { isEqual } from 'lodash'
|
||||
|
||||
export function filterDefined<T>(array: (T | null | undefined)[]) {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
}
|
|
@ -8,14 +8,7 @@ const formatter = new Intl.NumberFormat('en-US', {
|
|||
})
|
||||
|
||||
export function formatMoney(amount: number) {
|
||||
const newAmount =
|
||||
// handle -0 case
|
||||
Math.round(amount) === 0
|
||||
? 0
|
||||
: // Handle 499.9999999999999 case
|
||||
(amount > 0 ? Math.floor : Math.ceil)(
|
||||
amount + 0.00000000001 * Math.sign(amount)
|
||||
)
|
||||
const newAmount = Math.round(amount) === 0 ? 0 : Math.floor(amount) // handle -0 case
|
||||
return ENV_CONFIG.moneyMoniker + formatter.format(newAmount).replace('$', '')
|
||||
}
|
||||
|
||||
|
@ -40,34 +33,18 @@ export function formatPercent(zeroToOne: number) {
|
|||
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
|
||||
export function formatLargeNumber(num: number, sigfigs = 2): string {
|
||||
const absNum = Math.abs(num)
|
||||
if (absNum < 1) return showPrecision(num, sigfigs)
|
||||
|
||||
if (absNum < 100) return showPrecision(num, 2)
|
||||
if (absNum < 1000) return showPrecision(num, 3)
|
||||
if (absNum < 10000) return showPrecision(num, 4)
|
||||
if (absNum < 1000) {
|
||||
return '' + Number(num.toPrecision(sigfigs))
|
||||
}
|
||||
|
||||
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 i = Math.floor(Math.log10(num) / 3)
|
||||
|
||||
const numStr = showPrecision(num / Math.pow(10, 3 * i), 2)
|
||||
return `${numStr}${suffix[i] ?? ''}`
|
||||
const suffixIdx = Math.floor(Math.log10(absNum) / 3)
|
||||
const suffixStr = suffix[suffixIdx]
|
||||
const numStr = (num / Math.pow(10, 3 * suffixIdx)).toPrecision(sigfigs)
|
||||
return `${Number(numStr)}${suffixStr}`
|
||||
}
|
||||
|
||||
export function toCamelCase(words: string) {
|
||||
|
|
|
@ -34,17 +34,3 @@ export function median(xs: number[]) {
|
|||
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
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { union } from 'lodash'
|
||||
|
||||
export const removeUndefinedProps = <T extends object>(obj: T): T => {
|
||||
export const removeUndefinedProps = <T>(obj: T): T => {
|
||||
const newObj: any = {}
|
||||
|
||||
for (const key of Object.keys(obj)) {
|
||||
|
@ -37,3 +37,4 @@ export const subtractObjects = <T extends { [key: string]: number }>(
|
|||
|
||||
return newObj as T
|
||||
}
|
||||
|
||||
|
|
|
@ -1,115 +1,29 @@
|
|||
import { generateText, JSONContent, Node } from '@tiptap/core'
|
||||
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'
|
||||
import { MAX_TAG_LENGTH } from '../contract'
|
||||
|
||||
/** get first url in text. like "notion.so " -> "http://notion.so"; "notion" -> null */
|
||||
export function getUrl(text: string) {
|
||||
const results = find(text, 'url')
|
||||
return results.length ? results[0].href : null
|
||||
}
|
||||
|
||||
// TODO: fuzzy matching
|
||||
export const wordIn = (word: string, corpus: string) =>
|
||||
corpus.toLocaleLowerCase().includes(word.toLocaleLowerCase())
|
||||
|
||||
const checkAgainstQuery = (query: string, corpus: string) =>
|
||||
query.split(' ').every((word) => wordIn(word, corpus))
|
||||
|
||||
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)
|
||||
export function parseTags(text: string) {
|
||||
const regex = /(?:^|\s)(?:[#][a-z0-9_]+)/gi
|
||||
const matches = (text.match(regex) || []).map((match) =>
|
||||
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)
|
||||
}
|
||||
}
|
||||
return uniq(mentions)
|
||||
uniqueTags.reverse()
|
||||
return uniqueTags
|
||||
}
|
||||
|
||||
// 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)
|
||||
export function parseWordsAsTags(text: string) {
|
||||
const taggedText = text
|
||||
.split(/\s+/)
|
||||
.map((tag) => (tag.startsWith('#') ? tag : `#${tag}`))
|
||||
.join(' ')
|
||||
return parseTags(taggedText)
|
||||
}
|
||||
|
|
|
@ -46,10 +46,3 @@ export const shuffle = (array: unknown[], rand: () => number) => {
|
|||
;[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)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,2 @@
|
|||
export const MINUTE_MS = 60 * 1000
|
||||
export const HOUR_MS = 60 * MINUTE_MS
|
||||
export const HOUR_MS = 60 * 60 * 1000
|
||||
export const DAY_MS = 24 * HOUR_MS
|
||||
|
||||
export const sleep = (ms: number) =>
|
||||
new Promise((resolve) => setTimeout(resolve, ms))
|
||||
|
|
|
@ -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
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
|
@ -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
|
||||
},
|
||||
})
|
|
@ -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)
|
43
dev.sh
43
dev.sh
|
@ -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
|
335
docs/docs/api.md
335
docs/docs/api.md
|
@ -34,54 +34,6 @@ 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.
|
||||
|
@ -111,6 +63,7 @@ Requires no authorization.
|
|||
"creatorAvatarUrl":"https://lh3.googleusercontent.com/a-/AOh14GiZyl1lBehuBMGyJYJhZd-N-mstaUtgE4xdI22lLw=s96-c",
|
||||
"closeTime":1653893940000,
|
||||
"question":"Will I write a new blog post today?",
|
||||
"description":"I'm supposed to, or else Beeminder charges me $90.\nTentative topic ideas:\n- \"Manifold funding, a history\"\n- \"Markets and bounties allow trades through time\"\n- \"equity vs money vs time\"\n\nClose date updated to 2022-05-29 11:59 pm",
|
||||
"tags":[
|
||||
"personal",
|
||||
"commitments"
|
||||
|
@ -148,6 +101,7 @@ Requires no authorization.
|
|||
// Market attributes. All times are in milliseconds since epoch
|
||||
closeTime?: number // Min of creator's chosen date, and resolutionTime
|
||||
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.
|
||||
|
@ -158,16 +112,13 @@ Requires no authorization.
|
|||
// i.e. https://manifold.markets/Austin/test-market is the same as https://manifold.markets/foo/test-market
|
||||
url: string
|
||||
|
||||
outcomeType: string // BINARY, FREE_RESPONSE, MULTIPLE_CHOICE, NUMERIC, or PSEUDO_NUMERIC
|
||||
outcomeType: string // BINARY, FREE_RESPONSE, or NUMERIC
|
||||
mechanism: string // dpm-2 or cpmm-1
|
||||
|
||||
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
|
||||
|
@ -177,8 +128,6 @@ Requires no authorization.
|
|||
resolutionTime?: number
|
||||
resolution?: string
|
||||
resolutionProbability?: number // Used for BINARY markets resolved to MKT
|
||||
|
||||
lastUpdatedTime?: number
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -411,9 +360,7 @@ Requires no authorization.
|
|||
type FullMarket = LiteMarket & {
|
||||
bets: Bet[]
|
||||
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
|
||||
answers?: Answer[]
|
||||
}
|
||||
|
||||
type Bet = {
|
||||
|
@ -451,64 +398,6 @@ Requires no authorization.
|
|||
```
|
||||
- Response type: A `FullMarket` ; same as above.
|
||||
|
||||
### `GET /v0/users`
|
||||
|
||||
Lists all users.
|
||||
|
||||
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.
|
||||
|
@ -522,20 +411,6 @@ Parameters:
|
|||
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:
|
||||
|
||||
|
@ -547,20 +422,15 @@ $ curl https://manifold.markets/api/v0/bet -X POST -H 'Content-Type: application
|
|||
"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`.
|
||||
- `outcomeType`: Required. One of `BINARY`, `FREE_RESPONSE`, or `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.
|
||||
|
||||
|
@ -572,12 +442,6 @@ 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:
|
||||
|
||||
|
@ -591,197 +455,8 @@ $ curl https://manifold.markets/api/v0/market -X POST -H 'Content-Type: applicat
|
|||
"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
|
||||
|
||||
- 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
|
||||
|
|
|
@ -8,40 +8,15 @@ A list of community-created projects built on, or related to, Manifold Markets.
|
|||
|
||||
## 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!
|
||||
- [CivicDashboard](https://civicdash.org/dashboard) - Uses Manifold to for tracked solutions for the SF city government
|
||||
- [Research.Bet](https://research.bet/) - Prediction market for scientific papers, using Manifold
|
||||
|
||||
## 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
|
||||
|
|
|
@ -15,22 +15,6 @@ Our community is the beating heart of Manifold; your individual contributions ar
|
|||
|
||||
## Awarded bounties
|
||||
|
||||
💥 *Awarded on 2022-10-07*
|
||||
|
||||
**[Pepe](https://manifold.markets/Pepe): M$10,000**
|
||||
**[Jack](https://manifold.markets/jack): M$2,000**
|
||||
**[Martin](https://manifold.markets/MartinRandall): M$2,000**
|
||||
**[Yev](https://manifold.markets/Yev): M$2,000**
|
||||
**[Michael](https://manifold.markets/MichaelWheatley): M$2,000**
|
||||
|
||||
- For discovering an infinite mana exploit using limit orders, and informing the Manifold team of it privately.
|
||||
|
||||
**[Matt](https://manifold.markets/MattP): M$5,000**
|
||||
**[Adrian](https://manifold.markets/ahalekelly): M$5,000**
|
||||
**[Yev](https://manifold.markets/Yev): M$5,000**
|
||||
|
||||
- For discovering an AMM liquidity exploit and informing the Manifold team of it privately.
|
||||
|
||||
🎈 *Awarded on 2022-06-14*
|
||||
|
||||
**[Wasabipesto](https://manifold.markets/wasabipesto): M$20,000**
|
||||
|
|
|
@ -4,7 +4,11 @@
|
|||
|
||||
### 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?
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ for the pool to be sorted into.
|
|||
- 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 creation fee for the first market created each day is provided by Manifold.
|
||||
- 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.
|
||||
|
|
|
@ -26,7 +26,8 @@ const config = {
|
|||
docs: {
|
||||
routeBasePath: '/',
|
||||
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/manifold/tree/main/docs/docs',
|
||||
remarkPlugins: [math],
|
||||
rehypePlugins: [katex],
|
||||
},
|
||||
|
@ -71,7 +72,7 @@ const config = {
|
|||
label: 'Docs',
|
||||
},
|
||||
{
|
||||
href: 'https://github.com/manifoldmarkets/manifold/tree/main/docs/docs',
|
||||
href: 'https://github.com/manifoldmarkets/docs',
|
||||
label: 'GitHub',
|
||||
position: 'right',
|
||||
},
|
||||
|
@ -115,7 +116,7 @@ const config = {
|
|||
},
|
||||
{
|
||||
label: 'GitHub',
|
||||
href: 'https://github.com/manifoldmarkets/manifold/',
|
||||
href: 'https://github.com/manifoldmarkets/docs',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -30,8 +30,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "2.0.0-beta.17",
|
||||
"@tsconfig/docusaurus": "^1.0.4",
|
||||
"@types/react": "^17.0.2"
|
||||
"@tsconfig/docusaurus": "^1.0.4"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
|
3
docs2/.env.example
Normal file
3
docs2/.env.example
Normal file
|
@ -0,0 +1,3 @@
|
|||
NEXT_PUBLIC_DOCSEARCH_APP_ID=R2IYF7ETH7
|
||||
NEXT_PUBLIC_DOCSEARCH_API_KEY=599cec31baffa4868cae4e79f180729b
|
||||
NEXT_PUBLIC_DOCSEARCH_INDEX_NAME=docsearch
|
3
docs2/.eslintrc.json
Normal file
3
docs2/.eslintrc.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
32
docs2/.gitignore
vendored
Normal file
32
docs2/.gitignore
vendored
Normal file
|
@ -0,0 +1,32 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
3
docs2/.vscode/extensions.json
vendored
Normal file
3
docs2/.vscode/extensions.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"recommendations": ["bradlc.vscode-tailwindcss"]
|
||||
}
|
129
docs2/LICENSE.md
Normal file
129
docs2/LICENSE.md
Normal file
|
@ -0,0 +1,129 @@
|
|||
# Tailwind UI License
|
||||
|
||||
## Personal License
|
||||
|
||||
Tailwind Labs Inc. grants you an on-going, non-exclusive license to use the Components and Templates.
|
||||
|
||||
The license grants permission to **one individual** (the Licensee) to access and use the Components and Templates.
|
||||
|
||||
You **can**:
|
||||
|
||||
- Use the Components and Templates to create unlimited End Products.
|
||||
- Modify the Components and Templates to create derivative components and templates. Those components and templates are subject to this license.
|
||||
- Use the Components and Templates to create unlimited End Products for unlimited Clients.
|
||||
- Use the Components and Templates to create End Products where the End Product is sold to End Users.
|
||||
- Use the Components and Templates to create End Products that are open source and freely available to End Users.
|
||||
|
||||
You **cannot**:
|
||||
|
||||
- Use the Components and Templates to create End Products that are designed to allow an End User to build their own End Products using the Components and Templates or derivatives of the Components and Templates.
|
||||
- Re-distribute the Components and Templates or derivatives of the Components and Templates separately from an End Product, neither in code or as design assets.
|
||||
- Share your access to the Components and Templates with any other individuals.
|
||||
- Use the Components and Templates to produce anything that may be deemed by Tailwind Labs Inc, in their sole and absolute discretion, to be competitive or in conflict with the business of Tailwind Labs Inc.
|
||||
|
||||
### Example usage
|
||||
|
||||
Examples of usage **allowed** by the license:
|
||||
|
||||
- Creating a personal website by yourself.
|
||||
- Creating a website or web application for a client that will be owned by that client.
|
||||
- Creating a commercial SaaS application (like an invoicing app for example) where end users have to pay a fee to use the application.
|
||||
- Creating a commercial self-hosted web application that is sold to end users for a one-time fee.
|
||||
- Creating a web application where the primary purpose is clearly not to simply re-distribute the components (like a conference organization app that uses the components for its UI for example) that is free and open source, where the source code is publicly available.
|
||||
|
||||
Examples of usage **not allowed** by the license:
|
||||
|
||||
- Creating a repository of your favorite Tailwind UI components or templates (or derivatives based on Tailwind UI components or templates) and publishing it publicly.
|
||||
- Creating a React or Vue version of Tailwind UI and making it available either for sale or for free.
|
||||
- Create a Figma or Sketch UI kit based on the Tailwind UI component designs.
|
||||
- Creating a "website builder" project where end users can build their own websites using components or templates included with or derived from Tailwind UI.
|
||||
- Creating a theme, template, or project starter kit using the components or templates and making it available either for sale or for free.
|
||||
- Creating an admin panel tool (like [Laravel Nova](https://nova.laravel.com/) or [ActiveAdmin](https://activeadmin.info/)) that is made available either for sale or for free.
|
||||
|
||||
In simple terms, use Tailwind UI for anything you like as long as it doesn't compete with Tailwind UI.
|
||||
|
||||
### Personal License Definitions
|
||||
|
||||
Licensee is the individual who has purchased a Personal License.
|
||||
|
||||
Components and Templates are the source code and design assets made available to the Licensee after purchasing a Tailwind UI license.
|
||||
|
||||
End Product is any artifact produced that incorporates the Components or Templates or derivatives of the Components or Templates.
|
||||
|
||||
End User is a user of an End Product.
|
||||
|
||||
Client is an individual or entity receiving custom professional services directly from the Licensee, produced specifically for that individual or entity. Customers of software-as-a-service products are not considered clients for the purpose of this document.
|
||||
|
||||
## Team License
|
||||
|
||||
Tailwind Labs Inc. grants you an on-going, non-exclusive license to use the Components and Templates.
|
||||
|
||||
The license grants permission for **up to 25 Employees and Contractors of the Licensee** to access and use the Components and Templates.
|
||||
|
||||
You **can**:
|
||||
|
||||
- Use the Components and Templates to create unlimited End Products.
|
||||
- Modify the Components and Templates to create derivative components and templates. Those components and templates are subject to this license.
|
||||
- Use the Components and Templates to create unlimited End Products for unlimited Clients.
|
||||
- Use the Components and Templates to create End Products where the End Product is sold to End Users.
|
||||
- Use the Components and Templates to create End Products that are open source and freely available to End Users.
|
||||
|
||||
You **cannot**:
|
||||
|
||||
- Use the Components or Templates to create End Products that are designed to allow an End User to build their own End Products using the Components or Templates or derivatives of the Components or Templates.
|
||||
- Re-distribute the Components or Templates or derivatives of the Components or Templates separately from an End Product.
|
||||
- Use the Components or Templates to create End Products that are the property of any individual or entity other than the Licensee or Clients of the Licensee.
|
||||
- Use the Components or Templates to produce anything that may be deemed by Tailwind Labs Inc, in their sole and absolute discretion, to be competitive or in conflict with the business of Tailwind Labs Inc.
|
||||
|
||||
### Example usage
|
||||
|
||||
Examples of usage **allowed** by the license:
|
||||
|
||||
- Creating a website for your company
|
||||
- Creating a website or web application for a client that will be owned by that client
|
||||
- Creating a commercial SaaS application (like an invoicing app for example) where end users have to pay a fee to use the application
|
||||
- Creating a commercial self-hosted web application that is sold to end users for a one-time fee
|
||||
- Creating a web application where the primary purpose is clearly not to simply re-distribute the components or templates (like a conference organization app that uses the components or a template for its UI for example) that is free and open source, where the source code is publicly available
|
||||
|
||||
Examples of use **not allowed** by the license:
|
||||
|
||||
- Creating a repository of your favorite Tailwind UI components or template (or derivatives based on Tailwind UI components or templates) and publishing it publicly
|
||||
- Creating a React or Vue version of Tailwind UI and making it available either for sale or for free
|
||||
- Creating a "website builder" project where end users can build their own websites using components or templates included with or derived from Tailwind UI
|
||||
- Creating a theme or template using the components or templates and making it available either for sale or for free
|
||||
- Creating an admin panel tool (like [Laravel Nova](https://nova.laravel.com/) or [ActiveAdmin](https://activeadmin.info/)) that is made available either for sale or for free
|
||||
- Creating any End Product that is not the sole property of either your company or a client of your company. For example your employees/contractors can't use your company Tailwind UI license to build their own websites or side projects.
|
||||
|
||||
### Team License Definitions
|
||||
|
||||
Licensee is the business entity who has purchased a Team License.
|
||||
|
||||
Components and Templates are the source code and design assets made available to the Licensee after purchasing a Tailwind UI license.
|
||||
|
||||
End Product is any artifact produced that incorporates the Components or Templates or derivatives of the Components or Templates.
|
||||
|
||||
End User is a user of an End Product.
|
||||
|
||||
Employee is a full-time or part-time employee of the Licensee.
|
||||
|
||||
Contractor is an individual or business entity contracted to perform services for the Licensee.
|
||||
|
||||
Client is an individual or entity receiving custom professional services directly from the Licensee, produced specifically for that individual or entity. Customers of software-as-a-service products are not considered clients for the purpose of this document.
|
||||
|
||||
## Enforcement
|
||||
|
||||
If you are found to be in violation of the license, access to your Tailwind UI account will be terminated, and a refund may be issued at our discretion. When license violation is blatant and malicious (such as intentionally redistributing the Components or Templates through private warez channels), no refund will be issued.
|
||||
|
||||
The copyright of the Components and Templates is owned by Tailwind Labs Inc. You are granted only the permissions described in this license; all other rights are reserved. Tailwind Labs Inc. reserves the right to pursue legal remedies for any unauthorized use of the Components or Templates outside the scope of this license.
|
||||
|
||||
## Liability
|
||||
|
||||
Tailwind Labs Inc.’s liability to you for costs, damages, or other losses arising from your use of the Components or Templates — including third-party claims against you — is limited to a refund of your license fee. Tailwind Labs Inc. may not be held liable for any consequential damages related to your use of the Components or Templates.
|
||||
|
||||
This Agreement is governed by the laws of the Province of Ontario and the applicable laws of Canada. Legal proceedings related to this Agreement may only be brought in the courts of Ontario. You agree to service of process at the e-mail address on your original order.
|
||||
|
||||
## Questions?
|
||||
|
||||
Unsure which license you need, or unsure if your use case is covered by our licenses?
|
||||
|
||||
Email us at [support@tailwindui.com](mailto:support@tailwindui.com) with your questions.
|
38
docs2/README.md
Normal file
38
docs2/README.md
Normal file
|
@ -0,0 +1,38 @@
|
|||
# Syntax
|
||||
|
||||
Syntax is a [Tailwind UI](https://tailwindui.com) site template built using [Tailwind CSS](https://tailwindcss.com) and [Next.js](https://nextjs.org).
|
||||
|
||||
## Getting started
|
||||
|
||||
To get started with this template, first install the npm dependencies:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
cp .env.example .env.local
|
||||
```
|
||||
|
||||
Next, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Finally, open [http://localhost:3000](http://localhost:3000) in your browser to view the website.
|
||||
|
||||
## Customizing
|
||||
|
||||
You can start editing this template by modifying the files in the `/src` folder. The site will auto-update as you edit these files.
|
||||
|
||||
## License
|
||||
|
||||
This site template is a commercial product and is licensed under the [Tailwind UI license](https://tailwindui.com/license).
|
||||
|
||||
## Learn more
|
||||
|
||||
To learn more about the technologies used in this site template, see the following resources:
|
||||
|
||||
- [Tailwind CSS](https://tailwindcss.com/docs) - the official Tailwind CSS documentation
|
||||
- [Next.js](https://nextjs.org/docs) - the official Next.js documentation
|
||||
- [Headless UI](https://headlessui.dev) - the official Headless UI documentation
|
||||
- [Markdoc](https://markdoc.io) - the official Markdoc documentation
|
||||
- [DocSearch](https://docsearch.algolia.com) - the official DocSearch documentation
|
8
docs2/jsconfig.json
Normal file
8
docs2/jsconfig.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
}
|
||||
}
|
26
docs2/markdoc/nodes.js
Normal file
26
docs2/markdoc/nodes.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { Fence } from '@/components/Fence'
|
||||
|
||||
const nodes = {
|
||||
document: {
|
||||
render: undefined,
|
||||
},
|
||||
th: {
|
||||
attributes: {
|
||||
scope: {
|
||||
type: String,
|
||||
default: 'col',
|
||||
},
|
||||
},
|
||||
render: (props) => <th {...props} />,
|
||||
},
|
||||
fence: {
|
||||
render: Fence,
|
||||
attributes: {
|
||||
language: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default nodes
|
47
docs2/markdoc/tags.js
Normal file
47
docs2/markdoc/tags.js
Normal file
|
@ -0,0 +1,47 @@
|
|||
import { Callout } from '@/components/Callout'
|
||||
import { LinkGrid } from '@/components/LinkGrid'
|
||||
|
||||
const tags = {
|
||||
callout: {
|
||||
attributes: {
|
||||
title: { type: String },
|
||||
type: {
|
||||
type: String,
|
||||
default: 'note',
|
||||
matches: ['note', 'warning'],
|
||||
errorLevel: 'critical',
|
||||
},
|
||||
},
|
||||
render: Callout,
|
||||
},
|
||||
figure: {
|
||||
selfClosing: true,
|
||||
attributes: {
|
||||
src: { type: String },
|
||||
alt: { type: String },
|
||||
caption: { type: String },
|
||||
},
|
||||
render: ({ src, alt = '', caption }) => (
|
||||
<figure>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img src={src} alt={alt} />
|
||||
<figcaption>{caption}</figcaption>
|
||||
</figure>
|
||||
),
|
||||
},
|
||||
'link-grid': {
|
||||
render: LinkGrid,
|
||||
},
|
||||
'link-grid-link': {
|
||||
selfClosing: true,
|
||||
render: LinkGrid.Link,
|
||||
attributes: {
|
||||
title: { type: String },
|
||||
description: { type: String },
|
||||
icon: { type: String },
|
||||
href: { type: String },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default tags
|
9
docs2/next.config.js
Normal file
9
docs2/next.config.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
const withMarkdoc = require('@markdoc/next.js')
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
pageExtensions: ['js', 'jsx', 'md'],
|
||||
}
|
||||
|
||||
module.exports = withMarkdoc()(nextConfig)
|
36
docs2/package.json
Normal file
36
docs2/package.json
Normal file
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"name": "tailwindui-documentation",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"browserslist": "defaults, not ie <= 11",
|
||||
"dependencies": {
|
||||
"@docsearch/react": "^3.1.0",
|
||||
"@headlessui/react": "^1.6.5",
|
||||
"@markdoc/markdoc": "^0.1.3",
|
||||
"@markdoc/next.js": "^0.1.4",
|
||||
"@sindresorhus/slugify": "^2.1.0",
|
||||
"@tailwindcss/typography": "^0.5.2",
|
||||
"autoprefixer": "^10.4.7",
|
||||
"clsx": "^1.1.1",
|
||||
"focus-visible": "^5.2.0",
|
||||
"next": "12.1.6",
|
||||
"postcss-focus-visible": "^6.0.4",
|
||||
"postcss-import": "^14.1.0",
|
||||
"prism-react-renderer": "^1.3.3",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"tailwindcss": "^3.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "8.17.0",
|
||||
"eslint-config-next": "12.1.6",
|
||||
"prettier": "^2.7.1",
|
||||
"prettier-plugin-tailwindcss": "^0.1.11"
|
||||
}
|
||||
}
|
10
docs2/postcss.config.js
Normal file
10
docs2/postcss.config.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
'postcss-import': {},
|
||||
tailwindcss: {},
|
||||
'postcss-focus-visible': {
|
||||
replaceWith: '[data-focus-visible-added]',
|
||||
},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
5
docs2/prettier.config.js
Normal file
5
docs2/prettier.config.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
module.exports = {
|
||||
singleQuote: true,
|
||||
semi: false,
|
||||
plugins: [require('prettier-plugin-tailwindcss')],
|
||||
}
|
BIN
docs2/public/favicon.ico
Normal file
BIN
docs2/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
BIN
docs2/public/fonts/Inter-italic.var.woff2
Normal file
BIN
docs2/public/fonts/Inter-italic.var.woff2
Normal file
Binary file not shown.
BIN
docs2/public/fonts/Inter-roman.var.woff2
Normal file
BIN
docs2/public/fonts/Inter-roman.var.woff2
Normal file
Binary file not shown.
93
docs2/public/fonts/lexend.txt
Normal file
93
docs2/public/fonts/lexend.txt
Normal file
|
@ -0,0 +1,93 @@
|
|||
Copyright 2018 The Lexend Project Authors (https://github.com/googlefonts/lexend), with Reserved Font Name “RevReading Lexend”.
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
BIN
docs2/public/fonts/lexend.woff2
Normal file
BIN
docs2/public/fonts/lexend.woff2
Normal file
Binary file not shown.
21
docs2/src/components/Button.jsx
Normal file
21
docs2/src/components/Button.jsx
Normal file
|
@ -0,0 +1,21 @@
|
|||
import Link from 'next/link'
|
||||
import clsx from 'clsx'
|
||||
|
||||
const styles = {
|
||||
primary:
|
||||
'rounded-full bg-sky-300 py-2 px-4 text-sm font-semibold text-slate-900 hover:bg-sky-200 active:bg-sky-500 focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-300/50',
|
||||
secondary:
|
||||
'rounded-full bg-slate-800 py-2 px-4 text-sm font-medium text-white hover:bg-slate-700 active:text-slate-400 focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white/50',
|
||||
}
|
||||
|
||||
export function Button({ variant = 'primary', className, ...props }) {
|
||||
return <button className={clsx(styles[variant], className)} {...props} />
|
||||
}
|
||||
|
||||
export function ButtonLink({ variant = 'primary', className, href, ...props }) {
|
||||
return (
|
||||
<Link href={href}>
|
||||
<a className={clsx(styles[variant], className)} {...props} />
|
||||
</Link>
|
||||
)
|
||||
}
|
41
docs2/src/components/Callout.jsx
Normal file
41
docs2/src/components/Callout.jsx
Normal file
|
@ -0,0 +1,41 @@
|
|||
import clsx from 'clsx'
|
||||
|
||||
import { Icon } from '@/components/Icon'
|
||||
|
||||
const styles = {
|
||||
note: {
|
||||
container:
|
||||
'bg-sky-50 dark:bg-slate-800/60 dark:ring-1 dark:ring-slate-300/10',
|
||||
title: 'text-sky-900 dark:text-sky-400',
|
||||
body: 'text-sky-800 prose-code:text-sky-900 dark:text-slate-300 dark:prose-code:text-slate-300 prose-a:text-sky-900 [--tw-prose-background:theme(colors.sky.50)]',
|
||||
},
|
||||
warning: {
|
||||
container:
|
||||
'bg-amber-50 dark:bg-slate-800/60 dark:ring-1 dark:ring-slate-300/10',
|
||||
title: 'text-amber-900 dark:text-amber-500',
|
||||
body: 'text-amber-800 prose-code:text-amber-900 prose-a:text-amber-900 [--tw-prose-underline:theme(colors.amber.400)] dark:[--tw-prose-underline:theme(colors.sky.700)] [--tw-prose-background:theme(colors.amber.50)] dark:text-slate-300 dark:prose-code:text-slate-300',
|
||||
},
|
||||
}
|
||||
|
||||
const icons = {
|
||||
note: (props) => <Icon icon="lightbulb" {...props} />,
|
||||
warning: (props) => <Icon icon="warning" color="amber" {...props} />,
|
||||
}
|
||||
|
||||
export function Callout({ type = 'note', title, children }) {
|
||||
let IconComponent = icons[type]
|
||||
|
||||
return (
|
||||
<div className={clsx('my-8 flex rounded-3xl p-6', styles[type].container)}>
|
||||
<IconComponent className="h-8 w-8 flex-none" />
|
||||
<div className="ml-4 flex-auto">
|
||||
<p className={clsx('m-0 font-display text-xl', styles[type].title)}>
|
||||
{title}
|
||||
</p>
|
||||
<div className={clsx('prose mt-2.5', styles[type].body)}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
28
docs2/src/components/Fence.jsx
Normal file
28
docs2/src/components/Fence.jsx
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { Fragment } from 'react'
|
||||
import Highlight, { defaultProps } from 'prism-react-renderer'
|
||||
|
||||
export function Fence({ children, language }) {
|
||||
return (
|
||||
<Highlight
|
||||
{...defaultProps}
|
||||
code={children.trimEnd()}
|
||||
language={language}
|
||||
theme={undefined}
|
||||
>
|
||||
{({ className, style, tokens, getTokenProps }) => (
|
||||
<pre className={className} style={style}>
|
||||
<code>
|
||||
{tokens.map((line, index) => (
|
||||
<Fragment key={index}>
|
||||
{line.map((token, index) => (
|
||||
<span key={index} {...getTokenProps({ token })} />
|
||||
))}
|
||||
{'\n'}
|
||||
</Fragment>
|
||||
))}
|
||||
</code>
|
||||
</pre>
|
||||
)}
|
||||
</Highlight>
|
||||
)
|
||||
}
|
179
docs2/src/components/Hero.jsx
Normal file
179
docs2/src/components/Hero.jsx
Normal file
|
@ -0,0 +1,179 @@
|
|||
import { Fragment } from 'react'
|
||||
import Image from 'next/image'
|
||||
import clsx from 'clsx'
|
||||
import Highlight, { defaultProps } from 'prism-react-renderer'
|
||||
|
||||
import { ButtonLink } from '@/components/Button'
|
||||
import { HeroBackground } from '@/components/HeroBackground'
|
||||
import blurCyanImage from '@/images/blur-cyan.png'
|
||||
import blurIndigoImage from '@/images/blur-indigo.png'
|
||||
|
||||
const codeLanguage = 'javascript'
|
||||
const code = `export default {
|
||||
strategy: 'predictive',
|
||||
engine: {
|
||||
cpus: 12,
|
||||
backups: ['./storage/cache.wtf'],
|
||||
},
|
||||
}`
|
||||
|
||||
const tabs = [
|
||||
{ name: 'cache-advance.config.js', isActive: true },
|
||||
{ name: 'package.json', isActive: false },
|
||||
]
|
||||
|
||||
export function Hero() {
|
||||
return (
|
||||
<div className="overflow-hidden bg-slate-900 dark:-mb-32 dark:-mt-[4.5rem] dark:pb-32 dark:pt-[4.5rem] dark:lg:-mt-[4.75rem] dark:lg:pt-[4.75rem]">
|
||||
<div className="py-16 sm:px-2 lg:relative lg:py-20 lg:px-0">
|
||||
<div className="mx-auto grid max-w-2xl grid-cols-1 items-center gap-y-16 gap-x-8 px-4 lg:max-w-8xl lg:grid-cols-2 lg:px-8 xl:gap-x-16 xl:px-12">
|
||||
<div className="relative z-10 md:text-center lg:text-left">
|
||||
<div className="absolute bottom-full right-full -mr-72 -mb-56 opacity-50">
|
||||
<Image
|
||||
src={blurCyanImage}
|
||||
alt=""
|
||||
layout="fixed"
|
||||
width={530}
|
||||
height={530}
|
||||
unoptimized
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<p className="inline bg-gradient-to-r from-indigo-200 via-sky-400 to-indigo-200 bg-clip-text font-display text-5xl tracking-tight text-transparent">
|
||||
Never miss the cache again.
|
||||
</p>
|
||||
<p className="mt-3 text-2xl tracking-tight text-slate-400">
|
||||
Cache every single thing your app could ever do ahead of time,
|
||||
so your code never even has to run at all.
|
||||
</p>
|
||||
<div className="mt-8 flex space-x-4 md:justify-center lg:justify-start">
|
||||
<ButtonLink href="/">Get started</ButtonLink>
|
||||
<ButtonLink href="/" variant="secondary">
|
||||
View on GitHub
|
||||
</ButtonLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative lg:static xl:pl-10">
|
||||
<div className="absolute inset-x-[-50vw] -top-32 -bottom-48 [mask-image:linear-gradient(transparent,white,white)] dark:[mask-image:linear-gradient(transparent,white,transparent)] lg:left-[calc(50%+14rem)] lg:right-0 lg:-top-32 lg:-bottom-32 lg:[mask-image:none] lg:dark:[mask-image:linear-gradient(white,white,transparent)]">
|
||||
<HeroBackground className="absolute top-1/2 left-1/2 -translate-y-1/2 -translate-x-1/2 lg:left-0 lg:translate-x-0 lg:-translate-y-[60%]" />
|
||||
</div>
|
||||
<div className="relative">
|
||||
<div className="absolute -top-64 -right-64">
|
||||
<Image
|
||||
src={blurCyanImage}
|
||||
alt=""
|
||||
layout="fixed"
|
||||
width={530}
|
||||
height={530}
|
||||
unoptimized
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute -bottom-40 -right-44">
|
||||
<Image
|
||||
src={blurIndigoImage}
|
||||
alt=""
|
||||
layout="fixed"
|
||||
width={567}
|
||||
height={567}
|
||||
unoptimized
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute inset-0 rounded-2xl bg-gradient-to-tr from-sky-300 via-sky-300/70 to-blue-300 opacity-10 blur-lg" />
|
||||
<div className="absolute inset-0 rounded-2xl bg-gradient-to-tr from-sky-300 via-sky-300/70 to-blue-300 opacity-10" />
|
||||
<div className="relative rounded-2xl bg-[#0A101F]/80 ring-1 ring-white/10 backdrop-blur">
|
||||
<div className="absolute -top-px left-20 right-11 h-px bg-gradient-to-r from-sky-300/0 via-sky-300/70 to-sky-300/0" />
|
||||
<div className="absolute -bottom-px left-11 right-20 h-px bg-gradient-to-r from-blue-400/0 via-blue-400 to-blue-400/0" />
|
||||
<div className="pl-4 pt-4">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="h-2.5 w-auto stroke-slate-500/30"
|
||||
fill="none"
|
||||
>
|
||||
<circle cx="5" cy="5" r="4.5" />
|
||||
<circle cx="21" cy="5" r="4.5" />
|
||||
<circle cx="37" cy="5" r="4.5" />
|
||||
</svg>
|
||||
<div className="mt-4 flex space-x-2 text-xs">
|
||||
{tabs.map((tab) => (
|
||||
<div
|
||||
key={tab.name}
|
||||
className={clsx('flex h-6 rounded-full', {
|
||||
'bg-gradient-to-r from-sky-400/30 via-sky-400 to-sky-400/30 p-px font-medium text-sky-300':
|
||||
tab.isActive,
|
||||
'text-slate-500': !tab.isActive,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'flex items-center rounded-full px-2.5',
|
||||
{ 'bg-slate-800': tab.isActive }
|
||||
)}
|
||||
>
|
||||
{tab.name}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-6 flex items-start px-1 text-sm">
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="select-none border-r border-slate-300/5 pr-4 font-mono text-slate-600"
|
||||
>
|
||||
{Array.from({
|
||||
length: code.split('\n').length,
|
||||
}).map((_, index) => (
|
||||
<Fragment key={index}>
|
||||
{(index + 1).toString().padStart(2, '0')}
|
||||
<br />
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
<Highlight
|
||||
{...defaultProps}
|
||||
code={code}
|
||||
language={codeLanguage}
|
||||
theme={undefined}
|
||||
>
|
||||
{({
|
||||
className,
|
||||
style,
|
||||
tokens,
|
||||
getLineProps,
|
||||
getTokenProps,
|
||||
}) => (
|
||||
<pre
|
||||
className={clsx(
|
||||
className,
|
||||
'flex overflow-x-auto pb-6'
|
||||
)}
|
||||
style={style}
|
||||
>
|
||||
<code className="px-4">
|
||||
{tokens.map((line, index) => (
|
||||
<div key={index} {...getLineProps({ line })}>
|
||||
{line.map((token, index) => (
|
||||
<span
|
||||
key={index}
|
||||
{...getTokenProps({ token })}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</code>
|
||||
</pre>
|
||||
)}
|
||||
</Highlight>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
181
docs2/src/components/HeroBackground.jsx
Normal file
181
docs2/src/components/HeroBackground.jsx
Normal file
|
@ -0,0 +1,181 @@
|
|||
import { useId } from 'react'
|
||||
|
||||
export function HeroBackground(props) {
|
||||
let id = useId()
|
||||
|
||||
return (
|
||||
<svg aria-hidden="true" width={668} height={1069} fill="none" {...props}>
|
||||
<defs>
|
||||
<clipPath id={`${id}-clip-path`}>
|
||||
<path
|
||||
fill="#fff"
|
||||
transform="rotate(-180 334 534.4)"
|
||||
d="M0 0h668v1068.8H0z"
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g opacity=".4" clipPath={`url(#${id}-clip-path)`} strokeWidth={4}>
|
||||
<path
|
||||
opacity=".3"
|
||||
d="M584.5 770.4v-474M484.5 770.4v-474M384.5 770.4v-474M283.5 769.4v-474M183.5 768.4v-474M83.5 767.4v-474"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<path
|
||||
d="M83.5 221.275v6.587a50.1 50.1 0 0 0 22.309 41.686l55.581 37.054a50.102 50.102 0 0 1 22.309 41.686v6.587M83.5 716.012v6.588a50.099 50.099 0 0 0 22.309 41.685l55.581 37.054a50.102 50.102 0 0 1 22.309 41.686v6.587M183.7 584.5v6.587a50.1 50.1 0 0 0 22.31 41.686l55.581 37.054a50.097 50.097 0 0 1 22.309 41.685v6.588M384.101 277.637v6.588a50.1 50.1 0 0 0 22.309 41.685l55.581 37.054a50.1 50.1 0 0 1 22.31 41.686v6.587M384.1 770.288v6.587a50.1 50.1 0 0 1-22.309 41.686l-55.581 37.054A50.099 50.099 0 0 0 283.9 897.3v6.588"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<path
|
||||
d="M384.1 770.288v6.587a50.1 50.1 0 0 1-22.309 41.686l-55.581 37.054A50.099 50.099 0 0 0 283.9 897.3v6.588M484.3 594.937v6.587a50.1 50.1 0 0 1-22.31 41.686l-55.581 37.054A50.1 50.1 0 0 0 384.1 721.95v6.587M484.3 872.575v6.587a50.1 50.1 0 0 1-22.31 41.686l-55.581 37.054a50.098 50.098 0 0 0-22.309 41.686v6.582M584.501 663.824v39.988a50.099 50.099 0 0 1-22.31 41.685l-55.581 37.054a50.102 50.102 0 0 0-22.309 41.686v6.587M283.899 945.637v6.588a50.1 50.1 0 0 1-22.309 41.685l-55.581 37.05a50.12 50.12 0 0 0-22.31 41.69v6.59M384.1 277.637c0 19.946 12.763 37.655 31.686 43.962l137.028 45.676c18.923 6.308 31.686 24.016 31.686 43.962M183.7 463.425v30.69c0 21.564 13.799 40.709 34.257 47.529l134.457 44.819c18.922 6.307 31.686 24.016 31.686 43.962M83.5 102.288c0 19.515 13.554 36.412 32.604 40.645l235.391 52.309c19.05 4.234 32.605 21.13 32.605 40.646M83.5 463.425v-58.45M183.699 542.75V396.625M283.9 1068.8V945.637M83.5 363.225v-141.95M83.5 179.524v-77.237M83.5 60.537V0M384.1 630.425V277.637M484.301 830.824V594.937M584.5 1068.8V663.825M484.301 555.275V452.988M584.5 622.075V452.988M384.1 728.537v-56.362M384.1 1068.8v-20.88M384.1 1006.17V770.287M283.9 903.888V759.85M183.699 1066.71V891.362M83.5 1068.8V716.012M83.5 674.263V505.175"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<circle
|
||||
cx="83.5"
|
||||
cy="384.1"
|
||||
r="10.438"
|
||||
transform="rotate(-180 83.5 384.1)"
|
||||
fill="#1E293B"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<circle
|
||||
cx="83.5"
|
||||
cy="200.399"
|
||||
r="10.438"
|
||||
transform="rotate(-180 83.5 200.399)"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<circle
|
||||
cx="83.5"
|
||||
cy="81.412"
|
||||
r="10.438"
|
||||
transform="rotate(-180 83.5 81.412)"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<circle
|
||||
cx="183.699"
|
||||
cy="375.75"
|
||||
r="10.438"
|
||||
transform="rotate(-180 183.699 375.75)"
|
||||
fill="#1E293B"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<circle
|
||||
cx="183.699"
|
||||
cy="563.625"
|
||||
r="10.438"
|
||||
transform="rotate(-180 183.699 563.625)"
|
||||
fill="#1E293B"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<circle
|
||||
cx="384.1"
|
||||
cy="651.3"
|
||||
r="10.438"
|
||||
transform="rotate(-180 384.1 651.3)"
|
||||
fill="#1E293B"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<circle
|
||||
cx="484.301"
|
||||
cy="574.062"
|
||||
r="10.438"
|
||||
transform="rotate(-180 484.301 574.062)"
|
||||
fill="#0EA5E9"
|
||||
fillOpacity=".42"
|
||||
stroke="#0EA5E9"
|
||||
/>
|
||||
<circle
|
||||
cx="384.1"
|
||||
cy="749.412"
|
||||
r="10.438"
|
||||
transform="rotate(-180 384.1 749.412)"
|
||||
fill="#1E293B"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<circle
|
||||
cx="384.1"
|
||||
cy="1027.05"
|
||||
r="10.438"
|
||||
transform="rotate(-180 384.1 1027.05)"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<circle
|
||||
cx="283.9"
|
||||
cy="924.763"
|
||||
r="10.438"
|
||||
transform="rotate(-180 283.9 924.763)"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<circle
|
||||
cx="183.699"
|
||||
cy="870.487"
|
||||
r="10.438"
|
||||
transform="rotate(-180 183.699 870.487)"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<circle
|
||||
cx="283.9"
|
||||
cy="738.975"
|
||||
r="10.438"
|
||||
transform="rotate(-180 283.9 738.975)"
|
||||
fill="#1E293B"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<circle
|
||||
cx="83.5"
|
||||
cy="695.138"
|
||||
r="10.438"
|
||||
transform="rotate(-180 83.5 695.138)"
|
||||
fill="#1E293B"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<circle
|
||||
cx="83.5"
|
||||
cy="484.3"
|
||||
r="10.438"
|
||||
transform="rotate(-180 83.5 484.3)"
|
||||
fill="#0EA5E9"
|
||||
fillOpacity=".42"
|
||||
stroke="#0EA5E9"
|
||||
/>
|
||||
<circle
|
||||
cx="484.301"
|
||||
cy="432.112"
|
||||
r="10.438"
|
||||
transform="rotate(-180 484.301 432.112)"
|
||||
fill="#1E293B"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<circle
|
||||
cx="584.5"
|
||||
cy="432.112"
|
||||
r="10.438"
|
||||
transform="rotate(-180 584.5 432.112)"
|
||||
fill="#1E293B"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<circle
|
||||
cx="584.5"
|
||||
cy="642.95"
|
||||
r="10.438"
|
||||
transform="rotate(-180 584.5 642.95)"
|
||||
fill="#1E293B"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<circle
|
||||
cx="484.301"
|
||||
cy="851.699"
|
||||
r="10.438"
|
||||
transform="rotate(-180 484.301 851.699)"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<circle
|
||||
cx="384.1"
|
||||
cy="256.763"
|
||||
r="10.438"
|
||||
transform="rotate(-180 384.1 256.763)"
|
||||
stroke="#334155"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
77
docs2/src/components/Icon.jsx
Normal file
77
docs2/src/components/Icon.jsx
Normal file
|
@ -0,0 +1,77 @@
|
|||
import { useId } from 'react'
|
||||
import clsx from 'clsx'
|
||||
|
||||
import { InstallationIcon } from '@/components/icons/InstallationIcon'
|
||||
import { LightbulbIcon } from '@/components/icons/LightbulbIcon'
|
||||
import { PluginsIcon } from '@/components/icons/PluginsIcon'
|
||||
import { PresetsIcon } from '@/components/icons/PresetsIcon'
|
||||
import { ThemingIcon } from '@/components/icons/ThemingIcon'
|
||||
import { WarningIcon } from '@/components/icons/WarningIcon'
|
||||
|
||||
const icons = {
|
||||
installation: InstallationIcon,
|
||||
presets: PresetsIcon,
|
||||
plugins: PluginsIcon,
|
||||
theming: ThemingIcon,
|
||||
lightbulb: LightbulbIcon,
|
||||
warning: WarningIcon,
|
||||
}
|
||||
|
||||
const iconStyles = {
|
||||
blue: '[--icon-foreground:theme(colors.slate.900)] [--icon-background:theme(colors.white)]',
|
||||
amber:
|
||||
'[--icon-foreground:theme(colors.amber.900)] [--icon-background:theme(colors.amber.100)]',
|
||||
}
|
||||
|
||||
export function Icon({ color = 'blue', icon, className, ...props }) {
|
||||
let id = useId()
|
||||
let IconComponent = icons[icon]
|
||||
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
viewBox="0 0 32 32"
|
||||
fill="none"
|
||||
className={clsx(className, iconStyles[color])}
|
||||
{...props}
|
||||
>
|
||||
<IconComponent id={id} color={color} />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
const gradients = {
|
||||
blue: [
|
||||
{ stopColor: '#0EA5E9' },
|
||||
{ stopColor: '#22D3EE', offset: '.527' },
|
||||
{ stopColor: '#818CF8', offset: 1 },
|
||||
],
|
||||
amber: [
|
||||
{ stopColor: '#FDE68A', offset: '.08' },
|
||||
{ stopColor: '#F59E0B', offset: '.837' },
|
||||
],
|
||||
}
|
||||
|
||||
export function Gradient({ color = 'blue', ...props }) {
|
||||
return (
|
||||
<radialGradient
|
||||
cx={0}
|
||||
cy={0}
|
||||
r={1}
|
||||
gradientUnits="userSpaceOnUse"
|
||||
{...props}
|
||||
>
|
||||
{gradients[color].map((stop, index) => (
|
||||
<stop key={index} {...stop} />
|
||||
))}
|
||||
</radialGradient>
|
||||
)
|
||||
}
|
||||
|
||||
export function LightMode({ className, ...props }) {
|
||||
return <g className={clsx('dark:hidden', className)} {...props} />
|
||||
}
|
||||
|
||||
export function DarkMode({ className, ...props }) {
|
||||
return <g className={clsx('hidden dark:inline', className)} {...props} />
|
||||
}
|
275
docs2/src/components/Layout.jsx
Normal file
275
docs2/src/components/Layout.jsx
Normal file
|
@ -0,0 +1,275 @@
|
|||
import { useCallback, useEffect, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import clsx from 'clsx'
|
||||
|
||||
import { Hero } from '@/components/Hero'
|
||||
import { Logo } from '@/components/Logo'
|
||||
import { MobileNavigation } from '@/components/MobileNavigation'
|
||||
import { Navigation } from '@/components/Navigation'
|
||||
import { Prose } from '@/components/Prose'
|
||||
import { Search } from '@/components/Search'
|
||||
import { ThemeSelector } from '@/components/ThemeSelector'
|
||||
|
||||
function Header({ navigation }) {
|
||||
let [isScrolled, setIsScrolled] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
function onScroll() {
|
||||
setIsScrolled(window.scrollY > 0)
|
||||
}
|
||||
onScroll()
|
||||
window.addEventListener('scroll', onScroll)
|
||||
return () => {
|
||||
window.removeEventListener('scroll', onScroll)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<header
|
||||
className={clsx(
|
||||
'sticky top-0 z-50 flex flex-wrap items-center justify-between bg-white px-4 py-5 shadow-md shadow-slate-900/5 transition duration-500 dark:shadow-none sm:px-6 lg:px-8',
|
||||
{
|
||||
'dark:bg-slate-900/95 dark:backdrop-blur dark:[@supports(backdrop-filter:blur(0))]:bg-slate-900/75':
|
||||
isScrolled,
|
||||
'dark:bg-transparent': !isScrolled,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div className="mr-6 lg:hidden">
|
||||
<MobileNavigation navigation={navigation} />
|
||||
</div>
|
||||
<div className="relative flex flex-grow basis-0 items-center">
|
||||
<Link href="/">
|
||||
<a className="block w-10 overflow-hidden lg:w-auto">
|
||||
<span className="sr-only">Home page</span>
|
||||
<Logo />
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="-my-5 mr-6 sm:mr-8 md:mr-0">
|
||||
<Search />
|
||||
</div>
|
||||
<div className="relative flex basis-0 justify-end space-x-6 sm:space-x-8 md:flex-grow">
|
||||
<ThemeSelector className="relative z-10" />
|
||||
<Link href="https://github.com">
|
||||
<a className="group">
|
||||
<span className="sr-only">GitHub</span>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
viewBox="0 0 16 16"
|
||||
className="h-6 w-6 fill-slate-400 group-hover:fill-slate-500 dark:group-hover:fill-slate-300"
|
||||
>
|
||||
<path d="M8 0C3.58 0 0 3.58 0 8C0 11.54 2.29 14.53 5.47 15.59C5.87 15.66 6.02 15.42 6.02 15.21C6.02 15.02 6.01 14.39 6.01 13.72C4 14.09 3.48 13.23 3.32 12.78C3.23 12.55 2.84 11.84 2.5 11.65C2.22 11.5 1.82 11.13 2.49 11.12C3.12 11.11 3.57 11.7 3.72 11.94C4.44 13.15 5.59 12.81 6.05 12.6C6.12 12.08 6.33 11.73 6.56 11.53C4.78 11.33 2.92 10.64 2.92 7.58C2.92 6.71 3.23 5.99 3.74 5.43C3.66 5.23 3.38 4.41 3.82 3.31C3.82 3.31 4.49 3.1 6.02 4.13C6.66 3.95 7.34 3.86 8.02 3.86C8.7 3.86 9.38 3.95 10.02 4.13C11.55 3.09 12.22 3.31 12.22 3.31C12.66 4.41 12.38 5.23 12.3 5.43C12.81 5.99 13.12 6.7 13.12 7.58C13.12 10.65 11.25 11.33 9.47 11.53C9.76 11.78 10.01 12.26 10.01 13.01C10.01 14.08 10 14.94 10 15.21C10 15.42 10.15 15.67 10.55 15.59C13.71 14.53 16 11.53 16 8C16 3.58 12.42 0 8 0Z" />
|
||||
</svg>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
export function Layout({ children, title, navigation, tableOfContents }) {
|
||||
let router = useRouter()
|
||||
let isHomePage = router.pathname === '/'
|
||||
let allLinks = navigation.flatMap((section) => section.links)
|
||||
let linkIndex = allLinks.findIndex((link) => link.href === router.pathname)
|
||||
let previousPage = allLinks[linkIndex - 1]
|
||||
let nextPage = allLinks[linkIndex + 1]
|
||||
let section = navigation.find((section) =>
|
||||
section.links.find((link) => link.href === router.pathname)
|
||||
)
|
||||
let currentSection = useTableOfContents(tableOfContents)
|
||||
|
||||
function isActive(section) {
|
||||
if (section.id === currentSection) {
|
||||
return true
|
||||
}
|
||||
if (!section.children) {
|
||||
return false
|
||||
}
|
||||
return section.children.findIndex(isActive) > -1
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header navigation={navigation} />
|
||||
|
||||
{isHomePage && <Hero />}
|
||||
|
||||
<div className="relative mx-auto flex max-w-8xl justify-center sm:px-2 lg:px-8 xl:px-12">
|
||||
<div className="hidden lg:relative lg:block lg:flex-none">
|
||||
<div className="absolute inset-y-0 right-0 w-[50vw] bg-slate-50 dark:hidden" />
|
||||
<div className="sticky top-[4.5rem] -ml-0.5 h-[calc(100vh-4.5rem)] overflow-y-auto py-16 pl-0.5">
|
||||
<div className="absolute top-16 bottom-0 right-0 hidden h-12 w-px bg-gradient-to-t from-slate-800 dark:block" />
|
||||
<div className="absolute top-28 bottom-0 right-0 hidden w-px bg-slate-800 dark:block" />
|
||||
<Navigation
|
||||
navigation={navigation}
|
||||
className="w-64 pr-8 xl:w-72 xl:pr-16"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-w-0 max-w-2xl flex-auto px-4 py-16 lg:max-w-none lg:pr-0 lg:pl-8 xl:px-16">
|
||||
<article>
|
||||
{(title || section) && (
|
||||
<header className="mb-9 space-y-1">
|
||||
{section && (
|
||||
<p className="font-display text-sm font-medium text-sky-500">
|
||||
{section.title}
|
||||
</p>
|
||||
)}
|
||||
{title && (
|
||||
<h1 className="font-display text-3xl tracking-tight text-slate-900 dark:text-white">
|
||||
{title}
|
||||
</h1>
|
||||
)}
|
||||
</header>
|
||||
)}
|
||||
<Prose>{children}</Prose>
|
||||
</article>
|
||||
<dl className="mt-12 flex border-t border-slate-200 pt-6 dark:border-slate-800">
|
||||
{previousPage && (
|
||||
<div>
|
||||
<dt className="font-display text-sm font-medium text-slate-900 dark:text-white">
|
||||
Previous
|
||||
</dt>
|
||||
<dd className="mt-1">
|
||||
<Link href={previousPage.href}>
|
||||
<a className="text-base font-semibold text-slate-500 hover:text-slate-600 dark:text-slate-400 dark:hover:text-slate-300">
|
||||
← {previousPage.title}
|
||||
</a>
|
||||
</Link>
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
{nextPage && (
|
||||
<div className="ml-auto text-right">
|
||||
<dt className="font-display text-sm font-medium text-slate-900 dark:text-white">
|
||||
Next
|
||||
</dt>
|
||||
<dd className="mt-1">
|
||||
<Link href={nextPage.href}>
|
||||
<a className="text-base font-semibold text-slate-500 hover:text-slate-600 dark:text-slate-400 dark:hover:text-slate-300">
|
||||
{nextPage.title} →
|
||||
</a>
|
||||
</Link>
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
</dl>
|
||||
</div>
|
||||
<div className="hidden xl:sticky xl:top-[4.5rem] xl:-mr-6 xl:block xl:h-[calc(100vh-4.5rem)] xl:flex-none xl:overflow-y-auto xl:py-16 xl:pr-6">
|
||||
<nav aria-labelledby="on-this-page-title" className="w-56">
|
||||
{tableOfContents.length > 0 && (
|
||||
<>
|
||||
<h2
|
||||
id="on-this-page-title"
|
||||
className="font-display text-sm font-medium text-slate-900 dark:text-white"
|
||||
>
|
||||
On this page
|
||||
</h2>
|
||||
<ul className="mt-4 space-y-3 text-sm">
|
||||
{tableOfContents.map((section) => (
|
||||
<li key={section.id}>
|
||||
<h3>
|
||||
<Link href={`#${section.id}`}>
|
||||
<a
|
||||
className={clsx(
|
||||
isActive(section)
|
||||
? 'text-sky-500'
|
||||
: 'font-normal text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-300'
|
||||
)}
|
||||
>
|
||||
{section.title}
|
||||
</a>
|
||||
</Link>
|
||||
</h3>
|
||||
{section.children.length > 0 && (
|
||||
<ul className="mt-2 space-y-3 pl-5 text-slate-500 dark:text-slate-400">
|
||||
{section.children.map((subSection) => (
|
||||
<li key={subSection.id}>
|
||||
<Link href={`#${subSection.id}`}>
|
||||
<a
|
||||
className={
|
||||
isActive(subSection)
|
||||
? 'text-sky-500'
|
||||
: 'hover:text-slate-600 dark:hover:text-slate-300'
|
||||
}
|
||||
>
|
||||
{subSection.title}
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function useTableOfContents(tableOfContents) {
|
||||
let [currentSection, setCurrentSection] = useState(tableOfContents[0]?.id)
|
||||
|
||||
let getHeadings = useCallback(() => {
|
||||
function* traverse(node) {
|
||||
if (Array.isArray(node)) {
|
||||
for (let child of node) {
|
||||
yield* traverse(child)
|
||||
}
|
||||
} else {
|
||||
let el = document.getElementById(node.id)
|
||||
if (!el) return
|
||||
|
||||
let style = window.getComputedStyle(el)
|
||||
let scrollMt = parseFloat(style.scrollMarginTop)
|
||||
|
||||
let top = window.scrollY + el.getBoundingClientRect().top - scrollMt
|
||||
yield { id: node.id, top }
|
||||
|
||||
for (let child of node.children ?? []) {
|
||||
yield* traverse(child)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(traverse(tableOfContents))
|
||||
}, [tableOfContents])
|
||||
|
||||
useEffect(() => {
|
||||
let headings = getHeadings()
|
||||
if (tableOfContents.length === 0 || headings.length === 0) return
|
||||
function onScroll() {
|
||||
let sortedHeadings = headings.concat([]).sort((a, b) => a.top - b.top)
|
||||
|
||||
let top = window.pageYOffset
|
||||
let current = sortedHeadings[0].id
|
||||
for (let i = 0; i < sortedHeadings.length; i++) {
|
||||
if (top >= sortedHeadings[i].top) {
|
||||
current = sortedHeadings[i].id
|
||||
}
|
||||
}
|
||||
setCurrentSection(current)
|
||||
}
|
||||
window.addEventListener('scroll', onScroll, {
|
||||
capture: true,
|
||||
passive: true,
|
||||
})
|
||||
onScroll()
|
||||
return () => {
|
||||
window.removeEventListener('scroll', onScroll, {
|
||||
capture: true,
|
||||
passive: true,
|
||||
})
|
||||
}
|
||||
}, [getHeadings, tableOfContents])
|
||||
|
||||
return currentSection
|
||||
}
|
33
docs2/src/components/LinkGrid.jsx
Normal file
33
docs2/src/components/LinkGrid.jsx
Normal file
|
@ -0,0 +1,33 @@
|
|||
import NextLink from 'next/link'
|
||||
|
||||
import { Icon } from '@/components/Icon'
|
||||
|
||||
export function LinkGrid({ children }) {
|
||||
return (
|
||||
<div className="not-prose my-12 grid grid-cols-1 gap-6 sm:grid-cols-2">
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
LinkGrid.Link = function Link({ title, description, href, icon }) {
|
||||
return (
|
||||
<div className="group relative rounded-xl border border-slate-200 dark:border-slate-800">
|
||||
<div className="absolute -inset-px rounded-xl border-2 border-transparent opacity-0 [background:linear-gradient(var(--link-grid-hover-bg,theme(colors.sky.50)),var(--link-grid-hover-bg,theme(colors.sky.50)))_padding-box,linear-gradient(to_top,theme(colors.indigo.400),theme(colors.cyan.400),theme(colors.sky.500))_border-box] group-hover:opacity-100 dark:[--link-grid-hover-bg:theme(colors.slate.800)]" />
|
||||
<div className="relative overflow-hidden rounded-xl p-6">
|
||||
<Icon icon={icon} className="h-8 w-8" />
|
||||
<h2 className="mt-4 font-display text-base text-slate-900 dark:text-white">
|
||||
<NextLink href={href}>
|
||||
<a>
|
||||
<span className="absolute -inset-px rounded-xl" />
|
||||
{title}
|
||||
</a>
|
||||
</NextLink>
|
||||
</h2>
|
||||
<p className="mt-1 text-sm text-slate-700 dark:text-slate-400">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
26
docs2/src/components/Logo.jsx
Normal file
26
docs2/src/components/Logo.jsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
export function Logo() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 227 36"
|
||||
className="h-9 w-auto fill-slate-700 dark:fill-sky-100"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="#38BDF8"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={3}
|
||||
d="M10.308 5L18 17.5 10.308 30 2.615 17.5 10.308 5z"
|
||||
/>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="#38BDF8"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={3}
|
||||
d="M18 17.5L10.308 5h15.144l7.933 12.5M18 17.5h15.385L25.452 30H10.308L18 17.5z"
|
||||
/>
|
||||
<path d="M55.96 26.2c-1.027 0-1.973-.173-2.84-.52a6.96 6.96 0 01-2.24-1.5 6.979 6.979 0 01-1.46-2.3c-.347-.893-.52-1.867-.52-2.92 0-1.027.18-1.973.54-2.84a6.71 6.71 0 011.52-2.28 6.922 6.922 0 012.3-1.52 7.48 7.48 0 012.86-.54c.667 0 1.32.093 1.96.28a6.12 6.12 0 011.78.78 5.7 5.7 0 011.4 1.24l-1.88 2.08a6.272 6.272 0 00-1-.82 3.728 3.728 0 00-1.08-.54 3.542 3.542 0 00-1.2-.2 4.14 4.14 0 00-1.62.32 3.991 3.991 0 00-1.3.9 4.197 4.197 0 00-.9 1.38 4.755 4.755 0 00-.32 1.78c0 .667.107 1.273.32 1.82.213.533.513.993.9 1.38.387.373.847.667 1.38.88.547.2 1.147.3 1.8.3a4.345 4.345 0 002.34-.68c.347-.213.653-.46.92-.74l1.46 2.34c-.32.36-.753.687-1.3.98a7.784 7.784 0 01-1.8.7c-.667.16-1.34.24-2.02.24zm6.99-.2l5.48-14h2.68l5.46 14h-3.08l-2.82-7.54c-.08-.213-.18-.487-.3-.82a922.595 922.595 0 00-.68-2.12 13.694 13.694 0 01-.24-.86l.54-.02c-.08.307-.174.627-.28.96-.094.32-.194.653-.3 1-.108.333-.22.66-.34.98-.12.32-.234.633-.34.94L65.91 26h-2.96zm2.54-2.94l.98-2.42h6.42l1 2.42h-8.4zm19.794 3.14c-1.026 0-1.973-.173-2.84-.52a6.96 6.96 0 01-2.24-1.5 6.98 6.98 0 01-1.46-2.3c-.346-.893-.52-1.867-.52-2.92 0-1.027.18-1.973.54-2.84a6.71 6.71 0 011.52-2.28 6.923 6.923 0 012.3-1.52 7.48 7.48 0 012.86-.54c.667 0 1.32.093 1.96.28a6.118 6.118 0 011.78.78c.547.347 1.014.76 1.4 1.24l-1.88 2.08a6.272 6.272 0 00-1-.82 3.728 3.728 0 00-1.08-.54 3.542 3.542 0 00-1.2-.2 4.14 4.14 0 00-1.62.32 3.992 3.992 0 00-1.3.9 4.197 4.197 0 00-.9 1.38 4.755 4.755 0 00-.32 1.78c0 .667.107 1.273.32 1.82.214.533.514.993.9 1.38.387.373.847.667 1.38.88.547.2 1.147.3 1.8.3a4.345 4.345 0 002.34-.68 4.53 4.53 0 00.92-.74l1.46 2.34c-.32.36-.753.687-1.3.98a7.784 7.784 0 01-1.8.7c-.666.16-1.34.24-2.02.24zm17.469-.2V12h3v14h-3zm-8.82 0V12h3v14h-3zm1.2-5.62l.02-2.72h9.14v2.72h-9.16zM110.402 26V12h9.46v2.64h-6.54v8.72h6.68V26h-9.6zm1.4-5.86v-2.56h7.1v2.56h-7.1zM122.437 26l5.48-14h2.68l5.46 14h-3.08l-2.82-7.54c-.08-.213-.18-.487-.3-.82l-.34-1.06-.34-1.06a14.73 14.73 0 01-.24-.86l.54-.02c-.08.307-.173.627-.28.96a63.3 63.3 0 01-.3 1c-.106.333-.22.66-.34.98-.12.32-.233.633-.34.94l-2.82 7.48h-2.96zm2.54-2.94l.98-2.42h6.42l1 2.42h-8.4zM139.023 26V12h5.74c1.027 0 1.953.173 2.78.52.84.333 1.56.813 2.16 1.44a6.097 6.097 0 011.4 2.2c.32.853.48 1.8.48 2.84 0 1.027-.16 1.973-.48 2.84a6.438 6.438 0 01-1.38 2.22 6.394 6.394 0 01-2.16 1.44c-.84.333-1.773.5-2.8.5h-5.74zm3-2.18l-.32-.52h2.96c.6 0 1.14-.1 1.62-.3.48-.213.887-.5 1.22-.86.347-.373.607-.827.78-1.36.173-.533.26-1.127.26-1.78a5.56 5.56 0 00-.26-1.76 3.595 3.595 0 00-.78-1.36 3.323 3.323 0 00-1.22-.86 3.948 3.948 0 00-1.62-.32h-3.02l.38-.48v9.6zM158.671 26l-5.58-14h3.18l2.92 7.58c.16.413.293.78.4 1.1.12.307.22.6.3.88.093.267.18.533.26.8.08.253.16.533.24.84l-.58.02c.107-.413.213-.793.32-1.14.107-.36.227-.733.36-1.12.133-.387.3-.847.5-1.38l2.76-7.58h3.16l-5.62 14h-2.62zm8.114 0l5.48-14h2.68l5.46 14h-3.08l-2.82-7.54c-.08-.213-.18-.487-.3-.82l-.34-1.06-.34-1.06a13.293 13.293 0 01-.24-.86l.54-.02c-.08.307-.173.627-.28.96a63.3 63.3 0 01-.3 1c-.107.333-.22.66-.34.98-.12.32-.233.633-.34.94l-2.82 7.48h-2.96zm2.54-2.94l.98-2.42h6.42l1 2.42h-8.4zM183.371 26V12h2.68l7.74 10.46h-.56c-.054-.413-.1-.813-.14-1.2l-.12-1.2c-.027-.413-.054-.833-.08-1.26-.014-.44-.027-.9-.04-1.38a56.825 56.825 0 01-.02-1.6V12h2.94v14h-2.72l-7.9-10.56.76.02c.066.693.12 1.287.16 1.78a36.623 36.623 0 01.18 2.2c.026.267.04.52.04.76.013.24.02.493.02.76V26h-2.94zm23.175.2c-1.027 0-1.973-.173-2.84-.52-.853-.36-1.6-.86-2.24-1.5a6.979 6.979 0 01-1.46-2.3c-.347-.893-.52-1.867-.52-2.92 0-1.027.18-1.973.54-2.84a6.71 6.71 0 011.52-2.28 6.919 6.919 0 012.3-1.52 7.48 7.48 0 012.86-.54c.667 0 1.32.093 1.96.28a6.12 6.12 0 011.78.78 5.7 5.7 0 011.4 1.24l-1.88 2.08a6.259 6.259 0 00-1-.82 3.721 3.721 0 00-1.08-.54 3.54 3.54 0 00-1.2-.2 4.14 4.14 0 00-1.62.32 3.991 3.991 0 00-1.3.9 4.206 4.206 0 00-.9 1.38 4.76 4.76 0 00-.32 1.78c0 .667.107 1.273.32 1.82.213.533.513.993.9 1.38.387.373.847.667 1.38.88.547.2 1.147.3 1.8.3a4.35 4.35 0 002.34-.68c.347-.213.653-.46.92-.74l1.46 2.34c-.32.36-.753.687-1.3.98a7.773 7.773 0 01-1.8.7c-.667.16-1.34.24-2.02.24zm8.649-.2V12h9.46v2.64h-6.54v8.72h6.68V26h-9.6zm1.4-5.86v-2.56h7.1v2.56h-7.1z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
79
docs2/src/components/MobileNavigation.jsx
Normal file
79
docs2/src/components/MobileNavigation.jsx
Normal file
|
@ -0,0 +1,79 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { Dialog } from '@headlessui/react'
|
||||
|
||||
import { Logo } from '@/components/Logo'
|
||||
import { Navigation } from '@/components/Navigation'
|
||||
|
||||
export function MobileNavigation({ navigation }) {
|
||||
let router = useRouter()
|
||||
let [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return
|
||||
|
||||
function onRouteChange() {
|
||||
setIsOpen(false)
|
||||
}
|
||||
|
||||
router.events.on('routeChangeComplete', onRouteChange)
|
||||
router.events.on('routeChangeError', onRouteChange)
|
||||
|
||||
return () => {
|
||||
router.events.off('routeChangeComplete', onRouteChange)
|
||||
router.events.off('routeChangeError', onRouteChange)
|
||||
}
|
||||
}, [router, isOpen])
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsOpen(true)}
|
||||
className="relative"
|
||||
>
|
||||
<span className="sr-only">Open navigation</span>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="h-6 w-6 stroke-slate-500"
|
||||
fill="none"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
>
|
||||
<path d="M4 7h16M4 12h16M4 17h16" />
|
||||
</svg>
|
||||
</button>
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
onClose={setIsOpen}
|
||||
className="fixed inset-0 z-50 flex items-start overflow-y-auto bg-slate-900/50 pr-10 backdrop-blur lg:hidden"
|
||||
>
|
||||
<Dialog.Panel className="min-h-full w-full max-w-xs bg-white px-4 pt-5 pb-12 dark:bg-slate-900 sm:px-6">
|
||||
<Dialog.Title className="sr-only">Navigation</Dialog.Title>
|
||||
<div className="flex items-center">
|
||||
<button type="button" onClick={() => setIsOpen(false)}>
|
||||
<span className="sr-only">Close navigation</span>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="h-6 w-6 stroke-slate-500"
|
||||
fill="none"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
>
|
||||
<path d="M5 5l14 14M19 5l-14 14" />
|
||||
</svg>
|
||||
</button>
|
||||
<Link href="/">
|
||||
<a className="ml-6 block w-10 overflow-hidden lg:w-auto">
|
||||
<span className="sr-only">Home page</span>
|
||||
<Logo />
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<Navigation navigation={navigation} className="mt-5 px-1" />
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
42
docs2/src/components/Navigation.jsx
Normal file
42
docs2/src/components/Navigation.jsx
Normal file
|
@ -0,0 +1,42 @@
|
|||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import clsx from 'clsx'
|
||||
|
||||
export function Navigation({ navigation, className }) {
|
||||
let router = useRouter()
|
||||
|
||||
return (
|
||||
<nav className={clsx('text-base lg:text-sm', className)}>
|
||||
<ul className="space-y-9">
|
||||
{navigation.map((section) => (
|
||||
<li key={section.title}>
|
||||
<h2 className="font-display font-medium text-slate-900 dark:text-white">
|
||||
{section.title}
|
||||
</h2>
|
||||
<ul className="mt-2 space-y-2 border-l-2 border-slate-100 dark:border-slate-800 lg:mt-4 lg:space-y-4 lg:border-slate-200">
|
||||
{section.links.map((link) => (
|
||||
<li key={link.href} className="relative">
|
||||
<Link href={link.href}>
|
||||
<a
|
||||
className={clsx(
|
||||
'block w-full pl-3.5 before:pointer-events-none before:absolute before:-left-1 before:top-1/2 before:h-1.5 before:w-1.5 before:-translate-y-1/2 before:rounded-full',
|
||||
{
|
||||
'font-semibold text-sky-500 before:bg-sky-500':
|
||||
link.href === router.pathname,
|
||||
'text-slate-500 before:hidden before:bg-slate-300 hover:text-slate-600 hover:before:block dark:text-slate-400 dark:before:bg-slate-700 dark:hover:text-slate-300':
|
||||
link.href !== router.pathname,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{link.title}
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
)
|
||||
}
|
25
docs2/src/components/Prose.jsx
Normal file
25
docs2/src/components/Prose.jsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
import clsx from 'clsx'
|
||||
|
||||
export function Prose({ as: Component = 'div', className, ...props }) {
|
||||
return (
|
||||
<Component
|
||||
className={clsx(
|
||||
className,
|
||||
'prose prose-slate max-w-none dark:prose-invert dark:text-slate-400',
|
||||
// headings
|
||||
'prose-headings:scroll-mt-28 prose-headings:font-display prose-headings:font-normal lg:prose-headings:scroll-mt-[8.5rem]',
|
||||
// lead
|
||||
'prose-lead:text-slate-500 dark:prose-lead:text-slate-400',
|
||||
// links
|
||||
'prose-a:font-semibold dark:prose-a:text-sky-400',
|
||||
// link underline
|
||||
'prose-a:no-underline prose-a:shadow-[inset_0_-2px_0_0_var(--tw-prose-background,#fff),inset_0_calc(-1*(var(--tw-prose-underline-size,4px)+2px))_0_0_var(--tw-prose-underline,theme(colors.sky.300))] hover:prose-a:[--tw-prose-underline-size:6px] dark:[--tw-prose-background:theme(colors.slate.900)] dark:prose-a:shadow-[inset_0_calc(-1*var(--tw-prose-underline-size,2px))_0_0_var(--tw-prose-underline,theme(colors.sky.800))] dark:hover:prose-a:[--tw-prose-underline-size:6px]',
|
||||
// pre
|
||||
'prose-pre:rounded-xl prose-pre:bg-slate-900 prose-pre:shadow-lg dark:prose-pre:bg-slate-800/60 dark:prose-pre:shadow-none dark:prose-pre:ring-1 dark:prose-pre:ring-slate-300/10',
|
||||
// hr
|
||||
'dark:prose-hr:border-slate-800'
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
81
docs2/src/components/Search.jsx
Normal file
81
docs2/src/components/Search.jsx
Normal file
|
@ -0,0 +1,81 @@
|
|||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import Link from 'next/link'
|
||||
import Router from 'next/router'
|
||||
import { DocSearchModal, useDocSearchKeyboardEvents } from '@docsearch/react'
|
||||
|
||||
const docSearchConfig = {
|
||||
appId: process.env.NEXT_PUBLIC_DOCSEARCH_APP_ID,
|
||||
apiKey: process.env.NEXT_PUBLIC_DOCSEARCH_API_KEY,
|
||||
indexName: process.env.NEXT_PUBLIC_DOCSEARCH_INDEX_NAME,
|
||||
}
|
||||
|
||||
function Hit({ hit, children }) {
|
||||
return (
|
||||
<Link href={hit.url}>
|
||||
<a>{children}</a>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export function Search() {
|
||||
let [isOpen, setIsOpen] = useState(false)
|
||||
let [modifierKey, setModifierKey] = useState()
|
||||
|
||||
const onOpen = useCallback(() => {
|
||||
setIsOpen(true)
|
||||
}, [setIsOpen])
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
setIsOpen(false)
|
||||
}, [setIsOpen])
|
||||
|
||||
useDocSearchKeyboardEvents({ isOpen, onOpen, onClose })
|
||||
|
||||
useEffect(() => {
|
||||
setModifierKey(
|
||||
/(Mac|iPhone|iPod|iPad)/i.test(navigator.platform) ? '⌘' : 'Ctrl '
|
||||
)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="group flex h-6 w-6 items-center justify-center sm:justify-start md:h-auto md:w-80 md:flex-none md:rounded-lg md:py-2.5 md:pl-4 md:pr-3.5 md:text-sm md:ring-1 md:ring-slate-200 md:hover:ring-slate-300 dark:md:bg-slate-800/75 dark:md:ring-inset dark:md:ring-white/5 dark:md:hover:bg-slate-700/40 dark:md:hover:ring-slate-500 lg:w-96"
|
||||
onClick={onOpen}
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="h-5 w-5 flex-none fill-slate-400 group-hover:fill-slate-500 dark:fill-slate-500 md:group-hover:fill-slate-400"
|
||||
>
|
||||
<path d="M16.293 17.707a1 1 0 0 0 1.414-1.414l-1.414 1.414ZM9 14a5 5 0 0 1-5-5H2a7 7 0 0 0 7 7v-2ZM4 9a5 5 0 0 1 5-5V2a7 7 0 0 0-7 7h2Zm5-5a5 5 0 0 1 5 5h2a7 7 0 0 0-7-7v2Zm8.707 12.293-3.757-3.757-1.414 1.414 3.757 3.757 1.414-1.414ZM14 9a4.98 4.98 0 0 1-1.464 3.536l1.414 1.414A6.98 6.98 0 0 0 16 9h-2Zm-1.464 3.536A4.98 4.98 0 0 1 9 14v2a6.98 6.98 0 0 0 4.95-2.05l-1.414-1.414Z" />
|
||||
</svg>
|
||||
<span className="sr-only md:not-sr-only md:ml-2 md:text-slate-500 md:dark:text-slate-400">
|
||||
Search docs
|
||||
</span>
|
||||
{modifierKey && (
|
||||
<kbd className="ml-auto hidden font-medium text-slate-400 dark:text-slate-500 md:block">
|
||||
<kbd className="font-sans">{modifierKey}</kbd>
|
||||
<kbd className="font-sans">K</kbd>
|
||||
</kbd>
|
||||
)}
|
||||
</button>
|
||||
{isOpen &&
|
||||
createPortal(
|
||||
<DocSearchModal
|
||||
{...docSearchConfig}
|
||||
initialScrollY={window.scrollY}
|
||||
onClose={onClose}
|
||||
hitComponent={Hit}
|
||||
navigator={{
|
||||
navigate({ itemUrl }) {
|
||||
Router.push(itemUrl)
|
||||
},
|
||||
}}
|
||||
/>,
|
||||
document.body
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
121
docs2/src/components/ThemeSelector.jsx
Normal file
121
docs2/src/components/ThemeSelector.jsx
Normal file
|
@ -0,0 +1,121 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { Listbox } from '@headlessui/react'
|
||||
import clsx from 'clsx'
|
||||
|
||||
const themes = [
|
||||
{ name: 'Light', value: 'light', icon: LightIcon },
|
||||
{ name: 'Dark', value: 'dark', icon: DarkIcon },
|
||||
{ name: 'System', value: 'system', icon: SystemIcon },
|
||||
]
|
||||
|
||||
function IconBase({ children, ...props }) {
|
||||
return (
|
||||
<svg aria-hidden="true" viewBox="0 0 16 16" {...props}>
|
||||
{children}
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function LightIcon(props) {
|
||||
return (
|
||||
<IconBase {...props}>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M7 1a1 1 0 0 1 2 0v1a1 1 0 1 1-2 0V1Zm4 7a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm2.657-5.657a1 1 0 0 0-1.414 0l-.707.707a1 1 0 0 0 1.414 1.414l.707-.707a1 1 0 0 0 0-1.414Zm-1.415 11.313-.707-.707a1 1 0 0 1 1.415-1.415l.707.708a1 1 0 0 1-1.415 1.414ZM16 7.999a1 1 0 0 0-1-1h-1a1 1 0 1 0 0 2h1a1 1 0 0 0 1-1ZM7 14a1 1 0 1 1 2 0v1a1 1 0 1 1-2 0v-1Zm-2.536-2.464a1 1 0 0 0-1.414 0l-.707.707a1 1 0 0 0 1.414 1.414l.707-.707a1 1 0 0 0 0-1.414Zm0-8.486A1 1 0 0 1 3.05 4.464l-.707-.707a1 1 0 0 1 1.414-1.414l.707.707ZM3 8a1 1 0 0 0-1-1H1a1 1 0 0 0 0 2h1a1 1 0 0 0 1-1Z"
|
||||
/>
|
||||
</IconBase>
|
||||
)
|
||||
}
|
||||
|
||||
function DarkIcon(props) {
|
||||
return (
|
||||
<IconBase {...props}>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M7.23 3.333C7.757 2.905 7.68 2 7 2a6 6 0 1 0 0 12c.68 0 .758-.905.23-1.332A5.989 5.989 0 0 1 5 8c0-1.885.87-3.568 2.23-4.668ZM12 5a1 1 0 0 1 1 1 1 1 0 0 0 1 1 1 1 0 1 1 0 2 1 1 0 0 0-1 1 1 1 0 1 1-2 0 1 1 0 0 0-1-1 1 1 0 1 1 0-2 1 1 0 0 0 1-1 1 1 0 0 1 1-1Z"
|
||||
/>
|
||||
</IconBase>
|
||||
)
|
||||
}
|
||||
|
||||
function SystemIcon(props) {
|
||||
return (
|
||||
<IconBase {...props}>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M1 4a3 3 0 0 1 3-3h8a3 3 0 0 1 3 3v4a3 3 0 0 1-3 3h-1.5l.31 1.242c.084.333.36.573.63.808.091.08.182.158.264.24A1 1 0 0 1 11 15H5a1 1 0 0 1-.704-1.71c.082-.082.173-.16.264-.24.27-.235.546-.475.63-.808L5.5 11H4a3 3 0 0 1-3-3V4Zm3-1a1 1 0 0 0-1 1v4a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H4Z"
|
||||
/>
|
||||
</IconBase>
|
||||
)
|
||||
}
|
||||
|
||||
export function ThemeSelector(props) {
|
||||
let [selectedTheme, setSelectedTheme] = useState()
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedTheme) {
|
||||
document.documentElement.setAttribute('data-theme', selectedTheme.value)
|
||||
} else {
|
||||
setSelectedTheme(
|
||||
themes.find(
|
||||
(theme) =>
|
||||
theme.value === document.documentElement.getAttribute('data-theme')
|
||||
)
|
||||
)
|
||||
}
|
||||
}, [selectedTheme])
|
||||
|
||||
return (
|
||||
<Listbox
|
||||
as="div"
|
||||
value={selectedTheme}
|
||||
onChange={setSelectedTheme}
|
||||
{...props}
|
||||
>
|
||||
<Listbox.Label className="sr-only">Theme</Listbox.Label>
|
||||
<Listbox.Button className="flex h-6 w-6 items-center justify-center rounded-lg shadow-md shadow-black/5 ring-1 ring-black/5 dark:bg-slate-700 dark:ring-inset dark:ring-white/5">
|
||||
<span className="sr-only">{selectedTheme?.name}</span>
|
||||
<LightIcon className="hidden h-4 w-4 fill-sky-400 [[data-theme=light]_&]:block" />
|
||||
<DarkIcon className="hidden h-4 w-4 fill-sky-400 [[data-theme=dark]_&]:block" />
|
||||
<LightIcon className="hidden h-4 w-4 fill-slate-400 [:not(.dark)[data-theme=system]_&]:block" />
|
||||
<DarkIcon className="hidden h-4 w-4 fill-slate-400 [.dark[data-theme=system]_&]:block" />
|
||||
</Listbox.Button>
|
||||
<Listbox.Options className="absolute top-full left-1/2 mt-3 w-36 -translate-x-1/2 space-y-1 rounded-xl bg-white p-3 text-sm font-medium shadow-md shadow-black/5 ring-1 ring-black/5 dark:bg-slate-800 dark:ring-white/5">
|
||||
{themes.map((theme) => (
|
||||
<Listbox.Option
|
||||
key={theme.value}
|
||||
value={theme}
|
||||
className={({ active, selected }) =>
|
||||
clsx(
|
||||
'flex cursor-pointer select-none items-center rounded-[0.625rem] p-1',
|
||||
{
|
||||
'text-sky-500': selected,
|
||||
'text-slate-900 dark:text-white': active && !selected,
|
||||
'text-slate-700 dark:text-slate-400': !active && !selected,
|
||||
'bg-slate-100 dark:bg-slate-900/40': active,
|
||||
}
|
||||
)
|
||||
}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
<div className="rounded-md bg-white p-1 shadow ring-1 ring-slate-900/5 dark:bg-slate-700 dark:ring-inset dark:ring-white/5">
|
||||
<theme.icon
|
||||
className={clsx('h-4 w-4', {
|
||||
'fill-sky-400 dark:fill-sky-400': selected,
|
||||
'fill-slate-400': !selected,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-3">{theme.name}</div>
|
||||
</>
|
||||
)}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</Listbox>
|
||||
)
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user