Migrate addLiquidity and withdrawLiquidity functions to v2 (#627)

This commit is contained in:
Marshall Polaris 2022-07-08 15:08:17 -07:00 committed by GitHub
parent ed0544212d
commit d9f42caa6a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 167 additions and 205 deletions

View File

@ -1,105 +1,96 @@
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'
import { z } from 'zod'
import { Contract } from '../../common/contract'
import { User } from '../../common/user'
import { removeUndefinedProps } from '../../common/util/object'
import { redeemShares } from './redeem-shares'
import { getNewLiquidityProvision } from '../../common/add-liquidity'
import { APIError, newEndpoint, validate } from './api'
export const addLiquidity = functions.runWith({ minInstances: 1 }).https.onCall(
async (
data: {
amount: number
contractId: string
},
context
) => {
const userId = context?.auth?.uid
if (!userId) return { status: 'error', message: 'Not authorized' }
const bodySchema = z.object({
contractId: z.string(),
amount: z.number().gt(0),
})
const { amount, contractId } = data
export const addliquidity = newEndpoint({}, async (req, auth) => {
const { amount, contractId } = validate(bodySchema, req.body)
if (amount <= 0 || isNaN(amount) || !isFinite(amount))
return { status: 'error', message: 'Invalid amount' }
if (!isFinite(amount)) throw new APIError(400, 'Invalid amount')
// 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
// run as transaction to prevent race conditions
return await firestore
.runTransaction(async (transaction) => {
const userDoc = firestore.doc(`users/${auth.uid}`)
const userSnap = await transaction.get(userDoc)
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' }
const contract = contractSnap.data() as Contract
if (
contract.mechanism !== 'cpmm-1' ||
(contract.outcomeType !== 'BINARY' &&
contract.outcomeType !== 'PSEUDO_NUMERIC')
)
return { status: 'error', message: 'Invalid contract' }
const contractDoc = firestore.doc(`contracts/${contractId}`)
const contractSnap = await transaction.get(contractDoc)
if (!contractSnap.exists) throw new APIError(400, 'Invalid contract')
const contract = contractSnap.data() as Contract
if (
contract.mechanism !== 'cpmm-1' ||
(contract.outcomeType !== 'BINARY' &&
contract.outcomeType !== 'PSEUDO_NUMERIC')
)
throw new APIError(400, 'Invalid contract')
const { closeTime } = contract
if (closeTime && Date.now() > closeTime)
return { status: 'error', message: 'Trading is closed' }
const { closeTime } = contract
if (closeTime && Date.now() > closeTime)
throw new APIError(400, 'Trading is closed')
if (user.balance < amount)
return { status: 'error', message: 'Insufficient balance' }
if (user.balance < amount) throw new APIError(400, 'Insufficient balance')
const newLiquidityProvisionDoc = firestore
.collection(`contracts/${contractId}/liquidity`)
.doc()
const newLiquidityProvisionDoc = firestore
.collection(`contracts/${contractId}/liquidity`)
.doc()
const { newLiquidityProvision, newPool, newP, newTotalLiquidity } =
getNewLiquidityProvision(
user,
amount,
contract,
newLiquidityProvisionDoc.id
)
if (newP !== undefined && !isFinite(newP)) {
return {
status: 'error',
message: 'Liquidity injection rejected due to overflow error.',
}
}
transaction.update(
contractDoc,
removeUndefinedProps({
pool: newPool,
p: newP,
totalLiquidity: newTotalLiquidity,
})
const { newLiquidityProvision, newPool, newP, newTotalLiquidity } =
getNewLiquidityProvision(
user,
amount,
contract,
newLiquidityProvisionDoc.id
)
const newBalance = user.balance - amount
const newTotalDeposits = user.totalDeposits - amount
if (!isFinite(newBalance)) {
throw new Error('Invalid user balance for ' + user.username)
if (newP !== undefined && !isFinite(newP)) {
return {
status: 'error',
message: 'Liquidity injection rejected due to overflow error.',
}
}
transaction.update(userDoc, {
balance: newBalance,
totalDeposits: newTotalDeposits,
transaction.update(
contractDoc,
removeUndefinedProps({
pool: newPool,
p: newP,
totalLiquidity: newTotalLiquidity,
})
)
transaction.create(newLiquidityProvisionDoc, newLiquidityProvision)
const newBalance = user.balance - amount
const newTotalDeposits = user.totalDeposits - amount
return { status: 'success', newLiquidityProvision }
if (!isFinite(newBalance)) {
throw new APIError(500, 'Invalid user balance for ' + user.username)
}
transaction.update(userDoc, {
balance: newBalance,
totalDeposits: newTotalDeposits,
})
.then(async (result) => {
await redeemShares(userId, contractId)
return result
})
}
)
transaction.create(newLiquidityProvisionDoc, newLiquidityProvision)
return newLiquidityProvision
})
.then(async (result) => {
await redeemShares(auth.uid, contractId)
return result
})
})
const firestore = admin.firestore()

View File

@ -16,7 +16,6 @@ export * from './update-metrics'
export * from './update-stats'
export * from './backup-db'
export * from './market-close-notifications'
export * from './add-liquidity'
export * from './on-create-answer'
export * from './on-update-contract'
export * from './on-create-contract'
@ -36,6 +35,7 @@ export * from './place-bet'
export * from './sell-bet'
export * from './sell-shares'
export * from './create-contract'
export * from './add-liquidity'
export * from './withdraw-liquidity'
export * from './create-group'
export * from './resolve-market'

View File

@ -1,5 +1,5 @@
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'
import { z } from 'zod'
import { CPMMContract } from '../../common/contract'
import { User } from '../../common/user'
@ -10,129 +10,107 @@ import { Bet } from '../../common/bet'
import { getProbability } from '../../common/calculate'
import { noFees } from '../../common/fees'
import { APIError } from './api'
import { APIError, newEndpoint, validate } from './api'
import { redeemShares } from './redeem-shares'
export const withdrawLiquidity = functions
.runWith({ minInstances: 1 })
.https.onCall(
async (
data: {
contractId: string
},
context
) => {
const userId = context?.auth?.uid
if (!userId) return { status: 'error', message: 'Not authorized' }
const bodySchema = z.object({
contractId: z.string(),
})
const { contractId } = data
if (!contractId)
return { status: 'error', message: 'Missing contract id' }
export const withdrawliquidity = newEndpoint({}, async (req, auth) => {
const { contractId } = validate(bodySchema, req.body)
return await firestore
.runTransaction(async (trans) => {
const lpDoc = firestore.doc(`users/${userId}`)
const lpSnap = await trans.get(lpDoc)
if (!lpSnap.exists) throw new APIError(400, 'User not found.')
const lp = lpSnap.data() as User
return await firestore
.runTransaction(async (trans) => {
const lpDoc = firestore.doc(`users/${auth.uid}`)
const lpSnap = await trans.get(lpDoc)
if (!lpSnap.exists) throw new APIError(400, 'User not found.')
const lp = lpSnap.data() as User
const contractDoc = firestore.doc(`contracts/${contractId}`)
const contractSnap = await trans.get(contractDoc)
if (!contractSnap.exists)
throw new APIError(400, 'Contract not found.')
const contract = contractSnap.data() as CPMMContract
const contractDoc = firestore.doc(`contracts/${contractId}`)
const contractSnap = await trans.get(contractDoc)
if (!contractSnap.exists) throw new APIError(400, 'Contract not found.')
const contract = contractSnap.data() as CPMMContract
const liquidityCollection = firestore.collection(
`contracts/${contractId}/liquidity`
)
const liquidityCollection = firestore.collection(
`contracts/${contractId}/liquidity`
)
const liquiditiesSnap = await trans.get(liquidityCollection)
const liquiditiesSnap = await trans.get(liquidityCollection)
const liquidities = liquiditiesSnap.docs.map(
(doc) => doc.data() as LiquidityProvision
)
const liquidities = liquiditiesSnap.docs.map(
(doc) => doc.data() as LiquidityProvision
)
const userShares = getUserLiquidityShares(
userId,
contract,
liquidities
)
const userShares = getUserLiquidityShares(auth.uid, contract, liquidities)
// zero all added amounts for now
// can add support for partial withdrawals in the future
liquiditiesSnap.docs
.filter(
(_, i) =>
!liquidities[i].isAnte && liquidities[i].userId === userId
)
.forEach((doc) => trans.update(doc.ref, { amount: 0 }))
// zero all added amounts for now
// can add support for partial withdrawals in the future
liquiditiesSnap.docs
.filter(
(_, i) => !liquidities[i].isAnte && liquidities[i].userId === auth.uid
)
.forEach((doc) => trans.update(doc.ref, { amount: 0 }))
const payout = Math.min(...Object.values(userShares))
if (payout <= 0) return {}
const payout = Math.min(...Object.values(userShares))
if (payout <= 0) return {}
const newBalance = lp.balance + payout
const newTotalDeposits = lp.totalDeposits + payout
trans.update(lpDoc, {
balance: newBalance,
totalDeposits: newTotalDeposits,
} as Partial<User>)
const newBalance = lp.balance + payout
const newTotalDeposits = lp.totalDeposits + payout
trans.update(lpDoc, {
balance: newBalance,
totalDeposits: newTotalDeposits,
} as Partial<User>)
const newPool = subtractObjects(contract.pool, userShares)
const newPool = subtractObjects(contract.pool, userShares)
const minPoolShares = Math.min(...Object.values(newPool))
const adjustedTotal = contract.totalLiquidity - payout
const minPoolShares = Math.min(...Object.values(newPool))
const adjustedTotal = contract.totalLiquidity - payout
// total liquidity is a bogus number; use minPoolShares to prevent from going negative
const newTotalLiquidity = Math.max(adjustedTotal, minPoolShares)
// total liquidity is a bogus number; use minPoolShares to prevent from going negative
const newTotalLiquidity = Math.max(adjustedTotal, minPoolShares)
trans.update(contractDoc, {
pool: newPool,
totalLiquidity: newTotalLiquidity,
})
trans.update(contractDoc, {
pool: newPool,
totalLiquidity: newTotalLiquidity,
})
const prob = getProbability(contract)
const prob = getProbability(contract)
// surplus shares become user's bets
const bets = Object.entries(userShares)
.map(([outcome, shares]) =>
shares - payout < 1 // don't create bet if less than 1 share
? undefined
: ({
userId: userId,
contractId: contract.id,
amount:
(outcome === 'YES' ? prob : 1 - prob) * (shares - payout),
shares: shares - payout,
outcome,
probBefore: prob,
probAfter: prob,
createdTime: Date.now(),
isLiquidityProvision: true,
fees: noFees,
} as Omit<Bet, 'id'>)
)
.filter((x) => x !== undefined)
// surplus shares become user's bets
const bets = Object.entries(userShares)
.map(([outcome, shares]) =>
shares - payout < 1 // don't create bet if less than 1 share
? undefined
: ({
userId: auth.uid,
contractId: contract.id,
amount:
(outcome === 'YES' ? prob : 1 - prob) * (shares - payout),
shares: shares - payout,
outcome,
probBefore: prob,
probAfter: prob,
createdTime: Date.now(),
isLiquidityProvision: true,
fees: noFees,
} as Omit<Bet, 'id'>)
)
.filter((x) => x !== undefined)
for (const bet of bets) {
const doc = firestore
.collection(`contracts/${contract.id}/bets`)
.doc()
trans.create(doc, { id: doc.id, ...bet })
}
for (const bet of bets) {
const doc = firestore.collection(`contracts/${contract.id}/bets`).doc()
trans.create(doc, { id: doc.id, ...bet })
}
return userShares
})
.then(async (result) => {
// redeem surplus bet with pre-existing bets
await redeemShares(userId, contractId)
console.log('userid', userId, 'withdraws', result)
return { status: 'success', userShares: result }
})
.catch((e) => {
return { status: 'error', message: e.message }
})
}
)
return userShares
})
.then(async (result) => {
// redeem surplus bet with pre-existing bets
await redeemShares(auth.uid, contractId)
console.log('userid', auth.uid, 'withdraws', result)
return result
})
})
const firestore = admin.firestore()

View File

@ -4,7 +4,7 @@ import { useEffect, useState } from 'react'
import { CPMMContract } from 'common/contract'
import { formatMoney } from 'common/util/format'
import { useUser } from 'web/hooks/use-user'
import { addLiquidity, withdrawLiquidity } from 'web/lib/firebase/fn-call'
import { addLiquidity, withdrawLiquidity } from 'web/lib/firebase/api-call'
import { AmountInput } from './amount-input'
import { Row } from './layout/row'
import { useUserLiquidity } from 'web/hooks/use-liquidity'
@ -90,14 +90,10 @@ function AddLiquidityPanel(props: { contract: CPMMContract }) {
setIsSuccess(false)
addLiquidity({ amount, contractId })
.then((r) => {
if (r.status === 'success') {
setIsSuccess(true)
setError(undefined)
setIsLoading(false)
} else {
setError('Server error')
}
.then((_) => {
setIsSuccess(true)
setError(undefined)
setIsLoading(false)
})
.catch((_) => setError('Server error'))

View File

@ -54,6 +54,14 @@ export function changeUserInfo(params: any) {
return call(getFunctionUrl('changeuserinfo'), 'POST', params)
}
export function addLiquidity(params: any) {
return call(getFunctionUrl('addliquidity'), 'POST', params)
}
export function withdrawLiquidity(params: any) {
return call(getFunctionUrl('withdrawliquidity'), 'POST', params)
}
export function createMarket(params: any) {
return call(getFunctionUrl('createmarket'), 'POST', params)
}

View File

@ -9,11 +9,6 @@ import { safeLocalStorage } from '../util/local'
export const cloudFunction = <RequestData, ResponseData>(name: string) =>
httpsCallable<RequestData, ResponseData>(functions, name)
export const withdrawLiquidity = cloudFunction<
{ contractId: string },
{ status: 'error' | 'success'; userShares: { [outcome: string]: number } }
>('withdrawLiquidity')
export const transact = cloudFunction<
Omit<Txn, 'id' | 'createdTime'>,
{ status: 'error' | 'success'; message?: string; txn?: Txn }
@ -42,12 +37,6 @@ export const createUser: () => Promise<User | null> = () => {
.catch(() => null)
}
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 const claimManalink = cloudFunction<
string,
{ status: 'error' | 'success'; message?: string }