@fivenorth/loop-sdk 0.6.4 → 0.7.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 +17 -0
- package/dist/index.js +167 -36
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -184,6 +184,23 @@ Common instrument overrides (pass into the `instrument` argument above):
|
|
|
184
184
|
|
|
185
185
|
Swap in the admin/id for the specific instrument you hold in the Loop wallet.
|
|
186
186
|
|
|
187
|
+
#### USDC withdraw helper
|
|
188
|
+
|
|
189
|
+
```javascript
|
|
190
|
+
await loop.wallet.extension.usdcBridge.withdrawalUSDCxToEthereum(
|
|
191
|
+
'0xYourEthAddress',
|
|
192
|
+
'10.5', // amount in USDCx
|
|
193
|
+
{
|
|
194
|
+
reference: 'optional memo',
|
|
195
|
+
requestTimeout: 5 * 60 * 1000, // optional override (ms)
|
|
196
|
+
},
|
|
197
|
+
);
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Notes:
|
|
201
|
+
- Uses the connect-based withdraw endpoint to prepare the transaction and sends it over Wallet Connect.
|
|
202
|
+
- The helper auto-reconnects the websocket if it was closed before sending the request.
|
|
203
|
+
|
|
187
204
|
# API
|
|
188
205
|
|
|
189
206
|
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);
|
|
2233
|
+
};
|
|
2234
|
+
ws.onerror = (event) => {
|
|
2235
|
+
if (this.ws === ws) {
|
|
2236
|
+
this.ws = null;
|
|
2237
|
+
}
|
|
2238
|
+
onError?.(event);
|
|
2225
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,6 +2323,7 @@ class Provider {
|
|
|
2269
2323
|
email;
|
|
2270
2324
|
auth_token;
|
|
2271
2325
|
requests = new Map;
|
|
2326
|
+
requestTimeout = DEFAULT_REQUEST_TIMEOUT_MS;
|
|
2272
2327
|
constructor({ connection, party_id, public_key, auth_token, email }) {
|
|
2273
2328
|
if (!connection) {
|
|
2274
2329
|
throw new Error("Provider requires a connection object.");
|
|
@@ -2279,6 +2334,9 @@ class Provider {
|
|
|
2279
2334
|
this.email = email;
|
|
2280
2335
|
this.auth_token = auth_token;
|
|
2281
2336
|
}
|
|
2337
|
+
getAuthToken() {
|
|
2338
|
+
return this.auth_token;
|
|
2339
|
+
}
|
|
2282
2340
|
handleResponse(message) {
|
|
2283
2341
|
console.log("Received response:", message);
|
|
2284
2342
|
if (message.request_id) {
|
|
@@ -2334,48 +2392,124 @@ class Provider {
|
|
|
2334
2392
|
async signMessage(message) {
|
|
2335
2393
|
return this.sendRequest("sign_raw_message" /* SIGN_RAW_MESSAGE */, message);
|
|
2336
2394
|
}
|
|
2395
|
+
async ensureConnected() {
|
|
2396
|
+
if (this.connection.ws && this.connection.ws.readyState === WebSocket.OPEN) {
|
|
2397
|
+
return;
|
|
2398
|
+
}
|
|
2399
|
+
if (typeof this.connection.reconnectWebSocket === "function") {
|
|
2400
|
+
await this.connection.reconnectWebSocket();
|
|
2401
|
+
if (this.connection.ws && this.connection.ws.readyState === WebSocket.OPEN) {
|
|
2402
|
+
return;
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
throw new Error("Not connected.");
|
|
2406
|
+
}
|
|
2337
2407
|
sendRequest(messageType, params = {}, options) {
|
|
2338
2408
|
return new Promise((resolve, reject) => {
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
const
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
} else {
|
|
2359
|
-
resolve(response.payload);
|
|
2360
|
-
}
|
|
2361
|
-
} else {
|
|
2362
|
-
elapsedTime += intervalTime;
|
|
2363
|
-
if (elapsedTime >= requestTimeout) {
|
|
2409
|
+
const ensure = async () => {
|
|
2410
|
+
try {
|
|
2411
|
+
await this.ensureConnected();
|
|
2412
|
+
} catch (error) {
|
|
2413
|
+
reject(error);
|
|
2414
|
+
return;
|
|
2415
|
+
}
|
|
2416
|
+
const requestId = generateRequestId();
|
|
2417
|
+
this.connection.ws.send(JSON.stringify({
|
|
2418
|
+
request_id: requestId,
|
|
2419
|
+
type: messageType,
|
|
2420
|
+
payload: params
|
|
2421
|
+
}));
|
|
2422
|
+
const intervalTime = 300;
|
|
2423
|
+
let elapsedTime = 0;
|
|
2424
|
+
const timeoutMs = options?.requestTimeout ?? this.requestTimeout;
|
|
2425
|
+
const intervalId = setInterval(() => {
|
|
2426
|
+
const response = this.requests.get(requestId);
|
|
2427
|
+
if (response) {
|
|
2364
2428
|
clearInterval(intervalId);
|
|
2365
2429
|
this.requests.delete(requestId);
|
|
2366
|
-
|
|
2430
|
+
if (response.type === "reject_request" /* REJECT_REQUEST */) {
|
|
2431
|
+
reject(new RejectRequestError);
|
|
2432
|
+
} else {
|
|
2433
|
+
resolve(response.payload);
|
|
2434
|
+
}
|
|
2435
|
+
} else {
|
|
2436
|
+
elapsedTime += intervalTime;
|
|
2437
|
+
if (elapsedTime >= timeoutMs) {
|
|
2438
|
+
clearInterval(intervalId);
|
|
2439
|
+
this.requests.delete(requestId);
|
|
2440
|
+
reject(new RequestTimeoutError(timeoutMs));
|
|
2441
|
+
}
|
|
2367
2442
|
}
|
|
2368
|
-
}
|
|
2369
|
-
}
|
|
2443
|
+
}, intervalTime);
|
|
2444
|
+
};
|
|
2445
|
+
ensure();
|
|
2370
2446
|
});
|
|
2371
2447
|
}
|
|
2372
2448
|
}
|
|
2373
2449
|
|
|
2450
|
+
// src/extensions/usdc/index.ts
|
|
2451
|
+
class UsdcBridge {
|
|
2452
|
+
getProvider;
|
|
2453
|
+
constructor(getProvider) {
|
|
2454
|
+
this.getProvider = getProvider;
|
|
2455
|
+
}
|
|
2456
|
+
requireProvider() {
|
|
2457
|
+
const provider = this.getProvider();
|
|
2458
|
+
if (!provider) {
|
|
2459
|
+
throw new Error("SDK not connected. Call connect() and wait for acceptance first.");
|
|
2460
|
+
}
|
|
2461
|
+
return provider;
|
|
2462
|
+
}
|
|
2463
|
+
withdrawalUSDCxToEthereum(recipient, amount, options) {
|
|
2464
|
+
const provider = this.requireProvider();
|
|
2465
|
+
const amountStr = typeof amount === "number" ? amount.toString() : amount;
|
|
2466
|
+
const withdrawRequest = {
|
|
2467
|
+
recipient,
|
|
2468
|
+
amount: amountStr,
|
|
2469
|
+
reference: options?.reference
|
|
2470
|
+
};
|
|
2471
|
+
return prepareUsdcWithdraw(provider.connection, provider.getAuthToken(), withdrawRequest).then((preparedPayload) => provider.submitTransaction({
|
|
2472
|
+
commands: preparedPayload.commands,
|
|
2473
|
+
disclosedContracts: preparedPayload.disclosedContracts,
|
|
2474
|
+
packageIdSelectionPreference: preparedPayload.packageIdSelectionPreference,
|
|
2475
|
+
actAs: preparedPayload.actAs,
|
|
2476
|
+
readAs: preparedPayload.readAs,
|
|
2477
|
+
synchronizerId: preparedPayload.synchronizerId
|
|
2478
|
+
}, { requestTimeout: options?.requestTimeout }));
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
async function prepareUsdcWithdraw(connection, authToken, params) {
|
|
2482
|
+
const payload = {
|
|
2483
|
+
recipient: params.recipient,
|
|
2484
|
+
amount: params.amount
|
|
2485
|
+
};
|
|
2486
|
+
if (params.reference) {
|
|
2487
|
+
payload.reference = params.reference;
|
|
2488
|
+
}
|
|
2489
|
+
const response = await fetch(`${connection.apiUrl}/api/v1/.connect/pair/usdc/withdraw`, {
|
|
2490
|
+
method: "POST",
|
|
2491
|
+
headers: {
|
|
2492
|
+
"Content-Type": "application/json",
|
|
2493
|
+
Authorization: `Bearer ${authToken}`
|
|
2494
|
+
},
|
|
2495
|
+
body: JSON.stringify(payload)
|
|
2496
|
+
});
|
|
2497
|
+
if (!response.ok) {
|
|
2498
|
+
throw new Error("Failed to prepare USDC withdrawal.");
|
|
2499
|
+
}
|
|
2500
|
+
const data = await response.json();
|
|
2501
|
+
return data.payload;
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2374
2504
|
// src/wallet.ts
|
|
2375
2505
|
class LoopWallet {
|
|
2376
2506
|
getProvider;
|
|
2507
|
+
extension;
|
|
2377
2508
|
constructor(getProvider) {
|
|
2378
2509
|
this.getProvider = getProvider;
|
|
2510
|
+
this.extension = {
|
|
2511
|
+
usdcBridge: new UsdcBridge(this.getProvider)
|
|
2512
|
+
};
|
|
2379
2513
|
}
|
|
2380
2514
|
requireProvider() {
|
|
2381
2515
|
const provider = this.getProvider();
|
|
@@ -2531,9 +2665,6 @@ class LoopSDK {
|
|
|
2531
2665
|
this.onReject?.();
|
|
2532
2666
|
this.hideQrCode();
|
|
2533
2667
|
console.log("[LoopSDK] HANDSHAKE_REJECT: closing popup (if exists)");
|
|
2534
|
-
if (this.popupWindow && !this.popupWindow.closed) {
|
|
2535
|
-
this.popupWindow.close();
|
|
2536
|
-
}
|
|
2537
2668
|
this.popupWindow = null;
|
|
2538
2669
|
} else if (this.provider) {
|
|
2539
2670
|
this.provider.handleResponse(message);
|