@fivenorth/loop-sdk 0.7.0 → 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 +10 -1
- package/dist/index.js +127 -21
- 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
|
@@ -2324,7 +2324,8 @@ class Provider {
|
|
|
2324
2324
|
auth_token;
|
|
2325
2325
|
requests = new Map;
|
|
2326
2326
|
requestTimeout = DEFAULT_REQUEST_TIMEOUT_MS;
|
|
2327
|
-
|
|
2327
|
+
hooks;
|
|
2328
|
+
constructor({ connection, party_id, public_key, auth_token, email, hooks }) {
|
|
2328
2329
|
if (!connection) {
|
|
2329
2330
|
throw new Error("Provider requires a connection object.");
|
|
2330
2331
|
}
|
|
@@ -2333,6 +2334,7 @@ class Provider {
|
|
|
2333
2334
|
this.public_key = public_key;
|
|
2334
2335
|
this.email = email;
|
|
2335
2336
|
this.auth_token = auth_token;
|
|
2337
|
+
this.hooks = hooks;
|
|
2336
2338
|
}
|
|
2337
2339
|
getAuthToken() {
|
|
2338
2340
|
return this.auth_token;
|
|
@@ -2355,6 +2357,7 @@ class Provider {
|
|
|
2355
2357
|
async transfer(recipient, amount, instrument, options) {
|
|
2356
2358
|
const amountStr = typeof amount === "number" ? amount.toString() : amount;
|
|
2357
2359
|
const { requestedAt, executeBefore, requestTimeout } = options || {};
|
|
2360
|
+
const message = options?.message;
|
|
2358
2361
|
const resolveDate = (value, fallbackMs) => {
|
|
2359
2362
|
if (value instanceof Date) {
|
|
2360
2363
|
return value.toISOString();
|
|
@@ -2387,7 +2390,7 @@ class Provider {
|
|
|
2387
2390
|
actAs: preparedPayload.actAs,
|
|
2388
2391
|
readAs: preparedPayload.readAs,
|
|
2389
2392
|
synchronizerId: preparedPayload.synchronizerId
|
|
2390
|
-
}, { requestTimeout });
|
|
2393
|
+
}, { requestTimeout, message });
|
|
2391
2394
|
}
|
|
2392
2395
|
async signMessage(message) {
|
|
2393
2396
|
return this.sendRequest("sign_raw_message" /* SIGN_RAW_MESSAGE */, message);
|
|
@@ -2406,19 +2409,36 @@ class Provider {
|
|
|
2406
2409
|
}
|
|
2407
2410
|
sendRequest(messageType, params = {}, options) {
|
|
2408
2411
|
return new Promise((resolve, reject) => {
|
|
2412
|
+
const requestId = generateRequestId();
|
|
2413
|
+
const requestContext = this.hooks?.onRequestStart?.(messageType, options?.requestLabel);
|
|
2409
2414
|
const ensure = async () => {
|
|
2410
2415
|
try {
|
|
2411
2416
|
await this.ensureConnected();
|
|
2412
2417
|
} catch (error) {
|
|
2418
|
+
this.hooks?.onRequestFinish?.({
|
|
2419
|
+
status: "error",
|
|
2420
|
+
messageType,
|
|
2421
|
+
requestLabel: options?.requestLabel,
|
|
2422
|
+
requestContext
|
|
2423
|
+
});
|
|
2413
2424
|
reject(error);
|
|
2414
2425
|
return;
|
|
2415
2426
|
}
|
|
2416
|
-
const
|
|
2417
|
-
this.connection.ws.send(JSON.stringify({
|
|
2427
|
+
const requestBody = {
|
|
2418
2428
|
request_id: requestId,
|
|
2419
2429
|
type: messageType,
|
|
2420
2430
|
payload: params
|
|
2421
|
-
}
|
|
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
|
+
};
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
this.connection.ws.send(JSON.stringify(requestBody));
|
|
2422
2442
|
const intervalTime = 300;
|
|
2423
2443
|
let elapsedTime = 0;
|
|
2424
2444
|
const timeoutMs = options?.requestTimeout ?? this.requestTimeout;
|
|
@@ -2428,8 +2448,20 @@ class Provider {
|
|
|
2428
2448
|
clearInterval(intervalId);
|
|
2429
2449
|
this.requests.delete(requestId);
|
|
2430
2450
|
if (response.type === "reject_request" /* REJECT_REQUEST */) {
|
|
2451
|
+
this.hooks?.onRequestFinish?.({
|
|
2452
|
+
status: "rejected",
|
|
2453
|
+
messageType,
|
|
2454
|
+
requestLabel: options?.requestLabel,
|
|
2455
|
+
requestContext
|
|
2456
|
+
});
|
|
2431
2457
|
reject(new RejectRequestError);
|
|
2432
2458
|
} else {
|
|
2459
|
+
this.hooks?.onRequestFinish?.({
|
|
2460
|
+
status: "success",
|
|
2461
|
+
messageType,
|
|
2462
|
+
requestLabel: options?.requestLabel,
|
|
2463
|
+
requestContext
|
|
2464
|
+
});
|
|
2433
2465
|
resolve(response.payload);
|
|
2434
2466
|
}
|
|
2435
2467
|
} else {
|
|
@@ -2437,6 +2469,12 @@ class Provider {
|
|
|
2437
2469
|
if (elapsedTime >= timeoutMs) {
|
|
2438
2470
|
clearInterval(intervalId);
|
|
2439
2471
|
this.requests.delete(requestId);
|
|
2472
|
+
this.hooks?.onRequestFinish?.({
|
|
2473
|
+
status: "timeout",
|
|
2474
|
+
messageType,
|
|
2475
|
+
requestLabel: options?.requestLabel,
|
|
2476
|
+
requestContext
|
|
2477
|
+
});
|
|
2440
2478
|
reject(new RequestTimeoutError(timeoutMs));
|
|
2441
2479
|
}
|
|
2442
2480
|
}
|
|
@@ -2475,7 +2513,7 @@ class UsdcBridge {
|
|
|
2475
2513
|
actAs: preparedPayload.actAs,
|
|
2476
2514
|
readAs: preparedPayload.readAs,
|
|
2477
2515
|
synchronizerId: preparedPayload.synchronizerId
|
|
2478
|
-
}, { requestTimeout: options?.requestTimeout }));
|
|
2516
|
+
}, { requestTimeout: options?.requestTimeout, message: options?.message }));
|
|
2479
2517
|
}
|
|
2480
2518
|
}
|
|
2481
2519
|
async function prepareUsdcWithdraw(connection, authToken, params) {
|
|
@@ -2531,6 +2569,7 @@ class LoopSDK {
|
|
|
2531
2569
|
connection = null;
|
|
2532
2570
|
provider = null;
|
|
2533
2571
|
openMode = "popup";
|
|
2572
|
+
requestSigningMode = "popup";
|
|
2534
2573
|
popupWindow = null;
|
|
2535
2574
|
redirectUrl;
|
|
2536
2575
|
onAccept = null;
|
|
@@ -2555,10 +2594,12 @@ class LoopSDK {
|
|
|
2555
2594
|
this.onReject = onReject || null;
|
|
2556
2595
|
const resolvedOptions = {
|
|
2557
2596
|
openMode: "popup",
|
|
2597
|
+
requestSigningMode: "popup",
|
|
2558
2598
|
redirectUrl: undefined,
|
|
2559
2599
|
...options ?? {}
|
|
2560
2600
|
};
|
|
2561
2601
|
this.openMode = resolvedOptions.openMode;
|
|
2602
|
+
this.requestSigningMode = resolvedOptions.requestSigningMode;
|
|
2562
2603
|
this.redirectUrl = resolvedOptions.redirectUrl;
|
|
2563
2604
|
this.connection = new Connection({ network, walletUrl, apiUrl });
|
|
2564
2605
|
}
|
|
@@ -2579,7 +2620,15 @@ class LoopSDK {
|
|
|
2579
2620
|
try {
|
|
2580
2621
|
const verifiedAccount = await this.connection.verifySession(authToken);
|
|
2581
2622
|
if (verifiedAccount.party_id === partyId) {
|
|
2582
|
-
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;
|
|
2583
2632
|
this.onAccept?.(this.provider);
|
|
2584
2633
|
if (ticketId) {
|
|
2585
2634
|
this.connection.connectWebSocket(ticketId, this.handleWebSocketMessage.bind(this));
|
|
@@ -2618,12 +2667,7 @@ class LoopSDK {
|
|
|
2618
2667
|
const { ticket_id: ticketId } = await this.connection.getTicket(this.appName, sessionId, this.version);
|
|
2619
2668
|
this.ticketId = ticketId;
|
|
2620
2669
|
localStorage.setItem("loop_connect", JSON.stringify({ sessionId, ticketId }));
|
|
2621
|
-
const
|
|
2622
|
-
url.searchParams.set("ticketId", ticketId);
|
|
2623
|
-
if (this.redirectUrl) {
|
|
2624
|
-
url.searchParams.set("redirectUrl", this.redirectUrl);
|
|
2625
|
-
}
|
|
2626
|
-
const connectUrl = url.toString();
|
|
2670
|
+
const connectUrl = this.buildConnectUrl(ticketId);
|
|
2627
2671
|
this.showQrCode(connectUrl);
|
|
2628
2672
|
this.connection.connectWebSocket(ticketId, this.handleWebSocketMessage.bind(this));
|
|
2629
2673
|
} catch (error) {
|
|
@@ -2638,11 +2682,19 @@ class LoopSDK {
|
|
|
2638
2682
|
console.log("[LoopSDK] Entering HANDSHAKE_ACCEPT flow");
|
|
2639
2683
|
const { authToken, partyId, publicKey, email } = message.payload || {};
|
|
2640
2684
|
if (authToken && partyId && publicKey) {
|
|
2641
|
-
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
|
+
});
|
|
2642
2693
|
const connectionInfoRaw = localStorage.getItem("loop_connect");
|
|
2643
2694
|
if (connectionInfoRaw) {
|
|
2644
2695
|
try {
|
|
2645
2696
|
const connectionInfo = JSON.parse(connectionInfoRaw);
|
|
2697
|
+
this.ticketId = connectionInfo.ticketId || this.ticketId;
|
|
2646
2698
|
connectionInfo.authToken = authToken;
|
|
2647
2699
|
connectionInfo.partyId = partyId;
|
|
2648
2700
|
connectionInfo.publicKey = publicKey;
|
|
@@ -2670,11 +2722,51 @@ class LoopSDK {
|
|
|
2670
2722
|
this.provider.handleResponse(message);
|
|
2671
2723
|
}
|
|
2672
2724
|
}
|
|
2673
|
-
|
|
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() {
|
|
2674
2740
|
if (typeof window === "undefined") {
|
|
2675
|
-
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 {}
|
|
2676
2761
|
}
|
|
2677
|
-
|
|
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") {
|
|
2678
2770
|
const width = 480;
|
|
2679
2771
|
const height = 720;
|
|
2680
2772
|
const left = (window.innerWidth - width) / 2 + window.screenX;
|
|
@@ -2682,16 +2774,15 @@ class LoopSDK {
|
|
|
2682
2774
|
const features = `width=${width},height=${height},` + `left=${left},top=${top},` + "menubar=no,toolbar=no,location=no," + "resizable=yes,scrollbars=yes,status=no";
|
|
2683
2775
|
const popup = window.open(url, "loop-wallet", features);
|
|
2684
2776
|
if (!popup) {
|
|
2685
|
-
window.open(url, "_blank", "noopener,noreferrer");
|
|
2686
|
-
return;
|
|
2777
|
+
return window.open(url, "_blank", "noopener,noreferrer");
|
|
2687
2778
|
}
|
|
2688
2779
|
this.popupWindow = popup;
|
|
2689
2780
|
try {
|
|
2690
2781
|
popup.focus();
|
|
2691
2782
|
} catch {}
|
|
2692
|
-
return;
|
|
2783
|
+
return popup;
|
|
2693
2784
|
}
|
|
2694
|
-
window.open(url, "_blank", "noopener,noreferrer");
|
|
2785
|
+
return window.open(url, "_blank", "noopener,noreferrer");
|
|
2695
2786
|
}
|
|
2696
2787
|
showQrCode(url) {
|
|
2697
2788
|
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
@@ -2756,6 +2847,21 @@ class LoopSDK {
|
|
|
2756
2847
|
}
|
|
2757
2848
|
return this.provider;
|
|
2758
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
|
+
}
|
|
2759
2865
|
}
|
|
2760
2866
|
var loop = new LoopSDK;
|
|
2761
2867
|
export {
|