From a6d2e3594b988fbe5619f8b5d0396feb0349b18b Mon Sep 17 00:00:00 2001 From: James Grugett Date: Sun, 27 Mar 2022 23:31:56 -0500 Subject: [PATCH] Sell shares cloud function. --- common/calculate-cpmm.ts | 2 +- common/sell-bet.ts | 15 +++---- functions/src/index.ts | 1 + functions/src/sell-bet.ts | 22 +++++----- functions/src/sell-shares.ts | 78 +++++++++++++++++++++++++++++++++ web/components/amount-input.tsx | 10 ++--- web/components/bet-panel.tsx | 14 +++--- web/lib/firebase/api-call.ts | 9 +++- 8 files changed, 116 insertions(+), 35 deletions(-) create mode 100644 functions/src/sell-shares.ts diff --git a/common/calculate-cpmm.ts b/common/calculate-cpmm.ts index 45de7392..1d698872 100644 --- a/common/calculate-cpmm.ts +++ b/common/calculate-cpmm.ts @@ -131,7 +131,7 @@ export function calculateCpmmShareValue( export function calculateCpmmSale( contract: FullContract, - bet: Bet + bet: { shares: number; outcome: string } ) { const { shares, outcome } = bet diff --git a/common/sell-bet.ts b/common/sell-bet.ts index 246e8649..31c18d7f 100644 --- a/common/sell-bet.ts +++ b/common/sell-bet.ts @@ -85,21 +85,24 @@ export const getSellBetInfo = ( export const getCpmmSellBetInfo = ( user: User, - bet: Bet, + shares: number, + outcome: 'YES' | 'NO', contract: FullContract, newBetId: string ) => { const { pool, p } = contract - const { id: betId, amount, shares, outcome } = bet - const { saleValue, newPool, fees } = calculateCpmmSale(contract, bet) + const { saleValue, newPool, fees } = calculateCpmmSale(contract, { + shares, + outcome, + }) const probBefore = getCpmmProbability(pool, p) const probAfter = getCpmmProbability(newPool, p) console.log( 'SELL M$', - amount, + shares, outcome, 'for M$', saleValue, @@ -117,10 +120,6 @@ export const getCpmmSellBetInfo = ( probBefore, probAfter, createdTime: Date.now(), - sale: { - amount: saleValue, - betId, - }, fees, } diff --git a/functions/src/index.ts b/functions/src/index.ts index 5be29b4d..dedf42a1 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -7,6 +7,7 @@ export * from './place-bet' export * from './resolve-market' export * from './stripe' export * from './sell-bet' +export * from './sell-shares' export * from './create-contract' export * from './create-user' export * from './create-fold' diff --git a/functions/src/sell-bet.ts b/functions/src/sell-bet.ts index 4b31cfde..fff88716 100644 --- a/functions/src/sell-bet.ts +++ b/functions/src/sell-bet.ts @@ -4,7 +4,7 @@ import * as functions from 'firebase-functions' import { Contract } from '../../common/contract' import { User } from '../../common/user' import { Bet } from '../../common/bet' -import { getCpmmSellBetInfo, getSellBetInfo } from '../../common/sell-bet' +import { getSellBetInfo } from '../../common/sell-bet' import { addObjects, removeUndefinedProps } from '../../common/util/object' import { Fees } from '../../common/fees' @@ -34,8 +34,14 @@ export const sellBet = functions.runWith({ minInstances: 1 }).https.onCall( if (!contractSnap.exists) return { status: 'error', message: 'Invalid contract' } const contract = contractSnap.data() as Contract - const { closeTime, mechanism, collectedFees, volume } = contract + + if (mechanism !== 'dpm-2') + return { + status: 'error', + message: 'Sell shares only works with mechanism dpm-2', + } + if (closeTime && Date.now() > closeTime) return { status: 'error', message: 'Trading is closed' } @@ -57,15 +63,7 @@ export const sellBet = functions.runWith({ minInstances: 1 }).https.onCall( newTotalBets, newBalance, fees, - } = - mechanism === 'dpm-2' - ? getSellBetInfo(user, bet, contract, newBetDoc.id) - : (getCpmmSellBetInfo( - user, - bet, - contract as any, - newBetDoc.id - ) as any) + } = getSellBetInfo(user, bet, contract, newBetDoc.id) if (!isFinite(newBalance)) { throw new Error('Invalid user balance for ' + user.username) @@ -81,7 +79,7 @@ export const sellBet = functions.runWith({ minInstances: 1 }).https.onCall( totalShares: newTotalShares, totalBets: newTotalBets, collectedFees: addObjects(fees ?? {}, collectedFees ?? {}), - volume: volume + bet.amount, + volume: volume + Math.abs(newBet.amount), }) ) diff --git a/functions/src/sell-shares.ts b/functions/src/sell-shares.ts new file mode 100644 index 00000000..9ad4eed7 --- /dev/null +++ b/functions/src/sell-shares.ts @@ -0,0 +1,78 @@ +import * as admin from 'firebase-admin' +import * as functions from 'firebase-functions' + +import { Binary, CPMM, FullContract } from '../../common/contract' +import { User } from '../../common/user' +import { getCpmmSellBetInfo } from '../../common/sell-bet' +import { addObjects, removeUndefinedProps } from '../../common/util/object' + +export const sellShares = functions.runWith({ minInstances: 1 }).https.onCall( + async ( + data: { + contractId: string + shares: number + outcome: 'YES' | 'NO' + }, + context + ) => { + const userId = context?.auth?.uid + if (!userId) return { status: 'error', message: 'Not authorized' } + + const { contractId, shares, outcome } = data + + // Run as transaction to prevent race conditions. + return await firestore.runTransaction(async (transaction) => { + const userDoc = firestore.doc(`users/${userId}`) + const userSnap = await transaction.get(userDoc) + if (!userSnap.exists) + return { status: 'error', message: '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' } + const contract = contractSnap.data() as FullContract + const { closeTime, mechanism, collectedFees, volume } = contract + + if (mechanism !== 'cpmm-1') + return { + status: 'error', + message: 'Sell shares only works with mechanism cpmm-1', + } + + if (closeTime && Date.now() > closeTime) + return { status: 'error', message: 'Trading is closed' } + + const newBetDoc = firestore + .collection(`contracts/${contractId}/bets`) + .doc() + + const { newBet, newPool, newBalance, fees } = getCpmmSellBetInfo( + user, + shares, + outcome, + contract, + newBetDoc.id + ) + + if (!isFinite(newBalance)) { + throw new Error('Invalid user balance for ' + user.username) + } + transaction.update(userDoc, { balance: newBalance }) + transaction.create(newBetDoc, newBet) + transaction.update( + contractDoc, + removeUndefinedProps({ + pool: newPool, + collectedFees: addObjects(fees ?? {}, collectedFees ?? {}), + volume: volume + Math.abs(newBet.amount), + }) + ) + + return { status: 'success' } + }) + } +) + +const firestore = admin.firestore() diff --git a/web/components/amount-input.tsx b/web/components/amount-input.tsx index 0af3ee94..ff6af9fe 100644 --- a/web/components/amount-input.tsx +++ b/web/components/amount-input.tsx @@ -212,12 +212,12 @@ export function SellAmountInput(props: { const prevLoanAmount = _.sumBy(openUserBets, (bet) => bet.loanAmount ?? 0) const sharesSold = Math.min(amount ?? 0, yesShares || noShares) - const sellAmount = calculateCpmmSale(contract, { + const { saleValue } = calculateCpmmSale(contract, { shares: sharesSold, - outcome: sellOutcome, - } as Bet).saleValue + outcome: sellOutcome as 'YES' | 'NO', + }) - const loanRepaid = Math.min(prevLoanAmount, sellAmount) + const loanRepaid = Math.min(prevLoanAmount, saleValue) const onAmountChange = (amount: number | undefined) => { onChange(amount) @@ -248,7 +248,7 @@ export function SellAmountInput(props: { Sale proceeds{' '} - {formatMoney(sellAmount)} + {formatMoney(saleValue)} {prevLoanAmount && ( diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index f44609da..99bb1259 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -16,7 +16,7 @@ import { import { Title } from './title' import { firebaseLogin, User } from '../lib/firebase/users' import { Bet } from '../../common/bet' -import { placeBet } from '../lib/firebase/api-call' +import { placeBet, sellShares } from '../lib/firebase/api-call' import { BuyAmountInput, SellAmountInput } from './amount-input' import { InfoTooltip } from './info-tooltip' import { OutcomeLabel } from './outcome-label' @@ -196,7 +196,7 @@ function BuyPanel(props: { setBetAmount(undefined) if (onBuySuccess) onBuySuccess() } else { - setError(result?.error || 'Error placing bet') + setError(result?.message || 'Error placing bet') setIsSubmitting(false) } } @@ -339,13 +339,13 @@ function SellPanel(props: { setError(undefined) setIsSubmitting(true) - const result = await placeBet({ + const result = await sellShares({ shares: amount, outcome: sharesOutcome, contractId: contract.id, - }).then((r) => r.data as any) + }).then((r) => r.data) - console.log('placed bet. Result:', result) + console.log('Sold shares. Result:', result) if (result?.status === 'success') { setIsSubmitting(false) @@ -353,7 +353,7 @@ function SellPanel(props: { setAmount(undefined) if (onSellSuccess) onSellSuccess() } else { - setError(result?.error || 'Error selling') + setError(result?.message || 'Error selling') setIsSubmitting(false) } } @@ -362,7 +362,7 @@ function SellPanel(props: { const { newPool } = calculateCpmmSale(contract, { shares: amount ?? 0, outcome: sharesOutcome, - } as Bet) + }) const resultProb = getCpmmProbability(newPool, contract.p) return ( diff --git a/web/lib/firebase/api-call.ts b/web/lib/firebase/api-call.ts index 3782c011..6236cd64 100644 --- a/web/lib/firebase/api-call.ts +++ b/web/lib/firebase/api-call.ts @@ -18,6 +18,13 @@ export const createFold = cloudFunction< 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 }, { @@ -38,8 +45,6 @@ export const resolveMarket = cloudFunction< { status: 'error' | 'success'; message?: string } >('resolveMarket') -export const sellBet = cloudFunction('sellBet') - export const createUser: () => Promise = () => { let deviceToken = window.localStorage.getItem('device-token') if (!deviceToken) {