mirror of
https://github.com/Xahau/xahau.js.git
synced 2026-04-29 15:37:50 +00:00
Move ripple-rest/api into src/api, exposing RippleAPI
This commit is contained in:
28
npm-shrinkwrap.json
generated
28
npm-shrinkwrap.json
generated
@@ -26,6 +26,20 @@
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-1.2.1.tgz"
|
||||
},
|
||||
"jayschema": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/jayschema/-/jayschema-0.3.1.tgz",
|
||||
"dependencies": {
|
||||
"when": {
|
||||
"version": "3.4.6",
|
||||
"resolved": "https://registry.npmjs.org/when/-/when-3.4.6.tgz"
|
||||
}
|
||||
}
|
||||
},
|
||||
"jayschema-error-messages": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/jayschema-error-messages/-/jayschema-error-messages-1.0.3.tgz"
|
||||
},
|
||||
"lodash": {
|
||||
"version": "3.9.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-3.9.3.tgz"
|
||||
@@ -34,10 +48,24 @@
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.5.2.tgz"
|
||||
},
|
||||
"ripple-lib-transactionparser": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ripple-lib-transactionparser/-/ripple-lib-transactionparser-0.3.2.tgz",
|
||||
"dependencies": {
|
||||
"bignumber.js": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-1.4.1.tgz"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ripple-wallet-generator": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/ripple-wallet-generator/-/ripple-wallet-generator-1.0.3.tgz"
|
||||
},
|
||||
"simple-asyncify": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/simple-asyncify/-/simple-asyncify-0.1.0.tgz"
|
||||
},
|
||||
"sjcl": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sjcl/-/sjcl-1.0.3.tgz"
|
||||
|
||||
@@ -18,9 +18,13 @@
|
||||
"babel-runtime": "^5.3.2",
|
||||
"bignumber.js": "^2.0.3",
|
||||
"extend": "~1.2.1",
|
||||
"jayschema": "^0.3.1",
|
||||
"jayschema-error-messages": "^1.0.3",
|
||||
"lodash": "^3.1.0",
|
||||
"lru-cache": "~2.5.0",
|
||||
"ripple-lib-transactionparser": "^0.3.2",
|
||||
"ripple-wallet-generator": "^1.0.3",
|
||||
"simple-asyncify": "^0.1.0",
|
||||
"sjcl": "^1.0.3",
|
||||
"ws": "~0.7.1"
|
||||
},
|
||||
@@ -31,6 +35,7 @@
|
||||
"babel-loader": "^5.0.0",
|
||||
"coveralls": "~2.10.0",
|
||||
"eslint": "^0.18.0",
|
||||
"eventemitter2": "^0.4.14",
|
||||
"gulp": "~3.8.10",
|
||||
"gulp-bump": "~0.1.13",
|
||||
"gulp-clean-dest": "^0.1.0",
|
||||
|
||||
57
src/api/api.js
Normal file
57
src/api/api.js
Normal file
@@ -0,0 +1,57 @@
|
||||
'use strict';
|
||||
const ripple = require('./common').core;
|
||||
const generateWallet = require('./generate/wallet');
|
||||
const server = require('./server/server');
|
||||
const balances = require('./ledger/balances');
|
||||
const settings = require('./ledger/settings');
|
||||
const transactions = require('./ledger/transactions');
|
||||
const trustlines = require('./ledger/trustlines');
|
||||
const notifications = require('./ledger/notifications');
|
||||
const payments = require('./ledger/payments');
|
||||
const orders = require('./ledger/orders');
|
||||
const preparePayment = require('./transaction/payment');
|
||||
const prepareOrder = require('./transaction/order');
|
||||
const prepareOrderCancellation = require('./transaction/ordercancellation');
|
||||
const prepareTrustline = require('./transaction/trustline');
|
||||
const prepareSettings = require('./transaction/settings');
|
||||
const sign = require('./transaction/sign');
|
||||
const submit = require('./transaction/submit');
|
||||
const errors = require('./common').errors;
|
||||
|
||||
function RippleAPI(options) {
|
||||
this.remote = new ripple.Remote(options);
|
||||
}
|
||||
|
||||
RippleAPI.prototype = {
|
||||
generateWallet: generateWallet,
|
||||
|
||||
connect: server.connect,
|
||||
getServerStatus: server.getServerStatus,
|
||||
getFee: server.getFee,
|
||||
isConnected: server.isConnected,
|
||||
|
||||
getBalances: balances.getBalances,
|
||||
getPayment: payments.getPayment,
|
||||
getAccountPayments: payments.getAccountPayments,
|
||||
getPathFind: payments.getPathFind,
|
||||
getTrustlines: trustlines.getTrustlines,
|
||||
getOrder: orders.getOrder,
|
||||
getOrders: orders.getOrders,
|
||||
getOrderBook: orders.getOrderBook,
|
||||
getSettings: settings.getSettings,
|
||||
getTransaction: transactions.getTransaction,
|
||||
getNotification: notifications.getNotification,
|
||||
getNotifications: notifications.getNotifications,
|
||||
|
||||
preparePayment: preparePayment,
|
||||
prepareTrustline: prepareTrustline,
|
||||
prepareOrder: prepareOrder,
|
||||
prepareOrderCancellation: prepareOrderCancellation,
|
||||
prepareSettings: prepareSettings,
|
||||
sign: sign,
|
||||
submit: submit,
|
||||
|
||||
errors: errors
|
||||
};
|
||||
|
||||
module.exports = RippleAPI;
|
||||
104
src/api/common/constants.js
Normal file
104
src/api/common/constants.js
Normal file
@@ -0,0 +1,104 @@
|
||||
'use strict';
|
||||
const ripple = require('./core');
|
||||
|
||||
const AccountRootFlags = {
|
||||
PasswordSpent: {
|
||||
name: 'password_spent',
|
||||
value: ripple.Remote.flags.account_root.PasswordSpent
|
||||
},
|
||||
RequireDestTag: {
|
||||
name: 'require_destination_tag',
|
||||
value: ripple.Remote.flags.account_root.RequireDestTag
|
||||
},
|
||||
RequireAuth: {
|
||||
name: 'require_authorization',
|
||||
value: ripple.Remote.flags.account_root.RequireAuth
|
||||
},
|
||||
DisallowXRP: {
|
||||
name: 'disallow_xrp',
|
||||
value: ripple.Remote.flags.account_root.DisallowXRP
|
||||
},
|
||||
DisableMaster: {
|
||||
name: 'disable_master',
|
||||
value: ripple.Remote.flags.account_root.DisableMaster
|
||||
},
|
||||
NoFreeze: {
|
||||
name: 'no_freeze',
|
||||
value: ripple.Remote.flags.account_root.NoFreeze
|
||||
},
|
||||
GlobalFreeze: {
|
||||
name: 'global_freeze',
|
||||
value: ripple.Remote.flags.account_root.GlobalFreeze
|
||||
},
|
||||
DefaultRipple: {
|
||||
name: 'default_ripple',
|
||||
value: ripple.Remote.flags.account_root.DefaultRipple
|
||||
}
|
||||
};
|
||||
|
||||
const AccountRootFields = {
|
||||
Sequence: {name: 'transaction_sequence'},
|
||||
EmailHash: {name: 'email_hash', encoding: 'hex', length: 32, defaults: '0'},
|
||||
WalletLocator: {name: 'wallet_locator', encoding: 'hex',
|
||||
length: 64, defaults: '0'},
|
||||
WalletSize: {name: 'wallet_size', defaults: 0},
|
||||
MessageKey: {name: 'message_key'},
|
||||
Domain: {name: 'domain', encoding: 'hex'},
|
||||
TransferRate: {name: 'transfer_rate', defaults: 0},
|
||||
Signers: {name: 'signers'}
|
||||
};
|
||||
|
||||
const AccountSetIntFlags = {
|
||||
NoFreeze: {name: 'no_freeze',
|
||||
value: ripple.Transaction.set_clear_flags.AccountSet.asfNoFreeze},
|
||||
GlobalFreeze: {name: 'global_freeze',
|
||||
value: ripple.Transaction.set_clear_flags.AccountSet.asfGlobalFreeze},
|
||||
DefaultRipple: {name: 'default_ripple',
|
||||
value: ripple.Transaction.set_clear_flags.AccountSet.asfDefaultRipple}
|
||||
};
|
||||
|
||||
const AccountSetFlags = {
|
||||
RequireDestTag: {name: 'require_destination_tag', set: 'RequireDestTag',
|
||||
unset: 'OptionalDestTag'},
|
||||
RequireAuth: {name: 'require_authorization', set: 'RequireAuth',
|
||||
unset: 'OptionalAuth'},
|
||||
DisallowXRP: {name: 'disallow_xrp', set: 'DisallowXRP', unset: 'AllowXRP'}
|
||||
};
|
||||
|
||||
const AccountSetResponseFlags = {
|
||||
RequireDestTag: {name: 'require_destination_tag',
|
||||
value: ripple.Transaction.flags.AccountSet.RequireDestTag},
|
||||
RequireAuth: {name: 'require_authorization',
|
||||
value: ripple.Transaction.flags.AccountSet.RequireAuth},
|
||||
DisallowXRP: {name: 'disallow_xrp',
|
||||
value: ripple.Transaction.flags.AccountSet.DisallowXRP}
|
||||
};
|
||||
|
||||
const OfferCreateFlags = {
|
||||
Passive: {name: 'passive',
|
||||
value: ripple.Transaction.flags.OfferCreate.Passive},
|
||||
ImmediateOrCancel: {name: 'immediate_or_cancel',
|
||||
value: ripple.Transaction.flags.OfferCreate.ImmediateOrCancel},
|
||||
FillOrKill: {name: 'fill_or_kill',
|
||||
value: ripple.Transaction.flags.OfferCreate.FillOrKill},
|
||||
Sell: {name: 'sell', value: ripple.Transaction.flags.OfferCreate.Sell}
|
||||
};
|
||||
|
||||
const TrustSetResponseFlags = {
|
||||
NoRipple: {name: 'prevent_rippling',
|
||||
value: ripple.Transaction.flags.TrustSet.NoRipple},
|
||||
SetFreeze: {name: 'account_trustline_frozen',
|
||||
value: ripple.Transaction.flags.TrustSet.SetFreeze},
|
||||
SetAuth: {name: 'authorized',
|
||||
value: ripple.Transaction.flags.TrustSet.SetAuth}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
AccountRootFlags: AccountRootFlags,
|
||||
AccountRootFields: AccountRootFields,
|
||||
AccountSetIntFlags: AccountSetIntFlags,
|
||||
AccountSetFlags: AccountSetFlags,
|
||||
AccountSetResponseFlags: AccountSetResponseFlags,
|
||||
OfferCreateFlags: OfferCreateFlags,
|
||||
TrustSetResponseFlags: TrustSetResponseFlags
|
||||
};
|
||||
2
src/api/common/core.js
Normal file
2
src/api/common/core.js
Normal file
@@ -0,0 +1,2 @@
|
||||
'use strict';
|
||||
module.exports = require('../../core');
|
||||
94
src/api/common/errors.js
Normal file
94
src/api/common/errors.js
Normal file
@@ -0,0 +1,94 @@
|
||||
/* eslint-disable valid-jsdoc */
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Base class for all errors
|
||||
*/
|
||||
function RippleError(message) {
|
||||
this.message = message;
|
||||
}
|
||||
RippleError.prototype = new Error();
|
||||
RippleError.prototype.name = 'RippleError';
|
||||
|
||||
/**
|
||||
* Invalid Request Error
|
||||
* Missing parameters or invalid parameters
|
||||
*/
|
||||
function InvalidRequestError(message) {
|
||||
this.message = message;
|
||||
}
|
||||
InvalidRequestError.prototype = new RippleError();
|
||||
InvalidRequestError.prototype.name = 'InvalidRequestError';
|
||||
InvalidRequestError.prototype.error = 'restINVALID_PARAMETER';
|
||||
|
||||
/**
|
||||
* Network Error
|
||||
* Timeout, disconnects and too busy
|
||||
*/
|
||||
function NetworkError(message) {
|
||||
this.message = message;
|
||||
}
|
||||
NetworkError.prototype = new RippleError();
|
||||
NetworkError.prototype.name = 'NetworkError';
|
||||
|
||||
/**
|
||||
* Rippled NetworkError
|
||||
* Failed transactions, no paths found, not enough balance, etc.
|
||||
*/
|
||||
function RippledNetworkError(message) {
|
||||
this.message = message !== undefined ? message : 'Cannot connect to rippled';
|
||||
}
|
||||
RippledNetworkError.prototype = new NetworkError();
|
||||
RippledNetworkError.prototype.error = 'restRIPPLED_NETWORK_ERR';
|
||||
|
||||
/**
|
||||
* Transaction Error
|
||||
* Failed transactions, no paths found, not enough balance, etc.
|
||||
*/
|
||||
function TransactionError(message) {
|
||||
this.message = message;
|
||||
}
|
||||
TransactionError.prototype = new RippleError();
|
||||
TransactionError.prototype.name = 'TransactionError';
|
||||
|
||||
/**
|
||||
* Not Found Error
|
||||
* Asset could not be found
|
||||
*/
|
||||
function NotFoundError(message) {
|
||||
this.message = message;
|
||||
}
|
||||
NotFoundError.prototype = new RippleError();
|
||||
NotFoundError.prototype.name = 'NotFoundError';
|
||||
NotFoundError.prototype.error = 'restNOT_FOUND';
|
||||
|
||||
/**
|
||||
* Time Out Error
|
||||
* Request timed out
|
||||
*/
|
||||
function TimeOutError(message) {
|
||||
this.message = message;
|
||||
}
|
||||
TimeOutError.prototype = new RippleError();
|
||||
TimeOutError.prototype.name = 'TimeOutError';
|
||||
|
||||
/**
|
||||
* API Error
|
||||
* API logic failed to do what it intended
|
||||
*/
|
||||
function ApiError(message) {
|
||||
this.message = message;
|
||||
}
|
||||
ApiError.prototype = new RippleError();
|
||||
ApiError.prototype.name = 'ApiError';
|
||||
|
||||
module.exports = {
|
||||
InvalidRequestError: InvalidRequestError,
|
||||
NetworkError: NetworkError,
|
||||
TransactionError: TransactionError,
|
||||
RippledNetworkError: RippledNetworkError,
|
||||
NotFoundError: NotFoundError,
|
||||
TimeOutError: TimeOutError,
|
||||
ApiError: ApiError,
|
||||
RippleError: RippleError
|
||||
};
|
||||
14
src/api/common/index.js
Normal file
14
src/api/common/index.js
Normal file
@@ -0,0 +1,14 @@
|
||||
'use strict';
|
||||
const utils = require('./utils');
|
||||
|
||||
module.exports = {
|
||||
core: require('./core'),
|
||||
constants: require('./constants'),
|
||||
errors: require('./errors'),
|
||||
schemaValidator: require('./schema-validator'),
|
||||
validate: require('./validate'),
|
||||
server: require('./server'),
|
||||
dropsToXrp: utils.dropsToXrp,
|
||||
xrpToDrops: utils.xrpToDrops,
|
||||
convertAmount: utils.convertAmount
|
||||
};
|
||||
45
src/api/common/schema-validator.js
Normal file
45
src/api/common/schema-validator.js
Normal file
@@ -0,0 +1,45 @@
|
||||
'use strict';
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const JaySchema = require('jayschema');
|
||||
const formatJaySchemaErrors = require('jayschema-error-messages');
|
||||
|
||||
const baseDir = path.join(__dirname, './schemas');
|
||||
|
||||
module.exports = (function() {
|
||||
const validator = new JaySchema();
|
||||
const validate = validator.validate;
|
||||
|
||||
// If schema is valid, return true. Otherwise
|
||||
// return array of validation errors
|
||||
validator.validate = function() {
|
||||
const errors = validate.apply(validator, arguments);
|
||||
return {
|
||||
err: errors,
|
||||
errors: formatJaySchemaErrors(errors),
|
||||
isValid: errors.length === 0
|
||||
};
|
||||
};
|
||||
|
||||
validator.isValid = function() {
|
||||
return validator.validate.apply(validator, arguments).isValid;
|
||||
};
|
||||
|
||||
// Load Schemas
|
||||
fs.readdirSync(baseDir).filter(function(fileName) {
|
||||
return /^[\w\s]+\.json$/.test(fileName);
|
||||
})
|
||||
.map(function(fileName) {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(path.join(baseDir, fileName), 'utf8'));
|
||||
} catch (e) {
|
||||
throw new Error('Failed to parse schema: ' + fileName);
|
||||
}
|
||||
})
|
||||
.forEach(function(schema) {
|
||||
schema.id = schema.title;
|
||||
validator.register(schema);
|
||||
});
|
||||
|
||||
return validator;
|
||||
})();
|
||||
71
src/api/common/schemas/AccountSettings.json
Normal file
71
src/api/common/schemas/AccountSettings.json
Normal file
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"title": "AccountSettings",
|
||||
"description": "An object ",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"account": {
|
||||
"description": "The Ripple address of the account in question",
|
||||
"$ref": "RippleAddress"
|
||||
},
|
||||
"regular_key": {
|
||||
"description": "The hash of an optional additional public key that can be used for signing and verifying transactions",
|
||||
"$ref": "RippleAddress"
|
||||
},
|
||||
"domain": {
|
||||
"description": "The domain associated with this account. The ripple.txt file can be looked up to verify this information",
|
||||
"$ref": "URL"
|
||||
},
|
||||
"email_hash": {
|
||||
"description": "The MD5 128-bit hash of the account owner's email address",
|
||||
"$ref": "Hash128"
|
||||
},
|
||||
"message_key": {
|
||||
"description": "An optional public key, represented as hex, that can be set to allow others to send encrypted messages to the account owner",
|
||||
"type": "string",
|
||||
"pattern": "^([0-9a-fA-F]{2}){0,33}$"
|
||||
},
|
||||
"transfer_rate": {
|
||||
"description": "A number representation of the rate charged each time a holder of currency issued by this account transfers it. By default the rate is 100. A rate of 101 is a 1% charge on top of the amount being transferred. Up to nine decimal places are supported",
|
||||
"type": "UINT32"
|
||||
},
|
||||
"password_spent": {
|
||||
"description": "If false, then this account can submit a special SetRegularKey transaction without a transaction fee.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"require_destination_tag": {
|
||||
"description": "If set to true incoming payments will only be validated if they include a destination_tag. This may be used primarily by gateways that operate exclusively with hosted wallets",
|
||||
"type": "boolean"
|
||||
},
|
||||
"require_authorization": {
|
||||
"description": "If set to true incoming trustlines will only be validated if this account first creates a trustline to the counterparty with the authorized flag set to true. This may be used by gateways to prevent accounts unknown to them from holding currencies they issue",
|
||||
"type": "boolean"
|
||||
},
|
||||
"disallow_xrp": {
|
||||
"description": "If set to true incoming XRP payments will be allowed",
|
||||
"type": "boolean"
|
||||
},
|
||||
"disable_master": {
|
||||
"description": "If true, the master secret key cannot be used to sign transactions for this account. Can only be set to true if a Regular Key is defined for the account.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"transaction_sequence": {
|
||||
"description": "A string representation of the last sequence number of a validated transaction created by this account",
|
||||
"$ref": "UINT32"
|
||||
},
|
||||
"trustline_count": {
|
||||
"description": "The number of trustlines owned by this account. This value does not include incoming trustlines where this account has not explicitly reciprocated trust",
|
||||
"$ref": "UINT32"
|
||||
},
|
||||
"ledger": {
|
||||
"description": "The string representation of the index number of the ledger containing these account settings or, in the case of historical queries, of the transaction that modified these settings",
|
||||
"type": "string",
|
||||
"pattern": "^[0-9]+$"
|
||||
},
|
||||
"hash": {
|
||||
"description": "If this object was returned by a historical query this value will be the hash of the transaction that modified these settings. The transaction hash is used throughout the Ripple Protocol to uniquely identify a particular transaction",
|
||||
"$ref": "Hash256"
|
||||
}
|
||||
},
|
||||
"required": ["account"]
|
||||
}
|
||||
28
src/api/common/schemas/Amount.json
Normal file
28
src/api/common/schemas/Amount.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"title": "Amount",
|
||||
"description": "An Amount on the Ripple Protocol, used also for XRP in the ripple-rest API",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"value": {
|
||||
"description": "The quantity of the currency, denoted as a string to retain floating point precision",
|
||||
"type": "string",
|
||||
"$ref": "FloatString"
|
||||
},
|
||||
"currency": {
|
||||
"description": "The three-character code or hex string used to denote currencies",
|
||||
"$ref": "Currency"
|
||||
},
|
||||
"issuer": {
|
||||
"description": "The Ripple account address of the currency's issuer or gateway, or an empty string if the currency is XRP",
|
||||
"type": "string",
|
||||
"pattern": "^$|^r[1-9A-HJ-NP-Za-km-z]{25,33}$"
|
||||
},
|
||||
"counterparty": {
|
||||
"description": "The Ripple account address of the currency's issuer or gateway, or an empty string if the currency is XRP",
|
||||
"type": "string",
|
||||
"pattern": "^$|^r[1-9A-HJ-NP-Za-km-z]{25,33}$"
|
||||
}
|
||||
},
|
||||
"required": ["value", "currency"]
|
||||
}
|
||||
23
src/api/common/schemas/Balance.json
Normal file
23
src/api/common/schemas/Balance.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"title": "Balance",
|
||||
"description": "A simplified representation of an account Balance",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"value": {
|
||||
"description": "The quantity of the currency, denoted as a string to retain floating point precision",
|
||||
"type": "string",
|
||||
"$ref": "FloatString"
|
||||
},
|
||||
"currency": {
|
||||
"description": "The currency expressed as a three-character code",
|
||||
"$ref": "Currency"
|
||||
},
|
||||
"counterparty": {
|
||||
"description": "The Ripple account address of the currency's issuer or gateway, or an empty string if the currency is XRP",
|
||||
"type": "string",
|
||||
"pattern": "^$|^r[1-9A-HJ-NP-Za-km-z]{25,33}$"
|
||||
}
|
||||
},
|
||||
"required": [ "value", "currency" ]
|
||||
}
|
||||
7
src/api/common/schemas/Currency.json
Normal file
7
src/api/common/schemas/Currency.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"title": "Currency",
|
||||
"description": "The three-character code or hex string used to denote currencies",
|
||||
"type": "string",
|
||||
"pattern": "^([a-zA-Z0-9]{3}|[A-Fa-f0-9]{40})$"
|
||||
}
|
||||
7
src/api/common/schemas/FloatString.json
Normal file
7
src/api/common/schemas/FloatString.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"title": "FloatString",
|
||||
"description": "A string representation of a floating point number",
|
||||
"type": "string",
|
||||
"pattern": "^[-+]?[0-9]*[.]?[0-9]+([eE][-+]?[0-9]+)?$"
|
||||
}
|
||||
7
src/api/common/schemas/Hash128.json
Normal file
7
src/api/common/schemas/Hash128.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"title": "Hash128",
|
||||
"description": "The hex representation of a 128-bit hash",
|
||||
"type": "string",
|
||||
"pattern": "^$|^[A-Fa-f0-9]{32}$"
|
||||
}
|
||||
7
src/api/common/schemas/Hash256.json
Normal file
7
src/api/common/schemas/Hash256.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"title": "Hash256",
|
||||
"description": "The hex representation of a 256-bit hash",
|
||||
"type": "string",
|
||||
"pattern": "^[A-Fa-f0-9]{64}$"
|
||||
}
|
||||
58
src/api/common/schemas/Notification.json
Normal file
58
src/api/common/schemas/Notification.json
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"title": "Notification",
|
||||
"description": "A ",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"account": {
|
||||
"description": "The Ripple address of the account to which the notification pertains",
|
||||
"$ref": "RippleAddress"
|
||||
},
|
||||
"type": {
|
||||
"description": "The resource type this notification corresponds to. Possible values are \"payment\", \"order\", \"trustline\", \"accountsettings\"",
|
||||
"type": "string",
|
||||
"pattern": "^payment|order|trustline|accountsettings$"
|
||||
},
|
||||
"direction": {
|
||||
"description": "The direction of the transaction, from the perspective of the account being queried. Possible values are \"incoming\", \"outgoing\", and \"passthrough\"",
|
||||
"type": "string",
|
||||
"pattern": "^incoming|outgoing|passthrough$"
|
||||
},
|
||||
"state": {
|
||||
"description": "The state of the transaction from the perspective of the Ripple Ledger. Possible values are \"validated\" and \"failed\"",
|
||||
"type": "string",
|
||||
"pattern": "^validated|failed$"
|
||||
},
|
||||
"result": {
|
||||
"description": "The rippled code indicating the success or failure type of the transaction. The code \"tesSUCCESS\" indicates that the transaction was successfully validated and written into the Ripple Ledger. All other codes will begin with the following prefixes: \"tec\", \"tef\", \"tel\", or \"tej\"",
|
||||
"type": "string",
|
||||
"pattern": "te[cfjlms][A-Za-z_]+"
|
||||
},
|
||||
"ledger": {
|
||||
"description": "The string representation of the index number of the ledger containing the validated or failed transaction. Failed payments will only be written into the Ripple Ledger if they fail after submission to a rippled and a Ripple Network fee is claimed",
|
||||
"type": "string",
|
||||
"pattern": "^[0-9]+$"
|
||||
},
|
||||
"hash": {
|
||||
"description": "The 256-bit hash of the transaction. This is used throughout the Ripple protocol as the unique identifier for the transaction",
|
||||
"$ref": "Hash256"
|
||||
},
|
||||
"timestamp": {
|
||||
"description": "The timestamp representing when the transaction was validated and written into the Ripple ledger",
|
||||
"$ref": "Timestamp"
|
||||
},
|
||||
"transaction_url": {
|
||||
"description": "A URL that can be used to fetch the full resource this notification corresponds to",
|
||||
"type": "string"
|
||||
},
|
||||
"previous_notification_url": {
|
||||
"description": "A URL that can be used to fetch the notification that preceded this one chronologically",
|
||||
"type": "string"
|
||||
},
|
||||
"next_notification_url": {
|
||||
"description": "A URL that can be used to fetch the notification that followed this one chronologically",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
}
|
||||
86
src/api/common/schemas/Order.json
Normal file
86
src/api/common/schemas/Order.json
Normal file
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"title": "Order",
|
||||
"description": "A simplified Order object used by the ripple-rest API (note that \"orders\" are referred to elsewhere in the Ripple protocol as \"offers\")",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"account": {
|
||||
"description": "The Ripple account address of the order's creator",
|
||||
"$ref": "RippleAddress"
|
||||
},
|
||||
"type": {
|
||||
"description": "If set to true the order it indicates that the creator is looking to receive the base_amount in exchange for the counter_amount. If undefined or set to false it indicates that the creator is looking to sell the base_amount to receive the counter_amount",
|
||||
"enum": ["buy", "sell"]
|
||||
},
|
||||
"taker_pays": {
|
||||
"description": "The amount of currency the seller_account is seeking to buy. If other orders take part of this one, this value will change to represent the amount left in the order. This may be specified along with the counter_amount OR exchange_rate but not both. When the order is parsed from the Ripple Ledger the base currency will be determined according to the Priority Ranking of Currencies (XRP,EUR,GBP,AUD,NZD,USD,CAD,CHF,JPY,CNY) and if neither currency is listed in the ranking the base currency will be the one that is alphabetically first",
|
||||
"$ref": "Amount"
|
||||
},
|
||||
"taker_gets": {
|
||||
"description": "The amount of currency being sold. If other orders take part of this one, this value will change to represent the amount left in the order. This may be specified along with the base_amount OR the exchange_rate but not both",
|
||||
"$ref": "Amount"
|
||||
},
|
||||
"exchange_rate": {
|
||||
"description": "A string representation of the order price, defined as the cost one unit of the base currency in terms of the counter currency. This may be specified along with the base_amount OR the counter_amount but not both. If it is unspecified it will be computed automatically based on the counter_amount divided by the base_amount",
|
||||
"$ref": "FloatString"
|
||||
},
|
||||
"passive": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"expiration_timestamp": {
|
||||
"description": "The ISO combined date and time string representing the point beyond which the order will no longer be considered active or valid",
|
||||
"$ref": "Timestamp"
|
||||
},
|
||||
"ledger_timeout": {
|
||||
"description": "A string representation of the number of ledger closes after submission during which the order should be considered active",
|
||||
"type": "string",
|
||||
"pattern": "^[0-9]*$"
|
||||
},
|
||||
"immediate_or_cancel": {
|
||||
"description": "If set to true this order will only take orders that are available at the time of execution and will not create an entry in the Ripple Ledger",
|
||||
"type": "boolean"
|
||||
},
|
||||
"fill_or_kill": {
|
||||
"description": "If set to true this order will only take orders that fill the base_amount and are available at the time of execution and will not create an entry in the Ripple Ledger",
|
||||
"type": "boolean"
|
||||
},
|
||||
"maximize_buy_or_sell": {
|
||||
"description": "If set to true and it is a buy order it will buy up to the base_amount even if the counter_amount is exceeded, if it is a sell order it will sell up to the counter_amount even if the base_amount is exceeded",
|
||||
"type": "boolean"
|
||||
},
|
||||
"cancel_replace": {
|
||||
"description": "If this is set to the sequence number of an outstanding order, that order will be cancelled and replaced with this one",
|
||||
"type": "string",
|
||||
"pattern": "^d*$"
|
||||
},
|
||||
"sequence": {
|
||||
"description": "The sequence number of this order from the perspective of the seller_account. The seller_account and the sequence number uniquely identify the order in the Ripple Ledger",
|
||||
"type": "string",
|
||||
"pattern": "^[0-9]*$"
|
||||
},
|
||||
"fee": {
|
||||
"description": "The Ripple Network transaction fee, represented in whole XRP (NOT \"drops\", or millionths of an XRP, which is used elsewhere in the Ripple protocol) used to create the order",
|
||||
"$ref": "FloatString"
|
||||
},
|
||||
"state": {
|
||||
"description": "If the order is active the state will be \"active\". If this object represents a historical order the state will be \"validated\", \"filled\" if the order was removed because it was fully filled, \"cancelled\" if it was deleted by the owner, \"expired\" if it reached the expiration_timestamp, or \"failed\" if there was an error with the initial attempt to place the order",
|
||||
"type": "string",
|
||||
"pattern": "^active|validated|filled|cancelled|expired|failed$"
|
||||
},
|
||||
"ledger": {
|
||||
"description": "The string representation of the index number of the ledger containing this order or, in the case of historical queries, of the transaction that modified this Order. ",
|
||||
"type": "string",
|
||||
"pattern": "^[0-9]+$"
|
||||
},
|
||||
"hash": {
|
||||
"description": "When returned as the result of a historical query this will be the hash of Ripple transaction that created, modified, or deleted this order. The transaction hash is used throughout the Ripple Protocol to uniquely identify a particular transaction",
|
||||
"$ref": "Hash256"
|
||||
},
|
||||
"previous": {
|
||||
"description": "If the order was modified or partially filled this will be a full Order object. If the previous object also had a previous object that will be removed to reduce data complexity. Order changes can be walked backwards by querying the API for previous.hash repeatedly",
|
||||
"$ref": "Order"
|
||||
}
|
||||
},
|
||||
"required": ["type", "taker_gets", "taker_pays"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
106
src/api/common/schemas/Payment.json
Normal file
106
src/api/common/schemas/Payment.json
Normal file
@@ -0,0 +1,106 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"title": "Payment",
|
||||
"description": "A flattened Payment object used by the ripple-rest API",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"source_account": {
|
||||
"description": "The Ripple account address of the Payment sender",
|
||||
"$ref": "RippleAddress"
|
||||
},
|
||||
"source_tag": {
|
||||
"description": "A string representing an unsigned 32-bit integer most commonly used to refer to a sender's hosted account at a Ripple gateway",
|
||||
"$ref": "UINT32"
|
||||
},
|
||||
"source_amount": {
|
||||
"description": "An optional amount that can be specified to constrain cross-currency payments. The amount the source_account will send or has send",
|
||||
"$ref": "Amount"
|
||||
},
|
||||
"source_amount_submitted": {
|
||||
"description": "An optional amount that can be specified to constrain cross-currency payments. The amount the source_account intended to send",
|
||||
"$ref": "Amount"
|
||||
},
|
||||
"source_slippage": {
|
||||
"description": "An optional cushion for the source_amount to increase the likelihood that the payment will succeed. The source_account will never be charged more than source_amount.value + source_slippage",
|
||||
"$ref": "FloatString"
|
||||
},
|
||||
"destination_account": {
|
||||
"$ref": "RippleAddress"
|
||||
},
|
||||
"destination_tag": {
|
||||
"description": "A string representing an unsigned 32-bit integer most commonly used to refer to a receiver's hosted account at a Ripple gateway",
|
||||
"$ref": "UINT32"
|
||||
},
|
||||
"destination_amount": {
|
||||
"description": "The amount the destination_account will receive or has received",
|
||||
"$ref": "Amount"
|
||||
},
|
||||
"destination_amount_submitted": {
|
||||
"description": "The amount the destination_account was intended to receive",
|
||||
"$ref": "Amount"
|
||||
},
|
||||
"invoice_id": {
|
||||
"description": "A 256-bit hash that can be used to identify a particular payment",
|
||||
"$ref": "Hash256"
|
||||
},
|
||||
"paths": {
|
||||
"description": "A \"stringified\" version of the Ripple PathSet structure that users should treat as opaque",
|
||||
"type": "string"
|
||||
},
|
||||
"partial_payment": {
|
||||
"description": "A boolean that, if set to true, indicates that this payment should go through even if the whole amount cannot be delivered because of a lack of liquidity or funds in the source_account account",
|
||||
"type": "boolean"
|
||||
},
|
||||
"no_direct_ripple": {
|
||||
"description": "A boolean that can be set to true if paths are specified and the sender would like the Ripple Network to disregard any direct paths from the source_account to the destination_account. This may be used to take advantage of an arbitrage opportunity or by gateways wishing to issue balances from a hot wallet to a user who has mistakenly set a trustline directly to the hot wallet",
|
||||
"type": "boolean"
|
||||
},
|
||||
"direction": {
|
||||
"description": "The direction of the payment, from the perspective of the account being queried. Possible values are \"incoming\", \"outgoing\", and \"passthrough\"",
|
||||
"type": "string",
|
||||
"pattern": "^incoming|outgoing|passthrough$"
|
||||
},
|
||||
"state": {
|
||||
"description": "The state of the payment from the perspective of the Ripple Ledger. Possible values are \"validated\" and \"failed\" and \"new\" if the payment has not been submitted yet",
|
||||
"type": "string",
|
||||
"pattern": "^validated|failed|new$"
|
||||
},
|
||||
"result": {
|
||||
"description": "The rippled code indicating the success or failure type of the payment. The code \"tesSUCCESS\" indicates that the payment was successfully validated and written into the Ripple Ledger. All other codes will begin with the following prefixes: \"tec\", \"tef\", \"tel\", or \"tej\"",
|
||||
"type": "string",
|
||||
"pattern": "te[cfjlms][A-Za-z_]+"
|
||||
},
|
||||
"ledger": {
|
||||
"description": "The string representation of the index number of the ledger containing the validated or failed payment. Failed payments will only be written into the Ripple Ledger if they fail after submission to a rippled and a Ripple Network fee is claimed",
|
||||
"type": "string",
|
||||
"pattern": "^[0-9]+$"
|
||||
},
|
||||
"hash": {
|
||||
"description": "The 256-bit hash of the payment. This is used throughout the Ripple protocol as the unique identifier for the transaction",
|
||||
"$ref": "Hash256"
|
||||
},
|
||||
"timestamp": {
|
||||
"description": "The timestamp representing when the payment was validated and written into the Ripple ledger",
|
||||
"$ref": "Timestamp"
|
||||
},
|
||||
"fee": {
|
||||
"description": "The Ripple Network transaction fee, represented in whole XRP (NOT \"drops\", or millionths of an XRP, which is used elsewhere in the Ripple protocol)",
|
||||
"$ref": "FloatString"
|
||||
},
|
||||
"source_balance_changes": {
|
||||
"description": "Parsed from the validated transaction metadata, this array represents all of the changes to balances held by the source_account. Most often this will have one amount representing the Ripple Network fee and, if the source_amount was not XRP, one amount representing the actual source_amount that was sent",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "Amount"
|
||||
}
|
||||
},
|
||||
"destination_balance_changes": {
|
||||
"description": "Parsed from the validated transaction metadata, this array represents the changes to balances held by the destination_account. The summation of the balance changes should equal the destination_amount. Use the balance changes to validate the destination_amount.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "Amount"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["source_account", "destination_account", "destination_amount"]
|
||||
}
|
||||
7
src/api/common/schemas/Timestamp.json
Normal file
7
src/api/common/schemas/Timestamp.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"title": "Timestamp",
|
||||
"description": "An ISO 8601 combined date and time timestamp",
|
||||
"type": "string",
|
||||
"pattern": "^$|^[0-9]{4}-[0-1][0-9]-[0-3][0-9]T(2[0-3]|[01][0-9]):[0-5][0-9]:[0-5][0-9](Z|[+](2[0-3]|[01][0-9]):[0-5][0-9])$"
|
||||
}
|
||||
58
src/api/common/schemas/Trustline.json
Normal file
58
src/api/common/schemas/Trustline.json
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"title": "Trustline",
|
||||
"description": "A simplified Trustline object used by the ripple-rest API",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"account": {
|
||||
"description": "The account from whose perspective this trustline is being viewed",
|
||||
"$ref": "RippleAddress"
|
||||
},
|
||||
"counterparty": {
|
||||
"description": "The other party in this trustline",
|
||||
"$ref": "RippleAddress"
|
||||
},
|
||||
"currency": {
|
||||
"description": "The code of the currency in which this trustline denotes trust",
|
||||
"$ref": "Currency"
|
||||
},
|
||||
"limit": {
|
||||
"description": "The maximum value of the currency that the account may hold issued by the counterparty",
|
||||
"$ref": "FloatString"
|
||||
},
|
||||
"reciprocated_limit": {
|
||||
"description": "The maximum value of the currency that the counterparty may hold issued by the account",
|
||||
"$ref": "FloatString"
|
||||
},
|
||||
"authorized_by_account": {
|
||||
"description": "Set to true if the account has explicitly authorized the counterparty to hold currency it issues. This is only necessary if the account's settings include require_authorization_for_incoming_trustlines",
|
||||
"type": "boolean"
|
||||
},
|
||||
"authorized_by_counterparty": {
|
||||
"description": "Set to true if the counterparty has explicitly authorized the account to hold currency it issues. This is only necessary if the counterparty's settings include require_authorization_for_incoming_trustlines",
|
||||
"type": "boolean"
|
||||
},
|
||||
"account_allows_rippling": {
|
||||
"description": "If true it indicates that the account allows pairwise rippling out through this trustline",
|
||||
"type": "boolean"
|
||||
},
|
||||
"counterparty_allows_rippling": {
|
||||
"description": "If true it indicates that the counterparty allows pairwise rippling out through this trustline",
|
||||
"type": "boolean"
|
||||
},
|
||||
"ledger": {
|
||||
"description": "The string representation of the index number of the ledger containing this trustline or, in the case of historical queries, of the transaction that modified this Trustline",
|
||||
"type": "string",
|
||||
"pattern": "^[0-9]+$"
|
||||
},
|
||||
"hash": {
|
||||
"description": "If this object was returned by a historical query this value will be the hash of the transaction that modified this Trustline. The transaction hash is used throughout the Ripple Protocol to uniquely identify a particular transaction",
|
||||
"$ref": "Hash256"
|
||||
},
|
||||
"previous": {
|
||||
"description": "If the trustline was changed this will be a full Trustline object representing the previous values. If the previous object also had a previous object that will be removed to reduce data complexity. Trustline changes can be walked backwards by querying the API for previous.hash repeatedly",
|
||||
"$ref": "Trustline"
|
||||
}
|
||||
},
|
||||
"required": ["account", "limit"]
|
||||
}
|
||||
7
src/api/common/schemas/UINT32.json
Normal file
7
src/api/common/schemas/UINT32.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"title": "UINT32",
|
||||
"description": "A string representation of an unsigned 32-bit integer (0-4294967295)",
|
||||
"type": "string",
|
||||
"pattern": "^(429496729[0-5]|42949672[0-8][0-9]|4294967[01][0-9]{2}|429496[0-6][0-9]{3}|42949[0-5][0-9]{4}|4294[0-8][0-9]{5}|429[0-3][0-9]{6}|42[0-8][0-9]{7}|4[01][0-9]{8}|[1-3][0-9]{9}|[1-9][0-9]{8}|[1-9][0-9]{7}|[1-9][0-9]{6}|[1-9][0-9]{5}|[1-9][0-9]{4}|[1-9][0-9]{3}|[1-9][0-9]{2}|[1-9][0-9]|[0-9])$"
|
||||
}
|
||||
7
src/api/common/schemas/URL.json
Normal file
7
src/api/common/schemas/URL.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"title": "URL",
|
||||
"description": "A standard URL",
|
||||
"type": "string",
|
||||
"pattern": "^(ftp:\/\/|http:\/\/|https:\/\/)?([A-Za-z0-9_]+:{0,1}[A-Za-z0-9_]*@)?(^([ \t\r\n\f])+)(:[0-9]+)?(\/|\/([[A-Za-z0-9_]#!:.?+=&%@!-\/]))?$"
|
||||
}
|
||||
116
src/api/common/server.js
Normal file
116
src/api/common/server.js
Normal file
@@ -0,0 +1,116 @@
|
||||
/* eslint-disable valid-jsdoc */
|
||||
'use strict';
|
||||
const async = require('async');
|
||||
|
||||
/**
|
||||
* If a ledger is not received in this time, consider the connection offline
|
||||
*/
|
||||
const CONNECTION_TIMEOUT = 1000 * 30;
|
||||
|
||||
/**
|
||||
* Determine if remote is connected based on time of last ledger closed
|
||||
*
|
||||
* @param {Server} server
|
||||
* @return {Boolean}
|
||||
*/
|
||||
function isConnected(remote) {
|
||||
if (isNaN(remote._ledger_current_index)) {
|
||||
// Remote is missing the index of last ledger closed. Unprepared to submit
|
||||
// transactions
|
||||
return false;
|
||||
}
|
||||
|
||||
const server = remote.getServer();
|
||||
if (!server) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (remote._stand_alone) {
|
||||
// If rippled is in standalone mode we can assume there will not be a
|
||||
// ledger close within 30 seconds.
|
||||
return true;
|
||||
}
|
||||
|
||||
return (Date.now() - server._lastLedgerClose) <= CONNECTION_TIMEOUT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if remote is connected and attempt to reconnect if not
|
||||
*
|
||||
* @param {Remote} remote
|
||||
* @param {Function} callback
|
||||
*/
|
||||
function ensureConnected(remote, callback) {
|
||||
if (remote.getServer()) {
|
||||
callback(null, isConnected(remote));
|
||||
} else {
|
||||
callback(null, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Remote} remote
|
||||
* @param {Function} callback
|
||||
*/
|
||||
function getStatus(remote, callback) {
|
||||
function checkConnectivity(_callback) {
|
||||
ensureConnected(remote, _callback);
|
||||
}
|
||||
|
||||
function requestServerInfo(connected, _callback) {
|
||||
remote.requestServerInfo(_callback);
|
||||
}
|
||||
|
||||
function prepareResponse(server_info, _callback) {
|
||||
const results = { };
|
||||
|
||||
results.rippled_server_url = remote.getServer()._url;
|
||||
results.rippled_server_status = server_info.info;
|
||||
|
||||
_callback(null, results);
|
||||
}
|
||||
|
||||
const steps = [
|
||||
checkConnectivity,
|
||||
requestServerInfo,
|
||||
prepareResponse
|
||||
];
|
||||
|
||||
async.waterfall(steps, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Remote} remote
|
||||
* @param {Number|String} ledger
|
||||
* @param {Function} callback
|
||||
*/
|
||||
function remoteHasLedger(remote, ledger, callback) {
|
||||
const ledger_index = Number(ledger);
|
||||
|
||||
function handleStatus(err, status) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
const ledger_range = status.rippled_server_status.complete_ledgers;
|
||||
const match = ledger_range.match(/([0-9]+)-([0-9]+)$/);
|
||||
const min = Number(match[1]);
|
||||
const max = Number(match[2]);
|
||||
|
||||
if (ledger_index >= min && ledger_index <= max) {
|
||||
callback(null, true);
|
||||
} else {
|
||||
callback(null, false);
|
||||
}
|
||||
}
|
||||
|
||||
getStatus(remote, handleStatus);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
CONNECTION_TIMEOUT: CONNECTION_TIMEOUT,
|
||||
getStatus: getStatus,
|
||||
isConnected: isConnected,
|
||||
ensureConnected: ensureConnected,
|
||||
remoteHasLedger: remoteHasLedger
|
||||
};
|
||||
27
src/api/common/utils.js
Normal file
27
src/api/common/utils.js
Normal file
@@ -0,0 +1,27 @@
|
||||
'use strict';
|
||||
const BigNumber = require('bignumber.js');
|
||||
|
||||
function dropsToXrp(drops) {
|
||||
return (new BigNumber(drops)).dividedBy(1000000.0).toString();
|
||||
}
|
||||
|
||||
function xrpToDrops(xrp) {
|
||||
return (new BigNumber(xrp)).times(1000000.0).floor().toString();
|
||||
}
|
||||
|
||||
function convertAmount(amount) {
|
||||
if (amount.currency === 'XRP') {
|
||||
return xrpToDrops(amount.value);
|
||||
}
|
||||
return {
|
||||
currency: amount.currency,
|
||||
issuer: amount.counterparty ? amount.counterparty : amount.issuer,
|
||||
value: amount.value
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
dropsToXrp: dropsToXrp,
|
||||
xrpToDrops: xrpToDrops,
|
||||
convertAmount: convertAmount
|
||||
};
|
||||
583
src/api/common/validate.js
Normal file
583
src/api/common/validate.js
Normal file
@@ -0,0 +1,583 @@
|
||||
'use strict';
|
||||
const _ = require('lodash');
|
||||
const InvalidRequestError = require('./errors.js').InvalidRequestError;
|
||||
const validator = require('./schema-validator');
|
||||
const ripple = require('./core');
|
||||
const utils = require('./utils');
|
||||
|
||||
function error(text) {
|
||||
return new InvalidRequestError(text);
|
||||
}
|
||||
|
||||
/* TODO:
|
||||
function invalid(type, value) {
|
||||
return error('Not a valid ' + type + ': ' + JSON.stringify(value));
|
||||
}
|
||||
*/
|
||||
|
||||
function missing(name) {
|
||||
return error('Parameter missing: ' + name);
|
||||
}
|
||||
|
||||
function isValidAddress(address) {
|
||||
return address ? ripple.UInt160.is_valid(address) : false;
|
||||
}
|
||||
|
||||
function validateAddress(address) {
|
||||
if (!isValidAddress(address)) {
|
||||
throw error('Parameter is not a valid Ripple address: account');
|
||||
// TODO: thow invalid('Ripple address', address);
|
||||
}
|
||||
}
|
||||
|
||||
function validateAddressAndSecret(obj) {
|
||||
const address = obj.address;
|
||||
const secret = obj.secret;
|
||||
validateAddress(address);
|
||||
if (!secret) {
|
||||
throw missing('secret');
|
||||
}
|
||||
try {
|
||||
if (!ripple.Seed.from_json(secret).get_key(address)) {
|
||||
throw error('Invalid secret', secret);
|
||||
}
|
||||
} catch (exception) {
|
||||
throw error('Invalid secret', secret);
|
||||
}
|
||||
}
|
||||
|
||||
function validateAddressAndMaybeSecret(obj) {
|
||||
if (obj.secret === undefined) {
|
||||
validateAddress(obj.address);
|
||||
} else {
|
||||
validateAddressAndSecret(obj);
|
||||
}
|
||||
}
|
||||
|
||||
function validateCurrency(currency) {
|
||||
if (!validator.isValid(currency, 'Currency')) {
|
||||
throw error('Parameter is not a valid currency: currency');
|
||||
// TODO: throw invalid('currency', currency);
|
||||
}
|
||||
}
|
||||
|
||||
function validateCounterparty(counterparty) {
|
||||
if (!isValidAddress(counterparty)) {
|
||||
throw error('Parameter is not a valid Ripple address: counterparty');
|
||||
// TODO: throw invalid('counterparty', counterparty);
|
||||
}
|
||||
}
|
||||
|
||||
function validateIssue(issue) {
|
||||
validateCurrency(issue.currency);
|
||||
validateCounterparty(issue.counterparty);
|
||||
}
|
||||
|
||||
function validateLedger(ledger) {
|
||||
if (!(utils.isValidLedgerSequence(ledger)
|
||||
|| utils.isValidLedgerHash(ledger)
|
||||
|| utils.isValidLedgerWord(ledger))) {
|
||||
throw error('Invalid or Missing Parameter: ledger');
|
||||
// TODO: throw invalid('ledger', ledger);
|
||||
}
|
||||
}
|
||||
|
||||
function validatePaging(options) {
|
||||
if (options.marker) {
|
||||
if (!options.ledger) {
|
||||
throw error('Invalid or Missing Parameter: ledger');
|
||||
// TODO: throw missing('ledger');
|
||||
}
|
||||
if (!(utils.isValidLedgerSequence(options.ledger)
|
||||
|| utils.isValidLedgerHash(options.ledger))) {
|
||||
throw error('Invalid or Missing Parameter: ledger');
|
||||
// TODO: throw invalid('ledger', options.ledger);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateLimit(limit) {
|
||||
if (!(limit === 'all' || !_.isNaN(Number(limit)))) {
|
||||
throw error('Invalid or Missing Parameter: limit');
|
||||
// TODO: throw invalid('limit', limit);
|
||||
}
|
||||
}
|
||||
|
||||
function validateIdentifier(identifier) {
|
||||
if (!validator.isValid(identifier, 'Hash256')) {
|
||||
throw error('Parameter is not a valid transaction hash: identifier');
|
||||
}
|
||||
}
|
||||
|
||||
function validateSequence(sequence) {
|
||||
if (!(Number(sequence) >= 0)) {
|
||||
throw error(
|
||||
'Invalid parameter: sequence. Sequence must be a positive number');
|
||||
}
|
||||
}
|
||||
|
||||
/* TODO:
|
||||
function validateSchema(object, schemaName) {
|
||||
const schemaErrors = validator.validate(object, schemaName).errors;
|
||||
if (!_.isEmpty(schemaErrors.fields)) {
|
||||
throw invalid(schemaName, schemaErrors.fields);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
function isValidValue(value) {
|
||||
return typeof value === 'string' && value.length > 0 && isFinite(value);
|
||||
}
|
||||
|
||||
function isValidCurrency(currency) {
|
||||
return currency && validator.isValid(currency, 'Currency');
|
||||
}
|
||||
|
||||
function isValidIssue(issue) {
|
||||
return issue && isValidCurrency(issue.currency)
|
||||
&& ((issue.currency === 'XRP' && !issue.counterparty && !issue.issuer)
|
||||
|| (issue.currency !== 'XRP' && isValidAddress(
|
||||
issue.counterparty || issue.issuer)));
|
||||
}
|
||||
|
||||
function isValidAmount(amount) {
|
||||
return isValidIssue(amount) && isValidValue(amount.value);
|
||||
}
|
||||
|
||||
function validateOrder(order) {
|
||||
if (!order) {
|
||||
throw error('Missing parameter: order. '
|
||||
+ 'Submission must have order object in JSON form');
|
||||
} else if (!/^buy|sell$/.test(order.type)) {
|
||||
throw error('Parameter must be "buy" or "sell": type');
|
||||
} else if (!_.isUndefined(order.passive) && !_.isBoolean(order.passive)) {
|
||||
throw error('Parameter must be a boolean: passive');
|
||||
} else if (!_.isUndefined(order.immediate_or_cancel)
|
||||
&& !_.isBoolean(order.immediate_or_cancel)) {
|
||||
throw error('Parameter must be a boolean: immediate_or_cancel');
|
||||
} else if (!_.isUndefined(order.fill_or_kill)
|
||||
&& !_.isBoolean(order.fill_or_kill)) {
|
||||
throw error('Parameter must be a boolean: fill_or_kill');
|
||||
} else if (!isValidAmount(order.taker_gets)) {
|
||||
throw error('Parameter must be a valid Amount object: taker_gets');
|
||||
} else if (!isValidAmount(order.taker_pays)) {
|
||||
throw error('Parameter must be a valid Amount object: taker_pays');
|
||||
}
|
||||
// TODO: validateSchema(order, 'Order');
|
||||
}
|
||||
|
||||
function validateOrderbook(orderbook) {
|
||||
if (orderbook.counter && orderbook.counter.currency === 'XRP'
|
||||
&& orderbook.counter.counterparty) {
|
||||
throw error('Invalid parameter: counter. XRP cannot have counterparty');
|
||||
}
|
||||
if (orderbook.base && orderbook.base.currency === 'XRP'
|
||||
&& orderbook.base.counterparty) {
|
||||
throw error('Invalid parameter: base. XRP cannot have counterparty');
|
||||
}
|
||||
if (!isValidIssue(orderbook.base)) {
|
||||
throw error('Invalid parameter: base. '
|
||||
+ 'Must be a currency string in the form currency+counterparty');
|
||||
}
|
||||
if (!isValidIssue(orderbook.counter)) {
|
||||
throw error('Invalid parameter: counter. '
|
||||
+ 'Must be a currency string in the form currency+counterparty');
|
||||
}
|
||||
}
|
||||
|
||||
function validateLastLedgerSequence(lastLedgerSequence) {
|
||||
if (!utils.isValidLedgerSequence(lastLedgerSequence)) {
|
||||
throw error('Invalid parameter: last_ledger_sequence');
|
||||
}
|
||||
}
|
||||
|
||||
function validatePaymentMemos(memos) {
|
||||
if (!Array.isArray(memos)) {
|
||||
throw error(
|
||||
'Invalid parameter: memos. Must be an array with memo objects');
|
||||
}
|
||||
|
||||
if (memos.length === 0) {
|
||||
throw error('Invalid parameter: memos. '
|
||||
+ 'Must contain at least one Memo object, '
|
||||
+ 'otherwise omit the memos property');
|
||||
}
|
||||
|
||||
for (let m = 0; m < memos.length; m++) {
|
||||
const memo = memos[m];
|
||||
if (memo.MemoType && !/(undefined|string)/.test(typeof memo.MemoType)) {
|
||||
throw error(
|
||||
'Invalid parameter: MemoType. MemoType must be a string');
|
||||
}
|
||||
if (!/(undefined|string)/.test(typeof memo.MemoData)) {
|
||||
throw error(
|
||||
'Invalid parameter: MemoData. MemoData must be a string');
|
||||
}
|
||||
if (!memo.MemoData && !memo.MemoType) {
|
||||
throw error('Missing parameter: '
|
||||
+ 'MemoData or MemoType. For a memo object MemoType or MemoData '
|
||||
+ 'are both optional, as long as one of them is present');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validatePayment(payment) {
|
||||
if (!isValidAddress(payment.source_account)) {
|
||||
throw error('Invalid parameter: source_account. '
|
||||
+ 'Must be a valid Ripple address');
|
||||
}
|
||||
|
||||
if (!isValidAddress(payment.destination_account)) {
|
||||
throw error('Invalid parameter: '
|
||||
+ 'destination_account. Must be a valid Ripple address');
|
||||
}
|
||||
|
||||
if (payment.source_tag &&
|
||||
(!validator.isValid(payment.source_tag, 'UINT32'))) {
|
||||
throw error('Invalid parameter: source_tag. '
|
||||
+ 'Must be a string representation of an unsiged 32-bit integer');
|
||||
}
|
||||
|
||||
if (payment.destination_tag
|
||||
&& (!validator.isValid(payment.destination_tag, 'UINT32'))) {
|
||||
throw error('Invalid parameter: '
|
||||
+ 'destination_tag. Must be a string representation of an unsiged '
|
||||
+ '32-bit integer');
|
||||
}
|
||||
|
||||
if (!payment.destination_amount
|
||||
|| (!validator.isValid(payment.destination_amount, 'Amount'))) {
|
||||
throw error('Invalid parameter: '
|
||||
+ 'destination_amount. Must be a valid Amount object');
|
||||
}
|
||||
|
||||
if (payment.source_amount // source_amount is optional
|
||||
&& (!validator.isValid(payment.source_amount, 'Amount'))) {
|
||||
throw error(
|
||||
'Invalid parameter: source_amount. Must be a valid Amount object');
|
||||
}
|
||||
|
||||
if (payment.destination_amount
|
||||
&& payment.destination_amount.currency.toUpperCase() === 'XRP'
|
||||
&& payment.destination_amount.issuer) {
|
||||
throw error(
|
||||
'Invalid parameter: destination_amount. XRP cannot have issuer');
|
||||
}
|
||||
if (payment.source_amount
|
||||
&& payment.source_amount.currency.toUpperCase() === 'XRP'
|
||||
&& payment.source_amount.issuer) {
|
||||
throw error(
|
||||
'Invalid parameter: source_amount. XRP cannot have issuer');
|
||||
}
|
||||
|
||||
if (payment.source_slippage
|
||||
&& !validator.isValid(payment.source_slippage, 'FloatString')) {
|
||||
throw error(
|
||||
'Invalid parameter: source_slippage. Must be a valid FloatString');
|
||||
}
|
||||
|
||||
if (payment.invoice_id
|
||||
&& !validator.isValid(payment.invoice_id, 'Hash256')) {
|
||||
throw error(
|
||||
'Invalid parameter: invoice_id. Must be a valid Hash256');
|
||||
}
|
||||
|
||||
if (payment.paths) {
|
||||
if (typeof payment.paths === 'string') {
|
||||
try {
|
||||
JSON.parse(payment.paths);
|
||||
} catch (exception) {
|
||||
throw error(
|
||||
'Invalid parameter: paths. Must be a valid JSON string or object');
|
||||
}
|
||||
} else if (typeof payment.paths === 'object') {
|
||||
try {
|
||||
JSON.parse(JSON.stringify(payment.paths));
|
||||
} catch (exception) {
|
||||
throw error(
|
||||
'Invalid parameter: paths. Must be a valid JSON string or object');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (payment.hasOwnProperty('partial_payment')
|
||||
&& typeof payment.partial_payment !== 'boolean') {
|
||||
throw error(
|
||||
'Invalid parameter: partial_payment. Must be a boolean');
|
||||
}
|
||||
|
||||
if (payment.hasOwnProperty('no_direct_ripple')
|
||||
&& typeof payment.no_direct_ripple !== 'boolean') {
|
||||
throw error(
|
||||
'Invalid parameter: no_direct_ripple. Must be a boolean');
|
||||
}
|
||||
|
||||
if (payment.hasOwnProperty('memos')) {
|
||||
validatePaymentMemos(payment.memos);
|
||||
}
|
||||
}
|
||||
|
||||
function validatePathFind(pathfind) {
|
||||
if (!pathfind.source_account) {
|
||||
throw error(
|
||||
'Missing parameter: source_account. Must be a valid Ripple address');
|
||||
}
|
||||
|
||||
if (!pathfind.destination_account) {
|
||||
throw error('Missing parameter: destination_account. '
|
||||
+ 'Must be a valid Ripple address');
|
||||
}
|
||||
|
||||
if (!isValidAddress(pathfind.source_account)) {
|
||||
throw error('Parameter is not a valid Ripple address: account');
|
||||
}
|
||||
|
||||
if (!isValidAddress(pathfind.destination_account)) {
|
||||
throw error('Parameter is not a valid Ripple address: destination_account');
|
||||
}
|
||||
|
||||
if (!pathfind.destination_amount) {
|
||||
throw error('Missing parameter: destination_amount. '
|
||||
+ 'Must be an amount string in the form value+currency+issuer');
|
||||
}
|
||||
|
||||
if (!validator.isValid(pathfind.destination_amount, 'Amount')) {
|
||||
throw error('Invalid parameter: destination_amount. '
|
||||
+ 'Must be an amount string in the form value+currency+issuer');
|
||||
}
|
||||
}
|
||||
|
||||
function validateSettings(settings) {
|
||||
if (typeof settings !== 'object') {
|
||||
throw error('Invalid parameter: settings');
|
||||
}
|
||||
if (!/(undefined|string)/.test(typeof settings.domain)) {
|
||||
throw error('Parameter must be a string: domain');
|
||||
}
|
||||
if (!/(undefined|string)/.test(typeof settings.wallet_locator)) {
|
||||
throw error('Parameter must be a string: wallet_locator');
|
||||
}
|
||||
if (!/(undefined|string)/.test(typeof settings.email_hash)) {
|
||||
throw error('Parameter must be a string: email_hash');
|
||||
}
|
||||
if (!/(undefined|string)/.test(typeof settings.message_key)) {
|
||||
throw error('Parameter must be a string: message_key');
|
||||
}
|
||||
if (!/(undefined|number)/.test(typeof settings.transfer_rate)) {
|
||||
if (settings.transfer_rate !== '') {
|
||||
throw error('Parameter must be a number: transfer_rate');
|
||||
}
|
||||
}
|
||||
if (!/(undefined|number)/.test(typeof settings.wallet_size)) {
|
||||
if (settings.wallet_size !== '') {
|
||||
throw error('Parameter must be a number: wallet_size');
|
||||
}
|
||||
}
|
||||
if (!/(undefined|boolean)/.test(typeof settings.no_freeze)) {
|
||||
throw error('Parameter must be a boolean: no_freeze');
|
||||
}
|
||||
if (!/(undefined|boolean)/.test(typeof settings.global_freeze)) {
|
||||
throw error('Parameter must be a boolean: global_freeze');
|
||||
}
|
||||
if (!/(undefined|boolean)/.test(typeof settings.password_spent)) {
|
||||
throw error('Parameter must be a boolean: password_spent');
|
||||
}
|
||||
if (!/(undefined|boolean)/.test(typeof settings.disable_master)) {
|
||||
throw error('Parameter must be a boolean: disable_master');
|
||||
}
|
||||
if (!/(undefined|boolean)/.test(typeof settings.require_destination_tag)) {
|
||||
throw error('Parameter must be a boolean: require_destination_tag');
|
||||
}
|
||||
if (!/(undefined|boolean)/.test(typeof settings.require_authorization)) {
|
||||
throw error('Parameter must be a boolean: require_authorization');
|
||||
}
|
||||
if (!/(undefined|boolean)/.test(typeof settings.disallow_xrp)) {
|
||||
throw error('Parameter must be a boolean: disallow_xrp');
|
||||
}
|
||||
|
||||
const setCollision = (typeof settings.no_freeze === 'boolean')
|
||||
&& (typeof settings.global_freeze === 'boolean')
|
||||
&& settings.no_freeze === settings.global_freeze;
|
||||
|
||||
if (setCollision) {
|
||||
throw error('Unable to set/clear no_freeze and global_freeze');
|
||||
}
|
||||
}
|
||||
|
||||
function validateTrustline(trustline) {
|
||||
if (typeof trustline !== 'object') {
|
||||
throw error('Invalid parameter: trustline');
|
||||
}
|
||||
if (_.isUndefined(trustline.limit)) {
|
||||
throw error('Parameter missing: trustline.limit');
|
||||
}
|
||||
if (isNaN(trustline.limit)) {
|
||||
throw error('Parameter is not a number: trustline.limit');
|
||||
}
|
||||
if (!trustline.currency) {
|
||||
throw error('Parameter missing: trustline.currency');
|
||||
}
|
||||
if (!validator.isValid(trustline.currency, 'Currency')) {
|
||||
throw error('Parameter is not a valid currency: trustline.currency');
|
||||
}
|
||||
if (!trustline.counterparty) {
|
||||
throw error('Parameter missing: trustline.counterparty');
|
||||
}
|
||||
if (!isValidAddress(trustline.counterparty)) {
|
||||
throw error('Parameter is not a Ripple address: trustline.counterparty');
|
||||
}
|
||||
if (!/^(undefined|number)$/.test(typeof trustline.quality_in)) {
|
||||
throw error('Parameter must be a number: trustline.quality_in');
|
||||
}
|
||||
if (!/^(undefined|number)$/.test(typeof trustline.quality_out)) {
|
||||
throw error('Parameter must be a number: trustline.quality_out');
|
||||
}
|
||||
if (!/^(undefined|boolean)$/.test(typeof trustline.account_allows_rippling)) {
|
||||
throw error('Parameter must be a boolean: trustline.allow_rippling');
|
||||
}
|
||||
// TODO: validateSchema(trustline, 'Trustline');
|
||||
}
|
||||
|
||||
function validateTxJSON(txJSON) {
|
||||
if (typeof txJSON !== 'object') {
|
||||
throw error('tx_json must be an object, not: ' + typeof txJSON);
|
||||
}
|
||||
if (!isValidAddress(txJSON.Account)) {
|
||||
throw error('tx_json.Account must be a valid Ripple address, got: '
|
||||
+ txJSON.Account);
|
||||
}
|
||||
}
|
||||
|
||||
function validateBlob(blob) {
|
||||
if (typeof blob !== 'string') {
|
||||
throw error('tx_blob must be a string, not: ' + typeof blob);
|
||||
}
|
||||
if (blob.length === 0) {
|
||||
throw error('tx_blob must not be empty');
|
||||
}
|
||||
if (!blob.match(/[0-9A-F]+/g)) {
|
||||
throw error('tx_blob must be an uppercase hex string, got: ' + blob);
|
||||
}
|
||||
}
|
||||
|
||||
function isNumeric(value) {
|
||||
return !isNaN(parseFloat(value)) && isFinite(value);
|
||||
}
|
||||
|
||||
function validateNonNegativeStringFloat(value, name) {
|
||||
if (typeof value !== 'string') {
|
||||
throw error(name + ' must be a string, not: ' + typeof value);
|
||||
}
|
||||
if (!isNumeric(value)) {
|
||||
throw error(name + ' must be a numeric string, not: ' + value);
|
||||
}
|
||||
if (parseFloat(value) < 0) {
|
||||
throw error(name + ' must be non-negative, got: ' + parseFloat(value));
|
||||
}
|
||||
}
|
||||
|
||||
function validateNonNegativeStringInteger(value, name) {
|
||||
validateNonNegativeStringFloat(value, name);
|
||||
if (value.indexOf('.') !== -1) {
|
||||
throw error(name + ' must be an integer, got: ' + value);
|
||||
}
|
||||
}
|
||||
|
||||
function validateLedgerRange(options) {
|
||||
if (_.isUndefined(options.min_ledger) || _.isUndefined(options.max_ledger)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const minLedger = Number(options.min_ledger);
|
||||
const maxLedger = Number(options.max_ledger);
|
||||
|
||||
if (!utils.isValidLedgerSequence(minLedger)) {
|
||||
throw error('Invalid parameter: min_ledger must be a number');
|
||||
}
|
||||
if (!utils.isValidLedgerSequence(maxLedger)) {
|
||||
throw error('Invalid parameter: max_ledger must be a number');
|
||||
}
|
||||
if (minLedger > maxLedger) {
|
||||
throw error('Invalid parameter: max_ledger must be '
|
||||
+ 'greater than min_ledger');
|
||||
}
|
||||
}
|
||||
|
||||
function validateOptions(options) {
|
||||
if (options.max_fee !== undefined) {
|
||||
validateNonNegativeStringFloat(options.max_fee, 'max_fee');
|
||||
}
|
||||
if (options.fixed_fee !== undefined) {
|
||||
validateNonNegativeStringFloat(options.fixed_fee, 'fixed_fee');
|
||||
}
|
||||
if (options.max_fee !== undefined && options.fixed_fee !== undefined) {
|
||||
throw error('"max_fee" and "fixed_fee" are mutually exclusive options');
|
||||
}
|
||||
if (options.last_ledger_sequence !== undefined) {
|
||||
validateNonNegativeStringInteger(options.last_ledger_sequence,
|
||||
'last_ledger_sequence');
|
||||
}
|
||||
if (options.last_ledger_offset !== undefined) {
|
||||
validateNonNegativeStringInteger(options.last_ledger_offset,
|
||||
'last_ledger_offset');
|
||||
}
|
||||
if (options.last_ledger_sequence !== undefined
|
||||
&& options.last_ledger_offset !== undefined) {
|
||||
throw error('"last_ledger_sequence" and "last_ledger_offset" are'
|
||||
+ ' mutually exclusive options');
|
||||
}
|
||||
if (options.sequence !== undefined) {
|
||||
validateNonNegativeStringInteger(options.sequence, 'sequence');
|
||||
}
|
||||
if (options.limit !== undefined) {
|
||||
validateLimit(options.limit);
|
||||
}
|
||||
if (options.ledger !== undefined) {
|
||||
validateLedger(options.ledger);
|
||||
}
|
||||
if (options.validated !== undefined && !_.isBoolean(options.validated)) {
|
||||
throw error('"validated" must be boolean, not: ' + options.validated);
|
||||
}
|
||||
if (options.submit !== undefined && !_.isBoolean(options.submit)) {
|
||||
throw error('"submit" must be boolean, not: ' + options.submit);
|
||||
}
|
||||
validateLedgerRange(options);
|
||||
validatePaging(options);
|
||||
}
|
||||
|
||||
function createValidators(validatorMap) {
|
||||
const result = {};
|
||||
_.forEach(validatorMap, function(validateFunction, key) {
|
||||
result[key] = function(value, optional) {
|
||||
if (value === undefined || value === null) {
|
||||
if (!optional) {
|
||||
throw missing(key);
|
||||
}
|
||||
} else {
|
||||
validateFunction(value);
|
||||
}
|
||||
};
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = createValidators({
|
||||
address: validateAddress,
|
||||
addressAndSecret: validateAddressAndSecret,
|
||||
addressAndMaybeSecret: validateAddressAndMaybeSecret,
|
||||
currency: validateCurrency,
|
||||
counterparty: validateCounterparty,
|
||||
issue: validateIssue,
|
||||
identifier: validateIdentifier,
|
||||
sequence: validateSequence,
|
||||
order: validateOrder,
|
||||
orderbook: validateOrderbook,
|
||||
last_ledger_sequence: validateLastLedgerSequence,
|
||||
payment: validatePayment,
|
||||
pathfind: validatePathFind,
|
||||
settings: validateSettings,
|
||||
trustline: validateTrustline,
|
||||
txJSON: validateTxJSON,
|
||||
blob: validateBlob,
|
||||
options: validateOptions
|
||||
});
|
||||
13
src/api/generate/wallet.js
Normal file
13
src/api/generate/wallet.js
Normal file
@@ -0,0 +1,13 @@
|
||||
'use strict';
|
||||
const common = require('../common');
|
||||
|
||||
function generateWallet(callback) {
|
||||
const wallet = common.core.Wallet.generate();
|
||||
if (wallet) {
|
||||
callback(null, {wallet: wallet});
|
||||
} else {
|
||||
callback(new common.errors.ApiError('Could not generate wallet'));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = generateWallet;
|
||||
168
src/api/ledger/balances.js
Normal file
168
src/api/ledger/balances.js
Normal file
@@ -0,0 +1,168 @@
|
||||
/* globals Promise: true */
|
||||
/* eslint-disable valid-jsdoc */
|
||||
'use strict';
|
||||
const utils = require('./utils');
|
||||
const validator = utils.common.schemaValidator;
|
||||
const validate = utils.common.validate;
|
||||
|
||||
const DefaultPageLimit = 200;
|
||||
|
||||
/**
|
||||
* Request the balances for a given account
|
||||
*
|
||||
* Notes:
|
||||
* In order to use paging, you must provide at least ledger as a query
|
||||
* parameter. Additionally, any limit lower than 10 will be bumped up to 10.
|
||||
*
|
||||
* @url
|
||||
* @param {RippleAddress} request.params.account
|
||||
* - account to retrieve balances for
|
||||
*
|
||||
* @query
|
||||
* @param {String ISO 4217 Currency Code} [request.query.currency]
|
||||
* - only request balances with given currency
|
||||
* @param {RippleAddress} [request.query.counterparty]
|
||||
* - only request balances with given counterparty
|
||||
* @param {String} [request.query.marker] - start position in response paging
|
||||
* @param {Number String} [request.query.limit] - max results per response
|
||||
* @param {Number String} [request.query.ledger] - identifier
|
||||
*
|
||||
*/
|
||||
function getBalances(account, options, callback) {
|
||||
validate.address(account);
|
||||
validate.currency(options.currency, true);
|
||||
validate.counterparty(options.counterparty, true);
|
||||
validate.options(options);
|
||||
|
||||
const self = this;
|
||||
|
||||
const currencyRE = new RegExp(options.currency ?
|
||||
('^' + options.currency.toUpperCase() + '$') : /./);
|
||||
|
||||
function getXRPBalance() {
|
||||
const promise = new Promise(function(resolve, reject) {
|
||||
const accountInfoRequest = self.remote.requestAccountInfo({
|
||||
account: account,
|
||||
ledger: utils.parseLedger(options.ledger)
|
||||
});
|
||||
|
||||
const lines = [];
|
||||
accountInfoRequest.once('error', reject);
|
||||
accountInfoRequest.once('success', function(result) {
|
||||
lines.push({
|
||||
value: utils.common.dropsToXrp(result.account_data.Balance),
|
||||
currency: 'XRP',
|
||||
counterparty: ''
|
||||
});
|
||||
|
||||
result.lines = lines;
|
||||
resolve(result);
|
||||
});
|
||||
|
||||
accountInfoRequest.request();
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
function getLineBalances(prevResult) {
|
||||
const isAggregate = options.limit === 'all';
|
||||
if (prevResult && (!isAggregate || !prevResult.marker)) {
|
||||
return Promise.resolve(prevResult);
|
||||
}
|
||||
|
||||
const promise = new Promise(function(resolve, reject) {
|
||||
let accountLinesRequest;
|
||||
let marker;
|
||||
let ledger;
|
||||
let limit;
|
||||
|
||||
if (prevResult) {
|
||||
marker = prevResult.marker;
|
||||
limit = prevResult.limit;
|
||||
ledger = prevResult.ledger_index;
|
||||
} else {
|
||||
marker = options.marker;
|
||||
limit = validator.isValid(options.limit, 'UINT32')
|
||||
? Number(options.limit) : DefaultPageLimit;
|
||||
ledger = utils.parseLedger(options.ledger);
|
||||
}
|
||||
|
||||
accountLinesRequest = self.remote.requestAccountLines({
|
||||
account: account,
|
||||
marker: marker,
|
||||
limit: limit,
|
||||
ledger: ledger
|
||||
});
|
||||
|
||||
if (options.counterparty) {
|
||||
accountLinesRequest.message.peer = options.counterparty;
|
||||
}
|
||||
|
||||
accountLinesRequest.once('error', reject);
|
||||
accountLinesRequest.once('success', function(nextResult) {
|
||||
const lines = [];
|
||||
nextResult.lines.forEach(function(line) {
|
||||
if (options.frozen && !line.freeze) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currencyRE.test(line.currency)) {
|
||||
lines.push({
|
||||
value: line.balance,
|
||||
currency: line.currency,
|
||||
counterparty: line.account
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
nextResult.lines = prevResult ? prevResult.lines.concat(lines) : lines;
|
||||
resolve(nextResult);
|
||||
});
|
||||
accountLinesRequest.request();
|
||||
});
|
||||
|
||||
return promise.then(getLineBalances);
|
||||
}
|
||||
|
||||
function getAccountBalances() {
|
||||
if (options.counterparty || options.frozen) {
|
||||
return getLineBalances();
|
||||
}
|
||||
|
||||
if (options.currency) {
|
||||
if (options.currency === 'XRP') {
|
||||
return getXRPBalance();
|
||||
}
|
||||
return getLineBalances();
|
||||
}
|
||||
|
||||
return Promise.all([getXRPBalance(), getLineBalances()])
|
||||
.then(function(values) {
|
||||
const xrpBalance = values[0].lines[0];
|
||||
const lineBalances = values[1];
|
||||
lineBalances.lines.unshift(xrpBalance);
|
||||
return Promise.resolve(lineBalances);
|
||||
});
|
||||
}
|
||||
|
||||
function respondWithBalances(result) {
|
||||
let balances = {};
|
||||
|
||||
if (result.marker) {
|
||||
balances.marker = result.marker;
|
||||
}
|
||||
|
||||
balances.limit = result.limit;
|
||||
balances.ledger = result.ledger_index;
|
||||
balances.balances = result.lines;
|
||||
|
||||
callback(null, balances);
|
||||
}
|
||||
|
||||
getAccountBalances()
|
||||
.then(respondWithBalances)
|
||||
.catch(callback);
|
||||
}
|
||||
|
||||
module.exports.getBalances = getBalances;
|
||||
103
src/api/ledger/notification_parser.js
Normal file
103
src/api/ledger/notification_parser.js
Normal file
@@ -0,0 +1,103 @@
|
||||
/* eslint-disable valid-jsdoc */
|
||||
'use strict';
|
||||
const ripple = require('./utils').common.core;
|
||||
|
||||
/**
|
||||
* Convert a Ripple transaction in the JSON format,
|
||||
* along with some additional pieces of information,
|
||||
* into a Notification object.
|
||||
*
|
||||
* @param {Ripple Transaction in JSON Format} notification_details.transaction
|
||||
* @param {RippleAddress} notification_details.account
|
||||
* @param {Hex-encoded String}
|
||||
* notification_details.previous_transaction_identifier
|
||||
* @param {Hex-encoded String}
|
||||
* notification_details.next_transaction_identifier
|
||||
*
|
||||
* @returns {Notification}
|
||||
*/
|
||||
function NotificationParser() {}
|
||||
|
||||
NotificationParser.prototype.parse = function(notification_details, urlBase) {
|
||||
const transaction = notification_details.transaction;
|
||||
const account = notification_details.account;
|
||||
const previous_transaction_identifier =
|
||||
notification_details.previous_transaction_identifier;
|
||||
const next_transaction_identifier =
|
||||
notification_details.next_transaction_identifier;
|
||||
|
||||
const metadata = transaction.meta || { };
|
||||
|
||||
const notification = {
|
||||
account: account,
|
||||
type: transaction.TransactionType.toLowerCase(),
|
||||
direction: '', // set below
|
||||
state: (metadata.TransactionResult === 'tesSUCCESS'
|
||||
? 'validated' : 'failed'),
|
||||
result: metadata.TransactionResult || '',
|
||||
ledger: '' + transaction.ledger_index,
|
||||
hash: transaction.hash,
|
||||
timestamp: '',// set below
|
||||
transaction_url: '', // set below
|
||||
previous_transaction_identifier:
|
||||
notification_details.previous_transaction_identifier || '',
|
||||
previous_notification_url: '', // set below
|
||||
next_transaction_identifier:
|
||||
notification_details.next_transaction_identifier || '',
|
||||
next_notification_url: '' // set below
|
||||
};
|
||||
|
||||
notification.timestamp = transaction.date ?
|
||||
new Date(ripple.utils.time.fromRipple(transaction.date)).toISOString() : '';
|
||||
|
||||
// Direction
|
||||
if (account === transaction.Account) {
|
||||
notification.direction = 'outgoing';
|
||||
} else if (transaction.TransactionType === 'Payment'
|
||||
&& transaction.Destination !== account) {
|
||||
notification.direction = 'passthrough';
|
||||
} else {
|
||||
notification.direction = 'incoming';
|
||||
}
|
||||
|
||||
// Notification URL
|
||||
|
||||
if (notification.type === 'payment') {
|
||||
notification.transaction_url = urlBase + '/v1/accounts/'
|
||||
+ notification.account
|
||||
+ '/payments/'
|
||||
+ notification.hash;
|
||||
} else if (notification.type === 'offercreate'
|
||||
|| notification.type === 'offercancel') {
|
||||
notification.type = 'order';
|
||||
notification.transaction_url = urlBase + '/v1/accounts/'
|
||||
+ notification.account
|
||||
+ '/orders/' + notification.hash;
|
||||
} else if (notification.type === 'trustset') {
|
||||
notification.type = 'trustline';
|
||||
notification.transaction_url = urlBase + '/v1/transactions/'
|
||||
+ notification.hash;
|
||||
} else if (notification.type === 'accountset') {
|
||||
notification.type = 'settings';
|
||||
notification.transaction_url = urlBase + '/v1/transactions/'
|
||||
+ notification.hash;
|
||||
}
|
||||
|
||||
// Next notification URL
|
||||
|
||||
if (next_transaction_identifier) {
|
||||
notification.next_notification_url = urlBase + '/v1/accounts/'
|
||||
+ notification.account + '/notifications/' + next_transaction_identifier;
|
||||
}
|
||||
|
||||
// Previous notification URL
|
||||
if (previous_transaction_identifier) {
|
||||
notification.previous_notification_url = urlBase + '/v1/accounts/'
|
||||
+ notification.account + '/notifications/'
|
||||
+ previous_transaction_identifier;
|
||||
}
|
||||
|
||||
return notification;
|
||||
};
|
||||
|
||||
module.exports = new NotificationParser();
|
||||
329
src/api/ledger/notifications.js
Normal file
329
src/api/ledger/notifications.js
Normal file
@@ -0,0 +1,329 @@
|
||||
/* eslint-disable valid-jsdoc */
|
||||
'use strict';
|
||||
const _ = require('lodash');
|
||||
const async = require('async');
|
||||
const transactions = require('./transactions');
|
||||
const NotificationParser = require('./notification_parser.js');
|
||||
const utils = require('./utils.js');
|
||||
const validate = utils.common.validate;
|
||||
const server = utils.common.server;
|
||||
|
||||
/**
|
||||
* Find the previous and next transaction hashes.
|
||||
* Report errors to the client using res.json
|
||||
* or pass the notificationDetails with the added fields
|
||||
* back to the callback.
|
||||
*
|
||||
* @param {Remote} $.remote
|
||||
* @param {Express.js Response} res
|
||||
* @param {RippleAddress} notificationDetails.account
|
||||
* @param {Ripple Transaction in JSON Format} notificationDetails.transaction
|
||||
* @param {Hex-encoded String} notificationDetails.identifier
|
||||
* @param {Function} callback
|
||||
*
|
||||
* @callback
|
||||
* @param {Error} error
|
||||
* @param {Object} notificationDetails
|
||||
**/
|
||||
|
||||
function attachPreviousAndNextTransactionIdentifiers(api,
|
||||
notificationDetails, topCallback) {
|
||||
|
||||
// Get all of the transactions affecting the specified
|
||||
// account in the given ledger. This is done so that
|
||||
// we can query for one more than that number on either
|
||||
// side to ensure that we'll find the next and previous
|
||||
// transactions, no matter how many transactions the
|
||||
// given account had in the same ledger
|
||||
function getAccountTransactionsInBaseTransactionLedger(callback) {
|
||||
const params = {
|
||||
account: notificationDetails.account,
|
||||
ledger_index_min: notificationDetails.transaction.ledger_index,
|
||||
ledger_index_max: notificationDetails.transaction.ledger_index,
|
||||
exclude_failed: false,
|
||||
max: 99999999,
|
||||
limit: 200 // arbitrary, just checking number of transactions in ledger
|
||||
};
|
||||
|
||||
transactions.getAccountTransactions(api, params, callback);
|
||||
}
|
||||
|
||||
// Query for one more than the numTransactionsInLedger
|
||||
// going forward and backwards to get a range of transactions
|
||||
// that will definitely include the next and previous transactions
|
||||
function getNextAndPreviousTransactions(txns, callback) {
|
||||
const numTransactionsInLedger = txns.length;
|
||||
async.concat([false, true], function(earliestFirst, concat_callback) {
|
||||
const params = {
|
||||
account: notificationDetails.account,
|
||||
max: numTransactionsInLedger + 1,
|
||||
min: numTransactionsInLedger + 1,
|
||||
limit: numTransactionsInLedger + 1,
|
||||
earliestFirst: earliestFirst
|
||||
};
|
||||
|
||||
// In rippled -1 corresponds to the first or last ledger
|
||||
// in its database, depending on whether it is the min or max value
|
||||
if (params.earliestFirst) {
|
||||
params.ledger_index_max = -1;
|
||||
params.ledger_index_min = notificationDetails.transaction.ledger_index;
|
||||
} else {
|
||||
params.ledger_index_max = notificationDetails.transaction.ledger_index;
|
||||
params.ledger_index_min = -1;
|
||||
}
|
||||
|
||||
transactions.getAccountTransactions(api, params, concat_callback);
|
||||
|
||||
}, callback);
|
||||
|
||||
}
|
||||
|
||||
// Sort the transactions returned by ledger_index and remove duplicates
|
||||
function sortTransactions(allTransactions, callback) {
|
||||
allTransactions.push(notificationDetails.transaction);
|
||||
|
||||
const txns = _.uniq(allTransactions, function(tx) {
|
||||
return tx.hash;
|
||||
});
|
||||
|
||||
txns.sort(utils.compareTransactions);
|
||||
|
||||
callback(null, txns);
|
||||
}
|
||||
|
||||
// Find the baseTransaction amongst the results. Because the
|
||||
// transactions have been sorted, the next and previous transactions
|
||||
// will be the ones on either side of the base transaction
|
||||
function findPreviousAndNextTransactions(txns, callback) {
|
||||
|
||||
// Find the index in the array of the baseTransaction
|
||||
const baseTransactionIndex = _.findIndex(txns, function(possibility) {
|
||||
return possibility.hash === notificationDetails.transaction.hash;
|
||||
});
|
||||
|
||||
// The previous transaction is the one with an index in
|
||||
// the array of baseTransactionIndex - 1
|
||||
if (baseTransactionIndex > 0) {
|
||||
const previous_transaction = txns[baseTransactionIndex - 1];
|
||||
notificationDetails.previous_transaction_identifier =
|
||||
previous_transaction.hash;
|
||||
}
|
||||
|
||||
// The next transaction is the one with an index in
|
||||
// the array of baseTransactionIndex + 1
|
||||
if (baseTransactionIndex + 1 < txns.length) {
|
||||
const next_transaction = txns[baseTransactionIndex + 1];
|
||||
notificationDetails.next_transaction_identifier = next_transaction.hash;
|
||||
}
|
||||
|
||||
callback(null, notificationDetails);
|
||||
}
|
||||
|
||||
const steps = [
|
||||
getAccountTransactionsInBaseTransactionLedger,
|
||||
getNextAndPreviousTransactions,
|
||||
sortTransactions,
|
||||
findPreviousAndNextTransactions
|
||||
];
|
||||
|
||||
async.waterfall(steps, topCallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a notification corresponding to the specified
|
||||
* account and transaction identifier. Send errors back
|
||||
* to the client using the res.json method or pass
|
||||
* the notification json to the callback function.
|
||||
*
|
||||
* @param {Remote} $.remote
|
||||
* @param {RippleAddress} req.params.account
|
||||
* @param {Hex-encoded String} req.params.identifier
|
||||
* @param {Express.js Response} res
|
||||
* @param {Function} callback
|
||||
*
|
||||
* @callback
|
||||
* @param {Error} error
|
||||
* @param {Notification} notification
|
||||
*/
|
||||
function getNotificationHelper(api, account, identifier, urlBase, topCallback) {
|
||||
|
||||
function getTransaction(callback) {
|
||||
try {
|
||||
transactions.getTransaction(api, account, identifier, {}, callback);
|
||||
} catch(err) {
|
||||
callback(err);
|
||||
}
|
||||
}
|
||||
|
||||
function checkLedger(baseTransaction, callback) {
|
||||
server.remoteHasLedger(api.remote, baseTransaction.ledger_index,
|
||||
function(error, remoteHasLedger) {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
if (remoteHasLedger) {
|
||||
callback(null, baseTransaction);
|
||||
} else {
|
||||
callback(new utils.common.errors.NotFoundError(
|
||||
'Cannot Get Notification. ' +
|
||||
'This transaction is not in the ripple\'s complete ledger set. ' +
|
||||
'Because there is a gap in the rippled\'s historical database it ' +
|
||||
'is not possible to determine the transactions that precede this one')
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function prepareNotificationDetails(baseTransaction, callback) {
|
||||
const notificationDetails = {
|
||||
account: account,
|
||||
identifier: identifier,
|
||||
transaction: baseTransaction
|
||||
};
|
||||
|
||||
attachPreviousAndNextTransactionIdentifiers(api, notificationDetails,
|
||||
callback);
|
||||
}
|
||||
|
||||
function formatNotificationResponse(notificationDetails, callback) {
|
||||
const notification = NotificationParser.parse(notificationDetails, urlBase);
|
||||
callback(null, {notification: notification});
|
||||
}
|
||||
|
||||
const steps = [
|
||||
getTransaction,
|
||||
checkLedger,
|
||||
prepareNotificationDetails,
|
||||
formatNotificationResponse
|
||||
];
|
||||
|
||||
async.waterfall(steps, topCallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a notification corresponding to the specified
|
||||
* account and transaction identifier. Uses the res.json
|
||||
* method to send errors or a notification back to the client.
|
||||
*
|
||||
* @param {Remote} $.remote
|
||||
* @param {/lib/config-loader} $.config
|
||||
* @param {RippleAddress} req.params.account
|
||||
* @param {Hex-encoded String} req.params.identifier
|
||||
*/
|
||||
function getNotification(account, identifier, urlBase, callback) {
|
||||
validate.address(account);
|
||||
validate.paymentIdentifier(identifier);
|
||||
|
||||
return getNotificationHelper(this, account, identifier, urlBase, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a notifications corresponding to the specified
|
||||
* account.
|
||||
*
|
||||
* This function calls transactions.getAccountTransactions
|
||||
* recursively to retrieve results_per_page number of transactions
|
||||
* and filters the results using client-specified parameters.
|
||||
*
|
||||
* @param {RippleAddress} account
|
||||
* @param {string} urlBase - The url to use for the transaction status URL
|
||||
*
|
||||
* @param {string} options.source_account
|
||||
* @param {Number} options.ledger_min
|
||||
* @param {Number} options.ledger_max
|
||||
* @param {string} [false] options.earliest_first
|
||||
* @param {string[]} options.types - @see transactions.getAccountTransactions
|
||||
*
|
||||
*/
|
||||
// TODO: If given ledger range, check for ledger gaps
|
||||
function getNotifications(account, urlBase, options, callback) {
|
||||
validate.address(account);
|
||||
|
||||
const self = this;
|
||||
|
||||
function getTransactions(_callback) {
|
||||
|
||||
const resultsPerPage = options.results_per_page ||
|
||||
transactions.DEFAULT_RESULTS_PER_PAGE;
|
||||
const offset = resultsPerPage * ((options.page || 1) - 1);
|
||||
|
||||
const args = {
|
||||
account: account,
|
||||
direction: options.direction,
|
||||
min: resultsPerPage,
|
||||
max: resultsPerPage,
|
||||
ledger_index_min: options.ledger_min,
|
||||
ledger_index_max: options.ledger_max,
|
||||
offset: offset,
|
||||
earliestFirst: options.earliest_first
|
||||
};
|
||||
|
||||
transactions.getAccountTransactions(self, args, _callback);
|
||||
}
|
||||
|
||||
function parseNotifications(baseTransactions, _callback) {
|
||||
const numTransactions = baseTransactions.length;
|
||||
|
||||
function parseNotification(transaction, __callback) {
|
||||
const args = {
|
||||
account: account,
|
||||
identifier: transaction.hash,
|
||||
transaction: transaction
|
||||
};
|
||||
|
||||
// Attaching previous and next identifiers
|
||||
const idx = baseTransactions.indexOf(transaction);
|
||||
const previous = baseTransactions[idx + 1];
|
||||
const next = baseTransactions[idx - 1];
|
||||
|
||||
if (!options.earliest_first) {
|
||||
args.previous_transaction_identifier = previous ?
|
||||
previous.hash : undefined;
|
||||
args.next_transaction_identifier = next ? next.hash : undefined;
|
||||
} else {
|
||||
args.previous_transaction_identifier = next ? next.hash : undefined;
|
||||
args.next_transaction_identifier = previous ? previous.hash : undefined;
|
||||
}
|
||||
|
||||
args.previous_transaction_identifier = args.previous_hash;
|
||||
args.next_transaction_identifier = args.next_hash;
|
||||
|
||||
const firstAndPaging = options.page &&
|
||||
(options.earliest_first ?
|
||||
args.previous_transaction_identifier === undefined :
|
||||
args.next_transaction_identifier === undefined);
|
||||
|
||||
const last = idx === numTransactions - 1;
|
||||
|
||||
if (firstAndPaging || last) {
|
||||
attachPreviousAndNextTransactionIdentifiers(self, args,
|
||||
function(err, _args) {
|
||||
return __callback(err, NotificationParser.parse(_args, urlBase));
|
||||
}
|
||||
);
|
||||
} else {
|
||||
return __callback(null, NotificationParser.parse(args, urlBase));
|
||||
}
|
||||
}
|
||||
|
||||
return async.map(baseTransactions, parseNotification, _callback);
|
||||
}
|
||||
|
||||
function formatResponse(notifications, _callback) {
|
||||
_callback(null, {notifications: notifications});
|
||||
}
|
||||
|
||||
const steps = [
|
||||
getTransactions,
|
||||
_.partial(utils.attachDate, self),
|
||||
parseNotifications,
|
||||
formatResponse
|
||||
];
|
||||
|
||||
return async.waterfall(steps, callback);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getNotification: getNotification,
|
||||
getNotifications: getNotifications
|
||||
};
|
||||
330
src/api/ledger/orders.js
Normal file
330
src/api/ledger/orders.js
Normal file
@@ -0,0 +1,330 @@
|
||||
/* globals Promise: true */
|
||||
/* eslint-disable valid-jsdoc */
|
||||
'use strict';
|
||||
const _ = require('lodash');
|
||||
const bignum = require('bignumber.js');
|
||||
const asyncify = require('simple-asyncify');
|
||||
const TxToRestConverter = require('./tx-to-rest-converter.js');
|
||||
const utils = require('./utils');
|
||||
const ripple = utils.common.core;
|
||||
const errors = utils.common.errors;
|
||||
const validate = utils.common.validate;
|
||||
const validator = utils.common.schemaValidator;
|
||||
|
||||
const DefaultPageLimit = 200;
|
||||
|
||||
/**
|
||||
* Get orders from the ripple network
|
||||
*
|
||||
* @query
|
||||
* @param {String} [request.query.limit]
|
||||
* - Set a limit to the number of results returned
|
||||
* @param {String} [request.query.marker]
|
||||
* - Used to paginate results
|
||||
* @param {String} [request.query.ledger]
|
||||
* - The ledger index to query against
|
||||
* - (required if request.query.marker is present)
|
||||
*
|
||||
* @url
|
||||
* @param {RippleAddress} request.params.account
|
||||
* - The ripple address to query orders
|
||||
*
|
||||
*/
|
||||
function getOrders(account, options, callback) {
|
||||
const self = this;
|
||||
|
||||
validate.address(account);
|
||||
validate.options(options);
|
||||
|
||||
function getAccountOrders(prevResult) {
|
||||
const isAggregate = options.limit === 'all';
|
||||
if (prevResult && (!isAggregate || !prevResult.marker)) {
|
||||
return Promise.resolve(prevResult);
|
||||
}
|
||||
|
||||
const promise = new Promise(function(resolve, reject) {
|
||||
let accountOrdersRequest;
|
||||
let marker;
|
||||
let ledger;
|
||||
let limit;
|
||||
|
||||
if (prevResult) {
|
||||
marker = prevResult.marker;
|
||||
limit = prevResult.limit;
|
||||
ledger = prevResult.ledger_index;
|
||||
} else {
|
||||
marker = options.marker;
|
||||
limit = validator.isValid(options.limit, 'UINT32') ?
|
||||
Number(options.limit) : DefaultPageLimit;
|
||||
ledger = utils.parseLedger(options.ledger);
|
||||
}
|
||||
|
||||
accountOrdersRequest = self.remote.requestAccountOffers({
|
||||
account: account,
|
||||
marker: marker,
|
||||
limit: limit,
|
||||
ledger: ledger
|
||||
});
|
||||
|
||||
accountOrdersRequest.once('error', reject);
|
||||
accountOrdersRequest.once('success', function(nextResult) {
|
||||
nextResult.offers = prevResult ?
|
||||
nextResult.offers.concat(prevResult.offers) : nextResult.offers;
|
||||
resolve(nextResult);
|
||||
});
|
||||
accountOrdersRequest.request();
|
||||
});
|
||||
|
||||
return promise.then(getAccountOrders);
|
||||
}
|
||||
|
||||
function getParsedOrders(offers) {
|
||||
return _.reduce(offers, function(orders, off) {
|
||||
const sequence = off.seq;
|
||||
const type = off.flags & ripple.Remote.flags.offer.Sell ? 'sell' : 'buy';
|
||||
const passive = (off.flags & ripple.Remote.flags.offer.Passive) !== 0;
|
||||
|
||||
const taker_gets = utils.parseCurrencyAmount(off.taker_gets);
|
||||
const taker_pays = utils.parseCurrencyAmount(off.taker_pays);
|
||||
|
||||
orders.push({
|
||||
type: type,
|
||||
taker_gets: taker_gets,
|
||||
taker_pays: taker_pays,
|
||||
sequence: sequence,
|
||||
passive: passive
|
||||
});
|
||||
|
||||
return orders;
|
||||
}, []);
|
||||
}
|
||||
|
||||
function respondWithOrders(result) {
|
||||
const promise = new Promise(function(resolve) {
|
||||
const orders = {};
|
||||
|
||||
if (result.marker) {
|
||||
orders.marker = result.marker;
|
||||
}
|
||||
|
||||
orders.limit = result.limit;
|
||||
orders.ledger = result.ledger_index;
|
||||
orders.validated = result.validated;
|
||||
orders.orders = getParsedOrders(result.offers);
|
||||
|
||||
resolve(callback(null, orders));
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
getAccountOrders()
|
||||
.then(respondWithOrders)
|
||||
.catch(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the most recent spapshot of the order book for a currency pair
|
||||
*
|
||||
* @url
|
||||
* @param {RippleAddress} request.params.account
|
||||
* - The ripple address to use as point-of-view
|
||||
* (returns unfunded orders for this account)
|
||||
* @param {String ISO 4217 Currency Code + RippleAddress} request.params.base
|
||||
* - Base currency as currency+issuer
|
||||
* @param {String ISO 4217 Currency Code + RippleAddress}
|
||||
* request.params.counter - Counter currency as currency+issuer
|
||||
*
|
||||
* @query
|
||||
* @param {String} [request.query.limit]
|
||||
* - Set a limit to the number of results returned
|
||||
*
|
||||
* @param {Express.js Request} request
|
||||
*/
|
||||
function getOrderBook(account, base, counter, options, callback) {
|
||||
const self = this;
|
||||
|
||||
const params = _.merge(options, {
|
||||
validated: true,
|
||||
order_book: base + '/' + counter,
|
||||
base: utils.parseCurrencyQuery(base),
|
||||
counter: utils.parseCurrencyQuery(counter)
|
||||
});
|
||||
validate.address(account);
|
||||
validate.orderbook(params);
|
||||
validate.options(options);
|
||||
|
||||
function getLastValidatedLedger(parameters) {
|
||||
const promise = new Promise(function(resolve, reject) {
|
||||
const ledgerRequest = self.remote.requestLedger('validated');
|
||||
|
||||
ledgerRequest.once('success', function(res) {
|
||||
parameters.ledger = res.ledger.ledger_index;
|
||||
resolve(parameters);
|
||||
});
|
||||
|
||||
ledgerRequest.once('error', reject);
|
||||
ledgerRequest.request();
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
function getBookOffers(taker_gets, taker_pays, parameters) {
|
||||
const promise = new Promise(function(resolve, reject) {
|
||||
const bookOffersRequest = self.remote.requestBookOffers({
|
||||
taker_gets: {currency: taker_gets.currency,
|
||||
issuer: taker_gets.counterparty},
|
||||
taker_pays: {currency: taker_pays.currency,
|
||||
issuer: taker_pays.counterparty},
|
||||
ledger: parameters.ledger,
|
||||
limit: parameters.limit,
|
||||
taker: account
|
||||
});
|
||||
|
||||
bookOffersRequest.once('success', resolve);
|
||||
bookOffersRequest.once('error', reject);
|
||||
bookOffersRequest.request();
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
function getBids(parameters) {
|
||||
const taker_gets = parameters.counter;
|
||||
const taker_pays = parameters.base;
|
||||
|
||||
return getBookOffers(taker_gets, taker_pays, parameters);
|
||||
}
|
||||
|
||||
function getAsks(parameters) {
|
||||
const taker_gets = parameters.base;
|
||||
const taker_pays = parameters.counter;
|
||||
|
||||
return getBookOffers(taker_gets, taker_pays, parameters);
|
||||
}
|
||||
|
||||
function getBidsAndAsks(parameters) {
|
||||
return Promise.join(
|
||||
getBids(parameters),
|
||||
getAsks(parameters),
|
||||
function(bids, asks) {
|
||||
return [bids, asks, parameters];
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function getParsedBookOffers(offers, isAsk) {
|
||||
return offers.reduce(function(orderBook, off) {
|
||||
let price;
|
||||
const order_maker = off.Account;
|
||||
const sequence = off.Sequence;
|
||||
|
||||
// Transaction Flags
|
||||
const passive = (off.Flags & ripple.Remote.flags.offer.Passive) !== 0;
|
||||
const sell = (off.Flags & ripple.Remote.flags.offer.Sell) !== 0;
|
||||
|
||||
const taker_gets_total = utils.parseCurrencyAmount(off.TakerGets);
|
||||
const taker_gets_funded = off.taker_gets_funded ?
|
||||
utils.parseCurrencyAmount(off.taker_gets_funded) : taker_gets_total;
|
||||
|
||||
const taker_pays_total = utils.parseCurrencyAmount(off.TakerPays);
|
||||
const taker_pays_funded = off.taker_pays_funded ?
|
||||
utils.parseCurrencyAmount(off.taker_pays_funded) : taker_pays_total;
|
||||
|
||||
if (isAsk) {
|
||||
price = {
|
||||
currency: taker_pays_total.currency,
|
||||
counterparty: taker_pays_total.counterparty,
|
||||
value: bignum(taker_pays_total.value).div(
|
||||
bignum(taker_gets_total.value))
|
||||
};
|
||||
} else {
|
||||
price = {
|
||||
currency: taker_gets_total.currency,
|
||||
counterparty: taker_gets_total.counterparty,
|
||||
value: bignum(taker_gets_total.value).div(
|
||||
bignum(taker_pays_total.value))
|
||||
};
|
||||
}
|
||||
|
||||
price.value = price.value.toString();
|
||||
|
||||
orderBook.push({
|
||||
price: price,
|
||||
taker_gets_funded: taker_gets_funded,
|
||||
taker_gets_total: taker_gets_total,
|
||||
taker_pays_funded: taker_pays_funded,
|
||||
taker_pays_total: taker_pays_total,
|
||||
order_maker: order_maker,
|
||||
sequence: sequence,
|
||||
passive: passive,
|
||||
sell: sell
|
||||
});
|
||||
|
||||
return orderBook;
|
||||
}, []);
|
||||
}
|
||||
|
||||
function respondWithOrderBook(bids, asks, parameters) {
|
||||
const promise = new Promise(function(resolve) {
|
||||
const orderBook = {
|
||||
order_book: parameters.order_book,
|
||||
ledger: parameters.ledger,
|
||||
validated: parameters.validated,
|
||||
bids: getParsedBookOffers(bids.offers),
|
||||
asks: getParsedBookOffers(asks.offers, true)
|
||||
};
|
||||
|
||||
resolve(callback(null, orderBook));
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
getLastValidatedLedger(params)
|
||||
.then(getBidsAndAsks)
|
||||
.spread(respondWithOrderBook)
|
||||
.catch(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an Order transaction (`OfferCreate` or `OfferCancel`)
|
||||
*
|
||||
* @url
|
||||
* @param {RippleAddress} request.params.account
|
||||
* @param {String} request.params.identifier
|
||||
*
|
||||
* @param {Express.js Request} request
|
||||
*/
|
||||
function getOrder(account, identifier, callback) {
|
||||
validate.address(account);
|
||||
validate.identifier(identifier);
|
||||
|
||||
const txRequest = this.remote.requestTx({
|
||||
hash: identifier
|
||||
});
|
||||
|
||||
txRequest.once('error', callback);
|
||||
txRequest.once('transaction', function(response) {
|
||||
if (response.TransactionType !== 'OfferCreate'
|
||||
&& response.TransactionType !== 'OfferCancel') {
|
||||
callback(new errors.InvalidRequestError('Invalid parameter: identifier. '
|
||||
+ 'The transaction corresponding to the given identifier '
|
||||
+ 'is not an order'));
|
||||
} else {
|
||||
const options = {
|
||||
account: account,
|
||||
identifier: identifier
|
||||
};
|
||||
asyncify(TxToRestConverter.parseOrderFromTx)(response, options, callback);
|
||||
}
|
||||
});
|
||||
txRequest.request();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getOrders: getOrders,
|
||||
getOrderBook: getOrderBook,
|
||||
getOrder: getOrder
|
||||
};
|
||||
322
src/api/ledger/payments.js
Normal file
322
src/api/ledger/payments.js
Normal file
@@ -0,0 +1,322 @@
|
||||
/* eslint-disable valid-jsdoc */
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
const async = require('async');
|
||||
const asyncify = require('simple-asyncify');
|
||||
const bignum = require('bignumber.js');
|
||||
const transactions = require('./transactions');
|
||||
const TxToRestConverter = require('./tx-to-rest-converter.js');
|
||||
const utils = require('./utils');
|
||||
const ripple = utils.common.core;
|
||||
const validator = utils.common.schemaValidator;
|
||||
const validate = utils.common.validate;
|
||||
|
||||
const InvalidRequestError = utils.common.errors.InvalidRequestError;
|
||||
const NotFoundError = utils.common.errors.NotFoundError;
|
||||
const TimeOutError = utils.common.errors.TimeOutError;
|
||||
|
||||
const DEFAULT_RESULTS_PER_PAGE = 10;
|
||||
|
||||
/**
|
||||
* Formats a transaction into ripple-rest Payment format
|
||||
*
|
||||
* @param {RippleAddress} account
|
||||
* @param {Transaction} transaction
|
||||
* @param {Function} callback
|
||||
*
|
||||
* @callback
|
||||
* @param {Error} error
|
||||
* @param {RippleRestTransaction} transaction
|
||||
*/
|
||||
function formatPaymentHelper(account, txJSON) {
|
||||
if (!(txJSON && /^payment$/i.test(txJSON.TransactionType))) {
|
||||
throw new InvalidRequestError('Not a payment. The transaction '
|
||||
+ 'corresponding to the given identifier is not a payment.');
|
||||
}
|
||||
const metadata = {
|
||||
hash: txJSON.hash || '',
|
||||
ledger: String(!_.isUndefined(txJSON.inLedger) ?
|
||||
txJSON.inLedger : txJSON.ledger_index),
|
||||
state: txJSON.validated === true ? 'validated' : 'pending'
|
||||
};
|
||||
const message = {tx_json: txJSON};
|
||||
const meta = txJSON.meta;
|
||||
const parsed = TxToRestConverter.parsePaymentFromTx(account, message, meta);
|
||||
return _.assign({payment: parsed.payment}, metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the details of a particular payment from the Remote
|
||||
* and return it in the ripple-rest Payment format.
|
||||
*
|
||||
* @param {Remote} remote
|
||||
* @param {RippleAddress} req.params.account
|
||||
* @param {Hex-encoded String|ASCII printable character String}
|
||||
* req.params.identifier
|
||||
*/
|
||||
function getPayment(account, identifier, callback) {
|
||||
const self = this;
|
||||
|
||||
validate.address(account);
|
||||
validate.paymentIdentifier(identifier);
|
||||
|
||||
function getTransaction(_callback) {
|
||||
transactions.getTransaction(self, account, identifier, {}, _callback);
|
||||
}
|
||||
|
||||
const steps = [
|
||||
getTransaction,
|
||||
asyncify(_.partial(formatPaymentHelper, account))
|
||||
];
|
||||
|
||||
async.waterfall(steps, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the details of multiple payments from the Remote
|
||||
*
|
||||
* This function calls transactions.getAccountTransactions
|
||||
* recursively to retrieve results_per_page number of transactions
|
||||
* and filters the results by type "payment", along with the other
|
||||
* client-specified parameters.
|
||||
*
|
||||
* @param {Remote} remote
|
||||
* @param {RippleAddress} req.params.account
|
||||
* @param {RippleAddress} req.query.source_account
|
||||
* @param {RippleAddress} req.query.destination_account
|
||||
* @param {String "incoming"|"outgoing"} req.query.direction
|
||||
* @param {Number} [-1] req.query.start_ledger
|
||||
* @param {Number} [-1] req.query.end_ledger
|
||||
* @param {Boolean} [false] req.query.earliest_first
|
||||
* @param {Boolean} [false] req.query.exclude_failed
|
||||
* @param {Number} [20] req.query.results_per_page
|
||||
* @param {Number} [1] req.query.page
|
||||
*/
|
||||
function getAccountPayments(account, source_account, destination_account,
|
||||
direction, options, callback) {
|
||||
const self = this;
|
||||
|
||||
function getTransactions(_callback) {
|
||||
const args = {
|
||||
account: account,
|
||||
source_account: source_account,
|
||||
destination_account: destination_account,
|
||||
direction: direction,
|
||||
min: options.results_per_page,
|
||||
max: options.results_per_page,
|
||||
offset: (options.results_per_page || DEFAULT_RESULTS_PER_PAGE)
|
||||
* ((options.page || 1) - 1),
|
||||
types: ['payment'],
|
||||
earliestFirst: options.earliest_first
|
||||
};
|
||||
|
||||
transactions.getAccountTransactions(self,
|
||||
_.merge(options, args), _callback);
|
||||
}
|
||||
|
||||
function formatTransactions(_transactions) {
|
||||
return _transactions.map(_.partial(formatPaymentHelper, account));
|
||||
}
|
||||
|
||||
function formatResponse(_transactions) {
|
||||
return {payments: _transactions};
|
||||
}
|
||||
|
||||
const steps = [
|
||||
getTransactions,
|
||||
_.partial(utils.attachDate, self),
|
||||
asyncify(formatTransactions),
|
||||
asyncify(formatResponse)
|
||||
];
|
||||
|
||||
async.waterfall(steps, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a ripple path find, a.k.a. payment options,
|
||||
* for a given set of parameters and respond to the
|
||||
* client with an array of fully-formed Payments.
|
||||
*
|
||||
* @param {Remote} remote
|
||||
* @param {RippleAddress} req.params.source_account
|
||||
* @param {Amount Array ["USD r...,XRP,..."]} req.query.source_currencies
|
||||
* - Note that Express.js middleware replaces "+" signs with spaces.
|
||||
* Clients should use "+" signs but the values here will end up
|
||||
* as spaces
|
||||
* @param {RippleAddress} req.params.destination_account
|
||||
* @param {Amount "1+USD+r..."} req.params.destination_amount_string
|
||||
*/
|
||||
function getPathFind(source_account, destination_account,
|
||||
destination_amount_string, source_currency_strings, callback) {
|
||||
const self = this;
|
||||
|
||||
const destination_amount = utils.renameCounterpartyToIssuer(
|
||||
utils.parseCurrencyQuery(destination_amount_string || ''));
|
||||
|
||||
validate.pathfind({
|
||||
source_account: source_account,
|
||||
destination_account: destination_account,
|
||||
destination_amount: destination_amount,
|
||||
source_currency_strings: source_currency_strings
|
||||
});
|
||||
|
||||
const source_currencies = [];
|
||||
// Parse source currencies
|
||||
// Note that the source_currencies should be in the form
|
||||
// "USD r...,BTC,XRP". The issuer is optional but if provided should be
|
||||
// separated from the currency by a single space.
|
||||
if (source_currency_strings) {
|
||||
const sourceCurrencyStrings = source_currency_strings.split(',');
|
||||
for (let c = 0; c < sourceCurrencyStrings.length; c++) {
|
||||
// Remove leading and trailing spaces
|
||||
sourceCurrencyStrings[c] = sourceCurrencyStrings[c].replace(
|
||||
/(^[ ])|([ ]$)/g, '');
|
||||
// If there is a space, there should be a valid issuer after the space
|
||||
if (/ /.test(sourceCurrencyStrings[c])) {
|
||||
const currencyIssuerArray = sourceCurrencyStrings[c].split(' ');
|
||||
const currencyObject = {
|
||||
currency: currencyIssuerArray[0],
|
||||
issuer: currencyIssuerArray[1]
|
||||
};
|
||||
if (validator.isValid(currencyObject.currency, 'Currency')
|
||||
&& ripple.UInt160.is_valid(currencyObject.issuer)) {
|
||||
source_currencies.push(currencyObject);
|
||||
} else {
|
||||
callback(new InvalidRequestError('Invalid parameter: '
|
||||
+ 'source_currencies. Must be a list of valid currencies'));
|
||||
return;
|
||||
}
|
||||
} else if (validator.isValid(sourceCurrencyStrings[c], 'Currency')) {
|
||||
source_currencies.push({currency: sourceCurrencyStrings[c]});
|
||||
} else {
|
||||
callback(new InvalidRequestError('Invalid parameter: '
|
||||
+ 'source_currencies. Must be a list of valid currencies'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function prepareOptions() {
|
||||
const pathfindParams = {
|
||||
src_account: source_account,
|
||||
dst_account: destination_account,
|
||||
dst_amount: utils.common.convertAmount(destination_amount)
|
||||
};
|
||||
if (typeof pathfindParams.dst_amount === 'object'
|
||||
&& !pathfindParams.dst_amount.issuer) {
|
||||
// Convert blank issuer to sender's address
|
||||
// (Ripple convention for 'any issuer')
|
||||
// https://ripple.com/build/transactions/
|
||||
// #special-issuer-values-for-sendmax-and-amount
|
||||
// https://ripple.com/build/ripple-rest/#counterparties-in-payments
|
||||
|
||||
pathfindParams.dst_amount.issuer = pathfindParams.dst_account;
|
||||
}
|
||||
if (source_currencies.length > 0) {
|
||||
pathfindParams.src_currencies = source_currencies;
|
||||
}
|
||||
return pathfindParams;
|
||||
}
|
||||
|
||||
function findPath(pathfindParams, _callback) {
|
||||
const request = self.remote.requestRipplePathFind(pathfindParams);
|
||||
request.once('error', _callback);
|
||||
request.once('success', function(pathfindResults) {
|
||||
pathfindResults.source_account = pathfindParams.src_account;
|
||||
pathfindResults.source_currencies = pathfindParams.src_currencies;
|
||||
pathfindResults.destination_amount = pathfindParams.dst_amount;
|
||||
_callback(null, pathfindResults);
|
||||
});
|
||||
|
||||
function reconnectRippled() {
|
||||
self.remote.disconnect(function() {
|
||||
self.remote.connect();
|
||||
});
|
||||
}
|
||||
request.timeout(utils.common.server.CONNECTION_TIMEOUT, function() {
|
||||
request.removeAllListeners();
|
||||
reconnectRippled();
|
||||
_callback(new TimeOutError('Path request timeout'));
|
||||
});
|
||||
request.request();
|
||||
}
|
||||
|
||||
function addDirectXrpPath(pathfindResults, _callback) {
|
||||
// Check if destination_amount is XRP and if destination_account accepts XRP
|
||||
if (typeof pathfindResults.destination_amount.currency === 'string'
|
||||
|| pathfindResults.destination_currencies.indexOf('XRP') === -1) {
|
||||
return _callback(null, pathfindResults);
|
||||
}
|
||||
// Check source_account balance
|
||||
self.remote.requestAccountInfo({account: pathfindResults.source_account},
|
||||
function(error, result) {
|
||||
if (error) {
|
||||
return _callback(new Error(
|
||||
'Cannot get account info for source_account. ' + error));
|
||||
}
|
||||
if (!result || !result.account_data || !result.account_data.Balance) {
|
||||
return _callback(new Error('Internal Error. Malformed account info : '
|
||||
+ JSON.stringify(result)));
|
||||
}
|
||||
// Add XRP "path" only if the source_account has enough money
|
||||
// to execute the payment
|
||||
if (bignum(result.account_data.Balance).greaterThan(
|
||||
pathfindResults.destination_amount)) {
|
||||
pathfindResults.alternatives.unshift({
|
||||
paths_canonical: [],
|
||||
paths_computed: [],
|
||||
source_amount: pathfindResults.destination_amount
|
||||
});
|
||||
}
|
||||
_callback(null, pathfindResults);
|
||||
});
|
||||
}
|
||||
|
||||
function formatPath(pathfindResults) {
|
||||
const alternatives = pathfindResults.alternatives;
|
||||
if (alternatives && alternatives.length > 0) {
|
||||
return TxToRestConverter.parsePaymentsFromPathFind(pathfindResults);
|
||||
}
|
||||
if (pathfindResults.destination_currencies.indexOf(
|
||||
destination_amount.currency) === -1) {
|
||||
throw new NotFoundError('No paths found. ' +
|
||||
'The destination_account does not accept ' +
|
||||
destination_amount.currency +
|
||||
', they only accept: ' +
|
||||
pathfindResults.destination_currencies.join(', '));
|
||||
} else if (pathfindResults.source_currencies
|
||||
&& pathfindResults.source_currencies.length > 0) {
|
||||
throw new NotFoundError('No paths found. Please ensure' +
|
||||
' that the source_account has sufficient funds to execute' +
|
||||
' the payment in one of the specified source_currencies. If it does' +
|
||||
' there may be insufficient liquidity in the network to execute' +
|
||||
' this payment right now');
|
||||
} else {
|
||||
throw new NotFoundError('No paths found.' +
|
||||
' Please ensure that the source_account has sufficient funds to' +
|
||||
' execute the payment. If it does there may be insufficient liquidity' +
|
||||
' in the network to execute this payment right now');
|
||||
}
|
||||
}
|
||||
|
||||
function formatResponse(payments) {
|
||||
return {payments: payments};
|
||||
}
|
||||
|
||||
const steps = [
|
||||
asyncify(prepareOptions),
|
||||
findPath,
|
||||
addDirectXrpPath,
|
||||
asyncify(formatPath),
|
||||
asyncify(formatResponse)
|
||||
];
|
||||
|
||||
async.waterfall(steps, callback);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getPayment: getPayment,
|
||||
getAccountPayments: getAccountPayments,
|
||||
getPathFind: getPathFind
|
||||
};
|
||||
61
src/api/ledger/settings.js
Normal file
61
src/api/ledger/settings.js
Normal file
@@ -0,0 +1,61 @@
|
||||
/* eslint-disable valid-jsdoc */
|
||||
'use strict';
|
||||
const _ = require('lodash');
|
||||
const TxToRestConverter = require('./tx-to-rest-converter.js');
|
||||
const utils = require('./utils');
|
||||
const validate = utils.common.validate;
|
||||
const constants = utils.common.constants;
|
||||
|
||||
function parseFieldsFromResponse(responseBody, fields) {
|
||||
let parsedBody = {};
|
||||
|
||||
for (let fieldName in fields) {
|
||||
const field = fields[fieldName];
|
||||
let value = responseBody[fieldName] || '';
|
||||
if (field.encoding === 'hex' && !field.length) {
|
||||
value = new Buffer(value, 'hex').toString('ascii');
|
||||
}
|
||||
parsedBody[field.name] = value;
|
||||
}
|
||||
|
||||
return parsedBody;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves account settings for a given account
|
||||
*
|
||||
* @url
|
||||
* @param {String} request.params.account
|
||||
*
|
||||
*/
|
||||
function getSettings(account, callback) {
|
||||
validate.address(account);
|
||||
|
||||
this.remote.requestAccountInfo({account: account}, function(error, info) {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
const data = info.account_data;
|
||||
const settings = {
|
||||
account: data.Account,
|
||||
transfer_rate: '0'
|
||||
};
|
||||
|
||||
// Attach account flags
|
||||
_.extend(settings, TxToRestConverter.parseFlagsFromResponse(data.Flags,
|
||||
constants.AccountRootFlags));
|
||||
|
||||
// Attach account fields
|
||||
_.extend(settings, parseFieldsFromResponse(data,
|
||||
constants.AccountRootFields));
|
||||
|
||||
settings.transaction_sequence = String(settings.transaction_sequence);
|
||||
|
||||
callback(null, {settings: settings});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getSettings: getSettings
|
||||
};
|
||||
326
src/api/ledger/transactions.js
Normal file
326
src/api/ledger/transactions.js
Normal file
@@ -0,0 +1,326 @@
|
||||
/* eslint-disable valid-jsdoc */
|
||||
'use strict';
|
||||
const _ = require('lodash');
|
||||
const assert = require('assert');
|
||||
const async = require('async');
|
||||
const utils = require('./utils');
|
||||
const validate = utils.common.validate;
|
||||
const errors = utils.common.errors;
|
||||
|
||||
const DEFAULT_RESULTS_PER_PAGE = 10;
|
||||
|
||||
/**
|
||||
* Retrieve a transaction from the Remote based on the account and hash
|
||||
*
|
||||
* Note that if any errors are encountered while executing this function
|
||||
* they will be sent back to the client through the res. If the query is
|
||||
* successful it will be passed to the callback function
|
||||
*
|
||||
* @global
|
||||
* @param {Remote} remote
|
||||
*
|
||||
* @param {RippleAddress} account
|
||||
* @param {Hex-encoded String|ASCII printable character String} identifier
|
||||
* @param {Object} options
|
||||
* @param {Function} callback
|
||||
*
|
||||
* @callback
|
||||
* @param {Error} error
|
||||
* @param {Transaction} transaction
|
||||
*/
|
||||
function getTransaction(api, account, identifier, requestOptions, callback) {
|
||||
try {
|
||||
assert.strictEqual(typeof requestOptions, 'object');
|
||||
validate.address(account, true);
|
||||
validate.identifier(identifier);
|
||||
validate.options(requestOptions);
|
||||
} catch(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
const options = {};
|
||||
options.hash = identifier;
|
||||
|
||||
const isLedgerRangeRequest = !_.isUndefined(requestOptions.min_ledger)
|
||||
&& !_.isUndefined(requestOptions.max_ledger);
|
||||
|
||||
if (isLedgerRangeRequest) {
|
||||
const minLedger = Number(options.min_ledger);
|
||||
const maxLedger = Number(options.max_ledger);
|
||||
for (let i = minLedger; i <= maxLedger; i++) {
|
||||
if (!api.remote.getServer().hasLedger(i)) {
|
||||
return callback(new errors.NotFoundError('Ledger not found'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function queryTransaction(async_callback) {
|
||||
api.remote.requestTx({hash: options.hash}, async_callback);
|
||||
}
|
||||
|
||||
function checkIfRelatedToAccount(transaction, async_callback) {
|
||||
if (options.account) {
|
||||
const transactionString = JSON.stringify(transaction);
|
||||
const account_regex = new RegExp(options.account);
|
||||
if (!account_regex.test(transactionString)) {
|
||||
return async_callback(new errors.InvalidRequestError(
|
||||
'Transaction specified did not affect the given account'));
|
||||
}
|
||||
}
|
||||
async_callback(null, transaction);
|
||||
}
|
||||
|
||||
function attachDate(transaction, async_callback) {
|
||||
if (!transaction || transaction.date || !transaction.ledger_index) {
|
||||
return async_callback(null, transaction);
|
||||
}
|
||||
|
||||
api.remote.requestLedger(transaction.ledger_index,
|
||||
function(error, ledgerRequest) {
|
||||
if (error) {
|
||||
return async_callback(new errors.NotFoundError(
|
||||
'Transaction ledger not found'));
|
||||
}
|
||||
|
||||
if (typeof ledgerRequest.ledger.close_time === 'number') {
|
||||
transaction.date = ledgerRequest.ledger.close_time;
|
||||
}
|
||||
|
||||
async_callback(null, transaction);
|
||||
});
|
||||
}
|
||||
|
||||
const steps = [
|
||||
queryTransaction,
|
||||
checkIfRelatedToAccount,
|
||||
attachDate
|
||||
];
|
||||
|
||||
async.waterfall(steps, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around the standard ripple-lib requestAccountTx function
|
||||
*
|
||||
* @param {Remote} remote
|
||||
* @param {RippleAddress} options.account
|
||||
* @param {Number} [-1] options.ledger_index_min
|
||||
* @param {Number} [-1] options.ledger_index_max
|
||||
* @param {Boolean} [false] options.earliestFirst
|
||||
* @param {Boolean} [false] options.binary
|
||||
* @param {opaque value} options.marker
|
||||
* @param {Function} callback
|
||||
*
|
||||
* @callback
|
||||
* @param {Error} error
|
||||
* @param {Array of transactions in JSON format} response.transactions
|
||||
* @param {opaque value} response.marker
|
||||
*/
|
||||
function getAccountTx(api, options, callback) {
|
||||
const params = {
|
||||
account: options.account,
|
||||
ledger_index_min: options.ledger_index_min || options.ledger_index || -1,
|
||||
ledger_index_max: options.ledger_index_max || options.ledger_index || -1,
|
||||
limit: options.limit || DEFAULT_RESULTS_PER_PAGE,
|
||||
forward: options.earliestFirst,
|
||||
marker: options.marker
|
||||
};
|
||||
if (options.binary) {
|
||||
params.binary = true;
|
||||
}
|
||||
api.remote.requestAccountTx(params, function(error, account_tx_results) {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
let transactions = [];
|
||||
account_tx_results.transactions.forEach(function(tx_entry) {
|
||||
if (!tx_entry.validated) {
|
||||
return;
|
||||
}
|
||||
const tx = tx_entry.tx;
|
||||
tx.meta = tx_entry.meta;
|
||||
tx.validated = tx_entry.validated;
|
||||
transactions.push(tx);
|
||||
});
|
||||
callback(null, {
|
||||
transactions: transactions,
|
||||
marker: account_tx_results.marker
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter transactions based on the given set of options.
|
||||
*
|
||||
* @param {Array of transactions in JSON format} transactions
|
||||
* @param {Boolean} [false] options.exclude_failed
|
||||
* @param {Array of Strings} options.types Possible values are "payment",
|
||||
* "offercreate", "offercancel", "trustset", "accountset"
|
||||
* @param {RippleAddress} options.source_account
|
||||
* @param {RippleAddress} options.destination_account
|
||||
* @param {String} options.direction Possible values are "incoming", "outgoing"
|
||||
*
|
||||
* @returns {Array of transactions in JSON format} filtered_transactions
|
||||
*/
|
||||
function transactionFilter(transactions, options) {
|
||||
const filtered_transactions = transactions.filter(function(transaction) {
|
||||
if (options.exclude_failed) {
|
||||
if (transaction.state === 'failed' || (transaction.meta
|
||||
&& transaction.meta.TransactionResult !== 'tesSUCCESS')) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (options.types && options.types.length > 0) {
|
||||
if (options.types.indexOf(
|
||||
transaction.TransactionType.toLowerCase()) === -1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (options.source_account) {
|
||||
if (transaction.Account !== options.source_account) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (options.destination_account) {
|
||||
if (transaction.Destination !== options.destination_account) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (options.direction) {
|
||||
if (options.direction === 'outgoing'
|
||||
&& transaction.Account !== options.account) {
|
||||
return false;
|
||||
}
|
||||
if (options.direction === 'incoming' && transaction.Destination
|
||||
&& transaction.Destination !== options.account) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
return filtered_transactions;
|
||||
}
|
||||
|
||||
function getTransactionsHelper(api, options, callback) {
|
||||
getAccountTx(api, options, function(error, results) {
|
||||
if (error) {
|
||||
callback(error);
|
||||
} else {
|
||||
// Set marker so that when this function is called again
|
||||
// recursively it starts from the last place it left off
|
||||
options.marker = results.marker;
|
||||
callback(null, results.transactions);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively get transactions for the specified account from
|
||||
* the Remote. If options.min is set, this will
|
||||
* recurse until it has retrieved that number of transactions or
|
||||
* it has reached the end of the account's transaction history.
|
||||
*
|
||||
* @param {Remote} remote
|
||||
* @param {RippleAddress} options.account
|
||||
* @param {Number} [-1] options.ledger_index_min
|
||||
* @param {Number} [-1] options.ledger_index_max
|
||||
* @param {Boolean} [false] options.earliestFirst
|
||||
* @param {Boolean} [false] options.binary
|
||||
* @param {Boolean} [false] options.exclude_failed
|
||||
* @param {Number} [DEFAULT_RESULTS_PER_PAGE] options.min
|
||||
* @param {Number} [DEFAULT_RESULTS_PER_PAGE] options.max
|
||||
* @param {Array of Strings} options.types Possible values are "payment",
|
||||
* "offercreate", "offercancel", "trustset", "accountset"
|
||||
* @param {opaque value} options.marker
|
||||
* @param {Array of Transactions} options.previous_transactions
|
||||
* Included automatically when this function is called recursively
|
||||
* @param {Express.js Response} res
|
||||
* @param {Function} callback
|
||||
*
|
||||
* @callback
|
||||
* @param {Error} error
|
||||
* @param {Array of transactions in JSON format} transactions
|
||||
*/
|
||||
function getAccountTransactions(api, options, callback) {
|
||||
try {
|
||||
validate.address(options.account);
|
||||
} catch(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!options.min) {
|
||||
options.min = module.exports.DEFAULT_RESULTS_PER_PAGE;
|
||||
}
|
||||
if (!options.max) {
|
||||
options.max = Math.max(options.min,
|
||||
module.exports.DEFAULT_RESULTS_PER_PAGE);
|
||||
}
|
||||
if (!options.limit) {
|
||||
options.limit = module.exports.DEFAULT_LIMIT;
|
||||
}
|
||||
|
||||
function queryTransactions(async_callback) {
|
||||
getTransactionsHelper(api, options, async_callback);
|
||||
}
|
||||
|
||||
function filterTransactions(transactions, async_callback) {
|
||||
async_callback(null, transactionFilter(transactions, options));
|
||||
}
|
||||
|
||||
function sortTransactions(transactions, async_callback) {
|
||||
const compare = options.earliestFirst ? utils.compareTransactions :
|
||||
_.rearg(utils.compareTransactions, 1, 0);
|
||||
transactions.sort(compare);
|
||||
async_callback(null, transactions);
|
||||
}
|
||||
|
||||
function mergeAndTruncateResults(txns, async_callback) {
|
||||
let transactions = txns;
|
||||
if (options.previous_transactions
|
||||
&& options.previous_transactions.length > 0) {
|
||||
transactions = options.previous_transactions.concat(transactions);
|
||||
}
|
||||
if (options.offset && options.offset > 0) {
|
||||
const offset_remaining = options.offset - transactions.length;
|
||||
transactions = transactions.slice(options.offset);
|
||||
options.offset = offset_remaining;
|
||||
}
|
||||
if (transactions.length > options.max) {
|
||||
transactions = transactions.slice(0, options.max);
|
||||
}
|
||||
async_callback(null, transactions);
|
||||
}
|
||||
|
||||
function asyncWaterfallCallback(error, transactions) {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
if (!options.min || transactions.length >= options.min || !options.marker) {
|
||||
callback(null, transactions);
|
||||
} else {
|
||||
options.previous_transactions = transactions;
|
||||
setImmediate(function() {
|
||||
getAccountTransactions(api, options, callback);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const steps = [
|
||||
queryTransactions,
|
||||
filterTransactions,
|
||||
sortTransactions,
|
||||
mergeAndTruncateResults
|
||||
];
|
||||
|
||||
async.waterfall(steps, asyncWaterfallCallback);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DEFAULT_LIMIT: 200,
|
||||
DEFAULT_RESULTS_PER_PAGE: DEFAULT_RESULTS_PER_PAGE,
|
||||
NUM_TRANSACTION_TYPES: 5,
|
||||
DEFAULT_LEDGER_BUFFER: 3,
|
||||
getTransaction: getTransaction,
|
||||
getAccountTransactions: getAccountTransactions
|
||||
};
|
||||
131
src/api/ledger/trustlines.js
Normal file
131
src/api/ledger/trustlines.js
Normal file
@@ -0,0 +1,131 @@
|
||||
/* globals Promise: true */
|
||||
/* eslint-disable valid-jsdoc */
|
||||
'use strict';
|
||||
const utils = require('./utils');
|
||||
const validator = utils.common.schemaValidator;
|
||||
const validate = utils.common.validate;
|
||||
|
||||
const DefaultPageLimit = 200;
|
||||
|
||||
/**
|
||||
* Retrieves all trustlines for a given account
|
||||
*
|
||||
* Notes:
|
||||
* In order to use paging, you must provide at least ledger as a query parameter
|
||||
* Additionally, any limit lower than 10 will be bumped up to 10.
|
||||
*
|
||||
* @url
|
||||
* @param {String} request.params.account - account to retrieve trustlines for
|
||||
*
|
||||
* @query
|
||||
* @param {String ISO 4217 Currency Code} [request.query.currency]
|
||||
* - only request trustlines with given currency
|
||||
* @param {RippleAddress} [request.query.counterparty]
|
||||
* - only request trustlines with given counterparty
|
||||
* @param {String} [request.query.marker] - start position in response paging
|
||||
* @param {Number String} [request.query.limit] - max results per response
|
||||
* @param {Number String} [request.query.ledger] - identifier
|
||||
*
|
||||
*/
|
||||
function getTrustLines(account, options, callback) {
|
||||
validate.address(account);
|
||||
validate.currency(options.currency, true);
|
||||
validate.counterparty(options.counterparty, true);
|
||||
validate.options(options);
|
||||
|
||||
const self = this;
|
||||
|
||||
const currencyRE = new RegExp(options.currency ?
|
||||
('^' + options.currency.toUpperCase() + '$') : /./);
|
||||
|
||||
function getAccountLines(prevResult) {
|
||||
const isAggregate = options.limit === 'all';
|
||||
if (prevResult && (!isAggregate || !prevResult.marker)) {
|
||||
return Promise.resolve(prevResult);
|
||||
}
|
||||
|
||||
const promise = new Promise(function(resolve, reject) {
|
||||
let accountLinesRequest;
|
||||
let marker;
|
||||
let ledger;
|
||||
let limit;
|
||||
|
||||
if (prevResult) {
|
||||
marker = prevResult.marker;
|
||||
limit = prevResult.limit;
|
||||
ledger = prevResult.ledger_index;
|
||||
} else {
|
||||
marker = options.marker;
|
||||
limit = validator.isValid(options.limit, 'UINT32')
|
||||
? Number(options.limit) : DefaultPageLimit;
|
||||
ledger = utils.parseLedger(options.ledger);
|
||||
}
|
||||
|
||||
accountLinesRequest = self.remote.requestAccountLines({
|
||||
account: account,
|
||||
marker: marker,
|
||||
limit: limit,
|
||||
ledger: ledger
|
||||
});
|
||||
|
||||
if (options.counterparty) {
|
||||
accountLinesRequest.message.peer = options.counterparty;
|
||||
}
|
||||
|
||||
accountLinesRequest.once('error', reject);
|
||||
accountLinesRequest.once('success', function(nextResult) {
|
||||
|
||||
const lines = [ ];
|
||||
nextResult.lines.forEach(function(line) {
|
||||
if (!currencyRE.test(line.currency)) {
|
||||
return;
|
||||
}
|
||||
lines.push({
|
||||
account: account,
|
||||
counterparty: line.account,
|
||||
currency: line.currency,
|
||||
limit: line.limit,
|
||||
reciprocated_limit: line.limit_peer,
|
||||
account_allows_rippling: line.no_ripple ? !line.no_ripple : true,
|
||||
counterparty_allows_rippling: line.no_ripple_peer
|
||||
? !line.no_ripple_peer : true,
|
||||
account_trustline_frozen: line.freeze ? line.freeze : false,
|
||||
counterparty_trustline_frozen: line.freeze_peer
|
||||
? line.freeze_peer : false
|
||||
});
|
||||
});
|
||||
|
||||
nextResult.lines = prevResult ? prevResult.lines.concat(lines) : lines;
|
||||
resolve([nextResult]);
|
||||
});
|
||||
accountLinesRequest.request();
|
||||
});
|
||||
|
||||
return promise.spread(getAccountLines);
|
||||
}
|
||||
|
||||
function respondWithTrustlines(result) {
|
||||
const promise = new Promise(function(resolve) {
|
||||
const trustlines = {};
|
||||
|
||||
if (result.marker) {
|
||||
trustlines.marker = result.marker;
|
||||
}
|
||||
|
||||
trustlines.limit = result.limit;
|
||||
trustlines.ledger = result.ledger_index;
|
||||
trustlines.validated = result.validated;
|
||||
trustlines.trustlines = result.lines;
|
||||
|
||||
resolve(callback(null, trustlines));
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
getAccountLines()
|
||||
.then(respondWithTrustlines)
|
||||
.catch(callback);
|
||||
}
|
||||
|
||||
module.exports.getTrustLines = getTrustLines;
|
||||
371
src/api/ledger/tx-to-rest-converter.js
Normal file
371
src/api/ledger/tx-to-rest-converter.js
Normal file
@@ -0,0 +1,371 @@
|
||||
/* eslint-disable valid-jsdoc */
|
||||
'use strict';
|
||||
const _ = require('lodash');
|
||||
const transactionParser = require('ripple-lib-transactionparser');
|
||||
const utils = require('./utils');
|
||||
const ripple = utils.common.core;
|
||||
const constants = utils.common.constants;
|
||||
const parseBalanceChanges = transactionParser.parseBalanceChanges;
|
||||
const parseOrderBookChanges = transactionParser.parseOrderBookChanges;
|
||||
const dropsToXrp = utils.common.dropsToXrp;
|
||||
|
||||
// This is just to support the legacy naming of "counterparty", this
|
||||
// function should be removed when "issuer" is eliminated
|
||||
function renameCounterpartyToIssuerInOrderChanges(orderChanges) {
|
||||
return _.mapValues(orderChanges, function(changes) {
|
||||
return _.map(changes, function(change) {
|
||||
return utils.renameCounterpartyToIssuerInOrder(change);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function renameCounterpartyToIssuerInBalanceChanges(balanceChanges) {
|
||||
return _.mapValues(balanceChanges, function(changes) {
|
||||
return _.map(changes, function(change) {
|
||||
return utils.renameCounterpartyToIssuer(change);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper that parses bit flags from ripple response
|
||||
*
|
||||
* @param {Number} responseFlags - Integer flag on the ripple response
|
||||
* @param {Object} flags - Object with parameter name and bit flag value pairs
|
||||
*
|
||||
* @returns {Object} parsedFlags - Object with parameter name and boolean
|
||||
* flags depending on response flag
|
||||
*/
|
||||
function parseFlagsFromResponse(responseFlags, flags) {
|
||||
const parsedFlags = {};
|
||||
|
||||
for (let flagName in flags) {
|
||||
const flag = flags[flagName];
|
||||
parsedFlags[flag.name] = Boolean(responseFlags & flag.value);
|
||||
}
|
||||
|
||||
return parsedFlags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a transaction in rippled tx format
|
||||
* to a ripple-rest payment
|
||||
*
|
||||
* @param {transaction} tx
|
||||
* @param {Function} callback
|
||||
* @param {Object} options
|
||||
*
|
||||
* @callback
|
||||
* @param {Error} error
|
||||
* @param {Object} payment
|
||||
*/
|
||||
|
||||
function isPartialPayment(tx) {
|
||||
return (tx.Flags & ripple.Transaction.flags.Payment.PartialPayment) !== 0;
|
||||
}
|
||||
|
||||
function isNoDirectRipple(tx) {
|
||||
return (tx.Flags & ripple.Transaction.flags.Payment.NoRippleDirect) !== 0;
|
||||
}
|
||||
|
||||
function convertAmount(amount) {
|
||||
if (typeof amount === 'string') {
|
||||
return {
|
||||
value: dropsToXrp(amount),
|
||||
currency: 'XRP',
|
||||
issuer: ''
|
||||
};
|
||||
}
|
||||
return amount;
|
||||
}
|
||||
|
||||
function parsePaymentMeta(account, tx, meta) {
|
||||
if (_.isUndefined(meta) || _.isEmpty(meta)) {
|
||||
return {};
|
||||
}
|
||||
if (meta.TransactionResult === 'tejSecretInvalid') {
|
||||
throw new Error('Invalid secret provided.');
|
||||
}
|
||||
|
||||
const balanceChanges = renameCounterpartyToIssuerInBalanceChanges(
|
||||
parseBalanceChanges(meta));
|
||||
|
||||
const order_changes = renameCounterpartyToIssuerInOrderChanges(
|
||||
parseOrderBookChanges(meta))[account];
|
||||
|
||||
const partialPayment = (isPartialPayment(tx) && meta.DeliveredAmount) ? {
|
||||
destination_amount_submitted: convertAmount(tx.Amount),
|
||||
source_amount_submitted: convertAmount(tx.SendMax || tx.Amount)
|
||||
} : {};
|
||||
|
||||
return _.assign({
|
||||
result: meta.TransactionResult,
|
||||
balance_changes: balanceChanges[account] || [],
|
||||
source_balance_changes: balanceChanges[tx.Account] || [],
|
||||
destination_balance_changes: balanceChanges[tx.Destination] || [],
|
||||
order_changes: order_changes || []
|
||||
}, partialPayment);
|
||||
}
|
||||
|
||||
function parsePaymentFromTx(account, message, meta) {
|
||||
if (!account) {
|
||||
throw new Error('Internal Error. must supply options.account');
|
||||
}
|
||||
|
||||
const tx = message.tx_json;
|
||||
if (tx.TransactionType !== 'Payment') {
|
||||
throw new Error('Not a payment. The transaction corresponding to '
|
||||
+ 'the given identifier is not a payment.');
|
||||
}
|
||||
|
||||
let amount;
|
||||
// if there is a DeliveredAmount we should use it over Amount there should
|
||||
// always be a DeliveredAmount if the partial payment flag is set. also
|
||||
// there shouldn't be a DeliveredAmount if there's no partial payment flag
|
||||
if (isPartialPayment(tx) && meta && meta.DeliveredAmount) {
|
||||
amount = meta.DeliveredAmount;
|
||||
} else {
|
||||
amount = tx.Amount;
|
||||
}
|
||||
|
||||
const source_amount = utils.parseCurrencyAmount(tx.SendMax || amount, true);
|
||||
const destination_amount = utils.parseCurrencyAmount(amount, true);
|
||||
|
||||
const payment = {
|
||||
// User supplied
|
||||
source_account: tx.Account,
|
||||
source_tag: (tx.SourceTag ? '' + tx.SourceTag : ''),
|
||||
source_amount: source_amount,
|
||||
source_slippage: '0', // TODO: why is this hard-coded?
|
||||
destination_account: tx.Destination,
|
||||
destination_tag: (tx.DestinationTag ? '' + tx.DestinationTag : ''),
|
||||
destination_amount: destination_amount,
|
||||
// Advanced options
|
||||
invoice_id: tx.InvoiceID || '',
|
||||
paths: JSON.stringify(tx.Paths || []),
|
||||
no_direct_ripple: isNoDirectRipple(tx),
|
||||
partial_payment: isPartialPayment(tx),
|
||||
// TODO: Update to use `unaffected` when perspective account in URI
|
||||
// is not affected
|
||||
direction: (account === tx.Account ? 'outgoing' :
|
||||
(account === tx.Destination ? 'incoming' : 'passthrough')),
|
||||
timestamp: (tx.date
|
||||
? new Date(ripple.utils.toTimestamp(tx.date)).toISOString() : ''),
|
||||
fee: dropsToXrp(tx.Fee) || ''
|
||||
};
|
||||
|
||||
if (Array.isArray(tx.Memos) && tx.Memos.length > 0) {
|
||||
payment.memos = [];
|
||||
for (let m = 0; m < tx.Memos.length; m++) {
|
||||
payment.memos.push(tx.Memos[m].Memo);
|
||||
}
|
||||
}
|
||||
|
||||
const fullPayment = _.assign(payment, parsePaymentMeta(account, tx, meta));
|
||||
return _.assign({payment: fullPayment}, meta);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an OfferCreate or OfferCancel transaction in rippled tx format
|
||||
* to a ripple-rest order_change
|
||||
*
|
||||
* @param {Object} tx
|
||||
* @param {Object} options
|
||||
* @param {String} options.account - The account to use as perspective when
|
||||
* parsing the transaction.
|
||||
*
|
||||
* @returns {Promise.<Object,Error>} - resolves to a parsed OrderChange
|
||||
* transaction or an Error
|
||||
*/
|
||||
|
||||
function parseOrderFromTx(tx, options) {
|
||||
if (!options.account) {
|
||||
throw new Error('Internal Error. must supply options.account');
|
||||
}
|
||||
if (tx.TransactionType !== 'OfferCreate'
|
||||
&& tx.TransactionType !== 'OfferCancel') {
|
||||
throw new Error('Invalid parameter: identifier. The transaction '
|
||||
+ 'corresponding to the given identifier is not an order');
|
||||
}
|
||||
if (tx.meta !== undefined && tx.meta.TransactionResult !== undefined) {
|
||||
if (tx.meta.TransactionResult === 'tejSecretInvalid') {
|
||||
throw new Error('Invalid secret provided.');
|
||||
}
|
||||
}
|
||||
|
||||
let order;
|
||||
const flags = parseFlagsFromResponse(tx.flags, constants.OfferCreateFlags);
|
||||
const action = tx.TransactionType === 'OfferCreate'
|
||||
? 'order_create' : 'order_cancel';
|
||||
const balance_changes = tx.meta
|
||||
? parseBalanceChanges(tx.meta)[options.account] || [] : [];
|
||||
const timestamp = tx.date
|
||||
? new Date(ripple.utils.toTimestamp(tx.date)).toISOString() : '';
|
||||
const order_changes = tx.meta ?
|
||||
parseOrderBookChanges(tx.meta)[options.account] : [];
|
||||
|
||||
let direction;
|
||||
if (options.account === tx.Account) {
|
||||
direction = 'outgoing';
|
||||
} else if (balance_changes.length && order_changes.length) {
|
||||
direction = 'incoming';
|
||||
} else {
|
||||
direction = 'passthrough';
|
||||
}
|
||||
|
||||
if (action === 'order_create') {
|
||||
order = {
|
||||
account: tx.Account,
|
||||
taker_pays: utils.parseCurrencyAmount(tx.TakerPays),
|
||||
taker_gets: utils.parseCurrencyAmount(tx.TakerGets),
|
||||
passive: flags.passive,
|
||||
immediate_or_cancel: flags.immediate_or_cancel,
|
||||
fill_or_kill: flags.fill_or_kill,
|
||||
type: flags.sell ? 'sell' : 'buy',
|
||||
sequence: tx.Sequence
|
||||
};
|
||||
} else {
|
||||
order = {
|
||||
account: tx.Account,
|
||||
type: 'cancel',
|
||||
sequence: tx.Sequence,
|
||||
cancel_sequence: tx.OfferSequence
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
hash: tx.hash,
|
||||
ledger: tx.ledger_index,
|
||||
validated: tx.validated,
|
||||
timestamp: timestamp,
|
||||
fee: dropsToXrp(tx.Fee),
|
||||
action: action,
|
||||
direction: direction,
|
||||
order: order,
|
||||
balance_changes: balance_changes,
|
||||
order_changes: order_changes || []
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the pathfind results returned from rippled into an
|
||||
* array of payments in the ripple-rest format. The client should be
|
||||
* able to submit any of the payments in the array back to ripple-rest.
|
||||
*
|
||||
* @param {rippled Pathfind results} pathfindResults
|
||||
* @param {Amount} options.destination_amount Since this is not returned by
|
||||
* rippled in the pathfind results it can either be added
|
||||
* to the results or included in the options here
|
||||
* @param {RippleAddress} options.source_account Since this is not returned
|
||||
* by rippled in the pathfind results it can either be added
|
||||
* to the results or included in the options here
|
||||
*
|
||||
* @returns {Array of Payments} payments
|
||||
*/
|
||||
function parsePaymentsFromPathFind(pathfindResults) {
|
||||
return pathfindResults.alternatives.map(function(alternative) {
|
||||
return {
|
||||
source_account: pathfindResults.source_account,
|
||||
source_tag: '',
|
||||
source_amount: (typeof alternative.source_amount === 'string' ?
|
||||
{
|
||||
value: dropsToXrp(alternative.source_amount),
|
||||
currency: 'XRP',
|
||||
issuer: ''
|
||||
} :
|
||||
{
|
||||
value: alternative.source_amount.value,
|
||||
currency: alternative.source_amount.currency,
|
||||
issuer: (typeof alternative.source_amount.issuer !== 'string'
|
||||
|| alternative.source_amount.issuer === pathfindResults.source_account
|
||||
? '' : alternative.source_amount.issuer)
|
||||
}),
|
||||
source_slippage: '0',
|
||||
destination_account: pathfindResults.destination_account,
|
||||
destination_tag: '',
|
||||
destination_amount: (
|
||||
typeof pathfindResults.destination_amount === 'string' ?
|
||||
{
|
||||
value: dropsToXrp(pathfindResults.destination_amount),
|
||||
currency: 'XRP',
|
||||
issuer: ''
|
||||
} :
|
||||
{
|
||||
value: pathfindResults.destination_amount.value,
|
||||
currency: pathfindResults.destination_amount.currency,
|
||||
issuer: pathfindResults.destination_amount.issuer
|
||||
}),
|
||||
invoice_id: '',
|
||||
paths: JSON.stringify(alternative.paths_computed),
|
||||
partial_payment: false,
|
||||
no_direct_ripple: false
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function parseOrderCancellationResponse(message, meta) {
|
||||
const order = {
|
||||
account: message.tx_json.Account,
|
||||
fee: dropsToXrp(message.tx_json.Fee),
|
||||
offer_sequence: message.tx_json.OfferSequence,
|
||||
sequence: message.tx_json.Sequence
|
||||
};
|
||||
return _.assign({order: order}, meta);
|
||||
}
|
||||
|
||||
function parseOrderResponse(message, meta) {
|
||||
const order = {
|
||||
account: message.tx_json.Account,
|
||||
taker_gets: utils.parseCurrencyAmount(message.tx_json.TakerGets),
|
||||
taker_pays: utils.parseCurrencyAmount(message.tx_json.TakerPays),
|
||||
fee: dropsToXrp(message.tx_json.Fee),
|
||||
type: (message.tx_json.Flags & ripple.Transaction.flags.OfferCreate.Sell)
|
||||
> 0 ? 'sell' : 'buy',
|
||||
sequence: message.tx_json.Sequence
|
||||
};
|
||||
return _.assign({order: order}, meta);
|
||||
}
|
||||
|
||||
function parseTrustLineResponse(message, meta) {
|
||||
const limit = message.tx_json.LimitAmount;
|
||||
const parsedFlags = parseFlagsFromResponse(message.tx_json.Flags,
|
||||
constants.TrustSetResponseFlags);
|
||||
const trustline = {
|
||||
account: message.tx_json.Account,
|
||||
limit: limit.value,
|
||||
currency: limit.currency,
|
||||
counterparty: limit.issuer,
|
||||
account_allows_rippling: !parsedFlags.prevent_rippling,
|
||||
account_trustline_frozen: parsedFlags.account_trustline_frozen,
|
||||
authorized: parsedFlags.authorized ? parsedFlags.authorized : undefined
|
||||
};
|
||||
return _.assign({trustline: trustline}, meta);
|
||||
}
|
||||
|
||||
function parseSettingsResponse(settings, message, meta) {
|
||||
const _settings = {};
|
||||
for (let flagName in constants.AccountSetIntFlags) {
|
||||
const flag = constants.AccountSetIntFlags[flagName];
|
||||
_settings[flag.name] = settings[flag.name];
|
||||
}
|
||||
|
||||
for (let fieldName in constants.AccountRootFields) {
|
||||
const field = constants.AccountRootFields[fieldName];
|
||||
_settings[field.name] = settings[field.name];
|
||||
}
|
||||
|
||||
_.assign(_settings, parseFlagsFromResponse(message.tx_json.Flags,
|
||||
constants.AccountSetResponseFlags));
|
||||
return _.assign({settings: _settings}, meta);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
parsePaymentFromTx: parsePaymentFromTx,
|
||||
parsePaymentsFromPathFind: parsePaymentsFromPathFind,
|
||||
parseOrderFromTx: parseOrderFromTx,
|
||||
parseCancelOrderFromTx: parseOrderCancellationResponse,
|
||||
parseSubmitOrderFromTx: parseOrderResponse,
|
||||
parseTrustResponseFromTx: parseTrustLineResponse,
|
||||
parseSettingsResponseFromTx: parseSettingsResponse,
|
||||
parseFlagsFromResponse: parseFlagsFromResponse
|
||||
};
|
||||
165
src/api/ledger/utils.js
Normal file
165
src/api/ledger/utils.js
Normal file
@@ -0,0 +1,165 @@
|
||||
/* eslint-disable valid-jsdoc */
|
||||
'use strict';
|
||||
const _ = require('lodash');
|
||||
const async = require('async');
|
||||
const asyncify = require('simple-asyncify');
|
||||
const common = require('../common');
|
||||
const ripple = common.core;
|
||||
const validator = common.schemaValidator;
|
||||
|
||||
function renameCounterpartyToIssuer(amount) {
|
||||
if (amount === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
const issuer = amount.counterparty === undefined ?
|
||||
amount.issuer : amount.counterparty;
|
||||
const withIssuer = _.assign({}, amount, {issuer: issuer});
|
||||
return _.omit(withIssuer, 'counterparty');
|
||||
}
|
||||
|
||||
function renameCounterpartyToIssuerInOrder(order) {
|
||||
const taker_gets = renameCounterpartyToIssuer(order.taker_gets);
|
||||
const taker_pays = renameCounterpartyToIssuer(order.taker_pays);
|
||||
const changes = {taker_gets: taker_gets, taker_pays: taker_pays};
|
||||
return _.assign({}, order, _.omit(changes, _.isUndefined));
|
||||
}
|
||||
|
||||
function isValidHash256(hash) {
|
||||
return validator.isValid(hash, 'Hash256');
|
||||
}
|
||||
|
||||
function parseLedger(ledger) {
|
||||
if (/^current$|^closed$|^validated$/.test(ledger)) {
|
||||
return ledger;
|
||||
}
|
||||
|
||||
if (ledger && Number(ledger) >= 0 && isFinite(Number(ledger))) {
|
||||
return Number(ledger);
|
||||
}
|
||||
|
||||
if (isValidHash256(ledger)) {
|
||||
return ledger;
|
||||
}
|
||||
|
||||
return 'validated';
|
||||
}
|
||||
|
||||
function parseCurrencyAmount(rippledAmount, useIssuer) {
|
||||
const amount = {};
|
||||
|
||||
if (typeof rippledAmount === 'string') {
|
||||
amount.currency = 'XRP';
|
||||
amount.value = common.dropsToXrp(rippledAmount);
|
||||
if (useIssuer) {
|
||||
amount.issuer = '';
|
||||
} else {
|
||||
amount.counterparty = '';
|
||||
}
|
||||
} else {
|
||||
amount.currency = rippledAmount.currency;
|
||||
amount.value = rippledAmount.value;
|
||||
if (useIssuer) {
|
||||
amount.issuer = rippledAmount.issuer;
|
||||
} else {
|
||||
amount.counterparty = rippledAmount.issuer;
|
||||
}
|
||||
}
|
||||
|
||||
return amount;
|
||||
}
|
||||
|
||||
|
||||
function parseCurrencyQuery(query) {
|
||||
const params = query.split('+');
|
||||
|
||||
if (!isNaN(params[0])) {
|
||||
return {
|
||||
value: (params.length >= 1 ? params[0] : ''),
|
||||
currency: (params.length >= 2 ? params[1] : ''),
|
||||
counterparty: (params.length >= 3 ? params[2] : '')
|
||||
};
|
||||
}
|
||||
return {
|
||||
currency: (params.length >= 1 ? params[0] : ''),
|
||||
counterparty: (params.length >= 2 ? params[1] : '')
|
||||
};
|
||||
}
|
||||
|
||||
function signum(num) {
|
||||
return (num === 0) ? 0 : (num > 0 ? 1 : -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Order two rippled transactions based on their ledger_index.
|
||||
* If two transactions took place in the same ledger, sort
|
||||
* them based on TransactionIndex
|
||||
* See: https://ripple.com/build/transactions/
|
||||
*
|
||||
* @param {Object} first
|
||||
* @param {Object} second
|
||||
* @returns {Number} [-1, 0, 1]
|
||||
*/
|
||||
function compareTransactions(first, second) {
|
||||
if (first.ledger_index === second.ledger_index) {
|
||||
return signum(
|
||||
Number(first.meta.TransactionIndex) -
|
||||
Number(second.meta.TransactionIndex));
|
||||
}
|
||||
return Number(first.ledger_index) < Number(second.ledger_index) ? -1 : 1;
|
||||
}
|
||||
|
||||
function isValidLedgerSequence(ledger) {
|
||||
return (Number(ledger) >= 0) && isFinite(Number(ledger));
|
||||
}
|
||||
|
||||
function isValidLedgerHash(ledger) {
|
||||
return ripple.UInt256.is_valid(ledger);
|
||||
}
|
||||
|
||||
function isValidLedgerWord(ledger) {
|
||||
return (/^current$|^closed$|^validated$/.test(ledger));
|
||||
}
|
||||
|
||||
function attachDate(api, baseTransactions, callback) {
|
||||
const groupedTx = _.groupBy(baseTransactions, function(tx) {
|
||||
return tx.ledger_index;
|
||||
});
|
||||
|
||||
function attachDateToTransactions(transactions, data) {
|
||||
return _.map(transactions, function(tx) {
|
||||
return _.assign(tx, {date: data.ledger.close_time});
|
||||
});
|
||||
}
|
||||
|
||||
function getLedger(ledgerIndex, _callback) {
|
||||
api.remote.requestLedger({ledger_index: ledgerIndex}, _callback);
|
||||
}
|
||||
|
||||
function attachDateToLedgerTransactions(_groupedTx, ledger, _callback) {
|
||||
const transactions = _groupedTx[ledger];
|
||||
async.waterfall([
|
||||
_.partial(getLedger, Number(ledger)),
|
||||
asyncify(_.partial(attachDateToTransactions, transactions))
|
||||
], _callback);
|
||||
}
|
||||
|
||||
const ledgers = _.keys(groupedTx);
|
||||
const flatMap = async.seq(async.map, asyncify(_.flatten));
|
||||
const iterator = _.partial(attachDateToLedgerTransactions, groupedTx);
|
||||
flatMap(ledgers, iterator, callback);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isValidLedgerSequence: isValidLedgerSequence,
|
||||
isValidLedgerWord: isValidLedgerWord,
|
||||
isValidLedgerHash: isValidLedgerHash,
|
||||
parseLedger: parseLedger,
|
||||
parseCurrencyAmount: parseCurrencyAmount,
|
||||
parseCurrencyQuery: parseCurrencyQuery,
|
||||
compareTransactions: compareTransactions,
|
||||
renameCounterpartyToIssuer: renameCounterpartyToIssuer,
|
||||
renameCounterpartyToIssuerInOrder: renameCounterpartyToIssuerInOrder,
|
||||
attachDate: attachDate,
|
||||
common: common
|
||||
};
|
||||
|
||||
33
src/api/server/server.js
Normal file
33
src/api/server/server.js
Normal file
@@ -0,0 +1,33 @@
|
||||
'use strict';
|
||||
const common = require('../common');
|
||||
|
||||
function connect(callback) {
|
||||
this.remote.connect(callback);
|
||||
}
|
||||
|
||||
function isConnected() {
|
||||
return common.server.isConnected(this.remote);
|
||||
}
|
||||
|
||||
function getServerStatus(callback) {
|
||||
common.server.getStatus(this.remote, function(error, status) {
|
||||
if (error) {
|
||||
callback(new common.errors.RippledNetworkError(error.message));
|
||||
} else {
|
||||
callback(null, status);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getFee(callback) {
|
||||
const fee = this.remote.createTransaction()._computeFee();
|
||||
callback(null, {fee: common.dropsToXrp(fee)});
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
connect: connect,
|
||||
isConnected: isConnected,
|
||||
getServerStatus: getServerStatus,
|
||||
getFee: getFee
|
||||
};
|
||||
60
src/api/transaction/order.js
Normal file
60
src/api/transaction/order.js
Normal file
@@ -0,0 +1,60 @@
|
||||
'use strict';
|
||||
const _ = require('lodash');
|
||||
const utils = require('./utils');
|
||||
const ripple = utils.common.core;
|
||||
const validate = utils.common.validate;
|
||||
|
||||
function renameCounterpartyToIssuer(amount) {
|
||||
if (amount === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
const issuer = amount.counterparty === undefined ?
|
||||
amount.issuer : amount.counterparty;
|
||||
const withIssuer = _.assign({}, amount, {issuer: issuer});
|
||||
return _.omit(withIssuer, 'counterparty');
|
||||
}
|
||||
|
||||
function renameCounterpartyToIssuerInOrder(order) {
|
||||
const taker_gets = renameCounterpartyToIssuer(order.taker_gets);
|
||||
const taker_pays = renameCounterpartyToIssuer(order.taker_pays);
|
||||
const changes = {taker_gets: taker_gets, taker_pays: taker_pays};
|
||||
return _.assign({}, order, _.omit(changes, _.isUndefined));
|
||||
}
|
||||
|
||||
const OfferCreateFlags = {
|
||||
Passive: {name: 'passive', set: 'Passive'},
|
||||
ImmediateOrCancel: {name: 'immediate_or_cancel', set: 'ImmediateOrCancel'},
|
||||
FillOrKill: {name: 'fill_or_kill', set: 'FillOrKill'}
|
||||
};
|
||||
|
||||
function createOrderTransaction(account, order) {
|
||||
validate.address(account);
|
||||
validate.order(order);
|
||||
|
||||
const _order = renameCounterpartyToIssuerInOrder(order);
|
||||
const transaction = new ripple.Transaction();
|
||||
const takerPays = _order.taker_pays.currency !== 'XRP'
|
||||
? _order.taker_pays : utils.xrpToDrops(_order.taker_pays.value);
|
||||
const takerGets = _order.taker_gets.currency !== 'XRP'
|
||||
? _order.taker_gets : utils.xrpToDrops(_order.taker_gets.value);
|
||||
|
||||
transaction.offerCreate(account, ripple.Amount.from_json(takerPays),
|
||||
ripple.Amount.from_json(takerGets));
|
||||
|
||||
utils.setTransactionBitFlags(transaction, {
|
||||
input: _order,
|
||||
flags: OfferCreateFlags
|
||||
});
|
||||
|
||||
if (_order.type === 'sell') {
|
||||
transaction.setFlags('Sell');
|
||||
}
|
||||
return transaction;
|
||||
}
|
||||
|
||||
function prepareOrder(account, order, instructions, callback) {
|
||||
const transaction = createOrderTransaction(account, order);
|
||||
utils.createTxJSON(transaction, this.remote, instructions, callback);
|
||||
}
|
||||
|
||||
module.exports = utils.wrapCatch(prepareOrder);
|
||||
20
src/api/transaction/ordercancellation.js
Normal file
20
src/api/transaction/ordercancellation.js
Normal file
@@ -0,0 +1,20 @@
|
||||
'use strict';
|
||||
const utils = require('./utils');
|
||||
const validate = utils.common.validate;
|
||||
const ripple = utils.common.core;
|
||||
|
||||
function createOrderCancellationTransaction(account, sequence) {
|
||||
validate.address(account);
|
||||
validate.sequence(sequence);
|
||||
|
||||
const transaction = new ripple.Transaction();
|
||||
transaction.offerCancel(account, sequence);
|
||||
return transaction;
|
||||
}
|
||||
|
||||
function prepareOrderCancellation(account, sequence, instructions, callback) {
|
||||
const transaction = createOrderCancellationTransaction(account, sequence);
|
||||
utils.createTxJSON(transaction, this.remote, instructions, callback);
|
||||
}
|
||||
|
||||
module.exports = utils.wrapCatch(prepareOrderCancellation);
|
||||
125
src/api/transaction/payment.js
Normal file
125
src/api/transaction/payment.js
Normal file
@@ -0,0 +1,125 @@
|
||||
/* eslint-disable valid-jsdoc */
|
||||
'use strict';
|
||||
const BigNumber = require('bignumber.js');
|
||||
const utils = require('./utils');
|
||||
const ripple = utils.common.core;
|
||||
const validate = utils.common.validate;
|
||||
const convertAmount = utils.common.convertAmount;
|
||||
|
||||
function isSendMaxAllowed(payment) {
|
||||
const srcAmt = payment.source_amount;
|
||||
const dstAmt = payment.destination_amount;
|
||||
|
||||
// Don't set SendMax for XRP->XRP payment
|
||||
// temREDUNDANT_SEND_MAX removed in:
|
||||
// https://github.com/ripple/rippled/commit/
|
||||
// c522ffa6db2648f1d8a987843e7feabf1a0b7de8/
|
||||
return srcAmt && !(srcAmt.currency === 'XRP' && dstAmt.currency === 'XRP');
|
||||
}
|
||||
|
||||
function createPaymentTransaction(account, payment) {
|
||||
validate.address(account);
|
||||
validate.payment(payment);
|
||||
|
||||
// Convert blank issuer to sender's address
|
||||
// (Ripple convention for 'any issuer')
|
||||
// https://ripple.com/build/transactions/
|
||||
// #special-issuer-values-for-sendmax-and-amount
|
||||
// https://ripple.com/build/ripple-rest/#counterparties-in-payments
|
||||
if (payment.source_amount && payment.source_amount.currency !== 'XRP'
|
||||
&& payment.source_amount.issuer === '') {
|
||||
payment.source_amount.issuer = payment.source_account;
|
||||
}
|
||||
|
||||
// Convert blank issuer to destinations's address
|
||||
// (Ripple convention for 'any issuer')
|
||||
// https://ripple.com/build/transactions/
|
||||
// #special-issuer-values-for-sendmax-and-amount
|
||||
// https://ripple.com/build/ripple-rest/#counterparties-in-payments
|
||||
if (payment.destination_amount
|
||||
&& payment.destination_amount.currency !== 'XRP'
|
||||
&& payment.destination_amount.issuer === '') {
|
||||
payment.destination_amount.issuer = payment.destination_account;
|
||||
}
|
||||
// Uppercase currency codes
|
||||
if (payment.source_amount) {
|
||||
payment.source_amount.currency =
|
||||
payment.source_amount.currency.toUpperCase();
|
||||
}
|
||||
if (payment.destination_amount) {
|
||||
payment.destination_amount.currency =
|
||||
payment.destination_amount.currency.toUpperCase();
|
||||
}
|
||||
/* Construct payment */
|
||||
const transaction = new ripple.Transaction();
|
||||
const transactionData = {
|
||||
from: payment.source_account,
|
||||
to: payment.destination_account,
|
||||
amount: convertAmount(payment.destination_amount)
|
||||
};
|
||||
|
||||
// invoice_id Because transactionData is a object, transaction.payment
|
||||
// function is ignored invoiceID
|
||||
if (payment.invoice_id) {
|
||||
transaction.invoiceID(payment.invoice_id);
|
||||
}
|
||||
transaction.payment(transactionData);
|
||||
// Tags
|
||||
if (payment.source_tag) {
|
||||
transaction.sourceTag(parseInt(payment.source_tag, 10));
|
||||
}
|
||||
if (payment.destination_tag) {
|
||||
transaction.destinationTag(parseInt(payment.destination_tag, 10));
|
||||
}
|
||||
|
||||
// SendMax
|
||||
if (isSendMaxAllowed(payment)) {
|
||||
const max_value = new BigNumber(payment.source_amount.value)
|
||||
.plus(payment.source_slippage || 0).toString();
|
||||
|
||||
if (payment.source_amount.currency === 'XRP') {
|
||||
transaction.sendMax(utils.xrpToDrops(max_value));
|
||||
} else {
|
||||
transaction.sendMax({
|
||||
value: max_value,
|
||||
currency: payment.source_amount.currency,
|
||||
issuer: payment.source_amount.issuer
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Paths
|
||||
if (typeof payment.paths === 'string') {
|
||||
transaction.paths(JSON.parse(payment.paths));
|
||||
} else if (typeof payment.paths === 'object') {
|
||||
transaction.paths(payment.paths);
|
||||
}
|
||||
|
||||
// Memos
|
||||
if (payment.memos && Array.isArray(payment.memos)) {
|
||||
for (let m = 0; m < payment.memos.length; m++) {
|
||||
const memo = payment.memos[m];
|
||||
transaction.addMemo(memo.MemoType, memo.MemoFormat, memo.MemoData);
|
||||
}
|
||||
}
|
||||
|
||||
// Flags
|
||||
let flags = [];
|
||||
if (payment.partial_payment) {
|
||||
flags.push('PartialPayment');
|
||||
}
|
||||
if (payment.no_direct_ripple) {
|
||||
flags.push('NoRippleDirect');
|
||||
}
|
||||
if (flags.length > 0) {
|
||||
transaction.setFlags(flags);
|
||||
}
|
||||
return transaction;
|
||||
}
|
||||
|
||||
function preparePayment(account, payment, instructions, callback) {
|
||||
const transaction = createPaymentTransaction(account, payment);
|
||||
utils.createTxJSON(transaction, this.remote, instructions, callback);
|
||||
}
|
||||
|
||||
module.exports = utils.wrapCatch(preparePayment);
|
||||
159
src/api/transaction/settings.js
Normal file
159
src/api/transaction/settings.js
Normal file
@@ -0,0 +1,159 @@
|
||||
/* eslint-disable valid-jsdoc */
|
||||
'use strict';
|
||||
const _ = require('lodash');
|
||||
const assert = require('assert');
|
||||
const utils = require('./utils');
|
||||
const ripple = utils.common.core;
|
||||
const validate = utils.common.validate;
|
||||
const constants = utils.common.constants;
|
||||
const InvalidRequestError = utils.common.errors.InvalidRequestError;
|
||||
|
||||
// Emptry string passed to setting will clear it
|
||||
const CLEAR_SETTING = '';
|
||||
|
||||
/**
|
||||
* Pad the value of a fixed-length field
|
||||
*
|
||||
* @param {String} value
|
||||
* @param {Number} length
|
||||
* @return {String}
|
||||
*/
|
||||
function padValue(value, length) {
|
||||
assert.strictEqual(typeof value, 'string');
|
||||
assert.strictEqual(typeof length, 'number');
|
||||
|
||||
let result = value;
|
||||
|
||||
while (result.length < length) {
|
||||
result = '0' + result;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set integer flags on a transaction based on input and a flag map
|
||||
*
|
||||
* @param {Transaction} transaction
|
||||
* @param {Object} input - Object whose properties determine whether
|
||||
* to update the transaction's SetFlag or ClearFlag property
|
||||
* @param {Object} flags - Object that maps property names to transaction
|
||||
* integer flag values
|
||||
*
|
||||
* @returns undefined
|
||||
*/
|
||||
function setTransactionIntFlags(transaction, input, flags) {
|
||||
for (let flagName in flags) {
|
||||
const flag = flags[flagName];
|
||||
|
||||
if (!input.hasOwnProperty(flag.name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const value = input[flag.name];
|
||||
|
||||
if (value) {
|
||||
transaction.tx_json.SetFlag = flag.value;
|
||||
} else {
|
||||
transaction.tx_json.ClearFlag = flag.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set fields on a transaction based on input and fields schema object
|
||||
*
|
||||
* @param {Transaction} transaction
|
||||
* @param {Object} input - Object whose properties are used to set fields on
|
||||
* the transaction
|
||||
* @param {Object} fieldSchema - Object that holds the schema of each field
|
||||
*
|
||||
* @returns undefined
|
||||
*/
|
||||
function setTransactionFields(transaction, input, fieldSchema) {
|
||||
for (let fieldName in fieldSchema) {
|
||||
const field = fieldSchema[fieldName];
|
||||
let value = input[field.name];
|
||||
|
||||
if (typeof value === 'undefined') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The value required to clear an account root field varies
|
||||
if (value === CLEAR_SETTING && field.hasOwnProperty('defaults')) {
|
||||
value = field.defaults;
|
||||
}
|
||||
|
||||
if (field.encoding === 'hex') {
|
||||
// If the field is supposed to be hex, why don't we do a
|
||||
// toString('hex') on it?
|
||||
if (field.length) {
|
||||
// Field is fixed length, why are we checking here though?
|
||||
// We could move this to validateInputs
|
||||
if (value.length > field.length) {
|
||||
throw new InvalidRequestError(
|
||||
'Parameter length exceeded: ' + fieldName);
|
||||
} else if (value.length < field.length) {
|
||||
value = padValue(value, field.length);
|
||||
}
|
||||
} else {
|
||||
// Field is variable length. Expecting an ascii string as input.
|
||||
// This is currently only used for Domain field
|
||||
value = new Buffer(value, 'ascii').toString('hex');
|
||||
}
|
||||
|
||||
value = value.toUpperCase();
|
||||
}
|
||||
|
||||
transaction.tx_json[fieldName] = value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a numerical transfer rate in ripple-rest format to ripple-lib
|
||||
*
|
||||
* Note: A fee of 1% requires 101% of the destination to be sent for the
|
||||
* destination to receive 100%.
|
||||
* The transfer rate is specified as the input amount as fraction of 1.
|
||||
* To specify the default rate of 0%, a 100% input amount, specify 1.
|
||||
* To specify a rate of 1%, a 101% input amount, specify 1.01
|
||||
*
|
||||
* @param {Number|String} transferRate
|
||||
*
|
||||
* @returns {Number|String} numbers will be converted while strings
|
||||
* are returned
|
||||
*/
|
||||
function convertTransferRate(transferRate) {
|
||||
if (_.isNumber(transferRate)) {
|
||||
return transferRate * Math.pow(10, 9);
|
||||
}
|
||||
|
||||
return transferRate;
|
||||
}
|
||||
|
||||
function createSettingsTransaction(account, settings) {
|
||||
validate.address(account);
|
||||
validate.settings(settings);
|
||||
|
||||
const transaction = new ripple.Transaction();
|
||||
transaction.accountSet(account);
|
||||
|
||||
utils.setTransactionBitFlags(transaction, {
|
||||
input: settings,
|
||||
flags: constants.AccountSetFlags,
|
||||
clear_setting: CLEAR_SETTING
|
||||
});
|
||||
setTransactionIntFlags(transaction, settings, constants.AccountSetIntFlags);
|
||||
setTransactionFields(transaction, settings, constants.AccountRootFields);
|
||||
|
||||
transaction.tx_json.TransferRate = convertTransferRate(
|
||||
transaction.tx_json.TransferRate);
|
||||
return transaction;
|
||||
}
|
||||
|
||||
function prepareSettings(account, settings, instructions, callback) {
|
||||
const transaction = createSettingsTransaction(account, settings);
|
||||
utils.createTxJSON(transaction, this.remote, instructions, callback);
|
||||
}
|
||||
|
||||
module.exports = utils.wrapCatch(prepareSettings);
|
||||
65
src/api/transaction/sign.js
Normal file
65
src/api/transaction/sign.js
Normal file
@@ -0,0 +1,65 @@
|
||||
'use strict';
|
||||
const utils = require('./utils');
|
||||
const ripple = utils.common.core;
|
||||
const validate = utils.common.validate;
|
||||
|
||||
/**
|
||||
* These prefixes are inserted before the source material used to
|
||||
* generate various hashes. This is done to put each hash in its own
|
||||
* "space." This way, two different types of objects with the
|
||||
* same binary data will produce different hashes.
|
||||
*
|
||||
* Each prefix is a 4-byte value with the last byte set to zero
|
||||
* and the first three bytes formed from the ASCII equivalent of
|
||||
* some arbitrary string. For example "TXN".
|
||||
*/
|
||||
const HASH_TX_ID = 0x54584E00; // 'TXN'
|
||||
const HASH_TX_SIGN = 0x53545800; // 'STX'
|
||||
const HASH_TX_SIGN_TESTNET = 0x73747800; // 'stx'
|
||||
|
||||
function getKeyPair(address, secret) {
|
||||
return ripple.Seed.from_json(secret).get_key(address);
|
||||
}
|
||||
|
||||
function getPublicKeyHex(keypair) {
|
||||
return keypair.to_hex_pub();
|
||||
}
|
||||
|
||||
function serialize(txJSON) {
|
||||
return ripple.SerializedObject.from_json(txJSON);
|
||||
}
|
||||
|
||||
function hashSerialization(serialized, prefix) {
|
||||
return serialized.hash(prefix || HASH_TX_ID).to_hex();
|
||||
}
|
||||
|
||||
function hashJSON(txJSON, prefix) {
|
||||
return hashSerialization(serialize(txJSON), prefix);
|
||||
}
|
||||
|
||||
function signingHash(txJSON, isTestNet) {
|
||||
return hashJSON(txJSON, isTestNet ? HASH_TX_SIGN_TESTNET : HASH_TX_SIGN);
|
||||
}
|
||||
|
||||
function computeSignature(txJSON, keypair) {
|
||||
const signature = keypair.sign(signingHash(txJSON));
|
||||
return ripple.sjcl.codec.hex.fromBits(signature).toUpperCase();
|
||||
}
|
||||
|
||||
function sign(txJSON, secret) {
|
||||
validate.txJSON(txJSON);
|
||||
validate.addressAndSecret({address: txJSON.Account, secret: secret});
|
||||
|
||||
const keypair = getKeyPair(txJSON.Acccount, secret);
|
||||
if (txJSON.SigningPubKey === undefined) {
|
||||
txJSON.SigningPubKey = getPublicKeyHex(keypair);
|
||||
}
|
||||
txJSON.TxnSignature = computeSignature(txJSON, keypair);
|
||||
const serialized = serialize(txJSON);
|
||||
return {
|
||||
tx_blob: serialized.to_hex(),
|
||||
hash: hashSerialization(serialized, HASH_TX_ID)
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = sign;
|
||||
13
src/api/transaction/submit.js
Normal file
13
src/api/transaction/submit.js
Normal file
@@ -0,0 +1,13 @@
|
||||
'use strict';
|
||||
const utils = require('./utils');
|
||||
const ripple = utils.common.core;
|
||||
const validate = utils.common.validate;
|
||||
|
||||
function submit(tx_blob, callback) {
|
||||
validate.blob(tx_blob);
|
||||
const request = new ripple.Request(this.remote, 'submit');
|
||||
request.message.tx_blob = tx_blob;
|
||||
request.request(callback);
|
||||
}
|
||||
|
||||
module.exports = submit;
|
||||
51
src/api/transaction/trustline.js
Normal file
51
src/api/transaction/trustline.js
Normal file
@@ -0,0 +1,51 @@
|
||||
'use strict';
|
||||
const utils = require('./utils');
|
||||
const ripple = utils.common.core;
|
||||
const validate = utils.common.validate;
|
||||
|
||||
const TrustSetFlags = {
|
||||
SetAuth: {name: 'authorized', set: 'SetAuth'},
|
||||
ClearNoRipple: {name: 'account_allows_rippling', set: 'ClearNoRipple',
|
||||
unset: 'NoRipple'},
|
||||
SetFreeze: {name: 'account_trustline_frozen', set: 'SetFreeze',
|
||||
unset: 'ClearFreeze'}
|
||||
};
|
||||
|
||||
function createTrustLineTransaction(account, trustline) {
|
||||
validate.address(account);
|
||||
validate.trustline(trustline);
|
||||
|
||||
if (trustline && trustline.limit) {
|
||||
trustline.limit = String(trustline.limit);
|
||||
}
|
||||
|
||||
const transaction = new ripple.Transaction();
|
||||
const limit = [
|
||||
trustline.limit,
|
||||
trustline.currency,
|
||||
trustline.counterparty
|
||||
].join('/');
|
||||
|
||||
transaction.trustSet(account, limit);
|
||||
|
||||
if (typeof trustline.quality_in === 'number') {
|
||||
transaction.tx_json.QualityIn = trustline.quality_in;
|
||||
}
|
||||
if (typeof trustline.quality_out === 'number') {
|
||||
transaction.tx_json.QualityOut = trustline.quality_out;
|
||||
}
|
||||
|
||||
utils.setTransactionBitFlags(transaction, {
|
||||
input: trustline,
|
||||
flags: TrustSetFlags,
|
||||
clear_setting: ''
|
||||
});
|
||||
return transaction;
|
||||
}
|
||||
|
||||
function prepareTrustLine(account, trustline, instructions, callback) {
|
||||
const transaction = createTrustLineTransaction(account, trustline);
|
||||
utils.createTxJSON(transaction, this.remote, instructions, callback);
|
||||
}
|
||||
|
||||
module.exports = utils.wrapCatch(prepareTrustLine);
|
||||
104
src/api/transaction/utils.js
Normal file
104
src/api/transaction/utils.js
Normal file
@@ -0,0 +1,104 @@
|
||||
/* eslint-disable valid-jsdoc */
|
||||
'use strict';
|
||||
const BigNumber = require('bignumber.js');
|
||||
const common = require('../common');
|
||||
|
||||
/**
|
||||
* Helper that sets bit flags on transactions
|
||||
*
|
||||
* @param {Transaction} transaction - Transaction object that is used to submit
|
||||
* requests to ripple
|
||||
* @param {Object} options
|
||||
* @param {Object} options.flags - Holds flag names to set on transaction when
|
||||
* parameter values are true or false on input
|
||||
* @param {Object} options.input - Holds parameter values
|
||||
* @param {String} options.clear_setting - Used to check if parameter values
|
||||
* besides false mean false
|
||||
*
|
||||
*
|
||||
* @returns undefined
|
||||
*/
|
||||
function setTransactionBitFlags(transaction, options) {
|
||||
for (let flagName in options.flags) {
|
||||
const flag = options.flags[flagName];
|
||||
|
||||
// Set transaction flags
|
||||
if (!(flag.name in options.input)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let value = options.input[flag.name];
|
||||
|
||||
if (value === options.clear_setting) {
|
||||
value = false;
|
||||
}
|
||||
|
||||
if (flag.unset) {
|
||||
transaction.setFlags(value ? flag.set : flag.unset);
|
||||
} else if (flag.set && value) {
|
||||
transaction.setFlags(flag.set);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getFeeDrops(remote) {
|
||||
const feeUnits = 10; // all transactions currently have a fee of 10 fee units
|
||||
return remote.feeTx(feeUnits).to_text();
|
||||
}
|
||||
|
||||
function createTxJSON(transaction, remote, instructions, callback) {
|
||||
common.validate.options(instructions);
|
||||
|
||||
transaction.complete();
|
||||
const account = transaction.getAccount();
|
||||
const tx_json = transaction.tx_json;
|
||||
|
||||
if (instructions.last_ledger_sequence !== undefined) {
|
||||
tx_json.LastLedgerSequence =
|
||||
parseInt(instructions.last_ledger_sequence, 10);
|
||||
} else {
|
||||
const offset = instructions.last_ledger_offset !== undefined ?
|
||||
parseInt(instructions.last_ledger_offset, 10) : 3;
|
||||
tx_json.LastLedgerSequence = remote.getLedgerSequence() + offset;
|
||||
}
|
||||
|
||||
if (instructions.fixed_fee !== undefined) {
|
||||
tx_json.Fee = common.xrpToDrops(instructions.fixed_fee);
|
||||
} else {
|
||||
const serverFeeDrops = getFeeDrops(remote);
|
||||
if (instructions.max_fee !== undefined) {
|
||||
const maxFeeDrops = common.xrpToDrops(instructions.max_fee);
|
||||
tx_json.Fee = BigNumber.min(serverFeeDrops, maxFeeDrops).toString();
|
||||
} else {
|
||||
tx_json.Fee = serverFeeDrops;
|
||||
}
|
||||
}
|
||||
|
||||
if (instructions.sequence !== undefined) {
|
||||
tx_json.Sequence = parseInt(instructions.sequence, 10);
|
||||
callback(null, {tx_json: tx_json});
|
||||
} else {
|
||||
remote.findAccount(account).getNextSequence(function(error, sequence) {
|
||||
tx_json.Sequence = sequence;
|
||||
callback(null, {tx_json: tx_json});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function wrapCatch(asyncFunction) {
|
||||
return function() {
|
||||
const callback = arguments[arguments.length - 1];
|
||||
try {
|
||||
asyncFunction.apply(this, arguments);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
setTransactionBitFlags: setTransactionBitFlags,
|
||||
createTxJSON: createTxJSON,
|
||||
wrapCatch: wrapCatch,
|
||||
common: common
|
||||
};
|
||||
68
src/core/index.js
Normal file
68
src/core/index.js
Normal file
@@ -0,0 +1,68 @@
|
||||
'use strict';
|
||||
exports.Remote = require('./remote').Remote;
|
||||
exports.Request = require('./request').Request;
|
||||
exports.Amount = require('./amount').Amount;
|
||||
exports.Account = require('./account').Account;
|
||||
exports.Transaction = require('./transaction').Transaction;
|
||||
exports.Currency = require('./currency').Currency;
|
||||
exports.Base = require('./base').Base;
|
||||
exports.UInt128 = require('./uint128').UInt128;
|
||||
exports.UInt160 = require('./uint160').UInt160;
|
||||
exports.UInt256 = require('./uint256').UInt256;
|
||||
exports.Seed = require('./seed').Seed;
|
||||
exports.Meta = require('./meta').Meta;
|
||||
exports.SerializedObject = require('./serializedobject').SerializedObject;
|
||||
exports.RippleError = require('./rippleerror').RippleError;
|
||||
exports.Message = require('./message').Message;
|
||||
exports.binformat = require('./binformat');
|
||||
exports.utils = require('./utils');
|
||||
exports.Server = require('./server').Server;
|
||||
exports.Ledger = require('./ledger').Ledger;
|
||||
exports.TransactionQueue = require('./transactionqueue').TransactionQueue;
|
||||
exports.RangeSet = require('./rangeset').RangeSet;
|
||||
exports.convertBase = require('./baseconverter');
|
||||
|
||||
exports._test = {
|
||||
Log: require('./log'),
|
||||
PathFind: require('./pathfind').PathFind,
|
||||
TransactionManager: require('./transactionmanager').TransactionManager
|
||||
};
|
||||
|
||||
// Important: We do not guarantee any specific version of SJCL or for any
|
||||
// specific features to be included. The version and configuration may change at
|
||||
// any time without warning.
|
||||
//
|
||||
// However, for programs that are tied to a specific version of ripple.js like
|
||||
// the official client, it makes sense to expose the SJCL instance so we don't
|
||||
// have to include it twice.
|
||||
exports.sjcl = require('./utils').sjcl;
|
||||
exports.Wallet = require('ripple-wallet-generator')({sjcl: exports.sjcl});
|
||||
exports.types = require('./serializedtypes');
|
||||
|
||||
// camelCase to under_scored API conversion
|
||||
function attachUnderscored(name) {
|
||||
const o = exports[name];
|
||||
|
||||
Object.keys(o.prototype).forEach(function(key) {
|
||||
const UPPERCASE = /([A-Z]{1})[a-z]+/g;
|
||||
|
||||
if (!UPPERCASE.test(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const underscored = key.replace(UPPERCASE, function(c) {
|
||||
return '_' + c.toLowerCase();
|
||||
});
|
||||
|
||||
o.prototype[underscored] = o.prototype[key];
|
||||
});
|
||||
}
|
||||
|
||||
['Remote',
|
||||
'Request',
|
||||
'Transaction',
|
||||
'Account',
|
||||
'Server'
|
||||
].forEach(attachUnderscored);
|
||||
|
||||
// vim:sw=2:sts=2:ts=8:et
|
||||
70
src/index.js
70
src/index.js
@@ -1,68 +1,6 @@
|
||||
'use strict';
|
||||
exports.Remote = require('./remote').Remote;
|
||||
exports.Request = require('./request').Request;
|
||||
exports.Amount = require('./amount').Amount;
|
||||
exports.Account = require('./account').Account;
|
||||
exports.Transaction = require('./transaction').Transaction;
|
||||
exports.Currency = require('./currency').Currency;
|
||||
exports.Base = require('./base').Base;
|
||||
exports.UInt128 = require('./uint128').UInt128;
|
||||
exports.UInt160 = require('./uint160').UInt160;
|
||||
exports.UInt256 = require('./uint256').UInt256;
|
||||
exports.Seed = require('./seed').Seed;
|
||||
exports.Meta = require('./meta').Meta;
|
||||
exports.SerializedObject = require('./serializedobject').SerializedObject;
|
||||
exports.RippleError = require('./rippleerror').RippleError;
|
||||
exports.Message = require('./message').Message;
|
||||
exports.binformat = require('./binformat');
|
||||
exports.utils = require('./utils');
|
||||
exports.Server = require('./server').Server;
|
||||
exports.Ledger = require('./ledger').Ledger;
|
||||
exports.TransactionQueue = require('./transactionqueue').TransactionQueue;
|
||||
exports.RangeSet = require('./rangeset').RangeSet;
|
||||
exports.convertBase = require('./baseconverter');
|
||||
const _ = require('lodash');
|
||||
const core = require('./core');
|
||||
const RippleAPI = require('./api/api');
|
||||
|
||||
exports._test = {
|
||||
Log: require('./log'),
|
||||
PathFind: require('./pathfind').PathFind,
|
||||
TransactionManager: require('./transactionmanager').TransactionManager
|
||||
};
|
||||
|
||||
// Important: We do not guarantee any specific version of SJCL or for any
|
||||
// specific features to be included. The version and configuration may change at
|
||||
// any time without warning.
|
||||
//
|
||||
// However, for programs that are tied to a specific version of ripple.js like
|
||||
// the official client, it makes sense to expose the SJCL instance so we don't
|
||||
// have to include it twice.
|
||||
exports.sjcl = require('./utils').sjcl;
|
||||
exports.Wallet = require('ripple-wallet-generator')({sjcl: exports.sjcl});
|
||||
exports.types = require('./serializedtypes');
|
||||
|
||||
// camelCase to under_scored API conversion
|
||||
function attachUnderscored(name) {
|
||||
const o = exports[name];
|
||||
|
||||
Object.keys(o.prototype).forEach(function(key) {
|
||||
const UPPERCASE = /([A-Z]{1})[a-z]+/g;
|
||||
|
||||
if (!UPPERCASE.test(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const underscored = key.replace(UPPERCASE, function(c) {
|
||||
return '_' + c.toLowerCase();
|
||||
});
|
||||
|
||||
o.prototype[underscored] = o.prototype[key];
|
||||
});
|
||||
}
|
||||
|
||||
['Remote',
|
||||
'Request',
|
||||
'Transaction',
|
||||
'Account',
|
||||
'Server'
|
||||
].forEach(attachUnderscored);
|
||||
|
||||
// vim:sw=2:sts=2:ts=8:et
|
||||
module.exports = _.assign({}, core, {RippleAPI: RippleAPI});
|
||||
|
||||
36
test/api-test.js
Normal file
36
test/api-test.js
Normal file
@@ -0,0 +1,36 @@
|
||||
'use strict';
|
||||
const assert = require('assert');
|
||||
const setupAPI = require('./setup-api');
|
||||
const address = require('./fixtures/addresses').ACCOUNT;
|
||||
const paymentSpecification = require('./fixtures/payment-specification');
|
||||
const paymentResponse = require('./fixtures/payment-response');
|
||||
const balancesResponse = require('./fixtures/balances-response');
|
||||
|
||||
describe('RippleAPI', function() {
|
||||
beforeEach(setupAPI.setup);
|
||||
afterEach(setupAPI.teardown);
|
||||
|
||||
it('preparePayment', function(done) {
|
||||
const instructions = {lastLedgerOffset: 100};
|
||||
this.api.preparePayment(address, paymentSpecification, instructions,
|
||||
(error, response) => {
|
||||
if (error) {
|
||||
done(error);
|
||||
return;
|
||||
}
|
||||
assert.deepEqual(response, paymentResponse);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('getBalances', function(done) {
|
||||
this.api.getBalances(address, {}, (error, response) => {
|
||||
if (error) {
|
||||
done(error);
|
||||
return;
|
||||
}
|
||||
assert.deepEqual(response, balancesResponse);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
15
test/fixtures/addresses.js
vendored
15
test/fixtures/addresses.js
vendored
@@ -1,5 +1,10 @@
|
||||
module.exports.ACCOUNT = 'r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59';
|
||||
module.exports.OTHER_ACCOUNT = 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo';
|
||||
module.exports.THIRD_ACCOUNT = 'rwBYyfufTzk77zUSKEu4MvixfarC35av1J';
|
||||
module.exports.FOURTH_ACCOUNT = 'rJnZ4YHCUsHvQu7R6mZohevKJDHFzVD6Zr';
|
||||
module.exports.ISSUER = 'rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM';
|
||||
'use strict';
|
||||
module.exports = {
|
||||
ACCOUNT: 'r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59',
|
||||
OTHER_ACCOUNT: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo',
|
||||
THIRD_ACCOUNT: 'rwBYyfufTzk77zUSKEu4MvixfarC35av1J',
|
||||
FOURTH_ACCOUNT: 'rJnZ4YHCUsHvQu7R6mZohevKJDHFzVD6Zr',
|
||||
ISSUER: 'rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM',
|
||||
NOTFOUND: 'rajTAg3hon5Lcu1RxQQPxTgHvqfhc1EaUS',
|
||||
SECRET: 'shsWGZcmZz6YsWWmcnpfr6fLTdtFV'
|
||||
};
|
||||
|
||||
131
test/fixtures/balances-response.json
vendored
Normal file
131
test/fixtures/balances-response.json
vendored
Normal file
@@ -0,0 +1,131 @@
|
||||
{
|
||||
"limit": 200,
|
||||
"ledger": 8819951,
|
||||
"balances": [
|
||||
{
|
||||
"value": "922.913243",
|
||||
"currency": "XRP",
|
||||
"counterparty": ""
|
||||
},
|
||||
{
|
||||
"value": "0",
|
||||
"currency": "ASP",
|
||||
"counterparty": "r3vi7mWxru9rJCxETCyA1CHvzL96eZWx5z"
|
||||
},
|
||||
{
|
||||
"value": "0",
|
||||
"currency": "XAU",
|
||||
"counterparty": "r3vi7mWxru9rJCxETCyA1CHvzL96eZWx5z"
|
||||
},
|
||||
{
|
||||
"value": "2.497605752725159",
|
||||
"currency": "USD",
|
||||
"counterparty": "rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q"
|
||||
},
|
||||
{
|
||||
"value": "481.992867407479",
|
||||
"currency": "MXN",
|
||||
"counterparty": "rHpXfibHgSb64n8kK9QWDpdbfqSpYbM9a4"
|
||||
},
|
||||
{
|
||||
"value": "0.793598266778297",
|
||||
"currency": "EUR",
|
||||
"counterparty": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun"
|
||||
},
|
||||
{
|
||||
"value": "0",
|
||||
"currency": "CNY",
|
||||
"counterparty": "rnuF96W4SZoCJmbHYBFoJZpR8eCaxNvekK"
|
||||
},
|
||||
{
|
||||
"value": "1.294889190631542",
|
||||
"currency": "DYM",
|
||||
"counterparty": "rGwUWgN5BEg3QGNY3RX2HfYowjUTZdid3E"
|
||||
},
|
||||
{
|
||||
"value": "0.3488146605801446",
|
||||
"currency": "CHF",
|
||||
"counterparty": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"
|
||||
},
|
||||
{
|
||||
"value": "2.114103174931847",
|
||||
"currency": "BTC",
|
||||
"counterparty": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"
|
||||
},
|
||||
{
|
||||
"value": "0",
|
||||
"currency": "USD",
|
||||
"counterparty": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"
|
||||
},
|
||||
{
|
||||
"value": "-0.00111",
|
||||
"currency": "BTC",
|
||||
"counterparty": "rpgKWEmNqSDAGFhy5WDnsyPqfQxbWxKeVd"
|
||||
},
|
||||
{
|
||||
"value": "-0.1010780000080207",
|
||||
"currency": "BTC",
|
||||
"counterparty": "rBJ3YjwXi2MGbg7GVLuTXUWQ8DjL7tDXh4"
|
||||
},
|
||||
{
|
||||
"value": "1",
|
||||
"currency": "USD",
|
||||
"counterparty": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun"
|
||||
},
|
||||
{
|
||||
"value": "8.07619790068559",
|
||||
"currency": "CNY",
|
||||
"counterparty": "razqQKzJRdB4UxFPWf5NEpEG3WMkmwgcXA"
|
||||
},
|
||||
{
|
||||
"value": "7.292695098901099",
|
||||
"currency": "JPY",
|
||||
"counterparty": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"
|
||||
},
|
||||
{
|
||||
"value": "0",
|
||||
"currency": "AUX",
|
||||
"counterparty": "r3vi7mWxru9rJCxETCyA1CHvzL96eZWx5z"
|
||||
},
|
||||
{
|
||||
"value": "0",
|
||||
"currency": "USD",
|
||||
"counterparty": "r9vbV3EHvXWjSkeQ6CAcYVPGeq7TuiXY2X"
|
||||
},
|
||||
{
|
||||
"value": "12.41688780720394",
|
||||
"currency": "EUR",
|
||||
"counterparty": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"
|
||||
},
|
||||
{
|
||||
"value": "35",
|
||||
"currency": "USD",
|
||||
"counterparty": "rfF3PNkwkq1DygW2wum2HK3RGfgkJjdPVD"
|
||||
},
|
||||
{
|
||||
"value": "-5",
|
||||
"currency": "JOE",
|
||||
"counterparty": "rwUVoVMSURqNyvocPCcvLu3ygJzZyw8qwp"
|
||||
},
|
||||
{
|
||||
"value": "0",
|
||||
"currency": "USD",
|
||||
"counterparty": "rE6R3DWF9fBD7CyiQciePF9SqK58Ubp8o2"
|
||||
},
|
||||
{
|
||||
"value": "0",
|
||||
"currency": "JOE",
|
||||
"counterparty": "rE6R3DWF9fBD7CyiQciePF9SqK58Ubp8o2"
|
||||
},
|
||||
{
|
||||
"value": "0",
|
||||
"currency": "015841551A748AD2C1F76FF6ECB0CCCD00000000",
|
||||
"counterparty": "rs9M85karFkCRjvc6KMWn8Coigm9cbcgcx"
|
||||
},
|
||||
{
|
||||
"value": "0",
|
||||
"currency": "USD",
|
||||
"counterparty": "rEhDDUUNxpXgEHVJtC2cjXAgyx5VCFxdMF"
|
||||
}
|
||||
]
|
||||
}
|
||||
12
test/fixtures/hashes.js
vendored
Normal file
12
test/fixtures/hashes.js
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
VALID_TRANSACTION_HASH:
|
||||
'F4AB442A6D4CBB935D66E1DA7309A5FC71C7143ED4049053EC14E3875B0CF9BF',
|
||||
NOTFOUND_TRANSACTION_HASH:
|
||||
'D7FA4BBD23FAA88FC208BD194EC435D7A1FD9E2E8887B9C17A811A0739AA4AE4',
|
||||
INVALID_TRANSACTION_HASH:
|
||||
'XF4AB442A6D4CBB935D66E1DA7309A5FC71C7143ED4049053EC14E3875B0CF9BF',
|
||||
ORDER_HASH:
|
||||
'71AE74B03DE3B9A06C559AD4D173A362D96B7D2A5AA35F56B9EF21543D627F34'
|
||||
};
|
||||
699
test/fixtures/mock.js
vendored
Normal file
699
test/fixtures/mock.js
vendored
Normal file
@@ -0,0 +1,699 @@
|
||||
/* eslint-disable max-len */
|
||||
'use strict';
|
||||
const _ = require('lodash');
|
||||
const addresses = require('./addresses');
|
||||
const SerializedObject = require('ripple-lib').SerializedObject;
|
||||
const BASE_LEDGER_INDEX = 8819951;
|
||||
|
||||
module.exports.subscribeResponse = function(request) {
|
||||
return JSON.stringify({
|
||||
id: request.id,
|
||||
type: 'response',
|
||||
status: 'success',
|
||||
result: {
|
||||
fee_base: 10,
|
||||
fee_ref: 10,
|
||||
hostid: 'NAP',
|
||||
ledger_hash: '60EBABF55F6AB58864242CADA0B24FBEA027F2426917F39CA56576B335C0065A',
|
||||
ledger_index: BASE_LEDGER_INDEX,
|
||||
ledger_time: 463782770,
|
||||
load_base: 256,
|
||||
load_factor: 256,
|
||||
pubkey_node: 'n9Lt7DgQmxjHF5mYJsV2U9anALHmPem8PWQHWGpw4XMz79HA5aJY',
|
||||
random: 'EECFEE93BBB608914F190EC177B11DE52FC1D75D2C97DACBD26D2DFC6050E874',
|
||||
reserve_base: 20000000,
|
||||
reserve_inc: 5000000,
|
||||
server_status: 'full',
|
||||
validated_ledgers: '32570-' + BASE_LEDGER_INDEX.toString()
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.ledgerClose = function(offset) {
|
||||
const ledgerIndex = BASE_LEDGER_INDEX + (offset || 0);
|
||||
return JSON.stringify({
|
||||
type: 'ledgerClosed',
|
||||
fee_base: 10,
|
||||
fee_ref: 10,
|
||||
ledger_hash: 'BEAE5AA56874B7F1DE3AA19ED2B8CA61EBDAEC518E421F314B3EAE9AC12BDD02',
|
||||
ledger_index: ledgerIndex,
|
||||
ledger_time: 463782900,
|
||||
reserve_base: 20000000,
|
||||
reserve_inc: 5000000,
|
||||
txn_count: 5,
|
||||
validated_ledgers: '32570-' + ledgerIndex.toString()
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.accountInfoResponse = function(request) {
|
||||
return JSON.stringify({
|
||||
id: request.id,
|
||||
status: 'success',
|
||||
type: 'response',
|
||||
result: {
|
||||
account_data: {
|
||||
Account: request.account,
|
||||
Balance: '922913243',
|
||||
Domain: '6578616D706C652E636F6D',
|
||||
EmailHash: '23463B99B62A72F26ED677CC556C44E8',
|
||||
Flags: 655360,
|
||||
LedgerEntryType: 'AccountRoot',
|
||||
OwnerCount: 1,
|
||||
PreviousTxnID: '19899273706A9E040FDB5885EE991A1DC2BAD878A0D6E7DBCFB714E63BF737F7',
|
||||
PreviousTxnLgrSeq: 6614625,
|
||||
Sequence: 23,
|
||||
TransferRate: 1002000000,
|
||||
WalletLocator: '00000000000000000000000000000000000000000000000000000000DEADBEEF',
|
||||
index: '396400950EA27EB5710C0D5BE1D2B4689139F168AC5D07C13B8140EC3F82AE71',
|
||||
urlgravatar: 'http://www.gravatar.com/avatar/23463b99b62a72f26ed677cc556c44e8'
|
||||
},
|
||||
ledger_index: 9592219
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.accountNotFoundResponse = function(request) {
|
||||
return JSON.stringify({
|
||||
id: request.id,
|
||||
status: 'error',
|
||||
type: 'response',
|
||||
account: request.account,
|
||||
error: 'actNotFound',
|
||||
error_code: 15,
|
||||
error_message: 'Account not found.',
|
||||
ledger_current_index: 8861245,
|
||||
request: {
|
||||
account: request.account,
|
||||
command: 'account_info',
|
||||
id: request.id
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.accountLinesResponse = function(request, options={}) {
|
||||
_.defaults(options, {
|
||||
ledger: BASE_LEDGER_INDEX
|
||||
});
|
||||
|
||||
return JSON.stringify({
|
||||
id: request.id,
|
||||
status: 'success',
|
||||
type: 'response',
|
||||
result: {
|
||||
account: request.account,
|
||||
marker: options.marker,
|
||||
limit: request.limit,
|
||||
ledger_index: options.ledger,
|
||||
lines: [
|
||||
{
|
||||
account: 'r3vi7mWxru9rJCxETCyA1CHvzL96eZWx5z',
|
||||
balance: '0',
|
||||
currency: 'ASP',
|
||||
limit: '0',
|
||||
limit_peer: '10',
|
||||
quality_in: 0,
|
||||
quality_out: 0
|
||||
},
|
||||
{
|
||||
account: 'r3vi7mWxru9rJCxETCyA1CHvzL96eZWx5z',
|
||||
balance: '0',
|
||||
currency: 'XAU',
|
||||
limit: '0',
|
||||
limit_peer: '0',
|
||||
no_ripple: true,
|
||||
no_ripple_peer: true,
|
||||
quality_in: 0,
|
||||
quality_out: 0,
|
||||
freeze: true
|
||||
},
|
||||
{
|
||||
account: 'rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q',
|
||||
balance: '2.497605752725159',
|
||||
currency: 'USD',
|
||||
limit: '5',
|
||||
limit_peer: '0',
|
||||
no_ripple: true,
|
||||
quality_in: 0,
|
||||
quality_out: 0,
|
||||
freeze: true
|
||||
},
|
||||
{
|
||||
account: 'rHpXfibHgSb64n8kK9QWDpdbfqSpYbM9a4',
|
||||
balance: '481.992867407479',
|
||||
currency: 'MXN',
|
||||
limit: '1000',
|
||||
limit_peer: '0',
|
||||
quality_in: 0,
|
||||
quality_out: 0
|
||||
},
|
||||
{
|
||||
account: 'rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun',
|
||||
balance: '0.793598266778297',
|
||||
currency: 'EUR',
|
||||
limit: '1',
|
||||
limit_peer: '0',
|
||||
no_ripple: true,
|
||||
quality_in: 0,
|
||||
quality_out: 0
|
||||
},
|
||||
{
|
||||
account: 'rnuF96W4SZoCJmbHYBFoJZpR8eCaxNvekK',
|
||||
balance: '0',
|
||||
currency: 'CNY',
|
||||
limit: '3',
|
||||
limit_peer: '0',
|
||||
no_ripple: true,
|
||||
quality_in: 0,
|
||||
quality_out: 0
|
||||
},
|
||||
{
|
||||
account: 'rGwUWgN5BEg3QGNY3RX2HfYowjUTZdid3E',
|
||||
balance: '1.294889190631542',
|
||||
currency: 'DYM',
|
||||
limit: '3',
|
||||
limit_peer: '0',
|
||||
quality_in: 0,
|
||||
quality_out: 0
|
||||
},
|
||||
{
|
||||
account: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B',
|
||||
balance: '0.3488146605801446',
|
||||
currency: 'CHF',
|
||||
limit: '0',
|
||||
limit_peer: '0',
|
||||
quality_in: 0,
|
||||
quality_out: 0
|
||||
},
|
||||
{
|
||||
account: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B',
|
||||
balance: '2.114103174931847',
|
||||
currency: 'BTC',
|
||||
limit: '3',
|
||||
limit_peer: '0',
|
||||
quality_in: 0,
|
||||
quality_out: 0
|
||||
},
|
||||
{
|
||||
account: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B',
|
||||
balance: '0',
|
||||
currency: 'USD',
|
||||
limit: '5000',
|
||||
limit_peer: '0',
|
||||
quality_in: 0,
|
||||
quality_out: 0
|
||||
},
|
||||
{
|
||||
account: 'rpgKWEmNqSDAGFhy5WDnsyPqfQxbWxKeVd',
|
||||
balance: '-0.00111',
|
||||
currency: 'BTC',
|
||||
limit: '0',
|
||||
limit_peer: '10',
|
||||
quality_in: 0,
|
||||
quality_out: 0
|
||||
},
|
||||
{
|
||||
account: 'rBJ3YjwXi2MGbg7GVLuTXUWQ8DjL7tDXh4',
|
||||
balance: '-0.1010780000080207',
|
||||
currency: 'BTC',
|
||||
limit: '0',
|
||||
limit_peer: '10',
|
||||
quality_in: 0,
|
||||
quality_out: 0
|
||||
},
|
||||
{
|
||||
account: 'rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun',
|
||||
balance: '1',
|
||||
currency: 'USD',
|
||||
limit: '1',
|
||||
limit_peer: '0',
|
||||
quality_in: 0,
|
||||
quality_out: 0
|
||||
},
|
||||
{
|
||||
account: 'razqQKzJRdB4UxFPWf5NEpEG3WMkmwgcXA',
|
||||
balance: '8.07619790068559',
|
||||
currency: 'CNY',
|
||||
limit: '100',
|
||||
limit_peer: '0',
|
||||
no_ripple: true,
|
||||
quality_in: 0,
|
||||
quality_out: 0
|
||||
},
|
||||
{
|
||||
account: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B',
|
||||
balance: '7.292695098901099',
|
||||
currency: 'JPY',
|
||||
limit: '0',
|
||||
limit_peer: '0',
|
||||
no_ripple: true,
|
||||
quality_in: 0,
|
||||
quality_out: 0
|
||||
},
|
||||
{
|
||||
account: 'r3vi7mWxru9rJCxETCyA1CHvzL96eZWx5z',
|
||||
balance: '0',
|
||||
currency: 'AUX',
|
||||
limit: '0',
|
||||
limit_peer: '0',
|
||||
no_ripple: true,
|
||||
no_ripple_peer: true,
|
||||
quality_in: 0,
|
||||
quality_out: 0
|
||||
},
|
||||
{
|
||||
account: 'r9vbV3EHvXWjSkeQ6CAcYVPGeq7TuiXY2X',
|
||||
balance: '0',
|
||||
currency: 'USD',
|
||||
limit: '1',
|
||||
limit_peer: '0',
|
||||
no_ripple: true,
|
||||
quality_in: 0,
|
||||
quality_out: 0
|
||||
},
|
||||
{
|
||||
account: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B',
|
||||
balance: '12.41688780720394',
|
||||
currency: 'EUR',
|
||||
limit: '100',
|
||||
limit_peer: '0',
|
||||
no_ripple: true,
|
||||
quality_in: 0,
|
||||
quality_out: 0
|
||||
},
|
||||
{
|
||||
account: 'rfF3PNkwkq1DygW2wum2HK3RGfgkJjdPVD',
|
||||
balance: '35',
|
||||
currency: 'USD',
|
||||
limit: '500',
|
||||
limit_peer: '0',
|
||||
no_ripple: true,
|
||||
quality_in: 0,
|
||||
quality_out: 0
|
||||
},
|
||||
{
|
||||
account: 'rwUVoVMSURqNyvocPCcvLu3ygJzZyw8qwp',
|
||||
balance: '-5',
|
||||
currency: 'JOE',
|
||||
limit: '0',
|
||||
limit_peer: '50',
|
||||
no_ripple_peer: true,
|
||||
quality_in: 0,
|
||||
quality_out: 0
|
||||
},
|
||||
{
|
||||
account: 'rE6R3DWF9fBD7CyiQciePF9SqK58Ubp8o2',
|
||||
balance: '0',
|
||||
currency: 'USD',
|
||||
limit: '0',
|
||||
limit_peer: '100',
|
||||
no_ripple_peer: true,
|
||||
quality_in: 0,
|
||||
quality_out: 0
|
||||
},
|
||||
{
|
||||
account: 'rE6R3DWF9fBD7CyiQciePF9SqK58Ubp8o2',
|
||||
balance: '0',
|
||||
currency: 'JOE',
|
||||
limit: '0',
|
||||
limit_peer: '100',
|
||||
no_ripple_peer: true,
|
||||
quality_in: 0,
|
||||
quality_out: 0
|
||||
},
|
||||
{
|
||||
account: 'rs9M85karFkCRjvc6KMWn8Coigm9cbcgcx',
|
||||
balance: '0',
|
||||
currency: '015841551A748AD2C1F76FF6ECB0CCCD00000000',
|
||||
limit: '10.01037626125837',
|
||||
limit_peer: '0',
|
||||
no_ripple: true,
|
||||
quality_in: 0,
|
||||
quality_out: 0
|
||||
},
|
||||
{
|
||||
account: 'rEhDDUUNxpXgEHVJtC2cjXAgyx5VCFxdMF',
|
||||
balance: '0',
|
||||
currency: 'USD',
|
||||
limit: '0',
|
||||
limit_peer: '1',
|
||||
quality_in: 0,
|
||||
quality_out: 0,
|
||||
freeze: true
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.accountLinesCounterpartyResponse = function(request, options={}) {
|
||||
_.defaults(options, {
|
||||
ledger: BASE_LEDGER_INDEX
|
||||
});
|
||||
|
||||
return JSON.stringify({
|
||||
id: request.id,
|
||||
status: 'success',
|
||||
type: 'response',
|
||||
result: {
|
||||
account: request.account,
|
||||
marker: options.marker,
|
||||
limit: request.limit,
|
||||
ledger_index: options.ledger,
|
||||
lines: [
|
||||
{
|
||||
account: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B',
|
||||
balance: '0.3488146605801446',
|
||||
currency: 'CHF',
|
||||
limit: '0',
|
||||
limit_peer: '0',
|
||||
quality_in: 0,
|
||||
quality_out: 0
|
||||
},
|
||||
{
|
||||
account: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B',
|
||||
balance: '2.114103174931847',
|
||||
currency: 'BTC',
|
||||
limit: '3',
|
||||
limit_peer: '0',
|
||||
quality_in: 0,
|
||||
quality_out: 0
|
||||
},
|
||||
{
|
||||
account: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B',
|
||||
balance: '0',
|
||||
currency: 'USD',
|
||||
limit: '5000',
|
||||
limit_peer: '0',
|
||||
quality_in: 0,
|
||||
quality_out: 0
|
||||
},
|
||||
{
|
||||
account: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B',
|
||||
balance: '7.292695098901099',
|
||||
currency: 'JPY',
|
||||
limit: '0',
|
||||
limit_peer: '0',
|
||||
no_ripple: true,
|
||||
quality_in: 0,
|
||||
quality_out: 0
|
||||
},
|
||||
{
|
||||
account: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B',
|
||||
balance: '12.41688780720394',
|
||||
currency: 'EUR',
|
||||
limit: '100',
|
||||
limit_peer: '0',
|
||||
no_ripple: true,
|
||||
quality_in: 0,
|
||||
quality_out: 0
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.accountLinesNoCounterpartyResponse = function(request) {
|
||||
return JSON.stringify({
|
||||
id: request.id,
|
||||
status: 'success',
|
||||
type: 'response',
|
||||
result: {
|
||||
account: request.account,
|
||||
lines: []
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.ledgerResponse = function(request) {
|
||||
return JSON.stringify(
|
||||
{
|
||||
id: request.id,
|
||||
status: 'success',
|
||||
type: 'response',
|
||||
result: {
|
||||
ledger: {
|
||||
accepted: true,
|
||||
account_hash: 'EC028EC32896D537ECCA18D18BEBE6AE99709FEFF9EF72DBD3A7819E918D8B96',
|
||||
close_time: 464908910,
|
||||
close_time_human: '2014-Sep-24 21:21:50',
|
||||
close_time_resolution: 10,
|
||||
closed: true,
|
||||
hash: '0F7ED9F40742D8A513AE86029462B7A6768325583DF8EE21B7EC663019DD6A0F',
|
||||
ledger_hash: '0F7ED9F40742D8A513AE86029462B7A6768325583DF8EE21B7EC663019DD6A0F',
|
||||
ledger_index: '9038214',
|
||||
parent_hash: '4BB9CBE44C39DC67A1BE849C7467FE1A6D1F73949EA163C38A0121A15E04FFDE',
|
||||
seqNum: '9038214',
|
||||
totalCoins: '99999973964317514',
|
||||
total_coins: '99999973964317514',
|
||||
transaction_hash: 'ECB730839EB55B1B114D5D1AD2CD9A932C35BA9AB6D3A8C2F08935EAC2BAC239',
|
||||
transactions: [
|
||||
'1FC4D12C30CE206A6E23F46FAC62BD393BE9A79A1C452C6F3A04A13BC7A5E5A3',
|
||||
'E25C38FDB8DD4A2429649588638EE05D055EE6D839CABAF8ABFB4BD17CFE1F3E'
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const METADATA = module.exports.METADATA = {
|
||||
AffectedNodes: [
|
||||
{
|
||||
ModifiedNode: {
|
||||
FinalFields: {
|
||||
Account: 'r9tGqzZgKxVFvzKFdUqXAqTzazWBUia8Qr',
|
||||
BookDirectory: '4627DFFCFF8B5A265EDBD8AE8C14A52325DBFEDAF4F5C32E5E03E788E09BB000',
|
||||
BookNode: '0000000000000000',
|
||||
Flags: 0,
|
||||
OwnerNode: '0000000000000000',
|
||||
Sequence: 58,
|
||||
TakerGets: {
|
||||
currency: 'USD',
|
||||
issuer: addresses.OTHER_ACCOUNT,
|
||||
value: '5.648998'
|
||||
},
|
||||
TakerPays: '6208248802'
|
||||
},
|
||||
LedgerEntryType: 'Offer',
|
||||
LedgerIndex: '3CFB3C79D4F1BDB1EE5245259372576D926D9A875713422F7169A6CC60AFA68B',
|
||||
PreviousFields: {
|
||||
TakerGets: {
|
||||
currency: 'USD',
|
||||
issuer: addresses.OTHER_ACCOUNT,
|
||||
value: '5.65'
|
||||
},
|
||||
TakerPays: '6209350000'
|
||||
},
|
||||
PreviousTxnID: '8F571C346688D89AC1F737AE3B6BB5D976702B171CC7B4DE5CA3D444D5B8D6B4',
|
||||
PreviousTxnLgrSeq: 348433
|
||||
}
|
||||
},
|
||||
{
|
||||
ModifiedNode: {
|
||||
FinalFields: {
|
||||
Balance: {
|
||||
currency: 'USD',
|
||||
issuer: 'rrrrrrrrrrrrrrrrrrrrBZbvji',
|
||||
value: '-0.001'
|
||||
},
|
||||
Flags: 131072,
|
||||
HighLimit: {
|
||||
currency: 'USD',
|
||||
issuer: addresses.ISSUER,
|
||||
value: '1'
|
||||
},
|
||||
HighNode: '0000000000000000',
|
||||
LowLimit: {
|
||||
currency: 'USD',
|
||||
issuer: addresses.OTHER_ACCOUNT,
|
||||
value: '0'
|
||||
},
|
||||
LowNode: '0000000000000002'
|
||||
},
|
||||
LedgerEntryType: 'RippleState',
|
||||
LedgerIndex: '4BD1874F8F3A60EDB0C23F5BD43E07953C2B8741B226648310D113DE2B486F01',
|
||||
PreviousFields: {
|
||||
Balance: {
|
||||
currency: 'USD',
|
||||
issuer: 'rrrrrrrrrrrrrrrrrrrrBZbvji',
|
||||
value: '0'
|
||||
}
|
||||
},
|
||||
PreviousTxnID: '5B2006DAD0B3130F57ACF7CC5CCAC2EEBCD4B57AAA091A6FD0A24B073D08ABB8',
|
||||
PreviousTxnLgrSeq: 343703
|
||||
}
|
||||
},
|
||||
{
|
||||
ModifiedNode: {
|
||||
FinalFields: {
|
||||
Account: addresses.ACCOUNT,
|
||||
Balance: '9998898762',
|
||||
Flags: 0,
|
||||
OwnerCount: 3,
|
||||
Sequence: 5
|
||||
},
|
||||
LedgerEntryType: 'AccountRoot',
|
||||
LedgerIndex: '4F83A2CF7E70F77F79A307E6A472BFC2585B806A70833CCD1C26105BAE0D6E05',
|
||||
PreviousFields: {
|
||||
Balance: '9999999970',
|
||||
Sequence: 4
|
||||
},
|
||||
PreviousTxnID: '53354D84BAE8FDFC3F4DA879D984D24B929E7FEB9100D2AD9EFCD2E126BCCDC8',
|
||||
PreviousTxnLgrSeq: 343570
|
||||
}
|
||||
},
|
||||
{
|
||||
ModifiedNode: {
|
||||
FinalFields: {
|
||||
Account: 'r9tGqzZgKxVFvzKFdUqXAqTzazWBUia8Qr',
|
||||
Balance: '912695302618',
|
||||
Flags: 0,
|
||||
OwnerCount: 10,
|
||||
Sequence: 59
|
||||
},
|
||||
LedgerEntryType: 'AccountRoot',
|
||||
LedgerIndex: 'F3E119AAA87AF3607CF87F5523BB8278A83BCB4142833288305D767DD30C392A',
|
||||
PreviousFields: {
|
||||
Balance: '912694201420'
|
||||
},
|
||||
PreviousTxnID: '8F571C346688D89AC1F737AE3B6BB5D976702B171CC7B4DE5CA3D444D5B8D6B4',
|
||||
PreviousTxnLgrSeq: 348433
|
||||
}
|
||||
},
|
||||
{
|
||||
ModifiedNode: {
|
||||
FinalFields: {
|
||||
Balance: {
|
||||
currency: 'USD',
|
||||
issuer: 'rrrrrrrrrrrrrrrrrrrrBZbvji',
|
||||
value: '-5.5541638883365'
|
||||
},
|
||||
Flags: 131072,
|
||||
HighLimit: {
|
||||
currency: 'USD',
|
||||
issuer: 'r9tGqzZgKxVFvzKFdUqXAqTzazWBUia8Qr',
|
||||
value: '1000'
|
||||
},
|
||||
HighNode: '0000000000000000',
|
||||
LowLimit: {
|
||||
currency: 'USD',
|
||||
issuer: addresses.OTHER_ACCOUNT,
|
||||
value: '0'
|
||||
},
|
||||
LowNode: '000000000000000C'
|
||||
},
|
||||
LedgerEntryType: 'RippleState',
|
||||
LedgerIndex: 'FA1255C2E0407F1945BCF9351257C7C5C28B0F5F09BB81C08D35A03E9F0136BC',
|
||||
PreviousFields: {
|
||||
Balance: {
|
||||
currency: 'USD',
|
||||
issuer: 'rrrrrrrrrrrrrrrrrrrrBZbvji',
|
||||
value: '-5.5551658883365'
|
||||
}
|
||||
},
|
||||
PreviousTxnID: '8F571C346688D89AC1F737AE3B6BB5D976702B171CC7B4DE5CA3D444D5B8D6B4',
|
||||
PreviousTxnLgrSeq: 348433
|
||||
}
|
||||
}
|
||||
],
|
||||
TransactionIndex: 0,
|
||||
TransactionResult: 'tesSUCCESS'
|
||||
};
|
||||
|
||||
const BINARY_TRANSACTION = module.exports.BINARY_TRANSACTION = {
|
||||
Account: addresses.ACCOUNT,
|
||||
Amount: {
|
||||
currency: 'USD',
|
||||
issuer: addresses.ISSUER,
|
||||
value: '0.001'
|
||||
},
|
||||
Destination: addresses.ISSUER,
|
||||
Fee: '10',
|
||||
Flags: 0,
|
||||
Paths: [
|
||||
[
|
||||
{
|
||||
currency: 'USD',
|
||||
issuer: addresses.OTHER_ACCOUNT,
|
||||
type: 48,
|
||||
type_hex: '0000000000000030'
|
||||
},
|
||||
{
|
||||
account: addresses.OTHER_ACCOUNT,
|
||||
currency: 'USD',
|
||||
issuer: addresses.OTHER_ACCOUNT,
|
||||
type: 49,
|
||||
type_hex: '0000000000000031'
|
||||
}
|
||||
]
|
||||
],
|
||||
SendMax: '1112209',
|
||||
Sequence: 4,
|
||||
SigningPubKey: '02BC8C02199949B15C005B997E7C8594574E9B02BA2D0628902E0532989976CF9D',
|
||||
TransactionType: 'Payment',
|
||||
TxnSignature: '304502204EE3E9D1B01D8959B08450FCA9E22025AF503DEF310E34A93863A85CAB3C0BC5022100B61F5B567F77026E8DEED89EED0B7CAF0E6C96C228A2A65216F0DC2D04D52083'
|
||||
};
|
||||
|
||||
const BINARY_TRANSACTION_SYNTH = module.exports.BINARY_TRANSACTION_SYNTH = {
|
||||
date: 416447810,
|
||||
hash: 'F4AB442A6D4CBB935D66E1DA7309A5FC71C7143ED4049053EC14E3875B0CF9BF',
|
||||
inLedger: 348860,
|
||||
ledger_index: 348860
|
||||
};
|
||||
|
||||
module.exports.transactionResponse = function(request) {
|
||||
return JSON.stringify({
|
||||
id: request.id,
|
||||
status: 'success',
|
||||
type: 'response',
|
||||
result: _.extend({
|
||||
meta: SerializedObject.from_json(METADATA).to_hex(),
|
||||
tx: SerializedObject.from_json(BINARY_TRANSACTION).to_hex()
|
||||
}, BINARY_TRANSACTION_SYNTH)
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.transactionNotFoundResponse = function(request) {
|
||||
return JSON.stringify({
|
||||
id: request.id,
|
||||
status: 'error',
|
||||
type: 'response',
|
||||
error: 'txnNotFound',
|
||||
error_code: 24,
|
||||
error_message: 'Transaction not found.',
|
||||
request: {
|
||||
command: 'tx',
|
||||
id: request.id,
|
||||
transaction: 'E08D6E9754025CA2534A78707605E0601F03ACE063687A0CA1BDDACFCD1698C7'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.serverInfoResponse = function(request) {
|
||||
return JSON.stringify({
|
||||
id: request.id,
|
||||
status: 'success',
|
||||
type: 'response',
|
||||
result: {
|
||||
info: {
|
||||
build_version: '0.24.0-rc1',
|
||||
complete_ledgers: '32570-6595042',
|
||||
hostid: 'ARTS',
|
||||
last_close: {converge_time_s: 2.007, proposers: 4},
|
||||
load_factor: 1,
|
||||
peers: 53,
|
||||
pubkey_node: 'n94wWvFUmaKGYrKUGgpv1DyYgDeXRGdACkNQaSe7zJiy5Znio7UC',
|
||||
server_state: 'full',
|
||||
validated_ledger: {
|
||||
age: 5,
|
||||
base_fee_xrp: 0.00001,
|
||||
hash: '4482DEE5362332F54A4036ED57EE1767C9F33CF7CE5A6670355C16CECE381D46',
|
||||
reserve_base_xrp: 20,
|
||||
reserve_inc_xrp: 5,
|
||||
seq: 6595042
|
||||
},
|
||||
validation_quorum: 3
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
16
test/fixtures/payment-response.json
vendored
Normal file
16
test/fixtures/payment-response.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"tx_json": {
|
||||
"Flags": 0,
|
||||
"TransactionType": "Payment",
|
||||
"Account": "r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59",
|
||||
"Destination": "rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo",
|
||||
"Amount": {
|
||||
"value": "0.01",
|
||||
"currency": "USD",
|
||||
"issuer": "rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM"
|
||||
},
|
||||
"LastLedgerSequence": 8819955,
|
||||
"Fee": "12",
|
||||
"Sequence": 23
|
||||
}
|
||||
}
|
||||
9
test/fixtures/payment-specification.json
vendored
Normal file
9
test/fixtures/payment-specification.json
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"source_account": "r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59",
|
||||
"destination_account": "rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo",
|
||||
"destination_amount": {
|
||||
"value": "0.01",
|
||||
"currency": "USD",
|
||||
"issuer": "rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM"
|
||||
}
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
--reporter spec --timeout 10000 --slow 500 --compilers js:babel/register
|
||||
--reporter spec --timeout 5000 --slow 500 --compilers js:babel/register
|
||||
|
||||
111
test/mock-rippled.js
Normal file
111
test/mock-rippled.js
Normal file
@@ -0,0 +1,111 @@
|
||||
'use strict';
|
||||
const _ = require('lodash');
|
||||
const assert = require('assert');
|
||||
const WebSocketServer = require('ws').Server;
|
||||
const EventEmitter2 = require('eventemitter2').EventEmitter2;
|
||||
const fixtures = require('./fixtures/mock');
|
||||
const addresses = require('./fixtures/addresses');
|
||||
const hashes = require('./fixtures/hashes');
|
||||
|
||||
module.exports = function(port) {
|
||||
const mock = new WebSocketServer({port: port});
|
||||
_.assign(mock, EventEmitter2.prototype);
|
||||
|
||||
const close = mock.close;
|
||||
mock.close = function() {
|
||||
if (mock.expectedRequests !== undefined) {
|
||||
const allRequestsMade = _.every(mock.expectedRequests, function(counter) {
|
||||
return counter === 0;
|
||||
});
|
||||
if (!allRequestsMade) {
|
||||
const json = JSON.stringify(mock.expectedRequests, null, 2);
|
||||
const indent = ' ';
|
||||
const indented = indent + json.replace(/\n/g, '\n' + indent);
|
||||
assert(false, 'Not all expected requests were made:\n' + indented);
|
||||
}
|
||||
}
|
||||
close.call(mock);
|
||||
};
|
||||
|
||||
mock.expect = function(expectedRequests) {
|
||||
mock.expectedRequests = expectedRequests;
|
||||
};
|
||||
|
||||
mock.once('connection', function(conn) {
|
||||
conn.on('message', function(messageJSON) {
|
||||
const message = JSON.parse(messageJSON);
|
||||
mock.emit('request_' + message.command, message, conn);
|
||||
});
|
||||
});
|
||||
|
||||
mock.onAny(function() {
|
||||
if (this.event.indexOf('request_') !== 0) {
|
||||
return;
|
||||
}
|
||||
if (mock.listeners(this.event).length === 0) {
|
||||
throw new Error('No event handler registered for ' + this.event);
|
||||
}
|
||||
if (mock.expectedRequests === undefined) {
|
||||
return; // TODO: fail here to require expectedRequests
|
||||
}
|
||||
const expectedCount = mock.expectedRequests[this.event];
|
||||
if (expectedCount === undefined || expectedCount === 0) {
|
||||
throw new Error('Unexpected request: ' + this.event);
|
||||
}
|
||||
mock.expectedRequests[this.event] -= 1;
|
||||
});
|
||||
|
||||
mock.on('request_server_info', function(message, conn) {
|
||||
assert.strictEqual(message.command, 'server_info');
|
||||
conn.send(fixtures.serverInfoResponse(message));
|
||||
});
|
||||
|
||||
mock.on('request_subscribe', function(message, conn) {
|
||||
assert.strictEqual(message.command, 'subscribe');
|
||||
if (message.accounts) {
|
||||
assert.strictEqual(message.accounts[0], addresses.ACCOUNT);
|
||||
} else {
|
||||
assert.deepEqual(message.streams, ['ledger', 'server']);
|
||||
}
|
||||
conn.send(fixtures.subscribeResponse(message));
|
||||
});
|
||||
|
||||
mock.on('request_account_info', function(message, conn) {
|
||||
assert.strictEqual(message.command, 'account_info');
|
||||
if (message.account === addresses.ACCOUNT) {
|
||||
conn.send(fixtures.accountInfoResponse(message));
|
||||
} else if (message.account === addresses.NOTFOUND) {
|
||||
conn.send(fixtures.accountNotFoundResponse(message));
|
||||
} else {
|
||||
assert(false, 'Unrecognized account address: ' + message.account);
|
||||
}
|
||||
});
|
||||
|
||||
mock.on('request_ledger', function(message, conn) {
|
||||
assert.strictEqual(message.command, 'ledger');
|
||||
conn.send(fixtures.ledgerResponse(message));
|
||||
});
|
||||
|
||||
mock.on('request_tx', function(message, conn) {
|
||||
assert.strictEqual(message.command, 'tx');
|
||||
if (message.transaction === hashes.VALID_TRANSACTION_HASH) {
|
||||
conn.send(fixtures.transactionResponse(message));
|
||||
} else if (message.transaction === hashes.NOTFOUND_TRANSACTION_HASH) {
|
||||
conn.send(fixtures.transactionNotFoundResponse(message));
|
||||
} else {
|
||||
assert(false, 'Unrecognized transaction hash: ' + message.transaction);
|
||||
}
|
||||
});
|
||||
|
||||
mock.on('request_account_lines', function(message, conn) {
|
||||
if (message.account === addresses.ACCOUNT) {
|
||||
conn.send(fixtures.accountLinesResponse(message));
|
||||
} else if (message.account === addresses.OTHER_ACCOUNT) {
|
||||
conn.send(fixtures.accountLinesCounterpartyResponse(message));
|
||||
} else {
|
||||
assert(false, 'Unrecognized account address: ' + message.account);
|
||||
}
|
||||
});
|
||||
|
||||
return mock;
|
||||
};
|
||||
53
test/setup-api.js
Normal file
53
test/setup-api.js
Normal file
@@ -0,0 +1,53 @@
|
||||
'use strict';
|
||||
const net = require('net');
|
||||
const RippleAPI = require('../src').RippleAPI;
|
||||
const fixtures = require('./fixtures/mock');
|
||||
const createMockRippled = require('./mock-rippled');
|
||||
|
||||
// using a free port instead of a constant port enables parallelization
|
||||
function getFreePort(callback) {
|
||||
const server = net.createServer();
|
||||
let port;
|
||||
server.on('listening', function() {
|
||||
port = server.address().port;
|
||||
server.close();
|
||||
});
|
||||
server.on('close', function() {
|
||||
callback(null, port);
|
||||
});
|
||||
server.on('error', function(error) {
|
||||
callback(error);
|
||||
});
|
||||
server.listen(0);
|
||||
}
|
||||
|
||||
function setupMockRippledConnection(testcase, port, done) {
|
||||
testcase.mockRippled = createMockRippled(port);
|
||||
testcase.api = new RippleAPI({servers: ['ws://localhost:' + port]});
|
||||
testcase.api.connect(() => {
|
||||
testcase.api.remote.getServer().once('ledger_closed', () => done());
|
||||
testcase.api.remote.getServer().emit('message',
|
||||
JSON.parse(fixtures.ledgerClose(0)));
|
||||
});
|
||||
}
|
||||
|
||||
function setup(done) {
|
||||
getFreePort((error, port) => {
|
||||
if (error) {
|
||||
throw new Error('Unable to obtain a free port: ' + error);
|
||||
}
|
||||
setupMockRippledConnection(this, port, done);
|
||||
});
|
||||
}
|
||||
|
||||
function teardown(done) {
|
||||
this.api.remote.disconnect(() => {
|
||||
this.mockRippled.close();
|
||||
setImmediate(done);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
setup: setup,
|
||||
teardown: teardown
|
||||
};
|
||||
Reference in New Issue
Block a user