From 0ec15ff2f8cf2cfd89ea286575e81f2d57ad75b4 Mon Sep 17 00:00:00 2001 From: mantikoros <95266179+mantikoros@users.noreply.github.com> Date: Mon, 10 Oct 2022 21:56:16 -0500 Subject: [PATCH] Make liquidity great again (#1020) * add subsidy * drizzle liquidity * update liquidity panel * remove addliquidity * update cloud functions index * remove json endpoints * imports * drizzle liquidity: add velocity; dev script; run every minute * adjust speed * logging * liquidity button, dialog * modal size * modal * info table * pay back excess liquidity * remove client withdrawal * house liquidity subsidy * disable liquidity button if market resolved or closed * format tip amount --- common/add-liquidity.ts | 16 +- common/calculate-cpmm.ts | 45 +--- common/contract.ts | 3 +- common/economy.ts | 2 + common/new-contract.ts | 1 + common/payouts-fixed.ts | 13 +- common/util/format.ts | 10 + .../src/{add-liquidity.ts => add-subsidy.ts} | 69 +---- functions/src/drizzle-liquidity.ts | 69 +++++ functions/src/helpers/add-house-subsidy.ts | 42 +++ functions/src/index.ts | 13 +- functions/src/on-create-bet.ts | 15 +- functions/src/resolve-market.ts | 11 +- functions/src/scripts/drizzle.ts | 8 + functions/src/serve.ts | 4 - functions/src/withdraw-liquidity.ts | 121 --------- .../contract/contract-info-dialog.tsx | 22 +- .../contract/extra-contract-actions-row.tsx | 4 + .../contract/liquidity-bounty-panel.tsx | 248 ------------------ web/components/contract/liquidity-button.tsx | 92 +++++++ web/components/contract/liquidity-modal.tsx | 108 ++++++++ web/components/contract/tip-button.tsx | 9 +- web/hooks/use-liquidity.ts | 7 +- web/lib/firebase/api.ts | 8 +- 24 files changed, 421 insertions(+), 519 deletions(-) rename functions/src/{add-liquidity.ts => add-subsidy.ts} (55%) create mode 100644 functions/src/drizzle-liquidity.ts create mode 100644 functions/src/helpers/add-house-subsidy.ts create mode 100644 functions/src/scripts/drizzle.ts delete mode 100644 functions/src/withdraw-liquidity.ts delete mode 100644 web/components/contract/liquidity-bounty-panel.tsx create mode 100644 web/components/contract/liquidity-button.tsx create mode 100644 web/components/contract/liquidity-modal.tsx diff --git a/common/add-liquidity.ts b/common/add-liquidity.ts index 9271bbbf..47b3c1e9 100644 --- a/common/add-liquidity.ts +++ b/common/add-liquidity.ts @@ -1,4 +1,4 @@ -import { addCpmmLiquidity, getCpmmLiquidity } from './calculate-cpmm' +import { getCpmmLiquidity } from './calculate-cpmm' import { CPMMContract } from './contract' import { LiquidityProvision } from './liquidity-provision' @@ -8,25 +8,23 @@ export const getNewLiquidityProvision = ( contract: CPMMContract, newLiquidityProvisionId: string ) => { - const { pool, p, totalLiquidity } = contract + const { pool, p, totalLiquidity, subsidyPool } = contract - const { newPool, newP } = addCpmmLiquidity(pool, p, amount) - - const liquidity = - getCpmmLiquidity(newPool, newP) - getCpmmLiquidity(pool, newP) + const liquidity = getCpmmLiquidity(pool, p) const newLiquidityProvision: LiquidityProvision = { id: newLiquidityProvisionId, userId: userId, contractId: contract.id, amount, - pool: newPool, - p: newP, + pool, + p, liquidity, createdTime: Date.now(), } const newTotalLiquidity = (totalLiquidity ?? 0) + amount + const newSubsidyPool = (subsidyPool ?? 0) + amount - return { newLiquidityProvision, newPool, newP, newTotalLiquidity } + return { newLiquidityProvision, newTotalLiquidity, newSubsidyPool } } diff --git a/common/calculate-cpmm.ts b/common/calculate-cpmm.ts index 346fca79..ab8aabbe 100644 --- a/common/calculate-cpmm.ts +++ b/common/calculate-cpmm.ts @@ -1,11 +1,10 @@ -import { sum, groupBy, mapValues, sumBy } from 'lodash' +import { groupBy, mapValues, sumBy } from 'lodash' import { LimitBet } from './bet' import { CREATOR_FEE, Fees, LIQUIDITY_FEE, PLATFORM_FEE } from './fees' import { LiquidityProvision } from './liquidity-provision' import { computeFills } from './new-bet' import { binarySearch } from './util/algos' -import { addObjects } from './util/object' export type CpmmState = { pool: { [outcome: string]: number } @@ -267,48 +266,22 @@ export function addCpmmLiquidity( return { newPool, liquidity, newP } } -const calculateLiquidityDelta = (p: number) => (l: LiquidityProvision) => { - const oldLiquidity = getCpmmLiquidity(l.pool, p) +export function getCpmmLiquidityPoolWeights(liquidities: LiquidityProvision[]) { + const userAmounts = groupBy(liquidities, (w) => w.userId) + const totalAmount = sumBy(liquidities, (w) => w.amount) - const newPool = addObjects(l.pool, { YES: l.amount, NO: l.amount }) - const newLiquidity = getCpmmLiquidity(newPool, p) - - const liquidity = newLiquidity - oldLiquidity - return liquidity -} - -export function getCpmmLiquidityPoolWeights( - state: CpmmState, - liquidities: LiquidityProvision[], - excludeAntes: boolean -) { - const calcLiqudity = calculateLiquidityDelta(state.p) - const liquidityShares = liquidities.map(calcLiqudity) - const shareSum = sum(liquidityShares) - - const weights = liquidityShares.map((shares, i) => ({ - weight: shares / shareSum, - providerId: liquidities[i].userId, - })) - - const includedWeights = excludeAntes - ? weights.filter((_, i) => !liquidities[i].isAnte) - : weights - - const userWeights = groupBy(includedWeights, (w) => w.providerId) - const totalUserWeights = mapValues(userWeights, (userWeight) => - sumBy(userWeight, (w) => w.weight) + return mapValues( + userAmounts, + (amounts) => sumBy(amounts, (w) => w.amount) / totalAmount ) - return totalUserWeights } export function getUserLiquidityShares( userId: string, state: CpmmState, - liquidities: LiquidityProvision[], - excludeAntes: boolean + liquidities: LiquidityProvision[] ) { - const weights = getCpmmLiquidityPoolWeights(state, liquidities, excludeAntes) + const weights = getCpmmLiquidityPoolWeights(liquidities) const userWeight = weights[userId] ?? 0 return mapValues(state.pool, (shares) => userWeight * shares) diff --git a/common/contract.ts b/common/contract.ts index 2656b5d5..dd2aa70a 100644 --- a/common/contract.ts +++ b/common/contract.ts @@ -91,7 +91,8 @@ export type CPMM = { mechanism: 'cpmm-1' pool: { [outcome: string]: number } p: number // probability constant in y^p * n^(1-p) = k - totalLiquidity: number // in M$ + totalLiquidity: number // for historical reasons, this the total subsidy amount added in M$ + subsidyPool: number // current value of subsidy pool in M$ prob: number probChanges: { day: number diff --git a/common/economy.ts b/common/economy.ts index d9a9433a..c828b0d3 100644 --- a/common/economy.ts +++ b/common/economy.ts @@ -16,3 +16,5 @@ export const BETTING_STREAK_BONUS_MAX = econ?.BETTING_STREAK_BONUS_MAX ?? 25 export const BETTING_STREAK_RESET_HOUR = econ?.BETTING_STREAK_RESET_HOUR ?? 7 export const FREE_MARKETS_PER_USER_MAX = econ?.FREE_MARKETS_PER_USER_MAX ?? 5 export const COMMENT_BOUNTY_AMOUNT = econ?.COMMENT_BOUNTY_AMOUNT ?? 250 + +export const UNIQUE_BETTOR_LIQUIDITY = 20 diff --git a/common/new-contract.ts b/common/new-contract.ts index 89afb0c0..6a7f57da 100644 --- a/common/new-contract.ts +++ b/common/new-contract.ts @@ -112,6 +112,7 @@ const getBinaryCpmmProps = (initialProb: number, ante: number) => { mechanism: 'cpmm-1', outcomeType: 'BINARY', totalLiquidity: ante, + subsidyPool: 0, initialProbability: p, p, pool: pool, diff --git a/common/payouts-fixed.ts b/common/payouts-fixed.ts index 99e03fac..74e9fe16 100644 --- a/common/payouts-fixed.ts +++ b/common/payouts-fixed.ts @@ -1,4 +1,3 @@ - import { Bet } from './bet' import { getProbability } from './calculate' import { getCpmmLiquidityPoolWeights } from './calculate-cpmm' @@ -56,10 +55,10 @@ export const getLiquidityPoolPayouts = ( outcome: string, liquidities: LiquidityProvision[] ) => { - const { pool } = contract - const finalPool = pool[outcome] + const { pool, subsidyPool } = contract + const finalPool = pool[outcome] + subsidyPool - const weights = getCpmmLiquidityPoolWeights(contract, liquidities, false) + const weights = getCpmmLiquidityPoolWeights(liquidities) return Object.entries(weights).map(([providerId, weight]) => ({ userId: providerId, @@ -95,10 +94,10 @@ export const getLiquidityPoolProbPayouts = ( p: number, liquidities: LiquidityProvision[] ) => { - const { pool } = contract - const finalPool = p * pool.YES + (1 - p) * pool.NO + const { pool, subsidyPool } = contract + const finalPool = p * pool.YES + (1 - p) * pool.NO + subsidyPool - const weights = getCpmmLiquidityPoolWeights(contract, liquidities, false) + const weights = getCpmmLiquidityPoolWeights(liquidities) return Object.entries(weights).map(([providerId, weight]) => ({ userId: providerId, diff --git a/common/util/format.ts b/common/util/format.ts index fd3e7551..ce7849aa 100644 --- a/common/util/format.ts +++ b/common/util/format.ts @@ -60,6 +60,16 @@ export function formatLargeNumber(num: number, sigfigs = 2): string { return `${numStr}${suffix[i] ?? ''}` } +export function shortFormatNumber(num: number): string { + if (num < 1000) return showPrecision(num, 3) + + const suffix = ['', 'K', 'M', 'B', 'T', 'Q'] + const i = Math.floor(Math.log10(num) / 3) + + const numStr = showPrecision(num / Math.pow(10, 3 * i), 2) + return `${numStr}${suffix[i] ?? ''}` +} + export function toCamelCase(words: string) { const camelCase = words .split(' ') diff --git a/functions/src/add-liquidity.ts b/functions/src/add-subsidy.ts similarity index 55% rename from functions/src/add-liquidity.ts rename to functions/src/add-subsidy.ts index e6090111..b3ed1895 100644 --- a/functions/src/add-liquidity.ts +++ b/functions/src/add-subsidy.ts @@ -3,24 +3,18 @@ import { z } from 'zod' import { Contract, CPMMContract } from '../../common/contract' import { User } from '../../common/user' -import { removeUndefinedProps } from '../../common/util/object' import { getNewLiquidityProvision } from '../../common/add-liquidity' import { APIError, newEndpoint, validate } from './api' -import { - DEV_HOUSE_LIQUIDITY_PROVIDER_ID, - HOUSE_LIQUIDITY_PROVIDER_ID, -} from '../../common/antes' -import { isProd } from './utils' const bodySchema = z.object({ contractId: z.string(), amount: z.number().gt(0), }) -export const addliquidity = newEndpoint({}, async (req, auth) => { +export const addsubsidy = newEndpoint({}, async (req, auth) => { const { amount, contractId } = validate(bodySchema, req.body) - if (!isFinite(amount)) throw new APIError(400, 'Invalid amount') + if (!isFinite(amount) || amount < 1) throw new APIError(400, 'Invalid amount') // run as transaction to prevent race conditions return await firestore.runTransaction(async (transaction) => { @@ -50,7 +44,7 @@ export const addliquidity = newEndpoint({}, async (req, auth) => { .collection(`contracts/${contractId}/liquidity`) .doc() - const { newLiquidityProvision, newPool, newP, newTotalLiquidity } = + const { newLiquidityProvision, newTotalLiquidity, newSubsidyPool } = getNewLiquidityProvision( user.id, amount, @@ -58,21 +52,10 @@ export const addliquidity = newEndpoint({}, async (req, auth) => { 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, - }) - ) + transaction.update(contractDoc, { + subsidyPool: newSubsidyPool, + totalLiquidity: newTotalLiquidity, + } as Partial) const newBalance = user.balance - amount const newTotalDeposits = user.totalDeposits - amount @@ -93,41 +76,3 @@ export const addliquidity = newEndpoint({}, async (req, auth) => { }) const firestore = admin.firestore() - -export const addHouseLiquidity = (contract: CPMMContract, amount: number) => { - return firestore.runTransaction(async (transaction) => { - const newLiquidityProvisionDoc = firestore - .collection(`contracts/${contract.id}/liquidity`) - .doc() - - const providerId = isProd() - ? HOUSE_LIQUIDITY_PROVIDER_ID - : DEV_HOUSE_LIQUIDITY_PROVIDER_ID - - const { newLiquidityProvision, newPool, newP, newTotalLiquidity } = - getNewLiquidityProvision( - providerId, - amount, - contract, - newLiquidityProvisionDoc.id - ) - - if (newP !== undefined && !isFinite(newP)) { - throw new APIError( - 500, - 'Liquidity injection rejected due to overflow error.' - ) - } - - transaction.update( - firestore.doc(`contracts/${contract.id}`), - removeUndefinedProps({ - pool: newPool, - p: newP, - totalLiquidity: newTotalLiquidity, - }) - ) - - transaction.create(newLiquidityProvisionDoc, newLiquidityProvision) - }) -} diff --git a/functions/src/drizzle-liquidity.ts b/functions/src/drizzle-liquidity.ts new file mode 100644 index 00000000..7757dee0 --- /dev/null +++ b/functions/src/drizzle-liquidity.ts @@ -0,0 +1,69 @@ +import * as functions from 'firebase-functions' +import * as admin from 'firebase-admin' + +import { CPMMContract } from '../../common/contract' +import { batchedWaitAll } from '../../common/util/promise' +import { APIError } from '../../common/api' +import { addCpmmLiquidity } from '../../common/calculate-cpmm' +import { formatMoneyWithDecimals } from '../../common/util/format' + +const firestore = admin.firestore() + +export const drizzleLiquidity = async () => { + const snap = await firestore + .collection('contracts') + .where('subsidyPool', '>', 1e-7) + .get() + + const contractIds = snap.docs.map((doc) => doc.id) + console.log('found', contractIds.length, 'markets to drizzle') + console.log() + + await batchedWaitAll( + contractIds.map((cid) => () => drizzleMarket(cid)), + 10 + ) +} + +export const drizzleLiquidityScheduler = functions.pubsub + .schedule('* * * * *') // every minute + .onRun(drizzleLiquidity) + +const drizzleMarket = async (contractId: string) => { + await firestore.runTransaction(async (trans) => { + const snap = await trans.get(firestore.doc(`contracts/${contractId}`)) + const contract = snap.data() as CPMMContract + const { subsidyPool, pool, p, slug, popularityScore } = contract + if ((subsidyPool ?? 0) < 1e-7) return + + const r = Math.random() + const logPopularity = Math.log10((popularityScore ?? 0) + 1) + const v = Math.max(1, Math.min(5, logPopularity)) + const amount = subsidyPool <= 0.5 ? subsidyPool : r * v * 0.01 * subsidyPool + + const { newPool, newP } = addCpmmLiquidity(pool, p, amount) + + if (!isFinite(newP)) { + throw new APIError( + 500, + 'Liquidity injection rejected due to overflow error.' + ) + } + + await trans.update(firestore.doc(`contracts/${contract.id}`), { + pool: newPool, + p: newP, + subsidyPool: subsidyPool - amount, + }) + + console.log( + 'added subsidy', + formatMoneyWithDecimals(amount), + 'of', + formatMoneyWithDecimals(subsidyPool), + 'pool to', + slug + ) + console.log() + }) +} diff --git a/functions/src/helpers/add-house-subsidy.ts b/functions/src/helpers/add-house-subsidy.ts new file mode 100644 index 00000000..afba8bcf --- /dev/null +++ b/functions/src/helpers/add-house-subsidy.ts @@ -0,0 +1,42 @@ +import * as admin from 'firebase-admin' + +import { CPMMContract } from '../../../common/contract' +import { isProd } from '../utils' +import { + DEV_HOUSE_LIQUIDITY_PROVIDER_ID, + HOUSE_LIQUIDITY_PROVIDER_ID, +} from '../../../common/antes' +import { getNewLiquidityProvision } from '../../../common/add-liquidity' + +const firestore = admin.firestore() + +export const addHouseSubsidy = (contractId: string, amount: number) => { + return firestore.runTransaction(async (transaction) => { + const newLiquidityProvisionDoc = firestore + .collection(`contracts/${contractId}/liquidity`) + .doc() + + const providerId = isProd() + ? HOUSE_LIQUIDITY_PROVIDER_ID + : DEV_HOUSE_LIQUIDITY_PROVIDER_ID + + const contractDoc = firestore.doc(`contracts/${contractId}`) + const snap = await contractDoc.get() + const contract = snap.data() as CPMMContract + + const { newLiquidityProvision, newTotalLiquidity, newSubsidyPool } = + getNewLiquidityProvision( + providerId, + amount, + contract, + newLiquidityProvisionDoc.id + ) + + transaction.update(contractDoc, { + subsidyPool: newSubsidyPool, + totalLiquidity: newTotalLiquidity, + } as Partial) + + transaction.create(newLiquidityProvisionDoc, newLiquidityProvision) + }) +} diff --git a/functions/src/index.ts b/functions/src/index.ts index a6d120c8..b64155a3 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -31,6 +31,7 @@ export * from './reset-weekly-emails-flags' export * from './on-update-contract-follow' export * from './on-update-like' export * from './weekly-portfolio-emails' +export * from './drizzle-liquidity' // v2 export * from './health' @@ -44,8 +45,6 @@ export * from './sell-bet' export * from './sell-shares' export * from './claim-manalink' export * from './create-market' -export * from './add-liquidity' -export * from './withdraw-liquidity' export * from './create-group' export * from './resolve-market' export * from './unsubscribe' @@ -53,6 +52,7 @@ export * from './stripe' export * from './mana-bonus-email' export * from './close-market' export * from './update-comment-bounty' +export * from './add-subsidy' import { health } from './health' import { transact } from './transact' @@ -65,9 +65,7 @@ import { sellbet } from './sell-bet' import { sellshares } from './sell-shares' import { claimmanalink } from './claim-manalink' import { createmarket } from './create-market' -import { addliquidity } from './add-liquidity' import { addcommentbounty, awardcommentbounty } from './update-comment-bounty' -import { withdrawliquidity } from './withdraw-liquidity' import { creategroup } from './create-group' import { resolvemarket } from './resolve-market' import { closemarket } from './close-market' @@ -78,6 +76,7 @@ import { acceptchallenge } from './accept-challenge' import { createpost } from './create-post' import { savetwitchcredentials } from './save-twitch-credentials' import { updatemetrics } from './update-metrics' +import { addsubsidy } from './add-subsidy' const toCloudFunction = ({ opts, handler }: EndpointDefinition) => { return onRequest(opts, handler as any) @@ -93,10 +92,9 @@ const sellBetFunction = toCloudFunction(sellbet) const sellSharesFunction = toCloudFunction(sellshares) const claimManalinkFunction = toCloudFunction(claimmanalink) const createMarketFunction = toCloudFunction(createmarket) -const addLiquidityFunction = toCloudFunction(addliquidity) +const addSubsidyFunction = toCloudFunction(addsubsidy) const addCommentBounty = toCloudFunction(addcommentbounty) const awardCommentBounty = toCloudFunction(awardcommentbounty) -const withdrawLiquidityFunction = toCloudFunction(withdrawliquidity) const createGroupFunction = toCloudFunction(creategroup) const resolveMarketFunction = toCloudFunction(resolvemarket) const closeMarketFunction = toCloudFunction(closemarket) @@ -121,8 +119,7 @@ export { sellSharesFunction as sellshares, claimManalinkFunction as claimmanalink, createMarketFunction as createmarket, - addLiquidityFunction as addliquidity, - withdrawLiquidityFunction as withdrawliquidity, + addSubsidyFunction as addsubsidy, createGroupFunction as creategroup, resolveMarketFunction as resolvemarket, closeMarketFunction as closemarket, diff --git a/functions/src/on-create-bet.ts b/functions/src/on-create-bet.ts index fc473b6d..0175a63e 100644 --- a/functions/src/on-create-bet.ts +++ b/functions/src/on-create-bet.ts @@ -25,6 +25,7 @@ import { BETTING_STREAK_BONUS_MAX, BETTING_STREAK_RESET_HOUR, UNIQUE_BETTOR_BONUS_AMOUNT, + UNIQUE_BETTOR_LIQUIDITY, } from '../../common/economy' import { DEV_HOUSE_LIQUIDITY_PROVIDER_ID, @@ -34,6 +35,7 @@ import { APIError } from '../../common/api' import { User } from '../../common/user' import { DAY_MS } from '../../common/util/time' import { BettingStreakBonusTxn, UniqueBettorBonusTxn } from '../../common/txn' +import { addHouseSubsidy } from './helpers/add-house-subsidy' import { StreakerBadge, streakerBadgeRarityThresholds, @@ -108,7 +110,7 @@ const updateBettingStreak = async ( const newBettingStreak = (bettor?.currentBettingStreak ?? 0) + 1 // Otherwise, add 1 to their betting streak - await trans.update(userDoc, { + trans.update(userDoc, { currentBettingStreak: newBettingStreak, lastBetTime: bet.createdTime, }) @@ -198,7 +200,7 @@ const updateUniqueBettorsAndGiveCreatorBonus = async ( log(`Got ${previousUniqueBettorIds} unique bettors`) isNewUniqueBettor && log(`And a new unique bettor ${bettor.id}`) - await trans.update(contractDoc, { + trans.update(contractDoc, { uniqueBettorIds: newUniqueBettorIds, uniqueBettorCount: newUniqueBettorIds.length, }) @@ -211,8 +213,13 @@ const updateUniqueBettorsAndGiveCreatorBonus = async ( return { newUniqueBettorIds } } ) + if (!newUniqueBettorIds) return + if (oldContract.mechanism === 'cpmm-1') { + await addHouseSubsidy(oldContract.id, UNIQUE_BETTOR_LIQUIDITY) + } + const bonusTxnDetails = { contractId: oldContract.id, uniqueNewBettorId: bettor.id, @@ -222,7 +229,9 @@ const updateUniqueBettorsAndGiveCreatorBonus = async ( : DEV_HOUSE_LIQUIDITY_PROVIDER_ID const fromSnap = await firestore.doc(`users/${fromUserId}`).get() if (!fromSnap.exists) throw new APIError(400, 'From user not found.') + const fromUser = fromSnap.data() as User + const result = await firestore.runTransaction(async (trans) => { const bonusTxn: TxnData = { fromId: fromUser.id, @@ -235,7 +244,9 @@ const updateUniqueBettorsAndGiveCreatorBonus = async ( description: JSON.stringify(bonusTxnDetails), data: bonusTxnDetails, } as Omit + const { status, message, txn } = await runTxn(trans, bonusTxn) + return { status, newUniqueBettorIds, message, txn } }) diff --git a/functions/src/resolve-market.ts b/functions/src/resolve-market.ts index ca8f5fc0..4230f0ac 100644 --- a/functions/src/resolve-market.ts +++ b/functions/src/resolve-market.ts @@ -9,7 +9,15 @@ import { RESOLUTIONS, } from '../../common/contract' import { Bet } from '../../common/bet' -import { getContractPath, getUser, getValues, isProd, log, payUser, revalidateStaticProps } from './utils' +import { + getContractPath, + getUser, + getValues, + isProd, + log, + payUser, + revalidateStaticProps, +} from './utils' import { getLoanPayouts, getPayouts, @@ -145,6 +153,7 @@ export const resolvemarket = newEndpoint(opts, async (req, auth) => { resolutions, collectedFees, }), + subsidyPool: 0, } await contractDoc.update(updatedContract) diff --git a/functions/src/scripts/drizzle.ts b/functions/src/scripts/drizzle.ts new file mode 100644 index 00000000..c38b6659 --- /dev/null +++ b/functions/src/scripts/drizzle.ts @@ -0,0 +1,8 @@ +import { initAdmin } from './script-init' +initAdmin() + +import { drizzleLiquidity } from '../drizzle-liquidity' + +if (require.main === module) { + drizzleLiquidity().then(() => process.exit()) +} diff --git a/functions/src/serve.ts b/functions/src/serve.ts index d861dcbc..bc09029d 100644 --- a/functions/src/serve.ts +++ b/functions/src/serve.ts @@ -19,8 +19,6 @@ import { sellbet } from './sell-bet' import { sellshares } from './sell-shares' import { claimmanalink } from './claim-manalink' import { createmarket } from './create-market' -import { addliquidity } from './add-liquidity' -import { withdrawliquidity } from './withdraw-liquidity' import { creategroup } from './create-group' import { resolvemarket } from './resolve-market' import { unsubscribe } from './unsubscribe' @@ -61,10 +59,8 @@ addJsonEndpointRoute('/sellbet', sellbet) addJsonEndpointRoute('/sellshares', sellshares) addJsonEndpointRoute('/claimmanalink', claimmanalink) addJsonEndpointRoute('/createmarket', createmarket) -addJsonEndpointRoute('/addliquidity', addliquidity) addJsonEndpointRoute('/addCommentBounty', addcommentbounty) addJsonEndpointRoute('/awardCommentBounty', awardcommentbounty) -addJsonEndpointRoute('/withdrawliquidity', withdrawliquidity) addJsonEndpointRoute('/creategroup', creategroup) addJsonEndpointRoute('/resolvemarket', resolvemarket) addJsonEndpointRoute('/unsubscribe', unsubscribe) diff --git a/functions/src/withdraw-liquidity.ts b/functions/src/withdraw-liquidity.ts deleted file mode 100644 index 53974f7d..00000000 --- a/functions/src/withdraw-liquidity.ts +++ /dev/null @@ -1,121 +0,0 @@ -import * as admin from 'firebase-admin' -import { z } from 'zod' - -import { CPMMContract } from '../../common/contract' -import { User } from '../../common/user' -import { subtractObjects } from '../../common/util/object' -import { LiquidityProvision } from '../../common/liquidity-provision' -import { getUserLiquidityShares } from '../../common/calculate-cpmm' -import { Bet } from '../../common/bet' -import { getProbability } from '../../common/calculate' -import { noFees } from '../../common/fees' - -import { APIError, newEndpoint, validate } from './api' -import { redeemShares } from './redeem-shares' - -const bodySchema = z.object({ - contractId: z.string(), -}) - -export const withdrawliquidity = newEndpoint({}, async (req, auth) => { - const { contractId } = validate(bodySchema, req.body) - - 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 liquidityCollection = firestore.collection( - `contracts/${contractId}/liquidity` - ) - - const liquiditiesSnap = await trans.get(liquidityCollection) - - const liquidities = liquiditiesSnap.docs.map( - (doc) => doc.data() as LiquidityProvision - ) - - const userShares = getUserLiquidityShares( - auth.uid, - contract, - liquidities, - true - ) - - // 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 newBalance = lp.balance + payout - const newTotalDeposits = lp.totalDeposits + payout - trans.update(lpDoc, { - balance: newBalance, - totalDeposits: newTotalDeposits, - } as Partial) - - const newPool = subtractObjects(contract.pool, userShares) - - 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) - - trans.update(contractDoc, { - pool: newPool, - totalLiquidity: newTotalLiquidity, - }) - - 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: 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) - ) - .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 }) - } - - 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() diff --git a/web/components/contract/contract-info-dialog.tsx b/web/components/contract/contract-info-dialog.tsx index 1f33e4e1..e1c36901 100644 --- a/web/components/contract/contract-info-dialog.tsx +++ b/web/components/contract/contract-info-dialog.tsx @@ -7,7 +7,6 @@ import { capitalize } from 'lodash' import { Contract } from 'common/contract' import { formatMoney, formatPercent } from 'common/util/format' import { contractPool, updateContract } from 'web/lib/firebase/contracts' -import { LiquidityBountyPanel } from 'web/components/contract/liquidity-bounty-panel' import { Col } from '../layout/col' import { Modal } from '../layout/modal' import { Title } from '../title' @@ -55,6 +54,7 @@ export function ContractInfoDialog(props: { outcomeType, id, elasticity, + pool, } = contract const typeDisplay = @@ -172,10 +172,25 @@ export function ContractInfoDialog(props: { + Liquidity subsidies - {mechanism === 'cpmm-1' ? 'Liquidity pool' : 'Betting pool'} + {mechanism === 'cpmm-1' + ? formatMoney(contract.totalLiquidity) + : formatMoney(100)} + + + + + Pool + + {mechanism === 'cpmm-1' && outcomeType === 'BINARY' + ? `${Math.round(pool.YES)} YES, ${Math.round(pool.NO)} NO` + : mechanism === 'cpmm-1' && outcomeType === 'PSEUDO_NUMERIC' + ? `${Math.round(pool.YES)} HIGHER, ${Math.round( + pool.NO + )} LOWER` + : contractPool(contract)} - {contractPool(contract)} {/* Show a path to Firebase if user is an admin, or we're on localhost */} @@ -228,7 +243,6 @@ export function ContractInfoDialog(props: { - {!contract.resolution && } diff --git a/web/components/contract/extra-contract-actions-row.tsx b/web/components/contract/extra-contract-actions-row.tsx index 809c6172..d81132b9 100644 --- a/web/components/contract/extra-contract-actions-row.tsx +++ b/web/components/contract/extra-contract-actions-row.tsx @@ -9,6 +9,7 @@ import { FollowMarketButton } from 'web/components/follow-market-button' import { LikeMarketButton } from 'web/components/contract/like-market-button' import { ContractInfoDialog } from 'web/components/contract/contract-info-dialog' import { Tooltip } from '../tooltip' +import { LiquidityButton } from './liquidity-button' export function ExtraContractActionsRow(props: { contract: Contract }) { const { contract } = props @@ -18,6 +19,9 @@ export function ExtraContractActionsRow(props: { contract: Contract }) { return ( + {contract.mechanism === 'cpmm-1' && ( + + )} - - - {isSuccess && amount && ( -
Success! Added {formatMoney(amount)} in liquidity.
- )} - - {isLoading &&
Processing...
} - - - - - ) -} - -function ViewLiquidityPanel(props: { contract: CPMMContract }) { - const { contract } = props - const { pool } = contract - const { YES: yesShares, NO: noShares } = pool - - return ( - -
- The liquidity pool for this market currently contains: -
- - {yesShares.toFixed(2)} shares - - - - {noShares.toFixed(2)} shares - - - ) -} - -function WithdrawLiquidityPanel(props: { - contract: CPMMContract - lpShares: { YES: number; NO: number } -}) { - const { contract, lpShares } = props - const { YES: yesShares, NO: noShares } = lpShares - - const [_error, setError] = useState(undefined) - const [isSuccess, setIsSuccess] = useState(false) - const [isLoading, setIsLoading] = useState(false) - - const submit = () => { - setIsLoading(true) - setIsSuccess(false) - - withdrawLiquidity({ contractId: contract.id }) - .then((_) => { - setIsSuccess(true) - setError(undefined) - setIsLoading(false) - }) - .catch((_) => setError('Server error')) - - track('withdraw liquidity') - } - - if (isSuccess) - return ( -
- Success! Your liquidity was withdrawn. -
- ) - - if (!yesShares && !noShares) - return ( -
- You do not have any liquidity positions to withdraw. -
- ) - - return ( - -
- Your liquidity position is currently: -
- - - {yesShares.toFixed(2)} shares - - - - {noShares.toFixed(2)} shares - - - - - - - {isLoading &&
Processing...
} - - ) -} diff --git a/web/components/contract/liquidity-button.tsx b/web/components/contract/liquidity-button.tsx new file mode 100644 index 00000000..1447ee07 --- /dev/null +++ b/web/components/contract/liquidity-button.tsx @@ -0,0 +1,92 @@ +import { useState } from 'react' +import clsx from 'clsx' + +import { Button } from 'web/components/button' +import { formatMoney, shortFormatNumber } from 'common/util/format' +import { Col } from 'web/components/layout/col' +import { Tooltip } from '../tooltip' +import { CPMMContract } from 'common/contract' +import { User } from 'common/user' +import { useLiquidity } from 'web/hooks/use-liquidity' +import { LiquidityModal } from './liquidity-modal' + +export function LiquidityButton(props: { + contract: CPMMContract + user: User | undefined | null +}) { + const { contract, user } = props + const { totalLiquidity: total } = contract + + const lp = useLiquidity(contract.id) + const userActive = lp?.find((l) => l.userId === user?.id) !== undefined + + const [open, setOpen] = useState(false) + + const disabled = + contract.isResolved || (contract.closeTime ?? Infinity) < Date.now() + + return ( + + setOpen(true)} + disabled={disabled} + /> + + + ) +} + +function LiquidityIconButton(props: { + total: number + onClick: () => void + userActive: boolean + isCompact?: boolean + disabled?: boolean +}) { + const { total, userActive, isCompact, onClick, disabled } = props + + return ( + + ) +} diff --git a/web/components/contract/liquidity-modal.tsx b/web/components/contract/liquidity-modal.tsx new file mode 100644 index 00000000..1cc337cd --- /dev/null +++ b/web/components/contract/liquidity-modal.tsx @@ -0,0 +1,108 @@ +import { CPMMContract } from 'common/contract' +import { formatMoney } from 'common/util/format' +import { useState } from 'react' +import { useUser } from 'web/hooks/use-user' +import { addSubsidy } from 'web/lib/firebase/api' +import { track } from 'web/lib/service/analytics' +import { AmountInput } from '../amount-input' +import { Button } from '../button' +import { InfoTooltip } from '../info-tooltip' +import { Col } from '../layout/col' +import { Modal } from '../layout/modal' +import { Row } from '../layout/row' +import { Title } from '../title' + +export function LiquidityModal(props: { + contract: CPMMContract + isOpen: boolean + setOpen: (open: boolean) => void +}) { + const { contract, isOpen, setOpen } = props + const { totalLiquidity } = contract + + return ( + + + + + <div>Total liquidity subsidies: {formatMoney(totalLiquidity)}</div> + <AddLiquidityPanel contract={contract as CPMMContract} /> + </Col> + </Modal> + ) +} + +function AddLiquidityPanel(props: { contract: CPMMContract }) { + const { contract } = props + const { id: contractId, slug } = 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) + + addSubsidy({ amount, contractId }) + .then((_) => { + setIsSuccess(true) + setError(undefined) + setIsLoading(false) + }) + .catch((_) => setError('Server error')) + + track('add liquidity', { amount, contractId, slug }) + } + + return ( + <> + <div className="mb-4 text-gray-500"> + Contribute your M$ to make this market more accurate by subsidizing + trading.{' '} + <InfoTooltip text="Liquidity is how much money traders can make if they're right. The more traders can earn, the greater the incentive to find the correct probability." /> + </div> + + <Row> + <AmountInput + amount={amount} + onChange={onAmountChange} + label="M$" + error={error} + disabled={isLoading} + inputClassName="w-16 mr-4" + /> + <Button size="md" color="blue" onClick={submit} disabled={isLoading}> + Add + </Button> + </Row> + + {isSuccess && amount && ( + <div>Success! Added {formatMoney(amount)} in liquidity.</div> + )} + + {isLoading && <div>Processing...</div>} + </> + ) +} diff --git a/web/components/contract/tip-button.tsx b/web/components/contract/tip-button.tsx index 0315c676..61a74e74 100644 --- a/web/components/contract/tip-button.tsx +++ b/web/components/contract/tip-button.tsx @@ -1,7 +1,8 @@ -import { HeartIcon } from '@heroicons/react/outline' -import { Button } from 'web/components/button' -import { formatMoney } from 'common/util/format' import clsx from 'clsx' +import { HeartIcon } from '@heroicons/react/outline' + +import { Button } from 'web/components/button' +import { formatMoney, shortFormatNumber } from 'common/util/format' import { Col } from 'web/components/layout/col' import { Tooltip } from '../tooltip' @@ -51,7 +52,7 @@ export function TipButton(props: { : 'sm:text-2xs text-[0.5rem]' )} > - {totalTipped} + {shortFormatNumber(totalTipped)} </div> )} </Col> diff --git a/web/hooks/use-liquidity.ts b/web/hooks/use-liquidity.ts index 9c7c2b6f..9c610f3b 100644 --- a/web/hooks/use-liquidity.ts +++ b/web/hooks/use-liquidity.ts @@ -21,11 +21,6 @@ export const useLiquidity = (contractId: string) => { export const useUserLiquidity = (contract: CPMMContract, userId: string) => { const liquidities = useLiquidity(contract.id) - const userShares = getUserLiquidityShares( - userId, - contract, - liquidities ?? [], - true - ) + const userShares = getUserLiquidityShares(userId, contract, liquidities ?? []) return userShares } diff --git a/web/lib/firebase/api.ts b/web/lib/firebase/api.ts index 3e803bc6..d17ee9c4 100644 --- a/web/lib/firebase/api.ts +++ b/web/lib/firebase/api.ts @@ -42,8 +42,8 @@ export function changeUserInfo(params: any) { return call(getFunctionUrl('changeuserinfo'), 'POST', params) } -export function addLiquidity(params: any) { - return call(getFunctionUrl('addliquidity'), 'POST', params) +export function addSubsidy(params: any) { + return call(getFunctionUrl('addsubsidy'), 'POST', params) } export function addCommentBounty(params: any) { @@ -54,10 +54,6 @@ export function awardCommentBounty(params: any) { return call(getFunctionUrl('awardcommentbounty'), 'POST', params) } -export function withdrawLiquidity(params: any) { - return call(getFunctionUrl('withdrawliquidity'), 'POST', params) -} - export function createMarket(params: any) { return call(getFunctionUrl('createmarket'), 'POST', params) }