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,146 +1,112 @@
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
import { APIError, newEndpoint, parseCredentials, lookupUser } from './api'
|
import { APIError, newEndpoint, validate } from './api'
|
||||||
import { Contract } from '../../common/contract'
|
import { Contract } from '../../common/contract'
|
||||||
import { User } from '../../common/user'
|
import { User } from '../../common/user'
|
||||||
import {
|
import {
|
||||||
|
BetInfo,
|
||||||
getNewBinaryCpmmBetInfo,
|
getNewBinaryCpmmBetInfo,
|
||||||
getNewBinaryDpmBetInfo,
|
getNewBinaryDpmBetInfo,
|
||||||
getNewMultiBetInfo,
|
getNewMultiBetInfo,
|
||||||
getNumericBetsInfo,
|
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 userDoc = firestore.doc(`users/${bettor.id}`)
|
|
||||||
const userSnap = await transaction.get(userDoc)
|
|
||||||
if (!userSnap.exists) throw new APIError(400, 'User not found')
|
|
||||||
const user = userSnap.data() as User
|
|
||||||
|
|
||||||
const contractDoc = firestore.doc(`contracts/${contractId}`)
|
const result = await firestore.runTransaction(async (trans) => {
|
||||||
const contractSnap = await transaction.get(contractDoc)
|
const userDoc = firestore.doc(`users/${bettor.id}`)
|
||||||
if (!contractSnap.exists) throw new APIError(400, 'Invalid contract')
|
const userSnap = await trans.get(userDoc)
|
||||||
const contract = contractSnap.data() as Contract
|
if (!userSnap.exists) throw new APIError(400, 'User not found.')
|
||||||
|
const user = userSnap.data() as User
|
||||||
|
if (user.balance < amount) throw new APIError(400, 'Insufficient balance.')
|
||||||
|
|
||||||
const { closeTime, outcomeType, mechanism, collectedFees, volume } =
|
const contractDoc = firestore.doc(`contracts/${contractId}`)
|
||||||
contract
|
const contractSnap = await trans.get(contractDoc)
|
||||||
if (closeTime && Date.now() > closeTime)
|
if (!contractSnap.exists) throw new APIError(400, 'Contract not found.')
|
||||||
throw new APIError(400, 'Trading is closed')
|
const contract = contractSnap.data() as Contract
|
||||||
|
|
||||||
const yourBetsSnap = await transaction.get(
|
const loanAmount = 0
|
||||||
contractDoc.collection('bets').where('userId', '==', bettor.id)
|
const { closeTime, outcomeType, mechanism, collectedFees, volume } =
|
||||||
)
|
contract
|
||||||
const yourBets = yourBetsSnap.docs.map((doc) => doc.data() as Bet)
|
if (closeTime && Date.now() > closeTime)
|
||||||
|
throw new APIError(400, 'Trading is closed.')
|
||||||
|
|
||||||
const loanAmount = 0 // getLoanAmount(yourBets, amount)
|
const {
|
||||||
if (user.balance < amount) throw new APIError(400, 'Insufficient balance')
|
newBet,
|
||||||
|
newPool,
|
||||||
if (outcomeType === 'FREE_RESPONSE') {
|
newTotalShares,
|
||||||
const answerSnap = await transaction.get(
|
newTotalBets,
|
||||||
contractDoc.collection('answers').doc(outcome)
|
newTotalLiquidity,
|
||||||
)
|
newP,
|
||||||
if (!answerSnap.exists) throw new APIError(400, 'Invalid contract')
|
} = await (async (): Promise<BetInfo> => {
|
||||||
|
if (outcomeType == 'BINARY' && mechanism == 'dpm-2') {
|
||||||
|
const { outcome } = validate(binarySchema, req.body)
|
||||||
|
return getNewBinaryDpmBetInfo(outcome, amount, contract, loanAmount)
|
||||||
|
} else if (outcomeType == 'BINARY' && mechanism == 'cpmm-1') {
|
||||||
|
const { outcome } = validate(binarySchema, req.body)
|
||||||
|
return getNewBinaryCpmmBetInfo(outcome, amount, contract, loanAmount)
|
||||||
|
} else if (outcomeType == 'FREE_RESPONSE' && mechanism == 'dpm-2') {
|
||||||
|
const { outcome } = validate(freeResponseSchema, req.body)
|
||||||
|
const answerDoc = contractDoc.collection('answers').doc(outcome)
|
||||||
|
const answerSnap = await trans.get(answerDoc)
|
||||||
|
if (!answerSnap.exists) throw new APIError(400, 'Invalid answer')
|
||||||
|
return getNewMultiBetInfo(outcome, amount, contract, loanAmount)
|
||||||
|
} else if (outcomeType == 'NUMERIC' && mechanism == 'dpm-2') {
|
||||||
|
const { outcome, value } = validate(numericSchema, req.body)
|
||||||
|
return getNumericBetsInfo(value, outcome, amount, contract)
|
||||||
|
} else {
|
||||||
|
throw new APIError(500, 'Contract has invalid type/mechanism.')
|
||||||
}
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
const newBetDoc = firestore
|
if (newP != null && !isFinite(newP)) {
|
||||||
.collection(`contracts/${contractId}/bets`)
|
throw new APIError(400, 'Trade rejected due to overflow error.')
|
||||||
.doc()
|
}
|
||||||
|
|
||||||
const {
|
const newBalance = user.balance - amount - loanAmount
|
||||||
newBet,
|
const betDoc = contractDoc.collection('bets').doc()
|
||||||
newPool,
|
trans.create(betDoc, { id: betDoc.id, userId: user.id, ...newBet })
|
||||||
newTotalShares,
|
trans.update(userDoc, { balance: newBalance })
|
||||||
newTotalBets,
|
trans.update(
|
||||||
newBalance,
|
contractDoc,
|
||||||
newTotalLiquidity,
|
removeUndefinedProps({
|
||||||
fees,
|
pool: newPool,
|
||||||
newP,
|
p: newP,
|
||||||
} =
|
totalShares: newTotalShares,
|
||||||
outcomeType === 'BINARY'
|
totalBets: newTotalBets,
|
||||||
? mechanism === 'dpm-2'
|
totalLiquidity: newTotalLiquidity,
|
||||||
? getNewBinaryDpmBetInfo(
|
collectedFees: addObjects(newBet.fees, collectedFees),
|
||||||
user,
|
volume: volume + amount,
|
||||||
outcome as 'YES' | 'NO',
|
})
|
||||||
amount,
|
)
|
||||||
contract,
|
|
||||||
loanAmount,
|
|
||||||
newBetDoc.id
|
|
||||||
)
|
|
||||||
: (getNewBinaryCpmmBetInfo(
|
|
||||||
user,
|
|
||||||
outcome as 'YES' | 'NO',
|
|
||||||
amount,
|
|
||||||
contract,
|
|
||||||
loanAmount,
|
|
||||||
newBetDoc.id
|
|
||||||
) as any)
|
|
||||||
: 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)) {
|
return { betId: betDoc.id }
|
||||||
throw new APIError(400, 'Trade rejected due to overflow error.')
|
})
|
||||||
}
|
|
||||||
|
|
||||||
transaction.create(newBetDoc, newBet)
|
await redeemShares(bettor.id, contractId)
|
||||||
|
return result
|
||||||
transaction.update(
|
|
||||||
contractDoc,
|
|
||||||
removeUndefinedProps({
|
|
||||||
pool: newPool,
|
|
||||||
p: newP,
|
|
||||||
totalShares: newTotalShares,
|
|
||||||
totalBets: newTotalBets,
|
|
||||||
totalLiquidity: newTotalLiquidity,
|
|
||||||
collectedFees: addObjects<Fees>(fees ?? {}, collectedFees ?? {}),
|
|
||||||
volume: volume + Math.abs(amount),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!isFinite(newBalance)) {
|
|
||||||
throw new APIError(500, 'Invalid user balance for ' + user.username)
|
|
||||||
}
|
|
||||||
|
|
||||||
transaction.update(userDoc, { balance: newBalance })
|
|
||||||
|
|
||||||
return { betId: newBetDoc.id }
|
|
||||||
})
|
|
||||||
.then(async (result) => {
|
|
||||||
await redeemShares(bettor.id, contractId)
|
|
||||||
return result
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const firestore = admin.firestore()
|
const firestore = admin.firestore()
|
||||||
|
|
|
@ -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>
|
||||||
<div className="text-blue-400">{resolutionValue}</div>
|
|
||||||
|
{resolution === 'CANCEL' ? (
|
||||||
|
<CancelLabel />
|
||||||
|
) : (
|
||||||
|
<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,12 +230,13 @@ 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 (
|
||||||
|
|
|
@ -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,27 +102,29 @@ 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
|
||||||
// Render a clickable question. See https://gridjs.io/docs/examples/react-cells for docs
|
.sort((a, b) => b.createdTime - a.createdTime)
|
||||||
contracts.map((contract) => {
|
.map((contract) => {
|
||||||
// @ts-ignore
|
// Render a clickable question. See https://gridjs.io/docs/examples/react-cells for docs
|
||||||
contract.questionLink = r(
|
const 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"
|
||||||
href={contractPath(contract)}
|
href={contractPath(contract)}
|
||||||
>
|
>
|
||||||
{contract.question}
|
{contract.question}
|
||||||
</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'],
|
[
|
||||||
['website', 'Website URL'],
|
['bio', 'Bio'],
|
||||||
['twitterHandle', 'Twitter'],
|
['website', 'Website URL'],
|
||||||
['discordHandle', 'Discord'],
|
['twitterHandle', 'Twitter'],
|
||||||
].map(([field, label]) => (
|
['discordHandle', 'Discord'],
|
||||||
<EditUserField
|
] as const
|
||||||
user={user}
|
).map(([field, label]) => (
|
||||||
// @ts-ignore
|
<EditUserField user={user} field={field} label={label} />
|
||||||
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