Merge branch 'dev'
This commit is contained in:
commit
041f7423de
|
|
@ -68,6 +68,7 @@ const CHAIN_COLORS: Record<string, string> = {
|
||||||
"80002": "#a855f7",
|
"80002": "#a855f7",
|
||||||
"43113": "#fb923c",
|
"43113": "#fb923c",
|
||||||
"97": "#fbbf24",
|
"97": "#fbbf24",
|
||||||
|
"local": "#22c55e",
|
||||||
};
|
};
|
||||||
|
|
||||||
const COLORS_TX = { cyan: "#00d4ff", green: "#4ade80", red: "#f87171" };
|
const COLORS_TX = { cyan: "#00d4ff", green: "#4ade80", red: "#f87171" };
|
||||||
|
|
@ -75,7 +76,7 @@ const COLORS_TX = { cyan: "#00d4ff", green: "#4ade80", red: "#f87171" };
|
||||||
const CHAIN_NAMES: Record<string, string> = {
|
const CHAIN_NAMES: Record<string, string> = {
|
||||||
"1": "Ethereum", "10": "Optimism", "100": "Gnosis", "137": "Polygon",
|
"1": "Ethereum", "10": "Optimism", "100": "Gnosis", "137": "Polygon",
|
||||||
"8453": "Base", "42161": "Arbitrum", "42220": "Celo", "43114": "Avalanche",
|
"8453": "Base", "42161": "Arbitrum", "42220": "Celo", "43114": "Avalanche",
|
||||||
"56": "BSC", "324": "zkSync",
|
"56": "BSC", "324": "zkSync", "local": "CRDT",
|
||||||
};
|
};
|
||||||
|
|
||||||
const EXAMPLE_WALLETS = [
|
const EXAMPLE_WALLETS = [
|
||||||
|
|
@ -706,7 +707,6 @@ class FolkWalletViewer extends HTMLElement {
|
||||||
|
|
||||||
private async loadTransfers() {
|
private async loadTransfers() {
|
||||||
if (this.isDemo || this.transfers || this.transfersLoading) return;
|
if (this.isDemo || this.transfers || this.transfersLoading) return;
|
||||||
if (this.walletType !== "safe") return; // Transfer API only for Safes
|
|
||||||
|
|
||||||
this.transfersLoading = true;
|
this.transfersLoading = true;
|
||||||
this.render();
|
this.render();
|
||||||
|
|
@ -715,14 +715,14 @@ class FolkWalletViewer extends HTMLElement {
|
||||||
const base = this.getApiBase();
|
const base = this.getApiBase();
|
||||||
const chainDataMap = new Map<string, any>();
|
const chainDataMap = new Map<string, any>();
|
||||||
|
|
||||||
// Fan out requests to all detected chains
|
// Fan out requests to all detected chains (Safe wallets only)
|
||||||
|
if (this.walletType === "safe") {
|
||||||
await Promise.allSettled(
|
await Promise.allSettled(
|
||||||
this.detectedChains.map(async (ch) => {
|
this.detectedChains.map(async (ch) => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${base}/api/safe/${ch.chainId}/${this.address}/transfers?limit=2000`);
|
const res = await fetch(`${base}/api/safe/${ch.chainId}/${this.address}/transfers?limit=2000`);
|
||||||
if (!res.ok) return;
|
if (!res.ok) return;
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
// Parse results into incoming/outgoing
|
|
||||||
const incoming: any[] = [];
|
const incoming: any[] = [];
|
||||||
const outgoing: any[] = [];
|
const outgoing: any[] = [];
|
||||||
const results = data.results || [];
|
const results = data.results || [];
|
||||||
|
|
@ -730,7 +730,6 @@ class FolkWalletViewer extends HTMLElement {
|
||||||
if (tx.txType === "MULTISIG_TRANSACTION") {
|
if (tx.txType === "MULTISIG_TRANSACTION") {
|
||||||
outgoing.push(tx);
|
outgoing.push(tx);
|
||||||
}
|
}
|
||||||
// Incoming transfers from transfers array inside txs
|
|
||||||
if (tx.transfers) {
|
if (tx.transfers) {
|
||||||
for (const t of tx.transfers) {
|
for (const t of tx.transfers) {
|
||||||
if (t.to?.toLowerCase() === this.address.toLowerCase()) {
|
if (t.to?.toLowerCase() === this.address.toLowerCase()) {
|
||||||
|
|
@ -743,6 +742,83 @@ class FolkWalletViewer extends HTMLElement {
|
||||||
} catch {}
|
} catch {}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch CRDT token transfers (available for all wallet types when authenticated)
|
||||||
|
if (this.isAuthenticated && this.userDID) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${base}/api/crdt-tokens/transfers?did=${encodeURIComponent(this.userDID)}`);
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
const entries = data.entries || [];
|
||||||
|
if (entries.length > 0) {
|
||||||
|
const incoming: any[] = [];
|
||||||
|
const outgoing: any[] = [];
|
||||||
|
const walletAddr = this.address.toLowerCase();
|
||||||
|
|
||||||
|
// Map DIDs to stable pseudo-addresses for the transform layer
|
||||||
|
// The current user's DID → this.address (so transforms recognize it)
|
||||||
|
// Other DIDs → deterministic short hex from the DID string
|
||||||
|
const didToAddr = (did: string): string => {
|
||||||
|
if (did === this.userDID) return walletAddr;
|
||||||
|
if (!did || did === 'system') return '0x0000000000000000000000000000000000000000';
|
||||||
|
// Deterministic pseudo-address from DID
|
||||||
|
let hash = 0;
|
||||||
|
for (let i = 0; i < did.length; i++) hash = ((hash << 5) - hash + did.charCodeAt(i)) | 0;
|
||||||
|
const hex = Math.abs(hash).toString(16).padStart(8, '0');
|
||||||
|
return `0x${hex}${'0'.repeat(32)}`.slice(0, 42);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build a label lookup for DID short names
|
||||||
|
const didLabels = new Map<string, string>();
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.toLabel) didLabels.set(entry.to, entry.toLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fromAddr = entry.type === 'mint' ? didToAddr(entry.issuedBy || 'system') : didToAddr(entry.from);
|
||||||
|
const toAddr = didToAddr(entry.to);
|
||||||
|
const fromLabel = entry.type === 'mint' ? 'Mint' : (didLabels.get(entry.from) || entry.from?.slice(-12) || 'system');
|
||||||
|
const toLabel = entry.toLabel || entry.to?.slice(-12) || '';
|
||||||
|
|
||||||
|
const shaped = {
|
||||||
|
type: "ERC20_TRANSFER",
|
||||||
|
transferType: "ERC20_TRANSFER",
|
||||||
|
from: fromAddr,
|
||||||
|
to: toAddr,
|
||||||
|
value: String(entry.amount),
|
||||||
|
tokenInfo: {
|
||||||
|
symbol: entry.tokenSymbol,
|
||||||
|
name: entry.tokenSymbol,
|
||||||
|
decimals: entry.tokenDecimals,
|
||||||
|
},
|
||||||
|
executionDate: new Date(entry.timestamp).toISOString(),
|
||||||
|
blockTimestamp: new Date(entry.timestamp).toISOString(),
|
||||||
|
_fromLabel: fromLabel,
|
||||||
|
_toLabel: toLabel,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (entry.to === this.userDID) {
|
||||||
|
incoming.push(shaped);
|
||||||
|
}
|
||||||
|
if (entry.from === this.userDID) {
|
||||||
|
outgoing.push({
|
||||||
|
txType: "MULTISIG_TRANSACTION",
|
||||||
|
isExecuted: true,
|
||||||
|
executionDate: shaped.executionDate,
|
||||||
|
value: "0",
|
||||||
|
to: toAddr,
|
||||||
|
transfers: [{ ...shaped, from: walletAddr }],
|
||||||
|
dataDecoded: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chainDataMap.set("local", { incoming, outgoing, chainId: "local" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
this.transfers = chainDataMap;
|
this.transfers = chainDataMap;
|
||||||
this.computeVizData();
|
this.computeVizData();
|
||||||
|
|
@ -795,6 +871,7 @@ class FolkWalletViewer extends HTMLElement {
|
||||||
for (const ch of this.detectedChains) {
|
for (const ch of this.detectedChains) {
|
||||||
chainColorMap[ch.name.toLowerCase()] = ch.color;
|
chainColorMap[ch.name.toLowerCase()] = ch.color;
|
||||||
}
|
}
|
||||||
|
chainColorMap["crdt"] = "#22c55e";
|
||||||
|
|
||||||
switch (this.activeView) {
|
switch (this.activeView) {
|
||||||
case "timeline":
|
case "timeline":
|
||||||
|
|
@ -952,7 +1029,7 @@ class FolkWalletViewer extends HTMLElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
private hasData(): boolean {
|
private hasData(): boolean {
|
||||||
return this.detectedChains.length > 0;
|
return this.detectedChains.length > 0 || this.crdtBalances.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── EIP-6963 Provider Discovery ──
|
// ── EIP-6963 Provider Discovery ──
|
||||||
|
|
@ -2035,8 +2112,8 @@ class FolkWalletViewer extends HTMLElement {
|
||||||
{ id: "flow", label: "Flow Map" },
|
{ id: "flow", label: "Flow Map" },
|
||||||
{ id: "sankey", label: "Sankey" },
|
{ id: "sankey", label: "Sankey" },
|
||||||
];
|
];
|
||||||
// Only show viz tabs for Safe wallets (or demo)
|
// Show viz tabs for Safe wallets, demo, or when CRDT tokens exist
|
||||||
const showViz = this.walletType === "safe" || this.isDemo;
|
const showViz = this.walletType === "safe" || this.isDemo || (this.isAuthenticated && this.crdtBalances.length > 0);
|
||||||
const visibleTabs = showViz ? tabs : [tabs[0], tabs[1]];
|
const visibleTabs = showViz ? tabs : [tabs[0], tabs[1]];
|
||||||
|
|
||||||
return `
|
return `
|
||||||
|
|
|
||||||
|
|
@ -176,7 +176,7 @@ export function transformToTimelineData(
|
||||||
hasUsdEstimate: usd !== null,
|
hasUsdEstimate: usd !== null,
|
||||||
chain: chainName,
|
chain: chainName,
|
||||||
chainId,
|
chainId,
|
||||||
from: shortenAddress(transfer.from),
|
from: transfer._fromLabel || shortenAddress(transfer.from),
|
||||||
fromFull: transfer.from,
|
fromFull: transfer.from,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -187,7 +187,7 @@ export function transformToTimelineData(
|
||||||
for (const tx of data.outgoing) {
|
for (const tx of data.outgoing) {
|
||||||
if (!tx.isExecuted) continue;
|
if (!tx.isExecuted) continue;
|
||||||
|
|
||||||
const txTransfers: { to: string; value: number; symbol: string; usd: number | null }[] = [];
|
const txTransfers: { to: string; value: number; symbol: string; usd: number | null; label?: string }[] = [];
|
||||||
|
|
||||||
// Check transfers array if available
|
// Check transfers array if available
|
||||||
if (tx.transfers && tx.transfers.length > 0) {
|
if (tx.transfers && tx.transfers.length > 0) {
|
||||||
|
|
@ -196,7 +196,7 @@ export function transformToTimelineData(
|
||||||
const value = getTransferValue(t);
|
const value = getTransferValue(t);
|
||||||
const symbol = getTokenSymbol(t);
|
const symbol = getTokenSymbol(t);
|
||||||
if (value > 0) {
|
if (value > 0) {
|
||||||
txTransfers.push({ to: t.to, value, symbol, usd: estimateUSD(value, symbol) });
|
txTransfers.push({ to: t.to, value, symbol, usd: estimateUSD(value, symbol), label: t._toLabel });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -249,7 +249,7 @@ export function transformToTimelineData(
|
||||||
hasUsdEstimate: t.usd !== null,
|
hasUsdEstimate: t.usd !== null,
|
||||||
chain: chainName,
|
chain: chainName,
|
||||||
chainId,
|
chainId,
|
||||||
to: shortenAddress(t.to),
|
to: t.label || shortenAddress(t.to),
|
||||||
toFull: t.to,
|
toFull: t.to,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -269,9 +269,9 @@ export function transformToSankeyData(chainData: any, safeAddress: string, chain
|
||||||
const nodeMap = new Map<string, number>();
|
const nodeMap = new Map<string, number>();
|
||||||
const nodes: SankeyNode[] = [];
|
const nodes: SankeyNode[] = [];
|
||||||
const links: SankeyLink[] = [];
|
const links: SankeyLink[] = [];
|
||||||
const walletLabel = "Safe Wallet";
|
const walletLabel = chainId === "local" ? "My Wallet" : "Safe Wallet";
|
||||||
|
|
||||||
function getNodeIndex(address: string, type: "wallet" | "source" | "target"): number {
|
function getNodeIndex(address: string, type: "wallet" | "source" | "target", label?: string): number {
|
||||||
const key = address.toLowerCase() === safeAddress.toLowerCase()
|
const key = address.toLowerCase() === safeAddress.toLowerCase()
|
||||||
? "wallet"
|
? "wallet"
|
||||||
: `${type}:${address.toLowerCase()}`;
|
: `${type}:${address.toLowerCase()}`;
|
||||||
|
|
@ -279,10 +279,10 @@ export function transformToSankeyData(chainData: any, safeAddress: string, chain
|
||||||
if (!nodeMap.has(key)) {
|
if (!nodeMap.has(key)) {
|
||||||
const idx = nodes.length;
|
const idx = nodes.length;
|
||||||
nodeMap.set(key, idx);
|
nodeMap.set(key, idx);
|
||||||
const label = address.toLowerCase() === safeAddress.toLowerCase()
|
const name = address.toLowerCase() === safeAddress.toLowerCase()
|
||||||
? walletLabel
|
? walletLabel
|
||||||
: shortenAddress(address);
|
: (label || shortenAddress(address));
|
||||||
nodes.push({ name: label, type, address });
|
nodes.push({ name, type, address });
|
||||||
}
|
}
|
||||||
return nodeMap.get(key)!;
|
return nodeMap.get(key)!;
|
||||||
}
|
}
|
||||||
|
|
@ -291,7 +291,7 @@ export function transformToSankeyData(chainData: any, safeAddress: string, chain
|
||||||
getNodeIndex(safeAddress, "wallet");
|
getNodeIndex(safeAddress, "wallet");
|
||||||
|
|
||||||
// Aggregate inflows by source address + token
|
// Aggregate inflows by source address + token
|
||||||
const inflowAgg = new Map<string, { from: string; value: number; symbol: string }>();
|
const inflowAgg = new Map<string, { from: string; value: number; symbol: string; label?: string }>();
|
||||||
if (chainData.incoming) {
|
if (chainData.incoming) {
|
||||||
for (const transfer of chainData.incoming) {
|
for (const transfer of chainData.incoming) {
|
||||||
const value = getTransferValue(transfer);
|
const value = getTransferValue(transfer);
|
||||||
|
|
@ -299,7 +299,7 @@ export function transformToSankeyData(chainData: any, safeAddress: string, chain
|
||||||
if (value <= 0 || !transfer.from) continue;
|
if (value <= 0 || !transfer.from) continue;
|
||||||
|
|
||||||
const key = `${transfer.from.toLowerCase()}:${symbol}`;
|
const key = `${transfer.from.toLowerCase()}:${symbol}`;
|
||||||
const existing = inflowAgg.get(key) || { from: transfer.from, value: 0, symbol };
|
const existing = inflowAgg.get(key) || { from: transfer.from, value: 0, symbol, label: transfer._fromLabel };
|
||||||
existing.value += value;
|
existing.value += value;
|
||||||
inflowAgg.set(key, existing);
|
inflowAgg.set(key, existing);
|
||||||
}
|
}
|
||||||
|
|
@ -307,22 +307,25 @@ export function transformToSankeyData(chainData: any, safeAddress: string, chain
|
||||||
|
|
||||||
// Add inflow links
|
// Add inflow links
|
||||||
for (const [, agg] of inflowAgg) {
|
for (const [, agg] of inflowAgg) {
|
||||||
const sourceIdx = getNodeIndex(agg.from, "source");
|
const sourceIdx = getNodeIndex(agg.from, "source", agg.label);
|
||||||
const walletIdx = nodeMap.get("wallet")!;
|
const walletIdx = nodeMap.get("wallet")!;
|
||||||
links.push({ source: sourceIdx, target: walletIdx, value: agg.value, token: agg.symbol });
|
links.push({ source: sourceIdx, target: walletIdx, value: agg.value, token: agg.symbol });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aggregate outflows by target address + token
|
// Aggregate outflows by target address + token
|
||||||
const outflowAgg = new Map<string, { to: string; value: number; symbol: string }>();
|
const outflowAgg = new Map<string, { to: string; value: number; symbol: string; label?: string }>();
|
||||||
if (chainData.outgoing) {
|
if (chainData.outgoing) {
|
||||||
for (const tx of chainData.outgoing) {
|
for (const tx of chainData.outgoing) {
|
||||||
if (!tx.isExecuted) continue;
|
if (!tx.isExecuted) continue;
|
||||||
|
|
||||||
|
// Extract label from transfers if available (CRDT entries)
|
||||||
|
const txLabel = tx.transfers?.[0]?._toLabel;
|
||||||
|
|
||||||
if (tx.value && tx.value !== "0" && tx.to) {
|
if (tx.value && tx.value !== "0" && tx.to) {
|
||||||
const val = parseFloat(tx.value) / 1e18;
|
const val = parseFloat(tx.value) / 1e18;
|
||||||
const sym = (chainId && CHAIN_NATIVE_SYMBOL[chainId]) || "ETH";
|
const sym = (chainId && CHAIN_NATIVE_SYMBOL[chainId]) || "ETH";
|
||||||
const key = `${tx.to.toLowerCase()}:${sym}`;
|
const key = `${tx.to.toLowerCase()}:${sym}`;
|
||||||
const existing = outflowAgg.get(key) || { to: tx.to, value: 0, symbol: sym };
|
const existing = outflowAgg.get(key) || { to: tx.to, value: 0, symbol: sym, label: txLabel };
|
||||||
existing.value += val;
|
existing.value += val;
|
||||||
outflowAgg.set(key, existing);
|
outflowAgg.set(key, existing);
|
||||||
}
|
}
|
||||||
|
|
@ -366,13 +369,29 @@ export function transformToSankeyData(chainData: any, safeAddress: string, chain
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback: process embedded transfers (CRDT entries use this path)
|
||||||
|
if (tx.transfers && tx.transfers.length > 0 && (!tx.value || tx.value === "0") && !tx.dataDecoded) {
|
||||||
|
for (const t of tx.transfers) {
|
||||||
|
if (t.from?.toLowerCase() === safeAddress.toLowerCase()) {
|
||||||
|
const value = getTransferValue(t);
|
||||||
|
const symbol = getTokenSymbol(t);
|
||||||
|
if (value > 0 && t.to) {
|
||||||
|
const key = `${t.to.toLowerCase()}:${symbol}`;
|
||||||
|
const existing = outflowAgg.get(key) || { to: t.to, value: 0, symbol, label: t._toLabel };
|
||||||
|
existing.value += value;
|
||||||
|
outflowAgg.set(key, existing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add outflow links
|
// Add outflow links
|
||||||
const walletIdx = nodeMap.get("wallet")!;
|
const walletIdx = nodeMap.get("wallet")!;
|
||||||
for (const [, agg] of outflowAgg) {
|
for (const [, agg] of outflowAgg) {
|
||||||
const targetIdx = getNodeIndex(agg.to, "target");
|
const targetIdx = getNodeIndex(agg.to, "target", agg.label);
|
||||||
links.push({ source: walletIdx, target: targetIdx, value: agg.value, token: agg.symbol });
|
links.push({ source: walletIdx, target: targetIdx, value: agg.value, token: agg.symbol });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -442,8 +461,9 @@ export function transformToMultichainData(
|
||||||
}
|
}
|
||||||
|
|
||||||
const from = transfer.from || "Unknown";
|
const from = transfer.from || "Unknown";
|
||||||
const key = shortenAddress(from);
|
const fromName = transfer._fromLabel || shortenAddress(from);
|
||||||
const existing = inflowAgg.get(key) || { from: shortenAddress(from), value: 0, token: symbol };
|
const key = fromName;
|
||||||
|
const existing = inflowAgg.get(key) || { from: fromName, value: 0, token: symbol };
|
||||||
existing.value += usdVal;
|
existing.value += usdVal;
|
||||||
inflowAgg.set(key, existing);
|
inflowAgg.set(key, existing);
|
||||||
|
|
||||||
|
|
@ -451,7 +471,7 @@ export function transformToMultichainData(
|
||||||
chainId, chainName,
|
chainId, chainName,
|
||||||
date: date || "",
|
date: date || "",
|
||||||
from: transfer.from,
|
from: transfer.from,
|
||||||
fromShort: shortenAddress(transfer.from),
|
fromShort: transfer._fromLabel || shortenAddress(transfer.from),
|
||||||
token: symbol,
|
token: symbol,
|
||||||
amount: value,
|
amount: value,
|
||||||
usd: usdVal,
|
usd: usdVal,
|
||||||
|
|
@ -477,12 +497,13 @@ export function transformToMultichainData(
|
||||||
if (!chainMaxDate || d > chainMaxDate) chainMaxDate = d;
|
if (!chainMaxDate || d > chainMaxDate) chainMaxDate = d;
|
||||||
}
|
}
|
||||||
|
|
||||||
const outTransfers: { to: string; value: number; symbol: string }[] = [];
|
const outTransfers: { to: string; value: number; symbol: string; label?: string }[] = [];
|
||||||
|
const txLabel = tx.transfers?.[0]?._toLabel;
|
||||||
|
|
||||||
if (tx.value && tx.value !== "0" && tx.to) {
|
if (tx.value && tx.value !== "0" && tx.to) {
|
||||||
const val = parseFloat(tx.value) / 1e18;
|
const val = parseFloat(tx.value) / 1e18;
|
||||||
const sym = CHAIN_NATIVE_SYMBOL[chainId] || "ETH";
|
const sym = CHAIN_NATIVE_SYMBOL[chainId] || "ETH";
|
||||||
outTransfers.push({ to: tx.to, value: val, symbol: sym });
|
outTransfers.push({ to: tx.to, value: val, symbol: sym, label: txLabel });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tx.dataDecoded?.method === "transfer") {
|
if (tx.dataDecoded?.method === "transfer") {
|
||||||
|
|
@ -510,6 +531,19 @@ export function transformToMultichainData(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback: embedded transfers (CRDT entries)
|
||||||
|
if (outTransfers.length === 0 && 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) {
|
||||||
|
outTransfers.push({ to: t.to, value, symbol, label: t._toLabel });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const t of outTransfers) {
|
for (const t of outTransfers) {
|
||||||
const usd = estimateUSD(t.value, t.symbol);
|
const usd = estimateUSD(t.value, t.symbol);
|
||||||
const usdVal = usd !== null ? usd : t.value;
|
const usdVal = usd !== null ? usd : t.value;
|
||||||
|
|
@ -519,8 +553,9 @@ export function transformToMultichainData(
|
||||||
allAddresses.add(t.to.toLowerCase());
|
allAddresses.add(t.to.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = shortenAddress(t.to);
|
const toName = t.label || shortenAddress(t.to);
|
||||||
const existing = outflowAgg.get(key) || { to: shortenAddress(t.to), value: 0, token: t.symbol };
|
const key = toName;
|
||||||
|
const existing = outflowAgg.get(key) || { to: toName, value: 0, token: t.symbol };
|
||||||
existing.value += usdVal;
|
existing.value += usdVal;
|
||||||
outflowAgg.set(key, existing);
|
outflowAgg.set(key, existing);
|
||||||
|
|
||||||
|
|
@ -528,7 +563,7 @@ export function transformToMultichainData(
|
||||||
chainId, chainName,
|
chainId, chainName,
|
||||||
date: date || "",
|
date: date || "",
|
||||||
to: t.to,
|
to: t.to,
|
||||||
toShort: shortenAddress(t.to),
|
toShort: t.label || shortenAddress(t.to),
|
||||||
token: t.symbol,
|
token: t.symbol,
|
||||||
amount: t.value,
|
amount: t.value,
|
||||||
usd: usdVal,
|
usd: usdVal,
|
||||||
|
|
|
||||||
|
|
@ -798,7 +798,7 @@ routes.post("/api/safe/:chainId/:address/add-owner-proposal", async (c) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── CRDT Token API ──
|
// ── CRDT Token API ──
|
||||||
import { getTokenDoc, listTokenDocs, getAllBalances, getBalance, mintTokens } from "../../server/token-service";
|
import { getTokenDoc, listTokenDocs, getAllBalances, getBalance, mintTokens, getAllTransfers } from "../../server/token-service";
|
||||||
import { tokenDocId } from "../../server/token-schemas";
|
import { tokenDocId } from "../../server/token-schemas";
|
||||||
import type { TokenLedgerDoc } from "../../server/token-schemas";
|
import type { TokenLedgerDoc } from "../../server/token-schemas";
|
||||||
|
|
||||||
|
|
@ -888,6 +888,13 @@ routes.post("/api/crdt-tokens/:tokenId/mint", async (c) => {
|
||||||
return c.json({ ok: true, minted: amount, to: toDid });
|
return c.json({ ok: true, minted: amount, to: toDid });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Get all CRDT token transfers (optionally filtered by DID)
|
||||||
|
routes.get("/api/crdt-tokens/transfers", (c) => {
|
||||||
|
const did = c.req.query("did");
|
||||||
|
const entries = getAllTransfers(did || undefined);
|
||||||
|
return c.json({ entries });
|
||||||
|
});
|
||||||
|
|
||||||
// ── Yield API routes ──
|
// ── Yield API routes ──
|
||||||
import { getYieldRates } from "./lib/yield-rates";
|
import { getYieldRates } from "./lib/yield-rates";
|
||||||
import { getYieldPositions } from "./lib/yield-positions";
|
import { getYieldPositions } from "./lib/yield-positions";
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,63 @@ export function getAllBalances(doc: TokenLedgerDoc): Record<string, { did: strin
|
||||||
return holders;
|
return holders;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Transfer tokens between DIDs. */
|
||||||
|
export function transferTokens(
|
||||||
|
tokenId: string,
|
||||||
|
fromDid: string,
|
||||||
|
fromLabel: string,
|
||||||
|
toDid: string,
|
||||||
|
toLabel: string,
|
||||||
|
amount: number,
|
||||||
|
memo: string,
|
||||||
|
issuedBy: string,
|
||||||
|
timestamp?: number,
|
||||||
|
): boolean {
|
||||||
|
const docId = tokenDocId(tokenId);
|
||||||
|
ensureTokenDoc(tokenId);
|
||||||
|
|
||||||
|
const entryId = `xfer-${timestamp || Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||||
|
const result = _syncServer!.changeDoc<TokenLedgerDoc>(docId, `transfer ${amount} from ${fromLabel} to ${toLabel}`, (d) => {
|
||||||
|
d.entries[entryId] = {
|
||||||
|
id: entryId,
|
||||||
|
to: toDid,
|
||||||
|
toLabel,
|
||||||
|
amount,
|
||||||
|
memo,
|
||||||
|
type: 'transfer',
|
||||||
|
from: fromDid,
|
||||||
|
timestamp: timestamp || Date.now(),
|
||||||
|
issuedBy,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return result !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get all ledger entries across all tokens, optionally filtered by DID. */
|
||||||
|
export function getAllTransfers(filterDid?: string): Array<LedgerEntry & { tokenId: string; tokenSymbol: string; tokenDecimals: number; tokenIcon: string }> {
|
||||||
|
const docIds = listTokenDocs();
|
||||||
|
const entries: Array<LedgerEntry & { tokenId: string; tokenSymbol: string; tokenDecimals: number; tokenIcon: string }> = [];
|
||||||
|
|
||||||
|
for (const docId of docIds) {
|
||||||
|
const tokenId = docId.replace('global:tokens:ledgers:', '');
|
||||||
|
const doc = getTokenDoc(tokenId);
|
||||||
|
if (!doc || !doc.token.name) continue;
|
||||||
|
|
||||||
|
for (const entry of Object.values(doc.entries)) {
|
||||||
|
if (filterDid && entry.to !== filterDid && entry.from !== filterDid) continue;
|
||||||
|
entries.push({
|
||||||
|
...entry,
|
||||||
|
tokenId: doc.token.id,
|
||||||
|
tokenSymbol: doc.token.symbol,
|
||||||
|
tokenDecimals: doc.token.decimals,
|
||||||
|
tokenIcon: doc.token.icon,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries.sort((a, b) => a.timestamp - b.timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
/** Mint tokens to a DID. */
|
/** Mint tokens to a DID. */
|
||||||
export function mintTokens(
|
export function mintTokens(
|
||||||
tokenId: string,
|
tokenId: string,
|
||||||
|
|
@ -80,11 +137,12 @@ export function mintTokens(
|
||||||
amount: number,
|
amount: number,
|
||||||
memo: string,
|
memo: string,
|
||||||
issuedBy: string,
|
issuedBy: string,
|
||||||
|
timestamp?: number,
|
||||||
): boolean {
|
): boolean {
|
||||||
const docId = tokenDocId(tokenId);
|
const docId = tokenDocId(tokenId);
|
||||||
ensureTokenDoc(tokenId);
|
ensureTokenDoc(tokenId);
|
||||||
|
|
||||||
const entryId = `mint-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
const entryId = `mint-${timestamp || Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||||
const result = _syncServer!.changeDoc<TokenLedgerDoc>(docId, `mint ${amount} to ${toLabel}`, (d) => {
|
const result = _syncServer!.changeDoc<TokenLedgerDoc>(docId, `mint ${amount} to ${toLabel}`, (d) => {
|
||||||
d.entries[entryId] = {
|
d.entries[entryId] = {
|
||||||
id: entryId,
|
id: entryId,
|
||||||
|
|
@ -94,7 +152,7 @@ export function mintTokens(
|
||||||
memo,
|
memo,
|
||||||
type: 'mint',
|
type: 'mint',
|
||||||
from: '',
|
from: '',
|
||||||
timestamp: Date.now(),
|
timestamp: timestamp || Date.now(),
|
||||||
issuedBy,
|
issuedBy,
|
||||||
};
|
};
|
||||||
d.token.totalSupply = (d.token.totalSupply || 0) + amount;
|
d.token.totalSupply = (d.token.totalSupply || 0) + amount;
|
||||||
|
|
@ -112,45 +170,109 @@ export function getTokenDoc(tokenId: string): TokenLedgerDoc | undefined {
|
||||||
return _syncServer!.getDoc<TokenLedgerDoc>(tokenDocId(tokenId));
|
return _syncServer!.getDoc<TokenLedgerDoc>(tokenDocId(tokenId));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Seed the cUSDC token with 5 cUSDC minted to jeff. */
|
/** Seed the full DAO token ecosystem — BFT governance, cUSDC stablecoin, multiple treasuries. */
|
||||||
export async function seedCUSDC() {
|
export async function seedCUSDC() {
|
||||||
const tokenId = 'cusdc';
|
// Check if already seeded by looking at BFT token (last one created)
|
||||||
const doc = ensureTokenDoc(tokenId);
|
const bftDoc = getTokenDoc('bft');
|
||||||
|
if (bftDoc && bftDoc.token.name && Object.keys(bftDoc.entries).length > 0) {
|
||||||
// Skip if already fully seeded (has entries)
|
console.log('[TokenService] DAO ecosystem already seeded, skipping');
|
||||||
if (doc.token.name && Object.keys(doc.entries).length > 0) {
|
|
||||||
console.log('[TokenService] cUSDC already seeded, skipping');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up token definition (idempotent — only writes if not yet defined)
|
// ── DAO participants ──
|
||||||
|
const jeffDid = process.env.SEED_JEFF_DID || 'did:key:jAV6y4tg8UbKJEkN0npvX8CTdJkSCGpU';
|
||||||
|
const treasuryDid = 'did:key:treasury-main-rspace-dao-2026';
|
||||||
|
const grantsDid = 'did:key:treasury-grants-committee-2026';
|
||||||
|
const devFundDid = 'did:key:treasury-dev-fund-rspace-2026';
|
||||||
|
const aliceDid = 'did:key:alice-contributor-rspace-2026';
|
||||||
|
const bobDid = 'did:key:bob-auditor-rspace-2026';
|
||||||
|
const carolDid = 'did:key:carol-designer-rspace-2026';
|
||||||
|
|
||||||
|
// Timestamps spread over the last 60 days
|
||||||
|
const now = Date.now();
|
||||||
|
const day = 86400000;
|
||||||
|
const t = (daysAgo: number) => now - daysAgo * day;
|
||||||
|
|
||||||
|
// ── 1. cUSDC — CRDT stablecoin ──
|
||||||
|
ensureTokenDef('cusdc', {
|
||||||
|
name: 'CRDT USDC', symbol: 'cUSDC', decimals: 6,
|
||||||
|
description: 'CRDT-native stablecoin pegged to USDC, stored as an Automerge document',
|
||||||
|
icon: '💵', color: '#2775ca',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mint to main treasury
|
||||||
|
mintTokens('cusdc', treasuryDid, 'DAO Treasury', 500_000_000_000, 'Genesis allocation — 500K cUSDC', 'system', t(58));
|
||||||
|
// Treasury funds grants committee
|
||||||
|
transferTokens('cusdc', treasuryDid, 'DAO Treasury', grantsDid, 'Grants Committee', 100_000_000_000, 'Q1 2026 grants budget', 'system', t(55));
|
||||||
|
// Treasury funds dev fund
|
||||||
|
transferTokens('cusdc', treasuryDid, 'DAO Treasury', devFundDid, 'Dev Fund', 75_000_000_000, 'Q1 2026 development budget', 'system', t(54));
|
||||||
|
// Grants committee pays contributors
|
||||||
|
transferTokens('cusdc', grantsDid, 'Grants Committee', aliceDid, 'Alice', 15_000_000_000, 'Grant: local-first sync engine', 'system', t(48));
|
||||||
|
transferTokens('cusdc', grantsDid, 'Grants Committee', bobDid, 'Bob', 8_000_000_000, 'Grant: security audit Q1', 'system', t(45));
|
||||||
|
transferTokens('cusdc', grantsDid, 'Grants Committee', carolDid, 'Carol', 12_000_000_000, 'Grant: UX redesign sprint', 'system', t(42));
|
||||||
|
// Dev fund pays jeff
|
||||||
|
transferTokens('cusdc', devFundDid, 'Dev Fund', jeffDid, 'jeff', 25_000_000_000, 'Core dev compensation — Jan', 'system', t(40));
|
||||||
|
transferTokens('cusdc', devFundDid, 'Dev Fund', aliceDid, 'Alice', 10_000_000_000, 'CRDT module development', 'system', t(35));
|
||||||
|
// Second round
|
||||||
|
transferTokens('cusdc', grantsDid, 'Grants Committee', bobDid, 'Bob', 5_000_000_000, 'Follow-up audit: identity module', 'system', t(30));
|
||||||
|
transferTokens('cusdc', devFundDid, 'Dev Fund', jeffDid, 'jeff', 25_000_000_000, 'Core dev compensation — Feb', 'system', t(25));
|
||||||
|
transferTokens('cusdc', treasuryDid, 'DAO Treasury', grantsDid, 'Grants Committee', 50_000_000_000, 'Grants top-up — mid-quarter', 'system', t(22));
|
||||||
|
transferTokens('cusdc', grantsDid, 'Grants Committee', carolDid, 'Carol', 8_000_000_000, 'Design system components', 'system', t(18));
|
||||||
|
// Recent activity
|
||||||
|
transferTokens('cusdc', devFundDid, 'Dev Fund', jeffDid, 'jeff', 25_000_000_000, 'Core dev compensation — Mar', 'system', t(10));
|
||||||
|
transferTokens('cusdc', treasuryDid, 'DAO Treasury', devFundDid, 'Dev Fund', 50_000_000_000, 'Q2 2026 dev budget (early)', 'system', t(5));
|
||||||
|
transferTokens('cusdc', grantsDid, 'Grants Committee', aliceDid, 'Alice', 20_000_000_000, 'Grant: rMaps privacy module', 'system', t(3));
|
||||||
|
|
||||||
|
// ── 2. BFT — Governance token ──
|
||||||
|
ensureTokenDef('bft', {
|
||||||
|
name: 'BioFi Token', symbol: 'BFT', decimals: 18,
|
||||||
|
description: 'Governance token for the rSpace DAO — used for voting, delegation, and reputation',
|
||||||
|
icon: '🌱', color: '#22c55e',
|
||||||
|
});
|
||||||
|
|
||||||
|
const e18 = 1_000_000_000_000_000_000; // 10^18 — but we store as number so use smaller scale
|
||||||
|
const bft = (n: number) => n * 1_000_000; // Use 6 effective decimals for demo (display divides by 10^18)
|
||||||
|
|
||||||
|
// Genesis mint to treasury
|
||||||
|
mintTokens('bft', treasuryDid, 'DAO Treasury', bft(10_000_000), 'Genesis: 10M BFT', 'system', t(58));
|
||||||
|
// Distribute to sub-treasuries
|
||||||
|
transferTokens('bft', treasuryDid, 'DAO Treasury', grantsDid, 'Grants Committee', bft(2_000_000), 'Grants committee allocation', 'system', t(56));
|
||||||
|
transferTokens('bft', treasuryDid, 'DAO Treasury', devFundDid, 'Dev Fund', bft(1_500_000), 'Dev fund allocation', 'system', t(56));
|
||||||
|
// Vest to contributors
|
||||||
|
transferTokens('bft', treasuryDid, 'DAO Treasury', jeffDid, 'jeff', bft(500_000), 'Founder vesting — month 1', 'system', t(50));
|
||||||
|
transferTokens('bft', grantsDid, 'Grants Committee', aliceDid, 'Alice', bft(100_000), 'Contributor reward: sync engine', 'system', t(46));
|
||||||
|
transferTokens('bft', grantsDid, 'Grants Committee', bobDid, 'Bob', bft(50_000), 'Contributor reward: audit', 'system', t(43));
|
||||||
|
transferTokens('bft', grantsDid, 'Grants Committee', carolDid, 'Carol', bft(75_000), 'Contributor reward: UX', 'system', t(40));
|
||||||
|
transferTokens('bft', devFundDid, 'Dev Fund', jeffDid, 'jeff', bft(200_000), 'Dev milestone: rwallet launch', 'system', t(35));
|
||||||
|
// Second vesting round
|
||||||
|
transferTokens('bft', treasuryDid, 'DAO Treasury', jeffDid, 'jeff', bft(500_000), 'Founder vesting — month 2', 'system', t(20));
|
||||||
|
transferTokens('bft', grantsDid, 'Grants Committee', aliceDid, 'Alice', bft(150_000), 'Grant reward: rMaps', 'system', t(15));
|
||||||
|
transferTokens('bft', devFundDid, 'Dev Fund', bobDid, 'Bob', bft(80_000), 'Security bounty payout', 'system', t(12));
|
||||||
|
transferTokens('bft', devFundDid, 'Dev Fund', carolDid, 'Carol', bft(60_000), 'Design system completion', 'system', t(8));
|
||||||
|
// Recent
|
||||||
|
transferTokens('bft', treasuryDid, 'DAO Treasury', jeffDid, 'jeff', bft(500_000), 'Founder vesting — month 3', 'system', t(2));
|
||||||
|
|
||||||
|
console.log('[TokenService] DAO ecosystem seeded: cUSDC + BFT tokens with treasury flows');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Helper to define a token if not yet defined. */
|
||||||
|
function ensureTokenDef(tokenId: string, def: { name: string; symbol: string; decimals: number; description: string; icon: string; color: string }) {
|
||||||
|
const doc = ensureTokenDoc(tokenId);
|
||||||
|
if (doc.token.name) return; // Already defined
|
||||||
const docId = tokenDocId(tokenId);
|
const docId = tokenDocId(tokenId);
|
||||||
if (!doc.token.name) {
|
_syncServer!.changeDoc<TokenLedgerDoc>(docId, `define ${def.symbol} token`, (d) => {
|
||||||
_syncServer!.changeDoc<TokenLedgerDoc>(docId, 'define cUSDC token', (d) => {
|
d.token.id = tokenId;
|
||||||
d.token.id = 'cusdc';
|
d.token.name = def.name;
|
||||||
d.token.name = 'CRDT USDC';
|
d.token.symbol = def.symbol;
|
||||||
d.token.symbol = 'cUSDC';
|
d.token.decimals = def.decimals;
|
||||||
d.token.decimals = 6;
|
d.token.description = def.description;
|
||||||
d.token.description = 'CRDT-native stablecoin pegged to USDC, stored as an Automerge document';
|
d.token.icon = def.icon;
|
||||||
d.token.icon = '💵';
|
d.token.color = def.color;
|
||||||
d.token.color = '#2775ca';
|
|
||||||
d.token.createdAt = Date.now();
|
d.token.createdAt = Date.now();
|
||||||
d.token.createdBy = 'system';
|
d.token.createdBy = 'system';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve jeff's DID — use known DID with env override support
|
|
||||||
const jeffDid = process.env.SEED_JEFF_DID || 'did:key:jAV6y4tg8UbKJEkN0npvX8CTdJkSCGpU';
|
|
||||||
|
|
||||||
// Mint 5 cUSDC (5 × 10^6 base units)
|
|
||||||
const success = mintTokens(tokenId, jeffDid, 'jeff', 5_000_000, 'Initial seed mint', 'system');
|
|
||||||
if (success) {
|
|
||||||
console.log(`[TokenService] cUSDC seeded: 5 cUSDC minted to jeff (${jeffDid})`);
|
|
||||||
} else {
|
|
||||||
console.error('[TokenService] Failed to mint cUSDC to jeff');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Mint $5 fUSDC welcome balance for a new user. */
|
/** Mint $5 fUSDC welcome balance for a new user. */
|
||||||
export function mintWelcomeBalance(did: string, username: string): boolean {
|
export function mintWelcomeBalance(did: string, username: string): boolean {
|
||||||
if (!_syncServer) {
|
if (!_syncServer) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue