windkit 0.2.3 → 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,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0
4
+
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
+
3
13
  All notable changes to this project are documented in this file.
4
14
 
5
15
  This project follows [Semantic Versioning](https://semver.org/).
package/README.md CHANGED
@@ -24,6 +24,85 @@ Designed for production environments, including low-RAM mobile devices.
24
24
 
25
25
  ---
26
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.
103
+
104
+ ---
105
+
27
106
  ## 📦 Installation
28
107
 
29
108
  ```bash
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.3",
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
+ }