evernode-js-client 0.5.12 → 0.5.13

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,301 @@
1
+ const xrpl = require('xrpl');
2
+ const kp = require('ripple-keypairs');
3
+ const { EventEmitter } = require('./event-emitter');
4
+ const { DefaultValues } = require('./defaults');
5
+ const { TransactionHelper } = require('./transaction-helper');
6
+ const { XrplApiEvents } = require('./xrpl-common');
7
+
8
+ const MAX_PAGE_LIMIT = 400;
9
+ const API_REQ_TYPE = {
10
+ NAMESPACE_ENTRIES: 'namespace_entries',
11
+ ACCOUNT_OBJECTS: 'account_objects',
12
+ LINES: 'lines',
13
+ ACCOUNT_NFTS: 'account_nfts',
14
+ OFFERS: 'offers',
15
+ TRANSACTIONS: 'transactions'
16
+ }
17
+
18
+ class XrplApi {
19
+
20
+ #rippledServer;
21
+ #client;
22
+ #events = new EventEmitter();
23
+ #addressSubscriptions = [];
24
+ #maintainConnection = false;
25
+
26
+ constructor(rippledServer = null, options = {}) {
27
+
28
+ this.#rippledServer = rippledServer || DefaultValues.rippledServer;
29
+ this.#initXrplClient(options.xrplClientOptions);
30
+ }
31
+
32
+ async #initXrplClient(xrplClientOptions = {}) {
33
+
34
+ if (this.#client) { // If the client already exists, clean it up.
35
+ this.#client.removeAllListeners(); // Remove existing event listeners to avoid them getting called from the old client object.
36
+ await this.#client.disconnect();
37
+ this.#client = null;
38
+ }
39
+
40
+ this.#client = new xrpl.Client(this.#rippledServer, xrplClientOptions);
41
+
42
+ this.#client.on('error', (errorCode, errorMessage) => {
43
+ console.log(errorCode + ': ' + errorMessage);
44
+ });
45
+
46
+ this.#client.on('disconnected', (code) => {
47
+ if (this.#maintainConnection) {
48
+ console.log(`Connection failure for ${this.#rippledServer} (code:${code})`);
49
+ console.log("Reinitializing xrpl client.");
50
+ this.#initXrplClient().then(() => this.#connectXrplClient(true));
51
+ }
52
+ });
53
+
54
+ this.#client.on('ledgerClosed', (ledger) => {
55
+ this.ledgerIndex = ledger.ledger_index;
56
+ this.#events.emit(XrplApiEvents.LEDGER, ledger);
57
+ });
58
+
59
+ this.#client.on("transaction", async (data) => {
60
+ if (data.validated) {
61
+ // NFTokenAcceptOffer transactions does not contain a Destination. So we check whether the accepted offer is created by which subscribed account
62
+ if (data.transaction.TransactionType === 'NFTokenAcceptOffer') {
63
+ // We take all the offers created by subscribed accounts in previous ledger until we get the respective offer.
64
+ for (const subscription of this.#addressSubscriptions) {
65
+ const offer = (await this.getNftOffers(subscription.address, { ledger_index: data.ledger_index - 1 }))?.find(o => o.index === (data.transaction.NFTokenSellOffer || data.transaction.NFTokenBuyOffer));
66
+ // When we find the respective offer. We populate the destination and offer info and then we break the loop.
67
+ if (offer) {
68
+ // We populate some sell offer properties to the transaction to be sent with the event.
69
+ data.transaction.Destination = subscription.address;
70
+ // Replace the offer with the found offer object.
71
+ if (data.transaction.NFTokenSellOffer)
72
+ data.transaction.NFTokenSellOffer = offer;
73
+ else if (data.transaction.NFTokenBuyOffer)
74
+ data.transaction.NFTokenBuyOffer = offer;
75
+ break;
76
+ }
77
+ }
78
+ }
79
+
80
+ const matches = this.#addressSubscriptions.filter(s => s.address === data.transaction.Destination); // Only incoming transactions.
81
+ if (matches.length > 0) {
82
+ const tx = {
83
+ LedgerHash: data.ledger_hash,
84
+ LedgerIndex: data.ledger_index,
85
+ ...data.transaction
86
+ }; // Create an object copy. Otherwise xrpl client will mutate the transaction object,
87
+ const eventName = tx.TransactionType.toLowerCase();
88
+ // Emit the event only for successful transactions, Otherwise emit error.
89
+ if (data.engine_result === "tesSUCCESS") {
90
+ tx.Memos = TransactionHelper.deserializeMemos(tx.Memos);
91
+ matches.forEach(s => s.handler(eventName, tx));
92
+ }
93
+ else {
94
+ matches.forEach(s => s.handler(eventName, null, data.engine_result_message));
95
+ }
96
+ }
97
+ }
98
+ });
99
+ }
100
+
101
+ async #connectXrplClient(reconnect = false) {
102
+
103
+ if (reconnect) {
104
+ let attempts = 0;
105
+ while (this.#maintainConnection) { // Keep attempting until consumer calls disconnect() manually.
106
+ console.log(`Reconnection attempt ${++attempts}`);
107
+ try {
108
+ await this.#client.connect();
109
+ break;
110
+ }
111
+ catch {
112
+ if (this.#maintainConnection) {
113
+ const delaySec = 2 * attempts; // Retry with backoff delay.
114
+ console.log(`Attempt ${attempts} failed. Retrying in ${delaySec}s...`);
115
+ await new Promise(resolve => setTimeout(resolve, delaySec * 1000));
116
+ }
117
+ }
118
+ }
119
+ }
120
+ else {
121
+ // Single attempt and throw error. Used for initial connect() call.
122
+ await this.#client.connect();
123
+ }
124
+
125
+ // After connection established, check again whether maintainConnections has become false.
126
+ // This is in case the consumer has called disconnect() while connection is being established.
127
+ if (this.#maintainConnection) {
128
+ this.ledgerIndex = await this.#client.getLedgerIndex();
129
+ this.#subscribeToStream('ledger');
130
+
131
+ // Re-subscribe to existing account address subscriptions (in case this is a reconnect)
132
+ if (this.#addressSubscriptions.length > 0)
133
+ await this.#client.request({ command: 'subscribe', accounts: this.#addressSubscriptions.map(s => s.address) });
134
+ }
135
+ else {
136
+ await this.disconnect();
137
+ }
138
+ }
139
+
140
+ async #requestWithPaging(requestObj, requestType) {
141
+ let res = [];
142
+ let checked = false;
143
+ let resp;
144
+ let count = requestObj?.limit;
145
+
146
+ while ((!count || count > 0) && (!checked || resp?.result?.marker)) {
147
+ checked = true;
148
+ requestObj.limit = count ? Math.min(count, MAX_PAGE_LIMIT) : MAX_PAGE_LIMIT;
149
+ if (resp?.result?.marker)
150
+ requestObj.marker = resp?.result?.marker;
151
+ else
152
+ delete requestObj.marker;
153
+ resp = (await this.#client.request(requestObj));
154
+ if (resp?.result && resp?.result[requestType])
155
+ res.push(...resp.result[requestType]);
156
+ if (count)
157
+ count -= requestObj.limit;
158
+ }
159
+
160
+ return res;
161
+ }
162
+
163
+ on(event, handler) {
164
+ this.#events.on(event, handler);
165
+ }
166
+
167
+ once(event, handler) {
168
+ this.#events.once(event, handler);
169
+ }
170
+
171
+ off(event, handler = null) {
172
+ this.#events.off(event, handler);
173
+ }
174
+
175
+ async connect() {
176
+ if (this.#maintainConnection)
177
+ return;
178
+
179
+ this.#maintainConnection = true;
180
+ await this.#connectXrplClient();
181
+ }
182
+
183
+ async disconnect() {
184
+ this.#maintainConnection = false;
185
+
186
+ if (this.#client.isConnected()) {
187
+ await this.#client.disconnect().catch(console.error);
188
+ }
189
+ }
190
+
191
+ async isValidKeyForAddress(publicKey, address) {
192
+ const info = await this.getAccountInfo(address);
193
+ const accountFlags = xrpl.parseAccountRootFlags(info.Flags);
194
+ const regularKey = info.RegularKey;
195
+ const derivedPubKeyAddress = kp.deriveAddress(publicKey);
196
+
197
+ // If the master key is disabled the derived pubkey address should be the regular key.
198
+ // Otherwise it could be account address or the regular key
199
+ if (accountFlags.lsfDisableMaster)
200
+ return regularKey && (derivedPubKeyAddress === regularKey);
201
+ else
202
+ return derivedPubKeyAddress === address || (regularKey && derivedPubKeyAddress === regularKey);
203
+ }
204
+
205
+ async isAccountExists(address) {
206
+ try {
207
+ await this.#client.request({ command: 'account_info', account: address });
208
+ return true;
209
+ }
210
+ catch (e) {
211
+ if (e.data.error === 'actNotFound') return false;
212
+ else throw e;
213
+ }
214
+ }
215
+
216
+ async getAccountInfo(address) {
217
+ const resp = (await this.#client.request({ command: 'account_info', account: address }));
218
+ return resp?.result?.account_data;
219
+ }
220
+
221
+ async getAccountObjects(address, options) {
222
+ return this.#requestWithPaging({ command: 'account_objects', account: address, ...options }, API_REQ_TYPE.ACCOUNT_OBJECTS);
223
+ }
224
+
225
+ async getNamespaceEntries(address, namespaceId, options) {
226
+ return this.#requestWithPaging({ command: 'account_namespace', account: address, namespace_id: namespaceId, ...options }, API_REQ_TYPE.NAMESPACE_ENTRIES);
227
+ }
228
+
229
+ async getNftOffers(address, options) {
230
+ const offers = await this.getAccountObjects(address, options);
231
+ // TODO: Pass rippled filter parameter when xrpl.js supports it.
232
+ return offers.filter(o => o.LedgerEntryType == 'NFTokenOffer');
233
+ }
234
+
235
+ async getTrustlines(address, options) {
236
+ return this.#requestWithPaging({ command: 'account_lines', account: address, ledger_index: "validated", ...options }, API_REQ_TYPE.LINES);
237
+ }
238
+
239
+ async getAccountTrx(address, options) {
240
+ return this.#requestWithPaging({ command: 'account_tx', account: address, ...options }, API_REQ_TYPE.TRANSACTIONS);
241
+ }
242
+
243
+ async getNfts(address, options) {
244
+ return this.#requestWithPaging({ command: 'account_nfts', account: address, ledger_index: "validated", ...options }, API_REQ_TYPE.ACCOUNT_NFTS);
245
+ }
246
+
247
+ async getOffers(address, options) {
248
+ return this.#requestWithPaging({ command: 'account_offers', account: address, ledger_index: "validated", ...options }, API_REQ_TYPE.OFFERS);
249
+ }
250
+
251
+ async getSellOffers(nfTokenId, options = {}) {
252
+ return this.#requestWithPaging({ command: 'nft_sell_offers', nft_id: nfTokenId, ledger_index: "validated", ...options }, API_REQ_TYPE.OFFERS);
253
+ }
254
+
255
+ async getBuyOffers(nfTokenId, options = {}) {
256
+ return this.#requestWithPaging({ command: 'nft_buy_offers', nft_id: nfTokenId, ledger_index: "validated", ...options }, API_REQ_TYPE.OFFERS);
257
+ }
258
+
259
+ async getLedgerEntry(index, options) {
260
+ try {
261
+ const resp = (await this.#client.request({ command: 'ledger_entry', index: index, ledger_index: "validated", ...options }));
262
+ return resp?.result?.node;
263
+
264
+ } catch (e) {
265
+ if (e?.data?.error === 'entryNotFound')
266
+ return null;
267
+ throw e;
268
+ }
269
+ }
270
+
271
+ async submitAndVerify(tx, options) {
272
+ return await this.#client.submitAndWait(tx, options);
273
+ }
274
+
275
+ async subscribeToAddress(address, handler) {
276
+ this.#addressSubscriptions.push({ address: address, handler: handler });
277
+ await this.#client.request({ command: 'subscribe', accounts: [address] });
278
+ }
279
+
280
+ async unsubscribeFromAddress(address, handler) {
281
+ for (let i = this.#addressSubscriptions.length - 1; i >= 0; i--) {
282
+ const sub = this.#addressSubscriptions[i];
283
+ if (sub.address === address && sub.handler === handler)
284
+ this.#addressSubscriptions.splice(i, 1);
285
+ }
286
+ await this.#client.request({ command: 'unsubscribe', accounts: [address] });
287
+ }
288
+
289
+ async getTransactionFee(txBlob) {
290
+ const fees = await this.#client.request({ command: 'fee', tx_blob: txBlob });
291
+ return fees?.result?.drops?.base_fee;
292
+ }
293
+
294
+ async #subscribeToStream(streamName) {
295
+ await this.#client.request({ command: 'subscribe', streams: [streamName] });
296
+ }
297
+ }
298
+
299
+ module.exports = {
300
+ XrplApi
301
+ }
@@ -0,0 +1,17 @@
1
+ const XrplApiEvents = {
2
+ LEDGER: 'ledger',
3
+ PAYMENT: 'payment',
4
+ NFT_OFFER_CREATE: 'nftokencreateoffer',
5
+ NFT_OFFER_ACCEPT: 'nftokenacceptoffer'
6
+ }
7
+
8
+ const XrplConstants = {
9
+ MAX_LEDGER_OFFSET: 10,
10
+ XRP: 'XRP',
11
+ MIN_XRP_AMOUNT: '1' // drops
12
+ }
13
+
14
+ module.exports = {
15
+ XrplApiEvents,
16
+ XrplConstants
17
+ }