UNPKG

xdb-digitalbits-base

Version:
428 lines (358 loc) 17.9 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); exports.TransactionBuilder = exports.TimeoutInfinite = exports.BASE_FEE = undefined; var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); exports.isValidDate = isValidDate; var _jsXdr = require('js-xdr'); var _bignumber = require('bignumber.js'); var _bignumber2 = _interopRequireDefault(_bignumber); var _clone = require('lodash/clone'); var _clone2 = _interopRequireDefault(_clone); var _isUndefined = require('lodash/isUndefined'); var _isUndefined2 = _interopRequireDefault(_isUndefined); var _isString = require('lodash/isString'); var _isString2 = _interopRequireDefault(_isString); var _digitalbitsXdr_generated = require('./generated/digitalbits-xdr_generated'); var _digitalbitsXdr_generated2 = _interopRequireDefault(_digitalbitsXdr_generated); var _transaction = require('./transaction'); var _fee_bump_transaction = require('./fee_bump_transaction'); var _memo = require('./memo'); var _decode_encode_muxed_account = require('./util/decode_encode_muxed_account'); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } /** * Minimum base fee for transactions. If this fee is below the network * minimum, the transaction will fail. The more operations in the * transaction, the greater the required fee. Use {@link * Server#fetchBaseFee} to get an accurate value of minimum transaction * fee on the network. * * @constant * @see [Fees](https://developers.digitalbits.io/guides/concepts/fees.html) */ var BASE_FEE = exports.BASE_FEE = '100'; // nibbs /** * @constant * @see {@link TransactionBuilder#setTimeout} */ var TimeoutInfinite = exports.TimeoutInfinite = 0; /** * <p>Transaction builder helps constructs a new `{@link Transaction}` using the * given {@link Account} as the transaction's "source account". The transaction * will use the current sequence number of the given account as its sequence * number and increment the given account's sequence number by one. The given * source account must include a private key for signing the transaction or an * error will be thrown.</p> * * <p>Operations can be added to the transaction via their corresponding builder * methods, and each returns the TransactionBuilder object so they can be * chained together. After adding the desired operations, call the `build()` * method on the `TransactionBuilder` to return a fully constructed `{@link * Transaction}` that can be signed. The returned transaction will contain the * sequence number of the source account and include the signature from the * source account.</p> * * <p><strong>Be careful about unsubmitted transactions!</strong> When you build * a transaction, digitalbits-sdk automatically increments the source account's * sequence number. If you end up not submitting this transaction and submitting * another one instead, it'll fail due to the sequence number being wrong. So if * you decide not to use a built transaction, make sure to update the source * account's sequence number with Server.loadAccount * before creating another transaction.</p> * * <p>The following code example creates a new transaction with {@link * Operation.createAccount} and {@link Operation.payment} operations. The * Transaction's source account first funds `destinationA`, then sends a payment * to `destinationB`. The built transaction is then signed by * `sourceKeypair`.</p> * * ``` * var transaction = new TransactionBuilder(source, { fee, networkPassphrase: Networks.TESTNET }) * .addOperation(Operation.createAccount({ * destination: destinationA, * startingBalance: "20" * })) // <- funds and creates destinationA * .addOperation(Operation.payment({ * destination: destinationB, * amount: "100", * asset: Asset.native() * })) // <- sends 100 XDB to destinationB * .setTimeout(30) * .build(); * * transaction.sign(sourceKeypair); * ``` * * @constructor * * @param {Account} sourceAccount - source account for this transaction * @param {object} opts - Options object * @param {string} opts.fee - max fee you're willing to pay per * operation in this transaction (**in nibbs**) * * @param {object} [opts.timebounds] - timebounds for the * validity of this transaction * @param {number|string|Date} [opts.timebounds.minTime] - 64-bit UNIX * timestamp or Date object * @param {number|string|Date} [opts.timebounds.maxTime] - 64-bit UNIX * timestamp or Date object * @param {Memo} [opts.memo] - memo for the transaction * @param {string} [opts.networkPassphrase] passphrase of the * target DigitalBits network (e.g. "LiveNet Global DigitalBits Network ; February 2021" * for the pubnet) * @param {bool} [opts.withMuxing] - Indicates that the source account of * every transaction created by this Builder can be interpreted as a proper * muxed account (i.e. coming from an M... address). By default, this option * is disabled until muxed accounts are mature. */ var TransactionBuilder = exports.TransactionBuilder = function () { function TransactionBuilder(sourceAccount) { var opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; _classCallCheck(this, TransactionBuilder); if (!sourceAccount) { throw new Error('must specify source account for the transaction'); } if ((0, _isUndefined2.default)(opts.fee)) { throw new Error('must specify fee for the transaction (in nibbs)'); } this.source = sourceAccount; this.operations = []; this.baseFee = (0, _isUndefined2.default)(opts.fee) ? BASE_FEE : opts.fee; this.timebounds = (0, _clone2.default)(opts.timebounds) || null; this.memo = opts.memo || _memo.Memo.none(); this.networkPassphrase = opts.networkPassphrase || null; this.supportMuxedAccounts = opts.withMuxing || false; } /** * Adds an operation to the transaction. * @param {xdr.Operation} operation The xdr operation object, use {@link Operation} static methods. * @returns {TransactionBuilder} */ _createClass(TransactionBuilder, [{ key: 'addOperation', value: function addOperation(operation) { this.operations.push(operation); return this; } /** * Adds a memo to the transaction. * @param {Memo} memo {@link Memo} object * @returns {TransactionBuilder} */ }, { key: 'addMemo', value: function addMemo(memo) { this.memo = memo; return this; } /** * Because of the distributed nature of the DigitalBits network it is possible that the status of your transaction * will be determined after a long time if the network is highly congested. * If you want to be sure to receive the status of the transaction within a given period you should set the * {@link TimeBounds} with <code>maxTime</code> on the transaction (this is what <code>setTimeout</code> does * internally; if there's <code>minTime</code> set but no <code>maxTime</code> it will be added). * Call to <code>TransactionBuilder.setTimeout</code> is required if Transaction does not have <code>max_time</code> set. * If you don't want to set timeout, use <code>{@link TimeoutInfinite}</code>. In general you should set * <code>{@link TimeoutInfinite}</code> only in smart contracts. * * Please note that Frontier may still return <code>504 Gateway Timeout</code> error, even for short timeouts. * In such case you need to resubmit the same transaction again without making any changes to receive a status. * This method is using the machine system time (UTC), make sure it is set correctly. * @param {number} timeout Number of seconds the transaction is good. Can't be negative. * If the value is `0`, the transaction is good indefinitely. * @return {TransactionBuilder} * @see TimeoutInfinite */ }, { key: 'setTimeout', value: function setTimeout(timeout) { if (this.timebounds !== null && this.timebounds.maxTime > 0) { throw new Error('TimeBounds.max_time has been already set - setting timeout would overwrite it.'); } if (timeout < 0) { throw new Error('timeout cannot be negative'); } if (timeout > 0) { var timeoutTimestamp = Math.floor(Date.now() / 1000) + timeout; if (this.timebounds === null) { this.timebounds = { minTime: 0, maxTime: timeoutTimestamp }; } else { this.timebounds = { minTime: this.timebounds.minTime, maxTime: timeoutTimestamp }; } } else { this.timebounds = { minTime: 0, maxTime: 0 }; } return this; } /** * Set network nassphrase for the Transaction that will be built. * * @param {string} [networkPassphrase] passphrase of the target DigitalBits network (e.g. "LiveNet Global DigitalBits Network ; February 2021"). * @returns {TransactionBuilder} */ }, { key: 'setNetworkPassphrase', value: function setNetworkPassphrase(networkPassphrase) { this.networkPassphrase = networkPassphrase; return this; } /** * Enable support for muxed accounts for the Transaction that will be built. * @returns {TransactionBuilder} */ }, { key: 'enableMuxedAccounts', value: function enableMuxedAccounts() { this.supportMuxedAccounts = true; return this; } /** * This will build the transaction. * It will also increment the source account's sequence number by 1. * @returns {Transaction} This method will return the built {@link Transaction}. */ }, { key: 'build', value: function build() { var sequenceNumber = new _bignumber2.default(this.source.sequenceNumber()).add(1); var fee = new _bignumber2.default(this.baseFee).mul(this.operations.length).toNumber(); var attrs = { fee: fee, seqNum: _digitalbitsXdr_generated2.default.SequenceNumber.fromString(sequenceNumber.toString()), memo: this.memo ? this.memo.toXDRObject() : null }; if (this.timebounds === null || typeof this.timebounds.minTime === 'undefined' || typeof this.timebounds.maxTime === 'undefined') { throw new Error('TimeBounds has to be set or you must call setTimeout(TimeoutInfinite).'); } if (isValidDate(this.timebounds.minTime)) { this.timebounds.minTime = this.timebounds.minTime.getTime() / 1000; } if (isValidDate(this.timebounds.maxTime)) { this.timebounds.maxTime = this.timebounds.maxTime.getTime() / 1000; } this.timebounds.minTime = _jsXdr.UnsignedHyper.fromString(this.timebounds.minTime.toString()); this.timebounds.maxTime = _jsXdr.UnsignedHyper.fromString(this.timebounds.maxTime.toString()); attrs.timeBounds = new _digitalbitsXdr_generated2.default.TimeBounds(this.timebounds); attrs.sourceAccount = (0, _decode_encode_muxed_account.decodeAddressToMuxedAccount)(this.source.accountId(), this.supportMuxedAccounts); attrs.ext = new _digitalbitsXdr_generated2.default.TransactionExt(0); var xtx = new _digitalbitsXdr_generated2.default.Transaction(attrs); xtx.operations(this.operations); var txEnvelope = new _digitalbitsXdr_generated2.default.TransactionEnvelope.envelopeTypeTx(new _digitalbitsXdr_generated2.default.TransactionV1Envelope({ tx: xtx })); var tx = new _transaction.Transaction(txEnvelope, this.networkPassphrase, this.supportMuxedAccounts); this.source.incrementSequenceNumber(); return tx; } /** * Builds a {@link FeeBumpTransaction}, enabling you to resubmit an existing * transaction with a higher fee. * * @param {Keypair|string} feeSource - account paying for the transaction, * in the form of either a Keypair (only the public key is used) or * an account ID (in G... or M... form, but refer to `withMuxing`) * @param {string} baseFee - max fee willing to pay per operation * in inner transaction (**in nibbs**) * @param {Transaction} innerTx - {@link Transaction} to be bumped by * the fee bump transaction * @param {string} networkPassphrase - passphrase of the target DigitalBits * network (e.g. "LiveNet Global DigitalBits Network ; February 2021") * @param {bool} [withMuxing] - allows fee sources to be proper * muxed accounts (i.e. coming from an M... address). By default, this * option is disabled until muxed accounts are mature. * * @todo Alongside the next major version bump, this type signature can be * changed to be less awkward: accept a MuxedAccount as the `feeSource` * rather than a keypair or string. * * @note Your fee-bump amount should be 10x the original fee. * * @returns {FeeBumpTransaction} */ }], [{ key: 'buildFeeBumpTransaction', value: function buildFeeBumpTransaction(feeSource, baseFee, innerTx, networkPassphrase, withMuxing) { var innerOps = innerTx.operations.length; var innerBaseFeeRate = new _bignumber2.default(innerTx.fee).div(innerOps); var base = new _bignumber2.default(baseFee); // The fee rate for fee bump is at least the fee rate of the inner transaction if (base.lessThan(innerBaseFeeRate)) { throw new Error('Invalid baseFee, it should be at least ' + innerBaseFeeRate + ' nibbs.'); } var minBaseFee = new _bignumber2.default(BASE_FEE); // The fee rate is at least the minimum fee if (base.lessThan(minBaseFee)) { throw new Error('Invalid baseFee, it should be at least ' + minBaseFee + ' nibbs.'); } var innerTxEnvelope = innerTx.toEnvelope(); if (innerTxEnvelope.switch() === _digitalbitsXdr_generated2.default.EnvelopeType.envelopeTypeTxV0()) { var v0Tx = innerTxEnvelope.v0().tx(); var v1Tx = new _digitalbitsXdr_generated2.default.Transaction({ sourceAccount: new _digitalbitsXdr_generated2.default.MuxedAccount.keyTypeEd25519(v0Tx.sourceAccountEd25519()), fee: v0Tx.fee(), seqNum: v0Tx.seqNum(), timeBounds: v0Tx.timeBounds(), memo: v0Tx.memo(), operations: v0Tx.operations(), ext: new _digitalbitsXdr_generated2.default.TransactionExt(0) }); innerTxEnvelope = new _digitalbitsXdr_generated2.default.TransactionEnvelope.envelopeTypeTx(new _digitalbitsXdr_generated2.default.TransactionV1Envelope({ tx: v1Tx, signatures: innerTxEnvelope.v0().signatures() })); } var feeSourceAccount = void 0; if ((0, _isString2.default)(feeSource)) { feeSourceAccount = (0, _decode_encode_muxed_account.decodeAddressToMuxedAccount)(feeSource, withMuxing); } else { feeSourceAccount = feeSource.xdrMuxedAccount(); } var tx = new _digitalbitsXdr_generated2.default.FeeBumpTransaction({ feeSource: feeSourceAccount, fee: _digitalbitsXdr_generated2.default.Int64.fromString(base.mul(innerOps + 1).toString()), innerTx: _digitalbitsXdr_generated2.default.FeeBumpTransactionInnerTx.envelopeTypeTx(innerTxEnvelope.v1()), ext: new _digitalbitsXdr_generated2.default.FeeBumpTransactionExt(0) }); var feeBumpTxEnvelope = new _digitalbitsXdr_generated2.default.FeeBumpTransactionEnvelope({ tx: tx, signatures: [] }); var envelope = new _digitalbitsXdr_generated2.default.TransactionEnvelope.envelopeTypeTxFeeBump(feeBumpTxEnvelope); return new _fee_bump_transaction.FeeBumpTransaction(envelope, networkPassphrase, withMuxing); } /** * Build a {@link Transaction} or {@link FeeBumpTransaction} from an xdr.TransactionEnvelope. * @param {string|xdr.TransactionEnvelope} envelope - The transaction envelope object or base64 encoded string. * @param {string} networkPassphrase - networkPassphrase of the target DigitalBits network (e.g. "LiveNet Global DigitalBits Network ; February 2021"). * @returns {Transaction|FeeBumpTransaction} */ }, { key: 'fromXDR', value: function fromXDR(envelope, networkPassphrase) { if (typeof envelope === 'string') { envelope = _digitalbitsXdr_generated2.default.TransactionEnvelope.fromXDR(envelope, 'base64'); } if (envelope.switch() === _digitalbitsXdr_generated2.default.EnvelopeType.envelopeTypeTxFeeBump()) { return new _fee_bump_transaction.FeeBumpTransaction(envelope, networkPassphrase); } return new _transaction.Transaction(envelope, networkPassphrase); } }]); return TransactionBuilder; }(); /** * Checks whether a provided object is a valid Date. * @argument {Date} d date object * @returns {boolean} */ function isValidDate(d) { // isnan is okay here because it correctly checks for invalid date objects // eslint-disable-next-line no-restricted-globals return d instanceof Date && !isNaN(d); }