evernode-js-client 0.4.51 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,275 @@
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 getAccountInfo(address) {
206
+ const resp = (await this.#client.request({ command: 'account_info', account: address }));
207
+ return resp?.result?.account_data;
208
+ }
209
+
210
+ async getAccountObjects(address, options) {
211
+ return this.#requestWithPaging({ command: 'account_objects', account: address, ...options }, API_REQ_TYPE.ACCOUNT_OBJECTS);
212
+ }
213
+
214
+ async getNamespaceEntries(address, namespaceId, options) {
215
+ return this.#requestWithPaging({ command: 'account_namespace', account: address, namespace_id: namespaceId, ...options }, API_REQ_TYPE.NAMESPACE_ENTRIES);
216
+ }
217
+
218
+ async getNftOffers(address, options) {
219
+ const offers = await this.getAccountObjects(address, options);
220
+ // TODO: Pass rippled filter parameter when xrpl.js supports it.
221
+ return offers.filter(o => o.LedgerEntryType == 'NFTokenOffer');
222
+ }
223
+
224
+ async getTrustlines(address, options) {
225
+ return this.#requestWithPaging({ command: 'account_lines', account: address, ledger_index: "validated", ...options }, API_REQ_TYPE.LINES);
226
+ }
227
+
228
+ async getAccountTrx(address, options) {
229
+ return this.#requestWithPaging({ command: 'account_tx', account: address, ...options }, API_REQ_TYPE.TRANSACTIONS);
230
+ }
231
+
232
+ async getNfts(address, options) {
233
+ return this.#requestWithPaging({ command: 'account_nfts', account: address, ledger_index: "validated", ...options }, API_REQ_TYPE.ACCOUNT_NFTS);
234
+ }
235
+
236
+ async getOffers(address, options) {
237
+ return this.#requestWithPaging({ command: 'account_offers', account: address, ledger_index: "validated", ...options }, API_REQ_TYPE.OFFERS);
238
+ }
239
+
240
+ async getLedgerEntry(index, options) {
241
+ const resp = (await this.#client.request({ command: 'ledger_entry', index: index, ledger_index: "validated", ...options }));
242
+ return resp?.result?.node;
243
+ }
244
+
245
+ async submitAndVerify(tx, options) {
246
+ return await this.#client.submitAndWait(tx, options);
247
+ }
248
+
249
+ async subscribeToAddress(address, handler) {
250
+ this.#addressSubscriptions.push({ address: address, handler: handler });
251
+ await this.#client.request({ command: 'subscribe', accounts: [address] });
252
+ }
253
+
254
+ async unsubscribeFromAddress(address, handler) {
255
+ for (let i = this.#addressSubscriptions.length - 1; i >= 0; i--) {
256
+ const sub = this.#addressSubscriptions[i];
257
+ if (sub.address === address && sub.handler === handler)
258
+ this.#addressSubscriptions.splice(i, 1);
259
+ }
260
+ await this.#client.request({ command: 'unsubscribe', accounts: [address] });
261
+ }
262
+
263
+ async getTransactionFee(txBlob) {
264
+ const fees = await this.#client.request({ command: 'fee', tx_blob: txBlob });
265
+ return fees?.result?.drops?.base_fee;
266
+ }
267
+
268
+ async #subscribeToStream(streamName) {
269
+ await this.#client.request({ command: 'subscribe', streams: [streamName] });
270
+ }
271
+ }
272
+
273
+ module.exports = {
274
+ XrplApi
275
+ }
@@ -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
+ }