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
This commit is contained in:
parent
8bb9885aee
commit
0ec15ff2f8
|
@ -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 }
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -112,6 +112,7 @@ const getBinaryCpmmProps = (initialProb: number, ante: number) => {
|
|||
mechanism: 'cpmm-1',
|
||||
outcomeType: 'BINARY',
|
||||
totalLiquidity: ante,
|
||||
subsidyPool: 0,
|
||||
initialProbability: p,
|
||||
p,
|
||||
pool: pool,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(' ')
|
||||
|
|
|
@ -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,
|
||||
transaction.update(contractDoc, {
|
||||
subsidyPool: newSubsidyPool,
|
||||
totalLiquidity: newTotalLiquidity,
|
||||
})
|
||||
)
|
||||
} as Partial<CPMMContract>)
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
69
functions/src/drizzle-liquidity.ts
Normal file
69
functions/src/drizzle-liquidity.ts
Normal file
|
@ -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()
|
||||
})
|
||||
}
|
42
functions/src/helpers/add-house-subsidy.ts
Normal file
42
functions/src/helpers/add-house-subsidy.ts
Normal file
|
@ -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<CPMMContract>)
|
||||
|
||||
transaction.create(newLiquidityProvisionDoc, newLiquidityProvision)
|
||||
})
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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<UniqueBettorBonusTxn, 'id' | 'createdTime'>
|
||||
|
||||
const { status, message, txn } = await runTxn(trans, bonusTxn)
|
||||
|
||||
return { status, newUniqueBettorIds, message, txn }
|
||||
})
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
8
functions/src/scripts/drizzle.ts
Normal file
8
functions/src/scripts/drizzle.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { initAdmin } from './script-init'
|
||||
initAdmin()
|
||||
|
||||
import { drizzleLiquidity } from '../drizzle-liquidity'
|
||||
|
||||
if (require.main === module) {
|
||||
drizzleLiquidity().then(() => process.exit())
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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<User>)
|
||||
|
||||
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<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 })
|
||||
}
|
||||
|
||||
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()
|
|
@ -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: {
|
|||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>Liquidity subsidies</td>
|
||||
<td>
|
||||
{mechanism === 'cpmm-1' ? 'Liquidity pool' : 'Betting pool'}
|
||||
{mechanism === 'cpmm-1'
|
||||
? formatMoney(contract.totalLiquidity)
|
||||
: formatMoney(100)}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>Pool</td>
|
||||
<td>
|
||||
{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)}
|
||||
</td>
|
||||
<td>{contractPool(contract)}</td>
|
||||
</tr>
|
||||
|
||||
{/* Show a path to Firebase if user is an admin, or we're on localhost */}
|
||||
|
@ -228,7 +243,6 @@ export function ContractInfoDialog(props: {
|
|||
<Row className="flex-wrap">
|
||||
<DuplicateContractButton contract={contract} />
|
||||
</Row>
|
||||
{!contract.resolution && <LiquidityBountyPanel contract={contract} />}
|
||||
</Col>
|
||||
</Modal>
|
||||
</>
|
||||
|
|
|
@ -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 (
|
||||
<Row>
|
||||
<FollowMarketButton contract={contract} user={user} />
|
||||
{contract.mechanism === 'cpmm-1' && (
|
||||
<LiquidityButton contract={contract} user={user} />
|
||||
)}
|
||||
<LikeMarketButton contract={contract} user={user} />
|
||||
<Tooltip text="Share" placement="bottom" noTap noFade>
|
||||
<Button
|
||||
|
|
|
@ -1,248 +0,0 @@
|
|||
import clsx from 'clsx'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { Contract, CPMMContract } from 'common/contract'
|
||||
import { formatMoney } from 'common/util/format'
|
||||
import { useUser } from 'web/hooks/use-user'
|
||||
import { addLiquidity, withdrawLiquidity } from 'web/lib/firebase/api'
|
||||
import { AmountInput } from 'web/components/amount-input'
|
||||
import { Row } from 'web/components/layout/row'
|
||||
import { useUserLiquidity } from 'web/hooks/use-liquidity'
|
||||
import { Tabs } from 'web/components/layout/tabs'
|
||||
import { NoLabel, YesLabel } from 'web/components/outcome-label'
|
||||
import { Col } from 'web/components/layout/col'
|
||||
import { track } from 'web/lib/service/analytics'
|
||||
import { InfoTooltip } from 'web/components/info-tooltip'
|
||||
import { BETTORS, PRESENT_BET } from 'common/user'
|
||||
import { buildArray } from 'common/util/array'
|
||||
import { useAdmin } from 'web/hooks/use-admin'
|
||||
import { AlertBox } from '../alert-box'
|
||||
import { Spacer } from '../layout/spacer'
|
||||
|
||||
export function LiquidityBountyPanel(props: { contract: Contract }) {
|
||||
const { contract } = props
|
||||
|
||||
const isCPMM = contract.mechanism === 'cpmm-1'
|
||||
const user = useUser()
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const lpShares = isCPMM && useUserLiquidity(contract, user?.id ?? '')
|
||||
|
||||
const [showWithdrawal, setShowWithdrawal] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!showWithdrawal && lpShares && lpShares.YES && lpShares.NO)
|
||||
setShowWithdrawal(true)
|
||||
}, [showWithdrawal, lpShares])
|
||||
|
||||
const isCreator = user?.id === contract.creatorId
|
||||
const isAdmin = useAdmin()
|
||||
|
||||
if (!isCreator && !isAdmin && !showWithdrawal) return <></>
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
tabs={buildArray(
|
||||
(isCreator || isAdmin) &&
|
||||
isCPMM && {
|
||||
title: (isAdmin ? '[Admin] ' : '') + 'Subsidize',
|
||||
content: <AddLiquidityPanel contract={contract} />,
|
||||
},
|
||||
showWithdrawal &&
|
||||
isCPMM && {
|
||||
title: 'Withdraw',
|
||||
content: (
|
||||
<WithdrawLiquidityPanel
|
||||
contract={contract}
|
||||
lpShares={lpShares as { YES: number; NO: number }}
|
||||
/>
|
||||
),
|
||||
},
|
||||
|
||||
(isCreator || isAdmin) &&
|
||||
isCPMM && {
|
||||
title: 'Pool',
|
||||
content: <ViewLiquidityPanel contract={contract} />,
|
||||
}
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
addLiquidity({ 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.{' '}
|
||||
<InfoTooltip
|
||||
text={`More liquidity stabilizes the market, encouraging ${BETTORS} to ${PRESENT_BET}.`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Row>
|
||||
<AmountInput
|
||||
amount={amount}
|
||||
onChange={onAmountChange}
|
||||
label="M$"
|
||||
error={error}
|
||||
disabled={isLoading}
|
||||
inputClassName="w-28"
|
||||
/>
|
||||
<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>}
|
||||
|
||||
<Spacer h={2} />
|
||||
<AlertBox
|
||||
title="Withdrawals ending"
|
||||
text="Manifold is moving to a new system for handling subsidization. As part of this process, liquidity withdrawals will be disabled shortly. Feel free to withdraw any outstanding liquidity you've added now."
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function ViewLiquidityPanel(props: { contract: CPMMContract }) {
|
||||
const { contract } = props
|
||||
const { pool } = contract
|
||||
const { YES: yesShares, NO: noShares } = pool
|
||||
|
||||
return (
|
||||
<Col className="mb-4">
|
||||
<div className="mb-4 text-gray-500">
|
||||
The liquidity pool for this market currently contains:
|
||||
</div>
|
||||
<span>
|
||||
{yesShares.toFixed(2)} <YesLabel /> shares
|
||||
</span>
|
||||
|
||||
<span>
|
||||
{noShares.toFixed(2)} <NoLabel /> shares
|
||||
</span>
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
|
||||
function WithdrawLiquidityPanel(props: {
|
||||
contract: CPMMContract
|
||||
lpShares: { YES: number; NO: number }
|
||||
}) {
|
||||
const { contract, lpShares } = props
|
||||
const { YES: yesShares, NO: noShares } = lpShares
|
||||
|
||||
const [_error, setError] = useState<string | undefined>(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 (
|
||||
<div className="text-gray-500">
|
||||
Success! Your liquidity was withdrawn.
|
||||
</div>
|
||||
)
|
||||
|
||||
if (!yesShares && !noShares)
|
||||
return (
|
||||
<div className="text-gray-500">
|
||||
You do not have any liquidity positions to withdraw.
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<Col>
|
||||
<div className="mb-4 text-gray-500">
|
||||
Your liquidity position is currently:
|
||||
</div>
|
||||
|
||||
<span>
|
||||
{yesShares.toFixed(2)} <YesLabel /> shares
|
||||
</span>
|
||||
|
||||
<span>
|
||||
{noShares.toFixed(2)} <NoLabel /> shares
|
||||
</span>
|
||||
|
||||
<Row className="mt-4 mb-2">
|
||||
<button
|
||||
className={clsx(
|
||||
'btn btn-outline btn-sm ml-2',
|
||||
isLoading && 'btn-disabled'
|
||||
)}
|
||||
onClick={submit}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Withdraw
|
||||
</button>
|
||||
</Row>
|
||||
|
||||
{isLoading && <div>Processing...</div>}
|
||||
</Col>
|
||||
)
|
||||
}
|
92
web/components/contract/liquidity-button.tsx
Normal file
92
web/components/contract/liquidity-button.tsx
Normal file
|
@ -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 (
|
||||
<Tooltip
|
||||
text={`${formatMoney(total)} in liquidity subsidies`}
|
||||
placement="bottom"
|
||||
noTap
|
||||
noFade
|
||||
>
|
||||
<LiquidityIconButton
|
||||
total={total}
|
||||
userActive={userActive}
|
||||
onClick={() => setOpen(true)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<LiquidityModal contract={contract} isOpen={open} setOpen={setOpen} />
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
function LiquidityIconButton(props: {
|
||||
total: number
|
||||
onClick: () => void
|
||||
userActive: boolean
|
||||
isCompact?: boolean
|
||||
disabled?: boolean
|
||||
}) {
|
||||
const { total, userActive, isCompact, onClick, disabled } = props
|
||||
|
||||
return (
|
||||
<Button
|
||||
size={'sm'}
|
||||
className={clsx(
|
||||
'max-w-xs self-center pt-1',
|
||||
isCompact && 'px-0 py-0',
|
||||
disabled && 'hover:bg-inherit'
|
||||
)}
|
||||
color={'gray-white'}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Col className={'relative items-center sm:flex-row'}>
|
||||
<span
|
||||
className={clsx(
|
||||
'text-xl sm:text-2xl',
|
||||
total > 0 ? 'mr-2' : '',
|
||||
userActive ? '' : 'grayscale'
|
||||
)}
|
||||
>
|
||||
💧
|
||||
</span>
|
||||
{total > 0 && (
|
||||
<div
|
||||
className={clsx(
|
||||
'bg-greyscale-5 absolute ml-3.5 mt-2 h-4 w-4 rounded-full align-middle text-white sm:mt-3 sm:h-5 sm:w-5 sm:px-1',
|
||||
total > 99
|
||||
? 'text-[0.4rem] sm:text-[0.5rem]'
|
||||
: 'sm:text-2xs text-[0.5rem]'
|
||||
)}
|
||||
>
|
||||
{shortFormatNumber(total)}
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
</Button>
|
||||
)
|
||||
}
|
108
web/components/contract/liquidity-modal.tsx
Normal file
108
web/components/contract/liquidity-modal.tsx
Normal file
|
@ -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 (
|
||||
<Modal open={isOpen} setOpen={setOpen} size="sm">
|
||||
<Col className="gap-2.5 rounded bg-white p-4 pb-8 sm:gap-4">
|
||||
<Title className="!mt-0 !mb-2" text="💧 Add a subsidy" />
|
||||
|
||||
<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>}
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user