diff options
| author | Adam Soltys <[email protected]> | 2026-08-01 23:07:02 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-08-01 23:07:02 -0700 |
| commit | 83ad00a39863bb6a61e15977202fe2feea2ca696 (patch) | |
| tree | 251325b582093cdcaf08ba52981ee9905e018e9c /src | |
| parent | 88d93de66b9ba713c5cc1a8fc5b1528ea1d74fb8 (diff) | |
| download | coinosv3-83ad00a39863bb6a61e15977202fe2feea2ca696.tar.gz coinosv3-83ad00a39863bb6a61e15977202fe2feea2ca696.zip | |
Drop npub-locked gifts; collapse Nostr login behind a button
Gifts are plain bearer links again: the recipient-npub field, the
one-time claim code, the DM delivery, the /lg/ claim route and the
NIP-07 in-place decrypt are all gone, along with the crypto that only
they used. Unclaimed gifts remain reclaimable by the sender, so nothing
can be stranded by the removal.
The unlock screen shows a single 'Sign in with Nostr' button that
reveals the login options when tapped, instead of the full card
competing with create/import.
Co-Authored-By: Claude Fable 5 <[email protected]>
Diffstat (limited to 'src')
| -rw-r--r-- | src/app.js | 5 | ||||
| -rw-r--r-- | src/features/gifts-wallet.js | 18 | ||||
| -rw-r--r-- | src/features/gifts.js | 157 | ||||
| -rw-r--r-- | src/features/nostrlogin.js | 14 | ||||
| -rw-r--r-- | src/i18n.js | 19 | ||||
| -rw-r--r-- | src/nostr.js | 21 |
6 files changed, 26 insertions, 208 deletions
@@ -76,12 +76,7 @@ const ui = { pubkeyShown: false, // account public key revealed in Settings giftMode: false, // gift sub-view active on the Send page giftAmount: '', // gift-create amount input - giftLockNpub: '', // optional: lock the gift to this recipient nostr npub - giftLocked: false, // the just-created gift is locked to a nostr key giftClaimCode: null, // the one-time claim code DM'd to the recipient (fallback) - giftDmStatus: null, // 'sending' | 'sent' | 'failed' — nostr DM of the claim code - claimLocked: null, // a locked gift being opened: { v, amount, to, ct } - claimCodeInput: '', // the claim code the recipient pastes from their nostr DM viewGift: null, // re-viewing a previously created gift's link/QR { code, locked, amount, claimCode } claimChoose: null, // opening a gift with existing wallets present: { code } — pick a target giftCode: null, // last-created gift PSBT code diff --git a/src/features/gifts-wallet.js b/src/features/gifts-wallet.js index 96e4978..93ef268 100644 --- a/src/features/gifts-wallet.js +++ b/src/features/gifts-wallet.js @@ -6,7 +6,6 @@ import * as btc from '@scure/btc-signer'; import { hex, base64urlnopad, base32nopad } from '@scure/base'; import { utxoId } from '../wallet.js'; -import { encryptGiftPayload } from '../nostr.js'; import { p2wpkh } from '@scure/btc-signer/payment'; import { concatBytes } from '@noble/hashes/utils'; @@ -447,20 +446,3 @@ export function buildClaimTx(code, toAddress, feeRate, net) { // public (anyone can see what it is and who it's for), but the claim payload (the // gift code) is NIP-44-encrypted to the recipient via an ephemeral key — so the // link can be shared openly yet only the recipient's nostr key can claim it. -const LOCKED_GIFT_VERSION = 1; -// Encrypt the gift's claim payload under a one-time code. The code is delivered -// to the recipient via a nostr DM; the public link carries only the ciphertext. -// Returns { blob (for the /lg/ link), claimCode (to DM) }. -export function lockGift(code, amount, recipientPkHex) { - const { code: claimCode, ctCode, eph, ctKey } = encryptGiftPayload(code, recipientPkHex); - const blob = base64urlnopad.encode(new TextEncoder().encode(JSON.stringify({ v: LOCKED_GIFT_VERSION, amount, to: recipientPkHex, ct: ctCode, eph, ctKey }))); - return { blob, claimCode }; -} -// The public fields of a locked-gift blob (or null). Decryption needs the code. -export function previewLockedGift(blob) { - try { - const o = JSON.parse(new TextDecoder().decode(base64urlnopad.decode(blob))); - if (o.v === LOCKED_GIFT_VERSION && typeof o.amount === 'number' && o.to && o.ct) return o; - } catch {} - return null; -} diff --git a/src/features/gifts.js b/src/features/gifts.js index 86bc3b6..740b9f8 100644 --- a/src/features/gifts.js +++ b/src/features/gifts.js @@ -4,8 +4,7 @@ import { newMnemonic } from '../wallet.js'; import { getNetwork } from '../api.js'; -import { installGiftWallet, previewGift, giftOutpoints, buildClaimTx, giftMinimum, lockGift, previewLockedGift } from './gifts-wallet.js'; -import { parseNostrPubkey, npubOf, fetchNostrProfile, decryptWithCode } from '../nostr.js'; +import { installGiftWallet, previewGift, giftOutpoints, buildClaimTx, giftMinimum } from './gifts-wallet.js'; import { t } from '../i18n.js'; import { qrSvg } from '../qr.js'; import { fmtBtc, timeAgo, shortAddr } from '../format.js'; @@ -50,25 +49,10 @@ export function giftsFeature(ctx) { } } - // A gift locked to a nostr key: /lg/<base64url>. Returns the public preview - // ({ amount, to, eph, ct }) — claimed by the recipient's own wallet, not a fresh - // one, so we don't auto-create a wallet here. - function readLockedGiftHash() { - try { - const m = location.pathname.match(/^\/lg\/([A-Za-z0-9_-]+)\/?$/); - if (!m) return null; - history.replaceState(null, '', '/'); - return previewLockedGift(m[1]); - } catch { - return null; - } - } - // Entry point for claiming a gift code. If the recipient already has wallet(s), // offer to claim into one or make a new one; otherwise go straight to a fresh // wallet (the original first-timer flow). function claimGift(code) { - ui.claimLocked = null; ui.claimArkAmount = null; ui.claimAuto = false; // An ark gift can only land in a wallet on ITS network — other wallets @@ -394,68 +378,13 @@ export function giftsFeature(ctx) { render(); } - // A locked gift opened from a link: the amount + recipient are public. The - // recipient pastes the claim code from their nostr DM; that decrypts the payload, - // then it's claimed into a fresh wallet exactly like a normal gift. - function lockedGiftClaimView() { - const lk = ui.claimLocked; - // NIP-07: if a nostr browser extension is present, the recipient can decrypt - // in-place — no code needed. - const hasExt = !!(typeof globalThis !== 'undefined' && globalThis.nostr && lk.eph && lk.ctKey); - return h('div', { class: 'col', style: 'gap:16px;padding:16px;max-width:460px;margin:0 auto;width:100%' }, - h('div', { class: 'card col', style: 'gap:14px;align-items:center' }, - h('h3', { style: 'margin:0' }, t('giftForYou')), - h('div', { class: 'amt' }, fmtAmount(lk.amount), ' ', unitTag()), - h('div', { class: 'row gap6', style: 'align-items:center' }, - h('span', { class: 'small muted' }, t('giftLockedTo')), - profileChip(lk.to, { size: 36 })), - ui.claimError && h('div', { class: 'notice err' }, ui.claimError), - hasExt ? h('button', { class: 'btn-primary btn-block', disabled: ui.busy, onClick: claimViaExtension }, ui.busy ? h('span', { class: 'spinner' }) : t('claimWithExtension')) : null, - hasExt ? h('div', { class: 'small faint', style: 'text-align:center' }, t('orEnterCode')) : h('p', { class: 'small muted', style: 'text-align:center;margin:0' }, t('giftCodeHint')), - h('input', { type: 'text', class: 'mono-input', style: 'width:100%', placeholder: t('claimCodePlaceholder'), - autocapitalize: 'none', autocomplete: 'off', spellcheck: 'false', value: ui.claimCodeInput, - onInput: (e) => { ui.claimCodeInput = e.target.value; } }), - h('button', { class: (hasExt ? '' : 'btn-primary ') + 'btn-block', onClick: submitLockedCode }, t('claimGift')), - h('button', { class: 'btn-ghost btn-block', onClick: () => { ui.claimLocked = null; ui.claimCodeInput = ''; ui.claimError = ''; render(); } }, t('dismiss')) - ) - ); - } - - async function claimViaExtension() { - const ext = typeof globalThis !== 'undefined' && globalThis.nostr; - if (!ext) return; - ui.busy = true; ui.claimError = ''; render(); - try { - const pk = await ext.getPublicKey(); - if (pk !== ui.claimLocked.to) { ui.claimError = t('extWrongAccount'); ui.busy = false; render(); return; } - if (!ext.nip44 || !ext.nip44.decrypt) { ui.claimError = t('extNoNip44'); ui.busy = false; render(); return; } - const payload = await ext.nip44.decrypt(ui.claimLocked.eph, ui.claimLocked.ctKey); - if (!payload || !previewGift(payload)) { ui.claimError = t('extDecryptFailed'); ui.busy = false; render(); return; } - ui.busy = false; ui.claimLocked = null; ui.claimCodeInput = ''; ui.claimError = ''; - claimGift(payload); // claim into an existing wallet or a fresh one - } catch { - ui.busy = false; ui.claimError = t('extDecryptFailed'); render(); - } - } - - function submitLockedCode() { - const code = (ui.claimCodeInput || '').trim(); - if (!code) return; - let payload = null; - try { payload = decryptWithCode(code, ui.claimLocked.ct); } catch {} - if (!payload || !previewGift(payload)) { ui.claimError = t('claimCodeWrong'); render(); return; } - ui.claimLocked = null; ui.claimCodeInput = ''; ui.claimError = ''; - claimGift(payload); // claim into an existing wallet or a fresh one - } - - // Re-view a previously created gift's link + QR (from its stored record), so the - // sender can re-share an unclaimed gift. For a locked gift, also surfaces the - // claim code that was DM'd, in case it needs re-sending. + // Re-view a previously created gift's link + QR (from its stored record), so + // the sender can re-share an unclaimed gift. function viewGiftView() { const g = ui.viewGift; // { code, locked, amount, claimCode } - const url = `${location.origin}/${g.locked ? 'lg' : 'g'}/${g.code}`; + const url = `${location.origin}/g/${g.code}`; let svg = null; - try { svg = g.locked ? qrSvg(url) : qrSvg(url.toUpperCase(), { ec: 'L', mode: 'Alphanumeric' }); } catch {} + try { svg = qrSvg(url.toUpperCase(), { ec: 'L', mode: 'Alphanumeric' }); } catch {} return h('div', { class: 'col', style: 'gap:16px;padding:16px;max-width:460px;margin:0 auto;width:100%' }, h('div', { class: 'card col', style: 'gap:12px;align-items:center' }, h('h3', { style: 'margin:0' }, t('giftView')), @@ -525,27 +454,11 @@ export function giftsFeature(ctx) { doCreateGift(amt, rate); } - // Finalize a freshly-built gift. With a recipient npub it's a locked gift: the - // claim payload is encrypted under a one-time code, the link carries only the - // ciphertext, and the code is DM'd to the recipient over nostr. Without, it's a - // plain bearer gift. + // Finalize a freshly-built gift: a plain bearer link. function finishGift(g, fallbackAmt) { const amount = g.amount || fallbackAmt || 0; - const pk = ui.giftLockNpub.trim() ? parseNostrPubkey(ui.giftLockNpub) : null; - if (!pk) { - ui.giftCode = g.code; ui.giftLocked = false; ui.giftClaimCode = null; ui.giftDmStatus = null; - wallet.recordGift({ code: g.code, locked: false, amount, outpoints: g.reserved }); - return; - } - const { blob, claimCode } = lockGift(g.code, amount, pk); - ui.giftCode = blob; ui.giftLocked = true; ui.giftClaimCode = claimCode; ui.giftDmStatus = 'sending'; - wallet.recordGift({ code: blob, locked: true, amount, claimCode, outpoints: g.reserved }); - const dmText = t('giftDmText', { amount: fmtAmount(amount) + ' ' + unitLabel(), link: giftUrl(), code: claimCode }); - if (wallet.sendNostrDM) { - wallet.sendNostrDM(pk, dmText).then((ok) => { ui.giftDmStatus = ok ? 'sent' : 'failed'; render(); }).catch(() => { ui.giftDmStatus = 'failed'; render(); }); - } else { - ui.giftDmStatus = 'failed'; // no sync/nostr feature in this build — the claim code fallback shows - } + ui.giftCode = g.code; + wallet.recordGift({ code: g.code, locked: false, amount, outpoints: g.reserved }); } function giftCard() { @@ -556,28 +469,18 @@ export function giftsFeature(ctx) { h('h3', {}, t('giftLink')), ui.giftCode ? h('div', { class: 'col', style: 'align-items:center;gap:10px' }, - ui.giftLocked && (() => { - const lk = previewLockedGift(ui.giftCode); - if (!lk) return null; - return h('div', { class: 'col gap6', style: 'width:100%;align-items:center' }, - h('div', { class: 'row gap6', style: 'align-items:center' }, h('span', { class: 'small muted' }, t('giftLockedTo')), profileChip(lk.to)), - ui.giftDmStatus === 'sent' ? h('div', { class: 'small', style: 'color:var(--ok,#2a8)' }, t('giftDmSent')) - : ui.giftDmStatus === 'failed' ? h('div', { class: 'col gap6', style: 'align-items:center' }, h('div', { class: 'small err' }, t('giftDmFailed')), ui.giftClaimCode ? copyBtn(ui.giftClaimCode, t('giftCopyCode')) : null) - : h('div', { class: 'row gap6', style: 'align-items:center' }, h('span', { class: 'spinner sm' }), h('span', { class: 'small muted' }, t('giftDmSending')))); - })(), - // A bearer gift uppercases the URL for a smaller alphanumeric QR (the // base32 code is case-insensitive). A locked gift's payload is // case-sensitive base64url, so it uses a plain byte-mode QR. A large // (many-input) gift can exceed QR capacity — then fall back to the link. (() => { let svg = null; - try { svg = ui.giftLocked ? qrSvg(giftUrl()) : qrSvg(giftUrl().toUpperCase(), { ec: 'L', mode: 'Alphanumeric' }); } catch {} + try { svg = qrSvg(giftUrl().toUpperCase(), { ec: 'L', mode: 'Alphanumeric' }); } catch {} return svg ? h('div', { html: svg }) : h('div', { class: 'small faint', style: 'text-align:center;padding:8px' }, t('giftQrTooLong')); })(), h('div', { class: 'addr-box break', style: 'width:100%;font-size:12px' }, giftUrl()), h('div', { class: 'row gap6 wrap' }, copyBtn(giftUrl(), t('copyLink')), - h('button', { class: 'btn-sm grow', onClick: () => { ui.giftCode = null; ui.giftAmount = ''; ui.giftMax = false; ui.giftLockNpub = ''; ui.giftLocked = false; ui.giftClaimCode = null; ui.giftDmStatus = null; ui.giftIsArk = false; render(); } }, t('giftAnother')) + h('button', { class: 'btn-sm grow', onClick: () => { ui.giftCode = null; ui.giftAmount = ''; ui.giftMax = false; ui.giftIsArk = false; render(); } }, t('giftAnother')) ) ) : ui.giftSplitOffer @@ -629,17 +532,6 @@ export function giftsFeature(ctx) { ui.giftSource === 'ark' ? t('giftArkNote') : ui.giftMax ? t('giftAllNote') : t('giftMinNote', { n: fmtAmount(giftMinimum(giftRate())) + ' ' + unitLabel() })), - (() => { - const raw = ui.giftLockNpub.trim(); - const pk = raw ? parseNostrPubkey(raw) : null; - return h('div', { class: 'col gap6' }, - h('input', { type: 'text', class: 'mono-input', placeholder: t('giftLockPlaceholder'), - autocapitalize: 'none', autocomplete: 'off', spellcheck: 'false', value: ui.giftLockNpub, - onInput: (e) => { ui.giftLockNpub = e.target.value; render(); } }), - !raw ? h('div', { class: 'small faint' }, t('giftLockHint')) - : pk ? h('div', { class: 'row gap6', style: 'align-items:center' }, h('span', { class: 'small muted' }, t('giftLockTo')), profileChip(pk)) - : h('div', { class: 'small err' }, t('giftLockInvalid'))); - })(), ui.giftError && h('div', { class: 'notice err' }, ui.giftError), ui.busy ? h('button', { class: 'btn-block', disabled: true }, h('span', { class: 'spinner sm' })) @@ -797,7 +689,7 @@ export function giftsFeature(ctx) { // 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}/${ui.giftLocked ? 'lg' : ui.giftIsArk ? 'ag' : 'g'}/${ui.giftCode}`; + return `${location.origin}/${ui.giftIsArk ? 'ag' : 'g'}/${ui.giftCode}`; } function giftRate() { @@ -850,31 +742,6 @@ export function giftsFeature(ctx) { render(); } - // --- nostr profiles (avatar + name) — cached, lazily fetched, re-renders ---- - const _profileCache = new Map(); // pubkeyHex -> profile | null | 'loading' - function nostrProfile(pkHex) { - if (_profileCache.has(pkHex)) return _profileCache.get(pkHex); - _profileCache.set(pkHex, 'loading'); - fetchNostrProfile(pkHex).then((p) => { _profileCache.set(pkHex, p || null); render(); }).catch(() => { _profileCache.set(pkHex, null); render(); }); - return 'loading'; - } - // A row showing the recipient's avatar + name (or a shortened npub fallback). - function profileChip(pkHex, { size = 30 } = {}) { - const p = nostrProfile(pkHex); - const npub = npubOf(pkHex) || pkHex; - const short = npub.slice(0, 12) + '…' + npub.slice(-4); - const avatar = (pic) => - pic - ? h('img', { src: pic, style: `width:${size}px;height:${size}px;border-radius:50%;object-fit:cover;flex:0 0 auto`, onError: (e) => { e.target.style.visibility = 'hidden'; } }) - : h('div', { style: `width:${size}px;height:${size}px;border-radius:50%;background:#9993;flex:0 0 auto` }); - if (p === 'loading') return h('div', { class: 'row gap6', style: 'align-items:center' }, h('span', { class: 'spinner sm' }), h('span', { class: 'small muted' }, short)); - const name = (p && p.name) || short; - return h('div', { class: 'row gap6', style: 'align-items:center;min-width:0' }, - avatar(p && p.picture), - h('span', { class: p && p.name ? '' : 'small muted', style: 'overflow:hidden;text-overflow:ellipsis;white-space:nowrap' }, name)); - } - - // "Payment received!" takeover right after a gift claim (no scan wait). function claimCelebration() { // A gift was just claimed into this wallet — celebrate immediately (the // background scan credits the coin and advances the address meanwhile). @@ -907,12 +774,10 @@ export function giftsFeature(ctx) { bootUrl() { const code = readGiftHash(); if (code) { claimGift(code); return true; } // bearer gift → claim into an existing wallet or a new one - ui.claimLocked = readLockedGiftHash(); // locked gift → claim with your own wallet return false; }, giftOpened(code) { checkGiftClaimed(code); }, screenView() { - if (ui.claimLocked) return lockedGiftClaimView(); if (ui.viewGift) return viewGiftView(); if (ui.claimChoose) return claimChooseView(); if (ui.screen === 'claim') return claimScreen(); diff --git a/src/features/nostrlogin.js b/src/features/nostrlogin.js index 7d4bdc1..d6b73e8 100644 --- a/src/features/nostrlogin.js +++ b/src/features/nostrlogin.js @@ -16,6 +16,9 @@ import { walletForSigner, publishWalletBackup, } from '../nostr-login.js'; +// The nostr ostrich, in the brand's monochrome. +const OSTRICH = '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" style="vertical-align:-3px"><path d="M6.6 2.2c1.5 0 2.8 1.2 2.8 2.8v.6h1.9c2.7 0 4.9 2.2 4.9 4.9 0 .7-.5 1.2-1.2 1.2h-2.3c-.5 0-.9.4-.9.9v1.1c0 1.6.5 3.1 1.4 4.4l1.3 1.8c.3.4 0 1-.5 1H9.2c-.4 0-.7-.3-.7-.7 0-2.6-.9-5.1-2.6-7.1A6.9 6.9 0 0 1 3.8 8.4V5c0-1.6 1.3-2.8 2.8-2.8Zm.2 2.1a.8.8 0 1 0 0 1.6.8.8 0 0 0 0-1.6Z"/></svg>'; + export function nostrLoginFeature(ctx) { const { h, ui, render, wallet, toast } = ctx; @@ -149,8 +152,17 @@ export function nostrLoginFeature(ctx) { h('button', { class: 'btn-primary grow', disabled: ui.nostrLoginBusy, onClick: createForSigner }, ui.nostrLoginBusy ? h('span', { class: 'spinner' }) : t('nlCreateBtn')))); } + // Collapsed by default: one button, expanded only when asked for. + if (!ui.nostrLoginOpen) { + return h('button', { + class: 'btn-block row gap6', style: 'align-items:center;justify-content:center', + onClick: () => { ui.nostrLoginOpen = true; ui.nostrLoginError = ''; render(); }, + }, h('span', { html: OSTRICH }), t('nlSignIn')); + } return h('div', { class: 'card col', style: 'gap:10px' }, - h('h3', { style: 'margin:0' }, t('nlTitle')), + h('div', { class: 'row between' }, + h('h3', { style: 'margin:0' }, t('nlTitle')), + h('span', { class: 'linklike small', onClick: () => { ui.nostrLoginOpen = false; render(); } }, t('cancel'))), h('p', { class: 'small muted', style: 'margin:0' }, t('nlDesc')), signerButtons(loginWith)); } diff --git a/src/i18n.js b/src/i18n.js index c1722d2..6e62e35 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -234,31 +234,14 @@ const STR = { giftExceedsBalance: 'That’s more than your balance.', giftNeedsHeadroom: 'Gift up to {n}, or tap Max to gift your whole balance.', giftAnother: 'Create another', - giftLockPlaceholder: 'Lock to npub (optional)', - giftLockHint: 'Optionally lock this gift to a nostr account: the link stays public, but only they get the claim code — sent to them as a nostr DM.', - giftLockTo: 'For', - giftLockInvalid: 'Not a valid npub or hex pubkey.', - giftLockedTo: 'For', giftForYou: 'A gift for you', claimGift: 'Claim', dismiss: 'Dismiss', - giftCodeHint: 'Check your nostr direct messages for the claim code, then paste it below.', - claimCodePlaceholder: 'Paste claim code', - claimCodeWrong: 'That code didn’t work — check your nostr DM and try again.', - giftDmText: "🎁 You've received a Bitcoin gift of {amount}! Claim it here: {link}\n\nClaim code: {code}", - giftDmSent: 'Claim code sent to them via nostr DM.', - giftDmSending: 'Sending the claim code…', - giftDmFailed: 'Couldn’t send the nostr DM. Share this code with them privately:', giftCopyCode: 'Copy claim code', giftQrTooLong: 'Too large for a QR code — share the link below instead.', giftConsolidateExplain: 'This gift would bundle {count} coins, making it costly for the recipient to claim. Splitting one coin first (~{fee}) gives them a clean single-input gift.', giftConsolidateFirst: 'Split into one coin first', giftUseManyCoins: 'Use {count} coins anyway', - claimWithExtension: 'Claim with nostr extension', - orEnterCode: 'or paste the claim code', - extWrongAccount: 'Your extension is signed in as a different nostr account than this gift is for.', - extNoNip44: 'Your nostr extension doesn’t support NIP-44 — paste the code instead.', - extDecryptFailed: 'Couldn’t decrypt with the extension — try the code.', nostrKeyTitle: 'Nostr address', nostrKeyDesc: 'Share this so others can lock a gift to you — the claim code is sent here as a nostr DM.', giftSplitExplain: 'This gift would lock {lock} until it’s claimed ({change} of it comes back to you then). You can split a right-sized coin first for about {fee} so only the gift amount stays locked.', @@ -300,7 +283,6 @@ const STR = { claimIntoExisting: 'Receive into one of your wallets:', claimOr: 'or', goToWallet: 'Go to wallet', - giftClaimedToast: 'Gift claimed', claimBtn: 'Claim it', claimFeeNote: '~{n} fee', claimFailed: 'This gift has already been claimed or is no longer available.', @@ -428,6 +410,7 @@ const STR = { arkLnPayFailed: 'Payment failed', arkLnRefunded: 'Payment failed — funds returned to your Ark balance.', arkLnPaidTitle: 'Paid!', + nlSignIn: 'Sign in with Nostr', nlTitle: 'Log in with Nostr', nlDesc: 'Use your Nostr account to open your wallet — on any device, with the same account.', nlExtension: 'Use browser extension', diff --git a/src/nostr.js b/src/nostr.js index 0992122..3d0ea2f 100644 --- a/src/nostr.js +++ b/src/nostr.js @@ -58,8 +58,6 @@ export async function publishOn(relays, evt) { } catch { return false; } } -// --- locking a gift to a recipient's Nostr key --------------------------- -// An npub or 64-hex nostr pubkey → hex, or null if it isn't one. // NIP-47 wallet services sign with their own per-connection key and must // speak whichever encryption the client asked for, so export both schemes. export { nip04, nip44 }; @@ -71,26 +69,9 @@ export function parseNostrPubkey(input) { try { const d = nip19decode(s); if (d.type === 'npub') return d.data; } catch {} return null; } +// An npub or 64-hex nostr pubkey → hex, or null if it isn't one. export function npubOf(pkHex) { try { return npubEncode(pkHex); } catch { return null; } } -// Encrypt a gift payload two ways: (1) under a fresh one-time code, delivered to -// the recipient out-of-band via a nostr DM — the manual path; and (2) to the -// recipient's nostr pubkey via an ephemeral key, so a NIP-07 browser extension -// can decrypt it in-place with no code. Both decrypt to the same payload. -export function encryptGiftPayload(plaintext, recipientPkHex) { - const codeKey = randomBytes(32); - const ephSk = generateSecretKey(); - return { - code: base64urlnopad.encode(codeKey), - ctCode: nip44.encrypt(plaintext, codeKey), - eph: getPublicKey(ephSk), - ctKey: nip44.encrypt(plaintext, nip44.getConversationKey(ephSk, recipientPkHex)), - }; -} -export function decryptWithCode(code, ct) { - return nip44.decrypt(ct, base64urlnopad.decode((code || '').trim())); -} - // Profiles live across the network, so look them up on popular relays (broader // than the sync relays). export const PROFILE_RELAYS = ['wss://relay.damus.io', 'wss://nos.lol', 'wss://relay.primal.net', 'wss://relay.coinos.io']; |
