From a267f7f37fd178ebb58871188067b571b18a67f1 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sun, 28 Jun 2026 14:25:09 -0700 Subject: Gifts: re-view link/QR for unclaimed gifts + mark claimed ones in history MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Persist a per-account gift record (shareable link/code + amount, plus a locked gift's claim code) when a gift is created, so: - Unclaimed gifts in History get a 'Gift link' button that re-opens the QR + link (and the claim code for locked gifts), to re-share without recreating the gift. - Once a gift's coin is spent and we didn't revoke it, it shows in History as 'claimed' instead of vanishing. Revoking marks the record so a reclaim isn't mislabelled as claimed. Verified on regtest: create → outstanding=1, link stored; after the recipient claims → outstanding=0, claimed=1 with the right amount. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/wallet.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) (limited to 'src/wallet.js') diff --git a/src/wallet.js b/src/wallet.js index 6e3ea81..4a8ddc4 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -1549,6 +1549,32 @@ export class Wallet { })); } + // Persistent gift records (per account): the shareable link/code + amount, so a + // gift's link/QR can be re-viewed later and a claimed one identified in history. + _giftRecordsKey() { return this._cacheKey() + ':giftrec'; } + giftRecords() { + if (!this._giftRecords) { try { this._giftRecords = JSON.parse(localStorage.getItem(this._giftRecordsKey()) || '{}'); } catch { this._giftRecords = {}; } } + return this._giftRecords; + } + _saveGiftRecords() { try { localStorage.setItem(this._giftRecordsKey(), JSON.stringify(this.giftRecords())); } catch {} } + recordGift({ code, locked, amount, claimCode, outpoints }) { + if (!outpoints || !outpoints.length) return; + this.giftRecords()[outpoints[0]] = { id: outpoints[0], code, locked: !!locked, amount, claimCode: claimCode || null, outpoints, created: Date.now(), revoked: false }; + this._saveGiftRecords(); + } + giftLink(id) { return this.giftRecords()[id] || null; } + markGiftRevoked(id) { const r = this.giftRecords()[id]; if (r) { r.revoked = true; this._saveGiftRecords(); } } + // Gifts whose coin has been spent and weren't revoked by us → claimed. (Only + // once scanned, so an un-synced wallet doesn't mislabel everything as claimed.) + claimedGifts() { + if (!this.loaded) return []; + const live = new Set(this.utxos.map((u) => utxoId(u))); + return Object.values(this.giftRecords()) + .filter((r) => !r.revoked && r.outpoints && !r.outpoints.some((o) => live.has(o))) + .map((r) => ({ id: r.id, amount: r.amount, locked: r.locked, created: r.created })) + .sort((a, b) => (b.created || 0) - (a.created || 0)); + } + // Best-fit coin selection for a gift of `gift` sats (needs gift + a dust // change): the smallest single confirmed coin that covers it — one input and // the least value locked until claim — else the fewest large coins. Returns -- cgit v1.2.3