From 3f12df8a6237a9e0308c44b11fc487cb88ea5e9f Mon Sep 17 00:00:00 2001 From: Sol Date: Tue, 28 Apr 2026 16:45:05 +0000 Subject: [PATCH 1/2] feat: add custodian_type (hot/cold/ops) to reserve addresses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add custodianType to ReserveAddress: hot (on-chain reserve contracts), cold (custody multisigs, Falcon Finance), ops (operational accounts, rebalancer bot, ops multisig) - Expose custodian_type on /api/v2/addresses per address - Add custodian_type to each collateral source in /api/v2/reserve - Add by_custodian breakdown (hot_usd, cold_usd, ops_usd) to collateral - Remove label disambiguation suffixes — no longer needed with custodian_type for differentiation Co-Authored-By: Claude Opus 4.6 (1M context) --- src/api/v2/config/reserve-addresses.config.ts | 144 ++++-------------- src/api/v2/dto/v2-addresses.dto.ts | 1 + src/api/v2/dto/v2-reserve.dto.ts | 8 + src/api/v2/services/v2-addresses.service.ts | 6 +- src/api/v2/services/v2-positions.service.ts | 28 +++- 5 files changed, 70 insertions(+), 117 deletions(-) diff --git a/src/api/v2/config/reserve-addresses.config.ts b/src/api/v2/config/reserve-addresses.config.ts index 8032681..0e2fb3a 100644 --- a/src/api/v2/config/reserve-addresses.config.ts +++ b/src/api/v2/config/reserve-addresses.config.ts @@ -13,128 +13,42 @@ import { Chain } from '@types'; * The old configs remain for v1 backward compatibility. */ +export type CustodianType = 'hot' | 'cold' | 'ops'; + export interface ReserveAddress { address: string; chain: Chain; label: string; + custodianType: CustodianType; } export const RESERVE_ADDRESSES: ReserveAddress[] = [ // --- Celo --- - { - address: '0x9380fA34Fd9e4Fd14c06305fd7B6199089eD4eb9', - chain: Chain.CELO, - label: 'Mento Pools Liquidity Reserve', - }, - { - address: '0x87647780180b8f55980c7d3ffefe08a9b29e9ae1', - chain: Chain.CELO, - label: 'Custody Multisig', - }, - { - address: '0xD3D2e5c5Af667DA817b2D752d86c8f40c22137E1', - chain: Chain.CELO, - label: 'Ops Multisig', - }, - { - address: '0x6dec25d7be9bf6c6fc302977629f2e801e98611c', - chain: Chain.CELO, - label: 'Operational Account', - }, - { - address: '0x13a9803d547332c81ebc6060f739821264dbcf1e', - chain: Chain.CELO, - label: 'Operational Account', - }, - { - address: '0x619600F4ec13C38868841cB83100F611eCF94eE8', - chain: Chain.CELO, - label: 'Falcon Finance', - }, - { - address: '0xaa8299fc6a685b5f9ce9bda8d0b3ea3d54731976', - chain: Chain.CELO, - label: 'Rebalancer Bot', - }, + { address: '0x9380fA34Fd9e4Fd14c06305fd7B6199089eD4eb9', chain: Chain.CELO, label: 'Mento Pools Liquidity Reserve', custodianType: 'hot' }, + { address: '0x87647780180b8f55980c7d3ffefe08a9b29e9ae1', chain: Chain.CELO, label: 'Custody Multisig', custodianType: 'cold' }, + { address: '0xD3D2e5c5Af667DA817b2D752d86c8f40c22137E1', chain: Chain.CELO, label: 'Ops Multisig', custodianType: 'ops' }, + { address: '0x6dec25d7be9bf6c6fc302977629f2e801e98611c', chain: Chain.CELO, label: 'Operational Account', custodianType: 'ops' }, + { address: '0x13a9803d547332c81ebc6060f739821264dbcf1e', chain: Chain.CELO, label: 'Operational Account', custodianType: 'ops' }, + { address: '0x619600F4ec13C38868841cB83100F611eCF94eE8', chain: Chain.CELO, label: 'Falcon Finance', custodianType: 'cold' }, + { address: '0xaa8299fc6a685b5f9ce9bda8d0b3ea3d54731976', chain: Chain.CELO, label: 'Rebalancer Bot', custodianType: 'ops' }, // --- Ethereum --- - { - address: '0x9380fA34Fd9e4Fd14c06305fd7B6199089eD4eb9', - chain: Chain.ETHEREUM, - label: 'Mento Pools Liquidity Reserve', - }, - { - address: '0xd0697f70E79476195B742d5aFAb14BE50f98CC1E', - chain: Chain.ETHEREUM, - label: 'ETH Custody Multisig', - }, - { - address: '0xD3D2e5c5Af667DA817b2D752d86c8f40c22137E1', - chain: Chain.ETHEREUM, - label: 'Ops Multisig', - }, - { - address: '0xaa8299fc6a685b5f9ce9bda8d0b3ea3d54731976', - chain: Chain.ETHEREUM, - label: 'Rebalancer Bot', - }, - { - address: '0x6dec25d7be9bf6c6fc302977629f2e801e98611c', - chain: Chain.ETHEREUM, - label: 'Operational Account', - }, + { address: '0x9380fA34Fd9e4Fd14c06305fd7B6199089eD4eb9', chain: Chain.ETHEREUM, label: 'Mento Pools Liquidity Reserve', custodianType: 'hot' }, + { address: '0xd0697f70E79476195B742d5aFAb14BE50f98CC1E', chain: Chain.ETHEREUM, label: 'ETH Custody Multisig', custodianType: 'cold' }, + { address: '0xD3D2e5c5Af667DA817b2D752d86c8f40c22137E1', chain: Chain.ETHEREUM, label: 'Ops Multisig', custodianType: 'ops' }, + { address: '0xaa8299fc6a685b5f9ce9bda8d0b3ea3d54731976', chain: Chain.ETHEREUM, label: 'Rebalancer Bot', custodianType: 'ops' }, + { address: '0x6dec25d7be9bf6c6fc302977629f2e801e98611c', chain: Chain.ETHEREUM, label: 'Operational Account', custodianType: 'ops' }, // --- Monad --- - { - address: '0x4255Cf38e51516766180b33122029A88Cb853806', - chain: Chain.MONAD, - label: 'ReserveV2', - }, - { - address: '0x87647780180B8f55980C7D3fFeFe08a9B29e9aE1', - chain: Chain.MONAD, - label: 'Reserve Safe', - }, - { - address: '0x6dec25d7be9bf6c6fc302977629f2e801e98611c', - chain: Chain.MONAD, - label: 'Operational Account', - }, - { - address: '0x13a9803d547332c81ebc6060f739821264dbcf1e', - chain: Chain.MONAD, - label: 'Operational Account', - }, + { address: '0x4255Cf38e51516766180b33122029A88Cb853806', chain: Chain.MONAD, label: 'ReserveV2', custodianType: 'hot' }, + { address: '0x87647780180B8f55980C7D3fFeFe08a9B29e9aE1', chain: Chain.MONAD, label: 'Reserve Safe', custodianType: 'cold' }, + { address: '0x6dec25d7be9bf6c6fc302977629f2e801e98611c', chain: Chain.MONAD, label: 'Operational Account', custodianType: 'ops' }, + { address: '0x13a9803d547332c81ebc6060f739821264dbcf1e', chain: Chain.MONAD, label: 'Operational Account', custodianType: 'ops' }, ]; -/** - * Precomputed map of (chain + address lowercase) → disambiguated label. - * Only adds a suffix when multiple addresses share the same label on the same chain. - */ -const disambiguatedLabels: Map = (() => { - // Count how many addresses share each (chain, label) pair - const labelCounts = new Map(); - for (const a of RESERVE_ADDRESSES) { - const key = `${a.chain}:${a.label}`; - labelCounts.set(key, (labelCounts.get(key) ?? 0) + 1); - } - - const map = new Map(); - for (const a of RESERVE_ADDRESSES) { - const key = `${a.chain}:${a.label}`; - const isDuplicate = (labelCounts.get(key) ?? 0) > 1; - const label = isDuplicate ? `${a.label} (${a.address.slice(0, 6)})` : a.label; - map.set(`${a.chain}:${a.address.toLowerCase()}`, label); - } - return map; -})(); - -/** Get all reserve addresses for a specific chain, with disambiguated labels */ +/** Get all reserve addresses for a specific chain */ export function getReserveAddressesByChain(chain: Chain): ReserveAddress[] { - return RESERVE_ADDRESSES.filter((a) => a.chain === chain).map((a) => ({ - ...a, - label: disambiguatedLabels.get(`${a.chain}:${a.address.toLowerCase()}`) ?? a.label, - })); + return RESERVE_ADDRESSES.filter((a) => a.chain === chain); } /** Check if an address is a reserve address (case-insensitive) on any chain */ @@ -143,14 +57,14 @@ export function isReserveAddress(address: string): boolean { return RESERVE_ADDRESSES.some((a) => a.address.toLowerCase() === lower); } -/** Get the label for a reserve address (case-insensitive), or null if not found. - * If the address exists on multiple chains, returns the first match with disambiguation. */ +/** Get the label for a reserve address (case-insensitive), or null if not found. */ export function getReserveAddressLabel(address: string): string | null { const lower = address.toLowerCase(); - for (const a of RESERVE_ADDRESSES) { - if (a.address.toLowerCase() === lower) { - return disambiguatedLabels.get(`${a.chain}:${lower}`) ?? a.label; - } - } - return null; + return RESERVE_ADDRESSES.find((a) => a.address.toLowerCase() === lower)?.label ?? null; +} + +/** Get the custodian type for a reserve address (case-insensitive). */ +export function getReserveAddressCustodianType(address: string): CustodianType | null { + const lower = address.toLowerCase(); + return RESERVE_ADDRESSES.find((a) => a.address.toLowerCase() === lower)?.custodianType ?? null; } diff --git a/src/api/v2/dto/v2-addresses.dto.ts b/src/api/v2/dto/v2-addresses.dto.ts index d0c2291..679a9f1 100644 --- a/src/api/v2/dto/v2-addresses.dto.ts +++ b/src/api/v2/dto/v2-addresses.dto.ts @@ -4,6 +4,7 @@ import { Chain } from '@types'; export class V2AddressDto { @ApiProperty() address: string; @ApiProperty() label: string; + @ApiProperty({ enum: ['hot', 'cold', 'ops'], required: false }) custodian_type?: string; @ApiProperty({ required: false }) description?: string; } diff --git a/src/api/v2/dto/v2-reserve.dto.ts b/src/api/v2/dto/v2-reserve.dto.ts index aa85cf0..b225f5a 100644 --- a/src/api/v2/dto/v2-reserve.dto.ts +++ b/src/api/v2/dto/v2-reserve.dto.ts @@ -13,6 +13,7 @@ export class V2CollateralSourceDto { @ApiProperty() identifier: string; @ApiProperty() balance: string; @ApiProperty() usd_value: number; + @ApiProperty({ enum: ['hot', 'cold', 'ops'] }) custodian_type: string; } export class V2CollateralAssetDto { @@ -24,8 +25,15 @@ export class V2CollateralAssetDto { @ApiProperty({ type: [V2CollateralSourceDto] }) sources: V2CollateralSourceDto[]; } +export class V2CustodianBreakdownDto { + @ApiProperty() hot_usd: number; + @ApiProperty() cold_usd: number; + @ApiProperty() ops_usd: number; +} + export class V2CollateralDto { @ApiProperty() total_usd: number; + @ApiProperty() by_custodian: V2CustodianBreakdownDto; @ApiProperty({ type: [V2CollateralAssetDto] }) assets: V2CollateralAssetDto[]; } diff --git a/src/api/v2/services/v2-addresses.service.ts b/src/api/v2/services/v2-addresses.service.ts index a2ed1c1..4ec040c 100644 --- a/src/api/v2/services/v2-addresses.service.ts +++ b/src/api/v2/services/v2-addresses.service.ts @@ -21,7 +21,11 @@ export class V2AddressesService { const group = categoryMap.get(categoryKey)!; for (const addr of addresses) { if (!group.addresses.some((a) => a.address.toLowerCase() === addr.address.toLowerCase())) { - group.addresses.push({ address: addr.address, label: addr.label }); + group.addresses.push({ + address: addr.address, + label: addr.label, + custodian_type: addr.custodianType, + }); } } diff --git a/src/api/v2/services/v2-positions.service.ts b/src/api/v2/services/v2-positions.service.ts index 4aaafd1..9fd6318 100644 --- a/src/api/v2/services/v2-positions.service.ts +++ b/src/api/v2/services/v2-positions.service.ts @@ -12,6 +12,7 @@ import { FpmmPositionsService, FpmmPosition } from './fpmm-positions.service'; import { PrimitiveCacheService, STALE_WARNING_THRESHOLD_MS } from './primitive-cache.service'; import { DataWarning } from '../dto/v2-meta.dto'; import { getFiatTickerFromSymbol } from '@common/constants'; +import { getReserveAddressCustodianType } from '../config/reserve-addresses.config'; import { Chain } from '@types'; // --- Aggregated position types for the orchestrator output --- @@ -33,6 +34,7 @@ export interface CollateralSource { identifier: string; balance: string; usd_value: number; + custodian_type: string; } export interface CollateralAssetSummary { @@ -44,8 +46,15 @@ export interface CollateralAssetSummary { sources: CollateralSource[]; } +export interface CustodianBreakdown { + hot_usd: number; + cold_usd: number; + ops_usd: number; +} + export interface CollateralSummary { total_usd: number; + by_custodian: CustodianBreakdown; assets: CollateralAssetSummary[]; } @@ -430,6 +439,7 @@ export class V2PositionsService { identifier: p.address, balance: p.balance, usd_value: p.usd_value, + custodian_type: getReserveAddressCustodianType(p.address) ?? 'ops', }, p.usd_value, ); @@ -448,6 +458,7 @@ export class V2PositionsService { identifier: p.address, balance: p.balance, usd_value: p.usd_value, + custodian_type: getReserveAddressCustodianType(p.address) ?? 'ops', }, p.usd_value, ); @@ -457,6 +468,7 @@ export class V2PositionsService { for (const p of positions.univ3_positions) { const poolLabel = `UniV3 ${p.token0.symbol}/${p.token1.symbol} — ${p.owner_label}`; const poolId = `${p.pool_address}#${p.position_id}`; + const ct = getReserveAddressCustodianType(p.owner) ?? 'ops'; const amount0 = Number(p.token0.amount); const amount1 = Number(p.token1.amount); if (amount0 > 0) { @@ -467,6 +479,7 @@ export class V2PositionsService { identifier: poolId, balance: p.token0.amount, usd_value: usd0, + custodian_type: ct, }); } if (amount1 > 0) { @@ -477,6 +490,7 @@ export class V2PositionsService { identifier: poolId, balance: p.token1.amount, usd_value: usd1, + custodian_type: ct, }); } } @@ -490,6 +504,7 @@ export class V2PositionsService { identifier: p.pool_address, balance: p.collateral_token.amount.toString(), usd_value: usd, + custodian_type: getReserveAddressCustodianType(p.lp_holder) ?? 'ops', }); } @@ -507,6 +522,7 @@ export class V2PositionsService { identifier: `${p.pool_address}:${p.depositor}`, balance: p.collateral_gained, usd_value: p.collateral_gained_usd, + custodian_type: getReserveAddressCustodianType(p.depositor) ?? 'ops', }, p.collateral_gained_usd, ); @@ -516,6 +532,16 @@ export class V2PositionsService { const buckets = Array.from(byKey.values()).sort((a, b) => b.usdValue - a.usdValue); const totalUsd = buckets.reduce((sum, a) => sum + a.usdValue, 0); + // Compute custodian breakdown from all sources + const byCustodian: CustodianBreakdown = { hot_usd: 0, cold_usd: 0, ops_usd: 0 }; + for (const b of buckets) { + for (const s of b.sources) { + if (s.custodian_type === 'hot') byCustodian.hot_usd += s.usd_value; + else if (s.custodian_type === 'cold') byCustodian.cold_usd += s.usd_value; + else byCustodian.ops_usd += s.usd_value; + } + } + const assets: CollateralAssetSummary[] = buckets.map((b) => ({ symbol: b.symbol, chain: b.chain, @@ -525,7 +551,7 @@ export class V2PositionsService { sources: [...b.sources].sort((x, y) => y.usd_value - x.usd_value), })); - return { total_usd: totalUsd, assets }; + return { total_usd: totalUsd, by_custodian: byCustodian, assets }; } /** From e4034083040d9815421a34d2279e72f6baf7e7cc Mon Sep 17 00:00:00 2001 From: Sol Date: Tue, 28 Apr 2026 16:47:02 +0000 Subject: [PATCH 2/2] fix: prettier formatting for reserve addresses table Co-Authored-By: Claude Opus 4.6 (1M context) --- src/api/v2/config/reserve-addresses.config.ts | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/src/api/v2/config/reserve-addresses.config.ts b/src/api/v2/config/reserve-addresses.config.ts index 0e2fb3a..f1ba621 100644 --- a/src/api/v2/config/reserve-addresses.config.ts +++ b/src/api/v2/config/reserve-addresses.config.ts @@ -22,28 +22,27 @@ export interface ReserveAddress { custodianType: CustodianType; } +/* prettier-ignore */ export const RESERVE_ADDRESSES: ReserveAddress[] = [ // --- Celo --- - { address: '0x9380fA34Fd9e4Fd14c06305fd7B6199089eD4eb9', chain: Chain.CELO, label: 'Mento Pools Liquidity Reserve', custodianType: 'hot' }, - { address: '0x87647780180b8f55980c7d3ffefe08a9b29e9ae1', chain: Chain.CELO, label: 'Custody Multisig', custodianType: 'cold' }, - { address: '0xD3D2e5c5Af667DA817b2D752d86c8f40c22137E1', chain: Chain.CELO, label: 'Ops Multisig', custodianType: 'ops' }, - { address: '0x6dec25d7be9bf6c6fc302977629f2e801e98611c', chain: Chain.CELO, label: 'Operational Account', custodianType: 'ops' }, - { address: '0x13a9803d547332c81ebc6060f739821264dbcf1e', chain: Chain.CELO, label: 'Operational Account', custodianType: 'ops' }, - { address: '0x619600F4ec13C38868841cB83100F611eCF94eE8', chain: Chain.CELO, label: 'Falcon Finance', custodianType: 'cold' }, - { address: '0xaa8299fc6a685b5f9ce9bda8d0b3ea3d54731976', chain: Chain.CELO, label: 'Rebalancer Bot', custodianType: 'ops' }, - + { address: '0x9380fA34Fd9e4Fd14c06305fd7B6199089eD4eb9', chain: Chain.CELO, label: 'Mento Pools Liquidity Reserve', custodianType: 'hot' }, + { address: '0x87647780180b8f55980c7d3ffefe08a9b29e9ae1', chain: Chain.CELO, label: 'Custody Multisig', custodianType: 'cold' }, + { address: '0xD3D2e5c5Af667DA817b2D752d86c8f40c22137E1', chain: Chain.CELO, label: 'Ops Multisig', custodianType: 'ops' }, + { address: '0x6dec25d7be9bf6c6fc302977629f2e801e98611c', chain: Chain.CELO, label: 'Operational Account', custodianType: 'ops' }, + { address: '0x13a9803d547332c81ebc6060f739821264dbcf1e', chain: Chain.CELO, label: 'Operational Account', custodianType: 'ops' }, + { address: '0x619600F4ec13C38868841cB83100F611eCF94eE8', chain: Chain.CELO, label: 'Falcon Finance', custodianType: 'cold' }, + { address: '0xaa8299fc6a685b5f9ce9bda8d0b3ea3d54731976', chain: Chain.CELO, label: 'Rebalancer Bot', custodianType: 'ops' }, // --- Ethereum --- { address: '0x9380fA34Fd9e4Fd14c06305fd7B6199089eD4eb9', chain: Chain.ETHEREUM, label: 'Mento Pools Liquidity Reserve', custodianType: 'hot' }, - { address: '0xd0697f70E79476195B742d5aFAb14BE50f98CC1E', chain: Chain.ETHEREUM, label: 'ETH Custody Multisig', custodianType: 'cold' }, - { address: '0xD3D2e5c5Af667DA817b2D752d86c8f40c22137E1', chain: Chain.ETHEREUM, label: 'Ops Multisig', custodianType: 'ops' }, - { address: '0xaa8299fc6a685b5f9ce9bda8d0b3ea3d54731976', chain: Chain.ETHEREUM, label: 'Rebalancer Bot', custodianType: 'ops' }, - { address: '0x6dec25d7be9bf6c6fc302977629f2e801e98611c', chain: Chain.ETHEREUM, label: 'Operational Account', custodianType: 'ops' }, - + { address: '0xd0697f70E79476195B742d5aFAb14BE50f98CC1E', chain: Chain.ETHEREUM, label: 'ETH Custody Multisig', custodianType: 'cold' }, + { address: '0xD3D2e5c5Af667DA817b2D752d86c8f40c22137E1', chain: Chain.ETHEREUM, label: 'Ops Multisig', custodianType: 'ops' }, + { address: '0xaa8299fc6a685b5f9ce9bda8d0b3ea3d54731976', chain: Chain.ETHEREUM, label: 'Rebalancer Bot', custodianType: 'ops' }, + { address: '0x6dec25d7be9bf6c6fc302977629f2e801e98611c', chain: Chain.ETHEREUM, label: 'Operational Account', custodianType: 'ops' }, // --- Monad --- - { address: '0x4255Cf38e51516766180b33122029A88Cb853806', chain: Chain.MONAD, label: 'ReserveV2', custodianType: 'hot' }, - { address: '0x87647780180B8f55980C7D3fFeFe08a9B29e9aE1', chain: Chain.MONAD, label: 'Reserve Safe', custodianType: 'cold' }, - { address: '0x6dec25d7be9bf6c6fc302977629f2e801e98611c', chain: Chain.MONAD, label: 'Operational Account', custodianType: 'ops' }, - { address: '0x13a9803d547332c81ebc6060f739821264dbcf1e', chain: Chain.MONAD, label: 'Operational Account', custodianType: 'ops' }, + { address: '0x4255Cf38e51516766180b33122029A88Cb853806', chain: Chain.MONAD, label: 'ReserveV2', custodianType: 'hot' }, + { address: '0x87647780180B8f55980C7D3fFeFe08a9B29e9aE1', chain: Chain.MONAD, label: 'Reserve Safe', custodianType: 'cold' }, + { address: '0x6dec25d7be9bf6c6fc302977629f2e801e98611c', chain: Chain.MONAD, label: 'Operational Account', custodianType: 'ops' }, + { address: '0x13a9803d547332c81ebc6060f739821264dbcf1e', chain: Chain.MONAD, label: 'Operational Account', custodianType: 'ops' }, ]; /** Get all reserve addresses for a specific chain */