Merge branch 'main' into automated-market-resolution
# Conflicts: # common/contract.ts # functions/src/create-contract.ts # web/pages/create.tsx
This commit is contained in:
		
						commit
						50cee1a6d3
					
				| 
						 | 
					@ -21,6 +21,8 @@ module.exports = {
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
  rules: {
 | 
					  rules: {
 | 
				
			||||||
 | 
					    'no-extra-semi': 'off',
 | 
				
			||||||
 | 
					    'no-unused-vars': 'off',
 | 
				
			||||||
    'no-constant-condition': ['error', { checkLoops: false }],
 | 
					    'no-constant-condition': ['error', { checkLoops: false }],
 | 
				
			||||||
    'lodash/import-scope': [2, 'member'],
 | 
					    'lodash/import-scope': [2, 'member'],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -34,8 +34,6 @@ export type FullContract<
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  closeEmailsSent?: number
 | 
					  closeEmailsSent?: number
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  manaLimitPerUser?: number
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  volume: number
 | 
					  volume: number
 | 
				
			||||||
  volume24Hours: number
 | 
					  volume24Hours: number
 | 
				
			||||||
  volume7Days: number
 | 
					  volume7Days: number
 | 
				
			||||||
| 
						 | 
					@ -102,9 +100,10 @@ export type Numeric = {
 | 
				
			||||||
export type outcomeType = 'BINARY' | 'MULTI' | 'FREE_RESPONSE' | 'NUMERIC'
 | 
					export type outcomeType = 'BINARY' | 'MULTI' | 'FREE_RESPONSE' | 'NUMERIC'
 | 
				
			||||||
export type resolutionType = 'MANUAL' | 'COMBINED'
 | 
					export type resolutionType = 'MANUAL' | 'COMBINED'
 | 
				
			||||||
export type resolution = 'YES' | 'NO' | 'MKT' | 'CANCEL'
 | 
					export type resolution = 'YES' | 'NO' | 'MKT' | 'CANCEL'
 | 
				
			||||||
export const RESOLUTIONS = ['YES' , 'NO' , 'MKT' , 'CANCEL']
 | 
					
 | 
				
			||||||
export const OUTCOME_TYPES = ['BINARY', 'MULTI', 'FREE_RESPONSE', 'NUMERIC']
 | 
					export const RESOLUTIONS = [ 'YES', 'NO', 'MKT', 'CANCEL'] as const
 | 
				
			||||||
export const RESOLUTION_TYPES = ['MANUAL', 'COMBINED']
 | 
					export const OUTCOME_TYPES = [ 'BINARY', 'MULTI', 'FREE_RESPONSE', 'NUMERIC'] as const
 | 
				
			||||||
 | 
					export const RESOLUTION_TYPES = ['MANUAL', 'COMBINED'] as const
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const MAX_QUESTION_LENGTH = 480
 | 
					export const MAX_QUESTION_LENGTH = 480
 | 
				
			||||||
export const MAX_DESCRIPTION_LENGTH = 10000
 | 
					export const MAX_DESCRIPTION_LENGTH = 10000
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -18,18 +18,25 @@ import {
 | 
				
			||||||
  Multi,
 | 
					  Multi,
 | 
				
			||||||
  NumericContract,
 | 
					  NumericContract,
 | 
				
			||||||
} from './contract'
 | 
					} from './contract'
 | 
				
			||||||
import { User } from './user'
 | 
					 | 
				
			||||||
import { noFees } from './fees'
 | 
					import { noFees } from './fees'
 | 
				
			||||||
import { addObjects } from './util/object'
 | 
					import { addObjects } from './util/object'
 | 
				
			||||||
import { NUMERIC_FIXED_VAR } from './numeric-constants'
 | 
					import { NUMERIC_FIXED_VAR } from './numeric-constants'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type CandidateBet<T extends Bet> = Omit<T, 'id' | 'userId'>
 | 
				
			||||||
 | 
					export type BetInfo = {
 | 
				
			||||||
 | 
					  newBet: CandidateBet<Bet>
 | 
				
			||||||
 | 
					  newPool?: { [outcome: string]: number }
 | 
				
			||||||
 | 
					  newTotalShares?: { [outcome: string]: number }
 | 
				
			||||||
 | 
					  newTotalBets?: { [outcome: string]: number }
 | 
				
			||||||
 | 
					  newTotalLiquidity?: number
 | 
				
			||||||
 | 
					  newP?: number
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const getNewBinaryCpmmBetInfo = (
 | 
					export const getNewBinaryCpmmBetInfo = (
 | 
				
			||||||
  user: User,
 | 
					 | 
				
			||||||
  outcome: 'YES' | 'NO',
 | 
					  outcome: 'YES' | 'NO',
 | 
				
			||||||
  amount: number,
 | 
					  amount: number,
 | 
				
			||||||
  contract: FullContract<CPMM, Binary>,
 | 
					  contract: FullContract<CPMM, Binary>,
 | 
				
			||||||
  loanAmount: number,
 | 
					  loanAmount: number
 | 
				
			||||||
  newBetId: string
 | 
					 | 
				
			||||||
) => {
 | 
					) => {
 | 
				
			||||||
  const { shares, newPool, newP, fees } = calculateCpmmPurchase(
 | 
					  const { shares, newPool, newP, fees } = calculateCpmmPurchase(
 | 
				
			||||||
    contract,
 | 
					    contract,
 | 
				
			||||||
| 
						 | 
					@ -37,15 +44,11 @@ export const getNewBinaryCpmmBetInfo = (
 | 
				
			||||||
    outcome
 | 
					    outcome
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const newBalance = user.balance - (amount - loanAmount)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const { pool, p, totalLiquidity } = contract
 | 
					  const { pool, p, totalLiquidity } = contract
 | 
				
			||||||
  const probBefore = getCpmmProbability(pool, p)
 | 
					  const probBefore = getCpmmProbability(pool, p)
 | 
				
			||||||
  const probAfter = getCpmmProbability(newPool, newP)
 | 
					  const probAfter = getCpmmProbability(newPool, newP)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const newBet: Bet = {
 | 
					  const newBet: CandidateBet<Bet> = {
 | 
				
			||||||
    id: newBetId,
 | 
					 | 
				
			||||||
    userId: user.id,
 | 
					 | 
				
			||||||
    contractId: contract.id,
 | 
					    contractId: contract.id,
 | 
				
			||||||
    amount,
 | 
					    amount,
 | 
				
			||||||
    shares,
 | 
					    shares,
 | 
				
			||||||
| 
						 | 
					@ -60,16 +63,14 @@ export const getNewBinaryCpmmBetInfo = (
 | 
				
			||||||
  const { liquidityFee } = fees
 | 
					  const { liquidityFee } = fees
 | 
				
			||||||
  const newTotalLiquidity = (totalLiquidity ?? 0) + liquidityFee
 | 
					  const newTotalLiquidity = (totalLiquidity ?? 0) + liquidityFee
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return { newBet, newPool, newP, newBalance, newTotalLiquidity, fees }
 | 
					  return { newBet, newPool, newP, newTotalLiquidity }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const getNewBinaryDpmBetInfo = (
 | 
					export const getNewBinaryDpmBetInfo = (
 | 
				
			||||||
  user: User,
 | 
					 | 
				
			||||||
  outcome: 'YES' | 'NO',
 | 
					  outcome: 'YES' | 'NO',
 | 
				
			||||||
  amount: number,
 | 
					  amount: number,
 | 
				
			||||||
  contract: FullContract<DPM, Binary>,
 | 
					  contract: FullContract<DPM, Binary>,
 | 
				
			||||||
  loanAmount: number,
 | 
					  loanAmount: number
 | 
				
			||||||
  newBetId: string
 | 
					 | 
				
			||||||
) => {
 | 
					) => {
 | 
				
			||||||
  const { YES: yesPool, NO: noPool } = contract.pool
 | 
					  const { YES: yesPool, NO: noPool } = contract.pool
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -97,9 +98,7 @@ export const getNewBinaryDpmBetInfo = (
 | 
				
			||||||
  const probBefore = getDpmProbability(contract.totalShares)
 | 
					  const probBefore = getDpmProbability(contract.totalShares)
 | 
				
			||||||
  const probAfter = getDpmProbability(newTotalShares)
 | 
					  const probAfter = getDpmProbability(newTotalShares)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const newBet: Bet = {
 | 
					  const newBet: CandidateBet<Bet> = {
 | 
				
			||||||
    id: newBetId,
 | 
					 | 
				
			||||||
    userId: user.id,
 | 
					 | 
				
			||||||
    contractId: contract.id,
 | 
					    contractId: contract.id,
 | 
				
			||||||
    amount,
 | 
					    amount,
 | 
				
			||||||
    loanAmount,
 | 
					    loanAmount,
 | 
				
			||||||
| 
						 | 
					@ -111,18 +110,14 @@ export const getNewBinaryDpmBetInfo = (
 | 
				
			||||||
    fees: noFees,
 | 
					    fees: noFees,
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const newBalance = user.balance - (amount - loanAmount)
 | 
					  return { newBet, newPool, newTotalShares, newTotalBets }
 | 
				
			||||||
 | 
					 | 
				
			||||||
  return { newBet, newPool, newTotalShares, newTotalBets, newBalance }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const getNewMultiBetInfo = (
 | 
					export const getNewMultiBetInfo = (
 | 
				
			||||||
  user: User,
 | 
					 | 
				
			||||||
  outcome: string,
 | 
					  outcome: string,
 | 
				
			||||||
  amount: number,
 | 
					  amount: number,
 | 
				
			||||||
  contract: FullContract<DPM, Multi | FreeResponse>,
 | 
					  contract: FullContract<DPM, Multi | FreeResponse>,
 | 
				
			||||||
  loanAmount: number,
 | 
					  loanAmount: number
 | 
				
			||||||
  newBetId: string
 | 
					 | 
				
			||||||
) => {
 | 
					) => {
 | 
				
			||||||
  const { pool, totalShares, totalBets } = contract
 | 
					  const { pool, totalShares, totalBets } = contract
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -140,9 +135,7 @@ export const getNewMultiBetInfo = (
 | 
				
			||||||
  const probBefore = getDpmOutcomeProbability(totalShares, outcome)
 | 
					  const probBefore = getDpmOutcomeProbability(totalShares, outcome)
 | 
				
			||||||
  const probAfter = getDpmOutcomeProbability(newTotalShares, outcome)
 | 
					  const probAfter = getDpmOutcomeProbability(newTotalShares, outcome)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const newBet: Bet = {
 | 
					  const newBet: CandidateBet<Bet> = {
 | 
				
			||||||
    id: newBetId,
 | 
					 | 
				
			||||||
    userId: user.id,
 | 
					 | 
				
			||||||
    contractId: contract.id,
 | 
					    contractId: contract.id,
 | 
				
			||||||
    amount,
 | 
					    amount,
 | 
				
			||||||
    loanAmount,
 | 
					    loanAmount,
 | 
				
			||||||
| 
						 | 
					@ -154,18 +147,14 @@ export const getNewMultiBetInfo = (
 | 
				
			||||||
    fees: noFees,
 | 
					    fees: noFees,
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const newBalance = user.balance - (amount - loanAmount)
 | 
					  return { newBet, newPool, newTotalShares, newTotalBets }
 | 
				
			||||||
 | 
					 | 
				
			||||||
  return { newBet, newPool, newTotalShares, newTotalBets, newBalance }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const getNumericBetsInfo = (
 | 
					export const getNumericBetsInfo = (
 | 
				
			||||||
  user: User,
 | 
					 | 
				
			||||||
  value: number,
 | 
					  value: number,
 | 
				
			||||||
  outcome: string,
 | 
					  outcome: string,
 | 
				
			||||||
  amount: number,
 | 
					  amount: number,
 | 
				
			||||||
  contract: NumericContract,
 | 
					  contract: NumericContract
 | 
				
			||||||
  newBetId: string
 | 
					 | 
				
			||||||
) => {
 | 
					) => {
 | 
				
			||||||
  const { pool, totalShares, totalBets } = contract
 | 
					  const { pool, totalShares, totalBets } = contract
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -187,9 +176,7 @@ export const getNumericBetsInfo = (
 | 
				
			||||||
  const probBefore = getDpmOutcomeProbability(totalShares, outcome)
 | 
					  const probBefore = getDpmOutcomeProbability(totalShares, outcome)
 | 
				
			||||||
  const probAfter = getDpmOutcomeProbability(newTotalShares, outcome)
 | 
					  const probAfter = getDpmOutcomeProbability(newTotalShares, outcome)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const newBet: NumericBet = {
 | 
					  const newBet: CandidateBet<NumericBet> = {
 | 
				
			||||||
    id: newBetId,
 | 
					 | 
				
			||||||
    userId: user.id,
 | 
					 | 
				
			||||||
    contractId: contract.id,
 | 
					    contractId: contract.id,
 | 
				
			||||||
    value,
 | 
					    value,
 | 
				
			||||||
    amount,
 | 
					    amount,
 | 
				
			||||||
| 
						 | 
					@ -203,9 +190,7 @@ export const getNumericBetsInfo = (
 | 
				
			||||||
    fees: noFees,
 | 
					    fees: noFees,
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const newBalance = user.balance - amount
 | 
					  return { newBet, newPool, newTotalShares, newTotalBets }
 | 
				
			||||||
 | 
					 | 
				
			||||||
  return { newBet, newPool, newTotalShares, newTotalBets, newBalance }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const getLoanAmount = (yourBets: Bet[], newBetAmount: number) => {
 | 
					export const getLoanAmount = (yourBets: Bet[], newBetAmount: number) => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -32,8 +32,7 @@ export function getNewContract(
 | 
				
			||||||
  // used for numeric markets
 | 
					  // used for numeric markets
 | 
				
			||||||
  bucketCount: number,
 | 
					  bucketCount: number,
 | 
				
			||||||
  min: number,
 | 
					  min: number,
 | 
				
			||||||
  max: number,
 | 
					  max: number
 | 
				
			||||||
  manaLimitPerUser: number
 | 
					 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
  const tags = parseTags(
 | 
					  const tags = parseTags(
 | 
				
			||||||
    `${question} ${description} ${extraTags.map((tag) => `#${tag}`).join(' ')}`
 | 
					    `${question} ${description} ${extraTags.map((tag) => `#${tag}`).join(' ')}`
 | 
				
			||||||
| 
						 | 
					@ -78,7 +77,6 @@ export function getNewContract(
 | 
				
			||||||
      liquidityFee: 0,
 | 
					      liquidityFee: 0,
 | 
				
			||||||
      platformFee: 0,
 | 
					      platformFee: 0,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    manaLimitPerUser,
 | 
					 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return contract as Contract
 | 
					  return contract as Contract
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,13 +3,7 @@ import { sum, groupBy, sumBy, mapValues } from 'lodash'
 | 
				
			||||||
import { Bet, NumericBet } from './bet'
 | 
					import { Bet, NumericBet } from './bet'
 | 
				
			||||||
import { deductDpmFees, getDpmProbability } from './calculate-dpm'
 | 
					import { deductDpmFees, getDpmProbability } from './calculate-dpm'
 | 
				
			||||||
import { DPM, FreeResponse, FullContract, Multi } from './contract'
 | 
					import { DPM, FreeResponse, FullContract, Multi } from './contract'
 | 
				
			||||||
import {
 | 
					import { DPM_CREATOR_FEE, DPM_FEES, DPM_PLATFORM_FEE } from './fees'
 | 
				
			||||||
  DPM_CREATOR_FEE,
 | 
					 | 
				
			||||||
  DPM_FEES,
 | 
					 | 
				
			||||||
  DPM_PLATFORM_FEE,
 | 
					 | 
				
			||||||
  Fees,
 | 
					 | 
				
			||||||
  noFees,
 | 
					 | 
				
			||||||
} from './fees'
 | 
					 | 
				
			||||||
import { addObjects } from './util/object'
 | 
					import { addObjects } from './util/object'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const getDpmCancelPayouts = (
 | 
					export const getDpmCancelPayouts = (
 | 
				
			||||||
| 
						 | 
					@ -31,7 +25,7 @@ export const getDpmCancelPayouts = (
 | 
				
			||||||
    payouts,
 | 
					    payouts,
 | 
				
			||||||
    creatorPayout: 0,
 | 
					    creatorPayout: 0,
 | 
				
			||||||
    liquidityPayouts: [],
 | 
					    liquidityPayouts: [],
 | 
				
			||||||
    collectedFees: contract.collectedFees ?? noFees,
 | 
					    collectedFees: contract.collectedFees,
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -57,17 +51,11 @@ export const getDpmStandardPayouts = (
 | 
				
			||||||
  const profits = sumBy(payouts, (po) => Math.max(0, po.profit))
 | 
					  const profits = sumBy(payouts, (po) => Math.max(0, po.profit))
 | 
				
			||||||
  const creatorFee = DPM_CREATOR_FEE * profits
 | 
					  const creatorFee = DPM_CREATOR_FEE * profits
 | 
				
			||||||
  const platformFee = DPM_PLATFORM_FEE * profits
 | 
					  const platformFee = DPM_PLATFORM_FEE * profits
 | 
				
			||||||
 | 
					  const collectedFees = addObjects(contract.collectedFees, {
 | 
				
			||||||
  const finalFees: Fees = {
 | 
					 | 
				
			||||||
    creatorFee,
 | 
					    creatorFee,
 | 
				
			||||||
    platformFee,
 | 
					    platformFee,
 | 
				
			||||||
    liquidityFee: 0,
 | 
					    liquidityFee: 0,
 | 
				
			||||||
  }
 | 
					  })
 | 
				
			||||||
 | 
					 | 
				
			||||||
  const collectedFees = addObjects<Fees>(
 | 
					 | 
				
			||||||
    finalFees,
 | 
					 | 
				
			||||||
    contract.collectedFees ?? {}
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  console.log(
 | 
					  console.log(
 | 
				
			||||||
    'resolved',
 | 
					    'resolved',
 | 
				
			||||||
| 
						 | 
					@ -115,17 +103,11 @@ export const getNumericDpmPayouts = (
 | 
				
			||||||
  const profits = sumBy(payouts, (po) => Math.max(0, po.profit))
 | 
					  const profits = sumBy(payouts, (po) => Math.max(0, po.profit))
 | 
				
			||||||
  const creatorFee = DPM_CREATOR_FEE * profits
 | 
					  const creatorFee = DPM_CREATOR_FEE * profits
 | 
				
			||||||
  const platformFee = DPM_PLATFORM_FEE * profits
 | 
					  const platformFee = DPM_PLATFORM_FEE * profits
 | 
				
			||||||
 | 
					  const collectedFees = addObjects(contract.collectedFees, {
 | 
				
			||||||
  const finalFees: Fees = {
 | 
					 | 
				
			||||||
    creatorFee,
 | 
					    creatorFee,
 | 
				
			||||||
    platformFee,
 | 
					    platformFee,
 | 
				
			||||||
    liquidityFee: 0,
 | 
					    liquidityFee: 0,
 | 
				
			||||||
  }
 | 
					  })
 | 
				
			||||||
 | 
					 | 
				
			||||||
  const collectedFees = addObjects<Fees>(
 | 
					 | 
				
			||||||
    finalFees,
 | 
					 | 
				
			||||||
    contract.collectedFees ?? {}
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  console.log(
 | 
					  console.log(
 | 
				
			||||||
    'resolved numeric bucket: ',
 | 
					    'resolved numeric bucket: ',
 | 
				
			||||||
| 
						 | 
					@ -174,17 +156,11 @@ export const getDpmMktPayouts = (
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const creatorFee = DPM_CREATOR_FEE * profits
 | 
					  const creatorFee = DPM_CREATOR_FEE * profits
 | 
				
			||||||
  const platformFee = DPM_PLATFORM_FEE * profits
 | 
					  const platformFee = DPM_PLATFORM_FEE * profits
 | 
				
			||||||
 | 
					  const collectedFees = addObjects(contract.collectedFees, {
 | 
				
			||||||
  const finalFees: Fees = {
 | 
					 | 
				
			||||||
    creatorFee,
 | 
					    creatorFee,
 | 
				
			||||||
    platformFee,
 | 
					    platformFee,
 | 
				
			||||||
    liquidityFee: 0,
 | 
					    liquidityFee: 0,
 | 
				
			||||||
  }
 | 
					  })
 | 
				
			||||||
 | 
					 | 
				
			||||||
  const collectedFees = addObjects<Fees>(
 | 
					 | 
				
			||||||
    finalFees,
 | 
					 | 
				
			||||||
    contract.collectedFees ?? {}
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  console.log(
 | 
					  console.log(
 | 
				
			||||||
    'resolved MKT',
 | 
					    'resolved MKT',
 | 
				
			||||||
| 
						 | 
					@ -233,17 +209,11 @@ export const getPayoutsMultiOutcome = (
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const creatorFee = DPM_CREATOR_FEE * profits
 | 
					  const creatorFee = DPM_CREATOR_FEE * profits
 | 
				
			||||||
  const platformFee = DPM_PLATFORM_FEE * profits
 | 
					  const platformFee = DPM_PLATFORM_FEE * profits
 | 
				
			||||||
 | 
					  const collectedFees = addObjects(contract.collectedFees, {
 | 
				
			||||||
  const finalFees: Fees = {
 | 
					 | 
				
			||||||
    creatorFee,
 | 
					    creatorFee,
 | 
				
			||||||
    platformFee,
 | 
					    platformFee,
 | 
				
			||||||
    liquidityFee: 0,
 | 
					    liquidityFee: 0,
 | 
				
			||||||
  }
 | 
					  })
 | 
				
			||||||
 | 
					 | 
				
			||||||
  const collectedFees = addObjects<Fees>(
 | 
					 | 
				
			||||||
    finalFees,
 | 
					 | 
				
			||||||
    contract.collectedFees ?? noFees
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  console.log(
 | 
					  console.log(
 | 
				
			||||||
    'resolved',
 | 
					    'resolved',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -17,6 +17,7 @@ module.exports = {
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
  rules: {
 | 
					  rules: {
 | 
				
			||||||
 | 
					    'no-extra-semi': 'off',
 | 
				
			||||||
    'no-unused-vars': 'off',
 | 
					    'no-unused-vars': 'off',
 | 
				
			||||||
    'no-constant-condition': ['error', { checkLoops: false }],
 | 
					    'no-constant-condition': ['error', { checkLoops: false }],
 | 
				
			||||||
    'lodash/import-scope': [2, 'member'],
 | 
					    'lodash/import-scope': [2, 'member'],
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -28,7 +28,8 @@
 | 
				
			||||||
    "mailgun-js": "0.22.0",
 | 
					    "mailgun-js": "0.22.0",
 | 
				
			||||||
    "module-alias": "2.2.2",
 | 
					    "module-alias": "2.2.2",
 | 
				
			||||||
    "react-query": "3.39.0",
 | 
					    "react-query": "3.39.0",
 | 
				
			||||||
    "stripe": "8.194.0"
 | 
					    "stripe": "8.194.0",
 | 
				
			||||||
 | 
					    "zod": "3.17.2"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "devDependencies": {
 | 
					  "devDependencies": {
 | 
				
			||||||
    "@types/mailgun-js": "0.22.12",
 | 
					    "@types/mailgun-js": "0.22.12",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,7 @@
 | 
				
			||||||
import * as admin from 'firebase-admin'
 | 
					import * as admin from 'firebase-admin'
 | 
				
			||||||
import * as functions from 'firebase-functions'
 | 
					import * as functions from 'firebase-functions'
 | 
				
			||||||
import * as Cors from 'cors'
 | 
					import * as Cors from 'cors'
 | 
				
			||||||
 | 
					import { z } from 'zod'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { User, PrivateUser } from '../../common/user'
 | 
					import { User, PrivateUser } from '../../common/user'
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
| 
						 | 
					@ -8,10 +9,11 @@ import {
 | 
				
			||||||
  CORS_ORIGIN_LOCALHOST,
 | 
					  CORS_ORIGIN_LOCALHOST,
 | 
				
			||||||
} from '../../common/envs/constants'
 | 
					} from '../../common/envs/constants'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Output = Record<string, unknown>
 | 
				
			||||||
type Request = functions.https.Request
 | 
					type Request = functions.https.Request
 | 
				
			||||||
type Response = functions.Response
 | 
					type Response = functions.Response
 | 
				
			||||||
type Handler = (req: Request, res: Response) => Promise<any>
 | 
					 | 
				
			||||||
type AuthedUser = [User, PrivateUser]
 | 
					type AuthedUser = [User, PrivateUser]
 | 
				
			||||||
 | 
					type Handler = (req: Request, user: AuthedUser) => Promise<Output>
 | 
				
			||||||
type JwtCredentials = { kind: 'jwt'; data: admin.auth.DecodedIdToken }
 | 
					type JwtCredentials = { kind: 'jwt'; data: admin.auth.DecodedIdToken }
 | 
				
			||||||
type KeyCredentials = { kind: 'key'; data: string }
 | 
					type KeyCredentials = { kind: 'key'; data: string }
 | 
				
			||||||
type Credentials = JwtCredentials | KeyCredentials
 | 
					type Credentials = JwtCredentials | KeyCredentials
 | 
				
			||||||
| 
						 | 
					@ -19,10 +21,13 @@ type Credentials = JwtCredentials | KeyCredentials
 | 
				
			||||||
export class APIError {
 | 
					export class APIError {
 | 
				
			||||||
  code: number
 | 
					  code: number
 | 
				
			||||||
  msg: string
 | 
					  msg: string
 | 
				
			||||||
  constructor(code: number, msg: string) {
 | 
					  details: unknown
 | 
				
			||||||
 | 
					  constructor(code: number, msg: string, details?: unknown) {
 | 
				
			||||||
    this.code = code
 | 
					    this.code = code
 | 
				
			||||||
    this.msg = msg
 | 
					    this.msg = msg
 | 
				
			||||||
 | 
					    this.details = details
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					  toJson() {}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const parseCredentials = async (req: Request): Promise<Credentials> => {
 | 
					export const parseCredentials = async (req: Request): Promise<Credentials> => {
 | 
				
			||||||
| 
						 | 
					@ -40,14 +45,11 @@ export const parseCredentials = async (req: Request): Promise<Credentials> => {
 | 
				
			||||||
    case 'Bearer':
 | 
					    case 'Bearer':
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        const jwt = await admin.auth().verifyIdToken(payload)
 | 
					        const jwt = await admin.auth().verifyIdToken(payload)
 | 
				
			||||||
        if (!jwt.user_id) {
 | 
					 | 
				
			||||||
          throw new APIError(403, 'JWT must contain Manifold user ID.')
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return { kind: 'jwt', data: jwt }
 | 
					        return { kind: 'jwt', data: jwt }
 | 
				
			||||||
      } catch (err) {
 | 
					      } catch (err) {
 | 
				
			||||||
        // This is somewhat suspicious, so get it into the firebase console
 | 
					        // This is somewhat suspicious, so get it into the firebase console
 | 
				
			||||||
        functions.logger.error('Error verifying Firebase JWT: ', err)
 | 
					        functions.logger.error('Error verifying Firebase JWT: ', err)
 | 
				
			||||||
        throw new APIError(403, `Error validating token: ${err}.`)
 | 
					        throw new APIError(403, 'Error validating token.')
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    case 'Key':
 | 
					    case 'Key':
 | 
				
			||||||
      return { kind: 'key', data: payload }
 | 
					      return { kind: 'key', data: payload }
 | 
				
			||||||
| 
						 | 
					@ -63,6 +65,9 @@ export const lookupUser = async (creds: Credentials): Promise<AuthedUser> => {
 | 
				
			||||||
  switch (creds.kind) {
 | 
					  switch (creds.kind) {
 | 
				
			||||||
    case 'jwt': {
 | 
					    case 'jwt': {
 | 
				
			||||||
      const { user_id } = creds.data
 | 
					      const { user_id } = creds.data
 | 
				
			||||||
 | 
					      if (typeof user_id !== 'string') {
 | 
				
			||||||
 | 
					        throw new APIError(403, 'JWT must contain Manifold user ID.')
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
      const [userSnap, privateUserSnap] = await Promise.all([
 | 
					      const [userSnap, privateUserSnap] = await Promise.all([
 | 
				
			||||||
        users.doc(user_id).get(),
 | 
					        users.doc(user_id).get(),
 | 
				
			||||||
        privateUsers.doc(user_id).get(),
 | 
					        privateUsers.doc(user_id).get(),
 | 
				
			||||||
| 
						 | 
					@ -109,6 +114,27 @@ export const applyCors = (
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const zTimestamp = () => {
 | 
				
			||||||
 | 
					  return z.preprocess((arg) => {
 | 
				
			||||||
 | 
					    return typeof arg == 'number' ? new Date(arg) : undefined
 | 
				
			||||||
 | 
					  }, z.date())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const validate = <T extends z.ZodTypeAny>(schema: T, val: unknown) => {
 | 
				
			||||||
 | 
					  const result = schema.safeParse(val)
 | 
				
			||||||
 | 
					  if (!result.success) {
 | 
				
			||||||
 | 
					    const issues = result.error.issues.map((i) => {
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        field: i.path.join('.') || null,
 | 
				
			||||||
 | 
					        error: i.message,
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    throw new APIError(400, 'Error validating request.', issues)
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    return result.data as z.infer<T>
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const newEndpoint = (methods: [string], fn: Handler) =>
 | 
					export const newEndpoint = (methods: [string], fn: Handler) =>
 | 
				
			||||||
  functions.runWith({ minInstances: 1 }).https.onRequest(async (req, res) => {
 | 
					  functions.runWith({ minInstances: 1 }).https.onRequest(async (req, res) => {
 | 
				
			||||||
    await applyCors(req, res, {
 | 
					    await applyCors(req, res, {
 | 
				
			||||||
| 
						 | 
					@ -120,12 +146,17 @@ export const newEndpoint = (methods: [string], fn: Handler) =>
 | 
				
			||||||
        const allowed = methods.join(', ')
 | 
					        const allowed = methods.join(', ')
 | 
				
			||||||
        throw new APIError(405, `This endpoint supports only ${allowed}.`)
 | 
					        throw new APIError(405, `This endpoint supports only ${allowed}.`)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      res.status(200).json(await fn(req, res))
 | 
					      const authedUser = await lookupUser(await parseCredentials(req))
 | 
				
			||||||
 | 
					      res.status(200).json(await fn(req, authedUser))
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
      if (e instanceof APIError) {
 | 
					      if (e instanceof APIError) {
 | 
				
			||||||
        // Emit a 200 anyway here for now, for backwards compatibility
 | 
					        const output: { [k: string]: unknown } = { message: e.msg }
 | 
				
			||||||
        res.status(e.code).json({ message: e.msg })
 | 
					        if (e.details != null) {
 | 
				
			||||||
 | 
					          output.details = e.details
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        res.status(e.code).json(output)
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
 | 
					        functions.logger.error(e)
 | 
				
			||||||
        res.status(500).json({ message: 'An unknown error occurred.' })
 | 
					        res.status(500).json({ message: 'An unknown error occurred.' })
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -12,7 +12,6 @@ import { getNewMultiBetInfo } from '../../common/new-bet'
 | 
				
			||||||
import { Answer, MAX_ANSWER_LENGTH } from '../../common/answer'
 | 
					import { Answer, MAX_ANSWER_LENGTH } from '../../common/answer'
 | 
				
			||||||
import { getContract, getValues } from './utils'
 | 
					import { getContract, getValues } from './utils'
 | 
				
			||||||
import { sendNewAnswerEmail } from './emails'
 | 
					import { sendNewAnswerEmail } from './emails'
 | 
				
			||||||
import { Bet } from '../../common/bet'
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
 | 
					export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
 | 
				
			||||||
  async (
 | 
					  async (
 | 
				
			||||||
| 
						 | 
					@ -61,11 +60,6 @@ export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
 | 
				
			||||||
      if (closeTime && Date.now() > closeTime)
 | 
					      if (closeTime && Date.now() > closeTime)
 | 
				
			||||||
        return { status: 'error', message: 'Trading is closed' }
 | 
					        return { status: 'error', message: 'Trading is closed' }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const yourBetsSnap = await transaction.get(
 | 
					 | 
				
			||||||
        contractDoc.collection('bets').where('userId', '==', userId)
 | 
					 | 
				
			||||||
      )
 | 
					 | 
				
			||||||
      const yourBets = yourBetsSnap.docs.map((doc) => doc.data() as Bet)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const [lastAnswer] = await getValues<Answer>(
 | 
					      const [lastAnswer] = await getValues<Answer>(
 | 
				
			||||||
        firestore
 | 
					        firestore
 | 
				
			||||||
          .collection(`contracts/${contractId}/answers`)
 | 
					          .collection(`contracts/${contractId}/answers`)
 | 
				
			||||||
| 
						 | 
					@ -99,23 +93,20 @@ export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      transaction.create(newAnswerDoc, answer)
 | 
					      transaction.create(newAnswerDoc, answer)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const newBetDoc = firestore
 | 
					      const loanAmount = 0
 | 
				
			||||||
        .collection(`contracts/${contractId}/bets`)
 | 
					 | 
				
			||||||
        .doc()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const loanAmount = 0 // getLoanAmount(yourBets, amount)
 | 
					      const { newBet, newPool, newTotalShares, newTotalBets } =
 | 
				
			||||||
 | 
					 | 
				
			||||||
      const { newBet, newPool, newTotalShares, newTotalBets, newBalance } =
 | 
					 | 
				
			||||||
        getNewMultiBetInfo(
 | 
					        getNewMultiBetInfo(
 | 
				
			||||||
          user,
 | 
					 | 
				
			||||||
          answerId,
 | 
					          answerId,
 | 
				
			||||||
          amount,
 | 
					          amount,
 | 
				
			||||||
          contract as FullContract<DPM, FreeResponse>,
 | 
					          contract as FullContract<DPM, FreeResponse>,
 | 
				
			||||||
          loanAmount,
 | 
					          loanAmount
 | 
				
			||||||
          newBetDoc.id
 | 
					 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      transaction.create(newBetDoc, newBet)
 | 
					      const newBalance = user.balance - amount
 | 
				
			||||||
 | 
					      const betDoc = firestore.collection(`contracts/${contractId}/bets`).doc()
 | 
				
			||||||
 | 
					      transaction.create(betDoc, { id: betDoc.id, userId: user.id, ...newBet })
 | 
				
			||||||
 | 
					      transaction.update(userDoc, { balance: newBalance })
 | 
				
			||||||
      transaction.update(contractDoc, {
 | 
					      transaction.update(contractDoc, {
 | 
				
			||||||
        pool: newPool,
 | 
					        pool: newPool,
 | 
				
			||||||
        totalShares: newTotalShares,
 | 
					        totalShares: newTotalShares,
 | 
				
			||||||
| 
						 | 
					@ -124,13 +115,7 @@ export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
 | 
				
			||||||
        volume: volume + amount,
 | 
					        volume: volume + amount,
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (!isFinite(newBalance)) {
 | 
					      return { status: 'success', answerId, betId: betDoc.id, answer }
 | 
				
			||||||
        throw new Error('Invalid user balance for ' + user.username)
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      transaction.update(userDoc, { balance: newBalance })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      return { status: 'success', answerId, betId: newBetDoc.id, answer }
 | 
					 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const { answer } = result
 | 
					    const { answer } = result
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,5 @@
 | 
				
			||||||
import * as admin from 'firebase-admin'
 | 
					import * as admin from 'firebase-admin'
 | 
				
			||||||
 | 
					import { z } from 'zod'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  Binary,
 | 
					  Binary,
 | 
				
			||||||
| 
						 | 
					@ -19,7 +20,7 @@ import { slugify } from '../../common/util/slugify'
 | 
				
			||||||
import { randomString } from '../../common/util/random'
 | 
					import { randomString } from '../../common/util/random'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { chargeUser } from './utils'
 | 
					import { chargeUser } from './utils'
 | 
				
			||||||
import { APIError, newEndpoint, parseCredentials, lookupUser } from './api'
 | 
					import { APIError, newEndpoint, validate, zTimestamp } from './api'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  FIXED_ANTE,
 | 
					  FIXED_ANTE,
 | 
				
			||||||
| 
						 | 
					@ -28,94 +29,65 @@ import {
 | 
				
			||||||
  getFreeAnswerAnte,
 | 
					  getFreeAnswerAnte,
 | 
				
			||||||
  getNumericAnte,
 | 
					  getNumericAnte,
 | 
				
			||||||
  HOUSE_LIQUIDITY_PROVIDER_ID,
 | 
					  HOUSE_LIQUIDITY_PROVIDER_ID,
 | 
				
			||||||
  MINIMUM_ANTE,
 | 
					 | 
				
			||||||
} from '../../common/antes'
 | 
					} from '../../common/antes'
 | 
				
			||||||
import { getNoneAnswer } from '../../common/answer'
 | 
					import { getNoneAnswer } from '../../common/answer'
 | 
				
			||||||
import { getNewContract } from '../../common/new-contract'
 | 
					import { getNewContract } from '../../common/new-contract'
 | 
				
			||||||
import { NUMERIC_BUCKET_COUNT } from '../../common/numeric-constants'
 | 
					import { NUMERIC_BUCKET_COUNT } from '../../common/numeric-constants'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const createContract = newEndpoint(['POST'], async (req, _res) => {
 | 
					const bodySchema = z.object({
 | 
				
			||||||
  const [creator, _privateUser] = await lookupUser(await parseCredentials(req))
 | 
					  question: z.string().min(1).max(MAX_QUESTION_LENGTH),
 | 
				
			||||||
  let {
 | 
					  description: z.string().max(MAX_DESCRIPTION_LENGTH),
 | 
				
			||||||
    question,
 | 
					  tags: z.array(z.string().min(1).max(MAX_TAG_LENGTH)).optional(),
 | 
				
			||||||
    outcomeType,
 | 
					  closeTime: zTimestamp().refine(
 | 
				
			||||||
    description,
 | 
					    (date) => date.getTime() > new Date().getTime(),
 | 
				
			||||||
    initialProb,
 | 
					    'Close time must be in the future.'
 | 
				
			||||||
    closeTime,
 | 
					  ),
 | 
				
			||||||
    tags,
 | 
					  outcomeType: z.enum(OUTCOME_TYPES),
 | 
				
			||||||
    min,
 | 
					  resolutionType: z.enum(RESOLUTION_TYPES),
 | 
				
			||||||
    max,
 | 
					  automaticResolution: z.enum(RESOLUTIONS),
 | 
				
			||||||
    manaLimitPerUser,
 | 
					  automaticResolutionTime: z.date()
 | 
				
			||||||
    resolutionType,
 | 
					  }).refine(
 | 
				
			||||||
    automaticResolution,
 | 
					    (data) => data.automaticResolutionTime.getTime() > data.closeTime.getTime(),
 | 
				
			||||||
    automaticResolutionTime
 | 
					    'Resolution time must be after close time.'
 | 
				
			||||||
  } = req.body || {}
 | 
					  ).refine(
 | 
				
			||||||
 | 
					    (data) => data.resolutionType === 'MANUAL' && data.automaticResolutionTime,
 | 
				
			||||||
  if (!question || typeof question != 'string')
 | 
					    'Time for automatic resolution specified even tho the resolution is \'MANUAL\''
 | 
				
			||||||
    throw new APIError(400, 'Missing or invalid question field')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  question = question.slice(0, MAX_QUESTION_LENGTH)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (typeof description !== 'string')
 | 
					 | 
				
			||||||
    throw new APIError(400, 'Invalid description field')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  description = description.slice(0, MAX_DESCRIPTION_LENGTH)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (tags !== undefined && !Array.isArray(tags))
 | 
					 | 
				
			||||||
    throw new APIError(400, 'Invalid tags field')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  tags = (tags || []).map((tag: string) =>
 | 
					 | 
				
			||||||
    tag.toString().slice(0, MAX_TAG_LENGTH)
 | 
					 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  outcomeType = outcomeType ?? 'BINARY'
 | 
					const binarySchema = z.object({
 | 
				
			||||||
 | 
					  initialProb: z.number().min(1).max(99),
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (!OUTCOME_TYPES.includes(outcomeType))
 | 
					const numericSchema = z.object({
 | 
				
			||||||
    throw new APIError(400, 'Invalid outcomeType')
 | 
					  min: z.number(),
 | 
				
			||||||
 | 
					  max: z.number(),
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (
 | 
					export const createContract = newEndpoint(['POST'], async (req, [user, _]) => {
 | 
				
			||||||
    outcomeType === 'NUMERIC' &&
 | 
					  const { question, description, tags, closeTime, outcomeType, resolutionType, automaticResolution, automaticResolutionTime } = validate(
 | 
				
			||||||
    !(
 | 
					    bodySchema,
 | 
				
			||||||
      min !== undefined &&
 | 
					    req.body
 | 
				
			||||||
      max !== undefined &&
 | 
					 | 
				
			||||||
      isFinite(min) &&
 | 
					 | 
				
			||||||
      isFinite(max) &&
 | 
					 | 
				
			||||||
      min < max &&
 | 
					 | 
				
			||||||
      max - min > 0.01
 | 
					 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
    throw new APIError(400, 'Invalid range')
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (
 | 
					  let min, max, initialProb
 | 
				
			||||||
    outcomeType === 'BINARY' &&
 | 
					  if (outcomeType === 'NUMERIC') {
 | 
				
			||||||
    (!initialProb || initialProb < 1 || initialProb > 99)
 | 
					    ;({ min, max } = validate(numericSchema, req.body))
 | 
				
			||||||
  )
 | 
					    if (max - min <= 0.01) throw new APIError(400, 'Invalid range.')
 | 
				
			||||||
    throw new APIError(400, 'Invalid initial probability')
 | 
					  }
 | 
				
			||||||
 | 
					  if (outcomeType === 'BINARY') {
 | 
				
			||||||
  resolutionType = resolutionType ?? 'MANUAL'
 | 
					    ;({ initialProb } = validate(binarySchema, req.body))
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
  if (!RESOLUTION_TYPES.includes(resolutionType))
 | 
					 | 
				
			||||||
  throw new APIError(400, 'Invalid resolutionType')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  automaticResolution = automaticResolution ?? 'MANUAL'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (!RESOLUTIONS.includes(automaticResolution))
 | 
					 | 
				
			||||||
  throw new APIError(400, 'Invalid automaticResolution')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (automaticResolution === 'MANUAL' && automaticResolutionTime)
 | 
					 | 
				
			||||||
  throw new APIError(400, 'automaticResolutionTime specified even tho automaticResolution is \'MANUAL\'')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (automaticResolutionTime && automaticResolutionTime < closeTime)
 | 
					 | 
				
			||||||
  throw new APIError(400, 'resolutionTime < closeTime')
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Uses utc time on server:
 | 
					  // Uses utc time on server:
 | 
				
			||||||
  const yesterday = new Date()
 | 
					  const today = new Date()
 | 
				
			||||||
  yesterday.setUTCDate(yesterday.getUTCDate() - 1)
 | 
					  let freeMarketResetTime = today.setUTCHours(16, 0, 0, 0)
 | 
				
			||||||
  const freeMarketResetTime = yesterday.setUTCHours(16, 0, 0, 0)
 | 
					  if (today.getTime() < freeMarketResetTime) {
 | 
				
			||||||
 | 
					    freeMarketResetTime = freeMarketResetTime - 24 * 60 * 60 * 1000
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const userContractsCreatedTodaySnapshot = await firestore
 | 
					  const userContractsCreatedTodaySnapshot = await firestore
 | 
				
			||||||
    .collection(`contracts`)
 | 
					    .collection(`contracts`)
 | 
				
			||||||
    .where('creatorId', '==', creator.id)
 | 
					    .where('creatorId', '==', user.id)
 | 
				
			||||||
    .where('createdTime', '>=', freeMarketResetTime)
 | 
					    .where('createdTime', '>=', freeMarketResetTime)
 | 
				
			||||||
    .get()
 | 
					    .get()
 | 
				
			||||||
  console.log('free market reset time: ', freeMarketResetTime)
 | 
					  console.log('free market reset time: ', freeMarketResetTime)
 | 
				
			||||||
| 
						 | 
					@ -123,18 +95,9 @@ export const createContract = newEndpoint(['POST'], async (req, _res) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const ante = FIXED_ANTE
 | 
					  const ante = FIXED_ANTE
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (
 | 
					 | 
				
			||||||
    ante === undefined ||
 | 
					 | 
				
			||||||
    ante < MINIMUM_ANTE ||
 | 
					 | 
				
			||||||
    (ante > creator.balance && !isFree) ||
 | 
					 | 
				
			||||||
    isNaN(ante) ||
 | 
					 | 
				
			||||||
    !isFinite(ante)
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
    throw new APIError(400, 'Invalid ante')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  console.log(
 | 
					  console.log(
 | 
				
			||||||
    'creating contract for',
 | 
					    'creating contract for',
 | 
				
			||||||
    creator.username,
 | 
					    user.username,
 | 
				
			||||||
    'on',
 | 
					    'on',
 | 
				
			||||||
    question,
 | 
					    question,
 | 
				
			||||||
    'ante:',
 | 
					    'ante:',
 | 
				
			||||||
| 
						 | 
					@ -142,34 +105,31 @@ export const createContract = newEndpoint(['POST'], async (req, _res) => {
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const slug = await getSlug(question)
 | 
					  const slug = await getSlug(question)
 | 
				
			||||||
 | 
					 | 
				
			||||||
  const contractRef = firestore.collection('contracts').doc()
 | 
					  const contractRef = firestore.collection('contracts').doc()
 | 
				
			||||||
 | 
					 | 
				
			||||||
  const contract = getNewContract(
 | 
					  const contract = getNewContract(
 | 
				
			||||||
    contractRef.id,
 | 
					    contractRef.id,
 | 
				
			||||||
    slug,
 | 
					    slug,
 | 
				
			||||||
    creator,
 | 
					    user,
 | 
				
			||||||
    question,
 | 
					    question,
 | 
				
			||||||
    outcomeType,
 | 
					    outcomeType,
 | 
				
			||||||
    description,
 | 
					    description,
 | 
				
			||||||
    initialProb,
 | 
					    initialProb ?? 0,
 | 
				
			||||||
    ante,
 | 
					    ante,
 | 
				
			||||||
    closeTime,
 | 
					    closeTime.getTime(),
 | 
				
			||||||
    tags ?? [],
 | 
					    tags ?? [],
 | 
				
			||||||
    resolutionType,
 | 
					    resolutionType,
 | 
				
			||||||
    automaticResolution,
 | 
					    automaticResolution,
 | 
				
			||||||
    automaticResolutionTime,
 | 
					    automaticResolutionTime.getTime(),
 | 
				
			||||||
    NUMERIC_BUCKET_COUNT,
 | 
					    NUMERIC_BUCKET_COUNT,
 | 
				
			||||||
    min ?? 0,
 | 
					    min ?? 0,
 | 
				
			||||||
    max ?? 0,
 | 
					    max ?? 0
 | 
				
			||||||
    manaLimitPerUser ?? 0,
 | 
					 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (!isFree && ante) await chargeUser(creator.id, ante, true)
 | 
					  if (!isFree && ante) await chargeUser(user.id, ante, true)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await contractRef.create(contract)
 | 
					  await contractRef.create(contract)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const providerId = isFree ? HOUSE_LIQUIDITY_PROVIDER_ID : creator.id
 | 
					  const providerId = isFree ? HOUSE_LIQUIDITY_PROVIDER_ID : user.id
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (outcomeType === 'BINARY' && contract.mechanism === 'dpm-2') {
 | 
					  if (outcomeType === 'BINARY' && contract.mechanism === 'dpm-2') {
 | 
				
			||||||
    const yesBetDoc = firestore
 | 
					    const yesBetDoc = firestore
 | 
				
			||||||
| 
						 | 
					@ -179,7 +139,7 @@ export const createContract = newEndpoint(['POST'], async (req, _res) => {
 | 
				
			||||||
    const noBetDoc = firestore.collection(`contracts/${contract.id}/bets`).doc()
 | 
					    const noBetDoc = firestore.collection(`contracts/${contract.id}/bets`).doc()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const { yesBet, noBet } = getAnteBets(
 | 
					    const { yesBet, noBet } = getAnteBets(
 | 
				
			||||||
      creator,
 | 
					      user,
 | 
				
			||||||
      contract as FullContract<DPM, Binary>,
 | 
					      contract as FullContract<DPM, Binary>,
 | 
				
			||||||
      yesBetDoc.id,
 | 
					      yesBetDoc.id,
 | 
				
			||||||
      noBetDoc.id
 | 
					      noBetDoc.id
 | 
				
			||||||
| 
						 | 
					@ -205,7 +165,7 @@ export const createContract = newEndpoint(['POST'], async (req, _res) => {
 | 
				
			||||||
      .collection(`contracts/${contract.id}/answers`)
 | 
					      .collection(`contracts/${contract.id}/answers`)
 | 
				
			||||||
      .doc('0')
 | 
					      .doc('0')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const noneAnswer = getNoneAnswer(contract.id, creator)
 | 
					    const noneAnswer = getNoneAnswer(contract.id, user)
 | 
				
			||||||
    await noneAnswerDoc.set(noneAnswer)
 | 
					    await noneAnswerDoc.set(noneAnswer)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const anteBetDoc = firestore
 | 
					    const anteBetDoc = firestore
 | 
				
			||||||
| 
						 | 
					@ -224,7 +184,7 @@ export const createContract = newEndpoint(['POST'], async (req, _res) => {
 | 
				
			||||||
      .doc()
 | 
					      .doc()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const anteBet = getNumericAnte(
 | 
					    const anteBet = getNumericAnte(
 | 
				
			||||||
      creator,
 | 
					      user,
 | 
				
			||||||
      contract as FullContract<DPM, Numeric>,
 | 
					      contract as FullContract<DPM, Numeric>,
 | 
				
			||||||
      ante,
 | 
					      ante,
 | 
				
			||||||
      anteBetDoc.id
 | 
					      anteBetDoc.id
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,122 +1,95 @@
 | 
				
			||||||
import * as admin from 'firebase-admin'
 | 
					import * as admin from 'firebase-admin'
 | 
				
			||||||
 | 
					import { z } from 'zod'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { APIError, newEndpoint, parseCredentials, lookupUser } from './api'
 | 
					import { APIError, newEndpoint, validate } from './api'
 | 
				
			||||||
import { Contract } from '../../common/contract'
 | 
					import { Contract } from '../../common/contract'
 | 
				
			||||||
import { User } from '../../common/user'
 | 
					import { User } from '../../common/user'
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
 | 
					  BetInfo,
 | 
				
			||||||
  getNewBinaryCpmmBetInfo,
 | 
					  getNewBinaryCpmmBetInfo,
 | 
				
			||||||
  getNewBinaryDpmBetInfo,
 | 
					  getNewBinaryDpmBetInfo,
 | 
				
			||||||
  getNewMultiBetInfo,
 | 
					  getNewMultiBetInfo,
 | 
				
			||||||
  getNumericBetsInfo,
 | 
					  getNumericBetsInfo,
 | 
				
			||||||
} from '../../common/new-bet'
 | 
					} from '../../common/new-bet'
 | 
				
			||||||
import { addObjects, removeUndefinedProps } from '../../common/util/object'
 | 
					import { addObjects, removeUndefinedProps } from '../../common/util/object'
 | 
				
			||||||
import { Bet } from '../../common/bet'
 | 
					 | 
				
			||||||
import { redeemShares } from './redeem-shares'
 | 
					import { redeemShares } from './redeem-shares'
 | 
				
			||||||
import { Fees } from '../../common/fees'
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const placeBet = newEndpoint(['POST'], async (req, _res) => {
 | 
					const bodySchema = z.object({
 | 
				
			||||||
  const [bettor, _privateUser] = await lookupUser(await parseCredentials(req))
 | 
					  contractId: z.string(),
 | 
				
			||||||
  const { amount, outcome, contractId, value } = req.body || {}
 | 
					  amount: z.number().gte(1),
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (amount < 1 || isNaN(amount) || !isFinite(amount))
 | 
					const binarySchema = z.object({
 | 
				
			||||||
    throw new APIError(400, 'Invalid amount')
 | 
					  outcome: z.enum(['YES', 'NO']),
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (outcome !== 'YES' && outcome !== 'NO' && isNaN(+outcome))
 | 
					const freeResponseSchema = z.object({
 | 
				
			||||||
    throw new APIError(400, 'Invalid outcome')
 | 
					  outcome: z.string(),
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (value !== undefined && !isFinite(value))
 | 
					const numericSchema = z.object({
 | 
				
			||||||
    throw new APIError(400, 'Invalid value')
 | 
					  outcome: z.string(),
 | 
				
			||||||
 | 
					  value: z.number(),
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // run as transaction to prevent race conditions
 | 
					export const placeBet = newEndpoint(['POST'], async (req, [bettor, _]) => {
 | 
				
			||||||
  return await firestore
 | 
					  const { amount, contractId } = validate(bodySchema, req.body)
 | 
				
			||||||
    .runTransaction(async (transaction) => {
 | 
					
 | 
				
			||||||
 | 
					  const result = await firestore.runTransaction(async (trans) => {
 | 
				
			||||||
    const userDoc = firestore.doc(`users/${bettor.id}`)
 | 
					    const userDoc = firestore.doc(`users/${bettor.id}`)
 | 
				
			||||||
      const userSnap = await transaction.get(userDoc)
 | 
					    const userSnap = await trans.get(userDoc)
 | 
				
			||||||
      if (!userSnap.exists) throw new APIError(400, 'User not found')
 | 
					    if (!userSnap.exists) throw new APIError(400, 'User not found.')
 | 
				
			||||||
    const user = userSnap.data() as User
 | 
					    const user = userSnap.data() as User
 | 
				
			||||||
 | 
					    if (user.balance < amount) throw new APIError(400, 'Insufficient balance.')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const contractDoc = firestore.doc(`contracts/${contractId}`)
 | 
					    const contractDoc = firestore.doc(`contracts/${contractId}`)
 | 
				
			||||||
      const contractSnap = await transaction.get(contractDoc)
 | 
					    const contractSnap = await trans.get(contractDoc)
 | 
				
			||||||
      if (!contractSnap.exists) throw new APIError(400, 'Invalid contract')
 | 
					    if (!contractSnap.exists) throw new APIError(400, 'Contract not found.')
 | 
				
			||||||
    const contract = contractSnap.data() as Contract
 | 
					    const contract = contractSnap.data() as Contract
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const loanAmount = 0
 | 
				
			||||||
    const { closeTime, outcomeType, mechanism, collectedFees, volume } =
 | 
					    const { closeTime, outcomeType, mechanism, collectedFees, volume } =
 | 
				
			||||||
      contract
 | 
					      contract
 | 
				
			||||||
    if (closeTime && Date.now() > closeTime)
 | 
					    if (closeTime && Date.now() > closeTime)
 | 
				
			||||||
        throw new APIError(400, 'Trading is closed')
 | 
					      throw new APIError(400, 'Trading is closed.')
 | 
				
			||||||
 | 
					 | 
				
			||||||
      const yourBetsSnap = await transaction.get(
 | 
					 | 
				
			||||||
        contractDoc.collection('bets').where('userId', '==', bettor.id)
 | 
					 | 
				
			||||||
      )
 | 
					 | 
				
			||||||
      const yourBets = yourBetsSnap.docs.map((doc) => doc.data() as Bet)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const loanAmount = 0 // getLoanAmount(yourBets, amount)
 | 
					 | 
				
			||||||
      if (user.balance < amount) throw new APIError(400, 'Insufficient balance')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (outcomeType === 'FREE_RESPONSE') {
 | 
					 | 
				
			||||||
        const answerSnap = await transaction.get(
 | 
					 | 
				
			||||||
          contractDoc.collection('answers').doc(outcome)
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        if (!answerSnap.exists) throw new APIError(400, 'Invalid contract')
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const newBetDoc = firestore
 | 
					 | 
				
			||||||
        .collection(`contracts/${contractId}/bets`)
 | 
					 | 
				
			||||||
        .doc()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const {
 | 
					    const {
 | 
				
			||||||
      newBet,
 | 
					      newBet,
 | 
				
			||||||
      newPool,
 | 
					      newPool,
 | 
				
			||||||
      newTotalShares,
 | 
					      newTotalShares,
 | 
				
			||||||
      newTotalBets,
 | 
					      newTotalBets,
 | 
				
			||||||
        newBalance,
 | 
					 | 
				
			||||||
      newTotalLiquidity,
 | 
					      newTotalLiquidity,
 | 
				
			||||||
        fees,
 | 
					 | 
				
			||||||
      newP,
 | 
					      newP,
 | 
				
			||||||
      } =
 | 
					    } = await (async (): Promise<BetInfo> => {
 | 
				
			||||||
        outcomeType === 'BINARY'
 | 
					      if (outcomeType == 'BINARY' && mechanism == 'dpm-2') {
 | 
				
			||||||
          ? mechanism === 'dpm-2'
 | 
					        const { outcome } = validate(binarySchema, req.body)
 | 
				
			||||||
            ? getNewBinaryDpmBetInfo(
 | 
					        return getNewBinaryDpmBetInfo(outcome, amount, contract, loanAmount)
 | 
				
			||||||
                user,
 | 
					      } else if (outcomeType == 'BINARY' && mechanism == 'cpmm-1') {
 | 
				
			||||||
                outcome as 'YES' | 'NO',
 | 
					        const { outcome } = validate(binarySchema, req.body)
 | 
				
			||||||
                amount,
 | 
					        return getNewBinaryCpmmBetInfo(outcome, amount, contract, loanAmount)
 | 
				
			||||||
                contract,
 | 
					      } else if (outcomeType == 'FREE_RESPONSE' && mechanism == 'dpm-2') {
 | 
				
			||||||
                loanAmount,
 | 
					        const { outcome } = validate(freeResponseSchema, req.body)
 | 
				
			||||||
                newBetDoc.id
 | 
					        const answerDoc = contractDoc.collection('answers').doc(outcome)
 | 
				
			||||||
              )
 | 
					        const answerSnap = await trans.get(answerDoc)
 | 
				
			||||||
            : (getNewBinaryCpmmBetInfo(
 | 
					        if (!answerSnap.exists) throw new APIError(400, 'Invalid answer')
 | 
				
			||||||
                user,
 | 
					        return getNewMultiBetInfo(outcome, amount, contract, loanAmount)
 | 
				
			||||||
                outcome as 'YES' | 'NO',
 | 
					      } else if (outcomeType == 'NUMERIC' && mechanism == 'dpm-2') {
 | 
				
			||||||
                amount,
 | 
					        const { outcome, value } = validate(numericSchema, req.body)
 | 
				
			||||||
                contract,
 | 
					        return getNumericBetsInfo(value, outcome, amount, contract)
 | 
				
			||||||
                loanAmount,
 | 
					      } else {
 | 
				
			||||||
                newBetDoc.id
 | 
					        throw new APIError(500, 'Contract has invalid type/mechanism.')
 | 
				
			||||||
              ) as any)
 | 
					      }
 | 
				
			||||||
          : outcomeType === 'NUMERIC' && mechanism === 'dpm-2'
 | 
					    })()
 | 
				
			||||||
          ? getNumericBetsInfo(
 | 
					 | 
				
			||||||
              user,
 | 
					 | 
				
			||||||
              value,
 | 
					 | 
				
			||||||
              outcome,
 | 
					 | 
				
			||||||
              amount,
 | 
					 | 
				
			||||||
              contract,
 | 
					 | 
				
			||||||
              newBetDoc.id
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
          : getNewMultiBetInfo(
 | 
					 | 
				
			||||||
              user,
 | 
					 | 
				
			||||||
              outcome,
 | 
					 | 
				
			||||||
              amount,
 | 
					 | 
				
			||||||
              contract as any,
 | 
					 | 
				
			||||||
              loanAmount,
 | 
					 | 
				
			||||||
              newBetDoc.id
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (newP !== undefined && !isFinite(newP)) {
 | 
					    if (newP != null && !isFinite(newP)) {
 | 
				
			||||||
      throw new APIError(400, 'Trade rejected due to overflow error.')
 | 
					      throw new APIError(400, 'Trade rejected due to overflow error.')
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      transaction.create(newBetDoc, newBet)
 | 
					    const newBalance = user.balance - amount - loanAmount
 | 
				
			||||||
 | 
					    const betDoc = contractDoc.collection('bets').doc()
 | 
				
			||||||
      transaction.update(
 | 
					    trans.create(betDoc, { id: betDoc.id, userId: user.id, ...newBet })
 | 
				
			||||||
 | 
					    trans.update(userDoc, { balance: newBalance })
 | 
				
			||||||
 | 
					    trans.update(
 | 
				
			||||||
      contractDoc,
 | 
					      contractDoc,
 | 
				
			||||||
      removeUndefinedProps({
 | 
					      removeUndefinedProps({
 | 
				
			||||||
        pool: newPool,
 | 
					        pool: newPool,
 | 
				
			||||||
| 
						 | 
					@ -124,23 +97,16 @@ export const placeBet = newEndpoint(['POST'], async (req, _res) => {
 | 
				
			||||||
        totalShares: newTotalShares,
 | 
					        totalShares: newTotalShares,
 | 
				
			||||||
        totalBets: newTotalBets,
 | 
					        totalBets: newTotalBets,
 | 
				
			||||||
        totalLiquidity: newTotalLiquidity,
 | 
					        totalLiquidity: newTotalLiquidity,
 | 
				
			||||||
          collectedFees: addObjects<Fees>(fees ?? {}, collectedFees ?? {}),
 | 
					        collectedFees: addObjects(newBet.fees, collectedFees),
 | 
				
			||||||
          volume: volume + Math.abs(amount),
 | 
					        volume: volume + amount,
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (!isFinite(newBalance)) {
 | 
					    return { betId: betDoc.id }
 | 
				
			||||||
        throw new APIError(500, 'Invalid user balance for ' + user.username)
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      transaction.update(userDoc, { balance: newBalance })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      return { betId: newBetDoc.id }
 | 
					 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
    .then(async (result) => {
 | 
					
 | 
				
			||||||
  await redeemShares(bettor.id, contractId)
 | 
					  await redeemShares(bettor.id, contractId)
 | 
				
			||||||
  return result
 | 
					  return result
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
})
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const firestore = admin.firestore()
 | 
					const firestore = admin.firestore()
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,7 @@ import * as functions from 'firebase-functions'
 | 
				
			||||||
import * as admin from 'firebase-admin'
 | 
					import * as admin from 'firebase-admin'
 | 
				
			||||||
import { difference, uniq, mapValues, groupBy, sumBy } from 'lodash'
 | 
					import { difference, uniq, mapValues, groupBy, sumBy } from 'lodash'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { Contract, RESOLUTIONS } from '../../common/contract'
 | 
					import { Contract, resolution, RESOLUTIONS } from '../../common/contract'
 | 
				
			||||||
import { User } from '../../common/user'
 | 
					import { User } from '../../common/user'
 | 
				
			||||||
import { Bet } from '../../common/bet'
 | 
					import { Bet } from '../../common/bet'
 | 
				
			||||||
import { getUser, isProd, payUser } from './utils'
 | 
					import { getUser, isProd, payUser } from './utils'
 | 
				
			||||||
| 
						 | 
					@ -21,7 +21,7 @@ export const resolveMarket = functions
 | 
				
			||||||
  .https.onCall(
 | 
					  .https.onCall(
 | 
				
			||||||
    async (
 | 
					    async (
 | 
				
			||||||
      data: {
 | 
					      data: {
 | 
				
			||||||
        outcome: string
 | 
					        outcome: resolution
 | 
				
			||||||
        value?: number
 | 
					        value?: number
 | 
				
			||||||
        contractId: string
 | 
					        contractId: string
 | 
				
			||||||
        probabilityInt?: number
 | 
					        probabilityInt?: number
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										23
									
								
								functions/src/scripts/backfill-fees.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								functions/src/scripts/backfill-fees.ts
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,23 @@
 | 
				
			||||||
 | 
					// We have many old contracts without a collectedFees data structure. Let's fill them in.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import * as admin from 'firebase-admin'
 | 
				
			||||||
 | 
					import { initAdmin } from './script-init'
 | 
				
			||||||
 | 
					import { noFees } from '../../../common/fees'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					initAdmin()
 | 
				
			||||||
 | 
					const firestore = admin.firestore()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if (require.main === module) {
 | 
				
			||||||
 | 
					  const contractsRef = firestore.collection('contracts')
 | 
				
			||||||
 | 
					  contractsRef.get().then(async (contractsSnaps) => {
 | 
				
			||||||
 | 
					    console.log(`Loaded ${contractsSnaps.size} contracts.`)
 | 
				
			||||||
 | 
					    const needsFilling = contractsSnaps.docs.filter((ct) => {
 | 
				
			||||||
 | 
					      return !('collectedFees' in ct.data())
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    console.log(`Found ${needsFilling.length} contracts to update.`)
 | 
				
			||||||
 | 
					    await Promise.all(
 | 
				
			||||||
 | 
					      needsFilling.map((ct) => ct.ref.update({ collectedFees: noFees }))
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    console.log(`Updated all contracts.`)
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -78,7 +78,7 @@ export const sellBet = functions.runWith({ minInstances: 1 }).https.onCall(
 | 
				
			||||||
          pool: newPool,
 | 
					          pool: newPool,
 | 
				
			||||||
          totalShares: newTotalShares,
 | 
					          totalShares: newTotalShares,
 | 
				
			||||||
          totalBets: newTotalBets,
 | 
					          totalBets: newTotalBets,
 | 
				
			||||||
          collectedFees: addObjects<Fees>(fees ?? {}, collectedFees ?? {}),
 | 
					          collectedFees: addObjects(fees, collectedFees),
 | 
				
			||||||
          volume: volume + Math.abs(newBet.amount),
 | 
					          volume: volume + Math.abs(newBet.amount),
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
      )
 | 
					      )
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -101,7 +101,7 @@ export const sellShares = functions.runWith({ minInstances: 1 }).https.onCall(
 | 
				
			||||||
        removeUndefinedProps({
 | 
					        removeUndefinedProps({
 | 
				
			||||||
          pool: newPool,
 | 
					          pool: newPool,
 | 
				
			||||||
          p: newP,
 | 
					          p: newP,
 | 
				
			||||||
          collectedFees: addObjects(fees ?? {}, collectedFees ?? {}),
 | 
					          collectedFees: addObjects(fees, collectedFees),
 | 
				
			||||||
          volume: volume + Math.abs(newBet.amount),
 | 
					          volume: volume + Math.abs(newBet.amount),
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
      )
 | 
					      )
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,11 +5,13 @@ import Stripe from 'stripe'
 | 
				
			||||||
import { getPrivateUser, getUser, isProd, payUser } from './utils'
 | 
					import { getPrivateUser, getUser, isProd, payUser } from './utils'
 | 
				
			||||||
import { sendThankYouEmail } from './emails'
 | 
					import { sendThankYouEmail } from './emails'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type StripeSession = Stripe.Event.Data.Object & { id: any, metadata: any}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type StripeTransaction = {
 | 
					export type StripeTransaction = {
 | 
				
			||||||
  userId: string
 | 
					  userId: string
 | 
				
			||||||
  manticDollarQuantity: number
 | 
					  manticDollarQuantity: number
 | 
				
			||||||
  sessionId: string
 | 
					  sessionId: string
 | 
				
			||||||
  session: any
 | 
					  session: StripeSession
 | 
				
			||||||
  timestamp: number
 | 
					  timestamp: number
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -96,14 +98,14 @@ export const stripeWebhook = functions
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (event.type === 'checkout.session.completed') {
 | 
					    if (event.type === 'checkout.session.completed') {
 | 
				
			||||||
      const session = event.data.object as any
 | 
					      const session = event.data.object as StripeSession
 | 
				
			||||||
      await issueMoneys(session)
 | 
					      await issueMoneys(session)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    res.status(200).send('success')
 | 
					    res.status(200).send('success')
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const issueMoneys = async (session: any) => {
 | 
					const issueMoneys = async (session: StripeSession) => {
 | 
				
			||||||
  const { id: sessionId } = session
 | 
					  const { id: sessionId } = session
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const query = await firestore
 | 
					  const query = await firestore
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,10 +1,21 @@
 | 
				
			||||||
module.exports = {
 | 
					module.exports = {
 | 
				
			||||||
  parser: '@typescript-eslint/parser',
 | 
					  parser: '@typescript-eslint/parser',
 | 
				
			||||||
  plugins: ['lodash'],
 | 
					  plugins: ['lodash'],
 | 
				
			||||||
  extends: ['plugin:react-hooks/recommended', 'plugin:@next/next/recommended'],
 | 
					  extends: [
 | 
				
			||||||
 | 
					    'plugin:@typescript-eslint/recommended',
 | 
				
			||||||
 | 
					    'plugin:react-hooks/recommended',
 | 
				
			||||||
 | 
					    'plugin:@next/next/recommended',
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
  rules: {
 | 
					  rules: {
 | 
				
			||||||
 | 
					    '@typescript-eslint/no-empty-function': 'off',
 | 
				
			||||||
 | 
					    '@typescript-eslint/no-unused-vars': 'off',
 | 
				
			||||||
 | 
					    '@typescript-eslint/no-explicit-any': 'off',
 | 
				
			||||||
    '@next/next/no-img-element': 'off',
 | 
					    '@next/next/no-img-element': 'off',
 | 
				
			||||||
    '@next/next/no-typos': 'off',
 | 
					    '@next/next/no-typos': 'off',
 | 
				
			||||||
    'lodash/import-scope': [2, 'member'],
 | 
					    'lodash/import-scope': [2, 'member'],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					  env: {
 | 
				
			||||||
 | 
					    browser: true,
 | 
				
			||||||
 | 
					    node: true,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,3 +1,4 @@
 | 
				
			||||||
 | 
					import { ReactNode } from 'react'
 | 
				
			||||||
import Head from 'next/head'
 | 
					import Head from 'next/head'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type OgCardProps = {
 | 
					export type OgCardProps = {
 | 
				
			||||||
| 
						 | 
					@ -35,7 +36,7 @@ export function SEO(props: {
 | 
				
			||||||
  title: string
 | 
					  title: string
 | 
				
			||||||
  description: string
 | 
					  description: string
 | 
				
			||||||
  url?: string
 | 
					  url?: string
 | 
				
			||||||
  children?: any[]
 | 
					  children?: ReactNode
 | 
				
			||||||
  ogCardProps?: OgCardProps
 | 
					  ogCardProps?: OgCardProps
 | 
				
			||||||
}) {
 | 
					}) {
 | 
				
			||||||
  const { title, description, url, children, ogCardProps } = props
 | 
					  const { title, description, url, children, ogCardProps } = props
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,7 @@
 | 
				
			||||||
import clsx from 'clsx'
 | 
					import clsx from 'clsx'
 | 
				
			||||||
import { useState } from 'react'
 | 
					import { useState, ReactNode } from 'react'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function AdvancedPanel(props: { children: any }) {
 | 
					export function AdvancedPanel(props: { children: ReactNode }) {
 | 
				
			||||||
  const { children } = props
 | 
					  const { children } = props
 | 
				
			||||||
  const [collapsed, setCollapsed] = useState(true)
 | 
					  const [collapsed, setCollapsed] = useState(true)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,5 @@
 | 
				
			||||||
import clsx from 'clsx'
 | 
					import clsx from 'clsx'
 | 
				
			||||||
 | 
					import React from 'react'
 | 
				
			||||||
import { useUser } from 'web/hooks/use-user'
 | 
					import { useUser } from 'web/hooks/use-user'
 | 
				
			||||||
import { formatMoney } from 'common/util/format'
 | 
					import { formatMoney } from 'common/util/format'
 | 
				
			||||||
import { Col } from './layout/col'
 | 
					import { Col } from './layout/col'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,6 @@
 | 
				
			||||||
import Router from 'next/router'
 | 
					import Router from 'next/router'
 | 
				
			||||||
import clsx from 'clsx'
 | 
					import clsx from 'clsx'
 | 
				
			||||||
 | 
					import { MouseEvent } from 'react'
 | 
				
			||||||
import { UserCircleIcon } from '@heroicons/react/solid'
 | 
					import { UserCircleIcon } from '@heroicons/react/solid'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function Avatar(props: {
 | 
					export function Avatar(props: {
 | 
				
			||||||
| 
						 | 
					@ -15,7 +16,7 @@ export function Avatar(props: {
 | 
				
			||||||
  const onClick =
 | 
					  const onClick =
 | 
				
			||||||
    noLink && username
 | 
					    noLink && username
 | 
				
			||||||
      ? undefined
 | 
					      ? undefined
 | 
				
			||||||
      : (e: any) => {
 | 
					      : (e: MouseEvent) => {
 | 
				
			||||||
          e.stopPropagation()
 | 
					          e.stopPropagation()
 | 
				
			||||||
          Router.push(`/${username}`)
 | 
					          Router.push(`/${username}`)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -50,7 +50,7 @@ import { trackLatency } from 'web/lib/firebase/tracking'
 | 
				
			||||||
import { NumericContract } from 'common/contract'
 | 
					import { NumericContract } from 'common/contract'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type BetSort = 'newest' | 'profit' | 'closeTime' | 'value'
 | 
					type BetSort = 'newest' | 'profit' | 'closeTime' | 'value'
 | 
				
			||||||
type BetFilter = 'open' | 'closed' | 'resolved' | 'all'
 | 
					type BetFilter = 'open' | 'sold' | 'closed' | 'resolved' | 'all'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function BetsList(props: { user: User }) {
 | 
					export function BetsList(props: { user: User }) {
 | 
				
			||||||
  const { user } = props
 | 
					  const { user } = props
 | 
				
			||||||
| 
						 | 
					@ -107,6 +107,7 @@ export function BetsList(props: { user: User }) {
 | 
				
			||||||
      !FILTERS.resolved(c) && (c.closeTime ?? Infinity) < Date.now(),
 | 
					      !FILTERS.resolved(c) && (c.closeTime ?? Infinity) < Date.now(),
 | 
				
			||||||
    open: (c) => !(FILTERS.closed(c) || FILTERS.resolved(c)),
 | 
					    open: (c) => !(FILTERS.closed(c) || FILTERS.resolved(c)),
 | 
				
			||||||
    all: () => true,
 | 
					    all: () => true,
 | 
				
			||||||
 | 
					    sold: () => true,
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  const SORTS: Record<BetSort, (c: Contract) => number> = {
 | 
					  const SORTS: Record<BetSort, (c: Contract) => number> = {
 | 
				
			||||||
    profit: (c) => contractsMetrics[c.id].profit,
 | 
					    profit: (c) => contractsMetrics[c.id].profit,
 | 
				
			||||||
| 
						 | 
					@ -122,10 +123,14 @@ export function BetsList(props: { user: User }) {
 | 
				
			||||||
    .reverse()
 | 
					    .reverse()
 | 
				
			||||||
    .filter(FILTERS[filter])
 | 
					    .filter(FILTERS[filter])
 | 
				
			||||||
    .filter((c) => {
 | 
					    .filter((c) => {
 | 
				
			||||||
      if (sort === 'profit') return true
 | 
					      if (filter === 'all') return true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // Filter out contracts where you don't have shares anymore.
 | 
					 | 
				
			||||||
      const metrics = contractsMetrics[c.id]
 | 
					      const metrics = contractsMetrics[c.id]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Filter for contracts you sold out of.
 | 
				
			||||||
 | 
					      if (filter === 'sold') return metrics.payout === 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Filter for contracts where you currently have shares.
 | 
				
			||||||
      return metrics.payout > 0
 | 
					      return metrics.payout > 0
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -181,6 +186,7 @@ export function BetsList(props: { user: User }) {
 | 
				
			||||||
            onChange={(e) => setFilter(e.target.value as BetFilter)}
 | 
					            onChange={(e) => setFilter(e.target.value as BetFilter)}
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            <option value="open">Open</option>
 | 
					            <option value="open">Open</option>
 | 
				
			||||||
 | 
					            <option value="sold">Sold</option>
 | 
				
			||||||
            <option value="closed">Closed</option>
 | 
					            <option value="closed">Closed</option>
 | 
				
			||||||
            <option value="resolved">Resolved</option>
 | 
					            <option value="resolved">Resolved</option>
 | 
				
			||||||
            <option value="all">All</option>
 | 
					            <option value="all">All</option>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
// Adapted from https://stackoverflow.com/a/50884055/1222351
 | 
					// Adapted from https://stackoverflow.com/a/50884055/1222351
 | 
				
			||||||
import { useEffect, useState } from 'react'
 | 
					import React, { useEffect, useState } from 'react'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function ClientRender(props: { children: React.ReactNode }) {
 | 
					export function ClientRender(props: { children: React.ReactNode }) {
 | 
				
			||||||
  const { children } = props
 | 
					  const { children } = props
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
import clsx from 'clsx'
 | 
					import clsx from 'clsx'
 | 
				
			||||||
import { useState } from 'react'
 | 
					import { ReactNode, useState } from 'react'
 | 
				
			||||||
import { Col } from './layout/col'
 | 
					import { Col } from './layout/col'
 | 
				
			||||||
import { Modal } from './layout/modal'
 | 
					import { Modal } from './layout/modal'
 | 
				
			||||||
import { Row } from './layout/row'
 | 
					import { Row } from './layout/row'
 | 
				
			||||||
| 
						 | 
					@ -20,7 +20,7 @@ export function ConfirmationButton(props: {
 | 
				
			||||||
    className?: string
 | 
					    className?: string
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  onSubmit: () => void
 | 
					  onSubmit: () => void
 | 
				
			||||||
  children: any
 | 
					  children: ReactNode
 | 
				
			||||||
}) {
 | 
					}) {
 | 
				
			||||||
  const { id, openModalBtn, cancelBtn, submitBtn, onSubmit, children } = props
 | 
					  const { id, openModalBtn, cancelBtn, submitBtn, onSubmit, children } = props
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -20,6 +20,7 @@ import {
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  AnswerLabel,
 | 
					  AnswerLabel,
 | 
				
			||||||
  BinaryContractOutcomeLabel,
 | 
					  BinaryContractOutcomeLabel,
 | 
				
			||||||
 | 
					  CancelLabel,
 | 
				
			||||||
  FreeResponseOutcomeLabel,
 | 
					  FreeResponseOutcomeLabel,
 | 
				
			||||||
} from '../outcome-label'
 | 
					} from '../outcome-label'
 | 
				
			||||||
import { getOutcomeProbability, getTopAnswer } from 'common/calculate'
 | 
					import { getOutcomeProbability, getTopAnswer } from 'common/calculate'
 | 
				
			||||||
| 
						 | 
					@ -240,7 +241,12 @@ export function NumericResolutionOrExpectation(props: {
 | 
				
			||||||
      {resolution ? (
 | 
					      {resolution ? (
 | 
				
			||||||
        <>
 | 
					        <>
 | 
				
			||||||
          <div className={clsx('text-base text-gray-500')}>Resolved</div>
 | 
					          <div className={clsx('text-base text-gray-500')}>Resolved</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          {resolution === 'CANCEL' ? (
 | 
				
			||||||
 | 
					            <CancelLabel />
 | 
				
			||||||
 | 
					          ) : (
 | 
				
			||||||
            <div className="text-blue-400">{resolutionValue}</div>
 | 
					            <div className="text-blue-400">{resolutionValue}</div>
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
        </>
 | 
					        </>
 | 
				
			||||||
      ) : (
 | 
					      ) : (
 | 
				
			||||||
        <>
 | 
					        <>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -95,7 +95,7 @@ export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              <tr>
 | 
					              <tr>
 | 
				
			||||||
                <td>Creator earnings</td>
 | 
					                <td>Creator earnings</td>
 | 
				
			||||||
                <td>{formatMoney(contract.collectedFees?.creatorFee ?? 0)}</td>
 | 
					                <td>{formatMoney(contract.collectedFees.creatorFee)}</td>
 | 
				
			||||||
              </tr>
 | 
					              </tr>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              <tr>
 | 
					              <tr>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -230,13 +230,14 @@ function QuickOutcomeView(props: {
 | 
				
			||||||
    case 'NUMERIC':
 | 
					    case 'NUMERIC':
 | 
				
			||||||
      display = formatLargeNumber(getExpectedValue(contract as NumericContract))
 | 
					      display = formatLargeNumber(getExpectedValue(contract as NumericContract))
 | 
				
			||||||
      break
 | 
					      break
 | 
				
			||||||
    case 'FREE_RESPONSE':
 | 
					    case 'FREE_RESPONSE': {
 | 
				
			||||||
      const topAnswer = getTopAnswer(contract as FreeResponseContract)
 | 
					      const topAnswer = getTopAnswer(contract as FreeResponseContract)
 | 
				
			||||||
      display =
 | 
					      display =
 | 
				
			||||||
        topAnswer &&
 | 
					        topAnswer &&
 | 
				
			||||||
        formatPercent(getOutcomeProbability(contract, topAnswer.id))
 | 
					        formatPercent(getOutcomeProbability(contract, topAnswer.id))
 | 
				
			||||||
      break
 | 
					      break
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Col className={clsx('items-center text-3xl', textColor)}>
 | 
					    <Col className={clsx('items-center text-3xl', textColor)}>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,3 +1,4 @@
 | 
				
			||||||
 | 
					import React from 'react'
 | 
				
			||||||
import dayjs from 'dayjs'
 | 
					import dayjs from 'dayjs'
 | 
				
			||||||
import utc from 'dayjs/plugin/utc'
 | 
					import utc from 'dayjs/plugin/utc'
 | 
				
			||||||
import timezone from 'dayjs/plugin/timezone'
 | 
					import timezone from 'dayjs/plugin/timezone'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -343,7 +343,7 @@ function groupBetsAndComments(
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // iterate through the bets and comment activity items and add them to the items in order of comment creation time:
 | 
					  // iterate through the bets and comment activity items and add them to the items in order of comment creation time:
 | 
				
			||||||
  const unorderedBetsAndComments = [...commentsWithoutBets, ...groupedBets]
 | 
					  const unorderedBetsAndComments = [...commentsWithoutBets, ...groupedBets]
 | 
				
			||||||
  let sortedBetsAndComments = sortBy(unorderedBetsAndComments, (item) => {
 | 
					  const sortedBetsAndComments = sortBy(unorderedBetsAndComments, (item) => {
 | 
				
			||||||
    if (item.type === 'comment') {
 | 
					    if (item.type === 'comment') {
 | 
				
			||||||
      return item.comment.createdTime
 | 
					      return item.comment.createdTime
 | 
				
			||||||
    } else if (item.type === 'bet') {
 | 
					    } else if (item.type === 'bet') {
 | 
				
			||||||
| 
						 | 
					@ -540,7 +540,7 @@ export function getSpecificContractActivityItems(
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
  const { mode } = options
 | 
					  const { mode } = options
 | 
				
			||||||
  let items = [] as ActivityItem[]
 | 
					  const items = [] as ActivityItem[]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  switch (mode) {
 | 
					  switch (mode) {
 | 
				
			||||||
    case 'bets':
 | 
					    case 'bets':
 | 
				
			||||||
| 
						 | 
					@ -559,7 +559,7 @@ export function getSpecificContractActivityItems(
 | 
				
			||||||
      )
 | 
					      )
 | 
				
			||||||
      break
 | 
					      break
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    case 'comments':
 | 
					    case 'comments': {
 | 
				
			||||||
      const nonFreeResponseComments = comments.filter((comment) =>
 | 
					      const nonFreeResponseComments = comments.filter((comment) =>
 | 
				
			||||||
        commentIsGeneralComment(comment, contract)
 | 
					        commentIsGeneralComment(comment, contract)
 | 
				
			||||||
      )
 | 
					      )
 | 
				
			||||||
| 
						 | 
					@ -585,6 +585,7 @@ export function getSpecificContractActivityItems(
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
      break
 | 
					      break
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    case 'free-response-comment-answer-groups':
 | 
					    case 'free-response-comment-answer-groups':
 | 
				
			||||||
      items.push(
 | 
					      items.push(
 | 
				
			||||||
        ...getAnswerAndCommentInputGroups(
 | 
					        ...getAnswerAndCommentInputGroups(
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -21,7 +21,7 @@ export function CopyLinkDateTimeComponent(props: {
 | 
				
			||||||
    event: React.MouseEvent<HTMLAnchorElement, MouseEvent>
 | 
					    event: React.MouseEvent<HTMLAnchorElement, MouseEvent>
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    event.preventDefault()
 | 
					    event.preventDefault()
 | 
				
			||||||
    let elementLocation = `https://${ENV_CONFIG.domain}${contractPath(
 | 
					    const elementLocation = `https://${ENV_CONFIG.domain}${contractPath(
 | 
				
			||||||
      contract
 | 
					      contract
 | 
				
			||||||
    )}#${elementId}`
 | 
					    )}#${elementId}`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,12 +1,14 @@
 | 
				
			||||||
 | 
					import { ReactNode } from 'react'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const JoinSpans = (props: {
 | 
					export const JoinSpans = (props: {
 | 
				
			||||||
  children: any[]
 | 
					  children: ReactNode[]
 | 
				
			||||||
  separator?: JSX.Element | string
 | 
					  separator?: ReactNode
 | 
				
			||||||
}) => {
 | 
					}) => {
 | 
				
			||||||
  const { separator } = props
 | 
					  const { separator } = props
 | 
				
			||||||
  const children = props.children.filter((x) => !!x)
 | 
					  const children = props.children.filter((x) => !!x)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (children.length === 0) return <></>
 | 
					  if (children.length === 0) return <></>
 | 
				
			||||||
  if (children.length === 1) return children[0]
 | 
					  if (children.length === 1) return <>{children[0]}</>
 | 
				
			||||||
  if (children.length === 2)
 | 
					  if (children.length === 2)
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <>
 | 
					      <>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,8 +1,8 @@
 | 
				
			||||||
import clsx from 'clsx'
 | 
					import clsx from 'clsx'
 | 
				
			||||||
import { CSSProperties, Ref } from 'react'
 | 
					import { CSSProperties, Ref, ReactNode } from 'react'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function Col(props: {
 | 
					export function Col(props: {
 | 
				
			||||||
  children?: any
 | 
					  children?: ReactNode
 | 
				
			||||||
  className?: string
 | 
					  className?: string
 | 
				
			||||||
  style?: CSSProperties
 | 
					  style?: CSSProperties
 | 
				
			||||||
  ref?: Ref<HTMLDivElement>
 | 
					  ref?: Ref<HTMLDivElement>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,9 +1,9 @@
 | 
				
			||||||
import { Fragment } from 'react'
 | 
					import { Fragment, ReactNode } from 'react'
 | 
				
			||||||
import { Dialog, Transition } from '@headlessui/react'
 | 
					import { Dialog, Transition } from '@headlessui/react'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// From https://tailwindui.com/components/application-ui/overlays/modals
 | 
					// From https://tailwindui.com/components/application-ui/overlays/modals
 | 
				
			||||||
export function Modal(props: {
 | 
					export function Modal(props: {
 | 
				
			||||||
  children: React.ReactNode
 | 
					  children: ReactNode
 | 
				
			||||||
  open: boolean
 | 
					  open: boolean
 | 
				
			||||||
  setOpen: (open: boolean) => void
 | 
					  setOpen: (open: boolean) => void
 | 
				
			||||||
}) {
 | 
					}) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,8 @@
 | 
				
			||||||
import clsx from 'clsx'
 | 
					import clsx from 'clsx'
 | 
				
			||||||
 | 
					import { ReactNode } from 'react'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function Row(props: {
 | 
					export function Row(props: {
 | 
				
			||||||
  children?: any
 | 
					  children?: ReactNode
 | 
				
			||||||
  className?: string
 | 
					  className?: string
 | 
				
			||||||
  id?: string
 | 
					  id?: string
 | 
				
			||||||
}) {
 | 
					}) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,12 +1,12 @@
 | 
				
			||||||
import clsx from 'clsx'
 | 
					import clsx from 'clsx'
 | 
				
			||||||
import Link from 'next/link'
 | 
					import Link from 'next/link'
 | 
				
			||||||
import { useState } from 'react'
 | 
					import { ReactNode, useState } from 'react'
 | 
				
			||||||
import { Row } from './row'
 | 
					import { Row } from './row'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Tab = {
 | 
					type Tab = {
 | 
				
			||||||
  title: string
 | 
					  title: string
 | 
				
			||||||
  tabIcon?: JSX.Element
 | 
					  tabIcon?: ReactNode
 | 
				
			||||||
  content: JSX.Element
 | 
					  content: ReactNode
 | 
				
			||||||
  // If set, change the url to this href when the tab is selected
 | 
					  // If set, change the url to this href when the tab is selected
 | 
				
			||||||
  href?: string
 | 
					  href?: string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,14 +4,14 @@ import { SiteLink } from './site-link'
 | 
				
			||||||
// Return a JSX span, linkifying @username, #hashtags, and https://...
 | 
					// Return a JSX span, linkifying @username, #hashtags, and https://...
 | 
				
			||||||
// TODO: Use a markdown parser instead of rolling our own here.
 | 
					// TODO: Use a markdown parser instead of rolling our own here.
 | 
				
			||||||
export function Linkify(props: { text: string; gray?: boolean }) {
 | 
					export function Linkify(props: { text: string; gray?: boolean }) {
 | 
				
			||||||
  let { text, gray } = props
 | 
					  const { text, gray } = props
 | 
				
			||||||
  // Replace "m1234" with "ϻ1234"
 | 
					  // Replace "m1234" with "ϻ1234"
 | 
				
			||||||
  // const mRegex = /(\W|^)m(\d+)/g
 | 
					  // const mRegex = /(\W|^)m(\d+)/g
 | 
				
			||||||
  // text = text.replace(mRegex, (_, pre, num) => `${pre}ϻ${num}`)
 | 
					  // text = text.replace(mRegex, (_, pre, num) => `${pre}ϻ${num}`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Find instances of @username, #hashtag, and https://...
 | 
					  // Find instances of @username, #hashtag, and https://...
 | 
				
			||||||
  const regex =
 | 
					  const regex =
 | 
				
			||||||
    /(?:^|\s)(?:[@#][a-z0-9_]+|https?:\/\/[-A-Za-z0-9+&@#\/%?=~_()|!:,.;]*[-A-Za-z0-9+&@#\/%=~_|])/gi
 | 
					    /(?:^|\s)(?:[@#][a-z0-9_]+|https?:\/\/[-A-Za-z0-9+&@#/%?=~_()|!:,.;]*[-A-Za-z0-9+&@#/%=~_|])/gi
 | 
				
			||||||
  const matches = text.match(regex) || []
 | 
					  const matches = text.match(regex) || []
 | 
				
			||||||
  const links = matches.map((match) => {
 | 
					  const links = matches.map((match) => {
 | 
				
			||||||
    // Matches are in the form: " @username" or "https://example.com"
 | 
					    // Matches are in the form: " @username" or "https://example.com"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -17,7 +17,7 @@ import { Avatar } from '../avatar'
 | 
				
			||||||
import clsx from 'clsx'
 | 
					import clsx from 'clsx'
 | 
				
			||||||
import { useRouter } from 'next/router'
 | 
					import { useRouter } from 'next/router'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getNavigation(username: String) {
 | 
					function getNavigation(username: string) {
 | 
				
			||||||
  return [
 | 
					  return [
 | 
				
			||||||
    { name: 'Home', href: '/home', icon: HomeIcon },
 | 
					    { name: 'Home', href: '/home', icon: HomeIcon },
 | 
				
			||||||
    { name: 'Activity', href: '/activity', icon: ChatAltIcon },
 | 
					    { name: 'Activity', href: '/activity', icon: ChatAltIcon },
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -25,7 +25,7 @@ import {
 | 
				
			||||||
  useHasCreatedContractToday,
 | 
					  useHasCreatedContractToday,
 | 
				
			||||||
} from 'web/hooks/use-has-created-contract-today'
 | 
					} from 'web/hooks/use-has-created-contract-today'
 | 
				
			||||||
import { Row } from '../layout/row'
 | 
					import { Row } from '../layout/row'
 | 
				
			||||||
import { useEffect, useState } from 'react'
 | 
					import React, { useEffect, useState } from 'react'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Create an icon from the url of an image
 | 
					// Create an icon from the url of an image
 | 
				
			||||||
function IconFromUrl(url: string): React.ComponentType<{ className?: string }> {
 | 
					function IconFromUrl(url: string): React.ComponentType<{ className?: string }> {
 | 
				
			||||||
| 
						 | 
					@ -130,7 +130,7 @@ export default function Sidebar(props: { className?: string }) {
 | 
				
			||||||
    const nextUtcResetTime = getUtcFreeMarketResetTime(false)
 | 
					    const nextUtcResetTime = getUtcFreeMarketResetTime(false)
 | 
				
			||||||
    const interval = setInterval(() => {
 | 
					    const interval = setInterval(() => {
 | 
				
			||||||
      const now = new Date().getTime()
 | 
					      const now = new Date().getTime()
 | 
				
			||||||
      let timeUntil = nextUtcResetTime - now
 | 
					      const timeUntil = nextUtcResetTime - now
 | 
				
			||||||
      const hoursUntil = timeUntil / 1000 / 60 / 60
 | 
					      const hoursUntil = timeUntil / 1000 / 60 / 60
 | 
				
			||||||
      const minutesUntil = Math.floor((hoursUntil * 60) % 60)
 | 
					      const minutesUntil = Math.floor((hoursUntil * 60) % 60)
 | 
				
			||||||
      const secondsUntil = Math.floor((hoursUntil * 60 * 60) % 60)
 | 
					      const secondsUntil = Math.floor((hoursUntil * 60 * 60) % 60)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,7 @@
 | 
				
			||||||
import clsx from 'clsx'
 | 
					import clsx from 'clsx'
 | 
				
			||||||
 | 
					import { ReactNode } from 'react'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import React from 'react'
 | 
				
			||||||
import { Col } from './layout/col'
 | 
					import { Col } from './layout/col'
 | 
				
			||||||
import { Spacer } from './layout/spacer'
 | 
					import { Spacer } from './layout/spacer'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,7 +15,7 @@ export function NumberInput(props: {
 | 
				
			||||||
  inputClassName?: string
 | 
					  inputClassName?: string
 | 
				
			||||||
  // Needed to focus the amount input
 | 
					  // Needed to focus the amount input
 | 
				
			||||||
  inputRef?: React.MutableRefObject<any>
 | 
					  inputRef?: React.MutableRefObject<any>
 | 
				
			||||||
  children?: any
 | 
					  children?: ReactNode
 | 
				
			||||||
}) {
 | 
					}) {
 | 
				
			||||||
  const {
 | 
					  const {
 | 
				
			||||||
    numberString,
 | 
					    numberString,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -102,12 +102,10 @@ function NumericBuyPanel(props: {
 | 
				
			||||||
  const betDisabled = isSubmitting || !betAmount || !bucketChoice || error
 | 
					  const betDisabled = isSubmitting || !betAmount || !bucketChoice || error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const { newBet, newPool, newTotalShares, newTotalBets } = getNumericBetsInfo(
 | 
					  const { newBet, newPool, newTotalShares, newTotalBets } = getNumericBetsInfo(
 | 
				
			||||||
    { id: 'dummy', balance: 0 } as User, // a little hackish
 | 
					 | 
				
			||||||
    value ?? 0,
 | 
					    value ?? 0,
 | 
				
			||||||
    bucketChoice ?? 'NaN',
 | 
					    bucketChoice ?? 'NaN',
 | 
				
			||||||
    betAmount ?? 0,
 | 
					    betAmount ?? 0,
 | 
				
			||||||
    contract,
 | 
					    contract
 | 
				
			||||||
    'dummy id'
 | 
					 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const { probAfter: outcomeProb, shares } = newBet
 | 
					  const { probAfter: outcomeProb, shares } = newBet
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,5 @@
 | 
				
			||||||
import clsx from 'clsx'
 | 
					import clsx from 'clsx'
 | 
				
			||||||
 | 
					import { ReactNode } from 'react'
 | 
				
			||||||
import { Answer } from 'common/answer'
 | 
					import { Answer } from 'common/answer'
 | 
				
			||||||
import { getProbability } from 'common/calculate'
 | 
					import { getProbability } from 'common/calculate'
 | 
				
			||||||
import { getValueFromBucket } from 'common/calculate-dpm'
 | 
					import { getValueFromBucket } from 'common/calculate-dpm'
 | 
				
			||||||
| 
						 | 
					@ -157,7 +158,7 @@ export function AnswerLabel(props: {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function FreeResponseAnswerToolTip(props: {
 | 
					function FreeResponseAnswerToolTip(props: {
 | 
				
			||||||
  text: string
 | 
					  text: string
 | 
				
			||||||
  children?: React.ReactNode
 | 
					  children?: ReactNode
 | 
				
			||||||
}) {
 | 
					}) {
 | 
				
			||||||
  const { text } = props
 | 
					  const { text } = props
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,5 @@
 | 
				
			||||||
import clsx from 'clsx'
 | 
					import clsx from 'clsx'
 | 
				
			||||||
 | 
					import { ReactNode } from 'react'
 | 
				
			||||||
import { BottomNavBar } from './nav/nav-bar'
 | 
					import { BottomNavBar } from './nav/nav-bar'
 | 
				
			||||||
import Sidebar from './nav/sidebar'
 | 
					import Sidebar from './nav/sidebar'
 | 
				
			||||||
import { Toaster } from 'react-hot-toast'
 | 
					import { Toaster } from 'react-hot-toast'
 | 
				
			||||||
| 
						 | 
					@ -6,9 +7,9 @@ import { Toaster } from 'react-hot-toast'
 | 
				
			||||||
export function Page(props: {
 | 
					export function Page(props: {
 | 
				
			||||||
  margin?: boolean
 | 
					  margin?: boolean
 | 
				
			||||||
  assertUser?: 'signed-in' | 'signed-out'
 | 
					  assertUser?: 'signed-in' | 'signed-out'
 | 
				
			||||||
  rightSidebar?: React.ReactNode
 | 
					  rightSidebar?: ReactNode
 | 
				
			||||||
  suspend?: boolean
 | 
					  suspend?: boolean
 | 
				
			||||||
  children?: any
 | 
					  children?: ReactNode
 | 
				
			||||||
}) {
 | 
					}) {
 | 
				
			||||||
  const { margin, assertUser, children, rightSidebar, suspend } = props
 | 
					  const { margin, assertUser, children, rightSidebar, suspend } = props
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,9 +1,10 @@
 | 
				
			||||||
import clsx from 'clsx'
 | 
					import clsx from 'clsx'
 | 
				
			||||||
 | 
					import { ReactNode } from 'react'
 | 
				
			||||||
import Link from 'next/link'
 | 
					import Link from 'next/link'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const SiteLink = (props: {
 | 
					export const SiteLink = (props: {
 | 
				
			||||||
  href: string
 | 
					  href: string
 | 
				
			||||||
  children?: any
 | 
					  children?: ReactNode
 | 
				
			||||||
  onClick?: () => void
 | 
					  onClick?: () => void
 | 
				
			||||||
  className?: string
 | 
					  className?: string
 | 
				
			||||||
}) => {
 | 
					}) => {
 | 
				
			||||||
| 
						 | 
					@ -30,7 +31,7 @@ export const SiteLink = (props: {
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function MaybeLink(props: { href: string; children: React.ReactNode }) {
 | 
					function MaybeLink(props: { href: string; children: ReactNode }) {
 | 
				
			||||||
  const { href, children } = props
 | 
					  const { href, children } = props
 | 
				
			||||||
  return href.startsWith('http') ? (
 | 
					  return href.startsWith('http') ? (
 | 
				
			||||||
    <>{children}</>
 | 
					    <>{children}</>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,6 +5,7 @@ import { Contract, updateContract } from 'web/lib/firebase/contracts'
 | 
				
			||||||
import { Col } from './layout/col'
 | 
					import { Col } from './layout/col'
 | 
				
			||||||
import { Row } from './layout/row'
 | 
					import { Row } from './layout/row'
 | 
				
			||||||
import { TagsList } from './tags-list'
 | 
					import { TagsList } from './tags-list'
 | 
				
			||||||
 | 
					import { MAX_TAG_LENGTH } from 'common/contract'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function TagsInput(props: { contract: Contract; className?: string }) {
 | 
					export function TagsInput(props: { contract: Contract; className?: string }) {
 | 
				
			||||||
  const { contract, className } = props
 | 
					  const { contract, className } = props
 | 
				
			||||||
| 
						 | 
					@ -36,6 +37,7 @@ export function TagsInput(props: { contract: Contract; className?: string }) {
 | 
				
			||||||
          className="input input-sm input-bordered resize-none"
 | 
					          className="input input-sm input-bordered resize-none"
 | 
				
			||||||
          disabled={isSubmitting}
 | 
					          disabled={isSubmitting}
 | 
				
			||||||
          value={tagText}
 | 
					          value={tagText}
 | 
				
			||||||
 | 
					          maxLength={MAX_TAG_LENGTH}
 | 
				
			||||||
          onChange={(e) => setTagText(e.target.value || '')}
 | 
					          onChange={(e) => setTagText(e.target.value || '')}
 | 
				
			||||||
          onKeyDown={(e) => {
 | 
					          onKeyDown={(e) => {
 | 
				
			||||||
            if (e.key === 'Enter' && !e.shiftKey) {
 | 
					            if (e.key === 'Enter' && !e.shiftKey) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
import clsx from 'clsx'
 | 
					import clsx from 'clsx'
 | 
				
			||||||
import React from 'react'
 | 
					import React, { ReactNode } from 'react'
 | 
				
			||||||
import { formatMoney } from 'common/util/format'
 | 
					import { formatMoney } from 'common/util/format'
 | 
				
			||||||
import { Col } from './layout/col'
 | 
					import { Col } from './layout/col'
 | 
				
			||||||
import { Row } from './layout/row'
 | 
					import { Row } from './layout/row'
 | 
				
			||||||
| 
						 | 
					@ -231,7 +231,7 @@ function Button(props: {
 | 
				
			||||||
  className?: string
 | 
					  className?: string
 | 
				
			||||||
  onClick?: () => void
 | 
					  onClick?: () => void
 | 
				
			||||||
  color: 'green' | 'red' | 'blue' | 'yellow' | 'gray'
 | 
					  color: 'green' | 'red' | 'blue' | 'yellow' | 'gray'
 | 
				
			||||||
  children?: any
 | 
					  children?: ReactNode
 | 
				
			||||||
}) {
 | 
					}) {
 | 
				
			||||||
  const { className, onClick, children, color } = props
 | 
					  const { className, onClick, children, color } = props
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,10 +6,10 @@ import fetch, { Headers, Response } from 'node-fetch'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getProxiedRequestHeaders(req: NextApiRequest, whitelist: string[]) {
 | 
					function getProxiedRequestHeaders(req: NextApiRequest, whitelist: string[]) {
 | 
				
			||||||
  const result = new Headers()
 | 
					  const result = new Headers()
 | 
				
			||||||
  for (let name of whitelist) {
 | 
					  for (const name of whitelist) {
 | 
				
			||||||
    const v = req.headers[name.toLowerCase()]
 | 
					    const v = req.headers[name.toLowerCase()]
 | 
				
			||||||
    if (Array.isArray(v)) {
 | 
					    if (Array.isArray(v)) {
 | 
				
			||||||
      for (let vv of v) {
 | 
					      for (const vv of v) {
 | 
				
			||||||
        result.append(name, vv)
 | 
					        result.append(name, vv)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    } else if (v != null) {
 | 
					    } else if (v != null) {
 | 
				
			||||||
| 
						 | 
					@ -23,7 +23,7 @@ function getProxiedRequestHeaders(req: NextApiRequest, whitelist: string[]) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getProxiedResponseHeaders(res: Response, whitelist: string[]) {
 | 
					function getProxiedResponseHeaders(res: Response, whitelist: string[]) {
 | 
				
			||||||
  const result: { [k: string]: string } = {}
 | 
					  const result: { [k: string]: string } = {}
 | 
				
			||||||
  for (let name of whitelist) {
 | 
					  for (const name of whitelist) {
 | 
				
			||||||
    const v = res.headers.get(name)
 | 
					    const v = res.headers.get(name)
 | 
				
			||||||
    if (v != null) {
 | 
					    if (v != null) {
 | 
				
			||||||
      result[name] = v
 | 
					      result[name] = v
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,13 +9,15 @@ export const app = getApps().length ? getApp() : initializeApp(FIREBASE_CONFIG)
 | 
				
			||||||
export const db = getFirestore()
 | 
					export const db = getFirestore()
 | 
				
			||||||
export const functions = getFunctions()
 | 
					export const functions = getFunctions()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const EMULATORS_STARTED = 'EMULATORS_STARTED'
 | 
					declare global {
 | 
				
			||||||
 | 
					  /* eslint-disable-next-line no-var */
 | 
				
			||||||
 | 
					  var EMULATORS_STARTED: boolean
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function startEmulators() {
 | 
					function startEmulators() {
 | 
				
			||||||
  // I don't like this but this is the only way to reconnect to the emulators without error, see: https://stackoverflow.com/questions/65066963/firebase-firestore-emulator-error-host-has-been-set-in-both-settings-and-usee
 | 
					  // I don't like this but this is the only way to reconnect to the emulators without error, see: https://stackoverflow.com/questions/65066963/firebase-firestore-emulator-error-host-has-been-set-in-both-settings-and-usee
 | 
				
			||||||
  // @ts-ignore
 | 
					  if (!global.EMULATORS_STARTED) {
 | 
				
			||||||
  if (!global[EMULATORS_STARTED]) {
 | 
					    global.EMULATORS_STARTED = true
 | 
				
			||||||
    // @ts-ignore
 | 
					 | 
				
			||||||
    global[EMULATORS_STARTED] = true
 | 
					 | 
				
			||||||
    connectFirestoreEmulator(db, 'localhost', 8080)
 | 
					    connectFirestoreEmulator(db, 'localhost', 8080)
 | 
				
			||||||
    connectFunctionsEmulator(functions, 'localhost', 5001)
 | 
					    connectFunctionsEmulator(functions, 'localhost', 5001)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -21,7 +21,7 @@ export function copyToClipboard(text: string) {
 | 
				
			||||||
    document.queryCommandSupported('copy')
 | 
					    document.queryCommandSupported('copy')
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    console.log('copy 3')
 | 
					    console.log('copy 3')
 | 
				
			||||||
    var textarea = document.createElement('textarea')
 | 
					    const textarea = document.createElement('textarea')
 | 
				
			||||||
    textarea.textContent = text
 | 
					    textarea.textContent = text
 | 
				
			||||||
    textarea.style.position = 'fixed' // Prevent scrolling to bottom of page in Microsoft Edge.
 | 
					    textarea.style.position = 'fixed' // Prevent scrolling to bottom of page in Microsoft Edge.
 | 
				
			||||||
    document.body.appendChild(textarea)
 | 
					    document.body.appendChild(textarea)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,12 +13,12 @@ function firstLine(msg: string) {
 | 
				
			||||||
function printBuildInfo() {
 | 
					function printBuildInfo() {
 | 
				
			||||||
  // These are undefined if e.g. dev server
 | 
					  // These are undefined if e.g. dev server
 | 
				
			||||||
  if (process.env.NEXT_PUBLIC_VERCEL_ENV) {
 | 
					  if (process.env.NEXT_PUBLIC_VERCEL_ENV) {
 | 
				
			||||||
    let env = process.env.NEXT_PUBLIC_VERCEL_ENV
 | 
					    const env = process.env.NEXT_PUBLIC_VERCEL_ENV
 | 
				
			||||||
    let msg = process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_MESSAGE
 | 
					    const msg = process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_MESSAGE
 | 
				
			||||||
    let owner = process.env.NEXT_PUBLIC_VERCEL_GIT_REPO_OWNER
 | 
					    const owner = process.env.NEXT_PUBLIC_VERCEL_GIT_REPO_OWNER
 | 
				
			||||||
    let repo = process.env.NEXT_PUBLIC_VERCEL_GIT_REPO_SLUG
 | 
					    const repo = process.env.NEXT_PUBLIC_VERCEL_GIT_REPO_SLUG
 | 
				
			||||||
    let sha = process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA
 | 
					    const sha = process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA
 | 
				
			||||||
    let url = `https://github.com/${owner}/${repo}/commit/${sha}`
 | 
					    const url = `https://github.com/${owner}/${repo}/commit/${sha}`
 | 
				
			||||||
    console.info(`Build: ${env} / ${firstLine(msg || '???')} / ${url}`)
 | 
					    console.info(`Build: ${env} / ${firstLine(msg || '???')} / ${url}`)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,28 +19,22 @@ function avatarHtml(avatarUrl: string) {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function UsersTable() {
 | 
					function UsersTable() {
 | 
				
			||||||
  let users = useUsers()
 | 
					  const users = useUsers()
 | 
				
			||||||
  let privateUsers = usePrivateUsers()
 | 
					  const privateUsers = usePrivateUsers()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Map private users by user id
 | 
					  // Map private users by user id
 | 
				
			||||||
  const privateUsersById = mapKeys(privateUsers, 'id')
 | 
					  const privateUsersById = mapKeys(privateUsers, 'id')
 | 
				
			||||||
  console.log('private users by id', privateUsersById)
 | 
					  console.log('private users by id', privateUsersById)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // For each user, set their email from the PrivateUser
 | 
					  // For each user, set their email from the PrivateUser
 | 
				
			||||||
  users = users.map((user) => {
 | 
					  const fullUsers = users
 | 
				
			||||||
    // @ts-ignore
 | 
					    .map((user) => {
 | 
				
			||||||
    user.email = privateUsersById[user.id]?.email
 | 
					      return { email: privateUsersById[user.id]?.email, ...user }
 | 
				
			||||||
    return user
 | 
					 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
					    .sort((a, b) => b.createdTime - a.createdTime)
 | 
				
			||||||
  // Sort users by createdTime descending, by default
 | 
					 | 
				
			||||||
  users = users.sort((a, b) => b.createdTime - a.createdTime)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  function exportCsv() {
 | 
					  function exportCsv() {
 | 
				
			||||||
    const csv = users
 | 
					    const csv = fullUsers.map((u) => [u.email, u.name].join(', ')).join('\n')
 | 
				
			||||||
      // @ts-ignore
 | 
					 | 
				
			||||||
      .map((u) => [u.email, u.name].join(', '))
 | 
					 | 
				
			||||||
      .join('\n')
 | 
					 | 
				
			||||||
    const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' })
 | 
					    const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' })
 | 
				
			||||||
    const url = URL.createObjectURL(blob)
 | 
					    const url = URL.createObjectURL(blob)
 | 
				
			||||||
    const a = document.createElement('a')
 | 
					    const a = document.createElement('a')
 | 
				
			||||||
| 
						 | 
					@ -108,13 +102,14 @@ function UsersTable() {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function ContractsTable() {
 | 
					function ContractsTable() {
 | 
				
			||||||
  let contracts = useContracts() ?? []
 | 
					  const contracts = useContracts() ?? []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Sort users by createdTime descending, by default
 | 
					  // Sort users by createdTime descending, by default
 | 
				
			||||||
  contracts.sort((a, b) => b.createdTime - a.createdTime)
 | 
					  const displayContracts = contracts
 | 
				
			||||||
 | 
					    .sort((a, b) => b.createdTime - a.createdTime)
 | 
				
			||||||
 | 
					    .map((contract) => {
 | 
				
			||||||
      // Render a clickable question. See https://gridjs.io/docs/examples/react-cells for docs
 | 
					      // Render a clickable question. See https://gridjs.io/docs/examples/react-cells for docs
 | 
				
			||||||
  contracts.map((contract) => {
 | 
					      const questionLink = r(
 | 
				
			||||||
    // @ts-ignore
 | 
					 | 
				
			||||||
    contract.questionLink = r(
 | 
					 | 
				
			||||||
        <div className="w-60">
 | 
					        <div className="w-60">
 | 
				
			||||||
          <a
 | 
					          <a
 | 
				
			||||||
            className="hover:underline hover:decoration-indigo-400 hover:decoration-2"
 | 
					            className="hover:underline hover:decoration-indigo-400 hover:decoration-2"
 | 
				
			||||||
| 
						 | 
					@ -124,11 +119,12 @@ function ContractsTable() {
 | 
				
			||||||
          </a>
 | 
					          </a>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      )
 | 
					      )
 | 
				
			||||||
 | 
					      return { questionLink, ...contract }
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Grid
 | 
					    <Grid
 | 
				
			||||||
      data={contracts}
 | 
					      data={displayContracts}
 | 
				
			||||||
      columns={[
 | 
					      columns={[
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          id: 'creatorUsername',
 | 
					          id: 'creatorUsername',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,6 @@
 | 
				
			||||||
import { sortBy, sumBy, uniqBy } from 'lodash'
 | 
					import { sortBy, sumBy, uniqBy } from 'lodash'
 | 
				
			||||||
import clsx from 'clsx'
 | 
					import clsx from 'clsx'
 | 
				
			||||||
import { useEffect, useRef, useState } from 'react'
 | 
					import React, { useEffect, useRef, useState } from 'react'
 | 
				
			||||||
import { Col } from 'web/components/layout/col'
 | 
					import { Col } from 'web/components/layout/col'
 | 
				
			||||||
import { Row } from 'web/components/layout/row'
 | 
					import { Row } from 'web/components/layout/row'
 | 
				
			||||||
import { Page } from 'web/components/page'
 | 
					import { Page } from 'web/components/page'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,7 +11,7 @@ import { FIXED_ANTE, MINIMUM_ANTE } from 'common/antes'
 | 
				
			||||||
import { InfoTooltip } from 'web/components/info-tooltip'
 | 
					import { InfoTooltip } from 'web/components/info-tooltip'
 | 
				
			||||||
import { Page } from 'web/components/page'
 | 
					import { Page } from 'web/components/page'
 | 
				
			||||||
import { Row } from 'web/components/layout/row'
 | 
					import { Row } from 'web/components/layout/row'
 | 
				
			||||||
import { MAX_DESCRIPTION_LENGTH, outcomeType, RESOLUTIONS, resolution, resolutionType } from 'common/contract'
 | 
					import { MAX_DESCRIPTION_LENGTH, MAX_QUESTION_LENGTH, outcomeType, resolution, resolutionType } from 'common/contract'
 | 
				
			||||||
import { formatMoney } from 'common/util/format'
 | 
					import { formatMoney } from 'common/util/format'
 | 
				
			||||||
import { useHasCreatedContractToday } from 'web/hooks/use-has-created-contract-today'
 | 
					import { useHasCreatedContractToday } from 'web/hooks/use-has-created-contract-today'
 | 
				
			||||||
import { removeUndefinedProps } from 'common/util/object'
 | 
					import { removeUndefinedProps } from 'common/util/object'
 | 
				
			||||||
| 
						 | 
					@ -37,6 +37,7 @@ export default function Create() {
 | 
				
			||||||
                placeholder="e.g. Will the Democrats win the 2024 US presidential election?"
 | 
					                placeholder="e.g. Will the Democrats win the 2024 US presidential election?"
 | 
				
			||||||
                className="input input-bordered resize-none"
 | 
					                className="input input-bordered resize-none"
 | 
				
			||||||
                autoFocus
 | 
					                autoFocus
 | 
				
			||||||
 | 
					                maxLength={MAX_QUESTION_LENGTH}
 | 
				
			||||||
                value={question}
 | 
					                value={question}
 | 
				
			||||||
                onChange={(e) => setQuestion(e.target.value || '')}
 | 
					                onChange={(e) => setQuestion(e.target.value || '')}
 | 
				
			||||||
              />
 | 
					              />
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -63,7 +63,7 @@ export default function Folds(props: {
 | 
				
			||||||
  const [query, setQuery] = useState('')
 | 
					  const [query, setQuery] = useState('')
 | 
				
			||||||
  // Copied from contracts-list.tsx; extract if we copy this again
 | 
					  // Copied from contracts-list.tsx; extract if we copy this again
 | 
				
			||||||
  const queryWords = query.toLowerCase().split(' ')
 | 
					  const queryWords = query.toLowerCase().split(' ')
 | 
				
			||||||
  function check(corpus: String) {
 | 
					  function check(corpus: string) {
 | 
				
			||||||
    return queryWords.every((word) => corpus.toLowerCase().includes(word))
 | 
					    return queryWords.every((word) => corpus.toLowerCase().includes(word))
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,4 @@
 | 
				
			||||||
import { useEffect, useState } from 'react'
 | 
					import React, { useEffect, useState } from 'react'
 | 
				
			||||||
import { RefreshIcon } from '@heroicons/react/outline'
 | 
					import { RefreshIcon } from '@heroicons/react/outline'
 | 
				
			||||||
import Router from 'next/router'
 | 
					import Router from 'next/router'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -21,7 +21,7 @@ import Textarea from 'react-expanding-textarea'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function EditUserField(props: {
 | 
					function EditUserField(props: {
 | 
				
			||||||
  user: User
 | 
					  user: User
 | 
				
			||||||
  field: 'bio' | 'bannerUrl' | 'twitterHandle' | 'discordHandle'
 | 
					  field: 'bio' | 'website' | 'bannerUrl' | 'twitterHandle' | 'discordHandle'
 | 
				
			||||||
  label: string
 | 
					  label: string
 | 
				
			||||||
}) {
 | 
					}) {
 | 
				
			||||||
  const { user, field, label } = props
 | 
					  const { user, field, label } = props
 | 
				
			||||||
| 
						 | 
					@ -220,18 +220,15 @@ export default function ProfilePage() {
 | 
				
			||||||
                }}
 | 
					                }}
 | 
				
			||||||
              />
 | 
					              />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              {[
 | 
					              {(
 | 
				
			||||||
 | 
					                [
 | 
				
			||||||
                  ['bio', 'Bio'],
 | 
					                  ['bio', 'Bio'],
 | 
				
			||||||
                  ['website', 'Website URL'],
 | 
					                  ['website', 'Website URL'],
 | 
				
			||||||
                  ['twitterHandle', 'Twitter'],
 | 
					                  ['twitterHandle', 'Twitter'],
 | 
				
			||||||
                  ['discordHandle', 'Discord'],
 | 
					                  ['discordHandle', 'Discord'],
 | 
				
			||||||
              ].map(([field, label]) => (
 | 
					                ] as const
 | 
				
			||||||
                <EditUserField
 | 
					              ).map(([field, label]) => (
 | 
				
			||||||
                  user={user}
 | 
					                <EditUserField user={user} field={field} label={label} />
 | 
				
			||||||
                  // @ts-ignore
 | 
					 | 
				
			||||||
                  field={field}
 | 
					 | 
				
			||||||
                  label={label}
 | 
					 | 
				
			||||||
                />
 | 
					 | 
				
			||||||
              ))}
 | 
					              ))}
 | 
				
			||||||
            </>
 | 
					            </>
 | 
				
			||||||
          )}
 | 
					          )}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -112,7 +112,7 @@ function TableRowEnd(props: { entry: Entry | null; isNew?: boolean }) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function NewBidTable(props: {
 | 
					function NewBidTable(props: {
 | 
				
			||||||
  steps: number
 | 
					  steps: number
 | 
				
			||||||
  bids: any[]
 | 
					  bids: Array<{ yesBid: number; noBid: number }>
 | 
				
			||||||
  setSteps: (steps: number) => void
 | 
					  setSteps: (steps: number) => void
 | 
				
			||||||
  setBids: (bids: any[]) => void
 | 
					  setBids: (bids: any[]) => void
 | 
				
			||||||
}) {
 | 
					}) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5410,3 +5410,8 @@ yocto-queue@^0.1.0:
 | 
				
			||||||
  version "0.1.0"
 | 
					  version "0.1.0"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
 | 
					  resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
 | 
				
			||||||
  integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
 | 
					  integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					zod@3.17.2:
 | 
				
			||||||
 | 
					  version "3.17.2"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/zod/-/zod-3.17.2.tgz#d20b32146a3b5068f8f71768b4f9a4bfe52cddb0"
 | 
				
			||||||
 | 
					  integrity sha512-L8UPS2J/F3dIA8gsPTvGjd8wSRuwR1Td4AqR2Nw8r8BgcLIbZZ5/tCII7hbTLXTQDhxUnnsFdHwpETGajt5i3A==
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue
	
	Block a user