windkit 0.2.1 → 0.2.3
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 +91 -0
- package/README.md +265 -46
- package/index.js +3 -0
- package/package.json +39 -41
- package/src/StoreSession.js +103 -0
- package/src/WalletSession.js +375 -0
- package/src/WindConnector.js +294 -0
- package/dist/index.cjs +0 -365
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -70
- package/dist/index.d.ts +0 -70
- package/dist/index.js +0 -342
- package/dist/index.js.map +0 -1
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
// windkit/WalletSession.js
|
|
2
|
+
// Ultra Clean++ (single replace)
|
|
3
|
+
// ✅ fixes peerID case bug in ACTIVE_ACCOUNT_CHANGED
|
|
4
|
+
// ✅ bounded ping heartbeat (jitter) for low-end
|
|
5
|
+
// ✅ strict request mapping + timeout cleanup
|
|
6
|
+
// ✅ minimal surface: transact/signRequest/signMessage/sharedSecret
|
|
7
|
+
|
|
8
|
+
import { Base64u, IdentityProof, SigningRequest } from "@wharfkit/signing-request";
|
|
9
|
+
import {
|
|
10
|
+
Checksum512,
|
|
11
|
+
Name,
|
|
12
|
+
PermissionLevel,
|
|
13
|
+
PublicKey,
|
|
14
|
+
Serializer,
|
|
15
|
+
Signature,
|
|
16
|
+
SignedTransaction,
|
|
17
|
+
} from "@wharfkit/antelope";
|
|
18
|
+
|
|
19
|
+
import zlib from "pako";
|
|
20
|
+
import { loadSession, saveSession } from "./StoreSession.js";
|
|
21
|
+
|
|
22
|
+
const DEFAULT_TIMEOUT_MS = 90_000;
|
|
23
|
+
const PING_MS = 15_000;
|
|
24
|
+
const PING_JITTER_MS = 2_000;
|
|
25
|
+
|
|
26
|
+
function uuid() {
|
|
27
|
+
try {
|
|
28
|
+
if (globalThis?.crypto?.randomUUID) return globalThis.crypto.randomUUID();
|
|
29
|
+
} catch {}
|
|
30
|
+
return `id-${Math.random().toString(16).slice(2)}-${Date.now().toString(16)}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function setTimer(fn, ms) {
|
|
34
|
+
try {
|
|
35
|
+
return globalThis.setTimeout(fn, ms);
|
|
36
|
+
} catch {
|
|
37
|
+
return setTimeout(fn, ms);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function clearTimer(id) {
|
|
42
|
+
try {
|
|
43
|
+
globalThis.clearTimeout(id);
|
|
44
|
+
} catch {
|
|
45
|
+
clearTimeout(id);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function normalizeTimeoutMs(v, fallback = DEFAULT_TIMEOUT_MS) {
|
|
50
|
+
const n = Number(v);
|
|
51
|
+
if (!Number.isFinite(n)) return fallback;
|
|
52
|
+
return Math.max(1000, Math.trunc(n));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function errorFromReply(reply, fallback) {
|
|
56
|
+
const err = reply?.error;
|
|
57
|
+
if (typeof err === "string") return new Error(err);
|
|
58
|
+
if (err?.message) return new Error(err.message);
|
|
59
|
+
return new Error(fallback || "Request rejected.");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* WalletSession
|
|
64
|
+
* - Wraps a PeerJS DataConnection (DApp->Wallet)
|
|
65
|
+
* - Sends requests to wallet and maps replies by id
|
|
66
|
+
*/
|
|
67
|
+
export class WalletSession {
|
|
68
|
+
static ChainID = "f9f432b1851b5c179d2091a96f593aaed50ec7466b74f89301f957a83e56ce1f";
|
|
69
|
+
|
|
70
|
+
/** @type {import("peerjs").DataConnection} */
|
|
71
|
+
#connection;
|
|
72
|
+
|
|
73
|
+
/** @type {Map<string, {resolve:Function, reject:Function, timeout:any}>} */
|
|
74
|
+
#pending = new Map();
|
|
75
|
+
|
|
76
|
+
/** @type {{zlib:any, abiProvider?:any} | undefined} */
|
|
77
|
+
#encodingOptions;
|
|
78
|
+
|
|
79
|
+
/** @type {PermissionLevel | undefined} */
|
|
80
|
+
#permissionLevel;
|
|
81
|
+
|
|
82
|
+
/** @type {(permission: PermissionLevel) => void | undefined} */
|
|
83
|
+
#accountChangeListener;
|
|
84
|
+
|
|
85
|
+
/** @type {() => void | undefined} */
|
|
86
|
+
#closeListener;
|
|
87
|
+
|
|
88
|
+
/** @type {(error: Error) => void | undefined} */
|
|
89
|
+
#errorListener;
|
|
90
|
+
|
|
91
|
+
/** @type {any} */
|
|
92
|
+
#pingTimer = null;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @param {import("peerjs").DataConnection} connection
|
|
96
|
+
*/
|
|
97
|
+
constructor(connection) {
|
|
98
|
+
this.#connection = connection;
|
|
99
|
+
|
|
100
|
+
connection.on("data", (msg) => this.#onDataReceived(msg));
|
|
101
|
+
|
|
102
|
+
connection.on("close", () => {
|
|
103
|
+
this.#stopPing();
|
|
104
|
+
|
|
105
|
+
for (const [id, p] of this.#pending.entries()) {
|
|
106
|
+
try {
|
|
107
|
+
clearTimer(p.timeout);
|
|
108
|
+
} catch {}
|
|
109
|
+
try {
|
|
110
|
+
p.reject(new Error("Wallet connection closed."));
|
|
111
|
+
} catch {}
|
|
112
|
+
this.#pending.delete(id);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (this.#closeListener) this.#closeListener();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
connection.on("error", (error) => {
|
|
119
|
+
if (this.#errorListener) this.#errorListener(error);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
this.#startPing();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Optional ABI cache provider (recommended when creating requests).
|
|
127
|
+
* @param {import("@wharfkit/abicache").ABICache} cache
|
|
128
|
+
*/
|
|
129
|
+
setABICache(cache) {
|
|
130
|
+
this.#encodingOptions = { zlib, abiProvider: cache };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
onAccountChange(listener) {
|
|
134
|
+
this.#accountChangeListener = listener;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
onClose(listener) {
|
|
138
|
+
this.#closeListener = listener;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
onError(listener) {
|
|
142
|
+
this.#errorListener = listener;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
isOpen() {
|
|
146
|
+
return Boolean(this.#connection?.open);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
close() {
|
|
150
|
+
this.#stopPing();
|
|
151
|
+
try {
|
|
152
|
+
this.#connection.close();
|
|
153
|
+
} catch {}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
metadata() {
|
|
157
|
+
return this.#connection.metadata;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
get permissionLevel() {
|
|
161
|
+
return this.#permissionLevel;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
set permissionLevel(value) {
|
|
165
|
+
this.#permissionLevel = value;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
get actor() {
|
|
169
|
+
return this.#permissionLevel?.actor ?? Name.from("");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
get permission() {
|
|
173
|
+
return this.#permissionLevel?.permission ?? Name.from("");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* @typedef {Object} TransactArguments
|
|
178
|
+
* @property {import("@wharfkit/antelope").Action=} action
|
|
179
|
+
* @property {Array<import("@wharfkit/antelope").Action>=} actions
|
|
180
|
+
* @property {import("@wharfkit/antelope").Transaction=} transaction
|
|
181
|
+
*/
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* @typedef {Object} TransactOptions
|
|
185
|
+
* @property {boolean=} broadcast True to broadcast, false for sign-only.
|
|
186
|
+
* @property {number=} timeoutMs Per-request timeout (default 90s).
|
|
187
|
+
*/
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Create a VSR signing request for a transaction and send to wallet.
|
|
191
|
+
* @param {TransactArguments} args
|
|
192
|
+
* @param {TransactOptions=} options
|
|
193
|
+
* @returns {Promise<SignedTransaction|any>} SignedTransaction (sign-only) OR wallet push result (broadcast)
|
|
194
|
+
*/
|
|
195
|
+
async transact(args, options) {
|
|
196
|
+
const willBroadcast = typeof options?.broadcast === "boolean" ? options.broadcast : true;
|
|
197
|
+
|
|
198
|
+
const requestArgs = { ...args, chainId: WalletSession.ChainID };
|
|
199
|
+
const req = await SigningRequest.create(requestArgs, this.#encodingOptions);
|
|
200
|
+
req.setBroadcast(willBroadcast);
|
|
201
|
+
|
|
202
|
+
const vsr = req.encode(true, false, "vsr:");
|
|
203
|
+
return this.signRequest(vsr, options?.timeoutMs);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Send a signing request string to the wallet.
|
|
208
|
+
* @param {string} vsr
|
|
209
|
+
* @param {number=} timeoutMs
|
|
210
|
+
* @returns {Promise<any|SignedTransaction>}
|
|
211
|
+
*/
|
|
212
|
+
signRequest(vsr, timeoutMs) {
|
|
213
|
+
return this.#request("signRequest", { vsr }, timeoutMs, (reply) => {
|
|
214
|
+
if (reply?.code === "SENT") return reply.result;
|
|
215
|
+
if (reply?.code === "SIGNED") return SignedTransaction.from(reply.result);
|
|
216
|
+
throw errorFromReply(reply, "Signing request rejected.");
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Ask wallet to sign an arbitrary message.
|
|
222
|
+
* @param {string} message
|
|
223
|
+
* @param {number=} timeoutMs
|
|
224
|
+
* @returns {Promise<Signature>}
|
|
225
|
+
*/
|
|
226
|
+
signMessage(message, timeoutMs) {
|
|
227
|
+
return this.#request("signMessage", { message }, timeoutMs, (reply) => {
|
|
228
|
+
if (reply?.code === "SIGNED") return Signature.from(reply.result.signature);
|
|
229
|
+
throw errorFromReply(reply, "Message signing rejected.");
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Ask wallet to derive a shared secret from a public key.
|
|
235
|
+
* @param {PublicKey} publicKey
|
|
236
|
+
* @param {number=} timeoutMs
|
|
237
|
+
* @returns {Promise<Checksum512>}
|
|
238
|
+
*/
|
|
239
|
+
sharedSecret(publicKey, timeoutMs) {
|
|
240
|
+
return this.#request("sharedSecret", { key: publicKey.toString() }, timeoutMs, (reply) => {
|
|
241
|
+
if (reply?.code === "CREATED") return Checksum512.from(reply.result.secret);
|
|
242
|
+
throw errorFromReply(reply, "Shared secret creation failed.");
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Internal: handle ACTIVE_ACCOUNT_CHANGED.
|
|
248
|
+
* @param {string} auth
|
|
249
|
+
*/
|
|
250
|
+
#onChangeAccount(auth) {
|
|
251
|
+
const proof = Serializer.decode({ data: Base64u.decode(auth), type: IdentityProof });
|
|
252
|
+
this.permissionLevel = proof.signer;
|
|
253
|
+
|
|
254
|
+
const session = loadSession();
|
|
255
|
+
if (session) {
|
|
256
|
+
// ✅ FIX: property is peerID (case-sensitive)
|
|
257
|
+
saveSession({
|
|
258
|
+
peerID: session.peerID,
|
|
259
|
+
permission: proof.signer.toString(),
|
|
260
|
+
expiration: proof.expiration,
|
|
261
|
+
auth,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (this.#accountChangeListener) this.#accountChangeListener(proof.signer);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
#startPing() {
|
|
269
|
+
if (this.#pingTimer) return;
|
|
270
|
+
|
|
271
|
+
const loop = () => {
|
|
272
|
+
if (!this.isOpen()) return;
|
|
273
|
+
|
|
274
|
+
const jitter = Math.floor(Math.random() * PING_JITTER_MS);
|
|
275
|
+
this.#pingTimer = setTimer(() => {
|
|
276
|
+
this.#ping().catch(() => {});
|
|
277
|
+
loop();
|
|
278
|
+
}, PING_MS + jitter);
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
loop();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
#stopPing() {
|
|
285
|
+
if (this.#pingTimer != null) {
|
|
286
|
+
clearTimer(this.#pingTimer);
|
|
287
|
+
this.#pingTimer = null;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async #ping() {
|
|
292
|
+
if (!this.isOpen()) return;
|
|
293
|
+
try {
|
|
294
|
+
this.#connection.send({ method: "ping", id: uuid(), params: { time: Date.now() } });
|
|
295
|
+
} catch {
|
|
296
|
+
// best-effort only
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Core request helper with timeout + reply mapping.
|
|
302
|
+
* @template T
|
|
303
|
+
* @param {string} method
|
|
304
|
+
* @param {any} params
|
|
305
|
+
* @param {number=} timeoutMs
|
|
306
|
+
* @param {(reply:any)=>T} mapper
|
|
307
|
+
* @returns {Promise<T>}
|
|
308
|
+
*/
|
|
309
|
+
#request(method, params, timeoutMs, mapper) {
|
|
310
|
+
if (!this.isOpen()) return Promise.reject(new Error("Wallet connection is not open."));
|
|
311
|
+
|
|
312
|
+
const id = uuid();
|
|
313
|
+
const ms = normalizeTimeoutMs(timeoutMs, DEFAULT_TIMEOUT_MS);
|
|
314
|
+
|
|
315
|
+
return new Promise((resolve, reject) => {
|
|
316
|
+
const timeout = setTimer(() => {
|
|
317
|
+
this.#pending.delete(id);
|
|
318
|
+
reject(new Error(`${method} timed out.`));
|
|
319
|
+
}, ms);
|
|
320
|
+
|
|
321
|
+
this.#pending.set(id, {
|
|
322
|
+
timeout,
|
|
323
|
+
resolve: (reply) => {
|
|
324
|
+
try {
|
|
325
|
+
const out = mapper(reply);
|
|
326
|
+
clearTimer(timeout);
|
|
327
|
+
this.#pending.delete(id);
|
|
328
|
+
resolve(out);
|
|
329
|
+
} catch (e) {
|
|
330
|
+
clearTimer(timeout);
|
|
331
|
+
this.#pending.delete(id);
|
|
332
|
+
reject(e);
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
reject: (e) => {
|
|
336
|
+
clearTimer(timeout);
|
|
337
|
+
this.#pending.delete(id);
|
|
338
|
+
reject(e);
|
|
339
|
+
},
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
try {
|
|
343
|
+
this.#connection.send({ method, id, params });
|
|
344
|
+
} catch (e) {
|
|
345
|
+
clearTimer(timeout);
|
|
346
|
+
this.#pending.delete(id);
|
|
347
|
+
reject(e);
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Handle incoming messages from wallet.
|
|
354
|
+
* @param {any} data
|
|
355
|
+
*/
|
|
356
|
+
#onDataReceived(data) {
|
|
357
|
+
if (!data || typeof data !== "object") return;
|
|
358
|
+
|
|
359
|
+
// Reply to pending request
|
|
360
|
+
if (data.id && this.#pending.has(data.id)) {
|
|
361
|
+
const p = this.#pending.get(data.id);
|
|
362
|
+
try {
|
|
363
|
+
p.resolve(data);
|
|
364
|
+
} catch (e) {
|
|
365
|
+
p.reject(e);
|
|
366
|
+
}
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Wallet push notifications
|
|
371
|
+
if (data.code === "ACTIVE_ACCOUNT_CHANGED" && data?.result?.auth) {
|
|
372
|
+
this.#onChangeAccount(data.result.auth);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
// windkit/WindConnector.js
|
|
2
|
+
// Ultra Clean++ (single replace)
|
|
3
|
+
// ✅ PeerID embedded into VSR (infoKey "pi")
|
|
4
|
+
// ✅ connect() reuses stored peerID (stable pairing)
|
|
5
|
+
// ✅ LOGIN_OK → decode IdentityProof → yield WalletSession
|
|
6
|
+
// ✅ low-end friendly: minimal work, no polling loops
|
|
7
|
+
|
|
8
|
+
import { Base64u, IdentityProof, SigningRequest } from "@wharfkit/signing-request";
|
|
9
|
+
import { Serializer } from "@wharfkit/antelope";
|
|
10
|
+
import Peer from "peerjs";
|
|
11
|
+
import zlib from "pako";
|
|
12
|
+
|
|
13
|
+
import { WalletSession } from "./WalletSession.js";
|
|
14
|
+
import { loadSession, saveSession, clearSession } from "./StoreSession.js";
|
|
15
|
+
|
|
16
|
+
function uuid() {
|
|
17
|
+
try {
|
|
18
|
+
if (globalThis?.crypto?.randomUUID) return globalThis.crypto.randomUUID();
|
|
19
|
+
} catch {}
|
|
20
|
+
return `id-${Math.random().toString(16).slice(2)}-${Date.now().toString(16)}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function originSafe() {
|
|
24
|
+
try {
|
|
25
|
+
return globalThis?.location?.origin || "";
|
|
26
|
+
} catch {
|
|
27
|
+
return "";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function defaultPeerConfig() {
|
|
32
|
+
return {
|
|
33
|
+
iceServers: [
|
|
34
|
+
{ urls: "stun:stun.l.google.com:19302" },
|
|
35
|
+
{ urls: "stun:stun1.l.google.com:3478" },
|
|
36
|
+
{ urls: "stun:stun.relay.metered.ca:80" },
|
|
37
|
+
{
|
|
38
|
+
urls: "turn:asia.relay.metered.ca:80",
|
|
39
|
+
username: "b66cd40a117bddb5cde924ab",
|
|
40
|
+
credential: "4jRmuTehVCZ2a/S+",
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
sdpSemantics: "unified-plan",
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function mergePeerOptions(userOptions) {
|
|
48
|
+
const opts = userOptions ? { ...userOptions } : {};
|
|
49
|
+
const base = defaultPeerConfig();
|
|
50
|
+
|
|
51
|
+
// Keep base config but allow user overrides
|
|
52
|
+
if (!opts.config) {
|
|
53
|
+
opts.config = base;
|
|
54
|
+
return opts;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const userCfg = { ...opts.config };
|
|
58
|
+
const userIce = Array.isArray(userCfg.iceServers) ? userCfg.iceServers : null;
|
|
59
|
+
|
|
60
|
+
opts.config = {
|
|
61
|
+
...base,
|
|
62
|
+
...userCfg,
|
|
63
|
+
iceServers: userIce ? [...base.iceServers, ...userIce] : base.iceServers,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return opts;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* WindConnector (DApp-side)
|
|
71
|
+
* - Owns PeerJS instance (DApp is server peer)
|
|
72
|
+
* - Creates VSR identity login request
|
|
73
|
+
* - Waits for wallet connection + LOGIN_OK, then yields WalletSession
|
|
74
|
+
*/
|
|
75
|
+
export class WindConnector {
|
|
76
|
+
/** @type {Peer | null} */
|
|
77
|
+
#peer = null;
|
|
78
|
+
|
|
79
|
+
/** @type {import("peerjs").PeerJSOption} */
|
|
80
|
+
#peerOptions;
|
|
81
|
+
|
|
82
|
+
/** @type {string | null} */
|
|
83
|
+
#peerId = null;
|
|
84
|
+
|
|
85
|
+
/** @type {Map<string, Function>} */
|
|
86
|
+
#listeners = new Map();
|
|
87
|
+
|
|
88
|
+
/** @type {any} */
|
|
89
|
+
#identityArgs;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @param {import("peerjs").PeerJSOption=} options Optional PeerJS options.
|
|
93
|
+
*/
|
|
94
|
+
constructor(options) {
|
|
95
|
+
this.#peerOptions = mergePeerOptions(options);
|
|
96
|
+
|
|
97
|
+
this.#identityArgs = {
|
|
98
|
+
scope: "vexanium",
|
|
99
|
+
chainId: WalletSession.ChainID,
|
|
100
|
+
callback: originSafe(),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Add extra STUN/TURN server.
|
|
106
|
+
* @param {RTCIceServer} server
|
|
107
|
+
*/
|
|
108
|
+
addIceServer(server) {
|
|
109
|
+
if (!this.#peerOptions.config) this.#peerOptions.config = { iceServers: [] };
|
|
110
|
+
if (!Array.isArray(this.#peerOptions.config.iceServers)) this.#peerOptions.config.iceServers = [];
|
|
111
|
+
this.#peerOptions.config.iceServers.push(server);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Set PeerJS signaling server (optional).
|
|
116
|
+
* @param {string} host
|
|
117
|
+
* @param {number=} port
|
|
118
|
+
* @param {string=} path
|
|
119
|
+
* @param {boolean=} secure
|
|
120
|
+
*/
|
|
121
|
+
setServer(host, port, path, secure) {
|
|
122
|
+
this.#peerOptions.host = host;
|
|
123
|
+
if (typeof port === "number") this.#peerOptions.port = port;
|
|
124
|
+
if (typeof path === "string") this.#peerOptions.path = path;
|
|
125
|
+
if (typeof secure === "boolean") this.#peerOptions.secure = secure;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Subscribe to events.
|
|
130
|
+
* Supported: "open", "close", "disconnected", "error", "connection", "session"
|
|
131
|
+
* @param {string} event
|
|
132
|
+
* @param {Function} func
|
|
133
|
+
*/
|
|
134
|
+
on(event, func) {
|
|
135
|
+
this.#listeners.set(String(event || ""), func);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Unsubscribe.
|
|
140
|
+
* @param {string} event
|
|
141
|
+
*/
|
|
142
|
+
off(event) {
|
|
143
|
+
this.#listeners.delete(String(event || ""));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Connect to signaling server. Resolves when Peer is "open".
|
|
148
|
+
* @returns {Promise<string>} peerId
|
|
149
|
+
*/
|
|
150
|
+
async connect() {
|
|
151
|
+
if (this.#peer && !this.#peer.destroyed) return this.#peer.id;
|
|
152
|
+
|
|
153
|
+
const stored = loadSession();
|
|
154
|
+
this.#peerId = stored?.peerID || this.#peerId || `VEX-${uuid()}`;
|
|
155
|
+
|
|
156
|
+
const peer = new Peer(this.#peerId, this.#peerOptions);
|
|
157
|
+
this.#peer = peer;
|
|
158
|
+
|
|
159
|
+
peer.on("connection", (conn) => this.#onConnection(conn));
|
|
160
|
+
|
|
161
|
+
// Bridge PeerJS events to our listeners (except "session" which is internal)
|
|
162
|
+
for (const [key, fn] of this.#listeners.entries()) {
|
|
163
|
+
if (key === "session") continue;
|
|
164
|
+
try {
|
|
165
|
+
peer.on(key, fn);
|
|
166
|
+
} catch {}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return await new Promise((resolve, reject) => {
|
|
170
|
+
const onOpen = (id) => {
|
|
171
|
+
try {
|
|
172
|
+
peer.off("error", onError);
|
|
173
|
+
} catch {}
|
|
174
|
+
resolve(id);
|
|
175
|
+
};
|
|
176
|
+
const onError = (err) => {
|
|
177
|
+
try {
|
|
178
|
+
peer.off("open", onOpen);
|
|
179
|
+
} catch {}
|
|
180
|
+
reject(err);
|
|
181
|
+
};
|
|
182
|
+
peer.once("open", onOpen);
|
|
183
|
+
peer.once("error", onError);
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
disconnect() {
|
|
188
|
+
try {
|
|
189
|
+
this.#peer?.disconnect();
|
|
190
|
+
} catch {}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
destroy() {
|
|
194
|
+
try {
|
|
195
|
+
this.#peer?.destroy();
|
|
196
|
+
} catch {}
|
|
197
|
+
this.#peer = null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
reconnect() {
|
|
201
|
+
try {
|
|
202
|
+
this.#peer?.reconnect();
|
|
203
|
+
} catch {}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
isDisconnected() {
|
|
207
|
+
return this.#peer ? this.#peer.disconnected : true;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
isDestroyed() {
|
|
211
|
+
return this.#peer ? this.#peer.destroyed : true;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Create VSR identity request for login.
|
|
216
|
+
* Put this string into:
|
|
217
|
+
* - QR payload, or
|
|
218
|
+
* - wallet login URL query (?vsr=...)
|
|
219
|
+
*
|
|
220
|
+
* @param {string} name App name
|
|
221
|
+
* @param {string=} icon App icon URL
|
|
222
|
+
* @returns {string} "vsr:...."
|
|
223
|
+
*/
|
|
224
|
+
createLoginRequest(name, icon) {
|
|
225
|
+
const session = loadSession();
|
|
226
|
+
|
|
227
|
+
if (session) {
|
|
228
|
+
// reuse peer id + identity hints
|
|
229
|
+
this.#peerId = session.peerID;
|
|
230
|
+
|
|
231
|
+
const [actor, perm] = String(session.permission).split("@");
|
|
232
|
+
this.#identityArgs.account = actor;
|
|
233
|
+
this.#identityArgs.permission = perm;
|
|
234
|
+
} else {
|
|
235
|
+
clearSession();
|
|
236
|
+
this.#peerId = `VEX-${uuid()}`;
|
|
237
|
+
delete this.#identityArgs.account;
|
|
238
|
+
delete this.#identityArgs.permission;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const req = SigningRequest.identity(this.#identityArgs, { zlib });
|
|
242
|
+
|
|
243
|
+
// Compact app info keys
|
|
244
|
+
req.setInfoKey("pi", this.#peerId); // peer id
|
|
245
|
+
req.setInfoKey("na", String(name || "")); // app name
|
|
246
|
+
if (icon) req.setInfoKey("ic", String(icon)); // icon
|
|
247
|
+
req.setInfoKey("do", originSafe()); // dapp origin
|
|
248
|
+
|
|
249
|
+
// If a stored auth proof exists, pass it (wallet may optimize)
|
|
250
|
+
if (session?.auth) req.setInfoKey("auth", session.auth);
|
|
251
|
+
|
|
252
|
+
return req.encode(true, false, "vsr:");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* @param {import("peerjs").DataConnection} conn
|
|
257
|
+
*/
|
|
258
|
+
#onConnection(conn) {
|
|
259
|
+
const onConn = this.#listeners.get("connection");
|
|
260
|
+
if (onConn) onConn(conn);
|
|
261
|
+
|
|
262
|
+
// Expect LOGIN_OK first (one-shot)
|
|
263
|
+
conn.once("data", (payload) => {
|
|
264
|
+
if (!payload || typeof payload !== "object") return;
|
|
265
|
+
if (payload.code !== "LOGIN_OK") return;
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
const auth = payload?.result?.auth;
|
|
269
|
+
if (!auth || typeof auth !== "string") return;
|
|
270
|
+
|
|
271
|
+
const proof = Serializer.decode({
|
|
272
|
+
data: Base64u.decode(auth),
|
|
273
|
+
type: IdentityProof,
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const session = new WalletSession(conn);
|
|
277
|
+
session.permissionLevel = proof.signer;
|
|
278
|
+
|
|
279
|
+
saveSession({
|
|
280
|
+
peerID: this.#peerId || conn.peer,
|
|
281
|
+
permission: proof.signer.toString(),
|
|
282
|
+
expiration: proof.expiration,
|
|
283
|
+
auth,
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
const onSession = this.#listeners.get("session");
|
|
287
|
+
if (onSession) onSession(session, proof);
|
|
288
|
+
} catch (e) {
|
|
289
|
+
const onError = this.#listeners.get("error");
|
|
290
|
+
if (onError) onError(e);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|