@parity/product-sdk-host 0.15.1 → 0.17.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/src/accounts.ts CHANGED
@@ -6,7 +6,8 @@
6
6
  * `getAccountsProvider()` returns the full accounts surface — user identity
7
7
  * (`getUserId` / `requestLogin`), the user's existing wallet accounts
8
8
  * (`getLegacyAccounts`), app-scoped product accounts (`getProductAccount` /
9
- * `getProductAccountAlias`), Ring VRF proofs (`createRingVRFProof`), connection
9
+ * `getProductAccountAlias`), Ring VRF proofs (`createRingVRFProof`), sr25519 VRF
10
+ * signatures over a caller-supplied Merlin transcript (`signVrf`), connection
10
11
  * status, and PAPI `PolkadotSigner` factories for both product and legacy
11
12
  * accounts.
12
13
  *
@@ -27,7 +28,6 @@ import { AccountId, type PolkadotSigner } from "polkadot-api";
27
28
 
28
29
  import type {
29
30
  ContextualAlias as WireAlias,
30
- DerivationIndex,
31
31
  HostAccountConnectionStatusSubscribeItem,
32
32
  HostAccountCreateProofResponse as WireRingVRFProof,
33
33
  HostRequestLoginResponse,
@@ -35,14 +35,22 @@ import type {
35
35
  ProductAccount as WireProductAccount,
36
36
  ProductAccountId,
37
37
  ProductProofContext,
38
+ RegisteredRingVrfKey as WireRegisteredRingVrfKey,
38
39
  RingLocation,
40
+ RingVrfKeyDisclosure,
39
41
  TrUApiClient,
40
42
  VersionedHostAccountCreateProofError,
41
43
  VersionedHostAccountGetAliasError,
42
44
  VersionedHostAccountGetError,
45
+ VersionedHostAccountListRingVrfKeysError,
46
+ VersionedHostAccountRegisterRingVrfKeyError,
47
+ VersionedHostAccountRingVrfSignError,
48
+ VersionedHostAccountSignVrfError,
43
49
  VersionedHostGetLegacyAccountsError,
44
50
  VersionedHostGetUserIdError,
45
51
  VersionedHostRequestLoginError,
52
+ VrfSignature as WireVrfSignature,
53
+ VrfTranscriptItem as WireVrfTranscriptItem,
46
54
  scale,
47
55
  } from "@parity/truapi";
48
56
 
@@ -58,17 +66,22 @@ import type { HostSubscription } from "./types.js";
58
66
  * (`{ productId, suffix }`), expanded by the host into the 32-byte context a
59
67
  * proof or alias is bound to.
60
68
  * - `DerivationIndex` — the tagged selector `ProductProofContext.suffix`
61
- * carries: `{ tag: "Left", value: number }` for a plain index, or
62
- * `{ tag: "Right", value: HexString }` for a raw 32-byte index.
69
+ * carries: `{ tag: "Index", value: number }` for a plain index, or
70
+ * `{ tag: "Raw", value: HexString }` for a raw 32-byte index.
63
71
  */
64
- export type { DerivationIndex, ProductProofContext, RingLocation } from "@parity/truapi";
72
+ export type {
73
+ DerivationIndex,
74
+ ProductProofContext,
75
+ RingLocation,
76
+ RingVrfKeyDisclosure,
77
+ } from "@parity/truapi";
65
78
 
66
79
  // The account/alias shapes come from `@parity/truapi`'s generated specs; we
67
80
  // derive the SDK-facing views from them so the field inventory tracks the
68
81
  // protocol automatically, and override only the fields the adapter re-encodes:
69
82
  // byte fields decoded from `0x`-prefixed `HexString`s to `Uint8Array`s, and
70
83
  // the tagged derivation-index selector kept as a plain `number` (wrapped back
71
- // into `Left` at the wire boundary). Shapes re-exported verbatim (e.g.
84
+ // into `Index` at the wire boundary). Shapes re-exported verbatim (e.g.
72
85
  // `ProductProofContext`) track the wire as-is. Same pattern as
73
86
  // `@parity/product-sdk-statement-store`.
74
87
 
@@ -105,6 +118,72 @@ export type ProductAccount = Omit<ProductAccountId, "derivationIndex"> &
105
118
  publicKey: Uint8Array;
106
119
  };
107
120
 
121
+ /**
122
+ * How callers address a product account: app identifier plus an optional index,
123
+ * defaulting to 0. A {@link ProductAccount} satisfies this, so an account from
124
+ * {@link AccountsProvider.getProductAccount} can be passed straight back in.
125
+ */
126
+ export type ProductAccountLookup = Omit<ProductAccountId, "derivationIndex"> & {
127
+ /** Plain account index within the product subtree. Defaults to 0. */
128
+ derivationIndex?: number;
129
+ };
130
+
131
+ declare const ringVrfKeyHandleBrand: unique symbol;
132
+
133
+ /**
134
+ * Opaque public name of a registered ring-VRF key.
135
+ *
136
+ * Handles come from {@link AccountsProvider.listRingVrfKeys}; product code
137
+ * cannot construct one from a derivation index.
138
+ */
139
+ export type RingVrfKeyHandle = {
140
+ readonly [ringVrfKeyHandleBrand]: "RingVrfKeyHandle";
141
+ };
142
+
143
+ /** Ring-VRF member public key, decoded from the wire's hex string. */
144
+ export type RingVrfPublicKey = Uint8Array;
145
+
146
+ /** Registered key metadata returned by the host. */
147
+ export type RegisteredRingVrfKey = Omit<WireRegisteredRingVrfKey, "handle" | "publicKey"> & {
148
+ /** Opaque handle to pass back for alias and proof requests. */
149
+ handle: RingVrfKeyHandle;
150
+ /** Present when public-key disclosure was granted. */
151
+ publicKey?: RingVrfPublicKey;
152
+ };
153
+
154
+ function sameRingLocation(a: RingLocation, b: RingLocation): boolean {
155
+ if (
156
+ a.chainId.toLowerCase() !== b.chainId.toLowerCase() ||
157
+ a.junctions.length !== b.junctions.length
158
+ ) {
159
+ return false;
160
+ }
161
+ return a.junctions.every((junction, index) => {
162
+ const candidate = b.junctions[index];
163
+ if (junction.tag === "PalletInstance") {
164
+ return candidate.tag === "PalletInstance" && junction.value === candidate.value;
165
+ }
166
+ return (
167
+ candidate.tag === "CollectionId" &&
168
+ junction.value.toLowerCase() === candidate.value.toLowerCase()
169
+ );
170
+ });
171
+ }
172
+
173
+ /**
174
+ * Select a registered key by its declared ring and return its opaque handle.
175
+ *
176
+ * Consumers must not hard-code another product's derivation index. Registry
177
+ * order breaks ties when an owner declares multiple keys for the same ring.
178
+ */
179
+ export function findRingVrfKeyHandle(
180
+ keys: RegisteredRingVrfKey[],
181
+ ring: RingLocation,
182
+ ): RingVrfKeyHandle | undefined {
183
+ return keys.find((key) => key.rings.some((candidate) => sameRingLocation(candidate, ring)))
184
+ ?.handle;
185
+ }
186
+
108
187
  /**
109
188
  * A contextual alias obtained from Ring VRF.
110
189
  *
@@ -129,6 +208,21 @@ export type RingVRFProof = Omit<WireRingVRFProof, "proof" | "contextualAlias"> &
129
208
  contextualAlias: ContextualAlias;
130
209
  };
131
210
 
211
+ /**
212
+ * One `append_message(label, value)` call replayed against a VRF transcript.
213
+ * Merlin labels are ASCII by convention: use `utf8ToBytes("round")`.
214
+ *
215
+ * Derived from `@parity/truapi`'s `VrfTranscriptItem`, decoded to bytes.
216
+ */
217
+ export type VrfTranscriptItem = { [K in keyof WireVrfTranscriptItem]: Uint8Array };
218
+
219
+ /**
220
+ * An sr25519 VRF signature: the pre-output and its DLEQ proof.
221
+ *
222
+ * Derived from `@parity/truapi`'s `VrfSignature`, decoded to bytes.
223
+ */
224
+ export type VrfSignature = { [K in keyof WireVrfSignature]: Uint8Array };
225
+
132
226
  /**
133
227
  * Accounts provider handle, backed by `truApi.account.*` / `truApi.signing.*`.
134
228
  * Surfaces the user's wallet accounts, app-scoped product accounts, Ring VRF,
@@ -152,10 +246,32 @@ export interface AccountsProvider {
152
246
  derivationIndex?: number,
153
247
  ): ResultAsync<ProductAccount, scale.CallErrorValue<VersionedHostAccountGetError>>;
154
248
  /**
155
- * Derive the contextual alias for a proof context and ring. The host
156
- * selects the member key within the ring — no per-account addressing.
249
+ * Register a ring-VRF key owned by the calling product.
250
+ *
251
+ * `index` is the plain derivation index within the product's ring-VRF
252
+ * domain; the adapter wraps it into the wire's tagged selector.
253
+ *
254
+ * Registration returns the key's public key. Call {@link listRingVrfKeys}
255
+ * afterward to obtain the opaque handle required by alias and proof calls.
157
256
  */
257
+ registerRingVrfKey(
258
+ index: number,
259
+ ring: RingLocation,
260
+ ): ResultAsync<
261
+ RingVrfPublicKey,
262
+ scale.CallErrorValue<VersionedHostAccountRegisterRingVrfKeyError>
263
+ >;
264
+ /** List an owner's registered ring-VRF keys. */
265
+ listRingVrfKeys(
266
+ owner: string,
267
+ disclosure?: RingVrfKeyDisclosure,
268
+ ): ResultAsync<
269
+ RegisteredRingVrfKey[],
270
+ scale.CallErrorValue<VersionedHostAccountListRingVrfKeysError>
271
+ >;
272
+ /** Derive a contextual alias with an explicitly registered ring-VRF key. */
158
273
  getProductAccountAlias(
274
+ keyHandle: RingVrfKeyHandle,
159
275
  context: ProductProofContext,
160
276
  location: RingLocation,
161
277
  ): ResultAsync<ContextualAlias, scale.CallErrorValue<VersionedHostAccountGetAliasError>>;
@@ -164,15 +280,50 @@ export interface AccountsProvider {
164
280
  scale.CallErrorValue<VersionedHostGetLegacyAccountsError>
165
281
  >;
166
282
  /**
167
- * Generate a Ring VRF proof binding `message` to the product-scoped
168
- * `context`. The host selects the member key within the ring; the result
169
- * carries the proof plus its verification values ({@link RingVRFProof}).
283
+ * Generate a Ring VRF proof with an explicitly registered key, binding
284
+ * `message` to the product-scoped `context`.
170
285
  */
171
286
  createRingVRFProof(
287
+ keyHandle: RingVrfKeyHandle,
172
288
  context: ProductProofContext,
173
289
  location: RingLocation,
174
290
  message: Uint8Array,
175
291
  ): ResultAsync<RingVRFProof, scale.CallErrorValue<VersionedHostAccountCreateProofError>>;
292
+ /**
293
+ * Sign `message` directly with an explicitly registered ring-VRF key.
294
+ *
295
+ * Unlike {@link createRingVRFProof} this proves nothing about ring
296
+ * membership; it is the plain signature under the member key, for
297
+ * protocols that carry their own proof.
298
+ */
299
+ ringVrfSign(
300
+ keyHandle: RingVrfKeyHandle,
301
+ message: Uint8Array,
302
+ ): ResultAsync<Uint8Array, scale.CallErrorValue<VersionedHostAccountRingVrfSignError>>;
303
+ /**
304
+ * Produce an sr25519 VRF signature from a product account (RFC-0023).
305
+ *
306
+ * The host builds a Merlin transcript from `transcriptLabel` and `items`,
307
+ * then signs it with the account's key. Unlike {@link createRingVRFProof},
308
+ * this names the signing account instead of proving ring membership.
309
+ *
310
+ * The caller owns four things the types cannot enforce:
311
+ *
312
+ * - Domain separation. A label borrowed from another protocol makes the
313
+ * output replayable across both.
314
+ * - Freshness. The VRF is deterministic, so per-round values belong in
315
+ * `items`.
316
+ * - Size. Hosts cap the transcript at 32 items and 8 KiB total.
317
+ * - Authorization. An `AutoSigning` allowance makes these calls silent. It
318
+ * is not VRF-scoped, so it covers other signing by that account too.
319
+ *
320
+ * Hosts predating the call reject it through the error channel.
321
+ */
322
+ signVrf(
323
+ account: ProductAccountLookup,
324
+ transcriptLabel: Uint8Array,
325
+ items: VrfTranscriptItem[],
326
+ ): ResultAsync<VrfSignature, scale.CallErrorValue<VersionedHostAccountSignVrfError>>;
176
327
  /**
177
328
  * Build a `PolkadotSigner` for a product account. Signing routes through the
178
329
  * host's `createTransaction` path: the host decodes the metadata and forwards
@@ -191,21 +342,29 @@ export interface AccountsProvider {
191
342
  }
192
343
 
193
344
  /**
194
- * Derive the host's extrinsic-extension version from SCALE-encoded metadata:
195
- * v4 → 0, otherwise the latest supported version. `unifyMetadata` normalizes
196
- * v14/v15 so `.extrinsic.version` is an array.
345
+ * Map metadata's supported extrinsic formats to the host wire protocol.
197
346
  *
198
- * Indirected through {@link deps} so the SCALE decode (which needs a real
199
- * metadata blob) can be stubbed in unit tests while the rest of the `signTx`
200
- * flow genesis extraction, extension mapping, the host call is exercised.
347
+ * V4 carries the account signature in its envelope. V5 General transactions
348
+ * delegate authorization to the runtime's extension pipeline, but metadata
349
+ * alone does not say whether the connected host can implement that pipeline.
350
+ * Prefer an advertised V4 until the host protocol can negotiate V5
351
+ * authorization capabilities; V5-only and unknown future runtimes retain the
352
+ * previous highest-version behavior and the host remains authoritative.
201
353
  */
202
- function deriveTxExtVersion(metadata: Uint8Array): number {
203
- const versions = unifyMetadata(decAnyMetadata(metadata)).extrinsic.version;
354
+ function selectHostTxExtVersion(versions: readonly number[]): number {
204
355
  if (versions.length === 0) {
205
356
  throw new Error("No extrinsic version found in metadata");
206
357
  }
207
- const latestVersion = versions.reduce((acc, v) => Math.max(acc, v), 0);
208
- return latestVersion === 4 ? 0 : latestVersion;
358
+ if (versions.includes(4)) {
359
+ return 0;
360
+ }
361
+ return versions.reduce((acc, version) => Math.max(acc, version), 0);
362
+ }
363
+
364
+ /** Derive the host's transaction-extension version from SCALE metadata. */
365
+ function deriveTxExtVersion(metadata: Uint8Array): number {
366
+ const versions = unifyMetadata(decAnyMetadata(metadata)).extrinsic.version;
367
+ return selectHostTxExtVersion(versions);
209
368
  }
210
369
 
211
370
  /** Internal seam so `import.meta.vitest` can stub the metadata decode. @internal */
@@ -228,12 +387,17 @@ function toHostExtensions(
228
387
  }));
229
388
  }
230
389
 
231
- /** Build the wire `ProductAccountId`, wrapping the plain index as a `Left` selector. */
232
- function toWireProductAccountId(
233
- dotNsIdentifier: string,
234
- derivationIndex: number,
235
- ): ProductAccountId {
236
- return { dotNsIdentifier, derivationIndex: { tag: "Left", value: derivationIndex } };
390
+ /**
391
+ * Build the wire `ProductAccountId`: default the index to 0, wrap it as `Index`.
392
+ *
393
+ * Destructured rather than spread, so passing a full {@link ProductAccount}
394
+ * cannot leak its `publicKey` onto the wire.
395
+ */
396
+ function toWireProductAccountId({
397
+ dotNsIdentifier,
398
+ derivationIndex = 0,
399
+ }: ProductAccountLookup): ProductAccountId {
400
+ return { dotNsIdentifier, derivationIndex: { tag: "Index", value: derivationIndex } };
237
401
  }
238
402
 
239
403
  /** Build an {@link AccountsProvider} over a TruAPI client's `account` / `signing` domains. */
@@ -253,7 +417,7 @@ function adaptAccountsProvider(client: TrUApiClient): AccountsProvider {
253
417
  getProductAccount(dotNsIdentifier, derivationIndex = 0) {
254
418
  return account
255
419
  .getAccount({
256
- productAccountId: toWireProductAccountId(dotNsIdentifier, derivationIndex),
420
+ productAccountId: toWireProductAccountId({ dotNsIdentifier, derivationIndex }),
257
421
  })
258
422
  .map((response) => ({
259
423
  publicKey: fromHex(response.account.publicKey),
@@ -261,11 +425,31 @@ function adaptAccountsProvider(client: TrUApiClient): AccountsProvider {
261
425
  derivationIndex,
262
426
  }));
263
427
  },
264
- getProductAccountAlias(context, location) {
265
- return account.getAccountAlias({ context, ringLocation: location }).map((response) => ({
266
- context: fromHex(response.context),
267
- alias: fromHex(response.alias),
268
- }));
428
+ registerRingVrfKey(index, ring) {
429
+ return account
430
+ .registerRingVrfKey({ index: { tag: "Index", value: index }, ring })
431
+ .map(fromHex);
432
+ },
433
+ listRingVrfKeys(owner, disclosure = "Anonymized") {
434
+ return account.listRingVrfKeys({ owner, disclosure }).map((keys) =>
435
+ keys.map((key) => ({
436
+ ...key,
437
+ handle: key.handle as unknown as RingVrfKeyHandle,
438
+ publicKey: key.publicKey === undefined ? undefined : fromHex(key.publicKey),
439
+ })),
440
+ );
441
+ },
442
+ getProductAccountAlias(keyHandle, context, location) {
443
+ return account
444
+ .getAccountAlias({
445
+ keyHandle: keyHandle as unknown as ProductAccountId,
446
+ context,
447
+ ringLocation: location,
448
+ })
449
+ .map((response) => ({
450
+ context: fromHex(response.context),
451
+ alias: fromHex(response.alias),
452
+ }));
269
453
  },
270
454
  getLegacyAccounts() {
271
455
  return account.getLegacyAccounts().map((response) =>
@@ -275,9 +459,10 @@ function adaptAccountsProvider(client: TrUApiClient): AccountsProvider {
275
459
  })),
276
460
  );
277
461
  },
278
- createRingVRFProof(context, location, message) {
462
+ createRingVRFProof(keyHandle, context, location, message) {
279
463
  return account
280
464
  .createAccountProof({
465
+ keyHandle: keyHandle as unknown as ProductAccountId,
281
466
  context,
282
467
  ringLocation: location,
283
468
  message: toHex(message),
@@ -292,11 +477,31 @@ function adaptAccountsProvider(client: TrUApiClient): AccountsProvider {
292
477
  ringRevision: response.ringRevision,
293
478
  }));
294
479
  },
480
+ ringVrfSign(keyHandle, message) {
481
+ return account
482
+ .ringVrfSign({
483
+ keyHandle: keyHandle as unknown as ProductAccountId,
484
+ message: toHex(message),
485
+ })
486
+ .map(fromHex);
487
+ },
488
+ signVrf(account_, transcriptLabel, items) {
489
+ return account
490
+ .signVrf({
491
+ account: toWireProductAccountId(account_),
492
+ transcriptLabel: toHex(transcriptLabel),
493
+ items: items.map(({ label, value }) => ({
494
+ label: toHex(label),
495
+ value: toHex(value),
496
+ })),
497
+ })
498
+ .map((response) => ({
499
+ preOutput: fromHex(response.preOutput),
500
+ proof: fromHex(response.proof),
501
+ }));
502
+ },
295
503
  getProductAccountSigner(account_) {
296
- const productAccountId = toWireProductAccountId(
297
- account_.dotNsIdentifier,
298
- account_.derivationIndex,
299
- );
504
+ const productAccountId = toWireProductAccountId(account_);
300
505
 
301
506
  return {
302
507
  publicKey: account_.publicKey,
@@ -390,6 +595,22 @@ export async function getAccountsProvider(): Promise<AccountsProvider | null> {
390
595
  if (import.meta.vitest) {
391
596
  const { test, expect, vi } = import.meta.vitest;
392
597
 
598
+ test("host signing prefers V4 on a dual V4/V5 runtime", () => {
599
+ expect(selectHostTxExtVersion([4, 5])).toBe(0);
600
+ });
601
+
602
+ test("host signing uses V5 when V4 is unavailable", () => {
603
+ expect(selectHostTxExtVersion([5])).toBe(5);
604
+ });
605
+
606
+ test("host signing maps a V4-only runtime to the wire sentinel", () => {
607
+ expect(selectHostTxExtVersion([4])).toBe(0);
608
+ });
609
+
610
+ test("host signing rejects metadata with no extrinsic version", () => {
611
+ expect(() => selectHostTxExtVersion([])).toThrow("No extrinsic version found in metadata");
612
+ });
613
+
393
614
  /** Minimal fake of the truapi account/signing domains used to test the adapter. */
394
615
  function makeFakeClient(opts: { onCall?: (method: string, args: unknown) => void } = {}) {
395
616
  const okMatch = (value: unknown) => ({
@@ -405,6 +626,35 @@ if (import.meta.vitest) {
405
626
  account: {
406
627
  getUserId: method("getUserId", { primaryUsername: "alice.dot" }),
407
628
  getAccount: method("getAccount", { account: { publicKey: "0xaa" } }),
629
+ registerRingVrfKey: method("registerRingVrfKey", "0x0304"),
630
+ ringVrfSign: method("ringVrfSign", "0xba5eba11"),
631
+ listRingVrfKeys: method("listRingVrfKeys", [
632
+ {
633
+ handle: {
634
+ dotNsIdentifier: "people.dot",
635
+ derivationIndex: { tag: "Index", value: 0 },
636
+ },
637
+ rings: [
638
+ {
639
+ chainId: "0x01",
640
+ junctions: [{ tag: "PalletInstance", value: 1 }],
641
+ },
642
+ ],
643
+ },
644
+ {
645
+ handle: {
646
+ dotNsIdentifier: "people.dot",
647
+ derivationIndex: { tag: "Index", value: 1 },
648
+ },
649
+ rings: [
650
+ {
651
+ chainId: "0x02",
652
+ junctions: [{ tag: "CollectionId", value: "0xaabb" }],
653
+ },
654
+ ],
655
+ publicKey: "0x0102",
656
+ },
657
+ ]),
408
658
  getAccountAlias: method("getAccountAlias", { context: "0x01", alias: "0x02" }),
409
659
  getLegacyAccounts: method("getLegacyAccounts", {
410
660
  accounts: [{ publicKey: "0xbb", name: "Bob" }],
@@ -415,6 +665,7 @@ if (import.meta.vitest) {
415
665
  ringIndex: 3,
416
666
  ringRevision: 7,
417
667
  }),
668
+ signVrf: method("signVrf", { preOutput: "0xaa11", proof: "0xbb22" }),
418
669
  connectionStatusSubscribe: () => ({
419
670
  subscribe: () => ({ unsubscribe: vi.fn() }),
420
671
  [Symbol.observable as symbol]() {
@@ -452,7 +703,7 @@ if (import.meta.vitest) {
452
703
  {
453
704
  productAccountId: {
454
705
  dotNsIdentifier: "app.dot",
455
- derivationIndex: { tag: "Left", value: 2 },
706
+ derivationIndex: { tag: "Index", value: 2 },
456
707
  },
457
708
  },
458
709
  ]);
@@ -463,13 +714,135 @@ if (import.meta.vitest) {
463
714
  });
464
715
  });
465
716
 
717
+ test("getProductAccount defaults the derivation index in both the request and the result", async () => {
718
+ const calls: Array<[string, unknown]> = [];
719
+ const client = makeFakeClient({ onCall: (m, a) => calls.push([m, a]) });
720
+ const provider = adaptAccountsProvider(client);
721
+ const account = await provider.getProductAccount("app.dot").match(
722
+ (a) => a,
723
+ () => null,
724
+ );
725
+ expect(calls[0]).toEqual([
726
+ "getAccount",
727
+ {
728
+ productAccountId: {
729
+ dotNsIdentifier: "app.dot",
730
+ derivationIndex: { tag: "Index", value: 0 },
731
+ },
732
+ },
733
+ ]);
734
+ // The resolved index must reach the caller too, not just the wire.
735
+ expect(account?.derivationIndex).toBe(0);
736
+ });
737
+
738
+ test("registerRingVrfKey wraps the numeric index and decodes the public key", async () => {
739
+ const calls: Array<[string, unknown]> = [];
740
+ const provider = adaptAccountsProvider(
741
+ makeFakeClient({ onCall: (method, args) => calls.push([method, args]) }),
742
+ );
743
+ const ring: RingLocation = {
744
+ chainId: "0x01",
745
+ junctions: [{ tag: "PalletInstance", value: 67 }],
746
+ };
747
+ const index = 2;
748
+ const publicKey = await provider.registerRingVrfKey(index, ring).match(
749
+ (value) => value,
750
+ () => null,
751
+ );
752
+
753
+ expect(calls[0]).toEqual([
754
+ "registerRingVrfKey",
755
+ { index: { tag: "Index", value: 2 }, ring },
756
+ ]);
757
+ expect(publicKey).toEqual(fromHex("0x0304"));
758
+ });
759
+
760
+ test("listRingVrfKeys selects by ring without exposing a raw index", async () => {
761
+ const calls: Array<[string, unknown]> = [];
762
+ const provider = adaptAccountsProvider(
763
+ makeFakeClient({ onCall: (method, args) => calls.push([method, args]) }),
764
+ );
765
+ const keys = await provider.listRingVrfKeys("people.dot", "PublicKey").match(
766
+ (value) => value,
767
+ () => [],
768
+ );
769
+ expect(calls[0]).toEqual([
770
+ "listRingVrfKeys",
771
+ { owner: "people.dot", disclosure: "PublicKey" },
772
+ ]);
773
+ expect(keys[1].publicKey).toEqual(fromHex("0x0102"));
774
+ expect(
775
+ findRingVrfKeyHandle(keys, {
776
+ chainId: "0x02",
777
+ junctions: [{ tag: "CollectionId", value: "0xAABB" }],
778
+ }),
779
+ ).toEqual(keys[1].handle);
780
+ });
781
+
782
+ test("getProductAccountAlias passes the selected key handle", async () => {
783
+ const calls: Array<[string, unknown]> = [];
784
+ const provider = adaptAccountsProvider(
785
+ makeFakeClient({ onCall: (method, args) => calls.push([method, args]) }),
786
+ );
787
+ const keys = await provider.listRingVrfKeys("people.dot").match(
788
+ (value) => value,
789
+ () => [],
790
+ );
791
+ calls.length = 0;
792
+ const keyHandle = keys[1].handle;
793
+ const context: ProductProofContext = {
794
+ productId: "app.dot",
795
+ suffix: { tag: "Index", value: 0 },
796
+ };
797
+ const ring: RingLocation = {
798
+ chainId: "0x01",
799
+ junctions: [{ tag: "PalletInstance", value: 1 }],
800
+ };
801
+ const alias = await provider.getProductAccountAlias(keyHandle, context, ring).match(
802
+ (value) => value,
803
+ () => null,
804
+ );
805
+ expect(calls[0]).toEqual(["getAccountAlias", { keyHandle, context, ringLocation: ring }]);
806
+ expect(alias).toEqual({ context: fromHex("0x01"), alias: fromHex("0x02") });
807
+ });
808
+
809
+ test("ringVrfSign passes the selected handle and decodes the signature", async () => {
810
+ const calls: Array<[string, unknown]> = [];
811
+ const provider = adaptAccountsProvider(
812
+ makeFakeClient({ onCall: (method, args) => calls.push([method, args]) }),
813
+ );
814
+ const keys = await provider.listRingVrfKeys("people.dot").match(
815
+ (value) => value,
816
+ () => [],
817
+ );
818
+ calls.length = 0;
819
+ const keyHandle = keys[1].handle;
820
+ const signature = await provider.ringVrfSign(keyHandle, new Uint8Array([1, 2, 3])).match(
821
+ (value) => value,
822
+ () => null,
823
+ );
824
+ expect(calls[0]).toEqual([
825
+ "ringVrfSign",
826
+ { keyHandle, message: toHex(new Uint8Array([1, 2, 3])) },
827
+ ]);
828
+ expect(signature).toEqual(fromHex("0xba5eba11"));
829
+ });
830
+
466
831
  test("createRingVRFProof hex-encodes the message and decodes the proof response", async () => {
467
832
  const calls: Array<[string, unknown]> = [];
468
833
  const client = makeFakeClient({ onCall: (m, a) => calls.push([m, a]) });
469
834
  const provider = adaptAccountsProvider(client);
835
+ const keyHandle = (
836
+ await provider.listRingVrfKeys("people.dot").match(
837
+ (value) => value,
838
+ () => [],
839
+ )
840
+ )[0].handle;
841
+ calls.length = 0;
470
842
  const proof = await provider
471
843
  .createRingVRFProof(
472
- { productId: "app.dot", suffix: { tag: "Left", value: 0 } },
844
+ keyHandle,
845
+ { productId: "app.dot", suffix: { tag: "Index", value: 0 } },
473
846
  { chainId: "0x01", junctions: [{ tag: "PalletInstance", value: 1 }] },
474
847
  new Uint8Array([1, 2, 3]),
475
848
  )
@@ -479,7 +852,11 @@ if (import.meta.vitest) {
479
852
  );
480
853
  expect(calls[0][0]).toBe("createAccountProof");
481
854
  expect(calls[0][1]).toEqual({
482
- context: { productId: "app.dot", suffix: { tag: "Left", value: 0 } },
855
+ keyHandle: {
856
+ dotNsIdentifier: "people.dot",
857
+ derivationIndex: { tag: "Index", value: 0 },
858
+ },
859
+ context: { productId: "app.dot", suffix: { tag: "Index", value: 0 } },
483
860
  ringLocation: { chainId: "0x01", junctions: [{ tag: "PalletInstance", value: 1 }] },
484
861
  message: toHex(new Uint8Array([1, 2, 3])),
485
862
  });
@@ -491,6 +868,68 @@ if (import.meta.vitest) {
491
868
  });
492
869
  });
493
870
 
871
+ test("signVrf hex-encodes the transcript and decodes the signature", async () => {
872
+ const calls: Array<[string, unknown]> = [];
873
+ const client = makeFakeClient({ onCall: (m, a) => calls.push([m, a]) });
874
+ const provider = adaptAccountsProvider(client);
875
+ const transcriptLabel = new Uint8Array([1, 2, 3]);
876
+ const itemLabel = new Uint8Array([4]);
877
+ const itemValue = new Uint8Array([5, 6]);
878
+ const signature = await provider
879
+ .signVrf({ dotNsIdentifier: "app.dot", derivationIndex: 3 }, transcriptLabel, [
880
+ { label: itemLabel, value: itemValue },
881
+ ])
882
+ .match(
883
+ (s) => s,
884
+ () => null,
885
+ );
886
+ expect(calls[0]).toEqual([
887
+ "signVrf",
888
+ {
889
+ account: {
890
+ dotNsIdentifier: "app.dot",
891
+ derivationIndex: { tag: "Index", value: 3 },
892
+ },
893
+ transcriptLabel: toHex(transcriptLabel),
894
+ items: [{ label: toHex(itemLabel), value: toHex(itemValue) }],
895
+ },
896
+ ]);
897
+ expect(signature).toEqual({ preOutput: fromHex("0xaa11"), proof: fromHex("0xbb22") });
898
+ });
899
+
900
+ test("signVrf defaults the derivation index", async () => {
901
+ const calls: Array<[string, unknown]> = [];
902
+ const client = makeFakeClient({ onCall: (m, a) => calls.push([m, a]) });
903
+ const provider = adaptAccountsProvider(client);
904
+ await provider.signVrf({ dotNsIdentifier: "app.dot" }, new Uint8Array([1]), []).match(
905
+ (s) => s,
906
+ () => null,
907
+ );
908
+ expect((calls[0][1] as { account: unknown }).account).toEqual({
909
+ dotNsIdentifier: "app.dot",
910
+ derivationIndex: { tag: "Index", value: 0 },
911
+ });
912
+ });
913
+
914
+ test("signVrf sends only the id fields when given a full product account", async () => {
915
+ const calls: Array<[string, unknown]> = [];
916
+ const client = makeFakeClient({ onCall: (m, a) => calls.push([m, a]) });
917
+ const provider = adaptAccountsProvider(client);
918
+ const account: ProductAccount = {
919
+ dotNsIdentifier: "app.dot",
920
+ derivationIndex: 1,
921
+ publicKey: new Uint8Array(32).fill(0xaa),
922
+ };
923
+ await provider.signVrf(account, new Uint8Array([1]), []).match(
924
+ (s) => s,
925
+ () => null,
926
+ );
927
+ expect(Object.keys((calls[0][1] as { account: object }).account)).toEqual([
928
+ "dotNsIdentifier",
929
+ "derivationIndex",
930
+ ]);
931
+ });
932
+
494
933
  test("the product signer signs bytes via signing.signRaw", async () => {
495
934
  const calls: Array<[string, unknown]> = [];
496
935
  const client = makeFakeClient({ onCall: (m, a) => calls.push([m, a]) });
@@ -504,7 +943,10 @@ if (import.meta.vitest) {
504
943
  expect(calls.at(-1)).toEqual([
505
944
  "signRaw",
506
945
  {
507
- account: { dotNsIdentifier: "app.dot", derivationIndex: { tag: "Left", value: 0 } },
946
+ account: {
947
+ dotNsIdentifier: "app.dot",
948
+ derivationIndex: { tag: "Index", value: 0 },
949
+ },
508
950
  payload: { tag: "Bytes", value: { bytes: toHex(new Uint8Array([9, 9])) } },
509
951
  },
510
952
  ]);
@@ -591,7 +1033,7 @@ if (import.meta.vitest) {
591
1033
  expect(calls.at(-1)).toEqual([
592
1034
  "createTransaction",
593
1035
  {
594
- signer: { dotNsIdentifier: "app.dot", derivationIndex: { tag: "Left", value: 0 } },
1036
+ signer: { dotNsIdentifier: "app.dot", derivationIndex: { tag: "Index", value: 0 } },
595
1037
  genesisHash: toHex(new Uint8Array([0x01, 0x02])),
596
1038
  callData: toHex(new Uint8Array([0xca, 0x11])),
597
1039
  extensions: expectedHostExtensions,