@qrlwallet/connect 0.1.0 → 3.0.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/README.md +13 -32
- package/dist/index.d.mts +483 -113
- package/dist/index.d.ts +483 -113
- package/dist/index.js +1409 -444
- package/dist/index.mjs +1382 -442
- package/package.json +8 -6
package/dist/index.mjs
CHANGED
|
@@ -5,94 +5,153 @@ import EventEmitter4 from "eventemitter3";
|
|
|
5
5
|
import EventEmitter3 from "eventemitter3";
|
|
6
6
|
import { v4 as uuidv4 } from "uuid";
|
|
7
7
|
|
|
8
|
-
// src/
|
|
9
|
-
import
|
|
8
|
+
// src/KeyExchange.ts
|
|
9
|
+
import EventEmitter from "eventemitter3";
|
|
10
10
|
|
|
11
|
-
// src/
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
// src/PQCrypto.ts
|
|
12
|
+
import { ml_kem768 } from "@noble/post-quantum/ml-kem.js";
|
|
13
|
+
var textEncoder = new TextEncoder();
|
|
14
|
+
var LABEL = textEncoder.encode("pq-pair/v1");
|
|
15
|
+
var LABEL_AEAD_SUFFIX = textEncoder.encode(" aead");
|
|
16
|
+
var DIR_DAPP_TX = new Uint8Array([0, 0, 0, 1]);
|
|
17
|
+
var DIR_WALLET_TX = new Uint8Array([0, 0, 0, 2]);
|
|
18
|
+
var ML_KEM_768_CT_LEN = 1088;
|
|
19
|
+
var AEAD_KEY_LEN = 32;
|
|
20
|
+
function subtle() {
|
|
21
|
+
const c = globalThis.crypto;
|
|
22
|
+
if (!c || !c.subtle) {
|
|
23
|
+
throw new Error("PQCrypto: WebCrypto SubtleCrypto is not available in this environment");
|
|
24
|
+
}
|
|
25
|
+
return c.subtle;
|
|
15
26
|
}
|
|
16
|
-
function
|
|
17
|
-
|
|
18
|
-
console.log(`[QRLConnect:${tag}]`, ...args);
|
|
19
|
-
}
|
|
27
|
+
function bs(u) {
|
|
28
|
+
return u;
|
|
20
29
|
}
|
|
21
|
-
function
|
|
22
|
-
|
|
30
|
+
function kemKeygen() {
|
|
31
|
+
const { publicKey, secretKey } = ml_kem768.keygen();
|
|
32
|
+
return { pk: publicKey, sk: secretKey };
|
|
23
33
|
}
|
|
24
|
-
function
|
|
25
|
-
|
|
34
|
+
function kemEncaps(pk) {
|
|
35
|
+
const { cipherText, sharedSecret } = ml_kem768.encapsulate(pk);
|
|
36
|
+
return { ct: cipherText, ss: sharedSecret };
|
|
37
|
+
}
|
|
38
|
+
function kemDecaps(sk, ct) {
|
|
39
|
+
return ml_kem768.decapsulate(ct, sk);
|
|
40
|
+
}
|
|
41
|
+
async function transcriptHash(cid, pk, ct) {
|
|
42
|
+
const buf = concat(LABEL, cid, pk, ct);
|
|
43
|
+
return new Uint8Array(await subtle().digest("SHA-256", bs(buf)));
|
|
44
|
+
}
|
|
45
|
+
async function deriveAeadKey(ss, htx) {
|
|
46
|
+
const ikm = await subtle().importKey("raw", bs(ss), "HKDF", false, ["deriveKey"]);
|
|
47
|
+
const info = concat(LABEL, LABEL_AEAD_SUFFIX, htx);
|
|
48
|
+
return subtle().deriveKey(
|
|
49
|
+
{ name: "HKDF", hash: "SHA-256", salt: new Uint8Array(32), info: bs(info) },
|
|
50
|
+
ikm,
|
|
51
|
+
{ name: "AES-GCM", length: 256 },
|
|
52
|
+
true,
|
|
53
|
+
["encrypt", "decrypt"]
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
async function importRawAeadKey(raw) {
|
|
57
|
+
if (raw.length !== AEAD_KEY_LEN) {
|
|
58
|
+
throw new Error(`PQCrypto: raw AEAD key must be ${AEAD_KEY_LEN} bytes`);
|
|
59
|
+
}
|
|
60
|
+
return subtle().importKey("raw", bs(raw), { name: "AES-GCM", length: 256 }, true, [
|
|
61
|
+
"encrypt",
|
|
62
|
+
"decrypt"
|
|
63
|
+
]);
|
|
64
|
+
}
|
|
65
|
+
async function exportRawAeadKey(key) {
|
|
66
|
+
const buf = await subtle().exportKey("raw", key);
|
|
67
|
+
return new Uint8Array(buf);
|
|
68
|
+
}
|
|
69
|
+
function nonce(dir, seq) {
|
|
70
|
+
if (dir.length !== 4) {
|
|
71
|
+
throw new Error("PQCrypto: direction tag must be 4 bytes");
|
|
72
|
+
}
|
|
73
|
+
const n = new Uint8Array(12);
|
|
74
|
+
n.set(dir, 0);
|
|
75
|
+
new DataView(n.buffer, n.byteOffset, n.byteLength).setBigUint64(4, BigInt(seq), true);
|
|
76
|
+
return n;
|
|
77
|
+
}
|
|
78
|
+
function aad(htx, seq) {
|
|
79
|
+
const out = new Uint8Array(htx.length + 8);
|
|
80
|
+
out.set(htx, 0);
|
|
81
|
+
new DataView(out.buffer, out.byteOffset, out.byteLength).setBigUint64(
|
|
82
|
+
htx.length,
|
|
83
|
+
BigInt(seq),
|
|
84
|
+
true
|
|
85
|
+
);
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
async function seal(key, dir, seq, htx, pt) {
|
|
89
|
+
const ct = await subtle().encrypt(
|
|
90
|
+
{ name: "AES-GCM", iv: bs(nonce(dir, seq)), additionalData: bs(aad(htx, seq)) },
|
|
91
|
+
key,
|
|
92
|
+
bs(pt)
|
|
93
|
+
);
|
|
94
|
+
return new Uint8Array(ct);
|
|
95
|
+
}
|
|
96
|
+
async function open(key, dir, seq, htx, ct) {
|
|
97
|
+
const pt = await subtle().decrypt(
|
|
98
|
+
{ name: "AES-GCM", iv: bs(nonce(dir, seq)), additionalData: bs(aad(htx, seq)) },
|
|
99
|
+
key,
|
|
100
|
+
bs(ct)
|
|
101
|
+
);
|
|
102
|
+
return new Uint8Array(pt);
|
|
103
|
+
}
|
|
104
|
+
function zeroize(b) {
|
|
105
|
+
b.fill(0);
|
|
106
|
+
}
|
|
107
|
+
function concat(...parts) {
|
|
108
|
+
let total = 0;
|
|
109
|
+
for (const p of parts) total += p.length;
|
|
110
|
+
const out = new Uint8Array(total);
|
|
111
|
+
let offset = 0;
|
|
112
|
+
for (const p of parts) {
|
|
113
|
+
out.set(p, offset);
|
|
114
|
+
offset += p.length;
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
26
117
|
}
|
|
27
|
-
|
|
28
|
-
// src/ECIESClient.ts
|
|
29
118
|
function toBase64(bytes) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
119
|
+
const CHUNK = 32768;
|
|
120
|
+
let bin = "";
|
|
121
|
+
for (let i = 0; i < bytes.length; i += CHUNK) {
|
|
122
|
+
const slice = bytes.subarray(i, i + CHUNK);
|
|
123
|
+
bin += String.fromCharCode.apply(null, slice);
|
|
33
124
|
}
|
|
34
|
-
return btoa(
|
|
125
|
+
return btoa(bin);
|
|
35
126
|
}
|
|
36
|
-
function fromBase64(
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
for (let i = 0; i <
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
127
|
+
function fromBase64(b64) {
|
|
128
|
+
const bin = atob(b64);
|
|
129
|
+
const out = new Uint8Array(bin.length);
|
|
130
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
function constantTimeEquals(a, b) {
|
|
134
|
+
if (a.length !== b.length) return false;
|
|
135
|
+
let d = 0;
|
|
136
|
+
for (let i = 0; i < a.length; i++) d |= a[i] ^ b[i];
|
|
137
|
+
return d === 0;
|
|
43
138
|
}
|
|
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
139
|
|
|
79
140
|
// src/config.ts
|
|
80
141
|
var DEFAULT_RELAY_URL = "https://qrlwallet.com";
|
|
81
142
|
var RELAY_PATH = "/relay";
|
|
82
|
-
var PROTOCOL_VERSION =
|
|
143
|
+
var PROTOCOL_VERSION = 2;
|
|
83
144
|
var SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
84
145
|
var STORAGE_KEY_PREFIX = "@qrlwallet/connect";
|
|
85
146
|
var REQUEST_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
86
147
|
var WALLET_UNRESPONSIVE_MS = 30 * 1e3;
|
|
148
|
+
var RECONNECT_WALLET_PROBE_MS = 12 * 1e3;
|
|
87
149
|
var RESTRICTED_METHODS = /* @__PURE__ */ new Set([
|
|
88
150
|
"qrl_requestAccounts",
|
|
89
151
|
"qrl_sendTransaction",
|
|
90
152
|
"qrl_signTransaction",
|
|
91
|
-
"
|
|
92
|
-
"personal_sign",
|
|
153
|
+
"qrl_signMessage",
|
|
93
154
|
"qrl_signTypedData",
|
|
94
|
-
"qrl_signTypedData_v3",
|
|
95
|
-
"qrl_signTypedData_v4",
|
|
96
155
|
"wallet_addQrlChain",
|
|
97
156
|
"wallet_switchQrlChain"
|
|
98
157
|
]);
|
|
@@ -165,154 +224,300 @@ var ConnectionStatus = /* @__PURE__ */ ((ConnectionStatus2) => {
|
|
|
165
224
|
return ConnectionStatus2;
|
|
166
225
|
})(ConnectionStatus || {});
|
|
167
226
|
|
|
227
|
+
// src/utils/logger.ts
|
|
228
|
+
var debugEnabled = false;
|
|
229
|
+
function setDebug(enabled) {
|
|
230
|
+
debugEnabled = enabled;
|
|
231
|
+
}
|
|
232
|
+
function log(tag, ...args) {
|
|
233
|
+
if (debugEnabled) {
|
|
234
|
+
console.log(`[QRLConnect:${tag}]`, ...args);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function warn(tag, ...args) {
|
|
238
|
+
console.warn(`[QRLConnect:${tag}]`, ...args);
|
|
239
|
+
}
|
|
240
|
+
function error(tag, ...args) {
|
|
241
|
+
console.error(`[QRLConnect:${tag}]`, ...args);
|
|
242
|
+
}
|
|
243
|
+
|
|
168
244
|
// src/KeyExchange.ts
|
|
245
|
+
var textEncoder2 = new TextEncoder();
|
|
246
|
+
var textDecoder = new TextDecoder();
|
|
247
|
+
var HELLO_WALLET = textEncoder2.encode("hello/wallet/v1");
|
|
248
|
+
var HELLO_DAPP = textEncoder2.encode("hello/dapp/v1");
|
|
169
249
|
var KeyExchange = class extends EventEmitter {
|
|
170
|
-
constructor(
|
|
250
|
+
constructor(isOriginator, restored) {
|
|
171
251
|
super();
|
|
172
|
-
this.
|
|
173
|
-
this.
|
|
252
|
+
this.keypair = null;
|
|
253
|
+
this.session = null;
|
|
254
|
+
this.step = "key_handshake_SYN" /* SYN */;
|
|
174
255
|
this.awaitingSynAck = false;
|
|
175
256
|
this.awaitingAck = false;
|
|
176
|
-
this.
|
|
177
|
-
this.ecies = ecies;
|
|
257
|
+
this.keysExchanged = false;
|
|
178
258
|
this.isOriginator = isOriginator;
|
|
179
|
-
if (
|
|
180
|
-
this.
|
|
259
|
+
if (restored) {
|
|
260
|
+
this.session = restored;
|
|
181
261
|
this.keysExchanged = true;
|
|
182
|
-
|
|
262
|
+
this.step = "key_handshake_ACK" /* ACK */;
|
|
263
|
+
log("KeyExchange", "Hydrated from persisted session");
|
|
183
264
|
}
|
|
184
265
|
}
|
|
185
|
-
/**
|
|
186
|
-
|
|
266
|
+
/**
|
|
267
|
+
* Originator (dApp) side: begin a fresh handshake by generating an
|
|
268
|
+
* ephemeral ML-KEM-768 keypair. Returns pk to embed in the QR URI.
|
|
269
|
+
*/
|
|
270
|
+
initiate() {
|
|
271
|
+
if (!this.isOriginator) {
|
|
272
|
+
throw new Error("KeyExchange: responder cannot initiate");
|
|
273
|
+
}
|
|
274
|
+
this.resetInternal(false);
|
|
275
|
+
this.keypair = kemKeygen();
|
|
187
276
|
this.step = "key_handshake_SYN" /* SYN */;
|
|
188
277
|
this.awaitingSynAck = true;
|
|
189
278
|
this.emit("step_change", this.step);
|
|
279
|
+
return this.keypair.pk;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Originator: handle a SYNACK received over the relay.
|
|
283
|
+
* Returns an ACK wire message to send, or null if the SYNACK was a
|
|
284
|
+
* duplicate (handshake already complete).
|
|
285
|
+
*/
|
|
286
|
+
async onSynAck(cid, msg) {
|
|
287
|
+
if (!this.isOriginator) {
|
|
288
|
+
warn("KeyExchange", "Responder received SYNACK \u2014 ignoring");
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
if (!this.awaitingSynAck || !this.keypair) {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
this.awaitingSynAck = false;
|
|
295
|
+
const ct = fromBase64(msg.ct);
|
|
296
|
+
const c0 = fromBase64(msg.c0);
|
|
297
|
+
if (ct.length !== ML_KEM_768_CT_LEN) {
|
|
298
|
+
throw new Error(`KeyExchange: bad ct length ${ct.length}`);
|
|
299
|
+
}
|
|
300
|
+
const ss = kemDecaps(this.keypair.sk, ct);
|
|
301
|
+
const htx = await transcriptHash(cid, this.keypair.pk, ct);
|
|
302
|
+
const key = await deriveAeadKey(ss, htx);
|
|
303
|
+
let hello;
|
|
304
|
+
try {
|
|
305
|
+
hello = await open(key, DIR_WALLET_TX, 0, htx, c0);
|
|
306
|
+
} catch {
|
|
307
|
+
throw new Error(
|
|
308
|
+
"KeyExchange: wallet hello AEAD tag failed (tampered SYNACK, wrong QR, or protocol mismatch)"
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
if (!constantTimeEquals(hello, HELLO_WALLET)) {
|
|
312
|
+
throw new Error("KeyExchange: wallet hello mismatch");
|
|
313
|
+
}
|
|
314
|
+
zeroize(ss);
|
|
315
|
+
zeroize(this.keypair.sk);
|
|
316
|
+
this.keypair = null;
|
|
317
|
+
const c1 = await seal(key, DIR_DAPP_TX, 0, htx, HELLO_DAPP);
|
|
318
|
+
this.session = {
|
|
319
|
+
cid,
|
|
320
|
+
key,
|
|
321
|
+
htx,
|
|
322
|
+
sendDir: DIR_DAPP_TX,
|
|
323
|
+
recvDir: DIR_WALLET_TX,
|
|
324
|
+
sendSeq: 1,
|
|
325
|
+
recvSeq: 1
|
|
326
|
+
};
|
|
327
|
+
this.keysExchanged = true;
|
|
328
|
+
this.step = "key_handshake_ACK" /* ACK */;
|
|
329
|
+
this.emit("keys_exchanged");
|
|
330
|
+
this.emit("step_change", this.step);
|
|
190
331
|
return {
|
|
191
|
-
type: "
|
|
192
|
-
|
|
332
|
+
type: "key_handshake_ACK" /* ACK */,
|
|
333
|
+
c1: toBase64(c1),
|
|
193
334
|
v: PROTOCOL_VERSION
|
|
194
335
|
};
|
|
195
336
|
}
|
|
196
337
|
/**
|
|
197
|
-
*
|
|
198
|
-
* Returns
|
|
338
|
+
* Responder (wallet) side: begin handshake from a scanned QR.
|
|
339
|
+
* Returns the SYNACK wire message to emit on the relay.
|
|
199
340
|
*/
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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;
|
|
341
|
+
async receiveQR(cid, pk) {
|
|
342
|
+
if (this.isOriginator) {
|
|
343
|
+
throw new Error("KeyExchange: originator cannot consume a QR");
|
|
265
344
|
}
|
|
345
|
+
this.resetInternal(false);
|
|
346
|
+
const { ct, ss } = kemEncaps(pk);
|
|
347
|
+
const htx = await transcriptHash(cid, pk, ct);
|
|
348
|
+
const key = await deriveAeadKey(ss, htx);
|
|
349
|
+
const c0 = await seal(key, DIR_WALLET_TX, 0, htx, HELLO_WALLET);
|
|
350
|
+
zeroize(ss);
|
|
351
|
+
this.session = {
|
|
352
|
+
cid,
|
|
353
|
+
key,
|
|
354
|
+
htx,
|
|
355
|
+
sendDir: DIR_WALLET_TX,
|
|
356
|
+
recvDir: DIR_DAPP_TX,
|
|
357
|
+
sendSeq: 1,
|
|
358
|
+
recvSeq: 1
|
|
359
|
+
};
|
|
360
|
+
this.awaitingAck = true;
|
|
361
|
+
this.step = "key_handshake_SYNACK" /* SYNACK */;
|
|
362
|
+
this.emit("step_change", this.step);
|
|
363
|
+
return {
|
|
364
|
+
type: "key_handshake_SYNACK" /* SYNACK */,
|
|
365
|
+
ct: toBase64(ct),
|
|
366
|
+
c0: toBase64(c0),
|
|
367
|
+
v: PROTOCOL_VERSION
|
|
368
|
+
};
|
|
266
369
|
}
|
|
267
|
-
/**
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
370
|
+
/**
|
|
371
|
+
* Responder: verify an incoming ACK and finalize the session.
|
|
372
|
+
* Duplicates are silently ignored (handshake is idempotent).
|
|
373
|
+
*/
|
|
374
|
+
async onAck(msg) {
|
|
375
|
+
if (this.isOriginator) {
|
|
376
|
+
warn("KeyExchange", "Originator received ACK \u2014 ignoring");
|
|
377
|
+
return;
|
|
271
378
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
379
|
+
if (!this.awaitingAck) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (!this.session) {
|
|
383
|
+
throw new Error("KeyExchange: onAck called without a session");
|
|
384
|
+
}
|
|
385
|
+
this.awaitingAck = false;
|
|
386
|
+
const c1 = fromBase64(msg.c1);
|
|
387
|
+
let hello;
|
|
388
|
+
try {
|
|
389
|
+
hello = await open(this.session.key, DIR_DAPP_TX, 0, this.session.htx, c1);
|
|
390
|
+
} catch {
|
|
391
|
+
throw new Error("KeyExchange: dApp hello AEAD tag failed");
|
|
392
|
+
}
|
|
393
|
+
if (!constantTimeEquals(hello, HELLO_DAPP)) {
|
|
394
|
+
throw new Error("KeyExchange: dApp hello mismatch");
|
|
395
|
+
}
|
|
396
|
+
this.keysExchanged = true;
|
|
397
|
+
this.step = "key_handshake_ACK" /* ACK */;
|
|
398
|
+
this.emit("keys_exchanged");
|
|
399
|
+
}
|
|
400
|
+
/** Encrypt a string for the counterparty. Returns base64. */
|
|
401
|
+
async encryptMessage(data) {
|
|
402
|
+
if (!this.session) {
|
|
403
|
+
throw new Error("KeyExchange: cannot encrypt \u2014 session not established");
|
|
404
|
+
}
|
|
405
|
+
const pt = textEncoder2.encode(data);
|
|
406
|
+
const ct = await seal(
|
|
407
|
+
this.session.key,
|
|
408
|
+
this.session.sendDir,
|
|
409
|
+
this.session.sendSeq,
|
|
410
|
+
this.session.htx,
|
|
411
|
+
pt
|
|
412
|
+
);
|
|
413
|
+
this.session.sendSeq++;
|
|
414
|
+
return toBase64(ct);
|
|
415
|
+
}
|
|
416
|
+
/** Decrypt a base64 ciphertext from the counterparty. */
|
|
417
|
+
async decryptMessage(b64) {
|
|
418
|
+
if (!this.session) {
|
|
419
|
+
throw new Error("KeyExchange: cannot decrypt \u2014 session not established");
|
|
420
|
+
}
|
|
421
|
+
const ct = fromBase64(b64);
|
|
422
|
+
const pt = await open(
|
|
423
|
+
this.session.key,
|
|
424
|
+
this.session.recvDir,
|
|
425
|
+
this.session.recvSeq,
|
|
426
|
+
this.session.htx,
|
|
427
|
+
ct
|
|
428
|
+
);
|
|
429
|
+
this.session.recvSeq++;
|
|
430
|
+
return textDecoder.decode(pt);
|
|
431
|
+
}
|
|
432
|
+
/** Reset all state for a fresh handshake. Zeroes any live secret-key buffer. */
|
|
279
433
|
reset() {
|
|
280
|
-
this.
|
|
434
|
+
this.resetInternal(true);
|
|
435
|
+
}
|
|
436
|
+
resetInternal(emit) {
|
|
437
|
+
if (this.keypair) {
|
|
438
|
+
zeroize(this.keypair.sk);
|
|
439
|
+
this.keypair = null;
|
|
440
|
+
}
|
|
441
|
+
this.session = null;
|
|
281
442
|
this.keysExchanged = false;
|
|
282
443
|
this.awaitingSynAck = false;
|
|
283
444
|
this.awaitingAck = false;
|
|
284
445
|
this.step = "key_handshake_SYN" /* SYN */;
|
|
285
|
-
|
|
446
|
+
if (emit) this.emit("step_change", this.step);
|
|
286
447
|
}
|
|
287
448
|
areKeysExchanged() {
|
|
288
449
|
return this.keysExchanged;
|
|
289
450
|
}
|
|
290
|
-
|
|
291
|
-
return this.
|
|
451
|
+
getSession() {
|
|
452
|
+
return this.session;
|
|
292
453
|
}
|
|
293
454
|
getCurrentStep() {
|
|
294
455
|
return this.step;
|
|
295
456
|
}
|
|
457
|
+
/** Export the current session as a JSON-safe persisted record. */
|
|
458
|
+
async exportPersisted() {
|
|
459
|
+
if (!this.session) return null;
|
|
460
|
+
return {
|
|
461
|
+
cid: toBase64(this.session.cid),
|
|
462
|
+
kAeadRaw: toBase64(await exportRawAeadKey(this.session.key)),
|
|
463
|
+
htx: toBase64(this.session.htx),
|
|
464
|
+
sendDir: toBase64(this.session.sendDir),
|
|
465
|
+
recvDir: toBase64(this.session.recvDir),
|
|
466
|
+
sendSeq: this.session.sendSeq,
|
|
467
|
+
recvSeq: this.session.recvSeq
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
/** Rehydrate a Session from its persisted form. */
|
|
471
|
+
static async sessionFromPersisted(p) {
|
|
472
|
+
const key = await importRawAeadKey(fromBase64(p.kAeadRaw));
|
|
473
|
+
return {
|
|
474
|
+
cid: fromBase64(p.cid),
|
|
475
|
+
key,
|
|
476
|
+
htx: fromBase64(p.htx),
|
|
477
|
+
sendDir: fromBase64(p.sendDir),
|
|
478
|
+
recvDir: fromBase64(p.recvDir),
|
|
479
|
+
sendSeq: p.sendSeq,
|
|
480
|
+
recvSeq: p.recvSeq
|
|
481
|
+
};
|
|
482
|
+
}
|
|
296
483
|
};
|
|
297
484
|
|
|
298
485
|
// src/SocketClient.ts
|
|
299
486
|
import { io } from "socket.io-client";
|
|
300
487
|
import EventEmitter2 from "eventemitter3";
|
|
488
|
+
var PENDING_JOIN_TIMEOUT_MS = 2e4;
|
|
301
489
|
var SocketClient = class extends EventEmitter2 {
|
|
302
490
|
constructor(relayUrl, clientType) {
|
|
303
491
|
super();
|
|
304
492
|
this.socket = null;
|
|
305
493
|
this.channelId = null;
|
|
494
|
+
// The dApp's own PK (base64) that must be uploaded with every join
|
|
495
|
+
// attempt so the relay can bind it to the channel. Wallet-side leaves
|
|
496
|
+
// this undefined.
|
|
497
|
+
this.publicKeyBase64 = null;
|
|
306
498
|
this.seq = 0;
|
|
307
499
|
this.pendingJoin = null;
|
|
308
500
|
this.relayUrl = relayUrl;
|
|
309
501
|
this.clientType = clientType;
|
|
310
502
|
}
|
|
503
|
+
/**
|
|
504
|
+
* dApp-only: stash the PK so it gets uploaded on every join_channel
|
|
505
|
+
* attempt (including auto-rejoins after reconnect).
|
|
506
|
+
*/
|
|
507
|
+
setPublicKey(publicKeyBase64) {
|
|
508
|
+
if (this.clientType !== "dapp") {
|
|
509
|
+
throw new Error("SocketClient.setPublicKey is only valid for dApp clients");
|
|
510
|
+
}
|
|
511
|
+
this.publicKeyBase64 = publicKeyBase64;
|
|
512
|
+
}
|
|
311
513
|
/**
|
|
312
514
|
* Connect to the relay server.
|
|
313
515
|
*/
|
|
314
516
|
connect() {
|
|
315
|
-
if (this.socket
|
|
517
|
+
if (this.socket) {
|
|
518
|
+
if (!this.socket.connected) this.socket.connect();
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
316
521
|
log("Socket", `Connecting to ${this.relayUrl}`);
|
|
317
522
|
this.socket = io(this.relayUrl, {
|
|
318
523
|
path: RELAY_PATH,
|
|
@@ -329,17 +534,11 @@ var SocketClient = class extends EventEmitter2 {
|
|
|
329
534
|
if (this.channelId) {
|
|
330
535
|
const reconnectChannelId = this.channelId;
|
|
331
536
|
this.joinChannelNow(reconnectChannelId).then((result) => {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
this.pendingJoin = null;
|
|
335
|
-
}
|
|
336
|
-
this.emit("reconnected");
|
|
537
|
+
this.settlePendingJoin(reconnectChannelId, { result });
|
|
538
|
+
this.emit("reconnected", result);
|
|
337
539
|
}).catch((err) => {
|
|
338
540
|
const reconnectErr = err instanceof Error ? err : new Error(String(err));
|
|
339
|
-
|
|
340
|
-
this.pendingJoin.reject(reconnectErr);
|
|
341
|
-
this.pendingJoin = null;
|
|
342
|
-
}
|
|
541
|
+
this.settlePendingJoin(reconnectChannelId, { error: reconnectErr });
|
|
343
542
|
error("Socket", `Rejoin failed: ${reconnectErr.message}`);
|
|
344
543
|
this.emit("error", reconnectErr);
|
|
345
544
|
});
|
|
@@ -363,7 +562,24 @@ var SocketClient = class extends EventEmitter2 {
|
|
|
363
562
|
});
|
|
364
563
|
}
|
|
365
564
|
/**
|
|
366
|
-
*
|
|
565
|
+
* Resolve or reject the currently-pending `joinChannel` promise, if it
|
|
566
|
+
* matches the given channelId. Clears the watchdog timer on settle so
|
|
567
|
+
* it doesn't fire after the fact.
|
|
568
|
+
*/
|
|
569
|
+
settlePendingJoin(channelId, outcome) {
|
|
570
|
+
if (!this.pendingJoin || this.pendingJoin.channelId !== channelId) return;
|
|
571
|
+
clearTimeout(this.pendingJoin.watchdog);
|
|
572
|
+
const pending = this.pendingJoin;
|
|
573
|
+
this.pendingJoin = null;
|
|
574
|
+
if ("result" in outcome) {
|
|
575
|
+
pending.resolve(outcome.result);
|
|
576
|
+
} else {
|
|
577
|
+
pending.reject(outcome.error);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Join a relay channel. Resolves with any buffered messages plus the
|
|
582
|
+
* dApp's public key bound to the channel (for wallet joins).
|
|
367
583
|
*/
|
|
368
584
|
async joinChannel(channelId) {
|
|
369
585
|
if (!this.socket) {
|
|
@@ -377,6 +593,7 @@ var SocketClient = class extends EventEmitter2 {
|
|
|
377
593
|
if (this.pendingJoin.channelId === channelId) {
|
|
378
594
|
return this.pendingJoin.promise;
|
|
379
595
|
}
|
|
596
|
+
clearTimeout(this.pendingJoin.watchdog);
|
|
380
597
|
this.pendingJoin.reject(new Error("Join request superseded by a newer channel"));
|
|
381
598
|
this.pendingJoin = null;
|
|
382
599
|
}
|
|
@@ -386,11 +603,17 @@ var SocketClient = class extends EventEmitter2 {
|
|
|
386
603
|
resolvePending = resolve;
|
|
387
604
|
rejectPending = reject;
|
|
388
605
|
});
|
|
606
|
+
const watchdog = setTimeout(() => {
|
|
607
|
+
this.settlePendingJoin(channelId, {
|
|
608
|
+
error: new Error(`joinChannel timed out after ${PENDING_JOIN_TIMEOUT_MS}ms`)
|
|
609
|
+
});
|
|
610
|
+
}, PENDING_JOIN_TIMEOUT_MS);
|
|
389
611
|
this.pendingJoin = {
|
|
390
612
|
channelId,
|
|
391
613
|
promise,
|
|
392
614
|
resolve: resolvePending,
|
|
393
|
-
reject: rejectPending
|
|
615
|
+
reject: rejectPending,
|
|
616
|
+
watchdog
|
|
394
617
|
};
|
|
395
618
|
return promise;
|
|
396
619
|
}
|
|
@@ -400,13 +623,22 @@ var SocketClient = class extends EventEmitter2 {
|
|
|
400
623
|
reject(new Error("Socket not connected"));
|
|
401
624
|
return;
|
|
402
625
|
}
|
|
626
|
+
const payload = { channelId, clientType: this.clientType };
|
|
627
|
+
if (this.clientType === "dapp" && this.publicKeyBase64) {
|
|
628
|
+
payload.publicKey = this.publicKeyBase64;
|
|
629
|
+
}
|
|
403
630
|
this.socket.emit(
|
|
404
631
|
"join_channel",
|
|
405
|
-
|
|
632
|
+
payload,
|
|
406
633
|
(response) => {
|
|
407
634
|
if (response.success) {
|
|
408
635
|
log("Socket", `Joined channel ${channelId}`);
|
|
409
|
-
resolve({
|
|
636
|
+
resolve({
|
|
637
|
+
bufferedMessages: response.bufferedMessages || [],
|
|
638
|
+
channelPublicKey: response.channelPublicKey ?? null,
|
|
639
|
+
participants: response.participants || [],
|
|
640
|
+
terminated: response.terminated === true
|
|
641
|
+
});
|
|
410
642
|
} else {
|
|
411
643
|
error("Socket", `Failed to join channel: ${response.error}`);
|
|
412
644
|
reject(new Error(response.error || "Failed to join channel"));
|
|
@@ -443,6 +675,7 @@ var SocketClient = class extends EventEmitter2 {
|
|
|
443
675
|
*/
|
|
444
676
|
leaveChannel() {
|
|
445
677
|
if (this.pendingJoin) {
|
|
678
|
+
clearTimeout(this.pendingJoin.watchdog);
|
|
446
679
|
this.pendingJoin.reject(new Error("Channel left before join completed"));
|
|
447
680
|
this.pendingJoin = null;
|
|
448
681
|
}
|
|
@@ -456,6 +689,7 @@ var SocketClient = class extends EventEmitter2 {
|
|
|
456
689
|
*/
|
|
457
690
|
disconnect() {
|
|
458
691
|
if (this.pendingJoin) {
|
|
692
|
+
clearTimeout(this.pendingJoin.watchdog);
|
|
459
693
|
this.pendingJoin.reject(new Error("Socket disconnected before join completed"));
|
|
460
694
|
this.pendingJoin = null;
|
|
461
695
|
}
|
|
@@ -475,111 +709,267 @@ var SocketClient = class extends EventEmitter2 {
|
|
|
475
709
|
}
|
|
476
710
|
};
|
|
477
711
|
|
|
712
|
+
// src/utils/base45.ts
|
|
713
|
+
var ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:";
|
|
714
|
+
var DECODE = (() => {
|
|
715
|
+
const table = new Int8Array(128).fill(-1);
|
|
716
|
+
for (let i = 0; i < ALPHABET.length; i++) {
|
|
717
|
+
table[ALPHABET.charCodeAt(i)] = i;
|
|
718
|
+
}
|
|
719
|
+
return table;
|
|
720
|
+
})();
|
|
721
|
+
function decodeChar(charCode) {
|
|
722
|
+
if (charCode < 0 || charCode >= 128) return -1;
|
|
723
|
+
return DECODE[charCode];
|
|
724
|
+
}
|
|
725
|
+
function base45Encode(bytes) {
|
|
726
|
+
const n = bytes.length;
|
|
727
|
+
let out = "";
|
|
728
|
+
let i = 0;
|
|
729
|
+
while (i + 2 <= n) {
|
|
730
|
+
const v = bytes[i] << 8 | bytes[i + 1];
|
|
731
|
+
const c2 = Math.floor(v / 2025);
|
|
732
|
+
const r = v - c2 * 2025;
|
|
733
|
+
const c1 = Math.floor(r / 45);
|
|
734
|
+
const c0 = r - c1 * 45;
|
|
735
|
+
out += ALPHABET[c0] + ALPHABET[c1] + ALPHABET[c2];
|
|
736
|
+
i += 2;
|
|
737
|
+
}
|
|
738
|
+
if (i < n) {
|
|
739
|
+
const v = bytes[i];
|
|
740
|
+
const c1 = Math.floor(v / 45);
|
|
741
|
+
const c0 = v - c1 * 45;
|
|
742
|
+
out += ALPHABET[c0] + ALPHABET[c1];
|
|
743
|
+
}
|
|
744
|
+
return out;
|
|
745
|
+
}
|
|
746
|
+
function base45Decode(s) {
|
|
747
|
+
const len = s.length;
|
|
748
|
+
if (len === 0) return new Uint8Array(0);
|
|
749
|
+
const full = Math.floor(len / 3);
|
|
750
|
+
const tail = len - full * 3;
|
|
751
|
+
if (tail !== 0 && tail !== 2) {
|
|
752
|
+
throw new Error("base45: invalid input length");
|
|
753
|
+
}
|
|
754
|
+
const outLen = full * 2 + (tail === 2 ? 1 : 0);
|
|
755
|
+
const out = new Uint8Array(outLen);
|
|
756
|
+
for (let g = 0; g < full; g++) {
|
|
757
|
+
const d0 = decodeChar(s.charCodeAt(g * 3));
|
|
758
|
+
const d1 = decodeChar(s.charCodeAt(g * 3 + 1));
|
|
759
|
+
const d2 = decodeChar(s.charCodeAt(g * 3 + 2));
|
|
760
|
+
if (d0 < 0 || d1 < 0 || d2 < 0) {
|
|
761
|
+
throw new Error("base45: invalid character");
|
|
762
|
+
}
|
|
763
|
+
const v = d0 + d1 * 45 + d2 * 2025;
|
|
764
|
+
if (v > 65535) {
|
|
765
|
+
throw new Error("base45: decoded group exceeds 0xFFFF");
|
|
766
|
+
}
|
|
767
|
+
out[g * 2] = v >> 8 & 255;
|
|
768
|
+
out[g * 2 + 1] = v & 255;
|
|
769
|
+
}
|
|
770
|
+
if (tail === 2) {
|
|
771
|
+
const d0 = decodeChar(s.charCodeAt(full * 3));
|
|
772
|
+
const d1 = decodeChar(s.charCodeAt(full * 3 + 1));
|
|
773
|
+
if (d0 < 0 || d1 < 0) {
|
|
774
|
+
throw new Error("base45: invalid character");
|
|
775
|
+
}
|
|
776
|
+
const v = d0 + d1 * 45;
|
|
777
|
+
if (v > 255) {
|
|
778
|
+
throw new Error("base45: decoded byte exceeds 0xFF");
|
|
779
|
+
}
|
|
780
|
+
out[outLen - 1] = v;
|
|
781
|
+
}
|
|
782
|
+
return out;
|
|
783
|
+
}
|
|
784
|
+
|
|
478
785
|
// src/utils/qrUri.ts
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
if (
|
|
489
|
-
|
|
490
|
-
}
|
|
491
|
-
|
|
786
|
+
var MAGIC = new Uint8Array([80, 81, 80, 50]);
|
|
787
|
+
var FP_LABEL = new TextEncoder().encode("pq-fp/v2");
|
|
788
|
+
var CID_LEN = 16;
|
|
789
|
+
var FP_LEN = 32;
|
|
790
|
+
var BLOB_LEN = 4 + CID_LEN + FP_LEN;
|
|
791
|
+
function bs2(u) {
|
|
792
|
+
return u;
|
|
793
|
+
}
|
|
794
|
+
async function computeFingerprint(cid, pk) {
|
|
795
|
+
if (cid.length !== CID_LEN) {
|
|
796
|
+
throw new Error(`qrUri: cid must be ${CID_LEN} bytes`);
|
|
797
|
+
}
|
|
798
|
+
const c = globalThis.crypto;
|
|
799
|
+
if (!c || !c.subtle) {
|
|
800
|
+
throw new Error("qrUri: WebCrypto SubtleCrypto is not available");
|
|
801
|
+
}
|
|
802
|
+
const buf = new Uint8Array(FP_LABEL.length + cid.length + pk.length);
|
|
803
|
+
buf.set(FP_LABEL, 0);
|
|
804
|
+
buf.set(cid, FP_LABEL.length);
|
|
805
|
+
buf.set(pk, FP_LABEL.length + cid.length);
|
|
806
|
+
const digest = await c.subtle.digest("SHA-256", bs2(buf));
|
|
807
|
+
return new Uint8Array(digest);
|
|
808
|
+
}
|
|
809
|
+
function fingerprintEquals(a, b) {
|
|
810
|
+
if (a.length !== b.length) return false;
|
|
811
|
+
let diff = 0;
|
|
812
|
+
for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
|
|
813
|
+
return diff === 0;
|
|
814
|
+
}
|
|
815
|
+
async function generateConnectionURI(cid, pk, relayUrl) {
|
|
816
|
+
if (cid.length !== CID_LEN) {
|
|
817
|
+
throw new Error(`qrUri: cid must be ${CID_LEN} bytes (got ${cid.length})`);
|
|
818
|
+
}
|
|
819
|
+
const fp = await computeFingerprint(cid, pk);
|
|
820
|
+
const blob = new Uint8Array(BLOB_LEN);
|
|
821
|
+
blob.set(MAGIC, 0);
|
|
822
|
+
blob.set(cid, 4);
|
|
823
|
+
blob.set(fp, 4 + CID_LEN);
|
|
824
|
+
const params = new URLSearchParams({ q: base45Encode(blob) });
|
|
825
|
+
if (relayUrl) params.set("r", relayUrl);
|
|
826
|
+
return `qrlconnect://?${params.toString()}`;
|
|
827
|
+
}
|
|
828
|
+
var PQP1_MAGIC = new Uint8Array([80, 81, 80, 49]);
|
|
829
|
+
function startsWith(buf, prefix) {
|
|
830
|
+
if (buf.length < prefix.length) return false;
|
|
831
|
+
for (let i = 0; i < prefix.length; i++) {
|
|
832
|
+
if (buf[i] !== prefix[i]) return false;
|
|
833
|
+
}
|
|
834
|
+
return true;
|
|
492
835
|
}
|
|
493
|
-
function parseConnectionURI(uri) {
|
|
836
|
+
async function parseConnectionURI(uri) {
|
|
837
|
+
if (typeof uri !== "string" || uri.length === 0) {
|
|
838
|
+
throw new Error("qrUri: empty URI");
|
|
839
|
+
}
|
|
840
|
+
if (!/^qrlconnect:/i.test(uri)) {
|
|
841
|
+
throw new Error("qrUri: not a qrlconnect URI");
|
|
842
|
+
}
|
|
843
|
+
let params;
|
|
494
844
|
try {
|
|
495
|
-
const
|
|
496
|
-
|
|
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
|
-
};
|
|
845
|
+
const swapped = new URL(uri.replace(/^qrlconnect:\/?\/?/i, "https://qrlconnect/"));
|
|
846
|
+
params = swapped.searchParams;
|
|
515
847
|
} catch {
|
|
516
|
-
|
|
848
|
+
throw new Error("qrUri: malformed URI");
|
|
849
|
+
}
|
|
850
|
+
if (params.has("channelId") || params.has("pubKey")) {
|
|
851
|
+
throw new Error(
|
|
852
|
+
"qrUri: legacy v1 URI detected \u2014 this wallet and this dApp must both run protocol v2"
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
const q = params.get("q");
|
|
856
|
+
if (!q) {
|
|
857
|
+
throw new Error("qrUri: missing q parameter");
|
|
858
|
+
}
|
|
859
|
+
let blob;
|
|
860
|
+
try {
|
|
861
|
+
blob = base45Decode(q);
|
|
862
|
+
} catch (err) {
|
|
863
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
864
|
+
throw new Error(`qrUri: base45 decode failed: ${msg}`);
|
|
865
|
+
}
|
|
866
|
+
if (blob.length !== BLOB_LEN) {
|
|
867
|
+
if (blob.length === 1208 && startsWith(blob, PQP1_MAGIC)) {
|
|
868
|
+
throw new Error("qrUri: legacy PQP1 URI detected \u2014 regenerate the QR with a v2.0+ dApp SDK");
|
|
869
|
+
}
|
|
870
|
+
throw new Error(`qrUri: expected ${BLOB_LEN}-byte blob, got ${blob.length}`);
|
|
871
|
+
}
|
|
872
|
+
if (!startsWith(blob, MAGIC)) {
|
|
873
|
+
throw new Error("qrUri: bad PQP2 magic");
|
|
874
|
+
}
|
|
875
|
+
const cid = blob.slice(4, 4 + CID_LEN);
|
|
876
|
+
const fp = blob.slice(4 + CID_LEN, 4 + CID_LEN + FP_LEN);
|
|
877
|
+
const relayUrl = params.get("r") || void 0;
|
|
878
|
+
return { cid, fp, relayUrl };
|
|
879
|
+
}
|
|
880
|
+
function cidToString(cid) {
|
|
881
|
+
if (cid.length !== CID_LEN) {
|
|
882
|
+
throw new Error(`cidToString: expected ${CID_LEN}-byte cid`);
|
|
883
|
+
}
|
|
884
|
+
let hex = "";
|
|
885
|
+
for (let i = 0; i < CID_LEN; i++) {
|
|
886
|
+
hex += cid[i].toString(16).padStart(2, "0");
|
|
887
|
+
}
|
|
888
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
889
|
+
}
|
|
890
|
+
function cidFromString(s) {
|
|
891
|
+
const hex = s.replace(/-/g, "");
|
|
892
|
+
if (!/^[0-9a-f]{32}$/i.test(hex)) {
|
|
893
|
+
throw new Error("cidFromString: not a 128-bit hex string");
|
|
894
|
+
}
|
|
895
|
+
const out = new Uint8Array(CID_LEN);
|
|
896
|
+
for (let i = 0; i < CID_LEN; i++) {
|
|
897
|
+
out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
517
898
|
}
|
|
899
|
+
return out;
|
|
900
|
+
}
|
|
901
|
+
function cidRandom() {
|
|
902
|
+
const c = globalThis.crypto;
|
|
903
|
+
if (!c || typeof c.getRandomValues !== "function") {
|
|
904
|
+
throw new Error("qrUri: crypto.getRandomValues is not available");
|
|
905
|
+
}
|
|
906
|
+
return c.getRandomValues(new Uint8Array(CID_LEN));
|
|
518
907
|
}
|
|
519
908
|
|
|
520
909
|
// src/ConnectionManager.ts
|
|
521
910
|
var DAPP_PARTICIPANT_CONFLICT_ERROR_MSG = "dapp participant is already connected";
|
|
911
|
+
var TERMINATE_SEND_TIMEOUT_MS = 800;
|
|
522
912
|
var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
|
|
523
913
|
constructor(options) {
|
|
524
914
|
super();
|
|
915
|
+
this.keyExchange = null;
|
|
525
916
|
this.status = "disconnected" /* DISCONNECTED */;
|
|
526
917
|
this.connectedAccounts = [];
|
|
527
918
|
this.unresponsiveTimer = null;
|
|
528
|
-
this.
|
|
919
|
+
this.reconnectProbeTimer = null;
|
|
529
920
|
this.failedReconnects = 0;
|
|
530
|
-
this.synSent = false;
|
|
531
|
-
this.synAttempts = 0;
|
|
532
921
|
this.walletPresent = false;
|
|
922
|
+
this.pendingRestore = null;
|
|
923
|
+
this.messageQueue = Promise.resolve();
|
|
533
924
|
this.dappMetadata = options.dappMetadata;
|
|
534
925
|
this.relayUrl = options.relayUrl || DEFAULT_RELAY_URL;
|
|
535
926
|
this.chainId = options.chainId || "0x0";
|
|
536
927
|
this.storageKey = options.storageKey || `${STORAGE_KEY_PREFIX}:session`;
|
|
537
|
-
const
|
|
538
|
-
if (
|
|
539
|
-
this.channelId =
|
|
540
|
-
this.
|
|
541
|
-
this.
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
session.otherPublicKey || void 0
|
|
546
|
-
);
|
|
547
|
-
this.connectedAccounts = session.connectedAccounts;
|
|
548
|
-
log("ConnectionManager", `Restored session for channel ${this.channelId}`);
|
|
928
|
+
const stored = this.readStoredSession();
|
|
929
|
+
if (stored) {
|
|
930
|
+
this.channelId = stored.channelId;
|
|
931
|
+
this.connectedAccounts = stored.connectedAccounts;
|
|
932
|
+
this.chainId = stored.chainId;
|
|
933
|
+
this.dappMetadata = stored.dappMetadata;
|
|
934
|
+
this.pendingRestore = stored;
|
|
935
|
+
log("ConnectionManager", `Found persisted session for channel ${this.channelId}`);
|
|
549
936
|
} else {
|
|
550
937
|
this.channelId = uuidv4();
|
|
551
|
-
this.ecies = new ECIESClient();
|
|
552
|
-
this.keyExchange = new KeyExchange(this.ecies, true);
|
|
553
938
|
}
|
|
554
939
|
this.socketClient = new SocketClient(this.relayUrl, "dapp");
|
|
555
940
|
this.setupSocketListeners();
|
|
556
|
-
this.setupKeyExchangeListeners();
|
|
557
941
|
}
|
|
942
|
+
// ── Setup ──────────────────────────────────────────────────
|
|
558
943
|
setupSocketListeners() {
|
|
559
944
|
this.socketClient.on("connected", () => {
|
|
560
945
|
if (this.status === "reconnecting" /* RECONNECTING */) {
|
|
561
946
|
this.failedReconnects = 0;
|
|
562
|
-
log("ConnectionManager", "Reconnected to relay");
|
|
563
947
|
}
|
|
564
948
|
});
|
|
565
|
-
this.socketClient.on("reconnected", () => {
|
|
566
|
-
if (
|
|
949
|
+
this.socketClient.on("reconnected", (result) => {
|
|
950
|
+
if (result?.terminated) {
|
|
951
|
+
log("ConnectionManager", "Channel terminated, observed on auto-reconnect");
|
|
952
|
+
this.handleSessionTerminated();
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
if (result) {
|
|
956
|
+
this.walletPresent = result.participants.includes("wallet");
|
|
957
|
+
}
|
|
958
|
+
if (this.keyExchange?.areKeysExchanged()) {
|
|
567
959
|
if (this.walletPresent) {
|
|
960
|
+
this.clearReconnectProbe();
|
|
568
961
|
this.setStatus("connected" /* CONNECTED */);
|
|
569
962
|
this.failedReconnects = 0;
|
|
570
963
|
} else {
|
|
571
964
|
this.setStatus("waiting" /* WAITING */);
|
|
965
|
+
this.armReconnectProbe();
|
|
572
966
|
}
|
|
573
|
-
this.synAttempts = 0;
|
|
574
|
-
this.clearSynRetryTimer();
|
|
575
967
|
} else {
|
|
576
|
-
this.
|
|
968
|
+
this.setStatus("waiting" /* WAITING */);
|
|
577
969
|
}
|
|
578
970
|
});
|
|
579
|
-
this.socketClient.on("disconnected", (
|
|
971
|
+
this.socketClient.on("disconnected", () => {
|
|
580
972
|
this.walletPresent = false;
|
|
581
|
-
this.synSent = false;
|
|
582
|
-
this.clearSynRetryTimer();
|
|
583
973
|
if (this.status === "connected" /* CONNECTED */) {
|
|
584
974
|
this.setStatus("reconnecting" /* RECONNECTING */);
|
|
585
975
|
this.failedReconnects++;
|
|
@@ -589,7 +979,7 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
|
|
|
589
979
|
}
|
|
590
980
|
});
|
|
591
981
|
this.socketClient.on("message", (data) => {
|
|
592
|
-
this.
|
|
982
|
+
this.enqueueRelayMessage(data);
|
|
593
983
|
});
|
|
594
984
|
this.socketClient.on("error", (err) => {
|
|
595
985
|
this.emit("error", err);
|
|
@@ -599,20 +989,21 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
|
|
|
599
989
|
this.walletPresent = true;
|
|
600
990
|
log("ConnectionManager", "Wallet joined channel");
|
|
601
991
|
this.clearUnresponsiveTimer();
|
|
602
|
-
|
|
992
|
+
this.clearReconnectProbe();
|
|
993
|
+
if (this.keyExchange?.areKeysExchanged()) {
|
|
603
994
|
this.failedReconnects = 0;
|
|
604
995
|
this.setStatus("connected" /* CONNECTED */);
|
|
605
|
-
} else if (!this.synSent) {
|
|
606
|
-
this.sendSYN();
|
|
607
996
|
}
|
|
608
997
|
}
|
|
998
|
+
if (data.event === "close") {
|
|
999
|
+
log("ConnectionManager", "Wallet closed the channel (explicit terminate)");
|
|
1000
|
+
this.handleSessionTerminated();
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
609
1003
|
if (data.event === "disconnect" || data.event === "leave") {
|
|
610
1004
|
if (data.clientType === "wallet" || !data.clientType) {
|
|
611
1005
|
this.walletPresent = false;
|
|
612
|
-
if (
|
|
613
|
-
this.synSent = false;
|
|
614
|
-
this.clearSynRetryTimer();
|
|
615
|
-
} else if (this.status === "connected" /* CONNECTED */ || this.status === "reconnecting" /* RECONNECTING */) {
|
|
1006
|
+
if (this.keyExchange?.areKeysExchanged() && (this.status === "connected" /* CONNECTED */ || this.status === "reconnecting" /* RECONNECTING */)) {
|
|
616
1007
|
this.setStatus("waiting" /* WAITING */);
|
|
617
1008
|
}
|
|
618
1009
|
log("ConnectionManager", "Wallet left channel");
|
|
@@ -621,15 +1012,15 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
|
|
|
621
1012
|
});
|
|
622
1013
|
}
|
|
623
1014
|
setupKeyExchangeListeners() {
|
|
1015
|
+
if (!this.keyExchange) return;
|
|
624
1016
|
this.keyExchange.on("keys_exchanged", () => {
|
|
625
1017
|
log("ConnectionManager", "Key exchange complete");
|
|
1018
|
+
this.clearReconnectProbe();
|
|
626
1019
|
this.setStatus("connected" /* CONNECTED */);
|
|
627
1020
|
this.walletPresent = true;
|
|
628
|
-
this.
|
|
629
|
-
this.
|
|
630
|
-
this.
|
|
631
|
-
this.saveSession();
|
|
632
|
-
this.sendEncrypted({
|
|
1021
|
+
this.failedReconnects = 0;
|
|
1022
|
+
void this.persistSession();
|
|
1023
|
+
void this.sendEncrypted({
|
|
633
1024
|
type: "originator_info" /* ORIGINATOR_INFO */,
|
|
634
1025
|
originatorInfo: {
|
|
635
1026
|
...this.dappMetadata,
|
|
@@ -638,16 +1029,25 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
|
|
|
638
1029
|
});
|
|
639
1030
|
});
|
|
640
1031
|
}
|
|
1032
|
+
// ── Public API ─────────────────────────────────────────────
|
|
641
1033
|
/**
|
|
642
|
-
* Generate
|
|
1034
|
+
* Generate a new v2 connection URI. Rotates channel id and keypair.
|
|
1035
|
+
* Returns a `qrlconnect://?q=…` URI safe for QR-rendering or deep-link.
|
|
643
1036
|
*/
|
|
644
1037
|
async getConnectionURI(retryOnConflict = true) {
|
|
645
1038
|
this.setStatus("connecting" /* CONNECTING */);
|
|
646
|
-
this.
|
|
1039
|
+
this.clearReconnectProbe();
|
|
1040
|
+
this.pendingRestore = null;
|
|
647
1041
|
this.walletPresent = false;
|
|
648
|
-
this.
|
|
649
|
-
|
|
650
|
-
|
|
1042
|
+
if (this.keyExchange) {
|
|
1043
|
+
this.keyExchange.reset();
|
|
1044
|
+
} else {
|
|
1045
|
+
this.keyExchange = new KeyExchange(true);
|
|
1046
|
+
this.setupKeyExchangeListeners();
|
|
1047
|
+
}
|
|
1048
|
+
const pk = this.keyExchange.initiate();
|
|
1049
|
+
this.channelId = uuidv4();
|
|
1050
|
+
this.socketClient.setPublicKey(toBase64(pk));
|
|
651
1051
|
this.socketClient.connect();
|
|
652
1052
|
try {
|
|
653
1053
|
await this.socketClient.joinChannel(this.channelId);
|
|
@@ -657,48 +1057,73 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
|
|
|
657
1057
|
"ConnectionManager",
|
|
658
1058
|
"Channel already has an active dApp participant. Rotating to a fresh channel."
|
|
659
1059
|
);
|
|
660
|
-
this.
|
|
1060
|
+
this.channelId = uuidv4();
|
|
661
1061
|
return this.getConnectionURI(false);
|
|
662
1062
|
}
|
|
663
1063
|
this.setStatus("disconnected" /* DISCONNECTED */);
|
|
664
1064
|
throw err;
|
|
665
1065
|
}
|
|
666
1066
|
this.setStatus("waiting" /* WAITING */);
|
|
667
|
-
const uri = generateConnectionURI(
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
chainId: this.chainId,
|
|
674
|
-
relayUrl: this.relayUrl
|
|
675
|
-
});
|
|
676
|
-
log("ConnectionManager", `Generated connection URI for channel ${this.channelId}`);
|
|
1067
|
+
const uri = await generateConnectionURI(
|
|
1068
|
+
cidFromString(this.channelId),
|
|
1069
|
+
pk,
|
|
1070
|
+
this.relayUrl === DEFAULT_RELAY_URL ? void 0 : this.relayUrl
|
|
1071
|
+
);
|
|
1072
|
+
log("ConnectionManager", `Generated v2 connection URI for channel ${this.channelId}`);
|
|
677
1073
|
return uri;
|
|
678
1074
|
}
|
|
679
1075
|
/**
|
|
680
1076
|
* Reconnect to an existing session.
|
|
1077
|
+
* Returns false if there is nothing to restore.
|
|
681
1078
|
*/
|
|
682
1079
|
async reconnect() {
|
|
683
|
-
|
|
684
|
-
if (!session) return false;
|
|
1080
|
+
if (!this.pendingRestore) return false;
|
|
685
1081
|
this.setStatus("reconnecting" /* RECONNECTING */);
|
|
686
1082
|
this.walletPresent = false;
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
1083
|
+
try {
|
|
1084
|
+
const session = await KeyExchange.sessionFromPersisted(this.pendingRestore.keyExchange);
|
|
1085
|
+
this.keyExchange = new KeyExchange(true, session);
|
|
1086
|
+
this.setupKeyExchangeListeners();
|
|
1087
|
+
} catch (err) {
|
|
1088
|
+
error("ConnectionManager", "Failed to hydrate persisted session:", err);
|
|
1089
|
+
this.clearSession();
|
|
1090
|
+
this.pendingRestore = null;
|
|
1091
|
+
this.setStatus("disconnected" /* DISCONNECTED */);
|
|
1092
|
+
return false;
|
|
1093
|
+
}
|
|
1094
|
+
return this.joinAndSettle();
|
|
1095
|
+
}
|
|
1096
|
+
/**
|
|
1097
|
+
* (Re)open the socket, join the persisted channel, drain buffered relay
|
|
1098
|
+
* messages, and settle status. Shared by the cold-restore reconnect() and
|
|
1099
|
+
* the warm resume() path whose socket was torn down by the reconnect probe.
|
|
1100
|
+
* Assumes channelId and a hydrated keyExchange are already in place, and
|
|
1101
|
+
* that the caller has reset walletPresent + set RECONNECTING status.
|
|
1102
|
+
*/
|
|
1103
|
+
async joinAndSettle() {
|
|
690
1104
|
this.socketClient.connect();
|
|
691
1105
|
try {
|
|
692
|
-
const { bufferedMessages } = await this.socketClient.joinChannel(
|
|
1106
|
+
const { bufferedMessages, participants, terminated } = await this.socketClient.joinChannel(
|
|
1107
|
+
this.channelId
|
|
1108
|
+
);
|
|
1109
|
+
if (terminated) {
|
|
1110
|
+
log("ConnectionManager", "Stored session was terminated by the wallet; dropping it");
|
|
1111
|
+
this.handleSessionTerminated();
|
|
1112
|
+
return false;
|
|
1113
|
+
}
|
|
1114
|
+
if (participants.includes("wallet")) {
|
|
1115
|
+
this.walletPresent = true;
|
|
1116
|
+
}
|
|
693
1117
|
for (const msg of bufferedMessages) {
|
|
694
|
-
this.
|
|
1118
|
+
this.enqueueRelayMessage(msg);
|
|
695
1119
|
}
|
|
696
|
-
|
|
1120
|
+
await this.messageQueue;
|
|
1121
|
+
if (this.keyExchange?.areKeysExchanged() && this.walletPresent) {
|
|
1122
|
+
this.clearReconnectProbe();
|
|
697
1123
|
this.setStatus("connected" /* CONNECTED */);
|
|
698
|
-
} else if (this.keyExchange.areKeysExchanged()) {
|
|
699
|
-
this.setStatus("waiting" /* WAITING */);
|
|
700
1124
|
} else {
|
|
701
|
-
this.
|
|
1125
|
+
this.setStatus("waiting" /* WAITING */);
|
|
1126
|
+
this.armReconnectProbe();
|
|
702
1127
|
}
|
|
703
1128
|
return true;
|
|
704
1129
|
} catch (err) {
|
|
@@ -708,10 +1133,69 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
|
|
|
708
1133
|
}
|
|
709
1134
|
}
|
|
710
1135
|
/**
|
|
711
|
-
*
|
|
1136
|
+
* Nudge the connection back to life after the dApp tab / wallet app was
|
|
1137
|
+
* backgrounded. Idempotent and safe to call from visibilitychange / online
|
|
1138
|
+
* / pageshow handlers. If a restored session has not yet been hydrated,
|
|
1139
|
+
* runs the full reconnect(); otherwise just re-opens the socket, whose
|
|
1140
|
+
* own connect handler re-joins the channel and drains the relay buffer.
|
|
1141
|
+
*/
|
|
1142
|
+
resume() {
|
|
1143
|
+
if (this.status === "connected" /* CONNECTED */) return;
|
|
1144
|
+
if (this.pendingRestore && !this.keyExchange?.areKeysExchanged()) {
|
|
1145
|
+
void this.reconnect();
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
if (this.channelId && this.keyExchange?.areKeysExchanged()) {
|
|
1149
|
+
if (this.socketClient.getChannelId()) {
|
|
1150
|
+
this.socketClient.connect();
|
|
1151
|
+
} else {
|
|
1152
|
+
this.setStatus("reconnecting" /* RECONNECTING */);
|
|
1153
|
+
this.walletPresent = false;
|
|
1154
|
+
void this.joinAndSettle();
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
/**
|
|
1159
|
+
* A session was terminated for good (wallet sent a relay 'close', or the
|
|
1160
|
+
* join ack reported a tombstone). Clear local state and surface
|
|
1161
|
+
* DISCONNECTED so the consumer drops to a fresh-pairing UI.
|
|
1162
|
+
*/
|
|
1163
|
+
handleSessionTerminated() {
|
|
1164
|
+
this.clearReconnectProbe();
|
|
1165
|
+
this.clearUnresponsiveTimer();
|
|
1166
|
+
this.walletPresent = false;
|
|
1167
|
+
this.pendingRestore = null;
|
|
1168
|
+
this.keyExchange = null;
|
|
1169
|
+
this.socketClient.leaveChannel();
|
|
1170
|
+
this.socketClient.disconnect();
|
|
1171
|
+
this.clearSession();
|
|
1172
|
+
this.setStatus("disconnected" /* DISCONNECTED */);
|
|
1173
|
+
}
|
|
1174
|
+
armReconnectProbe() {
|
|
1175
|
+
this.clearReconnectProbe();
|
|
1176
|
+
this.reconnectProbeTimer = setTimeout(() => {
|
|
1177
|
+
this.reconnectProbeTimer = null;
|
|
1178
|
+
if (this.walletPresent) return;
|
|
1179
|
+
warn(
|
|
1180
|
+
"ConnectionManager",
|
|
1181
|
+
`No wallet rejoined within ${RECONNECT_WALLET_PROBE_MS}ms; treating reconnect as dead`
|
|
1182
|
+
);
|
|
1183
|
+
this.socketClient.leaveChannel();
|
|
1184
|
+
this.socketClient.disconnect();
|
|
1185
|
+
this.setStatus("disconnected" /* DISCONNECTED */);
|
|
1186
|
+
}, RECONNECT_WALLET_PROBE_MS);
|
|
1187
|
+
}
|
|
1188
|
+
clearReconnectProbe() {
|
|
1189
|
+
if (this.reconnectProbeTimer) {
|
|
1190
|
+
clearTimeout(this.reconnectProbeTimer);
|
|
1191
|
+
this.reconnectProbeTimer = null;
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
/**
|
|
1195
|
+
* Send a JSON-RPC request to the wallet (async, fire-and-forget).
|
|
712
1196
|
*/
|
|
713
1197
|
sendJsonRpc(request) {
|
|
714
|
-
if (!this.keyExchange
|
|
1198
|
+
if (!this.keyExchange?.areKeysExchanged()) {
|
|
715
1199
|
throw new Error("Not connected: key exchange not complete");
|
|
716
1200
|
}
|
|
717
1201
|
this.sendEncrypted({
|
|
@@ -720,16 +1204,93 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
|
|
|
720
1204
|
id: request.id,
|
|
721
1205
|
method: request.method,
|
|
722
1206
|
params: request.params
|
|
1207
|
+
}).catch((err) => {
|
|
1208
|
+
error("ConnectionManager", "Failed to send JSON-RPC:", err);
|
|
723
1209
|
});
|
|
724
1210
|
this.startUnresponsiveTimer();
|
|
725
1211
|
}
|
|
1212
|
+
getStatus() {
|
|
1213
|
+
return this.status;
|
|
1214
|
+
}
|
|
1215
|
+
getAccounts() {
|
|
1216
|
+
return this.connectedAccounts;
|
|
1217
|
+
}
|
|
1218
|
+
getChainId() {
|
|
1219
|
+
return this.chainId;
|
|
1220
|
+
}
|
|
1221
|
+
getChannelId() {
|
|
1222
|
+
return this.channelId;
|
|
1223
|
+
}
|
|
1224
|
+
/** Check (sync) if a persisted session exists and has not expired. */
|
|
1225
|
+
hasStoredSession() {
|
|
1226
|
+
if (typeof localStorage === "undefined") return false;
|
|
1227
|
+
try {
|
|
1228
|
+
const raw = localStorage.getItem(this.storageKey);
|
|
1229
|
+
if (!raw) return false;
|
|
1230
|
+
const session = JSON.parse(raw);
|
|
1231
|
+
if (session.version !== 2) return false;
|
|
1232
|
+
return Date.now() - session.createdAt <= SESSION_TTL_MS;
|
|
1233
|
+
} catch {
|
|
1234
|
+
return false;
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
/**
|
|
1238
|
+
* Best-effort TERMINATE delivery before we tear the socket down. Without
|
|
1239
|
+
* awaiting, the socket.disconnect() below would win the race against the
|
|
1240
|
+
* outbound emit, the wallet would only see `participants_changed:
|
|
1241
|
+
* disconnect` and enter its stale-session grace period instead of an
|
|
1242
|
+
* instant disconnect. Mirrors the wallet side's pattern.
|
|
1243
|
+
*/
|
|
1244
|
+
async flushTerminate() {
|
|
1245
|
+
if (!this.keyExchange?.areKeysExchanged()) return;
|
|
1246
|
+
const send = this.sendEncrypted({ type: "terminate" /* TERMINATE */ }).catch(() => {
|
|
1247
|
+
});
|
|
1248
|
+
const timeout = new Promise((resolve) => setTimeout(resolve, TERMINATE_SEND_TIMEOUT_MS));
|
|
1249
|
+
await Promise.race([send, timeout]);
|
|
1250
|
+
}
|
|
726
1251
|
/**
|
|
727
|
-
*
|
|
1252
|
+
* Reset to a fresh channel + keypair. Sends TERMINATE to any live peer,
|
|
1253
|
+
* drops the persisted session, and prepares a clean state for
|
|
1254
|
+
* getConnectionURI() to be called again.
|
|
728
1255
|
*/
|
|
729
|
-
|
|
1256
|
+
async resetForNewChannel() {
|
|
1257
|
+
this.clearUnresponsiveTimer();
|
|
1258
|
+
this.clearReconnectProbe();
|
|
1259
|
+
this.walletPresent = false;
|
|
1260
|
+
await this.flushTerminate();
|
|
1261
|
+
this.socketClient.leaveChannel();
|
|
1262
|
+
this.socketClient.disconnect();
|
|
1263
|
+
this.clearSession();
|
|
1264
|
+
this.connectedAccounts = [];
|
|
1265
|
+
this.pendingRestore = null;
|
|
1266
|
+
this.channelId = uuidv4();
|
|
1267
|
+
this.keyExchange = null;
|
|
1268
|
+
this.socketClient = new SocketClient(this.relayUrl, "dapp");
|
|
1269
|
+
this.setupSocketListeners();
|
|
1270
|
+
this.setStatus("disconnected" /* DISCONNECTED */);
|
|
1271
|
+
}
|
|
1272
|
+
async disconnect() {
|
|
1273
|
+
this.clearUnresponsiveTimer();
|
|
1274
|
+
this.clearReconnectProbe();
|
|
1275
|
+
this.walletPresent = false;
|
|
1276
|
+
await this.flushTerminate();
|
|
1277
|
+
this.socketClient.leaveChannel();
|
|
1278
|
+
this.socketClient.disconnect();
|
|
1279
|
+
this.setStatus("disconnected" /* DISCONNECTED */);
|
|
1280
|
+
this.clearSession();
|
|
1281
|
+
this.connectedAccounts = [];
|
|
1282
|
+
this.pendingRestore = null;
|
|
1283
|
+
this.keyExchange = null;
|
|
1284
|
+
}
|
|
1285
|
+
// ── Internals ──────────────────────────────────────────────
|
|
1286
|
+
enqueueRelayMessage(data) {
|
|
1287
|
+
this.messageQueue = this.messageQueue.then(() => this.handleRelayMessage(data)).catch((err) => error("ConnectionManager", "messageQueue handler error:", err));
|
|
1288
|
+
}
|
|
1289
|
+
async handleRelayMessage(data) {
|
|
730
1290
|
if (data.clientType === "wallet") {
|
|
731
1291
|
this.walletPresent = true;
|
|
732
|
-
|
|
1292
|
+
this.clearReconnectProbe();
|
|
1293
|
+
if (this.keyExchange?.areKeysExchanged() && this.status !== "connected" /* CONNECTED */) {
|
|
733
1294
|
this.failedReconnects = 0;
|
|
734
1295
|
this.setStatus("connected" /* CONNECTED */);
|
|
735
1296
|
}
|
|
@@ -737,16 +1298,18 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
|
|
|
737
1298
|
const message = data.message;
|
|
738
1299
|
if (typeof message === "object" && message !== null) {
|
|
739
1300
|
const msg = message;
|
|
740
|
-
if (msg.type === "
|
|
741
|
-
this.
|
|
742
|
-
|
|
743
|
-
|
|
1301
|
+
if (msg.type === "key_handshake_SYNACK" /* SYNACK */) {
|
|
1302
|
+
await this.handleSynAck(message);
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
if (msg.type === "key_handshake_SYN" /* SYN */ || msg.type === "key_handshake_ACK" /* ACK */) {
|
|
1306
|
+
warn("ConnectionManager", `Unexpected ${msg.type} on dApp side \u2014 ignoring`);
|
|
744
1307
|
return;
|
|
745
1308
|
}
|
|
746
1309
|
}
|
|
747
|
-
if (typeof message === "string" && this.keyExchange
|
|
1310
|
+
if (typeof message === "string" && this.keyExchange?.areKeysExchanged()) {
|
|
748
1311
|
try {
|
|
749
|
-
const decrypted = this.keyExchange.decryptMessage(message);
|
|
1312
|
+
const decrypted = await this.keyExchange.decryptMessage(message);
|
|
750
1313
|
const parsed = JSON.parse(decrypted);
|
|
751
1314
|
this.handleDecryptedMessage(parsed);
|
|
752
1315
|
} catch (err) {
|
|
@@ -754,9 +1317,18 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
|
|
|
754
1317
|
}
|
|
755
1318
|
}
|
|
756
1319
|
}
|
|
757
|
-
|
|
1320
|
+
async handleSynAck(msg) {
|
|
1321
|
+
if (!this.keyExchange) return;
|
|
758
1322
|
this.setStatus("key_exchange" /* KEY_EXCHANGE */);
|
|
759
|
-
|
|
1323
|
+
let response;
|
|
1324
|
+
try {
|
|
1325
|
+
response = await this.keyExchange.onSynAck(cidFromString(this.channelId), msg);
|
|
1326
|
+
} catch (err) {
|
|
1327
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
1328
|
+
error("ConnectionManager", "SYNACK processing failed:", e);
|
|
1329
|
+
this.emit("error", e);
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
760
1332
|
if (response) {
|
|
761
1333
|
this.sendPlaintext(response);
|
|
762
1334
|
}
|
|
@@ -773,17 +1345,13 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
|
|
|
773
1345
|
const chainChanged = this.chainId !== nextChainId;
|
|
774
1346
|
this.connectedAccounts = nextAccounts;
|
|
775
1347
|
this.chainId = nextChainId;
|
|
776
|
-
this.
|
|
1348
|
+
void this.persistSession();
|
|
777
1349
|
this.emit("wallet_info", {
|
|
778
1350
|
accounts: this.connectedAccounts,
|
|
779
1351
|
chainId: this.chainId
|
|
780
1352
|
});
|
|
781
|
-
if (accountsChanged)
|
|
782
|
-
|
|
783
|
-
}
|
|
784
|
-
if (chainChanged) {
|
|
785
|
-
this.emit("chain_changed", this.chainId);
|
|
786
|
-
}
|
|
1353
|
+
if (accountsChanged) this.emit("accounts_changed", this.connectedAccounts);
|
|
1354
|
+
if (chainChanged) this.emit("chain_changed", this.chainId);
|
|
787
1355
|
break;
|
|
788
1356
|
}
|
|
789
1357
|
case "jsonrpc" /* JSONRPC */: {
|
|
@@ -793,14 +1361,13 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
|
|
|
793
1361
|
}
|
|
794
1362
|
case "terminate" /* TERMINATE */: {
|
|
795
1363
|
log("ConnectionManager", "Received terminate from wallet");
|
|
796
|
-
this.disconnect();
|
|
1364
|
+
void this.disconnect();
|
|
797
1365
|
break;
|
|
798
1366
|
}
|
|
799
1367
|
default:
|
|
800
1368
|
log("ConnectionManager", `Unhandled message type: ${type}`);
|
|
801
1369
|
}
|
|
802
1370
|
}
|
|
803
|
-
/** Send a plaintext (unencrypted) message through the relay. */
|
|
804
1371
|
sendPlaintext(message) {
|
|
805
1372
|
this.socketClient.sendMessage({
|
|
806
1373
|
id: this.channelId,
|
|
@@ -810,20 +1377,16 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
|
|
|
810
1377
|
error("ConnectionManager", "Failed to send plaintext:", err);
|
|
811
1378
|
});
|
|
812
1379
|
}
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
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);
|
|
1380
|
+
async sendEncrypted(message) {
|
|
1381
|
+
if (!this.keyExchange?.areKeysExchanged()) {
|
|
1382
|
+
throw new Error("sendEncrypted: not connected");
|
|
826
1383
|
}
|
|
1384
|
+
const encrypted = await this.keyExchange.encryptMessage(JSON.stringify(message));
|
|
1385
|
+
await this.socketClient.sendMessage({
|
|
1386
|
+
id: this.channelId,
|
|
1387
|
+
clientType: "dapp",
|
|
1388
|
+
message: encrypted
|
|
1389
|
+
});
|
|
827
1390
|
}
|
|
828
1391
|
startUnresponsiveTimer() {
|
|
829
1392
|
this.clearUnresponsiveTimer();
|
|
@@ -837,46 +1400,6 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
|
|
|
837
1400
|
this.unresponsiveTimer = null;
|
|
838
1401
|
}
|
|
839
1402
|
}
|
|
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
1403
|
setStatus(status) {
|
|
881
1404
|
if (this.status !== status) {
|
|
882
1405
|
this.status = status;
|
|
@@ -889,105 +1412,44 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
|
|
|
889
1412
|
}
|
|
890
1413
|
areArraysEqual(a, b) {
|
|
891
1414
|
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
|
-
}
|
|
1415
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
895
1416
|
return true;
|
|
896
1417
|
}
|
|
897
|
-
|
|
898
|
-
|
|
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() {
|
|
1418
|
+
// ── Persistence ────────────────────────────────────────────
|
|
1419
|
+
async persistSession() {
|
|
970
1420
|
if (typeof localStorage === "undefined") return;
|
|
1421
|
+
if (!this.keyExchange) return;
|
|
1422
|
+
const persistedKex = await this.keyExchange.exportPersisted();
|
|
1423
|
+
if (!persistedKex) return;
|
|
971
1424
|
const session = {
|
|
1425
|
+
version: 2,
|
|
972
1426
|
channelId: this.channelId,
|
|
973
|
-
|
|
974
|
-
otherPublicKey: this.keyExchange.getOtherPublicKey(),
|
|
1427
|
+
keyExchange: persistedKex,
|
|
975
1428
|
dappMetadata: this.dappMetadata,
|
|
976
1429
|
connectedAccounts: this.connectedAccounts,
|
|
977
1430
|
chainId: this.chainId,
|
|
978
|
-
createdAt: Date.now(),
|
|
1431
|
+
createdAt: this.pendingRestore?.createdAt ?? Date.now(),
|
|
979
1432
|
lastActivity: Date.now()
|
|
980
1433
|
};
|
|
981
|
-
|
|
1434
|
+
try {
|
|
1435
|
+
localStorage.setItem(this.storageKey, JSON.stringify(session));
|
|
1436
|
+
} catch (err) {
|
|
1437
|
+
warn("ConnectionManager", "localStorage.setItem failed:", err);
|
|
1438
|
+
}
|
|
982
1439
|
}
|
|
983
|
-
|
|
1440
|
+
readStoredSession() {
|
|
984
1441
|
if (typeof localStorage === "undefined") return null;
|
|
985
1442
|
try {
|
|
986
1443
|
const raw = localStorage.getItem(this.storageKey);
|
|
987
1444
|
if (!raw) return null;
|
|
988
1445
|
const session = JSON.parse(raw);
|
|
989
|
-
if (
|
|
990
|
-
|
|
1446
|
+
if (session.version !== 2) {
|
|
1447
|
+
log("ConnectionManager", "Dropping legacy (pre-v2) session from storage");
|
|
1448
|
+
localStorage.removeItem(this.storageKey);
|
|
1449
|
+
return null;
|
|
1450
|
+
}
|
|
1451
|
+
if (!session.createdAt || Date.now() - session.createdAt > SESSION_TTL_MS) {
|
|
1452
|
+
localStorage.removeItem(this.storageKey);
|
|
991
1453
|
return null;
|
|
992
1454
|
}
|
|
993
1455
|
return session;
|
|
@@ -1001,8 +1463,6 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
|
|
|
1001
1463
|
}
|
|
1002
1464
|
};
|
|
1003
1465
|
_ConnectionManager.MAX_RECONNECT_FAILURES = 5;
|
|
1004
|
-
_ConnectionManager.SYN_RETRY_MS = 5e3;
|
|
1005
|
-
_ConnectionManager.MAX_SYN_ATTEMPTS = 6;
|
|
1006
1466
|
var ConnectionManager = _ConnectionManager;
|
|
1007
1467
|
|
|
1008
1468
|
// src/utils/platform.ts
|
|
@@ -1019,10 +1479,29 @@ function getAppStoreUrl() {
|
|
|
1019
1479
|
|
|
1020
1480
|
// src/QRLConnectProvider.ts
|
|
1021
1481
|
var requestCounter = 0;
|
|
1482
|
+
var QRL_CONNECT_PROVIDER_INFO = {
|
|
1483
|
+
name: "MyQRLWallet (Mobile)",
|
|
1484
|
+
rdns: "com.qrlwallet.connect",
|
|
1485
|
+
icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMAAAADACAYAAABS3GwHAAAAAXNSR0IArs4c6QAAH1dJREFUeF7tnXt8XGWZ+L/PmaT3JpOkrVCKgD9wFcELwgoLArIsWnDLCnIVgZ8gF1lcylVsk5xMKuXiiiKLn7L82OWiK1ZBtgpdFRDdAu4P1EVAuaNoQbrNTJJek5nzbN+TpE0nk8yZmTMzZybv/AXNe3ue9/me9/a8zyvYn9XAJNaATGLZrehWA1gArBFMag1YACZ191vhLQDWBia1BiwAk7r7rfAWAGsDk1oDFoBJ3f1WeAuAtYFJrQELwKTufiu8BcDawKTWwKQEQF0cwGGtPQjMtv6ngA8m8ViJJ6D1TsekAEDZZugu01MeHxLlIBXmi7A7ymwxKNjfdg1ohkEV3sDhj+LxG8fhN7M7eVGkPmGodwBEXRp7MpwdE7pUZBYwq96FDpHnLQqbFF2dXsf5c29hY72NCnVrC6+6TGvxOA6HK1TlQyNGUbcCh2j1I0WNnv8IbFH0+kbhG7Nc3ixDdVUpsi7tQRXp66TDE7lSYEZVNFuPlQoZ9XhePF0UX8bL9SBi3QHwxtXMnTqFOx2Rj6J2kRu6kZq1gOqrHpzV0sWaWp8S1RUA6tLQq6wAOdvf5bG/smlAlDcyaT2m9RqeKVslFSi4bgDQk4gl38VpTkzutF/+CliOX4X+clD427kuaytVY9j11A0AvS4f8zx5QMROe8I2knHLM5MhR7/a4nJpxeoMuaK6AEBdZvSqfBPhePv1D9lC8hSnqhlxeGfc5ZXK1hxObXUBwLp2PhgTecQRZoejFltKYA0IKqqrmoQTxSUdOF9EEtY8AGbLM9nJchG5quaFiYhRFNoMsyBOD+rRbct5rtC81U5f8zbzrMuU3TxeR2RetZU5WetXYTOefqolwX21poOaB6CvnUM8kTXYxW9VbU887WzuJlHVRhRRec0D0NPOGXbrs4ieDz2LfqvZ5Yxac5qreQCS7ZwtMbnd7v6EbtEFFegvhLs43gJQkNpKT5zswEWks+ZJLl0VVSvBOM2J6oPNXRxnAahwN6RclqnKEgtAhRU/XN0Oj1H9SdzlGAtAhfsh1cEyRJaEXW2WK3DYxVesvOwrXWF/KCwAFevK3BWVGYC1Ar9WYSvKXwD7hm1A5VafwgaUx0Ukqei7BPYL01FwBAAR/XFzJx+1I0C5ezSr/LIBoDyVdvSCQXhu17UM9s5jgcbkq8Ci2oFAN6ny8fQWnpwzg409MF/SnOU0SHdYmwYWgAobfHZ1ZQFAzf1XvTCeMK7VO34plwNB/hNlapXFDlS94+llTd18ZXRic3Le6/I0iBkJSv7ZKVDJKiytgLIAAF7G0/e2dfPsTsZzHo2pXUiJSPRvmQnqoIc1uTyWreFkB3eKyKdL0/xQbjsChKHFEsooFwADm3S3eTeMvfua7GCDiMwsockVyaqKxlQPberm8Ryj5i2IXBhGQ+wIEIYWSyijXAA0bNLdZlUAAH8PvQT5x82qaMbRw9pyjAA9nXzDQS4Io1oLQBhaLKGMcgFQiRFg2Hg8M48QQ4HxZwrrHvMEAKQ6+QYWAN/qyvLxKcGeC85aLgDKPgIMXS7/sQ5yVXwKT/dvpcVr5DOKXI3QUnLHWAAC2VLJeg5USxkT9Q6fBIdchVdOAPwvv2r/xkH2WrCc9SNtNwNBbwfLNYy7DRaAQCZRDwAkPJX2kAUpPwDo3S1djNmJSbmcLCp3KEwL1IPjJao0AKI/jtuDsJK6rKjMPS6fc5Cbzf52iBCUDYBRi8abW7q4OFvoZDtHSExWmbilRSlkx3Ay7iI4zDXAqOp+EHdZZE+CS+q1wjMnl7IIR75vFpAWgFH6q9AIsL1G1RXNCS6stUBZIdpM4cYbRo7XFzN99mzWqMgHQhSmeiNAgiPEq60RQJWNip7SmuCHYfRpJcsI0WYq2eyd61rfznsckTUiNIfUCm9wUBfMvYY3xkxRQjgIG5oGae4pkMuRgvx7yVMgQTPkPgcIdQpk9rJEvxnv4sxa+/rXxTaob0oraOz9E2ci0sFQ3P9SwfYym3WPtuv5Y6UBSLkchcr9Jox7KTAbyDzRQ3MdhKU6uBWRz5ZSvn9ioWz20LtFuKrFJVVSeVXKXKqhVKnZY6s1r75sgDkDyhUxOA+V4heRDt5AWt89bxkv5likGleIkn2BxNGbm10+n13+epePxVRWopTmbmF8gWJ6WFPHWFeIpMud4skZJXReWhx9IOOxYn2Sh/f5OltLKKuqWesGgGwtbn8GCfhpQBUfCby0nljbDKa1XkdvrmxJl3jqNbbsuSfpoOWOLsfUQS+NNG9btLtsyq7jyfNofHsDU+euY4D34BVahyn/uWdx5s5lytwj2Swnk8mu49WzmbZnC/LURtL984O//OK33Zye1mAArPFMoG4BCGjzNtkk14AFYJIbwGQX3wIw2S1gkstvAZjkBjDZxa9bAMyDGX9cwJQFG0lTwEJvshvEePI/tRZpGyC25wGofL52d32y5atbAPpdTsio3BFmBAQLh7kCqa+0dLF/veiibgFIuZwLcmsIh2L10tchyaFes9AoLl5IBVa1mLoFINnOHeLImVXVbn1W7pHWD8W/xJP1IF5dAvCIS8P7lZcFeXs9dFKUZFDjAuHpP8QT3FyLvj91vwbwb1W5nIgn37FvBoSPztAlfn1gQDhlnsuG8GuobIl1NwKoy7Skx68dERPK0P7Ko4GUJ/rhVre23wg2qqk7AFJL+aTE5K6SrxSWx3DqotThUeC7zeb6ppkS1fCvrgDQi5na28IqRP6mhvsk8k33LV7QtKfHz0nwg1peC9QVAKkOrlGRq+tKqAjiMOqT/9y0Rj12+lJ+H8FmBmpSXdiKv/Bt5xxicovqtj3qQKLbRKVoYCSol6Dfbe7i1FodBWreVozx9yzhEKdRvo2ye10ubEqx1HLnFVRVv7RZWD4/x/2Gcldfavk1D8CGL/LegUZWjez517xApfZoNfKbB0TQpc0u/1hrI0FN24u5DB8TVqrIu+2XvxqWP6rO4TcVkg537OWypcqtCVx9rQIgqaW8Q2PyXYT3GX+fWhUkcE/VQEJVMg66uOm33CIrx17FjKIINWk3PVfR7EzjRypykDX+aJmVwFtk9LCmZbxUC9OhmgPALHqTnSxzRK6wOz7RMv7trRFe1y16eMu1vBbRFo5qatRbmNW+1BIOpIHHFWmw8/4Id57oyuZd+ZScz2CEW1lbrhD9LvMGPX4mw34+NTd8RdkSwm6bkgQ9KZ7gobCLDrO8mrEhf+rTwRdFJGFuedVMw8PsrVorS/VXzQk+GOW1QM3YUZ/LnLTKzwXMY8/2VwsaGLo7cGFzglujCkFN2JL5+qdcFm8Ld2gOWuyvhjSg8OtG0YWz3LEvbkZBjJqwp3VXMnvKdHpGFr5RUJxtQ34NDL+AuQXVM+IJvpc/R+VT1AQAyXYucWLyFfMKTOVVZGssRQPDdwe+3exyehTvDkTeoNRlSm+Ge3Dk70rpCJu3ihpQtPlNnSq3Rm9LNPIA9Lrs7XnyIxH2qmIX2qpL0YDxGPX0spYEN5ZSTDnyRh6AVAd/DfIfKsQi39hy9FC9lKm8Ek/o/4maOJG3qWQnX0fkIuvzEzXTCd6e4cszPekBPWTucl4InrP8KSMPQE8nv3WQd5VfFWNrGHPbW1GzkFPV14FnUNZFbmEnzBy+GHSAiDSqICObB9Xq7GE9blTVk1oTPFiNvhyvzmrpJJAO3rycmVNn0C9SHW/nkW08NUG2RB9FuL95Xx7iuW0hMjuHoyFETYMj1HYhSdhPMhxNTBahvBthrlRrJ01RD726JcH1UToUi1r3jQZDejr4uIPcX5UAV+b1Q/QnONzcAE/MdnkrELURTGRiJa3P8L6Yw+miXKQisZFmVswABBXVrzUJl0UprmjF5C/ULvyL7h2ciyMrKh3gVmGTI7q8B768p8vWKH2xCtXj6PTDwQMOwOFuVfmLkB8Xz9s0z9PVr/yZRQdGaDs00gCkOvmyiCyuKADK8+roF+Iu99eL4WdbZu9S9vEa+GdUjjB/q5QRqOqj8d34myi5SFdK9rxfh+wEZuHW67LaD3JVuXnrgIge0eTyi3o1/hE9+weMHj/FkYONrithCIo+E5dt3qEuAwUbRJkyVELuopruA9DFGlQOKaqAAjMppDSjn2pdxgP5spp7CZkBFiA4NOZLXdm/Dw7C1Eb6XofX3pPH0JIue6KsBDmwEoag6Np4G++I0gszlZC7KAsYHgEeAzm4qAIKyTR0UrksnqBzvC+/eXc4leZwhHYceYfAjAqOTIVIgwqDqvqWeNwbb+BLEy06e5ZwmBPjUUScgiopIrGqbo47tEqEokZEHAB5DCgrAMNbnS9s3qof3TXHHVazcOxbyt6eQyeIceiKrM6yFrx+/E5zZiHC4rjLfbng9l3NO7gBkcvKLpio9vcyc/cb2VwEP2XJUnaZi2310AhQGQDU04tbu7k5V1t7ExzkZbhdkP2KlaVa+bYf5Cn9it7csh/tuV6O37yMPbYMysOAGdnK97MABNdtpQBA9c1mh91yTRP8ub7Hz6iPtwYGRPXSpi5uyT69Ni9q9u7LTYp8rqy7QhaAiAFgDmc87WpKkMieHqx1mTE9wz0Sk+OiOtcPrs3tez9vOsL+TS7/k53XvKugMVlpAShcq2XJUaERwMuoHtuW4D+yhTCLQ2mQ+2Hboq0sElahUEE9T29rSXB+NvDrL6bJaZNUWV0l7AgQvNMrAYAqr3uqC9u6eTarZdLTzhXiyHV1Y/wjAopmnK28rWk563daNJs1VydrEdkleC8VmFJV+/vtIjiQ1ioBAPDSgOjCeS4vZRtDXxf/piqnBGpsLSUy0760Htu8jNVZu0YGgJsRubBsUz47AgS3lAoB8AKiC+Mur+xkDC5OUlkjyMF1NwIYQVU/mX1J3d/u7SShIkssAMHttGwpKwTA7zZv1YXZ+//+oZfHHxHZtZyLwnyvy5UNPtUL4glWZI8AqXYSErMAlM2oCym4EgAI/NbbqsdmB3E1APR6rFWRt4UJwIjBi/rz7w0qJEVGHQoN+Ty1qjllhl0E/PinYf881c+1JviGBaByjoAF92HVAVD+pAwtCIv9Eo/+woswiKc/8Rzubsjwe1F6BgdZ23ItfduVsxIn9d8saHRoGoixv6PmPjQnINIc5rTEArDDHIvt24INutAM9QCAcUXwlM2oPtrQyOLZS3mhkCuUfjzUq2hypnM1npwHNCFsv8xSqE5H0lsALAC+BiacApU6AhiHNE8fUFixPsnD+3zdvKNV/K/vat6pU/i0Il+gxKmRBcACUF4AhpzQuuNvsExuJW32XYo3/R05zdqkP80+Xsy4bMs7ii3TAmABKB8Awp8yGU20dvPP5bhU43tvtvNeiXEXxkGviMtCFgALQLkASKvoOXG4eyIffGPE27vAjA1d/v830MngyF/ywbPRZf6gmscnCg8ZYwGwAJQDgC2qem1Lgq6JpiappeylDoc7wpGKHOXH8Bm5iuL5M6XfAQ9kMtzb18gvJ3pytH8p7047sgrBj7gWdEfDAmABCB0AFb0j3sn/HW+XR29iamqd71/0KYG9TajHcacv5mEJpVdV18QGuXz2cp7PeZnFnFekOUEdvkMBwZMsABaAsAH4Q0x04WyX57K//mY7N/kFdpdp3CciHyh4zq4kM+j5rb/l3lxv75ry+1xu90TOCurJaQGwAIQHwNAzQFc3jxPxzGxfelO4B+T9xeza+JMipX+bA09nPMFXc40EySXsITF5hIARtC0AFoDQAPAjHXSxIJdhPusyZb7HE/6Xv8jfdvcJYat6+tl4YtsCO2tb1Xdk6+CfPJELg6wDLAAWgFAA8C/Ue3pVvJvrs+3bxDWdPoP/p1K6S/WOu736cibDEXO+xJ+y6+t1+UtP5fEgL2haACwAoQAAvInocXGXX2YbZMrlKFTMu1jxIj/+O2XbMRLoyrjLyWPWGkMerK8gske+UcACYAEIC4BfDogeNy/HC4gpl9tQOScM49+5DPViwvtmuzyTXXayg8WIfMUCEFzr+XQVvKSQU9aCM5yn+mCLw8dzHXqlOiSF0ByyWoZi/Xi6rCVBR3bZ61zmN6qMmR5lp7MjgB0BSh4BzJTEQ5e1ddGebWAblnJM2pHVZQvrLnLPuvXeWdkOdn4kC+XPgsyaCDwLgAUgFABQPbMlYXxydvz8EORLuZIGWV7wnn+e4WLUjtAvnK16XPbF9hcvZurcVn6uyEETDe0WAAtAyQCYAjzVM1oTfDMbgGQnNznD75qFOQUa5VL6pDl4y47voytoTK31PUWPNvWOB4EFwAJQVgBSndxUrof9hiHICcDwucMDiJibZBaAAF8fuwge705wgAsxonpGc44RoKedy5yYXF/Gly2fdHKMAGYKNKeV/8wX7tyOAHYEKHkEGHJR0EuzH3/2T2XbOcUT+Va5niASYVUTeqq4bBr9kXt9MdNnN5kQLxMHt7IAWABKBsC3f/T7LV18InukNY/S9apsNKeyAUbhwpIM3Tb7WryLS7NdIt5ymdXoSV++EO4WAAtAKACI8POB6Xrc3KuMs9rOv2Qn3xPkhMKsO39qAU88PbypmzVj6mznbBz5l3zzWguABSAUAIA/eKLHteY4le1p51REbhdh+kQL0vwmvyOFmXY56GMvvsGRuV5aTHbwMxH5cL4yLQAWgLAAwPP0tNZuvp1tdH1X05ZulB+K8KHQAFDdOOjxgXnLeDG7vrVX8fYZ0+TVINMuC4AFIDwAVH/RkuCQXO7Q2165PFg9+b4Kb8s3Lcn31QbSil4bfw4318WYVAdLcKQ7yOGbBcACEBoAZiWcUT21rZvvjFkMD/npm6nQvypMCWDkOyXZfvJr5v2ijzfBUbmeGO2/mrmZKWKiPR8QpA4LgAUgPAD8kvTphkGOmXUNfx4DwdB7vGepSIfAgiAG6pc4klDNS/V6+5ZNXLHLlzE7S9mQSLKDC0TEvHHmBBlpLAAWgJABwNOMuvFlLBvv8npSWRBD/k2FQyaapuwcQUs3Ocq5TXO4d7y3dde77BvzeEpFpgVda1gALAChAjB8b/fPafS0OQl+OsFbww19cLLncYkgB+SK8+nfMlPWgd7uDHLD7OX0jFden8ucTIZ7cOSokS61I0DQMXYoXRB9FVZiSKlr4T7AaFGHIViP6okt3Tw6kRp6Xfb2PB4Wkd1zTGnw3RzgR/mCa6U6+S7IopFYoUE7044AdgQIdQQYbcQCj2U8vaC127+xNW5M0GQn3YIszQbA5E+LLmxzR4VNz0qUWk6LbuVGQc40H4pCv2QWAAtA2QAYXsD+QVQvb3a2nQa7eLlGg94OLtKhhevOi1qPR+IxXSQuG3Lkk75raPW28E8qcmKxD2hYACwAZQVgWL0DqLpbNnFTrt2bng4ucnIAgMfDAzE9fl4WACYy9AY4POPJahWmFvrVHw2TBcACUAkA/Pu7qC5t7mJ59kJ2XADg4QEZC4A5Wc40ym8QSn63zAJgAagYAIJ2N7m4JQPgMsdTed68IVbqPoMFwAJQMQAUTcRdukICwESObrMAlKoBC0DlADAvxXSFNgJYAMKz/REbCLnEkIqrtXOAnGKbNQB6XbPLF0sdAYy/T3qKvCTmobwSf3YKZEeA0EYAcz3LcVjleST9qP4yfLg49F8iyg/iORzlCl0Er7uS2Y1TuV6FmX5IIvMz9Snq3zmI8WGQ+dYbtLCvQ9DDw8JKDSF1TYwAQlLRS+Kd3DXRmXouV4ZCARiy93FqUehtZ091+JGI7J1P/XYEsCNAKCOAonfG2zhvPEe1iQyx0HOAfEZt/v7WUvaZEhNzAj2h67UFwAIQCgC5wqIEMVSTZlwAlIcGHP277IOwIOWqy5Q+5SFFDpsovQXAAhAKAI7qlU0JbghinNlp+jq5wUMuz/53VZ7C0aNbXFKFlvuqy7QW5Tcw8TTIAmABCAUA4L/Mqe1c1wSkDfYYtpnHr1/C/IZGeRQdet1xp5+Sdhz9xGyXH5p/D1LuyNqgbwlLvJgkbFiU4J8OuwgeLzKcx1oVeduwEY6vUdUHcfhXzC6QQybt5V6oNjioCpoZpKXR4RKdIHqDSSeqyx3hcY+h4Fe5yjVl+ovjNLNo4EjgErMnlK9T7QhgR4DtI8CmrXrsrtfy2mgLN45nvUEBGCIkox6bRHJ7fvpl+zdd/G3LmQiNeb9R/hkCW/AYGHeHyZwyDN2DnGoc5IK6RlsALABDGlCex9OF8WWYcCLbfwaAlPIyyJ55R4C8lhytBP67Bp6e09bN7TvJDJJqJyExWRLkLKEoqUS1v5eZu9/I5qLylyFTvtGyDFUGK7IS5wDAS7JVFzZfy0ujW+W6OJ/P8KA4ckxkFRRMjTulGrmdo6qLWhOsygagr5OEShkBUNX+fgtAoK6rCADKy+LpwuasQFNmUZnq5DZBPhOosTWSyAfArC8yelC8m6fGjAAd3CWOnG5HgAh0aEUAEPodRxc2dYyNs5layieJiXn8ouB4PhFQ35gm7LibqU/HXd4vQ2uMHdM+RXpdkiDhv2s2UoudAgU3jQoBoJ6nn25J8K3s7cZ1VzK/cbo8BLwreKujndJsLXlbObR1OU+Mcc5rZ3/HkafLKoEFILh6KwGA/wn09OF4gqPHfBHNW18dfEaRFSLEgrc8milNVGkVXfkrOOMjLunsVvZ2cLGK3FTW1lsAgqu3EgAMtyatg3pQyzX8Ort1eh6NqV1YIkhH2V58DK6SklKq0hNz9NAmF3OnYKffepemmHIPyMdKqiRfZgtAPg1lz0nlMeDg4LmKS6mqqzb0c0qu7bk3L2fmtFl0qifnOg7xkb324mqqfC6BAU91TczjwqZlmCuVY37JJRwuDfIgMKOsLbQABFdvBUeAbcEb/MXwyc0uJsDsmJ9ezNRknL8Uh0tRFokjYtoX6S1Sc+SGvuDAP+JwX/aLkiNCGtl6W3gakXcG750iU1oAgiuukgAMtUqfHhAOncgL0/e5uYEZPX2c1ugwLwP7IGV4Bim4mnZOaRoorBN4LZPhv1ti23a3XHSCUI1Tkhm+5sTk/LJtfY5uoQUgeM9WHACzP47++6atfHbX5SY25/i/nS6mjBv7LbisoaUcNSTlc6LzH/Pr4HQVuYUQrlkGksECEEhNQ99jf1+6MmuAnVrl6bc3xThnftYLjMFbHv2U3zmJ2DHv4SPq+TfIKjeTswAEN45qAeD7rIneoxmua+nmV8FbXDspezr8MO6LRaS8i95slVgAghtJtQDwW+g/Rcobnqrb2sVt2WcEwaWITkpfn0v5IDHuVJF9io0rWpJEFoDg6qsqACPN9KM86P9XjxUNaZ6YNXXbI9QuW4NLUeWUCskvsHtsOvuqcqIipwvMGPLMrsLPAhBc6ZEAYEdzzcnpWoW3QJ8R5QmHsaepwaUrX8rtoagd9lI4DJVdBXYDP5xKdX8WgOD6jxgA2xtupkNR2vjJpdFR74sV/HZA8B4qIqUFILjSogpAcAmqm7Jq05yJxLYABDcKC0BwXdVMSgtA8K7yAejkMUTK7gsUvFU2ZSkaUHRTXGgTly2llBNm3qpsBgQRwACQ7OSnMUc+XGsOaEHkm5xp9JV1Pey7z9ejs5MWXQCGriV+VUQuroifyuS0yIpKLaJrmnblI3I+gxWteILKIg1Abwfn4sgKC0BUzKW0dgj6gybhE5LjQk5pJRefO7IAGJFSSziQmPxXrV9GKb576iinf7qu3XHzXFTWfeRqShlpAIzHYrKDPzgiC6qpJFt3KBrYguoZ8QT3TvR+cig1FVBIpAEwciQ7uVFE/sFOgwro1YglHYrfSDLj6F/NyXEls5rNjTwAPe0cKo6sFphVTUXZuovXwBAAel9LghOKL6U8OSMPQP81zM1skfsRDimPCmyp5daACplG9IBZLuUNu1KEIJEHwMjU4/L3DnKTnQYV0cNRyKJ6T3MXp0Vp8TuilpoAQG9iau96ngTZLwr9adsQXAMKKRE9Me7ycPBclUtZEwAYdaxfwsFOTB4UIV459diaStKA2e709PaX3uTCA2+NzuHXaJlqBgB1aUjBF0Wl04TEL6ljbOaya2DIbVyfUTi8mOeeyt7A4QpqBgB/LXAVzTKVR8SR99v1QKVMpLh6VPl9rFE/2tSeOxhXcaWGn6umADDi97vMS3vcjshx5v9rToDw+zB6Jaqm1eOk+POskpVkotfAHS2qSfvZ4LLLoCercXivmBfZ7S9CGtD/QTil2eWRfLGJotDomjWedS7zG5TlIJ8cHdOyZgWKgjWU0gb//TN93vM4z0SkE3eC99JKqSfkvDVtL4+4NLzPY7Eg1/nL4qjH6wy58yJTnDF91bviczlPPh8dX/8g+qlpAEYE7GlnfxEWI3K8QGsQwW2aEDSgbFb0CXW4bf16vheliy5BpasLAIyw+hWm9/Wx27aAVuercpEg00bcqEeiJNSNsEF7txzphp56VRH9oedxIw6/irv01sJ8P5c66s4m/MC159GQmsup0sCxiOyB0qZKswgzUaZplCI6l8NIQyhTzLQGBkT8J01Tah7sVv29Oqz20jzU1s3voujaUKjodQdAtgKSLnFJ00YDzQqzxGNqxqv9J48K7eiC0zvoFIetXobNoqQGlU2tL7A26tuahcpZ9wBMoJDJLHsQO4l6/K8gMuRNY40gr4psgnrWgAWgnnvXypZXAxaAvCqyCepZAxaAeu5dK1teDVgA8qrIJqhnDVgA6rl3rWx5NfC/zGV/wHfd+nEAAAAASUVORK5CYII="
|
|
1486
|
+
};
|
|
1487
|
+
var EIP6963_ANNOUNCE_EVENT = "eip6963:announceProvider";
|
|
1488
|
+
var EIP6963_REQUEST_EVENT = "eip6963:requestProvider";
|
|
1489
|
+
function generateUuid() {
|
|
1490
|
+
if (typeof globalThis !== "undefined" && globalThis.crypto?.randomUUID) {
|
|
1491
|
+
return globalThis.crypto.randomUUID();
|
|
1492
|
+
}
|
|
1493
|
+
const r = (n) => Math.floor(Math.random() * n);
|
|
1494
|
+
const hex = (n, len) => n.toString(16).padStart(len, "0");
|
|
1495
|
+
return `${hex(r(4294967296), 8)}-${hex(r(65536), 4)}-${hex(16384 | r(4096), 4)}-${hex(32768 | r(16384), 4)}-${hex(r(4294967296), 8)}${hex(r(65536), 4)}`;
|
|
1496
|
+
}
|
|
1022
1497
|
var QRLConnectProvider = class extends EventEmitter4 {
|
|
1023
1498
|
constructor(options) {
|
|
1024
1499
|
super();
|
|
1025
1500
|
this.pendingRequests = /* @__PURE__ */ new Map();
|
|
1501
|
+
this.eip6963Detail = null;
|
|
1502
|
+
this.eip6963RequestListener = null;
|
|
1503
|
+
this.resumeListener = null;
|
|
1504
|
+
this.resumeDebounce = null;
|
|
1026
1505
|
this.isQRLConnect = true;
|
|
1027
1506
|
this.options = options;
|
|
1028
1507
|
if (options.debug) {
|
|
@@ -1038,6 +1517,81 @@ var QRLConnectProvider = class extends EventEmitter4 {
|
|
|
1038
1517
|
if (options.autoReconnect !== false) {
|
|
1039
1518
|
this.connectionManager.reconnect();
|
|
1040
1519
|
}
|
|
1520
|
+
this.setupResumeListeners();
|
|
1521
|
+
if (options.announceProvider !== false) {
|
|
1522
|
+
this.startEip6963Announce();
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
startEip6963Announce() {
|
|
1526
|
+
if (typeof window === "undefined" || typeof CustomEvent === "undefined") {
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
1529
|
+
const overrides = this.options.providerInfo ?? {};
|
|
1530
|
+
this.eip6963Detail = Object.freeze({
|
|
1531
|
+
info: Object.freeze({
|
|
1532
|
+
uuid: generateUuid(),
|
|
1533
|
+
name: overrides.name ?? QRL_CONNECT_PROVIDER_INFO.name,
|
|
1534
|
+
icon: overrides.icon ?? QRL_CONNECT_PROVIDER_INFO.icon,
|
|
1535
|
+
rdns: overrides.rdns ?? QRL_CONNECT_PROVIDER_INFO.rdns
|
|
1536
|
+
}),
|
|
1537
|
+
provider: this
|
|
1538
|
+
});
|
|
1539
|
+
const announce = () => {
|
|
1540
|
+
if (!this.eip6963Detail) return;
|
|
1541
|
+
window.dispatchEvent(new CustomEvent(EIP6963_ANNOUNCE_EVENT, { detail: this.eip6963Detail }));
|
|
1542
|
+
};
|
|
1543
|
+
this.eip6963RequestListener = announce;
|
|
1544
|
+
window.addEventListener(EIP6963_REQUEST_EVENT, announce);
|
|
1545
|
+
announce();
|
|
1546
|
+
}
|
|
1547
|
+
/**
|
|
1548
|
+
* Re-open the relay socket when the dApp tab regains focus or the network
|
|
1549
|
+
* comes back. Debounced because visibilitychange + online can fire together.
|
|
1550
|
+
* No-op when already connected (ConnectionManager.resume guards that).
|
|
1551
|
+
*/
|
|
1552
|
+
setupResumeListeners() {
|
|
1553
|
+
if (typeof window === "undefined" || typeof document === "undefined") return;
|
|
1554
|
+
if (this.resumeListener) return;
|
|
1555
|
+
const resume = () => {
|
|
1556
|
+
if (typeof document !== "undefined" && document.visibilityState === "hidden") return;
|
|
1557
|
+
if (this.resumeDebounce) clearTimeout(this.resumeDebounce);
|
|
1558
|
+
this.resumeDebounce = setTimeout(() => {
|
|
1559
|
+
this.resumeDebounce = null;
|
|
1560
|
+
this.connectionManager.resume();
|
|
1561
|
+
}, 300);
|
|
1562
|
+
};
|
|
1563
|
+
this.resumeListener = resume;
|
|
1564
|
+
document.addEventListener("visibilitychange", resume);
|
|
1565
|
+
window.addEventListener("online", resume);
|
|
1566
|
+
window.addEventListener("pageshow", resume);
|
|
1567
|
+
}
|
|
1568
|
+
/**
|
|
1569
|
+
* Remove the foreground/online resume listeners. Called on disconnect so a
|
|
1570
|
+
* torn-down session can't be silently revived by a later tab focus; the
|
|
1571
|
+
* listeners are re-armed by getConnectionURI()/newConnection() if the same
|
|
1572
|
+
* provider is re-paired. Safe in any env.
|
|
1573
|
+
*/
|
|
1574
|
+
teardownResumeListeners() {
|
|
1575
|
+
if (typeof window !== "undefined" && typeof document !== "undefined" && this.resumeListener) {
|
|
1576
|
+
document.removeEventListener("visibilitychange", this.resumeListener);
|
|
1577
|
+
window.removeEventListener("online", this.resumeListener);
|
|
1578
|
+
window.removeEventListener("pageshow", this.resumeListener);
|
|
1579
|
+
}
|
|
1580
|
+
this.resumeListener = null;
|
|
1581
|
+
if (this.resumeDebounce) {
|
|
1582
|
+
clearTimeout(this.resumeDebounce);
|
|
1583
|
+
this.resumeDebounce = null;
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
/**
|
|
1587
|
+
* Stop announcing this provider over EIP-6963. Safe to call from any env.
|
|
1588
|
+
*/
|
|
1589
|
+
stopEip6963Announce() {
|
|
1590
|
+
if (typeof window !== "undefined" && this.eip6963RequestListener) {
|
|
1591
|
+
window.removeEventListener(EIP6963_REQUEST_EVENT, this.eip6963RequestListener);
|
|
1592
|
+
}
|
|
1593
|
+
this.eip6963RequestListener = null;
|
|
1594
|
+
this.eip6963Detail = null;
|
|
1041
1595
|
}
|
|
1042
1596
|
setupConnectionListeners() {
|
|
1043
1597
|
this.connectionManager.on("status_changed", (status) => {
|
|
@@ -1095,6 +1649,7 @@ var QRLConnectProvider = class extends EventEmitter4 {
|
|
|
1095
1649
|
* Generate a connection URI for QR code display or deep link redirect.
|
|
1096
1650
|
*/
|
|
1097
1651
|
async getConnectionURI() {
|
|
1652
|
+
this.setupResumeListeners();
|
|
1098
1653
|
return this.connectionManager.getConnectionURI();
|
|
1099
1654
|
}
|
|
1100
1655
|
/**
|
|
@@ -1202,35 +1757,420 @@ var QRLConnectProvider = class extends EventEmitter4 {
|
|
|
1202
1757
|
pending.reject(new Error("Connection reset"));
|
|
1203
1758
|
}
|
|
1204
1759
|
this.pendingRequests.clear();
|
|
1205
|
-
this.connectionManager.resetForNewChannel();
|
|
1760
|
+
await this.connectionManager.resetForNewChannel();
|
|
1206
1761
|
return this.getConnectionURI();
|
|
1207
1762
|
}
|
|
1208
1763
|
/**
|
|
1209
|
-
* Disconnect from wallet and clean up.
|
|
1764
|
+
* Disconnect from wallet and clean up. Returns once the TERMINATE has
|
|
1765
|
+
* either been flushed to the relay or the 800ms best-effort window has
|
|
1766
|
+
* elapsed — the wallet gets an instant disconnect instead of landing in
|
|
1767
|
+
* its stale-session grace period.
|
|
1210
1768
|
*/
|
|
1211
|
-
disconnect() {
|
|
1769
|
+
async disconnect() {
|
|
1212
1770
|
for (const [, pending] of this.pendingRequests) {
|
|
1213
1771
|
pending.reject(new Error("Disconnected"));
|
|
1214
1772
|
}
|
|
1215
1773
|
this.pendingRequests.clear();
|
|
1216
|
-
this.
|
|
1774
|
+
this.teardownResumeListeners();
|
|
1775
|
+
await this.connectionManager.disconnect();
|
|
1217
1776
|
}
|
|
1218
1777
|
};
|
|
1778
|
+
|
|
1779
|
+
// src/signing/ctx.ts
|
|
1780
|
+
var SCHEME_VERSION_MSG = "QRL-SIGN-MSG-v1";
|
|
1781
|
+
var SCHEME_VERSION_TYPED = "QRL-SIGN-TYPED-v1";
|
|
1782
|
+
var SCHEME_TAG_MSG = new TextEncoder().encode(SCHEME_VERSION_MSG);
|
|
1783
|
+
var SCHEME_TAG_TYPED = new TextEncoder().encode(SCHEME_VERSION_TYPED);
|
|
1784
|
+
var DIGEST_LEN = 64;
|
|
1785
|
+
|
|
1786
|
+
// src/signing/messageDigest.ts
|
|
1787
|
+
import { shake256 } from "@noble/hashes/sha3.js";
|
|
1788
|
+
|
|
1789
|
+
// src/signing/bytes.ts
|
|
1790
|
+
function hexToBytes(hex) {
|
|
1791
|
+
if (typeof hex !== "string") throw new Error("hex must be a string");
|
|
1792
|
+
if (!/^0x([0-9a-fA-F]{2})*$/.test(hex)) {
|
|
1793
|
+
throw new Error("hex must be 0x-prefixed even-length");
|
|
1794
|
+
}
|
|
1795
|
+
const n = (hex.length - 2) / 2;
|
|
1796
|
+
const out = new Uint8Array(n);
|
|
1797
|
+
for (let i = 0; i < n; i++) {
|
|
1798
|
+
out[i] = parseInt(hex.slice(2 + i * 2, 4 + i * 2), 16);
|
|
1799
|
+
}
|
|
1800
|
+
return out;
|
|
1801
|
+
}
|
|
1802
|
+
function bytesToHex(bytes) {
|
|
1803
|
+
if (!(bytes instanceof Uint8Array)) {
|
|
1804
|
+
throw new Error("bytes must be a Uint8Array");
|
|
1805
|
+
}
|
|
1806
|
+
let s = "0x";
|
|
1807
|
+
for (const v of bytes) s += v.toString(16).padStart(2, "0");
|
|
1808
|
+
return s;
|
|
1809
|
+
}
|
|
1810
|
+
function concatBytes(...parts) {
|
|
1811
|
+
return concatBytesArr(parts);
|
|
1812
|
+
}
|
|
1813
|
+
function concatBytesArr(parts) {
|
|
1814
|
+
let total = 0;
|
|
1815
|
+
for (const p of parts) total += p.length;
|
|
1816
|
+
const out = new Uint8Array(total);
|
|
1817
|
+
let off = 0;
|
|
1818
|
+
for (const p of parts) {
|
|
1819
|
+
out.set(p, off);
|
|
1820
|
+
off += p.length;
|
|
1821
|
+
}
|
|
1822
|
+
return out;
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
// src/signing/messageDigest.ts
|
|
1826
|
+
function computeMessageDigest(messageBytes) {
|
|
1827
|
+
if (!(messageBytes instanceof Uint8Array)) {
|
|
1828
|
+
throw new Error("messageBytes must be a Uint8Array");
|
|
1829
|
+
}
|
|
1830
|
+
return shake256(concatBytes(SCHEME_TAG_MSG, messageBytes), { dkLen: DIGEST_LEN });
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
// src/signing/typedData.ts
|
|
1834
|
+
import { shake256 as shake2562 } from "@noble/hashes/sha3.js";
|
|
1835
|
+
var SLOT = 32;
|
|
1836
|
+
var ATOMIC_RE = /^(?:(address|bool|string|bytes)|(u?int)(\d+)|bytes(\d+)|(.+?)\[(\d*)\])$/;
|
|
1837
|
+
var MAX_TYPE_DEPTH = 12;
|
|
1838
|
+
function parseFieldType(type, types, depth = 0) {
|
|
1839
|
+
if (depth > MAX_TYPE_DEPTH) throw new Error(`type nesting too deep: ${type}`);
|
|
1840
|
+
if (Object.prototype.hasOwnProperty.call(types, type)) {
|
|
1841
|
+
return { kind: "ref", name: type };
|
|
1842
|
+
}
|
|
1843
|
+
const m = ATOMIC_RE.exec(type);
|
|
1844
|
+
if (!m) throw new Error(`unknown type: ${type}`);
|
|
1845
|
+
const [, atomic, intKind, intWidthStr, bytesWidthStr, innerType, sizeStr] = m;
|
|
1846
|
+
if (atomic) return { kind: atomic };
|
|
1847
|
+
if (intKind) {
|
|
1848
|
+
const width = Number(intWidthStr);
|
|
1849
|
+
if (!Number.isInteger(width) || width < 8 || width > 256 || width % 8 !== 0) {
|
|
1850
|
+
throw new Error(`invalid int width: ${type}`);
|
|
1851
|
+
}
|
|
1852
|
+
return { kind: intKind === "uint" ? "uintN" : "intN", width };
|
|
1853
|
+
}
|
|
1854
|
+
if (bytesWidthStr) {
|
|
1855
|
+
const width = Number(bytesWidthStr);
|
|
1856
|
+
if (!Number.isInteger(width) || width < 1 || width > 32) {
|
|
1857
|
+
throw new Error(`invalid bytesN width: ${type}`);
|
|
1858
|
+
}
|
|
1859
|
+
return { kind: "bytesN", width };
|
|
1860
|
+
}
|
|
1861
|
+
if (innerType !== void 0) {
|
|
1862
|
+
parseFieldType(innerType, types, depth + 1);
|
|
1863
|
+
const size = sizeStr ? Number(sizeStr) : void 0;
|
|
1864
|
+
if (sizeStr && (!Number.isInteger(size) || size <= 0)) {
|
|
1865
|
+
throw new Error(`invalid array size: ${type}`);
|
|
1866
|
+
}
|
|
1867
|
+
return { kind: "array", inner: innerType, size };
|
|
1868
|
+
}
|
|
1869
|
+
throw new Error(`unhandled type: ${type}`);
|
|
1870
|
+
}
|
|
1871
|
+
function baseTypeName(type) {
|
|
1872
|
+
return type.replace(/(\[\d*\])+$/, "");
|
|
1873
|
+
}
|
|
1874
|
+
function collectDependencies(primary, types) {
|
|
1875
|
+
if (!Object.prototype.hasOwnProperty.call(types, primary)) {
|
|
1876
|
+
throw new Error(`unknown primary type: ${primary}`);
|
|
1877
|
+
}
|
|
1878
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1879
|
+
const visit = (name, path) => {
|
|
1880
|
+
if (path.includes(name)) {
|
|
1881
|
+
throw new Error(`cyclic type reference: ${[...path, name].join(" -> ")}`);
|
|
1882
|
+
}
|
|
1883
|
+
if (visited.has(name)) return;
|
|
1884
|
+
visited.add(name);
|
|
1885
|
+
for (const f of types[name]) {
|
|
1886
|
+
const base = baseTypeName(f.type);
|
|
1887
|
+
if (Object.prototype.hasOwnProperty.call(types, base)) {
|
|
1888
|
+
visit(base, [...path, name]);
|
|
1889
|
+
}
|
|
1890
|
+
parseFieldType(f.type, types);
|
|
1891
|
+
}
|
|
1892
|
+
};
|
|
1893
|
+
visit(primary, []);
|
|
1894
|
+
return visited;
|
|
1895
|
+
}
|
|
1896
|
+
function validateTypeMap(types) {
|
|
1897
|
+
for (const [name, def] of Object.entries(types)) {
|
|
1898
|
+
if (!Array.isArray(def) || def.length === 0) {
|
|
1899
|
+
throw new Error(`empty or invalid struct: ${name}`);
|
|
1900
|
+
}
|
|
1901
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1902
|
+
for (const f of def) {
|
|
1903
|
+
if (!f || typeof f.name !== "string" || !f.name) {
|
|
1904
|
+
throw new Error(`bad field in ${name}`);
|
|
1905
|
+
}
|
|
1906
|
+
if (typeof f.type !== "string" || !f.type) {
|
|
1907
|
+
throw new Error(`bad field type in ${name}.${f.name}`);
|
|
1908
|
+
}
|
|
1909
|
+
if (seen.has(f.name)) {
|
|
1910
|
+
throw new Error(`duplicate field "${f.name}" in ${name}`);
|
|
1911
|
+
}
|
|
1912
|
+
seen.add(f.name);
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
function encodeType(primary, types) {
|
|
1917
|
+
const deps = collectDependencies(primary, types);
|
|
1918
|
+
const others = [...deps].filter((n) => n !== primary).sort();
|
|
1919
|
+
return [primary, ...others].map((name) => {
|
|
1920
|
+
const fields = types[name];
|
|
1921
|
+
const inner = fields.map((f) => `${f.type} ${f.name}`).join(",");
|
|
1922
|
+
return `${name}(${inner})`;
|
|
1923
|
+
}).join("");
|
|
1924
|
+
}
|
|
1925
|
+
function typeHash(primary, types) {
|
|
1926
|
+
return shake2562(new TextEncoder().encode(encodeType(primary, types)), { dkLen: DIGEST_LEN });
|
|
1927
|
+
}
|
|
1928
|
+
function parseQAddress(addr) {
|
|
1929
|
+
if (typeof addr !== "string" || !/^Q[0-9a-fA-F]{40}$/.test(addr)) {
|
|
1930
|
+
throw new Error(`invalid Q-address: ${String(addr)}`);
|
|
1931
|
+
}
|
|
1932
|
+
return hexToBytes("0x" + addr.slice(1).toLowerCase());
|
|
1933
|
+
}
|
|
1934
|
+
function padLeft32(bytes) {
|
|
1935
|
+
if (bytes.length > SLOT) throw new Error("cannot pad: bytes > 32");
|
|
1936
|
+
const out = new Uint8Array(SLOT);
|
|
1937
|
+
out.set(bytes, SLOT - bytes.length);
|
|
1938
|
+
return out;
|
|
1939
|
+
}
|
|
1940
|
+
function padRight32(bytes) {
|
|
1941
|
+
if (bytes.length > SLOT) throw new Error("cannot pad: bytes > 32");
|
|
1942
|
+
const out = new Uint8Array(SLOT);
|
|
1943
|
+
out.set(bytes, 0);
|
|
1944
|
+
return out;
|
|
1945
|
+
}
|
|
1946
|
+
function bigIntToSlot(value, width, signed) {
|
|
1947
|
+
if (signed) {
|
|
1948
|
+
const limit = 1n << BigInt(width - 1);
|
|
1949
|
+
if (value >= limit || value < -limit) {
|
|
1950
|
+
throw new Error(`int${width} out of range: ${value}`);
|
|
1951
|
+
}
|
|
1952
|
+
} else {
|
|
1953
|
+
if (value < 0n) throw new Error(`uint${width} negative: ${value}`);
|
|
1954
|
+
const limit = 1n << BigInt(width);
|
|
1955
|
+
if (value >= limit) throw new Error(`uint${width} out of range: ${value}`);
|
|
1956
|
+
}
|
|
1957
|
+
const slotMask = (1n << 256n) - 1n;
|
|
1958
|
+
const repr = value < 0n ? value + (1n << 256n) & slotMask : value;
|
|
1959
|
+
const out = new Uint8Array(SLOT);
|
|
1960
|
+
let v = repr;
|
|
1961
|
+
for (let i = SLOT - 1; i >= 0; i--) {
|
|
1962
|
+
out[i] = Number(v & 0xffn);
|
|
1963
|
+
v >>= 8n;
|
|
1964
|
+
}
|
|
1965
|
+
return out;
|
|
1966
|
+
}
|
|
1967
|
+
function parseIntValue(v, typeLabel) {
|
|
1968
|
+
if (typeof v === "bigint") return v;
|
|
1969
|
+
if (typeof v === "string") {
|
|
1970
|
+
if (/^-?0x[0-9a-fA-F]+$/i.test(v)) {
|
|
1971
|
+
const isNegative = v.startsWith("-");
|
|
1972
|
+
const abs = BigInt(isNegative ? v.slice(1) : v);
|
|
1973
|
+
return isNegative ? -abs : abs;
|
|
1974
|
+
}
|
|
1975
|
+
if (!/^-?(0|[1-9]\d*)$/.test(v)) {
|
|
1976
|
+
throw new Error(`invalid ${typeLabel} string: ${v}`);
|
|
1977
|
+
}
|
|
1978
|
+
return BigInt(v);
|
|
1979
|
+
}
|
|
1980
|
+
if (typeof v === "number") {
|
|
1981
|
+
if (!Number.isInteger(v) || !Number.isSafeInteger(v)) {
|
|
1982
|
+
throw new Error(`unsafe ${typeLabel} number: ${v}, pass as string`);
|
|
1983
|
+
}
|
|
1984
|
+
return BigInt(v);
|
|
1985
|
+
}
|
|
1986
|
+
throw new Error(`unsupported ${typeLabel} value: ${typeof v}`);
|
|
1987
|
+
}
|
|
1988
|
+
function encodeField(type, value, types) {
|
|
1989
|
+
const parsed = parseFieldType(type, types);
|
|
1990
|
+
switch (parsed.kind) {
|
|
1991
|
+
case "ref":
|
|
1992
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
1993
|
+
throw new Error(`struct field expects object: ${type}`);
|
|
1994
|
+
}
|
|
1995
|
+
return hashStruct(parsed.name, value, types);
|
|
1996
|
+
case "address":
|
|
1997
|
+
if (typeof value !== "string") {
|
|
1998
|
+
throw new Error(`address field expects string: ${typeof value}`);
|
|
1999
|
+
}
|
|
2000
|
+
return padLeft32(parseQAddress(value));
|
|
2001
|
+
case "bool":
|
|
2002
|
+
if (typeof value !== "boolean") {
|
|
2003
|
+
throw new Error(`bool field expects boolean: ${typeof value}`);
|
|
2004
|
+
}
|
|
2005
|
+
return padLeft32(new Uint8Array([value ? 1 : 0]));
|
|
2006
|
+
case "string":
|
|
2007
|
+
if (typeof value !== "string") {
|
|
2008
|
+
throw new Error(`string field expects string: ${typeof value}`);
|
|
2009
|
+
}
|
|
2010
|
+
return shake2562(new TextEncoder().encode(value), { dkLen: DIGEST_LEN });
|
|
2011
|
+
case "bytes":
|
|
2012
|
+
if (typeof value !== "string") {
|
|
2013
|
+
throw new Error(`bytes field expects 0x-hex string: ${typeof value}`);
|
|
2014
|
+
}
|
|
2015
|
+
return shake2562(hexToBytes(value), { dkLen: DIGEST_LEN });
|
|
2016
|
+
case "uintN":
|
|
2017
|
+
return bigIntToSlot(parseIntValue(value, `uint${parsed.width}`), parsed.width, false);
|
|
2018
|
+
case "intN":
|
|
2019
|
+
return bigIntToSlot(parseIntValue(value, `int${parsed.width}`), parsed.width, true);
|
|
2020
|
+
case "bytesN": {
|
|
2021
|
+
if (typeof value !== "string") {
|
|
2022
|
+
throw new Error(`bytes${parsed.width} expects 0x-hex string: ${typeof value}`);
|
|
2023
|
+
}
|
|
2024
|
+
const raw = hexToBytes(value);
|
|
2025
|
+
if (raw.length !== parsed.width) {
|
|
2026
|
+
throw new Error(`bytes${parsed.width} requires ${parsed.width} bytes, got ${raw.length}`);
|
|
2027
|
+
}
|
|
2028
|
+
return padRight32(raw);
|
|
2029
|
+
}
|
|
2030
|
+
case "array": {
|
|
2031
|
+
if (!Array.isArray(value)) {
|
|
2032
|
+
throw new Error(`array field expects array: ${typeof value}`);
|
|
2033
|
+
}
|
|
2034
|
+
if (parsed.size !== void 0 && value.length !== parsed.size) {
|
|
2035
|
+
throw new Error(`fixed array ${type} requires length ${parsed.size}, got ${value.length}`);
|
|
2036
|
+
}
|
|
2037
|
+
const chunks = value.map((v) => encodeField(parsed.inner, v, types));
|
|
2038
|
+
return shake2562(concatBytesArr(chunks), { dkLen: DIGEST_LEN });
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
function hashStruct(primary, data, types) {
|
|
2043
|
+
const fields = types[primary];
|
|
2044
|
+
if (!fields) throw new Error(`unknown struct: ${primary}`);
|
|
2045
|
+
const expected = new Set(fields.map((f) => f.name));
|
|
2046
|
+
for (const k of Object.keys(data)) {
|
|
2047
|
+
if (!expected.has(k)) throw new Error(`unknown field in ${primary}: ${k}`);
|
|
2048
|
+
}
|
|
2049
|
+
const parts = [typeHash(primary, types)];
|
|
2050
|
+
for (const f of fields) {
|
|
2051
|
+
if (!(f.name in data)) throw new Error(`missing field ${primary}.${f.name}`);
|
|
2052
|
+
parts.push(encodeField(f.type, data[f.name], types));
|
|
2053
|
+
}
|
|
2054
|
+
return shake2562(concatBytesArr(parts), { dkLen: DIGEST_LEN });
|
|
2055
|
+
}
|
|
2056
|
+
var RESERVED_DOMAIN_FIELDS = {
|
|
2057
|
+
name: "string",
|
|
2058
|
+
version: "string",
|
|
2059
|
+
chainId: "uint256",
|
|
2060
|
+
verifyingContract: "address",
|
|
2061
|
+
salt: "bytes32"
|
|
2062
|
+
};
|
|
2063
|
+
function validateDomainTypes(types) {
|
|
2064
|
+
const def = types.QRLDomain;
|
|
2065
|
+
if (!def) throw new Error("QRLDomain type is required");
|
|
2066
|
+
let hasName = false;
|
|
2067
|
+
for (const f of def) {
|
|
2068
|
+
const expected = RESERVED_DOMAIN_FIELDS[f.name];
|
|
2069
|
+
if (!expected) {
|
|
2070
|
+
throw new Error(
|
|
2071
|
+
`QRLDomain field "${f.name}" not in reserved set (name, version, chainId, verifyingContract, salt)`
|
|
2072
|
+
);
|
|
2073
|
+
}
|
|
2074
|
+
if (f.type !== expected) {
|
|
2075
|
+
throw new Error(`QRLDomain field "${f.name}" must be type "${expected}", got "${f.type}"`);
|
|
2076
|
+
}
|
|
2077
|
+
if (f.name === "name") hasName = true;
|
|
2078
|
+
}
|
|
2079
|
+
if (!hasName) throw new Error("QRLDomain.name is required");
|
|
2080
|
+
}
|
|
2081
|
+
function validatePayloadReachability(primary, types) {
|
|
2082
|
+
const reachable = /* @__PURE__ */ new Set();
|
|
2083
|
+
for (const root of ["QRLDomain", primary]) {
|
|
2084
|
+
for (const t of collectDependencies(root, types)) reachable.add(t);
|
|
2085
|
+
}
|
|
2086
|
+
for (const k of Object.keys(types)) {
|
|
2087
|
+
if (!reachable.has(k)) throw new Error(`unused referenced type: ${k}`);
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
function computeTypedDataDigest(payload) {
|
|
2091
|
+
if (!payload || typeof payload !== "object") {
|
|
2092
|
+
throw new Error("invalid typed data payload");
|
|
2093
|
+
}
|
|
2094
|
+
validateTypeMap(payload.types);
|
|
2095
|
+
validateDomainTypes(payload.types);
|
|
2096
|
+
validatePayloadReachability(payload.primaryType, payload.types);
|
|
2097
|
+
const domainHash = hashStruct("QRLDomain", payload.domain, payload.types);
|
|
2098
|
+
const messageHash = hashStruct(payload.primaryType, payload.message, payload.types);
|
|
2099
|
+
return shake2562(concatBytes(SCHEME_TAG_TYPED, domainHash, messageHash), { dkLen: DIGEST_LEN });
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
// src/signing/verify.ts
|
|
2103
|
+
import * as mldsa from "@theqrl/mldsa87";
|
|
2104
|
+
function bytesOrHex(v) {
|
|
2105
|
+
if (v instanceof Uint8Array) return v;
|
|
2106
|
+
if (typeof v === "string") return hexToBytes(v);
|
|
2107
|
+
throw new Error("expected Uint8Array or 0x-hex string");
|
|
2108
|
+
}
|
|
2109
|
+
function verifyMessage({
|
|
2110
|
+
signature,
|
|
2111
|
+
publicKey,
|
|
2112
|
+
messageBytes
|
|
2113
|
+
}) {
|
|
2114
|
+
try {
|
|
2115
|
+
const sig = bytesOrHex(signature);
|
|
2116
|
+
const pk = bytesOrHex(publicKey);
|
|
2117
|
+
const msg = bytesOrHex(messageBytes);
|
|
2118
|
+
const digest = computeMessageDigest(msg);
|
|
2119
|
+
return mldsa.cryptoSignVerify(sig, digest, pk, SCHEME_TAG_MSG);
|
|
2120
|
+
} catch {
|
|
2121
|
+
return false;
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
function verifyTypedData({ signature, publicKey, payload }) {
|
|
2125
|
+
try {
|
|
2126
|
+
const sig = bytesOrHex(signature);
|
|
2127
|
+
const pk = bytesOrHex(publicKey);
|
|
2128
|
+
const digest = computeTypedDataDigest(payload);
|
|
2129
|
+
return mldsa.cryptoSignVerify(sig, digest, pk, SCHEME_TAG_TYPED);
|
|
2130
|
+
} catch {
|
|
2131
|
+
return false;
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
1219
2134
|
export {
|
|
2135
|
+
BLOB_LEN,
|
|
2136
|
+
CID_LEN,
|
|
1220
2137
|
ConnectionManager,
|
|
1221
2138
|
ConnectionStatus,
|
|
1222
2139
|
DEFAULT_RELAY_URL,
|
|
1223
|
-
|
|
2140
|
+
DIGEST_LEN,
|
|
2141
|
+
FP_LEN,
|
|
1224
2142
|
KeyExchange,
|
|
1225
2143
|
KeyExchangeMessageType,
|
|
1226
2144
|
MessageType,
|
|
2145
|
+
PROTOCOL_VERSION,
|
|
1227
2146
|
QRLConnectProvider as QRLConnect,
|
|
1228
2147
|
QRLConnectProvider,
|
|
2148
|
+
QRL_CONNECT_PROVIDER_INFO,
|
|
1229
2149
|
RESTRICTED_METHODS,
|
|
2150
|
+
SCHEME_TAG_MSG,
|
|
2151
|
+
SCHEME_TAG_TYPED,
|
|
2152
|
+
SCHEME_VERSION_MSG,
|
|
2153
|
+
SCHEME_VERSION_TYPED,
|
|
1230
2154
|
SocketClient,
|
|
1231
2155
|
UNRESTRICTED_METHODS,
|
|
2156
|
+
bytesToHex,
|
|
2157
|
+
cidFromString,
|
|
2158
|
+
cidRandom,
|
|
2159
|
+
cidToString,
|
|
2160
|
+
computeFingerprint,
|
|
2161
|
+
computeMessageDigest,
|
|
2162
|
+
computeTypedDataDigest,
|
|
2163
|
+
concatBytes,
|
|
2164
|
+
encodeField,
|
|
2165
|
+
encodeType,
|
|
2166
|
+
fingerprintEquals,
|
|
1232
2167
|
generateConnectionURI,
|
|
1233
2168
|
getAppStoreUrl,
|
|
2169
|
+
hashStruct,
|
|
2170
|
+
hexToBytes,
|
|
1234
2171
|
isMobileBrowser,
|
|
1235
|
-
parseConnectionURI
|
|
2172
|
+
parseConnectionURI,
|
|
2173
|
+
typeHash,
|
|
2174
|
+
verifyMessage,
|
|
2175
|
+
verifyTypedData
|
|
1236
2176
|
};
|