evernode-js-client 0.4.53 → 0.5.0
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/.eslintrc.json +14 -0
- package/LICENSE +21 -0
- package/README.md +26 -0
- package/clean-pkg.sh +4 -0
- package/npm-readme.md +4 -0
- package/package.json +16 -1
- package/remove-versions.sh +10 -0
- package/src/clients/base-evernode-client.js +567 -0
- package/src/clients/host-client.js +357 -0
- package/src/clients/registry-client.js +52 -0
- package/src/clients/tenant-client.js +264 -0
- package/src/defaults.js +21 -0
- package/src/eccrypto.js +258 -0
- package/src/encryption-helper.js +41 -0
- package/src/event-emitter.js +45 -0
- package/src/evernode-common.js +103 -0
- package/src/evernode-helpers.js +14 -0
- package/src/firestore/firestore-handler.js +309 -0
- package/src/index.js +37 -0
- package/src/state-helpers.js +283 -0
- package/src/transaction-helper.js +62 -0
- package/src/util-helpers.js +48 -0
- package/src/xfl-helpers.js +130 -0
- package/src/xrpl-account.js +473 -0
- package/src/xrpl-api.js +275 -0
- package/src/xrpl-common.js +17 -0
- package/test/package-lock.json +884 -0
- package/test/package.json +9 -0
- package/test/test.js +379 -0
- package/index.js +0 -15166
package/.eslintrc.json
ADDED
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
package/npm-readme.md
ADDED
package/package.json
CHANGED
@@ -1,11 +1,26 @@
|
|
1
1
|
{
|
2
2
|
"name": "evernode-js-client",
|
3
|
-
"
|
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,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
|
+
}
|