diff --git a/src/api/common/constants.js b/src/api/common/constants.js index fc43972d..7c2f038d 100644 --- a/src/api/common/constants.js +++ b/src/api/common/constants.js @@ -37,32 +37,27 @@ const AccountRootFlags = { }; const AccountRootFields = { - Sequence: {name: 'transaction_sequence'}, - EmailHash: {name: 'email_hash', encoding: 'hex', length: 32, defaults: '0'}, - WalletLocator: {name: 'wallet_locator', encoding: 'hex', + Sequence: {name: 'sequence'}, + EmailHash: {name: 'emailHash', encoding: 'hex', length: 32, defaults: '0'}, + WalletLocator: {name: 'walletLocator', encoding: 'hex', length: 64, defaults: '0'}, - WalletSize: {name: 'wallet_size', defaults: 0}, - MessageKey: {name: 'message_key'}, + WalletSize: {name: 'walletSize', defaults: 0}, + MessageKey: {name: 'messageKey'}, Domain: {name: 'domain', encoding: 'hex'}, - TransferRate: {name: 'transfer_rate', defaults: 0}, + TransferRate: {name: 'transferRate', 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} + noFreeze: ripple.Transaction.set_clear_flags.AccountSet.asfNoFreeze, + globalFreeze: ripple.Transaction.set_clear_flags.AccountSet.asfGlobalFreeze, + defaultRipple: 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'} + requireDestinationTag: {set: 'RequireDestTag', unset: 'OptionalDestTag'}, + requireAuthorization: {set: 'RequireAuth', unset: 'OptionalAuth'}, + disallowIncomingXRP: {set: 'DisallowXRP', unset: 'AllowXRP'} }; const AccountSetResponseFlags = { diff --git a/src/api/common/schemas/cancellation.json b/src/api/common/schemas/cancellation.json index 3d95d8ac..2c97d6af 100644 --- a/src/api/common/schemas/cancellation.json +++ b/src/api/common/schemas/cancellation.json @@ -3,10 +3,7 @@ "title": "cancellation", "type": "object", "properties": { - "orderSequence": { - "type": "integer", - "minimum": 0 - } + "orderSequence": {"$ref": "sequence"} }, "required": ["orderSequence"], "additionalProperties": false diff --git a/src/api/common/schemas/instructions.json b/src/api/common/schemas/instructions.json index 8a35378d..5d342c4f 100644 --- a/src/api/common/schemas/instructions.json +++ b/src/api/common/schemas/instructions.json @@ -6,8 +6,7 @@ "properties": { "sequence": { "description": "The sequence number, relative to the initiating account, of this transaction.", - "type": "integer", - "minimum": 1 + "$ref": "sequence" }, "fee": { "description": "Fixed Fee", diff --git a/src/api/common/schemas/ledgerversion.json b/src/api/common/schemas/ledgerversion.json index 9b0154f4..cd71d35f 100644 --- a/src/api/common/schemas/ledgerversion.json +++ b/src/api/common/schemas/ledgerversion.json @@ -3,5 +3,5 @@ "title": "ledgerVersion", "description": "A ledger version number", "type": "integer", - "minimum": 0 + "minimum": 1 } diff --git a/src/api/common/schemas/sequence.json b/src/api/common/schemas/sequence.json new file mode 100644 index 00000000..29e8d6f9 --- /dev/null +++ b/src/api/common/schemas/sequence.json @@ -0,0 +1,7 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "sequence", + "description": "An account transaction sequence number", + "type": "integer", + "minimum": 1 +} diff --git a/src/api/common/schemas/settings.json b/src/api/common/schemas/settings.json index 19f7f95f..d5add67a 100644 --- a/src/api/common/schemas/settings.json +++ b/src/api/common/schemas/settings.json @@ -3,9 +3,22 @@ "title": "settings", "type": "object", "properties": { - "name": {"type": "string"}, - "value": {"type": "string"} + "passwordSpent": {"type": "boolean"}, + "requireDestinationTag": {"type": "boolean"}, + "requireAuthorization": {"type": "boolean"}, + "disallowIncomingXRP": {"type": "boolean"}, + "disableMasterKey": {"type": "boolean"}, + "noFreeze": {"type": "boolean"}, + "globalFreeze": {"type": "boolean"}, + "defaultRipple": {"type": "boolean"}, + "emailHash": {"type": "string"}, + "walletLocator": {"$ref": "hash256"}, + "walletSize": {"type": "integer"}, + "messageKey": {"type": "string"}, + "domain": {"type": "string"}, + "transferRate": {"type": "integer"}, + "signers": {"type": "string"} }, - "required": ["name", "value"], + "minProperties": 1, "additionalProperties": false } diff --git a/src/api/common/schemas/tx.json b/src/api/common/schemas/tx.json index ee59b1eb..c59be65b 100644 --- a/src/api/common/schemas/tx.json +++ b/src/api/common/schemas/tx.json @@ -5,5 +5,6 @@ "type": "object", "properties": { "Account": {"$ref": "address"} - } + }, + "required": ["Account"] } diff --git a/src/api/common/validate.js b/src/api/common/validate.js index 52e1213a..c6de30a0 100644 --- a/src/api/common/validate.js +++ b/src/api/common/validate.js @@ -42,7 +42,7 @@ module.exports = { addressAndSecret: validateAddressAndSecret, currency: _.partial(schemaValidate, 'currency'), identifier: _.partial(schemaValidate, 'hash256'), - sequence: _.partial(schemaValidate, 'natural'), + sequence: _.partial(schemaValidate, 'sequence'), order: _.partial(schemaValidate, 'order'), orderbook: _.partial(schemaValidate, 'orderbook'), payment: _.partial(schemaValidate, 'payment'), diff --git a/src/api/ledger/tx-to-rest-converter.js b/src/api/ledger/tx-to-rest-converter.js index f7d7f733..cf805ce3 100644 --- a/src/api/ledger/tx-to-rest-converter.js +++ b/src/api/ledger/tx-to-rest-converter.js @@ -345,13 +345,11 @@ function parseTrustLineResponse(message, 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]; + _settings[flagName] = settings[flagName]; } for (let fieldName in constants.AccountRootFields) { - const field = constants.AccountRootFields[fieldName]; - _settings[field.name] = settings[field.name]; + _settings[fieldName] = settings[fieldName]; } _.assign(_settings, parseFlagsFromResponse(message.tx_json.Flags, diff --git a/src/api/transaction/order.js b/src/api/transaction/order.js index 34f20a10..b282fe2e 100644 --- a/src/api/transaction/order.js +++ b/src/api/transaction/order.js @@ -17,9 +17,9 @@ function renameCounterpartyToIssuer(amount) { } const OfferCreateFlags = { - Passive: {name: 'passive', set: 'Passive'}, - ImmediateOrCancel: {name: 'immediateOrCancel', set: 'ImmediateOrCancel'}, - FillOrKill: {name: 'fillOrKill', set: 'FillOrKill'} + passive: {set: 'Passive'}, + immediateOrCancel: {set: 'ImmediateOrCancel'}, + fillOrKill: {set: 'FillOrKill'} }; function toRippledAmount(amount) { @@ -37,17 +37,13 @@ function createOrderTransaction(account, order) { const takerGets = toRippledAmount(order.direction === 'buy' ? order.totalPrice : order.quantity); - transaction.offerCreate(account, ripple.Amount.from_json(takerPays), - ripple.Amount.from_json(takerGets)); - - utils.setTransactionBitFlags(transaction, { - input: order, - flags: OfferCreateFlags - }); + transaction.offerCreate(account, takerPays, takerGets); + utils.setTransactionBitFlags(transaction, order, OfferCreateFlags); if (order.direction === 'sell') { transaction.setFlags('Sell'); } + return transaction; } diff --git a/src/api/transaction/settings.js b/src/api/transaction/settings.js index f76b917f..89bffea1 100644 --- a/src/api/transaction/settings.js +++ b/src/api/transaction/settings.js @@ -2,7 +2,6 @@ /* 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; @@ -12,26 +11,6 @@ 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 * @@ -43,20 +22,15 @@ function padValue(value, length) { * * @returns undefined */ -function setTransactionIntFlags(transaction, input, flags) { +function setTransactionIntFlags(transaction, values, flags) { for (let flagName in flags) { - const flag = flags[flagName]; + const value = values[flagName]; + const code = 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; + if (value === true) { + transaction.tx_json.SetFlag = code; + } else if (value === false) { + transaction.tx_json.ClearFlag = code; } } } @@ -95,7 +69,7 @@ function setTransactionFields(transaction, input, fieldSchema) { throw new InvalidRequestError( 'Parameter length exceeded: ' + fieldName); } else if (value.length < field.length) { - value = padValue(value, field.length); + value = _.padLeft(value, field.length); } } else { // Field is variable length. Expecting an ascii string as input. @@ -139,16 +113,15 @@ function createSettingsTransaction(account, settings) { const transaction = new ripple.Transaction(); transaction.accountSet(account); - utils.setTransactionBitFlags(transaction, { - input: settings, - flags: constants.AccountSetFlags, - clear_setting: CLEAR_SETTING - }); + utils.setTransactionBitFlags(transaction, settings, + constants.AccountSetFlags); setTransactionIntFlags(transaction, settings, constants.AccountSetIntFlags); setTransactionFields(transaction, settings, constants.AccountRootFields); - transaction.tx_json.TransferRate = convertTransferRate( - transaction.tx_json.TransferRate); + if (transaction.tx_json.TransferRate !== undefined) { + transaction.tx_json.TransferRate = convertTransferRate( + transaction.tx_json.TransferRate); + } return transaction; } diff --git a/src/api/transaction/trustline.js b/src/api/transaction/trustline.js index cf3b48ee..c5d58e7e 100644 --- a/src/api/transaction/trustline.js +++ b/src/api/transaction/trustline.js @@ -5,42 +5,25 @@ 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'} + authorized: {set: 'SetAuth'}, + allowRippling: {set: 'ClearNoRipple', unset: 'NoRipple'}, + frozed: {set: 'SetFreeze', unset: 'ClearFreeze'} }; function createTrustLineTransaction(account, trustline) { validate.address(account); validate.trustline(trustline); - if (trustline && trustline.limit) { - trustline.limit = String(trustline.limit); - } + const limit = { + currency: trustline.currency, + issuer: trustline.counterparty, + value: 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: '' - }); + transaction.trustSet(account, limit, + trustline.qualityIn, trustline.qualityOut); + utils.setTransactionBitFlags(transaction, trustline, TrustSetFlags); return transaction; } diff --git a/src/api/transaction/utils.js b/src/api/transaction/utils.js index 00aecc8f..7823cbeb 100644 --- a/src/api/transaction/utils.js +++ b/src/api/transaction/utils.js @@ -19,26 +19,17 @@ const common = require('../common'); * * @returns undefined */ -/*:: type FlagOptions = {flags: any; input: any; clear_setting?: string} */ -function setTransactionBitFlags(transaction: any, options: FlagOptions): void { - for (let flagName in options.flags) { - const flag = options.flags[flagName]; +function setTransactionBitFlags(transaction: any, values: any, flags: any): + void { + for (let flagName in flags) { + const flagValue = values[flagName]; + const flagConversions = flags[flagName]; - // Set transaction flags - if (!(flag.name in options.input)) { - continue; + if (flagValue === true && flagConversions.set !== undefined) { + transaction.setFlags(flagConversions.set); } - - 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); + if (flagValue === false && flagConversions.unset !== undefined) { + transaction.setFlags(flagConversions.unset); } } } diff --git a/test/api-test.js b/test/api-test.js index e9750ba8..c412f7d8 100644 --- a/test/api-test.js +++ b/test/api-test.js @@ -1,51 +1,81 @@ 'use strict'; -const assert = require('assert'); +const _ = require('lodash'); +const assert = require('assert-diff'); const setupAPI = require('./setup-api'); const address = require('./fixtures/addresses').ACCOUNT; const paymentSpecification = require('./fixtures/payment-specification'); const paymentResponse = require('./fixtures/payment-response'); const orderSpecification = require('./fixtures/order-specification'); const orderResponse = require('./fixtures/order-response'); +const trustlineSpecification = + require('./fixtures/trustline-specification'); +const trustlineResponse = require('./fixtures/trustline-response'); const balancesResponse = require('./fixtures/balances-response'); +const orderCancellationResponse = + require('./fixtures/ordercancellation-response'); +const settingsSpecification = require('./fixtures/settings-specification'); +const settingsResponse = require('./fixtures/settings-response'); +const signInput = require('./fixtures/sign-input'); +const signOutput = require('./fixtures/sign-output'); +const MockPRNG = require('./mock-prng'); +const sjcl = require('../src').sjcl; + +function checkResult(expected, done, error, response) { + if (error) { + done(error); + return; + } + assert.deepEqual(response, expected); + done(); +} + +function withDeterministicPRNG(f) { + const prng = sjcl.random; + sjcl.random = new MockPRNG(); + f(); + sjcl.random = prng; +} describe('RippleAPI', function() { + const instructions = {maxLedgerVersionOffset: 100}; beforeEach(setupAPI.setup); afterEach(setupAPI.teardown); it('preparePayment', function(done) { - const instructions = {maxLedgerVersionOffset: 100}; this.api.preparePayment(address, paymentSpecification, instructions, - (error, response) => { - if (error) { - done(error); - return; - } - assert.deepEqual(response, paymentResponse); - done(); - }); + _.partial(checkResult, paymentResponse, done)); }); it('prepareOrder', function(done) { - const instructions = {maxLedgerVersionOffset: 100}; this.api.prepareOrder(address, orderSpecification, instructions, - (error, response) => { - if (error) { - done(error); - return; - } - assert.deepEqual(response, orderResponse); - done(); + _.partial(checkResult, orderResponse, done)); + }); + + it('prepareOrderCancellation', function(done) { + this.api.prepareOrderCancellation(address, 23, instructions, + _.partial(checkResult, orderCancellationResponse, done)); + }); + + it('prepareTrustline', function(done) { + this.api.prepareTrustline(address, trustlineSpecification, + instructions, _.partial(checkResult, trustlineResponse, done)); + }); + + it('prepareSettings', function(done) { + this.api.prepareSettings(address, settingsSpecification, + instructions, _.partial(checkResult, settingsResponse, done)); + }); + + it('sign', function() { + const secret = 'shsWGZcmZz6YsWWmcnpfr6fLTdtFV'; + withDeterministicPRNG(() => { + const result = this.api.sign(signInput, secret); + assert.deepEqual(result, signOutput); }); }); it('getBalances', function(done) { - this.api.getBalances(address, {}, (error, response) => { - if (error) { - done(error); - return; - } - assert.deepEqual(response, balancesResponse); - done(); - }); + this.api.getBalances(address, {}, + _.partial(checkResult, balancesResponse, done)); }); }); diff --git a/test/fixtures/ordercancellation-response.json b/test/fixtures/ordercancellation-response.json new file mode 100644 index 00000000..d48e3ac9 --- /dev/null +++ b/test/fixtures/ordercancellation-response.json @@ -0,0 +1,9 @@ +{ + "Flags": 0, + "TransactionType": "OfferCancel", + "Account": "r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59", + "OfferSequence": 23, + "LastLedgerSequence": 8820052, + "Fee": "12", + "Sequence": 23 +} diff --git a/test/fixtures/settings-response.json b/test/fixtures/settings-response.json new file mode 100644 index 00000000..ebb2cc42 --- /dev/null +++ b/test/fixtures/settings-response.json @@ -0,0 +1,11 @@ +{ + "Flags": 1048576, + "TransactionType": "AccountSet", + "Account": "r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59", + "SetFlag": 6, + "EmailHash": "98B4375E1D753E5B91627516F6D70977", + "Domain": "726970706C652E636F6D", + "LastLedgerSequence": 8820052, + "Fee": "12", + "Sequence": 23 +} diff --git a/test/fixtures/settings-specification.json b/test/fixtures/settings-specification.json new file mode 100644 index 00000000..1bc41adf --- /dev/null +++ b/test/fixtures/settings-specification.json @@ -0,0 +1,6 @@ +{ + "domain": "ripple.com", + "emailHash": "98B4375E1D753E5B91627516F6D70977", + "noFreeze": true, + "disallowIncomingXRP": true +} diff --git a/test/fixtures/sign-input.json b/test/fixtures/sign-input.json new file mode 100644 index 00000000..79ba1875 --- /dev/null +++ b/test/fixtures/sign-input.json @@ -0,0 +1,9 @@ +{ + "Flags": 0, + "TransactionType": "AccountSet", + "Account": "r3GgMwvgvP8h4yVWvjH1dPZNvC37TjzBBE", + "Domain": "726970706C652E636F6D", + "LastLedgerSequence": 8820052, + "Fee": "12", + "Sequence": 23 +} diff --git a/test/fixtures/sign-output.json b/test/fixtures/sign-output.json new file mode 100644 index 00000000..66f45260 --- /dev/null +++ b/test/fixtures/sign-output.json @@ -0,0 +1,4 @@ +{ + "tx_blob": "12000322000000002400000017201B0086955468400000000000000C732102F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D87446304402207660BDEF67105CE1EBA9AD35DC7156BAB43FF1D47633199EE257D70B6B9AAFBF02207F5517BC8AEF2ADC1325897ECDBA8C673838048BCA62F4E98B252F19BE88796D770A726970706C652E636F6D81144FBFF73DA4ECF9B701940F27341FA8020C313443", + "hash": "DB44C111583A95AF973A0B0A40348D90512FCBCDDCA3315A286D2BF4FAC100F1" +} diff --git a/test/fixtures/trustline-response.json b/test/fixtures/trustline-response.json new file mode 100644 index 00000000..e6c5414f --- /dev/null +++ b/test/fixtures/trustline-response.json @@ -0,0 +1,15 @@ +{ + "Flags": 131072, + "TransactionType": "TrustSet", + "Account": "r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59", + "LimitAmount": { + "value": "10000", + "currency": "USD", + "issuer": "rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM" + }, + "QualityIn": 500000000, + "QualityOut": 500000000, + "LastLedgerSequence": 8820052, + "Fee": "12", + "Sequence": 23 +} diff --git a/test/fixtures/trustline-specification.json b/test/fixtures/trustline-specification.json new file mode 100644 index 00000000..72384761 --- /dev/null +++ b/test/fixtures/trustline-specification.json @@ -0,0 +1,9 @@ +{ + "currency": "USD", + "counterparty": "rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM", + "limit": "10000", + "qualityIn": 500000000, + "qualityOut": 500000000, + "allowRippling": false, + "frozen": false +} diff --git a/test/mock-prng.js b/test/mock-prng.js new file mode 100644 index 00000000..2f9cf362 --- /dev/null +++ b/test/mock-prng.js @@ -0,0 +1,26 @@ +'use strict'; +const _ = require('lodash'); + +const SEED = + '3045022100A58B0460BC5092CB4F96155C19125A4E079C870663F1D5E8BBC9BD0'; + +function MockPRNG(seed) { + if (seed && seed.length < 8) { + throw new Error('seed must be a hex string of at least 8 characters'); + } + this.position = 0; + this.seed = seed || SEED; +} + +MockPRNG.prototype.randomWord = function() { + const i = this.position; + this.position = (i + 8) % this.seed.length; + const data = this.seed + this.seed.slice(8); + return parseInt(data.slice(i, i + 8), 16); +}; + +MockPRNG.prototype.randomWords = function(n) { + return _.times(n, () => this.randomWord()); +}; + +module.exports = MockPRNG;