diff options
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']; |
