@parity/product-sdk-signer 0.7.0 → 0.8.1
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 +28 -2
- package/dist/index.js +44 -51
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
- package/src/providers/host.ts +108 -135
- package/src/signer-manager.ts +87 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parity/product-sdk-signer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
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-
|
|
25
|
-
"@parity/product-sdk-
|
|
23
|
+
"@parity/product-sdk-keys": "0.3.10",
|
|
24
|
+
"@parity/product-sdk-logger": "0.1.1",
|
|
25
|
+
"@parity/product-sdk-host": "0.10.2"
|
|
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.9",
|
|
29
|
+
"@novasamatech/host-api": "^0.8.9"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"tsup": "^8.5.1",
|
package/src/providers/host.ts
CHANGED
|
@@ -3,12 +3,7 @@
|
|
|
3
3
|
import { deriveH160, ss58Encode } from "@parity/product-sdk-address";
|
|
4
4
|
import { createLogger } from "@parity/product-sdk-logger";
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
HostRejectedError,
|
|
8
|
-
HostUnavailableError,
|
|
9
|
-
NoAccountsError,
|
|
10
|
-
type SignerError,
|
|
11
|
-
} from "../errors.js";
|
|
6
|
+
import { HostRejectedError, HostUnavailableError, type SignerError } from "../errors.js";
|
|
12
7
|
import { withRetry } from "../retry.js";
|
|
13
8
|
import type { ConnectionStatus, ProviderType, Result, SignerAccount } from "../types.js";
|
|
14
9
|
import { err, ok } from "../types.js";
|
|
@@ -24,6 +19,22 @@ export interface HostProviderOptions {
|
|
|
24
19
|
maxRetries?: number;
|
|
25
20
|
/** Initial retry delay in ms. Default: 500 */
|
|
26
21
|
retryDelay?: number;
|
|
22
|
+
/**
|
|
23
|
+
* Dapp identifier the SDK falls back to when {@link productAccount} is
|
|
24
|
+
* not set, so `connect()` can still surface a usable account on hosts
|
|
25
|
+
* that don't enumerate legacy accounts.
|
|
26
|
+
*
|
|
27
|
+
* The value is treated as a dotNS identifier (`.dot` is appended if
|
|
28
|
+
* missing) and routed through `getProductAccount(dappName, 0)`. If the
|
|
29
|
+
* host rejects the derivation (e.g. the identifier isn't registered),
|
|
30
|
+
* `connect()` resolves with an empty accounts list rather than
|
|
31
|
+
* throwing — consumers can still drive the explicit signing paths
|
|
32
|
+
* (`signMessageWithDotNsIdentity`, `getLegacyAccountSigner`).
|
|
33
|
+
*
|
|
34
|
+
* Wired through from `SignerManager` automatically; only set directly
|
|
35
|
+
* when instantiating `HostProvider` outside the manager.
|
|
36
|
+
*/
|
|
37
|
+
dappName?: string;
|
|
27
38
|
/**
|
|
28
39
|
* Custom SDK loader. Defaults to `import("@novasamatech/host-api-wrapper")`.
|
|
29
40
|
* Override this for testing or custom SDK setups.
|
|
@@ -147,7 +158,6 @@ const PRODUCT_SIGNER_TYPE = "createTransaction" as const;
|
|
|
147
158
|
|
|
148
159
|
/** @internal */
|
|
149
160
|
export interface AccountsProvider {
|
|
150
|
-
getLegacyAccounts: () => NeverthrowResultAsync<RawAccount[], unknown>;
|
|
151
161
|
getLegacyAccountSigner: (account: ProductAccount) => import("polkadot-api").PolkadotSigner;
|
|
152
162
|
getProductAccount: (
|
|
153
163
|
dotNsIdentifier: string,
|
|
@@ -237,6 +247,7 @@ export class HostProvider implements SignerProvider {
|
|
|
237
247
|
private readonly loadHostApiEnum: () => Promise<HostApiEnumHelper>;
|
|
238
248
|
private readonly requestChainSubmitPermission: boolean;
|
|
239
249
|
private readonly productAccount: HostProviderOptions["productAccount"];
|
|
250
|
+
private readonly dappName: string | undefined;
|
|
240
251
|
|
|
241
252
|
private accountsProvider: AccountsProvider | null = null;
|
|
242
253
|
private statusCleanup: (() => void) | null = null;
|
|
@@ -255,6 +266,7 @@ export class HostProvider implements SignerProvider {
|
|
|
255
266
|
options?.requestTransactionSubmitPermission ??
|
|
256
267
|
true;
|
|
257
268
|
this.productAccount = options?.productAccount;
|
|
269
|
+
this.dappName = options?.dappName;
|
|
258
270
|
}
|
|
259
271
|
|
|
260
272
|
async connect(signal?: AbortSignal): Promise<Result<SignerAccount[], SignerError>> {
|
|
@@ -552,11 +564,23 @@ export class HostProvider implements SignerProvider {
|
|
|
552
564
|
|
|
553
565
|
// Step 4: Fetch accounts.
|
|
554
566
|
//
|
|
555
|
-
//
|
|
556
|
-
//
|
|
557
|
-
//
|
|
558
|
-
//
|
|
559
|
-
//
|
|
567
|
+
// Both branches end in `fetchProductSignerAccount`. The difference
|
|
568
|
+
// is only where the dotNS identifier comes from:
|
|
569
|
+
// - explicit `productAccount` option (caller-supplied), OR
|
|
570
|
+
// - implicit derivation from `dappName` (the SDK-managed default
|
|
571
|
+
// for hosts that don't enumerate accounts, e.g. Polkadot Desktop).
|
|
572
|
+
//
|
|
573
|
+
// On hosts that don't enumerate accounts (PoP / product-account
|
|
574
|
+
// hosts), `getLegacyAccounts()` returns `[]` by design — the host
|
|
575
|
+
// exposes only per-dapp product accounts and never the user's
|
|
576
|
+
// identity account. The implicit derivation path matches that
|
|
577
|
+
// contract: derive the per-dapp account using the consumer's
|
|
578
|
+
// `dappName` as the identifier, surface it on `connect()`. When
|
|
579
|
+
// the host rejects the derivation (typically because the dapp's
|
|
580
|
+
// dotNS identifier isn't registered for this user), we resolve
|
|
581
|
+
// with an empty accounts list rather than throwing so consumers
|
|
582
|
+
// can still drive the explicit-name signing paths
|
|
583
|
+
// (`signMessageWithDotNsIdentity`, `getLegacyAccountSigner`).
|
|
560
584
|
let signerAccounts: SignerAccount[];
|
|
561
585
|
if (this.productAccount) {
|
|
562
586
|
const accountResult = await this.fetchProductSignerAccount(
|
|
@@ -567,48 +591,44 @@ export class HostProvider implements SignerProvider {
|
|
|
567
591
|
);
|
|
568
592
|
if (!accountResult.ok) return accountResult;
|
|
569
593
|
signerAccounts = [accountResult.value];
|
|
570
|
-
} else {
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
594
|
+
} else if (this.dappName) {
|
|
595
|
+
// `.dot` is appended if missing so `"my-app"` and `"my-app.dot"`
|
|
596
|
+
// resolve to the same identifier on the host side.
|
|
597
|
+
const dotNsIdentifier = this.dappName.endsWith(".dot")
|
|
598
|
+
? this.dappName
|
|
599
|
+
: `${this.dappName}.dot`;
|
|
600
|
+
const accountResult = await this.fetchProductSignerAccount(
|
|
601
|
+
provider,
|
|
602
|
+
dotNsIdentifier,
|
|
603
|
+
0,
|
|
604
|
+
true,
|
|
605
|
+
);
|
|
606
|
+
if (!accountResult.ok) {
|
|
607
|
+
// Soft-degrade: host couldn't derive a product account for
|
|
608
|
+
// this dappName (most commonly because the identifier isn't
|
|
609
|
+
// registered for this user). Returning [] lets `connect()`
|
|
610
|
+
// resolve successfully — consumers handle the empty list
|
|
611
|
+
// and drive explicit signing paths.
|
|
612
|
+
log.warn(
|
|
613
|
+
"host could not derive a product account for dappName; resolving with empty accounts",
|
|
614
|
+
{
|
|
615
|
+
dotNsIdentifier,
|
|
616
|
+
error: accountResult.error.message,
|
|
577
617
|
},
|
|
578
|
-
)) as RawAccount[];
|
|
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
|
-
}
|
|
598
|
-
log.error("failed to get accounts from host", { cause });
|
|
599
|
-
return err(
|
|
600
|
-
new HostRejectedError(
|
|
601
|
-
cause instanceof Error ? cause.message : "Failed to get accounts from host",
|
|
602
|
-
),
|
|
603
618
|
);
|
|
619
|
+
signerAccounts = [];
|
|
620
|
+
} else {
|
|
621
|
+
signerAccounts = [accountResult.value];
|
|
604
622
|
}
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
623
|
+
} else {
|
|
624
|
+
// No `productAccount`, no `dappName` — caller asked the SDK to
|
|
625
|
+
// pick accounts with no hints. We can't, so resolve with an
|
|
626
|
+
// empty list. Consumers driving explicit-name signing still
|
|
627
|
+
// work; consumers expecting enumeration get a clear `[]`.
|
|
628
|
+
log.warn(
|
|
629
|
+
"no productAccount or dappName configured; resolving connect() with empty accounts",
|
|
630
|
+
);
|
|
631
|
+
signerAccounts = [];
|
|
612
632
|
}
|
|
613
633
|
|
|
614
634
|
// Step 5: Request ChainSubmit permission up-front.
|
|
@@ -708,30 +728,6 @@ export class HostProvider implements SignerProvider {
|
|
|
708
728
|
const account = accountResult.value;
|
|
709
729
|
return ok({ ...account, name: account.name ?? primaryUsername });
|
|
710
730
|
}
|
|
711
|
-
|
|
712
|
-
private mapAccounts(rawAccounts: ReadonlyArray<RawAccount>): SignerAccount[] {
|
|
713
|
-
return rawAccounts.map((raw) => {
|
|
714
|
-
const address = ss58Encode(raw.publicKey, this.ss58Prefix);
|
|
715
|
-
const h160Address = deriveH160(raw.publicKey);
|
|
716
|
-
return {
|
|
717
|
-
address,
|
|
718
|
-
h160Address,
|
|
719
|
-
publicKey: raw.publicKey,
|
|
720
|
-
name: raw.name ?? null,
|
|
721
|
-
source: "host" as const,
|
|
722
|
-
getSigner: () => {
|
|
723
|
-
if (!this.accountsProvider) {
|
|
724
|
-
throw new Error("Host provider is disconnected");
|
|
725
|
-
}
|
|
726
|
-
return this.accountsProvider.getLegacyAccountSigner({
|
|
727
|
-
dotNsIdentifier: "",
|
|
728
|
-
derivationIndex: 0,
|
|
729
|
-
publicKey: raw.publicKey,
|
|
730
|
-
});
|
|
731
|
-
},
|
|
732
|
-
};
|
|
733
|
-
});
|
|
734
|
-
}
|
|
735
731
|
}
|
|
736
732
|
|
|
737
733
|
/**
|
|
@@ -948,92 +944,73 @@ if (import.meta.vitest) {
|
|
|
948
944
|
expect(mockProvider.getLegacyAccounts).not.toHaveBeenCalled();
|
|
949
945
|
});
|
|
950
946
|
|
|
951
|
-
test("
|
|
952
|
-
//
|
|
953
|
-
//
|
|
954
|
-
//
|
|
955
|
-
//
|
|
956
|
-
|
|
947
|
+
test("connect with dappName fallback derives a product account", async () => {
|
|
948
|
+
// Without an explicit `productAccount`, the SDK derives the
|
|
949
|
+
// dotNS identifier from `dappName` (with `.dot` appended if
|
|
950
|
+
// missing). This is the path hosts that don't enumerate
|
|
951
|
+
// accounts (PoP / Polkadot Desktop) take by default.
|
|
952
|
+
const productPubkey = new Uint8Array(32).fill(0x42);
|
|
957
953
|
const mockProvider = createMockProvider({
|
|
958
|
-
|
|
959
|
-
|
|
954
|
+
accounts: [{ publicKey: productPubkey, name: undefined }],
|
|
955
|
+
primaryUsername: "alice",
|
|
960
956
|
});
|
|
961
957
|
const provider = new HostProvider({
|
|
962
958
|
maxRetries: 1,
|
|
963
|
-
|
|
964
|
-
// safety-net path, not the pre-check path.
|
|
959
|
+
dappName: "my-cli",
|
|
965
960
|
loadSdk: () => Promise.resolve(createMockSdk(mockProvider)),
|
|
966
961
|
});
|
|
967
962
|
const result = await provider.connect();
|
|
968
963
|
|
|
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
964
|
expect(result.ok).toBe(true);
|
|
996
965
|
if (result.ok) {
|
|
997
966
|
expect(result.value).toHaveLength(1);
|
|
998
|
-
expect(result.value[0].
|
|
967
|
+
expect(result.value[0].publicKey).toEqual(productPubkey);
|
|
968
|
+
expect(result.value[0].source).toBe("host");
|
|
999
969
|
}
|
|
1000
|
-
|
|
970
|
+
// `.dot` appended automatically.
|
|
971
|
+
expect(mockProvider.getProductAccount).toHaveBeenCalledWith("my-cli.dot", 0);
|
|
1001
972
|
});
|
|
1002
973
|
|
|
1003
|
-
test("
|
|
1004
|
-
const
|
|
974
|
+
test("connect with dappName already ending in .dot doesn't double-append", async () => {
|
|
975
|
+
const productPubkey = new Uint8Array(32).fill(0x77);
|
|
976
|
+
const mockProvider = createMockProvider({
|
|
977
|
+
accounts: [{ publicKey: productPubkey, name: undefined }],
|
|
978
|
+
});
|
|
1005
979
|
const provider = new HostProvider({
|
|
1006
980
|
maxRetries: 1,
|
|
981
|
+
dappName: "my-cli.dot",
|
|
1007
982
|
loadSdk: () => Promise.resolve(createMockSdk(mockProvider)),
|
|
1008
983
|
});
|
|
1009
984
|
const result = await provider.connect();
|
|
1010
985
|
|
|
1011
|
-
expect(result.ok).toBe(
|
|
1012
|
-
|
|
1013
|
-
expect(result.error).toBeInstanceOf(HostRejectedError);
|
|
1014
|
-
}
|
|
986
|
+
expect(result.ok).toBe(true);
|
|
987
|
+
expect(mockProvider.getProductAccount).toHaveBeenCalledWith("my-cli.dot", 0);
|
|
1015
988
|
});
|
|
1016
989
|
|
|
1017
|
-
test("
|
|
1018
|
-
|
|
990
|
+
test("connect with dappName fallback resolves to [] when host rejects derivation", async () => {
|
|
991
|
+
// Soft-degrade: when the host can't derive a product account
|
|
992
|
+
// for the dappName (commonly because the dotNS identifier isn't
|
|
993
|
+
// registered for this user), connect() returns ok([]) rather
|
|
994
|
+
// than throwing. Consumers handle the empty list and drive
|
|
995
|
+
// explicit signing paths.
|
|
996
|
+
const mockProvider = createMockProvider({ shouldReject: true, error: "Rejected" });
|
|
1019
997
|
const provider = new HostProvider({
|
|
1020
998
|
maxRetries: 1,
|
|
999
|
+
dappName: "not-registered",
|
|
1021
1000
|
loadSdk: () => Promise.resolve(createMockSdk(mockProvider)),
|
|
1022
1001
|
});
|
|
1023
1002
|
const result = await provider.connect();
|
|
1024
1003
|
|
|
1025
|
-
expect(result.ok).toBe(
|
|
1026
|
-
if (
|
|
1027
|
-
expect(result.
|
|
1004
|
+
expect(result.ok).toBe(true);
|
|
1005
|
+
if (result.ok) {
|
|
1006
|
+
expect(result.value).toEqual([]);
|
|
1028
1007
|
}
|
|
1029
1008
|
});
|
|
1030
1009
|
|
|
1031
|
-
test("
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
];
|
|
1036
|
-
const mockProvider = createMockProvider({ accounts: rawAccounts });
|
|
1010
|
+
test("connect resolves to [] when neither productAccount nor dappName is set", async () => {
|
|
1011
|
+
// Caller asked us to pick accounts with no hints. We can't, so
|
|
1012
|
+
// resolve with an empty list rather than throwing.
|
|
1013
|
+
const mockProvider = createMockProvider({});
|
|
1037
1014
|
const provider = new HostProvider({
|
|
1038
1015
|
maxRetries: 1,
|
|
1039
1016
|
loadSdk: () => Promise.resolve(createMockSdk(mockProvider)),
|
|
@@ -1042,11 +1019,7 @@ if (import.meta.vitest) {
|
|
|
1042
1019
|
|
|
1043
1020
|
expect(result.ok).toBe(true);
|
|
1044
1021
|
if (result.ok) {
|
|
1045
|
-
expect(result.value).
|
|
1046
|
-
expect(result.value[0].name).toBe("Alice");
|
|
1047
|
-
expect(result.value[0].source).toBe("host");
|
|
1048
|
-
expect(result.value[0].publicKey).toEqual(rawAccounts[0].publicKey);
|
|
1049
|
-
expect(result.value[1].name).toBeNull();
|
|
1022
|
+
expect(result.value).toEqual([]);
|
|
1050
1023
|
}
|
|
1051
1024
|
});
|
|
1052
1025
|
|
package/src/signer-manager.ts
CHANGED
|
@@ -337,6 +337,26 @@ export class SignerManager {
|
|
|
337
337
|
|
|
338
338
|
// ── Host-only: Product Account API ─────────────────────────────
|
|
339
339
|
|
|
340
|
+
/**
|
|
341
|
+
* Fetch the connected user's host identity.
|
|
342
|
+
*
|
|
343
|
+
* This uses the Host API identity permission path and is only available
|
|
344
|
+
* when connected through the host provider. The primary username can be
|
|
345
|
+
* used by higher-level helpers that need to bind an action to the user's
|
|
346
|
+
* DotNS / people username.
|
|
347
|
+
*/
|
|
348
|
+
async getUserId(): Promise<Result<{ primaryUsername: string }, SignerError>> {
|
|
349
|
+
if (this.isDestroyed) return err(new DestroyedError());
|
|
350
|
+
|
|
351
|
+
const host = this.getHostProvider();
|
|
352
|
+
if (!host) {
|
|
353
|
+
return err(
|
|
354
|
+
new HostUnavailableError("User identity requires a host provider connection"),
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
return host.getUserId();
|
|
358
|
+
}
|
|
359
|
+
|
|
340
360
|
/**
|
|
341
361
|
* Get an app-scoped product account from the host.
|
|
342
362
|
*
|
|
@@ -504,6 +524,7 @@ export class SignerManager {
|
|
|
504
524
|
ss58Prefix: this.ss58Prefix,
|
|
505
525
|
maxRetries: this.maxRetries,
|
|
506
526
|
retryDelay: 500,
|
|
527
|
+
dappName: this.dappName,
|
|
507
528
|
});
|
|
508
529
|
case "dev":
|
|
509
530
|
return new DevProvider({
|
|
@@ -889,4 +910,70 @@ if (import.meta.vitest) {
|
|
|
889
910
|
manager.destroy();
|
|
890
911
|
});
|
|
891
912
|
});
|
|
913
|
+
|
|
914
|
+
describe("SignerManager.getUserId", () => {
|
|
915
|
+
test("returns HostUnavailableError when not connected via host (dev provider)", async () => {
|
|
916
|
+
const manager = new SignerManager({
|
|
917
|
+
createProvider: () => mockProvider(),
|
|
918
|
+
});
|
|
919
|
+
await manager.connect("dev");
|
|
920
|
+
await flush();
|
|
921
|
+
|
|
922
|
+
// Dev provider is active, not host — getUserId() should refuse
|
|
923
|
+
// rather than attempt a host RPC.
|
|
924
|
+
const result = await manager.getUserId();
|
|
925
|
+
expect(result.ok).toBe(false);
|
|
926
|
+
if (!result.ok) {
|
|
927
|
+
expect(result.error).toBeInstanceOf(HostUnavailableError);
|
|
928
|
+
expect(result.error.message).toMatch(/host provider/i);
|
|
929
|
+
}
|
|
930
|
+
manager.destroy();
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
test("returns DestroyedError after destroy()", async () => {
|
|
934
|
+
const manager = new SignerManager({
|
|
935
|
+
createProvider: () => mockProvider(),
|
|
936
|
+
});
|
|
937
|
+
await manager.connect("dev");
|
|
938
|
+
await flush();
|
|
939
|
+
manager.destroy();
|
|
940
|
+
|
|
941
|
+
const result = await manager.getUserId();
|
|
942
|
+
expect(result.ok).toBe(false);
|
|
943
|
+
if (!result.ok) {
|
|
944
|
+
expect(result.error).toBeInstanceOf(DestroyedError);
|
|
945
|
+
}
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
test("delegates to HostProvider.getUserId when connected via host", async () => {
|
|
949
|
+
// Build a host-shaped fake provider that satisfies the SignerManager's
|
|
950
|
+
// `activeProvider === "host"` check and exposes a getUserId that
|
|
951
|
+
// resolves to a known username.
|
|
952
|
+
const fakeHostProvider = {
|
|
953
|
+
type: "host" as const,
|
|
954
|
+
connect: vi.fn().mockResolvedValue(ok([mockAccount()])),
|
|
955
|
+
disconnect: vi.fn(),
|
|
956
|
+
onStatusChange: vi.fn().mockReturnValue(() => {}),
|
|
957
|
+
onAccountsChange: vi.fn().mockReturnValue(() => {}),
|
|
958
|
+
getUserId: vi.fn().mockResolvedValue(ok({ primaryUsername: "alice.dot" })),
|
|
959
|
+
};
|
|
960
|
+
|
|
961
|
+
const manager = new SignerManager({
|
|
962
|
+
createProvider: (type) =>
|
|
963
|
+
type === "host"
|
|
964
|
+
? (fakeHostProvider as unknown as SignerProvider)
|
|
965
|
+
: mockProvider(),
|
|
966
|
+
});
|
|
967
|
+
await manager.connect("host");
|
|
968
|
+
await flush();
|
|
969
|
+
|
|
970
|
+
const result = await manager.getUserId();
|
|
971
|
+
expect(result.ok).toBe(true);
|
|
972
|
+
if (result.ok) {
|
|
973
|
+
expect(result.value.primaryUsername).toBe("alice.dot");
|
|
974
|
+
}
|
|
975
|
+
expect(fakeHostProvider.getUserId).toHaveBeenCalledTimes(1);
|
|
976
|
+
manager.destroy();
|
|
977
|
+
});
|
|
978
|
+
});
|
|
892
979
|
}
|