From ed2df7b89e51654a74c5d094a1aad59bdbc2fb78 Mon Sep 17 00:00:00 2001 From: Maksym H Date: Thu, 14 May 2026 22:41:13 +0100 Subject: [PATCH] 0.8.2: add SCALE-compact length prefix to v4 extrinsic on the wire MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In 0.8.1 the handler returned the bare inner frame [0x84][signer][sig][extras][callData] — but RPC and polkadot-api decoders expect the outer wire form with a leading compact length, so byte 0 (0x84) was read as part of the length and decoding failed. Prepend compactToU8a(inner.length) to the returned bytes so the extrinsic is wire-ready. Updated integration test #45 to strip the compact prefix before asserting on the v4 frame; the assertion now also verifies that the compact-decoded length matches the inner body length. --- CHANGELOG.md | 7 +++++++ forum-post.md | 10 +++++----- package.json | 2 +- src/browser/host-runtime.ts | 23 +++++++++++++++-------- test/integration.spec.ts | 9 ++++++--- 5 files changed, 34 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25c961f..435c1f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 0.8.2 + +### Fixed + +- **`handleCreateTransaction` now prepends the SCALE-compact length prefix** to the returned bytes. `0.8.1` returned the bare `[0x84][signer][sig][extras][callData]` frame, which is the *inner* extrinsic body — RPC and polkadot-api decoders expect the outer wire form `[compact len][0x84]…`, so a length-prefixed decoder reads byte 0 as part of the length and fails. The handler now produces the wire form directly. +- Integration test #45 was updated to strip the compact prefix before asserting on the v4 frame, so the test would have caught the missing prefix. + ## 0.8.1 ### Fixed diff --git a/forum-post.md b/forum-post.md index c62aef3..9ec7e19 100644 --- a/forum-post.md +++ b/forum-post.md @@ -349,23 +349,23 @@ _Thanks to [@TarikGul](https://github.com/TarikGul) for spotting and fixing this --- -# host-api-test-sdk 0.8.1 +# host-api-test-sdk 0.8.2 -Bumps `@novasamatech/*` to `0.7.9-4` and fixes `handleCreateTransaction` to return a real signed v4 extrinsic. In `0.8.0` it echoed `params.callData` back — fine for `result.ok === true` checks, broken the moment a product tried to submit the bytes. +Bumps `@novasamatech/*` to `0.7.9-4` and fixes `handleCreateTransaction` to return a real signed v4 extrinsic on the wire. Earlier `0.8.x` releases either echoed `callData` back or returned the inner extrinsic frame without its SCALE-compact length prefix — both of which fail the moment a product tries to submit the bytes through polkadot-api / RPC. ## What changed - **`handleCreateTransaction` / `handleCreateTransactionWithLegacyAccount`** — the request is now a flat object (`signer`, `genesisHash`, `callData`, `extensions`, `txExtVersion`); no more tuple wrapping, no `context` block, all fields are `Uint8Array`. Exports `VersionedPublicTxPayload` / `TxPayloadV1Public` are gone — use `ProductAccountTransaction` / `LegacyTransaction`. -- The handler now signs `callData || extras || additionalSigned` sr25519 and returns a v4 signed-extrinsic frame: `[0x84][MultiAddress::Id + AccountId32][Sr25519 + sig][extras][callData]`. v5 is not emitted yet (paseo-asset-hub-next runs `extrinsic.version: [4]` only). +- The handler signs `callData || extras || additionalSigned` with sr25519 and returns the full v4 wire form: `[compact len][0x84][MultiAddress::Id + AccountId32][Sr25519 + sig][extras][callData]`. v5 is not emitted yet (paseo-asset-hub-next runs `extrinsic.version: [4]` only). - Upstream also removed the attestation service and simplified SSO; `@novasamatech/product-sdk` is being renamed to `@novasamatech/host-api-wrapper` (`0.7.9-5+` is under the new name; we stay on `product-sdk@0.7.9-4` for this release). ## What you need to do -- Upgrade to `0.8.1`. No product-side code change required — the wrapper API didn't move. +- Upgrade to `0.8.2`. No product-side code change required — the wrapper API didn't move. - If you constructed `createTransaction` requests by hand, switch to the flat `ProductAccountTransaction` shape and include `genesisHash`. - If your tests asserted on the bytes being equal to `callData`, drop that assumption and decode the response as a v4 extrinsic instead. - If your runtime negotiates v5 general extrinsics, file an issue — v5 support is a follow-up. -`0.8.0` is deprecated on npm; upgrade. +`0.8.0` and `0.8.1` are superseded; skip straight to `0.8.2`. --- diff --git a/package.json b/package.json index 73d6360..f2ec1a6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@parity/host-api-test-sdk", - "version": "0.8.1", + "version": "0.8.2", "description": "Lightweight test host for Spektr product E2E testing — embeds dapps with auto-signing dev accounts, no Docker needed", "license": "MIT", "repository": { diff --git a/src/browser/host-runtime.ts b/src/browser/host-runtime.ts index eabd399..d4117ed 100644 --- a/src/browser/host-runtime.ts +++ b/src/browser/host-runtime.ts @@ -31,7 +31,7 @@ import { import { Keyring } from "@polkadot/keyring"; import type { KeyringPair } from "@polkadot/keyring/types"; import { TypeRegistry } from "@polkadot/types"; -import { u8aToHex } from "@polkadot/util"; +import { compactToU8a, u8aToHex } from "@polkadot/util"; import { blake2AsHex, blake2AsU8a, cryptoWaitReady } from "@polkadot/util-crypto"; import { ResultAsync } from "neverthrow"; import { getWsProvider } from "polkadot-api/ws"; @@ -172,7 +172,9 @@ function getPairByPublicKey(pubkey: Uint8Array): KeyringPair | undefined { * Build a v4 signed extrinsic from a SCALE-encoded call plus per-extension * `extra` / `additionalSigned` blobs, signed sr25519 by `pair`. * - * Layout (no outer compact length prefix): + * Layout (with outer SCALE-compact length prefix — what RPC and polkadot-api + * decoders expect on the wire): + * [compact len] length of the bytes that follow * [0x84] version 4 + signed bit * [0x00][AccountId32] MultiAddress::Id * [0x01][signature 64B] MultiSignature::Sr25519 @@ -208,13 +210,18 @@ function buildSignedV4Extrinsic( const toSign = payload.length > 256 ? blake2AsU8a(payload, 256) : payload; const signature = pair.sign(toSign); // sr25519, 64 bytes - const out = new Uint8Array(1 + 1 + 32 + 1 + 64 + extras.length + callData.length); + const inner = new Uint8Array(1 + 1 + 32 + 1 + 64 + extras.length + callData.length); let p = 0; - out[p++] = 0x84; - out[p++] = 0x00; out.set(pair.publicKey, p); p += 32; - out[p++] = 0x01; out.set(signature, p); p += 64; - out.set(extras, p); p += extras.length; - out.set(callData, p); + inner[p++] = 0x84; + inner[p++] = 0x00; inner.set(pair.publicKey, p); p += 32; + inner[p++] = 0x01; inner.set(signature, p); p += 64; + inner.set(extras, p); p += extras.length; + inner.set(callData, p); + + const lenPrefix = compactToU8a(inner.length); + const out = new Uint8Array(lenPrefix.length + inner.length); + out.set(lenPrefix, 0); + out.set(inner, lenPrefix.length); return out; } diff --git a/test/integration.spec.ts b/test/integration.spec.ts index 369e9c9..888c0d8 100644 --- a/test/integration.spec.ts +++ b/test/integration.spec.ts @@ -15,7 +15,7 @@ import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { Keyring } from '@polkadot/keyring'; import { cryptoWaitReady, sr25519Verify } from '@polkadot/util-crypto'; -import { hexToU8a, u8aToHex } from '@polkadot/util'; +import { compactFromU8a, hexToU8a, u8aToHex } from '@polkadot/util'; import { createTestHostServer } from '../dist/index.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -1298,8 +1298,11 @@ test.describe('Create transaction', () => { expect(result.signedHex).toBeDefined(); // test-product sends callData = [0, 0], no extensions - // expected layout: [0x84][0x00 + 32B pubkey][0x01 + 64B sig][0 extras][2B callData] - const bytes = hexToU8a(result.signedHex!); + // wire layout: [compact len][0x84][0x00 + 32B pubkey][0x01 + 64B sig][0 extras][2B callData] + const wire = hexToU8a(result.signedHex!); + const [offset, innerLen] = compactFromU8a(wire); // [bytesUsed, BN value] + const bytes = wire.slice(offset); + expect(bytes.length).toBe(innerLen.toNumber()); expect(bytes.length).toBe(1 + 1 + 32 + 1 + 64 + 2); expect(bytes[0]).toBe(0x84); // v4 + signed bit expect(bytes[1]).toBe(0x00); // MultiAddress::Id