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,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
+ }