diff --git a/CHANGELOG.md b/CHANGELOG.md index c8925be3..375a4d13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ ## Unreleased +### Update +- Migrates code to use the native `BigInt` wherever possible over the `BigNumber` dependency ([#607](https://github.com/stellar/js-stellar-base/pull/607)). + ## [v9.0.0](https://github.com/stellar/js-stellar-base/compare/v8.2.2..v9.0.0) diff --git a/config/.eslintrc.js b/config/.eslintrc.js index c9bd0afc..1ed1e0a9 100644 --- a/config/.eslintrc.js +++ b/config/.eslintrc.js @@ -1,6 +1,7 @@ module.exports = { env: { - es6: true + es6: true, + es2020: true }, extends: ['airbnb-base', 'prettier'], plugins: ['@babel', 'prettier', 'prefer-import'], diff --git a/package.json b/package.json index 944f4d85..f85097fd 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "keywords": [ "stellar" ], - "author": "George Kudrayvtsev ", + "author": "Stellar Development Foundation ", "license": "Apache-2.0", "bugs": { "url": "https://github.com/stellar/js-stellar-base/issues" @@ -84,6 +84,7 @@ "buffer": "^6.0.3", "chai": "^4.3.7", "cross-env": "^7.0.3", + "crypto-browserify": "^3.12.0", "eslint": "^8.37.0", "eslint-config-airbnb-base": "^15.0.0", "eslint-config-prettier": "^8.8.0", diff --git a/src/account.js b/src/account.js index 82563453..3991715b 100644 --- a/src/account.js +++ b/src/account.js @@ -1,5 +1,3 @@ -import BigNumber from 'bignumber.js'; - import { StrKey } from './strkey'; /** @@ -33,7 +31,11 @@ export class Account { } this._accountId = accountId; - this.sequence = new BigNumber(sequence); + try { + this.sequence = BigInt(sequence); + } catch (e) { + throw new Error(`sequence is not a number: ${sequence}`); + } } /** @@ -57,6 +59,6 @@ export class Account { * @returns {void} */ incrementSequenceNumber() { - this.sequence = this.sequence.plus(1); + this.sequence += 1n; } } diff --git a/src/index.js b/src/index.js index 78a14744..e6197c9f 100644 --- a/src/index.js +++ b/src/index.js @@ -1,9 +1,6 @@ /* eslint-disable import/no-import-module-exports */ -import BigNumber from 'bignumber.js'; import xdr from './xdr'; -BigNumber.DEBUG = true; // gives us exceptions on bad constructor values - export { xdr }; export { hash } from './hashing'; export { sign, verify, FastSigning } from './signing'; diff --git a/src/memo.js b/src/memo.js index c2781d12..d68f6258 100644 --- a/src/memo.js +++ b/src/memo.js @@ -1,5 +1,4 @@ import { UnsignedHyper } from 'js-xdr'; -import BigNumber from 'bignumber.js'; import xdr from './xdr'; /** @@ -102,22 +101,11 @@ export class Memo { throw error; } - let number; try { - number = new BigNumber(value); + BigInt(value); // throws on invalid, inf, or NaN } catch (e) { throw error; } - - // Infinity - if (!number.isFinite()) { - throw error; - } - - // NaN - if (number.isNaN()) { - throw error; - } } static _validateTextValue(value) { diff --git a/src/operation.js b/src/operation.js index 1d03e217..33153a9b 100644 --- a/src/operation.js +++ b/src/operation.js @@ -19,6 +19,8 @@ import { const ONE = 10000000; const MAX_INT64 = '9223372036854775807'; +BigNumber.DEBUG = true; // gives us exceptions on bad constructor values + /** * When set using `{@link Operation.setOptions}` option, requires the issuing * account to give other accounts permission before they can hold the issuing @@ -502,10 +504,10 @@ export class Operation { if (price.n && price.d) { xdrObject = new xdr.Price(price); } else { - const approx = best_r(price); + const [n, d] = best_r(price); xdrObject = new xdr.Price({ - n: parseInt(approx[0], 10), - d: parseInt(approx[1], 10) + n: parseInt(n, 10), + d: parseInt(d, 10) }); } diff --git a/src/operations/bump_sequence.js b/src/operations/bump_sequence.js index a1488436..03401677 100644 --- a/src/operations/bump_sequence.js +++ b/src/operations/bump_sequence.js @@ -1,5 +1,5 @@ import { Hyper } from 'js-xdr'; -import BigNumber from 'bignumber.js'; + import xdr from '../xdr'; /** @@ -19,8 +19,7 @@ export function bumpSequence(opts) { } try { - // eslint-disable-next-line no-new - new BigNumber(opts.bumpTo); + BigInt(opts.bumpTo); } catch (e) { throw new Error('bumpTo must be a stringified number'); } diff --git a/src/operations/change_trust.js b/src/operations/change_trust.js index 65641850..c229951a 100644 --- a/src/operations/change_trust.js +++ b/src/operations/change_trust.js @@ -1,10 +1,9 @@ import { Hyper } from 'js-xdr'; -import BigNumber from 'bignumber.js'; import xdr from '../xdr'; import { Asset } from '../asset'; import { LiquidityPoolAsset } from '../liquidity_pool_asset'; -const MAX_INT64 = '9223372036854775807'; +const MAX_INT64 = 9223372036854775807n; /** * Returns an XDR ChangeTrustOp. A "change trust" operation adds, removes, or updates a @@ -36,7 +35,7 @@ export function changeTrust(opts) { if (opts.limit) { attributes.limit = this._toXDRAmount(opts.limit); } else { - attributes.limit = Hyper.fromString(new BigNumber(MAX_INT64).toString()); + attributes.limit = Hyper.fromString(MAX_INT64.toString()); } if (opts.source) { diff --git a/src/transaction_builder.js b/src/transaction_builder.js index 495316aa..c23e986c 100644 --- a/src/transaction_builder.js +++ b/src/transaction_builder.js @@ -1,5 +1,4 @@ import { UnsignedHyper } from 'js-xdr'; -import BigNumber from 'bignumber.js'; import xdr from './xdr'; import { Transaction } from './transaction'; @@ -440,10 +439,10 @@ export class TransactionBuilder { * @returns {Transaction} This method will return the built {@link Transaction}. */ build() { - const sequenceNumber = new BigNumber(this.source.sequenceNumber()).plus(1); - const fee = new BigNumber(this.baseFee) - .times(this.operations.length) - .toNumber(); + const sequenceNumber = BigInt(this.source.sequenceNumber()) + 1n; + const fee = Number( + BigInt.asIntN(32, BigInt(this.baseFee) * BigInt(this.operations.length)) + ); // base tx is int32 in XDR, feebump is int64 const attrs = { fee, seqNum: xdr.SequenceNumber.fromString(sequenceNumber.toString()), @@ -568,23 +567,23 @@ export class TransactionBuilder { innerTx, networkPassphrase ) { - const innerOps = innerTx.operations.length; - const innerBaseFeeRate = new BigNumber(innerTx.fee).div(innerOps); - const base = new BigNumber(baseFee); + const innerOps = BigInt(innerTx.operations.length); + const innerBaseFeeRate = BigInt(innerTx.fee) / innerOps; // truncates + const base = BigInt(baseFee); // The fee rate for fee bump is at least the fee rate of the inner transaction - if (base.lt(innerBaseFeeRate)) { + if (base < innerBaseFeeRate) { throw new Error( - `Invalid baseFee, it should be at least ${innerBaseFeeRate} stroops.` + `Invalid baseFee (${baseFee}), it should be at least ${innerBaseFeeRate} stroops.` ); } - const minBaseFee = new BigNumber(BASE_FEE); + const minBaseFee = BigInt(BASE_FEE); // The fee rate is at least the minimum fee - if (base.lt(minBaseFee)) { + if (base < minBaseFee) { throw new Error( - `Invalid baseFee, it should be at least ${minBaseFee} stroops.` + `Invalid baseFee (${baseFee}), it should be at least ${minBaseFee} stroops.` ); } @@ -619,7 +618,7 @@ export class TransactionBuilder { const tx = new xdr.FeeBumpTransaction({ feeSource: feeSourceAccount, - fee: xdr.Int64.fromString(base.times(innerOps + 1).toString()), + fee: xdr.Int64.fromString(BigInt.asIntN(64, (base * (innerOps + 1n)).toString())), innerTx: xdr.FeeBumpTransactionInnerTx.envelopeTypeTx( innerTxEnvelope.v1() ), diff --git a/src/util/continued_fraction.js b/src/util/continued_fraction.js index 51255f82..c36297c5 100644 --- a/src/util/continued_fraction.js +++ b/src/util/continued_fraction.js @@ -4,24 +4,62 @@ import BigNumber from 'bignumber.js'; const MAX_INT = ((1 << 31) >>> 0) - 1; /** - * Calculates and returns the best rational approximation of the given real number. + * Calculates and returns the best rational (fractional) approximation of the + * given real number. + * * @private - * @param {string|number|BigNumber} rawNumber Real number - * @throws Error Throws `Error` when the best rational approximation cannot be found. - * @returns {array} first element is n (numerator), second element is d (denominator) + * + * This is used internally to convert real-number-like prices into fractions for + * XDR to use as part of DEX offer & LP management. + * + * @param {string|number|BigInt} rawNumber the "real" number to approximate + * + * @returns {number[]} the numerator and denominator of the fractional + * approximation, respectively, where neither value exceeds `MAX_INT32` + * + * @throws {Error} throws an `Error` when no good rational approximation can be + * found. */ export function best_r(rawNumber) { - let number = new BigNumber(rawNumber); + BigNumber.DEBUG = true; // gives us exceptions on bad constructor values + + // NOTE: We can't convert this to use BigInt because the rational component is + // crucial to calculating the approximation. + let number = BigNumber(rawNumber); let a; let f; + + // We start with 0/1 and 1/0 as our approximations (the latter is technically + // undefined but we need it as a starting point) const fractions = [ [new BigNumber(0), new BigNumber(1)], [new BigNumber(1), new BigNumber(0)] ]; let i = 2; + /* + The algorithm is a form of the continued fraction expansion (hinted at by the + filename): + + > A continued fraction is an expression obtained through an iterative process + > of representing a number as the sum of its integer part and the reciprocal + > of another number, then writing this other number as the sum of its integer + > part and another reciprocal, and so on. + + https://en.wikipedia.org/wiki/Continued_fraction + + We run this loop until either: + + - any part of the fraction exceeds MAX_INT (though JS can handle bigger + numbers just fine, the xdr.Price object uses int32 values), OR + + - the "remainder" (`f` in the below loop) is zero (this means we've gotten a + perfect approximation) + */ // eslint-disable-next-line no-constant-condition while (true) { + // Compare the delta between the rational `number` and its truncated integer + // equivalent: `f` is everything after the decimal point. if (number.gt(MAX_INT)) { break; } @@ -39,8 +77,8 @@ export function best_r(rawNumber) { number = new BigNumber(1).div(f); i += 1; } - const [n, d] = fractions[fractions.length - 1]; + const [n, d] = fractions[fractions.length - 1]; if (n.isZero() || d.isZero()) { throw new Error("Couldn't find approximation"); } diff --git a/test/unit/transaction_builder_test.js b/test/unit/transaction_builder_test.js index 4419eb3d..a31a3cd4 100644 --- a/test/unit/transaction_builder_test.js +++ b/test/unit/transaction_builder_test.js @@ -528,7 +528,7 @@ describe('TransactionBuilder', function () { innerTx, networkPassphrase ); - }).to.throw(/Invalid baseFee, it should be at least 200 stroops./); + }).to.throw(/it should be at least 200 stroops/i); innerTx = new StellarBase.TransactionBuilder(innerAccount, { fee: '80', @@ -556,7 +556,7 @@ describe('TransactionBuilder', function () { innerTx, networkPassphrase ); - }).to.throw(/Invalid baseFee, it should be at least 100 stroops./); + }).to.throw(/it should be at least 100 stroops/i); innerTx = new StellarBase.TransactionBuilder(innerAccount, { fee: '100',