Compare commits

...

30 Commits

Author SHA1 Message Date
Alan Cohen
b56752e45b 0.16.10 2016-03-24 14:52:36 -07:00
Chris Clark
4632f511ab Merge pull request #711 from darkdarkdragon/reconnect_fix2
[FIX] on connect return error if server doesn't have validated ledgers
2016-03-24 14:27:50 -07:00
Ivan Tivonenko
e17b6f172d [FIX] on connect return error if server doesn't have validated ledgers 2016-03-24 22:26:33 +02:00
Alan Cohen
eb04e878ba 0.16.9 2016-03-22 18:39:33 -07:00
Chris Clark
c71febd116 Merge pull request #709 from darkdarkdragon/reconnect_fix
Reconnection fix
2016-03-22 18:12:28 -07:00
Ivan Tivonenko
69c1ccbb6b [TASK] emit connected and disconnected events from api 2016-03-23 02:05:07 +02:00
Ivan Tivonenko
499b8c8d8b [FIX] fix multiple reconnections issue 2016-03-23 01:18:08 +02:00
Chris Clark
ea009f9a84 Merge pull request #708 from clark800/fix-maker-exchange-rate
Fix makerExchangeRate for getOrders when rippled provides quality
2016-03-22 14:14:23 -07:00
Chris Clark
dc784d4567 Fix makerExchangeRate for getOrders when rippled provides quality 2016-03-22 13:21:26 -07:00
Alan Cohen
9ffc8a2c0b Merge pull request #707 from ripple/fix/filter-source-amount
FIX: Filtering source_amount pathfind correctly
2016-03-21 14:48:37 -07:00
Alan Cohen
5b20fe573e FIX: Filtering source_amount pathfind correctly
Bug:

```js
api.connect().then(() => {
  const pathfind = {
    source: {
      address: USDCold,
      amount: {
        currency: 'USD',
        value: '1.00' // <<<< Rippled response has "1" not "1.00"
      }
    },
    destination: {
      address: EURCold,
      amount: {currency: 'EUR'}
    }
  };
  return api.getPaths(pathfind).then(paths => {
    console.log('PATHS: \n', JSON.stringify(paths, null, 2));
  });
}).catch(console.log);

```
2016-03-21 11:44:55 -07:00
Chris Clark
7e466bb80f Merge pull request #705 from darkdarkdragon/code_comment
[FIX] add small code comment
2016-03-15 12:14:05 -07:00
Ivan Tivonenko
1f8418b447 [FIX] add small code comment 2016-03-15 20:58:22 +02:00
Alan Cohen
ccfc57fc62 Merge pull request #704 from darkdarkdragon/fix_edge_test
[FIX] fix test hang in microsoft edge
2016-03-15 10:27:01 -07:00
Ivan Tivonenko
1d31fccd72 [FIX] fix test hang in microsoft edge 2016-03-15 05:04:47 +02:00
Alan Cohen
9ac1a89e48 0.16.8 2016-03-09 12:30:10 -08:00
Chris Clark
bfc0696324 Merge pull request #701 from h0vhannes/develop
Documentation fixes
2016-03-09 12:24:55 -08:00
Hovhannes Kuloghlyan
e33e782f9e [DOC] Fix transaction spelling in getTransaction description 2016-03-09 22:14:09 +04:00
Hovhannes Kuloghlyan
f2b591d1b2 [DOC] Fix transaction responses link in submit() description 2016-03-09 22:07:54 +04:00
Chris Clark
fe9af5153d Merge pull request #699 from darkdarkdragon/develop_connection_error
[FIX] fix connection error handling
2016-02-24 14:12:12 -08:00
Ivan Tivonenko
0dfdd0a601 [FIX] change eslint version to 2.1 2016-02-24 23:08:49 +02:00
Ivan Tivonenko
4acc42e1b6 [FIX] fix connection error handling
if connection to server can't be established, reject Promise of `connect()` method caller,
instead of throwing error event on base object.
2016-02-24 23:08:47 +02:00
Alan Cohen
7c9a179865 Merge pull request #698 from h0vhannes/develop
[FIX] Fix typo in PrepareTrustline description
2016-02-23 08:44:43 -07:00
Hovhannes Kuloghlyan
c6296a4918 [FIX] Modify PrepareTrustline doc source to pass tests 2016-02-23 16:45:32 +04:00
Hovhannes Kuloghlyan
cc399f1164 [FIX] Fix typo in PrepareTrustline description 2016-02-21 17:02:27 +03:00
Alan Cohen
e4ffb96646 Update version to 0.16.7 2016-02-12 11:38:51 -08:00
Chris Clark
8d34428dac Merge pull request #694 from lumberj/add-deliveredAmount
Add deliveredAmount to payment outcome
2016-02-12 11:25:01 -08:00
Alan Cohen
353637a0c0 Add TODO comment for fixing workaround for rippled bug 2016-02-12 10:56:35 -08:00
Alan Cohen
06f847c2d0 Fix lint errors 2016-02-10 19:45:51 -08:00
Alan Cohen
0c2f9d0e62 Add deliveredAmount to payment outcome
It is impossible to reliably compute the delivered amount from the metadata due
to fixed precision. If the partial payment flag is not set and the transaction
succeeded, the delivered amount should always be considered to be the amount
specified in the transaction.
2016-02-10 17:25:27 -08:00
31 changed files with 523 additions and 64 deletions

View File

@@ -61,6 +61,8 @@
- [API Events](#api-events)
- [ledger](#ledger)
- [error](#error)
- [connected](#connected)
- [disconnected](#disconnected)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
@@ -90,6 +92,14 @@ const api = new RippleAPI({
api.on('error', (errorCode, errorMessage) => {
console.log(errorCode + ': ' + errorMessage);
});
api.on('connected', () => {
console.log('connected');
});
api.on('disconnected', (code) => {
// code - [close code](https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent) sent by the server
// will be 1000 if this was normal closure
console.log('disconnected, code:', code);
});
api.connect().then(() => {
/* insert code here */
}).then(() => {
@@ -811,7 +821,7 @@ Name | Type | Description
id | [id](#transaction-id) | A hash of the transaction that can be used to identify it.
address | [address](#ripple-address) | The address of the account that initiated the transaction.
sequence | [sequence](#account-sequence-number) | The account sequence number of the transaction for the account that initiated it.
type | [transactionType](#transaction-types) | The type of the tranasction.
type | [transactionType](#transaction-types) | The type of the transaction.
specification | object | A specification that would produce the same outcome as this transaction. The structure of the specification depends on the value of the `type` field (see [Transaction Types](#transaction-types) for details). *Note:* This is **not** necessarily the same as the original specification.
outcome | object | The outcome of the transaction (what effects it had).
*outcome.* result | string | Result code returned by rippled. See [Transaction Results](https://ripple.com/build/transactions/#full-transaction-response-list) for a complete list.
@@ -828,6 +838,7 @@ outcome | object | The outcome of the transaction (what effects it had).
*outcome.orderbookChanges.\*[].* makerExchangeRate | [value](#value) | *Optional* The exchange rate between the `quantity` currency and the `totalPrice` currency from the point of view of the maker.
*outcome.* ledgerVersion | integer | The ledger version that the transaction was validated in.
*outcome.* indexInLedger | integer | The ordering index of the transaction in the ledger.
*outcome.* deliveredAmount | [amount](#amount) | *Optional* For payment transactions, it is impossible to reliably compute the actual delivered amount from the balanceChanges due to fixed precision. If the payment is not a partial payment and the transaction succeeded, the deliveredAmount should always be considered to be the amount specified in the transaction.
*outcome.* timestamp | date-time string | *Optional* The timestamp when the transaction was validated. (May be missing when requesting transactions in binary mode.)
### Example
@@ -867,6 +878,11 @@ return api.getTransaction(id).then(transaction => {
"result": "tesSUCCESS",
"timestamp": "2013-03-12T23:56:50.000Z",
"fee": "0.00001",
"deliveredAmount": {
"currency": "USD",
"value": "0.001",
"counterparty": "rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM"
},
"balanceChanges": {
"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo": [
{
@@ -1001,6 +1017,11 @@ return api.getTransactions(address).then(transaction => {
"outcome": {
"result": "tesSUCCESS",
"fee": "0.00001",
"deliveredAmount": {
"currency": "USD",
"value": "0.001",
"counterparty": "rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM"
},
"balanceChanges": {
"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo": [
{
@@ -1093,6 +1114,11 @@ return api.getTransactions(address).then(transaction => {
"outcome": {
"result": "tesSUCCESS",
"fee": "0.00001",
"deliveredAmount": {
"currency": "USD",
"value": "0.001",
"counterparty": "rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM"
},
"balanceChanges": {
"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo": [
{
@@ -2908,7 +2934,7 @@ const trustline = {
}
]
};
return api.preparePayment(address, trustline).then(prepared =>
return api.prepareTrustline(address, trustline).then(prepared =>
{/* ... */});
```
@@ -3391,7 +3417,7 @@ This method returns an object with the following structure:
Name | Type | Description
---- | ---- | -----------
resultCode | string | The result code returned by rippled. [List of tranasction responses](http://pages.lightthenight.org/gba/SanFran15/ripple)
resultCode | string | The result code returned by rippled. [List of transaction responses](https://ripple.com/build/transactions/#full-transaction-response-list)
resultMessage | string | Human-readable explanation of the status of the transaction.
### Example
@@ -3577,3 +3603,35 @@ api.on('error', (errorCode, errorMessage, data) => {
tooBusy: The server is too busy to help you now.
```
## connected
This event is emitted after connection successfully opened.
### Example
```javascript
api.on('connected', () => {
console.log('Connection is open now.');
});
```
## disconnected
This event is emitted when connection is closed.
### Return Value
The only parameter is a number containing the [close code](https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent) send by the server.
### Example
```javascript
api.on('disconnected', (code) => {
if (code !== 1000) {
console.log('Connection is closed due to error.');
} else {
console.log('Connection is closed normally.');
}
});
```

View File

@@ -11,6 +11,14 @@ const api = new RippleAPI({
api.on('error', (errorCode, errorMessage) => {
console.log(errorCode + ': ' + errorMessage);
});
api.on('connected', () => {
console.log('connected');
});
api.on('disconnected', (code) => {
// code - [close code](https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent) sent by the server
// will be 1000 if this was normal closure
console.log('disconnected, code:', code);
});
api.connect().then(() => {
/* insert code here */
}).then(() => {

View File

@@ -47,3 +47,35 @@ api.on('error', (errorCode, errorMessage, data) => {
```
tooBusy: The server is too busy to help you now.
```
## connected
This event is emitted after connection successfully opened.
### Example
```javascript
api.on('connected', () => {
console.log('Connection is open now.');
});
```
## disconnected
This event is emitted when connection is closed.
### Return Value
The only parameter is a number containing the [close code](https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent) send by the server.
### Example
```javascript
api.on('disconnected', (code) => {
if (code !== 1000) {
console.log('Connection is closed due to error.');
} else {
console.log('Connection is closed normally.');
}
});
```

View File

@@ -23,7 +23,7 @@ All "prepare*" methods have the same return type.
```javascript
const address = 'r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59';
const trustline = <%- importFile('test/fixtures/requests/prepare-trustline.json') %>;
return api.preparePayment(address, trustline).then(prepared =>
return api.prepareTrustline(address, trustline).then(prepared =>
{/* ... */});
```

2
npm-shrinkwrap.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "ripple-lib",
"version": "0.16.6",
"version": "0.16.10",
"dependencies": {
"ajv": {
"version": "1.4.10",

View File

@@ -1,6 +1,6 @@
{
"name": "ripple-lib",
"version": "0.16.6",
"version": "0.16.10",
"license": "ISC",
"description": "A JavaScript API for interacting with Ripple in Node.js and the browser",
"files": [
@@ -33,7 +33,7 @@
"assert-diff": "^1.0.1",
"babel-cli": "^6.4.0",
"babel-core": "^6.4.0",
"babel-eslint": "^4.1.6",
"babel-eslint": "^4.1.8",
"babel-loader": "^6.2.1",
"babel-plugin-syntax-flow": "^6.3.13",
"babel-plugin-transform-flow-strip-types": "^6.4.0",
@@ -43,7 +43,7 @@
"coveralls": "^2.10.0",
"doctoc": "^0.15.0",
"ejs": "^2.3.4",
"eslint": "^1.3.0",
"eslint": "^2.1.0",
"eventemitter2": "^0.4.14",
"flow-bin": "^0.14",
"gulp": "^3.8.10",

View File

@@ -1,4 +1,3 @@
'use strict';
const _ = require('lodash');
const MochaSauce = require('mocha-in-sauce');
@@ -18,7 +17,6 @@ function main() {
maxDuration: 180000,
// the current build name (optional)
build: Date.now(),
seleniumVersion: '2.50.1',
url: testUrl,
runSauceConnect: true
};

View File

@@ -1,5 +1,5 @@
/* @flow */
'use strict';
'use strict'; // eslint-disable-line
/* eslint-disable max-len */
// Enable core-js polyfills. This allows use of ES6/7 extensions listed here:
@@ -89,6 +89,12 @@ class RippleAPI extends EventEmitter {
this.connection.on('error', (errorCode, errorMessage, data) => {
this.emit('error', errorCode, errorMessage, data);
});
this.connection.on('connected', () => {
this.emit('connected');
});
this.connection.on('disconnected', code => {
this.emit('disconnected', code);
});
} else {
// use null object pattern to provide better error message if user
// tries to call a method that requires a connection

View File

@@ -1,4 +1,5 @@
'use strict';
'use strict'; // eslint-disable-line
const _ = require('lodash');
const {EventEmitter} = require('events');
const WebSocket = require('ws');
@@ -36,6 +37,8 @@ class Connection extends EventEmitter {
this._ledgerVersion = null;
this._availableLedgerVersions = new RangeSet();
this._nextRequestID = 1;
this._retry = 0;
this._retryTimer = null;
}
_updateLedgerVersions(data) {
@@ -96,28 +99,91 @@ class Connection extends EventEmitter {
return this._state === WebSocket.OPEN && this._isReady;
}
_onUnexpectedClose(resolve = function() {}, reject = function() {}) {
_onUnexpectedClose(beforeOpen, resolve, reject, code) {
if (this._onOpenErrorBound) {
this._ws.removeListener('error', this._onOpenErrorBound);
this._onOpenErrorBound = null;
}
this._ws = null;
this._isReady = false;
this.connect().then(resolve, reject);
if (beforeOpen) {
// connection was closed before it was properly opened, so we must return
// error to connect's caller
this.connect().then(resolve, reject);
} else {
// if first parameter ws lib sends close code,
// but sometimes it forgots about it, so default to 1006 - CLOSE_ABNORMAL
this.emit('disconnected', code || 1006);
this._retryConnect();
}
}
_calculateTimeout(retriesCount) {
return (retriesCount < 40)
// First, for 2 seconds: 20 times per second
? (1000 / 20)
: (retriesCount < 40 + 60)
// Then, for 1 minute: once per second
? (1000)
: (retriesCount < 40 + 60 + 60)
// Then, for 10 minutes: once every 10 seconds
? (10 * 1000)
// Then: once every 30 seconds
: (30 * 1000);
}
_retryConnect() {
this._retry += 1;
const retryTimeout = this._calculateTimeout(this._retry);
this._retryTimer = setTimeout(() => {
this.connect().catch(this._retryConnect.bind(this));
}, retryTimeout);
}
_clearReconnectTimer() {
clearTimeout(this._retryTimer);
this._retryTimer = null;
}
_onOpen() {
this._ws.removeListener('close', this._onUnexpectedCloseBound);
this._onUnexpectedCloseBound = this._onUnexpectedClose.bind(this);
this._ws.once('close', this._onUnexpectedCloseBound);
this._ws.removeListener('error', this._onOpenErrorBound);
this._onOpenErrorBound = null;
const request = {
command: 'subscribe',
streams: ['ledger']
};
return this.request(request).then(data => {
if (_.isEmpty(data) || !data.ledger_index) {
// rippled instance doesn't have validated ledgers
return this._disconnect(false).then(() => {
throw new NotConnectedError('Rippled not initialized');
});
}
this._updateLedgerVersions(data);
this._ws.removeListener('close', this._onUnexpectedCloseBound);
this._onUnexpectedCloseBound =
this._onUnexpectedClose.bind(this, false, null, null);
this._ws.once('close', this._onUnexpectedCloseBound);
this._retry = 0;
this._ws.on('error', error =>
this.emit('error', 'websocket', error.message, error));
this._isReady = true;
this.emit('connected');
return undefined;
});
}
_onOpenError(reject, error) {
this._onOpenErrorBound = null;
reject(new NotConnectedError(error && error.message));
}
_createWebSocket() {
const options = {};
if (this._proxyURL !== undefined) {
@@ -162,6 +228,7 @@ class Connection extends EventEmitter {
}
connect() {
this._clearReconnectTimer();
return new Promise((resolve, reject) => {
if (!this._url) {
reject(new ConnectionError(
@@ -177,14 +244,17 @@ class Connection extends EventEmitter {
// should still be emitted; the "ws" documentation says: "The close
// event is also emitted when then underlying net.Socket closes the
// connection (end or close)."
this._ws.on('error', error =>
this.emit('error', 'websocket', error.message, error));
// In case if there is connection error (say, server is not responding)
// we must return this error to connection's caller. After successful
// opening, we will forward all errors to main api object.
this._onOpenErrorBound = this._onOpenError.bind(this, reject);
this._ws.once('error', this._onOpenErrorBound);
this._ws.on('message', this._onMessage.bind(this));
// in browser close event can came before open event, so we must
// resolve connect's promise after reconnect in that case.
// after open event we will rebound _onUnexpectedCloseBound
// without resolve and reject functions
this._onUnexpectedCloseBound = this._onUnexpectedClose.bind(this,
this._onUnexpectedCloseBound = this._onUnexpectedClose.bind(this, true,
resolve, reject);
this._ws.once('close', this._onUnexpectedCloseBound);
this._ws.once('open', () => this._onOpen().then(resolve, reject));
@@ -193,6 +263,14 @@ class Connection extends EventEmitter {
}
disconnect() {
return this._disconnect(true);
}
_disconnect(calledByUser) {
if (calledByUser) {
this._clearReconnectTimer();
this._retry = 0;
}
return new Promise(resolve => {
if (this._state === WebSocket.CLOSED) {
resolve();
@@ -200,9 +278,12 @@ class Connection extends EventEmitter {
this._ws.once('close', resolve);
} else {
this._ws.removeListener('close', this._onUnexpectedCloseBound);
this._ws.once('close', () => {
this._ws.once('close', code => {
this._ws = null;
this._isReady = false;
if (calledByUser) {
this.emit('disconnected', code || 1000); // 1000 - CLOSE_NORMAL
}
resolve();
});
this._ws.close();

View File

@@ -2,7 +2,7 @@
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "amount",
"link": "amount",
"description": "An Amount on the Ripple Protocol, used also for XRP in the ripple-rest API",
"description": "An Amount on the Ripple Protocol",
"allOf": [
{"$ref": "amountbase"},
{"required": ["value"]}

View File

@@ -2,7 +2,7 @@
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "transactionType",
"link": "transaction-types",
"description": "The type of the tranasction.",
"description": "The type of the transaction.",
"type": "string",
"enum": ["payment", "order", "orderCancellation", "trustline", "settings",
"suspendedPaymentCreation", "suspendedPaymentCancellation",

View File

@@ -17,6 +17,10 @@
"$ref": "value",
"description": "The XRP fee that was charged for the transaction."
},
"deliveredAmount": {
"$ref": "amount",
"description": "For payment transactions, it is impossible to reliably compute the actual delivered amount from the balanceChanges due to fixed precision. If the payment is not a partial payment and the transaction succeeded, the deliveredAmount should always be considered to be the amount specified in the transaction."
},
"balanceChanges": {
"type": "object",
"additionalProperties": {

View File

@@ -5,7 +5,7 @@
"properties": {
"resultCode": {
"type": "string",
"description": "The result code returned by rippled. [List of tranasction responses](http://pages.lightthenight.org/gba/SanFran15/ripple)"
"description": "The result code returned by rippled. [List of transaction responses](https://ripple.com/build/transactions/#full-transaction-response-list)"
},
"resultMessage": {
"type": "string",

View File

@@ -1,4 +1,4 @@
'use strict';
'use strict'; // eslint-disable-line
const _ = require('lodash');
const {convertKeysFromSnakeCaseToCamelCase} = require('./utils');
import type {Connection} from './connection';
@@ -43,18 +43,20 @@ function getServerInfo(connection: Connection): Promise<GetServerInfoResponse> {
return connection.request({command: 'server_info'}).then(response => {
const info = convertKeysFromSnakeCaseToCamelCase(response.info);
renameKeys(info, {hostid: 'hostID'});
renameKeys(info.validatedLedger, {
baseFeeXrp: 'baseFeeXRP',
reserveBaseXrp: 'reserveBaseXRP',
reserveIncXrp: 'reserveIncrementXRP',
seq: 'ledgerVersion'
});
info.validatedLedger.baseFeeXRP =
info.validatedLedger.baseFeeXRP.toString();
info.validatedLedger.reserveBaseXRP =
info.validatedLedger.reserveBaseXRP.toString();
info.validatedLedger.reserveIncrementXRP =
info.validatedLedger.reserveIncrementXRP.toString();
if (info.validatedLedger) {
renameKeys(info.validatedLedger, {
baseFeeXrp: 'baseFeeXRP',
reserveBaseXrp: 'reserveBaseXRP',
reserveIncXrp: 'reserveIncrementXRP',
seq: 'ledgerVersion'
});
info.validatedLedger.baseFeeXRP =
info.validatedLedger.baseFeeXRP.toString();
info.validatedLedger.reserveBaseXRP =
info.validatedLedger.reserveBaseXRP.toString();
info.validatedLedger.reserveIncrementXRP =
info.validatedLedger.reserveIncrementXRP.toString();
}
return info;
});
}

View File

@@ -1,5 +1,5 @@
/* @flow */
'use strict';
'use strict'; // eslint-disable-line strict
const utils = require('./utils');
const flags = require('./flags').orderFlags;
const parseAmount = require('./amount');
@@ -31,11 +31,14 @@ function parseAccountOrder(address: string, order: Object): Object {
expirationTime: utils.parseTimestamp(order.expiration)
});
const makerExchangeRate = order.quality ?
utils.adjustQualityForXRP(order.quality.toString(),
takerGetsAmount.currency, takerPaysAmount.currency) :
computeQuality(takerGetsAmount, takerPaysAmount);
const properties = {
maker: address,
sequence: order.seq,
makerExchangeRate: order.quality ? order.quality.toString()
: computeQuality(takerGetsAmount, takerPaysAmount)
makerExchangeRate: makerExchangeRate
};
return {specification, properties};

View File

@@ -1,6 +1,6 @@
/* @flow */
'use strict';
const utils = require('./utils');
const utils = require('../utils');
import type {Amount, RippledAmount} from '../../common/types.js';
@@ -8,7 +8,7 @@ function parseAmount(amount: RippledAmount): Amount {
if (typeof amount === 'string') {
return {
currency: 'XRP',
value: utils.dropsToXrp(amount)
value: utils.common.dropsToXrp(amount)
};
}
return {

View File

@@ -6,10 +6,6 @@ const utils = require('./utils');
const parseAmount = require('./amount');
const txFlags = utils.txFlags;
function isPartialPayment(tx) {
return (tx.Flags & txFlags.Payment.PartialPayment) !== 0;
}
function isNoDirectRipple(tx) {
return (tx.Flags & txFlags.Payment.NoRippleDirect) !== 0;
}
@@ -45,7 +41,7 @@ function parsePayment(tx: Object): Object {
memos: utils.parseMemos(tx),
invoiceID: tx.InvoiceID,
paths: tx.Paths ? JSON.stringify(tx.Paths) : undefined,
allowPartialPayment: isPartialPayment(tx) || undefined,
allowPartialPayment: utils.isPartialPayment(tx) || undefined,
noDirectRipple: isNoDirectRipple(tx) || undefined,
limitQuality: isQualityLimited(tx) || undefined
});

View File

@@ -4,6 +4,9 @@ const _ = require('lodash');
const transactionParser = require('ripple-lib-transactionparser');
const utils = require('../utils');
const BigNumber = require('bignumber.js');
const parseAmount = require('./amount');
import type {Amount} from '../common/types.js';
function adjustQualityForXRP(
quality: string, takerGetsCurrency: string, takerPaysCurrency: string
@@ -35,19 +38,39 @@ function removeEmptyCounterparty(amount) {
}
function removeEmptyCounterpartyInBalanceChanges(balanceChanges) {
_.forEach(balanceChanges, (changes) => {
_.forEach(balanceChanges, changes => {
_.forEach(changes, removeEmptyCounterparty);
});
}
function removeEmptyCounterpartyInOrderbookChanges(orderbookChanges) {
_.forEach(orderbookChanges, (changes) => {
_.forEach(changes, (change) => {
_.forEach(orderbookChanges, changes => {
_.forEach(changes, change => {
_.forEach(change, removeEmptyCounterparty);
});
});
}
function isPartialPayment(tx) {
return (tx.Flags & utils.common.txFlags.Payment.PartialPayment) !== 0;
}
function parseDeliveredAmount(tx: Object): Amount | void {
let deliveredAmount;
// TODO: Workaround for existing rippled bug where delivered_amount may not be
// provided for account_tx
if (tx.TransactionType === 'Payment') {
if (tx.meta.delivered_amount) {
deliveredAmount = parseAmount(tx.meta.delivered_amount);
} else if (tx.Amount && !isPartialPayment(tx)) {
deliveredAmount = parseAmount(tx.Amount);
}
}
return deliveredAmount;
}
function parseOutcome(tx: Object): ?Object {
const metadata = tx.meta || tx.metaData;
if (!metadata) {
@@ -58,15 +81,16 @@ function parseOutcome(tx: Object): ?Object {
removeEmptyCounterpartyInBalanceChanges(balanceChanges);
removeEmptyCounterpartyInOrderbookChanges(orderbookChanges);
return {
return utils.common.removeUndefined({
result: tx.meta.TransactionResult,
timestamp: parseTimestamp(tx.date),
fee: utils.common.dropsToXrp(tx.Fee),
balanceChanges: balanceChanges,
orderbookChanges: orderbookChanges,
ledgerVersion: tx.ledger_index,
indexInLedger: tx.meta.TransactionIndex
};
indexInLedger: tx.meta.TransactionIndex,
deliveredAmount: parseDeliveredAmount(tx)
});
}
function hexToString(hex: string): ?string {
@@ -77,7 +101,7 @@ function parseMemos(tx: Object): ?Array<Object> {
if (!Array.isArray(tx.Memos) || tx.Memos.length === 0) {
return undefined;
}
return tx.Memos.map((m) => {
return tx.Memos.map(m => {
return utils.common.removeUndefined({
type: m.Memo.parsed_memo_type || hexToString(m.Memo.MemoType),
format: m.Memo.parsed_memo_format || hexToString(m.Memo.MemoFormat),
@@ -93,6 +117,7 @@ module.exports = {
hexToString,
parseTimestamp,
adjustQualityForXRP,
isPartialPayment,
dropsToXrp: utils.common.dropsToXrp,
constants: utils.common.constants,
txFlags: utils.common.txFlags,

View File

@@ -93,7 +93,7 @@ function filterSourceFundsLowPaths(pathfind: PathFind,
paths.alternatives = _.filter(paths.alternatives, alt => {
return alt.source_amount &&
pathfind.source.amount &&
alt.source_amount.value === pathfind.source.amount.value;
new BigNumber(alt.source_amount.value).eq(pathfind.source.amount.value);
});
}
return paths;

View File

@@ -1,5 +1,5 @@
/* eslint-disable max-nested-callbacks */
'use strict';
'use strict'; // eslint-disable-line
const _ = require('lodash');
const assert = require('assert-diff');
const setupAPI = require('./setup-api');
@@ -981,6 +981,19 @@ describe('RippleAPI', function() {
});
});
it('getServerInfo - no validated ledger', function() {
this.api.connection._send(JSON.stringify({
command: 'config',
data: {serverInfoWithoutValidated: true}
}));
return this.api.getServerInfo().then(info => {
assert.strictEqual(info.networkLedger, 'waiting');
}).catch(error => {
assert(false, 'Should not throw Error, got ' + String(error));
});
});
it('getFee', function() {
return this.api.getFee().then(fee => {
assert.strictEqual(fee, '0.000012');

View File

@@ -1,5 +1,5 @@
'use strict'; // eslint-disable-line
/* eslint-disable max-nested-callbacks */
'use strict';
const _ = require('lodash');
const net = require('net');
@@ -106,6 +106,28 @@ describe('Connection', function() {
});
});
it('should throw NotConnectedError if server not responding ', function(
done
) {
if (process.browser) {
const phantomTest = /PhantomJS/;
if (phantomTest.test(navigator.userAgent)) {
// inside PhantomJS this one just hangs, so skip as not very relevant
done();
return;
}
}
// Address where no one listens
const connection =
new utils.common.Connection('ws://testripple.circleci.com:129');
connection.on('error', done);
connection.connect().catch(error => {
assert(error instanceof this.api.errors.NotConnectedError);
done();
});
});
it('DisconnectedError', function() {
this.api.connection._send(JSON.stringify({
command: 'config',
@@ -172,6 +194,81 @@ describe('Connection', function() {
}, 1);
});
it('reconnect on several unexpected close', function(done) {
if (process.browser) {
// can't be tested in browser this way, so skipping
done();
return;
}
this.timeout(7000);
const self = this;
function breakConnection() {
setTimeout(() => {
self.mockRippled.close();
setTimeout(() => {
self.mockRippled = setupAPI.createMockRippled(self._mockedServerPort);
}, 1500);
}, 21);
}
let connectsCount = 0;
let disconnectsCount = 0;
let code = 0;
this.api.connection.on('disconnected', _code => {
code = _code;
disconnectsCount += 1;
});
this.api.connection.on('connected', () => {
connectsCount += 1;
if (connectsCount < 3) {
breakConnection();
}
if (connectsCount === 3) {
if (disconnectsCount !== 3) {
done(new Error('disconnectsCount must be equal to 3 (got ' +
disconnectsCount + ' instead)'));
} else if (code !== 1006) {
done(new Error('disconnect must send code 1006 (got ' + code +
' instead)'));
} else {
done();
}
}
});
breakConnection();
});
it('should emit disconnected event with code 1000 (CLOSE_NORMAL)',
function(done
) {
this.api.once('disconnected', code => {
assert.strictEqual(code, 1000);
done();
});
this.api.disconnect();
});
it('should emit disconnected event with code 1006 (CLOSE_ABNORMAL)',
function(done
) {
if (process.browser) {
// can't be tested in browser this way, so skipping
done();
return;
}
this.api.once('disconnected', code => {
assert.strictEqual(code, 1006);
done();
});
this.mockRippled.close();
});
it('should emit connected event on after reconnect', function(done) {
this.api.once('connected', done);
this.api.connection._ws.close();
});
it('Multiply connect calls', function() {
return this.api.connect().then(() => {
return this.api.connect();
@@ -282,4 +379,58 @@ describe('Connection', function() {
});
this.api.connection._ws.emit('message', JSON.stringify(message));
});
it('should throw NotConnectedError if server does not have validated ledgers',
function() {
if (process.browser) {
// do not work in browser now, skipping
return false;
}
this.timeout(3000);
this.api.connection._send(JSON.stringify({
command: 'global_config',
data: {returnEmptySubscribeRequest: 1}
}));
const api = new RippleAPI({server: this.api.connection._url});
return api.connect().then(() => {
assert(false, 'Must have thrown!');
}, error => {
assert(error instanceof this.api.errors.NotConnectedError,
'Must throw NotConnectedError, got instead ' + String(error));
});
});
it('should try to reconnect on empty subscribe response on reconnect',
function(done) {
if (process.browser) {
// do not work in browser now, skipping
done();
return;
}
this.timeout(23000);
this.api.on('error', error => {
done(error || new Error('Should not emit error.'));
});
let disconncedCount = 0;
this.api.on('connected', () => {
done(disconncedCount !== 1 ?
new Error('Wrong number of disconnects') : undefined);
});
this.api.on('disconnected', () => {
disconncedCount++;
});
this.api.connection._send(JSON.stringify({
command: 'global_config',
data: {returnEmptySubscribeRequest: 3}
}));
this.api.connection._send(JSON.stringify({
command: 'test_command',
data: {disconnectIn: 10}
}));
});
});

View File

@@ -3,7 +3,7 @@
"address": "r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59",
"amount": {
"currency": "USD",
"value": "5"
"value": "5.00"
}
},
"destination": {

View File

@@ -34,6 +34,10 @@
"outcome": {
"result": "tesSUCCESS",
"fee": "0.00001",
"deliveredAmount": {
"currency": "XRP",
"value": "10000"
},
"balanceChanges": {
"rLQBHVhFnaC5gLEkgr6HgBJJ3bgeZHg9cj": [
{

View File

@@ -24,6 +24,11 @@
"result": "tesSUCCESS",
"timestamp": "2013-03-12T23:56:50.000Z",
"fee": "0.00001",
"deliveredAmount": {
"currency": "USD",
"value": "0.001",
"counterparty": "rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM"
},
"balanceChanges": {
"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo": [
{

View File

@@ -30,6 +30,11 @@
"outcome": {
"result": "tesSUCCESS",
"fee": "0.00001",
"deliveredAmount": {
"currency": "USD",
"value": "0.001",
"counterparty": "rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM"
},
"balanceChanges": {
"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo": [
{
@@ -122,6 +127,11 @@
"outcome": {
"result": "tesSUCCESS",
"fee": "0.00001",
"deliveredAmount": {
"currency": "USD",
"value": "0.001",
"counterparty": "rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM"
},
"balanceChanges": {
"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo": [
{

View File

@@ -1,4 +1,4 @@
'use strict';
'use strict'; // eslint-disable-line strict
const _ = require('lodash');
const addresses = require('../addresses');
@@ -160,6 +160,7 @@ module.exports = function(request, options = {}) {
{
'flags': 0,
'seq': 814018,
'quality': '16922629533.64839',
'taker_gets': {
'currency': 'NZD',
'issuer': 'rsP3mgGb2tcYUrxiLFiHJiQXhsziegtwBc',

7
test/fixtures/rippled/empty.json vendored Normal file
View File

@@ -0,0 +1,7 @@
{
"id": 0,
"status": "success",
"type": "response",
"result": {
}
}

View File

@@ -1,4 +1,4 @@
'use strict';
'use strict'; // eslint-disable-line
module.exports = {
submit: {
@@ -12,6 +12,7 @@ module.exports = {
withSettingsTx: require('./ledger-with-settings-tx'),
withStateAsHashes: require('./ledger-with-state-as-hashes')
},
empty: require('./empty'),
subscribe: require('./subscribe'),
unsubscribe: require('./unsubscribe'),
account_info: {
@@ -31,6 +32,7 @@ module.exports = {
},
server_info: {
normal: require('./server-info'),
noValidated: require('./server-info-no-validated'),
error: require('./server-info-error')
},
path_find: {

View File

@@ -0,0 +1,31 @@
{
"id": 0,
"status": "success",
"type": "response",
"result": {
"info": {
"build_version": "0.30.1-hf2",
"complete_ledgers": "empty",
"hostid": "ARTS",
"io_latency_ms": 1,
"last_close": {
"converge_time_s": 2.007,
"proposers": 4
},
"load_factor": 1,
"peers": 53,
"pubkey_node": "n94wWvFUmaKGYrKUGgpv1DyYgDeXRGdACkNQaSe7zJiy5Znio7UC",
"server_state": "connected",
"network_ledger" : "waiting",
"closed_ledger": {
"age": 5,
"base_fee_xrp": 0.00001,
"hash": "4482DEE5362332F54A4036ED57EE1767C9F33CF7CE5A6670355C16CECE381D46",
"reserve_base_xrp": 20,
"reserve_inc_xrp": 5,
"seq": 6595042
},
"validation_quorum": 3
}
}
}

View File

@@ -1,4 +1,4 @@
'use strict';
'use strict'; // eslint-disable-line
const _ = require('lodash');
const assert = require('assert');
const WebSocketServer = require('ws').Server;
@@ -79,6 +79,8 @@ module.exports = function(port) {
});
});
mock.config = {};
mock.onAny(function() {
if (this.event.indexOf('request_') !== 0) {
return;
@@ -101,6 +103,18 @@ module.exports = function(port) {
conn.config = _.assign(conn.config, request.data);
});
mock.on('request_test_command', function(request, conn) {
assert.strictEqual(request.command, 'test_command');
if (request.data.disconnectIn) {
setTimeout(conn.terminate.bind(conn), request.data.disconnectIn);
}
});
mock.on('request_global_config', function(request, conn) {
assert.strictEqual(request.command, 'global_config');
mock.config = _.assign(conn.config, request.data);
});
mock.on('request_echo', function(request, conn) {
assert.strictEqual(request.command, 'echo');
conn.send(JSON.stringify(request.data));
@@ -112,6 +126,8 @@ module.exports = function(port) {
conn.send(createResponse(request, fixtures.server_info.error));
} else if (conn.config.disconnectOnServerInfo) {
conn.close();
} else if (conn.config.serverInfoWithoutValidated) {
conn.send(createResponse(request, fixtures.server_info.noValidated));
} else {
conn.send(createResponse(request, fixtures.server_info.normal));
}
@@ -119,7 +135,10 @@ module.exports = function(port) {
mock.on('request_subscribe', function(request, conn) {
assert.strictEqual(request.command, 'subscribe');
if (request.accounts) {
if (mock.config.returnEmptySubscribeRequest) {
mock.config.returnEmptySubscribeRequest--;
conn.send(createResponse(request, fixtures.empty));
} else if (request.accounts) {
assert(_.indexOf(_.values(addresses), request.accounts[0]) !== -1);
}
conn.send(createResponse(request, fixtures.subscribe));

View File

@@ -1,4 +1,5 @@
'use strict';
'use strict'; // eslint-disable-line
const net = require('net');
const RippleAPI = require('ripple-api').RippleAPI;
const RippleAPIBroadcast = require('ripple-api').RippleAPIBroadcast;
@@ -27,6 +28,7 @@ function getFreePort() {
function setupMockRippledConnection(testcase, port) {
return new Promise((resolve, reject) => {
testcase.mockRippled = createMockRippled(port);
testcase._mockedServerPort = port;
testcase.api = new RippleAPI({server: 'ws://localhost:' + port});
testcase.api.connect().then(() => {
testcase.api.once('ledger', () => resolve());
@@ -73,5 +75,6 @@ function teardown(done) {
module.exports = {
setup: setup,
teardown: teardown,
setupBroadcast: setupBroadcast
setupBroadcast: setupBroadcast,
createMockRippled: createMockRippled
};