@fivenorth/loop-sdk 0.7.0 → 0.7.2

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 +10 -1
  2. package/dist/index.js +136 -24
  3. 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
@@ -192,6 +200,7 @@ await loop.wallet.extension.usdcBridge.withdrawalUSDCxToEthereum(
192
200
  '10.5', // amount in USDCx
193
201
  {
194
202
  reference: 'optional memo',
203
+ message: 'Withdraw 10.5 USDCx to 0xabc', // optional custom prompt text
195
204
  requestTimeout: 5 * 60 * 1000, // optional override (ms)
196
205
  },
197
206
  );
package/dist/index.js CHANGED
@@ -2209,7 +2209,10 @@ class Connection {
2209
2209
  party_id: data?.party_id,
2210
2210
  auth_token: authToken,
2211
2211
  public_key: data?.public_key,
2212
- email
2212
+ email,
2213
+ has_preapproval: data?.has_preapproval,
2214
+ has_merge_delegation: data?.has_merge_delegation,
2215
+ usdc_bridge_access: data?.usdc_bridge_access
2213
2216
  };
2214
2217
  return account;
2215
2218
  }
@@ -2324,7 +2327,8 @@ class Provider {
2324
2327
  auth_token;
2325
2328
  requests = new Map;
2326
2329
  requestTimeout = DEFAULT_REQUEST_TIMEOUT_MS;
2327
- constructor({ connection, party_id, public_key, auth_token, email }) {
2330
+ hooks;
2331
+ constructor({ connection, party_id, public_key, auth_token, email, hooks }) {
2328
2332
  if (!connection) {
2329
2333
  throw new Error("Provider requires a connection object.");
2330
2334
  }
@@ -2333,6 +2337,7 @@ class Provider {
2333
2337
  this.public_key = public_key;
2334
2338
  this.email = email;
2335
2339
  this.auth_token = auth_token;
2340
+ this.hooks = hooks;
2336
2341
  }
2337
2342
  getAuthToken() {
2338
2343
  return this.auth_token;
@@ -2343,10 +2348,13 @@ class Provider {
2343
2348
  this.requests.set(message.request_id, message);
2344
2349
  }
2345
2350
  }
2346
- async getHolding() {
2351
+ getHolding() {
2347
2352
  return this.connection.getHolding(this.auth_token);
2348
2353
  }
2349
- async getActiveContracts(params) {
2354
+ getAccount() {
2355
+ return this.connection.verifySession(this.auth_token);
2356
+ }
2357
+ getActiveContracts(params) {
2350
2358
  return this.connection.getActiveContracts(this.auth_token, params);
2351
2359
  }
2352
2360
  async submitTransaction(payload, options) {
@@ -2355,6 +2363,7 @@ class Provider {
2355
2363
  async transfer(recipient, amount, instrument, options) {
2356
2364
  const amountStr = typeof amount === "number" ? amount.toString() : amount;
2357
2365
  const { requestedAt, executeBefore, requestTimeout } = options || {};
2366
+ const message = options?.message;
2358
2367
  const resolveDate = (value, fallbackMs) => {
2359
2368
  if (value instanceof Date) {
2360
2369
  return value.toISOString();
@@ -2387,7 +2396,7 @@ class Provider {
2387
2396
  actAs: preparedPayload.actAs,
2388
2397
  readAs: preparedPayload.readAs,
2389
2398
  synchronizerId: preparedPayload.synchronizerId
2390
- }, { requestTimeout });
2399
+ }, { requestTimeout, message });
2391
2400
  }
2392
2401
  async signMessage(message) {
2393
2402
  return this.sendRequest("sign_raw_message" /* SIGN_RAW_MESSAGE */, message);
@@ -2406,19 +2415,36 @@ class Provider {
2406
2415
  }
2407
2416
  sendRequest(messageType, params = {}, options) {
2408
2417
  return new Promise((resolve, reject) => {
2418
+ const requestId = generateRequestId();
2419
+ const requestContext = this.hooks?.onRequestStart?.(messageType, options?.requestLabel);
2409
2420
  const ensure = async () => {
2410
2421
  try {
2411
2422
  await this.ensureConnected();
2412
2423
  } catch (error) {
2424
+ this.hooks?.onRequestFinish?.({
2425
+ status: "error",
2426
+ messageType,
2427
+ requestLabel: options?.requestLabel,
2428
+ requestContext
2429
+ });
2413
2430
  reject(error);
2414
2431
  return;
2415
2432
  }
2416
- const requestId = generateRequestId();
2417
- this.connection.ws.send(JSON.stringify({
2433
+ const requestBody = {
2418
2434
  request_id: requestId,
2419
2435
  type: messageType,
2420
2436
  payload: params
2421
- }));
2437
+ };
2438
+ if (options?.message) {
2439
+ requestBody.ticket = { message: options.message };
2440
+ if (typeof params === "object" && params !== null && !Array.isArray(params)) {
2441
+ requestBody.payload = {
2442
+ ...params,
2443
+ ticket: { message: options.message }
2444
+ };
2445
+ }
2446
+ }
2447
+ this.connection.ws.send(JSON.stringify(requestBody));
2422
2448
  const intervalTime = 300;
2423
2449
  let elapsedTime = 0;
2424
2450
  const timeoutMs = options?.requestTimeout ?? this.requestTimeout;
@@ -2428,8 +2454,20 @@ class Provider {
2428
2454
  clearInterval(intervalId);
2429
2455
  this.requests.delete(requestId);
2430
2456
  if (response.type === "reject_request" /* REJECT_REQUEST */) {
2457
+ this.hooks?.onRequestFinish?.({
2458
+ status: "rejected",
2459
+ messageType,
2460
+ requestLabel: options?.requestLabel,
2461
+ requestContext
2462
+ });
2431
2463
  reject(new RejectRequestError);
2432
2464
  } else {
2465
+ this.hooks?.onRequestFinish?.({
2466
+ status: "success",
2467
+ messageType,
2468
+ requestLabel: options?.requestLabel,
2469
+ requestContext
2470
+ });
2433
2471
  resolve(response.payload);
2434
2472
  }
2435
2473
  } else {
@@ -2437,6 +2475,12 @@ class Provider {
2437
2475
  if (elapsedTime >= timeoutMs) {
2438
2476
  clearInterval(intervalId);
2439
2477
  this.requests.delete(requestId);
2478
+ this.hooks?.onRequestFinish?.({
2479
+ status: "timeout",
2480
+ messageType,
2481
+ requestLabel: options?.requestLabel,
2482
+ requestContext
2483
+ });
2440
2484
  reject(new RequestTimeoutError(timeoutMs));
2441
2485
  }
2442
2486
  }
@@ -2475,7 +2519,7 @@ class UsdcBridge {
2475
2519
  actAs: preparedPayload.actAs,
2476
2520
  readAs: preparedPayload.readAs,
2477
2521
  synchronizerId: preparedPayload.synchronizerId
2478
- }, { requestTimeout: options?.requestTimeout }));
2522
+ }, { requestTimeout: options?.requestTimeout, message: options?.message }));
2479
2523
  }
2480
2524
  }
2481
2525
  async function prepareUsdcWithdraw(connection, authToken, params) {
@@ -2531,6 +2575,7 @@ class LoopSDK {
2531
2575
  connection = null;
2532
2576
  provider = null;
2533
2577
  openMode = "popup";
2578
+ requestSigningMode = "popup";
2534
2579
  popupWindow = null;
2535
2580
  redirectUrl;
2536
2581
  onAccept = null;
@@ -2555,10 +2600,12 @@ class LoopSDK {
2555
2600
  this.onReject = onReject || null;
2556
2601
  const resolvedOptions = {
2557
2602
  openMode: "popup",
2603
+ requestSigningMode: "popup",
2558
2604
  redirectUrl: undefined,
2559
2605
  ...options ?? {}
2560
2606
  };
2561
2607
  this.openMode = resolvedOptions.openMode;
2608
+ this.requestSigningMode = resolvedOptions.requestSigningMode;
2562
2609
  this.redirectUrl = resolvedOptions.redirectUrl;
2563
2610
  this.connection = new Connection({ network, walletUrl, apiUrl });
2564
2611
  }
@@ -2579,7 +2626,15 @@ class LoopSDK {
2579
2626
  try {
2580
2627
  const verifiedAccount = await this.connection.verifySession(authToken);
2581
2628
  if (verifiedAccount.party_id === partyId) {
2582
- this.provider = new Provider({ connection: this.connection, party_id: partyId, auth_token: authToken, public_key: publicKey, email });
2629
+ this.provider = new Provider({
2630
+ connection: this.connection,
2631
+ party_id: partyId,
2632
+ auth_token: authToken,
2633
+ public_key: publicKey,
2634
+ email,
2635
+ hooks: this.createProviderHooks()
2636
+ });
2637
+ this.ticketId = ticketId || null;
2583
2638
  this.onAccept?.(this.provider);
2584
2639
  if (ticketId) {
2585
2640
  this.connection.connectWebSocket(ticketId, this.handleWebSocketMessage.bind(this));
@@ -2618,12 +2673,7 @@ class LoopSDK {
2618
2673
  const { ticket_id: ticketId } = await this.connection.getTicket(this.appName, sessionId, this.version);
2619
2674
  this.ticketId = ticketId;
2620
2675
  localStorage.setItem("loop_connect", JSON.stringify({ sessionId, ticketId }));
2621
- const url = new URL("/.connect/", this.connection.walletUrl);
2622
- url.searchParams.set("ticketId", ticketId);
2623
- if (this.redirectUrl) {
2624
- url.searchParams.set("redirectUrl", this.redirectUrl);
2625
- }
2626
- const connectUrl = url.toString();
2676
+ const connectUrl = this.buildConnectUrl(ticketId);
2627
2677
  this.showQrCode(connectUrl);
2628
2678
  this.connection.connectWebSocket(ticketId, this.handleWebSocketMessage.bind(this));
2629
2679
  } catch (error) {
@@ -2638,11 +2688,19 @@ class LoopSDK {
2638
2688
  console.log("[LoopSDK] Entering HANDSHAKE_ACCEPT flow");
2639
2689
  const { authToken, partyId, publicKey, email } = message.payload || {};
2640
2690
  if (authToken && partyId && publicKey) {
2641
- this.provider = new Provider({ connection: this.connection, party_id: partyId, auth_token: authToken, public_key: publicKey, email });
2691
+ this.provider = new Provider({
2692
+ connection: this.connection,
2693
+ party_id: partyId,
2694
+ auth_token: authToken,
2695
+ public_key: publicKey,
2696
+ email,
2697
+ hooks: this.createProviderHooks()
2698
+ });
2642
2699
  const connectionInfoRaw = localStorage.getItem("loop_connect");
2643
2700
  if (connectionInfoRaw) {
2644
2701
  try {
2645
2702
  const connectionInfo = JSON.parse(connectionInfoRaw);
2703
+ this.ticketId = connectionInfo.ticketId || this.ticketId;
2646
2704
  connectionInfo.authToken = authToken;
2647
2705
  connectionInfo.partyId = partyId;
2648
2706
  connectionInfo.publicKey = publicKey;
@@ -2670,11 +2728,51 @@ class LoopSDK {
2670
2728
  this.provider.handleResponse(message);
2671
2729
  }
2672
2730
  }
2673
- openWallet(url) {
2731
+ buildConnectUrl(ticketId) {
2732
+ const url = new URL("/.connect/", this.connection.walletUrl);
2733
+ url.searchParams.set("ticketId", ticketId);
2734
+ if (this.redirectUrl) {
2735
+ url.searchParams.set("redirectUrl", this.redirectUrl);
2736
+ }
2737
+ return url.toString();
2738
+ }
2739
+ buildDashboardUrl() {
2740
+ if (!this.connection) {
2741
+ throw new Error("Connection not initialized");
2742
+ }
2743
+ return this.connection.walletUrl;
2744
+ }
2745
+ openRequestUi() {
2674
2746
  if (typeof window === "undefined") {
2675
- return;
2747
+ return null;
2748
+ }
2749
+ if (!this.ticketId) {
2750
+ console.warn("[LoopSDK] Cannot open wallet UI for request: no active ticket.");
2751
+ return null;
2676
2752
  }
2677
- if (this.openMode === "popup") {
2753
+ const dashboardUrl = this.buildDashboardUrl();
2754
+ const targetMode = this.requestSigningMode === "tab" ? "tab" : "popup";
2755
+ const opened = this.openWallet(dashboardUrl, targetMode);
2756
+ if (opened) {
2757
+ this.popupWindow = opened;
2758
+ return opened;
2759
+ }
2760
+ return null;
2761
+ }
2762
+ closePopupIfExists() {
2763
+ if (this.popupWindow && !this.popupWindow.closed) {
2764
+ try {
2765
+ this.popupWindow.close();
2766
+ } catch {}
2767
+ }
2768
+ this.popupWindow = null;
2769
+ }
2770
+ openWallet(url, mode) {
2771
+ if (typeof window === "undefined") {
2772
+ return null;
2773
+ }
2774
+ const targetMode = mode || this.openMode;
2775
+ if (targetMode === "popup") {
2678
2776
  const width = 480;
2679
2777
  const height = 720;
2680
2778
  const left = (window.innerWidth - width) / 2 + window.screenX;
@@ -2682,16 +2780,15 @@ class LoopSDK {
2682
2780
  const features = `width=${width},height=${height},` + `left=${left},top=${top},` + "menubar=no,toolbar=no,location=no," + "resizable=yes,scrollbars=yes,status=no";
2683
2781
  const popup = window.open(url, "loop-wallet", features);
2684
2782
  if (!popup) {
2685
- window.open(url, "_blank", "noopener,noreferrer");
2686
- return;
2783
+ return window.open(url, "_blank", "noopener,noreferrer");
2687
2784
  }
2688
2785
  this.popupWindow = popup;
2689
2786
  try {
2690
2787
  popup.focus();
2691
2788
  } catch {}
2692
- return;
2789
+ return popup;
2693
2790
  }
2694
- window.open(url, "_blank", "noopener,noreferrer");
2791
+ return window.open(url, "_blank", "noopener,noreferrer");
2695
2792
  }
2696
2793
  showQrCode(url) {
2697
2794
  if (typeof window === "undefined" || typeof document === "undefined") {
@@ -2756,6 +2853,21 @@ class LoopSDK {
2756
2853
  }
2757
2854
  return this.provider;
2758
2855
  }
2856
+ createProviderHooks() {
2857
+ return {
2858
+ onRequestStart: () => {
2859
+ return this.openRequestUi();
2860
+ },
2861
+ onRequestFinish: ({ requestContext }) => {
2862
+ const win = requestContext;
2863
+ if (win) {
2864
+ setTimeout(() => {
2865
+ this.closePopupIfExists();
2866
+ }, 800);
2867
+ }
2868
+ }
2869
+ };
2870
+ }
2759
2871
  }
2760
2872
  var loop = new LoopSDK;
2761
2873
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fivenorth/loop-sdk",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "author": "hello@fivenorth.io",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",