Rewrite client for new public APIs to use fetch instead of callables (#241)

* Rename `lib/firebase/api-call` -> `lib/firebase/fn-call`

This relieves ambiguity now that we will be using our actual
public API in the client.

* Rewrite client API calls to createContract, placeBet

* Tiny fixup for client market creation code
This commit is contained in:
Marshall Polaris 2022-05-19 15:04:34 -07:00 committed by GitHub
parent d4a49789d1
commit 20f4b97d8b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 179 additions and 131 deletions

View File

@ -4,7 +4,7 @@ import { useState } from 'react'
import { Contract } from 'common/contract' import { Contract } from 'common/contract'
import { formatMoney } from 'common/util/format' import { formatMoney } from 'common/util/format'
import { useUser } from 'web/hooks/use-user' import { useUser } from 'web/hooks/use-user'
import { addLiquidity } from 'web/lib/firebase/api-call' import { addLiquidity } from 'web/lib/firebase/fn-call'
import { AmountInput } from './amount-input' import { AmountInput } from './amount-input'
import { Row } from './layout/row' import { Row } from './layout/row'

View File

@ -6,7 +6,7 @@ import { Answer } from 'common/answer'
import { DPM, FreeResponse, FullContract } from 'common/contract' import { DPM, FreeResponse, FullContract } from 'common/contract'
import { BuyAmountInput } from '../amount-input' import { BuyAmountInput } from '../amount-input'
import { Col } from '../layout/col' import { Col } from '../layout/col'
import { placeBet } from 'web/lib/firebase/api-call' import { APIError, placeBet } from 'web/lib/firebase/api-call'
import { Row } from '../layout/row' import { Row } from '../layout/row'
import { Spacer } from '../layout/spacer' import { Spacer } from '../layout/spacer'
import { import {
@ -52,22 +52,26 @@ export function AnswerBetPanel(props: {
setError(undefined) setError(undefined)
setIsSubmitting(true) setIsSubmitting(true)
const result = await placeBet({ placeBet({
amount: betAmount, amount: betAmount,
outcome: answerId, outcome: answerId,
contractId: contract.id, contractId: contract.id,
}).then((r) => r.data as any) })
.then((r) => {
console.log('placed bet. Result:', result) console.log('placed bet. Result:', r)
setIsSubmitting(false)
if (result?.status === 'success') { setBetAmount(undefined)
setIsSubmitting(false) props.closePanel()
setBetAmount(undefined) })
props.closePanel() .catch((e) => {
} else { if (e instanceof APIError) {
setError(result?.message || 'Error placing bet') setError(e.toString())
setIsSubmitting(false) } else {
} console.error(e)
setError('Error placing bet')
}
setIsSubmitting(false)
})
} }
const betDisabled = isSubmitting || !betAmount || error const betDisabled = isSubmitting || !betAmount || error

View File

@ -4,7 +4,7 @@ import { useState } from 'react'
import { DPM, FreeResponse, FullContract } from 'common/contract' import { DPM, FreeResponse, FullContract } from 'common/contract'
import { Col } from '../layout/col' import { Col } from '../layout/col'
import { resolveMarket } from 'web/lib/firebase/api-call' import { resolveMarket } from 'web/lib/firebase/fn-call'
import { Row } from '../layout/row' import { Row } from '../layout/row'
import { ChooseCancelSelector } from '../yes-no-selector' import { ChooseCancelSelector } from '../yes-no-selector'
import { ResolveConfirmationButton } from '../confirmation-button' import { ResolveConfirmationButton } from '../confirmation-button'

View File

@ -5,7 +5,7 @@ import Textarea from 'react-expanding-textarea'
import { DPM, FreeResponse, FullContract } from 'common/contract' import { DPM, FreeResponse, FullContract } from 'common/contract'
import { BuyAmountInput } from '../amount-input' import { BuyAmountInput } from '../amount-input'
import { Col } from '../layout/col' import { Col } from '../layout/col'
import { createAnswer } from 'web/lib/firebase/api-call' import { createAnswer } from 'web/lib/firebase/fn-call'
import { Row } from '../layout/row' import { Row } from '../layout/row'
import { import {
formatMoney, formatMoney,

View File

@ -16,7 +16,8 @@ import {
import { Title } from './title' import { Title } from './title'
import { firebaseLogin, User } from 'web/lib/firebase/users' import { firebaseLogin, User } from 'web/lib/firebase/users'
import { Bet } from 'common/bet' import { Bet } from 'common/bet'
import { placeBet, sellShares } from 'web/lib/firebase/api-call' import { APIError, placeBet } from 'web/lib/firebase/api-call'
import { sellShares } from 'web/lib/firebase/fn-call'
import { AmountInput, BuyAmountInput } from './amount-input' import { AmountInput, BuyAmountInput } from './amount-input'
import { InfoTooltip } from './info-tooltip' import { InfoTooltip } from './info-tooltip'
import { BinaryOutcomeLabel } from './outcome-label' import { BinaryOutcomeLabel } from './outcome-label'
@ -240,23 +241,27 @@ function BuyPanel(props: {
setError(undefined) setError(undefined)
setIsSubmitting(true) setIsSubmitting(true)
const result = await placeBet({ placeBet({
amount: betAmount, amount: betAmount,
outcome: betChoice, outcome: betChoice,
contractId: contract.id, contractId: contract.id,
}).then((r) => r.data as any) })
.then((r) => {
console.log('placed bet. Result:', result) console.log('placed bet. Result:', r)
setIsSubmitting(false)
if (result?.status === 'success') { setWasSubmitted(true)
setIsSubmitting(false) setBetAmount(undefined)
setWasSubmitted(true) if (onBuySuccess) onBuySuccess()
setBetAmount(undefined) })
if (onBuySuccess) onBuySuccess() .catch((e) => {
} else { if (e instanceof APIError) {
setError(result?.message || 'Error placing bet') setError(e.toString())
setIsSubmitting(false) } else {
} console.error(e)
setError('Error placing bet')
}
setIsSubmitting(false)
})
} }
const betDisabled = isSubmitting || !betAmount || error const betDisabled = isSubmitting || !betAmount || error

View File

@ -22,7 +22,7 @@ import {
} from 'web/lib/firebase/contracts' } from 'web/lib/firebase/contracts'
import { Row } from './layout/row' import { Row } from './layout/row'
import { UserLink } from './user-page' import { UserLink } from './user-page'
import { sellBet } from 'web/lib/firebase/api-call' import { sellBet } from 'web/lib/firebase/fn-call'
import { ConfirmationButton } from './confirmation-button' import { ConfirmationButton } from './confirmation-button'
import { OutcomeLabel, YesLabel, NoLabel } from './outcome-label' import { OutcomeLabel, YesLabel, NoLabel } from './outcome-label'
import { filterDefined } from 'common/util/array' import { filterDefined } from 'common/util/array'

View File

@ -3,7 +3,7 @@ import { useRouter } from 'next/router'
import { useState } from 'react' import { useState } from 'react'
import { PlusCircleIcon } from '@heroicons/react/solid' import { PlusCircleIcon } from '@heroicons/react/solid'
import { parseWordsAsTags } from 'common/util/parse' import { parseWordsAsTags } from 'common/util/parse'
import { createFold } from 'web/lib/firebase/api-call' import { createFold } from 'web/lib/firebase/fn-call'
import { foldPath } from 'web/lib/firebase/folds' import { foldPath } from 'web/lib/firebase/folds'
import { toCamelCase } from 'common/util/format' import { toCamelCase } from 'common/util/format'
import { ConfirmationButton } from '../confirmation-button' import { ConfirmationButton } from '../confirmation-button'

View File

@ -6,7 +6,7 @@ import { User } from 'web/lib/firebase/users'
import { NumberCancelSelector } from './yes-no-selector' import { NumberCancelSelector } from './yes-no-selector'
import { Spacer } from './layout/spacer' import { Spacer } from './layout/spacer'
import { ResolveConfirmationButton } from './confirmation-button' import { ResolveConfirmationButton } from './confirmation-button'
import { resolveMarket } from 'web/lib/firebase/api-call' import { resolveMarket } from 'web/lib/firebase/fn-call'
import { NumericContract } from 'common/contract' import { NumericContract } from 'common/contract'
import { BucketInput } from './bucket-input' import { BucketInput } from './bucket-input'

View File

@ -6,7 +6,7 @@ import { User } from 'web/lib/firebase/users'
import { YesNoCancelSelector } from './yes-no-selector' import { YesNoCancelSelector } from './yes-no-selector'
import { Spacer } from './layout/spacer' import { Spacer } from './layout/spacer'
import { ResolveConfirmationButton } from './confirmation-button' import { ResolveConfirmationButton } from './confirmation-button'
import { resolveMarket } from 'web/lib/firebase/api-call' import { resolveMarket } from 'web/lib/firebase/fn-call'
import { ProbabilitySelector } from './probability-selector' import { ProbabilitySelector } from './probability-selector'
import { DPM_CREATOR_FEE } from 'common/fees' import { DPM_CREATOR_FEE } from 'common/fees'
import { getProbability } from 'common/calculate' import { getProbability } from 'common/calculate'

View File

@ -1,80 +1,45 @@
import { httpsCallable } from 'firebase/functions' import { auth } from './users'
import { Fold } from 'common/fold' import { app, functions } from './init'
import { Txn } from 'common/txn'
import { User } from 'common/user'
import { randomString } from 'common/util/random'
import './init'
import { functions } from './init'
export const cloudFunction = <RequestData, ResponseData>(name: string) => export class APIError extends Error {
httpsCallable<RequestData, ResponseData>(functions, name) code: number
constructor(code: number, message: string) {
export const createContract = cloudFunction('createContract') super(message)
this.code = code
export const createFold = cloudFunction< this.name = 'APIError'
{ name: string; about: string; tags: string[] },
{ status: 'error' | 'success'; message?: string; fold?: Fold }
>('createFold')
export const transact = cloudFunction<
Omit<Txn, 'id' | 'createdTime'>,
{ status: 'error' | 'success'; message?: string; txn?: Txn }
>('transact')
export const placeBet = cloudFunction('placeBet')
export const sellBet = cloudFunction('sellBet')
export const sellShares = cloudFunction<
{ contractId: string; shares: number; outcome: 'YES' | 'NO' },
{ status: 'error' | 'success'; message?: string }
>('sellShares')
export const createAnswer = cloudFunction<
{ contractId: string; text: string; amount: number },
{
status: 'error' | 'success'
message?: string
answerId?: string
betId?: string
} }
>('createAnswer') }
export const resolveMarket = cloudFunction< export async function call(name: string, method: string, params: any) {
{ const user = auth.currentUser
outcome: string if (user == null) {
value?: number throw new Error('Must be signed in to make API calls.')
contractId: string
probabilityInt?: number
resolutions?: { [outcome: string]: number }
},
{ status: 'error' | 'success'; message?: string }
>('resolveMarket')
export const createUser: () => Promise<User | null> = () => {
let deviceToken = window.localStorage.getItem('device-token')
if (!deviceToken) {
deviceToken = randomString()
window.localStorage.setItem('device-token', deviceToken)
} }
const token = await user.getIdToken()
return cloudFunction('createUser')({ deviceToken }) const region = functions.region
.then((r) => (r.data as any)?.user || null) const projectId = app.options.projectId
.catch(() => null) const url = `https://${region}-${projectId}.cloudfunctions.net/${name}`
const req = new Request(url, {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
method: method,
body: JSON.stringify({ data: params }),
})
return await fetch(req).then(async (resp) => {
const json = (await resp.json()) as { [k: string]: any }
if (json.data.status == 'error') {
throw new APIError(resp.status, json.data.message)
}
return json.data
})
} }
export const changeUserInfo = (data: { export function createContract(params: any) {
username?: string return call('createContract', 'POST', params)
name?: string
avatarUrl?: string
}) => {
return cloudFunction('changeUserInfo')(data)
.then((r) => r.data as { status: string; message?: string })
.catch((e) => ({ status: 'error', message: e.message }))
} }
export const addLiquidity = (data: { amount: number; contractId: string }) => { export function placeBet(params: any) {
return cloudFunction('addLiquidity')(data) return call('placeBet', 'POST', params)
.then((r) => r.data as { status: string })
.catch((e) => ({ status: 'error', message: e.message }))
} }

View File

@ -0,0 +1,76 @@
import { httpsCallable } from 'firebase/functions'
import { Fold } from 'common/fold'
import { Txn } from 'common/txn'
import { User } from 'common/user'
import { randomString } from 'common/util/random'
import './init'
import { functions } from './init'
export const cloudFunction = <RequestData, ResponseData>(name: string) =>
httpsCallable<RequestData, ResponseData>(functions, name)
export const createFold = cloudFunction<
{ name: string; about: string; tags: string[] },
{ status: 'error' | 'success'; message?: string; fold?: Fold }
>('createFold')
export const transact = cloudFunction<
Omit<Txn, 'id' | 'createdTime'>,
{ status: 'error' | 'success'; message?: string; txn?: Txn }
>('transact')
export const sellBet = cloudFunction('sellBet')
export const sellShares = cloudFunction<
{ contractId: string; shares: number; outcome: 'YES' | 'NO' },
{ status: 'error' | 'success'; message?: string }
>('sellShares')
export const createAnswer = cloudFunction<
{ contractId: string; text: string; amount: number },
{
status: 'error' | 'success'
message?: string
answerId?: string
betId?: string
}
>('createAnswer')
export const resolveMarket = cloudFunction<
{
outcome: string
value?: number
contractId: string
probabilityInt?: number
resolutions?: { [outcome: string]: number }
},
{ status: 'error' | 'success'; message?: string }
>('resolveMarket')
export const createUser: () => Promise<User | null> = () => {
let deviceToken = window.localStorage.getItem('device-token')
if (!deviceToken) {
deviceToken = randomString()
window.localStorage.setItem('device-token', deviceToken)
}
return cloudFunction('createUser')({ deviceToken })
.then((r) => (r.data as any)?.user || null)
.catch(() => null)
}
export const changeUserInfo = (data: {
username?: string
name?: string
avatarUrl?: string
}) => {
return cloudFunction('changeUserInfo')(data)
.then((r) => r.data as { status: string; message?: string })
.catch((e) => ({ status: 'error', message: e.message }))
}
export const addLiquidity = (data: { amount: number; contractId: string }) => {
return cloudFunction('addLiquidity')(data)
.then((r) => r.data as { status: string })
.catch((e) => ({ status: 'error', message: e.message }))
}

View File

@ -22,7 +22,7 @@ import _ from 'lodash'
import { app } from './init' import { app } from './init'
import { PrivateUser, User } from 'common/user' import { PrivateUser, User } from 'common/user'
import { createUser } from './api-call' import { createUser } from './fn-call'
import { getValue, getValues, listenForValue, listenForValues } from './utils' import { getValue, getValues, listenForValue, listenForValues } from './utils'
import { DAY_MS } from 'common/util/time' import { DAY_MS } from 'common/util/time'
import { feed } from 'common/feed' import { feed } from 'common/feed'

View File

@ -10,7 +10,7 @@ import { Spacer } from 'web/components/layout/spacer'
import { User } from 'common/user' import { User } from 'common/user'
import { useUser } from 'web/hooks/use-user' import { useUser } from 'web/hooks/use-user'
import { Linkify } from 'web/components/linkify' import { Linkify } from 'web/components/linkify'
import { transact } from 'web/lib/firebase/api-call' import { transact } from 'web/lib/firebase/fn-call'
import { charities, Charity } from 'common/charity' import { charities, Charity } from 'common/charity'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import Custom404 from '../404' import Custom404 from '../404'

View File

@ -124,26 +124,24 @@ export function NewContract(props: { question: string; tag?: string }) {
setIsSubmitting(true) setIsSubmitting(true)
const result: any = await createContract( try {
removeUndefinedProps({ const result = await createContract(
question, removeUndefinedProps({
outcomeType, question,
description, outcomeType,
initialProb, description,
ante, initialProb,
closeTime, ante,
tags: category ? [category] : undefined, closeTime,
min, tags: category ? [category] : undefined,
max, min,
}) max,
).then((r) => r.data || {}) })
)
if (result.status !== 'success') { await router.push(contractPath(result.contract as Contract))
console.log('error creating contract', result) } catch (e) {
return console.log('error creating contract', e)
} }
await router.push(contractPath(result.contract as Contract))
} }
const descriptionPlaceholder = const descriptionPlaceholder =

View File

@ -155,7 +155,7 @@ ${TEST_VALUE}
ante, ante,
closeTime, closeTime,
tags: parseWordsAsTags(tags), tags: parseWordsAsTags(tags),
}).then((r) => (r.data as any).contract) }).then((r) => r.contract)
setCreatedContracts((prev) => [...prev, contract]) setCreatedContracts((prev) => [...prev, contract])
} }
@ -237,7 +237,7 @@ ${TEST_VALUE}
<label className="label mb-1 gap-2"> <label className="label mb-1 gap-2">
<span>Market ante</span> <span>Market ante</span>
<InfoTooltip <InfoTooltip
text={`Subsidize your market to encourage trading. Ante bets are set to match your initial probability. text={`Subsidize your market to encourage trading. Ante bets are set to match your initial probability.
You earn ${0.01 * 100}% of trading volume.`} You earn ${0.01 * 100}% of trading volume.`}
/> />
</label> </label>

View File

@ -9,7 +9,7 @@ import { Title } from 'web/components/title'
import { usePrivateUser, useUser } from 'web/hooks/use-user' import { usePrivateUser, useUser } from 'web/hooks/use-user'
import { formatMoney } from 'common/util/format' import { formatMoney } from 'common/util/format'
import { cleanDisplayName, cleanUsername } from 'common/util/clean-username' import { cleanDisplayName, cleanUsername } from 'common/util/clean-username'
import { changeUserInfo } from 'web/lib/firebase/api-call' import { changeUserInfo } from 'web/lib/firebase/fn-call'
import { uploadImage } from 'web/lib/firebase/storage' import { uploadImage } from 'web/lib/firebase/storage'
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'