mirror of
https://github.com/XRPLF/rippled.git
synced 2025-11-20 11:05:54 +00:00
Add patches for ripple-lib core
This commit is contained in:
@@ -20,6 +20,7 @@
|
||||
"simple-jsonrpc": "~0.0.2"
|
||||
},
|
||||
"scripts": {
|
||||
"pretest": "node test/pretest.js",
|
||||
"test": "mocha test/websocket-test.js test/server-test.js test/*-test.{js,coffee}"
|
||||
},
|
||||
"repository": {
|
||||
|
||||
12
test/pretest.js
Normal file
12
test/pretest.js
Normal file
@@ -0,0 +1,12 @@
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var joinPath = path.join.bind(path, __dirname);
|
||||
|
||||
fs.readdirSync(joinPath('ripple-lib')).forEach(function(fileName) {
|
||||
var src_path = joinPath('ripple-lib', fileName);
|
||||
var dst_path = joinPath('../node_modules/ripple-lib/dist/npm/core/', fileName);
|
||||
|
||||
console.log(src_path + ' > ' + dst_path);
|
||||
|
||||
fs.writeFileSync(dst_path, fs.readFileSync(src_path));
|
||||
});
|
||||
301
test/ripple-lib/binformat.js
Normal file
301
test/ripple-lib/binformat.js
Normal file
@@ -0,0 +1,301 @@
|
||||
'use strict';
|
||||
|
||||
/*eslint-disable max-len,spaced-comment,array-bracket-spacing,key-spacing*/
|
||||
/*eslint-disable no-multi-spaces,comma-spacing*/
|
||||
/*eslint-disable no-multi-spaces:0,space-in-brackets:0,key-spacing:0,comma-spacing:0*/
|
||||
|
||||
/**
|
||||
* Data type map.
|
||||
*
|
||||
* Mapping of type ids to data types. The type id is specified by the high
|
||||
*
|
||||
* For reference, see rippled's definition:
|
||||
* https://github.com/ripple/rippled/blob/develop/src/ripple/data/protocol
|
||||
* /SField.cpp
|
||||
*/
|
||||
|
||||
exports.types = [undefined,
|
||||
|
||||
// Common
|
||||
'Int16', // 1
|
||||
'Int32', // 2
|
||||
'Int64', // 3
|
||||
'Hash128', // 4
|
||||
'Hash256', // 5
|
||||
'Amount', // 6
|
||||
'VL', // 7
|
||||
'Account', // 8
|
||||
|
||||
// 9-13 reserved
|
||||
undefined, // 9
|
||||
undefined, // 10
|
||||
undefined, // 11
|
||||
undefined, // 12
|
||||
undefined, // 13
|
||||
|
||||
'Object', // 14
|
||||
'Array', // 15
|
||||
|
||||
// Uncommon
|
||||
'Int8', // 16
|
||||
'Hash160', // 17
|
||||
'PathSet', // 18
|
||||
'Vector256' // 19
|
||||
];
|
||||
|
||||
/**
|
||||
* Field type map.
|
||||
*
|
||||
* Mapping of field type id to field type name.
|
||||
*/
|
||||
|
||||
var FIELDS_MAP = exports.fields = {
|
||||
// Common types
|
||||
1: { // Int16
|
||||
1: 'LedgerEntryType',
|
||||
2: 'TransactionType',
|
||||
3: 'SignerWeight'
|
||||
},
|
||||
2: { // Int32
|
||||
2: 'Flags',
|
||||
3: 'SourceTag',
|
||||
4: 'Sequence',
|
||||
5: 'PreviousTxnLgrSeq',
|
||||
6: 'LedgerSequence',
|
||||
7: 'CloseTime',
|
||||
8: 'ParentCloseTime',
|
||||
9: 'SigningTime',
|
||||
10: 'Expiration',
|
||||
11: 'TransferRate',
|
||||
12: 'WalletSize',
|
||||
13: 'OwnerCount',
|
||||
14: 'DestinationTag',
|
||||
// Skip 15
|
||||
16: 'HighQualityIn',
|
||||
17: 'HighQualityOut',
|
||||
18: 'LowQualityIn',
|
||||
19: 'LowQualityOut',
|
||||
20: 'QualityIn',
|
||||
21: 'QualityOut',
|
||||
22: 'StampEscrow',
|
||||
23: 'BondAmount',
|
||||
24: 'LoadFee',
|
||||
25: 'OfferSequence',
|
||||
26: 'FirstLedgerSequence',
|
||||
27: 'LastLedgerSequence',
|
||||
28: 'TransactionIndex',
|
||||
29: 'OperationLimit',
|
||||
30: 'ReferenceFeeUnits',
|
||||
31: 'ReserveBase',
|
||||
32: 'ReserveIncrement',
|
||||
33: 'SetFlag',
|
||||
34: 'ClearFlag',
|
||||
35: 'SignerQuorum',
|
||||
36: 'CancelAfter',
|
||||
37: 'FinishAfter',
|
||||
38: 'SignerListID'
|
||||
},
|
||||
3: { // Int64
|
||||
1: 'IndexNext',
|
||||
2: 'IndexPrevious',
|
||||
3: 'BookNode',
|
||||
4: 'OwnerNode',
|
||||
5: 'BaseFee',
|
||||
6: 'ExchangeRate',
|
||||
7: 'LowNode',
|
||||
8: 'HighNode'
|
||||
},
|
||||
4: { // Hash128
|
||||
1: 'EmailHash'
|
||||
},
|
||||
5: { // Hash256
|
||||
1: 'LedgerHash',
|
||||
2: 'ParentHash',
|
||||
3: 'TransactionHash',
|
||||
4: 'AccountHash',
|
||||
5: 'PreviousTxnID',
|
||||
6: 'LedgerIndex',
|
||||
7: 'WalletLocator',
|
||||
8: 'RootIndex',
|
||||
9: 'AccountTxnID',
|
||||
16: 'BookDirectory',
|
||||
17: 'InvoiceID',
|
||||
18: 'Nickname',
|
||||
19: 'Amendment',
|
||||
20: 'TicketID',
|
||||
21: 'Digest'
|
||||
},
|
||||
6: { // Amount
|
||||
1: 'Amount',
|
||||
2: 'Balance',
|
||||
3: 'LimitAmount',
|
||||
4: 'TakerPays',
|
||||
5: 'TakerGets',
|
||||
6: 'LowLimit',
|
||||
7: 'HighLimit',
|
||||
8: 'Fee',
|
||||
9: 'SendMax',
|
||||
16: 'MinimumOffer',
|
||||
17: 'RippleEscrow',
|
||||
18: 'DeliveredAmount'
|
||||
},
|
||||
7: { // VL
|
||||
1: 'PublicKey',
|
||||
2: 'MessageKey',
|
||||
3: 'SigningPubKey',
|
||||
4: 'TxnSignature',
|
||||
5: 'Generator',
|
||||
6: 'Signature',
|
||||
7: 'Domain',
|
||||
8: 'FundCode',
|
||||
9: 'RemoveCode',
|
||||
10: 'ExpireCode',
|
||||
11: 'CreateCode',
|
||||
12: 'MemoType',
|
||||
13: 'MemoData',
|
||||
14: 'MemoFormat',
|
||||
17: 'Proof'
|
||||
},
|
||||
8: { // Account
|
||||
1: 'Account',
|
||||
2: 'Owner',
|
||||
3: 'Destination',
|
||||
4: 'Issuer',
|
||||
7: 'Target',
|
||||
8: 'RegularKey'
|
||||
},
|
||||
14: { // Object
|
||||
1: undefined, // end of Object
|
||||
2: 'TransactionMetaData',
|
||||
3: 'CreatedNode',
|
||||
4: 'DeletedNode',
|
||||
5: 'ModifiedNode',
|
||||
6: 'PreviousFields',
|
||||
7: 'FinalFields',
|
||||
8: 'NewFields',
|
||||
9: 'TemplateEntry',
|
||||
10: 'Memo',
|
||||
11: 'SignerEntry',
|
||||
16: 'Signer'
|
||||
},
|
||||
15: { // Array
|
||||
1: undefined, // end of Array
|
||||
2: 'SigningAccounts',
|
||||
3: 'Signers',
|
||||
4: 'SignerEntries',
|
||||
5: 'Template',
|
||||
6: 'Necessary',
|
||||
7: 'Sufficient',
|
||||
8: 'AffectedNodes',
|
||||
9: 'Memos'
|
||||
},
|
||||
|
||||
// Uncommon types
|
||||
16: { // Int8
|
||||
1: 'CloseResolution',
|
||||
2: 'Method',
|
||||
3: 'TransactionResult'
|
||||
},
|
||||
17: { // Hash160
|
||||
1: 'TakerPaysCurrency',
|
||||
2: 'TakerPaysIssuer',
|
||||
3: 'TakerGetsCurrency',
|
||||
4: 'TakerGetsIssuer'
|
||||
},
|
||||
18: { // PathSet
|
||||
1: 'Paths'
|
||||
},
|
||||
19: { // Vector256
|
||||
1: 'Indexes',
|
||||
2: 'Hashes',
|
||||
3: 'Amendments'
|
||||
}
|
||||
};
|
||||
|
||||
var INVERSE_FIELDS_MAP = exports.fieldsInverseMap = {};
|
||||
|
||||
Object.keys(FIELDS_MAP).forEach(function (k1) {
|
||||
Object.keys(FIELDS_MAP[k1]).forEach(function (k2) {
|
||||
INVERSE_FIELDS_MAP[FIELDS_MAP[k1][k2]] = [Number(k1), Number(k2)];
|
||||
});
|
||||
});
|
||||
|
||||
var REQUIRED = exports.REQUIRED = 0;
|
||||
var OPTIONAL = exports.OPTIONAL = 1;
|
||||
var DEFAULT = exports.DEFAULT = 2;
|
||||
|
||||
var base = [['TransactionType', REQUIRED], ['Flags', OPTIONAL], ['SourceTag', OPTIONAL], ['LastLedgerSequence', OPTIONAL], ['Account', REQUIRED], ['Sequence', REQUIRED], ['Fee', REQUIRED], ['OperationLimit', OPTIONAL], ['SigningPubKey', REQUIRED], ['TxnSignature', OPTIONAL], ['AccountTxnID', OPTIONAL], ['Memos', OPTIONAL], ['Signers', OPTIONAL]];
|
||||
|
||||
exports.tx = {
|
||||
AccountSet: [3].concat(base, [['EmailHash', OPTIONAL], ['WalletLocator', OPTIONAL], ['WalletSize', OPTIONAL], ['MessageKey', OPTIONAL], ['Domain', OPTIONAL], ['TransferRate', OPTIONAL], ['SetFlag', OPTIONAL], ['ClearFlag', OPTIONAL]]),
|
||||
TrustSet: [20].concat(base, [['LimitAmount', OPTIONAL], ['QualityIn', OPTIONAL], ['QualityOut', OPTIONAL]]),
|
||||
OfferCreate: [7].concat(base, [['TakerPays', REQUIRED], ['TakerGets', REQUIRED], ['Expiration', OPTIONAL], ['OfferSequence', OPTIONAL]]),
|
||||
OfferCancel: [8].concat(base, [['OfferSequence', REQUIRED]]),
|
||||
SetRegularKey: [5].concat(base, [['RegularKey', OPTIONAL]]),
|
||||
Payment: [0].concat(base, [['Destination', REQUIRED], ['Amount', REQUIRED], ['SendMax', OPTIONAL], ['Paths', DEFAULT], ['InvoiceID', OPTIONAL], ['DestinationTag', OPTIONAL]]),
|
||||
Contract: [9].concat(base, [['Expiration', REQUIRED], ['BondAmount', REQUIRED], ['StampEscrow', REQUIRED], ['RippleEscrow', REQUIRED], ['CreateCode', OPTIONAL], ['FundCode', OPTIONAL], ['RemoveCode', OPTIONAL], ['ExpireCode', OPTIONAL]]),
|
||||
RemoveContract: [10].concat(base, [['Target', REQUIRED]]),
|
||||
EnableFeature: [100].concat(base, [['Feature', REQUIRED]]),
|
||||
EnableAmendment: [100].concat(base, [['Amendment', REQUIRED]]),
|
||||
SetFee: [101].concat(base, [['BaseFee', REQUIRED], ['ReferenceFeeUnits', REQUIRED], ['ReserveBase', REQUIRED], ['ReserveIncrement', REQUIRED]]),
|
||||
TicketCreate: [10].concat(base, [['Target', OPTIONAL], ['Expiration', OPTIONAL]]),
|
||||
TicketCancel: [11].concat(base, [['TicketID', REQUIRED]]),
|
||||
SignerListSet: [12].concat(base, [['SignerQuorum', REQUIRED], ['SignerEntries', OPTIONAL]]),
|
||||
SuspendedPaymentCreate: [1].concat(base, [['Destination', REQUIRED], ['Amount', REQUIRED], ['Digest', OPTIONAL], ['CancelAfter', OPTIONAL], ['FinishAfter', OPTIONAL], ['DestinationTag', OPTIONAL]]),
|
||||
SuspendedPaymentFinish: [2].concat(base, [['Owner', REQUIRED], ['OfferSequence', REQUIRED], ['Method', OPTIONAL], ['Digest', OPTIONAL], ['Proof', OPTIONAL]]),
|
||||
SuspendedPaymentCancel: [4].concat(base, [['Owner', REQUIRED], ['OfferSequence', REQUIRED]])
|
||||
};
|
||||
|
||||
var sleBase = [['LedgerIndex', OPTIONAL], ['LedgerEntryType', REQUIRED], ['Flags', REQUIRED]];
|
||||
|
||||
exports.ledger = {
|
||||
AccountRoot: [97].concat(sleBase, [['Sequence', REQUIRED], ['PreviousTxnLgrSeq', REQUIRED], ['TransferRate', OPTIONAL], ['WalletSize', OPTIONAL], ['OwnerCount', REQUIRED], ['EmailHash', OPTIONAL], ['PreviousTxnID', REQUIRED], ['AccountTxnID', OPTIONAL], ['WalletLocator', OPTIONAL], ['Balance', REQUIRED], ['MessageKey', OPTIONAL], ['Domain', OPTIONAL], ['Account', REQUIRED], ['RegularKey', OPTIONAL]]),
|
||||
Contract: [99].concat(sleBase, [['PreviousTxnLgrSeq', REQUIRED], ['Expiration', REQUIRED], ['BondAmount', REQUIRED], ['PreviousTxnID', REQUIRED], ['Balance', REQUIRED], ['FundCode', OPTIONAL], ['RemoveCode', OPTIONAL], ['ExpireCode', OPTIONAL], ['CreateCode', OPTIONAL], ['Account', REQUIRED], ['Owner', REQUIRED], ['Issuer', REQUIRED]]),
|
||||
DirectoryNode: [100].concat(sleBase, [['IndexNext', OPTIONAL], ['IndexPrevious', OPTIONAL], ['ExchangeRate', OPTIONAL], ['RootIndex', REQUIRED], ['Owner', OPTIONAL], ['TakerPaysCurrency', OPTIONAL], ['TakerPaysIssuer', OPTIONAL], ['TakerGetsCurrency', OPTIONAL], ['TakerGetsIssuer', OPTIONAL], ['Indexes', REQUIRED]]),
|
||||
EnabledFeatures: [102].concat(sleBase, [['Features', REQUIRED]]),
|
||||
FeeSettings: [115].concat(sleBase, [['ReferenceFeeUnits', REQUIRED], ['ReserveBase', REQUIRED], ['ReserveIncrement', REQUIRED], ['BaseFee', REQUIRED], ['LedgerIndex', OPTIONAL]]),
|
||||
GeneratorMap: [103].concat(sleBase, [['Generator', REQUIRED]]),
|
||||
LedgerHashes: [104].concat(sleBase, [['LedgerEntryType', REQUIRED], ['Flags', REQUIRED], ['FirstLedgerSequence', OPTIONAL], ['LastLedgerSequence', OPTIONAL], ['LedgerIndex', OPTIONAL], ['Hashes', REQUIRED]]),
|
||||
Nickname: [110].concat(sleBase, [['LedgerEntryType', REQUIRED], ['Flags', REQUIRED], ['LedgerIndex', OPTIONAL], ['MinimumOffer', OPTIONAL], ['Account', REQUIRED]]),
|
||||
Offer: [111].concat(sleBase, [['LedgerEntryType', REQUIRED], ['Flags', REQUIRED], ['Sequence', REQUIRED], ['PreviousTxnLgrSeq', REQUIRED], ['Expiration', OPTIONAL], ['BookNode', REQUIRED], ['OwnerNode', REQUIRED], ['PreviousTxnID', REQUIRED], ['LedgerIndex', OPTIONAL], ['BookDirectory', REQUIRED], ['TakerPays', REQUIRED], ['TakerGets', REQUIRED], ['Account', REQUIRED]]),
|
||||
RippleState: [114].concat(sleBase, [['LedgerEntryType', REQUIRED], ['Flags', REQUIRED], ['PreviousTxnLgrSeq', REQUIRED], ['HighQualityIn', OPTIONAL], ['HighQualityOut', OPTIONAL], ['LowQualityIn', OPTIONAL], ['LowQualityOut', OPTIONAL], ['LowNode', OPTIONAL], ['HighNode', OPTIONAL], ['PreviousTxnID', REQUIRED], ['LedgerIndex', OPTIONAL], ['Balance', REQUIRED], ['LowLimit', REQUIRED], ['HighLimit', REQUIRED]]),
|
||||
SignerList: [83].concat(sleBase, [['OwnerNode', REQUIRED], ['SignerQuorum', REQUIRED], ['SignerEntries', REQUIRED], ['SignerListID', REQUIRED], ['PreviousTxnID', REQUIRED], ['PreviousTxnLgrSeq', REQUIRED]])
|
||||
};
|
||||
|
||||
exports.metadata = [['DeliveredAmount', OPTIONAL], ['TransactionIndex', REQUIRED], ['TransactionResult', REQUIRED], ['AffectedNodes', REQUIRED]];
|
||||
|
||||
exports.ter = {
|
||||
tesSUCCESS: 0,
|
||||
tecCLAIM: 100,
|
||||
tecPATH_PARTIAL: 101,
|
||||
tecUNFUNDED_ADD: 102,
|
||||
tecUNFUNDED_OFFER: 103,
|
||||
tecUNFUNDED_PAYMENT: 104,
|
||||
tecFAILED_PROCESSING: 105,
|
||||
tecDIR_FULL: 121,
|
||||
tecINSUF_RESERVE_LINE: 122,
|
||||
tecINSUF_RESERVE_OFFER: 123,
|
||||
tecNO_DST: 124,
|
||||
tecNO_DST_INSUF_XRP: 125,
|
||||
tecNO_LINE_INSUF_RESERVE: 126,
|
||||
tecNO_LINE_REDUNDANT: 127,
|
||||
tecPATH_DRY: 128,
|
||||
tecUNFUNDED: 129, // Deprecated, old ambiguous unfunded.
|
||||
tecNO_ALTERNATIVE_KEY: 130,
|
||||
tecNO_REGULAR_KEY: 131,
|
||||
tecOWNERS: 132,
|
||||
tecNO_ISSUER: 133,
|
||||
tecNO_AUTH: 134,
|
||||
tecNO_LINE: 135,
|
||||
tecINSUFF_FEE: 136,
|
||||
tecFROZEN: 137,
|
||||
tecNO_TARGET: 138,
|
||||
tecNO_PERMISSION: 139,
|
||||
tecNO_ENTRY: 140,
|
||||
tecINSUFFICIENT_RESERVE: 141,
|
||||
tecNEED_MASTER_KEY: 142,
|
||||
tecDST_TAG_NEEDED: 143,
|
||||
tecINTERNAL: 144,
|
||||
tecOVERSIZE: 145
|
||||
};
|
||||
|
||||
39
test/ripple-lib/hashprefixes.js
Normal file
39
test/ripple-lib/hashprefixes.js
Normal file
@@ -0,0 +1,39 @@
|
||||
'use strict';
|
||||
|
||||
// TODO: move in helpers from serializedtypes to utils
|
||||
function toBytes(n) {
|
||||
return [n >>> 24, n >>> 16 & 0xff, n >>> 8 & 0xff, n & 0xff];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefix for hashing functions.
|
||||
*
|
||||
* These prefixes are inserted before the source material used to
|
||||
* generate various hashes. This is done to put each hash in its own
|
||||
* "space." This way, two different types of objects with the
|
||||
* same binary data will produce different hashes.
|
||||
*
|
||||
* Each prefix is a 4-byte value with the last byte set to zero
|
||||
* and the first three bytes formed from the ASCII equivalent of
|
||||
* some arbitrary string. For example "TXN".
|
||||
*/
|
||||
|
||||
// transaction plus signature to give transaction ID
|
||||
exports.HASH_TX_ID = 0x54584E00; // 'TXN'
|
||||
// transaction plus metadata
|
||||
exports.HASH_TX_NODE = 0x534E4400; // 'TND'
|
||||
// inner node in tree
|
||||
exports.HASH_INNER_NODE = 0x4D494E00; // 'MIN'
|
||||
// leaf node in tree
|
||||
exports.HASH_LEAF_NODE = 0x4D4C4E00; // 'MLN'
|
||||
// inner transaction to sign
|
||||
exports.HASH_TX_SIGN = 0x53545800; // 'STX'
|
||||
// inner transaction to sign (TESTNET)
|
||||
exports.HASH_TX_SIGN_TESTNET = 0x73747800; // 'stx'
|
||||
// inner transaction to multisign
|
||||
exports.HASH_TX_MULTISIGN = 0x534D5400; // 'SMT'
|
||||
|
||||
Object.keys(exports).forEach(function (k) {
|
||||
exports[k + '_BYTES'] = toBytes(exports[k]);
|
||||
});
|
||||
|
||||
2332
test/ripple-lib/remote.js
Normal file
2332
test/ripple-lib/remote.js
Normal file
File diff suppressed because it is too large
Load Diff
1651
test/ripple-lib/transaction.js
Normal file
1651
test/ripple-lib/transaction.js
Normal file
File diff suppressed because it is too large
Load Diff
740
test/ripple-lib/transactionmanager.js
Normal file
740
test/ripple-lib/transactionmanager.js
Normal file
@@ -0,0 +1,740 @@
|
||||
'use strict';
|
||||
|
||||
var _ = require('lodash');
|
||||
var util = require('util');
|
||||
var assert = require('assert');
|
||||
var async = require('async');
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var Transaction = require('./transaction').Transaction;
|
||||
var RippleError = require('./rippleerror').RippleError;
|
||||
var PendingQueue = require('./transactionqueue').TransactionQueue;
|
||||
var log = require('./log').internal.sub('transactionmanager');
|
||||
|
||||
/**
|
||||
* @constructor TransactionManager
|
||||
* @param {Account} account
|
||||
*/
|
||||
|
||||
function TransactionManager(account) {
|
||||
EventEmitter.call(this);
|
||||
|
||||
var self = this;
|
||||
|
||||
this._account = account;
|
||||
this._accountID = account._account_id;
|
||||
this._remote = account._remote;
|
||||
this._nextSequence = undefined;
|
||||
this._maxFee = this._remote.max_fee;
|
||||
this._maxAttempts = this._remote.max_attempts;
|
||||
this._submissionTimeout = this._remote.submission_timeout;
|
||||
this._pending = new PendingQueue();
|
||||
|
||||
this._account.on('transaction-outbound', function (res) {
|
||||
self._transactionReceived(res);
|
||||
});
|
||||
|
||||
this._remote.on('load_changed', function (load) {
|
||||
self._adjustFees(load);
|
||||
});
|
||||
|
||||
function updatePendingStatus(ledger) {
|
||||
self._updatePendingStatus(ledger);
|
||||
}
|
||||
|
||||
this._remote.on('ledger_closed', updatePendingStatus);
|
||||
|
||||
function handleReconnect() {
|
||||
self._handleReconnect(function () {
|
||||
// Handle reconnect, account_tx procedure first, before
|
||||
// hooking back into ledger_closed
|
||||
self._remote.on('ledger_closed', updatePendingStatus);
|
||||
});
|
||||
}
|
||||
|
||||
this._remote.on('disconnect', function () {
|
||||
self._remote.removeListener('ledger_closed', updatePendingStatus);
|
||||
self._remote.once('connect', handleReconnect);
|
||||
});
|
||||
|
||||
// Query server for next account transaction sequence
|
||||
this._loadSequence();
|
||||
}
|
||||
|
||||
util.inherits(TransactionManager, EventEmitter);
|
||||
|
||||
TransactionManager._isNoOp = function (transaction) {
|
||||
return typeof transaction === 'object' && typeof transaction.tx_json === 'object' && transaction.tx_json.TransactionType === 'AccountSet' && transaction.tx_json.Flags === 0;
|
||||
};
|
||||
|
||||
TransactionManager._isRemoteError = function (error) {
|
||||
return typeof error === 'object' && error.error === 'remoteError' && typeof error.remote === 'object';
|
||||
};
|
||||
|
||||
TransactionManager._isNotFound = function (error) {
|
||||
return TransactionManager._isRemoteError(error) && /^(txnNotFound|transactionNotFound)$/.test(error.remote.error);
|
||||
};
|
||||
|
||||
TransactionManager._isTooBusy = function (error) {
|
||||
return TransactionManager._isRemoteError(error) && error.remote.error === 'tooBusy';
|
||||
};
|
||||
|
||||
/**
|
||||
* Normalize transactions received from account transaction stream and
|
||||
* account_tx
|
||||
*
|
||||
* @param {Transaction}
|
||||
* @return {Transaction} normalized
|
||||
* @api private
|
||||
*/
|
||||
|
||||
TransactionManager.normalizeTransaction = function (tx) {
|
||||
var transaction = {};
|
||||
var keys = Object.keys(tx);
|
||||
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
var k = keys[i];
|
||||
switch (k) {
|
||||
case 'transaction':
|
||||
// Account transaction stream
|
||||
transaction.tx_json = tx[k];
|
||||
break;
|
||||
case 'tx':
|
||||
// account_tx response
|
||||
transaction.engine_result = tx.meta.TransactionResult;
|
||||
transaction.result = transaction.engine_result;
|
||||
transaction.tx_json = tx[k];
|
||||
transaction.hash = tx[k].hash;
|
||||
transaction.ledger_index = tx[k].ledger_index;
|
||||
transaction.type = 'transaction';
|
||||
transaction.validated = tx.validated;
|
||||
break;
|
||||
case 'meta':
|
||||
case 'metadata':
|
||||
transaction.metadata = tx[k];
|
||||
break;
|
||||
case 'mmeta':
|
||||
// Don't copy mmeta
|
||||
break;
|
||||
default:
|
||||
transaction[k] = tx[k];
|
||||
}
|
||||
}
|
||||
|
||||
return transaction;
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle received transaction from two possible sources
|
||||
*
|
||||
* + Account transaction stream (normal operation)
|
||||
* + account_tx (after reconnect)
|
||||
*
|
||||
* @param {Object} transaction
|
||||
* @api private
|
||||
*/
|
||||
|
||||
TransactionManager.prototype._transactionReceived = function (tx) {
|
||||
var transaction = TransactionManager.normalizeTransaction(tx);
|
||||
|
||||
if (!transaction.validated) {
|
||||
// Transaction has not been validated
|
||||
return;
|
||||
}
|
||||
|
||||
if (transaction.tx_json.Account !== this._accountID) {
|
||||
// Received transaction's account does not match
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._remote.trace) {
|
||||
log.info('transaction received:', transaction.tx_json);
|
||||
}
|
||||
|
||||
this._pending.addReceivedSequence(transaction.tx_json.Sequence);
|
||||
|
||||
var hash = transaction.tx_json.hash;
|
||||
var submission = this._pending.getSubmission(hash);
|
||||
|
||||
if (!(submission instanceof Transaction)) {
|
||||
// The received transaction does not correlate to one submitted
|
||||
this._pending.addReceivedId(hash, transaction);
|
||||
return;
|
||||
}
|
||||
|
||||
// ND: A `success` handler will `finalize` this later
|
||||
switch (transaction.engine_result) {
|
||||
case 'tesSUCCESS':
|
||||
submission.emit('success', transaction);
|
||||
break;
|
||||
default:
|
||||
submission.emit('error', transaction);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Adjust pending transactions' fees in real-time. This does not resubmit
|
||||
* pending transactions; they will be resubmitted periodically with an updated
|
||||
* fee (and as a consequence, a new transaction ID) if not already validated
|
||||
*
|
||||
* ND: note, that `Fee` is a component of a transactionID
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
TransactionManager.prototype._adjustFees = function () {
|
||||
var self = this;
|
||||
|
||||
if (!this._remote.local_fee) {
|
||||
return;
|
||||
}
|
||||
|
||||
function maxFeeExceeded(transaction) {
|
||||
// Don't err until attempting to resubmit
|
||||
transaction.once('presubmit', function () {
|
||||
transaction.emit('error', 'tejMaxFeeExceeded');
|
||||
});
|
||||
}
|
||||
|
||||
this._pending.forEach(function (transaction) {
|
||||
if (transaction._setFixedFee) {
|
||||
return;
|
||||
}
|
||||
|
||||
var oldFee = transaction.tx_json.Fee;
|
||||
var newFee = transaction._computeFee();
|
||||
|
||||
if (Number(newFee) > self._maxFee) {
|
||||
// Max transaction fee exceeded, abort submission
|
||||
maxFeeExceeded(transaction);
|
||||
return;
|
||||
}
|
||||
|
||||
transaction.tx_json.Fee = newFee;
|
||||
transaction.emit('fee_adjusted', oldFee, newFee);
|
||||
|
||||
if (self._remote.trace) {
|
||||
log.info('fee adjusted:', transaction.tx_json, oldFee, newFee);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get pending transactions
|
||||
*
|
||||
* @return {Array} pending transactions
|
||||
*/
|
||||
|
||||
TransactionManager.prototype.getPending = function () {
|
||||
return this._pending;
|
||||
};
|
||||
|
||||
/**
|
||||
* Legacy code. Update transaction status after excessive ledgers pass. One of
|
||||
* either "missing" or "lost"
|
||||
*
|
||||
* @param {Object} ledger data
|
||||
* @api private
|
||||
*/
|
||||
|
||||
TransactionManager.prototype._updatePendingStatus = function (ledger) {
|
||||
assert.strictEqual(typeof ledger, 'object');
|
||||
assert.strictEqual(typeof ledger.ledger_index, 'number');
|
||||
|
||||
this._pending.forEach(function (transaction) {
|
||||
if (transaction.finalized) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (ledger.ledger_index - transaction.submitIndex) {
|
||||
case 4:
|
||||
transaction.emit('missing', ledger);
|
||||
break;
|
||||
case 8:
|
||||
transaction.emit('lost', ledger);
|
||||
break;
|
||||
}
|
||||
|
||||
if (ledger.ledger_index > transaction.tx_json.LastLedgerSequence) {
|
||||
// Transaction must fail
|
||||
transaction.emit('error', new RippleError('tejMaxLedger', 'Transaction LastLedgerSequence exceeded'));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Fill an account transaction sequence
|
||||
TransactionManager.prototype._fillSequence = function (tx, callback) {
|
||||
var self = this;
|
||||
|
||||
function submitFill(sequence, fCallback) {
|
||||
var fillTransaction = self._remote.createTransaction('AccountSet', {
|
||||
account: self._accountID
|
||||
});
|
||||
fillTransaction.tx_json.Sequence = sequence;
|
||||
|
||||
// Secrets may be set on a per-transaction basis
|
||||
if (tx._secret) {
|
||||
fillTransaction.secret(tx._secret);
|
||||
}
|
||||
|
||||
fillTransaction.once('submitted', fCallback);
|
||||
fillTransaction.submit();
|
||||
}
|
||||
|
||||
function sequenceLoaded(err, sequence) {
|
||||
if (typeof sequence !== 'number') {
|
||||
log.info('fill sequence: failed to fetch account transaction sequence');
|
||||
return callback();
|
||||
}
|
||||
|
||||
var sequenceDiff = tx.tx_json.Sequence - sequence;
|
||||
var submitted = 0;
|
||||
|
||||
async.whilst(function () {
|
||||
return submitted < sequenceDiff;
|
||||
}, function (asyncCallback) {
|
||||
submitFill(sequence, function (res) {
|
||||
++submitted;
|
||||
if (res.engine_result === 'tesSUCCESS') {
|
||||
self.emit('sequence_filled', err);
|
||||
}
|
||||
asyncCallback();
|
||||
});
|
||||
}, function () {
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this._loadSequence(sequenceLoaded);
|
||||
};
|
||||
|
||||
/**
|
||||
* Load account transaction sequence
|
||||
*
|
||||
* @param [Function] callback
|
||||
* @api private
|
||||
*/
|
||||
|
||||
TransactionManager.prototype._loadSequence = function (callback_) {
|
||||
var self = this;
|
||||
var callback = typeof callback_ === 'function' ? callback_ : function () {};
|
||||
|
||||
function sequenceLoaded(err, sequence) {
|
||||
if (err || typeof sequence !== 'number') {
|
||||
if (self._remote.trace) {
|
||||
log.info('error requesting account transaction sequence', err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self._nextSequence = sequence;
|
||||
self.emit('sequence_loaded', sequence);
|
||||
callback(err, sequence);
|
||||
}
|
||||
|
||||
this._account.getNextSequence(sequenceLoaded);
|
||||
};
|
||||
|
||||
/**
|
||||
* On reconnect, load account_tx in case a pending transaction succeeded while
|
||||
* disconnected
|
||||
*
|
||||
* @param [Function] callback
|
||||
* @api private
|
||||
*/
|
||||
|
||||
TransactionManager.prototype._handleReconnect = function (callback_) {
|
||||
var self = this;
|
||||
var callback = typeof callback_ === 'function' ? callback_ : function () {};
|
||||
|
||||
if (!this._pending.length()) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
function handleTransactions(err, transactions) {
|
||||
if (err || typeof transactions !== 'object') {
|
||||
if (self._remote.trace) {
|
||||
log.info('error requesting account_tx', err);
|
||||
}
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(transactions.transactions)) {
|
||||
// Treat each transaction in account transaction history as received
|
||||
transactions.transactions.forEach(self._transactionReceived, self);
|
||||
}
|
||||
|
||||
callback();
|
||||
|
||||
self._loadSequence(function () {
|
||||
// Resubmit pending transactions after sequence is loaded
|
||||
self._resubmit();
|
||||
});
|
||||
}
|
||||
|
||||
var options = {
|
||||
account: this._accountID,
|
||||
ledger_index_min: this._pending.getMinLedger(),
|
||||
ledger_index_max: -1,
|
||||
binary: true,
|
||||
parseBinary: true,
|
||||
limit: 20
|
||||
};
|
||||
|
||||
this._remote.requestAccountTx(options, handleTransactions);
|
||||
};
|
||||
|
||||
/**
|
||||
* Wait for specified number of ledgers to pass
|
||||
*
|
||||
* @param {Number} ledgers
|
||||
* @param {Function} callback
|
||||
* @api private
|
||||
*/
|
||||
|
||||
TransactionManager.prototype._waitLedgers = function (ledgers, callback) {
|
||||
assert.strictEqual(typeof ledgers, 'number');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (ledgers < 1) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
var self = this;
|
||||
var closes = 0;
|
||||
|
||||
function ledgerClosed() {
|
||||
if (++closes === ledgers) {
|
||||
self._remote.removeListener('ledger_closed', ledgerClosed);
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
this._remote.on('ledger_closed', ledgerClosed);
|
||||
};
|
||||
|
||||
/**
|
||||
* Resubmit pending transactions. If a transaction is specified, it will be
|
||||
* resubmitted. Otherwise, all pending transactions will be resubmitted
|
||||
*
|
||||
* @param [Number] ledgers to wait before resubmitting
|
||||
* @param [Transaction] pending transactions to resubmit
|
||||
* @api private
|
||||
*/
|
||||
|
||||
TransactionManager.prototype._resubmit = function (ledgers_, pending_) {
|
||||
var self = this;
|
||||
|
||||
var ledgers = ledgers_;
|
||||
var pending = pending_;
|
||||
|
||||
if (arguments.length === 1) {
|
||||
pending = ledgers;
|
||||
ledgers = 0;
|
||||
}
|
||||
|
||||
ledgers = ledgers || 0;
|
||||
pending = pending instanceof Transaction ? [pending] : this.getPending().getQueue();
|
||||
|
||||
function resubmitTransaction(transaction, next) {
|
||||
if (!transaction || transaction.finalized) {
|
||||
// Transaction has been finalized, nothing to do
|
||||
return;
|
||||
}
|
||||
|
||||
// Find ID within cache of received (validated) transaction IDs
|
||||
var received = transaction.findId(self._pending._idCache);
|
||||
|
||||
if (received) {
|
||||
switch (received.engine_result) {
|
||||
case 'tesSUCCESS':
|
||||
transaction.emit('success', received);
|
||||
break;
|
||||
default:
|
||||
transaction.emit('error', received);
|
||||
}
|
||||
}
|
||||
|
||||
if (!transaction.isResubmittable()) {
|
||||
// Rather than resubmit, wait for the transaction to fail due to
|
||||
// LastLedgerSequence's being exceeded. The ultimate error emitted on
|
||||
// transaction is 'tejMaxLedger'; should be definitive
|
||||
return;
|
||||
}
|
||||
|
||||
while (self._pending.hasSequence(transaction.tx_json.Sequence)) {
|
||||
// Sequence number has been consumed by another transaction
|
||||
transaction.tx_json.Sequence += 1;
|
||||
|
||||
if (self._remote.trace) {
|
||||
log.info('incrementing sequence:', transaction.tx_json);
|
||||
}
|
||||
}
|
||||
|
||||
if (self._remote.trace) {
|
||||
log.info('resubmit:', transaction.tx_json);
|
||||
}
|
||||
|
||||
transaction.once('submitted', function (m) {
|
||||
transaction.emit('resubmitted', m);
|
||||
next();
|
||||
});
|
||||
|
||||
self._request(transaction);
|
||||
}
|
||||
|
||||
this._waitLedgers(ledgers, function () {
|
||||
async.eachSeries(pending, resubmitTransaction);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Prepare submit request
|
||||
*
|
||||
* @param {Transaction} transaction to submit
|
||||
* @return {Request} submit request
|
||||
* @api private
|
||||
*/
|
||||
|
||||
TransactionManager.prototype._prepareRequest = function (tx) {
|
||||
var submitRequest = this._remote.requestSubmit();
|
||||
|
||||
if (this._remote.local_signing) {
|
||||
tx.sign();
|
||||
|
||||
var serialized = tx.serialize();
|
||||
submitRequest.txBlob(serialized.to_hex());
|
||||
|
||||
var hash = tx.hash(null, null, serialized);
|
||||
tx.addId(hash);
|
||||
} else {
|
||||
if (tx.hasMultiSigners()) {
|
||||
submitRequest.message.command = 'submit_multisigned';
|
||||
}
|
||||
|
||||
// ND: `build_path` is completely ignored when doing local signing as
|
||||
// `Paths` is a component of the signed blob, the `tx_blob` is signed,
|
||||
// sealed and delivered, and the txn unmodified.
|
||||
// TODO: perhaps an exception should be raised if build_path is attempted
|
||||
// while local signing
|
||||
submitRequest.buildPath(tx._build_path);
|
||||
submitRequest.secret(tx._secret);
|
||||
submitRequest.txJson(tx.tx_json);
|
||||
}
|
||||
|
||||
return submitRequest;
|
||||
};
|
||||
|
||||
/**
|
||||
* Send `submit` request, handle response
|
||||
*
|
||||
* @param {Transaction} transaction to submit
|
||||
* @api private
|
||||
*/
|
||||
|
||||
TransactionManager.prototype._request = function (tx) {
|
||||
var self = this;
|
||||
var remote = this._remote;
|
||||
|
||||
if (tx.finalized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tx.attempts > this._maxAttempts) {
|
||||
tx.emit('error', new RippleError('tejAttemptsExceeded'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (tx.attempts > 0 && !remote.local_signing) {
|
||||
var errMessage = 'Automatic resubmission requires local signing';
|
||||
tx.emit('error', new RippleError('tejLocalSigningRequired', errMessage));
|
||||
return;
|
||||
}
|
||||
|
||||
if (Number(tx.tx_json.Fee) > tx._maxFee) {
|
||||
tx.emit('error', new RippleError('tejMaxFeeExceeded'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (remote.trace) {
|
||||
log.info('submit transaction:', tx.tx_json);
|
||||
}
|
||||
|
||||
function transactionFailed(message) {
|
||||
if (message.engine_result === 'tefPAST_SEQ') {
|
||||
// Transaction may succeed after Sequence is updated
|
||||
self._resubmit(1, tx);
|
||||
}
|
||||
}
|
||||
|
||||
function transactionRetry() {
|
||||
// XXX This may no longer be necessary. Instead, update sequence numbers
|
||||
// after a transaction fails definitively
|
||||
self._fillSequence(tx, function () {
|
||||
self._resubmit(1, tx);
|
||||
});
|
||||
}
|
||||
|
||||
function transactionFailedLocal(message) {
|
||||
if (message.engine_result === 'telINSUF_FEE_P') {
|
||||
// Transaction may succeed after Fee is updated
|
||||
self._resubmit(1, tx);
|
||||
}
|
||||
}
|
||||
|
||||
function submissionError(error) {
|
||||
// Either a tem-class error or generic server error such as tooBusy. This
|
||||
// should be a definitive failure
|
||||
if (TransactionManager._isTooBusy(error)) {
|
||||
self._waitLedgers(1, function () {
|
||||
tx.once('submitted', function (m) {
|
||||
tx.emit('resubmitted', m);
|
||||
});
|
||||
self._request(tx);
|
||||
});
|
||||
} else {
|
||||
self._nextSequence--;
|
||||
tx.emit('error', error);
|
||||
}
|
||||
}
|
||||
|
||||
function submitted(message) {
|
||||
if (tx.finalized) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ND: If for some unknown reason our hash wasn't computed correctly this
|
||||
// is an extra measure.
|
||||
if (message.tx_json && message.tx_json.hash) {
|
||||
tx.addId(message.tx_json.hash);
|
||||
}
|
||||
|
||||
message.result = message.engine_result || '';
|
||||
|
||||
tx.result = message;
|
||||
tx.responses += 1;
|
||||
|
||||
if (remote.trace) {
|
||||
log.info('submit response:', message);
|
||||
}
|
||||
|
||||
tx.emit('submitted', message);
|
||||
|
||||
switch (message.result.slice(0, 3)) {
|
||||
case 'tes':
|
||||
tx.emit('proposed', message);
|
||||
break;
|
||||
case 'tec':
|
||||
break;
|
||||
case 'ter':
|
||||
transactionRetry(message);
|
||||
break;
|
||||
case 'tef':
|
||||
transactionFailed(message);
|
||||
break;
|
||||
case 'tel':
|
||||
transactionFailedLocal(message);
|
||||
break;
|
||||
default:
|
||||
// tem
|
||||
submissionError(message);
|
||||
}
|
||||
}
|
||||
|
||||
function requestTimeout() {
|
||||
// ND: What if the response is just slow and we get a response that
|
||||
// `submitted` above will cause to have concurrent resubmit logic streams?
|
||||
// It's simpler to just mute handlers and look out for finalized
|
||||
// `transaction` messages.
|
||||
if (tx.finalized) {
|
||||
return;
|
||||
}
|
||||
|
||||
tx.emit('timeout');
|
||||
|
||||
if (remote.isConnected()) {
|
||||
if (remote.trace) {
|
||||
log.info('timeout:', tx.tx_json);
|
||||
}
|
||||
self._resubmit(1, tx);
|
||||
}
|
||||
}
|
||||
|
||||
tx.submitIndex = this._remote.getLedgerSequence() + 1;
|
||||
|
||||
if (tx.attempts === 0) {
|
||||
tx.initialSubmitIndex = tx.submitIndex;
|
||||
}
|
||||
|
||||
var submitRequest = this._prepareRequest(tx);
|
||||
submitRequest.once('error', submitted);
|
||||
submitRequest.once('success', submitted);
|
||||
|
||||
tx.emit('presubmit');
|
||||
|
||||
submitRequest.broadcast().request();
|
||||
tx.attempts++;
|
||||
|
||||
tx.emit('postsubmit');
|
||||
|
||||
submitRequest.timeout(self._submissionTimeout, requestTimeout);
|
||||
};
|
||||
|
||||
/**
|
||||
* Entry point for TransactionManager submission
|
||||
*
|
||||
* @param {Transaction} tx
|
||||
*/
|
||||
|
||||
TransactionManager.prototype.submit = function (tx) {
|
||||
var self = this;
|
||||
|
||||
if (typeof this._nextSequence !== 'number') {
|
||||
// If sequence number is not yet known, defer until it is.
|
||||
this.once('sequence_loaded', function () {
|
||||
self.submit(tx);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (tx.finalized) {
|
||||
// Finalized transactions must stop all activity
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_.isNumber(tx.tx_json.Sequence)) {
|
||||
// Honor manually-set sequences
|
||||
tx.setSequence(this._nextSequence++);
|
||||
}
|
||||
|
||||
if (_.isUndefined(tx.tx_json.LastLedgerSequence)) {
|
||||
tx.setLastLedgerSequence();
|
||||
}
|
||||
|
||||
if (tx.hasMultiSigners()) {
|
||||
tx.setResubmittable(false);
|
||||
tx.setSigningPubKey('');
|
||||
}
|
||||
|
||||
tx.once('cleanup', function () {
|
||||
self.getPending().remove(tx);
|
||||
});
|
||||
|
||||
if (!tx.complete()) {
|
||||
this._nextSequence -= 1;
|
||||
return;
|
||||
}
|
||||
|
||||
// ND: this is the ONLY place we put the tx into the queue. The
|
||||
// TransactionQueue queue is merely a list, so any mutations to tx._hash
|
||||
// will cause subsequent look ups (eg. inside 'transaction-outbound'
|
||||
// validated transaction clearing) to fail.
|
||||
this._pending.push(tx);
|
||||
this._request(tx);
|
||||
};
|
||||
|
||||
exports.TransactionManager = TransactionManager;
|
||||
|
||||
Reference in New Issue
Block a user