diff --git a/CHANGELOG.md b/CHANGELOG.md index 466fbb0f8..9e8301534 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- fixed: (EVM) Honor the `evmScanApiKey` init option on `api.etherscan.io`. Since the Etherscan V2 upgrade, key lookup for that host required the deprecated `etherscanApiKey` and threw before considering `evmScanApiKey`, so a build configured with only the supported option sent no key. Etherscan V2 rejects keyless requests, leaving the EvmScan adapter (the sole transaction-history source on 15 EVM networks) unable to return transactions, which pinned wallet sync at 50% forever. An empty string or empty array now counts as unconfigured so it falls through to the deprecated per-network keys. + ## 4.86.2 (2026-07-17) - changed: (Base) Use dynamic `eth_feeHistory` fee estimation instead of a hardcoded 2 gwei priority floor. The previous static floor caused sends to overpay by ~200x during normal network conditions. The dynamic algorithm tracks real-time percentile-based priority fees with a 2x base-fee buffer, so fees rise naturally during congestion spikes and drop to market rate otherwise. The static `minPriorityFee` fallback (used only when `eth_feeHistory` fails) is lowered from 2 gwei to 0.1 gwei, a conservative 20x reduction that covered the observed p99 priority fee in a 1,024-block Base sample. diff --git a/src/ethereum/fees/feeProviders.ts b/src/ethereum/fees/feeProviders.ts index df1fbdceb..97f065c65 100644 --- a/src/ethereum/fees/feeProviders.ts +++ b/src/ethereum/fees/feeProviders.ts @@ -339,6 +339,17 @@ export const fetchFeesFromInfoServer = async ( return asEthereumFees(json) } +/** + * An init option counts as missing when it is absent, an empty string, or an + * empty array. The GUI defaults `evmScanApiKey` to `[]`, so a plain null check + * would treat an unconfigured build as configured and send a keyless request. + */ +const hasApiKey = (apiKey: unknown): boolean => { + if (apiKey == null) return false + if (Array.isArray(apiKey)) return apiKey.length > 0 + return apiKey !== '' +} + // Get API key for Etherscan v2 API or network-specific scan APIs export const getEvmScanApiKey = ( initOptions: JsonObject, @@ -351,15 +362,23 @@ export const getEvmScanApiKey = ( const { currencyCode } = info - // If we have a server URL and it's etherscan.io, use the Ethereum API key + // `evmScanApiKey` is the supported option and is valid for every Etherscan v2 + // network, etherscan.io included. It must be checked first, otherwise a build + // configured with only this option gets no key at all, Etherscan v2 rejects + // the keyless request, and transaction history never syncs. + if (hasApiKey(evmScanApiKey)) return evmScanApiKey + + // Etherscan v2 rejects keyless requests with 'Missing/Invalid API Key', so a + // build with no usable key cannot query this server at all: if (serverUrl.includes('etherscan.io')) { - if (etherscanApiKey == null) - throw new Error(`Missing etherscanApiKey for etherscan.io`) + if (!hasApiKey(etherscanApiKey)) + throw new Error(`Missing evmScanApiKey for etherscan.io`) + log.warn( + "INIT OPTION 'etherscanApiKey' IS DEPRECATED. USE 'evmScanApiKey' INSTEAD" + ) return etherscanApiKey } - if (evmScanApiKey != null) return evmScanApiKey - // For networks that don't support Etherscan v2, fall back to network-specific keys if (currencyCode === 'ETH' && etherscanApiKey != null) { log.warn( diff --git a/test/ethereum/fees/getEvmScanApiKey.test.ts b/test/ethereum/fees/getEvmScanApiKey.test.ts new file mode 100644 index 000000000..2b307d965 --- /dev/null +++ b/test/ethereum/fees/getEvmScanApiKey.test.ts @@ -0,0 +1,69 @@ +import { expect } from 'chai' +import { EdgeLog } from 'edge-core-js/types' +import { describe, it } from 'mocha' + +import { getEvmScanApiKey } from '../../../src/ethereum/fees/feeProviders' +import { currencyInfo as info } from '../../../src/ethereum/info/ethereumInfo' + +describe('getEvmScanApiKey', function () { + const etherscanServer = 'https://api.etherscan.io' + const otherScanServer = 'https://api.routescan.io' + + const warnings: string[] = [] + const log = Object.assign(() => {}, { + warn: (...args: any[]) => { + warnings.push(String(args[0])) + }, + error: () => {}, + crash: () => {} + }) as unknown as EdgeLog + + it('uses evmScanApiKey for etherscan.io', function () { + // A build configured with only the supported option must work. Preferring + // the deprecated etherscanApiKey here left transaction history unsynced. + const out = getEvmScanApiKey( + { evmScanApiKey: ['key1'] }, + info, + log, + etherscanServer + ) + expect(out).deep.equals(['key1']) + }) + + it('uses evmScanApiKey for non-etherscan.io servers', function () { + const out = getEvmScanApiKey( + { evmScanApiKey: ['key1'] }, + info, + log, + otherScanServer + ) + expect(out).deep.equals(['key1']) + }) + + it('falls back to the deprecated etherscanApiKey when evmScanApiKey is an empty array', function () { + // The GUI defaults evmScanApiKey to [], so an empty array means unconfigured: + const out = getEvmScanApiKey( + { evmScanApiKey: [], etherscanApiKey: ['legacy'] }, + info, + log, + etherscanServer + ) + expect(out).deep.equals(['legacy']) + }) + + it('falls back to the deprecated etherscanApiKey when evmScanApiKey is absent', function () { + const out = getEvmScanApiKey( + { etherscanApiKey: ['legacy'] }, + info, + log, + etherscanServer + ) + expect(out).deep.equals(['legacy']) + }) + + it('throws for etherscan.io when no usable key is configured', function () { + expect(() => + getEvmScanApiKey({ evmScanApiKey: [] }, info, log, etherscanServer) + ).to.throw('Missing evmScanApiKey for etherscan.io') + }) +})