aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-06-24 14:41:36 -0700
committerAdam Soltys <[email protected]>2026-06-24 14:41:36 -0700
commitc81616ed0024c0cec693b552de5c5b6d7056eff3 (patch)
treea26b493f6590f3be33569c62f4f2769c60c32662 /src
parent3463af1d81c2712d657bbf30ecba98ffe5873729 (diff)
downloadcoinosv3-c81616ed0024c0cec693b552de5c5b6d7056eff3.tar.gz
coinosv3-c81616ed0024c0cec693b552de5c5b6d7056eff3.zip
Add account switching and watch-only wallets
Accounts: a switcher (header chip → Accounts screen) lets you keep several wallets loaded and flip between them without re-importing. Per the chosen model, full (seed) accounts live only in sessionStorage — ephemeral, no seed on disk; watch-only accounts (no seed) are persisted in localStorage and reload across restarts. enterWallet is refactored around an accounts list (add/switch/remove/lock); Add wallet reaches the unlock screen with a back button. Watch-only: load any wallet from its account xpub/zpub (parseAccountKey normalizes zpub→xpub; the existing public-derivation path yields the same addresses). Signing is blocked, the Send tab and seed/sync/offline cards are hidden, and a watch-only note is shown. Settings gains a "Public key" card (export this wallet's zpub) so it can be watched read-only elsewhere. Password-encrypted persistence for full accounts is the planned next step. New strings translated across all 19 locales. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Diffstat (limited to 'src')
-rw-r--r--src/app.js321
-rw-r--r--src/i18n.js17
-rw-r--r--src/locales/ar.json19
-rw-r--r--src/locales/bn.json19
-rw-r--r--src/locales/de.json20
-rw-r--r--src/locales/es.json18
-rw-r--r--src/locales/fr.json18
-rw-r--r--src/locales/hi.json19
-rw-r--r--src/locales/id.json18
-rw-r--r--src/locales/ja.json20
-rw-r--r--src/locales/ko.json19
-rw-r--r--src/locales/mr.json19
-rw-r--r--src/locales/pt.json19
-rw-r--r--src/locales/ru.json19
-rw-r--r--src/locales/sw.json19
-rw-r--r--src/locales/ta.json19
-rw-r--r--src/locales/te.json19
-rw-r--r--src/locales/tr.json18
-rw-r--r--src/locales/ur.json19
-rw-r--r--src/locales/vi.json18
-rw-r--r--src/locales/zh.json19
-rw-r--r--src/wallet.js62
22 files changed, 664 insertions, 94 deletions
diff --git a/src/app.js b/src/app.js
index 98b5d3a..8e2359e 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, previewGift, buildClaimTx, giftMinimum } from './wallet.js';
+import { Wallet, newMnemonic, isValidMnemonic, utxoId, previewGift, buildClaimTx, giftMinimum, parseAccountKey, xpubToZpub } from './wallet.js';
import { qrSvg } from './qr.js';
import { scanQr } from './scan.js';
import { getSyncConfig, setSyncConfig } from './nostr.js';
@@ -26,7 +26,10 @@ const ui = {
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'
+ unlockTab: 'create', // 'create' | 'import' | 'watch'
+ watchXpub: '', // watch-only xpub/zpub input
+ watchLabel: '', // watch-only account label input
+ fromWallet: false, // unlock screen reached as "add wallet" (show a back button)
createStep: 'gen', // 'gen' | 'confirm'
draftMnemonic: '',
confirm: [], // [{ index, value }]
@@ -35,6 +38,7 @@ const ui = {
passphrase: '',
showPass: false,
revealShown: false, // recovery phrase unmasked on the Backup tab (after the warning)
+ pubkeyShown: false, // account public key revealed in Settings
giftMode: false, // gift sub-view active on the Send page
giftAmount: '', // gift-create amount input
giftCode: null, // last-created gift PSBT code
@@ -157,11 +161,13 @@ function render() {
const screen =
ui.screen === 'wallet'
? walletScreen()
- : ui.screen === 'claim'
- ? claimScreen()
- : ui.screen === 'howItWorks'
- ? howItWorksScreen()
- : unlockScreen();
+ : ui.screen === 'accounts'
+ ? accountsScreen()
+ : ui.screen === 'claim'
+ ? claimScreen()
+ : ui.screen === 'howItWorks'
+ ? howItWorksScreen()
+ : unlockScreen();
root.replaceChildren(screen, footer());
}
wallet.subscribe(render);
@@ -249,21 +255,54 @@ function unlockScreen() {
h(
'div',
{ class: 'tabs' },
- tabBtn(t('createNew'), ui.unlockTab === 'create', () => {
- ui.unlockTab = 'create';
- render();
- }),
- tabBtn(t('importExisting'), ui.unlockTab === 'import', () => {
- ui.unlockTab = 'import';
- render();
- })
+ tabBtn(t('createNew'), ui.unlockTab === 'create', () => { ui.unlockTab = 'create'; ui.unlockError = ''; render(); }),
+ tabBtn(t('importExisting'), ui.unlockTab === 'import', () => { ui.unlockTab = 'import'; ui.unlockError = ''; render(); }),
+ tabBtn(t('watchOnly'), ui.unlockTab === 'watch', () => { ui.unlockTab = 'watch'; ui.unlockError = ''; render(); })
),
- ui.unlockTab === 'create' ? createPane() : importPane(),
+ ui.unlockTab === 'create' ? createPane() : ui.unlockTab === 'import' ? importPane() : watchPane(),
ui.unlockError && h('div', { class: 'notice err' }, ui.unlockError)
- )
+ ),
+ ui.fromWallet && accounts.length
+ ? h('button', { class: 'btn-ghost btn-block', onClick: () => { ui.fromWallet = false; ui.screen = 'wallet'; ui.unlockError = ''; render(); } }, t('back'))
+ : null
);
}
+function watchPane() {
+ return h(
+ 'div',
+ { class: 'col', style: 'gap:12px; margin-top:14px' },
+ h('p', { class: 'small muted', style: 'margin:0' }, t('watchOnlyDesc')),
+ h('div', { class: 'field' },
+ h('span', { class: 'lab' }, t('watchXpubLabel')),
+ h('textarea', { rows: '3', placeholder: 'zpub… / xpub…', value: ui.watchXpub, onInput: (e) => (ui.watchXpub = e.target.value) })
+ ),
+ h('div', { class: 'field' },
+ h('span', { class: 'lab' }, t('labelOptional')),
+ h('input', { type: 'text', placeholder: defaultLabel('watch'), value: ui.watchLabel, onInput: (e) => (ui.watchLabel = e.target.value) })
+ ),
+ h('button', { class: 'btn-primary btn-block', onClick: addWatchOnly }, t('watchOnlyAdd'))
+ );
+}
+
+function addWatchOnly() {
+ ui.unlockError = '';
+ let xpub;
+ try {
+ xpub = parseAccountKey(ui.watchXpub);
+ } catch (e) {
+ ui.unlockError = e.message;
+ render();
+ return;
+ }
+ const label = (ui.watchLabel || '').trim() || defaultLabel('watch');
+ const acc = addOrGetAccount({ type: 'watch', label, xpub });
+ ui.watchXpub = '';
+ ui.watchLabel = '';
+ ui.fromWallet = false;
+ activateAccount(acc);
+}
+
// ================================================================ HOW IT WORKS
function howItWorksScreen() {
const back = () => {
@@ -501,11 +540,24 @@ async function openWallet(mnemonic) {
await enterWallet(m, ui.passphrase);
}
-// Load a wallet and start scanning. Persists to sessionStorage so a refresh
-// restores the open wallet (cleared on logout or when the tab closes).
+// Register a full (seed) wallet as an account and open it.
async function enterWallet(mnemonic, passphrase, opts = {}) {
- wallet.load({ mnemonic, passphrase, netName: 'mainnet', offline: false });
- persistSession(mnemonic, passphrase);
+ const acc = addOrGetAccount({
+ type: 'full',
+ label: defaultLabel('full'),
+ mnemonic: (mnemonic || '').trim().replace(/\s+/g, ' '),
+ passphrase: passphrase || '',
+ });
+ await activateAccount(acc, opts);
+}
+
+// Load an account into the wallet and start scanning. Full-account seeds are
+// kept in sessionStorage (ephemeral); a refresh restores the open account.
+async function activateAccount(acc, opts = {}) {
+ activeId = acc.id;
+ if (acc.type === 'watch') wallet.load({ xpub: acc.xpub, netName: 'mainnet', offline: false });
+ else wallet.load({ mnemonic: acc.mnemonic, passphrase: acc.passphrase || '', netName: 'mainnet', offline: false });
+ persistAccounts();
const hadCache = wallet.restoreCache(); // show last-known balance/history instantly
// An opened gift link starts on a claim/back-up screen instead of the wallet.
ui.screen = opts.gift ? 'claim' : 'wallet';
@@ -520,6 +572,7 @@ async function enterWallet(mnemonic, passphrase, opts = {}) {
ui.send = blankSend();
ui.draft = null;
ui.sendResult = null;
+ ui.giftMode = false;
ui.offlineFallback = false;
render();
@@ -531,8 +584,9 @@ async function enterWallet(mnemonic, passphrase, opts = {}) {
}
try {
// Cross-device state: pull the latest from Nostr (may supply state on a
- // device with no local cache, or a newer copy from another device).
- const hadNostr = await wallet.syncFromNostr();
+ // device with no local cache, or a newer copy from another device). Skipped
+ // for watch-only accounts, which have no seed-derived sync key.
+ const hadNostr = wallet.watchOnly ? false : await wallet.syncFromNostr();
// Always do a full scan to fully reconcile (balance, history confirmations,
// spends, stale cache/relay state). Cache/Nostr just gave us something to
// show instantly; a partial frontier-only refresh leaves the state stale and
@@ -553,28 +607,91 @@ async function enterWallet(mnemonic, passphrase, opts = {}) {
}
}
-const SESSION_KEY = 'btc-wallet-session';
+// --- accounts -------------------------------------------------------------
+// The working set of wallets you can switch between. Full (seed-bearing)
+// accounts live only in sessionStorage — ephemeral, wiped when the browser
+// closes (no seed on disk by default). Watch-only accounts hold just an xpub,
+// so they're additionally persisted in localStorage and reload across restarts.
+const ACCOUNTS_KEY = 'btc-wallet-accounts'; // sessionStorage: session list + active
+const WATCH_KEY = 'btc-wallet-watch'; // localStorage: persisted watch-only accounts
-function persistSession(mnemonic, passphrase) {
- try {
- sessionStorage.setItem(SESSION_KEY, JSON.stringify({ mnemonic, passphrase }));
- } catch {}
+let accounts = [];
+let activeId = null;
+
+const genId = () => 'a' + Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
+const credId = (a) => (a.type === 'watch' ? 'w:' + a.xpub : 'f:' + a.mnemonic + '|' + (a.passphrase || ''));
+const activeAccount = () => accounts.find((a) => a.id === activeId) || null;
+
+function defaultLabel(type) {
+ const n = accounts.filter((a) => a.type === type).length + 1;
+ return t(type === 'watch' ? 'watchLabelN' : 'walletLabelN', { n });
}
-function clearSession() {
+function loadWatchAccounts() {
+ try { return JSON.parse(localStorage.getItem(WATCH_KEY) || '[]'); } catch { return []; }
+}
+function saveWatchAccounts() {
try {
- sessionStorage.removeItem(SESSION_KEY);
+ const watch = accounts.filter((a) => a.type === 'watch').map((a) => ({ id: a.id, label: a.label, type: 'watch', xpub: a.xpub }));
+ localStorage.setItem(WATCH_KEY, JSON.stringify(watch));
} catch {}
}
+function persistAccounts() {
+ try { sessionStorage.setItem(ACCOUNTS_KEY, JSON.stringify({ accounts, activeId })); } catch {}
+}
+function clearAccounts() {
+ accounts = [];
+ activeId = null;
+ try { sessionStorage.removeItem(ACCOUNTS_KEY); } catch {}
+}
-// Restore an open wallet after a page refresh (same tab session only).
-function restoreSession() {
- let saved;
- try {
- saved = JSON.parse(sessionStorage.getItem(SESSION_KEY) || 'null');
- } catch {}
- if (saved && saved.mnemonic && isValidMnemonic(saved.mnemonic)) {
- enterWallet(saved.mnemonic, saved.passphrase || '');
+// Add an account (deduped by credential), returning the stored object.
+function addOrGetAccount(partial) {
+ const cid = credId(partial);
+ let acc = accounts.find((a) => credId(a) === cid);
+ if (!acc) {
+ acc = { id: genId(), ...partial };
+ accounts.push(acc);
+ if (acc.type === 'watch') saveWatchAccounts();
+ persistAccounts();
+ }
+ return acc;
+}
+
+function removeAccount(id) {
+ const acc = accounts.find((a) => a.id === id);
+ if (!acc) return;
+ accounts = accounts.filter((a) => a.id !== id);
+ if (acc.type === 'watch') saveWatchAccounts();
+ persistAccounts();
+ if (activeId === id) {
+ if (accounts.length) activateAccount(accounts[0]);
+ else lock();
+ } else {
+ render();
+ }
+}
+
+function switchAccount(id) {
+ const acc = accounts.find((a) => a.id === id);
+ if (acc) activateAccount(acc);
+}
+
+// Restore accounts after a refresh (sessionStorage), or on a fresh session seed
+// the list from persisted watch-only accounts. Returns true if one was opened.
+function restoreAccountsState() {
+ let sess = null;
+ try { sess = JSON.parse(sessionStorage.getItem(ACCOUNTS_KEY) || 'null'); } catch {}
+ if (sess && Array.isArray(sess.accounts) && sess.accounts.length) {
+ accounts = sess.accounts;
+ const active = accounts.find((a) => a.id === sess.activeId) || accounts[0];
+ activateAccount(active);
+ return true;
+ }
+ const watch = loadWatchAccounts();
+ if (watch.length) {
+ accounts = watch.slice();
+ activateAccount(accounts[0]);
return true;
}
return false;
@@ -603,16 +720,21 @@ async function retryOnline() {
function lock() {
wallet.stopRealtime();
- clearSession();
+ clearAccounts();
wallet.load({ mnemonic: '', passphrase: '', netName: 'mainnet', offline: false });
wallet.mnemonic = '';
ui.screen = 'unlock';
+ ui.unlockTab = 'create';
+ ui.fromWallet = false;
+ ui.watchXpub = '';
+ ui.watchLabel = '';
ui.createStep = 'gen';
ui.draftMnemonic = '';
ui.importText = '';
ui.passphrase = '';
ui.confirm = [];
ui.revealShown = false;
+ ui.pubkeyShown = false;
ui.giftMode = false;
ui.giftAmount = '';
ui.giftCode = null;
@@ -627,6 +749,7 @@ function lock() {
// ================================================================ WALLET
function brandHeader(withLock) {
+ const acc = activeAccount();
return h(
'div',
{ class: 'row between' },
@@ -636,7 +759,9 @@ function brandHeader(withLock) {
h('div', { class: 'logo' }, '₿'),
h('h1', {}, t('appTitle'))
),
- withLock && h('button', { class: 'btn-sm', onClick: lock }, t('logout'))
+ withLock &&
+ h('button', { class: 'btn-sm', onClick: () => { ui.screen = 'accounts'; render(); } },
+ (acc ? acc.label : t('accounts')) + ' ▾')
);
}
@@ -657,32 +782,40 @@ function settingsTab() {
return h(
'div',
{ class: 'col', style: 'gap:16px' },
- h(
- 'div',
- { class: 'card col' },
- h('h3', {}, t('recoveryPhrase')),
- h('div', { class: 'warn-box' }, t('recoveryWarn')),
- h('div', { class: 'words' }, cells),
- shown && wallet.passphrase
- ? h('div', { class: 'col gap6' },
- h('span', { class: 'lab' }, t('bip39Passphrase')),
- h('div', { class: 'addr-box' }, wallet.passphrase)
- )
- : null,
- shown
- ? h('div', { class: 'row gap6 wrap' },
- copyBtn(wallet.mnemonic, t('copyPhrase')),
- wallet.passphrase ? copyBtn(wallet.passphrase, t('copyPassphrase')) : null,
- h('button', { class: 'btn-sm grow', onClick: () => { ui.revealShown = false; render(); } }, t('hide'))
- )
- : h('button', { class: 'btn-primary btn-block', onClick: () => { ui.revealShown = true; render(); } }, t('revealRecovery'))
- ),
- h(
- 'div',
- { class: 'card col' },
- h('h3', {}, t('offlineTransfer')),
- snapshotActions()
- ),
+ wallet.watchOnly
+ ? h('div', { class: 'card col' },
+ h('h3', {}, t('watchOnly')),
+ h('p', { class: 'small muted', style: 'margin:0' }, t('watchOnlyNote'))
+ )
+ : h(
+ 'div',
+ { class: 'card col' },
+ h('h3', {}, t('recoveryPhrase')),
+ h('div', { class: 'warn-box' }, t('recoveryWarn')),
+ h('div', { class: 'words' }, cells),
+ shown && wallet.passphrase
+ ? h('div', { class: 'col gap6' },
+ h('span', { class: 'lab' }, t('bip39Passphrase')),
+ h('div', { class: 'addr-box' }, wallet.passphrase)
+ )
+ : null,
+ shown
+ ? h('div', { class: 'row gap6 wrap' },
+ copyBtn(wallet.mnemonic, t('copyPhrase')),
+ wallet.passphrase ? copyBtn(wallet.passphrase, t('copyPassphrase')) : null,
+ h('button', { class: 'btn-sm grow', onClick: () => { ui.revealShown = false; render(); } }, t('hide'))
+ )
+ : h('button', { class: 'btn-primary btn-block', onClick: () => { ui.revealShown = true; render(); } }, t('revealRecovery'))
+ ),
+ pubkeyCard(),
+ wallet.watchOnly
+ ? null
+ : h(
+ 'div',
+ { class: 'card col' },
+ h('h3', {}, t('offlineTransfer')),
+ snapshotActions()
+ ),
h(
'div',
{ class: 'card col' },
@@ -695,7 +828,7 @@ function settingsTab() {
wallet.scanning ? t('scanning') : t('rescanWallet'))
),
explorerCard(),
- syncCard(),
+ wallet.watchOnly ? null : syncCard(),
h(
'div',
{ class: 'card col' },
@@ -705,6 +838,28 @@ function settingsTab() {
);
}
+// Public key export: the account zpub, for watching this wallet elsewhere
+// (read-only). Gated behind a reveal since it exposes all your addresses.
+function pubkeyCard() {
+ let zpub = '';
+ try { zpub = xpubToZpub(wallet.accountXpub()); } catch {}
+ return h(
+ 'div',
+ { class: 'card col' },
+ h('h3', {}, t('publicKey')),
+ h('p', { class: 'small muted', style: 'margin:0' }, t('publicKeyDesc')),
+ ui.pubkeyShown
+ ? h('div', { class: 'col gap6' },
+ h('div', { class: 'addr-box break', style: 'font-size:12px' }, zpub),
+ h('div', { class: 'row gap6 wrap' },
+ copyBtn(zpub, t('copyKey')),
+ h('button', { class: 'btn-sm grow', onClick: () => { ui.pubkeyShown = false; render(); } }, t('hide'))
+ )
+ )
+ : h('button', { class: 'btn-block', onClick: () => { ui.pubkeyShown = true; render(); } }, t('showPublicKey'))
+ );
+}
+
// 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() {
@@ -1022,6 +1177,33 @@ function claimScreen() {
);
}
+// Account switcher: pick a wallet, add another, or lock the session.
+function accountsScreen() {
+ return h(
+ 'div',
+ { class: 'col', style: 'gap:16px' },
+ brandHeader(false),
+ h('div', { class: 'card col' },
+ h('h3', {}, t('accounts')),
+ h('div', { class: 'col', style: 'gap:0' },
+ accounts.map((a) => {
+ const isActive = a.id === activeId;
+ return h('div', { class: 'row between', style: 'padding:10px 0;border-bottom:1px solid var(--line)' },
+ h('button', {
+ class: 'linklike', style: 'text-align:left;flex:1;font-size:15px;' + (isActive ? 'font-weight:600' : ''),
+ onClick: () => { if (isActive) { ui.screen = 'wallet'; render(); } else switchAccount(a.id); },
+ }, (isActive ? '● ' : '○ ') + a.label + (a.type === 'watch' ? ' · ' + t('watchOnlyTag') : '')),
+ h('button', { class: 'btn-sm', title: t('remove'), onClick: () => removeAccount(a.id) }, '✕')
+ );
+ })
+ ),
+ h('button', { class: 'btn-block', onClick: () => { ui.screen = 'unlock'; ui.unlockTab = 'create'; ui.fromWallet = true; ui.unlockError = ''; render(); } }, t('addWallet')),
+ h('button', { class: 'btn-ghost btn-block', onClick: lock }, t('lockAll'))
+ ),
+ h('button', { class: 'btn-ghost btn-block', onClick: () => { ui.screen = 'wallet'; render(); } }, t('back'))
+ );
+}
+
function walletScreen() {
return h(
'div',
@@ -1066,7 +1248,8 @@ function balanceCard() {
function tabsBar() {
const tabs = [
['receive', t('tabReceive')],
- ['send', t('tabSend')],
+ // Watch-only wallets can't sign, so no Send tab.
+ ...(wallet.watchOnly ? [] : [['send', t('tabSend')]]),
['history', t('tabHistory')],
['settings', t('tabSettings')],
];
@@ -1862,7 +2045,7 @@ applyDir();
loadLocale(getLang()).finally(() => {
const code = readGiftHash();
if (code) { enterWallet(newMnemonic(), '', { gift: code }); return; }
- if (!restoreSession()) render();
+ if (!restoreAccountsState()) render();
});
diff --git a/src/i18n.js b/src/i18n.js
index abb9015..f75e014 100644
--- a/src/i18n.js
+++ b/src/i18n.js
@@ -36,6 +36,23 @@ const STR = {
appTitle: 'Hal Wallet',
home: 'Home',
logout: 'Logout',
+ accounts: 'Accounts',
+ addWallet: 'Add wallet',
+ lockAll: 'Lock all',
+ remove: 'Remove',
+ watchOnly: 'Watch-only',
+ watchOnlyTag: 'watch-only',
+ watchOnlyAdd: 'Add watch-only',
+ watchOnlyDesc: 'Watch any wallet from its account public key. You can receive and view balance and history, but not send.',
+ watchOnlyNote: 'This is a watch-only wallet. You can receive and view, but you cannot send.',
+ watchXpubLabel: 'Account xpub or zpub',
+ labelOptional: 'Label (optional)',
+ walletLabelN: 'Wallet {n}',
+ watchLabelN: 'Watch {n}',
+ publicKey: 'Public key',
+ publicKeyDesc: 'Share this to watch this wallet read-only on another device or app. It reveals your addresses, not your keys.',
+ copyKey: 'Copy key',
+ showPublicKey: 'Show public key',
createNew: 'Create new',
importExisting: 'Import existing',
genIntro: 'Generate a new 12-word seed phrase. Write it down. It’s the only way to recover this wallet.',
diff --git a/src/locales/ar.json b/src/locales/ar.json
index 76843bc..cdfb0f8 100644
--- a/src/locales/ar.json
+++ b/src/locales/ar.json
@@ -172,5 +172,22 @@
"giftReclaimed": "تم استرجاع العملة",
"giftRevokeConfirm": "إلغاء هذه الهدية الآن؟ يعيد ذلك العملة إلى محفظتك ويعطّل الرابط.",
"giftReclaimedTag": "لا يزال قابلاً للمطالبة",
- "giftMinNote": "الحد الأدنى {n} بالرسوم الحالية، ليتجاوز الغبار عند المطالبة."
+ "giftMinNote": "الحد الأدنى {n} بالرسوم الحالية، ليتجاوز الغبار عند المطالبة.",
+ "accounts": "الحسابات",
+ "addWallet": "إضافة محفظة",
+ "lockAll": "قفل الكل",
+ "watchOnly": "للمراقبة فقط",
+ "watchOnlyTag": "للمراقبة فقط",
+ "watchOnlyAdd": "إضافة محفظة للمراقبة",
+ "watchOnlyDesc": "راقب أي محفظة عبر المفتاح العام لحسابها. يمكنك الاستلام وعرض الرصيد والسجل، لكن لا يمكنك الإرسال.",
+ "watchOnlyNote": "هذه محفظة للمراقبة فقط. يمكنك الاستلام والعرض، لكن لا يمكنك الإرسال.",
+ "watchXpubLabel": "xpub أو zpub الحساب",
+ "labelOptional": "التسمية (اختيارية)",
+ "walletLabelN": "محفظة {n}",
+ "watchLabelN": "مراقبة {n}",
+ "publicKey": "المفتاح العام",
+ "publicKeyDesc": "شارك هذا لمراقبة هذه المحفظة للقراءة فقط على جهاز أو تطبيق آخر. يكشف عناوينك، لا مفاتيحك.",
+ "copyKey": "نسخ المفتاح",
+ "showPublicKey": "إظهار المفتاح العام",
+ "remove": "إزالة"
} \ No newline at end of file
diff --git a/src/locales/bn.json b/src/locales/bn.json
index 27d657b..211d077 100644
--- a/src/locales/bn.json
+++ b/src/locales/bn.json
@@ -170,5 +170,22 @@
"giftReclaimed": "কয়েন ফেরত নেওয়া হয়েছে",
"giftRevokeConfirm": "এই উপহার এখনই বাতিল করবেন? এটি কয়েনটি আপনার ওয়ালেটে ফেরত পাঠায় এবং লিঙ্কটি নিষ্ক্রিয় করে।",
"giftReclaimedTag": "এখনও দাবিযোগ্য",
- "giftMinNote": "বর্তমান ফিতে কমপক্ষে {n}, যাতে দাবি করার সময় এটি ডাস্ট ছাড়িয়ে যায়।"
+ "giftMinNote": "বর্তমান ফিতে কমপক্ষে {n}, যাতে দাবি করার সময় এটি ডাস্ট ছাড়িয়ে যায়।",
+ "accounts": "অ্যাকাউন্ট",
+ "addWallet": "ওয়ালেট যোগ করুন",
+ "lockAll": "সব লক করুন",
+ "remove": "সরান",
+ "watchOnly": "শুধু দেখুন",
+ "watchOnlyTag": "শুধু-দেখা",
+ "watchOnlyAdd": "শুধু-দেখা যোগ করুন",
+ "watchOnlyDesc": "যেকোনো ওয়ালেটকে তার অ্যাকাউন্ট পাবলিক কী দিয়ে দেখুন। আপনি গ্রহণ করতে এবং ব্যালেন্স ও ইতিহাস দেখতে পারবেন, কিন্তু পাঠাতে পারবেন না।",
+ "watchOnlyNote": "এটি একটি শুধু-দেখা ওয়ালেট। আপনি গ্রহণ ও দেখতে পারবেন, কিন্তু পাঠাতে পারবেন না।",
+ "watchXpubLabel": "অ্যাকাউন্ট xpub বা zpub",
+ "labelOptional": "লেবেল (ঐচ্ছিক)",
+ "walletLabelN": "ওয়ালেট {n}",
+ "watchLabelN": "ওয়াচ {n}",
+ "publicKey": "পাবলিক কী",
+ "publicKeyDesc": "অন্য ডিভাইস বা অ্যাপে এই ওয়ালেট শুধু-পঠনযোগ্যভাবে দেখতে এটি শেয়ার করুন। এটি আপনার ঠিকানা প্রকাশ করে, আপনার কী নয়।",
+ "copyKey": "কী কপি",
+ "showPublicKey": "পাবলিক কী দেখান"
} \ No newline at end of file
diff --git a/src/locales/de.json b/src/locales/de.json
index 83968e3..fbb88da 100644
--- a/src/locales/de.json
+++ b/src/locales/de.json
@@ -172,5 +172,21 @@
"giftReclaimed": "Münze zurückgeholt",
"giftRevokeConfirm": "Dieses Geschenk jetzt widerrufen? Die Münze geht zurück in deine Wallet und der Link wird deaktiviert.",
"giftReclaimedTag": "noch einlösbar",
- "giftMinNote": "Mindestens {n} bei aktuellen Gebühren, damit es beim Einlösen über dem Dust-Limit liegt."
-} \ No newline at end of file
+ "giftMinNote": "Mindestens {n} bei aktuellen Gebühren, damit es beim Einlösen über dem Dust-Limit liegt.",
+ "accounts": "Konten",
+ "addWallet": "Wallet hinzufügen",
+ "lockAll": "Alle sperren",
+ "watchOnly": "Nur-Lesen",
+ "watchOnlyTag": "nur-lesen",
+ "watchOnlyAdd": "Nur-Lesen hinzufügen",
+ "watchOnlyDesc": "Beobachte eine beliebige Wallet über ihren öffentlichen Account-Schlüssel. Du kannst empfangen sowie Guthaben und Verlauf einsehen, aber nicht senden.",
+ "watchOnlyNote": "Dies ist eine Nur-Lesen-Wallet. Du kannst empfangen und einsehen, aber nicht senden.",
+ "watchXpubLabel": "Account-xpub oder -zpub",
+ "labelOptional": "Bezeichnung (optional)",
+ "walletLabelN": "Wallet {n}",
+ "watchLabelN": "Beobachtung {n}",
+ "publicKey": "Öffentlicher Schlüssel",
+ "publicKeyDesc": "Teile dies, um diese Wallet auf einem anderen Gerät oder in einer anderen App nur-lesend zu beobachten. Er gibt deine Adressen preis, nicht deine Schlüssel.",
+ "copyKey": "Schlüssel kopieren",
+ "showPublicKey": "Öffentlichen Schlüssel anzeigen"
+}
diff --git a/src/locales/es.json b/src/locales/es.json
index c3c77fe..a9b8132 100644
--- a/src/locales/es.json
+++ b/src/locales/es.json
@@ -1,4 +1,20 @@
{
+ "accounts": "Cuentas",
+ "addWallet": "Añadir monedero",
+ "lockAll": "Bloquear todo",
+ "watchOnly": "Solo lectura",
+ "watchOnlyTag": "solo lectura",
+ "watchOnlyAdd": "Añadir solo lectura",
+ "watchOnlyDesc": "Observa cualquier monedero desde su clave pública de cuenta. Puedes recibir y ver el saldo y el historial, pero no enviar.",
+ "watchOnlyNote": "Este es un monedero de solo lectura. Puedes recibir y ver, pero no puedes enviar.",
+ "watchXpubLabel": "xpub o zpub de cuenta",
+ "labelOptional": "Etiqueta (opcional)",
+ "walletLabelN": "Monedero {n}",
+ "watchLabelN": "Observado {n}",
+ "publicKey": "Clave pública",
+ "publicKeyDesc": "Compártela para observar este monedero en modo solo lectura en otro dispositivo o app. Revela tus direcciones, no tus claves.",
+ "copyKey": "Copiar clave",
+ "showPublicKey": "Mostrar clave pública",
"passphrase": "Frase de contraseña",
"howItWorks": "Cómo funciona",
"installApp": "Instalar app",
@@ -81,7 +97,7 @@
"nSelected": "{n} seleccionadas",
"addRecipient": "+ Añadir destinatario",
"reviewTx": "Revisar transacción",
- "remove": "Quitar",
+ "remove": "Eliminar",
"enterRecipientAddr": "Introduce una dirección de destino.",
"enterAddrForN": "Introduce una dirección para el destinatario {n}.",
"enterValidAmtForN": "Introduce un importe válido para el destinatario {n}.",
diff --git a/src/locales/fr.json b/src/locales/fr.json
index 4a22eee..b09cd92 100644
--- a/src/locales/fr.json
+++ b/src/locales/fr.json
@@ -172,5 +172,21 @@
"giftReclaimed": "Pièce récupérée",
"giftRevokeConfirm": "Révoquer ce cadeau maintenant ? La pièce revient dans votre portefeuille et le lien est désactivé.",
"giftReclaimedTag": "encore réclamable",
- "giftMinNote": "Minimum {n} aux frais actuels, pour dépasser la poussière lors de la réclamation."
+ "giftMinNote": "Minimum {n} aux frais actuels, pour dépasser la poussière lors de la réclamation.",
+ "accounts": "Comptes",
+ "addWallet": "Ajouter un portefeuille",
+ "lockAll": "Tout verrouiller",
+ "watchOnly": "Lecture seule",
+ "watchOnlyTag": "lecture seule",
+ "watchOnlyAdd": "Ajouter en lecture seule",
+ "watchOnlyDesc": "Surveillez n’importe quel portefeuille à partir de sa clé publique de compte. Vous pouvez recevoir et consulter le solde et l’historique, mais pas envoyer.",
+ "watchOnlyNote": "Ceci est un portefeuille en lecture seule. Vous pouvez recevoir et consulter, mais vous ne pouvez pas envoyer.",
+ "watchXpubLabel": "xpub ou zpub du compte",
+ "labelOptional": "Libellé (facultatif)",
+ "walletLabelN": "Portefeuille {n}",
+ "watchLabelN": "Surveillance {n}",
+ "publicKey": "Clé publique",
+ "publicKeyDesc": "Partagez-la pour surveiller ce portefeuille en lecture seule sur un autre appareil ou une autre application. Elle révèle vos adresses, pas vos clés.",
+ "copyKey": "Copier la clé",
+ "showPublicKey": "Afficher la clé publique"
} \ No newline at end of file
diff --git a/src/locales/hi.json b/src/locales/hi.json
index 5b88673..b3cbc36 100644
--- a/src/locales/hi.json
+++ b/src/locales/hi.json
@@ -172,5 +172,22 @@
"giftReclaimed": "सिक्का वापस लिया गया",
"giftRevokeConfirm": "इस उपहार को अभी रद्द करें? यह सिक्का आपके वॉलेट में वापस भेजता है और लिंक को निष्क्रिय कर देता है।",
"giftReclaimedTag": "अब भी दावा-योग्य",
- "giftMinNote": "मौजूदा शुल्क पर कम से कम {n}, ताकि दावा करते समय यह डस्ट से ऊपर रहे।"
+ "giftMinNote": "मौजूदा शुल्क पर कम से कम {n}, ताकि दावा करते समय यह डस्ट से ऊपर रहे।",
+ "accounts": "खाते",
+ "addWallet": "वॉलेट जोड़ें",
+ "lockAll": "सभी लॉक करें",
+ "remove": "हटाएँ",
+ "watchOnly": "केवल-देखें",
+ "watchOnlyTag": "केवल-देखें",
+ "watchOnlyAdd": "केवल-देखें जोड़ें",
+ "watchOnlyDesc": "किसी भी वॉलेट को उसकी अकाउंट पब्लिक की से देखें। आप प्राप्त कर सकते हैं और बैलेंस व इतिहास देख सकते हैं, पर भेज नहीं सकते।",
+ "watchOnlyNote": "यह केवल-देखें वॉलेट है। आप प्राप्त और देख सकते हैं, पर भेज नहीं सकते।",
+ "watchXpubLabel": "अकाउंट xpub या zpub",
+ "labelOptional": "लेबल (वैकल्पिक)",
+ "walletLabelN": "वॉलेट {n}",
+ "watchLabelN": "देखें {n}",
+ "publicKey": "पब्लिक की",
+ "publicKeyDesc": "इस वॉलेट को किसी अन्य डिवाइस या ऐप पर केवल-देखें मोड में देखने के लिए इसे साझा करें। यह आपके पते दिखाता है, आपकी कुंजियाँ नहीं।",
+ "copyKey": "की कॉपी करें",
+ "showPublicKey": "पब्लिक की दिखाएँ"
} \ No newline at end of file
diff --git a/src/locales/id.json b/src/locales/id.json
index 31d324f..9d6befc 100644
--- a/src/locales/id.json
+++ b/src/locales/id.json
@@ -172,5 +172,21 @@
"giftReclaimed": "Koin ditarik kembali",
"giftRevokeConfirm": "Cabut hadiah ini sekarang? Ini mengembalikan koin ke dompet Anda dan menonaktifkan tautannya.",
"giftReclaimedTag": "masih bisa diklaim",
- "giftMinNote": "Minimal {n} pada biaya saat ini, agar melampaui dust saat diklaim."
+ "giftMinNote": "Minimal {n} pada biaya saat ini, agar melampaui dust saat diklaim.",
+ "accounts": "Akun",
+ "addWallet": "Tambah dompet",
+ "lockAll": "Kunci semua",
+ "watchOnly": "Hanya pantau",
+ "watchOnlyTag": "hanya pantau",
+ "watchOnlyAdd": "Tambah hanya pantau",
+ "watchOnlyDesc": "Pantau dompet apa pun dari kunci publik akunnya. Anda bisa menerima serta melihat saldo dan riwayat, tetapi tidak bisa mengirim.",
+ "watchOnlyNote": "Ini dompet hanya pantau. Anda bisa menerima dan melihat, tetapi tidak bisa mengirim.",
+ "watchXpubLabel": "xpub atau zpub akun",
+ "labelOptional": "Label (opsional)",
+ "walletLabelN": "Dompet {n}",
+ "watchLabelN": "Pantau {n}",
+ "publicKey": "Kunci publik",
+ "publicKeyDesc": "Bagikan ini untuk memantau dompet ini secara baca-saja di perangkat atau aplikasi lain. Ini menampilkan alamat Anda, bukan kunci Anda.",
+ "copyKey": "Salin kunci",
+ "showPublicKey": "Tampilkan kunci publik"
} \ No newline at end of file
diff --git a/src/locales/ja.json b/src/locales/ja.json
index 35b91a8..b990fd8 100644
--- a/src/locales/ja.json
+++ b/src/locales/ja.json
@@ -172,5 +172,21 @@
"giftReclaimed": "コインを取り戻しました",
"giftRevokeConfirm": "このギフトを今すぐ取り消しますか?コインがウォレットに戻り、リンクが無効になります。",
"giftReclaimedTag": "まだ請求可能",
- "giftMinNote": "現在の手数料で最低 {n}。請求時にダストを上回ります。"
-} \ No newline at end of file
+ "giftMinNote": "現在の手数料で最低 {n}。請求時にダストを上回ります。",
+ "accounts": "アカウント",
+ "addWallet": "ウォレットを追加",
+ "lockAll": "すべてロック",
+ "watchOnly": "ウォッチ専用",
+ "watchOnlyTag": "ウォッチ専用",
+ "watchOnlyAdd": "ウォッチ専用を追加",
+ "watchOnlyDesc": "アカウントの公開鍵から任意のウォレットをウォッチできます。受取、残高と履歴の表示はできますが、送金はできません。",
+ "watchOnlyNote": "これはウォッチ専用ウォレットです。受取と表示はできますが、送金はできません。",
+ "watchXpubLabel": "アカウントの xpub または zpub",
+ "labelOptional": "ラベル(任意)",
+ "walletLabelN": "ウォレット {n}",
+ "watchLabelN": "ウォッチ {n}",
+ "publicKey": "公開鍵",
+ "publicKeyDesc": "これを共有すると、別の端末やアプリでこのウォレットを読み取り専用でウォッチできます。鍵ではなくアドレスが公開されます。",
+ "copyKey": "鍵をコピー",
+ "showPublicKey": "公開鍵を表示"
+}
diff --git a/src/locales/ko.json b/src/locales/ko.json
index 6f87ae7..5227148 100644
--- a/src/locales/ko.json
+++ b/src/locales/ko.json
@@ -172,5 +172,22 @@
"giftReclaimed": "코인 회수됨",
"giftRevokeConfirm": "이 선물을 지금 취소할까요? 코인이 지갑으로 돌아가고 링크가 비활성화됩니다.",
"giftReclaimedTag": "아직 청구 가능",
- "giftMinNote": "현재 수수료 기준 최소 {n}, 청구 시 더스트를 넘도록."
+ "giftMinNote": "현재 수수료 기준 최소 {n}, 청구 시 더스트를 넘도록.",
+ "accounts": "계정",
+ "addWallet": "지갑 추가",
+ "lockAll": "모두 잠그기",
+ "remove": "삭제",
+ "watchOnly": "조회 전용",
+ "watchOnlyTag": "조회 전용",
+ "watchOnlyAdd": "조회 전용 추가",
+ "watchOnlyDesc": "계정 공개키로 지갑을 조회만 할 수 있습니다. 잔액과 내역을 받고 볼 수 있지만 보낼 수는 없습니다.",
+ "watchOnlyNote": "조회 전용 지갑입니다. 받고 볼 수는 있지만 보낼 수는 없습니다.",
+ "watchXpubLabel": "계정 xpub 또는 zpub",
+ "labelOptional": "라벨 (선택)",
+ "walletLabelN": "지갑 {n}",
+ "watchLabelN": "조회 {n}",
+ "publicKey": "공개키",
+ "publicKeyDesc": "이 키를 공유하면 다른 기기나 앱에서 이 지갑을 조회 전용으로 볼 수 있습니다. 주소는 드러나지만 키는 드러나지 않습니다.",
+ "copyKey": "키 복사",
+ "showPublicKey": "공개키 표시"
} \ No newline at end of file
diff --git a/src/locales/mr.json b/src/locales/mr.json
index a4c864e..ed90448 100644
--- a/src/locales/mr.json
+++ b/src/locales/mr.json
@@ -172,5 +172,22 @@
"giftReclaimed": "नाणे परत घेतले",
"giftRevokeConfirm": "ही भेट आत्ता रद्द करायची? हे नाणे तुमच्या वॉलेटमध्ये परत पाठवते आणि लिंक निष्क्रिय करते.",
"giftReclaimedTag": "अजूनही दावायोग्य",
- "giftMinNote": "सध्याच्या शुल्कावर किमान {n}, जेणेकरून दावा करताना ते डस्टपेक्षा जास्त असेल."
+ "giftMinNote": "सध्याच्या शुल्कावर किमान {n}, जेणेकरून दावा करताना ते डस्टपेक्षा जास्त असेल.",
+ "accounts": "खाती",
+ "addWallet": "वॉलेट जोडा",
+ "lockAll": "सर्व लॉक करा",
+ "remove": "काढा",
+ "watchOnly": "फक्त-पाहण्यासाठी",
+ "watchOnlyTag": "फक्त-पाहण्यासाठी",
+ "watchOnlyAdd": "फक्त-पाहण्यासाठी जोडा",
+ "watchOnlyDesc": "कोणतेही वॉलेट त्याच्या खाते सार्वजनिक कीवरून पाहा. तुम्ही शिल्लक व इतिहास मिळवू व पाहू शकता, पण पाठवू शकत नाही.",
+ "watchOnlyNote": "हे फक्त-पाहण्यासाठीचे वॉलेट आहे. तुम्ही मिळवू व पाहू शकता, पण पाठवू शकत नाही.",
+ "watchXpubLabel": "खाते xpub किंवा zpub",
+ "labelOptional": "लेबल (ऐच्छिक)",
+ "walletLabelN": "वॉलेट {n}",
+ "watchLabelN": "वॉच {n}",
+ "publicKey": "सार्वजनिक की",
+ "publicKeyDesc": "दुसऱ्या उपकरणावर किंवा अॅपवर हे वॉलेट फक्त-वाचण्यासाठी पाहण्यासाठी हे शेअर करा. यातून तुमचे पत्ते दिसतात, तुमच्या की नाहीत.",
+ "copyKey": "की कॉपी करा",
+ "showPublicKey": "सार्वजनिक की दाखवा"
} \ No newline at end of file
diff --git a/src/locales/pt.json b/src/locales/pt.json
index 0710d94..9e9d210 100644
--- a/src/locales/pt.json
+++ b/src/locales/pt.json
@@ -172,5 +172,22 @@
"giftReclaimed": "Moeda recuperada",
"giftRevokeConfirm": "Revogar este presente agora? Isso devolve a moeda à sua carteira e desativa o link.",
"giftReclaimedTag": "ainda resgatável",
- "giftMinNote": "Mínimo {n} com as taxas atuais, para superar a poeira ao ser resgatado."
+ "giftMinNote": "Mínimo {n} com as taxas atuais, para superar a poeira ao ser resgatado.",
+ "accounts": "Contas",
+ "addWallet": "Adicionar carteira",
+ "lockAll": "Bloquear tudo",
+ "remove": "Remover",
+ "watchOnly": "Somente leitura",
+ "watchOnlyTag": "somente leitura",
+ "watchOnlyAdd": "Adicionar somente leitura",
+ "watchOnlyDesc": "Acompanhe qualquer carteira a partir da chave pública da conta. Você pode receber e ver o saldo e o histórico, mas não enviar.",
+ "watchOnlyNote": "Esta é uma carteira somente leitura. Você pode receber e visualizar, mas não pode enviar.",
+ "watchXpubLabel": "xpub ou zpub da conta",
+ "labelOptional": "Rótulo (opcional)",
+ "walletLabelN": "Carteira {n}",
+ "watchLabelN": "Observação {n}",
+ "publicKey": "Chave pública",
+ "publicKeyDesc": "Compartilhe isto para acompanhar esta carteira somente leitura em outro dispositivo ou aplicativo. Revela seus endereços, não suas chaves.",
+ "copyKey": "Copiar chave",
+ "showPublicKey": "Mostrar chave pública"
} \ No newline at end of file
diff --git a/src/locales/ru.json b/src/locales/ru.json
index 0654f21..07112ce 100644
--- a/src/locales/ru.json
+++ b/src/locales/ru.json
@@ -172,5 +172,22 @@
"giftReclaimed": "Монета возвращена",
"giftRevokeConfirm": "Отозвать этот подарок сейчас? Монета вернётся в ваш кошелёк, а ссылка будет отключена.",
"giftReclaimedTag": "ещё можно получить",
- "giftMinNote": "Минимум {n} при текущих комиссиях, чтобы при получении сумма была выше пыли."
+ "giftMinNote": "Минимум {n} при текущих комиссиях, чтобы при получении сумма была выше пыли.",
+ "accounts": "Счета",
+ "addWallet": "Добавить кошелёк",
+ "lockAll": "Заблокировать все",
+ "remove": "Удалить",
+ "watchOnly": "Только просмотр",
+ "watchOnlyTag": "только просмотр",
+ "watchOnlyAdd": "Добавить только для просмотра",
+ "watchOnlyDesc": "Следите за любым кошельком по его публичному ключу счёта. Вы можете получать монеты и видеть баланс и историю, но не можете отправлять.",
+ "watchOnlyNote": "Это кошелёк только для просмотра. Вы можете получать и просматривать, но не можете отправлять.",
+ "watchXpubLabel": "xpub или zpub счёта",
+ "labelOptional": "Метка (необязательно)",
+ "walletLabelN": "Кошелёк {n}",
+ "watchLabelN": "Просмотр {n}",
+ "publicKey": "Публичный ключ",
+ "publicKeyDesc": "Поделитесь этим, чтобы следить за кошельком только для чтения на другом устройстве или в приложении. Это раскрывает ваши адреса, но не ключи.",
+ "copyKey": "Копировать ключ",
+ "showPublicKey": "Показать публичный ключ"
} \ No newline at end of file
diff --git a/src/locales/sw.json b/src/locales/sw.json
index 0e41dcb..98a53a4 100644
--- a/src/locales/sw.json
+++ b/src/locales/sw.json
@@ -172,5 +172,22 @@
"giftReclaimed": "Sarafu imerudishwa",
"giftRevokeConfirm": "Batilisha zawadi hii sasa? Hii inarudisha sarafu kwenye pochi yako na kuzima kiungo.",
"giftReclaimedTag": "bado inaweza kudaiwa",
- "giftMinNote": "Kima cha chini {n} kwa ada za sasa, ili kuzidi vumbi inapodaiwa."
+ "giftMinNote": "Kima cha chini {n} kwa ada za sasa, ili kuzidi vumbi inapodaiwa.",
+ "accounts": "Akaunti",
+ "addWallet": "Ongeza pochi",
+ "lockAll": "Funga zote",
+ "remove": "Ondoa",
+ "watchOnly": "Kutazama tu",
+ "watchOnlyTag": "kutazama tu",
+ "watchOnlyAdd": "Ongeza ya kutazama tu",
+ "watchOnlyDesc": "Tazama pochi yoyote kwa kutumia ufunguo wa umma wa akaunti yake. Unaweza kupokea na kuona salio na historia, lakini si kutuma.",
+ "watchOnlyNote": "Hii ni pochi ya kutazama tu. Unaweza kupokea na kutazama, lakini huwezi kutuma.",
+ "watchXpubLabel": "xpub au zpub ya akaunti",
+ "labelOptional": "Lebo (si lazima)",
+ "walletLabelN": "Pochi {n}",
+ "watchLabelN": "Tazama {n}",
+ "publicKey": "Ufunguo wa umma",
+ "publicKeyDesc": "Shiriki huu ili kutazama pochi hii kwa kusoma tu kwenye kifaa au programu nyingine. Unaonyesha anwani zako, si funguo zako.",
+ "copyKey": "Nakili ufunguo",
+ "showPublicKey": "Onyesha ufunguo wa umma"
} \ No newline at end of file
diff --git a/src/locales/ta.json b/src/locales/ta.json
index 2b9df36..b157a31 100644
--- a/src/locales/ta.json
+++ b/src/locales/ta.json
@@ -172,5 +172,22 @@
"giftReclaimed": "நாணயம் திரும்பப் பெறப்பட்டது",
"giftRevokeConfirm": "இந்த பரிசை இப்போதே ரத்து செய்யவா? இது நாணயத்தை உங்கள் வாலெட்டுக்குத் திருப்பி, இணைப்பை முடக்கும்.",
"giftReclaimedTag": "இன்னும் கோரத்தக்கது",
- "giftMinNote": "தற்போதைய கட்டணத்தில் குறைந்தது {n}, கோரும்போது அது டஸ்ட்டைத் தாண்டும்."
+ "giftMinNote": "தற்போதைய கட்டணத்தில் குறைந்தது {n}, கோரும்போது அது டஸ்ட்டைத் தாண்டும்.",
+ "accounts": "கணக்குகள்",
+ "addWallet": "வாலட்டைச் சேர்",
+ "lockAll": "அனைத்தையும் பூட்டு",
+ "remove": "அகற்று",
+ "watchOnly": "பார்வை மட்டும்",
+ "watchOnlyTag": "பார்வை மட்டும்",
+ "watchOnlyAdd": "பார்வை மட்டும் சேர்",
+ "watchOnlyDesc": "எந்த வாலட்டையும் அதன் கணக்கு பொது விசையிலிருந்து பார்க்கலாம். பெறவும் இருப்பு மற்றும் வரலாற்றைப் பார்க்கவும் முடியும், ஆனால் அனுப்ப முடியாது.",
+ "watchOnlyNote": "இது பார்வை மட்டும் வாலட். பெறவும் பார்க்கவும் முடியும், ஆனால் அனுப்ப முடியாது.",
+ "watchXpubLabel": "கணக்கு xpub அல்லது zpub",
+ "labelOptional": "சிட்டை (விருப்பம்)",
+ "walletLabelN": "வாலட் {n}",
+ "watchLabelN": "பார்வை {n}",
+ "publicKey": "பொது விசை",
+ "publicKeyDesc": "மற்றொரு சாதனத்தில் அல்லது செயலியில் இந்த வாலட்டைப் பார்வை மட்டும் கண்காணிக்க இதைப் பகிரவும். இது உங்கள் முகவரிகளை வெளிப்படுத்தும், உங்கள் விசைகளை அல்ல.",
+ "copyKey": "விசையை நகலெடு",
+ "showPublicKey": "பொது விசையைக் காட்டு"
} \ No newline at end of file
diff --git a/src/locales/te.json b/src/locales/te.json
index 99525bd..37380ff 100644
--- a/src/locales/te.json
+++ b/src/locales/te.json
@@ -170,5 +170,22 @@
"giftReclaimed": "నాణెం తిరిగి తీసుకోబడింది",
"giftRevokeConfirm": "ఈ బహుమతిని ఇప్పుడే రద్దు చేయాలా? ఇది నాణేన్ని మీ వాలెట్‌కు తిరిగి పంపి, లింక్‌ను నిష్క్రియం చేస్తుంది.",
"giftReclaimedTag": "ఇంకా క్లెయిమ్ చేయదగినది",
- "giftMinNote": "ప్రస్తుత ఫీజుల వద్ద కనీసం {n}, తద్వారా క్లెయిమ్ చేసేటప్పుడు అది డస్ట్‌ను దాటుతుంది."
+ "giftMinNote": "ప్రస్తుత ఫీజుల వద్ద కనీసం {n}, తద్వారా క్లెయిమ్ చేసేటప్పుడు అది డస్ట్‌ను దాటుతుంది.",
+ "accounts": "ఖాతాలు",
+ "addWallet": "వాలెట్ జోడించు",
+ "lockAll": "అన్నీ లాక్ చేయి",
+ "remove": "తొలగించు",
+ "watchOnly": "చూడటం మాత్రమే",
+ "watchOnlyTag": "చూడటం మాత్రమే",
+ "watchOnlyAdd": "చూడటం-మాత్రమే జోడించు",
+ "watchOnlyDesc": "ఏదైనా వాలెట్‌ను దాని ఖాతా పబ్లిక్ కీతో గమనించండి. మీరు స్వీకరించవచ్చు, బ్యాలెన్స్, చరిత్రను చూడవచ్చు, కానీ పంపలేరు.",
+ "watchOnlyNote": "ఇది చూడటం-మాత్రమే వాలెట్. మీరు స్వీకరించవచ్చు, చూడవచ్చు, కానీ పంపలేరు.",
+ "watchXpubLabel": "ఖాతా xpub లేదా zpub",
+ "labelOptional": "లేబుల్ (ఐచ్ఛికం)",
+ "walletLabelN": "వాలెట్ {n}",
+ "watchLabelN": "వాచ్ {n}",
+ "publicKey": "పబ్లిక్ కీ",
+ "publicKeyDesc": "ఈ వాలెట్‌ను మరో పరికరం లేదా యాప్‌లో చదవడం-మాత్రమే గమనించడానికి దీన్ని పంచుకోండి. ఇది మీ చిరునామాలను వెల్లడిస్తుంది, మీ కీలను కాదు.",
+ "copyKey": "కీ కాపీ",
+ "showPublicKey": "పబ్లిక్ కీ చూపు"
} \ No newline at end of file
diff --git a/src/locales/tr.json b/src/locales/tr.json
index 44a05ce..0277452 100644
--- a/src/locales/tr.json
+++ b/src/locales/tr.json
@@ -172,5 +172,21 @@
"giftReclaimed": "Para geri alındı",
"giftRevokeConfirm": "Bu hediye şimdi iptal edilsin mi? Bu, parayı cüzdanınıza geri gönderir ve bağlantıyı devre dışı bırakır.",
"giftReclaimedTag": "hâlâ talep edilebilir",
- "giftMinNote": "Mevcut ücretlerde en az {n}, böylece talep edildiğinde tozu aşar."
+ "giftMinNote": "Mevcut ücretlerde en az {n}, böylece talep edildiğinde tozu aşar.",
+ "accounts": "Hesaplar",
+ "addWallet": "Cüzdan ekle",
+ "lockAll": "Tümünü kilitle",
+ "watchOnly": "Yalnızca izleme",
+ "watchOnlyTag": "yalnızca izleme",
+ "watchOnlyAdd": "Yalnızca izleme ekle",
+ "watchOnlyDesc": "Herhangi bir cüzdanı hesap genel anahtarından izleyin. Bakiyeyi ve geçmişi alıp görüntüleyebilirsiniz, ancak gönderemezsiniz.",
+ "watchOnlyNote": "Bu yalnızca izleme amaçlı bir cüzdandır. Alıp görüntüleyebilir, ancak gönderemezsiniz.",
+ "watchXpubLabel": "Hesap xpub veya zpub",
+ "labelOptional": "Etiket (isteğe bağlı)",
+ "walletLabelN": "Cüzdan {n}",
+ "watchLabelN": "İzleme {n}",
+ "publicKey": "Genel anahtar",
+ "publicKeyDesc": "Bu cüzdanı başka bir cihaz veya uygulamada salt okunur izlemek için bunu paylaşın. Anahtarlarınızı değil, adreslerinizi açığa çıkarır.",
+ "copyKey": "Anahtarı kopyala",
+ "showPublicKey": "Genel anahtarı göster"
} \ No newline at end of file
diff --git a/src/locales/ur.json b/src/locales/ur.json
index ebc667e..a8c2e56 100644
--- a/src/locales/ur.json
+++ b/src/locales/ur.json
@@ -172,5 +172,22 @@
"giftReclaimed": "سکہ واپس لے لیا گیا",
"giftRevokeConfirm": "اس تحفے کو ابھی منسوخ کریں؟ یہ سکہ آپ کے والیٹ میں واپس بھیجتا ہے اور لنک کو غیر فعال کر دیتا ہے۔",
"giftReclaimedTag": "اب بھی قابلِ وصول",
- "giftMinNote": "موجودہ فیس پر کم از کم {n}، تاکہ وصول کرتے وقت یہ ڈسٹ سے اوپر رہے۔"
+ "giftMinNote": "موجودہ فیس پر کم از کم {n}، تاکہ وصول کرتے وقت یہ ڈسٹ سے اوپر رہے۔",
+ "accounts": "اکاؤنٹس",
+ "addWallet": "والیٹ شامل کریں",
+ "lockAll": "سب کو لاک کریں",
+ "remove": "ہٹائیں",
+ "watchOnly": "صرف دیکھنے کے لیے",
+ "watchOnlyTag": "صرف دیکھنے کے لیے",
+ "watchOnlyAdd": "صرف دیکھنے والا شامل کریں",
+ "watchOnlyDesc": "کسی بھی والیٹ کو اس کی اکاؤنٹ پبلک کلید سے دیکھیں۔ آپ وصول کر سکتے ہیں اور بیلنس و تاریخ دیکھ سکتے ہیں، لیکن بھیج نہیں سکتے۔",
+ "watchOnlyNote": "یہ صرف دیکھنے والا والیٹ ہے۔ آپ وصول اور دیکھ سکتے ہیں، لیکن بھیج نہیں سکتے۔",
+ "watchXpubLabel": "اکاؤنٹ xpub یا zpub",
+ "labelOptional": "لیبل (اختیاری)",
+ "walletLabelN": "والیٹ {n}",
+ "watchLabelN": "واچ {n}",
+ "publicKey": "پبلک کلید",
+ "publicKeyDesc": "اسے شیئر کریں تاکہ کسی اور ڈیوائس یا ایپ پر اس والیٹ کو صرف پڑھنے کے لیے دیکھا جا سکے۔ یہ آپ کے پتے ظاہر کرتی ہے، کلیدیں نہیں۔",
+ "copyKey": "کلید کاپی کریں",
+ "showPublicKey": "پبلک کلید دکھائیں"
} \ No newline at end of file
diff --git a/src/locales/vi.json b/src/locales/vi.json
index b715777..4c8e58c 100644
--- a/src/locales/vi.json
+++ b/src/locales/vi.json
@@ -172,5 +172,21 @@
"giftReclaimed": "Đã thu lại coin",
"giftRevokeConfirm": "Thu hồi quà này ngay? Việc này chuyển đồng coin về ví của bạn và vô hiệu hóa liên kết.",
"giftReclaimedTag": "vẫn có thể nhận",
- "giftMinNote": "Tối thiểu {n} ở mức phí hiện tại, để vượt mức bụi khi được nhận."
+ "giftMinNote": "Tối thiểu {n} ở mức phí hiện tại, để vượt mức bụi khi được nhận.",
+ "accounts": "Tài khoản",
+ "addWallet": "Thêm ví",
+ "lockAll": "Khóa tất cả",
+ "watchOnly": "Chỉ theo dõi",
+ "watchOnlyTag": "chỉ theo dõi",
+ "watchOnlyAdd": "Thêm ví chỉ theo dõi",
+ "watchOnlyDesc": "Theo dõi bất kỳ ví nào từ khóa công khai tài khoản của nó. Bạn có thể nhận và xem số dư cùng lịch sử, nhưng không thể gửi.",
+ "watchOnlyNote": "Đây là ví chỉ theo dõi. Bạn có thể nhận và xem, nhưng không thể gửi.",
+ "watchXpubLabel": "xpub hoặc zpub của tài khoản",
+ "labelOptional": "Nhãn (tùy chọn)",
+ "walletLabelN": "Ví {n}",
+ "watchLabelN": "Theo dõi {n}",
+ "publicKey": "Khóa công khai",
+ "publicKeyDesc": "Chia sẻ khóa này để theo dõi ví này ở chế độ chỉ đọc trên thiết bị hoặc ứng dụng khác. Nó tiết lộ địa chỉ của bạn, chứ không phải khóa của bạn.",
+ "copyKey": "Sao chép khóa",
+ "showPublicKey": "Hiện khóa công khai"
} \ No newline at end of file
diff --git a/src/locales/zh.json b/src/locales/zh.json
index 074b4e8..cf7de6f 100644
--- a/src/locales/zh.json
+++ b/src/locales/zh.json
@@ -172,5 +172,22 @@
"giftReclaimed": "已收回",
"giftRevokeConfirm": "立即撤销此礼物?这会将这笔币退回你的钱包并使链接失效。",
"giftReclaimedTag": "仍可领取",
- "giftMinNote": "按当前手续费至少 {n},以便领取时高于尘埃。"
+ "giftMinNote": "按当前手续费至少 {n},以便领取时高于尘埃。",
+ "accounts": "账户",
+ "addWallet": "添加钱包",
+ "lockAll": "全部锁定",
+ "remove": "移除",
+ "watchOnly": "仅查看",
+ "watchOnlyTag": "仅查看",
+ "watchOnlyAdd": "添加仅查看",
+ "watchOnlyDesc": "通过账户公钥查看任意钱包。你可以接收并查看余额和历史记录,但无法发送。",
+ "watchOnlyNote": "这是一个仅查看钱包。你可以接收并查看,但无法发送。",
+ "watchXpubLabel": "账户 xpub 或 zpub",
+ "labelOptional": "标签(可选)",
+ "walletLabelN": "钱包 {n}",
+ "watchLabelN": "查看 {n}",
+ "publicKey": "公钥",
+ "publicKeyDesc": "分享它即可在另一台设备或应用上以只读方式查看此钱包。它会暴露你的地址,而非你的密钥。",
+ "copyKey": "复制密钥",
+ "showPublicKey": "显示公钥"
} \ No newline at end of file
diff --git a/src/wallet.js b/src/wallet.js
index 39848e8..cacd313 100644
--- a/src/wallet.js
+++ b/src/wallet.js
@@ -11,7 +11,7 @@
import { HDKey } from '@scure/bip32';
import { generateMnemonic, mnemonicToSeedSync, validateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
-import { hex, base64urlnopad } from '@scure/base';
+import { hex, base64urlnopad, base58check } from '@scure/base';
import { sha256 } from '@noble/hashes/sha256';
import * as btc from '@scure/btc-signer';
import { p2wpkh } from '@scure/btc-signer/payment';
@@ -110,15 +110,18 @@ export class Wallet {
}
// --- setup --------------------------------------------------------------
- load({ mnemonic, passphrase = '', netName = 'mainnet', offline = false }) {
+ load({ mnemonic = '', passphrase = '', xpub = '', netName = 'mainnet', offline = false }) {
this.stopRealtime();
- this.mnemonic = mnemonic.trim().replace(/\s+/g, ' ');
+ this.mnemonic = (mnemonic || '').trim().replace(/\s+/g, ' ');
this.passphrase = passphrase;
+ this.xpub = xpub || '';
+ this.watchOnly = !!this.xpub && !this.mnemonic; // no seed = view/receive only
this.netName = netName;
this.offline = offline;
this.api = new Api(netName);
this.api.offline = offline;
this._account = null;
+ this._accountKey = null;
this._addrCache.clear();
this._reserved = null; // gift coins set aside from spending (lazy-loaded)
this._reclaimed = null; // gift coins freed for spending but link still live
@@ -169,6 +172,15 @@ export class Wallet {
// --- derivation ---------------------------------------------------------
account() {
+ // Watch-only wallets are loaded directly from the account public key.
+ if (this.watchOnly) {
+ if (!this._account || this._accountKey !== this.xpub) {
+ this._account = HDKey.fromExtendedKey(this.xpub);
+ this._accountKey = this.xpub;
+ this._addrCache.clear();
+ }
+ return this._account;
+ }
const key = `${this.netName}|${this.mnemonic}|${this.passphrase}`;
if (this._account && this._accountKey === key) return this._account;
const seed = mnemonicToSeedSync(this.mnemonic, this.passphrase);
@@ -180,6 +192,11 @@ export class Wallet {
return this._account;
}
+ // The account-level extended public key (xpub / zpub) for export → watch-only.
+ accountXpub() {
+ return this.account().publicExtendedKey;
+ }
+
node(chain, index) {
// Relative derivation from the account node (chain/index are non-hardened).
return this.account().deriveChild(chain).deriveChild(index);
@@ -633,6 +650,7 @@ export class Wallet {
// Sign every input of an already-built Transaction with the matching HD key.
sign(tx) {
+ if (this.watchOnly) throw new Error('Watch-only wallet — no keys to sign with.');
const byOutpoint = new Map();
for (const u of this.utxos) byOutpoint.set(utxoId(u), u);
for (let i = 0; i < tx.inputsLength; i++) {
@@ -1113,7 +1131,8 @@ export class Wallet {
// then shows the last-known balance/history instantly while a fresh scan
// runs in the background.
_cacheKey() {
- const bytes = new TextEncoder().encode(`${this.mnemonic}\n${this.passphrase}`);
+ const id = this.watchOnly ? this.xpub : `${this.mnemonic}\n${this.passphrase}`;
+ const bytes = new TextEncoder().encode(id);
return 'btc-wallet-cache:' + hex.encode(sha256(bytes)).slice(0, 32);
}
@@ -1291,6 +1310,41 @@ export function utxoId(u) {
return `${u.txid}:${u.vout}`;
}
+// Watch-only: accept a native-segwit account xpub or zpub and normalize it to
+// xpub version bytes (the key material is identical; only the prefix differs),
+// so HDKey can load it. Throws on anything else (private keys, wrong type).
+const _b58c = base58check(sha256);
+const _XPUB_VER = Uint8Array.from([0x04, 0x88, 0xb2, 0x1e]);
+const _ZPUB_VER = Uint8Array.from([0x04, 0xb2, 0x47, 0x46]);
+export function parseAccountKey(s) {
+ let data;
+ try {
+ data = _b58c.decode((s || '').trim());
+ } catch {
+ throw new Error('Not a valid extended public key.');
+ }
+ if (data.length !== 78) throw new Error('Not a valid extended public key.');
+ const ver = hex.encode(data.slice(0, 4));
+ if (ver !== hex.encode(_XPUB_VER) && ver !== hex.encode(_ZPUB_VER))
+ throw new Error('Use an account xpub or zpub (native segwit).');
+ const out = new Uint8Array(data);
+ out.set(_XPUB_VER, 0);
+ const xpub = _b58c.encode(out);
+ try {
+ HDKey.fromExtendedKey(xpub);
+ } catch {
+ throw new Error('Not a valid extended public key.');
+ }
+ return xpub;
+}
+
+// Convert a standard account xpub to a BIP84 zpub for export/interop.
+export function xpubToZpub(xpub) {
+ const data = new Uint8Array(_b58c.decode(xpub));
+ data.set(_ZPUB_VER, 0);
+ return _b58c.encode(data);
+}
+
// 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);