- Write these words down and keep them safe. Anyone with them
- can take your funds; if you lose them, your wallet is gone.
+
+
+
-
-
Import Private Key
-
- Paste your private key below. This wallet will have a single
- address.
-
-
-
-
-
-
-
- This password encrypts your private key on this device.
- You will need it to send funds.
-
-
-
-
-
-
-
-
diff --git a/src/popup/index.js b/src/popup/index.js
index e0e42aa..f343d6f 100644
--- a/src/popup/index.js
+++ b/src/popup/index.js
@@ -10,7 +10,6 @@ const { $, showView } = require("./views/helpers");
const home = require("./views/home");
const welcome = require("./views/welcome");
const addWallet = require("./views/addWallet");
-const importKey = require("./views/importKey");
const addressDetail = require("./views/addressDetail");
const addressToken = require("./views/addressToken");
const send = require("./views/send");
@@ -54,7 +53,6 @@ const ctx = {
renderWalletList,
doRefreshAndRender,
showAddWalletView: () => addWallet.show(),
- showImportKeyView: () => importKey.show(),
showAddressDetail: () => addressDetail.show(),
showAddressToken: () => addressToken.show(),
showAddTokenView: () => addToken.show(),
@@ -217,7 +215,6 @@ async function init() {
welcome.init(ctx);
addWallet.init(ctx);
- importKey.init(ctx);
home.init(ctx);
addressDetail.init(ctx);
addressToken.init(ctx);
diff --git a/src/popup/views/addWallet.js b/src/popup/views/addWallet.js
index eed7ac9..9fcfed6 100644
--- a/src/popup/views/addWallet.js
+++ b/src/popup/views/addWallet.js
@@ -3,114 +3,285 @@ const {
generateMnemonic,
hdWalletFromMnemonic,
isValidMnemonic,
+ addressFromPrivateKey,
+ hdWalletFromXprv,
+ isValidXprv,
} = require("../../shared/wallet");
const { encryptWithPassword } = require("../../shared/vault");
const { state, saveState } = require("../../shared/state");
const { scanForAddresses } = require("../../shared/balances");
+let currentMode = "mnemonic";
+
+const MODES = ["mnemonic", "privkey", "xprv"];
+
+const PASSWORD_HINTS = {
+ mnemonic:
+ "This password encrypts your recovery phrase on this device. You will need it to send funds.",
+ privkey:
+ "This password encrypts your private key on this device. You will need it to send funds.",
+ xprv: "This password encrypts your key on this device. You will need it to send funds.",
+};
+
+function switchMode(mode) {
+ currentMode = mode;
+ for (const m of MODES) {
+ $("add-wallet-section-" + m).classList.toggle("hidden", m !== mode);
+ const tab = $("tab-" + m);
+ const isActive = m === mode;
+ // Active: bold, solid border on top/sides, no bottom border (connects to content)
+ tab.classList.toggle("font-bold", isActive);
+ tab.classList.toggle("border-solid", isActive);
+ tab.classList.toggle("border-border", isActive);
+ tab.classList.toggle("border-b-bg", isActive);
+ tab.classList.toggle("bg-bg", isActive);
+ // Inactive: muted text, dashed border on top/sides, transparent bottom, hover invert
+ tab.classList.toggle("text-muted", !isActive);
+ tab.classList.toggle("border-dashed", !isActive);
+ tab.classList.toggle("border-border-light", !isActive);
+ tab.classList.toggle("border-b-transparent", !isActive);
+ tab.classList.toggle("hover:bg-fg", !isActive);
+ tab.classList.toggle("hover:text-bg", !isActive);
+ }
+ $("add-wallet-password-hint").textContent = PASSWORD_HINTS[mode];
+}
+
function show() {
$("wallet-mnemonic").value = "";
+ $("import-private-key").value = "";
+ $("import-xprv-key").value = "";
$("add-wallet-password").value = "";
$("add-wallet-password-confirm").value = "";
$("add-wallet-phrase-warning").classList.add("hidden");
+ switchMode("mnemonic");
showView("add-wallet");
}
+function validatePassword() {
+ const pw = $("add-wallet-password").value;
+ const pw2 = $("add-wallet-password-confirm").value;
+ if (!pw) {
+ showFlash("Please choose a password.");
+ return null;
+ }
+ if (pw.length < 12) {
+ showFlash("Password must be at least 12 characters.");
+ return null;
+ }
+ if (pw !== pw2) {
+ showFlash("Passwords do not match.");
+ return null;
+ }
+ return pw;
+}
+
+async function importMnemonic(ctx) {
+ const mnemonic = $("wallet-mnemonic").value.trim();
+ if (!mnemonic) {
+ showFlash("Enter a recovery phrase or press the die to generate one.");
+ return;
+ }
+ const words = mnemonic.split(/\s+/);
+ if (words.length !== 12 && words.length !== 24) {
+ showFlash(
+ "Recovery phrase must be 12 or 24 words. You entered " +
+ words.length +
+ ".",
+ );
+ return;
+ }
+ if (!isValidMnemonic(mnemonic)) {
+ showFlash("Invalid recovery phrase. Check for typos.");
+ return;
+ }
+ const pw = validatePassword();
+ if (!pw) return;
+ const { xpub, firstAddress } = hdWalletFromMnemonic(mnemonic);
+ const duplicate = state.wallets.find(
+ (w) =>
+ w.type === "hd" &&
+ w.addresses[0] &&
+ w.addresses[0].address.toLowerCase() === firstAddress.toLowerCase(),
+ );
+ if (duplicate) {
+ showFlash(
+ "This recovery phrase is already added (" + duplicate.name + ").",
+ );
+ return;
+ }
+ const encrypted = await encryptWithPassword(mnemonic, pw);
+ const walletNum = state.wallets.length + 1;
+ const wallet = {
+ type: "hd",
+ name: "Wallet " + walletNum,
+ xpub: xpub,
+ encryptedSecret: encrypted,
+ nextIndex: 1,
+ addresses: [
+ { address: firstAddress, balance: "0.0000", tokenBalances: [] },
+ ],
+ };
+ state.wallets.push(wallet);
+ state.hasWallet = true;
+ await saveState();
+ ctx.renderWalletList();
+ showView("main");
+
+ // Scan for used HD addresses beyond index 0.
+ showFlash("Scanning for addresses...", 30000);
+ const scan = await scanForAddresses(xpub, state.rpcUrl);
+ if (scan.addresses.length > 1) {
+ wallet.addresses = scan.addresses.map((a) => ({
+ address: a.address,
+ balance: "0.0000",
+ tokenBalances: [],
+ }));
+ wallet.nextIndex = scan.nextIndex;
+ await saveState();
+ ctx.renderWalletList();
+ showFlash("Found " + scan.addresses.length + " addresses.");
+ } else {
+ showFlash("Ready.", 1000);
+ }
+
+ ctx.doRefreshAndRender();
+}
+
+async function importPrivateKey(ctx) {
+ const key = $("import-private-key").value.trim();
+ if (!key) {
+ showFlash("Please enter your private key.");
+ return;
+ }
+ let addr;
+ try {
+ addr = addressFromPrivateKey(key);
+ } catch (e) {
+ showFlash("Invalid private key.");
+ return;
+ }
+ const pw = validatePassword();
+ if (!pw) return;
+ const duplicate = state.wallets.find(
+ (w) =>
+ w.type === "key" &&
+ w.addresses[0] &&
+ w.addresses[0].address.toLowerCase() === addr.toLowerCase(),
+ );
+ if (duplicate) {
+ showFlash(
+ "This private key is already added (" + duplicate.name + ").",
+ );
+ return;
+ }
+ const encrypted = await encryptWithPassword(key, pw);
+ const walletNum = state.wallets.length + 1;
+ state.wallets.push({
+ type: "key",
+ name: "Wallet " + walletNum,
+ encryptedSecret: encrypted,
+ addresses: [{ address: addr, balance: "0.0000", tokenBalances: [] }],
+ });
+ state.hasWallet = true;
+ await saveState();
+ ctx.renderWalletList();
+ showView("main");
+
+ ctx.doRefreshAndRender();
+}
+
+async function importXprvKey(ctx) {
+ const xprv = $("import-xprv-key").value.trim();
+ if (!xprv) {
+ showFlash("Please enter your extended private key.");
+ return;
+ }
+ if (!isValidXprv(xprv)) {
+ showFlash("Invalid extended private key.");
+ return;
+ }
+ let result;
+ try {
+ result = hdWalletFromXprv(xprv);
+ } catch (e) {
+ showFlash("Invalid extended private key.");
+ return;
+ }
+ const { xpub, firstAddress } = result;
+ const duplicate = state.wallets.find(
+ (w) =>
+ (w.type === "hd" || w.type === "xprv") &&
+ w.addresses[0] &&
+ w.addresses[0].address.toLowerCase() === firstAddress.toLowerCase(),
+ );
+ if (duplicate) {
+ showFlash("This key is already added (" + duplicate.name + ").");
+ return;
+ }
+ const pw = validatePassword();
+ if (!pw) return;
+ const encrypted = await encryptWithPassword(xprv, pw);
+ const walletNum = state.wallets.length + 1;
+ const wallet = {
+ type: "xprv",
+ name: "Wallet " + walletNum,
+ xpub: xpub,
+ encryptedSecret: encrypted,
+ nextIndex: 1,
+ addresses: [
+ { address: firstAddress, balance: "0.0000", tokenBalances: [] },
+ ],
+ };
+ state.wallets.push(wallet);
+ state.hasWallet = true;
+ await saveState();
+ ctx.renderWalletList();
+ showView("main");
+
+ // Scan for used HD addresses beyond index 0.
+ showFlash("Scanning for addresses...", 30000);
+ const scan = await scanForAddresses(xpub, state.rpcUrl);
+ if (scan.addresses.length > 1) {
+ wallet.addresses = scan.addresses.map((a) => ({
+ address: a.address,
+ balance: "0.0000",
+ tokenBalances: [],
+ }));
+ wallet.nextIndex = scan.nextIndex;
+ await saveState();
+ ctx.renderWalletList();
+ showFlash("Found " + scan.addresses.length + " addresses.");
+ } else {
+ showFlash("Ready.", 1000);
+ }
+
+ ctx.doRefreshAndRender();
+}
+
function init(ctx) {
+ // Tab click handlers
+ $("tab-mnemonic").addEventListener("click", () => switchMode("mnemonic"));
+ $("tab-privkey").addEventListener("click", () => switchMode("privkey"));
+ $("tab-xprv").addEventListener("click", () => switchMode("xprv"));
+
+ // Generate mnemonic
$("btn-generate-phrase").addEventListener("click", () => {
$("wallet-mnemonic").value = generateMnemonic();
$("add-wallet-phrase-warning").classList.remove("hidden");
});
+ // Import / confirm
$("btn-add-wallet-confirm").addEventListener("click", async () => {
- const mnemonic = $("wallet-mnemonic").value.trim();
- if (!mnemonic) {
- showFlash(
- "Enter a recovery phrase or press the die to generate one.",
- );
- return;
+ if (currentMode === "mnemonic") {
+ await importMnemonic(ctx);
+ } else if (currentMode === "privkey") {
+ await importPrivateKey(ctx);
+ } else if (currentMode === "xprv") {
+ await importXprvKey(ctx);
}
- const words = mnemonic.split(/\s+/);
- if (words.length !== 12 && words.length !== 24) {
- showFlash(
- "Recovery phrase must be 12 or 24 words. You entered " +
- words.length +
- ".",
- );
- return;
- }
- if (!isValidMnemonic(mnemonic)) {
- showFlash("Invalid recovery phrase. Check for typos.");
- return;
- }
- const pw = $("add-wallet-password").value;
- const pw2 = $("add-wallet-password-confirm").value;
- if (!pw) {
- showFlash("Please choose a password.");
- return;
- }
- if (pw.length < 12) {
- showFlash("Password must be at least 12 characters.");
- return;
- }
- if (pw !== pw2) {
- showFlash("Passwords do not match.");
- return;
- }
- const { xpub, firstAddress } = hdWalletFromMnemonic(mnemonic);
- const duplicate = state.wallets.find(
- (w) =>
- w.type === "hd" &&
- w.addresses[0] &&
- w.addresses[0].address.toLowerCase() ===
- firstAddress.toLowerCase(),
- );
- if (duplicate) {
- showFlash(
- "This recovery phrase is already added (" +
- duplicate.name +
- ").",
- );
- return;
- }
- const encrypted = await encryptWithPassword(mnemonic, pw);
- const walletNum = state.wallets.length + 1;
- const wallet = {
- type: "hd",
- name: "Wallet " + walletNum,
- xpub: xpub,
- encryptedSecret: encrypted,
- nextIndex: 1,
- addresses: [
- { address: firstAddress, balance: "0.0000", tokenBalances: [] },
- ],
- };
- state.wallets.push(wallet);
- state.hasWallet = true;
- await saveState();
- ctx.renderWalletList();
- showView("main");
-
- // Scan for used HD addresses beyond index 0.
- showFlash("Scanning for addresses...", 30000);
- const scan = await scanForAddresses(xpub, state.rpcUrl);
- if (scan.addresses.length > 1) {
- wallet.addresses = scan.addresses.map((a) => ({
- address: a.address,
- balance: "0.0000",
- tokenBalances: [],
- }));
- wallet.nextIndex = scan.nextIndex;
- await saveState();
- ctx.renderWalletList();
- showFlash("Found " + scan.addresses.length + " addresses.");
- } else {
- showFlash("Ready.", 1000);
- }
-
- ctx.doRefreshAndRender();
});
+ // Back button
$("btn-add-wallet-back").addEventListener("click", () => {
if (!state.hasWallet) {
showView("welcome");
@@ -119,11 +290,6 @@ function init(ctx) {
showView("main");
}
});
-
- $("btn-add-wallet-import-key").addEventListener(
- "click",
- ctx.showImportKeyView,
- );
}
module.exports = { init, show };
diff --git a/src/popup/views/helpers.js b/src/popup/views/helpers.js
index 8fdfe65..2db8191 100644
--- a/src/popup/views/helpers.js
+++ b/src/popup/views/helpers.js
@@ -13,7 +13,6 @@ const { state, saveState } = require("../../shared/state");
const VIEWS = [
"welcome",
"add-wallet",
- "import-key",
"main",
"address",
"address-token",
diff --git a/src/popup/views/home.js b/src/popup/views/home.js
index ea923e0..68237e8 100644
--- a/src/popup/views/home.js
+++ b/src/popup/views/home.js
@@ -239,7 +239,7 @@ function render(ctx) {
html += `
`;
html += `
`;
html += `${wallet.name}`;
- if (wallet.type === "hd") {
+ if (wallet.type === "hd" || wallet.type === "xprv") {
html += ``;
}
html += `
`;
diff --git a/src/popup/views/importKey.js b/src/popup/views/importKey.js
deleted file mode 100644
index a3324a3..0000000
--- a/src/popup/views/importKey.js
+++ /dev/null
@@ -1,69 +0,0 @@
-const { $, showView, showFlash } = require("./helpers");
-const { addressFromPrivateKey } = require("../../shared/wallet");
-const { encryptWithPassword } = require("../../shared/vault");
-const { state, saveState } = require("../../shared/state");
-
-function show() {
- $("import-private-key").value = "";
- $("import-key-password").value = "";
- $("import-key-password-confirm").value = "";
- showView("import-key");
-}
-
-function init(ctx) {
- $("btn-import-key-confirm").addEventListener("click", async () => {
- const key = $("import-private-key").value.trim();
- if (!key) {
- showFlash("Please enter your private key.");
- return;
- }
- let addr;
- try {
- addr = addressFromPrivateKey(key);
- } catch (e) {
- showFlash("Invalid private key.");
- return;
- }
- const pw = $("import-key-password").value;
- const pw2 = $("import-key-password-confirm").value;
- if (!pw) {
- showFlash("Please choose a password.");
- return;
- }
- if (pw.length < 12) {
- showFlash("Password must be at least 12 characters.");
- return;
- }
- if (pw !== pw2) {
- showFlash("Passwords do not match.");
- return;
- }
- const encrypted = await encryptWithPassword(key, pw);
- const walletNum = state.wallets.length + 1;
- state.wallets.push({
- type: "key",
- name: "Wallet " + walletNum,
- encryptedSecret: encrypted,
- addresses: [
- { address: addr, balance: "0.0000", tokenBalances: [] },
- ],
- });
- state.hasWallet = true;
- await saveState();
- ctx.renderWalletList();
- showView("main");
-
- ctx.doRefreshAndRender();
- });
-
- $("btn-import-key-back").addEventListener("click", () => {
- if (!state.hasWallet) {
- showView("welcome");
- } else {
- ctx.renderWalletList();
- showView("main");
- }
- });
-}
-
-module.exports = { init, show };
diff --git a/src/shared/wallet.js b/src/shared/wallet.js
index c666add..ff29648 100644
--- a/src/shared/wallet.js
+++ b/src/shared/wallet.js
@@ -24,6 +24,26 @@ function hdWalletFromMnemonic(mnemonic) {
return { xpub, firstAddress };
}
+function hdWalletFromXprv(xprv) {
+ const root = HDNodeWallet.fromExtendedKey(xprv);
+ if (!root.privateKey) {
+ throw new Error("Not an extended private key (xprv).");
+ }
+ const node = root.derivePath("44'/60'/0'/0");
+ const xpub = node.neuter().extendedKey;
+ const firstAddress = node.deriveChild(0).address;
+ return { xpub, firstAddress };
+}
+
+function isValidXprv(key) {
+ try {
+ const node = HDNodeWallet.fromExtendedKey(key);
+ return !!node.privateKey;
+ } catch {
+ return false;
+ }
+}
+
function addressFromPrivateKey(key) {
const w = new Wallet(key);
return w.address;
@@ -38,6 +58,11 @@ function getSignerForAddress(walletData, addrIndex, decryptedSecret) {
);
return node.deriveChild(addrIndex);
}
+ if (walletData.type === "xprv") {
+ const root = HDNodeWallet.fromExtendedKey(decryptedSecret);
+ const node = root.derivePath("44'/60'/0'/0");
+ return node.deriveChild(addrIndex);
+ }
return new Wallet(decryptedSecret);
}
@@ -49,6 +74,8 @@ module.exports = {
generateMnemonic,
deriveAddressFromXpub,
hdWalletFromMnemonic,
+ hdWalletFromXprv,
+ isValidXprv,
addressFromPrivateKey,
getSignerForAddress,
isValidMnemonic,