evernode-js-client 0.5.10 → 0.5.11

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,609 @@
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
+ const { EvernodeHelpers } = require('../evernode-helpers');
14
+
15
+ class BaseEvernodeClient {
16
+
17
+ #watchEvents;
18
+ #autoSubscribe;
19
+ #ownsXrplApi = false;
20
+ #firestoreHandler;
21
+
22
+ constructor(xrpAddress, xrpSecret, watchEvents, autoSubscribe = false, options = {}) {
23
+
24
+ this.connected = false;
25
+ this.registryAddress = options.registryAddress || DefaultValues.registryAddress;
26
+
27
+ this.xrplApi = options.xrplApi || DefaultValues.xrplApi || new XrplApi(options.rippledServer);
28
+ if (!options.xrplApi && !DefaultValues.xrplApi)
29
+ this.#ownsXrplApi = true;
30
+
31
+ this.xrplAcc = new XrplAccount(xrpAddress, xrpSecret, { xrplApi: this.xrplApi });
32
+ this.accKeyPair = xrpSecret && this.xrplAcc.deriveKeypair();
33
+ this.#watchEvents = watchEvents;
34
+ this.#autoSubscribe = autoSubscribe;
35
+ this.events = new EventEmitter();
36
+ this.#firestoreHandler = new FirestoreHandler()
37
+
38
+ this.xrplAcc.on(XrplApiEvents.PAYMENT, (tx, error) => this.#handleEvernodeEvent(tx, error));
39
+ this.xrplAcc.on(XrplApiEvents.NFT_OFFER_CREATE, (tx, error) => this.#handleEvernodeEvent(tx, error));
40
+ this.xrplAcc.on(XrplApiEvents.NFT_OFFER_ACCEPT, (tx, error) => this.#handleEvernodeEvent(tx, error));
41
+
42
+ }
43
+
44
+ /**
45
+ * Listens to the subscribed events. This will listen for the event without detaching the handler until it's 'off'.
46
+ * @param {string} event Event name.
47
+ * @param {function(event)} handler Callback function to handle the event.
48
+ */
49
+ on(event, handler) {
50
+ this.events.on(event, handler);
51
+ }
52
+
53
+ /**
54
+ * Listens to the subscribed events. This will listen only once and detach the handler.
55
+ * @param {string} event Event name.
56
+ * @param {function(event)} handler Callback function to handle the event.
57
+ */
58
+ once(event, handler) {
59
+ this.events.once(event, handler);
60
+ }
61
+
62
+ /**
63
+ * Detach the listener event.
64
+ * @param {string} event Event name.
65
+ * @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.
66
+ */
67
+ off(event, handler = null) {
68
+ this.events.off(event, handler);
69
+ }
70
+
71
+ /**
72
+ * Connects the client to xrpl server and do the config loading and subscriptions. 'subscribe' is called inside this.
73
+ * @returns boolean value, 'true' if success.
74
+ */
75
+ async connect() {
76
+ if (this.connected)
77
+ return true;
78
+
79
+ await this.xrplApi.connect();
80
+
81
+ // Invoking the info command to check the account existence. This is important to
82
+ // identify a network reset from XRPL.
83
+ await this.xrplAcc.getInfo();
84
+
85
+ this.config = await this.#getEvernodeConfig();
86
+ this.connected = true;
87
+
88
+ if (this.#autoSubscribe)
89
+ await this.subscribe();
90
+
91
+ return true;
92
+ }
93
+
94
+ /**
95
+ * Disconnects the client to xrpl server and do the un-subscriptions. 'unsubscribe' is called inside this.
96
+ */
97
+ async disconnect() {
98
+ await this.unsubscribe();
99
+
100
+ if (this.#ownsXrplApi)
101
+ await this.xrplApi.disconnect();
102
+ }
103
+
104
+ /**
105
+ * Subscribes to the registry client events.
106
+ */
107
+ async subscribe() {
108
+ await this.xrplAcc.subscribe();
109
+ }
110
+
111
+ /**
112
+ * Unsubscribes from the registry client events.
113
+ */
114
+ async unsubscribe() {
115
+ await this.xrplAcc.unsubscribe();
116
+ }
117
+
118
+ /**
119
+ * Get the EVR balance in the registry account.
120
+ * @returns The available EVR amount as a 'string'.
121
+ */
122
+ async getEVRBalance() {
123
+ const lines = await this.xrplAcc.getTrustLines(EvernodeConstants.EVR, this.config.evrIssuerAddress);
124
+ if (lines.length > 0)
125
+ return lines[0].balance;
126
+ else
127
+ return '0';
128
+ }
129
+
130
+ /**
131
+ * Get all XRPL hook states in the registry account.
132
+ * @returns The list of hook states including Evernode configuration and hosts.
133
+ */
134
+ async getHookStates() {
135
+ const regAcc = new XrplAccount(this.registryAddress, null, { xrplApi: this.xrplApi });
136
+ const configs = await regAcc.getNamespaceEntries(EvernodeConstants.HOOK_NAMESPACE);
137
+
138
+ if (configs)
139
+ return configs.filter(c => c.LedgerEntryType === 'HookState').map(c => { return { key: c.HookStateKey, data: c.HookStateData } });
140
+ return [];
141
+ }
142
+
143
+ /**
144
+ * Get the moment from the given index (timestamp).
145
+ * @param {number} index [Optional] Index (timestamp) to get the moment value.
146
+ * @returns The moment of the given index (timestamp) as 'number'. Returns current moment if index (timestamp) is not given.
147
+ */
148
+ async getMoment(index = null) {
149
+ const i = index || UtilHelpers.getCurrentUnixTime();
150
+ const m = this.config.momentBaseInfo.baseTransitionMoment + Math.floor((i - this.config.momentBaseInfo.baseIdx) / this.config.momentSize);
151
+ await Promise.resolve();
152
+ return m;
153
+ }
154
+
155
+ /**
156
+ * Get start index (timestamp) of the moment.
157
+ * @param {number} index [Optional] Index (timestamp) to get the moment value.
158
+ * @returns The index (timestamp) of the moment as a 'number'. Returns the current moment's start index (timestamp) if ledger index parameter is not given.
159
+ */
160
+ async getMomentStartIndex(index = null) {
161
+ const i = index || UtilHelpers.getCurrentUnixTime();
162
+
163
+ const m = Math.floor((i - this.config.momentBaseInfo.baseIdx) / this.config.momentSize);
164
+
165
+ await Promise.resolve(); // Awaiter placeholder for future async requirements.
166
+ return this.config.momentBaseInfo.baseIdx + (m * this.config.momentSize);
167
+ }
168
+
169
+ /**
170
+ * Get Evernode configuration
171
+ * @returns An object with all the configuration and their values.
172
+ */
173
+ async #getEvernodeConfig() {
174
+ let states = await this.getHookStates();
175
+ const configStateKeys = {
176
+ evrIssuerAddress: HookStateKeys.EVR_ISSUER_ADDR,
177
+ foundationAddress: HookStateKeys.FOUNDATION_ADDR,
178
+ hostRegFee: HookStateKeys.HOST_REG_FEE,
179
+ momentSize: HookStateKeys.MOMENT_SIZE,
180
+ hostHeartbeatFreq: HookStateKeys.HOST_HEARTBEAT_FREQ,
181
+ momentBaseInfo: HookStateKeys.MOMENT_BASE_INFO,
182
+ purchaserTargetPrice: HookStateKeys.PURCHASER_TARGET_PRICE,
183
+ leaseAcquireWindow: HookStateKeys.LEASE_ACQUIRE_WINDOW,
184
+ rewardInfo: HookStateKeys.REWARD_INFO,
185
+ rewardConfiguration: HookStateKeys.REWARD_CONFIGURATION,
186
+ hostCount: HookStateKeys.HOST_COUNT,
187
+ momentTransitInfo: HookStateKeys.MOMENT_TRANSIT_INFO,
188
+ registryMaxTrxEmitFee: HookStateKeys.MAX_TRX_EMISSION_FEE
189
+ }
190
+ let config = {};
191
+ for (const [key, value] of Object.entries(configStateKeys)) {
192
+ const stateKey = Buffer.from(value, 'hex');
193
+ const stateDataBin = StateHelpers.getStateData(states, value);
194
+ if (stateDataBin) {
195
+ const stateData = Buffer.from(StateHelpers.getStateData(states, value), 'hex');
196
+ const decoded = StateHelpers.decodeStateData(stateKey, stateData);
197
+ config[key] = decoded.value;
198
+ }
199
+ }
200
+ return config;
201
+ }
202
+
203
+ /**
204
+ * Loads the configs from XRPL hook and updates the in-memory config.
205
+ */
206
+ async refreshConfig() {
207
+ this.config = await this.#getEvernodeConfig();
208
+ }
209
+
210
+ /**
211
+ * Extracts transaction info and emits the Evernode event.
212
+ * @param {object} tx XRPL transaction to be handled.
213
+ * @param {any} error Error if there's any.
214
+ */
215
+ async #handleEvernodeEvent(tx, error) {
216
+ if (error)
217
+ console.error(error);
218
+ else if (!tx)
219
+ console.log('handleEvernodeEvent: Invalid transaction.');
220
+ else {
221
+ const ev = await this.extractEvernodeEvent(tx);
222
+ if (ev && this.#watchEvents.find(e => e === ev.name))
223
+ this.events.emit(ev.name, ev.data);
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Extracts the transaction info from a given transaction.
229
+ * @param {object} tx Transaction to be deserialized and extracted.
230
+ * @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.
231
+ */
232
+ async extractEvernodeEvent(tx) {
233
+ if (tx.TransactionType === 'NFTokenAcceptOffer' && tx.NFTokenSellOffer && tx.Memos.length >= 1 &&
234
+ tx.Memos[0].type === MemoTypes.ACQUIRE_LEASE && tx.Memos[0].format === MemoFormats.BASE64 && tx.Memos[0].data) {
235
+
236
+ // If our account is the destination host account, then decrypt the payload.
237
+ let payload = tx.Memos[0].data;
238
+ if (tx.Destination === this.xrplAcc.address) {
239
+ const decrypted = this.accKeyPair && await EncryptionHelper.decrypt(this.accKeyPair.privateKey, payload);
240
+ if (decrypted)
241
+ payload = decrypted;
242
+ else
243
+ console.log('Failed to decrypt acquire data.');
244
+ }
245
+
246
+ return {
247
+ name: EvernodeEvents.AcquireLease,
248
+ data: {
249
+ transaction: tx,
250
+ host: tx.Destination,
251
+ nfTokenId: tx.NFTokenSellOffer?.NFTokenID,
252
+ leaseAmount: tx.NFTokenSellOffer?.Amount?.value,
253
+ acquireRefId: tx.hash,
254
+ tenant: tx.Account,
255
+ payload: payload
256
+ }
257
+ }
258
+ }
259
+
260
+ else if (tx.TransactionType === 'NFTokenAcceptOffer' && tx.NFTokenBuyOffer && tx.Memos.length >= 1 &&
261
+ tx.Memos[0].type === MemoTypes.HOST_POST_DEREG && tx.Memos[0].format === MemoFormats.HEX && tx.Memos[0].data) {
262
+ return {
263
+ name: EvernodeEvents.HostPostDeregistered,
264
+ data: {
265
+ transaction: tx,
266
+ nfTokenId: tx.NFTokenBuyOffer.NFTokenID,
267
+ flags: tx.Flags,
268
+ hash: tx.hash
269
+ }
270
+ }
271
+ }
272
+
273
+ else if (tx.Memos.length >= 2 &&
274
+ tx.Memos[0].type === MemoTypes.ACQUIRE_SUCCESS && tx.Memos[0].data &&
275
+ tx.Memos[1].type === MemoTypes.ACQUIRE_REF && tx.Memos[1].data) {
276
+
277
+ let payload = tx.Memos[0].data;
278
+ const acquireRefId = tx.Memos[1].data;
279
+
280
+ // If our account is the destination user account, then decrypt the payload.
281
+ if (tx.Memos[0].format === MemoFormats.BASE64 && tx.Destination === this.xrplAcc.address) {
282
+ const decrypted = this.accKeyPair && await EncryptionHelper.decrypt(this.accKeyPair.privateKey, payload);
283
+ if (decrypted)
284
+ payload = decrypted;
285
+ else
286
+ console.log('Failed to decrypt instance data.');
287
+ }
288
+
289
+ return {
290
+ name: EvernodeEvents.AcquireSuccess,
291
+ data: {
292
+ transaction: tx,
293
+ acquireRefId: acquireRefId,
294
+ payload: payload
295
+ }
296
+ }
297
+
298
+ }
299
+ else if (tx.Memos.length >= 2 &&
300
+ tx.Memos[0].type === MemoTypes.ACQUIRE_ERROR && tx.Memos[0].data &&
301
+ tx.Memos[1].type === MemoTypes.ACQUIRE_REF && tx.Memos[1].data) {
302
+
303
+ let error = tx.Memos[0].data;
304
+ const acquireRefId = tx.Memos[1].data;
305
+
306
+ if (tx.Memos[0].format === MemoFormats.JSON)
307
+ error = JSON.parse(error).reason;
308
+
309
+ return {
310
+ name: EvernodeEvents.AcquireError,
311
+ data: {
312
+ transaction: tx,
313
+ acquireRefId: acquireRefId,
314
+ reason: error
315
+ }
316
+ }
317
+ }
318
+ else if (tx.Memos.length >= 1 &&
319
+ tx.Memos[0].type === MemoTypes.HOST_REG && tx.Memos[0].format === MemoFormats.HEX && tx.Memos[0].data) {
320
+
321
+ return {
322
+ name: EvernodeEvents.HostRegistered,
323
+ data: {
324
+ transaction: tx,
325
+ host: tx.Account
326
+ }
327
+ }
328
+ }
329
+ else if (tx.Memos.length >= 1 && tx.Memos[0].type === MemoTypes.HOST_DEREG) {
330
+ return {
331
+ name: EvernodeEvents.HostDeregistered,
332
+ data: {
333
+ transaction: tx,
334
+ host: tx.Account
335
+ }
336
+ }
337
+ }
338
+ else if (tx.Memos.length >= 1 &&
339
+ tx.Memos[0].type === MemoTypes.HEARTBEAT) {
340
+
341
+ return {
342
+ name: EvernodeEvents.Heartbeat,
343
+ data: {
344
+ transaction: tx,
345
+ host: tx.Account
346
+ }
347
+ }
348
+ }
349
+ else if (tx.Memos.length >= 1 &&
350
+ tx.Memos[0].type === MemoTypes.EXTEND_LEASE && tx.Memos[0].format === MemoFormats.HEX && tx.Memos[0].data) {
351
+
352
+ let nfTokenId = tx.Memos[0].data;
353
+
354
+ return {
355
+ name: EvernodeEvents.ExtendLease,
356
+ data: {
357
+ transaction: tx,
358
+ extendRefId: tx.hash,
359
+ tenant: tx.Account,
360
+ currency: tx.Amount.currency,
361
+ payment: parseFloat(tx.Amount.value),
362
+ nfTokenId: nfTokenId
363
+ }
364
+ }
365
+ }
366
+ else if (tx.Memos.length >= 2 &&
367
+ tx.Memos[0].type === MemoTypes.EXTEND_SUCCESS && tx.Memos[0].format === MemoFormats.HEX && tx.Memos[0].data &&
368
+ tx.Memos[1].type === MemoTypes.EXTEND_REF && tx.Memos[1].format === MemoFormats.HEX && tx.Memos[1].data) {
369
+
370
+ const extendResBuf = Buffer.from(tx.Memos[0].data, 'hex');
371
+ const extendRefId = tx.Memos[1].data;
372
+
373
+ return {
374
+ name: EvernodeEvents.ExtendSuccess,
375
+ data: {
376
+ transaction: tx,
377
+ extendRefId: extendRefId,
378
+ expiryMoment: extendResBuf.readUInt32BE()
379
+ }
380
+ }
381
+
382
+ }
383
+ else if (tx.Memos.length >= 2 &&
384
+ tx.Memos[0].type === MemoTypes.EXTEND_ERROR && tx.Memos[0].data &&
385
+ tx.Memos[1].type === MemoTypes.EXTEND_REF && tx.Memos[1].data) {
386
+
387
+ let error = tx.Memos[0].data;
388
+ const extendRefId = tx.Memos[1].data;
389
+
390
+ if (tx.Memos[0].format === MemoFormats.JSON)
391
+ error = JSON.parse(error).reason;
392
+
393
+ return {
394
+ name: EvernodeEvents.ExtendError,
395
+ data: {
396
+ transaction: tx,
397
+ extendRefId: extendRefId,
398
+ reason: error
399
+ }
400
+ }
401
+ }
402
+ else if (tx.Memos.length >= 1 &&
403
+ tx.Memos[0].type === MemoTypes.REGISTRY_INIT && tx.Memos[0].format === MemoFormats.HEX && tx.Memos[0].data) {
404
+
405
+ return {
406
+ name: EvernodeEvents.RegistryInitialized,
407
+ data: {
408
+ transaction: tx
409
+ }
410
+ }
411
+ }
412
+ else if (tx.Memos.length >= 1 &&
413
+ tx.Memos[0].type === MemoTypes.HOST_UPDATE_INFO && tx.Memos[0].format === MemoFormats.HEX && tx.Memos[0].data) {
414
+
415
+ return {
416
+ name: EvernodeEvents.HostRegUpdated,
417
+ data: {
418
+ transaction: tx,
419
+ host: tx.Account
420
+ }
421
+ }
422
+ }
423
+ else if (tx.Memos.length >= 1 &&
424
+ tx.Memos[0].type === MemoTypes.DEAD_HOST_PRUNE && tx.Memos[0].format === MemoFormats.HEX && tx.Memos[0].data) {
425
+
426
+ const addrsBuf = Buffer.from(tx.Memos[0].data, 'hex');
427
+
428
+ return {
429
+ name: EvernodeEvents.DeadHostPrune,
430
+ data: {
431
+ transaction: tx,
432
+ host: codec.encodeAccountID(addrsBuf)
433
+ }
434
+ }
435
+ }
436
+ else if (tx.Memos.length >= 1 &&
437
+ tx.Memos[0].type === MemoTypes.HOST_REBATE) {
438
+
439
+ return {
440
+ name: EvernodeEvents.HostRebate,
441
+ data: {
442
+ transaction: tx,
443
+ host: tx.Account
444
+ }
445
+ }
446
+ }
447
+ else if (tx.Memos.length >= 1 &&
448
+ tx.Memos[0].type === MemoTypes.HOST_TRANSFER && tx.Memos[0].format === MemoFormats.HEX && tx.Memos[0].data) {
449
+
450
+ const addrsBuf = Buffer.from(tx.Memos[0].data, 'hex');
451
+
452
+ return {
453
+ name: EvernodeEvents.HostTransfer,
454
+ data: {
455
+ transaction: tx,
456
+ transferee: codec.encodeAccountID(addrsBuf)
457
+ }
458
+ }
459
+ }
460
+
461
+ return null;
462
+ }
463
+
464
+ /**
465
+ * Get the registered host information.
466
+ * @param {string} hostAddress [Optional] Address of the host.
467
+ * @returns The registered host information object. Returns null is not registered.
468
+ */
469
+ async getHostInfo(hostAddress = this.xrplAcc.address) {
470
+ try {
471
+ const addrStateKey = StateHelpers.generateHostAddrStateKey(hostAddress);
472
+ const addrStateIndex = StateHelpers.getHookStateIndex(this.registryAddress, addrStateKey);
473
+ const addrLedgerEntry = await this.xrplApi.getLedgerEntry(addrStateIndex);
474
+ const addrStateData = addrLedgerEntry?.HookStateData;
475
+ if (addrStateData) {
476
+ const addrStateDecoded = StateHelpers.decodeHostAddressState(Buffer.from(addrStateKey, 'hex'), Buffer.from(addrStateData, 'hex'));
477
+ const curMomentStartIdx = await this.getMomentStartIndex();
478
+ addrStateDecoded.active = (addrStateDecoded.lastHeartbeatIndex > (this.config.hostHeartbeatFreq * this.config.momentSize) ?
479
+ (addrStateDecoded.lastHeartbeatIndex >= (curMomentStartIdx - (this.config.hostHeartbeatFreq * this.config.momentSize))) :
480
+ (addrStateDecoded.lastHeartbeatIndex > 0))
481
+
482
+ const nftIdStatekey = StateHelpers.generateTokenIdStateKey(addrStateDecoded.nfTokenId);
483
+ const nftIdStateIndex = StateHelpers.getHookStateIndex(this.registryAddress, nftIdStatekey);
484
+ const nftIdLedgerEntry = await this.xrplApi.getLedgerEntry(nftIdStateIndex);
485
+
486
+ const nftIdStateData = nftIdLedgerEntry?.HookStateData;
487
+ if (nftIdStateData) {
488
+ const nftIdStateDecoded = StateHelpers.decodeTokenIdState(Buffer.from(nftIdStateData, 'hex'));
489
+ return { ...addrStateDecoded, ...nftIdStateDecoded };
490
+ }
491
+ }
492
+ }
493
+ catch (e) {
494
+ // If the exception is entryNotFound from Rippled there's no entry for the host, So return null.
495
+ if (e?.data?.error !== 'entryNotFound')
496
+ throw e;
497
+ }
498
+
499
+ return null;
500
+ }
501
+
502
+ /**
503
+ * 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.
504
+ * @param {object} filters [Optional] Filter criteria to filter the hosts. The filter key can be a either property of the host.
505
+ * @param {number} pageSize [Optional] Page size for the results.
506
+ * @param {string} nextPageToken [Optional] Next page's token, If received by the previous result set.
507
+ * @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.
508
+ */
509
+ async getHosts(filters = null, pageSize = null, nextPageToken = null) {
510
+ const hosts = await this.#firestoreHandler.getHosts(filters, pageSize, nextPageToken);
511
+ const curMomentStartIdx = await this.getMomentStartIndex();
512
+
513
+ return await Promise.all((hosts.nextPageToken ? hosts.data : hosts).map(async host => {
514
+ const hostAcc = new XrplAccount(host.address);
515
+ host.domain = await hostAcc.getDomain();
516
+
517
+ host.active = (host.lastHeartbeatIndex > (this.config.hostHeartbeatFreq * this.config.momentSize) ?
518
+ (host.lastHeartbeatIndex >= (curMomentStartIdx - (this.config.hostHeartbeatFreq * this.config.momentSize))) :
519
+ (host.lastHeartbeatIndex > 0));
520
+ return host;
521
+ }));
522
+ }
523
+
524
+ /**
525
+ * Get all Evernode configuration without paginating.
526
+ * @returns The list of configuration.
527
+ */
528
+ async getAllConfigs() {
529
+ let fullConfigList = [];
530
+ const configs = await this.#firestoreHandler.getConfigs();
531
+ if (configs.nextPageToken) {
532
+ let currentPageToken = configs.nextPageToken;
533
+ let nextConfigs = null;
534
+ fullConfigList = fullConfigList.concat(configs.data);
535
+ while (currentPageToken) {
536
+ nextConfigs = await this.#firestoreHandler.getConfigs(null, 50, currentPageToken);
537
+ fullConfigList = fullConfigList.concat(nextConfigs.nextPageToken ? nextConfigs.data : nextConfigs);
538
+ currentPageToken = nextConfigs.nextPageToken;
539
+ }
540
+ } else {
541
+ fullConfigList = fullConfigList.concat(configs);
542
+ }
543
+
544
+ return fullConfigList;
545
+ }
546
+
547
+ /**
548
+ * Get all the hosts without paginating.
549
+ * @returns The list of hosts.
550
+ */
551
+ async getAllHosts() {
552
+ let fullHostList = [];
553
+ const hosts = await this.#firestoreHandler.getHosts();
554
+ if (hosts.nextPageToken) {
555
+ let currentPageToken = hosts.nextPageToken;
556
+ let nextHosts = null;
557
+ fullHostList = fullHostList.concat(hosts.data);
558
+ while (currentPageToken) {
559
+ nextHosts = await this.#firestoreHandler.getHosts(null, 50, currentPageToken);
560
+ fullHostList = fullHostList.concat(nextHosts.nextPageToken ? nextHosts.data : nextHosts);
561
+ currentPageToken = nextHosts.nextPageToken;
562
+ }
563
+ } else {
564
+ fullHostList = fullHostList.concat(hosts);
565
+ }
566
+
567
+ return fullHostList;
568
+ }
569
+
570
+ /**
571
+ * 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.
572
+ * @param {string} hostAddress XRPL address of the host to be pruned.
573
+ */
574
+ async pruneDeadHost(hostAddress) {
575
+ if (this.xrplAcc.address === this.registryAddress)
576
+ throw 'Invalid function call';
577
+
578
+ let memoData = Buffer.allocUnsafe(20);
579
+ codec.decodeAccountID(hostAddress).copy(memoData);
580
+
581
+ // To obtain registration NFT Page Keylet and index.
582
+ const hostAcc = new XrplAccount(hostAddress, null, { xrplApi: this.xrplApi });
583
+ const regNFT = (await hostAcc.getNfts()).find(n => n.URI.startsWith(EvernodeConstants.NFT_PREFIX_HEX) && n.Issuer === this.registryAddress);
584
+ if (regNFT) {
585
+ // Check whether the token was actually issued from Evernode registry contract.
586
+ const issuerHex = regNFT.NFTokenID.substr(8, 40);
587
+ const issuerAddr = codec.encodeAccountID(Buffer.from(issuerHex, 'hex'));
588
+ if (issuerAddr == this.registryAddress) {
589
+ const nftPageDataBuf = await EvernodeHelpers.getNFTPageAndLocation(regNFT.NFTokenID, hostAcc, this.xrplApi);
590
+
591
+ await this.xrplAcc.makePayment(this.registryAddress,
592
+ XrplConstants.MIN_XRP_AMOUNT,
593
+ XrplConstants.XRP,
594
+ null,
595
+ [
596
+ { type: MemoTypes.DEAD_HOST_PRUNE, format: MemoFormats.HEX, data: memoData.toString('hex') },
597
+ { type: MemoTypes.HOST_REGISTRY_REF, format: MemoFormats.HEX, data: nftPageDataBuf.toString('hex') }
598
+ ]);
599
+ } else
600
+ throw "Invalid Registration NFT."
601
+ } else
602
+ throw "No Registration NFT was found for the Host account."
603
+
604
+ }
605
+ }
606
+
607
+ module.exports = {
608
+ BaseEvernodeClient
609
+ }