feat: add export private key from address detail view
All checks were successful
check / check (push) Successful in 22s

Adds an 'Export Private Key' button to the address detail view.
Clicking it opens a password confirmation screen; after verification,
the derived private key is displayed in a copyable field with a
security warning. The key is cleared when navigating away.

Closes #19
This commit is contained in:
user
2026-02-28 01:19:36 -08:00
parent fb67359b3f
commit ca78da2e07
2 changed files with 113 additions and 0 deletions

View File

@@ -307,6 +307,16 @@
</button>
</div>
<!-- actions row 2 -->
<div class="flex gap-2 mb-3">
<button
id="btn-export-privkey"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer flex-1 text-xs"
>
Export Private Key
</button>
</div>
<!-- transactions -->
<div class="mt-3">
<div class="border-b border-border pb-1 mb-1">
@@ -318,6 +328,49 @@
</div>
</div>
<!-- ============ EXPORT PRIVATE KEY VIEW ============ -->
<div id="view-export-privkey" class="view hidden">
<button
id="btn-export-privkey-back"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer mb-2"
>
&lt; Back
</button>
<h2 class="font-bold mb-3">Export Private Key</h2>
<p class="text-xs mb-1" id="export-privkey-label"></p>
<p class="text-xs mb-3 text-muted">
Warning: anyone with this private key can access and
transfer all funds from this address. Never share it.
</p>
<div
id="export-privkey-flash"
class="text-xs mb-2 hidden"
></div>
<div id="export-privkey-password-section" class="mb-2">
<label class="block mb-1">Password</label>
<input
type="password"
id="export-privkey-password"
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
placeholder="Enter your password to continue"
/>
<button
id="btn-export-privkey-confirm"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer mt-2"
>
Reveal
</button>
</div>
<div id="export-privkey-result" class="hidden">
<div
id="export-privkey-value"
class="border border-dashed border-border p-2 font-mono text-xs break-all cursor-pointer mb-1"
title="Click to copy"
></div>
<p class="text-xs text-muted">Click to copy.</p>
</div>
</div>
<!-- ============ ADDRESS-TOKEN DETAIL VIEW ============ -->
<div id="view-address-token" class="view hidden">
<button

View File

@@ -18,6 +18,8 @@ const { resolveEnsNames } = require("../../shared/ens");
const { updateSendBalance, renderSendTokenSelect } = require("./send");
const { log } = require("../../shared/log");
const makeBlockie = require("ethereum-blockies-base64");
const { decryptWithPassword } = require("../../shared/vault");
const { getSignerForAddress } = require("../../shared/wallet");
let ctx;
@@ -261,6 +263,64 @@ function init(_ctx) {
});
$("btn-add-token").addEventListener("click", ctx.showAddTokenView);
$("btn-export-privkey").addEventListener("click", () => {
const wallet = state.wallets[state.selectedWallet];
const addr = wallet.addresses[state.selectedAddress];
const title = addressTitle(addr.address, state.wallets);
const label = title || "Address " + (state.selectedAddress + 1);
$("export-privkey-label").textContent = "Exporting key for: " + label;
$("export-privkey-password").value = "";
$("export-privkey-flash").classList.add("hidden");
$("export-privkey-flash").textContent = "";
$("export-privkey-password-section").classList.remove("hidden");
$("export-privkey-result").classList.add("hidden");
$("export-privkey-value").textContent = "";
showView("export-privkey");
});
$("btn-export-privkey-confirm").addEventListener("click", async () => {
const password = $("export-privkey-password").value;
if (!password) {
$("export-privkey-flash").textContent = "Password is required.";
$("export-privkey-flash").classList.remove("hidden");
return;
}
const wallet = state.wallets[state.selectedWallet];
try {
const secret = await decryptWithPassword(
wallet.encryptedSecret,
password,
);
const signer = getSignerForAddress(
wallet,
state.selectedAddress,
secret,
);
const privateKey = signer.privateKey;
$("export-privkey-password-section").classList.add("hidden");
$("export-privkey-value").textContent = privateKey;
$("export-privkey-result").classList.remove("hidden");
$("export-privkey-flash").classList.add("hidden");
} catch {
$("export-privkey-flash").textContent = "Wrong password.";
$("export-privkey-flash").classList.remove("hidden");
}
});
$("export-privkey-value").addEventListener("click", () => {
const key = $("export-privkey-value").textContent;
if (key) {
navigator.clipboard.writeText(key);
showFlash("Copied!");
}
});
$("btn-export-privkey-back").addEventListener("click", () => {
$("export-privkey-value").textContent = "";
$("export-privkey-password").value = "";
show();
});
}
module.exports = { init, show };