@fivenorth/loop-sdk 0.10.0 → 0.11.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 +70 -0
- package/dist/connection.d.ts +40 -0
- package/dist/connection.d.ts.map +1 -0
- package/dist/connection.js +290 -0
- package/dist/errors.d.ts +13 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +33 -0
- package/dist/extensions/usdc/index.d.ts +11 -0
- package/dist/extensions/usdc/index.d.ts.map +1 -0
- package/dist/extensions/usdc/index.js +52 -0
- package/dist/extensions/usdc/types.d.ts +25 -0
- package/dist/extensions/usdc/types.d.ts.map +1 -0
- package/dist/extensions/usdc/types.js +1 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +284 -229
- package/dist/provider.d.ts +61 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +247 -0
- package/dist/server/index.d.ts +23 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +117 -0
- package/dist/server/signer.d.ts +15 -0
- package/dist/server/signer.d.ts.map +1 -0
- package/dist/server/signer.js +56 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +38 -0
- package/dist/session.d.ts +39 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +92 -0
- package/dist/types.d.ts +116 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +10 -0
- package/dist/wallet.d.ts +10 -0
- package/dist/wallet.d.ts.map +1 -0
- package/dist/wallet.js +22 -0
- package/package.json +21 -4
package/README.md
CHANGED
|
@@ -237,6 +237,76 @@ Notes:
|
|
|
237
237
|
|
|
238
238
|
Coming soon
|
|
239
239
|
|
|
240
|
+
## Loop Server Signing API
|
|
241
|
+
|
|
242
|
+
Loop SDK also supports a server-side signing flow. Instead of a wallet popup, your backend signs and submits transactions directly using the user's private key.
|
|
243
|
+
|
|
244
|
+
### Installation
|
|
245
|
+
|
|
246
|
+
For server-side usage, you need to install the SDK and `node-forge`:
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
bun add @fivenorth/loop-sdk node-forge
|
|
250
|
+
# or
|
|
251
|
+
npm install @fivenorth/loop-sdk node-forge
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
**Note:** `node-forge` is a peer dependency and must be installed manually when using the server SDK. It's not required for browser usage.
|
|
255
|
+
|
|
256
|
+
### Usage
|
|
257
|
+
|
|
258
|
+
```javascript
|
|
259
|
+
import { loop } from '@fivenorth/loop-sdk/server';
|
|
260
|
+
|
|
261
|
+
// Initialize with private key
|
|
262
|
+
loop.init({
|
|
263
|
+
privateKey: process.env.PRIVATE_KEY, // hex-encoded Ed25519 private key
|
|
264
|
+
partyId: process.env.PARTY_ID, // your party ID
|
|
265
|
+
network: 'local', // or 'devnet', 'mainnet'
|
|
266
|
+
walletUrl: process.env.WALLET_URL, // optional
|
|
267
|
+
apiUrl: process.env.API_URL, // optional
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// Authenticate to get API access
|
|
271
|
+
await loop.authenticate();
|
|
272
|
+
|
|
273
|
+
// Get the provider to interact with the ledger
|
|
274
|
+
const provider = loop.getProvider();
|
|
275
|
+
|
|
276
|
+
// List holdings
|
|
277
|
+
const holdings = await provider.getHolding();
|
|
278
|
+
console.log(holdings);
|
|
279
|
+
|
|
280
|
+
// Get active contracts
|
|
281
|
+
const contracts = await provider.getActiveContracts({
|
|
282
|
+
templateId: '#splice-amulet:Splice.Amulet:Amulet'
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// Transfer tokens
|
|
286
|
+
const preparedPayload = await provider.transfer(
|
|
287
|
+
'recipient::partyId',
|
|
288
|
+
1,
|
|
289
|
+
{
|
|
290
|
+
instrument_admin: '',
|
|
291
|
+
instrument_id: 'Amulet',
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
requestedAt: new Date(),
|
|
295
|
+
executeBefore: new Date(Date.now() + 24 * 60 * 60 * 1000),
|
|
296
|
+
}
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
// Execute the transaction
|
|
300
|
+
const result = await loop.executeTransaction(preparedPayload);
|
|
301
|
+
console.log('Transfer result:', result);
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Example ideas:
|
|
305
|
+
- List pending transfers
|
|
306
|
+
- Accept a pending transfer
|
|
307
|
+
- Automated transaction processing
|
|
308
|
+
|
|
309
|
+
|
|
240
310
|
# Development Guide
|
|
241
311
|
|
|
242
312
|
This section is only if you want to actively develop the SDK itself. To use the SDK, follow the `#Usage Guide` section
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Network, Account, Holding, TransferRequest, PreparedTransferPayload, ExchangeApiKeyResponse, TransactionPayload, PreparedSubmissionResponse, ExecuteSubmissionResquest } from './types';
|
|
2
|
+
import { SessionInfo } from './session';
|
|
3
|
+
export declare class Connection {
|
|
4
|
+
walletUrl: string;
|
|
5
|
+
apiUrl: string;
|
|
6
|
+
ws: WebSocket | null;
|
|
7
|
+
private network;
|
|
8
|
+
private ticketId;
|
|
9
|
+
private onMessageHandler;
|
|
10
|
+
private reconnectPromise;
|
|
11
|
+
private status;
|
|
12
|
+
constructor({ network, walletUrl, apiUrl }: {
|
|
13
|
+
network?: Network;
|
|
14
|
+
walletUrl?: string;
|
|
15
|
+
apiUrl?: string;
|
|
16
|
+
});
|
|
17
|
+
connectInProgress(): boolean;
|
|
18
|
+
getTicket(appName: string, sessionId: string, version: string): Promise<{
|
|
19
|
+
ticket_id: string;
|
|
20
|
+
}>;
|
|
21
|
+
getHolding(authToken: string): Promise<Holding[]>;
|
|
22
|
+
getActiveContracts(authToken: string, params?: {
|
|
23
|
+
templateId?: string;
|
|
24
|
+
interfaceId?: string;
|
|
25
|
+
}): Promise<any[]>;
|
|
26
|
+
prepareTransfer(authToken: string, params: TransferRequest): Promise<PreparedTransferPayload>;
|
|
27
|
+
verifySession(authToken: string): Promise<Account>;
|
|
28
|
+
connectWebSocket(ticketId: string, onMessage: (event: MessageEvent) => void): void;
|
|
29
|
+
reconnect(): Promise<void>;
|
|
30
|
+
exchangeApiKey({ publicKey, signature, epoch }: {
|
|
31
|
+
publicKey: string;
|
|
32
|
+
signature: string;
|
|
33
|
+
epoch: number;
|
|
34
|
+
}): Promise<ExchangeApiKeyResponse>;
|
|
35
|
+
prepareTransaction(session: SessionInfo, params: TransactionPayload): Promise<PreparedSubmissionResponse>;
|
|
36
|
+
executeTransaction(session: SessionInfo, params: ExecuteSubmissionResquest): Promise<PreparedSubmissionResponse>;
|
|
37
|
+
private websocketUrl;
|
|
38
|
+
private attachWebSocket;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=connection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../src/connection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACR,OAAO,EACP,OAAO,EACP,OAAO,EACP,eAAe,EACf,uBAAuB,EAEvB,sBAAsB,EACtB,kBAAkB,EAClB,0BAA0B,EAC1B,yBAAyB,EAC5B,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAGxC,qBAAa,UAAU;IACZ,SAAS,EAAE,MAAM,CAA4B;IAC7C,MAAM,EAAE,MAAM,CAA4B;IAC1C,EAAE,EAAE,SAAS,GAAG,IAAI,CAAQ;IACnC,OAAO,CAAC,OAAO,CAAmB;IAClC,OAAO,CAAC,QAAQ,CAAuB;IACvC,OAAO,CAAC,gBAAgB,CAAgD;IACxE,OAAO,CAAC,gBAAgB,CAA8B;IACtD,OAAO,CAAC,MAAM,CAA+D;gBAEjE,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE;IAmCtG,iBAAiB,IAAI,OAAO;IAItB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IAoB9F,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;IAgBjD,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE;QAAE,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;IAwB7G,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,uBAAuB,CAAC;IA4C7F,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAmCxD,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI;IAwB3E,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IA+BpB,cAAc,CAAC,EAAC,SAAS,EAAE,SAAS,EAAE,KAAK,EAAC,EAAE;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAC,GAAG,OAAO,CAAC,sBAAsB,CAAC;IAqB3I,kBAAkB,CAAC,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,0BAA0B,CAAC;IAenG,kBAAkB,CAAC,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,yBAAyB,GAAG,OAAO,CAAC,0BAA0B,CAAC;IAwBtH,OAAO,CAAC,YAAY;IAKpB,OAAO,CAAC,eAAe;CAqC1B"}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { UnauthorizedError } from './errors';
|
|
2
|
+
import { generateRequestId } from './provider';
|
|
3
|
+
export class Connection {
|
|
4
|
+
walletUrl = 'https://cantonloop.com';
|
|
5
|
+
apiUrl = 'https://cantonloop.com';
|
|
6
|
+
ws = null;
|
|
7
|
+
network = 'main';
|
|
8
|
+
ticketId = null;
|
|
9
|
+
onMessageHandler = null;
|
|
10
|
+
reconnectPromise = null;
|
|
11
|
+
status = 'disconnected';
|
|
12
|
+
constructor({ network, walletUrl, apiUrl }) {
|
|
13
|
+
this.network = network || 'main';
|
|
14
|
+
// Set default common value based on network
|
|
15
|
+
switch (this.network) {
|
|
16
|
+
case 'local':
|
|
17
|
+
this.walletUrl = 'http://localhost:3000';
|
|
18
|
+
this.apiUrl = 'http://localhost:8080';
|
|
19
|
+
break;
|
|
20
|
+
case 'devnet':
|
|
21
|
+
case 'dev':
|
|
22
|
+
this.walletUrl = 'https://devnet.cantonloop.com';
|
|
23
|
+
this.apiUrl = 'https://devnet.cantonloop.com';
|
|
24
|
+
break;
|
|
25
|
+
case 'testnet':
|
|
26
|
+
case 'test':
|
|
27
|
+
this.walletUrl = 'https://testnet.cantonloop.com';
|
|
28
|
+
this.apiUrl = 'https://testnet.cantonloop.com';
|
|
29
|
+
break;
|
|
30
|
+
case 'mainnet':
|
|
31
|
+
case 'main':
|
|
32
|
+
this.walletUrl = 'https://cantonloop.com';
|
|
33
|
+
this.apiUrl = 'https://cantonloop.com';
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
// More useful when developing locally
|
|
37
|
+
if (walletUrl) {
|
|
38
|
+
this.walletUrl = walletUrl;
|
|
39
|
+
}
|
|
40
|
+
if (apiUrl) {
|
|
41
|
+
this.apiUrl = apiUrl;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
connectInProgress() {
|
|
45
|
+
return this.status === 'connecting' || this.status === 'connected';
|
|
46
|
+
}
|
|
47
|
+
async getTicket(appName, sessionId, version) {
|
|
48
|
+
const response = await fetch(`${this.apiUrl}/api/v1/.connect/pair/tickets`, {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers: {
|
|
51
|
+
'Content-Type': 'application/json',
|
|
52
|
+
},
|
|
53
|
+
body: JSON.stringify({
|
|
54
|
+
app_name: appName,
|
|
55
|
+
session_id: sessionId,
|
|
56
|
+
version: version,
|
|
57
|
+
}),
|
|
58
|
+
});
|
|
59
|
+
if (!response.ok) {
|
|
60
|
+
throw new Error('Failed to get ticket from server.');
|
|
61
|
+
}
|
|
62
|
+
return response.json();
|
|
63
|
+
}
|
|
64
|
+
async getHolding(authToken) {
|
|
65
|
+
const response = await fetch(`${this.apiUrl}/api/v1/.connect/pair/account/holding`, {
|
|
66
|
+
method: 'GET',
|
|
67
|
+
headers: {
|
|
68
|
+
'Content-Type': 'application/json',
|
|
69
|
+
'Authorization': `Bearer ${authToken}`,
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
throw new Error('Failed to get holdings. ' + await response.text());
|
|
74
|
+
}
|
|
75
|
+
return response.json();
|
|
76
|
+
}
|
|
77
|
+
async getActiveContracts(authToken, params) {
|
|
78
|
+
const url = new URL(`${this.apiUrl}/api/v1/.connect/pair/account/active-contracts`);
|
|
79
|
+
if (params?.templateId) {
|
|
80
|
+
url.searchParams.append('templateId', params.templateId);
|
|
81
|
+
}
|
|
82
|
+
if (params?.interfaceId) {
|
|
83
|
+
url.searchParams.append('interfaceId', params.interfaceId);
|
|
84
|
+
}
|
|
85
|
+
const response = await fetch(url.toString(), {
|
|
86
|
+
method: 'GET',
|
|
87
|
+
headers: {
|
|
88
|
+
'Content-Type': 'application/json',
|
|
89
|
+
'Authorization': `Bearer ${authToken}`,
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
if (!response.ok) {
|
|
93
|
+
throw new Error('Failed to get active contracts.');
|
|
94
|
+
}
|
|
95
|
+
return response.json();
|
|
96
|
+
}
|
|
97
|
+
async prepareTransfer(authToken, params) {
|
|
98
|
+
const payload = {
|
|
99
|
+
recipient: params.recipient,
|
|
100
|
+
amount: params.amount,
|
|
101
|
+
};
|
|
102
|
+
if (params.instrument) {
|
|
103
|
+
if (params.instrument.instrument_admin) {
|
|
104
|
+
payload.instrument_admin = params.instrument.instrument_admin;
|
|
105
|
+
}
|
|
106
|
+
if (params.instrument.instrument_id) {
|
|
107
|
+
payload.instrument_id = params.instrument.instrument_id;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (params.requested_at) {
|
|
111
|
+
payload.requested_at = params.requested_at;
|
|
112
|
+
}
|
|
113
|
+
if (params.execute_before) {
|
|
114
|
+
payload.execute_before = params.execute_before;
|
|
115
|
+
}
|
|
116
|
+
if (params.memo) {
|
|
117
|
+
payload.memo = params.memo;
|
|
118
|
+
}
|
|
119
|
+
const response = await fetch(`${this.apiUrl}/api/v1/.connect/pair/transfer`, {
|
|
120
|
+
method: 'POST',
|
|
121
|
+
headers: {
|
|
122
|
+
'Content-Type': 'application/json',
|
|
123
|
+
'Authorization': `Bearer ${authToken}`,
|
|
124
|
+
},
|
|
125
|
+
body: JSON.stringify(payload),
|
|
126
|
+
});
|
|
127
|
+
if (!response.ok) {
|
|
128
|
+
console.error('Failed to prepare transfer.', await response.text());
|
|
129
|
+
throw new Error('Failed to prepare transfer.');
|
|
130
|
+
}
|
|
131
|
+
const data = await response.json();
|
|
132
|
+
return data.payload;
|
|
133
|
+
}
|
|
134
|
+
async verifySession(authToken) {
|
|
135
|
+
const response = await fetch(`${this.apiUrl}/api/v1/.connect/pair/account`, {
|
|
136
|
+
method: 'GET',
|
|
137
|
+
headers: {
|
|
138
|
+
'Content-Type': 'application/json',
|
|
139
|
+
'Authorization': `Bearer ${authToken}`,
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
if (!response.ok) {
|
|
143
|
+
if (response.status === 401 || response.status === 403) {
|
|
144
|
+
throw new UnauthorizedError();
|
|
145
|
+
}
|
|
146
|
+
throw new Error(`Session verification failed with status ${response.status}.`);
|
|
147
|
+
}
|
|
148
|
+
const data = await response.json();
|
|
149
|
+
const email = data?.email;
|
|
150
|
+
if (!data?.party_id || !data?.public_key) {
|
|
151
|
+
throw new Error('Invalid session verification response.');
|
|
152
|
+
}
|
|
153
|
+
// Map fields from the response to the account object, handling camelCase and snake_case.
|
|
154
|
+
const account = {
|
|
155
|
+
party_id: data?.party_id,
|
|
156
|
+
auth_token: authToken,
|
|
157
|
+
public_key: data?.public_key,
|
|
158
|
+
email,
|
|
159
|
+
has_preapproval: data?.has_preapproval,
|
|
160
|
+
has_merge_delegation: data?.has_merge_delegation,
|
|
161
|
+
usdc_bridge_access: data?.usdc_bridge_access,
|
|
162
|
+
};
|
|
163
|
+
return account;
|
|
164
|
+
}
|
|
165
|
+
connectWebSocket(ticketId, onMessage) {
|
|
166
|
+
if (this.ws &&
|
|
167
|
+
(this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) &&
|
|
168
|
+
this.ticketId !== ticketId) {
|
|
169
|
+
// When connecting to a new ticket, we need to close the existing socket first
|
|
170
|
+
this.ws.close();
|
|
171
|
+
this.ws = null;
|
|
172
|
+
}
|
|
173
|
+
// prevent opening multiple sockets for same ticket
|
|
174
|
+
if (this.status === 'connecting' || this.status === 'connected') {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
// set the message handler and ticket id to re-use for reconnecting
|
|
178
|
+
this.onMessageHandler = onMessage;
|
|
179
|
+
this.ticketId = ticketId;
|
|
180
|
+
this.status = 'connecting';
|
|
181
|
+
this.attachWebSocket(ticketId, onMessage);
|
|
182
|
+
}
|
|
183
|
+
reconnect() {
|
|
184
|
+
if (!this.ticketId || !this.onMessageHandler) {
|
|
185
|
+
return Promise.reject(new Error('Cannot reconnect without a known ticket.'));
|
|
186
|
+
}
|
|
187
|
+
return new Promise((resolve, reject) => {
|
|
188
|
+
let opened = false;
|
|
189
|
+
this.attachWebSocket(this.ticketId, this.onMessageHandler, () => {
|
|
190
|
+
opened = true;
|
|
191
|
+
resolve();
|
|
192
|
+
}, () => {
|
|
193
|
+
if (opened) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
reject(new Error('Failed to reconnect to ticket server.'));
|
|
197
|
+
}, () => {
|
|
198
|
+
if (opened) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
reject(new Error('Failed to reconnect to ticket server.'));
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
// exchangeApiKey is used to exchange the API key for the public key and signature to use in a server session
|
|
206
|
+
async exchangeApiKey({ publicKey, signature, epoch }) {
|
|
207
|
+
const response = await fetch(`${this.apiUrl}/api/v1/.connect/pair/apikey`, {
|
|
208
|
+
method: 'POST',
|
|
209
|
+
headers: {
|
|
210
|
+
'Content-Type': 'application/json',
|
|
211
|
+
},
|
|
212
|
+
body: JSON.stringify({
|
|
213
|
+
public_key: publicKey,
|
|
214
|
+
signature: signature,
|
|
215
|
+
epoch: epoch,
|
|
216
|
+
})
|
|
217
|
+
});
|
|
218
|
+
if (!response.ok) {
|
|
219
|
+
throw new Error('Failed to get API key from server.');
|
|
220
|
+
}
|
|
221
|
+
return response.json();
|
|
222
|
+
}
|
|
223
|
+
// send transaction to v2/interactive-submisison/prepare endpoint to get the prepared transaction
|
|
224
|
+
prepareTransaction(session, params) {
|
|
225
|
+
return fetch(`${this.apiUrl}/api/v1/.connect/tickets/prepare-transaction`, {
|
|
226
|
+
method: 'POST',
|
|
227
|
+
headers: {
|
|
228
|
+
'Content-Type': 'application/json',
|
|
229
|
+
'Authorization': `Bearer ${session.userApiKey}`,
|
|
230
|
+
},
|
|
231
|
+
body: JSON.stringify({
|
|
232
|
+
payload: params,
|
|
233
|
+
ticket_id: session.ticketId,
|
|
234
|
+
})
|
|
235
|
+
}).then(response => response.json());
|
|
236
|
+
}
|
|
237
|
+
// execute a signed transaction with v2/interactive-submisison/execute endpoint
|
|
238
|
+
async executeTransaction(session, params) {
|
|
239
|
+
if (!session.ticketId) {
|
|
240
|
+
throw new Error('Ticket ID is required');
|
|
241
|
+
}
|
|
242
|
+
const resp = fetch(`${this.apiUrl}/api/v1/.connect/tickets/execute-transaction`, {
|
|
243
|
+
method: 'POST',
|
|
244
|
+
headers: {
|
|
245
|
+
'Content-Type': 'application/json',
|
|
246
|
+
'Authorization': `Bearer ${session.userApiKey}`,
|
|
247
|
+
},
|
|
248
|
+
body: JSON.stringify({
|
|
249
|
+
ticket_id: session.ticketId,
|
|
250
|
+
request_id: generateRequestId(),
|
|
251
|
+
command_id: params.command_id,
|
|
252
|
+
signature: params.signature,
|
|
253
|
+
transaction_data: params.transaction_data,
|
|
254
|
+
}),
|
|
255
|
+
});
|
|
256
|
+
return (await resp).json();
|
|
257
|
+
}
|
|
258
|
+
websocketUrl(ticketId) {
|
|
259
|
+
return `${this.network === 'local' ? 'ws' : 'wss'}://${this.apiUrl.replace('https://', '').replace('http://', '')}/api/v1/.connect/pair/ws/${encodeURIComponent(ticketId)}`;
|
|
260
|
+
}
|
|
261
|
+
// attachWebSocket is a helper function to setup even handler on a websocket object and assign to our ws
|
|
262
|
+
attachWebSocket(ticketId, onMessage, onOpen, onError, onClose) {
|
|
263
|
+
const wsUrl = this.websocketUrl(ticketId);
|
|
264
|
+
const ws = new WebSocket(wsUrl);
|
|
265
|
+
ws.onmessage = onMessage;
|
|
266
|
+
ws.onopen = () => {
|
|
267
|
+
this.status = 'connected';
|
|
268
|
+
console.log('[LoopSDK] Connected to ticket server.');
|
|
269
|
+
onOpen?.();
|
|
270
|
+
};
|
|
271
|
+
ws.onclose = (event) => {
|
|
272
|
+
this.status = 'disconnected';
|
|
273
|
+
if (this.ws === ws) {
|
|
274
|
+
this.ws = null;
|
|
275
|
+
}
|
|
276
|
+
console.log('[LoopSDK] Disconnected from ticket server.');
|
|
277
|
+
onClose?.(event);
|
|
278
|
+
};
|
|
279
|
+
ws.onerror = (event) => {
|
|
280
|
+
// if it's already close, another close is a no-op
|
|
281
|
+
this.status = 'disconnected';
|
|
282
|
+
ws.close();
|
|
283
|
+
if (this.ws === ws) {
|
|
284
|
+
this.ws = null;
|
|
285
|
+
}
|
|
286
|
+
onError?.(event);
|
|
287
|
+
};
|
|
288
|
+
this.ws = ws;
|
|
289
|
+
}
|
|
290
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare class RequestTimeoutError extends Error {
|
|
2
|
+
constructor(timeout: number);
|
|
3
|
+
}
|
|
4
|
+
export declare class RejectRequestError extends Error {
|
|
5
|
+
constructor();
|
|
6
|
+
}
|
|
7
|
+
export declare class UnauthorizedError extends Error {
|
|
8
|
+
code?: string;
|
|
9
|
+
constructor(code?: string);
|
|
10
|
+
}
|
|
11
|
+
export declare function extractErrorCode(message: any): string | null;
|
|
12
|
+
export declare function isUnauthCode(code: string | null | undefined): code is string;
|
|
13
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,qBAAa,mBAAoB,SAAQ,KAAK;gBAC9B,OAAO,EAAE,MAAM;CAG9B;AAED,qBAAa,kBAAmB,SAAQ,KAAK;;CAI5C;AAED,qBAAa,iBAAkB,SAAQ,KAAK;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;gBACT,IAAI,CAAC,EAAE,MAAM;CAI5B;AAID,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,GAAG,GAAG,MAAM,GAAG,IAAI,CAQ5D;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI,IAAI,MAAM,CAK5E"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export class RequestTimeoutError extends Error {
|
|
2
|
+
constructor(timeout) {
|
|
3
|
+
super(`Request timed out after ${timeout}ms.`);
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
export class RejectRequestError extends Error {
|
|
7
|
+
constructor() {
|
|
8
|
+
super('Request was rejected by the wallet.');
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export class UnauthorizedError extends Error {
|
|
12
|
+
code;
|
|
13
|
+
constructor(code) {
|
|
14
|
+
super(code || 'Unauthorized');
|
|
15
|
+
this.code = code;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const UNAUTH_CODES = new Set(['UNAUTHENTICATED', 'UNAUTHORIZED', 'SESSION_EXPIRED', 'LOGGED_OUT']);
|
|
19
|
+
export function extractErrorCode(message) {
|
|
20
|
+
if (typeof message?.error?.code === 'string' && message.error.code.length > 0) {
|
|
21
|
+
return message.error.code;
|
|
22
|
+
}
|
|
23
|
+
if (message?.type === 'unauthorized' && typeof message?.code === 'string') {
|
|
24
|
+
return message.code;
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
export function isUnauthCode(code) {
|
|
29
|
+
if (!code) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
return UNAUTH_CODES.has(code);
|
|
33
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { WithdrawOptions, UsdcBridgeExtension, WithdrawUsdcRequest, PreparedWithdrawPayload } from './types';
|
|
2
|
+
import type { Provider } from '../../provider';
|
|
3
|
+
import type { Connection } from '../../connection';
|
|
4
|
+
export declare class UsdcBridge implements UsdcBridgeExtension {
|
|
5
|
+
private getProvider;
|
|
6
|
+
constructor(getProvider: () => Provider | null);
|
|
7
|
+
private requireProvider;
|
|
8
|
+
withdrawalUSDCxToEthereum(recipient: string, amount: string | number, options?: WithdrawOptions): Promise<any>;
|
|
9
|
+
}
|
|
10
|
+
export declare function prepareUsdcWithdraw(connection: Connection, authToken: string, params: WithdrawUsdcRequest): Promise<PreparedWithdrawPayload>;
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/extensions/usdc/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,uBAAuB,EAA2B,MAAM,SAAS,CAAC;AAC3I,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,qBAAa,UAAW,YAAW,mBAAmB;IACpD,OAAO,CAAC,WAAW,CAAwB;gBAE/B,WAAW,EAAE,MAAM,QAAQ,GAAG,IAAI;IAI9C,OAAO,CAAC,eAAe;IAQvB,yBAAyB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,GAAG,CAAC;CAwB/G;AAED,wBAAsB,mBAAmB,CAAC,UAAU,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAyBlJ"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export class UsdcBridge {
|
|
2
|
+
getProvider;
|
|
3
|
+
constructor(getProvider) {
|
|
4
|
+
this.getProvider = getProvider;
|
|
5
|
+
}
|
|
6
|
+
requireProvider() {
|
|
7
|
+
const provider = this.getProvider();
|
|
8
|
+
if (!provider) {
|
|
9
|
+
throw new Error('SDK not connected. Call connect() and wait for acceptance first.');
|
|
10
|
+
}
|
|
11
|
+
return provider;
|
|
12
|
+
}
|
|
13
|
+
withdrawalUSDCxToEthereum(recipient, amount, options) {
|
|
14
|
+
const provider = this.requireProvider();
|
|
15
|
+
const amountStr = typeof amount === 'number' ? amount.toString() : amount;
|
|
16
|
+
const withdrawRequest = {
|
|
17
|
+
recipient,
|
|
18
|
+
amount: amountStr,
|
|
19
|
+
reference: options?.reference,
|
|
20
|
+
};
|
|
21
|
+
return prepareUsdcWithdraw(provider.connection, provider.getAuthToken(), withdrawRequest).then((preparedPayload) => provider.submitTransaction({
|
|
22
|
+
commands: preparedPayload.commands,
|
|
23
|
+
disclosedContracts: preparedPayload.disclosedContracts,
|
|
24
|
+
packageIdSelectionPreference: preparedPayload.packageIdSelectionPreference,
|
|
25
|
+
actAs: preparedPayload.actAs,
|
|
26
|
+
readAs: preparedPayload.readAs,
|
|
27
|
+
synchronizerId: preparedPayload.synchronizerId,
|
|
28
|
+
}, { requestTimeout: options?.requestTimeout, message: options?.message }));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export async function prepareUsdcWithdraw(connection, authToken, params) {
|
|
32
|
+
const payload = {
|
|
33
|
+
recipient: params.recipient,
|
|
34
|
+
amount: params.amount,
|
|
35
|
+
};
|
|
36
|
+
if (params.reference) {
|
|
37
|
+
payload.reference = params.reference;
|
|
38
|
+
}
|
|
39
|
+
const response = await fetch(`${connection.apiUrl}/api/v1/.connect/pair/usdc/withdraw`, {
|
|
40
|
+
method: 'POST',
|
|
41
|
+
headers: {
|
|
42
|
+
'Content-Type': 'application/json',
|
|
43
|
+
'Authorization': `Bearer ${authToken}`,
|
|
44
|
+
},
|
|
45
|
+
body: JSON.stringify(payload),
|
|
46
|
+
});
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
throw new Error('Failed to prepare USDC withdrawal.');
|
|
49
|
+
}
|
|
50
|
+
const data = await response.json();
|
|
51
|
+
return data.payload;
|
|
52
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type WithdrawUsdcRequest = {
|
|
2
|
+
recipient: string;
|
|
3
|
+
amount: string;
|
|
4
|
+
reference?: string;
|
|
5
|
+
};
|
|
6
|
+
export type PreparedWithdrawPayload = {
|
|
7
|
+
actAs: string[];
|
|
8
|
+
readAs: string[];
|
|
9
|
+
synchronizerId: string;
|
|
10
|
+
commands: any[];
|
|
11
|
+
disclosedContracts: any[];
|
|
12
|
+
packageIdSelectionPreference: string[];
|
|
13
|
+
};
|
|
14
|
+
export type ConnectWithdrawResponse = {
|
|
15
|
+
payload: PreparedWithdrawPayload;
|
|
16
|
+
};
|
|
17
|
+
export type WithdrawOptions = {
|
|
18
|
+
reference?: string;
|
|
19
|
+
requestTimeout?: number;
|
|
20
|
+
message?: string;
|
|
21
|
+
};
|
|
22
|
+
export interface UsdcBridgeExtension {
|
|
23
|
+
withdrawalUSDCxToEthereum(recipient: string, amount: string | number, options?: WithdrawOptions): Promise<any>;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/extensions/usdc/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,mBAAmB,GAAG;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,GAAG,EAAE,CAAC;IAChB,kBAAkB,EAAE,GAAG,EAAE,CAAC;IAC1B,4BAA4B,EAAE,MAAM,EAAE,CAAC;CACxC,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,OAAO,EAAE,uBAAuB,CAAC;CAClC,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,WAAW,mBAAmB;IAClC,yBAAyB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;CAChH"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Provider } from "./provider";
|
|
2
|
+
import type { Network, Wallet, RunTransactionResponse } from "./types";
|
|
3
|
+
declare class LoopSDK {
|
|
4
|
+
private version;
|
|
5
|
+
private appName;
|
|
6
|
+
private connection;
|
|
7
|
+
private session;
|
|
8
|
+
private provider;
|
|
9
|
+
private openMode;
|
|
10
|
+
private requestSigningMode;
|
|
11
|
+
private popupWindow;
|
|
12
|
+
private redirectUrl?;
|
|
13
|
+
private onAccept;
|
|
14
|
+
private onReject;
|
|
15
|
+
private onTransactionUpdate;
|
|
16
|
+
private overlay;
|
|
17
|
+
wallet: Wallet;
|
|
18
|
+
constructor();
|
|
19
|
+
init({ appName, network, walletUrl, apiUrl, onAccept, onReject, onTransactionUpdate, options, }: {
|
|
20
|
+
appName: string;
|
|
21
|
+
network?: Network;
|
|
22
|
+
walletUrl?: string;
|
|
23
|
+
apiUrl?: string;
|
|
24
|
+
onAccept?: (provider: Provider) => void;
|
|
25
|
+
onReject?: () => void;
|
|
26
|
+
onTransactionUpdate?: (payload: RunTransactionResponse, message: any) => void;
|
|
27
|
+
options?: {
|
|
28
|
+
openMode?: "popup" | "tab";
|
|
29
|
+
requestSigningMode?: "popup" | "tab";
|
|
30
|
+
redirectUrl?: string;
|
|
31
|
+
};
|
|
32
|
+
}): void;
|
|
33
|
+
private loadSessionInfo;
|
|
34
|
+
autoConnect(): Promise<void>;
|
|
35
|
+
connect(): Promise<void>;
|
|
36
|
+
private handleWebSocketMessage;
|
|
37
|
+
getConnectUrl(): string;
|
|
38
|
+
private buildConnectUrl;
|
|
39
|
+
private buildDashboardUrl;
|
|
40
|
+
private openRequestUi;
|
|
41
|
+
private closePopupIfExists;
|
|
42
|
+
private openWallet;
|
|
43
|
+
private injectModalStyles;
|
|
44
|
+
private showQrCode;
|
|
45
|
+
private hideQrCode;
|
|
46
|
+
logout(): void;
|
|
47
|
+
private requireProvider;
|
|
48
|
+
private createProviderHooks;
|
|
49
|
+
}
|
|
50
|
+
export declare const loop: LoopSDK;
|
|
51
|
+
export * from "./extensions/usdc/types";
|
|
52
|
+
export * from "./types";
|
|
53
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAqB,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEzD,OAAO,KAAK,EAGX,OAAO,EAEP,MAAM,EACN,sBAAsB,EACtB,MAAM,SAAS,CAAC;AAIjB,cAAM,OAAO;IACZ,OAAO,CAAC,OAAO,CAAmB;IAElC,OAAO,CAAC,OAAO,CAAqB;IACpC,OAAO,CAAC,UAAU,CAA2B;IAC7C,OAAO,CAAC,OAAO,CAA4B;IAC3C,OAAO,CAAC,QAAQ,CAAyB;IACzC,OAAO,CAAC,QAAQ,CAA4B;IAC5C,OAAO,CAAC,kBAAkB,CAA4B;IACtD,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,WAAW,CAAC,CAAS;IAE7B,OAAO,CAAC,QAAQ,CAA+C;IAC/D,OAAO,CAAC,QAAQ,CAA6B;IAC3C,OAAO,CAAC,mBAAmB,CAA0E;IACvG,OAAO,CAAC,OAAO,CAA+B;IACvC,MAAM,EAAE,MAAM,CAAC;;IAMtB,IAAI,CAAC,EACJ,OAAO,EACP,OAAO,EACP,SAAS,EACT,MAAM,EACN,QAAQ,EACR,QAAQ,EACR,mBAAmB,EACnB,OAAO,GACP,EAAE;QACF,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,KAAK,IAAI,CAAC;QACxC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;QACtB,mBAAmB,CAAC,EAAE,CAAC,OAAO,EAAE,sBAAsB,EAAE,OAAO,EAAE,GAAG,KAAK,IAAI,CAAC;QAC9E,OAAO,CAAC,EAAE;YACT,QAAQ,CAAC,EAAE,OAAO,GAAG,KAAK,CAAC;YAC3B,kBAAkB,CAAC,EAAE,OAAO,GAAG,KAAK,CAAC;YACrC,WAAW,CAAC,EAAE,MAAM,CAAC;SACrB,CAAC;KACF;YAiCa,eAAe;IA6CvB,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IA8B5B,OAAO;IA2Cb,OAAO,CAAC,sBAAsB;IA8DvB,aAAa,IAAI,MAAM;IAO9B,OAAO,CAAC,eAAe;IASvB,OAAO,CAAC,iBAAiB;IAOzB,OAAO,CAAC,aAAa;IAqBrB,OAAO,CAAC,kBAAkB;IAW1B,OAAO,CAAC,UAAU;IAuClB,OAAO,CAAC,iBAAiB;IAyIzB,OAAO,CAAC,UAAU;IAoFlB,OAAO,CAAC,UAAU;IAOlB,MAAM;IAQN,OAAO,CAAC,eAAe;IASvB,OAAO,CAAC,mBAAmB;CAe3B;AAED,eAAO,MAAM,IAAI,SAAgB,CAAC;AAClC,cAAc,yBAAyB,CAAC;AACxC,cAAc,SAAS,CAAC"}
|