diff options
Diffstat (limited to 'src/features/ark.js')
| -rw-r--r-- | src/features/ark.js | 60 |
1 files changed, 58 insertions, 2 deletions
diff --git a/src/features/ark.js b/src/features/ark.js index c02b9f9..9b7cd65 100644 --- a/src/features/ark.js +++ b/src/features/ark.js @@ -151,6 +151,49 @@ export function installArkWallet(wallet) { // which it might spend/exit); the movement log for ark history; and // done board/offboard/exit actions (stripped of hex) so on-chain rows still // label. In-flight actions and their signing material stay device-local. +const RENEW_STEP_RANK = { created: 0, submitted: 1, issued: 2, done: 3, failed: 3 }; +// when a renewal began: its action id carries the moment (refresh-<ms>, +// refresh-rescue-<ms>) +const renewalStartedAt = (id) => Number(String(id).split('-').pop()) || 0; + +function renewalSliver(a) { + return { + id: a.id, step: a.step, superseded: !!a.superseded, manual: !!a.manual, + outAmountSat: a.outAmountSat, feeSat: a.feeSat || 0, + unlockHash: a.unlockHash || null, fundingTxid: a.fundingTxid || null, + }; +} + +// Other devices' renewals, by id: the furthest-along telling wins (a step +// never goes backwards), this device's own actions never count as remote, +// and anything begun more than two days ago is dropped — a renewal finishes +// within hours, and a device that stopped publishing mustn't leave a card +// behind forever. +export function mergeRemoteRenewals(a, b, now = Date.now()) { + const own = new Set((a.actions || []).map((x) => x.id)); + // b's own renewals are remote to us; a.renewals only exists when a was + // itself adopted whole from a snapshot + const byId = new Map(); + for (const r of [...(a.remoteRenewals || []), ...(a.renewals || []), ...(b.remoteRenewals || []), ...(b.renewals || [])]) { + if (!r || !r.id || own.has(r.id)) continue; + if (now - renewalStartedAt(r.id) > 2 * 86_400_000) continue; + const cur = byId.get(r.id); + if (!cur || (RENEW_STEP_RANK[r.step] ?? 0) > (RENEW_STEP_RANK[cur.step] ?? 0) + || (!cur.fundingTxid && r.fundingTxid && r.step === cur.step)) byId.set(r.id, { ...r }); + } + return [...byId.values()].sort((x, y) => renewalStartedAt(x.id) - renewalStartedAt(y.id)); +} + +// Renewals another device has in flight, as this device should show them: +// not finished there, and not already finished as far as this device's +// history knows (the "Renewed" row synced ahead of the final telling). +export function remoteRenewalsInFlight(state) { + const settled = new Set((state.movements || []) + .filter((m) => m.type === 'refresh' && m.status === 'complete' && m.unlockHash).map((m) => m.unlockHash)); + return (state.remoteRenewals || []).filter((r) => + !['done', 'failed'].includes(r.step) && !(r.unlockHash && settled.has(r.unlockHash))); +} + export function slimArkForSync(s) { if (!s) return null; const HEAVY = ['bytes', 'destBytes', 'changeBytes', 'vtxoBytes', 'txHex', 'fundingTxHex', 'outputVtxos']; @@ -183,6 +226,12 @@ export function slimArkForSync(s) { })); return [...core, ...spends]; })(), + // This device's own renewals, for DISPLAY elsewhere only — a phone that + // knew nothing showed no "Renewal in progress" while the laptop's round + // ran and the balance sat locked. No signing material: nothing but the + // device that started a renewal can ever drive it. Terminal ones ride + // too, so the finished telling replaces the in-progress one. + renewals: (s.actions || []).filter((a) => a.type === 'refresh').map(renewalSliver), movements: (s.movements || []).map((m, i, arr) => // Older rows only need to render a history line; the bolt11 (~400 chars // each) and preimage are what pushed snapshots past relay size limits. @@ -288,6 +337,8 @@ export function mergeArkStates(a, b) { // cursor with its coins/history, just as the !a case above does, rather // than replaying the entire mailbox on a first login. out.mailboxCheckpoint = pristine ? b.mailboxCheckpoint || 0 : a.mailboxCheckpoint || 0; + out.remoteRenewals = mergeRemoteRenewals(a, b); + delete out.renewals; // a snapshot's own list, adopted above as remote out.nextKeyIndex = Math.max(a.nextKeyIndex || 1, b.nextKeyIndex || 1); out.nextLnRecvIndex = Math.max(a.nextLnRecvIndex || 0, b.nextLnRecvIndex || 0); out.receiveAckTs = Math.max(a.receiveAckTs || 0, b.receiveAckTs || 0); @@ -2337,12 +2388,17 @@ export function arkFeature(ctx) { } if (ui.arkRenewConfirm) ui.arkRenewConfirm = null; // nothing selected any more const stageOf = (a) => a.step === 'issued' ? t('arkRenewStageFinishing') : a.step === 'submitted' ? t('arkRenewStageRound') : t('arkRenewStageQueued'); - const renewingCard = renewing.length ? h('div', { class: 'card col', style: 'gap:8px' }, + // another device's renewals, shown but never driven from here (it holds + // their signing material); they lock the balance just the same + const elsewhere = st ? remoteRenewalsInFlight(st) : []; + const renewingRows = [...renewing, ...elsewhere.map((r) => ({ ...r, elsewhere: true }))]; + const renewingCard = renewingRows.length ? h('div', { class: 'card col', style: 'gap:8px' }, h('div', { class: 'row gap6', style: 'align-items:center' }, h('h4', { style: 'margin:0' }, t('arkRenewingTitle')), h('span', { class: 'tag pending' }, t('arkExitPendingTag'))), - ...renewing.map((a) => h('div', { class: 'col', style: 'gap:2px' }, + ...renewingRows.map((a) => h('div', { class: 'col', style: 'gap:2px' }, h('div', { class: 'small' }, t('arkRenewingLine', { amount: fmtSats(a.inAmountSat || (a.outAmountSat + (a.feeSat || 0))), fee: a.feeSat ? t('arkRenewingFee', { fee: fmtSats(a.feeSat) }) : '' })), h('div', { class: 'small muted' }, stageOf(a)), + a.elsewhere ? h('div', { class: 'small faint' }, t('arkRenewElsewhere')) : null, // the round's transaction, once the round has run: the thing the // wait is for, there to be looked at a.fundingTxid ? (() => { |
