diff --git a/src/constants/blockchains.ts b/src/constants/blockchains.ts index 8564919..119579b 100644 --- a/src/constants/blockchains.ts +++ b/src/constants/blockchains.ts @@ -52,6 +52,24 @@ const BLOCKCHAINS: {[chain in SupportedChains]: IBlockchainObject} = { raw: `https://rinkeby.etherscan.io/getRawTx?tx=${TRANSACTION_ID_PLACEHOLDER}` } }, + [SupportedChains.Ethgoerli]: { + code: SupportedChains.Ethgoerli, + name: 'Ethereum Testnet', + signatureValue: 'ethereumGoerli', + transactionTemplates: { + full: `https://goerli.etherscan.io/tx/${TRANSACTION_ID_PLACEHOLDER}`, + raw: `https://goerli.etherscan.io/getRawTx?tx=${TRANSACTION_ID_PLACEHOLDER}` + } + }, + [SupportedChains.Ethsepolia]: { + code: SupportedChains.Ethsepolia, + name: 'Ethereum Testnet', + signatureValue: 'ethereumSepolia', + transactionTemplates: { + full: `https://sepolia.etherscan.io/tx/${TRANSACTION_ID_PLACEHOLDER}`, + raw: `https://sepolia.etherscan.io/getRawTx?tx=${TRANSACTION_ID_PLACEHOLDER}` + } + }, [SupportedChains.Mocknet]: { code: SupportedChains.Mocknet, name: 'Mocknet', diff --git a/src/constants/supported-chains.ts b/src/constants/supported-chains.ts index 11d5221..d9cff5f 100644 --- a/src/constants/supported-chains.ts +++ b/src/constants/supported-chains.ts @@ -3,6 +3,8 @@ export enum SupportedChains { Ethmain = 'ethmain', Ethropst = 'ethropst', Ethrinkeby = 'ethrinkeby', + Ethgoerli = 'ethgoerli', + Ethsepolia = 'ethsepolia', Mocknet = 'mocknet', Regtest = 'regtest', Testnet = 'testnet' diff --git a/src/explorers/ethereum/etherscan.ts b/src/explorers/ethereum/etherscan.ts index d851fa7..26a9807 100644 --- a/src/explorers/ethereum/etherscan.ts +++ b/src/explorers/ethereum/etherscan.ts @@ -1,32 +1,47 @@ import request from '../../services/request'; import { stripHashPrefix } from '../../utils/stripHashPrefix'; import { buildTransactionServiceUrl } from '../../services/transaction-apis'; -import { BLOCKCHAINS, isTestChain } from '../../constants/blockchains'; +import { BLOCKCHAINS } from '../../constants/blockchains'; import { TransactionData } from '../../models/transactionData'; import { TRANSACTION_APIS, TRANSACTION_ID_PLACEHOLDER } from '../../constants/api'; -import { ExplorerAPI, ExplorerURLs, IParsingFunctionAPI } from '../../models/explorers'; +import { ExplorerAPI, IParsingFunctionAPI } from '../../models/explorers'; import CONFIG from '../../constants/config'; import { SupportedChains } from '../../constants/supported-chains'; const MAIN_API_BASE_URL = 'https://api.etherscan.io/api?module=proxy'; -const TEST_API_BASE_URL = 'https://api-ropsten.etherscan.io/api?module=proxy'; -const serviceURL: ExplorerURLs = { - main: `${MAIN_API_BASE_URL}&action=eth_getTransactionByHash&txhash=${TRANSACTION_ID_PLACEHOLDER}`, - test: `${TEST_API_BASE_URL}&action=eth_getTransactionByHash&txhash=${TRANSACTION_ID_PLACEHOLDER}` -}; + +function getApiBaseURL (chain: SupportedChains): string { + const testnetNameMap = { + [SupportedChains.Ethropst]: 'ropsten', + [SupportedChains.Ethrinkeby]: 'rinkeby', + [SupportedChains.Ethgoerli]: 'goerli', + [SupportedChains.Ethsepolia]: 'sepolia' + }; + if (!testnetNameMap[chain]) { + return MAIN_API_BASE_URL; + } + const testnetName: string = testnetNameMap[chain]; + return `https://api-${testnetName}.etherscan.io/api?module=proxy`; +} + +function getTransactionServiceURL (chain: SupportedChains): string { + const baseUrl = getApiBaseURL(chain); + return `${baseUrl}&action=eth_getTransactionByHash&txhash=${TRANSACTION_ID_PLACEHOLDER}`; +} // TODO: use tests/explorers/mocks/mockEtherscanResponse as type async function parsingFunction ({ jsonResponse, chain, key, keyPropertyName }: IParsingFunctionAPI): Promise { + const baseUrl = getApiBaseURL(chain); const getBlockByNumberServiceUrls: Partial = { serviceURL: { - main: `${MAIN_API_BASE_URL}&action=eth_getBlockByNumber&boolean=true&tag=${TRANSACTION_ID_PLACEHOLDER}`, - test: `${TEST_API_BASE_URL}&action=eth_getBlockByNumber&boolean=true&tag=${TRANSACTION_ID_PLACEHOLDER}` + main: `${baseUrl}&action=eth_getBlockByNumber&boolean=true&tag=${TRANSACTION_ID_PLACEHOLDER}`, + test: `${baseUrl}&action=eth_getBlockByNumber&boolean=true&tag=${TRANSACTION_ID_PLACEHOLDER}` } }; const getBlockNumberServiceUrls: Partial = { serviceURL: { - main: `${MAIN_API_BASE_URL}&action=eth_blockNumber`, - test: `${TEST_API_BASE_URL}&action=eth_blockNumber` + main: `${baseUrl}&action=eth_blockNumber`, + test: `${baseUrl}&action=eth_blockNumber` } }; @@ -57,7 +72,7 @@ async function parsingFunction ({ jsonResponse, chain, key, keyPropertyName }: I keyPropertyName } as ExplorerAPI, transactionId: blockNumber, - isTestApi: isTestChain(chain) + chain }); try { @@ -80,7 +95,7 @@ async function parsingFunction ({ jsonResponse, chain, key, keyPropertyName }: I key, keyPropertyName } as ExplorerAPI, - isTestApi: isTestChain(chain) + chain }); let response: string; @@ -107,7 +122,7 @@ async function parsingFunction ({ jsonResponse, chain, key, keyPropertyName }: I } export const explorerApi: ExplorerAPI = { - serviceURL, + serviceURL: getTransactionServiceURL, serviceName: TRANSACTION_APIS.etherscan, parsingFunction, priority: -1 diff --git a/src/explorers/explorer.ts b/src/explorers/explorer.ts index 7cc0520..c2d8fe8 100644 --- a/src/explorers/explorer.ts +++ b/src/explorers/explorer.ts @@ -1,6 +1,5 @@ import { buildTransactionServiceUrl } from '../services/transaction-apis'; import request from '../services/request'; -import { isTestChain } from '../constants/blockchains'; import { TransactionData } from '../models/transactionData'; import { ExplorerAPI, TExplorerFunctionsArray } from '../models/explorers'; import { explorerApi as EtherscanApi } from './ethereum/etherscan'; @@ -27,7 +26,7 @@ export async function getTransactionFromApi ( const requestUrl = buildTransactionServiceUrl({ explorerAPI, transactionId, - isTestApi: isTestChain(chain) + chain }); try { diff --git a/src/lookForTx.ts b/src/lookForTx.ts index 3d7ea11..71dfc51 100644 --- a/src/lookForTx.ts +++ b/src/lookForTx.ts @@ -16,6 +16,8 @@ export function getExplorersByChain (chain: SupportedChains, explorerAPIs: TExpl case BLOCKCHAINS[SupportedChains.Ethmain].code: case BLOCKCHAINS[SupportedChains.Ethropst].code: case BLOCKCHAINS[SupportedChains.Ethrinkeby].code: + case BLOCKCHAINS[SupportedChains.Ethgoerli].code: + case BLOCKCHAINS[SupportedChains.Ethsepolia].code: return explorerAPIs.ethereum; default: if (!explorerAPIs.custom?.length) { diff --git a/src/models/explorers.ts b/src/models/explorers.ts index a538b79..f266a52 100644 --- a/src/models/explorers.ts +++ b/src/models/explorers.ts @@ -24,7 +24,7 @@ export interface IParsingFunctionAPI { export type TExplorerParsingFunction = ((data: IParsingFunctionAPI) => TransactionData) | ((data: IParsingFunctionAPI) => Promise); export interface ExplorerAPI { - serviceURL?: string | ExplorerURLs; + serviceURL?: string | ExplorerURLs | ((chain: SupportedChains) => string); priority?: 0 | 1 | -1; // 0: custom APIs will run before the default APIs, 1: after, -1: reserved to default APIs parsingFunction?: TExplorerParsingFunction; serviceName?: TRANSACTION_APIS; // in case one would want to overload the default explorers diff --git a/src/services/transaction-apis.ts b/src/services/transaction-apis.ts index f98f752..3c2e767 100644 --- a/src/services/transaction-apis.ts +++ b/src/services/transaction-apis.ts @@ -1,6 +1,8 @@ import { ExplorerAPI } from '../models/explorers'; import { TRANSACTION_ID_PLACEHOLDER } from '../constants/api'; import { safelyAppendUrlParameter } from '../utils/url'; +import { SupportedChains } from '../constants/supported-chains'; +import { isTestChain } from '../constants/blockchains'; function appendApiIdentifier (url: string, explorerAPI: ExplorerAPI): string { if (!explorerAPI.key) { @@ -18,15 +20,25 @@ export function buildTransactionServiceUrl ({ explorerAPI, transactionIdPlaceholder = TRANSACTION_ID_PLACEHOLDER, transactionId = '', - isTestApi = false + chain }: { explorerAPI: ExplorerAPI; transactionIdPlaceholder?: string; transactionId?: string; - isTestApi?: boolean; + chain?: SupportedChains; }): string { const { serviceURL } = explorerAPI; - let apiUrl = typeof serviceURL === 'string' ? serviceURL : (isTestApi ? serviceURL.test : serviceURL.main); + let apiUrl: string; + if (typeof serviceURL === 'string') { + apiUrl = serviceURL; + } else if (typeof serviceURL === 'object') { + const isTestApi = chain ? isTestChain(chain) : false; + apiUrl = isTestApi ? serviceURL.test : serviceURL.main; + } else if (typeof serviceURL === 'function') { + apiUrl = serviceURL(chain); + } else { + throw new Error(`serviceURL is an unexpected type for explorerAPI ${explorerAPI.serviceName}`); + } apiUrl = apiUrl.replace(transactionIdPlaceholder, transactionId); apiUrl = appendApiIdentifier(apiUrl, explorerAPI); return apiUrl; diff --git a/tests/explorers/ethereum/etherscan.test.ts b/tests/explorers/ethereum/etherscan.test.ts index e686416..4243f36 100644 --- a/tests/explorers/ethereum/etherscan.test.ts +++ b/tests/explorers/ethereum/etherscan.test.ts @@ -3,6 +3,7 @@ import * as mockEtherscanResponse from '../mocks/mockEtherscanResponse.json'; import { explorerApi } from '../../../src/explorers/ethereum/etherscan'; import * as RequestServices from '../../../src/services/request'; import { TransactionData } from '../../../src/models/transactionData'; +import { SupportedChains } from '../../../src/constants/supported-chains'; function getMockEtherscanResponse (): typeof mockEtherscanResponse { return JSON.parse(JSON.stringify(mockEtherscanResponse)); @@ -24,7 +25,7 @@ describe('Etherscan Explorer test suite', function () { time: new Date('2019-06-02T08:38:26.000Z') }; - const res = await explorerApi.parsingFunction({ jsonResponse: mockResponse }); + const res = await explorerApi.parsingFunction({ jsonResponse: mockResponse, chain: SupportedChains.Ethropst }); expect(res).toEqual(assertionTransactionData); }); diff --git a/tests/lookForTx/lookForTx.test.ts b/tests/lookForTx/lookForTx.test.ts index 10ee8e5..b12fa82 100644 --- a/tests/lookForTx/lookForTx.test.ts +++ b/tests/lookForTx/lookForTx.test.ts @@ -98,6 +98,22 @@ describe('getExplorersByChain test suite', function () { }); }); + describe('given the chain is Ethereum goerli', function () { + it('should use the ethereum specific explorers', function () { + const selectedSelectors = getExplorersByChain(SupportedChains.Ethgoerli, explorers.getDefaultExplorers()); + // because they are wrapped, we don't necessarily have the deep nature of the result, so we use a weak test to ensure + expect(selectedSelectors.length).toBe(2); + }); + }); + + describe('given the chain is Ethereum sepolia', function () { + it('should use the ethereum specific explorers', function () { + const selectedSelectors = getExplorersByChain(SupportedChains.Ethsepolia, explorers.getDefaultExplorers()); + // because they are wrapped, we don't necessarily have the deep nature of the result, so we use a weak test to ensure + expect(selectedSelectors.length).toBe(2); + }); + }); + describe('given the chain is Bitcoin mainnet', function () { it('should use the bitcoin specific explorers', function () { const selectedSelectors = getExplorersByChain(SupportedChains.Bitcoin, explorers.getDefaultExplorers()); diff --git a/tests/services/transaction-apis.test.ts b/tests/services/transaction-apis.test.ts index 6ab2697..5898c1c 100644 --- a/tests/services/transaction-apis.test.ts +++ b/tests/services/transaction-apis.test.ts @@ -1,6 +1,7 @@ import { buildTransactionServiceUrl } from '../../src/services/transaction-apis'; import { explorerApi as Blockcypher } from '../../src/explorers/bitcoin/blockcypher'; import { explorerApi as Etherscan } from '../../src/explorers/ethereum/etherscan'; +import { SupportedChains } from '../../src/constants/supported-chains'; describe('Transaction APIs test suite', function () { let fixtureApi; @@ -12,7 +13,7 @@ describe('Transaction APIs test suite', function () { fixtureApi = Blockcypher; }); - describe('given isTestApi is set to false', function () { + describe('given chain is set to null', function () { it('should return the mainnet address with the transaction ID', function () { expect(buildTransactionServiceUrl({ explorerAPI: fixtureApi, @@ -21,22 +22,98 @@ describe('Transaction APIs test suite', function () { }); }); - describe('given isTestApi is set to true', function () { + describe('given chain is set to the testnet', function () { it('should return the testnet address with the transaction ID', function () { expect(buildTransactionServiceUrl({ explorerAPI: fixtureApi, transactionId: fixtureTransactionId, - isTestApi: true + chain: SupportedChains.Testnet })).toEqual(`https://api.blockcypher.com/v1/btc/test3/txs/${fixtureTransactionId}?limit=500`); }); }); }); + describe('handling Etherscan APIs', function () { + beforeEach(function () { + fixtureApi = Etherscan; + }); + + describe('given chain is set to null', function () { + it('should return the mainnet address with the transaction ID', function () { + expect(buildTransactionServiceUrl({ + explorerAPI: fixtureApi, + transactionId: fixtureTransactionId + })).toEqual(`https://api.etherscan.io/api?module=proxy&action=eth_getTransactionByHash&txhash=${fixtureTransactionId}`); + }); + }); + + describe('given chain is set to the mainnet', function () { + it('should return the mainnet address with the transaction ID', function () { + expect(buildTransactionServiceUrl({ + explorerAPI: fixtureApi, + transactionId: fixtureTransactionId, + chain: SupportedChains.Ethmain + })).toEqual(`https://api.etherscan.io/api?module=proxy&action=eth_getTransactionByHash&txhash=${fixtureTransactionId}`); + }); + }); + + describe('given chain is set to the ropsten', function () { + it('should return the ropsten address with the transaction ID', function () { + expect(buildTransactionServiceUrl({ + explorerAPI: fixtureApi, + transactionId: fixtureTransactionId, + chain: SupportedChains.Ethropst + })).toEqual(`https://api-ropsten.etherscan.io/api?module=proxy&action=eth_getTransactionByHash&txhash=${fixtureTransactionId}`); + }); + }); + + describe('given chain is set to the rinkeby', function () { + it('should return the rinkeby address with the transaction ID', function () { + expect(buildTransactionServiceUrl({ + explorerAPI: fixtureApi, + transactionId: fixtureTransactionId, + chain: SupportedChains.Ethrinkeby + })).toEqual(`https://api-rinkeby.etherscan.io/api?module=proxy&action=eth_getTransactionByHash&txhash=${fixtureTransactionId}`); + }); + }); + + describe('given chain is set to the goerli', function () { + it('should return the goerli address with the transaction ID', function () { + expect(buildTransactionServiceUrl({ + explorerAPI: fixtureApi, + transactionId: fixtureTransactionId, + chain: SupportedChains.Ethgoerli + })).toEqual(`https://api-goerli.etherscan.io/api?module=proxy&action=eth_getTransactionByHash&txhash=${fixtureTransactionId}`); + }); + }); + + describe('given chain is set to the sepolia', function () { + it('should return the sepolia address with the transaction ID', function () { + expect(buildTransactionServiceUrl({ + explorerAPI: fixtureApi, + transactionId: fixtureTransactionId, + chain: SupportedChains.Ethsepolia + })).toEqual(`https://api-sepolia.etherscan.io/api?module=proxy&action=eth_getTransactionByHash&txhash=${fixtureTransactionId}`); + }); + }); + + describe('and the serviceURL is not set', function () { + it('should throw', function () { + expect(() => { + buildTransactionServiceUrl({ + explorerAPI: JSON.parse(JSON.stringify(Etherscan)) + }); + }).toThrow('serviceURL is an unexpected type for explorerAPI etherscan'); + }); + }); + }); + describe('given it is called with an API token', function () { const fixtureAPIToken = 'a-test-api-token'; beforeEach(function () { fixtureApi = JSON.parse(JSON.stringify(Etherscan)); + fixtureApi.serviceURL = Etherscan.serviceURL; fixtureApi.key = fixtureAPIToken; fixtureApi.keyPropertyName = 'apikey'; }); @@ -47,7 +124,7 @@ describe('Transaction APIs test suite', function () { explorerAPI: fixtureApi, transactionId: fixtureTransactionId }); - const expectedOutput = `https://api.etherscan.io/api?module=proxy&action=eth_getTransactionByHash&txhash=fixture-transaction-id&apikey=${fixtureAPIToken}`; + const expectedOutput = `https://api.etherscan.io/api?module=proxy&action=eth_getTransactionByHash&txhash=${fixtureTransactionId}&apikey=${fixtureAPIToken}`; expect(output).toBe(expectedOutput); }); });