@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/dist/chain-discovery-BEvu7HaV.d.ts +107 -0
- package/dist/chunk-PAKEUP2Q.js +89 -0
- package/dist/chunk-PAKEUP2Q.js.map +1 -0
- package/dist/index.d.ts +108 -16
- package/dist/index.js +266 -56
- package/dist/index.js.map +1 -1
- package/dist/testing.d.ts +39 -8
- package/dist/testing.js +26 -7
- package/dist/testing.js.map +1 -1
- package/package.json +4 -4
- package/src/accounts.ts +485 -43
- package/src/chain-discovery.ts +287 -0
- package/src/chains.ts +10 -3
- package/src/index.ts +17 -1
- package/src/papi-provider.ts +370 -37
- package/src/payments.ts +1 -1
- package/src/testing.ts +115 -13
- package/src/transport.ts +228 -4
- package/src/truapi.ts +3 -2
- package/dist/chunk-GDXSV7JV.js +0 -50
- package/dist/chunk-GDXSV7JV.js.map +0 -1
- package/dist/transport-B0cdhwrp.d.ts +0 -31
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`),
|
|
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: "
|
|
62
|
-
* `{ tag: "
|
|
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 {
|
|
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 `
|
|
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
|
-
*
|
|
156
|
-
*
|
|
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
|
|
168
|
-
* `
|
|
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
|
-
*
|
|
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
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
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
|
|
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
|
-
|
|
208
|
-
|
|
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
|
-
/**
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
265
|
-
return account
|
|
266
|
-
|
|
267
|
-
|
|
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: "
|
|
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
|
-
|
|
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
|
-
|
|
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: {
|
|
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: "
|
|
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,
|