aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-06-24 16:39:55 -0700
committerAdam Soltys <[email protected]>2026-06-24 16:39:55 -0700
commite30c0623c28815a8249c32a79e28afbbc9e88633 (patch)
treec2a4ef59fbb4ceca29727f3e1aa85380c53566ce /src
parentd3199c42de3652f1e4b81a59b8b0756b9c2c4eb7 (diff)
downloadcoinosv3-e30c0623c28815a8249c32a79e28afbbc9e88633.tar.gz
coinosv3-e30c0623c28815a8249c32a79e28afbbc9e88633.zip
Fold watch-only into Import; support xprv spending import
Home screen is back to two tabs (Create / Import). The Import box now takes a recovery phrase, an xpub/zpub, or an xprv/zprv: - mnemonic → full wallet (as before) - xpub/zpub → watch-only (view/receive, no Send) - xprv/zprv → full spending wallet with no mnemonic. parseExtendedKey classifies + normalizes version bytes; account() derives the BIP84 path from a master xprv (depth 0) or uses an account-level xprv as-is; signing works (only watch-only blocks it). Vault persists xprv accounts too; Settings shows an "Imported key" note instead of a recovery card and hides Nostr sync (no seed). Public-key export still works. Note: a seed phrase cannot be derived from an xprv (one-way KDF), so an xprv wallet has no recovery words by design. New strings in all 19 locales. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Diffstat (limited to 'src')
-rw-r--r--src/app.js87
-rw-r--r--src/i18n.js5
-rw-r--r--src/locales/ar.json8
-rw-r--r--src/locales/bn.json8
-rw-r--r--src/locales/de.json9
-rw-r--r--src/locales/es.json7
-rw-r--r--src/locales/fr.json9
-rw-r--r--src/locales/hi.json9
-rw-r--r--src/locales/id.json7
-rw-r--r--src/locales/ja.json9
-rw-r--r--src/locales/ko.json9
-rw-r--r--src/locales/mr.json9
-rw-r--r--src/locales/pt.json8
-rw-r--r--src/locales/ru.json8
-rw-r--r--src/locales/sw.json8
-rw-r--r--src/locales/ta.json8
-rw-r--r--src/locales/te.json8
-rw-r--r--src/locales/tr.json7
-rw-r--r--src/locales/ur.json8
-rw-r--r--src/locales/vi.json9
-rw-r--r--src/locales/zh.json8
-rw-r--r--src/wallet.js67
22 files changed, 199 insertions, 116 deletions
diff --git a/src/app.js b/src/app.js
index c77ee44..71e5185 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, parseAccountKey, xpubToZpub, encryptVault, decryptVault } from './wallet.js';
+import { Wallet, newMnemonic, isValidMnemonic, utxoId, previewGift, buildClaimTx, giftMinimum, parseExtendedKey, xpubToZpub, encryptVault, decryptVault } from './wallet.js';
import { qrSvg } from './qr.js';
import { scanQr } from './scan.js';
import { getSyncConfig, setSyncConfig } from './nostr.js';
@@ -261,10 +261,9 @@ function unlockScreen() {
'div',
{ class: 'tabs' },
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(); })
+ tabBtn(t('importExisting'), ui.unlockTab === 'import', () => { ui.unlockTab = 'import'; ui.unlockError = ''; render(); })
),
- ui.unlockTab === 'create' ? createPane() : ui.unlockTab === 'import' ? importPane() : watchPane(),
+ ui.unlockTab === 'create' ? createPane() : importPane(),
ui.unlockError && h('div', { class: 'notice err' }, ui.unlockError)
),
ui.fromWallet && accounts.length
@@ -273,41 +272,6 @@ function unlockScreen() {
);
}
-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, { fresh: true });
-}
-
// ================================================================ HOW IT WORKS
function howItWorksScreen() {
const back = () => {
@@ -499,9 +463,9 @@ function importPane() {
h(
'label',
{ class: 'field' },
- h('span', { class: 'lab' }, t('seedPhrase')),
+ h('span', { class: 'lab' }, t('importLabel')),
h('textarea', {
- placeholder: t('seedPlaceholder'),
+ placeholder: t('importPlaceholder'),
autocapitalize: 'none',
autocomplete: 'off',
spellcheck: 'false',
@@ -534,15 +498,20 @@ function optionsPanel() {
);
}
-async function openWallet(mnemonic) {
+// Import accepts a recovery phrase, an xpub/zpub (watch-only), or an xprv/zprv
+// (full spending). Classify the pasted text and open the right kind of wallet.
+async function openWallet(input) {
ui.unlockError = '';
- const m = (mnemonic || '').trim().replace(/\s+/g, ' ');
- if (!isValidMnemonic(m)) {
- ui.unlockError = t('invalidSeed');
- render();
- return;
- }
- await enterWallet(m, ui.passphrase);
+ const raw = (input || '').trim();
+ const m = raw.replace(/\s+/g, ' ');
+ if (isValidMnemonic(m)) { await enterWallet(m, ui.passphrase); return; }
+ let pk;
+ try { pk = parseExtendedKey(raw); } catch { ui.unlockError = t('invalidImport'); render(); return; }
+ const acc = pk.kind === 'xpub'
+ ? addOrGetAccount({ type: 'watch', label: defaultLabel('watch'), xpub: pk.key })
+ : addOrGetAccount({ type: 'full', label: defaultLabel('full'), xprv: pk.key });
+ ui.fromWallet = false;
+ await activateAccount(acc, { fresh: true });
}
// Register a full (seed) wallet as an account and open it.
@@ -561,6 +530,7 @@ async function enterWallet(mnemonic, passphrase, opts = {}) {
async function activateAccount(acc, opts = {}) {
activeId = acc.id;
if (acc.type === 'watch') wallet.load({ xpub: acc.xpub, netName: 'mainnet', offline: false });
+ else if (acc.xprv) wallet.load({ xprv: acc.xprv, 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
@@ -632,7 +602,7 @@ 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 credId = (a) => (a.type === 'watch' ? 'w:' + a.xpub : a.xprv ? 'x:' + a.xprv : 'f:' + a.mnemonic + '|' + (a.passphrase || ''));
const activeAccount = () => accounts.find((a) => a.id === activeId) || null;
function defaultLabel(type) {
@@ -726,7 +696,7 @@ function hasVault() { return !!loadVaultBlob(); }
function writeVault() {
if (vaultPassword == null) return;
const list = accounts.filter((a) => a.type === 'full' && a.persisted)
- .map((a) => ({ label: a.label, mnemonic: a.mnemonic, passphrase: a.passphrase || '' }));
+ .map((a) => (a.xprv ? { label: a.label, xprv: a.xprv } : { label: a.label, mnemonic: a.mnemonic, passphrase: a.passphrase || '' }));
try {
if (!list.length) localStorage.removeItem(VAULT_KEY);
else localStorage.setItem(VAULT_KEY, JSON.stringify(encryptVault(list, vaultPassword)));
@@ -735,7 +705,11 @@ function writeVault() {
function mergeVaultList(list) {
for (const v of list) {
- const acc = addOrGetAccount({ type: 'full', label: v.label || defaultLabel('full'), mnemonic: v.mnemonic, passphrase: v.passphrase || '' });
+ const acc = addOrGetAccount(
+ v.xprv
+ ? { type: 'full', label: v.label || defaultLabel('full'), xprv: v.xprv }
+ : { type: 'full', label: v.label || defaultLabel('full'), mnemonic: v.mnemonic, passphrase: v.passphrase || '' }
+ );
acc.persisted = true;
}
}
@@ -786,7 +760,7 @@ function unlockVault() {
try { list = decryptVault(loadVaultBlob(), ui.vaultPw); } catch { ui.vaultError = t('pwWrong'); render(); return; }
vaultPassword = ui.vaultPw;
accounts = list
- .map((v) => ({ id: genId(), type: 'full', label: v.label || defaultLabel('full'), mnemonic: v.mnemonic, passphrase: v.passphrase || '', persisted: true }))
+ .map((v) => ({ id: genId(), type: 'full', label: v.label || defaultLabel('full'), mnemonic: v.mnemonic || '', passphrase: v.passphrase || '', xprv: v.xprv || '', persisted: true }))
.concat(loadWatchAccounts());
ui.vaultPw = '';
ui.vaultError = '';
@@ -894,6 +868,11 @@ function settingsTab() {
h('h3', {}, t('watchOnly')),
h('p', { class: 'small muted', style: 'margin:0' }, t('watchOnlyNote'))
)
+ : !wallet.mnemonic
+ ? h('div', { class: 'card col' },
+ h('h3', {}, t('importedKey')),
+ h('p', { class: 'small muted', style: 'margin:0' }, t('importedKeyNote'))
+ )
: h(
'div',
{ class: 'card col' },
@@ -935,7 +914,7 @@ function settingsTab() {
wallet.scanning ? t('scanning') : t('rescanWallet'))
),
explorerCard(),
- wallet.watchOnly ? null : syncCard(),
+ wallet.watchOnly || !wallet.mnemonic ? null : syncCard(),
h(
'div',
{ class: 'card col' },
diff --git a/src/i18n.js b/src/i18n.js
index 9a48796..0615ec9 100644
--- a/src/i18n.js
+++ b/src/i18n.js
@@ -92,6 +92,11 @@ const STR = {
show: 'Show',
hide: 'Hide',
invalidSeed: 'Invalid seed phrase — check the spelling and word order.',
+ importLabel: 'Recovery phrase or key',
+ importPlaceholder: 'Your 12 or 24 words, or an xpub / xprv',
+ invalidImport: 'That is not a valid recovery phrase, xpub, or xprv.',
+ importedKey: 'Imported key',
+ importedKeyNote: 'This wallet was imported from an extended private key, so there is no recovery phrase. Back up that key itself to keep the wallet.',
keysNote: 'Keys never leave your browser. Works fully offline. Save this page and open it from your filesystem.',
howItWorks: 'How it works',
installApp: 'Install app',
diff --git a/src/locales/ar.json b/src/locales/ar.json
index 3522b15..ccbcfa7 100644
--- a/src/locales/ar.json
+++ b/src/locales/ar.json
@@ -189,7 +189,6 @@
"publicKeyDesc": "شارك هذا لمراقبة هذه المحفظة للقراءة فقط على جهاز أو تطبيق آخر. يكشف عناوينك، لا مفاتيحك.",
"copyKey": "نسخ المفتاح",
"showPublicKey": "إظهار المفتاح العام",
- "remove": "إزالة",
"saveDevice": "الحفظ على هذا الجهاز",
"forgetDevice": "محفوظة · نسيان",
"savedTag": "محفوظة",
@@ -206,5 +205,10 @@
"unlockSavedDesc": "أدخل كلمة السر لفتح المحافظ المحفوظة على هذا الجهاز.",
"unlock": "فتح",
"useAnotherWallet": "استخدام محفظة أخرى",
- "save": "حفظ"
+ "save": "حفظ",
+ "importLabel": "عبارة الاسترداد أو المفتاح",
+ "importPlaceholder": "كلماتك الـ 12 أو 24، أو xpub / xprv",
+ "invalidImport": "هذه ليست عبارة استرداد أو xpub أو xprv صالحة.",
+ "importedKey": "مفتاح مُستورد",
+ "importedKeyNote": "تم استيراد هذه المحفظة من مفتاح خاص ممتد، لذا لا توجد عبارة استرداد. احتفظ بنسخة من هذا المفتاح نفسه للحفاظ على المحفظة."
} \ No newline at end of file
diff --git a/src/locales/bn.json b/src/locales/bn.json
index 45b2df8..2d9d8ca 100644
--- a/src/locales/bn.json
+++ b/src/locales/bn.json
@@ -174,7 +174,6 @@
"accounts": "অ্যাকাউন্ট",
"addWallet": "ওয়ালেট যোগ করুন",
"lockAll": "সব লক করুন",
- "remove": "সরান",
"watchOnly": "শুধু দেখুন",
"watchOnlyTag": "শুধু-দেখা",
"watchOnlyAdd": "শুধু-দেখা যোগ করুন",
@@ -204,5 +203,10 @@
"unlockSavedDesc": "এই ডিভাইসে সেভ করা ওয়ালেটগুলো খুলতে আপনার পাসওয়ার্ড লিখুন।",
"unlock": "আনলক করুন",
"useAnotherWallet": "অন্য একটি ওয়ালেট ব্যবহার করুন",
- "save": "সেভ করুন"
+ "save": "সেভ করুন",
+ "importLabel": "রিকভারি ফ্রেজ বা কী",
+ "importPlaceholder": "আপনার 12 বা 24টি শব্দ, অথবা একটি xpub / xprv",
+ "invalidImport": "এটি বৈধ রিকভারি ফ্রেজ, xpub বা xprv নয়।",
+ "importedKey": "আমদানি করা কী",
+ "importedKeyNote": "এই ওয়ালেটটি একটি বর্ধিত প্রাইভেট কী থেকে আমদানি করা হয়েছে, তাই কোনো রিকভারি ফ্রেজ নেই। ওয়ালেট রাখতে সেই কী-টিরই ব্যাকআপ নিন।"
} \ No newline at end of file
diff --git a/src/locales/de.json b/src/locales/de.json
index 515a3f2..6c73d09 100644
--- a/src/locales/de.json
+++ b/src/locales/de.json
@@ -205,5 +205,10 @@
"unlockSavedDesc": "Gib dein Passwort ein, um die auf diesem Gerät gespeicherten Wallets zu öffnen.",
"unlock": "Entsperren",
"useAnotherWallet": "Andere Wallet verwenden",
- "save": "Speichern"
-}
+ "save": "Speichern",
+ "importLabel": "Wiederherstellungsphrase oder Schlüssel",
+ "importPlaceholder": "Deine 12 oder 24 Wörter oder ein xpub / xprv",
+ "invalidImport": "Das ist keine gültige Wiederherstellungsphrase, kein xpub oder xprv.",
+ "importedKey": "Importierter Schlüssel",
+ "importedKeyNote": "Diese Wallet wurde aus einem erweiterten privaten Schlüssel importiert und hat daher keine Wiederherstellungsphrase. Sichere diesen Schlüssel selbst, um die Wallet zu behalten."
+} \ No newline at end of file
diff --git a/src/locales/es.json b/src/locales/es.json
index 09b17f1..d88cc22 100644
--- a/src/locales/es.json
+++ b/src/locales/es.json
@@ -205,5 +205,10 @@
"unlockSavedDesc": "Introduce tu contraseña para abrir los monederos guardados en este dispositivo.",
"unlock": "Desbloquear",
"useAnotherWallet": "Usar otro monedero",
- "save": "Guardar"
+ "save": "Guardar",
+ "importLabel": "Frase de recuperación o clave",
+ "importPlaceholder": "Tus 12 o 24 palabras, o un xpub / xprv",
+ "invalidImport": "Eso no es una frase de recuperación, xpub o xprv válida.",
+ "importedKey": "Clave importada",
+ "importedKeyNote": "Esta cartera se importó desde una clave privada extendida, así que no tiene frase de recuperación. Guarda esa clave para conservar la cartera."
} \ No newline at end of file
diff --git a/src/locales/fr.json b/src/locales/fr.json
index 6aa57be..72acc0c 100644
--- a/src/locales/fr.json
+++ b/src/locales/fr.json
@@ -205,5 +205,10 @@
"unlockSavedDesc": "Saisissez votre mot de passe pour ouvrir les portefeuilles enregistrés sur cet appareil.",
"unlock": "Déverrouiller",
"useAnotherWallet": "Utiliser un autre portefeuille",
- "save": "Enregistrer"
-}
+ "save": "Enregistrer",
+ "importLabel": "Phrase de récupération ou clé",
+ "importPlaceholder": "Vos 12 ou 24 mots, ou un xpub / xprv",
+ "invalidImport": "Ce n’est pas une phrase de récupération, un xpub ou un xprv valide.",
+ "importedKey": "Clé importée",
+ "importedKeyNote": "Ce portefeuille a été importé depuis une clé privée étendue, il n’a donc pas de phrase de récupération. Sauvegardez cette clé pour conserver le portefeuille."
+} \ No newline at end of file
diff --git a/src/locales/hi.json b/src/locales/hi.json
index c7418cb..8a37f15 100644
--- a/src/locales/hi.json
+++ b/src/locales/hi.json
@@ -205,5 +205,10 @@
"unlockSavedDesc": "इस डिवाइस पर सहेजे गए वॉलेट खोलने के लिए अपना पासवर्ड दर्ज करें।",
"unlock": "अनलॉक करें",
"useAnotherWallet": "दूसरा वॉलेट उपयोग करें",
- "save": "सहेजें"
-}
+ "save": "सहेजें",
+ "importLabel": "रिकवरी वाक्यांश या कुंजी",
+ "importPlaceholder": "आपके 12 या 24 शब्द, या xpub / xprv",
+ "invalidImport": "यह मान्य रिकवरी वाक्यांश, xpub या xprv नहीं है।",
+ "importedKey": "आयातित कुंजी",
+ "importedKeyNote": "यह वॉलेट एक विस्तारित निजी कुंजी से आयात किया गया था, इसलिए कोई रिकवरी वाक्यांश नहीं है। वॉलेट बनाए रखने के लिए उस कुंजी का बैकअप लें।"
+} \ No newline at end of file
diff --git a/src/locales/id.json b/src/locales/id.json
index d5d9bbc..7690eae 100644
--- a/src/locales/id.json
+++ b/src/locales/id.json
@@ -205,5 +205,10 @@
"unlockSavedDesc": "Masukkan kata sandi Anda untuk membuka dompet yang tersimpan di perangkat ini.",
"unlock": "Buka",
"useAnotherWallet": "Gunakan dompet lain",
- "save": "Simpan"
+ "save": "Simpan",
+ "importLabel": "Frasa pemulihan atau kunci",
+ "importPlaceholder": "12 atau 24 kata Anda, atau xpub / xprv",
+ "invalidImport": "Itu bukan frasa pemulihan, xpub, atau xprv yang valid.",
+ "importedKey": "Kunci yang diimpor",
+ "importedKeyNote": "Dompet ini diimpor dari kunci privat yang diperluas, jadi tidak ada frasa pemulihan. Cadangkan kunci itu sendiri untuk menyimpan dompet."
} \ No newline at end of file
diff --git a/src/locales/ja.json b/src/locales/ja.json
index 55ea846..dc6017d 100644
--- a/src/locales/ja.json
+++ b/src/locales/ja.json
@@ -205,5 +205,10 @@
"unlockSavedDesc": "この端末に保存したウォレットを開くにはパスワードを入力してください。",
"unlock": "解除",
"useAnotherWallet": "別のウォレットを使う",
- "save": "保存"
-}
+ "save": "保存",
+ "importLabel": "復元フレーズまたは鍵",
+ "importPlaceholder": "12 または 24 個の単語、または xpub / xprv",
+ "invalidImport": "有効な復元フレーズ、xpub、xprv ではありません。",
+ "importedKey": "インポートした鍵",
+ "importedKeyNote": "このウォレットは拡張秘密鍵からインポートされたため、復元フレーズはありません。ウォレットを保持するにはその鍵自体をバックアップしてください。"
+} \ No newline at end of file
diff --git a/src/locales/ko.json b/src/locales/ko.json
index dbe6775..6ef7917 100644
--- a/src/locales/ko.json
+++ b/src/locales/ko.json
@@ -205,5 +205,10 @@
"unlockSavedDesc": "이 기기에 저장된 지갑을 열려면 비밀번호를 입력하세요.",
"unlock": "잠금 해제",
"useAnotherWallet": "다른 지갑 사용",
- "save": "저장"
-}
+ "save": "저장",
+ "importLabel": "복구 문구 또는 키",
+ "importPlaceholder": "12개 또는 24개 단어, 또는 xpub / xprv",
+ "invalidImport": "유효한 복구 문구, xpub 또는 xprv가 아닙니다.",
+ "importedKey": "가져온 키",
+ "importedKeyNote": "이 지갑은 확장 개인 키에서 가져왔으므로 복구 문구가 없습니다. 지갑을 유지하려면 그 키 자체를 백업하세요."
+} \ No newline at end of file
diff --git a/src/locales/mr.json b/src/locales/mr.json
index 2481153..4505c45 100644
--- a/src/locales/mr.json
+++ b/src/locales/mr.json
@@ -205,5 +205,10 @@
"unlockSavedDesc": "या डिव्हाइसवर सेव्ह केलेली वॉलेट उघडण्यासाठी तुमचा पासवर्ड टाका.",
"unlock": "अनलॉक करा",
"useAnotherWallet": "दुसरे वॉलेट वापरा",
- "save": "सेव्ह करा"
-}
+ "save": "सेव्ह करा",
+ "importLabel": "पुनर्प्राप्ती वाक्यांश किंवा की",
+ "importPlaceholder": "तुमचे 12 किंवा 24 शब्द, किंवा xpub / xprv",
+ "invalidImport": "हा वैध पुनर्प्राप्ती वाक्यांश, xpub किंवा xprv नाही.",
+ "importedKey": "आयात केलेली की",
+ "importedKeyNote": "हे वॉलेट विस्तारित खाजगी कीवरून आयात केले होते, त्यामुळे पुनर्प्राप्ती वाक्यांश नाही. वॉलेट टिकवण्यासाठी तीच की बॅकअप करा."
+} \ No newline at end of file
diff --git a/src/locales/pt.json b/src/locales/pt.json
index fdb7828..83d5fb7 100644
--- a/src/locales/pt.json
+++ b/src/locales/pt.json
@@ -176,7 +176,6 @@
"accounts": "Contas",
"addWallet": "Adicionar carteira",
"lockAll": "Bloquear tudo",
- "remove": "Remover",
"watchOnly": "Somente leitura",
"watchOnlyTag": "somente leitura",
"watchOnlyAdd": "Adicionar somente leitura",
@@ -206,5 +205,10 @@
"unlockSavedDesc": "Digite sua senha para abrir as carteiras salvas neste dispositivo.",
"unlock": "Desbloquear",
"useAnotherWallet": "Usar outra carteira",
- "save": "Salvar"
+ "save": "Salvar",
+ "importLabel": "Frase de recuperação ou chave",
+ "importPlaceholder": "Suas 12 ou 24 palavras, ou um xpub / xprv",
+ "invalidImport": "Isso não é uma frase de recuperação, xpub ou xprv válida.",
+ "importedKey": "Chave importada",
+ "importedKeyNote": "Esta carteira foi importada de uma chave privada estendida, então não há frase de recuperação. Faça backup dessa chave para manter a carteira."
} \ No newline at end of file
diff --git a/src/locales/ru.json b/src/locales/ru.json
index 1649fd6..0cc1e49 100644
--- a/src/locales/ru.json
+++ b/src/locales/ru.json
@@ -176,7 +176,6 @@
"accounts": "Счета",
"addWallet": "Добавить кошелёк",
"lockAll": "Заблокировать все",
- "remove": "Удалить",
"watchOnly": "Только просмотр",
"watchOnlyTag": "только просмотр",
"watchOnlyAdd": "Добавить только для просмотра",
@@ -206,5 +205,10 @@
"unlockSavedDesc": "Введите пароль, чтобы открыть кошельки, сохранённые на этом устройстве.",
"unlock": "Разблокировать",
"useAnotherWallet": "Использовать другой кошелёк",
- "save": "Сохранить"
+ "save": "Сохранить",
+ "importLabel": "Фраза восстановления или ключ",
+ "importPlaceholder": "Ваши 12 или 24 слова, либо xpub / xprv",
+ "invalidImport": "Это недействительная фраза восстановления, xpub или xprv.",
+ "importedKey": "Импортированный ключ",
+ "importedKeyNote": "Этот кошелёк импортирован из расширенного приватного ключа, поэтому у него нет фразы восстановления. Сохраните сам этот ключ, чтобы не потерять кошелёк."
} \ No newline at end of file
diff --git a/src/locales/sw.json b/src/locales/sw.json
index 5876383..6480d5b 100644
--- a/src/locales/sw.json
+++ b/src/locales/sw.json
@@ -176,7 +176,6 @@
"accounts": "Akaunti",
"addWallet": "Ongeza pochi",
"lockAll": "Funga zote",
- "remove": "Ondoa",
"watchOnly": "Kutazama tu",
"watchOnlyTag": "kutazama tu",
"watchOnlyAdd": "Ongeza ya kutazama tu",
@@ -206,5 +205,10 @@
"unlockSavedDesc": "Ingiza nenosiri lako ili kufungua pochi zilizohifadhiwa kwenye kifaa hiki.",
"unlock": "Fungua",
"useAnotherWallet": "Tumia pochi nyingine",
- "save": "Hifadhi"
+ "save": "Hifadhi",
+ "importLabel": "Kifungu cha kurejesha au ufunguo",
+ "importPlaceholder": "Maneno yako 12 au 24, au xpub / xprv",
+ "invalidImport": "Hiyo si kifungu sahihi cha kurejesha, xpub, au xprv.",
+ "importedKey": "Ufunguo ulioagizwa",
+ "importedKeyNote": "Pochi hii iliagizwa kutoka kwa ufunguo wa faragha uliopanuliwa, kwa hiyo hakuna kifungu cha kurejesha. Hifadhi ufunguo huo wenyewe ili kuendelea kuwa na pochi."
} \ No newline at end of file
diff --git a/src/locales/ta.json b/src/locales/ta.json
index f772d43..a8c8f57 100644
--- a/src/locales/ta.json
+++ b/src/locales/ta.json
@@ -176,7 +176,6 @@
"accounts": "கணக்குகள்",
"addWallet": "வாலட்டைச் சேர்",
"lockAll": "அனைத்தையும் பூட்டு",
- "remove": "அகற்று",
"watchOnly": "பார்வை மட்டும்",
"watchOnlyTag": "பார்வை மட்டும்",
"watchOnlyAdd": "பார்வை மட்டும் சேர்",
@@ -206,5 +205,10 @@
"unlockSavedDesc": "இந்தச் சாதனத்தில் சேமித்த வாலட்டுகளைத் திறக்க உங்கள் கடவுச்சொல்லை உள்ளிடவும்.",
"unlock": "திற",
"useAnotherWallet": "வேறு வாலட்டைப் பயன்படுத்து",
- "save": "சேமி"
+ "save": "சேமி",
+ "importLabel": "மீட்பு சொற்றொடர் அல்லது விசை",
+ "importPlaceholder": "உங்கள் 12 அல்லது 24 சொற்கள், அல்லது xpub / xprv",
+ "invalidImport": "அது செல்லுபடியாகும் மீட்பு சொற்றொடர், xpub அல்லது xprv அல்ல.",
+ "importedKey": "இறக்குமதி செய்த விசை",
+ "importedKeyNote": "இந்த வாலெட் ஒரு நீட்டிக்கப்பட்ட தனிப்பட்ட விசையிலிருந்து இறக்குமதி செய்யப்பட்டது, எனவே மீட்பு சொற்றொடர் இல்லை. வாலெட்டை வைத்திருக்க அந்த விசையையே காப்புப் பிரதி எடுக்கவும்."
} \ No newline at end of file
diff --git a/src/locales/te.json b/src/locales/te.json
index babcb33..9e7534c 100644
--- a/src/locales/te.json
+++ b/src/locales/te.json
@@ -174,7 +174,6 @@
"accounts": "ఖాతాలు",
"addWallet": "వాలెట్ జోడించు",
"lockAll": "అన్నీ లాక్ చేయి",
- "remove": "తొలగించు",
"watchOnly": "చూడటం మాత్రమే",
"watchOnlyTag": "చూడటం మాత్రమే",
"watchOnlyAdd": "చూడటం-మాత్రమే జోడించు",
@@ -204,5 +203,10 @@
"unlockSavedDesc": "ఈ పరికరంలో సేవ్ చేసిన వాలెట్‌లను తెరవడానికి మీ పాస్‌వర్డ్ నమోదు చేయండి.",
"unlock": "అన్‌లాక్",
"useAnotherWallet": "మరో వాలెట్ వాడండి",
- "save": "సేవ్ చేయి"
+ "save": "సేవ్ చేయి",
+ "importLabel": "రికవరీ పదబంధం లేదా కీ",
+ "importPlaceholder": "మీ 12 లేదా 24 పదాలు, లేదా ఒక xpub / xprv",
+ "invalidImport": "అది చెల్లుబాటు అయ్యే రికవరీ పదబంధం, xpub లేదా xprv కాదు.",
+ "importedKey": "దిగుమతి చేసిన కీ",
+ "importedKeyNote": "ఈ వాలెట్ విస్తరించిన ప్రైవేట్ కీ నుండి దిగుమతి చేయబడింది, కాబట్టి రికవరీ పదబంధం లేదు. వాలెట్‌ను ఉంచుకోవడానికి ఆ కీనే బ్యాకప్ చేయండి."
} \ No newline at end of file
diff --git a/src/locales/tr.json b/src/locales/tr.json
index 50acb8c..1fa5880 100644
--- a/src/locales/tr.json
+++ b/src/locales/tr.json
@@ -205,5 +205,10 @@
"unlockSavedDesc": "Bu cihaza kaydedilen cüzdanları açmak için parolanızı girin.",
"unlock": "Aç",
"useAnotherWallet": "Başka bir cüzdan kullan",
- "save": "Kaydet"
+ "save": "Kaydet",
+ "importLabel": "Kurtarma ifadesi veya anahtar",
+ "importPlaceholder": "12 veya 24 kelimeniz ya da bir xpub / xprv",
+ "invalidImport": "Bu geçerli bir kurtarma ifadesi, xpub veya xprv değil.",
+ "importedKey": "İçe aktarılan anahtar",
+ "importedKeyNote": "Bu cüzdan genişletilmiş bir özel anahtardan içe aktarıldı, bu yüzden kurtarma ifadesi yok. Cüzdanı korumak için o anahtarın kendisini yedekleyin."
} \ No newline at end of file
diff --git a/src/locales/ur.json b/src/locales/ur.json
index 55f7801..cddfcc6 100644
--- a/src/locales/ur.json
+++ b/src/locales/ur.json
@@ -176,7 +176,6 @@
"accounts": "اکاؤنٹس",
"addWallet": "والیٹ شامل کریں",
"lockAll": "سب کو لاک کریں",
- "remove": "ہٹائیں",
"watchOnly": "صرف دیکھنے کے لیے",
"watchOnlyTag": "صرف دیکھنے کے لیے",
"watchOnlyAdd": "صرف دیکھنے والا شامل کریں",
@@ -206,5 +205,10 @@
"unlockSavedDesc": "اس ڈیوائس پر محفوظ شدہ والیٹ کھولنے کے لیے اپنا پاس ورڈ درج کریں۔",
"unlock": "کھولیں",
"useAnotherWallet": "کوئی اور والیٹ استعمال کریں",
- "save": "محفوظ کریں"
+ "save": "محفوظ کریں",
+ "importLabel": "ریکوری فقرہ یا کلید",
+ "importPlaceholder": "آپ کے 12 یا 24 الفاظ، یا ایک xpub / xprv",
+ "invalidImport": "یہ درست ریکوری فقرہ، xpub یا xprv نہیں ہے۔",
+ "importedKey": "درآمد شدہ کلید",
+ "importedKeyNote": "یہ والیٹ ایک توسیعی نجی کلید سے درآمد کیا گیا تھا، اس لیے کوئی ریکوری فقرہ نہیں ہے۔ والیٹ برقرار رکھنے کے لیے اسی کلید کا بیک اپ رکھیں۔"
} \ No newline at end of file
diff --git a/src/locales/vi.json b/src/locales/vi.json
index 0c319f1..87ab44f 100644
--- a/src/locales/vi.json
+++ b/src/locales/vi.json
@@ -205,5 +205,10 @@
"unlockSavedDesc": "Nhập mật khẩu để mở các ví đã lưu trên thiết bị này.",
"unlock": "Mở khóa",
"useAnotherWallet": "Dùng ví khác",
- "save": "Lưu"
-}
+ "save": "Lưu",
+ "importLabel": "Cụm từ khôi phục hoặc khóa",
+ "importPlaceholder": "12 hoặc 24 từ của bạn, hoặc một xpub / xprv",
+ "invalidImport": "Đó không phải là cụm từ khôi phục, xpub hoặc xprv hợp lệ.",
+ "importedKey": "Khóa đã nhập",
+ "importedKeyNote": "Ví này được nhập từ một khóa riêng tư mở rộng, nên không có cụm từ khôi phục. Hãy sao lưu chính khóa đó để giữ ví."
+} \ No newline at end of file
diff --git a/src/locales/zh.json b/src/locales/zh.json
index 72bc340..35bd3a5 100644
--- a/src/locales/zh.json
+++ b/src/locales/zh.json
@@ -176,7 +176,6 @@
"accounts": "账户",
"addWallet": "添加钱包",
"lockAll": "全部锁定",
- "remove": "移除",
"watchOnly": "仅查看",
"watchOnlyTag": "仅查看",
"watchOnlyAdd": "添加仅查看",
@@ -206,5 +205,10 @@
"unlockSavedDesc": "输入你的密码以打开此设备上保存的钱包。",
"unlock": "解锁",
"useAnotherWallet": "使用其他钱包",
- "save": "保存"
+ "save": "保存",
+ "importLabel": "恢复短语或密钥",
+ "importPlaceholder": "你的 12 或 24 个单词,或 xpub / xprv",
+ "invalidImport": "这不是有效的恢复短语、xpub 或 xprv。",
+ "importedKey": "导入的密钥",
+ "importedKeyNote": "该钱包是从扩展私钥导入的,因此没有恢复短语。请备份该密钥本身以保留钱包。"
} \ No newline at end of file
diff --git a/src/wallet.js b/src/wallet.js
index 9e7bb32..82bdae6 100644
--- a/src/wallet.js
+++ b/src/wallet.js
@@ -114,12 +114,13 @@ export class Wallet {
}
// --- setup --------------------------------------------------------------
- load({ mnemonic = '', passphrase = '', xpub = '', netName = 'mainnet', offline = false }) {
+ load({ mnemonic = '', passphrase = '', xpub = '', xprv = '', netName = 'mainnet', offline = false }) {
this.stopRealtime();
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.xprv = xprv || ''; // spending wallet imported from an extended private key
+ this.watchOnly = !!this.xpub && !this.mnemonic && !this.xprv; // view/receive only
this.netName = netName;
this.offline = offline;
this.api = new Api(netName);
@@ -176,24 +177,27 @@ export class Wallet {
// --- derivation ---------------------------------------------------------
account() {
- // Watch-only wallets are loaded directly from the account public key.
+ const { coin } = this.netCfg;
+ // Cache key identifies the source so a reload rebuilds when it changes.
+ const key = this.watchOnly ? 'pub:' + this.xpub
+ : this.xprv ? 'prv:' + this.xprv
+ : `${this.netName}|${this.mnemonic}|${this.passphrase}`;
+ if (this._account && this._accountKey === key) return this._account;
+ let acct;
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;
+ acct = HDKey.fromExtendedKey(this.xpub); // account-level public key
+ } else if (this.xprv) {
+ // A master xprv (depth 0) needs the BIP84 account path derived from it;
+ // an already account-level xprv is used as-is.
+ const node = HDKey.fromExtendedKey(this.xprv);
+ acct = node.depth === 0 ? node.derive(`m/84'/${coin}'/0'`) : node;
+ } else {
+ acct = HDKey.fromMasterSeed(mnemonicToSeedSync(this.mnemonic, this.passphrase)).derive(`m/84'/${coin}'/0'`);
}
- const key = `${this.netName}|${this.mnemonic}|${this.passphrase}`;
- if (this._account && this._accountKey === key) return this._account;
- const seed = mnemonicToSeedSync(this.mnemonic, this.passphrase);
- const root = HDKey.fromMasterSeed(seed);
- const { coin } = this.netCfg;
- this._account = root.derive(`m/84'/${coin}'/0'`);
+ this._account = acct;
this._accountKey = key;
this._addrCache.clear();
- return this._account;
+ return acct;
}
// The account-level extended public key (xpub / zpub) for export → watch-only.
@@ -1144,7 +1148,7 @@ export class Wallet {
// then shows the last-known balance/history instantly while a fresh scan
// runs in the background.
_cacheKey() {
- const id = this.watchOnly ? this.xpub : `${this.mnemonic}\n${this.passphrase}`;
+ const id = this.watchOnly ? this.xpub : this.xprv ? this.xprv : `${this.mnemonic}\n${this.passphrase}`;
const bytes = new TextEncoder().encode(id);
return 'btc-wallet-cache:' + hex.encode(sha256(bytes)).slice(0, 32);
}
@@ -1329,26 +1333,35 @@ export function utxoId(u) {
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) {
+const _XPRV_VER = Uint8Array.from([0x04, 0x88, 0xad, 0xe4]);
+const _ZPRV_VER = Uint8Array.from([0x04, 0xb2, 0x43, 0x0c]);
+
+// Classify a pasted extended key. xpub/zpub → public (watch-only); xprv/zprv →
+// private (spending). Version bytes are normalized to the standard xpub/xprv set
+// (key material is identical; only the prefix differs) so HDKey can load it.
+// Returns { kind: 'xpub' | 'xprv', key } or throws.
+export function parseExtendedKey(s) {
let data;
try {
data = _b58c.decode((s || '').trim());
} catch {
- throw new Error('Not a valid extended public key.');
+ throw new Error('Not a valid recovery phrase or key.');
}
- if (data.length !== 78) throw new Error('Not a valid extended public key.');
+ if (data.length !== 78) throw new Error('Not a valid recovery phrase or 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).');
+ let kind, norm;
+ if (ver === hex.encode(_XPUB_VER) || ver === hex.encode(_ZPUB_VER)) { kind = 'xpub'; norm = _XPUB_VER; }
+ else if (ver === hex.encode(_XPRV_VER) || ver === hex.encode(_ZPRV_VER)) { kind = 'xprv'; norm = _XPRV_VER; }
+ else throw new Error('Unrecognized key type — use a native-segwit xpub/zpub or xprv/zprv.');
const out = new Uint8Array(data);
- out.set(_XPUB_VER, 0);
- const xpub = _b58c.encode(out);
+ out.set(norm, 0);
+ const key = _b58c.encode(out);
try {
- HDKey.fromExtendedKey(xpub);
+ HDKey.fromExtendedKey(key);
} catch {
- throw new Error('Not a valid extended public key.');
+ throw new Error('Not a valid extended key.');
}
- return xpub;
+ return { kind, key };
}
// Encrypted vault for persisting seed-bearing accounts on this device. Key is