Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 30 additions & 117 deletions src/api/v2/config/reserve-addresses.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,128 +13,41 @@ 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;
}

/* 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',
},

{ 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<string, string> = (() => {
// Count how many addresses share each (chain, label) pair
const labelCounts = new Map<string, number>();
for (const a of RESERVE_ADDRESSES) {
const key = `${a.chain}:${a.label}`;
labelCounts.set(key, (labelCounts.get(key) ?? 0) + 1);
}

const map = new Map<string, string>();
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 */
Expand All @@ -143,14 +56,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;
}
1 change: 1 addition & 0 deletions src/api/v2/dto/v2-addresses.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
8 changes: 8 additions & 0 deletions src/api/v2/dto/v2-reserve.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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[];
}

Expand Down
6 changes: 5 additions & 1 deletion src/api/v2/services/v2-addresses.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
}
}

Expand Down
28 changes: 27 additions & 1 deletion src/api/v2/services/v2-positions.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ---
Expand All @@ -33,6 +34,7 @@ export interface CollateralSource {
identifier: string;
balance: string;
usd_value: number;
custodian_type: string;
}

export interface CollateralAssetSummary {
Expand All @@ -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[];
}

Expand Down Expand Up @@ -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,
);
Expand All @@ -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,
);
Expand All @@ -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) {
Expand All @@ -467,6 +479,7 @@ export class V2PositionsService {
identifier: poolId,
balance: p.token0.amount,
usd_value: usd0,
custodian_type: ct,
});
}
if (amount1 > 0) {
Expand All @@ -477,6 +490,7 @@ export class V2PositionsService {
identifier: poolId,
balance: p.token1.amount,
usd_value: usd1,
custodian_type: ct,
});
}
}
Expand All @@ -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',
});
}

Expand All @@ -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,
);
Expand All @@ -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,
Expand All @@ -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 };
}

/**
Expand Down
Loading