@fivenorth/loop-sdk 0.11.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 +61 -0
- package/dist/connection.js +290 -0
- package/dist/errors.js +33 -0
- package/dist/extensions/usdc/index.js +52 -0
- package/dist/extensions/usdc/types.js +1 -0
- package/dist/index.js +0 -32
- package/dist/provider.js +247 -0
- package/dist/server/index.js +116 -35343
- package/dist/server/signer.js +56 -0
- package/dist/server.js +38 -0
- package/dist/session.js +92 -0
- package/dist/types.js +10 -0
- package/dist/wallet.js +22 -0
- package/package.json +9 -6
package/dist/provider.js
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { MessageType } from './types';
|
|
2
|
+
import { RejectRequestError, RequestTimeoutError, UnauthorizedError, extractErrorCode, isUnauthCode } from './errors';
|
|
3
|
+
export const DEFAULT_REQUEST_TIMEOUT_MS = 300000; // 5 minutes
|
|
4
|
+
// Use polyfill only on HTTP (crypt.randomUUID requires HTTPS or localhost)
|
|
5
|
+
// In production (HTTPS), native randomUUID will be used
|
|
6
|
+
function generateUUID() {
|
|
7
|
+
return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, (c) => {
|
|
8
|
+
const gCrypto = globalThis.crypto;
|
|
9
|
+
if (!gCrypto?.getRandomValues) { // fallback for if crypto is not available
|
|
10
|
+
const n = Number(c);
|
|
11
|
+
return ((n ^ (Math.random() * 16) >> (n / 4))).toString(16);
|
|
12
|
+
}
|
|
13
|
+
// use crypto API
|
|
14
|
+
const arr = gCrypto.getRandomValues(new Uint8Array(1));
|
|
15
|
+
const byte = arr[0];
|
|
16
|
+
const n = Number(c);
|
|
17
|
+
return ((n ^ ((byte & 15) >> (n / 4)))).toString(16);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
export function generateRequestId() {
|
|
21
|
+
const gCrypto = globalThis.crypto;
|
|
22
|
+
if (gCrypto?.randomUUID) {
|
|
23
|
+
return gCrypto.randomUUID();
|
|
24
|
+
}
|
|
25
|
+
return generateUUID();
|
|
26
|
+
}
|
|
27
|
+
export class Provider {
|
|
28
|
+
connection;
|
|
29
|
+
party_id;
|
|
30
|
+
public_key;
|
|
31
|
+
email;
|
|
32
|
+
auth_token;
|
|
33
|
+
requests = new Map();
|
|
34
|
+
requestTimeout = DEFAULT_REQUEST_TIMEOUT_MS;
|
|
35
|
+
hooks;
|
|
36
|
+
constructor({ connection, party_id, public_key, auth_token, email, hooks }) {
|
|
37
|
+
if (!connection) {
|
|
38
|
+
throw new Error('Provider requires a connection object.');
|
|
39
|
+
}
|
|
40
|
+
this.connection = connection;
|
|
41
|
+
this.party_id = party_id;
|
|
42
|
+
this.public_key = public_key;
|
|
43
|
+
this.email = email;
|
|
44
|
+
this.auth_token = auth_token;
|
|
45
|
+
this.hooks = hooks;
|
|
46
|
+
}
|
|
47
|
+
getAuthToken() {
|
|
48
|
+
return this.auth_token;
|
|
49
|
+
}
|
|
50
|
+
// handle all responses from the websocket except for handshake_accept, handshake_reject
|
|
51
|
+
handleResponse(message) {
|
|
52
|
+
console.log('Received response:', message);
|
|
53
|
+
if (message?.type === MessageType.TRANSACTION_COMPLETED &&
|
|
54
|
+
(message?.payload?.update_id || message?.payload?.update_data || message?.payload?.status)) {
|
|
55
|
+
if (message?.payload?.error_message) {
|
|
56
|
+
message.payload.error = { error_message: message.payload.error_message };
|
|
57
|
+
delete message.payload.error_message;
|
|
58
|
+
}
|
|
59
|
+
this.hooks?.onTransactionUpdate?.(message.payload, message);
|
|
60
|
+
}
|
|
61
|
+
if (message.request_id) {
|
|
62
|
+
this.requests.set(message.request_id, message);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
getHolding() {
|
|
66
|
+
return this.connection.getHolding(this.auth_token);
|
|
67
|
+
}
|
|
68
|
+
// get the current account connected to the provider
|
|
69
|
+
// This is useful for dApps to know if the user has pre approval or merge delegation permissions to ensure UTXO consolidation is in place
|
|
70
|
+
getAccount() {
|
|
71
|
+
return this.connection.verifySession(this.auth_token);
|
|
72
|
+
}
|
|
73
|
+
getActiveContracts(params) {
|
|
74
|
+
return this.connection.getActiveContracts(this.auth_token, params);
|
|
75
|
+
}
|
|
76
|
+
// submit a transaction to be signed by the wallet to the websocket
|
|
77
|
+
async submitTransaction(payload, options) {
|
|
78
|
+
const requestPayload = options?.estimateTraffic ? { ...payload, estimate_traffic: true } : payload;
|
|
79
|
+
const executionMode = options?.executionMode;
|
|
80
|
+
const finalPayload = executionMode === 'wait'
|
|
81
|
+
? { ...requestPayload, execution_mode: 'wait' }
|
|
82
|
+
: requestPayload;
|
|
83
|
+
return this.sendRequest(MessageType.RUN_TRANSACTION, finalPayload, options);
|
|
84
|
+
}
|
|
85
|
+
async submitAndWaitForTransaction(payload, options) {
|
|
86
|
+
const requestPayload = options?.estimateTraffic ? { ...payload, estimate_traffic: true } : payload;
|
|
87
|
+
return this.sendRequest(MessageType.RUN_TRANSACTION, { ...requestPayload, execution_mode: 'wait' }, options);
|
|
88
|
+
}
|
|
89
|
+
async transfer(recipient, amount, instrument, options) {
|
|
90
|
+
const amountStr = typeof amount === 'number' ? amount.toString() : amount;
|
|
91
|
+
const { requestedAt, executeBefore, requestTimeout, estimateTraffic, memo } = options || {};
|
|
92
|
+
const message = options?.message;
|
|
93
|
+
const resolveDate = (value, fallbackMs) => {
|
|
94
|
+
if (value instanceof Date) {
|
|
95
|
+
return value.toISOString();
|
|
96
|
+
}
|
|
97
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
if (fallbackMs) {
|
|
101
|
+
return new Date(Date.now() + fallbackMs).toISOString();
|
|
102
|
+
}
|
|
103
|
+
return new Date().toISOString();
|
|
104
|
+
};
|
|
105
|
+
const requestedAtIso = resolveDate(requestedAt);
|
|
106
|
+
const executeBeforeIso = resolveDate(executeBefore, 24 * 60 * 60 * 1000);
|
|
107
|
+
const transferRequest = {
|
|
108
|
+
recipient,
|
|
109
|
+
amount: amountStr,
|
|
110
|
+
instrument: {
|
|
111
|
+
instrument_admin: instrument?.instrument_admin,
|
|
112
|
+
instrument_id: instrument?.instrument_id || 'Amulet',
|
|
113
|
+
},
|
|
114
|
+
requested_at: requestedAtIso,
|
|
115
|
+
execute_before: executeBeforeIso,
|
|
116
|
+
};
|
|
117
|
+
if (memo) {
|
|
118
|
+
transferRequest.memo = memo;
|
|
119
|
+
}
|
|
120
|
+
const preparedPayload = await this.connection.prepareTransfer(this.auth_token, transferRequest);
|
|
121
|
+
const submitFn = options?.executionMode === 'wait'
|
|
122
|
+
? this.submitAndWaitForTransaction.bind(this)
|
|
123
|
+
: this.submitTransaction.bind(this);
|
|
124
|
+
return submitFn({
|
|
125
|
+
commands: preparedPayload.commands,
|
|
126
|
+
disclosedContracts: preparedPayload.disclosedContracts,
|
|
127
|
+
packageIdSelectionPreference: preparedPayload.packageIdSelectionPreference,
|
|
128
|
+
actAs: preparedPayload.actAs,
|
|
129
|
+
readAs: preparedPayload.readAs,
|
|
130
|
+
synchronizerId: preparedPayload.synchronizerId,
|
|
131
|
+
}, { requestTimeout, message, estimateTraffic });
|
|
132
|
+
}
|
|
133
|
+
// submit a raw message to be signed by the wallet to the websocket
|
|
134
|
+
async signMessage(message) {
|
|
135
|
+
return this.sendRequest(MessageType.SIGN_RAW_MESSAGE, message);
|
|
136
|
+
}
|
|
137
|
+
async ensureConnected() {
|
|
138
|
+
if (this.connection.ws && this.connection.ws.readyState === WebSocket.OPEN) {
|
|
139
|
+
return Promise.resolve();
|
|
140
|
+
}
|
|
141
|
+
await this.connection.reconnect();
|
|
142
|
+
if (this.connection.ws && this.connection.ws.readyState === WebSocket.OPEN) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
throw new Error('Not connected.');
|
|
146
|
+
}
|
|
147
|
+
sendRequest(messageType, params = {}, options) {
|
|
148
|
+
return new Promise((resolve, reject) => {
|
|
149
|
+
const requestId = generateRequestId();
|
|
150
|
+
let requestContext;
|
|
151
|
+
const ensure = async () => {
|
|
152
|
+
try {
|
|
153
|
+
await this.ensureConnected();
|
|
154
|
+
requestContext = await this.hooks?.onRequestStart?.(messageType, options?.requestLabel);
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
console.error('[LoopSDK] error when checking connection status', error);
|
|
158
|
+
this.hooks?.onRequestFinish?.({
|
|
159
|
+
status: 'error',
|
|
160
|
+
messageType,
|
|
161
|
+
requestLabel: options?.requestLabel,
|
|
162
|
+
requestContext,
|
|
163
|
+
});
|
|
164
|
+
reject(error);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const requestBody = {
|
|
168
|
+
request_id: requestId,
|
|
169
|
+
type: messageType,
|
|
170
|
+
payload: params,
|
|
171
|
+
};
|
|
172
|
+
if (options?.message) {
|
|
173
|
+
requestBody.ticket = { message: options.message };
|
|
174
|
+
if (typeof params === 'object' && params !== null && !Array.isArray(params)) {
|
|
175
|
+
requestBody.payload = {
|
|
176
|
+
...params,
|
|
177
|
+
ticket: { message: options.message },
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
this.connection.ws.send(JSON.stringify(requestBody));
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
console.error('[LoopSDK] error when sending request', error);
|
|
186
|
+
reject(error);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const intervalTime = 300; // 300ms
|
|
190
|
+
let elapsedTime = 0;
|
|
191
|
+
const timeoutMs = options?.requestTimeout ?? this.requestTimeout;
|
|
192
|
+
const intervalId = setInterval(() => {
|
|
193
|
+
const response = this.requests.get(requestId);
|
|
194
|
+
if (response) {
|
|
195
|
+
clearInterval(intervalId);
|
|
196
|
+
this.requests.delete(requestId);
|
|
197
|
+
const code = extractErrorCode(response);
|
|
198
|
+
if (isUnauthCode(code)) {
|
|
199
|
+
this.hooks?.onRequestFinish?.({
|
|
200
|
+
status: 'error',
|
|
201
|
+
messageType,
|
|
202
|
+
requestLabel: options?.requestLabel,
|
|
203
|
+
requestContext,
|
|
204
|
+
errorCode: code,
|
|
205
|
+
});
|
|
206
|
+
reject(new UnauthorizedError(code));
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (response.type === MessageType.REJECT_REQUEST) {
|
|
210
|
+
this.hooks?.onRequestFinish?.({
|
|
211
|
+
status: 'rejected',
|
|
212
|
+
messageType,
|
|
213
|
+
requestLabel: options?.requestLabel,
|
|
214
|
+
requestContext,
|
|
215
|
+
});
|
|
216
|
+
reject(new RejectRequestError());
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
this.hooks?.onRequestFinish?.({
|
|
220
|
+
status: 'success',
|
|
221
|
+
messageType,
|
|
222
|
+
requestLabel: options?.requestLabel,
|
|
223
|
+
requestContext,
|
|
224
|
+
});
|
|
225
|
+
resolve(response.payload);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
elapsedTime += intervalTime;
|
|
230
|
+
if (elapsedTime >= timeoutMs) {
|
|
231
|
+
clearInterval(intervalId);
|
|
232
|
+
this.requests.delete(requestId);
|
|
233
|
+
this.hooks?.onRequestFinish?.({
|
|
234
|
+
status: 'timeout',
|
|
235
|
+
messageType,
|
|
236
|
+
requestLabel: options?.requestLabel,
|
|
237
|
+
requestContext,
|
|
238
|
+
});
|
|
239
|
+
reject(new RequestTimeoutError(timeoutMs));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}, intervalTime);
|
|
243
|
+
};
|
|
244
|
+
void ensure();
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|