diff --git a/packages/xahau/src/models/transactions/oracleSet.ts b/packages/xahau/src/models/transactions/oracleSet.ts index 6b3f395d..efd14615 100644 --- a/packages/xahau/src/models/transactions/oracleSet.ts +++ b/packages/xahau/src/models/transactions/oracleSet.ts @@ -1,5 +1,6 @@ import { ValidationError } from '../../errors' import { PriceData } from '../common' +import { isHex } from '../utils' import { BaseTransaction, @@ -12,6 +13,8 @@ import { const PRICE_DATA_SERIES_MAX_LENGTH = 10 const SCALE_MAX = 10 +const MINIMUM_ASSET_PRICE_LENGTH = 1 +const MAXIMUM_ASSET_PRICE_LENGTH = 16 /** * Creates a new Oracle ledger entry or updates the fields of an existing one, using the Oracle ID. @@ -82,7 +85,7 @@ export function validateOracleSet(tx: Record): void { validateOptionalField(tx, 'AssetClass', isString) - // eslint-disable-next-line max-lines-per-function -- necessary to validate many fields + /* eslint-disable max-statements, max-lines-per-function -- necessary to validate many fields */ validateRequiredField(tx, 'PriceDataSeries', (value) => { if (!Array.isArray(value)) { throw new ValidationError('OracleSet: PriceDataSeries must be an array') @@ -142,14 +145,32 @@ export function validateOracleSet(tx: Record): void { ) } - if ( - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- we are validating the type - 'AssetPrice' in priceData.PriceData && - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- we are validating the type - !isNumber(priceData.PriceData.AssetPrice) - ) { - throw new ValidationError('OracleSet: invalid field AssetPrice') + /* eslint-disable @typescript-eslint/no-unsafe-member-access, max-depth -- + we need to validate priceData.PriceData.AssetPrice value */ + if ('AssetPrice' in priceData.PriceData) { + if (!isNumber(priceData.PriceData.AssetPrice)) { + if (typeof priceData.PriceData.AssetPrice !== 'string') { + throw new ValidationError( + 'OracleSet: Field AssetPrice must be a string or a number', + ) + } + if (!isHex(priceData.PriceData.AssetPrice)) { + throw new ValidationError( + 'OracleSet: Field AssetPrice must be a valid hex string', + ) + } + if ( + priceData.PriceData.AssetPrice.length < + MINIMUM_ASSET_PRICE_LENGTH || + priceData.PriceData.AssetPrice.length > MAXIMUM_ASSET_PRICE_LENGTH + ) { + throw new ValidationError( + `OracleSet: Length of AssetPrice field must be between ${MINIMUM_ASSET_PRICE_LENGTH} and ${MAXIMUM_ASSET_PRICE_LENGTH} characters long`, + ) + } + } } + /* eslint-enable @typescript-eslint/no-unsafe-member-access, max-depth */ if ( // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- we are validating the type @@ -173,4 +194,5 @@ export function validateOracleSet(tx: Record): void { } return true }) + /* eslint-enable max-statements, max-lines-per-function */ } diff --git a/packages/xahau/test/integration/transactions/oracleSet.test.ts b/packages/xahau/test/integration/transactions/oracleSet.test.ts index 5927963d..9725b2f8 100644 --- a/packages/xahau/test/integration/transactions/oracleSet.test.ts +++ b/packages/xahau/test/integration/transactions/oracleSet.test.ts @@ -39,6 +39,17 @@ describe('OracleSet', function () { Scale: 3, }, }, + { + PriceData: { + BaseAsset: 'XRP', + QuoteAsset: 'INR', + // 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 + AssetPrice: 'ffffffffffffffff', + Scale: 3, + }, + }, ], Provider: stringToHex('chainlink'), URI: '6469645F6578616D706C65', @@ -62,12 +73,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, ) diff --git a/packages/xahau/test/models/oracleSet.test.ts b/packages/xahau/test/models/oracleSet.test.ts index 18ce5d87..b7f90a61 100644 --- a/packages/xahau/test/models/oracleSet.test.ts +++ b/packages/xahau/test/models/oracleSet.test.ts @@ -167,8 +167,24 @@ describe('OracleSet', function () { }) it(`throws w/ invalid AssetPrice of PriceDataSeries`, function () { - tx.PriceDataSeries[0].PriceData.AssetPrice = '1234' - const errorMessage = 'OracleSet: invalid field AssetPrice' + // value cannot be parsed as hexadecimal number + tx.PriceDataSeries[0].PriceData.AssetPrice = 'ghij' + const errorMessage = + 'OracleSet: Field AssetPrice must be a valid hex string' + assert.throws(() => validateOracleSet(tx), ValidationError, errorMessage) + assert.throws(() => validate(tx), ValidationError, errorMessage) + }) + + it(`verifies valid AssetPrice of PriceDataSeries`, function () { + // valid string which can be parsed as hexadecimal number + tx.PriceDataSeries[0].PriceData.AssetPrice = 'ab15' + assert.doesNotThrow(() => validate(tx)) + }) + + it(`throws w/ invalid AssetPrice type in PriceDataSeries`, function () { + tx.PriceDataSeries[0].PriceData.AssetPrice = ['sample', 'invalid', 'type'] + const errorMessage = + 'OracleSet: Field AssetPrice must be a string or a number' assert.throws(() => validateOracleSet(tx), ValidationError, errorMessage) assert.throws(() => validate(tx), ValidationError, errorMessage) })