@parity/product-sdk-host 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parity/product-sdk-host",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Host container detection and storage access for Polkadot Desktop and Mobile environments",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -18,8 +18,10 @@
18
18
  "src"
19
19
  ],
20
20
  "dependencies": {
21
- "@novasamatech/host-api": "^0.8.9",
22
- "@novasamatech/host-api-wrapper": "^0.8.9",
21
+ "@parity/truapi": "^0.3.2",
22
+ "@polkadot-api/json-rpc-provider": "^0.2.0",
23
+ "@polkadot-api/substrate-bindings": "^0.20.3",
24
+ "neverthrow": "^8.2.0",
23
25
  "polkadot-api": "^2.1.6",
24
26
  "@parity/product-sdk-logger": "0.1.1"
25
27
  },
@@ -0,0 +1,544 @@
1
+ // Copyright 2026 Parity Technologies (UK) Ltd.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * Host wallet accounts, backed by `truApi.account.*` and `truApi.signing.*`.
5
+ *
6
+ * `getAccountsProvider()` returns the full accounts surface — user identity
7
+ * (`getUserId` / `requestLogin`), the user's existing wallet accounts
8
+ * (`getLegacyAccounts`), app-scoped product accounts (`getProductAccount` /
9
+ * `getProductAccountAlias`), Ring VRF proofs (`createRingVRFProof`), connection
10
+ * status, and PAPI `PolkadotSigner` factories for both product and legacy
11
+ * accounts.
12
+ *
13
+ * The signer factories build a PAPI `PolkadotSigner` directly over
14
+ * `truApi.signing.createTransaction` (product) /
15
+ * `createTransactionWithLegacyAccount` (legacy) — `signTx` derives the
16
+ * metadata-driven `txExtVersion` and maps the signed extensions to the host's
17
+ * wire shape; `signBytes` calls `signing.signRaw(WithLegacyAccount)`. No PJS
18
+ * bridge is involved, so opaque signed extensions (e.g. Paseo Next's `AsPgas`)
19
+ * survive end-to-end.
20
+ *
21
+ * @module
22
+ */
23
+
24
+ import { decAnyMetadata, unifyMetadata } from "@polkadot-api/substrate-bindings";
25
+ import type { ResultAsync } from "neverthrow";
26
+ import { AccountId, type PolkadotSigner } from "polkadot-api";
27
+
28
+ import type {
29
+ HostAccountConnectionStatusSubscribeItem,
30
+ HostAccountCreateProofError,
31
+ HostAccountGetAliasResponse as WireAlias,
32
+ HostAccountGetError,
33
+ HostGetUserIdError,
34
+ HostRequestLoginError,
35
+ HostRequestLoginResponse,
36
+ LegacyAccount as WireLegacyAccount,
37
+ ProductAccount as WireProductAccount,
38
+ ProductAccountId,
39
+ RingLocation,
40
+ TrUApiClient,
41
+ } from "@parity/truapi";
42
+
43
+ import { getClient, subscribeWithInterrupt } from "./transport.js";
44
+ import { fromHex, toHex, unwrapHostResult } from "./truapi.js";
45
+ import type { HostSubscription } from "./types.js";
46
+
47
+ /** Ring location for Ring VRF proofs (`{ genesisHash, ringRootHash, hints? }`). Re-exported from `@parity/truapi`. */
48
+ export type { RingLocation } from "@parity/truapi";
49
+
50
+ // The account/alias shapes come from `@parity/truapi`'s generated specs; we
51
+ // derive the SDK-facing views from them so the field inventory tracks the
52
+ // protocol automatically, and override only the byte fields the adapter
53
+ // decodes (the wire types carry `0x`-prefixed `HexString`s, whereas these
54
+ // surface decoded `Uint8Array`s). Same pattern as `@parity/product-sdk-statement-store`.
55
+
56
+ /**
57
+ * One of the user's existing wallet accounts, surfaced through the host and
58
+ * identified by its public key and an optional name. Contrast with
59
+ * {@link ProductAccount}, which is also user-controlled but derived by the
60
+ * host for a specific app rather than picked from the user's existing keys.
61
+ *
62
+ * Derived from `@parity/truapi`'s `LegacyAccount`, with `publicKey` decoded to bytes.
63
+ */
64
+ export type HostAccount = Omit<WireLegacyAccount, "publicKey"> & {
65
+ /** Raw public key bytes. */
66
+ publicKey: Uint8Array;
67
+ };
68
+
69
+ /**
70
+ * A product account — an app-scoped derived account managed by the host wallet.
71
+ *
72
+ * The host derives a unique keypair for each app (identified by `dotNsIdentifier`)
73
+ * so apps get their own account that the user controls but is scoped to the app.
74
+ *
75
+ * Combines `@parity/truapi`'s `ProductAccountId` (the `{ dotNsIdentifier,
76
+ * derivationIndex }` lookup key) with the `ProductAccount` payload, with
77
+ * `publicKey` decoded to bytes.
78
+ */
79
+ export type ProductAccount = ProductAccountId &
80
+ Omit<WireProductAccount, "publicKey"> & {
81
+ /** Raw public key bytes. */
82
+ publicKey: Uint8Array;
83
+ };
84
+
85
+ /**
86
+ * A contextual alias obtained from Ring VRF.
87
+ *
88
+ * Proves account membership in a ring without revealing which account.
89
+ *
90
+ * Derived from `@parity/truapi`'s alias response, with both fields decoded to bytes.
91
+ */
92
+ export type ContextualAlias = { [K in keyof WireAlias]: Uint8Array };
93
+
94
+ /**
95
+ * Accounts provider handle, backed by `truApi.account.*` / `truApi.signing.*`.
96
+ * Surfaces the user's wallet accounts, app-scoped product accounts, Ring VRF,
97
+ * user identity, connection status, and `PolkadotSigner` factories.
98
+ *
99
+ * Lookup methods return a neverthrow `ResultAsync` (use `.match(ok, err)`);
100
+ * the signer factories return a synchronous PAPI `PolkadotSigner`.
101
+ */
102
+ export interface AccountsProvider {
103
+ getUserId(): ResultAsync<{ primaryUsername: string }, HostGetUserIdError>;
104
+ requestLogin(reason?: string): ResultAsync<HostRequestLoginResponse, HostRequestLoginError>;
105
+ getProductAccount(
106
+ dotNsIdentifier: string,
107
+ derivationIndex?: number,
108
+ ): ResultAsync<ProductAccount, HostAccountGetError>;
109
+ getProductAccountAlias(
110
+ dotNsIdentifier: string,
111
+ derivationIndex?: number,
112
+ ): ResultAsync<ContextualAlias, HostAccountGetError>;
113
+ getLegacyAccounts(): ResultAsync<HostAccount[], HostAccountGetError>;
114
+ createRingVRFProof(
115
+ dotNsIdentifier: string,
116
+ derivationIndex: number,
117
+ location: RingLocation,
118
+ message: Uint8Array,
119
+ ): ResultAsync<Uint8Array, HostAccountCreateProofError>;
120
+ /**
121
+ * Build a `PolkadotSigner` for a product account. Signing routes through the
122
+ * host's `createTransaction` path: the host decodes the metadata and forwards
123
+ * the opaque signed-extension bytes, so unknown extensions survive end-to-end.
124
+ */
125
+ getProductAccountSigner(account: ProductAccount): PolkadotSigner;
126
+ /**
127
+ * Build a `PolkadotSigner` for one of the user's existing wallet accounts.
128
+ * `name` is accepted for callsite ergonomics but unused — the signer is
129
+ * derived from `publicKey` alone.
130
+ */
131
+ getLegacyAccountSigner(account: { publicKey: Uint8Array; name?: string }): PolkadotSigner;
132
+ subscribeAccountConnectionStatus(
133
+ callback: (status: HostAccountConnectionStatusSubscribeItem) => void,
134
+ ): HostSubscription;
135
+ }
136
+
137
+ /**
138
+ * Derive the host's extrinsic-extension version from SCALE-encoded metadata:
139
+ * v4 → 0, otherwise the latest supported version. `unifyMetadata` normalizes
140
+ * v14/v15 so `.extrinsic.version` is an array.
141
+ *
142
+ * Indirected through {@link deps} so the SCALE decode (which needs a real
143
+ * metadata blob) can be stubbed in unit tests while the rest of the `signTx`
144
+ * flow — genesis extraction, extension mapping, the host call — is exercised.
145
+ */
146
+ function deriveTxExtVersion(metadata: Uint8Array): number {
147
+ const versions = unifyMetadata(decAnyMetadata(metadata)).extrinsic.version;
148
+ if (versions.length === 0) {
149
+ throw new Error("No extrinsic version found in metadata");
150
+ }
151
+ const latestVersion = versions.reduce((acc, v) => Math.max(acc, v), 0);
152
+ return latestVersion === 4 ? 0 : latestVersion;
153
+ }
154
+
155
+ /** Internal seam so `import.meta.vitest` can stub the metadata decode. @internal */
156
+ const deps = { deriveTxExtVersion };
157
+
158
+ /**
159
+ * Map a PAPI `signTx` call's signed extensions onto the host's
160
+ * `TxPayloadExtension` wire shape (hex-encoded `extra` / `additionalSigned`).
161
+ */
162
+ function toHostExtensions(
163
+ signedExtensions: Record<
164
+ string,
165
+ { identifier: string; value: Uint8Array; additionalSigned: Uint8Array }
166
+ >,
167
+ ) {
168
+ return Object.values(signedExtensions).map((ext) => ({
169
+ id: ext.identifier,
170
+ extra: toHex(ext.value),
171
+ additionalSigned: toHex(ext.additionalSigned),
172
+ }));
173
+ }
174
+
175
+ /** Build an {@link AccountsProvider} over a TruAPI client's `account` / `signing` domains. */
176
+ function adaptAccountsProvider(client: TrUApiClient): AccountsProvider {
177
+ const account = client.account;
178
+ const signing = client.signing;
179
+
180
+ return {
181
+ getUserId() {
182
+ return account.getUserId().map((response) => ({
183
+ primaryUsername: response.primaryUsername,
184
+ }));
185
+ },
186
+ requestLogin(reason) {
187
+ return account.requestLogin({ reason });
188
+ },
189
+ getProductAccount(dotNsIdentifier, derivationIndex = 0) {
190
+ return account
191
+ .getAccount({ productAccountId: { dotNsIdentifier, derivationIndex } })
192
+ .map((response) => ({
193
+ publicKey: fromHex(response.account.publicKey),
194
+ dotNsIdentifier,
195
+ derivationIndex,
196
+ }));
197
+ },
198
+ getProductAccountAlias(dotNsIdentifier, derivationIndex = 0) {
199
+ return account
200
+ .getAccountAlias({ productAccountId: { dotNsIdentifier, derivationIndex } })
201
+ .map((response) => ({
202
+ context: fromHex(response.context),
203
+ alias: fromHex(response.alias),
204
+ }));
205
+ },
206
+ getLegacyAccounts() {
207
+ return account.getLegacyAccounts().map((response) =>
208
+ response.accounts.map((a) => ({
209
+ publicKey: fromHex(a.publicKey),
210
+ name: a.name,
211
+ })),
212
+ );
213
+ },
214
+ createRingVRFProof(dotNsIdentifier, derivationIndex, location, message) {
215
+ return account
216
+ .createAccountProof({
217
+ productAccountId: { dotNsIdentifier, derivationIndex },
218
+ ringLocation: location,
219
+ context: toHex(message),
220
+ })
221
+ .map((response) => fromHex(response.proof));
222
+ },
223
+ getProductAccountSigner(account_) {
224
+ const productAccountId = {
225
+ dotNsIdentifier: account_.dotNsIdentifier,
226
+ derivationIndex: account_.derivationIndex,
227
+ };
228
+
229
+ return {
230
+ publicKey: account_.publicKey,
231
+ async signTx(callData, signedExtensions, metadata) {
232
+ const checkGenesis = signedExtensions.CheckGenesis;
233
+ if (!checkGenesis) {
234
+ throw new Error("Can't find genesis hash on transaction");
235
+ }
236
+
237
+ const response = await unwrapHostResult(
238
+ signing.createTransaction({
239
+ signer: productAccountId,
240
+ genesisHash: toHex(checkGenesis.additionalSigned),
241
+ callData: toHex(callData),
242
+ extensions: toHostExtensions(signedExtensions),
243
+ txExtVersion: deps.deriveTxExtVersion(metadata),
244
+ }),
245
+ "createTransaction failed",
246
+ );
247
+ return fromHex(response.transaction);
248
+ },
249
+ async signBytes(data) {
250
+ const response = await unwrapHostResult(
251
+ signing.signRaw({
252
+ account: productAccountId,
253
+ payload: { tag: "Bytes", value: { bytes: toHex(data) } },
254
+ }),
255
+ "signRaw failed",
256
+ );
257
+ return fromHex(response.signature);
258
+ },
259
+ };
260
+ },
261
+ getLegacyAccountSigner(account_) {
262
+ // `createTransactionWithLegacyAccount` identifies the signer by its
263
+ // raw account id (hex public key); `signRawWithLegacyAccount` takes an
264
+ // SS58 address the wallet can match. Compute both up front.
265
+ const signerHex = toHex(account_.publicKey);
266
+ const ss58Address = AccountId().dec(account_.publicKey);
267
+
268
+ return {
269
+ publicKey: account_.publicKey,
270
+ async signTx(callData, signedExtensions, metadata) {
271
+ const checkGenesis = signedExtensions.CheckGenesis;
272
+ if (!checkGenesis) {
273
+ throw new Error("Can't find genesis hash on transaction");
274
+ }
275
+
276
+ const response = await unwrapHostResult(
277
+ signing.createTransactionWithLegacyAccount({
278
+ signer: signerHex,
279
+ genesisHash: toHex(checkGenesis.additionalSigned),
280
+ callData: toHex(callData),
281
+ extensions: toHostExtensions(signedExtensions),
282
+ txExtVersion: deps.deriveTxExtVersion(metadata),
283
+ }),
284
+ "createTransactionWithLegacyAccount failed",
285
+ );
286
+ return fromHex(response.transaction);
287
+ },
288
+ async signBytes(data) {
289
+ const response = await unwrapHostResult(
290
+ signing.signRawWithLegacyAccount({
291
+ signer: ss58Address,
292
+ payload: { tag: "Bytes", value: { bytes: toHex(data) } },
293
+ }),
294
+ "signRawWithLegacyAccount failed",
295
+ );
296
+ return fromHex(response.signature);
297
+ },
298
+ };
299
+ },
300
+ subscribeAccountConnectionStatus(callback) {
301
+ return subscribeWithInterrupt(account.connectionStatusSubscribe(), callback);
302
+ },
303
+ };
304
+ }
305
+
306
+ /**
307
+ * Get the accounts provider for managing host accounts, backed by
308
+ * `truApi.account.*` / `truApi.signing.*`. Returns `null` when running outside
309
+ * a host container.
310
+ *
311
+ * @returns The accounts provider, or `null` if unavailable.
312
+ */
313
+ export async function getAccountsProvider(): Promise<AccountsProvider | null> {
314
+ const client = await getClient();
315
+ return client ? adaptAccountsProvider(client) : null;
316
+ }
317
+
318
+ if (import.meta.vitest) {
319
+ const { test, expect, vi } = import.meta.vitest;
320
+
321
+ /** Minimal fake of the truapi account/signing domains used to test the adapter. */
322
+ function makeFakeClient(opts: { onCall?: (method: string, args: unknown) => void } = {}) {
323
+ const okMatch = (value: unknown) => ({
324
+ // neverthrow ResultAsync surface used by the adapter: .map + .match.
325
+ map: (fn: (v: unknown) => unknown) => okMatch(fn(value)),
326
+ match: (ok: (v: unknown) => unknown, _err: (e: unknown) => unknown) => ok(value),
327
+ });
328
+ const method = (name: string, response: unknown) => (args: unknown) => {
329
+ opts.onCall?.(name, args);
330
+ return okMatch(response);
331
+ };
332
+ return {
333
+ account: {
334
+ getUserId: method("getUserId", { primaryUsername: "alice.dot" }),
335
+ getAccount: method("getAccount", { account: { publicKey: "0xaa" } }),
336
+ getAccountAlias: method("getAccountAlias", { context: "0x01", alias: "0x02" }),
337
+ getLegacyAccounts: method("getLegacyAccounts", {
338
+ accounts: [{ publicKey: "0xbb", name: "Bob" }],
339
+ }),
340
+ createAccountProof: method("createAccountProof", { proof: "0xc0ffee" }),
341
+ connectionStatusSubscribe: () => ({
342
+ subscribe: () => ({ unsubscribe: vi.fn() }),
343
+ [Symbol.observable as symbol]() {
344
+ return this;
345
+ },
346
+ }),
347
+ },
348
+ signing: {
349
+ createTransaction: method("createTransaction", { transaction: "0xdead" }),
350
+ createTransactionWithLegacyAccount: method("createTransactionWithLegacyAccount", {
351
+ transaction: "0xfeed",
352
+ }),
353
+ signRaw: method("signRaw", { signature: "0xbeef" }),
354
+ signRawWithLegacyAccount: method("signRawWithLegacyAccount", {
355
+ signature: "0xcafe",
356
+ }),
357
+ },
358
+ } as unknown as TrUApiClient;
359
+ }
360
+
361
+ test("getAccountsProvider returns null outside a container", async () => {
362
+ expect(await getAccountsProvider()).toBeNull();
363
+ });
364
+
365
+ test("getProductAccount decodes the public key and carries the identifier", async () => {
366
+ const calls: Array<[string, unknown]> = [];
367
+ const client = makeFakeClient({ onCall: (m, a) => calls.push([m, a]) });
368
+ const provider = adaptAccountsProvider(client);
369
+ const account = await provider.getProductAccount("app.dot", 2).match(
370
+ (a) => a,
371
+ () => null,
372
+ );
373
+ expect(calls[0]).toEqual([
374
+ "getAccount",
375
+ { productAccountId: { dotNsIdentifier: "app.dot", derivationIndex: 2 } },
376
+ ]);
377
+ expect(account).toEqual({
378
+ publicKey: fromHex("0xaa"),
379
+ dotNsIdentifier: "app.dot",
380
+ derivationIndex: 2,
381
+ });
382
+ });
383
+
384
+ test("createRingVRFProof hex-encodes the message as the proof context", async () => {
385
+ const calls: Array<[string, unknown]> = [];
386
+ const client = makeFakeClient({ onCall: (m, a) => calls.push([m, a]) });
387
+ const provider = adaptAccountsProvider(client);
388
+ const proof = await provider
389
+ .createRingVRFProof(
390
+ "app.dot",
391
+ 0,
392
+ { genesisHash: "0x01", ringRootHash: "0x02" },
393
+ new Uint8Array([1, 2, 3]),
394
+ )
395
+ .match(
396
+ (p) => p,
397
+ () => null,
398
+ );
399
+ expect(calls[0][0]).toBe("createAccountProof");
400
+ expect((calls[0][1] as { context: string }).context).toBe(toHex(new Uint8Array([1, 2, 3])));
401
+ expect(proof).toEqual(fromHex("0xc0ffee"));
402
+ });
403
+
404
+ test("the product signer signs bytes via signing.signRaw", async () => {
405
+ const calls: Array<[string, unknown]> = [];
406
+ const client = makeFakeClient({ onCall: (m, a) => calls.push([m, a]) });
407
+ const provider = adaptAccountsProvider(client);
408
+ const signer = provider.getProductAccountSigner({
409
+ dotNsIdentifier: "app.dot",
410
+ derivationIndex: 0,
411
+ publicKey: new Uint8Array(32).fill(0xaa),
412
+ });
413
+ const signature = await signer.signBytes(new Uint8Array([9, 9]));
414
+ expect(calls.at(-1)).toEqual([
415
+ "signRaw",
416
+ {
417
+ account: { dotNsIdentifier: "app.dot", derivationIndex: 0 },
418
+ payload: { tag: "Bytes", value: { bytes: toHex(new Uint8Array([9, 9])) } },
419
+ },
420
+ ]);
421
+ expect(signature).toEqual(fromHex("0xbeef"));
422
+ });
423
+
424
+ test("the legacy signer signs bytes via signing.signRawWithLegacyAccount (by SS58 address)", async () => {
425
+ const calls: Array<[string, unknown]> = [];
426
+ const client = makeFakeClient({ onCall: (m, a) => calls.push([m, a]) });
427
+ const provider = adaptAccountsProvider(client);
428
+ const publicKey = new Uint8Array(32).fill(0xbb);
429
+ const signer = provider.getLegacyAccountSigner({ publicKey });
430
+ const signature = await signer.signBytes(new Uint8Array([7, 7]));
431
+ // signRawWithLegacyAccount identifies the signer by SS58 address, not raw pubkey.
432
+ expect(calls.at(-1)).toEqual([
433
+ "signRawWithLegacyAccount",
434
+ {
435
+ signer: AccountId().dec(publicKey),
436
+ payload: { tag: "Bytes", value: { bytes: toHex(new Uint8Array([7, 7])) } },
437
+ },
438
+ ]);
439
+ expect(signature).toEqual(fromHex("0xcafe"));
440
+ });
441
+
442
+ test("the legacy signer's signTx throws without a CheckGenesis extension", async () => {
443
+ const provider = adaptAccountsProvider(makeFakeClient());
444
+ const signer = provider.getLegacyAccountSigner({
445
+ publicKey: new Uint8Array(32).fill(0xbb),
446
+ });
447
+ await expect(signer.signTx(new Uint8Array([1]), {}, new Uint8Array(), 0)).rejects.toThrow(
448
+ "Can't find genesis hash on transaction",
449
+ );
450
+ });
451
+
452
+ // Signed extensions PAPI hands to `signTx`. `CheckGenesis.additionalSigned`
453
+ // carries the genesis hash the signer pulls out; the rest are mapped to the
454
+ // host's `{ id, extra, additionalSigned }` wire shape.
455
+ const sampleExtensions = {
456
+ CheckGenesis: {
457
+ identifier: "CheckGenesis",
458
+ value: new Uint8Array([]),
459
+ additionalSigned: new Uint8Array([0x01, 0x02]),
460
+ },
461
+ CheckNonce: {
462
+ identifier: "CheckNonce",
463
+ value: new Uint8Array([0x05]),
464
+ additionalSigned: new Uint8Array([]),
465
+ },
466
+ };
467
+ const expectedHostExtensions = [
468
+ {
469
+ id: "CheckGenesis",
470
+ extra: toHex(new Uint8Array([])),
471
+ additionalSigned: toHex(new Uint8Array([0x01, 0x02])),
472
+ },
473
+ {
474
+ id: "CheckNonce",
475
+ extra: toHex(new Uint8Array([0x05])),
476
+ additionalSigned: toHex(new Uint8Array([])),
477
+ },
478
+ ];
479
+
480
+ test("the product signer's signTx builds createTransaction from genesis + extensions", async () => {
481
+ // Stub the metadata decode (needs a real SCALE blob) so the rest of the
482
+ // signTx flow — genesis extraction, extension mapping, the host call,
483
+ // response decode — is exercised against a fixed txExtVersion.
484
+ vi.spyOn(deps, "deriveTxExtVersion").mockReturnValue(0);
485
+ const calls: Array<[string, unknown]> = [];
486
+ const client = makeFakeClient({ onCall: (m, a) => calls.push([m, a]) });
487
+ const provider = adaptAccountsProvider(client);
488
+ const signer = provider.getProductAccountSigner({
489
+ dotNsIdentifier: "app.dot",
490
+ derivationIndex: 0,
491
+ publicKey: new Uint8Array(32).fill(0xaa),
492
+ });
493
+
494
+ const signed = await signer.signTx(
495
+ new Uint8Array([0xca, 0x11]),
496
+ sampleExtensions,
497
+ new Uint8Array([0x6d]),
498
+ 0,
499
+ );
500
+
501
+ expect(calls.at(-1)).toEqual([
502
+ "createTransaction",
503
+ {
504
+ signer: { dotNsIdentifier: "app.dot", derivationIndex: 0 },
505
+ genesisHash: toHex(new Uint8Array([0x01, 0x02])),
506
+ callData: toHex(new Uint8Array([0xca, 0x11])),
507
+ extensions: expectedHostExtensions,
508
+ txExtVersion: 0,
509
+ },
510
+ ]);
511
+ expect(signed).toEqual(fromHex("0xdead"));
512
+ vi.restoreAllMocks();
513
+ });
514
+
515
+ test("the legacy signer's signTx builds createTransactionWithLegacyAccount (signer = hex pubkey)", async () => {
516
+ vi.spyOn(deps, "deriveTxExtVersion").mockReturnValue(0);
517
+ const calls: Array<[string, unknown]> = [];
518
+ const client = makeFakeClient({ onCall: (m, a) => calls.push([m, a]) });
519
+ const provider = adaptAccountsProvider(client);
520
+ const publicKey = new Uint8Array(32).fill(0xbb);
521
+ const signer = provider.getLegacyAccountSigner({ publicKey });
522
+
523
+ const signed = await signer.signTx(
524
+ new Uint8Array([0xca, 0x11]),
525
+ sampleExtensions,
526
+ new Uint8Array([0x6d]),
527
+ 0,
528
+ );
529
+
530
+ expect(calls.at(-1)).toEqual([
531
+ "createTransactionWithLegacyAccount",
532
+ {
533
+ // createTransactionWithLegacyAccount identifies the signer by raw hex pubkey.
534
+ signer: toHex(publicKey),
535
+ genesisHash: toHex(new Uint8Array([0x01, 0x02])),
536
+ callData: toHex(new Uint8Array([0xca, 0x11])),
537
+ extensions: expectedHostExtensions,
538
+ txExtVersion: 0,
539
+ },
540
+ ]);
541
+ expect(signed).toEqual(fromHex("0xfeed"));
542
+ vi.restoreAllMocks();
543
+ });
544
+ }