Hello guys,
I was using truapi-server and I noticed that browse.dot does not work on it. I asked Fable to analyze it, and this is what it reported. Maybe the best scenario would be to change the Product-SDK to support the truapi-server convention?
TL;DR: browse.dot is broken because of a wire-contract mismatch between the
product's bundled Nova papp SDK and our in-process truapi-server host: the SDK sends a
self-generated followSubscriptionId ("follow_0") with every follow-scoped chainHead
request, our host only accepts the transport-assigned request id, so every
chainHead_v1_header/storage/call is rejected with "unknown follow subscription id" —
which the product can't even display, because of a second mismatch in the error
encoding that mangles every host error message into an empty string. The
polkadot-api client inside the product then retries at a fixed 250ms forever
(the ChainHead subfollow request failed storm) and the product's own 8s
timeout/reset watchdog loops on top (reviveCall:failed → resetBrowseSdk). The
Electron predecessor never hit this because its host is @novasamatech/host-api — the
same vendor as the product SDK — and the Flutter host explicitly tolerates the
self-generated id. The host transport itself (frame server → broker → Asset Hub WS) is
healthy and battery-verified.
follow-id contract mismatch: Nova papp SDK vs truapi-server
This is the root cause of browse.dot not working. Confidence: CONFIRMED (traced in
code at both ends, against the actual installed product bundle).
The product does not open its own WebSocket. Its bundled Nova papp SDK implements a
polkadot-api JsonRpcProvider over our frame protocol:
chainHead_v1_follow → host remote_chain_head_follow_subscribe frame subscription.
The SDK replies to polkadot-api with a synthetic subscription id — follow_${n}
from a local counter (PapiProvider, product bundle main-pretty.js:15534-15920).
- Every subsequent
chainHead_v1_header/storage/call passes that synthetic id verbatim
as followSubscriptionId.
- The truapi wire contract requires the transport-assigned request id of the subscribe
start frame (truapi/src/transport.ts:22-28; the upstream playground does
followSubscriptionId: sub.subscriptionId). The Nova SDK's transport doesn't even
expose that id (its subscribe() returns only {unsubscribe, onInterrupt},
main-pretty.js:5626-5677) — the product structurally cannot comply.
- truapi-server registers follows under
follow_id(cx.request_id())
(runtime.rs:1529) and looks the product-supplied string up
(runtime.rs:1545/1577/1593) → require_remote_follow misses →
RuntimeFailure::host_failure("unknown follow subscription id …")
(chain_runtime.rs:997-1028) → JSON-RPC error -32603 back to the product.
The follow subscription itself works (the Initialized event arrives via the
subscription stream, which needs no follow id) — only request-shaped follow-ups die.
Those follow-ups are exactly polkadot-api observable-client's "enhanced follow"
subfollow requests fired on Initialized: getRawHeader(block) plus
storage(block, "hash", "0x3a636f6465") (the :code hash, for runtime-change
detection) — matching the console message verbatim. With the follow never settling,
the papi client never obtains a runtime context, so ReviveApi dry-run calls
(reviveCall, the dotNS resolution path) are never dispatched and hit the product's
own 8s timeout.
Why it works on polkadot-desktop / mobile:
- The Electron host is
@novasamatech/host-api / host-substrate-chain-connection
0.8.10 (polkadot-desktop/package.json:104-110) — the same vendor as the product's
papp SDK; its host tolerates its own SDK's conventions.
Hello guys,
I was using
truapi-serverand I noticed thatbrowse.dotdoes not work on it. I asked Fable to analyze it, and this is what it reported. Maybe the best scenario would be to change the Product-SDK to support thetruapi-serverconvention?TL;DR: browse.dot is broken because of a wire-contract mismatch between the
product's bundled Nova papp SDK and our in-process truapi-server host: the SDK sends a
self-generated
followSubscriptionId("follow_0") with every follow-scoped chainHeadrequest, our host only accepts the transport-assigned request id, so every
chainHead_v1_header/storage/callis rejected with "unknown follow subscription id" —which the product can't even display, because of a second mismatch in the error
encoding that mangles every host error message into an empty string. The
polkadot-api client inside the product then retries at a fixed 250ms forever
(the
ChainHead subfollow request failedstorm) and the product's own 8stimeout/reset watchdog loops on top (
reviveCall:failed→resetBrowseSdk). TheElectron predecessor never hit this because its host is
@novasamatech/host-api— thesame vendor as the product SDK — and the Flutter host explicitly tolerates the
self-generated id. The host transport itself (frame server → broker → Asset Hub WS) is
healthy and battery-verified.
follow-id contract mismatch: Nova papp SDK vs truapi-server
This is the root cause of browse.dot not working. Confidence: CONFIRMED (traced in
code at both ends, against the actual installed product bundle).
The product does not open its own WebSocket. Its bundled Nova papp SDK implements a
polkadot-api
JsonRpcProviderover our frame protocol:chainHead_v1_follow→ hostremote_chain_head_follow_subscribeframe subscription.The SDK replies to polkadot-api with a synthetic subscription id —
follow_${n}from a local counter (PapiProvider, product bundle
main-pretty.js:15534-15920).chainHead_v1_header/storage/callpasses that synthetic id verbatimas
followSubscriptionId.start frame (
truapi/src/transport.ts:22-28; the upstream playground doesfollowSubscriptionId: sub.subscriptionId). The Nova SDK's transport doesn't evenexpose that id (its
subscribe()returns only{unsubscribe, onInterrupt},main-pretty.js:5626-5677) — the product structurally cannot comply.follow_id(cx.request_id())(
runtime.rs:1529) and looks the product-supplied string up(
runtime.rs:1545/1577/1593) →require_remote_followmisses →RuntimeFailure::host_failure("unknown follow subscription id …")(
chain_runtime.rs:997-1028) → JSON-RPC error-32603back to the product.The follow subscription itself works (the
Initializedevent arrives via thesubscription stream, which needs no follow id) — only request-shaped follow-ups die.
Those follow-ups are exactly polkadot-api observable-client's "enhanced follow"
subfollow requests fired on
Initialized:getRawHeader(block)plusstorage(block, "hash", "0x3a636f6465")(the:codehash, for runtime-changedetection) — matching the console message verbatim. With the follow never settling,
the papi client never obtains a runtime context, so
ReviveApidry-run calls(
reviveCall, the dotNS resolution path) are never dispatched and hit the product'sown 8s timeout.
Why it works on polkadot-desktop / mobile:
@novasamatech/host-api/host-substrate-chain-connection0.8.10 (
polkadot-desktop/package.json:104-110) — the same vendor as the product'spapp SDK; its host tolerates its own SDK's conventions.