@polkadot-apps/signer 0.1.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/container.d.ts +11 -0
- package/dist/container.d.ts.map +1 -0
- package/dist/container.js +98 -0
- package/dist/container.js.map +1 -0
- package/dist/errors.d.ts +56 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +226 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/providers/dev.d.ts +39 -0
- package/dist/providers/dev.d.ts.map +1 -0
- package/dist/providers/dev.js +232 -0
- package/dist/providers/dev.js.map +1 -0
- package/dist/providers/extension.d.ts +46 -0
- package/dist/providers/extension.d.ts.map +1 -0
- package/dist/providers/extension.js +363 -0
- package/dist/providers/extension.js.map +1 -0
- package/dist/providers/host.d.ts +160 -0
- package/dist/providers/host.d.ts.map +1 -0
- package/dist/providers/host.js +724 -0
- package/dist/providers/host.js.map +1 -0
- package/dist/providers/types.d.ts +45 -0
- package/dist/providers/types.d.ts.map +1 -0
- package/dist/providers/types.js +2 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/retry.d.ts +23 -0
- package/dist/retry.d.ts.map +1 -0
- package/dist/retry.js +197 -0
- package/dist/retry.js.map +1 -0
- package/dist/signer-manager.d.ts +168 -0
- package/dist/signer-manager.d.ts.map +1 -0
- package/dist/signer-manager.js +1447 -0
- package/dist/signer-manager.js.map +1 -0
- package/dist/sleep.d.ts +9 -0
- package/dist/sleep.d.ts.map +1 -0
- package/dist/sleep.js +85 -0
- package/dist/sleep.js.map +1 -0
- package/dist/types.d.ts +96 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +71 -0
- package/dist/types.js.map +1 -0
- package/package.json +41 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { seedToAccount } from "@polkadot-apps/keys";
|
|
2
|
+
import { createLogger } from "@polkadot-apps/logger";
|
|
3
|
+
import { ok } from "../types.js";
|
|
4
|
+
const log = createLogger("signer:dev");
|
|
5
|
+
/** The well-known Substrate development mnemonic phrase. */
|
|
6
|
+
const DEV_PHRASE = "bottom drive obey lake curtain smoke basket hold race lonely fit walk";
|
|
7
|
+
/** Standard Substrate dev account names. */
|
|
8
|
+
const DEFAULT_DEV_NAMES = ["Alice", "Bob", "Charlie", "Dave", "Eve", "Ferdie"];
|
|
9
|
+
/**
|
|
10
|
+
* Provider for Substrate development accounts.
|
|
11
|
+
*
|
|
12
|
+
* Uses the well-known DEV_PHRASE with hard derivation paths (`//Alice`, `//Bob`, etc.)
|
|
13
|
+
* to create deterministic accounts for local development and testing.
|
|
14
|
+
*/
|
|
15
|
+
export class DevProvider {
|
|
16
|
+
type = "dev";
|
|
17
|
+
names;
|
|
18
|
+
mnemonic;
|
|
19
|
+
ss58Prefix;
|
|
20
|
+
keyType;
|
|
21
|
+
constructor(options) {
|
|
22
|
+
this.names = options?.names ?? DEFAULT_DEV_NAMES;
|
|
23
|
+
this.mnemonic = options?.mnemonic ?? DEV_PHRASE;
|
|
24
|
+
this.ss58Prefix = options?.ss58Prefix ?? 42;
|
|
25
|
+
this.keyType = options?.keyType ?? "sr25519";
|
|
26
|
+
}
|
|
27
|
+
async connect() {
|
|
28
|
+
log.debug("creating dev accounts", { names: this.names, keyType: this.keyType });
|
|
29
|
+
const accounts = this.names.map((name) => {
|
|
30
|
+
const derived = seedToAccount(this.mnemonic, `//${name}`, this.ss58Prefix, this.keyType);
|
|
31
|
+
return {
|
|
32
|
+
address: derived.ss58Address,
|
|
33
|
+
h160Address: derived.h160Address,
|
|
34
|
+
publicKey: derived.publicKey,
|
|
35
|
+
name,
|
|
36
|
+
source: "dev",
|
|
37
|
+
getSigner: () => derived.signer,
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
log.info("dev accounts ready", { count: accounts.length });
|
|
41
|
+
return ok(accounts);
|
|
42
|
+
}
|
|
43
|
+
disconnect() {
|
|
44
|
+
// Dev accounts are stateless — nothing to clean up.
|
|
45
|
+
}
|
|
46
|
+
onStatusChange(_callback) {
|
|
47
|
+
// Dev accounts are always "connected" — no status changes to emit.
|
|
48
|
+
return () => { };
|
|
49
|
+
}
|
|
50
|
+
onAccountsChange(_callback) {
|
|
51
|
+
// Dev accounts are static — no account changes to emit.
|
|
52
|
+
return () => { };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (import.meta.vitest) {
|
|
56
|
+
const { test, expect, describe } = import.meta.vitest;
|
|
57
|
+
// Well-known Alice address on generic substrate (prefix 42)
|
|
58
|
+
const ALICE_ADDRESS = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY";
|
|
59
|
+
describe("DevProvider", () => {
|
|
60
|
+
test("connect returns 6 accounts by default", async () => {
|
|
61
|
+
const provider = new DevProvider();
|
|
62
|
+
const result = await provider.connect();
|
|
63
|
+
expect(result.ok).toBe(true);
|
|
64
|
+
if (result.ok) {
|
|
65
|
+
expect(result.value).toHaveLength(6);
|
|
66
|
+
expect(result.value.map((a) => a.name)).toEqual([
|
|
67
|
+
"Alice",
|
|
68
|
+
"Bob",
|
|
69
|
+
"Charlie",
|
|
70
|
+
"Dave",
|
|
71
|
+
"Eve",
|
|
72
|
+
"Ferdie",
|
|
73
|
+
]);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
test("all accounts have source 'dev'", async () => {
|
|
77
|
+
const provider = new DevProvider();
|
|
78
|
+
const result = await provider.connect();
|
|
79
|
+
if (result.ok) {
|
|
80
|
+
for (const account of result.value) {
|
|
81
|
+
expect(account.source).toBe("dev");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
test("Alice has well-known address", async () => {
|
|
86
|
+
const provider = new DevProvider();
|
|
87
|
+
const result = await provider.connect();
|
|
88
|
+
if (result.ok) {
|
|
89
|
+
expect(result.value[0].address).toBe(ALICE_ADDRESS);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
test("addresses are deterministic", async () => {
|
|
93
|
+
const a = new DevProvider();
|
|
94
|
+
const b = new DevProvider();
|
|
95
|
+
const ra = await a.connect();
|
|
96
|
+
const rb = await b.connect();
|
|
97
|
+
if (ra.ok && rb.ok) {
|
|
98
|
+
expect(ra.value.map((x) => x.address)).toEqual(rb.value.map((x) => x.address));
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
test("each account has 32-byte publicKey", async () => {
|
|
102
|
+
const provider = new DevProvider();
|
|
103
|
+
const result = await provider.connect();
|
|
104
|
+
if (result.ok) {
|
|
105
|
+
for (const account of result.value) {
|
|
106
|
+
expect(account.publicKey).toBeInstanceOf(Uint8Array);
|
|
107
|
+
expect(account.publicKey.length).toBe(32);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
test("getSigner returns signer with matching publicKey", async () => {
|
|
112
|
+
const provider = new DevProvider();
|
|
113
|
+
const result = await provider.connect();
|
|
114
|
+
if (result.ok) {
|
|
115
|
+
for (const account of result.value) {
|
|
116
|
+
const signer = account.getSigner();
|
|
117
|
+
expect(signer.publicKey).toEqual(account.publicKey);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
test("custom names subset", async () => {
|
|
122
|
+
const provider = new DevProvider({ names: ["Alice", "Bob"] });
|
|
123
|
+
const result = await provider.connect();
|
|
124
|
+
if (result.ok) {
|
|
125
|
+
expect(result.value).toHaveLength(2);
|
|
126
|
+
expect(result.value.map((a) => a.name)).toEqual(["Alice", "Bob"]);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
test("custom mnemonic produces different addresses", async () => {
|
|
130
|
+
const customMnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
|
|
131
|
+
const defaultProvider = new DevProvider();
|
|
132
|
+
const customProvider = new DevProvider({ mnemonic: customMnemonic });
|
|
133
|
+
const defResult = await defaultProvider.connect();
|
|
134
|
+
const cusResult = await customProvider.connect();
|
|
135
|
+
if (defResult.ok && cusResult.ok) {
|
|
136
|
+
expect(defResult.value[0].address).not.toBe(cusResult.value[0].address);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
test("custom ss58Prefix changes address encoding", async () => {
|
|
140
|
+
const generic = new DevProvider({ ss58Prefix: 42 });
|
|
141
|
+
const polkadot = new DevProvider({ ss58Prefix: 0 });
|
|
142
|
+
const rg = await generic.connect();
|
|
143
|
+
const rp = await polkadot.connect();
|
|
144
|
+
if (rg.ok && rp.ok) {
|
|
145
|
+
// Different address strings
|
|
146
|
+
expect(rg.value[0].address).not.toBe(rp.value[0].address);
|
|
147
|
+
// Same underlying public key
|
|
148
|
+
expect(rg.value[0].publicKey).toEqual(rp.value[0].publicKey);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
test("disconnect is idempotent", () => {
|
|
152
|
+
const provider = new DevProvider();
|
|
153
|
+
// Should not throw
|
|
154
|
+
provider.disconnect();
|
|
155
|
+
provider.disconnect();
|
|
156
|
+
});
|
|
157
|
+
test("onStatusChange returns no-op unsubscribe", () => {
|
|
158
|
+
const provider = new DevProvider();
|
|
159
|
+
const callback = () => { };
|
|
160
|
+
const unsub = provider.onStatusChange(callback);
|
|
161
|
+
expect(typeof unsub).toBe("function");
|
|
162
|
+
unsub(); // should not throw
|
|
163
|
+
});
|
|
164
|
+
test("onAccountsChange returns no-op unsubscribe", () => {
|
|
165
|
+
const provider = new DevProvider();
|
|
166
|
+
const callback = () => { };
|
|
167
|
+
const unsub = provider.onAccountsChange(callback);
|
|
168
|
+
expect(typeof unsub).toBe("function");
|
|
169
|
+
unsub(); // should not throw
|
|
170
|
+
});
|
|
171
|
+
test("type is 'dev'", () => {
|
|
172
|
+
const provider = new DevProvider();
|
|
173
|
+
expect(provider.type).toBe("dev");
|
|
174
|
+
});
|
|
175
|
+
test("empty names array returns zero accounts", async () => {
|
|
176
|
+
const provider = new DevProvider({ names: [] });
|
|
177
|
+
const result = await provider.connect();
|
|
178
|
+
if (result.ok) {
|
|
179
|
+
expect(result.value).toHaveLength(0);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
test("default keyType is sr25519 (backward compatible)", async () => {
|
|
183
|
+
const provider = new DevProvider();
|
|
184
|
+
const result = await provider.connect();
|
|
185
|
+
if (result.ok) {
|
|
186
|
+
// Alice address matches well-known sr25519 address
|
|
187
|
+
expect(result.value[0].address).toBe(ALICE_ADDRESS);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
describe("DevProvider ed25519", () => {
|
|
192
|
+
test("ed25519 produces different addresses than sr25519", async () => {
|
|
193
|
+
const sr = new DevProvider({ keyType: "sr25519" });
|
|
194
|
+
const ed = new DevProvider({ keyType: "ed25519" });
|
|
195
|
+
const srResult = await sr.connect();
|
|
196
|
+
const edResult = await ed.connect();
|
|
197
|
+
if (srResult.ok && edResult.ok) {
|
|
198
|
+
expect(srResult.value[0].address).not.toBe(edResult.value[0].address);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
test("ed25519 addresses are deterministic", async () => {
|
|
202
|
+
const a = new DevProvider({ keyType: "ed25519" });
|
|
203
|
+
const b = new DevProvider({ keyType: "ed25519" });
|
|
204
|
+
const ra = await a.connect();
|
|
205
|
+
const rb = await b.connect();
|
|
206
|
+
if (ra.ok && rb.ok) {
|
|
207
|
+
expect(ra.value.map((x) => x.address)).toEqual(rb.value.map((x) => x.address));
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
test("ed25519 getSigner has matching publicKey", async () => {
|
|
211
|
+
const provider = new DevProvider({ keyType: "ed25519" });
|
|
212
|
+
const result = await provider.connect();
|
|
213
|
+
if (result.ok) {
|
|
214
|
+
for (const account of result.value) {
|
|
215
|
+
const signer = account.getSigner();
|
|
216
|
+
expect(signer.publicKey).toEqual(account.publicKey);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
test("ed25519 accounts have 32-byte publicKey", async () => {
|
|
221
|
+
const provider = new DevProvider({ keyType: "ed25519" });
|
|
222
|
+
const result = await provider.connect();
|
|
223
|
+
if (result.ok) {
|
|
224
|
+
for (const account of result.value) {
|
|
225
|
+
expect(account.publicKey).toBeInstanceOf(Uint8Array);
|
|
226
|
+
expect(account.publicKey.length).toBe(32);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
//# sourceMappingURL=dev.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dev.js","sourceRoot":"","sources":["../../src/providers/dev.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { InjectedExtension } from "polkadot-api/pjs-signer";
|
|
2
|
+
import { type SignerError } from "../errors.js";
|
|
3
|
+
import type { ConnectionStatus, ProviderType, Result, SignerAccount } from "../types.js";
|
|
4
|
+
import type { SignerProvider, Unsubscribe } from "./types.js";
|
|
5
|
+
/** Subset of the polkadot-api/pjs-signer API we use. Injected for testability. */
|
|
6
|
+
export interface ExtensionApi {
|
|
7
|
+
getInjectedExtensions: () => string[];
|
|
8
|
+
connectInjectedExtension: (name: string, dappName?: string) => Promise<InjectedExtension>;
|
|
9
|
+
}
|
|
10
|
+
/** Options for the browser extension provider. */
|
|
11
|
+
export interface ExtensionProviderOptions {
|
|
12
|
+
/** Target a specific extension by name (e.g., "talisman", "polkadot-js"). */
|
|
13
|
+
extensionName?: string;
|
|
14
|
+
/** App name shown when requesting permission from the extension. Default: "Polkadot App" */
|
|
15
|
+
dappName?: string;
|
|
16
|
+
/** Time in ms to wait for extension injection. Default: 500 */
|
|
17
|
+
injectionWait?: number;
|
|
18
|
+
/**
|
|
19
|
+
* Custom extension API. Defaults to polkadot-api/pjs-signer functions.
|
|
20
|
+
* @internal
|
|
21
|
+
*/
|
|
22
|
+
api?: ExtensionApi;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Provider for browser-injected wallet extensions.
|
|
26
|
+
*
|
|
27
|
+
* Discovers available extensions via `window.injectedWeb3`, connects to the
|
|
28
|
+
* first available (or a specific named) extension, and maps accounts to
|
|
29
|
+
* `SignerAccount` instances.
|
|
30
|
+
*/
|
|
31
|
+
export declare class ExtensionProvider implements SignerProvider {
|
|
32
|
+
readonly type: ProviderType;
|
|
33
|
+
private readonly extensionName;
|
|
34
|
+
private readonly dappName;
|
|
35
|
+
private readonly injectionWait;
|
|
36
|
+
private readonly apiOverride;
|
|
37
|
+
private extension;
|
|
38
|
+
private accountUnsubscribe;
|
|
39
|
+
private accountListeners;
|
|
40
|
+
constructor(options?: ExtensionProviderOptions);
|
|
41
|
+
connect(signal?: AbortSignal): Promise<Result<SignerAccount[], SignerError>>;
|
|
42
|
+
disconnect(): void;
|
|
43
|
+
onStatusChange(_callback: (status: ConnectionStatus) => void): Unsubscribe;
|
|
44
|
+
onAccountsChange(callback: (accounts: SignerAccount[]) => void): Unsubscribe;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=extension.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extension.d.ts","sourceRoot":"","sources":["../../src/providers/extension.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAA2B,MAAM,yBAAyB,CAAC;AAM1F,OAAO,EAAkD,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAChG,OAAO,KAAK,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEzF,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAM9D,kFAAkF;AAClF,MAAM,WAAW,YAAY;IACzB,qBAAqB,EAAE,MAAM,MAAM,EAAE,CAAC;IACtC,wBAAwB,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,iBAAiB,CAAC,CAAC;CAC7F;AAED,kDAAkD;AAClD,MAAM,WAAW,wBAAwB;IACrC,6EAA6E;IAC7E,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,4FAA4F;IAC5F,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,+DAA+D;IAC/D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,GAAG,CAAC,EAAE,YAAY,CAAC;CACtB;AAUD;;;;;;GAMG;AACH,qBAAa,iBAAkB,YAAW,cAAc;IACpD,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAe;IAC1C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAqB;IACnD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA2B;IAEvD,OAAO,CAAC,SAAS,CAAkC;IACnD,OAAO,CAAC,kBAAkB,CAA6B;IACvD,OAAO,CAAC,gBAAgB,CAAkD;gBAE9D,OAAO,CAAC,EAAE,wBAAwB;IAOxC,OAAO,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,aAAa,EAAE,EAAE,WAAW,CAAC,CAAC;IAwFlF,UAAU,IAAI,IAAI;IAalB,cAAc,CAAC,SAAS,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,GAAG,WAAW;IAK1E,gBAAgB,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,aAAa,EAAE,KAAK,IAAI,GAAG,WAAW;CAM/E"}
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { deriveH160 } from "@polkadot-apps/address";
|
|
2
|
+
import { createLogger } from "@polkadot-apps/logger";
|
|
3
|
+
import { sleep } from "../sleep.js";
|
|
4
|
+
import { ExtensionNotFoundError, ExtensionRejectedError } from "../errors.js";
|
|
5
|
+
import { err, ok } from "../types.js";
|
|
6
|
+
const log = createLogger("signer:extension");
|
|
7
|
+
const DEFAULT_INJECTION_WAIT = 500;
|
|
8
|
+
/* @integration */
|
|
9
|
+
async function defaultApi() {
|
|
10
|
+
const { getInjectedExtensions, connectInjectedExtension } = await import("polkadot-api/pjs-signer");
|
|
11
|
+
return { getInjectedExtensions, connectInjectedExtension };
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Provider for browser-injected wallet extensions.
|
|
15
|
+
*
|
|
16
|
+
* Discovers available extensions via `window.injectedWeb3`, connects to the
|
|
17
|
+
* first available (or a specific named) extension, and maps accounts to
|
|
18
|
+
* `SignerAccount` instances.
|
|
19
|
+
*/
|
|
20
|
+
export class ExtensionProvider {
|
|
21
|
+
type = "extension";
|
|
22
|
+
extensionName;
|
|
23
|
+
dappName;
|
|
24
|
+
injectionWait;
|
|
25
|
+
apiOverride;
|
|
26
|
+
extension = null;
|
|
27
|
+
accountUnsubscribe = null;
|
|
28
|
+
accountListeners = new Set();
|
|
29
|
+
constructor(options) {
|
|
30
|
+
this.extensionName = options?.extensionName;
|
|
31
|
+
this.dappName = options?.dappName ?? "Polkadot App";
|
|
32
|
+
this.injectionWait = options?.injectionWait ?? DEFAULT_INJECTION_WAIT;
|
|
33
|
+
this.apiOverride = options?.api;
|
|
34
|
+
}
|
|
35
|
+
async connect(signal) {
|
|
36
|
+
log.debug("waiting for extension injection", { wait: this.injectionWait });
|
|
37
|
+
// Wait for extensions to inject into the page
|
|
38
|
+
await sleep(this.injectionWait, signal);
|
|
39
|
+
if (signal?.aborted) {
|
|
40
|
+
return err(new ExtensionNotFoundError(this.extensionName ?? "*", "Connection aborted"));
|
|
41
|
+
}
|
|
42
|
+
// Load the API (dynamic import or injected override)
|
|
43
|
+
let api;
|
|
44
|
+
try {
|
|
45
|
+
api = this.apiOverride ?? (await defaultApi());
|
|
46
|
+
}
|
|
47
|
+
catch (cause) {
|
|
48
|
+
log.warn("polkadot-api/pjs-signer not available", { cause });
|
|
49
|
+
return err(new ExtensionNotFoundError(this.extensionName ?? "*", "polkadot-api/pjs-signer not available"));
|
|
50
|
+
}
|
|
51
|
+
let available;
|
|
52
|
+
try {
|
|
53
|
+
available = api.getInjectedExtensions();
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// getInjectedExtensions accesses window.injectedWeb3 and throws in non-browser envs
|
|
57
|
+
log.warn("cannot access browser extensions (no window)");
|
|
58
|
+
return err(new ExtensionNotFoundError(this.extensionName ?? "*", "Browser environment not available"));
|
|
59
|
+
}
|
|
60
|
+
log.debug("detected extensions", { available });
|
|
61
|
+
if (available.length === 0) {
|
|
62
|
+
log.warn("no browser extensions detected");
|
|
63
|
+
return err(new ExtensionNotFoundError(this.extensionName ?? "*", "No browser extensions detected"));
|
|
64
|
+
}
|
|
65
|
+
// Pick target extension
|
|
66
|
+
const targetName = this.extensionName ?? available[0];
|
|
67
|
+
if (!available.includes(targetName)) {
|
|
68
|
+
log.warn("requested extension not found", { targetName, available });
|
|
69
|
+
return err(new ExtensionNotFoundError(targetName));
|
|
70
|
+
}
|
|
71
|
+
// Connect
|
|
72
|
+
let ext;
|
|
73
|
+
try {
|
|
74
|
+
ext = await api.connectInjectedExtension(targetName, this.dappName);
|
|
75
|
+
}
|
|
76
|
+
catch (cause) {
|
|
77
|
+
log.error("extension rejected connection", { targetName, cause });
|
|
78
|
+
return err(new ExtensionRejectedError(targetName, cause instanceof Error
|
|
79
|
+
? cause.message
|
|
80
|
+
: `Extension "${targetName}" rejected connection`));
|
|
81
|
+
}
|
|
82
|
+
this.extension = ext;
|
|
83
|
+
const accounts = mapAccounts(ext);
|
|
84
|
+
log.info("extension connected", { name: targetName, accounts: accounts.length });
|
|
85
|
+
// Subscribe to account changes from the extension
|
|
86
|
+
this.accountUnsubscribe = ext.subscribe((rawAccounts) => {
|
|
87
|
+
const updated = mapInjectedAccounts(rawAccounts);
|
|
88
|
+
for (const listener of this.accountListeners) {
|
|
89
|
+
listener(updated);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
return ok(accounts);
|
|
93
|
+
}
|
|
94
|
+
disconnect() {
|
|
95
|
+
if (this.accountUnsubscribe) {
|
|
96
|
+
this.accountUnsubscribe();
|
|
97
|
+
this.accountUnsubscribe = null;
|
|
98
|
+
}
|
|
99
|
+
if (this.extension) {
|
|
100
|
+
this.extension.disconnect();
|
|
101
|
+
this.extension = null;
|
|
102
|
+
}
|
|
103
|
+
this.accountListeners.clear();
|
|
104
|
+
log.debug("extension disconnected");
|
|
105
|
+
}
|
|
106
|
+
onStatusChange(_callback) {
|
|
107
|
+
// Browser extensions don't emit status changes.
|
|
108
|
+
return () => { };
|
|
109
|
+
}
|
|
110
|
+
onAccountsChange(callback) {
|
|
111
|
+
this.accountListeners.add(callback);
|
|
112
|
+
return () => {
|
|
113
|
+
this.accountListeners.delete(callback);
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function mapAccounts(ext) {
|
|
118
|
+
return mapInjectedAccounts(ext.getAccounts());
|
|
119
|
+
}
|
|
120
|
+
function mapInjectedAccounts(accounts) {
|
|
121
|
+
return accounts.map((acct) => ({
|
|
122
|
+
address: acct.address,
|
|
123
|
+
h160Address: deriveH160(acct.polkadotSigner.publicKey),
|
|
124
|
+
publicKey: acct.polkadotSigner.publicKey,
|
|
125
|
+
name: acct.name ?? null,
|
|
126
|
+
source: "extension",
|
|
127
|
+
getSigner: () => acct.polkadotSigner,
|
|
128
|
+
}));
|
|
129
|
+
}
|
|
130
|
+
if (import.meta.vitest) {
|
|
131
|
+
const { test, expect, describe, vi } = import.meta.vitest;
|
|
132
|
+
const mockPubKey = new Uint8Array(32).fill(0xaa);
|
|
133
|
+
const mockSigner = { publicKey: mockPubKey };
|
|
134
|
+
function makeMockApi(options) {
|
|
135
|
+
const polkadotAccounts = (options.accounts ?? []).map((a) => ({
|
|
136
|
+
address: a.address,
|
|
137
|
+
name: a.name,
|
|
138
|
+
polkadotSigner: mockSigner,
|
|
139
|
+
}));
|
|
140
|
+
const mockExtension = {
|
|
141
|
+
name: "mock-extension",
|
|
142
|
+
getAccounts: () => polkadotAccounts,
|
|
143
|
+
subscribe: (_cb) => () => { },
|
|
144
|
+
disconnect: vi.fn(),
|
|
145
|
+
};
|
|
146
|
+
return {
|
|
147
|
+
getInjectedExtensions: () => options.extensions ?? [],
|
|
148
|
+
connectInjectedExtension: options.connectError
|
|
149
|
+
? () => Promise.reject(options.connectError)
|
|
150
|
+
: () => Promise.resolve(mockExtension),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
describe("ExtensionProvider", () => {
|
|
154
|
+
test("returns EXTENSION_NOT_FOUND when no extensions detected", async () => {
|
|
155
|
+
const api = makeMockApi({ extensions: [] });
|
|
156
|
+
const provider = new ExtensionProvider({ injectionWait: 0, api });
|
|
157
|
+
const result = await provider.connect();
|
|
158
|
+
expect(result.ok).toBe(false);
|
|
159
|
+
if (!result.ok) {
|
|
160
|
+
expect(result.error).toBeInstanceOf(ExtensionNotFoundError);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
test("returns EXTENSION_NOT_FOUND for specific missing extension", async () => {
|
|
164
|
+
const api = makeMockApi({ extensions: ["talisman"] });
|
|
165
|
+
const provider = new ExtensionProvider({
|
|
166
|
+
extensionName: "polkadot-js",
|
|
167
|
+
injectionWait: 0,
|
|
168
|
+
api,
|
|
169
|
+
});
|
|
170
|
+
const result = await provider.connect();
|
|
171
|
+
expect(result.ok).toBe(false);
|
|
172
|
+
if (!result.ok) {
|
|
173
|
+
expect(result.error).toBeInstanceOf(ExtensionNotFoundError);
|
|
174
|
+
if (result.error instanceof ExtensionNotFoundError) {
|
|
175
|
+
expect(result.error.extensionName).toBe("polkadot-js");
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
test("returns EXTENSION_REJECTED when enable fails", async () => {
|
|
180
|
+
const api = makeMockApi({
|
|
181
|
+
extensions: ["talisman"],
|
|
182
|
+
connectError: new Error("User rejected"),
|
|
183
|
+
});
|
|
184
|
+
const provider = new ExtensionProvider({
|
|
185
|
+
extensionName: "talisman",
|
|
186
|
+
injectionWait: 0,
|
|
187
|
+
api,
|
|
188
|
+
});
|
|
189
|
+
const result = await provider.connect();
|
|
190
|
+
expect(result.ok).toBe(false);
|
|
191
|
+
if (!result.ok) {
|
|
192
|
+
expect(result.error).toBeInstanceOf(ExtensionRejectedError);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
test("maps accounts correctly on success", async () => {
|
|
196
|
+
const api = makeMockApi({
|
|
197
|
+
extensions: ["talisman"],
|
|
198
|
+
accounts: [
|
|
199
|
+
{ address: "5GrwvaEF...", name: "My Account" },
|
|
200
|
+
{ address: "5FHneW46...", name: "Second" },
|
|
201
|
+
],
|
|
202
|
+
});
|
|
203
|
+
const provider = new ExtensionProvider({ injectionWait: 0, api });
|
|
204
|
+
const result = await provider.connect();
|
|
205
|
+
expect(result.ok).toBe(true);
|
|
206
|
+
if (result.ok) {
|
|
207
|
+
expect(result.value).toHaveLength(2);
|
|
208
|
+
expect(result.value[0].address).toBe("5GrwvaEF...");
|
|
209
|
+
expect(result.value[0].name).toBe("My Account");
|
|
210
|
+
expect(result.value[0].source).toBe("extension");
|
|
211
|
+
expect(result.value[1].address).toBe("5FHneW46...");
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
test("accounts without names have null name", async () => {
|
|
215
|
+
const api = makeMockApi({
|
|
216
|
+
extensions: ["test-ext"],
|
|
217
|
+
accounts: [{ address: "5GrwvaEF..." }],
|
|
218
|
+
});
|
|
219
|
+
const provider = new ExtensionProvider({ injectionWait: 0, api });
|
|
220
|
+
const result = await provider.connect();
|
|
221
|
+
if (result.ok) {
|
|
222
|
+
expect(result.value[0].name).toBeNull();
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
test("disconnect cleans up extension", async () => {
|
|
226
|
+
const api = makeMockApi({
|
|
227
|
+
extensions: ["test-ext"],
|
|
228
|
+
accounts: [{ address: "5GrwvaEF..." }],
|
|
229
|
+
});
|
|
230
|
+
const provider = new ExtensionProvider({ injectionWait: 0, api });
|
|
231
|
+
await provider.connect();
|
|
232
|
+
provider.disconnect();
|
|
233
|
+
// No error thrown = success
|
|
234
|
+
});
|
|
235
|
+
test("disconnect is idempotent", () => {
|
|
236
|
+
const provider = new ExtensionProvider({ injectionWait: 0 });
|
|
237
|
+
provider.disconnect();
|
|
238
|
+
provider.disconnect();
|
|
239
|
+
});
|
|
240
|
+
test("AbortSignal cancels during injection wait", async () => {
|
|
241
|
+
const controller = new AbortController();
|
|
242
|
+
controller.abort();
|
|
243
|
+
const api = makeMockApi({ extensions: ["talisman"] });
|
|
244
|
+
const provider = new ExtensionProvider({ injectionWait: 5000, api });
|
|
245
|
+
const result = await provider.connect(controller.signal);
|
|
246
|
+
expect(result.ok).toBe(false);
|
|
247
|
+
if (!result.ok) {
|
|
248
|
+
expect(result.error).toBeInstanceOf(ExtensionNotFoundError);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
test("connects to first available when no extensionName specified", async () => {
|
|
252
|
+
let connectedName;
|
|
253
|
+
const api = {
|
|
254
|
+
getInjectedExtensions: () => ["first-ext", "second-ext"],
|
|
255
|
+
connectInjectedExtension: async (name) => {
|
|
256
|
+
connectedName = name;
|
|
257
|
+
return {
|
|
258
|
+
name,
|
|
259
|
+
getAccounts: () => [],
|
|
260
|
+
subscribe: () => () => { },
|
|
261
|
+
disconnect: () => { },
|
|
262
|
+
};
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
const provider = new ExtensionProvider({ injectionWait: 0, api });
|
|
266
|
+
await provider.connect();
|
|
267
|
+
expect(connectedName).toBe("first-ext");
|
|
268
|
+
});
|
|
269
|
+
test("onStatusChange returns no-op unsubscribe", () => {
|
|
270
|
+
const provider = new ExtensionProvider();
|
|
271
|
+
const unsub = provider.onStatusChange(() => { });
|
|
272
|
+
expect(typeof unsub).toBe("function");
|
|
273
|
+
unsub();
|
|
274
|
+
});
|
|
275
|
+
test("onAccountsChange adds and removes listener", () => {
|
|
276
|
+
const provider = new ExtensionProvider();
|
|
277
|
+
const cb = () => { };
|
|
278
|
+
const unsub = provider.onAccountsChange(cb);
|
|
279
|
+
expect(typeof unsub).toBe("function");
|
|
280
|
+
unsub();
|
|
281
|
+
});
|
|
282
|
+
test("type is 'extension'", () => {
|
|
283
|
+
const provider = new ExtensionProvider();
|
|
284
|
+
expect(provider.type).toBe("extension");
|
|
285
|
+
});
|
|
286
|
+
test("uses custom dappName", async () => {
|
|
287
|
+
let usedDappName;
|
|
288
|
+
const api = {
|
|
289
|
+
getInjectedExtensions: () => ["ext"],
|
|
290
|
+
connectInjectedExtension: async (_name, dappName) => {
|
|
291
|
+
usedDappName = dappName;
|
|
292
|
+
return {
|
|
293
|
+
name: "ext",
|
|
294
|
+
getAccounts: () => [],
|
|
295
|
+
subscribe: () => () => { },
|
|
296
|
+
disconnect: () => { },
|
|
297
|
+
};
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
const provider = new ExtensionProvider({
|
|
301
|
+
dappName: "MyApp",
|
|
302
|
+
injectionWait: 0,
|
|
303
|
+
api,
|
|
304
|
+
});
|
|
305
|
+
await provider.connect();
|
|
306
|
+
expect(usedDappName).toBe("MyApp");
|
|
307
|
+
});
|
|
308
|
+
test("getSigner returns signer from injected account", async () => {
|
|
309
|
+
const api = makeMockApi({
|
|
310
|
+
extensions: ["ext"],
|
|
311
|
+
accounts: [{ address: "5GrwvaEF..." }],
|
|
312
|
+
});
|
|
313
|
+
const provider = new ExtensionProvider({ injectionWait: 0, api });
|
|
314
|
+
const result = await provider.connect();
|
|
315
|
+
if (result.ok) {
|
|
316
|
+
const signer = result.value[0].getSigner();
|
|
317
|
+
expect(signer.publicKey).toEqual(mockPubKey);
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
test("subscription callback fires onAccountsChange listeners", async () => {
|
|
321
|
+
let subscribeCb;
|
|
322
|
+
const api = {
|
|
323
|
+
getInjectedExtensions: () => ["test-ext"],
|
|
324
|
+
connectInjectedExtension: async () => ({
|
|
325
|
+
name: "test-ext",
|
|
326
|
+
getAccounts: () => [
|
|
327
|
+
{ address: "5Initial", name: "Initial", polkadotSigner: mockSigner },
|
|
328
|
+
],
|
|
329
|
+
subscribe: (cb) => {
|
|
330
|
+
subscribeCb = cb;
|
|
331
|
+
return () => { };
|
|
332
|
+
},
|
|
333
|
+
disconnect: () => { },
|
|
334
|
+
}),
|
|
335
|
+
};
|
|
336
|
+
const provider = new ExtensionProvider({ injectionWait: 0, api });
|
|
337
|
+
const receivedAccounts = [];
|
|
338
|
+
provider.onAccountsChange((accounts) => receivedAccounts.push(accounts));
|
|
339
|
+
await provider.connect();
|
|
340
|
+
// Simulate account change from extension
|
|
341
|
+
subscribeCb([
|
|
342
|
+
{ address: "5NewAddr1", name: "New1", polkadotSigner: mockSigner },
|
|
343
|
+
{ address: "5NewAddr2", name: "New2", polkadotSigner: mockSigner },
|
|
344
|
+
]);
|
|
345
|
+
expect(receivedAccounts).toHaveLength(1);
|
|
346
|
+
expect(receivedAccounts[0]).toHaveLength(2);
|
|
347
|
+
expect(receivedAccounts[0][0].address).toBe("5NewAddr1");
|
|
348
|
+
expect(receivedAccounts[0][1].address).toBe("5NewAddr2");
|
|
349
|
+
});
|
|
350
|
+
test("connect without api override uses defaultApi (returns no extensions in Node)", async () => {
|
|
351
|
+
// This exercises the defaultApi() dynamic import path.
|
|
352
|
+
// In Node, polkadot-api/pjs-signer loads successfully but
|
|
353
|
+
// getInjectedExtensions() returns [] since there's no window.injectedWeb3.
|
|
354
|
+
const provider = new ExtensionProvider({ injectionWait: 0 });
|
|
355
|
+
const result = await provider.connect();
|
|
356
|
+
expect(result.ok).toBe(false);
|
|
357
|
+
if (!result.ok) {
|
|
358
|
+
expect(result.error).toBeInstanceOf(ExtensionNotFoundError);
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
//# sourceMappingURL=extension.js.map
|