BREAKING CHANGE: "servers" parameter changed to single "server" and added a new broadcast wrapper class for multiple servers

This commit is contained in:
Chris Clark
2015-11-24 15:00:47 -08:00
parent a124635c2c
commit 7061e9afe4
9 changed files with 243 additions and 160 deletions

View File

@@ -112,7 +112,7 @@ authorization | string | *Optional* Username and password for HTTP basic authent
feeCushion | number | *Optional* Factor to multiply estimated fee by to provide a cushion in case the required fee rises during submission of a transaction. Defaults to `1.2`.
proxy | uri string | *Optional* URI for HTTP/HTTPS proxy to use to connect to the rippled server.
proxyAuthorization | string | *Optional* Username and password for HTTP basic authentication to the proxy in the format **username:password**.
servers | array\<uri string\> | *Optional* Array of rippled servers to connect to. Currently only one server is supported.
server | uri string | *Optional* URI for rippled websocket port to connect to. Must start with `wss://` or `ws://`.
timeout | integer | *Optional* Timeout in milliseconds before considering a request to have failed.
trace | boolean | *Optional* If true, log rippled requests and responses to stdout.
trustedCertificates | array\<string\> | *Optional* Array of PEM-formatted SSL certificates to trust when connecting to a proxy. This is useful if you want to use a self-signed certificate on the proxy server. Note: Each element must contain a single certificate; concatenated certificates are not valid.

144
src/api.js Normal file
View File

@@ -0,0 +1,144 @@
/* @flow */
'use strict';
/* eslint-disable max-len */
// Enable core-js polyfills. This allows use of ES6/7 extensions listed here:
// https://github.com/zloirock/core-js/blob/fb0890f32dabe8d4d88a4350d1b268446127132e/shim.js#L1-L103
/* eslint-enable max-len */
// In node.js env, polyfill might be already loaded (from any npm package),
// that's why we do this check.
if (!global._babelPolyfill) {
require('babel-core/polyfill');
}
const _ = require('lodash');
const EventEmitter = require('events').EventEmitter;
const common = require('./common');
const server = require('./server/server');
const connect = server.connect;
const disconnect = server.disconnect;
const getServerInfo = server.getServerInfo;
const getFee = server.getFee;
const isConnected = server.isConnected;
const getLedgerVersion = server.getLedgerVersion;
const getTransaction = require('./ledger/transaction');
const getTransactions = require('./ledger/transactions');
const getTrustlines = require('./ledger/trustlines');
const getBalances = require('./ledger/balances');
const getBalanceSheet = require('./ledger/balance-sheet');
const getPaths = require('./ledger/pathfind');
const getOrders = require('./ledger/orders');
const getOrderbook = require('./ledger/orderbook');
const getSettings = require('./ledger/settings');
const getAccountInfo = require('./ledger/accountinfo');
const preparePayment = require('./transaction/payment');
const prepareTrustline = require('./transaction/trustline');
const prepareOrder = require('./transaction/order');
const prepareOrderCancellation = require('./transaction/ordercancellation');
const prepareSuspendedPaymentCreation =
require('./transaction/suspended-payment-creation');
const prepareSuspendedPaymentExecution =
require('./transaction/suspended-payment-execution');
const prepareSuspendedPaymentCancellation =
require('./transaction/suspended-payment-cancellation');
const prepareSettings = require('./transaction/settings');
const sign = require('./transaction/sign');
const submit = require('./transaction/submit');
const errors = require('./common').errors;
const generateAddress = common.generateAddressAPI;
const computeLedgerHash = require('./offline/ledgerhash');
const getLedger = require('./ledger/ledger');
type APIOptions = {
server?: string,
feeCushion?: number,
trace?: boolean,
proxy?: string,
timeout?: number
}
// prevent access to non-validated ledger versions
class RestrictedConnection extends common.Connection {
request(request, timeout) {
const ledger_index = request.ledger_index;
if (ledger_index !== undefined && ledger_index !== 'validated') {
if (!_.isNumber(ledger_index) || ledger_index > this._ledgerVersion) {
return Promise.reject(new errors.LedgerVersionError(
`ledgerVersion ${ledger_index} is greater than server\'s ` +
`most recent validated ledger: ${this._ledgerVersion}`));
}
}
return super.request(request, timeout);
}
}
class RippleAPI extends EventEmitter {
constructor(options: APIOptions = {}) {
common.validate.apiOptions(options);
super();
this._feeCushion = options.feeCushion || 1.2;
const serverURL = options.server;
if (serverURL !== undefined) {
this.connection = new RestrictedConnection(serverURL, options);
this.connection.on('ledgerClosed', message => {
this.emit('ledger', server.formatLedgerClose(message));
});
this.connection.on('error', (type, info) => {
this.emit('error', type, info);
});
} else {
// use null object pattern to provide better error message if user
// tries to call a method that requires a connection
this.connection = new RestrictedConnection(null, options);
}
}
}
_.assign(RippleAPI.prototype, {
connect,
disconnect,
isConnected,
getServerInfo,
getFee,
getLedgerVersion,
getTransaction,
getTransactions,
getTrustlines,
getBalances,
getBalanceSheet,
getPaths,
getOrders,
getOrderbook,
getSettings,
getAccountInfo,
getLedger,
preparePayment,
prepareTrustline,
prepareOrder,
prepareOrderCancellation,
prepareSuspendedPaymentCreation,
prepareSuspendedPaymentExecution,
prepareSuspendedPaymentCancellation,
prepareSettings,
sign,
submit,
generateAddress,
computeLedgerHash,
errors
});
// these are exposed only for use by unit tests; they are not part of the API
RippleAPI._PRIVATE = {
validate: common.validate,
RangeSet: require('./common/rangeset').RangeSet,
ledgerUtils: require('./ledger/utils'),
schemaValidator: require('./common/schema-validator')
};
module.exports = {
RippleAPI
};

66
src/broadcast.js Normal file
View File

@@ -0,0 +1,66 @@
'use strict';
const _ = require('lodash');
const RippleAPI = require('./api').RippleAPI;
class RippleAPIBroadcast extends RippleAPI {
constructor(servers, options) {
super(options);
this.ledgerVersion = 0;
const apis = servers.map(server => new RippleAPI(
_.assign({}, options, {server})
));
this.getMethodNames().forEach(name => {
this[name] = function() { // eslint-disable-line no-loop-func
return Promise.race(apis.map(api => api[name].apply(api, arguments)));
};
});
// connection methods must be overridden to apply to all api instances
this.connect = function() {
return Promise.all(apis.map(api => api.connect()));
};
this.disconnect = function() {
return Promise.all(apis.map(api => api.disconnect()));
};
this.isConnected = function() {
return _.every(apis.map(api => api.isConnected()));
};
// synchronous methods are all passed directly to the first api instance
const defaultAPI = apis[0];
const syncMethods = ['sign', 'generateAddress', 'computeLedgerHash'];
syncMethods.forEach(name => {
this[name] = defaultAPI[name].bind(defaultAPI);
});
apis.forEach(api => {
api.on('ledger', this.onLedgerEvent.bind(this));
api.on('error', (type, info) => this.emit('error', type, info));
});
}
onLedgerEvent(ledger) {
if (ledger.ledgerVersion > this.ledgerVersion) {
this.ledgerVersion = ledger.ledgerVersion;
this.emit('ledger', ledger);
}
}
getMethodNames() {
const methodNames = [];
for (const name in RippleAPI.prototype) {
if (RippleAPI.prototype.hasOwnProperty(name)) {
if (typeof RippleAPI.prototype[name] === 'function') {
methodNames.push(name);
}
}
}
return methodNames;
}
}
module.exports = {
RippleAPIBroadcast
};

View File

@@ -12,15 +12,11 @@
"minimum": 1,
"description": "Factor to multiply estimated fee by to provide a cushion in case the required fee rises during submission of a transaction. Defaults to `1.2`."
},
"servers": {
"type": "array",
"description": "Array of rippled servers to connect to. Currently only one server is supported.",
"items": {
"type": "string",
"description": "URI for rippled websocket port. Must start with `wss://` or `ws://`.",
"format": "uri",
"pattern": "^wss?://"
}
"server": {
"type": "string",
"description": "URI for rippled websocket port to connect to. Must start with `wss://` or `ws://`.",
"format": "uri",
"pattern": "^wss?://"
},
"proxy": {
"format": "uri",

View File

@@ -1,146 +1,6 @@
/* @flow */
'use strict';
/* eslint-disable max-len */
// Enable core-js polyfills. This allows use of ES6/7 extensions listed here:
// https://github.com/zloirock/core-js/blob/fb0890f32dabe8d4d88a4350d1b268446127132e/shim.js#L1-L103
/* eslint-enable max-len */
// In node.js env, polyfill might be already loaded (from any npm package),
// that's why we do this check.
if (!global._babelPolyfill) {
require('babel-core/polyfill');
}
const _ = require('lodash');
const EventEmitter = require('events').EventEmitter;
const common = require('./common');
const server = require('./server/server');
const connect = server.connect;
const disconnect = server.disconnect;
const getServerInfo = server.getServerInfo;
const getFee = server.getFee;
const isConnected = server.isConnected;
const getLedgerVersion = server.getLedgerVersion;
const getTransaction = require('./ledger/transaction');
const getTransactions = require('./ledger/transactions');
const getTrustlines = require('./ledger/trustlines');
const getBalances = require('./ledger/balances');
const getBalanceSheet = require('./ledger/balance-sheet');
const getPaths = require('./ledger/pathfind');
const getOrders = require('./ledger/orders');
const getOrderbook = require('./ledger/orderbook');
const getSettings = require('./ledger/settings');
const getAccountInfo = require('./ledger/accountinfo');
const preparePayment = require('./transaction/payment');
const prepareTrustline = require('./transaction/trustline');
const prepareOrder = require('./transaction/order');
const prepareOrderCancellation = require('./transaction/ordercancellation');
const prepareSuspendedPaymentCreation =
require('./transaction/suspended-payment-creation');
const prepareSuspendedPaymentExecution =
require('./transaction/suspended-payment-execution');
const prepareSuspendedPaymentCancellation =
require('./transaction/suspended-payment-cancellation');
const prepareSettings = require('./transaction/settings');
const sign = require('./transaction/sign');
const submit = require('./transaction/submit');
const errors = require('./common').errors;
const generateAddress = common.generateAddressAPI;
const computeLedgerHash = require('./offline/ledgerhash');
const getLedger = require('./ledger/ledger');
type APIOptions = {
servers?: Array<string>,
feeCushion?: number,
trace?: boolean,
proxy?: string,
timeout?: number
}
// prevent access to non-validated ledger versions
class RestrictedConnection extends common.Connection {
request(request, timeout) {
const ledger_index = request.ledger_index;
if (ledger_index !== undefined && ledger_index !== 'validated') {
if (!_.isNumber(ledger_index) || ledger_index > this._ledgerVersion) {
return Promise.reject(new errors.LedgerVersionError(
`ledgerVersion ${ledger_index} is greater than server\'s ` +
`most recent validated ledger: ${this._ledgerVersion}`));
}
}
return super.request(request, timeout);
}
}
class RippleAPI extends EventEmitter {
constructor(options: APIOptions = {}) {
common.validate.apiOptions(options);
super();
this._feeCushion = options.feeCushion || 1.2;
if (options.servers !== undefined) {
const servers: Array<string> = options.servers;
if (servers.length === 1) {
this.connection = new RestrictedConnection(servers[0], options);
this.connection.on('ledgerClosed', message => {
this.emit('ledger', server.formatLedgerClose(message));
});
this.connection.on('error', (type, info) => {
this.emit('error', type, info);
});
} else {
throw new errors.RippleError('Multi-server not implemented');
}
} else {
// use null object pattern to provide better error message if user
// tries to call a method that requires a connection
this.connection = new RestrictedConnection(null, options);
}
}
}
_.assign(RippleAPI.prototype, {
connect,
disconnect,
isConnected,
getServerInfo,
getFee,
getLedgerVersion,
getTransaction,
getTransactions,
getTrustlines,
getBalances,
getBalanceSheet,
getPaths,
getOrders,
getOrderbook,
getSettings,
getAccountInfo,
getLedger,
preparePayment,
prepareTrustline,
prepareOrder,
prepareOrderCancellation,
prepareSuspendedPaymentCreation,
prepareSuspendedPaymentExecution,
prepareSuspendedPaymentCancellation,
prepareSettings,
sign,
submit,
generateAddress,
computeLedgerHash,
errors
});
// these are exposed only for use by unit tests; they are not part of the API
RippleAPI._PRIVATE = {
validate: common.validate,
RangeSet: require('./common/rangeset').RangeSet,
ledgerUtils: require('./ledger/utils'),
schemaValidator: require('./common/schema-validator')
module.exports = {
RippleAPI: require('./api').RippleAPI,
RippleAPIBroadcast: require('./broadcast').RippleAPIBroadcast
};
module.exports.RippleAPI = RippleAPI;

View File

@@ -79,8 +79,8 @@ describe('RippleAPI', function() {
}, /XRP to XRP payments cannot be partial payments/);
});
it('preparePayment - address must match payment.source.address',
function() {
it('preparePayment - address must match payment.source.address', function(
) {
assert.throws(() => {
this.api.preparePayment(address, requests.preparePayment.wrongAddress);
}, /address must match payment.source.address/);
@@ -1300,7 +1300,7 @@ describe('RippleAPI - offline', function() {
/* eslint-disable no-unused-vars */
it('RippleAPI - implicit server port', function() {
const api = new RippleAPI({servers: ['wss://s1.ripple.com']});
const api = new RippleAPI({server: 'wss://s1.ripple.com'});
});
/* eslint-enable no-unused-vars */
it('RippleAPI invalid options', function() {
@@ -1308,12 +1308,12 @@ describe('RippleAPI - offline', function() {
});
it('RippleAPI valid options', function() {
const api = new RippleAPI({servers: ['wss://s:1']});
const api = new RippleAPI({server: 'wss://s:1'});
assert.deepEqual(api.connection._url, 'wss://s:1');
});
it('RippleAPI invalid server uri', function() {
assert.throws(() => new RippleAPI({servers: ['wss//s:1']}));
assert.throws(() => new RippleAPI({server: 'wss//s:1'}));
});
});

View File

@@ -0,0 +1,17 @@
'use strict';
const {RippleAPIBroadcast} = require('../../src');
function main() {
const servers = ['wss://s1.ripple.com', 'wss://s2.ripple.com'];
const api = new RippleAPIBroadcast(servers);
api.connect().then(() => {
api.getServerInfo().then(info => {
console.log(JSON.stringify(info, null, 2));
});
api.on('ledger', ledger => {
console.log(JSON.stringify(ledger, null, 2));
});
});
}
main();

View File

@@ -58,7 +58,7 @@ function testTransaction(testcase, type, lastClosedLedgerVersion, prepared) {
}
function setup() {
this.api = new RippleAPI({servers: ['wss://s1.ripple.com']});
this.api = new RippleAPI({server: 'wss://s1.ripple.com'});
console.log('CONNECTING...');
return this.api.connect().then(() => {
console.log('CONNECTED...');

View File

@@ -23,7 +23,7 @@ function getFreePort(callback) {
function setupMockRippledConnection(testcase, port, done) {
testcase.mockRippled = createMockRippled(port);
testcase.api = new RippleAPI({servers: ['ws://localhost:' + port]});
testcase.api = new RippleAPI({server: 'ws://localhost:' + port});
testcase.api.connect().then(() => {
testcase.api.once('ledger', () => done());
testcase.api.connection._ws.emit('message', JSON.stringify(ledgerClosed));