From f5ee48da70c0f04841a100f7448203203143d8b2 Mon Sep 17 00:00:00 2001 From: Valazan Date: Mon, 12 Feb 2024 13:10:23 +0100 Subject: [PATCH] 509 implement rpc methods high priority 5 (#543) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: eth_subscribe for newHeads events * feat: adding a subscription manager to the provider * feat: adding multiple log subscriptions * feat: adding eth_unsubscribe method * fix: get best block in eth_subscribe * test: generate random hex * feat: adding provider poll instance stop listen * feat: updating the format of emitted events * test: erc20 contract logs * test: multiple log subscriptions * feat: implementation of eth_unsubscribe response * test: improving eth_unsubscribe tests * refactor: remove subscriptionHelper and add generateRandomHex to dataUtils * refactor: log events waiting in provider tests * test: updating timeout for asynchronous operations in provider tests * test: updating timeout for asynchronous operations in provider tests * refactor: improve provider blocks handling * fix: block number increment in provider * test: no provider error in eth_subscribe and eth_unsubscribe * refactor: type import for EventPoll and add contract creation test case * docs: removing 'to implement' comment for eth_subscribe and eth_unsubscribe * test: adding more expects on block subscription * refactor: renaming generateRandomHex to generateRandomHexOfLength * docs: renaming generateRandomHex to generateRandomHexOfLength --------- Co-authored-by: Rodolfo Pietro CalabrĂ² <33911400+rodolfopietro97@users.noreply.github.com> --- packages/core/src/utils/data/data.ts | 17 +- packages/core/tests/utils/data/data.test.ts | 39 +++ .../src/thor-client/contracts/index.ts | 1 + .../src/thor-client/contracts/model/index.ts | 1 + packages/network/src/utils/poll/index.ts | 6 +- .../contracts/contract.solo.test.ts | 37 ++- packages/provider/src/providers/constants.ts | 1 + packages/provider/src/providers/types.d.ts | 113 +++++++- .../src/providers/vechain-provider.ts | 163 ++++++++++- .../src/utils/const/rpc-mapper/rpc-methods.ts | 4 +- .../methods-map/methods/eth_subscribe.ts | 120 +++++++-- .../methods-map/methods/eth_unsubscribe.ts | 77 ++++-- .../src/utils/rpc-mapper/rpc-mapper.ts | 9 +- packages/provider/tests/providers/fixture.ts | 211 ++++++++++++++- packages/provider/tests/providers/helpers.ts | 48 ++++ .../providers/vechain-provider.solo.test.ts | 252 +++++++++++++++++- .../vechain-provider.testnet.test.ts | 30 ++- .../methods/eth_accounts/eth_accounts.test.ts | 1 + .../eth_requestAccounts.test.ts | 3 +- .../eth_subscribe/eth_subscribe.test.ts | 70 +++-- .../eth_unsubscribe/eth_unsubscribe.test.ts | 80 ++++-- 21 files changed, 1159 insertions(+), 124 deletions(-) create mode 100644 packages/provider/src/providers/constants.ts create mode 100644 packages/provider/tests/providers/helpers.ts diff --git a/packages/core/src/utils/data/data.ts b/packages/core/src/utils/data/data.ts index 84fcfc3c3..a67e50f8d 100644 --- a/packages/core/src/utils/data/data.ts +++ b/packages/core/src/utils/data/data.ts @@ -8,6 +8,7 @@ import { } from '../const'; import { type HexConfig } from './types'; import { assert, buildError, DATA } from '@vechain/vechain-sdk-errors'; +import * as crypto from 'crypto'; /** * Convert data to a hexadecimal string representation. @@ -207,6 +208,19 @@ const decodeBytes32String = (value: string): string => { } }; +/** + * Generates a random hexadecimal string of a specified length. + * + * @param stringLength - The length of the hexadecimal string to generate. This is twice the number of bytes that will be generated, since each byte is represented by two hexadecimal characters. + * @returns A random hexadecimal string of the specified length. + */ +const generateRandomHexOfLength = (stringLength: number): string => { + // Ensure the number of bytes generated is half the size of the desired hex string length + // since each byte will be converted to two hex characters. + const bytes = Math.ceil(stringLength / 2); + return crypto.randomBytes(bytes).toString('hex').substring(0, stringLength); +}; + export const dataUtils = { toHexString, isHexString, @@ -216,5 +230,6 @@ export const dataUtils = { isNumeric, isThorId, encodeBytes32String, - decodeBytes32String + decodeBytes32String, + generateRandomHexOfLength }; diff --git a/packages/core/tests/utils/data/data.test.ts b/packages/core/tests/utils/data/data.test.ts index 15089a42f..35f94a40e 100644 --- a/packages/core/tests/utils/data/data.test.ts +++ b/packages/core/tests/utils/data/data.test.ts @@ -243,4 +243,43 @@ describe('utils/hex', () => { ); }); }); + + /** + * Test suite for the `generateRandomHexOfLength` method of the `subscriptionHelper`. + * This suite verifies that the method behaves as expected under various conditions. + * @group unit/helpers/subscription + */ + describe('subscriptionHelper.generateRandomHexOfLength', () => { + /** + * Tests that the `generateRandomHexOfLength` method returns a string of the correct length. + * The length of the generated string should match the specified size. + */ + test('should return a string of the correct length', () => { + const size = 8; + const hex = dataUtils.generateRandomHexOfLength(size); + expect(hex).toHaveLength(size); + }); + + /** + * Ensures that the `generateRandomHexOfLength` method produces a string containing only valid hexadecimal characters. + * The output should match a regular expression that allows only characters 0-9 and a-f. + */ + test('should only contain hexadecimal characters', () => { + const size = 8; + const hex = dataUtils.generateRandomHexOfLength(size); + // This regex matches strings that only contain characters 0-9 and a-f + expect(hex).toMatch(/^[0-9a-f]+$/); + }); + + /** + * Verifies that consecutive calls to `generateRandomHexOfLength` return different values. + * This test confirms the randomness and uniqueness of the generated strings over multiple invocations. + */ + test('should return different values on subsequent calls', () => { + const size = 8; + const hex1 = dataUtils.generateRandomHexOfLength(size); + const hex2 = dataUtils.generateRandomHexOfLength(size); + expect(hex1).not.toEqual(hex2); + }); + }); }); diff --git a/packages/network/src/thor-client/contracts/index.ts b/packages/network/src/thor-client/contracts/index.ts index 902d1e89a..71a6cbcb8 100644 --- a/packages/network/src/thor-client/contracts/index.ts +++ b/packages/network/src/thor-client/contracts/index.ts @@ -1,2 +1,3 @@ export * from './contracts-module'; export * from './types.d'; +export * from './model'; diff --git a/packages/network/src/thor-client/contracts/model/index.ts b/packages/network/src/thor-client/contracts/model/index.ts index 9e7b4d864..2a3d5f0b8 100644 --- a/packages/network/src/thor-client/contracts/model/index.ts +++ b/packages/network/src/thor-client/contracts/model/index.ts @@ -1 +1,2 @@ export { ContractFactory } from './contract-factory'; +export { Contract } from './contract'; diff --git a/packages/network/src/utils/poll/index.ts b/packages/network/src/utils/poll/index.ts index 887c8d123..f41152bf7 100644 --- a/packages/network/src/utils/poll/index.ts +++ b/packages/network/src/utils/poll/index.ts @@ -2,10 +2,10 @@ import { SyncPoll } from './sync'; // Asynchronous Event Polling -import { createEventPoll, EventPoll } from './event'; +import { createEventPoll, type EventPoll } from './event'; // Types export * from './types.d'; -const Poll = { SyncPoll, EventPoll, createEventPoll }; -export { Poll }; +const Poll = { SyncPoll, createEventPoll }; +export { Poll, type EventPoll }; diff --git a/packages/network/tests/thor-client/contracts/contract.solo.test.ts b/packages/network/tests/thor-client/contracts/contract.solo.test.ts index 44acf979a..a1dad6425 100644 --- a/packages/network/tests/thor-client/contracts/contract.solo.test.ts +++ b/packages/network/tests/thor-client/contracts/contract.solo.test.ts @@ -14,8 +14,12 @@ import { testingContractTestCases } from './fixture'; import { addressUtils, type DeployParams } from '@vechain/vechain-sdk-core'; -import { ThorClient, type TransactionReceipt } from '../../../src'; -import { type ContractFactory } from '../../../src/thor-client/contracts/model'; +import { + Contract, + ThorClient, + type TransactionReceipt, + type ContractFactory +} from '../../../src'; import { ContractDeploymentFailedError } from '@vechain/vechain-sdk-errors'; /** @@ -55,6 +59,20 @@ describe('ThorClient - Contracts', () => { return await contractFactory.startDeployment(deployParams); } + /** + * Test case for deploying a smart contract using the contract factory. + */ + test('create a new contract', () => { + // Poll until the transaction receipt is available + const contract: Contract = new Contract( + '0x123', + deployedContractAbi, + thorSoloClient + ); + expect(contract.address).toBeDefined(); + expect(contract.abi).toBeDefined(); + }, 10000); + /** * Test case for deploying a smart contract using the contract factory. */ @@ -63,7 +81,12 @@ describe('ThorClient - Contracts', () => { const response = await createExampleContractFactory(); // Poll until the transaction receipt is available - const contract = await response.waitForDeployment(); + const contract: Contract = await response.waitForDeployment(); + + expect(contract.address).toBeDefined(); + expect(contract.abi).toBeDefined(); + expect(contract.deployTransactionReceipt).toBeDefined(); + // Extract the contract address from the transaction receipt const contractAddress = contract.address; @@ -133,7 +156,7 @@ describe('ThorClient - Contracts', () => { expect(factory.getDeployTransaction()).not.toBe(undefined); - const contract = await factory.waitForDeployment(); + const contract: Contract = await factory.waitForDeployment(); expect(contract.address).not.toBe(null); expect(addressUtils.isAddress(contract.address)).toBe(true); @@ -159,7 +182,7 @@ describe('ThorClient - Contracts', () => { factory = await factory.startDeployment(); - const contract = await factory.waitForDeployment(); + const contract: Contract = await factory.waitForDeployment(); // Execute a 'transfer' transaction on the deployed contract, // transferring a specified amount of tokens @@ -206,7 +229,7 @@ describe('ThorClient - Contracts', () => { const factory = await createExampleContractFactory(); // Wait for the deployment to complete and obtain the contract instance - const contract = await factory.waitForDeployment(); + const contract: Contract = await factory.waitForDeployment(); // Retrieve the bytecode of the deployed contract const contractBytecodeResponse = @@ -224,7 +247,7 @@ describe('ThorClient - Contracts', () => { const factory = await createExampleContractFactory(); // Wait for the deployment to complete and obtain the contract instance - const contract = await factory.waitForDeployment(); + const contract: Contract = await factory.waitForDeployment(); const callFunctionSetResponse = await thorSoloClient.contracts.executeContractTransaction( diff --git a/packages/provider/src/providers/constants.ts b/packages/provider/src/providers/constants.ts new file mode 100644 index 000000000..a836d5a49 --- /dev/null +++ b/packages/provider/src/providers/constants.ts @@ -0,0 +1 @@ +export const POLLING_INTERVAL = 5000; diff --git a/packages/provider/src/providers/types.d.ts b/packages/provider/src/providers/types.d.ts index f360b6efc..0216f1399 100644 --- a/packages/provider/src/providers/types.d.ts +++ b/packages/provider/src/providers/types.d.ts @@ -1,5 +1,111 @@ import { type vechain_sdk_core_ethers } from '@vechain/vechain-sdk-core'; +/** + * Represents the parameters for a subscription. + * This interface includes all necessary details for managing a subscription. + */ +interface SubscriptionParams { + /** + * The unique identifier for the subscription. + * This string uniquely identifies the subscription instance. + */ + readonly subscription: string; + + /** + * The result associated with the subscription. + * This can be of any type and contains the data or outcome that the subscription yields. + */ + readonly result: unknown; +} + +/** + * Describes an event related to a subscription. + * This interface encapsulates the method invoked and the parameters associated with the subscription event. + */ +interface SubscriptionEvent { + /** + * The name of the method associated with the subscription event. + */ + readonly method: string; + + /** + * The parameters associated with the subscription event. + * This includes all necessary details such as the subscription identifier and the result. + */ + readonly params: SubscriptionParams; +} + +/** + * Defines the options used to filter events in a subscription. These options can specify which events to include based on various blockchain parameters. + */ +interface FilterOptions { + /** + * The contract address or addresses to filter for events. + */ + address?: string | string[]; + + /** + * The starting block number (inclusive) from which to begin filtering events. + */ + fromBlock?: string; + + /** + * The ending block number (inclusive) at which to stop filtering events. + */ + toBlock?: string; + + /** + * An array of topic identifiers to filter events. Each event must match all specified topics to be included. + */ + topics?: string[]; + + /** + * The hash of a specific block. If defined, only events from this block are included. + */ + blockhash?: string; +} + +/** + * Represents a subscription to a specific type of data or event within a system. + * This could be used for subscribing to updates or changes in the data. + */ +interface Subscription { + /** + * The type of subscription, indicating what kind of data or events this subscription pertains to. + */ + type: string; + + /** + * Optional configuration options for the subscription that can filter or modify the data received. + */ + options?: FilterOptions; +} + +interface NewHeadsSubscription { + readonly subscriptionId: string; + readonly subscription: Subscription; +} + +/** + * Manages multiple subscriptions within a system, keeping track of active subscriptions and the current block number. + */ +interface SubscriptionManager { + /** + * A map of subscription identifiers to Subscription objects, keeping track of all log-related subscriptions. + */ + logSubscriptions: Map; + + /** + * An optional collection of subscriptions specifically for new block headers, indexed by a unique identifier. + */ + newHeadsSubscription?: NewHeadsSubscription; + + /** + * The most recent block number that has been processed or observed by the manager, serving as a point of reference for new events. + */ + currentBlockNumber: number; +} + /** * An `EventEmitterable` interface is akin to an EventEmitter, * but with asynchronous access to its methods. @@ -79,4 +185,9 @@ interface ContractRunner { sendTransaction?: (tx: TransactionRequest) => Promise; } -export { type EventEmitterable }; +export { + type EventEmitterable, + type SubscriptionEvent, + type SubscriptionManager, + type FilterOptions +}; diff --git a/packages/provider/src/providers/vechain-provider.ts b/packages/provider/src/providers/vechain-provider.ts index 6218c144b..20f711ac6 100644 --- a/packages/provider/src/providers/vechain-provider.ts +++ b/packages/provider/src/providers/vechain-provider.ts @@ -4,14 +4,37 @@ import { type EIP1193RequestArguments } from '../eip1193'; import { assert, DATA } from '@vechain/vechain-sdk-errors'; -import { type ThorClient } from '@vechain/vechain-sdk-network'; -import { RPC_METHODS, RPCMethodsMap } from '../utils'; +import { + type BlockDetail, + type EventPoll, + Poll, + type ThorClient +} from '@vechain/vechain-sdk-network'; +import { ethGetLogs, RPC_METHODS, RPCMethodsMap } from '../utils'; import { type Wallet } from '@vechain/vechain-sdk-wallet'; +import { + type FilterOptions, + type SubscriptionEvent, + type SubscriptionManager +} from './types'; +import { POLLING_INTERVAL } from './constants'; +import { vechain_sdk_core_ethers } from '@vechain/vechain-sdk-core'; /** * Our core provider class for vechain */ class VechainProvider extends EventEmitter implements EIP1193ProviderMessage { + public readonly subscriptionManager: SubscriptionManager = { + logSubscriptions: new Map(), + currentBlockNumber: 0 + }; + + /** + * Poll instance for subscriptions + * @private + */ + private pollInstance?: EventPoll; + /** * Constructor for VechainProvider * @@ -23,15 +46,18 @@ class VechainProvider extends EventEmitter implements EIP1193ProviderMessage { readonly wallet?: Wallet ) { super(); + this.startSubscriptionsPolling(); } /** - * Destroys the provider by closing the thorClient - * This is due to the fact that thorClient might be initialized with a polling interval to - * keep the head block updated. + * Destroys the provider by closing the thorClient and stopping the provider poll instance if present. + * This is due to the fact that thorClient and the provider might be initialized with a polling interval. */ public destroy(): void { this.thorClient.destroy(); + if (this.pollInstance !== undefined) { + this.pollInstance.stopListen(); + } } public async request(args: EIP1193RequestArguments): Promise { @@ -46,10 +72,133 @@ class VechainProvider extends EventEmitter implements EIP1193ProviderMessage { ); // Get the method from the RPCMethodsMap and call it - return await RPCMethodsMap(this.thorClient, this.wallet)[args.method]( - args.params as unknown[] + return await RPCMethodsMap(this.thorClient, this, this.wallet)[ + args.method + ](args.params as unknown[]); + } + + /** + * Initializes and starts the polling mechanism for subscription events. + * This method sets up an event poll that periodically checks for new events related to active + * subscriptions, such as 'newHeads' or log subscriptions. When new data is available, it emits + * these events to listeners. + * + * This method leverages the `Poll.createEventPoll` utility to create the polling mechanism, + * which is then started by invoking `startListen` on the poll instance. + */ + private startSubscriptionsPolling(): void { + this.pollInstance = Poll.createEventPoll(async () => { + const data: SubscriptionEvent[] = []; + + const currentBlock = await this.getCurrentBlock(); + + if (currentBlock !== null) { + if ( + this.subscriptionManager.newHeadsSubscription !== undefined + ) { + data.push({ + method: 'eth_subscription', + params: { + subscription: + this.subscriptionManager.newHeadsSubscription + .subscriptionId, + result: currentBlock + } + }); + } + if (this.subscriptionManager.logSubscriptions.size > 0) { + const logs = await this.getLogsRPC(); + data.push(...logs); + } + + this.subscriptionManager.currentBlockNumber++; + } + return data; + }, POLLING_INTERVAL).onData( + (subscriptionEvents: SubscriptionEvent[]) => { + subscriptionEvents.forEach((event) => { + this.emit('message', event); + }); + } + ); + + this.pollInstance.startListen(); + } + + /** + * Fetches logs for all active log subscriptions managed by `subscriptionManager`. + * This method iterates over each log subscription, constructs filter options based on the + * subscription details, and then queries for logs using these filter options. + * + * Each log query is performed asynchronously, and the method waits for all queries to complete + * before returning. The result for each subscription is encapsulated in a `SubscriptionEvent` + * object, which includes the subscription ID and the fetched logs. + * + * This function is intended to be called when there's a need to update or fetch the latest + * logs for all active subscriptions, typically in response to a new block being mined or + * at regular intervals to keep subscription data up to date. + * + * @returns {Promise} A promise that resolves to an array of `SubscriptionEvent` + * objects, each containing the subscription ID and the corresponding logs fetched for that + * subscription. The promise resolves to an empty array if there are no active log subscriptions. + */ + private async getLogsRPC(): Promise { + // Convert the logSubscriptions Map to an array of promises, each promise corresponds to a log fetch operation + const promises = Array.from( + this.subscriptionManager.logSubscriptions.entries() + ).map(async ([subscriptionId, subscriptionDetails]) => { + const currentBlock = vechain_sdk_core_ethers.toQuantity( + this.subscriptionManager.currentBlockNumber + ); + // Construct filter options for the Ethereum logs query based on the subscription details + const filterOptions: FilterOptions = { + address: subscriptionDetails.options?.address, // Contract address to filter the logs by + fromBlock: currentBlock, + toBlock: currentBlock, + topics: subscriptionDetails.options?.topics // Topics to filter the logs by + }; + + // Fetch logs based on the filter options and construct a SubscriptionEvent object + return { + method: 'eth_subscription', + params: { + subscription: subscriptionId, // Subscription ID + result: await ethGetLogs(this.thorClient, [filterOptions]) // The actual log data fetched from Ethereum node + } + }; + }); + + // Wait for all log fetch operations to complete and return an array of SubscriptionEvent objects + const subscriptionEvents = await Promise.all(promises); + // Filter out empty results + return subscriptionEvents.filter( + (event) => event.params.result.length > 0 ); } + + private async getCurrentBlock(): Promise { + // Initialize result to null, indicating no block found initially + let result: BlockDetail | null = null; + + // Proceed only if there are active log subscriptions or a new heads subscription is present + if ( + this.subscriptionManager.logSubscriptions.size > 0 || + this.subscriptionManager.newHeadsSubscription !== undefined + ) { + // Fetch the block details for the current block number + const block = await this.thorClient.blocks.getBlock( + this.subscriptionManager.currentBlockNumber + ); + + // If the block is successfully fetched (not undefined or null), update the result and increment the block number + if (block !== undefined && block !== null) { + result = block; // Set the fetched block as the result + } + } + + // Return the fetched block details or null if no block was fetched + return result; + } } export { VechainProvider }; diff --git a/packages/provider/src/utils/const/rpc-mapper/rpc-methods.ts b/packages/provider/src/utils/const/rpc-mapper/rpc-methods.ts index 3bf920095..d402ff14e 100644 --- a/packages/provider/src/utils/const/rpc-mapper/rpc-methods.ts +++ b/packages/provider/src/utils/const/rpc-mapper/rpc-methods.ts @@ -48,8 +48,8 @@ enum RPC_METHODS { eth_syncing = 'eth_syncing', net_version = 'net_version', web3_clientVersion = 'web3_clientVersion', - eth_subscribe = 'eth_subscribe', // TEMPORARY COMMENT - TO IMPLEMENT - eth_unsubscribe = 'eth_unsubscribe', // TEMPORARY COMMENT - TO IMPLEMENT + eth_subscribe = 'eth_subscribe', + eth_unsubscribe = 'eth_unsubscribe', debug_traceTransaction = 'debug_traceTransaction', // TEMPORARY COMMENT - TO IMPLEMENT debug_traceCall = 'debug_traceCall', // TEMPORARY COMMENT - TO IMPLEMENT evm_mine = 'evm_mine', diff --git a/packages/provider/src/utils/rpc-mapper/methods-map/methods/eth_subscribe.ts b/packages/provider/src/utils/rpc-mapper/methods-map/methods/eth_subscribe.ts index 1327bffdc..d1d175e6d 100644 --- a/packages/provider/src/utils/rpc-mapper/methods-map/methods/eth_subscribe.ts +++ b/packages/provider/src/utils/rpc-mapper/methods-map/methods/eth_subscribe.ts @@ -1,32 +1,110 @@ import { type ThorClient } from '@vechain/vechain-sdk-network'; -import { buildError, FUNCTION } from '@vechain/vechain-sdk-errors'; +import { + type FilterOptions, + type VechainProvider +} from '../../../../providers'; +import { buildError, ERROR_CODES } from '@vechain/vechain-sdk-errors'; +import { dataUtils } from '@vechain/vechain-sdk-core'; /** - * RPC Method eth_subscribe implementation + * Enumerates the types of subscriptions supported by the`eth_subscribe` RPC method. + */ +enum SUBSCRIPTION_TYPE { + /** + * Subscription type for receiving notifications about new blocks added to the blockchain. + */ + NEW_HEADS = 'newHeads', + + /** + * Subscription type for receiving log entries that match specific filter criteria, + * allowing clients to listen for specific events emitted by smart contracts. + */ + LOGS = 'logs' +} + +/** + * Defines the parameter types accepted by the `eth_subscribe` RPC method. + */ +type ethSubscribeParams = [SUBSCRIPTION_TYPE, string | string[]] | unknown[]; + +/** + * Initiates a subscription to the blockchain events based on the specified parameters. + * This function supports subscriptions to new block headers ('newHeads') and log entries ('logs') + * that match given filter criteria. It ensures that the provided parameters are valid and that + * the provider is available before setting up the subscription and generating a unique subscription ID. * - * @param thorClient - The thor client instance to use. - * @param params - The standard array of rpc call parameters. - * @note: - * * params[0]: ... - * * params[1]: ... - * * params[n]: ... + * @param thorClient - An instance of `ThorClient` used to interact with the blockchain, such as + * retrieving the current best block when setting up a new subscription. + * @param params - Parameters for the subscription, conforming to `ethSubscribeParams`. The first + * element of the array specifies the type of subscription, and the second element + * (if present) provides additional options, such as filter criteria for log subscriptions. + * @param provider - An optional `VechainProvider` instance that contains the subscription manager. + * The subscription manager is used to store and manage active subscriptions. + * If the provider is not provided or is undefined, the function throws an error. + * + * @returns A `Promise` that resolves to a string representing the unique ID of the created subscription. + * + * @throws An error if the provider is undefined, indicating that the provider is not available, + * or if the first parameter in `params` is not a valid subscription type. */ const ethSubscribe = async ( thorClient: ThorClient, - params: unknown[] -): Promise => { - // To avoid eslint error - await Promise.resolve(0); - - // Not implemented yet - throw buildError( - FUNCTION.NOT_IMPLEMENTED, - 'Method "eth_subscribe" not not implemented yet', - { - params, - thorClient + params: ethSubscribeParams, + provider?: VechainProvider +): Promise => { + if (provider === undefined) { + throw buildError( + ERROR_CODES.JSONRPC.INTERNAL_ERROR, + 'Provider not available', + { + message: 'The Provider is not defined', + code: -32603 + } + ); + } + if ( + params[0] !== SUBSCRIPTION_TYPE.NEW_HEADS && + params[0] !== SUBSCRIPTION_TYPE.LOGS + ) { + throw buildError( + ERROR_CODES.JSONRPC.INVALID_PARAMS, + 'Invalid subscription type param', + { + message: 'Invalid subscription type param', + code: -32602 + } + ); + } + + // I check if some subscription is already active, if not I set a new starting block number for the subscription + if ( + provider.subscriptionManager.logSubscriptions.size === 0 && + provider.subscriptionManager.newHeadsSubscription === undefined + ) { + const block = await thorClient.blocks.getBestBlock(); + + if (block !== undefined && block !== null) { + provider.subscriptionManager.currentBlockNumber = block.number; } - ); + } + const subscriptionId = dataUtils.generateRandomHexOfLength(32); + + if (params.includes(SUBSCRIPTION_TYPE.NEW_HEADS)) { + provider.subscriptionManager.newHeadsSubscription = { + subscriptionId, + subscription: { + type: SUBSCRIPTION_TYPE.NEW_HEADS + } + }; + } + + if (params.includes(SUBSCRIPTION_TYPE.LOGS)) { + provider.subscriptionManager.logSubscriptions.set(subscriptionId, { + type: SUBSCRIPTION_TYPE.LOGS, + options: params[1] as FilterOptions + }); + } + return subscriptionId; }; export { ethSubscribe }; diff --git a/packages/provider/src/utils/rpc-mapper/methods-map/methods/eth_unsubscribe.ts b/packages/provider/src/utils/rpc-mapper/methods-map/methods/eth_unsubscribe.ts index f1a32323f..f79b854f6 100644 --- a/packages/provider/src/utils/rpc-mapper/methods-map/methods/eth_unsubscribe.ts +++ b/packages/provider/src/utils/rpc-mapper/methods-map/methods/eth_unsubscribe.ts @@ -1,32 +1,63 @@ -import { type ThorClient } from '@vechain/vechain-sdk-network'; -import { buildError, FUNCTION } from '@vechain/vechain-sdk-errors'; +import type { VechainProvider } from '../../../../providers'; +import { buildError, ERROR_CODES } from '@vechain/vechain-sdk-errors'; /** - * RPC Method eth_unsubscribe implementation + * Asynchronously unsubscribes from a vechain event subscription. + * This function attempts to unsubscribe from either 'newHeads' or log subscriptions + * based on the provided `subscriptionId`. If the provider is not available or the + * `subscriptionId` does not match any active subscriptions, it may throw an error + * or return `false`, respectively. * - * @param thorClient - The thor client instance to use. - * @param params - The standard array of rpc call parameters. - * @note: - * * params[0]: ... - * * params[1]: ... - * * params[n]: ... + * @param params - An array containing the subscription ID as its first element. + * The subscription ID is used to identify and unsubscribe from the corresponding + * Ethereum event subscription. + * @param provider - An optional `VechainProvider` instance that contains the + * subscription manager. This manager holds the active subscriptions and is used + * to unsubscribe from them. If the provider is not provided or is undefined, + * the function throws an error indicating that the provider is not available. + * + * @returns A `Promise` that resolves to `true` if the unsubscription was successful, + * or `false` if the specified subscription ID does not match any active subscriptions. + * + * @throws An error with a JSON-RPC internal error code (-32603) if the provider is + * not available. The error includes a message indicating that the provider is + * not defined. */ const ethUnsubscribe = async ( - thorClient: ThorClient, - params: unknown[] -): Promise => { - // To avoid eslint error - await Promise.resolve(0); + params: unknown[], + provider?: VechainProvider +): Promise => { + let result: boolean = false; + if (provider === undefined) { + throw buildError( + ERROR_CODES.JSONRPC.INTERNAL_ERROR, + 'Provider not available', + { + message: 'The Provider is not defined', + code: -32603 + } + ); + } + const subscriptionId = params[0] as string; + + // Unsubscribe from 'newHeads' events if the subscription ID matches the newHeads subscription + if ( + provider.subscriptionManager.newHeadsSubscription !== undefined && + subscriptionId === + provider.subscriptionManager.newHeadsSubscription.subscriptionId + ) { + provider.subscriptionManager.newHeadsSubscription = undefined; + result = true; + } + // Unsubscribe from log events if the subscription ID matches a log subscription + else { + result = + provider.subscriptionManager.logSubscriptions.delete( + subscriptionId + ); + } - // Not implemented yet - throw buildError( - FUNCTION.NOT_IMPLEMENTED, - 'Method "eth_unsubscribe" not not implemented yet', - { - params, - thorClient - } - ); + return await Promise.resolve(result); }; export { ethUnsubscribe }; diff --git a/packages/provider/src/utils/rpc-mapper/rpc-mapper.ts b/packages/provider/src/utils/rpc-mapper/rpc-mapper.ts index fbf765c81..fcf673f50 100644 --- a/packages/provider/src/utils/rpc-mapper/rpc-mapper.ts +++ b/packages/provider/src/utils/rpc-mapper/rpc-mapper.ts @@ -86,6 +86,7 @@ import { import { type Wallet } from '@vechain/vechain-sdk-wallet'; import { ethRequestAccounts } from './methods-map/methods/eth_requestAccounts'; import { type LogsRPC } from '../formatter/logs'; +import { type VechainProvider } from '../../providers'; /** * Map of RPC methods to their implementations with our SDK. @@ -97,9 +98,11 @@ import { type LogsRPC } from '../formatter/logs'; * * @param thorClient - ThorClient instance. * @param wallet - Wallet instance. It is optional because the majority of the methods do not require a wallet. + * @param provider */ const RPCMethodsMap = ( thorClient: ThorClient, + provider?: VechainProvider, wallet?: Wallet ): Record> => { /** @@ -200,12 +203,12 @@ const RPCMethodsMap = ( return await web3ClientVersion(); }, - [RPC_METHODS.eth_subscribe]: async (params) => { - await ethSubscribe(thorClient, params); + [RPC_METHODS.eth_subscribe]: async (params): Promise => { + return await ethSubscribe(thorClient, params, provider); }, [RPC_METHODS.eth_unsubscribe]: async (params) => { - await ethUnsubscribe(thorClient, params); + return await ethUnsubscribe(params, provider); }, [RPC_METHODS.debug_traceTransaction]: async (params) => { diff --git a/packages/provider/tests/providers/fixture.ts b/packages/provider/tests/providers/fixture.ts index da49e3710..5d78d1458 100644 --- a/packages/provider/tests/providers/fixture.ts +++ b/packages/provider/tests/providers/fixture.ts @@ -1,4 +1,8 @@ -import { vechain_sdk_core_ethers, unitsUtils } from '@vechain/vechain-sdk-core'; +import { + vechain_sdk_core_ethers, + unitsUtils, + type InterfaceAbi +} from '@vechain/vechain-sdk-core'; import { zeroBlock } from '../rpc-mapper/methods/eth_getBlockByNumber/fixture'; import { TESTING_CONTRACT_ADDRESS, @@ -55,6 +59,20 @@ const providerMethodsTestCasesSolo = [ } ]; +const logsInput = { + address: [ + '0x0000000000000000000000000000456e65726779', + '0x0000000000000000000000000000456e65726779' + ], + fromBlock: vechain_sdk_core_ethers.toQuantity(0), + toBlock: vechain_sdk_core_ethers.toQuantity(100000), + topics: [ + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', + '0x0000000000000000000000005034aa590125b64023a0262112b98d72e3c8e40e', + '0x0000000000000000000000005034aa590125b64023a0262112b98d72e3c8e40e' + ] +}; + /** * Test cases for provider methods - Mainnet */ @@ -73,8 +91,197 @@ const providerMethodsTestCasesMainnet = [ } ]; +const ERC20_BYTECODE: string = + '0x60806040523480156200001157600080fd5b506040518060400160405280600b81526020017f53616d706c65546f6b656e0000000000000000000000000000000000000000008152506040518060400160405280600281526020017f535400000000000000000000000000000000000000000000000000000000000081525081600390816200008f91906200062c565b508060049081620000a191906200062c565b505050620000e633620000b9620000ec60201b60201c565b60ff16600a620000ca919062000896565b620f4240620000da9190620008e7565b620000f560201b60201c565b62000a3a565b60006012905090565b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff16036200016a5760006040517fec442f0500000000000000000000000000000000000000000000000000000000815260040162000161919062000977565b60405180910390fd5b6200017e600083836200018260201b60201c565b5050565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff1603620001d8578060026000828254620001cb919062000994565b92505081905550620002ae565b60008060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205490508181101562000267578381836040517fe450d38c0000000000000000000000000000000000000000000000000000000081526004016200025e93929190620009e0565b60405180910390fd5b8181036000808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550505b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1603620002f9578060026000828254039250508190555062000346565b806000808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055505b8173ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef83604051620003a5919062000a1d565b60405180910390a3505050565b600081519050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b600060028204905060018216806200043457607f821691505b6020821081036200044a5762000449620003ec565b5b50919050565b60008190508160005260206000209050919050565b60006020601f8301049050919050565b600082821b905092915050565b600060088302620004b47fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8262000475565b620004c0868362000475565b95508019841693508086168417925050509392505050565b6000819050919050565b6000819050919050565b60006200050d620005076200050184620004d8565b620004e2565b620004d8565b9050919050565b6000819050919050565b6200052983620004ec565b62000541620005388262000514565b84845462000482565b825550505050565b600090565b6200055862000549565b620005658184846200051e565b505050565b5b818110156200058d57620005816000826200054e565b6001810190506200056b565b5050565b601f821115620005dc57620005a68162000450565b620005b18462000465565b81016020851015620005c1578190505b620005d9620005d08562000465565b8301826200056a565b50505b505050565b600082821c905092915050565b60006200060160001984600802620005e1565b1980831691505092915050565b60006200061c8383620005ee565b9150826002028217905092915050565b6200063782620003b2565b67ffffffffffffffff811115620006535762000652620003bd565b5b6200065f82546200041b565b6200066c82828562000591565b600060209050601f831160018114620006a457600084156200068f578287015190505b6200069b85826200060e565b8655506200070b565b601f198416620006b48662000450565b60005b82811015620006de57848901518255600182019150602085019450602081019050620006b7565b86831015620006fe5784890151620006fa601f891682620005ee565b8355505b6001600288020188555050505b505050505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60008160011c9050919050565b6000808291508390505b6001851115620007a15780860481111562000779576200077862000713565b5b6001851615620007895780820291505b8081029050620007998562000742565b945062000759565b94509492505050565b600082620007bc57600190506200088f565b81620007cc57600090506200088f565b8160018114620007e55760028114620007f05762000826565b60019150506200088f565b60ff84111562000805576200080462000713565b5b8360020a9150848211156200081f576200081e62000713565b5b506200088f565b5060208310610133831016604e8410600b8410161715620008605782820a9050838111156200085a576200085962000713565b5b6200088f565b6200086f84848460016200074f565b9250905081840481111562000889576200088862000713565b5b81810290505b9392505050565b6000620008a382620004d8565b9150620008b083620004d8565b9250620008df7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8484620007aa565b905092915050565b6000620008f482620004d8565b91506200090183620004d8565b92508282026200091181620004d8565b915082820484148315176200092b576200092a62000713565b5b5092915050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006200095f8262000932565b9050919050565b620009718162000952565b82525050565b60006020820190506200098e600083018462000966565b92915050565b6000620009a182620004d8565b9150620009ae83620004d8565b9250828201905080821115620009c957620009c862000713565b5b92915050565b620009da81620004d8565b82525050565b6000606082019050620009f7600083018662000966565b62000a066020830185620009cf565b62000a156040830184620009cf565b949350505050565b600060208201905062000a346000830184620009cf565b92915050565b610e558062000a4a6000396000f3fe608060405234801561001057600080fd5b50600436106100935760003560e01c8063313ce56711610066578063313ce5671461013457806370a082311461015257806395d89b4114610182578063a9059cbb146101a0578063dd62ed3e146101d057610093565b806306fdde0314610098578063095ea7b3146100b657806318160ddd146100e657806323b872dd14610104575b600080fd5b6100a0610200565b6040516100ad9190610aa9565b60405180910390f35b6100d060048036038101906100cb9190610b64565b610292565b6040516100dd9190610bbf565b60405180910390f35b6100ee6102b5565b6040516100fb9190610be9565b60405180910390f35b61011e60048036038101906101199190610c04565b6102bf565b60405161012b9190610bbf565b60405180910390f35b61013c6102ee565b6040516101499190610c73565b60405180910390f35b61016c60048036038101906101679190610c8e565b6102f7565b6040516101799190610be9565b60405180910390f35b61018a61033f565b6040516101979190610aa9565b60405180910390f35b6101ba60048036038101906101b59190610b64565b6103d1565b6040516101c79190610bbf565b60405180910390f35b6101ea60048036038101906101e59190610cbb565b6103f4565b6040516101f79190610be9565b60405180910390f35b60606003805461020f90610d2a565b80601f016020809104026020016040519081016040528092919081815260200182805461023b90610d2a565b80156102885780601f1061025d57610100808354040283529160200191610288565b820191906000526020600020905b81548152906001019060200180831161026b57829003601f168201915b5050505050905090565b60008061029d61047b565b90506102aa818585610483565b600191505092915050565b6000600254905090565b6000806102ca61047b565b90506102d7858285610495565b6102e2858585610529565b60019150509392505050565b60006012905090565b60008060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050919050565b60606004805461034e90610d2a565b80601f016020809104026020016040519081016040528092919081815260200182805461037a90610d2a565b80156103c75780601f1061039c576101008083540402835291602001916103c7565b820191906000526020600020905b8154815290600101906020018083116103aa57829003601f168201915b5050505050905090565b6000806103dc61047b565b90506103e9818585610529565b600191505092915050565b6000600160008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054905092915050565b600033905090565b610490838383600161061d565b505050565b60006104a184846103f4565b90507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff81146105235781811015610513578281836040517ffb8f41b200000000000000000000000000000000000000000000000000000000815260040161050a93929190610d6a565b60405180910390fd5b6105228484848403600061061d565b5b50505050565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff160361059b5760006040517f96c6fd1e0000000000000000000000000000000000000000000000000000000081526004016105929190610da1565b60405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff160361060d5760006040517fec442f050000000000000000000000000000000000000000000000000000000081526004016106049190610da1565b60405180910390fd5b6106188383836107f4565b505050565b600073ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff160361068f5760006040517fe602df050000000000000000000000000000000000000000000000000000000081526004016106869190610da1565b60405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff16036107015760006040517f94280d620000000000000000000000000000000000000000000000000000000081526004016106f89190610da1565b60405180910390fd5b81600160008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000208190555080156107ee578273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925846040516107e59190610be9565b60405180910390a35b50505050565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff160361084657806002600082825461083a9190610deb565b92505081905550610919565b60008060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050818110156108d2578381836040517fe450d38c0000000000000000000000000000000000000000000000000000000081526004016108c993929190610d6a565b60405180910390fd5b8181036000808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550505b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff160361096257806002600082825403925050819055506109af565b806000808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055505b8173ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef83604051610a0c9190610be9565b60405180910390a3505050565b600081519050919050565b600082825260208201905092915050565b60005b83811015610a53578082015181840152602081019050610a38565b60008484015250505050565b6000601f19601f8301169050919050565b6000610a7b82610a19565b610a858185610a24565b9350610a95818560208601610a35565b610a9e81610a5f565b840191505092915050565b60006020820190508181036000830152610ac38184610a70565b905092915050565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000610afb82610ad0565b9050919050565b610b0b81610af0565b8114610b1657600080fd5b50565b600081359050610b2881610b02565b92915050565b6000819050919050565b610b4181610b2e565b8114610b4c57600080fd5b50565b600081359050610b5e81610b38565b92915050565b60008060408385031215610b7b57610b7a610acb565b5b6000610b8985828601610b19565b9250506020610b9a85828601610b4f565b9150509250929050565b60008115159050919050565b610bb981610ba4565b82525050565b6000602082019050610bd46000830184610bb0565b92915050565b610be381610b2e565b82525050565b6000602082019050610bfe6000830184610bda565b92915050565b600080600060608486031215610c1d57610c1c610acb565b5b6000610c2b86828701610b19565b9350506020610c3c86828701610b19565b9250506040610c4d86828701610b4f565b9150509250925092565b600060ff82169050919050565b610c6d81610c57565b82525050565b6000602082019050610c886000830184610c64565b92915050565b600060208284031215610ca457610ca3610acb565b5b6000610cb284828501610b19565b91505092915050565b60008060408385031215610cd257610cd1610acb565b5b6000610ce085828601610b19565b9250506020610cf185828601610b19565b9150509250929050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b60006002820490506001821680610d4257607f821691505b602082108103610d5557610d54610cfb565b5b50919050565b610d6481610af0565b82525050565b6000606082019050610d7f6000830186610d5b565b610d8c6020830185610bda565b610d996040830184610bda565b949350505050565b6000602082019050610db66000830184610d5b565b92915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b6000610df682610b2e565b9150610e0183610b2e565b9250828201905080821115610e1957610e18610dbc565b5b9291505056fea2646970667358221220912f5265edaea44910db734f0d00fccd257c78dba79c126931551eaad1a334f764736f6c63430008170033'; + +const ERC721_BYTECODE: string = + '0x60806040523480156200001157600080fd5b506040518060400160405280600981526020017f53616d706c654e465400000000000000000000000000000000000000000000008152506040518060400160405280600481526020017f534e46540000000000000000000000000000000000000000000000000000000081525081600090816200008f919062000324565b508060019081620000a1919062000324565b5050506200040b565b600081519050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b600060028204905060018216806200012c57607f821691505b602082108103620001425762000141620000e4565b5b50919050565b60008190508160005260206000209050919050565b60006020601f8301049050919050565b600082821b905092915050565b600060088302620001ac7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff826200016d565b620001b886836200016d565b95508019841693508086168417925050509392505050565b6000819050919050565b6000819050919050565b600062000205620001ff620001f984620001d0565b620001da565b620001d0565b9050919050565b6000819050919050565b6200022183620001e4565b6200023962000230826200020c565b8484546200017a565b825550505050565b600090565b6200025062000241565b6200025d81848462000216565b505050565b5b8181101562000285576200027960008262000246565b60018101905062000263565b5050565b601f821115620002d4576200029e8162000148565b620002a9846200015d565b81016020851015620002b9578190505b620002d1620002c8856200015d565b83018262000262565b50505b505050565b600082821c905092915050565b6000620002f960001984600802620002d9565b1980831691505092915050565b6000620003148383620002e6565b9150826002028217905092915050565b6200032f82620000aa565b67ffffffffffffffff8111156200034b576200034a620000b5565b5b62000357825462000113565b6200036482828562000289565b600060209050601f8311600181146200039c576000841562000387578287015190505b62000393858262000306565b86555062000403565b601f198416620003ac8662000148565b60005b82811015620003d657848901518255600182019150602085019450602081019050620003af565b86831015620003f65784890151620003f2601f891682620002e6565b8355505b6001600288020188555050505b505050505050565b611ea0806200041b6000396000f3fe608060405234801561001057600080fd5b50600436106100ea5760003560e01c8063691c65d41161008c578063a22cb46511610066578063a22cb4651461026f578063b88d4fde1461028b578063c87b56dd146102a7578063e985e9c5146102d7576100ea565b8063691c65d4146101f157806370a082311461022157806395d89b4114610251576100ea565b8063095ea7b3116100c8578063095ea7b31461016d57806323b872dd1461018957806342842e0e146101a55780636352211e146101c1576100ea565b806301ffc9a7146100ef57806306fdde031461011f578063081812fc1461013d575b600080fd5b61010960048036038101906101049190611673565b610307565b60405161011691906116bb565b60405180910390f35b6101276103e9565b6040516101349190611766565b60405180910390f35b610157600480360381019061015291906117be565b61047b565b604051610164919061182c565b60405180910390f35b61018760048036038101906101829190611873565b610497565b005b6101a3600480360381019061019e91906118b3565b6104ad565b005b6101bf60048036038101906101ba91906118b3565b6105af565b005b6101db60048036038101906101d691906117be565b6105cf565b6040516101e8919061182c565b60405180910390f35b61020b60048036038101906102069190611906565b6105e1565b6040516102189190611942565b60405180910390f35b61023b60048036038101906102369190611906565b610610565b6040516102489190611942565b60405180910390f35b6102596106ca565b6040516102669190611766565b60405180910390f35b61028960048036038101906102849190611989565b61075c565b005b6102a560048036038101906102a09190611afe565b610772565b005b6102c160048036038101906102bc91906117be565b61078f565b6040516102ce9190611766565b60405180910390f35b6102f160048036038101906102ec9190611b81565b6107f8565b6040516102fe91906116bb565b60405180910390f35b60007f80ac58cd000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916827bffffffffffffffffffffffffffffffffffffffffffffffffffffffff191614806103d257507f5b5e139f000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916827bffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916145b806103e257506103e18261088c565b5b9050919050565b6060600080546103f890611bf0565b80601f016020809104026020016040519081016040528092919081815260200182805461042490611bf0565b80156104715780601f1061044657610100808354040283529160200191610471565b820191906000526020600020905b81548152906001019060200180831161045457829003601f168201915b5050505050905090565b6000610486826108f6565b506104908261097e565b9050919050565b6104a982826104a46109bb565b6109c3565b5050565b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff160361051f5760006040517f64a0ae92000000000000000000000000000000000000000000000000000000008152600401610516919061182c565b60405180910390fd5b6000610533838361052e6109bb565b6109d5565b90508373ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff16146105a9578382826040517f64283d7b0000000000000000000000000000000000000000000000000000000081526004016105a093929190611c21565b60405180910390fd5b50505050565b6105ca83838360405180602001604052806000815250610772565b505050565b60006105da826108f6565b9050919050565b600080600660008154809291906105f790611c87565b9190505590506106078382610bef565b80915050919050565b60008073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff16036106835760006040517f89c62b6400000000000000000000000000000000000000000000000000000000815260040161067a919061182c565b60405180910390fd5b600360008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050919050565b6060600180546106d990611bf0565b80601f016020809104026020016040519081016040528092919081815260200182805461070590611bf0565b80156107525780601f1061072757610100808354040283529160200191610752565b820191906000526020600020905b81548152906001019060200180831161073557829003601f168201915b5050505050905090565b61076e6107676109bb565b8383610c0d565b5050565b61077d8484846104ad565b61078984848484610d7c565b50505050565b606061079a826108f6565b5060006107a5610f33565b905060008151116107c557604051806020016040528060008152506107f0565b806107cf84610f4a565b6040516020016107e0929190611d0b565b6040516020818303038152906040525b915050919050565b6000600560008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060009054906101000a900460ff16905092915050565b60007f01ffc9a7000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916827bffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916149050919050565b60008061090283611018565b9050600073ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff160361097557826040517f7e27328900000000000000000000000000000000000000000000000000000000815260040161096c9190611942565b60405180910390fd5b80915050919050565b60006004600083815260200190815260200160002060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff169050919050565b600033905090565b6109d08383836001611055565b505050565b6000806109e184611018565b9050600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff1614610a2357610a2281848661121a565b5b600073ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff1614610ab457610a65600085600080611055565b6001600360008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825403925050819055505b600073ffffffffffffffffffffffffffffffffffffffff168573ffffffffffffffffffffffffffffffffffffffff1614610b37576001600360008773ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055505b846002600086815260200190815260200160002060006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550838573ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef60405160405180910390a4809150509392505050565b610c098282604051806020016040528060008152506112de565b5050565b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1603610c7e57816040517f5b08ba18000000000000000000000000000000000000000000000000000000008152600401610c75919061182c565b60405180910390fd5b80600560008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060006101000a81548160ff0219169083151502179055508173ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167f17307eab39ab6107e8899845ad3d59bd9653f200f220920489ca2b5937696c3183604051610d6f91906116bb565b60405180910390a3505050565b60008373ffffffffffffffffffffffffffffffffffffffff163b1115610f2d578273ffffffffffffffffffffffffffffffffffffffff1663150b7a02610dc06109bb565b8685856040518563ffffffff1660e01b8152600401610de29493929190611d84565b6020604051808303816000875af1925050508015610e1e57506040513d601f19601f82011682018060405250810190610e1b9190611de5565b60015b610ea2573d8060008114610e4e576040519150601f19603f3d011682016040523d82523d6000602084013e610e53565b606091505b506000815103610e9a57836040517f64a0ae92000000000000000000000000000000000000000000000000000000008152600401610e91919061182c565b60405180910390fd5b805181602001fd5b63150b7a0260e01b7bffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916817bffffffffffffffffffffffffffffffffffffffffffffffffffffffff191614610f2b57836040517f64a0ae92000000000000000000000000000000000000000000000000000000008152600401610f22919061182c565b60405180910390fd5b505b50505050565b606060405180602001604052806000815250905090565b606060006001610f59846112fa565b01905060008167ffffffffffffffff811115610f7857610f776119d3565b5b6040519080825280601f01601f191660200182016040528015610faa5781602001600182028036833780820191505090505b509050600082602001820190505b60011561100d578080600190039150507f3031323334353637383961626364656600000000000000000000000000000000600a86061a8153600a858161100157611000611e12565b5b04945060008503610fb8575b819350505050919050565b60006002600083815260200190815260200160002060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff169050919050565b808061108e5750600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1614155b156111c257600061109e846108f6565b9050600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff161415801561110957508273ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff1614155b801561111c575061111a81846107f8565b155b1561115e57826040517fa9fbf51f000000000000000000000000000000000000000000000000000000008152600401611155919061182c565b60405180910390fd5b81156111c057838573ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92560405160405180910390a45b505b836004600085815260200190815260200160002060006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555050505050565b61122583838361144d565b6112d957600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff160361129a57806040517f7e2732890000000000000000000000000000000000000000000000000000000081526004016112919190611942565b60405180910390fd5b81816040517f177e802f0000000000000000000000000000000000000000000000000000000081526004016112d0929190611e41565b60405180910390fd5b505050565b6112e8838361150e565b6112f56000848484610d7c565b505050565b600080600090507a184f03e93ff9f4daa797ed6e38ed64bf6a1f0100000000000000008310611358577a184f03e93ff9f4daa797ed6e38ed64bf6a1f010000000000000000838161134e5761134d611e12565b5b0492506040810190505b6d04ee2d6d415b85acef81000000008310611395576d04ee2d6d415b85acef8100000000838161138b5761138a611e12565b5b0492506020810190505b662386f26fc1000083106113c457662386f26fc1000083816113ba576113b9611e12565b5b0492506010810190505b6305f5e10083106113ed576305f5e10083816113e3576113e2611e12565b5b0492506008810190505b612710831061141257612710838161140857611407611e12565b5b0492506004810190505b60648310611435576064838161142b5761142a611e12565b5b0492506002810190505b600a8310611444576001810190505b80915050919050565b60008073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff161415801561150557508273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff1614806114c657506114c584846107f8565b5b8061150457508273ffffffffffffffffffffffffffffffffffffffff166114ec8361097e565b73ffffffffffffffffffffffffffffffffffffffff16145b5b90509392505050565b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff16036115805760006040517f64a0ae92000000000000000000000000000000000000000000000000000000008152600401611577919061182c565b60405180910390fd5b600061158e838360006109d5565b9050600073ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff16146116025760006040517f73c6ac6e0000000000000000000000000000000000000000000000000000000081526004016115f9919061182c565b60405180910390fd5b505050565b6000604051905090565b600080fd5b600080fd5b60007fffffffff0000000000000000000000000000000000000000000000000000000082169050919050565b6116508161161b565b811461165b57600080fd5b50565b60008135905061166d81611647565b92915050565b60006020828403121561168957611688611611565b5b60006116978482850161165e565b91505092915050565b60008115159050919050565b6116b5816116a0565b82525050565b60006020820190506116d060008301846116ac565b92915050565b600081519050919050565b600082825260208201905092915050565b60005b838110156117105780820151818401526020810190506116f5565b60008484015250505050565b6000601f19601f8301169050919050565b6000611738826116d6565b61174281856116e1565b93506117528185602086016116f2565b61175b8161171c565b840191505092915050565b60006020820190508181036000830152611780818461172d565b905092915050565b6000819050919050565b61179b81611788565b81146117a657600080fd5b50565b6000813590506117b881611792565b92915050565b6000602082840312156117d4576117d3611611565b5b60006117e2848285016117a9565b91505092915050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000611816826117eb565b9050919050565b6118268161180b565b82525050565b6000602082019050611841600083018461181d565b92915050565b6118508161180b565b811461185b57600080fd5b50565b60008135905061186d81611847565b92915050565b6000806040838503121561188a57611889611611565b5b60006118988582860161185e565b92505060206118a9858286016117a9565b9150509250929050565b6000806000606084860312156118cc576118cb611611565b5b60006118da8682870161185e565b93505060206118eb8682870161185e565b92505060406118fc868287016117a9565b9150509250925092565b60006020828403121561191c5761191b611611565b5b600061192a8482850161185e565b91505092915050565b61193c81611788565b82525050565b60006020820190506119576000830184611933565b92915050565b611966816116a0565b811461197157600080fd5b50565b6000813590506119838161195d565b92915050565b600080604083850312156119a05761199f611611565b5b60006119ae8582860161185e565b92505060206119bf85828601611974565b9150509250929050565b600080fd5b600080fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b611a0b8261171c565b810181811067ffffffffffffffff82111715611a2a57611a296119d3565b5b80604052505050565b6000611a3d611607565b9050611a498282611a02565b919050565b600067ffffffffffffffff821115611a6957611a686119d3565b5b611a728261171c565b9050602081019050919050565b82818337600083830152505050565b6000611aa1611a9c84611a4e565b611a33565b905082815260208101848484011115611abd57611abc6119ce565b5b611ac8848285611a7f565b509392505050565b600082601f830112611ae557611ae46119c9565b5b8135611af5848260208601611a8e565b91505092915050565b60008060008060808587031215611b1857611b17611611565b5b6000611b268782880161185e565b9450506020611b378782880161185e565b9350506040611b48878288016117a9565b925050606085013567ffffffffffffffff811115611b6957611b68611616565b5b611b7587828801611ad0565b91505092959194509250565b60008060408385031215611b9857611b97611611565b5b6000611ba68582860161185e565b9250506020611bb78582860161185e565b9150509250929050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b60006002820490506001821680611c0857607f821691505b602082108103611c1b57611c1a611bc1565b5b50919050565b6000606082019050611c36600083018661181d565b611c436020830185611933565b611c50604083018461181d565b949350505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b6000611c9282611788565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8203611cc457611cc3611c58565b5b600182019050919050565b600081905092915050565b6000611ce5826116d6565b611cef8185611ccf565b9350611cff8185602086016116f2565b80840191505092915050565b6000611d178285611cda565b9150611d238284611cda565b91508190509392505050565b600081519050919050565b600082825260208201905092915050565b6000611d5682611d2f565b611d608185611d3a565b9350611d708185602086016116f2565b611d798161171c565b840191505092915050565b6000608082019050611d99600083018761181d565b611da6602083018661181d565b611db36040830185611933565b8181036060830152611dc58184611d4b565b905095945050505050565b600081519050611ddf81611647565b92915050565b600060208284031215611dfb57611dfa611611565b5b6000611e0984828501611dd0565b91505092915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601260045260246000fd5b6000604082019050611e56600083018561181d565b611e636020830184611933565b939250505056fea2646970667358221220af86505f0580469a6f5c34114f42782e33b1678efb535f9aeaeb0817e598ec2c64736f6c63430008160033'; + +const ERC20_ABI: InterfaceAbi = [ + { inputs: [], stateMutability: 'nonpayable', type: 'constructor' }, + { + inputs: [ + { internalType: 'address', name: 'spender', type: 'address' }, + { internalType: 'uint256', name: 'allowance', type: 'uint256' }, + { internalType: 'uint256', name: 'needed', type: 'uint256' } + ], + name: 'ERC20InsufficientAllowance', + type: 'error' + }, + { + inputs: [ + { internalType: 'address', name: 'sender', type: 'address' }, + { internalType: 'uint256', name: 'balance', type: 'uint256' }, + { internalType: 'uint256', name: 'needed', type: 'uint256' } + ], + name: 'ERC20InsufficientBalance', + type: 'error' + }, + { + inputs: [ + { internalType: 'address', name: 'approver', type: 'address' } + ], + name: 'ERC20InvalidApprover', + type: 'error' + }, + { + inputs: [ + { internalType: 'address', name: 'receiver', type: 'address' } + ], + name: 'ERC20InvalidReceiver', + type: 'error' + }, + { + inputs: [{ internalType: 'address', name: 'sender', type: 'address' }], + name: 'ERC20InvalidSender', + type: 'error' + }, + { + inputs: [{ internalType: 'address', name: 'spender', type: 'address' }], + name: 'ERC20InvalidSpender', + type: 'error' + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'owner', + type: 'address' + }, + { + indexed: true, + internalType: 'address', + name: 'spender', + type: 'address' + }, + { + indexed: false, + internalType: 'uint256', + name: 'value', + type: 'uint256' + } + ], + name: 'Approval', + type: 'event' + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'from', + type: 'address' + }, + { + indexed: true, + internalType: 'address', + name: 'to', + type: 'address' + }, + { + indexed: false, + internalType: 'uint256', + name: 'value', + type: 'uint256' + } + ], + name: 'Transfer', + type: 'event' + }, + { + inputs: [ + { internalType: 'address', name: 'owner', type: 'address' }, + { internalType: 'address', name: 'spender', type: 'address' } + ], + name: 'allowance', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [ + { internalType: 'address', name: 'spender', type: 'address' }, + { internalType: 'uint256', name: 'value', type: 'uint256' } + ], + name: 'approve', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [{ internalType: 'address', name: 'account', type: 'address' }], + name: 'balanceOf', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [], + name: 'decimals', + outputs: [{ internalType: 'uint8', name: '', type: 'uint8' }], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [], + name: 'name', + outputs: [{ internalType: 'string', name: '', type: 'string' }], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [], + name: 'symbol', + outputs: [{ internalType: 'string', name: '', type: 'string' }], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [], + name: 'totalSupply', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [ + { internalType: 'address', name: 'to', type: 'address' }, + { internalType: 'uint256', name: 'value', type: 'uint256' } + ], + name: 'transfer', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [ + { internalType: 'address', name: 'from', type: 'address' }, + { internalType: 'address', name: 'to', type: 'address' }, + { internalType: 'uint256', name: 'value', type: 'uint256' } + ], + name: 'transferFrom', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function' + } +]; + +const TEST_ACCOUNT = { + privateKey: + '706e6acd567fdc22db54aead12cb39db01c4832f149f95299aa8dd8bef7d28ff', + address: '0xf02f557c753edf5fcdcbfe4c1c3a448b3cc84d54' +}; + export { providerMethodsTestCasesTestnet, providerMethodsTestCasesSolo, - providerMethodsTestCasesMainnet + providerMethodsTestCasesMainnet, + logsInput, + ERC20_BYTECODE, + ERC20_ABI, + ERC721_BYTECODE, + TEST_ACCOUNT }; diff --git a/packages/provider/tests/providers/helpers.ts b/packages/provider/tests/providers/helpers.ts new file mode 100644 index 000000000..7c7d538b5 --- /dev/null +++ b/packages/provider/tests/providers/helpers.ts @@ -0,0 +1,48 @@ +import { type SubscriptionEvent, type VechainProvider } from '../../src'; +import { + ERC20_ABI, + ERC20_BYTECODE, + ERC721_BYTECODE, + TEST_ACCOUNT +} from './fixture'; +import { type Contract, type ThorClient } from '@vechain/vechain-sdk-network'; +import { ERC721_ABI } from '@vechain/vechain-sdk-core'; + +export async function waitForMessage( + provider: VechainProvider +): Promise { + return await new Promise((resolve) => { + provider.on('message', (message) => { + resolve(message as SubscriptionEvent); + provider.destroy(); + }); + }); +} + +export async function deployERC20Contract( + thorClient: ThorClient +): Promise { + const factory = thorClient.contracts.createContractFactory( + ERC20_ABI, + ERC20_BYTECODE, + TEST_ACCOUNT.privateKey + ); + + await factory.startDeployment(); + + return await factory.waitForDeployment(); +} + +export async function deployERC721Contract( + thorClient: ThorClient +): Promise { + const factory = thorClient.contracts.createContractFactory( + ERC721_ABI, + ERC721_BYTECODE, + TEST_ACCOUNT.privateKey + ); + + await factory.startDeployment(); + + return await factory.waitForDeployment(); +} diff --git a/packages/provider/tests/providers/vechain-provider.solo.test.ts b/packages/provider/tests/providers/vechain-provider.solo.test.ts index caebf359e..ee90d4db8 100644 --- a/packages/provider/tests/providers/vechain-provider.solo.test.ts +++ b/packages/provider/tests/providers/vechain-provider.solo.test.ts @@ -1,9 +1,14 @@ import { afterEach, beforeEach, describe, expect, test } from '@jest/globals'; -import { VechainProvider } from '../../src'; +import { type SubscriptionEvent, VechainProvider } from '../../src'; import { InvalidDataTypeError } from '@vechain/vechain-sdk-errors'; import { ThorClient } from '@vechain/vechain-sdk-network'; import { soloNetwork } from '../fixture'; -import { providerMethodsTestCasesSolo } from './fixture'; +import { providerMethodsTestCasesSolo, TEST_ACCOUNT } from './fixture'; +import { + deployERC20Contract, + deployERC721Contract, + waitForMessage +} from './helpers'; /** * Vechain provider tests - Solo Network @@ -26,7 +31,7 @@ describe('Vechain provider tests', () => { }); /** - * Destory thor client and provider after each test + * Destroy thor client and provider after each test */ afterEach(() => { provider.destroy(); @@ -64,6 +69,247 @@ describe('Vechain provider tests', () => { expect(rpcCall).not.toBe('0x0'); }); + /** + * eth_subscribe latest blocks RPC call test + */ + test('Should be able to get to subscribe to the latest blocks', async () => { + // Call RPC function + const subscriptionId = await provider.request({ + method: 'eth_subscribe', + params: ['newHeads'] + }); + + const messageReceived = new Promise((resolve) => { + provider.on('message', (message) => { + resolve(message); + provider.destroy(); + }); + }); + + const message = (await messageReceived) as SubscriptionEvent; + + // Optionally, you can do assertions or other operations with the message + expect(message).toBeDefined(); + expect(message.method).toBe('eth_subscription'); + expect(message.params).toBeDefined(); + expect(message.params.subscription).toBe(subscriptionId); + expect(message.params.result).toBeDefined(); + }, 12000); + + /** + * eth_subscribe latest blocks and then unsubscribe RPC call test + */ + test('Should be able to get to subscribe to the latest blocks and then unsubscribe', async () => { + // Call RPC function + const subscriptionId = await provider.request({ + method: 'eth_subscribe', + params: ['newHeads'] + }); + + expect(subscriptionId).toBeDefined(); + expect( + provider.subscriptionManager.newHeadsSubscription?.subscriptionId + ).toBe(subscriptionId); + + await provider.request({ + method: 'eth_unsubscribe', + params: [subscriptionId] + }); + + expect( + provider.subscriptionManager.newHeadsSubscription?.subscriptionId + ).toBeUndefined(); + }); + + /** + * eth_getSubscribe invalid call + */ + test('Should not be able to subscribe since the subscription type is invalid', async () => { + // Call RPC function + await expect( + async () => + await provider.request({ + method: 'eth_subscribe', + params: ['invalid'] + }) + ).rejects.toThrowError('Invalid subscription type'); + }, 12000); + + /** + * Tests the ability to subscribe to and receive the latest log events from an ERC20 contract using the `eth_subscribe` RPC call. + * + * The test performs the following operations: + * 1. Deploys an ERC20 contract to simulate a real-world token contract scenario. + * 2. Sets up a log subscription for the deployed contract's address to listen for all emitted events (no specific topics). + * 3. Executes a `transfer` transaction on the ERC20 contract to simulate activity that would generate a log event. + * 4. Waits for a message from the subscription, indicating a log event was captured. + * 5. Validates the received message to ensure it contains the expected structure and data: + * - The message is defined and has the correct method. + * - The log event's address matches the ERC20 contract's address. + * - Topics and data within the log event are present and correctly formatted. + * + * @remarks + * - The `waitForMessage` function is assumed to be a utility that returns a Promise which resolves when a new message is received from the subscription. + * - The test uses `@ts-expect-error` annotations to bypass TypeScript's type checking for certain properties we expect to be present in the log event message. This is due to the generic nature of the `message` object, which doesn't have a predefined type that includes the expected fields. + * - An extended timeout of 30 seconds is set to accommodate potential delays in contract deployment, transaction execution, and event propagation. + * + * @throws {Error} If the received message doesn't match the expected format or if the log event details are incorrect, indicating an issue with the subscription or the event emission process. + */ + test('Should be able to get to subscribe to the latest logs of an erc20 contract', async () => { + const contract = await deployERC20Contract(thorClient); + + const logsParams = { + address: [contract.address], + topics: [] + }; + + // Call RPC function to subscribe to logs + const rpcCall = await provider.request({ + method: 'eth_subscribe', + params: ['logs', logsParams] + }); + // Wait for the subscription to receive a message (log event) + const messageReceived = waitForMessage(provider); + + // Execute a contract transaction to generate a log event + await thorClient.contracts.executeContractTransaction( + TEST_ACCOUNT.privateKey, + contract.address, + contract.abi, + 'transfer', + [TEST_ACCOUNT.address, 100] + ); + + const message = await messageReceived; + + // Clean up the subscription + provider.destroy(); + + // Assertions to validate the received message + expect(message).toBeDefined(); + expect(message.method).toBeDefined(); + expect(message.params).toBeDefined(); + + // @ts-expect-error - Asserting that the log event contains the expected contract address + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect(message.params.result[0].address).toBe(contract.address); + + // @ts-expect-error - Asserting that the log event contains defined topics + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect(message.params.result[0].topics).toBeDefined(); + + // @ts-expect-error - Asserting that the log event contains data + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect(message.params.result[0].data).toBeDefined(); + + // Validate the RPC call was successful + expect(rpcCall).not.toBe('0x0'); + }, 30000); // Extended timeout for asynchronous operations + + /** + * Tests the ability to subscribe to and receive log events for both ERC20 and ERC721 token contracts. + * + * This test performs the following operations: + * 1. Deploys an ERC20 and an ERC721 contract to simulate token transactions. + * 2. Sets up subscriptions to listen for log events from both contracts using the `eth_subscribe` method. + * 3. Executes transactions on both contracts: + * - For the ERC20 contract, it simulates a token transfer. + * - For the ERC721 contract, it simulates minting a new token. + * 4. Waits for and collects log events emitted by these transactions. + * 5. Asserts that the received log events match the expected results, including: + * - The presence and count of the log events. + * - Matching subscription IDs to verify the correct source of the events. + * - Non-empty log data to ensure event details are captured. + * + * Note: The test uses `@ts-expect-error` to assert the presence of `params.result` in log events, + * acknowledging potential TypeScript type safety concerns while expecting the data to be present. + * + * @remarks + * The test includes an extended timeout of 10 seconds to accommodate the asynchronous nature of + * blockchain transactions and event subscriptions. + * + * @throws {Error} If any of the assertions fail, indicating a problem with event subscription or log data capture. + */ + test('Should be able to subscribe to the latest logs of an erc20 and erc721 contract', async () => { + // Test setup: Deploy contracts and set up event subscriptions + const erc20Contract = await deployERC20Contract(thorClient); + const erc721Contract = await deployERC721Contract(thorClient); + + const erc20logsParams = { + address: [erc20Contract.address], + topics: [] + }; + + const erc721logsParams = { + address: [erc721Contract.address], + topics: [] + }; + + const erc20Subscription = await provider.request({ + method: 'eth_subscribe', + params: ['logs', erc20logsParams] + }); + + const erc721Subscription = await provider.request({ + method: 'eth_subscribe', + params: ['logs', erc721logsParams] + }); + + // Collect and assert log events + let results: SubscriptionEvent[] = []; + const eventPromise = new Promise((resolve) => { + provider.on('message', (message: SubscriptionEvent) => { + results.push(message); + if (results.length >= 2) { + provider.destroy(); + resolve(results); + } + }); + }); + + // Execute transactions that should emit events + await thorClient.contracts.executeContractTransaction( + TEST_ACCOUNT.privateKey, + erc20Contract.address, + erc20Contract.abi, + 'transfer', + [TEST_ACCOUNT.address, 100] + ); + + await thorClient.contracts.executeContractTransaction( + TEST_ACCOUNT.privateKey, + erc721Contract.address, + erc721Contract.abi, + 'mintItem', + [TEST_ACCOUNT.address] + ); + + results = (await eventPromise) as SubscriptionEvent[]; + + // Assertions to validate the received log events + expect(results).toBeDefined(); + expect(results.length).toBeGreaterThan(1); + expect( + results.filter((x) => x.params.subscription === erc20Subscription) + .length + ).toBeGreaterThan(0); + expect( + results.filter((x) => x.params.subscription === erc721Subscription) + .length + ).toBeGreaterThan(0); + + expect(results[0].method).toBe('eth_subscription'); + expect(results[1].method).toBe('eth_subscription'); + + // @ts-expect-error - Asserting that log data is present + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect(results[0].params.result.length).toBeGreaterThan(0); + + // @ts-expect-error - Asserting that log data is present + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect(results[1].params.result.length).toBeGreaterThan(0); + }, 30000); // Extended timeout for asynchronous operations + /** * Invalid RPC method tests */ diff --git a/packages/provider/tests/providers/vechain-provider.testnet.test.ts b/packages/provider/tests/providers/vechain-provider.testnet.test.ts index 8427e538e..8be0e3a23 100644 --- a/packages/provider/tests/providers/vechain-provider.testnet.test.ts +++ b/packages/provider/tests/providers/vechain-provider.testnet.test.ts @@ -4,6 +4,7 @@ import { InvalidDataTypeError } from '@vechain/vechain-sdk-errors'; import { ThorClient } from '@vechain/vechain-sdk-network'; import { testNetwork } from '../fixture'; import { providerMethodsTestCasesTestnet } from './fixture'; +import { waitForMessage } from './helpers'; /** * Vechain provider tests @@ -51,7 +52,7 @@ describe('Vechain provider tests', () => { ); /** - * eth_getBalance RPC call test + * eth_blockNumber RPC call test */ test('Should be able to get the latest block number', async () => { // Call RPC function @@ -64,6 +65,33 @@ describe('Vechain provider tests', () => { expect(rpcCall).not.toBe('0x0'); }); + /** + * eth_subscribe latest blocks RPC call test + */ + test('Should be able to get to subscribe to the latest blocks', async () => { + // Call RPC function + const rpcCall = await provider.request({ + method: 'eth_subscribe', + params: ['newHeads'] + }); + + const messageReceived = waitForMessage(provider); + + const message = await messageReceived; + + // Optionally, you can do assertions or other operations with the message + expect(message).toBeDefined(); + expect(message.method).toBe('eth_subscription'); + expect(message.params.subscription).toBeDefined(); + + // Compare the result with the expected value + expect(rpcCall).not.toBe('0x0'); + }, 12000); + + /** + * eth_getBalance RPC call test + */ + /** * Invalid RPC method tests */ diff --git a/packages/provider/tests/rpc-mapper/methods/eth_accounts/eth_accounts.test.ts b/packages/provider/tests/rpc-mapper/methods/eth_accounts/eth_accounts.test.ts index e3d4a3150..ff3c0ca8c 100644 --- a/packages/provider/tests/rpc-mapper/methods/eth_accounts/eth_accounts.test.ts +++ b/packages/provider/tests/rpc-mapper/methods/eth_accounts/eth_accounts.test.ts @@ -41,6 +41,7 @@ describe('RPC Mapper - eth_accounts method tests', () => { // Get accounts const accounts = (await RPCMethodsMap( thorClient, + undefined, THOR_SOLO_ACCOUNTS_BASE_WALLET as Wallet )[RPC_METHODS.eth_accounts]([])) as string[]; diff --git a/packages/provider/tests/rpc-mapper/methods/eth_requestAccounts/eth_requestAccounts.test.ts b/packages/provider/tests/rpc-mapper/methods/eth_requestAccounts/eth_requestAccounts.test.ts index f19170f19..52d6901e0 100644 --- a/packages/provider/tests/rpc-mapper/methods/eth_requestAccounts/eth_requestAccounts.test.ts +++ b/packages/provider/tests/rpc-mapper/methods/eth_requestAccounts/eth_requestAccounts.test.ts @@ -42,6 +42,7 @@ describe('RPC Mapper - eth_requestAccounts method tests', () => { // Get accounts const accounts = (await RPCMethodsMap( thorClient, + undefined, THOR_SOLO_ACCOUNTS_BASE_WALLET as Wallet )[RPC_METHODS.eth_requestAccounts]([])) as string[]; @@ -77,7 +78,7 @@ describe('RPC Mapper - eth_requestAccounts method tests', () => { // Error with empty wallet await expect( - RPCMethodsMap(thorClient, emptyBaseWallet)[ + RPCMethodsMap(thorClient, undefined, emptyBaseWallet)[ RPC_METHODS.eth_requestAccounts ]([]) ).rejects.toThrow(ProviderRpcError); diff --git a/packages/provider/tests/rpc-mapper/methods/eth_subscribe/eth_subscribe.test.ts b/packages/provider/tests/rpc-mapper/methods/eth_subscribe/eth_subscribe.test.ts index 13cac93d2..a7229e659 100644 --- a/packages/provider/tests/rpc-mapper/methods/eth_subscribe/eth_subscribe.test.ts +++ b/packages/provider/tests/rpc-mapper/methods/eth_subscribe/eth_subscribe.test.ts @@ -1,8 +1,8 @@ import { afterEach, beforeEach, describe, expect, test } from '@jest/globals'; -import { NotImplementedError } from '@vechain/vechain-sdk-errors'; -import { RPC_METHODS, RPCMethodsMap } from '../../../../src'; import { ThorClient } from '@vechain/vechain-sdk-network'; import { testNetwork } from '../../../fixture'; +import { RPC_METHODS, RPCMethodsMap, VechainProvider } from '../../../../src'; +import { JSONRPCInternalError } from '@vechain/vechain-sdk-errors'; /** * RPC Mapper integration tests for 'eth_subscribe' method @@ -11,58 +11,82 @@ import { testNetwork } from '../../../fixture'; */ describe('RPC Mapper - eth_subscribe method tests', () => { /** - * Thor client instance + * ThorClient and provider instances */ let thorClient: ThorClient; + let provider: VechainProvider; /** - * Init thor client before each test + * Inti thor client and provider before each test */ beforeEach(() => { - // Init thor client thorClient = new ThorClient(testNetwork); + provider = new VechainProvider(thorClient); }); /** - * Destroy thor client after each test + * Destroy thor client and provider after each test */ afterEach(() => { - thorClient.destroy(); + provider.destroy(); }); /** - * eth_subscribe RPC call tests - Positive cases + * Describes the test suite for positive test cases of the `eth_subscribe` RPC method. + * This suite includes various scenarios where `eth_subscribe` is expected to succeed, + * verifying the correct behavior of subscription functionalities for different types + * of events in Ethereum, such as `newHeads`. */ describe('eth_subscribe - Positive cases', () => { /** - * Positive case 1 - ... Description ... + * Tests successful subscription to the 'newHeads' event. + * It verifies that the RPC call to `eth_subscribe` with 'newHeads' as a parameter + * successfully returns a subscription ID, and that the ID has the expected length + * of 32 characters, indicating a valid response format. */ - test('eth_subscribe - positive case 1', async () => { - // NOT IMPLEMENTED YET! + test('eth_subscribe - new latest blocks subscription', async () => { + // Call RPC function + const rpcCall = (await provider.request({ + method: 'eth_subscribe', + params: ['newHeads'] + })) as string; + + // Verify the length of the subscription ID + expect(rpcCall.length).toEqual(32); + }); + + test('eth_subscribe - no provider', async () => { + // Attempts to unsubscribe with no provider and expects an error. await expect( async () => - await RPCMethodsMap(thorClient)[RPC_METHODS.eth_subscribe]([ - -1 - ]) - ).rejects.toThrowError(NotImplementedError); + await RPCMethodsMap(thorClient)[RPC_METHODS.eth_subscribe]( + [] + ) + ).rejects.toThrowError(JSONRPCInternalError); }); }); /** - * eth_subscribe RPC call tests - Negative cases + * Describes the test suite for negative test cases of the `eth_subscribe` RPC method. + * This suite focuses on scenarios where `eth_subscribe` is expected to fail, such as + * when invalid parameters are provided. The aim is to ensure the method handles errors + * gracefully and in accordance with the JSON-RPC specifications. */ describe('eth_subscribe - Negative cases', () => { /** - * Negative case 1 - ... Description ... + * Tests the behavior of `eth_subscribe` when an invalid subscription type is provided. + * The test expects the RPC call to throw an error, demonstrating that the method + * properly validates input parameters and handles invalid requests as per the + * JSON-RPC error handling conventions. */ - test('eth_subscribe - negative case 1', async () => { - // NOT IMPLEMENTED YET! + test('eth_subscribe - invalid subscription', async () => { await expect( async () => - await RPCMethodsMap(thorClient)[RPC_METHODS.eth_subscribe]([ - 'SOME_RANDOM_PARAM' - ]) - ).rejects.toThrowError(NotImplementedError); + await provider.request({ + method: 'eth_subscribe', + params: ['invalidSubscriptionType'] + }) + ).rejects.toThrowError(); // Ideally, specify the expected error for more precise testing. }); }); }); diff --git a/packages/provider/tests/rpc-mapper/methods/eth_unsubscribe/eth_unsubscribe.test.ts b/packages/provider/tests/rpc-mapper/methods/eth_unsubscribe/eth_unsubscribe.test.ts index 01d4e1d99..8a403e008 100644 --- a/packages/provider/tests/rpc-mapper/methods/eth_unsubscribe/eth_unsubscribe.test.ts +++ b/packages/provider/tests/rpc-mapper/methods/eth_unsubscribe/eth_unsubscribe.test.ts @@ -1,8 +1,8 @@ import { afterEach, beforeEach, describe, expect, test } from '@jest/globals'; -import { NotImplementedError } from '@vechain/vechain-sdk-errors'; -import { RPC_METHODS, RPCMethodsMap } from '../../../../src'; import { ThorClient } from '@vechain/vechain-sdk-network'; import { testNetwork } from '../../../fixture'; +import { RPC_METHODS, RPCMethodsMap, VechainProvider } from '../../../../src'; +import { JSONRPCInternalError } from '@vechain/vechain-sdk-errors'; /** * RPC Mapper integration tests for 'eth_unsubscribe' method @@ -11,58 +11,86 @@ import { testNetwork } from '../../../fixture'; */ describe('RPC Mapper - eth_unsubscribe method tests', () => { /** - * Thor client instance + * ThorClient and provider instances */ let thorClient: ThorClient; + let provider: VechainProvider; /** - * Init thor client before each test + * Inti thor client and provider before each test */ beforeEach(() => { - // Init thor client thorClient = new ThorClient(testNetwork); + provider = new VechainProvider(thorClient); }); /** - * Destroy thor client after each test + * Destroy thor client and provider after each test */ afterEach(() => { - thorClient.destroy(); + provider.destroy(); }); - /** - * eth_unsubscribe RPC call tests - Positive cases - */ + // Describes the test suite for positive cases of the `eth_unsubscribe` RPC method. describe('eth_unsubscribe - Positive cases', () => { /** - * Positive case 1 - ... Description ... + * Tests the successful unsubscription from 'newHeads' events. + * It first subscribes to 'newHeads' events using 'eth_subscribe', and then + * attempts to unsubscribe using 'eth_unsubscribe'. The test verifies that + * the unsubscription is successful and the result is as expected. */ test('eth_unsubscribe - positive case 1', async () => { - // NOT IMPLEMENTED YET! - await expect( - async () => - await RPCMethodsMap(thorClient)[ - RPC_METHODS.eth_unsubscribe - ]([-1]) - ).rejects.toThrowError(NotImplementedError); + // Subscribes to 'newHeads' events. + const ethSubscribeResult = (await provider.request({ + method: 'eth_subscribe', + params: ['newHeads'] + })) as string; + + // Checks if the subscription result is defined. + expect(ethSubscribeResult).toBeDefined(); + + // Verifies the 'newHeads' subscription is present in the subscription manager. + expect( + provider.subscriptionManager.newHeadsSubscription + ).toBeDefined(); + + // Unsubscribes from 'newHeads' events. + const ethUnsubscribeResult = (await provider.request({ + method: 'eth_unsubscribe', + params: [ethSubscribeResult] // Should unsubscribe using the subscription ID returned by 'eth_subscribe', not 'newHeads'. + })) as boolean; + + // Asserts the unsubscription result is true, indicating success. + expect(ethUnsubscribeResult).toEqual(true); }); }); - /** - * eth_unsubscribe RPC call tests - Negative cases - */ + // Describes the test suite for negative cases of the `eth_unsubscribe` RPC method. describe('eth_unsubscribe - Negative cases', () => { /** - * Negative case 1 - ... Description ... + * Tests the behavior of 'eth_unsubscribe' with invalid parameters. + * The test attempts to unsubscribe using an invalid subscription ID and expects + * an error to be thrown, specifically a JSONRPCInternalError, indicating the + * failure of the operation due to invalid parameters. */ - test('eth_unsubscribe - negative case 1', async () => { - // NOT IMPLEMENTED YET! + test('eth_unsubscribe - invalid subscription id', async () => { + // Attempts to unsubscribe with an invalid subscription ID and expects an error. + expect( + await provider.request({ + method: 'eth_unsubscribe', + params: ['invalid_subscription_id'] + }) + ).toBe(false); + }); + + test('eth_unsubscribe - no provider', async () => { + // Attempts to unsubscribe with no provider and expects an error. await expect( async () => await RPCMethodsMap(thorClient)[ RPC_METHODS.eth_unsubscribe - ](['SOME_RANDOM_PARAM']) - ).rejects.toThrowError(NotImplementedError); + ]([]) + ).rejects.toThrowError(JSONRPCInternalError); }); }); });