Skip to content

Commit 68061f7

Browse files
authored
feat(grpc): make grpc usable (#402)
# Description of change - add README - `sdk/grpc/README.md` - add typedoc - `sdk/grpc/typedoc.json` - `scripts/generate-sdk-typedoc.sh` - add checkpoint-follower example - `sdk/examples/src/grpc-checkpoint-follower.ts` - `sdk/examples/package.json` - `sdk/examples/README.md` - `pnpm-lock.yaml` - add size-limit - `sdk/grpc/package.json` - `pnpm-lock.yaml` - add local node e2e tests - `sdk/grpc/test/localnet/setup.ts` - `sdk/grpc/test/localnet/localnet.test.ts` - `sdk/grpc/package.json` - `sdk/grpc/README.md` - `pnpm-lock.yaml` - `.github/workflows/_e2e.yml` - `.github/workflows/turbo_hierarchy.yml` - `.github/workflows/turbo_nightly.yml` ## Links to any relevant issues fixes #346 ## How the change has been tested Describe the tests that you ran to verify your changes. Make sure to provide instructions for the maintainer as well as any relevant configurations.
1 parent e0da35d commit 68061f7

13 files changed

Lines changed: 675 additions & 5 deletions

File tree

‎.github/workflows/_e2e.yml‎

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ on:
5959
description: Names SDK
6060
type: boolean
6161
default: false
62+
isGrpc:
63+
description: gRPC SDK
64+
type: boolean
65+
default: false
6266
workflow_call:
6367
inputs:
6468
isNightly:
@@ -103,6 +107,9 @@ on:
103107
isNamesSDK:
104108
type: boolean
105109
required: true
110+
isGrpc:
111+
type: boolean
112+
required: true
106113
secrets:
107114
EVM_BRIDGE_PREVIEW_DEFAULT_NETWORK:
108115
required: true
@@ -124,7 +131,7 @@ env:
124131
jobs:
125132
localnet:
126133
name: Localnet
127-
if: inputs.isNightly || inputs.isRpc || inputs.isExplorer || inputs.isAppsBackend || inputs.isTypescriptSDK || inputs.isWallet || inputs.isGraphQlTransport || inputs.isWalletDashboard || inputs.isEvmBridge || inputs.isIscSDK || inputs.isNamesDapp || inputs.isNamesDisplay || inputs.isNamesSDK
134+
if: inputs.isNightly || inputs.isRpc || inputs.isExplorer || inputs.isAppsBackend || inputs.isTypescriptSDK || inputs.isWallet || inputs.isGraphQlTransport || inputs.isWalletDashboard || inputs.isEvmBridge || inputs.isIscSDK || inputs.isNamesDapp || inputs.isNamesDisplay || inputs.isNamesSDK || inputs.isGrpc
128135
runs-on: self-hosted-x64
129136
services:
130137
postgres:
@@ -175,6 +182,7 @@ jobs:
175182
echo "runNamesDapp=${{ inputs.isNightly || inputs.isNamesDapp }}" >> $GITHUB_OUTPUT
176183
echo "runNamesDisplay=${{ inputs.isNightly || inputs.isNamesDisplay }}" >> $GITHUB_OUTPUT
177184
echo "runNamesSDK=${{ inputs.isNightly || inputs.isNamesSDK || inputs.isNamesDapp }}" >> $GITHUB_OUTPUT
185+
echo "runGrpc=${{ inputs.isNightly || inputs.isGrpc }}" >> $GITHUB_OUTPUT
178186
179187
- name: Download IOTA Binary
180188
env:
@@ -345,6 +353,24 @@ jobs:
345353
continue-on-error: ${{ inputs.isNightly }}
346354
run: pnpm dlx concurrently --kill-others --success command-1 "$E2E_RUN_LOCAL_NET_CMD" 'pnpm --filter=@iota/kiosk test:e2e'
347355

356+
# ─── gRPC SDK ────────────────────────────────────────────────────────────
357+
358+
- name: Build gRPC SDK
359+
id: build-grpc
360+
if: (!cancelled() && !failure()) && steps.conditions.outputs.runGrpc == 'true'
361+
continue-on-error: ${{ inputs.isNightly }}
362+
run: pnpm turbo --filter=@iota/grpc build
363+
364+
# The gRPC API is off unless the node config turns it on.
365+
- name: Run gRPC SDK e2e tests
366+
id: run-grpc
367+
if: (!cancelled() && !failure()) && steps.build-grpc.outcome == 'success' && steps.conditions.outputs.runGrpc == 'true'
368+
continue-on-error: ${{ inputs.isNightly }}
369+
run: |
370+
pnpm dlx concurrently --kill-others --success command-1 \
371+
"$E2E_RUN_LOCAL_NET_CMD --node-config-override fullnode:enable-grpc-api=true --node-config-override \"fullnode:grpc-api-config={address: '0.0.0.0:50051'}\"" \
372+
'pnpm --filter=@iota/grpc test:e2e:localnet'
373+
348374
# ─── Wallet ──────────────────────────────────────────────────────────────
349375

350376
- name: Run Local net
@@ -527,6 +553,7 @@ jobs:
527553
[[ "${{ steps.run-names-dapp.outcome }}" == 'failure' ]] && failed+=('Names Dapp')
528554
[[ "${{ steps.run-names-display.outcome }}" == 'failure' ]] && failed+=('Names Display')
529555
[[ "${{ steps.run-names-sdk.outcome }}" == 'failure' ]] && failed+=('Names SDK')
556+
[[ "${{ steps.run-grpc.outcome }}" == 'failure' ]] && failed+=('gRPC SDK')
530557
531558
if [[ ${#failed[@]} -gt 0 ]]; then
532559
echo "Failed test groups: ${failed[*]}"

‎.github/workflows/turbo_hierarchy.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ jobs:
6868
isNamesDapp: ${{ needs.diff.outputs.isNamesDapp == 'true' }}
6969
isNamesDisplay: ${{ needs.diff.outputs.isNamesDisplay == 'true' }}
7070
isNamesSDK: ${{ needs.diff.outputs.isNamesSDK == 'true' }}
71+
isGrpc: ${{ needs.diff.outputs.isGrpc == 'true' }}
7172
secrets:
7273
EVM_BRIDGE_PREVIEW_DEFAULT_NETWORK: ${{ secrets.EVM_BRIDGE_PREVIEW_DEFAULT_NETWORK }}
7374
EVM_BRIDGE_CONFIG_TESTS: ${{ secrets.EVM_BRIDGE_CONFIG_TESTS }}

‎.github/workflows/turbo_nightly.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ jobs:
6868
isNamesDapp: true
6969
isNamesDisplay: true
7070
isNamesSDK: true
71+
isGrpc: true
7172
secrets:
7273
EVM_BRIDGE_PREVIEW_DEFAULT_NETWORK: ${{ secrets.EVM_BRIDGE_PREVIEW_DEFAULT_NETWORK }}
7374
EVM_BRIDGE_CONFIG_TESTS: ${{ secrets.EVM_BRIDGE_CONFIG_TESTS }}

‎pnpm-lock.yaml‎

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎scripts/generate-sdk-typedoc.sh‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
rm -rf generated-docs
55
mkdir -p generated-docs
66

7-
packages="bcs dapp-kit graphql-transport isc-sdk kiosk ledgerjs-hw-app-iota names signers typescript wallet-standard"
7+
packages="bcs dapp-kit graphql-transport grpc isc-sdk kiosk ledgerjs-hw-app-iota names signers typescript wallet-standard"
88

99
for package in $packages; do
1010
# Generate typedoc for the package from its own directory

‎sdk/examples/README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Runnable examples demonstrating common operations with the IOTA TypeScript SDK.
1010
| `get-balance.ts` | Fetch an account's IOTA balance and coin objects from devnet |
1111
| `transfer-iota.ts` | Build and execute a transaction to transfer IOTA tokens on devnet |
1212
| `tx-with-graphql.ts` | Execute a transaction and query details via GraphQL transport |
13+
| `grpc-checkpoint-follower.ts` | Follow a network's checkpoints over gRPC, resuming if the stream drops |
1314
| `move-authenticator.ts` | Publish, create and send with an on-chain account (Move Authenticator) |
1415
| `move-authenticator-existing.ts` | Send with an on-chain account (Move Authenticator) |
1516

‎sdk/examples/package.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"exports": {
1111
"./custom-signer.js": "./dist/esm/custom-signer.js",
1212
"./get-balance.js": "./dist/esm/get-balance.js",
13+
"./grpc-checkpoint-follower.js": "./dist/esm/grpc-checkpoint-follower.js",
1314
"./transfer-iota.js": "./dist/esm/transfer-iota.js",
1415
"./tx-with-graphql.js": "./dist/esm/tx-with-graphql.js"
1516
},
@@ -25,6 +26,7 @@
2526
},
2627
"dependencies": {
2728
"@iota/graphql-transport": "workspace:*",
29+
"@iota/grpc": "workspace:*",
2830
"@iota/iota-sdk": "workspace:*",
2931
"@noble/curves": "^1.4.2"
3032
},
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Copyright (c) 2026 IOTA Stiftung
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { setTimeout as sleep } from 'node:timers/promises';
5+
import type { GrpcNetwork } from '@iota/grpc';
6+
import { CheckpointResponseField, GRPC_URLS, IotaGrpcClient } from '@iota/grpc';
7+
8+
/**
9+
* Example: Checkpoint Follower
10+
*
11+
* This example follows a network over gRPC, printing each checkpoint as the
12+
* network produces it. When the stream drops, it starts a new one after the
13+
* last checkpoint it received, so none are skipped. Press Ctrl+C to stop.
14+
*
15+
* It follows the network given as its first argument (mainnet, testnet, devnet
16+
* or localnet), or testnet without one. It starts at the latest checkpoint, or
17+
* at the sequence number given as its second argument:
18+
*
19+
* pnpm example ./src/grpc-checkpoint-follower.ts
20+
* pnpm example ./src/grpc-checkpoint-follower.ts mainnet
21+
* pnpm example ./src/grpc-checkpoint-follower.ts testnet 274000000
22+
*/
23+
24+
// Give up after this many failures in a row without receiving a checkpoint.
25+
const MAX_FAILURES = 5;
26+
27+
// Read the network from the first argument, defaulting to testnet
28+
const network = process.argv[2] || 'testnet';
29+
if (!Object.hasOwn(GRPC_URLS, network)) {
30+
throw new Error(
31+
`Unknown network '${network}': use one of ${Object.keys(GRPC_URLS).join(', ')}`,
32+
);
33+
}
34+
35+
// Create a client connected to that network
36+
const client = new IotaGrpcClient({ network: network as GrpcNetwork });
37+
38+
// Stop on Ctrl+C
39+
const controller = new AbortController();
40+
process.on('SIGINT', () => controller.abort());
41+
42+
// The next checkpoint to ask for. Undefined asks for the latest.
43+
let next = process.argv[3] === undefined ? undefined : BigInt(process.argv[3]);
44+
let failures = 0;
45+
46+
console.log(`Following ${network} from ${next ?? 'the latest checkpoint'}...`);
47+
48+
while (!controller.signal.aborted) {
49+
try {
50+
const stream = client.streamCheckpoints({
51+
startSequenceNumber: next,
52+
// By default only the summary is sent: ask for the transaction digests too
53+
readMask: [
54+
CheckpointResponseField.CHECKPOINT_SUMMARY,
55+
CheckpointResponseField.TRANSACTIONS_TRANSACTION_DIGEST,
56+
],
57+
signal: controller.signal,
58+
});
59+
60+
for await (const item of stream.items) {
61+
if (item.kind !== 'checkpoint') {
62+
continue;
63+
}
64+
65+
console.log(
66+
`Checkpoint ${item.sequenceNumber}: ${item.transactions.length} transactions`,
67+
);
68+
next = item.sequenceNumber + 1n;
69+
failures = 0;
70+
}
71+
} catch (error) {
72+
// Ctrl+C ends the stream with a CANCELED error
73+
if (controller.signal.aborted) {
74+
break;
75+
}
76+
77+
failures++;
78+
if (failures > MAX_FAILURES) {
79+
throw error;
80+
}
81+
82+
// Wait 1, 2, 4, 8 and 16 seconds between attempts
83+
const delayMs = 1_000 * 2 ** (failures - 1);
84+
console.warn(`Stream dropped: ${(error as Error).message}`);
85+
console.warn(`Resuming at ${next ?? 'the latest checkpoint'} in ${delayMs / 1_000}s...`);
86+
await sleep(delayMs, undefined, { signal: controller.signal }).catch(() => {});
87+
}
88+
}
89+
90+
console.log('Stopped.');

0 commit comments

Comments
 (0)