windkit 0.2.2 → 0.3.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/CHANGELOG.md CHANGED
@@ -1,54 +1,101 @@
1
1
  # Changelog
2
2
 
3
- All notable changes to this project will be documented in this file.
3
+ ## 0.3.0
4
4
 
5
- This project follows Semantic Versioning.
5
+ - Added Wind Wallet in-app browser injected provider support.
6
+ - Added `InjectedWalletSession`.
7
+ - Added `WindClient` hybrid connector.
8
+ - Added `connectWindWallet()` helper.
9
+ - Keeps existing QR/VSR + PeerJS flow as fallback.
10
+ - Exposes provider detection helpers: `getInjectedWindProvider()` and `isWindWalletInjected()`.
11
+
12
+
13
+ All notable changes to this project are documented in this file.
14
+
15
+ This project follows [Semantic Versioning](https://semver.org/).
6
16
 
7
17
  ---
8
18
 
9
- ## [0.2.2] - 2026-03-02
19
+ ## [0.2.3] - 2026-03-02
20
+
21
+ Published: 2026-03-02T14:07:04Z
10
22
 
11
23
  ### Changed
12
24
 
13
25
  - Session storage key standardized to `"vex-session"`.
14
- - Login identity request now stores compact app info keys:
15
- - `pi` (peer id), `na` (app name), `ic` (icon), `do` (origin), optional `auth` (cached proof).
16
- - Request method names standardized:
17
- - `signRequest`, `signMessage`, `sharedSecret`.
26
+ - Login identity payload compacted with standardized keys:
27
+ - `pi` (peer id)
28
+ - `na` (app name)
29
+ - `ic` (icon)
30
+ - `do` (origin)
31
+ - optional `auth` (cached identity proof)
32
+ - RPC method names standardized:
33
+ - `signRequest`
34
+ - `signMessage`
35
+ - `sharedSecret`
18
36
 
19
37
  ### Added
20
38
 
21
- - `clearSession()` helper.
22
- - Session reuse:
23
- - Reuses saved `peerID` and hints (`account@permission`) when present.
39
+ - `clearSession()` helper for manual session reset.
40
+ - Smart session reuse:
41
+ - Reuses stored `peerID`
42
+ - Reuses permission hints (`account@permission`) when available.
24
43
 
25
44
  ### Fixed
26
45
 
27
- - Safer session loading (auto-clears invalid or expired payloads).
28
- - Improved cleanup on disconnect (best-effort destroy/disconnect).
46
+ - Hardened session loader:
47
+ - Automatically clears expired or malformed session payloads.
48
+ - Improved disconnect cleanup:
49
+ - Safe destroy/disconnect (best-effort, non-throwing).
29
50
 
30
51
  ### Improved
31
52
 
32
- - Low-memory friendly behavior:
33
- - Lightweight heartbeat/ping with jitter (best-effort).
34
- - Minimal allocations on message routing.
53
+ - Optimized for low-memory environments:
54
+ - Lightweight heartbeat with jitter
55
+ - Reduced message routing allocations
56
+ - Lower background CPU usage on mobile devices
57
+
58
+ ---
59
+
60
+ ## [0.2.1] - 2025-10-13
61
+
62
+ Published: 2025-10-13T16:28:38Z
63
+
64
+ ### Added
65
+
66
+ - PeerJS signaling support.
67
+ - Session persistence via `sessionStorage`.
68
+ - Transaction signing via Vexanium Signing Request (VSR).
69
+ - Message signing support.
70
+ - Shared secret derivation (ECDH).
35
71
 
36
72
  ---
37
73
 
38
- ## [0.2.1]
74
+ ## [0.2.0] - 2025-10-06
75
+
76
+ Published: 2025-10-06T14:44:23Z
39
77
 
40
78
  ### Added
41
79
 
42
- - Initial PeerJS signaling support.
43
- - Session persistence via sessionStorage.
44
- - Transaction signing via VSR.
45
- - Message signing.
46
- - Shared secret (ECDH).
80
+ - Structured WebRTC connection lifecycle.
81
+ - WalletSession abstraction layer.
82
+ - Basic login flow via embedded PeerID inside VSR.
83
+ - Request/response routing via internal request IDs.
84
+
85
+ ### Improved
86
+
87
+ - More predictable session initialization flow.
88
+ - Internal protocol normalization groundwork.
47
89
 
48
90
  ---
49
91
 
50
- ## [0.1.1]
92
+ ## [0.1.1] - 2025-08-12
93
+
94
+ Published: 2025-08-12T01:33:25Z
51
95
 
52
96
  ### Added
53
97
 
54
98
  - Initial public release of WindKit.
99
+ - Basic VSR identity login.
100
+ - WebRTC DataConnection integration.
101
+ - Minimal transaction signing flow.
package/README.md CHANGED
@@ -7,39 +7,125 @@ WindKit is a lightweight WebRTC protocol for connecting Vexanium DApps to Wind W
7
7
 
8
8
  It enables secure cross-device login and transaction signing without requiring browser extensions.
9
9
 
10
+ Designed for production environments, including low-RAM mobile devices.
11
+
10
12
  ---
11
13
 
12
- ## Features
14
+ ## Features
13
15
 
14
16
  - Cross-device login via VSR (Vexanium Signing Request)
15
17
  - Transaction signing (single action, multiple actions, or full transaction)
16
- - Optional broadcast (sign-only or broadcast)
18
+ - Optional broadcast control (sign-only or broadcast)
17
19
  - Message signing
18
20
  - Shared secret derivation (ECDH)
19
- - Session persistence via sessionStorage
20
- - Lightweight keepalive (low memory footprint)
21
- - Pure ESM module
21
+ - Session persistence via `sessionStorage`
22
+ - Low-memory heartbeat strategy
23
+ - Pure ESM module (JavaScript-only)
24
+
25
+ ---
26
+
27
+ ## 🧩 In-App Browser Injected Provider
28
+
29
+ WindKit now supports Wind Wallet's in-app DApp Browser provider, similar to MetaMask / Phantom / Rabby.
30
+
31
+ When a DApp is opened inside Wind Wallet Browser, Wind Wallet injects:
32
+
33
+ ```js
34
+ window.wind
35
+ window.windwallet
36
+ window.windWallet
37
+ window.vexanium
38
+ window.vex
39
+ ```
40
+
41
+ Use the hybrid client to support both injected provider and QR/VSR fallback:
42
+
43
+ ```js
44
+ import { WindClient } from "windkit";
45
+
46
+ const wind = new WindClient();
47
+
48
+ const session = await wind.connect({
49
+ name: "My Vexanium DApp",
50
+ icon: "https://example.com/icon.png",
51
+
52
+ // Called only when the DApp is opened outside Wind Wallet Browser.
53
+ // Render this VSR as QR, or show your existing login modal.
54
+ onLoginRequest(vsr) {
55
+ console.log("Show QR:", vsr);
56
+ }
57
+ });
58
+
59
+ console.log("Connected:", session.permissionLevel?.toString());
60
+ ```
61
+
62
+ ### Direct injected request
63
+
64
+ ```js
65
+ import { InjectedWalletSession } from "windkit";
66
+
67
+ if (InjectedWalletSession.isAvailable()) {
68
+ const session = await InjectedWalletSession.connect();
69
+
70
+ await session.signMessage("Hello Wind!");
71
+ }
72
+ ```
73
+
74
+ ### Provider API
75
+
76
+ Inside Wind Wallet Browser, DApps can also call the provider directly:
77
+
78
+ ```js
79
+ const accounts = await window.wind.request({
80
+ method: "vex_requestAccounts"
81
+ });
82
+
83
+ const signature = await window.wind.request({
84
+ method: "vex_signMessage",
85
+ params: ["Hello Wind!"]
86
+ });
87
+
88
+ const result = await window.wind.request({
89
+ method: "signRequest",
90
+ params: ["vsr:..."]
91
+ });
92
+ ```
93
+
94
+ ### Recommended DApp logic
95
+
96
+ ```txt
97
+ 1. Try injected provider first: window.wind / window.vexanium.
98
+ 2. If not available, use QR/VSR + PeerJS fallback.
99
+ 3. Keep signing approval inside Wind Wallet.
100
+ ```
101
+
102
+ This keeps old QR pairing working while enabling MetaMask-style connect inside the Wind Wallet app.
22
103
 
23
104
  ---
24
105
 
25
- ## Installation
106
+ ## 📦 Installation
26
107
 
108
+ ```bash
27
109
  npm install windkit
110
+ ```
28
111
 
29
- WindKit is ESM-only.
112
+ WindKit is **ESM-only**.
30
113
 
31
- Your project must use:
114
+ Your project must include:
32
115
 
116
+ ```json
33
117
  {
34
118
  "type": "module"
35
119
  }
120
+ ```
36
121
 
37
122
  ---
38
123
 
39
- ## Quick Start
124
+ ## 🚀 Quick Start
40
125
 
41
126
  ### Create Connector
42
127
 
128
+ ```js
43
129
  import { WindConnector } from "windkit";
44
130
 
45
131
  const connector = new WindConnector();
@@ -49,29 +135,37 @@ connector.on("session", (session, proof) => {
49
135
  });
50
136
 
51
137
  await connector.connect();
138
+ ```
52
139
 
53
- By default, WindKit relies on PeerJS default signaling behavior (no host/port/path/secure configured).
140
+ By default, WindKit uses PeerJS default signaling.
54
141
 
55
142
  ---
56
143
 
57
- ### Use Your Own PeerJS Server (Optional)
144
+ ## 🌐 Custom PeerJS Server (Optional)
58
145
 
146
+ ```js
59
147
  connector.setServer("peer.yourdomain.com", 443, "/", true);
148
+ ```
60
149
 
61
150
  Signature:
62
151
 
63
- setServer(host, port, path, secure)
152
+ ```js
153
+ setServer(host, port, path, secure);
154
+ ```
64
155
 
65
- Add custom STUN/TURN server:
156
+ Add custom ICE server:
66
157
 
158
+ ```js
67
159
  connector.addIceServer({
68
160
  urls: "stun:stun.cloudflare.com:3478"
69
161
  });
162
+ ```
70
163
 
71
164
  ---
72
165
 
73
- ## Login Flow (VSR)
166
+ ## 🔐 Login Flow (VSR)
74
167
 
168
+ ```js
75
169
  const vsr = connector.createLoginRequest(
76
170
  "My Vexanium DApp",
77
171
  "https://example.com/icon.png"
@@ -83,20 +177,21 @@ window.open(
83
177
  `https://wallet.windcrypto.com/login?vsr=${encodeURIComponent(payload)}`,
84
178
  "Wind Wallet"
85
179
  );
180
+ ```
86
181
 
87
182
  Wallet flow:
88
183
 
89
184
  1. Decode VSR
90
185
  2. Connect to embedded PeerID
91
- 3. Send LOGIN_OK
186
+ 3. Send `LOGIN_OK`
92
187
  4. Emit session
93
188
 
94
189
  ---
95
190
 
96
- ## Session Handling
191
+ ## 🔄 Session Handling
97
192
 
193
+ ```js
98
194
  connector.on("session", (session, proof) => {
99
-
100
195
  session.onClose(() => {
101
196
  console.log("Wallet disconnected");
102
197
  });
@@ -107,13 +202,15 @@ connector.on("session", (session, proof) => {
107
202
 
108
203
  window.appSession = session;
109
204
  });
205
+ ```
110
206
 
111
207
  ---
112
208
 
113
- ## Send Transaction
209
+ ## ✍️ Send Transaction
114
210
 
115
211
  ### With ABI Cache (Recommended)
116
212
 
213
+ ```js
117
214
  import { Action } from "@wharfkit/antelope";
118
215
  import { ABICache } from "@wharfkit/abicache";
119
216
 
@@ -130,7 +227,7 @@ const action = Action.from(
130
227
  from: "alice",
131
228
  to: "bob",
132
229
  quantity: "1.0000 VEX",
133
- memo: "test"
230
+ memo: "WindKit test"
134
231
  },
135
232
  authorization: [appSession.permissionLevel]
136
233
  },
@@ -140,119 +237,145 @@ const action = Action.from(
140
237
  const result = await appSession.transact({ action });
141
238
 
142
239
  console.log(result.transaction_id ?? result.id);
240
+ ```
143
241
 
144
242
  ---
145
243
 
146
244
  ### Sign Only (No Broadcast)
147
245
 
246
+ ```js
148
247
  await appSession.transact(
149
248
  { action },
150
249
  { broadcast: false }
151
250
  );
251
+ ```
152
252
 
153
253
  ---
154
254
 
155
- ## Sign Message
255
+ ## 📝 Sign Message
156
256
 
257
+ ```js
157
258
  const signature = await appSession.signMessage("Hello Wind!");
158
259
  console.log(signature.toString());
260
+ ```
159
261
 
160
262
  ---
161
263
 
162
- ## Shared Secret (ECDH)
264
+ ## 🔑 Shared Secret (ECDH)
163
265
 
266
+ ```js
164
267
  import { PublicKey } from "@wharfkit/antelope";
165
268
 
166
269
  const pub = PublicKey.from("PUB_K1_...");
167
270
  const secret = await appSession.sharedSecret(pub);
168
271
 
169
272
  console.log(secret.toString());
273
+ ```
170
274
 
171
275
  ---
172
276
 
173
- ## Session Storage
277
+ ## 💾 Session Storage
174
278
 
175
279
  WindKit stores session data in:
176
280
 
281
+ ```js
177
282
  sessionStorage["vex-session"]
283
+ ```
178
284
 
179
285
  Example structure:
180
286
 
287
+ ```json
181
288
  {
182
289
  "peerID": "VEX-xxxx",
183
290
  "permission": "account@active",
184
291
  "expiration": "2026-03-01T12:00:00",
185
292
  "auth": "base64u_identity_proof"
186
293
  }
294
+ ```
187
295
 
188
- To clear session manually:
296
+ Clear session manually:
189
297
 
298
+ ```js
190
299
  import { clearSession } from "windkit";
191
300
 
192
301
  clearSession();
302
+ ```
193
303
 
194
304
  ---
195
305
 
196
- ## Architecture
306
+ ## 🏗 Architecture
307
+
308
+ ### WindConnector
197
309
 
198
- WindConnector
199
310
  - Creates VSR identity login
200
311
  - Hosts PeerJS PeerID (DApp-side)
201
312
  - Waits for wallet connection
202
313
  - Emits session
203
314
 
204
- WalletSession
315
+ ### WalletSession
316
+
205
317
  - Sends:
206
- - signRequest
207
- - signMessage
208
- - sharedSecret
318
+ - `signRequest`
319
+ - `signMessage`
320
+ - `sharedSecret`
209
321
  - Routes replies via request IDs
210
- - Lightweight keepalive ping
322
+ - Lightweight heartbeat ping
211
323
  - Handles account change events
212
324
 
213
325
  ---
214
326
 
215
- ## Protocol Notes
327
+ ## 🔎 Protocol Notes
328
+
329
+ Transaction signing method:
216
330
 
217
- Transaction signing method name:
218
- "signRequest"
331
+ ```
332
+ signRequest
333
+ ```
219
334
 
220
335
  Wallet push events handled:
336
+
337
+ ```
221
338
  LOGIN_OK
222
339
  ACTIVE_ACCOUNT_CHANGED
340
+ ```
223
341
 
224
- All communication occurs via PeerJS DataConnection.
342
+ All communication occurs over PeerJS `DataConnection`.
225
343
 
226
344
  ---
227
345
 
228
- ## Technical Details
346
+ ## Technical Details
347
+
348
+ Chain ID is internally fixed via:
229
349
 
230
- Chain ID is fixed internally via:
350
+ ```js
231
351
  WalletSession.ChainID
352
+ ```
353
+
354
+ Dependencies:
355
+
356
+ - `@wharfkit/signing-request`
357
+ - `@wharfkit/antelope`
358
+ - `peerjs`
359
+ - `pako`
232
360
 
233
- Uses:
234
- - @wharfkit/signing-request
235
- - @wharfkit/antelope
236
- - peerjs
237
- - pako (zlib compression)
361
+ Optimized for:
238
362
 
239
- Designed for:
240
363
  - Low-RAM mobile devices
241
- - Background tabs
242
- - Unstable WebRTC environments
364
+ - Background browser tabs
365
+ - Unstable WebRTC networks
243
366
 
244
367
  ---
245
368
 
246
- ## Security Model
369
+ ## 🔐 Security Model
247
370
 
248
371
  - IdentityProof can be verified by the DApp (recommended).
249
372
  - Private keys never leave the wallet.
250
373
  - VSR ensures transaction integrity.
251
- - PeerID is embedded inside VSR to prevent misrouting.
374
+ - PeerID is embedded inside the VSR payload to prevent misrouting.
252
375
 
253
376
  ---
254
377
 
255
- ## License
378
+ ## 📄 License
256
379
 
257
380
  MIT License
258
381
  © Wind Stack
package/index.js CHANGED
@@ -1,3 +1,11 @@
1
1
  export { WindConnector } from "./src/WindConnector.js";
2
2
  export { WalletSession } from "./src/WalletSession.js";
3
- export { saveSession, loadSession, clearSession } from "./src/StoreSession.js";
3
+ export {
4
+ InjectedWalletSession,
5
+ WindClient,
6
+ connectWindWallet,
7
+ getInjectedWindProvider,
8
+ isWindWalletInjected,
9
+ requestWithTimeout,
10
+ } from "./src/InjectedWindProvider.js";
11
+ export { saveSession, loadSession, clearSession } from "./src/StoreSession.js";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "windkit",
3
- "version": "0.2.2",
4
- "description": "Lightweight protocol to connect Vexanium DApps to Wind Wallet via PeerJS and VSR.",
3
+ "version": "0.3.0",
4
+ "description": "Hybrid protocol to connect Vexanium DApps to Wind Wallet using injected provider, PeerJS, and VSR.",
5
5
  "license": "MIT",
6
6
  "author": "windstack",
7
7
  "type": "module",
@@ -35,7 +35,11 @@
35
35
  "wharfkit",
36
36
  "signing-request",
37
37
  "vsr",
38
- "blockchain"
38
+ "blockchain",
39
+ "injected-provider",
40
+ "in-app-browser",
41
+ "metamask-compatible",
42
+ "phantom-compatible"
39
43
  ],
40
44
  "sideEffects": false,
41
45
  "publishConfig": {
@@ -56,4 +60,4 @@
56
60
  "engines": {
57
61
  "node": ">=18"
58
62
  }
59
- }
63
+ }
@@ -0,0 +1,518 @@
1
+ // windkit/InjectedWindProvider.js
2
+ // WindKit injected-provider bridge
3
+ // ✅ In-app browser first (window.wind / window.windwallet / window.vexanium)
4
+ // ✅ Keeps QR/VSR + PeerJS fallback untouched
5
+ // ✅ MetaMask-style request({ method, params }) API for Wind Wallet browser
6
+ // ✅ No wallet keys, no signing logic here — DApp-side transport only
7
+
8
+ import { SigningRequest } from "@wharfkit/signing-request";
9
+ import {
10
+ Checksum512,
11
+ Name,
12
+ PermissionLevel,
13
+ PublicKey,
14
+ Signature,
15
+ SignedTransaction,
16
+ } from "@wharfkit/antelope";
17
+ import zlib from "pako";
18
+
19
+ import { WindConnector } from "./WindConnector.js";
20
+
21
+ const DEFAULT_TIMEOUT_MS = 90_000;
22
+
23
+ function normalizeTimeoutMs(v, fallback = DEFAULT_TIMEOUT_MS) {
24
+ const n = Number(v);
25
+ if (!Number.isFinite(n)) return fallback;
26
+ return Math.max(1000, Math.trunc(n));
27
+ }
28
+
29
+ function setTimer(fn, ms) {
30
+ try {
31
+ return globalThis.setTimeout(fn, ms);
32
+ } catch {
33
+ return setTimeout(fn, ms);
34
+ }
35
+ }
36
+
37
+ function clearTimer(id) {
38
+ try {
39
+ globalThis.clearTimeout(id);
40
+ } catch {
41
+ clearTimeout(id);
42
+ }
43
+ }
44
+
45
+ function getWindowLike() {
46
+ try {
47
+ if (typeof globalThis !== "undefined") return globalThis;
48
+ } catch {}
49
+ return undefined;
50
+ }
51
+
52
+ function normalizeParams(params) {
53
+ if (params === undefined) return [];
54
+ return Array.isArray(params) ? params : [params];
55
+ }
56
+
57
+ function replyError(reply, fallback) {
58
+ const err = reply?.error;
59
+ if (typeof err === "string") return new Error(err);
60
+ if (err?.message) return new Error(err.message);
61
+ if (reply?.message) return new Error(reply.message);
62
+ return new Error(fallback || "Wind Wallet request failed.");
63
+ }
64
+
65
+ function parseAccountLike(value) {
66
+ if (!value) return "";
67
+ if (typeof value === "string") return value;
68
+ if (Array.isArray(value)) return parseAccountLike(value[0]);
69
+ if (typeof value === "object") {
70
+ return (
71
+ value.permission ||
72
+ value.permissionLevel ||
73
+ value.account ||
74
+ value.address ||
75
+ value.actor ||
76
+ ""
77
+ );
78
+ }
79
+ return "";
80
+ }
81
+
82
+ function toPermissionLevel(value) {
83
+ const raw = parseAccountLike(value);
84
+ if (!raw) return undefined;
85
+
86
+ // Vexanium permission is canonical actor@permission.
87
+ if (String(raw).includes("@")) {
88
+ return PermissionLevel.from(String(raw));
89
+ }
90
+
91
+ // Some injected providers may return { actor, permission } separately.
92
+ if (value && typeof value === "object" && value.actor && value.permission) {
93
+ return PermissionLevel.from(`${value.actor}@${value.permission}`);
94
+ }
95
+
96
+ return undefined;
97
+ }
98
+
99
+ function isRequestProvider(value) {
100
+ return Boolean(value && typeof value.request === "function");
101
+ }
102
+
103
+ /**
104
+ * Return the first Wind-compatible injected provider available in the current browser.
105
+ *
106
+ * Wind Wallet app injects several aliases for compatibility:
107
+ * - window.wind
108
+ * - window.windwallet
109
+ * - window.windWallet
110
+ * - window.vexanium
111
+ * - window.vex
112
+ */
113
+ export function getInjectedWindProvider(scope) {
114
+ const w = scope || getWindowLike();
115
+ if (!w) return null;
116
+
117
+ const candidates = [
118
+ w.wind,
119
+ w.windwallet,
120
+ w.windWallet,
121
+ w.vexanium,
122
+ w.vex,
123
+ ];
124
+
125
+ for (const provider of candidates) {
126
+ if (isRequestProvider(provider)) return provider;
127
+ }
128
+
129
+ return null;
130
+ }
131
+
132
+ export function isWindWalletInjected(scope) {
133
+ const provider = getInjectedWindProvider(scope);
134
+ return Boolean(provider?.isWindWallet || provider?.isWind || provider?.request);
135
+ }
136
+
137
+ /**
138
+ * InjectedWalletSession
139
+ * DApp-side session object for Wind Wallet in-app browser.
140
+ *
141
+ * It intentionally mirrors WalletSession's public API:
142
+ * - transact()
143
+ * - signRequest()
144
+ * - signMessage()
145
+ * - sharedSecret()
146
+ * - permissionLevel
147
+ */
148
+ export class InjectedWalletSession {
149
+ static ChainID = "f9f432b1851b5c179d2091a96f593aaed50ec7466b74f89301f957a83e56ce1f";
150
+
151
+ /** @type {any} */
152
+ #provider;
153
+
154
+ /** @type {{zlib:any, abiProvider?:any} | undefined} */
155
+ #encodingOptions;
156
+
157
+ /** @type {PermissionLevel | undefined} */
158
+ #permissionLevel;
159
+
160
+ /** @type {(permission: PermissionLevel) => void | undefined} */
161
+ #accountChangeListener;
162
+
163
+ /** @type {() => void | undefined} */
164
+ #closeListener;
165
+
166
+ /** @type {(error: Error) => void | undefined} */
167
+ #errorListener;
168
+
169
+ constructor(provider, permissionLevel) {
170
+ if (!isRequestProvider(provider)) {
171
+ throw new Error("Wind Wallet injected provider was not found.");
172
+ }
173
+
174
+ this.#provider = provider;
175
+
176
+ const perm = toPermissionLevel(permissionLevel);
177
+ if (perm) this.#permissionLevel = perm;
178
+
179
+ this.#bindProviderEvents();
180
+ }
181
+
182
+ static isAvailable(scope) {
183
+ return isWindWalletInjected(scope);
184
+ }
185
+
186
+ static getProvider(scope) {
187
+ return getInjectedWindProvider(scope);
188
+ }
189
+
190
+ /**
191
+ * Request accounts from the injected Wind Wallet provider.
192
+ * @param {{provider?: any, timeoutMs?: number}=} options
193
+ */
194
+ static async connect(options = {}) {
195
+ const provider = options.provider || getInjectedWindProvider();
196
+ if (!provider) throw new Error("Wind Wallet injected provider is not available.");
197
+
198
+ const result = await requestWithTimeout(
199
+ provider,
200
+ "vex_requestAccounts",
201
+ [],
202
+ options.timeoutMs
203
+ );
204
+
205
+ const permission = toPermissionLevel(result) || toPermissionLevel(result?.accounts);
206
+ return new InjectedWalletSession(provider, permission || result);
207
+ }
208
+
209
+ #bindProviderEvents() {
210
+ const provider = this.#provider;
211
+
212
+ // MetaMask-style provider events if Wind Wallet exposes them later.
213
+ if (typeof provider.on === "function") {
214
+ try {
215
+ provider.on("accountsChanged", (accounts) => {
216
+ const next = toPermissionLevel(accounts);
217
+ if (next) {
218
+ this.#permissionLevel = next;
219
+ this.#accountChangeListener?.(next);
220
+ }
221
+ });
222
+ } catch {}
223
+
224
+ try {
225
+ provider.on("disconnect", () => {
226
+ this.#closeListener?.();
227
+ });
228
+ } catch {}
229
+ }
230
+ }
231
+
232
+ setABICache(cache) {
233
+ this.#encodingOptions = { zlib, abiProvider: cache };
234
+ }
235
+
236
+ onAccountChange(listener) {
237
+ this.#accountChangeListener = listener;
238
+ }
239
+
240
+ onClose(listener) {
241
+ this.#closeListener = listener;
242
+ }
243
+
244
+ onError(listener) {
245
+ this.#errorListener = listener;
246
+ }
247
+
248
+ isOpen() {
249
+ return true;
250
+ }
251
+
252
+ close() {
253
+ // Injected browser sessions are page-scoped. No persistent socket to close.
254
+ this.#closeListener?.();
255
+ }
256
+
257
+ metadata() {
258
+ return { transport: "injected", wallet: "Wind Wallet" };
259
+ }
260
+
261
+ get permissionLevel() {
262
+ return this.#permissionLevel;
263
+ }
264
+
265
+ set permissionLevel(value) {
266
+ const perm = toPermissionLevel(value);
267
+ this.#permissionLevel = perm || value;
268
+ }
269
+
270
+ get actor() {
271
+ return this.#permissionLevel?.actor ?? Name.from("");
272
+ }
273
+
274
+ get permission() {
275
+ return this.#permissionLevel?.permission ?? Name.from("");
276
+ }
277
+
278
+ async request(method, params, timeoutMs) {
279
+ return await requestWithTimeout(this.#provider, method, params, timeoutMs);
280
+ }
281
+
282
+ /**
283
+ * Create a VSR signing request for a transaction and send to wallet.
284
+ * Matches WalletSession.transact().
285
+ */
286
+ async transact(args, options) {
287
+ const willBroadcast = typeof options?.broadcast === "boolean" ? options.broadcast : true;
288
+ const requestArgs = { ...args, chainId: InjectedWalletSession.ChainID };
289
+
290
+ const req = await SigningRequest.create(requestArgs, this.#encodingOptions);
291
+ req.setBroadcast(willBroadcast);
292
+
293
+ const vsr = req.encode(true, false, "vsr:");
294
+ return this.signRequest(vsr, options?.timeoutMs);
295
+ }
296
+
297
+ /**
298
+ * Send a VSR string to Wind Wallet approval UI.
299
+ */
300
+ async signRequest(vsr, timeoutMs) {
301
+ const payload = typeof vsr === "string" ? vsr : String(vsr || "");
302
+ let reply;
303
+
304
+ if (typeof this.#provider.signRequest === "function") {
305
+ reply = await withTimeout(
306
+ Promise.resolve(this.#provider.signRequest(payload)),
307
+ timeoutMs,
308
+ "signRequest"
309
+ );
310
+ } else {
311
+ reply = await this.request("signRequest", [payload], timeoutMs);
312
+ }
313
+
314
+ if (reply?.code === "SENT") return reply.result;
315
+ if (reply?.code === "SIGNED") return SignedTransaction.from(reply.result);
316
+ if (reply?.signatures || reply?.transaction) return SignedTransaction.from(reply);
317
+ if (reply?.transaction_id || reply?.processed) return reply;
318
+
319
+ throw replyError(reply, "Signing request rejected.");
320
+ }
321
+
322
+ async signMessage(message, timeoutMs) {
323
+ const reply = await this.request("vex_signMessage", [message], timeoutMs);
324
+
325
+ if (typeof reply === "string") return Signature.from(reply);
326
+ if (reply?.signature) return Signature.from(reply.signature);
327
+ if (reply?.code === "SIGNED" && reply?.result?.signature) {
328
+ return Signature.from(reply.result.signature);
329
+ }
330
+
331
+ throw replyError(reply, "Message signing rejected.");
332
+ }
333
+
334
+ async sharedSecret(publicKey, timeoutMs) {
335
+ const key = publicKey?.toString ? publicKey.toString() : String(publicKey || "");
336
+ const reply = await this.request("vex_sharedSecret", [key], timeoutMs);
337
+
338
+ if (typeof reply === "string") return Checksum512.from(reply);
339
+ if (reply?.secret) return Checksum512.from(reply.secret);
340
+ if (reply?.code === "CREATED" && reply?.result?.secret) {
341
+ return Checksum512.from(reply.result.secret);
342
+ }
343
+
344
+ throw replyError(reply, "Shared secret creation failed.");
345
+ }
346
+ }
347
+
348
+ /**
349
+ * Low-level helper for request({ method, params }) providers.
350
+ */
351
+ export async function requestWithTimeout(provider, method, params, timeoutMs) {
352
+ if (!isRequestProvider(provider)) {
353
+ throw new Error("Wind Wallet injected provider is not available.");
354
+ }
355
+
356
+ const req = provider.request({
357
+ method: String(method || ""),
358
+ params: normalizeParams(params),
359
+ });
360
+
361
+ return await withTimeout(Promise.resolve(req), timeoutMs, method);
362
+ }
363
+
364
+ async function withTimeout(promise, timeoutMs, label = "request") {
365
+ const ms = normalizeTimeoutMs(timeoutMs, DEFAULT_TIMEOUT_MS);
366
+
367
+ return await new Promise((resolve, reject) => {
368
+ const timer = setTimer(() => {
369
+ reject(new Error(`${label} timed out.`));
370
+ }, ms);
371
+
372
+ promise.then(
373
+ (value) => {
374
+ clearTimer(timer);
375
+ resolve(value);
376
+ },
377
+ (error) => {
378
+ clearTimer(timer);
379
+ reject(error);
380
+ }
381
+ );
382
+ });
383
+ }
384
+
385
+ /**
386
+ * WindClient
387
+ * Hybrid connector for DApps.
388
+ *
389
+ * connect() chooses:
390
+ * 1. Injected provider when opened inside Wind Wallet browser.
391
+ * 2. QR/VSR + PeerJS fallback when opened in a normal browser.
392
+ */
393
+ export class WindClient {
394
+ #connector;
395
+ #listeners = new Map();
396
+ #options;
397
+
398
+ constructor(options = {}) {
399
+ this.#options = { preferInjected: true, ...options };
400
+ }
401
+
402
+ on(event, listener) {
403
+ this.#listeners.set(String(event || ""), listener);
404
+ if (this.#connector && typeof this.#connector.on === "function") {
405
+ this.#connector.on(event, listener);
406
+ }
407
+ }
408
+
409
+ off(event) {
410
+ this.#listeners.delete(String(event || ""));
411
+ if (this.#connector && typeof this.#connector.off === "function") {
412
+ this.#connector.off(event);
413
+ }
414
+ }
415
+
416
+ get connector() {
417
+ return this.#connector;
418
+ }
419
+
420
+ isInjectedAvailable() {
421
+ return isWindWalletInjected();
422
+ }
423
+
424
+ /**
425
+ * Connect to Wind Wallet.
426
+ *
427
+ * @param {{
428
+ * name?: string,
429
+ * icon?: string,
430
+ * preferInjected?: boolean,
431
+ * timeoutMs?: number,
432
+ * walletUrl?: string,
433
+ * openWallet?: (vsr:string, connector:WindConnector)=>void,
434
+ * onLoginRequest?: (vsr:string, connector:WindConnector)=>void,
435
+ * peerOptions?: any
436
+ * }=} options
437
+ *
438
+ * For non-injected fallback, pass onLoginRequest(vsr) to show QR/modal.
439
+ */
440
+ async connect(options = {}) {
441
+ const opts = { ...this.#options, ...options };
442
+
443
+ if (opts.preferInjected !== false && isWindWalletInjected()) {
444
+ const session = await InjectedWalletSession.connect({ timeoutMs: opts.timeoutMs });
445
+ this.#listeners.get("session")?.(session, { transport: "injected" });
446
+ return session;
447
+ }
448
+
449
+ const connector = new WindConnector(opts.peerOptions);
450
+ this.#connector = connector;
451
+
452
+ for (const [event, listener] of this.#listeners.entries()) {
453
+ connector.on(event, listener);
454
+ }
455
+
456
+ await connector.connect();
457
+ const vsr = connector.createLoginRequest(opts.name || "Wind DApp", opts.icon || "");
458
+
459
+ return await new Promise((resolve, reject) => {
460
+ const cleanup = () => {
461
+ clearTimer(timeout);
462
+ };
463
+
464
+ const timeout = setTimer(() => {
465
+ cleanup();
466
+ const err = new Error("Wind Wallet peer login timed out.");
467
+ err.vsr = vsr;
468
+ reject(err);
469
+ }, normalizeTimeoutMs(opts.timeoutMs, DEFAULT_TIMEOUT_MS));
470
+
471
+ connector.on("session", (session, proof) => {
472
+ cleanup();
473
+ this.#listeners.get("session")?.(session, proof);
474
+ resolve(session);
475
+ });
476
+
477
+ connector.on("error", (error) => {
478
+ this.#listeners.get("error")?.(error);
479
+ });
480
+
481
+ if (typeof opts.onLoginRequest === "function") {
482
+ opts.onLoginRequest(vsr, connector);
483
+ return;
484
+ }
485
+
486
+ if (typeof opts.openWallet === "function") {
487
+ opts.openWallet(vsr, connector);
488
+ return;
489
+ }
490
+
491
+ if (opts.walletUrl && typeof globalThis?.open === "function") {
492
+ const payload = vsr.startsWith("vsr:") ? vsr.slice(4) : vsr;
493
+ const url = `${String(opts.walletUrl).replace(/\/$/, "")}/login?vsr=${encodeURIComponent(payload)}`;
494
+ try {
495
+ globalThis.open(url, "Wind Wallet");
496
+ } catch {}
497
+ return;
498
+ }
499
+
500
+ // No UI handler was provided. Reject with the generated VSR so the DApp can render it.
501
+ const err = new Error(
502
+ "Wind Wallet injected provider not found. Show this VSR as QR or pass onLoginRequest(vsr)."
503
+ );
504
+ err.vsr = vsr;
505
+ err.connector = connector;
506
+ cleanup();
507
+ reject(err);
508
+ });
509
+ }
510
+ }
511
+
512
+ /**
513
+ * Convenience helper.
514
+ */
515
+ export async function connectWindWallet(options = {}) {
516
+ const client = new WindClient(options);
517
+ return await client.connect(options);
518
+ }