From 9a5f7bc858059dde3fdc4b20b3a2d8cb3b5f17b5 Mon Sep 17 00:00:00 2001 From: w Date: Sat, 15 Aug 2026 22:14:47 -0400 Subject: [PATCH 1/4] Add registered ring VRF key selection --- product-sdk/packages/host/src/accounts.ts | 234 ++++++++++++++++-- product-sdk/packages/host/src/index.ts | 6 +- product-sdk/packages/host/src/payments.ts | 2 +- product-sdk/packages/host/src/testing.ts | 3 + product-sdk/packages/host/src/truapi.ts | 4 +- .../sdk/src/identity/product-account.ts | 6 +- product-sdk/packages/signer/src/index.ts | 3 + .../packages/signer/src/providers/host.ts | 115 ++++++++- .../packages/signer/src/providers/index.ts | 3 + .../packages/signer/src/signer-manager.ts | 49 +++- .../deprecate-context-alias.md | 9 +- .../truapi-09-ring-vrf-keys.md | 20 ++ product-sdk/pnpm-lock.yaml | 12 +- product-sdk/pnpm-workspace.yaml | 2 +- 14 files changed, 406 insertions(+), 62 deletions(-) create mode 100644 product-sdk/pending-changesets/truapi-09-ring-vrf-keys.md diff --git a/product-sdk/packages/host/src/accounts.ts b/product-sdk/packages/host/src/accounts.ts index cbbcf9e8..d536e9c7 100644 --- a/product-sdk/packages/host/src/accounts.ts +++ b/product-sdk/packages/host/src/accounts.ts @@ -36,11 +36,14 @@ import type { ProductAccount as WireProductAccount, ProductAccountId, ProductProofContext, + RegisteredRingVrfKey as WireRegisteredRingVrfKey, RingLocation, + RingVrfKeyDisclosure, TrUApiClient, VersionedHostAccountCreateProofError, VersionedHostAccountGetAliasError, VersionedHostAccountGetError, + VersionedHostAccountListRingVrfKeysError, VersionedHostAccountSignVrfError, VersionedHostGetLegacyAccountsError, VersionedHostGetUserIdError, @@ -62,17 +65,22 @@ import type { HostSubscription } from "./types.js"; * (`{ productId, suffix }`), expanded by the host into the 32-byte context a * proof or alias is bound to. * - `DerivationIndex` — the tagged selector `ProductProofContext.suffix` - * carries: `{ tag: "Left", value: number }` for a plain index, or - * `{ tag: "Right", value: HexString }` for a raw 32-byte index. + * carries: `{ tag: "Index", value: number }` for a plain index, or + * `{ tag: "Raw", value: HexString }` for a raw 32-byte index. */ -export type { DerivationIndex, ProductProofContext, RingLocation } from "@parity/truapi"; +export type { + DerivationIndex, + ProductProofContext, + RingLocation, + RingVrfKeyDisclosure, +} from "@parity/truapi"; // The account/alias shapes come from `@parity/truapi`'s generated specs; we // derive the SDK-facing views from them so the field inventory tracks the // protocol automatically, and override only the fields the adapter re-encodes: // byte fields decoded from `0x`-prefixed `HexString`s to `Uint8Array`s, and // the tagged derivation-index selector kept as a plain `number` (wrapped back -// into `Left` at the wire boundary). Shapes re-exported verbatim (e.g. +// into `Index` at the wire boundary). Shapes re-exported verbatim (e.g. // `ProductProofContext`) track the wire as-is. Same pattern as // `@parity/product-sdk-statement-store`. @@ -119,6 +127,62 @@ export type ProductAccountLookup = Omit & { derivationIndex?: number; }; +declare const ringVrfKeyHandleBrand: unique symbol; + +/** + * Opaque public name of a registered ring-VRF key. + * + * Handles come from {@link AccountsProvider.listRingVrfKeys}; product code + * cannot construct one from a derivation index. + */ +export type RingVrfKeyHandle = { + readonly [ringVrfKeyHandleBrand]: "RingVrfKeyHandle"; +}; + +/** Ring-VRF member public key, decoded from the wire's hex string. */ +export type RingVrfPublicKey = Uint8Array; + +/** Registered key metadata returned by the host. */ +export type RegisteredRingVrfKey = Omit & { + /** Opaque handle to pass back for alias and proof requests. */ + handle: RingVrfKeyHandle; + /** Present when public-key disclosure was granted. */ + publicKey?: RingVrfPublicKey; +}; + +function sameRingLocation(a: RingLocation, b: RingLocation): boolean { + if ( + a.chainId.toLowerCase() !== b.chainId.toLowerCase() || + a.junctions.length !== b.junctions.length + ) { + return false; + } + return a.junctions.every((junction, index) => { + const candidate = b.junctions[index]; + if (junction.tag === "PalletInstance") { + return candidate.tag === "PalletInstance" && junction.value === candidate.value; + } + return ( + candidate.tag === "CollectionId" && + junction.value.toLowerCase() === candidate.value.toLowerCase() + ); + }); +} + +/** + * Select a registered key by its declared ring and return its opaque handle. + * + * Consumers must not hard-code another product's derivation index. Registry + * order breaks ties when an owner declares multiple keys for the same ring. + */ +export function findRingVrfKeyHandle( + keys: RegisteredRingVrfKey[], + ring: RingLocation, +): RingVrfKeyHandle | undefined { + return keys.find((key) => key.rings.some((candidate) => sameRingLocation(candidate, ring))) + ?.handle; +} + /** * A contextual alias obtained from Ring VRF. * @@ -180,11 +244,17 @@ export interface AccountsProvider { dotNsIdentifier: string, derivationIndex?: number, ): ResultAsync>; - /** - * Derive the contextual alias for a proof context and ring. The host - * selects the member key within the ring — no per-account addressing. - */ + /** List an owner's registered ring-VRF keys. */ + listRingVrfKeys( + owner: string, + disclosure?: RingVrfKeyDisclosure, + ): ResultAsync< + RegisteredRingVrfKey[], + scale.CallErrorValue + >; + /** Derive a contextual alias with an explicitly registered ring-VRF key. */ getProductAccountAlias( + keyHandle: RingVrfKeyHandle, context: ProductProofContext, location: RingLocation, ): ResultAsync>; @@ -193,11 +263,11 @@ export interface AccountsProvider { scale.CallErrorValue >; /** - * Generate a Ring VRF proof binding `message` to the product-scoped - * `context`. The host selects the member key within the ring; the result - * carries the proof plus its verification values ({@link RingVRFProof}). + * Generate a Ring VRF proof with an explicitly registered key, binding + * `message` to the product-scoped `context`. */ createRingVRFProof( + keyHandle: RingVrfKeyHandle, context: ProductProofContext, location: RingLocation, message: Uint8Array, @@ -282,7 +352,7 @@ function toHostExtensions( } /** - * Build the wire `ProductAccountId`: default the index to 0, wrap it as `Left`. + * Build the wire `ProductAccountId`: default the index to 0, wrap it as `Index`. * * Destructured rather than spread, so passing a full {@link ProductAccount} * cannot leak its `publicKey` onto the wire. @@ -291,7 +361,7 @@ function toWireProductAccountId({ dotNsIdentifier, derivationIndex = 0, }: ProductAccountLookup): ProductAccountId { - return { dotNsIdentifier, derivationIndex: { tag: "Left", value: derivationIndex } }; + return { dotNsIdentifier, derivationIndex: { tag: "Index", value: derivationIndex } }; } /** Build an {@link AccountsProvider} over a TruAPI client's `account` / `signing` domains. */ @@ -319,11 +389,26 @@ function adaptAccountsProvider(client: TrUApiClient): AccountsProvider { derivationIndex, })); }, - getProductAccountAlias(context, location) { - return account.getAccountAlias({ context, ringLocation: location }).map((response) => ({ - context: fromHex(response.context), - alias: fromHex(response.alias), - })); + listRingVrfKeys(owner, disclosure = "Anonymized") { + return account.listRingVrfKeys({ owner, disclosure }).map((keys) => + keys.map((key) => ({ + ...key, + handle: key.handle as unknown as RingVrfKeyHandle, + publicKey: key.publicKey === undefined ? undefined : fromHex(key.publicKey), + })), + ); + }, + getProductAccountAlias(keyHandle, context, location) { + return account + .getAccountAlias({ + keyHandle: keyHandle as unknown as ProductAccountId, + context, + ringLocation: location, + }) + .map((response) => ({ + context: fromHex(response.context), + alias: fromHex(response.alias), + })); }, getLegacyAccounts() { return account.getLegacyAccounts().map((response) => @@ -333,9 +418,10 @@ function adaptAccountsProvider(client: TrUApiClient): AccountsProvider { })), ); }, - createRingVRFProof(context, location, message) { + createRingVRFProof(keyHandle, context, location, message) { return account .createAccountProof({ + keyHandle: keyHandle as unknown as ProductAccountId, context, ringLocation: location, message: toHex(message), @@ -475,6 +561,33 @@ if (import.meta.vitest) { account: { getUserId: method("getUserId", { primaryUsername: "alice.dot" }), getAccount: method("getAccount", { account: { publicKey: "0xaa" } }), + listRingVrfKeys: method("listRingVrfKeys", [ + { + handle: { + dotNsIdentifier: "people.dot", + derivationIndex: { tag: "Index", value: 0 }, + }, + rings: [ + { + chainId: "0x01", + junctions: [{ tag: "PalletInstance", value: 1 }], + }, + ], + }, + { + handle: { + dotNsIdentifier: "people.dot", + derivationIndex: { tag: "Index", value: 1 }, + }, + rings: [ + { + chainId: "0x02", + junctions: [{ tag: "CollectionId", value: "0xaabb" }], + }, + ], + publicKey: "0x0102", + }, + ]), getAccountAlias: method("getAccountAlias", { context: "0x01", alias: "0x02" }), getLegacyAccounts: method("getLegacyAccounts", { accounts: [{ publicKey: "0xbb", name: "Bob" }], @@ -523,7 +636,7 @@ if (import.meta.vitest) { { productAccountId: { dotNsIdentifier: "app.dot", - derivationIndex: { tag: "Left", value: 2 }, + derivationIndex: { tag: "Index", value: 2 }, }, }, ]); @@ -547,7 +660,7 @@ if (import.meta.vitest) { { productAccountId: { dotNsIdentifier: "app.dot", - derivationIndex: { tag: "Left", value: 0 }, + derivationIndex: { tag: "Index", value: 0 }, }, }, ]); @@ -555,13 +668,70 @@ if (import.meta.vitest) { expect(account?.derivationIndex).toBe(0); }); + test("listRingVrfKeys selects by ring without exposing a raw index", async () => { + const calls: Array<[string, unknown]> = []; + const provider = adaptAccountsProvider( + makeFakeClient({ onCall: (method, args) => calls.push([method, args]) }), + ); + const keys = await provider.listRingVrfKeys("people.dot", "PublicKey").match( + (value) => value, + () => [], + ); + expect(calls[0]).toEqual([ + "listRingVrfKeys", + { owner: "people.dot", disclosure: "PublicKey" }, + ]); + expect(keys[1].publicKey).toEqual(fromHex("0x0102")); + expect( + findRingVrfKeyHandle(keys, { + chainId: "0x02", + junctions: [{ tag: "CollectionId", value: "0xAABB" }], + }), + ).toEqual(keys[1].handle); + }); + + test("getProductAccountAlias passes the selected key handle", async () => { + const calls: Array<[string, unknown]> = []; + const provider = adaptAccountsProvider( + makeFakeClient({ onCall: (method, args) => calls.push([method, args]) }), + ); + const keys = await provider.listRingVrfKeys("people.dot").match( + (value) => value, + () => [], + ); + calls.length = 0; + const keyHandle = keys[1].handle; + const context: ProductProofContext = { + productId: "app.dot", + suffix: { tag: "Index", value: 0 }, + }; + const ring: RingLocation = { + chainId: "0x01", + junctions: [{ tag: "PalletInstance", value: 1 }], + }; + const alias = await provider.getProductAccountAlias(keyHandle, context, ring).match( + (value) => value, + () => null, + ); + expect(calls[0]).toEqual(["getAccountAlias", { keyHandle, context, ringLocation: ring }]); + expect(alias).toEqual({ context: fromHex("0x01"), alias: fromHex("0x02") }); + }); + test("createRingVRFProof hex-encodes the message and decodes the proof response", async () => { const calls: Array<[string, unknown]> = []; const client = makeFakeClient({ onCall: (m, a) => calls.push([m, a]) }); const provider = adaptAccountsProvider(client); + const keyHandle = ( + await provider.listRingVrfKeys("people.dot").match( + (value) => value, + () => [], + ) + )[0].handle; + calls.length = 0; const proof = await provider .createRingVRFProof( - { productId: "app.dot", suffix: { tag: "Left", value: 0 } }, + keyHandle, + { productId: "app.dot", suffix: { tag: "Index", value: 0 } }, { chainId: "0x01", junctions: [{ tag: "PalletInstance", value: 1 }] }, new Uint8Array([1, 2, 3]), ) @@ -571,7 +741,11 @@ if (import.meta.vitest) { ); expect(calls[0][0]).toBe("createAccountProof"); expect(calls[0][1]).toEqual({ - context: { productId: "app.dot", suffix: { tag: "Left", value: 0 } }, + keyHandle: { + dotNsIdentifier: "people.dot", + derivationIndex: { tag: "Index", value: 0 }, + }, + context: { productId: "app.dot", suffix: { tag: "Index", value: 0 } }, ringLocation: { chainId: "0x01", junctions: [{ tag: "PalletInstance", value: 1 }] }, message: toHex(new Uint8Array([1, 2, 3])), }); @@ -601,7 +775,10 @@ if (import.meta.vitest) { expect(calls[0]).toEqual([ "signVrf", { - account: { dotNsIdentifier: "app.dot", derivationIndex: { tag: "Left", value: 3 } }, + account: { + dotNsIdentifier: "app.dot", + derivationIndex: { tag: "Index", value: 3 }, + }, transcriptLabel: toHex(transcriptLabel), items: [{ label: toHex(itemLabel), value: toHex(itemValue) }], }, @@ -619,7 +796,7 @@ if (import.meta.vitest) { ); expect((calls[0][1] as { account: unknown }).account).toEqual({ dotNsIdentifier: "app.dot", - derivationIndex: { tag: "Left", value: 0 }, + derivationIndex: { tag: "Index", value: 0 }, }); }); @@ -655,7 +832,10 @@ if (import.meta.vitest) { expect(calls.at(-1)).toEqual([ "signRaw", { - account: { dotNsIdentifier: "app.dot", derivationIndex: { tag: "Left", value: 0 } }, + account: { + dotNsIdentifier: "app.dot", + derivationIndex: { tag: "Index", value: 0 }, + }, payload: { tag: "Bytes", value: { bytes: toHex(new Uint8Array([9, 9])) } }, }, ]); @@ -742,7 +922,7 @@ if (import.meta.vitest) { expect(calls.at(-1)).toEqual([ "createTransaction", { - signer: { dotNsIdentifier: "app.dot", derivationIndex: { tag: "Left", value: 0 } }, + signer: { dotNsIdentifier: "app.dot", derivationIndex: { tag: "Index", value: 0 } }, genesisHash: toHex(new Uint8Array([0x01, 0x02])), callData: toHex(new Uint8Array([0xca, 0x11])), extensions: expectedHostExtensions, diff --git a/product-sdk/packages/host/src/index.ts b/product-sdk/packages/host/src/index.ts index 6ab69e11..ea99bb29 100644 --- a/product-sdk/packages/host/src/index.ts +++ b/product-sdk/packages/host/src/index.ts @@ -67,7 +67,7 @@ export { export type { HostErrorPayload } from "./errors.js"; // Accounts — host wallet accounts, product accounts, Ring VRF, and signers. -export { getAccountsProvider } from "./accounts.js"; +export { getAccountsProvider, findRingVrfKeyHandle } from "./accounts.js"; export type { AccountsProvider, DerivationIndex, @@ -78,6 +78,10 @@ export type { ProductProofContext, RingLocation, RingVRFProof, + RegisteredRingVrfKey, + RingVrfKeyDisclosure, + RingVrfKeyHandle, + RingVrfPublicKey, VrfSignature, VrfTranscriptItem, } from "./accounts.js"; diff --git a/product-sdk/packages/host/src/payments.ts b/product-sdk/packages/host/src/payments.ts index ae425b15..2c131c07 100644 --- a/product-sdk/packages/host/src/payments.ts +++ b/product-sdk/packages/host/src/payments.ts @@ -97,7 +97,7 @@ function adaptPaymentManager(client: TrUApiClient): PaymentManager { * const payments = await getPaymentManager(); * if (payments) { * const sub = payments.subscribeBalance((b) => { ... }); - * await payments.topUp(1_000_000n, { tag: "ProductAccount", value: { derivationIndex: { tag: "Left", value: 0 } } }); + * await payments.topUp(1_000_000n, { tag: "ProductAccount", value: { derivationIndex: { tag: "Index", value: 0 } } }); * const { id } = await payments.requestPayment(500n, "0x…"); * sub.unsubscribe(); * } diff --git a/product-sdk/packages/host/src/testing.ts b/product-sdk/packages/host/src/testing.ts index ea307ccc..5405dcd5 100644 --- a/product-sdk/packages/host/src/testing.ts +++ b/product-sdk/packages/host/src/testing.ts @@ -180,6 +180,9 @@ export function createFakeTruApiClient(options?: CreateFakeTruApiClientOptions): getUserId: () => okAsync({ primaryUsername }), requestLogin: () => okAsync("Success"), getAccount: () => okAsync({ account: { publicKey } }), + registerRingVrfKey: () => okAsync(publicKey), + listRingVrfKeys: () => okAsync([]), + ringVrfSign: () => okAsync(signature), getAccountAlias: () => okAsync({ context: toHex(new Uint8Array([1])), alias: toHex(new Uint8Array([2])) }), getLegacyAccounts: () => okAsync({ accounts: legacyAccounts }), diff --git a/product-sdk/packages/host/src/truapi.ts b/product-sdk/packages/host/src/truapi.ts index b7c68c7d..7695e735 100644 --- a/product-sdk/packages/host/src/truapi.ts +++ b/product-sdk/packages/host/src/truapi.ts @@ -183,8 +183,8 @@ export async function createHostPreimageManager(): Promise NeverthrowResultAsync; getProductAccountSigner: (account: ProductAccount) => import("polkadot-api").PolkadotSigner; + listRingVrfKeys: ( + owner: string, + disclosure?: RingVrfKeyDisclosure, + ) => NeverthrowResultAsync; getProductAccountAlias: ( + keyHandle: RingVrfKeyHandle, context: ProductProofContext, location: RingLocation, ) => NeverthrowResultAsync; getUserId: () => NeverthrowResultAsync<{ primaryUsername: string }, unknown>; createRingVRFProof: ( + keyHandle: RingVrfKeyHandle, context: ProductProofContext, location: RingLocation, message: Uint8Array, @@ -421,6 +433,33 @@ export class HostProvider implements SignerProvider { return this.accountsProvider.getProductAccountSigner(account); } + /** List an owner's registered ring-VRF keys. */ + async listRingVrfKeys( + owner: string, + disclosure: RingVrfKeyDisclosure = "Anonymized", + ): Promise> { + if (!this.accountsProvider) { + return err(new HostUnavailableError("Host provider is not connected")); + } + + try { + const keys = (await this.accountsProvider.listRingVrfKeys(owner, disclosure).match( + (result) => result, + (error) => { + throw new Error( + `Host rejected ring VRF key list request: ${formatError(error)}`, + ); + }, + )) as RegisteredRingVrfKey[]; + return ok(keys); + } catch (cause) { + const message = + cause instanceof Error ? cause.message : "Failed to list registered ring VRF keys"; + log.error("failed to list registered ring VRF keys", { error: message }); + return err(new HostRejectedError(message)); + } + } + /** * Get a contextual alias for a product account via Ring VRF. * @@ -430,6 +469,7 @@ export class HostProvider implements SignerProvider { * Requires a prior successful `connect()` call. */ async getProductAccountAlias( + keyHandle: RingVrfKeyHandle, context: ProductProofContext, location: RingLocation, ): Promise> { @@ -439,7 +479,7 @@ export class HostProvider implements SignerProvider { try { const alias = (await this.accountsProvider - .getProductAccountAlias(context, location) + .getProductAccountAlias(keyHandle, context, location) .match( (result) => result, (error) => { @@ -493,13 +533,14 @@ export class HostProvider implements SignerProvider { /** * Create a Ring VRF proof for anonymous operations. * - * Proves that a member of the ring at the given location produced the - * proof without revealing which member — the host selects the member key. + * Proves that the explicitly selected registered key belongs to the ring + * at the given location without revealing which member produced the proof. * Returns the proof plus its verification values ({@link RingVRFProof}). * * Requires a prior successful `connect()` call. */ async createRingVRFProof( + keyHandle: RingVrfKeyHandle, context: ProductProofContext, location: RingLocation, message: Uint8Array, @@ -510,7 +551,7 @@ export class HostProvider implements SignerProvider { try { const proof = (await this.accountsProvider - .createRingVRFProof(context, location, message) + .createRingVRFProof(keyHandle, context, location, message) .match( (result) => result, (error) => { @@ -908,6 +949,14 @@ if (import.meta.vitest) { }, }), getProductAccountSigner: vi.fn().mockReturnValue(mockSigner), + listRingVrfKeys: vi.fn().mockReturnValue({ + match: async (onOk: (v: unknown) => unknown, onErr: (e: unknown) => unknown) => { + if (shouldReject) { + return onErr(options.error ?? "Unknown"); + } + return onOk([]); + }, + }), getProductAccountAlias: vi.fn().mockReturnValue({ match: async (onOk: (v: unknown) => unknown, onErr: (e: unknown) => unknown) => { if (shouldReject) { @@ -1452,6 +1501,60 @@ if (import.meta.vitest) { }); }); + describe("HostProvider ring VRF keys", () => { + test("lists a key and forwards its opaque handle to alias and proof calls", async () => { + const mockProvider = createMockProvider({ + accounts: [{ publicKey: new Uint8Array(32).fill(0xa2), name: undefined }], + }); + const keyHandle = { + dotNsIdentifier: "people.dot", + derivationIndex: { tag: "Index", value: 0 }, + } as unknown as RingVrfKeyHandle; + const ring: RingLocation = { + chainId: "0x01", + junctions: [{ tag: "PalletInstance", value: 67 }], + }; + mockProvider.listRingVrfKeys.mockReturnValue({ + match: async ( + onOk: (value: RegisteredRingVrfKey[]) => unknown, + _onErr: (error: unknown) => unknown, + ) => onOk([{ handle: keyHandle, rings: [ring] }]), + }); + const provider = new HostProvider({ + maxRetries: 1, + loadAccountsProvider: loadProvider(mockProvider), + requestChainSubmitPermissionFn: grantPermission(), + productAccount: { dotNsIdentifier: "myapp.dot", requestName: false }, + }); + await provider.connect(); + + const listed = await provider.listRingVrfKeys("people.dot", "Anonymized"); + expect(listed.ok).toBe(true); + expect(mockProvider.listRingVrfKeys).toHaveBeenCalledWith("people.dot", "Anonymized"); + if (!listed.ok) return; + + const context: ProductProofContext = { + productId: "myapp.dot", + suffix: { tag: "Index", value: 0 }, + }; + await provider.getProductAccountAlias(listed.value[0].handle, context, ring); + expect(mockProvider.getProductAccountAlias).toHaveBeenCalledWith( + keyHandle, + context, + ring, + ); + + const message = new Uint8Array([1, 2, 3]); + await provider.createRingVRFProof(listed.value[0].handle, context, ring, message); + expect(mockProvider.createRingVRFProof).toHaveBeenCalledWith( + keyHandle, + context, + ring, + message, + ); + }); + }); + describe("HostProvider.signVrf", () => { const account = { dotNsIdentifier: "myapp.dot", derivationIndex: 0 }; const label = new Uint8Array([1, 2, 3]); diff --git a/product-sdk/packages/signer/src/providers/index.ts b/product-sdk/packages/signer/src/providers/index.ts index 67594d10..a20881a1 100644 --- a/product-sdk/packages/signer/src/providers/index.ts +++ b/product-sdk/packages/signer/src/providers/index.ts @@ -15,6 +15,9 @@ export type { ContextualAlias, DerivationIndex, ProductProofContext, + RegisteredRingVrfKey, + RingVrfKeyDisclosure, + RingVrfKeyHandle, RingLocation, RingVRFProof, } from "./host.js"; diff --git a/product-sdk/packages/signer/src/signer-manager.ts b/product-sdk/packages/signer/src/signer-manager.ts index e8ba998b..66aa1d6c 100644 --- a/product-sdk/packages/signer/src/signer-manager.ts +++ b/product-sdk/packages/signer/src/signer-manager.ts @@ -20,6 +20,9 @@ import type { ProductAccount, ProductAccountLookup, ProductProofContext, + RegisteredRingVrfKey, + RingVrfKeyDisclosure, + RingVrfKeyHandle, RingLocation, RingVRFProof, VrfSignature, @@ -397,14 +400,37 @@ export class SignerManager { } /** - * Get a contextual alias for a proof context and ring via Ring VRF. + * List ring-VRF keys registered by a product. + * + * Use the returned key handles, rather than hard-coding a derivation index, + * when requesting aliases or proofs. + */ + async listRingVrfKeys( + owner: string, + disclosure: RingVrfKeyDisclosure = "Anonymized", + ): Promise> { + if (this.isDestroyed) return err(new DestroyedError()); + + const host = this.getHostProvider(); + if (!host) { + return err( + new HostUnavailableError( + "Ring VRF key listing requires a host provider connection", + ), + ); + } + return host.listRingVrfKeys(owner, disclosure); + } + + /** + * Get a contextual alias for an explicitly selected ring-VRF key. * * Aliases prove account membership in a ring without revealing which - * account produced the alias; the host selects the member key. Only - * available when connected via the host provider — returns - * HOST_UNAVAILABLE otherwise. + * account produced the alias. Only available when connected via the host + * provider — returns HOST_UNAVAILABLE otherwise. */ async getProductAccountAlias( + keyHandle: RingVrfKeyHandle, context: ProductProofContext, location: RingLocation, ): Promise> { @@ -418,19 +444,20 @@ export class SignerManager { ), ); } - return host.getProductAccountAlias(context, location); + return host.getProductAccountAlias(keyHandle, context, location); } /** * Create a Ring VRF proof for anonymous operations. * - * Proves that a ring member at the given location produced the proof - * without revealing which member — the host selects the member key. The - * result carries the proof plus its verification values. Only available - * when connected via the host provider — returns HOST_UNAVAILABLE - * otherwise. + * Proves that the explicitly selected registered key belongs to the ring + * at the given location without revealing which member produced the proof. + * The result carries the proof plus its verification values. Only + * available when connected via the host provider — returns + * HOST_UNAVAILABLE otherwise. */ async createRingVRFProof( + keyHandle: RingVrfKeyHandle, context: ProductProofContext, location: RingLocation, message: Uint8Array, @@ -443,7 +470,7 @@ export class SignerManager { new HostUnavailableError("Ring VRF proofs require a host provider connection"), ); } - return host.createRingVRFProof(context, location, message); + return host.createRingVRFProof(keyHandle, context, location, message); } /** diff --git a/product-sdk/pending-changesets/deprecate-context-alias.md b/product-sdk/pending-changesets/deprecate-context-alias.md index 69949624..0f322450 100644 --- a/product-sdk/pending-changesets/deprecate-context-alias.md +++ b/product-sdk/pending-changesets/deprecate-context-alias.md @@ -14,9 +14,10 @@ nothing surfaces until value arrives at it. and identity's `RingLocation`. Each function was a debug log followed by an unconditional `throw`, with no branch or early return, so no working consumer could exist and this break is compile-time only. The real ring VRF operations already live on `SignerManager` in -`@parity/product-sdk-signer` as `getProductAccountAlias` and `createRingVRFProof`, host-backed so -the host selects the member key within the ring. Identity's `RingLocation` was also the wrong -shape, `{ringIndex, memberIndex}` against the protocol type `{chainId, junctions}`. +`@parity/product-sdk-signer` as `getProductAccountAlias(keyHandle, context, location)` and +`createRingVRFProof(keyHandle, context, location, message)`, host-backed and using an opaque +registered key handle selected by ring. Identity's `RingLocation` was also the wrong shape, +`{ringIndex, memberIndex}` against the protocol type `{chainId, junctions}`. **Deprecated, removal in `@parity/product-sdk` 0.23.0:** `deriveContextAlias`, `verifyContextAlias`, `ContextAliasInfo`. Their output is unchanged, so a caller using an alias as @@ -32,7 +33,7 @@ bytes would break identifier consumers silently, with no compile error. |---|---| | An account that holds or spends value | `SignerManager.getProductAccount(dotNsIdentifier, index)` from `@parity/product-sdk-signer` | | The address offline, with no host | `deriveProductAccountPublicKey` from `@parity/product-sdk-keys`, the canonical sr25519 soft derivation | -| An unlinkable per-context alias | `SignerManager.getProductAccountAlias(context, location)`, plus `createRingVRFProof` for proofs | +| An unlinkable per-context alias | Select a registered key by ring, then call `SignerManager.getProductAccountAlias(keyHandle, context, location)` or `createRingVRFProof(keyHandle, context, location, message)` from `@parity/product-sdk-signer` | | A context-scoped identifier, never used as an account | `blake2b256` from `@parity/product-sdk/crypto`: the same bytes, without address packaging | The DotNS half of `./identity` is unaffected (`resolveDotNs`, `reverseDotNs`, `isDotNsAvailable`, diff --git a/product-sdk/pending-changesets/truapi-09-ring-vrf-keys.md b/product-sdk/pending-changesets/truapi-09-ring-vrf-keys.md new file mode 100644 index 00000000..63587a64 --- /dev/null +++ b/product-sdk/pending-changesets/truapi-09-ring-vrf-keys.md @@ -0,0 +1,20 @@ +--- +"@parity/product-sdk-host": minor +"@parity/product-sdk-signer": minor +"@parity/product-sdk": minor +--- + +**Update TrUAPI to 0.9 and require registered ring-VRF key handles.** + +`AccountsProvider`, `HostProvider`, and `SignerManager` now expose +`listRingVrfKeys(owner, disclosure?)`. The returned `RegisteredRingVrfKey` entries carry opaque +`RingVrfKeyHandle` values. `findRingVrfKeyHandle(keys, ring)` selects a handle by declared +`RingLocation`, so products do not hard-code another product's derivation index. + +`getProductAccountAlias` and `createRingVRFProof` now require that handle as their first argument. +This is a compile-time breaking change. It matches TrUAPI 0.9, where the host no longer chooses a +ring member key implicitly and rejects malformed legacy requests before application dispatch. + +The dependency update also adopts TrUAPI's renamed derivation-index variants: `Index` replaces +`Left` and `Raw` replaces `Right`. The SDK's ergonomic numeric product-account APIs are unchanged; +the host adapter performs the `Index` conversion at the wire boundary. diff --git a/product-sdk/pnpm-lock.yaml b/product-sdk/pnpm-lock.yaml index 6e8c8154..e0ffc3dd 100644 --- a/product-sdk/pnpm-lock.yaml +++ b/product-sdk/pnpm-lock.yaml @@ -10,8 +10,8 @@ catalogs: specifier: ^0.12.1 version: 0.12.1 '@parity/truapi': - specifier: ^0.7.0 - version: 0.7.0 + specifier: ^0.9.0 + version: 0.9.0 '@playwright/test': specifier: ^1.60.0 version: 1.60.0 @@ -535,7 +535,7 @@ importers: version: link:../result '@parity/truapi': specifier: 'catalog:' - version: 0.7.0 + version: 0.9.0 '@polkadot-api/json-rpc-provider': specifier: ^0.2.0 version: 0.2.0 @@ -1654,8 +1654,8 @@ packages: '@playwright/test': optional: true - '@parity/truapi@0.7.0': - resolution: {integrity: sha512-xBGZP7/l74I0WGUnHgoFNYah+20I3JgykJ5pNUVsw3mQzNoCHuZSVJW6sUq7fzLvRF4wM2glPTPxUOiFKgfE9A==} + '@parity/truapi@0.9.0': + resolution: {integrity: sha512-mndLyirfLFGYhlMKj10kMvovUk8v5h58z3f4u47yDb6bn5cmYlkf37fIsXC3rXEBGRL78yMv83O0xWrS3K2RVA==} '@playwright/test@1.60.0': resolution: {integrity: sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==} @@ -3905,7 +3905,7 @@ snapshots: optionalDependencies: '@playwright/test': 1.60.0 - '@parity/truapi@0.7.0': + '@parity/truapi@0.9.0': dependencies: '@noble/hashes': 2.2.0 neverthrow: 8.2.0 diff --git a/product-sdk/pnpm-workspace.yaml b/product-sdk/pnpm-workspace.yaml index 15e8f07c..b164654b 100644 --- a/product-sdk/pnpm-workspace.yaml +++ b/product-sdk/pnpm-workspace.yaml @@ -5,7 +5,7 @@ packages: catalog: "@parity/host-api-test-sdk": ^0.12.1 - "@parity/truapi": ^0.7.0 + "@parity/truapi": ^0.9.0 "@playwright/test": ^1.60.0 "@polkadot-api/json-rpc-provider": ^0.2.0 "@polkadot-api/substrate-bindings": ^0.20.3 From 4d616b0bea7d48e78ef29b6acf2d22dbf2535b3e Mon Sep 17 00:00:00 2001 From: w Date: Sun, 16 Aug 2026 04:51:42 -0400 Subject: [PATCH 2/4] Re-export the host ring location type --- .../packages/signer/src/providers/host.ts | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/product-sdk/packages/signer/src/providers/host.ts b/product-sdk/packages/signer/src/providers/host.ts index 90f5a7fa..78aaa97d 100644 --- a/product-sdk/packages/signer/src/providers/host.ts +++ b/product-sdk/packages/signer/src/providers/host.ts @@ -5,6 +5,7 @@ import { getAccountsProvider, type ProductAccountLookup, type RegisteredRingVrfKey, + type RingLocation, type RingVrfKeyDisclosure, type RingVrfKeyHandle, type ProductProofContext, @@ -136,23 +137,6 @@ export interface ContextualAlias { alias: Uint8Array; } -/** - * Location of a Ring VRF ring on-chain: the hosting chain's genesis hash plus - * the junction path addressing the ring within it. - * - * Structurally matches `RingLocation` from `@parity/truapi`, re-exported by - * `@parity/product-sdk-host`. Declared locally with `string` in place of the - * branded hex types so callers can pass plain strings. - */ -export interface RingLocation { - /** Genesis hash of the chain hosting the ring. */ - chainId: string; - /** Path addressing the ring within the chain. */ - junctions: Array< - { tag: "PalletInstance"; value: number } | { tag: "CollectionId"; value: string } - >; -} - /** * Ring VRF key-management and request shapes re-exported from * `@parity/product-sdk-host`. Host is a hard dependency, so these come from one @@ -164,6 +148,7 @@ export type { ProductProofContext, RegisteredRingVrfKey, RingVrfKeyDisclosure, + RingLocation, RingVrfKeyHandle, VrfSignature, VrfTranscriptItem, From 79171173a4621143c36256aa46c0de3b1c8bcdd6 Mon Sep 17 00:00:00 2001 From: w Date: Tue, 18 Aug 2026 11:28:31 -0400 Subject: [PATCH 3/4] Add ring VRF key registration --- product-sdk/packages/host/src/accounts.ts | 37 +++++++++++++ product-sdk/packages/signer/src/index.ts | 1 + .../packages/signer/src/providers/host.ts | 55 +++++++++++++++++++ .../packages/signer/src/providers/index.ts | 1 + .../packages/signer/src/signer-manager.ts | 25 +++++++++ .../deprecate-context-alias.md | 6 +- .../truapi-09-ring-vrf-keys.md | 8 ++- 7 files changed, 129 insertions(+), 4 deletions(-) diff --git a/product-sdk/packages/host/src/accounts.ts b/product-sdk/packages/host/src/accounts.ts index d536e9c7..29d45a0f 100644 --- a/product-sdk/packages/host/src/accounts.ts +++ b/product-sdk/packages/host/src/accounts.ts @@ -44,6 +44,7 @@ import type { VersionedHostAccountGetAliasError, VersionedHostAccountGetError, VersionedHostAccountListRingVrfKeysError, + VersionedHostAccountRegisterRingVrfKeyError, VersionedHostAccountSignVrfError, VersionedHostGetLegacyAccountsError, VersionedHostGetUserIdError, @@ -244,6 +245,19 @@ export interface AccountsProvider { dotNsIdentifier: string, derivationIndex?: number, ): ResultAsync>; + /** + * Register a ring-VRF key owned by the calling product. + * + * Registration returns the key's public key. Call {@link listRingVrfKeys} + * afterward to obtain the opaque handle required by alias and proof calls. + */ + registerRingVrfKey( + index: DerivationIndex, + ring: RingLocation, + ): ResultAsync< + RingVrfPublicKey, + scale.CallErrorValue + >; /** List an owner's registered ring-VRF keys. */ listRingVrfKeys( owner: string, @@ -389,6 +403,9 @@ function adaptAccountsProvider(client: TrUApiClient): AccountsProvider { derivationIndex, })); }, + registerRingVrfKey(index, ring) { + return account.registerRingVrfKey({ index, ring }).map(fromHex); + }, listRingVrfKeys(owner, disclosure = "Anonymized") { return account.listRingVrfKeys({ owner, disclosure }).map((keys) => keys.map((key) => ({ @@ -561,6 +578,7 @@ if (import.meta.vitest) { account: { getUserId: method("getUserId", { primaryUsername: "alice.dot" }), getAccount: method("getAccount", { account: { publicKey: "0xaa" } }), + registerRingVrfKey: method("registerRingVrfKey", "0x0304"), listRingVrfKeys: method("listRingVrfKeys", [ { handle: { @@ -668,6 +686,25 @@ if (import.meta.vitest) { expect(account?.derivationIndex).toBe(0); }); + test("registerRingVrfKey forwards the derivation selector and decodes the public key", async () => { + const calls: Array<[string, unknown]> = []; + const provider = adaptAccountsProvider( + makeFakeClient({ onCall: (method, args) => calls.push([method, args]) }), + ); + const ring: RingLocation = { + chainId: "0x01", + junctions: [{ tag: "PalletInstance", value: 67 }], + }; + const index: DerivationIndex = { tag: "Index", value: 2 }; + const publicKey = await provider.registerRingVrfKey(index, ring).match( + (value) => value, + () => null, + ); + + expect(calls[0]).toEqual(["registerRingVrfKey", { index, ring }]); + expect(publicKey).toEqual(fromHex("0x0304")); + }); + test("listRingVrfKeys selects by ring without exposing a raw index", async () => { const calls: Array<[string, unknown]> = []; const provider = adaptAccountsProvider( diff --git a/product-sdk/packages/signer/src/index.ts b/product-sdk/packages/signer/src/index.ts index f2dd9a08..0c006be1 100644 --- a/product-sdk/packages/signer/src/index.ts +++ b/product-sdk/packages/signer/src/index.ts @@ -66,6 +66,7 @@ export type { RegisteredRingVrfKey, RingVrfKeyDisclosure, RingVrfKeyHandle, + RingVrfPublicKey, RingLocation, RingVRFProof, VrfSignature, diff --git a/product-sdk/packages/signer/src/providers/host.ts b/product-sdk/packages/signer/src/providers/host.ts index 78aaa97d..4b04df96 100644 --- a/product-sdk/packages/signer/src/providers/host.ts +++ b/product-sdk/packages/signer/src/providers/host.ts @@ -3,11 +3,13 @@ import { deriveH160, ss58Encode } from "@parity/product-sdk-address"; import { getAccountsProvider, + type DerivationIndex, type ProductAccountLookup, type RegisteredRingVrfKey, type RingLocation, type RingVrfKeyDisclosure, type RingVrfKeyHandle, + type RingVrfPublicKey, type ProductProofContext, type RemotePermission, requestPermission, @@ -150,6 +152,7 @@ export type { RingVrfKeyDisclosure, RingLocation, RingVrfKeyHandle, + RingVrfPublicKey, VrfSignature, VrfTranscriptItem, } from "@parity/product-sdk-host"; @@ -193,6 +196,10 @@ export interface AccountsProvider { derivationIndex?: number, ) => NeverthrowResultAsync; getProductAccountSigner: (account: ProductAccount) => import("polkadot-api").PolkadotSigner; + registerRingVrfKey: ( + index: DerivationIndex, + ring: RingLocation, + ) => NeverthrowResultAsync; listRingVrfKeys: ( owner: string, disclosure?: RingVrfKeyDisclosure, @@ -418,6 +425,38 @@ export class HostProvider implements SignerProvider { return this.accountsProvider.getProductAccountSigner(account); } + /** + * Register a ring-VRF key owned by the calling product. + * + * Call {@link listRingVrfKeys} afterward to obtain the opaque handle used + * by alias and proof requests. + */ + async registerRingVrfKey( + index: DerivationIndex, + ring: RingLocation, + ): Promise> { + if (!this.accountsProvider) { + return err(new HostUnavailableError("Host provider is not connected")); + } + + try { + const publicKey = (await this.accountsProvider.registerRingVrfKey(index, ring).match( + (result) => result, + (error) => { + throw new Error( + `Host rejected ring VRF key registration: ${formatError(error)}`, + ); + }, + )) as RingVrfPublicKey; + return ok(publicKey); + } catch (cause) { + const message = + cause instanceof Error ? cause.message : "Failed to register ring VRF key"; + log.error("failed to register ring VRF key", { error: message }); + return err(new HostRejectedError(message)); + } + } + /** List an owner's registered ring-VRF keys. */ async listRingVrfKeys( owner: string, @@ -934,6 +973,14 @@ if (import.meta.vitest) { }, }), getProductAccountSigner: vi.fn().mockReturnValue(mockSigner), + registerRingVrfKey: vi.fn().mockReturnValue({ + match: async (onOk: (v: unknown) => unknown, onErr: (e: unknown) => unknown) => { + if (shouldReject) { + return onErr(options.error ?? "Unknown"); + } + return onOk(new Uint8Array([3, 4])); + }, + }), listRingVrfKeys: vi.fn().mockReturnValue({ match: async (onOk: (v: unknown) => unknown, onErr: (e: unknown) => unknown) => { if (shouldReject) { @@ -1513,6 +1560,14 @@ if (import.meta.vitest) { }); await provider.connect(); + const index: DerivationIndex = { tag: "Index", value: 0 }; + const registered = await provider.registerRingVrfKey(index, ring); + expect(registered.ok).toBe(true); + expect(mockProvider.registerRingVrfKey).toHaveBeenCalledWith(index, ring); + if (registered.ok) { + expect(registered.value).toEqual(new Uint8Array([3, 4])); + } + const listed = await provider.listRingVrfKeys("people.dot", "Anonymized"); expect(listed.ok).toBe(true); expect(mockProvider.listRingVrfKeys).toHaveBeenCalledWith("people.dot", "Anonymized"); diff --git a/product-sdk/packages/signer/src/providers/index.ts b/product-sdk/packages/signer/src/providers/index.ts index a20881a1..f4785690 100644 --- a/product-sdk/packages/signer/src/providers/index.ts +++ b/product-sdk/packages/signer/src/providers/index.ts @@ -18,6 +18,7 @@ export type { RegisteredRingVrfKey, RingVrfKeyDisclosure, RingVrfKeyHandle, + RingVrfPublicKey, RingLocation, RingVRFProof, } from "./host.js"; diff --git a/product-sdk/packages/signer/src/signer-manager.ts b/product-sdk/packages/signer/src/signer-manager.ts index 66aa1d6c..6005d695 100644 --- a/product-sdk/packages/signer/src/signer-manager.ts +++ b/product-sdk/packages/signer/src/signer-manager.ts @@ -18,11 +18,13 @@ import { HostProvider } from "./providers/host.js"; import type { ContextualAlias, ProductAccount, + DerivationIndex, ProductAccountLookup, ProductProofContext, RegisteredRingVrfKey, RingVrfKeyDisclosure, RingVrfKeyHandle, + RingVrfPublicKey, RingLocation, RingVRFProof, VrfSignature, @@ -399,6 +401,29 @@ export class SignerManager { return host.getProductAccount(dotNsIdentifier, derivationIndex); } + /** + * Register a ring-VRF key owned by this product. + * + * Use {@link listRingVrfKeys} afterward to obtain the opaque handle needed + * by alias and proof requests. + */ + async registerRingVrfKey( + index: DerivationIndex, + ring: RingLocation, + ): Promise> { + if (this.isDestroyed) return err(new DestroyedError()); + + const host = this.getHostProvider(); + if (!host) { + return err( + new HostUnavailableError( + "Ring VRF key registration requires a host provider connection", + ), + ); + } + return host.registerRingVrfKey(index, ring); + } + /** * List ring-VRF keys registered by a product. * diff --git a/product-sdk/pending-changesets/deprecate-context-alias.md b/product-sdk/pending-changesets/deprecate-context-alias.md index 0f322450..9a2cfbde 100644 --- a/product-sdk/pending-changesets/deprecate-context-alias.md +++ b/product-sdk/pending-changesets/deprecate-context-alias.md @@ -39,6 +39,6 @@ bytes would break identifier consumers silently, with no compile error. The DotNS half of `./identity` is unaffected (`resolveDotNs`, `reverseDotNs`, `isDotNsAvailable`, `resolvePeopleUsernameOwner` and the name helpers), and the subpath itself is not deprecated. -`@parity/product-sdk-signer` takes a patch for one doc comment: its local `RingLocation` claimed -to match the product-sdk shape, which was the opposite shape and is now deleted. No type or -behaviour change. +`@parity/product-sdk-signer` takes a patch here for the context-alias migration wording. The +separate TrUAPI 0.9 changeset documents the `RingLocation` type break and supplies the release's +minor bump. diff --git a/product-sdk/pending-changesets/truapi-09-ring-vrf-keys.md b/product-sdk/pending-changesets/truapi-09-ring-vrf-keys.md index 63587a64..5dd99108 100644 --- a/product-sdk/pending-changesets/truapi-09-ring-vrf-keys.md +++ b/product-sdk/pending-changesets/truapi-09-ring-vrf-keys.md @@ -7,7 +7,8 @@ **Update TrUAPI to 0.9 and require registered ring-VRF key handles.** `AccountsProvider`, `HostProvider`, and `SignerManager` now expose -`listRingVrfKeys(owner, disclosure?)`. The returned `RegisteredRingVrfKey` entries carry opaque +`registerRingVrfKey(index, ring)` and `listRingVrfKeys(owner, disclosure?)`. Registration returns +the decoded ring-VRF public key; listing returns `RegisteredRingVrfKey` entries with opaque `RingVrfKeyHandle` values. `findRingVrfKeyHandle(keys, ring)` selects a handle by declared `RingLocation`, so products do not hard-code another product's derivation index. @@ -18,3 +19,8 @@ ring member key implicitly and rejects malformed legacy requests before applicat The dependency update also adopts TrUAPI's renamed derivation-index variants: `Index` replaces `Left` and `Raw` replaces `Right`. The SDK's ergonomic numeric product-account APIs are unchanged; the host adapter performs the `Index` conversion at the wire boundary. + +The signer package's re-exported `RingLocation` now uses TrUAPI's `` chainId: `0x${string}` `` +instead of a plain `string`; callers loading chain IDs from configuration must narrow or validate +them before assignment. Custom `HostProviderOptions.loadAccountsProvider` implementations must +also provide the newly required `registerRingVrfKey` and `listRingVrfKeys` methods. From 44ca7b2ef2176d2ba76fbad6415bed9848007e16 Mon Sep 17 00:00:00 2001 From: Imod7 Date: Tue, 18 Aug 2026 18:05:20 +0200 Subject: [PATCH 4/4] refactor(host,signer): take a plain derivation index for key registration --- product-sdk/packages/host/src/accounts.ts | 19 +++++++++++++------ .../packages/signer/src/providers/host.ts | 7 +++---- .../packages/signer/src/signer-manager.ts | 3 +-- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/product-sdk/packages/host/src/accounts.ts b/product-sdk/packages/host/src/accounts.ts index 29d45a0f..08ddc072 100644 --- a/product-sdk/packages/host/src/accounts.ts +++ b/product-sdk/packages/host/src/accounts.ts @@ -28,7 +28,6 @@ import { AccountId, type PolkadotSigner } from "polkadot-api"; import type { ContextualAlias as WireAlias, - DerivationIndex, HostAccountConnectionStatusSubscribeItem, HostAccountCreateProofResponse as WireRingVRFProof, HostRequestLoginResponse, @@ -248,11 +247,14 @@ export interface AccountsProvider { /** * Register a ring-VRF key owned by the calling product. * + * `index` is the plain derivation index within the product's ring-VRF + * domain; the adapter wraps it into the wire's tagged selector. + * * Registration returns the key's public key. Call {@link listRingVrfKeys} * afterward to obtain the opaque handle required by alias and proof calls. */ registerRingVrfKey( - index: DerivationIndex, + index: number, ring: RingLocation, ): ResultAsync< RingVrfPublicKey, @@ -404,7 +406,9 @@ function adaptAccountsProvider(client: TrUApiClient): AccountsProvider { })); }, registerRingVrfKey(index, ring) { - return account.registerRingVrfKey({ index, ring }).map(fromHex); + return account + .registerRingVrfKey({ index: { tag: "Index", value: index }, ring }) + .map(fromHex); }, listRingVrfKeys(owner, disclosure = "Anonymized") { return account.listRingVrfKeys({ owner, disclosure }).map((keys) => @@ -686,7 +690,7 @@ if (import.meta.vitest) { expect(account?.derivationIndex).toBe(0); }); - test("registerRingVrfKey forwards the derivation selector and decodes the public key", async () => { + test("registerRingVrfKey wraps the numeric index and decodes the public key", async () => { const calls: Array<[string, unknown]> = []; const provider = adaptAccountsProvider( makeFakeClient({ onCall: (method, args) => calls.push([method, args]) }), @@ -695,13 +699,16 @@ if (import.meta.vitest) { chainId: "0x01", junctions: [{ tag: "PalletInstance", value: 67 }], }; - const index: DerivationIndex = { tag: "Index", value: 2 }; + const index = 2; const publicKey = await provider.registerRingVrfKey(index, ring).match( (value) => value, () => null, ); - expect(calls[0]).toEqual(["registerRingVrfKey", { index, ring }]); + expect(calls[0]).toEqual([ + "registerRingVrfKey", + { index: { tag: "Index", value: 2 }, ring }, + ]); expect(publicKey).toEqual(fromHex("0x0304")); }); diff --git a/product-sdk/packages/signer/src/providers/host.ts b/product-sdk/packages/signer/src/providers/host.ts index 4b04df96..6edb481e 100644 --- a/product-sdk/packages/signer/src/providers/host.ts +++ b/product-sdk/packages/signer/src/providers/host.ts @@ -3,7 +3,6 @@ import { deriveH160, ss58Encode } from "@parity/product-sdk-address"; import { getAccountsProvider, - type DerivationIndex, type ProductAccountLookup, type RegisteredRingVrfKey, type RingLocation, @@ -197,7 +196,7 @@ export interface AccountsProvider { ) => NeverthrowResultAsync; getProductAccountSigner: (account: ProductAccount) => import("polkadot-api").PolkadotSigner; registerRingVrfKey: ( - index: DerivationIndex, + index: number, ring: RingLocation, ) => NeverthrowResultAsync; listRingVrfKeys: ( @@ -432,7 +431,7 @@ export class HostProvider implements SignerProvider { * by alias and proof requests. */ async registerRingVrfKey( - index: DerivationIndex, + index: number, ring: RingLocation, ): Promise> { if (!this.accountsProvider) { @@ -1560,7 +1559,7 @@ if (import.meta.vitest) { }); await provider.connect(); - const index: DerivationIndex = { tag: "Index", value: 0 }; + const index = 0; const registered = await provider.registerRingVrfKey(index, ring); expect(registered.ok).toBe(true); expect(mockProvider.registerRingVrfKey).toHaveBeenCalledWith(index, ring); diff --git a/product-sdk/packages/signer/src/signer-manager.ts b/product-sdk/packages/signer/src/signer-manager.ts index 6005d695..deb5834c 100644 --- a/product-sdk/packages/signer/src/signer-manager.ts +++ b/product-sdk/packages/signer/src/signer-manager.ts @@ -18,7 +18,6 @@ import { HostProvider } from "./providers/host.js"; import type { ContextualAlias, ProductAccount, - DerivationIndex, ProductAccountLookup, ProductProofContext, RegisteredRingVrfKey, @@ -408,7 +407,7 @@ export class SignerManager { * by alias and proof requests. */ async registerRingVrfKey( - index: DerivationIndex, + index: number, ring: RingLocation, ): Promise> { if (this.isDestroyed) return err(new DestroyedError());