From 20f4b97d8bb788dec6e3362afa82caa9d4e80291 Mon Sep 17 00:00:00 2001 From: Marshall Polaris Date: Thu, 19 May 2022 15:04:34 -0700 Subject: [PATCH] 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 --- web/components/add-liquidity-panel.tsx | 2 +- web/components/answers/answer-bet-panel.tsx | 32 +++--- .../answers/answer-resolve-panel.tsx | 2 +- .../answers/create-answer-panel.tsx | 2 +- web/components/bet-panel.tsx | 35 +++--- web/components/bets-list.tsx | 2 +- web/components/folds/create-fold-button.tsx | 2 +- web/components/numeric-resolution-panel.tsx | 2 +- web/components/resolution-panel.tsx | 2 +- web/lib/firebase/api-call.ts | 107 ++++++------------ web/lib/firebase/fn-call.ts | 76 +++++++++++++ web/lib/firebase/users.ts | 2 +- web/pages/charity/[charitySlug].tsx | 2 +- web/pages/create.tsx | 36 +++--- web/pages/make-predictions.tsx | 4 +- web/pages/profile.tsx | 2 +- 16 files changed, 179 insertions(+), 131 deletions(-) create mode 100644 web/lib/firebase/fn-call.ts diff --git a/web/components/add-liquidity-panel.tsx b/web/components/add-liquidity-panel.tsx index 240bde7d..c1deb637 100644 --- a/web/components/add-liquidity-panel.tsx +++ b/web/components/add-liquidity-panel.tsx @@ -4,7 +4,7 @@ import { useState } from 'react' import { Contract } from 'common/contract' import { formatMoney } from 'common/util/format' 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 { Row } from './layout/row' diff --git a/web/components/answers/answer-bet-panel.tsx b/web/components/answers/answer-bet-panel.tsx index 8c424bea..fb9e1270 100644 --- a/web/components/answers/answer-bet-panel.tsx +++ b/web/components/answers/answer-bet-panel.tsx @@ -6,7 +6,7 @@ import { Answer } from 'common/answer' import { DPM, FreeResponse, FullContract } from 'common/contract' import { BuyAmountInput } from '../amount-input' 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 { Spacer } from '../layout/spacer' import { @@ -52,22 +52,26 @@ export function AnswerBetPanel(props: { setError(undefined) setIsSubmitting(true) - const result = await placeBet({ + placeBet({ amount: betAmount, outcome: answerId, contractId: contract.id, - }).then((r) => r.data as any) - - console.log('placed bet. Result:', result) - - if (result?.status === 'success') { - setIsSubmitting(false) - setBetAmount(undefined) - props.closePanel() - } else { - setError(result?.message || 'Error placing bet') - setIsSubmitting(false) - } + }) + .then((r) => { + console.log('placed bet. Result:', r) + setIsSubmitting(false) + setBetAmount(undefined) + props.closePanel() + }) + .catch((e) => { + if (e instanceof APIError) { + setError(e.toString()) + } else { + console.error(e) + setError('Error placing bet') + } + setIsSubmitting(false) + }) } const betDisabled = isSubmitting || !betAmount || error diff --git a/web/components/answers/answer-resolve-panel.tsx b/web/components/answers/answer-resolve-panel.tsx index 70eb1299..38cee874 100644 --- a/web/components/answers/answer-resolve-panel.tsx +++ b/web/components/answers/answer-resolve-panel.tsx @@ -4,7 +4,7 @@ import { useState } from 'react' import { DPM, FreeResponse, FullContract } from 'common/contract' 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 { ChooseCancelSelector } from '../yes-no-selector' import { ResolveConfirmationButton } from '../confirmation-button' diff --git a/web/components/answers/create-answer-panel.tsx b/web/components/answers/create-answer-panel.tsx index eefae83a..d0ae6996 100644 --- a/web/components/answers/create-answer-panel.tsx +++ b/web/components/answers/create-answer-panel.tsx @@ -5,7 +5,7 @@ import Textarea from 'react-expanding-textarea' import { DPM, FreeResponse, FullContract } from 'common/contract' import { BuyAmountInput } from '../amount-input' 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 { formatMoney, diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index 8796f842..6fdf8cea 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -16,7 +16,8 @@ import { import { Title } from './title' import { firebaseLogin, User } from 'web/lib/firebase/users' 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 { InfoTooltip } from './info-tooltip' import { BinaryOutcomeLabel } from './outcome-label' @@ -240,23 +241,27 @@ function BuyPanel(props: { setError(undefined) setIsSubmitting(true) - const result = await placeBet({ + placeBet({ amount: betAmount, outcome: betChoice, contractId: contract.id, - }).then((r) => r.data as any) - - console.log('placed bet. Result:', result) - - if (result?.status === 'success') { - setIsSubmitting(false) - setWasSubmitted(true) - setBetAmount(undefined) - if (onBuySuccess) onBuySuccess() - } else { - setError(result?.message || 'Error placing bet') - setIsSubmitting(false) - } + }) + .then((r) => { + console.log('placed bet. Result:', r) + setIsSubmitting(false) + setWasSubmitted(true) + setBetAmount(undefined) + if (onBuySuccess) onBuySuccess() + }) + .catch((e) => { + if (e instanceof APIError) { + setError(e.toString()) + } else { + console.error(e) + setError('Error placing bet') + } + setIsSubmitting(false) + }) } const betDisabled = isSubmitting || !betAmount || error diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx index 24fa5934..51d96efc 100644 --- a/web/components/bets-list.tsx +++ b/web/components/bets-list.tsx @@ -22,7 +22,7 @@ import { } from 'web/lib/firebase/contracts' import { Row } from './layout/row' 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 { OutcomeLabel, YesLabel, NoLabel } from './outcome-label' import { filterDefined } from 'common/util/array' diff --git a/web/components/folds/create-fold-button.tsx b/web/components/folds/create-fold-button.tsx index d00964c9..042982ea 100644 --- a/web/components/folds/create-fold-button.tsx +++ b/web/components/folds/create-fold-button.tsx @@ -3,7 +3,7 @@ import { useRouter } from 'next/router' import { useState } from 'react' import { PlusCircleIcon } from '@heroicons/react/solid' 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 { toCamelCase } from 'common/util/format' import { ConfirmationButton } from '../confirmation-button' diff --git a/web/components/numeric-resolution-panel.tsx b/web/components/numeric-resolution-panel.tsx index cb208a9f..cbda7f9c 100644 --- a/web/components/numeric-resolution-panel.tsx +++ b/web/components/numeric-resolution-panel.tsx @@ -6,7 +6,7 @@ import { User } from 'web/lib/firebase/users' import { NumberCancelSelector } from './yes-no-selector' import { Spacer } from './layout/spacer' 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 { BucketInput } from './bucket-input' diff --git a/web/components/resolution-panel.tsx b/web/components/resolution-panel.tsx index 356587fa..d37e9084 100644 --- a/web/components/resolution-panel.tsx +++ b/web/components/resolution-panel.tsx @@ -6,7 +6,7 @@ import { User } from 'web/lib/firebase/users' import { YesNoCancelSelector } from './yes-no-selector' import { Spacer } from './layout/spacer' 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 { DPM_CREATOR_FEE } from 'common/fees' import { getProbability } from 'common/calculate' diff --git a/web/lib/firebase/api-call.ts b/web/lib/firebase/api-call.ts index 2bd23e39..ebc90d20 100644 --- a/web/lib/firebase/api-call.ts +++ b/web/lib/firebase/api-call.ts @@ -1,80 +1,45 @@ -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' +import { auth } from './users' +import { app, functions } from './init' -export const cloudFunction = (name: string) => - httpsCallable(functions, name) - -export const createContract = cloudFunction('createContract') - -export const createFold = cloudFunction< - { name: string; about: string; tags: string[] }, - { status: 'error' | 'success'; message?: string; fold?: Fold } ->('createFold') - -export const transact = cloudFunction< - Omit, - { 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 +export class APIError extends Error { + code: number + constructor(code: number, message: string) { + super(message) + this.code = code + this.name = 'APIError' } ->('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 = () => { - let deviceToken = window.localStorage.getItem('device-token') - if (!deviceToken) { - deviceToken = randomString() - window.localStorage.setItem('device-token', deviceToken) +export async function call(name: string, method: string, params: any) { + const user = auth.currentUser + if (user == null) { + throw new Error('Must be signed in to make API calls.') } - - return cloudFunction('createUser')({ deviceToken }) - .then((r) => (r.data as any)?.user || null) - .catch(() => null) + const token = await user.getIdToken() + const region = functions.region + const projectId = app.options.projectId + 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: { - 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 function createContract(params: any) { + return call('createContract', 'POST', params) } -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 })) +export function placeBet(params: any) { + return call('placeBet', 'POST', params) } diff --git a/web/lib/firebase/fn-call.ts b/web/lib/firebase/fn-call.ts new file mode 100644 index 00000000..72a2a110 --- /dev/null +++ b/web/lib/firebase/fn-call.ts @@ -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 = (name: string) => + httpsCallable(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, + { 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 = () => { + 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 })) +} diff --git a/web/lib/firebase/users.ts b/web/lib/firebase/users.ts index d7acd2bb..e66dc177 100644 --- a/web/lib/firebase/users.ts +++ b/web/lib/firebase/users.ts @@ -22,7 +22,7 @@ import _ from 'lodash' import { app } from './init' import { PrivateUser, User } from 'common/user' -import { createUser } from './api-call' +import { createUser } from './fn-call' import { getValue, getValues, listenForValue, listenForValues } from './utils' import { DAY_MS } from 'common/util/time' import { feed } from 'common/feed' diff --git a/web/pages/charity/[charitySlug].tsx b/web/pages/charity/[charitySlug].tsx index c943df9d..2f9f88db 100644 --- a/web/pages/charity/[charitySlug].tsx +++ b/web/pages/charity/[charitySlug].tsx @@ -10,7 +10,7 @@ import { Spacer } from 'web/components/layout/spacer' import { User } from 'common/user' import { useUser } from 'web/hooks/use-user' 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 { useRouter } from 'next/router' import Custom404 from '../404' diff --git a/web/pages/create.tsx b/web/pages/create.tsx index fec37708..b68bde82 100644 --- a/web/pages/create.tsx +++ b/web/pages/create.tsx @@ -124,26 +124,24 @@ export function NewContract(props: { question: string; tag?: string }) { setIsSubmitting(true) - const result: any = await createContract( - removeUndefinedProps({ - question, - outcomeType, - description, - initialProb, - ante, - closeTime, - tags: category ? [category] : undefined, - min, - max, - }) - ).then((r) => r.data || {}) - - if (result.status !== 'success') { - console.log('error creating contract', result) - return + try { + const result = await createContract( + removeUndefinedProps({ + question, + outcomeType, + description, + initialProb, + ante, + closeTime, + tags: category ? [category] : undefined, + min, + max, + }) + ) + await router.push(contractPath(result.contract as Contract)) + } catch (e) { + console.log('error creating contract', e) } - - await router.push(contractPath(result.contract as Contract)) } const descriptionPlaceholder = diff --git a/web/pages/make-predictions.tsx b/web/pages/make-predictions.tsx index 54929bd7..081b29b7 100644 --- a/web/pages/make-predictions.tsx +++ b/web/pages/make-predictions.tsx @@ -155,7 +155,7 @@ ${TEST_VALUE} ante, closeTime, tags: parseWordsAsTags(tags), - }).then((r) => (r.data as any).contract) + }).then((r) => r.contract) setCreatedContracts((prev) => [...prev, contract]) } @@ -237,7 +237,7 @@ ${TEST_VALUE} diff --git a/web/pages/profile.tsx b/web/pages/profile.tsx index 7a920270..ea206f99 100644 --- a/web/pages/profile.tsx +++ b/web/pages/profile.tsx @@ -9,7 +9,7 @@ import { Title } from 'web/components/title' import { usePrivateUser, useUser } from 'web/hooks/use-user' import { formatMoney } from 'common/util/format' 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 { Col } from 'web/components/layout/col' import { Row } from 'web/components/layout/row'