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
52 changes: 52 additions & 0 deletions src/common/serviceKeys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { ServiceKeys } from './types'
import { pickRandom } from './utils'

/**
* Retrieves a random service key (API key) for a given host index.
*
* @param serviceKeys - An object mapping host strings to arrays of API keys
* @param index - The host string to look up (typically obtained from getServiceKeyIndex)
* @returns A randomly selected API key string, or undefined if no keys are available for the given index
*
* @example
* ```typescript
* const serviceKeys = {
* 'api.example.com': ['key1', 'key2', 'key3']
* }
* const key = getRandomServiceKey(serviceKeys, 'api.example.com')
* // Returns one of: 'key1', 'key2', or 'key3'
* ```
*/
export function getRandomServiceKey(
serviceKeys: ServiceKeys,
index: string
): string | undefined {
const keys = serviceKeys[index]
if (keys == null || keys.length === 0) return undefined
return pickRandom(keys, 1)[0]
}

/**
* Extracts the host from a URL string to use as an index for ServiceKeys.
*
* This function normalizes URLs by constructing a proper URL object, which handles
* both full URLs and host-only strings. The host is used as the key to look up
* service keys in the ServiceKeys object.
*
* The reason for this function is to define the index or key for the
* ServiceKey object from a URL string to determine the relevant API keys for
* the service provider.
*
* @param urlString - A URL string or host string (e.g., 'https://api.example.com' or 'api.example.com')
* @returns The host portion of the URL (e.g., 'api.example.com')
*
* @example
* ```typescript
* getServiceKeyIndex('https://api.example.com/path') // Returns 'api.example.com'
* getServiceKeyIndex('api.example.com') // Returns 'api.example.com'
* ```
*/
export function getServiceKeyIndex(urlString: string): string {
const url = new URL(urlString, `https://${urlString}`)
return url.host
}
5 changes: 5 additions & 0 deletions src/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,3 +205,8 @@ export const asEdgeToken = asObject({
export const asInfoServerTokens = asObject({
infoServerTokens: asMaybe(asArray(asUnknown))
})

export interface ServiceKeys {
[host: string]: string[]
}
export const asServiceKeys: Cleaner<ServiceKeys> = asObject(asArray(asString))
3 changes: 2 additions & 1 deletion src/ethereum/EthereumTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
} from '../common/utils'
import { ethereumPlugins } from './ethereumInfos'
import {
asEthereumInitOptions,
asEthereumPrivateKeys,
asSafeEthWalletInfo,
EthereumInfoPayload,
Expand All @@ -50,7 +51,7 @@ export class EthereumTools implements EdgeCurrencyTools {
this.currencyInfo = currencyInfo
this.io = io
this.networkInfo = networkInfo
this.initOptions = initOptions
this.initOptions = asEthereumInitOptions(initOptions)
}

async getDisplayPrivateKey(
Expand Down
28 changes: 24 additions & 4 deletions src/ethereum/ethereumTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,29 +18,50 @@ import { EdgeSpendInfo } from 'edge-core-js/types'
import {
asIntegerString,
asSafeCommonWalletInfo,
asServiceKeys,
ServiceKeys,
WalletConnectPayload
} from '../common/types'
import { FeeAlgorithmConfig } from './feeAlgorithms/feeAlgorithmTypes'
import type { NetworkAdapterConfig } from './networkAdapters/networkAdapterTypes'

export interface EthereumInitOptions {
serviceKeys: ServiceKeys
infuraProjectId?: string

/** @deprecated Use serviceKeys instead */
alchemyApiKey?: string
/** @deprecated Use serviceKeys instead */
amberdataApiKey?: string
/** @deprecated Use serviceKeys instead */
blockchairApiKey?: string
/** @deprecated Use serviceKeys instead */
blockcypherApiKey?: string
/** @deprecated Use serviceKeys instead */
drpcApiKey?: string
/** For Etherscan v2 API */
/** For Etherscan v2 API
* @deprecated Use serviceKeys instead
*/
etherscanApiKey?: string | string[]
/** For bespoke scan APIs unsupported by Etherscan v2 API (e.g. fantomscan) */
/** For bespoke scan APIs unsupported by Etherscan v2 API (e.g. fantomscan)
* @deprecated Use serviceKeys instead
*/
evmScanApiKey?: string | string[]
/** @deprecated Use serviceKeys instead */
gasStationApiKey?: string
infuraProjectId?: string
/** @deprecated Use serviceKeys instead */
nowNodesApiKey?: string
/** @deprecated Use serviceKeys instead */
poktPortalApiKey?: string
/** @deprecated Use serviceKeys instead */
quiknodeApiKey?: string
}

export const asEthereumInitOptions = asObject<EthereumInitOptions>({
serviceKeys: asOptional(asServiceKeys, () => ({})),
infuraProjectId: asOptional(asString),

// Deprecated:
alchemyApiKey: asOptional(asString),
amberdataApiKey: asOptional(asString),
blockchairApiKey: asOptional(asString),
Expand All @@ -49,7 +70,6 @@ export const asEthereumInitOptions = asObject<EthereumInitOptions>({
etherscanApiKey: asOptional(asEither(asString, asArray(asString))),
evmScanApiKey: asOptional(asEither(asString, asArray(asString))),
gasStationApiKey: asOptional(asString),
infuraProjectId: asOptional(asString),
nowNodesApiKey: asOptional(asString),
poktPortalApiKey: asOptional(asString),
quiknodeApiKey: asOptional(asString)
Expand Down
35 changes: 29 additions & 6 deletions src/ethereum/fees/feeProviders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import {
} from 'edge-core-js/types'

import { fetchInfo } from '../../common/network'
import {
getRandomServiceKey,
getServiceKeyIndex
} from '../../common/serviceKeys'
import { hexToDecimal, pickRandom } from '../../common/utils'
import {
GAS_PRICE_SANITY_CHECK,
Expand Down Expand Up @@ -346,36 +350,55 @@ export const getEvmScanApiKey = (
log: EdgeLog,
serverUrl: string
): string | string[] | undefined => {
const { evmScanApiKey, etherscanApiKey, bscscanApiKey, polygonscanApiKey } =
initOptions
const {
evmScanApiKey,
etherscanApiKey,
bscscanApiKey,
polygonscanApiKey,
serviceKeys
} = initOptions

const { currencyCode } = info
const serviceKey = getRandomServiceKey(
serviceKeys,
getServiceKeyIndex(serverUrl)
)

if (serviceKey != null) return serviceKey

// If we have a server URL and it's etherscan.io, use the Ethereum API key
if (serverUrl.includes('etherscan.io')) {
if (etherscanApiKey == null)
throw new Error(`Missing etherscanApiKey for etherscan.io`)
log.warn(
"INIT OPTION 'etherscanApiKey' IS DEPRECATED. USE 'serviceKeys' INSTEAD"
)
return etherscanApiKey
}

if (evmScanApiKey != null) return evmScanApiKey
if (evmScanApiKey != null) {
log.warn(
"INIT OPTION 'evmScanApiKey' IS DEPRECATED. USE 'serviceKeys' INSTEAD"
)
return evmScanApiKey
}

// For networks that don't support Etherscan v2, fall back to network-specific keys
if (currencyCode === 'ETH' && etherscanApiKey != null) {
log.warn(
"INIT OPTION 'etherscanApiKey' IS DEPRECATED. USE 'evmScanApiKey' INSTEAD"
"INIT OPTION 'etherscanApiKey' IS DEPRECATED. USE 'serviceKeys' INSTEAD"
)
return etherscanApiKey
}
if (currencyCode === 'BNB' && bscscanApiKey != null) {
log.warn(
"INIT OPTION 'bscscanApiKey' IS DEPRECATED. USE 'evmScanApiKey' INSTEAD"
"INIT OPTION 'bscscanApiKey' IS DEPRECATED. USE 'serviceKeys' INSTEAD"
)
return bscscanApiKey
}
if (currencyCode === 'POL' && polygonscanApiKey != null) {
log.warn(
"INIT OPTION 'polygonscanApiKey' IS DEPRECATED. USE 'evmScanApiKey' INSTEAD"
"INIT OPTION 'polygonscanApiKey' IS DEPRECATED. USE 'serviceKeys' INSTEAD"
)
return polygonscanApiKey
}
Expand Down
44 changes: 36 additions & 8 deletions src/ethereum/networkAdapters/AmberdataAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { base58ToHexAddress } from '../../tron/tronUtils'
import {
getRandomServiceKey,
getServiceKeyIndex
} from '../../common/serviceKeys'
import { EthereumNetworkUpdate } from '../EthereumNetwork'
import { asRpcResultString } from '../ethereumTypes'
import { NetworkAdapter } from './networkAdapterTypes'
Expand Down Expand Up @@ -52,10 +55,22 @@ export class AmberdataAdapter extends NetworkAdapter<AmberdataAdapterConfig> {
method: string,
params: string[] = []
): Promise<any> {
const { amberdataApiKey = '' } = this.ethEngine.initOptions
const { amberdataApiKey, serviceKeys } = this.ethEngine.initOptions

return await this.serialServers(async url => {
let apiKey = getRandomServiceKey(serviceKeys, getServiceKeyIndex(url))

if (apiKey == null && amberdataApiKey != null) {
this.ethEngine.log.warn(
"INIT OPTION 'amberdataApiKey' IS DEPRECATED. USE 'serviceKeys' INSTEAD"
)
apiKey = amberdataApiKey
}

if (apiKey == null) {
throw new Error('Missing API key')
}

return await this.serialServers(async baseUrl => {
const url = `${this.config.servers[0]}`
const body = {
jsonrpc: '2.0',
method: method,
Expand All @@ -65,7 +80,7 @@ export class AmberdataAdapter extends NetworkAdapter<AmberdataAdapterConfig> {
const response = await this.ethEngine.fetchCors(url, {
headers: {
'x-amberdata-blockchain-id': this.config.amberdataBlockchainId,
'x-api-key': amberdataApiKey,
'x-api-key': apiKey,
'Content-Type': 'application/json'
},
method: 'POST',
Expand All @@ -82,14 +97,27 @@ export class AmberdataAdapter extends NetworkAdapter<AmberdataAdapterConfig> {

// TODO: Clean return type
private async fetchGetAmberdataApi(path: string): Promise<any> {
const { amberdataApiKey = '' } = this.ethEngine.initOptions
const { amberdataApiKey, serviceKeys } = this.ethEngine.initOptions

return await this.serialServers(async baseUrl => {
const url = `${base58ToHexAddress}${path}`
let apiKey = getRandomServiceKey(serviceKeys, getServiceKeyIndex(baseUrl))

if (apiKey == null && amberdataApiKey != null) {
this.ethEngine.log.warn(
"INIT OPTION 'amberdataApiKey' IS DEPRECATED. USE 'serviceKeys' INSTEAD"
)
apiKey = amberdataApiKey
}

if (apiKey == null) {
throw new Error('Missing API key')
}

const url = `${baseUrl}${path}`
const response = await this.ethEngine.fetchCors(url, {
headers: {
'x-amberdata-blockchain-id': this.config.amberdataBlockchainId,
'x-api-key': amberdataApiKey
'x-api-key': apiKey
}
})
if (!response.ok) {
Expand Down
16 changes: 15 additions & 1 deletion src/ethereum/networkAdapters/BlockbookWsAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import { asBoolean, asJSON, asMaybe, asObject, asString } from 'cleaners'
import WebSocket from 'isomorphic-ws'

import { makePeriodicTask, PeriodicTask } from '../../common/periodicTask'
import {
getRandomServiceKey,
getServiceKeyIndex
} from '../../common/serviceKeys'
import { pickRandomOne } from '../../common/utils'
import { EthereumEngine } from '../EthereumEngine'
import { EthereumInitOptions } from '../ethereumTypes'
Expand Down Expand Up @@ -208,9 +212,19 @@ export class BlockbookWsAdapter extends NetworkAdapter<BlockbookWsAdapterConfig>

while (servers.length > 0) {
const server = pickRandomOne(this.servers)
let apiKey = getRandomServiceKey(
this.ethEngine.initOptions.serviceKeys,
getServiceKeyIndex(server.url)
)

if (server.keyType != null) {
const apiKey = this.ethEngine.initOptions[server.keyType]
if (apiKey == null) {
apiKey = this.ethEngine.initOptions[server.keyType]
if (apiKey != null)
this.ethEngine.log.warn(
`INIT OPTION '${server.keyType}' IS DEPRECATED. USE 'serviceKeys' INSTEAD`
)
}

// Check for missing API key
if (apiKey == null) {
Expand Down
25 changes: 20 additions & 5 deletions src/ethereum/networkAdapters/BlockchairAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import {
getRandomServiceKey,
getServiceKeyIndex
} from '../../common/serviceKeys'
import { safeErrorMessage } from '../../common/utils'
import { EthereumNetworkUpdate } from '../EthereumNetwork'
import {
Expand Down Expand Up @@ -88,13 +92,24 @@ export class BlockchairAdapter extends NetworkAdapter<BlockchairAdapterConfig> {
path: string,
includeKey: boolean = false
): Promise<any> {
const { blockchairApiKey } = this.ethEngine.initOptions
const { blockchairApiKey, serviceKeys } = this.ethEngine.initOptions

return await this.serialServers(async baseUrl => {
const keyParam =
includeKey && blockchairApiKey != null ? `&key=${blockchairApiKey}` : ''
const url = `${baseUrl}${path}`
const response = await this.ethEngine.fetchCors(`${url}${keyParam}`)
let apiKey: string | undefined

if (includeKey) {
apiKey = getRandomServiceKey(serviceKeys, getServiceKeyIndex(baseUrl))

if (apiKey == null && blockchairApiKey != null) {
this.ethEngine.log.warn(
"INIT OPTION 'blockchairApiKey' IS DEPRECATED. USE 'serviceKeys' INSTEAD"
)
apiKey = blockchairApiKey
}
}

const url = `${baseUrl}${path}${apiKey != null ? `&key=${apiKey}` : ''}`
const response = await this.ethEngine.fetchCors(url)
if (!response.ok) {
const resBody = await response.text()
this.throwError(response, 'fetchGetBlockchair', url, resBody)
Expand Down
22 changes: 17 additions & 5 deletions src/ethereum/networkAdapters/BlockcypherAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { EdgeTransaction } from 'edge-core-js/types'
import parse from 'url-parse'

import {
getRandomServiceKey,
getServiceKeyIndex
} from '../../common/serviceKeys'
import { BroadcastResults } from '../EthereumNetwork'
import { NetworkAdapter } from './networkAdapterTypes'

Expand Down Expand Up @@ -50,13 +54,21 @@ export class BlockcypherAdapter extends NetworkAdapter<BlockcypherAdapterConfig>
body: any,
baseUrl: string
): Promise<any> {
const { blockcypherApiKey } = this.ethEngine.initOptions
let apiKey = ''
if (blockcypherApiKey != null && blockcypherApiKey.length > 5) {
apiKey = '&token=' + blockcypherApiKey
const { blockcypherApiKey, serviceKeys } = this.ethEngine.initOptions
let apiKey = getRandomServiceKey(serviceKeys, getServiceKeyIndex(baseUrl))

if (
apiKey == null &&
blockcypherApiKey != null &&
blockcypherApiKey.length > 5
) {
apiKey = blockcypherApiKey
this.ethEngine.log.warn(
"INIT OPTION 'blockcypherApiKey' IS DEPRECATED. USE 'serviceKeys' INSTEAD"
)
}

const url = `${baseUrl}/${cmd}${apiKey}`
const url = `${baseUrl}/${cmd}${apiKey != null ? `&token=${apiKey}` : ''}`
const response = await this.ethEngine.fetchCors(url, {
headers: {
Accept: 'application/json',
Expand Down
Loading
Loading