@parity/product-sdk-signer 0.6.3 → 0.7.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 +60 -6
- package/dist/index.js +54 -2
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/errors.ts +12 -1
- package/src/providers/host.ts +307 -16
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parity/product-sdk-signer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Signer manager for Polkadot — Host API and dev accounts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -20,13 +20,13 @@
|
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"polkadot-api": "^2.1.5",
|
|
22
22
|
"@parity/product-sdk-address": "0.1.1",
|
|
23
|
-
"@parity/product-sdk-
|
|
24
|
-
"@parity/product-sdk-
|
|
23
|
+
"@parity/product-sdk-host": "0.10.0",
|
|
24
|
+
"@parity/product-sdk-keys": "0.3.8",
|
|
25
25
|
"@parity/product-sdk-logger": "0.1.1"
|
|
26
26
|
},
|
|
27
27
|
"optionalDependencies": {
|
|
28
|
-
"@novasamatech/host-api-wrapper": "^0.8.
|
|
29
|
-
"@novasamatech/host-api": "^0.8.
|
|
28
|
+
"@novasamatech/host-api-wrapper": "^0.8.7",
|
|
29
|
+
"@novasamatech/host-api": "^0.8.7"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"tsup": "^8.5.1",
|
package/src/errors.ts
CHANGED
|
@@ -10,7 +10,18 @@ export class SignerError extends Error {
|
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* The Host API is not available.
|
|
15
|
+
*
|
|
16
|
+
* Common causes:
|
|
17
|
+
* - The app is loaded outside a Polkadot host container (a regular browser tab
|
|
18
|
+
* under `npm run dev`, no iframe, no WebView). This is the dominant case
|
|
19
|
+
* during local development.
|
|
20
|
+
* - The optional `@novasamatech/host-api(-wrapper)` peer is not installed.
|
|
21
|
+
*
|
|
22
|
+
* Branch with `instanceof HostUnavailableError` to surface a "open this app
|
|
23
|
+
* in a Polkadot host, or pick a dev provider" message to the user.
|
|
24
|
+
*/
|
|
14
25
|
export class HostUnavailableError extends SignerError {
|
|
15
26
|
constructor(message = "Host API is not available") {
|
|
16
27
|
super(message);
|
package/src/providers/host.ts
CHANGED
|
@@ -55,8 +55,6 @@ export interface HostProviderOptions {
|
|
|
55
55
|
* `dotNsIdentifier`, skipping the legacy fetch entirely. For apps
|
|
56
56
|
* that sign exclusively with a per-dapp derived account.
|
|
57
57
|
*
|
|
58
|
-
* `SignerAccount.name` is populated best-effort from
|
|
59
|
-
* `accounts.getUserId().primaryUsername`; failures leave it null.
|
|
60
58
|
* Signing is pinned to `createTransaction` (see PR #96).
|
|
61
59
|
*/
|
|
62
60
|
productAccount?: {
|
|
@@ -64,6 +62,20 @@ export interface HostProviderOptions {
|
|
|
64
62
|
dotNsIdentifier: string;
|
|
65
63
|
/** Derivation index within the app scope. Default: 0. */
|
|
66
64
|
derivationIndex?: number;
|
|
65
|
+
/**
|
|
66
|
+
* Populate `SignerAccount.name` best-effort from
|
|
67
|
+
* `accounts.getUserId().primaryUsername`.
|
|
68
|
+
*
|
|
69
|
+
* On by default. Set to `false` to skip the fetch: `getUserId`
|
|
70
|
+
* triggers a host identity-permission prompt, so apps that don't
|
|
71
|
+
* render the user's name (those with their own display chain, e.g.
|
|
72
|
+
* registry username → fallback) can opt out and avoid the prompt.
|
|
73
|
+
* When enabled and the fetch fails (NotConnected, PermissionDenied,
|
|
74
|
+
* codec drift) the name stays null and connect still succeeds. The
|
|
75
|
+
* name can also be fetched later on demand via
|
|
76
|
+
* {@link HostProvider.getUserId}. Default: `true`.
|
|
77
|
+
*/
|
|
78
|
+
requestName?: boolean;
|
|
67
79
|
};
|
|
68
80
|
}
|
|
69
81
|
|
|
@@ -181,6 +193,15 @@ export interface ProductSdkModule {
|
|
|
181
193
|
createAccountsProvider: () => AccountsProvider;
|
|
182
194
|
/** Present from product-sdk ≥ 0.6; used to request TransactionSubmit. */
|
|
183
195
|
hostApi?: HostApiPermissionBridge;
|
|
196
|
+
/**
|
|
197
|
+
* `sandboxTransport.isCorrectEnvironment()` returns `false` when the app
|
|
198
|
+
* is loaded outside a Polkadot host container (e.g. a regular browser
|
|
199
|
+
* tab). Calling `getLegacyAccounts()` / `getProductAccount()` in that
|
|
200
|
+
* state surfaces the upstream `Environment is not correct` exception,
|
|
201
|
+
* so we pre-check during `connect()` and raise a specific
|
|
202
|
+
* {@link HostUnavailableError} with actionable guidance instead.
|
|
203
|
+
*/
|
|
204
|
+
sandboxTransport?: { isCorrectEnvironment(): boolean };
|
|
184
205
|
}
|
|
185
206
|
|
|
186
207
|
/* @integration */
|
|
@@ -196,9 +217,13 @@ async function defaultLoadHostApiEnum(): Promise<HostApiEnumHelper> {
|
|
|
196
217
|
/**
|
|
197
218
|
* Provider for the Host API (Polkadot Desktop / Android).
|
|
198
219
|
*
|
|
199
|
-
* Dynamically imports `@novasamatech/host-api-wrapper` at runtime
|
|
200
|
-
*
|
|
201
|
-
*
|
|
220
|
+
* Dynamically imports `@novasamatech/host-api-wrapper` at runtime. Apps running
|
|
221
|
+
* outside a host container — e.g. a plain browser tab during `npm run dev` —
|
|
222
|
+
* resolve to {@link HostUnavailableError} with guidance on what to do (open
|
|
223
|
+
* the app inside a Polkadot host or pick a non-host provider). The check uses
|
|
224
|
+
* the wrapper's `sandboxTransport.isCorrectEnvironment()` predicate and runs
|
|
225
|
+
* before any host RPC call, so the user never sees the upstream
|
|
226
|
+
* `Environment is not correct` exception leaking through.
|
|
202
227
|
*
|
|
203
228
|
* Supports both non-product accounts (user's external wallets) and product
|
|
204
229
|
* accounts (app-scoped derived accounts managed by the host).
|
|
@@ -395,6 +420,43 @@ export class HostProvider implements SignerProvider {
|
|
|
395
420
|
}
|
|
396
421
|
}
|
|
397
422
|
|
|
423
|
+
/**
|
|
424
|
+
* Fetch the connected user's primary username from the host.
|
|
425
|
+
*
|
|
426
|
+
* Use this to retrieve the name lazily — e.g. on a profile screen that
|
|
427
|
+
* actually displays it — when `connect()` ran without
|
|
428
|
+
* `productAccount.requestName` (the default) and so never fetched it.
|
|
429
|
+
* Like the connect-time fetch this triggers a host identity-permission
|
|
430
|
+
* prompt; unlike it, the result is returned as a structured `Result` so
|
|
431
|
+
* callers can react to a `PermissionDenied` / `NotConnected` rejection
|
|
432
|
+
* explicitly instead of silently falling back to a nameless account.
|
|
433
|
+
*
|
|
434
|
+
* Requires a prior successful `connect()` call.
|
|
435
|
+
*/
|
|
436
|
+
async getUserId(): Promise<Result<{ primaryUsername: string }, SignerError>> {
|
|
437
|
+
if (!this.accountsProvider) {
|
|
438
|
+
return err(new HostUnavailableError("Host provider is not connected"));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
try {
|
|
442
|
+
const result = (await this.accountsProvider.getUserId().match(
|
|
443
|
+
(value) => value,
|
|
444
|
+
(error) => {
|
|
445
|
+
throw new Error(`Host rejected user id request: ${formatError(error)}`);
|
|
446
|
+
},
|
|
447
|
+
)) as { primaryUsername: string };
|
|
448
|
+
|
|
449
|
+
return ok(result);
|
|
450
|
+
} catch (cause) {
|
|
451
|
+
log.error("failed to get user id", { cause });
|
|
452
|
+
return err(
|
|
453
|
+
new HostRejectedError(
|
|
454
|
+
cause instanceof Error ? cause.message : "Failed to get user id",
|
|
455
|
+
),
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
398
460
|
/**
|
|
399
461
|
* Create a Ring VRF proof for anonymous operations.
|
|
400
462
|
*
|
|
@@ -454,11 +516,41 @@ export class HostProvider implements SignerProvider {
|
|
|
454
516
|
);
|
|
455
517
|
}
|
|
456
518
|
|
|
457
|
-
// Step 2:
|
|
519
|
+
// Step 2: Verify we're actually running inside a host container.
|
|
520
|
+
//
|
|
521
|
+
// The upstream `host-api` transport throws `Error('Environment is not
|
|
522
|
+
// correct')` from inside `getLegacyAccounts()` / `getProductAccount()`
|
|
523
|
+
// when `sandboxTransport.isCorrectEnvironment()` returns false (i.e.
|
|
524
|
+
// we're not in an iframe under Polkadot Desktop, or a WebView under
|
|
525
|
+
// Polkadot Mobile). Without this pre-check, that exception used to
|
|
526
|
+
// surface as `HostRejectedError("Host rejected account request:
|
|
527
|
+
// Environment is not correct")` — misleading because no host rejected
|
|
528
|
+
// anything; there's no host at all.
|
|
529
|
+
//
|
|
530
|
+
// Returning `HostUnavailableError` here matches the TSDoc contract
|
|
531
|
+
// ("Apps running outside a host container will gracefully get a
|
|
532
|
+
// HOST_UNAVAILABLE error") and gives consumers actionable guidance.
|
|
533
|
+
//
|
|
534
|
+
// The `sandboxTransport` field is optional in `ProductSdkModule` so
|
|
535
|
+
// older wrapper versions (or test mocks that don't supply it) keep
|
|
536
|
+
// working — we fall through to the existing flow and rely on the
|
|
537
|
+
// catch in Step 4 as a safety net.
|
|
538
|
+
if (sdk.sandboxTransport && !sdk.sandboxTransport.isCorrectEnvironment()) {
|
|
539
|
+
log.warn("not inside a host container — Host API unavailable");
|
|
540
|
+
return err(
|
|
541
|
+
new HostUnavailableError(
|
|
542
|
+
"Host API is not available: not running inside a Polkadot host container. " +
|
|
543
|
+
"Open this app inside Polkadot Desktop or the Polkadot Mobile WebView, " +
|
|
544
|
+
"or pick a non-host signer provider (e.g. dev accounts).",
|
|
545
|
+
),
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Step 3: Create accounts provider
|
|
458
550
|
const provider = sdk.createAccountsProvider();
|
|
459
551
|
this.accountsProvider = provider;
|
|
460
552
|
|
|
461
|
-
// Step
|
|
553
|
+
// Step 4: Fetch accounts.
|
|
462
554
|
//
|
|
463
555
|
// When `productAccount` is configured, skip the legacy fetch entirely
|
|
464
556
|
// and return a single product account. Product-account-only apps
|
|
@@ -471,6 +563,7 @@ export class HostProvider implements SignerProvider {
|
|
|
471
563
|
provider,
|
|
472
564
|
this.productAccount.dotNsIdentifier,
|
|
473
565
|
this.productAccount.derivationIndex ?? 0,
|
|
566
|
+
this.productAccount.requestName ?? true,
|
|
474
567
|
);
|
|
475
568
|
if (!accountResult.ok) return accountResult;
|
|
476
569
|
signerAccounts = [accountResult.value];
|
|
@@ -484,6 +577,24 @@ export class HostProvider implements SignerProvider {
|
|
|
484
577
|
},
|
|
485
578
|
)) as RawAccount[];
|
|
486
579
|
} catch (cause) {
|
|
580
|
+
// Safety net: upstream `host-api/transport.js` throws
|
|
581
|
+
// `Error('Environment is not correct')` synchronously inside
|
|
582
|
+
// `getLegacyAccounts()` when the env check fails. The Step 2
|
|
583
|
+
// pre-check catches this normally, but we also re-classify
|
|
584
|
+
// here for older wrappers that don't expose `sandboxTransport`
|
|
585
|
+
// and for races where the env flips after the pre-check.
|
|
586
|
+
// Without this, the user sees a misleading "Host rejected
|
|
587
|
+
// account request:" prefix for an error nothing rejected.
|
|
588
|
+
if (cause instanceof Error && /environment is not correct/i.test(cause.message)) {
|
|
589
|
+
log.warn("not inside a host container (detected at getLegacyAccounts)");
|
|
590
|
+
return err(
|
|
591
|
+
new HostUnavailableError(
|
|
592
|
+
"Host API is not available: not running inside a Polkadot host container. " +
|
|
593
|
+
"Open this app inside Polkadot Desktop or the Polkadot Mobile WebView, " +
|
|
594
|
+
"or pick a non-host signer provider (e.g. dev accounts).",
|
|
595
|
+
),
|
|
596
|
+
);
|
|
597
|
+
}
|
|
487
598
|
log.error("failed to get accounts from host", { cause });
|
|
488
599
|
return err(
|
|
489
600
|
new HostRejectedError(
|
|
@@ -500,7 +611,7 @@ export class HostProvider implements SignerProvider {
|
|
|
500
611
|
signerAccounts = this.mapAccounts(rawAccounts);
|
|
501
612
|
}
|
|
502
613
|
|
|
503
|
-
// Step
|
|
614
|
+
// Step 5: Request ChainSubmit permission up-front.
|
|
504
615
|
//
|
|
505
616
|
// The host gates signing on this permission — without it, the
|
|
506
617
|
// production host rejects every sign request with `PermissionDenied`
|
|
@@ -561,14 +672,19 @@ export class HostProvider implements SignerProvider {
|
|
|
561
672
|
provider: AccountsProvider,
|
|
562
673
|
dotNsIdentifier: string,
|
|
563
674
|
derivationIndex: number,
|
|
675
|
+
requestName: boolean,
|
|
564
676
|
): Promise<Result<SignerAccount, SignerError>> {
|
|
565
|
-
//
|
|
566
|
-
//
|
|
567
|
-
//
|
|
568
|
-
//
|
|
569
|
-
//
|
|
570
|
-
//
|
|
677
|
+
// The name fetch is on by default; `requestName: false` opts out.
|
|
678
|
+
// `getUserId` triggers a host identity-permission prompt, so apps
|
|
679
|
+
// that don't render the user's name can skip it. When enabled it
|
|
680
|
+
// runs in parallel with the account fetch — they're independent host
|
|
681
|
+
// RPCs — and its failures (NotConnected, PermissionDenied, codec
|
|
682
|
+
// drift) resolve to `null` so they never abort connect; the account
|
|
683
|
+
// name then falls back to whatever `getProductAccount` returned
|
|
684
|
+
// (typically also null, since product accounts are nameless on the
|
|
685
|
+
// host side).
|
|
571
686
|
const fetchUsername = async (): Promise<string | null> => {
|
|
687
|
+
if (!requestName) return null;
|
|
572
688
|
try {
|
|
573
689
|
return await provider.getUserId().match(
|
|
574
690
|
(result) => result.primaryUsername,
|
|
@@ -747,11 +863,21 @@ if (import.meta.vitest) {
|
|
|
747
863
|
mockProvider: ReturnType<typeof createMockProvider>,
|
|
748
864
|
opts?: {
|
|
749
865
|
hostApi?: HostApiPermissionBridge;
|
|
866
|
+
/**
|
|
867
|
+
* When provided, the mock's `sandboxTransport.isCorrectEnvironment()`
|
|
868
|
+
* returns this value — exercises the env-check branch added in the
|
|
869
|
+
* `connect()` flow. Omit to skip the check entirely (older-wrapper
|
|
870
|
+
* compatibility path).
|
|
871
|
+
*/
|
|
872
|
+
isCorrectEnvironment?: boolean;
|
|
750
873
|
},
|
|
751
874
|
): ProductSdkModule {
|
|
752
875
|
return {
|
|
753
876
|
createAccountsProvider: () => mockProvider as unknown as AccountsProvider,
|
|
754
877
|
...(opts?.hostApi ? { hostApi: opts.hostApi } : {}),
|
|
878
|
+
...(opts?.isCorrectEnvironment !== undefined
|
|
879
|
+
? { sandboxTransport: { isCorrectEnvironment: () => opts.isCorrectEnvironment! } }
|
|
880
|
+
: {}),
|
|
755
881
|
};
|
|
756
882
|
}
|
|
757
883
|
|
|
@@ -791,6 +917,89 @@ if (import.meta.vitest) {
|
|
|
791
917
|
}
|
|
792
918
|
});
|
|
793
919
|
|
|
920
|
+
test("returns HOST_UNAVAILABLE with actionable guidance when not inside a host container", async () => {
|
|
921
|
+
// Repro for playground-cli#4: `pg mod foo` + `npm run dev` opens
|
|
922
|
+
// localhost in a plain browser tab (no iframe, no WebView).
|
|
923
|
+
// sandboxTransport.isCorrectEnvironment() returns false, and
|
|
924
|
+
// pre-fix we surfaced the upstream "Environment is not correct"
|
|
925
|
+
// as `HostRejectedError("Host rejected account request: ...")`.
|
|
926
|
+
// Post-fix: we pre-check during connect() and return a specific
|
|
927
|
+
// HostUnavailableError naming the host container and pointing
|
|
928
|
+
// the user at the fix path.
|
|
929
|
+
const mockProvider = createMockProvider({ accounts: [] });
|
|
930
|
+
const provider = new HostProvider({
|
|
931
|
+
maxRetries: 1,
|
|
932
|
+
loadSdk: () =>
|
|
933
|
+
Promise.resolve(createMockSdk(mockProvider, { isCorrectEnvironment: false })),
|
|
934
|
+
});
|
|
935
|
+
const result = await provider.connect();
|
|
936
|
+
|
|
937
|
+
expect(result.ok).toBe(false);
|
|
938
|
+
if (!result.ok) {
|
|
939
|
+
expect(result.error).toBeInstanceOf(HostUnavailableError);
|
|
940
|
+
expect(result.error.message).toMatch(
|
|
941
|
+
/not running inside a Polkadot host container/i,
|
|
942
|
+
);
|
|
943
|
+
expect(result.error.message).toMatch(/Polkadot Desktop|Polkadot Mobile/i);
|
|
944
|
+
}
|
|
945
|
+
// We never reached `getLegacyAccounts()` — proves the env check
|
|
946
|
+
// short-circuits before any RPC call, so users in a dev browser
|
|
947
|
+
// never see the upstream exception text leak through.
|
|
948
|
+
expect(mockProvider.getLegacyAccounts).not.toHaveBeenCalled();
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
test("safety net: re-classifies upstream 'Environment is not correct' as HOST_UNAVAILABLE", async () => {
|
|
952
|
+
// For older wrappers (or test mocks) that don't supply
|
|
953
|
+
// `sandboxTransport`, the Step 2 pre-check is skipped and the
|
|
954
|
+
// upstream throw surfaces at `getLegacyAccounts()`. The catch
|
|
955
|
+
// in Step 4 must re-classify it rather than wrapping with the
|
|
956
|
+
// misleading "Host rejected account request:" prefix.
|
|
957
|
+
const mockProvider = createMockProvider({
|
|
958
|
+
shouldReject: true,
|
|
959
|
+
error: "Environment is not correct",
|
|
960
|
+
});
|
|
961
|
+
const provider = new HostProvider({
|
|
962
|
+
maxRetries: 1,
|
|
963
|
+
// sandboxTransport intentionally omitted — exercises the
|
|
964
|
+
// safety-net path, not the pre-check path.
|
|
965
|
+
loadSdk: () => Promise.resolve(createMockSdk(mockProvider)),
|
|
966
|
+
});
|
|
967
|
+
const result = await provider.connect();
|
|
968
|
+
|
|
969
|
+
expect(result.ok).toBe(false);
|
|
970
|
+
if (!result.ok) {
|
|
971
|
+
expect(result.error).toBeInstanceOf(HostUnavailableError);
|
|
972
|
+
// Must NOT contain the misleading "Host rejected account request:" prefix.
|
|
973
|
+
expect(result.error.message).not.toMatch(/Host rejected/i);
|
|
974
|
+
expect(result.error.message).toMatch(
|
|
975
|
+
/not running inside a Polkadot host container/i,
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
test("connect proceeds when sandboxTransport reports a correct environment", async () => {
|
|
981
|
+
// Mirror of the existing happy path, but with an explicit
|
|
982
|
+
// `isCorrectEnvironment: true` to prove the pre-check doesn't
|
|
983
|
+
// false-fail when the env IS correct.
|
|
984
|
+
const rawAccounts: RawAccountTest[] = [
|
|
985
|
+
{ publicKey: new Uint8Array(32).fill(0x42), name: "Alice" },
|
|
986
|
+
];
|
|
987
|
+
const mockProvider = createMockProvider({ accounts: rawAccounts });
|
|
988
|
+
const provider = new HostProvider({
|
|
989
|
+
maxRetries: 1,
|
|
990
|
+
loadSdk: () =>
|
|
991
|
+
Promise.resolve(createMockSdk(mockProvider, { isCorrectEnvironment: true })),
|
|
992
|
+
});
|
|
993
|
+
const result = await provider.connect();
|
|
994
|
+
|
|
995
|
+
expect(result.ok).toBe(true);
|
|
996
|
+
if (result.ok) {
|
|
997
|
+
expect(result.value).toHaveLength(1);
|
|
998
|
+
expect(result.value[0].address).toMatch(/^5/);
|
|
999
|
+
}
|
|
1000
|
+
expect(mockProvider.getLegacyAccounts).toHaveBeenCalled();
|
|
1001
|
+
});
|
|
1002
|
+
|
|
794
1003
|
test("returns HOST_REJECTED when getLegacyAccounts fails", async () => {
|
|
795
1004
|
const mockProvider = createMockProvider({ shouldReject: true, error: "Rejected" });
|
|
796
1005
|
const provider = new HostProvider({
|
|
@@ -899,7 +1108,7 @@ if (import.meta.vitest) {
|
|
|
899
1108
|
unsub();
|
|
900
1109
|
});
|
|
901
1110
|
|
|
902
|
-
test("productAccount
|
|
1111
|
+
test("productAccount populates name via getUserId by default and skips the legacy fetch", async () => {
|
|
903
1112
|
const productPubkey = new Uint8Array(32).fill(0xcc);
|
|
904
1113
|
const mockProvider = createMockProvider({
|
|
905
1114
|
accounts: [{ publicKey: productPubkey, name: undefined }],
|
|
@@ -932,7 +1141,33 @@ if (import.meta.vitest) {
|
|
|
932
1141
|
expect(mockProvider.getLegacyAccounts).not.toHaveBeenCalled();
|
|
933
1142
|
});
|
|
934
1143
|
|
|
935
|
-
test("productAccount
|
|
1144
|
+
test("productAccount with requestName:false skips getUserId (no identity prompt) and leaves name null", async () => {
|
|
1145
|
+
// Opt-out: `getUserId` triggers a host identity-permission prompt,
|
|
1146
|
+
// so apps that don't render the name set `requestName: false`.
|
|
1147
|
+
const productPubkey = new Uint8Array(32).fill(0xab);
|
|
1148
|
+
const mockProvider = createMockProvider({
|
|
1149
|
+
accounts: [{ publicKey: productPubkey, name: undefined }],
|
|
1150
|
+
primaryUsername: "alice",
|
|
1151
|
+
});
|
|
1152
|
+
const provider = new HostProvider({
|
|
1153
|
+
maxRetries: 1,
|
|
1154
|
+
loadSdk: () => Promise.resolve(createMockSdk(mockProvider)),
|
|
1155
|
+
productAccount: { dotNsIdentifier: "myapp.dot", requestName: false },
|
|
1156
|
+
});
|
|
1157
|
+
const result = await provider.connect();
|
|
1158
|
+
|
|
1159
|
+
expect(result.ok).toBe(true);
|
|
1160
|
+
if (result.ok) {
|
|
1161
|
+
expect(result.value).toHaveLength(1);
|
|
1162
|
+
expect(result.value[0].publicKey).toEqual(productPubkey);
|
|
1163
|
+
expect(result.value[0].name).toBeNull();
|
|
1164
|
+
}
|
|
1165
|
+
expect(mockProvider.getProductAccount).toHaveBeenCalledWith("myapp.dot", 0);
|
|
1166
|
+
expect(mockProvider.getUserId).not.toHaveBeenCalled();
|
|
1167
|
+
expect(mockProvider.getLegacyAccounts).not.toHaveBeenCalled();
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
test("productAccount survives getUserId failure (name stays null, connect still succeeds)", async () => {
|
|
936
1171
|
const productPubkey = new Uint8Array(32).fill(0xee);
|
|
937
1172
|
const mockProvider = createMockProvider({
|
|
938
1173
|
accounts: [{ publicKey: productPubkey, name: undefined }],
|
|
@@ -957,6 +1192,62 @@ if (import.meta.vitest) {
|
|
|
957
1192
|
}
|
|
958
1193
|
});
|
|
959
1194
|
|
|
1195
|
+
test("getUserId() retrieves the primary username after a connect that opted out of the name fetch", async () => {
|
|
1196
|
+
// The escape hatch for `requestName: false`: connect without the
|
|
1197
|
+
// prompt (name=null), then fetch the name lazily later — e.g. when
|
|
1198
|
+
// a profile screen needs to display it.
|
|
1199
|
+
const productPubkey = new Uint8Array(32).fill(0xa1);
|
|
1200
|
+
const mockProvider = createMockProvider({
|
|
1201
|
+
accounts: [{ publicKey: productPubkey, name: undefined }],
|
|
1202
|
+
primaryUsername: "alice",
|
|
1203
|
+
});
|
|
1204
|
+
const provider = new HostProvider({
|
|
1205
|
+
maxRetries: 1,
|
|
1206
|
+
loadSdk: () => Promise.resolve(createMockSdk(mockProvider)),
|
|
1207
|
+
productAccount: { dotNsIdentifier: "myapp.dot", requestName: false },
|
|
1208
|
+
});
|
|
1209
|
+
const connectResult = await provider.connect();
|
|
1210
|
+
expect(connectResult.ok).toBe(true);
|
|
1211
|
+
if (connectResult.ok) expect(connectResult.value[0].name).toBeNull();
|
|
1212
|
+
// Not fetched during connect...
|
|
1213
|
+
expect(mockProvider.getUserId).not.toHaveBeenCalled();
|
|
1214
|
+
|
|
1215
|
+
// ...but reachable on demand afterwards.
|
|
1216
|
+
const userId = await provider.getUserId();
|
|
1217
|
+
expect(userId.ok).toBe(true);
|
|
1218
|
+
if (userId.ok) expect(userId.value.primaryUsername).toBe("alice");
|
|
1219
|
+
expect(mockProvider.getUserId).toHaveBeenCalledTimes(1);
|
|
1220
|
+
});
|
|
1221
|
+
|
|
1222
|
+
test("getUserId() returns HostUnavailableError before connect", async () => {
|
|
1223
|
+
const provider = new HostProvider({ maxRetries: 1 });
|
|
1224
|
+
const result = await provider.getUserId();
|
|
1225
|
+
expect(result.ok).toBe(false);
|
|
1226
|
+
if (!result.ok) expect(result.error).toBeInstanceOf(HostUnavailableError);
|
|
1227
|
+
});
|
|
1228
|
+
|
|
1229
|
+
test("getUserId() surfaces a host rejection as HostRejectedError", async () => {
|
|
1230
|
+
const mockProvider = createMockProvider({
|
|
1231
|
+
accounts: [{ publicKey: new Uint8Array(32).fill(0xa2), name: undefined }],
|
|
1232
|
+
});
|
|
1233
|
+
const provider = new HostProvider({
|
|
1234
|
+
maxRetries: 1,
|
|
1235
|
+
loadSdk: () => Promise.resolve(createMockSdk(mockProvider)),
|
|
1236
|
+
productAccount: { dotNsIdentifier: "myapp.dot", requestName: false },
|
|
1237
|
+
});
|
|
1238
|
+
await provider.connect();
|
|
1239
|
+
// After connect, force the host to reject the on-demand fetch.
|
|
1240
|
+
mockProvider.getUserId.mockReturnValue({
|
|
1241
|
+
match: async (
|
|
1242
|
+
_onOk: (v: { primaryUsername: string }) => unknown,
|
|
1243
|
+
onErr: (e: unknown) => unknown,
|
|
1244
|
+
) => onErr({ tag: "v1", value: { tag: "GetUserIdErr::PermissionDenied" } }),
|
|
1245
|
+
});
|
|
1246
|
+
const result = await provider.getUserId();
|
|
1247
|
+
expect(result.ok).toBe(false);
|
|
1248
|
+
if (!result.ok) expect(result.error).toBeInstanceOf(HostRejectedError);
|
|
1249
|
+
});
|
|
1250
|
+
|
|
960
1251
|
test("productAccount option succeeds when host has no legacy accounts (regression: signer 0.5.0 NoAccountsError)", async () => {
|
|
961
1252
|
// Without the option, this scenario returned `err(NoAccountsError)`
|
|
962
1253
|
// before any product-account fetch could happen — breaking every
|