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
This commit is contained in:
parent
1af0740f10
commit
004969aa66
35
common/add-liquidity.ts
Normal file
35
common/add-liquidity.ts
Normal file
|
@ -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<CPMM, Binary>,
|
||||||
|
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 }
|
||||||
|
}
|
103
functions/src/add-liquidity.ts
Normal file
103
functions/src/add-liquidity.ts
Normal file
|
@ -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()
|
|
@ -21,3 +21,4 @@ export * from './update-user-metrics'
|
||||||
export * from './backup-db'
|
export * from './backup-db'
|
||||||
export * from './change-user-info'
|
export * from './change-user-info'
|
||||||
export * from './market-close-emails'
|
export * from './market-close-emails'
|
||||||
|
export * from './add-liquidity'
|
||||||
|
|
85
web/components/add-liquidity-panel.tsx
Normal file
85
web/components/add-liquidity-panel.tsx
Normal file
|
@ -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<number | undefined>(undefined)
|
||||||
|
const [error, setError] = useState<string | undefined>(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 (
|
||||||
|
<>
|
||||||
|
<div>Subsidize this market by adding liquidity for traders.</div>
|
||||||
|
|
||||||
|
<Row>
|
||||||
|
<AmountInput
|
||||||
|
amount={amount}
|
||||||
|
onChange={onAmountChange}
|
||||||
|
label="M$"
|
||||||
|
error={error}
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className={clsx('btn btn-primary ml-2', isLoading && 'btn-disabled')}
|
||||||
|
onClick={submit}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
{isSuccess && amount && (
|
||||||
|
<div>Success! Added {formatMoney(amount)} in liquidity.</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isLoading && <div>Processing...</div>}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -11,6 +11,7 @@ import {
|
||||||
contractPath,
|
contractPath,
|
||||||
getBinaryProbPercent,
|
getBinaryProbPercent,
|
||||||
} from '../../lib/firebase/contracts'
|
} from '../../lib/firebase/contracts'
|
||||||
|
import { AddLiquidityPanel } from '../add-liquidity-panel'
|
||||||
import { CopyLinkButton } from '../copy-link-button'
|
import { CopyLinkButton } from '../copy-link-button'
|
||||||
import { Col } from '../layout/col'
|
import { Col } from '../layout/col'
|
||||||
import { Modal } from '../layout/modal'
|
import { Modal } from '../layout/modal'
|
||||||
|
@ -110,8 +111,16 @@ export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) {
|
||||||
|
|
||||||
<div className="text-gray-500">Tags</div>
|
<div className="text-gray-500">Tags</div>
|
||||||
<TagsInput contract={contract} />
|
<TagsInput contract={contract} />
|
||||||
|
|
||||||
<div />
|
<div />
|
||||||
|
|
||||||
|
{contract.mechanism === 'cpmm-1' &&
|
||||||
|
!contract.resolution &&
|
||||||
|
(!closeTime || closeTime > Date.now()) && (
|
||||||
|
<>
|
||||||
|
<div className="text-gray-500">Add liquidity</div>
|
||||||
|
<AddLiquidityPanel contract={contract} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
</Modal>
|
</Modal>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -66,3 +66,9 @@ export const changeUserInfo = (data: {
|
||||||
.then((r) => r.data as { status: string; message?: string })
|
.then((r) => r.data as { status: string; message?: string })
|
||||||
.catch((e) => ({ status: 'error', message: e.message }))
|
.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 }))
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user