This commit is contained in:
		
						commit
						dc7e47cb2b
					
				|  | @ -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