From 748afc2dec19f2521c86cb5132068ba542bdcb36 Mon Sep 17 00:00:00 2001 From: Juan P Lopez Date: Thu, 31 Oct 2024 21:55:34 -0500 Subject: [PATCH] feat(core): update exporter metrics (#4617) * feat(core): update exporter metrics * fix: avoid multiple queries to db for the same account * fix: logger level for getRealAssetsVersusLiabilities * fix: real assets vs liabilities calc --- core/api/src/servers/exporter.ts | 138 +++++++++++++++++-- core/api/src/services/ledger/admin-legacy.ts | 26 +++- 2 files changed, 150 insertions(+), 14 deletions(-) diff --git a/core/api/src/servers/exporter.ts b/core/api/src/servers/exporter.ts index 51d26a8222..36936dba14 100644 --- a/core/api/src/servers/exporter.ts +++ b/core/api/src/servers/exporter.ts @@ -9,7 +9,7 @@ import { SECS_PER_5_MINS, } from "@/config" -import { Lightning, OnChain } from "@/app" +import { Lightning, OnChain, Prices } from "@/app" import { toSeconds } from "@/domain/primitives" @@ -32,6 +32,7 @@ import { activateLndHealthCheck } from "@/services/lnd/health" import { ledgerAdmin, setupMongoConnection } from "@/services/mongodb" import { timeoutWithCancel } from "@/utils" +import { displayAmountFromNumber, UsdDisplayCurrency } from "@/domain/fiat" const TIMEOUT_WALLET_BALANCE = 30000 @@ -40,12 +41,35 @@ const logger = baseLogger.child({ module: "exporter" }) const prefix = "galoy" const main = async () => { - const { getLiabilitiesBalance, getLndBalance, getBitcoindBalance, getOnChainBalance } = - ledgerAdmin + const { getLndBalance, getBitcoindBalance, getOnChainBalance } = ledgerAdmin + + createGauge({ + name: "assets", + description: "how much money (BTC) is on books", + collect: async () => { + const getDealerBalance = async (getId: () => Promise) => { + const walletId = await getId() + return getWalletBalance(walletId) + } + + const btcAssets = await ledgerAdmin.getAssetsBalance() + const dealerBtcLiabilities = await getDealerBalance(getDealerBtcWalletId) + + // Dealer BTC liabilities must be deducted from assets because + // Stablesats deposits and withdrawals are processed directly through Bria. + return Math.abs(btcAssets) - dealerBtcLiabilities + }, + }) + createGauge({ name: "liabilities", - description: "how much money customers has", - collect: getLiabilitiesBalance, + description: "how much money (BTC) customers has", + collect: async () => { + const liabilities = await getUserLiabilities() + if (liabilities instanceof Error) return 0 + + return liabilities + }, }) createGauge({ @@ -109,6 +133,12 @@ const main = async () => { }, }) + createGauge({ + name: "realAssetsVsLiabilities", + description: "do we have enough Bitcoin to cover users' liabilities", + collect: getRealAssetsVersusLiabilities, + }) + createGauge({ name: "assetsEqLiabilities", description: "do we have a balanced book", @@ -271,14 +301,31 @@ const createWalletGauge = ({ }) } +const inProgressBalanceQueries = new Map>() + const getWalletBalance = async (walletId: WalletId): Promise => { - const walletBalance = await LedgerService().getWalletBalance(walletId) - if (walletBalance instanceof Error) { - logger.warn({ walletId, walletBalance }, "impossible to get balance") - return 0 + const inProgressKey = `wallet-${walletId}` + + const inProgress = inProgressBalanceQueries.get(inProgressKey) + if (inProgress) { + return inProgress } - return walletBalance + const balancePromise = (async () => { + try { + const walletBalance = await LedgerService().getWalletBalance(walletId) + if (walletBalance instanceof Error) { + logger.warn({ walletId, walletBalance }, "impossible to get balance") + return 0 + } + return walletBalance + } finally { + inProgressBalanceQueries.delete(inProgressKey) + } + })() + + inProgressBalanceQueries.set(inProgressKey, balancePromise) + return balancePromise } const createColdStorageWalletGauge = () => { @@ -311,6 +358,77 @@ const getAssetsLiabilitiesDifference = async () => { return assets + liabilities } +const getUserLiabilities = async () => { + const getDealerBalance = async (getId: () => Promise) => { + const walletId = await getId() + return getWalletBalance(walletId) + } + + const btcLiabilities = await ledgerAdmin.getLiabilitiesBalance() + const dealerBtcLiabilities = await getDealerBalance(getDealerBtcWalletId) + + // Dealer BTC liabilities must be deducted from liabilities because + // Stablesats deposits and withdrawals are processed directly through Bria. + const customerBtcLiabilities = btcLiabilities - dealerBtcLiabilities + + const dealerUsdLiabilities = await getDealerBalance(getDealerUsdWalletId) + logger.info( + { + btcLiabilities, + dealerBtcLiabilities, + customerBtcLiabilities, + dealerUsdLiabilities, + }, + "getUserLiabilities balances", + ) + const dealerUsdLiabilitiesDisplay = displayAmountFromNumber({ + amount: Math.abs(dealerUsdLiabilities), + currency: UsdDisplayCurrency, + }) + if (dealerUsdLiabilitiesDisplay instanceof Error) return dealerUsdLiabilitiesDisplay + + const dealerUsdLiabilitiesInSatsAmount = await Prices.estimateWalletsAmounts({ + amount: Number(dealerUsdLiabilitiesDisplay.displayInMajor), + currency: UsdDisplayCurrency, + }) + logger.info( + { + mayor: dealerUsdLiabilitiesDisplay.displayInMajor, + currency: UsdDisplayCurrency, + dealerUsdLiabilitiesInSatsAmount, + }, + "getUserLiabilities usd balances", + ) + if (dealerUsdLiabilitiesInSatsAmount instanceof Error) + return dealerUsdLiabilitiesInSatsAmount + + return ( + customerBtcLiabilities + Number(dealerUsdLiabilitiesInSatsAmount.btcSatAmount.amount) + ) +} + +const getRealAssetsVersusLiabilities = async () => { + const [liabilitiesBalance, lndBalance, coldStorage, hotBalance] = await Promise.all([ + getUserLiabilities(), + Lightning.getTotalBalance(), + OnChain.getColdBalance(), + OnChain.getHotBalance(), + ]) + + const liabilities = liabilitiesBalance instanceof Error ? 0 : liabilitiesBalance + const lnd = lndBalance instanceof Error ? 0 : lndBalance + const briaHot = hotBalance instanceof Error ? 0 : Number(hotBalance.amount) + const briaCold = coldStorage instanceof Error ? 0 : Number(coldStorage.amount) + + logger.info( + { liabilities, lnd, briaHot, briaCold }, + "getRealAssetsVersusLiabilities balances", + ) + + // if it is a negative value then it must match with exchange stablesats balance + return lnd + briaCold + briaHot - liabilities +} + export const getBookingVersusRealWorldAssets = async () => { const [lightning, bitcoin, onChain, lndBalance, coldStorage, hotBalance] = await Promise.all([ diff --git a/core/api/src/services/ledger/admin-legacy.ts b/core/api/src/services/ledger/admin-legacy.ts index 77a2b59f1a..cb49008fb0 100644 --- a/core/api/src/services/ledger/admin-legacy.ts +++ b/core/api/src/services/ledger/admin-legacy.ts @@ -16,12 +16,30 @@ import { UnknownLedgerError, } from "@/domain/ledger" +const inProgressQueries = new Map>() + const getWalletBalance = async (account: string, query = {}) => { const params = { account, currency: "BTC", ...query } - const { balance } = await MainBookAdmin.balance(params, { - readPreference: "secondaryPreferred", - }) - return balance + const inProgressKey = `${account}-${JSON.stringify(params)}` + + const inProgress = inProgressQueries.get(inProgressKey) + if (inProgress) { + return inProgress + } + + const balancePromise = (async () => { + try { + const { balance } = await MainBookAdmin.balance(params, { + readPreference: "secondaryPreferred", + }) + return balance + } finally { + inProgressQueries.delete(inProgressKey) + } + })() + + inProgressQueries.set(inProgressKey, balancePromise) + return balancePromise } export const getAssetsBalance = (endDate?: Date) =>