Implement onRequest
versions of createContract
, placeBet
functions (#227)
* Reimplement createContract and placeBet cloud functions * Fix broken warmup function error handling
This commit is contained in:
parent
aafd2a226f
commit
cd7efb03ca
|
@ -20,6 +20,7 @@
|
||||||
"main": "lib/functions/src/index.js",
|
"main": "lib/functions/src/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-query-firebase/firestore": "0.4.2",
|
"@react-query-firebase/firestore": "0.4.2",
|
||||||
|
"cors": "2.8.5",
|
||||||
"fetch": "1.1.0",
|
"fetch": "1.1.0",
|
||||||
"firebase-admin": "10.0.0",
|
"firebase-admin": "10.0.0",
|
||||||
"firebase-functions": "3.16.0",
|
"firebase-functions": "3.16.0",
|
||||||
|
|
129
functions/src/api.ts
Normal file
129
functions/src/api.ts
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
import * as admin from 'firebase-admin'
|
||||||
|
import * as functions from 'firebase-functions'
|
||||||
|
import * as Cors from 'cors'
|
||||||
|
|
||||||
|
import { User, PrivateUser } from 'common/user'
|
||||||
|
|
||||||
|
type Request = functions.https.Request
|
||||||
|
type Response = functions.Response
|
||||||
|
type Handler = (req: Request, res: Response) => Promise<any>
|
||||||
|
type AuthedUser = [User, PrivateUser]
|
||||||
|
type JwtCredentials = { kind: 'jwt'; data: admin.auth.DecodedIdToken }
|
||||||
|
type KeyCredentials = { kind: 'key'; data: string }
|
||||||
|
type Credentials = JwtCredentials | KeyCredentials
|
||||||
|
|
||||||
|
export class APIError {
|
||||||
|
code: number
|
||||||
|
msg: string
|
||||||
|
constructor(code: number, msg: string) {
|
||||||
|
this.code = code
|
||||||
|
this.msg = msg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const parseCredentials = async (req: Request): Promise<Credentials> => {
|
||||||
|
const authHeader = req.get('Authorization')
|
||||||
|
if (!authHeader) {
|
||||||
|
throw new APIError(403, 'Missing Authorization header.')
|
||||||
|
}
|
||||||
|
const authParts = authHeader.split(' ')
|
||||||
|
if (authParts.length !== 2) {
|
||||||
|
throw new APIError(403, 'Invalid Authorization header.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const [scheme, payload] = authParts
|
||||||
|
switch (scheme) {
|
||||||
|
case 'Bearer':
|
||||||
|
try {
|
||||||
|
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 }
|
||||||
|
} catch (err) {
|
||||||
|
// This is somewhat suspicious, so get it into the firebase console
|
||||||
|
functions.logger.error('Error verifying Firebase JWT: ', err)
|
||||||
|
throw new APIError(403, `Error validating token: ${err}.`)
|
||||||
|
}
|
||||||
|
case 'Key':
|
||||||
|
return { kind: 'key', data: payload }
|
||||||
|
default:
|
||||||
|
throw new APIError(403, 'Invalid auth scheme; must be "Key" or "Bearer".')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const lookupUser = async (creds: Credentials): Promise<AuthedUser> => {
|
||||||
|
const firestore = admin.firestore()
|
||||||
|
const users = firestore.collection('users')
|
||||||
|
const privateUsers = firestore.collection('private-users')
|
||||||
|
switch (creds.kind) {
|
||||||
|
case 'jwt': {
|
||||||
|
const { user_id } = creds.data
|
||||||
|
const [userSnap, privateUserSnap] = await Promise.all([
|
||||||
|
users.doc(user_id).get(),
|
||||||
|
privateUsers.doc(user_id).get(),
|
||||||
|
])
|
||||||
|
if (!userSnap.exists || !privateUserSnap.exists) {
|
||||||
|
throw new APIError(403, 'No user exists with the provided ID.')
|
||||||
|
}
|
||||||
|
const user = userSnap.data() as User
|
||||||
|
const privateUser = privateUserSnap.data() as PrivateUser
|
||||||
|
return [user, privateUser]
|
||||||
|
}
|
||||||
|
case 'key': {
|
||||||
|
const key = creds.data
|
||||||
|
const privateUserQ = await privateUsers.where('apiKey', '==', key).get()
|
||||||
|
if (privateUserQ.empty) {
|
||||||
|
throw new APIError(403, `No private user exists with API key ${key}.`)
|
||||||
|
}
|
||||||
|
const privateUserSnap = privateUserQ.docs[0]
|
||||||
|
const userSnap = await users.doc(privateUserSnap.id).get()
|
||||||
|
if (!userSnap.exists) {
|
||||||
|
throw new APIError(403, `No user exists with ID ${privateUserSnap.id}.`)
|
||||||
|
}
|
||||||
|
const user = userSnap.data() as User
|
||||||
|
const privateUser = privateUserSnap.data() as PrivateUser
|
||||||
|
return [user, privateUser]
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new APIError(500, 'Invalid credential type.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CORS_ORIGIN_MANIFOLD = /^https?:\/\/.+\.manifold\.markets$/
|
||||||
|
export const CORS_ORIGIN_LOCALHOST = /^http:\/\/localhost:\d+$/
|
||||||
|
|
||||||
|
export const applyCors = (req: any, res: any, params: object) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
Cors(params)(req, res, (result) => {
|
||||||
|
if (result instanceof Error) {
|
||||||
|
return reject(result)
|
||||||
|
}
|
||||||
|
return resolve(result)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const newEndpoint = (methods: [string], fn: Handler) =>
|
||||||
|
functions.runWith({ minInstances: 1 }).https.onRequest(async (req, res) => {
|
||||||
|
await applyCors(req, res, {
|
||||||
|
origins: [CORS_ORIGIN_MANIFOLD, CORS_ORIGIN_LOCALHOST],
|
||||||
|
methods: methods,
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
if (!methods.includes(req.method)) {
|
||||||
|
const allowed = methods.join(', ')
|
||||||
|
throw new APIError(405, `This endpoint supports only ${allowed}.`)
|
||||||
|
}
|
||||||
|
const data = await fn(req, res)
|
||||||
|
data.status = 'success'
|
||||||
|
res.status(200).json({ data: data })
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof APIError) {
|
||||||
|
// Emit a 200 anyway here for now, for backwards compatibility
|
||||||
|
res.status(200).json({ data: { status: 'error', message: e.msg } })
|
||||||
|
} else {
|
||||||
|
res.status(500).json({ data: { status: 'error', message: '???' } })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
|
@ -1,8 +1,7 @@
|
||||||
import * as functions from 'firebase-functions'
|
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
import * as _ from 'lodash'
|
|
||||||
|
|
||||||
import { chargeUser, getUser } from './utils'
|
import { chargeUser } from './utils'
|
||||||
|
import { APIError, newEndpoint, parseCredentials, lookupUser } from './api'
|
||||||
import {
|
import {
|
||||||
Binary,
|
Binary,
|
||||||
Contract,
|
Contract,
|
||||||
|
@ -13,7 +12,6 @@ import {
|
||||||
MAX_DESCRIPTION_LENGTH,
|
MAX_DESCRIPTION_LENGTH,
|
||||||
MAX_QUESTION_LENGTH,
|
MAX_QUESTION_LENGTH,
|
||||||
MAX_TAG_LENGTH,
|
MAX_TAG_LENGTH,
|
||||||
outcomeType,
|
|
||||||
} from '../../common/contract'
|
} from '../../common/contract'
|
||||||
import { slugify } from '../../common/util/slugify'
|
import { slugify } from '../../common/util/slugify'
|
||||||
import { randomString } from '../../common/util/random'
|
import { randomString } from '../../common/util/random'
|
||||||
|
@ -28,69 +26,55 @@ import {
|
||||||
} from '../../common/antes'
|
} from '../../common/antes'
|
||||||
import { getNoneAnswer } from '../../common/answer'
|
import { getNoneAnswer } from '../../common/answer'
|
||||||
|
|
||||||
export const createContract = functions
|
export const createContract = newEndpoint(['POST'], async (req, _res) => {
|
||||||
.runWith({ minInstances: 1 })
|
const [creator, _privateUser] = await lookupUser(await parseCredentials(req))
|
||||||
.https.onCall(
|
|
||||||
async (
|
|
||||||
data: {
|
|
||||||
question: string
|
|
||||||
outcomeType: outcomeType
|
|
||||||
description: string
|
|
||||||
initialProb: number
|
|
||||||
ante: number
|
|
||||||
closeTime: number
|
|
||||||
tags?: string[]
|
|
||||||
manaLimitPerUser?: number
|
|
||||||
},
|
|
||||||
context
|
|
||||||
) => {
|
|
||||||
const userId = context?.auth?.uid
|
|
||||||
if (!userId) return { status: 'error', message: 'Not authorized' }
|
|
||||||
|
|
||||||
const creator = await getUser(userId)
|
|
||||||
if (!creator) return { status: 'error', message: 'User not found' }
|
|
||||||
|
|
||||||
let {
|
let {
|
||||||
question,
|
question,
|
||||||
|
outcomeType,
|
||||||
description,
|
description,
|
||||||
initialProb,
|
initialProb,
|
||||||
closeTime,
|
closeTime,
|
||||||
tags,
|
tags,
|
||||||
manaLimitPerUser,
|
manaLimitPerUser,
|
||||||
} = data
|
} = req.body.data || {}
|
||||||
|
|
||||||
if (!question || typeof question != 'string')
|
if (!question || typeof question != 'string')
|
||||||
return { status: 'error', message: 'Missing or invalid question field' }
|
throw new APIError(400, 'Missing or invalid question field')
|
||||||
|
|
||||||
question = question.slice(0, MAX_QUESTION_LENGTH)
|
question = question.slice(0, MAX_QUESTION_LENGTH)
|
||||||
|
|
||||||
if (typeof description !== 'string')
|
if (typeof description !== 'string')
|
||||||
return { status: 'error', message: 'Invalid description field' }
|
throw new APIError(400, 'Invalid description field')
|
||||||
|
|
||||||
description = description.slice(0, MAX_DESCRIPTION_LENGTH)
|
description = description.slice(0, MAX_DESCRIPTION_LENGTH)
|
||||||
|
|
||||||
if (tags !== undefined && !_.isArray(tags))
|
if (tags !== undefined && !Array.isArray(tags))
|
||||||
return { status: 'error', message: 'Invalid tags field' }
|
throw new APIError(400, 'Invalid tags field')
|
||||||
tags = tags?.map((tag) => tag.toString().slice(0, MAX_TAG_LENGTH))
|
|
||||||
|
|
||||||
let outcomeType = data.outcomeType ?? 'BINARY'
|
tags = (tags || []).map((tag: string) =>
|
||||||
|
tag.toString().slice(0, MAX_TAG_LENGTH)
|
||||||
|
)
|
||||||
|
|
||||||
|
outcomeType = outcomeType ?? 'BINARY'
|
||||||
if (!['BINARY', 'MULTI', 'FREE_RESPONSE'].includes(outcomeType))
|
if (!['BINARY', 'MULTI', 'FREE_RESPONSE'].includes(outcomeType))
|
||||||
return { status: 'error', message: 'Invalid outcomeType' }
|
throw new APIError(400, 'Invalid outcomeType')
|
||||||
|
|
||||||
if (
|
if (
|
||||||
outcomeType === 'BINARY' &&
|
outcomeType === 'BINARY' &&
|
||||||
(!initialProb || initialProb < 1 || initialProb > 99)
|
(!initialProb || initialProb < 1 || initialProb > 99)
|
||||||
)
|
)
|
||||||
return { status: 'error', message: 'Invalid initial probability' }
|
throw new APIError(400, 'Invalid initial probability')
|
||||||
|
|
||||||
// uses utc time on server:
|
// uses utc time on server:
|
||||||
const today = new Date().setHours(0, 0, 0, 0)
|
const today = new Date().setHours(0, 0, 0, 0)
|
||||||
const userContractsCreatedTodaySnapshot = await firestore
|
const userContractsCreatedTodaySnapshot = await firestore
|
||||||
.collection(`contracts`)
|
.collection(`contracts`)
|
||||||
.where('creatorId', '==', userId)
|
.where('creatorId', '==', creator.id)
|
||||||
.where('createdTime', '>=', today)
|
.where('createdTime', '>=', today)
|
||||||
.get()
|
.get()
|
||||||
const isFree = userContractsCreatedTodaySnapshot.size === 0
|
const isFree = userContractsCreatedTodaySnapshot.size === 0
|
||||||
|
|
||||||
const ante = FIXED_ANTE // data.ante
|
const ante = FIXED_ANTE
|
||||||
|
|
||||||
if (
|
if (
|
||||||
ante === undefined ||
|
ante === undefined ||
|
||||||
|
@ -99,7 +83,7 @@ export const createContract = functions
|
||||||
isNaN(ante) ||
|
isNaN(ante) ||
|
||||||
!isFinite(ante)
|
!isFinite(ante)
|
||||||
)
|
)
|
||||||
return { status: 'error', message: 'Invalid ante' }
|
throw new APIError(400, 'Invalid ante')
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
'creating contract for',
|
'creating contract for',
|
||||||
|
@ -187,9 +171,8 @@ export const createContract = functions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { status: 'success', contract }
|
return { contract: contract }
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
const getSlug = async (question: string) => {
|
const getSlug = async (question: string) => {
|
||||||
const proposedSlug = slugify(question)
|
const proposedSlug = slugify(question)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import * as functions from 'firebase-functions'
|
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
|
|
||||||
|
import { APIError, newEndpoint, parseCredentials, lookupUser } from './api'
|
||||||
import { Contract } from '../../common/contract'
|
import { Contract } from '../../common/contract'
|
||||||
import { User } from '../../common/user'
|
import { User } from '../../common/user'
|
||||||
import {
|
import {
|
||||||
|
@ -14,68 +14,54 @@ import { redeemShares } from './redeem-shares'
|
||||||
import { Fees } from '../../common/fees'
|
import { Fees } from '../../common/fees'
|
||||||
import { hasUserHitManaLimit } from '../../common/calculate'
|
import { hasUserHitManaLimit } from '../../common/calculate'
|
||||||
|
|
||||||
export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
|
export const placeBet = newEndpoint(['POST'], async (req, _res) => {
|
||||||
async (
|
const [bettor, _privateUser] = await lookupUser(await parseCredentials(req))
|
||||||
data: {
|
const { amount, outcome, contractId } = req.body.data || {}
|
||||||
amount: number
|
|
||||||
outcome: string
|
|
||||||
contractId: string
|
|
||||||
},
|
|
||||||
context
|
|
||||||
) => {
|
|
||||||
const userId = context?.auth?.uid
|
|
||||||
if (!userId) return { status: 'error', message: 'Not authorized' }
|
|
||||||
|
|
||||||
const { amount, outcome, contractId } = data
|
|
||||||
|
|
||||||
if (amount <= 0 || isNaN(amount) || !isFinite(amount))
|
if (amount <= 0 || isNaN(amount) || !isFinite(amount))
|
||||||
return { status: 'error', message: 'Invalid amount' }
|
throw new APIError(400, 'Invalid amount')
|
||||||
|
|
||||||
if (outcome !== 'YES' && outcome !== 'NO' && isNaN(+outcome))
|
if (outcome !== 'YES' && outcome !== 'NO' && isNaN(+outcome))
|
||||||
return { status: 'error', message: 'Invalid outcome' }
|
throw new APIError(400, 'Invalid outcome')
|
||||||
|
|
||||||
// run as transaction to prevent race conditions
|
// run as transaction to prevent race conditions
|
||||||
return await firestore
|
return await firestore
|
||||||
.runTransaction(async (transaction) => {
|
.runTransaction(async (transaction) => {
|
||||||
const userDoc = firestore.doc(`users/${userId}`)
|
const userDoc = firestore.doc(`users/${bettor.id}`)
|
||||||
const userSnap = await transaction.get(userDoc)
|
const userSnap = await transaction.get(userDoc)
|
||||||
if (!userSnap.exists)
|
if (!userSnap.exists) throw new APIError(400, 'User not found')
|
||||||
return { status: 'error', message: 'User not found' }
|
|
||||||
const user = userSnap.data() as User
|
const user = userSnap.data() as User
|
||||||
|
|
||||||
const contractDoc = firestore.doc(`contracts/${contractId}`)
|
const contractDoc = firestore.doc(`contracts/${contractId}`)
|
||||||
const contractSnap = await transaction.get(contractDoc)
|
const contractSnap = await transaction.get(contractDoc)
|
||||||
if (!contractSnap.exists)
|
if (!contractSnap.exists) throw new APIError(400, 'Invalid contract')
|
||||||
return { status: 'error', message: 'Invalid contract' }
|
|
||||||
const contract = contractSnap.data() as Contract
|
const contract = contractSnap.data() as Contract
|
||||||
|
|
||||||
const { closeTime, outcomeType, mechanism, collectedFees, volume } =
|
const { closeTime, outcomeType, mechanism, collectedFees, volume } =
|
||||||
contract
|
contract
|
||||||
if (closeTime && Date.now() > closeTime)
|
if (closeTime && Date.now() > closeTime)
|
||||||
return { status: 'error', message: 'Trading is closed' }
|
throw new APIError(400, 'Trading is closed')
|
||||||
|
|
||||||
const yourBetsSnap = await transaction.get(
|
const yourBetsSnap = await transaction.get(
|
||||||
contractDoc.collection('bets').where('userId', '==', userId)
|
contractDoc.collection('bets').where('userId', '==', bettor.id)
|
||||||
)
|
)
|
||||||
const yourBets = yourBetsSnap.docs.map((doc) => doc.data() as Bet)
|
const yourBets = yourBetsSnap.docs.map((doc) => doc.data() as Bet)
|
||||||
|
|
||||||
const loanAmount = 0 // getLoanAmount(yourBets, amount)
|
const loanAmount = 0 // getLoanAmount(yourBets, amount)
|
||||||
if (user.balance < amount)
|
if (user.balance < amount) throw new APIError(400, 'Insufficient balance')
|
||||||
return { status: 'error', message: 'Insufficient balance' }
|
|
||||||
|
|
||||||
if (outcomeType === 'FREE_RESPONSE') {
|
if (outcomeType === 'FREE_RESPONSE') {
|
||||||
const answerSnap = await transaction.get(
|
const answerSnap = await transaction.get(
|
||||||
contractDoc.collection('answers').doc(outcome)
|
contractDoc.collection('answers').doc(outcome)
|
||||||
)
|
)
|
||||||
if (!answerSnap.exists)
|
if (!answerSnap.exists) throw new APIError(400, 'Invalid contract')
|
||||||
return { status: 'error', message: 'Invalid contract' }
|
|
||||||
|
|
||||||
const { status, message } = hasUserHitManaLimit(
|
const { status, message } = hasUserHitManaLimit(
|
||||||
contract,
|
contract,
|
||||||
yourBets,
|
yourBets,
|
||||||
amount
|
amount
|
||||||
)
|
)
|
||||||
if (status === 'error') return { status, message: message }
|
if (status === 'error') throw new APIError(400, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
const newBetDoc = firestore
|
const newBetDoc = firestore
|
||||||
|
@ -120,10 +106,7 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
)
|
)
|
||||||
|
|
||||||
if (newP !== undefined && !isFinite(newP)) {
|
if (newP !== undefined && !isFinite(newP)) {
|
||||||
return {
|
throw new APIError(400, 'Trade rejected due to overflow error.')
|
||||||
status: 'error',
|
|
||||||
message: 'Trade rejected due to overflow error.',
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction.create(newBetDoc, newBet)
|
transaction.create(newBetDoc, newBet)
|
||||||
|
@ -142,18 +125,17 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!isFinite(newBalance)) {
|
if (!isFinite(newBalance)) {
|
||||||
throw new Error('Invalid user balance for ' + user.username)
|
throw new APIError(500, 'Invalid user balance for ' + user.username)
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction.update(userDoc, { balance: newBalance })
|
transaction.update(userDoc, { balance: newBalance })
|
||||||
|
|
||||||
return { status: 'success', betId: newBetDoc.id }
|
return { betId: newBetDoc.id }
|
||||||
})
|
})
|
||||||
.then(async (result) => {
|
.then(async (result) => {
|
||||||
await redeemShares(userId, contractId)
|
await redeemShares(bettor.id, contractId)
|
||||||
return result
|
return result
|
||||||
})
|
})
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
const firestore = admin.firestore()
|
const firestore = admin.firestore()
|
||||||
|
|
|
@ -214,7 +214,7 @@ function BuyPanel(props: {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// warm up cloud function
|
// warm up cloud function
|
||||||
placeBet({}).catch()
|
placeBet({}).catch(() => {})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -553,7 +553,10 @@ function BetRow(props: { bet: Bet; contract: Contract; saleBet?: Bet }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const warmUpSellBet = _.throttle(() => sellBet({}).catch(), 5000 /* ms */)
|
const warmUpSellBet = _.throttle(
|
||||||
|
() => sellBet({}).catch(() => {}),
|
||||||
|
5000 /* ms */
|
||||||
|
)
|
||||||
|
|
||||||
function SellButton(props: { contract: Contract; bet: Bet }) {
|
function SellButton(props: { contract: Contract; bet: Bet }) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -20,7 +20,7 @@ export function ResolutionPanel(props: {
|
||||||
}) {
|
}) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// warm up cloud function
|
// warm up cloud function
|
||||||
resolveMarket({} as any).catch()
|
resolveMarket({} as any).catch(() => {})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const { contract, className } = props
|
const { contract, className } = props
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
"@nivo/line": "0.74.0",
|
"@nivo/line": "0.74.0",
|
||||||
"algoliasearch": "4.13.0",
|
"algoliasearch": "4.13.0",
|
||||||
"clsx": "1.1.1",
|
"clsx": "1.1.1",
|
||||||
"cors": "^2.8.5",
|
"cors": "2.8.5",
|
||||||
"daisyui": "1.16.4",
|
"daisyui": "1.16.4",
|
||||||
"dayjs": "1.10.7",
|
"dayjs": "1.10.7",
|
||||||
"firebase": "9.6.0",
|
"firebase": "9.6.0",
|
||||||
|
|
|
@ -61,7 +61,7 @@ export function NewContract(props: { question: string; tag?: string }) {
|
||||||
}, [creator])
|
}, [creator])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
createContract({}).catch() // warm up function
|
createContract({}).catch(() => {}) // warm up function
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const [outcomeType, setOutcomeType] = useState<outcomeType>('BINARY')
|
const [outcomeType, setOutcomeType] = useState<outcomeType>('BINARY')
|
||||||
|
|
|
@ -1874,7 +1874,7 @@ core-util-is@~1.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
|
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
|
||||||
integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
|
integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
|
||||||
|
|
||||||
cors@^2.8.5:
|
cors@2.8.5, cors@^2.8.5:
|
||||||
version "2.8.5"
|
version "2.8.5"
|
||||||
resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29"
|
resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29"
|
||||||
integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==
|
integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==
|
||||||
|
|
Loading…
Reference in New Issue
Block a user