diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 266c79a9f3..f5df3a1ef4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,6 +6,7 @@ on: env: SDK_ERROR_MESSAGES_VERSION: "master" V4_API_KEY_MAINNET: ${{ secrets.V4_API_KEY_MAINNET }} + V4_API_KEY_TESTNET: ${{ secrets.V4_API_KEY_TESTNET }} V3_API_KEY_MAINNET: ${{ secrets.V3_API_KEY_MAINNET }} V3_API_KEY_TESTNET: ${{ secrets.V3_API_KEY_TESTNET }} NON_TATUM_RPC_ETH_URL: ${{ secrets.NON_TATUM_RPC_ETH_URL }} diff --git a/.github/workflows/release-pr.yaml b/.github/workflows/release-pr.yaml index 892f3192e2..8949e3ca5b 100644 --- a/.github/workflows/release-pr.yaml +++ b/.github/workflows/release-pr.yaml @@ -7,6 +7,7 @@ on: env: V4_API_KEY_MAINNET: ${{ secrets.V4_API_KEY_MAINNET }} + V4_API_KEY_TESTNET: ${{ secrets.V4_API_KEY_TESTNET }} V3_API_KEY_MAINNET: ${{ secrets.V3_API_KEY_MAINNET }} V3_API_KEY_TESTNET: ${{ secrets.V3_API_KEY_TESTNET }} NON_TATUM_RPC_ETH_URL: ${{ secrets.NON_TATUM_RPC_ETH_URL }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 4aab421383..2fd500e0ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## [4.1.16] - 2023.10.24 +### Added +- Added Beacon chain v1 support for the Ethereum + ## [4.1.15] - 2023.10.24 ### Added - Added estimatefee rpc method to the Bitcoin Cash network diff --git a/package.json b/package.json index 9795076ab6..d0a0599251 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tatumio/tatum", - "version": "4.1.15", + "version": "4.1.16", "description": "Tatum JS SDK", "author": "Tatum", "repository": "https://github.com/tatumio/tatum-js", diff --git a/src/dto/GetI.ts b/src/dto/GetI.ts new file mode 100644 index 0000000000..c4358b76a1 --- /dev/null +++ b/src/dto/GetI.ts @@ -0,0 +1,5 @@ +export interface GetI { + path: string + // eslint-disable-next-line @typescript-eslint/no-explicit-any + basePath?: string +} diff --git a/src/dto/Network.ts b/src/dto/Network.ts index a11d4f1898..ac289ef40b 100644 --- a/src/dto/Network.ts +++ b/src/dto/Network.ts @@ -256,6 +256,12 @@ export const EVM_ARCHIVE_NON_ARCHIVE_LOAD_BALANCER_NETWORKS = [ Network.CHILIZ, ] +export const EVM_ARCHIVE_NON_ARCHIVE_BEACON_LOAD_BALANCER_NETWORKS = [ + Network.ETHEREUM, + Network.ETHEREUM_SEPOLIA, + Network.ETHEREUM_HOLESKY, +] + export const TRON_NETWORKS = [Network.TRON, Network.TRON_SHASTA] export const EOS_NETWORKS = [Network.EOS, Network.EOS_TESTNET] @@ -291,6 +297,9 @@ export const isEvmLoadBalancerNetwork = (network: Network) => EVM_LOAD_BALANCER_ export const isEvmArchiveNonArchiveLoadBalancerNetwork = (network: Network) => EVM_ARCHIVE_NON_ARCHIVE_LOAD_BALANCER_NETWORKS.includes(network) +export const isEvmArchiveNonArchiveBeaconLoadBalancerNetwork = (network: Network) => + EVM_ARCHIVE_NON_ARCHIVE_BEACON_LOAD_BALANCER_NETWORKS.includes(network) + export const isTronLoadBalancerNetwork = (network: Network) => TRON_LOAD_BALANCER_NETWORKS.includes(network) export const isEosLoadBalancerNetwork = (network: Network) => EOS_LOAD_BALANCER_NETWORKS.includes(network) diff --git a/src/dto/rpc/EvmBasedRpcInterface.ts b/src/dto/rpc/EvmBasedRpcInterface.ts index 967e1e2b17..af2737f7a4 100644 --- a/src/dto/rpc/EvmBasedRpcInterface.ts +++ b/src/dto/rpc/EvmBasedRpcInterface.ts @@ -216,3 +216,82 @@ export interface EvmBasedRpcInterface { txPoolInspect(): Promise> } + +export interface EvmBeaconResponse { + data: T + execution_optimistic?: boolean + finalized?: boolean +} + +export type StateId = 'head' | 'genesis' | 'finalized' | 'justified' | string; +export type BlockId = 'head' | 'genesis' | 'finalized' | string; + +export interface BlockQuery { + blockId: BlockId +} + +export interface StateCommitteesQuery { + stateId: StateId + epoch?: string; + index?: string; + slot?: string; +} + +export interface StateQuery { + stateId: StateId +} + +export interface StateSyncCommitteesQuery extends StateQuery { + epoch?: string; +} + +export interface ValidatorBalancesQuery extends StateQuery { + id?: string[]; // Array of either hex encoded public key or validator index +} + +export type ValidatorStatus = + | 'pending_initialized' + | 'pending_queued' + | 'active_ongoing' + | 'active_exiting' + | 'active_slashed' + | 'exited_unslashed' + | 'exited_slashed' + | 'withdrawal_possible' + | 'withdrawal_done' + | 'active' + | 'pending' + | 'exited' + | 'withdrawal'; + +export interface ValidatorsQuery extends StateQuery { + id?: string[]; // Array of either hex encoded public key or validator index + status?: ValidatorStatus[]; +} + +export interface ValidatorQuery extends StateQuery { + validatorId: string; // Either a hex encoded public key or validator index +} + + +export interface EvmBeaconV1Interface { + getGenesis(): Promise> + getBlockHeaders(query?: { slot?: string, parentRoot?: string }): Promise> + getBlockHeader(query:BlockQuery): Promise> + getBlockRoot(query: BlockQuery): Promise> + getBlockAttestations(query: BlockQuery): Promise> + getStateCommittees(query: StateCommitteesQuery): Promise> + getStateFinalityCheckpoints(query: StateQuery): Promise> + getStateFork(query: StateQuery): Promise> + getStateRoot(query: StateQuery): Promise> + getStateSyncCommittees(query: StateSyncCommitteesQuery): Promise> + getStateValidatorBalances(query: ValidatorBalancesQuery): Promise> + getStateValidators(query: ValidatorsQuery): Promise> + getStateValidator(query: ValidatorQuery): Promise> +} + +export interface EvmBasedBeaconRpcSuite extends EvmBasedRpcInterface, AbstractRpcInterface { + beacon: { + v1: EvmBeaconV1Interface + } +} diff --git a/src/e2e/ipfs.spec.ts b/src/e2e/ipfs.spec.ts new file mode 100644 index 0000000000..ac0c86b405 --- /dev/null +++ b/src/e2e/ipfs.spec.ts @@ -0,0 +1,13 @@ +import { EvmE2eUtils } from './rpc/evm/evm.e2e.utils'; +import { Network } from '../dto'; +import fs from 'fs'; + +describe.skip('IPFS', () => { + it('should upload file to IPFS', async () => { + const tatum = await EvmE2eUtils.initTatum(Network.ETHEREUM, process.env.V4_API_KEY_MAINNET); + const fileData = fs.readFileSync('./test.txt'); // Adjust the path to your file + const response = await tatum.ipfs.uploadFile({ file: fileData }); + expect(response.status).toBe('SUCCESS'); + expect(response.data.ipfsHash).toBeDefined(); + }); +}); diff --git a/src/e2e/rpc/evm/eth/tatum.rpc.beacon.spec.ts b/src/e2e/rpc/evm/eth/tatum.rpc.beacon.spec.ts new file mode 100644 index 0000000000..0a98af37d9 --- /dev/null +++ b/src/e2e/rpc/evm/eth/tatum.rpc.beacon.spec.ts @@ -0,0 +1,71 @@ +import { Network } from '../../../../dto' +import { EvmE2eUtils } from '../evm.e2e.utils' +import { Ethereum } from '../../../../service' + +describe('Beacon', () => { + describe('v1', () => { + const networks = [ + Network.ETHEREUM_HOLESKY, + Network.ETHEREUM_SEPOLIA, + Network.ETHEREUM + ]; + + describe.each(networks)('%s', (network) => { + it('should get genesis', async () => { + const tatum = await EvmE2eUtils.initTatum(network, process.env.V4_API_KEY_TESTNET); + const { data } = await tatum.rpc.beacon.v1.getGenesis(); + await tatum.destroy(); + expect(data).toBeDefined(); + }); + + it('should get state root', async () => { + const tatum = await EvmE2eUtils.initTatum(network, process.env.V4_API_KEY_TESTNET); + const { data } = await tatum.rpc.beacon.v1.getStateRoot({ stateId: 'head' }); + await tatum.destroy(); + expect(data).toBeDefined(); + }); + + it('should get block headers', async () => { + const tatum = await EvmE2eUtils.initTatum(network, process.env.V4_API_KEY_TESTNET); + const { data } = await tatum.rpc.beacon.v1.getBlockHeaders({ slot: '1'}); + await tatum.destroy(); + expect(data).toBeDefined(); + }); + + it('should get block root', async () => { + const tatum = await EvmE2eUtils.initTatum(network, process.env.V4_API_KEY_TESTNET); + const { data } = await tatum.rpc.beacon.v1.getBlockRoot({ blockId: 'head' }); + await tatum.destroy(); + expect(data).toBeDefined(); + }); + + it('should get state committees', async () => { + const tatum = await EvmE2eUtils.initTatum(network, process.env.V4_API_KEY_TESTNET); + const { data } = await tatum.rpc.beacon.v1.getStateCommittees({ stateId: 'head' }); + await tatum.destroy(); + expect(data).toBeDefined(); + }); + + it('should get state finality checkpoints', async () => { + const tatum = await EvmE2eUtils.initTatum(network, process.env.V4_API_KEY_TESTNET); + const { data } = await tatum.rpc.beacon.v1.getStateFinalityCheckpoints({ stateId: 'head' }); + await tatum.destroy(); + expect(data).toBeDefined(); + }); + + it('should get state fork', async () => { + const tatum = await EvmE2eUtils.initTatum(network, process.env.V4_API_KEY_TESTNET); + const { data } = await tatum.rpc.beacon.v1.getStateFork({ stateId: 'head' }); + await tatum.destroy(); + expect(data).toBeDefined(); + }); + + it('should get state sync committees', async () => { + const tatum = await EvmE2eUtils.initTatum(network, process.env.V4_API_KEY_TESTNET); + const { data } = await tatum.rpc.beacon.v1.getStateSyncCommittees({ stateId: 'head' }); + await tatum.destroy(); + expect(data).toBeDefined(); + }); + }) + }) +}) diff --git a/src/e2e/rpc/evm/tatum.rpc.ethereum.spec.ts b/src/e2e/rpc/evm/eth/tatum.rpc.ethereum.spec.ts similarity index 97% rename from src/e2e/rpc/evm/tatum.rpc.ethereum.spec.ts rename to src/e2e/rpc/evm/eth/tatum.rpc.ethereum.spec.ts index e859fa463e..38ab8a5a69 100644 --- a/src/e2e/rpc/evm/tatum.rpc.ethereum.spec.ts +++ b/src/e2e/rpc/evm/eth/tatum.rpc.ethereum.spec.ts @@ -1,6 +1,6 @@ import process from 'process' -import { BaseEvm, Network, RpcNodeType, TatumSDK } from '../../../service' -import { EvmE2eUtils } from './evm.e2e.utils' +import { BaseEvm, Network, RpcNodeType, TatumSDK } from '../../../../service' +import { EvmE2eUtils } from '../evm.e2e.utils' describe('Ethereum', () => { it('should get token total supply', async () => { diff --git a/src/e2e/rpc/rpc.e2e.utils.ts b/src/e2e/rpc/rpc.e2e.utils.ts index b1568e945e..2ec1a2beaf 100644 --- a/src/e2e/rpc/rpc.e2e.utils.ts +++ b/src/e2e/rpc/rpc.e2e.utils.ts @@ -10,7 +10,7 @@ export const RpcE2eUtils = { retryDelay: 2000, apiKey: { v4: apiKey ?? process.env.V4_API_KEY_MAINNET, - }, + } } }, } diff --git a/src/service/rpc/evm/AbstractBeaconV1EvmRpc.ts b/src/service/rpc/evm/AbstractBeaconV1EvmRpc.ts new file mode 100644 index 0000000000..e2919ad7a6 --- /dev/null +++ b/src/service/rpc/evm/AbstractBeaconV1EvmRpc.ts @@ -0,0 +1,86 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Service } from 'typedi' +import { + BlockQuery, + EvmBeaconV1Interface, + EvmBeaconResponse, + StateCommitteesQuery, + StateQuery, + StateSyncCommitteesQuery, ValidatorBalancesQuery, ValidatorQuery, ValidatorsQuery, +} from '../../../dto' +import { GetI } from '../../../dto/GetI' +import { Constant, Utils } from '../../../util' + +@Service() +export abstract class AbstractBeaconV1EvmRpc implements EvmBeaconV1Interface { + protected abstract get(get: GetI): Promise + + getBlockAttestations({ blockId, ...rest }: BlockQuery): Promise> { + const path = Utils.addQueryParams(`${Constant.BEACON_PREFIX}/blocks/${blockId}/attestations`, rest); + return this.get({ path }); + } + + getBlockHeader({ blockId, ...rest }: BlockQuery): Promise> { + const path = Utils.addQueryParams(`${Constant.BEACON_PREFIX}/blocks/${blockId}/header`, rest); + return this.get({ path }); + } + getBlockHeaders({ slot, parentRoot, ...rest }: { slot?: string; parentRoot?: string } = {}): Promise> { + const queryParams = { + ...(slot ? { slot } : {}), + ...(parentRoot ? { parentRoot } : {}), + ...rest + }; + const path = Utils.addQueryParams(`${Constant.BEACON_PREFIX}/headers`, queryParams); + return this.get({ path }); + } + + getBlockRoot({ blockId, ...rest }: BlockQuery): Promise> { + const path = Utils.addQueryParams(`${Constant.BEACON_PREFIX}/blocks/${blockId}/root`, rest); + return this.get({ path }); + } + + getGenesis(): Promise> { + const path = Utils.addQueryParams(`${Constant.BEACON_PREFIX}/genesis`); + return this.get({ path }); + } + + getStateCommittees({ stateId, ...rest }: StateCommitteesQuery): Promise> { + const path = Utils.addQueryParams(`${Constant.BEACON_PREFIX}/states/${stateId}/committees`, rest); + return this.get({ path }); + } + + getStateFinalityCheckpoints({ stateId, ...rest }: StateQuery): Promise> { + const path = Utils.addQueryParams(`${Constant.BEACON_PREFIX}/states/${stateId}/finality_checkpoints`, rest); + return this.get({ path }); + } + + getStateFork({ stateId, ...rest }: StateQuery): Promise> { + const path = Utils.addQueryParams(`${Constant.BEACON_PREFIX}/states/${stateId}/fork`, rest); + return this.get({ path }); + } + + getStateRoot({ stateId, ...rest }: StateQuery): Promise> { + const path = Utils.addQueryParams(`${Constant.BEACON_PREFIX}/states/${stateId}/root`, rest); + return this.get({ path }); + } + + getStateSyncCommittees({ stateId, ...rest }: StateSyncCommitteesQuery): Promise> { + const path = Utils.addQueryParams(`${Constant.BEACON_PREFIX}/states/${stateId}/sync_committees`, rest); + return this.get({ path }); + } + + getStateValidator({ stateId, validatorId, ...rest }: ValidatorQuery): Promise> { + const path = Utils.addQueryParams(`${Constant.BEACON_PREFIX}/states/${stateId}/validators/${validatorId}`, rest); + return this.get({ path }); + } + + getStateValidatorBalances({ stateId, ...rest }: ValidatorBalancesQuery): Promise> { + const path = Utils.addQueryParams(`${Constant.BEACON_PREFIX}/states/${stateId}/validator_balances`, rest); + return this.get({ path }); + } + + getStateValidators({ stateId, ...rest }: ValidatorsQuery): Promise> { + const path = Utils.addQueryParams(`${Constant.BEACON_PREFIX}/states/${stateId}/validators`, rest); + return this.get({ path }); + } +} diff --git a/src/service/rpc/evm/BeaconV1EvmRpc.ts b/src/service/rpc/evm/BeaconV1EvmRpc.ts new file mode 100644 index 0000000000..0a8aee7f94 --- /dev/null +++ b/src/service/rpc/evm/BeaconV1EvmRpc.ts @@ -0,0 +1,29 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Container, Service } from 'typedi' +import { LoadBalancer } from '../generic/LoadBalancer' +import { AbstractBeaconV1EvmRpc } from './AbstractBeaconV1EvmRpc' +import { GetI } from '../../../dto/GetI' + +@Service({ + factory: (data: { id: string }) => { + return new BeaconV1EvmRpc(data.id) + }, + transient: true, +}) +export class BeaconV1EvmRpc extends AbstractBeaconV1EvmRpc { + protected readonly loadBalancer: LoadBalancer + + constructor(id: string) { + super() + this.loadBalancer = Container.of(id).get(LoadBalancer) + } + + public destroy() { + this.loadBalancer.destroy() + } + + protected get(get: GetI): Promise { + return this.loadBalancer.get(get) + } + +} diff --git a/src/service/rpc/evm/EvmBeaconArchiveLoadBalancerRpc.ts b/src/service/rpc/evm/EvmBeaconArchiveLoadBalancerRpc.ts new file mode 100644 index 0000000000..2fc929f0ef --- /dev/null +++ b/src/service/rpc/evm/EvmBeaconArchiveLoadBalancerRpc.ts @@ -0,0 +1,24 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Container, Service } from 'typedi' +import { EvmBasedBeaconRpcSuite } from '../../../dto' +import { LoadBalancer } from '../generic/LoadBalancer' +import { EvmArchiveLoadBalancerRpc } from './EvmArchiveLoadBalancerRpc' +import { BeaconV1EvmRpc } from './BeaconV1EvmRpc' + +@Service({ + factory: (data: { id: string }) => { + return new EvmBeaconArchiveLoadBalancerRpc(data.id) + }, + transient: true, +}) +export class EvmBeaconArchiveLoadBalancerRpc extends EvmArchiveLoadBalancerRpc implements EvmBasedBeaconRpcSuite { + protected readonly loadBalancerRpc: LoadBalancer + public readonly beacon = { + v1: Container.of(this.id).get(BeaconV1EvmRpc), + }; + + constructor(private id: string) { + super(id); + this.loadBalancerRpc = Container.of(id).get(LoadBalancer); + } +} diff --git a/src/service/rpc/generic/LoadBalancer.ts b/src/service/rpc/generic/LoadBalancer.ts index 5c09bebbe7..16112b97fa 100644 --- a/src/service/rpc/generic/LoadBalancer.ts +++ b/src/service/rpc/generic/LoadBalancer.ts @@ -6,6 +6,7 @@ import { PostI } from '../../../dto/PostI' import { AbstractRpcInterface } from '../../../dto/rpc/AbstractJsonRpcInterface' import { CONFIG, Constant, Utils } from '../../../util' import { RpcNode, RpcNodeType } from '../../tatum' +import { GetI } from '../../../dto/GetI' interface RpcStatus { node: { @@ -42,6 +43,20 @@ interface InitRemoteHostsParams { noSSRFCheck?: boolean } +enum RequestType { + RPC = 'RPC', + POST = 'POST', + GET = 'GET', + BATCH = 'BATCH', +} + +interface HandleFailedRpcCallParams { + rpcCall: JsonRpcCall | JsonRpcCall[] | PostI | GetI + e: unknown + nodeType: RpcNodeType + requestType: RequestType +} + @Service({ factory: (data: { id: string }) => { return new LoadBalancer(data.id) @@ -379,11 +394,11 @@ export class LoadBalancer implements AbstractRpcInterface { } } - async handleFailedRpcCall(rpcCall: JsonRpcCall | JsonRpcCall[] | PostI, e: unknown, nodeType: RpcNodeType) { + async handleFailedRpcCall({rpcCall, e, nodeType, requestType}: HandleFailedRpcCallParams) { const { rpc: rpcConfig } = Container.of(this.id).get(CONFIG) const { url } = this.getActiveUrl(nodeType) const activeIndex = this.getActiveIndex(nodeType) - if ('method' in rpcCall && typeof rpcCall.method === 'string') { + if (requestType === RequestType.RPC && 'method' in rpcCall) { Utils.log({ id: this.id, message: `Failed to call RPC ${rpcCall.method} on ${url}. Error: ${JSON.stringify( @@ -391,7 +406,7 @@ export class LoadBalancer implements AbstractRpcInterface { Object.getOwnPropertyNames(e), )}`, }) - } else if (Array.isArray(rpcCall) && rpcCall.every((item) => 'method' in item)) { + } else if (requestType === RequestType.BATCH && Array.isArray(rpcCall)) { const methods = rpcCall.map((item) => item.method).join(', ') Utils.log({ id: this.id, @@ -400,13 +415,21 @@ export class LoadBalancer implements AbstractRpcInterface { Object.getOwnPropertyNames(e), )}`, }) - } else if ('path' in rpcCall && typeof rpcCall.path === 'string') { + } else if (requestType === RequestType.POST && 'path' in rpcCall && 'body' in rpcCall) { Utils.log({ id: this.id, message: `Failed to call request on url ${rpcCall.basePath}${rpcCall.path} with body ${JSON.stringify( rpcCall.body, )}. Error: ${JSON.stringify(e, Object.getOwnPropertyNames(e))}`, }) + } else if (requestType === RequestType.GET && 'path' in rpcCall) { + Utils.log({ + id: this.id, + message: `Failed to call request on url ${rpcCall.basePath}${rpcCall.path}. Error: ${JSON.stringify( + e, + Object.getOwnPropertyNames(e), + )}`, + }) } else { // Handle other cases Utils.log({ @@ -464,7 +487,7 @@ export class LoadBalancer implements AbstractRpcInterface { }) return await this.connector.rpcCall(url, rpcCall) } catch (e) { - await this.handleFailedRpcCall(rpcCall, e, type) + await this.handleFailedRpcCall({ rpcCall, e, nodeType: type, requestType: RequestType.RPC }) return await this.rawRpcCall(rpcCall) } } @@ -474,21 +497,38 @@ export class LoadBalancer implements AbstractRpcInterface { try { return await this.connector.rpcCall(url, rpcCall) } catch (e) { - await this.handleFailedRpcCall(rpcCall, e, type) + await this.handleFailedRpcCall({ rpcCall, e, nodeType: type, requestType: RequestType.BATCH }) return await this.rawBatchRpcCall(rpcCall) } } async post({ path, body, basePath }: PostI): Promise { const { url, type } = this.getActiveNormalUrlWithFallback() + const basePathUrl= basePath ?? url try { - return await this.connector.post({ basePath: basePath ?? url, path, body }) + return await this.connector.post({ basePath: basePathUrl, path, body }) } catch (e) { - await this.handleFailedRpcCall({ path, body, basePath }, e, type) + await this.handleFailedRpcCall({ + rpcCall: { path, body, basePath: basePathUrl }, + e, + nodeType: type, + requestType: RequestType.POST, + }) return await this.post({ path, body, basePath }) } } + async get({ path, basePath }: GetI): Promise { + const { url, type } = this.getActiveNormalUrlWithFallback() + const basePathUrl= basePath ?? url + try { + return await this.connector.get({ basePath: basePathUrl, path }) + } catch (e) { + await this.handleFailedRpcCall({ rpcCall: { path, basePath: basePathUrl }, e, nodeType: type, requestType: RequestType.GET }) + return await this.get({ path, basePath }) + } + } + getRpcNodeUrl(): string { return this.getActiveNormalUrlWithFallback().url } diff --git a/src/service/tatum/tatum.evm.ts b/src/service/tatum/tatum.evm.ts index b3317b92c8..68fbd94d76 100644 --- a/src/service/tatum/tatum.evm.ts +++ b/src/service/tatum/tatum.evm.ts @@ -1,5 +1,5 @@ import { Container } from 'typedi' -import { EvmBasedRpcSuite } from '../../dto' +import { EvmBasedBeaconRpcSuite, EvmBasedRpcSuite } from '../../dto' import { NativeEvmBasedRpcSuite } from '../../dto/rpc/NativeEvmBasedRpcInterface' import { CONFIG, Utils } from '../../util' import { Address } from '../address' @@ -99,7 +99,14 @@ export class Klaytn extends NotificationEvm { } // Full support for chains -export class Ethereum extends FullEvm {} +export class Ethereum extends FullEvm { + rpc: EvmBasedBeaconRpcSuite + + constructor(id: string) { + super(id) + this.rpc = Utils.getRpc(id, Container.of(id).get(CONFIG)) + } +} export class Polygon extends FullEvm {} export class Celo extends FullEvm {} export class BinanceSmartChain extends FullEvm {} diff --git a/src/util/constant.ts b/src/util/constant.ts index 34ef2b556e..281655943c 100644 --- a/src/util/constant.ts +++ b/src/util/constant.ts @@ -357,4 +357,5 @@ export const Constant = { RPC: 'https://api.shasta.trongrid.io/jsonrpc', }, EOS_PREFIX: 'v1/chain/', + BEACON_PREFIX: '/eth/v1/beacon', } diff --git a/src/util/util.shared.ts b/src/util/util.shared.ts index 42a3330333..cf45cd473f 100644 --- a/src/util/util.shared.ts +++ b/src/util/util.shared.ts @@ -6,6 +6,7 @@ import { AddressEventNotificationChain, isEosLoadBalancerNetwork, isEosNetwork, + isEvmArchiveNonArchiveBeaconLoadBalancerNetwork, isEvmArchiveNonArchiveLoadBalancerNetwork, isEvmBasedNetwork, isEvmLoadBalancerNetwork, @@ -77,6 +78,7 @@ import { XrpLoadBalancerRpc } from '../service/rpc/other/XrpLoadBalancerRpc' import { UtxoLoadBalancerRpc } from '../service/rpc/utxo/UtxoLoadBalancerRpc' import { Constant } from './constant' import { CONFIG } from './di.tokens' +import { EvmBeaconArchiveLoadBalancerRpc } from '../service/rpc/evm/EvmBeaconArchiveLoadBalancerRpc' import { UtxoLoadBalancerRpcEstimateFee } from '../service/rpc/utxo/UtxoLoadBalancerRpcEstimateFee' import { UtxoRpcEstimateFee } from '../service/rpc/utxo/UtxoRpcEstimateFee' @@ -99,6 +101,10 @@ export const Utils = { return Container.of(id).get(NativeEvmArchiveLoadBalancerRpc) as T } + if (isEvmArchiveNonArchiveBeaconLoadBalancerNetwork(network)) { + return Container.of(id).get(EvmBeaconArchiveLoadBalancerRpc) as T + } + if (isEvmArchiveNonArchiveLoadBalancerNetwork(network)) { return Container.of(id).get(EvmArchiveLoadBalancerRpc) as T } @@ -557,4 +563,28 @@ export const Utils = { } return rpc?.nodes?.[0].url || `${Constant.TATUM_API_URL.V3}blockchain/node/${network}`.concat(path || '') }, + addQueryParams: (basePath: string, queryParams?: Record): string => { + let queryString = ''; + + if (queryParams) { + const query = Utils.convertObjCamelToSnake(queryParams); + const params: string[] = []; + + Object.entries(query).forEach(([key, value]) => { + if (Array.isArray(value)) { + value.forEach(val => { + params.push(`${encodeURIComponent(key)}=${encodeURIComponent(val)}`); + }); + } else { + params.push(`${encodeURIComponent(key)}=${encodeURIComponent(value as string)}`); + } + }); + + if (params.length > 0) { + queryString = '?' + params.join('&'); + } + } + + return basePath + queryString; + }, } diff --git a/yarn.lock b/yarn.lock index c39d4d1c8a..a1427eef57 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1437,6 +1437,11 @@ cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" +data-uri-to-buffer@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz#d8feb2b2881e6a4f58c2e08acfd0e2834e26222e" + integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A== + debug@^4.1.0, debug@^4.1.1: version "4.3.2" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" @@ -1735,6 +1740,14 @@ fb-watchman@^2.0.0: dependencies: bser "2.1.1" +fetch-blob@^3.1.2, fetch-blob@^3.1.4: + version "3.2.0" + resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.2.0.tgz#f09b8d4bbd45adc6f0c20b7e787e793e309dcce9" + integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ== + dependencies: + node-domexception "^1.0.0" + web-streams-polyfill "^3.0.3" + file-entry-cache@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" @@ -1770,6 +1783,13 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.2.tgz#64bfed5cb68fe3ca78b3eb214ad97b63bedce561" integrity sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA== +formdata-polyfill@^4.0.10: + version "4.0.10" + resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423" + integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g== + dependencies: + fetch-blob "^3.1.2" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -2564,6 +2584,20 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= +node-domexception@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" + integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== + +node-fetch@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.3.2.tgz#d1e889bacdf733b4ff3b2b243eb7a12866a0b78b" + integrity sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA== + dependencies: + data-uri-to-buffer "^4.0.0" + fetch-blob "^3.1.4" + formdata-polyfill "^4.0.10" + node-int64@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" @@ -3141,6 +3175,11 @@ walker@^1.0.8: dependencies: makeerror "1.0.12" +web-streams-polyfill@^3.0.3: + version "3.2.1" + resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6" + integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q== + which@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"