feat: multi-wallet architecture with localStorage state, EIP-712 verification, cross-tab WS leadership

- wallets.js: WalletManager state management with chain validation
- verifier.js: WalletVerifier EIP-712 signing via window.ethereum
- stream.js: WalletStreamManager with BroadcastChannel leader election, backoff + jitter
- AGENTS.md: updated with coding guidelines
This commit is contained in:
Dione
2026-06-06 12:16:41 +00:00
commit a61e0b0457
17 changed files with 4037 additions and 0 deletions

151
test/test_stream.js Normal file
View File

@ -0,0 +1,151 @@
import { WalletManager } from '../wallets.js';
import { WalletStreamManager } from '../stream.js';
// --- 1. Environment Mocks (Storage & Timers) ---
const mockStorage = {};
globalThis.localStorage = {
getItem: (key) => mockStorage[key] || null,
setItem: (key, value) => { mockStorage[key] = String(value); },
clear: () => { Object.keys(mockStorage).forEach(k => delete mockStorage[k]); }
};
// Speed up time so our backoff tests don't take 30 actual seconds to run
const originalSetTimeout = globalThis.setTimeout;
globalThis.setTimeout = (callback, delay) => {
return originalSetTimeout(callback, 1); // Force all retries to happen instantly
};
// Mock location for stream.js WS_URL evaluation
globalThis.location = {
protocol: 'http:',
host: 'localhost:3000'
};
// --- 2. The Mock WebSocket Server Mirror ---
const mockInstances = [];
let lastSentPayload = null;
let connectionCount = 0;
class MockWebSocket {
constructor(url) {
this.url = url;
this.readyState = 0; // CONNECTING
connectionCount++;
mockInstances.push(this);
originalSetTimeout(() => {
this.readyState = 1; // OPEN
if (this.onopen) this.onopen();
}, 2);
}
send(data) {
lastSentPayload = JSON.parse(data);
}
close() {
this.readyState = 3; // CLOSED
if (this.onclose) this.onclose({ code: 1000, reason: "Normal closure" });
}
// Helper method for our test runner to mimic the server pushing data to client
simulateServerPush(msgObject) {
if (this.onmessage) {
this.onmessage({ data: JSON.stringify(msgObject) });
}
}
// Helper method to simulate a sudden dirty network disconnect
simulateNetworkDrop() {
this.readyState = 3;
if (this.onclose) this.onclose({ code: 1006, reason: "Abnormal Drop" });
}
}
globalThis.WebSocket = MockWebSocket;
// --- 3. Test Runner Execution ---
function assert(condition, message) {
if (!condition) throw new Error(`❌ FAIL: ${message}`);
console.log(`✅ PASS: ${message}`);
}
async function runStreamTests() {
console.log("🚀 Starting WalletStreamManager Real-Time Test Suite...\n");
localStorage.clear();
// Seed two dummy wallets to simulate an existing local state
const wm = new WalletManager();
wm.addWallet("0x04f728C520C438A000f7A5E9d904F0e725FFAEFE", "base", "Base Acc");
wm.addWallet("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", "bitcoin", "Satoshi Acc");
const stream = new WalletStreamManager(wm);
stream.connect();
// Wait a few milliseconds for our mocked async connection handshakes to settle
await new Promise(r => originalSetTimeout(r, 10));
// --- TEST 1: Multiplexed On-Open Subscription ---
assert(connectionCount === 1, "WebSocket established exactly one connection");
assert(lastSentPayload !== null, "Server received subscription frame on startup");
assert(lastSentPayload.action === "subscribe", "Payload correctly set action to 'subscribe'");
assert(lastSentPayload.subscriptions.length === 2, "Multiplexed frame packed all stored wallets successfully");
// --- TEST 2: Real-time Message Callback Routing ---
let balanceUpdatedCalled = false;
stream.on('balance_update', (msg) => {
if (msg.address === "0x04f728C520C438A000f7A5E9d904F0e725FFAEFE") {
balanceUpdatedCalled = true;
}
});
// Access the underlying mock instance to simulate the server pushing data
const activeSocketInstance = stream._ws;
activeSocketInstance.simulateServerPush({
type: "balance_update",
address: "0x04f728C520C438A000f7A5E9d904F0e725FFAEFE",
chain: "base",
balance: "50000000000"
});
assert(balanceUpdatedCalled === true, "Incoming server events route seamlessly to their respective listeners");
// --- TEST 3: Callback Error Isolation Safeguard ---
let secondaryListenerExecuted = false;
stream.on('transfer', () => {
throw new Error("CRASHING INTRUDER: I am a broken UI component callback!");
});
stream.on('transfer', () => {
secondaryListenerExecuted = true;
});
// Push a transfer event. The first listener throws an exception, but shouldn't halt execution.
try {
activeSocketInstance.simulateServerPush({ type: "transfer", tx_hash: "0xabc" });
assert(secondaryListenerExecuted === true, "ERROR ISOLATION: Second event handler fired successfully despite preceding listener throwing a hard error");
} catch (e) {
assert(false, "The StreamManager leaked a callback exception and crashed the event loop");
}
// --- TEST 4: Dynamic Delta Streaming Tracker Updates ---
const dynamicWallet = { address: "H7vjR5vP6dfYuxUzoZ76yvBpx6Z3zM7p9qE1vRkWk3xp", chain: "solana" };
stream.subscribeToNewWallet(dynamicWallet);
assert(lastSentPayload.action === "subscribe_wallet", "Dynamic frame used accurate 'subscribe_wallet' verb");
assert(lastSentPayload.wallet?.address === dynamicWallet.address, "Dynamic delta subscription dispatched for runtime updates without connection cycling");
// --- TEST 5: Resilient Exponential Backoff Retry Loop ---
console.log("\n🕵 Triggering a sudden network failure drop scenario...");
const preDropCount = connectionCount;
stream._ws.simulateNetworkDrop(); // Crash the current open pipe
await new Promise(r => originalSetTimeout(r, 15)); // Wait for fast-forward timers to run the retry loops
assert(connectionCount > preDropCount, "RESILIENCE: Stream Manager successfully caught connection drop and automatically invoked reconnection logic");
// Cleanup
stream.disconnect();
console.log("\n🎉 All core real-time streaming transport client tests passed!");
}
runStreamTests().catch(console.error);