From fb18fdc8c01a8d5d9505363dd5999dd87d5dcc70 Mon Sep 17 00:00:00 2001 From: Dread Date: Mon, 6 Jul 2026 22:11:11 -0500 Subject: [PATCH] feat(metrics): treasury USDT balance gauge (ENG-485 part 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `funder` gauge tracks the funder's DEFAULT wallet (BTC). The cutover treasury is the funder's USDT wallet (resolved by currency, ENG-482), so treasury drain during a cutover run — fee reimbursements, rollback top-ups — is invisible to metrics and alerts. Adds a `treasury_usdt` wallet gauge resolving the funder's USDT wallet, mirroring the cutover treasury resolver. When the treasury isn't provisioned yet (prod pre-cutover) the resolver throws and the gauge's existing cache-fallback path handles it — it simply doesn't report until the wallet exists. Part 1 (IbexError httpCode propagation) is NOT included — see ENG-485: the ibex-client ApiError constructor does `super(e.stack)`, discarding the original error's status entirely, so httpCode cannot be propagated without an upstream ibex-client change. Documented on the ticket. Refs ENG-485 Co-Authored-By: Claude Fable 5 --- src/servers/exporter.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/servers/exporter.ts b/src/servers/exporter.ts index 3b8e6a797..716a31f62 100644 --- a/src/servers/exporter.ts +++ b/src/servers/exporter.ts @@ -6,6 +6,7 @@ import { EXPORTER_PORT, SECS_PER_5_MINS } from "@config" import { Lightning, OnChain } from "@app" import { toSeconds } from "@domain/primitives" +import { WalletCurrency } from "@domain/shared" import { LocalCacheService } from "@services/cache" import { LedgerService } from "@services/ledger" @@ -19,6 +20,7 @@ import { LndService } from "@services/lnd" import { baseLogger } from "@services/logger" import { ledgerAdmin, setupMongoConnection } from "@services/mongodb" import { Account } from "@services/mongoose/schema" +import { WalletsRepository } from "@services/mongoose" import { addAttributesToCurrentSpan, asyncRunInSpan, @@ -158,11 +160,32 @@ const main = async () => { }, }) + // ENG-485: the cutover treasury is the funder's USDT wallet, resolved by + // currency — NOT the funder's default wallet (which the `funder` gauge + // above tracks, and which is BTC). Treasury drain during a cutover run is + // otherwise invisible to metrics. Mirrors the cutover treasury resolver. + const getFunderUsdtWalletId = async (): Promise => { + const funderDefaultWalletId = await getFunderWalletId() + const funderWallet = await WalletsRepository().findById(funderDefaultWalletId) + if (funderWallet instanceof Error) throw funderWallet + if (funderWallet.currency === WalletCurrency.Usdt) return funderWallet.id + const funderWallets = await WalletsRepository().listByAccountId( + funderWallet.accountId, + ) + if (funderWallets instanceof Error) throw funderWallets + const usdtWallet = funderWallets.find((w) => w.currency === WalletCurrency.Usdt) + if (usdtWallet === undefined) { + throw new Error("Funder account has no USDT wallet — treasury not provisioned") + } + return usdtWallet.id + } + const galoyWallets = [ { name: "dealer_btc", getId: getDealerBtcWalletId }, { name: "dealer_usd", getId: getDealerUsdWalletId }, { name: "funder", getId: getFunderWalletId }, { name: "bankowner", getId: getBankOwnerWalletId }, + { name: "treasury_usdt", getId: getFunderUsdtWalletId }, ] for (const wallet of galoyWallets) { createWalletGauge({ walletName: wallet.name, getId: wallet.getId })