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:
Milli 2022-05-28 00:41:38 +02:00
commit 50cee1a6d3
56 changed files with 408 additions and 447 deletions

View File

@ -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'],
}, },

View File

@ -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

View File

@ -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) => {

View File

@ -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

View File

@ -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',

View File

@ -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'],

View File

@ -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",

View File

@ -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.' })
} }
} }

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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

View 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.`)
})
}

View File

@ -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),
}) })
) )

View File

@ -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),
}) })
) )

View File

@ -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

View File

@ -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,
},
} }

View File

@ -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

View File

@ -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)

View File

@ -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'

View File

@ -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}`)
} }

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -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>
)}
</> </>
) : ( ) : (
<> <>

View File

@ -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>

View File

@ -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 (

View File

@ -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'

View File

@ -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(

View File

@ -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}`

View File

@ -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 (
<> <>

View File

@ -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>

View File

@ -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
}) { }) {

View File

@ -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
}) { }) {

View File

@ -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
} }

View File

@ -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"

View File

@ -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 },

View File

@ -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)

View File

@ -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,

View File

@ -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

View File

@ -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 (

View File

@ -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

View File

@ -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}</>

View File

@ -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) {

View File

@ -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

View File

@ -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

View File

@ -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)
} }

View File

@ -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)

View File

@ -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}`)
} }
} }

View File

@ -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',

View File

@ -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'

View File

@ -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 || '')}
/> />

View File

@ -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))
} }

View File

@ -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}
/>
))} ))}
</> </>
)} )}

View File

@ -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
}) { }) {

View File

@ -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==