@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.
- package/README.md +10 -1
- package/dist/index.js +136 -24
- 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
|
-
|
|
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
|
-
|
|
2351
|
+
getHolding() {
|
|
2347
2352
|
return this.connection.getHolding(this.auth_token);
|
|
2348
2353
|
}
|
|
2349
|
-
|
|
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
|
|
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({
|
|
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
|
|
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({
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|