evernode-js-client 0.5.9 → 0.5.11

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,45 @@
1
+ class EventEmitter {
2
+ constructor() {
3
+ this.handlers = {};
4
+ }
5
+
6
+ on(event, handler) {
7
+ if (!this.handlers[event])
8
+ this.handlers[event] = [];
9
+ this.handlers[event].push({
10
+ once: false,
11
+ func: handler
12
+ });
13
+ }
14
+
15
+ once(event, handler) {
16
+ if (!this.handlers[event])
17
+ this.handlers[event] = [];
18
+ this.handlers[event].push({
19
+ once: true,
20
+ func: handler
21
+ });
22
+ }
23
+
24
+ off(event, handler = null) {
25
+ if (this.handlers[event]) {
26
+ if (handler)
27
+ this.handlers[event] = this.handlers[event].filter(h => h !== handler);
28
+ else
29
+ delete this.handlers[event];
30
+ }
31
+ }
32
+
33
+ emit(event, value, error = null) {
34
+ if (this.handlers[event]) {
35
+ this.handlers[event].forEach(handler => handler.func(value, error));
36
+
37
+ // Rmove all handlers marked as 'once'.
38
+ this.handlers[event] = this.handlers[event].filter(h => !h.once);
39
+ }
40
+ }
41
+ }
42
+
43
+ module.exports = {
44
+ EventEmitter
45
+ }
@@ -0,0 +1,113 @@
1
+ const EvernodeConstants = {
2
+ EVR: 'EVR',
3
+ NFT_PREFIX_HEX: '657672686F7374', // evrhost
4
+ LEASE_NFT_PREFIX_HEX: '6576726C65617365', // evrlease
5
+ HOOK_NAMESPACE: '01EAF09326B4911554384121FF56FA8FECC215FDDE2EC35D9E59F2C53EC665A0',
6
+ NOW_IN_EVRS: "0.00000001"
7
+ }
8
+
9
+ const MemoTypes = {
10
+ ACQUIRE_LEASE: 'evnAcquireLease',
11
+ ACQUIRE_SUCCESS: 'evnAcquireSuccess',
12
+ ACQUIRE_ERROR: 'evnAcquireError',
13
+ ACQUIRE_REF: 'evnAcquireRef',
14
+ HOST_REG: 'evnHostReg',
15
+ HOST_DEREG: 'evnHostDereg',
16
+ HOST_UPDATE_INFO: 'evnHostUpdateReg',
17
+ HEARTBEAT: 'evnHeartbeat',
18
+ HOST_POST_DEREG: 'evnHostPostDereg',
19
+ HOST_TRANSFER: 'evnTransfer',
20
+ EXTEND_LEASE: 'evnExtendLease',
21
+ EXTEND_SUCCESS: 'evnExtendSuccess',
22
+ EXTEND_ERROR: 'evnExtendError',
23
+ EXTEND_REF: 'evnExtendRef',
24
+ REGISTRY_INIT: 'evnInitialize',
25
+ REFUND: 'evnRefund',
26
+ REFUND_REF: 'evnRefundRef',
27
+ DEAD_HOST_PRUNE: 'evnDeadHostPrune',
28
+ HOST_REBATE: 'evnHostRebate',
29
+ HOST_REGISTRY_REF: 'evnHostRegistryRef'
30
+ }
31
+
32
+ const MemoFormats = {
33
+ TEXT: 'text/plain',
34
+ JSON: 'text/json',
35
+ BASE64: 'base64',
36
+ HEX: 'hex'
37
+ }
38
+
39
+ const ErrorCodes = {
40
+ ACQUIRE_ERR: 'ACQUIRE_ERR',
41
+ EXTEND_ERR: 'EXTEND_ERR'
42
+ }
43
+
44
+ const ErrorReasons = {
45
+ TRANSACTION_FAILURE: 'TRANSACTION_FAILURE',
46
+ NO_OFFER: 'NO_OFFER',
47
+ NO_NFT: 'NO_NFT',
48
+ INTERNAL_ERR: 'INTERNAL_ERR',
49
+ TIMEOUT: 'TIMEOUT',
50
+ HOST_INVALID: 'HOST_INVALID',
51
+ HOST_INACTIVE: 'HOST_INACTIVE',
52
+ NO_STATE_KEY: 'NO_STATE_KEY'
53
+ }
54
+
55
+ // All keys are prefixed with 'EVR' (0x455652)
56
+ // Config keys sub-prefix: 0x01
57
+ const HookStateKeys = {
58
+ // Configuration.
59
+ EVR_ISSUER_ADDR: "4556520100000000000000000000000000000000000000000000000000000001",
60
+ FOUNDATION_ADDR: "4556520100000000000000000000000000000000000000000000000000000002",
61
+ MOMENT_SIZE: "4556520100000000000000000000000000000000000000000000000000000003",
62
+ MINT_LIMIT: "4556520100000000000000000000000000000000000000000000000000000004",
63
+ FIXED_REG_FEE: "4556520100000000000000000000000000000000000000000000000000000005",
64
+ HOST_HEARTBEAT_FREQ: "4556520100000000000000000000000000000000000000000000000000000006",
65
+ PURCHASER_TARGET_PRICE: "4556520100000000000000000000000000000000000000000000000000000007",
66
+ LEASE_ACQUIRE_WINDOW: "4556520100000000000000000000000000000000000000000000000000000008",
67
+ REWARD_CONFIGURATION: "4556520100000000000000000000000000000000000000000000000000000009",
68
+ MAX_TOLERABLE_DOWNTIME: "455652010000000000000000000000000000000000000000000000000000000A",
69
+ MOMENT_TRANSIT_INFO: "455652010000000000000000000000000000000000000000000000000000000B",
70
+ MAX_TRX_EMISSION_FEE: "455652010000000000000000000000000000000000000000000000000000000C",
71
+
72
+
73
+ // Singleton
74
+ HOST_COUNT: "4556523200000000000000000000000000000000000000000000000000000000",
75
+ MOMENT_BASE_INFO: "4556523300000000000000000000000000000000000000000000000000000000",
76
+ HOST_REG_FEE: "4556523400000000000000000000000000000000000000000000000000000000",
77
+ MAX_REG: "4556523500000000000000000000000000000000000000000000000000000000",
78
+ REWARD_INFO: "4556523600000000000000000000000000000000000000000000000000000000",
79
+
80
+ // Prefixes
81
+ PREFIX_HOST_TOKENID: "45565202",
82
+ PREFIX_HOST_ADDR: "45565203",
83
+ PREFIX_TRANSFEREE_ADDR: "45565204",
84
+ }
85
+
86
+ const EvernodeEvents = {
87
+ HostRegistered: "HostRegistered",
88
+ HostDeregistered: "HostDeregistered",
89
+ HostPostDeregistered: "HostPostDeregistered",
90
+ HostTransfer: "HostTransfer",
91
+ AcquireLease: "AcquireLease",
92
+ AcquireSuccess: "AcquireSuccess",
93
+ AcquireError: "AcquireError",
94
+ Heartbeat: "Heartbeat",
95
+ ExtendLease: "ExtendLease",
96
+ ExtendSuccess: "ExtendSuccess",
97
+ ExtendError: "ExtendError",
98
+ HostRegUpdated: "HostRegUpdated",
99
+ HostReRegistered: "HostReRegistered",
100
+ RegistryInitialized: "RegistryInitialized",
101
+ DeadHostPrune: "DeadHostPrune",
102
+ HostRebate: "HostRebate"
103
+ }
104
+
105
+ module.exports = {
106
+ EvernodeConstants,
107
+ MemoTypes,
108
+ MemoFormats,
109
+ ErrorCodes,
110
+ ErrorReasons,
111
+ HookStateKeys,
112
+ EvernodeEvents
113
+ }
@@ -0,0 +1,45 @@
1
+ const { EvernodeConstants } = require('./evernode-common');
2
+ const NFT_PAGE_LEDGER_ENTRY_TYPE_HEX = '0050';
3
+
4
+ class EvernodeHelpers {
5
+ static async getLeaseOffers(xrplAcc) {
6
+ const hostNfts = (await xrplAcc.getNfts()).filter(nft => nft.URI.startsWith(EvernodeConstants.LEASE_NFT_PREFIX_HEX));
7
+ const hostTokenIDs = hostNfts.map(nft => nft.NFTokenID);
8
+ const nftOffers = (await xrplAcc.getNftOffers())?.filter(offer => (offer.Flags == 1 && hostTokenIDs.includes(offer.NFTokenID))); // Filter only sell offers
9
+ return nftOffers;
10
+ }
11
+
12
+ static async getNFTPageAndLocation(nfTokenId, xrplAcc, xrplApi, buffer = true) {
13
+
14
+ const nftPageApprxKeylet = xrplAcc.generateKeylet('nftPage', { nfTokenId: nfTokenId });
15
+ const nftPageMaxKeylet = xrplAcc.generateKeylet('nftPageMax');
16
+ // Index is the last 32 bytes of the Keylet (Last 64 HEX characters).
17
+ let page = await xrplApi.getLedgerEntry(nftPageMaxKeylet.substring(4, 68));
18
+ while (page?.PreviousPageMin) {
19
+ // Compare the low 96 bits. (Last 24 HEX characters).
20
+ if (Number('0x' + page.index.substring(40, 64)) >= Number('0x' + nftPageApprxKeylet.substring(40, 64))) {
21
+ // Check the existence of the NFToken
22
+ let token = page.NFTokens.find(n => n.NFToken.NFTokenID == nfTokenId);
23
+ if (!token) {
24
+ page = await xrplApi.getLedgerEntry(page.PreviousPageMin);
25
+ }
26
+ else
27
+ break;
28
+ }
29
+ }
30
+
31
+ const nftPageInfo = page.NFTokens.map((n, loc) => { return { NFTPage: NFT_PAGE_LEDGER_ENTRY_TYPE_HEX + page.index, NFTokenID: n.NFToken.NFTokenID, location: loc } }).find(n => n.NFTokenID == nfTokenId);
32
+ if (buffer) {
33
+ let locBuf = Buffer.allocUnsafe(2);
34
+ locBuf.writeUInt16BE(nftPageInfo.location);
35
+ // <NFT_PAGE_KEYLET(34 bytes)><LOCATION(2 bytes)>
36
+ return Buffer.concat([Buffer.from(nftPageInfo.NFTPage, "hex"), locBuf]);
37
+ }
38
+
39
+ return nftPageInfo;
40
+ }
41
+ }
42
+
43
+ module.exports = {
44
+ EvernodeHelpers
45
+ }
@@ -0,0 +1,309 @@
1
+ const https = require('https');
2
+ const { DefaultValues } = require('../defaults');
3
+
4
+ const FirestoreOperations = {
5
+ EQUAL: 'EQUAL',
6
+ AND: 'AND'
7
+ }
8
+
9
+ class FirestoreHandler {
10
+ #projectId = null;
11
+ #collectionPrefix = null;
12
+
13
+ constructor(options = {}) {
14
+ this.#projectId = options.stateIndexId || DefaultValues.stateIndexId;
15
+ this.#collectionPrefix = options.collectionPrefix || DefaultValues.registryAddress;
16
+ }
17
+
18
+ /**
19
+ * Convert given document value object to real format and snake_case key to camelCase.
20
+ * @param key Name of the property.
21
+ * @param value Value to be parsed.
22
+ * @returns Parsed key and value.
23
+ */
24
+ #parseValue(key, value) {
25
+ // Convert snake_case to camelCase.
26
+ const ccKey = key.replace(/_([a-z])/g, function (g) { return g[1].toUpperCase(); });
27
+ const type = Object.keys(value)[0];
28
+ let parsed;
29
+ switch (type) {
30
+ case 'integerValue':
31
+ parsed = parseInt(value[type]);
32
+ break;
33
+ case 'floatValue':
34
+ parsed = parseFloat(value[type]);
35
+ break;
36
+ case 'mapValue':
37
+ parsed = {};
38
+ for (const [subKey, subValue] of Object.entries(value[type].fields)) {
39
+ const field = this.#parseValue(subKey, subValue);
40
+ parsed[field.key] = field.value;
41
+ }
42
+ break;
43
+ default:
44
+ parsed = value[type];
45
+ break;
46
+ }
47
+ return { key: ccKey, value: parsed };
48
+ }
49
+
50
+ /**
51
+ * Get values filtered according to the given body.
52
+ * @param body Body to parsed in to the query reqest.
53
+ * @returns Result set.
54
+ */
55
+ async #runQuery(body) {
56
+ const url = this.buildApiPath(null, null, true);
57
+ return await this.sendRequest('POST', url, null, body);
58
+ }
59
+
60
+ /**
61
+ * Get documents from a collection.
62
+ * @param collectionId Name of the collection.
63
+ * @param pageSize Optianal page size if result set needed to be paginated
64
+ * @param nextPageToken Next page token of a paginated result set.
65
+ * @returns Result set.
66
+ */
67
+ async #read(collectionId, pageSize = null, nextPageToken = null) {
68
+ if (!collectionId)
69
+ throw { type: 'Validation Error', message: 'collectionId is required' };
70
+
71
+ const url = this.buildApiPath(collectionId);
72
+ let params = (pageSize || nextPageToken) ? {} : null;
73
+ if (pageSize)
74
+ params = { pageSize: pageSize };
75
+ if (nextPageToken)
76
+ params = { pageToken: nextPageToken, ...params };
77
+
78
+ return await this.sendRequest('GET', url, params);
79
+ }
80
+
81
+ /**
82
+ * Get documents from a collection with filtering support.
83
+ * @param collectionId Name of the collection.
84
+ * @param filter Optional filters to filter documents.
85
+ * @param pageSize Optianal page size if result set needed to be paginated
86
+ * @param nextPageToken Next page token of a paginated result set.
87
+ * @returns Parsed readable result set.
88
+ */
89
+ async #getDocuments(collectionId, filters = null, pageSize = null, nextPageToken = null) {
90
+ if (filters && (pageSize || nextPageToken))
91
+ throw { type: 'Validation Error', message: 'Pagination isn\'t supported with filter.' };
92
+
93
+ let data;
94
+ // If there are filters send requst in query mode.
95
+ if (filters) {
96
+ // Prepare the query body from given filters.
97
+ let where = {
98
+ compositeFilter: {
99
+ filters: Object.entries(filters).map(([key, value]) => {
100
+ const field = this.convertValue(key, value);
101
+ return {
102
+ fieldFilter: {
103
+ field: { fieldPath: field.key },
104
+ op: FirestoreOperations.EQUAL,
105
+ value: field.value
106
+ }
107
+ }
108
+ }),
109
+ op: filters.operator ? filters.operator : FirestoreOperations.AND
110
+ }
111
+ };
112
+ for (const [key, value] of Object.entries(filters)) {
113
+ const field = this.convertValue(key, value);
114
+ let fieldFilter = {
115
+ field: { fieldPath: field.key },
116
+ op: FirestoreOperations.EQUAL,
117
+ value: field.value
118
+ }
119
+ where.compositeFilter.filters.push({ fieldFilter: fieldFilter });
120
+ }
121
+
122
+ let body = {
123
+ structuredQuery: {
124
+ where: where,
125
+ from: [{ collectionId: collectionId }]
126
+ }
127
+ };
128
+ data = await this.#runQuery(body);
129
+ data = data ? JSON.parse(data) : [];
130
+ if (data && data.length && data[0].document) {
131
+ return data.map(d => {
132
+ return this.parseDocument(d.document);
133
+ });
134
+ }
135
+ }
136
+ else {
137
+ data = await this.#read(collectionId, pageSize, nextPageToken);
138
+ data = data ? JSON.parse(data) : {};
139
+ if (data.documents && data.documents.length) {
140
+ const list = data.documents.map(d => {
141
+ return this.parseDocument(d);
142
+ });
143
+ return data.nextPageToken ? {
144
+ data: list,
145
+ nextPageToken: data.nextPageToken
146
+ } : list;
147
+ }
148
+ }
149
+
150
+ return [];
151
+ }
152
+
153
+ getCollectionId(collection) {
154
+ // Document if is generated with given prefix.
155
+ return `${this.#collectionPrefix}_${collection}`;
156
+ }
157
+
158
+ /**
159
+ * Send http requst.
160
+ * @param httpMethod GET/POST/PATCH/DELETE
161
+ * @param url Url for the request.
162
+ * @param params Optional query params.
163
+ * @param data Optional request body.
164
+ * @param options Optional options.
165
+ * @returns Result set.
166
+ */
167
+ async sendRequest(httpMethod, url, params = null, data = null, options = null) {
168
+ const urlObj = new URL(url);
169
+ // Populate uri params to the URL object.
170
+ if (params) {
171
+ for (const [key, value] of Object.entries(params)) {
172
+ if (value) {
173
+ // If value is a array, populate it with same name.
174
+ if (typeof value === 'object') {
175
+ for (const val of value) {
176
+ if (val)
177
+ urlObj.searchParams.append(key, val);
178
+ }
179
+ }
180
+ else
181
+ urlObj.searchParams.set(key, value);
182
+ }
183
+ }
184
+ }
185
+
186
+ let reqOptions = {
187
+ method: httpMethod,
188
+ protocol: urlObj.protocol,
189
+ hostname: urlObj.hostname,
190
+ port: urlObj.port,
191
+ path: `${urlObj.pathname}?${urlObj.searchParams.toString()}`,
192
+ ...(options ? options : {})
193
+ };
194
+
195
+ return new Promise(async (resolve, reject) => {
196
+ // Send request and collect data chuncks to a buffer. Then return the result set on end event.
197
+ // Resolve only if the status is 200.
198
+ const req = https.request(reqOptions, (res) => {
199
+ let resData = '';
200
+ res.on('data', (d) => {
201
+ resData += d;
202
+ });
203
+ res.on('end', async () => {
204
+ if (res.statusCode === 200) {
205
+ resolve(resData);
206
+ return;
207
+ }
208
+ reject({ status: res.statusCode, data: resData });
209
+ return;
210
+ });
211
+ }).on('error', async (e) => {
212
+ reject(e);
213
+ return;
214
+ });
215
+
216
+ if (data)
217
+ req.write(typeof data === 'object' ? JSON.stringify(data) : data);
218
+
219
+ req.end();
220
+ });
221
+ }
222
+
223
+ /**
224
+ * Generate url for firestore.
225
+ * @param collectionId Optional collection name.
226
+ * @param documentId Optional document name.
227
+ * @param isQuery Whether a query request or not.
228
+ * @returns The generated url.
229
+ */
230
+ buildApiPath(collectionId = null, documentId = null, isQuery = false) {
231
+ let path = `https://firestore.googleapis.com/v1/projects/${this.#projectId}/databases/(default)/documents`;
232
+ if (collectionId)
233
+ path += `/${collectionId.toString()}`;
234
+ if (documentId)
235
+ path += `/${documentId.toString()}`;
236
+ if (isQuery)
237
+ path += ':runQuery';
238
+ return path;
239
+ }
240
+
241
+ /**
242
+ * Generate firestore document value object from given key and value, Convert camelCase key to snake_case.
243
+ * @param key Name of the value.
244
+ * @param value Value to be parsed.
245
+ * @returns Generated firestore document object.
246
+ */
247
+ convertValue(key, value) {
248
+ // Convert camelCase to snake_case.
249
+ const uKey = key.replace(/([A-Z])/g, function (g) { return `_${g[0].toLocaleLowerCase()}`; });
250
+ let val = {};
251
+ let type
252
+ switch (typeof value) {
253
+ case 'number':
254
+ type = (value % 1 > 0 ? 'float' : 'integer');
255
+ val = value;
256
+ break;
257
+ case 'object':
258
+ type = 'map';
259
+ val = {
260
+ fields: {}
261
+ }
262
+ // Prepare the firestore write body with the given data object.
263
+ for (const [subKey, subValue] of Object.entries(value)) {
264
+ const field = this.convertValue(subKey, subValue);
265
+ val.fields[field.key] = field.value;
266
+ }
267
+ break;
268
+ default:
269
+ type = 'string';
270
+ val = value;
271
+ break;
272
+ }
273
+ type = `${type}Value`;
274
+ let obj = {};
275
+ obj[type] = val;
276
+ return { key: uKey, value: obj };
277
+ }
278
+
279
+ /**
280
+ * Convert the firestore document to human readable simplified json object.
281
+ * @param document Firestore document.
282
+ * @returns Simplified json object for the document.
283
+ */
284
+ parseDocument(document) {
285
+ let item = {
286
+ id: document.name.split('/').pop(),
287
+ createTime: new Date(document.createTime),
288
+ updateTime: new Date(document.updateTime)
289
+ };
290
+ for (const [key, value] of Object.entries(document.fields)) {
291
+ const field = this.#parseValue(key, value);
292
+ item[field.key] = field.value;
293
+ }
294
+ return item;
295
+ }
296
+
297
+ async getHosts(filters = null, pageSize = null, nextPageToken = null) {
298
+ return await this.#getDocuments(this.getCollectionId('hosts'), filters, pageSize, nextPageToken);
299
+ }
300
+
301
+ async getConfigs(filters = null, pageSize = null, nextPageToken = null) {
302
+ return await this.#getDocuments(this.getCollectionId('configs'), filters, pageSize, nextPageToken);
303
+ }
304
+ }
305
+
306
+ module.exports = {
307
+ FirestoreHandler,
308
+ FirestoreOperations
309
+ }
package/src/index.js ADDED
@@ -0,0 +1,37 @@
1
+ const { Defaults } = require('./defaults');
2
+ const { RegistryClient, RegistryEvents } = require("./clients/registry-client");
3
+ const { TenantClient, TenantEvents } = require("./clients/tenant-client");
4
+ const { HostClient, HostEvents } = require("./clients/host-client");
5
+ const { XrplApi } = require('./xrpl-api');
6
+ const { XrplApiEvents, XrplConstants } = require('./xrpl-common');
7
+ const { XrplAccount } = require('./xrpl-account');
8
+ const { EvernodeConstants, HookStateKeys, MemoTypes } = require('./evernode-common');
9
+ const { XflHelpers } = require('./xfl-helpers');
10
+ const { FirestoreHandler } = require('./firestore/firestore-handler');
11
+ const { StateHelpers } = require('./state-helpers');
12
+ const { UtilHelpers } = require('./util-helpers');
13
+ const { TransactionHelper } = require('./transaction-helper');
14
+ const { EncryptionHelper } = require('./encryption-helper');
15
+
16
+ module.exports = {
17
+ RegistryClient,
18
+ RegistryEvents,
19
+ TenantClient,
20
+ TenantEvents,
21
+ HostClient,
22
+ HostEvents,
23
+ XrplApi,
24
+ XrplApiEvents,
25
+ XrplConstants,
26
+ XrplAccount,
27
+ EvernodeConstants,
28
+ Defaults,
29
+ XflHelpers,
30
+ StateHelpers,
31
+ FirestoreHandler,
32
+ UtilHelpers,
33
+ TransactionHelper,
34
+ EncryptionHelper,
35
+ HookStateKeys,
36
+ MemoTypes
37
+ }