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.
package/.eslintrc.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "env": {
3
+ "browser": true,
4
+ "commonjs": true,
5
+ "es2021": true
6
+ },
7
+ "extends": "eslint:recommended",
8
+ "parserOptions": {
9
+ "ecmaVersion": 13
10
+ },
11
+ "rules": {
12
+ "no-async-promise-executor": "off"
13
+ }
14
+ }
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 HotPocketDev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,26 @@
1
+ # Evernode js client
2
+ Javascript client library for Evernode.
3
+ (Only tested on NodeJS)
4
+
5
+ ## Prerequisites
6
+ ```
7
+ npm i -g @vercel/ncc
8
+ ```
9
+
10
+ ## Publish to npm
11
+ First, update the version in package.json.
12
+ ```
13
+ npm install
14
+ npm login
15
+ npm run publish
16
+ ```
17
+
18
+ ## Running tests
19
+ ```
20
+ cd test
21
+ npm install
22
+ node test.js
23
+ ```
24
+
25
+ ## NPM package
26
+ https://www.npmjs.com/package/evernode-js-client
package/clean-pkg.sh ADDED
@@ -0,0 +1,4 @@
1
+ #!/bin/bash
2
+ # Create a clean dist/package.json for publishing.
3
+ node -p "const pkg=require('./package.json'); delete pkg.scripts; delete pkg.devDependencies; JSON.stringify(pkg, null, 4)" \
4
+ > dist/package.json
package/npm-readme.md ADDED
@@ -0,0 +1,4 @@
1
+ # Evernode library for nodejs and browser.
2
+ Evernode library contains helper methods to interact with Evernode ecosystem.
3
+
4
+ - [Source code](https://github.com/HotPocketDev/evernode-js-client)
package/package.json CHANGED
@@ -1,11 +1,26 @@
1
1
  {
2
2
  "name": "evernode-js-client",
3
- "version": "0.4.51",
3
+ "description": "Javascript client library for Evernode.",
4
+ "keywords": [
5
+ "Evernode"
6
+ ],
7
+ "homepage": "https://github.com/HotPocketDev/evernode-js-client",
8
+ "license": "MIT",
9
+ "version": "0.5.0",
10
+ "scripts": {
11
+ "lint": "./node_modules/.bin/eslint src/**/*.js",
12
+ "build": "npm run lint && ncc build src/index.js -e elliptic -e xrpl -e ripple-address-codec -e ripple-keypairs -o dist/",
13
+ "bundle": "npm run build && ./clean-pkg.sh",
14
+ "publish": "npm run bundle && npm publish ./dist"
15
+ },
4
16
  "dependencies": {
5
17
  "elliptic": "6.5.4",
6
18
  "ripple-address-codec": "4.2.0",
7
19
  "ripple-keypairs": "1.1.0",
8
20
  "xrpl": "2.2.1",
9
21
  "xrpl-binary-codec": "1.4.2"
22
+ },
23
+ "devDependencies": {
24
+ "eslint": "8.3.0"
10
25
  }
11
26
  }
@@ -0,0 +1,10 @@
1
+ #!/bin/bash
2
+ # This script removes previous npm package versions. (need to be logged into npm first)
3
+
4
+ for i in {0..36}
5
+ do
6
+ pkg="evernode-js-client@0.4.$i"
7
+ echo Unpublishing $pkg
8
+ npm unpublish $pkg
9
+ sleep 2
10
+ done
@@ -0,0 +1,567 @@
1
+ const codec = require('ripple-address-codec');
2
+ const { Buffer } = require('buffer');
3
+ const { XrplApi } = require('../xrpl-api');
4
+ const { XrplAccount } = require('../xrpl-account');
5
+ const { XrplApiEvents, XrplConstants } = require('../xrpl-common');
6
+ const { EvernodeEvents, MemoTypes, MemoFormats, EvernodeConstants, HookStateKeys } = require('../evernode-common');
7
+ const { DefaultValues } = require('../defaults');
8
+ const { EncryptionHelper } = require('../encryption-helper');
9
+ const { EventEmitter } = require('../event-emitter');
10
+ const { UtilHelpers } = require('../util-helpers');
11
+ const { FirestoreHandler } = require('../firestore/firestore-handler');
12
+ const { StateHelpers } = require('../state-helpers');
13
+
14
+ class BaseEvernodeClient {
15
+
16
+ #watchEvents;
17
+ #autoSubscribe;
18
+ #ownsXrplApi = false;
19
+ #firestoreHandler;
20
+
21
+ constructor(xrpAddress, xrpSecret, watchEvents, autoSubscribe = false, options = {}) {
22
+
23
+ this.connected = false;
24
+ this.registryAddress = options.registryAddress || DefaultValues.registryAddress;
25
+
26
+ this.xrplApi = options.xrplApi || DefaultValues.xrplApi || new XrplApi(options.rippledServer);
27
+ if (!options.xrplApi && !DefaultValues.xrplApi)
28
+ this.#ownsXrplApi = true;
29
+
30
+ this.xrplAcc = new XrplAccount(xrpAddress, xrpSecret, { xrplApi: this.xrplApi });
31
+ this.accKeyPair = xrpSecret && this.xrplAcc.deriveKeypair();
32
+ this.#watchEvents = watchEvents;
33
+ this.#autoSubscribe = autoSubscribe;
34
+ this.events = new EventEmitter();
35
+ this.#firestoreHandler = new FirestoreHandler()
36
+
37
+ this.xrplAcc.on(XrplApiEvents.PAYMENT, (tx, error) => this.#handleEvernodeEvent(tx, error));
38
+ this.xrplAcc.on(XrplApiEvents.NFT_OFFER_CREATE, (tx, error) => this.#handleEvernodeEvent(tx, error));
39
+ this.xrplAcc.on(XrplApiEvents.NFT_OFFER_ACCEPT, (tx, error) => this.#handleEvernodeEvent(tx, error));
40
+
41
+ }
42
+
43
+ /**
44
+ * Listens to the subscribed events. This will listen for the event without detaching the handler until it's 'off'.
45
+ * @param {string} event Event name.
46
+ * @param {function(event)} handler Callback function to handle the event.
47
+ */
48
+ on(event, handler) {
49
+ this.events.on(event, handler);
50
+ }
51
+
52
+ /**
53
+ * Listens to the subscribed events. This will listen only once and detach the handler.
54
+ * @param {string} event Event name.
55
+ * @param {function(event)} handler Callback function to handle the event.
56
+ */
57
+ once(event, handler) {
58
+ this.events.once(event, handler);
59
+ }
60
+
61
+ /**
62
+ * Detach the listener event.
63
+ * @param {string} event Event name.
64
+ * @param {function(event)} handler (optional) Can be sent if a specific handler need to be detached. All the handlers will be detached if not specified.
65
+ */
66
+ off(event, handler = null) {
67
+ this.events.off(event, handler);
68
+ }
69
+
70
+ /**
71
+ * Connects the client to xrpl server and do the config loading and subscriptions. 'subscribe' is called inside this.
72
+ * @returns boolean value, 'true' if success.
73
+ */
74
+ async connect() {
75
+ if (this.connected)
76
+ return true;
77
+
78
+ await this.xrplApi.connect();
79
+
80
+ // Invoking the info command to check the account existence. This is important to
81
+ // identify a network reset from XRPL.
82
+ await this.xrplAcc.getInfo();
83
+
84
+ this.config = await this.#getEvernodeConfig();
85
+ this.connected = true;
86
+
87
+ if (this.#autoSubscribe)
88
+ await this.subscribe();
89
+
90
+ return true;
91
+ }
92
+
93
+ /**
94
+ * Disconnects the client to xrpl server and do the un-subscriptions. 'unsubscribe' is called inside this.
95
+ */
96
+ async disconnect() {
97
+ await this.unsubscribe();
98
+
99
+ if (this.#ownsXrplApi)
100
+ await this.xrplApi.disconnect();
101
+ }
102
+
103
+ /**
104
+ * Subscribes to the registry client events.
105
+ */
106
+ async subscribe() {
107
+ await this.xrplAcc.subscribe();
108
+ }
109
+
110
+ /**
111
+ * Unsubscribes from the registry client events.
112
+ */
113
+ async unsubscribe() {
114
+ await this.xrplAcc.unsubscribe();
115
+ }
116
+
117
+ /**
118
+ * Get the EVR balance in the registry account.
119
+ * @returns The available EVR amount as a 'string'.
120
+ */
121
+ async getEVRBalance() {
122
+ const lines = await this.xrplAcc.getTrustLines(EvernodeConstants.EVR, this.config.evrIssuerAddress);
123
+ if (lines.length > 0)
124
+ return lines[0].balance;
125
+ else
126
+ return '0';
127
+ }
128
+
129
+ /**
130
+ * Get all XRPL hook states in the registry account.
131
+ * @returns The list of hook states including Evernode configuration and hosts.
132
+ */
133
+ async getHookStates() {
134
+ const regAcc = new XrplAccount(this.registryAddress, null, { xrplApi: this.xrplApi });
135
+ const configs = await regAcc.getNamespaceEntries(EvernodeConstants.HOOK_NAMESPACE);
136
+
137
+ if (configs)
138
+ return configs.filter(c => c.LedgerEntryType === 'HookState').map(c => { return { key: c.HookStateKey, data: c.HookStateData } });
139
+ return [];
140
+ }
141
+
142
+ /**
143
+ * Get the moment from the given XRP ledger index. (1 Moment - 1190 XRP ledgers).
144
+ * @param {number} ledgerIndex [Optional] Ledger index to get the moment value.
145
+ * @returns The moment of the given XPR ledger index as 'number'. Returns current moment if XRP ledger index is not given.
146
+ */
147
+ async getMoment(ledgerIndex = null) {
148
+ const lv = ledgerIndex || this.xrplApi.ledgerIndex;
149
+ const m = Math.floor((lv - this.config.momentBaseIdx) / this.config.momentSize);
150
+
151
+ await Promise.resolve(); // Awaiter placeholder for future async requirements.
152
+ return m;
153
+ }
154
+
155
+ /**
156
+ * Get start XRP ledger index of the moment (of the given XRPL index).
157
+ * @param {number} ledgerIndex [Optional] Ledger index to get the moment value.
158
+ * @returns The XRP ledger index of the moment (of the given XRPL index) as a 'number'. Returns the current moment's start XRP ledger index if ledger index parameter is not given.
159
+ */
160
+ async getMomentStartIndex(ledgerIndex = null) {
161
+ const lv = ledgerIndex || this.xrplApi.ledgerIndex;
162
+ const m = Math.floor((lv - this.config.momentBaseIdx) / this.config.momentSize);
163
+
164
+ await Promise.resolve(); // Awaiter placeholder for future async requirements.
165
+ return this.config.momentBaseIdx + (m * this.config.momentSize);
166
+ }
167
+
168
+ /**
169
+ * Get Evernode configuration
170
+ * @returns An object with all the configuration and their values.
171
+ */
172
+ async #getEvernodeConfig() {
173
+ let states = await this.getHookStates();
174
+ const configStateKeys = {
175
+ evrIssuerAddress: HookStateKeys.EVR_ISSUER_ADDR,
176
+ foundationAddress: HookStateKeys.FOUNDATION_ADDR,
177
+ hostRegFee: HookStateKeys.HOST_REG_FEE,
178
+ momentSize: HookStateKeys.MOMENT_SIZE,
179
+ hostHeartbeatFreq: HookStateKeys.HOST_HEARTBEAT_FREQ,
180
+ momentBaseIdx: HookStateKeys.MOMENT_BASE_IDX,
181
+ purchaserTargetPrice: HookStateKeys.PURCHASER_TARGET_PRICE,
182
+ leaseAcquireWindow: HookStateKeys.LEASE_ACQUIRE_WINDOW,
183
+ rewardInfo: HookStateKeys.REWARD_INFO,
184
+ rewardConfiguaration: HookStateKeys.REWARD_CONFIGURATION,
185
+ hostCount: HookStateKeys.HOST_COUNT
186
+ }
187
+ let config = {};
188
+ for (const [key, value] of Object.entries(configStateKeys)) {
189
+ const stateKey = Buffer.from(value, 'hex');
190
+ const stateData = Buffer.from(UtilHelpers.getStateData(states, value), 'hex');
191
+ const decoded = StateHelpers.decodeStateData(stateKey, stateData);
192
+ config[key] = decoded.value;
193
+ }
194
+ return config;
195
+ }
196
+
197
+ /**
198
+ * Loads the configs from XRPL hook and updates the in-memory config.
199
+ */
200
+ async refreshConfig() {
201
+ this.config = await this.#getEvernodeConfig();
202
+ }
203
+
204
+ /**
205
+ * Extracts transaction info and emits the Evernode event.
206
+ * @param {object} tx XRPL transaction to be handled.
207
+ * @param {any} error Error if there's any.
208
+ */
209
+ async #handleEvernodeEvent(tx, error) {
210
+ if (error)
211
+ console.error(error);
212
+ else if (!tx)
213
+ console.log('handleEvernodeEvent: Invalid transaction.');
214
+ else {
215
+ const ev = await this.extractEvernodeEvent(tx);
216
+ if (ev && this.#watchEvents.find(e => e === ev.name))
217
+ this.events.emit(ev.name, ev.data);
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Extracts the transaction info from a given transaction.
223
+ * @param {object} tx Transaction to be deserialized and extracted.
224
+ * @returns The event object in the format {name: '', data: {}}. Returns null if not handled. Note: You need to deserialize memos before passing the transaction to this function.
225
+ */
226
+ async extractEvernodeEvent(tx) {
227
+ if (tx.TransactionType === 'NFTokenAcceptOffer' && tx.NFTokenSellOffer && tx.Memos.length >= 1 &&
228
+ tx.Memos[0].type === MemoTypes.ACQUIRE_LEASE && tx.Memos[0].format === MemoFormats.BASE64 && tx.Memos[0].data) {
229
+
230
+ // If our account is the destination host account, then decrypt the payload.
231
+ let payload = tx.Memos[0].data;
232
+ if (tx.Destination === this.xrplAcc.address) {
233
+ const decrypted = this.accKeyPair && await EncryptionHelper.decrypt(this.accKeyPair.privateKey, payload);
234
+ if (decrypted)
235
+ payload = decrypted;
236
+ else
237
+ console.log('Failed to decrypt acquire data.');
238
+ }
239
+
240
+ return {
241
+ name: EvernodeEvents.AcquireLease,
242
+ data: {
243
+ transaction: tx,
244
+ host: tx.Destination,
245
+ nfTokenId: tx.NFTokenSellOffer?.NFTokenID,
246
+ leaseAmount: tx.NFTokenSellOffer?.Amount?.value,
247
+ acquireRefId: tx.hash,
248
+ tenant: tx.Account,
249
+ payload: payload
250
+ }
251
+ }
252
+ }
253
+
254
+ else if (tx.TransactionType === 'NFTokenAcceptOffer' && tx.NFTokenBuyOffer && tx.Memos.length >= 1 &&
255
+ tx.Memos[0].type === MemoTypes.HOST_POST_DEREG && tx.Memos[0].format === MemoFormats.HEX && tx.Memos[0].data) {
256
+ return {
257
+ name: EvernodeEvents.HostPostDeregistered,
258
+ data: {
259
+ transaction: tx,
260
+ nfTokenId: tx.NFTokenBuyOffer.NFTokenID,
261
+ flags: tx.Flags,
262
+ hash: tx.hash
263
+ }
264
+ }
265
+ }
266
+
267
+ else if (tx.Memos.length >= 2 &&
268
+ tx.Memos[0].type === MemoTypes.ACQUIRE_SUCCESS && tx.Memos[0].data &&
269
+ tx.Memos[1].type === MemoTypes.ACQUIRE_REF && tx.Memos[1].data) {
270
+
271
+ let payload = tx.Memos[0].data;
272
+ const acquireRefId = tx.Memos[1].data;
273
+
274
+ // If our account is the destination user account, then decrypt the payload.
275
+ if (tx.Memos[0].format === MemoFormats.BASE64 && tx.Destination === this.xrplAcc.address) {
276
+ const decrypted = this.accKeyPair && await EncryptionHelper.decrypt(this.accKeyPair.privateKey, payload);
277
+ if (decrypted)
278
+ payload = decrypted;
279
+ else
280
+ console.log('Failed to decrypt instance data.');
281
+ }
282
+
283
+ return {
284
+ name: EvernodeEvents.AcquireSuccess,
285
+ data: {
286
+ transaction: tx,
287
+ acquireRefId: acquireRefId,
288
+ payload: payload
289
+ }
290
+ }
291
+
292
+ }
293
+ else if (tx.Memos.length >= 2 &&
294
+ tx.Memos[0].type === MemoTypes.ACQUIRE_ERROR && tx.Memos[0].data &&
295
+ tx.Memos[1].type === MemoTypes.ACQUIRE_REF && tx.Memos[1].data) {
296
+
297
+ let error = tx.Memos[0].data;
298
+ const acquireRefId = tx.Memos[1].data;
299
+
300
+ if (tx.Memos[0].format === MemoFormats.JSON)
301
+ error = JSON.parse(error).reason;
302
+
303
+ return {
304
+ name: EvernodeEvents.AcquireError,
305
+ data: {
306
+ transaction: tx,
307
+ acquireRefId: acquireRefId,
308
+ reason: error
309
+ }
310
+ }
311
+ }
312
+ else if (tx.Memos.length >= 1 &&
313
+ tx.Memos[0].type === MemoTypes.HOST_REG && tx.Memos[0].format === MemoFormats.TEXT && tx.Memos[0].data) {
314
+
315
+ const parts = tx.Memos[0].data.split(';');
316
+ return {
317
+ name: EvernodeEvents.HostRegistered,
318
+ data: {
319
+ transaction: tx,
320
+ host: tx.Account,
321
+ token: parts[0],
322
+ instanceSize: parts[1],
323
+ location: parts[2]
324
+ }
325
+ }
326
+ }
327
+ else if (tx.Memos.length >= 1 && tx.Memos[0].type === MemoTypes.HOST_DEREG) {
328
+ return {
329
+ name: EvernodeEvents.HostDeregistered,
330
+ data: {
331
+ transaction: tx,
332
+ host: tx.Account
333
+ }
334
+ }
335
+ }
336
+ else if (tx.Memos.length >= 1 &&
337
+ tx.Memos[0].type === MemoTypes.HEARTBEAT) {
338
+
339
+ return {
340
+ name: EvernodeEvents.Heartbeat,
341
+ data: {
342
+ transaction: tx,
343
+ host: tx.Account
344
+ }
345
+ }
346
+ }
347
+ else if (tx.Memos.length >= 1 &&
348
+ tx.Memos[0].type === MemoTypes.EXTEND_LEASE && tx.Memos[0].format === MemoFormats.HEX && tx.Memos[0].data) {
349
+
350
+ let nfTokenId = tx.Memos[0].data;
351
+
352
+ return {
353
+ name: EvernodeEvents.ExtendLease,
354
+ data: {
355
+ transaction: tx,
356
+ extendRefId: tx.hash,
357
+ tenant: tx.Account,
358
+ currency: tx.Amount.currency,
359
+ payment: parseFloat(tx.Amount.value),
360
+ nfTokenId: nfTokenId
361
+ }
362
+ }
363
+ }
364
+ else if (tx.Memos.length >= 2 &&
365
+ tx.Memos[0].type === MemoTypes.EXTEND_SUCCESS && tx.Memos[0].format === MemoFormats.HEX && tx.Memos[0].data &&
366
+ tx.Memos[1].type === MemoTypes.EXTEND_REF && tx.Memos[1].format === MemoFormats.HEX && tx.Memos[1].data) {
367
+
368
+ const extendResBuf = Buffer.from(tx.Memos[0].data, 'hex');
369
+ const extendRefId = tx.Memos[1].data;
370
+
371
+ return {
372
+ name: EvernodeEvents.ExtendSuccess,
373
+ data: {
374
+ transaction: tx,
375
+ extendRefId: extendRefId,
376
+ expiryMoment: extendResBuf.readUInt32BE()
377
+ }
378
+ }
379
+
380
+ }
381
+ else if (tx.Memos.length >= 2 &&
382
+ tx.Memos[0].type === MemoTypes.EXTEND_ERROR && tx.Memos[0].data &&
383
+ tx.Memos[1].type === MemoTypes.EXTEND_REF && tx.Memos[1].data) {
384
+
385
+ let error = tx.Memos[0].data;
386
+ const extendRefId = tx.Memos[1].data;
387
+
388
+ if (tx.Memos[0].format === MemoFormats.JSON)
389
+ error = JSON.parse(error).reason;
390
+
391
+ return {
392
+ name: EvernodeEvents.ExtendError,
393
+ data: {
394
+ transaction: tx,
395
+ extendRefId: extendRefId,
396
+ reason: error
397
+ }
398
+ }
399
+ }
400
+ else if (tx.Memos.length >= 1 &&
401
+ tx.Memos[0].type === MemoTypes.REGISTRY_INIT && tx.Memos[0].format === MemoFormats.HEX && tx.Memos[0].data) {
402
+
403
+ return {
404
+ name: EvernodeEvents.RegistryInitialized,
405
+ data: {
406
+ transaction: tx
407
+ }
408
+ }
409
+ }
410
+ else if (tx.Memos.length >= 1 &&
411
+ tx.Memos[0].type === MemoTypes.HOST_UPDATE_INFO && tx.Memos[0].format === MemoFormats.TEXT && tx.Memos[0].data) {
412
+
413
+ const specs = tx.Memos[0].data.split(';');
414
+
415
+ return {
416
+ name: EvernodeEvents.HostRegUpdated,
417
+ data: {
418
+ transaction: tx,
419
+ host: tx.Account,
420
+ version: specs[specs.length - 1],
421
+ specs: specs,
422
+ }
423
+ }
424
+ }
425
+ else if (tx.Memos.length >= 1 &&
426
+ tx.Memos[0].type === MemoTypes.DEAD_HOST_PRUNE && tx.Memos[0].format === MemoFormats.HEX && tx.Memos[0].data) {
427
+
428
+ const addrsBuf = Buffer.from(tx.Memos[0].data, 'hex');
429
+
430
+ return {
431
+ name: EvernodeEvents.DeadHostPrune,
432
+ data: {
433
+ transaction: tx,
434
+ host: codec.encodeAccountID(addrsBuf)
435
+ }
436
+ }
437
+ }
438
+
439
+ return null;
440
+ }
441
+
442
+ /**
443
+ * Get the registered host information.
444
+ * @param {string} hostAddress [Optional] Address of the host.
445
+ * @returns The registered host information object. Returns null is not registered.
446
+ */
447
+ async getHostInfo(hostAddress = this.xrplAcc.address) {
448
+ try {
449
+ const addrStateKey = StateHelpers.generateHostAddrStateKey(hostAddress);
450
+ const addrStateIndex = StateHelpers.getHookStateIndex(this.registryAddress, addrStateKey);
451
+ const addrLedgerEntry = await this.xrplApi.getLedgerEntry(addrStateIndex);
452
+ const addrStateData = addrLedgerEntry?.HookStateData;
453
+ if (addrStateData) {
454
+ const addrStateDecoded = StateHelpers.decodeHostAddressState(Buffer.from(addrStateKey, 'hex'), Buffer.from(addrStateData, 'hex'));
455
+ const curMomentStartIdx = await this.getMomentStartIndex();
456
+ addrStateDecoded.active = (addrStateDecoded.lastHeartbeatLedger > (this.config.hostHeartbeatFreq * this.config.momentSize) ?
457
+ (addrStateDecoded.lastHeartbeatLedger >= (curMomentStartIdx - (this.config.hostHeartbeatFreq * this.config.momentSize))) :
458
+ (addrStateDecoded.lastHeartbeatLedger > 0))
459
+
460
+ const nftIdStatekey = StateHelpers.generateTokenIdStateKey(addrStateDecoded.nfTokenId);
461
+ const nftIdStateIndex = StateHelpers.getHookStateIndex(this.registryAddress, nftIdStatekey);
462
+ const nftIdLedgerEntry = await this.xrplApi.getLedgerEntry(nftIdStateIndex);
463
+
464
+ const nftIdStateData = nftIdLedgerEntry?.HookStateData;
465
+ if (nftIdStateData) {
466
+ const nftIdStateDecoded = StateHelpers.decodeTokenIdState(Buffer.from(nftIdStateData, 'hex'));
467
+ return { ...addrStateDecoded, ...nftIdStateDecoded };
468
+ }
469
+ }
470
+ }
471
+ catch (e) {
472
+ // If the exception is entryNotFound from Rippled there's no entry for the host, So return null.
473
+ if (e?.data?.error !== 'entryNotFound')
474
+ throw e;
475
+ }
476
+
477
+ return null;
478
+ }
479
+
480
+ /**
481
+ * Get all the hosts registered in Evernode. The result's are paginated. Default page size is 20. Note: Specifying both filter and pagination does not supported.
482
+ * @param {object} filters [Optional] Filter criteria to filter the hosts. The filter key can be a either property of the host.
483
+ * @param {number} pageSize [Optional] Page size for the results.
484
+ * @param {string} nextPageToken [Optional] Next page's token, If received by the previous result set.
485
+ * @returns The list of active hosts. The response will be in '{data: [], nextPageToken: ''}' only if there are more pages. Otherwise the response will only contain the host list.
486
+ */
487
+ async getHosts(filters = null, pageSize = null, nextPageToken = null) {
488
+ const hosts = await this.#firestoreHandler.getHosts(filters, pageSize, nextPageToken);
489
+ const curMomentStartIdx = await this.getMomentStartIndex();
490
+ // Populate the host active status.
491
+ (hosts.nextPageToken ? hosts.data : hosts).forEach(h => {
492
+ h.active = (h.lastHeartbeatLedger > (this.config.hostHeartbeatFreq * this.config.momentSize) ?
493
+ (h.lastHeartbeatLedger >= (curMomentStartIdx - (this.config.hostHeartbeatFreq * this.config.momentSize))) :
494
+ (h.lastHeartbeatLedger > 0))
495
+ });
496
+ return hosts;
497
+ }
498
+
499
+ /**
500
+ * Get all Evernode configuration without paginating.
501
+ * @returns The list of configuration.
502
+ */
503
+ async getAllConfigs() {
504
+ let fullConfigList = [];
505
+ const configs = await this.#firestoreHandler.getConfigs();
506
+ if (configs.nextPageToken) {
507
+ let currentPageToken = configs.nextPageToken;
508
+ let nextConfigs = null;
509
+ fullConfigList = fullConfigList.concat(configs.data);
510
+ while (currentPageToken) {
511
+ nextConfigs = await this.#firestoreHandler.getConfigs(null, 50, currentPageToken);
512
+ fullConfigList = fullConfigList.concat(nextConfigs.nextPageToken ? nextConfigs.data : nextConfigs);
513
+ currentPageToken = nextConfigs.nextPageToken;
514
+ }
515
+ } else {
516
+ fullConfigList = fullConfigList.concat(configs);
517
+ }
518
+
519
+ return fullConfigList;
520
+ }
521
+
522
+ /**
523
+ * Get all the hosts without paginating.
524
+ * @returns The list of hosts.
525
+ */
526
+ async getAllHosts() {
527
+ let fullHostList = [];
528
+ const hosts = await this.#firestoreHandler.getHosts();
529
+ if (hosts.nextPageToken) {
530
+ let currentPageToken = hosts.nextPageToken;
531
+ let nextHosts = null;
532
+ fullHostList = fullHostList.concat(hosts.data);
533
+ while (currentPageToken) {
534
+ nextHosts = await this.#firestoreHandler.getHosts(null, 50, currentPageToken);
535
+ fullHostList = fullHostList.concat(nextHosts.nextPageToken ? nextHosts.data : nextHosts);
536
+ currentPageToken = nextHosts.nextPageToken;
537
+ }
538
+ } else {
539
+ fullHostList = fullHostList.concat(hosts);
540
+ }
541
+
542
+ return fullHostList;
543
+ }
544
+
545
+ /**
546
+ * Remove a host which is inactive for a long period. The inactivity is checked by Evernode it self and only pruned if inactive thresholds are met.
547
+ * @param {string} hostAddress XRPL address of the host to be pruned.
548
+ */
549
+ async pruneDeadHost(hostAddress) {
550
+ if (this.xrplAcc.address === this.registryAddress)
551
+ throw 'Invalid function call';
552
+
553
+ let memoData = Buffer.allocUnsafe(20);
554
+ codec.decodeAccountID(hostAddress).copy(memoData);
555
+
556
+ await this.xrplAcc.makePayment(this.registryAddress,
557
+ XrplConstants.MIN_XRP_AMOUNT,
558
+ XrplConstants.XRP,
559
+ null,
560
+ [{ type: MemoTypes.DEAD_HOST_PRUNE, format: MemoFormats.HEX, data: memoData.toString('hex') }]);
561
+
562
+ }
563
+ }
564
+
565
+ module.exports = {
566
+ BaseEvernodeClient
567
+ }