import { HexInt } from '@vechain/sdk-core'; import { JSONRPCMethodNotFound, JSONRPCMethodNotImplemented } from '@vechain/sdk-errors'; import { EventEmitter } from 'events'; import { type VeChainSigner } from '../../../signer'; import { type CompressedBlockDetail, type ThorClient } from '../../../thor-client'; import { type EventPoll, Poll, vnsUtils } from '../../../utils'; import { type EIP1193ProviderMessage, type EIP1193RequestArguments } from '../../eip1193'; import { type ProviderInternalWallet } from '../../helpers'; import { ethGetLogs, POLLING_INTERVAL, RPC_METHODS, RPCMethodsMap } from '../../utils'; import { type FilterOptions, type SubscriptionEvent, type SubscriptionManager } from './types'; /** * 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 * * @param thorClient - ThorClient instance. * @param wallet - ProviderInternalWallet instance. It is optional because the majority of the methods do not require a wallet. * @param enableDelegation - Enable fee delegation or not. * @throws {JSONRPCInvalidParams} * */ constructor( readonly thorClient: ThorClient, readonly wallet?: ProviderInternalWallet, readonly enableDelegation: boolean = false ) { super(); } /** * Destroys the provider by closing the thorClient and stopping the provider poll instance if present. * This is because thorClient and the provider might be initialized with a polling interval. */ public destroy(): void { this.thorClient.destroy(); if (this.pollInstance !== undefined) { this.pollInstance.stopListen(); this.pollInstance = undefined; } } /** * This method is used to send a request to the provider. * Basically, it is a wrapper around the RPCMethodsMap. * * @param args - Method and parameters to be used for the request. * @returns The result of the request. * @throws {JSONRPCMethodNotFound} */ public async request(args: EIP1193RequestArguments): Promise { // Check if the method is supported if ( !Object.values(RPC_METHODS) .map((key) => key.toString()) .includes(args.method) ) { const error = new JSONRPCMethodNotFound( 'VeChainProvider.request()', 'Method not found', { code: -32601, message: 'Method not found' } ); // Override the error message with our custom formatted message throw error; } const methodsMap = RPCMethodsMap(this.thorClient, this); // If method is in enum but not in map, throw "not implemented" if (!(args.method in methodsMap)) { throw new JSONRPCMethodNotImplemented( args.method, 'Method not implemented', { code: -32004, message: 'Method not supported' } ); } // Get the method from the RPCMethodsMap and call it return await methodsMap[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. */ public startSubscriptionsPolling(): boolean { let result = false; if (this.pollInstance === undefined) { 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(); result = true; } return result; } /** * Stops the polling mechanism for subscription events. * This method stops the polling mechanism for subscription events, if it is active. * * @returns {boolean} A boolean indicating whether the polling mechanism was stopped. */ public stopSubscriptionsPolling(): boolean { let result = false; if (this.pollInstance !== undefined) { this.pollInstance.stopListen(); this.pollInstance = undefined; result = true; } return result; } /** * Checks if there are active subscriptions. * This method checks if there are any active log subscriptions or a new heads subscription. * * @returns {boolean} A boolean indicating whether there are active subscriptions. */ public isThereActiveSubscriptions(): boolean { return ( this.subscriptionManager.logSubscriptions.size > 0 || this.subscriptionManager.newHeadsSubscription !== undefined ); } /** * Returns the poll instance for subscriptions. */ public getPollInstance(): EventPoll | undefined { return this.pollInstance; } /** * 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 = HexInt.of( this.subscriptionManager.currentBlockNumber ).toString(); // 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 ); } /** * Fetches the current block details from the VeChain node. * * @private */ private async getCurrentBlock(): Promise { // Initialize the result to null, indicating no block found initially let result: CompressedBlockDetail | null = null; // Proceed only if there are active log subscriptions or a new heads subscription is present if (this.isThereActiveSubscriptions()) { // Fetch the block details for the current block number const block = await this.thorClient.blocks.getBlockCompressed( this.subscriptionManager.currentBlockNumber ); // If the block is successfully fetched (not undefined or null), update the result. 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; } /** * Get a signer into the internal wallet provider * for the given address. * * @param addressOrIndex - Address of index of the account. * @returns The signer for the given address. */ async getSigner( addressOrIndex?: string | number ): Promise { if (this.wallet === undefined) { return null; } return await this.wallet?.getSigner(this, addressOrIndex); } /** * Use vet.domains to resolve name to address * @param vnsName - The name to resolve * @returns the address for a name or null */ async resolveName(vnsName: string): Promise { return await vnsUtils.resolveName(this.thorClient, vnsName); } /** * Use vet.domains to look up a verified primary name for an address * @param address - The address to lookup * @returns the primary name for an address or null */ async lookupAddress(address: string): Promise { return await vnsUtils.lookupAddress(this.thorClient, address); } } export { VeChainProvider };