diff --git a/docs/index.md b/docs/index.md index 20c1aecf..eb44013e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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\ | *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\ | *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. diff --git a/src/api.js b/src/api.js new file mode 100644 index 00000000..75d560b0 --- /dev/null +++ b/src/api.js @@ -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 +}; diff --git a/src/broadcast.js b/src/broadcast.js new file mode 100644 index 00000000..122c3def --- /dev/null +++ b/src/broadcast.js @@ -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 +}; diff --git a/src/common/schemas/input/api-options.json b/src/common/schemas/input/api-options.json index 8c4bb8ff..0ad9e345 100644 --- a/src/common/schemas/input/api-options.json +++ b/src/common/schemas/input/api-options.json @@ -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", diff --git a/src/index.js b/src/index.js index 098296e5..c1419184 100644 --- a/src/index.js +++ b/src/index.js @@ -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, - 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 = 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; diff --git a/test/api-test.js b/test/api-test.js index 59d53fce..da00be84 100644 --- a/test/api-test.js +++ b/test/api-test.js @@ -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'})); }); }); diff --git a/test/integration/broadcast-test.js b/test/integration/broadcast-test.js new file mode 100644 index 00000000..fca74216 --- /dev/null +++ b/test/integration/broadcast-test.js @@ -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(); diff --git a/test/integration/integration-test.js b/test/integration/integration-test.js index 2e3c0686..06719b6f 100644 --- a/test/integration/integration-test.js +++ b/test/integration/integration-test.js @@ -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...'); diff --git a/test/setup-api.js b/test/setup-api.js index f981de1a..14d29446 100644 --- a/test/setup-api.js +++ b/test/setup-api.js @@ -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));