Token auto-discovery, tx history, balance polling, EIP-6963, UI overhaul
All checks were successful
check / check (push) Successful in 14s
All checks were successful
check / check (push) Successful in 14s
Major changes: - Fetch token balances and tx history from Blockscout API (configurable) - Remove manual token discovery (discoverTokens) in favor of Blockscout - HD address gap scanning on mnemonic import - Duplicate mnemonic detection on wallet add - EIP-6963 multi-wallet discovery + selectedAddress updates in inpage - Two-tier balance refresh: 10s while popup open, 60s background - Fix $0.00 flash before prices load (return null when no prices) - No-layout-shift: min-height on total value element - Aligned balance columns (42ch address width, consistent USD column) - All errors use flash messages instead of off-screen error divs - Settings gear in global title bar, add-wallet moved to settings pane - Settings wells with light grey background, configurable Blockscout URL - Consistent "< Back" buttons top-left on all views - Address titles (Address 1.1, 1.2, etc.) on main and detail views - Send view shows current balance of selected asset - Clickable affordance policy added to README - Shortened mnemonic backup warning - Fix broken background script constant imports
This commit is contained in:
@@ -21,8 +21,21 @@
|
||||
>@sneak</a
|
||||
>
|
||||
</h1>
|
||||
<button
|
||||
id="btn-settings"
|
||||
class="bg-transparent border-none text-fg cursor-pointer text-2xl p-0 leading-none"
|
||||
title="Settings"
|
||||
>
|
||||
⚙
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- ============ FLASH MESSAGE AREA ============ -->
|
||||
<div
|
||||
id="flash-msg"
|
||||
class="text-xs text-muted min-h-[1.25rem] mb-1"
|
||||
></div>
|
||||
|
||||
<!-- ============ WELCOME / FIRST USE ============ -->
|
||||
<div id="view-welcome" class="view hidden">
|
||||
<p class="mb-3">Welcome! To get started, add a wallet.</p>
|
||||
@@ -36,6 +49,12 @@
|
||||
|
||||
<!-- ============ ADD WALLET (unified create/import) ============ -->
|
||||
<div id="view-add-wallet" class="view hidden">
|
||||
<button
|
||||
id="btn-add-wallet-back"
|
||||
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer mb-2"
|
||||
>
|
||||
< Back
|
||||
</button>
|
||||
<h2 class="font-bold mb-2">Add Wallet</h2>
|
||||
<p class="mb-2">
|
||||
Enter your 12 or 24 word recovery phrase below, or click the
|
||||
@@ -62,10 +81,8 @@
|
||||
id="add-wallet-phrase-warning"
|
||||
class="text-xs mb-2 border border-border border-dashed p-2 hidden"
|
||||
>
|
||||
These words are your recovery phrase. Write them down on
|
||||
paper and keep them somewhere safe. Anyone with these words
|
||||
can access your funds. If you lose them, your wallet cannot
|
||||
be recovered.
|
||||
Write these words down and keep them safe. Anyone with them
|
||||
can take your funds; if you lose them, your wallet is gone.
|
||||
</div>
|
||||
<div class="mb-2" id="add-wallet-password-section">
|
||||
<label class="block mb-1">Choose a password</label>
|
||||
@@ -87,24 +104,12 @@
|
||||
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
id="btn-add-wallet-confirm"
|
||||
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
<button
|
||||
id="btn-add-wallet-back"
|
||||
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
id="add-wallet-error"
|
||||
class="mt-2 border border-border border-dashed p-1 hidden"
|
||||
></div>
|
||||
<button
|
||||
id="btn-add-wallet-confirm"
|
||||
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
<div class="mt-3 text-xs text-muted">
|
||||
Have a private key instead?
|
||||
<button
|
||||
@@ -118,6 +123,12 @@
|
||||
|
||||
<!-- ============ IMPORT PRIVATE KEY ============ -->
|
||||
<div id="view-import-key" class="view hidden">
|
||||
<button
|
||||
id="btn-import-key-back"
|
||||
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer mb-2"
|
||||
>
|
||||
< Back
|
||||
</button>
|
||||
<h2 class="font-bold mb-2">Import Private Key</h2>
|
||||
<p class="mb-2">
|
||||
Paste your private key below. This wallet will have a single
|
||||
@@ -151,84 +162,58 @@
|
||||
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
id="btn-import-key-confirm"
|
||||
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
|
||||
>
|
||||
Import
|
||||
</button>
|
||||
<button
|
||||
id="btn-import-key-back"
|
||||
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
id="import-key-error"
|
||||
class="mt-2 border border-border border-dashed p-1 hidden"
|
||||
></div>
|
||||
<button
|
||||
id="btn-import-key-confirm"
|
||||
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
|
||||
>
|
||||
Import
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- ============ MAIN VIEW: ALL WALLETS & ADDRESSES ============ -->
|
||||
<div id="view-main" class="view hidden">
|
||||
<!-- total portfolio value -->
|
||||
<div id="total-value" class="text-2xl font-bold mb-2"></div>
|
||||
<div
|
||||
id="total-value"
|
||||
class="text-2xl font-bold mb-2 min-h-[2rem]"
|
||||
></div>
|
||||
|
||||
<!-- wallet list -->
|
||||
<div id="wallet-list"></div>
|
||||
|
||||
<div class="mt-3 border-t border-border pt-2 flex gap-2">
|
||||
<button
|
||||
id="btn-main-add-wallet"
|
||||
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
|
||||
>
|
||||
+ Add wallet
|
||||
</button>
|
||||
<button
|
||||
id="btn-settings"
|
||||
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
|
||||
>
|
||||
Settings
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============ ADDRESS DETAIL VIEW ============ -->
|
||||
<div id="view-address" class="view hidden">
|
||||
<button
|
||||
id="btn-address-back"
|
||||
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer mb-2"
|
||||
>
|
||||
< Back
|
||||
</button>
|
||||
<div
|
||||
class="flex justify-between items-center border-b border-border pb-1 mb-2"
|
||||
>
|
||||
<h2 class="font-bold" id="address-title">Address</h2>
|
||||
<button
|
||||
id="btn-address-back"
|
||||
class="border border-border px-1 hover:bg-fg hover:text-bg cursor-pointer"
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="address-ens" class="font-bold mb-1 hidden"></div>
|
||||
<div
|
||||
id="address-full"
|
||||
class="text-xs break-all mb-1 cursor-pointer"
|
||||
class="flex text-xs mb-3 cursor-pointer"
|
||||
title="Click to copy"
|
||||
></div>
|
||||
<div
|
||||
class="text-xs text-muted mb-3"
|
||||
id="address-copied-msg"
|
||||
></div>
|
||||
|
||||
<!-- balance -->
|
||||
>
|
||||
<span
|
||||
id="address-full"
|
||||
class="shrink-0"
|
||||
style="width: 42ch"
|
||||
></span>
|
||||
<span
|
||||
id="address-usd-total"
|
||||
class="text-right text-muted flex-1"
|
||||
></span>
|
||||
</div>
|
||||
<!-- balances -->
|
||||
<div class="border-b border-border-light pb-2 mb-2">
|
||||
<div class="text-base font-bold">
|
||||
<span id="address-eth-balance">0.0000</span> ETH
|
||||
</div>
|
||||
<div
|
||||
class="text-xs text-muted"
|
||||
id="address-usd-value"
|
||||
></div>
|
||||
<div id="address-balances"></div>
|
||||
</div>
|
||||
|
||||
<!-- actions -->
|
||||
@@ -245,31 +230,33 @@
|
||||
>
|
||||
Receive
|
||||
</button>
|
||||
<button
|
||||
id="btn-add-token"
|
||||
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer flex-1"
|
||||
>
|
||||
+ Token
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- tokens -->
|
||||
<div>
|
||||
<div
|
||||
class="flex justify-between items-center border-b border-border pb-1 mb-1"
|
||||
>
|
||||
<h2 class="font-bold">Tokens</h2>
|
||||
<button
|
||||
id="btn-add-token"
|
||||
class="border border-border px-1 hover:bg-fg hover:text-bg cursor-pointer text-xs"
|
||||
>
|
||||
+ Add
|
||||
</button>
|
||||
<!-- transactions -->
|
||||
<div class="mt-3">
|
||||
<div class="border-b border-border pb-1 mb-1">
|
||||
<h2 class="font-bold">Transactions</h2>
|
||||
</div>
|
||||
<div id="token-list">
|
||||
<div class="text-muted text-xs py-1">
|
||||
No tokens added yet. Use "+ Add" to track a token.
|
||||
</div>
|
||||
<div id="tx-list">
|
||||
<div class="text-muted text-xs py-1">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============ SEND ============ -->
|
||||
<div id="view-send" class="view hidden">
|
||||
<button
|
||||
id="btn-send-back"
|
||||
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer mb-2"
|
||||
>
|
||||
< Back
|
||||
</button>
|
||||
<h2 class="font-bold mb-2">Send</h2>
|
||||
<div class="mb-2">
|
||||
<label class="block mb-1">What to send</label>
|
||||
@@ -290,7 +277,13 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="block mb-1">Amount</label>
|
||||
<div class="flex justify-between mb-1">
|
||||
<label>Amount</label>
|
||||
<span
|
||||
id="send-balance"
|
||||
class="text-xs text-muted"
|
||||
></span>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
id="send-amount"
|
||||
@@ -298,28 +291,22 @@
|
||||
placeholder="0.0"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
id="btn-send-review"
|
||||
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
|
||||
>
|
||||
Review
|
||||
</button>
|
||||
<button
|
||||
id="btn-send-back"
|
||||
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
id="send-error"
|
||||
class="mt-2 border border-border border-dashed p-1 hidden"
|
||||
></div>
|
||||
<button
|
||||
id="btn-send-review"
|
||||
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
|
||||
>
|
||||
Review
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- ============ CONFIRM TRANSACTION ============ -->
|
||||
<div id="view-confirm-tx" class="view hidden">
|
||||
<button
|
||||
id="btn-confirm-back"
|
||||
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer mb-2"
|
||||
>
|
||||
< Back
|
||||
</button>
|
||||
<h2 class="font-bold mb-2">Confirm Transaction</h2>
|
||||
<div class="mb-2">
|
||||
<div class="text-xs text-muted">From</div>
|
||||
@@ -350,20 +337,12 @@
|
||||
id="confirm-errors"
|
||||
class="mb-2 border border-border border-dashed p-2 hidden"
|
||||
></div>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
id="btn-confirm-send"
|
||||
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
<button
|
||||
id="btn-confirm-back"
|
||||
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
id="btn-confirm-send"
|
||||
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
<div
|
||||
id="confirm-status"
|
||||
class="mt-2 border border-border p-1 hidden"
|
||||
@@ -408,6 +387,12 @@
|
||||
|
||||
<!-- ============ RECEIVE ============ -->
|
||||
<div id="view-receive" class="view hidden">
|
||||
<button
|
||||
id="btn-receive-back"
|
||||
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer mb-2"
|
||||
>
|
||||
< Back
|
||||
</button>
|
||||
<h2 class="font-bold mb-2">Receive</h2>
|
||||
<p class="mb-2">
|
||||
Share this address with the sender. Make sure you only use
|
||||
@@ -420,24 +405,22 @@
|
||||
id="receive-address"
|
||||
class="border border-border p-2 break-all select-all mb-3"
|
||||
></div>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
id="btn-receive-copy"
|
||||
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
|
||||
>
|
||||
Copy address
|
||||
</button>
|
||||
<button
|
||||
id="btn-receive-back"
|
||||
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
id="btn-receive-copy"
|
||||
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
|
||||
>
|
||||
Copy address
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- ============ ADD TOKEN ============ -->
|
||||
<div id="view-add-token" class="view hidden">
|
||||
<button
|
||||
id="btn-add-token-back"
|
||||
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer mb-2"
|
||||
>
|
||||
< Back
|
||||
</button>
|
||||
<h2 class="font-bold mb-2">Add Token</h2>
|
||||
<p class="mb-2">
|
||||
Enter the contract address of the token you want to track.
|
||||
@@ -465,36 +448,44 @@
|
||||
class="flex flex-wrap gap-1"
|
||||
></div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
id="btn-add-token-confirm"
|
||||
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
<button
|
||||
id="btn-add-token-back"
|
||||
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
id="add-token-error"
|
||||
class="mt-2 border border-border border-dashed p-1 hidden"
|
||||
></div>
|
||||
<button
|
||||
id="btn-add-token-confirm"
|
||||
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- ============ SETTINGS ============ -->
|
||||
<div id="view-settings" class="view hidden">
|
||||
<h2 class="font-bold mb-2">Settings</h2>
|
||||
<button
|
||||
id="btn-settings-back"
|
||||
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer mb-2"
|
||||
>
|
||||
< Back
|
||||
</button>
|
||||
<h2 class="font-bold mb-3">Settings</h2>
|
||||
|
||||
<h2 class="font-bold mb-1">Network</h2>
|
||||
<p class="text-xs text-muted mb-1">
|
||||
The server used to talk to the Ethereum network. Change this
|
||||
if you run your own node or prefer a different provider.
|
||||
</p>
|
||||
<div class="mb-3">
|
||||
<div class="bg-well p-3 mx-1 mb-3">
|
||||
<h3 class="font-bold mb-1">Wallets</h3>
|
||||
<p class="text-xs text-muted mb-2">
|
||||
Add a new wallet from a recovery phrase or private key.
|
||||
</p>
|
||||
<button
|
||||
id="btn-main-add-wallet"
|
||||
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
|
||||
>
|
||||
+ Add wallet
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="bg-well p-3 mx-1 mb-3">
|
||||
<h3 class="font-bold mb-1">Ethereum RPC</h3>
|
||||
<p class="text-xs text-muted mb-1">
|
||||
The server used to talk to the Ethereum network. Change
|
||||
this if you run your own node or prefer a different
|
||||
provider.
|
||||
</p>
|
||||
<input
|
||||
type="text"
|
||||
id="settings-rpc"
|
||||
@@ -508,12 +499,22 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-border pt-2">
|
||||
<div class="bg-well p-3 mx-1 mb-3">
|
||||
<h3 class="font-bold mb-1">Blockscout API</h3>
|
||||
<p class="text-xs text-muted mb-1">
|
||||
Used to fetch token balances and transaction history.
|
||||
Change this if you run your own Blockscout instance.
|
||||
</p>
|
||||
<input
|
||||
type="text"
|
||||
id="settings-blockscout"
|
||||
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
|
||||
/>
|
||||
<button
|
||||
id="btn-settings-back"
|
||||
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
|
||||
id="btn-save-blockscout"
|
||||
class="border border-border px-2 py-1 mt-1 hover:bg-fg hover:text-bg cursor-pointer"
|
||||
>
|
||||
Back
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// AutistMask popup entry point.
|
||||
// Loads state, initializes views, triggers first render.
|
||||
|
||||
const { DEBUG } = require("../shared/wallet");
|
||||
const { DEBUG } = require("../shared/constants");
|
||||
const { state, saveState, loadState } = require("../shared/state");
|
||||
const { refreshPrices } = require("../shared/prices");
|
||||
const { refreshBalances } = require("../shared/balances");
|
||||
const { showView } = require("./views/helpers");
|
||||
const { $, showView } = require("./views/helpers");
|
||||
|
||||
const home = require("./views/home");
|
||||
const welcome = require("./views/welcome");
|
||||
@@ -23,13 +23,22 @@ function renderWalletList() {
|
||||
home.render(ctx);
|
||||
}
|
||||
|
||||
let refreshInFlight = false;
|
||||
|
||||
async function doRefreshAndRender() {
|
||||
await Promise.all([
|
||||
refreshPrices(),
|
||||
refreshBalances(state.wallets, state.trackedTokens, state.rpcUrl),
|
||||
]);
|
||||
await saveState();
|
||||
renderWalletList();
|
||||
if (refreshInFlight) return;
|
||||
refreshInFlight = true;
|
||||
try {
|
||||
await Promise.all([
|
||||
refreshPrices(),
|
||||
refreshBalances(state.wallets, state.rpcUrl, state.blockscoutUrl),
|
||||
]);
|
||||
state.lastBalanceRefresh = Date.now();
|
||||
await saveState();
|
||||
renderWalletList();
|
||||
} finally {
|
||||
refreshInFlight = false;
|
||||
}
|
||||
}
|
||||
|
||||
const ctx = {
|
||||
@@ -54,6 +63,12 @@ async function init() {
|
||||
|
||||
await loadState();
|
||||
|
||||
$("btn-settings").addEventListener("click", () => {
|
||||
$("settings-rpc").value = state.rpcUrl;
|
||||
$("settings-blockscout").value = state.blockscoutUrl;
|
||||
showView("settings");
|
||||
});
|
||||
|
||||
welcome.init(ctx);
|
||||
addWallet.init(ctx);
|
||||
importKey.init(ctx);
|
||||
@@ -72,6 +87,7 @@ async function init() {
|
||||
renderWalletList();
|
||||
showView("main");
|
||||
doRefreshAndRender();
|
||||
setInterval(doRefreshAndRender, 10000);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
--color-border: #000000;
|
||||
--color-border-light: #cccccc;
|
||||
--color-hover: #eeeeee;
|
||||
--color-well: #f5f5f5;
|
||||
}
|
||||
|
||||
body {
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
const { $, showError, hideError, showView } = require("./helpers");
|
||||
const { $, showView, showFlash } = require("./helpers");
|
||||
const { TOKENS } = require("../../shared/tokens");
|
||||
const { state, saveState } = require("../../shared/state");
|
||||
const {
|
||||
lookupTokenInfo,
|
||||
invalidateBalanceCache,
|
||||
refreshBalances,
|
||||
} = require("../../shared/balances");
|
||||
const { lookupTokenInfo } = require("../../shared/balances");
|
||||
const { isScamAddress } = require("../../shared/scamlist");
|
||||
const { log } = require("../../shared/log");
|
||||
|
||||
function show() {
|
||||
$("add-token-address").value = "";
|
||||
$("add-token-info").classList.add("hidden");
|
||||
hideError("add-token-error");
|
||||
const list = $("common-token-list");
|
||||
list.innerHTML = TOKENS.slice(0, 25)
|
||||
.map(
|
||||
@@ -30,8 +27,7 @@ function init(ctx) {
|
||||
$("btn-add-token-confirm").addEventListener("click", async () => {
|
||||
const contractAddr = $("add-token-address").value.trim();
|
||||
if (!contractAddr || !contractAddr.startsWith("0x")) {
|
||||
showError(
|
||||
"add-token-error",
|
||||
showFlash(
|
||||
"Please enter a valid contract address starting with 0x.",
|
||||
);
|
||||
return;
|
||||
@@ -40,18 +36,20 @@ function init(ctx) {
|
||||
(t) => t.address.toLowerCase() === contractAddr.toLowerCase(),
|
||||
);
|
||||
if (already) {
|
||||
showError(
|
||||
"add-token-error",
|
||||
already.symbol + " is already being tracked.",
|
||||
);
|
||||
showFlash(already.symbol + " is already being tracked.");
|
||||
return;
|
||||
}
|
||||
if (isScamAddress(contractAddr)) {
|
||||
showFlash("This address is on a known scam/fraud list.");
|
||||
return;
|
||||
}
|
||||
hideError("add-token-error");
|
||||
const infoEl = $("add-token-info");
|
||||
infoEl.textContent = "Looking up token...";
|
||||
infoEl.classList.remove("hidden");
|
||||
log.debugf("Looking up token contract", contractAddr);
|
||||
try {
|
||||
const info = await lookupTokenInfo(contractAddr, state.rpcUrl);
|
||||
log.infof("Adding token", info.symbol, contractAddr);
|
||||
state.trackedTokens.push({
|
||||
address: contractAddr,
|
||||
symbol: info.symbol,
|
||||
@@ -59,19 +57,12 @@ function init(ctx) {
|
||||
name: info.name,
|
||||
});
|
||||
await saveState();
|
||||
invalidateBalanceCache();
|
||||
await refreshBalances(
|
||||
state.wallets,
|
||||
state.trackedTokens,
|
||||
state.rpcUrl,
|
||||
);
|
||||
await saveState();
|
||||
ctx.doRefreshAndRender();
|
||||
ctx.showAddressDetail();
|
||||
} catch (e) {
|
||||
showError(
|
||||
"add-token-error",
|
||||
"Could not read token contract. Check the address.",
|
||||
);
|
||||
const detail = e.shortMessage || e.message || String(e);
|
||||
log.errorf("Token lookup failed for", contractAddr, detail);
|
||||
showFlash(detail);
|
||||
infoEl.classList.add("hidden");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { $, showError, hideError, showView } = require("./helpers");
|
||||
const { $, showView, showFlash } = require("./helpers");
|
||||
const {
|
||||
generateMnemonic,
|
||||
hdWalletFromMnemonic,
|
||||
@@ -6,13 +6,13 @@ const {
|
||||
} = require("../../shared/wallet");
|
||||
const { encryptWithPassword } = require("../../shared/vault");
|
||||
const { state, saveState } = require("../../shared/state");
|
||||
const { scanForAddresses } = require("../../shared/balances");
|
||||
|
||||
function show() {
|
||||
$("wallet-mnemonic").value = "";
|
||||
$("add-wallet-password").value = "";
|
||||
$("add-wallet-password-confirm").value = "";
|
||||
$("add-wallet-phrase-warning").classList.add("hidden");
|
||||
hideError("add-wallet-error");
|
||||
showView("add-wallet");
|
||||
}
|
||||
|
||||
@@ -25,16 +25,14 @@ function init(ctx) {
|
||||
$("btn-add-wallet-confirm").addEventListener("click", async () => {
|
||||
const mnemonic = $("wallet-mnemonic").value.trim();
|
||||
if (!mnemonic) {
|
||||
showError(
|
||||
"add-wallet-error",
|
||||
"Please enter a recovery phrase or press the die to generate one.",
|
||||
showFlash(
|
||||
"Enter a recovery phrase or press the die to generate one.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
const words = mnemonic.split(/\s+/);
|
||||
if (words.length !== 12 && words.length !== 24) {
|
||||
showError(
|
||||
"add-wallet-error",
|
||||
showFlash(
|
||||
"Recovery phrase must be 12 or 24 words. You entered " +
|
||||
words.length +
|
||||
".",
|
||||
@@ -42,34 +40,42 @@ function init(ctx) {
|
||||
return;
|
||||
}
|
||||
if (!isValidMnemonic(mnemonic)) {
|
||||
showError(
|
||||
"add-wallet-error",
|
||||
"Invalid recovery phrase. Please check for typos.",
|
||||
);
|
||||
showFlash("Invalid recovery phrase. Check for typos.");
|
||||
return;
|
||||
}
|
||||
const pw = $("add-wallet-password").value;
|
||||
const pw2 = $("add-wallet-password-confirm").value;
|
||||
if (!pw) {
|
||||
showError("add-wallet-error", "Please choose a password.");
|
||||
showFlash("Please choose a password.");
|
||||
return;
|
||||
}
|
||||
if (pw.length < 8) {
|
||||
showError(
|
||||
"add-wallet-error",
|
||||
"Password must be at least 8 characters.",
|
||||
);
|
||||
showFlash("Password must be at least 8 characters.");
|
||||
return;
|
||||
}
|
||||
if (pw !== pw2) {
|
||||
showError("add-wallet-error", "Passwords do not match.");
|
||||
showFlash("Passwords do not match.");
|
||||
return;
|
||||
}
|
||||
hideError("add-wallet-error");
|
||||
const encrypted = await encryptWithPassword(mnemonic, pw);
|
||||
const { xpub, firstAddress } = hdWalletFromMnemonic(mnemonic);
|
||||
const duplicate = state.wallets.find(
|
||||
(w) =>
|
||||
w.type === "hd" &&
|
||||
w.addresses[0] &&
|
||||
w.addresses[0].address.toLowerCase() ===
|
||||
firstAddress.toLowerCase(),
|
||||
);
|
||||
if (duplicate) {
|
||||
showFlash(
|
||||
"This recovery phrase is already added (" +
|
||||
duplicate.name +
|
||||
").",
|
||||
);
|
||||
return;
|
||||
}
|
||||
const encrypted = await encryptWithPassword(mnemonic, pw);
|
||||
const walletNum = state.wallets.length + 1;
|
||||
state.wallets.push({
|
||||
const wallet = {
|
||||
type: "hd",
|
||||
name: "Wallet " + walletNum,
|
||||
xpub: xpub,
|
||||
@@ -78,11 +84,28 @@ function init(ctx) {
|
||||
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();
|
||||
}
|
||||
|
||||
ctx.doRefreshAndRender();
|
||||
});
|
||||
|
||||
$("btn-add-wallet-back").addEventListener("click", () => {
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
const { $, showView } = require("./helpers");
|
||||
const { $, showView, showFlash, balanceLinesForAddress } = require("./helpers");
|
||||
const { state, currentAddress } = require("../../shared/state");
|
||||
const { formatUsd, getAddressValueUsd } = require("../../shared/prices");
|
||||
const { fetchRecentTransactions } = require("../../shared/transactions");
|
||||
const { updateSendBalance } = require("./send");
|
||||
const { log } = require("../../shared/log");
|
||||
const QRCode = require("qrcode");
|
||||
|
||||
function show() {
|
||||
const wallet = state.wallets[state.selectedWallet];
|
||||
const addr = wallet.addresses[state.selectedAddress];
|
||||
$("address-title").textContent = wallet.name;
|
||||
const wi = state.selectedWallet;
|
||||
const ai = state.selectedAddress;
|
||||
$("address-title").textContent =
|
||||
wallet.name + " \u2014 Address " + (wi + 1) + "." + (ai + 1);
|
||||
$("address-full").textContent = addr.address;
|
||||
$("address-copied-msg").textContent = "";
|
||||
$("address-eth-balance").textContent = addr.balance;
|
||||
$("address-usd-value").textContent = formatUsd(getAddressValueUsd(addr));
|
||||
$("address-usd-total").textContent = formatUsd(getAddressValueUsd(addr));
|
||||
const ensEl = $("address-ens");
|
||||
if (addr.ensName) {
|
||||
ensEl.textContent = addr.ensName;
|
||||
@@ -18,28 +22,71 @@ function show() {
|
||||
} else {
|
||||
ensEl.classList.add("hidden");
|
||||
}
|
||||
renderTokenList(addr);
|
||||
$("address-balances").innerHTML = balanceLinesForAddress(addr);
|
||||
renderSendTokenSelect(addr);
|
||||
$("tx-list").innerHTML =
|
||||
'<div class="text-muted text-xs py-1">Loading...</div>';
|
||||
showView("address");
|
||||
loadTransactions(addr.address);
|
||||
}
|
||||
|
||||
function renderTokenList(addr) {
|
||||
const list = $("token-list");
|
||||
const balances = addr.tokenBalances || [];
|
||||
if (balances.length === 0 && state.trackedTokens.length === 0) {
|
||||
function formatDate(timestamp) {
|
||||
const d = new Date(timestamp * 1000);
|
||||
const pad = (n) => String(n).padStart(2, "0");
|
||||
return (
|
||||
d.getFullYear() +
|
||||
"-" +
|
||||
pad(d.getMonth() + 1) +
|
||||
"-" +
|
||||
pad(d.getDate()) +
|
||||
" " +
|
||||
pad(d.getHours()) +
|
||||
":" +
|
||||
pad(d.getMinutes())
|
||||
);
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
const div = document.createElement("div");
|
||||
div.textContent = s;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
async function loadTransactions(address) {
|
||||
try {
|
||||
const txs = await fetchRecentTransactions(address, state.blockscoutUrl);
|
||||
renderTransactions(txs, address);
|
||||
} catch (e) {
|
||||
log.errorf("loadTransactions failed:", e.message);
|
||||
$("tx-list").innerHTML =
|
||||
'<div class="text-muted text-xs py-1">Failed to load transactions.</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderTransactions(txs, address) {
|
||||
const list = $("tx-list");
|
||||
if (txs.length === 0) {
|
||||
list.innerHTML =
|
||||
'<div class="text-muted text-xs py-1">No tokens added yet. Use "+ Add" to track a token.</div>';
|
||||
'<div class="text-muted text-xs py-1">No transactions found.</div>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = balances
|
||||
.map(
|
||||
(t) =>
|
||||
`<div class="py-1 border-b border-border-light flex justify-between">` +
|
||||
`<span>${t.symbol}</span>` +
|
||||
`<span>${t.balance || "0"}</span>` +
|
||||
`</div>`,
|
||||
)
|
||||
.join("");
|
||||
const addrLower = address.toLowerCase();
|
||||
let html = "";
|
||||
for (const tx of txs) {
|
||||
const arrow = tx.direction === "sent" ? "\u2192" : "\u2190";
|
||||
const counterparty = tx.direction === "sent" ? tx.to : tx.from;
|
||||
const label = tx.direction === "sent" ? "to" : "from";
|
||||
const errorClass = tx.isError ? ' style="opacity:0.5"' : "";
|
||||
const errorTag = tx.isError
|
||||
? ' <span class="text-muted">[failed]</span>'
|
||||
: "";
|
||||
html += `<div class="py-1 border-b border-border-light text-xs"${errorClass}>`;
|
||||
html += `<div>${formatDate(tx.timestamp)} ${arrow} ${escapeHtml(tx.value)} ${escapeHtml(tx.symbol)}${errorTag}</div>`;
|
||||
html += `<div class="text-muted break-all">${label}: ${escapeHtml(counterparty)}</div>`;
|
||||
html += `<div class="break-all"><a href="https://etherscan.io/tx/${escapeHtml(tx.hash)}" target="_blank" class="underline decoration-dashed">${escapeHtml(tx.hash)}</a></div>`;
|
||||
html += `</div>`;
|
||||
}
|
||||
list.innerHTML = html;
|
||||
}
|
||||
|
||||
function renderSendTokenSelect(addr) {
|
||||
@@ -58,10 +105,7 @@ function init(ctx) {
|
||||
const addr = $("address-full").textContent;
|
||||
if (addr) {
|
||||
navigator.clipboard.writeText(addr);
|
||||
$("address-copied-msg").textContent = "Copied!";
|
||||
setTimeout(() => {
|
||||
$("address-copied-msg").textContent = "";
|
||||
}, 2000);
|
||||
showFlash("Copied!");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -71,11 +115,17 @@ function init(ctx) {
|
||||
});
|
||||
|
||||
$("btn-send").addEventListener("click", () => {
|
||||
const addr =
|
||||
state.wallets[state.selectedWallet].addresses[
|
||||
state.selectedAddress
|
||||
];
|
||||
if (!addr.balance || parseFloat(addr.balance) === 0) {
|
||||
showFlash("Cannot send \u2014 zero balance.");
|
||||
return;
|
||||
}
|
||||
$("send-to").value = "";
|
||||
$("send-amount").value = "";
|
||||
$("send-password").value = "";
|
||||
$("send-fee-estimate").classList.add("hidden");
|
||||
$("send-status").classList.add("hidden");
|
||||
updateSendBalance();
|
||||
showView("send");
|
||||
});
|
||||
|
||||
|
||||
@@ -8,10 +8,7 @@ const { state } = require("../../shared/state");
|
||||
const { getSignerForAddress } = require("../../shared/wallet");
|
||||
const { decryptWithPassword } = require("../../shared/vault");
|
||||
const { formatUsd, getPrice } = require("../../shared/prices");
|
||||
const {
|
||||
getProvider,
|
||||
invalidateBalanceCache,
|
||||
} = require("../../shared/balances");
|
||||
const { getProvider } = require("../../shared/balances");
|
||||
const { isScamAddress } = require("../../shared/scamlist");
|
||||
|
||||
let pendingTx = null;
|
||||
@@ -163,12 +160,19 @@ function init(ctx) {
|
||||
});
|
||||
statusEl.textContent = "Sent. Waiting for confirmation...";
|
||||
const receipt = await tx.wait();
|
||||
statusEl.textContent =
|
||||
"Confirmed in block " +
|
||||
receipt.blockNumber +
|
||||
". Tx: " +
|
||||
receipt.hash;
|
||||
invalidateBalanceCache();
|
||||
statusEl.innerHTML = "";
|
||||
statusEl.appendChild(
|
||||
document.createTextNode(
|
||||
"Confirmed in block " + receipt.blockNumber + ". Tx: ",
|
||||
),
|
||||
);
|
||||
const link = document.createElement("a");
|
||||
link.href = "https://etherscan.io/tx/" + receipt.hash;
|
||||
link.target = "_blank";
|
||||
link.rel = "noopener";
|
||||
link.className = "underline decoration-dashed break-all";
|
||||
link.textContent = receipt.hash;
|
||||
statusEl.appendChild(link);
|
||||
ctx.doRefreshAndRender();
|
||||
} catch (e) {
|
||||
statusEl.textContent = "Failed: " + (e.shortMessage || e.message);
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
// Shared DOM helpers used by all views.
|
||||
|
||||
const { DEBUG } = require("../../shared/wallet");
|
||||
const { DEBUG } = require("../../shared/constants");
|
||||
const {
|
||||
formatUsd,
|
||||
getPrice,
|
||||
getAddressValueUsd,
|
||||
} = require("../../shared/prices");
|
||||
|
||||
const VIEWS = [
|
||||
"welcome",
|
||||
@@ -37,6 +42,7 @@ function showView(name) {
|
||||
el.classList.toggle("hidden", v !== name);
|
||||
}
|
||||
}
|
||||
clearFlash();
|
||||
if (DEBUG) {
|
||||
const banner = document.getElementById("debug-banner");
|
||||
if (banner) {
|
||||
@@ -45,4 +51,58 @@ function showView(name) {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { $, showError, hideError, showView };
|
||||
let flashTimer = null;
|
||||
|
||||
function clearFlash() {
|
||||
if (flashTimer) {
|
||||
clearTimeout(flashTimer);
|
||||
flashTimer = null;
|
||||
}
|
||||
$("flash-msg").textContent = "";
|
||||
}
|
||||
|
||||
function showFlash(msg, duration = 2000) {
|
||||
clearFlash();
|
||||
$("flash-msg").textContent = msg;
|
||||
flashTimer = setTimeout(() => {
|
||||
$("flash-msg").textContent = "";
|
||||
flashTimer = null;
|
||||
}, duration);
|
||||
}
|
||||
|
||||
function balanceLine(symbol, amount, price) {
|
||||
const qty = amount.toFixed(4);
|
||||
const usd = price ? formatUsd(amount * price) : "";
|
||||
return (
|
||||
`<div class="flex text-xs">` +
|
||||
`<span class="flex justify-between shrink-0" style="width:42ch">` +
|
||||
`<span>${symbol}</span>` +
|
||||
`<span>${qty}</span>` +
|
||||
`</span>` +
|
||||
`<span class="text-right text-muted flex-1">${usd}</span>` +
|
||||
`</div>`
|
||||
);
|
||||
}
|
||||
|
||||
function balanceLinesForAddress(addr) {
|
||||
let html = balanceLine(
|
||||
"ETH",
|
||||
parseFloat(addr.balance || "0"),
|
||||
getPrice("ETH"),
|
||||
);
|
||||
for (const t of addr.tokenBalances || []) {
|
||||
const bal = parseFloat(t.balance || "0");
|
||||
if (bal === 0) continue;
|
||||
html += balanceLine(t.symbol, bal, getPrice(t.symbol));
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
$,
|
||||
showError,
|
||||
hideError,
|
||||
showView,
|
||||
showFlash,
|
||||
balanceLinesForAddress,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { $, showView } = require("./helpers");
|
||||
const { $, showView, showFlash, balanceLinesForAddress } = require("./helpers");
|
||||
const { state, saveState } = require("../../shared/state");
|
||||
const { deriveAddressFromXpub } = require("../../shared/wallet");
|
||||
const {
|
||||
@@ -33,15 +33,17 @@ function render(ctx) {
|
||||
html += `</div>`;
|
||||
|
||||
wallet.addresses.forEach((addr, ai) => {
|
||||
html += `<div class="address-row py-1 border-b border-border-light cursor-pointer hover:bg-hover px-1" data-wallet="${wi}" data-address="${ai}">`;
|
||||
html += `<div class="address-row py-1 border-b border-border-light cursor-pointer hover:bg-hover" data-wallet="${wi}" data-address="${ai}">`;
|
||||
html += `<div class="text-xs font-bold">Address ${wi + 1}.${ai + 1}</div>`;
|
||||
if (addr.ensName) {
|
||||
html += `<div class="text-xs font-bold">${addr.ensName}</div>`;
|
||||
}
|
||||
html += `<div class="text-xs break-all">${addr.address}</div>`;
|
||||
html += `<div class="flex justify-between items-center">`;
|
||||
html += `<span class="text-xs">${addr.balance} ETH</span>`;
|
||||
html += `<span class="text-xs text-muted">${formatUsd(getAddressValueUsd(addr))}</span>`;
|
||||
const addrUsd = formatUsd(getAddressValueUsd(addr));
|
||||
html += `<div class="flex text-xs">`;
|
||||
html += `<span class="shrink-0" style="width:42ch">${addr.address}</span>`;
|
||||
html += `<span class="text-right text-muted flex-1">${addrUsd}</span>`;
|
||||
html += `</div>`;
|
||||
html += balanceLinesForAddress(addr);
|
||||
html += `</div>`;
|
||||
});
|
||||
|
||||
@@ -62,27 +64,25 @@ function render(ctx) {
|
||||
e.stopPropagation();
|
||||
const wi = parseInt(btn.dataset.wallet, 10);
|
||||
const wallet = state.wallets[wi];
|
||||
const newAddr = deriveAddressFromXpub(
|
||||
wallet.xpub,
|
||||
wallet.nextIndex,
|
||||
);
|
||||
wallet.addresses.push({
|
||||
address: deriveAddressFromXpub(wallet.xpub, wallet.nextIndex),
|
||||
address: newAddr,
|
||||
balance: "0.0000",
|
||||
tokenBalances: [],
|
||||
});
|
||||
wallet.nextIndex++;
|
||||
await saveState();
|
||||
render(ctx);
|
||||
ctx.doRefreshAndRender();
|
||||
});
|
||||
});
|
||||
|
||||
renderTotalValue();
|
||||
}
|
||||
|
||||
function init(ctx) {
|
||||
$("btn-settings").addEventListener("click", () => {
|
||||
$("settings-rpc").value = state.rpcUrl;
|
||||
showView("settings");
|
||||
});
|
||||
|
||||
$("btn-main-add-wallet").addEventListener("click", ctx.showAddWalletView);
|
||||
}
|
||||
function init(ctx) {}
|
||||
|
||||
module.exports = { init, render };
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { $, showError, hideError, showView } = require("./helpers");
|
||||
const { $, showView, showFlash } = require("./helpers");
|
||||
const { addressFromPrivateKey } = require("../../shared/wallet");
|
||||
const { encryptWithPassword } = require("../../shared/vault");
|
||||
const { state, saveState } = require("../../shared/state");
|
||||
@@ -7,7 +7,6 @@ function show() {
|
||||
$("import-private-key").value = "";
|
||||
$("import-key-password").value = "";
|
||||
$("import-key-password-confirm").value = "";
|
||||
hideError("import-key-error");
|
||||
showView("import-key");
|
||||
}
|
||||
|
||||
@@ -15,34 +14,30 @@ function init(ctx) {
|
||||
$("btn-import-key-confirm").addEventListener("click", async () => {
|
||||
const key = $("import-private-key").value.trim();
|
||||
if (!key) {
|
||||
showError("import-key-error", "Please enter your private key.");
|
||||
showFlash("Please enter your private key.");
|
||||
return;
|
||||
}
|
||||
let addr;
|
||||
try {
|
||||
addr = addressFromPrivateKey(key);
|
||||
} catch (e) {
|
||||
showError("import-key-error", "Invalid private key.");
|
||||
showFlash("Invalid private key.");
|
||||
return;
|
||||
}
|
||||
const pw = $("import-key-password").value;
|
||||
const pw2 = $("import-key-password-confirm").value;
|
||||
if (!pw) {
|
||||
showError("import-key-error", "Please choose a password.");
|
||||
showFlash("Please choose a password.");
|
||||
return;
|
||||
}
|
||||
if (pw.length < 8) {
|
||||
showError(
|
||||
"import-key-error",
|
||||
"Password must be at least 8 characters.",
|
||||
);
|
||||
showFlash("Password must be at least 8 characters.");
|
||||
return;
|
||||
}
|
||||
if (pw !== pw2) {
|
||||
showError("import-key-error", "Passwords do not match.");
|
||||
showFlash("Passwords do not match.");
|
||||
return;
|
||||
}
|
||||
hideError("import-key-error");
|
||||
const encrypted = await encryptWithPassword(key, pw);
|
||||
const walletNum = state.wallets.length + 1;
|
||||
state.wallets.push({
|
||||
@@ -57,6 +52,8 @@ function init(ctx) {
|
||||
await saveState();
|
||||
ctx.renderWalletList();
|
||||
showView("main");
|
||||
|
||||
ctx.doRefreshAndRender();
|
||||
});
|
||||
|
||||
$("btn-import-key-back").addEventListener("click", () => {
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
const { $ } = require("./helpers");
|
||||
const { $, showFlash } = require("./helpers");
|
||||
|
||||
function init(ctx) {
|
||||
$("btn-receive-copy").addEventListener("click", () => {
|
||||
const addr = $("receive-address").textContent;
|
||||
if (addr) navigator.clipboard.writeText(addr);
|
||||
if (addr) {
|
||||
navigator.clipboard.writeText(addr);
|
||||
showFlash("Copied!");
|
||||
}
|
||||
});
|
||||
|
||||
$("btn-receive-back").addEventListener("click", ctx.showAddressDetail);
|
||||
|
||||
@@ -1,22 +1,41 @@
|
||||
// Send view: collect To, Amount, Token. Then go to confirmation.
|
||||
|
||||
const { $, showError, hideError } = require("./helpers");
|
||||
const { $, showFlash } = require("./helpers");
|
||||
const { state, currentAddress } = require("../../shared/state");
|
||||
const { getProvider } = require("../../shared/balances");
|
||||
|
||||
function updateSendBalance() {
|
||||
const addr = currentAddress();
|
||||
if (!addr) return;
|
||||
const token = $("send-token").value;
|
||||
if (token === "ETH") {
|
||||
$("send-balance").textContent =
|
||||
"Current balance: " + (addr.balance || "0") + " ETH";
|
||||
} else {
|
||||
const tb = (addr.tokenBalances || []).find(
|
||||
(t) => t.address.toLowerCase() === token.toLowerCase(),
|
||||
);
|
||||
const symbol = tb ? tb.symbol : "?";
|
||||
const bal = tb ? tb.balance || "0" : "0";
|
||||
$("send-balance").textContent =
|
||||
"Current balance: " + bal + " " + symbol;
|
||||
}
|
||||
}
|
||||
|
||||
function init(ctx) {
|
||||
$("send-token").addEventListener("change", updateSendBalance);
|
||||
|
||||
$("btn-send-review").addEventListener("click", async () => {
|
||||
const to = $("send-to").value.trim();
|
||||
const amount = $("send-amount").value.trim();
|
||||
if (!to) {
|
||||
showError("send-error", "Please enter a recipient address.");
|
||||
showFlash("Please enter a recipient address.");
|
||||
return;
|
||||
}
|
||||
if (!amount || isNaN(parseFloat(amount)) || parseFloat(amount) <= 0) {
|
||||
showError("send-error", "Please enter a valid amount.");
|
||||
showFlash("Please enter a valid amount.");
|
||||
return;
|
||||
}
|
||||
hideError("send-error");
|
||||
|
||||
// Resolve ENS if needed
|
||||
let resolvedTo = to;
|
||||
@@ -26,13 +45,13 @@ function init(ctx) {
|
||||
const provider = getProvider(state.rpcUrl);
|
||||
const resolved = await provider.resolveName(to);
|
||||
if (!resolved) {
|
||||
showError("send-error", "Could not resolve " + to);
|
||||
showFlash("Could not resolve " + to);
|
||||
return;
|
||||
}
|
||||
resolvedTo = resolved;
|
||||
ensName = to;
|
||||
} catch (e) {
|
||||
showError("send-error", "Failed to resolve ENS name.");
|
||||
showFlash("Failed to resolve ENS name.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -53,4 +72,4 @@ function init(ctx) {
|
||||
$("btn-send-back").addEventListener("click", ctx.showAddressDetail);
|
||||
}
|
||||
|
||||
module.exports = { init };
|
||||
module.exports = { init, updateSendBalance };
|
||||
|
||||
@@ -1,12 +1,76 @@
|
||||
const { $, showView } = require("./helpers");
|
||||
const { $, showView, showFlash } = require("./helpers");
|
||||
const { state, saveState } = require("../../shared/state");
|
||||
const { ETHEREUM_MAINNET_CHAIN_ID } = require("../../shared/constants");
|
||||
const { log } = require("../../shared/log");
|
||||
|
||||
function init(ctx) {
|
||||
$("btn-save-rpc").addEventListener("click", async () => {
|
||||
state.rpcUrl = $("settings-rpc").value.trim();
|
||||
const url = $("settings-rpc").value.trim();
|
||||
if (!url) {
|
||||
showFlash("Please enter an RPC URL.");
|
||||
return;
|
||||
}
|
||||
showFlash("Testing endpoint...");
|
||||
try {
|
||||
const resp = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
jsonrpc: "2.0",
|
||||
id: 1,
|
||||
method: "eth_chainId",
|
||||
params: [],
|
||||
}),
|
||||
});
|
||||
const json = await resp.json();
|
||||
if (json.error) {
|
||||
log.errorf("RPC validation error:", json.error);
|
||||
showFlash("Endpoint returned error: " + json.error.message);
|
||||
return;
|
||||
}
|
||||
if (json.result !== ETHEREUM_MAINNET_CHAIN_ID) {
|
||||
showFlash(
|
||||
"Wrong network (expected mainnet, got chain " +
|
||||
json.result +
|
||||
").",
|
||||
);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
log.errorf("RPC validation fetch failed:", e.message);
|
||||
showFlash("Could not reach endpoint.");
|
||||
return;
|
||||
}
|
||||
state.rpcUrl = url;
|
||||
await saveState();
|
||||
showFlash("Saved.");
|
||||
});
|
||||
|
||||
$("btn-save-blockscout").addEventListener("click", async () => {
|
||||
const url = $("settings-blockscout").value.trim();
|
||||
if (!url) {
|
||||
showFlash("Please enter a Blockscout API URL.");
|
||||
return;
|
||||
}
|
||||
showFlash("Testing endpoint...");
|
||||
try {
|
||||
const resp = await fetch(url + "/stats");
|
||||
if (!resp.ok) {
|
||||
showFlash("Endpoint returned HTTP " + resp.status + ".");
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
log.errorf("Blockscout validation failed:", e.message);
|
||||
showFlash("Could not reach endpoint.");
|
||||
return;
|
||||
}
|
||||
state.blockscoutUrl = url;
|
||||
await saveState();
|
||||
showFlash("Saved.");
|
||||
});
|
||||
|
||||
$("btn-main-add-wallet").addEventListener("click", ctx.showAddWalletView);
|
||||
|
||||
$("btn-settings-back").addEventListener("click", () => {
|
||||
ctx.renderWalletList();
|
||||
showView("main");
|
||||
|
||||
Reference in New Issue
Block a user