From dae260d1a2cfb866d7b0f5c5620d5e0392bd4723 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Thu, 25 Jun 2026 16:50:21 -0700 Subject: Compact gift links: base32 codec + uppercase /g/ path for a smaller QR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gift links now carry a compact binary encoding of just the claim-essential data (presigned inputs + the committed change output) instead of a full PSBT (~25% fewer bytes), base32-encoded and served at an uppercase /g/ path. The QR renders the uppercase URL in the QR alphanumeric mode (5.5 bits/char vs byte mode's 8), so the grid drops from ~73 to ~53 modules — much easier to scan. - wallet: _encodeCompactGift/_decodeCompactGift + _giftTx dispatcher; legacy base64url-PSBT links still claim (auto-detected by a magic byte). Verified round-trip + claim-byte-identical across 1/2/3-input and whole-balance gifts, against the real exported preview/claim/outpoint functions. - app: giftUrl uses /g/; opener parses /g/ (and legacy #gift=); QR uses uppercase + Alphanumeric mode (qrSvg gains a mode option w/ byte fallback). - nginx already SPA-routes /g/*; the SW serves the shell for the new path. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/app.js | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) (limited to 'src/app.js') diff --git a/src/app.js b/src/app.js index fdc5b3f..708a56e 100644 --- a/src/app.js +++ b/src/app.js @@ -1081,7 +1081,7 @@ function pubkeyCard() { // Gift link: presign a chosen amount as a #gift= PSBT that whoever opens claims // into a fresh wallet only they control. The coin is reserved until claimed. function giftUrl() { - return `${location.origin}/#gift=${ui.giftCode}`; + return `${location.origin}/g/${ui.giftCode}`; } function giftRate() { return (wallet.feeRates && wallet.feeRates.halfHourFee) || 5; @@ -1170,7 +1170,10 @@ function giftCard() { h('div', { class: 'warn-box' }, t('giftLinkWarn')), ui.giftCode ? h('div', { class: 'col', style: 'align-items:center;gap:10px' }, - h('div', { html: qrSvg(giftUrl(), { ec: 'L' }) }), + // Uppercase the whole URL + alphanumeric mode → much smaller QR. The + // domain is case-insensitive and the base32 code is already uppercase; + // the opener parses /g/ and the code case-insensitively. + h('div', { html: qrSvg(giftUrl().toUpperCase(), { ec: 'L', mode: 'Alphanumeric' }) }), h('div', { class: 'addr-box break', style: 'width:100%;font-size:12px' }, giftUrl()), h('div', { class: 'row gap6 wrap' }, copyBtn(giftUrl(), t('copyLink')), @@ -1393,15 +1396,22 @@ function goHome() { } // ================================================================ GIFT / CLAIM -// Read a #gift= link, returning the code (and scrubbing it from the URL so -// the bearer instrument doesn't linger in the address bar / history). Validates -// that it decodes to a real gift. +// Read a gift link, returning the code (and scrubbing it from the URL so the +// bearer instrument doesn't linger in the address bar / history). New links are +// /g/; legacy links are #gift=. Both validate by +// decoding to a real gift. function readGiftHash() { try { - const m = location.hash.match(/^#gift=([A-Za-z0-9_-]+)$/); - if (!m) return null; - history.replaceState(null, '', location.pathname + location.search); - return previewGift(m[1]) ? m[1] : null; + let code = null; + const pm = location.pathname.match(/^\/g\/([A-Za-z0-9]+)\/?$/i); // new path form + if (pm) code = pm[1]; + else { + const hm = location.hash.match(/^#gift=([A-Za-z0-9_-]+)$/); // legacy hash form + if (hm) code = hm[1]; + } + if (!code) return null; + history.replaceState(null, '', '/'); + return previewGift(code) ? code : null; } catch { return null; } -- cgit v1.2.3