fix(rwallet): align timeline inflows/outflows with balance changes
- Use curveStepAfter for the balance river so step transitions happen exactly at transaction dates (curveBasis didn't pass through data points, causing waterfall shapes to disconnect from the river edges) - Update hardcoded USD estimates to current CoinGecko prices (2026-03-25) - Add SAFE, COW, ENS, LDO, BAL to the price estimate table - Fix BigInt→Number precision for large token balances (>2^53 wei) in both price-feed enrichment and transfer value parsing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
ff09d49127
commit
8ea537525a
|
|
@ -106,14 +106,24 @@ export function txExplorerLink(txHash: string, chainId: string): string {
|
|||
return `${base}/tx/${txHash}`;
|
||||
}
|
||||
|
||||
/** Convert raw wei string to human-readable number, preserving precision for large values */
|
||||
function weiToHuman(raw: string, decimals: number): number {
|
||||
try {
|
||||
const wei = BigInt(raw || "0");
|
||||
const divisor = 10n ** BigInt(decimals);
|
||||
return Number(wei / divisor) + Number(wei % divisor) / Number(divisor);
|
||||
} catch {
|
||||
return parseFloat(raw || "0") / Math.pow(10, decimals);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
return weiToHuman(transfer.value || "0", decimals);
|
||||
}
|
||||
if (transfer.type === "ETHER_TRANSFER" || transfer.transferType === "ETHER_TRANSFER") {
|
||||
return parseFloat(transfer.value || "0") / 1e18;
|
||||
return weiToHuman(transfer.value || "0", 18);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -134,14 +144,15 @@ const STABLECOINS = new Set([
|
|||
"GHO", "PYUSD", "DOLA", "Yield-USD", "yUSD",
|
||||
]);
|
||||
|
||||
// Approximate USD prices for major non-stablecoin tokens (updated periodically)
|
||||
// Approximate USD prices for major non-stablecoin tokens (updated 2026-03-25 from CoinGecko)
|
||||
const NATIVE_APPROX_USD: Record<string, number> = {
|
||||
ETH: 2500, WETH: 2500, stETH: 2500, cbETH: 2500, rETH: 2800, wstETH: 2900,
|
||||
MATIC: 0.40, POL: 0.40, WMATIC: 0.40,
|
||||
ETH: 2165, WETH: 2165, stETH: 2165, cbETH: 2165, rETH: 2500, wstETH: 2665,
|
||||
MATIC: 0.20, POL: 0.20, WMATIC: 0.20,
|
||||
BNB: 600, WBNB: 600,
|
||||
AVAX: 35, WAVAX: 35,
|
||||
AVAX: 20, WAVAX: 20,
|
||||
xDAI: 1, WXDAI: 1,
|
||||
CELO: 0.50, GNO: 250,
|
||||
CELO: 0.30, GNO: 129,
|
||||
SAFE: 0.10, COW: 0.21, ENS: 6.11, LDO: 0.30, BAL: 0.15,
|
||||
};
|
||||
|
||||
export function estimateUSD(value: number, symbol: string): number | null {
|
||||
|
|
@ -217,7 +228,7 @@ export function transformToTimelineData(
|
|||
// Fallback: parse from dataDecoded or direct value
|
||||
if (txTransfers.length === 0) {
|
||||
if (tx.value && tx.value !== "0") {
|
||||
const val = parseFloat(tx.value) / 1e18;
|
||||
const val = weiToHuman(tx.value, 18);
|
||||
const sym = CHAIN_NATIVE_SYMBOL[chainId] || "ETH";
|
||||
txTransfers.push({ to: tx.to, value: val, symbol: sym, usd: estimateUSD(val, sym) });
|
||||
}
|
||||
|
|
@ -226,7 +237,7 @@ export function transformToTimelineData(
|
|||
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;
|
||||
const val = weiToHuman(rawVal, 18);
|
||||
txTransfers.push({ to, value: val, symbol: "Token", usd: null });
|
||||
}
|
||||
|
||||
|
|
@ -235,14 +246,14 @@ export function transformToTimelineData(
|
|||
if (txsParam?.valueDecoded) {
|
||||
for (const inner of txsParam.valueDecoded) {
|
||||
if (inner.value && inner.value !== "0") {
|
||||
const val = parseFloat(inner.value) / 1e18;
|
||||
const val = weiToHuman(inner.value, 18);
|
||||
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;
|
||||
const val2 = weiToHuman(raw2, 18);
|
||||
txTransfers.push({ to: to2, value: val2, symbol: "Token", usd: null });
|
||||
}
|
||||
}
|
||||
|
|
@ -334,7 +345,7 @@ export function transformToSankeyData(chainData: any, safeAddress: string, chain
|
|||
const txLabel = tx.transfers?.[0]?._toLabel;
|
||||
|
||||
if (tx.value && tx.value !== "0" && tx.to) {
|
||||
const val = parseFloat(tx.value) / 1e18;
|
||||
const val = weiToHuman(tx.value, 18);
|
||||
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, label: txLabel };
|
||||
|
|
@ -347,7 +358,7 @@ export function transformToSankeyData(chainData: any, safeAddress: string, chain
|
|||
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 val = weiToHuman(rawVal, 18);
|
||||
const key = `${to.toLowerCase()}:Token`;
|
||||
const existing = outflowAgg.get(key) || { to, value: 0, symbol: "Token" };
|
||||
existing.value += val;
|
||||
|
|
@ -360,7 +371,7 @@ export function transformToSankeyData(chainData: any, safeAddress: string, chain
|
|||
if (txsParam?.valueDecoded) {
|
||||
for (const inner of txsParam.valueDecoded) {
|
||||
if (inner.value && inner.value !== "0" && inner.to) {
|
||||
const val = parseFloat(inner.value) / 1e18;
|
||||
const val = weiToHuman(inner.value, 18);
|
||||
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 };
|
||||
|
|
@ -371,7 +382,7 @@ export function transformToSankeyData(chainData: any, safeAddress: string, chain
|
|||
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 val2 = weiToHuman(raw2, 18);
|
||||
const key = `${to2.toLowerCase()}:Token`;
|
||||
const existing = outflowAgg.get(key) || { to: to2, value: 0, symbol: "Token" };
|
||||
existing.value += val2;
|
||||
|
|
@ -513,7 +524,7 @@ export function transformToMultichainData(
|
|||
const txLabel = tx.transfers?.[0]?._toLabel;
|
||||
|
||||
if (tx.value && tx.value !== "0" && tx.to) {
|
||||
const val = parseFloat(tx.value) / 1e18;
|
||||
const val = weiToHuman(tx.value, 18);
|
||||
const sym = CHAIN_NATIVE_SYMBOL[chainId] || "ETH";
|
||||
outTransfers.push({ to: tx.to, value: val, symbol: sym, label: txLabel });
|
||||
}
|
||||
|
|
@ -522,7 +533,7 @@ export function transformToMultichainData(
|
|||
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 (to) outTransfers.push({ to, value: weiToHuman(rawVal, 18), symbol: "Token" });
|
||||
}
|
||||
|
||||
if (tx.dataDecoded?.method === "multiSend") {
|
||||
|
|
@ -530,14 +541,14 @@ export function transformToMultichainData(
|
|||
if (txsParam?.valueDecoded) {
|
||||
for (const inner of txsParam.valueDecoded) {
|
||||
if (inner.value && inner.value !== "0" && inner.to) {
|
||||
const val = parseFloat(inner.value) / 1e18;
|
||||
const val = weiToHuman(inner.value, 18);
|
||||
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" });
|
||||
if (to2) outTransfers.push({ to: to2, value: weiToHuman(raw2, 18), symbol: "Token" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -213,7 +213,11 @@ export async function enrichWithPrices(
|
|||
if (price === 0) return b;
|
||||
|
||||
const decimals = b.token?.decimals ?? 18;
|
||||
const balHuman = Number(balWei) / Math.pow(10, decimals);
|
||||
// Split BigInt to preserve precision for values > 2^53
|
||||
const divisor = 10n ** BigInt(decimals);
|
||||
const intPart = balWei / divisor;
|
||||
const fracPart = balWei % divisor;
|
||||
const balHuman = Number(intPart) + Number(fracPart) / Number(divisor);
|
||||
const fiatValue = balHuman * price;
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -231,7 +231,9 @@ export function renderTimeline(
|
|||
|
||||
function drawContent(scale: any) {
|
||||
contentGroup.selectAll("*").remove();
|
||||
const smoothCurve = d3.curveBasis;
|
||||
// curveStepAfter: balance is constant between txs, steps at each tx.
|
||||
// This ensures the river edges align exactly with the waterfall shapes.
|
||||
const smoothCurve = d3.curveStepAfter;
|
||||
|
||||
// River glow
|
||||
contentGroup.append("path").datum(balanceData).attr("fill", "rgba(0,212,255,0.08)")
|
||||
|
|
|
|||
Loading…
Reference in New Issue