@fivenorth/loop-sdk 0.7.2 → 0.7.4
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 +428 -186
- package/package.json +29 -29
package/dist/index.js
CHANGED
|
@@ -2055,17 +2055,42 @@ var require_browser = __commonJS((exports) => {
|
|
|
2055
2055
|
// src/index.ts
|
|
2056
2056
|
var import_qrcode = __toESM(require_browser(), 1);
|
|
2057
2057
|
|
|
2058
|
-
// src/
|
|
2059
|
-
|
|
2060
|
-
(
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
}
|
|
2058
|
+
// src/errors.ts
|
|
2059
|
+
class RequestTimeoutError extends Error {
|
|
2060
|
+
constructor(timeout) {
|
|
2061
|
+
super(`Request timed out after ${timeout}ms.`);
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
class RejectRequestError extends Error {
|
|
2066
|
+
constructor() {
|
|
2067
|
+
super("Request was rejected by the wallet.");
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
class UnauthorizedError extends Error {
|
|
2072
|
+
code;
|
|
2073
|
+
constructor(code) {
|
|
2074
|
+
super(code || "Unauthorized");
|
|
2075
|
+
this.code = code;
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
var UNAUTH_CODES = new Set(["UNAUTHENTICATED", "UNAUTHORIZED", "SESSION_EXPIRED", "LOGGED_OUT"]);
|
|
2079
|
+
function extractErrorCode(message) {
|
|
2080
|
+
if (typeof message?.error?.code === "string" && message.error.code.length > 0) {
|
|
2081
|
+
return message.error.code;
|
|
2082
|
+
}
|
|
2083
|
+
if (message?.type === "unauthorized" && typeof message?.code === "string") {
|
|
2084
|
+
return message.code;
|
|
2085
|
+
}
|
|
2086
|
+
return null;
|
|
2087
|
+
}
|
|
2088
|
+
function isUnauthCode(code) {
|
|
2089
|
+
if (!code) {
|
|
2090
|
+
return false;
|
|
2091
|
+
}
|
|
2092
|
+
return UNAUTH_CODES.has(code);
|
|
2093
|
+
}
|
|
2069
2094
|
|
|
2070
2095
|
// src/connection.ts
|
|
2071
2096
|
class Connection {
|
|
@@ -2076,6 +2101,7 @@ class Connection {
|
|
|
2076
2101
|
ticketId = null;
|
|
2077
2102
|
onMessageHandler = null;
|
|
2078
2103
|
reconnectPromise = null;
|
|
2104
|
+
status = "disconnected";
|
|
2079
2105
|
constructor({ network, walletUrl, apiUrl }) {
|
|
2080
2106
|
this.network = network || "main";
|
|
2081
2107
|
switch (this.network) {
|
|
@@ -2106,6 +2132,9 @@ class Connection {
|
|
|
2106
2132
|
this.apiUrl = apiUrl;
|
|
2107
2133
|
}
|
|
2108
2134
|
}
|
|
2135
|
+
connectInProgress() {
|
|
2136
|
+
return this.status === "connecting" || this.status === "connected";
|
|
2137
|
+
}
|
|
2109
2138
|
async getTicket(appName, sessionId, version) {
|
|
2110
2139
|
const response = await fetch(`${this.apiUrl}/api/v1/.connect/pair/tickets`, {
|
|
2111
2140
|
method: "POST",
|
|
@@ -2198,7 +2227,10 @@ class Connection {
|
|
|
2198
2227
|
}
|
|
2199
2228
|
});
|
|
2200
2229
|
if (!response.ok) {
|
|
2201
|
-
|
|
2230
|
+
if (response.status === 401 || response.status === 403) {
|
|
2231
|
+
throw new UnauthorizedError;
|
|
2232
|
+
}
|
|
2233
|
+
throw new Error(`Session verification failed with status ${response.status}.`);
|
|
2202
2234
|
}
|
|
2203
2235
|
const data = await response.json();
|
|
2204
2236
|
const email = data?.email;
|
|
@@ -2216,45 +2248,24 @@ class Connection {
|
|
|
2216
2248
|
};
|
|
2217
2249
|
return account;
|
|
2218
2250
|
}
|
|
2219
|
-
websocketUrl(ticketId) {
|
|
2220
|
-
return `${this.network === "local" ? "ws" : "wss"}://${this.apiUrl.replace("https://", "").replace("http://", "")}/api/v1/.connect/pair/ws/${ticketId}`;
|
|
2221
|
-
}
|
|
2222
|
-
attachWebSocket(ticketId, onMessage, onOpen, onError, onClose) {
|
|
2223
|
-
const wsUrl = this.websocketUrl(ticketId);
|
|
2224
|
-
const ws = new WebSocket(wsUrl);
|
|
2225
|
-
ws.onmessage = onMessage;
|
|
2226
|
-
ws.onopen = () => {
|
|
2227
|
-
console.log("Connected to ticket server.");
|
|
2228
|
-
onOpen?.();
|
|
2229
|
-
};
|
|
2230
|
-
ws.onclose = (event) => {
|
|
2231
|
-
if (this.ws === ws) {
|
|
2232
|
-
this.ws = null;
|
|
2233
|
-
}
|
|
2234
|
-
console.log("Disconnected from ticket server.");
|
|
2235
|
-
onClose?.(event);
|
|
2236
|
-
};
|
|
2237
|
-
ws.onerror = (event) => {
|
|
2238
|
-
if (this.ws === ws) {
|
|
2239
|
-
this.ws = null;
|
|
2240
|
-
}
|
|
2241
|
-
onError?.(event);
|
|
2242
|
-
};
|
|
2243
|
-
this.ws = ws;
|
|
2244
|
-
}
|
|
2245
2251
|
connectWebSocket(ticketId, onMessage) {
|
|
2246
|
-
this.ticketId
|
|
2252
|
+
if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) && this.ticketId !== ticketId) {
|
|
2253
|
+
this.ws.close();
|
|
2254
|
+
this.ws = null;
|
|
2255
|
+
}
|
|
2256
|
+
if (this.status === "connecting" || this.status === "connected") {
|
|
2257
|
+
return;
|
|
2258
|
+
}
|
|
2247
2259
|
this.onMessageHandler = onMessage;
|
|
2260
|
+
this.ticketId = ticketId;
|
|
2261
|
+
this.status = "connecting";
|
|
2248
2262
|
this.attachWebSocket(ticketId, onMessage);
|
|
2249
2263
|
}
|
|
2250
2264
|
reconnect() {
|
|
2251
2265
|
if (!this.ticketId || !this.onMessageHandler) {
|
|
2252
2266
|
return Promise.reject(new Error("Cannot reconnect without a known ticket."));
|
|
2253
2267
|
}
|
|
2254
|
-
|
|
2255
|
-
return this.reconnectPromise;
|
|
2256
|
-
}
|
|
2257
|
-
this.reconnectPromise = new Promise((resolve, reject) => {
|
|
2268
|
+
return new Promise((resolve, reject) => {
|
|
2258
2269
|
let opened = false;
|
|
2259
2270
|
this.attachWebSocket(this.ticketId, this.onMessageHandler, () => {
|
|
2260
2271
|
opened = true;
|
|
@@ -2270,31 +2281,51 @@ class Connection {
|
|
|
2270
2281
|
}
|
|
2271
2282
|
reject(new Error("Failed to reconnect to ticket server."));
|
|
2272
2283
|
});
|
|
2273
|
-
}).finally(() => {
|
|
2274
|
-
this.reconnectPromise = null;
|
|
2275
2284
|
});
|
|
2276
|
-
return this.reconnectPromise;
|
|
2277
2285
|
}
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
return;
|
|
2281
|
-
}
|
|
2282
|
-
return this.reconnect();
|
|
2286
|
+
websocketUrl(ticketId) {
|
|
2287
|
+
return `${this.network === "local" ? "ws" : "wss"}://${this.apiUrl.replace("https://", "").replace("http://", "")}/api/v1/.connect/pair/ws/${encodeURIComponent(ticketId)}`;
|
|
2283
2288
|
}
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2289
|
+
attachWebSocket(ticketId, onMessage, onOpen, onError, onClose) {
|
|
2290
|
+
const wsUrl = this.websocketUrl(ticketId);
|
|
2291
|
+
const ws = new WebSocket(wsUrl);
|
|
2292
|
+
ws.onmessage = onMessage;
|
|
2293
|
+
ws.onopen = () => {
|
|
2294
|
+
this.status = "connected";
|
|
2295
|
+
console.log("[LoopSDK] Connected to ticket server.");
|
|
2296
|
+
onOpen?.();
|
|
2297
|
+
};
|
|
2298
|
+
ws.onclose = (event) => {
|
|
2299
|
+
this.status = "disconnected";
|
|
2300
|
+
if (this.ws === ws) {
|
|
2301
|
+
this.ws = null;
|
|
2302
|
+
}
|
|
2303
|
+
console.log("[LoopSDK] Disconnected from ticket server.");
|
|
2304
|
+
onClose?.(event);
|
|
2305
|
+
};
|
|
2306
|
+
ws.onerror = (event) => {
|
|
2307
|
+
this.status = "disconnected";
|
|
2308
|
+
ws.close();
|
|
2309
|
+
if (this.ws === ws) {
|
|
2310
|
+
this.ws = null;
|
|
2311
|
+
}
|
|
2312
|
+
onError?.(event);
|
|
2313
|
+
};
|
|
2314
|
+
this.ws = ws;
|
|
2290
2315
|
}
|
|
2291
2316
|
}
|
|
2292
2317
|
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2318
|
+
// src/types.ts
|
|
2319
|
+
var MessageType;
|
|
2320
|
+
((MessageType2) => {
|
|
2321
|
+
MessageType2["HANDSHAKE_ACCEPT"] = "handshake_accept";
|
|
2322
|
+
MessageType2["HANDSHAKE_REJECT"] = "handshake_reject";
|
|
2323
|
+
MessageType2["RUN_TRANSACTION"] = "run_transaction";
|
|
2324
|
+
MessageType2["RUN_TRANSACTION_RESPONSE"] = "run_transaction_response";
|
|
2325
|
+
MessageType2["SIGN_RAW_MESSAGE"] = "sign_raw_message";
|
|
2326
|
+
MessageType2["SIGN_RAW_MESSAGE_RESPONSE"] = "sign_raw_message_response";
|
|
2327
|
+
MessageType2["REJECT_REQUEST"] = "reject_request";
|
|
2328
|
+
})(MessageType ||= {});
|
|
2298
2329
|
|
|
2299
2330
|
// src/provider.ts
|
|
2300
2331
|
var DEFAULT_REQUEST_TIMEOUT_MS = 300000;
|
|
@@ -2403,24 +2434,24 @@ class Provider {
|
|
|
2403
2434
|
}
|
|
2404
2435
|
async ensureConnected() {
|
|
2405
2436
|
if (this.connection.ws && this.connection.ws.readyState === WebSocket.OPEN) {
|
|
2406
|
-
return;
|
|
2437
|
+
return Promise.resolve();
|
|
2407
2438
|
}
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
return;
|
|
2412
|
-
}
|
|
2439
|
+
await this.connection.reconnect();
|
|
2440
|
+
if (this.connection.ws && this.connection.ws.readyState === WebSocket.OPEN) {
|
|
2441
|
+
return;
|
|
2413
2442
|
}
|
|
2414
2443
|
throw new Error("Not connected.");
|
|
2415
2444
|
}
|
|
2416
2445
|
sendRequest(messageType, params = {}, options) {
|
|
2417
2446
|
return new Promise((resolve, reject) => {
|
|
2418
2447
|
const requestId = generateRequestId();
|
|
2419
|
-
|
|
2448
|
+
let requestContext;
|
|
2420
2449
|
const ensure = async () => {
|
|
2421
2450
|
try {
|
|
2422
2451
|
await this.ensureConnected();
|
|
2452
|
+
requestContext = await this.hooks?.onRequestStart?.(messageType, options?.requestLabel);
|
|
2423
2453
|
} catch (error) {
|
|
2454
|
+
console.error("[LoopSDK] error when checking connection status", error);
|
|
2424
2455
|
this.hooks?.onRequestFinish?.({
|
|
2425
2456
|
status: "error",
|
|
2426
2457
|
messageType,
|
|
@@ -2444,7 +2475,13 @@ class Provider {
|
|
|
2444
2475
|
};
|
|
2445
2476
|
}
|
|
2446
2477
|
}
|
|
2447
|
-
|
|
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
|
+
}
|
|
2448
2485
|
const intervalTime = 300;
|
|
2449
2486
|
let elapsedTime = 0;
|
|
2450
2487
|
const timeoutMs = options?.requestTimeout ?? this.requestTimeout;
|
|
@@ -2453,6 +2490,18 @@ class Provider {
|
|
|
2453
2490
|
if (response) {
|
|
2454
2491
|
clearInterval(intervalId);
|
|
2455
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
|
+
}
|
|
2456
2505
|
if (response.type === "reject_request" /* REJECT_REQUEST */) {
|
|
2457
2506
|
this.hooks?.onRequestFinish?.({
|
|
2458
2507
|
status: "rejected",
|
|
@@ -2491,6 +2540,81 @@ class Provider {
|
|
|
2491
2540
|
}
|
|
2492
2541
|
}
|
|
2493
2542
|
|
|
2543
|
+
// src/session.ts
|
|
2544
|
+
var STORAGE_KEY_LOOP_CONNECT = "loop_connect";
|
|
2545
|
+
|
|
2546
|
+
class SessionInfo {
|
|
2547
|
+
sessionId;
|
|
2548
|
+
ticketId;
|
|
2549
|
+
authToken;
|
|
2550
|
+
partyId;
|
|
2551
|
+
publicKey;
|
|
2552
|
+
email;
|
|
2553
|
+
_isAuthorized = false;
|
|
2554
|
+
constructor({ sessionId, ticketId, authToken, partyId, publicKey, email }) {
|
|
2555
|
+
this.sessionId = sessionId;
|
|
2556
|
+
this.ticketId = ticketId;
|
|
2557
|
+
this.authToken = authToken;
|
|
2558
|
+
this.partyId = partyId;
|
|
2559
|
+
this.publicKey = publicKey;
|
|
2560
|
+
this.email = email;
|
|
2561
|
+
}
|
|
2562
|
+
setTicketId(ticketId) {
|
|
2563
|
+
this.ticketId = ticketId;
|
|
2564
|
+
this.save();
|
|
2565
|
+
}
|
|
2566
|
+
authorized() {
|
|
2567
|
+
if (this.ticketId === undefined || this.sessionId === undefined || this.authToken === undefined || this.partyId === undefined || this.publicKey === undefined) {
|
|
2568
|
+
throw new Error("Session cannot be authorized without all required fields.");
|
|
2569
|
+
}
|
|
2570
|
+
this._isAuthorized = true;
|
|
2571
|
+
}
|
|
2572
|
+
isPreAuthorized() {
|
|
2573
|
+
return !this._isAuthorized && this.ticketId !== undefined && this.sessionId !== undefined && this.authToken !== undefined && this.partyId !== undefined && this.publicKey !== undefined;
|
|
2574
|
+
}
|
|
2575
|
+
isAuthorized() {
|
|
2576
|
+
return this._isAuthorized;
|
|
2577
|
+
}
|
|
2578
|
+
save() {
|
|
2579
|
+
localStorage.setItem("loop_connect", this.toJson());
|
|
2580
|
+
}
|
|
2581
|
+
reset() {
|
|
2582
|
+
localStorage.removeItem(STORAGE_KEY_LOOP_CONNECT);
|
|
2583
|
+
this.sessionId = generateRequestId();
|
|
2584
|
+
this._isAuthorized = false;
|
|
2585
|
+
this.ticketId = undefined;
|
|
2586
|
+
this.authToken = undefined;
|
|
2587
|
+
this.partyId = undefined;
|
|
2588
|
+
this.publicKey = undefined;
|
|
2589
|
+
this.email = undefined;
|
|
2590
|
+
}
|
|
2591
|
+
static fromStorage() {
|
|
2592
|
+
const existingConnectionRaw = localStorage.getItem(STORAGE_KEY_LOOP_CONNECT);
|
|
2593
|
+
if (!existingConnectionRaw) {
|
|
2594
|
+
return new SessionInfo({ sessionId: generateRequestId() });
|
|
2595
|
+
}
|
|
2596
|
+
let session = null;
|
|
2597
|
+
try {
|
|
2598
|
+
session = new SessionInfo(JSON.parse(existingConnectionRaw));
|
|
2599
|
+
} catch (error) {
|
|
2600
|
+
console.error("Failed to parse existing connection info, local storage is corrupted.", error);
|
|
2601
|
+
localStorage.removeItem(STORAGE_KEY_LOOP_CONNECT);
|
|
2602
|
+
session = new SessionInfo({ sessionId: generateRequestId() });
|
|
2603
|
+
}
|
|
2604
|
+
return session;
|
|
2605
|
+
}
|
|
2606
|
+
toJson() {
|
|
2607
|
+
return JSON.stringify({
|
|
2608
|
+
sessionId: this.sessionId,
|
|
2609
|
+
ticketId: this.ticketId,
|
|
2610
|
+
authToken: this.authToken,
|
|
2611
|
+
partyId: this.partyId,
|
|
2612
|
+
publicKey: this.publicKey,
|
|
2613
|
+
email: this.email
|
|
2614
|
+
});
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2494
2618
|
// src/extensions/usdc/index.ts
|
|
2495
2619
|
class UsdcBridge {
|
|
2496
2620
|
getProvider;
|
|
@@ -2570,9 +2694,10 @@ class LoopWallet {
|
|
|
2570
2694
|
|
|
2571
2695
|
// src/index.ts
|
|
2572
2696
|
class LoopSDK {
|
|
2573
|
-
version = "0.
|
|
2697
|
+
version = "0.7.3";
|
|
2574
2698
|
appName = "Unknown";
|
|
2575
2699
|
connection = null;
|
|
2700
|
+
session = null;
|
|
2576
2701
|
provider = null;
|
|
2577
2702
|
openMode = "popup";
|
|
2578
2703
|
requestSigningMode = "popup";
|
|
@@ -2581,7 +2706,6 @@ class LoopSDK {
|
|
|
2581
2706
|
onAccept = null;
|
|
2582
2707
|
onReject = null;
|
|
2583
2708
|
overlay = null;
|
|
2584
|
-
ticketId = null;
|
|
2585
2709
|
wallet;
|
|
2586
2710
|
constructor() {
|
|
2587
2711
|
this.wallet = new LoopWallet(() => this.provider);
|
|
@@ -2595,6 +2719,9 @@ class LoopSDK {
|
|
|
2595
2719
|
onReject,
|
|
2596
2720
|
options
|
|
2597
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
|
+
}
|
|
2598
2725
|
this.appName = appName;
|
|
2599
2726
|
this.onAccept = onAccept || null;
|
|
2600
2727
|
this.onReject = onReject || null;
|
|
@@ -2609,71 +2736,70 @@ class LoopSDK {
|
|
|
2609
2736
|
this.redirectUrl = resolvedOptions.redirectUrl;
|
|
2610
2737
|
this.connection = new Connection({ network, walletUrl, apiUrl });
|
|
2611
2738
|
}
|
|
2612
|
-
async
|
|
2613
|
-
if (
|
|
2614
|
-
|
|
2739
|
+
async loadSessionInfo() {
|
|
2740
|
+
if (this.session) {
|
|
2741
|
+
return;
|
|
2742
|
+
}
|
|
2743
|
+
this.session = SessionInfo.fromStorage();
|
|
2744
|
+
if (!this.session.isPreAuthorized()) {
|
|
2615
2745
|
return;
|
|
2616
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() {
|
|
2617
2766
|
if (!this.connection) {
|
|
2618
2767
|
throw new Error("SDK not initialized. Call init() first.");
|
|
2619
2768
|
}
|
|
2620
|
-
|
|
2621
|
-
if (
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
console.error("Auto-login failed, token is invalid. Starting new connection.", err);
|
|
2650
|
-
canReuseTicket = false;
|
|
2651
|
-
localStorage.removeItem("loop_connect");
|
|
2652
|
-
}
|
|
2653
|
-
}
|
|
2654
|
-
if (ticketId && canReuseTicket) {
|
|
2655
|
-
this.ticketId = ticketId;
|
|
2656
|
-
const url = new URL("/.connect/", this.connection.walletUrl);
|
|
2657
|
-
url.searchParams.set("ticketId", ticketId);
|
|
2658
|
-
if (this.redirectUrl) {
|
|
2659
|
-
url.searchParams.set("redirectUrl", this.redirectUrl);
|
|
2660
|
-
}
|
|
2661
|
-
const connectUrl = url.toString();
|
|
2662
|
-
this.showQrCode(connectUrl);
|
|
2663
|
-
this.connection.connectWebSocket(ticketId, this.handleWebSocketMessage.bind(this));
|
|
2664
|
-
return;
|
|
2665
|
-
}
|
|
2666
|
-
} catch (error) {
|
|
2667
|
-
console.error("Failed to parse existing connection info, creating a new one.", error);
|
|
2668
|
-
}
|
|
2669
|
-
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.session?.ticketId) {
|
|
2793
|
+
this.showQrCode(this.buildConnectUrl(this.session.ticketId));
|
|
2794
|
+
return;
|
|
2795
|
+
}
|
|
2796
|
+
if (this.session && this.session.isAuthorized()) {
|
|
2797
|
+
return;
|
|
2670
2798
|
}
|
|
2671
|
-
const sessionId = generateRequestId();
|
|
2672
2799
|
try {
|
|
2673
|
-
const { ticket_id: ticketId } = await this.connection.getTicket(this.appName, sessionId, this.version);
|
|
2674
|
-
this.ticketId
|
|
2675
|
-
|
|
2676
|
-
const connectUrl = this.buildConnectUrl(ticketId);
|
|
2800
|
+
const { ticket_id: ticketId } = await this.connection.getTicket(this.appName, this.session.sessionId, this.version);
|
|
2801
|
+
this.session.setTicketId(ticketId);
|
|
2802
|
+
const connectUrl = this.buildConnectUrl(this.session.ticketId);
|
|
2677
2803
|
this.showQrCode(connectUrl);
|
|
2678
2804
|
this.connection.connectWebSocket(ticketId, this.handleWebSocketMessage.bind(this));
|
|
2679
2805
|
} catch (error) {
|
|
@@ -2683,6 +2809,14 @@ class LoopSDK {
|
|
|
2683
2809
|
}
|
|
2684
2810
|
handleWebSocketMessage(event) {
|
|
2685
2811
|
const message = JSON.parse(event.data);
|
|
2812
|
+
const errCode = extractErrorCode(message);
|
|
2813
|
+
if (isUnauthCode(errCode)) {
|
|
2814
|
+
console.warn("[LoopSDK] Detected session invalidation:", errCode, {
|
|
2815
|
+
message
|
|
2816
|
+
});
|
|
2817
|
+
this.logout();
|
|
2818
|
+
return;
|
|
2819
|
+
}
|
|
2686
2820
|
console.log("[LoopSDK] WS message received:", message);
|
|
2687
2821
|
if (message.type === "handshake_accept" /* HANDSHAKE_ACCEPT */) {
|
|
2688
2822
|
console.log("[LoopSDK] Entering HANDSHAKE_ACCEPT flow");
|
|
@@ -2696,32 +2830,27 @@ class LoopSDK {
|
|
|
2696
2830
|
email,
|
|
2697
2831
|
hooks: this.createProviderHooks()
|
|
2698
2832
|
});
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
console.log("[LoopSDK] HANDSHAKE_ACCEPT: closing popup (if exists)");
|
|
2713
|
-
this.popupWindow = null;
|
|
2714
|
-
} catch (error) {
|
|
2715
|
-
console.error("Failed to update local storage with auth token.", error);
|
|
2716
|
-
}
|
|
2833
|
+
try {
|
|
2834
|
+
this.session.authToken = authToken;
|
|
2835
|
+
this.session.partyId = partyId;
|
|
2836
|
+
this.session.publicKey = publicKey;
|
|
2837
|
+
this.session.email = email;
|
|
2838
|
+
this.session.authorized();
|
|
2839
|
+
this.session.save();
|
|
2840
|
+
this.onAccept?.(this.provider);
|
|
2841
|
+
this.hideQrCode();
|
|
2842
|
+
console.log("[LoopSDK] HANDSHAKE_ACCEPT: closing popup (if exists)");
|
|
2843
|
+
this.popupWindow = null;
|
|
2844
|
+
} catch (error) {
|
|
2845
|
+
console.error("Failed to update local storage with auth token.", error);
|
|
2717
2846
|
}
|
|
2718
2847
|
}
|
|
2719
2848
|
} else if (message.type === "handshake_reject" /* HANDSHAKE_REJECT */) {
|
|
2720
2849
|
console.log("[LoopSDK] Entering HANDSHAKE_REJECT flow");
|
|
2721
|
-
localStorage.removeItem("loop_connect");
|
|
2722
2850
|
this.connection?.ws?.close();
|
|
2723
2851
|
this.onReject?.();
|
|
2724
2852
|
this.hideQrCode();
|
|
2853
|
+
this.session?.reset();
|
|
2725
2854
|
console.log("[LoopSDK] HANDSHAKE_REJECT: closing popup (if exists)");
|
|
2726
2855
|
this.popupWindow = null;
|
|
2727
2856
|
} else if (this.provider) {
|
|
@@ -2746,7 +2875,7 @@ class LoopSDK {
|
|
|
2746
2875
|
if (typeof window === "undefined") {
|
|
2747
2876
|
return null;
|
|
2748
2877
|
}
|
|
2749
|
-
if (!this.ticketId) {
|
|
2878
|
+
if (!this.session?.ticketId) {
|
|
2750
2879
|
console.warn("[LoopSDK] Cannot open wallet UI for request: no active ticket.");
|
|
2751
2880
|
return null;
|
|
2752
2881
|
}
|
|
@@ -2790,10 +2919,108 @@ class LoopSDK {
|
|
|
2790
2919
|
}
|
|
2791
2920
|
return window.open(url, "_blank", "noopener,noreferrer");
|
|
2792
2921
|
}
|
|
2793
|
-
|
|
2794
|
-
if (
|
|
2922
|
+
injectModalStyles() {
|
|
2923
|
+
if (document.getElementById("loop-connect-styles"))
|
|
2795
2924
|
return;
|
|
2796
|
-
|
|
2925
|
+
const style = document.createElement("style");
|
|
2926
|
+
style.id = "loop-connect-styles";
|
|
2927
|
+
style.textContent = `
|
|
2928
|
+
.loop-connect {
|
|
2929
|
+
position: fixed;
|
|
2930
|
+
inset: 0;
|
|
2931
|
+
background: oklch(0.222 0 0 / 0.85);
|
|
2932
|
+
backdrop-filter: blur(8px);
|
|
2933
|
+
display: flex;
|
|
2934
|
+
justify-content: center;
|
|
2935
|
+
align-items: center;
|
|
2936
|
+
z-index: 10000;
|
|
2937
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
2938
|
+
animation: fadeIn 0.2s ease-out;
|
|
2939
|
+
}
|
|
2940
|
+
.loop-connect dialog {
|
|
2941
|
+
position: relative;
|
|
2942
|
+
overflow: hidden;
|
|
2943
|
+
background: oklch(0.253 0.008 274.6);
|
|
2944
|
+
box-shadow: 0 4px 24px oklch(0 0 0 / 0.1);
|
|
2945
|
+
border: 1px solid oklch(0.41 0.01 278.4);
|
|
2946
|
+
border-radius: 32px;
|
|
2947
|
+
padding: 24px;
|
|
2948
|
+
display: flex;
|
|
2949
|
+
flex-direction: column;
|
|
2950
|
+
align-items: center;
|
|
2951
|
+
gap: 16px;
|
|
2952
|
+
color: oklch(0.975 0.005 280);
|
|
2953
|
+
}
|
|
2954
|
+
.loop-connect .bg-logo {
|
|
2955
|
+
position: absolute;
|
|
2956
|
+
right: -20px;
|
|
2957
|
+
top: -40px;
|
|
2958
|
+
width: 140px;
|
|
2959
|
+
height: auto;
|
|
2960
|
+
opacity: 0.06;
|
|
2961
|
+
pointer-events: none;
|
|
2962
|
+
}
|
|
2963
|
+
.loop-connect h3 {
|
|
2964
|
+
margin: 0;
|
|
2965
|
+
font-size: 18px;
|
|
2966
|
+
font-weight: 600;
|
|
2967
|
+
letter-spacing: -0.015em;
|
|
2968
|
+
}
|
|
2969
|
+
.loop-connect figure {
|
|
2970
|
+
margin: 0;
|
|
2971
|
+
background: oklch(1 0 0);
|
|
2972
|
+
padding: 8px;
|
|
2973
|
+
border-radius: 24px;
|
|
2974
|
+
display: flex;
|
|
2975
|
+
justify-content: center;
|
|
2976
|
+
border: 2px solid oklch(0.41 0.01 278.4);
|
|
2977
|
+
box-shadow: 0 4px 24px oklch(0 0 0 / 0.1);
|
|
2978
|
+
}
|
|
2979
|
+
.loop-connect img {
|
|
2980
|
+
display: block;
|
|
2981
|
+
width: 225px;
|
|
2982
|
+
height: 225px;
|
|
2983
|
+
}
|
|
2984
|
+
.loop-connect .divider {
|
|
2985
|
+
width: 100%;
|
|
2986
|
+
display: flex;
|
|
2987
|
+
align-items: center;
|
|
2988
|
+
gap: 16px;
|
|
2989
|
+
color: oklch(0.554 0.012 280.3);
|
|
2990
|
+
font-size: 13px;
|
|
2991
|
+
font-weight: 600;
|
|
2992
|
+
}
|
|
2993
|
+
.loop-connect .divider::before,
|
|
2994
|
+
.loop-connect .divider::after {
|
|
2995
|
+
content: "";
|
|
2996
|
+
flex: 1;
|
|
2997
|
+
height: 1px;
|
|
2998
|
+
background: oklch(0.45 0.01 278);
|
|
2999
|
+
}
|
|
3000
|
+
.loop-connect button {
|
|
3001
|
+
background: oklch(0.976 0.101 112.3);
|
|
3002
|
+
border: 1px solid oklch(0.82 0.16 110);
|
|
3003
|
+
color: oklch(0.222 0 0);
|
|
3004
|
+
padding: 16px 32px;
|
|
3005
|
+
border-radius: 24px;
|
|
3006
|
+
font-size: 15px;
|
|
3007
|
+
font-weight: 600;
|
|
3008
|
+
cursor: pointer;
|
|
3009
|
+
transition: all 0.2s ease;
|
|
3010
|
+
width: 100%;
|
|
3011
|
+
}
|
|
3012
|
+
.loop-connect button:hover {
|
|
3013
|
+
background: oklch(0.98 0.105 112.5);
|
|
3014
|
+
}
|
|
3015
|
+
@keyframes fadeIn {
|
|
3016
|
+
from { opacity: 0; }
|
|
3017
|
+
to { opacity: 1; }
|
|
3018
|
+
}
|
|
3019
|
+
`;
|
|
3020
|
+
document.head.appendChild(style);
|
|
3021
|
+
}
|
|
3022
|
+
showQrCode(url) {
|
|
3023
|
+
this.injectModalStyles();
|
|
2797
3024
|
import_qrcode.default.toDataURL(url, (err, dataUrl) => {
|
|
2798
3025
|
if (err) {
|
|
2799
3026
|
console.error("Failed to generate QR code", err);
|
|
@@ -2801,42 +3028,53 @@ class LoopSDK {
|
|
|
2801
3028
|
}
|
|
2802
3029
|
const overlay = document.createElement("div");
|
|
2803
3030
|
overlay.id = "loop-sdk-connect-overlay";
|
|
2804
|
-
overlay.className = "loop-sdk-connect-overlay";
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
3031
|
+
overlay.className = "loop-sdk-connect-overlay loop-connect";
|
|
3032
|
+
const dialog = document.createElement("dialog");
|
|
3033
|
+
dialog.open = true;
|
|
3034
|
+
const bgLogo = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
3035
|
+
bgLogo.setAttribute("class", "bg-logo");
|
|
3036
|
+
bgLogo.setAttribute("viewBox", "0 0 124.05 305.64");
|
|
3037
|
+
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
3038
|
+
path.setAttribute("d", "M24.58,99.47L124.05,0v224.42L24.58,124.95c-7.04-7.04-7.04-18.45,0-25.49Z");
|
|
3039
|
+
path.setAttribute("fill", "currentColor");
|
|
3040
|
+
const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
|
3041
|
+
rect.setAttribute("x", "12.89");
|
|
3042
|
+
rect.setAttribute("y", "194.48");
|
|
3043
|
+
rect.setAttribute("width", "98.27");
|
|
3044
|
+
rect.setAttribute("height", "98.27");
|
|
3045
|
+
rect.setAttribute("rx", "18.02");
|
|
3046
|
+
rect.setAttribute("ry", "18.02");
|
|
3047
|
+
rect.setAttribute("transform", "translate(-154.1 115.21) rotate(-45)");
|
|
3048
|
+
rect.setAttribute("fill", "currentColor");
|
|
3049
|
+
bgLogo.appendChild(path);
|
|
3050
|
+
bgLogo.appendChild(rect);
|
|
3051
|
+
const title = document.createElement("h3");
|
|
3052
|
+
title.textContent = "Scan with Phone";
|
|
3053
|
+
const figure = document.createElement("figure");
|
|
2821
3054
|
const img = document.createElement("img");
|
|
2822
3055
|
img.src = dataUrl;
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
3056
|
+
img.alt = "QR Code";
|
|
3057
|
+
figure.appendChild(img);
|
|
3058
|
+
const divider = document.createElement("div");
|
|
3059
|
+
divider.className = "divider";
|
|
3060
|
+
divider.textContent = "OR";
|
|
3061
|
+
const button = document.createElement("button");
|
|
3062
|
+
button.type = "button";
|
|
3063
|
+
button.textContent = "Continue in Browser";
|
|
3064
|
+
button.addEventListener("click", () => {
|
|
2831
3065
|
this.openWallet(url);
|
|
2832
|
-
};
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
3066
|
+
});
|
|
3067
|
+
dialog.appendChild(bgLogo);
|
|
3068
|
+
dialog.appendChild(title);
|
|
3069
|
+
dialog.appendChild(figure);
|
|
3070
|
+
dialog.appendChild(divider);
|
|
3071
|
+
dialog.appendChild(button);
|
|
3072
|
+
overlay.appendChild(dialog);
|
|
3073
|
+
overlay.addEventListener("click", (e) => {
|
|
2836
3074
|
if (e.target === overlay) {
|
|
2837
3075
|
this.hideQrCode();
|
|
2838
3076
|
}
|
|
2839
|
-
};
|
|
3077
|
+
});
|
|
2840
3078
|
document.body.appendChild(overlay);
|
|
2841
3079
|
this.overlay = overlay;
|
|
2842
3080
|
});
|
|
@@ -2847,6 +3085,12 @@ class LoopSDK {
|
|
|
2847
3085
|
this.overlay = null;
|
|
2848
3086
|
}
|
|
2849
3087
|
}
|
|
3088
|
+
logout() {
|
|
3089
|
+
this.session?.reset();
|
|
3090
|
+
this.provider = null;
|
|
3091
|
+
this.connection?.ws?.close();
|
|
3092
|
+
this.hideQrCode();
|
|
3093
|
+
}
|
|
2850
3094
|
requireProvider() {
|
|
2851
3095
|
if (!this.provider) {
|
|
2852
3096
|
throw new Error("SDK not connected. Call connect() and wait for acceptance first.");
|
|
@@ -2855,9 +3099,7 @@ class LoopSDK {
|
|
|
2855
3099
|
}
|
|
2856
3100
|
createProviderHooks() {
|
|
2857
3101
|
return {
|
|
2858
|
-
onRequestStart: () =>
|
|
2859
|
-
return this.openRequestUi();
|
|
2860
|
-
},
|
|
3102
|
+
onRequestStart: () => this.openRequestUi(),
|
|
2861
3103
|
onRequestFinish: ({ requestContext }) => {
|
|
2862
3104
|
const win = requestContext;
|
|
2863
3105
|
if (win) {
|
package/package.json
CHANGED
|
@@ -1,31 +1,31 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
2
|
+
"name": "@fivenorth/loop-sdk",
|
|
3
|
+
"version": "0.7.4",
|
|
4
|
+
"author": "hello@fivenorth.io",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.js",
|
|
7
|
+
"devDependencies": {
|
|
8
|
+
"@types/bun": "latest"
|
|
9
|
+
},
|
|
10
|
+
"peerDependencies": {
|
|
11
|
+
"typescript": "^5"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"repository": "github:fivenorth-io/loop-sdk",
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "bun build ./src/index.ts --outdir ./dist",
|
|
22
|
+
"prepublishOnly": "bun run build",
|
|
23
|
+
"start": "bun run src/server.ts"
|
|
24
|
+
},
|
|
25
|
+
"type": "module",
|
|
26
|
+
"types": "dist/index.d.ts",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@types/qrcode": "^1.5.6",
|
|
29
|
+
"qrcode": "^1.5.4"
|
|
30
|
+
}
|
|
31
31
|
}
|