@vechain/sdk-network
Version:
This module serves as the standard interface connecting decentralized applications (dApps) and users to the VeChainThor blockchain
321 lines (292 loc) • 12 kB
text/typescript
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<SubscriptionEvent[]>;
/**
* 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<unknown> {
// 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<SubscriptionEvent[]> | 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<SubscriptionEvent[]>} 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<SubscriptionEvent[]> {
// 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<CompressedBlockDetail | null> {
// 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<VeChainSigner | null> {
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<null | string> {
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<null | string> {
return await vnsUtils.lookupAddress(this.thorClient, address);
}
}
export { VeChainProvider };