From aa044138337c947b26eea910db57f2d5b743b5b8 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Thu, 25 Jun 2026 20:41:20 -0700 Subject: Add Silent Payments (BIP-352) sending MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pay sp1… addresses: the wallet derives the one-time taproot output from the recipient's address + the transaction's own input keys (ECDH), so payments are unlinkable on-chain with no interaction. New src/silentpay.js implements the sender derivation + bech32m address decode, validated against the official BIP-352 test vectors (all P2WPKH-input / single-recipient cases pass; the rest exercise input types Hal never spends). buildTx detects sp1… recipients, selects coins against a same-size placeholder taproot output, then swaps in the script derived from the final input set (updateOutput). Works for normal sends, multi-recipient, and send-max. The review screen shows the sp1… address with a 'Silent payment' label (the on-chain output is a derived one-time taproot address). End-to-end signing verified. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/app.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) (limited to 'src/app.js') diff --git a/src/app.js b/src/app.js index 21aea3d..d7fbc4e 100644 --- a/src/app.js +++ b/src/app.js @@ -9,6 +9,7 @@ import { qrSvg } from './qr.js'; import { scanQr } from './scan.js'; import { getSyncConfig, setSyncConfig } from './nostr.js'; import { getExplorerConfig, setExplorerConfig, EXPLORER_PRESETS, getRealtimeEnabled, setRealtimeEnabled } from './api.js'; +import { isSilentPaymentAddress } from './silentpay.js'; import { t, LANGS, getLang, setLang, isRTL, loadLocale } from './i18n.js'; import { fmtBtc, @@ -2155,7 +2156,9 @@ function recipientRow(s, r, i) { const check = h('div', { class: 'addr-check' }); const syncCheck = () => { const a = r.address.trim(); - check.replaceChildren(...addrVerifyNodes(a)); + const nodes = addrVerifyNodes(a); + if (a && isSilentPaymentAddress(a)) nodes.push(h('span', { class: 'small faint', style: 'margin-left:6px' }, '· ' + t('silentPaymentNote'))); + check.replaceChildren(...nodes); check.style.display = a ? '' : 'none'; }; @@ -2382,7 +2385,12 @@ function reviewView() { { class: 'summary col', style: 'gap:0' }, ...outs.map((o) => h('div', { class: 'line' }, - h('span', { class: 'k mono break' }, shortAddr(o.address, 14, 8)), + o.silent + ? h('span', { class: 'k col', style: 'gap:2px;align-items:flex-start' }, + h('span', { class: 'mono break' }, shortAddr(o.silent, 12, 8)), + h('span', { class: 'small faint' }, t('silentPaymentNote')) + ) + : h('span', { class: 'k mono break' }, shortAddr(o.address, 14, 8)), h('span', { class: 'v' }, fmtAmount(o.amount), ' ', unitTag()) ) ), -- cgit v1.2.3