diff --git a/test/api-test.ts b/test/api-test.ts deleted file mode 100644 index 7b4bccea..00000000 --- a/test/api-test.ts +++ /dev/null @@ -1,4335 +0,0 @@ -import assert from 'assert-diff'; -import BigNumber from 'bignumber.js'; -import _ from 'lodash'; -import {RippleAPI} from 'ripple-api'; -import {RecursiveData} from 'ripple-api/ledger/utils'; -import binary from 'ripple-binary-codec'; -import requests from './fixtures/requests'; -import responses from './fixtures/responses'; -import addresses from './fixtures/addresses.json'; -import hashes from './fixtures/hashes.json'; -import ledgerClosed from './fixtures/rippled/ledger-close-newer.json'; -import setupAPI from './setup-api'; -const {validate, schemaValidator} = RippleAPI._PRIVATE; -const address = addresses.ACCOUNT; -const utils = RippleAPI._PRIVATE.ledgerUtils; -assert.options.strict = true; - -// how long before each test case times out -const TIMEOUT = 20000; - -function closeLedger(connection) { - connection._ws.emit('message', JSON.stringify(ledgerClosed)); -} - -function checkResult(expected, schemaName, response) { - if (expected.txJSON) { - assert(response.txJSON); - assert.deepEqual(JSON.parse(response.txJSON), JSON.parse(expected.txJSON)); - } - if (expected.tx_json) { - assert(response.tx_json); - assert.deepEqual(response.tx_json, expected.tx_json); - } - assert.deepEqual(_.omit(response, ['txJSON', 'tx_json']), _.omit(expected, ['txJSON', 'tx_json'])) - if (schemaName) { - schemaValidator.schemaValidate(schemaName, response); - } - return response; -} - - -describe('RippleAPI', function () { - this.timeout(TIMEOUT); - const instructionsWithMaxLedgerVersionOffset = { maxLedgerVersionOffset: 100 }; - beforeEach(setupAPI.setup); - afterEach(setupAPI.teardown); - - it('error inspect', function () { - const error = new this.api.errors.RippleError('mess', { data: 1 }); - assert.strictEqual(error.inspect(), '[RippleError(mess, { data: 1 })]'); - }); - - - describe('dropsToXrp', function () { - it('works with a typical amount', function () { - const xrp = this.api.dropsToXrp('2000000') - assert.strictEqual(xrp, '2', '2 million drops equals 2 XRP') - }) - - it('works with fractions', function () { - let xrp = this.api.dropsToXrp('3456789') - assert.strictEqual(xrp, '3.456789', '3,456,789 drops equals 3.456789 XRP') - - xrp = this.api.dropsToXrp('3400000') - assert.strictEqual(xrp, '3.4', '3,400,000 drops equals 3.4 XRP') - - xrp = this.api.dropsToXrp('1') - assert.strictEqual(xrp, '0.000001', '1 drop equals 0.000001 XRP') - - xrp = this.api.dropsToXrp('1.0') - assert.strictEqual(xrp, '0.000001', '1.0 drops equals 0.000001 XRP') - - xrp = this.api.dropsToXrp('1.00') - assert.strictEqual(xrp, '0.000001', '1.00 drops equals 0.000001 XRP') - }) - - it('works with zero', function () { - let xrp = this.api.dropsToXrp('0') - assert.strictEqual(xrp, '0', '0 drops equals 0 XRP') - - // negative zero is equivalent to zero - xrp = this.api.dropsToXrp('-0') - assert.strictEqual(xrp, '0', '-0 drops equals 0 XRP') - - xrp = this.api.dropsToXrp('0.00') - assert.strictEqual(xrp, '0', '0.00 drops equals 0 XRP') - - xrp = this.api.dropsToXrp('000000000') - assert.strictEqual(xrp, '0', '000000000 drops equals 0 XRP') - }) - - it('works with a negative value', function () { - const xrp = this.api.dropsToXrp('-2000000') - assert.strictEqual(xrp, '-2', '-2 million drops equals -2 XRP') - }) - - it('works with a value ending with a decimal point', function () { - let xrp = this.api.dropsToXrp('2000000.') - assert.strictEqual(xrp, '2', '2000000. drops equals 2 XRP') - - xrp = this.api.dropsToXrp('-2000000.') - assert.strictEqual(xrp, '-2', '-2000000. drops equals -2 XRP') - }) - - it('works with BigNumber objects', function () { - let xrp = this.api.dropsToXrp(new BigNumber(2000000)) - assert.strictEqual(xrp, '2', '(BigNumber) 2 million drops equals 2 XRP') - - xrp = this.api.dropsToXrp(new BigNumber(-2000000)) - assert.strictEqual(xrp, '-2', '(BigNumber) -2 million drops equals -2 XRP') - - xrp = this.api.dropsToXrp(new BigNumber(2345678)) - assert.strictEqual(xrp, '2.345678', '(BigNumber) 2,345,678 drops equals 2.345678 XRP') - - xrp = this.api.dropsToXrp(new BigNumber(-2345678)) - assert.strictEqual(xrp, '-2.345678', '(BigNumber) -2,345,678 drops equals -2.345678 XRP') - }) - - it('works with a number', function() { - // This is not recommended. Use strings or BigNumber objects to avoid precision errors. - - let xrp = this.api.dropsToXrp(2000000) - assert.strictEqual(xrp, '2', '(number) 2 million drops equals 2 XRP') - - xrp = this.api.dropsToXrp(-2000000) - assert.strictEqual(xrp, '-2', '(number) -2 million drops equals -2 XRP') - }) - - it('throws with an amount with too many decimal places', function () { - assert.throws(() => { - this.api.dropsToXrp('1.2') - }, /has too many decimal places/) - - assert.throws(() => { - this.api.dropsToXrp('0.10') - }, /has too many decimal places/) - }) - - it('throws with an invalid value', function () { - assert.throws(() => { - this.api.dropsToXrp('FOO') - }, /invalid value/) - - assert.throws(() => { - this.api.dropsToXrp('1e-7') - }, /invalid value/) - - assert.throws(() => { - this.api.dropsToXrp('2,0') - }, /invalid value/) - - assert.throws(() => { - this.api.dropsToXrp('.') - }, /dropsToXrp: invalid value '\.', should be a BigNumber or string-encoded number\./) - }) - - it('throws with an amount more than one decimal point', function () { - assert.throws(() => { - this.api.dropsToXrp('1.0.0') - }, /dropsToXrp: invalid value '1\.0\.0'/) - - assert.throws(() => { - this.api.dropsToXrp('...') - }, /dropsToXrp: invalid value '\.\.\.'/) - }) - }) - - - describe('isValidAddress', function () { - it('returns true for valid address', function () { - assert(this.api.isValidAddress('rLczgQHxPhWtjkaQqn3Q6UM8AbRbbRvs5K')); - assert(this.api.isValidAddress(addresses.ACCOUNT_X)); - assert(this.api.isValidAddress(addresses.ACCOUNT_T)); - }) - - it('returns false for invalid address', function () { - assert(!this.api.isValidAddress('foobar')); - assert(!this.api.isValidAddress(addresses.ACCOUNT_X.slice(0, -1))); - assert(!this.api.isValidAddress(addresses.ACCOUNT_T.slice(1))); - }) - }) - - describe('isValidSecret', function () { - it('returns true for valid secret', function () { - assert(this.api.isValidSecret('snsakdSrZSLkYpCXxfRkS4Sh96PMK')); - }) - - it('returns false for invalid secret', function () { - assert(!this.api.isValidSecret('foobar')); - }) - }) - - describe('deriveKeypair', function () { - it('returns keypair for secret', function () { - var keypair = this.api.deriveKeypair('snsakdSrZSLkYpCXxfRkS4Sh96PMK'); - assert.equal(keypair.privateKey, '008850736302221AFD59FF9CA1A29D4975F491D726249302EE48A3078A8934D335'); - assert.equal(keypair.publicKey, '035332FBA71D705BD5D97014A833BE2BBB25BEFCD3506198E14AFEA241B98C2D06'); - }) - - it('returns keypair for ed25519 secret', function () { - var keypair = this.api.deriveKeypair('sEdV9eHWbibBnTj7b1H5kHfPfv7gudx'); - assert.equal(keypair.privateKey, 'ED5C2EF6C2E3200DFA6B72F47935C7F64D35453646EA34919192538F458C7BC30F'); - assert.equal(keypair.publicKey, 'ED0805EC4E728DB87C0CA6C420751F296C57A5F42D02E9E6150CE60694A44593E5'); - }) - - it('throws with an invalid secret', function (){ - assert.throws(() => { - this.api.deriveKeypair('...'); - }, /^Error: Non-base58 character$/) - }) - }) - - describe('deriveAddress', function () { - it('returns address for public key', function () { - var address = this.api.deriveAddress('035332FBA71D705BD5D97014A833BE2BBB25BEFCD3506198E14AFEA241B98C2D06'); - assert.equal(address, 'rLczgQHxPhWtjkaQqn3Q6UM8AbRbbRvs5K'); - }) - }) - - describe('deriveXAddress', function () { - it('returns address for public key', function () { - assert.equal(RippleAPI.deriveXAddress({ - publicKey: '035332FBA71D705BD5D97014A833BE2BBB25BEFCD3506198E14AFEA241B98C2D06', - tag: false, - test: false - }), 'XVZVpQj8YSVpNyiwXYSqvQoQqgBttTxAZwMcuJd4xteQHyt'); - assert.equal(RippleAPI.deriveXAddress({ - publicKey: '035332FBA71D705BD5D97014A833BE2BBB25BEFCD3506198E14AFEA241B98C2D06', - tag: false, - test: true - }), 'TVVrSWtmQQssgVcmoMBcFQZKKf56QscyWLKnUyiuZW8ALU4'); - }) - }) - - describe('pagination', function () { - - describe('hasNextPage', function () { - - it('returns true when there is another page', function () { - return this.api.request('ledger_data').then(response => { - assert(this.api.hasNextPage(response)); - } - ); - }); - - it('returns false when there are no more pages', function () { - return this.api.request('ledger_data').then(response => { - return this.api.requestNextPage('ledger_data', {}, response); - }).then(response => { - assert(!this.api.hasNextPage(response)); - }); - }); - - }); - - describe('requestNextPage', function () { - - it('requests the next page', function () { - return this.api.request('ledger_data').then(response => { - return this.api.requestNextPage('ledger_data', {}, response); - }).then(response => { - assert.equal(response.state[0].index, '000B714B790C3C79FEE00D17C4DEB436B375466F29679447BA64F265FD63D731') - }); - }); - - it('rejects when there are no more pages', function () { - return this.api.request('ledger_data').then(response => { - return this.api.requestNextPage('ledger_data', {}, response); - }).then(response => { - assert(!this.api.hasNextPage(response)) - return this.api.requestNextPage('ledger_data', {}, response); - }).then(() => { - assert(false, 'Should reject'); - }).catch(error => { - assert(error instanceof Error); - assert.equal(error.message, 'response does not have a next page') - }); - }); - - }); - - }); - - describe('prepareTransaction - auto-fillable fields', function () { - - // Fee: - - it('does not overwrite Fee in txJSON', function () { - const localInstructions = instructionsWithMaxLedgerVersionOffset - - const txJSON = { - TransactionType: 'DepositPreauth', - Account: address, - Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', - Fee: '10' - } - - return this.api.prepareTransaction(txJSON, localInstructions).then(response => { - const expected = { - txJSON: '{"TransactionType":"DepositPreauth","Account":"' + address + '","Authorize":"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo","Flags":2147483648,"LastLedgerSequence":8820051,"Fee":"10","Sequence":23}', - instructions: { - fee: '0.00001', // Notice there are not always 6 digits after the decimal point as trailing zeros are omitted - sequence: 23, - maxLedgerVersion: 8820051 - } - } - return checkResult(expected, 'prepare', response) - }) - }) - - it('does not overwrite Fee in Instructions', function () { - const localInstructions = _.defaults({ - fee: '0.000014', // CAUTION: This `fee` is specified in XRP, not drops. - }, instructionsWithMaxLedgerVersionOffset) - - const txJSON = { - TransactionType: 'DepositPreauth', - Account: address, - Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo' - } - - return this.api.prepareTransaction(txJSON, localInstructions).then(response => { - const expected = { - txJSON: '{"TransactionType":"DepositPreauth","Account":"' + address + '","Authorize":"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo","Flags":2147483648,"LastLedgerSequence":8820051,"Fee":"14","Sequence":23}', - instructions: { - fee: '0.000014', - sequence: 23, - maxLedgerVersion: 8820051 - } - } - return checkResult(expected, 'prepare', response) - }) - }) - - it('rejects Promise if both are set, even when txJSON.Fee matches instructions.fee', function (done) { - const localInstructions = _.defaults({ - fee: '0.000016' - }, instructionsWithMaxLedgerVersionOffset) - - const txJSON = { - TransactionType: 'DepositPreauth', - Account: address, - Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', - Fee: '16' - } - - try { - this.api.prepareTransaction(txJSON, localInstructions).then(response => { - done(new Error('Expected method to reject. Prepared transaction: ' + JSON.stringify(response))); - }).catch(err => { - assert.strictEqual(err.name, 'ValidationError'); - assert.strictEqual(err.message, '`Fee` in txJSON and `fee` in `instructions` cannot both be set'); - done(); - }).catch(done); // Finish test with assertion failure immediately instead of waiting for timeout. - } catch (err) { - done(new Error('Expected method to reject, but method threw. Thrown: ' + err)); - } - }) - - it('rejects Promise if both are set, when txJSON.Fee does not match instructions.fee', function (done) { - const localInstructions = _.defaults({ - fee: '0.000018' - }, instructionsWithMaxLedgerVersionOffset) - - const txJSON = { - TransactionType: 'DepositPreauth', - Account: address, - Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', - Fee: '20' - } - - try { - this.api.prepareTransaction(txJSON, localInstructions).then(response => { - done(new Error('Expected method to reject. Prepared transaction: ' + JSON.stringify(response))); - }).catch(err => { - assert.strictEqual(err.name, 'ValidationError'); - assert.strictEqual(err.message, '`Fee` in txJSON and `fee` in `instructions` cannot both be set'); - done(); - }).catch(done); // Finish test with assertion failure immediately instead of waiting for timeout. - } catch (err) { - done(new Error('Expected method to reject, but method threw. Thrown: ' + err)); - } - }) - - it('rejects Promise when the Fee is capitalized in Instructions', function (done) { - const localInstructions = _.defaults({ - Fee: '0.000022', // Intentionally capitalized in this test, but the correct field would be `fee` - }, instructionsWithMaxLedgerVersionOffset) - - const txJSON = { - TransactionType: 'DepositPreauth', - Account: address, - Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo' - } - - try { - this.api.prepareTransaction(txJSON, localInstructions).then(response => { - done(new Error('Expected method to reject. Prepared transaction: ' + JSON.stringify(response))); - }).catch(err => { - assert.strictEqual(err.name, 'ValidationError'); - assert.strictEqual(err.message, 'instance additionalProperty "Fee" exists in instance when not allowed'); - done(); - }).catch(done); // Finish test with assertion failure immediately instead of waiting for timeout. - } catch (err) { - done(new Error('Expected method to reject, but method threw. Thrown: ' + err)); - } - }) - - it('rejects Promise when the fee is specified in txJSON', function (done) { - const localInstructions = instructionsWithMaxLedgerVersionOffset - - const txJSON = { - TransactionType: 'DepositPreauth', - Account: address, - Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', - fee: '10' - } - - try { - this.api.prepareTransaction(txJSON, localInstructions).then(response => { - done(new Error('Expected method to reject. Prepared transaction: ' + JSON.stringify(response))); - }).catch(err => { - assert.strictEqual(err.name, 'ValidationError'); - assert.strictEqual(err.message, 'txJSON additionalProperty "fee" exists in instance when not allowed'); - done(); - }).catch(done); // Finish test with assertion failure immediately instead of waiting for timeout. - } catch (err) { - done(new Error('Expected method to reject, but method threw. Thrown: ' + err)); - } - }) - - // Sequence: - - it('does not overwrite Sequence in txJSON', function () { - const localInstructions = _.defaults({ - maxFee: '0.000012' - }, instructionsWithMaxLedgerVersionOffset) - - const txJSON = { - TransactionType: 'DepositPreauth', - Account: address, - Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', - Sequence: 100 - } - - return this.api.prepareTransaction(txJSON, localInstructions).then(response => { - const expected = { - txJSON: '{"TransactionType":"DepositPreauth","Account":"' + address + '","Authorize":"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo","Flags":2147483648,"LastLedgerSequence":8820051,"Fee":"12","Sequence":100}', - instructions: { - fee: '0.000012', - sequence: 100, - maxLedgerVersion: 8820051 - } - } - return checkResult(expected, 'prepare', response) - }) - }) - - it('does not overwrite Sequence in Instructions', function () { - const localInstructions = _.defaults({ - maxFee: '0.000012', - sequence: 100 - }, instructionsWithMaxLedgerVersionOffset) - - const txJSON = { - TransactionType: 'DepositPreauth', - Account: address, - Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo' - } - - return this.api.prepareTransaction(txJSON, localInstructions).then(response => { - const expected = { - txJSON: '{"TransactionType":"DepositPreauth","Account":"' + address + '","Authorize":"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo","Flags":2147483648,"LastLedgerSequence":8820051,"Fee":"12","Sequence":100}', - instructions: { - fee: '0.000012', - sequence: 100, - maxLedgerVersion: 8820051 - } - } - return checkResult(expected, 'prepare', response) - }) - }) - - it('does not overwrite Sequence when same sequence is provided in both txJSON and Instructions', function () { - const localInstructions = _.defaults({ - maxFee: '0.000012', - sequence: 100 - }, instructionsWithMaxLedgerVersionOffset) - - const txJSON = { - TransactionType: 'DepositPreauth', - Account: address, - Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', - Sequence: 100 - } - - return this.api.prepareTransaction(txJSON, localInstructions).then(response => { - const expected = { - txJSON: '{"TransactionType":"DepositPreauth","Account":"' + address + '","Authorize":"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo","Flags":2147483648,"LastLedgerSequence":8820051,"Fee":"12","Sequence":100}', - instructions: { - fee: '0.000012', - sequence: 100, - maxLedgerVersion: 8820051 - } - } - return checkResult(expected, 'prepare', response) - }) - }) - - it('rejects Promise when Sequence in txJSON does not match sequence in Instructions', function (done) { - const localInstructions = _.defaults({ - maxFee: '0.000012', - sequence: 100 - }, instructionsWithMaxLedgerVersionOffset) - - const txJSON = { - TransactionType: 'DepositPreauth', - Account: address, - Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', - Sequence: 101 - } - - try { - this.api.prepareTransaction(txJSON, localInstructions).then(response => { - done(new Error('Expected method to reject. Prepared transaction: ' + JSON.stringify(response))); - }).catch(err => { - assert.strictEqual(err.name, 'ValidationError'); - assert.strictEqual(err.message, '`Sequence` in txJSON must match `sequence` in `instructions`'); - done(); - }).catch(done); // Finish test with assertion failure immediately instead of waiting for timeout. - } catch (err) { - done(new Error('Expected method to reject, but method threw. Thrown: ' + err)); - } - }) - - it('rejects Promise when the Sequence is capitalized in Instructions', function (done) { - const localInstructions = _.defaults({ - maxFee: '0.000012', - Sequence: 100 // Intentionally capitalized in this test, but the correct field would be `sequence` - }, instructionsWithMaxLedgerVersionOffset) - - const txJSON = { - TransactionType: 'DepositPreauth', - Account: address, - Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo' - } - - try { - this.api.prepareTransaction(txJSON, localInstructions).then(response => { - done(new Error('Expected method to reject. Prepared transaction: ' + JSON.stringify(response))); - }).catch(err => { - assert.strictEqual(err.name, 'ValidationError'); - assert.strictEqual(err.message, 'instance additionalProperty "Sequence" exists in instance when not allowed'); - done(); - }).catch(done); // Finish test with assertion failure immediately instead of waiting for timeout. - } catch (err) { - done(new Error('Expected method to reject, but method threw. Thrown: ' + err)); - } - }) - - // LastLedgerSequence aka maxLedgerVersion/maxLedgerVersionOffset: - - it('does not overwrite LastLedgerSequence in txJSON', function () { - const localInstructions = {} - - const txJSON = { - TransactionType: 'DepositPreauth', - Account: address, - Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', - Fee: '10', - LastLedgerSequence: 8880000 - } - - return this.api.prepareTransaction(txJSON, localInstructions).then(response => { - const expected = { - txJSON: '{"TransactionType":"DepositPreauth","Account":"' + address + '","Authorize":"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo","Flags":2147483648,"LastLedgerSequence":8880000,"Fee":"10","Sequence":23}', - instructions: { - fee: '0.00001', // Notice there are not always 6 digits after the decimal point as trailing zeros are omitted - sequence: 23, - maxLedgerVersion: 8880000 - } - } - return checkResult(expected, 'prepare', response) - }) - }) - - it('does not overwrite maxLedgerVersion in Instructions', function () { - const localInstructions = { - "maxLedgerVersion": 8890000 - } - - const txJSON = { - TransactionType: 'DepositPreauth', - Account: address, - Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo' - } - - return this.api.prepareTransaction(txJSON, localInstructions).then(response => { - const expected = { - txJSON: '{"TransactionType":"DepositPreauth","Account":"' + address + '","Authorize":"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo","Flags":2147483648,"LastLedgerSequence":8890000,"Fee":"12","Sequence":23}', - instructions: { - fee: '0.000012', - sequence: 23, - maxLedgerVersion: 8890000 - } - } - return checkResult(expected, 'prepare', response) - }) - }) - - it('does not overwrite maxLedgerVersionOffset in Instructions', function () { - const localInstructions = _.defaults({ - maxLedgerVersionOffset: 124 - }, instructionsWithMaxLedgerVersionOffset) - - const txJSON = { - TransactionType: 'DepositPreauth', - Account: address, - Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo' - } - - return this.api.prepareTransaction(txJSON, localInstructions).then(response => { - const expected = { - txJSON: '{"TransactionType":"DepositPreauth","Account":"' + address + '","Authorize":"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo","Flags":2147483648,"LastLedgerSequence":8820075,"Fee":"12","Sequence":23}', - instructions: { - fee: '0.000012', - sequence: 23, - maxLedgerVersion: 8820075 - } - } - return checkResult(expected, 'prepare', response) - }) - }) - - it('rejects Promise if txJSON.LastLedgerSequence and instructions.maxLedgerVersion both are set', function (done) { - const localInstructions = { - maxLedgerVersion: 8900000 - } - - const txJSON = { - TransactionType: 'DepositPreauth', - Account: address, - Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', - Fee: '16', - LastLedgerSequence: 8900000 - } - - try { - this.api.prepareTransaction(txJSON, localInstructions).then(response => { - done(new Error('Expected method to reject. Prepared transaction: ' + JSON.stringify(response))); - }).catch(err => { - assert.strictEqual(err.name, 'ValidationError'); - assert.strictEqual(err.message, '`LastLedgerSequence` in txJSON and `maxLedgerVersion` in `instructions` cannot both be set'); - done(); - }).catch(done); // Finish test with assertion failure immediately instead of waiting for timeout. - } catch (err) { - done(new Error('Expected method to reject, but method threw. Thrown: ' + err)); - } - }) - - it('rejects Promise if txJSON.LastLedgerSequence and instructions.maxLedgerVersionOffset both are set', function (done) { - const localInstructions = _.defaults({ - maxLedgerVersionOffset: 123 - }, instructionsWithMaxLedgerVersionOffset) - - const txJSON = { - TransactionType: 'DepositPreauth', - Account: address, - Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', - Fee: '16', - LastLedgerSequence: 8900000 - } - - try { - this.api.prepareTransaction(txJSON, localInstructions).then(response => { - done(new Error('Expected method to reject. Prepared transaction: ' + JSON.stringify(response))); - }).catch(err => { - assert.strictEqual(err.name, 'ValidationError'); - assert.strictEqual(err.message, '`LastLedgerSequence` in txJSON and `maxLedgerVersionOffset` in `instructions` cannot both be set'); - done(); - }).catch(done); // Finish test with assertion failure immediately instead of waiting for timeout. - } catch (err) { - done(new Error('Expected method to reject, but method threw. Thrown: ' + err)); - } - }) - - it('rejects Promise if instructions.maxLedgerVersion and instructions.maxLedgerVersionOffset both are set', function (done) { - const localInstructions = _.defaults({ - maxLedgerVersion: 8900000, - maxLedgerVersionOffset: 123 - }, instructionsWithMaxLedgerVersionOffset) - - const txJSON = { - TransactionType: 'DepositPreauth', - Account: address, - Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', - Fee: '16' - } - - try { - this.api.prepareTransaction(txJSON, localInstructions).then(response => { - done(new Error('Expected method to reject. Prepared transaction: ' + JSON.stringify(response))); - }).catch(err => { - assert.strictEqual(err.name, 'ValidationError'); - assert.strictEqual(err.message, 'instance is of prohibited type [object Object]'); - // A better error message would be: '`maxLedgerVersion` in `instructions` and `maxLedgerVersionOffset` in `instructions` cannot both be set' - // Unfortunately, due to the schema validator, this is not possible without special-casing. - done(); - }).catch(done); // Finish test with assertion failure immediately instead of waiting for timeout. - } catch (err) { - done(new Error('Expected method to reject, but method threw. Thrown: ' + err)); - } - }) - - it('rejects Promise if txJSON.LastLedgerSequence and instructions.maxLedgerVersion and instructions.maxLedgerVersionOffset all are set', function (done) { - const localInstructions = _.defaults({ - maxLedgerVersion: 8900000, - maxLedgerVersionOffset: 123 - }, instructionsWithMaxLedgerVersionOffset) - - const txJSON = { - TransactionType: 'DepositPreauth', - Account: address, - Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', - Fee: '16', - LastLedgerSequence: 8900000 - } - - try { - this.api.prepareTransaction(txJSON, localInstructions).then(response => { - done(new Error('Expected method to reject. Prepared transaction: ' + JSON.stringify(response))); - }).catch(err => { - assert.strictEqual(err.name, 'ValidationError'); - assert.strictEqual(err.message, 'instance is of prohibited type [object Object]'); - // A better error message would be: 'At most one of the following can be set: `LastLedgerSequence` in txJSON, `maxLedgerVersion` in `instructions`, `maxLedgerVersionOffset` in `instructions`' - // Unfortunately, due to the schema validator, this is not possible without special-casing. - done(); - }).catch(done); // Finish test with assertion failure immediately instead of waiting for timeout. - } catch (err) { - done(new Error('Expected method to reject, but method threw. Thrown: ' + err)); - } - }) - - it('rejects Promise when the maxLedgerVersion is capitalized in Instructions', function (done) { - const localInstructions = _.defaults({ - MaxLedgerVersion: 8900000, // Intentionally capitalized in this test, but the correct field would be `maxLedgerVersion` - }, instructionsWithMaxLedgerVersionOffset) - - const txJSON = { - TransactionType: 'DepositPreauth', - Account: address, - Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo' - } - - try { - this.api.prepareTransaction(txJSON, localInstructions).then(response => { - done(new Error('Expected method to reject. Prepared transaction: ' + JSON.stringify(response))); - }).catch(err => { - assert.strictEqual(err.name, 'ValidationError'); - assert.strictEqual(err.message, 'instance additionalProperty "MaxLedgerVersion" exists in instance when not allowed'); - done(); - }).catch(done); // Finish test with assertion failure immediately instead of waiting for timeout. - } catch (err) { - done(new Error('Expected method to reject, but method threw. Thrown: ' + err)); - } - }) - - it('rejects Promise when the maxLedgerVersion is specified in txJSON', function (done) { - const localInstructions = instructionsWithMaxLedgerVersionOffset - - const txJSON = { - TransactionType: 'DepositPreauth', - Account: address, - Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', - maxLedgerVersion: 8900000 - } - - try { - this.api.prepareTransaction(txJSON, localInstructions).then(response => { - done(new Error('Expected method to reject. Prepared transaction: ' + JSON.stringify(response))); - }).catch(err => { - assert.strictEqual(err.name, 'ValidationError'); - assert.strictEqual(err.message, 'txJSON additionalProperty "maxLedgerVersion" exists in instance when not allowed'); - done(); - }).catch(done); // Finish test with assertion failure immediately instead of waiting for timeout. - } catch (err) { - done(new Error('Expected method to reject, but method threw. Thrown: ' + err)); - } - }) - - it('rejects Promise when the maxLedgerVersionOffset is specified in txJSON', function (done) { - const localInstructions = instructionsWithMaxLedgerVersionOffset - - const txJSON = { - TransactionType: 'DepositPreauth', - Account: address, - Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', - maxLedgerVersionOffset: 8900000 - } - - try { - this.api.prepareTransaction(txJSON, localInstructions).then(response => { - done(new Error('Expected method to reject. Prepared transaction: ' + JSON.stringify(response))); - }).catch(err => { - assert.strictEqual(err.name, 'ValidationError'); - assert.strictEqual(err.message, 'txJSON additionalProperty "maxLedgerVersionOffset" exists in instance when not allowed'); - done(); - }).catch(done); // Finish test with assertion failure immediately instead of waiting for timeout. - } catch (err) { - done(new Error('Expected method to reject, but method threw. Thrown: ' + err)); - } - }) - - it('rejects Promise when the sequence is specified in txJSON', function (done) { - const localInstructions = instructionsWithMaxLedgerVersionOffset - - const txJSON = { - TransactionType: 'DepositPreauth', - Account: address, - Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', - sequence: 8900000 - } - - try { - this.api.prepareTransaction(txJSON, localInstructions).then(response => { - done(new Error('Expected method to reject. Prepared transaction: ' + JSON.stringify(response))); - }).catch(err => { - assert.strictEqual(err.name, 'ValidationError'); - assert.strictEqual(err.message, 'txJSON additionalProperty "sequence" exists in instance when not allowed'); - done(); - }).catch(done); // Finish test with assertion failure immediately instead of waiting for timeout. - } catch (err) { - done(new Error('Expected method to reject, but method threw. Thrown: ' + err)); - } - }) - - // Paths: is not auto-filled by ripple-lib. - - // Other errors: - - it('rejects Promise when an unrecognized field is in Instructions', function (done) { - const localInstructions = _.defaults({ - maxFee: '0.000012', - foo: 'bar' - }, instructionsWithMaxLedgerVersionOffset) - - const txJSON = { - TransactionType: 'DepositPreauth', - Account: address, - Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo' - } - - try { - this.api.prepareTransaction(txJSON, localInstructions).then(response => { - done(new Error('Expected method to reject. Prepared transaction: ' + JSON.stringify(response))); - }).catch(err => { - assert.strictEqual(err.name, 'ValidationError'); - assert.strictEqual(err.message, 'instance additionalProperty "foo" exists in instance when not allowed'); - done(); - }).catch(done); // Finish test with assertion failure immediately instead of waiting for timeout. - } catch (err) { - done(new Error('Expected method to reject, but method threw. Thrown: ' + err)); - } - }) - }) - - it('rejects Promise when Account is missing', function (done) { - const localInstructions = _.defaults({ - maxFee: '0.000012' - }, instructionsWithMaxLedgerVersionOffset) - - const txJSON = { - TransactionType: 'DepositPreauth', - Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo' - } - - try { - this.api.prepareTransaction(txJSON, localInstructions).then(response => { - done(new Error('Expected method to reject. Prepared transaction: ' + JSON.stringify(response))); - }).catch(err => { - // assert.strictEqual(err.name, 'RippledError'); - // assert.strictEqual(err.message, 'Missing field \'account\'.'); - // assert.strictEqual(err.data.error, 'invalidParams'); - assert.strictEqual(err.name, 'ValidationError'); - assert.strictEqual(err.message, 'instance requires property "Account"'); - done(); - }).catch(done); // Finish test with assertion failure immediately instead of waiting for timeout. - } catch (err) { - done(new Error('Expected method to reject, but method threw. Thrown: ' + err)); - } - }) - - it('rejects Promise when Account is not a string', function (done) { - const localInstructions = _.defaults({ - maxFee: '0.000012' - }, instructionsWithMaxLedgerVersionOffset) - - const txJSON = { - Account: 1234, - TransactionType: 'DepositPreauth', - Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo' - } - - try { - this.api.prepareTransaction(txJSON, localInstructions).then(response => { - done(new Error('Expected method to reject. Prepared transaction: ' + JSON.stringify(response))); - }).catch(err => { - assert.strictEqual(err.name, 'ValidationError'); - assert.strictEqual(err.message, 'instance.Account is not of a type(s) string,instance.Account is not exactly one from ,'); - done(); - }).catch(done); // Finish test with assertion failure immediately instead of waiting for timeout. - } catch (err) { - done(new Error('Expected method to reject, but method threw. Thrown: ' + err)); - } - }) - - it('rejects Promise when Account is invalid', function (done) { - const localInstructions = _.defaults({ - maxFee: '0.000012' - }, instructionsWithMaxLedgerVersionOffset) - - const txJSON = { - Account: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xkXXXX', // Invalid checksum - TransactionType: 'DepositPreauth', - Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo' - } - - try { - this.api.prepareTransaction(txJSON, localInstructions).then(response => { - done(new Error('Expected method to reject. Prepared transaction: ' + JSON.stringify(response))); - }).catch(err => { - assert.strictEqual(err.name, 'ValidationError'); - assert.strictEqual(err.message, 'instance.Account is not exactly one from ,'); - done(); - }).catch(done); // Finish test with assertion failure immediately instead of waiting for timeout. - } catch (err) { - done(new Error('Expected method to reject, but method threw. Thrown: ' + err)); - } - }) - - it('rejects Promise when Account is valid but non-existent on the ledger', function (done) { - const localInstructions = _.defaults({ - maxFee: '0.000012' - }, instructionsWithMaxLedgerVersionOffset) - - const txJSON = { - Account: 'rogvkYnY8SWjxkJNgU4ZRVfLeRyt5DR9i', - TransactionType: 'DepositPreauth', - Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo' - } - - try { - this.api.prepareTransaction(txJSON, localInstructions).then(response => { - done(new Error('Expected method to reject. Prepared transaction: ' + JSON.stringify(response))); - }).catch(err => { - assert.strictEqual(err.name, 'RippledError'); - assert.strictEqual(err.message, 'Account not found.'); - done(); - }).catch(done); // Finish test with assertion failure immediately instead of waiting for timeout. - } catch (err) { - done(new Error('Expected method to reject, but method threw. Thrown: ' + err)); - } - }) - - it('rejects Promise when TransactionType is missing', function (done) { - const localInstructions = _.defaults({ - maxFee: '0.000012' - }, instructionsWithMaxLedgerVersionOffset) - - const txJSON = { - Account: address, - Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo' - } - - try { - this.api.prepareTransaction(txJSON, localInstructions).then(response => { - done(new Error('Expected method to reject. Prepared transaction: ' + JSON.stringify(response))); - }).catch(err => { - // If not caught by ripple-lib validation, the rippled error looks like: - // { error: 'invalidTransaction', - // error_exception: 'Field not found', - // id: 4, - // request: - // { command: 'submit', - // id: 4, - // tx_blob: '24000000032B7735940068400000000000000C732102E1EA8199F570E7F997A7B34EDFDA0A7D8B38173A17450B121A2EB048FDD16CA97446304402206CE34A79A44AEF15786F23DB25C8420E739C167E66750C0B7999EE4BF74A93A1022052E077A6435548F0EE0C5FE2EAB1E5A56376BA360F924DA2E162CCA6C7CB30CB8114D51F9A17208CF113AF23B97ECD5FCD314FBAE52E' }, - // status: 'error', - // type: 'response' } - assert.strictEqual(err.name, 'ValidationError'); - assert.strictEqual(err.message, 'instance requires property "TransactionType"'); - done(); - }).catch(done); // Finish test with assertion failure immediately instead of waiting for timeout. - } catch (err) { - done(new Error('Expected method to reject, but method threw. Thrown: ' + err)); - } - }) - - // Note: This transaction will fail at the `sign` step: - // - // Error: DepositPreXXXX is not a valid name or ordinal for TransactionType - // - // at Function.from (ripple-binary-codec/distrib/npm/enums/index.js:43:15) - it('prepares tx when TransactionType is invalid', function () { - const localInstructions = _.defaults({ - maxFee: '0.000012' - }, instructionsWithMaxLedgerVersionOffset) - - const txJSON = { - Account: address, - TransactionType: 'DepositPreXXXX', - Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo' - } - - return this.api.prepareTransaction(txJSON, localInstructions).then(response => { - const expected = { - txJSON: '{"TransactionType":"DepositPreXXXX","Account":"' + address + '","Authorize":"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo","Flags":2147483648,"LastLedgerSequence":8820051,"Fee":"12","Sequence":23}', - instructions: { - fee: '0.000012', - sequence: 23, - maxLedgerVersion: 8820051 - } - } - return checkResult(expected, 'prepare', response) - }) - }) - - it('rejects Promise when TransactionType is not a string', function (done) { - const localInstructions = _.defaults({ - maxFee: '0.000012' - }, instructionsWithMaxLedgerVersionOffset) - - const txJSON = { - Account: address, - TransactionType: 1234, - Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo' - } - - try { - this.api.prepareTransaction(txJSON, localInstructions).then(response => { - done(new Error('Expected method to reject. Prepared transaction: ' + JSON.stringify(response))); - }).catch(err => { - assert.strictEqual(err.name, 'ValidationError'); - assert.strictEqual(err.message, 'instance.TransactionType is not of a type(s) string'); - done(); - }).catch(done); // Finish test with assertion failure immediately instead of waiting for timeout. - } catch (err) { - done(new Error('Expected method to reject, but method threw. Thrown: ' + err)); - } - }) - - // Note: This transaction will fail at the `submit` step: - // - // [RippledError(Submit failed, { resultCode: 'temMALFORMED', - // resultMessage: 'Malformed transaction.', - // engine_result: 'temMALFORMED', - // engine_result_code: -299, - // engine_result_message: 'Malformed transaction.', - // tx_blob: - // '120013240000000468400000000000000C732102E1EA8199F570E7F997A7B34EDFDA0A7D8B38173A17450B121A2EB048FDD16CA97446304402201F0EF6A2DE7F96966F7082294D14F3EC1EF59C21E29443E5858A0120079357A302203CDB7FEBDEAAD93FF39CB589B55778CB80DC3979F96F27E828D5E659BEB26B7A8114D51F9A17208CF113AF23B97ECD5FCD314FBAE52E', - // tx_json: - // { Account: 'rLRt8bmZFBEeM5VMSxZy15k8KKJEs68W6C', - // Fee: '12', - // Sequence: 4, - // SigningPubKey: - // '02E1EA8199F570E7F997A7B34EDFDA0A7D8B38173A17450B121A2EB048FDD16CA9', - // TransactionType: 'DepositPreauth', - // TxnSignature: - // '304402201F0EF6A2DE7F96966F7082294D14F3EC1EF59C21E29443E5858A0120079357A302203CDB7FEBDEAAD93FF39CB589B55778CB80DC3979F96F27E828D5E659BEB26B7A', - // hash: - // 'C181D470684311658852713DA81F8201062535C8DE2FF853F7DD9981BB85312F' } })] - it('prepares tx when a required field is missing', function () { - const localInstructions = _.defaults({ - maxFee: '0.000012' - }, instructionsWithMaxLedgerVersionOffset) - - const txJSON = { - Account: address, - TransactionType: 'DepositPreauth', - // Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo' // Normally required, intentionally removed - } - - return this.api.prepareTransaction(txJSON, localInstructions).then(response => { - const expected = { - txJSON: '{"TransactionType":"DepositPreauth","Account":"' + address + '","Flags":2147483648,"LastLedgerSequence":8820051,"Fee":"12","Sequence":23}', - instructions: { - fee: '0.000012', - sequence: 23, - maxLedgerVersion: 8820051 - } - } - return checkResult(expected, 'prepare', response) - }) - }) - - describe('preparePayment', function () { - - it('normal', function () { - const localInstructions = _.defaults({ - maxFee: '0.000012' - }, instructionsWithMaxLedgerVersionOffset); - return this.api.preparePayment( - address, requests.preparePayment.normal, localInstructions).then( - _.partial(checkResult, responses.preparePayment.normal, 'prepare')); - }); - - it('preparePayment - min amount xrp', function () { - const localInstructions = _.defaults({ - maxFee: '0.000012' - }, instructionsWithMaxLedgerVersionOffset); - return this.api.preparePayment( - address, requests.preparePayment.minAmountXRP, localInstructions).then( - _.partial(checkResult, - responses.preparePayment.minAmountXRP, 'prepare')); - }); - - it('preparePayment - min amount xrp2xrp', function () { - return this.api.preparePayment( - address, requests.preparePayment.minAmount, instructionsWithMaxLedgerVersionOffset).then( - _.partial(checkResult, - responses.preparePayment.minAmountXRPXRP, 'prepare')); - }); - - it('preparePayment - XRP to XRP', function () { - const payment = { - "source": { - "address": "r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59", - "maxAmount": { - "value": "1", - "currency": "XRP" - } - }, - "destination": { - "address": "rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo", - "amount": { - "value": "1", - "currency": "XRP" - } - } - } - return this.api.preparePayment(address, payment, instructionsWithMaxLedgerVersionOffset).then(response => { - const expected = { - txJSON: '{"TransactionType":"Payment","Account":"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59","Destination":"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo","Amount":"1000000","Flags":2147483648,"LastLedgerSequence":8820051,"Sequence":23,"Fee":"12"}', - instructions: { - fee: '0.000012', - sequence: 23, - maxLedgerVersion: 8820051 - } - } - return checkResult(expected, 'prepare', response) - }) - }); - - it('preparePayment - XRP drops to XRP drops', function () { - const payment = { - "source": { - "address": "r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59", - "maxAmount": { - "value": "1000000", - "currency": "drops" - } - }, - "destination": { - "address": "rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo", - "amount": { - "value": "1000000", - "currency": "drops" - } - } - } - return this.api.preparePayment(address, payment, instructionsWithMaxLedgerVersionOffset).then(response => { - const expected = { - txJSON: '{"TransactionType":"Payment","Account":"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59","Destination":"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo","Amount":"1000000","Flags":2147483648,"LastLedgerSequence":8820051,"Sequence":23,"Fee":"12"}', - instructions: { - fee: '0.000012', - sequence: 23, - maxLedgerVersion: 8820051 - } - } - return checkResult(expected, 'prepare', response) - }) - }); - - it('preparePayment - XRP drops to XRP', function () { - const payment = { - "source": { - "address": "r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59", - "maxAmount": { - "value": "1000000", - "currency": "drops" - } - }, - "destination": { - "address": "rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo", - "amount": { - "value": "1", - "currency": "XRP" - } - } - } - return this.api.preparePayment(address, payment, instructionsWithMaxLedgerVersionOffset).then(response => { - const expected = { - txJSON: '{"TransactionType":"Payment","Account":"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59","Destination":"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo","Amount":"1000000","Flags":2147483648,"LastLedgerSequence":8820051,"Sequence":23,"Fee":"12"}', - instructions: { - fee: '0.000012', - sequence: 23, - maxLedgerVersion: 8820051 - } - } - return checkResult(expected, 'prepare', response) - }) - }); - - it('preparePayment - XRP to XRP drops', function () { - const payment = { - "source": { - "address": "r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59", - "maxAmount": { - "value": "1", - "currency": "XRP" - } - }, - "destination": { - "address": "rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo", - "amount": { - "value": "1000000", - "currency": "drops" - } - } - } - return this.api.preparePayment(address, payment, instructionsWithMaxLedgerVersionOffset).then(response => { - const expected = { - txJSON: '{"TransactionType":"Payment","Account":"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59","Destination":"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo","Amount":"1000000","Flags":2147483648,"LastLedgerSequence":8820051,"Sequence":23,"Fee":"12"}', - instructions: { - fee: '0.000012', - sequence: 23, - maxLedgerVersion: 8820051 - } - } - return checkResult(expected, 'prepare', response) - }) - }); - - describe('errors', function () { - - const senderAddress = 'r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59'; - const recipientAddress = 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo'; - - it('rejects promise and does not throw when payment object is invalid', function (done) { - const payment = { - source: { - address: senderAddress, - amount: { // instead of `maxAmount` - value: '1000', - currency: 'drops' - } - }, - destination: { - address: recipientAddress, - amount: { - value: '1000', - currency: 'drops' - } - } - } - // Cannot use `assert.rejects` because then the test passes (with UnhandledPromiseRejectionWarning) even when it should not. - // See https://github.com/mochajs/mocha/issues/3097 - try { - this.api.preparePayment(senderAddress, payment).then(prepared => { - done(new Error('Expected method to reject. Prepared transaction: ' + JSON.stringify(prepared))); - }).catch(err => { - assert.strictEqual(err.name, 'ValidationError'); - assert.strictEqual(err.message, 'payment must specify either (source.maxAmount and destination.amount) or (source.amount and destination.minAmount)'); - done(); - }).catch(done); // Finish test with assertion failure immediately instead of waiting for timeout. - } catch (err) { - done(new Error('Expected method to reject, but method threw. Thrown: ' + err)); - } - }); - - it('rejects promise and does not throw when field is missing', function (done) { - const payment = { - source: { - address: senderAddress - // `maxAmount` missing - }, - destination: { - address: recipientAddress, - amount: { - value: '1000', - currency: 'drops' - } - } - } - // Cannot use `assert.rejects` because then the test passes (with UnhandledPromiseRejectionWarning) even when it should not. - // See https://github.com/mochajs/mocha/issues/3097 - try { - this.api.preparePayment(senderAddress, payment).then(prepared => { - done(new Error('Expected method to reject. Prepared transaction: ' + JSON.stringify(prepared))); - }).catch(err => { - assert.strictEqual(err.name, 'ValidationError'); - assert.strictEqual(err.message, 'instance.payment.source is not exactly one from ,'); - done(); - }).catch(done); // Finish test with assertion failure immediately instead of waiting for timeout. - } catch (err) { - done(new Error('Expected method to reject, but method threw. Thrown: ' + err)); - } - }); - - it('rejects promise and does not throw when fee exceeds maxFeeXRP', function (done) { - const payment = { - source: { - address: senderAddress, - maxAmount: { - value: '1000', - currency: 'drops' - } - }, - destination: { - address: recipientAddress, - amount: { - value: '1000', - currency: 'drops' - } - } - } - // Cannot use `assert.rejects` because then the test passes (with UnhandledPromiseRejectionWarning) even when it should not. - // See https://github.com/mochajs/mocha/issues/3097 - try { - this.api.preparePayment(senderAddress, payment, { - fee: '3' // XRP - }).then(prepared => { - done(new Error('Expected method to reject. Prepared transaction: ' + JSON.stringify(prepared))); - }).catch(err => { - assert.strictEqual(err.name, 'ValidationError'); - assert.strictEqual(err.message, 'Fee of 3 XRP exceeds max of 2 XRP. To use this fee, increase `maxFeeXRP` in the RippleAPI constructor.'); - done(); - }).catch(done); // Finish test with assertion failure immediately instead of waiting for timeout. - } catch (err) { - done(new Error('Expected method to reject, but method threw. Thrown: ' + err)); - } - }); - - it('preparePayment - XRP to XRP no partial', function (done) { - try { - // Cannot return promise because we want/expect it to reject. - this.api.preparePayment(address, requests.preparePayment.wrongPartial).then(prepared => { - done(new Error('Expected method to reject. Prepared transaction: ' + JSON.stringify(prepared))); - }).catch(err => { - assert.strictEqual(err.name, 'ValidationError'); - assert.strictEqual(err.message, 'XRP to XRP payments cannot be partial payments'); - done(); - }).catch(done); // Finish test with assertion failure immediately instead of waiting for timeout. - } catch (err) { - done(new Error('Expected method to reject, but method threw. Thrown: ' + err)); - } - }); - - it('preparePayment - address must match payment.source.address', function (done) { - try { - // Cannot return promise because we want/expect it to reject. - this.api.preparePayment(address, requests.preparePayment.wrongAddress).then(prepared => { - done(new Error('Expected method to reject. Prepared transaction: ' + JSON.stringify(prepared))); - }).catch(err => { - assert.strictEqual(err.name, 'ValidationError'); - assert.strictEqual(err.message, 'address must match payment.source.address'); - done(); - }).catch(done); // Finish test with assertion failure immediately instead of waiting for timeout. - } catch (err) { - done(new Error('Expected method to reject, but method threw. Thrown: ' + err)); - } - }); - - it('preparePayment - wrong amount', function (done) { - try { - // Cannot return promise because we want/expect it to reject. - this.api.preparePayment(address, requests.preparePayment.wrongAmount).then(prepared => { - done(new Error('Expected method to reject. Prepared transaction: ' + JSON.stringify(prepared))); - }).catch(err => { - assert.strictEqual(err.name, 'ValidationError'); - assert.strictEqual(err.message, 'payment must specify either (source.maxAmount and destination.amount) or (source.amount and destination.minAmount)'); - done(); - }).catch(done); // Finish test with assertion failure immediately instead of waiting for timeout. - } catch (err) { - done(new Error('Expected method to reject, but method threw. Thrown: ' + err)); - } - }); - - it('preparePayment - throws when fee exceeds 2 XRP', function (done) { - const localInstructions = _.defaults({ - fee: '2.1' - }, instructionsWithMaxLedgerVersionOffset); - - try { - // Cannot return promise because we want/expect it to reject. - this.api.preparePayment( - address, requests.preparePayment.normal, localInstructions).then(prepared => { - done(new Error('Expected method to reject. Prepared transaction: ' + JSON.stringify(prepared))); - }).catch(err => { - assert.strictEqual(err.name, 'ValidationError'); - assert.strictEqual(err.message, 'Fee of 2.1 XRP exceeds max of 2 XRP. To use this fee, increase `maxFeeXRP` in the RippleAPI constructor.'); - done(); - }).catch(done); // Finish test with assertion failure immediately instead of waiting for timeout. - } catch (err) { - done(new Error('Expected method to reject, but method threw. Thrown: ' + err)); - } - }); - }); - - it('preparePayment with all options specified', function () { - return this.api.getLedgerVersion().then(ver => { - const localInstructions = { - maxLedgerVersion: ver + 100, - fee: '0.000012' - }; - return this.api.preparePayment( - address, requests.preparePayment.allOptions, localInstructions).then( - _.partial(checkResult, - responses.preparePayment.allOptions, 'prepare')); - }); - }); - - it('preparePayment without counterparty set', function () { - const localInstructions = _.defaults({ sequence: 23 }, instructionsWithMaxLedgerVersionOffset); - return this.api.preparePayment( - address, requests.preparePayment.noCounterparty, localInstructions) - .then(_.partial(checkResult, responses.preparePayment.noCounterparty, - 'prepare')); - }); - - it('preparePayment - destination.minAmount', function () { - return this.api.preparePayment(address, responses.getPaths.sendAll[0], - instructionsWithMaxLedgerVersionOffset).then(_.partial(checkResult, - responses.preparePayment.minAmount, 'prepare')); - }); - - it('preparePayment - caps fee at 2 XRP by default', function () { - this.api._feeCushion = 1000000; - - const expectedResponse = { - "txJSON": "{\"Flags\":2147483648,\"TransactionType\":\"Payment\",\"Account\":\"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59\",\"Destination\":\"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo\",\"Amount\":{\"value\":\"0.01\",\"currency\":\"USD\",\"issuer\":\"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM\"},\"SendMax\":{\"value\":\"0.01\",\"currency\":\"USD\",\"issuer\":\"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM\"},\"LastLedgerSequence\":8820051,\"Fee\":\"2000000\",\"Sequence\":23}", - "instructions": { - "fee": "2", - "sequence": 23, - "maxLedgerVersion": 8820051 - } - } - - return this.api.preparePayment( - address, requests.preparePayment.normal, instructionsWithMaxLedgerVersionOffset).then( - _.partial(checkResult, expectedResponse, 'prepare')); - }); - - it('preparePayment - allows fee exceeding 2 XRP when maxFeeXRP is higher', function () { - this.api._maxFeeXRP = '2.2' - const localInstructions = _.defaults({ - fee: '2.1' - }, instructionsWithMaxLedgerVersionOffset); - - const expectedResponse = { - "txJSON": "{\"Flags\":2147483648,\"TransactionType\":\"Payment\",\"Account\":\"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59\",\"Destination\":\"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo\",\"Amount\":{\"value\":\"0.01\",\"currency\":\"USD\",\"issuer\":\"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM\"},\"SendMax\":{\"value\":\"0.01\",\"currency\":\"USD\",\"issuer\":\"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM\"},\"LastLedgerSequence\":8820051,\"Fee\":\"2100000\",\"Sequence\":23}", - "instructions": { - "fee": "2.1", - "sequence": 23, - "maxLedgerVersion": 8820051 - } - } - - return this.api.preparePayment( - address, requests.preparePayment.normal, localInstructions).then( - _.partial(checkResult, expectedResponse, 'prepare')); - }); - }); - - it('prepareOrder - buy order', function () { - const request = requests.prepareOrder.buy; - return this.api.prepareOrder(address, request) - .then(_.partial(checkResult, responses.prepareOrder.buy, 'prepare')); - }); - - it('prepareOrder - buy order with expiration', function () { - const request = requests.prepareOrder.expiration; - const response = responses.prepareOrder.expiration; - return this.api.prepareOrder(address, request, instructionsWithMaxLedgerVersionOffset) - .then(_.partial(checkResult, response, 'prepare')); - }); - - it('prepareOrder - sell order', function () { - const request = requests.prepareOrder.sell; - return this.api.prepareOrder(address, request, instructionsWithMaxLedgerVersionOffset).then( - _.partial(checkResult, responses.prepareOrder.sell, 'prepare')); - }); - - it('prepareOrder - invalid', function (done) { - const request = Object.assign({}, requests.prepareOrder.sell); - delete request.direction; // Make invalid - try { - this.api.prepareOrder(address, request, instructionsWithMaxLedgerVersionOffset).then(prepared => { - done(new Error('Expected method to reject. Prepared transaction: ' + JSON.stringify(prepared))); - }).catch(err => { - assert.strictEqual(err.name, 'ValidationError'); - assert.strictEqual(err.message, 'instance.order requires property "direction"'); - done(); - }).catch(done); // Finish test with assertion failure immediately instead of waiting for timeout. - } catch (err) { - done(new Error('Expected method to reject, but method threw. Thrown: ' + err)); - } - }); - - it('prepareOrderCancellation', function () { - const request = requests.prepareOrderCancellation.simple; - return this.api.prepareOrderCancellation(address, request, instructionsWithMaxLedgerVersionOffset) - .then(_.partial(checkResult, responses.prepareOrderCancellation.normal, - 'prepare')); - }); - - it('prepareOrderCancellation - no instructions', function () { - const request = requests.prepareOrderCancellation.simple; - return this.api.prepareOrderCancellation(address, request) - .then(_.partial(checkResult, - responses.prepareOrderCancellation.noInstructions, - 'prepare')); - }); - - it('prepareOrderCancellation - with memos', function () { - const request = requests.prepareOrderCancellation.withMemos; - return this.api.prepareOrderCancellation(address, request) - .then(_.partial(checkResult, - responses.prepareOrderCancellation.withMemos, - 'prepare')); - }); - - it('prepareOrderCancellation - invalid', function (done) { - const request = Object.assign({}, requests.prepareOrderCancellation.withMemos); - delete request.orderSequence; // Make invalid - try { - this.api.prepareOrderCancellation(address, request).then(prepared => { - done(new Error('Expected method to reject. Prepared transaction: ' + JSON.stringify(prepared))); - }).catch(err => { - assert.strictEqual(err.name, 'ValidationError'); - assert.strictEqual(err.message, 'instance.orderCancellation requires property "orderSequence"'); - done(); - }).catch(done); // Finish test with assertion failure immediately instead of waiting for timeout. - } catch (err) { - done(new Error('Expected method to reject, but method threw. Thrown: ' + err)); - } - }); - - it('prepareTrustline - simple', function () { - return this.api.prepareTrustline( - address, requests.prepareTrustline.simple, instructionsWithMaxLedgerVersionOffset).then( - _.partial(checkResult, responses.prepareTrustline.simple, 'prepare')); - }); - - it('prepareTrustline - frozen', function () { - return this.api.prepareTrustline( - address, requests.prepareTrustline.frozen).then( - _.partial(checkResult, responses.prepareTrustline.frozen, 'prepare')); - }); - - it('prepareTrustline - complex', function () { - return this.api.prepareTrustline( - address, requests.prepareTrustline.complex, instructionsWithMaxLedgerVersionOffset).then( - _.partial(checkResult, responses.prepareTrustline.complex, 'prepare')); - }); - - it('prepareTrustline - invalid', function (done) { - const trustline = Object.assign({}, requests.prepareTrustline.complex); - delete trustline.limit; // Make invalid - try { - this.api.prepareTrustline( - address, trustline, instructionsWithMaxLedgerVersionOffset).then(prepared => { - done(new Error('Expected method to reject. Prepared transaction: ' + JSON.stringify(prepared))); - }).catch(err => { - assert.strictEqual(err.name, 'ValidationError'); - assert.strictEqual(err.message, 'instance.trustline requires property "limit"'); - done(); - }).catch(done); // Finish test with assertion failure immediately instead of waiting for timeout. - } catch (err) { - done(new Error('Expected method to reject, but method threw. Thrown: ' + err)); - } - }); - - it('prepareEscrowCreation', function () { - const localInstructions = _.defaults({ - maxFee: '0.000012' - }, instructionsWithMaxLedgerVersionOffset); - return this.api.prepareEscrowCreation( - address, requests.prepareEscrowCreation.normal, - localInstructions).then( - _.partial(checkResult, responses.prepareEscrowCreation.normal, - 'prepare')); - }); - - it('prepareEscrowCreation full', function () { - return this.api.prepareEscrowCreation( - address, requests.prepareEscrowCreation.full).then( - _.partial(checkResult, responses.prepareEscrowCreation.full, - 'prepare')); - }); - - it('prepareEscrowCreation - invalid', function (done) { - const escrow = Object.assign({}, requests.prepareEscrowCreation.full); - delete escrow.amount; // Make invalid - try { - this.api.prepareEscrowCreation( - address, escrow).then(prepared => { - done(new Error('Expected method to reject. Prepared transaction: ' + JSON.stringify(prepared))); - }).catch(err => { - assert.strictEqual(err.name, 'ValidationError'); - assert.strictEqual(err.message, 'instance.escrowCreation requires property "amount"'); - done(); - }).catch(done); // Finish test with assertion failure immediately instead of waiting for timeout. - } catch (err) { - done(new Error('Expected method to reject, but method threw. Thrown: ' + err)); - } - }); - - it('prepareEscrowExecution', function () { - return this.api.prepareEscrowExecution( - address, - requests.prepareEscrowExecution.normal, instructionsWithMaxLedgerVersionOffset).then( - _.partial(checkResult, - responses.prepareEscrowExecution.normal, - 'prepare')); - }); - - it('prepareEscrowExecution - simple', function () { - return this.api.prepareEscrowExecution( - address, - requests.prepareEscrowExecution.simple).then( - _.partial(checkResult, - responses.prepareEscrowExecution.simple, - 'prepare')); - }); - - it('prepareEscrowExecution - no condition', function (done) { - try { - this.api.prepareEscrowExecution(address, - requests.prepareEscrowExecution.noCondition, instructionsWithMaxLedgerVersionOffset).then(prepared => { - done(new Error('Expected method to reject. Prepared transaction: ' + JSON.stringify(prepared))); - }).catch(err => { - assert.strictEqual(err.name, 'ValidationError'); - assert.strictEqual(err.message, '"condition" and "fulfillment" fields on EscrowFinish must only be specified together.'); - done(); - }).catch(done); // Finish test with assertion failure immediately instead of waiting for timeout. - } catch (err) { - done(new Error('Expected method to reject, but method threw. Thrown: ' + err)); - } - }); - - it('prepareEscrowExecution - no fulfillment', function (done) { - try { - this.api.prepareEscrowExecution(address, - requests.prepareEscrowExecution.noFulfillment, instructionsWithMaxLedgerVersionOffset).then(prepared => { - done(new Error('Expected method to reject. Prepared transaction: ' + JSON.stringify(prepared))); - }).catch(err => { - assert.strictEqual(err.name, 'ValidationError'); - assert.strictEqual(err.message, '"condition" and "fulfillment" fields on EscrowFinish must only be specified together.'); - done(); - }).catch(done); // Finish test with assertion failure immediately instead of waiting for timeout. - } catch (err) { - done(new Error('Expected method to reject, but method threw. Thrown: ' + err)); - } - }); - - it('prepareEscrowCancellation', function () { - return this.api.prepareEscrowCancellation( - address, - requests.prepareEscrowCancellation.normal, instructionsWithMaxLedgerVersionOffset).then( - _.partial(checkResult, - responses.prepareEscrowCancellation.normal, - 'prepare')); - }); - - it('prepareEscrowCancellation with memos', function () { - return this.api.prepareEscrowCancellation( - address, - requests.prepareEscrowCancellation.memos).then( - _.partial(checkResult, - responses.prepareEscrowCancellation.memos, - 'prepare')); - }); - - it('prepareCheckCreate', function () { - const localInstructions = _.defaults({ - maxFee: '0.000012' - }, instructionsWithMaxLedgerVersionOffset); - return this.api.prepareCheckCreate( - address, requests.prepareCheckCreate.normal, - localInstructions).then( - _.partial(checkResult, responses.prepareCheckCreate.normal, - 'prepare')); - }); - - it('prepareCheckCreate full', function () { - return this.api.prepareCheckCreate( - address, requests.prepareCheckCreate.full).then( - _.partial(checkResult, responses.prepareCheckCreate.full, - 'prepare')); - }); - - it('prepareCheckCash amount', function () { - return this.api.prepareCheckCash( - address, requests.prepareCheckCash.amount).then( - _.partial(checkResult, responses.prepareCheckCash.amount, - 'prepare')); - }); - - it('prepareCheckCash deliverMin', function () { - return this.api.prepareCheckCash( - address, requests.prepareCheckCash.deliverMin).then( - _.partial(checkResult, responses.prepareCheckCash.deliverMin, - 'prepare')); - }); - - it('prepareCheckCancel', function () { - return this.api.prepareCheckCancel( - address, requests.prepareCheckCancel.normal).then( - _.partial(checkResult, responses.prepareCheckCancel.normal, - 'prepare')); - }); - - it('prepareTransaction - DepositPreauth - Authorize', function () { - const localInstructions = _.defaults({ - maxFee: '0.000012' - }, instructionsWithMaxLedgerVersionOffset) - - const txJSON = { - TransactionType: 'DepositPreauth', - Account: address, - Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo' - } - - return this.api.prepareTransaction(txJSON, localInstructions).then(response => { - const expected = { - txJSON: '{"TransactionType":"DepositPreauth","Account":"' + address + '","Authorize":"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo","Flags":2147483648,"LastLedgerSequence":8820051,"Fee":"12","Sequence":23}', - instructions: { - fee: '0.000012', - sequence: 23, - maxLedgerVersion: 8820051 - } - } - return checkResult(expected, 'prepare', response) - }) - }) - - it('prepareTransaction - DepositPreauth - Unauthorize', function () { - const localInstructions = _.defaults({ - maxFee: '0.000012' - }, instructionsWithMaxLedgerVersionOffset) - - const txJSON = { - TransactionType: 'DepositPreauth', - Account: address, - Unauthorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo' - } - - return this.api.prepareTransaction(txJSON, localInstructions).then(response => { - const expected = { - txJSON: '{"TransactionType":"DepositPreauth","Account":"' + address + '","Unauthorize":"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo","Flags":2147483648,"LastLedgerSequence":8820051,"Fee":"12","Sequence":23}', - instructions: { - fee: '0.000012', - sequence: 23, - maxLedgerVersion: 8820051 - } - } - return checkResult(expected, 'prepare', response) - }) - }) - - describe('prepareTransaction - Payment', function () { - - it('normal', function () { - const localInstructions = _.defaults({ - maxFee: '0.000012' - }, instructionsWithMaxLedgerVersionOffset); - - const txJSON = { - TransactionType: 'Payment', - Account: address, - Destination: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', - Amount: { - currency: 'USD', - issuer: 'rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM', - value: '0.01' - }, - SendMax: { - currency: 'USD', - issuer: 'rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM', - value: '0.01' - }, - Flags: 0 - } - - return this.api.prepareTransaction(txJSON, localInstructions).then( - _.partial(checkResult, responses.preparePayment.normal, 'prepare')); - }); - - // prepareTransaction - Payment - it('min amount xrp', function () { - const localInstructions = _.defaults({ - maxFee: '0.000012' - }, instructionsWithMaxLedgerVersionOffset); - - const txJSON = { - TransactionType: 'Payment', - Account: address, - Destination: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', - - // Max amount to send. Use 100 billion XRP to - // ensure that we send the full SendMax amount. - Amount: '100000000000000000', - - SendMax: { - currency: 'USD', - issuer: 'rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM', - value: '0.01' - }, - DeliverMin: '10000', - Flags: this.api.txFlags.Payment.PartialPayment - } - - return this.api.prepareTransaction(txJSON, localInstructions).then( - _.partial(checkResult, - responses.preparePayment.minAmountXRP, 'prepare')); - }); - - // prepareTransaction - Payment - it('min amount xrp2xrp', function () { - const txJSON = { - TransactionType: 'Payment', - Account: address, - Destination: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', - Amount: '10000', - Flags: 0 - } - return this.api.prepareTransaction(txJSON, instructionsWithMaxLedgerVersionOffset).then( - _.partial(checkResult, - responses.preparePayment.minAmountXRPXRP, 'prepare')); - }); - - // prepareTransaction - Payment - it('with all options specified', function () { - return this.api.getLedgerVersion().then(ver => { - const localInstructions = { - maxLedgerVersion: ver + 100, - fee: '0.000012' - }; - const txJSON = { - TransactionType: 'Payment', - Account: address, - Destination: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', - Amount: '10000', - InvoiceID: 'A98FD36C17BE2B8511AD36DC335478E7E89F06262949F36EB88E2D683BBCC50A', - SourceTag: 14, - DestinationTag: 58, - Memos: [ - { - Memo: { - MemoType: this.api.convertStringToHex('test'), - MemoFormat: this.api.convertStringToHex('text/plain'), - MemoData: this.api.convertStringToHex('texted data') - } - } - ], - Flags: 0 | this.api.txFlags.Payment.NoRippleDirect | this.api.txFlags.Payment.LimitQuality - } - return this.api.prepareTransaction(txJSON, localInstructions).then( - _.partial(checkResult, - responses.preparePayment.allOptions, 'prepare')); - }); - }); - - // prepareTransaction - Payment - it('fee is capped at default maxFee of 2 XRP (using txJSON.LastLedgerSequence)', function () { - this.api._feeCushion = 1000000; - - const txJSON = { - "Flags": 2147483648, - "TransactionType": "Payment", - "Account": "r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59", - "Destination": "rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo", - "Amount": { - "value": "0.01", - "currency": "USD", - "issuer": "rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM" - }, - "SendMax": { - "value": "0.01", - "currency": "USD", - "issuer": "rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM" - }, - "LastLedgerSequence": 8820051 - } - - const localInstructions = {} - - const expectedResponse = { - "txJSON": "{\"Flags\":2147483648,\"TransactionType\":\"Payment\",\"Account\":\"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59\",\"Destination\":\"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo\",\"Amount\":{\"value\":\"0.01\",\"currency\":\"USD\",\"issuer\":\"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM\"},\"SendMax\":{\"value\":\"0.01\",\"currency\":\"USD\",\"issuer\":\"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM\"},\"LastLedgerSequence\":8820051,\"Fee\":\"2000000\",\"Sequence\":23}", - "instructions": { - "fee": "2", - "sequence": 23, - "maxLedgerVersion": 8820051 - } - } - - return this.api.prepareTransaction(txJSON, localInstructions).then( - _.partial(checkResult, - expectedResponse, 'prepare')); - }); - - // prepareTransaction - Payment - it('fee is capped at default maxFee of 2 XRP (using instructions.maxLedgerVersion)', function () { - this.api._feeCushion = 1000000; - - const txJSON = { - "Flags": 2147483648, - "TransactionType": "Payment", - "Account": "r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59", - "Destination": "rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo", - "Amount": { - "value": "0.01", - "currency": "USD", - "issuer": "rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM" - }, - "SendMax": { - "value": "0.01", - "currency": "USD", - "issuer": "rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM" - } - } - - const localInstructions = { - "maxLedgerVersion": 8820051 - } - - const expectedResponse = { - "txJSON": "{\"Flags\":2147483648,\"TransactionType\":\"Payment\",\"Account\":\"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59\",\"Destination\":\"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo\",\"Amount\":{\"value\":\"0.01\",\"currency\":\"USD\",\"issuer\":\"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM\"},\"SendMax\":{\"value\":\"0.01\",\"currency\":\"USD\",\"issuer\":\"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM\"},\"LastLedgerSequence\":8820051,\"Fee\":\"2000000\",\"Sequence\":23}", - "instructions": { - "fee": "2", - "sequence": 23, - "maxLedgerVersion": 8820051 - } - } - - return this.api.prepareTransaction(txJSON, localInstructions).then( - _.partial(checkResult, - expectedResponse, 'prepare')); - }); - - // prepareTransaction - Payment - it('fee is capped to custom maxFeeXRP when maxFee exceeds maxFeeXRP', function () { - this.api._feeCushion = 1000000 - this.api._maxFeeXRP = '3' - const localInstructions = { - maxFee: '4' // We are testing that this does not matter; fee is still capped to maxFeeXRP - }; - - const txJSON = { - "Flags": 2147483648, - "TransactionType": "Payment", - "Account": "r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59", - "Destination": "rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo", - "Amount": { - "value": "0.01", - "currency": "USD", - "issuer": "rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM" - }, - "SendMax": { - "value": "0.01", - "currency": "USD", - "issuer": "rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM" - }, - "LastLedgerSequence": 8820051 - } - - const expectedResponse = { - "txJSON": "{\"Flags\":2147483648,\"TransactionType\":\"Payment\",\"Account\":\"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59\",\"Destination\":\"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo\",\"Amount\":{\"value\":\"0.01\",\"currency\":\"USD\",\"issuer\":\"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM\"},\"SendMax\":{\"value\":\"0.01\",\"currency\":\"USD\",\"issuer\":\"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM\"},\"LastLedgerSequence\":8820051,\"Fee\":\"3000000\",\"Sequence\":23}", - "instructions": { - "fee": "3", - "sequence": 23, - "maxLedgerVersion": 8820051 - } - } - - return this.api.prepareTransaction(txJSON, localInstructions).then( - _.partial(checkResult, - expectedResponse, 'prepare')); - }); - - // prepareTransaction - Payment - it('fee is capped to maxFee', function () { - this.api._feeCushion = 1000000 - this.api._maxFeeXRP = '5' - const localInstructions = { - maxFee: '4' // maxFeeXRP does not matter if maxFee is lower than maxFeeXRP - }; - - const txJSON = { - "Flags": 2147483648, - "TransactionType": "Payment", - "Account": "r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59", - "Destination": "rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo", - "Amount": { - "value": "0.01", - "currency": "USD", - "issuer": "rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM" - }, - "SendMax": { - "value": "0.01", - "currency": "USD", - "issuer": "rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM" - }, - "LastLedgerSequence": 8820051, - } - - const expectedResponse = { - "txJSON": "{\"Flags\":2147483648,\"TransactionType\":\"Payment\",\"Account\":\"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59\",\"Destination\":\"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo\",\"Amount\":{\"value\":\"0.01\",\"currency\":\"USD\",\"issuer\":\"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM\"},\"SendMax\":{\"value\":\"0.01\",\"currency\":\"USD\",\"issuer\":\"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM\"},\"LastLedgerSequence\":8820051,\"Fee\":\"4000000\",\"Sequence\":23}", - "instructions": { - "fee": "4", - "sequence": 23, - "maxLedgerVersion": 8820051 - } - } - - return this.api.prepareTransaction(txJSON, localInstructions).then( - _.partial(checkResult, - expectedResponse, 'prepare')); - }); - - it('fee - calculated fee does not use more than 6 decimal places', function () { - this.api.connection._send(JSON.stringify({ - command: 'config', - data: { loadFactor: 5407.96875 } - })); - - const expectedResponse = { - "txJSON": "{\"Flags\":2147483648,\"TransactionType\":\"Payment\",\"Account\":\"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59\",\"Destination\":\"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo\",\"Amount\":{\"value\":\"0.01\",\"currency\":\"USD\",\"issuer\":\"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM\"},\"SendMax\":{\"value\":\"0.01\",\"currency\":\"USD\",\"issuer\":\"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM\"},\"LastLedgerSequence\":8820051,\"Fee\":\"64896\",\"Sequence\":23}", - "instructions": { - "fee": "0.064896", - "sequence": 23, - "maxLedgerVersion": 8820051 - } - } - - return this.api.preparePayment( - address, requests.preparePayment.normal, instructionsWithMaxLedgerVersionOffset).then( - _.partial(checkResult, expectedResponse, 'prepare')); - }); - }); - - it('prepareTransaction - PaymentChannelCreate', function () { - const localInstructions = _.defaults({ - maxFee: '0.000012' - }, instructionsWithMaxLedgerVersionOffset); - return this.api.prepareTransaction({ - Account: address, - TransactionType: 'PaymentChannelCreate', - Amount: '1000000', // 1 XRP in drops. Use a string-encoded integer. - Destination: 'rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW', - SettleDelay: 86400, - PublicKey: '32D2471DB72B27E3310F355BB33E339BF26F8392D5A93D3BC0FC3B566612DA0F0A' - // If cancelAfter is used, you must use RippleTime. - // You can use `iso8601ToRippleTime()` to convert to RippleTime. - - // Other fields are available (but not used in this test), - // including `sourceTag` and `destinationTag`. - }, localInstructions).then( - _.partial(checkResult, responses.preparePaymentChannelCreate.normal, - 'prepare')); - }); - - it('prepareTransaction - PaymentChannelCreate full', function () { - const txJSON = { - Account: address, - TransactionType: 'PaymentChannelCreate', - Amount: this.api.xrpToDrops('1'), // or '1000000' - Destination: 'rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW', - SettleDelay: 86400, - - // Ensure this is in upper case if it is not already - PublicKey: '32D2471DB72B27E3310F355BB33E339BF26F8392D5A93D3BC0FC3B566612DA0F0A'.toUpperCase(), - - CancelAfter: this.api.iso8601ToRippleTime('2017-02-17T15:04:57Z'), - SourceTag: 11747, - DestinationTag: 23480 - } - - return this.api.prepareTransaction(txJSON).then( - _.partial(checkResult, responses.preparePaymentChannelCreate.full, - 'prepare')); - }); - - it('prepareTransaction - PaymentChannelFund', function () { - const localInstructions = _.defaults({ - maxFee: '0.000012' - }, instructionsWithMaxLedgerVersionOffset); - - const txJSON = { - Account: address, - TransactionType: 'PaymentChannelFund', - Channel: 'C1AE6DDDEEC05CF2978C0BAD6FE302948E9533691DC749DCDD3B9E5992CA6198', - Amount: this.api.xrpToDrops('1') // or '1000000' - } - - return this.api.prepareTransaction(txJSON, localInstructions).then( - _.partial(checkResult, responses.preparePaymentChannelFund.normal, - 'prepare')); - }); - - it('prepareTransaction - PaymentChannelFund full', function () { - const txJSON = { - Account: address, - TransactionType: 'PaymentChannelFund', - Channel: 'C1AE6DDDEEC05CF2978C0BAD6FE302948E9533691DC749DCDD3B9E5992CA6198', - Amount: this.api.xrpToDrops('1'), // or '1000000' - Expiration: this.api.iso8601ToRippleTime('2017-02-17T15:04:57Z') - } - - return this.api.prepareTransaction(txJSON).then( - _.partial(checkResult, responses.preparePaymentChannelFund.full, - 'prepare')); - }); - - it('prepareTransaction - PaymentChannelClaim', function () { - const localInstructions = _.defaults({ - maxFee: '0.000012' - }, instructionsWithMaxLedgerVersionOffset); - - const txJSON = { - Account: address, - TransactionType: 'PaymentChannelClaim', - Channel: 'C1AE6DDDEEC05CF2978C0BAD6FE302948E9533691DC749DCDD3B9E5992CA6198', - Flags: 0 - } - - return this.api.prepareTransaction(txJSON, localInstructions).then( - _.partial(checkResult, responses.preparePaymentChannelClaim.normal, - 'prepare')); - }); - - it('prepareTransaction - PaymentChannelClaim with renew', function () { - const localInstructions = _.defaults({ - maxFee: '0.000012' - }, instructionsWithMaxLedgerVersionOffset); - - const txJSON = { - Account: address, - TransactionType: 'PaymentChannelClaim', - Channel: 'C1AE6DDDEEC05CF2978C0BAD6FE302948E9533691DC749DCDD3B9E5992CA6198', - Balance: this.api.xrpToDrops('1'), // or '1000000' - Amount: this.api.xrpToDrops('1'), // or '1000000' - Signature: '30440220718D264EF05CAED7C781FF6DE298DCAC68D002562C9BF3A07C1E721B420C0DAB02203A5A4779EF4D2CCC7BC3EF886676D803A9981B928D3B8ACA483B80ECA3CD7B9B', - PublicKey: '32D2471DB72B27E3310F355BB33E339BF26F8392D5A93D3BC0FC3B566612DA0F0A', - Flags: 0 - } - txJSON.Flags |= this.api.txFlags.PaymentChannelClaim.Renew - - return this.api.prepareTransaction(txJSON, localInstructions).then( - _.partial(checkResult, responses.preparePaymentChannelClaim.renew, - 'prepare')); - }); - - it('prepareTransaction - PaymentChannelClaim with close', function () { - const localInstructions = _.defaults({ - maxFee: '0.000012' - }, instructionsWithMaxLedgerVersionOffset); - - const txJSON = { - Account: address, - TransactionType: 'PaymentChannelClaim', - Channel: 'C1AE6DDDEEC05CF2978C0BAD6FE302948E9533691DC749DCDD3B9E5992CA6198', - Balance: this.api.xrpToDrops('1'), // or 1000000 - Amount: this.api.xrpToDrops('1'), // or 1000000 - Signature: '30440220718D264EF05CAED7C781FF6DE298DCAC68D002562C9BF3A07C1E721B420C0DAB02203A5A4779EF4D2CCC7BC3EF886676D803A9981B928D3B8ACA483B80ECA3CD7B9B', - PublicKey: '32D2471DB72B27E3310F355BB33E339BF26F8392D5A93D3BC0FC3B566612DA0F0A', - Flags: 0 - } - txJSON.Flags |= this.api.txFlags.PaymentChannelClaim.Close - - return this.api.prepareTransaction(txJSON, localInstructions).then( - _.partial(checkResult, responses.preparePaymentChannelClaim.close, - 'prepare')); - }); - - it('preparePaymentChannelCreate', function () { - const localInstructions = _.defaults({ - maxFee: '0.000012' - }, instructionsWithMaxLedgerVersionOffset); - return this.api.preparePaymentChannelCreate( - address, requests.preparePaymentChannelCreate.normal, - localInstructions).then( - _.partial(checkResult, responses.preparePaymentChannelCreate.normal, - 'prepare')); - }); - - it('preparePaymentChannelCreate full', function () { - return this.api.preparePaymentChannelCreate( - address, requests.preparePaymentChannelCreate.full).then( - _.partial(checkResult, responses.preparePaymentChannelCreate.full, - 'prepare')); - }); - - it('preparePaymentChannelFund', function () { - const localInstructions = _.defaults({ - maxFee: '0.000012' - }, instructionsWithMaxLedgerVersionOffset); - return this.api.preparePaymentChannelFund( - address, requests.preparePaymentChannelFund.normal, - localInstructions).then( - _.partial(checkResult, responses.preparePaymentChannelFund.normal, - 'prepare')); - }); - - it('preparePaymentChannelFund full', function () { - return this.api.preparePaymentChannelFund( - address, requests.preparePaymentChannelFund.full).then( - _.partial(checkResult, responses.preparePaymentChannelFund.full, - 'prepare')); - }); - - it('preparePaymentChannelClaim', function () { - const localInstructions = _.defaults({ - maxFee: '0.000012' - }, instructionsWithMaxLedgerVersionOffset); - return this.api.preparePaymentChannelClaim( - address, requests.preparePaymentChannelClaim.normal, - localInstructions).then( - _.partial(checkResult, responses.preparePaymentChannelClaim.normal, - 'prepare')); - }); - - it('preparePaymentChannelClaim with renew', function () { - const localInstructions = _.defaults({ - maxFee: '0.000012' - }, instructionsWithMaxLedgerVersionOffset); - return this.api.preparePaymentChannelClaim( - address, requests.preparePaymentChannelClaim.renew, - localInstructions).then( - _.partial(checkResult, responses.preparePaymentChannelClaim.renew, - 'prepare')); - }); - - it('preparePaymentChannelClaim with close', function () { - const localInstructions = _.defaults({ - maxFee: '0.000012' - }, instructionsWithMaxLedgerVersionOffset); - return this.api.preparePaymentChannelClaim( - address, requests.preparePaymentChannelClaim.close, - localInstructions).then( - _.partial(checkResult, responses.preparePaymentChannelClaim.close, - 'prepare')); - }); - - it('rejects Promise on preparePaymentChannelClaim with renew and close', function (done) { - try { - this.api.preparePaymentChannelClaim( - address, requests.preparePaymentChannelClaim.full).then(prepared => { - done(new Error('Expected method to reject. Prepared transaction: ' + JSON.stringify(prepared))); - }).catch(err => { - assert.strictEqual(err.name, 'ValidationError'); - assert.strictEqual(err.message, '"renew" and "close" flags on PaymentChannelClaim are mutually exclusive'); - done(); - }).catch(done); // Finish test with assertion failure immediately instead of waiting for timeout. - } catch (err) { - done(new Error('Expected method to reject, but method threw. Thrown: ' + err)); - } - }); - - it('rejects Promise on preparePaymentChannelClaim with no signature', function (done) { - try { - this.api.preparePaymentChannelClaim( - address, requests.preparePaymentChannelClaim.noSignature).then(prepared => { - done(new Error('Expected method to reject. Prepared transaction: ' + JSON.stringify(prepared))); - }).catch(err => { - assert.strictEqual(err.name, 'ValidationError'); - assert.strictEqual(err.message, '"signature" and "publicKey" fields on PaymentChannelClaim must only be specified together.'); - done(); - }).catch(done); // Finish test with assertion failure immediately instead of waiting for timeout. - } catch (err) { - done(new Error('Expected method to reject, but method threw. Thrown: ' + err)); - } - }); - - it('sign', function () { - const secret = 'shsWGZcmZz6YsWWmcnpfr6fLTdtFV'; - const result = this.api.sign(requests.sign.normal.txJSON, secret); - assert.deepEqual(result, responses.sign.normal); - schemaValidator.schemaValidate('sign', result); - }); - - it('sign - already signed', function () { - const secret = 'shsWGZcmZz6YsWWmcnpfr6fLTdtFV'; - const result = this.api.sign(requests.sign.normal.txJSON, secret); - assert.throws(() => { - const tx = JSON.stringify(binary.decode(result.signedTransaction)); - this.api.sign(tx, secret); - }, /txJSON must not contain "TxnSignature" or "Signers" properties/); - }); - - it('sign - EscrowExecution', function () { - const secret = 'snoPBrXtMeMyMHUVTgbuqAfg1SUTb'; - const result = this.api.sign(requests.sign.escrow.txJSON, secret); - assert.deepEqual(result, responses.sign.escrow); - schemaValidator.schemaValidate('sign', result); - }); - - it('sign - signAs', function () { - const txJSON = requests.sign.signAs; - const secret = 'snoPBrXtMeMyMHUVTgbuqAfg1SUTb'; - const signature = this.api.sign(JSON.stringify(txJSON), secret, - { signAs: 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh' }); - assert.deepEqual(signature, responses.sign.signAs); - }); - - it('sign - withKeypair', function () { - const keypair = { - privateKey: - '00ACCD3309DB14D1A4FC9B1DAE608031F4408C85C73EE05E035B7DC8B25840107A', - publicKey: - '02F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D8' - }; - const result = this.api.sign(requests.sign.normal.txJSON, keypair); - assert.deepEqual(result, responses.sign.normal); - schemaValidator.schemaValidate('sign', result); - }); - - it('sign - withKeypair already signed', function () { - const keypair = { - privateKey: - '00ACCD3309DB14D1A4FC9B1DAE608031F4408C85C73EE05E035B7DC8B25840107A', - publicKey: - '02F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D8' - }; - const result = this.api.sign(requests.sign.normal.txJSON, keypair); - assert.throws(() => { - const tx = JSON.stringify(binary.decode(result.signedTransaction)); - this.api.sign(tx, keypair); - }, /txJSON must not contain "TxnSignature" or "Signers" properties/); - }); - - it('sign - withKeypair EscrowExecution', function () { - const keypair = { - privateKey: - '001ACAAEDECE405B2A958212629E16F2EB46B153EEE94CDD350FDEFF52795525B7', - publicKey: - '0330E7FC9D56BB25D6893BA3F317AE5BCF33B3291BD63DB32654A313222F7FD020' - }; - const result = this.api.sign(requests.sign.escrow.txJSON, keypair); - assert.deepEqual(result, responses.sign.escrow); - schemaValidator.schemaValidate('sign', result); - }); - - it('sign - withKeypair signAs', function () { - const txJSON = requests.sign.signAs; - const keypair = { - privateKey: - '001ACAAEDECE405B2A958212629E16F2EB46B153EEE94CDD350FDEFF52795525B7', - publicKey: - '0330E7FC9D56BB25D6893BA3F317AE5BCF33B3291BD63DB32654A313222F7FD020' - }; - const signature = this.api.sign(JSON.stringify(txJSON), keypair, { - signAs: 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh' - }); - assert.deepEqual(signature, responses.sign.signAs); - }); - - it('sign - succeeds - prepared payment', async function () { - const payment = await this.api.preparePayment('r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59', { - source: { - address: 'r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59', - maxAmount: { - value: '1', - currency: 'drops' - } - }, - destination: { - address: 'rQ3PTWGLCbPz8ZCicV5tCX3xuymojTng5r', - amount: { - value: '1', - currency: 'drops' - } - } - }); - const secret = 'shsWGZcmZz6YsWWmcnpfr6fLTdtFV'; - const result = this.api.sign(payment.txJSON, secret); - const expectedResult = { - signedTransaction: - '12000022800000002400000017201B008694F261400000000000000168400000000000000C732102F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D874473045022100A9C91D4CFAE45686146EE0B56D4C53A2E7C2D672FB834D43E0BE2D2E9106519A022075DDA2F92DE552B0C45D83D4E6D35889B3FBF51BFBBD9B25EBF70DE3C96D0D6681145E7B112523F68D2F5E879DB4EAC51C6698A693048314FDB08D07AAA0EB711793A3027304D688E10C3648', - id: - '88D6B913C66279EA31ADC25C5806C48B2D4E5680261666790A736E1961217700' - }; - assert.deepEqual(result, expectedResult); - schemaValidator.schemaValidate('sign', result); - }); - - it('sign - succeeds - no flags', async function () { - const txJSON = '{"TransactionType":"Payment","Account":"r45Rev1EXGxy2hAUmJPCne97KUE7qyrD3j","Destination":"rQ3PTWGLCbPz8ZCicV5tCX3xuymojTng5r","Amount":"20000000","Sequence":1,"Fee":"12"}'; - const secret = 'shotKgaEotpcYsshSE39vmSnBDRim'; - const result = this.api.sign(txJSON, secret); - const expectedResult = { - signedTransaction: - '1200002400000001614000000001312D0068400000000000000C7321022B05847086686F9D0499B13136B94AD4323EE1B67D4C429ECC987AB35ACFA34574473045022100C104B7B97C31FACA4597E7D6FCF13BD85BD11375963A62A0AC45B0061236E39802207784F157F6A98DFC85B051CDDF61CC3084C4F5750B82674801C8E9950280D1998114EE3046A5DDF8422C40DDB93F1D522BB4FE6419158314FDB08D07AAA0EB711793A3027304D688E10C3648', - id: - '0596925967F541BF332FF6756645B2576A9858414B5B363DC3D34915BE8A70D6' - }; - const decoded = binary.decode(result.signedTransaction); - assert(decoded.Flags === undefined, `Flags = ${decoded.Flags}, should be undefined`); - assert.deepEqual(result, expectedResult); - schemaValidator.schemaValidate('sign', result); - }); - - it('sign - throws when encoded tx does not match decoded tx - prepared payment', async function () { - const payment = await this.api.preparePayment('r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59', { - source: { - address: 'r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59', - maxAmount: { - value: '1.1234567', - currency: 'drops' - } - }, - destination: { - address: 'rQ3PTWGLCbPz8ZCicV5tCX3xuymojTng5r', - amount: { - value: '1.1234567', - currency: 'drops' - } - } - }); - const secret = 'shsWGZcmZz6YsWWmcnpfr6fLTdtFV'; - assert.throws( - () => { - this.api.sign(payment.txJSON, secret); - }, - /^Error: 1\.1234567 is an illegal amount/ - ); - }); - - it('sign - throws when encoded tx does not match decoded tx - prepared order', async function () { - const order = { - direction: 'sell', - quantity: { - currency: 'USD', - counterparty: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B', - value: '3.140000' - }, - totalPrice: { - currency: 'XRP', - value: '31415' - } - }; - const prepared = await this.api.prepareOrder('r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59', order, { - sequence: 123 - }); - const secret = 'shsWGZcmZz6YsWWmcnpfr6fLTdtFV'; - try { - this.api.sign(prepared.txJSON, secret); - return Promise.reject(new Error('api.sign should have thrown')); - } catch (error) { - assert.equal(error.name, 'ValidationError'); - assert.equal(error.message, 'Serialized transaction does not match original txJSON. See `error.data`'); - assert.deepEqual(error.data.diff, { - TakerGets: { - value: '3.14' - } - }); - } - }); - - it('sign - throws when encoded tx does not match decoded tx - AccountSet', function () { - const secret = 'shsWGZcmZz6YsWWmcnpfr6fLTdtFV'; - const request = { - "txJSON": "{\"Flags\":2147483648,\"TransactionType\":\"AccountSet\",\"Account\":\"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59\",\"Domain\":\"726970706C652E636F6D\",\"LastLedgerSequence\":8820051,\"Fee\":\"1.2\",\"Sequence\":23,\"SigningPubKey\":\"02F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D8\"}", - "instructions": { - "fee": "0.0000012", - "sequence": 23, - "maxLedgerVersion": 8820051 - } - } - - assert.throws( - () => { - this.api.sign(request.txJSON, secret); - }, - /Error: 1\.2 is an illegal amount/ - ); - }); - - it('sign - throws when encoded tx does not match decoded tx - higher fee', function () { - const secret = 'shsWGZcmZz6YsWWmcnpfr6fLTdtFV'; - const request = { - "txJSON": "{\"Flags\":2147483648,\"TransactionType\":\"AccountSet\",\"Account\":\"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59\",\"Domain\":\"726970706C652E636F6D\",\"LastLedgerSequence\":8820051,\"Fee\":\"1123456.7\",\"Sequence\":23,\"SigningPubKey\":\"02F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D8\"}", - "instructions": { - "fee": "1.1234567", - "sequence": 23, - "maxLedgerVersion": 8820051 - } - } - - assert.throws( - () => { - this.api.sign(request.txJSON, secret); - }, - /Error: 1123456\.7 is an illegal amount/ - ); - }); - - it('sign - throws when Fee exceeds maxFeeXRP (in drops)', function () { - const secret = 'shsWGZcmZz6YsWWmcnpfr6fLTdtFV'; - const request = { - "txJSON": "{\"Flags\":2147483648,\"TransactionType\":\"AccountSet\",\"Account\":\"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59\",\"Domain\":\"726970706C652E636F6D\",\"LastLedgerSequence\":8820051,\"Fee\":\"2010000\",\"Sequence\":23,\"SigningPubKey\":\"02F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D8\"}", - "instructions": { - "fee": "2.01", - "sequence": 23, - "maxLedgerVersion": 8820051 - } - } - - assert.throws(() => { - this.api.sign(request.txJSON, secret) - }, /Fee" should not exceed "2000000"\. To use a higher fee, set `maxFeeXRP` in the RippleAPI constructor\./) - }); - - it('sign - throws when Fee exceeds maxFeeXRP (in drops) - custom maxFeeXRP', function () { - this.api._maxFeeXRP = '1.9' - const secret = 'shsWGZcmZz6YsWWmcnpfr6fLTdtFV'; - const request = { - "txJSON": "{\"Flags\":2147483648,\"TransactionType\":\"AccountSet\",\"Account\":\"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59\",\"Domain\":\"726970706C652E636F6D\",\"LastLedgerSequence\":8820051,\"Fee\":\"2010000\",\"Sequence\":23,\"SigningPubKey\":\"02F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D8\"}", - "instructions": { - "fee": "2.01", - "sequence": 23, - "maxLedgerVersion": 8820051 - } - } - - assert.throws(() => { - this.api.sign(request.txJSON, secret) - }, /Fee" should not exceed "1900000"\. To use a higher fee, set `maxFeeXRP` in the RippleAPI constructor\./) - }); - - it('sign - permits fee exceeding 2000000 drops when maxFeeXRP is higher than 2 XRP', function () { - this.api._maxFeeXRP = '2.1' - const secret = 'shsWGZcmZz6YsWWmcnpfr6fLTdtFV'; - const request = { - "txJSON": "{\"Flags\":2147483648,\"TransactionType\":\"AccountSet\",\"Account\":\"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59\",\"Domain\":\"726970706C652E636F6D\",\"LastLedgerSequence\":8820051,\"Fee\":\"2010000\",\"Sequence\":23,\"SigningPubKey\":\"02F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D8\"}", - "instructions": { - "fee": "2.01", - "sequence": 23, - "maxLedgerVersion": 8820051 - } - } - - const result = this.api.sign(request.txJSON, secret) - - const expectedResponse = { - signedTransaction: "12000322800000002400000017201B008695536840000000001EAB90732102F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D8744630440220384FBB48EEE7B0E58BD89294A609F9407C51FBE8FA08A4B305B22E9A7489D66602200152315EFE752DA381E74493419871550D206AC6503841DA5F8C30E35D9E3892770A726970706C652E636F6D81145E7B112523F68D2F5E879DB4EAC51C6698A69304", - id: "A1586D6AF7B0821E7075E12A0132D9EB50BC1874A0749441201497F7561795FB" - } - - assert.deepEqual(result, expectedResponse) - schemaValidator.schemaValidate('sign', result) - }); - - it('submit', function () { - return this.api.submit(responses.sign.normal.signedTransaction).then(response => { - checkResult(responses.submit, 'submit', response); - }); - }); - - it('submit - failure', function () { - return this.api.submit('BAD').then(() => { - assert(false, 'Should throw RippledError'); - }).catch(error => { - assert(error instanceof this.api.errors.RippledError); - assert.strictEqual(error.data.resultCode, 'temBAD_FEE'); - }); - }); - - it('signPaymentChannelClaim', function () { - const privateKey = - 'ACCD3309DB14D1A4FC9B1DAE608031F4408C85C73EE05E035B7DC8B25840107A'; - const result = this.api.signPaymentChannelClaim( - requests.signPaymentChannelClaim.channel, - requests.signPaymentChannelClaim.amount, privateKey); - checkResult(responses.signPaymentChannelClaim, - 'signPaymentChannelClaim', result) - }); - - it('verifyPaymentChannelClaim', function () { - const publicKey = - '02F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D8'; - const result = this.api.verifyPaymentChannelClaim( - requests.signPaymentChannelClaim.channel, - requests.signPaymentChannelClaim.amount, - responses.signPaymentChannelClaim, publicKey); - checkResult(true, 'verifyPaymentChannelClaim', result) - }); - - it('verifyPaymentChannelClaim - invalid', function () { - const publicKey = - '03A6523FE4281DA48A6FD77FAF3CB77F5C7001ABA0B32BCEDE0369AC009758D7D9'; - const result = this.api.verifyPaymentChannelClaim( - requests.signPaymentChannelClaim.channel, - requests.signPaymentChannelClaim.amount, - responses.signPaymentChannelClaim, publicKey); - checkResult(false, - 'verifyPaymentChannelClaim', result) - }); - - it('combine', function () { - const combined = this.api.combine(requests.combine.setDomain); - checkResult(responses.combine.single, 'sign', combined); - }); - - it('combine - different transactions', function () { - const request = [requests.combine.setDomain[0]]; - const tx = binary.decode(requests.combine.setDomain[0]); - tx.Flags = 0; - request.push(binary.encode(tx)); - assert.throws(() => { - this.api.combine(request); - }, /txJSON is not the same for all signedTransactions/); - }); - - describe('RippleAPI', function () { - - it('getBalances', function () { - return this.api.getBalances(address).then( - _.partial(checkResult, responses.getBalances, 'getBalances')); - }); - - it('getBalances - limit', function () { - const options = { - limit: 3, - ledgerVersion: 123456 - }; - const expectedResponse = responses.getBalances.slice(0, 3); - return this.api.getBalances(address, options).then( - _.partial(checkResult, expectedResponse, 'getBalances')); - }); - - it('getBalances - limit & currency', function () { - const options = { - currency: 'USD', - limit: 3 - }; - const expectedResponse = _.filter(responses.getBalances, - item => item.currency === 'USD').slice(0, 3); - return this.api.getBalances(address, options).then( - _.partial(checkResult, expectedResponse, 'getBalances')); - }); - - it('getBalances - limit & currency & issuer', function () { - const options = { - currency: 'USD', - counterparty: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B', - limit: 3 - }; - const expectedResponse = _.filter(responses.getBalances, - item => item.currency === 'USD' && - item.counterparty === 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B').slice(0, 3); - return this.api.getBalances(address, options).then( - _.partial(checkResult, expectedResponse, 'getBalances')); - }); - }); - - it('getBalanceSheet', function () { - return this.api.getBalanceSheet(address).then( - _.partial(checkResult, responses.getBalanceSheet, 'getBalanceSheet')); - }); - - it('getBalanceSheet - invalid options', function () { - return this.api.getBalanceSheet(address, { invalid: 'options' }).then(() => { - assert(false, 'Should throw ValidationError'); - }).catch(error => { - assert(error instanceof this.api.errors.ValidationError); - }); - }); - - it('getBalanceSheet - empty', function () { - const options = { ledgerVersion: 123456 }; - return this.api.getBalanceSheet(address, options).then( - _.partial(checkResult, {}, 'getBalanceSheet')); - }); - - describe('getTransaction', () => { - it('getTransaction - payment', function () { - return this.api.getTransaction(hashes.VALID_TRANSACTION_HASH).then( - _.partial(checkResult, responses.getTransaction.payment, - 'getTransaction')); - }); - - it('getTransaction - payment - include raw transaction', function () { - const options = { - includeRawTransaction: true - } - return this.api.getTransaction( - hashes.VALID_TRANSACTION_HASH, options - ).then( - _.partial(checkResult, responses.getTransaction.paymentIncludeRawTransaction, - 'getTransaction')); - }); - - it('getTransaction - settings', function () { - const hash = - '4FB3ADF22F3C605E23FAEFAA185F3BD763C4692CAC490D9819D117CD33BFAA1B'; - return this.api.getTransaction(hash).then( - _.partial(checkResult, responses.getTransaction.settings, - 'getTransaction')); - }); - - it('getTransaction - settings - include raw transaction', function () { - const hash = - '4FB3ADF22F3C605E23FAEFAA185F3BD763C4692CAC490D9819D117CD33BFAA1B'; - const options = { - includeRawTransaction: true - } - const expected = Object.assign({}, responses.getTransaction.settings) // Avoid mutating test fixture - expected.rawTransaction = "{\"Account\":\"rLVKsA4F9iJBbA6rX2x4wCmkj6drgtqpQe\",\"Fee\":\"10\",\"Flags\":2147483648,\"Sequence\":1,\"SetFlag\":2,\"SigningPubKey\":\"03EA3ADCA632F125EC2CC4F7F6A82DE0DCE2B65290CAC1F22242C5163F0DA9652D\",\"TransactionType\":\"AccountSet\",\"TxnSignature\":\"3045022100DE8B666B1A31EA65011B0F32130AB91A5747E32FA49B3054CEE8E8362DBAB98A022040CF0CF254677A8E5CD04C59CA2ED7F6F15F7E184641BAE169C561650967B226\",\"date\":460832270,\"hash\":\"4FB3ADF22F3C605E23FAEFAA185F3BD763C4692CAC490D9819D117CD33BFAA1B\",\"inLedger\":8206418,\"ledger_index\":8206418,\"meta\":{\"AffectedNodes\":[{\"ModifiedNode\":{\"FinalFields\":{\"Account\":\"rLVKsA4F9iJBbA6rX2x4wCmkj6drgtqpQe\",\"Balance\":\"29999990\",\"Flags\":786432,\"OwnerCount\":0,\"Sequence\":2},\"LedgerEntryType\":\"AccountRoot\",\"LedgerIndex\":\"3F5072C4875F32ED770DAF3610A716600ED7C7BB0348FADC7A98E011BB2CD36F\",\"PreviousFields\":{\"Balance\":\"30000000\",\"Flags\":4194304,\"Sequence\":1},\"PreviousTxnID\":\"3FB0350A3742BBCC0D8AA3C5247D1AEC01177D0A24D9C34762BAA2FEA8AD88B3\",\"PreviousTxnLgrSeq\":8206397}}],\"TransactionIndex\":5,\"TransactionResult\":\"tesSUCCESS\"},\"validated\":true}" - return this.api.getTransaction(hash, options).then( - _.partial(checkResult, expected, - 'getTransaction')); - }); - - it('getTransaction - order', function () { - const hash = - '10A6FB4A66EE80BED46AAE4815D7DC43B97E944984CCD5B93BCF3F8538CABC51'; - closeLedger(this.api.connection); - return this.api.getTransaction(hash).then( - _.partial(checkResult, responses.getTransaction.order, - 'getTransaction')); - }); - - it('getTransaction - sell order', function () { - const hash = - '458101D51051230B1D56E9ACAFAA34451BF65FA000F95DF6F0FF5B3A62D83FC2'; - closeLedger(this.api.connection); - return this.api.getTransaction(hash).then( - _.partial(checkResult, responses.getTransaction.orderSell, - 'getTransaction')); - }); - - it('getTransaction - order cancellation', function () { - const hash = - '809335DD3B0B333865096217AA2F55A4DF168E0198080B3A090D12D88880FF0E'; - closeLedger(this.api.connection); - return this.api.getTransaction(hash).then( - _.partial(checkResult, responses.getTransaction.orderCancellation, - 'getTransaction')); - }); - - it('getTransaction - order with expiration cancellation', function () { - const hash = - '097B9491CC76B64831F1FEA82EAA93BCD728106D90B65A072C933888E946C40B'; - return this.api.getTransaction(hash).then( - _.partial(checkResult, - responses.getTransaction.orderWithExpirationCancellation, - 'getTransaction')); - }); - - it('getTransaction - trustline set', function () { - const hash = - '635A0769BD94710A1F6A76CDE65A3BC661B20B798807D1BBBDADCEA26420538D'; - return this.api.getTransaction(hash).then( - _.partial(checkResult, responses.getTransaction.trustline, - 'getTransaction')); - }); - - it('getTransaction - trustline frozen off', function () { - const hash = - 'FE72FAD0FA7CA904FB6C633A1666EDF0B9C73B2F5A4555D37EEF2739A78A531B'; - return this.api.getTransaction(hash).then( - _.partial(checkResult, responses.getTransaction.trustlineFrozenOff, - 'getTransaction')); - }); - - it('getTransaction - trustline no quality', function () { - const hash = - 'BAF1C678323C37CCB7735550C379287667D8288C30F83148AD3C1CB019FC9002'; - return this.api.getTransaction(hash).then( - _.partial(checkResult, responses.getTransaction.trustlineNoQuality, - 'getTransaction')); - }); - - it('getTransaction - trustline add memo', function () { - const hash = - '9D6AC5FD6545B2584885B85E36759EB6440CDD41B6C55859F84AFDEE2B428220'; - return this.api.getTransaction(hash).then( - _.partial(checkResult, responses.getTransaction.trustlineAddMemo, - 'getTransaction')); - }); - - it('getTransaction - not validated', function () { - const hash = - '4FB3ADF22F3C605E23FAEFAA185F3BD763C4692CAC490D9819D117CD33BFAA10'; - return this.api.getTransaction(hash).then((response) => { - console.log(response); - assert(false, 'Should throw NotFoundError'); - }).catch(error => { - assert(error instanceof this.api.errors.NotFoundError); - assert.equal(error.message, 'Transaction not found'); - }); - }); - - it('getTransaction - tracking on', function () { - const hash = - '8925FC8844A1E930E2CC76AD0A15E7665AFCC5425376D548BB1413F484C31B8C'; - return this.api.getTransaction(hash).then( - _.partial(checkResult, responses.getTransaction.trackingOn, - 'getTransaction')); - }); - - it('getTransaction - tracking off', function () { - const hash = - 'C8C5E20DFB1BF533D0D81A2ED23F0A3CBD1EF2EE8A902A1D760500473CC9C582'; - return this.api.getTransaction(hash).then( - _.partial(checkResult, responses.getTransaction.trackingOff, - 'getTransaction')); - }); - - it('getTransaction - set regular key', function () { - const hash = - '278E6687C1C60C6873996210A6523564B63F2844FB1019576C157353B1813E60'; - return this.api.getTransaction(hash).then( - _.partial(checkResult, responses.getTransaction.setRegularKey, - 'getTransaction')); - }); - - it('getTransaction - not found in range', function () { - const hash = - '809335DD3B0B333865096217AA2F55A4DF168E0198080B3A090D12D88880FF0E'; - const options = { - minLedgerVersion: 32570, - maxLedgerVersion: 32571 - }; - return this.api.getTransaction(hash, options).then(() => { - assert(false, 'Should throw NotFoundError'); - }).catch(error => { - assert(error instanceof this.api.errors.NotFoundError); - }); - }); - - it('getTransaction - not found by hash', function () { - const hash = hashes.NOTFOUND_TRANSACTION_HASH; - return this.api.getTransaction(hash).then(() => { - assert(false, 'Should throw NotFoundError'); - }).catch(error => { - assert(error instanceof this.api.errors.NotFoundError); - }); - }); - - it('getTransaction - missing ledger history', function () { - const hash = hashes.NOTFOUND_TRANSACTION_HASH; - // make gaps in history - closeLedger(this.api.connection); - return this.api.getTransaction(hash).then(() => { - assert(false, 'Should throw MissingLedgerHistoryError'); - }).catch(error => { - assert(error instanceof this.api.errors.MissingLedgerHistoryError); - }); - }); - - it('getTransaction - missing ledger history with ledger range', function () { - const hash = hashes.NOTFOUND_TRANSACTION_HASH; - const options = { - minLedgerVersion: 32569, - maxLedgerVersion: 32571 - }; - return this.api.getTransaction(hash, options).then(() => { - assert(false, 'Should throw MissingLedgerHistoryError'); - }).catch(error => { - assert(error instanceof this.api.errors.MissingLedgerHistoryError); - }); - }); - - it('getTransaction - not found - future maxLedgerVersion', function () { - const hash = hashes.NOTFOUND_TRANSACTION_HASH; - const options = { - maxLedgerVersion: 99999999999 - }; - return this.api.getTransaction(hash, options).then(() => { - assert(false, 'Should throw PendingLedgerVersionError'); - }).catch(error => { - assert(error instanceof this.api.errors.PendingLedgerVersionError); - assert.strictEqual(error.message, 'maxLedgerVersion is greater than server\'s' - + ' most recent validated ledger') - }); - }); - - it('getTransaction - transaction not validated', function () { - const hash = - '4FB3ADF22F3C605E23FAEFAA185F3BD763C4692CAC490D9819D117CD33BFAA11'; - return this.api.getTransaction(hash).then(() => { - assert(false, 'Should throw NotFoundError'); - }).catch(error => { - assert(error instanceof this.api.errors.NotFoundError); - assert(error.message.indexOf('Transaction has not been validated yet') !== -1); - }); - }); - - it('getTransaction - transaction ledger not found', function () { - const hash = - '4FB3ADF22F3C605E23FAEFAA185F3BD763C4692CAC490D9819D117CD33BFAA12'; - return this.api.getTransaction(hash).then(() => { - assert(false, 'Should throw NotFoundError'); - }).catch(error => { - assert(error instanceof this.api.errors.NotFoundError); - assert(error.message.indexOf('ledger not found') !== -1); - }); - }); - - it('getTransaction - ledger missing close time', function () { - const hash = - '0F7ED9F40742D8A513AE86029462B7A6768325583DF8EE21B7EC663019DD6A04'; - closeLedger(this.api.connection); - return this.api.getTransaction(hash).then(() => { - assert(false, 'Should throw UnexpectedError'); - }).catch(error => { - assert(error instanceof this.api.errors.UnexpectedError); - }); - }); - - // Checks - - it('getTransaction - CheckCreate', function () { - const hash = - '605A2E2C8E48AECAF5C56085D1AEAA0348DC838CE122C9188F94EB19DA05C2FE'; - return this.api.getTransaction(hash).then( - _.partial(checkResult, - responses.getTransaction.checkCreate, - 'getTransaction')); - }); - - it('getTransaction - CheckCancel', function () { - const hash = - 'B4105D1B2D83819647E4692B7C5843D674283F669524BD50C9614182E3A12CD4'; - return this.api.getTransaction(hash).then( - _.partial(checkResult, - responses.getTransaction.checkCancel, - 'getTransaction')); - }); - - it('getTransaction - CheckCash', function () { - const hash = - '8321208465F70BA52C28BCC4F646BAF3B012BA13B57576C0336F42D77E3E0749'; - return this.api.getTransaction(hash/*, options*/).then( - _.partial(checkResult, - responses.getTransaction.checkCash, - 'getTransaction')); - }); - - // Escrows - - it('getTransaction - EscrowCreation', function () { - const hash = - '144F272380BDB4F1BD92329A2178BABB70C20F59042C495E10BF72EBFB408EE1'; - return this.api.getTransaction(hash).then( - _.partial(checkResult, - responses.getTransaction.escrowCreation, - 'getTransaction')); - }); - - it('getTransaction - EscrowCancellation', function () { - const hash = - 'F346E542FFB7A8398C30A87B952668DAB48B7D421094F8B71776DA19775A3B22'; - return this.api.getTransaction(hash).then( - _.partial(checkResult, - responses.getTransaction.escrowCancellation, - 'getTransaction')); - }); - - it('getTransaction - EscrowExecution', function () { - const options = { - minLedgerVersion: 10, - maxLedgerVersion: 15 - }; - const hash = - 'CC5277137B3F25EE8B86259C83CB0EAADE818505E4E9BCBF19B1AC6FD136993B'; - return this.api.getTransaction(hash, options).then( - _.partial(checkResult, - responses.getTransaction.escrowExecution, - 'getTransaction')); - }); - - it('getTransaction - EscrowExecution simple', function () { - const hash = - 'CC5277137B3F25EE8B86259C83CB0EAADE818505E4E9BCBF19B1AC6FD1369931'; - return this.api.getTransaction(hash).then( - _.partial(checkResult, - responses.getTransaction.escrowExecutionSimple, - 'getTransaction')); - }); - - it('getTransaction - PaymentChannelCreate', function () { - const hash = - '0E9CA3AB1053FC0C1CBAA75F636FE1EC92F118C7056BBEF5D63E4C116458A16D'; - return this.api.getTransaction(hash).then( - _.partial(checkResult, - responses.getTransaction.paymentChannelCreate, - 'getTransaction')); - }); - - it('getTransaction - PaymentChannelFund', function () { - const hash = - 'CD053D8867007A6A4ACB7A432605FE476D088DCB515AFFC886CF2B4EB6D2AE8B'; - return this.api.getTransaction(hash).then( - _.partial(checkResult, - responses.getTransaction.paymentChannelFund, - 'getTransaction')); - }); - - it('getTransaction - PaymentChannelClaim', function () { - const hash = - '81B9ECAE7195EB6E8034AEDF44D8415A7A803E14513FDBB34FA984AB37D59563'; - return this.api.getTransaction(hash).then( - _.partial(checkResult, - responses.getTransaction.paymentChannelClaim, - 'getTransaction')); - }); - - it('getTransaction - no Meta', function () { - const hash = - 'AFB3ADF22F3C605E23FAEFAA185F3BD763C4692CAC490D9819D117CD33BFAA1B'; - return this.api.getTransaction(hash).then(result => { - assert.deepEqual(result, responses.getTransaction.noMeta); - }); - }); - - it('getTransaction - Unrecognized transaction type', function () { - const hash = - 'AFB3ADF22F3C605E23FAEFAA185F3BD763C4692CAC490D9819D117CD33BFAA11'; - closeLedger(this.api.connection); - return this.api.getTransaction(hash).then(() => { - assert(false, 'Unrecognized transaction type'); - }).catch(error => { - assert.strictEqual(error.message, 'Unrecognized transaction type'); - }); - }); - - it('getTransaction - amendment', function () { - const hash = - 'A971B83ABED51D83749B73F3C1AAA627CD965AFF74BE8CD98299512D6FB0658F'; - return this.api.getTransaction(hash).then(result => { - assert.deepEqual(result, responses.getTransaction.amendment); - }); - }); - - it('getTransaction - feeUpdate', function () { - const hash = - 'C6A40F56127436DCD830B1B35FF939FD05B5747D30D6542572B7A835239817AF'; - return this.api.getTransaction(hash).then(result => { - assert.deepEqual(result, responses.getTransaction.feeUpdate); - }); - }); - }); - - it('getTransactions', function () { - const options = { types: ['payment', 'order'], initiated: true, limit: 2 }; - return this.api.getTransactions(address, options).then( - _.partial(checkResult, responses.getTransactions.normal, - 'getTransactions')); - }); - - it('getTransactions - include raw transactions', function () { - const options = { - types: ['payment', 'order'], - initiated: true, - limit: 2, - includeRawTransactions: true - }; - return this.api.getTransactions(address, options).then( - _.partial(checkResult, responses.getTransactions.includeRawTransactions, - 'getTransactions')); - }); - - it('getTransactions - earliest first', function () { - const options = { - types: ['payment', 'order'], initiated: true, limit: 2, - earliestFirst: true - }; - const expected = _.cloneDeep(responses.getTransactions.normal) - .sort(utils.compareTransactions); - return this.api.getTransactions(address, options).then( - _.partial(checkResult, expected, 'getTransactions')); - }); - - - it('getTransactions - earliest first with start option', function () { - const options = { - types: ['payment', 'order'], initiated: true, limit: 2, - start: hashes.VALID_TRANSACTION_HASH, - earliestFirst: true - }; - return this.api.getTransactions(address, options).then(data => { - assert.strictEqual(data.length, 0); - }); - }); - - it('getTransactions - gap', function () { - const options = { - types: ['payment', 'order'], initiated: true, limit: 2, - maxLedgerVersion: 348858000 - }; - return this.api.getTransactions(address, options).then(() => { - assert(false, 'Should throw MissingLedgerHistoryError'); - }).catch(error => { - assert(error instanceof this.api.errors.MissingLedgerHistoryError); - }); - }); - - it('getTransactions - tx not found', function () { - const options = { - types: ['payment', 'order'], initiated: true, limit: 2, - start: hashes.NOTFOUND_TRANSACTION_HASH, - counterparty: address - }; - return this.api.getTransactions(address, options).then((response) => { - console.log(response); - assert(false, 'Should throw NotFoundError'); - }).catch(error => { - assert(error instanceof this.api.errors.NotFoundError); - }); - }); - - it('getTransactions - filters', function () { - const options = { - types: ['payment', 'order'], initiated: true, limit: 10, - excludeFailures: true, - counterparty: addresses.ISSUER - }; - return this.api.getTransactions(address, options).then(data => { - assert.strictEqual(data.length, 10); - assert(_.every(data, t => t.type === 'payment' || t.type === 'order')); - assert(_.every(data, t => t.outcome.result === 'tesSUCCESS')); - }); - }); - - it('getTransactions - filters for incoming', function () { - const options = { - types: ['payment', 'order'], initiated: false, limit: 10, - excludeFailures: true, - counterparty: addresses.ISSUER - }; - return this.api.getTransactions(address, options).then(data => { - assert.strictEqual(data.length, 10); - assert(_.every(data, t => t.type === 'payment' || t.type === 'order')); - assert(_.every(data, t => t.outcome.result === 'tesSUCCESS')); - }); - }); - - // this is the case where core.RippleError just falls - // through the api to the user - it('getTransactions - error', function () { - const options = { types: ['payment', 'order'], initiated: true, limit: 13 }; - return this.api.getTransactions(address, options).then(() => { - assert(false, 'Should throw RippleError'); - }).catch(error => { - assert(error instanceof this.api.errors.RippleError); - }); - }); - - // TODO: this doesn't test much, just that it doesn't crash - it('getTransactions with start option', function () { - const options = { - start: hashes.VALID_TRANSACTION_HASH, - earliestFirst: false, - limit: 2 - }; - return this.api.getTransactions(address, options).then( - _.partial(checkResult, responses.getTransactions.normal, - 'getTransactions')); - }); - - it('getTransactions - start transaction with zero ledger version', function ( - ) { - const options = { - start: '4FB3ADF22F3C605E23FAEFAA185F3BD763C4692CAC490D9819D117CD33BFAA13', - limit: 1 - }; - return this.api.getTransactions(address, options).then( - _.partial(checkResult, [], 'getTransactions')); - }); - - it('getTransactions - no options', function () { - return this.api.getTransactions(addresses.OTHER_ACCOUNT).then( - _.partial(checkResult, responses.getTransactions.one, 'getTransactions')); - }); - - it('getTrustlines - filtered', function () { - const options = { currency: 'USD' }; - return this.api.getTrustlines(address, options).then( - _.partial(checkResult, - responses.getTrustlines.filtered, 'getTrustlines')); - }); - - it('getTrustlines - more than 400 items', function () { - const options = { limit: 401 }; - return this.api.getTrustlines(addresses.THIRD_ACCOUNT, options).then( - _.partial(checkResult, responses.getTrustlines.moreThan400Items, 'getTrustlines')); - }); - - it('getTrustlines - no options', function () { - return this.api.getTrustlines(address).then( - _.partial(checkResult, responses.getTrustlines.all, 'getTrustlines')); - }); - - it('generateAddress', function () { - function random() { - return _.fill(Array(16), 0); - } - assert.deepEqual(this.api.generateAddress({ entropy: random() }), - responses.generateAddress); - }); - - it('generateAddress invalid', function () { - assert.throws(() => { - function random() { - return _.fill(Array(1), 0); - } - this.api.generateAddress({ entropy: random() }); - }, this.api.errors.UnexpectedError); - }); - - it('generateXAddress', function () { - function random() { - return _.fill(Array(16), 0); - } - assert.deepEqual(this.api.generateXAddress({ entropy: random() }), - responses.generateXAddress); - }); - - it('generateXAddress invalid', function () { - assert.throws(() => { - function random() { - return _.fill(Array(1), 0); - } - this.api.generateXAddress({ entropy: random() }); - }, this.api.errors.UnexpectedError); - }); - - it('getSettings', function () { - return this.api.getSettings(address).then( - _.partial(checkResult, responses.getSettings, 'getSettings')); - }); - - it('getSettings - options undefined', function () { - return this.api.getSettings(address, undefined).then( - _.partial(checkResult, responses.getSettings, 'getSettings')); - }); - - it('getSettings - invalid options', function () { - return this.api.getSettings(address, { invalid: 'options' }).then(() => { - assert(false, 'Should throw ValidationError'); - }).catch(error => { - assert(error instanceof this.api.errors.ValidationError); - }); - }); - - it('getAccountInfo', function () { - return this.api.getAccountInfo(address).then( - _.partial(checkResult, responses.getAccountInfo, 'getAccountInfo')); - }); - - it('getAccountInfo - options undefined', function () { - return this.api.getAccountInfo(address, undefined).then( - _.partial(checkResult, responses.getAccountInfo, 'getAccountInfo')); - }); - - it('getAccountInfo - invalid options', function () { - return this.api.getAccountInfo(address, { invalid: 'options' }).then(() => { - assert(false, 'Should throw ValidationError'); - }).catch(error => { - assert(error instanceof this.api.errors.ValidationError); - }); - }); - - it('getAccountObjects', function () { - return this.api.getAccountObjects(address).then(response => - checkResult(responses.getAccountObjects, 'AccountObjectsResponse', response)); - }); - - it('getAccountObjects - invalid options', function () { - // Intentionally no local validation of these options - return this.api.getAccountObjects(address, {invalid: 'options'}).then(response => - checkResult(responses.getAccountObjects, 'AccountObjectsResponse', response)); - }); - - it('request account_objects', function () { - return this.api.request('account_objects', { - account: address - }).then(response => - checkResult(responses.getAccountObjects, 'AccountObjectsResponse', response)); - }); - - it('request account_objects - invalid options', function () { - // Intentionally no local validation of these options - return this.api.request('account_objects', { - account: address, - invalid: 'options' - }).then(response => - checkResult(responses.getAccountObjects, 'AccountObjectsResponse', response)); - }); - - it('getOrders', function () { - return this.api.getOrders(address).then( - _.partial(checkResult, responses.getOrders, 'getOrders')); - }); - - it('getOrders - limit', function () { - return this.api.getOrders(address, { limit: 20 }).then( - _.partial(checkResult, responses.getOrders, 'getOrders')); - }); - - it('getOrders - invalid options', function () { - return this.api.getOrders(address, { invalid: 'options' }).then(() => { - assert(false, 'Should throw ValidationError'); - }).catch(error => { - assert(error instanceof this.api.errors.ValidationError); - }); - }); - - describe('formatBidsAndAsks', function () { - - it('normal', function () { - const orderbookInfo = { - "base": { - "currency": "USD", - "counterparty": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B" - }, - "counter": { - "currency": "BTC", - "counterparty": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B" - } - }; - - return Promise.all( - [ - this.api.request('book_offers', { - taker_gets: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.base), - taker_pays: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.counter), - ledger_index: 'validated', - limit: 20, - taker: address - }), - this.api.request('book_offers', { - taker_gets: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.counter), - taker_pays: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.base), - ledger_index: 'validated', - limit: 20, - taker: address - }) - ] - ).then(([directOfferResults, reverseOfferResults]) => { - const directOffers = (directOfferResults ? directOfferResults.offers : []).reduce((acc, res) => acc.concat(res), []) - const reverseOffers = (reverseOfferResults ? reverseOfferResults.offers : []).reduce((acc, res) => acc.concat(res), []) - const orderbook = RippleAPI.formatBidsAndAsks(orderbookInfo, [...directOffers, ...reverseOffers]); - assert.deepEqual(orderbook, responses.getOrderbook.normal); - }); - }); - - it('with XRP', function () { - const orderbookInfo = { - "base": { - "currency": "USD", - "counterparty": "rp8rJYTpodf8qbSCHVTNacf8nSW8mRakFw" - }, - "counter": { - "currency": "XRP" - } - }; - - return Promise.all( - [ - this.api.request('book_offers', { - taker_gets: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.base), - taker_pays: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.counter), - ledger_index: 'validated', - limit: 20, - taker: address - }), - this.api.request('book_offers', { - taker_gets: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.counter), - taker_pays: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.base), - ledger_index: 'validated', - limit: 20, - taker: address - }) - ] - ).then(([directOfferResults, reverseOfferResults]) => { - const directOffers = (directOfferResults ? directOfferResults.offers : []).reduce((acc, res) => acc.concat(res), []) - const reverseOffers = (reverseOfferResults ? reverseOfferResults.offers : []).reduce((acc, res) => acc.concat(res), []) - const orderbook = RippleAPI.formatBidsAndAsks(orderbookInfo, [...directOffers, ...reverseOffers]); - assert.deepEqual(orderbook, responses.getOrderbook.withXRP); - }); - }); - - function checkSortingOfOrders(orders) { - let previousRate = '0'; - for (var i = 0; i < orders.length; i++) { - const order = orders[i]; - let rate; - - // We calculate the quality of output/input here as a test. - // This won't hold in general because when output and input amounts get tiny, - // the quality can differ significantly. However, the offer stays in the - // order book where it was originally placed. It would be more consistent - // to check the quality from the offer book, but for the test data set, - // this calculation holds. - - if (order.specification.direction === 'buy') { - rate = (new BigNumber(order.specification.quantity.value)) - .dividedBy(order.specification.totalPrice.value) - .toString(); - } else { - rate = (new BigNumber(order.specification.totalPrice.value)) - .dividedBy(order.specification.quantity.value) - .toString(); - } - assert((new BigNumber(rate)).isGreaterThanOrEqualTo(previousRate), - 'Rates must be sorted from least to greatest: ' + - rate + ' should be >= ' + previousRate); - previousRate = rate; - } - return true; - } - - it('sample XRP/JPY book has orders sorted correctly', function () { - const orderbookInfo = { - "base": { // the first currency in pair - "currency": 'XRP' - }, - "counter": { - "currency": 'JPY', - "counterparty": "rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS" - } - }; - - const myAddress = 'rE9qNjzJXpiUbVomdv7R4xhrXVeH2oVmGR'; - - return Promise.all( - [ - this.api.request('book_offers', { - taker_gets: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.base), - taker_pays: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.counter), - ledger_index: 'validated', - limit: 400, // must match `test/fixtures/rippled/requests/1-taker_gets-XRP-taker_pays-JPY.json` - taker: myAddress - }), - this.api.request('book_offers', { - taker_gets: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.counter), - taker_pays: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.base), - ledger_index: 'validated', - limit: 400, // must match `test/fixtures/rippled/requests/2-taker_gets-JPY-taker_pays-XRP.json` - taker: myAddress - }) - ] - ).then(([directOfferResults, reverseOfferResults]) => { - const directOffers = (directOfferResults ? directOfferResults.offers : []).reduce((acc, res) => acc.concat(res), []) - const reverseOffers = (reverseOfferResults ? reverseOfferResults.offers : []).reduce((acc, res) => acc.concat(res), []) - const orderbook = RippleAPI.formatBidsAndAsks(orderbookInfo, [...directOffers, ...reverseOffers]); - assert.deepStrictEqual([], orderbook.bids); - return checkSortingOfOrders(orderbook.asks); - }); - }); - - it('sample USD/XRP book has orders sorted correctly', function () { - const orderbookInfo = { counter: { currency: 'XRP' }, - base: { currency: 'USD', - counterparty: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B' } }; - - const myAddress = 'rE9qNjzJXpiUbVomdv7R4xhrXVeH2oVmGR'; - - return Promise.all( - [ - this.api.request('book_offers', { - taker_gets: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.base), - taker_pays: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.counter), - ledger_index: 'validated', - limit: 400, // must match `test/fixtures/rippled/requests/1-taker_gets-XRP-taker_pays-JPY.json` - taker: myAddress - }), - this.api.request('book_offers', { - taker_gets: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.counter), - taker_pays: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.base), - ledger_index: 'validated', - limit: 400, // must match `test/fixtures/rippled/requests/2-taker_gets-JPY-taker_pays-XRP.json` - taker: myAddress - }) - ] - ).then(([directOfferResults, reverseOfferResults]) => { - const directOffers = (directOfferResults ? directOfferResults.offers : []).reduce((acc, res) => acc.concat(res), []) - const reverseOffers = (reverseOfferResults ? reverseOfferResults.offers : []).reduce((acc, res) => acc.concat(res), []) - const orderbook = RippleAPI.formatBidsAndAsks(orderbookInfo, [...directOffers, ...reverseOffers]); - return checkSortingOfOrders(orderbook.bids) && checkSortingOfOrders(orderbook.asks); - }); - }); - - it('sorted so that best deals come first', function () { - const orderbookInfo = { - "base": { - "currency": "USD", - "counterparty": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B" - }, - "counter": { - "currency": "BTC", - "counterparty": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B" - } - }; - - return Promise.all( - [ - this.api.request('book_offers', { - taker_gets: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.base), - taker_pays: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.counter), - ledger_index: 'validated', - limit: 20, - taker: address - }), - this.api.request('book_offers', { - taker_gets: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.counter), - taker_pays: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.base), - ledger_index: 'validated', - limit: 20, - taker: address - }) - ] - ).then(([directOfferResults, reverseOfferResults]) => { - const directOffers = (directOfferResults ? directOfferResults.offers : []).reduce((acc, res) => acc.concat(res), []) - const reverseOffers = (reverseOfferResults ? reverseOfferResults.offers : []).reduce((acc, res) => acc.concat(res), []) - const orderbook = RippleAPI.formatBidsAndAsks(orderbookInfo, [...directOffers, ...reverseOffers]); - - const bidRates = orderbook.bids.map(bid => bid.properties.makerExchangeRate); - const askRates = orderbook.asks.map(ask => ask.properties.makerExchangeRate); - // makerExchangeRate = quality = takerPays.value/takerGets.value - // so the best deal for the taker is the lowest makerExchangeRate - // bids and asks should be sorted so that the best deals come first - assert.deepEqual(_.sortBy(bidRates, x => Number(x)), bidRates); - assert.deepEqual(_.sortBy(askRates, x => Number(x)), askRates); - }); - }); - - it('currency & counterparty are correct', function () { - const orderbookInfo = { - "base": { - "currency": "USD", - "counterparty": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B" - }, - "counter": { - "currency": "BTC", - "counterparty": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B" - } - }; - - return Promise.all( - [ - this.api.request('book_offers', { - taker_gets: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.base), - taker_pays: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.counter), - ledger_index: 'validated', - limit: 20, - taker: address - }), - this.api.request('book_offers', { - taker_gets: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.counter), - taker_pays: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.base), - ledger_index: 'validated', - limit: 20, - taker: address - }) - ] - ).then(([directOfferResults, reverseOfferResults]) => { - const directOffers = (directOfferResults ? directOfferResults.offers : []).reduce((acc, res) => acc.concat(res), []) - const reverseOffers = (reverseOfferResults ? reverseOfferResults.offers : []).reduce((acc, res) => acc.concat(res), []) - const orderbook = RippleAPI.formatBidsAndAsks(orderbookInfo, [...directOffers, ...reverseOffers]); - - const orders = _.flatten([orderbook.bids, orderbook.asks]); - _.forEach(orders, order => { - const quantity = order.specification.quantity; - const totalPrice = order.specification.totalPrice; - const { base, counter } = requests.getOrderbook.normal; - assert.strictEqual(quantity.currency, base.currency); - assert.strictEqual(quantity.counterparty, base.counterparty); - assert.strictEqual(totalPrice.currency, counter.currency); - assert.strictEqual(totalPrice.counterparty, counter.counterparty); - }); - }); - }); - - it('direction is correct for bids and asks', function () { - const orderbookInfo = { - "base": { - "currency": "USD", - "counterparty": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B" - }, - "counter": { - "currency": "BTC", - "counterparty": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B" - } - }; - - return Promise.all( - [ - this.api.request('book_offers', { - taker_gets: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.base), - taker_pays: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.counter), - ledger_index: 'validated', - limit: 20, - taker: address - }), - this.api.request('book_offers', { - taker_gets: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.counter), - taker_pays: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.base), - ledger_index: 'validated', - limit: 20, - taker: address - }) - ] - ).then(([directOfferResults, reverseOfferResults]) => { - const directOffers = (directOfferResults ? directOfferResults.offers : []).reduce((acc, res) => acc.concat(res), []) - const reverseOffers = (reverseOfferResults ? reverseOfferResults.offers : []).reduce((acc, res) => acc.concat(res), []) - const orderbook = RippleAPI.formatBidsAndAsks(orderbookInfo, [...directOffers, ...reverseOffers]); - - assert( - _.every(orderbook.bids, bid => bid.specification.direction === 'buy')); - assert( - _.every(orderbook.asks, ask => ask.specification.direction === 'sell')); - }); - }); - }); - - describe('getOrderbook', function () { - - it('normal', function () { - return this.api.getOrderbook(address, - requests.getOrderbook.normal, { limit: 20 }).then( - _.partial(checkResult, - responses.getOrderbook.normal, 'getOrderbook')); - }); - - it('invalid options', function () { - return this.api.getOrderbook( - address, requests.getOrderbook.normal, { invalid: 'options' } - ).then(() => { - assert(false, 'Should throw ValidationError'); - }).catch(error => { - assert(error instanceof this.api.errors.ValidationError); - }); - }); - - it('with XRP', function () { - return this.api.getOrderbook(address, requests.getOrderbook.withXRP).then( - _.partial(checkResult, responses.getOrderbook.withXRP, 'getOrderbook')); - }); - - function checkSortingOfOrders(orders) { - let previousRate = '0'; - for (var i = 0; i < orders.length; i++) { - const order = orders[i]; - let rate; - - // We calculate the quality of output/input here as a test. - // This won't hold in general because when output and input amounts get tiny, - // the quality can differ significantly. However, the offer stays in the - // order book where it was originally placed. It would be more consistent - // to check the quality from the offer book, but for the test data set, - // this calculation holds. - - if (order.specification.direction === 'buy') { - rate = (new BigNumber(order.specification.quantity.value)) - .dividedBy(order.specification.totalPrice.value) - .toString(); - } else { - rate = (new BigNumber(order.specification.totalPrice.value)) - .dividedBy(order.specification.quantity.value) - .toString(); - } - assert((new BigNumber(rate)).isGreaterThanOrEqualTo(previousRate), - 'Rates must be sorted from least to greatest: ' + - rate + ' should be >= ' + previousRate); - previousRate = rate; - } - return true; - } - - it('sample XRP/JPY book has orders sorted correctly', function () { - const orderbookInfo = { - "base": { // the first currency in pair - "currency": 'XRP' - }, - "counter": { - "currency": 'JPY', - "counterparty": "rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS" - } - }; - - const myAddress = 'rE9qNjzJXpiUbVomdv7R4xhrXVeH2oVmGR'; - - return this.api.getOrderbook(myAddress, orderbookInfo).then(orderbook => { - assert.deepStrictEqual([], orderbook.bids); - return checkSortingOfOrders(orderbook.asks); - }); - }); - - it('sample USD/XRP book has orders sorted correctly', function () { - const orderbookInfo = { counter: { currency: 'XRP' }, - base: { currency: 'USD', - counterparty: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B' } }; - - const myAddress = 'rE9qNjzJXpiUbVomdv7R4xhrXVeH2oVmGR'; - - return this.api.getOrderbook(myAddress, orderbookInfo).then(orderbook => { - return checkSortingOfOrders(orderbook.bids) && checkSortingOfOrders(orderbook.asks); - }); - }); - - // WARNING: This test fails to catch the sorting bug, issue #766 - it('sorted so that best deals come first [bad test]', function () { - return this.api.getOrderbook(address, requests.getOrderbook.normal) - .then(data => { - const bidRates = data.bids.map(bid => bid.properties.makerExchangeRate); - const askRates = data.asks.map(ask => ask.properties.makerExchangeRate); - // makerExchangeRate = quality = takerPays.value/takerGets.value - // so the best deal for the taker is the lowest makerExchangeRate - // bids and asks should be sorted so that the best deals come first - assert.deepEqual(_.sortBy(bidRates, x => Number(x)), bidRates); - assert.deepEqual(_.sortBy(askRates, x => Number(x)), askRates); - }); - }); - - it('currency & counterparty are correct', function () { - return this.api.getOrderbook(address, requests.getOrderbook.normal) - .then(data => { - const orders = _.flatten([data.bids, data.asks]); - _.forEach(orders, order => { - const quantity = order.specification.quantity; - const totalPrice = order.specification.totalPrice; - const { base, counter } = requests.getOrderbook.normal; - assert.strictEqual(quantity.currency, base.currency); - assert.strictEqual(quantity.counterparty, base.counterparty); - assert.strictEqual(totalPrice.currency, counter.currency); - assert.strictEqual(totalPrice.counterparty, counter.counterparty); - }); - }); - }); - - it('direction is correct for bids and asks', function () { - return this.api.getOrderbook(address, requests.getOrderbook.normal) - .then(data => { - assert( - _.every(data.bids, bid => bid.specification.direction === 'buy')); - assert( - _.every(data.asks, ask => ask.specification.direction === 'sell')); - }); - }); - - }); - - it('getPaymentChannel', function () { - const channelId = - 'E30E709CF009A1F26E0E5C48F7AA1BFB79393764F15FB108BDC6E06D3CBD8415'; - return this.api.getPaymentChannel(channelId).then( - _.partial(checkResult, responses.getPaymentChannel.normal, - 'getPaymentChannel')); - }); - - it('getPaymentChannel - full', function () { - const channelId = - 'D77CD4713AA08195E6B6D0E5BC023DA11B052EBFF0B5B22EDA8AE85345BCF661'; - return this.api.getPaymentChannel(channelId).then( - _.partial(checkResult, responses.getPaymentChannel.full, - 'getPaymentChannel')); - }); - - it('getPaymentChannel - not found', function () { - const channelId = - 'DFA557EA3497585BFE83F0F97CC8E4530BBB99967736BB95225C7F0C13ACE708'; - return this.api.getPaymentChannel(channelId).then(() => { - assert(false, 'Should throw entryNotFound'); - }).catch(error => { - assert(error instanceof this.api.errors.RippledError); - assert.equal(error.message, 'entryNotFound'); - assert.equal(error.data.error, 'entryNotFound'); - }); - }); - - it('getPaymentChannel - wrong type', function () { - const channelId = - '8EF9CCB9D85458C8D020B3452848BBB42EAFDDDB69A93DD9D1223741A4CA562B'; - return this.api.getPaymentChannel(channelId).then(() => { - assert(false, 'Should throw NotFoundError'); - }).catch(error => { - assert(_.includes(error.message, - 'Payment channel ledger entry not found')); - assert(error instanceof this.api.errors.NotFoundError); - }); - }); - - it('getServerInfo', function () { - return this.api.getServerInfo().then( - _.partial(checkResult, responses.getServerInfo, 'getServerInfo')); - }); - - it('getServerInfo - error', function () { - this.api.connection._send(JSON.stringify({ - command: 'config', - data: { returnErrorOnServerInfo: true } - })); - - return this.api.getServerInfo().then(() => { - assert(false, 'Should throw NetworkError'); - }).catch(error => { - assert(error instanceof this.api.errors.RippledError); - assert.equal(error.message, 'You are placing too much load on the server.'); - assert.equal(error.data.error, 'slowDown'); - }); - }); - - it('getServerInfo - no validated ledger', function () { - this.api.connection._send(JSON.stringify({ - command: 'config', - data: { serverInfoWithoutValidated: true } - })); - - return this.api.getServerInfo().then(info => { - assert.strictEqual(info.networkLedger, 'waiting'); - }).catch(error => { - assert(false, 'Should not throw Error, got ' + String(error)); - }); - }); - - it('getFee', function () { - return this.api.getFee().then(fee => { - assert.strictEqual(fee, '0.000012'); - }); - }); - - it('getFee default', function () { - this.api._feeCushion = undefined; - return this.api.getFee().then(fee => { - assert.strictEqual(fee, '0.000012'); - }); - }); - - it('getFee - high load_factor', function () { - this.api.connection._send(JSON.stringify({ - command: 'config', - data: { highLoadFactor: true } - })); - - return this.api.getFee().then(fee => { - assert.strictEqual(fee, '2'); - }); - }); - - it('getFee - high load_factor with custom maxFeeXRP', function () { - // Ensure that overriding with high maxFeeXRP of '51540' causes no errors. - // (fee will actually be 51539.607552) - this.api._maxFeeXRP = '51540' - this.api.connection._send(JSON.stringify({ - command: 'config', - data: { highLoadFactor: true } - })); - - return this.api.getFee().then(fee => { - assert.strictEqual(fee, '51539.607552'); - }); - }); - - it('fee - default maxFee of 2 XRP', function () { - this.api._feeCushion = 1000000; - - const expectedResponse = { - "txJSON": "{\"Flags\":2147483648,\"TransactionType\":\"Payment\",\"Account\":\"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59\",\"Destination\":\"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo\",\"Amount\":{\"value\":\"0.01\",\"currency\":\"USD\",\"issuer\":\"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM\"},\"SendMax\":{\"value\":\"0.01\",\"currency\":\"USD\",\"issuer\":\"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM\"},\"LastLedgerSequence\":8820051,\"Fee\":\"2000000\",\"Sequence\":23}", - "instructions": { - "fee": "2", - "sequence": 23, - "maxLedgerVersion": 8820051 - } - } - - return this.api.preparePayment( - address, requests.preparePayment.normal, instructionsWithMaxLedgerVersionOffset).then( - _.partial(checkResult, expectedResponse, 'prepare')); - }); - - it('fee - capped to maxFeeXRP when maxFee exceeds maxFeeXRP', function () { - this.api._feeCushion = 1000000 - this.api._maxFeeXRP = '3' - const localInstructions = _.defaults({ - maxFee: '4' - }, instructionsWithMaxLedgerVersionOffset); - - const expectedResponse = { - "txJSON": "{\"Flags\":2147483648,\"TransactionType\":\"Payment\",\"Account\":\"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59\",\"Destination\":\"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo\",\"Amount\":{\"value\":\"0.01\",\"currency\":\"USD\",\"issuer\":\"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM\"},\"SendMax\":{\"value\":\"0.01\",\"currency\":\"USD\",\"issuer\":\"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM\"},\"LastLedgerSequence\":8820051,\"Fee\":\"3000000\",\"Sequence\":23}", - "instructions": { - "fee": "3", - "sequence": 23, - "maxLedgerVersion": 8820051 - } - } - - return this.api.preparePayment( - address, requests.preparePayment.normal, localInstructions).then( - _.partial(checkResult, expectedResponse, 'prepare')); - }); - - it('fee - capped to maxFee', function () { - this.api._feeCushion = 1000000 - this.api._maxFeeXRP = '5' - const localInstructions = _.defaults({ - maxFee: '4' - }, instructionsWithMaxLedgerVersionOffset); - - const expectedResponse = { - "txJSON": "{\"Flags\":2147483648,\"TransactionType\":\"Payment\",\"Account\":\"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59\",\"Destination\":\"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo\",\"Amount\":{\"value\":\"0.01\",\"currency\":\"USD\",\"issuer\":\"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM\"},\"SendMax\":{\"value\":\"0.01\",\"currency\":\"USD\",\"issuer\":\"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM\"},\"LastLedgerSequence\":8820051,\"Fee\":\"4000000\",\"Sequence\":23}", - "instructions": { - "fee": "4", - "sequence": 23, - "maxLedgerVersion": 8820051 - } - } - - return this.api.preparePayment( - address, requests.preparePayment.normal, localInstructions).then( - _.partial(checkResult, expectedResponse, 'prepare')); - }); - - it('fee - calculated fee does not use more than 6 decimal places', function () { - this.api.connection._send(JSON.stringify({ - command: 'config', - data: { loadFactor: 5407.96875 } - })); - - const expectedResponse = { - "txJSON": "{\"Flags\":2147483648,\"TransactionType\":\"Payment\",\"Account\":\"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59\",\"Destination\":\"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo\",\"Amount\":{\"value\":\"0.01\",\"currency\":\"USD\",\"issuer\":\"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM\"},\"SendMax\":{\"value\":\"0.01\",\"currency\":\"USD\",\"issuer\":\"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM\"},\"LastLedgerSequence\":8820051,\"Fee\":\"64896\",\"Sequence\":23}", - "instructions": { - "fee": "0.064896", - "sequence": 23, - "maxLedgerVersion": 8820051 - } - } - - return this.api.preparePayment( - address, requests.preparePayment.normal, instructionsWithMaxLedgerVersionOffset).then( - _.partial(checkResult, expectedResponse, 'prepare')); - }); - - it('getFee custom cushion', function () { - this.api._feeCushion = 1.4; - return this.api.getFee().then(fee => { - assert.strictEqual(fee, '0.000014'); - }); - }); - - // This is not recommended since it may result in attempting to pay - // less than the base fee. However, this test verifies - // the existing behavior. - it('getFee cushion less than 1.0', function () { - this.api._feeCushion = 0.9; - return this.api.getFee().then(fee => { - assert.strictEqual(fee, '0.000009'); - }); - }); - - it('disconnect & isConnected', function () { - assert.strictEqual(this.api.isConnected(), true); - return this.api.disconnect().then(() => { - assert.strictEqual(this.api.isConnected(), false); - }); - }); - - it('getLedgerVersion', function (done) { - this.api.getLedgerVersion().then(ver => { - assert.strictEqual(ver, 8819951); - done(); - }, done); - }); - - it('getFeeBase', function (done) { - this.api.connection.getFeeBase().then(fee => { - assert.strictEqual(fee, 10); - done(); - }, done); - }); - - it('getFeeRef', function (done) { - this.api.connection.getFeeRef().then(fee => { - assert.strictEqual(fee, 10); - done(); - }, done); - }); - - it('computeLedgerHash - given corrupt data - should fail', function () { - const request = { - includeTransactions: true, - includeState: true, - includeAllData: true, - ledgerVersion: 38129 - }; - return this.api.getLedger(request).then(ledger => { - assert.strictEqual(ledger.transactions[0].rawTransaction, "{\"Account\":\"r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV\",\"Amount\":\"10000000000\",\"Destination\":\"rLQBHVhFnaC5gLEkgr6HgBJJ3bgeZHg9cj\",\"Fee\":\"10\",\"Flags\":0,\"Sequence\":62,\"SigningPubKey\":\"034AADB09CFF4A4804073701EC53C3510CDC95917C2BB0150FB742D0C66E6CEE9E\",\"TransactionType\":\"Payment\",\"TxnSignature\":\"3045022022EB32AECEF7C644C891C19F87966DF9C62B1F34BABA6BE774325E4BB8E2DD62022100A51437898C28C2B297112DF8131F2BB39EA5FE613487DDD611525F1796264639\",\"hash\":\"3B1A4E1C9BB6A7208EB146BCDB86ECEA6068ED01466D933528CA2B4C64F753EF\",\"meta\":{\"AffectedNodes\":[{\"CreatedNode\":{\"LedgerEntryType\":\"AccountRoot\",\"LedgerIndex\":\"4C6ACBD635B0F07101F7FA25871B0925F8836155462152172755845CE691C49E\",\"NewFields\":{\"Account\":\"rLQBHVhFnaC5gLEkgr6HgBJJ3bgeZHg9cj\",\"Balance\":\"10000000000\",\"Sequence\":1}}},{\"ModifiedNode\":{\"FinalFields\":{\"Account\":\"r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV\",\"Balance\":\"981481999380\",\"Flags\":0,\"OwnerCount\":0,\"Sequence\":63},\"LedgerEntryType\":\"AccountRoot\",\"LedgerIndex\":\"B33FDD5CF3445E1A7F2BE9B06336BEBD73A5E3EE885D3EF93F7E3E2992E46F1A\",\"PreviousFields\":{\"Balance\":\"991481999390\",\"Sequence\":62},\"PreviousTxnID\":\"2485FDC606352F1B0785DA5DE96FB9DBAF43EB60ECBB01B7F6FA970F512CDA5F\",\"PreviousTxnLgrSeq\":31317}}],\"TransactionIndex\":0,\"TransactionResult\":\"tesSUCCESS\"},\"ledger_index\":38129}"); - - // Change Amount to 12000000000 - ledger.transactions[0].rawTransaction = "{\"Account\":\"r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV\",\"Amount\":\"12000000000\",\"Destination\":\"rLQBHVhFnaC5gLEkgr6HgBJJ3bgeZHg9cj\",\"Fee\":\"10\",\"Flags\":0,\"Sequence\":62,\"SigningPubKey\":\"034AADB09CFF4A4804073701EC53C3510CDC95917C2BB0150FB742D0C66E6CEE9E\",\"TransactionType\":\"Payment\",\"TxnSignature\":\"3045022022EB32AECEF7C644C891C19F87966DF9C62B1F34BABA6BE774325E4BB8E2DD62022100A51437898C28C2B297112DF8131F2BB39EA5FE613487DDD611525F1796264639\",\"hash\":\"3B1A4E1C9BB6A7208EB146BCDB86ECEA6068ED01466D933528CA2B4C64F753EF\",\"meta\":{\"AffectedNodes\":[{\"CreatedNode\":{\"LedgerEntryType\":\"AccountRoot\",\"LedgerIndex\":\"4C6ACBD635B0F07101F7FA25871B0925F8836155462152172755845CE691C49E\",\"NewFields\":{\"Account\":\"rLQBHVhFnaC5gLEkgr6HgBJJ3bgeZHg9cj\",\"Balance\":\"10000000000\",\"Sequence\":1}}},{\"ModifiedNode\":{\"FinalFields\":{\"Account\":\"r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV\",\"Balance\":\"981481999380\",\"Flags\":0,\"OwnerCount\":0,\"Sequence\":63},\"LedgerEntryType\":\"AccountRoot\",\"LedgerIndex\":\"B33FDD5CF3445E1A7F2BE9B06336BEBD73A5E3EE885D3EF93F7E3E2992E46F1A\",\"PreviousFields\":{\"Balance\":\"991481999390\",\"Sequence\":62},\"PreviousTxnID\":\"2485FDC606352F1B0785DA5DE96FB9DBAF43EB60ECBB01B7F6FA970F512CDA5F\",\"PreviousTxnLgrSeq\":31317}}],\"TransactionIndex\":0,\"TransactionResult\":\"tesSUCCESS\"},\"ledger_index\":38129}"; - - ledger.parentCloseTime = ledger.closeTime; - - let hash; - try { - hash = this.api.computeLedgerHash(ledger, {computeTreeHashes: true}); - } catch (error) { - assert(error instanceof this.api.errors.ValidationError); - assert.strictEqual(error.message, 'transactionHash in header does not match computed hash of transactions'); - assert.deepStrictEqual(error.data, { - transactionHashInHeader: 'DB83BF807416C5B3499A73130F843CF615AB8E797D79FE7D330ADF1BFA93951A', - computedHashOfTransactions: 'EAA1ADF4D627339450F0E95EA88B7069186DD64230BAEBDCF3EEC4D616A9FC68' - }); - return; - } - assert(false, 'Should throw ValidationError instead of producing hash: ' + hash); - }); - }); - - it('computeLedgerHash - given ledger without raw transactions - should throw', function () { - const request = { - includeTransactions: true, - includeState: true, - includeAllData: true, - ledgerVersion: 38129 - }; - return this.api.getLedger(request).then(ledger => { - assert.strictEqual(ledger.transactions[0].rawTransaction, "{\"Account\":\"r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV\",\"Amount\":\"10000000000\",\"Destination\":\"rLQBHVhFnaC5gLEkgr6HgBJJ3bgeZHg9cj\",\"Fee\":\"10\",\"Flags\":0,\"Sequence\":62,\"SigningPubKey\":\"034AADB09CFF4A4804073701EC53C3510CDC95917C2BB0150FB742D0C66E6CEE9E\",\"TransactionType\":\"Payment\",\"TxnSignature\":\"3045022022EB32AECEF7C644C891C19F87966DF9C62B1F34BABA6BE774325E4BB8E2DD62022100A51437898C28C2B297112DF8131F2BB39EA5FE613487DDD611525F1796264639\",\"hash\":\"3B1A4E1C9BB6A7208EB146BCDB86ECEA6068ED01466D933528CA2B4C64F753EF\",\"meta\":{\"AffectedNodes\":[{\"CreatedNode\":{\"LedgerEntryType\":\"AccountRoot\",\"LedgerIndex\":\"4C6ACBD635B0F07101F7FA25871B0925F8836155462152172755845CE691C49E\",\"NewFields\":{\"Account\":\"rLQBHVhFnaC5gLEkgr6HgBJJ3bgeZHg9cj\",\"Balance\":\"10000000000\",\"Sequence\":1}}},{\"ModifiedNode\":{\"FinalFields\":{\"Account\":\"r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV\",\"Balance\":\"981481999380\",\"Flags\":0,\"OwnerCount\":0,\"Sequence\":63},\"LedgerEntryType\":\"AccountRoot\",\"LedgerIndex\":\"B33FDD5CF3445E1A7F2BE9B06336BEBD73A5E3EE885D3EF93F7E3E2992E46F1A\",\"PreviousFields\":{\"Balance\":\"991481999390\",\"Sequence\":62},\"PreviousTxnID\":\"2485FDC606352F1B0785DA5DE96FB9DBAF43EB60ECBB01B7F6FA970F512CDA5F\",\"PreviousTxnLgrSeq\":31317}}],\"TransactionIndex\":0,\"TransactionResult\":\"tesSUCCESS\"},\"ledger_index\":38129}"); - - // Delete rawTransaction - delete ledger.transactions[0].rawTransaction; - - ledger.parentCloseTime = ledger.closeTime; - - let hash; - try { - hash = this.api.computeLedgerHash(ledger, {computeTreeHashes: true}); - } catch (error) { - assert(error instanceof this.api.errors.ValidationError); - assert.strictEqual(error.message, 'ledger' - + ' is missing raw transactions'); - return; - } - assert(false, 'Should throw ValidationError instead of producing hash: ' + hash); - }); - }); - - - it('computeLedgerHash - given ledger without state or transactions - only compute ledger hash', function () { - const request = { - includeTransactions: true, - includeState: true, - includeAllData: true, - ledgerVersion: 38129 - }; - return this.api.getLedger(request).then(ledger => { - assert.strictEqual(ledger.transactions[0].rawTransaction, "{\"Account\":\"r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV\",\"Amount\":\"10000000000\",\"Destination\":\"rLQBHVhFnaC5gLEkgr6HgBJJ3bgeZHg9cj\",\"Fee\":\"10\",\"Flags\":0,\"Sequence\":62,\"SigningPubKey\":\"034AADB09CFF4A4804073701EC53C3510CDC95917C2BB0150FB742D0C66E6CEE9E\",\"TransactionType\":\"Payment\",\"TxnSignature\":\"3045022022EB32AECEF7C644C891C19F87966DF9C62B1F34BABA6BE774325E4BB8E2DD62022100A51437898C28C2B297112DF8131F2BB39EA5FE613487DDD611525F1796264639\",\"hash\":\"3B1A4E1C9BB6A7208EB146BCDB86ECEA6068ED01466D933528CA2B4C64F753EF\",\"meta\":{\"AffectedNodes\":[{\"CreatedNode\":{\"LedgerEntryType\":\"AccountRoot\",\"LedgerIndex\":\"4C6ACBD635B0F07101F7FA25871B0925F8836155462152172755845CE691C49E\",\"NewFields\":{\"Account\":\"rLQBHVhFnaC5gLEkgr6HgBJJ3bgeZHg9cj\",\"Balance\":\"10000000000\",\"Sequence\":1}}},{\"ModifiedNode\":{\"FinalFields\":{\"Account\":\"r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV\",\"Balance\":\"981481999380\",\"Flags\":0,\"OwnerCount\":0,\"Sequence\":63},\"LedgerEntryType\":\"AccountRoot\",\"LedgerIndex\":\"B33FDD5CF3445E1A7F2BE9B06336BEBD73A5E3EE885D3EF93F7E3E2992E46F1A\",\"PreviousFields\":{\"Balance\":\"991481999390\",\"Sequence\":62},\"PreviousTxnID\":\"2485FDC606352F1B0785DA5DE96FB9DBAF43EB60ECBB01B7F6FA970F512CDA5F\",\"PreviousTxnLgrSeq\":31317}}],\"TransactionIndex\":0,\"TransactionResult\":\"tesSUCCESS\"},\"ledger_index\":38129}"); - - ledger.parentCloseTime = ledger.closeTime; - - const computeLedgerHash = this.api.computeLedgerHash; - const ValidationError = this.api.errors.ValidationError - function testCompute(ledger, expectedError) { - let hash = computeLedgerHash(ledger); - assert.strictEqual(hash, - 'E6DB7365949BF9814D76BCC730B01818EB9136A89DB224F3F9F5AAE4569D758E'); - - // fail if required to compute tree hashes - try { - hash = computeLedgerHash(ledger, {computeTreeHashes: true}); - } catch (error) { - assert(error instanceof ValidationError); - assert.strictEqual(error.message, expectedError); - return; - } - assert(false, 'Should throw ValidationError instead of producing hash: ' + hash); - } - - const transactions = ledger.transactions; - delete ledger.transactions; - testCompute(ledger, 'transactions property is missing from the ledger'); - delete ledger.rawState; - testCompute(ledger, 'transactions property is missing from the ledger'); - ledger.transactions = transactions; - testCompute(ledger, 'rawState property is missing from the ledger'); - }); - }); - - it('computeLedgerHash - wrong hash', function () { - const request = { - includeTransactions: true, - includeState: true, - includeAllData: true, - ledgerVersion: 38129 - }; - return this.api.getLedger(request).then( - _.partial(checkResult, responses.getLedger.full, 'getLedger')) - .then(response => { - const ledger = _.assign({}, response, { - parentCloseTime: response.closeTime, stateHash: - 'D9ABF622DA26EEEE48203085D4BC23B0F77DC6F8724AC33D975DA3CA492D2E44' - }); - assert.throws(() => { - this.api.computeLedgerHash(ledger); - }, /does not match computed hash of state/); - }); - }); - - it('RippleError with data', function () { - const error = new this.api.errors.RippleError('_message_', '_data_'); - assert.strictEqual(error.toString(), - '[RippleError(_message_, \'_data_\')]'); - }); - - it('NotFoundError default message', function () { - const error = new this.api.errors.NotFoundError(); - assert.strictEqual(error.toString(), - '[NotFoundError(Not found)]'); - }); - - it('common utils - toRippledAmount', function () { - const amount = { issuer: 'is', currency: 'c', value: 'v' }; - - assert.deepEqual(utils.common.toRippledAmount(amount), { - issuer: 'is', currency: 'c', value: 'v' - }); - }); - - it('ledger utils - renameCounterpartyToIssuerInOrder', function () { - const order = { - taker_gets: { counterparty: '1', currency: 'XRP' }, - taker_pays: { counterparty: '1', currency: 'XRP' } - }; - const expected = { - taker_gets: { issuer: '1', currency: 'XRP' }, - taker_pays: { issuer: '1', currency: 'XRP' } - }; - assert.deepEqual(utils.renameCounterpartyToIssuerInOrder(order), expected); - }); - - it('ledger utils - compareTransactions', function () { - // @ts-ignore - assert.strictEqual(utils.compareTransactions({}, {}), 0); - let first: any = { outcome: { ledgerVersion: 1, indexInLedger: 100 } }; - let second: any = { outcome: { ledgerVersion: 1, indexInLedger: 200 } }; - - assert.strictEqual(utils.compareTransactions(first, second), -1); - - first = { outcome: { ledgerVersion: 1, indexInLedger: 100 } }; - second = { outcome: { ledgerVersion: 1, indexInLedger: 100 } }; - - assert.strictEqual(utils.compareTransactions(first, second), 0); - - first = { outcome: { ledgerVersion: 1, indexInLedger: 200 } }; - second = { outcome: { ledgerVersion: 1, indexInLedger: 100 } }; - - assert.strictEqual(utils.compareTransactions(first, second), 1); - }); - - it('ledger utils - getRecursive', function () { - function getter(marker) { - return new Promise((resolve, reject) => { - if (marker !== undefined) { - reject(new Error()); - return; - } - resolve({ marker: 'A', results: [1] }); - }); - } - return utils.getRecursive(getter, 10).then(() => { - assert(false, 'Should throw Error'); - }).catch(error => { - assert(error instanceof Error); - }); - }); - - describe('schema-validator', function () { - it('valid', function () { - assert.doesNotThrow(function () { - schemaValidator.schemaValidate('hash256', - '0F7ED9F40742D8A513AE86029462B7A6768325583DF8EE21B7EC663019DD6A0F'); - }); - }); - - it('invalid', function () { - assert.throws(function () { - schemaValidator.schemaValidate('hash256', 'invalid'); - }, this.api.errors.ValidationError); - }); - - it('invalid - empty value', function () { - assert.throws(function () { - schemaValidator.schemaValidate('hash256', ''); - }, this.api.errors.ValidationError); - }); - - it('schema not found error', function () { - assert.throws(function () { - schemaValidator.schemaValidate('unexisting', 'anything'); - }, /no schema/); - }); - - }); - - describe('validator', function () { - - it('validateLedgerRange', function () { - const options = { - minLedgerVersion: 20000, - maxLedgerVersion: 10000 - }; - const thunk = _.partial(validate.getTransactions, - { address, options }); - assert.throws(thunk, this.api.errors.ValidationError); - assert.throws(thunk, - /minLedgerVersion must not be greater than maxLedgerVersion/); - }); - - it('secret', function () { - function validateSecret(secret) { - validate.sign({ txJSON: '', secret }); - } - assert.doesNotThrow(_.partial(validateSecret, - 'shzjfakiK79YQdMjy4h8cGGfQSV6u')); - assert.throws(_.partial(validateSecret, - 'shzjfakiK79YQdMjy4h8cGGfQSV6v'), this.api.errors.ValidationError); - assert.throws(_.partial(validateSecret, 1), - this.api.errors.ValidationError); - assert.throws(_.partial(validateSecret, ''), - this.api.errors.ValidationError); - assert.throws(_.partial(validateSecret, 's!!!'), - this.api.errors.ValidationError); - assert.throws(_.partial(validateSecret, 'passphrase'), - this.api.errors.ValidationError); - // 32 0s is a valid hex repr of seed bytes - const hex = new Array(33).join('0'); - assert.throws(_.partial(validateSecret, hex), - this.api.errors.ValidationError); - }); - - }); - - it('ledger event', function (done) { - this.api.on('ledger', message => { - checkResult(responses.ledgerEvent, 'ledgerEvent', message); - done(); - }); - closeLedger(this.api.connection); - }); -}); - -describe('RippleAPI - offline', function () { - it('prepareSettings and sign', function () { - const api = new RippleAPI(); - const secret = 'shsWGZcmZz6YsWWmcnpfr6fLTdtFV'; - const settings = requests.prepareSettings.domain; - const instructions = { - sequence: 23, - maxLedgerVersion: 8820051, - fee: '0.000012' - }; - return api.prepareSettings(address, settings, instructions).then(data => { - checkResult(responses.prepareSettings.flags, 'prepare', data); - assert.deepEqual(api.sign(data.txJSON, secret), - responses.prepareSettings.signed); - }); - }); - - it('getServerInfo - offline', function () { - const api = new RippleAPI(); - return api.getServerInfo().then(() => { - assert(false, 'Should throw error'); - }).catch(error => { - assert(error instanceof api.errors.NotConnectedError); - }); - }); - - it('computeLedgerHash', function () { - const api = new RippleAPI(); - const header = requests.computeLedgerHash.header; - const ledgerHash = api.computeLedgerHash(header); - assert.strictEqual(ledgerHash, - 'F4D865D83EB88C1A1911B9E90641919A1314F36E1B099F8E95FE3B7C77BE3349'); - }); - - it('computeLedgerHash - with transactions', function () { - const api = new RippleAPI(); - const header = _.omit(requests.computeLedgerHash.header, - 'transactionHash'); - header.rawTransactions = JSON.stringify( - requests.computeLedgerHash.transactions); - const ledgerHash = api.computeLedgerHash(header); - assert.strictEqual(ledgerHash, - 'F4D865D83EB88C1A1911B9E90641919A1314F36E1B099F8E95FE3B7C77BE3349'); - }); - - it('computeLedgerHash - incorrent transaction_hash', function () { - const api = new RippleAPI(); - const header = _.assign({}, requests.computeLedgerHash.header, - { - transactionHash: - '325EACC5271322539EEEC2D6A5292471EF1B3E72AE7180533EFC3B8F0AD435C9' - }); - header.rawTransactions = JSON.stringify( - requests.computeLedgerHash.transactions); - assert.throws(() => api.computeLedgerHash(header)); - }); - - it('RippleAPI - implicit server port', function () { - new RippleAPI({ server: 'wss://s1.ripple.com' }); - }); - - it('RippleAPI invalid options', function () { - assert.throws(() => new RippleAPI({ invalid: true } as any)); - }); - - it('RippleAPI valid options', function () { - const api = new RippleAPI({ server: 'wss://s:1' }); - const privateConnectionUrl = (api.connection as any)._url; - assert.deepEqual(privateConnectionUrl, 'wss://s:1'); - }); - - it('RippleAPI invalid server uri', function () { - assert.throws(() => new RippleAPI({ server: 'wss//s:1' })); - }); - - xit('RippleAPI connect() times out after 2 seconds', function () { - // TODO: Use a timer mock like https://jestjs.io/docs/en/timer-mocks - // to test that connect() times out after 2 seconds. - }); -}); diff --git a/test/api/combine/index.ts b/test/api/combine/index.ts new file mode 100644 index 00000000..618a2bef --- /dev/null +++ b/test/api/combine/index.ts @@ -0,0 +1,29 @@ +import assert from 'assert-diff' +import binary from 'ripple-binary-codec' +import requests from '../../fixtures/requests' +import responses from '../../fixtures/responses' +import { assertResultMatch, TestSuite } from '../../utils' +const { combine: REQUEST_FIXTURES } = requests +const { combine: RESPONSE_FIXTURES } = responses + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + 'combine': async (api, address) => { + const combined = api.combine(REQUEST_FIXTURES.setDomain) + assertResultMatch(combined, RESPONSE_FIXTURES.single, 'sign') + }, + + 'combine - different transactions': async (api, address) => { + const request = [REQUEST_FIXTURES.setDomain[0]] + const tx = binary.decode(REQUEST_FIXTURES.setDomain[0]) + tx.Flags = 0 + request.push(binary.encode(tx)) + assert.throws(() => { + api.combine(request) + }, /txJSON is not the same for all signedTransactions/) + } +} diff --git a/test/api/computeLedgerHash/index.ts b/test/api/computeLedgerHash/index.ts new file mode 100644 index 00000000..410f90a5 --- /dev/null +++ b/test/api/computeLedgerHash/index.ts @@ -0,0 +1,191 @@ +import assert from 'assert-diff' +import requests from '../../fixtures/requests' +import responses from '../../fixtures/responses' +import { assertResultMatch, TestSuite } from '../../utils' +const { computeLedgerHash: REQUEST_FIXTURES } = requests + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + 'given corrupt data - should fail': async (api, address) => { + const request = { + includeTransactions: true, + includeState: true, + includeAllData: true, + ledgerVersion: 38129 + } + const ledger = await api.getLedger(request) + assert.strictEqual( + // @ts-ignore + ledger.transactions[0].rawTransaction, + '{"Account":"r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV","Amount":"10000000000","Destination":"rLQBHVhFnaC5gLEkgr6HgBJJ3bgeZHg9cj","Fee":"10","Flags":0,"Sequence":62,"SigningPubKey":"034AADB09CFF4A4804073701EC53C3510CDC95917C2BB0150FB742D0C66E6CEE9E","TransactionType":"Payment","TxnSignature":"3045022022EB32AECEF7C644C891C19F87966DF9C62B1F34BABA6BE774325E4BB8E2DD62022100A51437898C28C2B297112DF8131F2BB39EA5FE613487DDD611525F1796264639","hash":"3B1A4E1C9BB6A7208EB146BCDB86ECEA6068ED01466D933528CA2B4C64F753EF","meta":{"AffectedNodes":[{"CreatedNode":{"LedgerEntryType":"AccountRoot","LedgerIndex":"4C6ACBD635B0F07101F7FA25871B0925F8836155462152172755845CE691C49E","NewFields":{"Account":"rLQBHVhFnaC5gLEkgr6HgBJJ3bgeZHg9cj","Balance":"10000000000","Sequence":1}}},{"ModifiedNode":{"FinalFields":{"Account":"r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV","Balance":"981481999380","Flags":0,"OwnerCount":0,"Sequence":63},"LedgerEntryType":"AccountRoot","LedgerIndex":"B33FDD5CF3445E1A7F2BE9B06336BEBD73A5E3EE885D3EF93F7E3E2992E46F1A","PreviousFields":{"Balance":"991481999390","Sequence":62},"PreviousTxnID":"2485FDC606352F1B0785DA5DE96FB9DBAF43EB60ECBB01B7F6FA970F512CDA5F","PreviousTxnLgrSeq":31317}}],"TransactionIndex":0,"TransactionResult":"tesSUCCESS"},"ledger_index":38129}' + ) + // @ts-ignore - Change Amount to 12000000000 + ledger.transactions[0].rawTransaction = + '{"Account":"r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV","Amount":"12000000000","Destination":"rLQBHVhFnaC5gLEkgr6HgBJJ3bgeZHg9cj","Fee":"10","Flags":0,"Sequence":62,"SigningPubKey":"034AADB09CFF4A4804073701EC53C3510CDC95917C2BB0150FB742D0C66E6CEE9E","TransactionType":"Payment","TxnSignature":"3045022022EB32AECEF7C644C891C19F87966DF9C62B1F34BABA6BE774325E4BB8E2DD62022100A51437898C28C2B297112DF8131F2BB39EA5FE613487DDD611525F1796264639","hash":"3B1A4E1C9BB6A7208EB146BCDB86ECEA6068ED01466D933528CA2B4C64F753EF","meta":{"AffectedNodes":[{"CreatedNode":{"LedgerEntryType":"AccountRoot","LedgerIndex":"4C6ACBD635B0F07101F7FA25871B0925F8836155462152172755845CE691C49E","NewFields":{"Account":"rLQBHVhFnaC5gLEkgr6HgBJJ3bgeZHg9cj","Balance":"10000000000","Sequence":1}}},{"ModifiedNode":{"FinalFields":{"Account":"r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV","Balance":"981481999380","Flags":0,"OwnerCount":0,"Sequence":63},"LedgerEntryType":"AccountRoot","LedgerIndex":"B33FDD5CF3445E1A7F2BE9B06336BEBD73A5E3EE885D3EF93F7E3E2992E46F1A","PreviousFields":{"Balance":"991481999390","Sequence":62},"PreviousTxnID":"2485FDC606352F1B0785DA5DE96FB9DBAF43EB60ECBB01B7F6FA970F512CDA5F","PreviousTxnLgrSeq":31317}}],"TransactionIndex":0,"TransactionResult":"tesSUCCESS"},"ledger_index":38129}' + ledger.parentCloseTime = ledger.closeTime + let hash + try { + hash = api.computeLedgerHash(ledger, { computeTreeHashes: true }) + } catch (error) { + assert(error instanceof api.errors.ValidationError) + assert.strictEqual( + error.message, + 'transactionHash in header does not match computed hash of transactions' + ) + assert.deepStrictEqual(error.data, { + transactionHashInHeader: + 'DB83BF807416C5B3499A73130F843CF615AB8E797D79FE7D330ADF1BFA93951A', + computedHashOfTransactions: + 'EAA1ADF4D627339450F0E95EA88B7069186DD64230BAEBDCF3EEC4D616A9FC68' + }) + return + } + assert( + false, + 'Should throw ValidationError instead of producing hash: ' + hash + ) + }, + + 'given ledger without raw transactions - should throw': async ( + api, + address + ) => { + const request = { + includeTransactions: true, + includeState: true, + includeAllData: true, + ledgerVersion: 38129 + } + const ledger = await api.getLedger(request) + assert.strictEqual( + // @ts-ignore + ledger.transactions[0].rawTransaction, + '{"Account":"r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV","Amount":"10000000000","Destination":"rLQBHVhFnaC5gLEkgr6HgBJJ3bgeZHg9cj","Fee":"10","Flags":0,"Sequence":62,"SigningPubKey":"034AADB09CFF4A4804073701EC53C3510CDC95917C2BB0150FB742D0C66E6CEE9E","TransactionType":"Payment","TxnSignature":"3045022022EB32AECEF7C644C891C19F87966DF9C62B1F34BABA6BE774325E4BB8E2DD62022100A51437898C28C2B297112DF8131F2BB39EA5FE613487DDD611525F1796264639","hash":"3B1A4E1C9BB6A7208EB146BCDB86ECEA6068ED01466D933528CA2B4C64F753EF","meta":{"AffectedNodes":[{"CreatedNode":{"LedgerEntryType":"AccountRoot","LedgerIndex":"4C6ACBD635B0F07101F7FA25871B0925F8836155462152172755845CE691C49E","NewFields":{"Account":"rLQBHVhFnaC5gLEkgr6HgBJJ3bgeZHg9cj","Balance":"10000000000","Sequence":1}}},{"ModifiedNode":{"FinalFields":{"Account":"r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV","Balance":"981481999380","Flags":0,"OwnerCount":0,"Sequence":63},"LedgerEntryType":"AccountRoot","LedgerIndex":"B33FDD5CF3445E1A7F2BE9B06336BEBD73A5E3EE885D3EF93F7E3E2992E46F1A","PreviousFields":{"Balance":"991481999390","Sequence":62},"PreviousTxnID":"2485FDC606352F1B0785DA5DE96FB9DBAF43EB60ECBB01B7F6FA970F512CDA5F","PreviousTxnLgrSeq":31317}}],"TransactionIndex":0,"TransactionResult":"tesSUCCESS"},"ledger_index":38129}' + ) + // Delete rawTransaction + // @ts-ignore - Delete rawTransaction + delete ledger.transactions[0].rawTransaction + ledger.parentCloseTime = ledger.closeTime + let hash + try { + hash = api.computeLedgerHash(ledger, { computeTreeHashes: true }) + } catch (error) { + assert(error instanceof api.errors.ValidationError) + assert.strictEqual( + error.message, + 'ledger' + ' is missing raw transactions' + ) + return + } + assert( + false, + 'Should throw ValidationError instead of producing hash: ' + hash + ) + }, + + 'given ledger without state or transactions - only compute ledger hash': async ( + api, + address + ) => { + const request = { + includeTransactions: true, + includeState: true, + includeAllData: true, + ledgerVersion: 38129 + } + const ledger = await api.getLedger(request) + assert.strictEqual( + // @ts-ignore + ledger.transactions[0].rawTransaction, + '{"Account":"r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV","Amount":"10000000000","Destination":"rLQBHVhFnaC5gLEkgr6HgBJJ3bgeZHg9cj","Fee":"10","Flags":0,"Sequence":62,"SigningPubKey":"034AADB09CFF4A4804073701EC53C3510CDC95917C2BB0150FB742D0C66E6CEE9E","TransactionType":"Payment","TxnSignature":"3045022022EB32AECEF7C644C891C19F87966DF9C62B1F34BABA6BE774325E4BB8E2DD62022100A51437898C28C2B297112DF8131F2BB39EA5FE613487DDD611525F1796264639","hash":"3B1A4E1C9BB6A7208EB146BCDB86ECEA6068ED01466D933528CA2B4C64F753EF","meta":{"AffectedNodes":[{"CreatedNode":{"LedgerEntryType":"AccountRoot","LedgerIndex":"4C6ACBD635B0F07101F7FA25871B0925F8836155462152172755845CE691C49E","NewFields":{"Account":"rLQBHVhFnaC5gLEkgr6HgBJJ3bgeZHg9cj","Balance":"10000000000","Sequence":1}}},{"ModifiedNode":{"FinalFields":{"Account":"r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV","Balance":"981481999380","Flags":0,"OwnerCount":0,"Sequence":63},"LedgerEntryType":"AccountRoot","LedgerIndex":"B33FDD5CF3445E1A7F2BE9B06336BEBD73A5E3EE885D3EF93F7E3E2992E46F1A","PreviousFields":{"Balance":"991481999390","Sequence":62},"PreviousTxnID":"2485FDC606352F1B0785DA5DE96FB9DBAF43EB60ECBB01B7F6FA970F512CDA5F","PreviousTxnLgrSeq":31317}}],"TransactionIndex":0,"TransactionResult":"tesSUCCESS"},"ledger_index":38129}' + ) + ledger.parentCloseTime = ledger.closeTime + const computeLedgerHash = api.computeLedgerHash + const ValidationError = api.errors.ValidationError + function testCompute(ledger, expectedError) { + let hash = computeLedgerHash(ledger) + assert.strictEqual( + hash, + 'E6DB7365949BF9814D76BCC730B01818EB9136A89DB224F3F9F5AAE4569D758E' + ) + // fail if required to compute tree hashes + try { + hash = computeLedgerHash(ledger, { computeTreeHashes: true }) + } catch (error) { + assert(error instanceof ValidationError) + assert.strictEqual(error.message, expectedError) + return + } + assert( + false, + 'Should throw ValidationError instead of producing hash: ' + hash + ) + } + + const transactions = ledger.transactions + delete ledger.transactions + testCompute(ledger, 'transactions property is missing from the ledger') + delete ledger.rawState + testCompute(ledger, 'transactions property is missing from the ledger') + ledger.transactions = transactions + testCompute(ledger, 'rawState property is missing from the ledger') + }, + + 'wrong hash': async (api, address) => { + const request = { + includeTransactions: true, + includeState: true, + includeAllData: true, + ledgerVersion: 38129 + } + const ledger = await api.getLedger(request) + assertResultMatch(ledger, responses.getLedger.full, 'getLedger') + const newLedger = { + ...ledger, + parentCloseTime: ledger.closeTime, + stateHash: + 'D9ABF622DA26EEEE48203085D4BC23B0F77DC6F8724AC33D975DA3CA492D2E44' + } + assert.throws(() => { + api.computeLedgerHash(newLedger) + }, /does not match computed hash of state/) + }, + + 'computeLedgerHash': async (api, address) => { + // const api = new RippleAPI() + const header = REQUEST_FIXTURES.header + const ledgerHash = api.computeLedgerHash(header) + assert.strictEqual( + ledgerHash, + 'F4D865D83EB88C1A1911B9E90641919A1314F36E1B099F8E95FE3B7C77BE3349' + ) + }, + + 'computeLedgerHash - with transactions': async (api, address) => { + // const api = new RippleAPI() + const header = { + ...REQUEST_FIXTURES.header, + transactionHash: undefined, + rawTransactions: JSON.stringify(REQUEST_FIXTURES.transactions) + } + const ledgerHash = api.computeLedgerHash(header) + assert.strictEqual( + ledgerHash, + 'F4D865D83EB88C1A1911B9E90641919A1314F36E1B099F8E95FE3B7C77BE3349' + ) + }, + + 'computeLedgerHash - incorrent transaction_hash': async (api, address) => { + // const api = new RippleAPI() + const header = Object.assign({}, REQUEST_FIXTURES.header, { + transactionHash: + '325EACC5271322539EEEC2D6A5292471EF1B3E72AE7180533EFC3B8F0AD435C9' + }) + header.rawTransactions = JSON.stringify(REQUEST_FIXTURES.transactions) + assert.throws(() => api.computeLedgerHash(header)) + } +} diff --git a/test/api/constructor/index.ts b/test/api/constructor/index.ts new file mode 100644 index 00000000..21b99c59 --- /dev/null +++ b/test/api/constructor/index.ts @@ -0,0 +1,29 @@ +import assert from 'assert-diff' +import { TestSuite } from '../../utils' +import { RippleAPI } from 'ripple-api' + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + 'RippleAPI - implicit server port': () => { + new RippleAPI({ server: 'wss://s1.ripple.com' }) + }, + + 'RippleAPI invalid options': () => { + // @ts-ignore - This is intentionally invalid + assert.throws(() => new RippleAPI({ invalid: true })) + }, + + 'RippleAPI valid options': () => { + const api = new RippleAPI({ server: 'wss://s:1' }) + const privateConnectionUrl = (api.connection as any)._url + assert.deepEqual(privateConnectionUrl, 'wss://s:1') + }, + + 'RippleAPI invalid server uri': () => { + assert.throws(() => new RippleAPI({ server: 'wss//s:1' })) + } +} diff --git a/test/api/deriveAddress/index.ts b/test/api/deriveAddress/index.ts new file mode 100644 index 00000000..763982d6 --- /dev/null +++ b/test/api/deriveAddress/index.ts @@ -0,0 +1,16 @@ +import assert from 'assert-diff' +import { TestSuite } from '../../utils' + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + 'returns address for public key': async (api, address) => { + var address = api.deriveAddress( + '035332FBA71D705BD5D97014A833BE2BBB25BEFCD3506198E14AFEA241B98C2D06' + ) + assert.equal(address, 'rLczgQHxPhWtjkaQqn3Q6UM8AbRbbRvs5K') + } +} diff --git a/test/api/deriveKeypair/index.ts b/test/api/deriveKeypair/index.ts new file mode 100644 index 00000000..259b1a05 --- /dev/null +++ b/test/api/deriveKeypair/index.ts @@ -0,0 +1,39 @@ +import assert from 'assert-diff' +import { TestSuite } from '../../utils' + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + 'returns keypair for secret': async (api, address) => { + var keypair = api.deriveKeypair('snsakdSrZSLkYpCXxfRkS4Sh96PMK') + assert.equal( + keypair.privateKey, + '008850736302221AFD59FF9CA1A29D4975F491D726249302EE48A3078A8934D335' + ) + assert.equal( + keypair.publicKey, + '035332FBA71D705BD5D97014A833BE2BBB25BEFCD3506198E14AFEA241B98C2D06' + ) + }, + + 'returns keypair for ed25519 secret': async (api, address) => { + var keypair = api.deriveKeypair('sEdV9eHWbibBnTj7b1H5kHfPfv7gudx') + assert.equal( + keypair.privateKey, + 'ED5C2EF6C2E3200DFA6B72F47935C7F64D35453646EA34919192538F458C7BC30F' + ) + assert.equal( + keypair.publicKey, + 'ED0805EC4E728DB87C0CA6C420751F296C57A5F42D02E9E6150CE60694A44593E5' + ) + }, + + 'throws with an invalid secret': async (api, address) => { + assert.throws(() => { + api.deriveKeypair('...') + }, /^Error: Non-base58 character$/) + } +} diff --git a/test/api/deriveXAddress/index.ts b/test/api/deriveXAddress/index.ts new file mode 100644 index 00000000..b12acbdf --- /dev/null +++ b/test/api/deriveXAddress/index.ts @@ -0,0 +1,31 @@ +import assert from 'assert-diff' +import { TestSuite } from '../../utils' +import { RippleAPI } from '../../../src' + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + 'returns address for public key': async (api, address) => { + assert.equal( + RippleAPI.deriveXAddress({ + publicKey: + '035332FBA71D705BD5D97014A833BE2BBB25BEFCD3506198E14AFEA241B98C2D06', + tag: false, + test: false + }), + 'XVZVpQj8YSVpNyiwXYSqvQoQqgBttTxAZwMcuJd4xteQHyt' + ) + assert.equal( + RippleAPI.deriveXAddress({ + publicKey: + '035332FBA71D705BD5D97014A833BE2BBB25BEFCD3506198E14AFEA241B98C2D06', + tag: false, + test: true + }), + 'TVVrSWtmQQssgVcmoMBcFQZKKf56QscyWLKnUyiuZW8ALU4' + ) + } +} diff --git a/test/api/dropsToXrp/index.ts b/test/api/dropsToXrp/index.ts new file mode 100644 index 00000000..890522ba --- /dev/null +++ b/test/api/dropsToXrp/index.ts @@ -0,0 +1,119 @@ +import assert from 'assert-diff' +import { TestSuite } from '../../utils' +import BigNumber from 'bignumber.js' + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + 'works with a typical amount': async api => { + const xrp = api.dropsToXrp('2000000') + assert.strictEqual(xrp, '2', '2 million drops equals 2 XRP') + }, + 'works with fractions': async api => { + let xrp = api.dropsToXrp('3456789') + assert.strictEqual(xrp, '3.456789', '3,456,789 drops equals 3.456789 XRP') + + xrp = api.dropsToXrp('3400000') + assert.strictEqual(xrp, '3.4', '3,400,000 drops equals 3.4 XRP') + + xrp = api.dropsToXrp('1') + assert.strictEqual(xrp, '0.000001', '1 drop equals 0.000001 XRP') + + xrp = api.dropsToXrp('1.0') + assert.strictEqual(xrp, '0.000001', '1.0 drops equals 0.000001 XRP') + + xrp = api.dropsToXrp('1.00') + assert.strictEqual(xrp, '0.000001', '1.00 drops equals 0.000001 XRP') + }, + 'works with zero': async api => { + let xrp = api.dropsToXrp('0') + assert.strictEqual(xrp, '0', '0 drops equals 0 XRP') + + // negative zero is equivalent to zero + xrp = api.dropsToXrp('-0') + assert.strictEqual(xrp, '0', '-0 drops equals 0 XRP') + + xrp = api.dropsToXrp('0.00') + assert.strictEqual(xrp, '0', '0.00 drops equals 0 XRP') + + xrp = api.dropsToXrp('000000000') + assert.strictEqual(xrp, '0', '000000000 drops equals 0 XRP') + }, + 'works with a negative value': async api => { + const xrp = api.dropsToXrp('-2000000') + assert.strictEqual(xrp, '-2', '-2 million drops equals -2 XRP') + }, + 'works with a value ending with a decimal point': async api => { + let xrp = api.dropsToXrp('2000000.') + assert.strictEqual(xrp, '2', '2000000. drops equals 2 XRP') + + xrp = api.dropsToXrp('-2000000.') + assert.strictEqual(xrp, '-2', '-2000000. drops equals -2 XRP') + }, + 'works with BigNumber objects': async api => { + let xrp = api.dropsToXrp(new BigNumber(2000000)) + assert.strictEqual(xrp, '2', '(BigNumber) 2 million drops equals 2 XRP') + + xrp = api.dropsToXrp(new BigNumber(-2000000)) + assert.strictEqual(xrp, '-2', '(BigNumber) -2 million drops equals -2 XRP') + + xrp = api.dropsToXrp(new BigNumber(2345678)) + assert.strictEqual( + xrp, + '2.345678', + '(BigNumber) 2,345,678 drops equals 2.345678 XRP' + ) + + xrp = api.dropsToXrp(new BigNumber(-2345678)) + assert.strictEqual( + xrp, + '-2.345678', + '(BigNumber) -2,345,678 drops equals -2.345678 XRP' + ) + }, + 'works with a number': async api => { + // This is not recommended. Use strings or BigNumber objects to avoid precision errors. + let xrp = api.dropsToXrp(2000000) + assert.strictEqual(xrp, '2', '(number) 2 million drops equals 2 XRP') + xrp = api.dropsToXrp(-2000000) + assert.strictEqual(xrp, '-2', '(number) -2 million drops equals -2 XRP') + }, + 'throws with an amount with too many decimal places': async api => { + assert.throws(() => { + api.dropsToXrp('1.2') + }, /has too many decimal places/) + + assert.throws(() => { + api.dropsToXrp('0.10') + }, /has too many decimal places/) + }, + 'throws with an invalid value': async api => { + assert.throws(() => { + api.dropsToXrp('FOO') + }, /invalid value/) + + assert.throws(() => { + api.dropsToXrp('1e-7') + }, /invalid value/) + + assert.throws(() => { + api.dropsToXrp('2,0') + }, /invalid value/) + + assert.throws(() => { + api.dropsToXrp('.') + }, /dropsToXrp: invalid value '\.', should be a BigNumber or string-encoded number\./) + }, + 'throws with an amount more than one decimal point': async api => { + assert.throws(() => { + api.dropsToXrp('1.0.0') + }, /dropsToXrp: invalid value '1\.0\.0'/) + + assert.throws(() => { + api.dropsToXrp('...') + }, /dropsToXrp: invalid value '\.\.\.'/) + } +} diff --git a/test/api/errors/index.ts b/test/api/errors/index.ts new file mode 100644 index 00000000..6c528c32 --- /dev/null +++ b/test/api/errors/index.ts @@ -0,0 +1,19 @@ +import assert from 'assert-diff' +import { TestSuite } from '../../utils' + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + 'RippleError with data': async (api, address) => { + const error = new api.errors.RippleError('_message_', '_data_') + assert.strictEqual(error.toString(), "[RippleError(_message_, '_data_')]") + }, + + 'NotFoundError default message': async (api, address) => { + const error = new api.errors.NotFoundError() + assert.strictEqual(error.toString(), '[NotFoundError(Not found)]') + } +} diff --git a/test/api/formatBidsAndAsks/index.ts b/test/api/formatBidsAndAsks/index.ts new file mode 100644 index 00000000..37840026 --- /dev/null +++ b/test/api/formatBidsAndAsks/index.ts @@ -0,0 +1,384 @@ +import BigNumber from 'bignumber.js' +import assert from 'assert-diff' +import { RippleAPI } from 'ripple-api' +import requests from '../../fixtures/requests' +import responses from '../../fixtures/responses' +import { TestSuite } from '../../utils' + +function checkSortingOfOrders(orders) { + let previousRate = '0' + for (var i = 0; i < orders.length; i++) { + const order = orders[i] + let rate + + // We calculate the quality of output/input here as a test. + // This won't hold in general because when output and input amounts get tiny, + // the quality can differ significantly. However, the offer stays in the + // order book where it was originally placed. It would be more consistent + // to check the quality from the offer book, but for the test data set, + // this calculation holds. + + if (order.specification.direction === 'buy') { + rate = new BigNumber(order.specification.quantity.value) + .dividedBy(order.specification.totalPrice.value) + .toString() + } else { + rate = new BigNumber(order.specification.totalPrice.value) + .dividedBy(order.specification.quantity.value) + .toString() + } + assert( + new BigNumber(rate).isGreaterThanOrEqualTo(previousRate), + 'Rates must be sorted from least to greatest: ' + + rate + + ' should be >= ' + + previousRate + ) + previousRate = rate + } + return true +} + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + 'normal': async (api, address) => { + const orderbookInfo = { + base: { + currency: 'USD', + counterparty: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B' + }, + counter: { + currency: 'BTC', + counterparty: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B' + } + } + + await Promise.all([ + api.request('book_offers', { + taker_gets: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.base), + taker_pays: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.counter), + ledger_index: 'validated', + limit: 20, + taker: address + }), + api.request('book_offers', { + taker_gets: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.counter), + taker_pays: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.base), + ledger_index: 'validated', + limit: 20, + taker: address + }) + ]).then(([directOfferResults, reverseOfferResults]) => { + const directOffers = (directOfferResults + ? directOfferResults.offers + : [] + ).reduce((acc, res) => acc.concat(res), []) + const reverseOffers = (reverseOfferResults + ? reverseOfferResults.offers + : [] + ).reduce((acc, res) => acc.concat(res), []) + const orderbook = RippleAPI.formatBidsAndAsks(orderbookInfo, [ + ...directOffers, + ...reverseOffers + ]) + assert.deepEqual(orderbook, responses.getOrderbook.normal) + }) + }, + + 'with XRP': async (api, address) => { + const orderbookInfo = { + base: { + currency: 'USD', + counterparty: 'rp8rJYTpodf8qbSCHVTNacf8nSW8mRakFw' + }, + counter: { + currency: 'XRP' + } + } + + await Promise.all([ + api.request('book_offers', { + taker_gets: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.base), + taker_pays: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.counter), + ledger_index: 'validated', + limit: 20, + taker: address + }), + api.request('book_offers', { + taker_gets: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.counter), + taker_pays: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.base), + ledger_index: 'validated', + limit: 20, + taker: address + }) + ]).then(([directOfferResults, reverseOfferResults]) => { + const directOffers = (directOfferResults + ? directOfferResults.offers + : [] + ).reduce((acc, res) => acc.concat(res), []) + const reverseOffers = (reverseOfferResults + ? reverseOfferResults.offers + : [] + ).reduce((acc, res) => acc.concat(res), []) + const orderbook = RippleAPI.formatBidsAndAsks(orderbookInfo, [ + ...directOffers, + ...reverseOffers + ]) + assert.deepEqual(orderbook, responses.getOrderbook.withXRP) + }) + }, + + 'sample XRP/JPY book has orders sorted correctly': async (api, address) => { + const orderbookInfo = { + base: { + // the first currency in pair + currency: 'XRP' + }, + counter: { + currency: 'JPY', + counterparty: 'rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS' + } + } + + const myAddress = 'rE9qNjzJXpiUbVomdv7R4xhrXVeH2oVmGR' + + await Promise.all([ + api.request('book_offers', { + taker_gets: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.base), + taker_pays: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.counter), + ledger_index: 'validated', + limit: 400, // must match `test/fixtures/rippled/requests/1-taker_gets-XRP-taker_pays-JPY.json` + taker: myAddress + }), + api.request('book_offers', { + taker_gets: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.counter), + taker_pays: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.base), + ledger_index: 'validated', + limit: 400, // must match `test/fixtures/rippled/requests/2-taker_gets-JPY-taker_pays-XRP.json` + taker: myAddress + }) + ]).then(([directOfferResults, reverseOfferResults]) => { + const directOffers = (directOfferResults + ? directOfferResults.offers + : [] + ).reduce((acc, res) => acc.concat(res), []) + const reverseOffers = (reverseOfferResults + ? reverseOfferResults.offers + : [] + ).reduce((acc, res) => acc.concat(res), []) + const orderbook = RippleAPI.formatBidsAndAsks(orderbookInfo, [ + ...directOffers, + ...reverseOffers + ]) + assert.deepStrictEqual([], orderbook.bids) + return checkSortingOfOrders(orderbook.asks) + }) + }, + + 'sample USD/XRP book has orders sorted correctly': async (api, address) => { + const orderbookInfo = { + counter: { currency: 'XRP' }, + base: { + currency: 'USD', + counterparty: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B' + } + } + + const myAddress = 'rE9qNjzJXpiUbVomdv7R4xhrXVeH2oVmGR' + + await Promise.all([ + api.request('book_offers', { + taker_gets: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.base), + taker_pays: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.counter), + ledger_index: 'validated', + limit: 400, // must match `test/fixtures/rippled/requests/1-taker_gets-XRP-taker_pays-JPY.json` + taker: myAddress + }), + api.request('book_offers', { + taker_gets: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.counter), + taker_pays: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.base), + ledger_index: 'validated', + limit: 400, // must match `test/fixtures/rippled/requests/2-taker_gets-JPY-taker_pays-XRP.json` + taker: myAddress + }) + ]).then(([directOfferResults, reverseOfferResults]) => { + const directOffers = (directOfferResults + ? directOfferResults.offers + : [] + ).reduce((acc, res) => acc.concat(res), []) + const reverseOffers = (reverseOfferResults + ? reverseOfferResults.offers + : [] + ).reduce((acc, res) => acc.concat(res), []) + const orderbook = RippleAPI.formatBidsAndAsks(orderbookInfo, [ + ...directOffers, + ...reverseOffers + ]) + return ( + checkSortingOfOrders(orderbook.bids) && + checkSortingOfOrders(orderbook.asks) + ) + }) + }, + + 'sorted so that best deals come first': async (api, address) => { + const orderbookInfo = { + base: { + currency: 'USD', + counterparty: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B' + }, + counter: { + currency: 'BTC', + counterparty: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B' + } + } + + await Promise.all([ + api.request('book_offers', { + taker_gets: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.base), + taker_pays: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.counter), + ledger_index: 'validated', + limit: 20, + taker: address + }), + api.request('book_offers', { + taker_gets: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.counter), + taker_pays: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.base), + ledger_index: 'validated', + limit: 20, + taker: address + }) + ]).then(([directOfferResults, reverseOfferResults]) => { + const directOffers = (directOfferResults + ? directOfferResults.offers + : [] + ).reduce((acc, res) => acc.concat(res), []) + const reverseOffers = (reverseOfferResults + ? reverseOfferResults.offers + : [] + ).reduce((acc, res) => acc.concat(res), []) + const orderbook = RippleAPI.formatBidsAndAsks(orderbookInfo, [ + ...directOffers, + ...reverseOffers + ]) + + const bidRates = orderbook.bids.map( + bid => bid.properties.makerExchangeRate + ) + const askRates = orderbook.asks.map( + ask => ask.properties.makerExchangeRate + ) + // makerExchangeRate = quality = takerPays.value/takerGets.value + // so the best deal for the taker is the lowest makerExchangeRate + // bids and asks should be sorted so that the best deals come first + assert.deepEqual(bidRates.map(x => Number(x)).sort(), bidRates) + assert.deepEqual(askRates.map(x => Number(x)).sort(), askRates) + }) + }, + + 'currency & counterparty are correct': async (api, address) => { + const orderbookInfo = { + base: { + currency: 'USD', + counterparty: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B' + }, + counter: { + currency: 'BTC', + counterparty: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B' + } + } + + await Promise.all([ + api.request('book_offers', { + taker_gets: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.base), + taker_pays: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.counter), + ledger_index: 'validated', + limit: 20, + taker: address + }), + api.request('book_offers', { + taker_gets: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.counter), + taker_pays: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.base), + ledger_index: 'validated', + limit: 20, + taker: address + }) + ]).then(([directOfferResults, reverseOfferResults]) => { + const directOffers = (directOfferResults + ? directOfferResults.offers + : [] + ).reduce((acc, res) => acc.concat(res), []) + const reverseOffers = (reverseOfferResults + ? reverseOfferResults.offers + : [] + ).reduce((acc, res) => acc.concat(res), []) + const orderbook = RippleAPI.formatBidsAndAsks(orderbookInfo, [ + ...directOffers, + ...reverseOffers + ]) + + const orders = [...orderbook.bids, ...orderbook.asks] + orders.forEach(order => { + const quantity = order.specification.quantity + const totalPrice = order.specification.totalPrice + const { base, counter } = requests.getOrderbook.normal + assert.strictEqual(quantity.currency, base.currency) + assert.strictEqual(quantity.counterparty, base.counterparty) + assert.strictEqual(totalPrice.currency, counter.currency) + assert.strictEqual(totalPrice.counterparty, counter.counterparty) + }) + }) + }, + + 'direction is correct for bids and asks': async (api, address) => { + const orderbookInfo = { + base: { + currency: 'USD', + counterparty: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B' + }, + counter: { + currency: 'BTC', + counterparty: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B' + } + } + + await Promise.all([ + api.request('book_offers', { + taker_gets: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.base), + taker_pays: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.counter), + ledger_index: 'validated', + limit: 20, + taker: address + }), + api.request('book_offers', { + taker_gets: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.counter), + taker_pays: RippleAPI.renameCounterpartyToIssuer(orderbookInfo.base), + ledger_index: 'validated', + limit: 20, + taker: address + }) + ]).then(([directOfferResults, reverseOfferResults]) => { + const directOffers = (directOfferResults + ? directOfferResults.offers + : [] + ).reduce((acc, res) => acc.concat(res), []) + const reverseOffers = (reverseOfferResults + ? reverseOfferResults.offers + : [] + ).reduce((acc, res) => acc.concat(res), []) + const orderbook = RippleAPI.formatBidsAndAsks(orderbookInfo, [ + ...directOffers, + ...reverseOffers + ]) + + assert(orderbook.bids.every(bid => bid.specification.direction === 'buy')) + assert( + orderbook.asks.every(ask => ask.specification.direction === 'sell') + ) + }) + } +} diff --git a/test/api/generateAddress/index.ts b/test/api/generateAddress/index.ts new file mode 100644 index 00000000..f763eac3 --- /dev/null +++ b/test/api/generateAddress/index.ts @@ -0,0 +1,30 @@ +import assert from 'assert-diff' +import responses from '../../fixtures/responses' +import { TestSuite } from '../../utils' +const { generateAddress: RESPONSE_FIXTURES } = responses + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + 'generateAddress': async (api, address) => { + function random(): number[] { + return new Array(16).fill(0) + } + assert.deepEqual( + api.generateAddress({ entropy: random() }), + RESPONSE_FIXTURES + ) + }, + + 'generateAddress invalid': async (api, address) => { + assert.throws(() => { + function random() { + return new Array(1).fill(0) + } + api.generateAddress({ entropy: random() }) + }, api.errors.UnexpectedError) + } +} diff --git a/test/api/generateXAddress/index.ts b/test/api/generateXAddress/index.ts new file mode 100644 index 00000000..3cd2f978 --- /dev/null +++ b/test/api/generateXAddress/index.ts @@ -0,0 +1,29 @@ +import assert from 'assert-diff' +import responses from '../../fixtures/responses' +import { TestSuite } from '../../utils' + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + 'generateXAddress': async (api, address) => { + function random() { + return new Array(16).fill(0) + } + assert.deepEqual( + api.generateXAddress({ entropy: random() }), + responses.generateXAddress + ) + }, + + 'generateXAddress invalid': async (api, address) => { + assert.throws(() => { + function random() { + return new Array(1).fill(0) + } + api.generateXAddress({ entropy: random() }) + }, api.errors.UnexpectedError) + } +} diff --git a/test/api/getAccountInfo/index.ts b/test/api/getAccountInfo/index.ts new file mode 100644 index 00000000..bd1b7edd --- /dev/null +++ b/test/api/getAccountInfo/index.ts @@ -0,0 +1,27 @@ +import responses from '../../fixtures/responses' +import { assertRejects, assertResultMatch, TestSuite } from '../../utils' + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + 'getAccountInfo': async (api, address) => { + const result = await api.getAccountInfo(address) + assertResultMatch(result, responses.getAccountInfo, 'getAccountInfo') + }, + + 'getAccountInfo - options undefined': async (api, address) => { + const result = await api.getAccountInfo(address, undefined) + assertResultMatch(result, responses.getAccountInfo, 'getAccountInfo') + }, + + 'getAccountInfo - invalid options': async (api, address) => { + await assertRejects( + // @ts-ignore - This is intentionally invalid + api.getAccountInfo(address, { invalid: 'options' }), + api.errors.ValidationError + ) + } +} diff --git a/test/api/getAccountObjects/index.ts b/test/api/getAccountObjects/index.ts new file mode 100644 index 00000000..14f1e692 --- /dev/null +++ b/test/api/getAccountObjects/index.ts @@ -0,0 +1,21 @@ +import responses from '../../fixtures/responses' +import { assertResultMatch, TestSuite } from '../../utils' +const { getAccountObjects: RESPONSE_FIXTURES } = responses + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + 'getAccountObjects': async (api, address) => { + const result = await api.getAccountObjects(address) + assertResultMatch(result, RESPONSE_FIXTURES, 'AccountObjectsResponse') + }, + + 'getAccountObjects - invalid options': async (api, address) => { + // @ts-ignore - This is intentionally invalid + const result = await api.getAccountObjects(address, { invalid: 'options' }) + assertResultMatch(result, RESPONSE_FIXTURES, 'AccountObjectsResponse') + } +} diff --git a/test/api/getBalanceSheet/index.ts b/test/api/getBalanceSheet/index.ts new file mode 100644 index 00000000..08406955 --- /dev/null +++ b/test/api/getBalanceSheet/index.ts @@ -0,0 +1,26 @@ +import { assertRejects, assertResultMatch, TestSuite } from '../../utils' + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + 'getBalanceSheet': async (api, address) => { + await api.getBalanceSheet(address) + }, + + 'getBalanceSheet - invalid options': async (api, address) => { + await assertRejects( + // @ts-ignore - This is intentionally invalid + api.getBalanceSheet(address, { invalid: 'options' }), + api.errors.ValidationError + ) + }, + + 'getBalanceSheet - empty': async (api, address) => { + const options = { ledgerVersion: 123456 } + const result = await api.getBalanceSheet(address, options) + assertResultMatch(result, {}, 'getBalanceSheet') + } +} diff --git a/test/api/getBalances/index.ts b/test/api/getBalances/index.ts new file mode 100644 index 00000000..80a7aed7 --- /dev/null +++ b/test/api/getBalances/index.ts @@ -0,0 +1,47 @@ +import responses from '../../fixtures/responses' +import { assertResultMatch, TestSuite } from '../../utils' + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + 'getBalances': async (api, address) => { + const result = await api.getBalances(address) + assertResultMatch(result, responses.getBalances, 'getBalances') + }, + + 'getBalances - limit': async (api, address) => { + const options = { limit: 3, ledgerVersion: 123456 } + const expectedResponse = responses.getBalances.slice(0, 3) + const result = await api.getBalances(address, options) + assertResultMatch(result, expectedResponse, 'getBalances') + }, + + 'getBalances - limit & currency': async (api, address) => { + const options = { currency: 'USD', limit: 3 } + const expectedResponse = responses.getBalances + .filter(item => item.currency === 'USD') + .slice(0, 3) + const result = await api.getBalances(address, options) + assertResultMatch(result, expectedResponse, 'getBalances') + }, + + 'getBalances - limit & currency & issuer': async (api, address) => { + const options = { + currency: 'USD', + counterparty: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B', + limit: 3 + } + const expectedResponse = responses.getBalances + .filter( + item => + item.currency === 'USD' && + item.counterparty === 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B' + ) + .slice(0, 3) + const result = await api.getBalances(address, options) + assertResultMatch(result, expectedResponse, 'getBalances') + } +} diff --git a/test/api/getFee/index.ts b/test/api/getFee/index.ts new file mode 100644 index 00000000..671b6117 --- /dev/null +++ b/test/api/getFee/index.ts @@ -0,0 +1,59 @@ +import assert from 'assert-diff' +import { TestSuite } from '../../utils' + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + 'getFee': async (api, address) => { + const fee = await api.getFee() + assert.strictEqual(fee, '0.000012') + }, + + 'getFee default': async (api, address) => { + api._feeCushion = undefined + const fee = await api.getFee() + assert.strictEqual(fee, '0.000012') + }, + + 'getFee - high load_factor': async (api, address) => { + api.connection._send( + JSON.stringify({ + command: 'config', + data: { highLoadFactor: true } + }) + ) + const fee = await api.getFee() + assert.strictEqual(fee, '2') + }, + + 'getFee - high load_factor with custom maxFeeXRP': async (api, address) => { + // Ensure that overriding with high maxFeeXRP of '51540' causes no errors. + // (fee will actually be 51539.607552) + api._maxFeeXRP = '51540' + api.connection._send( + JSON.stringify({ + command: 'config', + data: { highLoadFactor: true } + }) + ) + const fee = await api.getFee() + assert.strictEqual(fee, '51539.607552') + }, + + 'getFee custom cushion': async (api, address) => { + api._feeCushion = 1.4 + const fee = await api.getFee() + assert.strictEqual(fee, '0.000014') + }, + + // This is not recommended since it may result in attempting to pay + // less than the base fee. However, this test verifies the existing behavior. + 'getFee cushion less than 1.0': async (api, address) => { + api._feeCushion = 0.9 + const fee = await api.getFee() + assert.strictEqual(fee, '0.000009') + } +} diff --git a/test/api/getFeeBase/index.ts b/test/api/getFeeBase/index.ts new file mode 100644 index 00000000..32a4ed20 --- /dev/null +++ b/test/api/getFeeBase/index.ts @@ -0,0 +1,14 @@ +import assert from 'assert-diff' +import { TestSuite } from '../../utils' + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + 'default test': async (api, address) => { + const fee = await api.connection.getFeeBase() + assert.strictEqual(fee, 10) + } +} diff --git a/test/api/getFeeRef/index.ts b/test/api/getFeeRef/index.ts new file mode 100644 index 00000000..56e1c90c --- /dev/null +++ b/test/api/getFeeRef/index.ts @@ -0,0 +1,14 @@ +import assert from 'assert-diff' +import { TestSuite } from '../../utils' + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + 'default test': async (api, address) => { + const fee = await api.connection.getFeeRef() + assert.strictEqual(fee, 10) + } +} diff --git a/test/api/getLedgerVersion/index.ts b/test/api/getLedgerVersion/index.ts new file mode 100644 index 00000000..eb8dc0f8 --- /dev/null +++ b/test/api/getLedgerVersion/index.ts @@ -0,0 +1,14 @@ +import assert from 'assert-diff' +import { TestSuite } from '../../utils' + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + 'default test': async (api, address) => { + const ver = await api.getLedgerVersion() + assert.strictEqual(ver, 8819951) + } +} diff --git a/test/api/getOrderbook/index.ts b/test/api/getOrderbook/index.ts new file mode 100644 index 00000000..a285a84e --- /dev/null +++ b/test/api/getOrderbook/index.ts @@ -0,0 +1,150 @@ +import assert from 'assert-diff' +import responses from '../../fixtures/responses' +import requests from '../../fixtures/requests' +import { TestSuite, assertResultMatch, assertRejects } from '../../utils' +import BigNumber from 'bignumber.js' + +function checkSortingOfOrders(orders) { + let previousRate = '0' + for (var i = 0; i < orders.length; i++) { + const order = orders[i] + let rate + + // We calculate the quality of output/input here as a test. + // This won't hold in general because when output and input amounts get tiny, + // the quality can differ significantly. However, the offer stays in the + // order book where it was originally placed. It would be more consistent + // to check the quality from the offer book, but for the test data set, + // this calculation holds. + + if (order.specification.direction === 'buy') { + rate = new BigNumber(order.specification.quantity.value) + .dividedBy(order.specification.totalPrice.value) + .toString() + } else { + rate = new BigNumber(order.specification.totalPrice.value) + .dividedBy(order.specification.quantity.value) + .toString() + } + assert( + new BigNumber(rate).isGreaterThanOrEqualTo(previousRate), + 'Rates must be sorted from least to greatest: ' + + rate + + ' should be >= ' + + previousRate + ) + previousRate = rate + } + return true +} + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + 'normal': async (api, address) => { + const response = await api.getOrderbook( + address, + requests.getOrderbook.normal, + { limit: 20 } + ) + assertResultMatch(response, responses.getOrderbook.normal, 'getOrderbook') + }, + + 'invalid options': async (api, address) => { + assertRejects( + api.getOrderbook(address, requests.getOrderbook.normal, { + // @ts-ignore + invalid: 'options' + }), + api.errors.ValidationError + ) + }, + + 'with XRP': async (api, address) => { + const response = await api.getOrderbook( + address, + requests.getOrderbook.withXRP + ) + assertResultMatch(response, responses.getOrderbook.withXRP, 'getOrderbook') + }, + + 'sample XRP/JPY book has orders sorted correctly': async (api, address) => { + const orderbookInfo = { + base: { + // the first currency in pair + currency: 'XRP' + }, + counter: { + currency: 'JPY', + counterparty: 'rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS' + } + } + const myAddress = 'rE9qNjzJXpiUbVomdv7R4xhrXVeH2oVmGR' + const response = await api.getOrderbook(myAddress, orderbookInfo) + assert.deepStrictEqual([], response.bids) + checkSortingOfOrders(response.asks) + }, + + 'sample USD/XRP book has orders sorted correctly': async (api, address) => { + const orderbookInfo = { + counter: { currency: 'XRP' }, + base: { + currency: 'USD', + counterparty: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B' + } + } + const myAddress = 'rE9qNjzJXpiUbVomdv7R4xhrXVeH2oVmGR' + const response = await api.getOrderbook(myAddress, orderbookInfo) + checkSortingOfOrders(response.bids) + checkSortingOfOrders(response.asks) + }, + + // WARNING: This test fails to catch the sorting bug, issue #766 + 'sorted so that best deals come first [bad test]': async (api, address) => { + const response = await api.getOrderbook( + address, + requests.getOrderbook.normal + ) + const bidRates = response.bids.map(bid => bid.properties.makerExchangeRate) + const askRates = response.asks.map(ask => ask.properties.makerExchangeRate) + // makerExchangeRate = quality = takerPays.value/takerGets.value + // so the best deal for the taker is the lowest makerExchangeRate + // bids and asks should be sorted so that the best deals come first + assert.deepEqual( + bidRates.sort(x => Number(x)), + bidRates + ) + assert.deepEqual( + askRates.sort(x => Number(x)), + askRates + ) + }, + + 'currency & counterparty are correct': async (api, address) => { + const response = await api.getOrderbook( + address, + requests.getOrderbook.normal + ) + ;[...response.bids, ...response.asks].forEach(order => { + const quantity = order.specification.quantity + const totalPrice = order.specification.totalPrice + const { base, counter } = requests.getOrderbook.normal + assert.strictEqual(quantity.currency, base.currency) + assert.strictEqual(quantity.counterparty, base.counterparty) + assert.strictEqual(totalPrice.currency, counter.currency) + assert.strictEqual(totalPrice.counterparty, counter.counterparty) + }) + }, + + 'direction is correct for bids and asks': async (api, address) => { + const response = await api.getOrderbook( + address, + requests.getOrderbook.normal + ) + assert(response.bids.every(bid => bid.specification.direction === 'buy')) + assert(response.asks.every(ask => ask.specification.direction === 'sell')) + } +} diff --git a/test/api/getOrders/index.ts b/test/api/getOrders/index.ts new file mode 100644 index 00000000..2e8066b9 --- /dev/null +++ b/test/api/getOrders/index.ts @@ -0,0 +1,34 @@ +import responses from '../../fixtures/responses' +import { assertRejects, assertResultMatch, TestSuite } from '../../utils' + +export const config = { + // TODO: The mock server right now returns a hard-coded string, no matter + // what "Account" value you pass. We'll need it to support more accurate + // responses before we can turn these tests on. + skipXAddress: true +} + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + 'getOrders': async (api, address) => { + const result = await api.getOrders(address) + assertResultMatch(result, responses.getOrders, 'getOrders') + }, + + 'getOrders - limit': async (api, address) => { + const result = await api.getOrders(address, { limit: 20 }) + assertResultMatch(result, responses.getOrders, 'getOrders') + }, + + 'getOrders - invalid options': async (api, address) => { + await assertRejects( + // @ts-ignore - This is intentionally invalid + api.getOrders(address, { invalid: 'options' }), + api.errors.ValidationError + ) + } +} diff --git a/test/api/getPaths/index.ts b/test/api/getPaths/index.ts index 3421561f..b48617bf 100644 --- a/test/api/getPaths/index.ts +++ b/test/api/getPaths/index.ts @@ -1,7 +1,7 @@ import assert from 'assert-diff' import { assertResultMatch, assertRejects, TestSuite } from '../../utils' -import responses from '../../fixtures/responses' import requests from '../../fixtures/requests' +import responses from '../../fixtures/responses' import addresses from '../../fixtures/addresses.json' const { getPaths: REQUEST_FIXTURES } = requests const { getPaths: RESPONSE_FIXTURES } = responses diff --git a/test/api/getPaymentChannel/index.ts b/test/api/getPaymentChannel/index.ts new file mode 100644 index 00000000..e0f4beb5 --- /dev/null +++ b/test/api/getPaymentChannel/index.ts @@ -0,0 +1,44 @@ +import responses from '../../fixtures/responses' +import { assertRejects, assertResultMatch, TestSuite } from '../../utils' +const { getPaymentChannel: RESPONSE_FIXTURES } = responses + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + 'getPaymentChannel': async (api, address) => { + const channelId = + 'E30E709CF009A1F26E0E5C48F7AA1BFB79393764F15FB108BDC6E06D3CBD8415' + const result = await api.getPaymentChannel(channelId) + assertResultMatch(result, RESPONSE_FIXTURES.normal, 'getPaymentChannel') + }, + + 'getPaymentChannel - full': async (api, address) => { + const channelId = + 'D77CD4713AA08195E6B6D0E5BC023DA11B052EBFF0B5B22EDA8AE85345BCF661' + const result = await api.getPaymentChannel(channelId) + assertResultMatch(result, RESPONSE_FIXTURES.full, 'getPaymentChannel') + }, + + 'getPaymentChannel - not found': async (api, address) => { + const channelId = + 'DFA557EA3497585BFE83F0F97CC8E4530BBB99967736BB95225C7F0C13ACE708' + await assertRejects( + api.getPaymentChannel(channelId), + api.errors.RippledError, + 'entryNotFound' + ) + }, + + 'getPaymentChannel - wrong type': async (api, address) => { + const channelId = + '8EF9CCB9D85458C8D020B3452848BBB42EAFDDDB69A93DD9D1223741A4CA562B' + await assertRejects( + api.getPaymentChannel(channelId), + api.errors.NotFoundError, + 'Payment channel ledger entry not found' + ) + } +} diff --git a/test/api/getServerInfo/index.ts b/test/api/getServerInfo/index.ts new file mode 100644 index 00000000..aea396f5 --- /dev/null +++ b/test/api/getServerInfo/index.ts @@ -0,0 +1,48 @@ +import assert from 'assert-diff' +import responses from '../../fixtures/responses' +import { assertResultMatch, TestSuite, assertRejects } from '../../utils' + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + 'default': async (api, address) => { + const serverInfo = await api.getServerInfo() + assertResultMatch(serverInfo, responses.getServerInfo, 'getServerInfo') + }, + + 'error': async (api, address) => { + api.connection._send( + JSON.stringify({ + command: 'config', + data: { returnErrorOnServerInfo: true } + }) + ) + try { + await api.getServerInfo() + throw new Error('Should throw NetworkError') + } catch (err) { + assert(err instanceof api.errors.RippledError) + assert.equal(err.message, 'You are placing too much load on the server.') + assert.equal(err.data.error, 'slowDown') + } + }, + + 'no validated ledger': async (api, address) => { + api.connection._send( + JSON.stringify({ + command: 'config', + data: { serverInfoWithoutValidated: true } + }) + ) + const serverInfo = await api.getServerInfo() + assert.strictEqual(serverInfo.networkLedger, 'waiting') + }, + + 'getServerInfo - offline': async (api, address) => { + await api.disconnect() + return assertRejects(api.getServerInfo(), api.errors.NotConnectedError) + } +} diff --git a/test/api/getSettings/index.ts b/test/api/getSettings/index.ts new file mode 100644 index 00000000..e91d04b8 --- /dev/null +++ b/test/api/getSettings/index.ts @@ -0,0 +1,28 @@ +import responses from '../../fixtures/responses' +import { assertRejects, assertResultMatch, TestSuite } from '../../utils' +const { getSettings: RESPONSE_FIXTURES } = responses + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + 'getSettings': async (api, address) => { + const result = await api.getSettings(address) + assertResultMatch(result, RESPONSE_FIXTURES, 'getSettings') + }, + + 'getSettings - options undefined': async (api, address) => { + const result = await api.getSettings(address, undefined) + assertResultMatch(result, RESPONSE_FIXTURES, 'getSettings') + }, + + 'getSettings - invalid options': async (api, address) => { + await assertRejects( + // @ts-ignore - This is intentionally invalid + api.getSettings(address, { invalid: 'options' }), + api.errors.ValidationError + ) + } +} diff --git a/test/api/getTransaction/index.ts b/test/api/getTransaction/index.ts new file mode 100644 index 00000000..0bf0be8b --- /dev/null +++ b/test/api/getTransaction/index.ts @@ -0,0 +1,389 @@ +import assert from 'assert-diff' +import { + MissingLedgerHistoryError, + NotFoundError, + UnexpectedError +} from 'ripple-api/common/errors' +import { PendingLedgerVersionError } from '../../../src/common/errors' +import hashes from '../../fixtures/hashes.json' +import responses from '../../fixtures/responses' +import ledgerClosed from '../../fixtures/rippled/ledger-close-newer.json' +import { assertRejects, assertResultMatch, TestSuite } from '../../utils' +const { getTransaction: RESPONSE_FIXTURES } = responses + +function closeLedger(connection) { + connection._ws.emit('message', JSON.stringify(ledgerClosed)) +} + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + 'payment': async (api, address) => { + const response = await api.getTransaction(hashes.VALID_TRANSACTION_HASH) + assertResultMatch(response, RESPONSE_FIXTURES.payment, 'getTransaction') + }, + + 'payment - include raw transaction': async (api, address) => { + const options = { + includeRawTransaction: true + } + const response = await api.getTransaction( + hashes.VALID_TRANSACTION_HASH, + options + ) + assertResultMatch( + response, + RESPONSE_FIXTURES.paymentIncludeRawTransaction, + 'getTransaction' + ) + }, + + 'settings': async (api, address) => { + const hash = + '4FB3ADF22F3C605E23FAEFAA185F3BD763C4692CAC490D9819D117CD33BFAA1B' + const response = await api.getTransaction(hash) + assertResultMatch(response, RESPONSE_FIXTURES.settings, 'getTransaction') + }, + + 'settings - include raw transaction': async (api, address) => { + const hash = + '4FB3ADF22F3C605E23FAEFAA185F3BD763C4692CAC490D9819D117CD33BFAA1B' + const options = { + includeRawTransaction: true + } + const expected = Object.assign({}, RESPONSE_FIXTURES.settings) // Avoid mutating test fixture + expected.rawTransaction = + '{"Account":"rLVKsA4F9iJBbA6rX2x4wCmkj6drgtqpQe","Fee":"10","Flags":2147483648,"Sequence":1,"SetFlag":2,"SigningPubKey":"03EA3ADCA632F125EC2CC4F7F6A82DE0DCE2B65290CAC1F22242C5163F0DA9652D","TransactionType":"AccountSet","TxnSignature":"3045022100DE8B666B1A31EA65011B0F32130AB91A5747E32FA49B3054CEE8E8362DBAB98A022040CF0CF254677A8E5CD04C59CA2ED7F6F15F7E184641BAE169C561650967B226","date":460832270,"hash":"4FB3ADF22F3C605E23FAEFAA185F3BD763C4692CAC490D9819D117CD33BFAA1B","inLedger":8206418,"ledger_index":8206418,"meta":{"AffectedNodes":[{"ModifiedNode":{"FinalFields":{"Account":"rLVKsA4F9iJBbA6rX2x4wCmkj6drgtqpQe","Balance":"29999990","Flags":786432,"OwnerCount":0,"Sequence":2},"LedgerEntryType":"AccountRoot","LedgerIndex":"3F5072C4875F32ED770DAF3610A716600ED7C7BB0348FADC7A98E011BB2CD36F","PreviousFields":{"Balance":"30000000","Flags":4194304,"Sequence":1},"PreviousTxnID":"3FB0350A3742BBCC0D8AA3C5247D1AEC01177D0A24D9C34762BAA2FEA8AD88B3","PreviousTxnLgrSeq":8206397}}],"TransactionIndex":5,"TransactionResult":"tesSUCCESS"},"validated":true}' + const response = await api.getTransaction(hash, options) + assertResultMatch(response, expected, 'getTransaction') + }, + + 'order': async (api, address) => { + const hash = + '10A6FB4A66EE80BED46AAE4815D7DC43B97E944984CCD5B93BCF3F8538CABC51' + closeLedger(api.connection) + const response = await api.getTransaction(hash) + assertResultMatch(response, RESPONSE_FIXTURES.order, 'getTransaction') + }, + + 'sell order': async (api, address) => { + const hash = + '458101D51051230B1D56E9ACAFAA34451BF65FA000F95DF6F0FF5B3A62D83FC2' + closeLedger(api.connection) + const response = await api.getTransaction(hash) + assertResultMatch(response, RESPONSE_FIXTURES.orderSell, 'getTransaction') + }, + + 'order cancellation': async (api, address) => { + const hash = + '809335DD3B0B333865096217AA2F55A4DF168E0198080B3A090D12D88880FF0E' + closeLedger(api.connection) + const response = await api.getTransaction(hash) + assertResultMatch( + response, + RESPONSE_FIXTURES.orderCancellation, + 'getTransaction' + ) + }, + + 'order with expiration cancellation': async (api, address) => { + const hash = + '097B9491CC76B64831F1FEA82EAA93BCD728106D90B65A072C933888E946C40B' + const response = await api.getTransaction(hash) + assertResultMatch( + response, + RESPONSE_FIXTURES.orderWithExpirationCancellation, + 'getTransaction' + ) + }, + + 'trustline set': async (api, address) => { + const hash = + '635A0769BD94710A1F6A76CDE65A3BC661B20B798807D1BBBDADCEA26420538D' + const response = await api.getTransaction(hash) + assertResultMatch(response, RESPONSE_FIXTURES.trustline, 'getTransaction') + }, + + 'trustline frozen off': async (api, address) => { + const hash = + 'FE72FAD0FA7CA904FB6C633A1666EDF0B9C73B2F5A4555D37EEF2739A78A531B' + const response = await api.getTransaction(hash) + assertResultMatch( + response, + RESPONSE_FIXTURES.trustlineFrozenOff, + 'getTransaction' + ) + }, + + 'trustline no quality': async (api, address) => { + const hash = + 'BAF1C678323C37CCB7735550C379287667D8288C30F83148AD3C1CB019FC9002' + const response = await api.getTransaction(hash) + assertResultMatch( + response, + RESPONSE_FIXTURES.trustlineNoQuality, + 'getTransaction' + ) + }, + + 'trustline add memo': async (api, address) => { + const hash = + '9D6AC5FD6545B2584885B85E36759EB6440CDD41B6C55859F84AFDEE2B428220' + const response = await api.getTransaction(hash) + assertResultMatch( + response, + RESPONSE_FIXTURES.trustlineAddMemo, + 'getTransaction' + ) + }, + + 'not validated': async (api, address) => { + const hash = + '4FB3ADF22F3C605E23FAEFAA185F3BD763C4692CAC490D9819D117CD33BFAA10' + await assertRejects( + api.getTransaction(hash), + NotFoundError, + 'Transaction not found' + ) + }, + + 'tracking on': async (api, address) => { + const hash = + '8925FC8844A1E930E2CC76AD0A15E7665AFCC5425376D548BB1413F484C31B8C' + const response = await api.getTransaction(hash) + assertResultMatch(response, RESPONSE_FIXTURES.trackingOn, 'getTransaction') + }, + + 'tracking off': async (api, address) => { + const hash = + 'C8C5E20DFB1BF533D0D81A2ED23F0A3CBD1EF2EE8A902A1D760500473CC9C582' + const response = await api.getTransaction(hash) + assertResultMatch(response, RESPONSE_FIXTURES.trackingOff, 'getTransaction') + }, + + 'set regular key': async (api, address) => { + const hash = + '278E6687C1C60C6873996210A6523564B63F2844FB1019576C157353B1813E60' + const response = await api.getTransaction(hash) + assertResultMatch( + response, + RESPONSE_FIXTURES.setRegularKey, + 'getTransaction' + ) + }, + + 'not found in range': async (api, address) => { + const hash = + '809335DD3B0B333865096217AA2F55A4DF168E0198080B3A090D12D88880FF0E' + const options = { + minLedgerVersion: 32570, + maxLedgerVersion: 32571 + } + await assertRejects(api.getTransaction(hash, options), NotFoundError) + }, + + 'not found by hash': async (api, address) => { + const hash = hashes.NOTFOUND_TRANSACTION_HASH + + await assertRejects(api.getTransaction(hash), NotFoundError) + }, + + 'missing ledger history': async (api, address) => { + const hash = hashes.NOTFOUND_TRANSACTION_HASH + // make gaps in history + closeLedger(api.connection) + + await assertRejects(api.getTransaction(hash), MissingLedgerHistoryError) + }, + + 'missing ledger history with ledger range': async (api, address) => { + const hash = hashes.NOTFOUND_TRANSACTION_HASH + const options = { + minLedgerVersion: 32569, + maxLedgerVersion: 32571 + } + await assertRejects( + api.getTransaction(hash, options), + MissingLedgerHistoryError + ) + }, + + 'not found - future maxLedgerVersion': async (api, address) => { + const hash = hashes.NOTFOUND_TRANSACTION_HASH + const options = { + maxLedgerVersion: 99999999999 + } + await assertRejects( + api.getTransaction(hash, options), + PendingLedgerVersionError, + "maxLedgerVersion is greater than server's most recent validated ledger" + ) + }, + + 'transaction not validated': async (api, address) => { + const hash = + '4FB3ADF22F3C605E23FAEFAA185F3BD763C4692CAC490D9819D117CD33BFAA11' + await assertRejects( + api.getTransaction(hash), + NotFoundError, + /Transaction has not been validated yet/ + ) + }, + + 'transaction ledger not found': async (api, address) => { + const hash = + '4FB3ADF22F3C605E23FAEFAA185F3BD763C4692CAC490D9819D117CD33BFAA12' + await assertRejects( + api.getTransaction(hash), + NotFoundError, + /ledger not found/ + ) + }, + + 'ledger missing close time': async (api, address) => { + const hash = + '0F7ED9F40742D8A513AE86029462B7A6768325583DF8EE21B7EC663019DD6A04' + closeLedger(api.connection) + await assertRejects(api.getTransaction(hash), UnexpectedError) + }, + + // Checks + 'CheckCreate': async (api, address) => { + const hash = + '605A2E2C8E48AECAF5C56085D1AEAA0348DC838CE122C9188F94EB19DA05C2FE' + const response = await api.getTransaction(hash) + assertResultMatch(response, RESPONSE_FIXTURES.checkCreate, 'getTransaction') + }, + + 'CheckCancel': async (api, address) => { + const hash = + 'B4105D1B2D83819647E4692B7C5843D674283F669524BD50C9614182E3A12CD4' + const response = await api.getTransaction(hash) + assertResultMatch(response, RESPONSE_FIXTURES.checkCancel, 'getTransaction') + }, + + 'CheckCash': async (api, address) => { + const hash = + '8321208465F70BA52C28BCC4F646BAF3B012BA13B57576C0336F42D77E3E0749' + const response = await api.getTransaction(hash) + assertResultMatch(response, RESPONSE_FIXTURES.checkCash, 'getTransaction') + }, + + // Escrows + 'EscrowCreation': async (api, address) => { + const hash = + '144F272380BDB4F1BD92329A2178BABB70C20F59042C495E10BF72EBFB408EE1' + const response = await api.getTransaction(hash) + assertResultMatch( + response, + RESPONSE_FIXTURES.escrowCreation, + 'getTransaction' + ) + }, + + 'EscrowCancellation': async (api, address) => { + const hash = + 'F346E542FFB7A8398C30A87B952668DAB48B7D421094F8B71776DA19775A3B22' + const response = await api.getTransaction(hash) + assertResultMatch( + response, + RESPONSE_FIXTURES.escrowCancellation, + 'getTransaction' + ) + }, + + 'EscrowExecution': async (api, address) => { + const options = { + minLedgerVersion: 10, + maxLedgerVersion: 15 + } + const hash = + 'CC5277137B3F25EE8B86259C83CB0EAADE818505E4E9BCBF19B1AC6FD136993B' + const response = await api.getTransaction(hash, options) + assertResultMatch( + response, + RESPONSE_FIXTURES.escrowExecution, + 'getTransaction' + ) + }, + + 'EscrowExecution simple': async (api, address) => { + const hash = + 'CC5277137B3F25EE8B86259C83CB0EAADE818505E4E9BCBF19B1AC6FD1369931' + const response = await api.getTransaction(hash) + assertResultMatch( + response, + RESPONSE_FIXTURES.escrowExecutionSimple, + 'getTransaction' + ) + }, + + 'PaymentChannelCreate': async (api, address) => { + const hash = + '0E9CA3AB1053FC0C1CBAA75F636FE1EC92F118C7056BBEF5D63E4C116458A16D' + const response = await api.getTransaction(hash) + assertResultMatch( + response, + RESPONSE_FIXTURES.paymentChannelCreate, + 'getTransaction' + ) + }, + + 'PaymentChannelFund': async (api, address) => { + const hash = + 'CD053D8867007A6A4ACB7A432605FE476D088DCB515AFFC886CF2B4EB6D2AE8B' + const response = await api.getTransaction(hash) + assertResultMatch( + response, + RESPONSE_FIXTURES.paymentChannelFund, + 'getTransaction' + ) + }, + + 'PaymentChannelClaim': async (api, address) => { + const hash = + '81B9ECAE7195EB6E8034AEDF44D8415A7A803E14513FDBB34FA984AB37D59563' + const response = await api.getTransaction(hash) + assertResultMatch( + response, + RESPONSE_FIXTURES.paymentChannelClaim, + 'getTransaction' + ) + }, + + 'no Meta': async (api, address) => { + const hash = + 'AFB3ADF22F3C605E23FAEFAA185F3BD763C4692CAC490D9819D117CD33BFAA1B' + const response = await api.getTransaction(hash) + assert.deepEqual(response, RESPONSE_FIXTURES.noMeta) + }, + + 'Unrecognized transaction type': async (api, address) => { + const hash = + 'AFB3ADF22F3C605E23FAEFAA185F3BD763C4692CAC490D9819D117CD33BFAA11' + closeLedger(api.connection) + const response = await api.getTransaction(hash) + assert.strictEqual( + // @ts-ignore + response.specification.UNAVAILABLE, + 'Unrecognized transaction type.' + ) + }, + + 'amendment': async (api, address) => { + const hash = + 'A971B83ABED51D83749B73F3C1AAA627CD965AFF74BE8CD98299512D6FB0658F' + const response = await api.getTransaction(hash) + assertResultMatch(response, RESPONSE_FIXTURES.amendment) + }, + + 'feeUpdate': async (api, address) => { + const hash = + 'C6A40F56127436DCD830B1B35FF939FD05B5747D30D6542572B7A835239817AF' + const response = await api.getTransaction(hash) + assertResultMatch(response, RESPONSE_FIXTURES.feeUpdate) + } +} diff --git a/test/api/getTransactions/index.ts b/test/api/getTransactions/index.ts new file mode 100644 index 00000000..5b8b39ac --- /dev/null +++ b/test/api/getTransactions/index.ts @@ -0,0 +1,152 @@ +import { RippleAPI } from 'ripple-api' +import assert from 'assert-diff' +import { assertResultMatch, TestSuite, assertRejects } from '../../utils' +import responses from '../../fixtures/responses' +import hashes from '../../fixtures/hashes.json' +import addresses from '../../fixtures/addresses.json' +const utils = RippleAPI._PRIVATE.ledgerUtils +const { getTransactions: RESPONSE_FIXTURES } = responses + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + 'default': async (api, address) => { + const options = { types: ['payment', 'order'], initiated: true, limit: 2 } + const response = await api.getTransactions(address, options) + assertResultMatch(response, RESPONSE_FIXTURES.normal, 'getTransactions') + }, + + 'include raw transactions': async (api, address) => { + const options = { + types: ['payment', 'order'], + initiated: true, + limit: 2, + includeRawTransactions: true + } + const response = await api.getTransactions(address, options) + assertResultMatch( + response, + RESPONSE_FIXTURES.includeRawTransactions, + 'getTransactions' + ) + }, + + 'earliest first': async (api, address) => { + const options = { + types: ['payment', 'order'], + initiated: true, + limit: 2, + earliestFirst: true + } + const expected = Array.from(RESPONSE_FIXTURES.normal as any[]).sort( + utils.compareTransactions + ) + const response = await api.getTransactions(address, options) + assertResultMatch(response, expected, 'getTransactions') + }, + + 'earliest first with start option': async (api, address) => { + const options = { + types: ['payment', 'order'], + initiated: true, + limit: 2, + start: hashes.VALID_TRANSACTION_HASH, + earliestFirst: true + } + const response = await api.getTransactions(address, options) + assert.strictEqual(response.length, 0) + }, + + 'gap': async (api, address) => { + const options = { + types: ['payment', 'order'], + initiated: true, + limit: 2, + maxLedgerVersion: 348858000 + } + return assertRejects( + api.getTransactions(address, options), + api.errors.MissingLedgerHistoryError + ) + }, + + 'tx not found': async (api, address) => { + const options = { + types: ['payment', 'order'], + initiated: true, + limit: 2, + start: hashes.NOTFOUND_TRANSACTION_HASH, + counterparty: address + } + return assertRejects( + api.getTransactions(address, options), + api.errors.NotFoundError + ) + }, + + 'filters': async (api, address) => { + const options = { + types: ['payment', 'order'], + initiated: true, + limit: 10, + excludeFailures: true, + counterparty: addresses.ISSUER + } + const response = await api.getTransactions(address, options) + assert.strictEqual(response.length, 10) + response.forEach(t => assert(t.type === 'payment' || t.type === 'order')) + response.forEach(t => assert(t.outcome.result === 'tesSUCCESS')) + }, + + 'filters for incoming': async (api, address) => { + const options = { + types: ['payment', 'order'], + initiated: false, + limit: 10, + excludeFailures: true, + counterparty: addresses.ISSUER + } + const response = await api.getTransactions(address, options) + assert.strictEqual(response.length, 10) + response.forEach(t => assert(t.type === 'payment' || t.type === 'order')) + response.forEach(t => assert(t.outcome.result === 'tesSUCCESS')) + }, + + // this is the case where core.RippleError just falls + // through the api to the user + 'error': async (api, address) => { + const options = { types: ['payment', 'order'], initiated: true, limit: 13 } + return assertRejects( + api.getTransactions(address, options), + api.errors.RippleError + ) + }, + + // TODO: this doesn't test much, just that it doesn't crash + 'getTransactions with start option': async (api, address) => { + const options = { + start: hashes.VALID_TRANSACTION_HASH, + earliestFirst: false, + limit: 2 + } + const response = await api.getTransactions(address, options) + assertResultMatch(response, RESPONSE_FIXTURES.normal, 'getTransactions') + }, + + 'start transaction with zero ledger version': async (api, address) => { + const options = { + start: '4FB3ADF22F3C605E23FAEFAA185F3BD763C4692CAC490D9819D117CD33BFAA13', + limit: 1 + } + const response = await api.getTransactions(address, options) + assertResultMatch(response, [], 'getTransactions') + }, + + 'no options': async (api, address) => { + const response = await api.getTransactions(addresses.OTHER_ACCOUNT) + assertResultMatch(response, RESPONSE_FIXTURES.one, 'getTransactions') + } +} diff --git a/test/api/getTrustlines/index.ts b/test/api/getTrustlines/index.ts new file mode 100644 index 00000000..89dde403 --- /dev/null +++ b/test/api/getTrustlines/index.ts @@ -0,0 +1,31 @@ +import addresses from '../../fixtures/addresses.json' +import responses from '../../fixtures/responses' +import { assertResultMatch, TestSuite } from '../../utils' +const { getTrustlines: RESPONSE_FIXTURES } = responses + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + 'getTrustlines - filtered': async (api, address) => { + const options = { currency: 'USD' } + const result = await api.getTrustlines(address, options) + assertResultMatch(result, RESPONSE_FIXTURES.filtered, 'getTrustlines') + }, + + 'getTrustlines - more than 400 items': async (api, address) => { + const options = { limit: 401 } + const result = await api.getTrustlines(addresses.THIRD_ACCOUNT, options) + assertResultMatch( + result, + RESPONSE_FIXTURES.moreThan400Items, + 'getTrustlines' + ) + }, + + 'getTrustlines - no options': async (api, address) => { + await api.getTrustlines(address) + } +} diff --git a/test/api/hasNextPage/index.ts b/test/api/hasNextPage/index.ts new file mode 100644 index 00000000..7c670ff4 --- /dev/null +++ b/test/api/hasNextPage/index.ts @@ -0,0 +1,24 @@ +import assert from 'assert-diff' +import { TestSuite } from '../../utils' + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + 'returns true when there is another page': async (api, address) => { + const response = await api.request('ledger_data') + assert(api.hasNextPage(response)) + }, + + 'returns false when there are no more pages': async (api, address) => { + const response = await api.request('ledger_data') + const responseNextPage = await api.requestNextPage( + 'ledger_data', + {}, + response + ) + assert(!api.hasNextPage(responseNextPage)) + } +} diff --git a/test/api/isConnected/index.ts b/test/api/isConnected/index.ts new file mode 100644 index 00000000..1dbbd123 --- /dev/null +++ b/test/api/isConnected/index.ts @@ -0,0 +1,15 @@ +import assert from 'assert-diff' +import { TestSuite } from '../../utils' + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + 'disconnect & isConnected': async (api, address) => { + assert.strictEqual(api.isConnected(), true) + await api.disconnect() + assert.strictEqual(api.isConnected(), false) + } +} diff --git a/test/api/isValidAddress/index.ts b/test/api/isValidAddress/index.ts new file mode 100644 index 00000000..c5512370 --- /dev/null +++ b/test/api/isValidAddress/index.ts @@ -0,0 +1,17 @@ +import assert from 'assert-diff' +import { TestSuite } from '../../utils' +import addresses from '../../fixtures/addresses.json' + +export default { + 'returns true for valid address': async (api, address) => { + assert(api.isValidAddress('rLczgQHxPhWtjkaQqn3Q6UM8AbRbbRvs5K')) + assert(api.isValidAddress(addresses.ACCOUNT_X)) + assert(api.isValidAddress(addresses.ACCOUNT_T)) + }, + + 'returns false for invalid address': async (api, address) => { + assert(!api.isValidAddress('foobar')) + assert(!api.isValidAddress(addresses.ACCOUNT_X.slice(0, -1))) + assert(!api.isValidAddress(addresses.ACCOUNT_T.slice(1))) + } +} diff --git a/test/api/isValidSecret/index.ts b/test/api/isValidSecret/index.ts new file mode 100644 index 00000000..42b054dc --- /dev/null +++ b/test/api/isValidSecret/index.ts @@ -0,0 +1,12 @@ +import assert from 'assert-diff' +import { TestSuite } from '../../utils' + +export default { + 'returns true for valid secret': async (api, address) => { + assert(api.isValidSecret('snsakdSrZSLkYpCXxfRkS4Sh96PMK')) + }, + + 'returns false for invalid secret': async (api, address) => { + assert(!api.isValidSecret('foobar')) + } +} diff --git a/test/api/prepareCheckCancel/index.ts b/test/api/prepareCheckCancel/index.ts new file mode 100644 index 00000000..b53dd812 --- /dev/null +++ b/test/api/prepareCheckCancel/index.ts @@ -0,0 +1,18 @@ +import requests from '../../fixtures/requests' +import responses from '../../fixtures/responses' +import { assertResultMatch, TestSuite } from '../../utils' + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + prepareCheckCancel: async (api, address) => { + const result = await api.prepareCheckCancel( + address, + requests.prepareCheckCancel.normal + ) + assertResultMatch(result, responses.prepareCheckCancel.normal, 'prepare') + } +} diff --git a/test/api/prepareCheckCash/index.ts b/test/api/prepareCheckCash/index.ts new file mode 100644 index 00000000..ad311d00 --- /dev/null +++ b/test/api/prepareCheckCash/index.ts @@ -0,0 +1,26 @@ +import requests from '../../fixtures/requests' +import responses from '../../fixtures/responses' +import { assertResultMatch, TestSuite } from '../../utils' + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + 'prepareCheckCash amount': async (api, address) => { + const result = await api.prepareCheckCash( + address, + requests.prepareCheckCash.amount + ) + assertResultMatch(result, responses.prepareCheckCash.amount, 'prepare') + }, + + 'prepareCheckCash deliverMin': async (api, address) => { + const result = await api.prepareCheckCash( + address, + requests.prepareCheckCash.deliverMin + ) + assertResultMatch(result, responses.prepareCheckCash.deliverMin, 'prepare') + } +} diff --git a/test/api/prepareCheckCreate/index.ts b/test/api/prepareCheckCreate/index.ts new file mode 100644 index 00000000..a2f4bb19 --- /dev/null +++ b/test/api/prepareCheckCreate/index.ts @@ -0,0 +1,32 @@ +import requests from '../../fixtures/requests' +import responses from '../../fixtures/responses' +import { assertResultMatch, TestSuite } from '../../utils' +const instructionsWithMaxLedgerVersionOffset = { maxLedgerVersionOffset: 100 } + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + 'prepareCheckCreate': async (api, address) => { + const localInstructions = { + ...instructionsWithMaxLedgerVersionOffset, + maxFee: '0.000012' + } + const result = await api.prepareCheckCreate( + address, + requests.prepareCheckCreate.normal, + localInstructions + ) + assertResultMatch(result, responses.prepareCheckCreate.normal, 'prepare') + }, + + 'prepareCheckCreate full': async (api, address) => { + const result = await api.prepareCheckCreate( + address, + requests.prepareCheckCreate.full + ) + assertResultMatch(result, responses.prepareCheckCreate.full, 'prepare') + } +} diff --git a/test/api/prepareEscrowCancellation/index.ts b/test/api/prepareEscrowCancellation/index.ts new file mode 100644 index 00000000..51b7f44b --- /dev/null +++ b/test/api/prepareEscrowCancellation/index.ts @@ -0,0 +1,36 @@ +import requests from '../../fixtures/requests' +import responses from '../../fixtures/responses' +import { assertResultMatch, TestSuite } from '../../utils' +const instructionsWithMaxLedgerVersionOffset = { maxLedgerVersionOffset: 100 } + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + 'prepareEscrowCancellation': async (api, address) => { + const result = await api.prepareEscrowCancellation( + address, + requests.prepareEscrowCancellation.normal, + instructionsWithMaxLedgerVersionOffset + ) + assertResultMatch( + result, + responses.prepareEscrowCancellation.normal, + 'prepare' + ) + }, + + 'prepareEscrowCancellation with memos': async (api, address) => { + const result = await api.prepareEscrowCancellation( + address, + requests.prepareEscrowCancellation.memos + ) + assertResultMatch( + result, + responses.prepareEscrowCancellation.memos, + 'prepare' + ) + } +} diff --git a/test/api/prepareEscrowCreation/index.ts b/test/api/prepareEscrowCreation/index.ts new file mode 100644 index 00000000..20aab04a --- /dev/null +++ b/test/api/prepareEscrowCreation/index.ts @@ -0,0 +1,50 @@ +import requests from '../../fixtures/requests' +import responses from '../../fixtures/responses' +import { assertRejects, assertResultMatch, TestSuite } from '../../utils' +const instructionsWithMaxLedgerVersionOffset = { maxLedgerVersionOffset: 100 } + +export const config = { + // TODO: The mock server right now returns a hard-coded string, no matter + // what "Account" value you pass. We'll need it to support more accurate + // responses before we can turn these tests on. + skipXAddress: true +} + + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + 'prepareEscrowCreation': async (api, address) => { + const localInstructions = { + ...instructionsWithMaxLedgerVersionOffset, + maxFee: '0.000012' + } + const result = await api.prepareEscrowCreation( + address, + requests.prepareEscrowCreation.normal, + localInstructions + ) + assertResultMatch(result, responses.prepareEscrowCreation.normal, 'prepare') + }, + + 'prepareEscrowCreation full': async (api, address) => { + const result = await api.prepareEscrowCreation( + address, + requests.prepareEscrowCreation.full + ) + assertResultMatch(result, responses.prepareEscrowCreation.full, 'prepare') + }, + + 'prepareEscrowCreation - invalid': async (api, address) => { + const escrow = Object.assign({}, requests.prepareEscrowCreation.full) + delete escrow.amount // Make invalid + await assertRejects( + api.prepareEscrowCreation(address, escrow), + api.errors.ValidationError, + 'instance.escrowCreation requires property "amount"' + ) + } +} diff --git a/test/api/prepareEscrowExecution/index.ts b/test/api/prepareEscrowExecution/index.ts new file mode 100644 index 00000000..3279448d --- /dev/null +++ b/test/api/prepareEscrowExecution/index.ts @@ -0,0 +1,60 @@ +import { TestSuite, assertRejects, assertResultMatch } from '../../utils' +import requests from '../../fixtures/requests' +import responses from '../../fixtures/responses' +const instructionsWithMaxLedgerVersionOffset = { maxLedgerVersionOffset: 100 } + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + 'prepareEscrowExecution': async (api, address) => { + const result = await api.prepareEscrowExecution( + address, + requests.prepareEscrowExecution.normal, + instructionsWithMaxLedgerVersionOffset + ) + assertResultMatch( + result, + responses.prepareEscrowExecution.normal, + 'prepare' + ) + }, + + 'prepareEscrowExecution - simple': async (api, address) => { + const result = await api.prepareEscrowExecution( + address, + requests.prepareEscrowExecution.simple + ) + assertResultMatch( + result, + responses.prepareEscrowExecution.simple, + 'prepare' + ) + }, + + 'prepareEscrowExecution - no condition': async (api, address) => { + await assertRejects( + api.prepareEscrowExecution( + address, + requests.prepareEscrowExecution.noCondition, + instructionsWithMaxLedgerVersionOffset + ), + api.errors.ValidationError, + '"condition" and "fulfillment" fields on EscrowFinish must only be specified together.' + ) + }, + + 'prepareEscrowExecution - no fulfillment': async (api, address) => { + await assertRejects( + api.prepareEscrowExecution( + address, + requests.prepareEscrowExecution.noFulfillment, + instructionsWithMaxLedgerVersionOffset + ), + api.errors.ValidationError, + '"condition" and "fulfillment" fields on EscrowFinish must only be specified together.' + ) + } +} diff --git a/test/api/prepareOrder/index.ts b/test/api/prepareOrder/index.ts new file mode 100644 index 00000000..f2f224d7 --- /dev/null +++ b/test/api/prepareOrder/index.ts @@ -0,0 +1,52 @@ +import requests from '../../fixtures/requests' +import responses from '../../fixtures/responses' +import { assertRejects, assertResultMatch, TestSuite } from '../../utils' +const instructionsWithMaxLedgerVersionOffset = { maxLedgerVersionOffset: 100 } + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + 'buy order': async (api, address) => { + const request = requests.prepareOrder.buy + const result = await api.prepareOrder(address, request) + assertResultMatch(result, responses.prepareOrder.buy, 'prepare') + }, + + 'buy order with expiration': async (api, address) => { + const request = requests.prepareOrder.expiration + const response = responses.prepareOrder.expiration + const result = await api.prepareOrder( + address, + request, + instructionsWithMaxLedgerVersionOffset + ) + assertResultMatch(result, response, 'prepare') + }, + + 'sell order': async (api, address) => { + const request = requests.prepareOrder.sell + const result = await api.prepareOrder( + address, + request, + instructionsWithMaxLedgerVersionOffset + ) + assertResultMatch(result, responses.prepareOrder.sell, 'prepare') + }, + + 'invalid': async (api, address) => { + const request = Object.assign({}, requests.prepareOrder.sell) + delete request.direction // Make invalid + await assertRejects( + api.prepareOrder( + address, + request, + instructionsWithMaxLedgerVersionOffset + ), + api.errors.ValidationError, + 'instance.order requires property "direction"' + ) + } +} diff --git a/test/api/prepareOrderCancellation/index.ts b/test/api/prepareOrderCancellation/index.ts new file mode 100644 index 00000000..f3ab10e4 --- /dev/null +++ b/test/api/prepareOrderCancellation/index.ts @@ -0,0 +1,59 @@ +import requests from '../../fixtures/requests' +import responses from '../../fixtures/responses' +import { assertRejects, assertResultMatch, TestSuite } from '../../utils' +const instructionsWithMaxLedgerVersionOffset = { maxLedgerVersionOffset: 100 } + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + 'prepareOrderCancellation': async (api, address) => { + const request = requests.prepareOrderCancellation.simple + const result = await api.prepareOrderCancellation( + address, + request, + instructionsWithMaxLedgerVersionOffset + ) + assertResultMatch( + result, + responses.prepareOrderCancellation.normal, + 'prepare' + ) + }, + + 'no instructions': async (api, address) => { + const request = requests.prepareOrderCancellation.simple + const result = await api.prepareOrderCancellation(address, request) + assertResultMatch( + result, + responses.prepareOrderCancellation.noInstructions, + 'prepare' + ) + }, + + 'with memos': async (api, address) => { + const request = requests.prepareOrderCancellation.withMemos + const result = await api.prepareOrderCancellation(address, request) + assertResultMatch( + result, + responses.prepareOrderCancellation.withMemos, + 'prepare' + ) + }, + + 'invalid': async (api, address) => { + const request = Object.assign( + {}, + requests.prepareOrderCancellation.withMemos + ) + delete request.orderSequence // Make invalid + + await assertRejects( + api.prepareOrderCancellation(address, request), + api.errors.ValidationError, + 'instance.orderCancellation requires property "orderSequence"' + ) + } +} diff --git a/test/api/preparePayment/index.ts b/test/api/preparePayment/index.ts new file mode 100644 index 00000000..bedc4189 --- /dev/null +++ b/test/api/preparePayment/index.ts @@ -0,0 +1,441 @@ +import { assertResultMatch, TestSuite, assertRejects } from '../../utils' +import responses from '../../fixtures/responses' +import requests from '../../fixtures/requests' +import { ValidationError } from 'ripple-api/common/errors' +const instructionsWithMaxLedgerVersionOffset = { maxLedgerVersionOffset: 100 } +const { preparePayment: REQUEST_FIXTURES } = requests +const { preparePayment: RESPONSE_FIXTURES } = responses +const RECIPIENT_ADDRESS = 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo' + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + 'normal': async (api, address) => { + const localInstructions = { + ...instructionsWithMaxLedgerVersionOffset, + maxFee: '0.000012' + } + const response = await api.preparePayment( + address, + REQUEST_FIXTURES.normal, + localInstructions + ) + assertResultMatch(response, RESPONSE_FIXTURES.normal, 'prepare') + }, + + 'min amount xrp': async (api, address) => { + const localInstructions = { + ...instructionsWithMaxLedgerVersionOffset, + maxFee: '0.000012' + } + const response = await api.preparePayment( + address, + REQUEST_FIXTURES.minAmountXRP, + localInstructions + ) + assertResultMatch(response, RESPONSE_FIXTURES.minAmountXRP, 'prepare') + }, + + 'min amount xrp2xrp': async (api, address) => { + const response = await api.preparePayment( + address, + REQUEST_FIXTURES.minAmount, + instructionsWithMaxLedgerVersionOffset + ) + assertResultMatch(response, RESPONSE_FIXTURES.minAmountXRPXRP, 'prepare') + }, + + 'XRP to XRP': async (api, address) => { + const payment = { + source: { + address: 'r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59', + maxAmount: { value: '1', currency: 'XRP' } + }, + destination: { + address: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', + amount: { value: '1', currency: 'XRP' } + } + } + const expected = { + txJSON: + '{"TransactionType":"Payment","Account":"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59","Destination":"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo","Amount":"1000000","Flags":2147483648,"LastLedgerSequence":8820051,"Sequence":23,"Fee":"12"}', + instructions: { + fee: '0.000012', + sequence: 23, + maxLedgerVersion: 8820051 + } + } + const response = await api.preparePayment( + address, + payment, + instructionsWithMaxLedgerVersionOffset + ) + assertResultMatch(response, expected, 'prepare') + }, + + 'XRP drops to XRP drops': async (api, address) => { + const payment = { + source: { + address: 'r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59', + maxAmount: { value: '1000000', currency: 'drops' } + }, + destination: { + address: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', + amount: { value: '1000000', currency: 'drops' } + } + } + const expected = { + txJSON: + '{"TransactionType":"Payment","Account":"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59","Destination":"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo","Amount":"1000000","Flags":2147483648,"LastLedgerSequence":8820051,"Sequence":23,"Fee":"12"}', + instructions: { + fee: '0.000012', + sequence: 23, + maxLedgerVersion: 8820051 + } + } + const response = await api.preparePayment( + address, + payment, + instructionsWithMaxLedgerVersionOffset + ) + assertResultMatch(response, expected, 'prepare') + }, + + 'XRP drops to XRP': async (api, address) => { + const payment = { + source: { + address: 'r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59', + maxAmount: { value: '1000000', currency: 'drops' } + }, + destination: { + address: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', + amount: { value: '1', currency: 'XRP' } + } + } + const expected = { + txJSON: + '{"TransactionType":"Payment","Account":"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59","Destination":"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo","Amount":"1000000","Flags":2147483648,"LastLedgerSequence":8820051,"Sequence":23,"Fee":"12"}', + instructions: { + fee: '0.000012', + sequence: 23, + maxLedgerVersion: 8820051 + } + } + const response = await api.preparePayment( + address, + payment, + instructionsWithMaxLedgerVersionOffset + ) + assertResultMatch(response, expected, 'prepare') + }, + + 'XRP to XRP drops': async (api, address) => { + const payment = { + source: { + address: 'r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59', + maxAmount: { value: '1', currency: 'XRP' } + }, + destination: { + address: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', + amount: { value: '1000000', currency: 'drops' } + } + } + const expected = { + txJSON: + '{"TransactionType":"Payment","Account":"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59","Destination":"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo","Amount":"1000000","Flags":2147483648,"LastLedgerSequence":8820051,"Sequence":23,"Fee":"12"}', + instructions: { + fee: '0.000012', + sequence: 23, + maxLedgerVersion: 8820051 + } + } + const response = await api.preparePayment( + address, + payment, + instructionsWithMaxLedgerVersionOffset + ) + assertResultMatch(response, expected, 'prepare') + }, + + // Errors + 'rejects promise and does not throw when payment object is invalid': async ( + api, + address + ) => { + const payment = { + source: { + address: address, + // instead of `maxAmount` + amount: { value: '1000', currency: 'drops' } + }, + destination: { + address: RECIPIENT_ADDRESS, + amount: { value: '1000', currency: 'drops' } + } + } + + return assertRejects( + api.preparePayment(address, payment), + ValidationError, + 'payment must specify either (source.maxAmount and destination.amount) or (source.amount and destination.minAmount)' + ) + }, + + 'rejects promise and does not throw when field is missing': async ( + api, + address + ) => { + // Marking as "any" to get around the fact that TS won't allow this. + const payment: any = { + source: { address: address }, + destination: { + address: RECIPIENT_ADDRESS, + amount: { value: '1000', currency: 'drops' } + } + } + + return assertRejects( + api.preparePayment(address, payment), + ValidationError, + 'instance.payment.source is not exactly one from ,' + ) + }, + + 'rejects promise and does not throw when fee exceeds maxFeeXRP': async ( + api, + address + ) => { + const payment = { + source: { + address: address, + maxAmount: { value: '1000', currency: 'drops' } + }, + destination: { + address: RECIPIENT_ADDRESS, + amount: { value: '1000', currency: 'drops' } + } + } + return assertRejects( + api.preparePayment(address, payment, { fee: '3' }), + ValidationError, + 'Fee of 3 XRP exceeds max of 2 XRP. To use this fee, increase `maxFeeXRP` in the RippleAPI constructor.' + ) + }, + + 'XRP to XRP no partial': async (api, address) => { + return assertRejects( + api.preparePayment(address, REQUEST_FIXTURES.wrongPartial), + ValidationError, + 'XRP to XRP payments cannot be partial payments' + ) + }, + + 'address must match payment.source.address': async (api, address) => { + return assertRejects( + api.preparePayment(address, REQUEST_FIXTURES.wrongAddress), + ValidationError, + 'address must match payment.source.address' + ) + }, + + 'wrong amount': async (api, address) => { + return assertRejects( + api.preparePayment(address, REQUEST_FIXTURES.wrongAmount), + ValidationError, + 'payment must specify either (source.maxAmount and destination.amount) or (source.amount and destination.minAmount)' + ) + }, + + 'throws when fee exceeds 2 XRP': async (api, address) => { + const localInstructions = { + ...instructionsWithMaxLedgerVersionOffset, + fee: '2.1' + } + return assertRejects( + api.preparePayment(address, REQUEST_FIXTURES.normal, localInstructions), + ValidationError, + 'Fee of 2.1 XRP exceeds max of 2 XRP. To use this fee, increase `maxFeeXRP` in the RippleAPI constructor.' + ) + }, + + 'preparePayment with all options specified': async (api, address) => { + const version = await api.getLedgerVersion() + const localInstructions = { + maxLedgerVersion: version + 100, + fee: '0.000012' + } + const response = await api.preparePayment( + address, + REQUEST_FIXTURES.allOptions, + localInstructions + ) + assertResultMatch(response, RESPONSE_FIXTURES.allOptions, 'prepare') + }, + + 'preparePayment without counterparty set': async (api, address) => { + const localInstructions = { + ...instructionsWithMaxLedgerVersionOffset, + sequence: 23 + } + const response = await api.preparePayment( + address, + REQUEST_FIXTURES.noCounterparty, + localInstructions + ) + assertResultMatch(response, RESPONSE_FIXTURES.noCounterparty, 'prepare') + }, + + 'destination.minAmount': async (api, address) => { + const response = await api.preparePayment( + address, + responses.getPaths.sendAll[0], + instructionsWithMaxLedgerVersionOffset + ) + assertResultMatch(response, RESPONSE_FIXTURES.minAmount, 'prepare') + }, + + 'caps fee at 2 XRP by default': async (api, address) => { + api._feeCushion = 1000000 + const expectedResponse = { + txJSON: + '{"Flags":2147483648,"TransactionType":"Payment","Account":"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59","Destination":"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo","Amount":{"value":"0.01","currency":"USD","issuer":"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM"},"SendMax":{"value":"0.01","currency":"USD","issuer":"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM"},"LastLedgerSequence":8820051,"Fee":"2000000","Sequence":23}', + instructions: { + fee: '2', + sequence: 23, + maxLedgerVersion: 8820051 + } + } + const response = await api.preparePayment( + address, + REQUEST_FIXTURES.normal, + instructionsWithMaxLedgerVersionOffset + ) + assertResultMatch(response, expectedResponse, 'prepare') + }, + + 'allows fee exceeding 2 XRP when maxFeeXRP is higher': async ( + api, + address + ) => { + api._maxFeeXRP = '2.2' + const localInstructions = { + ...instructionsWithMaxLedgerVersionOffset, + fee: '2.1' + } + const expectedResponse = { + txJSON: + '{"Flags":2147483648,"TransactionType":"Payment","Account":"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59","Destination":"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo","Amount":{"value":"0.01","currency":"USD","issuer":"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM"},"SendMax":{"value":"0.01","currency":"USD","issuer":"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM"},"LastLedgerSequence":8820051,"Fee":"2100000","Sequence":23}', + instructions: { + fee: '2.1', + sequence: 23, + maxLedgerVersion: 8820051 + } + } + const response = await api.preparePayment( + address, + REQUEST_FIXTURES.normal, + localInstructions + ) + assertResultMatch(response, expectedResponse, 'prepare') + }, + + 'fee - default maxFee of 2 XRP': async (api, address) => { + api._feeCushion = 1000000 + const expectedResponse = { + txJSON: + '{"Flags":2147483648,"TransactionType":"Payment","Account":"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59","Destination":"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo","Amount":{"value":"0.01","currency":"USD","issuer":"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM"},"SendMax":{"value":"0.01","currency":"USD","issuer":"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM"},"LastLedgerSequence":8820051,"Fee":"2000000","Sequence":23}', + instructions: { + fee: '2', + sequence: 23, + maxLedgerVersion: 8820051 + } + } + const response = await api.preparePayment( + address, + requests.preparePayment.normal, + instructionsWithMaxLedgerVersionOffset + ) + assertResultMatch(response, expectedResponse, 'prepare') + }, + + 'fee - capped to maxFeeXRP when maxFee exceeds maxFeeXRP': async ( + api, + address + ) => { + api._feeCushion = 1000000 + api._maxFeeXRP = '3' + const localInstructions = { + ...instructionsWithMaxLedgerVersionOffset, + maxFee: '4' + } + const expectedResponse = { + txJSON: + '{"Flags":2147483648,"TransactionType":"Payment","Account":"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59","Destination":"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo","Amount":{"value":"0.01","currency":"USD","issuer":"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM"},"SendMax":{"value":"0.01","currency":"USD","issuer":"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM"},"LastLedgerSequence":8820051,"Fee":"3000000","Sequence":23}', + instructions: { + fee: '3', + sequence: 23, + maxLedgerVersion: 8820051 + } + } + const response = await api.preparePayment( + address, + requests.preparePayment.normal, + localInstructions + ) + assertResultMatch(response, expectedResponse, 'prepare') + }, + + 'fee - capped to maxFee': async (api, address) => { + api._feeCushion = 1000000 + api._maxFeeXRP = '5' + const localInstructions = { + ...instructionsWithMaxLedgerVersionOffset, + maxFee: '4' + } + const expectedResponse = { + txJSON: + '{"Flags":2147483648,"TransactionType":"Payment","Account":"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59","Destination":"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo","Amount":{"value":"0.01","currency":"USD","issuer":"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM"},"SendMax":{"value":"0.01","currency":"USD","issuer":"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM"},"LastLedgerSequence":8820051,"Fee":"4000000","Sequence":23}', + instructions: { + fee: '4', + sequence: 23, + maxLedgerVersion: 8820051 + } + } + const response = await api.preparePayment( + address, + requests.preparePayment.normal, + localInstructions + ) + assertResultMatch(response, expectedResponse, 'prepare') + }, + + 'fee - calculated fee does not use more than 6 decimal places': async ( + api, + address + ) => { + api.connection._send( + JSON.stringify({ + command: 'config', + data: { loadFactor: 5407.96875 } + }) + ) + const expectedResponse = { + txJSON: + '{"Flags":2147483648,"TransactionType":"Payment","Account":"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59","Destination":"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo","Amount":{"value":"0.01","currency":"USD","issuer":"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM"},"SendMax":{"value":"0.01","currency":"USD","issuer":"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM"},"LastLedgerSequence":8820051,"Fee":"64896","Sequence":23}', + instructions: { + fee: '0.064896', + sequence: 23, + maxLedgerVersion: 8820051 + } + } + const response = await api.preparePayment( + address, + requests.preparePayment.normal, + instructionsWithMaxLedgerVersionOffset + ) + assertResultMatch(response, expectedResponse, 'prepare') + } +} diff --git a/test/api/preparePaymentChannelClaim/index.ts b/test/api/preparePaymentChannelClaim/index.ts new file mode 100644 index 00000000..dc9d4d35 --- /dev/null +++ b/test/api/preparePaymentChannelClaim/index.ts @@ -0,0 +1,97 @@ +import assert from 'assert-diff' +import requests from '../../fixtures/requests' +import responses from '../../fixtures/responses' +import { assertResultMatch, TestSuite } from '../../utils' +const instructionsWithMaxLedgerVersionOffset = { maxLedgerVersionOffset: 100 } +const { preparePaymentChannelClaim: REQUEST_FIXTURES } = requests +const { preparePaymentChannelClaim: RESPONSE_FIXTURES } = responses + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + 'default': async (api, address) => { + const localInstructions = { + ...instructionsWithMaxLedgerVersionOffset, + maxFee: '0.000012' + } + const response = await api.preparePaymentChannelClaim( + address, + REQUEST_FIXTURES.normal, + localInstructions + ) + assertResultMatch(response, RESPONSE_FIXTURES.normal, 'prepare') + }, + + 'with renew': async (api, address) => { + const localInstructions = { + ...instructionsWithMaxLedgerVersionOffset, + maxFee: '0.000012' + } + const response = await api.preparePaymentChannelClaim( + address, + REQUEST_FIXTURES.renew, + localInstructions + ) + assertResultMatch(response, RESPONSE_FIXTURES.renew, 'prepare') + }, + + 'with close': async (api, address) => { + const localInstructions = { + ...instructionsWithMaxLedgerVersionOffset, + maxFee: '0.000012' + } + const response = await api.preparePaymentChannelClaim( + address, + REQUEST_FIXTURES.close, + localInstructions + ) + assertResultMatch(response, RESPONSE_FIXTURES.close, 'prepare') + }, + + 'rejects Promise on preparePaymentChannelClaim with renew and close': async ( + api, + address + ) => { + try { + const prepared = await api.preparePaymentChannelClaim( + address, + REQUEST_FIXTURES.full + ) + throw new Error( + 'Expected method to reject. Prepared transaction: ' + + JSON.stringify(prepared) + ) + } catch (err) { + assert.strictEqual(err.name, 'ValidationError') + assert.strictEqual( + err.message, + '"renew" and "close" flags on PaymentChannelClaim are mutually exclusive' + ) + } + }, + + 'rejects Promise on preparePaymentChannelClaim with no signature': async ( + api, + address + ) => { + try { + const prepared = await api.preparePaymentChannelClaim( + address, + REQUEST_FIXTURES.noSignature + ) + throw new Error( + 'Expected method to reject. Prepared transaction: ' + + JSON.stringify(prepared) + ) + } catch (err) { + assert.strictEqual(err.name, 'ValidationError') + assert.strictEqual( + err.message, + '"signature" and "publicKey" fields on PaymentChannelClaim must only be specified together.' + ) + } + } +} diff --git a/test/api/preparePaymentChannelCreate/index.ts b/test/api/preparePaymentChannelCreate/index.ts new file mode 100644 index 00000000..f523ce9e --- /dev/null +++ b/test/api/preparePaymentChannelCreate/index.ts @@ -0,0 +1,47 @@ +import requests from '../../fixtures/requests' +import responses from '../../fixtures/responses' +import { assertResultMatch, TestSuite } from '../../utils' +const instructionsWithMaxLedgerVersionOffset = { maxLedgerVersionOffset: 100 } + +export const config = { + // TODO: The mock server right now returns a hard-coded string, no matter + // what "Account" value you pass. We'll need it to support more accurate + // responses before we can turn these tests on. + skipXAddress: true +} + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + 'preparePaymentChannelCreate': async (api, address) => { + const localInstructions = { + ...instructionsWithMaxLedgerVersionOffset, + maxFee: '0.000012' + } + const result = await api.preparePaymentChannelCreate( + address, + requests.preparePaymentChannelCreate.normal, + localInstructions + ) + assertResultMatch( + result, + responses.preparePaymentChannelCreate.normal, + 'prepare' + ) + }, + + 'preparePaymentChannelCreate full': async (api, address) => { + const result = await api.preparePaymentChannelCreate( + address, + requests.preparePaymentChannelCreate.full + ) + assertResultMatch( + result, + responses.preparePaymentChannelCreate.full, + 'prepare' + ) + } +} diff --git a/test/api/preparePaymentChannelFund/index.ts b/test/api/preparePaymentChannelFund/index.ts new file mode 100644 index 00000000..8bc23dff --- /dev/null +++ b/test/api/preparePaymentChannelFund/index.ts @@ -0,0 +1,40 @@ +import requests from '../../fixtures/requests' +import responses from '../../fixtures/responses' +import { assertResultMatch, TestSuite } from '../../utils' +const instructionsWithMaxLedgerVersionOffset = { maxLedgerVersionOffset: 100 } + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + 'preparePaymentChannelFund': async (api, address) => { + const localInstructions = { + ...instructionsWithMaxLedgerVersionOffset, + maxFee: '0.000012' + } + const result = await api.preparePaymentChannelFund( + address, + requests.preparePaymentChannelFund.normal, + localInstructions + ) + assertResultMatch( + result, + responses.preparePaymentChannelFund.normal, + 'prepare' + ) + }, + + 'preparePaymentChannelFund full': async (api, address) => { + const result = await api.preparePaymentChannelFund( + address, + requests.preparePaymentChannelFund.full + ) + assertResultMatch( + result, + responses.preparePaymentChannelFund.full, + 'prepare' + ) + } +} diff --git a/test/api/prepareSettings/index.ts b/test/api/prepareSettings/index.ts index 33cf737e..c4fea9eb 100644 --- a/test/api/prepareSettings/index.ts +++ b/test/api/prepareSettings/index.ts @@ -234,5 +234,22 @@ export default { ) assert.strictEqual(err.name, 'ValidationError') } + }, + 'offline': async (api, address) => { + // const api = new RippleAPI() + const secret = 'shsWGZcmZz6YsWWmcnpfr6fLTdtFV' + + const settings = requests.prepareSettings.domain + const instructions = { + sequence: 23, + maxLedgerVersion: 8820051, + fee: '0.000012' + } + const result = await api.prepareSettings(address, settings, instructions) + assertResultMatch(result, responses.prepareSettings.flags, 'prepare') + assert.deepEqual( + api.sign(result.txJSON, secret), + responses.prepareSettings.signed + ) } } diff --git a/test/api/prepareTransaction/index.ts b/test/api/prepareTransaction/index.ts new file mode 100644 index 00000000..8b0cf939 --- /dev/null +++ b/test/api/prepareTransaction/index.ts @@ -0,0 +1,1219 @@ +import { RippledError, ValidationError } from 'ripple-api/common/errors' +import requests from '../../fixtures/requests' +import responses from '../../fixtures/responses' +import { assertRejects, assertResultMatch, TestSuite } from '../../utils' +const instructionsWithMaxLedgerVersionOffset = { maxLedgerVersionOffset: 100 } + +export const config = { + // TODO: The mock server right now returns a hard-coded string, no matter + // what "Account" value you pass. We'll need it to support more accurate + // responses before we can turn these tests on. + skipXAddress: true +} + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + 'auto-fillable fields - does not overwrite Fee in txJSON': async ( + api, + address + ) => { + const localInstructions = instructionsWithMaxLedgerVersionOffset + const txJSON = { + TransactionType: 'DepositPreauth', + Account: address, + Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', + Fee: '10' + } + const response = await api.prepareTransaction(txJSON, localInstructions) + const expected = { + txJSON: `{"TransactionType":"DepositPreauth","Account":"${address}","Authorize":"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo","Flags":2147483648,"LastLedgerSequence":8820051,"Fee":"10","Sequence":23}`, + instructions: { + fee: '0.00001', // Notice there are not always 6 digits after the decimal point as trailing zeros are omitted + sequence: 23, + maxLedgerVersion: 8820051 + } + } + return assertResultMatch(response, expected, 'prepare') + }, + + 'does not overwrite Fee in Instructions': async (api, address) => { + const localInstructions = { + ...instructionsWithMaxLedgerVersionOffset, + fee: '0.000014' // CAUTION: This `fee` is specified in XRP, not drops. + } + const txJSON = { + TransactionType: 'DepositPreauth', + Account: address, + Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo' + } + const response = await api.prepareTransaction(txJSON, localInstructions) + const expected = { + txJSON: `{"TransactionType":"DepositPreauth","Account":"${address}","Authorize":"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo","Flags":2147483648,"LastLedgerSequence":8820051,"Fee":"14","Sequence":23}`, + instructions: { + fee: '0.000014', + sequence: 23, + maxLedgerVersion: 8820051 + } + } + return assertResultMatch(response, expected, 'prepare') + }, + + 'rejects Promise if both are set, even when txJSON.Fee matches instructions.fee': async ( + api, + address + ) => { + const localInstructions = { + ...instructionsWithMaxLedgerVersionOffset, + fee: '0.000016' + } + const txJSON = { + TransactionType: 'DepositPreauth', + Account: address, + Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', + Fee: '16' + } + await assertRejects( + api.prepareTransaction(txJSON, localInstructions), + ValidationError, + '`Fee` in txJSON and `fee` in `instructions` cannot both be set' + ) + }, + + 'rejects Promise if both are set, when txJSON.Fee does not match instructions.fee': async ( + api, + address + ) => { + const localInstructions = { + ...instructionsWithMaxLedgerVersionOffset, + fee: '0.000018' + } + const txJSON = { + TransactionType: 'DepositPreauth', + Account: address, + Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', + Fee: '20' + } + await assertRejects( + api.prepareTransaction(txJSON, localInstructions), + ValidationError, + '`Fee` in txJSON and `fee` in `instructions` cannot both be set' + ) + }, + + 'rejects Promise when the Fee is capitalized in Instructions': async ( + api, + address + ) => { + const localInstructions = { + ...instructionsWithMaxLedgerVersionOffset, + Fee: '0.000022' // Intentionally capitalized in this test, but the correct field would be `fee` + } + const txJSON = { + TransactionType: 'DepositPreauth', + Account: address, + Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo' + } + await assertRejects( + api.prepareTransaction(txJSON, localInstructions), + ValidationError, + 'instance additionalProperty "Fee" exists in instance when not allowed' + ) + }, + + 'rejects Promise when the fee is specified in txJSON': async ( + api, + address + ) => { + const localInstructions = instructionsWithMaxLedgerVersionOffset + const txJSON = { + TransactionType: 'DepositPreauth', + Account: address, + Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', + fee: '10' + } + await assertRejects( + api.prepareTransaction(txJSON, localInstructions), + ValidationError, + 'txJSON additionalProperty "fee" exists in instance when not allowed' + ) + }, + + 'does not overwrite Sequence in txJSON': async (api, address) => { + const localInstructions = { + ...instructionsWithMaxLedgerVersionOffset, + maxFee: '0.000012' + } + const txJSON = { + TransactionType: 'DepositPreauth', + Account: address, + Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', + Sequence: 100 + } + const response = await api.prepareTransaction(txJSON, localInstructions) + const expected = { + txJSON: `{"TransactionType":"DepositPreauth","Account":"${address}","Authorize":"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo","Flags":2147483648,"LastLedgerSequence":8820051,"Fee":"12","Sequence":100}`, + instructions: { + fee: '0.000012', + sequence: 100, + maxLedgerVersion: 8820051 + } + } + return assertResultMatch(response, expected, 'prepare') + }, + + 'does not overwrite Sequence in Instructions': async (api, address) => { + const localInstructions = { + ...instructionsWithMaxLedgerVersionOffset, + maxFee: '0.000012', + sequence: 100 + } + const txJSON = { + TransactionType: 'DepositPreauth', + Account: address, + Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo' + } + const response = await api.prepareTransaction(txJSON, localInstructions) + const expected = { + txJSON: `{"TransactionType":"DepositPreauth","Account":"${address}","Authorize":"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo","Flags":2147483648,"LastLedgerSequence":8820051,"Fee":"12","Sequence":100}`, + instructions: { + fee: '0.000012', + sequence: 100, + maxLedgerVersion: 8820051 + } + } + return assertResultMatch(response, expected, 'prepare') + }, + + 'does not overwrite Sequence when same sequence is provided in both txJSON and Instructions': async ( + api, + address + ) => { + const localInstructions = { + ...instructionsWithMaxLedgerVersionOffset, + maxFee: '0.000012', + sequence: 100 + } + const txJSON = { + TransactionType: 'DepositPreauth', + Account: address, + Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', + Sequence: 100 + } + const response = await api.prepareTransaction(txJSON, localInstructions) + const expected = { + txJSON: `{"TransactionType":"DepositPreauth","Account":"${address}","Authorize":"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo","Flags":2147483648,"LastLedgerSequence":8820051,"Fee":"12","Sequence":100}`, + instructions: { + fee: '0.000012', + sequence: 100, + maxLedgerVersion: 8820051 + } + } + return assertResultMatch(response, expected, 'prepare') + }, + + 'rejects Promise when Sequence in txJSON does not match sequence in Instructions': async ( + api, + address + ) => { + const localInstructions = { + ...instructionsWithMaxLedgerVersionOffset, + maxFee: '0.000012', + sequence: 100 + } + const txJSON = { + TransactionType: 'DepositPreauth', + Account: address, + Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', + Sequence: 101 + } + await assertRejects( + api.prepareTransaction(txJSON, localInstructions), + ValidationError, + '`Sequence` in txJSON must match `sequence` in `instructions`' + ) + }, + + 'rejects Promise when the Sequence is capitalized in Instructions': async ( + api, + address + ) => { + const localInstructions = { + ...instructionsWithMaxLedgerVersionOffset, + maxFee: '0.000012', + Sequence: 100 // Intentionally capitalized in this test, but the correct field would be `sequence` + } + const txJSON = { + TransactionType: 'DepositPreauth', + Account: address, + Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo' + } + await assertRejects( + api.prepareTransaction(txJSON, localInstructions), + ValidationError, + 'instance additionalProperty "Sequence" exists in instance when not allowed' + ) + }, + + // LastLedgerSequence aka maxLedgerVersion/maxLedgerVersionOffset: + + 'does not overwrite LastLedgerSequence in txJSON': async (api, address) => { + const localInstructions = {} + const txJSON = { + TransactionType: 'DepositPreauth', + Account: address, + Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', + Fee: '10', + LastLedgerSequence: 8880000 + } + const response = await api.prepareTransaction(txJSON, localInstructions) + const expected = { + txJSON: `{"TransactionType":"DepositPreauth","Account":"${address}","Authorize":"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo","Flags":2147483648,"LastLedgerSequence":8880000,"Fee":"10","Sequence":23}`, + instructions: { + fee: '0.00001', // Notice there are not always 6 digits after the decimal point as trailing zeros are omitted + sequence: 23, + maxLedgerVersion: 8880000 + } + } + return assertResultMatch(response, expected, 'prepare') + }, + + 'does not overwrite maxLedgerVersion in Instructions': async ( + api, + address + ) => { + const localInstructions = { + maxLedgerVersion: 8890000 + } + const txJSON = { + TransactionType: 'DepositPreauth', + Account: address, + Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo' + } + const response = await api.prepareTransaction(txJSON, localInstructions) + const expected = { + txJSON: `{"TransactionType":"DepositPreauth","Account":"${address}","Authorize":"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo","Flags":2147483648,"LastLedgerSequence":8890000,"Fee":"12","Sequence":23}`, + instructions: { + fee: '0.000012', + sequence: 23, + maxLedgerVersion: 8890000 + } + } + return assertResultMatch(response, expected, 'prepare') + }, + + 'does not overwrite maxLedgerVersionOffset in Instructions': async ( + api, + address + ) => { + const localInstructions = { + ...instructionsWithMaxLedgerVersionOffset, + maxLedgerVersionOffset: 124 + } + const txJSON = { + TransactionType: 'DepositPreauth', + Account: address, + Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo' + } + const response = await api.prepareTransaction(txJSON, localInstructions) + const expected = { + txJSON: `{"TransactionType":"DepositPreauth","Account":"${address}","Authorize":"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo","Flags":2147483648,"LastLedgerSequence":8820075,"Fee":"12","Sequence":23}`, + instructions: { + fee: '0.000012', + sequence: 23, + maxLedgerVersion: 8820075 + } + } + return assertResultMatch(response, expected, 'prepare') + }, + + 'rejects Promise if txJSON.LastLedgerSequence and instructions.maxLedgerVersion both are set': async ( + api, + address + ) => { + const localInstructions = { + maxLedgerVersion: 8900000 + } + const txJSON = { + TransactionType: 'DepositPreauth', + Account: address, + Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', + Fee: '16', + LastLedgerSequence: 8900000 + } + await assertRejects( + api.prepareTransaction(txJSON, localInstructions), + ValidationError, + '`LastLedgerSequence` in txJSON and `maxLedgerVersion` in `instructions` cannot both be set' + ) + }, + + 'rejects Promise if txJSON.LastLedgerSequence and instructions.maxLedgerVersionOffset both are set': async ( + api, + address + ) => { + const localInstructions = { + ...instructionsWithMaxLedgerVersionOffset, + maxLedgerVersionOffset: 123 + } + const txJSON = { + TransactionType: 'DepositPreauth', + Account: address, + Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', + Fee: '16', + LastLedgerSequence: 8900000 + } + await assertRejects( + api.prepareTransaction(txJSON, localInstructions), + ValidationError, + '`LastLedgerSequence` in txJSON and `maxLedgerVersionOffset` in `instructions` cannot both be set' + ) + }, + + 'rejects Promise if instructions.maxLedgerVersion and instructions.maxLedgerVersionOffset both are set': async ( + api, + address + ) => { + const localInstructions = { + ...instructionsWithMaxLedgerVersionOffset, + maxLedgerVersion: 8900000, + maxLedgerVersionOffset: 123 + } + const txJSON = { + TransactionType: 'DepositPreauth', + Account: address, + Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', + Fee: '16' + } + await assertRejects( + api.prepareTransaction(txJSON, localInstructions), + ValidationError, + 'instance is of prohibited type [object Object]' + ) + }, + + 'rejects Promise if txJSON.LastLedgerSequence and instructions.maxLedgerVersion and instructions.maxLedgerVersionOffset all are set': async ( + api, + address + ) => { + const localInstructions = { + ...instructionsWithMaxLedgerVersionOffset, + maxLedgerVersion: 8900000, + maxLedgerVersionOffset: 123 + } + const txJSON = { + TransactionType: 'DepositPreauth', + Account: address, + Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', + Fee: '16', + LastLedgerSequence: 8900000 + } + await assertRejects( + api.prepareTransaction(txJSON, localInstructions), + ValidationError, + 'instance is of prohibited type [object Object]' + ) + }, + + 'rejects Promise when the maxLedgerVersion is capitalized in Instructions': async ( + api, + address + ) => { + const localInstructions = { + ...instructionsWithMaxLedgerVersionOffset, + MaxLedgerVersion: 8900000 // Intentionally capitalized in this test, but the correct field would be `maxLedgerVersion` + } + const txJSON = { + TransactionType: 'DepositPreauth', + Account: address, + Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo' + } + await assertRejects( + api.prepareTransaction(txJSON, localInstructions), + ValidationError, + 'instance additionalProperty "MaxLedgerVersion" exists in instance when not allowed' + ) + }, + + 'rejects Promise when the maxLedgerVersion is specified in txJSON': async ( + api, + address + ) => { + const localInstructions = instructionsWithMaxLedgerVersionOffset + const txJSON = { + TransactionType: 'DepositPreauth', + Account: address, + Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', + maxLedgerVersion: 8900000 + } + await assertRejects( + api.prepareTransaction(txJSON, localInstructions), + ValidationError, + 'txJSON additionalProperty "maxLedgerVersion" exists in instance when not allowed' + ) + }, + + 'rejects Promise when the maxLedgerVersionOffset is specified in txJSON': async ( + api, + address + ) => { + const localInstructions = instructionsWithMaxLedgerVersionOffset + const txJSON = { + TransactionType: 'DepositPreauth', + Account: address, + Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', + maxLedgerVersionOffset: 8900000 + } + await assertRejects( + api.prepareTransaction(txJSON, localInstructions), + ValidationError, + 'txJSON additionalProperty "maxLedgerVersionOffset" exists in instance when not allowed' + ) + }, + + 'rejects Promise when the sequence is specified in txJSON': async ( + api, + address + ) => { + const localInstructions = instructionsWithMaxLedgerVersionOffset + const txJSON = { + TransactionType: 'DepositPreauth', + Account: address, + Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', + sequence: 8900000 + } + await assertRejects( + api.prepareTransaction(txJSON, localInstructions), + ValidationError, + 'txJSON additionalProperty "sequence" exists in instance when not allowed' + ) + }, + + // Paths: is not auto-filled by ripple-lib. + + // Other errors: + + 'rejects Promise when an unrecognized field is in Instructions': async ( + api, + address + ) => { + const localInstructions = { + ...instructionsWithMaxLedgerVersionOffset, + maxFee: '0.000012', + foo: 'bar' + } + const txJSON = { + TransactionType: 'DepositPreauth', + Account: address, + Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo' + } + await assertRejects( + api.prepareTransaction(txJSON, localInstructions), + ValidationError, + 'instance additionalProperty "foo" exists in instance when not allowed' + ) + }, + + 'rejects Promise when Account is missing': async (api, address) => { + const localInstructions = { + ...instructionsWithMaxLedgerVersionOffset, + maxFee: '0.000012' + } + // Marking as "any" to get around the fact that TS won't allow this. + const txJSON: any = { + TransactionType: 'DepositPreauth', + Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo' + } + await assertRejects( + api.prepareTransaction(txJSON, localInstructions), + ValidationError, + 'instance requires property "Account"' + ) + }, + + 'rejects Promise when Account is not a string': async (api, address) => { + const localInstructions = { + ...instructionsWithMaxLedgerVersionOffset, + maxFee: '0.000012' + } + // Marking as "any" to get around the fact that TS won't allow this. + const txJSON: any = { + Account: 1234, + TransactionType: 'DepositPreauth', + Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo' + } + await assertRejects( + api.prepareTransaction(txJSON, localInstructions), + ValidationError, + 'instance.Account is not of a type(s) string,instance.Account is not exactly one from ,' + ) + }, + + 'rejects Promise when Account is invalid': async (api, address) => { + const localInstructions = { + ...instructionsWithMaxLedgerVersionOffset, + maxFee: '0.000012' + } + const txJSON = { + Account: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xkXXXX', // Invalid checksum + TransactionType: 'DepositPreauth', + Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo' + } + await assertRejects( + api.prepareTransaction(txJSON, localInstructions), + ValidationError, + 'instance.Account is not exactly one from ,' + ) + }, + + 'rejects Promise when Account is valid but non-existent on the ledger': async api => { + const localInstructions = { + ...instructionsWithMaxLedgerVersionOffset, + maxFee: '0.000012' + } + const txJSON = { + Account: 'rogvkYnY8SWjxkJNgU4ZRVfLeRyt5DR9i', + TransactionType: 'DepositPreauth', + Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo' + } + await assertRejects( + api.prepareTransaction(txJSON, localInstructions), + RippledError, + 'Account not found.' + ) + }, + + 'rejects Promise when TransactionType is missing': async (api, address) => { + const localInstructions = { + ...instructionsWithMaxLedgerVersionOffset, + maxFee: '0.000012' + } + // Marking as "any" to get around the fact that TS won't allow this. + const txJSON: any = { + Account: address, + Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo' + } + await assertRejects( + api.prepareTransaction(txJSON, localInstructions), + ValidationError, + 'instance requires property "TransactionType"' + ) + }, + + // Note: This transaction will fail at the `sign` step: + // + // Error: DepositPreXXXX is not a valid name or ordinal for TransactionType + // + // at Function.from (ripple-binary-codec/distrib/npm/enums/index.js:43:15) + 'prepares tx when TransactionType is invalid': async (api, address) => { + const localInstructions = { + ...instructionsWithMaxLedgerVersionOffset, + maxFee: '0.000012' + } + const txJSON = { + Account: address, + TransactionType: 'DepositPreXXXX', + Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo' + } + const response = await api.prepareTransaction(txJSON, localInstructions) + const expected = { + txJSON: `{"TransactionType":"DepositPreXXXX","Account":"${address}","Authorize":"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo","Flags":2147483648,"LastLedgerSequence":8820051,"Fee":"12","Sequence":23}`, + instructions: { + fee: '0.000012', + sequence: 23, + maxLedgerVersion: 8820051 + } + } + return assertResultMatch(response, expected, 'prepare') + }, + + 'rejects Promise when TransactionType is not a string': async ( + api, + address + ) => { + const localInstructions = { + ...instructionsWithMaxLedgerVersionOffset, + maxFee: '0.000012' + } + // Marking as "any" to get around the fact that TS won't allow this. + const txJSON: any = { + Account: address, + TransactionType: 1234, + Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo' + } + await assertRejects( + api.prepareTransaction(txJSON, localInstructions), + ValidationError, + 'instance.TransactionType is not of a type(s) string' + ) + }, + + // Note: This transaction will fail at the `submit` step: + // + // [RippledError(Submit failed, { resultCode: 'temMALFORMED', + // resultMessage: 'Malformed transaction.', + // engine_result: 'temMALFORMED', + // engine_result_code: -299, + // engine_result_message: 'Malformed transaction.', + // tx_blob: + // '120013240000000468400000000000000C732102E1EA8199F570E7F997A7B34EDFDA0A7D8B38173A17450B121A2EB048FDD16CA97446304402201F0EF6A2DE7F96966F7082294D14F3EC1EF59C21E29443E5858A0120079357A302203CDB7FEBDEAAD93FF39CB589B55778CB80DC3979F96F27E828D5E659BEB26B7A8114D51F9A17208CF113AF23B97ECD5FCD314FBAE52E', + // tx_json: + // { Account: 'rLRt8bmZFBEeM5VMSxZy15k8KKJEs68W6C', + // Fee: '12', + // Sequence: 4, + // SigningPubKey: + // '02E1EA8199F570E7F997A7B34EDFDA0A7D8B38173A17450B121A2EB048FDD16CA9', + // TransactionType: 'DepositPreauth', + // TxnSignature: + // '304402201F0EF6A2DE7F96966F7082294D14F3EC1EF59C21E29443E5858A0120079357A302203CDB7FEBDEAAD93FF39CB589B55778CB80DC3979F96F27E828D5E659BEB26B7A', + // hash: + // 'C181D470684311658852713DA81F8201062535C8DE2FF853F7DD9981BB85312F' } })] + 'prepares tx when a required field is missing': async (api, address) => { + const localInstructions = { + ...instructionsWithMaxLedgerVersionOffset, + maxFee: '0.000012' + } + const txJSON = { + Account: address, + TransactionType: 'DepositPreauth' + // Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo' // Normally required, intentionally removed + } + const response = await api.prepareTransaction(txJSON, localInstructions) + const expected = { + txJSON: `{"TransactionType":"DepositPreauth","Account":"${address}","Flags":2147483648,"LastLedgerSequence":8820051,"Fee":"12","Sequence":23}`, + instructions: { + fee: '0.000012', + sequence: 23, + maxLedgerVersion: 8820051 + } + } + return assertResultMatch(response, expected, 'prepare') + }, + + 'DepositPreauth - Authorize': async (api, address) => { + const localInstructions = { + ...instructionsWithMaxLedgerVersionOffset, + maxFee: '0.000012' + } + const txJSON = { + TransactionType: 'DepositPreauth', + Account: address, + Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo' + } + const expected = { + txJSON: + '{"TransactionType":"DepositPreauth","Account":"' + + address + + '","Authorize":"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo","Flags":2147483648,"LastLedgerSequence":8820051,"Fee":"12","Sequence":23}', + instructions: { + fee: '0.000012', + sequence: 23, + maxLedgerVersion: 8820051 + } + } + const response = await api.prepareTransaction(txJSON, localInstructions) + return assertResultMatch(response, expected, 'prepare') + }, + + 'DepositPreauth - Unauthorize': async (api, address) => { + const localInstructions = { + ...instructionsWithMaxLedgerVersionOffset, + maxFee: '0.000012' + } + + const txJSON = { + TransactionType: 'DepositPreauth', + Account: address, + Unauthorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo' + } + + const response = await api.prepareTransaction(txJSON, localInstructions) + const expected = { + txJSON: + '{"TransactionType":"DepositPreauth","Account":"' + + address + + '","Unauthorize":"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo","Flags":2147483648,"LastLedgerSequence":8820051,"Fee":"12","Sequence":23}', + instructions: { + fee: '0.000012', + sequence: 23, + maxLedgerVersion: 8820051 + } + } + return assertResultMatch(response, expected, 'prepare') + }, + + // prepareTransaction - Payment + 'Payment - normal': async (api, address) => { + const localInstructions = { + ...instructionsWithMaxLedgerVersionOffset, + maxFee: '0.000012' + } + + const txJSON = { + TransactionType: 'Payment', + Account: address, + Destination: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', + Amount: { + currency: 'USD', + issuer: 'rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM', + value: '0.01' + }, + SendMax: { + currency: 'USD', + issuer: 'rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM', + value: '0.01' + }, + Flags: 0 + } + + const response = await api.prepareTransaction(txJSON, localInstructions) + assertResultMatch(response, responses.preparePayment.normal, 'prepare') + }, + + 'min amount xrp': async (api, address) => { + const localInstructions = { + ...instructionsWithMaxLedgerVersionOffset, + maxFee: '0.000012' + } + + const txJSON = { + TransactionType: 'Payment', + Account: address, + Destination: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', + + // Max amount to send. Use 100 billion XRP to + // ensure that we send the full SendMax amount. + Amount: '100000000000000000', + + SendMax: { + currency: 'USD', + issuer: 'rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM', + value: '0.01' + }, + DeliverMin: '10000', + Flags: api.txFlags.Payment.PartialPayment + } + + const response = await api.prepareTransaction(txJSON, localInstructions) + assertResultMatch( + response, + responses.preparePayment.minAmountXRP, + 'prepare' + ) + }, + + 'min amount xrp2xrp': async (api, address) => { + const txJSON = { + TransactionType: 'Payment', + Account: address, + Destination: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', + Amount: '10000', + Flags: 0 + } + const response = await api.prepareTransaction( + txJSON, + instructionsWithMaxLedgerVersionOffset + ) + + assertResultMatch( + response, + responses.preparePayment.minAmountXRPXRP, + 'prepare' + ) + }, + + 'with all options specified': async (api, address) => { + const ver = await api.getLedgerVersion() + const localInstructions = { + maxLedgerVersion: ver + 100, + fee: '0.000012' + } + const txJSON = { + TransactionType: 'Payment', + Account: address, + Destination: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', + Amount: '10000', + InvoiceID: + 'A98FD36C17BE2B8511AD36DC335478E7E89F06262949F36EB88E2D683BBCC50A', + SourceTag: 14, + DestinationTag: 58, + Memos: [ + { + Memo: { + MemoType: api.convertStringToHex('test'), + MemoFormat: api.convertStringToHex('text/plain'), + MemoData: api.convertStringToHex('texted data') + } + } + ], + Flags: + 0 | + api.txFlags.Payment.NoRippleDirect | + api.txFlags.Payment.LimitQuality + } + const response = await api.prepareTransaction(txJSON, localInstructions) + assertResultMatch(response, responses.preparePayment.allOptions, 'prepare') + }, + + 'fee is capped at default maxFee of 2 XRP (using txJSON.LastLedgerSequence)': async ( + api, + address + ) => { + api._feeCushion = 1000000 + + const txJSON = { + Flags: 2147483648, + TransactionType: 'Payment', + Account: 'r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59', + Destination: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', + Amount: { + value: '0.01', + currency: 'USD', + issuer: 'rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM' + }, + SendMax: { + value: '0.01', + currency: 'USD', + issuer: 'rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM' + }, + LastLedgerSequence: 8820051 + } + const localInstructions = {} + const expectedResponse = { + txJSON: + '{"Flags":2147483648,"TransactionType":"Payment","Account":"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59","Destination":"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo","Amount":{"value":"0.01","currency":"USD","issuer":"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM"},"SendMax":{"value":"0.01","currency":"USD","issuer":"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM"},"LastLedgerSequence":8820051,"Fee":"2000000","Sequence":23}', + instructions: { + fee: '2', + sequence: 23, + maxLedgerVersion: 8820051 + } + } + const response = await api.prepareTransaction(txJSON, localInstructions) + assertResultMatch(response, expectedResponse, 'prepare') + }, + + 'fee is capped at default maxFee of 2 XRP (using instructions.maxLedgerVersion)': async ( + api, + address + ) => { + api._feeCushion = 1000000 + + const txJSON = { + Flags: 2147483648, + TransactionType: 'Payment', + Account: 'r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59', + Destination: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', + Amount: { + value: '0.01', + currency: 'USD', + issuer: 'rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM' + }, + SendMax: { + value: '0.01', + currency: 'USD', + issuer: 'rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM' + } + } + + const localInstructions = { + maxLedgerVersion: 8820051 + } + + const expectedResponse = { + txJSON: + '{"Flags":2147483648,"TransactionType":"Payment","Account":"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59","Destination":"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo","Amount":{"value":"0.01","currency":"USD","issuer":"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM"},"SendMax":{"value":"0.01","currency":"USD","issuer":"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM"},"LastLedgerSequence":8820051,"Fee":"2000000","Sequence":23}', + instructions: { + fee: '2', + sequence: 23, + maxLedgerVersion: 8820051 + } + } + + const response = await api.prepareTransaction(txJSON, localInstructions) + assertResultMatch(response, expectedResponse, 'prepare') + }, + + // prepareTransaction - Payment + 'fee is capped to custom maxFeeXRP when maxFee exceeds maxFeeXRP': async ( + api, + address + ) => { + api._feeCushion = 1000000 + api._maxFeeXRP = '3' + const localInstructions = { + maxFee: '4' // We are testing that this does not matter; fee is still capped to maxFeeXRP + } + + const txJSON = { + Flags: 2147483648, + TransactionType: 'Payment', + Account: 'r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59', + Destination: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', + Amount: { + value: '0.01', + currency: 'USD', + issuer: 'rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM' + }, + SendMax: { + value: '0.01', + currency: 'USD', + issuer: 'rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM' + }, + LastLedgerSequence: 8820051 + } + + const expectedResponse = { + txJSON: + '{"Flags":2147483648,"TransactionType":"Payment","Account":"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59","Destination":"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo","Amount":{"value":"0.01","currency":"USD","issuer":"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM"},"SendMax":{"value":"0.01","currency":"USD","issuer":"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM"},"LastLedgerSequence":8820051,"Fee":"3000000","Sequence":23}', + instructions: { + fee: '3', + sequence: 23, + maxLedgerVersion: 8820051 + } + } + + const response = await api.prepareTransaction(txJSON, localInstructions) + assertResultMatch(response, expectedResponse, 'prepare') + }, + + // prepareTransaction - Payment + 'fee is capped to maxFee': async (api, address) => { + api._feeCushion = 1000000 + api._maxFeeXRP = '5' + const localInstructions = { + maxFee: '4' // maxFeeXRP does not matter if maxFee is lower than maxFeeXRP + } + + const txJSON = { + Flags: 2147483648, + TransactionType: 'Payment', + Account: 'r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59', + Destination: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', + Amount: { + value: '0.01', + currency: 'USD', + issuer: 'rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM' + }, + SendMax: { + value: '0.01', + currency: 'USD', + issuer: 'rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM' + }, + LastLedgerSequence: 8820051 + } + + const expectedResponse = { + txJSON: + '{"Flags":2147483648,"TransactionType":"Payment","Account":"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59","Destination":"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo","Amount":{"value":"0.01","currency":"USD","issuer":"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM"},"SendMax":{"value":"0.01","currency":"USD","issuer":"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM"},"LastLedgerSequence":8820051,"Fee":"4000000","Sequence":23}', + instructions: { + fee: '4', + sequence: 23, + maxLedgerVersion: 8820051 + } + } + + const response = await api.prepareTransaction(txJSON, localInstructions) + assertResultMatch(response, expectedResponse, 'prepare') + }, + + 'fee - calculated fee does not use more than 6 decimal places': async ( + api, + address + ) => { + api.connection._send( + JSON.stringify({ + command: 'config', + data: { loadFactor: 5407.96875 } + }) + ) + + const expectedResponse = { + txJSON: + '{"Flags":2147483648,"TransactionType":"Payment","Account":"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59","Destination":"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo","Amount":{"value":"0.01","currency":"USD","issuer":"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM"},"SendMax":{"value":"0.01","currency":"USD","issuer":"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM"},"LastLedgerSequence":8820051,"Fee":"64896","Sequence":23}', + instructions: { + fee: '0.064896', + sequence: 23, + maxLedgerVersion: 8820051 + } + } + + const response = await api.preparePayment( + address, + requests.preparePayment.normal, + instructionsWithMaxLedgerVersionOffset + ) + assertResultMatch(response, expectedResponse, 'prepare') + }, + + 'PaymentChannelCreate': async (api, address) => { + const localInstructions = { + ...instructionsWithMaxLedgerVersionOffset, + maxFee: '0.000012' + } + const response = await api.prepareTransaction( + { + Account: address, + TransactionType: 'PaymentChannelCreate', + Amount: '1000000', // 1 XRP in drops. Use a string-encoded integer. + Destination: 'rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW', + SettleDelay: 86400, + PublicKey: + '32D2471DB72B27E3310F355BB33E339BF26F8392D5A93D3BC0FC3B566612DA0F0A' + // If cancelAfter is used, you must use RippleTime. + // You can use `iso8601ToRippleTime()` to convert to RippleTime. + + // Other fields are available (but not used in this test), + // including `sourceTag` and `destinationTag`. + }, + localInstructions + ) + assertResultMatch( + response, + responses.preparePaymentChannelCreate.normal, + 'prepare' + ) + }, + + 'PaymentChannelCreate full': async (api, address) => { + const txJSON = { + Account: address, + TransactionType: 'PaymentChannelCreate', + Amount: api.xrpToDrops('1'), // or '1000000' + Destination: 'rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW', + SettleDelay: 86400, + // Ensure this is in upper case if it is not already + PublicKey: '32D2471DB72B27E3310F355BB33E339BF26F8392D5A93D3BC0FC3B566612DA0F0A'.toUpperCase(), + CancelAfter: api.iso8601ToRippleTime('2017-02-17T15:04:57Z'), + SourceTag: 11747, + DestinationTag: 23480 + } + + const response = await api.prepareTransaction(txJSON) + assertResultMatch( + response, + responses.preparePaymentChannelCreate.full, + 'prepare' + ) + }, + + 'PaymentChannelFund': async (api, address) => { + const localInstructions = { + ...instructionsWithMaxLedgerVersionOffset, + maxFee: '0.000012' + } + const txJSON = { + Account: address, + TransactionType: 'PaymentChannelFund', + Channel: + 'C1AE6DDDEEC05CF2978C0BAD6FE302948E9533691DC749DCDD3B9E5992CA6198', + Amount: api.xrpToDrops('1') // or '1000000' + } + const response = await api.prepareTransaction(txJSON, localInstructions) + assertResultMatch( + response, + responses.preparePaymentChannelFund.normal, + 'prepare' + ) + }, + + 'PaymentChannelFund full': async (api, address) => { + const txJSON = { + Account: address, + TransactionType: 'PaymentChannelFund', + Channel: + 'C1AE6DDDEEC05CF2978C0BAD6FE302948E9533691DC749DCDD3B9E5992CA6198', + Amount: api.xrpToDrops('1'), // or '1000000' + Expiration: api.iso8601ToRippleTime('2017-02-17T15:04:57Z') + } + + const response = await api.prepareTransaction(txJSON) + assertResultMatch( + response, + responses.preparePaymentChannelFund.full, + 'prepare' + ) + }, + + 'PaymentChannelClaim': async (api, address) => { + const localInstructions = { + ...instructionsWithMaxLedgerVersionOffset, + maxFee: '0.000012' + } + + const txJSON = { + Account: address, + TransactionType: 'PaymentChannelClaim', + Channel: + 'C1AE6DDDEEC05CF2978C0BAD6FE302948E9533691DC749DCDD3B9E5992CA6198', + Flags: 0 + } + + const response = await api.prepareTransaction(txJSON, localInstructions) + assertResultMatch( + response, + responses.preparePaymentChannelClaim.normal, + 'prepare' + ) + }, + + 'PaymentChannelClaim with renew': async (api, address) => { + const localInstructions = { + ...instructionsWithMaxLedgerVersionOffset, + maxFee: '0.000012' + } + + const txJSON = { + Account: address, + TransactionType: 'PaymentChannelClaim', + Channel: + 'C1AE6DDDEEC05CF2978C0BAD6FE302948E9533691DC749DCDD3B9E5992CA6198', + Balance: api.xrpToDrops('1'), // or '1000000' + Amount: api.xrpToDrops('1'), // or '1000000' + Signature: + '30440220718D264EF05CAED7C781FF6DE298DCAC68D002562C9BF3A07C1E721B420C0DAB02203A5A4779EF4D2CCC7BC3EF886676D803A9981B928D3B8ACA483B80ECA3CD7B9B', + PublicKey: + '32D2471DB72B27E3310F355BB33E339BF26F8392D5A93D3BC0FC3B566612DA0F0A', + Flags: 0 + } + txJSON.Flags |= api.txFlags.PaymentChannelClaim.Renew + + const response = await api.prepareTransaction(txJSON, localInstructions) + assertResultMatch( + response, + responses.preparePaymentChannelClaim.renew, + 'prepare' + ) + }, + + 'PaymentChannelClaim with close': async (api, address) => { + const localInstructions = { + ...instructionsWithMaxLedgerVersionOffset, + maxFee: '0.000012' + } + + const txJSON = { + Account: address, + TransactionType: 'PaymentChannelClaim', + Channel: + 'C1AE6DDDEEC05CF2978C0BAD6FE302948E9533691DC749DCDD3B9E5992CA6198', + Balance: api.xrpToDrops('1'), // or 1000000 + Amount: api.xrpToDrops('1'), // or 1000000 + Signature: + '30440220718D264EF05CAED7C781FF6DE298DCAC68D002562C9BF3A07C1E721B420C0DAB02203A5A4779EF4D2CCC7BC3EF886676D803A9981B928D3B8ACA483B80ECA3CD7B9B', + PublicKey: + '32D2471DB72B27E3310F355BB33E339BF26F8392D5A93D3BC0FC3B566612DA0F0A', + Flags: 0 + } + txJSON.Flags |= api.txFlags.PaymentChannelClaim.Close + + const response = await api.prepareTransaction(txJSON, localInstructions) + assertResultMatch( + response, + responses.preparePaymentChannelClaim.close, + 'prepare' + ) + } +} diff --git a/test/api/prepareTrustline/index.ts b/test/api/prepareTrustline/index.ts new file mode 100644 index 00000000..6d026f2e --- /dev/null +++ b/test/api/prepareTrustline/index.ts @@ -0,0 +1,52 @@ +import requests from '../../fixtures/requests' +import responses from '../../fixtures/responses' +import { assertRejects, assertResultMatch, TestSuite } from '../../utils' +const instructionsWithMaxLedgerVersionOffset = { maxLedgerVersionOffset: 100 } + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + simple: async (api, address) => { + const result = await api.prepareTrustline( + address, + requests.prepareTrustline.simple, + instructionsWithMaxLedgerVersionOffset + ) + assertResultMatch(result, responses.prepareTrustline.simple, 'prepare') + }, + + frozen: async (api, address) => { + const result = await api.prepareTrustline( + address, + requests.prepareTrustline.frozen + ) + assertResultMatch(result, responses.prepareTrustline.frozen, 'prepare') + }, + + complex: async (api, address) => { + const result = await api.prepareTrustline( + address, + requests.prepareTrustline.complex, + instructionsWithMaxLedgerVersionOffset + ) + assertResultMatch(result, responses.prepareTrustline.complex, 'prepare') + }, + + invalid: async (api, address) => { + const trustline = Object.assign({}, requests.prepareTrustline.complex) + delete trustline.limit // Make invalid + + await assertRejects( + api.prepareTrustline( + address, + trustline, + instructionsWithMaxLedgerVersionOffset + ), + api.errors.ValidationError, + 'instance.trustline requires property "limit"' + ) + } +} diff --git a/test/api/request/index.ts b/test/api/request/index.ts new file mode 100644 index 00000000..bb989b0c --- /dev/null +++ b/test/api/request/index.ts @@ -0,0 +1,35 @@ +import responses from '../../fixtures/responses' +import { assertResultMatch, TestSuite } from '../../utils' + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + 'request account_objects': async (api, address) => { + const result = await api.request('account_objects', { + account: address + }) + + assertResultMatch( + result, + responses.getAccountObjects, + 'AccountObjectsResponse' + ) + }, + + 'request account_objects - invalid options': async (api, address) => { + // Intentionally no local validation of these options + const result = await api.request('account_objects', { + account: address, + invalid: 'options' + }) + + assertResultMatch( + result, + responses.getAccountObjects, + 'AccountObjectsResponse' + ) + } +} diff --git a/test/api/requestNextPage/index.ts b/test/api/requestNextPage/index.ts new file mode 100644 index 00000000..5559c28a --- /dev/null +++ b/test/api/requestNextPage/index.ts @@ -0,0 +1,38 @@ +import assert from 'assert-diff' +import { LedgerData } from 'ripple-api/common/types/objects' +import { assertRejects, TestSuite } from '../../utils' + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + 'requests the next page': async (api, address) => { + const response = await api.request('ledger_data') + const responseNextPage = await api.requestNextPage( + 'ledger_data', + {}, + response + ) + assert.equal( + responseNextPage.state[0].index, + '000B714B790C3C79FEE00D17C4DEB436B375466F29679447BA64F265FD63D731' + ) + }, + + 'rejects when there are no more pages': async (api, address) => { + const response = await api.request('ledger_data') + const responseNextPage = await api.requestNextPage( + 'ledger_data', + {}, + response + ) + assert(!api.hasNextPage(responseNextPage)) + await assertRejects( + api.requestNextPage('ledger_data', {}, responseNextPage), + Error, + 'response does not have a next page' + ) + } +} diff --git a/test/api/sign/index.ts b/test/api/sign/index.ts new file mode 100644 index 00000000..9cbd69bf --- /dev/null +++ b/test/api/sign/index.ts @@ -0,0 +1,312 @@ +import assert from 'assert-diff' +import { RippleAPI } from 'ripple-api' +import binary from 'ripple-binary-codec' +import requests from '../../fixtures/requests' +import responses from '../../fixtures/responses' +import { TestSuite } from '../../utils' +const { schemaValidator } = RippleAPI._PRIVATE +const { sign: REQUEST_FIXTURES } = requests +const { sign: RESPONSE_FIXTURES } = responses + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + 'sign': async (api, address) => { + const secret = 'shsWGZcmZz6YsWWmcnpfr6fLTdtFV' + const result = api.sign(REQUEST_FIXTURES.normal.txJSON, secret) + assert.deepEqual(result, RESPONSE_FIXTURES.normal) + schemaValidator.schemaValidate('sign', result) + }, + + 'already signed': async (api, address) => { + const secret = 'shsWGZcmZz6YsWWmcnpfr6fLTdtFV' + const result = api.sign(REQUEST_FIXTURES.normal.txJSON, secret) + assert.throws(() => { + const tx = JSON.stringify(binary.decode(result.signedTransaction)) + api.sign(tx, secret) + }, /txJSON must not contain "TxnSignature" or "Signers" properties/) + }, + + 'EscrowExecution': async (api, address) => { + const secret = 'snoPBrXtMeMyMHUVTgbuqAfg1SUTb' + const result = api.sign(REQUEST_FIXTURES.escrow.txJSON, secret) + assert.deepEqual(result, RESPONSE_FIXTURES.escrow) + schemaValidator.schemaValidate('sign', result) + }, + + 'signAs': async (api, address) => { + const txJSON = REQUEST_FIXTURES.signAs + const secret = 'snoPBrXtMeMyMHUVTgbuqAfg1SUTb' + const signature = api.sign(JSON.stringify(txJSON), secret, { + signAs: 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh' + }) + assert.deepEqual(signature, RESPONSE_FIXTURES.signAs) + }, + + 'withKeypair': async (api, address) => { + const keypair = { + privateKey: + '00ACCD3309DB14D1A4FC9B1DAE608031F4408C85C73EE05E035B7DC8B25840107A', + publicKey: + '02F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D8' + } + const result = api.sign(REQUEST_FIXTURES.normal.txJSON, keypair) + assert.deepEqual(result, RESPONSE_FIXTURES.normal) + schemaValidator.schemaValidate('sign', result) + }, + + 'withKeypair already signed': async (api, address) => { + const keypair = { + privateKey: + '00ACCD3309DB14D1A4FC9B1DAE608031F4408C85C73EE05E035B7DC8B25840107A', + publicKey: + '02F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D8' + } + const result = api.sign(REQUEST_FIXTURES.normal.txJSON, keypair) + assert.throws(() => { + const tx = JSON.stringify(binary.decode(result.signedTransaction)) + api.sign(tx, keypair) + }, /txJSON must not contain "TxnSignature" or "Signers" properties/) + }, + + 'withKeypair EscrowExecution': async (api, address) => { + const keypair = { + privateKey: + '001ACAAEDECE405B2A958212629E16F2EB46B153EEE94CDD350FDEFF52795525B7', + publicKey: + '0330E7FC9D56BB25D6893BA3F317AE5BCF33B3291BD63DB32654A313222F7FD020' + } + const result = api.sign(REQUEST_FIXTURES.escrow.txJSON, keypair) + assert.deepEqual(result, RESPONSE_FIXTURES.escrow) + schemaValidator.schemaValidate('sign', result) + }, + + 'withKeypair signAs': async (api, address) => { + const txJSON = REQUEST_FIXTURES.signAs + const keypair = { + privateKey: + '001ACAAEDECE405B2A958212629E16F2EB46B153EEE94CDD350FDEFF52795525B7', + publicKey: + '0330E7FC9D56BB25D6893BA3F317AE5BCF33B3291BD63DB32654A313222F7FD020' + } + const signature = api.sign(JSON.stringify(txJSON), keypair, { + signAs: 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh' + }) + assert.deepEqual(signature, RESPONSE_FIXTURES.signAs) + }, + + 'succeeds - prepared payment': async (api, address) => { + const payment = await api.preparePayment(address, { + source: { + address: address, + maxAmount: { + value: '1', + currency: 'drops' + } + }, + destination: { + address: 'rQ3PTWGLCbPz8ZCicV5tCX3xuymojTng5r', + amount: { + value: '1', + currency: 'drops' + } + } + }) + const secret = 'shsWGZcmZz6YsWWmcnpfr6fLTdtFV' + const result = api.sign(payment.txJSON, secret) + const expectedResult = { + signedTransaction: + '12000022800000002400000017201B008694F261400000000000000168400000000000000C732102F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D874473045022100A9C91D4CFAE45686146EE0B56D4C53A2E7C2D672FB834D43E0BE2D2E9106519A022075DDA2F92DE552B0C45D83D4E6D35889B3FBF51BFBBD9B25EBF70DE3C96D0D6681145E7B112523F68D2F5E879DB4EAC51C6698A693048314FDB08D07AAA0EB711793A3027304D688E10C3648', + id: '88D6B913C66279EA31ADC25C5806C48B2D4E5680261666790A736E1961217700' + } + assert.deepEqual(result, expectedResult) + schemaValidator.schemaValidate('sign', result) + }, + + 'succeeds - no flags': async (api, address) => { + const txJSON = + '{"TransactionType":"Payment","Account":"r45Rev1EXGxy2hAUmJPCne97KUE7qyrD3j","Destination":"rQ3PTWGLCbPz8ZCicV5tCX3xuymojTng5r","Amount":"20000000","Sequence":1,"Fee":"12"}' + const secret = 'shotKgaEotpcYsshSE39vmSnBDRim' + const result = api.sign(txJSON, secret) + const expectedResult = { + signedTransaction: + '1200002400000001614000000001312D0068400000000000000C7321022B05847086686F9D0499B13136B94AD4323EE1B67D4C429ECC987AB35ACFA34574473045022100C104B7B97C31FACA4597E7D6FCF13BD85BD11375963A62A0AC45B0061236E39802207784F157F6A98DFC85B051CDDF61CC3084C4F5750B82674801C8E9950280D1998114EE3046A5DDF8422C40DDB93F1D522BB4FE6419158314FDB08D07AAA0EB711793A3027304D688E10C3648', + id: '0596925967F541BF332FF6756645B2576A9858414B5B363DC3D34915BE8A70D6' + } + const decoded = binary.decode(result.signedTransaction) + assert( + decoded.Flags === undefined, + `Flags = ${decoded.Flags}, should be undefined` + ) + assert.deepEqual(result, expectedResult) + schemaValidator.schemaValidate('sign', result) + }, + + 'throws when encoded tx does not match decoded tx - prepared payment': async ( + api, + address + ) => { + const payment = await api.preparePayment(address, { + source: { + address: address, + maxAmount: { + value: '1.1234567', + currency: 'drops' + } + }, + destination: { + address: 'rQ3PTWGLCbPz8ZCicV5tCX3xuymojTng5r', + amount: { + value: '1.1234567', + currency: 'drops' + } + } + }) + const secret = 'shsWGZcmZz6YsWWmcnpfr6fLTdtFV' + assert.throws(() => { + api.sign(payment.txJSON, secret) + }, /^Error: 1\.1234567 is an illegal amount/) + }, + + 'throws when encoded tx does not match decoded tx - prepared order': async ( + api, + address + ) => { + const order = { + direction: 'sell', + quantity: { + currency: 'USD', + counterparty: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B', + value: '3.140000' + }, + totalPrice: { + currency: 'XRP', + value: '31415' + } + } + const prepared = await api.prepareOrder(address, order, { sequence: 123 }) + const secret = 'shsWGZcmZz6YsWWmcnpfr6fLTdtFV' + try { + api.sign(prepared.txJSON, secret) + return Promise.reject(new Error('api.sign should have thrown')) + } catch (error) { + assert.equal(error.name, 'ValidationError') + assert.equal( + error.message, + 'Serialized transaction does not match original txJSON. See `error.data`' + ) + assert.deepEqual(error.data.diff, { + TakerGets: { + value: '3.14' + } + }) + } + }, + + 'throws when encoded tx does not match decoded tx - AccountSet': async ( + api, + address + ) => { + const secret = 'shsWGZcmZz6YsWWmcnpfr6fLTdtFV' + const request = { + // TODO: This fails when address is X-Address + txJSON: `{"Flags":2147483648,"TransactionType":"AccountSet","Account":"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59","Domain":"726970706C652E636F6D","LastLedgerSequence":8820051,"Fee":"1.2","Sequence":23,"SigningPubKey":"02F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D8"}`, + instructions: { + fee: '0.0000012', + sequence: 23, + maxLedgerVersion: 8820051 + } + } + + assert.throws(() => { + api.sign(request.txJSON, secret) + }, /Error: 1\.2 is an illegal amount/) + }, + + 'throws when encoded tx does not match decoded tx - higher fee': async ( + api, + address + ) => { + const secret = 'shsWGZcmZz6YsWWmcnpfr6fLTdtFV' + const request = { + // TODO: This fails when address is X-Address + txJSON: `{"Flags":2147483648,"TransactionType":"AccountSet","Account":"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59","Domain":"726970706C652E636F6D","LastLedgerSequence":8820051,"Fee":"1123456.7","Sequence":23,"SigningPubKey":"02F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D8"}`, + instructions: { + fee: '1.1234567', + sequence: 23, + maxLedgerVersion: 8820051 + } + } + + assert.throws(() => { + api.sign(request.txJSON, secret) + }, /Error: 1123456\.7 is an illegal amount/) + }, + + 'throws when Fee exceeds maxFeeXRP (in drops)': async (api, address) => { + const secret = 'shsWGZcmZz6YsWWmcnpfr6fLTdtFV' + const request = { + txJSON: `{"Flags":2147483648,"TransactionType":"AccountSet","Account":"${address}","Domain":"726970706C652E636F6D","LastLedgerSequence":8820051,"Fee":"2010000","Sequence":23,"SigningPubKey":"02F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D8"}`, + instructions: { + fee: '2.01', + sequence: 23, + maxLedgerVersion: 8820051 + } + } + + assert.throws(() => { + api.sign(request.txJSON, secret) + }, /Fee" should not exceed "2000000"\. To use a higher fee, set `maxFeeXRP` in the RippleAPI constructor\./) + }, + + 'throws when Fee exceeds maxFeeXRP (in drops) - custom maxFeeXRP': async ( + api, + address + ) => { + api._maxFeeXRP = '1.9' + const secret = 'shsWGZcmZz6YsWWmcnpfr6fLTdtFV' + const request = { + txJSON: `{"Flags":2147483648,"TransactionType":"AccountSet","Account":"${address}","Domain":"726970706C652E636F6D","LastLedgerSequence":8820051,"Fee":"2010000","Sequence":23,"SigningPubKey":"02F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D8"}`, + instructions: { + fee: '2.01', + sequence: 23, + maxLedgerVersion: 8820051 + } + } + + assert.throws(() => { + api.sign(request.txJSON, secret) + }, /Fee" should not exceed "1900000"\. To use a higher fee, set `maxFeeXRP` in the RippleAPI constructor\./) + }, + + 'permits fee exceeding 2000000 drops when maxFeeXRP is higher than 2 XRP': async ( + api, + address + ) => { + api._maxFeeXRP = '2.1' + const secret = 'shsWGZcmZz6YsWWmcnpfr6fLTdtFV' + const request = { + // TODO: This fails when address is X-Address + txJSON: `{"Flags":2147483648,"TransactionType":"AccountSet","Account":"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59","Domain":"726970706C652E636F6D","LastLedgerSequence":8820051,"Fee":"2010000","Sequence":23,"SigningPubKey":"02F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D8"}`, + instructions: { + fee: '2.01', + sequence: 23, + maxLedgerVersion: 8820051 + } + } + + const result = api.sign(request.txJSON, secret) + + const expectedResponse = { + signedTransaction: + '12000322800000002400000017201B008695536840000000001EAB90732102F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D8744630440220384FBB48EEE7B0E58BD89294A609F9407C51FBE8FA08A4B305B22E9A7489D66602200152315EFE752DA381E74493419871550D206AC6503841DA5F8C30E35D9E3892770A726970706C652E636F6D81145E7B112523F68D2F5E879DB4EAC51C6698A69304', + id: 'A1586D6AF7B0821E7075E12A0132D9EB50BC1874A0749441201497F7561795FB' + } + + assert.deepEqual(result, expectedResponse) + schemaValidator.schemaValidate('sign', result) + } +} diff --git a/test/api/signPaymentChannelClaim/index.ts b/test/api/signPaymentChannelClaim/index.ts new file mode 100644 index 00000000..defe0ba9 --- /dev/null +++ b/test/api/signPaymentChannelClaim/index.ts @@ -0,0 +1,25 @@ +import { TestSuite, assertResultMatch } from '../../utils' +import requests from '../../fixtures/requests' +import responses from '../../fixtures/responses' + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + signPaymentChannelClaim: async (api, address) => { + const privateKey = + 'ACCD3309DB14D1A4FC9B1DAE608031F4408C85C73EE05E035B7DC8B25840107A' + const result = api.signPaymentChannelClaim( + requests.signPaymentChannelClaim.channel, + requests.signPaymentChannelClaim.amount, + privateKey + ) + assertResultMatch( + result, + responses.signPaymentChannelClaim, + 'signPaymentChannelClaim' + ) + } +} diff --git a/test/api/submit/index.ts b/test/api/submit/index.ts new file mode 100644 index 00000000..0e27c848 --- /dev/null +++ b/test/api/submit/index.ts @@ -0,0 +1,19 @@ +import responses from '../../fixtures/responses' +import { assertRejects, assertResultMatch, TestSuite } from '../../utils' + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + 'submit': async (api, address) => { + const result = await api.submit(responses.sign.normal.signedTransaction) + assertResultMatch(result, responses.submit, 'submit') + }, + + 'submit - failure': async (api, address) => { + await assertRejects(api.submit('BAD'), api.errors.RippledError) + // assert.strictEqual(error.data.resultCode, 'temBAD_FEE') + } +} diff --git a/test/api/verifyPaymentChannelClaim/index.ts b/test/api/verifyPaymentChannelClaim/index.ts new file mode 100644 index 00000000..d5f34dd7 --- /dev/null +++ b/test/api/verifyPaymentChannelClaim/index.ts @@ -0,0 +1,34 @@ +import requests from '../../fixtures/requests' +import responses from '../../fixtures/responses' +import { assertResultMatch, TestSuite } from '../../utils' + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + 'verifyPaymentChannelClaim': async (api, address) => { + const publicKey = + '02F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D8' + const result = api.verifyPaymentChannelClaim( + requests.signPaymentChannelClaim.channel, + requests.signPaymentChannelClaim.amount, + responses.signPaymentChannelClaim, + publicKey + ) + assertResultMatch(result, true, 'verifyPaymentChannelClaim') + }, + + 'verifyPaymentChannelClaim - invalid': async (api, address) => { + const publicKey = + '03A6523FE4281DA48A6FD77FAF3CB77F5C7001ABA0B32BCEDE0369AC009758D7D9' + const result = api.verifyPaymentChannelClaim( + requests.signPaymentChannelClaim.channel, + requests.signPaymentChannelClaim.amount, + responses.signPaymentChannelClaim, + publicKey + ) + assertResultMatch(result, false, 'verifyPaymentChannelClaim') + } +} diff --git a/test/ripple-api-test-private.ts b/test/ripple-api-test-private.ts new file mode 100644 index 00000000..93924edc --- /dev/null +++ b/test/ripple-api-test-private.ts @@ -0,0 +1,184 @@ +import assert from 'assert-diff' +import _ from 'lodash' +import { RippleAPI } from 'ripple-api' +import { RecursiveData } from 'ripple-api/ledger/utils' +import { assertRejects, assertResultMatch } from './utils' +import addresses from './fixtures/addresses.json' +import responses from './fixtures/responses' +import ledgerClosed from './fixtures/rippled/ledger-close-newer.json' +import setupAPI from './setup-api' + +const { validate, schemaValidator, ledgerUtils } = RippleAPI._PRIVATE +const address = addresses.ACCOUNT +assert.options.strict = true + +// how long before each test case times out +const TIMEOUT = 20000 + +describe('RippleAPI', function() { + this.timeout(TIMEOUT) + beforeEach(setupAPI.setup) + afterEach(setupAPI.teardown) + + it('RippleAPI - implicit server port', function() { + new RippleAPI({ server: 'wss://s1.ripple.com' }) + }) + + it('RippleAPI invalid options', function() { + // @ts-ignore - This is intentionally invalid + assert.throws(() => new RippleAPI({ invalid: true })) + }) + + it('RippleAPI valid options', function() { + const api = new RippleAPI({ server: 'wss://s:1' }) + const privateConnectionUrl = (api.connection as any)._url + assert.deepEqual(privateConnectionUrl, 'wss://s:1') + }) + + it('RippleAPI invalid server uri', function() { + assert.throws(() => new RippleAPI({ server: 'wss//s:1' })) + }) + + xit('RippleAPI connect() times out after 2 seconds', function() { + // TODO: Use a timer mock like https://jestjs.io/docs/en/timer-mocks + // to test that connect() times out after 2 seconds. + }) + + it('ledger closed event', function(done) { + this.api.on('ledger', message => { + assertResultMatch(message, responses.ledgerEvent, 'ledgerEvent') + done() + }) + this.api.connection._ws.emit('message', JSON.stringify(ledgerClosed)) + }) + + describe('[private] schema-validator', function() { + it('valid', function() { + assert.doesNotThrow(function() { + schemaValidator.schemaValidate( + 'hash256', + '0F7ED9F40742D8A513AE86029462B7A6768325583DF8EE21B7EC663019DD6A0F' + ) + }) + }) + + it('invalid', function() { + assert.throws(function() { + schemaValidator.schemaValidate('hash256', 'invalid') + }, this.api.errors.ValidationError) + }) + + it('invalid - empty value', function() { + assert.throws(function() { + schemaValidator.schemaValidate('hash256', '') + }, this.api.errors.ValidationError) + }) + + it('schema not found error', function() { + assert.throws(function() { + schemaValidator.schemaValidate('unexisting', 'anything') + }, /no schema/) + }) + }) + + describe('[private] validator', function() { + it('validateLedgerRange', function() { + const options = { + minLedgerVersion: 20000, + maxLedgerVersion: 10000 + } + const thunk = _.partial(validate.getTransactions, { address, options }) + assert.throws(thunk, this.api.errors.ValidationError) + assert.throws( + thunk, + /minLedgerVersion must not be greater than maxLedgerVersion/ + ) + }) + + it('secret', function() { + function validateSecret(secret) { + validate.sign({ txJSON: '', secret }) + } + assert.doesNotThrow( + _.partial(validateSecret, 'shzjfakiK79YQdMjy4h8cGGfQSV6u') + ) + assert.throws( + _.partial(validateSecret, 'shzjfakiK79YQdMjy4h8cGGfQSV6v'), + this.api.errors.ValidationError + ) + assert.throws( + _.partial(validateSecret, 1), + this.api.errors.ValidationError + ) + assert.throws( + _.partial(validateSecret, ''), + this.api.errors.ValidationError + ) + assert.throws( + _.partial(validateSecret, 's!!!'), + this.api.errors.ValidationError + ) + assert.throws( + _.partial(validateSecret, 'passphrase'), + this.api.errors.ValidationError + ) + // 32 0s is a valid hex repr of seed bytes + const hex = new Array(33).join('0') + assert.throws( + _.partial(validateSecret, hex), + this.api.errors.ValidationError + ) + }) + }) + + it('common utils - toRippledAmount', async () => { + const amount = { issuer: 'is', currency: 'c', value: 'v' } + assert.deepEqual(ledgerUtils.common.toRippledAmount(amount), { + issuer: 'is', + currency: 'c', + value: 'v' + }) + }) + + it('ledger utils - renameCounterpartyToIssuerInOrder', async () => { + const order = { + taker_gets: { counterparty: '1', currency: 'XRP' }, + taker_pays: { counterparty: '1', currency: 'XRP' } + } + const expected = { + taker_gets: { issuer: '1', currency: 'XRP' }, + taker_pays: { issuer: '1', currency: 'XRP' } + } + assert.deepEqual( + ledgerUtils.renameCounterpartyToIssuerInOrder(order), + expected + ) + }) + + it('ledger utils - compareTransactions', async () => { + // @ts-ignore + assert.strictEqual(ledgerUtils.compareTransactions({}, {}), 0) + let first: any = { outcome: { ledgerVersion: 1, indexInLedger: 100 } } + let second: any = { outcome: { ledgerVersion: 1, indexInLedger: 200 } } + assert.strictEqual(ledgerUtils.compareTransactions(first, second), -1) + first = { outcome: { ledgerVersion: 1, indexInLedger: 100 } } + second = { outcome: { ledgerVersion: 1, indexInLedger: 100 } } + assert.strictEqual(ledgerUtils.compareTransactions(first, second), 0) + first = { outcome: { ledgerVersion: 1, indexInLedger: 200 } } + second = { outcome: { ledgerVersion: 1, indexInLedger: 100 } } + assert.strictEqual(ledgerUtils.compareTransactions(first, second), 1) + }) + + it('ledger utils - getRecursive', async () => { + function getter(marker) { + return new Promise((resolve, reject) => { + if (marker !== undefined) { + reject(new Error()) + return + } + resolve({ marker: 'A', results: [1] }) + }) + } + await assertRejects(ledgerUtils.getRecursive(getter, 10), Error) + }) +})