Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
10 changes: 5 additions & 5 deletions forum-post.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

---
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
23 changes: 15 additions & 8 deletions src/browser/host-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
}

Expand Down
9 changes: 6 additions & 3 deletions test/integration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down Expand Up @@ -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
Expand Down
Loading