From 004969aa66443caf5f78b5db0955250059bb184c Mon Sep 17 00:00:00 2001 From: mantikoros <95266179+mantikoros@users.noreply.github.com> Date: Thu, 21 Apr 2022 12:58:12 -0500 Subject: [PATCH] user-added liquidity provision panel (#90) * user-added liquidity provision panel * AddLiquidityPanel: handle loading, errors * ContractInfoDialog: don't show add liquidity when market is closed * ContractInfoDialog: hide add liquidity for FR --- common/add-liquidity.ts | 35 ++++++ functions/src/add-liquidity.ts | 103 ++++++++++++++++++ functions/src/index.ts | 1 + web/components/add-liquidity-panel.tsx | 85 +++++++++++++++ .../contract/contract-info-dialog.tsx | 11 +- web/lib/firebase/api-call.ts | 6 + 6 files changed, 240 insertions(+), 1 deletion(-) create mode 100644 common/add-liquidity.ts create mode 100644 functions/src/add-liquidity.ts create mode 100644 web/components/add-liquidity-panel.tsx diff --git a/common/add-liquidity.ts b/common/add-liquidity.ts new file mode 100644 index 00000000..573b609d --- /dev/null +++ b/common/add-liquidity.ts @@ -0,0 +1,35 @@ +import { addCpmmLiquidity, getCpmmLiquidity } from './calculate-cpmm' +import { Binary, CPMM, FullContract } from './contract' +import { LiquidityProvision } from './liquidity-provision' +import { User } from './user' + +export const getNewLiquidityProvision = ( + user: User, + amount: number, + contract: FullContract, + newLiquidityProvisionId: string +) => { + const { pool, p, totalLiquidity } = contract + + const { newPool, newP } = addCpmmLiquidity(pool, p, amount) + + const liquidity = + getCpmmLiquidity(newPool, newP) - getCpmmLiquidity(pool, newP) + + const newLiquidityProvision: LiquidityProvision = { + id: newLiquidityProvisionId, + userId: user.id, + contractId: contract.id, + amount, + pool: newPool, + p: newP, + liquidity, + createdTime: Date.now(), + } + + const newTotalLiquidity = (totalLiquidity ?? 0) + amount + + const newBalance = user.balance - amount + + return { newLiquidityProvision, newPool, newP, newBalance, newTotalLiquidity } +} diff --git a/functions/src/add-liquidity.ts b/functions/src/add-liquidity.ts new file mode 100644 index 00000000..e37804d3 --- /dev/null +++ b/functions/src/add-liquidity.ts @@ -0,0 +1,103 @@ +import * as functions from 'firebase-functions' +import * as admin from 'firebase-admin' + +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' + +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 { amount, contractId } = data + + if (amount <= 0 || isNaN(amount) || !isFinite(amount)) + return { status: 'error', message: '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 + + 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' + ) + return { status: 'error', message: 'Invalid contract' } + + const { closeTime } = contract + if (closeTime && Date.now() > closeTime) + return { status: 'error', message: 'Trading is closed' } + + if (user.balance < amount) + return { status: 'error', message: 'Insufficient balance' } + + const newLiquidityProvisionDoc = firestore + .collection(`contracts/${contractId}/liquidity`) + .doc() + + const { + newLiquidityProvision, + newPool, + newP, + newBalance, + 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, + }) + ) + + if (!isFinite(newBalance)) { + throw new Error('Invalid user balance for ' + user.username) + } + + transaction.update(userDoc, { balance: newBalance }) + + transaction.create(newLiquidityProvisionDoc, newLiquidityProvision) + + return { status: 'success', newLiquidityProvision } + }) + .then(async (result) => { + await redeemShares(userId, contractId) + return result + }) + } +) + +const firestore = admin.firestore() diff --git a/functions/src/index.ts b/functions/src/index.ts index dedf42a1..19a4a054 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -21,3 +21,4 @@ export * from './update-user-metrics' export * from './backup-db' export * from './change-user-info' export * from './market-close-emails' +export * from './add-liquidity' diff --git a/web/components/add-liquidity-panel.tsx b/web/components/add-liquidity-panel.tsx new file mode 100644 index 00000000..4b2ed4c1 --- /dev/null +++ b/web/components/add-liquidity-panel.tsx @@ -0,0 +1,85 @@ +import clsx from 'clsx' +import { useState } from 'react' + +import { Contract } from '../../common/contract' +import { formatMoney } from '../../common/util/format' +import { useUser } from '../hooks/use-user' +import { addLiquidity } from '../lib/firebase/api-call' +import { AmountInput } from './amount-input' +import { Row } from './layout/row' + +export function AddLiquidityPanel(props: { contract: Contract }) { + const { contract } = props + const { id: contractId } = contract + + const user = useUser() + + const [amount, setAmount] = useState(undefined) + const [error, setError] = useState(undefined) + const [isSuccess, setIsSuccess] = useState(false) + const [isLoading, setIsLoading] = useState(false) + + const onAmountChange = (amount: number | undefined) => { + setIsSuccess(false) + setAmount(amount) + + // Check for errors. + if (amount !== undefined) { + if (user && user.balance < amount) { + setError('Insufficient balance') + } else if (amount < 1) { + setError('Minimum amount: ' + formatMoney(1)) + } else { + setError(undefined) + } + } + } + + const submit = () => { + if (!amount) return + + setIsLoading(true) + setIsSuccess(false) + + addLiquidity({ amount, contractId }) + .then((r) => { + if (r.status === 'success') { + setIsSuccess(true) + setError(undefined) + setIsLoading(false) + } else { + setError('Server error') + } + }) + .catch((e) => setError('Server error')) + } + + return ( + <> +
Subsidize this market by adding liquidity for traders.
+ + + + + + + {isSuccess && amount && ( +
Success! Added {formatMoney(amount)} in liquidity.
+ )} + + {isLoading &&
Processing...
} + + ) +} diff --git a/web/components/contract/contract-info-dialog.tsx b/web/components/contract/contract-info-dialog.tsx index a8d6a1aa..79ec45ed 100644 --- a/web/components/contract/contract-info-dialog.tsx +++ b/web/components/contract/contract-info-dialog.tsx @@ -11,6 +11,7 @@ import { contractPath, getBinaryProbPercent, } from '../../lib/firebase/contracts' +import { AddLiquidityPanel } from '../add-liquidity-panel' import { CopyLinkButton } from '../copy-link-button' import { Col } from '../layout/col' import { Modal } from '../layout/modal' @@ -110,8 +111,16 @@ export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) {
Tags
-
+ + {contract.mechanism === 'cpmm-1' && + !contract.resolution && + (!closeTime || closeTime > Date.now()) && ( + <> +
Add liquidity
+ + + )} diff --git a/web/lib/firebase/api-call.ts b/web/lib/firebase/api-call.ts index 6236cd64..cca726bc 100644 --- a/web/lib/firebase/api-call.ts +++ b/web/lib/firebase/api-call.ts @@ -66,3 +66,9 @@ export const 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 })) +}