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

View File

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

View File

@ -1,5 +1,5 @@
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin' import * as admin from 'firebase-admin'
import { z } from 'zod'
import { CPMMContract } from '../../common/contract' import { CPMMContract } from '../../common/contract'
import { User } from '../../common/user' import { User } from '../../common/user'
@ -10,36 +10,26 @@ import { Bet } from '../../common/bet'
import { getProbability } from '../../common/calculate' import { getProbability } from '../../common/calculate'
import { noFees } from '../../common/fees' import { noFees } from '../../common/fees'
import { APIError } from './api' import { APIError, newEndpoint, validate } from './api'
import { redeemShares } from './redeem-shares' import { redeemShares } from './redeem-shares'
export const withdrawLiquidity = functions const bodySchema = z.object({
.runWith({ minInstances: 1 }) contractId: z.string(),
.https.onCall( })
async (
data: {
contractId: string
},
context
) => {
const userId = context?.auth?.uid
if (!userId) return { status: 'error', message: 'Not authorized' }
const { contractId } = data export const withdrawliquidity = newEndpoint({}, async (req, auth) => {
if (!contractId) const { contractId } = validate(bodySchema, req.body)
return { status: 'error', message: 'Missing contract id' }
return await firestore return await firestore
.runTransaction(async (trans) => { .runTransaction(async (trans) => {
const lpDoc = firestore.doc(`users/${userId}`) const lpDoc = firestore.doc(`users/${auth.uid}`)
const lpSnap = await trans.get(lpDoc) const lpSnap = await trans.get(lpDoc)
if (!lpSnap.exists) throw new APIError(400, 'User not found.') if (!lpSnap.exists) throw new APIError(400, 'User not found.')
const lp = lpSnap.data() as User const lp = lpSnap.data() as User
const contractDoc = firestore.doc(`contracts/${contractId}`) const contractDoc = firestore.doc(`contracts/${contractId}`)
const contractSnap = await trans.get(contractDoc) const contractSnap = await trans.get(contractDoc)
if (!contractSnap.exists) if (!contractSnap.exists) throw new APIError(400, 'Contract not found.')
throw new APIError(400, 'Contract not found.')
const contract = contractSnap.data() as CPMMContract const contract = contractSnap.data() as CPMMContract
const liquidityCollection = firestore.collection( const liquidityCollection = firestore.collection(
@ -52,18 +42,13 @@ export const withdrawLiquidity = functions
(doc) => doc.data() as LiquidityProvision (doc) => doc.data() as LiquidityProvision
) )
const userShares = getUserLiquidityShares( const userShares = getUserLiquidityShares(auth.uid, contract, liquidities)
userId,
contract,
liquidities
)
// zero all added amounts for now // zero all added amounts for now
// can add support for partial withdrawals in the future // can add support for partial withdrawals in the future
liquiditiesSnap.docs liquiditiesSnap.docs
.filter( .filter(
(_, i) => (_, i) => !liquidities[i].isAnte && liquidities[i].userId === auth.uid
!liquidities[i].isAnte && liquidities[i].userId === userId
) )
.forEach((doc) => trans.update(doc.ref, { amount: 0 })) .forEach((doc) => trans.update(doc.ref, { amount: 0 }))
@ -98,7 +83,7 @@ export const withdrawLiquidity = functions
shares - payout < 1 // don't create bet if less than 1 share shares - payout < 1 // don't create bet if less than 1 share
? undefined ? undefined
: ({ : ({
userId: userId, userId: auth.uid,
contractId: contract.id, contractId: contract.id,
amount: amount:
(outcome === 'YES' ? prob : 1 - prob) * (shares - payout), (outcome === 'YES' ? prob : 1 - prob) * (shares - payout),
@ -114,9 +99,7 @@ export const withdrawLiquidity = functions
.filter((x) => x !== undefined) .filter((x) => x !== undefined)
for (const bet of bets) { for (const bet of bets) {
const doc = firestore const doc = firestore.collection(`contracts/${contract.id}/bets`).doc()
.collection(`contracts/${contract.id}/bets`)
.doc()
trans.create(doc, { id: doc.id, ...bet }) trans.create(doc, { id: doc.id, ...bet })
} }
@ -124,15 +107,10 @@ export const withdrawLiquidity = functions
}) })
.then(async (result) => { .then(async (result) => {
// redeem surplus bet with pre-existing bets // redeem surplus bet with pre-existing bets
await redeemShares(userId, contractId) await redeemShares(auth.uid, contractId)
console.log('userid', auth.uid, 'withdraws', result)
console.log('userid', userId, 'withdraws', result) return result
return { status: 'success', userShares: result }
}) })
.catch((e) => { })
return { status: 'error', message: e.message }
})
}
)
const firestore = admin.firestore() const firestore = admin.firestore()

View File

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

View File

@ -54,6 +54,14 @@ export function changeUserInfo(params: any) {
return call(getFunctionUrl('changeuserinfo'), 'POST', params) 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) { export function createMarket(params: any) {
return call(getFunctionUrl('createmarket'), 'POST', params) return call(getFunctionUrl('createmarket'), 'POST', params)
} }

View File

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