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

Support for PriceOracles #701

Merged
merged 32 commits into from
May 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
30b6522
include SetOracle and DeleteOracle transactions
ckeshava Apr 18, 2024
5764dc4
update the definitions.json with Oracle LedgerEntry types, new SField…
ckeshava Apr 18, 2024
c8a2bb3
[WIP] rename transactions to OracleSet, OracleDelete to maintain comp…
ckeshava Apr 19, 2024
6919e22
update the CI/CD file to latest rippled docker image.
ckeshava Apr 22, 2024
1545fee
snippets test for set, delete oracle
ckeshava Apr 29, 2024
e811c41
[WIP] updates to the SetOracle integration test
ckeshava Apr 30, 2024
d2237c2
GetAggregatePrice request implementation, unit and snippet tests.
ckeshava Apr 30, 2024
6a16e6e
LedgerEntry unit tests -- validate the behavior of PriceOracle objects
ckeshava Apr 30, 2024
d698d28
integration tests for OracleSet, OracleDelete transactions
ckeshava Apr 30, 2024
dad085f
Fix: use int data type, not str for oracle_document_id
ckeshava Apr 30, 2024
2af51b1
update changelog
ckeshava Apr 30, 2024
170dafd
update rippled version docker image
ckeshava May 1, 2024
bcec52c
fix: Use TypedDict instead of NestedModel to represent OracleInfo
ckeshava May 1, 2024
df462ba
address Mayukha's comments: rename files and variables to match defs.…
ckeshava May 1, 2024
7256c4c
include LedgerEntry verification in oracles snippets test
ckeshava May 4, 2024
9c7ce5e
address review comments by mayukha and omar
ckeshava May 6, 2024
9d1b476
remove snippets tests for PriceOracle feature
ckeshava May 6, 2024
23b0ef0
Update CHANGELOG.md
ckeshava May 6, 2024
d08ba7f
add unique values to each client run
mvadari May 6, 2024
c044ce7
Merge branch 'async-value' of https://github.com/XRPLF/xrpl-py into p…
ckeshava May 6, 2024
1a02a58
remove the usage of random.randint in OracleSet integraton tests
ckeshava May 6, 2024
7dd3f17
Merge branch 'poracle' of https://github.com/ckeshava/xrpl-py into po…
ckeshava May 6, 2024
958ee1a
Update tests/integration/transactions/test_delete_oracle.py
ckeshava May 7, 2024
915ff35
address review comments
ckeshava May 7, 2024
3957867
use "is not None" to check for non-existence of an optional field
ckeshava May 7, 2024
8251981
fixed the error message in OracleSet validation
ckeshava May 8, 2024
56ff610
Update tests/unit/models/requests/test_ledger_entry.py
ckeshava May 8, 2024
7f39f50
Update tests/unit/models/requests/test_ledger_entry.py
ckeshava May 8, 2024
db3d27f
rename test files to match transaction names
ckeshava May 8, 2024
72afddf
validate the exception message in negative test cases
ckeshava May 9, 2024
49370f1
include validity checks on LastUpdateTime
ckeshava May 9, 2024
e98980b
Merge branch 'main' into poracle
ckeshava May 9, 2024
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
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,
ckeshava marked this conversation as resolved.
Show resolved Hide resolved
provider=_PROVIDER,
asset_class=_ASSET_CLASS,
last_update_time=int(time.time()),
ckeshava marked this conversation as resolved.
Show resolved Hide resolved
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
Loading