evernode-js-client 0.5.10 → 0.5.11

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.
@@ -0,0 +1,515 @@
1
+ const xrpl = require('xrpl');
2
+ const kp = require('ripple-keypairs');
3
+ const codec = require('ripple-address-codec');
4
+ const crypto = require("crypto");
5
+ const { XrplConstants } = require('./xrpl-common');
6
+ const { TransactionHelper } = require('./transaction-helper');
7
+ const { EventEmitter } = require('./event-emitter');
8
+ const { DefaultValues } = require('./defaults');
9
+ const xrplCodec = require('xrpl-binary-codec');
10
+
11
+ class XrplAccount {
12
+
13
+ #events = new EventEmitter();
14
+ #subscribed = false;
15
+ #txStreamHandler;
16
+
17
+ constructor(address = null, secret = null, options = {}) {
18
+ if (!address && !secret)
19
+ throw "Both address and secret cannot be empty";
20
+
21
+ this.address = address;
22
+ this.secret = secret;
23
+ this.xrplApi = options.xrplApi || DefaultValues.xrplApi;
24
+
25
+ if (!this.xrplApi)
26
+ throw "XrplAccount: xrplApi not specified.";
27
+
28
+ if (!this.address && this.secret) {
29
+ this.wallet = xrpl.Wallet.fromSeed(this.secret);
30
+ this.address = this.wallet.classicAddress;
31
+ } else if (this.secret) {
32
+ const keypair = kp.deriveKeypair(this.secret);
33
+ const derivedPubKeyAddress = kp.deriveAddress(keypair.publicKey);
34
+ if (this.address == derivedPubKeyAddress)
35
+ this.wallet = xrpl.Wallet.fromSeed(this.secret);
36
+ else
37
+ this.wallet = xrpl.Wallet.fromSeed(this.secret, { masterAddress: this.address });
38
+ }
39
+
40
+ this.#txStreamHandler = (eventName, tx, error) => {
41
+ this.#events.emit(eventName, tx, error);
42
+ };
43
+ }
44
+
45
+ on(event, handler) {
46
+ this.#events.on(event, handler);
47
+ }
48
+
49
+ once(event, handler) {
50
+ this.#events.once(event, handler);
51
+ }
52
+
53
+ off(event, handler = null) {
54
+ this.#events.off(event, handler);
55
+ }
56
+
57
+ deriveKeypair() {
58
+ if (!this.secret)
59
+ throw 'Cannot derive key pair: Account secret is empty.';
60
+
61
+ return kp.deriveKeypair(this.secret);
62
+ }
63
+
64
+ async exists() {
65
+ return await this.xrplApi.isAccountExists(this.address);
66
+ }
67
+
68
+ async getInfo() {
69
+ return await this.xrplApi.getAccountInfo(this.address);
70
+ }
71
+
72
+ async getSequence() {
73
+ return (await this.getInfo())?.Sequence;
74
+ }
75
+
76
+ async getMintedNFTokens() {
77
+ return ((await this.getInfo())?.MintedNFTokens || 0);
78
+ }
79
+
80
+ async getBurnedNFTokens() {
81
+ return ((await this.getInfo())?.BurnedNFTokens || 0);
82
+ }
83
+
84
+ async getMessageKey() {
85
+ return (await this.getInfo())?.MessageKey;
86
+ }
87
+
88
+ async getDomain() {
89
+ const domain = (await this.getInfo())?.Domain;
90
+ return domain ? TransactionHelper.hexToASCII(domain) : null;
91
+ }
92
+
93
+ async getTrustLines(currency, issuer) {
94
+ const lines = await this.xrplApi.getTrustlines(this.address, {
95
+ limit: 399,
96
+ peer: issuer
97
+ });
98
+ return currency ? lines.filter(l => l.currency === currency) : lines;
99
+ }
100
+
101
+ async getChecks(fromAccount) {
102
+ return await this.xrplApi.getAccountObjects(fromAccount, { type: "check" });
103
+ }
104
+
105
+ async getNfts() {
106
+ return await this.xrplApi.getNfts(this.address, {
107
+ limit: 399
108
+ });
109
+ }
110
+
111
+ async getOffers() {
112
+ return await this.xrplApi.getOffers(this.address);
113
+ }
114
+
115
+ async getNftOffers() {
116
+ return await this.xrplApi.getNftOffers(this.address);
117
+ }
118
+
119
+ async getNftByUri(uri, isHexUri = false) {
120
+ const nfts = await this.getNfts();
121
+ const hexUri = isHexUri ? uri : TransactionHelper.asciiToHex(uri).toUpperCase();
122
+ return nfts.find(n => n.URI == hexUri);
123
+ }
124
+
125
+ async getAccountObjects(options) {
126
+ return await this.xrplApi.getAccountObjects(this.address, options);
127
+ }
128
+
129
+ async getNamespaceEntries(namespaceId, options = {}) {
130
+ return await this.xrplApi.getNamespaceEntries(this.address, namespaceId, options);
131
+ }
132
+
133
+ async getFlags() {
134
+ return xrpl.parseAccountRootFlags((await this.getInfo()).Flags);
135
+ }
136
+
137
+ async getAccountTrx(minLedgerIndex = -1, maxLedgerIndex = -1, isForward = true) {
138
+ return await this.xrplApi.getAccountTrx(this.address, { ledger_index_min: minLedgerIndex, ledger_index_max: maxLedgerIndex, forward: isForward });
139
+ }
140
+
141
+ async hasValidKeyPair() {
142
+ return await this.xrplApi.isValidKeyForAddress(this.wallet.publicKey, this.address);
143
+ }
144
+
145
+ setAccountFields(fields, options = {}) {
146
+ /**
147
+ * Example for fields
148
+ *
149
+ * fields = {
150
+ * Domain : "www.mydomain.com",
151
+ * Flags : { asfDefaultRipple: false, asfDisableMaster: true }
152
+ * }
153
+ *
154
+ */
155
+
156
+ if (Object.keys(fields).length === 0)
157
+ throw "AccountSet fields cannot be empty.";
158
+
159
+ const tx = {
160
+ TransactionType: 'AccountSet',
161
+ Account: this.address
162
+ };
163
+
164
+ for (const [key, value] of Object.entries(fields)) {
165
+
166
+ switch (key) {
167
+ case 'Domain':
168
+ tx.Domain = TransactionHelper.asciiToHex(value).toUpperCase();
169
+ break;
170
+
171
+ case 'Flags':
172
+ for (const [flagKey, flagValue] of Object.entries(value)) {
173
+ tx[(flagValue) ? 'SetFlag' : 'ClearFlag'] |= xrpl.AccountSetAsfFlags[flagKey];
174
+ }
175
+ break;
176
+
177
+ default:
178
+ tx[key] = value;
179
+ break;
180
+ }
181
+ }
182
+
183
+ return this.#submitAndVerifyTransaction(tx, options);
184
+ }
185
+
186
+ makePayment(toAddr, amount, currency, issuer = null, memos = null, options = {}) {
187
+
188
+ const amountObj = makeAmountObject(amount, currency, issuer);
189
+
190
+ return this.#submitAndVerifyTransaction({
191
+ TransactionType: 'Payment',
192
+ Account: this.address,
193
+ Amount: amountObj,
194
+ Destination: toAddr,
195
+ Memos: TransactionHelper.formatMemos(memos)
196
+ }, options);
197
+ }
198
+
199
+ setTrustLine(currency, issuer, limit, allowRippling = false, memos = null, options = {}) {
200
+
201
+ if (typeof limit !== 'string')
202
+ throw "Limit must be a string.";
203
+
204
+ let tx = {
205
+ TransactionType: 'TrustSet',
206
+ Account: this.address,
207
+ LimitAmount: {
208
+ currency: currency,
209
+ issuer: issuer,
210
+ value: limit
211
+ },
212
+ Memos: TransactionHelper.formatMemos(memos)
213
+ };
214
+
215
+ if (!allowRippling)
216
+ tx.Flags = 131072; // tfSetNoRipple;
217
+
218
+ return this.#submitAndVerifyTransaction(tx, options);
219
+ }
220
+
221
+ setRegularKey(regularKey, memos = null, options = {}) {
222
+
223
+ return this.#submitAndVerifyTransaction({
224
+ TransactionType: 'SetRegularKey',
225
+ Account: this.address,
226
+ RegularKey: regularKey,
227
+ Memos: TransactionHelper.formatMemos(memos)
228
+ }, options);
229
+ }
230
+
231
+ cashCheck(check, options = {}) {
232
+ const checkIDhasher = crypto.createHash('sha512')
233
+ checkIDhasher.update(Buffer.from('0043', 'hex'))
234
+ checkIDhasher.update(Buffer.from(codec.decodeAccountID(check.Account)))
235
+ const seqBuf = Buffer.alloc(4)
236
+ seqBuf.writeUInt32BE(check.Sequence, 0)
237
+ checkIDhasher.update(seqBuf)
238
+ const checkID = checkIDhasher.digest('hex').slice(0, 64).toUpperCase()
239
+ console.log("Calculated checkID:", checkID);
240
+
241
+ return this.#submitAndVerifyTransaction({
242
+ TransactionType: 'CheckCash',
243
+ Account: this.address,
244
+ CheckID: checkID,
245
+ Amount: {
246
+ currency: check.SendMax.currency,
247
+ issuer: check.SendMax.issuer,
248
+ value: check.SendMax.value
249
+ },
250
+ }, options);
251
+ }
252
+
253
+ offerSell(sellAmount, sellCurrency, sellIssuer, forAmount, forCurrency, forIssuer = null, expiration = 4294967295, memos = null, options = {}) {
254
+
255
+ const sellAmountObj = makeAmountObject(sellAmount, sellCurrency, sellIssuer);
256
+ const forAmountObj = makeAmountObject(forAmount, forCurrency, forIssuer);
257
+
258
+ return this.#submitAndVerifyTransaction({
259
+ TransactionType: 'OfferCreate',
260
+ Account: this.address,
261
+ TakerGets: sellAmountObj,
262
+ TakerPays: forAmountObj,
263
+ Expiration: expiration,
264
+ Memos: TransactionHelper.formatMemos(memos)
265
+ }, options);
266
+ }
267
+
268
+ offerBuy(buyAmount, buyCurrency, buyIssuer, forAmount, forCurrency, forIssuer = null, expiration = 4294967295, memos = null, options = {}) {
269
+
270
+ const buyAmountObj = makeAmountObject(buyAmount, buyCurrency, buyIssuer);
271
+ const forAmountObj = makeAmountObject(forAmount, forCurrency, forIssuer);
272
+
273
+ return this.#submitAndVerifyTransaction({
274
+ TransactionType: 'OfferCreate',
275
+ Account: this.address,
276
+ TakerGets: forAmountObj,
277
+ TakerPays: buyAmountObj,
278
+ Expiration: expiration,
279
+ Memos: TransactionHelper.formatMemos(memos)
280
+ }, options);
281
+ }
282
+
283
+ cancelOffer(offerSequence, memos = null, options = {}) {
284
+ return this.#submitAndVerifyTransaction({
285
+ TransactionType: 'OfferCancel',
286
+ Account: this.address,
287
+ OfferSequence: offerSequence,
288
+ Memos: TransactionHelper.formatMemos(memos)
289
+ }, options);
290
+ }
291
+
292
+ mintNft(uri, taxon, transferFee, flags = {}, memos = null, options = {}) {
293
+ return this.#submitAndVerifyTransaction({
294
+ TransactionType: 'NFTokenMint',
295
+ Account: this.address,
296
+ URI: flags.isHexUri ? uri : TransactionHelper.asciiToHex(uri).toUpperCase(),
297
+ NFTokenTaxon: taxon,
298
+ TransferFee: transferFee,
299
+ Flags: (flags.isBurnable ? 1 : 0) | (flags.isOnlyXRP ? 2 : 0) | (flags.isTrustLine ? 4 : 0) | (flags.isTransferable ? 8 : 0),
300
+ Memos: TransactionHelper.formatMemos(memos)
301
+ }, options);
302
+ }
303
+
304
+ offerSellNft(nfTokenId, amount, currency, issuer = null, destination = null, expiration = 4294967295, memos = null, options = {}) {
305
+
306
+ const amountObj = makeAmountObject(amount, currency, issuer);
307
+ const tx = {
308
+ TransactionType: 'NFTokenCreateOffer',
309
+ Account: this.address,
310
+ NFTokenID: nfTokenId,
311
+ Amount: amountObj,
312
+ Expiration: expiration,
313
+ Flags: 1, // tfSellToken
314
+ Memos: TransactionHelper.formatMemos(memos)
315
+ };
316
+
317
+ return this.#submitAndVerifyTransaction(destination ? { ...tx, Destination: destination } : tx, options);
318
+ }
319
+
320
+ offerBuyNft(nfTokenId, owner, amount, currency, issuer = null, expiration = 4294967295, memos = null, options = {}) {
321
+
322
+ const amountObj = makeAmountObject(amount, currency, issuer);
323
+
324
+ return this.#submitAndVerifyTransaction({
325
+ TransactionType: 'NFTokenCreateOffer',
326
+ Account: this.address,
327
+ NFTokenID: nfTokenId,
328
+ Owner: owner,
329
+ Amount: amountObj,
330
+ Expiration: expiration,
331
+ Flags: 0, // Buy offer
332
+ Memos: TransactionHelper.formatMemos(memos)
333
+ }, options);
334
+ }
335
+
336
+ sellNft(offerId, memos = null, options = {}) {
337
+
338
+ return this.#submitAndVerifyTransaction({
339
+ TransactionType: 'NFTokenAcceptOffer',
340
+ Account: this.address,
341
+ NFTokenBuyOffer: offerId,
342
+ Memos: TransactionHelper.formatMemos(memos)
343
+ }, options);
344
+ }
345
+
346
+ buyNft(offerId, memos = null, options = {}) {
347
+
348
+ return this.#submitAndVerifyTransaction({
349
+ TransactionType: 'NFTokenAcceptOffer',
350
+ Account: this.address,
351
+ NFTokenSellOffer: offerId,
352
+ Memos: TransactionHelper.formatMemos(memos)
353
+ }, options);
354
+ }
355
+
356
+ burnNft(nfTokenId, owner = null, memos = null, options = {}) {
357
+
358
+ const tx = {
359
+ TransactionType: 'NFTokenBurn',
360
+ Account: this.address,
361
+ NFTokenID: nfTokenId,
362
+ Memos: TransactionHelper.formatMemos(memos)
363
+ };
364
+
365
+ return this.#submitAndVerifyTransaction(owner ? { ...tx, Owner: owner } : tx, options);
366
+ }
367
+
368
+ generateKeylet(type, data = {}) {
369
+ switch (type) {
370
+ case 'nftPage': {
371
+ const accIdHex = (codec.decodeAccountID(this.address)).toString('hex').toUpperCase();
372
+ const tokenPortion = data?.nfTokenId.substr(40, 64);
373
+ return '0050' + accIdHex + tokenPortion;
374
+ }
375
+
376
+ case 'nftPageMax': {
377
+ const accIdHex = (codec.decodeAccountID(this.address)).toString('hex').toUpperCase();
378
+ return '0050' + accIdHex + 'F'.repeat(24);
379
+ }
380
+
381
+ case 'nftPageMin': {
382
+ const accIdHex = (codec.decodeAccountID(this.address)).toString('hex').toUpperCase();
383
+ return '0050' + accIdHex + '0'.repeat(24);
384
+ }
385
+
386
+ default:
387
+ return null;
388
+ }
389
+ }
390
+
391
+ async subscribe() {
392
+ // Subscribe only once. Otherwise event handlers will be duplicated.
393
+ if (this.#subscribed)
394
+ return;
395
+
396
+ await this.xrplApi.subscribeToAddress(this.address, this.#txStreamHandler);
397
+
398
+ this.#subscribed = true;
399
+ }
400
+
401
+ async unsubscribe() {
402
+ if (!this.#subscribed)
403
+ return;
404
+
405
+ await this.xrplApi.unsubscribeFromAddress(this.address, this.#txStreamHandler);
406
+ this.#subscribed = false;
407
+ }
408
+
409
+ #submitAndVerifyTransaction(tx, options) {
410
+
411
+ if (!this.wallet)
412
+ throw "no_secret";
413
+
414
+ // Returned format.
415
+ // {
416
+ // id: txHash, (if signing success)
417
+ // code: final transaction result code.
418
+ // details: submission and transaction details, (if signing success)
419
+ // error: Any error that prevents submission.
420
+ // }
421
+
422
+ return new Promise(async (resolve, reject) => {
423
+
424
+ // Attach tx options to the transaction.
425
+ const txOptions = {
426
+ LastLedgerSequence: options.maxLedgerIndex || (this.xrplApi.ledgerIndex + XrplConstants.MAX_LEDGER_OFFSET),
427
+ Sequence: options.sequence || await this.getSequence(),
428
+ SigningPubKey: '', // This field is required for fee calculation.
429
+ Fee: '0' // This field is required for fee calculation.
430
+ }
431
+ Object.assign(tx, txOptions);
432
+ const txnBlob = xrplCodec.encode(tx);
433
+ const fees = await this.xrplApi.getTransactionFee(txnBlob);
434
+ delete tx['SigningPubKey'];
435
+ tx.Fee = fees + '';
436
+
437
+ try {
438
+ const submission = await this.xrplApi.submitAndVerify(tx, { wallet: this.wallet });
439
+ const r = submission?.result;
440
+ const txResult = {
441
+ id: r?.hash,
442
+ code: r?.meta?.TransactionResult,
443
+ details: r
444
+ };
445
+
446
+ console.log("Transaction result: " + txResult.code);
447
+ if (txResult.code === "tesSUCCESS")
448
+ resolve(txResult);
449
+ else
450
+ reject(txResult);
451
+ }
452
+ catch (err) {
453
+ console.log("Error submitting transaction:", err);
454
+ reject({ error: err });
455
+ }
456
+
457
+ });
458
+ }
459
+
460
+ /**
461
+ * Submit the signed raw transaction.
462
+ * @param txBlob Signed and encoded transacion as a hex string.
463
+ */
464
+ submitTransactionBlob(txBlob) {
465
+
466
+ // Returned format.
467
+ // {
468
+ // id: txHash, (if signing success)
469
+ // code: final transaction result code.
470
+ // details: submission and transaction details, (if signing success)
471
+ // error: Any error that prevents submission.
472
+ // }
473
+
474
+ return new Promise(async (resolve, reject) => {
475
+ try {
476
+ const submission = await this.xrplApi.submitAndVerify(txBlob);
477
+ const r = submission?.result;
478
+ const txResult = {
479
+ id: r?.hash,
480
+ code: r?.meta?.TransactionResult,
481
+ details: r
482
+ };
483
+
484
+ console.log("Transaction result: " + txResult.code);
485
+ if (txResult.code === "tesSUCCESS")
486
+ resolve(txResult);
487
+ else
488
+ reject(txResult);
489
+ }
490
+ catch (err) {
491
+ console.log("Error submitting transaction:", err);
492
+ reject({ error: err });
493
+ }
494
+
495
+ });
496
+ }
497
+ }
498
+
499
+ function makeAmountObject(amount, currency, issuer) {
500
+ if (typeof amount !== 'string')
501
+ throw "Amount must be a string.";
502
+ if (currency !== XrplConstants.XRP && !issuer)
503
+ throw "Non-XRP currency must have an issuer.";
504
+
505
+ const amountObj = (currency == XrplConstants.XRP) ? amount : {
506
+ currency: currency,
507
+ issuer: issuer,
508
+ value: amount
509
+ }
510
+ return amountObj;
511
+ }
512
+
513
+ module.exports = {
514
+ XrplAccount
515
+ }