diff --git a/HISTORY.md b/HISTORY.md index 7ee698ab..c4e792ca 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -2,6 +2,18 @@ ## 1.0.0 (UNRELEASED) +### Breaking Changes + ++ Amounts in drops (recommended) and XRP are checked for validity. Some + methods may now throw a `BigNumber Error` or `ValidationError` if the amount + is invalid. This may include methods that previously did not throw. ++ Note that 1 drop is equivalent to 0.000001 XRP and 1 XRP is equivalent to 1,000,000 drops. + +### Other Changes + ++ Allow specifying amounts in drops for consistency with the `rippled` + APIs. ++ Export `xrpToDrops()` and `dropsToXrp()` functions. + Potentially breaking change: Improve errors. For example, `RippledError` now includes the full response from the `rippled` server ([#687](https://github.com/ripple/ripple-lib/issues/687)). `NotConnectedError` may be thrown with a different message than before. diff --git a/docs/index.md b/docs/index.md index e653167f..ca95edfd 100644 --- a/docs/index.md +++ b/docs/index.md @@ -221,14 +221,13 @@ Currencies are represented as either 3-character currency codes or 40-character ## Value A *value* is a quantity of a currency represented as a decimal string. Be careful: JavaScript's native number format does not have sufficient precision to represent all values. XRP has different precision from other currencies. -**XRP** has 6 significant digits past the decimal point. In other words, XRP cannot be divided into positive values smaller than `0.000001` (1e-6). XRP has a maximum value of `100000000000` (1e11). +**XRP** has 6 significant digits past the decimal point. In other words, XRP cannot be divided into positive values smaller than `0.000001` (1e-6). This smallest unit is called a "drop". XRP has a maximum value of `100000000000` (1e11). Some RippleAPI methods accept XRP in order to maintain compatibility with older versions of the API. For consistency with the `rippled` APIs, we recommend formally specifying XRP values in *drops* in all API requests, and converting them to XRP for display. This is similar to Bitcoin's *satoshis* and Ethereum's *wei*. 1 XRP = 1,000,000 drops. **Non-XRP values** have 16 decimal digits of precision, with a maximum value of `9999999999999999e80`. The smallest positive non-XRP value is `1e-81`. - ## Amount -Example amount: +Example 100.00 USD amount: ```json { @@ -238,15 +237,16 @@ Example amount: } ``` -Example XRP amount: +Example 3.0 XRP amount, in drops: ```json { - "currency": "XRP", - "value": "2000" + "currency": "drops", + "value": "3000000" } ``` +(Requires `ripple-lib` version 1.0.0 or higher.) -An *amount* is data structure representing a currency, a quantity of that currency, and the counterparty on the trustline that holds the value. For XRP, there is no counterparty. +An *amount* is an object specifying a currency, a quantity of that currency, and the counterparty (issuer) on the trustline that holds the value. For XRP, there is no counterparty. A *lax amount* allows the counterparty to be omitted for all currencies. If the counterparty is not specified in an amount within a transaction specification, then any counterparty may be used for that amount. @@ -256,8 +256,8 @@ A *balance* is an amount than can have a negative value. Name | Type | Description ---- | ---- | ----------- -currency | [currency](#currency) | The three-character code or hexadecimal string used to denote currencies -counterparty | [address](#address) | *Optional* The Ripple address of the account that owes or is owed the funds (omitted if `currency` is "XRP") +currency | [currency](#currency) | The three-character code or hexadecimal string used to denote currencies, or "drops" for the smallest unit of XRP. +counterparty | [address](#address) | *Optional* The Ripple address of the account that owes or is owed the funds (omitted if `currency` is "XRP" or "drops") value | [value](#value) | *Optional* The quantity of the currency, denoted as a string to retain floating point precision # Transaction Overview @@ -323,7 +323,7 @@ maxLedgerVersionOffset | integer | *Optional* Offset from current validated ledg sequence | [sequence](#account-sequence-number) | *Optional* The initiating account's sequence number for this transaction. signersCount | integer | *Optional* Number of signers that will be signing this transaction. -We recommended that you specify a `maxLedgerVersion` so that you can quickly determine that a failed transaction will never succeeed in the future. It is impossible for a transaction to succeed after the XRP Ledger's consensus-validated ledger version exceeds the transaction's `maxLedgerVersion`. If you omit `maxLedgerVersion`, the "prepare*" method automatically supplies a `maxLedgerVersion` equal to the current ledger plus 3, which it includes in the return value from the "prepare*" method. +We recommend that you specify a `maxLedgerVersion` so that you can quickly determine that a failed transaction will never succeeed in the future. It is impossible for a transaction to succeed after the XRP Ledger's consensus-validated ledger version exceeds the transaction's `maxLedgerVersion`. If you omit `maxLedgerVersion`, the "prepare\*" method automatically supplies a `maxLedgerVersion` equal to the current ledger plus 3, which it includes in the return value from the "prepare\*" method. ## Transaction ID @@ -465,10 +465,10 @@ passive | boolean | *Optional* If enabled, the offer will not consume offers tha "value": "10.1" }, "totalPrice": { - "currency": "XRP", - "value": "2" + "currency": "drops", + "value": "2000000" }, - "passive": true, + "passive": false, "fillOrKill": true } ``` @@ -632,8 +632,8 @@ invoiceID | string | *Optional* 256-bit hash, as a 64-character hexadecimal stri { "destination": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW", "sendMax": { - "currency": "XRP", - "value": "1" + "currency": "drops", + "value": "1000000" } } ``` @@ -673,8 +673,8 @@ deliverMin | [laxAmount](#amount) | *Optional* Redeem the Check for at least thi ```json { "amount": { - "currency": "XRP", - "value": "1" + "currency": "drops", + "value": "1000000" }, "checkID": "838766BA2B995C00744175F69A1B11E32C3DBC40E64801A4056FCBD657F57334" } @@ -4271,6 +4271,8 @@ instructions | object | The instructions for how to execute the transaction afte ```javascript const address = 'r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59'; + +// Buy 10.10 USD (of the specified issuer) for 2.0 XRP (2000000 drops), fill or kill. const order = { "direction": "buy", "quantity": { @@ -4279,10 +4281,10 @@ const order = { "value": "10.1" }, "totalPrice": { - "currency": "XRP", - "value": "2" + "currency": "drops", + "value": "2000000" }, - "passive": true, + "passive": false, "fillOrKill": true }; return api.prepareOrder(address, order) @@ -4292,7 +4294,7 @@ return api.prepareOrder(address, order) ```json { - "txJSON": "{\"Flags\":2147811328,\"TransactionType\":\"OfferCreate\",\"Account\":\"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59\",\"TakerGets\":\"2000000\",\"TakerPays\":{\"value\":\"10.1\",\"currency\":\"USD\",\"issuer\":\"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM\"},\"LastLedgerSequence\":8819954,\"Fee\":\"12\",\"Sequence\":23}", + "txJSON": "{\"Flags\":2147745792,\"TransactionType\":\"OfferCreate\",\"Account\":\"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59\",\"TakerGets\":\"2000000\",\"TakerPays\":{\"value\":\"10.1\",\"currency\":\"USD\",\"issuer\":\"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM\"},\"LastLedgerSequence\":8819954,\"Fee\":\"12\",\"Sequence\":23}", "instructions": { "fee": "0.000012", "sequence": 23, @@ -4798,8 +4800,8 @@ const address = 'r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59'; const checkCreate = { "destination": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW", "sendMax": { - "currency": "XRP", - "value": "1" + "currency": "drops", + "value": "1000000" } }; return api.prepareCheckCreate(address, checkCreate).then(prepared => @@ -4911,8 +4913,8 @@ instructions | object | The instructions for how to execute the transaction afte const address = 'r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59'; const checkCash = { "amount": { - "currency": "XRP", - "value": "1" + "currency": "drops", + "value": "1000000" }, "checkID": "838766BA2B995C00744175F69A1B11E32C3DBC40E64801A4056FCBD657F57334" }; diff --git a/docs/src/basictypes.md.ejs b/docs/src/basictypes.md.ejs index 9d62de0d..9efae9a2 100644 --- a/docs/src/basictypes.md.ejs +++ b/docs/src/basictypes.md.ejs @@ -19,14 +19,13 @@ Currencies are represented as either 3-character currency codes or 40-character ## Value A *value* is a quantity of a currency represented as a decimal string. Be careful: JavaScript's native number format does not have sufficient precision to represent all values. XRP has different precision from other currencies. -**XRP** has 6 significant digits past the decimal point. In other words, XRP cannot be divided into positive values smaller than `0.000001` (1e-6). XRP has a maximum value of `100000000000` (1e11). +**XRP** has 6 significant digits past the decimal point. In other words, XRP cannot be divided into positive values smaller than `0.000001` (1e-6). This smallest unit is called a "drop". XRP has a maximum value of `100000000000` (1e11). Some RippleAPI methods accept XRP in order to maintain compatibility with older versions of the API. For consistency with the `rippled` APIs, we recommend formally specifying XRP values in *drops* in all API requests, and converting them to XRP for display. This is similar to Bitcoin's *satoshis* and Ethereum's *wei*. 1 XRP = 1,000,000 drops. **Non-XRP values** have 16 decimal digits of precision, with a maximum value of `9999999999999999e80`. The smallest positive non-XRP value is `1e-81`. - ## Amount -Example amount: +Example 100.00 USD amount: ```json { @@ -36,15 +35,16 @@ Example amount: } ``` -Example XRP amount: +Example 3.0 XRP amount, in drops: ```json { - "currency": "XRP", - "value": "2000" + "currency": "drops", + "value": "3000000" } ``` +(Requires `ripple-lib` version 1.0.0 or higher.) -An *amount* is data structure representing a currency, a quantity of that currency, and the counterparty on the trustline that holds the value. For XRP, there is no counterparty. +An *amount* is an object specifying a currency, a quantity of that currency, and the counterparty (issuer) on the trustline that holds the value. For XRP, there is no counterparty. A *lax amount* allows the counterparty to be omitted for all currencies. If the counterparty is not specified in an amount within a transaction specification, then any counterparty may be used for that amount. diff --git a/docs/src/prepareOrder.md.ejs b/docs/src/prepareOrder.md.ejs index 91c795c0..2d22d441 100644 --- a/docs/src/prepareOrder.md.ejs +++ b/docs/src/prepareOrder.md.ejs @@ -22,6 +22,8 @@ All "prepare*" methods have the same return type. ```javascript const address = 'r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59'; + +// Buy 10.10 USD (of the specified issuer) for 2.0 XRP (2000000 drops), fill or kill. const order = <%- importFile('test/fixtures/requests/prepare-order.json') %>; return api.prepareOrder(address, order) .then(prepared => {/* ... */}); diff --git a/docs/src/transactions.md.ejs b/docs/src/transactions.md.ejs index db491a7d..76fa4431 100644 --- a/docs/src/transactions.md.ejs +++ b/docs/src/transactions.md.ejs @@ -53,7 +53,7 @@ Transaction instructions indicate how to execute a transaction, complementary wi <%- renderSchema("objects/instructions.json") %> -We recommended that you specify a `maxLedgerVersion` so that you can quickly determine that a failed transaction will never succeeed in the future. It is impossible for a transaction to succeed after the XRP Ledger's consensus-validated ledger version exceeds the transaction's `maxLedgerVersion`. If you omit `maxLedgerVersion`, the "prepare*" method automatically supplies a `maxLedgerVersion` equal to the current ledger plus 3, which it includes in the return value from the "prepare*" method. +We recommend that you specify a `maxLedgerVersion` so that you can quickly determine that a failed transaction will never succeeed in the future. It is impossible for a transaction to succeed after the XRP Ledger's consensus-validated ledger version exceeds the transaction's `maxLedgerVersion`. If you omit `maxLedgerVersion`, the "prepare\*" method automatically supplies a `maxLedgerVersion` equal to the current ledger plus 3, which it includes in the return value from the "prepare\*" method. ## Transaction ID diff --git a/src/api.ts b/src/api.ts index cdd6b76e..9b0b2e29 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,5 +1,5 @@ import {EventEmitter} from 'events' -import {Connection, errors, validate} from './common' +import {Connection, errors, validate, xrpToDrops, dropsToXrp} from './common' import { connect, disconnect, @@ -300,6 +300,9 @@ class RippleAPI extends EventEmitter { signPaymentChannelClaim = signPaymentChannelClaim verifyPaymentChannelClaim = verifyPaymentChannelClaim errors = errors + + xrpToDrops = xrpToDrops + dropsToXrp = dropsToXrp } export { diff --git a/src/common/schemas/objects/amount-base.json b/src/common/schemas/objects/amount-base.json index 01a8a8a8..09568e03 100644 --- a/src/common/schemas/objects/amount-base.json +++ b/src/common/schemas/objects/amount-base.json @@ -9,11 +9,11 @@ "$ref": "value" }, "currency": { - "description": "The three-character code or hexadecimal string used to denote currencies", + "description": "The three-character code or hexadecimal string used to denote currencies, or \"drops\" for the smallest unit of XRP.", "$ref": "currency" }, "counterparty": { - "description": "The Ripple address of the account that owes or is owed the funds (omitted if `currency` is \"XRP\")", + "description": "The Ripple address of the account that owes or is owed the funds (omitted if `currency` is \"XRP\" or \"drops\")", "$ref": "address" } }, @@ -24,7 +24,7 @@ "properties": { "currency": { "not": { - "enum": ["XRP"] + "enum": ["XRP", "drops"] } } }, @@ -33,7 +33,7 @@ { "properties": { "currency": { - "enum": ["XRP"] + "enum": ["XRP", "drops"] } }, "not": { diff --git a/src/common/schemas/objects/currency.json b/src/common/schemas/objects/currency.json index 9d45057f..dcf9440e 100644 --- a/src/common/schemas/objects/currency.json +++ b/src/common/schemas/objects/currency.json @@ -4,5 +4,5 @@ "description": "The three-character code or hexadecimal string used to denote currencies", "type": "string", "link": "currency", - "pattern": "^([a-zA-Z0-9<>(){}[\\]|?!@#$%^&*]{3}|[A-F0-9]{40})$" + "pattern": "^([a-zA-Z0-9<>(){}[\\]|?!@#$%^&*]{3}|[A-F0-9]{40}|drops)$" } diff --git a/src/common/utils.ts b/src/common/utils.ts index 089d331a..3781ec86 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -1,8 +1,8 @@ import * as _ from 'lodash' import BigNumber from 'bignumber.js' -const {deriveKeypair} = require('ripple-keypairs') - +import {deriveKeypair} from 'ripple-keypairs' import {Amount, RippledAmount} from './types/objects' +import {ValidationError} from './errors' function isValidSecret(secret: string): boolean { try { @@ -13,18 +13,86 @@ function isValidSecret(secret: string): boolean { } } -function dropsToXrp(drops: string): string { - return (new BigNumber(drops)).dividedBy(1000000.0).toString() +function dropsToXrp(drops: string | BigNumber): string { + if (typeof drops === 'string') { + if (!drops.match(/^-?[0-9]*\.?[0-9]*$/)) { + throw new ValidationError(`dropsToXrp: invalid value '${drops}',` + + ` should be a number matching (^-?[0-9]*\.?[0-9]*$).`) + } else if (drops === '.') { + throw new ValidationError(`dropsToXrp: invalid value '${drops}',` + + ` should be a BigNumber or string-encoded number.`) + } + } + + // Converting to BigNumber and then back to string should remove any + // decimal point followed by zeros, e.g. '1.00'. + // Important: specify base 10 to avoid exponential notation, e.g. '1e-7'. + drops = (new BigNumber(drops)).toString(10) + + // drops are only whole units + if (drops.includes('.')) { + throw new ValidationError(`dropsToXrp: value '${drops}' has` + + ` too many decimal places.`) + } + + // This should never happen; the value has already been + // validated above. This just ensures BigNumber did not do + // something unexpected. + if (!drops.match(/^-?[0-9]+$/)) { + throw new ValidationError(`dropsToXrp: failed sanity check -` + + ` value '${drops}',` + + ` does not match (^-?[0-9]+$).`) + } + + return (new BigNumber(drops)).dividedBy(1000000.0).toString(10) } -function xrpToDrops(xrp: string): string { - return (new BigNumber(xrp)).times(1000000.0).floor().toString() +function xrpToDrops(xrp: string | BigNumber): string { + if (typeof xrp === 'string') { + if (!xrp.match(/^-?[0-9]*\.?[0-9]*$/)) { + throw new ValidationError(`xrpToDrops: invalid value '${xrp}',` + + ` should be a number matching (^-?[0-9]*\.?[0-9]*$).`) + } else if (xrp === '.') { + throw new ValidationError(`xrpToDrops: invalid value '${xrp}',` + + ` should be a BigNumber or string-encoded number.`) + } + } + + // Important: specify base 10 to avoid exponential notation, e.g. '1e-7'. + xrp = (new BigNumber(xrp)).toString(10) + + // This should never happen; the value has already been + // validated above. This just ensures BigNumber did not do + // something unexpected. + if (!xrp.match(/^-?[0-9.]+$/)) { + throw new ValidationError(`xrpToDrops: failed sanity check -` + + ` value '${xrp}',` + + ` does not match (^-?[0-9.]+$).`) + } + + const components = xrp.split('.') + if (components.length > 2) { + throw new ValidationError(`xrpToDrops: failed sanity check -` + + ` value '${xrp}' has` + + ` too many decimal points.`) + } + + const fraction = components[1] || '0' + if (fraction.length > 6) { + throw new ValidationError(`xrpToDrops: value '${xrp}' has` + + ` too many decimal places.`) + } + + return (new BigNumber(xrp)).times(1000000.0).floor().toString(10) } function toRippledAmount(amount: Amount): RippledAmount { if (amount.currency === 'XRP') { return xrpToDrops(amount.value) } + if (amount.currency === 'drops') { + return amount.value + } return { currency: amount.currency, issuer: amount.counterparty ? amount.counterparty : diff --git a/test/api-test.js b/test/api-test.js index d8d53531..28342b83 100644 --- a/test/api-test.js +++ b/test/api-test.js @@ -15,6 +15,7 @@ const utils = RippleAPI._PRIVATE.ledgerUtils; const ledgerClosed = require('./fixtures/rippled/ledger-close-newer'); const schemaValidator = RippleAPI._PRIVATE.schemaValidator; const binary = require('ripple-binary-codec'); +const BigNumber = require('bignumber.js') assert.options.strict = true; // how long before each test case times out @@ -51,6 +52,224 @@ describe('RippleAPI', function () { assert.strictEqual(error.inspect(), '[RippleError(mess, { data: 1 })]'); }); + describe('xrpToDrops', function () { + it('works with a typical amount', function () { + const drops = this.api.xrpToDrops('2') + assert.strictEqual(drops, '2000000', '2 XRP equals 2 million drops') + }) + + it('works with fractions', function () { + let drops = this.api.xrpToDrops('3.456789') + assert.strictEqual(drops, '3456789', '3.456789 XRP equals 3,456,789 drops') + + drops = this.api.xrpToDrops('3.400000') + assert.strictEqual(drops, '3400000', '3.400000 XRP equals 3,400,000 drops') + + drops = this.api.xrpToDrops('0.000001') + assert.strictEqual(drops, '1', '0.000001 XRP equals 1 drop') + + drops = this.api.xrpToDrops('0.0000010') + assert.strictEqual(drops, '1', '0.0000010 XRP equals 1 drop') + }) + + it('works with zero', function () { + let drops = this.api.xrpToDrops('0') + assert.strictEqual(drops, '0', '0 XRP equals 0 drops') + + // negative zero is equivalent to zero + drops = this.api.xrpToDrops('-0') + assert.strictEqual(drops, '0', '-0 XRP equals 0 drops') + + drops = this.api.xrpToDrops('0.000000') + assert.strictEqual(drops, '0', '0.000000 XRP equals 0 drops') + + drops = this.api.xrpToDrops('0.0000000') + assert.strictEqual(drops, '0', '0.0000000 XRP equals 0 drops') + }) + + it('works with a negative value', function () { + const drops = this.api.xrpToDrops('-2') + assert.strictEqual(drops, '-2000000', '-2 XRP equals -2 million drops') + }) + + it('works with a value ending with a decimal point', function () { + let drops = this.api.xrpToDrops('2.') + assert.strictEqual(drops, '2000000', '2. XRP equals 2000000 drops') + + drops = this.api.xrpToDrops('-2.') + assert.strictEqual(drops, '-2000000', '-2. XRP equals -2000000 drops') + }) + + it('works with BigNumber objects', function () { + let drops = this.api.xrpToDrops(new BigNumber(2)) + assert.strictEqual(drops, '2000000', '(BigNumber) 2 XRP equals 2 million drops') + + drops = this.api.xrpToDrops(new BigNumber(-2)) + assert.strictEqual(drops, '-2000000', '(BigNumber) -2 XRP equals -2 million drops') + }) + + it('works with a number', function() { + // This is not recommended. Use strings or BigNumber objects to avoid precision errors. + + let drops = this.api.xrpToDrops(2) + assert.strictEqual(drops, '2000000', '(number) 2 XRP equals 2 million drops') + + drops = this.api.xrpToDrops(-2) + assert.strictEqual(drops, '-2000000', '(number) -2 XRP equals -2 million drops') + }) + + it('throws with an amount with too many decimal places', function () { + assert.throws(() => { + this.api.xrpToDrops('1.1234567') + }, /has too many decimal places/) + + assert.throws(() => { + this.api.xrpToDrops('0.0000001') + }, /has too many decimal places/) + }) + + it('throws with an invalid value', function () { + assert.throws(() => { + this.api.xrpToDrops('FOO') + }, /invalid value/) + + assert.throws(() => { + this.api.xrpToDrops('1e-7') + }, /invalid value/) + + assert.throws(() => { + this.api.xrpToDrops('2,0') + }, /invalid value/) + + assert.throws(() => { + this.api.xrpToDrops('.') + }, /xrpToDrops\: 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.xrpToDrops('1.0.0') + }, /xrpToDrops:\ invalid\ value\ '1\.0\.0'\,\ should\ be\ a\ number\ matching\ \(\^\-\?\[0\-9\]\*\.\?\[0\-9\]\*\$\)\./) + + assert.throws(() => { + this.api.xrpToDrops('...') + }, /xrpToDrops:\ invalid\ value\ '\.\.\.'\,\ should\ be\ a\ number\ matching\ \(\^\-\?\[0\-9\]\*\.\?\[0\-9\]\*\$\)\./) + }) + }) + + 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'\,\ should\ be\ a\ number\ matching\ \(\^\-\?\[0\-9\]\*\.\?\[0\-9\]\*\$\)\./) + + assert.throws(() => { + this.api.dropsToXrp('...') + }, /dropsToXrp:\ invalid\ value\ '\.\.\.'\,\ should\ be\ a\ number\ matching\ \(\^\-\?\[0\-9\]\*\.\?\[0\-9\]\*\$\)\./) + }) + }) describe('pagination', function () { describe('hasNextPage', function () { diff --git a/test/fixtures/requests/prepare-check-cash-amount.json b/test/fixtures/requests/prepare-check-cash-amount.json index a02ab588..484c6da0 100644 --- a/test/fixtures/requests/prepare-check-cash-amount.json +++ b/test/fixtures/requests/prepare-check-cash-amount.json @@ -1,7 +1,7 @@ { "amount": { - "currency": "XRP", - "value": "1" + "currency": "drops", + "value": "1000000" }, "checkID": "838766BA2B995C00744175F69A1B11E32C3DBC40E64801A4056FCBD657F57334" } diff --git a/test/fixtures/requests/prepare-check-create.json b/test/fixtures/requests/prepare-check-create.json index 4edfd38b..ef39c5d7 100644 --- a/test/fixtures/requests/prepare-check-create.json +++ b/test/fixtures/requests/prepare-check-create.json @@ -1,7 +1,7 @@ { "destination": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW", "sendMax": { - "currency": "XRP", - "value": "1" + "currency": "drops", + "value": "1000000" } } diff --git a/test/fixtures/requests/prepare-order.json b/test/fixtures/requests/prepare-order.json index 510f68a6..7674bca7 100644 --- a/test/fixtures/requests/prepare-order.json +++ b/test/fixtures/requests/prepare-order.json @@ -6,9 +6,9 @@ "value": "10.1" }, "totalPrice": { - "currency": "XRP", - "value": "2" + "currency": "drops", + "value": "2000000" }, - "passive": true, + "passive": false, "fillOrKill": true } diff --git a/test/fixtures/responses/prepare-order.json b/test/fixtures/responses/prepare-order.json index 5bd790c1..bef731cd 100644 --- a/test/fixtures/responses/prepare-order.json +++ b/test/fixtures/responses/prepare-order.json @@ -1,5 +1,5 @@ { - "txJSON": "{\"Flags\":2147811328,\"TransactionType\":\"OfferCreate\",\"Account\":\"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59\",\"TakerGets\":\"2000000\",\"TakerPays\":{\"value\":\"10.1\",\"currency\":\"USD\",\"issuer\":\"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM\"},\"LastLedgerSequence\":8819954,\"Fee\":\"12\",\"Sequence\":23}", + "txJSON": "{\"Flags\":2147745792,\"TransactionType\":\"OfferCreate\",\"Account\":\"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59\",\"TakerGets\":\"2000000\",\"TakerPays\":{\"value\":\"10.1\",\"currency\":\"USD\",\"issuer\":\"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM\"},\"LastLedgerSequence\":8819954,\"Fee\":\"12\",\"Sequence\":23}", "instructions": { "fee": "0.000012", "sequence": 23,