aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/app.js5
-rw-r--r--src/features/gifts-wallet.js18
-rw-r--r--src/features/gifts.js157
-rw-r--r--src/features/nostrlogin.js14
-rw-r--r--src/i18n.js19
-rw-r--r--src/nostr.js21
6 files changed, 26 insertions, 208 deletions
diff --git a/src/app.js b/src/app.js
index ded5ee4..8a33166 100644
--- a/src/app.js
+++ b/src/app.js
@@ -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'];