181 lines
5.6 KiB
TypeScript
181 lines
5.6 KiB
TypeScript
/**
|
|
* Maps Drips protocol state to rFlows FlowNode[] for visualization.
|
|
* Pure functions — no side effects, no network calls.
|
|
*/
|
|
|
|
import type { FlowNode, SourceNodeData, FunnelNodeData, OutcomeNodeData, DripsNodeConfig, SpendingAllocation } from './types';
|
|
import type { DripsAccountState, DripsStream, DripsSplit } from './drips-client';
|
|
import { dripsAmtToMonthly } from './drips-client';
|
|
|
|
// Common token decimals
|
|
const TOKEN_DECIMALS: Record<string, number> = {
|
|
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48': 6, // USDC (mainnet)
|
|
'0xdac17f958d2ee523a2206206994597c13d831ec7': 6, // USDT (mainnet)
|
|
'0x6b175474e89094c44da98b954eedeac495271d0f': 18, // DAI (mainnet)
|
|
'0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2': 18, // WETH (mainnet)
|
|
};
|
|
|
|
function getTokenDecimals(tokenAddress: string): number {
|
|
return TOKEN_DECIMALS[tokenAddress.toLowerCase()] ?? 18;
|
|
}
|
|
|
|
function getTokenSymbol(tokenAddress: string): string {
|
|
const symbols: Record<string, string> = {
|
|
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48': 'USDC',
|
|
'0xdac17f958d2ee523a2206206994597c13d831ec7': 'USDT',
|
|
'0x6b175474e89094c44da98b954eedeac495271d0f': 'DAI',
|
|
'0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2': 'WETH',
|
|
};
|
|
return symbols[tokenAddress.toLowerCase()] ?? 'TOKEN';
|
|
}
|
|
|
|
const SPENDING_COLORS = ['#3b82f6', '#8b5cf6', '#ec4899', '#06b6d4', '#10b981', '#6366f1'];
|
|
|
|
export interface MapDripsOptions {
|
|
/** Origin offset for layout positioning */
|
|
originX?: number;
|
|
originY?: number;
|
|
/** USD price per token (for rate conversion). Default 1 (stablecoins). */
|
|
usdPrice?: number;
|
|
}
|
|
|
|
/**
|
|
* Map DripsAccountState into FlowNode[] for rFlows canvas.
|
|
*
|
|
* Layout (3-tier grid):
|
|
* Row 0: Source nodes (senders streaming to this address)
|
|
* Row 1: Funnel node (the imported address itself)
|
|
* Row 2: Outcome nodes (split receivers)
|
|
*/
|
|
export function mapDripsToFlowNodes(state: DripsAccountState, options: MapDripsOptions = {}): FlowNode[] {
|
|
const { originX = 100, originY = 100, usdPrice = 1 } = options;
|
|
const nodes: FlowNode[] = [];
|
|
const now = Date.now();
|
|
const addr = state.address.toLowerCase();
|
|
const funnelId = `drips-${addr}`;
|
|
|
|
// ── Row 0: Source nodes (incoming streams grouped by sender) ──
|
|
|
|
const bySender = new Map<string, DripsStream[]>();
|
|
for (const s of state.incomingStreams) {
|
|
if (s.isPaused) continue;
|
|
const key = s.sender.toLowerCase();
|
|
if (!bySender.has(key)) bySender.set(key, []);
|
|
bySender.get(key)!.push(s);
|
|
}
|
|
|
|
let srcIdx = 0;
|
|
for (const [sender, streams] of bySender) {
|
|
const totalRate = streams.reduce((sum, s) => {
|
|
const dec = getTokenDecimals(s.tokenAddress);
|
|
return sum + dripsAmtToMonthly(s.amtPerSec, dec, usdPrice);
|
|
}, 0);
|
|
|
|
const shortAddr = `${sender.slice(0, 6)}...${sender.slice(-4)}`;
|
|
const tokens = [...new Set(streams.map(s => getTokenSymbol(s.tokenAddress)))].join('/');
|
|
const sourceId = `drips-src-${sender}`;
|
|
|
|
const dripsConfig: DripsNodeConfig = {
|
|
chainId: state.chainId,
|
|
address: sender,
|
|
streamIds: streams.map(s => s.id),
|
|
importedAt: now,
|
|
};
|
|
|
|
nodes.push({
|
|
id: sourceId,
|
|
type: 'source',
|
|
position: { x: originX + srcIdx * 280, y: originY },
|
|
data: {
|
|
label: `${shortAddr} (${tokens})`,
|
|
flowRate: Math.round(totalRate * 100) / 100,
|
|
sourceType: 'safe_wallet',
|
|
targetAllocations: [{
|
|
targetId: funnelId,
|
|
percentage: 100,
|
|
color: '#10b981',
|
|
}],
|
|
dripsConfig,
|
|
} as SourceNodeData,
|
|
});
|
|
srcIdx++;
|
|
}
|
|
|
|
// ── Row 1: Funnel node (the imported address) ──
|
|
|
|
const totalInflow = nodes.reduce((sum, n) => {
|
|
if (n.type === 'source') return sum + (n.data as SourceNodeData).flowRate;
|
|
return sum;
|
|
}, 0);
|
|
|
|
// Build spending allocations from splits
|
|
const spendingAllocations: SpendingAllocation[] = state.splits.map((split, i) => {
|
|
const receiverAddr = split.receiver.toLowerCase();
|
|
return {
|
|
targetId: `drips-out-${receiverAddr}`,
|
|
percentage: (split.weight / 1_000_000) * 100,
|
|
color: SPENDING_COLORS[i % SPENDING_COLORS.length],
|
|
};
|
|
});
|
|
|
|
// Unallocated remainder (no splits receiver) stays in funnel
|
|
const totalSplitPct = spendingAllocations.reduce((s, a) => s + a.percentage, 0);
|
|
|
|
const funnelConfig: DripsNodeConfig = {
|
|
chainId: state.chainId,
|
|
address: state.address,
|
|
importedAt: now,
|
|
};
|
|
|
|
nodes.push({
|
|
id: funnelId,
|
|
type: 'funnel',
|
|
position: { x: originX + Math.max(0, (srcIdx - 1)) * 140, y: originY + 250 },
|
|
data: {
|
|
label: `${state.address.slice(0, 6)}...${state.address.slice(-4)}`,
|
|
currentValue: 0,
|
|
overflowThreshold: totalInflow * 3, // 3 months buffer
|
|
capacity: totalInflow * 6,
|
|
inflowRate: totalInflow,
|
|
drainRate: totalInflow * (totalSplitPct / 100),
|
|
overflowAllocations: [],
|
|
spendingAllocations,
|
|
dripsConfig: funnelConfig,
|
|
} as FunnelNodeData,
|
|
});
|
|
|
|
// ── Row 2: Outcome nodes (split receivers) ──
|
|
|
|
state.splits.forEach((split, i) => {
|
|
const receiverAddr = split.receiver.toLowerCase();
|
|
const outcomeId = `drips-out-${receiverAddr}`;
|
|
const shortAddr = `${receiverAddr.slice(0, 6)}...${receiverAddr.slice(-4)}`;
|
|
const pct = (split.weight / 1_000_000) * 100;
|
|
const monthlyAmount = totalInflow * (pct / 100);
|
|
|
|
const dripsConfig: DripsNodeConfig = {
|
|
chainId: state.chainId,
|
|
address: split.receiver,
|
|
splitWeight: split.weight,
|
|
splitIndex: i,
|
|
importedAt: now,
|
|
};
|
|
|
|
nodes.push({
|
|
id: outcomeId,
|
|
type: 'outcome',
|
|
position: { x: originX + i * 280, y: originY + 500 },
|
|
data: {
|
|
label: `${shortAddr} (${pct.toFixed(1)}%)`,
|
|
description: `Drips split receiver — ${pct.toFixed(1)}% of incoming`,
|
|
fundingReceived: 0,
|
|
fundingTarget: monthlyAmount * 12, // annualized target
|
|
status: 'in-progress',
|
|
dripsConfig,
|
|
} as OutcomeNodeData,
|
|
});
|
|
});
|
|
|
|
return nodes;
|
|
}
|