@parity/product-sdk-signer 0.8.2 → 0.9.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 +37 -66
- package/dist/index.js +38 -42
- package/dist/index.js.map +1 -1
- package/package.json +3 -7
- package/src/errors.ts +2 -1
- package/src/providers/host.ts +198 -365
- package/src/signer-manager.ts +30 -1
- package/src/types.ts +10 -8
package/src/providers/host.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
// Copyright 2026 Parity Technologies (UK) Ltd.
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
import { deriveH160, ss58Encode } from "@parity/product-sdk-address";
|
|
4
|
+
import {
|
|
5
|
+
getAccountsProvider,
|
|
6
|
+
type RemotePermission,
|
|
7
|
+
requestPermission,
|
|
8
|
+
} from "@parity/product-sdk-host";
|
|
4
9
|
import { createLogger } from "@parity/product-sdk-logger";
|
|
5
10
|
|
|
6
11
|
import { HostRejectedError, HostUnavailableError, type SignerError } from "../errors.js";
|
|
@@ -36,17 +41,19 @@ export interface HostProviderOptions {
|
|
|
36
41
|
*/
|
|
37
42
|
dappName?: string;
|
|
38
43
|
/**
|
|
39
|
-
* Custom
|
|
40
|
-
*
|
|
44
|
+
* Custom accounts-provider loader. Defaults to `@parity/product-sdk-host`'s
|
|
45
|
+
* `getAccountsProvider`, which returns `null` outside a host container.
|
|
46
|
+
* Override for testing or custom host setups.
|
|
41
47
|
* @internal
|
|
42
48
|
*/
|
|
43
|
-
|
|
49
|
+
loadAccountsProvider?: () => Promise<AccountsProvider | null>;
|
|
44
50
|
/**
|
|
45
|
-
* Custom
|
|
46
|
-
* `
|
|
51
|
+
* Custom `ChainSubmit` permission requester. Defaults to a thin adapter over
|
|
52
|
+
* `@parity/product-sdk-host`'s `requestPermission` that unwraps its `Result`
|
|
53
|
+
* (throwing the typed error on the `err` channel). Override for testing.
|
|
47
54
|
* @internal
|
|
48
55
|
*/
|
|
49
|
-
|
|
56
|
+
requestChainSubmitPermissionFn?: (permission: RemotePermission) => Promise<boolean>;
|
|
50
57
|
/**
|
|
51
58
|
* Whether to request the host's `ChainSubmit` permission after a
|
|
52
59
|
* successful `connect()`. Without this, subsequent signing requests are
|
|
@@ -66,7 +73,7 @@ export interface HostProviderOptions {
|
|
|
66
73
|
* `dotNsIdentifier`, skipping the legacy fetch entirely. For apps
|
|
67
74
|
* that sign exclusively with a per-dapp derived account.
|
|
68
75
|
*
|
|
69
|
-
* Signing
|
|
76
|
+
* Signing goes through the host's `createTransaction` path (see PR #96).
|
|
70
77
|
*/
|
|
71
78
|
productAccount?: {
|
|
72
79
|
/** App identifier (e.g., `"playground.dot"`). */
|
|
@@ -140,33 +147,17 @@ interface NeverthrowResultAsync<T, E> {
|
|
|
140
147
|
match: <A, B = A>(ok: (t: T) => A, err: (e: E) => B) => Promise<A | B>;
|
|
141
148
|
}
|
|
142
149
|
|
|
143
|
-
/**
|
|
144
|
-
* Pin product-account signing to Nova's `host_create_transaction` path.
|
|
145
|
-
*
|
|
146
|
-
* The `createTransaction` path forwards opaque signed-extension bytes to
|
|
147
|
-
* the host for metadata-driven decoding, so unknown extensions (e.g.
|
|
148
|
-
* `AsPgas` on Paseo Next) survive end-to-end. The alternate
|
|
149
|
-
* `"signPayload"` path wraps via PJS and throws
|
|
150
|
-
* `"PJS does not support this signed-extension: AsPgas"` on those chains.
|
|
151
|
-
*
|
|
152
|
-
* Nova's `host-api-wrapper@0.8.0` already defaults to `"createTransaction"`,
|
|
153
|
-
* so this is a defensive pin rather than an opt-in — it guards against a
|
|
154
|
-
* future upstream default flip and makes the routing legible at the call
|
|
155
|
-
* site. The legacy-account signer doesn't expose this switch.
|
|
156
|
-
*/
|
|
157
|
-
const PRODUCT_SIGNER_TYPE = "createTransaction" as const;
|
|
158
|
-
|
|
159
150
|
/** @internal */
|
|
160
151
|
export interface AccountsProvider {
|
|
161
|
-
|
|
152
|
+
getLegacyAccounts: () => NeverthrowResultAsync<RawAccount[], unknown>;
|
|
153
|
+
getLegacyAccountSigner: (account: {
|
|
154
|
+
publicKey: Uint8Array;
|
|
155
|
+
}) => import("polkadot-api").PolkadotSigner;
|
|
162
156
|
getProductAccount: (
|
|
163
157
|
dotNsIdentifier: string,
|
|
164
158
|
derivationIndex?: number,
|
|
165
159
|
) => NeverthrowResultAsync<RawAccount, unknown>;
|
|
166
|
-
getProductAccountSigner: (
|
|
167
|
-
account: ProductAccount,
|
|
168
|
-
signerType?: "signPayload" | "createTransaction",
|
|
169
|
-
) => import("polkadot-api").PolkadotSigner;
|
|
160
|
+
getProductAccountSigner: (account: ProductAccount) => import("polkadot-api").PolkadotSigner;
|
|
170
161
|
getProductAccountAlias: (
|
|
171
162
|
dotNsIdentifier: string,
|
|
172
163
|
derivationIndex?: number,
|
|
@@ -183,57 +174,33 @@ export interface AccountsProvider {
|
|
|
183
174
|
) => { unsubscribe: () => void } | (() => void);
|
|
184
175
|
}
|
|
185
176
|
|
|
186
|
-
/** @internal */
|
|
187
|
-
export interface HostApiPermissionBridge {
|
|
188
|
-
/**
|
|
189
|
-
* Request a Host API permission. Product-sdk's `hostApi.permission(...)`
|
|
190
|
-
* takes a tagged enum like `enumValue("v1", { tag: "TransactionSubmit" })`
|
|
191
|
-
* and returns a neverthrow ResultAsync.
|
|
192
|
-
*/
|
|
193
|
-
permission: (request: unknown) => NeverthrowResultAsync<unknown, unknown>;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/** @internal */
|
|
197
|
-
export interface HostApiEnumHelper {
|
|
198
|
-
enumValue: (version: string, value: { tag: string; value?: unknown }) => unknown;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/** @internal */
|
|
202
|
-
export interface ProductSdkModule {
|
|
203
|
-
createAccountsProvider: () => AccountsProvider;
|
|
204
|
-
/** Present from product-sdk ≥ 0.6; used to request TransactionSubmit. */
|
|
205
|
-
hostApi?: HostApiPermissionBridge;
|
|
206
|
-
/**
|
|
207
|
-
* `sandboxTransport.isCorrectEnvironment()` returns `false` when the app
|
|
208
|
-
* is loaded outside a Polkadot host container (e.g. a regular browser
|
|
209
|
-
* tab). Calling `getLegacyAccounts()` / `getProductAccount()` in that
|
|
210
|
-
* state surfaces the upstream `Environment is not correct` exception,
|
|
211
|
-
* so we pre-check during `connect()` and raise a specific
|
|
212
|
-
* {@link HostUnavailableError} with actionable guidance instead.
|
|
213
|
-
*/
|
|
214
|
-
sandboxTransport?: { isCorrectEnvironment(): boolean };
|
|
215
|
-
}
|
|
216
|
-
|
|
217
177
|
/* @integration */
|
|
218
|
-
async function
|
|
219
|
-
|
|
178
|
+
async function defaultLoadAccountsProvider(): Promise<AccountsProvider | null> {
|
|
179
|
+
// `@parity/product-sdk-host`'s provider is structurally compatible with the
|
|
180
|
+
// (looser) shape declared above; the cast bridges the nominal gap.
|
|
181
|
+
return (await getAccountsProvider()) as unknown as AccountsProvider | null;
|
|
220
182
|
}
|
|
221
183
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
184
|
+
/**
|
|
185
|
+
* Default `requestChainSubmitPermissionFn`: bridge host's `Result`-returning
|
|
186
|
+
* {@link requestPermission} back to the option's `Promise<boolean>` contract by
|
|
187
|
+
* throwing the typed `HostError` on the `err` channel. The throw is caught
|
|
188
|
+
* (and warned, not fatal) at the connect-time call site.
|
|
189
|
+
*/
|
|
190
|
+
async function defaultRequestChainSubmitPermission(permission: RemotePermission): Promise<boolean> {
|
|
191
|
+
const result = await requestPermission(permission);
|
|
192
|
+
if (!result.ok) throw result.error;
|
|
193
|
+
return result.value;
|
|
225
194
|
}
|
|
226
195
|
|
|
227
196
|
/**
|
|
228
197
|
* Provider for the Host API (Polkadot Desktop / Android).
|
|
229
198
|
*
|
|
230
|
-
*
|
|
231
|
-
* outside a host container — e.g.
|
|
232
|
-
*
|
|
233
|
-
* the
|
|
234
|
-
*
|
|
235
|
-
* before any host RPC call, so the user never sees the upstream
|
|
236
|
-
* `Environment is not correct` exception leaking through.
|
|
199
|
+
* Backed by `@parity/product-sdk-host`'s `getAccountsProvider`, which talks to
|
|
200
|
+
* the host over `@parity/truapi`. Apps running outside a host container — e.g.
|
|
201
|
+
* a plain browser tab during `npm run dev` — get a `HOST_UNAVAILABLE` error
|
|
202
|
+
* (the provider resolves to `null`) with actionable guidance, surfaced before
|
|
203
|
+
* any host RPC call.
|
|
237
204
|
*
|
|
238
205
|
* Supports both non-product accounts (user's external wallets) and product
|
|
239
206
|
* accounts (app-scoped derived accounts managed by the host).
|
|
@@ -243,8 +210,10 @@ export class HostProvider implements SignerProvider {
|
|
|
243
210
|
private readonly ss58Prefix: number;
|
|
244
211
|
private readonly maxRetries: number;
|
|
245
212
|
private readonly retryDelay: number;
|
|
246
|
-
private readonly
|
|
247
|
-
private readonly
|
|
213
|
+
private readonly loadAccountsProvider: () => Promise<AccountsProvider | null>;
|
|
214
|
+
private readonly requestChainSubmitPermissionFn: (
|
|
215
|
+
permission: RemotePermission,
|
|
216
|
+
) => Promise<boolean>;
|
|
248
217
|
private readonly requestChainSubmitPermission: boolean;
|
|
249
218
|
private readonly productAccount: HostProviderOptions["productAccount"];
|
|
250
219
|
private readonly dappName: string | undefined;
|
|
@@ -258,8 +227,9 @@ export class HostProvider implements SignerProvider {
|
|
|
258
227
|
this.ss58Prefix = options?.ss58Prefix ?? 42;
|
|
259
228
|
this.maxRetries = options?.maxRetries ?? 3;
|
|
260
229
|
this.retryDelay = options?.retryDelay ?? 500;
|
|
261
|
-
this.
|
|
262
|
-
this.
|
|
230
|
+
this.loadAccountsProvider = options?.loadAccountsProvider ?? defaultLoadAccountsProvider;
|
|
231
|
+
this.requestChainSubmitPermissionFn =
|
|
232
|
+
options?.requestChainSubmitPermissionFn ?? defaultRequestChainSubmitPermission;
|
|
263
233
|
// New name takes precedence; fall back to the deprecated alias.
|
|
264
234
|
this.requestChainSubmitPermission =
|
|
265
235
|
options?.requestChainSubmitPermission ??
|
|
@@ -360,10 +330,7 @@ export class HostProvider implements SignerProvider {
|
|
|
360
330
|
if (!this.accountsProvider) {
|
|
361
331
|
throw new Error("Host provider is disconnected");
|
|
362
332
|
}
|
|
363
|
-
return this.accountsProvider.getProductAccountSigner(
|
|
364
|
-
productAccount,
|
|
365
|
-
PRODUCT_SIGNER_TYPE,
|
|
366
|
-
);
|
|
333
|
+
return this.accountsProvider.getProductAccountSigner(productAccount);
|
|
367
334
|
},
|
|
368
335
|
});
|
|
369
336
|
} catch (cause) {
|
|
@@ -382,17 +349,15 @@ export class HostProvider implements SignerProvider {
|
|
|
382
349
|
* Convenience method for when you already have the product account details.
|
|
383
350
|
* Requires a prior successful `connect()` call.
|
|
384
351
|
*
|
|
385
|
-
*
|
|
386
|
-
*
|
|
387
|
-
*
|
|
388
|
-
* metadata-driven decoding, rather than going through the PJS bridge
|
|
389
|
-
* that throws on unknown extensions.
|
|
352
|
+
* Signing routes through the host's `createTransaction` path, so unknown
|
|
353
|
+
* signed extensions (e.g. `AsPgas` on Paseo Next) are forwarded to the host
|
|
354
|
+
* as opaque bytes for metadata-driven decoding.
|
|
390
355
|
*/
|
|
391
356
|
getProductAccountSigner(account: ProductAccount): import("polkadot-api").PolkadotSigner {
|
|
392
357
|
if (!this.accountsProvider) {
|
|
393
358
|
throw new Error("Host provider is not connected");
|
|
394
359
|
}
|
|
395
|
-
return this.accountsProvider.getProductAccountSigner(account
|
|
360
|
+
return this.accountsProvider.getProductAccountSigner(account);
|
|
396
361
|
}
|
|
397
362
|
|
|
398
363
|
/**
|
|
@@ -513,41 +478,33 @@ export class HostProvider implements SignerProvider {
|
|
|
513
478
|
// ── Private ──────────────────────────────────────────────────────
|
|
514
479
|
|
|
515
480
|
private async tryConnect(): Promise<Result<SignerAccount[], SignerError>> {
|
|
516
|
-
// Step 1:
|
|
517
|
-
|
|
481
|
+
// Step 1: Obtain the host accounts provider. `null` (or a thrown error)
|
|
482
|
+
// means we're not inside a host container.
|
|
483
|
+
let provider: AccountsProvider | null;
|
|
518
484
|
try {
|
|
519
|
-
|
|
485
|
+
provider = await this.loadAccountsProvider();
|
|
520
486
|
} catch (cause) {
|
|
521
|
-
log.warn("
|
|
487
|
+
log.warn("host accounts provider unavailable", { cause });
|
|
522
488
|
return err(
|
|
523
489
|
new HostUnavailableError(
|
|
524
490
|
cause instanceof Error
|
|
525
|
-
? `
|
|
526
|
-
: "
|
|
491
|
+
? `host accounts provider failed: ${cause.message}`
|
|
492
|
+
: "host accounts provider is unavailable",
|
|
527
493
|
),
|
|
528
494
|
);
|
|
529
495
|
}
|
|
530
496
|
|
|
531
497
|
// Step 2: Verify we're actually running inside a host container.
|
|
532
498
|
//
|
|
533
|
-
//
|
|
534
|
-
//
|
|
535
|
-
//
|
|
536
|
-
//
|
|
537
|
-
//
|
|
538
|
-
//
|
|
539
|
-
//
|
|
540
|
-
//
|
|
541
|
-
|
|
542
|
-
// Returning `HostUnavailableError` here matches the TSDoc contract
|
|
543
|
-
// ("Apps running outside a host container will gracefully get a
|
|
544
|
-
// HOST_UNAVAILABLE error") and gives consumers actionable guidance.
|
|
545
|
-
//
|
|
546
|
-
// The `sandboxTransport` field is optional in `ProductSdkModule` so
|
|
547
|
-
// older wrapper versions (or test mocks that don't supply it) keep
|
|
548
|
-
// working — we fall through to the existing flow and rely on the
|
|
549
|
-
// catch in Step 4 as a safety net.
|
|
550
|
-
if (sdk.sandboxTransport && !sdk.sandboxTransport.isCorrectEnvironment()) {
|
|
499
|
+
// `getAccountsProvider()` resolves to `null` when the app isn't loaded
|
|
500
|
+
// inside a Polkadot host container (e.g. a plain browser tab during
|
|
501
|
+
// `npm run dev`, with no iframe under Polkadot Desktop or WebView under
|
|
502
|
+
// Polkadot Mobile). Returning `HostUnavailableError` here — before any
|
|
503
|
+
// host RPC call — matches the TSDoc contract ("Apps running outside a
|
|
504
|
+
// host container will gracefully get a HOST_UNAVAILABLE error") and
|
|
505
|
+
// gives consumers actionable guidance, rather than letting a later RPC
|
|
506
|
+
// surface a misleading rejection.
|
|
507
|
+
if (!provider) {
|
|
551
508
|
log.warn("not inside a host container — Host API unavailable");
|
|
552
509
|
return err(
|
|
553
510
|
new HostUnavailableError(
|
|
@@ -557,12 +514,9 @@ export class HostProvider implements SignerProvider {
|
|
|
557
514
|
),
|
|
558
515
|
);
|
|
559
516
|
}
|
|
560
|
-
|
|
561
|
-
// Step 3: Create accounts provider
|
|
562
|
-
const provider = sdk.createAccountsProvider();
|
|
563
517
|
this.accountsProvider = provider;
|
|
564
518
|
|
|
565
|
-
// Step
|
|
519
|
+
// Step 3: Fetch accounts.
|
|
566
520
|
//
|
|
567
521
|
// Both branches end in `fetchProductSignerAccount`. The difference
|
|
568
522
|
// is only where the dotNS identifier comes from:
|
|
@@ -631,7 +585,7 @@ export class HostProvider implements SignerProvider {
|
|
|
631
585
|
signerAccounts = [];
|
|
632
586
|
}
|
|
633
587
|
|
|
634
|
-
// Step
|
|
588
|
+
// Step 4: Request ChainSubmit permission up-front.
|
|
635
589
|
//
|
|
636
590
|
// The host gates signing on this permission — without it, the
|
|
637
591
|
// production host rejects every sign request with `PermissionDenied`
|
|
@@ -644,30 +598,13 @@ export class HostProvider implements SignerProvider {
|
|
|
644
598
|
// We don't fail `connect()` if this step fails: the consumer can still
|
|
645
599
|
// use the signer for read-only code paths, and the actual sign call
|
|
646
600
|
// will surface a clear error if permission is missing.
|
|
647
|
-
|
|
648
|
-
// The legal v1 RemotePermission variants per
|
|
649
|
-
// `@novasamatech/host-api@0.8.0` are: Remote, WebRtc, ChainSubmit,
|
|
650
|
-
// PreimageSubmit, StatementSubmit. ChainSubmit is the chain-tx
|
|
651
|
-
// permission (was named TransactionSubmit in earlier host-api
|
|
652
|
-
// revisions; renamed in 0.7). `WebRtc` was spelled `WebRTC` before
|
|
653
|
-
// 0.8.
|
|
654
|
-
if (this.requestChainSubmitPermission && sdk.hostApi) {
|
|
601
|
+
if (this.requestChainSubmitPermission) {
|
|
655
602
|
try {
|
|
656
|
-
const
|
|
657
|
-
const request = hostApiEnum.enumValue("v1", {
|
|
603
|
+
const granted = await this.requestChainSubmitPermissionFn({
|
|
658
604
|
tag: "ChainSubmit",
|
|
659
605
|
value: undefined,
|
|
660
606
|
});
|
|
661
|
-
|
|
662
|
-
() => {
|
|
663
|
-
log.debug("ChainSubmit permission granted");
|
|
664
|
-
},
|
|
665
|
-
(error) => {
|
|
666
|
-
log.warn("ChainSubmit permission rejected by host", {
|
|
667
|
-
error: formatError(error),
|
|
668
|
-
});
|
|
669
|
-
},
|
|
670
|
-
);
|
|
607
|
+
log.debug("ChainSubmit permission result", { granted });
|
|
671
608
|
} catch (cause) {
|
|
672
609
|
log.warn("failed to request ChainSubmit permission", { cause });
|
|
673
610
|
}
|
|
@@ -675,9 +612,11 @@ export class HostProvider implements SignerProvider {
|
|
|
675
612
|
|
|
676
613
|
log.info("host connected", { accounts: signerAccounts.length });
|
|
677
614
|
|
|
678
|
-
// Step
|
|
615
|
+
// Step 5: Subscribe to connection status. The host reports a string
|
|
616
|
+
// union (`"Connected"` / `"Disconnected"`); match case-insensitively.
|
|
679
617
|
const sub = provider.subscribeAccountConnectionStatus((status) => {
|
|
680
|
-
const mapped: ConnectionStatus =
|
|
618
|
+
const mapped: ConnectionStatus =
|
|
619
|
+
String(status).toLowerCase() === "connected" ? "connected" : "disconnected";
|
|
681
620
|
log.debug("host status changed", { status: mapped });
|
|
682
621
|
for (const listener of this.statusListeners) {
|
|
683
622
|
listener(mapped);
|
|
@@ -746,7 +685,12 @@ export class HostProvider implements SignerProvider {
|
|
|
746
685
|
function formatError(error: unknown): string {
|
|
747
686
|
if (!error || typeof error !== "object") return String(error);
|
|
748
687
|
const e = error as Record<string, unknown>;
|
|
749
|
-
|
|
688
|
+
// truapi GenericError is { reason } with no `tag`.
|
|
689
|
+
if (!("tag" in e)) {
|
|
690
|
+
if (typeof e.reason === "string") return e.reason;
|
|
691
|
+
if (typeof e.message === "string") return e.message;
|
|
692
|
+
return String(error);
|
|
693
|
+
}
|
|
750
694
|
|
|
751
695
|
const outerTag = String(e.tag);
|
|
752
696
|
const inner = e.value;
|
|
@@ -855,78 +799,58 @@ if (import.meta.vitest) {
|
|
|
855
799
|
};
|
|
856
800
|
}
|
|
857
801
|
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
hostApi?: HostApiPermissionBridge;
|
|
862
|
-
/**
|
|
863
|
-
* When provided, the mock's `sandboxTransport.isCorrectEnvironment()`
|
|
864
|
-
* returns this value — exercises the env-check branch added in the
|
|
865
|
-
* `connect()` flow. Omit to skip the check entirely (older-wrapper
|
|
866
|
-
* compatibility path).
|
|
867
|
-
*/
|
|
868
|
-
isCorrectEnvironment?: boolean;
|
|
869
|
-
},
|
|
870
|
-
): ProductSdkModule {
|
|
871
|
-
return {
|
|
872
|
-
createAccountsProvider: () => mockProvider as unknown as AccountsProvider,
|
|
873
|
-
...(opts?.hostApi ? { hostApi: opts.hostApi } : {}),
|
|
874
|
-
...(opts?.isCorrectEnvironment !== undefined
|
|
875
|
-
? { sandboxTransport: { isCorrectEnvironment: () => opts.isCorrectEnvironment! } }
|
|
876
|
-
: {}),
|
|
877
|
-
};
|
|
802
|
+
/** Wrap a mock accounts provider as the loader the HostProvider expects. */
|
|
803
|
+
function loadProvider(mockProvider: ReturnType<typeof createMockProvider>) {
|
|
804
|
+
return () => Promise.resolve(mockProvider as unknown as AccountsProvider);
|
|
878
805
|
}
|
|
879
806
|
|
|
880
|
-
/**
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
*/
|
|
884
|
-
function fakeResult<T>(value: T, error?: unknown): NeverthrowResultAsync<T, unknown> {
|
|
885
|
-
return {
|
|
886
|
-
match: async (onOk, onErr) => {
|
|
887
|
-
if (error !== undefined) return onErr(error);
|
|
888
|
-
return onOk(value);
|
|
889
|
-
},
|
|
890
|
-
};
|
|
807
|
+
/** A permission requester spy that always grants, unless overridden. */
|
|
808
|
+
function grantPermission() {
|
|
809
|
+
return vi.fn<(permission: RemotePermission) => Promise<boolean>>().mockResolvedValue(true);
|
|
891
810
|
}
|
|
892
811
|
|
|
893
|
-
const fakeHostApiEnum: HostApiEnumHelper = {
|
|
894
|
-
enumValue: (version, value) => ({ version, value }),
|
|
895
|
-
};
|
|
896
|
-
|
|
897
812
|
beforeEach(() => {
|
|
898
813
|
vi.restoreAllMocks();
|
|
899
814
|
});
|
|
900
815
|
|
|
901
816
|
describe("HostProvider", () => {
|
|
902
|
-
test("returns HOST_UNAVAILABLE when
|
|
817
|
+
test("returns HOST_UNAVAILABLE when the accounts-provider loader throws", async () => {
|
|
818
|
+
const provider = new HostProvider({
|
|
819
|
+
maxRetries: 1,
|
|
820
|
+
loadAccountsProvider: () => Promise.reject(new Error("boom")),
|
|
821
|
+
});
|
|
822
|
+
const result = await provider.connect();
|
|
823
|
+
|
|
824
|
+
expect(result.ok).toBe(false);
|
|
825
|
+
if (!result.ok) {
|
|
826
|
+
expect(result.error).toBeInstanceOf(HostUnavailableError);
|
|
827
|
+
expect(result.error.message).toContain("boom");
|
|
828
|
+
}
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
test("returns HOST_UNAVAILABLE when not inside a host container (provider null)", async () => {
|
|
903
832
|
const provider = new HostProvider({
|
|
904
833
|
maxRetries: 1,
|
|
905
|
-
|
|
834
|
+
loadAccountsProvider: () => Promise.resolve(null),
|
|
906
835
|
});
|
|
907
836
|
const result = await provider.connect();
|
|
908
837
|
|
|
909
838
|
expect(result.ok).toBe(false);
|
|
910
839
|
if (!result.ok) {
|
|
911
840
|
expect(result.error).toBeInstanceOf(HostUnavailableError);
|
|
912
|
-
expect(result.error.message).toContain("Cannot find module");
|
|
913
841
|
}
|
|
914
842
|
});
|
|
915
843
|
|
|
916
844
|
test("returns HOST_UNAVAILABLE with actionable guidance when not inside a host container", async () => {
|
|
917
|
-
//
|
|
918
|
-
//
|
|
919
|
-
//
|
|
920
|
-
//
|
|
921
|
-
//
|
|
922
|
-
// Post-fix: we pre-check during connect() and return a specific
|
|
923
|
-
// HostUnavailableError naming the host container and pointing
|
|
924
|
-
// the user at the fix path.
|
|
845
|
+
// Outside a host container `getAccountsProvider()` resolves to null;
|
|
846
|
+
// connect() returns a HostUnavailableError naming the host container
|
|
847
|
+
// and pointing the user at the fix path — without making any host
|
|
848
|
+
// RPC call. (Repro for playground-cli#4: `npm run dev` opens
|
|
849
|
+
// localhost in a plain browser tab — no iframe, no WebView.)
|
|
925
850
|
const mockProvider = createMockProvider({ accounts: [] });
|
|
926
851
|
const provider = new HostProvider({
|
|
927
852
|
maxRetries: 1,
|
|
928
|
-
|
|
929
|
-
Promise.resolve(createMockSdk(mockProvider, { isCorrectEnvironment: false })),
|
|
853
|
+
loadAccountsProvider: () => Promise.resolve(null),
|
|
930
854
|
});
|
|
931
855
|
const result = await provider.connect();
|
|
932
856
|
|
|
@@ -957,7 +881,8 @@ if (import.meta.vitest) {
|
|
|
957
881
|
const provider = new HostProvider({
|
|
958
882
|
maxRetries: 1,
|
|
959
883
|
dappName: "my-cli",
|
|
960
|
-
|
|
884
|
+
loadAccountsProvider: loadProvider(mockProvider),
|
|
885
|
+
requestChainSubmitPermissionFn: grantPermission(),
|
|
961
886
|
});
|
|
962
887
|
const result = await provider.connect();
|
|
963
888
|
|
|
@@ -979,7 +904,8 @@ if (import.meta.vitest) {
|
|
|
979
904
|
const provider = new HostProvider({
|
|
980
905
|
maxRetries: 1,
|
|
981
906
|
dappName: "my-cli.dot",
|
|
982
|
-
|
|
907
|
+
loadAccountsProvider: loadProvider(mockProvider),
|
|
908
|
+
requestChainSubmitPermissionFn: grantPermission(),
|
|
983
909
|
});
|
|
984
910
|
const result = await provider.connect();
|
|
985
911
|
|
|
@@ -987,6 +913,31 @@ if (import.meta.vitest) {
|
|
|
987
913
|
expect(mockProvider.getProductAccount).toHaveBeenCalledWith("my-cli.dot", 0);
|
|
988
914
|
});
|
|
989
915
|
|
|
916
|
+
test("connect succeeds when the default ChainSubmit permission request fails", async () => {
|
|
917
|
+
// No `requestChainSubmitPermissionFn` override → the default adapter
|
|
918
|
+
// (`defaultRequestChainSubmitPermission`) calls host's real
|
|
919
|
+
// `requestPermission`, which returns `err` outside a container. The
|
|
920
|
+
// adapter unwraps that and throws; the connect step catches it
|
|
921
|
+
// (warn, non-fatal — see Step 4) so `connect()` still resolves.
|
|
922
|
+
const productPubkey = new Uint8Array(32).fill(0x55);
|
|
923
|
+
const mockProvider = createMockProvider({
|
|
924
|
+
accounts: [{ publicKey: productPubkey, name: undefined }],
|
|
925
|
+
});
|
|
926
|
+
const provider = new HostProvider({
|
|
927
|
+
maxRetries: 1,
|
|
928
|
+
dappName: "my-cli",
|
|
929
|
+
loadAccountsProvider: loadProvider(mockProvider),
|
|
930
|
+
// requestChainSubmitPermission defaults to true; no Fn override.
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
const result = await provider.connect();
|
|
934
|
+
|
|
935
|
+
expect(result.ok).toBe(true);
|
|
936
|
+
if (result.ok) {
|
|
937
|
+
expect(result.value).toHaveLength(1);
|
|
938
|
+
}
|
|
939
|
+
});
|
|
940
|
+
|
|
990
941
|
test("connect with dappName fallback resolves to [] when host rejects derivation", async () => {
|
|
991
942
|
// Soft-degrade: when the host can't derive a product account
|
|
992
943
|
// for the dappName (commonly because the dotNS identifier isn't
|
|
@@ -997,7 +948,8 @@ if (import.meta.vitest) {
|
|
|
997
948
|
const provider = new HostProvider({
|
|
998
949
|
maxRetries: 1,
|
|
999
950
|
dappName: "not-registered",
|
|
1000
|
-
|
|
951
|
+
loadAccountsProvider: loadProvider(mockProvider),
|
|
952
|
+
requestChainSubmitPermissionFn: grantPermission(),
|
|
1001
953
|
});
|
|
1002
954
|
const result = await provider.connect();
|
|
1003
955
|
|
|
@@ -1013,7 +965,8 @@ if (import.meta.vitest) {
|
|
|
1013
965
|
const mockProvider = createMockProvider({});
|
|
1014
966
|
const provider = new HostProvider({
|
|
1015
967
|
maxRetries: 1,
|
|
1016
|
-
|
|
968
|
+
loadAccountsProvider: loadProvider(mockProvider),
|
|
969
|
+
requestChainSubmitPermissionFn: grantPermission(),
|
|
1017
970
|
});
|
|
1018
971
|
const result = await provider.connect();
|
|
1019
972
|
|
|
@@ -1023,19 +976,19 @@ if (import.meta.vitest) {
|
|
|
1023
976
|
}
|
|
1024
977
|
});
|
|
1025
978
|
|
|
1026
|
-
test("getProductAccountSigner
|
|
1027
|
-
//
|
|
1028
|
-
//
|
|
1029
|
-
//
|
|
1030
|
-
//
|
|
1031
|
-
// silently regress.
|
|
979
|
+
test("getProductAccountSigner delegates to the host accounts provider", async () => {
|
|
980
|
+
// The host accounts provider's getProductAccountSigner has a single
|
|
981
|
+
// signing path (the host's `createTransaction`, which forwards opaque
|
|
982
|
+
// signed extensions like AsPgas on Paseo Next). There is no PJS
|
|
983
|
+
// fallback to select, so it's called with just the account.
|
|
1032
984
|
const rawAccounts: RawAccountTest[] = [
|
|
1033
985
|
{ publicKey: new Uint8Array(32).fill(0xaa), name: "Alice" },
|
|
1034
986
|
];
|
|
1035
987
|
const mockProvider = createMockProvider({ accounts: rawAccounts });
|
|
1036
988
|
const provider = new HostProvider({
|
|
1037
989
|
maxRetries: 1,
|
|
1038
|
-
|
|
990
|
+
loadAccountsProvider: loadProvider(mockProvider),
|
|
991
|
+
requestChainSubmitPermissionFn: grantPermission(),
|
|
1039
992
|
});
|
|
1040
993
|
await provider.connect();
|
|
1041
994
|
|
|
@@ -1047,7 +1000,6 @@ if (import.meta.vitest) {
|
|
|
1047
1000
|
});
|
|
1048
1001
|
expect(mockProvider.getProductAccountSigner).toHaveBeenLastCalledWith(
|
|
1049
1002
|
expect.anything(),
|
|
1050
|
-
"createTransaction",
|
|
1051
1003
|
);
|
|
1052
1004
|
|
|
1053
1005
|
// Path 2: getSigner() returned from HostProvider.getProductAccount(...)
|
|
@@ -1057,7 +1009,6 @@ if (import.meta.vitest) {
|
|
|
1057
1009
|
productAccountResult.value.getSigner();
|
|
1058
1010
|
expect(mockProvider.getProductAccountSigner).toHaveBeenLastCalledWith(
|
|
1059
1011
|
expect.anything(),
|
|
1060
|
-
"createTransaction",
|
|
1061
1012
|
);
|
|
1062
1013
|
}
|
|
1063
1014
|
});
|
|
@@ -1089,7 +1040,8 @@ if (import.meta.vitest) {
|
|
|
1089
1040
|
});
|
|
1090
1041
|
const provider = new HostProvider({
|
|
1091
1042
|
maxRetries: 1,
|
|
1092
|
-
|
|
1043
|
+
loadAccountsProvider: loadProvider(mockProvider),
|
|
1044
|
+
requestChainSubmitPermissionFn: grantPermission(),
|
|
1093
1045
|
productAccount: { dotNsIdentifier: "myapp.dot", derivationIndex: 0 },
|
|
1094
1046
|
});
|
|
1095
1047
|
const result = await provider.connect();
|
|
@@ -1106,7 +1058,6 @@ if (import.meta.vitest) {
|
|
|
1106
1058
|
dotNsIdentifier: "myapp.dot",
|
|
1107
1059
|
derivationIndex: 0,
|
|
1108
1060
|
}),
|
|
1109
|
-
"createTransaction",
|
|
1110
1061
|
);
|
|
1111
1062
|
}
|
|
1112
1063
|
expect(mockProvider.getProductAccount).toHaveBeenCalledWith("myapp.dot", 0);
|
|
@@ -1124,7 +1075,8 @@ if (import.meta.vitest) {
|
|
|
1124
1075
|
});
|
|
1125
1076
|
const provider = new HostProvider({
|
|
1126
1077
|
maxRetries: 1,
|
|
1127
|
-
|
|
1078
|
+
loadAccountsProvider: loadProvider(mockProvider),
|
|
1079
|
+
requestChainSubmitPermissionFn: grantPermission(),
|
|
1128
1080
|
productAccount: { dotNsIdentifier: "myapp.dot", requestName: false },
|
|
1129
1081
|
});
|
|
1130
1082
|
const result = await provider.connect();
|
|
@@ -1150,11 +1102,12 @@ if (import.meta.vitest) {
|
|
|
1150
1102
|
match: async (
|
|
1151
1103
|
_onOk: (v: { primaryUsername: string }) => unknown,
|
|
1152
1104
|
onErr: (e: unknown) => unknown,
|
|
1153
|
-
) => onErr({ tag: "
|
|
1105
|
+
) => onErr({ tag: "PermissionDenied" }),
|
|
1154
1106
|
});
|
|
1155
1107
|
const provider = new HostProvider({
|
|
1156
1108
|
maxRetries: 1,
|
|
1157
|
-
|
|
1109
|
+
loadAccountsProvider: loadProvider(mockProvider),
|
|
1110
|
+
requestChainSubmitPermissionFn: grantPermission(),
|
|
1158
1111
|
productAccount: { dotNsIdentifier: "myapp.dot" },
|
|
1159
1112
|
});
|
|
1160
1113
|
const result = await provider.connect();
|
|
@@ -1176,7 +1129,8 @@ if (import.meta.vitest) {
|
|
|
1176
1129
|
});
|
|
1177
1130
|
const provider = new HostProvider({
|
|
1178
1131
|
maxRetries: 1,
|
|
1179
|
-
|
|
1132
|
+
loadAccountsProvider: loadProvider(mockProvider),
|
|
1133
|
+
requestChainSubmitPermissionFn: grantPermission(),
|
|
1180
1134
|
productAccount: { dotNsIdentifier: "myapp.dot", requestName: false },
|
|
1181
1135
|
});
|
|
1182
1136
|
const connectResult = await provider.connect();
|
|
@@ -1205,7 +1159,8 @@ if (import.meta.vitest) {
|
|
|
1205
1159
|
});
|
|
1206
1160
|
const provider = new HostProvider({
|
|
1207
1161
|
maxRetries: 1,
|
|
1208
|
-
|
|
1162
|
+
loadAccountsProvider: loadProvider(mockProvider),
|
|
1163
|
+
requestChainSubmitPermissionFn: grantPermission(),
|
|
1209
1164
|
productAccount: { dotNsIdentifier: "myapp.dot", requestName: false },
|
|
1210
1165
|
});
|
|
1211
1166
|
await provider.connect();
|
|
@@ -1238,7 +1193,8 @@ if (import.meta.vitest) {
|
|
|
1238
1193
|
});
|
|
1239
1194
|
const provider = new HostProvider({
|
|
1240
1195
|
maxRetries: 1,
|
|
1241
|
-
|
|
1196
|
+
loadAccountsProvider: loadProvider(mockProvider),
|
|
1197
|
+
requestChainSubmitPermissionFn: grantPermission(),
|
|
1242
1198
|
productAccount: { dotNsIdentifier: "playground.dot" },
|
|
1243
1199
|
});
|
|
1244
1200
|
const result = await provider.connect();
|
|
@@ -1252,157 +1208,58 @@ if (import.meta.vitest) {
|
|
|
1252
1208
|
});
|
|
1253
1209
|
|
|
1254
1210
|
describe("ChainSubmit permission request", () => {
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
captured.push(request);
|
|
1260
|
-
return fakeResult(undefined);
|
|
1261
|
-
},
|
|
1262
|
-
};
|
|
1263
|
-
const mockProvider = createMockProvider({
|
|
1264
|
-
accounts: [{ publicKey: new Uint8Array(32).fill(0x01) }],
|
|
1265
|
-
});
|
|
1266
|
-
const provider = new HostProvider({
|
|
1267
|
-
maxRetries: 1,
|
|
1268
|
-
loadSdk: () => Promise.resolve(createMockSdk(mockProvider, { hostApi })),
|
|
1269
|
-
loadHostApiEnum: () => Promise.resolve(fakeHostApiEnum),
|
|
1270
|
-
});
|
|
1271
|
-
|
|
1272
|
-
await provider.connect();
|
|
1273
|
-
|
|
1274
|
-
expect(captured).toHaveLength(1);
|
|
1275
|
-
// The fake hostApiEnum returns `{ version, value }` so we can
|
|
1276
|
-
// assert on the exact wire shape that would reach
|
|
1277
|
-
// host-api's RemotePermission codec.
|
|
1278
|
-
expect(captured[0]).toEqual({
|
|
1279
|
-
version: "v1",
|
|
1280
|
-
value: { tag: "ChainSubmit", value: undefined },
|
|
1281
|
-
});
|
|
1282
|
-
});
|
|
1283
|
-
|
|
1284
|
-
test("does NOT send a TransactionSubmit tag (the bug)", async () => {
|
|
1285
|
-
const captured: unknown[] = [];
|
|
1286
|
-
const hostApi: HostApiPermissionBridge = {
|
|
1287
|
-
permission: (request) => {
|
|
1288
|
-
captured.push(request);
|
|
1289
|
-
return fakeResult(undefined);
|
|
1290
|
-
},
|
|
1291
|
-
};
|
|
1211
|
+
function providerWithPermission(
|
|
1212
|
+
requestChainSubmitPermissionFn: (permission: RemotePermission) => Promise<boolean>,
|
|
1213
|
+
extra?: Partial<HostProviderOptions>,
|
|
1214
|
+
) {
|
|
1292
1215
|
const mockProvider = createMockProvider({
|
|
1293
1216
|
accounts: [{ publicKey: new Uint8Array(32).fill(0x01) }],
|
|
1294
1217
|
});
|
|
1295
|
-
|
|
1218
|
+
return new HostProvider({
|
|
1296
1219
|
maxRetries: 1,
|
|
1297
|
-
|
|
1298
|
-
|
|
1220
|
+
loadAccountsProvider: loadProvider(mockProvider),
|
|
1221
|
+
requestChainSubmitPermissionFn,
|
|
1222
|
+
...extra,
|
|
1299
1223
|
});
|
|
1224
|
+
}
|
|
1300
1225
|
|
|
1301
|
-
|
|
1226
|
+
test("requests the ChainSubmit permission on connect", async () => {
|
|
1227
|
+
const requestFn = grantPermission();
|
|
1228
|
+
await providerWithPermission(requestFn).connect();
|
|
1302
1229
|
|
|
1303
|
-
|
|
1304
|
-
expect(
|
|
1305
|
-
});
|
|
1306
|
-
|
|
1307
|
-
test("skipped when sdk.hostApi is unavailable (older product-sdk)", async () => {
|
|
1308
|
-
const mockProvider = createMockProvider({
|
|
1309
|
-
accounts: [{ publicKey: new Uint8Array(32).fill(0x01) }],
|
|
1310
|
-
});
|
|
1311
|
-
const provider = new HostProvider({
|
|
1312
|
-
maxRetries: 1,
|
|
1313
|
-
loadSdk: () => Promise.resolve(createMockSdk(mockProvider /* no hostApi */)),
|
|
1314
|
-
loadHostApiEnum: () => Promise.resolve(fakeHostApiEnum),
|
|
1315
|
-
});
|
|
1316
|
-
|
|
1317
|
-
const result = await provider.connect();
|
|
1318
|
-
// Connect should succeed even without the hostApi bridge —
|
|
1319
|
-
// permission is best-effort.
|
|
1320
|
-
expect(result.ok).toBe(true);
|
|
1230
|
+
expect(requestFn).toHaveBeenCalledTimes(1);
|
|
1231
|
+
expect(requestFn).toHaveBeenCalledWith({ tag: "ChainSubmit", value: undefined });
|
|
1321
1232
|
});
|
|
1322
1233
|
|
|
1323
1234
|
test("skipped when requestChainSubmitPermission is false", async () => {
|
|
1324
|
-
const
|
|
1325
|
-
|
|
1326
|
-
permission: (request) => {
|
|
1327
|
-
captured.push(request);
|
|
1328
|
-
return fakeResult(undefined);
|
|
1329
|
-
},
|
|
1330
|
-
};
|
|
1331
|
-
const mockProvider = createMockProvider({
|
|
1332
|
-
accounts: [{ publicKey: new Uint8Array(32).fill(0x01) }],
|
|
1333
|
-
});
|
|
1334
|
-
const provider = new HostProvider({
|
|
1335
|
-
maxRetries: 1,
|
|
1336
|
-
loadSdk: () => Promise.resolve(createMockSdk(mockProvider, { hostApi })),
|
|
1337
|
-
loadHostApiEnum: () => Promise.resolve(fakeHostApiEnum),
|
|
1235
|
+
const requestFn = grantPermission();
|
|
1236
|
+
await providerWithPermission(requestFn, {
|
|
1338
1237
|
requestChainSubmitPermission: false,
|
|
1339
|
-
});
|
|
1340
|
-
|
|
1341
|
-
await provider.connect();
|
|
1342
|
-
expect(captured).toHaveLength(0);
|
|
1238
|
+
}).connect();
|
|
1239
|
+
expect(requestFn).not.toHaveBeenCalled();
|
|
1343
1240
|
});
|
|
1344
1241
|
|
|
1345
1242
|
test("deprecated requestTransactionSubmitPermission alias still controls the request", async () => {
|
|
1346
|
-
const
|
|
1347
|
-
|
|
1348
|
-
permission: (request) => {
|
|
1349
|
-
captured.push(request);
|
|
1350
|
-
return fakeResult(undefined);
|
|
1351
|
-
},
|
|
1352
|
-
};
|
|
1353
|
-
const mockProvider = createMockProvider({
|
|
1354
|
-
accounts: [{ publicKey: new Uint8Array(32).fill(0x01) }],
|
|
1355
|
-
});
|
|
1356
|
-
const provider = new HostProvider({
|
|
1357
|
-
maxRetries: 1,
|
|
1358
|
-
loadSdk: () => Promise.resolve(createMockSdk(mockProvider, { hostApi })),
|
|
1359
|
-
loadHostApiEnum: () => Promise.resolve(fakeHostApiEnum),
|
|
1360
|
-
// Old name; new code path should still respect it as `false`.
|
|
1243
|
+
const requestFn = grantPermission();
|
|
1244
|
+
await providerWithPermission(requestFn, {
|
|
1361
1245
|
requestTransactionSubmitPermission: false,
|
|
1362
|
-
});
|
|
1363
|
-
|
|
1364
|
-
await provider.connect();
|
|
1365
|
-
expect(captured).toHaveLength(0);
|
|
1246
|
+
}).connect();
|
|
1247
|
+
expect(requestFn).not.toHaveBeenCalled();
|
|
1366
1248
|
});
|
|
1367
1249
|
|
|
1368
|
-
test("connect succeeds even when permission
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
const
|
|
1373
|
-
permission: () => fakeResult(undefined, { tag: "PermissionDenied" }),
|
|
1374
|
-
};
|
|
1375
|
-
const mockProvider = createMockProvider({
|
|
1376
|
-
accounts: [{ publicKey: new Uint8Array(32).fill(0x01) }],
|
|
1377
|
-
});
|
|
1378
|
-
const provider = new HostProvider({
|
|
1379
|
-
maxRetries: 1,
|
|
1380
|
-
loadSdk: () => Promise.resolve(createMockSdk(mockProvider, { hostApi })),
|
|
1381
|
-
loadHostApiEnum: () => Promise.resolve(fakeHostApiEnum),
|
|
1382
|
-
});
|
|
1383
|
-
|
|
1384
|
-
const result = await provider.connect();
|
|
1250
|
+
test("connect succeeds even when the permission is denied", async () => {
|
|
1251
|
+
const requestFn = vi
|
|
1252
|
+
.fn<(permission: RemotePermission) => Promise<boolean>>()
|
|
1253
|
+
.mockResolvedValue(false);
|
|
1254
|
+
const result = await providerWithPermission(requestFn).connect();
|
|
1385
1255
|
expect(result.ok).toBe(true);
|
|
1386
1256
|
});
|
|
1387
1257
|
|
|
1388
|
-
test("connect succeeds even when the
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
const hostApi: HostApiPermissionBridge = {
|
|
1394
|
-
permission: () => fakeResult(undefined),
|
|
1395
|
-
};
|
|
1396
|
-
const mockProvider = createMockProvider({
|
|
1397
|
-
accounts: [{ publicKey: new Uint8Array(32).fill(0x01) }],
|
|
1398
|
-
});
|
|
1399
|
-
const provider = new HostProvider({
|
|
1400
|
-
maxRetries: 1,
|
|
1401
|
-
loadSdk: () => Promise.resolve(createMockSdk(mockProvider, { hostApi })),
|
|
1402
|
-
loadHostApiEnum: () => Promise.reject(new Error("codec drift")),
|
|
1403
|
-
});
|
|
1404
|
-
|
|
1405
|
-
const result = await provider.connect();
|
|
1258
|
+
test("connect succeeds even when the permission request throws", async () => {
|
|
1259
|
+
const requestFn = vi
|
|
1260
|
+
.fn<(permission: RemotePermission) => Promise<boolean>>()
|
|
1261
|
+
.mockRejectedValue(new Error("host unreachable"));
|
|
1262
|
+
const result = await providerWithPermission(requestFn).connect();
|
|
1406
1263
|
expect(result.ok).toBe(true);
|
|
1407
1264
|
});
|
|
1408
1265
|
});
|
|
@@ -1420,6 +1277,10 @@ if (import.meta.vitest) {
|
|
|
1420
1277
|
expect(formatError(undefined)).toBe("undefined");
|
|
1421
1278
|
});
|
|
1422
1279
|
|
|
1280
|
+
test("surfaces GenericError.reason when there is no tag", () => {
|
|
1281
|
+
expect(formatError({ reason: "boom" })).toContain("boom");
|
|
1282
|
+
});
|
|
1283
|
+
|
|
1423
1284
|
test("surfaces inner Error name + message under the outer tag", () => {
|
|
1424
1285
|
// Simulates the exact shape the original bug produced:
|
|
1425
1286
|
// `{ tag: "v1", value: { name: "GenericError", message: "..." } }`
|
|
@@ -1463,32 +1324,4 @@ if (import.meta.vitest) {
|
|
|
1463
1324
|
expect(formatError({ tag: "v1", value: "code-42" })).toBe("v1 (code-42)");
|
|
1464
1325
|
});
|
|
1465
1326
|
});
|
|
1466
|
-
|
|
1467
|
-
describe("RemotePermission codec interop", () => {
|
|
1468
|
-
// Smoke test that the wire payload we build (`ChainSubmit`) round-trips
|
|
1469
|
-
// through the real host-api codec. The previous bug shipped
|
|
1470
|
-
// `TransactionSubmit`, which the codec rejects — locking this in here
|
|
1471
|
-
// catches a regression at the codec layer without needing the host.
|
|
1472
|
-
test("encodes ChainSubmit payload without throwing", async () => {
|
|
1473
|
-
const { RemotePermission } = await import("@novasamatech/host-api");
|
|
1474
|
-
const payload = { tag: "ChainSubmit" as const, value: undefined };
|
|
1475
|
-
const encoded = RemotePermission.enc(payload);
|
|
1476
|
-
expect(encoded).toBeInstanceOf(Uint8Array);
|
|
1477
|
-
const decoded = RemotePermission.dec(encoded);
|
|
1478
|
-
expect(decoded.tag).toBe("ChainSubmit");
|
|
1479
|
-
});
|
|
1480
|
-
|
|
1481
|
-
test("rejects the legacy TransactionSubmit tag", async () => {
|
|
1482
|
-
const { RemotePermission } = await import("@novasamatech/host-api");
|
|
1483
|
-
// `TransactionSubmit` is not a valid variant in v1 — the codec
|
|
1484
|
-
// should refuse to encode it. This proves the codec actually
|
|
1485
|
-
// validates tags (so test 1 isn't a tautology).
|
|
1486
|
-
expect(() =>
|
|
1487
|
-
RemotePermission.enc({
|
|
1488
|
-
tag: "TransactionSubmit",
|
|
1489
|
-
value: undefined,
|
|
1490
|
-
} as never),
|
|
1491
|
-
).toThrow();
|
|
1492
|
-
});
|
|
1493
|
-
});
|
|
1494
1327
|
}
|