diff options
| author | Adam Soltys <[email protected]> | 2026-06-24 13:38:08 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-06-24 13:38:08 -0700 |
| commit | 7234a3de9e200f36accb754f62163f0bd5360832 (patch) | |
| tree | f30b492cd8938503512a2f565a7d4a9b396afff1 /src | |
| parent | ffb5c30b54f913746cd6f9f79166cc4d0842dc78 (diff) | |
| download | coinosv3-7234a3de9e200f36accb754f62163f0bd5360832.tar.gz coinosv3-7234a3de9e200f36accb754f62163f0bd5360832.zip | |
Replace seed gift links with presigned PSBT claim links
Gifting no longer shares a seed. "Gift some Bitcoin" (Settings) presigns
a chosen amount as a #gift= link: one SIGHASH_SINGLE input (the sender's
change output is cryptographically fixed, so a front-runner can only take
the gift portion, never the change) plus the rest left open. The coin is
reserved per-wallet so normal sends skip it; reclaim it anytime.
Opening the link generates a brand-new wallet the sender never sees,
shows the amount, and on Claim adds the claimer's address as the output,
finalizes, and broadcasts — then prompts backup of the fresh seed. This
gives forward secrecy (sender locked out once confirmed), one-shot
semantics (claiming spends the input), and keeps funds in the sender's
wallet until claimed.
Crypto verified end-to-end (sign NONE/SINGLE -> add output -> finalize ->
valid tx; deterministic-signature equality proves the presigned sig is
valid for any output the claimer picks). Strings retranslated across all
19 locales.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Diffstat (limited to 'src')
| -rw-r--r-- | src/app.js | 126 | ||||
| -rw-r--r-- | src/i18n.js | 19 | ||||
| -rw-r--r-- | src/locales/ar.json | 16 | ||||
| -rw-r--r-- | src/locales/bn.json | 18 | ||||
| -rw-r--r-- | src/locales/de.json | 18 | ||||
| -rw-r--r-- | src/locales/es.json | 18 | ||||
| -rw-r--r-- | src/locales/fr.json | 18 | ||||
| -rw-r--r-- | src/locales/hi.json | 16 | ||||
| -rw-r--r-- | src/locales/id.json | 16 | ||||
| -rw-r--r-- | src/locales/ja.json | 16 | ||||
| -rw-r--r-- | src/locales/ko.json | 16 | ||||
| -rw-r--r-- | src/locales/mr.json | 16 | ||||
| -rw-r--r-- | src/locales/pt.json | 18 | ||||
| -rw-r--r-- | src/locales/ru.json | 18 | ||||
| -rw-r--r-- | src/locales/sw.json | 18 | ||||
| -rw-r--r-- | src/locales/ta.json | 16 | ||||
| -rw-r--r-- | src/locales/te.json | 18 | ||||
| -rw-r--r-- | src/locales/tr.json | 16 | ||||
| -rw-r--r-- | src/locales/ur.json | 18 | ||||
| -rw-r--r-- | src/locales/vi.json | 18 | ||||
| -rw-r--r-- | src/locales/zh.json | 16 | ||||
| -rw-r--r-- | src/wallet.js | 108 |
22 files changed, 460 insertions, 117 deletions
@@ -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, encodeGift, decodeGift } from './wallet.js'; +import { Wallet, newMnemonic, isValidMnemonic, utxoId, previewGift, buildClaimTx } from './wallet.js'; import { qrSvg } from './qr.js'; import { scanQr } from './scan.js'; import { getSyncConfig, setSyncConfig } from './nostr.js'; @@ -35,7 +35,12 @@ const ui = { passphrase: '', showPass: false, revealShown: false, // recovery phrase unmasked on the Backup tab (after the warning) - giftShown: false, // gift link revealed in Settings + giftAmount: '', // gift-create amount input (Settings) + giftCode: null, // last-created gift PSBT code + giftError: '', + claimCode: null, // gift code being claimed (opened from a #gift= link) + claimedAmount: 0, + claimError: '', offlineFallback: false, // auto-entered offline because the network was unreachable unlockError: '', @@ -500,9 +505,11 @@ 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 - // A claimed gift link starts on a welcome/back-up screen instead of the wallet. - ui.screen = opts.claim ? 'claim' : 'wallet'; + // An opened gift link starts on a claim/back-up screen instead of the wallet. + ui.screen = opts.gift ? 'claim' : 'wallet'; ui.claimStep = 'welcome'; + ui.claimCode = opts.gift || null; + ui.claimError = ''; 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 @@ -604,7 +611,9 @@ function lock() { ui.passphrase = ''; ui.confirm = []; ui.revealShown = false; - ui.giftShown = false; + ui.giftAmount = ''; + ui.giftCode = null; + ui.giftError = ''; ui.receiveSeenIndex = null; ui.txDetail = null; ui.broadcastTx = null; @@ -693,29 +702,61 @@ 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. +// 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}/#claim=${encodeGift(wallet.mnemonic, wallet.passphrase)}`; + return `${location.origin}/#gift=${ui.giftCode}`; +} +function createGiftLink() { + const amt = Math.round(Number(ui.giftAmount)); + if (!amt || amt < 546) { ui.giftError = t('giftAmountInvalid'); render(); return; } + const rate = (wallet.feeRates && wallet.feeRates.halfHourFee) || 5; + try { + ui.giftCode = wallet.createGift(amt, rate).code; + ui.giftError = ''; + } catch (e) { + ui.giftError = e.message; + } + render(); } function giftCard() { + const active = wallet.activeGifts(); 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 + ui.giftCode ? 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-sm grow', onClick: () => { ui.giftCode = null; ui.giftAmount = ''; render(); } }, t('giftAnother')) ) ) - : h('button', { class: 'btn-block', onClick: () => { ui.giftShown = true; render(); } }, t('giftLinkReveal')) + : h('div', { class: 'col gap6' }, + h('div', { class: 'input-group' }, + h('input', { type: 'number', min: '546', inputmode: 'numeric', placeholder: t('giftAmountLabel'), + value: ui.giftAmount, onInput: (e) => (ui.giftAmount = e.target.value) }), + h('span', { class: 'small muted', style: 'align-self:center' }, unitLabel()) + ), + ui.giftError && h('div', { class: 'notice err' }, ui.giftError), + h('button', { class: 'btn-block', onClick: createGiftLink }, t('giftLinkReveal')) + ), + active.length + ? h('div', { class: 'col gap6', style: 'margin-top:4px' }, + h('span', { class: 'small muted' }, t('giftReserved', { n: active.length })), + ...active.map((id) => { + const u = wallet.utxos.find((x) => utxoId(x) === id); + return h('div', { class: 'row between' }, + h('span', { class: 'small mono' }, u ? fmtAmount(u.value) + ' ' + unitLabel() : id.slice(0, 12) + '…'), + h('button', { class: 'btn-sm', onClick: () => { wallet.unreserve(id); render(); } }, t('giftReclaim')) + ); + }) + ) + : null ); } @@ -850,21 +891,44 @@ function goHome() { } // ================================================================ GIFT / CLAIM -// Read a #claim=<code> gift link, returning the seed (and scrubbing it from the -// URL so it doesn't linger in the address bar or history). -function readClaimHash() { +// Read a #gift=<psbt> link, returning the code (and scrubbing it from the URL so +// the bearer instrument doesn't linger in the address bar / history). Validates +// that it decodes to a real gift. +function readGiftHash() { try { - const m = location.hash.match(/^#claim=([A-Za-z0-9_~-]+)$/); + const m = location.hash.match(/^#gift=([A-Za-z0-9_-]+)$/); if (!m) return null; history.replaceState(null, '', location.pathname + location.search); - return decodeGift(m[1]); + return previewGift(m[1]) ? m[1] : null; } 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). +// Broadcast the presigned gift to this fresh wallet's first receive address. +async function doClaim() { + if (wallet.offline) { ui.claimError = t('scanOffline'); render(); return; } + ui.busy = true; + ui.claimError = ''; + render(); + try { + const rate = (wallet.feeRates && wallet.feeRates.halfHourFee) || 5; + const to = wallet.receive[0] ? wallet.receive[0].address : wallet.derive(0, 0).address; + const claim = buildClaimTx(ui.claimCode, to, rate, wallet.netCfg.net); + await wallet.broadcast(claim.hex); + ui.claimedAmount = claim.amount; + ui.claimStep = 'backup'; + wallet.scan().catch(() => {}); + } catch (e) { + ui.claimError = t('claimFailed'); + } + ui.busy = false; + render(); +} + +// Gift-claim flow: a fresh wallet only the claimer controls. Step 'welcome' +// shows the amount + a Claim button; 'backup' (after a successful claim) shows +// the fresh recovery phrase to write down. function claimScreen() { if (ui.claimStep === 'backup') { const words = wallet.mnemonic.split(' '); @@ -872,6 +936,11 @@ function claimScreen() { 'div', { class: 'col', style: 'gap:16px' }, brandHeader(false), + h('div', { class: 'card col', style: 'align-items:center;text-align:center;gap:8px' }, + h('div', { class: 'check-badge', style: 'background:var(--accent)' }, '✓'), + h('h2', { style: 'margin:0' }, t('claimedTitle')), + h('p', { class: 'muted', style: 'margin:0' }, t('claimedBody')) + ), h('div', { class: 'card col' }, h('h3', {}, t('recoveryPhrase')), h('div', { class: 'warn-box' }, t('writeDownWarn')), @@ -883,6 +952,9 @@ function claimScreen() { ) ); } + const pv = previewGift(ui.claimCode); + const estFee = Math.ceil((11 + 68 + 31 * 2) * ((wallet.feeRates && wallet.feeRates.halfHourFee) || 5)); + const approx = pv ? Math.max(0, pv.room - estFee) : 0; return h( 'div', { class: 'col', style: 'gap:16px' }, @@ -890,11 +962,15 @@ function claimScreen() { 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')) + h('div', { class: 'amt', style: 'font-size:30px' }, + h('span', { class: 'amount-pos' }, '~' + fmtAmount(approx)), ' ', unitTag('unit') + ), + h('p', { class: 'muted', style: 'margin:0' }, t('claimBody')) ), - 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')) + ui.claimError && h('div', { class: 'notice err' }, ui.claimError), + ui.busy + ? h('button', { class: 'btn-primary btn-block', disabled: true }, h('span', { class: 'spinner' })) + : h('button', { class: 'btn-primary btn-block', onClick: doClaim }, t('claimBtn')) ); } @@ -1723,8 +1799,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; } + const code = readGiftHash(); + if (code) { enterWallet(newMnemonic(), '', { gift: code }); return; } if (!restoreSession()) render(); }); diff --git a/src/i18n.js b/src/i18n.js index 0c38520..017b6fa 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -145,15 +145,22 @@ const STR = { 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.', + giftLink: 'Gift some Bitcoin', + giftLinkDesc: 'Presign an amount as a link. Whoever opens it claims the Bitcoin into a brand-new wallet that only they control.', + giftLinkWarn: '⚠ Anyone who opens this link can claim the amount, so share it only with the person you are gifting. Keep amounts small.', giftLinkReveal: 'Create gift link', + giftAmountLabel: 'Amount to gift', + giftAmountInvalid: 'Enter a valid amount.', + giftAnother: 'Create another', + giftReserved: '{n} coin set aside for unclaimed gifts', + giftReclaim: 'Reclaim', copyLink: 'Copy link', + claimBody: 'Claim it into a brand-new wallet that only you control.', + claimBtn: 'Claim it', + claimFailed: 'This gift has already been claimed or is no longer available.', + claimedTitle: 'Claimed!', + claimedBody: 'Your Bitcoin is on its way. Back up your recovery phrase to keep it safe.', 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 b78e83d..57f2ea6 100644 --- a/src/locales/ar.json +++ b/src/locales/ar.json @@ -151,9 +151,19 @@ "giftBackup": "احتفظ بنسخة احتياطية من هذه المحفظة", "giftSkip": "تخطّي الآن", "giftBackedUp": "لقد دوّنتها", - "giftLink": "أهدِ هذه المحفظة", - "giftLinkDesc": "أنشئ رابطاً يفتح هذه المحفظة بأموالها، لتمكين شخص ما بسرعة.", - "giftLinkWarn": "⚠ أي شخص يفتح هذا الرابط يتحكم في جميع أموال هذه المحفظة. لا تشاركه إلا لإهداء المحفظة، وأبقِ المبلغ صغيراً.", + "giftLink": "أهدِ بعض عملات Bitcoin", + "giftLinkDesc": "وقّع مبلغاً مسبقاً كرابط. من يفتحه يطالب بعملات Bitcoin في محفظة جديدة تماماً يتحكم بها وحده.", + "giftLinkWarn": "⚠ أي شخص يفتح هذا الرابط يمكنه المطالبة بالمبلغ، لذا شاركه فقط مع الشخص الذي تهديه إياه. أبقِ المبالغ صغيرة.", + "giftAmountLabel": "المبلغ المراد إهداؤه", + "giftAmountInvalid": "أدخل مبلغاً صحيحاً.", + "giftAnother": "إنشاء آخر", + "giftReserved": "{n} عملة مخصّصة للهدايا غير المُطالَب بها", + "giftReclaim": "استرجاع", + "claimBody": "طالب بها في محفظة جديدة تماماً تتحكم بها وحدك.", + "claimBtn": "طالب بها", + "claimFailed": "تمت المطالبة بهذه الهدية بالفعل أو لم تعد متاحة.", + "claimedTitle": "تمت المطالبة!", + "claimedBody": "عملات Bitcoin الخاصة بك في طريقها إليك. احتفظ بنسخة احتياطية من عبارة الاسترداد لإبقائها آمنة.", "giftLinkReveal": "إنشاء رابط الإهداء", "copyLink": "نسخ الرابط" } diff --git a/src/locales/bn.json b/src/locales/bn.json index d9dfe42..30b92a4 100644 --- a/src/locales/bn.json +++ b/src/locales/bn.json @@ -151,9 +151,17 @@ "giftBackup": "এই ওয়ালেট ব্যাকআপ করুন", "giftSkip": "আপাতত এড়িয়ে যান", "giftBackedUp": "আমি এটি লিখে রেখেছি", - "giftLink": "এই ওয়ালেট উপহার দিন", - "giftLinkDesc": "একটি লিঙ্ক তৈরি করুন যা এই ওয়ালেট ও এর তহবিল খোলে, যাতে কাউকে দ্রুত শুরু করিয়ে দেওয়া যায়।", - "giftLinkWarn": "⚠ যে কেউ এই লিঙ্ক খুললে এই ওয়ালেটের সব তহবিল নিয়ন্ত্রণ করে। কেবল ওয়ালেট দিয়ে দিতেই এটি শেয়ার করুন এবং পরিমাণ কম রাখুন।", - "giftLinkReveal": "উপহার লিঙ্ক তৈরি করুন", - "copyLink": "লিঙ্ক কপি" + "giftLink": "কিছু Bitcoin উপহার দিন", + "giftLinkDesc": "একটি পরিমাণ লিঙ্ক হিসেবে আগে থেকে সই করুন। যে এটি খুলবে সে Bitcoin-টি একটি একদম নতুন ওয়ালেটে দাবি করে, যা কেবল সে-ই নিয়ন্ত্রণ করে।", + "giftLinkWarn": "⚠ যে কেউ এই লিঙ্ক খুললে পরিমাণটি দাবি করতে পারে, তাই কেবল যাকে উপহার দিচ্ছেন তার সাথেই এটি শেয়ার করুন। পরিমাণ কম রাখুন।", + "giftAmountLabel": "উপহারের পরিমাণ", + "giftAmountInvalid": "একটি সঠিক পরিমাণ লিখুন।", + "giftAnother": "আরেকটি তৈরি করুন", + "giftReserved": "{n} কয়েন অদাবিকৃত উপহারের জন্য সরিয়ে রাখা হয়েছে", + "giftReclaim": "ফেরত নিন", + "claimBody": "এটি একটি একদম নতুন ওয়ালেটে দাবি করুন, যা কেবল আপনিই নিয়ন্ত্রণ করেন।", + "claimBtn": "দাবি করুন", + "claimFailed": "এই উপহারটি আগেই দাবি করা হয়েছে অথবা আর পাওয়া যাচ্ছে না।", + "claimedTitle": "দাবি করা হয়েছে!", + "claimedBody": "আপনার Bitcoin আসছে। এটি নিরাপদ রাখতে আপনার রিকভারি ফ্রেজ ব্যাকআপ করে নিন।" } diff --git a/src/locales/de.json b/src/locales/de.json index 49b291b..0b7b11e 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -151,9 +151,19 @@ "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.", + "giftLink": "Bitcoin verschenken", + "giftLinkDesc": "Signiere einen Betrag im Voraus als Link. Wer ihn öffnet, beansprucht das Bitcoin in einer brandneuen Wallet, die nur er selbst kontrolliert.", + "giftLinkWarn": "⚠ Wer diesen Link öffnet, kann den Betrag beanspruchen. Teile ihn daher nur mit der Person, die du beschenkst. Halte die Beträge klein.", "giftLinkReveal": "Geschenk-Link erstellen", - "copyLink": "Link kopieren" + "copyLink": "Link kopieren", + "giftAmountLabel": "Zu verschenkender Betrag", + "giftAmountInvalid": "Gib einen gültigen Betrag ein.", + "giftAnother": "Weiteres erstellen", + "giftReserved": "{n} Coin für unbeanspruchte Geschenke zurückgelegt", + "giftReclaim": "Zurückfordern", + "claimBody": "Beanspruche es in einer brandneuen Wallet, die nur du selbst kontrollierst.", + "claimBtn": "Beanspruchen", + "claimFailed": "Dieses Geschenk wurde bereits beansprucht oder ist nicht mehr verfügbar.", + "claimedTitle": "Beansprucht!", + "claimedBody": "Dein Bitcoin ist unterwegs. Sichere deine Wiederherstellungsphrase, um es zu schützen." } diff --git a/src/locales/es.json b/src/locales/es.json index 1f62f13..e1f1d43 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -151,9 +151,19 @@ "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.", + "giftLink": "Regala algo de Bitcoin", + "giftLinkDesc": "Prefirma un importe como enlace. Quien lo abra reclama el Bitcoin en una billetera totalmente nueva que solo esa persona controla.", + "giftLinkWarn": "⚠ Cualquiera que abra este enlace puede reclamar el importe, así que compártelo solo con la persona a quien se lo regalas. Mantén los importes pequeños.", "giftLinkReveal": "Crear enlace de regalo", - "copyLink": "Copiar enlace" + "copyLink": "Copiar enlace", + "giftAmountLabel": "Importe a regalar", + "giftAmountInvalid": "Introduce un importe válido.", + "giftAnother": "Crear otro", + "giftReserved": "{n} de monedas reservadas para regalos sin reclamar", + "giftReclaim": "Recuperar", + "claimBody": "Reclámalo en una billetera totalmente nueva que solo tú controlas.", + "claimBtn": "Reclamarlo", + "claimFailed": "Este regalo ya ha sido reclamado o ya no está disponible.", + "claimedTitle": "¡Reclamado!", + "claimedBody": "Tu Bitcoin está en camino. Haz una copia de seguridad de tu frase de recuperación para mantenerlo a salvo." } diff --git a/src/locales/fr.json b/src/locales/fr.json index c23e5ff..8754e3a 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -151,9 +151,19 @@ "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.", + "giftLink": "Offrir des Bitcoin", + "giftLinkDesc": "Présignez un montant sous forme de lien. Celui qui l’ouvre récupère les Bitcoin dans un tout nouveau portefeuille qu’il est seul à contrôler.", + "giftLinkWarn": "⚠ Quiconque ouvre ce lien peut récupérer le montant, alors ne le partagez qu’avec la personne à qui vous l’offrez. Gardez les montants faibles.", "giftLinkReveal": "Créer le lien cadeau", - "copyLink": "Copier le lien" + "copyLink": "Copier le lien", + "giftAmountLabel": "Montant à offrir", + "giftAmountInvalid": "Saisissez un montant valide.", + "giftAnother": "En créer un autre", + "giftReserved": "{n} de fonds mis de côté pour des cadeaux non réclamés", + "giftReclaim": "Récupérer", + "claimBody": "Récupérez-les dans un tout nouveau portefeuille que vous seul contrôlez.", + "claimBtn": "Récupérer", + "claimFailed": "Ce cadeau a déjà été réclamé ou n’est plus disponible.", + "claimedTitle": "Récupéré !", + "claimedBody": "Vos Bitcoin sont en route. Sauvegardez votre phrase de récupération pour les garder en sécurité." }
\ No newline at end of file diff --git a/src/locales/hi.json b/src/locales/hi.json index 90a9872..b3b439f 100644 --- a/src/locales/hi.json +++ b/src/locales/hi.json @@ -151,9 +151,19 @@ "giftBackup": "इस वॉलेट का बैकअप लें", "giftSkip": "अभी के लिए छोड़ें", "giftBackedUp": "मैंने इसे लिख लिया है", - "giftLink": "यह वॉलेट उपहार में दें", - "giftLinkDesc": "एक ऐसा लिंक बनाएँ जो इस वॉलेट को इसके फंड के साथ खोले, किसी को तेज़ी से जोड़ने के लिए।", - "giftLinkWarn": "⚠ जो भी इस लिंक को खोलेगा वह इस वॉलेट के सभी फंड को नियंत्रित करता है। इसे केवल वॉलेट देने के लिए साझा करें, और राशि कम रखें।", + "giftLink": "कुछ Bitcoin उपहार में दें", + "giftLinkDesc": "एक राशि को लिंक के रूप में पहले से साइन करें। जो भी इसे खोलेगा, वह Bitcoin को एक बिल्कुल नए वॉलेट में प्राप्त कर लेगा जिसे केवल वही नियंत्रित करता है।", + "giftLinkWarn": "⚠ जो भी इस लिंक को खोलेगा वह राशि प्राप्त कर सकता है, इसलिए इसे केवल उसी व्यक्ति के साथ साझा करें जिसे आप उपहार दे रहे हैं। राशि कम रखें।", + "giftAmountLabel": "उपहार में देने की राशि", + "giftAmountInvalid": "एक मान्य राशि दर्ज करें।", + "giftAnother": "एक और बनाएँ", + "giftReserved": "{n} सिक्का बिना दावे वाले उपहारों के लिए अलग रखा गया", + "giftReclaim": "वापस लें", + "claimBody": "इसे एक बिल्कुल नए वॉलेट में प्राप्त करें जिसे केवल आप नियंत्रित करते हैं।", + "claimBtn": "इसे प्राप्त करें", + "claimFailed": "इस उपहार का दावा पहले ही किया जा चुका है या यह अब उपलब्ध नहीं है।", + "claimedTitle": "प्राप्त हो गया!", + "claimedBody": "आपका Bitcoin रास्ते में है। इसे सुरक्षित रखने के लिए अपने रिकवरी वाक्यांश का बैकअप लें।", "giftLinkReveal": "उपहार लिंक बनाएँ", "copyLink": "लिंक कॉपी करें" } diff --git a/src/locales/id.json b/src/locales/id.json index 9199318..7255730 100644 --- a/src/locales/id.json +++ b/src/locales/id.json @@ -151,9 +151,19 @@ "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.", + "giftLink": "Hadiahkan Bitcoin", + "giftLinkDesc": "Tandatangani sejumlah dana sebagai tautan. Siapa pun yang membukanya mengklaim Bitcoin itu ke dompet baru yang hanya dia kendalikan.", + "giftLinkWarn": "⚠ Siapa pun yang membuka tautan ini bisa mengklaim jumlahnya, jadi bagikan hanya kepada orang yang Anda beri hadiah. Jaga jumlahnya tetap kecil.", + "giftAmountLabel": "Jumlah yang dihadiahkan", + "giftAmountInvalid": "Masukkan jumlah yang valid.", + "giftAnother": "Buat lagi", + "giftReserved": "{n} koin disisihkan untuk hadiah yang belum diklaim", + "giftReclaim": "Ambil kembali", + "claimBody": "Klaim ke dompet baru yang hanya Anda kendalikan.", + "claimBtn": "Klaim", + "claimFailed": "Hadiah ini sudah diklaim atau tidak lagi tersedia.", + "claimedTitle": "Diklaim!", + "claimedBody": "Bitcoin Anda sedang dalam perjalanan. Cadangkan frasa pemulihan Anda agar tetap aman.", "giftLinkReveal": "Buat tautan hadiah", "copyLink": "Salin tautan" } diff --git a/src/locales/ja.json b/src/locales/ja.json index 2c13e28..346f096 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -151,9 +151,19 @@ "giftBackup": "このウォレットをバックアップ", "giftSkip": "今はスキップ", "giftBackedUp": "書き留めました", - "giftLink": "このウォレットを贈る", - "giftLinkDesc": "このウォレットを資金ごと開くリンクを作成し、すばやく誰かに使い始めてもらえます。", - "giftLinkWarn": "⚠ このリンクを開いた人は、このウォレット内のすべての資金を管理できます。ウォレットを譲り渡すときだけ共有し、金額は少なめにしてください。", + "giftLink": "Bitcoin を贈る", + "giftLinkDesc": "金額をリンクとして事前署名します。リンクを開いた人が、その人だけが管理できる真新しいウォレットに Bitcoin を受け取ります。", + "giftLinkWarn": "⚠ このリンクを開いた人は誰でもその金額を受け取れるので、贈る相手とだけ共有してください。金額は少なめにしましょう。", + "giftAmountLabel": "贈る金額", + "giftAmountInvalid": "有効な金額を入力してください。", + "giftAnother": "もう一つ作成", + "giftReserved": "未受取のギフト用に {n} のコインを確保中", + "giftReclaim": "回収", + "claimBody": "あなただけが管理できる真新しいウォレットに受け取りましょう。", + "claimBtn": "受け取る", + "claimFailed": "このギフトはすでに受け取られたか、現在利用できません。", + "claimedTitle": "受け取りました!", + "claimedBody": "Bitcoin が間もなく届きます。安全に保つため、復元フレーズをバックアップしてください。", "giftLinkReveal": "ギフトリンクを作成", "copyLink": "リンクをコピー" } diff --git a/src/locales/ko.json b/src/locales/ko.json index c131955..3d0face 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -151,9 +151,19 @@ "giftBackup": "이 지갑 백업하기", "giftSkip": "나중에 하기", "giftBackedUp": "적어 두었습니다", - "giftLink": "이 지갑 선물하기", - "giftLinkDesc": "자금이 들어 있는 이 지갑을 여는 링크를 만들어 빠르게 다른 사람을 시작하게 하세요.", - "giftLinkWarn": "⚠ 이 링크를 여는 사람은 누구나 이 지갑의 모든 자금을 통제합니다. 지갑을 넘겨줄 때만 공유하고, 금액은 적게 유지하세요.", + "giftLink": "Bitcoin 선물하기", + "giftLinkDesc": "금액을 링크로 미리 서명하세요. 링크를 여는 사람은 그 Bitcoin을 자신만 통제하는 완전히 새로운 지갑으로 받게 됩니다.", + "giftLinkWarn": "⚠ 이 링크를 여는 사람은 누구나 금액을 받을 수 있으니, 선물할 상대에게만 공유하세요. 금액은 적게 유지하세요.", + "giftAmountLabel": "선물할 금액", + "giftAmountInvalid": "올바른 금액을 입력하세요.", + "giftAnother": "또 만들기", + "giftReserved": "받지 않은 선물을 위해 따로 둔 코인 {n}개", + "giftReclaim": "되찾기", + "claimBody": "자신만 통제하는 완전히 새로운 지갑으로 받으세요.", + "claimBtn": "받기", + "claimFailed": "이 선물은 이미 수령되었거나 더 이상 이용할 수 없습니다.", + "claimedTitle": "수령 완료!", + "claimedBody": "Bitcoin이 전송되고 있습니다. 안전하게 보관하려면 복구 문구를 백업하세요.", "giftLinkReveal": "선물 링크 만들기", "copyLink": "링크 복사" }
\ No newline at end of file diff --git a/src/locales/mr.json b/src/locales/mr.json index 087f43b..f5f0e8e 100644 --- a/src/locales/mr.json +++ b/src/locales/mr.json @@ -151,9 +151,19 @@ "giftBackup": "हे वॉलेट बॅकअप करा", "giftSkip": "आत्ता वगळा", "giftBackedUp": "मी ते लिहून ठेवले आहे", - "giftLink": "हे वॉलेट भेट द्या", - "giftLinkDesc": "एखाद्याला झटपट सुरुवात करून देण्यासाठी हे वॉलेट त्याच्या निधीसह उघडणारी लिंक तयार करा.", - "giftLinkWarn": "⚠ ही लिंक उघडणारा कोणीही या वॉलेटमधील सर्व निधीवर नियंत्रण ठेवतो. फक्त वॉलेट देऊन टाकण्यासाठीच ती शेअर करा आणि रक्कम कमी ठेवा.", + "giftLink": "थोडे Bitcoin भेट द्या", + "giftLinkDesc": "एखादी रक्कम लिंक म्हणून आधीच स्वाक्षरित करा. ती उघडणारा कोणीही ते Bitcoin फक्त त्याच्याच नियंत्रणातील एका नव्या कोऱ्या वॉलेटमध्ये घेतो.", + "giftLinkWarn": "⚠ ही लिंक उघडणारा कोणीही ती रक्कम घेऊ शकतो, म्हणून ज्याला भेट देत आहात फक्त त्याच्यासोबतच ती शेअर करा. रक्कम कमी ठेवा.", + "giftAmountLabel": "भेट देण्याची रक्कम", + "giftAmountInvalid": "वैध रक्कम टाका.", + "giftAnother": "आणखी एक तयार करा", + "giftReserved": "{n} नाणी न घेतलेल्या भेटींसाठी बाजूला ठेवली", + "giftReclaim": "परत मिळवा", + "claimBody": "ते फक्त तुमच्याच नियंत्रणातील एका नव्या कोऱ्या वॉलेटमध्ये घ्या.", + "claimBtn": "ते घ्या", + "claimFailed": "ही भेट आधीच घेतली गेली आहे किंवा आता उपलब्ध नाही.", + "claimedTitle": "घेतले!", + "claimedBody": "तुमचे Bitcoin येत आहे. ते सुरक्षित ठेवण्यासाठी तुमच्या रिकव्हरी वाक्यांशाचा बॅकअप घ्या.", "giftLinkReveal": "भेट लिंक तयार करा", "copyLink": "लिंक कॉपी करा" } diff --git a/src/locales/pt.json b/src/locales/pt.json index c52eb42..b0b1093 100644 --- a/src/locales/pt.json +++ b/src/locales/pt.json @@ -151,9 +151,19 @@ "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.", + "giftLink": "Presentear Bitcoin", + "giftLinkDesc": "Pré-assine um valor como um link. Quem o abrir resgata os Bitcoin para uma carteira novinha que só essa pessoa controla.", + "giftLinkWarn": "⚠ Qualquer pessoa que abrir este link pode resgatar o valor, então compartilhe-o apenas com a pessoa que você está presenteando. Mantenha os valores baixos.", "giftLinkReveal": "Criar link de presente", - "copyLink": "Copiar link" + "copyLink": "Copiar link", + "giftAmountLabel": "Valor a presentear", + "giftAmountInvalid": "Digite um valor válido.", + "giftAnother": "Criar outro", + "giftReserved": "{n} de moeda reservada para presentes não resgatados", + "giftReclaim": "Recuperar", + "claimBody": "Resgate para uma carteira novinha que só você controla.", + "claimBtn": "Resgatar", + "claimFailed": "Este presente já foi resgatado ou não está mais disponível.", + "claimedTitle": "Resgatado!", + "claimedBody": "Seus Bitcoin estão a caminho. Faça backup da sua frase de recuperação para mantê-los seguros." }
\ No newline at end of file diff --git a/src/locales/ru.json b/src/locales/ru.json index 97d971a..5ef7967 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -151,9 +151,19 @@ "giftBackup": "Сделать резервную копию кошелька", "giftSkip": "Пропустить пока", "giftBackedUp": "Я записал(а)", - "giftLink": "Подарить этот кошелёк", - "giftLinkDesc": "Создайте ссылку, которая открывает этот кошелёк с его средствами, чтобы быстро подключить кого-то.", - "giftLinkWarn": "⚠ Любой, кто откроет эту ссылку, контролирует все средства в этом кошельке. Делитесь ею только чтобы отдать кошелёк, и держите сумму небольшой.", + "giftLink": "Подарить немного Bitcoin", + "giftLinkDesc": "Заранее подпишите сумму в виде ссылки. Тот, кто откроет её, получит Bitcoin в совершенно новый кошелёк, которым управляет только он.", + "giftLinkWarn": "⚠ Любой, кто откроет эту ссылку, может получить эту сумму, поэтому делитесь ею только с тем, кому дарите. Держите суммы небольшими.", + "giftAmountLabel": "Сумма подарка", + "giftAmountInvalid": "Введите корректную сумму.", + "giftAnother": "Создать ещё один", + "giftReserved": "{n} монет отложено для невостребованных подарков", + "giftReclaim": "Вернуть", + "claimBody": "Получите его в совершенно новый кошелёк, которым управляете только вы.", + "claimBtn": "Получить", + "claimFailed": "Этот подарок уже получен или больше недоступен.", + "claimedTitle": "Получено!", + "claimedBody": "Ваш Bitcoin уже в пути. Сделайте резервную копию фразы восстановления, чтобы сохранить его в безопасности.", "giftLinkReveal": "Создать ссылку-подарок", "copyLink": "Копировать ссылку" -}
\ No newline at end of file +} diff --git a/src/locales/sw.json b/src/locales/sw.json index 708b510..1f5c9e5 100644 --- a/src/locales/sw.json +++ b/src/locales/sw.json @@ -151,9 +151,19 @@ "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.", + "giftLink": "Toa Bitcoin kama zawadi", + "giftLinkDesc": "Tia saini kiasi mapema kama kiungo. Yeyote anayekifungua hudai Bitcoin kwenye pochi mpya kabisa anayoidhibiti yeye pekee.", + "giftLinkWarn": "⚠ Yeyote anayefungua kiungo hiki anaweza kudai kiasi hicho, kwa hivyo kishiriki tu na mtu unayempa zawadi. Weka kiasi kidogo.", + "giftAmountLabel": "Kiasi cha kutoa zawadi", + "giftAmountInvalid": "Ingiza kiasi sahihi.", + "giftAnother": "Tengeneza nyingine", + "giftReserved": "{n} sarafu imewekwa kando kwa zawadi ambazo hazijadaiwa", + "giftReclaim": "Dai tena", + "claimBody": "Idai kwenye pochi mpya kabisa unayoidhibiti wewe pekee.", + "claimBtn": "Idai", + "claimFailed": "Zawadi hii tayari imedaiwa au haipatikani tena.", + "claimedTitle": "Imedaiwa!", + "claimedBody": "Bitcoin yako inakuja. Hifadhi kifungu chako cha kurejesha ili kuilinda.", "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 9692963..6fce3fb 100644 --- a/src/locales/ta.json +++ b/src/locales/ta.json @@ -151,9 +151,19 @@ "giftBackup": "இந்த வாலட்டைக் காப்புப்பிரதி எடு", "giftSkip": "இப்போதைக்குத் தவிர்", "giftBackedUp": "நான் எழுதி வைத்துவிட்டேன்", - "giftLink": "இந்த வாலட்டைப் பரிசளி", - "giftLinkDesc": "ஒருவரை விரைவாக இணைக்க, அதன் நிதியுடன் இந்த வாலட்டைத் திறக்கும் ஒரு இணைப்பை உருவாக்கவும்.", - "giftLinkWarn": "⚠ இந்த இணைப்பைத் திறக்கும் எவரும் இந்த வாலட்டில் உள்ள அனைத்து நிதியையும் கட்டுப்படுத்துவார்கள். வாலட்டை விட்டுக்கொடுக்க மட்டுமே இதைப் பகிரவும், தொகையைக் குறைவாக வைத்திருங்கள்.", + "giftLink": "கொஞ்சம் Bitcoin பரிசளி", + "giftLinkDesc": "ஒரு தொகையை இணைப்பாக முன்கூட்டியே கையொப்பமிடுங்கள். இதைத் திறப்பவர், அவர்கள் மட்டுமே கட்டுப்படுத்தும் புத்தம்புதிய வாலட்டிற்குள் Bitcoin-ஐ உரிமை கொள்வார்கள்.", + "giftLinkWarn": "⚠ இந்த இணைப்பைத் திறக்கும் எவரும் தொகையை உரிமை கொள்ளலாம், எனவே நீங்கள் பரிசளிக்கும் நபருடன் மட்டுமே இதைப் பகிரவும். தொகையைக் குறைவாக வைத்திருங்கள்.", + "giftAmountLabel": "பரிசளிக்கும் தொகை", + "giftAmountInvalid": "சரியான தொகையை உள்ளிடவும்.", + "giftAnother": "மற்றொன்றை உருவாக்கு", + "giftReserved": "உரிமை கொள்ளப்படாத பரிசுகளுக்காக {n} நாணயம் ஒதுக்கப்பட்டுள்ளது", + "giftReclaim": "மீட்டெடு", + "claimBody": "நீங்கள் மட்டுமே கட்டுப்படுத்தும் புத்தம்புதிய வாலட்டிற்குள் இதை உரிமை கொள்ளுங்கள்.", + "claimBtn": "உரிமை கொள்", + "claimFailed": "இந்தப் பரிசு ஏற்கனவே உரிமை கொள்ளப்பட்டுவிட்டது அல்லது இனி கிடைக்கவில்லை.", + "claimedTitle": "உரிமை கொள்ளப்பட்டது!", + "claimedBody": "உங்கள் Bitcoin வந்துகொண்டிருக்கிறது. அதைப் பாதுகாப்பாக வைக்க உங்கள் மீட்பு சொற்றொடரைக் காப்புப்பிரதி எடுங்கள்.", "giftLinkReveal": "பரிசு இணைப்பை உருவாக்கு", "copyLink": "இணைப்பை நகலெடு" }
\ No newline at end of file diff --git a/src/locales/te.json b/src/locales/te.json index 24dcca9..06dcd72 100644 --- a/src/locales/te.json +++ b/src/locales/te.json @@ -151,9 +151,17 @@ "giftBackup": "ఈ వాలెట్ను బ్యాకప్ చేయండి", "giftSkip": "ప్రస్తుతానికి దాటవేయి", "giftBackedUp": "నేను దాన్ని రాసుకున్నాను", - "giftLink": "ఈ వాలెట్ను బహుమతిగా ఇవ్వండి", - "giftLinkDesc": "ఎవరినైనా త్వరగా ఆన్బోర్డ్ చేయడానికి, ఈ వాలెట్ను దాని నిధులతో సహా తెరిచే లింక్ను సృష్టించండి.", - "giftLinkWarn": "⚠ ఈ లింక్ తెరిచిన ఎవరైనా ఈ వాలెట్లోని అన్ని నిధులను నియంత్రిస్తారు. వాలెట్ను ఇచ్చేయడానికి మాత్రమే దీన్ని పంచుకోండి, మొత్తాన్ని తక్కువగా ఉంచండి.", - "giftLinkReveal": "బహుమతి లింక్ సృష్టించు", - "copyLink": "లింక్ కాపీ" + "giftLink": "కొంత Bitcoin బహుమతిగా ఇవ్వండి", + "giftLinkDesc": "ఒక మొత్తాన్ని ముందుగా సంతకం చేసి లింక్గా ఇవ్వండి. దాన్ని తెరిచినవారు ఆ Bitcoinను తాము మాత్రమే నియంత్రించే సరికొత్త వాలెట్లోకి క్లెయిమ్ చేసుకుంటారు.", + "giftLinkWarn": "⚠ ఈ లింక్ తెరిచిన ఎవరైనా ఆ మొత్తాన్ని క్లెయిమ్ చేయగలరు, కాబట్టి మీరు బహుమతి ఇస్తున్న వ్యక్తికి మాత్రమే దీన్ని పంచుకోండి. మొత్తాలను తక్కువగా ఉంచండి.", + "giftAmountLabel": "బహుమతిగా ఇచ్చే మొత్తం", + "giftAmountInvalid": "సరైన మొత్తాన్ని నమోదు చేయండి.", + "giftAnother": "మరొకటి సృష్టించు", + "giftReserved": "క్లెయిమ్ చేయని బహుమతుల కోసం {n} నాణేలు పక్కన పెట్టబడ్డాయి", + "giftReclaim": "తిరిగి పొందు", + "claimBody": "మీరు మాత్రమే నియంత్రించే సరికొత్త వాలెట్లోకి దాన్ని క్లెయిమ్ చేసుకోండి.", + "claimBtn": "క్లెయిమ్ చేసుకోండి", + "claimFailed": "ఈ బహుమతి ఇప్పటికే క్లెయిమ్ చేయబడింది లేదా ఇక అందుబాటులో లేదు.", + "claimedTitle": "క్లెయిమ్ అయింది!", + "claimedBody": "మీ Bitcoin మీ వద్దకు వస్తోంది. దాన్ని సురక్షితంగా ఉంచడానికి మీ రికవరీ ఫ్రేజ్ను బ్యాకప్ చేయండి." } diff --git a/src/locales/tr.json b/src/locales/tr.json index 430e89e..39eab0f 100644 --- a/src/locales/tr.json +++ b/src/locales/tr.json @@ -151,9 +151,19 @@ "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.", + "giftLink": "Bitcoin hediye et", + "giftLinkDesc": "Bir tutarı önceden imzalayıp bağlantı olarak verin. Bağlantıyı açan, Bitcoin’i yalnızca kendi kontrolündeki yepyeni bir cüzdana alır.", + "giftLinkWarn": "⚠ Bu bağlantıyı açan herkes tutarı talep edebilir, bu yüzden yalnızca hediye ettiğiniz kişiyle paylaşın. Tutarları küçük tutun.", + "giftAmountLabel": "Hediye edilecek tutar", + "giftAmountInvalid": "Geçerli bir tutar girin.", + "giftAnother": "Bir tane daha oluştur", + "giftReserved": "Talep edilmemiş hediyeler için {n} coin ayrıldı", + "giftReclaim": "Geri al", + "claimBody": "Yalnızca kendi kontrolünüzdeki yepyeni bir cüzdana alın.", + "claimBtn": "Talep et", + "claimFailed": "Bu hediye zaten talep edilmiş veya artık kullanılamıyor.", + "claimedTitle": "Talep edildi!", + "claimedBody": "Bitcoin’iniz yolda. Güvende tutmak için kurtarma ifadenizi yedekleyin.", "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 90b812b..417f9f8 100644 --- a/src/locales/ur.json +++ b/src/locales/ur.json @@ -151,9 +151,19 @@ "giftBackup": "اس والیٹ کا بیک اپ بنائیں", "giftSkip": "ابھی چھوڑیں", "giftBackedUp": "میں نے اسے لکھ لیا ہے", - "giftLink": "یہ والیٹ تحفے میں دیں", - "giftLinkDesc": "ایک لنک بنائیں جو یہ والیٹ اس کی رقم سمیت کھولے، تاکہ کسی کو تیزی سے شروع کرایا جا سکے۔", - "giftLinkWarn": "⚠ جو بھی یہ لنک کھولے وہ اس والیٹ کی تمام رقم کو کنٹرول کرتا ہے۔ اسے صرف والیٹ دینے کے لیے شیئر کریں اور رقم کم رکھیں۔", + "giftLink": "کچھ Bitcoin تحفے میں دیں", + "giftLinkDesc": "ایک رقم کو بطور لنک پہلے سے دستخط کریں۔ جو بھی اسے کھولے وہ Bitcoin کو ایک بالکل نئے والیٹ میں وصول کر لیتا ہے جو صرف اسی کے کنٹرول میں ہوتا ہے۔", + "giftLinkWarn": "⚠ جو بھی یہ لنک کھولے وہ اس رقم کا دعویٰ کر سکتا ہے، اس لیے اسے صرف اسی شخص کے ساتھ شیئر کریں جسے آپ تحفہ دے رہے ہیں۔ رقم کم رکھیں۔", "giftLinkReveal": "تحفہ لنک بنائیں", - "copyLink": "لنک کاپی کریں" + "copyLink": "لنک کاپی کریں", + "giftAmountLabel": "تحفے کی رقم", + "giftAmountInvalid": "درست رقم درج کریں۔", + "giftAnother": "ایک اور بنائیں", + "giftReserved": "{n} سکے غیر وصول شدہ تحائف کے لیے الگ رکھے گئے", + "giftReclaim": "واپس لیں", + "claimBody": "اسے ایک بالکل نئے والیٹ میں وصول کریں جو صرف آپ کے کنٹرول میں ہو۔", + "claimBtn": "وصول کریں", + "claimFailed": "اس تحفے کا پہلے ہی دعویٰ کیا جا چکا ہے یا یہ اب دستیاب نہیں۔", + "claimedTitle": "وصول ہو گیا!", + "claimedBody": "آپ کا Bitcoin راستے میں ہے۔ اسے محفوظ رکھنے کے لیے اپنے بحالی فقرے کا بیک اپ بنا لیں۔" } diff --git a/src/locales/vi.json b/src/locales/vi.json index 6882d18..2c7786f 100644 --- a/src/locales/vi.json +++ b/src/locales/vi.json @@ -151,9 +151,19 @@ "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ỏ.", + "giftLink": "Tặng một ít Bitcoin", + "giftLinkDesc": "Ký trước một số tiền thành một liên kết. Ai mở liên kết sẽ nhận số Bitcoin đó vào một ví hoàn toàn mới mà chỉ riêng họ kiểm soát.", + "giftLinkWarn": "⚠ Bất kỳ ai mở liên kết này đều có thể nhận số tiền, vì vậy chỉ chia sẻ với người bạn muốn tặng. Hãy giữ số tiền nhỏ.", + "giftAmountLabel": "Số tiền muốn tặng", + "giftAmountInvalid": "Nhập một số tiền hợp lệ.", + "giftAnother": "Tạo cái khác", + "giftReserved": "{n} coin được để dành cho các món quà chưa nhận", + "giftReclaim": "Lấy lại", + "claimBody": "Nhận nó vào một ví hoàn toàn mới mà chỉ riêng bạn kiểm soát.", + "claimBtn": "Nhận ngay", + "claimFailed": "Món quà này đã được nhận hoặc không còn khả dụng.", + "claimedTitle": "Đã nhận!", + "claimedBody": "Bitcoin của bạn đang trên đường tới. Hãy sao lưu cụm từ khôi phục để giữ an toàn.", "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 b5740b8..6de1544 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -151,9 +151,19 @@ "giftBackup": "备份此钱包", "giftSkip": "暂时跳过", "giftBackedUp": "我已经抄写下来了", - "giftLink": "赠送此钱包", - "giftLinkDesc": "创建一个能打开此钱包(含其资金)的链接,方便快速帮人上手。", - "giftLinkWarn": "⚠ 任何打开此链接的人都能控制本钱包中的全部资金。仅在赠送钱包时分享它,并请保持金额较小。", + "giftLink": "赠送一些 Bitcoin", + "giftLinkDesc": "将一笔金额预签为一个链接。打开链接的人可将这笔 Bitcoin 领取到一个仅由他们自己掌控的全新钱包中。", + "giftLinkWarn": "⚠ 任何打开此链接的人都能领取这笔金额,所以请仅分享给你要赠送的对象。请保持金额较小。", + "giftAmountLabel": "赠送金额", + "giftAmountInvalid": "请输入有效金额。", + "giftAnother": "再创建一个", + "giftReserved": "{n} 枚币已预留给未领取的礼物", + "giftReclaim": "收回", + "claimBody": "将它领取到一个仅由你自己掌控的全新钱包中。", + "claimBtn": "领取", + "claimFailed": "此礼物已被领取或不再可用。", + "claimedTitle": "已领取!", + "claimedBody": "你的 Bitcoin 正在到账。请备份你的恢复助记词以妥善保管。", "giftLinkReveal": "创建赠送链接", "copyLink": "复制链接" }
\ No newline at end of file diff --git a/src/wallet.js b/src/wallet.js index 08f5d9e..06af3dc 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -9,7 +9,7 @@ // transactions, only the (txid, vout, value, address) of each UTXO. import { HDKey } from '@scure/bip32'; -import { generateMnemonic, mnemonicToSeedSync, validateMnemonic, mnemonicToEntropy, entropyToMnemonic } from '@scure/bip39'; +import { generateMnemonic, mnemonicToSeedSync, validateMnemonic } from '@scure/bip39'; import { wordlist } from '@scure/bip39/wordlists/english'; import { hex, base64urlnopad } from '@scure/base'; import { sha256 } from '@noble/hashes/sha256'; @@ -120,6 +120,7 @@ export class Wallet { this.api.offline = offline; this._account = null; this._addrCache.clear(); + this._reserved = null; // gift-reserved outpoints, lazy-loaded per wallet this._savedAt = 0; try { if (this.mnemonic) this.nostr.load(this.mnemonic, this.passphrase); @@ -577,7 +578,7 @@ export class Wallet { buildTx({ recipients, feeRate, coinIds = null, sendMax = false }) { const pool_ = coinIds ? this.utxos.filter((u) => coinIds.includes(utxoId(u))) - : this.utxos.slice(); + : this.utxos.filter((u) => !this.isReserved(utxoId(u))); // skip coins set aside for gifts if (!pool_.length) throw new Error('No spendable coins selected.'); const inputs = pool_.map((u) => { @@ -758,6 +759,72 @@ export class Wallet { return { hex: t.hex, txid: t.id, fee: pl.fee, oldFee: prep.oldFee, feeRate: pl.rate, outputs, replaces: prep.txid }; } + // --- gift links (coins set aside as claimable presigned transactions) ---- + // Outpoints reserved for unclaimed gifts are persisted per-wallet and skipped + // by normal coin selection so a send can't spend a gift out from under it. + _giftKey() { + return this._cacheKey() + ':gift'; + } + reservedSet() { + if (!this._reserved) { + try { + this._reserved = new Set(JSON.parse(localStorage.getItem(this._giftKey()) || '[]')); + } catch { + this._reserved = new Set(); + } + } + return this._reserved; + } + isReserved(id) { + return this.reservedSet().has(id); + } + _saveReserved() { + try { + localStorage.setItem(this._giftKey(), JSON.stringify([...this.reservedSet()])); + } catch {} + } + unreserve(id) { + this.reservedSet().delete(id); + this._saveReserved(); + } + // Active gifts whose coin is still unspent (drops claimed/spent ones). + activeGifts() { + const live = new Set(this.utxos.map((u) => utxoId(u))); + let changed = false; + for (const id of [...this.reservedSet()]) if (!live.has(id)) { this.reservedSet().delete(id); changed = true; } + if (changed) this._saveReserved(); + return [...this.reservedSet()]; + } + + // Build a SIGHASH_SINGLE-signed gift: input = a single coin, output0 = change + // back to us (cryptographically fixed), leaving the rest for the claimer to + // direct to their own fresh address. Reserves the coin. Returns { code, ... }. + createGift(amountSats, feeRate) { + const gift = BigInt(Math.round(amountSats)); + if (gift < 546n) throw new Error('Gift amount is too small.'); + const rate = Math.max(1, Math.round(feeRate)); + const claimFee = BigInt(Math.ceil((11 + 68 + 31 * 2) * rate)); // claimer's 1-in 2-out tx + const DUST = 294n; + const need = gift + claimFee + DUST; + const coin = this.utxos + .filter((u) => u.confirmed && !this.isReserved(utxoId(u))) + .sort((a, b) => a.value - b.value) + .find((u) => BigInt(u.value) >= need); + if (!coin) throw new Error('No single confirmed coin is large enough for that amount.'); + + const V = BigInt(coin.value); + const change = V - gift - claimFee; // back to us, >= DUST by construction + const pay = p2wpkh(this.derive(coin.chain, coin.index).pubkey, this.netCfg.net); + const t = new btc.Transaction({ allowUnknownOutputs: true }); + t.addInput({ ...pay, txid: coin.txid, index: coin.vout, sighashType: btc.SigHash.SINGLE, witnessUtxo: { script: pay.script, amount: V } }); + t.addOutputAddress(this.freshChange().address, change, this.netCfg.net); + t.signIdx(this.node(coin.chain, coin.index).privateKey, 0, [btc.SigHash.SINGLE]); + + this.reservedSet().add(utxoId(coin)); + this._saveReserved(); + return { code: base64urlnopad.encode(t.toPSBT()), amount: Number(gift), reserved: utxoId(coin) }; + } + // --- realtime (mempool.space WebSocket) --------------------------------- // Pushes us new mempool/confirmed transactions for our addresses so history // and balances update with no polling. @@ -1185,27 +1252,34 @@ 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) { +// Gift-link claiming (sender side is Wallet.createGift). A gift code is a +// base64url PSBT: one SIGHASH_SINGLE-signed input + the sender's change output. +// previewGift reports the room available to the claimer (before their fee); +// buildClaimTx adds the claimer's output and finalizes a broadcastable tx. +export function previewGift(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 }; + const tx = btc.Transaction.fromPSBT(base64urlnopad.decode(code)); + const inAmt = Number(tx.getInput(0).witnessUtxo.amount); + const change = Number(tx.getOutput(0).amount); + return { room: inAmt - change }; // claimer gets room − their chosen fee } catch { return null; } } +export function buildClaimTx(code, toAddress, feeRate, net) { + const tx = btc.Transaction.fromPSBT(base64urlnopad.decode(code)); + const inAmt = tx.getInput(0).witnessUtxo.amount; + const change = tx.getOutput(0).amount; + const room = inAmt - change; + const fee = BigInt(Math.max(1, Math.ceil((11 + 68 + 31 * 2) * Math.max(1, Math.round(feeRate))))); + const out = room - fee; + if (out < 294n) throw new Error('Gift is too small to claim at this fee rate.'); + tx.addOutputAddress(toAddress, out, net); + tx.finalize(); + return { hex: tx.hex, txid: tx.id, amount: Number(out), fee: Number(fee) }; +} + function firstUnused(chain) { const u = chain.find((a) => !a.used); return u ? u.index : chain.length; |
