Skip to content

Commit

Permalink
waitForBlock() (#310)
Browse files Browse the repository at this point in the history
* feat: add waitForBlock method

* fix: add test

* fix: fix typo

* fix: add comments

* fix: improve naming and tests

* fix: improve assert

* fix: change client to module for block in thor

* fix: use fixture

* fix: fix typos

* fix: add the maximum timeout option

* fix: update waitForBlock

* fix: improve waitForBlock options param

* fix: remove unused test

* fix: fix test
  • Loading branch information
fabiorigam authored Nov 30, 2023
1 parent e9580d0 commit f222463
Show file tree
Hide file tree
Showing 8 changed files with 193 additions and 6 deletions.
60 changes: 60 additions & 0 deletions packages/network/src/clients/thor-client/blocks/blocks-module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { DATA, assert } from '@vechainfoundation/vechain-sdk-errors';
import { type HttpClient, Poll } from '../../../utils';
import { type BlockDetail, BlocksClient } from '../../thorest-client';
import { type WaitForBlockOptions } from './types';

/** The `BlocksModule` class encapsulates functionality for interacting with blocks
* on the VechainThor blockchain.
*/
class BlocksModule {
/**
* Internal blocks client instance used for interacting with block-related endpoints.
*/
private readonly blocksClient: BlocksClient;

/**
* Initializes a new instance of the `NodeModule` class.
* @param httpClient - The HTTP client instance used for making HTTP requests.
*/
constructor(readonly httpClient: HttpClient) {
// Create an instance of BlocksClient using the provided HTTP client
this.blocksClient = new BlocksClient(httpClient);
}

/**
* Synchronously waits for a specific block revision using polling.
*
* @param revision - The block number or ID to wait for.
* @returns A promise that resolves to an object containing the block details.
*/
public async waitForBlock(
blockNumber: number,
options?: WaitForBlockOptions
): Promise<BlockDetail | null> {
assert(
blockNumber === undefined ||
blockNumber === null ||
typeof blockNumber !== 'number' ||
blockNumber >= 0,
DATA.INVALID_DATA_TYPE,
'Invalid blockNumber. The blockNumber must be a number representing a block number.',
{ blockNumber }
);

// Use the Poll.SyncPoll utility to repeatedly call getBestBlock with a specified interval
const block = await Poll.SyncPoll(
async () => await this.blocksClient.getBestBlock(),
{
requestIntervalInMilliseconds: options?.intervalMs,
maximumWaitingTimeInMilliseconds: options?.timeoutMs
}
).waitUntil((result) => {
// Continue polling until the result's block number matches the specified revision
return result != null && result?.number >= blockNumber;
});

return block;
}
}

export { BlocksModule };
2 changes: 2 additions & 0 deletions packages/network/src/clients/thor-client/blocks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './types.d';
export * from './blocks-module';
21 changes: 21 additions & 0 deletions packages/network/src/clients/thor-client/blocks/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/* --- Input options start --- */

/**
* Options for `waitForBlock` method.
*/
interface WaitForBlockOptions {
/**
* Timeout in milliseconds.
* After this time, the method will throw an error.
*/
timeoutMs?: number;
/**
* Interval in milliseconds.
* The method will check the blocks status every `intervalMs` milliseconds.
*/
intervalMs?: number;
}

/* --- Input options end --- */

export type { WaitForBlockOptions };
7 changes: 7 additions & 0 deletions packages/network/src/clients/thor-client/thor-client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { type HttpClient } from '../../utils';
import { NodesModule } from './nodes';
import { BlocksModule } from './blocks';
import { TransactionsModule } from './transactions';

/**
Expand All @@ -14,6 +15,11 @@ class ThorClient {
public readonly nodes: NodesModule;

/**
* The `BlocksModule` instance
*/
public readonly blocks: BlocksModule;

/*
* The `TransactionsModule` instance
*/
public readonly transactions: TransactionsModule;
Expand All @@ -25,6 +31,7 @@ class ThorClient {
*/
constructor(protected readonly httpClient: HttpClient) {
this.nodes = new NodesModule(httpClient);
this.blocks = new BlocksModule(httpClient);
this.transactions = new TransactionsModule(httpClient);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { describe, expect, test } from '@jest/globals';
import { thorClient, thorestClient } from '../../../fixture';
import { waitForBlockTestCases } from './fixture';

/**
* Blocks Module integration tests
*
* @group integration/clients/thor-client/blocks
*/
describe('Blocks Module', () => {
/**
* Test suite for waitForBlock method
*/
describe('waitForBlock', () => {
waitForBlockTestCases.forEach(({ description, options }) => {
test(
description,
async () => {
// Get best block
const bestBlock = await thorestClient.blocks.getBestBlock();
if (bestBlock != null) {
const expectedBlock =
await thorClient.blocks.waitForBlock(
bestBlock?.number + 1,
options
);
expect(expectedBlock?.number).toBe(
bestBlock?.number + 1
);
}
},
15000
);
});
});

test('waitForBlock - invalid blockNumber', async () => {
await expect(
async () => await thorClient.blocks.waitForBlock(-2)
).rejects.toThrowError(
'Invalid blockNumber. The blockNumber must be a number representing a block number.'
);
});

test('waitForBlock - maximumWaitingTimeInMilliseconds', async () => {
// Get best block
const bestBlock = await thorestClient.blocks.getBestBlock();
if (bestBlock != null) {
const block = await thorClient.blocks.waitForBlock(
bestBlock?.number + 2,
{
timeoutMs: 1000
}
);
expect(block).toBeDefined();
}
});
});
39 changes: 39 additions & 0 deletions packages/network/tests/clients/thor-client/blocks/fixture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* waitForBlock test cases
*/
const waitForBlockTestCases = [
{
description:
'Should wait for block without timeoutMs and intervalMs and return BlockDetail',
options: {
timeoutMs: undefined,
intervalMs: undefined
}
},
{
description:
'Should wait for block with timeoutMs and return BlockDetail',
options: {
timeoutMs: 10000,
intervalMs: undefined
}
},
{
description:
'Should wait for transaction with intervalMs and return BlockDetail',
options: {
timeoutMs: undefined,
intervalMs: 1000
}
},
{
description:
'Should wait for transaction with intervalMs & timeoutMs and return BlockDetail',
options: {
timeoutMs: 10000,
intervalMs: 1000
}
}
];

export { waitForBlockTestCases };
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import { InvalidDataTypeError } from '@vechainfoundation/vechain-sdk-errors';
import { ThorClient } from '../../../../src/clients/thor-client';

/**
* Node unit tests
* @group unit/clients/thor-client/nodes
* Node integration tests
* @group integration/clients/thor-client/nodes
*
*/
describe('Unit tests to check the Node health check is working for different scenarios', () => {
describe('Integration tests to check the Node health check is working for different scenarios', () => {
/**
* @internal
* a well-formed URL to ensure we get to the axios call in the node health check
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, test } from '@jest/globals';
import { thorSoloClient } from '../../../fixture';
import { thorClient } from '../../../fixture';
import { HttpClient } from '../../../../src';
import { HTTPClientError } from '@vechainfoundation/vechain-sdk-errors';
import { ThorClient } from '../../../../src/clients/thor-client';
Expand Down Expand Up @@ -32,8 +32,8 @@ describe('Integration tests to check the Node health check for different scenari
});

test('valid and available synchronized node', async () => {
const healtyNode = await thorSoloClient.nodes.isHealthy();
expect(healtyNode).toBe(true);
const healthyNode = await thorClient.nodes.isHealthy();
expect(healthyNode).toBe(true);
});

test('null or empty URL or blank URL', async () => {
Expand Down

1 comment on commit f222463

@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% (1244/1244) 100% (273/273) 100% (263/263)
Title Tests Skipped Failures Errors Time
core 347 0 💤 0 ❌ 0 🔥 1m 16s ⏱️
network 90 0 💤 0 ❌ 0 🔥 1m 27s ⏱️
errors 30 0 💤 0 ❌ 0 🔥 8.5s ⏱️

Please sign in to comment.