diff --git a/src/popup/index.html b/src/popup/index.html
index d5bbf90..0f39d15 100644
--- a/src/popup/index.html
+++ b/src/popup/index.html
@@ -119,6 +119,15 @@
Import private key
+
diff --git a/src/popup/index.js b/src/popup/index.js
index 5a12180..bfdb6ee 100644
--- a/src/popup/index.js
+++ b/src/popup/index.js
@@ -11,6 +11,7 @@ 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,6 +56,7 @@ const ctx = {
doRefreshAndRender,
showAddWalletView: () => addWallet.show(),
showImportKeyView: () => importKey.show(),
+ showImportXprvView: () => importXprv.show(),
showAddressDetail: () => addressDetail.show(),
showAddressToken: () => addressToken.show(),
showAddTokenView: () => addToken.show(),
@@ -210,6 +212,7 @@ 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 eed7ac9..ca8c4c6 100644
--- a/src/popup/views/addWallet.js
+++ b/src/popup/views/addWallet.js
@@ -124,6 +124,11 @@ function init(ctx) {
"click",
ctx.showImportKeyView,
);
+
+ $("btn-add-wallet-import-xprv").addEventListener(
+ "click",
+ ctx.showImportXprvView,
+ );
}
module.exports = { init, show };
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/importXprv.js b/src/popup/views/importXprv.js
new file mode 100644
index 0000000..f1ba60d
--- /dev/null
+++ b/src/popup/views/importXprv.js
@@ -0,0 +1,106 @@
+const { $, showView, showFlash } = require("./helpers");
+const { hdWalletFromXprv, isValidXprv } = require("../../shared/wallet");
+const { encryptWithPassword } = require("../../shared/vault");
+const { state, saveState } = require("../../shared/state");
+const { scanForAddresses } = require("../../shared/balances");
+
+function show() {
+ $("import-xprv-key").value = "";
+ $("import-xprv-password").value = "";
+ $("import-xprv-password-confirm").value = "";
+ showView("import-xprv");
+}
+
+function init(ctx) {
+ $("btn-import-xprv-confirm").addEventListener("click", async () => {
+ 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 = $("import-xprv-password").value;
+ const pw2 = $("import-xprv-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(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();
+ });
+
+ $("btn-import-xprv-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..8abebf6 100644
--- a/src/shared/wallet.js
+++ b/src/shared/wallet.js
@@ -24,6 +24,25 @@ function hdWalletFromMnemonic(mnemonic) {
return { xpub, firstAddress };
}
+function hdWalletFromXprv(xprv) {
+ const node = HDNodeWallet.fromExtendedKey(xprv);
+ if (!node.privateKey) {
+ throw new Error("Not an extended private key (xprv).");
+ }
+ 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 +57,10 @@ function getSignerForAddress(walletData, addrIndex, decryptedSecret) {
);
return node.deriveChild(addrIndex);
}
+ if (walletData.type === "xprv") {
+ const node = HDNodeWallet.fromExtendedKey(decryptedSecret);
+ return node.deriveChild(addrIndex);
+ }
return new Wallet(decryptedSecret);
}
@@ -49,6 +72,8 @@ module.exports = {
generateMnemonic,
deriveAddressFromXpub,
hdWalletFromMnemonic,
+ hdWalletFromXprv,
+ isValidXprv,
addressFromPrivateKey,
getSignerForAddress,
isValidMnemonic,