@fivenorth/loop-sdk 0.7.1 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +281 -148
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2067,6 +2067,43 @@ var MessageType;
|
|
|
2067
2067
|
MessageType2["REJECT_REQUEST"] = "reject_request";
|
|
2068
2068
|
})(MessageType ||= {});
|
|
2069
2069
|
|
|
2070
|
+
// src/errors.ts
|
|
2071
|
+
class RequestTimeoutError extends Error {
|
|
2072
|
+
constructor(timeout) {
|
|
2073
|
+
super(`Request timed out after ${timeout}ms.`);
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
class RejectRequestError extends Error {
|
|
2078
|
+
constructor() {
|
|
2079
|
+
super("Request was rejected by the wallet.");
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
class UnauthorizedError extends Error {
|
|
2084
|
+
code;
|
|
2085
|
+
constructor(code) {
|
|
2086
|
+
super(code || "Unauthorized");
|
|
2087
|
+
this.code = code;
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
var UNAUTH_CODES = new Set(["UNAUTHENTICATED", "UNAUTHORIZED", "SESSION_EXPIRED", "LOGGED_OUT"]);
|
|
2091
|
+
function extractErrorCode(message) {
|
|
2092
|
+
if (typeof message?.error?.code === "string" && message.error.code.length > 0) {
|
|
2093
|
+
return message.error.code;
|
|
2094
|
+
}
|
|
2095
|
+
if (message?.type === "unauthorized" && typeof message?.code === "string") {
|
|
2096
|
+
return message.code;
|
|
2097
|
+
}
|
|
2098
|
+
return null;
|
|
2099
|
+
}
|
|
2100
|
+
function isUnauthCode(code) {
|
|
2101
|
+
if (!code) {
|
|
2102
|
+
return false;
|
|
2103
|
+
}
|
|
2104
|
+
return UNAUTH_CODES.has(code);
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2070
2107
|
// src/connection.ts
|
|
2071
2108
|
class Connection {
|
|
2072
2109
|
walletUrl = "https://cantonloop.com";
|
|
@@ -2076,6 +2113,7 @@ class Connection {
|
|
|
2076
2113
|
ticketId = null;
|
|
2077
2114
|
onMessageHandler = null;
|
|
2078
2115
|
reconnectPromise = null;
|
|
2116
|
+
status = "disconnected";
|
|
2079
2117
|
constructor({ network, walletUrl, apiUrl }) {
|
|
2080
2118
|
this.network = network || "main";
|
|
2081
2119
|
switch (this.network) {
|
|
@@ -2106,6 +2144,9 @@ class Connection {
|
|
|
2106
2144
|
this.apiUrl = apiUrl;
|
|
2107
2145
|
}
|
|
2108
2146
|
}
|
|
2147
|
+
connectInProgress() {
|
|
2148
|
+
return this.status === "connecting" || this.status === "connected";
|
|
2149
|
+
}
|
|
2109
2150
|
async getTicket(appName, sessionId, version) {
|
|
2110
2151
|
const response = await fetch(`${this.apiUrl}/api/v1/.connect/pair/tickets`, {
|
|
2111
2152
|
method: "POST",
|
|
@@ -2198,7 +2239,10 @@ class Connection {
|
|
|
2198
2239
|
}
|
|
2199
2240
|
});
|
|
2200
2241
|
if (!response.ok) {
|
|
2201
|
-
|
|
2242
|
+
if (response.status === 401 || response.status === 403) {
|
|
2243
|
+
throw new UnauthorizedError;
|
|
2244
|
+
}
|
|
2245
|
+
throw new Error(`Session verification failed with status ${response.status}.`);
|
|
2202
2246
|
}
|
|
2203
2247
|
const data = await response.json();
|
|
2204
2248
|
const email = data?.email;
|
|
@@ -2209,49 +2253,31 @@ class Connection {
|
|
|
2209
2253
|
party_id: data?.party_id,
|
|
2210
2254
|
auth_token: authToken,
|
|
2211
2255
|
public_key: data?.public_key,
|
|
2212
|
-
email
|
|
2256
|
+
email,
|
|
2257
|
+
has_preapproval: data?.has_preapproval,
|
|
2258
|
+
has_merge_delegation: data?.has_merge_delegation,
|
|
2259
|
+
usdc_bridge_access: data?.usdc_bridge_access
|
|
2213
2260
|
};
|
|
2214
2261
|
return account;
|
|
2215
2262
|
}
|
|
2216
|
-
websocketUrl(ticketId) {
|
|
2217
|
-
return `${this.network === "local" ? "ws" : "wss"}://${this.apiUrl.replace("https://", "").replace("http://", "")}/api/v1/.connect/pair/ws/${ticketId}`;
|
|
2218
|
-
}
|
|
2219
|
-
attachWebSocket(ticketId, onMessage, onOpen, onError, onClose) {
|
|
2220
|
-
const wsUrl = this.websocketUrl(ticketId);
|
|
2221
|
-
const ws = new WebSocket(wsUrl);
|
|
2222
|
-
ws.onmessage = onMessage;
|
|
2223
|
-
ws.onopen = () => {
|
|
2224
|
-
console.log("Connected to ticket server.");
|
|
2225
|
-
onOpen?.();
|
|
2226
|
-
};
|
|
2227
|
-
ws.onclose = (event) => {
|
|
2228
|
-
if (this.ws === ws) {
|
|
2229
|
-
this.ws = null;
|
|
2230
|
-
}
|
|
2231
|
-
console.log("Disconnected from ticket server.");
|
|
2232
|
-
onClose?.(event);
|
|
2233
|
-
};
|
|
2234
|
-
ws.onerror = (event) => {
|
|
2235
|
-
if (this.ws === ws) {
|
|
2236
|
-
this.ws = null;
|
|
2237
|
-
}
|
|
2238
|
-
onError?.(event);
|
|
2239
|
-
};
|
|
2240
|
-
this.ws = ws;
|
|
2241
|
-
}
|
|
2242
2263
|
connectWebSocket(ticketId, onMessage) {
|
|
2243
|
-
this.ticketId
|
|
2264
|
+
if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) && this.ticketId !== ticketId) {
|
|
2265
|
+
this.ws.close();
|
|
2266
|
+
this.ws = null;
|
|
2267
|
+
}
|
|
2268
|
+
if (this.status === "connecting" || this.status === "connected") {
|
|
2269
|
+
return;
|
|
2270
|
+
}
|
|
2244
2271
|
this.onMessageHandler = onMessage;
|
|
2272
|
+
this.ticketId = ticketId;
|
|
2273
|
+
this.status = "connecting";
|
|
2245
2274
|
this.attachWebSocket(ticketId, onMessage);
|
|
2246
2275
|
}
|
|
2247
2276
|
reconnect() {
|
|
2248
2277
|
if (!this.ticketId || !this.onMessageHandler) {
|
|
2249
2278
|
return Promise.reject(new Error("Cannot reconnect without a known ticket."));
|
|
2250
2279
|
}
|
|
2251
|
-
|
|
2252
|
-
return this.reconnectPromise;
|
|
2253
|
-
}
|
|
2254
|
-
this.reconnectPromise = new Promise((resolve, reject) => {
|
|
2280
|
+
return new Promise((resolve, reject) => {
|
|
2255
2281
|
let opened = false;
|
|
2256
2282
|
this.attachWebSocket(this.ticketId, this.onMessageHandler, () => {
|
|
2257
2283
|
opened = true;
|
|
@@ -2267,29 +2293,37 @@ class Connection {
|
|
|
2267
2293
|
}
|
|
2268
2294
|
reject(new Error("Failed to reconnect to ticket server."));
|
|
2269
2295
|
});
|
|
2270
|
-
}).finally(() => {
|
|
2271
|
-
this.reconnectPromise = null;
|
|
2272
2296
|
});
|
|
2273
|
-
return this.reconnectPromise;
|
|
2274
2297
|
}
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
return;
|
|
2278
|
-
}
|
|
2279
|
-
return this.reconnect();
|
|
2280
|
-
}
|
|
2281
|
-
}
|
|
2282
|
-
|
|
2283
|
-
// src/errors.ts
|
|
2284
|
-
class RequestTimeoutError extends Error {
|
|
2285
|
-
constructor(timeout) {
|
|
2286
|
-
super(`Request timed out after ${timeout}ms.`);
|
|
2298
|
+
websocketUrl(ticketId) {
|
|
2299
|
+
return `${this.network === "local" ? "ws" : "wss"}://${this.apiUrl.replace("https://", "").replace("http://", "")}/api/v1/.connect/pair/ws/${encodeURIComponent(ticketId)}`;
|
|
2287
2300
|
}
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2301
|
+
attachWebSocket(ticketId, onMessage, onOpen, onError, onClose) {
|
|
2302
|
+
const wsUrl = this.websocketUrl(ticketId);
|
|
2303
|
+
const ws = new WebSocket(wsUrl);
|
|
2304
|
+
ws.onmessage = onMessage;
|
|
2305
|
+
ws.onopen = () => {
|
|
2306
|
+
this.status = "connected";
|
|
2307
|
+
console.log("[LoopSDK] Connected to ticket server.");
|
|
2308
|
+
onOpen?.();
|
|
2309
|
+
};
|
|
2310
|
+
ws.onclose = (event) => {
|
|
2311
|
+
this.status = "disconnected";
|
|
2312
|
+
if (this.ws === ws) {
|
|
2313
|
+
this.ws = null;
|
|
2314
|
+
}
|
|
2315
|
+
console.log("[LoopSDK] Disconnected from ticket server.");
|
|
2316
|
+
onClose?.(event);
|
|
2317
|
+
};
|
|
2318
|
+
ws.onerror = (event) => {
|
|
2319
|
+
this.status = "disconnected";
|
|
2320
|
+
ws.close();
|
|
2321
|
+
if (this.ws === ws) {
|
|
2322
|
+
this.ws = null;
|
|
2323
|
+
}
|
|
2324
|
+
onError?.(event);
|
|
2325
|
+
};
|
|
2326
|
+
this.ws = ws;
|
|
2293
2327
|
}
|
|
2294
2328
|
}
|
|
2295
2329
|
|
|
@@ -2345,10 +2379,13 @@ class Provider {
|
|
|
2345
2379
|
this.requests.set(message.request_id, message);
|
|
2346
2380
|
}
|
|
2347
2381
|
}
|
|
2348
|
-
|
|
2382
|
+
getHolding() {
|
|
2349
2383
|
return this.connection.getHolding(this.auth_token);
|
|
2350
2384
|
}
|
|
2351
|
-
|
|
2385
|
+
getAccount() {
|
|
2386
|
+
return this.connection.verifySession(this.auth_token);
|
|
2387
|
+
}
|
|
2388
|
+
getActiveContracts(params) {
|
|
2352
2389
|
return this.connection.getActiveContracts(this.auth_token, params);
|
|
2353
2390
|
}
|
|
2354
2391
|
async submitTransaction(payload, options) {
|
|
@@ -2397,24 +2434,24 @@ class Provider {
|
|
|
2397
2434
|
}
|
|
2398
2435
|
async ensureConnected() {
|
|
2399
2436
|
if (this.connection.ws && this.connection.ws.readyState === WebSocket.OPEN) {
|
|
2400
|
-
return;
|
|
2437
|
+
return Promise.resolve();
|
|
2401
2438
|
}
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
return;
|
|
2406
|
-
}
|
|
2439
|
+
await this.connection.reconnect();
|
|
2440
|
+
if (this.connection.ws && this.connection.ws.readyState === WebSocket.OPEN) {
|
|
2441
|
+
return;
|
|
2407
2442
|
}
|
|
2408
2443
|
throw new Error("Not connected.");
|
|
2409
2444
|
}
|
|
2410
2445
|
sendRequest(messageType, params = {}, options) {
|
|
2411
2446
|
return new Promise((resolve, reject) => {
|
|
2412
2447
|
const requestId = generateRequestId();
|
|
2413
|
-
|
|
2448
|
+
let requestContext;
|
|
2414
2449
|
const ensure = async () => {
|
|
2415
2450
|
try {
|
|
2416
2451
|
await this.ensureConnected();
|
|
2452
|
+
requestContext = await this.hooks?.onRequestStart?.(messageType, options?.requestLabel);
|
|
2417
2453
|
} catch (error) {
|
|
2454
|
+
console.error("[LoopSDK] error when checking connection status", error);
|
|
2418
2455
|
this.hooks?.onRequestFinish?.({
|
|
2419
2456
|
status: "error",
|
|
2420
2457
|
messageType,
|
|
@@ -2438,7 +2475,13 @@ class Provider {
|
|
|
2438
2475
|
};
|
|
2439
2476
|
}
|
|
2440
2477
|
}
|
|
2441
|
-
|
|
2478
|
+
try {
|
|
2479
|
+
this.connection.ws.send(JSON.stringify(requestBody));
|
|
2480
|
+
} catch (error) {
|
|
2481
|
+
console.error("[LoopSDK] error when sending request", error);
|
|
2482
|
+
reject(error);
|
|
2483
|
+
return;
|
|
2484
|
+
}
|
|
2442
2485
|
const intervalTime = 300;
|
|
2443
2486
|
let elapsedTime = 0;
|
|
2444
2487
|
const timeoutMs = options?.requestTimeout ?? this.requestTimeout;
|
|
@@ -2447,6 +2490,18 @@ class Provider {
|
|
|
2447
2490
|
if (response) {
|
|
2448
2491
|
clearInterval(intervalId);
|
|
2449
2492
|
this.requests.delete(requestId);
|
|
2493
|
+
const code = extractErrorCode(response);
|
|
2494
|
+
if (isUnauthCode(code)) {
|
|
2495
|
+
this.hooks?.onRequestFinish?.({
|
|
2496
|
+
status: "error",
|
|
2497
|
+
messageType,
|
|
2498
|
+
requestLabel: options?.requestLabel,
|
|
2499
|
+
requestContext,
|
|
2500
|
+
errorCode: code
|
|
2501
|
+
});
|
|
2502
|
+
reject(new UnauthorizedError(code));
|
|
2503
|
+
return;
|
|
2504
|
+
}
|
|
2450
2505
|
if (response.type === "reject_request" /* REJECT_REQUEST */) {
|
|
2451
2506
|
this.hooks?.onRequestFinish?.({
|
|
2452
2507
|
status: "rejected",
|
|
@@ -2562,11 +2617,87 @@ class LoopWallet {
|
|
|
2562
2617
|
}
|
|
2563
2618
|
}
|
|
2564
2619
|
|
|
2620
|
+
// src/session.ts
|
|
2621
|
+
var STORAGE_KEY_LOOP_CONNECT = "loop_connect";
|
|
2622
|
+
|
|
2623
|
+
class SessionInfo {
|
|
2624
|
+
sessionId;
|
|
2625
|
+
ticketId;
|
|
2626
|
+
authToken;
|
|
2627
|
+
partyId;
|
|
2628
|
+
publicKey;
|
|
2629
|
+
email;
|
|
2630
|
+
_isAuthorized = false;
|
|
2631
|
+
constructor({ sessionId, ticketId, authToken, partyId, publicKey, email }) {
|
|
2632
|
+
this.sessionId = sessionId;
|
|
2633
|
+
this.ticketId = ticketId;
|
|
2634
|
+
this.authToken = authToken;
|
|
2635
|
+
this.partyId = partyId;
|
|
2636
|
+
this.publicKey = publicKey;
|
|
2637
|
+
this.email = email;
|
|
2638
|
+
}
|
|
2639
|
+
setTicketId(ticketId) {
|
|
2640
|
+
this.ticketId = ticketId;
|
|
2641
|
+
this.save();
|
|
2642
|
+
}
|
|
2643
|
+
authorized() {
|
|
2644
|
+
if (this.ticketId === undefined || this.sessionId === undefined || this.authToken === undefined || this.partyId === undefined || this.publicKey === undefined) {
|
|
2645
|
+
throw new Error("Session cannot be authorized without all required fields.");
|
|
2646
|
+
}
|
|
2647
|
+
this._isAuthorized = true;
|
|
2648
|
+
}
|
|
2649
|
+
isPreAuthorized() {
|
|
2650
|
+
return !this._isAuthorized && this.ticketId !== undefined && this.sessionId !== undefined && this.authToken !== undefined && this.partyId !== undefined && this.publicKey !== undefined;
|
|
2651
|
+
}
|
|
2652
|
+
isAuthorized() {
|
|
2653
|
+
return this._isAuthorized;
|
|
2654
|
+
}
|
|
2655
|
+
save() {
|
|
2656
|
+
localStorage.setItem("loop_connect", this.toJson());
|
|
2657
|
+
}
|
|
2658
|
+
reset() {
|
|
2659
|
+
localStorage.removeItem(STORAGE_KEY_LOOP_CONNECT);
|
|
2660
|
+
this.sessionId = generateRequestId();
|
|
2661
|
+
this._isAuthorized = false;
|
|
2662
|
+
this.ticketId = undefined;
|
|
2663
|
+
this.authToken = undefined;
|
|
2664
|
+
this.partyId = undefined;
|
|
2665
|
+
this.publicKey = undefined;
|
|
2666
|
+
this.email = undefined;
|
|
2667
|
+
}
|
|
2668
|
+
static fromStorage() {
|
|
2669
|
+
const existingConnectionRaw = localStorage.getItem(STORAGE_KEY_LOOP_CONNECT);
|
|
2670
|
+
if (!existingConnectionRaw) {
|
|
2671
|
+
return new SessionInfo({ sessionId: generateRequestId() });
|
|
2672
|
+
}
|
|
2673
|
+
let session = null;
|
|
2674
|
+
try {
|
|
2675
|
+
session = new SessionInfo(JSON.parse(existingConnectionRaw));
|
|
2676
|
+
} catch (error) {
|
|
2677
|
+
console.error("Failed to parse existing connection info, local storage is corrupted.", error);
|
|
2678
|
+
localStorage.removeItem(STORAGE_KEY_LOOP_CONNECT);
|
|
2679
|
+
session = new SessionInfo({ sessionId: generateRequestId() });
|
|
2680
|
+
}
|
|
2681
|
+
return session;
|
|
2682
|
+
}
|
|
2683
|
+
toJson() {
|
|
2684
|
+
return JSON.stringify({
|
|
2685
|
+
sessionId: this.sessionId,
|
|
2686
|
+
ticketId: this.ticketId,
|
|
2687
|
+
authToken: this.authToken,
|
|
2688
|
+
partyId: this.partyId,
|
|
2689
|
+
publicKey: this.publicKey,
|
|
2690
|
+
email: this.email
|
|
2691
|
+
});
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
|
|
2565
2695
|
// src/index.ts
|
|
2566
2696
|
class LoopSDK {
|
|
2567
|
-
version = "0.
|
|
2697
|
+
version = "0.7.3";
|
|
2568
2698
|
appName = "Unknown";
|
|
2569
2699
|
connection = null;
|
|
2700
|
+
session = null;
|
|
2570
2701
|
provider = null;
|
|
2571
2702
|
openMode = "popup";
|
|
2572
2703
|
requestSigningMode = "popup";
|
|
@@ -2575,7 +2706,6 @@ class LoopSDK {
|
|
|
2575
2706
|
onAccept = null;
|
|
2576
2707
|
onReject = null;
|
|
2577
2708
|
overlay = null;
|
|
2578
|
-
ticketId = null;
|
|
2579
2709
|
wallet;
|
|
2580
2710
|
constructor() {
|
|
2581
2711
|
this.wallet = new LoopWallet(() => this.provider);
|
|
@@ -2589,6 +2719,9 @@ class LoopSDK {
|
|
|
2589
2719
|
onReject,
|
|
2590
2720
|
options
|
|
2591
2721
|
}) {
|
|
2722
|
+
if (typeof window === "undefined" || typeof document === "undefined" || typeof localStorage === "undefined") {
|
|
2723
|
+
throw new Error("LoopSDK can only be initialized in a browser environment with localStorage support.");
|
|
2724
|
+
}
|
|
2592
2725
|
this.appName = appName;
|
|
2593
2726
|
this.onAccept = onAccept || null;
|
|
2594
2727
|
this.onReject = onReject || null;
|
|
@@ -2603,70 +2736,68 @@ class LoopSDK {
|
|
|
2603
2736
|
this.redirectUrl = resolvedOptions.redirectUrl;
|
|
2604
2737
|
this.connection = new Connection({ network, walletUrl, apiUrl });
|
|
2605
2738
|
}
|
|
2606
|
-
async
|
|
2607
|
-
if (
|
|
2608
|
-
|
|
2739
|
+
async loadSessionInfo() {
|
|
2740
|
+
if (this.session) {
|
|
2741
|
+
return;
|
|
2742
|
+
}
|
|
2743
|
+
this.session = SessionInfo.fromStorage();
|
|
2744
|
+
if (!this.session.isPreAuthorized()) {
|
|
2609
2745
|
return;
|
|
2610
2746
|
}
|
|
2747
|
+
try {
|
|
2748
|
+
const verifiedAccount = await this.connection?.verifySession(this.session.authToken);
|
|
2749
|
+
if (!verifiedAccount || verifiedAccount?.party_id !== this.session.partyId) {
|
|
2750
|
+
console.warn("[LoopSDK] Stored partyId does not match verified account. Clearing cached session.");
|
|
2751
|
+
this.logout();
|
|
2752
|
+
return;
|
|
2753
|
+
}
|
|
2754
|
+
this.session.authorized();
|
|
2755
|
+
} catch (err) {
|
|
2756
|
+
if (err instanceof UnauthorizedError) {
|
|
2757
|
+
console.error("Unauthorized error when verifying session.", err);
|
|
2758
|
+
this.session.reset();
|
|
2759
|
+
return;
|
|
2760
|
+
}
|
|
2761
|
+
console.error("[LoopSDK] Failed to verify session.", err);
|
|
2762
|
+
throw err;
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
async autoConnect() {
|
|
2611
2766
|
if (!this.connection) {
|
|
2612
2767
|
throw new Error("SDK not initialized. Call init() first.");
|
|
2613
2768
|
}
|
|
2614
|
-
|
|
2615
|
-
if (
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
} catch (err) {
|
|
2643
|
-
console.error("Auto-login failed, token is invalid. Starting new connection.", err);
|
|
2644
|
-
canReuseTicket = false;
|
|
2645
|
-
localStorage.removeItem("loop_connect");
|
|
2646
|
-
}
|
|
2647
|
-
}
|
|
2648
|
-
if (ticketId && canReuseTicket) {
|
|
2649
|
-
this.ticketId = ticketId;
|
|
2650
|
-
const url = new URL("/.connect/", this.connection.walletUrl);
|
|
2651
|
-
url.searchParams.set("ticketId", ticketId);
|
|
2652
|
-
if (this.redirectUrl) {
|
|
2653
|
-
url.searchParams.set("redirectUrl", this.redirectUrl);
|
|
2654
|
-
}
|
|
2655
|
-
const connectUrl = url.toString();
|
|
2656
|
-
this.showQrCode(connectUrl);
|
|
2657
|
-
this.connection.connectWebSocket(ticketId, this.handleWebSocketMessage.bind(this));
|
|
2658
|
-
return;
|
|
2659
|
-
}
|
|
2660
|
-
} catch (error) {
|
|
2661
|
-
console.error("Failed to parse existing connection info, creating a new one.", error);
|
|
2662
|
-
}
|
|
2663
|
-
localStorage.removeItem("loop_connect");
|
|
2769
|
+
await this.loadSessionInfo();
|
|
2770
|
+
if (!this.session) {
|
|
2771
|
+
throw new Error("No valid session found. The network connection maynot available or the backend is not reachable.");
|
|
2772
|
+
}
|
|
2773
|
+
if (this.session.isAuthorized()) {
|
|
2774
|
+
this.provider = new Provider({
|
|
2775
|
+
connection: this.connection,
|
|
2776
|
+
party_id: this.session.partyId,
|
|
2777
|
+
auth_token: this.session.authToken,
|
|
2778
|
+
public_key: this.session.publicKey,
|
|
2779
|
+
email: this.session.email,
|
|
2780
|
+
hooks: this.createProviderHooks()
|
|
2781
|
+
});
|
|
2782
|
+
this.onAccept?.(this.provider);
|
|
2783
|
+
this.connection.connectWebSocket(this.session.ticketId, this.handleWebSocketMessage.bind(this));
|
|
2784
|
+
return Promise.resolve();
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
async connect() {
|
|
2788
|
+
if (!this.connection) {
|
|
2789
|
+
throw new Error("SDK not initialized. Call init() first.");
|
|
2790
|
+
}
|
|
2791
|
+
await this.autoConnect();
|
|
2792
|
+
if (this.connection?.connectInProgress() === true) {
|
|
2793
|
+
return;
|
|
2794
|
+
}
|
|
2795
|
+
if (this.session && this.session.isAuthorized()) {
|
|
2796
|
+
return;
|
|
2664
2797
|
}
|
|
2665
|
-
const sessionId = generateRequestId();
|
|
2666
2798
|
try {
|
|
2667
|
-
const { ticket_id: ticketId } = await this.connection.getTicket(this.appName, sessionId, this.version);
|
|
2668
|
-
this.ticketId
|
|
2669
|
-
localStorage.setItem("loop_connect", JSON.stringify({ sessionId, ticketId }));
|
|
2799
|
+
const { ticket_id: ticketId } = await this.connection.getTicket(this.appName, this.session.sessionId, this.version);
|
|
2800
|
+
this.session.setTicketId(ticketId);
|
|
2670
2801
|
const connectUrl = this.buildConnectUrl(ticketId);
|
|
2671
2802
|
this.showQrCode(connectUrl);
|
|
2672
2803
|
this.connection.connectWebSocket(ticketId, this.handleWebSocketMessage.bind(this));
|
|
@@ -2677,6 +2808,12 @@ class LoopSDK {
|
|
|
2677
2808
|
}
|
|
2678
2809
|
handleWebSocketMessage(event) {
|
|
2679
2810
|
const message = JSON.parse(event.data);
|
|
2811
|
+
const errCode = extractErrorCode(message);
|
|
2812
|
+
if (isUnauthCode(errCode)) {
|
|
2813
|
+
console.warn("[LoopSDK] Detected session invalidation:", errCode, { message });
|
|
2814
|
+
this.logout();
|
|
2815
|
+
return;
|
|
2816
|
+
}
|
|
2680
2817
|
console.log("[LoopSDK] WS message received:", message);
|
|
2681
2818
|
if (message.type === "handshake_accept" /* HANDSHAKE_ACCEPT */) {
|
|
2682
2819
|
console.log("[LoopSDK] Entering HANDSHAKE_ACCEPT flow");
|
|
@@ -2690,32 +2827,27 @@ class LoopSDK {
|
|
|
2690
2827
|
email,
|
|
2691
2828
|
hooks: this.createProviderHooks()
|
|
2692
2829
|
});
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
console.log("[LoopSDK] HANDSHAKE_ACCEPT: closing popup (if exists)");
|
|
2707
|
-
this.popupWindow = null;
|
|
2708
|
-
} catch (error) {
|
|
2709
|
-
console.error("Failed to update local storage with auth token.", error);
|
|
2710
|
-
}
|
|
2830
|
+
try {
|
|
2831
|
+
this.session.authToken = authToken;
|
|
2832
|
+
this.session.partyId = partyId;
|
|
2833
|
+
this.session.publicKey = publicKey;
|
|
2834
|
+
this.session.email = email;
|
|
2835
|
+
this.session.authorized();
|
|
2836
|
+
this.session.save();
|
|
2837
|
+
this.onAccept?.(this.provider);
|
|
2838
|
+
this.hideQrCode();
|
|
2839
|
+
console.log("[LoopSDK] HANDSHAKE_ACCEPT: closing popup (if exists)");
|
|
2840
|
+
this.popupWindow = null;
|
|
2841
|
+
} catch (error) {
|
|
2842
|
+
console.error("Failed to update local storage with auth token.", error);
|
|
2711
2843
|
}
|
|
2712
2844
|
}
|
|
2713
2845
|
} else if (message.type === "handshake_reject" /* HANDSHAKE_REJECT */) {
|
|
2714
2846
|
console.log("[LoopSDK] Entering HANDSHAKE_REJECT flow");
|
|
2715
|
-
localStorage.removeItem("loop_connect");
|
|
2716
2847
|
this.connection?.ws?.close();
|
|
2717
2848
|
this.onReject?.();
|
|
2718
2849
|
this.hideQrCode();
|
|
2850
|
+
this.session?.reset();
|
|
2719
2851
|
console.log("[LoopSDK] HANDSHAKE_REJECT: closing popup (if exists)");
|
|
2720
2852
|
this.popupWindow = null;
|
|
2721
2853
|
} else if (this.provider) {
|
|
@@ -2740,7 +2872,7 @@ class LoopSDK {
|
|
|
2740
2872
|
if (typeof window === "undefined") {
|
|
2741
2873
|
return null;
|
|
2742
2874
|
}
|
|
2743
|
-
if (!this.ticketId) {
|
|
2875
|
+
if (!this.session?.ticketId) {
|
|
2744
2876
|
console.warn("[LoopSDK] Cannot open wallet UI for request: no active ticket.");
|
|
2745
2877
|
return null;
|
|
2746
2878
|
}
|
|
@@ -2785,9 +2917,6 @@ class LoopSDK {
|
|
|
2785
2917
|
return window.open(url, "_blank", "noopener,noreferrer");
|
|
2786
2918
|
}
|
|
2787
2919
|
showQrCode(url) {
|
|
2788
|
-
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
2789
|
-
return;
|
|
2790
|
-
}
|
|
2791
2920
|
import_qrcode.default.toDataURL(url, (err, dataUrl) => {
|
|
2792
2921
|
if (err) {
|
|
2793
2922
|
console.error("Failed to generate QR code", err);
|
|
@@ -2841,6 +2970,12 @@ class LoopSDK {
|
|
|
2841
2970
|
this.overlay = null;
|
|
2842
2971
|
}
|
|
2843
2972
|
}
|
|
2973
|
+
logout() {
|
|
2974
|
+
this.session?.reset();
|
|
2975
|
+
this.provider = null;
|
|
2976
|
+
this.connection?.ws?.close();
|
|
2977
|
+
this.hideQrCode();
|
|
2978
|
+
}
|
|
2844
2979
|
requireProvider() {
|
|
2845
2980
|
if (!this.provider) {
|
|
2846
2981
|
throw new Error("SDK not connected. Call connect() and wait for acceptance first.");
|
|
@@ -2849,9 +2984,7 @@ class LoopSDK {
|
|
|
2849
2984
|
}
|
|
2850
2985
|
createProviderHooks() {
|
|
2851
2986
|
return {
|
|
2852
|
-
onRequestStart: () =>
|
|
2853
|
-
return this.openRequestUi();
|
|
2854
|
-
},
|
|
2987
|
+
onRequestStart: () => this.openRequestUi(),
|
|
2855
2988
|
onRequestFinish: ({ requestContext }) => {
|
|
2856
2989
|
const win = requestContext;
|
|
2857
2990
|
if (win) {
|