Skip to content

feat(ismp): add cross-chain types and a typed ismp_* client - #2

Merged
nol4lej merged 1 commit into
mainfrom
feat/ismp-types
Sep 9, 2026
Merged

nol4lej merged 1 commit into
mainfrom
feat/ismp-types

Conversation

@nol4lej

@nol4lej nol4lej commented Sep 9, 2026

Copy link
Copy Markdown
Member

Summary

Adds ISMP types and client.ismp, a typed client for the ismp_* RPC endpoints, so consumers can read cross-chain traffic without hand-rolling shapes. Read-only by design: dispatching a message is an extrinsic, and a root-only one on this chain.

Downstream of the Hyperbridge integration — privacy-explorer needs these types to render cross-chain activity, and it pins this package from the npm registry rather than a workspace link, so this has to ship before the explorer can consume it.

Two shapes a consumer would otherwise get wrong

Both captured from a running node, not inferred from the pallet source.

A request comes back wrapped in its variant, not flattened:

[{ "Post": { "source": "SUBSTRATE-orbi", "dest": "KUSAMA-1000", "nonce": 0, ... } }]

Reading result[0].source yields undefined. IsmpRequest is therefore a union that forces narrowing on the key first.

Each transport names a chain differently. The ismp_* RPCs serialise a state machine as a string ("KUSAMA-1000"); decoded block events give the SCALE enum ({ type: 'Kusama', value: 1000 }). Both are typed — assuming one silently reads undefined from the other. ismp_queryEvents was another surprise: it returns a map keyed by block hash, with unmatched blocks present as empty arrays, so the keys describe the range scanned rather than the blocks that matched.

Events

All seven ismpMessaging variants, plus the seven ismp variants worth modelling. The union documents what it deliberately omits and why — consensus-client lifecycle is answered better by the RPCs, RelayerFeeWithdrawn can only be triggered by the configured coprocessor, and Response is never deposited by pallet-ismp at all.

Includes the commitment that runtime spec 12 added to MessageReceived, MessageRejected, RequestTimedOut and GetResponseReceived. Before that field existed, an arrival could not be attributed to any message and an expiry could not be matched to what expired. The doc comments carry the two asymmetries that are easy to get backwards: PostRequestHandled fires on the destination while GetRequestHandled fires on the origin, and *Handled / *TimeoutHandled are different structs.

Null over throw

latestHeight and challengePeriod return null when a channel is unknown. The RPC answers absent state with an error code, which at the JSON-RPC layer is indistinguishable from a transport failure — and "not onboarded yet" is a normal state during setup, so making every caller wrap it in try/catch would be the wrong default.

Verification

Exercised against a live dev node with a real dispatched message, not only compiled: narrowing on Post resolves, all three lookups return null on an absent channel instead of throwing, events returns its block-hash map, and requests([]) short-circuits without a call.

857 tests pass across 35 files, including 7 new structural ones. typecheck:all, lint and build are clean.

Release

Version 0.2.0 → 0.3.0, additive only. The CHANGELOG section is in place — I reproduced the release workflow's gate (grep "^## \[0.3.0\]") to confirm it passes.

@nol4lej
nol4lej merged commit 7a23b1e into main Sep 9, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant