Skip to content

Commit

Permalink
509 implement rpc methods high priority 5 (#543)
Browse files Browse the repository at this point in the history
* 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ò <[email protected]>
  • Loading branch information
Valazan and rodolfopietro97 authored Feb 12, 2024
1 parent 1144487 commit f5ee48d
Show file tree
Hide file tree
Showing 21 changed files with 1,159 additions and 124 deletions.
17 changes: 16 additions & 1 deletion packages/core/src/utils/data/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand All @@ -216,5 +230,6 @@ export const dataUtils = {
isNumeric,
isThorId,
encodeBytes32String,
decodeBytes32String
decodeBytes32String,
generateRandomHexOfLength
};
39 changes: 39 additions & 0 deletions packages/core/tests/utils/data/data.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});
1 change: 1 addition & 0 deletions packages/network/src/thor-client/contracts/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './contracts-module';
export * from './types.d';
export * from './model';
1 change: 1 addition & 0 deletions packages/network/src/thor-client/contracts/model/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { ContractFactory } from './contract-factory';
export { Contract } from './contract';
6 changes: 3 additions & 3 deletions packages/network/src/utils/poll/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
37 changes: 30 additions & 7 deletions packages/network/tests/thor-client/contracts/contract.solo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand Down Expand Up @@ -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.
*/
Expand All @@ -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;

Expand Down Expand Up @@ -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);
Expand All @@ -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
Expand Down Expand Up @@ -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 =
Expand All @@ -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(
Expand Down
1 change: 1 addition & 0 deletions packages/provider/src/providers/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const POLLING_INTERVAL = 5000;
113 changes: 112 additions & 1 deletion packages/provider/src/providers/types.d.ts
Original file line number Diff line number Diff line change
@@ -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<string, Subscription>;

/**
* 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.
Expand Down Expand Up @@ -79,4 +185,9 @@ interface ContractRunner {
sendTransaction?: (tx: TransactionRequest) => Promise<TransactionResponse>;
}

export { type EventEmitterable };
export {
type EventEmitterable,
type SubscriptionEvent,
type SubscriptionManager,
type FilterOptions
};
Loading

1 comment on commit f5ee48d

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test Coverage

Summary

Lines Statements Branches Functions
Coverage: 100%
100% (2475/2475) 100% (497/497) 100% (525/525)
Title Tests Skipped Failures Errors Time
core 409 0 💤 0 ❌ 0 🔥 1m 7s ⏱️
network 234 0 💤 0 ❌ 0 🔥 2m 46s ⏱️
errors 43 0 💤 0 ❌ 0 🔥 9.73s ⏱️

Please sign in to comment.