Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integration test for OracleSet txn #2795

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
3 changes: 3 additions & 0 deletions packages/xrpl/HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ Subscribe to [the **xrpl-announce** mailing list](https://groups.google.com/g/xr
### Added
* parseTransactionFlags as a utility function in the xrpl package to streamline transactions flags-to-map conversion

### Fixed
* Expanded the supported range of values in `OracleSet` transaction's `AssetPrice` field. The change enables values in range [0, 18446744073709551615], both inclusive.

## 4.0.0 (2024-07-15)

### BREAKING CHANGES
Expand Down
2 changes: 1 addition & 1 deletion packages/xrpl/src/models/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ export interface PriceData {
* The asset price after applying the Scale precision level. It's not included if the last update transaction didn't include
* the BaseAsset/QuoteAsset pair.
*/
AssetPrice?: number | string
AssetPrice?: bigint | number | string

/**
* The scaling factor to apply to an asset price. For example, if Scale is 6 and original price is 0.155, then the scaled
Expand Down
10 changes: 7 additions & 3 deletions packages/xrpl/src/models/transactions/NFTokenAcceptOffer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,13 @@ export interface NFTokenAcceptOfferMetadata extends TransactionMetadataBase {
}

function validateNFTokenBrokerFee(tx: Record<string, unknown>): void {
const value = parseAmountValue(tx.NFTokenBrokerFee)
if (Number.isNaN(value)) {
throw new ValidationError('NFTokenAcceptOffer: invalid NFTokenBrokerFee')
let value: bigint
try {
value = parseAmountValue(tx.NFTokenBrokerFee)
} catch {
throw new ValidationError(
'NFTokenAcceptOffer: invalid NFTokenBrokerFee. BigInt constructor could not parse NFTokenBrokerFee',
)
}

if (value <= 0) {
Expand Down
11 changes: 10 additions & 1 deletion packages/xrpl/src/models/transactions/NFTokenCreateOffer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,16 @@ function validateNFTokenBuyOfferCases(tx: Record<string, unknown>): void {
)
}

if (parseAmountValue(tx.Amount) <= 0) {
let parsedAmount: bigint
try {
parsedAmount = parseAmountValue(tx.Amount)
} catch {
throw new ValidationError(
'NFTokenCreateOffer: Invalid Amount, Amount field could not be parsed by BigInt constructor',
)
}

if (parsedAmount <= 0) {
throw new ValidationError(
'NFTokenCreateOffer: Amount must be greater than 0 for buy offers',
)
Expand Down
14 changes: 9 additions & 5 deletions packages/xrpl/src/models/transactions/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export function isString(str: unknown): str is string {
* @returns Whether the number is properly formed.
*/
export function isNumber(num: unknown): num is number {
return typeof num === 'number'
return typeof num === 'number' || typeof num === 'bigint'
Copy link

@coderabbitai coderabbitai bot Oct 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider updating parseAmountValue to handle bigint

With the inclusion of bigint in the isNumber function, it's important to ensure that the parseAmountValue function can handle bigint values correctly. Currently, it uses parseFloat, which may not be suitable for bigint values.

Consider updating the parseAmountValue function to handle bigint values. Here's a suggested implementation:

export function parseAmountValue(amount: unknown): number {
  if (!isAmount(amount)) {
    return NaN;
  }
  if (typeof amount === 'string') {
    return parseFloat(amount);
  }
  if (typeof amount.value === 'bigint') {
    return Number(amount.value);
  }
  return parseFloat(amount.value);
}

This change ensures that bigint values are correctly converted to numbers, while maintaining the existing behavior for other types.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • I'm not aware of cases where OracleSet transaction's AssetPrice field is used as an Amount field.
  • Even if that is the case, type-coercion of bigint into number type causes loss of precision. I'd prefer to do it the other way around.
  • The precision supported by the Amount field is not as large as that of bigint. IssuedCurrencies use 54 bits whereas XRP Amounts need 57 bits, to represent their highest values. The former case needs further investigation, appropos the parseAmountValue function.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ckeshava, thank you for the clarification. You make a valid point regarding the potential loss of precision when converting bigint to number. It's important to preserve data integrity by avoiding such type coercion. Given that AssetPrice in OracleSet is not used as an Amount field, and considering the precision requirements for IssuedCurrencies and XRP amounts, further investigation into parseAmountValue is indeed warranted before making any changes.

(🐰🤔)


✏️ Learnings added
Learnt from: ckeshava
PR: XRPLF/xrpl.js#2795
File: packages/xrpl/src/models/transactions/common.ts:84-84
Timestamp: 2024-10-03T02:06:25.875Z
Learning: When dealing with `bigint` values, avoid converting them to `number` to prevent loss of precision.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Continuing on the last bullet point, I incorporated the suggestions in this commit d53dd5e

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ckeshava, great work! I see that you've updated the parseAmountValue function to return bigint and adjusted the validation logic accordingly. The addition of comprehensive unit tests for parseAmountValue enhances the robustness of the code. This effectively addresses the previous concern about loss of precision when converting bigint to number. Thank you for incorporating these changes!

(🐰😊)

🔗 Analysis chain
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Show the diff for the commit d53dd5eb6726177d218f3da11bc9eb266b4c0085
git show d53dd5eb6726177d218f3da11bc9eb266b4c0085

Length of output: 5244


🧠 Learnings used
Learnt from: ckeshava
PR: XRPLF/xrpl.js#2795
File: packages/xrpl/src/models/transactions/common.ts:84-84
Timestamp: 2024-10-03T02:06:26.057Z
Learning: When dealing with `bigint` values, avoid converting them to `number` to prevent loss of precision.

If you found this review helpful, would you consider giving us a shout-out on X?
Thank you for using CodeRabbit!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks rabbit :)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ckeshava, you're welcome! Happy to help!

(🐰🎉)


🧠 Learnings used
Learnt from: ckeshava
PR: XRPLF/xrpl.js#2795
File: packages/xrpl/src/models/transactions/common.ts:84-84
Timestamp: 2024-10-03T02:06:26.057Z
Learning: When dealing with `bigint` values, avoid converting them to `number` to prevent loss of precision.

}

/**
Expand Down Expand Up @@ -356,13 +356,17 @@ export function validateBaseTransaction(common: Record<string, unknown>): void {
*
* @param amount - An Amount to parse for its value.
* @returns The parsed amount value, or NaN if the amount count not be parsed.
* @throws ValidationError, if the input Amount is invalid
* @throws SyntaxError, if Amount cannot be parsed by BigInt constructor
*/
export function parseAmountValue(amount: unknown): number {
export function parseAmountValue(amount: unknown): bigint {
if (!isAmount(amount)) {
return NaN
throw new ValidationError(
'parseAmountValue: Specified input Amount is invalid',
)
}
if (typeof amount === 'string') {
return parseFloat(amount)
return BigInt(amount)
}
return parseFloat(amount.value)
return BigInt(amount.value)
}
20 changes: 19 additions & 1 deletion packages/xrpl/test/integration/transactions/oracleSet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ import { testTransaction } from '../utils'

// how long before each test case times out
const TIMEOUT = 20000
// Upper bound admissible value for AssetPrice field
// large numeric values necessarily have to use str type in Javascript
// number type uses double-precision floating point representation, hence represents a smaller range of values
const MAX_64_BIT_UNSIGNED_INT = '18446744073709551615'

describe('OracleSet', function () {
let testContext: XrplIntegrationTestContext
Expand All @@ -39,6 +43,14 @@ describe('OracleSet', function () {
Scale: 3,
},
},
{
PriceData: {
BaseAsset: 'XRP',
QuoteAsset: 'INR',
AssetPrice: BigInt(MAX_64_BIT_UNSIGNED_INT),
Scale: 3,
},
},
],
Provider: stringToHex('chainlink'),
URI: '6469645F6578616D706C65',
Expand All @@ -62,12 +74,18 @@ describe('OracleSet', function () {
assert.equal(oracle.Owner, testContext.wallet.classicAddress)
assert.equal(oracle.AssetClass, tx.AssetClass)
assert.equal(oracle.Provider, tx.Provider)
assert.equal(oracle.PriceDataSeries.length, 1)
assert.equal(oracle.PriceDataSeries.length, 2)
assert.equal(oracle.PriceDataSeries[0].PriceData.BaseAsset, 'XRP')
assert.equal(oracle.PriceDataSeries[0].PriceData.QuoteAsset, 'USD')
assert.equal(oracle.PriceDataSeries[0].PriceData.AssetPrice, '2e4')
assert.equal(oracle.PriceDataSeries[0].PriceData.Scale, 3)
assert.equal(oracle.Flags, 0)

// validate the serialization of large AssetPrice values
assert.equal(
oracle.PriceDataSeries[1].PriceData.AssetPrice,
'ffffffffffffffff',
)
},
TIMEOUT,
)
Expand Down
51 changes: 50 additions & 1 deletion packages/xrpl/test/models/NFTokenCreateOffer.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,59 @@
import { assert } from 'chai'

import { validate, ValidationError, NFTokenCreateOfferFlags } from '../../src'
import {
validate,
ValidationError,
NFTokenCreateOfferFlags,
IssuedCurrencyAmount,
} from '../../src'
import { parseAmountValue } from '../../src/models/transactions/common'

const NFTOKEN_ID =
'00090032B5F762798A53D543A014CAF8B297CFF8F2F937E844B17C9E00000003'

describe('parseAmountValue', function () {
it(`validate large amount values`, function () {
// (Upper bound of created XRP tokens) minus 12 drops
assert.equal(
parseAmountValue('99999999999999988'),
BigInt('99999999999999988'),
)

// Token Amounts or Issued Currencies are represented using 54 bits of precision in the XRP Ledger
// Docs: https://xrpl.org/docs/references/protocol/binary-format#token-amount-format
const highest_iou_amount: IssuedCurrencyAmount = {
currency: 'ABC',
issuer: 'rIssuerAddress',
// 54 bits can be used to safely represent a value of (2**54 - 1)
value: '18014398509481983',
}

assert.equal(
parseAmountValue(highest_iou_amount),
BigInt('18014398509481983'),
)
})

it(`validate non-positive amount values`, function () {
assert.equal(parseAmountValue('0'), BigInt(0))
assert.equal(parseAmountValue('-1234'), BigInt(-1234))
})

it(`validate invalid amount values`, function () {
assert.throws(
() => parseAmountValue(1234),
ValidationError,
'parseAmountValue: Specified input Amount is invalid',
)

assert.throws(
() => parseAmountValue('abcd'),
SyntaxError,
'Cannot convert abcd to a BigInt',
)
})
})

/**
* NFTokenCreateOffer Transaction Verification Testing.
*
Expand Down
Loading