Compare commits
28 Commits
3db8243a77
...
76320d1e1b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76320d1e1b | ||
|
|
304fbaf8f0 | ||
|
|
dac8d43b21 | ||
| 9444b06b52 | |||
| c2fdb3e0c1 | |||
| 0e68279037 | |||
| 2bb7fc5786 | |||
|
|
4157732f4b | ||
| e8ede7010a | |||
|
|
0c1150ac4d | ||
| a2fbb0e30d | |||
| 72a4dd3382 | |||
|
|
d3d9f9a8b0 | ||
| 24464ffe33 | |||
| 34c66d19c4 | |||
| e09904147b | |||
|
|
b02a1d3a55 | ||
|
|
9a7aa1f4fc | ||
| 9788db95f2 | |||
|
|
9981be6986 | ||
| 16f9e98b25 | |||
| 676109860a | |||
|
|
3daba279d2 | ||
|
|
70a8ac6f99 | ||
|
|
68bd909345 | ||
|
|
91c3b4e394 | ||
|
|
41794f8bf5 | ||
|
|
ca78da2e07 |
@@ -60,27 +60,27 @@
|
|||||||
<!-- Mode selector tabs -->
|
<!-- Mode selector tabs -->
|
||||||
<div class="flex gap-1 mb-3">
|
<div class="flex gap-1 mb-3">
|
||||||
<button
|
<button
|
||||||
id="btn-mode-mnemonic"
|
id="tab-mnemonic"
|
||||||
class="border border-border px-2 py-1 cursor-pointer text-xs bg-fg text-bg"
|
class="border border-border px-2 py-1 cursor-pointer text-xs hover:bg-fg hover:text-bg bg-fg text-bg"
|
||||||
>
|
>
|
||||||
Recovery Phrase
|
Recovery Phrase
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
id="btn-mode-key"
|
id="tab-privkey"
|
||||||
class="border border-border px-2 py-1 cursor-pointer text-xs hover:bg-fg hover:text-bg"
|
class="border border-border px-2 py-1 cursor-pointer text-xs hover:bg-fg hover:text-bg"
|
||||||
>
|
>
|
||||||
Private Key
|
Private Key
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
id="btn-mode-xprv"
|
id="tab-xprv"
|
||||||
class="border border-border px-2 py-1 cursor-pointer text-xs hover:bg-fg hover:text-bg"
|
class="border border-border px-2 py-1 cursor-pointer text-xs hover:bg-fg hover:text-bg"
|
||||||
>
|
>
|
||||||
xprv
|
Extended Key (xprv)
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ---- Mnemonic mode ---- -->
|
<!-- Mnemonic form section -->
|
||||||
<div id="add-wallet-mode-mnemonic">
|
<div id="add-wallet-section-mnemonic">
|
||||||
<p class="mb-2">
|
<p class="mb-2">
|
||||||
Enter your 12 or 24 word recovery phrase below, or click
|
Enter your 12 or 24 word recovery phrase below, or click
|
||||||
the button to roll the die for a new one.
|
the button to roll the die for a new one.
|
||||||
@@ -112,8 +112,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ---- Private key mode ---- -->
|
<!-- Private key form section -->
|
||||||
<div id="add-wallet-mode-key" class="hidden">
|
<div id="add-wallet-section-privkey" class="hidden">
|
||||||
<p class="mb-2">
|
<p class="mb-2">
|
||||||
Paste your private key below. This wallet will have a
|
Paste your private key below. This wallet will have a
|
||||||
single address.
|
single address.
|
||||||
@@ -128,8 +128,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ---- xprv mode ---- -->
|
<!-- Extended key (xprv) form section -->
|
||||||
<div id="add-wallet-mode-xprv" class="hidden">
|
<div id="add-wallet-section-xprv" class="hidden">
|
||||||
<p class="mb-2">
|
<p class="mb-2">
|
||||||
Paste your extended private key (xprv) below. This will
|
Paste your extended private key (xprv) below. This will
|
||||||
import the HD wallet and scan for used addresses.
|
import the HD wallet and scan for used addresses.
|
||||||
@@ -148,11 +148,11 @@
|
|||||||
<div class="mb-2" id="add-wallet-password-section">
|
<div class="mb-2" id="add-wallet-password-section">
|
||||||
<label class="block mb-1">Choose a password</label>
|
<label class="block mb-1">Choose a password</label>
|
||||||
<p
|
<p
|
||||||
id="add-wallet-password-hint"
|
|
||||||
class="text-xs text-muted mb-1"
|
class="text-xs text-muted mb-1"
|
||||||
|
id="add-wallet-password-hint"
|
||||||
>
|
>
|
||||||
This password encrypts your secret on this device. You
|
This password encrypts your recovery phrase on this
|
||||||
will need it to send funds.
|
device. You will need it to send funds.
|
||||||
</p>
|
</p>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
@@ -172,7 +172,7 @@
|
|||||||
id="btn-add-wallet-confirm"
|
id="btn-add-wallet-confirm"
|
||||||
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
|
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
|
||||||
>
|
>
|
||||||
Add
|
Import
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,36 +1,36 @@
|
|||||||
const { $, showView, showFlash, goBack } = require("./helpers");
|
const { $, showView, showFlash } = require("./helpers");
|
||||||
const {
|
const {
|
||||||
generateMnemonic,
|
generateMnemonic,
|
||||||
hdWalletFromMnemonic,
|
hdWalletFromMnemonic,
|
||||||
hdWalletFromXprv,
|
|
||||||
isValidMnemonic,
|
isValidMnemonic,
|
||||||
isValidXprv,
|
|
||||||
addressFromPrivateKey,
|
addressFromPrivateKey,
|
||||||
|
hdWalletFromXprv,
|
||||||
|
isValidXprv,
|
||||||
} = require("../../shared/wallet");
|
} = require("../../shared/wallet");
|
||||||
const { encryptWithPassword } = require("../../shared/vault");
|
const { encryptWithPassword } = require("../../shared/vault");
|
||||||
const { state, saveState } = require("../../shared/state");
|
const { state, saveState } = require("../../shared/state");
|
||||||
const { scanForAddresses } = require("../../shared/balances");
|
const { scanForAddresses } = require("../../shared/balances");
|
||||||
|
|
||||||
let currentMode = "mnemonic"; // "mnemonic" | "key" | "xprv"
|
let currentMode = "mnemonic";
|
||||||
|
|
||||||
const MODES = ["mnemonic", "key", "xprv"];
|
const MODES = ["mnemonic", "privkey", "xprv"];
|
||||||
|
|
||||||
function setMode(mode) {
|
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;
|
currentMode = mode;
|
||||||
for (const m of MODES) {
|
for (const m of MODES) {
|
||||||
const section = $("add-wallet-mode-" + m);
|
$("add-wallet-section-" + m).classList.toggle("hidden", m !== mode);
|
||||||
if (section) section.classList.toggle("hidden", m !== mode);
|
$("tab-" + m).classList.toggle("bg-fg", m === mode);
|
||||||
const btn = $("btn-mode-" + m);
|
$("tab-" + m).classList.toggle("text-bg", m === mode);
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
$("add-wallet-password-hint").textContent = PASSWORD_HINTS[mode];
|
||||||
}
|
}
|
||||||
|
|
||||||
function show() {
|
function show() {
|
||||||
@@ -40,58 +40,29 @@ function show() {
|
|||||||
$("add-wallet-password").value = "";
|
$("add-wallet-password").value = "";
|
||||||
$("add-wallet-password-confirm").value = "";
|
$("add-wallet-password-confirm").value = "";
|
||||||
$("add-wallet-phrase-warning").classList.add("hidden");
|
$("add-wallet-phrase-warning").classList.add("hidden");
|
||||||
setMode("mnemonic");
|
switchMode("mnemonic");
|
||||||
showView("add-wallet");
|
showView("add-wallet");
|
||||||
}
|
}
|
||||||
|
|
||||||
function init(ctx) {
|
function validatePassword() {
|
||||||
// Mode switching
|
const pw = $("add-wallet-password").value;
|
||||||
$("btn-mode-mnemonic").addEventListener("click", () => setMode("mnemonic"));
|
const pw2 = $("add-wallet-password-confirm").value;
|
||||||
$("btn-mode-key").addEventListener("click", () => setMode("key"));
|
if (!pw) {
|
||||||
$("btn-mode-xprv").addEventListener("click", () => setMode("xprv"));
|
showFlash("Please choose a password.");
|
||||||
|
return null;
|
||||||
$("btn-generate-phrase").addEventListener("click", () => {
|
}
|
||||||
$("wallet-mnemonic").value = generateMnemonic();
|
if (pw.length < 12) {
|
||||||
$("add-wallet-phrase-warning").classList.remove("hidden");
|
showFlash("Password must be at least 12 characters.");
|
||||||
});
|
return null;
|
||||||
|
}
|
||||||
$("btn-add-wallet-confirm").addEventListener("click", async () => {
|
if (pw !== pw2) {
|
||||||
// Shared password validation
|
showFlash("Passwords do not match.");
|
||||||
const pw = $("add-wallet-password").value;
|
return null;
|
||||||
const pw2 = $("add-wallet-password-confirm").value;
|
}
|
||||||
if (!pw) {
|
return 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentMode === "mnemonic") {
|
|
||||||
await handleMnemonic(ctx, pw);
|
|
||||||
} else if (currentMode === "key") {
|
|
||||||
await handlePrivateKey(ctx, pw);
|
|
||||||
} else if (currentMode === "xprv") {
|
|
||||||
await handleXprv(ctx, pw);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$("btn-add-wallet-back").addEventListener("click", () => {
|
|
||||||
if (!state.hasWallet) {
|
|
||||||
goBack("welcome");
|
|
||||||
} else {
|
|
||||||
ctx.renderWalletList();
|
|
||||||
goBack("main");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleMnemonic(ctx, pw) {
|
async function importMnemonic(ctx) {
|
||||||
const mnemonic = $("wallet-mnemonic").value.trim();
|
const mnemonic = $("wallet-mnemonic").value.trim();
|
||||||
if (!mnemonic) {
|
if (!mnemonic) {
|
||||||
showFlash("Enter a recovery phrase or press the die to generate one.");
|
showFlash("Enter a recovery phrase or press the die to generate one.");
|
||||||
@@ -110,6 +81,8 @@ async function handleMnemonic(ctx, pw) {
|
|||||||
showFlash("Invalid recovery phrase. Check for typos.");
|
showFlash("Invalid recovery phrase. Check for typos.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const pw = validatePassword();
|
||||||
|
if (!pw) return;
|
||||||
const { xpub, firstAddress } = hdWalletFromMnemonic(mnemonic);
|
const { xpub, firstAddress } = hdWalletFromMnemonic(mnemonic);
|
||||||
const duplicate = state.wallets.find(
|
const duplicate = state.wallets.find(
|
||||||
(w) =>
|
(w) =>
|
||||||
@@ -141,6 +114,7 @@ async function handleMnemonic(ctx, pw) {
|
|||||||
ctx.renderWalletList();
|
ctx.renderWalletList();
|
||||||
showView("main");
|
showView("main");
|
||||||
|
|
||||||
|
// Scan for used HD addresses beyond index 0.
|
||||||
showFlash("Scanning for addresses...", 30000);
|
showFlash("Scanning for addresses...", 30000);
|
||||||
const scan = await scanForAddresses(xpub, state.rpcUrl);
|
const scan = await scanForAddresses(xpub, state.rpcUrl);
|
||||||
if (scan.addresses.length > 1) {
|
if (scan.addresses.length > 1) {
|
||||||
@@ -160,7 +134,7 @@ async function handleMnemonic(ctx, pw) {
|
|||||||
ctx.doRefreshAndRender();
|
ctx.doRefreshAndRender();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handlePrivateKey(ctx, pw) {
|
async function importPrivateKey(ctx) {
|
||||||
const key = $("import-private-key").value.trim();
|
const key = $("import-private-key").value.trim();
|
||||||
if (!key) {
|
if (!key) {
|
||||||
showFlash("Please enter your private key.");
|
showFlash("Please enter your private key.");
|
||||||
@@ -173,6 +147,20 @@ async function handlePrivateKey(ctx, pw) {
|
|||||||
showFlash("Invalid private key.");
|
showFlash("Invalid private key.");
|
||||||
return;
|
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 encrypted = await encryptWithPassword(key, pw);
|
||||||
const walletNum = state.wallets.length + 1;
|
const walletNum = state.wallets.length + 1;
|
||||||
state.wallets.push({
|
state.wallets.push({
|
||||||
@@ -189,7 +177,7 @@ async function handlePrivateKey(ctx, pw) {
|
|||||||
ctx.doRefreshAndRender();
|
ctx.doRefreshAndRender();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleXprv(ctx, pw) {
|
async function importXprvKey(ctx) {
|
||||||
const xprv = $("import-xprv-key").value.trim();
|
const xprv = $("import-xprv-key").value.trim();
|
||||||
if (!xprv) {
|
if (!xprv) {
|
||||||
showFlash("Please enter your extended private key.");
|
showFlash("Please enter your extended private key.");
|
||||||
@@ -217,6 +205,8 @@ async function handleXprv(ctx, pw) {
|
|||||||
showFlash("This key is already added (" + duplicate.name + ").");
|
showFlash("This key is already added (" + duplicate.name + ").");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const pw = validatePassword();
|
||||||
|
if (!pw) return;
|
||||||
const encrypted = await encryptWithPassword(xprv, pw);
|
const encrypted = await encryptWithPassword(xprv, pw);
|
||||||
const walletNum = state.wallets.length + 1;
|
const walletNum = state.wallets.length + 1;
|
||||||
const wallet = {
|
const wallet = {
|
||||||
@@ -235,6 +225,7 @@ async function handleXprv(ctx, pw) {
|
|||||||
ctx.renderWalletList();
|
ctx.renderWalletList();
|
||||||
showView("main");
|
showView("main");
|
||||||
|
|
||||||
|
// Scan for used HD addresses beyond index 0.
|
||||||
showFlash("Scanning for addresses...", 30000);
|
showFlash("Scanning for addresses...", 30000);
|
||||||
const scan = await scanForAddresses(xpub, state.rpcUrl);
|
const scan = await scanForAddresses(xpub, state.rpcUrl);
|
||||||
if (scan.addresses.length > 1) {
|
if (scan.addresses.length > 1) {
|
||||||
@@ -254,4 +245,38 @@ async function handleXprv(ctx, pw) {
|
|||||||
ctx.doRefreshAndRender();
|
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 () => {
|
||||||
|
if (currentMode === "mnemonic") {
|
||||||
|
await importMnemonic(ctx);
|
||||||
|
} else if (currentMode === "privkey") {
|
||||||
|
await importPrivateKey(ctx);
|
||||||
|
} else if (currentMode === "xprv") {
|
||||||
|
await importXprvKey(ctx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Back button
|
||||||
|
$("btn-add-wallet-back").addEventListener("click", () => {
|
||||||
|
if (!state.hasWallet) {
|
||||||
|
showView("welcome");
|
||||||
|
} else {
|
||||||
|
ctx.renderWalletList();
|
||||||
|
showView("main");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = { init, show };
|
module.exports = { init, show };
|
||||||
|
|||||||
@@ -33,9 +33,6 @@ const VIEWS = [
|
|||||||
"export-privkey",
|
"export-privkey",
|
||||||
];
|
];
|
||||||
|
|
||||||
// Simple view history stack for back-button navigation.
|
|
||||||
const viewStack = [];
|
|
||||||
|
|
||||||
function $(id) {
|
function $(id) {
|
||||||
return document.getElementById(id);
|
return document.getElementById(id);
|
||||||
}
|
}
|
||||||
@@ -50,13 +47,7 @@ function hideError(id) {
|
|||||||
$(id).classList.add("hidden");
|
$(id).classList.add("hidden");
|
||||||
}
|
}
|
||||||
|
|
||||||
function showView(name, opts) {
|
function showView(name) {
|
||||||
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) {
|
for (const v of VIEWS) {
|
||||||
const el = document.getElementById(`view-${v}`);
|
const el = document.getElementById(`view-${v}`);
|
||||||
if (el) {
|
if (el) {
|
||||||
@@ -74,17 +65,6 @@ function showView(name, opts) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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;
|
let flashTimer = null;
|
||||||
|
|
||||||
function clearFlash() {
|
function clearFlash() {
|
||||||
@@ -283,7 +263,6 @@ module.exports = {
|
|||||||
showError,
|
showError,
|
||||||
hideError,
|
hideError,
|
||||||
showView,
|
showView,
|
||||||
goBack,
|
|
||||||
showFlash,
|
showFlash,
|
||||||
balanceLine,
|
balanceLine,
|
||||||
balanceLinesForAddress,
|
balanceLinesForAddress,
|
||||||
|
|||||||
@@ -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 };
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
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 };
|
|
||||||
@@ -153,9 +153,11 @@ async function fetchRecentTransactions(address, blockscoutUrl, count = 25) {
|
|||||||
|
|
||||||
// When a token transfer shares a hash with a normal tx, the normal tx
|
// When a token transfer shares a hash with a normal tx, the normal tx
|
||||||
// is the contract call (0 ETH) and the token transfer has the real
|
// is the contract call (0 ETH) and the token transfer has the real
|
||||||
// amount and symbol. Replace the normal tx with the token transfer,
|
// amount and symbol. A single transaction (e.g. a swap) can produce
|
||||||
// but preserve contract call metadata (direction, label, method) so
|
// multiple token transfers (one per token involved), so we key token
|
||||||
// swaps and other contract interactions display correctly.
|
// transfers by hash + contract address to keep all of them. We also
|
||||||
|
// preserve contract-call metadata (direction, label, method) from the
|
||||||
|
// matching normal tx so swaps display correctly.
|
||||||
for (const tt of ttJson.items || []) {
|
for (const tt of ttJson.items || []) {
|
||||||
const parsed = parseTokenTransfer(tt, addrLower);
|
const parsed = parseTokenTransfer(tt, addrLower);
|
||||||
const existing = txsByHash.get(parsed.hash);
|
const existing = txsByHash.get(parsed.hash);
|
||||||
@@ -164,8 +166,13 @@ async function fetchRecentTransactions(address, blockscoutUrl, count = 25) {
|
|||||||
parsed.directionLabel = existing.directionLabel;
|
parsed.directionLabel = existing.directionLabel;
|
||||||
parsed.isContractCall = true;
|
parsed.isContractCall = true;
|
||||||
parsed.method = existing.method;
|
parsed.method = existing.method;
|
||||||
|
// Remove the bare-hash normal tx so it doesn't appear as a
|
||||||
|
// duplicate with empty value; token transfers replace it.
|
||||||
|
txsByHash.delete(parsed.hash);
|
||||||
}
|
}
|
||||||
txsByHash.set(parsed.hash, parsed);
|
// Use composite key so multiple token transfers per tx are kept.
|
||||||
|
const ttKey = parsed.hash + ":" + (parsed.contractAddress || "");
|
||||||
|
txsByHash.set(ttKey, parsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
const txs = [...txsByHash.values()];
|
const txs = [...txsByHash.values()];
|
||||||
|
|||||||
Reference in New Issue
Block a user