Update schemas, use schemas for validation, and switch to is-my-json-valid

This commit is contained in:
Chris Clark
2015-05-28 18:15:32 -07:00
parent 62b5953abe
commit 908e306f04
52 changed files with 586 additions and 1278 deletions

34
npm-shrinkwrap.json generated
View File

@@ -26,20 +26,34 @@
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/extend/-/extend-1.2.1.tgz" "resolved": "https://registry.npmjs.org/extend/-/extend-1.2.1.tgz"
}, },
"jayschema": { "is-my-json-valid": {
"version": "0.3.1", "version": "2.12.0",
"resolved": "https://registry.npmjs.org/jayschema/-/jayschema-0.3.1.tgz", "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.12.0.tgz",
"dependencies": { "dependencies": {
"when": { "generate-function": {
"version": "3.4.6", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/when/-/when-3.4.6.tgz" "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz"
},
"generate-object-property": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz",
"dependencies": {
"is-property": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz"
}
}
},
"jsonpointer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-1.1.0.tgz"
},
"xtend": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.0.tgz"
} }
} }
}, },
"jayschema-error-messages": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/jayschema-error-messages/-/jayschema-error-messages-1.0.3.tgz"
},
"lodash": { "lodash": {
"version": "3.9.3", "version": "3.9.3",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-3.9.3.tgz" "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.9.3.tgz"

View File

@@ -18,8 +18,7 @@
"babel-runtime": "^5.3.2", "babel-runtime": "^5.3.2",
"bignumber.js": "^2.0.3", "bignumber.js": "^2.0.3",
"extend": "~1.2.1", "extend": "~1.2.1",
"jayschema": "^0.3.1", "is-my-json-valid": "^2.12.0",
"jayschema-error-messages": "^1.0.3",
"lodash": "^3.1.0", "lodash": "^3.1.0",
"lru-cache": "~2.5.0", "lru-cache": "~2.5.0",
"ripple-lib-transactionparser": "^0.3.2", "ripple-lib-transactionparser": "^0.3.2",
@@ -30,8 +29,8 @@
}, },
"devDependencies": { "devDependencies": {
"assert-diff": "^1.0.1", "assert-diff": "^1.0.1",
"babel": "^5.3.3", "babel": "~5.4.7",
"babel-core": "^5.3.2", "babel-core": "~5.4.7",
"babel-loader": "^5.0.0", "babel-loader": "^5.0.0",
"coveralls": "~2.10.0", "coveralls": "~2.10.0",
"eslint": "^0.18.0", "eslint": "^0.18.0",

View File

@@ -10,19 +10,13 @@ function RippleError(message) {
RippleError.prototype = new Error(); RippleError.prototype = new Error();
RippleError.prototype.name = 'RippleError'; RippleError.prototype.name = 'RippleError';
/** function ValidationError(message) {
* Invalid Request Error
* Missing parameters or invalid parameters
*/
function InvalidRequestError(message) {
this.message = message; this.message = message;
} }
InvalidRequestError.prototype = new RippleError(); ValidationError.prototype = new RippleError();
InvalidRequestError.prototype.name = 'InvalidRequestError'; ValidationError.prototype.name = 'ValidationError';
InvalidRequestError.prototype.error = 'restINVALID_PARAMETER';
/** /**
* Network Error
* Timeout, disconnects and too busy * Timeout, disconnects and too busy
*/ */
function NetworkError(message) { function NetworkError(message) {
@@ -32,17 +26,14 @@ NetworkError.prototype = new RippleError();
NetworkError.prototype.name = 'NetworkError'; NetworkError.prototype.name = 'NetworkError';
/** /**
* Rippled NetworkError
* Failed transactions, no paths found, not enough balance, etc. * Failed transactions, no paths found, not enough balance, etc.
*/ */
function RippledNetworkError(message) { function RippledNetworkError(message) {
this.message = message !== undefined ? message : 'Cannot connect to rippled'; this.message = message !== undefined ? message : 'Cannot connect to rippled';
} }
RippledNetworkError.prototype = new NetworkError(); RippledNetworkError.prototype = new NetworkError();
RippledNetworkError.prototype.error = 'restRIPPLED_NETWORK_ERR';
/** /**
* Transaction Error
* Failed transactions, no paths found, not enough balance, etc. * Failed transactions, no paths found, not enough balance, etc.
*/ */
function TransactionError(message) { function TransactionError(message) {
@@ -52,7 +43,6 @@ TransactionError.prototype = new RippleError();
TransactionError.prototype.name = 'TransactionError'; TransactionError.prototype.name = 'TransactionError';
/** /**
* Not Found Error
* Asset could not be found * Asset could not be found
*/ */
function NotFoundError(message) { function NotFoundError(message) {
@@ -60,10 +50,8 @@ function NotFoundError(message) {
} }
NotFoundError.prototype = new RippleError(); NotFoundError.prototype = new RippleError();
NotFoundError.prototype.name = 'NotFoundError'; NotFoundError.prototype.name = 'NotFoundError';
NotFoundError.prototype.error = 'restNOT_FOUND';
/** /**
* Time Out Error
* Request timed out * Request timed out
*/ */
function TimeOutError(message) { function TimeOutError(message) {
@@ -73,7 +61,6 @@ TimeOutError.prototype = new RippleError();
TimeOutError.prototype.name = 'TimeOutError'; TimeOutError.prototype.name = 'TimeOutError';
/** /**
* API Error
* API logic failed to do what it intended * API logic failed to do what it intended
*/ */
function ApiError(message) { function ApiError(message) {
@@ -83,7 +70,7 @@ ApiError.prototype = new RippleError();
ApiError.prototype.name = 'ApiError'; ApiError.prototype.name = 'ApiError';
module.exports = { module.exports = {
InvalidRequestError: InvalidRequestError, ValidationError: ValidationError,
NetworkError: NetworkError, NetworkError: NetworkError,
TransactionError: TransactionError, TransactionError: TransactionError,
RippledNetworkError: RippledNetworkError, RippledNetworkError: RippledNetworkError,

View File

@@ -5,7 +5,6 @@ module.exports = {
core: require('./core'), core: require('./core'),
constants: require('./constants'), constants: require('./constants'),
errors: require('./errors'), errors: require('./errors'),
schemaValidator: require('./schema-validator'),
validate: require('./validate'), validate: require('./validate'),
server: require('./server'), server: require('./server'),
dropsToXrp: utils.dropsToXrp, dropsToXrp: utils.dropsToXrp,

View File

@@ -1,45 +1,59 @@
'use strict'; 'use strict';
const _ = require('lodash');
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const JaySchema = require('jayschema'); const validator = require('is-my-json-valid');
const formatJaySchemaErrors = require('jayschema-error-messages'); const ripple = require('./core');
const ValidationError = require('./errors').ValidationError;
const baseDir = path.join(__dirname, './schemas'); let SCHEMAS = {};
module.exports = (function() { function isValidAddress(address) {
const validator = new JaySchema(); return ripple.UInt160.is_valid(address);
const validate = validator.validate; }
// If schema is valid, return true. Otherwise function isValidLedgerHash(ledgerHash) {
// return array of validation errors return ripple.UInt256.is_valid(ledgerHash);
validator.validate = function() { }
const errors = validate.apply(validator, arguments);
return {
err: errors,
errors: formatJaySchemaErrors(errors),
isValid: errors.length === 0
};
};
validator.isValid = function() { function loadSchema(filepath) {
return validator.validate.apply(validator, arguments).isValid; try {
}; return JSON.parse(fs.readFileSync(filepath, 'utf8'));
} catch (e) {
throw new Error('Failed to parse schema: ' + filepath);
}
}
// Load Schemas function loadSchemas(dir) {
fs.readdirSync(baseDir).filter(function(fileName) { const filenames = fs.readdirSync(dir).filter(name => name.endsWith('.json'));
return /^[\w\s]+\.json$/.test(fileName); const schemas = filenames.map(name => loadSchema(path.join(dir, name)));
}) return _.indexBy(schemas, 'title');
.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; function formatSchemaError(error) {
})(); return error.field + ' ' + error.message
+ (error.value ? ' (' + error.value + ')' : '');
}
function formatSchemaErrors(errors) {
return errors.map(formatSchemaError).join(', ');
}
function schemaValidate(schemaName, object) {
const formats = {address: isValidAddress,
ledgerHash: isValidLedgerHash};
const options = {schema: SCHEMAS, formats: formats,
verbose: true, greedy: true};
const schema = SCHEMAS[schemaName];
if (schema === undefined) {
throw new Error('schema not found for: ' + schemaName);
}
const validate = validator(schema, options);
const isValid = validate(object);
if (!isValid) {
throw new ValidationError(formatSchemaErrors(validate.errors));
}
}
SCHEMAS = loadSchemas(path.join(__dirname, './schemas'));
module.exports = schemaValidate;

View File

@@ -1,71 +0,0 @@
{
"$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"]
}

View File

@@ -1,28 +0,0 @@
{
"$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"]
}

View File

@@ -1,23 +0,0 @@
{
"$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" ]
}

View File

@@ -1,7 +0,0 @@
{
"$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]+)?$"
}

View File

@@ -1,7 +0,0 @@
{
"$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}$"
}

View File

@@ -1,58 +0,0 @@
{
"$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": []
}

View File

@@ -1,86 +0,0 @@
{
"$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
}

View File

@@ -1,106 +0,0 @@
{
"$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"]
}

View File

@@ -1,58 +0,0 @@
{
"$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"]
}

View File

@@ -1,7 +0,0 @@
{
"$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_]#!:.?+=&%@!-\/]))?$"
}

View File

@@ -0,0 +1,8 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "address",
"description": "A Ripple account address",
"type": "string",
"format": "address",
"pattern": "^r[1-9A-HJ-NP-Za-km-z]{25,34}$"
}

View File

@@ -0,0 +1,15 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "adjustment",
"type": "object",
"properties": {
"address": {"$ref": "address"},
"amount": {"$ref": "amount"},
"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"
}
},
"required": ["address", "amount"],
"additionalProperties": false
}

View File

@@ -0,0 +1,9 @@
{
"$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",
"allOf": [
{"$ref": "amountbase"},
{"required": ["value"]}
]
}

View File

@@ -0,0 +1,44 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "amountbase",
"description": "Base class for amount and issue",
"type": "object",
"properties": {
"value": {
"description": "The quantity of the currency, denoted as a string to retain floating point precision",
"$ref": "value"
},
"currency": {
"description": "The three-character code or hex string used to denote currencies",
"$ref": "currency"
},
"counterparty": {
"description": "The Ripple account address of the currency's issuer or gateway",
"$ref": "address"
}
},
"additionalProperties": false,
"required": ["currency"],
"oneOf": [
{
"properties": {
"currency": {
"not": {
"enum": ["XRP"]
}
}
},
"required": ["counterparty"]
},
{
"properties": {
"currency": {
"enum": ["XRP"]
}
},
"not": {
"required": ["counterparty"]
}
}
]
}

View File

@@ -0,0 +1,8 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "blob",
"description": "An uppercase hex string representation of a transaction",
"type": "string",
"minLength": "1",
"pattern": "^[0-9A-F]*$"
}

View File

@@ -0,0 +1,14 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "cancellation",
"type": "object",
"properties": {
"address": {"$ref": "address"},
"orderSequence": {
"type": "integer",
"minimum": 0
}
},
"required": ["address", "orderSequence"],
"additionalProperties": false
}

View File

@@ -1,7 +1,7 @@
{ {
"$schema": "http://json-schema.org/draft-04/schema#", "$schema": "http://json-schema.org/draft-04/schema#",
"title": "Currency", "title": "currency",
"description": "The three-character code or hex string used to denote currencies", "description": "The three-character code or hex string used to denote currencies",
"type": "string", "type": "string",
"pattern": "^([a-zA-Z0-9]{3}|[A-Fa-f0-9]{40})$" "pattern": "^([a-zA-Z0-9<>(){}[\\]|?!@#$%^&*]{3}|[A-Fa-f0-9]{40})$"
} }

View File

@@ -1,6 +1,6 @@
{ {
"$schema": "http://json-schema.org/draft-04/schema#", "$schema": "http://json-schema.org/draft-04/schema#",
"title": "Hash256", "title": "hash256",
"description": "The hex representation of a 256-bit hash", "description": "The hex representation of a 256-bit hash",
"type": "string", "type": "string",
"pattern": "^[A-Fa-f0-9]{64}$" "pattern": "^[A-Fa-f0-9]{64}$"

View File

@@ -0,0 +1,43 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "instructions",
"description": "Instructions for executing a transaction",
"type": "object",
"properties": {
"sequence": {
"description": "The sequence number, relative to the initiating account, of this transaction.",
"type": "integer",
"minimum": 1
},
"fee": {
"description": "Fixed Fee",
"$ref": "value"
},
"maxFee": {
"description": "Max Fee",
"$ref": "value"
},
"maxLedgerVersion": {
"description": "Highest ledger version number that a transaction can appear in.",
"$ref": "ledgerVersion"
},
"maxLedgerVersionOffset": {
"description": "Offset from current legder version to highest ledger version that a transaction can appear in.",
"type": "integer",
"minimum": 0
}
},
"additionalProperties": false,
"not": {
"anyOf": [
{
"description": "fee and maxFee are mutually exclusive",
"required": ["fee", "maxFee"]
},
{
"description": "maxLedgerVersion and maxLedgerVersionOffset are mutually exclusive",
"required": ["maxLedgerVersion", "maxLedgerVersionOffset"]
}
]
}
}

View File

@@ -0,0 +1,9 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "issue",
"description": "A currency-counterparty pair, or just currency if it's XRP",
"allOf": [
{"$ref": "amountbase"},
{"not": {"required": ["value"]}}
]
}

View File

@@ -0,0 +1,7 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "ledgerVersion",
"description": "A ledger version number",
"type": "integer",
"minimum": 0
}

View File

@@ -0,0 +1,22 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "memo",
"description": "Memo objects represent arbitrary data that can be included in a transaction",
"type": "object",
"properties": {
"type": {
"pattern": "^[A-Za-z0-9\\-._~:/?#[\\]@!$&'()*+,;=%]*$"
},
"format": {
"pattern": "^[A-Za-z0-9\\-._~:/?#[\\]@!$&'()*+,;=%]*$"
},
"data": {
"type": "string"
}
},
"additionalProperties": false,
"anyOf": [
{"required": ["data"]},
{"required": ["type"]}
]
}

View File

@@ -0,0 +1,34 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "options",
"description": "Options for a ledger request",
"type": "object",
"properties": {
"currency": {"$ref": "currency"},
"counterparty": {"$ref": "address"},
"limit": {
"type": "integer",
"minimum": 1
},
"ledgerVersion": {
"anyOf": [
{"enum": ["current", "closed", "validated"]},
{"$ref": "ledgerVersion"},
{"type": "string", "format": "ledgerHash"}
]
},
"minLedgerVersion": {"$ref": "ledgerVersion"},
"maxLedgerVersion": {"$ref": "ledgerVersion"},
"marker": {
"type": "string"
}
},
"additionalProperties": false,
"dependencies": {
"marker": ["ledgerVersion"]
},
"not": {
"description": "Fixed fee and max fee are mutually exclusive",
"required": ["fee", "maxFee"]
}
}

View File

@@ -0,0 +1,26 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "order",
"type": "object",
"properties": {
"address": {"$ref": "address"},
"direction": {
"type": "string",
"enum": ["buy", "sell"]
},
"quantity": {"$ref": "amount"},
"totalPrice": {"$ref": "amount"},
"immediateOrCancel": {"type": "boolean"},
"fillOrKill": {"type": "boolean"},
"passive": {
"description": "If enabled, the offer will not consume offers that exactly match it, and instead becomes an Offer node in the ledger. It will still consume offers that cross it.",
"type": "boolean"
}
},
"required": ["address", "direction", "quantity", "totalPrice"],
"additionalProperties": false,
"not": {
"description": "immediateOrCancel and fillOrKill are mutually exclusive",
"required": ["immediateOrCancel", "fillOrKill"]
}
}

View File

@@ -0,0 +1,11 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "orderbook",
"type": "object",
"properties": {
"base": {"$ref": "issue"},
"counter": {"$ref": "issue"}
},
"required": ["base", "counter"],
"additionalProperties": false
}

View File

@@ -0,0 +1,32 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "pathfind",
"type": "object",
"properties": {
"source": {
"type": "object",
"properties": {
"address": {"$ref": "address"},
"amounts": {
"type": "array",
"items": {
"type": "object",
"properties": {
"currency": {"$ref": "currency"},
"counterparty": {"$ref": "address"},
"value": {"$ref": "value"}
},
"required": ["currency"],
"additionalProperties": false
},
"uniqueItems": true
}
},
"additionalProperties": false,
"required": ["address"]
},
"destination": {"$ref": "adjustment"}
},
"required": ["source", "destination"],
"additionalProperties": false
}

View File

@@ -0,0 +1,33 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "payment",
"type": "object",
"properties": {
"source": {"$ref": "adjustment"},
"destination": {"$ref": "adjustment"},
"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": "value"
},
"memos": {
"type": "array",
"items": {
"$ref": "memo"
}
},
"invoiceID": {
"description": "A 256-bit hash that can be used to identify a particular payment",
"$ref": "hash256"
},
"allowPartialPayment": {
"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"
},
"noDirectRipple": {
"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"
}
},
"required": ["source", "destination"],
"additionalProperties": false
}

View File

@@ -0,0 +1,8 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "quality",
"description": "Ratio for incoming/outgoing transit fees represented in billionths. (For example, a value of 500 million represents a 0.5:1 ratio.) As a special case, 0 is treated as a 1:1 ratio.",
"type": "integer",
"minimum": 0,
"maximum": 1000000000
}

View File

@@ -0,0 +1,12 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "settings",
"type": "object",
"properties": {
"address": {"$ref": "address"},
"name": {"type": "string"},
"value": {"type": "string"}
},
"required": ["address", "name", "value"],
"additionalProperties": false
}

View File

@@ -1,7 +1,7 @@
{ {
"$schema": "http://json-schema.org/draft-04/schema#", "$schema": "http://json-schema.org/draft-04/schema#",
"title": "Timestamp", "title": "timestamp",
"description": "An ISO 8601 combined date and time timestamp", "description": "An ISO 8601 combined date and time timestamp",
"type": "string", "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])$" "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])$"
} }

View File

@@ -0,0 +1,17 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "trustline",
"type": "object",
"properties": {
"address": {"$ref": "address"},
"currency": {"$ref": "currency"},
"counterparty": {"$ref": "address"},
"limit": {"$ref": "value"},
"qualityIn": {"$ref": "quality"},
"qualityOut": {"$ref": "quality"},
"allowRippling": {"type": "boolean"},
"frozen": {"type": "boolean"}
},
"required": ["address", "currency", "counterparty", "limit"],
"additionalProperties": false
}

View File

@@ -0,0 +1,9 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "tx",
"description": "An object in rippled txJSON format",
"type": "object",
"properties": {
"Account": {"$ref": "address"}
}
}

View File

@@ -1,6 +1,6 @@
{ {
"$schema": "http://json-schema.org/draft-04/schema#", "$schema": "http://json-schema.org/draft-04/schema#",
"title": "UINT32", "title": "uint32",
"description": "A string representation of an unsigned 32-bit integer (0-4294967295)", "description": "A string representation of an unsigned 32-bit integer (0-4294967295)",
"type": "string", "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])$" "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])$"

View File

@@ -0,0 +1,7 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "value",
"description": "A string representation of a non-negative floating point number",
"type": "string",
"pattern": "^[0-9]*[.]?[0-9]+([eE][-+]?[0-9]+)?$"
}

View File

@@ -1,583 +1,56 @@
'use strict'; 'use strict';
const _ = require('lodash'); const _ = require('lodash');
const InvalidRequestError = require('./errors.js').InvalidRequestError; const ValidationError = require('./errors').ValidationError;
const validator = require('./schema-validator'); const schemaValidate = require('./schema-validator');
const ripple = require('./core'); const ripple = require('./core');
const utils = require('./utils');
function error(text) { function error(text) {
return new InvalidRequestError(text); return new ValidationError(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) { function validateAddressAndSecret(obj) {
const address = obj.address; const address = obj.address;
const secret = obj.secret; const secret = obj.secret;
validateAddress(address); schemaValidate('address', address);
if (!secret) { if (!secret) {
throw missing('secret'); throw error('Parameter missing: secret');
} }
try { try {
if (!ripple.Seed.from_json(secret).get_key(address)) { if (!ripple.Seed.from_json(secret).get_key(address)) {
throw error('Invalid secret', secret); throw error('secret does not match address');
} }
} catch (exception) { } catch (exception) {
throw error('Invalid secret', secret); throw error('secret does not match address');
}
}
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) { function validateLedgerRange(options) {
if (_.isUndefined(options.min_ledger) || _.isUndefined(options.max_ledger)) { if (_.isUndefined(options.minLedger) && _.isUndefined(options.maxLedger)) {
return; if (Number(options.minLedger) > Number(options.maxLedger)) {
} throw error('minLedger must not be greater than maxLedger');
}
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) { function validateOptions(options) {
if (options.max_fee !== undefined) { schemaValidate('options', options);
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); validateLedgerRange(options);
validatePaging(options);
} }
function createValidators(validatorMap) { module.exports = {
const result = {}; address: _.partial(schemaValidate, 'address'),
_.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, addressAndSecret: validateAddressAndSecret,
addressAndMaybeSecret: validateAddressAndMaybeSecret, currency: _.partial(schemaValidate, 'currency'),
currency: validateCurrency, identifier: _.partial(schemaValidate, 'hash256'),
counterparty: validateCounterparty, sequence: _.partial(schemaValidate, 'natural'),
issue: validateIssue, order: _.partial(schemaValidate, 'order'),
identifier: validateIdentifier, orderbook: _.partial(schemaValidate, 'orderbook'),
sequence: validateSequence, payment: _.partial(schemaValidate, 'payment'),
order: validateOrder, pathfind: _.partial(schemaValidate, 'pathfind'),
orderbook: validateOrderbook, settings: _.partial(schemaValidate, 'settings'),
last_ledger_sequence: validateLastLedgerSequence, trustline: _.partial(schemaValidate, 'trustline'),
payment: validatePayment, txJSON: _.partial(schemaValidate, 'tx'),
pathfind: validatePathFind, blob: _.partial(schemaValidate, 'blob'),
settings: validateSettings, options: validateOptions,
trustline: validateTrustline, instructions: _.partial(schemaValidate, 'instructions')
txJSON: validateTxJSON, };
blob: validateBlob,
options: validateOptions
});

View File

@@ -2,7 +2,6 @@
/* eslint-disable valid-jsdoc */ /* eslint-disable valid-jsdoc */
'use strict'; 'use strict';
const utils = require('./utils'); const utils = require('./utils');
const validator = utils.common.schemaValidator;
const validate = utils.common.validate; const validate = utils.common.validate;
const DefaultPageLimit = 200; const DefaultPageLimit = 200;
@@ -30,8 +29,6 @@ const DefaultPageLimit = 200;
*/ */
function getBalances(account, options, callback) { function getBalances(account, options, callback) {
validate.address(account); validate.address(account);
validate.currency(options.currency, true);
validate.counterparty(options.counterparty, true);
validate.options(options); validate.options(options);
const self = this; const self = this;
@@ -66,7 +63,7 @@ function getBalances(account, options, callback) {
} }
function getLineBalances(prevResult) { function getLineBalances(prevResult) {
const isAggregate = options.limit === 'all'; const isAggregate = options.limit === undefined;
if (prevResult && (!isAggregate || !prevResult.marker)) { if (prevResult && (!isAggregate || !prevResult.marker)) {
return Promise.resolve(prevResult); return Promise.resolve(prevResult);
} }
@@ -83,8 +80,7 @@ function getBalances(account, options, callback) {
ledger = prevResult.ledger_index; ledger = prevResult.ledger_index;
} else { } else {
marker = options.marker; marker = options.marker;
limit = validator.isValid(options.limit, 'UINT32') limit = options.limit || DefaultPageLimit;
? Number(options.limit) : DefaultPageLimit;
ledger = utils.parseLedger(options.ledger); ledger = utils.parseLedger(options.ledger);
} }

View File

@@ -212,7 +212,7 @@ function getNotificationHelper(api, account, identifier, urlBase, topCallback) {
*/ */
function getNotification(account, identifier, urlBase, callback) { function getNotification(account, identifier, urlBase, callback) {
validate.address(account); validate.address(account);
validate.paymentIdentifier(identifier); validate.identifier(identifier);
return getNotificationHelper(this, account, identifier, urlBase, callback); return getNotificationHelper(this, account, identifier, urlBase, callback);
} }

View File

@@ -9,7 +9,6 @@ const utils = require('./utils');
const ripple = utils.common.core; const ripple = utils.common.core;
const errors = utils.common.errors; const errors = utils.common.errors;
const validate = utils.common.validate; const validate = utils.common.validate;
const validator = utils.common.schemaValidator;
const DefaultPageLimit = 200; const DefaultPageLimit = 200;
@@ -37,7 +36,7 @@ function getOrders(account, options, callback) {
validate.options(options); validate.options(options);
function getAccountOrders(prevResult) { function getAccountOrders(prevResult) {
const isAggregate = options.limit === 'all'; const isAggregate = options.limit === undefined;
if (prevResult && (!isAggregate || !prevResult.marker)) { if (prevResult && (!isAggregate || !prevResult.marker)) {
return Promise.resolve(prevResult); return Promise.resolve(prevResult);
} }
@@ -54,8 +53,7 @@ function getOrders(account, options, callback) {
ledger = prevResult.ledger_index; ledger = prevResult.ledger_index;
} else { } else {
marker = options.marker; marker = options.marker;
limit = validator.isValid(options.limit, 'UINT32') ? limit = options.limit || DefaultPageLimit;
Number(options.limit) : DefaultPageLimit;
ledger = utils.parseLedger(options.ledger); ledger = utils.parseLedger(options.ledger);
} }
@@ -141,19 +139,17 @@ function getOrders(account, options, callback) {
* *
* @param {Express.js Request} request * @param {Express.js Request} request
*/ */
function getOrderBook(account, base, counter, options, callback) { function getOrderBook(account, orderbook, options, callback) {
const self = this; 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.address(account);
validate.orderbook(params); validate.orderbook(orderbook);
validate.options(options); validate.options(options);
const params = _.assign({}, orderbook, options, {
validated: true,
order_book: orderbook.base + '/' + orderbook.counter
});
function getLastValidatedLedger(parameters) { function getLastValidatedLedger(parameters) {
const promise = new Promise(function(resolve, reject) { const promise = new Promise(function(resolve, reject) {
const ledgerRequest = self.remote.requestLedger('validated'); const ledgerRequest = self.remote.requestLedger('validated');

View File

@@ -8,11 +8,9 @@ const bignum = require('bignumber.js');
const transactions = require('./transactions'); const transactions = require('./transactions');
const TxToRestConverter = require('./tx-to-rest-converter.js'); const TxToRestConverter = require('./tx-to-rest-converter.js');
const utils = require('./utils'); const utils = require('./utils');
const ripple = utils.common.core;
const validator = utils.common.schemaValidator;
const validate = utils.common.validate; const validate = utils.common.validate;
const InvalidRequestError = utils.common.errors.InvalidRequestError; const ValidationError = utils.common.errors.ValidationError;
const NotFoundError = utils.common.errors.NotFoundError; const NotFoundError = utils.common.errors.NotFoundError;
const TimeOutError = utils.common.errors.TimeOutError; const TimeOutError = utils.common.errors.TimeOutError;
@@ -31,7 +29,7 @@ const DEFAULT_RESULTS_PER_PAGE = 10;
*/ */
function formatPaymentHelper(account, txJSON) { function formatPaymentHelper(account, txJSON) {
if (!(txJSON && /^payment$/i.test(txJSON.TransactionType))) { if (!(txJSON && /^payment$/i.test(txJSON.TransactionType))) {
throw new InvalidRequestError('Not a payment. The transaction ' throw new ValidationError('Not a payment. The transaction '
+ 'corresponding to the given identifier is not a payment.'); + 'corresponding to the given identifier is not a payment.');
} }
const metadata = { const metadata = {
@@ -59,7 +57,7 @@ function getPayment(account, identifier, callback) {
const self = this; const self = this;
validate.address(account); validate.address(account);
validate.paymentIdentifier(identifier); validate.identifier(identifier);
function getTransaction(_callback) { function getTransaction(_callback) {
transactions.getTransaction(self, account, identifier, {}, _callback); transactions.getTransaction(self, account, identifier, {}, _callback);
@@ -147,61 +145,15 @@ function getAccountPayments(account, source_account, destination_account,
* @param {RippleAddress} req.params.destination_account * @param {RippleAddress} req.params.destination_account
* @param {Amount "1+USD+r..."} req.params.destination_amount_string * @param {Amount "1+USD+r..."} req.params.destination_amount_string
*/ */
function getPathFind(source_account, destination_account, function getPathFind(pathfind, callback) {
destination_amount_string, source_currency_strings, callback) {
const self = this; const self = this;
validate.pathfind(pathfind);
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() { function prepareOptions() {
const pathfindParams = { const pathfindParams = {
src_account: source_account, src_account: pathfind.source.address,
dst_account: destination_account, dst_account: pathfind.destination.address,
dst_amount: utils.common.convertAmount(destination_amount) dst_amount: utils.common.convertAmount(pathfind.destination.amount)
}; };
if (typeof pathfindParams.dst_amount === 'object' if (typeof pathfindParams.dst_amount === 'object'
&& !pathfindParams.dst_amount.issuer) { && !pathfindParams.dst_amount.issuer) {
@@ -210,11 +162,10 @@ function getPathFind(source_account, destination_account,
// https://ripple.com/build/transactions/ // https://ripple.com/build/transactions/
// #special-issuer-values-for-sendmax-and-amount // #special-issuer-values-for-sendmax-and-amount
// https://ripple.com/build/ripple-rest/#counterparties-in-payments // https://ripple.com/build/ripple-rest/#counterparties-in-payments
pathfindParams.dst_amount.issuer = pathfindParams.dst_account; pathfindParams.dst_amount.issuer = pathfindParams.dst_account;
} }
if (source_currencies.length > 0) { if (pathfind.source.amounts && pathfind.source.amounts.length > 0) {
pathfindParams.src_currencies = source_currencies; pathfindParams.src_currencies = pathfind.source.amounts;
} }
return pathfindParams; return pathfindParams;
} }
@@ -279,10 +230,10 @@ function getPathFind(source_account, destination_account,
return TxToRestConverter.parsePaymentsFromPathFind(pathfindResults); return TxToRestConverter.parsePaymentsFromPathFind(pathfindResults);
} }
if (pathfindResults.destination_currencies.indexOf( if (pathfindResults.destination_currencies.indexOf(
destination_amount.currency) === -1) { pathfind.destination.amount.currency) === -1) {
throw new NotFoundError('No paths found. ' + throw new NotFoundError('No paths found. ' +
'The destination_account does not accept ' + 'The destination_account does not accept ' +
destination_amount.currency + pathfind.destination.amount.currency +
', they only accept: ' + ', they only accept: ' +
pathfindResults.destination_currencies.join(', ')); pathfindResults.destination_currencies.join(', '));
} else if (pathfindResults.source_currencies } else if (pathfindResults.source_currencies

View File

@@ -31,7 +31,7 @@ const DEFAULT_RESULTS_PER_PAGE = 10;
function getTransaction(api, account, identifier, requestOptions, callback) { function getTransaction(api, account, identifier, requestOptions, callback) {
try { try {
assert.strictEqual(typeof requestOptions, 'object'); assert.strictEqual(typeof requestOptions, 'object');
validate.address(account, true); validate.address(account);
validate.identifier(identifier); validate.identifier(identifier);
validate.options(requestOptions); validate.options(requestOptions);
} catch(err) { } catch(err) {

View File

@@ -2,7 +2,6 @@
/* eslint-disable valid-jsdoc */ /* eslint-disable valid-jsdoc */
'use strict'; 'use strict';
const utils = require('./utils'); const utils = require('./utils');
const validator = utils.common.schemaValidator;
const validate = utils.common.validate; const validate = utils.common.validate;
const DefaultPageLimit = 200; const DefaultPageLimit = 200;
@@ -29,8 +28,6 @@ const DefaultPageLimit = 200;
*/ */
function getTrustLines(account, options, callback) { function getTrustLines(account, options, callback) {
validate.address(account); validate.address(account);
validate.currency(options.currency, true);
validate.counterparty(options.counterparty, true);
validate.options(options); validate.options(options);
const self = this; const self = this;
@@ -39,7 +36,7 @@ function getTrustLines(account, options, callback) {
('^' + options.currency.toUpperCase() + '$') : /./); ('^' + options.currency.toUpperCase() + '$') : /./);
function getAccountLines(prevResult) { function getAccountLines(prevResult) {
const isAggregate = options.limit === 'all'; const isAggregate = options.limit === undefined;
if (prevResult && (!isAggregate || !prevResult.marker)) { if (prevResult && (!isAggregate || !prevResult.marker)) {
return Promise.resolve(prevResult); return Promise.resolve(prevResult);
} }
@@ -56,8 +53,7 @@ function getTrustLines(account, options, callback) {
ledger = prevResult.ledger_index; ledger = prevResult.ledger_index;
} else { } else {
marker = options.marker; marker = options.marker;
limit = validator.isValid(options.limit, 'UINT32') limit = options.limit || DefaultPageLimit;
? Number(options.limit) : DefaultPageLimit;
ledger = utils.parseLedger(options.ledger); ledger = utils.parseLedger(options.ledger);
} }

View File

@@ -5,7 +5,6 @@ const async = require('async');
const asyncify = require('simple-asyncify'); const asyncify = require('simple-asyncify');
const common = require('../common'); const common = require('../common');
const ripple = common.core; const ripple = common.core;
const validator = common.schemaValidator;
function renameCounterpartyToIssuer(amount) { function renameCounterpartyToIssuer(amount) {
if (amount === undefined) { if (amount === undefined) {
@@ -25,7 +24,7 @@ function renameCounterpartyToIssuerInOrder(order) {
} }
function isValidHash256(hash) { function isValidHash256(hash) {
return validator.isValid(hash, 'Hash256'); return ripple.UInt256.is_valid(hash);
} }
function parseLedger(ledger) { function parseLedger(ledger) {
@@ -68,23 +67,6 @@ function parseCurrencyAmount(rippledAmount, useIssuer) {
return amount; 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) { function signum(num) {
return (num === 0) ? 0 : (num > 0 ? 1 : -1); return (num === 0) ? 0 : (num > 0 ? 1 : -1);
} }
@@ -108,18 +90,6 @@ function compareTransactions(first, second) {
return Number(first.ledger_index) < Number(second.ledger_index) ? -1 : 1; 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) { function attachDate(api, baseTransactions, callback) {
const groupedTx = _.groupBy(baseTransactions, function(tx) { const groupedTx = _.groupBy(baseTransactions, function(tx) {
return tx.ledger_index; return tx.ledger_index;
@@ -150,12 +120,8 @@ function attachDate(api, baseTransactions, callback) {
} }
module.exports = { module.exports = {
isValidLedgerSequence: isValidLedgerSequence,
isValidLedgerWord: isValidLedgerWord,
isValidLedgerHash: isValidLedgerHash,
parseLedger: parseLedger, parseLedger: parseLedger,
parseCurrencyAmount: parseCurrencyAmount, parseCurrencyAmount: parseCurrencyAmount,
parseCurrencyQuery: parseCurrencyQuery,
compareTransactions: compareTransactions, compareTransactions: compareTransactions,
renameCounterpartyToIssuer: renameCounterpartyToIssuer, renameCounterpartyToIssuer: renameCounterpartyToIssuer,
renameCounterpartyToIssuerInOrder: renameCounterpartyToIssuerInOrder, renameCounterpartyToIssuerInOrder: renameCounterpartyToIssuerInOrder,

View File

@@ -7,8 +7,8 @@ const validate = utils.common.validate;
const convertAmount = utils.common.convertAmount; const convertAmount = utils.common.convertAmount;
function isSendMaxAllowed(payment) { function isSendMaxAllowed(payment) {
const srcAmt = payment.source_amount; const srcAmt = payment.source.amount;
const dstAmt = payment.destination_amount; const dstAmt = payment.destination.amount;
// Don't set SendMax for XRP->XRP payment // Don't set SendMax for XRP->XRP payment
// temREDUNDANT_SEND_MAX removed in: // temREDUNDANT_SEND_MAX removed in:
@@ -17,98 +17,94 @@ function isSendMaxAllowed(payment) {
return srcAmt && !(srcAmt.currency === 'XRP' && dstAmt.currency === 'XRP'); return srcAmt && !(srcAmt.currency === 'XRP' && dstAmt.currency === 'XRP');
} }
function isIOUWithoutCounterparty(amount) {
return amount && amount.currency !== 'XRP'
&& amount.counterparty === undefined;
}
function applyAnyCounterpartyEncoding(payment) {
// Convert blank counterparty to sender or receiver's address
// (Ripple convention for 'any counterparty')
// https://ripple.com/build/transactions/
// #special-issuer-values-for-sendmax-and-amount
// https://ripple.com/build/ripple-rest/#counterparties-in-payments
if (isIOUWithoutCounterparty(payment.source.amount)) {
payment.source.amount.counterparty = payment.source.address;
}
if (isIOUWithoutCounterparty(payment.destination.amount)) {
payment.destination.amount.counterparty = payment.destination.address;
}
}
function uppercaseCurrencyCodes(payment) {
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();
}
}
function createPaymentTransaction(account, payment) { function createPaymentTransaction(account, payment) {
applyAnyCounterpartyEncoding(payment);
uppercaseCurrencyCodes(payment);
validate.address(account); validate.address(account);
validate.payment(payment); 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 transaction = new ripple.Transaction();
const transactionData = { const transactionData = {
from: payment.source_account, from: payment.source.address,
to: payment.destination_account, to: payment.destination.address,
amount: convertAmount(payment.destination_amount) amount: convertAmount(payment.destination.amount)
}; };
// invoice_id Because transactionData is a object, transaction.payment if (payment.invoiceID) {
// function is ignored invoiceID transaction.invoiceID(payment.invoiceID);
if (payment.invoice_id) {
transaction.invoiceID(payment.invoice_id);
} }
transaction.payment(transactionData); transaction.payment(transactionData);
// Tags
if (payment.source_tag) { if (payment.source.tag) {
transaction.sourceTag(parseInt(payment.source_tag, 10)); transaction.sourceTag(parseInt(payment.source.tag, 10));
} }
if (payment.destination_tag) { if (payment.destination.tag) {
transaction.destinationTag(parseInt(payment.destination_tag, 10)); transaction.destinationTag(parseInt(payment.destination.tag, 10));
} }
// SendMax
if (isSendMaxAllowed(payment)) { if (isSendMaxAllowed(payment)) {
const max_value = new BigNumber(payment.source_amount.value) const maxValue = new BigNumber(payment.source.amount.value)
.plus(payment.source_slippage || 0).toString(); .plus(payment.source.slippage || 0).toString();
if (payment.source_amount.currency === 'XRP') { if (payment.source_amount.currency === 'XRP') {
transaction.sendMax(utils.xrpToDrops(max_value)); transaction.sendMax(utils.xrpToDrops(maxValue));
} else { } else {
transaction.sendMax({ transaction.sendMax({
value: max_value, value: maxValue,
currency: payment.source_amount.currency, currency: payment.source.amount.currency,
issuer: payment.source_amount.issuer issuer: payment.source.amount.counterparty
}); });
} }
} }
// Paths
if (typeof payment.paths === 'string') { if (typeof payment.paths === 'string') {
transaction.paths(JSON.parse(payment.paths)); transaction.paths(JSON.parse(payment.paths));
} else if (typeof payment.paths === 'object') { } else if (typeof payment.paths === 'object') {
transaction.paths(payment.paths); transaction.paths(payment.paths);
} }
// Memos
if (payment.memos && Array.isArray(payment.memos)) { if (payment.memos && Array.isArray(payment.memos)) {
for (let m = 0; m < payment.memos.length; m++) { for (let m = 0; m < payment.memos.length; m++) {
const memo = payment.memos[m]; const memo = payment.memos[m];
transaction.addMemo(memo.MemoType, memo.MemoFormat, memo.MemoData); transaction.addMemo(memo.type, memo.format, memo.data);
} }
} }
// Flags
let flags = []; let flags = [];
if (payment.partial_payment) { if (payment.allowPartialPayment) {
flags.push('PartialPayment'); flags.push('PartialPayment');
} }
if (payment.no_direct_ripple) { if (payment.noDirectRipple) {
flags.push('NoRippleDirect'); flags.push('NoRippleDirect');
} }
if (flags.length > 0) { if (flags.length > 0) {

View File

@@ -47,27 +47,27 @@ function getFeeDrops(remote) {
} }
function createTxJSON(transaction, remote, instructions, callback) { function createTxJSON(transaction, remote, instructions, callback) {
common.validate.options(instructions); common.validate.instructions(instructions);
transaction.complete(); transaction.complete();
const account = transaction.getAccount(); const account = transaction.getAccount();
const tx_json = transaction.tx_json; const tx_json = transaction.tx_json;
if (instructions.last_ledger_sequence !== undefined) { if (instructions.maxLedgerVersion !== undefined) {
tx_json.LastLedgerSequence = tx_json.LastLedgerSequence =
parseInt(instructions.last_ledger_sequence, 10); parseInt(instructions.maxLedgerVersion, 10);
} else { } else {
const offset = instructions.last_ledger_offset !== undefined ? const offset = instructions.maxLedgerVersionOffset !== undefined ?
parseInt(instructions.last_ledger_offset, 10) : 3; parseInt(instructions.maxLedgerVersionOffset, 10) : 3;
tx_json.LastLedgerSequence = remote.getLedgerSequence() + offset; tx_json.LastLedgerSequence = remote.getLedgerSequence() + offset;
} }
if (instructions.fixed_fee !== undefined) { if (instructions.fee !== undefined) {
tx_json.Fee = common.xrpToDrops(instructions.fixed_fee); tx_json.Fee = common.xrpToDrops(instructions.fee);
} else { } else {
const serverFeeDrops = getFeeDrops(remote); const serverFeeDrops = getFeeDrops(remote);
if (instructions.max_fee !== undefined) { if (instructions.maxFee !== undefined) {
const maxFeeDrops = common.xrpToDrops(instructions.max_fee); const maxFeeDrops = common.xrpToDrops(instructions.maxFee);
tx_json.Fee = BigNumber.min(serverFeeDrops, maxFeeDrops).toString(); tx_json.Fee = BigNumber.min(serverFeeDrops, maxFeeDrops).toString();
} else { } else {
tx_json.Fee = serverFeeDrops; tx_json.Fee = serverFeeDrops;

View File

@@ -11,7 +11,7 @@ describe('RippleAPI', function() {
afterEach(setupAPI.teardown); afterEach(setupAPI.teardown);
it('preparePayment', function(done) { it('preparePayment', function(done) {
const instructions = {lastLedgerOffset: 100}; const instructions = {maxLedgerVersionOffset: 100};
this.api.preparePayment(address, paymentSpecification, instructions, this.api.preparePayment(address, paymentSpecification, instructions,
(error, response) => { (error, response) => {
if (error) { if (error) {

View File

@@ -9,7 +9,7 @@
"currency": "USD", "currency": "USD",
"issuer": "rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM" "issuer": "rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM"
}, },
"LastLedgerSequence": 8819955, "LastLedgerSequence": 8820052,
"Fee": "12", "Fee": "12",
"Sequence": 23 "Sequence": 23
} }

View File

@@ -1,9 +1,13 @@
{ {
"source_account": "r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59", "source": {
"destination_account": "rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo", "address": "r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59"
"destination_amount": { },
"value": "0.01", "destination": {
"currency": "USD", "address": "rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo",
"issuer": "rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM" "amount": {
"value": "0.01",
"currency": "USD",
"counterparty": "rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM"
}
} }
} }