JS: Improvements to transaction result handling for remote.js

This commit is contained in:
Arthur Britto
2012-10-16 15:49:42 -07:00
committed by Stefan Thomas
parent a989f8b599
commit 7a579f3d51

View File

@@ -5,11 +5,13 @@
//
// YYY Will later provide a network access which use multiple instances of this.
// YYY A better model might be to allow requesting a target state: keep connected or not.
// XXX Make subscribe target state.
// XXX Auto subscribe on connect.
//
// Node
var util = require('util');
var events = require('events');
var EventEmitter = require('events').EventEmitter;
// npm
var WebSocket = require('ws');
@@ -17,9 +19,9 @@ var WebSocket = require('ws');
var amount = require('./amount.js');
var Amount = amount.Amount;
// Events emmitted:
// 'success'
// 'error'
// Request events emmitted:
// 'success' : Request successful.
// 'error' : Request failed.
// 'remoteError'
// 'remoteUnexpected'
// 'remoteDisconnected'
@@ -33,11 +35,11 @@ var Request = function (remote, command) {
this.on('request', this.request_default);
};
Request.prototype = new events.EventEmitter;
Request.prototype = new EventEmitter;
// Return this. node EventEmitter's on doesn't return this.
Request.prototype.on = function (e, c) {
events.EventEmitter.prototype.on.call(this, e, c);
EventEmitter.prototype.on.call(this, e, c);
return this;
};
@@ -120,7 +122,7 @@ var Remote = function (trusted, websocket_ip, websocket_port, config, trace) {
};
};
Remote.prototype = new events.EventEmitter;
Remote.prototype = new EventEmitter;
var remoteConfig = function (config, server, trace) {
var serverConfig = config.servers[server];
@@ -128,6 +130,14 @@ var remoteConfig = function (config, server, trace) {
return new Remote(serverConfig.trusted, serverConfig.websocket_ip, serverConfig.websocket_port, config, trace);
};
var isTemMalformed = function (engine_result_code) {
return (engine_result_code >= -299 && engine_result_code < 199);
};
var isTefFailure = function (engine_result_code) {
return (engine_result_code >= -299 && engine_result_code < 199);
};
var flags = {
'OfferCreate' : {
'Passive' : 0x00010000,
@@ -238,6 +248,7 @@ Remote.prototype.connect_helper = function () {
});
}
}
break;
case 'ledgerClosed':
// XXX If not trusted, need to verify we consider ledger closed.
@@ -248,7 +259,7 @@ Remote.prototype.connect_helper = function () {
self.ledger_closed = message.ledger_closed;
self.ledger_current_index = message.ledger_closed_index + 1;
self.emit('ledger_closed');
self.emit('ledger_closed', self.ledger_closed, self.ledger_closed_index);
break;
default:
@@ -319,7 +330,7 @@ Remote.prototype.disconnect = function (done) {
var self = this;
var ws = this.ws;
if (self.trace) console.log("remote: disconnect");
if (this.trace) console.log("remote: disconnect");
ws.onclose = function () {
if (self.trace) console.log("remote: onclose: %s", ws.readyState);
@@ -333,8 +344,6 @@ Remote.prototype.disconnect = function (done) {
// Send a request.
// <-> request: what to send, consumed.
Remote.prototype.request = function (request) {
var self = this;
this.ws.response[request.message.id = this.id] = request;
this.id += 1; // Advance id.
@@ -356,7 +365,6 @@ Remote.prototype.request_ledger_current = function () {
return new Request(this, 'ledger_current');
};
// <-> request:
// --> ledger : optional
// --> ledger_index : optional
Remote.prototype.request_ledger_entry = function (type) {
@@ -415,6 +423,19 @@ Remote.prototype.request_ledger_entry = function (type) {
return request;
};
// --> ledger_closed : optional
Remote.prototype.request_transaction_entry = function (hash, ledger_closed) {
assert(this.trusted); // If not trusted, need to check proof, maybe talk packet protocol.
var request = new Request(this, 'transaction_entry');
request.message.transaction = hash;
if (ledger_closed)
request.message.ledger_closed = ledger_closed;
return request;
};
// Submit a transaction.
Remote.prototype.submit = function (transaction) {
var self = this;
@@ -479,15 +500,17 @@ Remote.prototype.server_subscribe = function () {
var request = new Request(this, 'server_subscribe');
request.on('success', function (message) {
self.ledger_current_index = message.ledger_current_index;
request.
on('success', function (message) {
self.ledger_closed = message.ledger_closed;
self.stand_alone = message.stand_alone;
self.ledger_current_index = message.ledger_current_index;
self.stand_alone = !!message.stand_alone;
self.emit('subscribed');
self.emit('ledger_closed');
});
self.emit('ledger_closed', self.ledger_closed, self.ledger_current_index-1);
})
.request();
// XXX Could give error events, maybe even time out.
@@ -497,14 +520,14 @@ Remote.prototype.server_subscribe = function () {
// Ask the remote to accept the current ledger.
// - To be notified when the ledger is accepted, server_subscribe() then listen to 'ledger_closed' events.
Remote.prototype.ledger_accept = function () {
if (this.stand_alone)
if (this.stand_alone || undefined === this.stand_alone)
{
var request = new Request(this, 'ledger_accept');
request.request();
}
else {
self.emit('error', {
this.emit('error', {
'error' : 'notStandAlone'
});
}
@@ -565,29 +588,139 @@ Remote.prototype.transaction = function () {
//
// Transactions
//
// Transaction events:
// 'success' : Transaction submitted without error.
// 'error' : Error submitting transaction.
// 'proposed: Advisory proposed status transaction.
// - A client should expect 0 to multiple results.
// - Might not get back. The remote might just forward the transaction.
// - A success could be reverted in final.
// - local error: other remotes might like it.
// - malformed error: local server thought it was malformed.
// - The client should only trust this when talking to a trusted server.
// 'final' : Final status of transaction.
// - Only expect a final from honest clients after a tesSUCCESS or ter*.
// 'state' : Follow the state of a transaction.
// 'clientSubmitted' - Sent to remote
// |- 'remoteError' - Remote rejected transaction.
// \- 'clientProposed' - Remote provisionally accepted transaction.
// |- 'clientMissing' - Transaction has not appeared in ledger as expected.
// | |- 'clientLost' - No longer monitoring missing transaction.
// |/
// |- 'tesSUCCESS' - Transaction in ledger as expected.
// |- 'ter...' - Transaction failed.
// |- 'tep...' - Transaction partially succeeded.
//
// Notes:
// - All transactions including locally errors and malformed errors may be
// forwarded.
// - A malicous server can:
// - give any proposed result.
// - it may declare something correct as incorrect or something correct as incorrect.
// - it may not communicate with the rest of the network.
// - may or may not forward.
//
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) {
this.prototype = events.EventEmitter; // XXX Node specific.
var self = this;
this.prototype = EventEmitter; // XXX Node specific.
this.remote = remote;
this.secret = undefined;
this.transaction = {}; // Transaction data.
this.transaction = { // Transaction data.
'Flags' : 0, // XXX Would be nice if server did not require this.
};
this.hash = undefined;
this.submit_index = undefined; // ledger_current_index was this when transaction was submited.
this.state = undefined; // Under construction.
this.on('success', function (message) {
if (message.engine_result) {
self.hash = message.transaction.hash;
self.set_state('clientProposed');
self.emit('proposed', {
'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');
});
};
Transaction.prototype = new events.EventEmitter;
Transaction.prototype = new EventEmitter;
// Return this. node EventEmitter's on doesn't return this.
Transaction.prototype.on = function (e, c) {
events.EventEmitter.prototype.on.call(this, e, c);
EventEmitter.prototype.on.call(this, e, c);
return this;
};
Transaction.prototype.consts = {
'telLOCAL_ERROR' : -399,
'temMALFORMED' : -299,
'tefFAILURE' : -199,
'terRETRY' : -99,
'tesSUCCESS' : 0,
'tepPARTIAL' : 100,
};
Transaction.prototype.isTelLocal = function (ter) {
return ter >= this.consts.telLOCAL_ERROR && ter < this.consts.temMALFORMED;
};
Transaction.prototype.isTemMalformed = function (ter) {
return ter >= this.consts.temMALFORMED && ter < this.consts.tefFAILURE;
};
Transaction.prototype.isTefFailure = function (ter) {
return ter >= this.consts.tefFAILURE && ter < this.consts.terRETRY;
};
Transaction.prototype.isTerRetry = function (ter) {
return ter >= this.consts.terRETRY && ter < this.consts.tesSUCCESS;
};
Transaction.prototype.isTepSuccess = function (ter) {
return ter >= this.consts.tesSUCCESS;
};
Transaction.prototype.isTepPartial = function (ter) {
return ter >= this.consts.tepPATH_PARTIAL;
};
Transaction.prototype.isRejected = function (ter) {
return this.isTelLocal(ter) || this.isTemMalformed(ter) || this.isTefFailure(ter);
};
Transaction.prototype.set_state = function (state) {
if (this.state !== state) {
this.state = state;
this.emit('state', state);
}
};
// Submit a transaction to the network.
// XXX Don't allow a submit without knowing ledger_closed_index.
// XXX Have a network canSubmit(), post events for following.
// XXX Also give broader status for tracking through network disconnects.
Transaction.prototype.submit = function () {
var self = this;
var transaction = this.transaction;
if (undefined === transaction.Fee) {
@@ -601,6 +734,50 @@ Transaction.prototype.submit = function () {
}
}
if (this.listeners('final').length) {
// There are listeners for 'final' arrange to emit it.
this.submit_index = this.remote.ledger_current_index;
var on_ledger_closed = function (ledger_closed, ledger_closed_index) {
var stop = false;
// XXX make sure self.hash is available.
self.remote.request_transaction_entry(self.hash, ledger_closed)
.on('success', function (message) {
// XXX Fake results for now.
if (!message.metadata.result)
message.metadata.result = 'tesSUCCESS';
self.set_state(message.metadata.result); // XXX Untested.
self.emit('final', message);
})
.on('error', function (message) {
if ('remoteError' === message.error
&& 'transactionNotFound' === message.remote.error) {
if (self.submit_index + SUBMIT_LOST < ledger_closed_index) {
self.set_state('clientLost'); // Gave up.
stop = true;
}
else if (self.submit_index + SUBMIT_MISSING < ledger_closed_index) {
self.set_state('clientMissing'); // We don't know what happened to transaction, still might find.
}
}
// XXX Could log other unexpectedness.
})
.request();
if (stop) {
self.removeListener('ledger_closed', on_ledger_closed);
self.emit('final', message);
}
};
this.remote.on('ledger_closed', on_ledger_closed);
}
this.set_state('clientSubmitted');
this.remote.submit(this);
return this;
@@ -628,7 +805,7 @@ Transaction.prototype.flags = function (flags) {
if (flags) {
var transaction_flags = exports.flags[this.transaction.TransactionType];
if (undefined == this.transaction.Flags)
if (undefined == this.transaction.Flags) // We plan to not define this field on new Transaction.
this.transaction.Flags = 0;
var flag_set = 'object' === typeof flags ? flags : [ flags ];
@@ -655,12 +832,18 @@ Transaction.prototype.flags = function (flags) {
//
// Transactions
//
// Construction:
// remote.transaction() // Build a transaction object.
// .offer_create(...) // Set major parameters.
// .flags() // Set optional parameters.
// .on() // Register for events.
// .submit(); // Send to network.
//
// Events:
// 'success' // Transaction was successfully submitted: hash, proposed TER
// 'error' // Error submitting transaction.
// 'closed' // Result from closed ledger: TER
//
// Allow config account defaults to be used.
Transaction.prototype.account_default = function (account) {