From a1bef6248a959763d7b351dc0104fd6685ac7c37 Mon Sep 17 00:00:00 2001 From: wltsmrz Date: Sat, 27 Jul 2013 05:57:30 +0900 Subject: [PATCH] Accommodate new transaction manager --- src/js/ripple/account.js | 73 +++--- src/js/ripple/transaction.js | 438 +++++++++++------------------------ 2 files changed, 176 insertions(+), 335 deletions(-) diff --git a/src/js/ripple/account.js b/src/js/ripple/account.js index 603ad7d1..f92296f4 100644 --- a/src/js/ripple/account.js +++ b/src/js/ripple/account.js @@ -11,43 +11,45 @@ // var network = require("./network.js"); -var EventEmitter = require('events').EventEmitter; -var util = require('util'); +var EventEmitter = require('events').EventEmitter; +var util = require('util'); +var extend = require('extend'); -var Amount = require('./amount').Amount; -var UInt160 = require('./uint160').UInt160; +var Amount = require('./amount').Amount; +var UInt160 = require('./uint160').UInt160; +var TransactionManager = require('./transactionmanager').TransactionManager; -var extend = require('extend'); -var Account = function (remote, account) { +function Account(remote, account) { EventEmitter.call(this); + var self = this; - this._remote = remote; - this._account = UInt160.from_json(account); + this._tx_manager = null; + this._remote = remote; + this._account = UInt160.from_json(account); this._account_id = this._account.to_json(); - this._subs = 0; + this._subs = 0; // Ledger entry object // Important: This must never be overwritten, only extend()-ed - this._entry = {}; + this._entry = { }; this.on('newListener', function (type, listener) { - if (Account.subscribe_events.indexOf(type) !== -1) { - if (!self._subs && 'open' === self._remote._online_state) { + if (~Account.subscribe_events.indexOf(type)) { + if (!self._subs && self._remote._connected) { self._remote.request_subscribe() .accounts(self._account_id) .request(); } - self._subs += 1; + self._subs += 1; } }); this.on('removeListener', function (type, listener) { - if (Account.subscribe_events.indexOf(type) !== -1) { - self._subs -= 1; - - if (!self._subs && 'open' === self._remote._online_state) { + if (~Account.subscribe_events.indexOf(type)) { + self._subs -= 1; + if (!self._subs && self._remote._connected) { self._remote.request_unsubscribe() .accounts(self._account_id) .request(); @@ -83,8 +85,7 @@ util.inherits(Account, EventEmitter); */ Account.subscribe_events = ['transaction', 'entry']; -Account.prototype.to_json = function () -{ +Account.prototype.to_json = function () { return this._account.to_json(); }; @@ -93,8 +94,7 @@ Account.prototype.to_json = function () * * Note: This does not tell you whether the account exists in the ledger. */ -Account.prototype.is_valid = function () -{ +Account.prototype.is_valid = function () { return this._account.is_valid(); }; @@ -106,18 +106,17 @@ Account.prototype.is_valid = function () * * @param {function (err, entry)} callback Called with the result */ -Account.prototype.entry = function (callback) -{ +Account.prototype.entry = function (callback) { var self = this; + var callback = typeof callback === 'function' + ? callback + : function(){}; self._remote.request_account_info(this._account_id) .on('success', function (e) { extend(self._entry, e.account_data); self.emit('entry', self._entry); - - if ("function" === typeof callback) { - callback(null, e); - } + callback(null, e); }) .on('error', function (e) { callback(e); @@ -127,14 +126,23 @@ Account.prototype.entry = function (callback) return this; }; +Account.prototype.get_next_sequence = function(callback) { + this._remote.request_account_info(this._account_id, function(err, entry) { + if (err) { + callback(err); + } else { + callback(null, entry.account_data.Sequence); + } + }); +}; + /** * Notify object of a relevant transaction. * * This is only meant to be called by the Remote class. You should never have to * call this yourself. */ -Account.prototype.notifyTx = function (message) -{ +Account.prototype.notifyTx = function (message) { // Only trigger the event if the account object is actually // subscribed - this prevents some weird phantom events from // occurring. @@ -143,6 +151,13 @@ Account.prototype.notifyTx = function (message) } }; +Account.prototype.submit = function(tx) { + if (!this._tx_manager) { + this._tx_manager = new TransactionManager(this); + } + this._tx_manager.submit(tx); +}; + exports.Account = Account; // vim:sw=2:sts=2:ts=8:et diff --git a/src/js/ripple/transaction.js b/src/js/ripple/transaction.js index 8cabe68e..43687493 100644 --- a/src/js/ripple/transaction.js +++ b/src/js/ripple/transaction.js @@ -1,4 +1,3 @@ -// // Transactions // // Construction: @@ -56,83 +55,63 @@ var SerializedObject = require('./serializedobject').SerializedObject; var config = require('./config'); -var SUBMIT_MISSING = 4; // Report missing. -var SUBMIT_LOST = 8; // Give up tracking. - // A class to implement transactions. // - Collects parameters // - Allow event listeners to be attached to determine the outcome. -var Transaction = function (remote) { +function Transaction(remote) { EventEmitter.call(this); - // YYY Make private as many variables as possible. var self = this; - this.callback = undefined; - this.remote = remote; - this._secret = undefined; - this._build_path = false; + this.remote = remote; + this._secret = void(0); + this._build_path = false; // Transaction data. - this.tx_json = { - 'Flags' : 0, // XXX Would be nice if server did not require this. - }; + this.tx_json = { Flags: 0 }; - this.hash = undefined; - this.submit_index = undefined; // ledger_current_index was this when transaction was submited. - this.state = undefined; // Under construction. - this.finalized = false; + this.hash = void(0); - this.on('success', function (message) { - if (message.engine_result) { - self.hash = message.tx_json.hash; + // ledger_current_index was this when transaction was submited. + this.submit_index = void(0); - self.set_state('client_proposed'); + // Under construction. + this.state = void(0); - self.emit('proposed', { - 'tx_json' : message.tx_json, - 'result' : message.engine_result, - 'result_code' : message.engine_result_code, - 'result_message' : message.engine_result_message, - 'rejected' : self.isRejected(message.engine_result_code), // If server is honest, don't expect a final if rejected. - }); - } - }); - - this.on('error', function (message) { - // Might want to give more detailed information. - self.set_state('remoteError'); - }); + this.finalized = false; + this._previous_signing_hash = void(0); }; util.inherits(Transaction, EventEmitter); // XXX This needs to be determined from the network. Transaction.fees = { - 'default' : 10, + default : Amount.from_json('10'), + nickname_create : Amount.from_json('1000'), + offer : Amount.from_json('10'), }; Transaction.flags = { - 'AccountSet' : { - 'RequireDestTag' : 0x00010000, - 'OptionalDestTag' : 0x00020000, - 'RequireAuth' : 0x00040000, - 'OptionalAuth' : 0x00080000, - 'DisallowXRP' : 0x00100000, - 'AllowXRP' : 0x00200000, + AccountSet : { + RequireDestTag : 0x00010000, + OptionalDestTag : 0x00020000, + RequireAuth : 0x00040000, + OptionalAuth : 0x00080000, + DisallowXRP : 0x00100000, + AllowXRP : 0x00200000, }, - 'OfferCreate' : { - 'Passive' : 0x00010000, - 'ImmediateOrCancel' : 0x00020000, - 'FillOrKill' : 0x00040000, - 'Sell' : 0x00080000, + OfferCreate : { + Passive : 0x00010000, + ImmediateOrCancel : 0x00020000, + FillOrKill : 0x00040000, + Sell : 0x00080000, }, - 'Payment' : { - 'NoRippleDirect' : 0x00010000, - 'PartialPayment' : 0x00020000, - 'LimitQuality' : 0x00040000, + Payment : { + NoRippleDirect : 0x00010000, + PartialPayment : 0x00020000, + LimitQuality : 0x00040000, }, }; @@ -185,6 +164,15 @@ Transaction.prototype.set_state = function (state) { } }; +/** + * TODO + * Actually do this right + */ + +Transaction.prototype.get_fee = function() { + return Transaction.fees['default'].to_json(); +}; + /** * Attempts to complete the transaction for submission. * @@ -195,11 +183,11 @@ Transaction.prototype.set_state = function (state) { Transaction.prototype.complete = function () { var tx_json = this.tx_json; - if ("undefined" === typeof tx_json.Fee && this.remote.local_fee) { - this.tx_json.Fee = "" + Math.ceil(this.remote.fee_tx() * this.fee_units()); + if (tx_json.Fee === void(0) && this.remote.local_fee) { + tx_json.Fee = Transaction.fees['default'].to_json(); } - if ("undefined" === typeof tx_json.SigningPubKey && (!this.remote || this.remote.local_signing)) { + if (tx_json.SigningPubKey === void(0) && (!this.remote || this.remote.local_signing)) { var seed = Seed.from_json(this._secret); var key = seed.get_key(this.tx_json.Account); tx_json.SigningPubKey = key.to_hex_pub(); @@ -223,6 +211,11 @@ Transaction.prototype.signing_hash = function () { Transaction.prototype.sign = function () { var seed = Seed.from_json(this._secret); var hash = this.signing_hash(); + + if (this.tx_json.TxnSignature && hash === this._previous_signing_hash) { + return; + } + var key = seed.get_key(this.tx_json.Account); var sig = key.sign(hash, 0); var hex = sjcl.codec.hex.fromBits(sig).toUpperCase(); @@ -236,206 +229,6 @@ Transaction.prototype._hasTransactionListeners = function() { || this.listeners('pending').length }; -// Submit a transaction to the network. -// XXX Don't allow a submit without knowing ledger_index. -// XXX Have a network canSubmit(), post events for following. -// XXX Also give broader status for tracking through network disconnects. -// callback = function (status, info) { -// // status is final status. Only works under a ledger_accepting conditions. -// switch status: -// case 'tesSUCCESS': all is well. -// case 'tejSecretUnknown': unable to sign transaction - secret unknown -// case 'tejServerUntrusted': sending secret to untrusted server. -// case 'tejInvalidAccount': locally detected error. -// case 'tejLost': locally gave up looking -// default: some other TER -// } - -Transaction.prototype.submit = function (callback) { - var self = this; - var tx_json = this.tx_json; - - this.callback = typeof callback === 'function' - ? callback - : function(){}; - - function finish(err) { - self.emit('error', err); - self.callback('error', err); - } - - if (typeof tx_json.Account !== 'string') { - finish({ - 'error' : 'tejInvalidAccount', - 'error_message' : 'Bad account.' - }); - return this; - } - - // YYY Might check paths for invalid accounts. - - this.complete(); - - //console.log('Callback or has listeners'); - - // There are listeners for callback, 'final', 'lost', or 'pending' arrange to emit them. - - this.submit_index = this.remote._ledger_current_index; - - // When a ledger closes, look for the result. - function on_ledger_closed(message) { - if (self.finalized) return; - - var ledger_hash = message.ledger_hash; - var ledger_index = message.ledger_index; - var stop = false; - - // XXX make sure self.hash is available. - var transaction_entry = self.remote.request_transaction_entry(self.hash) - - transaction_entry.ledger_hash(ledger_hash) - - transaction_entry.on('success', function (message) { - if (self.finalized) return; - self.set_state(message.metadata.TransactionResult); - self.remote.removeListener('ledger_closed', on_ledger_closed); - self.emit('final', message); - self.finalized = true; - self.callback(message.metadata.TransactionResult, message); - }); - - transaction_entry.on('error', function (message) { - if (self.finalized) return; - - if (message.error === 'remoteError' && message.remote.error === 'transactionNotFound') { - if (self.submit_index + SUBMIT_LOST < ledger_index) { - self.set_state('client_lost'); // Gave up. - self.emit('lost'); - self.callback('tejLost', message); - self.remote.removeListener('ledger_closed', on_ledger_closed); - self.emit('final', message); - self.finalized = true; - } else if (self.submit_index + SUBMIT_MISSING < ledger_index) { - self.set_state('client_missing'); // We don't know what happened to transaction, still might find. - self.emit('pending'); - } else { - self.emit('pending'); - } - } - // XXX Could log other unexpectedness. - }); - - transaction_entry.request(); - }; - - this.remote.on('ledger_closed', on_ledger_closed); - - this.once('error', function (message) { - self.callback(message.error, message); - }); - - this.set_state('client_submitted'); - - if (self.remote.local_sequence && !self.tx_json.Sequence) { - - self.tx_json.Sequence = this.remote.account_seq(self.tx_json.Account, 'ADVANCE'); - // console.log("Sequence: %s", self.tx_json.Sequence); - - if (!self.tx_json.Sequence) { - //console.log('NO SEQUENCE'); - - // Look in the last closed ledger. - var account_seq = this.remote.account_seq_cache(self.tx_json.Account, false) - - account_seq.on('success_account_seq_cache', function () { - // Try again. - self.submit(); - }) - - account_seq.on('error_account_seq_cache', function (message) { - // XXX Maybe be smarter about this. Don't want to trust an untrusted server for this seq number. - // Look in the current ledger. - self.remote.account_seq_cache(self.tx_json.Account, 'CURRENT') - .on('success_account_seq_cache', function () { - // Try again. - self.submit(); - }) - .on('error_account_seq_cache', function (message) { - // Forward errors. - self.emit('error', message); - }) - .request(); - }) - - account_seq.request(); - - return this; - } - - // If the transaction fails we want to either undo incrementing the sequence - // or submit a noop transaction to consume the sequence remotely. - this.once('success', function (res) { - if (typeof res.engine_result === 'string') { - switch (res.engine_result.slice(0, 3)) { - // Synchronous local error - case 'tej': - self.remote.account_seq(self.tx_json.Account, 'REWIND'); - break; - - case 'ter': - // XXX: What do we do in case of ter? - break; - - case 'tel': - case 'tem': - case 'tef': - // XXX Once we have a transaction submission manager class, we can - // check if there are any other transactions pending. If there are, - // we should submit a dummy transaction to ensure those - // transactions are still valid. - //var noop = self.remote.transaction().account_set(self.tx_json.Account); - //noop.submit(); - - // XXX Hotfix. This only works if no other transactions are pending. - self.remote.account_seq(self.tx_json.Account, 'REWIND'); - break; - } - } - }); - } - - // Prepare request - var request = this.remote.request_submit(); - - // Forward events - request.emit = this.emit.bind(this); - - if (!this._secret && !this.tx_json.Signature) { - finish({ - 'result' : 'tejSecretUnknown', - 'result_message' : "Could not sign transactions because we." - }); - return this; - } else if (this.remote.local_signing) { - this.sign(); - request.tx_blob(this.serialize().to_hex()); - } else { - if (!this.remote.trusted) { - finish({ - 'result' : 'tejServerUntrusted', - 'result_message' : "Attempt to give a secret to an untrusted server." - }); - } - - request.secret(this._secret); - request.build_path(this._build_path); - request.tx_json(this.tx_json); - } - - request.request(); - - return this; -} // // Set options for Transactions @@ -453,7 +246,7 @@ Transaction.prototype.build_path = function (build) { // tag should be undefined or a 32 bit integer. // YYY Add range checking for tag. Transaction.prototype.destination_tag = function (tag) { - if (tag !== undefined) { + if (tag !== void(0)) { this.tx_json.DestinationTag = tag; } @@ -463,9 +256,9 @@ Transaction.prototype.destination_tag = function (tag) { Transaction._path_rewrite = function (path) { var path_new = []; - for (var i = 0, l = path.length; i < l; i++) { - var node = path[i]; - var node_new = {}; + for (var i=0, l=path.length; i paths: undefined or array of path // A path is an array of objects containing some combination of: account, currency, issuer Transaction.prototype.paths = function (paths) { - for (var i = 0, l = paths.length; i < l; i++) { + for (var i=0, l=paths.length; i expiration : if not undefined, Date or Number // --> cancel_sequence : if not undefined, Sequence Transaction.prototype.offer_create = function (src, taker_pays, taker_gets, expiration, cancel_sequence) { - this._secret = this._account_secret(src); - this.tx_json.TransactionType = 'OfferCreate'; - this.tx_json.Account = UInt160.json_rewrite(src); - this.tx_json.TakerPays = Amount.json_rewrite(taker_pays); - this.tx_json.TakerGets = Amount.json_rewrite(taker_gets); + this._secret = this._account_secret(src); + this.tx_json.TransactionType = 'OfferCreate'; + this.tx_json.Account = UInt160.json_rewrite(src); + this.tx_json.TakerPays = Amount.json_rewrite(taker_pays); + this.tx_json.TakerGets = Amount.json_rewrite(taker_gets); + + if (this.remote.local_fee) { + this.tx_json.Fee = Transaction.fees.offer.to_json(); + } if (expiration) { this.tx_json.Expiration = expiration instanceof Date @@ -630,20 +427,20 @@ Transaction.prototype.offer_create = function (src, taker_pays, taker_gets, expi }; Transaction.prototype.password_fund = function (src, dst) { - this._secret = this._account_secret(src); - this.tx_json.TransactionType = 'PasswordFund'; - this.tx_json.Destination = UInt160.json_rewrite(dst); + this._secret = this._account_secret(src); + this.tx_json.TransactionType = 'PasswordFund'; + this.tx_json.Destination = UInt160.json_rewrite(dst); return this; } Transaction.prototype.password_set = function (src, authorized_key, generator, public_key, signature) { - this._secret = this._account_secret(src); - this.tx_json.TransactionType = 'PasswordSet'; - this.tx_json.RegularKey = authorized_key; - this.tx_json.Generator = generator; - this.tx_json.PublicKey = public_key; - this.tx_json.Signature = signature; + this._secret = this._account_secret(src); + this.tx_json.TransactionType = 'PasswordSet'; + this.tx_json.RegularKey = authorized_key; + this.tx_json.Generator = generator; + this.tx_json.PublicKey = public_key; + this.tx_json.Signature = signature; return this; } @@ -666,19 +463,19 @@ Transaction.prototype.password_set = function (src, authorized_key, generator, p // .set_flags() // .source_tag() Transaction.prototype.payment = function (src, dst, deliver_amount) { - this._secret = this._account_secret(src); - this.tx_json.TransactionType = 'Payment'; - this.tx_json.Account = UInt160.json_rewrite(src); - this.tx_json.Amount = Amount.json_rewrite(deliver_amount); - this.tx_json.Destination = UInt160.json_rewrite(dst); + this._secret = this._account_secret(src); + this.tx_json.TransactionType = 'Payment'; + this.tx_json.Account = UInt160.json_rewrite(src); + this.tx_json.Amount = Amount.json_rewrite(deliver_amount); + this.tx_json.Destination = UInt160.json_rewrite(dst); return this; } Transaction.prototype.ripple_line_set = function (src, limit, quality_in, quality_out) { - this._secret = this._account_secret(src); - this.tx_json.TransactionType = 'TrustSet'; - this.tx_json.Account = UInt160.json_rewrite(src); + this._secret = this._account_secret(src); + this.tx_json.TransactionType = 'TrustSet'; + this.tx_json.Account = UInt160.json_rewrite(src); // Allow limit of 0 through. if (limit !== undefined) @@ -706,20 +503,49 @@ Transaction.prototype.wallet_add = function (src, amount, authorized_key, public return this; }; -/** - * Returns the number of fee units this transaction will cost. - * - * Each Ripple transaction based on its type and makeup costs a certain number - * of fee units. The fee units are calculated on a per-server basis based on the - * current load on both the network and the server. - * - * @see https://ripple.com/wiki/Transaction_Fee - */ -Transaction.prototype.fee_units = function () -{ - return Transaction.fees["default"]; -}; +// Submit a transaction to the network. +// XXX Don't allow a submit without knowing ledger_index. +// XXX Have a network canSubmit(), post events for following. +// XXX Also give broader status for tracking through network disconnects. +// callback = function (status, info) { +// // status is final status. Only works under a ledger_accepting conditions. +// switch status: +// case 'tesSUCCESS': all is well. +// case 'tejSecretUnknown': unable to sign transaction - secret unknown +// case 'tejServerUntrusted': sending secret to untrusted server. +// case 'tejInvalidAccount': locally detected error. +// case 'tejLost': locally gave up looking +// default: some other TER +// } -exports.Transaction = Transaction; +Transaction.prototype.submit = function (callback) { + var self = this; + + this.callback = (typeof callback === 'function') ? callback : function(){}; + + this.once('error', function transaction_error(error, message) { + self.callback(error, message); + }); + + this.once('success', function transaction_success(message) { + self.callback(null, message); + }); + + var account = this.tx_json.Account; + + if (typeof account !== 'string') { + this.emit('error', { + error: 'tejInvalidAccount', + error_message: 'Account is unspecified' + }); + } else { + // YYY Might check paths for invalid accounts. + this.remote.get_account(account).submit(this); + } + + return this; +} + +exports.Transaction = Transaction; // vim:sw=2:sts=2:ts=8:et