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",
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@react-query-firebase/firestore": "0.4.2",
 | 
			
		||||
    "cors": "2.8.5",
 | 
			
		||||
    "fetch": "1.1.0",
 | 
			
		||||
    "firebase-admin": "10.0.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 _ from 'lodash'
 | 
			
		||||
 | 
			
		||||
import { chargeUser, getUser } from './utils'
 | 
			
		||||
import { chargeUser } from './utils'
 | 
			
		||||
import { APIError, newEndpoint, parseCredentials, lookupUser } from './api'
 | 
			
		||||
import {
 | 
			
		||||
  Binary,
 | 
			
		||||
  Contract,
 | 
			
		||||
| 
						 | 
				
			
			@ -13,7 +12,6 @@ import {
 | 
			
		|||
  MAX_DESCRIPTION_LENGTH,
 | 
			
		||||
  MAX_QUESTION_LENGTH,
 | 
			
		||||
  MAX_TAG_LENGTH,
 | 
			
		||||
  outcomeType,
 | 
			
		||||
} from '../../common/contract'
 | 
			
		||||
import { slugify } from '../../common/util/slugify'
 | 
			
		||||
import { randomString } from '../../common/util/random'
 | 
			
		||||
| 
						 | 
				
			
			@ -28,69 +26,55 @@ import {
 | 
			
		|||
} from '../../common/antes'
 | 
			
		||||
import { getNoneAnswer } from '../../common/answer'
 | 
			
		||||
 | 
			
		||||
export const createContract = functions
 | 
			
		||||
  .runWith({ minInstances: 1 })
 | 
			
		||||
  .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' }
 | 
			
		||||
 | 
			
		||||
export const createContract = newEndpoint(['POST'], async (req, _res) => {
 | 
			
		||||
  const [creator, _privateUser] = await lookupUser(await parseCredentials(req))
 | 
			
		||||
  let {
 | 
			
		||||
    question,
 | 
			
		||||
    outcomeType,
 | 
			
		||||
    description,
 | 
			
		||||
    initialProb,
 | 
			
		||||
    closeTime,
 | 
			
		||||
    tags,
 | 
			
		||||
    manaLimitPerUser,
 | 
			
		||||
      } = data
 | 
			
		||||
  } = req.body.data || {}
 | 
			
		||||
 | 
			
		||||
  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)
 | 
			
		||||
 | 
			
		||||
  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)
 | 
			
		||||
 | 
			
		||||
      if (tags !== undefined && !_.isArray(tags))
 | 
			
		||||
        return { status: 'error', message: 'Invalid tags field' }
 | 
			
		||||
      tags = tags?.map((tag) => tag.toString().slice(0, MAX_TAG_LENGTH))
 | 
			
		||||
  if (tags !== undefined && !Array.isArray(tags))
 | 
			
		||||
    throw new APIError(400, 'Invalid tags field')
 | 
			
		||||
 | 
			
		||||
      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))
 | 
			
		||||
        return { status: 'error', message: 'Invalid outcomeType' }
 | 
			
		||||
    throw new APIError(400, 'Invalid outcomeType')
 | 
			
		||||
 | 
			
		||||
  if (
 | 
			
		||||
    outcomeType === 'BINARY' &&
 | 
			
		||||
    (!initialProb || initialProb < 1 || initialProb > 99)
 | 
			
		||||
  )
 | 
			
		||||
        return { status: 'error', message: 'Invalid initial probability' }
 | 
			
		||||
    throw new APIError(400, 'Invalid initial probability')
 | 
			
		||||
 | 
			
		||||
  // uses utc time on server:
 | 
			
		||||
  const today = new Date().setHours(0, 0, 0, 0)
 | 
			
		||||
  const userContractsCreatedTodaySnapshot = await firestore
 | 
			
		||||
    .collection(`contracts`)
 | 
			
		||||
        .where('creatorId', '==', userId)
 | 
			
		||||
    .where('creatorId', '==', creator.id)
 | 
			
		||||
    .where('createdTime', '>=', today)
 | 
			
		||||
    .get()
 | 
			
		||||
  const isFree = userContractsCreatedTodaySnapshot.size === 0
 | 
			
		||||
 | 
			
		||||
      const ante = FIXED_ANTE // data.ante
 | 
			
		||||
  const ante = FIXED_ANTE
 | 
			
		||||
 | 
			
		||||
  if (
 | 
			
		||||
    ante === undefined ||
 | 
			
		||||
| 
						 | 
				
			
			@ -99,7 +83,7 @@ export const createContract = functions
 | 
			
		|||
    isNaN(ante) ||
 | 
			
		||||
    !isFinite(ante)
 | 
			
		||||
  )
 | 
			
		||||
        return { status: 'error', message: 'Invalid ante' }
 | 
			
		||||
    throw new APIError(400, 'Invalid ante')
 | 
			
		||||
 | 
			
		||||
  console.log(
 | 
			
		||||
    'creating contract for',
 | 
			
		||||
| 
						 | 
				
			
			@ -187,9 +171,8 @@ export const createContract = functions
 | 
			
		|||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
      return { status: 'success', contract }
 | 
			
		||||
    }
 | 
			
		||||
  )
 | 
			
		||||
  return { contract: contract }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const getSlug = async (question: string) => {
 | 
			
		||||
  const proposedSlug = slugify(question)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
import * as functions from 'firebase-functions'
 | 
			
		||||
import * as admin from 'firebase-admin'
 | 
			
		||||
 | 
			
		||||
import { APIError, newEndpoint, parseCredentials, lookupUser } from './api'
 | 
			
		||||
import { Contract } from '../../common/contract'
 | 
			
		||||
import { User } from '../../common/user'
 | 
			
		||||
import {
 | 
			
		||||
| 
						 | 
				
			
			@ -14,68 +14,54 @@ import { redeemShares } from './redeem-shares'
 | 
			
		|||
import { Fees } from '../../common/fees'
 | 
			
		||||
import { hasUserHitManaLimit } from '../../common/calculate'
 | 
			
		||||
 | 
			
		||||
export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
 | 
			
		||||
  async (
 | 
			
		||||
    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
 | 
			
		||||
export const placeBet = newEndpoint(['POST'], async (req, _res) => {
 | 
			
		||||
  const [bettor, _privateUser] = await lookupUser(await parseCredentials(req))
 | 
			
		||||
  const { amount, outcome, contractId } = req.body.data || {}
 | 
			
		||||
 | 
			
		||||
  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))
 | 
			
		||||
      return { status: 'error', message: 'Invalid outcome' }
 | 
			
		||||
    throw new APIError(400, 'Invalid outcome')
 | 
			
		||||
 | 
			
		||||
  // run as transaction to prevent race conditions
 | 
			
		||||
  return await firestore
 | 
			
		||||
    .runTransaction(async (transaction) => {
 | 
			
		||||
        const userDoc = firestore.doc(`users/${userId}`)
 | 
			
		||||
      const userDoc = firestore.doc(`users/${bettor.id}`)
 | 
			
		||||
      const userSnap = await transaction.get(userDoc)
 | 
			
		||||
        if (!userSnap.exists)
 | 
			
		||||
          return { status: 'error', message: 'User not found' }
 | 
			
		||||
      if (!userSnap.exists) throw new APIError(400, 'User not found')
 | 
			
		||||
      const user = userSnap.data() as User
 | 
			
		||||
 | 
			
		||||
      const contractDoc = firestore.doc(`contracts/${contractId}`)
 | 
			
		||||
      const contractSnap = await transaction.get(contractDoc)
 | 
			
		||||
        if (!contractSnap.exists)
 | 
			
		||||
          return { status: 'error', message: 'Invalid contract' }
 | 
			
		||||
      if (!contractSnap.exists) throw new APIError(400, 'Invalid contract')
 | 
			
		||||
      const contract = contractSnap.data() as Contract
 | 
			
		||||
 | 
			
		||||
      const { closeTime, outcomeType, mechanism, collectedFees, volume } =
 | 
			
		||||
        contract
 | 
			
		||||
      if (closeTime && Date.now() > closeTime)
 | 
			
		||||
          return { status: 'error', message: 'Trading is closed' }
 | 
			
		||||
        throw new APIError(400, 'Trading is closed')
 | 
			
		||||
 | 
			
		||||
      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 loanAmount = 0 // getLoanAmount(yourBets, amount)
 | 
			
		||||
        if (user.balance < amount)
 | 
			
		||||
          return { status: 'error', message: 'Insufficient balance' }
 | 
			
		||||
      if (user.balance < amount) throw new APIError(400, 'Insufficient balance')
 | 
			
		||||
 | 
			
		||||
      if (outcomeType === 'FREE_RESPONSE') {
 | 
			
		||||
        const answerSnap = await transaction.get(
 | 
			
		||||
          contractDoc.collection('answers').doc(outcome)
 | 
			
		||||
        )
 | 
			
		||||
          if (!answerSnap.exists)
 | 
			
		||||
            return { status: 'error', message: 'Invalid contract' }
 | 
			
		||||
        if (!answerSnap.exists) throw new APIError(400, 'Invalid contract')
 | 
			
		||||
 | 
			
		||||
        const { status, message } = hasUserHitManaLimit(
 | 
			
		||||
          contract,
 | 
			
		||||
          yourBets,
 | 
			
		||||
          amount
 | 
			
		||||
        )
 | 
			
		||||
          if (status === 'error') return { status, message: message }
 | 
			
		||||
        if (status === 'error') throw new APIError(400, message)
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const newBetDoc = firestore
 | 
			
		||||
| 
						 | 
				
			
			@ -120,10 +106,7 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
 | 
			
		|||
            )
 | 
			
		||||
 | 
			
		||||
      if (newP !== undefined && !isFinite(newP)) {
 | 
			
		||||
          return {
 | 
			
		||||
            status: 'error',
 | 
			
		||||
            message: 'Trade rejected due to overflow error.',
 | 
			
		||||
          }
 | 
			
		||||
        throw new APIError(400, 'Trade rejected due to overflow error.')
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      transaction.create(newBetDoc, newBet)
 | 
			
		||||
| 
						 | 
				
			
			@ -142,18 +125,17 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
 | 
			
		|||
      )
 | 
			
		||||
 | 
			
		||||
      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 })
 | 
			
		||||
 | 
			
		||||
        return { status: 'success', betId: newBetDoc.id }
 | 
			
		||||
      return { betId: newBetDoc.id }
 | 
			
		||||
    })
 | 
			
		||||
    .then(async (result) => {
 | 
			
		||||
        await redeemShares(userId, contractId)
 | 
			
		||||
      await redeemShares(bettor.id, contractId)
 | 
			
		||||
      return result
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
)
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const firestore = admin.firestore()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -214,7 +214,7 @@ function BuyPanel(props: {
 | 
			
		|||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    // warm up cloud function
 | 
			
		||||
    placeBet({}).catch()
 | 
			
		||||
    placeBet({}).catch(() => {})
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  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 }) {
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,7 +20,7 @@ export function ResolutionPanel(props: {
 | 
			
		|||
}) {
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    // warm up cloud function
 | 
			
		||||
    resolveMarket({} as any).catch()
 | 
			
		||||
    resolveMarket({} as any).catch(() => {})
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  const { contract, className } = props
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -23,7 +23,7 @@
 | 
			
		|||
    "@nivo/line": "0.74.0",
 | 
			
		||||
    "algoliasearch": "4.13.0",
 | 
			
		||||
    "clsx": "1.1.1",
 | 
			
		||||
    "cors": "^2.8.5",
 | 
			
		||||
    "cors": "2.8.5",
 | 
			
		||||
    "daisyui": "1.16.4",
 | 
			
		||||
    "dayjs": "1.10.7",
 | 
			
		||||
    "firebase": "9.6.0",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -61,7 +61,7 @@ export function NewContract(props: { question: string; tag?: string }) {
 | 
			
		|||
  }, [creator])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    createContract({}).catch() // warm up function
 | 
			
		||||
    createContract({}).catch(() => {}) // warm up function
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  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"
 | 
			
		||||
  integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
 | 
			
		||||
 | 
			
		||||
cors@^2.8.5:
 | 
			
		||||
cors@2.8.5, cors@^2.8.5:
 | 
			
		||||
  version "2.8.5"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29"
 | 
			
		||||
  integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue
	
	Block a user