From edac2b6bbacc9b19c24cf22f0f751e6c65cffca8 Mon Sep 17 00:00:00 2001 From: Sol Date: Wed, 29 Apr 2026 13:40:24 +0000 Subject: [PATCH] feat: flatten addresses API, add custodian types and descriptions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Flatten /v2/addresses response to { reserve: [...] } with chains[] per address - Remove CDP section from addresses endpoint (not relevant) - Add custodian_type (hot/cold/ops) and descriptions to all reserve addresses - Deduplicate address config using chains[] array (13 → 7 entries) - Rename labels: Mento V2/V3 Liquidity Reserve, Reserve Safe, Ops Safe, Ops Account - Remove 0x6dec and Falcon Finance from all accounting - Add 0x4255 (Mento V3 Liquidity Reserve) to Celo - Add by_custodian breakdown to collateral response Co-Authored-By: Claude Opus 4.6 (1M context) --- src/api/reserve/config/addresses.config.ts | 8 - src/api/v2/config/reserve-addresses.config.ts | 144 +++--------------- .../v2/controllers/v2-addresses.controller.ts | 2 +- src/api/v2/dto/v2-addresses.dto.ts | 14 +- src/api/v2/dto/v2-reserve.dto.ts | 8 + src/api/v2/services/v2-addresses.service.ts | 71 ++------- src/api/v2/services/v2-positions.service.ts | 28 +++- 7 files changed, 73 insertions(+), 202 deletions(-) diff --git a/src/api/reserve/config/addresses.config.ts b/src/api/reserve/config/addresses.config.ts index e1d039b..c9554b3 100644 --- a/src/api/reserve/config/addresses.config.ts +++ b/src/api/reserve/config/addresses.config.ts @@ -81,14 +81,6 @@ export const RESERVE_ADDRESS_CONFIGS: ReserveAddressConfig[] = [ assets: ['CELO', 'USDT'], description: 'Reserve assets held in the Aave protocol', }, - { - address: '0x619600F4ec13C38868841cB83100F611eCF94eE8', - chain: Chain.CELO, - category: AddressCategory.MENTO_RESERVE, - label: 'Falcon Finance Custodied Funds', - assets: ['CELO'], - description: 'Reserve assets custodied by Falcon Finance', - }, { address: '0xaa8299fc6a685b5f9ce9bda8d0b3ea3d54731976', chain: Chain.CELO, diff --git a/src/api/v2/config/reserve-addresses.config.ts b/src/api/v2/config/reserve-addresses.config.ts index 8032681..361357c 100644 --- a/src/api/v2/config/reserve-addresses.config.ts +++ b/src/api/v2/config/reserve-addresses.config.ts @@ -13,128 +13,30 @@ 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; + chains: Chain[]; label: string; + custodianType: CustodianType; + description?: string; } +/* prettier-ignore */ 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', - }, - - // --- 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', - }, - - // --- 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: '0x9380fA34Fd9e4Fd14c06305fd7B6199089eD4eb9', chains: [Chain.CELO, Chain.ETHEREUM], label: 'Mento V2 Liquidity Reserve', custodianType: 'hot', description: 'Active liquidity for stablecoin swaps in Mento V2' }, + { address: '0x4255Cf38e51516766180b33122029A88Cb853806', chains: [Chain.CELO, Chain.MONAD], label: 'Mento V3 Liquidity Reserve', custodianType: 'hot', description: 'Active liquidity for stablecoin swaps in Mento V3' }, + { address: '0x87647780180B8f55980C7D3fFeFe08a9B29e9aE1', chains: [Chain.CELO, Chain.MONAD], label: 'Reserve Safe', custodianType: 'cold', description: 'Mento Reserve Asset Custody' }, + { address: '0xd0697f70E79476195B742d5aFAb14BE50f98CC1E', chains: [Chain.ETHEREUM], label: 'Reserve Safe', custodianType: 'cold', description: 'Mento Reserve Asset Custody' }, + { address: '0xD3D2e5c5Af667DA817b2D752d86c8f40c22137E1', chains: [Chain.CELO, Chain.ETHEREUM], label: 'Ops Safe', custodianType: 'ops', description: 'Accounts used to provide liquidity to FPMMs and other protocols on behalf of the reserve' }, + { address: '0x13a9803d547332c81ebc6060f739821264dbcf1e', chains: [Chain.CELO, Chain.MONAD], label: 'Ops Account', custodianType: 'ops', description: 'Accounts used to provide liquidity to FPMMs and other protocols on behalf of the reserve' }, + { address: '0xaa8299fc6a685b5f9ce9bda8d0b3ea3d54731976', chains: [Chain.CELO, Chain.ETHEREUM], label: 'Rebalancer Bot', custodianType: 'ops', description: 'Account executing active rebalancing on behalf of the reserve' }, ]; -/** - * 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.chains.includes(chain)); } /** Check if an address is a reserve address (case-insensitive) on any chain */ @@ -143,14 +45,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/controllers/v2-addresses.controller.ts b/src/api/v2/controllers/v2-addresses.controller.ts index d527656..ded6f08 100644 --- a/src/api/v2/controllers/v2-addresses.controller.ts +++ b/src/api/v2/controllers/v2-addresses.controller.ts @@ -14,7 +14,7 @@ export class V2AddressesController { ) {} @Get() - @ApiOperation({ summary: 'Get reserve addresses grouped by network and category' }) + @ApiOperation({ summary: 'Get reserve addresses with chain and custodian metadata' }) @ApiResponse({ status: 200, type: V2AddressesResponseDto }) async getAddresses(): Promise { return this.cacheWarmerService.getOrRevalidate(V2_CACHE_KEYS.ADDRESSES, () => diff --git a/src/api/v2/dto/v2-addresses.dto.ts b/src/api/v2/dto/v2-addresses.dto.ts index d0c2291..6aab889 100644 --- a/src/api/v2/dto/v2-addresses.dto.ts +++ b/src/api/v2/dto/v2-addresses.dto.ts @@ -3,20 +3,12 @@ import { Chain } from '@types'; export class V2AddressDto { @ApiProperty() address: string; + @ApiProperty({ enum: Chain, isArray: true }) chains: Chain[]; @ApiProperty() label: string; + @ApiProperty({ enum: ['hot', 'cold', 'ops'] }) custodian_type: string; @ApiProperty({ required: false }) description?: string; } -export class V2AddressCategoryDto { - @ApiProperty() category: string; - @ApiProperty({ type: [V2AddressDto] }) addresses: V2AddressDto[]; -} - -export class V2NetworkAddressesDto { - @ApiProperty({ enum: Chain }) chain: Chain; - @ApiProperty({ type: [V2AddressCategoryDto] }) categories: V2AddressCategoryDto[]; -} - export class V2AddressesResponseDto { - @ApiProperty({ type: [V2NetworkAddressesDto] }) networks: V2NetworkAddressesDto[]; + @ApiProperty({ type: [V2AddressDto] }) reserve: V2AddressDto[]; } 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..7701c32 100644 --- a/src/api/v2/services/v2-addresses.service.ts +++ b/src/api/v2/services/v2-addresses.service.ts @@ -1,67 +1,18 @@ import { Injectable } from '@nestjs/common'; -import { getReserveAddressesByChain } from '../config/reserve-addresses.config'; -import { CDP_TROVE_CONFIGS, CDP_REGISTRIES } from '../config/cdp.config'; -import { V2AddressesResponseDto, V2NetworkAddressesDto, V2AddressCategoryDto } from '../dto/v2-addresses.dto'; -import { Chain } from '@types'; +import { RESERVE_ADDRESSES } from '../config/reserve-addresses.config'; +import { V2AddressesResponseDto } from '../dto/v2-addresses.dto'; @Injectable() export class V2AddressesService { getAddresses(): V2AddressesResponseDto { - const networkMap = new Map>(); - const chainOrder = [Chain.CELO, Chain.ETHEREUM, Chain.MONAD]; - - for (const chain of chainOrder) { - const addresses = getReserveAddressesByChain(chain); - if (addresses.length === 0) continue; - - const categoryMap = new Map(); - const categoryKey = 'Mento Reserve'; - categoryMap.set(categoryKey, { category: categoryKey, addresses: [] }); - - 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 }); - } - } - - networkMap.set(chain, categoryMap); - } - - // Add CDP contract addresses - for (const cdp of CDP_TROVE_CONFIGS) { - if (cdp.status !== 'active') continue; - - const contractAddress = cdp.contractAddress || CDP_REGISTRIES[cdp.stablecoin]; - if (!contractAddress) continue; - - if (!networkMap.has(cdp.chain)) { - networkMap.set(cdp.chain, new Map()); - } - - const categoryMap = networkMap.get(cdp.chain)!; - const categoryKey = 'CDP Contract'; - - if (!categoryMap.has(categoryKey)) { - categoryMap.set(categoryKey, { category: categoryKey, addresses: [] }); - } - - const label = cdp.contractAddress ? `${cdp.stablecoin} TroveManager` : `${cdp.stablecoin} AddressesRegistry`; - - categoryMap.get(categoryKey)!.addresses.push({ - address: contractAddress, - label, - description: `CDP system for minting ${cdp.stablecoin} with ${cdp.collateralToken} collateral`, - }); - } - - const networks: V2NetworkAddressesDto[] = chainOrder - .filter((c) => networkMap.has(c)) - .map((chain) => ({ - chain, - categories: Array.from(networkMap.get(chain)!.values()), - })); - - return { networks }; + return { + reserve: RESERVE_ADDRESSES.map((addr) => ({ + address: addr.address, + chains: addr.chains, + label: addr.label, + custodian_type: addr.custodianType, + description: addr.description, + })), + }; } } 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 }; } /**