Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/xrpl/HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Subscribe to [the **xrpl-announce** mailing list](https://groups.google.com/g/xr
### BREAKING CHANGES:
* `ED25519` is the default signing-algorithm used in the `Wallet.fromMnemonic` method. Users can explicitly specify `ecdsa-secp256k1` to retrieve the cryptographic material created using older versions of this package.
* `Client.getServerInfo()` and `Client.connect()` now throw if the `server_info` request fails, or if the response succeeds but does not include a `network_id`. Previously, these failures were swallowed and only logged via `console.error`, leaving `client.networkID` undefined and causing `autofill()` to omit the `NetworkID` field — producing transactions valid on the wrong network. Servers running rippled <1.11 (which do not return `network_id`) will now fail to connect; upgrade to rippled 1.11+ or set `client.networkID` manually after construction.
* `dropsToXrp()` now returns a base-10 decimal string instead of a JavaScript `number`, and `Client.getXrpBalance()` returns `Promise<string>` instead of `Promise<number>`. The previous implementation called `.toNumber()`, which silently dropped precision for amounts approaching the XRP supply (~10^17 drops): `xrpToDrops(dropsToXrp('9999999999999999'))` returned `'9999999999999998'`, losing one drop on the round-trip. Callers that need a JS number can wrap the result in `Number(...)`. ([#3316](https://github.com/XRPLF/xrpl.js/issues/3316))

### Added
* Add new fields to `ServerDefinitionsResponse`: `ACCOUNT_SET_FLAGS`, `LEDGER_ENTRY_FLAGS`, `LEDGER_ENTRY_FORMATS`, `TRANSACTION_FLAGS`, and `TRANSACTION_FORMATS`, reflecting new sections returned by `server_definitions` in rippled.
Expand Down
10 changes: 5 additions & 5 deletions packages/xrpl/src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -936,15 +936,15 @@ class Client extends EventEmitter<EventTypes> {
* @param [options] - Additional options for fetching the balance (optional).
* @param [options.ledger_hash] - The hash of the ledger to retrieve the balance from (optional).
* @param [options.ledger_index] - The index of the ledger to retrieve the balance from (optional).
* @returns A promise that resolves with the XRP balance as a number.
* @returns A promise that resolves with the XRP balance as a base-10 decimal string.
Comment thread
kuan121 marked this conversation as resolved.
Outdated
*/
public async getXrpBalance(
address: string,
options: {
ledger_hash?: string
ledger_index?: LedgerIndex
} = {},
): Promise<number> {
): Promise<string> {
const xrpRequest: AccountInfoRequest = {
command: 'account_info',
account: address,
Expand Down Expand Up @@ -1018,7 +1018,7 @@ class Client extends EventEmitter<EventTypes> {
const balances: Balance[] = []

// get XRP balance
let xrpPromise: Promise<number> = Promise.resolve(0)
let xrpPromise: Promise<string> = Promise.resolve('0')
if (!options.peer) {
xrpPromise = this.getXrpBalance(address, {
ledger_hash: options.ledger_hash,
Expand All @@ -1043,8 +1043,8 @@ class Client extends EventEmitter<EventTypes> {
const accountLinesBalance = linesResponses.flatMap((response) =>
formatBalances(response.result.lines),
)
if (xrpBalance !== 0) {
balances.push({ currency: 'XRP', value: xrpBalance.toString() })
if (xrpBalance !== '0') {
balances.push({ currency: 'XRP', value: xrpBalance })
}
balances.push(...accountLinesBalance)
},
Expand Down
2 changes: 1 addition & 1 deletion packages/xrpl/src/utils/getBalanceChanges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ function getXRPQuantity(
account: (node.FinalFields?.Account ?? node.NewFields?.Account) as string,
balance: {
currency: 'XRP',
value: dropsToXrp(value).toString(),
value: dropsToXrp(value),
},
}
}
Expand Down
19 changes: 16 additions & 3 deletions packages/xrpl/src/utils/xrpConversion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,19 @@ const SANITY_CHECK = /^-?[0-9.]+$/u
/**
* Convert Drops to XRP.
*
* Returns a base-10 decimal string (rather than a JavaScript `number`) so
* that the full precision of the drops value is preserved across the
* conversion. For amounts approaching the XRP supply (~10^17 drops), an
* IEEE-754 double cannot represent every drop exactly, which silently lost
* up to one drop on each `xrpToDrops(dropsToXrp(value))` round-trip.
* See xrpl.js issue #3316.
*
* @param dropsToConvert - Drops to convert to XRP. This can be a string, number, or BigNumber.
* @returns Amount in XRP.
* @returns Amount in XRP, as a base-10 decimal string.
* @throws When drops amount is invalid.
* @category Utilities
*/
export function dropsToXrp(dropsToConvert: BigNumber.Value): number {
export function dropsToXrp(dropsToConvert: BigNumber.Value): string {
/*
* Converting to BigNumber and then back to string should remove any
* decimal point followed by zeros, e.g. '1.00'.
Expand Down Expand Up @@ -50,7 +57,13 @@ export function dropsToXrp(dropsToConvert: BigNumber.Value): number {
)
}

return new BigNumber(drops).dividedBy(DROPS_PER_XRP).toNumber()
/*
* Use `.toString(BASE_TEN)` instead of `.toNumber()` so that the result
* preserves the full precision of the input. Drops are at most 6 decimal
* places of XRP, so the division terminates exactly within BigNumber's
* default precision and never produces exponential notation here.
*/
return new BigNumber(drops).dividedBy(DROPS_PER_XRP).toString(BASE_TEN)
}

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/xrpl/test/client/getXrpBalance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ describe('client.getXrpBalance', function () {
)
testContext.mockRippled!.addResponse('ledger', rippled.ledger.normal)
const result = await testContext.client.getXrpBalance(testcase.address)
assert.equal(result, 922.913243)
assert.strictEqual(result, '922.913243')
})
})
})
Expand Down
58 changes: 37 additions & 21 deletions packages/xrpl/test/utils/dropsToXrp.test.ts
Original file line number Diff line number Diff line change
@@ -1,94 +1,94 @@
import BigNumber from 'bignumber.js'
import { assert } from 'chai'

import { dropsToXrp } from '../../src/utils'
import { dropsToXrp, xrpToDrops } from '../../src/utils'

describe('dropsToXrp', function () {
it('works with a typical amount', function () {
const xrp = dropsToXrp('2000000')
assert.strictEqual(xrp, 2, '2 million drops equals 2 XRP')
assert.strictEqual(xrp, '2', '2 million drops equals 2 XRP')
})

it('works with fractions', function () {
let xrp = dropsToXrp('3456789')
assert.strictEqual(xrp, 3.456789, '3,456,789 drops equals 3.456789 XRP')
assert.strictEqual(xrp, '3.456789', '3,456,789 drops equals 3.456789 XRP')

xrp = dropsToXrp('3400000')
assert.strictEqual(xrp, 3.4, '3,400,000 drops equals 3.4 XRP')
assert.strictEqual(xrp, '3.4', '3,400,000 drops equals 3.4 XRP')

xrp = dropsToXrp('1')
assert.strictEqual(xrp, 0.000001, '1 drop equals 0.000001 XRP')
assert.strictEqual(xrp, '0.000001', '1 drop equals 0.000001 XRP')

xrp = dropsToXrp('1.0')
assert.strictEqual(xrp, 0.000001, '1.0 drops equals 0.000001 XRP')
assert.strictEqual(xrp, '0.000001', '1.0 drops equals 0.000001 XRP')

xrp = dropsToXrp('1.00')
assert.strictEqual(xrp, 0.000001, '1.00 drops equals 0.000001 XRP')
assert.strictEqual(xrp, '0.000001', '1.00 drops equals 0.000001 XRP')
})

it('works with zero', function () {
let xrp = dropsToXrp('0')
assert.strictEqual(xrp, 0, '0 drops equals 0 XRP')
assert.strictEqual(xrp, '0', '0 drops equals 0 XRP')

// negative zero is equivalent to zero
xrp = dropsToXrp('-0')
assert.strictEqual(xrp, 0, '-0 drops equals 0 XRP')
assert.strictEqual(xrp, '0', '-0 drops equals 0 XRP')

xrp = dropsToXrp('0.00')
assert.strictEqual(xrp, 0, '0.00 drops equals 0 XRP')
assert.strictEqual(xrp, '0', '0.00 drops equals 0 XRP')

xrp = dropsToXrp('000000000')
assert.strictEqual(xrp, 0, '000000000 drops equals 0 XRP')
assert.strictEqual(xrp, '0', '000000000 drops equals 0 XRP')
})

it('works with a negative value', function () {
const xrp = dropsToXrp('-2000000')
assert.strictEqual(xrp, -2, '-2 million drops equals -2 XRP')
assert.strictEqual(xrp, '-2', '-2 million drops equals -2 XRP')
})

it('works with a value ending with a decimal point', function () {
let xrp = dropsToXrp('2000000.')
assert.strictEqual(xrp, 2, '2000000. drops equals 2 XRP')
assert.strictEqual(xrp, '2', '2000000. drops equals 2 XRP')

xrp = dropsToXrp('-2000000.')
assert.strictEqual(xrp, -2, '-2000000. drops equals -2 XRP')
assert.strictEqual(xrp, '-2', '-2000000. drops equals -2 XRP')
})

it('works with BigNumber objects', function () {
let xrp = dropsToXrp(new BigNumber(2000000))
assert.strictEqual(xrp, 2, '(BigNumber) 2 million drops equals 2 XRP')
assert.strictEqual(xrp, '2', '(BigNumber) 2 million drops equals 2 XRP')

xrp = dropsToXrp(new BigNumber(-2000000))
assert.strictEqual(xrp, -2, '(BigNumber) -2 million drops equals -2 XRP')
assert.strictEqual(xrp, '-2', '(BigNumber) -2 million drops equals -2 XRP')

xrp = dropsToXrp(new BigNumber(2345678))
assert.strictEqual(
xrp,
2.345678,
'2.345678',
'(BigNumber) 2,345,678 drops equals 2.345678 XRP',
)

xrp = dropsToXrp(new BigNumber(-2345678))
assert.strictEqual(
xrp,
-2.345678,
'-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 = dropsToXrp(2000000)
assert.strictEqual(xrp, 2, '(number) 2 million drops equals 2 XRP')
assert.strictEqual(xrp, '2', '(number) 2 million drops equals 2 XRP')
xrp = dropsToXrp(-2000000)
assert.strictEqual(xrp, -2, '(number) -2 million drops equals -2 XRP')
assert.strictEqual(xrp, '-2', '(number) -2 million drops equals -2 XRP')
})

it('works with scientific notation', function () {
const xrp = dropsToXrp('1e6')
assert.strictEqual(
xrp,
1,
'1',
'(scientific notation string) 1e6 drops equals 1 XRP',
)
})
Expand Down Expand Up @@ -130,4 +130,20 @@ describe('dropsToXrp', function () {
dropsToXrp('...')
}, /dropsToXrp: invalid value '\.\.\.'/u)
})

// Regression test for xrpl.js#3316: dropsToXrp previously returned a JS
// number, which silently lost precision for amounts approaching the XRP
// supply (~10^17 drops). The round-trip xrpToDrops(dropsToXrp(d)) should
// return exactly `d` for every valid drops amount.
it('round-trips large amounts without precision loss (issue #3316)', function () {
const drops = '9999999999999999'
const xrp = dropsToXrp(drops)
assert.strictEqual(
xrp,
'9999999999.999999',
'large drops amounts are converted without losing the trailing drop',
)
const roundTripped = xrpToDrops(xrp)
assert.strictEqual(roundTripped, drops)
})
})
Loading