@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/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
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Provider } from '../provider';
|
|
2
|
+
import type { Network, TransactionPayload } from '../types';
|
|
3
|
+
import { Signer } from './signer';
|
|
4
|
+
export declare class LoopSDK {
|
|
5
|
+
private signer?;
|
|
6
|
+
private provider?;
|
|
7
|
+
private connection?;
|
|
8
|
+
private isAuthenticated;
|
|
9
|
+
private session?;
|
|
10
|
+
init({ privateKey, partyId, network, walletUrl, apiUrl }: {
|
|
11
|
+
privateKey: string;
|
|
12
|
+
partyId: string;
|
|
13
|
+
network?: Network;
|
|
14
|
+
walletUrl?: string;
|
|
15
|
+
apiUrl?: string;
|
|
16
|
+
}): void;
|
|
17
|
+
authenticate(): Promise<void>;
|
|
18
|
+
getSigner(): Signer;
|
|
19
|
+
getProvider(): Provider;
|
|
20
|
+
executeTransaction(payload: TransactionPayload): Promise<any>;
|
|
21
|
+
}
|
|
22
|
+
export declare const loop: LoopSDK;
|
|
23
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAsB,MAAM,aAAa,CAAC;AAG3D,OAAO,KAAK,EAAE,OAAO,EAAyE,kBAAkB,EAAyD,MAAM,UAAU,CAAC;AAE1L,OAAO,EAAa,MAAM,EAAE,MAAM,UAAU,CAAC;AA4C7C,qBAAa,OAAO;IAChB,OAAO,CAAC,MAAM,CAAC,CAAS;IACxB,OAAO,CAAC,QAAQ,CAAC,CAAc;IAC/B,OAAO,CAAC,UAAU,CAAC,CAAa;IAChC,OAAO,CAAC,eAAe,CAAkB;IACzC,OAAO,CAAC,OAAO,CAAC,CAAc;IAE9B,IAAI,CAAC,EAAC,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAC,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAC;IASzI,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;IAoCnC,SAAS,IAAI,MAAM;IAOnB,WAAW,IAAI,QAAQ;IAOjB,kBAAkB,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,GAAG,CAAC;CAuB7E;AAED,eAAO,MAAM,IAAI,SAAgB,CAAC"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { Provider } from '../provider';
|
|
2
|
+
import { Connection } from '../connection';
|
|
3
|
+
import { SessionInfo } from '../session';
|
|
4
|
+
import { getSigner } from './signer';
|
|
5
|
+
class RpcProvider extends Provider {
|
|
6
|
+
ticket_id;
|
|
7
|
+
user_api_key;
|
|
8
|
+
session;
|
|
9
|
+
constructor({ connection, party_id, public_key, auth_token, ticket_id, user_api_key, email, hooks }) {
|
|
10
|
+
super({ connection, party_id, public_key, auth_token, email, hooks });
|
|
11
|
+
this.ticket_id = ticket_id;
|
|
12
|
+
this.user_api_key = user_api_key;
|
|
13
|
+
this.session = new SessionInfo({
|
|
14
|
+
userApiKey: user_api_key,
|
|
15
|
+
ticketId: ticket_id,
|
|
16
|
+
partyId: party_id,
|
|
17
|
+
publicKey: public_key,
|
|
18
|
+
email: email,
|
|
19
|
+
sessionId: "",
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
async prepareSubmission(payload) {
|
|
23
|
+
return await this.connection.prepareTransaction(this.session, payload);
|
|
24
|
+
}
|
|
25
|
+
async executeSubmission(payload) {
|
|
26
|
+
return await this.connection.executeTransaction(this.session, payload);
|
|
27
|
+
}
|
|
28
|
+
async transfer(recipient, amount, instrument, options) {
|
|
29
|
+
return await this.connection.prepareTransfer(this.getAuthToken(), {
|
|
30
|
+
recipient,
|
|
31
|
+
amount: amount.toString(),
|
|
32
|
+
instrument: {
|
|
33
|
+
instrument_admin: instrument?.instrument_admin,
|
|
34
|
+
instrument_id: instrument?.instrument_id || 'Amulet',
|
|
35
|
+
},
|
|
36
|
+
requested_at: options?.requestedAt instanceof Date ? options?.requestedAt.toISOString() : options?.requestedAt || undefined,
|
|
37
|
+
execute_before: options?.executeBefore instanceof Date ? options?.executeBefore.toISOString() : options?.executeBefore || undefined,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export class LoopSDK {
|
|
42
|
+
signer;
|
|
43
|
+
provider;
|
|
44
|
+
connection;
|
|
45
|
+
isAuthenticated = false;
|
|
46
|
+
session;
|
|
47
|
+
init({ privateKey, partyId, network, walletUrl, apiUrl }) {
|
|
48
|
+
this.signer = getSigner(privateKey, partyId);
|
|
49
|
+
this.connection = new Connection({ network: network || 'local', walletUrl, apiUrl });
|
|
50
|
+
this.isAuthenticated = false;
|
|
51
|
+
}
|
|
52
|
+
// authenticate the user with the signer
|
|
53
|
+
// upon succesfully authenticated, the provider will be initialized and ready to send and sign tx
|
|
54
|
+
async authenticate() {
|
|
55
|
+
if (!this.signer || !this.connection) {
|
|
56
|
+
throw new Error('Signer and connection are required');
|
|
57
|
+
}
|
|
58
|
+
const publicKey = this.signer.getPublicKey();
|
|
59
|
+
const epoch = Date.now();
|
|
60
|
+
const signature = this.signer.signMessageAsHex(`Exchange API Key for ${this.signer.getPartyId()}\nTimestamp: ${epoch}`);
|
|
61
|
+
const apiKey = await this.connection.exchangeApiKey({ publicKey, signature, epoch });
|
|
62
|
+
if (!apiKey?.api_key) {
|
|
63
|
+
throw new Error('Failed to get API key from server.');
|
|
64
|
+
}
|
|
65
|
+
this.isAuthenticated = true;
|
|
66
|
+
this.session = new SessionInfo({
|
|
67
|
+
userApiKey: apiKey?.api_key,
|
|
68
|
+
authToken: apiKey?.auth_token,
|
|
69
|
+
email: apiKey?.email,
|
|
70
|
+
ticketId: apiKey?.ticket_id,
|
|
71
|
+
sessionId: apiKey?.session_id,
|
|
72
|
+
partyId: this.signer.getPartyId(),
|
|
73
|
+
publicKey: publicKey,
|
|
74
|
+
});
|
|
75
|
+
this.provider = new RpcProvider({
|
|
76
|
+
ticket_id: this.session?.ticketId,
|
|
77
|
+
connection: this.connection,
|
|
78
|
+
party_id: this.signer.getPartyId(),
|
|
79
|
+
user_api_key: apiKey?.api_key,
|
|
80
|
+
auth_token: this.session?.authToken,
|
|
81
|
+
public_key: publicKey,
|
|
82
|
+
email: this.session?.email,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
getSigner() {
|
|
86
|
+
if (!this.signer) {
|
|
87
|
+
throw new Error('Signer not initialized');
|
|
88
|
+
}
|
|
89
|
+
return this.signer;
|
|
90
|
+
}
|
|
91
|
+
getProvider() {
|
|
92
|
+
if (!this.provider) {
|
|
93
|
+
throw new Error('Provider not initialized');
|
|
94
|
+
}
|
|
95
|
+
return this.provider;
|
|
96
|
+
}
|
|
97
|
+
async executeTransaction(payload) {
|
|
98
|
+
if (!this.provider || !this.signer) {
|
|
99
|
+
throw new Error('Provider and signer are required');
|
|
100
|
+
}
|
|
101
|
+
// Prepare the transaction with interactive submission to get unsigned transaction hash
|
|
102
|
+
const preparedPayload = await this.provider?.prepareSubmission(payload);
|
|
103
|
+
if (!preparedPayload) {
|
|
104
|
+
throw new Error('Failed to prepare submission');
|
|
105
|
+
}
|
|
106
|
+
// now we sign the transaction hash which is base64 encoded from the response
|
|
107
|
+
const signedTransactionHash = this.getSigner().signTransactionHash(preparedPayload.transaction_hash);
|
|
108
|
+
// Combine the signed transaction hash with the transaction data to submit to the ledger
|
|
109
|
+
const submissionResponse = await this.provider?.executeSubmission({
|
|
110
|
+
command_id: preparedPayload.command_id,
|
|
111
|
+
transaction_data: preparedPayload.transaction_data,
|
|
112
|
+
signature: signedTransactionHash,
|
|
113
|
+
});
|
|
114
|
+
return submissionResponse;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export const loop = new LoopSDK();
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as forge from 'node-forge';
|
|
2
|
+
export declare const getSigner: (privateKeyHex: string, partyId: string) => Signer;
|
|
3
|
+
export declare class Signer {
|
|
4
|
+
private privateKey;
|
|
5
|
+
private publicKey;
|
|
6
|
+
private publicKeyHex;
|
|
7
|
+
private partyId;
|
|
8
|
+
constructor(privateKeyHex: string, partyId: string);
|
|
9
|
+
getPublicKey(): string;
|
|
10
|
+
signMessage(message: string): forge.Bytes;
|
|
11
|
+
signMessageAsHex(message: string): string;
|
|
12
|
+
getPartyId(): string;
|
|
13
|
+
signTransactionHash(transactionHash: string): string;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=signer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signer.d.ts","sourceRoot":"","sources":["../../src/server/signer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,YAAY,CAAC;AAEpC,eAAO,MAAM,SAAS,GAAI,eAAe,MAAM,EAAE,SAAS,MAAM,KAAG,MAElE,CAAA;AAED,qBAAa,MAAM;IACf,OAAO,CAAC,UAAU,CAAc;IAChC,OAAO,CAAC,SAAS,CAAc;IAC/B,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,OAAO,CAAS;gBAEZ,aAAa,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;IAc3C,YAAY,IAAI,MAAM;IAItB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,KAAK,CAAC,KAAK;IAQzC,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM;IASzC,UAAU,IAAI,MAAM;IAKpB,mBAAmB,CAAC,eAAe,EAAE,MAAM,GAAG,MAAM;CAa9D"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as forge from 'node-forge';
|
|
2
|
+
export const getSigner = (privateKeyHex, partyId) => {
|
|
3
|
+
return new Signer(privateKeyHex, partyId);
|
|
4
|
+
};
|
|
5
|
+
export class Signer {
|
|
6
|
+
privateKey;
|
|
7
|
+
publicKey;
|
|
8
|
+
publicKeyHex;
|
|
9
|
+
partyId;
|
|
10
|
+
constructor(privateKeyHex, partyId) {
|
|
11
|
+
if (!privateKeyHex || !partyId) {
|
|
12
|
+
throw new Error('Private key and party ID are required');
|
|
13
|
+
}
|
|
14
|
+
this.privateKey = forge.util.hexToBytes(privateKeyHex);
|
|
15
|
+
this.partyId = partyId;
|
|
16
|
+
const publicKey = forge.pki.ed25519.publicKeyFromPrivateKey({
|
|
17
|
+
privateKey: this.privateKey,
|
|
18
|
+
});
|
|
19
|
+
this.publicKey = publicKey;
|
|
20
|
+
this.publicKeyHex = forge.util.bytesToHex(publicKey);
|
|
21
|
+
}
|
|
22
|
+
getPublicKey() {
|
|
23
|
+
return this.publicKeyHex;
|
|
24
|
+
}
|
|
25
|
+
signMessage(message) {
|
|
26
|
+
return forge.pki.ed25519.sign({
|
|
27
|
+
message: message,
|
|
28
|
+
encoding: 'utf8',
|
|
29
|
+
privateKey: this.privateKey,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
signMessageAsHex(message) {
|
|
33
|
+
const signature = forge.pki.ed25519.sign({
|
|
34
|
+
message: message,
|
|
35
|
+
encoding: 'utf8',
|
|
36
|
+
privateKey: this.privateKey,
|
|
37
|
+
});
|
|
38
|
+
return forge.util.bytesToHex(signature);
|
|
39
|
+
}
|
|
40
|
+
getPartyId() {
|
|
41
|
+
return this.partyId;
|
|
42
|
+
}
|
|
43
|
+
// sign the transaction hash in base64 format and return the signature in hex format
|
|
44
|
+
signTransactionHash(transactionHash) {
|
|
45
|
+
if (!transactionHash) {
|
|
46
|
+
throw new Error('Transaction hash is required');
|
|
47
|
+
}
|
|
48
|
+
// Now we will sign the transaction hash
|
|
49
|
+
const signedRequest = forge.pki.ed25519.sign({
|
|
50
|
+
message: forge.util.decode64(transactionHash),
|
|
51
|
+
encoding: 'binary',
|
|
52
|
+
privateKey: this.privateKey,
|
|
53
|
+
});
|
|
54
|
+
return forge.util.bytesToHex(signedRequest);
|
|
55
|
+
}
|
|
56
|
+
}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":""}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { join } from 'path';
|
|
2
|
+
const server = Bun.serve({
|
|
3
|
+
port: 3030,
|
|
4
|
+
async fetch(req) {
|
|
5
|
+
const url = new URL(req.url);
|
|
6
|
+
const pathname = url.pathname;
|
|
7
|
+
// Serve the test page
|
|
8
|
+
if (pathname === '/') {
|
|
9
|
+
return new Response(Bun.file(join(import.meta.dir, '..', '/demo/test.html')));
|
|
10
|
+
}
|
|
11
|
+
// Intercept the request for the SDK and compile it on the fly
|
|
12
|
+
if (pathname === '/dist/index.js') {
|
|
13
|
+
console.log('Bundling src/index.ts for development...');
|
|
14
|
+
const build = await Bun.build({
|
|
15
|
+
entrypoints: [join(import.meta.dir, 'index.ts')],
|
|
16
|
+
target: 'browser',
|
|
17
|
+
sourcemap: 'inline', // Good for debugging
|
|
18
|
+
});
|
|
19
|
+
if (!build.success) {
|
|
20
|
+
console.error("Build failed:");
|
|
21
|
+
for (const message of build.logs) {
|
|
22
|
+
console.error(message);
|
|
23
|
+
}
|
|
24
|
+
return new Response('Build failed', { status: 500 });
|
|
25
|
+
}
|
|
26
|
+
// Serve the first build artifact (the JS file)
|
|
27
|
+
return new Response(build.outputs[0], {
|
|
28
|
+
headers: { 'Content-Type': 'application/javascript; charset=utf-8' },
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return new Response('Not Found', { status: 404 });
|
|
32
|
+
},
|
|
33
|
+
error(error) {
|
|
34
|
+
console.error(error);
|
|
35
|
+
return new Response('Internal Server Error', { status: 500 });
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
console.log(`Listening on http://localhost:${server.port}`);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session LifeCycle:
|
|
3
|
+
* 1. Initialize a new session with a session id
|
|
4
|
+
* 2. Set the ticket id when we exchange session id/appname, and user approve it and now we have a ticket id
|
|
5
|
+
* 3. Set the session as authorized when we succesfully validate the session with the backend
|
|
6
|
+
* 4. Save the session to localStorage
|
|
7
|
+
* 5. Reset the session when we need to start a new session
|
|
8
|
+
* 6. Validate the session when we need to check if the session is valid
|
|
9
|
+
* 7. Is authorized means the session is valid and the user is authenticated
|
|
10
|
+
* 8. Save the session to localStorage
|
|
11
|
+
*/
|
|
12
|
+
export declare class SessionInfo {
|
|
13
|
+
sessionId: string;
|
|
14
|
+
ticketId?: string;
|
|
15
|
+
authToken?: string;
|
|
16
|
+
partyId?: string;
|
|
17
|
+
publicKey?: string;
|
|
18
|
+
email?: string;
|
|
19
|
+
userApiKey?: string;
|
|
20
|
+
private _isAuthorized;
|
|
21
|
+
constructor({ sessionId, ticketId, authToken, partyId, publicKey, email, userApiKey }: {
|
|
22
|
+
sessionId: string;
|
|
23
|
+
ticketId?: string;
|
|
24
|
+
authToken?: string;
|
|
25
|
+
partyId?: string;
|
|
26
|
+
publicKey?: string;
|
|
27
|
+
email?: string;
|
|
28
|
+
userApiKey?: string;
|
|
29
|
+
});
|
|
30
|
+
setTicketId(ticketId: string): void;
|
|
31
|
+
authorized(): void;
|
|
32
|
+
isPreAuthorized(): boolean;
|
|
33
|
+
isAuthorized(): boolean;
|
|
34
|
+
save(): void;
|
|
35
|
+
reset(): void;
|
|
36
|
+
static fromStorage(): SessionInfo;
|
|
37
|
+
private toJson;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=session.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;GAUG;AACH,qBAAa,WAAW;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IAC3B,OAAO,CAAC,aAAa,CAAkB;gBAE3B,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE;QAAG,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE;IAW/N,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAMnC,UAAU,IAAI,IAAI;IAQlB,eAAe,IAAI,OAAO;IAK1B,YAAY,IAAI,OAAO;IAKvB,IAAI,IAAI,IAAI;IAKZ,KAAK,IAAI,IAAI;IAab,MAAM,CAAC,WAAW,IAAI,WAAW;IAqBjC,OAAO,CAAC,MAAM;CAUf"}
|
package/dist/session.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { generateRequestId } from './provider';
|
|
2
|
+
const STORAGE_KEY_LOOP_CONNECT = 'loop_connect';
|
|
3
|
+
/**
|
|
4
|
+
* Session LifeCycle:
|
|
5
|
+
* 1. Initialize a new session with a session id
|
|
6
|
+
* 2. Set the ticket id when we exchange session id/appname, and user approve it and now we have a ticket id
|
|
7
|
+
* 3. Set the session as authorized when we succesfully validate the session with the backend
|
|
8
|
+
* 4. Save the session to localStorage
|
|
9
|
+
* 5. Reset the session when we need to start a new session
|
|
10
|
+
* 6. Validate the session when we need to check if the session is valid
|
|
11
|
+
* 7. Is authorized means the session is valid and the user is authenticated
|
|
12
|
+
* 8. Save the session to localStorage
|
|
13
|
+
*/
|
|
14
|
+
export class SessionInfo {
|
|
15
|
+
sessionId;
|
|
16
|
+
ticketId;
|
|
17
|
+
authToken;
|
|
18
|
+
partyId;
|
|
19
|
+
publicKey;
|
|
20
|
+
email;
|
|
21
|
+
userApiKey;
|
|
22
|
+
_isAuthorized = false;
|
|
23
|
+
constructor({ sessionId, ticketId, authToken, partyId, publicKey, email, userApiKey }) {
|
|
24
|
+
this.sessionId = sessionId;
|
|
25
|
+
this.ticketId = ticketId;
|
|
26
|
+
this.authToken = authToken;
|
|
27
|
+
this.partyId = partyId;
|
|
28
|
+
this.publicKey = publicKey;
|
|
29
|
+
this.email = email;
|
|
30
|
+
this.userApiKey = userApiKey;
|
|
31
|
+
}
|
|
32
|
+
// set the ticket id when we exchange session id/appname, and user approve it and now we have a ticket id
|
|
33
|
+
setTicketId(ticketId) {
|
|
34
|
+
this.ticketId = ticketId;
|
|
35
|
+
this.save();
|
|
36
|
+
}
|
|
37
|
+
// set the session as authorized when we succesfully validate the session with the backend
|
|
38
|
+
authorized() {
|
|
39
|
+
if (this.ticketId === undefined || this.sessionId === undefined || this.authToken === undefined || this.partyId === undefined || this.publicKey === undefined) {
|
|
40
|
+
throw new Error('Session cannot be authorized without all required fields.');
|
|
41
|
+
}
|
|
42
|
+
this._isAuthorized = true;
|
|
43
|
+
}
|
|
44
|
+
// is pre authorized means the session is initialized and the ticket id is set together with auth and user information but we haven't validated the session with the backend yet
|
|
45
|
+
isPreAuthorized() {
|
|
46
|
+
return !this._isAuthorized && this.ticketId !== undefined && this.sessionId !== undefined && this.authToken !== undefined && this.partyId !== undefined && this.publicKey !== undefined;
|
|
47
|
+
}
|
|
48
|
+
// is authorized means the session is valid and the user is authenticated
|
|
49
|
+
isAuthorized() {
|
|
50
|
+
return this._isAuthorized;
|
|
51
|
+
}
|
|
52
|
+
// save persisted session info to localStorage
|
|
53
|
+
save() {
|
|
54
|
+
localStorage.setItem('loop_connect', this.toJson());
|
|
55
|
+
}
|
|
56
|
+
reset() {
|
|
57
|
+
localStorage.removeItem(STORAGE_KEY_LOOP_CONNECT);
|
|
58
|
+
this.sessionId = generateRequestId();
|
|
59
|
+
this._isAuthorized = false;
|
|
60
|
+
this.ticketId = undefined;
|
|
61
|
+
this.authToken = undefined;
|
|
62
|
+
this.partyId = undefined;
|
|
63
|
+
this.publicKey = undefined;
|
|
64
|
+
this.email = undefined;
|
|
65
|
+
}
|
|
66
|
+
static fromStorage() {
|
|
67
|
+
const existingConnectionRaw = localStorage.getItem(STORAGE_KEY_LOOP_CONNECT);
|
|
68
|
+
if (!existingConnectionRaw) {
|
|
69
|
+
return new SessionInfo({ sessionId: generateRequestId() });
|
|
70
|
+
}
|
|
71
|
+
let session = null;
|
|
72
|
+
try {
|
|
73
|
+
session = new SessionInfo(JSON.parse(existingConnectionRaw));
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
console.error('Failed to parse existing connection info, local storage is corrupted.', error);
|
|
77
|
+
localStorage.removeItem(STORAGE_KEY_LOOP_CONNECT);
|
|
78
|
+
session = new SessionInfo({ sessionId: generateRequestId() });
|
|
79
|
+
}
|
|
80
|
+
return session;
|
|
81
|
+
}
|
|
82
|
+
toJson() {
|
|
83
|
+
return JSON.stringify({
|
|
84
|
+
sessionId: this.sessionId,
|
|
85
|
+
ticketId: this.ticketId,
|
|
86
|
+
authToken: this.authToken,
|
|
87
|
+
partyId: this.partyId,
|
|
88
|
+
publicKey: this.publicKey,
|
|
89
|
+
email: this.email,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|