@parity/product-sdk-host 0.15.1 → 0.16.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
@@ -228,12 +379,17 @@ function toHostExtensions(
228
379
  }));
229
380
  }
230
381
 
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 } };
382
+ /**
383
+ * Build the wire `ProductAccountId`: default the index to 0, wrap it as `Index`.
384
+ *
385
+ * Destructured rather than spread, so passing a full {@link ProductAccount}
386
+ * cannot leak its `publicKey` onto the wire.
387
+ */
388
+ function toWireProductAccountId({
389
+ dotNsIdentifier,
390
+ derivationIndex = 0,
391
+ }: ProductAccountLookup): ProductAccountId {
392
+ return { dotNsIdentifier, derivationIndex: { tag: "Index", value: derivationIndex } };
237
393
  }
238
394
 
239
395
  /** Build an {@link AccountsProvider} over a TruAPI client's `account` / `signing` domains. */
@@ -253,7 +409,7 @@ function adaptAccountsProvider(client: TrUApiClient): AccountsProvider {
253
409
  getProductAccount(dotNsIdentifier, derivationIndex = 0) {
254
410
  return account
255
411
  .getAccount({
256
- productAccountId: toWireProductAccountId(dotNsIdentifier, derivationIndex),
412
+ productAccountId: toWireProductAccountId({ dotNsIdentifier, derivationIndex }),
257
413
  })
258
414
  .map((response) => ({
259
415
  publicKey: fromHex(response.account.publicKey),
@@ -261,11 +417,31 @@ function adaptAccountsProvider(client: TrUApiClient): AccountsProvider {
261
417
  derivationIndex,
262
418
  }));
263
419
  },
264
- getProductAccountAlias(context, location) {
265
- return account.getAccountAlias({ context, ringLocation: location }).map((response) => ({
266
- context: fromHex(response.context),
267
- alias: fromHex(response.alias),
268
- }));
420
+ registerRingVrfKey(index, ring) {
421
+ return account
422
+ .registerRingVrfKey({ index: { tag: "Index", value: index }, ring })
423
+ .map(fromHex);
424
+ },
425
+ listRingVrfKeys(owner, disclosure = "Anonymized") {
426
+ return account.listRingVrfKeys({ owner, disclosure }).map((keys) =>
427
+ keys.map((key) => ({
428
+ ...key,
429
+ handle: key.handle as unknown as RingVrfKeyHandle,
430
+ publicKey: key.publicKey === undefined ? undefined : fromHex(key.publicKey),
431
+ })),
432
+ );
433
+ },
434
+ getProductAccountAlias(keyHandle, context, location) {
435
+ return account
436
+ .getAccountAlias({
437
+ keyHandle: keyHandle as unknown as ProductAccountId,
438
+ context,
439
+ ringLocation: location,
440
+ })
441
+ .map((response) => ({
442
+ context: fromHex(response.context),
443
+ alias: fromHex(response.alias),
444
+ }));
269
445
  },
270
446
  getLegacyAccounts() {
271
447
  return account.getLegacyAccounts().map((response) =>
@@ -275,9 +451,10 @@ function adaptAccountsProvider(client: TrUApiClient): AccountsProvider {
275
451
  })),
276
452
  );
277
453
  },
278
- createRingVRFProof(context, location, message) {
454
+ createRingVRFProof(keyHandle, context, location, message) {
279
455
  return account
280
456
  .createAccountProof({
457
+ keyHandle: keyHandle as unknown as ProductAccountId,
281
458
  context,
282
459
  ringLocation: location,
283
460
  message: toHex(message),
@@ -292,11 +469,31 @@ function adaptAccountsProvider(client: TrUApiClient): AccountsProvider {
292
469
  ringRevision: response.ringRevision,
293
470
  }));
294
471
  },
472
+ ringVrfSign(keyHandle, message) {
473
+ return account
474
+ .ringVrfSign({
475
+ keyHandle: keyHandle as unknown as ProductAccountId,
476
+ message: toHex(message),
477
+ })
478
+ .map(fromHex);
479
+ },
480
+ signVrf(account_, transcriptLabel, items) {
481
+ return account
482
+ .signVrf({
483
+ account: toWireProductAccountId(account_),
484
+ transcriptLabel: toHex(transcriptLabel),
485
+ items: items.map(({ label, value }) => ({
486
+ label: toHex(label),
487
+ value: toHex(value),
488
+ })),
489
+ })
490
+ .map((response) => ({
491
+ preOutput: fromHex(response.preOutput),
492
+ proof: fromHex(response.proof),
493
+ }));
494
+ },
295
495
  getProductAccountSigner(account_) {
296
- const productAccountId = toWireProductAccountId(
297
- account_.dotNsIdentifier,
298
- account_.derivationIndex,
299
- );
496
+ const productAccountId = toWireProductAccountId(account_);
300
497
 
301
498
  return {
302
499
  publicKey: account_.publicKey,
@@ -405,6 +602,35 @@ if (import.meta.vitest) {
405
602
  account: {
406
603
  getUserId: method("getUserId", { primaryUsername: "alice.dot" }),
407
604
  getAccount: method("getAccount", { account: { publicKey: "0xaa" } }),
605
+ registerRingVrfKey: method("registerRingVrfKey", "0x0304"),
606
+ ringVrfSign: method("ringVrfSign", "0xba5eba11"),
607
+ listRingVrfKeys: method("listRingVrfKeys", [
608
+ {
609
+ handle: {
610
+ dotNsIdentifier: "people.dot",
611
+ derivationIndex: { tag: "Index", value: 0 },
612
+ },
613
+ rings: [
614
+ {
615
+ chainId: "0x01",
616
+ junctions: [{ tag: "PalletInstance", value: 1 }],
617
+ },
618
+ ],
619
+ },
620
+ {
621
+ handle: {
622
+ dotNsIdentifier: "people.dot",
623
+ derivationIndex: { tag: "Index", value: 1 },
624
+ },
625
+ rings: [
626
+ {
627
+ chainId: "0x02",
628
+ junctions: [{ tag: "CollectionId", value: "0xaabb" }],
629
+ },
630
+ ],
631
+ publicKey: "0x0102",
632
+ },
633
+ ]),
408
634
  getAccountAlias: method("getAccountAlias", { context: "0x01", alias: "0x02" }),
409
635
  getLegacyAccounts: method("getLegacyAccounts", {
410
636
  accounts: [{ publicKey: "0xbb", name: "Bob" }],
@@ -415,6 +641,7 @@ if (import.meta.vitest) {
415
641
  ringIndex: 3,
416
642
  ringRevision: 7,
417
643
  }),
644
+ signVrf: method("signVrf", { preOutput: "0xaa11", proof: "0xbb22" }),
418
645
  connectionStatusSubscribe: () => ({
419
646
  subscribe: () => ({ unsubscribe: vi.fn() }),
420
647
  [Symbol.observable as symbol]() {
@@ -452,7 +679,7 @@ if (import.meta.vitest) {
452
679
  {
453
680
  productAccountId: {
454
681
  dotNsIdentifier: "app.dot",
455
- derivationIndex: { tag: "Left", value: 2 },
682
+ derivationIndex: { tag: "Index", value: 2 },
456
683
  },
457
684
  },
458
685
  ]);
@@ -463,13 +690,135 @@ if (import.meta.vitest) {
463
690
  });
464
691
  });
465
692
 
693
+ test("getProductAccount defaults the derivation index in both the request and the result", async () => {
694
+ const calls: Array<[string, unknown]> = [];
695
+ const client = makeFakeClient({ onCall: (m, a) => calls.push([m, a]) });
696
+ const provider = adaptAccountsProvider(client);
697
+ const account = await provider.getProductAccount("app.dot").match(
698
+ (a) => a,
699
+ () => null,
700
+ );
701
+ expect(calls[0]).toEqual([
702
+ "getAccount",
703
+ {
704
+ productAccountId: {
705
+ dotNsIdentifier: "app.dot",
706
+ derivationIndex: { tag: "Index", value: 0 },
707
+ },
708
+ },
709
+ ]);
710
+ // The resolved index must reach the caller too, not just the wire.
711
+ expect(account?.derivationIndex).toBe(0);
712
+ });
713
+
714
+ test("registerRingVrfKey wraps the numeric index and decodes the public key", async () => {
715
+ const calls: Array<[string, unknown]> = [];
716
+ const provider = adaptAccountsProvider(
717
+ makeFakeClient({ onCall: (method, args) => calls.push([method, args]) }),
718
+ );
719
+ const ring: RingLocation = {
720
+ chainId: "0x01",
721
+ junctions: [{ tag: "PalletInstance", value: 67 }],
722
+ };
723
+ const index = 2;
724
+ const publicKey = await provider.registerRingVrfKey(index, ring).match(
725
+ (value) => value,
726
+ () => null,
727
+ );
728
+
729
+ expect(calls[0]).toEqual([
730
+ "registerRingVrfKey",
731
+ { index: { tag: "Index", value: 2 }, ring },
732
+ ]);
733
+ expect(publicKey).toEqual(fromHex("0x0304"));
734
+ });
735
+
736
+ test("listRingVrfKeys selects by ring without exposing a raw index", async () => {
737
+ const calls: Array<[string, unknown]> = [];
738
+ const provider = adaptAccountsProvider(
739
+ makeFakeClient({ onCall: (method, args) => calls.push([method, args]) }),
740
+ );
741
+ const keys = await provider.listRingVrfKeys("people.dot", "PublicKey").match(
742
+ (value) => value,
743
+ () => [],
744
+ );
745
+ expect(calls[0]).toEqual([
746
+ "listRingVrfKeys",
747
+ { owner: "people.dot", disclosure: "PublicKey" },
748
+ ]);
749
+ expect(keys[1].publicKey).toEqual(fromHex("0x0102"));
750
+ expect(
751
+ findRingVrfKeyHandle(keys, {
752
+ chainId: "0x02",
753
+ junctions: [{ tag: "CollectionId", value: "0xAABB" }],
754
+ }),
755
+ ).toEqual(keys[1].handle);
756
+ });
757
+
758
+ test("getProductAccountAlias passes the selected key handle", async () => {
759
+ const calls: Array<[string, unknown]> = [];
760
+ const provider = adaptAccountsProvider(
761
+ makeFakeClient({ onCall: (method, args) => calls.push([method, args]) }),
762
+ );
763
+ const keys = await provider.listRingVrfKeys("people.dot").match(
764
+ (value) => value,
765
+ () => [],
766
+ );
767
+ calls.length = 0;
768
+ const keyHandle = keys[1].handle;
769
+ const context: ProductProofContext = {
770
+ productId: "app.dot",
771
+ suffix: { tag: "Index", value: 0 },
772
+ };
773
+ const ring: RingLocation = {
774
+ chainId: "0x01",
775
+ junctions: [{ tag: "PalletInstance", value: 1 }],
776
+ };
777
+ const alias = await provider.getProductAccountAlias(keyHandle, context, ring).match(
778
+ (value) => value,
779
+ () => null,
780
+ );
781
+ expect(calls[0]).toEqual(["getAccountAlias", { keyHandle, context, ringLocation: ring }]);
782
+ expect(alias).toEqual({ context: fromHex("0x01"), alias: fromHex("0x02") });
783
+ });
784
+
785
+ test("ringVrfSign passes the selected handle and decodes the signature", async () => {
786
+ const calls: Array<[string, unknown]> = [];
787
+ const provider = adaptAccountsProvider(
788
+ makeFakeClient({ onCall: (method, args) => calls.push([method, args]) }),
789
+ );
790
+ const keys = await provider.listRingVrfKeys("people.dot").match(
791
+ (value) => value,
792
+ () => [],
793
+ );
794
+ calls.length = 0;
795
+ const keyHandle = keys[1].handle;
796
+ const signature = await provider.ringVrfSign(keyHandle, new Uint8Array([1, 2, 3])).match(
797
+ (value) => value,
798
+ () => null,
799
+ );
800
+ expect(calls[0]).toEqual([
801
+ "ringVrfSign",
802
+ { keyHandle, message: toHex(new Uint8Array([1, 2, 3])) },
803
+ ]);
804
+ expect(signature).toEqual(fromHex("0xba5eba11"));
805
+ });
806
+
466
807
  test("createRingVRFProof hex-encodes the message and decodes the proof response", async () => {
467
808
  const calls: Array<[string, unknown]> = [];
468
809
  const client = makeFakeClient({ onCall: (m, a) => calls.push([m, a]) });
469
810
  const provider = adaptAccountsProvider(client);
811
+ const keyHandle = (
812
+ await provider.listRingVrfKeys("people.dot").match(
813
+ (value) => value,
814
+ () => [],
815
+ )
816
+ )[0].handle;
817
+ calls.length = 0;
470
818
  const proof = await provider
471
819
  .createRingVRFProof(
472
- { productId: "app.dot", suffix: { tag: "Left", value: 0 } },
820
+ keyHandle,
821
+ { productId: "app.dot", suffix: { tag: "Index", value: 0 } },
473
822
  { chainId: "0x01", junctions: [{ tag: "PalletInstance", value: 1 }] },
474
823
  new Uint8Array([1, 2, 3]),
475
824
  )
@@ -479,7 +828,11 @@ if (import.meta.vitest) {
479
828
  );
480
829
  expect(calls[0][0]).toBe("createAccountProof");
481
830
  expect(calls[0][1]).toEqual({
482
- context: { productId: "app.dot", suffix: { tag: "Left", value: 0 } },
831
+ keyHandle: {
832
+ dotNsIdentifier: "people.dot",
833
+ derivationIndex: { tag: "Index", value: 0 },
834
+ },
835
+ context: { productId: "app.dot", suffix: { tag: "Index", value: 0 } },
483
836
  ringLocation: { chainId: "0x01", junctions: [{ tag: "PalletInstance", value: 1 }] },
484
837
  message: toHex(new Uint8Array([1, 2, 3])),
485
838
  });
@@ -491,6 +844,68 @@ if (import.meta.vitest) {
491
844
  });
492
845
  });
493
846
 
847
+ test("signVrf hex-encodes the transcript and decodes the signature", async () => {
848
+ const calls: Array<[string, unknown]> = [];
849
+ const client = makeFakeClient({ onCall: (m, a) => calls.push([m, a]) });
850
+ const provider = adaptAccountsProvider(client);
851
+ const transcriptLabel = new Uint8Array([1, 2, 3]);
852
+ const itemLabel = new Uint8Array([4]);
853
+ const itemValue = new Uint8Array([5, 6]);
854
+ const signature = await provider
855
+ .signVrf({ dotNsIdentifier: "app.dot", derivationIndex: 3 }, transcriptLabel, [
856
+ { label: itemLabel, value: itemValue },
857
+ ])
858
+ .match(
859
+ (s) => s,
860
+ () => null,
861
+ );
862
+ expect(calls[0]).toEqual([
863
+ "signVrf",
864
+ {
865
+ account: {
866
+ dotNsIdentifier: "app.dot",
867
+ derivationIndex: { tag: "Index", value: 3 },
868
+ },
869
+ transcriptLabel: toHex(transcriptLabel),
870
+ items: [{ label: toHex(itemLabel), value: toHex(itemValue) }],
871
+ },
872
+ ]);
873
+ expect(signature).toEqual({ preOutput: fromHex("0xaa11"), proof: fromHex("0xbb22") });
874
+ });
875
+
876
+ test("signVrf defaults the derivation index", async () => {
877
+ const calls: Array<[string, unknown]> = [];
878
+ const client = makeFakeClient({ onCall: (m, a) => calls.push([m, a]) });
879
+ const provider = adaptAccountsProvider(client);
880
+ await provider.signVrf({ dotNsIdentifier: "app.dot" }, new Uint8Array([1]), []).match(
881
+ (s) => s,
882
+ () => null,
883
+ );
884
+ expect((calls[0][1] as { account: unknown }).account).toEqual({
885
+ dotNsIdentifier: "app.dot",
886
+ derivationIndex: { tag: "Index", value: 0 },
887
+ });
888
+ });
889
+
890
+ test("signVrf sends only the id fields when given a full product account", async () => {
891
+ const calls: Array<[string, unknown]> = [];
892
+ const client = makeFakeClient({ onCall: (m, a) => calls.push([m, a]) });
893
+ const provider = adaptAccountsProvider(client);
894
+ const account: ProductAccount = {
895
+ dotNsIdentifier: "app.dot",
896
+ derivationIndex: 1,
897
+ publicKey: new Uint8Array(32).fill(0xaa),
898
+ };
899
+ await provider.signVrf(account, new Uint8Array([1]), []).match(
900
+ (s) => s,
901
+ () => null,
902
+ );
903
+ expect(Object.keys((calls[0][1] as { account: object }).account)).toEqual([
904
+ "dotNsIdentifier",
905
+ "derivationIndex",
906
+ ]);
907
+ });
908
+
494
909
  test("the product signer signs bytes via signing.signRaw", async () => {
495
910
  const calls: Array<[string, unknown]> = [];
496
911
  const client = makeFakeClient({ onCall: (m, a) => calls.push([m, a]) });
@@ -504,7 +919,10 @@ if (import.meta.vitest) {
504
919
  expect(calls.at(-1)).toEqual([
505
920
  "signRaw",
506
921
  {
507
- account: { dotNsIdentifier: "app.dot", derivationIndex: { tag: "Left", value: 0 } },
922
+ account: {
923
+ dotNsIdentifier: "app.dot",
924
+ derivationIndex: { tag: "Index", value: 0 },
925
+ },
508
926
  payload: { tag: "Bytes", value: { bytes: toHex(new Uint8Array([9, 9])) } },
509
927
  },
510
928
  ]);
@@ -591,7 +1009,7 @@ if (import.meta.vitest) {
591
1009
  expect(calls.at(-1)).toEqual([
592
1010
  "createTransaction",
593
1011
  {
594
- signer: { dotNsIdentifier: "app.dot", derivationIndex: { tag: "Left", value: 0 } },
1012
+ signer: { dotNsIdentifier: "app.dot", derivationIndex: { tag: "Index", value: 0 } },
595
1013
  genesisHash: toHex(new Uint8Array([0x01, 0x02])),
596
1014
  callData: toHex(new Uint8Array([0xca, 0x11])),
597
1015
  extensions: expectedHostExtensions,