Skip to content

Commit

Permalink
Added, tested and documented getblocks, getFinalizedBlocks
Browse files Browse the repository at this point in the history
  • Loading branch information
rasmus-kirk committed Jan 29, 2023
1 parent 8fbca55 commit 1b6ba9b
Show file tree
Hide file tree
Showing 7 changed files with 232 additions and 17 deletions.
5 changes: 5 additions & 0 deletions packages/common/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## 6.4.0

- Added `getFinalizedBlocks()` & `getBlocks()` GRPCv2 functions.
- Added public helper function `waitForTransactionFinalization()` to client

## 6.3.0

### Added
Expand Down
59 changes: 58 additions & 1 deletion packages/common/src/GRPCClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import * as translate from './GRPCTypeTranslation';
import { AccountAddress } from './types/accountAddress';
import { getAccountTransactionHandler } from './accountTransactions';
import { calculateEnergyCost } from './energyCost';
import { countSignatures } from './util';
import { countSignatures, mapAsyncIterable } from './util';
import {
serializeAccountTransactionPayload,
serializeCredentialDeploymentPayload,
Expand Down Expand Up @@ -305,6 +305,63 @@ export default class ConcordiumNodeClient {
.response;
return Buffer.from(response.value).toString('hex');
}

/**
* Gets a stream of finalized blocks.
*
* @param abortSignal an AbortSignal to close the stream. Note that the
* stream does not close itself as it is infinite, so usually you'd want
* to provide this parameter.
* @returns An AsyncIterator stream of finalized blocks.
*/
getFinalizedBlocks(
abortSignal?: AbortSignal
): AsyncIterable<v1.FinalizedBlockInfo> {
const opts = { abort: abortSignal };
const blocks = this.client.getFinalizedBlocks(v2.Empty, opts).responses;
return mapAsyncIterable(blocks, translate.commonBlockInfo);
}

/**
* Gets a stream of blocks.
*
* @param abortSignal an AbortSignal to close the stream. Note that the
* stream does not close itself as it is infinite, so usually you'd want
* to provide this parameter.
* @returns An AsyncIterator stream of finalized blocks.
*/
getBlocks(abortSignal?: AbortSignal): AsyncIterable<v1.FinalizedBlockInfo> {
const opts = { abort: abortSignal };
const blocks = this.client.getBlocks(v2.Empty, opts).responses;
return mapAsyncIterable(blocks, translate.commonBlockInfo);
}

/**
* Waits until given transaction is finalized.
*
* @param transactionHash a transaction hash as a bytearray.
* @returns A blockhash as a byte array.
*/
async waitForTransactionFinalization(
transactionHash: HexString
): Promise<HexString> {
const abortController = new AbortController();
const blockStream = this.getFinalizedBlocks(abortController.signal);

for await (const block of blockStream) {
const response = await this.getBlockItemStatus(transactionHash);

if (response.status === 'finalized') {
if (response.outcome.blockHash === block.hash) {
// Simply doing `abortController.abort()` causes an error.
// See: https://github.com/grpc/grpc-node/issues/1652
setImmediate(() => abortController.abort());
return block.hash;
}
}
}
throw Error('Unexpected end of stream');
}
}

export function getBlockHashInput(blockHash?: HexString): v2.BlockHashInput {
Expand Down
9 changes: 9 additions & 0 deletions packages/common/src/GRPCTypeTranslation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1626,6 +1626,15 @@ export function instanceInfo(instanceInfo: v2.InstanceInfo): v1.InstanceInfo {
}
}

export function commonBlockInfo(
blockInfo: v2.ArrivedBlockInfo | v2.FinalizedBlockInfo
): v1.CommonBlockInfo {
return {
hash: unwrapValToHex(blockInfo.hash),
height: unwrap(blockInfo.height?.value),
};
}

// ---------------------------- //
// --- V1 => V2 translation --- //
// ---------------------------- //
Expand Down
8 changes: 8 additions & 0 deletions packages/common/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,14 @@ export interface BlockInfo {
transactionEnergyCost: bigint;
}

export interface CommonBlockInfo {
hash: HexString;
height: bigint;
}

export type ArrivedBlockInfo = CommonBlockInfo;
export type FinalizedBlockInfo = CommonBlockInfo;

export interface ConsensusStatus {
bestBlock: string;
genesisBlock: string;
Expand Down
41 changes: 33 additions & 8 deletions packages/common/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,16 @@ export function secondsSinceEpoch(date: Date): bigint {
return BigInt(Math.floor(date.getTime() / 1000));
}

// Retrieves a value that might be undefined. Throws if value is undefined
export function unwrap<A>(x: A | undefined): A {
if (x === undefined) {
console.trace();
throw Error('Undefined value found.');
} else {
return x;
}
}

// Maps a `Record<A,C>` to a `Record<B,D>`.
// Works the same way as a list mapping, allowing both a value and key mapping.
// If `keyMapper()` is not provided, it will map `Record<A,C>` to `Record<A,D>`
Expand All @@ -152,12 +162,27 @@ export function mapRecord<
}
/* eslint-enable @typescript-eslint/no-explicit-any */

// Retrieves a value that might be undefined. Throws if value is undefined
export function unwrap<A>(x: A | undefined): A {
if (x === undefined) {
console.trace();
throw Error('Undefined value found.');
} else {
return x;
}
// Maps an infinite stream of type A to an infinite stream of type B
export function mapAsyncIterable<A, B>(
stream: AsyncIterable<A>,
mapper: (x: A) => B
): AsyncIterable<B> {
return {
[Symbol.asyncIterator]() {
return {
async next() {
for await (const val of stream) {
return {
done: false,
value: mapper(val),
};
}
return {
done: true,
value: undefined,
};
},
};
},
};
}
77 changes: 77 additions & 0 deletions packages/nodejs/READMEV2.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,3 +228,80 @@ const blockHash = 'fe88ff35454079c3df11d8ae13d5777babd61f28be58494efe51b6593e307
const moduleRef = '7e8398adc406a97db4d869c3fd7adc813a3183667a3a7db078ebae6f7dce5f64';
const source = await client.getModuleSource(moduleReference, blockHash);
```
## getBlocks
Returns a stream of blocks that is iterable. The following code will recieved blocks
as long as there is a connection to the node:
```js
// Create stream
const blockStream = client.getBlocks();

// Prints blocks infinitely
for await (const block of blockStream) {
console.log(block)
}
```
You can pass it an abort signal to close the connection. This is particurlary useful for this
function as it otherwise continues forever. An example of how to use `AbortSignal` can be seen below:
```js
// Create abort controller and block stream
const ac = new AbortController();
const blockStream = client.getBlocks(ac.signal);

// Only get one item then break
for await (const block of blockStream) {
console.log(block)
break
}

// Closes the stream
ac.abort();
```
## getFinalizedBlocks
Works exactly like `getBlocks()` but only returns finalized blocks:
```js
// Create stream
const blockStream = client.getFinalizedBlocks();

// Prints blocks infinitely
for await (const block of blockStream) {
console.log(block)
}
```
Likewise, you can also pass it an `AbortSignal`:
```js
// Create abort controller and block stream
const ac = new AbortController();
const blockStream = client.getFinalizedBlocks(ac.signal);

// Only get one item then break
for await (const block of blockStream) {
console.log(block)
break
}

// Closes the stream
ac.abort();
```
### waitForTransactionFinalization
This function waits for the given transaction hash (given as a hex string) to finalize and then returns
the blockhash of the block that contains given transaction as a hex string.
```js
const transactionHash = await client.sendAccountTransaction(
someTransaction,
signature
);

const blockHash = await client.waitForTransactionFinalization(
transactionHash
);
```
50 changes: 42 additions & 8 deletions packages/nodejs/test/clientV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,14 +117,11 @@ test.each([clientV2, clientWeb])(
}
);

test.each([clientV2, clientWeb])(
'NextAccountSequenceNumber',
async (client) => {
const nan = await client.getNextAccountNonce(testAccount);
expect(nan.nonce).toBeGreaterThanOrEqual(19n);
expect(nan.allFinal).toBeDefined();
}
);
test.each([clientV2, clientWeb])('nextAccountNonce', async (client) => {
const nan = await client.getNextAccountNonce(testAccount);
expect(nan.nonce).toBeGreaterThanOrEqual(19n);
expect(nan.allFinal).toBeDefined();
});

test.each([clientV2, clientWeb])('getAccountInfo', async (client) => {
const accountInfo = await getAccountInfoV2(client, testAccount);
Expand Down Expand Up @@ -537,3 +534,40 @@ test.each([clientV2, clientWeb])('createAccount', async (client) => {
)
).rejects.toThrow('expired');
});

// For tests that take a long time to run, is skipped by default
describe.skip('Long run-time test suite', () => {
const longTestTime = 45000;

// Sometimes fails as there is no guarantee that a new block comes fast enough.
test.each([clientV2, clientWeb])(
'getFinalizedBlocks',
async (client) => {
const ac = new AbortController();
const blockStream = client.getFinalizedBlocks(ac.signal);

for await (const block of blockStream) {
expect(block.height).toBeGreaterThan(1553503n);
ac.abort();
break;
}
},
longTestTime
);

// Sometimes fails as there is no guarantee that a new block comes fast enough.
test.each([clientV2, clientWeb])(
'getBlocks',
async (client) => {
const ac = new AbortController();
const blockStream = client.getBlocks(ac.signal);

for await (const block of blockStream) {
expect(block.height).toBeGreaterThan(1553503n);
ac.abort();
break;
}
},
longTestTime
);
});

0 comments on commit 1b6ba9b

Please sign in to comment.