Compare commits

..

1 Commits

Author SHA1 Message Date
user
cd412209a7 fix: cross-wallet duplicate address detection on import
All checks were successful
check / check (push) Successful in 22s
Previously each import method (mnemonic, private key, xprv) only checked
for duplicates among wallets of the same type. Now all three check the
derived address against ALL existing wallet addresses regardless of type,
and mnemonic/xprv imports also compare xpubs against existing HD/xprv
wallets.

Closes #111
2026-02-28 16:01:40 -08:00
11 changed files with 38 additions and 106 deletions

View File

@@ -19,16 +19,3 @@ body {
width: 396px; width: 396px;
overflow-x: hidden; overflow-x: hidden;
} }
/* Copy-flash feedback: inverts colors then fades back */
.copy-flash-active {
background-color: var(--color-fg) !important;
color: var(--color-bg) !important;
transition: none;
}
.copy-flash-fade {
transition:
background-color 300ms ease-out,
color 300ms ease-out;
}

View File

@@ -11,25 +11,6 @@ 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");
/**
* Check if an address already exists in ANY wallet (hd, xprv, or key).
* Returns the wallet object if found, or undefined.
*/
function findWalletByAddress(addr) {
const lower = addr.toLowerCase();
return state.wallets.find((w) =>
w.addresses.some((a) => a.address.toLowerCase() === lower),
);
}
/**
* Check if an xpub already exists in any HD-type wallet (hd or xprv).
* Returns the wallet object if found, or undefined.
*/
function findWalletByXpub(xpub) {
return state.wallets.find((w) => w.xpub && w.xpub === xpub);
}
let currentMode = "mnemonic"; let currentMode = "mnemonic";
const MODES = ["mnemonic", "privkey", "xprv"]; const MODES = ["mnemonic", "privkey", "xprv"];
@@ -116,16 +97,24 @@ async function importMnemonic(ctx) {
const pw = validatePassword(); const pw = validatePassword();
if (!pw) return; if (!pw) return;
const { xpub, firstAddress } = hdWalletFromMnemonic(mnemonic); const { xpub, firstAddress } = hdWalletFromMnemonic(mnemonic);
const xpubDup = findWalletByXpub(xpub); const addrDup = state.wallets.find((w) =>
if (xpubDup) { w.addresses.some(
(a) => a.address.toLowerCase() === firstAddress.toLowerCase(),
),
);
if (addrDup) {
showFlash( showFlash(
"This recovery phrase is already added (" + xpubDup.name + ").", "An address from this phrase already exists in " +
addrDup.name +
".",
); );
return; return;
} }
const addrDup = findWalletByAddress(firstAddress); const xpubDup = state.wallets.find(
if (addrDup) { (w) => (w.type === "hd" || w.type === "xprv") && w.xpub === xpub,
showFlash("Address already exists in wallet (" + addrDup.name + ")."); );
if (xpubDup) {
showFlash("This recovery phrase matches " + xpubDup.name + ".");
return; return;
} }
const encrypted = await encryptWithPassword(mnemonic, pw); const encrypted = await encryptWithPassword(mnemonic, pw);
@@ -181,11 +170,11 @@ async function importPrivateKey(ctx) {
} }
const pw = validatePassword(); const pw = validatePassword();
if (!pw) return; if (!pw) return;
const duplicate = findWalletByAddress(addr); const duplicate = state.wallets.find((w) =>
w.addresses.some((a) => a.address.toLowerCase() === addr.toLowerCase()),
);
if (duplicate) { if (duplicate) {
showFlash( showFlash("This address already exists in " + duplicate.name + ".");
"This address already exists in wallet (" + duplicate.name + ").",
);
return; return;
} }
const encrypted = await encryptWithPassword(key, pw); const encrypted = await encryptWithPassword(key, pw);
@@ -222,14 +211,22 @@ async function importXprvKey(ctx) {
return; return;
} }
const { xpub, firstAddress } = result; const { xpub, firstAddress } = result;
const xpubDup = findWalletByXpub(xpub); const addrDup = state.wallets.find((w) =>
if (xpubDup) { w.addresses.some(
showFlash("This key is already added (" + xpubDup.name + ")."); (a) => a.address.toLowerCase() === firstAddress.toLowerCase(),
),
);
if (addrDup) {
showFlash(
"An address from this key already exists in " + addrDup.name + ".",
);
return; return;
} }
const addrDup = findWalletByAddress(firstAddress); const xpubDup = state.wallets.find(
if (addrDup) { (w) => (w.type === "hd" || w.type === "xprv") && w.xpub === xpub,
showFlash("Address already exists in wallet (" + addrDup.name + ")."); );
if (xpubDup) {
showFlash("This key matches " + xpubDup.name + ".");
return; return;
} }
const pw = validatePassword(); const pw = validatePassword();

View File

@@ -2,7 +2,6 @@ const {
$, $,
showView, showView,
showFlash, showFlash,
flashCopyFeedback,
balanceLinesForAddress, balanceLinesForAddress,
addressDotHtml, addressDotHtml,
addressTitle, addressTitle,
@@ -95,10 +94,6 @@ function show() {
function isoDate(timestamp) { function isoDate(timestamp) {
const d = new Date(timestamp * 1000); const d = new Date(timestamp * 1000);
const pad = (n) => String(n).padStart(2, "0"); const pad = (n) => String(n).padStart(2, "0");
const off = -d.getTimezoneOffset();
const sign = off >= 0 ? "+" : "-";
const absOff = Math.abs(off);
const tz = sign + pad(Math.floor(absOff / 60)) + ":" + pad(absOff % 60);
return ( return (
d.getFullYear() + d.getFullYear() +
"-" + "-" +
@@ -110,8 +105,7 @@ function isoDate(timestamp) {
":" + ":" +
pad(d.getMinutes()) + pad(d.getMinutes()) +
":" + ":" +
pad(d.getSeconds()) + pad(d.getSeconds())
tz
); );
} }
@@ -247,7 +241,6 @@ function init(_ctx) {
if (addr) { if (addr) {
navigator.clipboard.writeText(addr); navigator.clipboard.writeText(addr);
showFlash("Copied!"); showFlash("Copied!");
flashCopyFeedback($("address-full"));
} }
}); });
@@ -365,7 +358,6 @@ function init(_ctx) {
if (key) { if (key) {
navigator.clipboard.writeText(key); navigator.clipboard.writeText(key);
showFlash("Copied!"); showFlash("Copied!");
flashCopyFeedback($("export-privkey-value"));
} }
}); });
@@ -374,7 +366,6 @@ function init(_ctx) {
if (full) { if (full) {
navigator.clipboard.writeText(full); navigator.clipboard.writeText(full);
showFlash("Copied!"); showFlash("Copied!");
flashCopyFeedback($("export-privkey-address"));
} }
}); });

View File

@@ -5,7 +5,6 @@ const {
$, $,
showView, showView,
showFlash, showFlash,
flashCopyFeedback,
addressDotHtml, addressDotHtml,
addressTitle, addressTitle,
escapeHtml, escapeHtml,
@@ -48,10 +47,6 @@ function etherscanAddressLink(address) {
function isoDate(timestamp) { function isoDate(timestamp) {
const d = new Date(timestamp * 1000); const d = new Date(timestamp * 1000);
const pad = (n) => String(n).padStart(2, "0"); const pad = (n) => String(n).padStart(2, "0");
const off = -d.getTimezoneOffset();
const sign = off >= 0 ? "+" : "-";
const absOff = Math.abs(off);
const tz = sign + pad(Math.floor(absOff / 60)) + ":" + pad(absOff % 60);
return ( return (
d.getFullYear() + d.getFullYear() +
"-" + "-" +
@@ -63,8 +58,7 @@ function isoDate(timestamp) {
":" + ":" +
pad(d.getMinutes()) + pad(d.getMinutes()) +
":" + ":" +
pad(d.getSeconds()) + pad(d.getSeconds())
tz
); );
} }
@@ -323,7 +317,6 @@ function init(_ctx) {
if (addr) { if (addr) {
navigator.clipboard.writeText(addr); navigator.clipboard.writeText(addr);
showFlash("Copied!"); showFlash("Copied!");
flashCopyFeedback($("address-token-full"));
} }
}); });
@@ -332,7 +325,6 @@ function init(_ctx) {
if (copyEl) { if (copyEl) {
navigator.clipboard.writeText(copyEl.dataset.copy); navigator.clipboard.writeText(copyEl.dataset.copy);
showFlash("Copied!"); showFlash("Copied!");
flashCopyFeedback(copyEl);
} }
}); });
@@ -381,7 +373,6 @@ function init(_ctx) {
copyEl.addEventListener("click", () => { copyEl.addEventListener("click", () => {
navigator.clipboard.writeText(copyEl.dataset.copy); navigator.clipboard.writeText(copyEl.dataset.copy);
showFlash("Copied!"); showFlash("Copied!");
flashCopyFeedback(copyEl);
}); });
} }
updateSendBalance(); updateSendBalance();

View File

@@ -15,7 +15,6 @@ const {
hideError, hideError,
showView, showView,
showFlash, showFlash,
flashCopyFeedback,
addressTitle, addressTitle,
addressDotHtml, addressDotHtml,
escapeHtml, escapeHtml,
@@ -118,7 +117,6 @@ function show(txInfo) {
copyEl.onclick = () => { copyEl.onclick = () => {
navigator.clipboard.writeText(copyEl.dataset.copy); navigator.clipboard.writeText(copyEl.dataset.copy);
showFlash("Copied!"); showFlash("Copied!");
flashCopyFeedback(copyEl);
}; };
} }
} else { } else {

View File

@@ -226,10 +226,6 @@ function formatAddressHtml(address, ensName, maxLen, title) {
function isoDate(timestamp) { function isoDate(timestamp) {
const d = new Date(timestamp * 1000); const d = new Date(timestamp * 1000);
const pad = (n) => String(n).padStart(2, "0"); const pad = (n) => String(n).padStart(2, "0");
const off = -d.getTimezoneOffset();
const sign = off >= 0 ? "+" : "-";
const absOff = Math.abs(off);
const tz = sign + pad(Math.floor(absOff / 60)) + ":" + pad(absOff % 60);
return ( return (
d.getFullYear() + d.getFullYear() +
"-" + "-" +
@@ -241,8 +237,7 @@ function isoDate(timestamp) {
":" + ":" +
pad(d.getMinutes()) + pad(d.getMinutes()) +
":" + ":" +
pad(d.getSeconds()) + pad(d.getSeconds())
tz
); );
} }
@@ -263,26 +258,12 @@ function timeAgo(timestamp) {
return years + " year" + (years !== 1 ? "s" : "") + " ago"; return years + " year" + (years !== 1 ? "s" : "") + " ago";
} }
function flashCopyFeedback(el) {
if (!el) return;
el.classList.remove("copy-flash-fade");
el.classList.add("copy-flash-active");
setTimeout(() => {
el.classList.remove("copy-flash-active");
el.classList.add("copy-flash-fade");
setTimeout(() => {
el.classList.remove("copy-flash-fade");
}, 350);
}, 100);
}
module.exports = { module.exports = {
$, $,
showError, showError,
hideError, hideError,
showView, showView,
showFlash, showFlash,
flashCopyFeedback,
balanceLine, balanceLine,
balanceLinesForAddress, balanceLinesForAddress,
addressColor, addressColor,

View File

@@ -2,7 +2,6 @@ const {
$, $,
showView, showView,
showFlash, showFlash,
flashCopyFeedback,
balanceLinesForAddress, balanceLinesForAddress,
isoDate, isoDate,
timeAgo, timeAgo,
@@ -86,10 +85,9 @@ function renderActiveAddress() {
el.innerHTML = el.innerHTML =
`<span class="underline decoration-dashed cursor-pointer" id="active-addr-copy">${dot}${escapeHtml(addr)}</span>` + `<span class="underline decoration-dashed cursor-pointer" id="active-addr-copy">${dot}${escapeHtml(addr)}</span>` +
`<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`; `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
$("active-addr-copy").addEventListener("click", (e) => { $("active-addr-copy").addEventListener("click", () => {
navigator.clipboard.writeText(addr); navigator.clipboard.writeText(addr);
showFlash("Copied!"); showFlash("Copied!");
flashCopyFeedback(e.currentTarget);
}); });
} else { } else {
el.textContent = ""; el.textContent = "";

View File

@@ -2,7 +2,6 @@ const {
$, $,
showView, showView,
showFlash, showFlash,
flashCopyFeedback,
formatAddressHtml, formatAddressHtml,
addressTitle, addressTitle,
} = require("./helpers"); } = require("./helpers");
@@ -61,12 +60,11 @@ function show() {
} }
function init(ctx) { function init(ctx) {
$("receive-address-block").addEventListener("click", (e) => { $("receive-address-block").addEventListener("click", () => {
const addr = $("receive-address-block").dataset.full; const addr = $("receive-address-block").dataset.full;
if (addr) { if (addr) {
navigator.clipboard.writeText(addr); navigator.clipboard.writeText(addr);
showFlash("Copied!"); showFlash("Copied!");
flashCopyFeedback(e.currentTarget);
} }
}); });
@@ -75,7 +73,6 @@ function init(ctx) {
if (addr) { if (addr) {
navigator.clipboard.writeText(addr); navigator.clipboard.writeText(addr);
showFlash("Copied!"); showFlash("Copied!");
flashCopyFeedback($("receive-address-block"));
} }
}); });

View File

@@ -5,7 +5,6 @@ const {
$, $,
showView, showView,
showFlash, showFlash,
flashCopyFeedback,
addressDotHtml, addressDotHtml,
addressTitle, addressTitle,
escapeHtml, escapeHtml,
@@ -172,7 +171,6 @@ function render() {
el.onclick = () => { el.onclick = () => {
navigator.clipboard.writeText(el.dataset.copy); navigator.clipboard.writeText(el.dataset.copy);
showFlash("Copied!"); showFlash("Copied!");
flashCopyFeedback(el);
}; };
}); });
} }
@@ -250,7 +248,6 @@ async function loadCalldata(txHash, toAddress) {
el.onclick = () => { el.onclick = () => {
navigator.clipboard.writeText(el.dataset.copy); navigator.clipboard.writeText(el.dataset.copy);
showFlash("Copied!"); showFlash("Copied!");
flashCopyFeedback(el);
}; };
}); });
} }

View File

@@ -4,7 +4,6 @@ const {
$, $,
showView, showView,
showFlash, showFlash,
flashCopyFeedback,
addressDotHtml, addressDotHtml,
addressTitle, addressTitle,
escapeHtml, escapeHtml,
@@ -78,7 +77,6 @@ function attachCopyHandlers(viewId) {
el.onclick = () => { el.onclick = () => {
navigator.clipboard.writeText(el.dataset.copy); navigator.clipboard.writeText(el.dataset.copy);
showFlash("Copied!"); showFlash("Copied!");
flashCopyFeedback(el);
}; };
}); });
} }

View File

@@ -487,10 +487,7 @@ function decode(data, toAddress) {
const deadlineDate = new Date(Number(deadline) * 1000); const deadlineDate = new Date(Number(deadline) * 1000);
details.push({ details.push({
label: "Deadline", label: "Deadline",
value: deadlineDate value: deadlineDate.toISOString().replace("T", " ").slice(0, 19),
.toISOString()
.replace("T", " ")
.replace(".000Z", "Z"),
}); });
return { return {