diff --git a/.gitignore b/.gitignore index 33d99e09..870069a7 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,6 @@ test/config.js /src-cov /coverage.html /coverage + +# Ignore IntelliJ files +.idea \ No newline at end of file diff --git a/src/js/ripple/amount.js b/src/js/ripple/amount.js index 9775c3d6..93cec17b 100644 --- a/src/js/ripple/amount.js +++ b/src/js/ripple/amount.js @@ -593,8 +593,23 @@ Amount.prototype.invert = function() { * 25.2 XRP => 25200000/XRP * USD 100.40 => 100.4/USD/? * 100 => 100000000/XRP + * + * + * The regular expression below matches above cases, broken down for better understanding: + * + * ^\s* // start with any amount of whitespace + * ([a-z]{3})? // optional any 3 letters + * \s* // any amount of whitespace + * (-)? // optional dash + * (\d+) // 1 or more digits + * (\.(\d*))? // optional . character with any amount of digits + * \s* // any amount of whitespace + * ([a-f0-9]{40}|[a-z0-9]{3})? // optional 40 character hex string OR 3 letters + * \s* // any amount of whitespace + * $ // end of string + * */ -Amount.human_RE = /^\s*([a-z]{3})?\s*(-)?(\d+)(?:\.(\d*))?\s*([a-f0-9]{40}|[a-z0-9]{3})?\s*$/i; +Amount.human_RE = /^\s*([a-z]{3})?\s*(-)?(\d+)(\.(\d*))?\s*([a-f0-9]{40}|[a-z0-9]{3})?\s*$/i; Amount.prototype.parse_human = function(j, opts) { opts = opts || {}; diff --git a/src/js/ripple/authinfo.js b/src/js/ripple/authinfo.js index 0f01495d..77ed7ca1 100644 --- a/src/js/ripple/authinfo.js +++ b/src/js/ripple/authinfo.js @@ -1,36 +1,60 @@ -var RippleTxt = require('./rippletxt').RippleTxt; -var request = require('superagent'); +var async = require('async'); +var superagent = require('superagent'); +var RippleTxt = require('./rippletxt').RippleTxt; -function AuthInfo () { +function AuthInfo() { this.rippleTxt = new RippleTxt(); -} +}; + +AuthInfo.prototype._getRippleTxt = function(domain, callback) { + this.rippleTxt.get(domain, callback); +}; + +AuthInfo.prototype._getUser = function(url, callback) { + superagent.get(url, callback); +}; + /** * Get auth info for a given username + * * @param {string} domain - Domain which hosts the user's info * @param {string} username - Username who's info we are retreiving * @param {function} fn - Callback function */ -AuthInfo.prototype.get = function (domain, username, fn) { + +AuthInfo.prototype.get = function(domain, username, callback) { var self = this; - - self.rippleTxt.get(domain, function(err, txt){ - if (err) return fn(err); - - processTxt(txt); - }); - - - function processTxt(txt) { - if (!txt.authinfo_url) return fn(new Error("Authentication is not supported on "+domain)); - var url = Array.isArray(txt.authinfo_url) ? txt.authinfo_url[0] : txt.authinfo_url; - url += "?domain="+domain+"&username="+username; - - request.get(url, function(err, resp){ - if (err || resp.error) return fn(new Error("Authentication info server unreachable")); - fn(null, resp.body); - }); - } + + function getRippleTxt(callback) { + self._getRippleTxt(domain, function(err, txt) { + if (err) { + return callback(err); + } + + if (!txt.authinfo_url) { + return callback(new Error('Authentication is not supported on ' + domain)); + } + + var url = Array.isArray(txt.authinfo_url) ? txt.authinfo_url[0] : txt.authinfo_url; + + url += '?domain=' + domain + '&username=' + username; + + callback(null, url); + }); + }; + + function getUser(url, callback) { + self._getUser(url, function(err, res) { + if (err || res.error) { + callback(new Error('Authentication info server unreachable')); + } else { + callback(null, res.body); + } + }); + }; + + async.waterfall([ getRippleTxt, getUser ], callback); }; -module.exports.AuthInfo = AuthInfo; \ No newline at end of file +exports.AuthInfo = AuthInfo; diff --git a/src/js/ripple/blob.js b/src/js/ripple/blob.js index db9428fe..7d429dbe 100644 --- a/src/js/ripple/blob.js +++ b/src/js/ripple/blob.js @@ -1,45 +1,44 @@ var crypt = require('./crypt').Crypt; var request = require('superagent'); -var async = require('async'); var extend = require("extend"); var BlobClient = {}; //Blob object class -var BlobObj = function (url, id, key) { - this.url = url; - this.id = id; - this.key = key; - this.data = {}; +function BlobObj(url, id, key) { + this.url = url; + this.id = id; + this.key = key; this.identity = new Identity(this); + this.data = { }; }; // Blob operations // Do NOT change the mapping of existing ops BlobObj.ops = { // Special - "noop" : 0, + noop: 0, // Simple ops - "set" : 16, - "unset" : 17, - "extend" : 18, + set: 16, + unset: 17, + extend: 18, // Meta ops - "push" : 32, - "pop" : 33, - "shift" : 34, - "unshift" : 35, - "filter" : 36 + push: 32, + pop: 33, + shift: 34, + unshift: 35, + filter: 36 }; -BlobObj.opsReverseMap = []; -for (var name in BlobObj.ops) { +BlobObj.opsReverseMap = [ ]; + +for (var name in BlobObj.ops) { BlobObj.opsReverseMap[BlobObj.ops[name]] = name; } - //Identity fields var identityRoot = 'identityVault'; var identityFields = [ @@ -83,147 +82,159 @@ var idTypeFields = [ 'other' ]; -/* +/** * Initialize a new blob object + * * @param {function} fn - Callback function */ -BlobObj.prototype.init = function (fn) { + +BlobObj.prototype.init = function(fn) { var self = this, url; - if (self.url.indexOf("://") === -1) self.url = "http://" + url; - + + if (self.url.indexOf('://') === -1) { + self.url = 'http://' + url; + } + url = self.url + '/v1/blob/' + self.id; - - request.get(url, function(err, resp){ - if (err || !resp.body || resp.body.result !== 'success') - return fn(new Error("Could not retrieve blob")); - - self.revision = resp.body.revision; - self.encrypted_secret = resp.body.encrypted_secret; - - if (!self.decrypt(resp.body.blob)) { - return fn(new Error("Error while decrypting blob")); + request.get(url, function(err, resp) { + if (err || !resp.body || resp.body.result !== 'success') { + return fn(new Error('Could not retrieve blob')); + } + + self.revision = resp.body.revision; + self.encrypted_secret = resp.body.encrypted_secret; + + if (!self.decrypt(resp.body.blob)) { + return fn(new Error('Error while decrypting blob')); + } + + //Apply patches + if (resp.body.patches && resp.body.patches.length) { + var successful = true; + resp.body.patches.forEach(function(patch) { + successful = successful && self.applyEncryptedPatch(patch); + }); + + if (successful) { + self.consolidate(); } - - //Apply patches - if (resp.body.patches && resp.body.patches.length) { - var successful = true; - resp.body.patches.forEach(function (patch) { - successful = successful && self.applyEncryptedPatch(patch); - }); + } - if (successful) self.consolidate(); - } - - fn(null, self);//return with newly decrypted blob - - }).timeout(8000); + //return with newly decrypted blob + fn(null, self); + }).timeout(8000); }; - -/* +/** * Consolidate - * Consolidate patches as a new revision + * * @param {function} fn - Callback function */ -BlobObj.prototype.consolidate = function (fn) { - - // Callback is optional - if ("function" !== typeof fn) fn = function(){}; - console.log("client: blob: consolidation at revision", this.revision); +BlobObj.prototype.consolidate = function(fn) { + // Callback is optional + if (typeof fn !== 'function') { + fn = function(){}; + } + + //console.log('client: blob: consolidation at revision', this.revision); var encrypted = this.encrypt(); var config = { - method : 'POST', - url : this.url + '/v1/blob/consolidate', - dataType : 'json', - data : { - blob_id : this.id, - data : encrypted, - revision : this.revision + method: 'POST', + url: this.url + '/v1/blob/consolidate', + dataType: 'json', + data: { + blob_id: this.id, + data: encrypted, + revision: this.revision }, }; - + var signed = crypt.signRequestHmac(config, this.data.auth_secret, this.id); - + request.post(signed.url) .send(signed.data) .end(function(err, resp) { - - // XXX Add better error information to exception - if (err) return fn(new Error("Failed to consolidate blob - XHR error")); - else if (resp.body && resp.body.result === 'success') return fn(null, resp.body); - else return fn(new Error("Failed to consolidate blob")); + // XXX Add better error information to exception + if (err) { + fn(new Error('Failed to consolidate blob - XHR error')); + } else if (resp.body && resp.body.result === 'success') { + fn(null, resp.body); + } else { + fn(new Error('Failed to consolidate blob')); + } }); }; - - -/* + +/** * ApplyEncryptedPatch - * save changes from a downloaded patch to the blob + * * @param {string} patch - encrypted patch string */ -BlobObj.prototype.applyEncryptedPatch = function (patch) -{ - try { - var params = JSON.parse(crypt.decrypt(this.key, patch)); - var op = params.shift(); - var path = params.shift(); - this.applyUpdate(op, path, params); +BlobObj.prototype.applyEncryptedPatch = function(patch) { + try { + var args = JSON.parse(crypt.decrypt(this.key, patch)); + var op = args.shift(); + var path = args.shift(); + + this.applyUpdate(op, path, args); this.revision++; return true; - } catch (err) { - console.log("client: blob: failed to apply patch:", err.toString()); - console.log(err.stack); + //console.log('client: blob: failed to apply patch:', err.toString()); + //console.log(err.stack); return false; } }; - /** * Encrypt secret with unlock key + * * @param {string} secretUnlockkey */ -BlobObj.prototype.encryptSecret = function (secretUnlockKey, secret) { + +BlobObj.prototype.encryptSecret = function(secretUnlockKey, secret) { return crypt.encrypt(secretUnlockKey, secret); }; /** * Decrypt secret with unlock key + * * @param {string} secretUnlockkey */ -BlobObj.prototype.decryptSecret = function (secretUnlockKey) { + +BlobObj.prototype.decryptSecret = function(secretUnlockKey) { return crypt.decrypt(secretUnlockKey, this.encrypted_secret); }; - /** - * Decrypt blob with crypt key + * Decrypt blob with crypt key + * * @param {string} data - encrypted blob data */ -BlobObj.prototype.decrypt = function (data) { - + +BlobObj.prototype.decrypt = function(data) { try { this.data = JSON.parse(crypt.decrypt(this.key, data)); return this; } catch (e) { - console.log("client: blob: decryption failed", e.toString()); - console.log(e.stack); + //console.log('client: blob: decryption failed', e.toString()); + //console.log(e.stack); return false; } }; - /** - * Encrypt blob with crypt key + * Encrypt blob with crypt key */ -BlobObj.prototype.encrypt = function() -{ - + +BlobObj.prototype.encrypt = function() { // Filter Angular metadata before encryption // if ('object' === typeof this.data && // 'object' === typeof this.data.contacts) @@ -232,68 +243,67 @@ BlobObj.prototype.encrypt = function() return crypt.encrypt(this.key, JSON.stringify(this.data)); }; - /** - * Encrypt recovery key + * Encrypt recovery key + * * @param {string} secret * @param {string} blobDecryptKey */ -BlobObj.prototype.encryptBlobCrypt = function (secret, blobDecryptKey) { + +BlobObj.prototype.encryptBlobCrypt = function(secret, blobDecryptKey) { var recoveryEncryptionKey = crypt.deriveRecoveryEncryptionKeyFromSecret(secret); return crypt.encrypt(recoveryEncryptionKey, blobDecryptKey); }; /** * Decrypt recovery key + * * @param {string} secret */ -BlobObj.prototype.decryptBlobCrypt = function (secret) { + +BlobObj.prototype.decryptBlobCrypt = function(secret) { var recoveryEncryptionKey = crypt.deriveRecoveryEncryptionKeyFromSecret(secret); return crypt.decrypt(recoveryEncryptionKey, this.encrypted_blobdecrypt_key); }; - - - /**** Blob updating functions ****/ - /** * Set blob element */ -BlobObj.prototype.set = function (pointer, value, fn) { + +BlobObj.prototype.set = function(pointer, value, fn) { this.applyUpdate('set', pointer, [value]); this.postUpdate('set', pointer, [value], fn); }; - /** * Remove blob element */ -BlobObj.prototype.unset = function (pointer, fn) { + +BlobObj.prototype.unset = function(pointer, fn) { this.applyUpdate('unset', pointer, []); this.postUpdate('unset', pointer, [], fn); }; - /** * Extend blob object */ -BlobObj.prototype.extend = function (pointer, value, fn) { + +BlobObj.prototype.extend = function(pointer, value, fn) { this.applyUpdate('extend', pointer, [value]); this.postUpdate('extend', pointer, [value], fn); }; - /** * Prepend blob array */ -BlobObj.prototype.unshift = function (pointer, value, fn) { + +BlobObj.prototype.unshift = function(pointer, value, fn) { this.applyUpdate('unshift', pointer, [value]); this.postUpdate('unshift', pointer, [value], fn); }; - /** * Filter the row(s) from an array. * @@ -302,49 +312,50 @@ BlobObj.prototype.unshift = function (pointer, value, fn) { * * The subcommands can be any commands with the pointer parameter left out. */ -BlobObj.prototype.filter = function (pointer, field, value, subcommands, callback) { - var params = Array.prototype.slice.apply(arguments); - if ("function" === typeof params[params.length-1]) { - callback = params.pop(); + +BlobObj.prototype.filter = function(pointer, field, value, subcommands, callback) { + var args = Array.prototype.slice.apply(arguments); + + if (typeof args[args.length - 1] === 'function') { + callback = args.pop(); } - params.shift(); + + args.shift(); // Normalize subcommands to minimize the patch size - params = params.slice(0, 2).concat(normalizeSubcommands(params.slice(2), true)); - - this.applyUpdate('filter', pointer, params); - this.postUpdate('filter', pointer, params, callback); -}; + args = args.slice(0, 2).concat(normalizeSubcommands(args.slice(2), true)); + this.applyUpdate('filter', pointer, args); + this.postUpdate('filter', pointer, args, callback); +}; /** * Apply udpdate to the blob */ -BlobObj.prototype.applyUpdate = function (op, path, params) { - + +BlobObj.prototype.applyUpdate = function(op, path, params) { // Exchange from numeric op code to string - if ("number" === typeof op) { + if (typeof op === 'number') { op = BlobObj.opsReverseMap[op]; } - if ("string" !== typeof op) { - throw new Error("Blob update op code must be a number or a valid op id string"); + + if (typeof op !== 'string') { + throw new Error('Blob update op code must be a number or a valid op id string'); } - // Separate each step in the "pointer" - var pointer = path.split("/"); - + // Separate each step in the 'pointer' + var pointer = path.split('/'); var first = pointer.shift(); - if (first !== "") { - throw new Error("Invalid JSON pointer: "+path); + + if (first !== '') { + throw new Error('Invalid JSON pointer: '+path); } this._traverse(this.data, pointer, path, op, params); }; - //for applyUpdate function -BlobObj.prototype._traverse = function (context, pointer, - originalPointer, op, params) { +BlobObj.prototype._traverse = function(context, pointer, originalPointer, op, params) { var _this = this; var part = _this.unescapeToken(pointer.shift()); @@ -352,16 +363,15 @@ BlobObj.prototype._traverse = function (context, pointer, if (part === '-') { part = context.length; } else if (part % 1 !== 0 && part >= 0) { - throw new Error("Invalid pointer, array element segments must be " + - "a positive integer, zero or '-'"); + throw new Error('Invalid pointer, array element segments must be a positive integer, zero or '-''); } - } else if ("object" !== typeof context) { + } else if (typeof context !== 'object') { return null; } else if (!context.hasOwnProperty(part)) { // Some opcodes create the path as they're going along - if (op === "set") { + if (op === 'set') { context[part] = {}; - } else if (op === "unshift") { + } else if (op === 'unshift') { context[part] = []; } else { return null; @@ -369,139 +379,133 @@ BlobObj.prototype._traverse = function (context, pointer, } if (pointer.length !== 0) { - return this._traverse(context[part], pointer, - originalPointer, op, params); + return this._traverse(context[part], pointer, originalPointer, op, params); } switch (op) { - case "set": - context[part] = params[0]; - break; - case "unset": - if (Array.isArray(context)) { - context.splice(part, 1); - } else { - delete context[part]; - } - break; - case "extend": - if ("object" !== typeof context[part]) { - throw new Error("Tried to extend a non-object"); - } - extend(true, context[part], params[0]); - break; - case "unshift": - if ("undefined" === typeof context[part]) { - context[part] = []; - } else if (!Array.isArray(context[part])) { - throw new Error("Operator 'unshift' must be applied to an array."); - } - context[part].unshift(params[0]); - break; - case "filter": - if (Array.isArray(context[part])) { - context[part].forEach(function (element, i) { - if ("object" === typeof element && - element.hasOwnProperty(params[0]) && - element[params[0]] === params[1]) { - var subpointer = originalPointer+"/"+i; - var subcommands = normalizeSubcommands(params.slice(2)); + case 'set': + context[part] = params[0]; + break; + case 'unset': + if (Array.isArray(context)) { + context.splice(part, 1); + } else { + delete context[part]; + } + break; + case 'extend': + if (typeof context[part] !== 'object') { + throw new Error('Tried to extend a non-object'); + } + extend(true, context[part], params[0]); + break; + case 'unshift': + if (typeof context[part] === 'undefined') { + context[part] = [ ]; + } else if (!Array.isArray(context[part])) { + throw new Error('Operator "unshift" must be applied to an array.'); + } + context[part].unshift(params[0]); + break; + case 'filter': + if (Array.isArray(context[part])) { + context[part].forEach(function(element, i) { + if (typeof element === 'object' && element.hasOwnProperty(params[0]) && element[params[0]] === params[1]) { + var subpointer = originalPointer + '/' + i; + var subcommands = normalizeSubcommands(params.slice(2)); - subcommands.forEach(function (subcommand) { - var op = subcommand[0]; - var pointer = subpointer+subcommand[1]; - _this.applyUpdate(op, pointer, subcommand.slice(2)); - }); - } - }); - } - break; - default: - throw new Error("Unsupported op "+op); + subcommands.forEach(function(subcommand) { + var op = subcommand[0]; + var pointer = subpointer + subcommand[1]; + _this.applyUpdate(op, pointer, subcommand.slice(2)); + }); + } + }); + } + break; + default: + throw new Error('Unsupported op '+op); } }; - -BlobObj.prototype.escapeToken = function (token) { - return token.replace(/[~\/]/g, function (key) { return key === "~" ? "~0" : "~1"; }); +BlobObj.prototype.escapeToken = function(token) { + return token.replace(/[~\/]/g, function(key) { + return key === '~' ? '~0' : '~1'; + }); }; - BlobObj.prototype.unescapeToken = function(str) { return str.replace(/~./g, function(m) { switch (m) { - case "~0": - return "~"; - case "~1": - return "/"; + case '~0': + return '~'; + case '~1': + return '/'; } - throw("Invalid tilde escape: " + m); + throw new Error('Invalid tilde escape: ' + m); }); }; - /** * Sumbit update to blob vault */ -BlobObj.prototype.postUpdate = function (op, pointer, params, fn) { - // Callback is optional - if ("function" !== typeof fn) fn = function(){}; - if ("string" === typeof op) { +BlobObj.prototype.postUpdate = function(op, pointer, params, fn) { + // Callback is optional + if (typeof fn !== 'function') { + fn = function(){}; + } + + if (typeof op === 'string') { op = BlobObj.ops[op]; } - if ("number" !== typeof op) { - throw new Error("Blob update op code must be a number or a valid op id string"); - } - if (op < 0 || op > 255) { - throw new Error("Blob update op code out of bounds"); + + if (typeof op !== 'number') { + throw new Error('Blob update op code must be a number or a valid op id string'); } - console.log("client: blob: submitting update", BlobObj.opsReverseMap[op], pointer, params); + if (op < 0 || op > 255) { + throw new Error('Blob update op code out of bounds'); + } + + //console.log('client: blob: submitting update', BlobObj.opsReverseMap[op], pointer, params); params.unshift(pointer); params.unshift(op); var config = { - method : 'POST', - url : this.url + '/v1/blob/patch', - dataType : 'json', - data : { - blob_id : this.id, - patch : crypt.encrypt(this.key, JSON.stringify(params)) + method: 'POST', + url: this.url + '/v1/blob/patch', + dataType: 'json', + data: { + blob_id: this.id, + patch: crypt.encrypt(this.key, JSON.stringify(params)) } }; - var signed = crypt.signRequestHmac(config, this.data.auth_secret, this.id); request.post(signed.url) - .send(signed.data) - .end(function(err, resp) { - if (err) - return fn(new Error("Patch could not be saved - XHR error")); - else if (!resp.body || resp.body.result !== 'success') - return fn(new Error("Patch could not be saved - bad result")); - - return fn(null, resp.body); + .send(signed.data) + .end(function(err, resp) { + if (err) { + fn(new Error('Patch could not be saved - XHR error')); + } else if (!resp.body || resp.body.result !== 'success') { + fn(new Error('Patch could not be saved - bad result')); + } else { + fn(null, resp.body); + } }); }; - - /***** helper functions *****/ - function normalizeSubcommands(subcommands, compress) { // Normalize parameter structure - if ("number" === typeof subcommands[0] || - "string" === typeof subcommands[0]) { + if (/(number|string)/.test(typeof subcommands[0])) { // Case 1: Single subcommand inline subcommands = [subcommands]; - } else if (subcommands.length === 1 && - Array.isArray(subcommands[0]) && - ("number" === typeof subcommands[0][0] || - "string" === typeof subcommands[0][0])) { + } else if (subcommands.length === 1 && Array.isArray(subcommands[0]) && /(number|string)/.test(typeof subcommands[0][0])) { // Case 2: Single subcommand as array // (nothing to do) } else if (Array.isArray(subcommands[0])) { @@ -510,16 +514,19 @@ function normalizeSubcommands(subcommands, compress) { } // Normalize op name and convert strings to numeric codes - subcommands = subcommands.map(function (subcommand) { - if ("string" === typeof subcommand[0]) { + subcommands = subcommands.map(function(subcommand) { + if (typeof subcommand[0] === 'string') { subcommand[0] = BlobObj.ops[subcommand[0]]; } - if ("number" !== typeof subcommand[0]) { - throw new Error("Invalid op in subcommand"); + + if (typeof subcommand[0] !== 'number') { + throw new Error('Invalid op in subcommand'); } - if ("string" !== typeof subcommand[1]) { - throw new Error("Invalid path in subcommand"); + + if (typeof subcommand[1] !== 'string') { + throw new Error('Invalid path in subcommand'); } + return subcommand; }); @@ -542,6 +549,7 @@ function normalizeSubcommands(subcommands, compress) { * Identity class * */ + var Identity = function (blob) { var self = this; self.blob = blob; @@ -559,12 +567,12 @@ var Identity = function (blob) { }; }; - /** * getFullAddress * returns the address formed into a text string * @param {string} key - Encryption key */ + Identity.prototype.getFullAddress = function (key) { if (!this.blob || !this.blob.data || @@ -592,6 +600,7 @@ Identity.prototype.getFullAddress = function (key) { * @param {string} key - Encryption key * @param {function} fn - Callback function */ + Identity.prototype.getAll = function (key) { if (!this.blob || !this.blob.data || !this.blob.data[identityRoot]) { @@ -606,13 +615,13 @@ Identity.prototype.getAll = function (key) { return result; }; - /** * get * get and decrypt a single identity field * @param {string} pointer - Field to retrieve * @param {string} key - Encryption key */ + Identity.prototype.get = function (pointer, key) { if (!this.blob || !this.blob.data || !this.blob.data[identityRoot]) { return null; @@ -651,7 +660,6 @@ Identity.prototype.get = function (pointer, key) { } }; - /** * set * set and encrypt a single identity field. @@ -660,6 +668,7 @@ Identity.prototype.get = function (pointer, key) { * @param {string} value - Unencrypted data * @param {function} fn - Callback function */ + Identity.prototype.set = function (pointer, key, value, fn) { var self = this; @@ -732,7 +741,6 @@ Identity.prototype.set = function (pointer, key, value, fn) { } }; - /** * unset * remove a single identity field - will only be removed @@ -741,6 +749,7 @@ Identity.prototype.set = function (pointer, key, value, fn) { * @param {string} key - Encryption key * @param {function} fn - Callback function */ + Identity.prototype.unset = function (pointer, key, fn) { //NOTE: this is rather useless since you can overwrite @@ -753,52 +762,62 @@ Identity.prototype.unset = function (pointer, key, fn) { this.blob.unset("/" + identityRoot+"/" + pointer, fn); }; - - /***** blob client methods ****/ /** * Blob object class */ -BlobClient.Blob = BlobObj; + +exports.Blob = BlobObj; /** - * Get ripple name for a given address + * Get ripple name for a given address */ -module.exports.getRippleName = function (url, address, fn) { - - if (!crypt.isValidAddress(address)) return fn (new Error("Invalid ripple address")); + +exports.getRippleName = function(url, address, fn) { + if (!crypt.isValidAddress(address)) { + return fn (new Error('Invalid ripple address')); + } + request.get(url + '/v1/user/' + address, function(err, resp){ - if (err) return fn(new Error("Unable to access vault sever")); - else if (resp.body && resp.body.username) return fn(null, resp.body.username); - else if (resp.body && resp.body.exists === false) return fn (new Error("No ripple name for this address")); - else return fn(new Error("Unable to determine if ripple name exists")); - }); -}; + if (err) { + fn(new Error('Unable to access vault sever')); + } else if (resp.body && resp.body.username) { + fn(null, resp.body.username); + } else if (resp.body && resp.body.exists === false) { + fn (new Error('No ripple name for this address')); + } else { + fn(new Error('Unable to determine if ripple name exists')); + } + }); +}; - -/* - * Retrive a blob with url, id and key +/** + * Retrive a blob with url, id and key */ + BlobClient.get = function (url, id, crypt, fn) { var blob = new BlobObj(url, id, crypt); blob.init(fn); }; - -/* +/** * Verify email address */ -BlobClient.verify = function (url, username, token, fn) { + +BlobClient.verify = function(url, username, token, fn) { url += '/v1/user/' + username + '/verify/' + token; request.get(url, function(err, resp){ - if (err) return fn(err); - else if (resp.body && resp.body.result === 'success') return fn(null, data); - else return fn(new Error("Failed to verify the account")); - }); + if (err) { + fn(err); + } else if (resp.body && resp.body.result === 'success') { + fn(null, data); + } else { + fn(new Error('Failed to verify the account')); + } + }); }; - /** * Create a blob object * @@ -812,52 +831,57 @@ BlobClient.verify = function (url, username, token, fn) { * @param {object} options.oldUserBlob * @param {function} fn */ -BlobClient.create = function (options, fn) { - - var blob = new BlobObj(options.url, options.id, options.crypt); - + +BlobClient.create = function(options, fn) { + var blob = new BlobObj(options.url, options.id, options.crypt); + blob.revision = 0; - blob.data = { - auth_secret : crypt.createSecret(8), - account_id : crypt.getAddress(options.masterkey), - email : options.email, - contacts : [], - created : (new Date()).toJSON() + + blob.data = { + auth_secret: crypt.createSecret(8), + account_id: crypt.getAddress(options.masterkey), + email: options.email, + contacts: [], + created: (new Date()).toJSON() }; - + blob.encrypted_secret = blob.encryptSecret(options.unlock, options.masterkey); // Migration if (options.oldUserBlob) { blob.data.contacts = options.oldUserBlob.data.contacts; } - + //post to the blob vault to create var config = { - method : "POST", - url : options.url + '/v1/user', - data : { - blob_id : options.id, - username : options.username, - address : blob.data.account_id, - auth_secret : blob.data.auth_secret, - data : blob.encrypt(), - email : options.email, - hostlink : options.activateLink, - encrypted_blobdecrypt_key : blob.encryptBlobCrypt(options.masterkey, options.crypt), - encrypted_secret : blob.encrypted_secret + method: 'POST', + url: options.url + '/v1/user', + data: { + blob_id: options.id, + username: options.username, + address: blob.data.account_id, + auth_secret: blob.data.auth_secret, + data: blob.encrypt(), + email: options.email, + hostlink: options.activateLink, + encrypted_blobdecrypt_key: blob.encryptBlobCrypt(options.masterkey, options.crypt), + encrypted_secret: blob.encrypted_secret } }; - + var signed = crypt.signRequestAsymmetric(config, options.masterkey, blob.data.account_id, options.id); - + request.post(signed) .send(signed.data) .end(function(err, resp) { - if (err) return fn(err); - else if (resp.body && resp.body.result === 'success') return fn(null, blob,resp.body); - else return fn(new Error("Could not create blob")); - }); + if (err) { + fn(err); + } else if (resp.body && resp.body.result === 'success') { + fn(null, blob,resp.body); + } else { + fn(new Error('Could not create blob')); + } + }); }; -module.exports.BlobClient = BlobClient; \ No newline at end of file +exports.BlobClient = BlobClient; diff --git a/src/js/ripple/crypt.js b/src/js/ripple/crypt.js index de4eb844..0bdd6bbf 100644 --- a/src/js/ripple/crypt.js +++ b/src/js/ripple/crypt.js @@ -1,34 +1,35 @@ -var sjcl = require('./utils').sjcl; -var base = require('./base').Base; -var UInt160 = require('./uint160').UInt160; -var message = require('./message'); -var request = require('superagent'); -var extend = require("extend"); -var parser = require("url"); -var Crypt = {}; - +var sjcl = require('./utils').sjcl; +var base = require('./base').Base; +var UInt160 = require('./uint160').UInt160; +var message = require('./message'); +var request = require('superagent'); +var querystring = require('querystring'); +var extend = require("extend"); +var parser = require("url"); +var Crypt = { }; + var cryptConfig = { - cipher : "aes", - mode : "ccm", + cipher : 'aes', + mode : 'ccm', ts : 64, // tag length ks : 256, // key size iter : 1000 // iterations (key derivation) }; - /** * Full domain hash based on SHA512 - */ -function fdh(data, bytelen) -{ + */ + +function fdh(data, bytelen) { var bitlen = bytelen << 3; - if (typeof data === "string") { + if (typeof data === 'string') { data = sjcl.codec.utf8String.toBits(data); } // Add hashing rounds until we exceed desired length in bits var counter = 0, output = []; + while (sjcl.bitArray.bitLength(output) < bitlen) { var hash = sjcl.hash.sha512.hash(sjcl.bitArray.concat([counter], data)); output = sjcl.bitArray.concat(output, hash); @@ -39,106 +40,111 @@ function fdh(data, bytelen) output = sjcl.bitArray.clamp(output, bitlen); return output; -} +}; /** * This is a function to derive different hashes from the same key. * Each hash is derived as HMAC-SHA512HALF(key, token). + * * @param {string} key * @param {string} hash - */ + */ + function keyHash(key, token) { var hmac = new sjcl.misc.hmac(key, sjcl.hash.sha512); return sjcl.codec.hex.fromBits(sjcl.bitArray.bitSlice(hmac.encrypt(token), 0, 256)); -} - - +}; /****** exposed functions ******/ - /** * KEY DERIVATION FUNCTION * * This service takes care of the key derivation, i.e. converting low-entropy * secret into higher entropy secret via either computationally expensive * processes or peer-assisted key derivation (PAKDF). + * * @param {object} opts * @param {string} purpose - Key type/purpose - * @param {string} username + * @param {string} username * @param {string} secret - Also known as passphrase/password * @param {function} fn */ -Crypt.derive = function (opts, purpose, username, secret, fn) { +Crypt.derive = function(opts, purpose, username, secret, fn) { var tokens; - if (purpose=='login') tokens = ['id', 'crypt']; - else tokens = ['unlock']; - var iExponent = new sjcl.bn(String(opts.exponent)), - iModulus = new sjcl.bn(String(opts.modulus)), - iAlpha = new sjcl.bn(String(opts.alpha)); + if (purpose === 'login') { + tokens = ['id', 'crypt']; + } else { + tokens = ['unlock']; + } - var publicInfo = "PAKDF_1_0_0:"+opts.host.length+":"+opts.host+ - ":"+username.length+":"+username+ - ":"+purpose.length+":"+purpose+ - ":", - publicSize = Math.ceil(Math.min((7+iModulus.bitLength()) >>> 3, 256)/8), - publicHash = fdh(publicInfo, publicSize), - publicHex = sjcl.codec.hex.fromBits(publicHash), - iPublic = new sjcl.bn(String(publicHex)).setBitM(0), - secretInfo = publicInfo+":"+secret.length+":"+secret+":", - secretSize = (7+iModulus.bitLength()) >>> 3, - secretHash = fdh(secretInfo, secretSize), - secretHex = sjcl.codec.hex.fromBits(secretHash), - iSecret = new sjcl.bn(String(secretHex)).mod(iModulus); + var iExponent = new sjcl.bn(String(opts.exponent)); + var iModulus = new sjcl.bn(String(opts.modulus)); + var iAlpha = new sjcl.bn(String(opts.alpha)); + + var publicInfo = [ 'PAKDF_1_0_0', opts.host.length, opts.host, username.length, username, purpose.length, purpose ].join(':') + ':'; + var publicSize = Math.ceil(Math.min((7 + iModulus.bitLength()) >>> 3, 256) / 8); + var publicHash = fdh(publicInfo, publicSize); + var publicHex = sjcl.codec.hex.fromBits(publicHash); + var iPublic = new sjcl.bn(String(publicHex)).setBitM(0); + var secretInfo = [ publicInfo, secret.length, secret ].join(':') + ':'; + var secretSize = (7 + iModulus.bitLength()) >>> 3; + var secretHash = fdh(secretInfo, secretSize); + var secretHex = sjcl.codec.hex.fromBits(secretHash); + var iSecret = new sjcl.bn(String(secretHex)).mod(iModulus); if (iSecret.jacobi(iModulus) !== 1) { iSecret = iSecret.mul(iAlpha).mod(iModulus); } var iRandom; + for (;;) { iRandom = sjcl.bn.random(iModulus, 0); - if (iRandom.jacobi(iModulus) === 1) + if (iRandom.jacobi(iModulus) === 1) { break; + } } var iBlind = iRandom.powermodMontgomery(iPublic.mul(iExponent), iModulus); var iSignreq = iSecret.mulmod(iBlind, iModulus); var signreq = sjcl.codec.hex.fromBits(iSignreq.toBits()); - + request.post(opts.url) - .send({ - info : publicInfo, - signreq : signreq - }).end(function(err, resp) { - - if (err || !resp) return fn(new Error("Could not query PAKDF server "+opts.host)); - - var data = resp.body || resp.text ? JSON.parse(resp.text) : {}; - - if (data.result !== 'success') return fn(new Error("Could not query PAKDF server "+opts.host)); - - var iSignres = new sjcl.bn(String(data.signres)); - var iRandomInv = iRandom.inverseMod(iModulus); - var iSigned = iSignres.mulmod(iRandomInv, iModulus); - var key = iSigned.toBits(); - var result = {}; - - tokens.forEach(function (token) { - result[token] = keyHash(key, token); + .send({ info: publicInfo, signreq: signreq }) + .end(function(err, resp) { + if (err || !resp) { + return fn(new Error('Could not query PAKDF server ' + opts.host)); + } + + var data = resp.body || resp.text ? JSON.parse(resp.text) : {}; + + if (data.result !== 'success') { + return fn(new Error('Could not query PAKDF server '+opts.host)); + } + + var iSignres = new sjcl.bn(String(data.signres)); + var iRandomInv = iRandom.inverseMod(iModulus); + var iSigned = iSignres.mulmod(iRandomInv, iModulus); + var key = iSigned.toBits(); + var result = { }; + + tokens.forEach(function(token) { + result[token] = keyHash(key, token); + }); + + fn(null, result); }); - - fn (null, result); - }); }; /** * Imported from ripple-client - * */ -Crypt.RippleAddress = (function () { + +Crypt.RippleAddress = (function() { + function append_int(a, i) { return [].concat(a, i >> 24, (i >> 16) & 0xff, (i >> 8) & 0xff, i & 0xff); } @@ -148,23 +154,24 @@ Crypt.RippleAddress = (function () { sjcl.hash.sha512.hash(sjcl.codec.bytes.toBits(bytes)), 0, 256 ); - } + }; function SHA256_RIPEMD160(bits) { return sjcl.hash.ripemd160.hash(sjcl.hash.sha256.hash(bits)); - } + }; - return function (seed) { + return function(seed) { this.seed = base.decode_check(33, seed); if (!this.seed) { - throw "Invalid seed."; + throw new Error('Invalid seed.'); } - this.getAddress = function (seq) { + this.getAddress = function(seq) { seq = seq || 0; var private_gen, public_gen, i = 0; + do { private_gen = sjcl.bn.fromBits(firstHalfOfSHA512(append_int(this.seed, i))); i++; @@ -174,6 +181,7 @@ Crypt.RippleAddress = (function () { var sec; i = 0; + do { sec = sjcl.bn.fromBits(firstHalfOfSHA512(append_int(append_int(public_gen.toBytesCompressed(), seq), i))); i++; @@ -188,11 +196,12 @@ Crypt.RippleAddress = (function () { /** * Encrypt data - * @params {string} key - * @params {string} data + * + * @param {string} key + * @param {string} data */ -Crypt.encrypt = function(key, data) -{ + +Crypt.encrypt = function(key, data) { key = sjcl.codec.hex.toBits(key); var opts = extend(true, {}, cryptConfig); @@ -208,12 +217,13 @@ Crypt.encrypt = function(key, data) return sjcl.codec.base64.fromBits(encryptedBits); }; - /** * Decrypt data - * @params {string} key - * @params {string} data + * + * @param {string} key + * @param {string} data */ + Crypt.decrypt = function (key, data) { key = sjcl.codec.hex.toBits(key); @@ -222,7 +232,7 @@ Crypt.decrypt = function (key, data) { var version = sjcl.bitArray.extract(encryptedBits, 0, 8); if (version !== 0) { - throw new Error("Unsupported encryption version: "+version); + throw new Error('Unsupported encryption version: '+version); } var encrypted = extend(true, {}, cryptConfig, { @@ -231,30 +241,33 @@ Crypt.decrypt = function (key, data) { }); return sjcl.decrypt(key, JSON.stringify(encrypted)); -}; +}; /** * Validate a ripple address + * * @param {string} address */ + Crypt.isValidAddress = function (address) { return UInt160.is_valid(address); }; - /** * Validate a ripple address + * * @param {integer} nWords - number of words */ + Crypt.createSecret = function (nWords) { return sjcl.codec.hex.fromBits(sjcl.random.randomWords(nWords)); }; - /** * Create a new master key */ + Crypt.createMaster = function () { return base.encode_check(33, sjcl.codec.bytes.fromBits(sjcl.random.randomWords(4))); }; @@ -262,78 +275,89 @@ Crypt.createMaster = function () { /** * Create a ripple address from a master key + * * @param {string} masterkey */ + Crypt.getAddress = function (masterkey) { return new Crypt.RippleAddress(masterkey).getAddress(); }; - /** * Hash data + * * @param {string} data */ + Crypt.hashSha512 = function (data) { return sjcl.codec.hex.fromBits(sjcl.hash.sha512.hash(data)); }; - /** * Sign a data string with a secret key + * * @param {string} secret * @param {string} data */ -Crypt.signString = function (secret, data) { + +Crypt.signString = function(secret, data) { var hmac = new sjcl.misc.hmac(sjcl.codec.hex.toBits(secret), sjcl.hash.sha512); return sjcl.codec.hex.fromBits(hmac.mac(data)); }; - /** * Create an an accout recovery key + * * @param {string} secret */ + Crypt.deriveRecoveryEncryptionKeyFromSecret = function(secret) { var seed = ripple.Seed.from_json(secret).to_bits(); var hmac = new sjcl.misc.hmac(seed, sjcl.hash.sha512); - var key = hmac.mac("ripple/hmac/recovery_encryption_key/v1"); + var key = hmac.mac('ripple/hmac/recovery_encryption_key/v1'); key = sjcl.bitArray.bitSlice(key, 0, 256); return sjcl.codec.hex.fromBits(key); }; /** * Convert base64 encoded data into base64url encoded data. + * * @param {String} base64 Data */ -Crypt.base64ToBase64Url = function (encodedData) { + +Crypt.base64ToBase64Url = function(encodedData) { return encodedData.replace(/\+/g, '-').replace(/\//g, '_').replace(/[=]+$/, ''); }; /** * Convert base64url encoded data into base64 encoded data. + * * @param {String} base64 Data */ -Crypt.base64UrlToBase64 = function (encodedData) { + +Crypt.base64UrlToBase64 = function(encodedData) { encodedData = encodedData.replace(/-/g, '+').replace(/_/g, '/'); + while (encodedData.length % 4) { encodedData += '='; } + return encodedData; }; - /** * Create a string from request parameters that * will be used to sign a request + * * @param {Object} config - request params * @param {Object} parsed - parsed url * @param {Object} date * @param {Object} mechanism - type of signing */ -Crypt.getStringToSign = function (config, parsed, date, mechanism) { + +Crypt.getStringToSign = function(config, parsed, date, mechanism) { // XXX This method doesn't handle signing GET requests correctly. The data // field will be merged into the search string, not the request body. - // Sort the properties of the JSON object into canonical form var canonicalData = JSON.stringify(copyObjectWithSortedKeys(config.data)); @@ -362,14 +386,15 @@ Crypt.getStringToSign = function (config, parsed, date, mechanism) { ].join('\n'); }; - /** * HMAC signed request + * * @param {Object} config * @param {Object} auth_secret * @param {Object} blob_id */ -Crypt.signRequestHmac = function (config, auth_secret, blob_id) { + +Crypt.signRequestHmac = function(config, auth_secret, blob_id) { config = extend(true, {}, config); // Parse URL @@ -378,24 +403,28 @@ Crypt.signRequestHmac = function (config, auth_secret, blob_id) { var signatureType = 'RIPPLE1-HMAC-SHA512'; var stringToSign = Crypt.getStringToSign(config, parsed, date, signatureType); var signature = Crypt.signString(auth_secret, stringToSign); - - config.url += (parsed.search ? "&" : "?") + - 'signature='+Crypt.base64ToBase64Url(signature)+ - '&signature_date='+date+ - '&signature_blob_id='+blob_id+ - '&signature_type='+signatureType; + var query = querystring.stringify({ + signature: Crypt.base64ToBase64Url(signature), + signature_date: date, + signature_blob_id: blob_id, + signature_type: signatureType + }); + + config.url += (parsed.search ? '&' : '?') + query; return config; -}; - +}; + /** * Asymmetric signed request + * * @param {Object} config * @param {Object} secretKey * @param {Object} account * @param {Object} blob_id */ -Crypt.signRequestAsymmetric = function (config, secretKey, account, blob_id) { + +Crypt.signRequestAsymmetric = function(config, secretKey, account, blob_id) { config = extend(true, {}, config); // Parse URL @@ -404,66 +433,72 @@ Crypt.signRequestAsymmetric = function (config, secretKey, account, blob_id) { var signatureType = 'RIPPLE1-ECDSA-SHA512'; var stringToSign = Crypt.getStringToSign(config, parsed, date, signatureType); var signature = message.signMessage(stringToSign, secretKey); - - config.url += (parsed.search ? "&" : "?") + - 'signature='+Crypt.base64ToBase64Url(signature)+ - '&signature_date='+date+ - '&signature_blob_id='+blob_id+ - '&signature_account='+account+ - '&signature_type='+signatureType; + + var query = querystring.stringify({ + signature: Crypt.base64ToBase64Url(signature), + signature_date: date, + signature_blob_id: blob_id, + signature_account: account, + signature_type: signatureType + }) + + config.url += (parsed.search ? '&' : '?') + query; return config; }; - //prepare for signing function copyObjectWithSortedKeys(object) { if (isPlainObject(object)) { var newObj = {}; var keysSorted = Object.keys(object).sort(); var key; + for (var i in keysSorted) { key = keysSorted[i]; if (Object.prototype.hasOwnProperty.call(object, key)) { newObj[key] = copyObjectWithSortedKeys(object[key]); } } + return newObj; } else if (Array.isArray(object)) { return object.map(copyObjectWithSortedKeys); } else { return object; } -} - +}; //from npm extend function isPlainObject(obj) { var hasOwn = Object.prototype.hasOwnProperty; var toString = Object.prototype.toString; - if (!obj || toString.call(obj) !== '[object Object]' || obj.nodeType || obj.setInterval) + if (!obj || toString.call(obj) !== '[object Object]' || obj.nodeType || obj.setInterval) { return false; + } var has_own_constructor = hasOwn.call(obj, 'constructor'); var has_is_property_of_method = hasOwn.call(obj.constructor.prototype, 'isPrototypeOf'); + // Not own constructor property must be Object - if (obj.constructor && !has_own_constructor && !has_is_property_of_method) + if (obj.constructor && !has_own_constructor && !has_is_property_of_method) { return false; + } // Own properties are enumerated firstly, so to speed up, // if last one is own, then all properties are own. var key; + for ( key in obj ) {} - return key === undefined || hasOwn.call( obj, key ); + return key === void(0) || hasOwn.call( obj, key ); } - -var dateAsIso8601 = (function () { +var dateAsIso8601 = (function() { function pad(n) { - return (n < 0 || n > 9 ? "" : "0") + n; - } + return (n < 0 || n > 9 ? '' : '0') + n; + }; return function dateAsIso8601() { var date = new Date(); @@ -476,5 +511,5 @@ var dateAsIso8601 = (function () { }; })(); +exports.Crypt = Crypt; -module.exports.Crypt = Crypt; \ No newline at end of file diff --git a/src/js/ripple/remote.js b/src/js/ripple/remote.js index 8edb9d32..5e20a78b 100644 --- a/src/js/ripple/remote.js +++ b/src/js/ripple/remote.js @@ -3,10 +3,10 @@ // - We use the W3C interface for node and browser compatibility: // http://www.w3.org/TR/websockets/#the-websocket-interface // -// This class is intended for both browser and node.js use. +// This class is intended for both browser and Node.js use. // // This class is designed to work via peer protocol via either the public or -// private websocket interfaces. The JavaScript class for the peer protocol +// private WebSocket interfaces. The JavaScript class for the peer protocol // has not yet been implemented. However, this class has been designed for it // to be a very simple drop option. // @@ -33,98 +33,96 @@ var config = require('./config'); var log = require('./log').internal.sub('remote'); /** - Interface to manage the connection to a Ripple server. - - This implementation uses WebSockets. - - Keys for opts: - - trace - max_listeners : Set maxListeners for remote; prevents EventEmitter warnings - connection_offset : Connect to remote servers on supplied interval (in seconds) - trusted : truthy, if remote is trusted - max_fee : Maximum acceptable transaction fee - fee_cushion : Extra fee multiplier to account for async fee changes. - servers : Array of server objects with the following form - canonical_signing : Signatures should be canonicalized and the "canonical" flag set - - { - host: - , port: - , secure: - } - - Events: - 'connect' - 'connected' (DEPRECATED) - 'disconnect' - 'disconnected' (DEPRECATED) - 'state': - - 'online' : Connected and subscribed. - - 'offline' : Not subscribed or not connected. - 'subscribed' : This indicates stand-alone is available. - - Server events: - 'ledger_closed' : A good indicate of ready to serve. - 'transaction' : Transactions we receive based on current subscriptions. - 'transaction_all' : Listening triggers a subscribe to all transactions - globally in the network. - - @param opts Connection options. - @param trace -*/ + * Interface to manage the connection to a Ripple server. + * + * This implementation uses WebSockets. + * + * Keys for opts: + * + * trace + * max_listeners : Set maxListeners for remote; prevents EventEmitter warnings + * connection_offset : Connect to remote servers on supplied interval (in seconds) + * trusted : truthy, if remote is trusted + * max_fee : Maximum acceptable transaction fee + * fee_cushion : Extra fee multiplier to account for async fee changes. + * servers : Array of server objects with the following form + * canonical_signing : Signatures should be canonicalized and the "canonical" flag set + * + * { + * host: + * , port: + * , secure: + * } + * + * Events: + * 'connect' + * 'disconnect' + * 'state': + * - 'online' : Connected and subscribed. + * - 'offline' : Not subscribed or not connected. + * 'subscribed' : This indicates stand-alone is available. + * + * Server events: + * 'ledger_closed' : A good indicate of ready to serve. + * 'transaction' : Transactions we receive based on current subscriptions. + * 'transaction_all' : Listening triggers a subscribe to all transactions + * globally in the network. + * + * @param opts Connection options. + * @param trace + */ function Remote(opts, trace) { EventEmitter.call(this); - var self = this; + var self = this; + var opts = opts || { }; + + this.trusted = Boolean(opts.trusted); + this.state = 'offline'; // 'online', 'offline' + this._server_fatal = false; // True, if we know server exited. + + this.local_sequence = Boolean(opts.local_sequence); // Locally track sequence numbers + this.local_fee = (typeof opts.local_fee === 'boolean') ? opts.local_fee : true;// Locally set fees + this.local_signing = (typeof opts.local_signing === 'boolean') ? opts.local_signing : true; + this.canonical_signing = (typeof opts.canonical_signing === 'boolean') ? opts.canonical_signing : true; + + this.fee_cushion = (typeof opts.fee_cushion === 'number') ? opts.fee_cushion : 1.2; + this.max_fee = (typeof opts.max_fee === 'number') ? opts.max_fee : Infinity; - this.trusted = Boolean(opts.trusted); - this.local_sequence = Boolean(opts.local_sequence); // Locally track sequence numbers - this.local_fee = (typeof opts.local_fee === 'undefined') ? true : opts.local_fee; // Locally set fees - this.local_signing = (typeof opts.local_signing === 'undefined') ? true : opts.local_signing; - this.canonical_signing = (typeof opts.canonical_signing === 'undefined') ? true : opts.canonical_signing; - this.fee_cushion = (typeof opts.fee_cushion === 'undefined') ? 1.2 : opts.fee_cushion; - this.max_fee = (typeof opts.max_fee === 'undefined') ? Infinity : opts.max_fee; - this.id = 0; - this.trace = opts.trace; - this._server_fatal = false; // True, if we know server exited. this._ledger_current_index = void(0); - this._ledger_hash = void(0); - this._ledger_time = void(0); - this._stand_alone = void(0); - this._testnet = void(0); - this._transaction_subs = 0; - this.online_target = false; - this._online_state = 'closed'; // 'open', 'closed', 'connecting', 'closing' - this.state = 'offline'; // 'online', 'offline' - this.retry_timer = void(0); - this.retry = void(0); - this._connection_count = 0; - this._connected = false; - this._connection_offset = 1000 * (typeof opts.connection_offset === 'number' ? opts.connection_offset : 5); - this._submission_timeout = 1000 * (typeof opts.submission_timeout === 'number' ? opts.submission_timeout : 10); + this._ledger_hash = void(0); + this._ledger_time = void(0); - this._received_tx = LRU({ max: 100 }); - this._cur_path_find = null; + this._stand_alone = void(0); + this._testnet = void(0); + this.trace = Boolean(opts.trace); + + this._transaction_subs = 0; + this._connection_count = 0; + this._connected = false; + + this._connection_offset = 1000 * (typeof opts.connection_offset === 'number' ? opts.connection_offset : 0); + this._submission_timeout = 1000 * (typeof opts.submission_timeout === 'number' ? opts.submission_timeout : 10); + + this._received_tx = LRU({ max: 100 }); + this._cur_path_find = null; // Local signing implies local fees and sequences if (this.local_signing) { this.local_sequence = true; - this.local_fee = true; + this.local_fee = true; } - this._servers = [ ]; + this._servers = [ ]; this._primary_server = void(0); // Cache information for accounts. // DEPRECATED, will be removed - this.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. - - // account : { seq : __ } - }; + // Consider sequence numbers stable if you know you're not generating bad transactions. + // Otherwise, clear it to have it automatically refreshed from the network. + // account : { seq : __ } + this.accounts = { }; // Account objects by AccountId. this._accounts = { }; @@ -133,11 +131,9 @@ function Remote(opts, trace) { this._books = { }; // Secrets that we know about. - this.secrets = { - // Secrets can be set by calling set_secret(account, secret). - - // account : secret - }; + // Secrets can be set by calling set_secret(account, secret). + // account : secret + this.secrets = { }; // Cache for various ledgers. // XXX Clear when ledger advances. @@ -147,18 +143,6 @@ function Remote(opts, trace) { } }; - // Fallback for previous API - if (!opts.hasOwnProperty('servers')) { - opts.servers = [ - { - host: opts.websocket_ip, - port: opts.websocket_port, - secure: opts.websocket_ssl, - trusted: opts.trusted - } - ]; - } - if (typeof this._connection_offset !== 'number') { throw new TypeError('Remote "connection_offset" configuration is not a Number'); } @@ -191,10 +175,6 @@ function Remote(opts, trace) { throw new TypeError('Remote "local_sequence" configuration is not a Boolean'); } - if (!Array.isArray(opts.servers)) { - throw new TypeError('Remote "servers" configuration is not an Array'); - } - if (!/^(undefined|number)$/.test(typeof opts.ping)) { throw new TypeError('Remote "ping" configuration is not a Number'); } @@ -203,16 +183,32 @@ function Remote(opts, trace) { throw new TypeError('Remote "storage" configuration is not an Object'); } - opts.servers.forEach(function(server) { + // Fallback for previous API + if (!opts.hasOwnProperty('servers') && opts.websocket_ip) { + opts.servers = [ + { + host: opts.websocket_ip, + port: opts.websocket_port, + secure: opts.websocket_ssl, + trusted: opts.trusted + } + ]; + } + + (opts.servers || []).forEach(function(server) { var pool = Number(server.pool) || 1; - while (pool--) { self.addServer(server); }; + while (pool--) { + self.addServer(server); + }; }); // This is used to remove Node EventEmitter warnings var maxListeners = opts.maxListeners || opts.max_listeners || 0; this._servers.concat(this).forEach(function(emitter) { - emitter.setMaxListeners(maxListeners); + if (emitter instanceof EventEmitter) { + emitter.setMaxListeners(maxListeners); + } }); function listenerAdded(type, listener) { @@ -237,37 +233,9 @@ function Remote(opts, trace) { this.on('removeListener', listenerRemoved); - function getPendingTransactions() { - self.storage.getPendingTransactions(function(err, transactions) { - if (err || !Array.isArray(transactions)) { - return; - } - - function resubmitTransaction(tx) { - var transaction = self.transaction(); - transaction.parseJson(tx.tx_json); - - Object.keys(tx).forEach(function(prop) { - switch (prop) { - case 'secret': - case 'submittedIDs': - case 'clientID': - case 'submitIndex': - transaction[prop] = tx[prop]; - break; - } - }); - - transaction.submit(); - }; - - transactions.forEach(resubmitTransaction); - }); - }; - if (opts.storage) { this.storage = opts.storage; - this.once('connect', getPendingTransactions); + this.once('connect', this.getPendingTransactions.bind(this)); } function pingServers() { @@ -288,38 +256,37 @@ util.inherits(Remote, EventEmitter); // Flags for ledger entries. In support of account_root(). Remote.flags = { // Account Root - account_root : { - PasswordSpent: 0x00010000, // True, if password set fee is spent. - RequireDestTag: 0x00020000, // True, to require a DestinationTag for payments. - RequireAuth: 0x00040000, // True, to require a authorization to hold IOUs. - DisallowXRP: 0x00080000, // True, to disallow sending XRP. - DisableMaster: 0x00100000 // True, force regular key. + account_root: { + PasswordSpent: 0x00010000, // True, if password set fee is spent. + RequireDestTag: 0x00020000, // True, to require a DestinationTag for payments. + RequireAuth: 0x00040000, // True, to require a authorization to hold IOUs. + DisallowXRP: 0x00080000, // True, to disallow sending XRP. + DisableMaster: 0x00100000 // True, force regular key. }, // Offer offer: { - Passive: 0x00010000, - Sell: 0x00020000 // True, offer was placed as a sell. + Passive: 0x00010000, + Sell: 0x00020000 // True, offer was placed as a sell. }, // Ripple State state: { - LowReserve: 0x00010000, // True, if entry counts toward reserve. - HighReserve: 0x00020000, - LowAuth: 0x00040000, - HighAuth: 0x00080000, - LowNoRipple: 0x00100000, - HighNoRipple: 0x00200000 + LowReserve: 0x00010000, // True, if entry counts toward reserve. + HighReserve: 0x00020000, + LowAuth: 0x00040000, + HighAuth: 0x00080000, + LowNoRipple: 0x00100000, + HighNoRipple: 0x00200000 } }; Remote.from_config = function(obj, trace) { var serverConfig = (typeof obj === 'string') ? config.servers[obj] : obj; - var remote = new Remote(serverConfig, trace); function initializeAccount(account) { - var accountInfo = this.accounts[account]; + var accountInfo = config.accounts[account]; if (typeof accountInfo === 'object') { if (accountInfo.secret) { // Index by nickname @@ -331,12 +298,155 @@ Remote.from_config = function(obj, trace) { }; if (config.accounts) { - Object.keys(config.accounts).forEach(initializeAccount, config); + Object.keys(config.accounts).forEach(initializeAccount); } return remote; }; +/** + * Check that server message is valid + * + * @param {Object} message + */ + +Remote.isValidMessage = function(message) { + return (typeof message === 'object') + && (typeof message.type === 'string'); +}; + +/** + * Check that server message contains valid + * ledger data + * + * @param {Object} message + */ + +Remote.isValidLedgerData = function(message) { + return (typeof message === 'object') + && (typeof message.fee_base === 'number') + && (typeof message.fee_ref === 'number') + && (typeof message.fee_base === 'number') + && (typeof message.ledger_hash === 'string') + && (typeof message.ledger_index === 'number') + && (typeof message.ledger_time === 'number') + && (typeof message.reserve_base === 'number') + && (typeof message.reserve_inc === 'number') + && (typeof message.txn_count === 'number'); +}; + +/** + * Check that server message contains valid + * load status data + * + * @param {Object} message + */ + +Remote.isValidLoadStatus = function(message) { + return (typeof message.load_base === 'number') + && (typeof message.load_factor === 'number'); +}; + +/** + * Set the emitted state: 'online' or 'offline' + * + * @param {String} state + */ + +Remote.prototype._setState = function(state) { + if (this.state !== state) { + if (this.trace) { + log.info('set_state:', state); + } + + this.state = state; + this.emit('state', state); + + switch (state) { + case 'online': + this._online_state = 'open'; + this._connected = true; + this.emit('connect'); + this.emit('connected'); + break; + case 'offline': + this._online_state = 'closed'; + this._connected = false; + this.emit('disconnect'); + this.emit('disconnected'); + break; + } + } +}; + +/** + * Inform remote that the remote server is not comming back. + */ + +Remote.prototype.setServerFatal = function() { + this._server_fatal = true; +}; + +/** + * Enable debug output + * + * @param {Boolean} trace + */ + +Remote.prototype.setTrace = function(trace) { + this.trace = (trace === void(0) || trace); + return this; +}; + +Remote.prototype._trace = function() { + if (this.trace) { + log.info.apply(log, arguments); + } +}; + +/** + * Store a secret - allows the Remote to automatically fill + * out auth information. + * + * @param {String} account + * @param {String} secret + */ + +Remote.prototype.setSecret = function(account, secret) { + this.secrets[account] = secret; +}; + +Remote.prototype.getPendingTransactions = function() { + var self = this; + + function resubmitTransaction(tx) { + if (typeof tx !== 'object') { + return; + } + + var transaction = self.transaction(); + transaction.parseJson(tx.tx_json); + transaction.clientID(tx.clientID); + Object.keys(tx).forEach(function(prop) { + switch (prop) { + case 'secret': + case 'submittedIDs': + case 'submitIndex': + transaction[prop] = tx[prop]; + break; + } + }); + + transaction.submit(); + }; + + this.storage.getPendingTransactions(function(err, transactions) { + if (!err && Array.isArray(transactions)) { + transactions.forEach(resubmitTransaction); + } + }); +}; + Remote.prototype.addServer = function(opts) { var self = this; @@ -351,7 +461,7 @@ Remote.prototype.addServer = function(opts) { function serverConnect() { self._connection_count += 1; - if (opts.primary || !self._primary_server) { + if (opts.primary) { self._setPrimaryServer(server); } if (self._connection_count === 1) { @@ -378,58 +488,11 @@ Remote.prototype.addServer = function(opts) { return this; }; -// Inform remote that the remote server is not comming back. -Remote.prototype.serverFatal = function() { - this._server_fatal = true; -}; - -// Set the emitted state: 'online' or 'offline' -Remote.prototype._setState = function(state) { - if (this.state !== state) { - this._trace('remote: set_state:', state); - - this.state = state; - - this.emit('state', state); - - switch (state) { - case 'online': - this._online_state = 'open'; - this._connected = true; - this.emit('connect'); - this.emit('connected'); - break; - - case 'offline': - this._online_state = 'closed'; - this._connected = false; - this.emit('disconnect'); - this.emit('disconnected'); - break; - } - } -}; - -Remote.prototype.setTrace = function(trace) { - this.trace = trace === void(0) || trace; - return this; -}; - -// Store a secret - allows the Remote to automatically fill out auth information. -Remote.prototype.setSecret = function(account, secret) { - this.secrets[account] = secret; -}; - -Remote.prototype._trace = function() { - if (this.trace) { - log.info.apply(log, arguments); - } -}; - /** * Connect to the Ripple network. * - * param {Function} callback + * @param {Function} callback + * @api public */ Remote.prototype.connect = function(online) { @@ -440,11 +503,9 @@ Remote.prototype.connect = function(online) { switch (typeof online) { case 'undefined': break; - case 'function': this.once('connect', online); break; - default: // Downwards compatibility if (!Boolean(online)) { @@ -455,14 +516,10 @@ Remote.prototype.connect = function(online) { var self = this; ;(function nextServer(i) { - var server = self._servers[i]; - server.connect(); - server._sid = ++i; - + self._servers[i].connect(); + var next = nextServer.bind(this, ++i); if (i < self._servers.length) { - setTimeout(function() { - nextServer(i); - }, self._connection_offset); + setTimeout(next, self._connection_offset); } })(0); @@ -471,7 +528,11 @@ Remote.prototype.connect = function(online) { /** * Disconnect from the Ripple network. + * + * @param {Function} callback + * @api public */ + Remote.prototype.disconnect = function(callback) { if (!this._servers.length) { throw new Error('No servers available, not disconnecting'); @@ -490,11 +551,24 @@ Remote.prototype.disconnect = function(callback) { return this; }; -// It is possible for messages to be dispatched after the connection is closed. +/** + * Handle server message. Server messages are proxied to + * the Remote, such that global events can be handled + * + * It is possible for messages to be dispatched after the + * connection is closed. + * + * @param {JSON} message + * @param {Server} server + */ + Remote.prototype._handleMessage = function(message, server) { var self = this; - try { message = JSON.parse(message); } catch(e) { } + try { + message = JSON.parse(message); + } catch (e) { + } if (!Remote.isValidMessage(message)) { // Unexpected response from remote. @@ -503,168 +577,208 @@ Remote.prototype._handleMessage = function(message, server) { } switch (message.type) { - case 'response': - // Handled by the server that sent the request - break; - case 'ledgerClosed': - // XXX If not trusted, need to verify we consider ledger closed. - // XXX Also need to consider a slow server or out of order response. - // XXX Be more defensive fields could be missing or of wrong type. - // YYY Might want to do some cache management. - if (!Remote.isValidLedgerData(message)) { - return; - } - - if (message.ledger_index >= this._ledger_current_index) { - this._ledger_time = message.ledger_time; - this._ledger_hash = message.ledger_hash; - this._ledger_current_index = message.ledger_index + 1; - this.emit('ledger_closed', message, server); - } + this._handleLedgerClosed(message); break; - case 'serverStatus': - self.emit('server_status', message); + this._handleServerStatus(message); break; - case 'transaction': - // To get these events, just subscribe to them. A subscribes and - // unsubscribes will be added as needed. - // XXX If not trusted, need proof. - - // De-duplicate transactions that are immediately following each other - var hash = message.transaction.hash; - - if (this._received_tx.get(hash)) { - break; - } - - if (message.validated) { - this._received_tx.set(hash, true); - } - - this._trace('remote: tx:', message); - - if (message.meta) { - // Process metadata - message.mmeta = new Meta(message.meta); - - // Pass the event on to any related Account objects - message.mmeta.getAffectedAccounts().forEach(function(account) { - account = self._accounts[account]; - if (account) { - account.notify(message); - } - }); - - // Pass the event on to any related OrderBooks - message.mmeta.getAffectedBooks().forEach(function(book) { - book = self._books[book]; - if (book) { - book.notify(message); - } - }); - } else { - [ 'Account', 'Destination' ].forEach(function(prop) { - var account = message.transaction[prop]; - if (account && (account = self.account(account))) { - account.notify(message); - } - }); - } - - this.emit('transaction', message); - this.emit('transaction_all', message); + this._handleTransaction(message); break; - case 'path_find': - // Pass the event to the currently open PathFind object - if (this._cur_path_find) { - this._cur_path_find.notify_update(message); - } - - this.emit('path_find_all', message); + this._handlePathFind(message); break; - - // All other messages default: - this._trace('remote: ' + message.type + ': ', message); - this.emit('net_' + message.type, message); + if (this.trace) { + log.info(message.type + ': ', message); + } break; } }; -Remote.isValidMessage = function(message) { - return (typeof message === 'object') - && (typeof message.type === 'string'); +/** + * Handle server ledger_closed event + * + * @param {Object} message + */ + +Remote.prototype._handleLedgerClosed = function(message) { + var self = this; + + // XXX If not trusted, need to verify we consider ledger closed. + // XXX Also need to consider a slow server or out of order response. + // XXX Be more defensive fields could be missing or of wrong type. + // YYY Might want to do some cache management. + if (!Remote.isValidLedgerData(message)) { + return; + } + + var ledgerAdvanced = message.ledger_index >= this._ledger_current_index; + + if (ledgerAdvanced) { + this._ledger_time = message.ledger_time; + this._ledger_hash = message.ledger_hash; + this._ledger_current_index = message.ledger_index + 1; + this.emit('ledger_closed', message); + } }; -Remote.isValidLedgerData = function(ledger) { - return (typeof ledger === 'object') - && (typeof ledger.fee_base === 'number') - && (typeof ledger.fee_ref === 'number') - && (typeof ledger.fee_base === 'number') - && (typeof ledger.ledger_hash === 'string') - && (typeof ledger.ledger_index === 'number') - && (typeof ledger.ledger_time === 'number') - && (typeof ledger.reserve_base === 'number') - && (typeof ledger.reserve_inc === 'number') - && (typeof ledger.txn_count === 'number'); +/** + * Handle server server_status event + * + * @param {Object} message + */ + +Remote.prototype._handleServerStatus = function(message) { + this.emit('server_status', message); }; -Remote.isLoadStatus = function(message) { - return (typeof message.load_base === 'number') - && (typeof message.load_factor === 'number'); +/** + * Handle server transaction event + * + * @param {Object} message + */ + +Remote.prototype._handleTransaction = function(message) { + var self = this; + + // XXX If not trusted, need proof. + + // De-duplicate transactions + var transactionHash = message.transaction.hash; + + if (this._received_tx.get(transactionHash)) { + return; + } + + if (message.validated) { + this._received_tx.set(transactionHash, true); + } + + if (this.trace) { + log.info('tx:', message); + } + + function notify(el) { + var item = this[el]; + if (item && typeof item.notify === 'function') { + item.notify(message); + } + }; + + var metadata = message.meta || message.metadata; + + if (metadata) { + // Process metadata + message.mmeta = new Meta(metadata); + + // Pass the event on to any related Account objects + var affectedAccounts = message.mmeta.getAffectedAccounts(); + affectedAccounts.forEach(notify.bind(this._accounts)); + + // Pass the event on to any related OrderBooks + var affectedBooks = message.mmeta.getAffectedBooks(); + affectedBooks.forEach(notify.bind(this._books)); + } else { + // Transaction could be from proposed transaction stream + [ 'Account', 'Destination' ].forEach(function(prop) { + notify.call(self._accounts, message.transaction[prop]); + }); + } + + this.emit('transaction', message); + this.emit('transaction_all', message); }; -Remote.prototype.ledgerHash = function() { +/** + * Handle server path_find event + * + * @param {Object} message + */ + +Remote.prototype._handlePathFind = function(message) { + var self = this; + + // Pass the event to the currently open PathFind object + if (this._cur_path_find) { + this._cur_path_find.notify_update(message); + } + + this.emit('path_find_all', message); +}; + +/** + * Returns the current ledger hash + * + * @return {String} ledger hash + */ + +Remote.prototype.getLedgerHash = function() { return this._ledger_hash; }; -Remote.prototype._setPrimaryServer = function(server) { +/** + * Set primary server. Primary server will be selected + * to handle requested regardless of its internally-tracked + * priority score + * + * @param {Server} server + */ + +Remote.prototype._setPrimaryServer = +Remote.prototype.setPrimaryServer = function(server) { if (this._primary_server) { this._primary_server._primary = false; } - this._primary_server = server; - this._primary_server._primary = true; + this._primary_server = server; + this._primary_server._primary = true; }; -Remote.prototype._serverIsAvailable = function(server) { - return server && server._connected; -}; +/** + * Select a server to handle a request. Servers are + * automatically prioritized + */ -Remote.prototype._nextServer = function() { - var result = null; - - for (var i=0, l=this._servers.length; i bScore) { + return 1; + } else if (aScore < bScore) { + return -1; + } else { + return 0; } + }; + + // Sort servers by score + this._servers.sort(sortByScore); + + var index = 0; + var server = this._servers[index]; + + while (!server._connected) { + server = this._servers[++index]; } return server; }; -// Send a request. -// <-> request: what to send, consumed. +/** + * Send a request. This method is called internally by Request + * objects. Each Request contains a reference to Remote, and + * Request.request calls Request.remote.request + * + * @param {Request} request + */ + Remote.prototype.request = function(request) { if (typeof request === 'string') { if (!/^request_/.test(request)) { @@ -676,9 +790,12 @@ Remote.prototype.request = function(request) { } else { throw new Error('Command does not exist: ' + request); } - } else if (!(request instanceof Request)) { + } + + if (!(request instanceof Request)) { throw new Error('Argument is not a Request'); } + if (!this._servers.length) { request.emit('error', new Error('No servers available')); } else if (!this._connected) { @@ -686,7 +803,7 @@ Remote.prototype.request = function(request) { } else if (request.server === null) { request.emit('error', new Error('Server does not exist')); } else { - var server = request.server || this._getServer(); + var server = request.server || this.getServer(); if (server) { server._request(request); } else { @@ -695,13 +812,58 @@ Remote.prototype.request = function(request) { } }; +/** + * Request ping + * + * @param [String] server host + * @param [Function] callback + * @return {Request} request + */ + +Remote.prototype.ping = +Remote.prototype.requestPing = function(host, callback) { + var request = new Request(this, 'ping'); + + switch (typeof host) { + case 'function': + callback = host; + break; + case 'string': + request.setServer(host); + break; + } + + var then = Date.now(); + + request.once('success', function() { + request.emit('pong', Date.now() - then); + }); + + request.callback(callback, 'pong'); + + return request; +}; + +/** + * Request server_info + * + * @param [Function] callback + * @return {Request} request + */ + Remote.prototype.requestServerInfo = function(callback) { return new Request(this, 'server_info').callback(callback); }; -// XXX This is a bad command. Some variants don't scale. -// XXX Require the server to be trusted. +/** + * Request ledger + * + * @return {Request} request + */ + Remote.prototype.requestLedger = function(ledger, options, callback) { + // XXX This is a bad command. Some variants don't scale. + // XXX Require the server to be trusted. //utils.assert(this.trusted); var request = new Request(this, 'ledger'); @@ -709,7 +871,7 @@ Remote.prototype.requestLedger = function(ledger, options, callback) { if (ledger) { // DEPRECATED: use .ledger_hash() or .ledger_index() //console.log('request_ledger: ledger parameter is deprecated'); - request.message.ledger = ledger; + request.message.ledger = ledger; } switch (typeof options) { @@ -733,7 +895,9 @@ Remote.prototype.requestLedger = function(ledger, options, callback) { default: //DEPRECATED - this._trace('request_ledger: full parameter is deprecated'); + if (this.trace) { + log.info('request_ledger: full parameter is deprecated'); + } request.message.full = true; break; } @@ -743,34 +907,64 @@ Remote.prototype.requestLedger = function(ledger, options, callback) { return request; }; +/** + * Request ledger_closed + * + * @param [Function] callback + * @return {Request} request + */ + Remote.prototype.requestLedgerClosed = Remote.prototype.requestLedgerHash = function(callback) { //utils.assert(this.trusted); // If not trusted, need to check proof. return new Request(this, 'ledger_closed').callback(callback); }; -// .ledger() -// .ledger_index() +/** + * Request ledger_header + * + * @param [Function] callback + * @return {Request} request + */ + Remote.prototype.requestLedgerHeader = function(callback) { return new Request(this, 'ledger_header').callback(callback); }; -// Get the current proposed ledger entry. May be closed (and revised) at any time (even before returning). -// Only for unit testing. +/** + * Request ledger_current + * + * Get the current proposed ledger entry. May be closed (and revised) + * at any time (even before returning). + * + * Only for unit testing. + * + * @param [Function] callback + * @return {Request} request + */ + Remote.prototype.requestLedgerCurrent = function(callback) { return new Request(this, 'ledger_current').callback(callback); }; -// --> type : the type of ledger entry. -// .ledger() -// .ledger_index() -// .offer_id() +/** + * Request ledger_entry + * + * @param [String] type + * @param [Function] callback + * @return {Request} request + */ + Remote.prototype.requestLedgerEntry = function(type, callback) { //utils.assert(this.trusted); // If not trusted, need to check proof, maybe talk packet protocol. var self = this; var request = new Request(this, 'ledger_entry'); + if (typeof type === 'function') { + callback = type; + } + // Transparent caching. When .request() is invoked, look in the Remote object for the result. // If not found, listen, cache result, and emit it. // @@ -837,7 +1031,14 @@ Remote.prototype.requestLedgerEntry = function(type, callback) { return request; }; -// .accounts(accounts, realtime) +/** + * Request subscribe + * + * @param {Array} streams + * @param [Function] callback + * @return {Request} request + */ + Remote.prototype.requestSubscribe = function(streams, callback) { var request = new Request(this, 'subscribe'); @@ -850,7 +1051,14 @@ Remote.prototype.requestSubscribe = function(streams, callback) { return request; }; -// .accounts(accounts, realtime) +/** + * Request usubscribe + * + * @param {Array} streams + * @param [Function] callback + * @return {Request} request + */ + Remote.prototype.requestUnsubscribe = function(streams, callback) { var request = new Request(this, 'unsubscribe'); @@ -863,26 +1071,32 @@ Remote.prototype.requestUnsubscribe = function(streams, callback) { return request; }; -// .ledger_choose() -// .ledger_hash() -// .ledger_index() -Remote.prototype.requestTransaction = -Remote.prototype.requestTransactionEntry = function(hash, ledger_hash, callback) { - //utils.assert(this.trusted); // If not trusted, need to check proof, maybe talk packet protocol. +/** + * Request transaction_entry + * + * @param {String} transaction hash + * @param {String|Number} ledger hash or sequence + * @param [Function] callback + * @return {Request} request + */ + +Remote.prototype.requestTransactionEntry = function(hash, ledgerHash, callback) { + //// If not trusted, need to check proof, maybe talk packet protocol. + //utils.assert(this.trusted); var request = new Request(this, 'transaction_entry'); request.txHash(hash); - switch (typeof ledger_hash) { + switch (typeof ledgerHash) { case 'string': case 'number': - request.ledgerSelect(ledger_hash); + request.ledgerSelect(ledgerHash); break; case 'undefined': case 'function': request.ledgerIndex('validated'); - callback = ledger_hash; + callback = ledgerHash; break; default: @@ -894,7 +1108,15 @@ Remote.prototype.requestTransactionEntry = function(hash, ledger_hash, callback) return request; }; -// DEPRECATED: use request_transaction_entry +/** + * Request tx + * + * @param {String} transaction hash + * @param [Function] callback + * @return {Request} request + */ + +Remote.prototype.requestTransaction = Remote.prototype.requestTx = function(hash, callback) { var request = new Request(this, 'tx'); @@ -904,6 +1126,12 @@ Remote.prototype.requestTx = function(hash, callback) { return request; }; +/** + * Account request abstraction + * + * @api private + */ + Remote.accountRequest = function(type, account, accountIndex, ledger, peer, callback) { if (typeof account === 'object') { var options = account; @@ -930,11 +1158,11 @@ Remote.accountRequest = function(type, account, accountIndex, ledger, peer, call request.message.index = accountIndex; } - if (typeof ledger !== 'undefined') { + if (!/^(undefined|function)$/.test(typeof ledger)) { request.ledgerChoose(ledger); } - if (typeof peer !== 'undefined') { + if (!/^(undefined|function)$/.test(typeof peer)) { request.message.peer = UInt160.json_rewrite(peer); } @@ -943,42 +1171,88 @@ Remote.accountRequest = function(type, account, accountIndex, ledger, peer, call return request; }; -Remote.prototype.requestAccountInfo = function(accountID, callback) { +/** + * Request account_info + * + * @param {String} ripple address + * @param [Function] callback + * @return {Request} + */ + +Remote.prototype.requestAccountInfo = function(account, callback) { var args = Array.prototype.concat.apply(['account_info'], arguments); return Remote.accountRequest.apply(this, args); }; -Remote.prototype.requestAccountCurrencies = function(accountID, callback) { +/** + * Request account_currencies + * + * @param {String} ripple address + * @param [Function] callback + * @return {Request} + */ + +Remote.prototype.requestAccountCurrencies = function(account, callback) { var args = Array.prototype.concat.apply(['account_currencies'], arguments); return Remote.accountRequest.apply(this, args); }; -// --> account_index: sub_account index (optional) -// --> current: true, for the current ledger. -Remote.prototype.requestAccountLines = function(accountID, account_index, ledger, peer, callback) { +/** + * Request account_lines + * + * @param {String} ripple address + * @param {Number] sub-account index + * @param [String|Number] ledger + * @param [String] peer + * @param [Function] callback + * @return {Request} + */ + +Remote.prototype.requestAccountLines = function(account, accountIndex, ledger, peer, callback) { // XXX Does this require the server to be trusted? //utils.assert(this.trusted); var args = Array.prototype.concat.apply(['account_lines'], arguments); return Remote.accountRequest.apply(this, args); }; -// --> account_index: sub_account index (optional) -// --> current: true, for the current ledger. -Remote.prototype.requestAccountOffers = function(accountID, account_index, ledger, callback) { +/** + * Request account_offers + * + * @param {String} ripple address + * @param {Number] sub-account index + * @param [String|Number] ledger + * @param [String] peer + * @param [Function] callback + * @return {Request} + */ + +Remote.prototype.requestAccountOffers = function(account, accountIndex, ledger, callback) { var args = Array.prototype.concat.apply(['account_offers'], arguments); return Remote.accountRequest.apply(this, args); }; -/* - account: account, - ledger_index_min: ledger_index, // optional, defaults to -1 if ledger_index_max is specified. - ledger_index_max: ledger_index, // optional, defaults to -1 if ledger_index_min is specified. - binary: boolean, // optional, defaults to false - count: boolean, // optional, defaults to false - descending: boolean, // optional, defaults to false - offset: integer, // optional, defaults to 0 - limit: integer // optional -*/ + +/** + * Request account_tx + * + * @param {Object} options + * + * @param {String} account + * @param [Number] ledger_index_min defaults to -1 if ledger_index_max is specified. + * @param [Number] ledger_index_max defaults to -1 if ledger_index_min is specified. + * @param [Boolean] binary, defaults to false + * @param [Boolean] parseBinary, defaults to true + * @param [Boolean] count, defaults to false + * @param [Boolean] descending, defaults to false + * @param [Number] offset, defaults to 0 + * @param [Number] limit + * + * @param [Function] filter + * @param [Function] map + * @param [Function] reduce + * @param [Function] callback + * @return {Request} + */ Remote.prototype.requestAccountTransactions = Remote.prototype.requestAccountTx = function(options, callback) { @@ -1116,6 +1390,10 @@ Remote.prototype.requestAccountTx = function(options, callback) { * * Returns a list of transactions that happened recently on the network. The * default number of transactions to be returned is 20. + * + * @param [Number] start + * @param [Function] callback + * @return {Request} */ Remote.prototype.requestTxHistory = function(start, callback) { @@ -1130,6 +1408,16 @@ Remote.prototype.requestTxHistory = function(start, callback) { return request; }; +/** + * Request book_offers + * + * @param {Object} gets + * @param {Object} pays + * @param {String} taker + * @param [Function] calback + * @return {Request} + */ + Remote.prototype.requestBookOffers = function(gets, pays, taker, callback) { if (gets.hasOwnProperty('pays')) { var options = gets; @@ -1170,6 +1458,14 @@ Remote.prototype.requestBookOffers = function(gets, pays, taker, callback) { return request; }; +/** + * Request wallet_accounts + * + * @param {String} seed + * @param [Function] callback + * @return {Request} + */ + Remote.prototype.requestWalletAccounts = function(seed, callback) { utils.assert(this.trusted); // Don't send secrets. @@ -1180,6 +1476,15 @@ Remote.prototype.requestWalletAccounts = function(seed, callback) { return request; }; +/** + * Request sign + * + * @param {String} secret + * @param {Object} tx_json + * @param [Function] callback + * @return {Request} + */ + Remote.prototype.requestSign = function(secret, tx_json, callback) { utils.assert(this.trusted); // Don't send secrets. @@ -1191,7 +1496,13 @@ Remote.prototype.requestSign = function(secret, tx_json, callback) { return request; }; -// Submit a transaction. +/** + * Request submit + * + * @param [Function] callback + * @return {Request} + */ + Remote.prototype.requestSubmit = function(callback) { return new Request(this, 'submit').callback(callback); }; @@ -1203,11 +1514,13 @@ Remote.prototype.requestSubmit = function(callback) { * the server_subscribe event. * * This function will create and return the request, but not submit it. + * + * @param [Function] callback + * @api private */ Remote.prototype._serverPrepareSubscribe = function(callback) { var self = this; - var feeds = [ 'ledger', 'server' ]; if (this._transaction_subs) { @@ -1218,7 +1531,7 @@ Remote.prototype._serverPrepareSubscribe = function(callback) { function serverSubscribed(message) { self._stand_alone = !!message.stand_alone; - self._testnet = !!message.testnet; + self._testnet = !!message.testnet; if (typeof message.random === 'string') { var rand = message.random.match(/[0-9A-F]{8}/ig); @@ -1231,9 +1544,9 @@ Remote.prototype._serverPrepareSubscribe = function(callback) { } if (message.ledger_hash && message.ledger_index) { - self._ledger_time = message.ledger_time; - self._ledger_hash = message.ledger_hash; - self._ledger_current_index = message.ledger_index+1; + self._ledger_time = message.ledger_time; + self._ledger_hash = message.ledger_hash; + self._ledger_current_index = message.ledger_index+1; self.emit('ledger_closed', message); } @@ -1246,27 +1559,43 @@ Remote.prototype._serverPrepareSubscribe = function(callback) { request.callback(callback, 'subscribed'); - // XXX Could give error events, maybe even time out. - return request; }; -// For unit testing: ask the remote to accept the current ledger. -// - To be notified when the ledger is accepted, server_subscribe() then listen to 'ledger_hash' events. -// A good way to be notified of the result of this is: -// remote.once('ledger_closed', function(ledger_closed, ledger_index) { ... } ); -Remote.prototype.ledgerAccept = function(callback) { - if (this._stand_alone) { - var request = new Request(this, 'ledger_accept'); - request.request(); - request.callback(callback); - } else { +/** + * For unit testing: ask the remote to accept the current ledger. + * To be notified when the ledger is accepted, server_subscribe() then listen to 'ledger_hash' events. + * A good way to be notified of the result of this is: + * remote.once('ledger_closed', function(ledger_closed, ledger_index) { ... } ); + * + * @param [Function] callback + */ + +Remote.prototype.ledgerAccept = +Remote.prototype.requestLedgerAccept = function(callback) { + if (!this._stand_alone) { this.emit('error', new RippleError('notStandAlone')); + return; } + var request = new Request(this, 'ledger_accept'); + + this.once('ledger_closed', function(ledger) { + request.emit('ledger_closed', ledger); + }); + + request.callback(callback, 'ledger_closed'); + request.request(); + return this; }; +/** + * Account root request abstraction + * + * @api private + */ + Remote.accountRootRequest = function(type, responseFilter, account, ledger, callback) { if (typeof account === 'object') { callback = ledger; @@ -1294,7 +1623,15 @@ Remote.accountRootRequest = function(type, responseFilter, account, ledger, call return request; }; -// Return a request to refresh the account balance. +/** + * Request account balance + * + * @param {String} account + * @param [String|Number] ledger + * @param [Function] callback + * @return {Request} + */ + Remote.prototype.requestAccountBalance = function(account, ledger, callback) { function responseFilter(message) { return Amount.from_json(message.node.Balance); @@ -1306,7 +1643,15 @@ Remote.prototype.requestAccountBalance = function(account, ledger, callback) { return request; }; -// Return a request to return the account flags. +/** + * Request account flags + * + * @param {String} account + * @param [String|Number] ledger + * @param [Function] callback + * @return {Request} + */ + Remote.prototype.requestAccountFlags = function(account, ledger, callback) { function responseFilter(message) { return message.node.Flags; @@ -1318,7 +1663,15 @@ Remote.prototype.requestAccountFlags = function(account, ledger, callback) { return request; }; -// Return a request to emit the owner count. +/** + * Request owner count + * + * @param {String} account + * @param [String|Number] ledger + * @param [Function] callback + * @return {Request} + */ + Remote.prototype.requestOwnerCount = function(account, ledger, callback) { function responseFilter(message) { return message.node.OwnerCount; @@ -1330,10 +1683,25 @@ Remote.prototype.requestOwnerCount = function(account, ledger, callback) { return request; }; +/** + * Get an account by accountID (address) + * + * + * @param {String} account + * @return {Account} + */ + Remote.prototype.getAccount = function(accountID) { return this._accounts[UInt160.json_rewrite(accountID)]; }; +/** + * Add an account by accountID (address) + * + * @param {String} account + * @return {Account} + */ + Remote.prototype.addAccount = function(accountID) { var account = new Account(this, accountID); @@ -1344,12 +1712,29 @@ Remote.prototype.addAccount = function(accountID) { return account; }; -Remote.prototype.account = function(accountID) { +/** + * Add an account if it does not exist, return the + * account by accountID (address) + * + * @param {String} account + * @return {Account} + */ + +Remote.prototype.account = +Remote.prototype.findAccount = function(accountID) { var account = this.getAccount(accountID); return account ? account : this.addAccount(accountID); }; -Remote.prototype.pathFind = function(src_account, dst_account, dst_amount, src_currencies) { +/** + * Create a pathfind + * + * @param {Object} options + * @return {PathFind} + */ + +Remote.prototype.pathFind = +Remote.prototype.createPathFind = function(src_account, dst_account, dst_amount, src_currencies) { if (typeof src_account === 'object') { var options = src_account; src_currencies = options.src_currencies; @@ -1375,7 +1760,16 @@ Remote.prepareTrade = function(currency, issuer) { return currency + (currency === 'XRP' ? '' : ('/' + issuer)); }; -Remote.prototype.book = function(currency_gets, issuer_gets, currency_pays, issuer_pays) { +/** + * Create an OrderBook if it does not exist, return + * the order book + * + * @param {Object} options + * @return {OrderBook} + */ + +Remote.prototype.book = +Remote.prototype.createOrderBook = function(currency_gets, issuer_gets, currency_pays, issuer_pays) { if (typeof currency_gets === 'object') { var options = currency_gets; issuer_pays = options.issuer_pays; @@ -1387,45 +1781,71 @@ Remote.prototype.book = function(currency_gets, issuer_gets, currency_pays, issu var gets = Remote.prepareTrade(currency_gets, issuer_gets); var pays = Remote.prepareTrade(currency_pays, issuer_pays); var key = gets + ':' + pays; - var book; - if (!this._books.hasOwnProperty(key)) { - book = new OrderBook(this, currency_gets, issuer_gets, currency_pays, issuer_pays, key); - if (book.is_valid()) { - this._books[key] = book; - } + if (this._books.hasOwnProperty(key)) { + return this._books[key]; } - return this._books[key]; + var book = new OrderBook(this, currency_gets, issuer_gets, currency_pays, issuer_pays, key); + + if (book.is_valid()) { + this._books[key] = book; + } + + return book; }; -// Return the next account sequence if possible. -// <-- undefined or Sequence -Remote.prototype.accountSeq = function(account, advance) { +/** + * Return the next account sequence + * + * @param {String} account + * @param {String} sequence modifier (ADVANCE or REWIND) + * @return {Number} sequence + */ + +Remote.prototype.accountSeq = +Remote.prototype.getAccountSequence = function(account, advance) { var account = UInt160.json_rewrite(account); var accountInfo = this.accounts[account]; - var seq; - if (accountInfo && accountInfo.seq) { - seq = accountInfo.seq; - var change = { ADVANCE: 1, REWIND: -1 }[advance.toUpperCase()] || 0; - accountInfo.seq += change; + if (!accountInfo) { + return; } + var seq = accountInfo.seq; + var change = { ADVANCE: 1, REWIND: -1 }[advance.toUpperCase()] || 0; + + accountInfo.seq += change; + return seq; }; -Remote.prototype.setAccountSeq = function(account, seq) { +/** + * Set account sequence + * + * @param {String} account + * @param {Number} sequence + */ + +Remote.prototype.setAccountSeq = function(account, sequence) { var account = UInt160.json_rewrite(account); if (!this.accounts.hasOwnProperty(account)) { this.accounts[account] = { }; } - this.accounts[account].seq = seq; + this.accounts[account].seq = sequence; }; -// Return a request to refresh accounts[account].seq. +/** + * Refresh an account's sequence from server + * + * @param {String} account + * @param [String|Number] ledger + * @param [Function] callback + * @return {Request} + */ + Remote.prototype.accountSeqCache = function(account, ledger, callback) { if (typeof account === 'object') { var options = account; @@ -1477,20 +1897,28 @@ Remote.prototype.accountSeqCache = function(account, ledger, callback) { return request; }; -// Mark an account's root node as dirty. +/** + * Mark an account's root node as dirty. + * + * @param {String} account + */ + Remote.prototype.dirtyAccountRoot = function(account) { var account = UInt160.json_rewrite(account); delete this.ledgers.current.account_root[account]; }; -// Return a request to get a ripple balance. -// -// --> account: String -// --> issuer: String -// --> currency: String -// --> current: bool : true = current ledger -// -// If does not exist: emit('error', 'error' : 'remoteError', 'remote' : { 'error' : 'entryNotFound' }) +/** + * Get an account's balance + * + * @param {String} account + * @param [String] issuer + * @param [String] currency + * @param [String|Number] ledger + * @param [Function] callback + * @return {Request} + */ + Remote.prototype.requestRippleBalance = function(account, issuer, currency, ledger, callback) { if (typeof account === 'object') { var options = account; @@ -1536,20 +1964,28 @@ Remote.prototype.requestRippleBalance = function(account, issuer, currency, ledg return request; }; -Remote.prepareCurrencies = function(ci) { - var ci_new = { }; +Remote.prepareCurrencies = function(currency) { + var newCurrency = { }; - if (ci.hasOwnProperty('issuer')) { - ci_new.issuer = UInt160.json_rewrite(ci.issuer); + if (currency.hasOwnProperty('issuer')) { + newCurrency.issuer = UInt160.json_rewrite(currency.issuer); } - if (ci.hasOwnProperty('currency')) { - ci_new.currency = Currency.json_rewrite(ci.currency); + if (currency.hasOwnProperty('currency')) { + newCurrency.currency = Currency.json_rewrite(currency.currency); } - return ci_new; + return newCurrency; }; +/** + * Request ripple_path_find + * + * @param {Object} options + * @param [Function] callback + * @return {Request} + */ + Remote.prototype.requestRipplePathFind = function(src_account, dst_account, dst_amount, src_currencies, callback) { if (typeof src_account === 'object') { var options = src_account; @@ -1575,6 +2011,14 @@ Remote.prototype.requestRipplePathFind = function(src_account, dst_account, dst_ return request; }; +/** + * Request path_find/create + * + * @param {Object} options + * @param [Function] callback + * @return {Request} + */ + Remote.prototype.requestPathFindCreate = function(src_account, dst_account, dst_amount, src_currencies, callback) { if (typeof src_account === 'object') { var options = src_account; @@ -1601,22 +2045,46 @@ Remote.prototype.requestPathFindCreate = function(src_account, dst_account, dst_ return request; }; -Remote.prototype.requestPathFindClose = function() { +/** + * Request path_find/close + * + * @param [Function] callback + * @return {Request} + */ + +Remote.prototype.requestPathFindClose = function(callback) { var request = new Request(this, 'path_find'); request.message.subcommand = 'close'; + request.callback(callback); return request; }; +/** + * Request unl_list + * + * @param [Function] callback + * @return {Request} + */ + Remote.prototype.requestUnlList = function(callback) { return new Request(this, 'unl_list').callback(callback); }; -Remote.prototype.requestUnlAdd = function(addr, comment, callback) { +/** + * Request unl_add + * + * @param {String} address + * @param {String} comment + * @param [Function] callback + * @return {Request} + */ + +Remote.prototype.requestUnlAdd = function(address, comment, callback) { var request = new Request(this, 'unl_add'); - request.message.node = addr; + request.message.node = address; if (comment) { // note is not specified anywhere, should remove? @@ -1628,7 +2096,14 @@ Remote.prototype.requestUnlAdd = function(addr, comment, callback) { return request; }; -// --> node: | +/** + * Request unl_delete + * + * @param {String} node + * @param [Function] callback + * @return {Request} + */ + Remote.prototype.requestUnlDelete = function(node, callback) { var request = new Request(this, 'unl_delete'); @@ -1638,10 +2113,26 @@ Remote.prototype.requestUnlDelete = function(node, callback) { return request; }; +/** + * Request peers + * + * @param [Function] callback + * @return {Request} + */ + Remote.prototype.requestPeers = function(callback) { return new Request(this, 'peers').callback(callback); }; +/** + * Request connect + * + * @param {String} ip + * @param {Number} port + * @param [Function] callback + * @return {Request} + */ + Remote.prototype.requestConnect = function(ip, port, callback) { var request = new Request(this, 'connect'); @@ -1656,17 +2147,31 @@ Remote.prototype.requestConnect = function(ip, port, callback) { return request; }; -Remote.prototype.createTransaction = -Remote.prototype.transaction = function(source, options, callback) { +/** + * Create a Transaction + * + * @param {String} source + * @param {Object} options + * @param [Function] callback + * @return {Request} + */ + +Remote.prototype.transaction = +Remote.prototype.createTransaction = function(source, options, callback) { var transaction = new Transaction(this); var transactionTypes = { - payment: 'payment', - accountset: 'accountSet', - trustset: 'trustSet', - offercreate: 'offerCreate', - offercancel: 'offerCancel', - sign: 'sign' + payment: 'payment', + accountset: 'accountSet', + trustset: 'trustSet', + offercreate: 'offerCreate', + offercancel: 'offerCancel', + claim: 'claim', + passwordfund: 'passwordFund', + passwordset: 'passwordSet', + setregularkey: 'setRegularKey', + walletadd: 'walletAdd', + sign: 'sign' }; var transactionType; @@ -1711,6 +2216,7 @@ Remote.prototype.transaction = function(source, options, callback) { * * This takes into account the last known network and local load fees. * + * @param {Number} fee units * @return {Amount} Final fee in XRP for specified number of fee units. */ @@ -1747,6 +2253,9 @@ Remote.prototype.feeTxUnit = function() { * Get the current recommended reserve base. * * Returns the base reserve with load fees and safety margin applied. + * + * @param {Number} owner count + * @return {Amount} */ Remote.prototype.reserve = function(owner_count) { @@ -1759,31 +2268,6 @@ Remote.prototype.reserve = function(owner_count) { return server._reserve(owner_count); }; -Remote.prototype.requestPing = -Remote.prototype.ping = function(host, callback) { - var request = new Request(this, 'ping'); - - switch (typeof host) { - case 'function': - callback = host; - break; - - case 'string': - request.setServer(host); - break; - } - - var then = Date.now(); - - request.once('success', function() { - request.emit('pong', Date.now() - then); - }); - - request.callback(callback, 'pong'); - - return request; -}; - exports.Remote = Remote; // vim:sw=2:sts=2:ts=8:et diff --git a/src/js/ripple/rippletxt.js b/src/js/ripple/rippletxt.js index 98eec5a0..781e1078 100644 --- a/src/js/ripple/rippletxt.js +++ b/src/js/ripple/rippletxt.js @@ -1,65 +1,82 @@ -var request = require('superagent'); - +var superagent = require('superagent'); function RippleTxt() { - this.txts = {}; -} + this.txts = { }; +}; +RippleTxt.urlTemplates = [ + 'https://ripple.{{domain}}/ripple.txt', + 'https://www.{{domain}}/ripple.txt', + 'https://{{domain}}/ripple.txt', + 'http://ripple.{{domain}}/ripple.txt', + 'http://www.{{domain}}/ripple.txt', + 'http://{{domain}}/ripple.txt' +]; + +RippleTxt.request = function() { + return request; +}; + +RippleTxt.prototype.request = function(url, callback) { + return superagent.get(url, callback); +}; /** * Gets the ripple.txt file for the given domain + * * @param {string} domain - Domain to retrieve file from * @param {function} fn - Callback function */ -RippleTxt.prototype.get = function (domain, fn) { + +RippleTxt.prototype.get = function(domain, fn) { var self = this; - + if (self.txts[domain]) { return fn(null, self.txts[domain]); } - - var urls = [ - 'https://ripple.'+domain+'/ripple.txt', - 'https://www.'+domain+'/ripple.txt', - 'https://'+domain+'/ripple.txt', - 'http://ripple.'+domain+'/ripple.txt', - 'http://www.'+domain+'/ripple.txt', - 'http://'+domain+'/ripple.txt' - ].reverse(); - - next(); - function next () { - if (!urls.length) return fn(new Error("No ripple.txt found")); - var url = urls.pop(); - request.get(url, function(err, resp) { - if (err || !resp.text) return next(); - - var sections = self.parse(resp.text); + ;(function nextUrl(i) { + var url = RippleTxt.urlTemplates[i]; + + if (!url) { + return fn(new Error('No ripple.txt found')); + } + + url = url.replace('{{domain}}', domain); + + self.request(url, function(err, resp) { + if (err || !resp.text) { + return nextUrl(++i); + } + + var sections = self.parse(resp.text); self.txts[domain] = sections; - fn(null, sections); - }); - } -}; + fn(null, sections); + }); + })(0); +}; /** * Parse a ripple.txt file + * * @param {string} txt - Unparsed ripple.txt data - */ -RippleTxt.prototype.parse = function (txt) { - - txt = txt.replace('\r\n', '\n'); - txt = txt.replace('\r', '\n'); - txt = txt.split('\n'); + */ + +RippleTxt.prototype.parse = function(txt) { + var txt = txt.replace(/\r?\n/g, '\n').split('\n') + var currentSection = ''; + var sections = { }; - var currentSection = "", sections = {}; for (var i = 0, l = txt.length; i < l; i++) { var line = txt[i]; + if (!line.length || line[0] === '#') { continue; - } else if (line[0] === '[' && line[line.length-1] === ']') { - currentSection = line.slice(1, line.length-1); + } + + if (line[0] === '[' && line[line.length - 1] === ']') { + currentSection = line.slice(1, line.length - 1); sections[currentSection] = []; } else { line = line.replace(/^\s+|\s+$/g, ''); @@ -72,4 +89,4 @@ RippleTxt.prototype.parse = function (txt) { return sections; }; -module.exports.RippleTxt = RippleTxt; \ No newline at end of file +exports.RippleTxt = RippleTxt; diff --git a/src/js/ripple/server.js b/src/js/ripple/server.js index 198e0810..4b9df6ab 100644 --- a/src/js/ripple/server.js +++ b/src/js/ripple/server.js @@ -7,17 +7,19 @@ var log = require('./log').internal.sub('server'); /** * @constructor Server + * * @param {Remote} Reference to a Remote object * @param {Object} Options - * - * host: String - * port: String or Number - * secure: Boolean + * @param {String} host + * @param {Number|String} port + * @param [Boolean] securec */ function Server(remote, opts) { EventEmitter.call(this); + var self = this; + if (typeof opts === 'string') { var parsedUrl = url.parse(opts); opts = { @@ -31,16 +33,6 @@ function Server(remote, opts) { throw new TypeError('Server configuration is not an Object'); } - if (!opts.host) { - opts.host = opts.websocket_ip; - } - if (!opts.port) { - opts.port = opts.websocket_port; - } - if (!opts.secure) { - opts.secure = opts.websocket_ssl; - } - if (isNaN(opts.port)) { throw new TypeError('Server port must be a number'); } @@ -50,7 +42,7 @@ function Server(remote, opts) { } if (typeof opts.secure !== 'boolean') { - opts.secure = false; + opts.secure = true; } // We want to allow integer strings as valid port numbers for backward compatibility @@ -66,52 +58,56 @@ function Server(remote, opts) { throw new TypeError('Server "secure" configuration is not a Boolean'); } - var self = this; + this._remote = remote; + this._opts = opts; + this._ws = void(0); - this._remote = remote; - this._opts = opts; - this._host = opts.host; - this._port = opts.port; - this._secure = opts.secure; - this._ws = void(0); this._connected = false; this._shouldConnect = false; this._state = 'offline'; - this._id = 0; - this._retry = 0; - this._requests = { }; - this._load_base = 256; - this._load_factor = 256; + + this._id = 0; + this._retry = 0; + this._requests = { }; + + this._load_base = 256; + this._load_factor = 256; + + this._fee = 10; this._fee_ref = 10; this._fee_base = 10; this._reserve_base = void(0); this._reserve_inc = void(0); this._fee_cushion = this._remote.fee_cushion; - this._opts.url = (opts.secure ? 'wss://' : 'ws://') + opts.host + ':' + opts.port; + this._lastLedgerIndex = NaN; + this._lastLedgerClose = NaN; - this.on('message', function(message) { + this._score = 0; + + this._scoreWeights = { + ledgerclose: 5, + response: 1 + }; + + this._url = this._opts.url = (this._opts.secure ? 'wss://' : 'ws://') + + this._opts.host + ':' + this._opts.port; + + this.on('message', onMessage); + + function onMessage(message) { self._handleMessage(message); - }); + }; - this.on('response_subscribe', function(message) { + this.on('response_subscribe', onSubscribeResponse); + + function onSubscribeResponse(message) { self._handleResponseSubscribe(message); - }); - - function checkServerActivity() { - if (isNaN(self._lastLedgerClose)) { - return; - } - - var delta = (Date.now() - self._lastLedgerClose); - - if (delta > (1000 * 20)) { - self.reconnect(); - } }; function setActivityInterval() { - self._activityInterval = setInterval(checkServerActivity, 1000); + var interval = self._checkActivity.bind(self); + self._activityInterval = setInterval(interval, 1000); }; this.on('disconnect', function onDisconnect() { @@ -119,8 +115,18 @@ function Server(remote, opts) { //self.once('ledger_closed', setActivityInterval); }); - this.once('ledger_closed', function() { - //setActiviyInterval(); + //this.once('ledger_closed', setActivityInterval); + + this._remote.on('ledger_closed', function(ledger) { + self._updateScore('ledgerclose', ledger); + }); + + this.on('response_ping', function(message, request) { + self._updateScore('response', request); + }); + + this.on('load_changed', function(load) { + self._updateScore('loadchange', load); }); }; @@ -143,6 +149,23 @@ Server.onlineStates = [ 'full' ]; +/** + * This is the final interface between client code and a socket connection to a + * `rippled` server. As such, this is a decent hook point to allow a WebSocket + * interface conforming object to be used as a basis to mock rippled. This + * avoids the need to bind a websocket server to a port and allows a more + * synchronous style of code to represent a client <-> server message sequence. + * We can also use this to log a message sequence to a buffer. + * + * @api private + */ + +Server.websocketConstructor = function() { + // We require this late, because websocket shims may be loaded after + // ripple-lib in the browser + return require('ws'); +}; + /** * Set server state * @@ -172,6 +195,76 @@ Server.prototype._setState = function(state) { } }; +/** + * Check that server is still active. + * + * Server activity is determined by ledger_closed events. + * Maximum delay to receive a ledger_closed event is 20s. + * + * If server is inactive, reconnect + * + * @api private + */ + +Server.prototype._checkActivity = function() { + if (!this._connected) { + return; + } + + if (isNaN(this._lastLedgerClose)) { + return; + } + + var delta = (Date.now() - this._lastLedgerClose); + + if (delta > (1000 * 25)) { + //this.reconnect(); + } +}; + +/** + * Server maintains a score for request prioritization. + * + * The score is determined by various data including + * this server's lag to receive ledger_closed events, + * ping response time, and load(fee) change + * + * @param {String} type + * @param {Object} data + * @api private + */ + +Server.prototype._updateScore = function(type, data) { + if (!this._connected) { + return; + } + + var weight = this._scoreWeights[type] || 1; + + switch (type) { + case 'ledgerclose': + // Ledger lag + var delta = data.ledger_index - this._lastLedgerIndex; + if (delta > 0) { + this._score += weight * delta; + } + break; + case 'response': + // Ping lag + var delta = Math.floor((Date.now() - data.time) / 200); + this._score += weight * delta; + break; + case 'loadchange': + // Load/fee change + this._fee = Number(this._computeFee(10)); + break; + } + + if (this._score > 1e3) { + //this.reconnect(); + } +}; + /** * Get the remote address for a server. * Incompatible with ripple-lib client build @@ -186,22 +279,6 @@ Server.prototype._remoteAddress = function() { return address; }; -/** This is the final interface between client code and a socket connection to a - * `rippled` server. As such, this is a decent hook point to allow a WebSocket - * interface conforming object to be used as a basis to mock rippled. This - * avoids the need to bind a websocket server to a port and allows a more - * synchronous style of code to represent a client <-> server message sequence. - * We can also use this to log a message sequence to a buffer. - * - * @api private - */ - -Server.websocketConstructor = function() { - // We require this late, because websocket shims may be loaded after - // ripple-lib in the browser - return require('ws'); -}; - /** * Disconnect from rippled WebSocket server * @@ -274,7 +351,6 @@ Server.prototype.connect = function() { }; ws.onopen = function onOpen() { - // If we are no longer the active socket, simply ignore any event if (ws === self._ws) { self.emit('socket_open'); // Subscribe to events @@ -283,7 +359,6 @@ Server.prototype.connect = function() { }; ws.onerror = function onError(e) { - // If we are no longer the active socket, simply ignore any event if (ws === self._ws) { self.emit('socket_error'); @@ -309,9 +384,7 @@ Server.prototype.connect = function() { } }; - // Failure to open. ws.onclose = function onClose() { - // If we are no longer the active socket, simply ignore any event if (ws === self._ws) { if (self._remote.trace) { log.info('onclose:', self._opts.url, ws.readyState); @@ -333,12 +406,16 @@ Server.prototype._retryConnect = function() { this._retry += 1; var retryTimeout = (this._retry < 40) - ? (1000 / 20) // First, for 2 seconds: 20 times per second + // First, for 2 seconds: 20 times per second + ? (1000 / 20) : (this._retry < 40 + 60) - ? (1000) // Then, for 1 minute: once per second + // Then, for 1 minute: once per second + ? (1000) : (this._retry < 40 + 60 + 60) - ? (10 * 1000) // Then, for 10 minutes: once every 10 seconds - : (30 * 1000); // Then: once every 30 seconds + // Then, for 10 minutes: once every 10 seconds + ? (10 * 1000) + // Then: once every 30 seconds + : (30 * 1000); function connectionRetry() { if (self._shouldConnect) { @@ -365,8 +442,10 @@ Server.prototype._handleClose = function() { this.emit('socket_close'); this._setState('offline'); + function noOp() {}; + // Prevent additional events from this socket - ws.onopen = ws.onerror = ws.onclose = ws.onmessage = function noOp() {}; + ws.onopen = ws.onerror = ws.onclose = ws.onmessage = noOp; if (self._shouldConnect) { this._retryConnect(); @@ -389,78 +468,125 @@ Server.prototype._handleMessage = function(message) { } if (!Server.isValidMessage(message)) { + this.emit('unexpected', message); return; } switch (message.type) { case 'ledgerClosed': - this._lastLedgerClose = Date.now(); - this.emit('ledger_closed', message); + this._handleLedgerClosed(message); break; - case 'serverStatus': - // This message is only received when online. - // As we are connected, it is the definitive final state. - - this._setState(~(Server.onlineStates.indexOf(message.server_status)) ? 'online' : 'offline'); - - if (Server.isLoadStatus(message)) { - self.emit('load', message, self); - self._remote.emit('load', message, self); - - if (message.load_base !== self._load_base || message.load_factor !== self._load_factor) { - // Load changed - self._load_base = message.load_base; - self._load_factor = message.load_factor; - self.emit('load_changed', message, self); - self._remote.emit('load_changed', message, self); - } - } + this._handleServerStatus(message); break; - case 'response': - // A response to a request. - var request = self._requests[message.id]; - delete self._requests[message.id]; - - if (!request) { - if (this._remote.trace) { - log.info('UNEXPECTED:', self._opts.url, message); - } - return; - } - - if (message.status === 'success') { - if (this._remote.trace) { - log.info('response:', self._opts.url, message); - } - - request.emit('success', message.result); - - [ self, self._remote ].forEach(function(emitter) { - emitter.emit('response_' + request.message.command, message.result, request, message); - }); - } else if (message.error) { - if (this._remote.trace) { - log.info('error:', self._opts.url, message); - } - - request.emit('error', { - error: 'remoteError', - error_message: 'Remote reported an error.', - remote: message - }); - } + this._handleResponse(message); break; - case 'path_find': - if (this._remote.trace) { - log.info('path_find:', self._opts.url, message); - } + this._handlePathFind(message); break; } }; +Server.prototype._handleLedgerClosed = function(message) { + this._lastLedgerIndex = message.ledger_index; + this._lastLedgerClose = Date.now(); + this.emit('ledger_closed', message); +}; + +Server.prototype._handleServerStatus = function(message) { + // This message is only received when online. + // As we are connected, it is the definitive final state. + var isOnline = ~Server.onlineStates.indexOf(message.server_status); + this._setState(isOnline ? 'online' : 'offline'); + + if (!Server.isLoadStatus(message)) { + return; + } + + this.emit('load', message, this); + this._remote.emit('load', message, this); + + var loadChanged = message.load_base !== this._load_base + || message.load_factor !== this._load_factor + + if (loadChanged) { + this._load_base = message.load_base; + this._load_factor = message.load_factor; + this.emit('load_changed', message, this); + this._remote.emit('load_changed', message, this); + } +}; + +Server.prototype._handleResponse = function(message) { + // A response to a request. + var request = this._requests[message.id]; + + delete this._requests[message.id]; + + if (!request) { + if (this._remote.trace) { + log.info('UNEXPECTED:', this._opts.url, message); + } + return; + } + + if (message.status === 'success') { + if (this._remote.trace) { + log.info('response:', this._opts.url, message); + } + + var command = request.message.command; + var result = message.result; + var responseEvent = 'response_' + command; + + request.emit('success', result); + + [ this, this._remote ].forEach(function(emitter) { + emitter.emit(responseEvent, result, request, message); + }); + } else if (message.error) { + if (this._remote.trace) { + log.info('error:', this._opts.url, message); + } + + var error = { + error: 'remoteError', + error_message: 'Remote reported an error.', + remote: message + }; + + request.emit('error', error); + } +}; + +Server.prototype._handlePathFind = function(message) { + if (this._remote.trace) { + log.info('path_find:', this._opts.url, message); + } +}; + +/** + * Handle subscription response messages. Subscription response + * messages indicate that a connection to the server is ready + * + * @api private + */ + +Server.prototype._handleResponseSubscribe = function(message) { + if (~(Server.onlineStates.indexOf(message.server_status))) { + this._setState('online'); + } + if (Server.isLoadStatus(message)) { + this._load_base = message.load_base || 256; + this._load_factor = message.load_factor || 256; + this._fee_ref = message.fee_ref; + this._fee_base = message.fee_base; + this._reserve_base = message.reserve_base; + this._reserve_inc = message.reserve_inc; + } +}; + /** * Check that received message from rippled is valid * @@ -484,27 +610,6 @@ Server.isLoadStatus = function(message) { && (typeof message.load_factor === 'number'); }; -/** - * Handle subscription response messages. Subscription response - * messages indicate that a connection to the server is ready - * - * @api private - */ - -Server.prototype._handleResponseSubscribe = function(message) { - if (~(Server.onlineStates.indexOf(message.server_status))) { - this._setState('online'); - } - if (Server.isLoadStatus(message)) { - this._load_base = message.load_base || 256; - this._load_factor = message.load_factor || 256; - this._fee_ref = message.fee_ref; - this._fee_base = message.fee_base; - this._reserve_base = message.reserve_base; - this._reserve_inc = message.reserve_inc; - } -}; - /** * Send JSON message to rippled WebSocket server * @@ -544,6 +649,7 @@ Server.prototype._request = function(request) { request.server = this; request.message.id = this._id; + request.time = Date.now(); this._requests[request.message.id] = request; @@ -564,8 +670,8 @@ Server.prototype._request = function(request) { Server.prototype._isConnected = function(request) { var isSubscribeRequest = request - && request.message.command === 'subscribe' - && this._ws.readyState === 1; + && request.message.command === 'subscribe' + && this._ws.readyState === 1; return this._connected || (this._ws && isSubscribeRequest); }; diff --git a/src/js/ripple/vaultclient.js b/src/js/ripple/vaultclient.js index d1f03252..41ed310a 100644 --- a/src/js/ripple/vaultclient.js +++ b/src/js/ripple/vaultclient.js @@ -1,229 +1,271 @@ -var AuthInfo = require('./authinfo').AuthInfo; +var async = require('async'); var blobClient = require('./blob').BlobClient; +var AuthInfo = require('./authinfo').AuthInfo; var crypt = require('./crypt').Crypt; - function VaultClient(opts) { - if (!opts) opts = {}; - else if (typeof opts === "string") opts = {domain:opts}; - - this.domain = opts.domain || 'ripple.com'; - this.authInfo = new AuthInfo(); - this.infos = {}; -} - - -/** - * Reduce username to standardized form. - * Strips whitespace at beginning and end. - * @param {string} username - Username to normalize - */ -VaultClient.prototype.normalizeUsername = function (username) { - username = ""+username; - username = username.trim(); - return username; + if (!opts) { + opts = { }; + } + + if (typeof opts === 'string') { + opts = { domain: opts }; + } + + this.domain = opts.domain || 'ripple.com'; + this.authInfo = new AuthInfo(); + this.infos = { }; }; - -/** - * Reduce password to standardized form. - * Strips whitespace at beginning and end. - * @param {string} password - password to normalize - */ -VaultClient.prototype.normalizePassword = function (password) { - password = ""+password; - password = password.trim(); - return password; -}; - - /** * Get a ripple name from a given account address, if it has one * @param {string} address - Account address to query * @param {string} url - Url of blob vault */ -VaultClient.prototype.getRippleName = function(address, url, fn) { +VaultClient.prototype.getRippleName = function(address, url, callback) { //use the url from previously retrieved authInfo, if necessary - if (!url) return fn(new Error("Blob vault URL is required")); - blobClient.getRippleName(url, address, fn); + if (!url) { + callback(new Error('Blob vault URL is required')); + } else { + blobClient.getRippleName(url, address, callback); + } }; - /** * Authenticate and retrieve a decrypted blob using a ripple name and password + * * @param {string} username * @param {string} password * @param {function} fn - Callback function */ -VaultClient.prototype.login = function(username, password, fn) { + +VaultClient.prototype.login = function(username, password, callback) { var self = this; - - self.authInfo.get(self.domain, username, function(err, authInfo){ - if (err) return fn(err); - - if (authInfo.version !== 3) { - return fn(new Error("This wallet is incompatible with this version of the vault-client.")); - } - - if (!authInfo.pakdf) { - return fn(new Error("No settings for PAKDF in auth packet.")); - } - if (!authInfo.exists) { - return fn(new Error("User does not exist.")); - } + function getAuthInfo(callback) { + self.authInfo.get(self.domain, username, function(err, authInfo) { + if (err) { + return callback(err); + } - if ("string" !== typeof authInfo.blobvault) { - return fn(new Error("No blobvault specified in the authinfo.")); - } - - + if (authInfo.version !== 3) { + return callback(new Error('This wallet is incompatible with this version of the vault-client.')); + } + + if (!authInfo.pakdf) { + return callback(new Error('No settings for PAKDF in auth packet.')); + } + + if (!authInfo.exists) { + return callback(new Error('User does not exist.')); + } + + if (typeof authInfo.blobvault !== 'string') { + return callback(new Error('No blobvault specified in the authinfo.')); + } + + callback(null, authInfo); + }); + }; + + function deriveLoginKeys(authInfo, callback) { //derive login keys - crypt.derive(authInfo.pakdf, 'login', username.toLowerCase(), password, function(err, keys){ - if (err) return fn(err); - - blobClient.get(authInfo.blobvault, keys.id, keys.crypt, function (err, blob) { - if (err) return fn(err); - - self.infos[keys.id] = authInfo; //save for relogin - - fn (null, { - blob : blob, - username : authInfo.username, - verified : authInfo.emailVerified - }); + crypt.derive(authInfo.pakdf, 'login', username.toLowerCase(), password, function(err, keys) { + if (err) { + callback(err); + } else { + callback(null, authInfo, keys); + } + }); + }; + + function getBlob(authInfo, keys, callback) { + blobClient.get(authInfo.blobvault, keys.id, keys.crypt, function(err, blob) { + if (err) { + return callback(err); + } + + //save for relogin + self.infos[keys.id] = authInfo; + + callback(null, { + blob: blob, + username: authInfo.username, + verified: authInfo.emailVerified }); }); - }); -}; + }; + var steps = [ + getAuthInfo, + deriveLoginKeys, + getBlob + ]; + + async.waterfall(steps, callback); +}; /** * Retreive and decrypt blob using a blob url, id and crypt derived previously. + * * @param {string} url - Blob vault url * @param {string} id - Blob id from previously retreived blob * @param {string} key - Blob decryption key * @param {function} fn - Callback function */ -VaultClient.prototype.relogin = function(url, id, key, fn) { - + +VaultClient.prototype.relogin = function(url, id, key, callback) { //use the url from previously retrieved authInfo, if necessary - if (!url && this.infos[id]) url = this.infos[id].blobvault; - - if (!url) return fn(new Error("Blob vault URL is required")); - - blobClient.get(url, id, key, function (err, blob) { - if (err) return fn(err); - - fn (null, { - blob : blob, - }); + if (!url && this.infos[id]) { + url = this.infos[id].blobvault; + } + + if (!url) { + return callback(new Error('Blob vault URL is required')); + } + + blobClient.get(url, id, key, function(err, blob) { + if (err) { + callback(err); + } else { + callback (null, { blob: blob }); + } }); }; - /** * Decrypt the secret key using a username and password + * * @param {string} username * @param {string} password * @param {string} encryptSecret * @param {function} fn - Callback function */ -VaultClient.prototype.unlock = function(username, password, encryptSecret, fn) { + +VaultClient.prototype.unlock = function(username, password, encryptSecret, callback) { var self = this; - - self.authInfo.get(self.domain, username, function(err, authInfo){ - if (err) return fn(err); - + + function deriveUnlockKey(authInfo, callback) { //derive unlock key - crypt.derive(authInfo.pakdf, 'unlock', username.toLowerCase(), password, function(err, keys){ - if (err) return fn(err); - - fn(null, { - keys : keys, - secret : crypt.decrypt(keys.unlock, encryptSecret) - }); - }); + crypt.derive(authInfo.pakdf, 'unlock', username.toLowerCase(), password, function(err, keys) { + if (err) { + return callback(err); + } + + callback(null, { + keys: keys, + secret: crypt.decrypt(keys.unlock, encryptSecret) + }); + }); + }; + + self.authInfo.get(self.domain, username, function(err, authInfo) { + if (err) { + callback(err); + } else { + deriveUnlockKey(authInfo, callback); + } }); }; - /** * Retrieve the decrypted blob and secret key in one step using * the username and password + * * @param {string} username * @param {string} password * @param {function} fn - Callback function */ -VaultClient.prototype.loginAndUnlock = function(username, password, fn) { + +VaultClient.prototype.loginAndUnlock = function(username, password, callback) { var self = this; - - this.login(username, password, function(err, resp){ - if (err) return fn(err); - - if (!resp.blob || !resp.blob.encrypted_secret) - return fn(new Error("Unable to retrieve blob and secret.")); - - if (!resp.blob.id || !resp.blob.key) - return fn(new Error("Unable to retrieve keys.")); - - //get authInfo via id - would have been saved from login - var authInfo = self.infos[resp.blob.id]; - if (!authInfo) return fn(new Error("Unable to find authInfo")); - + + function deriveUnlockKey(authInfo, blob, callback) { //derive unlock key - crypt.derive(authInfo.pakdf, 'unlock', username.toLowerCase(), password, function(err, keys){ - if (err) return fn(err); - - fn(null, { - blob : resp.blob, - unlock : keys.unlock, - secret : crypt.decrypt(keys.unlock, resp.blob.encrypted_secret), - username : authInfo.username, - verified : authInfo.emailVerified + crypt.derive(authInfo.pakdf, 'unlock', username.toLowerCase(), password, function(err, keys) { + if (err) { + return callback(err); + } + + callback(null, { + blob: blob, + unlock: keys.unlock, + secret: crypt.decrypt(keys.unlock, blob.encrypted_secret), + username: authInfo.username, + verified: authInfo.emailVerified }); - }); + }); + }; + + this.login(username, password, function(err, resp) { + if (err) { + return callback(err); + } + + if (!resp.blob || !resp.blob.encrypted_secret) { + return callback(new Error('Unable to retrieve blob and secret.')); + } + + if (!resp.blob.id || !resp.blob.key) { + return callback(new Error('Unable to retrieve keys.')); + } + + //get authInfo via id - would have been saved from login + var authInfo = self.infos[resp.blob.id]; + + if (!authInfo) { + return callback(new Error('Unable to find authInfo')); + } + + deriveUnlockKey(authInfo, resp.blob, callback); }); }; - /** * Check blobvault for existance of username + * * @param {string} username * @param {function} fn - Callback function */ -VaultClient.prototype.exists = function (username, fn) { - this.authInfo.get(this.domain, username.toLowerCase(), function(err, authInfo){ - if (err) return fn(err); - return fn(null, !!authInfo.exists); + +VaultClient.prototype.exists = function(username, callback) { + this.authInfo.get(this.domain, username.toLowerCase(), function(err, authInfo) { + if (err) { + callback(err); + } else { + callback(null, !!authInfo.exists); + } }); }; - -/* +/** * Verify an email address for an existing user + * * @param {string} username * @param {string} token - Verification token - * @param {function} fn - Callback function + * @param {function} fn - Callback function */ -VaultClient.prototype.verify = function (username, token, fn) { - - this.authInfo.get(this.domain, username.toLowerCase(), function (err, authInfo) { - if (err) return fn(err); - if ("string" !== typeof authInfo.blobvault) { - return fn(new Error("No blobvault specified in the authinfo.")); +VaultClient.prototype.verify = function(username, token, callback) { + var self = this; + + this.authInfo.get(this.domain, username.toLowerCase(), function(err, authInfo) { + if (err) { + return callback(err); } - blobClient.verify(authInfo.blobvault, username.toLowerCase(), token, fn); - }); + if (typeof authInfo.blobvault !== 'string') { + return callback(new Error('No blobvault specified in the authinfo.')); + } + + blobClient.verify(authInfo.blobvault, username.toLowerCase(), token, callback); + }); }; - -/* +/** * Register a new user and save to the blob vault - * + * * @param {object} options * @param {string} options.username * @param {string} options.password @@ -233,53 +275,77 @@ VaultClient.prototype.verify = function (username, token, fn) { * @param {object} options.oldUserBlob //optional * @param {function} fn */ -VaultClient.prototype.register = function (options, fn) { - var self = this, - username = this.normalizeUsername(options.username), - password = this.normalizePassword(options.password); - - self.authInfo.get(self.domain, username, function(err, authInfo){ - - if (err) return fn(err); - - if ("string" !== typeof authInfo.blobvault) { - return fn(new Error("No blobvault specified in the authinfo.")); - } - - - if (!authInfo.pakdf) { - return fn(new Error("No settings for PAKDF in auth packet.")); - } - - //derive login keys - crypt.derive(authInfo.pakdf, 'login', username.toLowerCase(), password, function(err, loginKeys){ - if (err) return fn(err); - - //derive unlock key - crypt.derive(authInfo.pakdf, 'unlock', username.toLowerCase(), password, function(err, unlockKeys){ - if (err) return fn(err); - - var params = { - 'url' : authInfo.blobvault, - 'id' : loginKeys.id, - 'crypt' : loginKeys.crypt, - 'unlock' : unlockKeys.unlock, - 'username' : username, - 'email' : options.email, - 'masterkey' : options.masterkey || crypt.createMaster(), - 'activateLink' : options.activateLink, - 'oldUserBlob' : options.oldUserBlob - }; - - blobClient.create(params, function(err, blob){ - if (err) return fn(err); - fn(null, blob, loginKeys, authInfo.username); - }); - }); +VaultClient.prototype.register = function(options, fn) { + var self = this; + var username = String(options.username).trim(); + var password = String(options.password).trim(); + + function getAuthInfo(callback) { + self.authInfo.get(self.domain, username, function(err, authInfo) { + if (err) { + return callback(err); + } + + if (typeof authInfo.blobvault !== 'string') { + return callback(new Error('No blobvault specified in the authinfo.')); + } + + if (!authInfo.pakdf) { + return callback(new Error('No settings for PAKDF in auth packet.')); + } + + callback(null, authInfo); }); - }); + }; + + function deriveKeys(authInfo, callback) { + // derive unlock and login keys + var keys = { }; + + function deriveKey(keyType, callback) { + crypt.derive(authInfo.pakdf, keyType, username.toLowerCase(), password, function(err, key) { + if (err) { + callback(err); + } else { + keys[keyType] = key; + callback(); + } + }); + }; + + async.eachSeries([ 'login', 'unlock' ], deriveKey, function(err) { + if (err) { + callback(err); + } else { + callback(null, authInfo, keys); + } + }); + }; + + function create(authInfo, keys) { + var params = { + url: authInfo.blobvault, + id: keys.loginKeys.id, + crypt: keys.loginKeys.crypt, + unlock: keys.unlockKeys.unlock, + username: username, + email: options.email, + masterkey: options.masterkey || crypt.createMaster(), + activateLink: options.activateLink, + oldUserBlob: options.oldUserBlob + }; + + blobClient.create(params, function(err, blob) { + if (err) { + callback(err); + } else { + callback(null, blob, loginKeys, authInfo.username); + } + }); + }; + + async.waterfall([ getAuthInfo, deriveKeys ], create); }; - -module.exports.VaultClient = VaultClient; +exports.VaultClient = VaultClient; diff --git a/test/message-test.js b/test/message-test.js index c6e8d29d..c0a76801 100644 --- a/test/message-test.js +++ b/test/message-test.js @@ -184,14 +184,12 @@ describe('Message', function(){ account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz' }; - Remote.prototype.addServer = function(){}; - var test_remote = new Remote({}); + //Remote.prototype.addServer = function(){}; + var test_remote = new Remote(); assert.throws(function(){ Message.verifyHashSignature(data); }, /(?=.*callback\ function).*/); - - }); it('should respond with an error if the hash is missing or invalid', function(done){ @@ -202,8 +200,8 @@ describe('Message', function(){ account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz' }; - Remote.prototype.addServer = function(){}; - var test_remote = new Remote({}); + //Remote.prototype.addServer = function(){}; + var test_remote = new Remote(); test_remote.state = 'online'; Message.verifyHashSignature(data, test_remote, function(err, valid){ @@ -221,8 +219,8 @@ describe('Message', function(){ signature: 'AAAAHOUJQzG/7BO82fGNt1TNE+GGVXKuQQ0N2nTO+iJETE69PiHnaAkkOzovM177OosxbKjpt3KvwuJflgUB2YGvgjk=' }; - Remote.prototype.addServer = function(){}; - var test_remote = new Remote({}); + //Remote.prototype.addServer = function(){}; + var test_remote = new Remote(); test_remote.state = 'online'; Message.verifyHashSignature(data, test_remote, function(err, valid){ @@ -240,8 +238,8 @@ describe('Message', function(){ account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz' }; - Remote.prototype.addServer = function(){}; - var test_remote = new Remote({}); + //Remote.prototype.addServer = function(){}; + var test_remote = new Remote(); test_remote.state = 'online'; Message.verifyHashSignature(data, test_remote, function(err, valid){ @@ -260,8 +258,8 @@ describe('Message', function(){ signature: 'AAAAHMIPCQGLgdnpX1Ccv1wHb56H4NggxIM6U08Qkb9mUjN2Vn9pZ3CHvq1yWLBi6NqpW+7kedLnmfu4VG2+y43p4Xs=' }; - Remote.prototype.addServer = function(){}; - var test_remote = new Remote({}); + //Remote.prototype.addServer = function(){}; + var test_remote = new Remote(); test_remote.state = 'online'; test_remote.request_account_info = function(account, callback) { if (account === data.account) { @@ -296,8 +294,8 @@ describe('Message', function(){ signature: 'AAAAG+dB/rAjZ5m8eQ/opcqQOJsFbKxOu9jq9KrOAlNO4OdcBDXyCBlkZqS9Xr8oZI2uh0boVsgYOS3pOLJz+Dh3Otk=' }; - Remote.prototype.addServer = function(){}; - var test_remote = new Remote({}); + //Remote.prototype.addServer = function(){}; + var test_remote = new Remote(); test_remote.state = 'online'; test_remote.request_account_info = function(account, callback) { if (account === data.account) { @@ -324,4 +322,4 @@ describe('Message', function(){ }); -}); \ No newline at end of file +}); diff --git a/test/remote-test.js b/test/remote-test.js index c5ab85a1..5c29d6ec 100644 --- a/test/remote-test.js +++ b/test/remote-test.js @@ -5,40 +5,182 @@ var Remote = utils.load_module('remote').Remote; var Server = utils.load_module('server').Server; var Request = utils.load_module('request').Request; -var options, spy, mock, stub, remote, callback; +var options, spy, mock, stub, remote, callback, database, tx; describe('Remote', function () { - describe('initialing a remote with options', function () { - beforeEach(function () { - options = { - trace : true, - trusted: true, - local_signing: true, - servers: [ - { host: 's-west.ripple.com', port: 443, secure: true }, - { host: 's-east.ripple.com', port: 443, secure: true } - ], + beforeEach(function () { + options = { + trace : true, + trusted: true, + local_signing: true, - blobvault : 'https://blobvault.payward.com', - persistent_auth : false, - transactions_per_page: 50, + servers: [ + { host: 's-west.ripple.com', port: 443, secure: true }, + { host: 's-east.ripple.com', port: 443, secure: true } + ], - bridge: { - out: { - // 'bitcoin': 'localhost:3000' - // 'bitcoin': 'https://www.bitstamp.net/ripple/bridge/out/bitcoin/' - } - }, + blobvault : 'https://blobvault.payward.com', + persistent_auth : false, + transactions_per_page: 50, - }; - }) - it('should add a server for each specified', function (done) { - var remote = new Remote(options); - done(); - }) + bridge: { + out: { + // 'bitcoin': 'localhost:3000' + // 'bitcoin': 'https://www.bitstamp.net/ripple/bridge/out/bitcoin/' + } + }, + + }; }) - describe('functions that return request objects', function () { + describe('remote server initialization - url object', function() { + it('should construct url', function (done) { + var remote = new Remote({ + servers: [ { host: 's-west.ripple.com', port: 443, secure: true } ], + }); + assert(Array.isArray(remote._servers)); + assert(remote._servers[0] instanceof Server); + assert.strictEqual(remote._servers[0]._url, 'wss://s-west.ripple.com:443'); + done(); + }) + }); + + describe('remote server initialization - url object - no secure property', function() { + it('should construct url', function (done) { + var remote = new Remote({ + servers: [ { host: 's-west.ripple.com', port: 443 } ] + }); + assert(Array.isArray(remote._servers)); + assert(remote._servers[0] instanceof Server); + assert.strictEqual(remote._servers[0]._url, 'wss://s-west.ripple.com:443'); + done(); + }) + }); + + describe('remote server initialization - url object - secure: false', function() { + it('should construct url', function (done) { + var remote = new Remote({ + servers: [ { host: 's-west.ripple.com', port: 443, secure: false } ] + }); + assert(Array.isArray(remote._servers)); + assert(remote._servers[0] instanceof Server); + assert.strictEqual(remote._servers[0]._url, 'ws://s-west.ripple.com:443'); + done(); + }) + }); + + describe('remote server initialization - url object - string port', function() { + it('should construct url', function (done) { + var remote = new Remote({ + servers: [ { host: 's-west.ripple.com', port: '443', secure: true } ] + }); + assert(Array.isArray(remote._servers)); + assert(remote._servers[0] instanceof Server); + assert.strictEqual(remote._servers[0]._url, 'wss://s-west.ripple.com:443'); + done(); + }) + }); + + describe('remote server initialization - url object - invalid host', function() { + it('should construct url', function (done) { + assert.throws( + function() { + var remote = new Remote({ + servers: [ { host: '+', port: 443, secure: true } ] + }); + }, Error); + done(); + }) + }); + + describe('remote server initialization - url object - invalid port', function() { + it('should construct url', function (done) { + assert.throws( + function() { + var remote = new Remote({ + servers: [ { host: 's-west.ripple.com', port: null, secure: true } ] + }); + }, TypeError); + done(); + }) + }); + + describe('remote server initialization - url object - port out of range', function() { + it('should construct url', function (done) { + assert.throws( + function() { + var remote = new Remote({ + servers: [ { host: 's-west.ripple.com', port: 65537, secure: true } ] + }); + }, Error); + done(); + }) + }); + + describe('remote server initialization - url string', function() { + it('should construct url', function (done) { + var remote = new Remote({ + servers: [ 'wss://s-west.ripple.com:443' ] + }); + assert(Array.isArray(remote._servers)); + assert(remote._servers[0] instanceof Server); + assert.strictEqual(remote._servers[0]._url, 'wss://s-west.ripple.com:443'); + done(); + }) + }); + + describe('remote server initialization - url string - ws://', function() { + it('should construct url', function (done) { + var remote = new Remote({ + servers: [ 'ws://s-west.ripple.com:443' ] + }); + assert(Array.isArray(remote._servers)); + assert(remote._servers[0] instanceof Server); + assert.strictEqual(remote._servers[0]._url, 'ws://s-west.ripple.com:443'); + done(); + }) + }); + + describe('remote server initialization - url string - invalid host', function() { + it('should construct url', function (done) { + assert.throws( + function() { + var remote = new Remote({ + servers: [ 'ws://+:443' ] + }); + }, Error + ); + done(); + }) + }); + + describe('remote server initialization - url string - invalid port', function() { + it('should construct url', function (done) { + assert.throws( + function() { + var remote = new Remote({ + servers: [ 'ws://s-west.ripple.com:null' ] + }); + }, Error + ); + done(); + }) + }); + + describe('remote server initialization - url string - port out of range', function() { + it('should construct url', function (done) { + assert.throws( + function() { + var remote = new Remote({ + servers: [ 'ws://s-west.ripple.com:65537:' ] + }); + }, Error + ); + done(); + }) + }); + + describe('request constructors', function () { beforeEach(function () { callback = function () {} remote = new Remote(options); @@ -100,4 +242,91 @@ describe('Remote', function () { }); }); }) + + describe('create remote and get pending transactions', function() { + before(function() { + tx = [{ + tx_json: { + Account : "r4qLSAzv4LZ9TLsR7diphGwKnSEAMQTSjS", + Amount : { + currency : "LTC", + issuer : "r4qLSAzv4LZ9TLsR7diphGwKnSEAMQTSjS", + value : "9.985" + }, + Destination : "r4qLSAzv4LZ9TLsR7diphGwKnSEAMQTSjS", + Fee : "15", + Flags : 0, + Paths : [ + [ + { + account : "rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q", + currency : "USD", + issuer : "rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q", + type : 49, + type_hex : "0000000000000031" + }, + { + currency : "LTC", + issuer : "rfYv1TXnwgDDK4WQNbFALykYuEBnrR4pDX", + type : 48, + type_hex : "0000000000000030" + }, + { + account : "rfYv1TXnwgDDK4WQNbFALykYuEBnrR4pDX", + currency : "LTC", + issuer : "rfYv1TXnwgDDK4WQNbFALykYuEBnrR4pDX", + type : 49, + type_hex : "0000000000000031" + } + ] + ], + SendMax : { + currency : "USD", + issuer : "r4qLSAzv4LZ9TLsR7diphGwKnSEAMQTSjS", + value : "30.30993068" + }, + Sequence : 415, + SigningPubKey : "02854B06CE8F3E65323F89260E9E19B33DA3E01B30EA4CA172612DE77973FAC58A", + TransactionType : "Payment", + TxnSignature : "304602210096C2F385530587DE573936CA51CB86B801A28F777C944E268212BE7341440B7F022100EBF0508A9145A56CDA7FAF314DF3BBE51C6EE450BA7E74D88516891A3608644E" + }, + clientID: '48631', + state: 'pending', + submitIndex: 1, + submittedIDs: ["304602210096C2F385530587DE573936CA51CB86B801A28F777C944E268212BE7341440B7F022100EBF0508A9145A56CDA7FAF314DF3BBE51C6EE450BA7E74D88516891A3608644E"], + secret: 'mysecret' + }]; + database = { + getPendingTransactions: function(callback) { + callback(null, tx); + } + } + }) + + it('should set transaction members correct ', function(done) { + remote = new Remote(options); + remote.storage = database; + remote.transaction = function() { + return { + clientID: function(id) { + if (typeof id === 'string') { + this._clientID = id; + } + return this; + }, + submit: function() { + assert.deepEqual(this._clientID, tx[0].clientID); + assert.deepEqual(this.submittedIDs,[tx[0].tx_json.TxnSignature]); + assert.equal(this.submitIndex, tx[0].submitIndex); + assert.equal(this.secret, tx[0].secret); + done(); + + }, + parseJson: function(json) {} + } + } + remote.getPendingTransactions(); + + }) + }) }) diff --git a/test/vault-test.js b/test/vault-test.js index 01b2e04f..2f556435 100644 --- a/test/vault-test.js +++ b/test/vault-test.js @@ -1,54 +1,116 @@ -var assert = require('assert'), - RippleTxt = require('../src/js/ripple/rippletxt').RippleTxt, - AuthInfo = require('../src/js/ripple/authinfo').AuthInfo, - VaultClient = require('../src/js/ripple/vaultclient').VaultClient, - Blob = require('../src/js/ripple/blob').BlobClient.Blob, - UInt256 = require('../src/js/ripple/uint256').UInt256; +var assert = require('assert'); +var RippleTxt = require('../src/js/ripple/rippletxt').RippleTxt; +var AuthInfo = require('../src/js/ripple/authinfo').AuthInfo; +var VaultClient = require('../src/js/ripple/vaultclient').VaultClient; +var Blob = require('../src/js/ripple/blob').Blob; +var UInt256 = require('../src/js/ripple/uint256').UInt256; var exampleData = { - id : "ef203d3e76552c0592384f909e6f61f1d1f02f61f07643ce015d8b0c9710dd2f", - crypt : "f0cc91a7c1091682c245cd8e13c246cc150b2cf98b17dd6ef092019c99dc9d82", - unlock : "3e15fe3218a9c664835a6f585582e14480112110ddbe50e5028d05fc5bd9b5f4", - blobURL : "https://id.staging.ripple.com", - username : "exampleUser", - password : "pass word", - domain : "staging.ripple.com", - encrypted_secret : "APYqtqvjJk/J324rx2BGGzUiQ3mtmMMhMsbrUmgxb00W2aFVQzCC2mqd58Z17gzeUUcjtjAm" + id: 'ef203d3e76552c0592384f909e6f61f1d1f02f61f07643ce015d8b0c9710dd2f', + crypt: 'f0cc91a7c1091682c245cd8e13c246cc150b2cf98b17dd6ef092019c99dc9d82', + unlock: '3e15fe3218a9c664835a6f585582e14480112110ddbe50e5028d05fc5bd9b5f4', + blobURL: 'https://id.staging.ripple.com', + username: 'exampleUser', + password: 'pass word', + domain: 'staging.ripple.com', + encrypted_secret: 'APYqtqvjJk/J324rx2BGGzUiQ3mtmMMhMsbrUmgxb00W2aFVQzCC2mqd58Z17gzeUUcjtjAm' }; +var rippleTxtRes = { + address: [ 'r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV' ], + validation_public_key: [ 'n9KPnVLn7ewVzHvn218DcEYsnWLzKerTDwhpofhk4Ym1RUq4TeGw' ], + domain: [ 'ripple.com' ], + ips: [ + '23.21.167.100 51235', + '23.23.201.55 51235', + '107.21.116.214 51235' + ], + validators: [ + 'n9KPnVLn7ewVzHvn218DcEYsnWLzKerTDwhpofhk4Ym1RUq4TeGw', + 'n9LFzWuhKNvXStHAuemfRKFVECLApowncMAM5chSCL9R5ECHGN4V', + 'n94rSdgTyBNGvYg8pZXGuNt59Y5bGAZGxbxyvjDaqD9ceRAgD85P', + 'redstem.com' + ], + authinfo_url: [ + 'https://id.staging.ripple.com/v1/authinfo' + ] +}; -process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; //must be set for self signed certs +var authInfoRes = { + body: { + version: 3, + blobvault: 'https://id.staging.ripple.com', + pakdf: { + modulus: 'c7f1bc1dfb1be82d244aef01228c1409c1988943ca9e21431f1669b4aa3864c9f37f3d51b2b4ba1ab9e80f59d267fda1521e88b05117993175e004543c6e3611242f24432ce8efa3b81f0ff660b4f91c5d52f2511a6f38181a7bf9abeef72db056508bbb4eeb5f65f161dd2d5b439655d2ae7081fcc62fdcb281520911d96700c85cdaf12e7d1f15b55ade867240722425198d4ce39019550c4c8a921fc231d3e94297688c2d77cd68ee8fdeda38b7f9a274701fef23b4eaa6c1a9c15b2d77f37634930386fc20ec291be95aed9956801e1c76601b09c413ad915ff03bfdc0b6b233686ae59e8caf11750b509ab4e57ee09202239baee3d6e392d1640185e1cd', + alpha: '7283d19e784f48a96062271a5fa6e2c3addf14e6ezf78a4bb61364856d580f13552008d7b9e3b60ebd9555e9f6c7778ec69f976757d206134e54d61ba9d588a7e37a77cf48060522478352d76db000366ef669a1b1ca93c5e3e05bc344afa1e8ccb15d3343da94180dccf590c2c32408c3f3f176c8885e95d988f1565ee9b80c12f72503ab49917792f907bbb9037487b0afed967fefc9ab090164597fcd391c43fab33029b38e66ff4af96cbf6d90a01b891f856ddd3d94e9c9b307fe01e1353a8c30edd5a94a0ebba5fe7161569000ad3b0d3568872d52b6fbdfce987a687e4b346ea702e8986b03b6b1b85536c813e46052a31ed64ec490d3ba38029544aa', + url: 'https://auth1.ripple.com/api/sign', + exponent: '010001', + host: 'auth1.ripple.com' + }, + exists: true, + username: 'exampleUser', + address: 'raVUps4RghLYkVBcpMaRbVKRTTzhesPXd', + emailVerified: true, + reserved: false + }, +}; +//must be set for self signed certs +process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; -describe('Ripple Txt', function () { - it('should get the context of a ripple.txt file from a given domain', function (done){ +describe('Ripple Txt', function() { + it('should get the content of a ripple.txt file from a given domain', function(done) { var rt = new RippleTxt(); - rt.get(exampleData.domain, function (err, resp){ - assert.ifError(err); - assert.equal(typeof resp, 'object'); + + var requestedUrls = [ ]; + + var testUrls = RippleTxt.urlTemplates.map(function(template) { + return template.replace('{{domain}}', 'localhost'); + }); + + rt.request = function(url, callback) { + requestedUrls.push(url); + callback(new Error()); + }; + + rt.get('localhost', function(err, resp) { + assert(err instanceof Error); + assert.strictEqual(resp, void(0)); + assert.deepEqual(requestedUrls, testUrls); done(); }); }); }); +describe('AuthInfo', function() { + it('should get auth info', function(done) { + var auth = new AuthInfo(); -describe('AuthInfo', function () { - var auth = new AuthInfo(); - - it ('should get authinfo given a domain and username', function (done){ - auth.get(exampleData.domain, exampleData.user, function (err, resp){ + auth._getRippleTxt = function(domain, callback) { + assert.strictEqual(domain, 'staging.ripple.com'); + callback(null, rippleTxtRes); + }; + + auth._getUser = function(url, callback) { + assert.strictEqual(url, 'https://id.staging.ripple.com/v1/authinfo?domain=staging.ripple.com&username=exampleUser'); + callback(null, authInfoRes); + }; + + auth.get(exampleData.domain, exampleData.username, function(err, resp) { assert.ifError(err); - assert.equal(typeof resp, 'object'); + Object.keys(authInfoRes.body).forEach(function(prop) { + assert(resp.hasOwnProperty(prop)); + }); done(); - }); - }); + }); + }); }); describe('VaultClient', function () { var client = new VaultClient(exampleData.domain); - - describe('#initialization', function () { - it('should be initialized with a domain', function () { + + describe('#initialization', function() { + it('should be initialized with a domain', function() { var client = new VaultClient({ domain: exampleData.domain }); assert.strictEqual(client.domain, exampleData.domain); }); @@ -58,24 +120,22 @@ describe('VaultClient', function () { assert.strictEqual(client.domain, 'ripple.com'); }); }); - - describe('#exists', function () { - it('should determine if a username exists on the domain', function (done) { + + describe('#exists', function() { + it('should determine if a username exists on the domain', function(done) { this.timeout(10000); client.exists(exampleData.username, function(err, resp) { - assert.ifError(err); - assert.equal(typeof resp, 'boolean'); + assert.strictEqual(typeof resp, 'boolean'); done(); }); }); }); - describe('#login', function () { - it('with username and password should retrive the blob, crypt key, and id', function (done) { + describe('#login', function() { + it('with username and password should retrive the blob, crypt key, and id', function(done) { this.timeout(10000); - client.login(exampleData.username, exampleData.password, function (err, resp) { - + client.login(exampleData.username, exampleData.password, function(err, resp) { assert.ifError(err); assert.equal(typeof resp, 'object'); assert(resp.blob instanceof Blob); @@ -89,13 +149,11 @@ describe('VaultClient', function () { }); }); }); - - - describe('#relogin', function () { - it('should retrieve the decrypted blob with blob vault url, id, and crypt key', function (done) { + + describe('#relogin', function() { + it('should retrieve the decrypted blob with blob vault url, id, and crypt key', function(done) { this.timeout(10000); client.relogin(exampleData.blobURL, exampleData.id, exampleData.crypt, function(err, resp) { - assert.ifError(err); assert.equal(typeof resp, 'object'); assert(resp.blob instanceof Blob); @@ -107,8 +165,7 @@ describe('VaultClient', function () { describe('#unlock', function() { it('should access the wallet secret using encryption secret, username and password', function (done) { this.timeout(10000); - client.unlock(exampleData.username, exampleData.password, exampleData.encrypted_secret, function (err, resp) { - + client.unlock(exampleData.username, exampleData.password, exampleData.encrypted_secret, function(err, resp) { assert.ifError(err); assert.equal(typeof resp, 'object'); assert.equal(typeof resp.keys, 'object'); @@ -122,17 +179,16 @@ describe('VaultClient', function () { describe('#loginAndUnlock', function () { it('should get the decrypted blob and decrypted secret given name and password', function (done) { this.timeout(10000); - client.loginAndUnlock(exampleData.username, exampleData.password, function (err, resp) { - + client.loginAndUnlock(exampleData.username, exampleData.password, function(err, resp) { assert.ifError(err); - assert.equal(typeof resp, 'object'); + assert.equal(typeof resp, 'object'); assert(resp.blob instanceof Blob); assert.equal(typeof resp.blob.id, 'string'); assert(UInt256.from_json(resp.blob.id).is_valid(), true); assert.equal(typeof resp.blob.key, 'string'); assert(UInt256.from_json(resp.blob.key).is_valid(), true); assert.equal(typeof resp.unlock, 'string'); - assert(UInt256.from_json(resp.unlock).is_valid(), true); + assert(UInt256.from_json(resp.unlock).is_valid(), true); assert.equal(typeof resp.secret, 'string'); assert.equal(typeof resp.username, 'string'); assert.equal(typeof resp.verified, 'boolean'); @@ -145,60 +201,17 @@ describe('VaultClient', function () { describe('Blob', function () { var vaultClient; - + vaultClient = new VaultClient({ domain: exampleData.domain }); - vaultClient.login(exampleData.username, exampleData.password, function (err,resp){ + + vaultClient.login(exampleData.username, exampleData.password, function(err,resp) { assert.ifError(err); var blob = resp.blob; - - - describe('#set', function () { - it('should set a new property in the blob', function (done) { - this.timeout(10000); - - blob.extend("/testObject", { - foo : [], - }, function(err, resp){ - assert.ifError(err); - assert.equal(resp.result, 'success'); - done(); - }); - }); - }); - - describe('#extend', function () { - it('should extend an object in the blob', function (done) { - this.timeout(10000); - - blob.extend("/testObject", { - foobar : "baz", - }, function(err, resp){ - assert.ifError(err); - assert.equal(resp.result, 'success'); - done(); - }); - }); - }); - - describe('#unset', function () { - it('should remove a property from the blob', function (done) { - this.timeout(10000); - - blob.unset("/testObject", function(err, resp){ - assert.ifError(err); - assert.equal(resp.result, 'success'); - done(); - }); - }); - }); - - describe('#unshift', function () { - it('should prepend an item to an array in the blob', function (done) { - this.timeout(10000); - - blob.unshift("/testArray", { - name : "bob", - address : "1234" + describe('#set', function() { + it('should set a new property in the blob', function(done) { + this.timeout(10000) + blob.extend('/testObject', { + foo: [], }, function(err, resp){ assert.ifError(err); assert.equal(resp.result, 'success'); @@ -206,36 +219,71 @@ describe('Blob', function () { }); }); }); - - describe('#filter', function () { - it('should find a specific entity in an array and apply subcommands to it', function(done) { - this.timeout(10000); - - blob.filter('/testArray', 'name', 'bob', 'extend', '', {description:"Alice"}, function(err, resp){ + + describe('#extend', function() { + it('should extend an object in the blob', function(done) { + this.timeout(10000) + blob.extend('/testObject', { + foobar: 'baz', + }, function(err, resp){ assert.ifError(err); assert.equal(resp.result, 'success'); done(); }); }); - }); + }); - describe('#consolidate', function () { - it('should consolidate and save changes to the blob', function (done) { - this.timeout(10000); - + describe('#unset', function() { + it('should remove a property from the blob', function(done) { + this.timeout(10000) + blob.unset('/testObject', function(err, resp){ + assert.ifError(err); + assert.equal(resp.result, 'success'); + done(); + }); + }); + }); + + describe('#unshift', function() { + it('should prepend an item to an array in the blob', function(done) { + this.timeout(10000) + blob.unshift('/testArray', { + name: 'bob', + address: '1234' + }, function(err, resp){ + assert.ifError(err); + assert.equal(resp.result, 'success'); + done(); + }); + }); + }); + + describe('#filter', function() { + it('should find a specific entity in an array and apply subcommands to it', function(done) { + this.timeout(10000) + + blob.filter('/testArray', 'name', 'bob', 'extend', '', {description:'Alice'}, function(err, resp){ + assert.ifError(err); + assert.equal(resp.result, 'success'); + done(); + }); + }); + }); + + describe('#consolidate', function() { + it('should consolidate and save changes to the blob', function(done) { + this.timeout(10000) blob.unset('/testArray', function(err, resp){ assert.ifError(err); assert.equal(resp.result, 'success'); - blob.consolidate(function(err, resp){ assert.ifError(err); assert.equal(resp.result, 'success'); done(); - }); - }); + }); + }); }); - }); - + }); /********* Identity tests ***********/ describe('#identity_set', function () { @@ -288,5 +336,4 @@ describe('Blob', function () { }); }); }); -}); - +});