evernode-js-client 0.4.51 → 0.5.0

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