Sell shares cloud function.

This commit is contained in:
James Grugett 2022-03-27 23:31:56 -05:00
parent 618bdf76a6
commit a6d2e3594b
8 changed files with 116 additions and 35 deletions

View File

@ -131,7 +131,7 @@ export function calculateCpmmShareValue(
export function calculateCpmmSale( export function calculateCpmmSale(
contract: FullContract<CPMM, Binary>, contract: FullContract<CPMM, Binary>,
bet: Bet bet: { shares: number; outcome: string }
) { ) {
const { shares, outcome } = bet const { shares, outcome } = bet

View File

@ -85,21 +85,24 @@ export const getSellBetInfo = (
export const getCpmmSellBetInfo = ( export const getCpmmSellBetInfo = (
user: User, user: User,
bet: Bet, shares: number,
outcome: 'YES' | 'NO',
contract: FullContract<CPMM, Binary>, contract: FullContract<CPMM, Binary>,
newBetId: string newBetId: string
) => { ) => {
const { pool, p } = contract 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 probBefore = getCpmmProbability(pool, p)
const probAfter = getCpmmProbability(newPool, p) const probAfter = getCpmmProbability(newPool, p)
console.log( console.log(
'SELL M$', 'SELL M$',
amount, shares,
outcome, outcome,
'for M$', 'for M$',
saleValue, saleValue,
@ -117,10 +120,6 @@ export const getCpmmSellBetInfo = (
probBefore, probBefore,
probAfter, probAfter,
createdTime: Date.now(), createdTime: Date.now(),
sale: {
amount: saleValue,
betId,
},
fees, fees,
} }

View File

@ -7,6 +7,7 @@ export * from './place-bet'
export * from './resolve-market' export * from './resolve-market'
export * from './stripe' export * from './stripe'
export * from './sell-bet' export * from './sell-bet'
export * from './sell-shares'
export * from './create-contract' export * from './create-contract'
export * from './create-user' export * from './create-user'
export * from './create-fold' export * from './create-fold'

View File

@ -4,7 +4,7 @@ import * as functions from 'firebase-functions'
import { Contract } from '../../common/contract' import { Contract } from '../../common/contract'
import { User } from '../../common/user' import { User } from '../../common/user'
import { Bet } from '../../common/bet' 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 { addObjects, removeUndefinedProps } from '../../common/util/object'
import { Fees } from '../../common/fees' import { Fees } from '../../common/fees'
@ -34,8 +34,14 @@ export const sellBet = functions.runWith({ minInstances: 1 }).https.onCall(
if (!contractSnap.exists) if (!contractSnap.exists)
return { status: 'error', message: 'Invalid contract' } return { status: 'error', message: 'Invalid contract' }
const contract = contractSnap.data() as Contract const contract = contractSnap.data() as Contract
const { closeTime, mechanism, collectedFees, volume } = 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) if (closeTime && Date.now() > closeTime)
return { status: 'error', message: 'Trading is closed' } return { status: 'error', message: 'Trading is closed' }
@ -57,15 +63,7 @@ export const sellBet = functions.runWith({ minInstances: 1 }).https.onCall(
newTotalBets, newTotalBets,
newBalance, newBalance,
fees, fees,
} = } = getSellBetInfo(user, bet, contract, newBetDoc.id)
mechanism === 'dpm-2'
? getSellBetInfo(user, bet, contract, newBetDoc.id)
: (getCpmmSellBetInfo(
user,
bet,
contract as any,
newBetDoc.id
) as any)
if (!isFinite(newBalance)) { if (!isFinite(newBalance)) {
throw new Error('Invalid user balance for ' + user.username) throw new Error('Invalid user balance for ' + user.username)
@ -81,7 +79,7 @@ export const sellBet = functions.runWith({ minInstances: 1 }).https.onCall(
totalShares: newTotalShares, totalShares: newTotalShares,
totalBets: newTotalBets, totalBets: newTotalBets,
collectedFees: addObjects<Fees>(fees ?? {}, collectedFees ?? {}), collectedFees: addObjects<Fees>(fees ?? {}, collectedFees ?? {}),
volume: volume + bet.amount, volume: volume + Math.abs(newBet.amount),
}) })
) )

View File

@ -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<CPMM, Binary>
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()

View File

@ -212,12 +212,12 @@ export function SellAmountInput(props: {
const prevLoanAmount = _.sumBy(openUserBets, (bet) => bet.loanAmount ?? 0) const prevLoanAmount = _.sumBy(openUserBets, (bet) => bet.loanAmount ?? 0)
const sharesSold = Math.min(amount ?? 0, yesShares || noShares) const sharesSold = Math.min(amount ?? 0, yesShares || noShares)
const sellAmount = calculateCpmmSale(contract, { const { saleValue } = calculateCpmmSale(contract, {
shares: sharesSold, shares: sharesSold,
outcome: sellOutcome, outcome: sellOutcome as 'YES' | 'NO',
} as Bet).saleValue })
const loanRepaid = Math.min(prevLoanAmount, sellAmount) const loanRepaid = Math.min(prevLoanAmount, saleValue)
const onAmountChange = (amount: number | undefined) => { const onAmountChange = (amount: number | undefined) => {
onChange(amount) onChange(amount)
@ -248,7 +248,7 @@ export function SellAmountInput(props: {
<Col className="gap-3 text-sm"> <Col className="gap-3 text-sm">
<Row className="items-center justify-between gap-2 text-gray-500"> <Row className="items-center justify-between gap-2 text-gray-500">
Sale proceeds{' '} Sale proceeds{' '}
<span className="text-neutral">{formatMoney(sellAmount)}</span> <span className="text-neutral">{formatMoney(saleValue)}</span>
</Row> </Row>
{prevLoanAmount && ( {prevLoanAmount && (
<Row className="items-center justify-between gap-2 text-gray-500"> <Row className="items-center justify-between gap-2 text-gray-500">

View File

@ -16,7 +16,7 @@ import {
import { Title } from './title' import { Title } from './title'
import { firebaseLogin, User } from '../lib/firebase/users' import { firebaseLogin, User } from '../lib/firebase/users'
import { Bet } from '../../common/bet' 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 { BuyAmountInput, SellAmountInput } from './amount-input'
import { InfoTooltip } from './info-tooltip' import { InfoTooltip } from './info-tooltip'
import { OutcomeLabel } from './outcome-label' import { OutcomeLabel } from './outcome-label'
@ -196,7 +196,7 @@ function BuyPanel(props: {
setBetAmount(undefined) setBetAmount(undefined)
if (onBuySuccess) onBuySuccess() if (onBuySuccess) onBuySuccess()
} else { } else {
setError(result?.error || 'Error placing bet') setError(result?.message || 'Error placing bet')
setIsSubmitting(false) setIsSubmitting(false)
} }
} }
@ -339,13 +339,13 @@ function SellPanel(props: {
setError(undefined) setError(undefined)
setIsSubmitting(true) setIsSubmitting(true)
const result = await placeBet({ const result = await sellShares({
shares: amount, shares: amount,
outcome: sharesOutcome, outcome: sharesOutcome,
contractId: contract.id, 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') { if (result?.status === 'success') {
setIsSubmitting(false) setIsSubmitting(false)
@ -353,7 +353,7 @@ function SellPanel(props: {
setAmount(undefined) setAmount(undefined)
if (onSellSuccess) onSellSuccess() if (onSellSuccess) onSellSuccess()
} else { } else {
setError(result?.error || 'Error selling') setError(result?.message || 'Error selling')
setIsSubmitting(false) setIsSubmitting(false)
} }
} }
@ -362,7 +362,7 @@ function SellPanel(props: {
const { newPool } = calculateCpmmSale(contract, { const { newPool } = calculateCpmmSale(contract, {
shares: amount ?? 0, shares: amount ?? 0,
outcome: sharesOutcome, outcome: sharesOutcome,
} as Bet) })
const resultProb = getCpmmProbability(newPool, contract.p) const resultProb = getCpmmProbability(newPool, contract.p)
return ( return (

View File

@ -18,6 +18,13 @@ export const createFold = cloudFunction<
export const placeBet = cloudFunction('placeBet') 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< export const createAnswer = cloudFunction<
{ contractId: string; text: string; amount: number }, { contractId: string; text: string; amount: number },
{ {
@ -38,8 +45,6 @@ export const resolveMarket = cloudFunction<
{ status: 'error' | 'success'; message?: string } { status: 'error' | 'success'; message?: string }
>('resolveMarket') >('resolveMarket')
export const sellBet = cloudFunction('sellBet')
export const createUser: () => Promise<User | null> = () => { export const createUser: () => Promise<User | null> = () => {
let deviceToken = window.localStorage.getItem('device-token') let deviceToken = window.localStorage.getItem('device-token')
if (!deviceToken) { if (!deviceToken) {