Compare commits
6 Commits
8379ab4685
...
fix/70-con
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc4e78c466 | ||
| e8ede7010a | |||
| a2fbb0e30d | |||
| 24464ffe33 | |||
| 34c66d19c4 | |||
|
|
9a7aa1f4fc |
@@ -422,11 +422,6 @@
|
||||
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">
|
||||
@@ -468,12 +463,12 @@
|
||||
</div>
|
||||
|
||||
<!-- ERC-20 token contract (hidden for ETH) -->
|
||||
<div id="confirm-token-section" class="mb-3 hidden">
|
||||
<div class="text-xs text-muted mb-1">Token contract</div>
|
||||
<div
|
||||
id="confirm-token-contract"
|
||||
class="text-xs break-all"
|
||||
></div>
|
||||
<div
|
||||
id="confirm-token-section"
|
||||
class="bg-hover rounded-md mx-1 p-3 mb-3 text-xs hidden"
|
||||
>
|
||||
<div class="font-bold mb-2">Token Contract</div>
|
||||
<div id="confirm-token-contract" class="break-all"></div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
@@ -642,9 +637,10 @@
|
||||
<div class="flex justify-center mb-3">
|
||||
<canvas id="receive-qr"></canvas>
|
||||
</div>
|
||||
<div class="border border-border p-2 break-all mb-3 text-xs">
|
||||
<span id="receive-dot"></span>
|
||||
<span id="receive-address" class="select-all"></span>
|
||||
<div
|
||||
class="border border-border p-2 break-all mb-3 text-xs cursor-pointer"
|
||||
>
|
||||
<span id="receive-address-block" class="select-all"></span>
|
||||
<span id="receive-etherscan-link"></span>
|
||||
</div>
|
||||
<button
|
||||
|
||||
@@ -12,7 +12,7 @@ const {
|
||||
balanceLine,
|
||||
} = require("./helpers");
|
||||
const { state, currentAddress, saveState } = require("../../shared/state");
|
||||
const { TOKEN_BY_ADDRESS } = require("../../shared/tokenList");
|
||||
const { TOKEN_BY_ADDRESS, resolveSymbol } = require("../../shared/tokenList");
|
||||
const {
|
||||
formatUsd,
|
||||
getPrice,
|
||||
@@ -96,14 +96,11 @@ function show() {
|
||||
const tb = (addr.tokenBalances || []).find(
|
||||
(t) => t.address.toLowerCase() === tokenId.toLowerCase(),
|
||||
);
|
||||
const tracked = (state.trackedTokens || []).find(
|
||||
(t) => t.address.toLowerCase() === tokenId.toLowerCase(),
|
||||
symbol = resolveSymbol(
|
||||
tokenId,
|
||||
addr.tokenBalances,
|
||||
state.trackedTokens,
|
||||
);
|
||||
symbol =
|
||||
(tb && tb.symbol) ||
|
||||
(tracked && tracked.symbol) ||
|
||||
(knownToken && knownToken.symbol) ||
|
||||
"?";
|
||||
amount = tb ? parseFloat(tb.balance || "0") : 0;
|
||||
price = getPrice(symbol);
|
||||
}
|
||||
|
||||
@@ -95,10 +95,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" id="confirm-token-contract-copy" 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>`;
|
||||
if (txInfo.tokenName) {
|
||||
$("confirm-token-contract").innerHTML +=
|
||||
`<div class="mt-1"><span class="text-muted">Name:</span> ${escapeHtml(txInfo.tokenName)}</div>`;
|
||||
}
|
||||
if (txInfo.tokenSymbol) {
|
||||
$("confirm-token-contract").innerHTML +=
|
||||
`<div class="mt-1"><span class="text-muted">Symbol:</span> ${escapeHtml(txInfo.tokenSymbol)}</div>`;
|
||||
}
|
||||
tokenSection.classList.remove("hidden");
|
||||
} else {
|
||||
tokenSection.classList.add("hidden");
|
||||
@@ -273,6 +285,13 @@ function hidePasswordModal() {
|
||||
}
|
||||
|
||||
function init(ctx) {
|
||||
$("confirm-token-section").addEventListener("click", (e) => {
|
||||
const copyEl = e.target.closest("[data-copy]");
|
||||
if (copyEl) {
|
||||
navigator.clipboard.writeText(copyEl.dataset.copy);
|
||||
}
|
||||
});
|
||||
|
||||
$("btn-confirm-send").addEventListener("click", () => {
|
||||
showPasswordModal();
|
||||
});
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
const { $, showView, showFlash, addressDotHtml } = require("./helpers");
|
||||
const {
|
||||
$,
|
||||
showView,
|
||||
showFlash,
|
||||
formatAddressHtml,
|
||||
addressTitle,
|
||||
} = require("./helpers");
|
||||
const { state, currentAddress } = require("../../shared/state");
|
||||
const QRCode = require("qrcode");
|
||||
|
||||
@@ -12,8 +18,12 @@ const EXT_ICON =
|
||||
function show() {
|
||||
const addr = currentAddress();
|
||||
const address = addr ? addr.address : "";
|
||||
$("receive-dot").innerHTML = address ? addressDotHtml(address) : "";
|
||||
$("receive-address").textContent = address;
|
||||
const title = address ? addressTitle(address, state.wallets) : null;
|
||||
const ensName = addr ? addr.ensName || null : null;
|
||||
$("receive-address-block").innerHTML = address
|
||||
? formatAddressHtml(address, ensName, null, title)
|
||||
: "";
|
||||
$("receive-address-block").dataset.full = address;
|
||||
const link = address ? `https://etherscan.io/address/${address}` : "";
|
||||
$("receive-etherscan-link").innerHTML = link
|
||||
? `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`
|
||||
@@ -50,8 +60,16 @@ function show() {
|
||||
}
|
||||
|
||||
function init(ctx) {
|
||||
$("receive-address-block").addEventListener("click", () => {
|
||||
const addr = $("receive-address-block").dataset.full;
|
||||
if (addr) {
|
||||
navigator.clipboard.writeText(addr);
|
||||
showFlash("Copied!");
|
||||
}
|
||||
});
|
||||
|
||||
$("btn-receive-copy").addEventListener("click", () => {
|
||||
const addr = $("receive-address").textContent;
|
||||
const addr = $("receive-address-block").dataset.full;
|
||||
if (addr) {
|
||||
navigator.clipboard.writeText(addr);
|
||||
showFlash("Copied!");
|
||||
|
||||
@@ -10,108 +10,11 @@ const {
|
||||
const { state, currentAddress } = require("../../shared/state");
|
||||
let ctx;
|
||||
const { getProvider } = require("../../shared/balances");
|
||||
const { KNOWN_SYMBOLS, TOKEN_BY_ADDRESS } = 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 {
|
||||
KNOWN_SYMBOLS,
|
||||
TOKEN_BY_ADDRESS,
|
||||
resolveSymbol,
|
||||
} = require("../../shared/tokenList");
|
||||
|
||||
const EXT_ICON =
|
||||
`<span style="display:inline-block;width:10px;height:10px;margin-left:4px;vertical-align:middle">` +
|
||||
@@ -174,15 +77,11 @@ function updateSendBalance() {
|
||||
const tb = (addr.tokenBalances || []).find(
|
||||
(t) => t.address.toLowerCase() === token.toLowerCase(),
|
||||
);
|
||||
const knownToken = TOKEN_BY_ADDRESS.get(token.toLowerCase());
|
||||
const tracked = (state.trackedTokens || []).find(
|
||||
(t) => t.address.toLowerCase() === token.toLowerCase(),
|
||||
const symbol = resolveSymbol(
|
||||
token,
|
||||
addr.tokenBalances,
|
||||
state.trackedTokens,
|
||||
);
|
||||
const symbol =
|
||||
(tb && tb.symbol) ||
|
||||
(tracked && tracked.symbol) ||
|
||||
(knownToken && knownToken.symbol) ||
|
||||
"?";
|
||||
const bal = tb ? tb.balance || "0" : "0";
|
||||
$("send-balance").textContent =
|
||||
"Current balance: " + bal + " " + symbol;
|
||||
@@ -193,13 +92,6 @@ 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();
|
||||
@@ -207,15 +99,6 @@ 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;
|
||||
@@ -245,20 +128,28 @@ function init(_ctx) {
|
||||
|
||||
let tokenSymbol = null;
|
||||
let tokenBalance = null;
|
||||
let tokenName = null;
|
||||
if (token !== "ETH") {
|
||||
const tb = (addr.tokenBalances || []).find(
|
||||
(t) => t.address.toLowerCase() === token.toLowerCase(),
|
||||
);
|
||||
const knownTk = TOKEN_BY_ADDRESS.get(token.toLowerCase());
|
||||
const trackedTk = (state.trackedTokens || []).find(
|
||||
(t) => t.address.toLowerCase() === token.toLowerCase(),
|
||||
tokenSymbol = resolveSymbol(
|
||||
token,
|
||||
addr.tokenBalances,
|
||||
state.trackedTokens,
|
||||
);
|
||||
tokenSymbol =
|
||||
(tb && tb.symbol) ||
|
||||
(trackedTk && trackedTk.symbol) ||
|
||||
(knownTk && knownTk.symbol) ||
|
||||
"?";
|
||||
tokenBalance = tb ? tb.balance || "0" : "0";
|
||||
// Resolve token name from balances, tracked tokens, or known list
|
||||
const lower = token.toLowerCase();
|
||||
tokenName =
|
||||
(tb && tb.name) ||
|
||||
(
|
||||
(state.trackedTokens || []).find(
|
||||
(t) => t.address.toLowerCase() === lower,
|
||||
) || {}
|
||||
).name ||
|
||||
(TOKEN_BY_ADDRESS.get(lower) || {}).name ||
|
||||
null;
|
||||
}
|
||||
|
||||
ctx.showConfirmTx({
|
||||
@@ -270,6 +161,7 @@ function init(_ctx) {
|
||||
balance: addr.balance,
|
||||
tokenSymbol: tokenSymbol,
|
||||
tokenBalance: tokenBalance,
|
||||
tokenName: tokenName,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -3645,10 +3645,27 @@ async function getTopTokenPrices(n) {
|
||||
return prices;
|
||||
}
|
||||
|
||||
// Resolve a token symbol from multiple sources, never returning "?".
|
||||
function resolveSymbol(tokenAddress, tokenBalances, trackedTokens) {
|
||||
const lower = (tokenAddress || "").toLowerCase();
|
||||
const tb = (tokenBalances || []).find(
|
||||
(t) => t.address.toLowerCase() === lower,
|
||||
);
|
||||
if (tb && tb.symbol) return tb.symbol;
|
||||
const known = TOKEN_BY_ADDRESS.get(lower);
|
||||
if (known && known.symbol) return known.symbol;
|
||||
const tracked = (trackedTokens || []).find(
|
||||
(t) => t.address.toLowerCase() === lower,
|
||||
);
|
||||
if (tracked && tracked.symbol) return tracked.symbol;
|
||||
return lower.slice(0, 10) + "\u2026";
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
TOKENS,
|
||||
TOKEN_BY_ADDRESS,
|
||||
KNOWN_SYMBOLS,
|
||||
getTopTokens,
|
||||
getTopTokenPrices,
|
||||
resolveSymbol,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user