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/src/xrpl-api.js
ADDED
@@ -0,0 +1,275 @@
|
|
1
|
+
const xrpl = require('xrpl');
|
2
|
+
const kp = require('ripple-keypairs');
|
3
|
+
const { EventEmitter } = require('./event-emitter');
|
4
|
+
const { DefaultValues } = require('./defaults');
|
5
|
+
const { TransactionHelper } = require('./transaction-helper');
|
6
|
+
const { XrplApiEvents } = require('./xrpl-common');
|
7
|
+
|
8
|
+
const MAX_PAGE_LIMIT = 400;
|
9
|
+
const API_REQ_TYPE = {
|
10
|
+
NAMESPACE_ENTRIES: 'namespace_entries',
|
11
|
+
ACCOUNT_OBJECTS: 'account_objects',
|
12
|
+
LINES: 'lines',
|
13
|
+
ACCOUNT_NFTS: 'account_nfts',
|
14
|
+
OFFERS: 'offers',
|
15
|
+
TRANSACTIONS: 'transactions'
|
16
|
+
}
|
17
|
+
|
18
|
+
class XrplApi {
|
19
|
+
|
20
|
+
#rippledServer;
|
21
|
+
#client;
|
22
|
+
#events = new EventEmitter();
|
23
|
+
#addressSubscriptions = [];
|
24
|
+
#maintainConnection = false;
|
25
|
+
|
26
|
+
constructor(rippledServer = null, options = {}) {
|
27
|
+
|
28
|
+
this.#rippledServer = rippledServer || DefaultValues.rippledServer;
|
29
|
+
this.#initXrplClient(options.xrplClientOptions);
|
30
|
+
}
|
31
|
+
|
32
|
+
async #initXrplClient(xrplClientOptions = {}) {
|
33
|
+
|
34
|
+
if (this.#client) { // If the client already exists, clean it up.
|
35
|
+
this.#client.removeAllListeners(); // Remove existing event listeners to avoid them getting called from the old client object.
|
36
|
+
await this.#client.disconnect();
|
37
|
+
this.#client = null;
|
38
|
+
}
|
39
|
+
|
40
|
+
this.#client = new xrpl.Client(this.#rippledServer, xrplClientOptions);
|
41
|
+
|
42
|
+
this.#client.on('error', (errorCode, errorMessage) => {
|
43
|
+
console.log(errorCode + ': ' + errorMessage);
|
44
|
+
});
|
45
|
+
|
46
|
+
this.#client.on('disconnected', (code) => {
|
47
|
+
if (this.#maintainConnection) {
|
48
|
+
console.log(`Connection failure for ${this.#rippledServer} (code:${code})`);
|
49
|
+
console.log("Reinitializing xrpl client.");
|
50
|
+
this.#initXrplClient().then(() => this.#connectXrplClient(true));
|
51
|
+
}
|
52
|
+
});
|
53
|
+
|
54
|
+
this.#client.on('ledgerClosed', (ledger) => {
|
55
|
+
this.ledgerIndex = ledger.ledger_index;
|
56
|
+
this.#events.emit(XrplApiEvents.LEDGER, ledger);
|
57
|
+
});
|
58
|
+
|
59
|
+
this.#client.on("transaction", async (data) => {
|
60
|
+
if (data.validated) {
|
61
|
+
// NFTokenAcceptOffer transactions does not contain a Destination. So we check whether the accepted offer is created by which subscribed account
|
62
|
+
if (data.transaction.TransactionType === 'NFTokenAcceptOffer') {
|
63
|
+
// We take all the offers created by subscribed accounts in previous ledger until we get the respective offer.
|
64
|
+
for (const subscription of this.#addressSubscriptions) {
|
65
|
+
const offer = (await this.getNftOffers(subscription.address, { ledger_index: data.ledger_index - 1 }))?.find(o => o.index === (data.transaction.NFTokenSellOffer || data.transaction.NFTokenBuyOffer));
|
66
|
+
// When we find the respective offer. We populate the destination and offer info and then we break the loop.
|
67
|
+
if (offer) {
|
68
|
+
// We populate some sell offer properties to the transaction to be sent with the event.
|
69
|
+
data.transaction.Destination = subscription.address;
|
70
|
+
// Replace the offer with the found offer object.
|
71
|
+
if (data.transaction.NFTokenSellOffer)
|
72
|
+
data.transaction.NFTokenSellOffer = offer;
|
73
|
+
else if (data.transaction.NFTokenBuyOffer)
|
74
|
+
data.transaction.NFTokenBuyOffer = offer;
|
75
|
+
break;
|
76
|
+
}
|
77
|
+
}
|
78
|
+
}
|
79
|
+
|
80
|
+
const matches = this.#addressSubscriptions.filter(s => s.address === data.transaction.Destination); // Only incoming transactions.
|
81
|
+
if (matches.length > 0) {
|
82
|
+
const tx = {
|
83
|
+
LedgerHash: data.ledger_hash,
|
84
|
+
LedgerIndex: data.ledger_index,
|
85
|
+
...data.transaction
|
86
|
+
}; // Create an object copy. Otherwise xrpl client will mutate the transaction object,
|
87
|
+
const eventName = tx.TransactionType.toLowerCase();
|
88
|
+
// Emit the event only for successful transactions, Otherwise emit error.
|
89
|
+
if (data.engine_result === "tesSUCCESS") {
|
90
|
+
tx.Memos = TransactionHelper.deserializeMemos(tx.Memos);
|
91
|
+
matches.forEach(s => s.handler(eventName, tx));
|
92
|
+
}
|
93
|
+
else {
|
94
|
+
matches.forEach(s => s.handler(eventName, null, data.engine_result_message));
|
95
|
+
}
|
96
|
+
}
|
97
|
+
}
|
98
|
+
});
|
99
|
+
}
|
100
|
+
|
101
|
+
async #connectXrplClient(reconnect = false) {
|
102
|
+
|
103
|
+
if (reconnect) {
|
104
|
+
let attempts = 0;
|
105
|
+
while (this.#maintainConnection) { // Keep attempting until consumer calls disconnect() manually.
|
106
|
+
console.log(`Reconnection attempt ${++attempts}`);
|
107
|
+
try {
|
108
|
+
await this.#client.connect();
|
109
|
+
break;
|
110
|
+
}
|
111
|
+
catch {
|
112
|
+
if (this.#maintainConnection) {
|
113
|
+
const delaySec = 2 * attempts; // Retry with backoff delay.
|
114
|
+
console.log(`Attempt ${attempts} failed. Retrying in ${delaySec}s...`);
|
115
|
+
await new Promise(resolve => setTimeout(resolve, delaySec * 1000));
|
116
|
+
}
|
117
|
+
}
|
118
|
+
}
|
119
|
+
}
|
120
|
+
else {
|
121
|
+
// Single attempt and throw error. Used for initial connect() call.
|
122
|
+
await this.#client.connect();
|
123
|
+
}
|
124
|
+
|
125
|
+
// After connection established, check again whether maintainConnections has become false.
|
126
|
+
// This is in case the consumer has called disconnect() while connection is being established.
|
127
|
+
if (this.#maintainConnection) {
|
128
|
+
this.ledgerIndex = await this.#client.getLedgerIndex();
|
129
|
+
this.#subscribeToStream('ledger');
|
130
|
+
|
131
|
+
// Re-subscribe to existing account address subscriptions (in case this is a reconnect)
|
132
|
+
if (this.#addressSubscriptions.length > 0)
|
133
|
+
await this.#client.request({ command: 'subscribe', accounts: this.#addressSubscriptions.map(s => s.address) });
|
134
|
+
}
|
135
|
+
else {
|
136
|
+
await this.disconnect();
|
137
|
+
}
|
138
|
+
}
|
139
|
+
|
140
|
+
async #requestWithPaging(requestObj, requestType) {
|
141
|
+
let res = [];
|
142
|
+
let checked = false;
|
143
|
+
let resp;
|
144
|
+
let count = requestObj?.limit;
|
145
|
+
|
146
|
+
while ((!count || count > 0) && (!checked || resp?.result?.marker)) {
|
147
|
+
checked = true;
|
148
|
+
requestObj.limit = count ? Math.min(count, MAX_PAGE_LIMIT) : MAX_PAGE_LIMIT;
|
149
|
+
if (resp?.result?.marker)
|
150
|
+
requestObj.marker = resp?.result?.marker;
|
151
|
+
else
|
152
|
+
delete requestObj.marker;
|
153
|
+
resp = (await this.#client.request(requestObj));
|
154
|
+
if (resp?.result && resp?.result[requestType])
|
155
|
+
res.push(...resp.result[requestType]);
|
156
|
+
if (count)
|
157
|
+
count -= requestObj.limit;
|
158
|
+
}
|
159
|
+
|
160
|
+
return res;
|
161
|
+
}
|
162
|
+
|
163
|
+
on(event, handler) {
|
164
|
+
this.#events.on(event, handler);
|
165
|
+
}
|
166
|
+
|
167
|
+
once(event, handler) {
|
168
|
+
this.#events.once(event, handler);
|
169
|
+
}
|
170
|
+
|
171
|
+
off(event, handler = null) {
|
172
|
+
this.#events.off(event, handler);
|
173
|
+
}
|
174
|
+
|
175
|
+
async connect() {
|
176
|
+
if (this.#maintainConnection)
|
177
|
+
return;
|
178
|
+
|
179
|
+
this.#maintainConnection = true;
|
180
|
+
await this.#connectXrplClient();
|
181
|
+
}
|
182
|
+
|
183
|
+
async disconnect() {
|
184
|
+
this.#maintainConnection = false;
|
185
|
+
|
186
|
+
if (this.#client.isConnected()) {
|
187
|
+
await this.#client.disconnect().catch(console.error);
|
188
|
+
}
|
189
|
+
}
|
190
|
+
|
191
|
+
async isValidKeyForAddress(publicKey, address) {
|
192
|
+
const info = await this.getAccountInfo(address);
|
193
|
+
const accountFlags = xrpl.parseAccountRootFlags(info.Flags);
|
194
|
+
const regularKey = info.RegularKey;
|
195
|
+
const derivedPubKeyAddress = kp.deriveAddress(publicKey);
|
196
|
+
|
197
|
+
// If the master key is disabled the derived pubkey address should be the regular key.
|
198
|
+
// Otherwise it could be account address or the regular key
|
199
|
+
if (accountFlags.lsfDisableMaster)
|
200
|
+
return regularKey && (derivedPubKeyAddress === regularKey);
|
201
|
+
else
|
202
|
+
return derivedPubKeyAddress === address || (regularKey && derivedPubKeyAddress === regularKey);
|
203
|
+
}
|
204
|
+
|
205
|
+
async getAccountInfo(address) {
|
206
|
+
const resp = (await this.#client.request({ command: 'account_info', account: address }));
|
207
|
+
return resp?.result?.account_data;
|
208
|
+
}
|
209
|
+
|
210
|
+
async getAccountObjects(address, options) {
|
211
|
+
return this.#requestWithPaging({ command: 'account_objects', account: address, ...options }, API_REQ_TYPE.ACCOUNT_OBJECTS);
|
212
|
+
}
|
213
|
+
|
214
|
+
async getNamespaceEntries(address, namespaceId, options) {
|
215
|
+
return this.#requestWithPaging({ command: 'account_namespace', account: address, namespace_id: namespaceId, ...options }, API_REQ_TYPE.NAMESPACE_ENTRIES);
|
216
|
+
}
|
217
|
+
|
218
|
+
async getNftOffers(address, options) {
|
219
|
+
const offers = await this.getAccountObjects(address, options);
|
220
|
+
// TODO: Pass rippled filter parameter when xrpl.js supports it.
|
221
|
+
return offers.filter(o => o.LedgerEntryType == 'NFTokenOffer');
|
222
|
+
}
|
223
|
+
|
224
|
+
async getTrustlines(address, options) {
|
225
|
+
return this.#requestWithPaging({ command: 'account_lines', account: address, ledger_index: "validated", ...options }, API_REQ_TYPE.LINES);
|
226
|
+
}
|
227
|
+
|
228
|
+
async getAccountTrx(address, options) {
|
229
|
+
return this.#requestWithPaging({ command: 'account_tx', account: address, ...options }, API_REQ_TYPE.TRANSACTIONS);
|
230
|
+
}
|
231
|
+
|
232
|
+
async getNfts(address, options) {
|
233
|
+
return this.#requestWithPaging({ command: 'account_nfts', account: address, ledger_index: "validated", ...options }, API_REQ_TYPE.ACCOUNT_NFTS);
|
234
|
+
}
|
235
|
+
|
236
|
+
async getOffers(address, options) {
|
237
|
+
return this.#requestWithPaging({ command: 'account_offers', account: address, ledger_index: "validated", ...options }, API_REQ_TYPE.OFFERS);
|
238
|
+
}
|
239
|
+
|
240
|
+
async getLedgerEntry(index, options) {
|
241
|
+
const resp = (await this.#client.request({ command: 'ledger_entry', index: index, ledger_index: "validated", ...options }));
|
242
|
+
return resp?.result?.node;
|
243
|
+
}
|
244
|
+
|
245
|
+
async submitAndVerify(tx, options) {
|
246
|
+
return await this.#client.submitAndWait(tx, options);
|
247
|
+
}
|
248
|
+
|
249
|
+
async subscribeToAddress(address, handler) {
|
250
|
+
this.#addressSubscriptions.push({ address: address, handler: handler });
|
251
|
+
await this.#client.request({ command: 'subscribe', accounts: [address] });
|
252
|
+
}
|
253
|
+
|
254
|
+
async unsubscribeFromAddress(address, handler) {
|
255
|
+
for (let i = this.#addressSubscriptions.length - 1; i >= 0; i--) {
|
256
|
+
const sub = this.#addressSubscriptions[i];
|
257
|
+
if (sub.address === address && sub.handler === handler)
|
258
|
+
this.#addressSubscriptions.splice(i, 1);
|
259
|
+
}
|
260
|
+
await this.#client.request({ command: 'unsubscribe', accounts: [address] });
|
261
|
+
}
|
262
|
+
|
263
|
+
async getTransactionFee(txBlob) {
|
264
|
+
const fees = await this.#client.request({ command: 'fee', tx_blob: txBlob });
|
265
|
+
return fees?.result?.drops?.base_fee;
|
266
|
+
}
|
267
|
+
|
268
|
+
async #subscribeToStream(streamName) {
|
269
|
+
await this.#client.request({ command: 'subscribe', streams: [streamName] });
|
270
|
+
}
|
271
|
+
}
|
272
|
+
|
273
|
+
module.exports = {
|
274
|
+
XrplApi
|
275
|
+
}
|
@@ -0,0 +1,17 @@
|
|
1
|
+
const XrplApiEvents = {
|
2
|
+
LEDGER: 'ledger',
|
3
|
+
PAYMENT: 'payment',
|
4
|
+
NFT_OFFER_CREATE: 'nftokencreateoffer',
|
5
|
+
NFT_OFFER_ACCEPT: 'nftokenacceptoffer'
|
6
|
+
}
|
7
|
+
|
8
|
+
const XrplConstants = {
|
9
|
+
MAX_LEDGER_OFFSET: 10,
|
10
|
+
XRP: 'XRP',
|
11
|
+
MIN_XRP_AMOUNT: '1' // drops
|
12
|
+
}
|
13
|
+
|
14
|
+
module.exports = {
|
15
|
+
XrplApiEvents,
|
16
|
+
XrplConstants
|
17
|
+
}
|