diff --git a/js/remote.js b/js/remote.js index ffab43ea..75e93297 100644 --- a/js/remote.js +++ b/js/remote.js @@ -12,194 +12,347 @@ var util = require('util'); var WebSocket = require('ws'); // --> trusted: truthy, if remote is trusted -var Remote = function(trusted, websocket_ip, websocket_port, trace) { - this.trusted = trusted; - this.websocket_ip = websocket_ip; - this.websocket_port = websocket_port; - this.id = 0; - this.trace = trace; +var Remote = function (trusted, websocket_ip, websocket_port, trace) { + this.trusted = trusted; + this.websocket_ip = websocket_ip; + this.websocket_port = websocket_port; + this.id = 0; + this.trace = trace; + this.ledger_closed = undefined; + this.ledger_current_index = undefined; + this.stand_alone = undefined; + + // Cache information for accounts. + this.accounts = { + }; + + // Cache for various ledgers. + // XXX Clear when ledger advances. + this.ledgers = { + 'current' : {} + }; }; -var remoteConfig = function(config, server, trace) { - var serverConfig = config.servers[server]; +var remoteConfig = function (config, server, trace) { + var serverConfig = config.servers[server]; - return new Remote(serverConfig.trusted, serverConfig.websocket_ip, serverConfig.websocket_port, trace); + return new Remote(serverConfig.trusted, serverConfig.websocket_ip, serverConfig.websocket_port, trace); }; -Remote.method('connect_helper', function() { - var self = this; +// XXX This needs to be determined from the network. +var fees = { + 'default' : 100, + 'account_create' : 1000, + 'nickname_create' : 1000, + 'offer' : 100, +}; - if (this.trace) - console.log("remote: connect: %s", this.url); +// For accounts we cache things like sequence numbers. +var accounts = { + // Consider sequence numbers stable if you know you're not generating bad transactions. + // Otherwise, clear it to have it automatically refreshed from the network. - this.ws = new WebSocket(this.url); +// acount : { seq : __ } +}; - var ws = this.ws; +Remote.method('connect_helper', function () { + var self = this; - ws.response = {}; + if (this.trace) + console.log("remote: connect: %s", this.url); - ws.onopen = function() { + this.ws = new WebSocket(this.url); + + var ws = this.ws; + + ws.response = {}; + + ws.onopen = function () { + if (this.trace) + console.log("remote: onopen: %s", ws.readyState); + + ws.onclose = undefined; + ws.onerror = undefined; + + self.done(ws.readyState); + }; + + ws.onerror = function () { + if (this.trace) + console.log("remote: onerror: %s", ws.readyState); + + ws.onclose = undefined; + + if (self.expire) { + if (this.trace) + console.log("remote: was expired"); + + self.done(ws.readyState); + } + else + { + // Delay and retry. + setTimeout(function () { if (this.trace) - console.log("remote: onopen: %s", ws.readyState); + console.log("remote: retry"); - ws.onclose = undefined; - ws.onerror = undefined; + self.connect_helper(); + }, 50); // Retry rate 50ms. + } + }; - self.done(ws.readyState); - }; + // Covers failure to open. + ws.onclose = function () { + if (this.trace) + console.log("remote: onclose: %s", ws.readyState); - ws.onerror = function() { - if (this.trace) - console.log("remote: onerror: %s", ws.readyState); + ws.onerror = undefined; - ws.onclose = undefined; + self.done(ws.readyState); + }; - if (self.expire) { - if (this.trace) - console.log("remote: was expired"); + // Node's ws module doesn't pass arguments to onmessage. + ws.on('message', function (json, flags) { + var message = JSON.parse(json); + // console.log("message: %s", json); - self.done(ws.readyState); - } - else - { - // Delay and retry. - setTimeout(function() { - if (this.trace) - console.log("remote: retry"); + if (message.type !== 'response') { + console.log("unexpected message: %s", json); - self.connect_helper(); - }, 50); // Retry rate 50ms. - } - }; + } else { + var done = ws.response[message.id]; - // Covers failure to open. - ws.onclose = function() { - if (this.trace) - console.log("remote: onclose: %s", ws.readyState); + if (done) { + done(message); - ws.onerror = undefined; - - self.done(ws.readyState); - }; - - // Node's ws module doesn't pass arguments to onmessage. - ws.on('message', function(json, flags) { - var message = JSON.parse(json); - // console.log("message: %s", json); - - if (message.type !== 'response') { - console.log("unexpected message: %s", json); - - } else { - var done = ws.response[message.id]; - - if (done) { - done(message); - - } else { - console.log("unexpected message id: %s", json); - } - } - }); + } else { + console.log("unexpected message id: %s", json); + } + } + }); }); // Target state is connectted. // done(readyState): // --> readyState: OPEN, CLOSED -Remote.method('connect', function(done, timeout) { - var self = this; +Remote.method('connect', function (done, timeout) { + var self = this; - this.url = util.format("ws://%s:%s", this.websocket_ip, this.websocket_port); - this.done = done; + this.url = util.format("ws://%s:%s", this.websocket_ip, this.websocket_port); + this.done = done; - if (timeout) { + if (timeout) { + if (this.trace) + console.log("remote: expire: false"); + + this.expire = false; + + setTimeout(function () { if (this.trace) - console.log("remote: expire: false"); + console.log("remote: expire: timeout"); - this.expire = false; + self.expire = true; + }, timeout); + } + else { + if (this.trace) + console.log("remote: expire: false"); - setTimeout(function () { - if (this.trace) - console.log("remote: expire: timeout"); + this.expire = true; + } - self.expire = true; - }, timeout); - } - else { - if (this.trace) - console.log("remote: expire: false"); - - this.expire = true; - } - - this.connect_helper(); + this.connect_helper(); }); // Target stated is disconnected. -Remote.method('disconnect', function(done) { - var ws = this.ws; +Remote.method('disconnect', function (done) { + var ws = this.ws; - ws.onclose = function() { - if (this.trace) - console.log("remote: onclose: %s", ws.readyState); + ws.onclose = function () { + if (this.trace) + console.log("remote: onclose: %s", ws.readyState); - done(ws.readyState); - }; + done(ws.readyState); + }; - ws.close(); + ws.close(); }); // Send a command. The comman should lack the id. // <-> command: what to send, consumed. -Remote.method('request', function(command, done) { - this.id += 1; // Advance id. +Remote.method('request', function (request, onDone, onFailure) { + this.id += 1; // Advance id. - var ws = this.ws; + var ws = this.ws; - command.id = this.id; + request.id = this.id; - ws.response[command.id] = done; + ws.response[request.id] = function (response) { + if (this.trace) + console.log("remote: response: %s", JSON.stringify(response)); - if (this.trace) - console.log("remote: send: %s", JSON.stringify(command)); + if (onFailure && response.error) + { + onFailure(response); + } + else + { + onDone(response); + } + }; - ws.send(JSON.stringify(command)); + if (this.trace) + console.log("remote: request: %s", JSON.stringify(request)); + + ws.send(JSON.stringify(request)); }); -Remote.method('ledger_closed', function(done) { - assert(this.trusted); // If not trusted, need to check proof. +Remote.method('request_ledger_closed', function (onDone, onFailure) { + assert(this.trusted); // If not trusted, need to check proof. - this.request({ 'command' : 'ledger_closed' }, done); + this.request({ 'command' : 'ledger_closed' }, onDone, onFailure); }); // Get the current proposed ledger entry. May be closed (and revised) at any time (even before returning). // Only for use by unit tests. -Remote.method('ledger_current', function(done) { - this.request({ 'command' : 'ledger_current' }, done); +Remote.method('request_ledger_current', function (onDone, onFailure) { + this.request({ 'command' : 'ledger_current' }, onDone, onFailure); }); -// <-> params: -// --> ledger : optional -// --> ledger_index : optional -Remote.method('ledger_entry', function(params, done) { - assert(this.trusted); // If not trusted, need to check proof, maybe talk packet protocol. +// <-> request: +// --> ledger : optional +// --> ledger_index : optional +// --> type +Remote.method('request_ledger_entry', function (req, onDone, onFailure) { + assert(this.trusted); // If not trusted, need to check proof, maybe talk packet protocol. - params.command = 'ledger_entry'; + req.command = 'ledger_entry'; - this.request(params, done); + if (req.ledger_closed) + { + // XXX Initial implementation no caching. + + this.request(req, onDone, onFailure); + } + else if (req.ledger_index) + { + // Current + // XXX Only allow with standalone mode. Must sync response with advance. + var entry; + + switch (req.type) { + case 'account_root': + var cache = this.ledgers.current.account_root; + + if (!cache) + { + cache = this.ledgers.current.account_root = {}; + } + + var entry = this.ledgers.current.account_root[req.account]; + break; + + default: + // This type not cached. + } + + if (entry) + { + onDone(entry); + } + else + { + // Not cached. + + // Submit request + this.request(req, function (r) { + // Got result. + switch (req.type) { + case 'account_root': + this.ledgers.current.account_root.account = r; + break; + + default: + // This type not cached. + } + onDone(r); + }, onFailure); + } + } }); // Submit a json transaction. // done(value) // <-> value: { 'status', status, 'result' : result, ... } // done may be called up to 3 times. -Remote.method('submit', function(json, done) { -// this.request(..., function() { -// }); +Remote.method('submit', function (json, private_key, onDone, onFailure) { + var req = {}; + + req.command = 'submit'; + req.json = json; + + if (private_key && !this.trusted) + { + onFailure({ 'error' : 'untrustedSever', 'request' : req }); + } + else + { + this.request(req, onDone, onFailure); + } }); -exports.Remote = Remote; -exports.remoteConfig = remoteConfig; +// +// Higher level functions. +// -// vim:ts=4 +// Subscribe to a server to get the current and closed ledger. +// XXX Set up routine to update on notification. +Remote.method('server_subscribe', function (onDone, onFailure) { + this.request({ + 'command' : 'server_subscribe' + }, function (r) { + this.ledger_current_index = r.ledger_current_index; + this.ledger_closed = r.ledger_closed; + this.stand_alone = r.stand_alone; + + onDone(); + }, onFailure); +}); + +// Refresh accounts[account].seq +// done(result); +Remote.method('account_seq', function (account, onDone, onFailure) { + var account_root_entry = this.accounts[account]; + + if (account_root_entry && account_root_entry.seq) + { + onDone(account_root_entry.seq); + } + else + { + // Need to get the ledger entry. + this.request_ledger_entry({ + 'ledger' : this.ledger_closed, + 'account_root' : account + }, function (r) { + // Extract the seqence number from the account root entry. + this.accounts[account].seq = r.seq; + + onDone(r.seq); + + }, onFailure); + } +}); + +// A submit that fills in the sequence number. +Remote.method('submit_seq', function (onDone, onFailure) { + +}); + +exports.Remote = Remote; +exports.remoteConfig = remoteConfig; +exports.fees = fees; +exports.accounts = accounts; + +// vim:sw=2:sts=2:ts=8