Compare commits

...

23 Commits

Author SHA1 Message Date
user
b799686cd4 fix: zero-tx warning layout shift and contract address false positive
All checks were successful
check / check (push) Successful in 22s
- Reserve space for the warning upfront using visibility:hidden instead
  of display:none, preventing layout shift per README policy
- Move warning HTML to index.html as a static element rather than
  injecting dynamically
- Skip warning for contract addresses (check getCode first) since
  getTransactionCount only returns outgoing tx nonce
- Collapse reserved space when warning is not needed (address has
  history, is a contract, or on RPC error)
2026-02-28 14:18:28 -08:00
user
9e177f04a4 feat: show red warning when sending to address with zero tx history
On the confirm-tx view, asynchronously check the recipient address
transaction count via getTransactionCount(). If zero, display a
prominent red warning advising the user to double-check the address.

Closes #82
2026-02-28 14:18:28 -08:00
dc8ec7d28f Merge pull request 'fix: make success-tx addresses clickable, fix USDT ETH bug, nest decoded details (closes #80)' (#94) from fix/issue-80-success-tx-display into main
All checks were successful
check / check (push) Successful in 10s
Reviewed-on: #94
2026-02-28 22:57:37 +01:00
user
2fbed343db fix: make success-tx addresses clickable, fix USDT ETH bug, nest decoded details (closes #80)
All checks were successful
check / check (push) Successful in 22s
- Add underline + click-to-copy (data-copy) to addresses in toAddressHtml()
  so they match the style used everywhere else in the extension
- Fix 'USDT ETH' display: add rawValue to Uniswap decoder Amount details
  and extract Token In info for proper symbol resolution in approval.js
- Hide redundant top-level Amount/To when decoded details are present
  (they already show the same info inside the decoded section)
- Wrap decoded calldata details in a bordered well for visual separation
2026-02-28 13:36:19 -08:00
699e080e3e Merge pull request 'fix: replace confirm-tx password modal with inline field (closes #78)' (#83) from fix/issue-78-inline-password into main
All checks were successful
check / check (push) Successful in 8s
Reviewed-on: #83
2026-02-28 22:28:18 +01:00
user
8f2bf9618e fix: replace confirm-tx password modal with inline field (closes #78)
All checks were successful
check / check (push) Successful in 22s
Replace the modal overlay password dialog in the confirm-tx view with
an inline password field, matching the pattern used by approve-tx and
approve-sign views for consistency.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 22:27:49 +01:00
069981baa0 Merge pull request 'fix: disable export-privkey and delete-wallet buttons during async processing' (#89) from fix/issue-86-disable-buttons-during-async into main
All checks were successful
check / check (push) Successful in 22s
Reviewed-on: #89
2026-02-28 22:27:27 +01:00
clawbot
886cd38a9b fix: disable export-privkey and delete-wallet buttons during async processing
All checks were successful
check / check (push) Successful in 9s
Closes #86
2026-02-28 22:27:09 +01:00
438d915f73 Merge pull request 'persist confirm-tx view across popup close/reopen (closes #77)' (#79) from fix/issue-77-confirm-tx-persist into main
All checks were successful
check / check (push) Successful in 8s
Reviewed-on: #79
2026-02-28 22:26:39 +01:00
user
78f961f416 persist confirm-tx view across popup close/reopen (closes #77)
All checks were successful
check / check (push) Successful in 23s
Add confirm-tx to RESTORABLE_VIEWS and save pendingTx in
state.viewData so the confirmation screen survives the popup
lifecycle. On restore, re-render the full confirmation view
including gas estimate.
2026-02-28 22:26:07 +01:00
6a214f1c58 Merge pull request 'fix: approve-tx/approve-sign error divs consistency with confirm-tx' (#92) from fix/84-approve-error-div-consistency into main
All checks were successful
check / check (push) Successful in 22s
Reviewed-on: #92
2026-02-28 22:25:37 +01:00
ad2ce3d8ff Merge branch 'main' into fix/84-approve-error-div-consistency
All checks were successful
check / check (push) Successful in 8s
2026-02-28 22:25:21 +01:00
b826279d8f Merge pull request 'fix: clear password field and error in showTxApproval' (#91) from fix/issue-85-clear-approve-tx-password into main
All checks were successful
check / check (push) Successful in 9s
Reviewed-on: #91
2026-02-28 22:24:55 +01:00
user
20ced62e1a fix: approve-tx/approve-sign error divs consistency with confirm-tx
All checks were successful
check / check (push) Successful in 22s
Add min-h-[1.25rem] and border styling to approve-tx-error and
approve-sign-error divs to prevent layout shift, matching the pattern
used by modal-password-error in confirm-tx view.

Replace direct DOM classList manipulation with showError()/hideError()
helpers from helpers.js for consistency.

Closes #84
2026-02-28 13:13:23 -08:00
user
9b69a60cca fix: clear password field and error in showTxApproval
All checks were successful
check / check (push) Successful in 22s
Clears #approve-tx-password value and hides #approve-tx-error when the
transaction approval view is shown, matching the pattern used in
showSignApproval and confirmTx.show.

Closes #85
2026-02-28 13:10:17 -08:00
3b6b18d168 Merge pull request 'fix: validate destination address on send view (closes #67)' (#68) from fix/67-validate-send-address into main
All checks were successful
check / check (push) Successful in 8s
Reviewed-on: #68
2026-02-28 21:38:22 +01:00
33ae5784e2 Merge branch 'main' into fix/67-validate-send-address
All checks were successful
check / check (push) Successful in 22s
2026-02-28 21:37:38 +01:00
cd30d94040 Merge pull request 'fix: make token contract display on confirm-tx consistent with other views' (#73) from fix/confirm-tx-contract-display into main
All checks were successful
check / check (push) Successful in 8s
Reviewed-on: #73
2026-02-28 21:33:53 +01:00
62bb54556c Merge branch 'main' into fix/confirm-tx-contract-display
All checks were successful
check / check (push) Successful in 22s
2026-02-28 21:33:24 +01:00
8e1856415a Merge branch 'main' into fix/67-validate-send-address
All checks were successful
check / check (push) Successful in 23s
2026-02-28 21:25:08 +01:00
user
9de7791553 fix: reset validation state when navigating to send view
All checks were successful
check / check (push) Successful in 22s
Clear the error/warning text and disable the review button when entering
the send view from home, address detail, or address token views. This
prevents stale validation messages from persisting after leaving and
returning to the send view.
2026-02-28 12:17:52 -08:00
user
ef2f862d23 fix: validate destination address on send view
- Validate Ethereum addresses (0x + 40 hex chars) and ENS names
- EIP-55 checksum validation for mixed-case addresses
- Block sending to zero address (0x0000...0000)
- Warn when sending to own address (allow but show warning)
- Inline error messages with reserved space (no layout shift)
- Disable Review button while address is invalid

Closes #67
2026-02-28 12:17:52 -08:00
a655c546b7 fix: make token contract display on confirm-tx consistent with other views
All checks were successful
check / check (push) Successful in 22s
Add color dot (addressDotHtml), dashed underline styling, and click-to-copy
functionality to the token contract address on the confirm-tx page, matching
the display pattern used in addressToken, txStatus, and other views.

Closes #70
2026-02-28 12:11:55 -08:00
11 changed files with 348 additions and 96 deletions

View File

@@ -496,6 +496,11 @@
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
placeholder="Address (0x...) or ENS name"
/>
<div
id="send-to-error"
class="text-xs"
style="min-height: 1.25rem; color: #cc0000"
></div>
</div>
<div class="mb-2">
<div class="flex justify-between mb-1">
@@ -572,15 +577,40 @@
<div id="confirm-fee-amount" class="text-xs"></div>
</div>
<div id="confirm-warnings" class="mb-2 hidden"></div>
<div
id="confirm-recipient-warning"
class="mb-2"
style="visibility: hidden"
>
<div
class="border border-red-500 border-dashed p-2 text-xs font-bold text-red-500"
>
WARNING: The recipient address has ZERO transaction
history. This may indicate a fresh or unused address.
Double-check the address before sending.
</div>
</div>
<div
id="confirm-errors"
class="mb-2 border border-border border-dashed p-2 hidden"
></div>
<div class="mb-2">
<label class="block mb-1 text-xs">Password</label>
<input
type="password"
id="confirm-tx-password"
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
/>
</div>
<div
id="confirm-tx-password-error"
class="text-xs mb-2 min-h-[1.25rem]"
></div>
<button
id="btn-confirm-send"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
Send
Sign &amp; Send
</button>
</div>
@@ -659,42 +689,6 @@
</button>
</div>
<!-- ============ PASSWORD MODAL ============ -->
<div
id="password-modal"
class="hidden fixed inset-0 bg-bg flex items-center justify-center z-50"
>
<div class="border border-border p-4 bg-bg w-80">
<h2 class="font-bold mb-2">Enter Password</h2>
<p class="text-xs text-muted mb-2">
Your password is needed to authorize this transaction.
</p>
<input
type="password"
id="modal-password"
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg mb-2"
/>
<div
id="modal-password-error"
class="text-xs mb-2 border border-border border-dashed p-1 hidden"
></div>
<div class="flex gap-2">
<button
id="btn-modal-confirm"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
Confirm
</button>
<button
id="btn-modal-cancel"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
Cancel
</button>
</div>
</div>
</div>
<!-- ============ RECEIVE ============ -->
<div id="view-receive" class="view hidden">
<button
@@ -1134,7 +1128,10 @@
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
/>
</div>
<div id="approve-tx-error" class="text-xs hidden mb-2"></div>
<div
id="approve-tx-error"
class="text-xs mb-2 border border-border border-dashed p-1 min-h-[1.25rem] hidden"
></div>
<div class="flex justify-between">
<button
id="btn-approve-tx"
@@ -1197,7 +1194,10 @@
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
/>
</div>
<div id="approve-sign-error" class="text-xs hidden mb-2"></div>
<div
id="approve-sign-error"
class="text-xs mb-2 border border-border border-dashed p-1 min-h-[1.25rem] hidden"
></div>
<div class="flex justify-between">
<button
id="btn-approve-sign"

View File

@@ -74,6 +74,7 @@ const RESTORABLE_VIEWS = new Set([
"receive",
"settings",
"settings-addtoken",
"confirm-tx",
"transaction",
"success-tx",
"error-tx",
@@ -127,6 +128,13 @@ function restoreView() {
case "settings-addtoken":
settingsAddToken.show();
break;
case "confirm-tx":
if (state.viewData && state.viewData.pendingTx) {
confirmTx.restore();
} else {
fallbackView();
}
break;
case "transaction":
if (state.viewData && state.viewData.tx) {
transactionDetail.render();

View File

@@ -15,7 +15,11 @@ const {
filterTransactions,
} = require("../../shared/transactions");
const { resolveEnsNames } = require("../../shared/ens");
const { updateSendBalance, renderSendTokenSelect } = require("./send");
const {
updateSendBalance,
renderSendTokenSelect,
resetSendValidation,
} = require("./send");
const { log } = require("../../shared/log");
const makeBlockie = require("ethereum-blockies-base64");
const { decryptWithPassword } = require("../../shared/vault");
@@ -259,6 +263,7 @@ function init(_ctx) {
$("send-token").classList.remove("hidden");
$("send-token-static").classList.add("hidden");
updateSendBalance();
resetSendValidation();
showView("send");
});
@@ -320,6 +325,9 @@ function init(_ctx) {
$("export-privkey-flash").classList.remove("hidden");
return;
}
const btn = $("btn-export-privkey-confirm");
btn.disabled = true;
btn.classList.add("text-muted");
const wallet = state.wallets[state.selectedWallet];
try {
const secret = await decryptWithPassword(
@@ -339,6 +347,9 @@ function init(_ctx) {
} catch {
$("export-privkey-flash").textContent = "Wrong password.";
$("export-privkey-flash").classList.remove("hidden");
} finally {
btn.disabled = false;
btn.classList.remove("text-muted");
}
});

View File

@@ -23,7 +23,11 @@ const {
filterTransactions,
} = require("../../shared/transactions");
const { resolveEnsNames } = require("../../shared/ens");
const { updateSendBalance, renderSendTokenSelect } = require("./send");
const {
updateSendBalance,
renderSendTokenSelect,
resetSendValidation,
} = require("./send");
const { log } = require("../../shared/log");
const makeBlockie = require("ethereum-blockies-base64");
@@ -372,6 +376,7 @@ function init(_ctx) {
});
}
updateSendBalance();
resetSendValidation();
showView("send");
});

View File

@@ -4,6 +4,8 @@ const {
addressTitle,
escapeHtml,
showView,
showError,
hideError,
} = require("./helpers");
const { state, saveState } = require("../../shared/state");
const { formatEther, formatUnits, Interface, toUtf8String } = require("ethers");
@@ -170,6 +172,8 @@ function showTxApproval(details) {
// If this is an ERC-20 call, try to extract the real recipient and amount
const decoded = decodeCalldata(details.txParams.data, toAddr || "");
if (decoded && decoded.details) {
let decodedTokenAddr = null;
let decodedTokenSymbol = null;
for (const d of decoded.details) {
if (d.label === "Recipient" && d.address) {
pendingTxDetails.to = d.address;
@@ -177,10 +181,20 @@ function showTxApproval(details) {
if (d.label === "Amount") {
pendingTxDetails.amount = d.rawValue || d.value;
}
if (d.label === "Token In" && d.isToken && d.address) {
const t = TOKEN_BY_ADDRESS.get(d.address.toLowerCase());
if (t) {
decodedTokenAddr = d.address;
decodedTokenSymbol = t.symbol;
}
}
}
if (token) {
pendingTxDetails.token = toAddr;
pendingTxDetails.tokenSymbol = token.symbol;
} else if (decodedTokenAddr) {
pendingTxDetails.token = decodedTokenAddr;
pendingTxDetails.tokenSymbol = decodedTokenSymbol;
}
}
@@ -254,6 +268,9 @@ function showTxApproval(details) {
$("approve-tx-data-section").classList.add("hidden");
}
$("approve-tx-password").value = "";
$("approve-tx-error").classList.add("hidden");
showView("approve-tx");
}
@@ -342,7 +359,7 @@ function showSignApproval(details) {
}
$("approve-sign-password").value = "";
$("approve-sign-error").classList.add("hidden");
hideError("approve-sign-error");
$("btn-approve-sign").disabled = false;
$("btn-approve-sign").classList.remove("text-muted");
@@ -407,11 +424,10 @@ function init(ctx) {
$("btn-approve-tx").addEventListener("click", () => {
const password = $("approve-tx-password").value;
if (!password) {
$("approve-tx-error").textContent = "Please enter your password.";
$("approve-tx-error").classList.remove("hidden");
showError("approve-tx-error", "Please enter your password.");
return;
}
$("approve-tx-error").classList.add("hidden");
hideError("approve-tx-error");
$("btn-approve-tx").disabled = true;
$("btn-approve-tx").classList.add("text-muted");
@@ -447,11 +463,10 @@ function init(ctx) {
$("btn-approve-sign").addEventListener("click", () => {
const password = $("approve-sign-password").value;
if (!password) {
$("approve-sign-error").textContent = "Please enter your password.";
$("approve-sign-error").classList.remove("hidden");
showError("approve-sign-error", "Please enter your password.");
return;
}
$("approve-sign-error").classList.add("hidden");
hideError("approve-sign-error");
$("btn-approve-sign").disabled = true;
$("btn-approve-sign").classList.add("text-muted");
@@ -469,8 +484,7 @@ function init(ctx) {
} else {
const msg =
(response && response.error) || "Signing failed.";
$("approve-sign-error").textContent = msg;
$("approve-sign-error").classList.remove("hidden");
showError("approve-sign-error", msg);
$("btn-approve-sign").disabled = false;
$("btn-approve-sign").classList.remove("text-muted");
}

View File

@@ -1,6 +1,6 @@
// Transaction confirmation view + password modal.
// Shows transaction details, warnings, errors. On proceed, opens
// password modal, decrypts secret, signs and broadcasts.
// Transaction confirmation view with inline password.
// Shows transaction details, warnings, errors. On Sign & Send,
// reads inline password, decrypts secret, signs and broadcasts.
const {
parseEther,
@@ -14,6 +14,7 @@ const {
showError,
hideError,
showView,
showFlash,
addressTitle,
addressDotHtml,
escapeHtml,
@@ -38,6 +39,13 @@ const EXT_ICON =
let pendingTx = null;
function restore() {
const d = state.viewData;
if (d && d.pendingTx) {
show(d.pendingTx);
}
}
function etherscanTokenLink(address) {
return `https://etherscan.io/token/${address}`;
}
@@ -95,11 +103,22 @@ function show(txInfo) {
// Token contract section (ERC-20 only)
const tokenSection = $("confirm-token-section");
if (isErc20) {
const dot = addressDotHtml(txInfo.token);
const link = etherscanTokenLink(txInfo.token);
$("confirm-token-contract").innerHTML =
escapeHtml(txInfo.token) +
` <a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
`<div class="flex items-center">${dot}` +
`<span class="break-all underline decoration-dashed cursor-pointer" data-copy="${escapeHtml(txInfo.token)}">${escapeHtml(txInfo.token)}</span>` +
`<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>` +
`</div>`;
tokenSection.classList.remove("hidden");
// Attach click-to-copy on the contract address
const copyEl = tokenSection.querySelector("[data-copy]");
if (copyEl) {
copyEl.onclick = () => {
navigator.clipboard.writeText(copyEl.dataset.copy);
showFlash("Copied!");
};
}
} else {
tokenSection.classList.add("hidden");
}
@@ -214,12 +233,24 @@ function show(txInfo) {
sendBtn.classList.remove("text-muted");
}
// Reset password field and error
$("confirm-tx-password").value = "";
hideError("confirm-tx-password-error");
// Gas estimate — show placeholder then fetch async
$("confirm-fee").classList.remove("hidden");
$("confirm-fee-amount").textContent = "Estimating...";
state.viewData = { pendingTx: txInfo };
showView("confirm-tx");
// Reset recipient warning: reserve space (visibility:hidden) while
// the async check runs, preventing layout shift per README policy.
const recipientWarning = $("confirm-recipient-warning");
recipientWarning.style.display = "";
recipientWarning.style.visibility = "hidden";
estimateGas(txInfo);
checkRecipientHistory(txInfo);
}
async function estimateGas(txInfo) {
@@ -262,39 +293,48 @@ async function estimateGas(txInfo) {
}
}
function showPasswordModal() {
$("modal-password").value = "";
hideError("modal-password-error");
$("password-modal").classList.remove("hidden");
}
function hidePasswordModal() {
$("password-modal").classList.add("hidden");
async function checkRecipientHistory(txInfo) {
const el = $("confirm-recipient-warning");
try {
const provider = getProvider(state.rpcUrl);
// Skip warning for contract addresses — they may legitimately
// have zero outgoing transactions (getTransactionCount returns
// the nonce, i.e. sent-tx count only).
const code = await provider.getCode(txInfo.to);
if (code && code !== "0x") {
// Contract address — hide the reserved space entirely
el.style.display = "none";
return;
}
const txCount = await provider.getTransactionCount(txInfo.to);
if (txCount === 0) {
el.style.visibility = "visible";
} else {
// Address has history — collapse the reserved space
el.style.display = "none";
}
} catch (e) {
log.errorf("recipient history check failed:", e.message);
// On error, collapse the reserved space rather than showing a
// false warning or leaving an empty gap
el.style.display = "none";
}
}
function init(ctx) {
$("btn-confirm-send").addEventListener("click", () => {
showPasswordModal();
});
$("btn-confirm-back").addEventListener("click", () => {
showView("send");
});
$("btn-modal-cancel").addEventListener("click", () => {
hidePasswordModal();
});
$("btn-modal-confirm").addEventListener("click", async () => {
const password = $("modal-password").value;
$("btn-confirm-send").addEventListener("click", async () => {
const password = $("confirm-tx-password").value;
if (!password) {
showError("modal-password-error", "Please enter your password.");
showError(
"confirm-tx-password-error",
"Please enter your password.",
);
return;
}
const wallet = state.wallets[state.selectedWallet];
let decryptedSecret;
hideError("modal-password-error");
hideError("confirm-tx-password-error");
try {
decryptedSecret = await decryptWithPassword(
@@ -302,11 +342,12 @@ function init(ctx) {
password,
);
} catch (e) {
showError("modal-password-error", "Wrong password.");
showError("confirm-tx-password-error", "Wrong password.");
return;
}
hidePasswordModal();
$("btn-confirm-send").disabled = true;
$("btn-confirm-send").classList.add("text-muted");
let tx;
try {
@@ -343,8 +384,15 @@ function init(ctx) {
decryptedSecret = null;
const hash = tx ? tx.hash : null;
txStatus.showError(pendingTx, hash, e.shortMessage || e.message);
} finally {
$("btn-confirm-send").disabled = false;
$("btn-confirm-send").classList.remove("text-muted");
}
});
$("btn-confirm-back").addEventListener("click", () => {
showView("send");
});
}
module.exports = { init, show };
module.exports = { init, show, restore };

View File

@@ -40,6 +40,10 @@ function init(_ctx) {
return;
}
const btn = $("btn-delete-wallet-confirm");
btn.disabled = true;
btn.classList.add("text-muted");
const walletIdx = deleteWalletIndex;
const wallet = state.wallets[walletIdx];
@@ -49,6 +53,8 @@ function init(_ctx) {
} catch (_e) {
$("delete-wallet-flash").textContent = "Wrong password.";
$("delete-wallet-flash").classList.remove("hidden");
btn.disabled = false;
btn.classList.remove("text-muted");
return;
}

View File

@@ -11,7 +11,11 @@ const {
truncateMiddle,
} = require("./helpers");
const { state, saveState, currentAddress } = require("../../shared/state");
const { updateSendBalance, renderSendTokenSelect } = require("./send");
const {
updateSendBalance,
renderSendTokenSelect,
resetSendValidation,
} = require("./send");
const { deriveAddressFromXpub } = require("../../shared/wallet");
const {
formatUsd,
@@ -388,6 +392,7 @@ function init(ctx) {
$("send-token-static").classList.add("hidden");
renderSendTokenSelect(addr);
updateSendBalance();
resetSendValidation();
showView("send");
});

View File

@@ -11,6 +11,107 @@ const { state, currentAddress } = require("../../shared/state");
let ctx;
const { getProvider } = require("../../shared/balances");
const { KNOWN_SYMBOLS, resolveSymbol } = require("../../shared/tokenList");
const { getAddress } = require("ethers");
const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
/**
* Validate a destination address string.
* Returns { valid: true } or { valid: false, error: "..." }.
*/
function validateToAddress(value) {
const v = value.trim();
if (!v) return { valid: false, error: "" };
// ENS names: contains a dot and doesn't start with 0x
if (v.includes(".") && !v.startsWith("0x")) {
// Basic ENS format check: at least one label before and after dot
if (/^[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+$/.test(v)) {
return { valid: true };
}
return {
valid: false,
error: "Please enter a valid ENS name.",
};
}
// Must look like an Ethereum address
if (!/^0x[0-9a-fA-F]{40}$/.test(v)) {
return {
valid: false,
error: "Please enter a valid Ethereum address.",
};
}
// Reject zero address
if (v.toLowerCase() === ZERO_ADDRESS) {
return {
valid: false,
error: "Sending to the zero address is not allowed.",
};
}
// EIP-55 checksum validation: all-lowercase is ok, otherwise must match checksum
if (v !== v.toLowerCase()) {
try {
const checksummed = getAddress(v);
if (checksummed !== v) {
return {
valid: false,
error: "Address checksum is invalid. Please double-check the address.",
};
}
} catch {
return {
valid: false,
error: "Address checksum is invalid. Please double-check the address.",
};
}
}
// Warn if sending to own address
const addr = currentAddress();
if (addr && v.toLowerCase() === addr.address.toLowerCase()) {
// Allow but will warn — we return valid with a warning
return {
valid: true,
warning: "This is your own address. Are you sure?",
};
}
return { valid: true };
}
function updateToValidation() {
const input = $("send-to");
const errorEl = $("send-to-error");
const btn = $("btn-send-review");
const value = input.value.trim();
if (!value) {
errorEl.textContent = "";
btn.disabled = true;
btn.classList.add("opacity-50");
return;
}
const result = validateToAddress(value);
if (!result.valid) {
errorEl.textContent = result.error;
errorEl.style.color = "#cc0000";
btn.disabled = true;
btn.classList.add("opacity-50");
} else if (result.warning) {
errorEl.textContent = result.warning;
errorEl.style.color = "#b8860b";
btn.disabled = false;
btn.classList.remove("opacity-50");
} else {
errorEl.textContent = "";
btn.disabled = false;
btn.classList.remove("opacity-50");
}
}
const EXT_ICON =
`<span style="display:inline-block;width:10px;height:10px;margin-left:4px;vertical-align:middle">` +
@@ -88,6 +189,13 @@ function init(_ctx) {
ctx = _ctx;
$("send-token").addEventListener("change", updateSendBalance);
// Initial state: disable review button until address is entered
$("btn-send-review").disabled = true;
$("btn-send-review").classList.add("opacity-50");
// Validate address on input
$("send-to").addEventListener("input", updateToValidation);
$("btn-send-review").addEventListener("click", async () => {
const to = $("send-to").value.trim();
const amount = $("send-amount").value.trim();
@@ -95,6 +203,15 @@ function init(_ctx) {
showFlash("Please enter a recipient address.");
return;
}
// Re-validate before proceeding
const validation = validateToAddress(to);
if (!validation.valid) {
showFlash(
validation.error || "Please enter a valid Ethereum address.",
);
return;
}
if (!amount || isNaN(parseFloat(amount)) || parseFloat(amount) <= 0) {
showFlash("Please enter a valid amount.");
return;
@@ -159,4 +276,19 @@ function init(_ctx) {
});
}
module.exports = { init, updateSendBalance, renderSendTokenSelect };
function resetSendValidation() {
const errorEl = $("send-to-error");
const btn = $("btn-send-review");
if (errorEl) errorEl.textContent = "";
if (btn) {
btn.disabled = true;
btn.classList.add("opacity-50");
}
}
module.exports = {
init,
updateSendBalance,
renderSendTokenSelect,
resetSendValidation,
};

View File

@@ -43,10 +43,11 @@ function toAddressHtml(address) {
if (title) {
return (
`<div class="flex items-center font-bold">${dot}${escapeHtml(title)}</div>` +
`<div class="break-all">${escapeHtml(address)}${extLink}</div>`
`<div class="break-all underline decoration-dashed cursor-pointer" data-copy="${escapeHtml(address)}">${escapeHtml(address)}</div>` +
extLink
);
}
return `<div class="flex items-center">${dot}<span class="break-all">${escapeHtml(address)}</span>${extLink}</div>`;
return `<div class="flex items-center">${dot}<span class="break-all underline decoration-dashed cursor-pointer" data-copy="${escapeHtml(address)}">${escapeHtml(address)}</span>${extLink}</div>`;
}
function txHashHtml(hash) {
@@ -139,7 +140,7 @@ function etherscanTokenLink(address) {
function decodedDetailsHtml(decoded) {
if (!decoded || !decoded.details) return "";
let html = "";
let html = `<div class="border border-border border-dashed p-2 mb-3">`;
if (decoded.name) {
html += `<div class="mb-2"><div class="text-xs text-muted mb-1">Action</div>`;
html += `<div class="font-bold">${escapeHtml(decoded.name)}</div></div>`;
@@ -164,20 +165,36 @@ function decodedDetailsHtml(decoded) {
}
html += `</div>`;
}
html += `</div>`;
return html;
}
function renderSuccess() {
const d = state.viewData;
if (!d || !d.hash) return;
$("success-tx-summary").textContent = d.amount + " " + d.symbol;
$("success-tx-to").innerHTML = toAddressHtml(d.to);
const hasDecoded = d.decoded && d.decoded.details;
// When decoded details are present, the Amount and To are already
// shown inside the decoded well — hide the top-level duplicates.
const summarySection = $("success-tx-summary").parentElement;
const toSection = $("success-tx-to").parentElement;
if (hasDecoded) {
summarySection.classList.add("hidden");
toSection.classList.add("hidden");
} else {
summarySection.classList.remove("hidden");
toSection.classList.remove("hidden");
$("success-tx-summary").textContent = d.amount + " " + d.symbol;
$("success-tx-to").innerHTML = toAddressHtml(d.to);
}
$("success-tx-block").textContent = String(d.blockNumber);
$("success-tx-hash").innerHTML = txHashHtml(d.hash);
// Show decoded calldata details if present
const decodedEl = $("success-tx-decoded");
if (decodedEl && d.decoded) {
if (decodedEl && hasDecoded) {
decodedEl.innerHTML = decodedDetailsHtml(d.decoded);
decodedEl.classList.remove("hidden");
} else if (decodedEl) {

View File

@@ -445,12 +445,18 @@ function decode(data, toAddress) {
const maxUint160 = BigInt(
"0xffffffffffffffffffffffffffffffffffffffff",
);
const amountStr =
inputAmount >= maxUint160
? "Unlimited"
: formatAmount(inputAmount, inInfo.decimals) +
(inSymbol ? " " + inSymbol : "");
details.push({ label: "Amount", value: amountStr });
const isUnlimited = inputAmount >= maxUint160;
const amountRaw = isUnlimited
? "Unlimited"
: formatAmount(inputAmount, inInfo.decimals);
const amountStr = isUnlimited
? "Unlimited"
: amountRaw + (inSymbol ? " " + inSymbol : "");
details.push({
label: "Amount",
value: amountStr,
rawValue: amountRaw,
});
}
if (outSymbol) {