- Write these words down and keep them safe. Anyone with them
- can take your funds; if you lose them, your wallet is gone.
+
+
+
+
+ Paste your private key below. This wallet will have a
+ single address.
+
+
+
+
+
+
+
+
+ Paste your extended private key (xprv) below. This will
+ import the HD wallet and scan for used addresses.
+
+
+
+
+
+
+
-
- This password encrypts your recovery phrase on this
- device. You will need it to send funds.
+
+ This password encrypts your secret on this device. You
+ will need it to send funds.
Add
-
- Have a private key instead?
-
-
-
- Have an extended private key (xprv)?
-
-
-
-
-
-
-
-
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.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Import Extended Private Key
-
- Paste your extended private key (xprv) below. This will
- import the HD wallet and scan for used addresses.
-
-
-
-
-
-
-
- This password encrypts your key on this device. You will
- need it to send funds.
-
-
-
-
-
-
-
-
diff --git a/src/popup/index.js b/src/popup/index.js
index bfdb6ee..29e27ab 100644
--- a/src/popup/index.js
+++ b/src/popup/index.js
@@ -10,8 +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 importXprv = require("./views/importXprv");
const addressDetail = require("./views/addressDetail");
const addressToken = require("./views/addressToken");
const send = require("./views/send");
@@ -55,8 +53,6 @@ const ctx = {
renderWalletList,
doRefreshAndRender,
showAddWalletView: () => addWallet.show(),
- showImportKeyView: () => importKey.show(),
- showImportXprvView: () => importXprv.show(),
showAddressDetail: () => addressDetail.show(),
showAddressToken: () => addressToken.show(),
showAddTokenView: () => addToken.show(),
@@ -211,8 +207,6 @@ async function init() {
welcome.init(ctx);
addWallet.init(ctx);
- importKey.init(ctx);
- importXprv.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 ca8c4c6..830602e 100644
--- a/src/popup/views/addWallet.js
+++ b/src/popup/views/addWallet.js
@@ -1,48 +1,62 @@
-const { $, showView, showFlash } = require("./helpers");
+const { $, showView, showFlash, goBack } = require("./helpers");
const {
generateMnemonic,
hdWalletFromMnemonic,
+ hdWalletFromXprv,
isValidMnemonic,
+ isValidXprv,
+ addressFromPrivateKey,
} = require("../../shared/wallet");
const { encryptWithPassword } = require("../../shared/vault");
const { state, saveState } = require("../../shared/state");
const { scanForAddresses } = require("../../shared/balances");
+let currentMode = "mnemonic"; // "mnemonic" | "key" | "xprv"
+
+const MODES = ["mnemonic", "key", "xprv"];
+
+function setMode(mode) {
+ currentMode = mode;
+ for (const m of MODES) {
+ const section = $("add-wallet-mode-" + m);
+ if (section) section.classList.toggle("hidden", m !== mode);
+ const btn = $("btn-mode-" + m);
+ if (btn) {
+ if (m === mode) {
+ btn.classList.add("bg-fg", "text-bg");
+ btn.classList.remove("hover:bg-fg", "hover:text-bg");
+ } else {
+ btn.classList.remove("bg-fg", "text-bg");
+ btn.classList.add("hover:bg-fg", "hover:text-bg");
+ }
+ }
+ }
+}
+
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");
+ setMode("mnemonic");
showView("add-wallet");
}
function init(ctx) {
+ // Mode switching
+ $("btn-mode-mnemonic").addEventListener("click", () => setMode("mnemonic"));
+ $("btn-mode-key").addEventListener("click", () => setMode("key"));
+ $("btn-mode-xprv").addEventListener("click", () => setMode("xprv"));
+
$("btn-generate-phrase").addEventListener("click", () => {
$("wallet-mnemonic").value = generateMnemonic();
$("add-wallet-phrase-warning").classList.remove("hidden");
});
$("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;
- }
- 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;
- }
+ // Shared password validation
const pw = $("add-wallet-password").value;
const pw2 = $("add-wallet-password-confirm").value;
if (!pw) {
@@ -57,78 +71,187 @@ function init(ctx) {
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);
+ if (currentMode === "mnemonic") {
+ await handleMnemonic(ctx, pw);
+ } else if (currentMode === "key") {
+ await handlePrivateKey(ctx, pw);
+ } else if (currentMode === "xprv") {
+ await handleXprv(ctx, pw);
}
-
- ctx.doRefreshAndRender();
});
$("btn-add-wallet-back").addEventListener("click", () => {
if (!state.hasWallet) {
- showView("welcome");
+ goBack("welcome");
} else {
ctx.renderWalletList();
- showView("main");
+ goBack("main");
}
});
+}
- $("btn-add-wallet-import-key").addEventListener(
- "click",
- ctx.showImportKeyView,
+async function handleMnemonic(ctx, pw) {
+ 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 { 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");
- $("btn-add-wallet-import-xprv").addEventListener(
- "click",
- ctx.showImportXprvView,
+ 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 handlePrivateKey(ctx, pw) {
+ 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 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 handleXprv(ctx, pw) {
+ 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 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");
+
+ 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();
}
module.exports = { init, show };
diff --git a/src/popup/views/helpers.js b/src/popup/views/helpers.js
index 8fdfe65..323e3e2 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",
@@ -34,6 +33,9 @@ const VIEWS = [
"export-privkey",
];
+// Simple view history stack for back-button navigation.
+const viewStack = [];
+
function $(id) {
return document.getElementById(id);
}
@@ -48,7 +50,13 @@ function hideError(id) {
$(id).classList.add("hidden");
}
-function showView(name) {
+function showView(name, opts) {
+ const skipPush = opts && opts.skipPush;
+ if (!skipPush && state.currentView && state.currentView !== name) {
+ viewStack.push(state.currentView);
+ // Keep the stack bounded to avoid unbounded growth.
+ if (viewStack.length > 20) viewStack.splice(0, viewStack.length - 20);
+ }
for (const v of VIEWS) {
const el = document.getElementById(`view-${v}`);
if (el) {
@@ -66,6 +74,17 @@ function showView(name) {
}
}
+// Navigate to the previous view in the stack. Falls back to fallbackView
+// (default "main") when the stack is empty.
+function goBack(fallbackView) {
+ const prev = viewStack.pop();
+ if (prev) {
+ showView(prev, { skipPush: true });
+ } else {
+ showView(fallbackView || "main", { skipPush: true });
+ }
+}
+
let flashTimer = null;
function clearFlash() {
@@ -264,6 +283,7 @@ module.exports = {
showError,
hideError,
showView,
+ goBack,
showFlash,
balanceLine,
balanceLinesForAddress,