diff --git a/packages/common/CHANGELOG.md b/packages/common/CHANGELOG.md index 9c3610c3..93794ac3 100644 --- a/packages/common/CHANGELOG.md +++ b/packages/common/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 6.5.0 + +- Added the following GRPCv2 functions: + - `getAccountList()` + - `getModuleList()` + - `getAncestors()` + - `getInstanceState()` + - `instanceStateLookup()` + ## 6.4.0 - Added `getFinalizedBlocks()` & `getBlocks()` GRPCv2 functions. diff --git a/packages/common/src/GRPCClient.ts b/packages/common/src/GRPCClient.ts index 069a50f3..3b9aa80b 100644 --- a/packages/common/src/GRPCClient.ts +++ b/packages/common/src/GRPCClient.ts @@ -1,6 +1,6 @@ import * as v1 from './types'; import * as v2 from '../grpc/v2/concordium/types'; -import { HexString } from './types'; +import { Base58String, HexString } from './types'; import { QueriesClient } from '../grpc/v2/concordium/service.client'; import type { RpcTransport } from '@protobuf-ts/runtime-rpc'; import { CredentialRegistrationId } from './types/CredentialRegistrationId'; @@ -449,6 +449,114 @@ export default class ConcordiumNodeClient { } }); } + + /** + * Retrieve a stream of accounts that exist at the end of the given block. + * + * @param blockHash an optional block hash to get the accounts at, otherwise retrieves from last finalized block. + * @param abortSignal an optional AbortSignal to close the stream. + * @returns an async iterable of account addresses represented as Base58 encoded strings. + */ + getAccountList( + blockHash?: HexString, + abortSignal?: AbortSignal + ): AsyncIterable { + const opts = { abort: abortSignal }; + const hash = getBlockHashInput(blockHash); + const asyncIter = this.client.getAccountList(hash, opts).responses; + return mapAsyncIterable(asyncIter, translate.unwrapToBase58); + } + + /** + * Get a stream of all smart contract modules' references. The stream will end + * when all modules that exist in the state at the end of the given + * block have been returned. + * + * @param blockHash an optional block hash to get the contract modules at, otherwise retrieves from last finalized block. + * @param abortSignal an optional AbortSignal to close the stream. + * @returns an async iterable of contract module references, represented as hex strings. + */ + getModuleList( + blockHash?: HexString, + abortSignal?: AbortSignal + ): AsyncIterable { + const opts = { abort: abortSignal }; + const hash = getBlockHashInput(blockHash); + const asyncIter = this.client.getModuleList(hash, opts).responses; + return mapAsyncIterable(asyncIter, translate.unwrapValToHex); + } + + /** + * Get a stream of ancestors for the provided block. + * Starting with the provided block itself, moving backwards until no more + * ancestors or the requested number of ancestors has been returned. + * + * @param maxAmountOfAncestors the maximum amount of ancestors as a bigint. + * @param blockHash a optional block hash to get the ancestors at, otherwise retrieves from last finalized block. + * @param abortSignal an optional AbortSignal to close the stream. + * @returns an async iterable of ancestors' block hashes as hex strings. + */ + getAncestors( + maxAmountOfAncestors: bigint, + blockHash?: HexString, + abortSignal?: AbortSignal + ): AsyncIterable { + const opts = { abort: abortSignal }; + const request: v2.AncestorsRequest = { + blockHash: getBlockHashInput(blockHash), + amount: maxAmountOfAncestors, + }; + const asyncIter = this.client.getAncestors(request, opts).responses; + return mapAsyncIterable(asyncIter, translate.unwrapValToHex); + } + + /** + * Get the exact state of a specific contract instance, streamed as a list of + * key-value pairs. The list is streamed in lexicographic order of keys. + * + * @param contractAddress the contract to get the state of. + * @param blockHash a optional block hash to get the instance states at, otherwise retrieves from last finalized block. + * @param abortSignal an optional AbortSignal to close the stream. + * @returns an async iterable of instance states as key-value pairs of hex strings. + */ + getInstanceState( + contractAddress: v1.ContractAddress, + blockHash?: HexString, + abortSignal?: AbortSignal + ): AsyncIterable { + const opts = { abort: abortSignal }; + const request: v2.InstanceInfoRequest = { + blockHash: getBlockHashInput(blockHash), + address: contractAddress, + }; + const asyncIter = this.client.getInstanceState(request, opts).responses; + return mapAsyncIterable(asyncIter, translate.instanceStateKVPair); + } + + /** + * Get the value at a specific key of a contract state. In contrast to + * `GetInstanceState` this is more efficient, but requires the user to know + * the specific key to look for. + * + * @param contractAddress the contract to get the state of. + * @param key the key of the desired contract state. + * @param blockHash a optional block hash to get the instance states at, otherwise retrieves from last finalized block. + * @returns the state of the contract at the given key as a hex string. + */ + async instanceStateLookup( + contractAddress: v1.ContractAddress, + key: HexString, + blockHash?: HexString + ): Promise { + const request: v2.InstanceStateLookupRequest = { + address: contractAddress, + key: Buffer.from(key, 'hex'), + blockHash: getBlockHashInput(blockHash), + }; + const response = await this.client.instanceStateLookup(request) + .response; + return translate.unwrapValToHex(response); + } } export function getBlockHashInput(blockHash?: HexString): v2.BlockHashInput { diff --git a/packages/common/src/GRPCTypeTranslation.ts b/packages/common/src/GRPCTypeTranslation.ts index 6603883e..5547cbc0 100644 --- a/packages/common/src/GRPCTypeTranslation.ts +++ b/packages/common/src/GRPCTypeTranslation.ts @@ -57,7 +57,11 @@ function unwrapToHex(bytes: Uint8Array | undefined): v1.HexString { return Buffer.from(unwrap(bytes)).toString('hex'); } -function unwrapToBase58( +export function unwrapValToHex(x: { value: Uint8Array } | undefined): string { + return unwrapToHex(unwrap(x).value); +} + +export function unwrapToBase58( address: v2.AccountAddress | undefined ): v1.Base58String { return bs58check.encode( @@ -65,10 +69,6 @@ function unwrapToBase58( ); } -function unwrapValToHex(x: { value: Uint8Array } | undefined): string { - return unwrapToHex(unwrap(x).value); -} - function trModuleRef(moduleRef: v2.ModuleRef | undefined): ModuleReference { return new ModuleReference(unwrapValToHex(moduleRef)); } @@ -1928,6 +1928,15 @@ export function commonBlockInfo( }; } +export function instanceStateKVPair( + state: v2.InstanceStateKVPair +): v1.InstanceStateKVPair { + return { + key: unwrapToHex(state.key), + value: unwrapToHex(state.value), + }; +} + // ---------------------------- // // --- V1 => V2 translation --- // // ---------------------------- // diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts index 4892f0bc..4d170e2a 100644 --- a/packages/common/src/types.ts +++ b/packages/common/src/types.ts @@ -1266,6 +1266,11 @@ export type InstanceInfoSerialized = | InstanceInfoSerializedV0 | InstanceInfoSerializedV1; +export interface InstanceStateKVPair { + key: HexString; + value: HexString; +} + export interface ContractContext { invoker?: ContractAddress | AccountAddress; contract: ContractAddress; diff --git a/packages/common/src/util.ts b/packages/common/src/util.ts index d54cff7e..ba9bb12d 100644 --- a/packages/common/src/util.ts +++ b/packages/common/src/util.ts @@ -189,3 +189,14 @@ export function mapAsyncIterable( }, }; } + +// Converts an async iterable to a list. Beware! this will not terminate if given an infinite stream. +export async function asyncIterableToList( + iterable: AsyncIterable +): Promise { + const list: A[] = []; + for await (const iter of iterable) { + list.push(iter); + } + return list; +} diff --git a/packages/nodejs/READMEV2.md b/packages/nodejs/READMEV2.md index 920cc98e..4d08a319 100644 --- a/packages/nodejs/READMEV2.md +++ b/packages/nodejs/READMEV2.md @@ -347,4 +347,89 @@ const transactionHash = await client.sendAccountTransaction( const blockHash = await client.waitForTransactionFinalization( transactionHash ); +``` + +### getAccountList +Retrieves the accounts that exists a the end of a given block as an async iterable. + +If a blockhash is not supplied it will pick the latest finalized block. An optional abortsignal can also be provided that closes the stream. + +```js +const blockHash = 'fe88ff35454079c3df11d8ae13d5777babd61f28be58494efe51b6593e30716e'; +const accounts: AsyncIterable = clientV2.getAccountList(blockHash); + +// Prints accounts +for await (const account of accounts) { + console.log(account); +} +``` + +### getModuleList +Retrieves all smart contract modules, as an async iterable, that exists in the state at the end of a given block. + +If a blockhash is not supplied it will pick the latest finalized block. An optional abortsignal can also be provided that closes the stream. + +```js +const blockHash = 'fe88ff35454079c3df11d8ae13d5777babd61f28be58494efe51b6593e30716e'; +const moduleRefs: AsyncIterable = clientV2.getModuleList(blockHash); + +// Prints module references +for await (const moduleRef of moduleRefs) { + console.log(moduleRef); +} +``` + +### getAncestors +Retrieves all smart contract modules that exists in the state at the end of a given block, as an async iterable of hex strings. A bigint representing the max number of ancestors to get must be provided. + +If a blockhash is not supplied it will pick the latest finalized block. An optional abortsignal can also be provided that closes the stream. + +```js +const maxNumberOfAncestors = 100n; +const blockHash = 'fe88ff35454079c3df11d8ae13d5777babd61f28be58494efe51b6593e30716e'; +const ancestors: AsyncIterable = clientV2.getAncestors(blockHash); + +// Prints ancestors +for await (const ancestor of ancestors) { + console.log(ancestor); +} +``` + +### getInstanceState +Get the exact state of a specific contract instance, streamed as a list of hex string key-value pairs. + +If a blockhash is not supplied it will pick the latest finalized block. An optional abortsignal can also be provided that closes the stream. + +```js +const contractAddress = { + index: 602n, + subindex: 0n, +}; +const blockHash = 'fe88ff35454079c3df11d8ae13d5777babd61f28be58494efe51b6593e30716e'; +const states: AsyncIterable = clientV2.getInstanceState(blockHash); + +// Prints instance state key-value pairs +for await (const state of states) { + console.log('key:', state.key); + console.log('value:', state.value); +} +``` + +### instanceStateLookup +Get the value at a specific key of a contract state as a hex string. + +In contrast to `GetInstanceState` this is more efficient, but requires the user to know the specific key to look for. + +If a blockhash is not supplied it will pick the latest finalized block. + +```js +const contract = { + index: 601n, + subindex: 0n, +}; +const key = '0000000000000000'; +const blockHash = 'fe88ff35454079c3df11d8ae13d5777babd61f28be58494efe51b6593e30716e' + +const state: HexString = await clientV2.instanceStateLookup(blockHash); +... ``` \ No newline at end of file diff --git a/packages/nodejs/test/clientV2.test.ts b/packages/nodejs/test/clientV2.test.ts index 102c15f3..c1a0c958 100644 --- a/packages/nodejs/test/clientV2.test.ts +++ b/packages/nodejs/test/clientV2.test.ts @@ -29,6 +29,7 @@ import { serializeAccountTransaction } from '@concordium/common-sdk/lib/serializ import { TextEncoder, TextDecoder } from 'util'; import 'isomorphic-fetch'; import { GrpcWebFetchTransport } from '@protobuf-ts/grpcweb-transport'; +import { asyncIterableToList } from '@concordium/common-sdk/src/util'; /* eslint-disable @typescript-eslint/no-explicit-any */ global.TextEncoder = TextEncoder as any; @@ -598,7 +599,54 @@ test.each([clientV2, clientWeb])('createAccount', async (client) => { ).rejects.toThrow('expired'); }); -// Tests, which take a long time to run, are skipped by default +test.each([clientV2, clientWeb])('getAccountList', async (client) => { + const blocks = await clientV1.getBlocksAtHeight(10n); + const accountIter = client.getAccountList(blocks[0]); + const accountList = await asyncIterableToList(accountIter); + expect(accountList).toEqual(expected.accountList); +}); + +test.each([clientV2, clientWeb])('getModuleList', async (client) => { + const blocks = await clientV1.getBlocksAtHeight(5000n); + const moduleIter = client.getModuleList(blocks[0]); + const moduleList = await asyncIterableToList(moduleIter); + expect(moduleList).toEqual(expected.moduleList); +}); + +test.each([clientV2, clientWeb])('getAncestors', async (client) => { + const ancestorsIter = client.getAncestors(3n, testBlockHash); + const ancestorsList = await asyncIterableToList(ancestorsIter); + expect(ancestorsList).toEqual(expected.ancestorList); +}); + +test.each([clientV2, clientWeb])('getInstanceState', async (client) => { + const contract = { + index: 602n, + subindex: 0n, + }; + const instanceStateIter = client.getInstanceState(contract, testBlockHash); + const instanceStateList = await asyncIterableToList(instanceStateIter); + + expect(instanceStateList).toEqual(expected.instanceStateList); +}); + +test.each([clientV2, clientWeb])('instanceStateLookup', async (client) => { + const key = '0000000000000000'; + const expectedValue = '0800000000000000'; + const contract = { + index: 601n, + subindex: 0n, + }; + const value = await client.instanceStateLookup( + contract, + key, + testBlockHash + ); + + expect(value).toEqual(expectedValue); +}); + +// For tests that take a long time to run, is skipped by default describe.skip('Long run-time test suite', () => { const longTestTime = 45000; diff --git a/packages/nodejs/test/resources/expectedJsons.ts b/packages/nodejs/test/resources/expectedJsons.ts index 6bd50baa..60906bb2 100644 --- a/packages/nodejs/test/resources/expectedJsons.ts +++ b/packages/nodejs/test/resources/expectedJsons.ts @@ -216,3 +216,50 @@ export const invokeInstanceResponseV0 = { }, ], }; + +export const accountList = [ + '3QK1rxUXV7GRk4Ng7Bs7qnbkdjyBdjzCytpTrSQN7BaJkiEfgZ', + '3U4sfVSqGG6XK8g6eho2qRYtnHc4MWJBG1dfxdtPGbfHwFxini', + '3gGBYDSpx2zWL3YMcqD48U5jVXYG4pJBDZqeY5CbMMKpxVBbc3', + '3kBx2h5Y2veb4hZgAJWPrr8RyQESKm5TjzF3ti1QQ4VSYLwK1G', + '3ntvNGT6tDuLYiSb5gMJSQAZfLPUJnzoizcFiVRWqLoctuXxpK', + '3v1JUB1R1JLFtcKvHqD9QFqe2NXeBF53tp69FLPHYipTjNgLrV', + '3y9DtDUL8xpf8i2yj9k44zMVkf4H1hkpBEQcXbJhrgcwYSGg41', + '42tFTDWvTmBd7hEacohuCfGFa9TsBKhsmXKeViQ7q7NoY7UadV', + '44Axe5eHnMkBinX7GKvUm5w6mX83JGdasijhvsMv5ZW2Wmgphg', + '48XGRnvQoG92T1AwETvW5pnJ1aRSPMKsWtGdKhTqyiNZzMk3Qn', + '4AnukgcopMC4crxfL1L9fUYw9MAkoo1yKLvH7eA1NAX7SxgyRY', + '4BTFaHx8CioLi8Xe7YiimpAK1oQMkbx5Wj6B8N7d7NXgmLvEZs', + '4EJJ1hVhbVZT2sR9xPzWUwFcJWK3fPX54z94zskTozFVk8Xd4L', +]; + +export const moduleList = [ + '67d568433bd72e4326241f262213d77f446db8ba03dfba351ae35c1b2e7e5109', + '6f0524700ed808a8fe0d7e23014c5138e4fac1fd8ec85c5e3591096f48609206', + 'ceb018e4cd3456c0ccc0bca14285a69fd55f4cb09c322195d49c5c22f85930fe', +]; + +export const ancestorList = [ + 'fe88ff35454079c3df11d8ae13d5777babd61f28be58494efe51b6593e30716e', + '28d92ec42dbda119f0b0207d3400b0573fe8baf4b0d3dbe44b86781ad6b655cf', + 'abc98d4866e92b0ac4722d523aee96cafcdd127694d565c532e149616dbad96c', +]; + +export const instanceStateList = [ + { + key: '', + value: '3b00000068747470733a2f2f72656c617965722d746573746e65742e746f6e692e73797374656d732f746f6b656e2f6d657461646174612f4d4f434b2e657402000000000000000300000000000000', + }, + { + key: '0000000000000000', + value: '0500000000000000', + }, + { + key: '0300000000000000008ffbe4209190c92b68b4f9d59cfb64305337a9cad018ac71156cc8f6e41f9fa5', + value: '0400000000000000', + }, + { + key: '040000000000000000', + value: '', + }, +];