Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
29 changes: 24 additions & 5 deletions src/ethereum/fees/feeProviders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(
Expand Down
69 changes: 69 additions & 0 deletions test/ethereum/fees/getEvmScanApiKey.test.ts
Original file line number Diff line number Diff line change
@@ -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')
})
})
Loading