mirror of
https://github.com/Xahau/xahau.js.git
synced 2025-11-20 12:15:51 +00:00
JS: Improvements to transaction result handling for remote.js
This commit is contained in:
committed by
Stefan Thomas
parent
a989f8b599
commit
7a579f3d51
275
js/remote.js
275
js/remote.js
@@ -5,11 +5,13 @@
|
|||||||
//
|
//
|
||||||
// YYY Will later provide a network access which use multiple instances of this.
|
// 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.
|
// 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
|
// Node
|
||||||
var util = require('util');
|
var util = require('util');
|
||||||
var events = require('events');
|
var EventEmitter = require('events').EventEmitter;
|
||||||
|
|
||||||
// npm
|
// npm
|
||||||
var WebSocket = require('ws');
|
var WebSocket = require('ws');
|
||||||
@@ -17,9 +19,9 @@ var WebSocket = require('ws');
|
|||||||
var amount = require('./amount.js');
|
var amount = require('./amount.js');
|
||||||
var Amount = amount.Amount;
|
var Amount = amount.Amount;
|
||||||
|
|
||||||
// Events emmitted:
|
// Request events emmitted:
|
||||||
// 'success'
|
// 'success' : Request successful.
|
||||||
// 'error'
|
// 'error' : Request failed.
|
||||||
// 'remoteError'
|
// 'remoteError'
|
||||||
// 'remoteUnexpected'
|
// 'remoteUnexpected'
|
||||||
// 'remoteDisconnected'
|
// 'remoteDisconnected'
|
||||||
@@ -33,11 +35,11 @@ var Request = function (remote, command) {
|
|||||||
this.on('request', this.request_default);
|
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.
|
// Return this. node EventEmitter's on doesn't return this.
|
||||||
Request.prototype.on = function (e, c) {
|
Request.prototype.on = function (e, c) {
|
||||||
events.EventEmitter.prototype.on.call(this, e, c);
|
EventEmitter.prototype.on.call(this, e, c);
|
||||||
|
|
||||||
return this;
|
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 remoteConfig = function (config, server, trace) {
|
||||||
var serverConfig = config.servers[server];
|
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);
|
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 = {
|
var flags = {
|
||||||
'OfferCreate' : {
|
'OfferCreate' : {
|
||||||
'Passive' : 0x00010000,
|
'Passive' : 0x00010000,
|
||||||
@@ -217,27 +227,28 @@ Remote.prototype.connect_helper = function () {
|
|||||||
else {
|
else {
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case 'response':
|
case 'response':
|
||||||
{
|
{
|
||||||
request = ws.response[message.id];
|
request = ws.response[message.id];
|
||||||
|
|
||||||
if (!request) {
|
if (!request) {
|
||||||
unexpected = true;
|
unexpected = true;
|
||||||
}
|
}
|
||||||
else if ('success' === message.result) {
|
else if ('success' === message.result) {
|
||||||
if (self.trace) console.log("message: %s", json);
|
if (self.trace) console.log("message: %s", json);
|
||||||
|
|
||||||
request.emit('success', message);
|
request.emit('success', message);
|
||||||
}
|
}
|
||||||
else if (message.error) {
|
else if (message.error) {
|
||||||
if (self.trace) console.log("message: %s", json);
|
if (self.trace) console.log("message: %s", json);
|
||||||
|
|
||||||
request.emit('error', {
|
request.emit('error', {
|
||||||
'error' : 'remoteError',
|
'error' : 'remoteError',
|
||||||
'error_message' : 'Remote reported an error.',
|
'error_message' : 'Remote reported an error.',
|
||||||
'remote' : message,
|
'remote' : message,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
break;
|
||||||
|
|
||||||
case 'ledgerClosed':
|
case 'ledgerClosed':
|
||||||
// XXX If not trusted, need to verify we consider ledger closed.
|
// XXX If not trusted, need to verify we consider ledger closed.
|
||||||
@@ -247,8 +258,8 @@ Remote.prototype.connect_helper = function () {
|
|||||||
|
|
||||||
self.ledger_closed = message.ledger_closed;
|
self.ledger_closed = message.ledger_closed;
|
||||||
self.ledger_current_index = message.ledger_closed_index + 1;
|
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;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -319,7 +330,7 @@ Remote.prototype.disconnect = function (done) {
|
|||||||
var self = this;
|
var self = this;
|
||||||
var ws = this.ws;
|
var ws = this.ws;
|
||||||
|
|
||||||
if (self.trace) console.log("remote: disconnect");
|
if (this.trace) console.log("remote: disconnect");
|
||||||
|
|
||||||
ws.onclose = function () {
|
ws.onclose = function () {
|
||||||
if (self.trace) console.log("remote: onclose: %s", ws.readyState);
|
if (self.trace) console.log("remote: onclose: %s", ws.readyState);
|
||||||
@@ -333,8 +344,6 @@ Remote.prototype.disconnect = function (done) {
|
|||||||
// Send a request.
|
// Send a request.
|
||||||
// <-> request: what to send, consumed.
|
// <-> request: what to send, consumed.
|
||||||
Remote.prototype.request = function (request) {
|
Remote.prototype.request = function (request) {
|
||||||
var self = this;
|
|
||||||
|
|
||||||
this.ws.response[request.message.id = this.id] = request;
|
this.ws.response[request.message.id = this.id] = request;
|
||||||
|
|
||||||
this.id += 1; // Advance id.
|
this.id += 1; // Advance id.
|
||||||
@@ -356,7 +365,6 @@ Remote.prototype.request_ledger_current = function () {
|
|||||||
return new Request(this, 'ledger_current');
|
return new Request(this, 'ledger_current');
|
||||||
};
|
};
|
||||||
|
|
||||||
// <-> request:
|
|
||||||
// --> ledger : optional
|
// --> ledger : optional
|
||||||
// --> ledger_index : optional
|
// --> ledger_index : optional
|
||||||
Remote.prototype.request_ledger_entry = function (type) {
|
Remote.prototype.request_ledger_entry = function (type) {
|
||||||
@@ -415,6 +423,19 @@ Remote.prototype.request_ledger_entry = function (type) {
|
|||||||
return request;
|
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.
|
// Submit a transaction.
|
||||||
Remote.prototype.submit = function (transaction) {
|
Remote.prototype.submit = function (transaction) {
|
||||||
var self = this;
|
var self = this;
|
||||||
@@ -479,15 +500,17 @@ Remote.prototype.server_subscribe = function () {
|
|||||||
|
|
||||||
var request = new Request(this, 'server_subscribe');
|
var request = new Request(this, 'server_subscribe');
|
||||||
|
|
||||||
request.on('success', function (message) {
|
request.
|
||||||
self.ledger_current_index = message.ledger_current_index;
|
on('success', function (message) {
|
||||||
self.ledger_closed = message.ledger_closed;
|
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('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.
|
// 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.
|
// Ask the remote to accept the current ledger.
|
||||||
// - To be notified when the ledger is accepted, server_subscribe() then listen to 'ledger_closed' events.
|
// - To be notified when the ledger is accepted, server_subscribe() then listen to 'ledger_closed' events.
|
||||||
Remote.prototype.ledger_accept = function () {
|
Remote.prototype.ledger_accept = function () {
|
||||||
if (this.stand_alone)
|
if (this.stand_alone || undefined === this.stand_alone)
|
||||||
{
|
{
|
||||||
var request = new Request(this, 'ledger_accept');
|
var request = new Request(this, 'ledger_accept');
|
||||||
|
|
||||||
request.request();
|
request.request();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
self.emit('error', {
|
this.emit('error', {
|
||||||
'error' : 'notStandAlone'
|
'error' : 'notStandAlone'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -565,29 +588,139 @@ Remote.prototype.transaction = function () {
|
|||||||
//
|
//
|
||||||
// Transactions
|
// 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.
|
// A class to implement transactions.
|
||||||
// - Collects parameters
|
// - Collects parameters
|
||||||
// - Allow event listeners to be attached to determine the outcome.
|
// - Allow event listeners to be attached to determine the outcome.
|
||||||
var Transaction = function (remote) {
|
var Transaction = function (remote) {
|
||||||
this.prototype = events.EventEmitter; // XXX Node specific.
|
var self = this;
|
||||||
|
|
||||||
|
this.prototype = EventEmitter; // XXX Node specific.
|
||||||
|
|
||||||
this.remote = remote;
|
this.remote = remote;
|
||||||
this.secret = undefined;
|
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.
|
// Return this. node EventEmitter's on doesn't return this.
|
||||||
Transaction.prototype.on = function (e, c) {
|
Transaction.prototype.on = function (e, c) {
|
||||||
events.EventEmitter.prototype.on.call(this, e, c);
|
EventEmitter.prototype.on.call(this, e, c);
|
||||||
|
|
||||||
return this;
|
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.
|
// 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 () {
|
Transaction.prototype.submit = function () {
|
||||||
|
var self = this;
|
||||||
var transaction = this.transaction;
|
var transaction = this.transaction;
|
||||||
|
|
||||||
if (undefined === transaction.Fee) {
|
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);
|
this.remote.submit(this);
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
@@ -628,7 +805,7 @@ Transaction.prototype.flags = function (flags) {
|
|||||||
if (flags) {
|
if (flags) {
|
||||||
var transaction_flags = exports.flags[this.transaction.TransactionType];
|
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;
|
this.transaction.Flags = 0;
|
||||||
|
|
||||||
var flag_set = 'object' === typeof flags ? flags : [ flags ];
|
var flag_set = 'object' === typeof flags ? flags : [ flags ];
|
||||||
@@ -655,12 +832,18 @@ Transaction.prototype.flags = function (flags) {
|
|||||||
//
|
//
|
||||||
// Transactions
|
// Transactions
|
||||||
//
|
//
|
||||||
// remote.transaction() // Build a transaction object.
|
// Construction:
|
||||||
|
// remote.transaction() // Build a transaction object.
|
||||||
// .offer_create(...) // Set major parameters.
|
// .offer_create(...) // Set major parameters.
|
||||||
// .flags() // Set optional parameters.
|
// .flags() // Set optional parameters.
|
||||||
// .on() // Register for events.
|
// .on() // Register for events.
|
||||||
// .submit(); // Send to network.
|
// .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.
|
// Allow config account defaults to be used.
|
||||||
Transaction.prototype.account_default = function (account) {
|
Transaction.prototype.account_default = function (account) {
|
||||||
|
|||||||
Reference in New Issue
Block a user