Compare commits

...

8 Commits

Author SHA1 Message Date
tequ
b0e8a9f294 after fixPriceOracleOrder 2026-06-18 22:25:25 +09:00
tequ
0a531625ed update fixture 2026-06-18 21:46:01 +09:00
tequ
503cf345a0 disable max-lines worning 2026-06-18 21:32:59 +09:00
Chenna Keshava B S
d89ee1abb3 fix #2911: Update OracleSet transaction model, unit, integ tests (#2913)
* fix #2911: Update OracleSet transaction model, unit, integ tests

---------

Co-authored-by: Omar Khan <khancodegt@gmail.com>
2026-06-18 21:25:36 +09:00
Omar Khan
bc8ae32ad0 feat: add input check for OracleSet params AssetPrice and Scale (#2699)
* add input check for AssetPrice and Scale being either both present or excluded
2026-06-18 21:24:53 +09:00
tequ
79f952737b fix definitions.json 2026-06-18 21:23:38 +09:00
tequ
f84ac1f587 Merge remote-tracking branch 'upstream/main-xahau' into PriceOracle 2026-06-18 21:20:11 +09:00
Omar Khan
6a8408309e feat: add Price Oracles support (#2688) 2026-06-18 21:19:49 +09:00
17 changed files with 955 additions and 2 deletions

View File

@@ -2,8 +2,6 @@
## Unreleased
## 2.1.0 (2024-06-03)
### Added
* Support for the Price Oracles amendment (XLS-47).

View File

@@ -4449,6 +4449,36 @@
"Flags": 0,
"Sequence": 62
}
},
{
"binary": "12003C2FFFFFFFFF2033000004D2750B6469645F6578616D706C65701D0863757272656E6379701E0870726F7669646572811401476926B590BA3245F63C829116A0A3AF7F382DF018E020301700000000000001E2041003011A0000000000000000000000000000000000000000021A0000000000000000000000005553440000000000E1F1",
"json": {
"TransactionType": "OracleSet",
"Account": "rfmDuhDyLGgx94qiwf3YF8BUV5j6KSvE8",
"OracleDocumentID": 1234,
"LastUpdateTime": 4294967295,
"PriceDataSeries": [
{
"PriceData": {
"BaseAsset": "XAH",
"QuoteAsset": "USD",
"AssetPrice": "00000000000001E2",
"Scale": 3
}
}
],
"Provider": "70726F7669646572",
"URI": "6469645F6578616D706C65",
"AssetClass": "63757272656E6379"
}
},
{
"binary": "12003D2033000004D2811401476926B590BA3245F63C829116A0A3AF7F382D",
"json": {
"TransactionType": "OracleDelete",
"Account": "rfmDuhDyLGgx94qiwf3YF8BUV5j6KSvE8",
"OracleDocumentID": 1234
}
}
],
"ledgerData": [{

View File

@@ -9,6 +9,7 @@ Subscribe to [the **xrpl-announce** mailing list](https://groups.google.com/g/xr
* Support for IOUClaimReward Amendment
* Support for NamedHooks
* Support for the `simulate` RPC ([XLS-69](https://github.com/XRPLF/XRPL-Standards/tree/master/XLS-0069-simulate))
* Support for the Price Oracles amendment (XLS-47).
## 4.0.4 (2026-05-27)
@@ -67,6 +68,9 @@ Subscribe to [the **xrpl-announce** mailing list](https://groups.google.com/g/xr
### Added
* Support for the Price Oracles amendment (XLS-47).
### Added
* Support for the Price Oracles amendment (XLS-47).
### Fixed
* Typo in `Channel` type `source_tab` -> `source_tag`
* Fix `client.requestAll` to handle filters better

View File

@@ -14,6 +14,7 @@ import ImportVLSequence from './ImportVLSequence'
import LedgerHashes from './LedgerHashes'
import NegativeUNL from './NegativeUNL'
import Offer from './Offer'
import Oracle from './Oracle'
import PayChannel from './PayChannel'
import RippleState from './RippleState'
import SignerList from './SignerList'
@@ -38,6 +39,7 @@ type LedgerEntry =
| LedgerHashes
| NegativeUNL
| Offer
| Oracle
| PayChannel
| RippleState
| SignerList
@@ -60,6 +62,7 @@ type LedgerEntryFilter =
| 'import_vl_sequence'
| 'hashes'
| 'offer'
| 'oracle'
| 'payment_channel'
| 'signer_list'
| 'state'

View File

@@ -0,0 +1,43 @@
import { PriceData } from '../common'
import { BaseLedgerEntry, HasPreviousTxnID } from './BaseLedgerEntry'
/**
* The Oracle object type describes a single Price Oracle instance.
*
* @category Ledger Entries
*/
export default interface Oracle extends BaseLedgerEntry, HasPreviousTxnID {
LedgerEntryType: 'Oracle'
/**
* The time the data was last updated, represented as a unix timestamp in seconds.
*/
LastUpdateTime: number
/**
* The XRPL account with update and delete privileges for the oracle.
*/
Owner: string
/**
* Describes the type of asset, such as "currency", "commodity", or "index".
*/
AssetClass: string
/**
* The oracle provider, such as Chainlink, Band, or DIA.
*/
Provider: string
/**
* An array of up to 10 PriceData objects.
*/
PriceDataSeries: PriceData[]
/**
* A bit-map of boolean flags. No flags are defined for the Oracle object
* type, so this value is always 0.
*/
Flags: 0
}

View File

@@ -23,6 +23,7 @@ import { LedgerEntry, LedgerEntryFilter } from './LedgerEntry'
import LedgerHashes from './LedgerHashes'
import NegativeUNL, { NEGATIVE_UNL_ID } from './NegativeUNL'
import Offer, { OfferFlags } from './Offer'
import Oracle from './Oracle'
import PayChannel from './PayChannel'
import RippleState, { RippleStateFlags } from './RippleState'
import SignerList, { SignerListFlags } from './SignerList'
@@ -60,6 +61,7 @@ export {
NegativeUNL,
Offer,
OfferFlags,
Oracle,
PayChannel,
RippleState,
RippleStateFlags,

View File

@@ -0,0 +1,119 @@
import { BaseRequest, BaseResponse } from './baseMethod'
/**
* The `get_aggregate_price` method retrieves the aggregate price of specified Oracle objects,
* returning three price statistics: mean, median, and trimmed mean.
* Returns an {@link GetAggregatePriceResponse}.
*
* @category Requests
*/
export interface GetAggregatePriceRequest extends BaseRequest {
command: 'get_aggregate_price'
/**
* The currency code of the asset to be priced.
*/
base_asset: string
/**
* The currency code of the asset to quote the price of the base asset.
*/
quote_asset: string
/**
* The oracle identifier.
*/
oracles: Array<{
/**
* The XRPL account that controls the Oracle object.
*/
account: string
/**
* A unique identifier of the price oracle for the Account
*/
oracle_document_id: string | number
}>
/**
* The percentage of outliers to trim. Valid trim range is 1-25. If included, the API returns statistics for the trimmed mean.
*/
trim?: number
/**
* Defines a time range in seconds for filtering out older price data. Default value is 0, which doesn't filter any data.
*/
trim_threshold?: number
}
/**
* Response expected from an {@link GetAggregatePriceRequest}.
*
* @category Responses
*/
export interface GetAggregatePriceResponse extends BaseResponse {
result: {
/**
* The statistics from the collected oracle prices.
*/
entire_set: {
/**
* The simple mean.
*/
mean: string
/**
* The size of the data set to calculate the mean.
*/
size: number
/**
* The standard deviation.
*/
standard_deviation: string
}
/**
* The trimmed statistics from the collected oracle prices. Only appears if the trim field was specified in the request.
*/
trimmed_set?: {
/**
* The simple mean of the trimmed data.
*/
mean: string
/**
* The size of the data to calculate the trimmed mean.
*/
size: number
/**
* The standard deviation of the trimmed data.
*/
standard_deviation: string
}
/**
* The median of the collected oracle prices.
*/
median: string
/**
* The most recent timestamp out of all LastUpdateTime values.
*/
time: number
/**
* The ledger index of the ledger version that was used to generate this
* response.
*/
ledger_current_index: number
/**
* If included and set to true, the information in this response comes from
* a validated ledger version. Otherwise, the information is subject to
* change.
*/
validated: boolean
}
}

View File

@@ -78,6 +78,10 @@ import {
GatewayBalancesRequest,
GatewayBalancesResponse,
} from './gatewayBalances'
import {
GetAggregatePriceRequest,
GetAggregatePriceResponse,
} from './getAggregatePrice'
import {
LedgerBinary,
LedgerModifiedOfferCreateTransaction,
@@ -221,6 +225,8 @@ type Request =
// utility methods
| PingRequest
| RandomRequest
// Price Oracle methods
| GetAggregatePriceRequest
/**
* @category Responses
@@ -269,6 +275,8 @@ type Response<Version extends APIVersion = typeof DEFAULT_API_VERSION> =
// utility methods
| PingResponse
| RandomResponse
// Price Oracle methods
| GetAggregatePriceResponse
export type RequestResponseMap<
T,
@@ -289,6 +297,8 @@ export type RequestResponseMap<
? AccountTxVersionResponseMap<Version>
: T extends GatewayBalancesRequest
? GatewayBalancesResponse
: T extends GetAggregatePriceRequest
? GetAggregatePriceResponse
: T extends NoRippleCheckRequest
? NoRippleCheckResponse
: // NOTE: The order of these LedgerRequest types is important
@@ -481,6 +491,8 @@ export {
GatewayBalance,
GatewayBalancesRequest,
GatewayBalancesResponse,
GetAggregatePriceRequest,
GetAggregatePriceResponse,
NoRippleCheckRequest,
NoRippleCheckResponse,
// ledger methods

View File

@@ -32,6 +32,8 @@ export {
OfferCreateFlagsInterface,
OfferCreate,
} from './offerCreate'
export { OracleDelete } from './oracleDelete'
export { OracleSet } from './oracleSet'
export { PaymentFlags, PaymentFlagsInterface, Payment } from './payment'
export {
PaymentChannelClaimFlags,

View File

@@ -0,0 +1,32 @@
import {
BaseTransaction,
isNumber,
validateBaseTransaction,
validateRequiredField,
} from './common'
/**
* Delete an Oracle ledger entry.
*
* @category Transaction Models
*/
export interface OracleDelete extends BaseTransaction {
TransactionType: 'OracleDelete'
/**
* A unique identifier of the price oracle for the Account.
*/
OracleDocumentID: number
}
/**
* Verify the form and type of a OracleDelete at runtime.
*
* @param tx - A OracleDelete Transaction.
* @throws When the OracleDelete is malformed.
*/
export function validateOracleDelete(tx: Record<string, unknown>): void {
validateBaseTransaction(tx)
validateRequiredField(tx, 'OracleDocumentID', isNumber)
}

View File

@@ -0,0 +1,198 @@
import { ValidationError } from '../../errors'
import { PriceData } from '../common'
import { isHex } from '../utils'
import {
BaseTransaction,
isNumber,
isString,
validateBaseTransaction,
validateOptionalField,
validateRequiredField,
} from './common'
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.
*
* The oracle provider must complete these steps before submitting this transaction:
* 1. Create or own the XRPL account in the Owner field and have enough XRP to meet the reserve and transaction fee requirements.
* 2. Publish the XRPL account public key, so it can be used for verification by dApps.
* 3. Publish a registry of available price oracles with their unique OracleDocumentID.
*
* @category Transaction Models
*/
export interface OracleSet extends BaseTransaction {
TransactionType: 'OracleSet'
/**
* A unique identifier of the price oracle for the Account.
*/
OracleDocumentID: number
/**
* The time the data was last updated, represented as a unix timestamp in seconds.
*/
LastUpdateTime: number
/**
* An array of up to 10 PriceData objects, each representing the price information
* for a token pair. More than five PriceData objects require two owner reserves.
*/
PriceDataSeries: PriceData[]
/**
* An arbitrary value that identifies an oracle provider, such as Chainlink, Band,
* or DIA. This field is a string, up to 256 ASCII hex encoded characters (0x20-0x7E).
* This field is required when creating a new Oracle ledger entry, but is optional for updates.
*/
Provider?: string
/**
* An optional Universal Resource Identifier to reference price data off-chain. This field is limited to 256 bytes.
*/
URI?: string
/**
* Describes the type of asset, such as "currency", "commodity", or "index". This field is a string, up to 16 ASCII
* hex encoded characters (0x20-0x7E). This field is required when creating a new Oracle ledger entry, but is optional
* for updates.
*/
AssetClass?: string
}
/**
* Verify the form and type of a OracleSet at runtime.
*
* @param tx - A OracleSet Transaction.
* @throws When the OracleSet is malformed.
*/
// eslint-disable-next-line max-lines-per-function -- necessary to validate many fields
export function validateOracleSet(tx: Record<string, unknown>): void {
validateBaseTransaction(tx)
validateRequiredField(tx, 'OracleDocumentID', isNumber)
validateRequiredField(tx, 'LastUpdateTime', isNumber)
validateOptionalField(tx, 'Provider', isString)
validateOptionalField(tx, 'URI', isString)
validateOptionalField(tx, 'AssetClass', isString)
/* 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')
}
if (value.length > PRICE_DATA_SERIES_MAX_LENGTH) {
throw new ValidationError(
`OracleSet: PriceDataSeries must have at most ${PRICE_DATA_SERIES_MAX_LENGTH} PriceData objects`,
)
}
// TODO: add support for handling inner objects easier (similar to validateRequiredField/validateOptionalField)
for (const priceData of value) {
if (typeof priceData !== 'object') {
throw new ValidationError(
'OracleSet: PriceDataSeries must be an array of objects',
)
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- we are validating the type
if (priceData.PriceData == null) {
throw new ValidationError(
'OracleSet: PriceDataSeries must have a `PriceData` object',
)
}
// check if priceData only has PriceData
if (Object.keys(priceData).length !== 1) {
throw new ValidationError(
'OracleSet: PriceDataSeries must only have a single PriceData object',
)
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- we are validating the type
if (typeof priceData.PriceData.BaseAsset !== 'string') {
throw new ValidationError(
'OracleSet: PriceDataSeries must have a `BaseAsset` string',
)
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- we are validating the type
if (typeof priceData.PriceData.QuoteAsset !== 'string') {
throw new ValidationError(
'OracleSet: PriceDataSeries must have a `QuoteAsset` string',
)
}
// Either AssetPrice and Scale are both present or both excluded
if (
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- we are validating the type
(priceData.PriceData.AssetPrice == null) !==
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- we are validating the type
(priceData.PriceData.Scale == null)
) {
throw new ValidationError(
'OracleSet: PriceDataSeries must have both `AssetPrice` and `Scale` if any are present',
)
}
/* 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
'Scale' in priceData.PriceData &&
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- we are validating the type
!isNumber(priceData.PriceData.Scale)
) {
throw new ValidationError('OracleSet: invalid field Scale')
}
if (
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- we are validating the type
priceData.PriceData.Scale < 0 ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- we are validating the type
priceData.PriceData.Scale > SCALE_MAX
) {
throw new ValidationError(
`OracleSet: Scale must be in range 0-${SCALE_MAX}`,
)
}
}
return true
})
/* eslint-enable max-statements, max-lines-per-function */
}

View File

@@ -24,6 +24,8 @@ import { Invoke, validateInvoke } from './invoke'
import { TransactionMetadata } from './metadata'
import { OfferCancel, validateOfferCancel } from './offerCancel'
import { OfferCreate, validateOfferCreate } from './offerCreate'
import { OracleDelete, validateOracleDelete } from './oracleDelete'
import { OracleSet, validateOracleSet } from './oracleSet'
import { Payment, validatePayment } from './payment'
import {
PaymentChannelClaim,
@@ -79,6 +81,8 @@ export type SubmittableTransaction =
| Invoke
| OfferCancel
| OfferCreate
| OracleDelete
| OracleSet
| Payment
| PaymentChannelClaim
| PaymentChannelCreate
@@ -249,6 +253,14 @@ export function validate(transaction: Record<string, unknown>): void {
validateOfferCreate(tx)
break
case 'OracleDelete':
validateOracleDelete(tx)
break
case 'OracleSet':
validateOracleSet(tx)
break
case 'Payment':
validatePayment(tx)
break
@@ -315,6 +327,7 @@ export function validate(transaction: Record<string, unknown>): void {
default:
throw new ValidationError(
// eslint-disable-next-line max-lines -- allowed here
`Invalid field TransactionType: ${tx.TransactionType}`,
)
}

View File

@@ -0,0 +1,78 @@
import { stringToHex } from '@xrplf/isomorphic/utils'
import { assert } from 'chai'
import { OracleSet } from '../../../src'
import serverUrl from '../serverUrl'
import {
setupClient,
teardownClient,
type XrplIntegrationTestContext,
} from '../setup'
import { testTransaction } from '../utils'
// how long before each test case times out
const TIMEOUT = 20000
describe('get_aggregate_price', function () {
let testContext: XrplIntegrationTestContext
beforeEach(async () => {
testContext = await setupClient(serverUrl)
})
afterEach(async () => teardownClient(testContext))
it(
'base',
async () => {
const tx: OracleSet = {
TransactionType: 'OracleSet',
Account: testContext.wallet.classicAddress,
OracleDocumentID: 1234,
LastUpdateTime: Math.floor(Date.now() / 1000),
PriceDataSeries: [
{
PriceData: {
BaseAsset: 'XRP',
QuoteAsset: 'USD',
AssetPrice: 740,
Scale: 3,
},
},
],
Provider: stringToHex('chainlink'),
URI: '6469645F6578616D706C65',
AssetClass: stringToHex('currency'),
}
await testTransaction(testContext.client, tx, testContext.wallet)
// confirm that the Oracle was actually created
const getAggregatePriceResponse = await testContext.client.request({
command: 'get_aggregate_price',
account: testContext.wallet.classicAddress,
base_asset: 'XRP',
quote_asset: 'USD',
trim: 20,
oracles: [
{
account: testContext.wallet.classicAddress,
oracle_document_id: 1234,
},
],
})
assert.deepEqual(getAggregatePriceResponse.result.entire_set, {
mean: '0.74',
size: 1,
standard_deviation: '0',
})
assert.deepEqual(getAggregatePriceResponse.result.trimmed_set, {
mean: '0.74',
size: 1,
standard_deviation: '0',
})
assert.equal(getAggregatePriceResponse.result.median, '0.74')
assert.equal(getAggregatePriceResponse.result.time, tx.LastUpdateTime)
},
TIMEOUT,
)
})

View File

@@ -0,0 +1,76 @@
import { stringToHex } from '@xrplf/isomorphic/utils'
import { assert } from 'chai'
import { OracleSet, OracleDelete } from '../../../src'
import serverUrl from '../serverUrl'
import {
setupClient,
teardownClient,
type XrplIntegrationTestContext,
} from '../setup'
import { testTransaction } from '../utils'
// how long before each test case times out
const TIMEOUT = 20000
describe('OracleDelete', function () {
let testContext: XrplIntegrationTestContext
beforeEach(async () => {
testContext = await setupClient(serverUrl)
})
afterEach(async () => teardownClient(testContext))
it(
'base',
async () => {
const setTx: OracleSet = {
TransactionType: 'OracleSet',
Account: testContext.wallet.classicAddress,
OracleDocumentID: 1234,
LastUpdateTime: Math.floor(Date.now() / 1000),
PriceDataSeries: [
{
PriceData: {
BaseAsset: 'XRP',
QuoteAsset: 'USD',
AssetPrice: 740,
Scale: 3,
},
},
],
Provider: stringToHex('chainlink'),
URI: '6469645F6578616D706C65',
AssetClass: stringToHex('currency'),
}
await testTransaction(testContext.client, setTx, testContext.wallet)
const aoResult = await testContext.client.request({
command: 'account_objects',
account: testContext.wallet.classicAddress,
type: 'oracle',
})
// confirm that the Oracle was created
assert.equal(aoResult.result.account_objects.length, 1)
const deleteTx: OracleDelete = {
TransactionType: 'OracleDelete',
Account: testContext.wallet.classicAddress,
OracleDocumentID: 1234,
}
await testTransaction(testContext.client, deleteTx, testContext.wallet)
const aoResult2 = await testContext.client.request({
command: 'account_objects',
account: testContext.wallet.classicAddress,
})
// confirm that the Oracle was actually deleted
assert.equal(aoResult2.result.account_objects.length, 0)
},
TIMEOUT,
)
})

View File

@@ -0,0 +1,91 @@
import { stringToHex } from '@xrplf/isomorphic/utils'
import { assert } from 'chai'
import { OracleSet } from '../../../src'
import { Oracle } from '../../../src/models/ledger'
import serverUrl from '../serverUrl'
import {
setupClient,
teardownClient,
type XrplIntegrationTestContext,
} from '../setup'
import { testTransaction } from '../utils'
// how long before each test case times out
const TIMEOUT = 20000
describe('OracleSet', function () {
let testContext: XrplIntegrationTestContext
beforeEach(async () => {
testContext = await setupClient(serverUrl)
})
afterEach(async () => teardownClient(testContext))
it(
'base',
async () => {
const tx: OracleSet = {
TransactionType: 'OracleSet',
Account: testContext.wallet.classicAddress,
OracleDocumentID: 1234,
LastUpdateTime: Math.floor(Date.now() / 1000),
PriceDataSeries: [
{
PriceData: {
BaseAsset: 'XRP',
QuoteAsset: 'USD',
AssetPrice: 740,
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',
AssetClass: stringToHex('currency'),
}
await testTransaction(testContext.client, tx, testContext.wallet)
const result = await testContext.client.request({
command: 'account_objects',
account: testContext.wallet.classicAddress,
type: 'oracle',
})
// confirm that the Oracle was actually created
assert.equal(result.result.account_objects.length, 1)
// confirm details of Oracle ledger entry object
const oracle = result.result.account_objects[0] as Oracle
assert.equal(oracle.LastUpdateTime, tx.LastUpdateTime)
assert.equal(oracle.Owner, testContext.wallet.classicAddress)
assert.equal(oracle.AssetClass, tx.AssetClass)
assert.equal(oracle.Provider, tx.Provider)
assert.equal(oracle.PriceDataSeries.length, 2)
assert.equal(oracle.PriceDataSeries[1].PriceData.BaseAsset, 'XRP')
assert.equal(oracle.PriceDataSeries[1].PriceData.QuoteAsset, 'USD')
assert.equal(oracle.PriceDataSeries[1].PriceData.AssetPrice, '2e4')
assert.equal(oracle.PriceDataSeries[1].PriceData.Scale, 3)
assert.equal(oracle.Flags, 0)
// validate the serialization of large AssetPrice values
assert.equal(
oracle.PriceDataSeries[0].PriceData.AssetPrice,
'ffffffffffffffff',
)
},
TIMEOUT,
)
})

View File

@@ -0,0 +1,40 @@
import { assert } from 'chai'
import { validate, ValidationError } from '../../src'
import { validateOracleDelete } from '../../src/models/transactions/oracleDelete'
/**
* OracleDelete Transaction Verification Testing.
*
* Providing runtime verification testing for each specific transaction type.
*/
describe('OracleDelete', function () {
let tx
beforeEach(function () {
tx = {
TransactionType: 'OracleDelete',
Account: 'rfmDuhDyLGgx94qiwf3YF8BUV5j6KSvE8',
OracleDocumentID: 1234,
} as any
})
it('verifies valid OracleDelete', function () {
assert.doesNotThrow(() => validateOracleDelete(tx))
assert.doesNotThrow(() => validate(tx))
})
it(`throws w/ missing field OracleDocumentID`, function () {
delete tx.OracleDocumentID
const errorMessage = 'OracleDelete: missing field OracleDocumentID'
assert.throws(() => validateOracleDelete(tx), ValidationError, errorMessage)
assert.throws(() => validate(tx), ValidationError, errorMessage)
})
it(`throws w/ invalid OracleDocumentID`, function () {
tx.OracleDocumentID = '1234'
const errorMessage = 'OracleDelete: invalid field OracleDocumentID'
assert.throws(() => validateOracleDelete(tx), ValidationError, errorMessage)
assert.throws(() => validate(tx), ValidationError, errorMessage)
})
})

View File

@@ -0,0 +1,212 @@
import { stringToHex } from '@xrplf/isomorphic/dist/utils'
import { assert } from 'chai'
import { validate, ValidationError } from '../../src'
import { validateOracleSet } from '../../src/models/transactions/oracleSet'
/**
* OracleSet Transaction Verification Testing.
*
* Providing runtime verification testing for each specific transaction type.
*/
describe('OracleSet', function () {
let tx
beforeEach(function () {
tx = {
TransactionType: 'OracleSet',
Account: 'rfmDuhDyLGgx94qiwf3YF8BUV5j6KSvE8',
OracleDocumentID: 1234,
LastUpdateTime: 768062172,
PriceDataSeries: [
{
PriceData: {
BaseAsset: 'XRP',
QuoteAsset: 'USD',
AssetPrice: 740,
Scale: 3,
},
},
],
Provider: stringToHex('chainlink'),
URI: '6469645F6578616D706C65',
AssetClass: stringToHex('currency'),
} as any
})
it('verifies valid OracleSet', function () {
assert.doesNotThrow(() => validateOracleSet(tx))
assert.doesNotThrow(() => validate(tx))
})
it(`throws w/ missing field OracleDocumentID`, function () {
delete tx.OracleDocumentID
const errorMessage = 'OracleSet: missing field OracleDocumentID'
assert.throws(() => validateOracleSet(tx), ValidationError, errorMessage)
assert.throws(() => validate(tx), ValidationError, errorMessage)
})
it(`throws w/ invalid OracleDocumentID`, function () {
tx.OracleDocumentID = '1234'
const errorMessage = 'OracleSet: invalid field OracleDocumentID'
assert.throws(() => validateOracleSet(tx), ValidationError, errorMessage)
assert.throws(() => validate(tx), ValidationError, errorMessage)
})
it(`throws w/ missing field LastUpdateTime`, function () {
delete tx.LastUpdateTime
const errorMessage = 'OracleSet: missing field LastUpdateTime'
assert.throws(() => validateOracleSet(tx), ValidationError, errorMessage)
assert.throws(() => validate(tx), ValidationError, errorMessage)
})
it(`throws w/ invalid LastUpdateTime`, function () {
tx.LastUpdateTime = '768062172'
const errorMessage = 'OracleSet: invalid field LastUpdateTime'
assert.throws(() => validateOracleSet(tx), ValidationError, errorMessage)
assert.throws(() => validate(tx), ValidationError, errorMessage)
})
it(`throws w/ missing invalid Provider`, function () {
tx.Provider = 1234
const errorMessage = 'OracleSet: invalid field Provider'
assert.throws(() => validateOracleSet(tx), ValidationError, errorMessage)
assert.throws(() => validate(tx), ValidationError, errorMessage)
})
it(`throws w/ missing invalid URI`, function () {
tx.URI = 1234
const errorMessage = 'OracleSet: invalid field URI'
assert.throws(() => validateOracleSet(tx), ValidationError, errorMessage)
assert.throws(() => validate(tx), ValidationError, errorMessage)
})
it(`throws w/ missing invalid AssetClass`, function () {
tx.AssetClass = 1234
const errorMessage = 'OracleSet: invalid field AssetClass'
assert.throws(() => validateOracleSet(tx), ValidationError, errorMessage)
assert.throws(() => validate(tx), ValidationError, errorMessage)
})
it(`throws w/ invalid PriceDataSeries must be an array`, function () {
tx.PriceDataSeries = 1234
const errorMessage = 'OracleSet: PriceDataSeries must be an array'
assert.throws(() => validateOracleSet(tx), ValidationError, errorMessage)
assert.throws(() => validate(tx), ValidationError, errorMessage)
})
it(`throws w/ invalid PriceDataSeries must be an array of objects`, function () {
tx.PriceDataSeries = [1234]
const errorMessage =
'OracleSet: PriceDataSeries must be an array of objects'
assert.throws(() => validateOracleSet(tx), ValidationError, errorMessage)
assert.throws(() => validate(tx), ValidationError, errorMessage)
})
it(`throws w/ PriceDataSeries must have at most 10 PriceData objects`, function () {
tx.PriceDataSeries = new Array(11).fill({
PriceData: {
BaseAsset: 'XRP',
QuoteAsset: 'USD',
AssetPrice: 740,
Scale: 3,
},
})
const errorMessage =
'OracleSet: PriceDataSeries must have at most 10 PriceData objects'
assert.throws(() => validateOracleSet(tx), ValidationError, errorMessage)
assert.throws(() => validate(tx), ValidationError, errorMessage)
})
it(`throws w/ PriceDataSeries must have a PriceData object`, function () {
delete tx.PriceDataSeries[0].PriceData
const errorMessage =
'OracleSet: PriceDataSeries must have a `PriceData` object'
assert.throws(() => validateOracleSet(tx), ValidationError, errorMessage)
assert.throws(() => validate(tx), ValidationError, errorMessage)
})
it(`throws w/ PriceDataSeries must only have a single PriceData object`, function () {
tx.PriceDataSeries[0].ExtraProp = 'extraprop'
const errorMessage =
'OracleSet: PriceDataSeries must only have a single PriceData object'
assert.throws(() => validateOracleSet(tx), ValidationError, errorMessage)
assert.throws(() => validate(tx), ValidationError, errorMessage)
})
it(`throws w/ missing BaseAsset of PriceDataSeries`, function () {
delete tx.PriceDataSeries[0].PriceData.BaseAsset
const errorMessage =
'OracleSet: PriceDataSeries must have a `BaseAsset` string'
assert.throws(() => validateOracleSet(tx), ValidationError, errorMessage)
assert.throws(() => validate(tx), ValidationError, errorMessage)
})
it(`throws w/ missing QuoteAsset of PriceDataSeries`, function () {
delete tx.PriceDataSeries[0].PriceData.QuoteAsset
const errorMessage =
'OracleSet: PriceDataSeries must have a `QuoteAsset` string'
assert.throws(() => validateOracleSet(tx), ValidationError, errorMessage)
assert.throws(() => validate(tx), ValidationError, errorMessage)
})
it(`throws w/ missing AssetPrice with Scale present of PriceDataSeries`, function () {
delete tx.PriceDataSeries[0].PriceData.AssetPrice
const errorMessage =
'OracleSet: PriceDataSeries must have both `AssetPrice` and `Scale` if any are present'
assert.throws(() => validateOracleSet(tx), ValidationError, errorMessage)
assert.throws(() => validate(tx), ValidationError, errorMessage)
})
it(`throws w/ missing Scale with AssetPrice present of PriceDataSeries`, function () {
delete tx.PriceDataSeries[0].PriceData.Scale
const errorMessage =
'OracleSet: PriceDataSeries must have both `AssetPrice` and `Scale` if any are present'
assert.throws(() => validateOracleSet(tx), ValidationError, errorMessage)
assert.throws(() => validate(tx), ValidationError, errorMessage)
})
it(`throws w/ invalid AssetPrice of PriceDataSeries`, function () {
// 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)
})
it(`throws w/ invalid Scale of PriceDataSeries`, function () {
tx.PriceDataSeries[0].PriceData.Scale = '1234'
const errorMessage = 'OracleSet: invalid field Scale'
assert.throws(() => validateOracleSet(tx), ValidationError, errorMessage)
assert.throws(() => validate(tx), ValidationError, errorMessage)
})
it(`throws w/ Scale must be in range 0-10 when above max`, function () {
tx.PriceDataSeries[0].PriceData.Scale = 11
const errorMessage = 'OracleSet: Scale must be in range 0-10'
assert.throws(() => validateOracleSet(tx), ValidationError, errorMessage)
assert.throws(() => validate(tx), ValidationError, errorMessage)
})
it(`throws w/ Scale must be in range 0-10 when below min`, function () {
tx.PriceDataSeries[0].PriceData.Scale = -1
const errorMessage = 'OracleSet: Scale must be in range 0-10'
assert.throws(() => validateOracleSet(tx), ValidationError, errorMessage)
assert.throws(() => validate(tx), ValidationError, errorMessage)
})
})