@fivenorth/loop-sdk 0.1.0 → 0.1.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 +139 -5
- package/dist/index.js +26 -20
- package/package.json +14 -14
package/README.md
CHANGED
|
@@ -1,20 +1,153 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Loop SDK
|
|
2
2
|
|
|
3
|
-
Loop SDK
|
|
3
|
+
Loop SDK allows dApps connect to a [Loop](https://cantonloop.com) account. The Loop wallet can be on mobile or on a desktop browser. All the interaction will happen inside the dApp. For signing, user will be prompted to sign either on their Loop wallet on mobile devices or on browser.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Limitation
|
|
6
6
|
|
|
7
7
|
Currently, we only support DAML transaction from the Splice build-in DAR files and Utility app DAR files.
|
|
8
8
|
|
|
9
9
|
There is no plan to upload and support third party DAR at this moment
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
## Quick overview
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
For a quick overview of how the code look like, you can take a look at this pen https://codepen.io/kureikain/pen/KwVGgLX.
|
|
14
|
+
|
|
15
|
+
## Usage guide
|
|
16
|
+
|
|
17
|
+
To use the Loop SDK, you first need to install it from NPM:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
bun add @fivenorth/loop-sdk
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Then you can import it in your dApp:
|
|
24
|
+
|
|
25
|
+
```javascript
|
|
26
|
+
import { loop } from '@fivenorth/loop-sdk';
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Note that, If you don't want to implement a build process, you can include the file directly with `unpkg` such as
|
|
30
|
+
|
|
31
|
+
```javascript
|
|
32
|
+
import { loop } from 'https://unpkg.com/@fivenorth/loop-sdk@0.1.1/dist';
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
An example of how we use it in that manner is on our [loopsdk demo](https://codepen.io/kureikain/pen/KwVGgLX)
|
|
36
|
+
|
|
37
|
+
### 1. Initialize the SDK
|
|
38
|
+
|
|
39
|
+
Before you can connect, you need to initialize the SDK. This is typically done once when your application loads.
|
|
40
|
+
|
|
41
|
+
```javascript
|
|
42
|
+
loop.init({
|
|
43
|
+
appName: 'My Awesome dApp',
|
|
44
|
+
network: 'local', // or 'devnet', 'mainnet'
|
|
45
|
+
onAccept: (provider) => {
|
|
46
|
+
console.log('Connected!', provider);
|
|
47
|
+
// You can now use the provider to interact with the wallet
|
|
48
|
+
},
|
|
49
|
+
onReject: () => {
|
|
50
|
+
console.log('Connection rejected by user.');
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The `init` method takes a configuration object with the following properties:
|
|
56
|
+
- `appName`: The name of your application, which will be displayed to the user in the Loop wallet.
|
|
57
|
+
- `network`: The network to connect to. Can be `local`, `devnet`, or `mainnet`.
|
|
58
|
+
- `onAccept`: A callback function that is called when the user accepts the connection. It receives a `provider` object.
|
|
59
|
+
- `onReject`: A callback function that is called when the user rejects the connection.
|
|
60
|
+
|
|
61
|
+
### 2. Connect to the wallet
|
|
62
|
+
|
|
63
|
+
To initiate the connection, call `loop.connect()`:
|
|
64
|
+
|
|
65
|
+
```javascript
|
|
66
|
+
loop.connect();
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
This will open a modal with a QR code for the user to scan with their Loop wallet.
|
|
70
|
+
|
|
71
|
+
### 3. Using the Provider
|
|
72
|
+
|
|
73
|
+
Once the connection is established, the `onAccept` callback will receive a `provider` object. This object provides methods to interact with the user's wallet and the DAML ledger.
|
|
74
|
+
|
|
75
|
+
The provider object has the `party_id` of the connected user.
|
|
76
|
+
|
|
77
|
+
#### Get Holdings
|
|
78
|
+
|
|
79
|
+
To get the user's token holdings:
|
|
80
|
+
|
|
81
|
+
```javascript
|
|
82
|
+
const holdings = await provider.getHolding();
|
|
83
|
+
console.log(holdings);
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
#### Get Active Contracts
|
|
87
|
+
|
|
88
|
+
You can query for active contracts by `templateId` or `interfaceId`.
|
|
89
|
+
|
|
90
|
+
By Template ID:
|
|
91
|
+
```javascript
|
|
92
|
+
const contracts = await provider.getActiveContracts({
|
|
93
|
+
templateId: '#splice-amulet:Splice.Amulet:Amulet'
|
|
94
|
+
});
|
|
95
|
+
console.log(contracts);
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
By Interface ID:
|
|
99
|
+
```javascript
|
|
100
|
+
const contracts = await provider.getActiveContracts({
|
|
101
|
+
interfaceId: '#splice-api-token-holding-v1:Splice.Api.Token.HoldingV1:Holding'
|
|
102
|
+
});
|
|
103
|
+
console.log(contracts);
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
#### Submit a Transaction
|
|
107
|
+
|
|
108
|
+
To submit a DAML transaction, you need to construct a command object and pass it to `submitTransaction`:
|
|
109
|
+
|
|
110
|
+
```javascript
|
|
111
|
+
const damlCommand = {
|
|
112
|
+
commands: [{
|
|
113
|
+
ExerciseCommand: {
|
|
114
|
+
templateId: "#splice-api-token-transfer-instruction-v1:Splice.Api.Token.TransferInstructionV1:TransferFactory",
|
|
115
|
+
contractId: 'your-contract-id', // The contract ID to exercise the choice on
|
|
116
|
+
choice: 'TransferFactory_Transfer',
|
|
117
|
+
choiceArgument: {
|
|
118
|
+
// ... your choice arguments
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}],
|
|
122
|
+
// ... other command properties
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const result = await provider.submitTransaction(damlCommand);
|
|
127
|
+
console.log('Transaction successful:', result);
|
|
128
|
+
} catch (error) {
|
|
129
|
+
console.error('Transaction failed:', error);
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
#### Sign a Message
|
|
134
|
+
|
|
135
|
+
You can request the user to sign an arbitrary message:
|
|
136
|
+
|
|
137
|
+
```javascript
|
|
138
|
+
const message = 'Hello, Loop!';
|
|
139
|
+
try {
|
|
140
|
+
const signature = await provider.signMessage(message);
|
|
141
|
+
console.log('Signature:', signature);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
console.error('Signing failed:', error);
|
|
144
|
+
}
|
|
145
|
+
```
|
|
14
146
|
|
|
15
147
|
# API
|
|
16
148
|
|
|
17
149
|
|
|
150
|
+
|
|
18
151
|
# Development Guide
|
|
19
152
|
|
|
20
153
|
This section is only if you want to actively develop the SDK itself. To use the SDK, follow the `#Usage Guide` section
|
|
@@ -36,4 +169,5 @@ bun run index.ts
|
|
|
36
169
|
|
|
37
170
|
```
|
|
38
171
|
bun run build
|
|
172
|
+
bun publish
|
|
39
173
|
```
|
package/dist/index.js
CHANGED
|
@@ -2080,14 +2080,17 @@ class Connection {
|
|
|
2080
2080
|
this.walletUrl = "http://localhost:3000";
|
|
2081
2081
|
this.apiUrl = "http://localhost:8080";
|
|
2082
2082
|
break;
|
|
2083
|
+
case "devnet":
|
|
2083
2084
|
case "dev":
|
|
2084
2085
|
this.walletUrl = "https://devnet.cantonloop.com";
|
|
2085
2086
|
this.apiUrl = "https://devnet.cantonloop.com";
|
|
2086
2087
|
break;
|
|
2088
|
+
case "testnet":
|
|
2087
2089
|
case "test":
|
|
2088
2090
|
this.walletUrl = "https://testnet.cantonloop.com";
|
|
2089
2091
|
this.apiUrl = "https://testnet.cantonloop.com";
|
|
2090
2092
|
break;
|
|
2093
|
+
case "mainnet":
|
|
2091
2094
|
case "main":
|
|
2092
2095
|
this.walletUrl = "https://cantonloop.com";
|
|
2093
2096
|
this.apiUrl = "https://cantonloop.com";
|
|
@@ -2161,7 +2164,16 @@ class Connection {
|
|
|
2161
2164
|
if (!response.ok) {
|
|
2162
2165
|
throw new Error("Session verification failed.");
|
|
2163
2166
|
}
|
|
2164
|
-
|
|
2167
|
+
const data = await response.json();
|
|
2168
|
+
if (!data?.party_id || !data?.public_key) {
|
|
2169
|
+
throw new Error("Invalid session verification response.");
|
|
2170
|
+
}
|
|
2171
|
+
const account = {
|
|
2172
|
+
party_id: data?.party_id,
|
|
2173
|
+
auth_token: authToken,
|
|
2174
|
+
public_key: data?.public_key
|
|
2175
|
+
};
|
|
2176
|
+
return account;
|
|
2165
2177
|
}
|
|
2166
2178
|
websocketUrl(ticketId) {
|
|
2167
2179
|
return `${this.network === "local" ? "ws" : "wss"}://${this.apiUrl.replace("https://", "").replace("http://", "")}/api/v1/.connect/pair/ws/${ticketId}`;
|
|
@@ -2200,15 +2212,17 @@ function generateRequestId() {
|
|
|
2200
2212
|
class Provider {
|
|
2201
2213
|
connection;
|
|
2202
2214
|
party_id;
|
|
2215
|
+
public_key;
|
|
2203
2216
|
auth_token;
|
|
2204
2217
|
requests = new Map;
|
|
2205
2218
|
requestTimeout = 30000;
|
|
2206
|
-
constructor({ connection, party_id, auth_token, requestTimeout }) {
|
|
2219
|
+
constructor({ connection, party_id, public_key, auth_token, requestTimeout }) {
|
|
2207
2220
|
if (!connection) {
|
|
2208
2221
|
throw new Error("Provider requires a connection object.");
|
|
2209
2222
|
}
|
|
2210
2223
|
this.connection = connection;
|
|
2211
2224
|
this.party_id = party_id;
|
|
2225
|
+
this.public_key = public_key;
|
|
2212
2226
|
this.auth_token = auth_token;
|
|
2213
2227
|
this.requestTimeout = requestTimeout || 30000;
|
|
2214
2228
|
}
|
|
@@ -2294,21 +2308,20 @@ class LoopSDK {
|
|
|
2294
2308
|
const existingConnectionRaw = localStorage.getItem("loop_connect");
|
|
2295
2309
|
if (existingConnectionRaw) {
|
|
2296
2310
|
try {
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
if (authToken && partyId) {
|
|
2311
|
+
const { ticketId, authToken, partyId, publicKey } = JSON.parse(existingConnectionRaw);
|
|
2312
|
+
if (authToken && partyId && publicKey) {
|
|
2300
2313
|
try {
|
|
2301
2314
|
const verifiedAccount = await this.connection.verifySession(authToken);
|
|
2302
2315
|
if (verifiedAccount.party_id === partyId) {
|
|
2303
|
-
this.provider = new Provider({ connection: this.connection, party_id: partyId, auth_token: authToken });
|
|
2316
|
+
this.provider = new Provider({ connection: this.connection, party_id: partyId, auth_token: authToken, public_key: publicKey });
|
|
2304
2317
|
this.onAccept?.(this.provider);
|
|
2305
2318
|
if (ticketId) {
|
|
2306
2319
|
this.connection.connectWebSocket(ticketId, this.handleWebSocketMessage.bind(this));
|
|
2307
2320
|
}
|
|
2308
2321
|
return;
|
|
2309
2322
|
}
|
|
2310
|
-
} catch (
|
|
2311
|
-
console.
|
|
2323
|
+
} catch (err) {
|
|
2324
|
+
console.error("Auto-login failed, token is invalid. Starting new connection.", err);
|
|
2312
2325
|
}
|
|
2313
2326
|
}
|
|
2314
2327
|
if (ticketId) {
|
|
@@ -2323,7 +2336,7 @@ class LoopSDK {
|
|
|
2323
2336
|
}
|
|
2324
2337
|
localStorage.removeItem("loop_connect");
|
|
2325
2338
|
}
|
|
2326
|
-
const sessionId =
|
|
2339
|
+
const sessionId = generateRequestId();
|
|
2327
2340
|
try {
|
|
2328
2341
|
const { ticket_id: ticketId } = await this.connection.getTicket(this.appName, sessionId, this.version);
|
|
2329
2342
|
this.ticketId = ticketId;
|
|
@@ -2337,31 +2350,27 @@ class LoopSDK {
|
|
|
2337
2350
|
}
|
|
2338
2351
|
}
|
|
2339
2352
|
handleWebSocketMessage(event) {
|
|
2340
|
-
console.log("Received message:", event.data);
|
|
2341
2353
|
const message = JSON.parse(event.data);
|
|
2342
2354
|
if (message.type === "handshake_accept" /* HANDSHAKE_ACCEPT */) {
|
|
2343
|
-
const { authToken, partyId } = message.payload || {};
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
this.provider = new Provider({ connection: this.connection, party_id: partyId, auth_token: authToken });
|
|
2347
|
-
console.log("Handshake accepted, token and account received.");
|
|
2355
|
+
const { authToken, partyId, publicKey } = message.payload || {};
|
|
2356
|
+
if (authToken && partyId && publicKey) {
|
|
2357
|
+
this.provider = new Provider({ connection: this.connection, party_id: partyId, auth_token: authToken, public_key: publicKey });
|
|
2348
2358
|
const connectionInfoRaw = localStorage.getItem("loop_connect");
|
|
2349
2359
|
if (connectionInfoRaw) {
|
|
2350
2360
|
try {
|
|
2351
2361
|
const connectionInfo = JSON.parse(connectionInfoRaw);
|
|
2352
2362
|
connectionInfo.authToken = authToken;
|
|
2353
2363
|
connectionInfo.partyId = partyId;
|
|
2364
|
+
connectionInfo.publicKey = publicKey;
|
|
2354
2365
|
localStorage.setItem("loop_connect", JSON.stringify(connectionInfo));
|
|
2355
2366
|
this.onAccept?.(this.provider);
|
|
2356
2367
|
this.hideQrCode();
|
|
2357
|
-
this.connection?.connectWebSocket(connectionInfo.ticketId, this.handleWebSocketMessage.bind(this));
|
|
2358
2368
|
} catch (error) {
|
|
2359
2369
|
console.error("Failed to update local storage with auth token.", error);
|
|
2360
2370
|
}
|
|
2361
2371
|
}
|
|
2362
2372
|
}
|
|
2363
2373
|
} else if (message.type === "handshake_reject" /* HANDSHAKE_REJECT */) {
|
|
2364
|
-
console.log("Connection rejected.");
|
|
2365
2374
|
localStorage.removeItem("loop_connect");
|
|
2366
2375
|
this.connection?.ws?.close();
|
|
2367
2376
|
this.onReject?.();
|
|
@@ -2416,9 +2425,6 @@ class LoopSDK {
|
|
|
2416
2425
|
this.overlay = null;
|
|
2417
2426
|
}
|
|
2418
2427
|
}
|
|
2419
|
-
generateSessionId() {
|
|
2420
|
-
return crypto.randomUUID();
|
|
2421
|
-
}
|
|
2422
2428
|
}
|
|
2423
2429
|
var loop = new LoopSDK;
|
|
2424
2430
|
export {
|
package/package.json
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
1
|
{
|
|
2
|
-
"author": "hello@fivenorth.io",
|
|
3
2
|
"name": "@fivenorth/loop-sdk",
|
|
4
|
-
"version": "0.1.
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"author": "hello@fivenorth.io",
|
|
6
5
|
"main": "dist/index.js",
|
|
7
|
-
"
|
|
8
|
-
"type": "module",
|
|
9
|
-
"scripts": {
|
|
10
|
-
"build": "bun build ./src/index.ts --outdir ./dist",
|
|
11
|
-
"prepublishOnly": "bun run build",
|
|
12
|
-
"start": "bun run src/server.ts"
|
|
13
|
-
},
|
|
6
|
+
"module": "dist/index.js",
|
|
14
7
|
"devDependencies": {
|
|
15
8
|
"@types/bun": "latest"
|
|
16
9
|
},
|
|
17
10
|
"peerDependencies": {
|
|
18
11
|
"typescript": "^5"
|
|
19
12
|
},
|
|
20
|
-
"dependencies": {
|
|
21
|
-
"@types/qrcode": "^1.5.5",
|
|
22
|
-
"qrcode": "^1.5.4"
|
|
23
|
-
},
|
|
24
13
|
"files": [
|
|
25
14
|
"dist"
|
|
26
15
|
],
|
|
27
16
|
"publishConfig": {
|
|
28
17
|
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "bun build ./src/index.ts --outdir ./dist",
|
|
21
|
+
"prepublishOnly": "bun run build",
|
|
22
|
+
"start": "bun run src/server.ts"
|
|
23
|
+
},
|
|
24
|
+
"type": "module",
|
|
25
|
+
"types": "dist/index.d.ts",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@types/qrcode": "^1.5.6",
|
|
28
|
+
"qrcode": "^1.5.4"
|
|
29
29
|
}
|
|
30
30
|
}
|