@qrlwallet/connect 0.1.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/LICENSE +21 -0
- package/README.md +247 -0
- package/dist/index.d.mts +409 -0
- package/dist/index.d.ts +409 -0
- package/dist/index.js +1288 -0
- package/dist/index.mjs +1236 -0
- package/package.json +70 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1236 @@
|
|
|
1
|
+
// src/QRLConnectProvider.ts
|
|
2
|
+
import EventEmitter4 from "eventemitter3";
|
|
3
|
+
|
|
4
|
+
// src/ConnectionManager.ts
|
|
5
|
+
import EventEmitter3 from "eventemitter3";
|
|
6
|
+
import { v4 as uuidv4 } from "uuid";
|
|
7
|
+
|
|
8
|
+
// src/ECIESClient.ts
|
|
9
|
+
import { PrivateKey, decrypt, encrypt } from "eciesjs";
|
|
10
|
+
|
|
11
|
+
// src/utils/logger.ts
|
|
12
|
+
var debugEnabled = false;
|
|
13
|
+
function setDebug(enabled) {
|
|
14
|
+
debugEnabled = enabled;
|
|
15
|
+
}
|
|
16
|
+
function log(tag, ...args) {
|
|
17
|
+
if (debugEnabled) {
|
|
18
|
+
console.log(`[QRLConnect:${tag}]`, ...args);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function warn(tag, ...args) {
|
|
22
|
+
console.warn(`[QRLConnect:${tag}]`, ...args);
|
|
23
|
+
}
|
|
24
|
+
function error(tag, ...args) {
|
|
25
|
+
console.error(`[QRLConnect:${tag}]`, ...args);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// src/ECIESClient.ts
|
|
29
|
+
function toBase64(bytes) {
|
|
30
|
+
let binary = "";
|
|
31
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
32
|
+
binary += String.fromCharCode(bytes[i]);
|
|
33
|
+
}
|
|
34
|
+
return btoa(binary);
|
|
35
|
+
}
|
|
36
|
+
function fromBase64(base64) {
|
|
37
|
+
const binary = atob(base64);
|
|
38
|
+
const bytes = new Uint8Array(binary.length);
|
|
39
|
+
for (let i = 0; i < binary.length; i++) {
|
|
40
|
+
bytes[i] = binary.charCodeAt(i);
|
|
41
|
+
}
|
|
42
|
+
return bytes;
|
|
43
|
+
}
|
|
44
|
+
var ECIESClient = class {
|
|
45
|
+
constructor(existingPrivateKey) {
|
|
46
|
+
if (existingPrivateKey) {
|
|
47
|
+
this.privateKey = PrivateKey.fromHex(existingPrivateKey);
|
|
48
|
+
log("ECIES", "Restored from existing private key");
|
|
49
|
+
} else {
|
|
50
|
+
this.privateKey = new PrivateKey();
|
|
51
|
+
log("ECIES", "Generated new ECIES keypair");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** Get public key as hex string */
|
|
55
|
+
getPublicKey() {
|
|
56
|
+
return this.privateKey.publicKey.toHex();
|
|
57
|
+
}
|
|
58
|
+
/** Get private key as hex string (for session persistence) */
|
|
59
|
+
getPrivateKeyHex() {
|
|
60
|
+
return this.privateKey.toHex();
|
|
61
|
+
}
|
|
62
|
+
/** Encrypt a string message for the given public key. Returns base64. */
|
|
63
|
+
encrypt(data, otherPublicKey) {
|
|
64
|
+
const encoded = new TextEncoder().encode(data);
|
|
65
|
+
const encrypted = encrypt(otherPublicKey, encoded);
|
|
66
|
+
return toBase64(encrypted);
|
|
67
|
+
}
|
|
68
|
+
/** Decrypt a base64-encoded message using our private key. */
|
|
69
|
+
decrypt(encryptedBase64) {
|
|
70
|
+
const encryptedBytes = fromBase64(encryptedBase64);
|
|
71
|
+
const decrypted = decrypt(this.privateKey.toHex(), encryptedBytes);
|
|
72
|
+
return new TextDecoder().decode(decrypted);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// src/KeyExchange.ts
|
|
77
|
+
import EventEmitter from "eventemitter3";
|
|
78
|
+
|
|
79
|
+
// src/config.ts
|
|
80
|
+
var DEFAULT_RELAY_URL = "https://qrlwallet.com";
|
|
81
|
+
var RELAY_PATH = "/relay";
|
|
82
|
+
var PROTOCOL_VERSION = 1;
|
|
83
|
+
var SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
84
|
+
var STORAGE_KEY_PREFIX = "@qrlwallet/connect";
|
|
85
|
+
var REQUEST_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
86
|
+
var WALLET_UNRESPONSIVE_MS = 30 * 1e3;
|
|
87
|
+
var RESTRICTED_METHODS = /* @__PURE__ */ new Set([
|
|
88
|
+
"qrl_requestAccounts",
|
|
89
|
+
"qrl_sendTransaction",
|
|
90
|
+
"qrl_signTransaction",
|
|
91
|
+
"qrl_sign",
|
|
92
|
+
"personal_sign",
|
|
93
|
+
"qrl_signTypedData",
|
|
94
|
+
"qrl_signTypedData_v3",
|
|
95
|
+
"qrl_signTypedData_v4",
|
|
96
|
+
"wallet_addQrlChain",
|
|
97
|
+
"wallet_switchQrlChain"
|
|
98
|
+
]);
|
|
99
|
+
var UNRESTRICTED_METHODS = /* @__PURE__ */ new Set([
|
|
100
|
+
"qrl_chainId",
|
|
101
|
+
"qrl_blockNumber",
|
|
102
|
+
"qrl_getBalance",
|
|
103
|
+
"qrl_getTransactionCount",
|
|
104
|
+
"qrl_getBlockByNumber",
|
|
105
|
+
"qrl_getBlockByHash",
|
|
106
|
+
"qrl_getTransactionByHash",
|
|
107
|
+
"qrl_getTransactionReceipt",
|
|
108
|
+
"qrl_call",
|
|
109
|
+
"qrl_estimateGas",
|
|
110
|
+
"qrl_gasPrice",
|
|
111
|
+
"qrl_getCode",
|
|
112
|
+
"qrl_getStorageAt",
|
|
113
|
+
"qrl_getLogs",
|
|
114
|
+
"qrl_getBlockTransactionCountByHash",
|
|
115
|
+
"qrl_getBlockTransactionCountByNumber",
|
|
116
|
+
"qrl_getTransactionByBlockHashAndIndex",
|
|
117
|
+
"qrl_getTransactionByBlockNumberAndIndex",
|
|
118
|
+
"qrl_accounts",
|
|
119
|
+
"net_version",
|
|
120
|
+
"net_listening",
|
|
121
|
+
"net_peerCount",
|
|
122
|
+
"web3_clientVersion",
|
|
123
|
+
"web3_sha3",
|
|
124
|
+
"qrl_syncing",
|
|
125
|
+
"qrl_coinbase",
|
|
126
|
+
"qrl_mining",
|
|
127
|
+
"qrl_hashrate",
|
|
128
|
+
"qrl_protocolVersion",
|
|
129
|
+
"qrl_getUncleCountByBlockHash",
|
|
130
|
+
"qrl_getUncleCountByBlockNumber",
|
|
131
|
+
"qrl_getUncleByBlockHashAndIndex",
|
|
132
|
+
"qrl_getUncleByBlockNumberAndIndex",
|
|
133
|
+
"qrl_getFilterChanges",
|
|
134
|
+
"qrl_getFilterLogs",
|
|
135
|
+
"qrl_newBlockFilter",
|
|
136
|
+
"qrl_newFilter",
|
|
137
|
+
"qrl_newPendingTransactionFilter",
|
|
138
|
+
"qrl_uninstallFilter"
|
|
139
|
+
]);
|
|
140
|
+
|
|
141
|
+
// src/types.ts
|
|
142
|
+
var KeyExchangeMessageType = /* @__PURE__ */ ((KeyExchangeMessageType2) => {
|
|
143
|
+
KeyExchangeMessageType2["SYN"] = "key_handshake_SYN";
|
|
144
|
+
KeyExchangeMessageType2["SYNACK"] = "key_handshake_SYNACK";
|
|
145
|
+
KeyExchangeMessageType2["ACK"] = "key_handshake_ACK";
|
|
146
|
+
return KeyExchangeMessageType2;
|
|
147
|
+
})(KeyExchangeMessageType || {});
|
|
148
|
+
var MessageType = /* @__PURE__ */ ((MessageType2) => {
|
|
149
|
+
MessageType2["KEY_EXCHANGE"] = "key_exchange";
|
|
150
|
+
MessageType2["JSONRPC"] = "jsonrpc";
|
|
151
|
+
MessageType2["WALLET_INFO"] = "wallet_info";
|
|
152
|
+
MessageType2["ORIGINATOR_INFO"] = "originator_info";
|
|
153
|
+
MessageType2["TERMINATE"] = "terminate";
|
|
154
|
+
MessageType2["PING"] = "ping";
|
|
155
|
+
MessageType2["READY"] = "ready";
|
|
156
|
+
return MessageType2;
|
|
157
|
+
})(MessageType || {});
|
|
158
|
+
var ConnectionStatus = /* @__PURE__ */ ((ConnectionStatus2) => {
|
|
159
|
+
ConnectionStatus2["DISCONNECTED"] = "disconnected";
|
|
160
|
+
ConnectionStatus2["CONNECTING"] = "connecting";
|
|
161
|
+
ConnectionStatus2["WAITING"] = "waiting";
|
|
162
|
+
ConnectionStatus2["KEY_EXCHANGE"] = "key_exchange";
|
|
163
|
+
ConnectionStatus2["CONNECTED"] = "connected";
|
|
164
|
+
ConnectionStatus2["RECONNECTING"] = "reconnecting";
|
|
165
|
+
return ConnectionStatus2;
|
|
166
|
+
})(ConnectionStatus || {});
|
|
167
|
+
|
|
168
|
+
// src/KeyExchange.ts
|
|
169
|
+
var KeyExchange = class extends EventEmitter {
|
|
170
|
+
constructor(ecies, isOriginator, otherPublicKey) {
|
|
171
|
+
super();
|
|
172
|
+
this.otherPublicKey = null;
|
|
173
|
+
this.keysExchanged = false;
|
|
174
|
+
this.awaitingSynAck = false;
|
|
175
|
+
this.awaitingAck = false;
|
|
176
|
+
this.step = "key_handshake_SYN" /* SYN */;
|
|
177
|
+
this.ecies = ecies;
|
|
178
|
+
this.isOriginator = isOriginator;
|
|
179
|
+
if (otherPublicKey) {
|
|
180
|
+
this.otherPublicKey = otherPublicKey;
|
|
181
|
+
this.keysExchanged = true;
|
|
182
|
+
log("KeyExchange", "Restored with existing other public key");
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/** Get the SYN message to start the handshake (originator/dApp side). */
|
|
186
|
+
createSYN() {
|
|
187
|
+
this.step = "key_handshake_SYN" /* SYN */;
|
|
188
|
+
this.awaitingSynAck = true;
|
|
189
|
+
this.emit("step_change", this.step);
|
|
190
|
+
return {
|
|
191
|
+
type: "key_handshake_SYN" /* SYN */,
|
|
192
|
+
pubkey: this.ecies.getPublicKey(),
|
|
193
|
+
v: PROTOCOL_VERSION
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Process an incoming key exchange message.
|
|
198
|
+
* Returns a response message to send back, or null if handshake is complete.
|
|
199
|
+
*/
|
|
200
|
+
onMessage(message) {
|
|
201
|
+
log("KeyExchange", `Received ${message.type}, isOriginator=${this.isOriginator}`);
|
|
202
|
+
switch (message.type) {
|
|
203
|
+
case "key_handshake_SYN" /* SYN */: {
|
|
204
|
+
if (this.isOriginator) {
|
|
205
|
+
warn("KeyExchange", "Originator received SYN - ignoring");
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
if (message.pubkey) {
|
|
209
|
+
this.otherPublicKey = message.pubkey;
|
|
210
|
+
}
|
|
211
|
+
this.step = "key_handshake_SYNACK" /* SYNACK */;
|
|
212
|
+
this.awaitingAck = true;
|
|
213
|
+
this.emit("step_change", this.step);
|
|
214
|
+
return {
|
|
215
|
+
type: "key_handshake_SYNACK" /* SYNACK */,
|
|
216
|
+
pubkey: this.ecies.getPublicKey(),
|
|
217
|
+
v: PROTOCOL_VERSION
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
case "key_handshake_SYNACK" /* SYNACK */: {
|
|
221
|
+
if (!this.isOriginator) {
|
|
222
|
+
warn("KeyExchange", "Non-originator received SYNACK - ignoring");
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
if (message.pubkey) {
|
|
226
|
+
this.otherPublicKey = message.pubkey;
|
|
227
|
+
}
|
|
228
|
+
if (!this.awaitingSynAck) {
|
|
229
|
+
return {
|
|
230
|
+
type: "key_handshake_ACK" /* ACK */,
|
|
231
|
+
v: PROTOCOL_VERSION
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
this.awaitingSynAck = false;
|
|
235
|
+
this.step = "key_handshake_ACK" /* ACK */;
|
|
236
|
+
if (!this.keysExchanged) {
|
|
237
|
+
this.keysExchanged = true;
|
|
238
|
+
this.emit("keys_exchanged");
|
|
239
|
+
}
|
|
240
|
+
this.emit("step_change", this.step);
|
|
241
|
+
return {
|
|
242
|
+
type: "key_handshake_ACK" /* ACK */,
|
|
243
|
+
v: PROTOCOL_VERSION
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
case "key_handshake_ACK" /* ACK */: {
|
|
247
|
+
if (this.isOriginator) {
|
|
248
|
+
warn("KeyExchange", "Originator received ACK - ignoring");
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
if (!this.awaitingAck) {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
this.awaitingAck = false;
|
|
255
|
+
if (!this.keysExchanged) {
|
|
256
|
+
this.keysExchanged = true;
|
|
257
|
+
this.emit("keys_exchanged");
|
|
258
|
+
}
|
|
259
|
+
log("KeyExchange", "Key exchange complete (wallet side)");
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
default:
|
|
263
|
+
warn("KeyExchange", `Unknown key exchange type: ${message.type}`);
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/** Encrypt a message for the counterparty. */
|
|
268
|
+
encryptMessage(data) {
|
|
269
|
+
if (!this.otherPublicKey) {
|
|
270
|
+
throw new Error("Cannot encrypt: key exchange not complete");
|
|
271
|
+
}
|
|
272
|
+
return this.ecies.encrypt(data, this.otherPublicKey);
|
|
273
|
+
}
|
|
274
|
+
/** Decrypt a message from the counterparty. */
|
|
275
|
+
decryptMessage(encryptedBase64) {
|
|
276
|
+
return this.ecies.decrypt(encryptedBase64);
|
|
277
|
+
}
|
|
278
|
+
/** Reset key exchange state for a fresh handshake (e.g. new QR code). */
|
|
279
|
+
reset() {
|
|
280
|
+
this.otherPublicKey = null;
|
|
281
|
+
this.keysExchanged = false;
|
|
282
|
+
this.awaitingSynAck = false;
|
|
283
|
+
this.awaitingAck = false;
|
|
284
|
+
this.step = "key_handshake_SYN" /* SYN */;
|
|
285
|
+
log("KeyExchange", "Reset for fresh handshake");
|
|
286
|
+
}
|
|
287
|
+
areKeysExchanged() {
|
|
288
|
+
return this.keysExchanged;
|
|
289
|
+
}
|
|
290
|
+
getOtherPublicKey() {
|
|
291
|
+
return this.otherPublicKey;
|
|
292
|
+
}
|
|
293
|
+
getCurrentStep() {
|
|
294
|
+
return this.step;
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
// src/SocketClient.ts
|
|
299
|
+
import { io } from "socket.io-client";
|
|
300
|
+
import EventEmitter2 from "eventemitter3";
|
|
301
|
+
var SocketClient = class extends EventEmitter2 {
|
|
302
|
+
constructor(relayUrl, clientType) {
|
|
303
|
+
super();
|
|
304
|
+
this.socket = null;
|
|
305
|
+
this.channelId = null;
|
|
306
|
+
this.seq = 0;
|
|
307
|
+
this.pendingJoin = null;
|
|
308
|
+
this.relayUrl = relayUrl;
|
|
309
|
+
this.clientType = clientType;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Connect to the relay server.
|
|
313
|
+
*/
|
|
314
|
+
connect() {
|
|
315
|
+
if (this.socket?.connected) return;
|
|
316
|
+
log("Socket", `Connecting to ${this.relayUrl}`);
|
|
317
|
+
this.socket = io(this.relayUrl, {
|
|
318
|
+
path: RELAY_PATH,
|
|
319
|
+
transports: ["websocket", "polling"],
|
|
320
|
+
reconnection: true,
|
|
321
|
+
reconnectionDelay: 1e3,
|
|
322
|
+
reconnectionDelayMax: 3e4,
|
|
323
|
+
reconnectionAttempts: Infinity,
|
|
324
|
+
timeout: 2e4
|
|
325
|
+
});
|
|
326
|
+
this.socket.on("connect", () => {
|
|
327
|
+
log("Socket", "Connected to relay");
|
|
328
|
+
this.emit("connected");
|
|
329
|
+
if (this.channelId) {
|
|
330
|
+
const reconnectChannelId = this.channelId;
|
|
331
|
+
this.joinChannelNow(reconnectChannelId).then((result) => {
|
|
332
|
+
if (this.pendingJoin && this.pendingJoin.channelId === reconnectChannelId) {
|
|
333
|
+
this.pendingJoin.resolve(result);
|
|
334
|
+
this.pendingJoin = null;
|
|
335
|
+
}
|
|
336
|
+
this.emit("reconnected");
|
|
337
|
+
}).catch((err) => {
|
|
338
|
+
const reconnectErr = err instanceof Error ? err : new Error(String(err));
|
|
339
|
+
if (this.pendingJoin && this.pendingJoin.channelId === reconnectChannelId) {
|
|
340
|
+
this.pendingJoin.reject(reconnectErr);
|
|
341
|
+
this.pendingJoin = null;
|
|
342
|
+
}
|
|
343
|
+
error("Socket", `Rejoin failed: ${reconnectErr.message}`);
|
|
344
|
+
this.emit("error", reconnectErr);
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
this.socket.on("disconnect", (reason) => {
|
|
349
|
+
log("Socket", `Disconnected: ${reason}`);
|
|
350
|
+
this.emit("disconnected", reason);
|
|
351
|
+
});
|
|
352
|
+
this.socket.on("message", (data) => {
|
|
353
|
+
log("Socket", `Message received in channel ${data.id}`);
|
|
354
|
+
this.emit("message", data);
|
|
355
|
+
});
|
|
356
|
+
this.socket.on("participants_changed", (data) => {
|
|
357
|
+
log("Socket", `Participants changed: ${data.event}`);
|
|
358
|
+
this.emit("participants_changed", data);
|
|
359
|
+
});
|
|
360
|
+
this.socket.on("connect_error", (err) => {
|
|
361
|
+
warn("Socket", `Connection error: ${err.message}`);
|
|
362
|
+
this.emit("error", err);
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Join a relay channel.
|
|
367
|
+
*/
|
|
368
|
+
async joinChannel(channelId) {
|
|
369
|
+
if (!this.socket) {
|
|
370
|
+
throw new Error("Socket not initialized. Call connect() before joinChannel()");
|
|
371
|
+
}
|
|
372
|
+
this.channelId = channelId;
|
|
373
|
+
if (this.socket.connected) {
|
|
374
|
+
return this.joinChannelNow(channelId);
|
|
375
|
+
}
|
|
376
|
+
if (this.pendingJoin) {
|
|
377
|
+
if (this.pendingJoin.channelId === channelId) {
|
|
378
|
+
return this.pendingJoin.promise;
|
|
379
|
+
}
|
|
380
|
+
this.pendingJoin.reject(new Error("Join request superseded by a newer channel"));
|
|
381
|
+
this.pendingJoin = null;
|
|
382
|
+
}
|
|
383
|
+
let resolvePending;
|
|
384
|
+
let rejectPending;
|
|
385
|
+
const promise = new Promise((resolve, reject) => {
|
|
386
|
+
resolvePending = resolve;
|
|
387
|
+
rejectPending = reject;
|
|
388
|
+
});
|
|
389
|
+
this.pendingJoin = {
|
|
390
|
+
channelId,
|
|
391
|
+
promise,
|
|
392
|
+
resolve: resolvePending,
|
|
393
|
+
reject: rejectPending
|
|
394
|
+
};
|
|
395
|
+
return promise;
|
|
396
|
+
}
|
|
397
|
+
joinChannelNow(channelId) {
|
|
398
|
+
return new Promise((resolve, reject) => {
|
|
399
|
+
if (!this.socket?.connected) {
|
|
400
|
+
reject(new Error("Socket not connected"));
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
this.socket.emit(
|
|
404
|
+
"join_channel",
|
|
405
|
+
{ channelId, clientType: this.clientType },
|
|
406
|
+
(response) => {
|
|
407
|
+
if (response.success) {
|
|
408
|
+
log("Socket", `Joined channel ${channelId}`);
|
|
409
|
+
resolve({ bufferedMessages: response.bufferedMessages || [] });
|
|
410
|
+
} else {
|
|
411
|
+
error("Socket", `Failed to join channel: ${response.error}`);
|
|
412
|
+
reject(new Error(response.error || "Failed to join channel"));
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
);
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Send a message through the relay.
|
|
420
|
+
*/
|
|
421
|
+
sendMessage(data) {
|
|
422
|
+
return new Promise((resolve, reject) => {
|
|
423
|
+
if (!this.socket?.connected) {
|
|
424
|
+
reject(new Error("Socket not connected"));
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
const dataWithSeq = { ...data, seq: this.seq++ };
|
|
428
|
+
this.socket.emit(
|
|
429
|
+
"message",
|
|
430
|
+
dataWithSeq,
|
|
431
|
+
(response) => {
|
|
432
|
+
if (response?.success) {
|
|
433
|
+
resolve({ success: true, buffered: response.buffered });
|
|
434
|
+
} else {
|
|
435
|
+
reject(new Error(response?.error || "Failed to send message"));
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
);
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Leave the current channel.
|
|
443
|
+
*/
|
|
444
|
+
leaveChannel() {
|
|
445
|
+
if (this.pendingJoin) {
|
|
446
|
+
this.pendingJoin.reject(new Error("Channel left before join completed"));
|
|
447
|
+
this.pendingJoin = null;
|
|
448
|
+
}
|
|
449
|
+
if (this.socket?.connected && this.channelId) {
|
|
450
|
+
this.socket.emit("leave_channel", { channelId: this.channelId });
|
|
451
|
+
}
|
|
452
|
+
this.channelId = null;
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Disconnect from the relay.
|
|
456
|
+
*/
|
|
457
|
+
disconnect() {
|
|
458
|
+
if (this.pendingJoin) {
|
|
459
|
+
this.pendingJoin.reject(new Error("Socket disconnected before join completed"));
|
|
460
|
+
this.pendingJoin = null;
|
|
461
|
+
}
|
|
462
|
+
this.channelId = null;
|
|
463
|
+
this.seq = 0;
|
|
464
|
+
if (this.socket) {
|
|
465
|
+
this.socket.removeAllListeners();
|
|
466
|
+
this.socket.disconnect();
|
|
467
|
+
this.socket = null;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
isConnected() {
|
|
471
|
+
return this.socket?.connected ?? false;
|
|
472
|
+
}
|
|
473
|
+
getChannelId() {
|
|
474
|
+
return this.channelId;
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
// src/utils/qrUri.ts
|
|
479
|
+
function generateConnectionURI(params) {
|
|
480
|
+
const searchParams = new URLSearchParams({
|
|
481
|
+
channelId: params.channelId,
|
|
482
|
+
pubKey: params.pubKey,
|
|
483
|
+
name: params.name,
|
|
484
|
+
url: params.url,
|
|
485
|
+
chainId: params.chainId,
|
|
486
|
+
relay: params.relayUrl
|
|
487
|
+
});
|
|
488
|
+
if (params.icon) {
|
|
489
|
+
searchParams.set("icon", params.icon);
|
|
490
|
+
}
|
|
491
|
+
return `qrlconnect://?${searchParams.toString()}`;
|
|
492
|
+
}
|
|
493
|
+
function parseConnectionURI(uri) {
|
|
494
|
+
try {
|
|
495
|
+
const queryString = uri.replace(/^qrlconnect:\/?\/?/, "");
|
|
496
|
+
const params = new URLSearchParams(queryString);
|
|
497
|
+
const channelId = params.get("channelId");
|
|
498
|
+
const pubKey = params.get("pubKey");
|
|
499
|
+
const name = params.get("name");
|
|
500
|
+
const url = params.get("url");
|
|
501
|
+
const chainId = params.get("chainId");
|
|
502
|
+
const relayUrl = params.get("relay");
|
|
503
|
+
if (!channelId || !pubKey || !name || !url || !chainId || !relayUrl) {
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
return {
|
|
507
|
+
channelId,
|
|
508
|
+
pubKey,
|
|
509
|
+
name,
|
|
510
|
+
url,
|
|
511
|
+
icon: params.get("icon") || void 0,
|
|
512
|
+
chainId,
|
|
513
|
+
relayUrl
|
|
514
|
+
};
|
|
515
|
+
} catch {
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// src/ConnectionManager.ts
|
|
521
|
+
var DAPP_PARTICIPANT_CONFLICT_ERROR_MSG = "dapp participant is already connected";
|
|
522
|
+
var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
|
|
523
|
+
constructor(options) {
|
|
524
|
+
super();
|
|
525
|
+
this.status = "disconnected" /* DISCONNECTED */;
|
|
526
|
+
this.connectedAccounts = [];
|
|
527
|
+
this.unresponsiveTimer = null;
|
|
528
|
+
this.synRetryTimer = null;
|
|
529
|
+
this.failedReconnects = 0;
|
|
530
|
+
this.synSent = false;
|
|
531
|
+
this.synAttempts = 0;
|
|
532
|
+
this.walletPresent = false;
|
|
533
|
+
this.dappMetadata = options.dappMetadata;
|
|
534
|
+
this.relayUrl = options.relayUrl || DEFAULT_RELAY_URL;
|
|
535
|
+
this.chainId = options.chainId || "0x0";
|
|
536
|
+
this.storageKey = options.storageKey || `${STORAGE_KEY_PREFIX}:session`;
|
|
537
|
+
const session = this.loadSession();
|
|
538
|
+
if (session) {
|
|
539
|
+
this.channelId = session.channelId;
|
|
540
|
+
this.ecies = new ECIESClient(session.privateKey);
|
|
541
|
+
this.keyExchange = new KeyExchange(
|
|
542
|
+
this.ecies,
|
|
543
|
+
true,
|
|
544
|
+
// dApp is always originator
|
|
545
|
+
session.otherPublicKey || void 0
|
|
546
|
+
);
|
|
547
|
+
this.connectedAccounts = session.connectedAccounts;
|
|
548
|
+
log("ConnectionManager", `Restored session for channel ${this.channelId}`);
|
|
549
|
+
} else {
|
|
550
|
+
this.channelId = uuidv4();
|
|
551
|
+
this.ecies = new ECIESClient();
|
|
552
|
+
this.keyExchange = new KeyExchange(this.ecies, true);
|
|
553
|
+
}
|
|
554
|
+
this.socketClient = new SocketClient(this.relayUrl, "dapp");
|
|
555
|
+
this.setupSocketListeners();
|
|
556
|
+
this.setupKeyExchangeListeners();
|
|
557
|
+
}
|
|
558
|
+
setupSocketListeners() {
|
|
559
|
+
this.socketClient.on("connected", () => {
|
|
560
|
+
if (this.status === "reconnecting" /* RECONNECTING */) {
|
|
561
|
+
this.failedReconnects = 0;
|
|
562
|
+
log("ConnectionManager", "Reconnected to relay");
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
this.socketClient.on("reconnected", () => {
|
|
566
|
+
if (this.keyExchange.areKeysExchanged()) {
|
|
567
|
+
if (this.walletPresent) {
|
|
568
|
+
this.setStatus("connected" /* CONNECTED */);
|
|
569
|
+
this.failedReconnects = 0;
|
|
570
|
+
} else {
|
|
571
|
+
this.setStatus("waiting" /* WAITING */);
|
|
572
|
+
}
|
|
573
|
+
this.synAttempts = 0;
|
|
574
|
+
this.clearSynRetryTimer();
|
|
575
|
+
} else {
|
|
576
|
+
this.sendSYN();
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
this.socketClient.on("disconnected", (_reason) => {
|
|
580
|
+
this.walletPresent = false;
|
|
581
|
+
this.synSent = false;
|
|
582
|
+
this.clearSynRetryTimer();
|
|
583
|
+
if (this.status === "connected" /* CONNECTED */) {
|
|
584
|
+
this.setStatus("reconnecting" /* RECONNECTING */);
|
|
585
|
+
this.failedReconnects++;
|
|
586
|
+
if (this.failedReconnects >= _ConnectionManager.MAX_RECONNECT_FAILURES) {
|
|
587
|
+
this.emit("connection_lost");
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
this.socketClient.on("message", (data) => {
|
|
592
|
+
this.handleRelayMessage(data);
|
|
593
|
+
});
|
|
594
|
+
this.socketClient.on("error", (err) => {
|
|
595
|
+
this.emit("error", err);
|
|
596
|
+
});
|
|
597
|
+
this.socketClient.on("participants_changed", (data) => {
|
|
598
|
+
if (data.event === "join" && data.clientType === "wallet") {
|
|
599
|
+
this.walletPresent = true;
|
|
600
|
+
log("ConnectionManager", "Wallet joined channel");
|
|
601
|
+
this.clearUnresponsiveTimer();
|
|
602
|
+
if (this.keyExchange.areKeysExchanged()) {
|
|
603
|
+
this.failedReconnects = 0;
|
|
604
|
+
this.setStatus("connected" /* CONNECTED */);
|
|
605
|
+
} else if (!this.synSent) {
|
|
606
|
+
this.sendSYN();
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
if (data.event === "disconnect" || data.event === "leave") {
|
|
610
|
+
if (data.clientType === "wallet" || !data.clientType) {
|
|
611
|
+
this.walletPresent = false;
|
|
612
|
+
if (!this.keyExchange.areKeysExchanged()) {
|
|
613
|
+
this.synSent = false;
|
|
614
|
+
this.clearSynRetryTimer();
|
|
615
|
+
} else if (this.status === "connected" /* CONNECTED */ || this.status === "reconnecting" /* RECONNECTING */) {
|
|
616
|
+
this.setStatus("waiting" /* WAITING */);
|
|
617
|
+
}
|
|
618
|
+
log("ConnectionManager", "Wallet left channel");
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
setupKeyExchangeListeners() {
|
|
624
|
+
this.keyExchange.on("keys_exchanged", () => {
|
|
625
|
+
log("ConnectionManager", "Key exchange complete");
|
|
626
|
+
this.setStatus("connected" /* CONNECTED */);
|
|
627
|
+
this.walletPresent = true;
|
|
628
|
+
this.synSent = false;
|
|
629
|
+
this.synAttempts = 0;
|
|
630
|
+
this.clearSynRetryTimer();
|
|
631
|
+
this.saveSession();
|
|
632
|
+
this.sendEncrypted({
|
|
633
|
+
type: "originator_info" /* ORIGINATOR_INFO */,
|
|
634
|
+
originatorInfo: {
|
|
635
|
+
...this.dappMetadata,
|
|
636
|
+
chainId: this.chainId
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Generate the connection URI and start listening for wallet connection.
|
|
643
|
+
*/
|
|
644
|
+
async getConnectionURI(retryOnConflict = true) {
|
|
645
|
+
this.setStatus("connecting" /* CONNECTING */);
|
|
646
|
+
this.keyExchange.reset();
|
|
647
|
+
this.walletPresent = false;
|
|
648
|
+
this.synSent = false;
|
|
649
|
+
this.synAttempts = 0;
|
|
650
|
+
this.clearSynRetryTimer();
|
|
651
|
+
this.socketClient.connect();
|
|
652
|
+
try {
|
|
653
|
+
await this.socketClient.joinChannel(this.channelId);
|
|
654
|
+
} catch (err) {
|
|
655
|
+
if (retryOnConflict && this.isDappParticipantConflictError(err)) {
|
|
656
|
+
warn(
|
|
657
|
+
"ConnectionManager",
|
|
658
|
+
"Channel already has an active dApp participant. Rotating to a fresh channel."
|
|
659
|
+
);
|
|
660
|
+
this.resetForNewChannel();
|
|
661
|
+
return this.getConnectionURI(false);
|
|
662
|
+
}
|
|
663
|
+
this.setStatus("disconnected" /* DISCONNECTED */);
|
|
664
|
+
throw err;
|
|
665
|
+
}
|
|
666
|
+
this.setStatus("waiting" /* WAITING */);
|
|
667
|
+
const uri = generateConnectionURI({
|
|
668
|
+
channelId: this.channelId,
|
|
669
|
+
pubKey: this.ecies.getPublicKey(),
|
|
670
|
+
name: this.dappMetadata.name,
|
|
671
|
+
url: this.dappMetadata.url,
|
|
672
|
+
icon: this.dappMetadata.icon,
|
|
673
|
+
chainId: this.chainId,
|
|
674
|
+
relayUrl: this.relayUrl
|
|
675
|
+
});
|
|
676
|
+
log("ConnectionManager", `Generated connection URI for channel ${this.channelId}`);
|
|
677
|
+
return uri;
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Reconnect to an existing session.
|
|
681
|
+
*/
|
|
682
|
+
async reconnect() {
|
|
683
|
+
const session = this.loadSession();
|
|
684
|
+
if (!session) return false;
|
|
685
|
+
this.setStatus("reconnecting" /* RECONNECTING */);
|
|
686
|
+
this.walletPresent = false;
|
|
687
|
+
this.synSent = false;
|
|
688
|
+
this.synAttempts = 0;
|
|
689
|
+
this.clearSynRetryTimer();
|
|
690
|
+
this.socketClient.connect();
|
|
691
|
+
try {
|
|
692
|
+
const { bufferedMessages } = await this.socketClient.joinChannel(this.channelId);
|
|
693
|
+
for (const msg of bufferedMessages) {
|
|
694
|
+
this.handleRelayMessage(msg);
|
|
695
|
+
}
|
|
696
|
+
if (this.keyExchange.areKeysExchanged() && this.walletPresent) {
|
|
697
|
+
this.setStatus("connected" /* CONNECTED */);
|
|
698
|
+
} else if (this.keyExchange.areKeysExchanged()) {
|
|
699
|
+
this.setStatus("waiting" /* WAITING */);
|
|
700
|
+
} else {
|
|
701
|
+
this.sendSYN();
|
|
702
|
+
}
|
|
703
|
+
return true;
|
|
704
|
+
} catch (err) {
|
|
705
|
+
error("ConnectionManager", "Reconnect failed:", err);
|
|
706
|
+
this.setStatus("disconnected" /* DISCONNECTED */);
|
|
707
|
+
return false;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Send a JSON-RPC request to the wallet.
|
|
712
|
+
*/
|
|
713
|
+
sendJsonRpc(request) {
|
|
714
|
+
if (!this.keyExchange.areKeysExchanged()) {
|
|
715
|
+
throw new Error("Not connected: key exchange not complete");
|
|
716
|
+
}
|
|
717
|
+
this.sendEncrypted({
|
|
718
|
+
type: "jsonrpc" /* JSONRPC */,
|
|
719
|
+
jsonrpc: "2.0",
|
|
720
|
+
id: request.id,
|
|
721
|
+
method: request.method,
|
|
722
|
+
params: request.params
|
|
723
|
+
});
|
|
724
|
+
this.startUnresponsiveTimer();
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Handle an incoming relay message.
|
|
728
|
+
*/
|
|
729
|
+
handleRelayMessage(data) {
|
|
730
|
+
if (data.clientType === "wallet") {
|
|
731
|
+
this.walletPresent = true;
|
|
732
|
+
if (this.keyExchange.areKeysExchanged() && this.status !== "connected" /* CONNECTED */) {
|
|
733
|
+
this.failedReconnects = 0;
|
|
734
|
+
this.setStatus("connected" /* CONNECTED */);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
const message = data.message;
|
|
738
|
+
if (typeof message === "object" && message !== null) {
|
|
739
|
+
const msg = message;
|
|
740
|
+
if (msg.type === "key_handshake_SYN" /* SYN */ || msg.type === "key_handshake_SYNACK" /* SYNACK */ || msg.type === "key_handshake_ACK" /* ACK */) {
|
|
741
|
+
this.handleKeyExchangeMessage(
|
|
742
|
+
msg
|
|
743
|
+
);
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
if (typeof message === "string" && this.keyExchange.areKeysExchanged()) {
|
|
748
|
+
try {
|
|
749
|
+
const decrypted = this.keyExchange.decryptMessage(message);
|
|
750
|
+
const parsed = JSON.parse(decrypted);
|
|
751
|
+
this.handleDecryptedMessage(parsed);
|
|
752
|
+
} catch (err) {
|
|
753
|
+
error("ConnectionManager", "Failed to decrypt message:", err);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
handleKeyExchangeMessage(msg) {
|
|
758
|
+
this.setStatus("key_exchange" /* KEY_EXCHANGE */);
|
|
759
|
+
const response = this.keyExchange.onMessage(msg);
|
|
760
|
+
if (response) {
|
|
761
|
+
this.sendPlaintext(response);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
handleDecryptedMessage(msg) {
|
|
765
|
+
this.clearUnresponsiveTimer();
|
|
766
|
+
const type = msg.type;
|
|
767
|
+
switch (type) {
|
|
768
|
+
case "wallet_info" /* WALLET_INFO */: {
|
|
769
|
+
const info = msg;
|
|
770
|
+
const nextAccounts = info.accounts || [];
|
|
771
|
+
const nextChainId = info.chainId || this.chainId;
|
|
772
|
+
const accountsChanged = !this.areArraysEqual(this.connectedAccounts, nextAccounts);
|
|
773
|
+
const chainChanged = this.chainId !== nextChainId;
|
|
774
|
+
this.connectedAccounts = nextAccounts;
|
|
775
|
+
this.chainId = nextChainId;
|
|
776
|
+
this.saveSession();
|
|
777
|
+
this.emit("wallet_info", {
|
|
778
|
+
accounts: this.connectedAccounts,
|
|
779
|
+
chainId: this.chainId
|
|
780
|
+
});
|
|
781
|
+
if (accountsChanged) {
|
|
782
|
+
this.emit("accounts_changed", this.connectedAccounts);
|
|
783
|
+
}
|
|
784
|
+
if (chainChanged) {
|
|
785
|
+
this.emit("chain_changed", this.chainId);
|
|
786
|
+
}
|
|
787
|
+
break;
|
|
788
|
+
}
|
|
789
|
+
case "jsonrpc" /* JSONRPC */: {
|
|
790
|
+
const response = msg;
|
|
791
|
+
this.emit("jsonrpc_response", response);
|
|
792
|
+
break;
|
|
793
|
+
}
|
|
794
|
+
case "terminate" /* TERMINATE */: {
|
|
795
|
+
log("ConnectionManager", "Received terminate from wallet");
|
|
796
|
+
this.disconnect();
|
|
797
|
+
break;
|
|
798
|
+
}
|
|
799
|
+
default:
|
|
800
|
+
log("ConnectionManager", `Unhandled message type: ${type}`);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
/** Send a plaintext (unencrypted) message through the relay. */
|
|
804
|
+
sendPlaintext(message) {
|
|
805
|
+
this.socketClient.sendMessage({
|
|
806
|
+
id: this.channelId,
|
|
807
|
+
clientType: "dapp",
|
|
808
|
+
message
|
|
809
|
+
}).catch((err) => {
|
|
810
|
+
error("ConnectionManager", "Failed to send plaintext:", err);
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
/** Send an encrypted message through the relay. */
|
|
814
|
+
sendEncrypted(message) {
|
|
815
|
+
try {
|
|
816
|
+
const encrypted = this.keyExchange.encryptMessage(JSON.stringify(message));
|
|
817
|
+
this.socketClient.sendMessage({
|
|
818
|
+
id: this.channelId,
|
|
819
|
+
clientType: "dapp",
|
|
820
|
+
message: encrypted
|
|
821
|
+
}).catch((err) => {
|
|
822
|
+
error("ConnectionManager", "Failed to send encrypted:", err);
|
|
823
|
+
});
|
|
824
|
+
} catch (err) {
|
|
825
|
+
error("ConnectionManager", "Encryption failed:", err);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
startUnresponsiveTimer() {
|
|
829
|
+
this.clearUnresponsiveTimer();
|
|
830
|
+
this.unresponsiveTimer = setTimeout(() => {
|
|
831
|
+
warn("ConnectionManager", "Wallet appears unresponsive");
|
|
832
|
+
}, WALLET_UNRESPONSIVE_MS);
|
|
833
|
+
}
|
|
834
|
+
clearUnresponsiveTimer() {
|
|
835
|
+
if (this.unresponsiveTimer) {
|
|
836
|
+
clearTimeout(this.unresponsiveTimer);
|
|
837
|
+
this.unresponsiveTimer = null;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
sendSYN() {
|
|
841
|
+
if (!this.socketClient.isConnected()) {
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
if (this.synAttempts >= _ConnectionManager.MAX_SYN_ATTEMPTS) {
|
|
845
|
+
this.synSent = false;
|
|
846
|
+
this.emit("error", new Error("Key exchange timed out waiting for wallet response"));
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
try {
|
|
850
|
+
const syn = this.keyExchange.createSYN();
|
|
851
|
+
this.sendPlaintext(syn);
|
|
852
|
+
this.synSent = true;
|
|
853
|
+
this.synAttempts++;
|
|
854
|
+
this.scheduleSynRetry();
|
|
855
|
+
log("ConnectionManager", "Sent SYN key exchange message");
|
|
856
|
+
} catch (err) {
|
|
857
|
+
error("ConnectionManager", "Failed to send SYN:", err);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
scheduleSynRetry() {
|
|
861
|
+
this.clearSynRetryTimer();
|
|
862
|
+
this.synRetryTimer = setTimeout(() => {
|
|
863
|
+
this.synRetryTimer = null;
|
|
864
|
+
if (this.keyExchange.areKeysExchanged()) {
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
this.synSent = false;
|
|
868
|
+
if (!this.walletPresent || !this.socketClient.isConnected()) {
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
this.sendSYN();
|
|
872
|
+
}, _ConnectionManager.SYN_RETRY_MS);
|
|
873
|
+
}
|
|
874
|
+
clearSynRetryTimer() {
|
|
875
|
+
if (this.synRetryTimer) {
|
|
876
|
+
clearTimeout(this.synRetryTimer);
|
|
877
|
+
this.synRetryTimer = null;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
setStatus(status) {
|
|
881
|
+
if (this.status !== status) {
|
|
882
|
+
this.status = status;
|
|
883
|
+
this.emit("status_changed", status);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
isDappParticipantConflictError(err) {
|
|
887
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
888
|
+
return msg.toLowerCase().includes(DAPP_PARTICIPANT_CONFLICT_ERROR_MSG);
|
|
889
|
+
}
|
|
890
|
+
areArraysEqual(a, b) {
|
|
891
|
+
if (a.length !== b.length) return false;
|
|
892
|
+
for (let i = 0; i < a.length; i++) {
|
|
893
|
+
if (a[i] !== b[i]) return false;
|
|
894
|
+
}
|
|
895
|
+
return true;
|
|
896
|
+
}
|
|
897
|
+
getStatus() {
|
|
898
|
+
return this.status;
|
|
899
|
+
}
|
|
900
|
+
getAccounts() {
|
|
901
|
+
return this.connectedAccounts;
|
|
902
|
+
}
|
|
903
|
+
getChainId() {
|
|
904
|
+
return this.chainId;
|
|
905
|
+
}
|
|
906
|
+
getChannelId() {
|
|
907
|
+
return this.channelId;
|
|
908
|
+
}
|
|
909
|
+
/** Check if a stored session exists (without loading/restoring it). */
|
|
910
|
+
hasStoredSession() {
|
|
911
|
+
if (typeof localStorage === "undefined") return false;
|
|
912
|
+
try {
|
|
913
|
+
const raw = localStorage.getItem(this.storageKey);
|
|
914
|
+
if (!raw) return false;
|
|
915
|
+
const session = JSON.parse(raw);
|
|
916
|
+
return Date.now() - session.createdAt <= SESSION_TTL_MS;
|
|
917
|
+
} catch {
|
|
918
|
+
return false;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* Reset the connection and create a new channel for a fresh pairing.
|
|
923
|
+
* Disconnects any existing session, generates a new channel ID and keys.
|
|
924
|
+
*/
|
|
925
|
+
resetForNewChannel() {
|
|
926
|
+
this.clearUnresponsiveTimer();
|
|
927
|
+
this.clearSynRetryTimer();
|
|
928
|
+
this.synSent = false;
|
|
929
|
+
this.synAttempts = 0;
|
|
930
|
+
this.walletPresent = false;
|
|
931
|
+
if (this.keyExchange.areKeysExchanged()) {
|
|
932
|
+
try {
|
|
933
|
+
this.sendEncrypted({ type: "terminate" /* TERMINATE */ });
|
|
934
|
+
} catch {
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
this.socketClient.leaveChannel();
|
|
938
|
+
this.socketClient.disconnect();
|
|
939
|
+
this.clearSession();
|
|
940
|
+
this.connectedAccounts = [];
|
|
941
|
+
this.channelId = uuidv4();
|
|
942
|
+
this.ecies = new ECIESClient();
|
|
943
|
+
this.keyExchange = new KeyExchange(this.ecies, true);
|
|
944
|
+
this.socketClient = new SocketClient(this.relayUrl, "dapp");
|
|
945
|
+
this.setupSocketListeners();
|
|
946
|
+
this.setupKeyExchangeListeners();
|
|
947
|
+
this.setStatus("disconnected" /* DISCONNECTED */);
|
|
948
|
+
}
|
|
949
|
+
/** Disconnect and clean up. */
|
|
950
|
+
disconnect() {
|
|
951
|
+
this.clearUnresponsiveTimer();
|
|
952
|
+
this.clearSynRetryTimer();
|
|
953
|
+
this.synSent = false;
|
|
954
|
+
this.synAttempts = 0;
|
|
955
|
+
this.walletPresent = false;
|
|
956
|
+
if (this.keyExchange.areKeysExchanged()) {
|
|
957
|
+
try {
|
|
958
|
+
this.sendEncrypted({ type: "terminate" /* TERMINATE */ });
|
|
959
|
+
} catch {
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
this.socketClient.leaveChannel();
|
|
963
|
+
this.socketClient.disconnect();
|
|
964
|
+
this.setStatus("disconnected" /* DISCONNECTED */);
|
|
965
|
+
this.clearSession();
|
|
966
|
+
this.connectedAccounts = [];
|
|
967
|
+
}
|
|
968
|
+
// --- Session persistence ---
|
|
969
|
+
saveSession() {
|
|
970
|
+
if (typeof localStorage === "undefined") return;
|
|
971
|
+
const session = {
|
|
972
|
+
channelId: this.channelId,
|
|
973
|
+
privateKey: this.ecies.getPrivateKeyHex(),
|
|
974
|
+
otherPublicKey: this.keyExchange.getOtherPublicKey(),
|
|
975
|
+
dappMetadata: this.dappMetadata,
|
|
976
|
+
connectedAccounts: this.connectedAccounts,
|
|
977
|
+
chainId: this.chainId,
|
|
978
|
+
createdAt: Date.now(),
|
|
979
|
+
lastActivity: Date.now()
|
|
980
|
+
};
|
|
981
|
+
localStorage.setItem(this.storageKey, JSON.stringify(session));
|
|
982
|
+
}
|
|
983
|
+
loadSession() {
|
|
984
|
+
if (typeof localStorage === "undefined") return null;
|
|
985
|
+
try {
|
|
986
|
+
const raw = localStorage.getItem(this.storageKey);
|
|
987
|
+
if (!raw) return null;
|
|
988
|
+
const session = JSON.parse(raw);
|
|
989
|
+
if (Date.now() - session.createdAt > SESSION_TTL_MS) {
|
|
990
|
+
this.clearSession();
|
|
991
|
+
return null;
|
|
992
|
+
}
|
|
993
|
+
return session;
|
|
994
|
+
} catch {
|
|
995
|
+
return null;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
clearSession() {
|
|
999
|
+
if (typeof localStorage === "undefined") return;
|
|
1000
|
+
localStorage.removeItem(this.storageKey);
|
|
1001
|
+
}
|
|
1002
|
+
};
|
|
1003
|
+
_ConnectionManager.MAX_RECONNECT_FAILURES = 5;
|
|
1004
|
+
_ConnectionManager.SYN_RETRY_MS = 5e3;
|
|
1005
|
+
_ConnectionManager.MAX_SYN_ATTEMPTS = 6;
|
|
1006
|
+
var ConnectionManager = _ConnectionManager;
|
|
1007
|
+
|
|
1008
|
+
// src/utils/platform.ts
|
|
1009
|
+
function isMobileBrowser() {
|
|
1010
|
+
if (typeof navigator === "undefined") return false;
|
|
1011
|
+
return /Android|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i.test(navigator.userAgent);
|
|
1012
|
+
}
|
|
1013
|
+
function getAppStoreUrl() {
|
|
1014
|
+
if (typeof navigator !== "undefined" && /android/i.test(navigator.userAgent)) {
|
|
1015
|
+
return "https://play.google.com/store/apps/details?id=com.chiefdg.myqrlwallet";
|
|
1016
|
+
}
|
|
1017
|
+
return "https://apps.apple.com/app/myqrlwallet/id6742219498";
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// src/QRLConnectProvider.ts
|
|
1021
|
+
var requestCounter = 0;
|
|
1022
|
+
var QRLConnectProvider = class extends EventEmitter4 {
|
|
1023
|
+
constructor(options) {
|
|
1024
|
+
super();
|
|
1025
|
+
this.pendingRequests = /* @__PURE__ */ new Map();
|
|
1026
|
+
this.isQRLConnect = true;
|
|
1027
|
+
this.options = options;
|
|
1028
|
+
if (options.debug) {
|
|
1029
|
+
setDebug(true);
|
|
1030
|
+
}
|
|
1031
|
+
this.connectionManager = new ConnectionManager({
|
|
1032
|
+
dappMetadata: options.dappMetadata,
|
|
1033
|
+
relayUrl: options.relayUrl,
|
|
1034
|
+
chainId: options.chainId,
|
|
1035
|
+
storageKey: options.storageKey
|
|
1036
|
+
});
|
|
1037
|
+
this.setupConnectionListeners();
|
|
1038
|
+
if (options.autoReconnect !== false) {
|
|
1039
|
+
this.connectionManager.reconnect();
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
setupConnectionListeners() {
|
|
1043
|
+
this.connectionManager.on("status_changed", (status) => {
|
|
1044
|
+
log("Provider", `Connection status: ${status}`);
|
|
1045
|
+
this.emit("statusChanged", status);
|
|
1046
|
+
if (status === "connected" /* CONNECTED */) {
|
|
1047
|
+
this.emit("connect", { chainId: this.connectionManager.getChainId() });
|
|
1048
|
+
}
|
|
1049
|
+
if (status === "disconnected" /* DISCONNECTED */) {
|
|
1050
|
+
this.emit("disconnect", {
|
|
1051
|
+
code: 4900,
|
|
1052
|
+
message: "Disconnected from QRL Wallet"
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
});
|
|
1056
|
+
this.connectionManager.on("accounts_changed", (accounts) => {
|
|
1057
|
+
this.emit("accountsChanged", accounts);
|
|
1058
|
+
});
|
|
1059
|
+
this.connectionManager.on("chain_changed", (chainId) => {
|
|
1060
|
+
this.emit("chainChanged", chainId);
|
|
1061
|
+
});
|
|
1062
|
+
this.connectionManager.on("jsonrpc_response", (response) => {
|
|
1063
|
+
const pending = this.pendingRequests.get(response.id);
|
|
1064
|
+
if (!pending) {
|
|
1065
|
+
warn("Provider", `No pending request for id ${response.id}`);
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
this.pendingRequests.delete(response.id);
|
|
1069
|
+
if (response.error) {
|
|
1070
|
+
pending.reject(new Error(response.error.message || "Request failed"));
|
|
1071
|
+
} else {
|
|
1072
|
+
pending.resolve(response.result);
|
|
1073
|
+
}
|
|
1074
|
+
});
|
|
1075
|
+
this.connectionManager.on("wallet_info", (info) => {
|
|
1076
|
+
for (const [id, pending] of this.pendingRequests) {
|
|
1077
|
+
if (pending.method === "qrl_requestAccounts" || pending.method === "qrl_accounts") {
|
|
1078
|
+
pending.resolve(info.accounts);
|
|
1079
|
+
this.pendingRequests.delete(id);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
});
|
|
1083
|
+
this.connectionManager.on("error", (err) => {
|
|
1084
|
+
warn("Provider", `ConnectionManager error: ${err.message}`);
|
|
1085
|
+
this.emit("message", { type: "error", data: err.message });
|
|
1086
|
+
});
|
|
1087
|
+
this.connectionManager.on("connection_lost", () => {
|
|
1088
|
+
for (const [, pending] of this.pendingRequests) {
|
|
1089
|
+
pending.reject(new Error("Connection to QRL Wallet lost"));
|
|
1090
|
+
}
|
|
1091
|
+
this.pendingRequests.clear();
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* Generate a connection URI for QR code display or deep link redirect.
|
|
1096
|
+
*/
|
|
1097
|
+
async getConnectionURI() {
|
|
1098
|
+
return this.connectionManager.getConnectionURI();
|
|
1099
|
+
}
|
|
1100
|
+
/**
|
|
1101
|
+
* Check if the current browser is mobile.
|
|
1102
|
+
*/
|
|
1103
|
+
isMobile() {
|
|
1104
|
+
return isMobileBrowser();
|
|
1105
|
+
}
|
|
1106
|
+
/**
|
|
1107
|
+
* Get the app store URL for the QRL Wallet app.
|
|
1108
|
+
*/
|
|
1109
|
+
getAppStoreUrl() {
|
|
1110
|
+
return getAppStoreUrl();
|
|
1111
|
+
}
|
|
1112
|
+
/**
|
|
1113
|
+
* EIP-1193 request method.
|
|
1114
|
+
*/
|
|
1115
|
+
async request(args) {
|
|
1116
|
+
const { method, params } = args;
|
|
1117
|
+
if (method === "qrl_chainId") {
|
|
1118
|
+
return this.connectionManager.getChainId();
|
|
1119
|
+
}
|
|
1120
|
+
if (method === "qrl_accounts") {
|
|
1121
|
+
const accounts = this.connectionManager.getAccounts();
|
|
1122
|
+
if (accounts.length > 0) return accounts;
|
|
1123
|
+
}
|
|
1124
|
+
if (!RESTRICTED_METHODS.has(method) && !UNRESTRICTED_METHODS.has(method)) {
|
|
1125
|
+
throw new Error(`Unsupported method: ${method}`);
|
|
1126
|
+
}
|
|
1127
|
+
if (this.connectionManager.getStatus() !== "connected" /* CONNECTED */) {
|
|
1128
|
+
throw new Error("Not connected to QRL Wallet");
|
|
1129
|
+
}
|
|
1130
|
+
const id = ++requestCounter;
|
|
1131
|
+
return new Promise((resolve, reject) => {
|
|
1132
|
+
const pending = {
|
|
1133
|
+
id,
|
|
1134
|
+
method,
|
|
1135
|
+
params,
|
|
1136
|
+
resolve,
|
|
1137
|
+
reject,
|
|
1138
|
+
timestamp: Date.now()
|
|
1139
|
+
};
|
|
1140
|
+
this.pendingRequests.set(id, pending);
|
|
1141
|
+
const timeout = setTimeout(() => {
|
|
1142
|
+
if (this.pendingRequests.has(id)) {
|
|
1143
|
+
this.pendingRequests.delete(id);
|
|
1144
|
+
reject(new Error(`Request timeout: ${method} (${REQUEST_TIMEOUT_MS}ms)`));
|
|
1145
|
+
}
|
|
1146
|
+
}, REQUEST_TIMEOUT_MS);
|
|
1147
|
+
const originalResolve = pending.resolve;
|
|
1148
|
+
const originalReject = pending.reject;
|
|
1149
|
+
pending.resolve = (result) => {
|
|
1150
|
+
clearTimeout(timeout);
|
|
1151
|
+
originalResolve(result);
|
|
1152
|
+
};
|
|
1153
|
+
pending.reject = (error2) => {
|
|
1154
|
+
clearTimeout(timeout);
|
|
1155
|
+
originalReject(error2);
|
|
1156
|
+
};
|
|
1157
|
+
this.connectionManager.sendJsonRpc({
|
|
1158
|
+
jsonrpc: "2.0",
|
|
1159
|
+
id,
|
|
1160
|
+
method,
|
|
1161
|
+
params
|
|
1162
|
+
});
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
/**
|
|
1166
|
+
* Get the current connection status.
|
|
1167
|
+
*/
|
|
1168
|
+
getStatus() {
|
|
1169
|
+
return this.connectionManager.getStatus();
|
|
1170
|
+
}
|
|
1171
|
+
/**
|
|
1172
|
+
* Get connected accounts.
|
|
1173
|
+
*/
|
|
1174
|
+
getAccounts() {
|
|
1175
|
+
return this.connectionManager.getAccounts();
|
|
1176
|
+
}
|
|
1177
|
+
/**
|
|
1178
|
+
* Get the channel ID for this connection.
|
|
1179
|
+
*/
|
|
1180
|
+
getChannelId() {
|
|
1181
|
+
return this.connectionManager.getChannelId();
|
|
1182
|
+
}
|
|
1183
|
+
/**
|
|
1184
|
+
* Check if connected and keys exchanged.
|
|
1185
|
+
*/
|
|
1186
|
+
isConnected() {
|
|
1187
|
+
return this.connectionManager.getStatus() === "connected" /* CONNECTED */;
|
|
1188
|
+
}
|
|
1189
|
+
/**
|
|
1190
|
+
* Check if a stored session exists that can be reconnected.
|
|
1191
|
+
*/
|
|
1192
|
+
hasStoredSession() {
|
|
1193
|
+
return this.connectionManager.hasStoredSession();
|
|
1194
|
+
}
|
|
1195
|
+
/**
|
|
1196
|
+
* Reset the connection and start a fresh pairing with a new channel.
|
|
1197
|
+
* Use this when the user wants to create a new connection instead of
|
|
1198
|
+
* reconnecting to an existing session.
|
|
1199
|
+
*/
|
|
1200
|
+
async newConnection() {
|
|
1201
|
+
for (const [, pending] of this.pendingRequests) {
|
|
1202
|
+
pending.reject(new Error("Connection reset"));
|
|
1203
|
+
}
|
|
1204
|
+
this.pendingRequests.clear();
|
|
1205
|
+
this.connectionManager.resetForNewChannel();
|
|
1206
|
+
return this.getConnectionURI();
|
|
1207
|
+
}
|
|
1208
|
+
/**
|
|
1209
|
+
* Disconnect from wallet and clean up.
|
|
1210
|
+
*/
|
|
1211
|
+
disconnect() {
|
|
1212
|
+
for (const [, pending] of this.pendingRequests) {
|
|
1213
|
+
pending.reject(new Error("Disconnected"));
|
|
1214
|
+
}
|
|
1215
|
+
this.pendingRequests.clear();
|
|
1216
|
+
this.connectionManager.disconnect();
|
|
1217
|
+
}
|
|
1218
|
+
};
|
|
1219
|
+
export {
|
|
1220
|
+
ConnectionManager,
|
|
1221
|
+
ConnectionStatus,
|
|
1222
|
+
DEFAULT_RELAY_URL,
|
|
1223
|
+
ECIESClient,
|
|
1224
|
+
KeyExchange,
|
|
1225
|
+
KeyExchangeMessageType,
|
|
1226
|
+
MessageType,
|
|
1227
|
+
QRLConnectProvider as QRLConnect,
|
|
1228
|
+
QRLConnectProvider,
|
|
1229
|
+
RESTRICTED_METHODS,
|
|
1230
|
+
SocketClient,
|
|
1231
|
+
UNRESTRICTED_METHODS,
|
|
1232
|
+
generateConnectionURI,
|
|
1233
|
+
getAppStoreUrl,
|
|
1234
|
+
isMobileBrowser,
|
|
1235
|
+
parseConnectionURI
|
|
1236
|
+
};
|