'; }).join('');
+ $('upd-more').hidden = !m.more; if (!m.more) updMore = false;
+ $('upd-more').textContent = updMore ? 'Less' : 'What changed'; $('upd-more').setAttribute('aria-expanded', updMore ? 'true' : 'false');
+ $('upd-all').hidden = !updMore;
+ $('upd-mark').className = 'upd-mark ' + m.ring;
+ var acts = asked ? [{ act: 'install', label: m.stage === 'ready' || m.stage === 'deferred' ? 'Installing' : 'Installs when ready', primary: true, disabled: true }, { act: 'later', label: 'Later' }] : m.actions;
+ $('upd-actions').innerHTML = acts.map(function (a) { return ''; }).join('');
+ wrap.dataset.dismiss = m.dismissable ? '1' : '';
+ }
+ if (m.ring === 'progress') { $('upd-arc').style.strokeDashoffset = (276.5 * (1 - m.pct / 100)).toFixed(1); $('upd-pct').textContent = m.pct + '%'; $('upd-pct').hidden = m.stage !== 'downloading'; }
+ else { $('upd-arc').style.strokeDashoffset = ''; $('upd-pct').hidden = true; }
+ var meta = [];
+ if (m.stage === 'downloading' && m.size) meta.push(m.pct + '% of ' + m.size);
+ else if (m.size && m.stage === 'available') meta.push(m.size + ' download');
+ else if (m.size && (m.stage === 'ready' || m.stage === 'manual' || m.stage === 'deferred' || m.stage === 'staging')) meta.push(m.size + ', downloaded');
+ if (m.note) meta.push(m.note);
+ $('upd-meta').textContent = meta.join(' · ');
+ if (wrap.hidden) { wrap.hidden = false; $('upd-card').focus({ preventScroll: true }); }
+ }
+ function cardLater() {
+ if (!updMem.open || !$('upd').dataset.dismiss) return;
+ var m = UpdateCard.model(state && state.update, state || {});
+ updMem.later = m ? m.key : updMem.later; updMem.open = false; $('upd').hidden = true; updSig = '';
+ }
+ $('upd').addEventListener('click', function (e) {
+ if (e.target === $('upd')) { cardLater(); return; }
+ var b = e.target.closest('[data-act]'); if (!b || b.disabled) return;
+ var act = b.dataset.act, m = UpdateCard.model(state && state.update, state || {});
+ if (act === 'later') cardLater();
+ else if (act === 'install' || act === 'retry') { post('/api/update/install').catch(function () {}); updAsked = m ? m.key : ''; updSig = ''; toast(act === 'retry' ? 'Trying the update again' : m && m.stage === 'ready' ? 'Installing now' : 'Installs as soon as it is downloaded'); if (state) renderUpdateCard(state); }
+ else if (act === 'open') post('/api/update/open').catch(function () {});
+ });
+ $('upd-more').onclick = function () { updMore = !updMore; $('upd-all').hidden = !updMore; this.textContent = updMore ? 'Less' : 'What changed'; this.setAttribute('aria-expanded', updMore ? 'true' : 'false'); };
+
+ document.addEventListener('visibilitychange', function () {
+ if (document.hidden) return;
+ poll();
+ if (state && state.phase === 'unlock' && !locking) focusLock();
+ });
+
+ resetSend();
poll();
- // the window came back (menu bar, Dock): the button gets the focus so Return unlocks; never a sheet by itself
- if (state && state.phase === 'unlock' && !locking) focusLock();
-});
-new MutationObserver(() => { if (document.body.dataset.view === 'settings') renderNetwork(); }).observe(document.body, { attributes: true, attributeFilter: ['data-view'] });
-
-poll();
-setInterval(poll, 2000);
+ setInterval(poll, 2000);
+})();
diff --git a/app/igneum-wallet/ui/index.html b/app/igneum-wallet/ui/index.html
index c931301cc..281779d17 100644
--- a/app/igneum-wallet/ui/index.html
+++ b/app/igneum-wallet/ui/index.html
@@ -4,33 +4,67 @@
Igneum Wallet
-
+
-
+
+
+
+
-
+
- IGNEUMWALLET
+ IGNEUMWALLET
+
+
+
Home
+
starting
-
-
+
-
- A new version of Igneum Wallet is ready.
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Quit Igneum Wallet? The bundled node stops with it.
+
+
+
@@ -38,12 +72,12 @@
-
+
devnet v4 · nothing is bought or sold
Igneum Wallet
-
Your IGN, your key, on this machine. The key is made here and never leaves this app.
+
Your IGN and your key, on this machine. The key is made here and never leaves this app.
-
01
Your key stays here
Encrypted with a password you choose. Signing happens inside the app.
+
01
Your key stays here
Sealed with a password you choose. Signing happens inside the app.
02
Final means verified
A payment is final when this wallet has checked the network's certificate itself.
03
Works with the miner
Uses the miner app's node when it runs here. Imports the miner's key in one tap.
@@ -51,31 +85,31 @@
-
Nobody from Igneum will ever ask for your words or your key.
+
Nobody from Igneum will ever ask for your words or your key.
-
step 1 of 3
+
step 1 of 3
Choose a password
It locks the key on this machine. Nobody can reset it for you. At least 8 characters.
-
-
+
+
-
step 2 of 3
+
step 2 of 3
Write these 24 words down
They are the wallet. Anyone with them has your IGN. They are shown once.
-
Address
+
your address
-
step 3 of 3
+
step 3 of 3
Type three of them
So the paper is right before the words are gone from the screen.
@@ -87,31 +121,28 @@
-
import
+
import
Bring a key here
Words from any wallet, a raw private key, or the key the miner app made on this machine.
-
-
-
-
+
+
+
+
-
-
The miner app's key file on this machine will be read. Rewards keep going to the same address.
-
-
+
+
The miner app's key file on this machine will be read. Rewards keep going to the same address.
+
+
-
+
-
+
Igneum Wallet
@@ -126,182 +157,224 @@
-
Forgot it? Only the 24 words or the key open this wallet again.
+
Forgot it? Only the 24 words or the key open this wallet again.
-
+
-
-
-
balance
-
-
-
-
0IGN
-
-
+
+
+
+
+
0IGN
+
-
-
+
+
not set
+
+ Your words are not written down yet. Back up now.
-
Your words have not been written down yet. Back up now.
-
-
-
node
+
+
+
+
-
-
finality
-
-
+
-
history
-
+
Recent
+
-
+
-
-
-
send
-
Send IGN
-
-
-
+
+
+
+
Send IGN
+
+
+
+
+ fee
+ shown when you review
+
+
+
-
+
-
-
-
Amount
-
To
-
Fee, at most
-
Leaves the wallet, at most
-
-
-
-
-
+
+
+
+
+
+
-
-
Sent
-
It is pending until a block carries it, then in a block, then final once this wallet verifies the checkpoint over it.
-
-
+
+
+
+
-
+
-
-
-
receive
-
Your address
-
-
-
-
Only IGN on the Igneum network. Rewards from the miner app land here when the miner pays to this address.
-
-
-
-
-
-
transaction
-
Transfer
-
-
-
-
-
-
-
-
-
settings
-
Settings
-
-
-
Touch ID
-
Unlock the wallet, confirm each send and show the backup with a fingerprint. The password still works everywhere.
-
-
-
-
-
-
-
-
Lock when idle
-
-
+
+
+
+
Your address
+
not set
+
+
+
Only IGN on the Igneum network. Rewards from the miner app land here when it pays this address. The code is drawn on this machine.
+
+
-
Backup
-
Show the 24 words (or the key) again. Needs the password.
-
+
+
+
+
History
+
+
+
+
+
+
+
+
Security
+
+
Touch ID
+
+
+
+
+
+
+
+
+
+
Lock when idle
+
+
+
+
+
+
Touch ID is turned off by a password change; turn it on again above.
+
+
+
+
+
+
Backup
+
+
+
+ Show the 24 words and the key on screen? Anyone who sees them has your IGN.
+
+
+
+
+
-
Private key
-
+
private key
-
Password
-
-
-
-
-
Export to MetaMask
-
Add the network, then import the private key. The key leaves this app only when you copy it here.
-
-
-
-
-
-
- Export the private key
-
A copied key can be stolen from the clipboard, a screenshot or a notes app. Paste it straight into MetaMask and clear the clipboard.
-
-
-
+
+
+
A copied key can be stolen from the clipboard, a screenshot or a notes app. Paste it straight into MetaMask and clear the clipboard.
+
+ Show the private key on screen? Anyone who sees it has your IGN.
+
+
+
+
+
+
-
Network
-
+
+
MetaMask network
+
-
-
This machine
-
-
-
-
-
-
-
Remove this wallet from this machine
-
The vault file is deleted. Only your 24 words or the key bring it back.
-
-
-
-
-
-
+
+
This machine
+
+
+
appearance
+
+
+
+
+
+
+
+
+
+
+
+
Igneum Wallet
+
Not checked yet.
+
+
+
+
+
+
+
+
+
+
+
Node
+
+
endpoint in use
+
none yet
+
+
+
The wallet picks the miner's node when it runs on this machine, else its own bundled node, else the public RPC. Finality is verified only with a node here.
+
+
+
+
+
Advanced
this machine
+
+
+
+
+
+ Remove this wallet from this machine? The vault file is deleted. Only your 24 words or the key bring it back.
+
+
+
+
+
+
+
+ leaves the strip above -->
diff --git a/app/igneum-wallet/ui/view.test.mjs b/app/igneum-wallet/ui/view.test.mjs
new file mode 100644
index 000000000..310906446
--- /dev/null
+++ b/app/igneum-wallet/ui/view.test.mjs
@@ -0,0 +1,161 @@
+// node --test app/igneum-wallet/ui/view.test.mjs (no dependencies)
+// Loads the View block of app.js (plain browser JS: the file is run with `module` defined and no `document`, so only
+// the pure block executes) and checks the words each page shows for a given state (wallet-ui-3).
+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)), 'app.js'), 'utf8');
+const mod = { exports: {} };
+new Function('module', src)(mod);
+const V = mod.exports.View;
+
+const ME = '0x7e5f4552091a69125d5dfcb7b8c2659029395bdf', B = '0x2b5ad5c4795c026514f8317c7a215e218dccd6cf';
+const NOW = 1_791_000_000;
+const node = (over) => ({ source: 'miner', state: 'ok', chain_id: 7762959, chain: 'igneum-devnet-20', block: 140286, evm: 'http://127.0.0.1:26790', grpc: '127.0.0.1:26610', synced: true, message: "using the miner app's node on this machine", finality_active: true, latest_locked: 4674, ...over });
+const fin = (over) => ({ verified_index: 4674, verified_hash: '0xf1', chain_number: 140246, signers: 11, voters: 12, fraction_total: 0.912, fraction_active: 0.98, weights_exact: true, verified_at: NOW - 14, message: 'checkpoint 4674 verified here', ...over });
+const st = (over) => ({ version: '0.1.5', phase: 'home', balance: '31.6321', balance_known: true, scanning: false, scanned_to: 140286, node: node(), finality: fin(), settings: { network: 'devnet', start_at_login: false, auto_update: true, idle_lock_min: 5, ask_on_open: true }, update: { status: 'current', version: '0.1.5', checked_at: NOW - 780 }, now: NOW, ...over });
+const entry = (over) => ({ hash: '0x' + 'c3d4'.repeat(16), kind: 'received', block: 140262, block_hash: '0xabcd', from: B, to: ME, value: '250000000000000000', fee: '25380000000000', ok: true, time: NOW - 140, finality: 'in_block', checkpoint: null, note: '', ...over });
+
+test('the five pages and their order', () => {
+ assert.deepEqual(V.PAGES.map((p) => p.id), ['home', 'send', 'receive', 'history', 'settings']);
+ assert.equal(V.page('history').title, 'History');
+ assert.equal(V.page('nonsense').id, 'home');
+});
+
+test('numbers: IGN from wei, short addresses, times', () => {
+ assert.equal(V.ign('1500000000000000000'), '1.5');
+ assert.equal(V.ign('250000000000000000', 6), '0.25');
+ assert.equal(V.ign('10140000000000000000', 4), '10.14');
+ assert.equal(V.ign('0'), '0');
+ assert.equal(V.ign('nonsense'), '0');
+ assert.equal(V.ign('76140000000000', 9), '0.00007614');
+ assert.equal(V.shortHex('0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf'), '0x7E5F45\u20265Bdf');
+ assert.equal(V.shortHex(''), '');
+ assert.equal(V.withCommas(140286), '140,286');
+ assert.equal(V.ago(14), '14 s ago'); assert.equal(V.ago(780), '13 min ago'); assert.equal(V.ago(7200), '2 h ago');
+ assert.match(V.timeWord(NOW - 140, NOW), /^\d\d:\d\d$/);
+ assert.match(V.timeWord(NOW - 86400 * 3, NOW), /^\d+ \w+ \d\d:\d\d$/);
+});
+
+test('the balance: the number only when known, else one line that says why (never a 0)', () => {
+ assert.deepEqual(V.balanceLine(st()), { known: true, text: '' });
+ assert.deepEqual(V.balanceLine(st({ balance_known: false })), { known: false, text: 'reading the balance' });
+ assert.deepEqual(V.balanceLine(st({ balance_known: false, scanning: true, scanned_to: 61200, node: node({ block: 140270 }) })), { known: false, text: 'reading the chain, 61,200 of 140,270 blocks' });
+ assert.deepEqual(V.balanceLine(st({ balance_known: false, node: node({ state: 'starting' }) })), { known: false, text: 'waiting for the node to start' });
+ assert.deepEqual(V.balanceLine(st({ balance_known: false, node: node({ state: 'off', source: 'none' }) })), { known: false, text: 'waiting for a node' });
+});
+
+test('money first when a price exists; else the one grey line, devnet or not', () => {
+ assert.deepEqual(V.moneyLine(st()), { money: '', text: 'no market price on devnet: coins have no value' });
+ assert.deepEqual(V.moneyLine(st({ settings: { network: 'testnet' } })), { money: '', text: 'no market price yet' });
+ assert.deepEqual(V.moneyLine(st({ price_gbp_per_ign: 0.5 })), { money: '\u00a315.82', text: "at today's price" });
+ // a price with an unknown balance is no money line
+ assert.equal(V.moneyLine(st({ price_gbp_per_ign: 0.5, balance_known: false })).money, '');
+});
+
+test('the node line: the state word, the source in plain words, the block, the verification', () => {
+ assert.deepEqual(V.nodeLine(st()), { text: "Node synced \u00b7 the miner app's node \u00b7 block 140,286 \u00b7 verified to checkpoint 4,674", sub: '', tone: 'ok' });
+ const pub = st({ node: node({ source: 'public', grpc: '', finality_active: false, latest_locked: 0, message: 'using the public RPC; no node here, so finality cannot be verified' }), finality: fin({ verified_index: 0, chain_number: null, message: 'no node here' }) });
+ assert.equal(V.nodeLine(pub).text, 'Node synced \u00b7 the public RPC \u00b7 block 140,286 \u00b7 certificates cannot be verified without a node');
+ const own = st({ node: node({ source: 'own', state: 'starting', block: 0, message: 'the bundled node is starting; the chain syncs from the seed' }), finality: fin({ verified_index: 0 }) });
+ assert.deepEqual(V.nodeLine(own), { text: 'Node starting \u00b7 the bundled node', sub: 'the bundled node is starting; the chain syncs from the seed', tone: '' });
+ const none = st({ node: node({ source: 'none', state: 'off', block: 0, message: 'no node: install Igneum Miner, or wait for the bundled node' }), finality: fin({ verified_index: 0 }) });
+ assert.deepEqual(V.nodeLine(none), { text: 'Node not found', sub: 'no node: install Igneum Miner, or wait for the bundled node', tone: 'bad' });
+ const lost = V.nodeLine(st({ node: node({ state: 'lost', message: 'history: connect 127.0.0.1:26790: Connection refused (os error 61)' }) }));
+ assert.equal(lost.tone, 'bad'); assert.equal(lost.sub, 'the node stopped answering; trying again');
+ assert.equal(V.nodeLine(st({ finality: fin({ verified_index: 0 }) })).text, "Node synced \u00b7 the miner app's node \u00b7 block 140,286 \u00b7 waiting for the first certificate");
+ assert.equal(V.sourceWords('external'), 'the node named by the environment');
+});
+
+test('the node details: every old card row with a one-line meaning, no n/a', () => {
+ const rows = V.nodeDetails(st(), NOW);
+ const keys = rows.map((r) => r[0]);
+ assert.deepEqual(keys, ['source', 'chain', 'block', 'endpoint', 'finality on the node', 'verified here', 'signed by', 'checkpoint height', 'weights', 'checked']);
+ assert.equal(rows[5][1], 'checkpoint 4,674'); assert.equal(rows[5][3], 'ok');
+ assert.equal(rows[6][1], '11 of 12 voters'); assert.match(rows[6][2], /^91\.2% of all weight/);
+ assert.equal(rows[9][1], '14 s ago');
+ for (const r of rows) assert.ok(!/n\/a/.test(r[1]) && r[1] !== '', r[0]);
+ const none = V.nodeDetails(st({ node: node({ source: 'public', evm: 'https://rpc.example', finality_active: false }), finality: fin({ verified_index: 0, message: 'no node here' }) }), NOW);
+ assert.equal(none.find((r) => r[0] === 'finality on the node')[1], 'not checkable without a node');
+ assert.deepEqual(none.find((r) => r[0] === 'verified here').slice(1), ['nothing yet', 'no node here', 'dim']);
+});
+
+test('the pill: the wallet\'s own words', () => {
+ assert.deepEqual(V.pillWords(st()), { text: 'synced', cls: 'on' });
+ assert.deepEqual(V.pillWords(st({ node: node({ source: 'public' }) })), { text: 'public rpc', cls: 'on' });
+ assert.deepEqual(V.pillWords(st({ node: node({ state: 'starting' }) })), { text: 'starting', cls: '' });
+ assert.deepEqual(V.pillWords(st({ node: node({ state: 'off', source: 'none' }) })), { text: 'no node', cls: 'warn' });
+ assert.deepEqual(V.pillWords(st({ node: node({ state: 'lost' }) })), { text: 'node lost', cls: 'warn' });
+ assert.deepEqual(V.pillWords(st({ phase: 'unlock' })), { text: 'locked', cls: '' });
+ assert.deepEqual(V.pillWords(st({ quitting: true })), { text: 'stopping', cls: '' });
+});
+
+test('one state word per row: the wallet\'s verified final wins, failed from the receipt, else the node\'s word, else the label', () => {
+ const e = entry();
+ assert.deepEqual(V.stateWord(entry({ finality: 'final', checkpoint: 4673, block: 139900 }), 'executed'), { word: 'finalised', tone: 'ok', why: 'under checkpoint 4,673, verified here' });
+ assert.deepEqual(V.stateWord(entry({ finality: 'failed', ok: false }), 'executed'), { word: 'failed', tone: 'bad', why: 'the execution failed; the fee was still paid' });
+ assert.deepEqual(V.stateWord(e, 'pending'), { word: 'pending', tone: '', why: 'waiting for a block' });
+ assert.deepEqual(V.stateWord(e, 'included'), { word: 'included', tone: '', why: 'in block 140,262, not executed yet' });
+ assert.deepEqual(V.stateWord(e, 'executed'), { word: 'executed', tone: 'ink', why: 'in block 140,262' });
+ assert.deepEqual(V.stateWord(e, 'proven'), { word: 'proven', tone: 'ink', why: 'in block 140,262, proof paid' });
+ // the node says finalised but this wallet has not verified the certificate: the word, not the molten tone
+ assert.deepEqual(V.stateWord(e, 'finalised'), { word: 'finalised', tone: 'ink', why: 'under a locked checkpoint, not yet verified here' });
+ assert.deepEqual(V.stateWord(e, 'finality not active'), { word: 'finality not active', tone: 'ink', why: 'under a locked checkpoint; finality is paused on the network' });
+ assert.equal(V.stateWord(e, 'unknown').word, 'pending');
+ // no node word yet (the public RPC, or not asked): the wallet's label
+ assert.deepEqual(V.stateWord(e, ''), { word: 'included', tone: '', why: 'in block 140,262' });
+ assert.deepEqual(V.stateWord(entry({ finality: 'pending', block: 0 }), ''), { word: 'pending', tone: '', why: 'waiting for a block' });
+ assert.equal(V.stateWord(entry({ finality: 'pending', block: 0 }), 'included').why, 'in a block, not executed yet');
+ // a reward has no transaction hash: executed in its block, finalised under a verified checkpoint
+ const r = entry({ hash: 'reward-140201-0', kind: 'reward', from: '', block: 140201 });
+ assert.deepEqual(V.stateWord(r, ''), { word: 'executed', tone: 'ink', why: 'in block 140,201' });
+ assert.equal(V.stateWord(entry({ hash: 'proving-1-0', kind: 'proving', finality: 'final', checkpoint: 9 }), '').word, 'finalised');
+ // never a stronger word than the chain's: the words the row can show are exactly the node's plus failed
+ const words = new Set(['pending', 'included', 'executed', 'proven', 'finalised', 'finality not active', 'failed']);
+ for (const w of ['pending', 'included', 'executed', 'proven', 'finalised', 'finality not active', 'unknown', '']) assert.ok(words.has(V.stateWord(e, w).word), w);
+});
+
+test('the row model: kind word, who, sign, amount, the rows that need the node\'s word', () => {
+ const m = V.rowModel(entry(), 'included', NOW);
+ assert.equal(m.kind, 'Received'); assert.equal(m.who, 'from 0x2b5ad5\u2026d6cf'); assert.equal(m.amount, '+0.25'); assert.equal(m.in, true); assert.equal(m.synthetic, false); assert.equal(m.nodeWord, 'included');
+ const s = V.rowModel(entry({ kind: 'sent', from: ME, to: B, value: '1500000000000000000' }), '', NOW);
+ assert.equal(s.kind, 'Sent'); assert.equal(s.who, 'to 0x2b5ad5\u2026d6cf'); assert.equal(s.amount, '\u22121.5'); assert.equal(s.in, false);
+ assert.equal(V.rowModel(entry({ kind: 'self', from: ME, to: ME }), '', NOW).who, 'to yourself');
+ assert.equal(V.rowModel(entry({ hash: 'reward-1-0', kind: 'reward' }), '', NOW).who, 'block reward');
+ assert.equal(V.rowModel(entry({ hash: 'proving-1-0', kind: 'proving' }), '', NOW).kind, 'Proving payout');
+ const list = [entry({ hash: '0x01', finality: 'pending' }), entry({ hash: '0x02' }), entry({ hash: 'reward-1-0', kind: 'reward' }), entry({ hash: '0x03', finality: 'final', checkpoint: 1 }), entry({ hash: '0x04', finality: 'failed' }), entry({ hash: '0x05' })];
+ assert.deepEqual(V.needsNodeWord(list, 12), ['0x01', '0x02', '0x05']);
+ assert.deepEqual(V.needsNodeWord(list, 2), ['0x01', '0x02']);
+});
+
+test('send: the fee line, the gas words behind Details, the question in place', () => {
+ const q = { to: B, display_to: '0x2B5AD5c4795c026514f8317c7a215E218DCCD6cF', value_ign: '1.5', fee_max_ign: '0.00007614', total_max_ign: '1.50007614', gas: 25380, base_fee_gwei: '1', tip_gwei: '1', max_fee: '3000000000' };
+ assert.deepEqual(V.feeWords(null), { line: 'shown when you review', detail: '' });
+ const f = V.feeWords(q);
+ assert.equal(f.line, 'at most 0.00007614 IGN');
+ assert.equal(f.detail, 'Gas 25,380 at a base fee of 1 gwei (burned) plus a 1 gwei tip (to the miner), capped at 3 gwei; what is not used comes back.');
+ assert.equal(V.gwei('1500000000'), '1.5 gwei'); assert.equal(V.gwei('25000000000'), '25 gwei'); assert.equal(V.gwei('250000'), '0.00025 gwei'.replace('0.00025', '0'));
+ assert.deepEqual(V.sendAsk(q, true, 'Touch ID'), { text: 'Send 1.5 IGN to 0x2B5AD5\u2026D6cF? Fee at most 0.00007614 IGN; 1.50007614 IGN leaves the wallet at most.', button: 'Confirm with Touch ID' });
+ assert.equal(V.sendAsk(q, false).button, 'Send now');
+ assert.equal(V.sendAsk(q, true, 'Windows Hello').button, 'Confirm with Windows Hello');
+});
+
+test('settings: the Touch ID sentence, the idle sentence, the update card note', () => {
+ assert.equal(V.bioSentence({ enrolled: true }, 'Touch ID', true), 'Touch ID is on. It unlocks the wallet, confirms each send and shows the backup; the password still works everywhere.');
+ assert.equal(V.bioSentence({ enrolled: false, available: true }, 'Touch ID', false), 'Touch ID needs the Igneum Wallet app window; this page is open in a browser.');
+ assert.equal(V.bioSentence({ enrolled: false, available: false }, 'Touch ID', true), 'Touch ID is not set up on this Mac.');
+ assert.equal(V.bioSentence({ enrolled: false, available: false, message: 'Windows Hello is not configured.' }, 'Windows Hello', true), 'Windows Hello is not configured.');
+ assert.equal(V.bioSentence({ enrolled: false, available: true }, 'Windows Hello', true), 'Unlock, confirm each send and show the backup with Windows Hello. The password still works everywhere.');
+ assert.equal(V.idleSentence(5, 'Touch ID'), 'The key is cleared after 5 minutes without you; Touch ID or the password opens it again.');
+ assert.equal(V.idleSentence(0, 'Touch ID'), 'The wallet stays open until you lock it.');
+ assert.equal(V.updateNote({ status: 'current', version: '0.1.5', checked_at: NOW - 780 }, NOW), 'Up to date, checked 13 min ago.');
+ assert.equal(V.updateNote({ status: 'unknown', version: '' }, NOW), 'Not checked yet.');
+ assert.equal(V.updateNote({ status: 'ready', version: '0.1.6', wait: 'installs as soon as nothing is being sent' }, NOW), 'Igneum Wallet 0.1.6 is ready. Installs as soon as nothing is being sent.');
+ assert.equal(V.updateNote({ status: 'current', version: '0.1.5', updated_from: '0.1.4', checked_at: NOW - 60 }, NOW), 'Updated from 0.1.4. Up to date, checked 1 min ago.');
+ assert.equal(V.updateNote({ status: 'off', version: '' }, NOW), 'Updates are off in this build.');
+ assert.equal(V.updateLine({ status: 'downloading', version: '0.1.6', size: 20080717, progress: 0.43 }).text, 'Downloading Igneum Wallet 0.1.6 (20 MB): 43%');
+ assert.equal(V.updateLine({ status: 'current', version: '0.1.5' }), null);
+});
diff --git a/app/mac/IgneumWallet.swift b/app/mac/IgneumWallet.swift
index d6c9ec06f..75eb705cd 100644
--- a/app/mac/IgneumWallet.swift
+++ b/app/mac/IgneumWallet.swift
@@ -130,7 +130,6 @@ final class App: NSObject, NSApplicationDelegate, WKNavigationDelegate, WKUIDele
window.center()
window.delegate = self
window.isReleasedWhenClosed = false
- window.appearance = NSAppearance(named: .darkAqua)
let conf = WKWebViewConfiguration()
conf.userContentController.add(self, name: "biometric") // the page's Touch ID bridge (Biometric.swift)
web = WKWebView(frame: window.contentView!.bounds, configuration: conf)
diff --git a/docs/plans/wallet-ui-3-audit.md b/docs/plans/wallet-ui-3-audit.md
new file mode 100644
index 000000000..b79f4fe1d
--- /dev/null
+++ b/docs/plans/wallet-ui-3-audit.md
@@ -0,0 +1,209 @@
+# Igneum Wallet UI 3: the audit, 6 October 2026
+
+the project lead, 18:5x UK: "rebuild the wallet to the same standard" as the redesigned miner (miner-ui-3, shipped in Igneum Miner
+0.3.14), "and update the site on that too". Branch `wallet-ui-3`, worktree `../igneum-wt-wallet-ui`, from `wallet-0.1.5`
+1fa6fa7 (the shipper's 0.1.5 skeleton: wallet-v1 and release-0.3.13 merged, the version files at 0.1.5). The design is
+`docs/plans/wallet-ui-3.md`; this file is what was found. The standard is `docs/plans/miner-ui-3.md` and its audit.
+
+## 1. How it was looked at
+
+| What | How |
+|---|---|
+| Every wallet screen | `tools/ui-mock/wallet.mjs` on port 4320 (new in this branch): a stand-in engine that serves `app/igneum-wallet/ui` and answers `/api/state` from a scenario. No vault, no key and no node were read; every address is one of the well-known test keys (`0x7E5F…5Bdf`, `0x2B5A…D6cF`). the project lead's live wallet data were not touched. |
+| The captures | `tools/ui-mock/shoot-wallet.mjs`: Playwright's own chromium (the cached Chrome for Testing build, headless, never Chrome.app) at 1280 x 820 (the shipped window is 1120 x 780, the hosts' floor 900 x 620), 900 x 600 and 390 x 844, dark and light. |
+| States this Mac cannot show | the scenarios: `welcome`, `unlock` (Touch ID enrolled), `home` (the miner app's node, a verified checkpoint, a history with every state), `public` (the public RPC, no certificate verification), `scanning` (the bundled node syncing), `nonode`, `empty`, `update` (an update ready; the card opens by itself). |
+
+Screenshots: `docs/plans/wallet-ui-3/00-*.png` to `24-*.png`, named below. The build's screenshots carry the same names
+with an `n` in front. `05-unlock-password` did not capture: the old lock screen's "Use password" link never became
+clickable under the mock (the control area keeps its height and the link sits under the fingerprint button's line);
+the 0.1.4 lock screen is kept as it is, so the build's `n05` covers it.
+
+## 2. What a first-time wallet user sees, screen by screen
+
+Rating: 10 = nothing to change. The three questions per screen, as the miner's audit: what does it say in jargon,
+which number has no unit or no meaning, which 0 or blank has no reason word. A fourth for a wallet: where does the
+user's money sit, and what does each state word let them do.
+
+### 2.1 Welcome (`00-welcome.png`), 8/10
+
+Good: one heading, one lead, three plain tiles (your key stays here, final means verified, works with the miner), two
+buttons, the "nobody will ask for your words" line. The eyebrow "devnet v4 · nothing is bought or sold" is honest.
+
+| Problem | Where |
+|---|---|
+| The coin is the retired ringed coin (`brand/README.md`: retired as a source; the miner-ui-3 welcome uses the mark). | the hero |
+| 60 px headline, 148 px coin, three tiles and two buttons need 1,000 px of height; at 900 x 620 (the hosts' floor) the buttons sit under the fold. | whole screen |
+
+### 2.2 Create (`01-create-password.png`, `02-create-words.png`), 8/10
+
+Good: three steps named, one sentence each, the words in a numbered grid, three typed back before the words are gone.
+Keep. The button "Make my words" is right; "Open my wallet" on step 3 is right.
+
+| Problem | Where |
+|---|---|
+| The address under the words is a 42-character string with no Copy and no word for what it is beyond "Address". | step 2 |
+| The step eyebrows are ember, the miner's are ash: two conventions for one thing. | the eyebrows |
+
+### 2.3 Import (`03-import.png`), 8/10
+
+Good: three modes as a segmented control, the miner's key as the third, the note that rewards keep going to the same
+address. Keep.
+
+### 2.4 The lock screen (`04-unlock-touch.png`), 9/10
+
+The 0.1.4 lock screen is the best screen in the app: the glow, the name, the short address, one control, the quiet
+"Use password". It keeps the retired coin; the build swaps the mark in and keeps everything else, including
+`lock-screen.js` and its tests.
+
+### 2.5 Home (`06-home.png`, lower `07-home-lower.png`), 6/10
+
+What a wallet user sees first. Good: the balance large, the address with Copy, Send and Receive on the card, the
+history on the same page.
+
+| Problem | Where |
+|---|---|
+| No money. "31.6321 IGN" with no pounds line and no word for why there is none. The miner's Earnings says "nothing is bought or sold on devnet" on its money line; the wallet says it only on the welcome eyebrow. | the balance |
+| Two engine cards side by side: Node (source, chain, block, finality, note: "igneum-devnet-20", "active, locked 4674", "using the miner app's node on this machine") and Finality ("verified here checkpoint 4674", "signed 11 of 12 voters, 91.2% of weight", "chain height 140,246", "weights at the checkpoint", "checked 6 Oct 19:05"). Ten rows of plumbing between the balance and the history. A wallet user needs one line: the node is synced and the wallet has verified up to a checkpoint. | the two cards |
+| The history's state column is the wallet's label in capitals: "PENDING", "IN BLOCK 140262", "FINAL · CP 4673", "FAILED". The node has five state words for a transaction (`igneum_getTransactionStatus`: pending, included, executed, proven, finalised; `vendor/igneum-node` `igneum/exec/src/rpc.rs`, "ONE state word, never a stronger word than the chain's own state") and the wallet shows none of them: "in block" covers included, executed and proven alike. | the history |
+| No reason under a state: "PENDING" (waiting for a block), "FAILED" (the execution failed; the fee was paid) are bare. | the history |
+| "SENT", "RECEIVED", "REWARD", "PROVING", "SELF" pills in capitals; "to 0x2b5ad5…ccd6cf" in lower-case hex while the balance card shows the checksummed form. | the history |
+| The amount column is the only coloured cell and it colours incoming molten; the state word, which is what changes, is grey. | the history |
+| The pill in the top bar: "MINER NODE · BLOCK 140,286" in capitals: the node's source and the chain height, neither of which a wallet user asked for. | top bar |
+| Settings is behind a gear icon; Receive and History have no place of their own; the page is one long scroll. | the chrome |
+
+Apple would: the balance first with its money line, Send and Receive, one node line with Details behind it, then
+the five newest rows with "All history". The engine rows go behind Details.
+
+### 2.6 Send (`08-send.png`, review `09-send-review.png`, sent `10-send-sent.png`), 7/10
+
+Good: two fields, Review, a review card with the amount, the address, the fee at most and what leaves the wallet at
+most; the Touch ID confirm on the Send button; a Sent screen.
+
+| Problem | Where |
+|---|---|
+| The fee note is a 40-word formula: "Fee = gas × price. Gas 25,380. Price = base fee 1 gwei (burned by the network) + tip 1 gwei (to the miner), capped at 3.000 gwei per gas; what is not used comes back." Gas, gwei, base fee, tip, cap: five terms a wallet user never chose. | the review |
+| The review is a second screen, not a confirmation in place: Edit goes back, Send now goes on, and the two fields are gone from view. | the review |
+| The Sent screen's one paragraph "It is pending until a block carries it, then in a block, then final once this wallet verifies the checkpoint over it." is the right idea as three state words, written as a sentence; the hash is a bare 66-character line with no Copy. | sent |
+| No balance on the form: "Amount, IGN" with no "of 31.6321" beside it, so the only way to learn the limit is the quote's refusal. | the form |
+
+### 2.7 Receive (`11-receive.png`), 8/10
+
+Good: the QR drawn locally, the address, Copy address, one note. Keep. The address is a paragraph, not a box with
+Copy beside it, so the eye finds the button under it rather than next to it.
+
+### 2.8 Transaction (`12-tx-final.png`), 6/10
+
+| Problem | Where |
+|---|---|
+| "FINAL · under checkpoint 4673, certificate verified by this wallet" in a mono box, then eight key-value rows: amount, when, from, to, fee paid, hash, block "139,900 · 0x0d9c…9c0d", verified checkpoint "4674 at chain height 140,246". Right facts, engine order. | whole screen |
+| It is a page of its own with Back; the history row it came from is out of view. | the chrome |
+
+### 2.9 Settings (`13-settings.png`, `14-settings-lower.png`, `15-settings-lowest.png`), 5/10
+
+The page with the most work in it. Seven cards, 2,000 px tall, every one with a password field.
+
+| Problem | Where |
+|---|---|
+| Three password fields on one page (Backup, Export the private key, Remove this wallet) plus two for the password change. Each sits open on the page waiting to be typed into, where the miner uses a question in place that opens only when asked. | Backup, Export, Password, Remove |
+| The switches are the browser's checkboxes (fixed 5 October 2026 to be visible at all), not the miner's track switch; the labels have no second sentence. | Touch ID, This machine |
+| "Export to MetaMask" lists network name, chain id "7762959 (0x76770f)", rpc url, symbol, decimals, then a warning box, then the password field. "Network" repeats source "miner", "ethereum rpc http://127.0.0.1:26790", "grpc 127.0.0.1:26610", chain id, network, public rpc. "This machine" lists machine "d937c69d", version, logs path, "miner key file present". Three cards of engine facts. | MetaMask, Network, This machine |
+| No Appearance. No light mode (``, one token set). | whole page |
+| The version line "Igneum Wallet 0.1.5 · up to date" sits as a mono line under the heading; the update controls (Check for updates, Install now, the auto-update checkbox) sit two cards down inside "This machine". The miner has one update card. | version-row, This machine |
+| "Remove this wallet from this machine" uses `confirm()`, a dialog the viewer never shows (the brief's rule, as the miner's quit). | `app.js` `remove-go` |
+| The node endpoint cannot be changed; the engine has no route for it (the source is chosen by `node.rs`: the miner's node, else the bundled node, else the public RPC). Shown, not settable. | Network |
+| The Back button at the foot is the only way out besides the gear. | foot |
+
+Apple would: Settings as plain rows with one sentence each, grouped: Security (Touch ID, ask on open, lock when idle,
+change password), Backup (show my words, export the key, MetaMask), This machine (start at login, Appearance), one
+update card, Node (the endpoint in use), Advanced (the ids, the folders, remove).
+
+### 2.10 The public RPC, scanning, no node, empty (`16-home-public.png`, `17-home-scanning.png`, `18-home-nonode.png`, `19-home-empty.png`), 6/10
+
+| Problem | Where |
+|---|---|
+| Public RPC: "finality not checkable without a node" and "no node here: certificates cannot be verified" in ember, twice, in two cards; the history's "IN BLOCK" rows carry no word that they will stay there. The pill says "PUBLIC NODE" when there is no node. | 16 |
+| Scanning: "reading the chain, 61,200 of 140,270 blocks" under a dimmed balance "0": a 0 in the hero while the real number is unknown. | 17 |
+| No node: balance "0" dimmed with "waiting for a node"; the Node card says "no node: install Igneum Miner, or wait for the bundled node". Right words, wrong place (a card, not the node line). | 18 |
+| Empty: "Nothing yet. Receive IGN, or point the miner app at this address." is good. "Your words have not been written down yet. Back up now." as a note under the buttons is good. Keep both. | 19 |
+
+### 2.11 The update card (`20-update-card.png`), 9/10
+
+The miner's card: the mark with the ring, "Igneum Wallet 0.1.6", one line, three notes, the size, Install now and
+Later. `update-card.js` already says `NAME = 'Igneum Wallet'` on this branch (the miner-ui-3 owed row "the wallet's copy
+still says Igneum Ember" was true of wallet-v1 before 0.1.3 and is closed here). Keep.
+
+### 2.12 Narrow (`21-narrow-900-home.png`, phone `22-phone-home.png`, `23-phone-settings.png`)
+
+900 x 600: usable; the two engine cards squeeze, the history's four columns hold. 390 wide: broken. The pill wraps to
+four lines over the gear, "31.6321 IGN" overflows the card to the right, the Node and Finality cards' values wrap one
+character per line ("ch / ec / kp / oi / nt"), Settings' password fields overflow. No phone layout exists.
+
+### 2.13 Light mode (`24-light-home.png`)
+
+None. The page is dark whatever the OS says. The Mac host also pins `window.appearance = darkAqua`
+(`app/mac/IgneumWallet.swift` `buildWindow`), so even a page that followed `prefers-color-scheme` would see dark.
+
+## 3. Every feature the old UI has (the build's tick list)
+
+| # | Feature | Where it is today | Route |
+|---|---|---|---|
+| 1 | Welcome: create or import | welcome | none |
+| 2 | Create: password, 24 words, three typed back | create, 3 steps | `api/create`, `api/create/confirm` |
+| 3 | Import: 24 words, a private key, the miner's key file (disabled when absent) | import | `api/import` |
+| 4 | The lock screen: Touch ID button, the one automatic prompt, Use password, the line under the button | unlock (`lock-screen.js`) | `api/unlock` (the host), the host bridge |
+| 5 | Lock (button, the host's menu, the idle lock with the activity ping every 20 s) | top bar | `api/lock`, `api/activity` |
+| 6 | Balance in IGN, "reading the chain N of M blocks", "reading the balance", "waiting for a node" | home | state |
+| 7 | Address with Copy | home | state |
+| 8 | "Your words have not been written down yet. Back up now." | home | state `backed_up` |
+| 9 | Send: to, amount, the quote (address, amount, balance checks), the review (amount, to, fee at most, leaves at most, the fee note), Touch ID confirm or Send now, Edit, the Sent screen with the hash, View, Done | send | `api/send/quote`, `api/send`, the host `confirm` |
+| 10 | Receive: the QR (SVG from the engine), the address, Copy address | receive | `api/receive` |
+| 11 | History: up to 60 rows, kind, who, when, amount, the wallet's label (pending, in block N, final · cp N, failed), a click opens the transaction | home | state `history` |
+| 12 | Transaction: the finality line, amount, when, from, to, fee paid, hash, block and block hash, note, the verified checkpoint | tx | state (`api/tx/` exists and is unused by the page) |
+| 13 | Node: source, chain, block, finality active and latest locked, the engine's message | home card | state `node` |
+| 14 | Finality: verified checkpoint, signers of voters and the weight fraction, chain height, weights exact or latest, checked at; else the message | home card | state `finality` |
+| 15 | Touch ID: turn on (password once, then the prompt), turn off, "Ask when the wallet opens", lock when idle (1, 5, 15, 60, never), the needs-enrol line after a password change, the "needs the app window" and "not set up" lines | settings | `api/biometric/*`, `api/settings` |
+| 16 | Backup: show the words and the key with the password or Touch ID, Hide, marks backed up | settings | `api/reveal`, `api/backed-up` |
+| 17 | Change the password | settings | `api/password` |
+| 18 | MetaMask: network name, chain id, RPC, symbol, decimals; Add to MetaMask (the site page); Copy network settings; export the key with the password or Touch ID, Copy key, Hide | settings | `api/network`, `api/open`, `api/reveal` |
+| 19 | Network: source, Ethereum RPC, gRPC, chain id, network, public RPC | settings | state |
+| 20 | This machine: start at login, install updates by itself, machine id, version, logs folder, miner key file present, Check for updates, Install now, the update note | settings | `api/settings`, `api/update/*` |
+| 21 | Remove this wallet (password, a `confirm()` dialog) | settings | `api/remove` |
+| 22 | The update banner (one strip) and the update card (`update-card.js`); Later per version and stage; the urgent path | everywhere | `api/update/install`, `open` |
+| 23 | The pill: source and block, starting, connecting, public rpc, stopping, engine gone | top bar | state |
+| 24 | The version in the header and the version line in Settings | chrome | state |
+| 25 | The welcome eyebrow names the network | welcome | state `settings.network` |
+| 26 | `?bio=touch`, `?update=[&auto=0][&card=1]`, `?host=mac` | URL | none |
+| 27 | The quit path (the host's menu; `api/quit` exists, no button) | none | `api/quit` |
+
+What the engine has that the page never shows: `api/tx/` with `node_status` (the node's one state word per
+transaction, `igneum_getTransactionStatus`), `events` (the engine's own activity lines: "sent 1.5 IGN to …", "earned
+10.14 IGN as a block reward"), `uptime_s`, `app_dir`. What no route gives: a market price (`price_gbp_per_ign`, owed by
+the engine as in the miner's plan, section 11), a settable node endpoint.
+
+## 4. The top ten findings
+
+| # | Finding | Consequence per tier |
+|---|---|---|
+| 1 | No money line and no reason for its absence: "31.6321 IGN" alone. The miner's Earnings says why there is no £. | Every user. A wallet that shows a number with no value and no word for it reads as a placeholder. |
+| 2 | The node's five state words (pending, included, executed, proven, finalised) are not shown; the history says "in block" for three of them and the design rule (one state word, never stronger than the chain's) is not applied. | Every user; most for a sender waiting to know whether the other side can rely on a payment. |
+| 3 | Ten rows of node and finality plumbing on the home screen between the balance and the history. | Every user; a home miner's wallet should read like the miner's Mine page: one node line, Details behind it. |
+| 4 | Settings is seven cards with three open password fields and browser checkboxes; the update controls are split between a mono line at the top and a card at the bottom; no Appearance. | Every user. |
+| 5 | The send fee is a 40-word gas formula; the review is a second screen rather than a question in place; the Sent screen's states are a sentence. | Every sender. |
+| 6 | A `confirm()` dialog on Remove (the viewer never shows one). | Every user who removes a wallet: the button appears to do nothing. |
+| 7 | No layout under 900 px: at 390 the pill covers the gear, the balance overflows, the engine cards wrap letter by letter. | Windows laptop users with a half-screen window; the Windows host's window can be dragged narrow. |
+| 8 | No light mode on the page and the Mac host pins dark appearance. | Every Mac user on a light desktop. |
+| 9 | Capitals everywhere a label sits (SENT, RECEIVED, IN BLOCK, MINER NODE · BLOCK 140,286): the engine's vocabulary shouted. | Every user. |
+| 10 | The chrome has no sections: Settings behind a gear, Receive and History without a place, Back buttons at the foot of each view. | Every user. |
+
+Smaller, fixed in the build with no mention elsewhere: the retired ringed coin on the welcome and the lock screen; the
+ember step eyebrows; the lower-case hex in the history rows; the bare hash on the Sent screen; the address under the
+words with no Copy; "PUBLIC NODE" when there is no node; the dimmed "0" while the balance is unknown.
+
+## 5. What the earlier wallet work set out to do and what it achieved
+
+0.1.0 to 0.1.4 (4 to 5 October 2026) built the engine's surface one screen at a time: the vault and the words, the
+quote and the send, the QR, the history with rewards, the certificate verification, Touch ID, the update card, the
+lock screen. Each screen is honest and complete; the lock screen and the update card are already at the standard.
+What it did not do: put the screens on one design (the chrome is the miner's 0.3.3 top bar with a gear), say what the
+balance is worth or why it cannot, use the node's state words, or lay out under 900 px. UI 3 keeps every feature,
+every route and the two finished screens, and puts the rest on the miner's system.
diff --git a/docs/plans/wallet-ui-3.md b/docs/plans/wallet-ui-3.md
new file mode 100644
index 000000000..f711d0d44
--- /dev/null
+++ b/docs/plans/wallet-ui-3.md
@@ -0,0 +1,246 @@
+# Igneum Wallet UI 3: the wallet on the miner's system
+
+6 October 2026. the project lead, 18:5x UK: "rebuild the wallet to the same standard" as the redesigned miner (miner-ui-3, in
+Igneum Miner 0.3.14), "and update the site on that too". The audit is `docs/plans/wallet-ui-3-audit.md`. Branch
+`wallet-ui-3`, worktree `../igneum-wt-wallet-ui`, from `wallet-0.1.5` 1fa6fa7. The 0.1.5 shipper takes it into the
+wallet cut; this branch never touches a release branch. The Rust engine is read as it is: every route the page needs
+exists (`server.rs`), so `src/` is untouched. One host line changes (section 9).
+
+## 1. The one job of each screen
+
+| Screen | Its one job | What it holds | What left it |
+|---|---|---|---|
+| Welcome, Create, Import | a wallet in under a minute, the words written down | as 0.1.4, the mark instead of the retired coin, the address with Copy under the words, ash step eyebrows | the coin |
+| Lock screen | one tap back in | as 0.1.4 (`lock-screen.js` and its tests unchanged), the mark on the glow | the coin |
+| Home | how much is here, and can it be relied on | the balance in IGN with its money line, the address with Copy, Send and Receive, the node line with Details, the five newest rows with "All history" | the Node and Finality cards (to the node line's Details), the long history (to its page) |
+| Send | one payment, confirmed where it was typed | address, amount with the balance beside it, the fee line, Review turns the form into a question in place, then the pending row | the second screen, the gas formula, the Sent paragraph |
+| Receive | the address, two ways | the address in a box with Copy, the QR, one line | nothing |
+| History | one row per transaction with the node's word | every row: kind, who, when, amount, the state word with its reason; a row opens its details in place | the capitals, the wallet-only label, the transaction page |
+| Settings | the switches, one sentence each | Security, Backup, This machine (with Appearance), one update card named Igneum Wallet, Node (the endpoint), Advanced | the seven cards, the three open password fields, the browser checkboxes, the `confirm()` |
+
+Five sections in the rail: Home, Send, Receive, History, Settings. Lock and Quit in the foot. The rail keeps the
+arrow keys and `aria-current`, as the miner's. Under 720 px it is a bottom tab bar with the five sections; Lock stays
+in the top bar; Quit lives in the host's menu (the Mac and Windows hosts both have one).
+
+## 2. Home
+
+```
+31.6321 IGN no market price on devnet: coins have no value
+0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf [Copy] [Send] [Receive]
+
+● Node synced · the miner app's node · block 140,286 · verified to checkpoint 4674 Details ⌄
+
+Recent All history
+ row · row · row · row · row
+```
+
+| Element | Rule |
+|---|---|
+| The balance | `state.balance` in Unbounded at `--t-h1`; "IGN" in mono beside it. While `balance_known` is false the number is not shown: the line reads `reading the balance` (node ok), `reading the chain, 61,200 of 140,270 blocks` (scanning) or `waiting for a node` (no node) in ash. Never a 0 for an unknown. |
+| The money line | with `price_gbp_per_ign` on the state (owed by the engine, as the miner's plan section 11): `≈ £12.34` with "at today's price" in ash. Without it on devnet: `no market price on devnet: coins have no value`. Without it elsewhere: `no market price yet`. Money first when it exists; one grey line explaining its absence when it does not. |
+| The address | the checksummed form in a box, Copy beside it (`data-copy`). The backup note `Your words are not written down yet.` with "Back up now" under it while `backed_up` is false, as before. |
+| Send, Receive | the two buttons; Send is the page's one primary. |
+| The node line | `View.nodeLine(state)`: the state word first (`synced`, `starting`, `connecting`, `lost`, `off`), then the source in plain words (`the miner app's node`, `the bundled node`, `the public RPC`, `the node named by the environment`), the block, and the verification: `verified to checkpoint 4674` when `finality.verified_index` is set, `certificates cannot be verified without a node` on the public RPC, `waiting for the first certificate` otherwise. The dot is molten when ok, ember when lost or off, ash while starting. |
+| Node details | the old Node and Finality cards' facts as `kv` rows with one-line meanings: source (the engine's message), chain (`igneum-devnet-20`, chain id), block, the endpoint in use with Copy, finality on the node (`active, locked 4674` or `paused` or `not checkable without a node`), the verified checkpoint (`4674`, "the newest certificate this wallet checked itself"), signed (`11 of 12 voters, 91.2% of all weight`), the checkpoint's chain height, weights (`taken at the checkpoint` or `the latest table`), checked (`14 s ago`). |
+| Recent | the five newest history rows (section 5's row), then "All history" opens the History page. Empty: `Nothing yet. Receive IGN, or point the miner app at this address.` |
+
+## 3. Send
+
+```
+To [0x… ]
+Amount [1.5 ] IGN of 31.6321 IGN
+Fee at most 0.00007614 IGN Details ⌄
+ [Review]
+```
+
+Review posts `api/send/quote` and turns the card's foot into the question, in place, the fields still visible above
+it and locked:
+
+```
+Send 1.5 IGN to 0x2B5A…D6cF? Fee at most 0.00007614 IGN; 1.50007614 IGN leaves the wallet at most.
+[Confirm with Touch ID] [Cancel]
+```
+
+| Element | Rule |
+|---|---|
+| The fee line | before a quote: `the fee is shown when you review`; after: `at most 0.00007614 IGN`. Details opens one grey line: `Gas 25,380 at a base fee of 1 gwei (burned) plus a 1 gwei tip (to the miner), capped at 3 gwei; what is not used comes back.` The numbers are the quote's (`gas`, `base_fee_gwei`, `tip_gwei`, `max_fee`). |
+| The balance beside the amount | `of 31.6321 IGN` from `state.balance`; absent while the balance is unknown. |
+| The ask | `View.sendAsk(quote)`: the words above; the button reads `Confirm with Touch ID` (or Windows Hello) when `confirm_needed` and the app window is the host, else `Send now`; the host-missing case keeps the old sentence in ember under the ask. Cancel unlocks the fields. |
+| After the send | the card's foot becomes the pending row: `● pending · waiting for a block` with the hash short and Copy, then `View in History` and `Send another`. The row's word follows the node (section 5) on every poll, so the user can watch it go pending → included → executed without leaving. |
+| Errors | the quote's refusals (not an address, more than the balance, the zero address) under the fields in ember, as before. |
+
+The update card never opens while the Send page is up (`UpdateCard.blocked`, `ctx.view === 'send'`, unchanged).
+
+## 4. Receive
+
+The address in a box with Copy, the QR under it (240 px, the engine's SVG), one line: `Only IGN on the Igneum
+network. Rewards from the miner app land here when it pays this address.` The QR is fetched when the page opens
+(`api/receive`), as before.
+
+## 5. History: one row per transaction, the node's word
+
+```
+↓ Received from 0x2B5A…D6cF · 19:03 +0.25 IGN ● included in block 140,262, not executed yet ⌄
+↑ Sent to 0x2B5A…D6cF · 19:06 −1.5 IGN ● pending waiting for a block
+◆ Reward block reward · 19:01 +10.14 IGN ● executed in block 140,201
+```
+
+| Part | Rule |
+|---|---|
+| Kind | a word, not a pill: Sent, Received, To yourself, Reward, Proving payout; an arrow glyph in ash before it. |
+| Who | `from 0x2b5a…d6cf` or `to …` as the engine gives it (lower-case hex, the first 6 and last 4), `block reward`, `shard payout`; the time as `19:03`, with the date when not today. The checksummed form needs keccak, which the page does not carry: the engine could add `display_from` and `display_to` (owed, section 10). |
+| Amount | `+0.25 IGN` (incoming, molten) or `−1.5 IGN` (outgoing, bone); up to six decimals, trailing zeros cut. |
+| State word | ONE word, the node's (Ledger P17, design 2.4: never a stronger word than the chain's). `View.stateWord(entry, nodeState)`: the wallet's own `final` (a checkpoint this wallet verified covers it) → `finalised`; the wallet's `failed` → `failed`; else the node's `state` from `api/tx/` (`pending`, `included`, `executed`, `proven`, `finalised`, `finality not active`) when the page has it; else the wallet's label (`pending` → `pending`, `in_block` → `included`). A reward or a proving payout has no transaction hash (`reward-N-i`): its row says `executed` in a block and `finalised` under a verified checkpoint. |
+| Reason | one grey line: pending `waiting for a block`; included `in block N, not executed yet`; executed `in block N`; proven `in block N, proof paid`; finalised `under checkpoint N, verified here` (or `under a locked checkpoint` when the word is the node's and the wallet has not verified it); finality not active `under a locked checkpoint; finality is paused on the network`; failed `the execution failed; the fee was still paid`. |
+| Colour | pending and included ash; executed and proven ink; finalised molten; failed ember. The dot carries the colour. |
+| Details | the chevron opens the row: hash with Copy, from, to (full checksummed), fee paid, block and block hash, the checkpoint, the engine's note, and the node's word verbatim (`node: proven`) so the two sources are side by side. |
+| Where the node's word comes from | `api/tx/` GET (exists, unused before). The page asks for the rows that are not `final` and not `failed`, at most 12 per poll, newest first, and keeps the answer per hash; a row the wallet marks `final` stops asking. On the public RPC the call is tried once and the fallback label is used when it errors. |
+
+The page lists every entry the state carries (up to 2,000 in the engine's file; the page renders them all, the list is
+short). Empty: as Home's.
+
+## 6. Settings
+
+Plain rows, one sentence each, grouped as the miner's:
+
+| Group | Rows |
+|---|---|
+| Security | Touch ID (or Windows Hello): on → `Touch ID is on. It unlocks the wallet, confirms each send and shows the backup; the password still works everywhere.` with Turn off; off → the switch opens the password field in place with Turn on (the enrolment flow as before: password once, then the prompt); the "needs the app window" and "not set up" lines as before · Ask for Touch ID when the wallet opens (switch) · Lock when idle (the select: 1, 5, 15, 60 minutes, never; one sentence) · Change password (a disclosure: current, new, Change) |
+| Backup | Show my words (a disclosure; the ask in place: `Show the 24 words and the key on screen? Anyone who sees them has your IGN.` then the password field or the Touch ID button, then the words grid, the key, Hide; marks `backed_up`) · Export the key for MetaMask (a disclosure with the warning sentence, the same ask, Copy key, Hide) · MetaMask network: one line (`Igneum devnet · chain id 7762959 · rpc.devnet.igneum.network`) with Add to MetaMask and Copy settings |
+| This machine | Start at login (switch: `Opens when you sign in; the wallet locks itself when idle.`) · Appearance: System / Light / Dark (the page's own storage, as the miner's) |
+| Updates | one card: `Igneum Wallet 0.1.5 · up to date, checked 13 min ago` · Check · Install now when ready · `Install updates by itself when nothing is being sent` (switch) |
+| Node | the endpoint in use with Copy (`http://127.0.0.1:26790`, read-only) · the source sentence (`the miner app's node on this machine`) · the public RPC · the chain id and the gRPC port in one grey line. A field to set the endpoint is owed: the engine chooses the source (`node.rs plan_own_node`, the probe order miner → bundled → public) and has no `api/settings {node_endpoint}`. The row says so in one line: `The wallet picks the miner's node when it runs here, else its own, else the public RPC.` |
+| Advanced (details) | the machine id · the logs folder · the wallet folder · `the miner's key file: present` · Remove this wallet from this machine: the password field and the ask in place `Remove this wallet from this machine? The vault file is deleted. Only your 24 words or the key bring it back. Remove Cancel` |
+
+The version row at the top of the old Settings and the Back button at its foot are gone: the update card carries the
+version, the rail carries the way out.
+
+## 7. Confirmations in place
+
+No `confirm()`, no dialogs. A costly action turns its own row into a question with two buttons:
+
+| Action | The row says |
+|---|---|
+| Send | `Send 1.5 IGN to 0x2B5A…D6cF? Fee at most 0.00007614 IGN; 1.50007614 IGN leaves the wallet at most. Confirm with Touch ID / Send now Cancel` |
+| Show my words, Export the key | `Show the 24 words and the key on screen? Anyone who sees them has your IGN. Show (or Show with Touch ID) Cancel` |
+| Remove the wallet | `Remove this wallet from this machine? The vault file is deleted. Only your 24 words or the key bring it back. Remove Cancel` |
+| Quit | `Quit Igneum Wallet? The bundled node stops with it. Quit Cancel` (the strip over the page, as the miner's) |
+| Lock, Copy, Touch ID off, the switches, Check | no question: each reverts in one tap and the toast says what happened |
+
+## 8. Type, spacing, colour, widths
+
+The miner's tokens, verbatim (`docs/plans/miner-ui-3.md` section 9): the eight type sizes, the six-step spacing scale,
+the radii, the dark and light sets, ember as the one accent, molten for live states. `app.css` starts from the miner's
+file; the wallet's own blocks (the words grid, the QR, the history row, the ask) are added on the same tokens.
+
+| Width | Layout |
+|---|---|
+| 1180 px and up | rail 196 px, the balance and its money line on one row, the history row with every cell |
+| 1000 to 1180 | the rail folds to icons |
+| 720 to 1000 | the history row's reason goes under the state word |
+| under 720 | a bottom tab bar (Home, Send, Receive, History, Settings), the balance stacks, the row is two lines (kind and who; amount and state), 16 px gutters, no horizontal scroll |
+
+## 9. The one host change
+
+`app/mac/IgneumWallet.swift` `buildWindow` sets `window.appearance = NSAppearance(named: .darkAqua)`, which pins the
+WKWebView's `prefers-color-scheme` to dark. The line is removed so System follows the Mac; the window's background
+stays obsidian until the page paints (the page covers it). Nothing else in the host moves. The Windows host
+(`app/windows/wallet-host.cpp`) does not pin a scheme; nothing to change.
+
+## 10. What the engine is asked for (none of it built here)
+
+| Field or route | Why | Owner |
+|---|---|---|
+| `price_gbp_per_ign` | the money line | the engine, when a market exists (the miner's plan says the same) |
+| `api/settings {node_endpoint}` or a `node_source` choice | the Node row's field | the engine; today `node.rs` chooses |
+| `node_status` per entry on `/api/state` | would save the per-row `api/tx/` calls | the engine, later; the page works without it |
+
+Until a field arrives the page treats it as unset: the money line explains, the Node row is read-only, the state words
+come from `api/tx/`.
+
+## 11. Tests
+
+`view.test.mjs` (new; `node --test`, no dependencies, loads the `View` block of `app.js` as the miner's does): the
+five pages and their titles; the state word for every combination of the wallet's label and the node's word
+(pending, included, executed, proven, finalised, finality not active, failed; a reward's words; the fallback without
+a node word); the reason line per word; the money line with and without a price, on devnet and off; the node line per
+source and state; the balance line while unknown (reading, scanning, waiting); the send ask's words with and without
+Touch ID; the fee words; the short address; the row model (kind word, who, sign, amount); the Settings' Touch ID
+sentence. `lock-screen.test.mjs` and `update-card.test.mjs` are unchanged and pass.
+
+## 12. The build
+
+| File | What |
+|---|---|
+| `app/igneum-wallet/ui/index.html` | the rail (five sections, Lock and Quit in the foot), the top bar with the page title and the pill, the quit ask strip, the welcome with the mark, Create and Import as before with ash eyebrows and the address Copy, the lock screen as 0.1.4 with the mark, the five pages, the update card, the toast |
+| `app/igneum-wallet/ui/app.css` | the miner's tokens and chrome (dark and light sets, the rail, the top bar, the notices strip, the ask, the disclosures, the segmented control, the switch, the kv, the update card, the bottom tab bar under 720 px), plus the wallet's blocks: the balance hero, the words grid, the checks, the QR, the history row and its details, the send card, the lock screen |
+| `app/igneum-wallet/ui/app.js` | `View` (pure, exported for the test): `PAGES`, `page`, `shortHex`, `ign`, `moneyLine`, `balanceLine`, `nodeWords`, `nodeLine`, `nodeDetails`, `rowModel`, `stateWord`, `stateReason`, `feeWords`, `sendAsk`, `bioSentence`, `updateCardLine`; the DOM block: the pages behind the rail, the per-hash node word cache (`api/tx/`), the asks, the appearance, `?page=`, the existing `?bio=`, `?update=`, `?host=` switches; the Touch ID bridge, the lock-screen logic and the update card logic carried over unchanged |
+| `app/igneum-wallet/ui/view.test.mjs` | section 11 |
+| `app/mac/IgneumWallet.swift` | section 9, one line |
+| `tools/ui-mock/wallet.mjs`, `tools/ui-mock/shoot-wallet.mjs` | the mock and the capture list (the audit's) |
+| `docs/plans/wallet-ui-3/n*.png` | the result, the same names as the audit's captures, plus the real Mac window (`m*.png`) from the built host on a fresh data dir |
+
+## 13. The build (6 October 2026, evening)
+
+Three UI files rewritten, `view.test.mjs` added, one Swift line removed, nothing in `src/`. `node --test` on the four UI
+files: 20 pass (11 view, 5 lock screen, 4 update card). The engine was built in this worktree under the Mac build lock
+(a warm `target`, 100 s) so the new files are compiled in (`include_str!`); the window host was built with `swiftc`
+under the same lock. Captures:
+
+| Set | What | How |
+|---|---|---|
+| `n00` to `n24` | every audit screen on the new UI, the same names | the mock on 4320, Playwright's cached chromium at nice 19, one browser, closed after the batch |
+| `m00-welcome`, `m01-home`, `m02-history`, `m03-receive`, `m04-settings`, `m05-light`, `m06-lock` | the real Mac window (`Igneum Wallet --snapshot`, 1120 x 780, the shipped size) on this worktree's engine, a fresh scratch data dir, a throwaway wallet created through the engine's own API for the shot and deleted with the dir afterwards (never funded, never the project lead's) | the engine at nice 19, the host at nice 19 |
+
+What the real run showed: the engine found the miner app's node on this Mac at 18:31:47Z and lost it 20 s later
+(`connect 127.0.0.1:26790: Connection refused`); on the second run it found no node at all. Node 1 on this Mac was
+not answering steadily between 18:31Z and 18:36Z. The wallet's words for it were right (`Node lost`, then `Node not
+found`, the pill `NODE LOST` / `NO NODE`, the balance line `waiting for a node`); one leak was fixed on the spot: the
+node line's sub showed the engine's raw error, it now says `the node stopped answering; trying again` and keeps the
+raw message in Details. A bare engine has no bundled `igneumd` beside it, so the second run's message is the packaged
+build's own ("igneumd is not next to the wallet"); the DMG carries the node.
+
+Light mode on the real window follows the page's `?theme=light` switch (added for the captures, never stored) and,
+with the host line removed, the Mac's own appearance.
+
+### What the 0.1.5 shipper takes
+
+| Item | Where |
+|---|---|
+| The three UI files, `view.test.mjs`, `update-card.js` and `lock-screen.js` unchanged | `app/igneum-wallet/ui/` |
+| The one Swift line (no pinned `darkAqua`) | `app/mac/IgneumWallet.swift` |
+| The mock and the shot script, the two docs and the capture folder | `tools/ui-mock/wallet.mjs`, `tools/ui-mock/shoot-wallet.mjs`, `docs/plans/wallet-ui-3*.md`, `docs/plans/wallet-ui-3/` |
+| The site: `site/wallet.html`, the wallet section of `site/index.html`, `site/img/wallet-home.webp` and `site/img/wallet-final.webp` (from the mock at 1120 x 780, example addresses, 2240 x 1560) | `site/` |
+| The DMG recipe is unchanged: `packaging/mac/build-wallet-dmg.sh` compiles the engine and the host from these files | the shipper's cut |
+| `tools/ci/no-secrets-check.sh` fails on `app/igneum-wallet/src/vault.rs:129` (a test fixture, a 64-hex key next to the word key) on `wallet-0.1.5` before this branch; untouched here, the shipper's to settle | pre-existing |
+| Owed to the engine: `price_gbp_per_ign`; a settable node endpoint; `display_from` and `display_to` per entry (the page has no keccak, so the row shows the plain hex short form); `node_status` per entry on the state | section 10 |
+
+### The tick list (section 3 of the audit)
+
+| # | Feature | Where it is now |
+|---|---|---|
+| 1 to 3 | Welcome, Create, Import | as before; the mark, the address Copy, ash eyebrows |
+| 4 | The lock screen | as 0.1.4; the mark |
+| 5 | Lock, the idle lock, the activity ping | Lock in the rail foot and the top bar on narrow widths; the ping unchanged |
+| 6 | The balance and its unknown states | Home, the hero; no 0 for an unknown |
+| 7 | The address with Copy | Home, Receive |
+| 8 | The backup note | Home |
+| 9 | Send: quote, review, Touch ID, Sent | Send, the ask in place, the pending row |
+| 10 | Receive | Receive |
+| 11 | History | History (every row), Home (five) |
+| 12 | Transaction details | the row's disclosure |
+| 13, 14 | Node and Finality | the node line and its Details |
+| 15 | Touch ID, ask on open, idle lock | Settings, Security |
+| 16 | Backup | Settings, Backup, with the ask |
+| 17 | Change password | Settings, Security, a disclosure |
+| 18 | MetaMask | Settings, Backup |
+| 19 | Network | Settings, Node |
+| 20 | This machine, updates | Settings, This machine and the update card |
+| 21 | Remove | Settings, Advanced, with the ask (no dialog) |
+| 22 | The update banner and card | unchanged (`update-card.js`); the banner is the notices strip |
+| 23 | The pill | the top bar: the wallet's own words (`synced`, `starting`, `no node`, `locked`, `stopping`, `engine gone`) |
+| 24 | The version | the update card; the rail foot |
+| 25 | The welcome eyebrow | unchanged |
+| 26 | The URL switches | as before, plus `?page=` |
+| 27 | Quit | the rail foot, with the ask strip |
+| new | The money line, the node's state words with reasons, the node line, Appearance and light mode, the bottom tab bar, the Node endpoint row | this build |
diff --git a/docs/plans/wallet-ui-3/00-welcome.png b/docs/plans/wallet-ui-3/00-welcome.png
new file mode 100644
index 000000000..e1fabcd98
Binary files /dev/null and b/docs/plans/wallet-ui-3/00-welcome.png differ
diff --git a/docs/plans/wallet-ui-3/01-create-password.png b/docs/plans/wallet-ui-3/01-create-password.png
new file mode 100644
index 000000000..000465698
Binary files /dev/null and b/docs/plans/wallet-ui-3/01-create-password.png differ
diff --git a/docs/plans/wallet-ui-3/02-create-words.png b/docs/plans/wallet-ui-3/02-create-words.png
new file mode 100644
index 000000000..be6d3a70f
Binary files /dev/null and b/docs/plans/wallet-ui-3/02-create-words.png differ
diff --git a/docs/plans/wallet-ui-3/03-import.png b/docs/plans/wallet-ui-3/03-import.png
new file mode 100644
index 000000000..3c9129637
Binary files /dev/null and b/docs/plans/wallet-ui-3/03-import.png differ
diff --git a/docs/plans/wallet-ui-3/04-unlock-touch.png b/docs/plans/wallet-ui-3/04-unlock-touch.png
new file mode 100644
index 000000000..86e377dcd
Binary files /dev/null and b/docs/plans/wallet-ui-3/04-unlock-touch.png differ
diff --git a/docs/plans/wallet-ui-3/06-home.png b/docs/plans/wallet-ui-3/06-home.png
new file mode 100644
index 000000000..87e98e6ff
Binary files /dev/null and b/docs/plans/wallet-ui-3/06-home.png differ
diff --git a/docs/plans/wallet-ui-3/07-home-lower.png b/docs/plans/wallet-ui-3/07-home-lower.png
new file mode 100644
index 000000000..17006327c
Binary files /dev/null and b/docs/plans/wallet-ui-3/07-home-lower.png differ
diff --git a/docs/plans/wallet-ui-3/08-send.png b/docs/plans/wallet-ui-3/08-send.png
new file mode 100644
index 000000000..ab0349d60
Binary files /dev/null and b/docs/plans/wallet-ui-3/08-send.png differ
diff --git a/docs/plans/wallet-ui-3/09-send-review.png b/docs/plans/wallet-ui-3/09-send-review.png
new file mode 100644
index 000000000..7911830d2
Binary files /dev/null and b/docs/plans/wallet-ui-3/09-send-review.png differ
diff --git a/docs/plans/wallet-ui-3/10-send-sent.png b/docs/plans/wallet-ui-3/10-send-sent.png
new file mode 100644
index 000000000..517564bbc
Binary files /dev/null and b/docs/plans/wallet-ui-3/10-send-sent.png differ
diff --git a/docs/plans/wallet-ui-3/11-receive.png b/docs/plans/wallet-ui-3/11-receive.png
new file mode 100644
index 000000000..64887a8cd
Binary files /dev/null and b/docs/plans/wallet-ui-3/11-receive.png differ
diff --git a/docs/plans/wallet-ui-3/12-tx-final.png b/docs/plans/wallet-ui-3/12-tx-final.png
new file mode 100644
index 000000000..a24206066
Binary files /dev/null and b/docs/plans/wallet-ui-3/12-tx-final.png differ
diff --git a/docs/plans/wallet-ui-3/13-settings.png b/docs/plans/wallet-ui-3/13-settings.png
new file mode 100644
index 000000000..41c45faa4
Binary files /dev/null and b/docs/plans/wallet-ui-3/13-settings.png differ
diff --git a/docs/plans/wallet-ui-3/14-settings-lower.png b/docs/plans/wallet-ui-3/14-settings-lower.png
new file mode 100644
index 000000000..372a38462
Binary files /dev/null and b/docs/plans/wallet-ui-3/14-settings-lower.png differ
diff --git a/docs/plans/wallet-ui-3/15-settings-lowest.png b/docs/plans/wallet-ui-3/15-settings-lowest.png
new file mode 100644
index 000000000..5374287e6
Binary files /dev/null and b/docs/plans/wallet-ui-3/15-settings-lowest.png differ
diff --git a/docs/plans/wallet-ui-3/16-home-public.png b/docs/plans/wallet-ui-3/16-home-public.png
new file mode 100644
index 000000000..ebfb0cc65
Binary files /dev/null and b/docs/plans/wallet-ui-3/16-home-public.png differ
diff --git a/docs/plans/wallet-ui-3/17-home-scanning.png b/docs/plans/wallet-ui-3/17-home-scanning.png
new file mode 100644
index 000000000..d501f2977
Binary files /dev/null and b/docs/plans/wallet-ui-3/17-home-scanning.png differ
diff --git a/docs/plans/wallet-ui-3/18-home-nonode.png b/docs/plans/wallet-ui-3/18-home-nonode.png
new file mode 100644
index 000000000..3cceabe73
Binary files /dev/null and b/docs/plans/wallet-ui-3/18-home-nonode.png differ
diff --git a/docs/plans/wallet-ui-3/19-home-empty.png b/docs/plans/wallet-ui-3/19-home-empty.png
new file mode 100644
index 000000000..7450db92e
Binary files /dev/null and b/docs/plans/wallet-ui-3/19-home-empty.png differ
diff --git a/docs/plans/wallet-ui-3/20-update-card.png b/docs/plans/wallet-ui-3/20-update-card.png
new file mode 100644
index 000000000..9146b8841
Binary files /dev/null and b/docs/plans/wallet-ui-3/20-update-card.png differ
diff --git a/docs/plans/wallet-ui-3/21-narrow-900-home.png b/docs/plans/wallet-ui-3/21-narrow-900-home.png
new file mode 100644
index 000000000..cd6ddc0ae
Binary files /dev/null and b/docs/plans/wallet-ui-3/21-narrow-900-home.png differ
diff --git a/docs/plans/wallet-ui-3/22-phone-home.png b/docs/plans/wallet-ui-3/22-phone-home.png
new file mode 100644
index 000000000..23208bcac
Binary files /dev/null and b/docs/plans/wallet-ui-3/22-phone-home.png differ
diff --git a/docs/plans/wallet-ui-3/23-phone-settings.png b/docs/plans/wallet-ui-3/23-phone-settings.png
new file mode 100644
index 000000000..1f668b07e
Binary files /dev/null and b/docs/plans/wallet-ui-3/23-phone-settings.png differ
diff --git a/docs/plans/wallet-ui-3/24-light-home.png b/docs/plans/wallet-ui-3/24-light-home.png
new file mode 100644
index 000000000..e17008289
Binary files /dev/null and b/docs/plans/wallet-ui-3/24-light-home.png differ
diff --git a/docs/plans/wallet-ui-3/m00-welcome.png b/docs/plans/wallet-ui-3/m00-welcome.png
new file mode 100644
index 000000000..e36667ecb
Binary files /dev/null and b/docs/plans/wallet-ui-3/m00-welcome.png differ
diff --git a/docs/plans/wallet-ui-3/m01-home.png b/docs/plans/wallet-ui-3/m01-home.png
new file mode 100644
index 000000000..fab9c2a90
Binary files /dev/null and b/docs/plans/wallet-ui-3/m01-home.png differ
diff --git a/docs/plans/wallet-ui-3/m02-history.png b/docs/plans/wallet-ui-3/m02-history.png
new file mode 100644
index 000000000..e9fa20077
Binary files /dev/null and b/docs/plans/wallet-ui-3/m02-history.png differ
diff --git a/docs/plans/wallet-ui-3/m03-receive.png b/docs/plans/wallet-ui-3/m03-receive.png
new file mode 100644
index 000000000..0d6bd71d7
Binary files /dev/null and b/docs/plans/wallet-ui-3/m03-receive.png differ
diff --git a/docs/plans/wallet-ui-3/m04-settings.png b/docs/plans/wallet-ui-3/m04-settings.png
new file mode 100644
index 000000000..3a071569e
Binary files /dev/null and b/docs/plans/wallet-ui-3/m04-settings.png differ
diff --git a/docs/plans/wallet-ui-3/m05-light.png b/docs/plans/wallet-ui-3/m05-light.png
new file mode 100644
index 000000000..9e9beffed
Binary files /dev/null and b/docs/plans/wallet-ui-3/m05-light.png differ
diff --git a/docs/plans/wallet-ui-3/m06-lock.png b/docs/plans/wallet-ui-3/m06-lock.png
new file mode 100644
index 000000000..53e535d4b
Binary files /dev/null and b/docs/plans/wallet-ui-3/m06-lock.png differ
diff --git a/docs/plans/wallet-ui-3/n00-welcome.png b/docs/plans/wallet-ui-3/n00-welcome.png
new file mode 100644
index 000000000..47be9745a
Binary files /dev/null and b/docs/plans/wallet-ui-3/n00-welcome.png differ
diff --git a/docs/plans/wallet-ui-3/n01-create-password.png b/docs/plans/wallet-ui-3/n01-create-password.png
new file mode 100644
index 000000000..f9328d95d
Binary files /dev/null and b/docs/plans/wallet-ui-3/n01-create-password.png differ
diff --git a/docs/plans/wallet-ui-3/n02-create-words.png b/docs/plans/wallet-ui-3/n02-create-words.png
new file mode 100644
index 000000000..9bb1fceb7
Binary files /dev/null and b/docs/plans/wallet-ui-3/n02-create-words.png differ
diff --git a/docs/plans/wallet-ui-3/n03-import.png b/docs/plans/wallet-ui-3/n03-import.png
new file mode 100644
index 000000000..f0eeb3b50
Binary files /dev/null and b/docs/plans/wallet-ui-3/n03-import.png differ
diff --git a/docs/plans/wallet-ui-3/n04-unlock-touch.png b/docs/plans/wallet-ui-3/n04-unlock-touch.png
new file mode 100644
index 000000000..269087870
Binary files /dev/null and b/docs/plans/wallet-ui-3/n04-unlock-touch.png differ
diff --git a/docs/plans/wallet-ui-3/n05-unlock-password.png b/docs/plans/wallet-ui-3/n05-unlock-password.png
new file mode 100644
index 000000000..ea6d820e3
Binary files /dev/null and b/docs/plans/wallet-ui-3/n05-unlock-password.png differ
diff --git a/docs/plans/wallet-ui-3/n06-home.png b/docs/plans/wallet-ui-3/n06-home.png
new file mode 100644
index 000000000..13a6b6009
Binary files /dev/null and b/docs/plans/wallet-ui-3/n06-home.png differ
diff --git a/docs/plans/wallet-ui-3/n07-home-lower.png b/docs/plans/wallet-ui-3/n07-home-lower.png
new file mode 100644
index 000000000..68e5e0747
Binary files /dev/null and b/docs/plans/wallet-ui-3/n07-home-lower.png differ
diff --git a/docs/plans/wallet-ui-3/n08-send.png b/docs/plans/wallet-ui-3/n08-send.png
new file mode 100644
index 000000000..b17b4a6e9
Binary files /dev/null and b/docs/plans/wallet-ui-3/n08-send.png differ
diff --git a/docs/plans/wallet-ui-3/n09-send-review.png b/docs/plans/wallet-ui-3/n09-send-review.png
new file mode 100644
index 000000000..b8b08bbd1
Binary files /dev/null and b/docs/plans/wallet-ui-3/n09-send-review.png differ
diff --git a/docs/plans/wallet-ui-3/n10-send-sent.png b/docs/plans/wallet-ui-3/n10-send-sent.png
new file mode 100644
index 000000000..5e6648a26
Binary files /dev/null and b/docs/plans/wallet-ui-3/n10-send-sent.png differ
diff --git a/docs/plans/wallet-ui-3/n11-receive.png b/docs/plans/wallet-ui-3/n11-receive.png
new file mode 100644
index 000000000..703724acf
Binary files /dev/null and b/docs/plans/wallet-ui-3/n11-receive.png differ
diff --git a/docs/plans/wallet-ui-3/n12-tx-final.png b/docs/plans/wallet-ui-3/n12-tx-final.png
new file mode 100644
index 000000000..772513272
Binary files /dev/null and b/docs/plans/wallet-ui-3/n12-tx-final.png differ
diff --git a/docs/plans/wallet-ui-3/n13-settings.png b/docs/plans/wallet-ui-3/n13-settings.png
new file mode 100644
index 000000000..27c3f317d
Binary files /dev/null and b/docs/plans/wallet-ui-3/n13-settings.png differ
diff --git a/docs/plans/wallet-ui-3/n14-settings-lower.png b/docs/plans/wallet-ui-3/n14-settings-lower.png
new file mode 100644
index 000000000..6065c9a7d
Binary files /dev/null and b/docs/plans/wallet-ui-3/n14-settings-lower.png differ
diff --git a/docs/plans/wallet-ui-3/n15-settings-lowest.png b/docs/plans/wallet-ui-3/n15-settings-lowest.png
new file mode 100644
index 000000000..df19a8501
Binary files /dev/null and b/docs/plans/wallet-ui-3/n15-settings-lowest.png differ
diff --git a/docs/plans/wallet-ui-3/n16-home-public.png b/docs/plans/wallet-ui-3/n16-home-public.png
new file mode 100644
index 000000000..72fd0a801
Binary files /dev/null and b/docs/plans/wallet-ui-3/n16-home-public.png differ
diff --git a/docs/plans/wallet-ui-3/n17-home-scanning.png b/docs/plans/wallet-ui-3/n17-home-scanning.png
new file mode 100644
index 000000000..624639246
Binary files /dev/null and b/docs/plans/wallet-ui-3/n17-home-scanning.png differ
diff --git a/docs/plans/wallet-ui-3/n18-home-nonode.png b/docs/plans/wallet-ui-3/n18-home-nonode.png
new file mode 100644
index 000000000..0b814e41f
Binary files /dev/null and b/docs/plans/wallet-ui-3/n18-home-nonode.png differ
diff --git a/docs/plans/wallet-ui-3/n19-home-empty.png b/docs/plans/wallet-ui-3/n19-home-empty.png
new file mode 100644
index 000000000..6451672be
Binary files /dev/null and b/docs/plans/wallet-ui-3/n19-home-empty.png differ
diff --git a/docs/plans/wallet-ui-3/n20-update-card.png b/docs/plans/wallet-ui-3/n20-update-card.png
new file mode 100644
index 000000000..8815fb586
Binary files /dev/null and b/docs/plans/wallet-ui-3/n20-update-card.png differ
diff --git a/docs/plans/wallet-ui-3/n21-narrow-900-home.png b/docs/plans/wallet-ui-3/n21-narrow-900-home.png
new file mode 100644
index 000000000..75ad21af7
Binary files /dev/null and b/docs/plans/wallet-ui-3/n21-narrow-900-home.png differ
diff --git a/docs/plans/wallet-ui-3/n22-phone-home.png b/docs/plans/wallet-ui-3/n22-phone-home.png
new file mode 100644
index 000000000..fe9f4c0d4
Binary files /dev/null and b/docs/plans/wallet-ui-3/n22-phone-home.png differ
diff --git a/docs/plans/wallet-ui-3/n23-phone-settings.png b/docs/plans/wallet-ui-3/n23-phone-settings.png
new file mode 100644
index 000000000..378529716
Binary files /dev/null and b/docs/plans/wallet-ui-3/n23-phone-settings.png differ
diff --git a/docs/plans/wallet-ui-3/n24-light-home.png b/docs/plans/wallet-ui-3/n24-light-home.png
new file mode 100644
index 000000000..4fa03d539
Binary files /dev/null and b/docs/plans/wallet-ui-3/n24-light-home.png differ
diff --git a/site/img/wallet-final.webp b/site/img/wallet-final.webp
index 3e99f50f0..166f4c25a 100644
Binary files a/site/img/wallet-final.webp and b/site/img/wallet-final.webp differ
diff --git a/site/img/wallet-home.webp b/site/img/wallet-home.webp
index aed154d64..eaae1dc5a 100644
Binary files a/site/img/wallet-home.webp and b/site/img/wallet-home.webp differ
diff --git a/site/index.html b/site/index.html
index cba189ea0..3d25f10b4 100644
--- a/site/index.html
+++ b/site/index.html
@@ -500,8 +500,8 @@ pre{margin:0;font-family:var(--f-mono);font-size:13px;line-height:1.6;color:var(
Tunes itself, in publicKernel variants raced every hour, hash per watt swept, every number in the bench table.
final · cp 5under a checkpoint this wallet verified
+
executedthe node's own word, never a stronger one
+
finalisedunder a checkpoint this wallet verified
Sealed on your machine24 words, three typed back, a password. Argon2id and XChaCha20-Poly1305. Nothing leaves.
-
Rewards in one historyBlock rewards, proving payouts and transfers. Send with the fee shown. Receive by a QR drawn locally.
+
Rewards in one historyBlock rewards, proving payouts and transfers, each with the node's state word. Send confirmed in place with the fee shown. Receive by a QR drawn locally.
Reads your own nodeWhen the miner is installed, the wallet uses its node. Nobody else sees your balance.
-
- Igneum Wallet on a private test network, 5 Oct 2026. The address is an example.
+
+ Igneum Wallet 0.1.5 on the devnet, 6 Oct 2026. The address is an example.