Skip to content

Commit fe71f2a

Browse files
committed
Merge branch 'feat/create-transaction-refinement' into feat/scheduled-notifications
2 parents 3800d91 + 3938ce7 commit fe71f2a

43 files changed

Lines changed: 603 additions & 993 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,24 @@
1+
## 0.7.9 (2026-05-11)
2+
3+
### 🚀 Features
4+
5+
- **host-papp:** user attestation has moved off the Host and onto the paired Polkadot Mobile app. The Host no longer drives the attestation flow during sign-in — the mobile app handles it end-to-end as part of pairing.
6+
- **host-papp:** `UserSession` gains a `createTransaction(payload)` method. The Host can now delegate product-account transaction signing to the paired Polkadot Mobile app via the new `CreateTransactionRequest` / `CreateTransactionResponse` SSO message pair (legacy-account signing stays Host-local).
7+
- **product-sdk:** new top-level `accounts` singleton (`createAccountsProvider()` with the default sandbox transport) for products that don't need a custom transport.
8+
- **product-sdk:** export `ProductAccountId` and `LegacyAccount` types.
9+
10+
### ⚠️ Breaking Changes
11+
12+
- **host-api:** `host_create_transaction` no longer takes a separate `account_id` parameter — the account is now part of the payload as a typed `signer` field.
13+
- **host-api:** `TxPayloadV1.signer` is now required and typed (`ProductAccountId` or `AccountId`) instead of `Option<str>`.
14+
- **host-api:** dropped the `context` field from `TxPayloadV1` (runtime metadata, token symbol/decimals, best block height). The signer derives these from the chain.
15+
- **host-api:** removed the `VersionedTxPayload` envelope from `host_create_transaction*` — pass the payload directly.
16+
- **product-sdk:** `getProductAccountSigner` now returns a `PolkadotSigner` whose `signTx` routes through `host_create_transaction` and returns the full signed extrinsic; `signBytes` routes through `host_sign_raw`. Previously `signTx` called `host_sign_payload` and returned a detached signature via `getPolkadotSignerFromPjs`. Callers no longer need to assemble the extrinsic themselves.
17+
- **product-sdk:** the `Signer.createTransaction` payload shape changed to match the new `TxPayloadV1` (no `version`, no `context`, typed `signer`).
18+
- **product-sdk:** new runtime dependency `@polkadot-api/substrate-bindings@^0.20.2` (used by `getProductAccountSigner` to decode metadata locally and pick `txExtVersion`).
19+
20+
> No compatibility shim. `host_create_transaction` had no production consumers and `host_create_transaction_with_legacy_account` is only reachable via `product-sdk`, which is bumped in lockstep.
21+
122
## 0.7.8 (2026-05-08)
223

324
### 🚀 Features

__tests__/hostApi/accounts.spec.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
createTransport,
1010
toHex,
1111
} from '@novasamatech/host-api';
12-
import type { AccountConnectionStatus, ProductAccount } from '@novasamatech/host-api-wrapper';
12+
import type { AccountConnectionStatus, LegacyAccount, ProductAccount } from '@novasamatech/host-api-wrapper';
1313
import { createAccountsProvider } from '@novasamatech/host-api-wrapper';
1414
import type { ContainerHandlerOf } from '@novasamatech/host-container';
1515
import { createContainer } from '@novasamatech/host-container';
@@ -29,11 +29,15 @@ function setup() {
2929
}
3030

3131
const mockPublicKey = new Uint8Array(32).fill(1);
32-
const mockAccount: ProductAccount = {
32+
const mockProductAccount: ProductAccount = {
3333
dotNsIdentifier: 'product.dot',
3434
derivationIndex: 0,
3535
publicKey: mockPublicKey,
3636
};
37+
const mockLegacyAccount: LegacyAccount = {
38+
publicKey: mockPublicKey,
39+
name: 'Test Account',
40+
};
3741

3842
const mockRingLocation: CodecType<typeof RingLocation> = {
3943
genesisHash: toHex(new Uint8Array(32).fill(0x22)),
@@ -95,14 +99,13 @@ describe('Host API: Accounts', () => {
9599
describe('getProductAccount', () => {
96100
it('should return account on success', async () => {
97101
const { container, accountsProvider } = setup();
98-
const expected = { publicKey: mockPublicKey };
99102

100-
container.handleAccountGet((_, { ok }) => ok(expected));
103+
container.handleAccountGet((_, { ok }) => ok({ publicKey: mockPublicKey }));
101104

102105
const result = await accountsProvider.getProductAccount('product.dot', 0);
103106

104107
expect(result.isOk()).toBe(true);
105-
expect(result._unsafeUnwrap()).toEqual(expected);
108+
expect(result._unsafeUnwrap()).toEqual(mockProductAccount);
106109
});
107110

108111
it('should pass dotNsIdentifier and derivationIndex to handler', async () => {
@@ -304,7 +307,7 @@ describe('Host API: Accounts', () => {
304307
describe('getProductAccountSigner', () => {
305308
it('should expose the correct public key', () => {
306309
const { accountsProvider } = setup();
307-
const signer = accountsProvider.getProductAccountSigner(mockAccount);
310+
const signer = accountsProvider.getProductAccountSigner(mockProductAccount);
308311

309312
expect(signer.publicKey).toEqual(mockPublicKey);
310313
});
@@ -320,11 +323,11 @@ describe('Host API: Accounts', () => {
320323
return ok({ signature: toHex(signatureBytes), signedTransaction: undefined });
321324
});
322325

323-
const signer = accountsProvider.getProductAccountSigner(mockAccount);
326+
const signer = accountsProvider.getProductAccountSigner(mockProductAccount);
324327
const result = await signer.signBytes(rawData);
325328

326329
expect(capturedParams).toEqual({
327-
account: [mockAccount.dotNsIdentifier, mockAccount.derivationIndex],
330+
account: [mockProductAccount.dotNsIdentifier, mockProductAccount.derivationIndex],
328331
payload: { tag: 'Bytes', value: rawData },
329332
});
330333
expect(result).toEqual(signatureBytes);
@@ -336,7 +339,7 @@ describe('Host API: Accounts', () => {
336339

337340
container.handleSignRaw((_, { err }) => err(error));
338341

339-
const signer = accountsProvider.getProductAccountSigner(mockAccount);
342+
const signer = accountsProvider.getProductAccountSigner(mockProductAccount);
340343

341344
await expect(signer.signBytes(new Uint8Array([1, 2, 3]))).rejects.toEqual(error);
342345
});
@@ -345,7 +348,7 @@ describe('Host API: Accounts', () => {
345348
describe('getLegacyAccountSigner', () => {
346349
it('should expose the correct public key', () => {
347350
const { accountsProvider } = setup();
348-
const signer = accountsProvider.getLegacyAccountSigner(mockAccount);
351+
const signer = accountsProvider.getLegacyAccountSigner(mockLegacyAccount);
349352

350353
expect(signer.publicKey).toEqual(mockPublicKey);
351354
});
@@ -361,7 +364,7 @@ describe('Host API: Accounts', () => {
361364
return ok({ signature: toHex(signatureBytes), signedTransaction: undefined });
362365
});
363366

364-
const signer = accountsProvider.getLegacyAccountSigner(mockAccount);
367+
const signer = accountsProvider.getLegacyAccountSigner(mockLegacyAccount);
365368
const result = await signer.signBytes(rawData);
366369

367370
expect(capturedParams).toMatchObject({ payload: { tag: 'Bytes', value: rawData } });
@@ -374,7 +377,7 @@ describe('Host API: Accounts', () => {
374377

375378
container.handleSignRawWithLegacyAccount((_, { err }) => err(error));
376379

377-
const signer = accountsProvider.getLegacyAccountSigner(mockAccount);
380+
const signer = accountsProvider.getLegacyAccountSigner(mockLegacyAccount);
378381

379382
await expect(signer.signBytes(new Uint8Array([1, 2, 3]))).rejects.toEqual(error);
380383
});

__tests__/hostApi/injectedWeb3Provider.spec.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -108,12 +108,12 @@ describe('Host API: injected web3 provider', () => {
108108
const response = new Uint8Array([0, 0, 1, 1]);
109109
const payload = {
110110
version: 1 as const,
111-
signer: 'test',
111+
signer: AccountId().dec(new Uint8Array(32)),
112112
callData: '0x0002' as const,
113113
extensions: [
114114
{
115-
id: 'test',
116-
additionalSigned: '0x0000' as const,
115+
id: 'CheckGenesis',
116+
additionalSigned: toHex(new Uint8Array(32)),
117117
extra: '0x0000' as const,
118118
},
119119
],
@@ -191,12 +191,12 @@ describe('Host API: injected web3 provider', () => {
191191

192192
const payload = {
193193
version: 1 as const,
194-
signer: 'test',
194+
signer: AccountId().dec(new Uint8Array(32)),
195195
callData: '0x0002' as const,
196196
extensions: [
197197
{
198-
id: 'test',
199-
additionalSigned: '0x0000' as const,
198+
id: 'CheckGenesis',
199+
additionalSigned: toHex(new Uint8Array(32)),
200200
extra: '0x0000' as const,
201201
},
202202
],

docs/design/host-api-protocol.md

Lines changed: 15 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -147,13 +147,11 @@ fn host_account_create_proof(
147147
fn host_get_legacy_accounts() -> Result<Vec<LegacyAccount>, RequestCredentialsErr>;
148148

149149
fn host_create_transaction(
150-
accountId: ProductAccountId,
151-
payload: VersionedTxPayload
150+
payload: TxPayloadV1<ProductAccountId>
152151
) -> Result<Vec<u8>, CreateTransactionErr>;
153152

154153
fn host_create_transaction_with_legacy_account(
155-
accountId: AccountId,
156-
payload: VersionedTxPayload
154+
payload: TxPayloadV1<AccountId>
157155
) -> Result<Vec<u8>, CreateTransactionErr>;
158156

159157
fn host_sign_raw_with_legacy_account(
@@ -765,14 +763,17 @@ fn host_get_legacy_accounts() -> Result<Vec<LegacyAccount>, RequestCredentialsEr
765763

766764
#### Create Transaction
767765

768-
Based on [https://github.com/polkadot-js/api/issues/6213](https://github.com/polkadot-js/api/issues/6213), but omitting the `version` field.\
769-
This format is capable of supporting both V4 and V5 extrinsics.
770-
There are two different methods for creating a transaction: `create_transaction` and `create_transaction_with_legacy_account`. `create_transaction` is bound to the Host API account model; `create_transaction_with_legacy_account`, on the other hand, can request signing with any legacy account, and the host should decide how to find or derive accounts for signing using the `signer` field as a reference.
766+
Derived from [https://github.com/polkadot-js/api/issues/6213](https://github.com/polkadot-js/api/issues/6213) but trimmed for an online-signer topology: the `version` field is omitted, and the `context` block (runtime metadata, token symbol/decimals, best block height) is dropped — the signer (Host or Account Holder) is online and derives those from the chain identified by `CheckGenesis` rather than trusting a product-supplied blob.
767+
768+
The format is capable of supporting both V4 and V5 extrinsics.
769+
770+
There are two methods for creating a transaction: `create_transaction` and `create_transaction_with_legacy_account`. Both take a `TxPayloadV1<Signer>` parametrized by the signer-identifier type — `ProductAccountId` for product accounts and `AccountId` for legacy accounts. The `signer` field is required and typed; there is no separate `account_id` parameter.
771771

772772
```rust
773773
enum CreateTransactionErr {
774774
FailedToDecode,
775775
Rejected,
776+
// Unsupported payload version
776777
// Failed to infer missing extensions, some extension is unsupported, etc.
777778
NotSupported(str),
778779
PermissionDenied,
@@ -785,35 +786,24 @@ struct TxPayloadExtensionV1 {
785786
additional_signed: Vec<u8>
786787
}
787788

788-
struct TxPayloadContext {
789-
metadata: Vec<u8>,
790-
token_symbol: str,
791-
token_decimals: u32,
792-
best_block_height: u32
793-
}
794-
795-
struct TxPayloadV1 {
796-
signer: Option<str>,
789+
struct TxPayloadV1<Signer> {
790+
signer: Signer,
797791
call_data: Vec<u8>,
798792
extensions: Vec<TxPayloadExtensionV1>,
799-
tx_ext_version: u8,
800-
context: TxPayloadContext
801-
}
802-
803-
enum VersionedTxPayload {
804-
V1(TxPayloadV1)
793+
tx_ext_version: u8
805794
}
806795

807796
fn host_create_transaction(
808-
account_id: ProductAccountId,
809-
payload: VersionedTxPayload
797+
payload: TxPayloadV1<ProductAccountId>
810798
) -> Result<Vec<u8>, CreateTransactionErr>;
811799

812800
fn host_create_transaction_with_legacy_account(
813-
payload: VersionedTxPayload
801+
payload: TxPayloadV1<AccountId>
814802
) -> Result<Vec<u8>, CreateTransactionErr>;
815803
```
816804

805+
> Note: `TxPayloadV1<Signer>` is type-level shorthand for one concrete encoding per call site; the SCALE codec on the wire is not generic.
806+
817807
#### Signing Raw
818808

819809
Signing of raw bytes. The interface implementation is similar to `signRaw` from `injectedWeb3`, added for backward compatibility.

0 commit comments

Comments
 (0)