@fivenorth/loop-sdk 0.6.5 → 0.7.1
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 +27 -1
- package/dist/index.js +288 -48
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -44,6 +44,7 @@ loop.init({
|
|
|
44
44
|
network: 'local', // or 'devnet', 'mainnet'
|
|
45
45
|
options: {
|
|
46
46
|
openMode: 'popup', // 'popup' (default) or 'tab'
|
|
47
|
+
requestSigningMode: 'popup', // 'popup' (default) or 'tab'
|
|
47
48
|
redirectUrl: 'https://myapp.com/after-connect', // optional redirect after approval
|
|
48
49
|
},
|
|
49
50
|
onAccept: (provider) => {
|
|
@@ -61,6 +62,7 @@ The `init` method takes a configuration object with the following properties:
|
|
|
61
62
|
- `network`: The network to connect to. Can be `local`, `devnet`, or `mainnet`.
|
|
62
63
|
- `options`: Optional object containing:
|
|
63
64
|
- `openMode`: Controls how Loop opens: `'popup'` (default) or `'tab'`.
|
|
65
|
+
- `requestSigningMode`: Controls how signing/transaction requests open the wallet UI after you're connected: `'popup'` (default) or `'tab'`.
|
|
64
66
|
- `redirectUrl`: Optional redirect URL the wallet will navigate back to after successful approval. If omitted, user stays on Loop dashboard.
|
|
65
67
|
- `onAccept`: A callback function that is called when the user accepts the connection. It receives a `provider` object.
|
|
66
68
|
- `onReject`: A callback function that is called when the user rejects the connection.
|
|
@@ -74,6 +76,7 @@ loop.connect();
|
|
|
74
76
|
```
|
|
75
77
|
|
|
76
78
|
This will open a modal with a QR code for the user to scan with their Loop wallet.
|
|
79
|
+
If you set `requestSigningMode` to `'popup'` (or `'tab'`), each signing/transaction request will also open the wallet dashboard and auto-close the popup once the wallet responds.
|
|
77
80
|
|
|
78
81
|
### 3. Using the Provider
|
|
79
82
|
|
|
@@ -132,7 +135,10 @@ const damlCommand = {
|
|
|
132
135
|
};
|
|
133
136
|
|
|
134
137
|
try {
|
|
135
|
-
const result = await provider.submitTransaction(damlCommand
|
|
138
|
+
const result = await provider.submitTransaction(damlCommand, {
|
|
139
|
+
// Optional: show a custom message in the wallet prompt
|
|
140
|
+
message: 'Transfer 10 CC to RetailStore',
|
|
141
|
+
});
|
|
136
142
|
console.log('Transaction successful:', result);
|
|
137
143
|
} catch (error) {
|
|
138
144
|
console.error('Transaction failed:', error);
|
|
@@ -165,6 +171,8 @@ await loop.wallet.transfer(
|
|
|
165
171
|
instrument_id: 'Amulet', // optional
|
|
166
172
|
},
|
|
167
173
|
{
|
|
174
|
+
// Optional: show a custom message in the wallet prompt
|
|
175
|
+
message: 'Send 5 CC to Alice',
|
|
168
176
|
requestedAt: new Date().toISOString(), // optional
|
|
169
177
|
executeBefore: new Date(Date.now() + 24*60*60*1000).toISOString(), // optional
|
|
170
178
|
requestTimeout: 5 * 60 * 1000, // optional (ms), defaults to 5 minutes
|
|
@@ -184,6 +192,24 @@ Common instrument overrides (pass into the `instrument` argument above):
|
|
|
184
192
|
|
|
185
193
|
Swap in the admin/id for the specific instrument you hold in the Loop wallet.
|
|
186
194
|
|
|
195
|
+
#### USDC withdraw helper
|
|
196
|
+
|
|
197
|
+
```javascript
|
|
198
|
+
await loop.wallet.extension.usdcBridge.withdrawalUSDCxToEthereum(
|
|
199
|
+
'0xYourEthAddress',
|
|
200
|
+
'10.5', // amount in USDCx
|
|
201
|
+
{
|
|
202
|
+
reference: 'optional memo',
|
|
203
|
+
message: 'Withdraw 10.5 USDCx to 0xabc', // optional custom prompt text
|
|
204
|
+
requestTimeout: 5 * 60 * 1000, // optional override (ms)
|
|
205
|
+
},
|
|
206
|
+
);
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Notes:
|
|
210
|
+
- Uses the connect-based withdraw endpoint to prepare the transaction and sends it over Wallet Connect.
|
|
211
|
+
- The helper auto-reconnects the websocket if it was closed before sending the request.
|
|
212
|
+
|
|
187
213
|
# API
|
|
188
214
|
|
|
189
215
|
Coming soon
|
package/dist/index.js
CHANGED
|
@@ -2073,6 +2073,9 @@ class Connection {
|
|
|
2073
2073
|
apiUrl = "https://cantonloop.com";
|
|
2074
2074
|
ws = null;
|
|
2075
2075
|
network = "main";
|
|
2076
|
+
ticketId = null;
|
|
2077
|
+
onMessageHandler = null;
|
|
2078
|
+
reconnectPromise = null;
|
|
2076
2079
|
constructor({ network, walletUrl, apiUrl }) {
|
|
2077
2080
|
this.network = network || "main";
|
|
2078
2081
|
switch (this.network) {
|
|
@@ -2213,16 +2216,67 @@ class Connection {
|
|
|
2213
2216
|
websocketUrl(ticketId) {
|
|
2214
2217
|
return `${this.network === "local" ? "ws" : "wss"}://${this.apiUrl.replace("https://", "").replace("http://", "")}/api/v1/.connect/pair/ws/${ticketId}`;
|
|
2215
2218
|
}
|
|
2216
|
-
|
|
2219
|
+
attachWebSocket(ticketId, onMessage, onOpen, onError, onClose) {
|
|
2217
2220
|
const wsUrl = this.websocketUrl(ticketId);
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
+
const ws = new WebSocket(wsUrl);
|
|
2222
|
+
ws.onmessage = onMessage;
|
|
2223
|
+
ws.onopen = () => {
|
|
2221
2224
|
console.log("Connected to ticket server.");
|
|
2225
|
+
onOpen?.();
|
|
2222
2226
|
};
|
|
2223
|
-
|
|
2227
|
+
ws.onclose = (event) => {
|
|
2228
|
+
if (this.ws === ws) {
|
|
2229
|
+
this.ws = null;
|
|
2230
|
+
}
|
|
2224
2231
|
console.log("Disconnected from ticket server.");
|
|
2232
|
+
onClose?.(event);
|
|
2225
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
|
+
connectWebSocket(ticketId, onMessage) {
|
|
2243
|
+
this.ticketId = ticketId;
|
|
2244
|
+
this.onMessageHandler = onMessage;
|
|
2245
|
+
this.attachWebSocket(ticketId, onMessage);
|
|
2246
|
+
}
|
|
2247
|
+
reconnect() {
|
|
2248
|
+
if (!this.ticketId || !this.onMessageHandler) {
|
|
2249
|
+
return Promise.reject(new Error("Cannot reconnect without a known ticket."));
|
|
2250
|
+
}
|
|
2251
|
+
if (this.reconnectPromise) {
|
|
2252
|
+
return this.reconnectPromise;
|
|
2253
|
+
}
|
|
2254
|
+
this.reconnectPromise = new Promise((resolve, reject) => {
|
|
2255
|
+
let opened = false;
|
|
2256
|
+
this.attachWebSocket(this.ticketId, this.onMessageHandler, () => {
|
|
2257
|
+
opened = true;
|
|
2258
|
+
resolve();
|
|
2259
|
+
}, () => {
|
|
2260
|
+
if (opened) {
|
|
2261
|
+
return;
|
|
2262
|
+
}
|
|
2263
|
+
reject(new Error("Failed to reconnect to ticket server."));
|
|
2264
|
+
}, () => {
|
|
2265
|
+
if (opened) {
|
|
2266
|
+
return;
|
|
2267
|
+
}
|
|
2268
|
+
reject(new Error("Failed to reconnect to ticket server."));
|
|
2269
|
+
});
|
|
2270
|
+
}).finally(() => {
|
|
2271
|
+
this.reconnectPromise = null;
|
|
2272
|
+
});
|
|
2273
|
+
return this.reconnectPromise;
|
|
2274
|
+
}
|
|
2275
|
+
async reconnectWebSocket() {
|
|
2276
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
2277
|
+
return;
|
|
2278
|
+
}
|
|
2279
|
+
return this.reconnect();
|
|
2226
2280
|
}
|
|
2227
2281
|
}
|
|
2228
2282
|
|
|
@@ -2269,7 +2323,9 @@ class Provider {
|
|
|
2269
2323
|
email;
|
|
2270
2324
|
auth_token;
|
|
2271
2325
|
requests = new Map;
|
|
2272
|
-
|
|
2326
|
+
requestTimeout = DEFAULT_REQUEST_TIMEOUT_MS;
|
|
2327
|
+
hooks;
|
|
2328
|
+
constructor({ connection, party_id, public_key, auth_token, email, hooks }) {
|
|
2273
2329
|
if (!connection) {
|
|
2274
2330
|
throw new Error("Provider requires a connection object.");
|
|
2275
2331
|
}
|
|
@@ -2278,6 +2334,10 @@ class Provider {
|
|
|
2278
2334
|
this.public_key = public_key;
|
|
2279
2335
|
this.email = email;
|
|
2280
2336
|
this.auth_token = auth_token;
|
|
2337
|
+
this.hooks = hooks;
|
|
2338
|
+
}
|
|
2339
|
+
getAuthToken() {
|
|
2340
|
+
return this.auth_token;
|
|
2281
2341
|
}
|
|
2282
2342
|
handleResponse(message) {
|
|
2283
2343
|
console.log("Received response:", message);
|
|
@@ -2297,6 +2357,7 @@ class Provider {
|
|
|
2297
2357
|
async transfer(recipient, amount, instrument, options) {
|
|
2298
2358
|
const amountStr = typeof amount === "number" ? amount.toString() : amount;
|
|
2299
2359
|
const { requestedAt, executeBefore, requestTimeout } = options || {};
|
|
2360
|
+
const message = options?.message;
|
|
2300
2361
|
const resolveDate = (value, fallbackMs) => {
|
|
2301
2362
|
if (value instanceof Date) {
|
|
2302
2363
|
return value.toISOString();
|
|
@@ -2329,53 +2390,164 @@ class Provider {
|
|
|
2329
2390
|
actAs: preparedPayload.actAs,
|
|
2330
2391
|
readAs: preparedPayload.readAs,
|
|
2331
2392
|
synchronizerId: preparedPayload.synchronizerId
|
|
2332
|
-
}, { requestTimeout });
|
|
2393
|
+
}, { requestTimeout, message });
|
|
2333
2394
|
}
|
|
2334
2395
|
async signMessage(message) {
|
|
2335
2396
|
return this.sendRequest("sign_raw_message" /* SIGN_RAW_MESSAGE */, message);
|
|
2336
2397
|
}
|
|
2398
|
+
async ensureConnected() {
|
|
2399
|
+
if (this.connection.ws && this.connection.ws.readyState === WebSocket.OPEN) {
|
|
2400
|
+
return;
|
|
2401
|
+
}
|
|
2402
|
+
if (typeof this.connection.reconnectWebSocket === "function") {
|
|
2403
|
+
await this.connection.reconnectWebSocket();
|
|
2404
|
+
if (this.connection.ws && this.connection.ws.readyState === WebSocket.OPEN) {
|
|
2405
|
+
return;
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
throw new Error("Not connected.");
|
|
2409
|
+
}
|
|
2337
2410
|
sendRequest(messageType, params = {}, options) {
|
|
2338
2411
|
return new Promise((resolve, reject) => {
|
|
2339
|
-
if (!this.connection.ws || this.connection.ws.readyState !== WebSocket.OPEN) {
|
|
2340
|
-
return reject(new Error("Not connected."));
|
|
2341
|
-
}
|
|
2342
2412
|
const requestId = generateRequestId();
|
|
2343
|
-
const
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2413
|
+
const requestContext = this.hooks?.onRequestStart?.(messageType, options?.requestLabel);
|
|
2414
|
+
const ensure = async () => {
|
|
2415
|
+
try {
|
|
2416
|
+
await this.ensureConnected();
|
|
2417
|
+
} catch (error) {
|
|
2418
|
+
this.hooks?.onRequestFinish?.({
|
|
2419
|
+
status: "error",
|
|
2420
|
+
messageType,
|
|
2421
|
+
requestLabel: options?.requestLabel,
|
|
2422
|
+
requestContext
|
|
2423
|
+
});
|
|
2424
|
+
reject(error);
|
|
2425
|
+
return;
|
|
2426
|
+
}
|
|
2427
|
+
const requestBody = {
|
|
2428
|
+
request_id: requestId,
|
|
2429
|
+
type: messageType,
|
|
2430
|
+
payload: params
|
|
2431
|
+
};
|
|
2432
|
+
if (options?.message) {
|
|
2433
|
+
requestBody.ticket = { message: options.message };
|
|
2434
|
+
if (typeof params === "object" && params !== null && !Array.isArray(params)) {
|
|
2435
|
+
requestBody.payload = {
|
|
2436
|
+
...params,
|
|
2437
|
+
ticket: { message: options.message }
|
|
2438
|
+
};
|
|
2360
2439
|
}
|
|
2361
|
-
}
|
|
2362
|
-
|
|
2363
|
-
|
|
2440
|
+
}
|
|
2441
|
+
this.connection.ws.send(JSON.stringify(requestBody));
|
|
2442
|
+
const intervalTime = 300;
|
|
2443
|
+
let elapsedTime = 0;
|
|
2444
|
+
const timeoutMs = options?.requestTimeout ?? this.requestTimeout;
|
|
2445
|
+
const intervalId = setInterval(() => {
|
|
2446
|
+
const response = this.requests.get(requestId);
|
|
2447
|
+
if (response) {
|
|
2364
2448
|
clearInterval(intervalId);
|
|
2365
2449
|
this.requests.delete(requestId);
|
|
2366
|
-
|
|
2450
|
+
if (response.type === "reject_request" /* REJECT_REQUEST */) {
|
|
2451
|
+
this.hooks?.onRequestFinish?.({
|
|
2452
|
+
status: "rejected",
|
|
2453
|
+
messageType,
|
|
2454
|
+
requestLabel: options?.requestLabel,
|
|
2455
|
+
requestContext
|
|
2456
|
+
});
|
|
2457
|
+
reject(new RejectRequestError);
|
|
2458
|
+
} else {
|
|
2459
|
+
this.hooks?.onRequestFinish?.({
|
|
2460
|
+
status: "success",
|
|
2461
|
+
messageType,
|
|
2462
|
+
requestLabel: options?.requestLabel,
|
|
2463
|
+
requestContext
|
|
2464
|
+
});
|
|
2465
|
+
resolve(response.payload);
|
|
2466
|
+
}
|
|
2467
|
+
} else {
|
|
2468
|
+
elapsedTime += intervalTime;
|
|
2469
|
+
if (elapsedTime >= timeoutMs) {
|
|
2470
|
+
clearInterval(intervalId);
|
|
2471
|
+
this.requests.delete(requestId);
|
|
2472
|
+
this.hooks?.onRequestFinish?.({
|
|
2473
|
+
status: "timeout",
|
|
2474
|
+
messageType,
|
|
2475
|
+
requestLabel: options?.requestLabel,
|
|
2476
|
+
requestContext
|
|
2477
|
+
});
|
|
2478
|
+
reject(new RequestTimeoutError(timeoutMs));
|
|
2479
|
+
}
|
|
2367
2480
|
}
|
|
2368
|
-
}
|
|
2369
|
-
}
|
|
2481
|
+
}, intervalTime);
|
|
2482
|
+
};
|
|
2483
|
+
ensure();
|
|
2370
2484
|
});
|
|
2371
2485
|
}
|
|
2372
2486
|
}
|
|
2373
2487
|
|
|
2488
|
+
// src/extensions/usdc/index.ts
|
|
2489
|
+
class UsdcBridge {
|
|
2490
|
+
getProvider;
|
|
2491
|
+
constructor(getProvider) {
|
|
2492
|
+
this.getProvider = getProvider;
|
|
2493
|
+
}
|
|
2494
|
+
requireProvider() {
|
|
2495
|
+
const provider = this.getProvider();
|
|
2496
|
+
if (!provider) {
|
|
2497
|
+
throw new Error("SDK not connected. Call connect() and wait for acceptance first.");
|
|
2498
|
+
}
|
|
2499
|
+
return provider;
|
|
2500
|
+
}
|
|
2501
|
+
withdrawalUSDCxToEthereum(recipient, amount, options) {
|
|
2502
|
+
const provider = this.requireProvider();
|
|
2503
|
+
const amountStr = typeof amount === "number" ? amount.toString() : amount;
|
|
2504
|
+
const withdrawRequest = {
|
|
2505
|
+
recipient,
|
|
2506
|
+
amount: amountStr,
|
|
2507
|
+
reference: options?.reference
|
|
2508
|
+
};
|
|
2509
|
+
return prepareUsdcWithdraw(provider.connection, provider.getAuthToken(), withdrawRequest).then((preparedPayload) => provider.submitTransaction({
|
|
2510
|
+
commands: preparedPayload.commands,
|
|
2511
|
+
disclosedContracts: preparedPayload.disclosedContracts,
|
|
2512
|
+
packageIdSelectionPreference: preparedPayload.packageIdSelectionPreference,
|
|
2513
|
+
actAs: preparedPayload.actAs,
|
|
2514
|
+
readAs: preparedPayload.readAs,
|
|
2515
|
+
synchronizerId: preparedPayload.synchronizerId
|
|
2516
|
+
}, { requestTimeout: options?.requestTimeout, message: options?.message }));
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
async function prepareUsdcWithdraw(connection, authToken, params) {
|
|
2520
|
+
const payload = {
|
|
2521
|
+
recipient: params.recipient,
|
|
2522
|
+
amount: params.amount
|
|
2523
|
+
};
|
|
2524
|
+
if (params.reference) {
|
|
2525
|
+
payload.reference = params.reference;
|
|
2526
|
+
}
|
|
2527
|
+
const response = await fetch(`${connection.apiUrl}/api/v1/.connect/pair/usdc/withdraw`, {
|
|
2528
|
+
method: "POST",
|
|
2529
|
+
headers: {
|
|
2530
|
+
"Content-Type": "application/json",
|
|
2531
|
+
Authorization: `Bearer ${authToken}`
|
|
2532
|
+
},
|
|
2533
|
+
body: JSON.stringify(payload)
|
|
2534
|
+
});
|
|
2535
|
+
if (!response.ok) {
|
|
2536
|
+
throw new Error("Failed to prepare USDC withdrawal.");
|
|
2537
|
+
}
|
|
2538
|
+
const data = await response.json();
|
|
2539
|
+
return data.payload;
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2374
2542
|
// src/wallet.ts
|
|
2375
2543
|
class LoopWallet {
|
|
2376
2544
|
getProvider;
|
|
2545
|
+
extension;
|
|
2377
2546
|
constructor(getProvider) {
|
|
2378
2547
|
this.getProvider = getProvider;
|
|
2548
|
+
this.extension = {
|
|
2549
|
+
usdcBridge: new UsdcBridge(this.getProvider)
|
|
2550
|
+
};
|
|
2379
2551
|
}
|
|
2380
2552
|
requireProvider() {
|
|
2381
2553
|
const provider = this.getProvider();
|
|
@@ -2397,6 +2569,7 @@ class LoopSDK {
|
|
|
2397
2569
|
connection = null;
|
|
2398
2570
|
provider = null;
|
|
2399
2571
|
openMode = "popup";
|
|
2572
|
+
requestSigningMode = "popup";
|
|
2400
2573
|
popupWindow = null;
|
|
2401
2574
|
redirectUrl;
|
|
2402
2575
|
onAccept = null;
|
|
@@ -2421,10 +2594,12 @@ class LoopSDK {
|
|
|
2421
2594
|
this.onReject = onReject || null;
|
|
2422
2595
|
const resolvedOptions = {
|
|
2423
2596
|
openMode: "popup",
|
|
2597
|
+
requestSigningMode: "popup",
|
|
2424
2598
|
redirectUrl: undefined,
|
|
2425
2599
|
...options ?? {}
|
|
2426
2600
|
};
|
|
2427
2601
|
this.openMode = resolvedOptions.openMode;
|
|
2602
|
+
this.requestSigningMode = resolvedOptions.requestSigningMode;
|
|
2428
2603
|
this.redirectUrl = resolvedOptions.redirectUrl;
|
|
2429
2604
|
this.connection = new Connection({ network, walletUrl, apiUrl });
|
|
2430
2605
|
}
|
|
@@ -2445,7 +2620,15 @@ class LoopSDK {
|
|
|
2445
2620
|
try {
|
|
2446
2621
|
const verifiedAccount = await this.connection.verifySession(authToken);
|
|
2447
2622
|
if (verifiedAccount.party_id === partyId) {
|
|
2448
|
-
this.provider = new Provider({
|
|
2623
|
+
this.provider = new Provider({
|
|
2624
|
+
connection: this.connection,
|
|
2625
|
+
party_id: partyId,
|
|
2626
|
+
auth_token: authToken,
|
|
2627
|
+
public_key: publicKey,
|
|
2628
|
+
email,
|
|
2629
|
+
hooks: this.createProviderHooks()
|
|
2630
|
+
});
|
|
2631
|
+
this.ticketId = ticketId || null;
|
|
2449
2632
|
this.onAccept?.(this.provider);
|
|
2450
2633
|
if (ticketId) {
|
|
2451
2634
|
this.connection.connectWebSocket(ticketId, this.handleWebSocketMessage.bind(this));
|
|
@@ -2484,12 +2667,7 @@ class LoopSDK {
|
|
|
2484
2667
|
const { ticket_id: ticketId } = await this.connection.getTicket(this.appName, sessionId, this.version);
|
|
2485
2668
|
this.ticketId = ticketId;
|
|
2486
2669
|
localStorage.setItem("loop_connect", JSON.stringify({ sessionId, ticketId }));
|
|
2487
|
-
const
|
|
2488
|
-
url.searchParams.set("ticketId", ticketId);
|
|
2489
|
-
if (this.redirectUrl) {
|
|
2490
|
-
url.searchParams.set("redirectUrl", this.redirectUrl);
|
|
2491
|
-
}
|
|
2492
|
-
const connectUrl = url.toString();
|
|
2670
|
+
const connectUrl = this.buildConnectUrl(ticketId);
|
|
2493
2671
|
this.showQrCode(connectUrl);
|
|
2494
2672
|
this.connection.connectWebSocket(ticketId, this.handleWebSocketMessage.bind(this));
|
|
2495
2673
|
} catch (error) {
|
|
@@ -2504,11 +2682,19 @@ class LoopSDK {
|
|
|
2504
2682
|
console.log("[LoopSDK] Entering HANDSHAKE_ACCEPT flow");
|
|
2505
2683
|
const { authToken, partyId, publicKey, email } = message.payload || {};
|
|
2506
2684
|
if (authToken && partyId && publicKey) {
|
|
2507
|
-
this.provider = new Provider({
|
|
2685
|
+
this.provider = new Provider({
|
|
2686
|
+
connection: this.connection,
|
|
2687
|
+
party_id: partyId,
|
|
2688
|
+
auth_token: authToken,
|
|
2689
|
+
public_key: publicKey,
|
|
2690
|
+
email,
|
|
2691
|
+
hooks: this.createProviderHooks()
|
|
2692
|
+
});
|
|
2508
2693
|
const connectionInfoRaw = localStorage.getItem("loop_connect");
|
|
2509
2694
|
if (connectionInfoRaw) {
|
|
2510
2695
|
try {
|
|
2511
2696
|
const connectionInfo = JSON.parse(connectionInfoRaw);
|
|
2697
|
+
this.ticketId = connectionInfo.ticketId || this.ticketId;
|
|
2512
2698
|
connectionInfo.authToken = authToken;
|
|
2513
2699
|
connectionInfo.partyId = partyId;
|
|
2514
2700
|
connectionInfo.publicKey = publicKey;
|
|
@@ -2536,11 +2722,51 @@ class LoopSDK {
|
|
|
2536
2722
|
this.provider.handleResponse(message);
|
|
2537
2723
|
}
|
|
2538
2724
|
}
|
|
2539
|
-
|
|
2725
|
+
buildConnectUrl(ticketId) {
|
|
2726
|
+
const url = new URL("/.connect/", this.connection.walletUrl);
|
|
2727
|
+
url.searchParams.set("ticketId", ticketId);
|
|
2728
|
+
if (this.redirectUrl) {
|
|
2729
|
+
url.searchParams.set("redirectUrl", this.redirectUrl);
|
|
2730
|
+
}
|
|
2731
|
+
return url.toString();
|
|
2732
|
+
}
|
|
2733
|
+
buildDashboardUrl() {
|
|
2734
|
+
if (!this.connection) {
|
|
2735
|
+
throw new Error("Connection not initialized");
|
|
2736
|
+
}
|
|
2737
|
+
return this.connection.walletUrl;
|
|
2738
|
+
}
|
|
2739
|
+
openRequestUi() {
|
|
2540
2740
|
if (typeof window === "undefined") {
|
|
2541
|
-
return;
|
|
2741
|
+
return null;
|
|
2742
|
+
}
|
|
2743
|
+
if (!this.ticketId) {
|
|
2744
|
+
console.warn("[LoopSDK] Cannot open wallet UI for request: no active ticket.");
|
|
2745
|
+
return null;
|
|
2746
|
+
}
|
|
2747
|
+
const dashboardUrl = this.buildDashboardUrl();
|
|
2748
|
+
const targetMode = this.requestSigningMode === "tab" ? "tab" : "popup";
|
|
2749
|
+
const opened = this.openWallet(dashboardUrl, targetMode);
|
|
2750
|
+
if (opened) {
|
|
2751
|
+
this.popupWindow = opened;
|
|
2752
|
+
return opened;
|
|
2753
|
+
}
|
|
2754
|
+
return null;
|
|
2755
|
+
}
|
|
2756
|
+
closePopupIfExists() {
|
|
2757
|
+
if (this.popupWindow && !this.popupWindow.closed) {
|
|
2758
|
+
try {
|
|
2759
|
+
this.popupWindow.close();
|
|
2760
|
+
} catch {}
|
|
2542
2761
|
}
|
|
2543
|
-
|
|
2762
|
+
this.popupWindow = null;
|
|
2763
|
+
}
|
|
2764
|
+
openWallet(url, mode) {
|
|
2765
|
+
if (typeof window === "undefined") {
|
|
2766
|
+
return null;
|
|
2767
|
+
}
|
|
2768
|
+
const targetMode = mode || this.openMode;
|
|
2769
|
+
if (targetMode === "popup") {
|
|
2544
2770
|
const width = 480;
|
|
2545
2771
|
const height = 720;
|
|
2546
2772
|
const left = (window.innerWidth - width) / 2 + window.screenX;
|
|
@@ -2548,16 +2774,15 @@ class LoopSDK {
|
|
|
2548
2774
|
const features = `width=${width},height=${height},` + `left=${left},top=${top},` + "menubar=no,toolbar=no,location=no," + "resizable=yes,scrollbars=yes,status=no";
|
|
2549
2775
|
const popup = window.open(url, "loop-wallet", features);
|
|
2550
2776
|
if (!popup) {
|
|
2551
|
-
window.open(url, "_blank", "noopener,noreferrer");
|
|
2552
|
-
return;
|
|
2777
|
+
return window.open(url, "_blank", "noopener,noreferrer");
|
|
2553
2778
|
}
|
|
2554
2779
|
this.popupWindow = popup;
|
|
2555
2780
|
try {
|
|
2556
2781
|
popup.focus();
|
|
2557
2782
|
} catch {}
|
|
2558
|
-
return;
|
|
2783
|
+
return popup;
|
|
2559
2784
|
}
|
|
2560
|
-
window.open(url, "_blank", "noopener,noreferrer");
|
|
2785
|
+
return window.open(url, "_blank", "noopener,noreferrer");
|
|
2561
2786
|
}
|
|
2562
2787
|
showQrCode(url) {
|
|
2563
2788
|
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
@@ -2622,6 +2847,21 @@ class LoopSDK {
|
|
|
2622
2847
|
}
|
|
2623
2848
|
return this.provider;
|
|
2624
2849
|
}
|
|
2850
|
+
createProviderHooks() {
|
|
2851
|
+
return {
|
|
2852
|
+
onRequestStart: () => {
|
|
2853
|
+
return this.openRequestUi();
|
|
2854
|
+
},
|
|
2855
|
+
onRequestFinish: ({ requestContext }) => {
|
|
2856
|
+
const win = requestContext;
|
|
2857
|
+
if (win) {
|
|
2858
|
+
setTimeout(() => {
|
|
2859
|
+
this.closePopupIfExists();
|
|
2860
|
+
}, 800);
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2863
|
+
};
|
|
2864
|
+
}
|
|
2625
2865
|
}
|
|
2626
2866
|
var loop = new LoopSDK;
|
|
2627
2867
|
export {
|