@parity/product-sdk-host 0.17.0 → 0.19.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/index.d.ts +94 -13
- package/dist/index.js +177 -69
- package/dist/index.js.map +1 -1
- package/dist/testing.d.ts +3 -2
- package/dist/testing.js +4 -2
- package/dist/testing.js.map +1 -1
- package/package.json +4 -4
- package/src/accounts.ts +295 -107
- package/src/errors.ts +46 -0
- package/src/index.ts +6 -0
- package/src/locale.ts +76 -0
- package/src/testing.ts +7 -4
- package/src/truapi.ts +139 -3
package/src/accounts.ts
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
import { decAnyMetadata, unifyMetadata } from "@polkadot-api/substrate-bindings";
|
|
26
|
-
import
|
|
26
|
+
import { errAsync, okAsync, ResultAsync } from "neverthrow";
|
|
27
27
|
import { AccountId, type PolkadotSigner } from "polkadot-api";
|
|
28
28
|
|
|
29
29
|
import type {
|
|
@@ -55,6 +55,7 @@ import type {
|
|
|
55
55
|
} from "@parity/truapi";
|
|
56
56
|
|
|
57
57
|
import { getClient, subscribeWithInterrupt } from "./transport.js";
|
|
58
|
+
import { HostResponseDecodeError } from "./errors.js";
|
|
58
59
|
import { fromHex, toHex, unwrapHostResult } from "./truapi.js";
|
|
59
60
|
import type { HostSubscription } from "./types.js";
|
|
60
61
|
|
|
@@ -223,6 +224,14 @@ export type VrfTranscriptItem = { [K in keyof WireVrfTranscriptItem]: Uint8Array
|
|
|
223
224
|
*/
|
|
224
225
|
export type VrfSignature = { [K in keyof WireVrfSignature]: Uint8Array };
|
|
225
226
|
|
|
227
|
+
/**
|
|
228
|
+
* A call's declared `Err` channel, plus {@link HostResponseDecodeError}: any
|
|
229
|
+
* host reply can fail to decode if the host and the product's `@parity/truapi`
|
|
230
|
+
* client are on different protocol versions, so every decoded call can surface
|
|
231
|
+
* it in addition to its own typed errors.
|
|
232
|
+
*/
|
|
233
|
+
export type WithDecodeError<E> = E | HostResponseDecodeError;
|
|
234
|
+
|
|
226
235
|
/**
|
|
227
236
|
* Accounts provider handle, backed by `truApi.account.*` / `truApi.signing.*`.
|
|
228
237
|
* Surfaces the user's wallet accounts, app-scoped product accounts, Ring VRF,
|
|
@@ -231,20 +240,29 @@ export type VrfSignature = { [K in keyof WireVrfSignature]: Uint8Array };
|
|
|
231
240
|
* Lookup methods return a neverthrow `ResultAsync` (use `.match(ok, err)`);
|
|
232
241
|
* the signer factories return a synchronous PAPI `PolkadotSigner`. The `err`
|
|
233
242
|
* channel carries truapi's canonical `CallErrorValue` envelope around the
|
|
234
|
-
* per-call versioned domain error, exactly as the generated client returns it
|
|
243
|
+
* per-call versioned domain error, exactly as the generated client returns it,
|
|
244
|
+
* plus a {@link HostResponseDecodeError} for the case where the host's reply
|
|
245
|
+
* cannot be decoded at all (a host/client protocol-version skew) — see
|
|
246
|
+
* {@link WithDecodeError}.
|
|
235
247
|
*/
|
|
236
248
|
export interface AccountsProvider {
|
|
237
249
|
getUserId(): ResultAsync<
|
|
238
250
|
{ primaryUsername: string },
|
|
239
|
-
scale.CallErrorValue<VersionedHostGetUserIdError
|
|
251
|
+
WithDecodeError<scale.CallErrorValue<VersionedHostGetUserIdError>>
|
|
240
252
|
>;
|
|
241
253
|
requestLogin(
|
|
242
254
|
reason?: string,
|
|
243
|
-
): ResultAsync<
|
|
255
|
+
): ResultAsync<
|
|
256
|
+
HostRequestLoginResponse,
|
|
257
|
+
WithDecodeError<scale.CallErrorValue<VersionedHostRequestLoginError>>
|
|
258
|
+
>;
|
|
244
259
|
getProductAccount(
|
|
245
260
|
dotNsIdentifier: string,
|
|
246
261
|
derivationIndex?: number,
|
|
247
|
-
): ResultAsync<
|
|
262
|
+
): ResultAsync<
|
|
263
|
+
ProductAccount,
|
|
264
|
+
WithDecodeError<scale.CallErrorValue<VersionedHostAccountGetError>>
|
|
265
|
+
>;
|
|
248
266
|
/**
|
|
249
267
|
* Register a ring-VRF key owned by the calling product.
|
|
250
268
|
*
|
|
@@ -259,7 +277,7 @@ export interface AccountsProvider {
|
|
|
259
277
|
ring: RingLocation,
|
|
260
278
|
): ResultAsync<
|
|
261
279
|
RingVrfPublicKey,
|
|
262
|
-
scale.CallErrorValue<VersionedHostAccountRegisterRingVrfKeyError
|
|
280
|
+
WithDecodeError<scale.CallErrorValue<VersionedHostAccountRegisterRingVrfKeyError>>
|
|
263
281
|
>;
|
|
264
282
|
/** List an owner's registered ring-VRF keys. */
|
|
265
283
|
listRingVrfKeys(
|
|
@@ -267,17 +285,20 @@ export interface AccountsProvider {
|
|
|
267
285
|
disclosure?: RingVrfKeyDisclosure,
|
|
268
286
|
): ResultAsync<
|
|
269
287
|
RegisteredRingVrfKey[],
|
|
270
|
-
scale.CallErrorValue<VersionedHostAccountListRingVrfKeysError
|
|
288
|
+
WithDecodeError<scale.CallErrorValue<VersionedHostAccountListRingVrfKeysError>>
|
|
271
289
|
>;
|
|
272
290
|
/** Derive a contextual alias with an explicitly registered ring-VRF key. */
|
|
273
291
|
getProductAccountAlias(
|
|
274
292
|
keyHandle: RingVrfKeyHandle,
|
|
275
293
|
context: ProductProofContext,
|
|
276
294
|
location: RingLocation,
|
|
277
|
-
): ResultAsync<
|
|
295
|
+
): ResultAsync<
|
|
296
|
+
ContextualAlias,
|
|
297
|
+
WithDecodeError<scale.CallErrorValue<VersionedHostAccountGetAliasError>>
|
|
298
|
+
>;
|
|
278
299
|
getLegacyAccounts(): ResultAsync<
|
|
279
300
|
HostAccount[],
|
|
280
|
-
scale.CallErrorValue<VersionedHostGetLegacyAccountsError
|
|
301
|
+
WithDecodeError<scale.CallErrorValue<VersionedHostGetLegacyAccountsError>>
|
|
281
302
|
>;
|
|
282
303
|
/**
|
|
283
304
|
* Generate a Ring VRF proof with an explicitly registered key, binding
|
|
@@ -288,7 +309,10 @@ export interface AccountsProvider {
|
|
|
288
309
|
context: ProductProofContext,
|
|
289
310
|
location: RingLocation,
|
|
290
311
|
message: Uint8Array,
|
|
291
|
-
): ResultAsync<
|
|
312
|
+
): ResultAsync<
|
|
313
|
+
RingVRFProof,
|
|
314
|
+
WithDecodeError<scale.CallErrorValue<VersionedHostAccountCreateProofError>>
|
|
315
|
+
>;
|
|
292
316
|
/**
|
|
293
317
|
* Sign `message` directly with an explicitly registered ring-VRF key.
|
|
294
318
|
*
|
|
@@ -299,7 +323,10 @@ export interface AccountsProvider {
|
|
|
299
323
|
ringVrfSign(
|
|
300
324
|
keyHandle: RingVrfKeyHandle,
|
|
301
325
|
message: Uint8Array,
|
|
302
|
-
): ResultAsync<
|
|
326
|
+
): ResultAsync<
|
|
327
|
+
Uint8Array,
|
|
328
|
+
WithDecodeError<scale.CallErrorValue<VersionedHostAccountRingVrfSignError>>
|
|
329
|
+
>;
|
|
303
330
|
/**
|
|
304
331
|
* Produce an sr25519 VRF signature from a product account (RFC-0023).
|
|
305
332
|
*
|
|
@@ -323,7 +350,10 @@ export interface AccountsProvider {
|
|
|
323
350
|
account: ProductAccountLookup,
|
|
324
351
|
transcriptLabel: Uint8Array,
|
|
325
352
|
items: VrfTranscriptItem[],
|
|
326
|
-
): ResultAsync<
|
|
353
|
+
): ResultAsync<
|
|
354
|
+
VrfSignature,
|
|
355
|
+
WithDecodeError<scale.CallErrorValue<VersionedHostAccountSignVrfError>>
|
|
356
|
+
>;
|
|
327
357
|
/**
|
|
328
358
|
* Build a `PolkadotSigner` for a product account. Signing routes through the
|
|
329
359
|
* host's `createTransaction` path: the host decodes the metadata and forwards
|
|
@@ -341,30 +371,32 @@ export interface AccountsProvider {
|
|
|
341
371
|
): HostSubscription;
|
|
342
372
|
}
|
|
343
373
|
|
|
374
|
+
const V5_FORMAT_SELECTOR = 5;
|
|
375
|
+
|
|
344
376
|
/**
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
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.
|
|
377
|
+
* Pick the `txExtVersion` for the host's `create_transaction` from the extrinsic formats
|
|
378
|
+
* the runtime offers. The host treats the field as a format switch: `0` builds V4, `5`
|
|
379
|
+
* builds a V5 general transaction, anything else is `NotSupported`. Prefer V4 while
|
|
380
|
+
* offered, since it carries the account signature in its envelope. The host derives the
|
|
381
|
+
* transaction-extension version from the metadata itself.
|
|
353
382
|
*/
|
|
354
|
-
function selectHostTxExtVersion(
|
|
355
|
-
if (
|
|
383
|
+
function selectHostTxExtVersion(formatVersions: readonly number[]): number {
|
|
384
|
+
if (formatVersions.length === 0) {
|
|
356
385
|
throw new Error("No extrinsic version found in metadata");
|
|
357
386
|
}
|
|
358
|
-
if (
|
|
387
|
+
if (formatVersions.includes(4)) {
|
|
359
388
|
return 0;
|
|
360
389
|
}
|
|
361
|
-
|
|
390
|
+
if (formatVersions.includes(5)) {
|
|
391
|
+
return V5_FORMAT_SELECTOR;
|
|
392
|
+
}
|
|
393
|
+
throw new Error(
|
|
394
|
+
`Runtime offers no extrinsic format 4 or 5 (offers: ${formatVersions.join(", ")}); the host protocol has no txExtVersion for it.`,
|
|
395
|
+
);
|
|
362
396
|
}
|
|
363
397
|
|
|
364
|
-
/** Derive the host's transaction-extension version from SCALE metadata. */
|
|
365
398
|
function deriveTxExtVersion(metadata: Uint8Array): number {
|
|
366
|
-
|
|
367
|
-
return selectHostTxExtVersion(versions);
|
|
399
|
+
return selectHostTxExtVersion(unifyMetadata(decAnyMetadata(metadata)).extrinsic.version);
|
|
368
400
|
}
|
|
369
401
|
|
|
370
402
|
/** Internal seam so `import.meta.vitest` can stub the metadata decode. @internal */
|
|
@@ -400,6 +432,28 @@ function toWireProductAccountId({
|
|
|
400
432
|
return { dotNsIdentifier, derivationIndex: { tag: "Index", value: derivationIndex } };
|
|
401
433
|
}
|
|
402
434
|
|
|
435
|
+
/**
|
|
436
|
+
* Route a thrown/rejected response-decode error onto the `Result` err channel.
|
|
437
|
+
*
|
|
438
|
+
* The truapi client catches a decode throw in its message handler and turns it
|
|
439
|
+
* into a promise rejection, then wraps each call with
|
|
440
|
+
* `ResultAsync.fromSafePromise`, which installs no rejection handler — so when
|
|
441
|
+
* the host's reply doesn't match the client's codec (a version skew), that
|
|
442
|
+
* rejection escapes the `Result` channel rather than landing on its err side,
|
|
443
|
+
* surfacing as a raw `RangeError`. Wrapping the call re-homes that rejection as
|
|
444
|
+
* a typed {@link HostResponseDecodeError} that names the call, while ok values
|
|
445
|
+
* and the call's own typed `Err` values pass through untouched.
|
|
446
|
+
*/
|
|
447
|
+
function guardDecode<T, E>(
|
|
448
|
+
call: string,
|
|
449
|
+
result: ResultAsync<T, E>,
|
|
450
|
+
): ResultAsync<T, E | HostResponseDecodeError> {
|
|
451
|
+
return ResultAsync.fromPromise(
|
|
452
|
+
Promise.resolve(result),
|
|
453
|
+
(cause) => new HostResponseDecodeError(call, cause),
|
|
454
|
+
).andThen((inner) => inner);
|
|
455
|
+
}
|
|
456
|
+
|
|
403
457
|
/** Build an {@link AccountsProvider} over a TruAPI client's `account` / `signing` domains. */
|
|
404
458
|
function adaptAccountsProvider(client: TrUApiClient): AccountsProvider {
|
|
405
459
|
const account = client.account;
|
|
@@ -407,98 +461,128 @@ function adaptAccountsProvider(client: TrUApiClient): AccountsProvider {
|
|
|
407
461
|
|
|
408
462
|
return {
|
|
409
463
|
getUserId() {
|
|
410
|
-
return
|
|
411
|
-
|
|
412
|
-
|
|
464
|
+
return guardDecode(
|
|
465
|
+
"getUserId",
|
|
466
|
+
account.getUserId().map((response) => ({
|
|
467
|
+
primaryUsername: response.primaryUsername,
|
|
468
|
+
})),
|
|
469
|
+
);
|
|
413
470
|
},
|
|
414
471
|
requestLogin(reason) {
|
|
415
|
-
return account.requestLogin({ reason });
|
|
472
|
+
return guardDecode("requestLogin", account.requestLogin({ reason }));
|
|
416
473
|
},
|
|
417
474
|
getProductAccount(dotNsIdentifier, derivationIndex = 0) {
|
|
418
|
-
return
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
475
|
+
return guardDecode(
|
|
476
|
+
"getProductAccount",
|
|
477
|
+
account
|
|
478
|
+
.getAccount({
|
|
479
|
+
productAccountId: toWireProductAccountId({
|
|
480
|
+
dotNsIdentifier,
|
|
481
|
+
derivationIndex,
|
|
482
|
+
}),
|
|
483
|
+
})
|
|
484
|
+
.map((response) => ({
|
|
485
|
+
publicKey: fromHex(response.account.publicKey),
|
|
486
|
+
dotNsIdentifier,
|
|
487
|
+
derivationIndex,
|
|
488
|
+
})),
|
|
489
|
+
);
|
|
427
490
|
},
|
|
428
491
|
registerRingVrfKey(index, ring) {
|
|
429
|
-
return
|
|
430
|
-
|
|
431
|
-
|
|
492
|
+
return guardDecode(
|
|
493
|
+
"registerRingVrfKey",
|
|
494
|
+
account
|
|
495
|
+
.registerRingVrfKey({ index: { tag: "Index", value: index }, ring })
|
|
496
|
+
.map(fromHex),
|
|
497
|
+
);
|
|
432
498
|
},
|
|
433
499
|
listRingVrfKeys(owner, disclosure = "Anonymized") {
|
|
434
|
-
return
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
500
|
+
return guardDecode(
|
|
501
|
+
"listRingVrfKeys",
|
|
502
|
+
account.listRingVrfKeys({ owner, disclosure }).map((keys) =>
|
|
503
|
+
keys.map((key) => ({
|
|
504
|
+
...key,
|
|
505
|
+
handle: key.handle as unknown as RingVrfKeyHandle,
|
|
506
|
+
publicKey: key.publicKey === undefined ? undefined : fromHex(key.publicKey),
|
|
507
|
+
})),
|
|
508
|
+
),
|
|
440
509
|
);
|
|
441
510
|
},
|
|
442
511
|
getProductAccountAlias(keyHandle, context, location) {
|
|
443
|
-
return
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
512
|
+
return guardDecode(
|
|
513
|
+
"getProductAccountAlias",
|
|
514
|
+
account
|
|
515
|
+
.getAccountAlias({
|
|
516
|
+
keyHandle: keyHandle as unknown as ProductAccountId,
|
|
517
|
+
context,
|
|
518
|
+
ringLocation: location,
|
|
519
|
+
})
|
|
520
|
+
.map((response) => ({
|
|
521
|
+
context: fromHex(response.context),
|
|
522
|
+
alias: fromHex(response.alias),
|
|
523
|
+
})),
|
|
524
|
+
);
|
|
453
525
|
},
|
|
454
526
|
getLegacyAccounts() {
|
|
455
|
-
return
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
527
|
+
return guardDecode(
|
|
528
|
+
"getLegacyAccounts",
|
|
529
|
+
account.getLegacyAccounts().map((response) =>
|
|
530
|
+
response.accounts.map((a) => ({
|
|
531
|
+
publicKey: fromHex(a.publicKey),
|
|
532
|
+
name: a.name,
|
|
533
|
+
})),
|
|
534
|
+
),
|
|
460
535
|
);
|
|
461
536
|
},
|
|
462
537
|
createRingVRFProof(keyHandle, context, location, message) {
|
|
463
|
-
return
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
538
|
+
return guardDecode(
|
|
539
|
+
"createRingVRFProof",
|
|
540
|
+
account
|
|
541
|
+
.createAccountProof({
|
|
542
|
+
keyHandle: keyHandle as unknown as ProductAccountId,
|
|
543
|
+
context,
|
|
544
|
+
ringLocation: location,
|
|
545
|
+
message: toHex(message),
|
|
546
|
+
})
|
|
547
|
+
.map((response) => ({
|
|
548
|
+
proof: fromHex(response.proof),
|
|
549
|
+
contextualAlias: {
|
|
550
|
+
context: fromHex(response.contextualAlias.context),
|
|
551
|
+
alias: fromHex(response.contextualAlias.alias),
|
|
552
|
+
},
|
|
553
|
+
ringIndex: response.ringIndex,
|
|
554
|
+
ringRevision: response.ringRevision,
|
|
555
|
+
})),
|
|
556
|
+
);
|
|
479
557
|
},
|
|
480
558
|
ringVrfSign(keyHandle, message) {
|
|
481
|
-
return
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
559
|
+
return guardDecode(
|
|
560
|
+
"ringVrfSign",
|
|
561
|
+
account
|
|
562
|
+
.ringVrfSign({
|
|
563
|
+
keyHandle: keyHandle as unknown as ProductAccountId,
|
|
564
|
+
message: toHex(message),
|
|
565
|
+
})
|
|
566
|
+
.map(fromHex),
|
|
567
|
+
);
|
|
487
568
|
},
|
|
488
569
|
signVrf(account_, transcriptLabel, items) {
|
|
489
|
-
return
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
570
|
+
return guardDecode(
|
|
571
|
+
"signVrf",
|
|
572
|
+
account
|
|
573
|
+
.signVrf({
|
|
574
|
+
account: toWireProductAccountId(account_),
|
|
575
|
+
transcriptLabel: toHex(transcriptLabel),
|
|
576
|
+
items: items.map(({ label, value }) => ({
|
|
577
|
+
label: toHex(label),
|
|
578
|
+
value: toHex(value),
|
|
579
|
+
})),
|
|
580
|
+
})
|
|
581
|
+
.map((response) => ({
|
|
582
|
+
preOutput: fromHex(response.preOutput),
|
|
583
|
+
proof: fromHex(response.proof),
|
|
496
584
|
})),
|
|
497
|
-
|
|
498
|
-
.map((response) => ({
|
|
499
|
-
preOutput: fromHex(response.preOutput),
|
|
500
|
-
proof: fromHex(response.proof),
|
|
501
|
-
}));
|
|
585
|
+
);
|
|
502
586
|
},
|
|
503
587
|
getProductAccountSigner(account_) {
|
|
504
588
|
const productAccountId = toWireProductAccountId(account_);
|
|
@@ -593,13 +677,13 @@ export async function getAccountsProvider(): Promise<AccountsProvider | null> {
|
|
|
593
677
|
}
|
|
594
678
|
|
|
595
679
|
if (import.meta.vitest) {
|
|
596
|
-
const { test, expect, vi } = import.meta.vitest;
|
|
680
|
+
const { test, expect, vi, describe } = import.meta.vitest;
|
|
597
681
|
|
|
598
|
-
test("host signing prefers V4 on a dual V4/V5 runtime", () => {
|
|
682
|
+
test("host signing prefers V4 (tx-ext version 0) on a dual V4/V5 runtime", () => {
|
|
599
683
|
expect(selectHostTxExtVersion([4, 5])).toBe(0);
|
|
600
684
|
});
|
|
601
685
|
|
|
602
|
-
test("host signing uses V5 when
|
|
686
|
+
test("host signing uses the V5 selector when the runtime offers no V4", () => {
|
|
603
687
|
expect(selectHostTxExtVersion([5])).toBe(5);
|
|
604
688
|
});
|
|
605
689
|
|
|
@@ -607,20 +691,41 @@ if (import.meta.vitest) {
|
|
|
607
691
|
expect(selectHostTxExtVersion([4])).toBe(0);
|
|
608
692
|
});
|
|
609
693
|
|
|
694
|
+
test("host signing prefers V5 over a format it does not know", () => {
|
|
695
|
+
// max(formats) would send 6, which no host accepts.
|
|
696
|
+
expect(selectHostTxExtVersion([5, 6])).toBe(5);
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
test("host signing rejects a runtime offering neither format 4 nor 5", () => {
|
|
700
|
+
expect(() => selectHostTxExtVersion([6])).toThrow(/no extrinsic format 4 or 5/i);
|
|
701
|
+
});
|
|
702
|
+
|
|
610
703
|
test("host signing rejects metadata with no extrinsic version", () => {
|
|
611
704
|
expect(() => selectHostTxExtVersion([])).toThrow("No extrinsic version found in metadata");
|
|
612
705
|
});
|
|
613
706
|
|
|
707
|
+
test("deriveTxExtVersion reads the format list out of every tracked chain's metadata", async () => {
|
|
708
|
+
const { readFileSync, readdirSync } = await import("node:fs");
|
|
709
|
+
const dir = new URL("../../descriptors/.papi/metadata/", import.meta.url);
|
|
710
|
+
const blobs = readdirSync(dir).filter((name) => name.endsWith(".scale"));
|
|
711
|
+
|
|
712
|
+
expect(blobs.length, "raise when a chain is added").toBeGreaterThanOrEqual(11);
|
|
713
|
+
// Every deployed runtime still offers format 4, so V4 wins. Fails the day one drops it.
|
|
714
|
+
for (const name of blobs) {
|
|
715
|
+
const metadata = new Uint8Array(readFileSync(new URL(name, dir)));
|
|
716
|
+
expect(deriveTxExtVersion(metadata), name).toBe(0);
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
|
|
614
720
|
/** Minimal fake of the truapi account/signing domains used to test the adapter. */
|
|
615
721
|
function makeFakeClient(opts: { onCall?: (method: string, args: unknown) => void } = {}) {
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
});
|
|
722
|
+
// A real neverthrow `okAsync`, not a hand-rolled `{ map, match }` stub:
|
|
723
|
+
// a stub with no `.then` would be passed through un-awaited by
|
|
724
|
+
// `guardDecode`'s `Promise.resolve(result)`, so the tests would bypass
|
|
725
|
+
// the guard's real path. A genuine `ResultAsync` exercises it.
|
|
621
726
|
const method = (name: string, response: unknown) => (args: unknown) => {
|
|
622
727
|
opts.onCall?.(name, args);
|
|
623
|
-
return
|
|
728
|
+
return okAsync(response);
|
|
624
729
|
};
|
|
625
730
|
return {
|
|
626
731
|
account: {
|
|
@@ -1073,4 +1178,87 @@ if (import.meta.vitest) {
|
|
|
1073
1178
|
expect(signed).toEqual(fromHex("0xfeed"));
|
|
1074
1179
|
vi.restoreAllMocks();
|
|
1075
1180
|
});
|
|
1181
|
+
|
|
1182
|
+
describe("response-decode boundary (guardDecode)", () => {
|
|
1183
|
+
// A client whose `createAccountProof` returns a REAL neverthrow
|
|
1184
|
+
// `ResultAsync` — the hand-rolled `okMatch` fake can't reject, and
|
|
1185
|
+
// rejection (a thrown SCALE decode) is exactly what this boundary
|
|
1186
|
+
// exists to catch. `createRingVRFProof` is the reported call (#270).
|
|
1187
|
+
function clientWithProof(result: ResultAsync<unknown, unknown>): TrUApiClient {
|
|
1188
|
+
return {
|
|
1189
|
+
account: { createAccountProof: () => result },
|
|
1190
|
+
} as unknown as TrUApiClient;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
const KEY_HANDLE = {
|
|
1194
|
+
dotNsIdentifier: "people.dot",
|
|
1195
|
+
derivationIndex: { tag: "Index", value: 0 },
|
|
1196
|
+
} as unknown as RingVrfKeyHandle;
|
|
1197
|
+
const CONTEXT = {
|
|
1198
|
+
productId: "app.dot",
|
|
1199
|
+
suffix: { tag: "Index", value: 0 },
|
|
1200
|
+
} as ProductProofContext;
|
|
1201
|
+
const RING: RingLocation = { chainId: "0x01", junctions: [] };
|
|
1202
|
+
const MESSAGE = new Uint8Array([1, 2, 3]);
|
|
1203
|
+
|
|
1204
|
+
const callProof = (result: ResultAsync<unknown, unknown>) =>
|
|
1205
|
+
adaptAccountsProvider(clientWithProof(result)).createRingVRFProof(
|
|
1206
|
+
KEY_HANDLE,
|
|
1207
|
+
CONTEXT,
|
|
1208
|
+
RING,
|
|
1209
|
+
MESSAGE,
|
|
1210
|
+
);
|
|
1211
|
+
|
|
1212
|
+
test("a thrown decode error (RangeError) becomes a HostResponseDecodeError naming the call", async () => {
|
|
1213
|
+
const rangeError = new RangeError("Offset is outside the bounds of the DataView");
|
|
1214
|
+
const result = await callProof(ResultAsync.fromSafePromise(Promise.reject(rangeError)));
|
|
1215
|
+
|
|
1216
|
+
expect(result.isErr()).toBe(true);
|
|
1217
|
+
const error = result._unsafeUnwrapErr();
|
|
1218
|
+
expect(error).toBeInstanceOf(HostResponseDecodeError);
|
|
1219
|
+
expect((error as HostResponseDecodeError).call).toBe("createRingVRFProof");
|
|
1220
|
+
// The original error is preserved as `cause` so a bug report can see it.
|
|
1221
|
+
expect((error as HostResponseDecodeError).cause).toBe(rangeError);
|
|
1222
|
+
});
|
|
1223
|
+
|
|
1224
|
+
test("a synchronous throw in the response mapping is caught too", async () => {
|
|
1225
|
+
// e.g. a malformed hex field reaching `fromHex` inside `.map`.
|
|
1226
|
+
const result = await callProof(
|
|
1227
|
+
okAsync({
|
|
1228
|
+
proof: "not-hex",
|
|
1229
|
+
contextualAlias: { context: "0x01", alias: "0x02" },
|
|
1230
|
+
ringIndex: 0,
|
|
1231
|
+
ringRevision: 0,
|
|
1232
|
+
}),
|
|
1233
|
+
);
|
|
1234
|
+
expect(result.isErr()).toBe(true);
|
|
1235
|
+
expect(result._unsafeUnwrapErr()).toBeInstanceOf(HostResponseDecodeError);
|
|
1236
|
+
});
|
|
1237
|
+
|
|
1238
|
+
test("a well-formed response passes through unchanged", async () => {
|
|
1239
|
+
const result = await callProof(
|
|
1240
|
+
okAsync({
|
|
1241
|
+
proof: "0xc0ffee",
|
|
1242
|
+
contextualAlias: { context: "0x01", alias: "0x02" },
|
|
1243
|
+
ringIndex: 3,
|
|
1244
|
+
ringRevision: 7,
|
|
1245
|
+
}),
|
|
1246
|
+
);
|
|
1247
|
+
expect(result.isOk()).toBe(true);
|
|
1248
|
+
const proof = result._unsafeUnwrap();
|
|
1249
|
+
expect(proof.proof).toEqual(fromHex("0xc0ffee"));
|
|
1250
|
+
expect(proof.ringIndex).toBe(3);
|
|
1251
|
+
expect(proof.ringRevision).toBe(7);
|
|
1252
|
+
});
|
|
1253
|
+
|
|
1254
|
+
test("the call's own typed Err passes through, not wrapped as a decode error", async () => {
|
|
1255
|
+
const typedErr = { tag: "Domain", value: { tag: "RingNotFound" } };
|
|
1256
|
+
const result = await callProof(errAsync(typedErr));
|
|
1257
|
+
|
|
1258
|
+
expect(result.isErr()).toBe(true);
|
|
1259
|
+
const error = result._unsafeUnwrapErr();
|
|
1260
|
+
expect(error).not.toBeInstanceOf(HostResponseDecodeError);
|
|
1261
|
+
expect(error).toEqual(typedErr);
|
|
1262
|
+
});
|
|
1263
|
+
});
|
|
1076
1264
|
}
|
package/src/errors.ts
CHANGED
|
@@ -144,6 +144,38 @@ export class HostCallFailedError extends HostError {
|
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
/**
|
|
148
|
+
* A host call could not be processed to completion: the `ResultAsync` the
|
|
149
|
+
* truapi client returns rejected instead of resolving to an ok/err. The usual
|
|
150
|
+
* cause is a response the client's SCALE codec can't decode (a
|
|
151
|
+
* `RangeError: Offset is outside the bounds of the DataView`) because the host
|
|
152
|
+
* and the `@parity/truapi` version the product is built against disagree on the
|
|
153
|
+
* wire shape of that call — a protocol-version skew. A host channel that closed
|
|
154
|
+
* mid-call looks identical from here, so this does not assert the skew; the
|
|
155
|
+
* real error is preserved on {@link cause}.
|
|
156
|
+
*
|
|
157
|
+
* The truapi client catches the decode throw in its message handler and turns
|
|
158
|
+
* it into a promise rejection, then wraps the call with
|
|
159
|
+
* `ResultAsync.fromSafePromise`, which installs no rejection handler — so the
|
|
160
|
+
* rejection escapes the `Result` channel rather than landing on its err side.
|
|
161
|
+
* Without this boundary that surfaces as a raw `RangeError` with a stack naming
|
|
162
|
+
* neither the call nor the cause. This names the call, so a bug report has
|
|
163
|
+
* somewhere to start.
|
|
164
|
+
*/
|
|
165
|
+
export class HostResponseDecodeError extends HostError {
|
|
166
|
+
/** The host-API call whose response failed to decode, e.g. `"createRingVRFProof"`. */
|
|
167
|
+
readonly call: string;
|
|
168
|
+
|
|
169
|
+
constructor(call: string, cause: unknown) {
|
|
170
|
+
super(
|
|
171
|
+
`Could not process the host's response to ${call}: ${formatHostError(cause)}. The usual cause is a protocol-version skew between the host app and the @parity/truapi version this product is built against; a host channel that closed mid-call looks the same.`,
|
|
172
|
+
{ cause },
|
|
173
|
+
);
|
|
174
|
+
this.name = "HostResponseDecodeError";
|
|
175
|
+
this.call = call;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
147
179
|
/** Check whether a value is any {@link HostError}. */
|
|
148
180
|
export function isHostError(error: unknown): error is HostError {
|
|
149
181
|
return error instanceof HostError;
|
|
@@ -192,9 +224,23 @@ if (import.meta.vitest) {
|
|
|
192
224
|
expect(e.message).toBe("submit failed: timeout");
|
|
193
225
|
});
|
|
194
226
|
|
|
227
|
+
test("HostResponseDecodeError names the call, interpolates the cause, and preserves it", () => {
|
|
228
|
+
const cause = new RangeError("Offset is outside the bounds of the DataView");
|
|
229
|
+
const e = new HostResponseDecodeError("createRingVRFProof", cause);
|
|
230
|
+
expect(e).toBeInstanceOf(HostError);
|
|
231
|
+
expect(e.name).toBe("HostResponseDecodeError");
|
|
232
|
+
expect(e.call).toBe("createRingVRFProof");
|
|
233
|
+
expect(e.cause).toBe(cause);
|
|
234
|
+
expect(e.message).toContain("createRingVRFProof");
|
|
235
|
+
// The rendered cause is in the message, not just on `.cause`.
|
|
236
|
+
expect(e.message).toContain("Offset is outside the bounds of the DataView");
|
|
237
|
+
expect(e.message).toContain("protocol-version skew");
|
|
238
|
+
});
|
|
239
|
+
|
|
195
240
|
test("isHostError narrows host errors only", () => {
|
|
196
241
|
expect(isHostError(new HostUnavailableError())).toBe(true);
|
|
197
242
|
expect(isHostError(new HostCallFailedError("x", { tag: "Denied" }))).toBe(true);
|
|
243
|
+
expect(isHostError(new HostResponseDecodeError("c", new Error("boom")))).toBe(true);
|
|
198
244
|
expect(isHostError(new Error("plain"))).toBe(false);
|
|
199
245
|
expect(isHostError("string")).toBe(false);
|
|
200
246
|
});
|
package/src/index.ts
CHANGED
|
@@ -70,6 +70,7 @@ export {
|
|
|
70
70
|
HostError,
|
|
71
71
|
HostUnavailableError,
|
|
72
72
|
HostCallFailedError,
|
|
73
|
+
HostResponseDecodeError,
|
|
73
74
|
isHostError,
|
|
74
75
|
formatHostError,
|
|
75
76
|
} from "./errors.js";
|
|
@@ -79,6 +80,7 @@ export type { HostErrorPayload } from "./errors.js";
|
|
|
79
80
|
export { getAccountsProvider, findRingVrfKeyHandle } from "./accounts.js";
|
|
80
81
|
export type {
|
|
81
82
|
AccountsProvider,
|
|
83
|
+
WithDecodeError,
|
|
82
84
|
DerivationIndex,
|
|
83
85
|
HostAccount,
|
|
84
86
|
ProductAccount,
|
|
@@ -103,6 +105,10 @@ export type { DevicePermissionKind, RemotePermissionItem } from "./permissions.j
|
|
|
103
105
|
export { getThemeProvider } from "./theme.js";
|
|
104
106
|
export type { ThemeMode, ThemeName, ThemeProvider, ThemeVariant } from "./theme.js";
|
|
105
107
|
|
|
108
|
+
// Locale provider
|
|
109
|
+
export { getLocaleProvider } from "./locale.js";
|
|
110
|
+
export type { LocaleInfo, LocaleProvider } from "./locale.js";
|
|
111
|
+
|
|
106
112
|
// Entropy derivation (RFC-0007)
|
|
107
113
|
export { deriveEntropy } from "./entropy.js";
|
|
108
114
|
|