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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- changed: Sign MoonPay buy/sell widget URLs and bind them to the customer's IP via the info server, for MoonPay's on-ramp IP-matching security upgrade.
- fixed: NYM max swaps from EVM wallets now report the correct limit error instead of an unsupported-route error (edge-exchange-plugins 2.52.1).
- fixed: Exchange rate queries no longer request each chain's own asset twice, which had been inflating every rate query with duplicate pairs.
- fixed: EVM wallets no longer stall at 50% sync on a build whose env.json configures `evmScanApiKey` without the deprecated `etherscanApiKey`.

## 4.50.0 (2026-07-21)

Expand Down
44 changes: 44 additions & 0 deletions src/__tests__/envConfig.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { describe, expect, it } from '@jest/globals'

import { asEnvConfig } from '../envConfig'

describe('asEnvConfig EVM api keys', () => {
it('supplies the deprecated etherscanApiKey from evmScanApiKey', () => {
// edge-currency-accountbased reads only `etherscanApiKey` for
// api.etherscan.io, and a keyless Etherscan V2 request is rejected, which
// leaves EVM wallets stuck at 50% sync:
const env = asEnvConfig({ ETHEREUM_INIT: { evmScanApiKey: ['key1'] } })
expect(env.ETHEREUM_INIT).toMatchObject({
evmScanApiKey: ['key1'],
etherscanApiKey: ['key1']
})
})

it('leaves an explicit etherscanApiKey alone', () => {
const env = asEnvConfig({
ETHEREUM_INIT: { evmScanApiKey: ['key1'], etherscanApiKey: ['legacy'] }
})
expect(env.ETHEREUM_INIT).toMatchObject({
evmScanApiKey: ['key1'],
etherscanApiKey: ['legacy']
})
})

it('adds no key when evmScanApiKey is unconfigured', () => {
const env = asEnvConfig({ ETHEREUM_INIT: {} })
expect(env.ETHEREUM_INIT).toMatchObject({
evmScanApiKey: [],
etherscanApiKey: undefined
})
})

it('applies to every EVM plugin init, not just ethereum', () => {
const env = asEnvConfig({ BASE_INIT: { evmScanApiKey: ['key1'] } })
expect(env.BASE_INIT).toMatchObject({ etherscanApiKey: ['key1'] })
})

it('keeps a disabled plugin init disabled', () => {
const env = asEnvConfig({ ETHEREUM_INIT: false })
expect(env.ETHEREUM_INIT).toBe(false)
})
})
23 changes: 22 additions & 1 deletion src/envConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,12 @@ function asCorePluginInit<T>(cleaner: Cleaner<T>): Cleaner<T | false> {
}
}

const asEvmApiKeys = asObject({
const asRawEvmApiKeys = asObject({
alethioApiKey: asOptional(asString, ''),
amberdataApiKey: asOptional(asString, ''),
blockchairApiKey: asOptional(asString, ''),
drpcApiKey: asOptional(asString, ''),
etherscanApiKey: asOptional(asArray(asString)),
evmScanApiKey: asOptional(asArray(asString), () => []),
gasStationApiKey: asOptional(asString, ''),
infuraProjectId: asOptional(asString, ''),
Expand All @@ -48,6 +49,26 @@ const asEvmApiKeys = asObject({
quiknodeApiKey: asOptional(asString, '')
}).withRest

/**
* Up to edge-currency-accountbased 4.86.2, key lookup for `api.etherscan.io`
* reads only the deprecated `etherscanApiKey` option and throws before it
* considers `evmScanApiKey`. Etherscan V2 rejects keyless requests, and its
* adapter is the sole source of transaction history on the EVM networks that
* route through that host, so a build supplying only `evmScanApiKey` leaves
* every one of those wallets stuck at 50% sync. Supply the deprecated name
* from the supported one so env.json only has to carry `evmScanApiKey`.
*
* Remove once the app depends on a release carrying the plugin-side fix.
*/
const asEvmApiKeys: Cleaner<ReturnType<typeof asRawEvmApiKeys>> = raw => {
const clean = asRawEvmApiKeys(raw)
if (clean.etherscanApiKey != null && clean.etherscanApiKey.length > 0) {
return clean
}
if (clean.evmScanApiKey.length === 0) return clean
return { ...clean, etherscanApiKey: clean.evmScanApiKey }
}

export const asEnvConfig = asObject({
// API keys:
EDGE_API_KEY: asOptional(asString, ''),
Expand Down