90 lines
5.7 KiB
JavaScript
90 lines
5.7 KiB
JavaScript
// node --test site/wallet-provider.test.mjs
|
|
// The Igneum Wallet's page provider (site/wallet-provider.js): an EIP-1193 provider a page gets when the wallet runs on
|
|
// the same machine (its page bridge on 127.0.0.1:26811). Known-failed first on 8 October 2026: the file did not exist,
|
|
// so igneum.network/swap could connect only an injected wallet. The rules: a page that already has window.ethereum
|
|
// keeps it (the Igneum provider is announced through EIP-6963 and sits at window.igneum); without one, the Igneum
|
|
// provider is window.ethereum; a request while the wallet is not open rejects with 4900; a pending answer is polled
|
|
// until the wallet decides; errors come back as {code, message}; accountsChanged and chainChanged fire from polling.
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFileSync } from 'node:fs';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { dirname, join } from 'node:path';
|
|
|
|
const src = readFileSync(join(dirname(fileURLToPath(import.meta.url)), 'wallet-provider.js'), 'utf8');
|
|
|
|
function boot(opts = {}) {
|
|
const calls = [];
|
|
const win = { location: { origin: 'https://igneum.network' }, dispatched: [], listeners: {}, setTimeout: (f) => { f(); return 1; }, clearTimeout() {}, setInterval: () => 1, clearInterval() {} };
|
|
win.addEventListener = (n, f) => { (win.listeners[n] = win.listeners[n] || []).push(f); };
|
|
win.dispatchEvent = (e) => { win.dispatched.push(e); (win.listeners[e.type] || []).forEach((f) => f(e)); return true; };
|
|
if (opts.existing) win.ethereum = opts.existing;
|
|
const fetch = async (url, init) => {
|
|
calls.push({ url, init });
|
|
if (opts.down) throw new Error('connection refused');
|
|
const body = init && init.body ? JSON.parse(init.body) : null;
|
|
if (url.endsWith('/bridge/hello')) return { ok: true, json: async () => ({ ok: true, wallet: 'igneum', version: '0.1.6', chain_id: 4463, chain_id_hex: '0x116f', bridge: 1 }) };
|
|
const r = opts.reply(body, calls.length);
|
|
return { ok: true, json: async () => r };
|
|
};
|
|
const Event = class { constructor(type, init) { this.type = type; this.detail = init && init.detail; } };
|
|
new Function('window', 'fetch', 'CustomEvent', 'Event', 'document', src)(win, fetch, Event, Event, { readyState: 'complete', addEventListener() {} });
|
|
return { win, calls };
|
|
}
|
|
|
|
test('without an injected wallet the Igneum provider is window.ethereum and is announced through EIP-6963', () => {
|
|
const { win } = boot({ reply: () => ({ result: [] }) });
|
|
assert.ok(win.ethereum && win.ethereum.isIgneum === true);
|
|
assert.equal(win.igneum, win.ethereum);
|
|
const ann = win.dispatched.filter((e) => e.type === 'eip6963:announceProvider');
|
|
assert.equal(ann.length, 1);
|
|
assert.equal(ann[0].detail.info.rdns, 'network.igneum.wallet');
|
|
assert.equal(ann[0].detail.provider, win.ethereum);
|
|
});
|
|
test('an injected wallet keeps window.ethereum; the Igneum provider sits at window.igneum and is still announced', () => {
|
|
const mm = { isMetaMask: true };
|
|
const { win } = boot({ existing: mm, reply: () => ({ result: [] }) });
|
|
assert.equal(win.ethereum, mm);
|
|
assert.ok(win.igneum && win.igneum.isIgneum);
|
|
assert.equal(win.dispatched.filter((e) => e.type === 'eip6963:announceProvider').length, 1);
|
|
});
|
|
test('a request while the wallet is not open rejects with 4900 and the words a person can act on', async () => {
|
|
const { win } = boot({ down: true });
|
|
await assert.rejects(win.igneum.request({ method: 'eth_requestAccounts' }), (e) => e.code === 4900 && /Igneum Wallet is not open/.test(e.message));
|
|
});
|
|
test('a direct result comes back; an error comes back as {code, message}; a pending answer is polled until decided', async () => {
|
|
let polls = 0;
|
|
const { win, calls } = boot({ reply: (body) => {
|
|
if (body.method === 'eth_chainId') return { result: '0x116f' };
|
|
if (body.method === 'eth_requestAccounts') return { pending: '1-ab' };
|
|
if (body.method === 'igneum_poll') { polls++; return polls < 3 ? { pending: '1-ab' } : { result: ['0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826'] }; }
|
|
if (body.method === 'wallet_switchEthereumChain') return { error: { code: 4902, message: 'one chain' } };
|
|
return { result: null };
|
|
} });
|
|
assert.equal(await win.igneum.request({ method: 'eth_chainId' }), '0x116f');
|
|
const acc = await win.igneum.request({ method: 'eth_requestAccounts' });
|
|
assert.deepEqual(acc, ['0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826']);
|
|
assert.equal(polls, 3);
|
|
assert.equal(win.igneum.selectedAddress, '0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826');
|
|
assert.equal(win.igneum.chainId, '0x116f');
|
|
await assert.rejects(win.igneum.request({ method: 'wallet_switchEthereumChain', params: [{ chainId: '0x1' }] }), (e) => e.code === 4902 && e.message === 'one chain');
|
|
const rpc = calls.filter((c) => c.url.endsWith('/bridge/rpc'));
|
|
assert.ok(rpc.every((c) => c.init.headers['X-Igneum-Bridge'] === '1' && c.init.method === 'POST'), 'every call carries the bridge header');
|
|
assert.ok(rpc.every((c) => c.url.startsWith('http://127.0.0.1:26811/')), 'the fixed loopback port');
|
|
});
|
|
test('accountsChanged and chainChanged fire from the poll when they differ; on() and removeListener() work', async () => {
|
|
let accounts = [];
|
|
const { win } = boot({ reply: (body) => body.method === 'eth_accounts' ? { result: accounts } : body.method === 'eth_chainId' ? { result: '0x116f' } : { result: null } });
|
|
const seen = [];
|
|
const h = (a) => seen.push(a);
|
|
win.igneum.on('accountsChanged', h);
|
|
await win.igneum._tick();
|
|
assert.deepEqual(seen, []);
|
|
accounts = ['0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826'];
|
|
await win.igneum._tick();
|
|
assert.deepEqual(seen, [accounts]);
|
|
win.igneum.removeListener('accountsChanged', h);
|
|
accounts = [];
|
|
await win.igneum._tick();
|
|
assert.equal(seen.length, 1);
|
|
});
|