diff --git a/backend/api/src/add-subsidy.ts b/backend/api/src/add-subsidy.ts index 4a64eb7d4e..b54b9640cc 100644 --- a/backend/api/src/add-subsidy.ts +++ b/backend/api/src/add-subsidy.ts @@ -5,6 +5,7 @@ import { getNewLiquidityProvision } from 'common/add-liquidity' import { APIError, type APIHandler } from './helpers/endpoint' import { SUBSIDY_FEE } from 'common/economy' import { runTxn } from 'shared/txn/run-txn' +import { type Txn } from 'common/txn' export const addLiquidity: APIHandler< 'market/:contractId/add-liquidity' @@ -35,21 +36,14 @@ export const addLiquidity: APIHandler< if (user.balance < amount) throw new APIError(403, 'Insufficient balance') - const newLiquidityProvisionDoc = firestore - .collection(`contracts/${contractId}/liquidity`) - .doc() - const subsidyAmount = (1 - SUBSIDY_FEE) * amount - const { newLiquidityProvision, newTotalLiquidity, newSubsidyPool } = - getNewLiquidityProvision( - user.id, - subsidyAmount, - contract, - newLiquidityProvisionDoc.id - ) + const { newTotalLiquidity, newSubsidyPool } = getNewLiquidityProvision( + subsidyAmount, + contract + ) - await runTxn(transaction, { + const { status, message, txn } = await runTxn(transaction, { fromId: user.id, amount: amount, toId: contractId, @@ -59,6 +53,10 @@ export const addLiquidity: APIHandler< fromType: 'USER', }) + if (status === 'error' || !txn) { + throw new APIError(500, message ?? 'Unknown error') + } + transaction.update(contractDoc, { subsidyPool: newSubsidyPool, totalLiquidity: newTotalLiquidity, @@ -70,8 +68,7 @@ export const addLiquidity: APIHandler< throw new APIError(500, 'Invalid user balance for ' + user.username) } - transaction.create(newLiquidityProvisionDoc, newLiquidityProvision) - return newLiquidityProvision + return txn as Txn }) } diff --git a/backend/api/src/create-answer-cpmm.ts b/backend/api/src/create-answer-cpmm.ts index 699caae186..0b0c949419 100644 --- a/backend/api/src/create-answer-cpmm.ts +++ b/backend/api/src/create-answer-cpmm.ts @@ -17,11 +17,13 @@ import { isAdminId } from 'common/envs/constants' import { Bet } from 'common/bet' import { floatingEqual } from 'common/util/math' import { noFees } from 'common/fees' -import { getCpmmInitialLiquidity } from 'common/antes' +import { getCpmmInitialLiquidityTxn } from 'common/antes' import { addUserToContractFollowers } from 'shared/follow-market' import { log } from 'shared/utils' import { createNewAnswerOnContractNotification } from 'shared/create-notification' import { removeUndefinedProps } from 'common/util/object' +import { addHouseSubsidyToAnswer } from 'shared/helpers/add-house-subsidy' +import { runTxn } from 'shared/txn/run-txn' export const createAnswerCPMM: APIHandler<'market/:contractId/answer'> = async ( props, @@ -167,18 +169,15 @@ export const createAnswerCpmmMain = async ( transaction.update(contractDoc, { totalLiquidity: FieldValue.increment(ANSWER_COST), }) - const liquidityDoc = firestore - .collection(`contracts/${contract.id}/liquidity`) - .doc() - const lp = getCpmmInitialLiquidity( + + const lp = getCpmmInitialLiquidityTxn( user.id, contract, - liquidityDoc.id, ANSWER_COST, - createdTime, newAnswer.id ) - transaction.create(liquidityDoc, lp) + + await runTxn(transaction, lp) } return { newAnswerId: newAnswer.id, contract, user } diff --git a/backend/api/src/create-market.ts b/backend/api/src/create-market.ts index ad12444188..d6e0c86599 100644 --- a/backend/api/src/create-market.ts +++ b/backend/api/src/create-market.ts @@ -1,6 +1,6 @@ import * as admin from 'firebase-admin' import { FieldValue, Transaction } from 'firebase-admin/firestore' -import { getCpmmInitialLiquidity } from 'common/antes' +import { getCpmmInitialLiquidityTxn } from 'common/antes' import { add_answers_mode, Contract, @@ -593,18 +593,14 @@ async function generateAntes( outcomeType === 'MULTIPLE_CHOICE' || outcomeType === 'NUMBER' ) { - const liquidityDoc = firestore - .collection(`contracts/${contract.id}/liquidity`) - .doc() - - const lp = getCpmmInitialLiquidity( + const lp = getCpmmInitialLiquidityTxn( providerId, contract as CPMMBinaryContract | CPMMMultiContract, - liquidityDoc.id, - ante, - contract.createdTime + ante ) - await liquidityDoc.set(lp) + await firestore.runTransaction(async (transaction) => { + runTxn(transaction, lp) + }) } } diff --git a/backend/functions/src/triggers/on-create-liquidity-provision.ts b/backend/functions/src/triggers/on-create-liquidity-provision.ts index 9fa9e64013..2c72dd7ead 100644 --- a/backend/functions/src/triggers/on-create-liquidity-provision.ts +++ b/backend/functions/src/triggers/on-create-liquidity-provision.ts @@ -1,45 +1,30 @@ -import * as functions from 'firebase-functions' -import { getContract, getUser, log } from 'shared/utils' +import { getContract, getUser } from 'shared/utils' import { createFollowOrMarketSubsidizedNotification } from 'shared/create-notification' -import { LiquidityProvision } from 'common/liquidity-provision' import { addUserToContractFollowers } from 'shared/follow-market' -import { - DEV_HOUSE_LIQUIDITY_PROVIDER_ID, - HOUSE_LIQUIDITY_PROVIDER_ID, -} from 'common/antes' -import { secrets } from 'common/secrets' +import { AddSubsidyTxn } from 'common/txn' -export const onCreateLiquidityProvision = functions - .runWith({ secrets }) - .firestore.document('contracts/{contractId}/liquidity/{liquidityId}') - .onCreate(async (change, context) => { - const liquidity = change.data() as LiquidityProvision - const { eventId } = context +// TODO: add this as continuation of add liquidity instances - // Ignore Manifold Markets liquidity for now - users see a notification for free market liquidity provision - if ( - liquidity.isAnte || - liquidity.userId === HOUSE_LIQUIDITY_PROVIDER_ID || - liquidity.userId === DEV_HOUSE_LIQUIDITY_PROVIDER_ID - ) - return +export const onCreateLiquidityProvision = async (txn: AddSubsidyTxn) => { + // Ignore Manifold Markets liquidity for now - users see a notification for free market liquidity provision + if (txn.fromType === 'BANK' || txn.data.isAnte) { + return + } - log(`onCreateLiquidityProvision: ${JSON.stringify(liquidity)}`) + const contract = await getContract(txn.toId) + if (!contract) + throw new Error('Could not find contract corresponding with liquidity') - const contract = await getContract(liquidity.contractId) - if (!contract) - throw new Error('Could not find contract corresponding with liquidity') - - const liquidityProvider = await getUser(liquidity.userId) - if (!liquidityProvider) throw new Error('Could not find liquidity provider') - await addUserToContractFollowers(contract.id, liquidityProvider.id) - await createFollowOrMarketSubsidizedNotification( - contract.id, - 'liquidity', - 'created', - liquidityProvider, - eventId, - liquidity.amount.toString(), - { contract } - ) - }) + const liquidityProvider = await getUser(txn.fromId) + if (!liquidityProvider) throw new Error('Could not find liquidity provider') + await addUserToContractFollowers(contract.id, liquidityProvider.id) + await createFollowOrMarketSubsidizedNotification( + contract.id, + 'liquidity', + 'created', + liquidityProvider, + txn.id, + txn.amount.toString(), + { contract } + ) +} diff --git a/backend/shared/src/helpers/add-house-subsidy.ts b/backend/shared/src/helpers/add-house-subsidy.ts index 2d3eb6d005..9aae26fb2d 100644 --- a/backend/shared/src/helpers/add-house-subsidy.ts +++ b/backend/shared/src/helpers/add-house-subsidy.ts @@ -1,44 +1,42 @@ import * as admin from 'firebase-admin' import { FieldValue } from 'firebase-admin/firestore' - import { CPMMContract, CPMMMultiContract } from 'common/contract' -import { isProd } from 'shared/utils' -import { - DEV_HOUSE_LIQUIDITY_PROVIDER_ID, - HOUSE_LIQUIDITY_PROVIDER_ID, -} from 'common/antes' import { getNewLiquidityProvision } from 'common/add-liquidity' +import { APIError } from 'common/api/utils' +import { runTxnFromBank } from 'shared/txn/run-txn' 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 transaction.get(contractDoc) const contract = snap.data() as CPMMContract | CPMMMultiContract - const { newLiquidityProvision, newTotalLiquidity, newSubsidyPool } = - getNewLiquidityProvision( - providerId, - amount, - contract, - newLiquidityProvisionDoc.id - ) + const { newTotalLiquidity, newSubsidyPool } = getNewLiquidityProvision( + amount, + contract + ) + + const { status, message, txn } = await runTxnFromBank(transaction, { + fromType: 'BANK', + amount, + toId: contractId, + toType: 'CONTRACT', + category: 'ADD_SUBSIDY', + token: 'M$', + }) + + if (status === 'error') { + throw new APIError(500, message ?? 'Unknown error') + } transaction.update(contractDoc, { subsidyPool: newSubsidyPool, totalLiquidity: newTotalLiquidity, } as Partial) - transaction.create(newLiquidityProvisionDoc, newLiquidityProvision) + return txn }) } @@ -48,25 +46,21 @@ export const addHouseSubsidyToAnswer = ( 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 transaction.get(contractDoc) - const contract = snap.data() as CPMMContract | CPMMMultiContract - const { newLiquidityProvision } = getNewLiquidityProvision( - providerId, + const { status, message, txn } = await runTxnFromBank(transaction, { + fromType: 'BANK', amount, - contract, - newLiquidityProvisionDoc.id, - answerId - ) + toId: contractId, + toType: 'CONTRACT', + category: 'ADD_SUBSIDY', + token: 'M$', + data: { answerId }, + }) + + if (status === 'error') { + throw new APIError(500, message ?? 'Unknown error') + } transaction.update(contractDoc, { totalLiquidity: FieldValue.increment(amount), @@ -80,6 +74,6 @@ export const addHouseSubsidyToAnswer = ( subsidyPool: FieldValue.increment(amount), }) - transaction.create(newLiquidityProvisionDoc, newLiquidityProvision) + return txn }) } diff --git a/backend/shared/src/resolve-market-helpers.ts b/backend/shared/src/resolve-market-helpers.ts index fcf10d281f..4a3fcd15ff 100644 --- a/backend/shared/src/resolve-market-helpers.ts +++ b/backend/shared/src/resolve-market-helpers.ts @@ -35,6 +35,7 @@ import { recordContractEdit } from 'shared/record-contract-edit' import { createSupabaseDirectClient } from './supabase/init' import { Answer } from 'common/answer' import { acquireLock, releaseLock } from './firestore-lock' +import { convertTxn } from 'common/supabase/txns' export type ResolutionParams = { outcome: string @@ -255,14 +256,26 @@ export const getDataAndPayoutInfo = async ( answerId: string | undefined ) => { const { id: contractId, creatorId, outcomeType } = unresolvedContract - const liquiditiesSnap = await firestore - .collection(`contracts/${contractId}/liquidity`) - .get() - - const liquidityDocs = liquiditiesSnap.docs.map( - (doc) => doc.data() as LiquidityProvision + const pg = createSupabaseDirectClient() + + const liquidityTxns = await pg.map( + `select * from txns + where category = 'ADD_SUBSIDY' + and to_id = $1`, + [contractId], + convertTxn ) + const liquidityDocs: LiquidityProvision[] = liquidityTxns.map((txn) => ({ + id: txn.id, + userId: txn.fromId, + contractId, + createdTime: txn.createdTime, + isAnte: txn.data?.isAnte, + answerId: txn.data?.answerId, + amount: txn.amount, + })) + const liquidities = unresolvedContract.mechanism === 'cpmm-multi-1' && outcomeType !== 'NUMBER' && @@ -278,7 +291,6 @@ export const getDataAndPayoutInfo = async ( ) { // Load bets from supabase as an optimization. // This type of multi choice generates a lot of extra bets that have shares = 0. - const pg = createSupabaseDirectClient() bets = await pg.map( `select * from contract_bets where contract_id = $1 diff --git a/common/src/add-liquidity.ts b/common/src/add-liquidity.ts index 04f1002ef6..a811e43ded 100644 --- a/common/src/add-liquidity.ts +++ b/common/src/add-liquidity.ts @@ -1,17 +1,12 @@ -import { getCpmmLiquidity } from './calculate-cpmm' import { CPMMContract, CPMMMultiContract, CPMMNumericContract, } from './contract' -import { LiquidityProvision } from './liquidity-provision' -import { removeUndefinedProps } from './util/object' export const getNewLiquidityProvision = ( - userId: string, amount: number, contract: CPMMContract | CPMMMultiContract | CPMMNumericContract, - newLiquidityProvisionId: string, answerId?: string ) => { const { totalLiquidity, subsidyPool } = contract @@ -20,25 +15,5 @@ export const getNewLiquidityProvision = ( // If answerId is defined, amount will be added to the answer's subsidy pool const newSubsidyPool = (subsidyPool ?? 0) + (answerId ? 0 : amount) - let pool: { [outcome: string]: number } | undefined - let liquidity: number | undefined - if (contract.mechanism === 'cpmm-1') { - pool = contract.pool - liquidity = getCpmmLiquidity(pool, contract.p) - } else { - liquidity = newTotalLiquidity - } - - const newLiquidityProvision: LiquidityProvision = removeUndefinedProps({ - id: newLiquidityProvisionId, - userId: userId, - contractId: contract.id, - answerId, - amount, - pool, - liquidity, - createdTime: Date.now(), - }) - - return { newLiquidityProvision, newTotalLiquidity, newSubsidyPool } + return { newTotalLiquidity, newSubsidyPool } } diff --git a/common/src/antes.ts b/common/src/antes.ts index f136efdc84..3151f62f69 100644 --- a/common/src/antes.ts +++ b/common/src/antes.ts @@ -5,10 +5,10 @@ import { CPMMMultiContract, } from './contract' import { User } from './user' -import { LiquidityProvision } from './liquidity-provision' import { noFees } from './fees' import { DpmAnswer } from './answer' import { removeUndefinedProps } from './util/object' +import { AddSubsidyTxn } from './txn' export const HOUSE_LIQUIDITY_PROVIDER_ID = 'IPTOzEqrpkWmEzh6hwvAyY9PqFb2' // @ManifoldMarkets' id export const DEV_HOUSE_LIQUIDITY_PROVIDER_ID = '94YYTk1AFWfbWMpfYcvnnwI1veP2' // @ManifoldMarkets' id @@ -18,33 +18,31 @@ type NormalizedBet = Omit< 'userAvatarUrl' | 'userName' | 'userUsername' > -export function getCpmmInitialLiquidity( +export function getCpmmInitialLiquidityTxn( providerId: string, contract: CPMMBinaryContract | CPMMMultiContract, - anteId: string, amount: number, - createdTime: number, answerId?: string ) { const { mechanism } = contract - const pool = mechanism === 'cpmm-1' ? { YES: 0, NO: 0 } : undefined + if (mechanism === 'cpmm-1') { + throw new Error(' DPM is deprecated by now') + } - const lp: LiquidityProvision = removeUndefinedProps({ - id: anteId, - userId: providerId, - contractId: contract.id, - isAnte: true, - // Unfortunately, createdTime is only properly set for MC answers after this commit. - createdTime, - // answerId is only properly set for MC answers after this commit AND answers added after the question is created. - answerId, - amount: amount, - liquidity: amount, - pool, - }) - - return lp + return { + category: 'ADD_SUBSIDY', + fromType: 'USER', + fromId: providerId, + toType: 'CONTRACT', + toId: contract.id, + amount, + token: 'M$', + data: removeUndefinedProps({ + isAnte: true, + answerId, + }), + } as const } export function getMultipleChoiceAntes( diff --git a/common/src/api/schema.ts b/common/src/api/schema.ts index 333d589834..dfd8605461 100644 --- a/common/src/api/schema.ts +++ b/common/src/api/schema.ts @@ -23,7 +23,6 @@ import { Lover } from 'common/love/lover' import { CPMMMultiContract, Contract } from 'common/contract' import { CompatibilityScore } from 'common/love/compatibility-score' import type { Txn, ManaPayTxn } from 'common/txn' -import { LiquidityProvision } from 'common/liquidity-provision' import { LiteUser } from './user-types' import { League } from 'common/leagues' import { searchProps } from './market-search-types' @@ -383,7 +382,7 @@ export const API = (_apiTypeCheck = { method: 'POST', visibility: 'public', authed: true, - returns: {} as LiquidityProvision, + returns: {} as Txn, props: z .object({ contractId: z.string(), diff --git a/common/src/liquidity-provision.ts b/common/src/liquidity-provision.ts index 1aeae6ef0c..d0e37523d0 100644 --- a/common/src/liquidity-provision.ts +++ b/common/src/liquidity-provision.ts @@ -10,8 +10,8 @@ export type LiquidityProvision = { answerId?: string amount: number // Ṁ quantity - liquidity: number // change in constant k after provision + // liquidity: number // change in constant k after provision // For cpmm-1: - pool?: { [outcome: string]: number } // pool shares before provision + // pool?: { [outcome: string]: number } // pool shares before provision } diff --git a/common/src/txn.ts b/common/src/txn.ts index 92a5419d47..52d34d8905 100644 --- a/common/src/txn.ts +++ b/common/src/txn.ts @@ -393,9 +393,14 @@ type LikePurchase = { type AddSubsidy = { category: 'ADD_SUBSIDY' - fromType: 'USER' + fromType: 'USER' | 'BANK' toType: 'CONTRACT' token: 'M$' + data: { + // TODO: these aren't necessarily up-to-date, we need to backfill fom LiquidtyProvision documents - which aren't up-to-date either + isAnte?: boolean + answerId?: string + } } type ReclaimMana = { diff --git a/firestore.rules b/firestore.rules index c8f3bcb3bc..9d07d05b54 100644 --- a/firestore.rules +++ b/firestore.rules @@ -83,10 +83,6 @@ service cloud.firestore { allow read; } - match /{somePath=**}/liquidity/{liquidityId} { - allow read; - } - match /{somePath=**}/answers/{answerId} { allow read; } diff --git a/web/components/contract/contract-tabs.tsx b/web/components/contract/contract-tabs.tsx index c7a6a6a5cb..7d5b07c450 100644 --- a/web/components/contract/contract-tabs.tsx +++ b/web/components/contract/contract-tabs.tsx @@ -2,12 +2,7 @@ import { groupBy, keyBy, last, mapValues, sortBy, sumBy, uniqBy } from 'lodash' import { memo, useEffect, useMemo, useReducer, useRef, useState } from 'react' import clsx from 'clsx' import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/solid' - import { Answer, DpmAnswer } from 'common/answer' -import { - DEV_HOUSE_LIQUIDITY_PROVIDER_ID, - HOUSE_LIQUIDITY_PROVIDER_ID, -} from 'common/antes' import { Bet } from 'common/bet' import { ContractComment } from 'common/comment' import { @@ -524,13 +519,7 @@ export const BetsTabContent = memo(function BetsTabContent(props: { const end = start + ITEMS_PER_PAGE const lps = useLiquidity(contract.id) ?? [] - const visibleLps = lps.filter( - (l) => - !l.isAnte && - l.userId !== HOUSE_LIQUIDITY_PROVIDER_ID && - l.userId !== DEV_HOUSE_LIQUIDITY_PROVIDER_ID && - l.amount > 0 - ) + const visibleLps = lps.filter((l) => !l.data.isAnte && l.fromType === 'USER') const isMultiNumber = outcomeType === 'NUMBER' const betsByBetGroupId = isMultiNumber ? groupBy(props.bets, (bet) => bet.betGroupId ?? bet.id) diff --git a/web/components/feed/feed-liquidity.tsx b/web/components/feed/feed-liquidity.tsx index b2ec259fa1..e2e3f258b7 100644 --- a/web/components/feed/feed-liquidity.tsx +++ b/web/components/feed/feed-liquidity.tsx @@ -5,23 +5,23 @@ import { Row } from 'web/components/layout/row' import { Avatar, EmptyAvatar } from 'web/components/widgets/avatar' import { formatMoney } from 'common/util/format' import { RelativeTimestamp } from 'web/components/relative-timestamp' -import { LiquidityProvision } from 'common/liquidity-provision' import { UserLink } from 'web/components/widgets/user-link' import { UserHovercard } from '../user/user-hovercard' +import { AddSubsidyTxn } from 'common/txn' export function FeedLiquidity(props: { className?: string - liquidity: LiquidityProvision + liquidity: AddSubsidyTxn }) { const { liquidity } = props - const { userId, createdTime } = liquidity + const { fromId, createdTime } = liquidity const isBeforeJune2022 = dayjs(createdTime).isBefore('2022-06-01') // eslint-disable-next-line react-hooks/rules-of-hooks - const bettor = isBeforeJune2022 ? undefined : useUserById(userId) ?? undefined + const bettor = isBeforeJune2022 ? undefined : useUserById(fromId) ?? undefined const user = useUser() - const isSelf = user?.id === userId + const isSelf = user?.id === fromId return (
@@ -29,7 +29,7 @@ export function FeedLiquidity(props: { {isSelf ? ( ) : bettor ? ( - + ) : ( @@ -48,7 +48,7 @@ export function FeedLiquidity(props: { } function LiquidityStatusText(props: { - liquidity: LiquidityProvision + liquidity: AddSubsidyTxn isSelf: boolean bettor?: User }) { diff --git a/web/hooks/use-liquidity.ts b/web/hooks/use-liquidity.ts index 46e3f99314..a4670be45d 100644 --- a/web/hooks/use-liquidity.ts +++ b/web/hooks/use-liquidity.ts @@ -1,15 +1,22 @@ +import { convertTxn } from 'common/supabase/txns' +import { run } from 'common/supabase/utils' +import { AddSubsidyTxn } from 'common/txn' import { useEffect, useState } from 'react' -import { LiquidityProvision } from 'common/liquidity-provision' -import { listenForLiquidity } from 'web/lib/firebase/liquidity' +import { db } from 'web/lib/supabase/db' export const useLiquidity = (contractId: string) => { - const [liquidities, setLiquidities] = useState< - LiquidityProvision[] | undefined - >(undefined) + const [liquidities, setLiquidities] = useState([]) useEffect(() => { - return listenForLiquidity(contractId, setLiquidities) - }, [contractId]) + run( + db + .from('txns') + .select() + .eq('category', 'ADD_SUBSIDY') + .eq('to_id', contractId) + .order('created_time', { ascending: true }) + ).then(({ data }) => setLiquidities(data.map(convertTxn) as any)) + }, []) return liquidities } diff --git a/web/lib/firebase/liquidity.ts b/web/lib/firebase/liquidity.ts deleted file mode 100644 index 28712c0c99..0000000000 --- a/web/lib/firebase/liquidity.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { collection, query } from 'firebase/firestore' - -import { db } from './init' -import { listenForValues } from './utils' -import { LiquidityProvision } from 'common/liquidity-provision' - -export function listenForLiquidity( - contractId: string, - setLiquidity: (lps: LiquidityProvision[]) => void -) { - const lpQuery = query(collection(db, 'contracts', contractId, 'liquidity')) - - return listenForValues(lpQuery, (lps) => { - lps.sort((lp1, lp2) => lp1.createdTime - lp2.createdTime) - setLiquidity(lps) - }) -}