590 lines
19 KiB
TypeScript
590 lines
19 KiB
TypeScript
/**
|
|
* Data Transform Module for rWallet
|
|
* Converts Safe Global API responses into formats for D3 visualizations.
|
|
* TypeScript port of rwallet-online/js/data-transform.js
|
|
*/
|
|
|
|
// ── Interfaces ──
|
|
|
|
export interface TimelineEntry {
|
|
date: Date;
|
|
type: "in" | "out";
|
|
amount: number;
|
|
token: string;
|
|
usd: number;
|
|
hasUsdEstimate: boolean;
|
|
chain: string;
|
|
chainId: string;
|
|
from?: string;
|
|
fromFull?: string;
|
|
to?: string;
|
|
toFull?: string;
|
|
}
|
|
|
|
export interface SankeyNode {
|
|
name: string;
|
|
type: "wallet" | "source" | "target";
|
|
address: string;
|
|
}
|
|
|
|
export interface SankeyLink {
|
|
source: number;
|
|
target: number;
|
|
value: number;
|
|
token: string;
|
|
}
|
|
|
|
export interface SankeyData {
|
|
nodes: SankeyNode[];
|
|
links: SankeyLink[];
|
|
}
|
|
|
|
export interface ChainStats {
|
|
transfers: number;
|
|
inflow: string;
|
|
outflow: string;
|
|
addresses: string;
|
|
period: string;
|
|
}
|
|
|
|
export interface TransferRecord {
|
|
chainId: string;
|
|
chainName: string;
|
|
date: string;
|
|
from?: string;
|
|
fromShort?: string;
|
|
to?: string;
|
|
toShort?: string;
|
|
token: string;
|
|
amount: number;
|
|
usd: number;
|
|
}
|
|
|
|
export interface MultichainData {
|
|
chainStats: Record<string, ChainStats>;
|
|
flowData: Record<string, FlowEntry[]>;
|
|
allTransfers: { incoming: TransferRecord[]; outgoing: TransferRecord[] };
|
|
}
|
|
|
|
export interface FlowEntry {
|
|
from: string;
|
|
to: string;
|
|
value: number;
|
|
token: string;
|
|
chain: string;
|
|
}
|
|
|
|
// ── Helpers ──
|
|
|
|
export function shortenAddress(addr: string): string {
|
|
if (!addr || addr.length < 10) return addr || "Unknown";
|
|
return addr.slice(0, 6) + "..." + addr.slice(-4);
|
|
}
|
|
|
|
const EXPLORER_URLS: Record<string, string> = {
|
|
"1": "https://etherscan.io",
|
|
"10": "https://optimistic.etherscan.io",
|
|
"100": "https://gnosisscan.io",
|
|
"137": "https://polygonscan.com",
|
|
"8453": "https://basescan.org",
|
|
"42161": "https://arbiscan.io",
|
|
"42220": "https://celoscan.io",
|
|
"43114": "https://snowtrace.io",
|
|
"56": "https://bscscan.com",
|
|
"324": "https://explorer.zksync.io",
|
|
};
|
|
|
|
export function explorerLink(address: string, chainId: string): string {
|
|
const base = EXPLORER_URLS[chainId];
|
|
if (!base) return "#";
|
|
return `${base}/address/${address}`;
|
|
}
|
|
|
|
export function txExplorerLink(txHash: string, chainId: string): string {
|
|
const base = EXPLORER_URLS[chainId];
|
|
if (!base) return "#";
|
|
return `${base}/tx/${txHash}`;
|
|
}
|
|
|
|
export function getTransferValue(transfer: any): number {
|
|
if (transfer.type === "ERC20_TRANSFER" || transfer.transferType === "ERC20_TRANSFER") {
|
|
const decimals = transfer.tokenInfo?.decimals || transfer.token?.decimals || 18;
|
|
const raw = transfer.value || "0";
|
|
return parseFloat(raw) / Math.pow(10, decimals);
|
|
}
|
|
if (transfer.type === "ETHER_TRANSFER" || transfer.transferType === "ETHER_TRANSFER") {
|
|
return parseFloat(transfer.value || "0") / 1e18;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
export function getTokenSymbol(transfer: any): string {
|
|
return transfer.tokenInfo?.symbol || transfer.token?.symbol || "ETH";
|
|
}
|
|
|
|
function getTokenName(transfer: any): string {
|
|
return transfer.tokenInfo?.name || transfer.token?.name || "Native";
|
|
}
|
|
|
|
// ── Stablecoin USD estimation ──
|
|
|
|
const STABLECOINS = new Set([
|
|
"USDC", "USDT", "DAI", "WXDAI", "BUSD", "TUSD", "USDP", "FRAX",
|
|
"LUSD", "GUSD", "sUSD", "USDD", "USDGLO", "USD+", "USDe", "crvUSD",
|
|
"GHO", "PYUSD", "DOLA", "Yield-USD", "yUSD",
|
|
]);
|
|
|
|
export function estimateUSD(value: number, symbol: string): number | null {
|
|
if (STABLECOINS.has(symbol)) return value;
|
|
return null;
|
|
}
|
|
|
|
// ── Native token symbols per chain ──
|
|
|
|
const CHAIN_NATIVE_SYMBOL: Record<string, string> = {
|
|
"1": "ETH", "10": "ETH", "100": "xDAI", "137": "MATIC",
|
|
"8453": "ETH", "42161": "ETH", "42220": "CELO", "43114": "AVAX",
|
|
"56": "BNB", "324": "ETH",
|
|
};
|
|
|
|
// ── Transform: Timeline Data (for Balance River) ──
|
|
|
|
export function transformToTimelineData(
|
|
chainDataMap: Map<string, any>,
|
|
safeAddress: string,
|
|
chainNames: Record<string, string>,
|
|
): TimelineEntry[] {
|
|
const timeline: any[] = [];
|
|
|
|
for (const [chainId, data] of chainDataMap) {
|
|
const chainName = (chainNames[chainId] || `chain-${chainId}`).toLowerCase();
|
|
|
|
// Incoming transfers
|
|
if (data.incoming) {
|
|
for (const transfer of data.incoming) {
|
|
const value = getTransferValue(transfer);
|
|
const symbol = getTokenSymbol(transfer);
|
|
if (value <= 0) continue;
|
|
|
|
const usd = estimateUSD(value, symbol);
|
|
timeline.push({
|
|
date: transfer.executionDate || transfer.blockTimestamp || transfer.timestamp,
|
|
type: "in",
|
|
amount: value,
|
|
token: symbol,
|
|
usd: usd !== null ? usd : value,
|
|
hasUsdEstimate: usd !== null,
|
|
chain: chainName,
|
|
chainId,
|
|
from: shortenAddress(transfer.from),
|
|
fromFull: transfer.from,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Outgoing multisig transactions
|
|
if (data.outgoing) {
|
|
for (const tx of data.outgoing) {
|
|
if (!tx.isExecuted) continue;
|
|
|
|
const txTransfers: { to: string; value: number; symbol: string; usd: number | null }[] = [];
|
|
|
|
// Check transfers array if available
|
|
if (tx.transfers && tx.transfers.length > 0) {
|
|
for (const t of tx.transfers) {
|
|
if (t.from?.toLowerCase() === safeAddress.toLowerCase()) {
|
|
const value = getTransferValue(t);
|
|
const symbol = getTokenSymbol(t);
|
|
if (value > 0) {
|
|
txTransfers.push({ to: t.to, value, symbol, usd: estimateUSD(value, symbol) });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback: parse from dataDecoded or direct value
|
|
if (txTransfers.length === 0) {
|
|
if (tx.value && tx.value !== "0") {
|
|
const val = parseFloat(tx.value) / 1e18;
|
|
const sym = CHAIN_NATIVE_SYMBOL[chainId] || "ETH";
|
|
txTransfers.push({ to: tx.to, value: val, symbol: sym, usd: estimateUSD(val, sym) });
|
|
}
|
|
|
|
if (tx.dataDecoded?.method === "transfer") {
|
|
const params = tx.dataDecoded.parameters || [];
|
|
const to = params.find((p: any) => p.name === "to")?.value;
|
|
const rawVal = params.find((p: any) => p.name === "value")?.value || "0";
|
|
const val = parseFloat(rawVal) / 1e18;
|
|
txTransfers.push({ to, value: val, symbol: "Token", usd: null });
|
|
}
|
|
|
|
if (tx.dataDecoded?.method === "multiSend") {
|
|
const txsParam = tx.dataDecoded.parameters?.find((p: any) => p.name === "transactions");
|
|
if (txsParam?.valueDecoded) {
|
|
for (const inner of txsParam.valueDecoded) {
|
|
if (inner.value && inner.value !== "0") {
|
|
const val = parseFloat(inner.value) / 1e18;
|
|
const sym = CHAIN_NATIVE_SYMBOL[chainId] || "ETH";
|
|
txTransfers.push({ to: inner.to, value: val, symbol: sym, usd: estimateUSD(val, sym) });
|
|
}
|
|
if (inner.dataDecoded?.method === "transfer") {
|
|
const to2 = inner.dataDecoded.parameters?.find((p: any) => p.name === "to")?.value;
|
|
const raw2 = inner.dataDecoded.parameters?.find((p: any) => p.name === "value")?.value || "0";
|
|
const val2 = parseFloat(raw2) / 1e18;
|
|
txTransfers.push({ to: to2, value: val2, symbol: "Token", usd: null });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const t of txTransfers) {
|
|
const usd = t.usd !== null ? t.usd : t.value;
|
|
timeline.push({
|
|
date: tx.executionDate,
|
|
type: "out",
|
|
amount: t.value,
|
|
token: t.symbol,
|
|
usd,
|
|
hasUsdEstimate: t.usd !== null,
|
|
chain: chainName,
|
|
chainId,
|
|
to: shortenAddress(t.to),
|
|
toFull: t.to,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return timeline
|
|
.filter((t) => t.date)
|
|
.map((t) => ({ ...t, date: new Date(t.date) }))
|
|
.sort((a, b) => a.date.getTime() - b.date.getTime());
|
|
}
|
|
|
|
// ── Transform: Sankey Data (for single-chain flow) ──
|
|
|
|
export function transformToSankeyData(chainData: any, safeAddress: string, chainId?: string): SankeyData {
|
|
const nodeMap = new Map<string, number>();
|
|
const nodes: SankeyNode[] = [];
|
|
const links: SankeyLink[] = [];
|
|
const walletLabel = "Safe Wallet";
|
|
|
|
function getNodeIndex(address: string, type: "wallet" | "source" | "target"): number {
|
|
const key = address.toLowerCase() === safeAddress.toLowerCase()
|
|
? "wallet"
|
|
: `${type}:${address.toLowerCase()}`;
|
|
|
|
if (!nodeMap.has(key)) {
|
|
const idx = nodes.length;
|
|
nodeMap.set(key, idx);
|
|
const label = address.toLowerCase() === safeAddress.toLowerCase()
|
|
? walletLabel
|
|
: shortenAddress(address);
|
|
nodes.push({ name: label, type, address });
|
|
}
|
|
return nodeMap.get(key)!;
|
|
}
|
|
|
|
// Wallet node always first
|
|
getNodeIndex(safeAddress, "wallet");
|
|
|
|
// Aggregate inflows by source address + token
|
|
const inflowAgg = new Map<string, { from: string; value: number; symbol: string }>();
|
|
if (chainData.incoming) {
|
|
for (const transfer of chainData.incoming) {
|
|
const value = getTransferValue(transfer);
|
|
const symbol = getTokenSymbol(transfer);
|
|
if (value <= 0 || !transfer.from) continue;
|
|
|
|
const key = `${transfer.from.toLowerCase()}:${symbol}`;
|
|
const existing = inflowAgg.get(key) || { from: transfer.from, value: 0, symbol };
|
|
existing.value += value;
|
|
inflowAgg.set(key, existing);
|
|
}
|
|
}
|
|
|
|
// Add inflow links
|
|
for (const [, agg] of inflowAgg) {
|
|
const sourceIdx = getNodeIndex(agg.from, "source");
|
|
const walletIdx = nodeMap.get("wallet")!;
|
|
links.push({ source: sourceIdx, target: walletIdx, value: agg.value, token: agg.symbol });
|
|
}
|
|
|
|
// Aggregate outflows by target address + token
|
|
const outflowAgg = new Map<string, { to: string; value: number; symbol: string }>();
|
|
if (chainData.outgoing) {
|
|
for (const tx of chainData.outgoing) {
|
|
if (!tx.isExecuted) continue;
|
|
|
|
if (tx.value && tx.value !== "0" && tx.to) {
|
|
const val = parseFloat(tx.value) / 1e18;
|
|
const sym = (chainId && CHAIN_NATIVE_SYMBOL[chainId]) || "ETH";
|
|
const key = `${tx.to.toLowerCase()}:${sym}`;
|
|
const existing = outflowAgg.get(key) || { to: tx.to, value: 0, symbol: sym };
|
|
existing.value += val;
|
|
outflowAgg.set(key, existing);
|
|
}
|
|
|
|
if (tx.dataDecoded?.method === "transfer") {
|
|
const params = tx.dataDecoded.parameters || [];
|
|
const to = params.find((p: any) => p.name === "to")?.value;
|
|
const rawVal = params.find((p: any) => p.name === "value")?.value || "0";
|
|
if (to) {
|
|
const val = parseFloat(rawVal) / 1e18;
|
|
const key = `${to.toLowerCase()}:Token`;
|
|
const existing = outflowAgg.get(key) || { to, value: 0, symbol: "Token" };
|
|
existing.value += val;
|
|
outflowAgg.set(key, existing);
|
|
}
|
|
}
|
|
|
|
if (tx.dataDecoded?.method === "multiSend") {
|
|
const txsParam = tx.dataDecoded.parameters?.find((p: any) => p.name === "transactions");
|
|
if (txsParam?.valueDecoded) {
|
|
for (const inner of txsParam.valueDecoded) {
|
|
if (inner.value && inner.value !== "0" && inner.to) {
|
|
const val = parseFloat(inner.value) / 1e18;
|
|
const sym = (chainId && CHAIN_NATIVE_SYMBOL[chainId]) || "ETH";
|
|
const key = `${inner.to.toLowerCase()}:${sym}`;
|
|
const existing = outflowAgg.get(key) || { to: inner.to, value: 0, symbol: sym };
|
|
existing.value += val;
|
|
outflowAgg.set(key, existing);
|
|
}
|
|
if (inner.dataDecoded?.method === "transfer") {
|
|
const to2 = inner.dataDecoded.parameters?.find((p: any) => p.name === "to")?.value;
|
|
const raw2 = inner.dataDecoded.parameters?.find((p: any) => p.name === "value")?.value || "0";
|
|
if (to2) {
|
|
const val2 = parseFloat(raw2) / 1e18;
|
|
const key = `${to2.toLowerCase()}:Token`;
|
|
const existing = outflowAgg.get(key) || { to: to2, value: 0, symbol: "Token" };
|
|
existing.value += val2;
|
|
outflowAgg.set(key, existing);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add outflow links
|
|
const walletIdx = nodeMap.get("wallet")!;
|
|
for (const [, agg] of outflowAgg) {
|
|
const targetIdx = getNodeIndex(agg.to, "target");
|
|
links.push({ source: walletIdx, target: targetIdx, value: agg.value, token: agg.symbol });
|
|
}
|
|
|
|
// Filter out tiny values (noise)
|
|
const maxValue = Math.max(...links.map((l) => l.value), 1);
|
|
const threshold = maxValue * 0.001;
|
|
const filteredLinks = links.filter((l) => l.value >= threshold);
|
|
|
|
return { nodes, links: filteredLinks };
|
|
}
|
|
|
|
// ── Transform: Multi-Chain Flow Data ──
|
|
|
|
function formatUSDValue(value: number): string {
|
|
if (value >= 1000000) return `~$${(value / 1000000).toFixed(1)}M`;
|
|
if (value >= 1000) return `~$${Math.round(value / 1000)}K`;
|
|
return `~$${Math.round(value)}`;
|
|
}
|
|
|
|
export function transformToMultichainData(
|
|
chainDataMap: Map<string, any>,
|
|
safeAddress: string,
|
|
chainNames: Record<string, string>,
|
|
): MultichainData {
|
|
const chainStats: Record<string, ChainStats> = {};
|
|
const flowData: Record<string, FlowEntry[]> = {};
|
|
const allTransfers: { incoming: TransferRecord[]; outgoing: TransferRecord[] } = { incoming: [], outgoing: [] };
|
|
let totalTransfers = 0;
|
|
let totalInflow = 0;
|
|
let totalOutflow = 0;
|
|
const allAddresses = new Set<string>();
|
|
let minDate: Date | null = null;
|
|
let maxDate: Date | null = null;
|
|
|
|
for (const [chainId, data] of chainDataMap) {
|
|
const chainName = (chainNames[chainId] || `chain-${chainId}`).toLowerCase();
|
|
let chainTransfers = 0;
|
|
let chainInflow = 0;
|
|
let chainOutflow = 0;
|
|
const chainAddresses = new Set<string>();
|
|
let chainMinDate: Date | null = null;
|
|
let chainMaxDate: Date | null = null;
|
|
const flows: FlowEntry[] = [];
|
|
|
|
// Incoming
|
|
const inflowAgg = new Map<string, { from: string; value: number; token: string }>();
|
|
if (data.incoming) {
|
|
for (const transfer of data.incoming) {
|
|
const value = getTransferValue(transfer);
|
|
const symbol = getTokenSymbol(transfer);
|
|
if (value <= 0) continue;
|
|
|
|
const usd = estimateUSD(value, symbol);
|
|
const usdVal = usd !== null ? usd : value;
|
|
chainTransfers++;
|
|
chainInflow += usdVal;
|
|
if (transfer.from) {
|
|
chainAddresses.add(transfer.from.toLowerCase());
|
|
allAddresses.add(transfer.from.toLowerCase());
|
|
}
|
|
|
|
const date = transfer.executionDate || transfer.blockTimestamp;
|
|
if (date) {
|
|
const d = new Date(date);
|
|
if (!chainMinDate || d < chainMinDate) chainMinDate = d;
|
|
if (!chainMaxDate || d > chainMaxDate) chainMaxDate = d;
|
|
}
|
|
|
|
const from = transfer.from || "Unknown";
|
|
const key = shortenAddress(from);
|
|
const existing = inflowAgg.get(key) || { from: shortenAddress(from), value: 0, token: symbol };
|
|
existing.value += usdVal;
|
|
inflowAgg.set(key, existing);
|
|
|
|
allTransfers.incoming.push({
|
|
chainId, chainName,
|
|
date: date || "",
|
|
from: transfer.from,
|
|
fromShort: shortenAddress(transfer.from),
|
|
token: symbol,
|
|
amount: value,
|
|
usd: usdVal,
|
|
});
|
|
}
|
|
}
|
|
|
|
for (const [, agg] of inflowAgg) {
|
|
flows.push({ from: agg.from, to: "Safe Wallet", value: Math.round(agg.value), token: agg.token, chain: chainName });
|
|
}
|
|
|
|
// Outgoing
|
|
const outflowAgg = new Map<string, { to: string; value: number; token: string }>();
|
|
if (data.outgoing) {
|
|
for (const tx of data.outgoing) {
|
|
if (!tx.isExecuted) continue;
|
|
chainTransfers++;
|
|
|
|
const date = tx.executionDate;
|
|
if (date) {
|
|
const d = new Date(date);
|
|
if (!chainMinDate || d < chainMinDate) chainMinDate = d;
|
|
if (!chainMaxDate || d > chainMaxDate) chainMaxDate = d;
|
|
}
|
|
|
|
const outTransfers: { to: string; value: number; symbol: string }[] = [];
|
|
|
|
if (tx.value && tx.value !== "0" && tx.to) {
|
|
const val = parseFloat(tx.value) / 1e18;
|
|
const sym = CHAIN_NATIVE_SYMBOL[chainId] || "ETH";
|
|
outTransfers.push({ to: tx.to, value: val, symbol: sym });
|
|
}
|
|
|
|
if (tx.dataDecoded?.method === "transfer") {
|
|
const params = tx.dataDecoded.parameters || [];
|
|
const to = params.find((p: any) => p.name === "to")?.value;
|
|
const rawVal = params.find((p: any) => p.name === "value")?.value || "0";
|
|
if (to) outTransfers.push({ to, value: parseFloat(rawVal) / 1e18, symbol: "Token" });
|
|
}
|
|
|
|
if (tx.dataDecoded?.method === "multiSend") {
|
|
const txsParam = tx.dataDecoded.parameters?.find((p: any) => p.name === "transactions");
|
|
if (txsParam?.valueDecoded) {
|
|
for (const inner of txsParam.valueDecoded) {
|
|
if (inner.value && inner.value !== "0" && inner.to) {
|
|
const val = parseFloat(inner.value) / 1e18;
|
|
const sym = CHAIN_NATIVE_SYMBOL[chainId] || "ETH";
|
|
outTransfers.push({ to: inner.to, value: val, symbol: sym });
|
|
}
|
|
if (inner.dataDecoded?.method === "transfer") {
|
|
const to2 = inner.dataDecoded.parameters?.find((p: any) => p.name === "to")?.value;
|
|
const raw2 = inner.dataDecoded.parameters?.find((p: any) => p.name === "value")?.value || "0";
|
|
if (to2) outTransfers.push({ to: to2, value: parseFloat(raw2) / 1e18, symbol: "Token" });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const t of outTransfers) {
|
|
const usd = estimateUSD(t.value, t.symbol);
|
|
const usdVal = usd !== null ? usd : t.value;
|
|
chainOutflow += usdVal;
|
|
if (t.to) {
|
|
chainAddresses.add(t.to.toLowerCase());
|
|
allAddresses.add(t.to.toLowerCase());
|
|
}
|
|
|
|
const key = shortenAddress(t.to);
|
|
const existing = outflowAgg.get(key) || { to: shortenAddress(t.to), value: 0, token: t.symbol };
|
|
existing.value += usdVal;
|
|
outflowAgg.set(key, existing);
|
|
|
|
allTransfers.outgoing.push({
|
|
chainId, chainName,
|
|
date: date || "",
|
|
to: t.to,
|
|
toShort: shortenAddress(t.to),
|
|
token: t.symbol,
|
|
amount: t.value,
|
|
usd: usdVal,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const [, agg] of outflowAgg) {
|
|
flows.push({ from: "Safe Wallet", to: agg.to, value: Math.round(agg.value), token: agg.token, chain: chainName });
|
|
}
|
|
|
|
const fmt = (d: Date) => d.toLocaleDateString("en-US", { month: "short", year: "numeric" });
|
|
const period = (chainMinDate && chainMaxDate)
|
|
? `${fmt(chainMinDate)} - ${fmt(chainMaxDate)}`
|
|
: "No data";
|
|
|
|
chainStats[chainName] = {
|
|
transfers: chainTransfers,
|
|
inflow: formatUSDValue(chainInflow),
|
|
outflow: formatUSDValue(chainOutflow),
|
|
addresses: String(chainAddresses.size),
|
|
period,
|
|
};
|
|
|
|
flowData[chainName] = flows;
|
|
|
|
totalTransfers += chainTransfers;
|
|
totalInflow += chainInflow;
|
|
totalOutflow += chainOutflow;
|
|
if (chainMinDate && (!minDate || chainMinDate < minDate)) minDate = chainMinDate;
|
|
if (chainMaxDate && (!maxDate || chainMaxDate > maxDate)) maxDate = chainMaxDate;
|
|
}
|
|
|
|
// Aggregate "all" stats
|
|
const fmtAll = (d: Date) => d.toLocaleDateString("en-US", { month: "short", year: "numeric" });
|
|
chainStats["all"] = {
|
|
transfers: totalTransfers,
|
|
inflow: formatUSDValue(totalInflow),
|
|
outflow: formatUSDValue(totalOutflow),
|
|
addresses: String(allAddresses.size),
|
|
period: (minDate && maxDate) ? `${fmtAll(minDate)} - ${fmtAll(maxDate)}` : "No data",
|
|
};
|
|
|
|
// Aggregate "all" flows: top 15 by value
|
|
const allFlows: FlowEntry[] = [];
|
|
for (const flows of Object.values(flowData)) {
|
|
allFlows.push(...flows);
|
|
}
|
|
allFlows.sort((a, b) => b.value - a.value);
|
|
flowData["all"] = allFlows.slice(0, 15);
|
|
|
|
// Sort transfers by date
|
|
allTransfers.incoming.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
|
allTransfers.outgoing.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
|
|
|
return { chainStats, flowData, allTransfers };
|
|
}
|