From ffb5c30b54f913746cd6f9f79166cc4d0842dc78 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Wed, 24 Jun 2026 09:45:24 -0700 Subject: Add shareable gift links for fast onboarding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Create a #claim= link from Settings → "Gift this wallet" (gated behind a reveal + warning, with a QR): the seed (and passphrase, if any) are encoded as BIP39 entropy in base64url in the URL fragment, which never reaches a server. Opening such a link loads that wallet and shows a welcome → balance → firm "back up your recovery phrase" flow, then drops into the wallet. The claim code is scrubbed from the address bar on load. Bearer by design — whoever opens the link controls the funds — so the creator UI warns to keep amounts small. New strings translated into all 19 locales. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/app.js | 99 +++++++++++++++++++++++++++++++++++++++++++++++++---- src/i18n.js | 10 ++++++ src/locales/ar.json | 14 ++++++-- src/locales/bn.json | 14 ++++++-- src/locales/de.json | 14 ++++++-- src/locales/es.json | 14 ++++++-- src/locales/fr.json | 12 ++++++- src/locales/hi.json | 14 ++++++-- src/locales/id.json | 14 ++++++-- src/locales/ja.json | 14 ++++++-- src/locales/ko.json | 12 ++++++- src/locales/mr.json | 14 ++++++-- src/locales/pt.json | 12 ++++++- src/locales/ru.json | 12 ++++++- src/locales/sw.json | 12 ++++++- src/locales/ta.json | 12 ++++++- src/locales/te.json | 14 ++++++-- src/locales/tr.json | 12 ++++++- src/locales/ur.json | 14 ++++++-- src/locales/vi.json | 12 ++++++- src/locales/zh.json | 12 ++++++- src/wallet.js | 25 ++++++++++++-- 22 files changed, 344 insertions(+), 38 deletions(-) (limited to 'src') diff --git a/src/app.js b/src/app.js index 911bca0..cf5a9de 100644 --- a/src/app.js +++ b/src/app.js @@ -4,7 +4,7 @@ // which rebuilds the active screen. Text inputs write back into `ui` on `input` // (without re-rendering) so their values survive structural re-renders. -import { Wallet, newMnemonic, isValidMnemonic, utxoId } from './wallet.js'; +import { Wallet, newMnemonic, isValidMnemonic, utxoId, encodeGift, decodeGift } from './wallet.js'; import { qrSvg } from './qr.js'; import { scanQr } from './scan.js'; import { getSyncConfig, setSyncConfig } from './nostr.js'; @@ -23,7 +23,8 @@ import { const wallet = new Wallet(); const ui = { - screen: 'unlock', // 'unlock' | 'wallet' | 'howItWorks' + screen: 'unlock', // 'unlock' | 'wallet' | 'claim' | 'howItWorks' + claimStep: null, // 'welcome' | 'backup' when opening a gift link returnScreen: 'unlock', // where 'howItWorks' returns to (Back / logo) unlockTab: 'create', // 'create' | 'import' createStep: 'gen', // 'gen' | 'confirm' @@ -34,6 +35,7 @@ const ui = { passphrase: '', showPass: false, revealShown: false, // recovery phrase unmasked on the Backup tab (after the warning) + giftShown: false, // gift link revealed in Settings offlineFallback: false, // auto-entered offline because the network was unreachable unlockError: '', @@ -148,9 +150,11 @@ function render() { const screen = ui.screen === 'wallet' ? walletScreen() - : ui.screen === 'howItWorks' - ? howItWorksScreen() - : unlockScreen(); + : ui.screen === 'claim' + ? claimScreen() + : ui.screen === 'howItWorks' + ? howItWorksScreen() + : unlockScreen(); root.replaceChildren(screen, footer()); } wallet.subscribe(render); @@ -492,11 +496,13 @@ async function openWallet(mnemonic) { // Load a wallet and start scanning. Persists to sessionStorage so a refresh // restores the open wallet (cleared on logout or when the tab closes). -async function enterWallet(mnemonic, passphrase) { +async function enterWallet(mnemonic, passphrase, opts = {}) { wallet.load({ mnemonic, passphrase, netName: 'mainnet', offline: false }); persistSession(mnemonic, passphrase); const hadCache = wallet.restoreCache(); // show last-known balance/history instantly - ui.screen = 'wallet'; + // A claimed gift link starts on a welcome/back-up screen instead of the wallet. + ui.screen = opts.claim ? 'claim' : 'wallet'; + ui.claimStep = 'welcome'; ui.tab = 'receive'; // Not baselined yet — stays null until the scan + ack logic below sets it, // so the celebration never fires for payments that were already there at @@ -598,6 +604,7 @@ function lock() { ui.passphrase = ''; ui.confirm = []; ui.revealShown = false; + ui.giftShown = false; ui.receiveSeenIndex = null; ui.txDetail = null; ui.broadcastTx = null; @@ -675,6 +682,7 @@ function settingsTab() { wallet.scanning ? t('scanning') : t('rescanWallet')) ), explorerCard(), + giftCard(), syncCard(), h( 'div', @@ -685,6 +693,32 @@ function settingsTab() { ); } +// Gift link: turn this whole wallet into a #claim= link (seed in the URL +// fragment) for quick onboarding. Gated behind a reveal + warning, since the +// link is bearer — whoever opens it controls the funds. +function giftUrl() { + return `${location.origin}/#claim=${encodeGift(wallet.mnemonic, wallet.passphrase)}`; +} +function giftCard() { + return h( + 'div', + { class: 'card col' }, + h('h3', {}, t('giftLink')), + h('p', { class: 'small muted', style: 'margin:0' }, t('giftLinkDesc')), + h('div', { class: 'warn-box' }, t('giftLinkWarn')), + ui.giftShown + ? h('div', { class: 'col', style: 'align-items:center;gap:10px' }, + h('div', { html: qrSvg(giftUrl()) }), + 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.giftShown = false; render(); } }, t('hide')) + ) + ) + : h('button', { class: 'btn-block', onClick: () => { ui.giftShown = true; render(); } }, t('giftLinkReveal')) + ); +} + // Block explorer / server selection: a preset (mempool.space, blockstream.info) // or a custom Esplora/electrs REST URL (e.g. your own node). function explorerCard() { @@ -815,6 +849,55 @@ function goHome() { render(); } +// ================================================================ GIFT / CLAIM +// Read a #claim= gift link, returning the seed (and scrubbing it from the +// URL so it doesn't linger in the address bar or history). +function readClaimHash() { + try { + const m = location.hash.match(/^#claim=([A-Za-z0-9_~-]+)$/); + if (!m) return null; + history.replaceState(null, '', location.pathname + location.search); + return decodeGift(m[1]); + } catch { + return null; + } +} + +// Onboarding flow shown when a gift link is opened: celebrate + balance, then a +// firm nudge to back up the recovery phrase (the recipient now owns this wallet). +function claimScreen() { + if (ui.claimStep === 'backup') { + const words = wallet.mnemonic.split(' '); + return h( + 'div', + { class: 'col', style: 'gap:16px' }, + brandHeader(false), + h('div', { class: 'card col' }, + h('h3', {}, t('recoveryPhrase')), + h('div', { class: 'warn-box' }, t('writeDownWarn')), + h('div', { class: 'words' }, + words.map((w, i) => h('div', { class: 'w' }, h('span', { class: 'n' }, i + 1), h('span', { class: 't' }, w))) + ), + h('div', { class: 'row gap6' }, copyBtn(wallet.mnemonic, t('copyPhrase'))), + h('button', { class: 'btn-primary btn-block', onClick: () => { ui.screen = 'wallet'; ui.claimStep = null; render(); } }, t('giftBackedUp')) + ) + ); + } + return h( + 'div', + { class: 'col', style: 'gap:16px' }, + brandHeader(false), + h('div', { class: 'card col', style: 'align-items:center;text-align:center;gap:12px' }, + h('div', { class: 'check-badge', style: 'background:var(--accent)' }, '🎁'), + h('h2', { style: 'margin:0' }, t('giftWelcome')), + h('p', { class: 'muted', style: 'margin:0' }, t('giftWelcomeBody')) + ), + balanceCard(), + h('button', { class: 'btn-primary btn-block', onClick: () => { ui.claimStep = 'backup'; render(); } }, t('giftBackup')), + h('button', { class: 'btn-block', onClick: () => { ui.screen = 'wallet'; ui.claimStep = null; render(); } }, t('giftSkip')) + ); +} + function walletScreen() { return h( 'div', @@ -1640,6 +1723,8 @@ async function importSnapshotFile(e) { // show the unlock screen. applyDir(); loadLocale(getLang()).finally(() => { + const gift = readClaimHash(); + if (gift) { enterWallet(gift.mnemonic, gift.passphrase, { claim: true }); return; } if (!restoreSession()) render(); }); diff --git a/src/i18n.js b/src/i18n.js index 10dc74d..0c38520 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -144,6 +144,16 @@ const STR = { copyId: 'Copy ID', backToHistory: '← Back to history', recoveryPhrase: 'Recovery phrase', + giftWelcome: 'You’ve been sent Bitcoin', + giftWelcomeBody: 'This wallet is yours now. Anyone with its recovery phrase controls the coins, so back it up before you do anything else.', + giftBackup: 'Back up this wallet', + giftSkip: 'Skip for now', + giftBackedUp: 'I’ve written it down', + giftLink: 'Gift this wallet', + giftLinkDesc: 'Create a link that opens this wallet, with its funds, for onboarding someone fast.', + giftLinkWarn: '⚠ Anyone who opens this link controls all the funds in this wallet. Only share it to give the wallet away, and keep the amount small.', + giftLinkReveal: 'Create gift link', + copyLink: 'Copy link', recoveryWarn: '⚠ Anyone who can see these words can steal your funds. Make sure nobody is watching before you reveal them.', bip39Passphrase: 'Passphrase', copyPassphrase: 'Copy passphrase', diff --git a/src/locales/ar.json b/src/locales/ar.json index 54a60c0..b78e83d 100644 --- a/src/locales/ar.json +++ b/src/locales/ar.json @@ -145,5 +145,15 @@ "bumpFee": "رفع الرسوم", "bumpConfirm": "استبدال برسوم أعلى؟", "replaceTx": "استبدال", - "bumpInsufficient": "لا توجد أموال كافية للرفع بهذا المعدل. جرّب معدلاً أقل." -} \ No newline at end of file + "bumpInsufficient": "لا توجد أموال كافية للرفع بهذا المعدل. جرّب معدلاً أقل.", + "giftWelcome": "لقد أُرسلت إليك عملة Bitcoin", + "giftWelcomeBody": "هذه المحفظة لك الآن. أي شخص يملك عبارة الاسترداد الخاصة بها يتحكم في العملات، لذا احتفظ بنسخة احتياطية قبل أن تفعل أي شيء آخر.", + "giftBackup": "احتفظ بنسخة احتياطية من هذه المحفظة", + "giftSkip": "تخطّي الآن", + "giftBackedUp": "لقد دوّنتها", + "giftLink": "أهدِ هذه المحفظة", + "giftLinkDesc": "أنشئ رابطاً يفتح هذه المحفظة بأموالها، لتمكين شخص ما بسرعة.", + "giftLinkWarn": "⚠ أي شخص يفتح هذا الرابط يتحكم في جميع أموال هذه المحفظة. لا تشاركه إلا لإهداء المحفظة، وأبقِ المبلغ صغيراً.", + "giftLinkReveal": "إنشاء رابط الإهداء", + "copyLink": "نسخ الرابط" +} diff --git a/src/locales/bn.json b/src/locales/bn.json index 8e0f100..d9dfe42 100644 --- a/src/locales/bn.json +++ b/src/locales/bn.json @@ -145,5 +145,15 @@ "bumpFee": "ফি বাড়ান", "bumpConfirm": "বেশি ফি দিয়ে প্রতিস্থাপন করবেন?", "replaceTx": "প্রতিস্থাপন", - "bumpInsufficient": "এই হারে বাড়ানোর মতো যথেষ্ট তহবিল নেই। কম হার চেষ্টা করুন।" -} \ No newline at end of file + "bumpInsufficient": "এই হারে বাড়ানোর মতো যথেষ্ট তহবিল নেই। কম হার চেষ্টা করুন।", + "giftWelcome": "আপনাকে Bitcoin পাঠানো হয়েছে", + "giftWelcomeBody": "এই ওয়ালেটটি এখন আপনার। এর রিকভারি ফ্রেজ যার কাছে আছে সে-ই কয়েনগুলো নিয়ন্ত্রণ করে, তাই অন্য কিছু করার আগে এটি ব্যাকআপ করে নিন।", + "giftBackup": "এই ওয়ালেট ব্যাকআপ করুন", + "giftSkip": "আপাতত এড়িয়ে যান", + "giftBackedUp": "আমি এটি লিখে রেখেছি", + "giftLink": "এই ওয়ালেট উপহার দিন", + "giftLinkDesc": "একটি লিঙ্ক তৈরি করুন যা এই ওয়ালেট ও এর তহবিল খোলে, যাতে কাউকে দ্রুত শুরু করিয়ে দেওয়া যায়।", + "giftLinkWarn": "⚠ যে কেউ এই লিঙ্ক খুললে এই ওয়ালেটের সব তহবিল নিয়ন্ত্রণ করে। কেবল ওয়ালেট দিয়ে দিতেই এটি শেয়ার করুন এবং পরিমাণ কম রাখুন।", + "giftLinkReveal": "উপহার লিঙ্ক তৈরি করুন", + "copyLink": "লিঙ্ক কপি" +} diff --git a/src/locales/de.json b/src/locales/de.json index 6ad55a8..49b291b 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -145,5 +145,15 @@ "bumpFee": "Gebühr erhöhen", "bumpConfirm": "Mit höherer Gebühr ersetzen?", "replaceTx": "Ersetzen", - "bumpInsufficient": "Nicht genug Guthaben für diese Gebührenrate. Versuche eine niedrigere Rate." -} \ No newline at end of file + "bumpInsufficient": "Nicht genug Guthaben für diese Gebührenrate. Versuche eine niedrigere Rate.", + "giftWelcome": "Dir wurde Bitcoin gesendet", + "giftWelcomeBody": "Diese Wallet gehört jetzt dir. Wer ihre Wiederherstellungsphrase besitzt, kontrolliert die Coins — sichere sie daher, bevor du irgendetwas anderes tust.", + "giftBackup": "Diese Wallet sichern", + "giftSkip": "Vorerst überspringen", + "giftBackedUp": "Ich habe sie aufgeschrieben", + "giftLink": "Diese Wallet verschenken", + "giftLinkDesc": "Erstelle einen Link, der diese Wallet samt Guthaben öffnet, um jemanden schnell einzurichten.", + "giftLinkWarn": "⚠ Wer diesen Link öffnet, kontrolliert das gesamte Guthaben dieser Wallet. Teile ihn nur, um die Wallet zu verschenken, und halte den Betrag klein.", + "giftLinkReveal": "Geschenk-Link erstellen", + "copyLink": "Link kopieren" +} diff --git a/src/locales/es.json b/src/locales/es.json index 5d98fb3..1f62f13 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -145,5 +145,15 @@ "bumpFee": "Aumentar comisión", "bumpConfirm": "¿Reemplazar con una comisión más alta?", "replaceTx": "Reemplazar", - "bumpInsufficient": "Fondos insuficientes para aumentar a esta tasa. Prueba una tasa más baja." -} \ No newline at end of file + "bumpInsufficient": "Fondos insuficientes para aumentar a esta tasa. Prueba una tasa más baja.", + "giftWelcome": "Te han enviado Bitcoin", + "giftWelcomeBody": "Esta billetera ahora es tuya. Cualquiera que tenga su frase de recuperación controla las monedas, así que haz una copia de seguridad antes de hacer cualquier otra cosa.", + "giftBackup": "Hacer copia de seguridad de esta billetera", + "giftSkip": "Omitir por ahora", + "giftBackedUp": "Ya la anoté", + "giftLink": "Regalar esta billetera", + "giftLinkDesc": "Crea un enlace que abre esta billetera, con sus fondos, para incorporar a alguien rápidamente.", + "giftLinkWarn": "⚠ Cualquiera que abra este enlace controla todos los fondos de esta billetera. Compártelo solo para regalar la billetera, y mantén el monto pequeño.", + "giftLinkReveal": "Crear enlace de regalo", + "copyLink": "Copiar enlace" +} diff --git a/src/locales/fr.json b/src/locales/fr.json index fd7f325..c23e5ff 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -145,5 +145,15 @@ "bumpFee": "Augmenter les frais", "bumpConfirm": "Remplacer avec des frais plus élevés ?", "replaceTx": "Remplacer", - "bumpInsufficient": "Fonds insuffisants pour augmenter à ce taux. Essayez un taux plus bas." + "bumpInsufficient": "Fonds insuffisants pour augmenter à ce taux. Essayez un taux plus bas.", + "giftWelcome": "Vous avez reçu des Bitcoin", + "giftWelcomeBody": "Ce portefeuille est désormais le vôtre. Quiconque possède sa phrase de récupération contrôle les fonds, alors sauvegardez-la avant toute autre chose.", + "giftBackup": "Sauvegarder ce portefeuille", + "giftSkip": "Plus tard", + "giftBackedUp": "Je l’ai notée", + "giftLink": "Offrir ce portefeuille", + "giftLinkDesc": "Créez un lien qui ouvre ce portefeuille, avec ses fonds, pour configurer quelqu’un rapidement.", + "giftLinkWarn": "⚠ Quiconque ouvre ce lien contrôle tous les fonds de ce portefeuille. Ne le partagez que pour donner le portefeuille, et gardez le montant faible.", + "giftLinkReveal": "Créer le lien cadeau", + "copyLink": "Copier le lien" } \ No newline at end of file diff --git a/src/locales/hi.json b/src/locales/hi.json index dfda450..90a9872 100644 --- a/src/locales/hi.json +++ b/src/locales/hi.json @@ -145,5 +145,15 @@ "bumpFee": "फ़ीस बढ़ाएँ", "bumpConfirm": "अधिक फ़ीस के साथ बदलें?", "replaceTx": "बदलें", - "bumpInsufficient": "इस दर पर बढ़ाने के लिए पर्याप्त धन नहीं। कम दर आज़माएँ।" -} \ No newline at end of file + "bumpInsufficient": "इस दर पर बढ़ाने के लिए पर्याप्त धन नहीं। कम दर आज़माएँ।", + "giftWelcome": "आपको Bitcoin भेजा गया है", + "giftWelcomeBody": "अब यह वॉलेट आपका है। जिसके पास इसका रिकवरी वाक्यांश होगा वही सिक्कों को नियंत्रित करता है, इसलिए कुछ और करने से पहले इसका बैकअप ले लें।", + "giftBackup": "इस वॉलेट का बैकअप लें", + "giftSkip": "अभी के लिए छोड़ें", + "giftBackedUp": "मैंने इसे लिख लिया है", + "giftLink": "यह वॉलेट उपहार में दें", + "giftLinkDesc": "एक ऐसा लिंक बनाएँ जो इस वॉलेट को इसके फंड के साथ खोले, किसी को तेज़ी से जोड़ने के लिए।", + "giftLinkWarn": "⚠ जो भी इस लिंक को खोलेगा वह इस वॉलेट के सभी फंड को नियंत्रित करता है। इसे केवल वॉलेट देने के लिए साझा करें, और राशि कम रखें।", + "giftLinkReveal": "उपहार लिंक बनाएँ", + "copyLink": "लिंक कॉपी करें" +} diff --git a/src/locales/id.json b/src/locales/id.json index 6a215a6..9199318 100644 --- a/src/locales/id.json +++ b/src/locales/id.json @@ -145,5 +145,15 @@ "bumpFee": "Naikkan biaya", "bumpConfirm": "Ganti dengan biaya lebih tinggi?", "replaceTx": "Ganti", - "bumpInsufficient": "Dana tidak cukup untuk menaikkan pada tarif ini. Coba tarif lebih rendah." -} \ No newline at end of file + "bumpInsufficient": "Dana tidak cukup untuk menaikkan pada tarif ini. Coba tarif lebih rendah.", + "giftWelcome": "Anda telah dikirimi Bitcoin", + "giftWelcomeBody": "Dompet ini sekarang milik Anda. Siapa pun yang memiliki frasa pemulihannya menguasai koinnya, jadi cadangkan dulu sebelum melakukan hal lain.", + "giftBackup": "Cadangkan dompet ini", + "giftSkip": "Lewati dulu", + "giftBackedUp": "Saya sudah mencatatnya", + "giftLink": "Hadiahkan dompet ini", + "giftLinkDesc": "Buat tautan yang membuka dompet ini, beserta dananya, untuk memulai seseorang dengan cepat.", + "giftLinkWarn": "⚠ Siapa pun yang membuka tautan ini menguasai seluruh dana di dompet ini. Bagikan hanya untuk menghadiahkan dompet, dan jaga jumlahnya tetap kecil.", + "giftLinkReveal": "Buat tautan hadiah", + "copyLink": "Salin tautan" +} diff --git a/src/locales/ja.json b/src/locales/ja.json index 1c6aef6..2c13e28 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -145,5 +145,15 @@ "bumpFee": "手数料を上げる", "bumpConfirm": "より高い手数料で置き換えますか?", "replaceTx": "置き換える", - "bumpInsufficient": "この手数料率に上げる資金が足りません。低い率をお試しください。" -} \ No newline at end of file + "bumpInsufficient": "この手数料率に上げる資金が足りません。低い率をお試しください。", + "giftWelcome": "Bitcoin が届きました", + "giftWelcomeBody": "このウォレットは今あなたのものです。復元フレーズを持つ人は誰でもコインを管理できるので、他の操作をする前にバックアップしてください。", + "giftBackup": "このウォレットをバックアップ", + "giftSkip": "今はスキップ", + "giftBackedUp": "書き留めました", + "giftLink": "このウォレットを贈る", + "giftLinkDesc": "このウォレットを資金ごと開くリンクを作成し、すばやく誰かに使い始めてもらえます。", + "giftLinkWarn": "⚠ このリンクを開いた人は、このウォレット内のすべての資金を管理できます。ウォレットを譲り渡すときだけ共有し、金額は少なめにしてください。", + "giftLinkReveal": "ギフトリンクを作成", + "copyLink": "リンクをコピー" +} diff --git a/src/locales/ko.json b/src/locales/ko.json index 13d83d4..c131955 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -145,5 +145,15 @@ "bumpFee": "수수료 올리기", "bumpConfirm": "더 높은 수수료로 교체할까요?", "replaceTx": "교체", - "bumpInsufficient": "이 수수료율로 올릴 자금이 부족합니다. 더 낮은 율을 시도하세요." + "bumpInsufficient": "이 수수료율로 올릴 자금이 부족합니다. 더 낮은 율을 시도하세요.", + "giftWelcome": "Bitcoin을 받았습니다", + "giftWelcomeBody": "이제 이 지갑은 당신의 것입니다. 복구 문구를 가진 사람은 누구나 코인을 통제할 수 있으니, 다른 작업을 하기 전에 먼저 백업하세요.", + "giftBackup": "이 지갑 백업하기", + "giftSkip": "나중에 하기", + "giftBackedUp": "적어 두었습니다", + "giftLink": "이 지갑 선물하기", + "giftLinkDesc": "자금이 들어 있는 이 지갑을 여는 링크를 만들어 빠르게 다른 사람을 시작하게 하세요.", + "giftLinkWarn": "⚠ 이 링크를 여는 사람은 누구나 이 지갑의 모든 자금을 통제합니다. 지갑을 넘겨줄 때만 공유하고, 금액은 적게 유지하세요.", + "giftLinkReveal": "선물 링크 만들기", + "copyLink": "링크 복사" } \ No newline at end of file diff --git a/src/locales/mr.json b/src/locales/mr.json index 6717d9d..087f43b 100644 --- a/src/locales/mr.json +++ b/src/locales/mr.json @@ -145,5 +145,15 @@ "bumpFee": "फी वाढवा", "bumpConfirm": "जास्त फीसह बदला?", "replaceTx": "बदला", - "bumpInsufficient": "या दराने वाढवण्यासाठी पुरेसा निधी नाही. कमी दर वापरून पहा." -} \ No newline at end of file + "bumpInsufficient": "या दराने वाढवण्यासाठी पुरेसा निधी नाही. कमी दर वापरून पहा.", + "giftWelcome": "तुम्हाला Bitcoin पाठवले आहे", + "giftWelcomeBody": "हे वॉलेट आता तुमचे आहे. ज्याच्याकडे याचा रिकव्हरी वाक्यांश आहे तो कॉइन्सवर नियंत्रण ठेवतो, म्हणून इतर काहीही करण्यापूर्वी त्याचा बॅकअप घ्या.", + "giftBackup": "हे वॉलेट बॅकअप करा", + "giftSkip": "आत्ता वगळा", + "giftBackedUp": "मी ते लिहून ठेवले आहे", + "giftLink": "हे वॉलेट भेट द्या", + "giftLinkDesc": "एखाद्याला झटपट सुरुवात करून देण्यासाठी हे वॉलेट त्याच्या निधीसह उघडणारी लिंक तयार करा.", + "giftLinkWarn": "⚠ ही लिंक उघडणारा कोणीही या वॉलेटमधील सर्व निधीवर नियंत्रण ठेवतो. फक्त वॉलेट देऊन टाकण्यासाठीच ती शेअर करा आणि रक्कम कमी ठेवा.", + "giftLinkReveal": "भेट लिंक तयार करा", + "copyLink": "लिंक कॉपी करा" +} diff --git a/src/locales/pt.json b/src/locales/pt.json index 12d07fc..c52eb42 100644 --- a/src/locales/pt.json +++ b/src/locales/pt.json @@ -145,5 +145,15 @@ "bumpFee": "Aumentar taxa", "bumpConfirm": "Substituir com uma taxa maior?", "replaceTx": "Substituir", - "bumpInsufficient": "Fundos insuficientes para aumentar nessa taxa. Tente uma taxa menor." + "bumpInsufficient": "Fundos insuficientes para aumentar nessa taxa. Tente uma taxa menor.", + "giftWelcome": "Você recebeu Bitcoin", + "giftWelcomeBody": "Esta carteira é sua agora. Qualquer pessoa com a frase de recuperação controla as moedas, então faça o backup antes de qualquer outra coisa.", + "giftBackup": "Fazer backup desta carteira", + "giftSkip": "Pular por enquanto", + "giftBackedUp": "Já anotei", + "giftLink": "Presentear esta carteira", + "giftLinkDesc": "Crie um link que abre esta carteira, com seus fundos, para integrar alguém rapidamente.", + "giftLinkWarn": "⚠ Qualquer pessoa que abrir este link controla todos os fundos desta carteira. Compartilhe-o apenas para doar a carteira e mantenha o valor baixo.", + "giftLinkReveal": "Criar link de presente", + "copyLink": "Copiar link" } \ No newline at end of file diff --git a/src/locales/ru.json b/src/locales/ru.json index 4f00e77..97d971a 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -145,5 +145,15 @@ "bumpFee": "Поднять комиссию", "bumpConfirm": "Заменить с более высокой комиссией?", "replaceTx": "Заменить", - "bumpInsufficient": "Недостаточно средств для повышения по этой ставке. Попробуйте ниже." + "bumpInsufficient": "Недостаточно средств для повышения по этой ставке. Попробуйте ниже.", + "giftWelcome": "Вам отправили Bitcoin", + "giftWelcomeBody": "Теперь этот кошелёк ваш. Любой, у кого есть его фраза восстановления, контролирует монеты, поэтому сделайте резервную копию, прежде чем делать что-либо ещё.", + "giftBackup": "Сделать резервную копию кошелька", + "giftSkip": "Пропустить пока", + "giftBackedUp": "Я записал(а)", + "giftLink": "Подарить этот кошелёк", + "giftLinkDesc": "Создайте ссылку, которая открывает этот кошелёк с его средствами, чтобы быстро подключить кого-то.", + "giftLinkWarn": "⚠ Любой, кто откроет эту ссылку, контролирует все средства в этом кошельке. Делитесь ею только чтобы отдать кошелёк, и держите сумму небольшой.", + "giftLinkReveal": "Создать ссылку-подарок", + "copyLink": "Копировать ссылку" } \ No newline at end of file diff --git a/src/locales/sw.json b/src/locales/sw.json index 73a2c37..708b510 100644 --- a/src/locales/sw.json +++ b/src/locales/sw.json @@ -145,5 +145,15 @@ "bumpFee": "Ongeza ada", "bumpConfirm": "Badilisha kwa ada ya juu zaidi?", "replaceTx": "Badilisha", - "bumpInsufficient": "Fedha hazitoshi kuongeza kwa kiwango hiki. Jaribu kiwango cha chini." + "bumpInsufficient": "Fedha hazitoshi kuongeza kwa kiwango hiki. Jaribu kiwango cha chini.", + "giftWelcome": "Umetumiwa Bitcoin", + "giftWelcomeBody": "Pochi hii sasa ni yako. Yeyote aliye na kifungu chake cha kurejesha anadhibiti sarafu, kwa hivyo kihifadhi kabla ya kufanya jambo lingine lolote.", + "giftBackup": "Hifadhi pochi hii", + "giftSkip": "Ruka kwa sasa", + "giftBackedUp": "Nimeshakiandika", + "giftLink": "Toa pochi hii kama zawadi", + "giftLinkDesc": "Tengeneza kiungo kinachofungua pochi hii, pamoja na fedha zake, ili kumkaribisha mtu haraka.", + "giftLinkWarn": "⚠ Yeyote anayefungua kiungo hiki anadhibiti fedha zote zilizomo kwenye pochi hii. Kishiriki tu ili kuachia pochi, na uweke kiasi kidogo.", + "giftLinkReveal": "Tengeneza kiungo cha zawadi", + "copyLink": "Nakili kiungo" } \ No newline at end of file diff --git a/src/locales/ta.json b/src/locales/ta.json index 2e0102b..9692963 100644 --- a/src/locales/ta.json +++ b/src/locales/ta.json @@ -145,5 +145,15 @@ "bumpFee": "கட்டணத்தை உயர்த்து", "bumpConfirm": "அதிக கட்டணத்துடன் மாற்றவா?", "replaceTx": "மாற்று", - "bumpInsufficient": "இந்த கட்டண விகிதத்தில் உயர்த்த போதிய நிதி இல்லை. குறைந்த விகிதத்தை முயற்சிக்கவும்." + "bumpInsufficient": "இந்த கட்டண விகிதத்தில் உயர்த்த போதிய நிதி இல்லை. குறைந்த விகிதத்தை முயற்சிக்கவும்.", + "giftWelcome": "உங்களுக்கு Bitcoin அனுப்பப்பட்டுள்ளது", + "giftWelcomeBody": "இந்த வாலட் இப்போது உங்களுடையது. அதன் மீட்பு சொற்றொடரை வைத்திருக்கும் எவரும் நாணயங்களைக் கட்டுப்படுத்துவார்கள், எனவே வேறு எதையும் செய்வதற்கு முன் அதைக் காப்புப்பிரதி எடுங்கள்.", + "giftBackup": "இந்த வாலட்டைக் காப்புப்பிரதி எடு", + "giftSkip": "இப்போதைக்குத் தவிர்", + "giftBackedUp": "நான் எழுதி வைத்துவிட்டேன்", + "giftLink": "இந்த வாலட்டைப் பரிசளி", + "giftLinkDesc": "ஒருவரை விரைவாக இணைக்க, அதன் நிதியுடன் இந்த வாலட்டைத் திறக்கும் ஒரு இணைப்பை உருவாக்கவும்.", + "giftLinkWarn": "⚠ இந்த இணைப்பைத் திறக்கும் எவரும் இந்த வாலட்டில் உள்ள அனைத்து நிதியையும் கட்டுப்படுத்துவார்கள். வாலட்டை விட்டுக்கொடுக்க மட்டுமே இதைப் பகிரவும், தொகையைக் குறைவாக வைத்திருங்கள்.", + "giftLinkReveal": "பரிசு இணைப்பை உருவாக்கு", + "copyLink": "இணைப்பை நகலெடு" } \ No newline at end of file diff --git a/src/locales/te.json b/src/locales/te.json index 05790a0..24dcca9 100644 --- a/src/locales/te.json +++ b/src/locales/te.json @@ -145,5 +145,15 @@ "bumpFee": "ఫీజు పెంచు", "bumpConfirm": "ఎక్కువ ఫీజుతో భర్తీ చేయాలా?", "replaceTx": "భర్తీ చేయి", - "bumpInsufficient": "ఈ రేటుకు పెంచడానికి సరిపడా నిధులు లేవు. తక్కువ రేటు ప్రయత్నించండి." -} \ No newline at end of file + "bumpInsufficient": "ఈ రేటుకు పెంచడానికి సరిపడా నిధులు లేవు. తక్కువ రేటు ప్రయత్నించండి.", + "giftWelcome": "మీకు Bitcoin పంపబడింది", + "giftWelcomeBody": "ఈ వాలెట్ ఇప్పుడు మీది. దాని రికవరీ ఫ్రేజ్ ఉన్న ఎవరైనా నాణేలను నియంత్రిస్తారు, కాబట్టి మరేదీ చేయడానికి ముందే దాన్ని బ్యాకప్ చేయండి.", + "giftBackup": "ఈ వాలెట్‌ను బ్యాకప్ చేయండి", + "giftSkip": "ప్రస్తుతానికి దాటవేయి", + "giftBackedUp": "నేను దాన్ని రాసుకున్నాను", + "giftLink": "ఈ వాలెట్‌ను బహుమతిగా ఇవ్వండి", + "giftLinkDesc": "ఎవరినైనా త్వరగా ఆన్‌బోర్డ్ చేయడానికి, ఈ వాలెట్‌ను దాని నిధులతో సహా తెరిచే లింక్‌ను సృష్టించండి.", + "giftLinkWarn": "⚠ ఈ లింక్ తెరిచిన ఎవరైనా ఈ వాలెట్‌లోని అన్ని నిధులను నియంత్రిస్తారు. వాలెట్‌ను ఇచ్చేయడానికి మాత్రమే దీన్ని పంచుకోండి, మొత్తాన్ని తక్కువగా ఉంచండి.", + "giftLinkReveal": "బహుమతి లింక్ సృష్టించు", + "copyLink": "లింక్ కాపీ" +} diff --git a/src/locales/tr.json b/src/locales/tr.json index df74fbb..430e89e 100644 --- a/src/locales/tr.json +++ b/src/locales/tr.json @@ -145,5 +145,15 @@ "bumpFee": "Ücreti artır", "bumpConfirm": "Daha yüksek ücretle değiştirilsin mi?", "replaceTx": "Değiştir", - "bumpInsufficient": "Bu oranda artırmak için yeterli bakiye yok. Daha düşük bir oran deneyin." + "bumpInsufficient": "Bu oranda artırmak için yeterli bakiye yok. Daha düşük bir oran deneyin.", + "giftWelcome": "Size Bitcoin gönderildi", + "giftWelcomeBody": "Bu cüzdan artık sizin. Kurtarma ifadesine sahip olan herkes paraları kontrol eder, bu yüzden başka bir şey yapmadan önce yedekleyin.", + "giftBackup": "Bu cüzdanı yedekle", + "giftSkip": "Şimdilik atla", + "giftBackedUp": "Yazdım", + "giftLink": "Bu cüzdanı hediye et", + "giftLinkDesc": "Bu cüzdanı, parasıyla birlikte açan bir bağlantı oluşturun; birini hızlıca başlatmak için.", + "giftLinkWarn": "⚠ Bu bağlantıyı açan herkes cüzdandaki tüm parayı kontrol eder. Yalnızca cüzdanı devretmek için paylaşın ve tutarı küçük tutun.", + "giftLinkReveal": "Hediye bağlantısı oluştur", + "copyLink": "Bağlantıyı kopyala" } \ No newline at end of file diff --git a/src/locales/ur.json b/src/locales/ur.json index e08e4d3..90b812b 100644 --- a/src/locales/ur.json +++ b/src/locales/ur.json @@ -145,5 +145,15 @@ "bumpFee": "فیس بڑھائیں", "bumpConfirm": "زیادہ فیس کے ساتھ بدلیں؟", "replaceTx": "بدلیں", - "bumpInsufficient": "اس ریٹ پر بڑھانے کے لیے کافی رقم نہیں۔ کم ریٹ آزمائیں۔" -} \ No newline at end of file + "bumpInsufficient": "اس ریٹ پر بڑھانے کے لیے کافی رقم نہیں۔ کم ریٹ آزمائیں۔", + "giftWelcome": "آپ کو Bitcoin بھیجا گیا ہے", + "giftWelcomeBody": "یہ والیٹ اب آپ کا ہے۔ جس کے پاس بھی اس کا بحالی فقرہ ہو وہ سکوں کو کنٹرول کرتا ہے، اس لیے کچھ اور کرنے سے پہلے اس کا بیک اپ بنا لیں۔", + "giftBackup": "اس والیٹ کا بیک اپ بنائیں", + "giftSkip": "ابھی چھوڑیں", + "giftBackedUp": "میں نے اسے لکھ لیا ہے", + "giftLink": "یہ والیٹ تحفے میں دیں", + "giftLinkDesc": "ایک لنک بنائیں جو یہ والیٹ اس کی رقم سمیت کھولے، تاکہ کسی کو تیزی سے شروع کرایا جا سکے۔", + "giftLinkWarn": "⚠ جو بھی یہ لنک کھولے وہ اس والیٹ کی تمام رقم کو کنٹرول کرتا ہے۔ اسے صرف والیٹ دینے کے لیے شیئر کریں اور رقم کم رکھیں۔", + "giftLinkReveal": "تحفہ لنک بنائیں", + "copyLink": "لنک کاپی کریں" +} diff --git a/src/locales/vi.json b/src/locales/vi.json index 39b2b10..6882d18 100644 --- a/src/locales/vi.json +++ b/src/locales/vi.json @@ -145,5 +145,15 @@ "bumpFee": "Tăng phí", "bumpConfirm": "Thay thế với phí cao hơn?", "replaceTx": "Thay thế", - "bumpInsufficient": "Không đủ tiền để tăng ở mức phí này. Hãy thử mức thấp hơn." + "bumpInsufficient": "Không đủ tiền để tăng ở mức phí này. Hãy thử mức thấp hơn.", + "giftWelcome": "Bạn đã được gửi Bitcoin", + "giftWelcomeBody": "Ví này giờ là của bạn. Bất kỳ ai có cụm từ khôi phục đều kiểm soát số coin, vì vậy hãy sao lưu trước khi làm bất kỳ điều gì khác.", + "giftBackup": "Sao lưu ví này", + "giftSkip": "Bỏ qua lúc này", + "giftBackedUp": "Tôi đã ghi lại rồi", + "giftLink": "Tặng ví này", + "giftLinkDesc": "Tạo một liên kết mở ví này, kèm số tiền trong đó, để giúp ai đó bắt đầu nhanh chóng.", + "giftLinkWarn": "⚠ Bất kỳ ai mở liên kết này đều kiểm soát toàn bộ số tiền trong ví. Chỉ chia sẻ để tặng ví đi, và hãy giữ số tiền nhỏ.", + "giftLinkReveal": "Tạo liên kết tặng", + "copyLink": "Sao chép liên kết" } \ No newline at end of file diff --git a/src/locales/zh.json b/src/locales/zh.json index 50958a8..b5740b8 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -145,5 +145,15 @@ "bumpFee": "提高手续费", "bumpConfirm": "用更高手续费替换?", "replaceTx": "替换", - "bumpInsufficient": "资金不足,无法以此费率提升。请尝试更低的费率。" + "bumpInsufficient": "资金不足,无法以此费率提升。请尝试更低的费率。", + "giftWelcome": "有人给你转了 Bitcoin", + "giftWelcomeBody": "这个钱包现在归你了。任何拿到它恢复助记词的人都能控制这些币,所以在做其他事之前请先备份好它。", + "giftBackup": "备份此钱包", + "giftSkip": "暂时跳过", + "giftBackedUp": "我已经抄写下来了", + "giftLink": "赠送此钱包", + "giftLinkDesc": "创建一个能打开此钱包(含其资金)的链接,方便快速帮人上手。", + "giftLinkWarn": "⚠ 任何打开此链接的人都能控制本钱包中的全部资金。仅在赠送钱包时分享它,并请保持金额较小。", + "giftLinkReveal": "创建赠送链接", + "copyLink": "复制链接" } \ No newline at end of file diff --git a/src/wallet.js b/src/wallet.js index edacc05..08f5d9e 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -9,9 +9,9 @@ // transactions, only the (txid, vout, value, address) of each UTXO. import { HDKey } from '@scure/bip32'; -import { generateMnemonic, mnemonicToSeedSync, validateMnemonic } from '@scure/bip39'; +import { generateMnemonic, mnemonicToSeedSync, validateMnemonic, mnemonicToEntropy, entropyToMnemonic } from '@scure/bip39'; import { wordlist } from '@scure/bip39/wordlists/english'; -import { hex } from '@scure/base'; +import { hex, base64urlnopad } from '@scure/base'; import { sha256 } from '@noble/hashes/sha256'; import * as btc from '@scure/btc-signer'; import { p2wpkh } from '@scure/btc-signer/payment'; @@ -1185,6 +1185,27 @@ export function utxoId(u) { return `${u.txid}:${u.vout}`; } +// Gift links: encode a wallet (12-word seed as BIP39 entropy, plus the optional +// passphrase) compactly for the #claim= fragment as base64url, and decode it +// back to { mnemonic, passphrase }. Returns null if the code isn't valid. +export function encodeGift(mnemonic, passphrase = '') { + let code = base64urlnopad.encode(mnemonicToEntropy(mnemonic.trim().replace(/\s+/g, ' '), wordlist)); + if (passphrase) code += '~' + base64urlnopad.encode(new TextEncoder().encode(passphrase)); + return code; +} + +export function decodeGift(code) { + try { + const [seedPart, passPart] = code.split('~'); + const mnemonic = entropyToMnemonic(base64urlnopad.decode(seedPart), wordlist); + if (!validateMnemonic(mnemonic, wordlist)) return null; + const passphrase = passPart ? new TextDecoder().decode(base64urlnopad.decode(passPart)) : ''; + return { mnemonic, passphrase }; + } catch { + return null; + } +} + function firstUnused(chain) { const u = chain.find((a) => !a.used); return u ? u.index : chain.length; -- cgit v1.2.3