xdb-digitalbits-base
Version:
Low level digitalbits support library
428 lines (358 loc) • 17.9 kB
JavaScript
'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);
}