From 03199c773bdf4a437dfbf854b38765aea1e925b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=CE=BA=CE=B1=CF=83=CF=83=CE=AC=CE=BD=CE=B4=CF=81=CE=B1=2Ee?= =?UTF-8?q?th?= <0xDADA@protonmail.com> Date: Tue, 25 Aug 2026 07:12:59 -0400 Subject: [PATCH] fix: create-wallet tries public rpc endpoints --- src/commands/createWallet.ts | 7 ++-- src/utils/rpc.ts | 48 +++++++++++++++++++------- tests/rpc.test.ts | 67 ++++++++++++++++++++++++++++++++++++ 3 files changed, 106 insertions(+), 16 deletions(-) create mode 100644 tests/rpc.test.ts diff --git a/src/commands/createWallet.ts b/src/commands/createWallet.ts index 7602687..1cd8618 100644 --- a/src/commands/createWallet.ts +++ b/src/commands/createWallet.ts @@ -13,6 +13,7 @@ import { import { parseStealthStartBlock } from "../lib/stealth/scan.js"; import { DEFAULT_DATA_DIR, + resolveOptionalRpcUrl, resolveRpcUrl, } from "../utils/rpc"; import { @@ -95,7 +96,7 @@ export function registerCreateWalletCommand(program: Command): void { "Password to encrypt this wallet (required with --non-interactive; else prompted)" ) .option("--mnemonic ", "Mnemonic phrase (required with --non-interactive --import)") - .option("--rpc-url ", "RPC URL (or set RPC_URL; default http://localhost:8545). New wallets fall back to public RPCs if the preferred endpoint fails") + .option("--rpc-url ", "RPC URL (or set RPC_URL). Optional for new wallets: a public RPC is used to record the current block if unset. Required with --import") .option("--testnet", "Use testnet chain ID (11155111) instead of mainnet (1)") .option( "--stealth-start-block ", @@ -187,10 +188,10 @@ export function registerCreateWalletCommand(program: Command): void { } } - // Prefer --rpc-url / RPC_URL / localhost default; create-wallet falls back to public RPCs on failure. + // New wallets: only use an RPC the user set; otherwise public endpoints (not localhost). const preferredRpcUrl = opts.import ? importRpcUrl - : resolveRpcUrl(opts.rpcUrl); + : resolveOptionalRpcUrl(opts.rpcUrl); try { const created = await createWalletOnDisk({ diff --git a/src/utils/rpc.ts b/src/utils/rpc.ts index 57b1ab7..28f6ec6 100644 --- a/src/utils/rpc.ts +++ b/src/utils/rpc.ts @@ -129,14 +129,24 @@ export async function makePublicClient(rpcUrl: string): Promise = { mainnet: [ - "https://cloudflare-eth.com", - "https://ethereum.publicnode.com", - "https://rpc.ankr.com/eth", + "https://eth.drpc.org", + "https://1rpc.io/eth", + "https://ethereum.public.blockpi.network/v1/rpc/public", + "https://gateway.tenderly.co/public/mainnet", ], sepolia: [ - "https://ethereum-sepolia-rpc.publicnode.com", - "https://rpc.sepolia.org", - "https://rpc2.sepolia.org", + "https://1rpc.io/sepolia", + "https://gateway.tenderly.co/public/sepolia", + "https://sepolia.gateway.tenderly.co", ], }; @@ -185,6 +196,20 @@ function publicRpcCandidates(testnet: boolean): readonly string[] { return testnet ? PUBLIC_RPC_URLS.sepolia : PUBLIC_RPC_URLS.mainnet; } +/** + * RPCs to try for a one-shot `eth_blockNumber` (create-wallet stealth start block). + * Localhost is included only when the caller passed it as `rpcUrl`. + */ +export function currentBlockRpcCandidates(opts: { + testnet: boolean; + rpcUrl?: string; +}): string[] { + const preferred = opts.rpcUrl?.trim(); + const publicUrls = publicRpcCandidates(opts.testnet); + if (!preferred) return [...publicUrls]; + return [preferred, ...publicUrls.filter((u) => u !== preferred)]; +} + /** * Current block height for mainnet or Sepolia. * Prefers `rpcUrl` when provided (must match the network); otherwise tries public RPCs. @@ -195,10 +220,7 @@ export async function fetchCurrentBlockNumber(opts: { rpcUrl?: string; }): Promise<{ blockNumber: bigint; rpcUrlUsed: string }> { const expectedChainId = opts.testnet ? 11155111n : 1n; - const preferred = opts.rpcUrl?.trim(); - const candidates = preferred - ? [preferred, ...publicRpcCandidates(opts.testnet).filter((u) => u !== preferred)] - : [...publicRpcCandidates(opts.testnet)]; + const candidates = currentBlockRpcCandidates(opts); const errors: string[] = []; for (const url of candidates) { diff --git a/tests/rpc.test.ts b/tests/rpc.test.ts new file mode 100644 index 0000000..aeced94 --- /dev/null +++ b/tests/rpc.test.ts @@ -0,0 +1,67 @@ +import assert from "node:assert/strict"; +import { afterEach, describe, it } from "node:test"; + +import { + DEFAULT_RPC_URL, + currentBlockRpcCandidates, + resolveOptionalRpcUrl, + resolveRpcUrl, +} from "../src/utils/rpc.js"; + +describe("resolveOptionalRpcUrl", () => { + const prev = process.env.RPC_URL; + afterEach(() => { + if (prev === undefined) delete process.env.RPC_URL; + else process.env.RPC_URL = prev; + }); + + it("returns undefined when neither flag nor env is set", () => { + delete process.env.RPC_URL; + assert.equal(resolveOptionalRpcUrl(undefined), undefined); + assert.equal(resolveOptionalRpcUrl(" "), undefined); + }); + + it("prefers --rpc-url over RPC_URL", () => { + process.env.RPC_URL = "https://from-env.example"; + assert.equal(resolveOptionalRpcUrl("https://from-flag.example"), "https://from-flag.example"); + }); + + it("uses RPC_URL when the flag is omitted", () => { + process.env.RPC_URL = "https://from-env.example"; + assert.equal(resolveOptionalRpcUrl(undefined), "https://from-env.example"); + }); +}); + +describe("resolveRpcUrl", () => { + const prev = process.env.RPC_URL; + afterEach(() => { + if (prev === undefined) delete process.env.RPC_URL; + else process.env.RPC_URL = prev; + }); + + it("falls back to localhost only when nothing is configured", () => { + delete process.env.RPC_URL; + assert.equal(resolveRpcUrl(undefined), DEFAULT_RPC_URL); + }); +}); + +describe("currentBlockRpcCandidates", () => { + it("does not default to localhost when no rpcUrl is provided", () => { + const urls = currentBlockRpcCandidates({ testnet: false }); + assert.ok(urls.length >= 1); + assert.equal(urls.some((u) => u.includes("localhost")), false); + assert.equal(urls.some((u) => u.includes("ankr.com")), false); + }); + + it("puts an explicit rpcUrl first and still tries public fallbacks", () => { + const preferred = "http://localhost:8545"; + const urls = currentBlockRpcCandidates({ testnet: false, rpcUrl: preferred }); + assert.equal(urls[0], preferred); + assert.ok(urls.length > 1); + }); + + it("uses Sepolia public RPCs for testnet", () => { + const urls = currentBlockRpcCandidates({ testnet: true }); + assert.ok(urls.every((u) => /sepolia/i.test(u))); + }); +});