Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Grpcv2-PR4: Mostly just new streaming methods #123

Merged
merged 5 commits into from
Feb 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/common/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
110 changes: 109 additions & 1 deletion packages/common/src/GRPCClient.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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<Base58String> {
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<HexString> {
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<HexString> {
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<v1.InstanceStateKVPair> {
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<HexString> {
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 {
Expand Down
19 changes: 14 additions & 5 deletions packages/common/src/GRPCTypeTranslation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,18 +57,18 @@ 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(
Buffer.concat([Buffer.of(1), unwrap(address?.value)])
);
}

function unwrapValToHex(x: { value: Uint8Array } | undefined): string {
return unwrapToHex(unwrap(x).value);
}

function trModuleRef(moduleRef: v2.ModuleRef | undefined): ModuleReference {
return new ModuleReference(unwrapValToHex(moduleRef));
}
Expand Down Expand Up @@ -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 --- //
// ---------------------------- //
Expand Down
5 changes: 5 additions & 0 deletions packages/common/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1266,6 +1266,11 @@ export type InstanceInfoSerialized =
| InstanceInfoSerializedV0
| InstanceInfoSerializedV1;

export interface InstanceStateKVPair {
key: HexString;
value: HexString;
}

export interface ContractContext {
invoker?: ContractAddress | AccountAddress;
contract: ContractAddress;
Expand Down
11 changes: 11 additions & 0 deletions packages/common/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,3 +189,14 @@ export function mapAsyncIterable<A, B>(
},
};
}

// Converts an async iterable to a list. Beware! this will not terminate if given an infinite stream.
export async function asyncIterableToList<A>(
iterable: AsyncIterable<A>
): Promise<A[]> {
const list: A[] = [];
for await (const iter of iterable) {
list.push(iter);
}
return list;
}
85 changes: 85 additions & 0 deletions packages/nodejs/READMEV2.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<AccountAddress> = 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<ModuleReference> = 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<HexString> = 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<InstanceStateKVPair> = 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);
...
```
50 changes: 49 additions & 1 deletion packages/nodejs/test/clientV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down
Loading