@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.
Files changed (3) hide show
  1. package/README.md +17 -0
  2. package/dist/index.js +167 -36
  3. 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
- connectWebSocket(ticketId, onMessage) {
2219
+ attachWebSocket(ticketId, onMessage, onOpen, onError, onClose) {
2217
2220
  const wsUrl = this.websocketUrl(ticketId);
2218
- this.ws = new WebSocket(wsUrl);
2219
- this.ws.onmessage = onMessage;
2220
- this.ws.onopen = () => {
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
- this.ws.onclose = () => {
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
- if (!this.connection.ws || this.connection.ws.readyState !== WebSocket.OPEN) {
2340
- return reject(new Error("Not connected."));
2341
- }
2342
- const requestId = generateRequestId();
2343
- const requestTimeout = options?.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT_MS;
2344
- this.connection.ws.send(JSON.stringify({
2345
- request_id: requestId,
2346
- type: messageType,
2347
- payload: params
2348
- }));
2349
- const intervalTime = 300;
2350
- let elapsedTime = 0;
2351
- const intervalId = setInterval(() => {
2352
- const response = this.requests.get(requestId);
2353
- if (response) {
2354
- clearInterval(intervalId);
2355
- this.requests.delete(requestId);
2356
- if (response.type === "reject_request" /* REJECT_REQUEST */) {
2357
- reject(new RejectRequestError);
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
- reject(new RequestTimeoutError(requestTimeout));
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
- }, intervalTime);
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fivenorth/loop-sdk",
3
- "version": "0.6.4",
3
+ "version": "0.7.0",
4
4
  "author": "hello@fivenorth.io",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",