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

122
test/test_tabs.js Normal file
View File

@ -0,0 +1,122 @@
import { WalletManager } from '../wallets.js';
import { WalletStreamManager } from '../stream.js';
// --- 1. Infrastructure Mocks ---
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]); }
};
// Accelerate timers so leader election (200ms) and backoff happen instantly
const originalSetTimeout = globalThis.setTimeout;
globalThis.setTimeout = (callback, delay) => originalSetTimeout(callback, 1);
// Standard WebSocket Mock
class MockWebSocket {
constructor(url) {
this.readyState = 1; // Immediately OPEN
originalSetTimeout(() => { if (this.onopen) this.onopen(); }, 1);
}
send(data) { globalLastSentPayload = JSON.parse(data); }
close() { if (this.onclose) this.onclose({ code: 1000 }); }
}
globalThis.WebSocket = MockWebSocket;
// --- 2. The Cross-Tab Broadcast Router Mock ---
const activeChannels = new Set();
class MockBroadcastChannel {
constructor(name) {
this.name = name;
activeChannels.add(this);
}
postMessage(data) {
// Distribute the message to all OTHER open channel instances simulation offline cross-tab delivery
for (const channel of activeChannels) {
if (channel !== this && channel.onmessage) {
channel.onmessage({ data });
}
}
}
close() { activeChannels.delete(this); }
}
globalThis.BroadcastChannel = MockBroadcastChannel;
// Global tracking variables for assertions
let globalLastSentPayload = null;
function assert(condition, message) {
if (!condition) throw new Error(`❌ FAIL: ${message}`);
console.log(`✅ PASS: ${message}`);
}
// --- 3. The Multi-Tab Simulation Scenario ---
async function runMultiTabTest() {
console.log("🚀 Starting Multi-Tab Synchronization & Leader Election Test Suite...\n");
localStorage.clear();
// Seed initial wallet configuration
const wm = new WalletManager();
wm.addWallet("0x04f728C520C438A000f7A5E9d904F0e725FFAEFE", "base", "Shared Wallet");
// --- STEP 1: Boot First Tab ---
console.log("▶️ Initializing Tab 1...");
const tab1 = new WalletStreamManager(wm);
tab1.connect();
await new Promise(r => originalSetTimeout(r, 15)); // Await leader election window
assert(tab1.isLeader === true, "Tab 1 detected no existing leaders and claimed leadership role.");
assert(tab1.ws !== null, "Tab 1 successfully initialized a physical WebSocket network connection.");
// --- STEP 2: Boot Second Tab ---
console.log("\n▶ Initializing Tab 2 (Simulating a new browser window/tab)...");
const tab2 = new WalletStreamManager(wm);
tab2.connect();
await new Promise(r => originalSetTimeout(r, 15));
assert(tab2.isLeader === false, "Tab 2 discovered active Leader and assigned itself as a Follower.");
assert(tab2.ws === undefined || tab2.ws === null, "Tab 2 kept its local WebSocket offline to save system resources.");
// --- STEP 3: Verify Event Cross-Broadcasting Flow ---
let tab2ReceivedData = false;
tab2.on('balance_update', (msg) => {
if (msg.balance === "9999") tab2ReceivedData = true;
});
// Simulate server pushing an update specifically to Tab 1's socket connection
tab1.ws.onmessage({
data: JSON.stringify({ type: "balance_update", balance: "9999" })
});
await new Promise(r => originalSetTimeout(r, 5));
assert(tab2ReceivedData === true, "Tab 1 received WebSocket data and pushed it locally across the BroadcastChannel to Tab 2.");
// --- STEP 4: Proxy Subscription Modification Check ---
console.log("\n▶ Simulating Follower (Tab 2) subscribing to a new asset dynamically...");
const newAsset = { address: "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", chain: "bitcoin" };
globalLastSentPayload = null;
tab2.subscribeToNewWallet(newAsset);
await new Promise(r => originalSetTimeout(r, 5));
assert(globalLastSentPayload !== null, "Tab 2's subscription was successfully intercepted and routed to the channel.");
assert(globalLastSentPayload.action === "subscribe_wallet", "Tab 1 successfully executed the proxy network subscription on behalf of Tab 2.");
// --- STEP 5: Graceful Failover Handover (Leader Death Scenario) ---
console.log("\n🕵 Simulating user abruptly closing Tab 1 (Leader)...");
// Simulate browser exit/unload routine for Tab 1
tab1.disconnect();
// Dispatch the exit event if your module binds directly to window event listeners
if (tab1._handleExit) tab1._handleExit();
await new Promise(r => originalSetTimeout(r, 15)); // Await emergency election frame
assert(tab2.isLeader === true, "Tab 2 caught the LEADER_DEATH signal and successfully promoted itself to Leader.");
assert(tab2.ws !== null, "Tab 2 gracefully opened a brand new physical WebSocket connection to pick up the stream.");
console.log("\n🎉 Massive Success! Cross-tab synchronization, proxy forwarding, and leader failovers function perfectly.");
}
runMultiTabTest().catch(console.error);