Spans: paritytech/truapi, paritytech/triangle-js-sdks (@novasamatech/host-api + host-container)
Summary:
the wire shapes - envelope, wire-id table, and most payload/error types - match between the generated @parity/truapi reference and the latest hand-written novasama host. Two divergences are still open and need code changes, and they point in different directions: (D1) on framework faults the current reference runtime (@parity/truapi-host) is silent while novasama correctly emits a terminal error frame - a fix on the reference side; (D2) novasama's PaymentTopUp encoding (error-variant order + key size) does not match the crate - a fix on the host-api side. A third gap is Android-only handler wiring. Transport, rate-limiting, and coin_payment are out of scope (below). This is not a "bump a version" cleanup.
Background
TrUAPI is the one wire language a product speaks to a host: fixed methods, fixed request/response/error shapes, a fixed numeric wire id each, all SCALE-encoded (positional binary - field and variant order is the contract). It is defined by the Rust crate rust/crates/truapi (the source of truth), from which the @parity/* code is generated.
Two things implement the host contract, and either can diverge:
- novasama
@novasamatech/host-api + host-container - hand-written TypeScript. Its codecs are hand-maintained and can drift from the crate (D2).
- the generated reference runtime - today the TypeScript
@parity/truapi-host; the Rust truapi-server → @parity/truapi-host-wasm is the successor but lives on an unmerged branch (PR-104). The current TS reference host has a behavioural gap the WASM runtime fixes (D1).
Every response frame:
┌───────────┬───────────┬────────────┬────────────────┬───────────┐
│ requestId │ action id │ version │ Result tag │ payload │
│ (str) │ (1 byte) │ V1 = 0x00 │ Ok=00 / Err=01 │ per type │
└───────────┴───────────┴────────────┴────────────────┴───────────┘
What conforms
- Envelope + wire-id table -
[requestId str][action id u8][version V1=0x00][Result Ok=0x00/Err=0x01][payload], verified end-to-end through encodeWireMessage and the generated indexedTaggedUnion({V1:[0,...]}). The table has 55 non-coin_payment method-actions plus 9 coin_payment (ids ≈136-163).
- Payload/error types cross-checked are byte-identical except
PaymentTopUp (D2): e.g. HostSignPayloadData (15 fields), HostGetUserIdResponse { primary_username }, Statement/SignedStatement with proof enums, chain types, genesis_hash as variable Vec<u8>.
- Error channel: there are 22 concrete per-method domain error enums (plus a shared
GenericError struct, referenced by ~17 versioned wrappers). The generic CallError tier (Domain / Denied / Unsupported / MalformedFrame / HostFailure) is stripped from the wire by codegen - it derives no Encode/Decode and is unwrapped to its inner domain type D (truapi-codegen/src/ts.rs, skip list in rustdoc.rs). So the only typed error on the wire is the per-method domain enum. This matters for D1.
Coverage honesty: the differential cross-checked ≈18-19 distinct payload types (shared types inherit); full per-method input-vector coverage across all 55 is the CI's job (T3), not yet executed. "Byte-identical everywhere" is the target the CI enforces, not a fully-executed claim today.
Open divergences (each needs a code change)
Ordered by severity. D1 first: it is the one where the caller literally hangs.
D1 · Framework faults: the reference runtime is silent; novasama emits a frame → reference-side fix
Call a method the host doesn't implement, denies, or that throws:
- novasama
host-container returns a terminal Result::Err frame - it folds the fault into the per-method domain enum's catch-all (…Err::Unknown { reason }) and answers. The caller gets an error.
- the current reference
@parity/truapi-host (TS dispatcher) sends nothing: unregistered wire id → drop (default onUnknownFrame); handler throw/reject → swallow; subscription-start error → swallow. The caller hangs / times out.
Root cause: the generic CallError tier has no wire representation (stripped by codegen, above), and the crate's default handler bodies return CallError::unavailable() = HostFailure { reason }, which cannot be SCALE-encoded; required methods (e.g. navigate_to) have no default body at all. So the TS reference has no typed channel for a framework fault - it drops. The "Not implemented → domain Unknown" mapping is host-side behaviour (novasama does it; dotli does it), not something the crate provides.
The Rust truapi-server (WASM successor, on the PR-104 branch) does emit a terminal frame on handler Err - so landing it closes this on the reference side.
Close: land the WASM runtime (T1, emits terminal frames), and have the conformance CI assert a terminal typed frame on faults - i.e. assert novasama's behaviour, not "match the silent TS host" (T3).
D2 · PaymentTopUp encoding drift (variant order + key size) → host-api code change
Variant order. The crate's HostPaymentTopUpError (v01/payment.rs) declares InsufficientFunds(0), InvalidSource(1), Unknown(2), PartialPayment(3). Current host-api (0.8.3, payments.ts) declares PartialPayment before Unknown, so their discriminants are swapped:
host-api 0.8.3 crate (source of truth)
Unknown{reason} 0x03 … 0x02 …
PartialPayment{c} 0x02 … 0x03 …
(0.8.3 added PartialPayment in the wrong slot; it was never a later reorder.)
Key size. The crate's PaymentTopUpSource uses 64-byte sr25519 secret keys ([u8; 64]); host-api still encodes Bytes(32). Divergent.
Close (T2): in host-api, reorder PaymentTopUpError so Unknown precedes PartialPayment (matching the crate) and widen PaymentTopUpSource keys to 64-byte sr25519. This is a code change, not a version bump - re-derive any version specifics from a current checkout / the npm registry (the local triangle-js-sdks is 0.8.3 and still divergent; there is no 0.8.4 in it).
D3 · get_user_id is unregistered on Android → host wiring
The codec matches on both sides (HostGetUserIdResponse { primary_username } ↔ host-api UserIdentity { primaryUsername }). dotli, iOS, and desktop each register a real handler returning { primaryUsername }; only Android leaves it unregistered, so the container falls back to GetUserIdErr::Unknown { reason: "Not implemented" }. (This is a domain Unknown, not an Unsupported - there is no Unsupported wire variant.)
Close (T4): register the get_user_id handler on Android, or confirm the omission is intentional.
Known & out of scope
- Transport establishment - by design, not a task. The client and the host establish the byte channel differently (MessagePort vs raw
window.postMessage, with different readiness handshakes across implementations). Same SCALE bytes, different carrier. A product selects the provider that matches its host; this is inherent to the client/host integration layer, and swapping it is expected on a migration. Out of scope for wire conformance. (The exact handshake mechanism differs between the base repo and the PR-104 sandbox layer; don't rely on a single "MessagePort vs postMessage" dichotomy.)
- Rate-limiting - integrator-owned on both sides, not a divergence.
host-container ships createRateLimiter as a standalone opt-in util; createContainer never wires it. TrUAPI standardizes no rate-limiting (no method, RFC, or wire id), and the runtime ships none. Whether the runtime should ever offer it as a framework capability is an open question below, not a conformance gap.
coin_payment - a shared future milestone, not a divergence. CoinPayment is part of the unified TrUApi trait (9 host methods, ids ≈136-163), so a fully-conformant host implements it eventually. But host-api has none (only the CoinPaymentPurseId = u32 selector), and on the reference side it is explicitly deferred: the PR-104 truapi-server carries an empty impl CoinPayment for ProductRuntimeHost {} ("outside this milestone"). Both sides unimplemented → nothing to close today.
statement_store/subscribe decode error seen in testing - reproduces only against a checkout with a mismatched host-api install (0.7.9 resolved under a 0.8.2 pin). Statement types are byte-identical across the crate, @parity/truapi, and current host-api; a clean install resolves it. Not a protocol difference.
Tasks
| # |
Task |
Closes |
Owner (repo) |
| T1 |
Land + publish the WASM reference runtime (truapi-server → @parity/truapi-host-wasm): it emits terminal frames on framework faults (closes D1) and gives the CI a stable runtime reference. Currently on the PR-104 branch and unpublished; base truapi still ships the TS @parity/truapi-host. |
D1 |
truapi |
| T2 |
host-api code change: reorder HostPaymentTopUpError so Unknown precedes PartialPayment (match the crate) + widen PaymentTopUpSource to 64-byte sr25519 keys. Re-derive version specifics from a current checkout / npm. |
D2 |
triangle-js-sdks |
| T3 |
Conformance CI in triangle-js-sdks: per distinct type decode → re-encode + direct dual-encode (catches fixed-byte-size drift scale-ts skips on decode) + failure-path frames, vs the crate codecs (@parity/truapi, published 0.3.x). Assert a terminal typed frame on faults (novasama's behaviour). The codec half can run today; the runtime/behavioural half waits on T1. |
D2 (+ D1 oracle) |
triangle-js-sdks |
| T4 |
Register the get_user_id handler on Android (or confirm the omission is intentional). |
D3 |
polkadot-app-android |
Open questions
- Rate-limiting ownership - TrUAPI standardizes none;
host-container offers createRateLimiter integrator-side
Spans:
paritytech/truapi,paritytech/triangle-js-sdks(@novasamatech/host-api+host-container)Summary:
the wire shapes - envelope, wire-id table, and most payload/error types - match between the generated
@parity/truapireference and the latest hand-written novasama host. Two divergences are still open and need code changes, and they point in different directions: (D1) on framework faults the current reference runtime (@parity/truapi-host) is silent while novasama correctly emits a terminal error frame - a fix on the reference side; (D2) novasama'sPaymentTopUpencoding (error-variant order + key size) does not match the crate - a fix on the host-api side. A third gap is Android-only handler wiring. Transport, rate-limiting, andcoin_paymentare out of scope (below). This is not a "bump a version" cleanup.Background
TrUAPI is the one wire language a product speaks to a host: fixed methods, fixed request/response/error shapes, a fixed numeric wire id each, all SCALE-encoded (positional binary - field and variant order is the contract). It is defined by the Rust crate
rust/crates/truapi(the source of truth), from which the@parity/*code is generated.Two things implement the host contract, and either can diverge:
@novasamatech/host-api+host-container- hand-written TypeScript. Its codecs are hand-maintained and can drift from the crate (D2).@parity/truapi-host; the Rusttruapi-server→@parity/truapi-host-wasmis the successor but lives on an unmerged branch (PR-104). The current TS reference host has a behavioural gap the WASM runtime fixes (D1).Every response frame:
What conforms
[requestId str][action id u8][version V1=0x00][Result Ok=0x00/Err=0x01][payload], verified end-to-end throughencodeWireMessageand the generatedindexedTaggedUnion({V1:[0,...]}). The table has 55 non-coin_paymentmethod-actions plus 9coin_payment(ids ≈136-163).PaymentTopUp(D2): e.g.HostSignPayloadData(15 fields),HostGetUserIdResponse { primary_username },Statement/SignedStatementwith proof enums, chain types,genesis_hashas variableVec<u8>.GenericErrorstruct, referenced by ~17 versioned wrappers). The genericCallErrortier (Domain / Denied / Unsupported / MalformedFrame / HostFailure) is stripped from the wire by codegen - it derives noEncode/Decodeand is unwrapped to its inner domain typeD(truapi-codegen/src/ts.rs, skip list inrustdoc.rs). So the only typed error on the wire is the per-method domain enum. This matters for D1.Coverage honesty: the differential cross-checked ≈18-19 distinct payload types (shared types inherit); full per-method input-vector coverage across all 55 is the CI's job (T3), not yet executed. "Byte-identical everywhere" is the target the CI enforces, not a fully-executed claim today.
Open divergences (each needs a code change)
Ordered by severity. D1 first: it is the one where the caller literally hangs.
D1 · Framework faults: the reference runtime is silent; novasama emits a frame → reference-side fix
Call a method the host doesn't implement, denies, or that throws:
host-containerreturns a terminalResult::Errframe - it folds the fault into the per-method domain enum's catch-all (…Err::Unknown { reason }) and answers. The caller gets an error.@parity/truapi-host(TS dispatcher) sends nothing: unregistered wire id → drop (defaultonUnknownFrame); handler throw/reject → swallow; subscription-start error → swallow. The caller hangs / times out.Root cause: the generic
CallErrortier has no wire representation (stripped by codegen, above), and the crate's default handler bodies returnCallError::unavailable()=HostFailure { reason }, which cannot be SCALE-encoded; required methods (e.g.navigate_to) have no default body at all. So the TS reference has no typed channel for a framework fault - it drops. The "Not implemented→ domainUnknown" mapping is host-side behaviour (novasama does it; dotli does it), not something the crate provides.The Rust
truapi-server(WASM successor, on the PR-104 branch) does emit a terminal frame on handlerErr- so landing it closes this on the reference side.Close: land the WASM runtime (T1, emits terminal frames), and have the conformance CI assert a terminal typed frame on faults - i.e. assert novasama's behaviour, not "match the silent TS host" (T3).
D2 ·
PaymentTopUpencoding drift (variant order + key size) → host-api code changeVariant order. The crate's
HostPaymentTopUpError(v01/payment.rs) declaresInsufficientFunds(0), InvalidSource(1), Unknown(2), PartialPayment(3). Currenthost-api(0.8.3,payments.ts) declaresPartialPaymentbeforeUnknown, so their discriminants are swapped:(0.8.3 added
PartialPaymentin the wrong slot; it was never a later reorder.)Key size. The crate's
PaymentTopUpSourceuses 64-byte sr25519 secret keys ([u8; 64]);host-apistill encodesBytes(32). Divergent.Close (T2): in
host-api, reorderPaymentTopUpErrorsoUnknownprecedesPartialPayment(matching the crate) and widenPaymentTopUpSourcekeys to 64-byte sr25519. This is a code change, not a version bump - re-derive any version specifics from a current checkout / the npm registry (the localtriangle-js-sdksis 0.8.3 and still divergent; there is no 0.8.4 in it).D3 ·
get_user_idis unregistered on Android → host wiringThe codec matches on both sides (
HostGetUserIdResponse { primary_username }↔ host-apiUserIdentity { primaryUsername }). dotli, iOS, and desktop each register a real handler returning{ primaryUsername }; only Android leaves it unregistered, so the container falls back toGetUserIdErr::Unknown { reason: "Not implemented" }. (This is a domainUnknown, not anUnsupported- there is noUnsupportedwire variant.)Close (T4): register the
get_user_idhandler on Android, or confirm the omission is intentional.Known & out of scope
window.postMessage, with different readiness handshakes across implementations). Same SCALE bytes, different carrier. A product selects the provider that matches its host; this is inherent to the client/host integration layer, and swapping it is expected on a migration. Out of scope for wire conformance. (The exact handshake mechanism differs between the base repo and the PR-104 sandbox layer; don't rely on a single "MessagePort vs postMessage" dichotomy.)host-containershipscreateRateLimiteras a standalone opt-in util;createContainernever wires it. TrUAPI standardizes no rate-limiting (no method, RFC, or wire id), and the runtime ships none. Whether the runtime should ever offer it as a framework capability is an open question below, not a conformance gap.coin_payment- a shared future milestone, not a divergence.CoinPaymentis part of the unifiedTrUApitrait (9 host methods, ids ≈136-163), so a fully-conformant host implements it eventually. Buthost-apihas none (only theCoinPaymentPurseId = u32selector), and on the reference side it is explicitly deferred: the PR-104truapi-servercarries an emptyimpl CoinPayment for ProductRuntimeHost {}("outside this milestone"). Both sides unimplemented → nothing to close today.statement_store/subscribedecode error seen in testing - reproduces only against a checkout with a mismatchedhost-apiinstall (0.7.9 resolved under a 0.8.2 pin). Statement types are byte-identical across the crate,@parity/truapi, and currenthost-api; a clean install resolves it. Not a protocol difference.Tasks
truapi-server→@parity/truapi-host-wasm): it emits terminal frames on framework faults (closes D1) and gives the CI a stable runtime reference. Currently on the PR-104 branch and unpublished; basetruapistill ships the TS@parity/truapi-host.host-apicode change: reorderHostPaymentTopUpErrorsoUnknownprecedesPartialPayment(match the crate) + widenPaymentTopUpSourceto 64-byte sr25519 keys. Re-derive version specifics from a current checkout / npm.triangle-js-sdks: per distinct typedecode → re-encode+ direct dual-encode (catches fixed-byte-size driftscale-tsskips on decode) + failure-path frames, vs the crate codecs (@parity/truapi, published 0.3.x). Assert a terminal typed frame on faults (novasama's behaviour). The codec half can run today; the runtime/behavioural half waits on T1.get_user_idhandler on Android (or confirm the omission is intentional).Open questions
host-containerofferscreateRateLimiterintegrator-side