Skip to content

Commit

Permalink
Support for PriceOracles (#701)
Browse files Browse the repository at this point in the history
Support for PriceOracle Amendment (XLS 47d)

---------

Co-authored-by: Omar Khan <[email protected]>
Co-authored-by: Mayukha Vadari <[email protected]>
Co-authored-by: pdp2121 <[email protected]>
  • Loading branch information
4 people authored May 9, 2024
1 parent 260d7dc commit 13e4012
Show file tree
Hide file tree
Showing 21 changed files with 1,020 additions and 21 deletions.
8 changes: 8 additions & 0 deletions .ci-config/rippled.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,11 @@ fixNFTokenRemint
# 2.0.0-b4 Amendments
XChainBridge
DID
# 2.2.0-b3 Amendments
fixNFTokenReserve
fixInnerObjTemplate
fixAMMOverflowOffer
PriceOracle
fixEmptyDID
fixXChainRewardRounding
fixPreviousTxnID
2 changes: 1 addition & 1 deletion .github/workflows/integration_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Integration test

env:
POETRY_VERSION: 1.4.2
RIPPLED_DOCKER_IMAGE: rippleci/rippled:2.0.0-b4
RIPPLED_DOCKER_IMAGE: rippleci/rippled:2.2.0-b3

on:
push:
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [[Unreleased]]
- Included `ctid` field in the `tx` request.

### Added
- Support for the Price Oracles amendment (XLS-47).

### Fixed
- Added support for `XChainModifyBridge` flag maps (fixing an issue with `NFTokenCreateOffer` flag names)
- Fixed `XChainModifyBridge` validation to allow just clearing of `MinAccountCreateAmount`
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ poetry run poe test_unit
To run integration tests, you'll need a standalone rippled node running with WS port `6006` and JSON RPC port `5005`. You can run a docker container for this:

```bash
docker run -p 5005:5005 -p 6006:6006 --interactive -t --volume $PWD/.ci-config:/opt/ripple/etc/ --platform linux/amd64 rippleci/rippled:2.0.0-b4 /opt/ripple/bin/rippled -a --conf /opt/ripple/etc/rippled.cfg
docker run -p 5005:5005 -p 6006:6006 --interactive -t --volume $PWD/.ci-config:/opt/ripple/etc/ --platform linux/amd64 rippleci/rippled:2.2.0-b3 /opt/ripple/bin/rippled -a --conf /opt/ripple/etc/rippled.cfg
```

Breaking down the command:
Expand Down
15 changes: 9 additions & 6 deletions tests/integration/it_utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Utility functions and variables for integration tests."""

import asyncio
import importlib
import inspect
Expand Down Expand Up @@ -246,7 +247,8 @@ def decorator(test_function):
# NOTE: passing `globals()` into `exec` is really bad practice and not safe at
# all, but in this case it's fine because it's only running test code

def _run_sync_test(self, client):
def _run_sync_test(self, client, value):
self.value = value
for i in range(num_retries):
try:
exec(
Expand All @@ -261,7 +263,8 @@ def _run_sync_test(self, client):
raise e
sleep(2)

async def _run_async_test(self, client):
async def _run_async_test(self, client, value):
self.value = value
if isinstance(client, AsyncWebsocketClient):
await client.open()
# this is happening with each test because IsolatedAsyncioTestCase is
Expand All @@ -285,16 +288,16 @@ def modified_test(self):
if not websockets_only:
with self.subTest(version="async", client="json"):
asyncio.run(
_run_async_test(self, _get_client(True, True, use_testnet))
_run_async_test(self, _get_client(True, True, use_testnet), 1)
)
with self.subTest(version="sync", client="json"):
_run_sync_test(self, _get_client(False, True, use_testnet))
_run_sync_test(self, _get_client(False, True, use_testnet), 2)
with self.subTest(version="async", client="websocket"):
asyncio.run(
_run_async_test(self, _get_client(True, False, use_testnet))
_run_async_test(self, _get_client(True, False, use_testnet), 3)
)
with self.subTest(version="sync", client="websocket"):
_run_sync_test(self, _get_client(False, False, use_testnet))
_run_sync_test(self, _get_client(False, False, use_testnet), 4)

return modified_test

Expand Down
59 changes: 59 additions & 0 deletions tests/integration/transactions/test_delete_oracle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import time

from tests.integration.integration_test_case import IntegrationTestCase
from tests.integration.it_utils import (
sign_and_reliable_submission_async,
test_async_and_sync,
)
from tests.integration.reusable_values import WALLET
from xrpl.models import AccountObjects, AccountObjectType, OracleDelete, OracleSet
from xrpl.models.response import ResponseStatus
from xrpl.models.transactions.oracle_set import PriceData
from xrpl.utils import str_to_hex

_PROVIDER = str_to_hex("chainlink")
_ASSET_CLASS = str_to_hex("currency")


class TestDeleteOracle(IntegrationTestCase):
@test_async_and_sync(globals())
async def test_basic(self, client):
oracle_id = self.value

# Create PriceOracle, to be deleted later
tx = OracleSet(
account=WALLET.address,
# unlike the integration tests for OracleSet transaction, we do not have to
# dynamically change the oracle_document_id for these integration tests.
# This is because the Oracle LedgerObject is deleted by the end of the test.
oracle_document_id=oracle_id,
provider=_PROVIDER,
asset_class=_ASSET_CLASS,
last_update_time=int(time.time()),
price_data_series=[
PriceData(
base_asset="XRP", quote_asset="USD", asset_price=740, scale=1
),
PriceData(
base_asset="BTC", quote_asset="EUR", asset_price=100, scale=2
),
],
)
response = await sign_and_reliable_submission_async(tx, WALLET, client)
self.assertEqual(response.status, ResponseStatus.SUCCESS)
self.assertEqual(response.result["engine_result"], "tesSUCCESS")

# Create PriceOracle to delete
tx = OracleDelete(
account=WALLET.address,
oracle_document_id=oracle_id,
)
response = await sign_and_reliable_submission_async(tx, WALLET, client)
self.assertEqual(response.status, ResponseStatus.SUCCESS)
self.assertEqual(response.result["engine_result"], "tesSUCCESS")

# confirm that the PriceOracle was actually deleted
account_objects_response = await client.request(
AccountObjects(account=WALLET.address, type=AccountObjectType.ORACLE)
)
self.assertEqual(len(account_objects_response.result["account_objects"]), 0)
53 changes: 53 additions & 0 deletions tests/integration/transactions/test_set_oracle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import time

from tests.integration.integration_test_case import IntegrationTestCase
from tests.integration.it_utils import (
sign_and_reliable_submission_async,
test_async_and_sync,
)
from tests.integration.reusable_values import WALLET
from xrpl.models import AccountObjects, AccountObjectType, OracleSet
from xrpl.models.response import ResponseStatus
from xrpl.models.transactions.oracle_set import PriceData
from xrpl.utils import str_to_hex

_PROVIDER = str_to_hex("provider")
_ASSET_CLASS = str_to_hex("currency")


class TestSetOracle(IntegrationTestCase):
@test_async_and_sync(globals())
async def test_all_fields(self, client):
tx = OracleSet(
account=WALLET.address,
# if oracle_document_id is not modified, the (sync, async) +
# (json, websocket) combination of integration tests will update the same
# oracle object using identical "LastUpdateTime". Updates to an oracle must
# be more recent than its previous LastUpdateTime
# a unique value is obtained for each combination of test run within the
# implementation of the test_async_and_sync decorator.
oracle_document_id=self.value,
provider=_PROVIDER,
asset_class=_ASSET_CLASS,
last_update_time=int(time.time()),
price_data_series=[
PriceData(
base_asset="XRP", quote_asset="USD", asset_price=740, scale=1
),
PriceData(
base_asset="BTC", quote_asset="EUR", asset_price=100, scale=2
),
],
)
response = await sign_and_reliable_submission_async(tx, WALLET, client)
self.assertEqual(response.status, ResponseStatus.SUCCESS)
self.assertEqual(response.result["engine_result"], "tesSUCCESS")

# confirm that the PriceOracle was actually created
account_objects_response = await client.request(
AccountObjects(account=WALLET.address, type=AccountObjectType.ORACLE)
)

# subsequent integration tests (sync/async + json/websocket) add one
# oracle object to the account
self.assertTrue(len(account_objects_response.result["account_objects"]) > 0)
68 changes: 68 additions & 0 deletions tests/unit/models/requests/test_get_aggregate_price.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
from unittest import TestCase

from xrpl.models import XRPLModelException
from xrpl.models.requests import GetAggregatePrice
from xrpl.models.requests.get_aggregate_price import Oracle

_ACCT_STR_1 = "rBwHKFS534tfG3mATXSycCnX8PAd3XJswj"
_ORACLE_DOC_ID_1 = 1

_ACCT_STR_2 = "rDMKwhm13oJBxBgiWS2SheZhKT5nZP8kez"
_ORACLE_DOC_ID_2 = 2


class TestGetAggregatePrice(TestCase):
def test_invalid_requests(self):
"""Unit test to validate invalid requests"""
with self.assertRaises(XRPLModelException):
# oracles array must contain at least one element
GetAggregatePrice(
base_asset="USD",
quote_asset="XRP",
oracles=[],
)

with self.assertRaises(XRPLModelException):
# base_asset is missing in the request
GetAggregatePrice(
quote_asset="XRP",
oracles=[
Oracle(account=_ACCT_STR_1, oracle_document_id=_ORACLE_DOC_ID_1),
Oracle(account=_ACCT_STR_2, oracle_document_id=_ORACLE_DOC_ID_2),
],
)

with self.assertRaises(XRPLModelException):
# quote_asset is missing in the request
GetAggregatePrice(
base_asset="USD",
oracles=[
Oracle(account=_ACCT_STR_1, oracle_document_id=_ORACLE_DOC_ID_1),
Oracle(account=_ACCT_STR_2, oracle_document_id=_ORACLE_DOC_ID_2),
],
)

def test_valid_request(self):
"""Unit test for validating archetypical requests"""
request = GetAggregatePrice(
base_asset="USD",
quote_asset="XRP",
oracles=[
Oracle(account=_ACCT_STR_1, oracle_document_id=_ORACLE_DOC_ID_1),
Oracle(account=_ACCT_STR_2, oracle_document_id=_ORACLE_DOC_ID_2),
],
)
self.assertTrue(request.is_valid())

# specifying trim and time_threshold value
request = GetAggregatePrice(
base_asset="USD",
quote_asset="XRP",
oracles=[
Oracle(account=_ACCT_STR_1, oracle_document_id=_ORACLE_DOC_ID_1),
Oracle(account=_ACCT_STR_2, oracle_document_id=_ORACLE_DOC_ID_2),
],
trim=20,
time_threshold=10,
)
self.assertTrue(request.is_valid())
35 changes: 34 additions & 1 deletion tests/unit/models/requests/test_ledger_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from xrpl.models import XRP, LedgerEntry, XChainBridge
from xrpl.models.exceptions import XRPLModelException
from xrpl.models.requests.ledger_entry import RippleState
from xrpl.models.requests.ledger_entry import Oracle, RippleState


class TestLedgerEntry(TestCase):
Expand Down Expand Up @@ -119,3 +119,36 @@ def test_has_multiple_query_params_is_invalid(self):
index="hello",
account_root="hello",
)

# fetch a valid PriceOracle object
def test_get_price_oracle(self):
# oracle_document_id is specified as uint
req = LedgerEntry(
oracle=Oracle(
account="rB6XJbxKx2oBSK1E3Hvh7KcZTCCBukWyhv",
oracle_document_id=1,
),
)
self.assertTrue(req.is_valid())

# oracle_document_id is specified as string
req = LedgerEntry(
oracle=Oracle(
account="rB6XJbxKx2oBSK1E3Hvh7KcZTCCBukWyhv",
oracle_document_id="1",
),
)
self.assertTrue(req.is_valid())

def test_invalid_price_oracle_object(self):
# missing oracle_document_id
with self.assertRaises(XRPLModelException):
LedgerEntry(
oracle=Oracle(account="rB6XJbxKx2oBSK1E3Hvh7KcZTCCBukWyhv"),
)

# missing account information
with self.assertRaises(XRPLModelException):
LedgerEntry(
oracle=Oracle(oracle_document_id=1),
)
14 changes: 14 additions & 0 deletions tests/unit/models/transactions/test_oracle_delete.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from unittest import TestCase

from xrpl.models.transactions import OracleDelete

_ACCOUNT = "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW"


class TestDeleteOracle(TestCase):
def test_valid(self):
tx = OracleDelete(
account=_ACCOUNT,
oracle_document_id=1,
)
self.assertTrue(tx.is_valid())
Loading

0 comments on commit 13e4012

Please sign in to comment.