From 30b65224252f4c2babbb139cef761670162d44e6 Mon Sep 17 00:00:00 2001 From: Chenna Keshava Date: Thu, 18 Apr 2024 13:16:50 -0700 Subject: [PATCH 01/29] include SetOracle and DeleteOracle transactions --- .../unit/models/requests/test_ledger_entry.py | 11 ++++- xrpl/models/requests/__init__.py | 1 + xrpl/models/requests/account_objects.py | 1 + xrpl/models/requests/ledger_entry.py | 26 ++++++++++++ xrpl/models/transactions/__init__.py | 4 ++ xrpl/models/transactions/delete_oracle.py | 25 +++++++++++ xrpl/models/transactions/set_oracle.py | 42 +++++++++++++++++++ .../transactions/types/transaction_type.py | 2 + 8 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 xrpl/models/transactions/delete_oracle.py create mode 100644 xrpl/models/transactions/set_oracle.py diff --git a/tests/unit/models/requests/test_ledger_entry.py b/tests/unit/models/requests/test_ledger_entry.py index 513ef9d1e..a250572da 100644 --- a/tests/unit/models/requests/test_ledger_entry.py +++ b/tests/unit/models/requests/test_ledger_entry.py @@ -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): @@ -30,6 +30,15 @@ def test_has_only_offer_is_valid(self): ) self.assertTrue(req.is_valid()) + def test_has_only_price_oracle_is_valid(self): + req = LedgerEntry( + ripple_state=Oracle( + account="account1", + oracle_document_id=1, + ), + ) + self.assertTrue(req.is_valid()) + def test_has_only_ripple_state_is_valid(self): req = LedgerEntry( ripple_state=RippleState( diff --git a/xrpl/models/requests/__init__.py b/xrpl/models/requests/__init__.py index 32b666a77..58ba3b2ba 100644 --- a/xrpl/models/requests/__init__.py +++ b/xrpl/models/requests/__init__.py @@ -66,6 +66,7 @@ "Fee", "GatewayBalances", "GenericRequest", + # "GetAggregatePrice", "Ledger", "LedgerClosed", "LedgerCurrent", diff --git a/xrpl/models/requests/account_objects.py b/xrpl/models/requests/account_objects.py index 9a6e50961..65e2f57da 100644 --- a/xrpl/models/requests/account_objects.py +++ b/xrpl/models/requests/account_objects.py @@ -26,6 +26,7 @@ class AccountObjectType(str, Enum): ESCROW = "escrow" NFT_OFFER = "nft_offer" OFFER = "offer" + ORACLE = "oracle" PAYMENT_CHANNEL = "payment_channel" SIGNER_LIST = "signer_list" STATE = "state" diff --git a/xrpl/models/requests/ledger_entry.py b/xrpl/models/requests/ledger_entry.py index 374dfe76e..8fe2f8bea 100644 --- a/xrpl/models/requests/ledger_entry.py +++ b/xrpl/models/requests/ledger_entry.py @@ -32,6 +32,7 @@ class LedgerEntryType(str, Enum): FEE = "fee" HASHES = "hashes" OFFER = "offer" + ORACLE = "oracle" PAYMENT_CHANNEL = "payment_channel" SIGNER_LIST = "signer_list" STATE = "state" @@ -132,6 +133,29 @@ class Offer(BaseModel): """ +@require_kwargs_on_init +@dataclass(frozen=True) +class Oracle(BaseModel): + """ + Required fields for requesting a Price Oracle Ledger Entry, if not querying by + object ID. + """ + + account: str = REQUIRED # type: ignore + """ + This field is required. + + :meta hide-value: + """ + + oracle_document_id: Union[str, int] = REQUIRED # type: ignore + """ + This field is required. + + :meta hide-value: + """ + + @require_kwargs_on_init @dataclass(frozen=True) class RippleState(BaseModel): @@ -223,6 +247,7 @@ class LedgerEntry(Request, LookupByLedgerRequest): directory: Optional[Union[str, Directory]] = None escrow: Optional[Union[str, Escrow]] = None offer: Optional[Union[str, Offer]] = None + oracle: Optional[Oracle] = None payment_channel: Optional[str] = None ripple_state: Optional[RippleState] = None ticket: Optional[Union[str, Ticket]] = None @@ -250,6 +275,7 @@ def _get_errors(self: LedgerEntry) -> Dict[str, str]: self.directory, self.escrow, self.offer, + self.oracle, self.payment_channel, self.ripple_state, self.ticket, diff --git a/xrpl/models/transactions/__init__.py b/xrpl/models/transactions/__init__.py index 498bd4832..2073d7b76 100644 --- a/xrpl/models/transactions/__init__.py +++ b/xrpl/models/transactions/__init__.py @@ -28,6 +28,7 @@ from xrpl.models.transactions.check_cash import CheckCash from xrpl.models.transactions.check_create import CheckCreate from xrpl.models.transactions.clawback import Clawback +from xrpl.models.transactions.delete_oracle import DeleteOracle from xrpl.models.transactions.deposit_preauth import DepositPreauth from xrpl.models.transactions.did_delete import DIDDelete from xrpl.models.transactions.did_set import DIDSet @@ -62,6 +63,7 @@ ) from xrpl.models.transactions.payment_channel_create import PaymentChannelCreate from xrpl.models.transactions.payment_channel_fund import PaymentChannelFund +from xrpl.models.transactions.set_oracle import SetOracle from xrpl.models.transactions.set_regular_key import SetRegularKey from xrpl.models.transactions.signer_list_set import SignerEntry, SignerListSet from xrpl.models.transactions.ticket_create import TicketCreate @@ -111,6 +113,7 @@ "CheckCash", "CheckCreate", "Clawback", + "DeleteOracle", "DepositPreauth", "DIDDelete", "DIDSet", @@ -139,6 +142,7 @@ "PaymentChannelFund", "PaymentFlag", "PaymentFlagInterface", + "SetOracle", "SetRegularKey", "Signer", "SignerEntry", diff --git a/xrpl/models/transactions/delete_oracle.py b/xrpl/models/transactions/delete_oracle.py new file mode 100644 index 000000000..e79214f9d --- /dev/null +++ b/xrpl/models/transactions/delete_oracle.py @@ -0,0 +1,25 @@ +"""Model for DeleteOracle transaction type.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Union + +from xrpl.models.required import REQUIRED +from xrpl.models.transactions.transaction import Transaction +from xrpl.models.transactions.types import TransactionType +from xrpl.models.utils import require_kwargs_on_init + + +@require_kwargs_on_init +@dataclass(frozen=True) +class DeleteOracle(Transaction): + """Represents a DeleteOracle transaction.""" + + account: str = REQUIRED # type: ignore + oracle_document_id: Union[int, str] = REQUIRED # type: ignore + + transaction_type: TransactionType = field( + default=TransactionType.DELETE_ORACLE, + init=False, + ) diff --git a/xrpl/models/transactions/set_oracle.py b/xrpl/models/transactions/set_oracle.py new file mode 100644 index 000000000..db38a7cd5 --- /dev/null +++ b/xrpl/models/transactions/set_oracle.py @@ -0,0 +1,42 @@ +"""Model for SetOracle transaction type.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import List, Optional + +from xrpl.models.base_model import BaseModel +from xrpl.models.required import REQUIRED +from xrpl.models.transactions.transaction import Transaction +from xrpl.models.transactions.types import TransactionType +from xrpl.models.utils import require_kwargs_on_init + + +@require_kwargs_on_init +@dataclass(frozen=True) +class SetOracle(Transaction): + """Represents a SetOracle transaction.""" + + account: str = REQUIRED # type: ignore + oracle_document_id: int = REQUIRED # type: ignore + provider: Optional[str] = None + uri: Optional[str] = None + asset_class: Optional[str] = None + last_update_time: int = REQUIRED # type: ignore + price_data_series: List[PriceData] = REQUIRED # type: ignore + + transaction_type: TransactionType = field( + default=TransactionType.SET_ORACLE, + init=False, + ) + + +@require_kwargs_on_init +@dataclass(frozen=True) +class PriceData(BaseModel): + """Represents one PriceData element. It is used in SetOracle transaction""" + + base_asset: str = REQUIRED # type: ignore + quote_asset: str = REQUIRED # type: ignore + asset_price: Optional[int] = None + scale: Optional[int] = None diff --git a/xrpl/models/transactions/types/transaction_type.py b/xrpl/models/transactions/types/transaction_type.py index 5520d6087..9ccb67ee0 100644 --- a/xrpl/models/transactions/types/transaction_type.py +++ b/xrpl/models/transactions/types/transaction_type.py @@ -18,6 +18,7 @@ class TransactionType(str, Enum): CHECK_CASH = "CheckCash" CHECK_CREATE = "CheckCreate" CLAWBACK = "Clawback" + DELETE_ORACLE = "DeleteOracle" DEPOSIT_PREAUTH = "DepositPreauth" DID_DELETE = "DIDDelete" DID_SET = "DIDSet" @@ -35,6 +36,7 @@ class TransactionType(str, Enum): PAYMENT_CHANNEL_CLAIM = "PaymentChannelClaim" PAYMENT_CHANNEL_CREATE = "PaymentChannelCreate" PAYMENT_CHANNEL_FUND = "PaymentChannelFund" + SET_ORACLE = "SetOracle" SET_REGULAR_KEY = "SetRegularKey" SIGNER_LIST_SET = "SignerListSet" TICKET_CREATE = "TicketCreate" From 5764dc4abdaa5a2dc4ccd36908e694d1a5afc257 Mon Sep 17 00:00:00 2001 From: Chenna Keshava Date: Thu, 18 Apr 2024 13:25:54 -0700 Subject: [PATCH 02/29] update the definitions.json with Oracle LedgerEntry types, new SFields, tecError codes --- .../binarycodec/definitions/definitions.json | 134 ++++++++++++++++-- 1 file changed, 122 insertions(+), 12 deletions(-) diff --git a/xrpl/core/binarycodec/definitions/definitions.json b/xrpl/core/binarycodec/definitions/definitions.json index c98e9a988..797be9ce2 100644 --- a/xrpl/core/binarycodec/definitions/definitions.json +++ b/xrpl/core/binarycodec/definitions/definitions.json @@ -23,6 +23,7 @@ "UInt512": 23, "Issue": 24, "XChainBridge": 25, + "Currency": 26, "Transaction": 10001, "LedgerEntry": 10002, "Validation": 10003, @@ -51,6 +52,7 @@ "NFTokenOffer": 55, "AMM": 121, "DID": 73, + "Oracle": 128, "Any": -3, "Child": -2, "Nickname": 110, @@ -141,40 +143,40 @@ [ "LedgerEntry", { - "nth": 1, + "nth": 257, "isVLEncoded": false, "isSerialized": false, - "isSigningField": true, + "isSigningField": false, "type": "LedgerEntry" } ], [ "Transaction", { - "nth": 1, + "nth": 257, "isVLEncoded": false, "isSerialized": false, - "isSigningField": true, + "isSigningField": false, "type": "Transaction" } ], [ "Validation", { - "nth": 1, + "nth": 257, "isVLEncoded": false, "isSerialized": false, - "isSigningField": true, + "isSigningField": false, "type": "Validation" } ], [ "Metadata", { - "nth": 1, + "nth": 257, "isVLEncoded": false, - "isSerialized": true, - "isSigningField": true, + "isSerialized": false, + "isSigningField": false, "type": "Metadata" } ], @@ -208,6 +210,16 @@ "type": "UInt8" } ], + [ + "Scale", + { + "nth": 4, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt8" + } + ], [ "TickSize", { @@ -498,6 +510,16 @@ "type": "UInt32" } ], + [ + "LastUpdateTime", + { + "nth": 15, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], [ "HighQualityIn", { @@ -828,6 +850,16 @@ "type": "UInt32" } ], + [ + "OracleDocumentID", + { + "nth": 51, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], [ "IndexNext", { @@ -1028,6 +1060,16 @@ "type": "UInt64" } ], + [ + "AssetPrice", + { + "nth": 23, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt64" + } + ], [ "EmailHash", { @@ -1918,6 +1960,26 @@ "type": "Blob" } ], + [ + "AssetClass", + { + "nth": 28, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "Blob" + } + ], + [ + "Provider", + { + "nth": 29, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "Blob" + } + ], [ "Account", { @@ -2128,6 +2190,26 @@ "type": "PathSet" } ], + [ + "BaseAsset", + { + "nth": 1, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Currency" + } + ], + [ + "QuoteAsset", + { + "nth": 2, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Currency" + } + ], [ "LockingChainIssue", { @@ -2458,6 +2540,16 @@ "type": "STObject" } ], + [ + "PriceData", + { + "nth": 32, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STObject" + } + ], [ "Signers", { @@ -2628,6 +2720,16 @@ "type": "STArray" } ], + [ + "PriceDataSeries", + { + "nth": 24, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STArray" + } + ], [ "AuthAccounts", { @@ -2656,6 +2758,7 @@ "telWRONG_NETWORK": -386, "telREQUIRES_NETWORK_ID": -385, "telNETWORK_ID_MAKES_TX_NON_CANONICAL": -384, + "telENV_RPC_FAILED": -383, "temMALFORMED": -299, "temBAD_AMOUNT": -298, @@ -2703,6 +2806,8 @@ "temXCHAIN_BRIDGE_BAD_MIN_ACCOUNT_CREATE_AMOUNT": -256, "temXCHAIN_BRIDGE_BAD_REWARD_AMOUNT": -255, "temEMPTY_DID": -254, + "temARRAY_EMPTY": -253, + "temARRAY_TOO_LARGE": -252, "tefFAILURE": -199, "tefALREADY": -198, @@ -2739,7 +2844,6 @@ "terQUEUED": -89, "terPRE_TICKET": -88, "terNO_AMM": -87, - "terSUBMITTED": -86, "tesSUCCESS": 0, @@ -2781,7 +2885,7 @@ "tecKILLED": 150, "tecHAS_OBLIGATIONS": 151, "tecTOO_SOON": 152, - "tecHOOK_ERROR": 153, + "tecHOOK_REJECTED": 153, "tecMAX_SEQUENCE_REACHED": 154, "tecNO_SUITABLE_NFTOKEN_PAGE": 155, "tecNFTOKEN_BUY_SELL_MISMATCH": 156, @@ -2815,7 +2919,11 @@ "tecXCHAIN_SELF_COMMIT": 184, "tecXCHAIN_BAD_PUBLIC_KEY_ACCOUNT_PAIR": 185, "tecXCHAIN_CREATE_ACCOUNT_DISABLED": 186, - "tecEMPTY_DID": 187 + "tecEMPTY_DID": 187, + "tecINVALID_UPDATE_TIME": 188, + "tecTOKEN_PAIR_NOT_FOUND": 189, + "tecARRAY_EMPTY": 190, + "tecARRAY_TOO_LARGE": 191 }, "TRANSACTION_TYPES": { "Invalid": -1, @@ -2864,6 +2972,8 @@ "XChainCreateBridge": 48, "DIDSet": 49, "DIDDelete": 50, + "OracleSet": 51, + "OracleDelete": 52, "EnableAmendment": 100, "SetFee": 101, "UNLModify": 102 From c8a2bb310fa975ddff3444d62082106876d78ed2 Mon Sep 17 00:00:00 2001 From: Chenna Keshava Date: Thu, 18 Apr 2024 20:25:37 -0700 Subject: [PATCH 03/29] [WIP] rename transactions to OracleSet, OracleDelete to maintain compatibility with definitions.json, update integration tests --- .../transactions/test_delete_oracle.py | 47 +++++++++++++++++++ .../transactions/test_set_oracle.py | 37 +++++++++++++++ .../models/transactions/test_delete_oracle.py | 14 ++++++ .../models/transactions/test_set_oracle.py | 32 +++++++++++++ .../binarycodec/definitions/definitions.py | 2 + xrpl/models/transactions/__init__.py | 8 ++-- xrpl/models/transactions/delete_oracle.py | 6 +-- xrpl/models/transactions/set_oracle.py | 8 ++-- .../transactions/types/transaction_type.py | 4 +- 9 files changed, 145 insertions(+), 13 deletions(-) create mode 100644 tests/integration/transactions/test_delete_oracle.py create mode 100644 tests/integration/transactions/test_set_oracle.py create mode 100644 tests/unit/models/transactions/test_delete_oracle.py create mode 100644 tests/unit/models/transactions/test_set_oracle.py diff --git a/tests/integration/transactions/test_delete_oracle.py b/tests/integration/transactions/test_delete_oracle.py new file mode 100644 index 000000000..568d46e26 --- /dev/null +++ b/tests/integration/transactions/test_delete_oracle.py @@ -0,0 +1,47 @@ +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 + +_PROVIDER = "chainlink" +_ASSET_CLASS = "currency" + + +class TestDeleteOracle(IntegrationTestCase): + @test_async_and_sync(globals()) + async def test_basic(self, client): + # Create PriceOracle to delete + setup_tx = OracleSet( + account=WALLET.address, + oracle_document_id=1, + provider=_PROVIDER, + asset_class=_ASSET_CLASS, + ) + response = await sign_and_reliable_submission_async(setup_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) + ) + self.assertEqual(len(account_objects_response.result["account_objects"]), 1) + + # Create PriceOracle to delete + tx = OracleDelete( + account=WALLET.address, + oracle_document_id=1, + ) + 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) diff --git a/tests/integration/transactions/test_set_oracle.py b/tests/integration/transactions/test_set_oracle.py new file mode 100644 index 000000000..d705d45ef --- /dev/null +++ b/tests/integration/transactions/test_set_oracle.py @@ -0,0 +1,37 @@ +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 + +_PROVIDER = "chainlink" +_ASSET_CLASS = "currency" + + +class TestSetOracle(IntegrationTestCase): + @test_async_and_sync(globals()) + async def test_all_fields(self, client): + tx = OracleSet( + account=WALLET.address, + oracle_document_id=1, + provider=_PROVIDER, + asset_class=_ASSET_CLASS, + last_update_time=int(time.time()), + price_data_series=[], + ) + 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) + ) + + # CK: Use a more stringent check to validate the Oracle object + self.assertEqual(len(account_objects_response.result["account_objects"]), 1) diff --git a/tests/unit/models/transactions/test_delete_oracle.py b/tests/unit/models/transactions/test_delete_oracle.py new file mode 100644 index 000000000..48e259d06 --- /dev/null +++ b/tests/unit/models/transactions/test_delete_oracle.py @@ -0,0 +1,14 @@ +from unittest import TestCase + +from xrpl.models.transactions import OracleDelete + +_ACCOUNT = "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW" + + +class TestSetOracle(TestCase): + def test_valid(self): + tx = OracleDelete( + account=_ACCOUNT, + oracle_document_id=1, + ) + self.assertTrue(tx.is_valid()) diff --git a/tests/unit/models/transactions/test_set_oracle.py b/tests/unit/models/transactions/test_set_oracle.py new file mode 100644 index 000000000..e9fef6d5f --- /dev/null +++ b/tests/unit/models/transactions/test_set_oracle.py @@ -0,0 +1,32 @@ +import time +from unittest import TestCase + +from xrpl.models.exceptions import XRPLModelException +from xrpl.models.transactions import OracleSet + +_ACCOUNT = "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW" +_PROVIDER = "chainlink" +_ASSET_CLASS = "currency" + + +class TestSetOracle(TestCase): + def test_valid(self): + tx = OracleSet( + account=_ACCOUNT, + oracle_document_id=1, + provider=_PROVIDER, + asset_class=_ASSET_CLASS, + last_update_time=int(time.time()), + price_data_series=[], + ) + self.assertTrue(tx.is_valid()) + + def test_missing_data_series(self): + with self.assertRaises(XRPLModelException): + OracleSet( + account=_ACCOUNT, + oracle_document_id=1, + provider=_PROVIDER, + asset_class=_ASSET_CLASS, + last_update_time=int(time.time()), + ) diff --git a/xrpl/core/binarycodec/definitions/definitions.py b/xrpl/core/binarycodec/definitions/definitions.py index e82608a09..a67214d20 100644 --- a/xrpl/core/binarycodec/definitions/definitions.py +++ b/xrpl/core/binarycodec/definitions/definitions.py @@ -191,6 +191,8 @@ def get_transaction_type_code(transaction_type: str) -> int: Returns: An integer representing the given transaction type string in an enum. """ + print("Available definitions.json transactions: ") + print(_DEFINITIONS["TRANSACTION_TYPES"]) return cast(int, _DEFINITIONS["TRANSACTION_TYPES"][transaction_type]) diff --git a/xrpl/models/transactions/__init__.py b/xrpl/models/transactions/__init__.py index 2073d7b76..0b3f83e17 100644 --- a/xrpl/models/transactions/__init__.py +++ b/xrpl/models/transactions/__init__.py @@ -28,7 +28,7 @@ from xrpl.models.transactions.check_cash import CheckCash from xrpl.models.transactions.check_create import CheckCreate from xrpl.models.transactions.clawback import Clawback -from xrpl.models.transactions.delete_oracle import DeleteOracle +from xrpl.models.transactions.delete_oracle import OracleDelete from xrpl.models.transactions.deposit_preauth import DepositPreauth from xrpl.models.transactions.did_delete import DIDDelete from xrpl.models.transactions.did_set import DIDSet @@ -63,7 +63,7 @@ ) from xrpl.models.transactions.payment_channel_create import PaymentChannelCreate from xrpl.models.transactions.payment_channel_fund import PaymentChannelFund -from xrpl.models.transactions.set_oracle import SetOracle +from xrpl.models.transactions.set_oracle import OracleSet from xrpl.models.transactions.set_regular_key import SetRegularKey from xrpl.models.transactions.signer_list_set import SignerEntry, SignerListSet from xrpl.models.transactions.ticket_create import TicketCreate @@ -113,7 +113,7 @@ "CheckCash", "CheckCreate", "Clawback", - "DeleteOracle", + "OracleDelete", "DepositPreauth", "DIDDelete", "DIDSet", @@ -142,7 +142,7 @@ "PaymentChannelFund", "PaymentFlag", "PaymentFlagInterface", - "SetOracle", + "OracleSet", "SetRegularKey", "Signer", "SignerEntry", diff --git a/xrpl/models/transactions/delete_oracle.py b/xrpl/models/transactions/delete_oracle.py index e79214f9d..6397fad1b 100644 --- a/xrpl/models/transactions/delete_oracle.py +++ b/xrpl/models/transactions/delete_oracle.py @@ -1,4 +1,4 @@ -"""Model for DeleteOracle transaction type.""" +"""Model for OracleDelete transaction type.""" from __future__ import annotations @@ -13,8 +13,8 @@ @require_kwargs_on_init @dataclass(frozen=True) -class DeleteOracle(Transaction): - """Represents a DeleteOracle transaction.""" +class OracleDelete(Transaction): + """Represents a OracleDelete transaction.""" account: str = REQUIRED # type: ignore oracle_document_id: Union[int, str] = REQUIRED # type: ignore diff --git a/xrpl/models/transactions/set_oracle.py b/xrpl/models/transactions/set_oracle.py index db38a7cd5..afffa543f 100644 --- a/xrpl/models/transactions/set_oracle.py +++ b/xrpl/models/transactions/set_oracle.py @@ -1,4 +1,4 @@ -"""Model for SetOracle transaction type.""" +"""Model for OracleSet transaction type.""" from __future__ import annotations @@ -14,8 +14,8 @@ @require_kwargs_on_init @dataclass(frozen=True) -class SetOracle(Transaction): - """Represents a SetOracle transaction.""" +class OracleSet(Transaction): + """Represents a OracleSet transaction.""" account: str = REQUIRED # type: ignore oracle_document_id: int = REQUIRED # type: ignore @@ -34,7 +34,7 @@ class SetOracle(Transaction): @require_kwargs_on_init @dataclass(frozen=True) class PriceData(BaseModel): - """Represents one PriceData element. It is used in SetOracle transaction""" + """Represents one PriceData element. It is used in OracleSet transaction""" base_asset: str = REQUIRED # type: ignore quote_asset: str = REQUIRED # type: ignore diff --git a/xrpl/models/transactions/types/transaction_type.py b/xrpl/models/transactions/types/transaction_type.py index 9ccb67ee0..309dfdc49 100644 --- a/xrpl/models/transactions/types/transaction_type.py +++ b/xrpl/models/transactions/types/transaction_type.py @@ -18,7 +18,7 @@ class TransactionType(str, Enum): CHECK_CASH = "CheckCash" CHECK_CREATE = "CheckCreate" CLAWBACK = "Clawback" - DELETE_ORACLE = "DeleteOracle" + DELETE_ORACLE = "OracleDelete" DEPOSIT_PREAUTH = "DepositPreauth" DID_DELETE = "DIDDelete" DID_SET = "DIDSet" @@ -36,7 +36,7 @@ class TransactionType(str, Enum): PAYMENT_CHANNEL_CLAIM = "PaymentChannelClaim" PAYMENT_CHANNEL_CREATE = "PaymentChannelCreate" PAYMENT_CHANNEL_FUND = "PaymentChannelFund" - SET_ORACLE = "SetOracle" + SET_ORACLE = "OracleSet" SET_REGULAR_KEY = "SetRegularKey" SIGNER_LIST_SET = "SignerListSet" TICKET_CREATE = "TicketCreate" From 6919e22b0949b037d4357ba864246b90bd232ba4 Mon Sep 17 00:00:00 2001 From: Chenna Keshava Date: Mon, 22 Apr 2024 13:38:25 -0700 Subject: [PATCH 04/29] update the CI/CD file to latest rippled docker image. remove extraneous debug print statements --- .ci-config/rippled.cfg | 2 ++ xrpl/core/binarycodec/definitions/definitions.py | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.ci-config/rippled.cfg b/.ci-config/rippled.cfg index 78b568e90..44b9e5d7c 100644 --- a/.ci-config/rippled.cfg +++ b/.ci-config/rippled.cfg @@ -170,3 +170,5 @@ fixNFTokenRemint # 2.0.0-b4 Amendments XChainBridge DID +# 2.2.0-b1 Amendments +PriceOracle diff --git a/xrpl/core/binarycodec/definitions/definitions.py b/xrpl/core/binarycodec/definitions/definitions.py index a67214d20..e82608a09 100644 --- a/xrpl/core/binarycodec/definitions/definitions.py +++ b/xrpl/core/binarycodec/definitions/definitions.py @@ -191,8 +191,6 @@ def get_transaction_type_code(transaction_type: str) -> int: Returns: An integer representing the given transaction type string in an enum. """ - print("Available definitions.json transactions: ") - print(_DEFINITIONS["TRANSACTION_TYPES"]) return cast(int, _DEFINITIONS["TRANSACTION_TYPES"][transaction_type]) From 1545fee85c5d126f4b9ba94373ad5f576815bd14 Mon Sep 17 00:00:00 2001 From: Chenna Keshava Date: Mon, 29 Apr 2024 15:48:09 -0700 Subject: [PATCH 05/29] snippets test for set, delete oracle --- snippets/oracle.py | 65 +++++++++++++++++++++++ xrpl/models/transactions/delete_oracle.py | 5 +- xrpl/models/transactions/set_oracle.py | 10 +++- 3 files changed, 75 insertions(+), 5 deletions(-) create mode 100644 snippets/oracle.py diff --git a/snippets/oracle.py b/snippets/oracle.py new file mode 100644 index 000000000..1e76eff46 --- /dev/null +++ b/snippets/oracle.py @@ -0,0 +1,65 @@ +"""Example of how we can set up an escrow""" +import time + +from xrpl.clients import JsonRpcClient +from xrpl.models.transactions.delete_oracle import OracleDelete +from xrpl.models.transactions.set_oracle import OracleSet, PriceData +from xrpl.transaction.reliable_submission import submit_and_wait +from xrpl.utils import str_to_hex +from xrpl.wallet import generate_faucet_wallet + +# Create a client to connect to the dev-network +client = JsonRpcClient("https://s.devnet.rippletest.net:51234") + +wallet = generate_faucet_wallet(client, debug=True) + + +_PROVIDER = str_to_hex("provider") +_ASSET_CLASS = str_to_hex("currency") +_ORACLE_DOC_ID = 1 + +create_tx = OracleSet( + account=wallet.address, + oracle_document_id=_ORACLE_DOC_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 = submit_and_wait(create_tx, client, wallet) +# TODO: Keshava +# print(response.result['meta']['TransactionResult'] == 'tesSUCCESS') # does not work +print( + "Result of SetOracle transaction: " + response.result["meta"]["TransactionResult"] +) + +# update the oracle data +update_tx = OracleSet( + account=wallet.address, + oracle_document_id=_ORACLE_DOC_ID, + last_update_time=int(time.time()), + price_data_series=[ + PriceData(base_asset="XRP", quote_asset="USD", asset_price=742, scale=1), + PriceData(base_asset="BTC", quote_asset="EUR", asset_price=103, scale=2), + ], +) +response = submit_and_wait(update_tx, client, wallet) + +print( + "Result of the Update Oracle transaction: " + + response.result["meta"]["TransactionResult"] +) + + +# delete the oracle +delete_tx = OracleDelete(account=wallet.address, oracle_document_id=_ORACLE_DOC_ID) +response = submit_and_wait(delete_tx, client, wallet) + +print( + "Result of DeleteOracle transaction: " + + response.result["meta"]["TransactionResult"] +) diff --git a/xrpl/models/transactions/delete_oracle.py b/xrpl/models/transactions/delete_oracle.py index 6397fad1b..34e398d03 100644 --- a/xrpl/models/transactions/delete_oracle.py +++ b/xrpl/models/transactions/delete_oracle.py @@ -3,7 +3,6 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import Union from xrpl.models.required import REQUIRED from xrpl.models.transactions.transaction import Transaction @@ -14,10 +13,10 @@ @require_kwargs_on_init @dataclass(frozen=True) class OracleDelete(Transaction): - """Represents a OracleDelete transaction.""" + """Represents an OracleDelete transaction.""" account: str = REQUIRED # type: ignore - oracle_document_id: Union[int, str] = REQUIRED # type: ignore + oracle_document_id: int = REQUIRED # type: ignore transaction_type: TransactionType = field( default=TransactionType.DELETE_ORACLE, diff --git a/xrpl/models/transactions/set_oracle.py b/xrpl/models/transactions/set_oracle.py index afffa543f..10b5130ea 100644 --- a/xrpl/models/transactions/set_oracle.py +++ b/xrpl/models/transactions/set_oracle.py @@ -5,7 +5,7 @@ from dataclasses import dataclass, field from typing import List, Optional -from xrpl.models.base_model import BaseModel +from xrpl.models.nested_model import NestedModel from xrpl.models.required import REQUIRED from xrpl.models.transactions.transaction import Transaction from xrpl.models.transactions.types import TransactionType @@ -19,9 +19,15 @@ class OracleSet(Transaction): account: str = REQUIRED # type: ignore oracle_document_id: int = REQUIRED # type: ignore + + """ + The below three fields must be hex-encoded. You can + use `xrpl.utils.str_to_hex` to convert a UTF-8 string to hex. + """ provider: Optional[str] = None uri: Optional[str] = None asset_class: Optional[str] = None + last_update_time: int = REQUIRED # type: ignore price_data_series: List[PriceData] = REQUIRED # type: ignore @@ -33,7 +39,7 @@ class OracleSet(Transaction): @require_kwargs_on_init @dataclass(frozen=True) -class PriceData(BaseModel): +class PriceData(NestedModel): """Represents one PriceData element. It is used in OracleSet transaction""" base_asset: str = REQUIRED # type: ignore From e811c415f87fcc4495aa5ce8e176e419fe9dec54 Mon Sep 17 00:00:00 2001 From: Chenna Keshava Date: Mon, 29 Apr 2024 17:08:59 -0700 Subject: [PATCH 06/29] [WIP] updates to the SetOracle integration test --- .../integration/transactions/test_set_oracle.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/integration/transactions/test_set_oracle.py b/tests/integration/transactions/test_set_oracle.py index d705d45ef..bcffc87e4 100644 --- a/tests/integration/transactions/test_set_oracle.py +++ b/tests/integration/transactions/test_set_oracle.py @@ -8,9 +8,11 @@ from tests.integration.reusable_values import WALLET from xrpl.models import AccountObjects, AccountObjectType, OracleSet from xrpl.models.response import ResponseStatus +from xrpl.models.transactions.set_oracle import PriceData +from xrpl.utils import str_to_hex -_PROVIDER = "chainlink" -_ASSET_CLASS = "currency" +_PROVIDER = str_to_hex("provider") +_ASSET_CLASS = str_to_hex("currency") class TestSetOracle(IntegrationTestCase): @@ -22,7 +24,14 @@ async def test_all_fields(self, client): provider=_PROVIDER, asset_class=_ASSET_CLASS, last_update_time=int(time.time()), - price_data_series=[], + 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) @@ -33,5 +42,4 @@ async def test_all_fields(self, client): AccountObjects(account=WALLET.address, type=AccountObjectType.ORACLE) ) - # CK: Use a more stringent check to validate the Oracle object self.assertEqual(len(account_objects_response.result["account_objects"]), 1) From d2237c25155579381e2b1e0079e2599498b74c18 Mon Sep 17 00:00:00 2001 From: Chenna Keshava Date: Mon, 29 Apr 2024 17:11:12 -0700 Subject: [PATCH 07/29] GetAggregatePrice request implementation, unit and snippet tests. --- snippets/oracle.py | 2 +- snippets/oracle_aggregate.py | 48 ++++++++++++ .../requests/test_get_aggregate_price.py | 76 +++++++++++++++++++ xrpl/models/requests/__init__.py | 3 +- xrpl/models/requests/get_aggregate_price.py | 54 +++++++++++++ xrpl/models/requests/request.py | 3 + 6 files changed, 184 insertions(+), 2 deletions(-) create mode 100644 snippets/oracle_aggregate.py create mode 100644 tests/unit/models/requests/test_get_aggregate_price.py create mode 100644 xrpl/models/requests/get_aggregate_price.py diff --git a/snippets/oracle.py b/snippets/oracle.py index 1e76eff46..94fe7b8a7 100644 --- a/snippets/oracle.py +++ b/snippets/oracle.py @@ -1,4 +1,4 @@ -"""Example of how we can set up an escrow""" +"""Example of how we can create, update and delete oracles""" import time from xrpl.clients import JsonRpcClient diff --git a/snippets/oracle_aggregate.py b/snippets/oracle_aggregate.py new file mode 100644 index 000000000..a0d5bb596 --- /dev/null +++ b/snippets/oracle_aggregate.py @@ -0,0 +1,48 @@ +"""Snippet demonstrates obtaining aggregate statistics of PriceOracles""" +import time + +from xrpl.clients import JsonRpcClient +from xrpl.models.requests.get_aggregate_price import GetAggregatePrice +from xrpl.models.transactions.set_oracle import OracleSet, PriceData +from xrpl.transaction.reliable_submission import submit_and_wait +from xrpl.utils import str_to_hex +from xrpl.wallet import generate_faucet_wallet + +# Create a client to connect to the dev-network +client = JsonRpcClient("https://s.devnet.rippletest.net:51234") + +_PROVIDER = str_to_hex("provider") +_ASSET_CLASS = str_to_hex("currency") + +# list stores the (account, oracle_document_id) information +oracle_info = [] + +for i in range(10): + # new (pseudo-random) addresses are generated + wallet = generate_faucet_wallet(client, debug=True) + create_tx = OracleSet( + account=wallet.address, + oracle_document_id=i, + 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 + i, scale=1 + ), + PriceData( + base_asset="BTC", quote_asset="EUR", asset_price=100 + i, scale=2 + ), + ], + ) + + response = submit_and_wait(create_tx, client, wallet) + + # store the (account, oracle_document_id) for future use + oracle_info.append({"account": wallet.address, "oracle_document_id": i}) + +get_agg_request = GetAggregatePrice( + base_asset="XRP", quote_asset="USD", oracles=oracle_info +) +response = client.request(get_agg_request) +print(response) diff --git a/tests/unit/models/requests/test_get_aggregate_price.py b/tests/unit/models/requests/test_get_aggregate_price.py new file mode 100644 index 000000000..e118ac473 --- /dev/null +++ b/tests/unit/models/requests/test_get_aggregate_price.py @@ -0,0 +1,76 @@ +from unittest import TestCase + +from xrpl.models import XRPLModelException +from xrpl.models.requests import GetAggregatePrice +from xrpl.models.requests.get_aggregate_price import OracleInfo + +_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=[ + OracleInfo( + account=_ACCT_STR_1, oracle_document_id=_ORACLE_DOC_ID_1 + ), + OracleInfo( + 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=[ + OracleInfo( + account=_ACCT_STR_1, oracle_document_id=_ORACLE_DOC_ID_1 + ), + OracleInfo( + 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=[ + OracleInfo(account=_ACCT_STR_1, oracle_document_id=_ORACLE_DOC_ID_1), + OracleInfo(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=[ + OracleInfo(account=_ACCT_STR_1, oracle_document_id=_ORACLE_DOC_ID_1), + OracleInfo(account=_ACCT_STR_2, oracle_document_id=_ORACLE_DOC_ID_2), + ], + trim=20, + time_threshold=10, + ) + self.assertTrue(request.is_valid()) diff --git a/xrpl/models/requests/__init__.py b/xrpl/models/requests/__init__.py index 58ba3b2ba..f6214f658 100644 --- a/xrpl/models/requests/__init__.py +++ b/xrpl/models/requests/__init__.py @@ -17,6 +17,7 @@ from xrpl.models.requests.fee import Fee from xrpl.models.requests.gateway_balances import GatewayBalances from xrpl.models.requests.generic_request import GenericRequest +from xrpl.models.requests.get_aggregate_price import GetAggregatePrice from xrpl.models.requests.ledger import Ledger from xrpl.models.requests.ledger_closed import LedgerClosed from xrpl.models.requests.ledger_current import LedgerCurrent @@ -66,7 +67,7 @@ "Fee", "GatewayBalances", "GenericRequest", - # "GetAggregatePrice", + "GetAggregatePrice", "Ledger", "LedgerClosed", "LedgerCurrent", diff --git a/xrpl/models/requests/get_aggregate_price.py b/xrpl/models/requests/get_aggregate_price.py new file mode 100644 index 000000000..09650e188 --- /dev/null +++ b/xrpl/models/requests/get_aggregate_price.py @@ -0,0 +1,54 @@ +""" +This module defines the GetAggregatePrice request API. It is used to fetch aggregate +statistics about the specified PriceOracles +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Dict, List, Optional + +from xrpl.models.nested_model import NestedModel +from xrpl.models.requests.request import Request, RequestMethod +from xrpl.models.required import REQUIRED +from xrpl.models.utils import require_kwargs_on_init + + +@require_kwargs_on_init +@dataclass(frozen=True) +class GetAggregatePrice(Request): + """This request returns aggregate stats pertaining to the specified input oracles""" + + method: RequestMethod = field(default=RequestMethod.GET_AGGREGATE_PRICE, init=False) + + """base_asset is the asset to be priced""" + base_asset: str = REQUIRED # type: ignore + + """quote_asset is the denomination in which the prices are expressed""" + quote_asset: str = REQUIRED # type: ignore + + oracles: List[OracleInfo] = REQUIRED # type: ignore + + """percentage of outliers to trim""" + trim: Optional[int] = None + + """time_threshold : defines a range of prices to include based on the timestamp + range - {most recent, most recent - time_threshold}""" + time_threshold: Optional[int] = None + + def _get_errors(self: GetAggregatePrice) -> Dict[str, str]: + errors = super()._get_errors() + if len(self.oracles) == 0: + errors[ + "GetAggregatePrice" + ] = "Oracles array must contain at least one element" + return errors + + +@require_kwargs_on_init +@dataclass(frozen=True) +class OracleInfo(NestedModel): + """Represents one PriceData element. It is used in OracleSet transaction""" + + oracle_document_id: str = REQUIRED # type: ignore + account: str = REQUIRED # type: ignore diff --git a/xrpl/models/requests/request.py b/xrpl/models/requests/request.py index 07da47849..3ac63ddd5 100644 --- a/xrpl/models/requests/request.py +++ b/xrpl/models/requests/request.py @@ -79,6 +79,9 @@ class RequestMethod(str, Enum): # amm methods AMM_INFO = "amm_info" + # price oracle methods + GET_AGGREGATE_PRICE = "get_aggregate_price" + # generic unknown/unsupported request # (there is no XRPL analog, this model is specific to xrpl-py) GENERIC_REQUEST = "zzgeneric_request" From 6a16e6e48613ef550947fe7cf9cd085317974d8c Mon Sep 17 00:00:00 2001 From: Chenna Keshava Date: Tue, 30 Apr 2024 10:32:47 -0700 Subject: [PATCH 08/29] LedgerEntry unit tests -- validate the behavior of PriceOracle objects --- .../unit/models/requests/test_ledger_entry.py | 42 +++++++++++++++---- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/tests/unit/models/requests/test_ledger_entry.py b/tests/unit/models/requests/test_ledger_entry.py index a250572da..d30626e97 100644 --- a/tests/unit/models/requests/test_ledger_entry.py +++ b/tests/unit/models/requests/test_ledger_entry.py @@ -30,15 +30,6 @@ def test_has_only_offer_is_valid(self): ) self.assertTrue(req.is_valid()) - def test_has_only_price_oracle_is_valid(self): - req = LedgerEntry( - ripple_state=Oracle( - account="account1", - oracle_document_id=1, - ), - ) - self.assertTrue(req.is_valid()) - def test_has_only_ripple_state_is_valid(self): req = LedgerEntry( ripple_state=RippleState( @@ -128,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_priceoracle(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_priceoracle_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), + ) From d698d28b82e5ea64ef90b6b2457b84b7a11c9848 Mon Sep 17 00:00:00 2001 From: Chenna Keshava Date: Tue, 30 Apr 2024 14:57:33 -0700 Subject: [PATCH 09/29] integration tests for OracleSet, OracleDelete transactions --- .../transactions/test_delete_oracle.py | 40 +++++++++++++------ .../transactions/test_set_oracle.py | 11 ++++- 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/tests/integration/transactions/test_delete_oracle.py b/tests/integration/transactions/test_delete_oracle.py index 568d46e26..8e24393f6 100644 --- a/tests/integration/transactions/test_delete_oracle.py +++ b/tests/integration/transactions/test_delete_oracle.py @@ -1,3 +1,6 @@ +import random +import time + from tests.integration.integration_test_case import IntegrationTestCase from tests.integration.it_utils import ( sign_and_reliable_submission_async, @@ -6,35 +9,46 @@ 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.set_oracle import PriceData +from xrpl.utils import str_to_hex -_PROVIDER = "chainlink" -_ASSET_CLASS = "currency" +_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): - # Create PriceOracle to delete - setup_tx = OracleSet( + oracle_id = random.randint(100, 10000) + + # Create PriceOracle, to be deleted later + tx = OracleSet( account=WALLET.address, - oracle_document_id=1, + # 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 + 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(setup_tx, WALLET, client) + 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) - ) - self.assertEqual(len(account_objects_response.result["account_objects"]), 1) - # Create PriceOracle to delete tx = OracleDelete( account=WALLET.address, - oracle_document_id=1, + oracle_document_id=oracle_id, ) response = await sign_and_reliable_submission_async(tx, WALLET, client) self.assertEqual(response.status, ResponseStatus.SUCCESS) diff --git a/tests/integration/transactions/test_set_oracle.py b/tests/integration/transactions/test_set_oracle.py index bcffc87e4..445b949cc 100644 --- a/tests/integration/transactions/test_set_oracle.py +++ b/tests/integration/transactions/test_set_oracle.py @@ -1,3 +1,4 @@ +import random import time from tests.integration.integration_test_case import IntegrationTestCase @@ -20,7 +21,11 @@ class TestSetOracle(IntegrationTestCase): async def test_all_fields(self, client): tx = OracleSet( account=WALLET.address, - oracle_document_id=1, + # 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 + oracle_document_id=random.randint(100, 300), provider=_PROVIDER, asset_class=_ASSET_CLASS, last_update_time=int(time.time()), @@ -42,4 +47,6 @@ async def test_all_fields(self, client): AccountObjects(account=WALLET.address, type=AccountObjectType.ORACLE) ) - self.assertEqual(len(account_objects_response.result["account_objects"]), 1) + # subsequent integration tests (sync/async + json/websocket) add one + # oracle object to the account + self.assertTrue(len(account_objects_response.result["account_objects"]) > 0) From dad085fa5bf64b8cf936e3fef12084df9603cd94 Mon Sep 17 00:00:00 2001 From: Chenna Keshava Date: Tue, 30 Apr 2024 15:25:10 -0700 Subject: [PATCH 10/29] Fix: use int data type, not str for oracle_document_id --- xrpl/models/requests/get_aggregate_price.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xrpl/models/requests/get_aggregate_price.py b/xrpl/models/requests/get_aggregate_price.py index 09650e188..7b91ff230 100644 --- a/xrpl/models/requests/get_aggregate_price.py +++ b/xrpl/models/requests/get_aggregate_price.py @@ -50,5 +50,5 @@ def _get_errors(self: GetAggregatePrice) -> Dict[str, str]: class OracleInfo(NestedModel): """Represents one PriceData element. It is used in OracleSet transaction""" - oracle_document_id: str = REQUIRED # type: ignore + oracle_document_id: int = REQUIRED # type: ignore account: str = REQUIRED # type: ignore From 2af51b13d651fa95421914a7fb52f500efad1260 Mon Sep 17 00:00:00 2001 From: Chenna Keshava Date: Tue, 30 Apr 2024 15:28:04 -0700 Subject: [PATCH 11/29] update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e03a52d4..e3aec5b69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 PriceOracle amendment (XLS-47d). + ### Fixed - Added support for `XChainModifyBridge` flag maps (fixing an issue with `NFTokenCreateOffer` flag names) - Fixed `XChainModifyBridge` validation to allow just clearing of `MinAccountCreateAmount` From 170dafda75e2ae50fc62b85cc5c2144e3a1798db Mon Sep 17 00:00:00 2001 From: Chenna Keshava Date: Wed, 1 May 2024 10:55:59 -0700 Subject: [PATCH 12/29] update rippled version docker image --- .github/workflows/integration_test.yml | 2 +- CONTRIBUTING.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/integration_test.yml b/.github/workflows/integration_test.yml index 6af6fff70..2cdedf614 100644 --- a/.github/workflows/integration_test.yml +++ b/.github/workflows/integration_test.yml @@ -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: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dcf7ce92b..af34609b7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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: From bcec52cdc65d18ebf8264dfa743bece0704ca803 Mon Sep 17 00:00:00 2001 From: Chenna Keshava Date: Wed, 1 May 2024 13:22:12 -0700 Subject: [PATCH 13/29] fix: Use TypedDict instead of NestedModel to represent OracleInfo --- snippets/oracle_aggregate.py | 4 ++-- xrpl/models/requests/get_aggregate_price.py | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/snippets/oracle_aggregate.py b/snippets/oracle_aggregate.py index a0d5bb596..928ede798 100644 --- a/snippets/oracle_aggregate.py +++ b/snippets/oracle_aggregate.py @@ -2,7 +2,7 @@ import time from xrpl.clients import JsonRpcClient -from xrpl.models.requests.get_aggregate_price import GetAggregatePrice +from xrpl.models.requests.get_aggregate_price import GetAggregatePrice, OracleInfo from xrpl.models.transactions.set_oracle import OracleSet, PriceData from xrpl.transaction.reliable_submission import submit_and_wait from xrpl.utils import str_to_hex @@ -39,7 +39,7 @@ response = submit_and_wait(create_tx, client, wallet) # store the (account, oracle_document_id) for future use - oracle_info.append({"account": wallet.address, "oracle_document_id": i}) + oracle_info.append(OracleInfo(account=wallet.address, oracle_document_id=i)) get_agg_request = GetAggregatePrice( base_asset="XRP", quote_asset="USD", oracles=oracle_info diff --git a/xrpl/models/requests/get_aggregate_price.py b/xrpl/models/requests/get_aggregate_price.py index 7b91ff230..e1b1aed1b 100644 --- a/xrpl/models/requests/get_aggregate_price.py +++ b/xrpl/models/requests/get_aggregate_price.py @@ -8,7 +8,8 @@ from dataclasses import dataclass, field from typing import Dict, List, Optional -from xrpl.models.nested_model import NestedModel +from typing_extensions import TypedDict + from xrpl.models.requests.request import Request, RequestMethod from xrpl.models.required import REQUIRED from xrpl.models.utils import require_kwargs_on_init @@ -47,7 +48,7 @@ def _get_errors(self: GetAggregatePrice) -> Dict[str, str]: @require_kwargs_on_init @dataclass(frozen=True) -class OracleInfo(NestedModel): +class OracleInfo(TypedDict): """Represents one PriceData element. It is used in OracleSet transaction""" oracle_document_id: int = REQUIRED # type: ignore From df462ba5ca42b12e4ab754c9c6446b990264f6da Mon Sep 17 00:00:00 2001 From: Chenna Keshava Date: Wed, 1 May 2024 13:41:26 -0700 Subject: [PATCH 14/29] address Mayukha's comments: rename files and variables to match defs.json - oracle_set and oracle_delete --- .ci-config/rippled.cfg | 8 +++++++- snippets/oracle.py | 4 ++-- snippets/oracle_aggregate.py | 2 +- tests/integration/transactions/test_delete_oracle.py | 2 +- tests/integration/transactions/test_set_oracle.py | 2 +- xrpl/models/transactions/__init__.py | 8 ++++---- .../transactions/{delete_oracle.py => oracle_delete.py} | 2 +- xrpl/models/transactions/{set_oracle.py => oracle_set.py} | 2 +- xrpl/models/transactions/types/transaction_type.py | 4 ++-- 9 files changed, 20 insertions(+), 14 deletions(-) rename xrpl/models/transactions/{delete_oracle.py => oracle_delete.py} (93%) rename xrpl/models/transactions/{set_oracle.py => oracle_set.py} (96%) diff --git a/.ci-config/rippled.cfg b/.ci-config/rippled.cfg index 44b9e5d7c..a8551be1f 100644 --- a/.ci-config/rippled.cfg +++ b/.ci-config/rippled.cfg @@ -170,5 +170,11 @@ fixNFTokenRemint # 2.0.0-b4 Amendments XChainBridge DID -# 2.2.0-b1 Amendments +# 2.2.0-b3 Amendments +fixNFTokenReserve +fixInnerObjTemplate +fixAMMOverflowOffer PriceOracle +fixEmptyDID +fixXChainRewardRounding +fixPreviousTxnID diff --git a/snippets/oracle.py b/snippets/oracle.py index 94fe7b8a7..e6f98e889 100644 --- a/snippets/oracle.py +++ b/snippets/oracle.py @@ -2,8 +2,8 @@ import time from xrpl.clients import JsonRpcClient -from xrpl.models.transactions.delete_oracle import OracleDelete -from xrpl.models.transactions.set_oracle import OracleSet, PriceData +from xrpl.models.transactions.oracle_delete import OracleDelete +from xrpl.models.transactions.oracle_set import OracleSet, PriceData from xrpl.transaction.reliable_submission import submit_and_wait from xrpl.utils import str_to_hex from xrpl.wallet import generate_faucet_wallet diff --git a/snippets/oracle_aggregate.py b/snippets/oracle_aggregate.py index 928ede798..c1a6e34db 100644 --- a/snippets/oracle_aggregate.py +++ b/snippets/oracle_aggregate.py @@ -3,7 +3,7 @@ from xrpl.clients import JsonRpcClient from xrpl.models.requests.get_aggregate_price import GetAggregatePrice, OracleInfo -from xrpl.models.transactions.set_oracle import OracleSet, PriceData +from xrpl.models.transactions.oracle_set import OracleSet, PriceData from xrpl.transaction.reliable_submission import submit_and_wait from xrpl.utils import str_to_hex from xrpl.wallet import generate_faucet_wallet diff --git a/tests/integration/transactions/test_delete_oracle.py b/tests/integration/transactions/test_delete_oracle.py index 8e24393f6..0eda3e233 100644 --- a/tests/integration/transactions/test_delete_oracle.py +++ b/tests/integration/transactions/test_delete_oracle.py @@ -9,7 +9,7 @@ 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.set_oracle import PriceData +from xrpl.models.transactions.oracle_set import PriceData from xrpl.utils import str_to_hex _PROVIDER = str_to_hex("chainlink") diff --git a/tests/integration/transactions/test_set_oracle.py b/tests/integration/transactions/test_set_oracle.py index 445b949cc..161c85006 100644 --- a/tests/integration/transactions/test_set_oracle.py +++ b/tests/integration/transactions/test_set_oracle.py @@ -9,7 +9,7 @@ from tests.integration.reusable_values import WALLET from xrpl.models import AccountObjects, AccountObjectType, OracleSet from xrpl.models.response import ResponseStatus -from xrpl.models.transactions.set_oracle import PriceData +from xrpl.models.transactions.oracle_set import PriceData from xrpl.utils import str_to_hex _PROVIDER = str_to_hex("provider") diff --git a/xrpl/models/transactions/__init__.py b/xrpl/models/transactions/__init__.py index 0b3f83e17..ee3304f1b 100644 --- a/xrpl/models/transactions/__init__.py +++ b/xrpl/models/transactions/__init__.py @@ -28,7 +28,6 @@ from xrpl.models.transactions.check_cash import CheckCash from xrpl.models.transactions.check_create import CheckCreate from xrpl.models.transactions.clawback import Clawback -from xrpl.models.transactions.delete_oracle import OracleDelete from xrpl.models.transactions.deposit_preauth import DepositPreauth from xrpl.models.transactions.did_delete import DIDDelete from xrpl.models.transactions.did_set import DIDSet @@ -55,6 +54,8 @@ OfferCreateFlag, OfferCreateFlagInterface, ) +from xrpl.models.transactions.oracle_delete import OracleDelete +from xrpl.models.transactions.oracle_set import OracleSet from xrpl.models.transactions.payment import Payment, PaymentFlag, PaymentFlagInterface from xrpl.models.transactions.payment_channel_claim import ( PaymentChannelClaim, @@ -63,7 +64,6 @@ ) from xrpl.models.transactions.payment_channel_create import PaymentChannelCreate from xrpl.models.transactions.payment_channel_fund import PaymentChannelFund -from xrpl.models.transactions.set_oracle import OracleSet from xrpl.models.transactions.set_regular_key import SetRegularKey from xrpl.models.transactions.signer_list_set import SignerEntry, SignerListSet from xrpl.models.transactions.ticket_create import TicketCreate @@ -113,7 +113,6 @@ "CheckCash", "CheckCreate", "Clawback", - "OracleDelete", "DepositPreauth", "DIDDelete", "DIDSet", @@ -134,6 +133,8 @@ "OfferCreate", "OfferCreateFlag", "OfferCreateFlagInterface", + "OracleDelete", + "OracleSet", "Payment", "PaymentChannelClaim", "PaymentChannelClaimFlag", @@ -142,7 +143,6 @@ "PaymentChannelFund", "PaymentFlag", "PaymentFlagInterface", - "OracleSet", "SetRegularKey", "Signer", "SignerEntry", diff --git a/xrpl/models/transactions/delete_oracle.py b/xrpl/models/transactions/oracle_delete.py similarity index 93% rename from xrpl/models/transactions/delete_oracle.py rename to xrpl/models/transactions/oracle_delete.py index 34e398d03..db34053c3 100644 --- a/xrpl/models/transactions/delete_oracle.py +++ b/xrpl/models/transactions/oracle_delete.py @@ -19,6 +19,6 @@ class OracleDelete(Transaction): oracle_document_id: int = REQUIRED # type: ignore transaction_type: TransactionType = field( - default=TransactionType.DELETE_ORACLE, + default=TransactionType.ORACLE_DELETE, init=False, ) diff --git a/xrpl/models/transactions/set_oracle.py b/xrpl/models/transactions/oracle_set.py similarity index 96% rename from xrpl/models/transactions/set_oracle.py rename to xrpl/models/transactions/oracle_set.py index 10b5130ea..73dd48f91 100644 --- a/xrpl/models/transactions/set_oracle.py +++ b/xrpl/models/transactions/oracle_set.py @@ -32,7 +32,7 @@ class OracleSet(Transaction): price_data_series: List[PriceData] = REQUIRED # type: ignore transaction_type: TransactionType = field( - default=TransactionType.SET_ORACLE, + default=TransactionType.ORACLE_SET, init=False, ) diff --git a/xrpl/models/transactions/types/transaction_type.py b/xrpl/models/transactions/types/transaction_type.py index 309dfdc49..0d14e2775 100644 --- a/xrpl/models/transactions/types/transaction_type.py +++ b/xrpl/models/transactions/types/transaction_type.py @@ -18,7 +18,6 @@ class TransactionType(str, Enum): CHECK_CASH = "CheckCash" CHECK_CREATE = "CheckCreate" CLAWBACK = "Clawback" - DELETE_ORACLE = "OracleDelete" DEPOSIT_PREAUTH = "DepositPreauth" DID_DELETE = "DIDDelete" DID_SET = "DIDSet" @@ -32,11 +31,12 @@ class TransactionType(str, Enum): NFTOKEN_MINT = "NFTokenMint" OFFER_CANCEL = "OfferCancel" OFFER_CREATE = "OfferCreate" + ORACLE_SET = "OracleSet" + ORACLE_DELETE = "OracleDelete" PAYMENT = "Payment" PAYMENT_CHANNEL_CLAIM = "PaymentChannelClaim" PAYMENT_CHANNEL_CREATE = "PaymentChannelCreate" PAYMENT_CHANNEL_FUND = "PaymentChannelFund" - SET_ORACLE = "OracleSet" SET_REGULAR_KEY = "SetRegularKey" SIGNER_LIST_SET = "SignerListSet" TICKET_CREATE = "TicketCreate" From 7256c4c8b950220186497f44f18921056493c73a Mon Sep 17 00:00:00 2001 From: Chenna Keshava Date: Fri, 3 May 2024 20:41:59 -0700 Subject: [PATCH 15/29] include LedgerEntry verification in oracles snippets test --- snippets/oracle.py | 40 +++++++++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/snippets/oracle.py b/snippets/oracle.py index e6f98e889..b3d280d81 100644 --- a/snippets/oracle.py +++ b/snippets/oracle.py @@ -2,6 +2,9 @@ import time from xrpl.clients import JsonRpcClient +from xrpl.models import LedgerEntry +from xrpl.models.requests.ledger_entry import Oracle +from xrpl.models.response import ResponseStatus from xrpl.models.transactions.oracle_delete import OracleDelete from xrpl.models.transactions.oracle_set import OracleSet, PriceData from xrpl.transaction.reliable_submission import submit_and_wait @@ -31,11 +34,10 @@ ) response = submit_and_wait(create_tx, client, wallet) -# TODO: Keshava -# print(response.result['meta']['TransactionResult'] == 'tesSUCCESS') # does not work -print( - "Result of SetOracle transaction: " + response.result["meta"]["TransactionResult"] -) + +if response.status != ResponseStatus.SUCCESS: + print("Create Oracle operation failed, recieved the following response") + print(response) # update the oracle data update_tx = OracleSet( @@ -49,17 +51,33 @@ ) response = submit_and_wait(update_tx, client, wallet) -print( - "Result of the Update Oracle transaction: " - + response.result["meta"]["TransactionResult"] +if response.status != ResponseStatus.SUCCESS: + print("Update Oracle operation failed, recieved the following response") + print(response) + +ledger_entry_req = LedgerEntry( + oracle=Oracle(account=wallet.address, oracle_document_id=_ORACLE_DOC_ID) ) +response = client.request(ledger_entry_req) + +if response.status != ResponseStatus.SUCCESS: + print("Test failed: Oracle Ledger Entry is not present") + print(response) # delete the oracle delete_tx = OracleDelete(account=wallet.address, oracle_document_id=_ORACLE_DOC_ID) response = submit_and_wait(delete_tx, client, wallet) -print( - "Result of DeleteOracle transaction: " - + response.result["meta"]["TransactionResult"] +if response.status != ResponseStatus.SUCCESS: + print("Delete Oracle operation failed, recieved the following response") + print(response) + +ledger_entry_req = LedgerEntry( + oracle=Oracle(account=wallet.address, oracle_document_id=_ORACLE_DOC_ID) ) +response = client.request(ledger_entry_req) + +if response.status != ResponseStatus.ERROR: + print("Test failed: Oracle Ledger Entry has not been deleted") + print(response) From 9c7ce5e9e7cabdb5105c193447a2deb27bc6f59e Mon Sep 17 00:00:00 2001 From: Chenna Keshava Date: Mon, 6 May 2024 12:37:27 -0700 Subject: [PATCH 16/29] address review comments by mayukha and omar --- snippets/oracle_aggregate.py | 4 +- .../requests/test_get_aggregate_price.py | 26 +-- .../models/transactions/test_set_oracle.py | 185 +++++++++++++++++- xrpl/models/requests/get_aggregate_price.py | 19 +- xrpl/models/transactions/oracle_delete.py | 5 + xrpl/models/transactions/oracle_set.py | 88 ++++++++- 6 files changed, 295 insertions(+), 32 deletions(-) diff --git a/snippets/oracle_aggregate.py b/snippets/oracle_aggregate.py index c1a6e34db..493f35c23 100644 --- a/snippets/oracle_aggregate.py +++ b/snippets/oracle_aggregate.py @@ -2,7 +2,7 @@ import time from xrpl.clients import JsonRpcClient -from xrpl.models.requests.get_aggregate_price import GetAggregatePrice, OracleInfo +from xrpl.models.requests.get_aggregate_price import GetAggregatePrice, Oracle from xrpl.models.transactions.oracle_set import OracleSet, PriceData from xrpl.transaction.reliable_submission import submit_and_wait from xrpl.utils import str_to_hex @@ -39,7 +39,7 @@ response = submit_and_wait(create_tx, client, wallet) # store the (account, oracle_document_id) for future use - oracle_info.append(OracleInfo(account=wallet.address, oracle_document_id=i)) + oracle_info.append(Oracle(account=wallet.address, oracle_document_id=i)) get_agg_request = GetAggregatePrice( base_asset="XRP", quote_asset="USD", oracles=oracle_info diff --git a/tests/unit/models/requests/test_get_aggregate_price.py b/tests/unit/models/requests/test_get_aggregate_price.py index e118ac473..dec902320 100644 --- a/tests/unit/models/requests/test_get_aggregate_price.py +++ b/tests/unit/models/requests/test_get_aggregate_price.py @@ -2,7 +2,7 @@ from xrpl.models import XRPLModelException from xrpl.models.requests import GetAggregatePrice -from xrpl.models.requests.get_aggregate_price import OracleInfo +from xrpl.models.requests.get_aggregate_price import Oracle _ACCT_STR_1 = "rBwHKFS534tfG3mATXSycCnX8PAd3XJswj" _ORACLE_DOC_ID_1 = 1 @@ -27,12 +27,8 @@ def test_invalid_requests(self): GetAggregatePrice( quote_asset="XRP", oracles=[ - OracleInfo( - account=_ACCT_STR_1, oracle_document_id=_ORACLE_DOC_ID_1 - ), - OracleInfo( - account=_ACCT_STR_2, oracle_document_id=_ORACLE_DOC_ID_2 - ), + Oracle(account=_ACCT_STR_1, oracle_document_id=_ORACLE_DOC_ID_1), + Oracle(account=_ACCT_STR_2, oracle_document_id=_ORACLE_DOC_ID_2), ], ) @@ -41,12 +37,8 @@ def test_invalid_requests(self): GetAggregatePrice( base_asset="USD", oracles=[ - OracleInfo( - account=_ACCT_STR_1, oracle_document_id=_ORACLE_DOC_ID_1 - ), - OracleInfo( - account=_ACCT_STR_2, oracle_document_id=_ORACLE_DOC_ID_2 - ), + Oracle(account=_ACCT_STR_1, oracle_document_id=_ORACLE_DOC_ID_1), + Oracle(account=_ACCT_STR_2, oracle_document_id=_ORACLE_DOC_ID_2), ], ) @@ -56,8 +48,8 @@ def test_valid_request(self): base_asset="USD", quote_asset="XRP", oracles=[ - OracleInfo(account=_ACCT_STR_1, oracle_document_id=_ORACLE_DOC_ID_1), - OracleInfo(account=_ACCT_STR_2, oracle_document_id=_ORACLE_DOC_ID_2), + 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()) @@ -67,8 +59,8 @@ def test_valid_request(self): base_asset="USD", quote_asset="XRP", oracles=[ - OracleInfo(account=_ACCT_STR_1, oracle_document_id=_ORACLE_DOC_ID_1), - OracleInfo(account=_ACCT_STR_2, oracle_document_id=_ORACLE_DOC_ID_2), + 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, diff --git a/tests/unit/models/transactions/test_set_oracle.py b/tests/unit/models/transactions/test_set_oracle.py index e9fef6d5f..eacc319ed 100644 --- a/tests/unit/models/transactions/test_set_oracle.py +++ b/tests/unit/models/transactions/test_set_oracle.py @@ -3,11 +3,20 @@ from xrpl.models.exceptions import XRPLModelException from xrpl.models.transactions import OracleSet +from xrpl.models.transactions.oracle_set import ( + MAX_ORACLE_PROVIDER, + MAX_ORACLE_SYMBOL_CLASS, + MAX_ORACLE_URI, + PriceData, +) _ACCOUNT = "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW" _PROVIDER = "chainlink" _ASSET_CLASS = "currency" +_EMPTY_PROVIDER = "" +_LENGTHY_PROVIDER = "X" * (MAX_ORACLE_PROVIDER + 1) + class TestSetOracle(TestCase): def test_valid(self): @@ -17,11 +26,19 @@ def test_valid(self): provider=_PROVIDER, asset_class=_ASSET_CLASS, last_update_time=int(time.time()), - price_data_series=[], + 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 + ), + ], ) self.assertTrue(tx.is_valid()) - def test_missing_data_series(self): + def validate_data_series(self): + # price_data_series is missing in the input with self.assertRaises(XRPLModelException): OracleSet( account=_ACCOUNT, @@ -30,3 +47,167 @@ def test_missing_data_series(self): asset_class=_ASSET_CLASS, last_update_time=int(time.time()), ) + + # price_data_series exceeds the mandated length (10 elements) + with self.assertRaises(XRPLModelException): + OracleSet( + account=_ACCOUNT, + oracle_document_id=1, + provider=_PROVIDER, + asset_class=_ASSET_CLASS, + last_update_time=int(time.time()), + price_data_series=[ + PriceData( + base_asset="XRP", quote_asset="USD1", asset_price=741, scale=1 + ), + PriceData( + base_asset="XRP", quote_asset="USD2", asset_price=742, scale=1 + ), + PriceData( + base_asset="XRP", quote_asset="USD3", asset_price=743, scale=1 + ), + PriceData( + base_asset="XRP", quote_asset="USD4", asset_price=744, scale=1 + ), + PriceData( + base_asset="XRP", quote_asset="USD5", asset_price=745, scale=1 + ), + PriceData( + base_asset="XRP", quote_asset="USD6", asset_price=746, scale=1 + ), + PriceData( + base_asset="XRP", quote_asset="USD7", asset_price=747, scale=1 + ), + PriceData( + base_asset="XRP", quote_asset="USD8", asset_price=748, scale=1 + ), + PriceData( + base_asset="XRP", quote_asset="USD9", asset_price=749, scale=1 + ), + PriceData( + base_asset="XRP", quote_asset="USD10", asset_price=750, scale=1 + ), + PriceData( + base_asset="XRP", quote_asset="USD11", asset_price=751, scale=1 + ), + ], + ) + + def validate_provider_field(self): + tx = OracleSet( + account=_ACCOUNT, + oracle_document_id=1, + provider=_PROVIDER, + asset_class=_ASSET_CLASS, + last_update_time=int(time.time()), + uri="https://some_data_provider.com/path", + 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 + ), + ], + ) + self.assertTrue(tx.is_valid()) + + # empty provider + with self.assertRaises(XRPLModelException): + OracleSet( + account=_ACCOUNT, + oracle_document_id=1, + provider=_EMPTY_PROVIDER, + asset_class=_ASSET_CLASS, + last_update_time=int(time.time()), + ) + + # provider exceeds MAX_ORACLE_PROVIDER characters + with self.assertRaises(XRPLModelException): + OracleSet( + account=_ACCOUNT, + oracle_document_id=1, + provider=_LENGTHY_PROVIDER, + asset_class=_ASSET_CLASS, + last_update_time=int(time.time()), + ) + + def validate_uri_field(self): + tx = OracleSet( + account=_ACCOUNT, + oracle_document_id=1, + provider=_PROVIDER, + asset_class=_ASSET_CLASS, + last_update_time=int(time.time()), + uri="https://some_data_provider.com/path", + 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 + ), + ], + ) + self.assertTrue(tx.is_valid()) + + # empty URI + with self.assertRaises(XRPLModelException): + OracleSet( + account=_ACCOUNT, + oracle_document_id=1, + provider=_PROVIDER, + asset_class=_ASSET_CLASS, + last_update_time=int(time.time()), + uri="", + ) + + # URI exceeds MAX_ORACLE_URI characters + with self.assertRaises(XRPLModelException): + OracleSet( + account=_ACCOUNT, + oracle_document_id=1, + provider=_PROVIDER, + asset_class=_ASSET_CLASS, + last_update_time=int(time.time()), + uri=("x" * (MAX_ORACLE_URI + 1)), + ) + + def validate_asset_class_field(self): + tx = OracleSet( + account=_ACCOUNT, + oracle_document_id=1, + provider=_PROVIDER, + asset_class=_ASSET_CLASS, + last_update_time=int(time.time()), + uri="https://some_data_provider.com/path", + 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 + ), + ], + ) + self.assertTrue(tx.is_valid()) + + # empty asset class + with self.assertRaises(XRPLModelException): + OracleSet( + account=_ACCOUNT, + oracle_document_id=1, + provider=_PROVIDER, + last_update_time=int(time.time()), + asset_class="", + ) + + # URI exceeds MAX_ORACLE_SYMBOL_CLASS characters + with self.assertRaises(XRPLModelException): + OracleSet( + account=_ACCOUNT, + oracle_document_id=1, + provider=_PROVIDER, + last_update_time=int(time.time()), + asset_class=("x" * (MAX_ORACLE_SYMBOL_CLASS + 1)), + ) diff --git a/xrpl/models/requests/get_aggregate_price.py b/xrpl/models/requests/get_aggregate_price.py index e1b1aed1b..7f0013c0b 100644 --- a/xrpl/models/requests/get_aggregate_price.py +++ b/xrpl/models/requests/get_aggregate_price.py @@ -22,20 +22,21 @@ class GetAggregatePrice(Request): method: RequestMethod = field(default=RequestMethod.GET_AGGREGATE_PRICE, init=False) - """base_asset is the asset to be priced""" base_asset: str = REQUIRED # type: ignore + """base_asset is the asset to be priced""" - """quote_asset is the denomination in which the prices are expressed""" quote_asset: str = REQUIRED # type: ignore + """quote_asset is the denomination in which the prices are expressed""" - oracles: List[OracleInfo] = REQUIRED # type: ignore + oracles: List[Oracle] = REQUIRED # type: ignore + """oracles is an array of oracle objects to aggregate over""" - """percentage of outliers to trim""" trim: Optional[int] = None + """percentage of outliers to trim""" + time_threshold: Optional[int] = None """time_threshold : defines a range of prices to include based on the timestamp range - {most recent, most recent - time_threshold}""" - time_threshold: Optional[int] = None def _get_errors(self: GetAggregatePrice) -> Dict[str, str]: errors = super()._get_errors() @@ -48,8 +49,12 @@ def _get_errors(self: GetAggregatePrice) -> Dict[str, str]: @require_kwargs_on_init @dataclass(frozen=True) -class OracleInfo(TypedDict): - """Represents one PriceData element. It is used in OracleSet transaction""" +class Oracle(TypedDict): + """Represents one Oracle element. It is used in GetAggregatePrice request""" oracle_document_id: int = REQUIRED # type: ignore + """oracle_document_id is a unique identifier of the Price Oracle for the given + Account""" + account: str = REQUIRED # type: ignore + """account is the Oracle's account.""" diff --git a/xrpl/models/transactions/oracle_delete.py b/xrpl/models/transactions/oracle_delete.py index db34053c3..54bf890b0 100644 --- a/xrpl/models/transactions/oracle_delete.py +++ b/xrpl/models/transactions/oracle_delete.py @@ -16,7 +16,12 @@ class OracleDelete(Transaction): """Represents an OracleDelete transaction.""" account: str = REQUIRED # type: ignore + """Account is the account that has the Oracle update and delete privileges. + This field corresponds to the Owner field on the PriceOracle ledger object.""" + oracle_document_id: int = REQUIRED # type: ignore + """OracleDocumentID is a unique identifier of the Price Oracle for the given + Account.""" transaction_type: TransactionType = field( default=TransactionType.ORACLE_DELETE, diff --git a/xrpl/models/transactions/oracle_set.py b/xrpl/models/transactions/oracle_set.py index 73dd48f91..b136cca1a 100644 --- a/xrpl/models/transactions/oracle_set.py +++ b/xrpl/models/transactions/oracle_set.py @@ -3,7 +3,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import List, Optional +from typing import Dict, List, Optional from xrpl.models.nested_model import NestedModel from xrpl.models.required import REQUIRED @@ -11,6 +11,11 @@ from xrpl.models.transactions.types import TransactionType from xrpl.models.utils import require_kwargs_on_init +MAX_ORACLE_DATA_SERIES = 10 +MAX_ORACLE_PROVIDER = 256 +MAX_ORACLE_URI = 256 +MAX_ORACLE_SYMBOL_CLASS = 16 + @require_kwargs_on_init @dataclass(frozen=True) @@ -18,24 +23,88 @@ class OracleSet(Transaction): """Represents a OracleSet transaction.""" account: str = REQUIRED # type: ignore + """Account is the XRPL account that has update and delete privileges on the Oracle + being set. This field corresponds to the Owner field on the PriceOracle ledger + object.""" + oracle_document_id: int = REQUIRED # type: ignore + """OracleDocumentID is a unique identifier of the Price Oracle for the given + Account.""" + provider: Optional[str] = None """ - The below three fields must be hex-encoded. You can - use `xrpl.utils.str_to_hex` to convert a UTF-8 string to hex. + This field must be hex-encoded. You can use `xrpl.utils.str_to_hex` to + convert a UTF-8 string to hex. """ - provider: Optional[str] = None + uri: Optional[str] = None + """ + This field must be hex-encoded. You can use `xrpl.utils.str_to_hex` to + convert a UTF-8 string to hex. + """ + asset_class: Optional[str] = None + """ + This field must be hex-encoded. You can use `xrpl.utils.str_to_hex` to + convert a UTF-8 string to hex. + """ last_update_time: int = REQUIRED # type: ignore + """LastUpdateTime is the specific point in time when the data was last updated. + The LastUpdateTime is represented as Unix Time - the number of seconds since + January 1, 1970 (00:00 UTC).""" + price_data_series: List[PriceData] = REQUIRED # type: ignore + """PriceDataSeries is an array of up to ten PriceData objects, where PriceData + represents the price information for a token pair""" transaction_type: TransactionType = field( default=TransactionType.ORACLE_SET, init=False, ) + def _get_errors(self: OracleSet) -> Dict[str, str]: + errors = super()._get_errors() + + # If price_data_series is not set, do not perform further validation + if "price_data_series" not in errors and ( + len(self.price_data_series) == 0 + or len(self.price_data_series) > MAX_ORACLE_DATA_SERIES + ): + errors["OracleSet: price_data_series"] = ( + "The Price Data Series list must have a length of >0 and <" + + str(MAX_ORACLE_DATA_SERIES) + + "." + ) + + if self.asset_class and ( + len(self.asset_class) == 0 + or len(self.asset_class) > MAX_ORACLE_SYMBOL_CLASS + ): + errors["OracleSet: asset_class"] = ( + "The asset_class field must have a length of >0 and <" + + str(MAX_ORACLE_SYMBOL_CLASS) + + "." + ) + + if self.provider and ( + len(self.provider) == 0 or len(self.provider) > MAX_ORACLE_PROVIDER + ): + errors["OracleSet: provider"] = ( + "The provider field must have a length of >0 and <" + + str(MAX_ORACLE_PROVIDER) + + "." + ) + + if self.uri and (len(self.uri) == 0 or len(self.uri) > MAX_ORACLE_URI): + errors["OracleSet: uri"] = ( + "The uri field must have a length of >0 and <" + + str(MAX_ORACLE_URI) + + "." + ) + + return errors + @require_kwargs_on_init @dataclass(frozen=True) @@ -43,6 +112,17 @@ class PriceData(NestedModel): """Represents one PriceData element. It is used in OracleSet transaction""" base_asset: str = REQUIRED # type: ignore + """BaseAsset refers to the primary asset within a trading pair. It is the asset + against which the price of the quote asset is quoted.""" + quote_asset: str = REQUIRED # type: ignore + """QuoteAsset represents the secondary or quote asset in a trading pair. It denotes + the price of one unit of the base asset.""" + asset_price: Optional[int] = None + """AssetPrice is the scaled asset price, which is the price value after applying + the scaling factor.""" + scale: Optional[int] = None + """Scale is the price's scaling factor. + It represents the price's precision level. """ From 9d1b476e8452d86f27178b7b35a217e2f6ce0924 Mon Sep 17 00:00:00 2001 From: Chenna Keshava Date: Mon, 6 May 2024 14:46:08 -0700 Subject: [PATCH 17/29] remove snippets tests for PriceOracle feature --- snippets/oracle.py | 83 ------------------------------------ snippets/oracle_aggregate.py | 48 --------------------- 2 files changed, 131 deletions(-) delete mode 100644 snippets/oracle.py delete mode 100644 snippets/oracle_aggregate.py diff --git a/snippets/oracle.py b/snippets/oracle.py deleted file mode 100644 index b3d280d81..000000000 --- a/snippets/oracle.py +++ /dev/null @@ -1,83 +0,0 @@ -"""Example of how we can create, update and delete oracles""" -import time - -from xrpl.clients import JsonRpcClient -from xrpl.models import LedgerEntry -from xrpl.models.requests.ledger_entry import Oracle -from xrpl.models.response import ResponseStatus -from xrpl.models.transactions.oracle_delete import OracleDelete -from xrpl.models.transactions.oracle_set import OracleSet, PriceData -from xrpl.transaction.reliable_submission import submit_and_wait -from xrpl.utils import str_to_hex -from xrpl.wallet import generate_faucet_wallet - -# Create a client to connect to the dev-network -client = JsonRpcClient("https://s.devnet.rippletest.net:51234") - -wallet = generate_faucet_wallet(client, debug=True) - - -_PROVIDER = str_to_hex("provider") -_ASSET_CLASS = str_to_hex("currency") -_ORACLE_DOC_ID = 1 - -create_tx = OracleSet( - account=wallet.address, - oracle_document_id=_ORACLE_DOC_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 = submit_and_wait(create_tx, client, wallet) - -if response.status != ResponseStatus.SUCCESS: - print("Create Oracle operation failed, recieved the following response") - print(response) - -# update the oracle data -update_tx = OracleSet( - account=wallet.address, - oracle_document_id=_ORACLE_DOC_ID, - last_update_time=int(time.time()), - price_data_series=[ - PriceData(base_asset="XRP", quote_asset="USD", asset_price=742, scale=1), - PriceData(base_asset="BTC", quote_asset="EUR", asset_price=103, scale=2), - ], -) -response = submit_and_wait(update_tx, client, wallet) - -if response.status != ResponseStatus.SUCCESS: - print("Update Oracle operation failed, recieved the following response") - print(response) - -ledger_entry_req = LedgerEntry( - oracle=Oracle(account=wallet.address, oracle_document_id=_ORACLE_DOC_ID) -) -response = client.request(ledger_entry_req) - -if response.status != ResponseStatus.SUCCESS: - print("Test failed: Oracle Ledger Entry is not present") - print(response) - - -# delete the oracle -delete_tx = OracleDelete(account=wallet.address, oracle_document_id=_ORACLE_DOC_ID) -response = submit_and_wait(delete_tx, client, wallet) - -if response.status != ResponseStatus.SUCCESS: - print("Delete Oracle operation failed, recieved the following response") - print(response) - -ledger_entry_req = LedgerEntry( - oracle=Oracle(account=wallet.address, oracle_document_id=_ORACLE_DOC_ID) -) -response = client.request(ledger_entry_req) - -if response.status != ResponseStatus.ERROR: - print("Test failed: Oracle Ledger Entry has not been deleted") - print(response) diff --git a/snippets/oracle_aggregate.py b/snippets/oracle_aggregate.py deleted file mode 100644 index 493f35c23..000000000 --- a/snippets/oracle_aggregate.py +++ /dev/null @@ -1,48 +0,0 @@ -"""Snippet demonstrates obtaining aggregate statistics of PriceOracles""" -import time - -from xrpl.clients import JsonRpcClient -from xrpl.models.requests.get_aggregate_price import GetAggregatePrice, Oracle -from xrpl.models.transactions.oracle_set import OracleSet, PriceData -from xrpl.transaction.reliable_submission import submit_and_wait -from xrpl.utils import str_to_hex -from xrpl.wallet import generate_faucet_wallet - -# Create a client to connect to the dev-network -client = JsonRpcClient("https://s.devnet.rippletest.net:51234") - -_PROVIDER = str_to_hex("provider") -_ASSET_CLASS = str_to_hex("currency") - -# list stores the (account, oracle_document_id) information -oracle_info = [] - -for i in range(10): - # new (pseudo-random) addresses are generated - wallet = generate_faucet_wallet(client, debug=True) - create_tx = OracleSet( - account=wallet.address, - oracle_document_id=i, - 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 + i, scale=1 - ), - PriceData( - base_asset="BTC", quote_asset="EUR", asset_price=100 + i, scale=2 - ), - ], - ) - - response = submit_and_wait(create_tx, client, wallet) - - # store the (account, oracle_document_id) for future use - oracle_info.append(Oracle(account=wallet.address, oracle_document_id=i)) - -get_agg_request = GetAggregatePrice( - base_asset="XRP", quote_asset="USD", oracles=oracle_info -) -response = client.request(get_agg_request) -print(response) From 23b0ef00b11377b81da46d6c0dd74f2e956a1544 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S <21219765+ckeshava@users.noreply.github.com> Date: Mon, 6 May 2024 14:47:00 -0700 Subject: [PATCH 18/29] Update CHANGELOG.md Co-authored-by: Omar Khan --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3aec5b69..e05640313 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Included `ctid` field in the `tx` request. ### Added -- Support for the PriceOracle amendment (XLS-47d). +- Support for the Price Oracles amendment (XLS-47). ### Fixed - Added support for `XChainModifyBridge` flag maps (fixing an issue with `NFTokenCreateOffer` flag names) From d08ba7fc00d8d4eaaa53a270d91f345ab1369427 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Mon, 6 May 2024 17:58:03 -0400 Subject: [PATCH 19/29] add unique values to each client run --- tests/integration/it_utils.py | 15 +++++++++------ tests/integration/transactions/test_payment.py | 1 + 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/tests/integration/it_utils.py b/tests/integration/it_utils.py index 72a74e3e3..b7713c861 100644 --- a/tests/integration/it_utils.py +++ b/tests/integration/it_utils.py @@ -1,4 +1,5 @@ """Utility functions and variables for integration tests.""" + import asyncio import importlib import inspect @@ -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( @@ -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 @@ -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 diff --git a/tests/integration/transactions/test_payment.py b/tests/integration/transactions/test_payment.py index 7e308debb..6b1389a6a 100644 --- a/tests/integration/transactions/test_payment.py +++ b/tests/integration/transactions/test_payment.py @@ -10,6 +10,7 @@ class TestPayment(IntegrationTestCase): @test_async_and_sync(globals()) async def test_basic_functionality(self, client): + print(self.value) response = await sign_and_reliable_submission_async( Payment( account=WALLET.address, From 1a02a5846c930caa0f9954b1d1cf274f1ef46a78 Mon Sep 17 00:00:00 2001 From: Chenna Keshava Date: Mon, 6 May 2024 15:30:38 -0700 Subject: [PATCH 20/29] remove the usage of random.randint in OracleSet integraton tests --- tests/integration/transactions/test_delete_oracle.py | 10 ++++------ tests/integration/transactions/test_payment.py | 1 - tests/integration/transactions/test_set_oracle.py | 5 +++-- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/tests/integration/transactions/test_delete_oracle.py b/tests/integration/transactions/test_delete_oracle.py index 0eda3e233..315192469 100644 --- a/tests/integration/transactions/test_delete_oracle.py +++ b/tests/integration/transactions/test_delete_oracle.py @@ -1,4 +1,3 @@ -import random import time from tests.integration.integration_test_case import IntegrationTestCase @@ -19,15 +18,14 @@ class TestDeleteOracle(IntegrationTestCase): @test_async_and_sync(globals()) async def test_basic(self, client): - oracle_id = random.randint(100, 10000) + oracle_id = 1 # Create PriceOracle, to be deleted later 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 + # 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 th end of the test. oracle_document_id=oracle_id, provider=_PROVIDER, asset_class=_ASSET_CLASS, diff --git a/tests/integration/transactions/test_payment.py b/tests/integration/transactions/test_payment.py index 6b1389a6a..7e308debb 100644 --- a/tests/integration/transactions/test_payment.py +++ b/tests/integration/transactions/test_payment.py @@ -10,7 +10,6 @@ class TestPayment(IntegrationTestCase): @test_async_and_sync(globals()) async def test_basic_functionality(self, client): - print(self.value) response = await sign_and_reliable_submission_async( Payment( account=WALLET.address, diff --git a/tests/integration/transactions/test_set_oracle.py b/tests/integration/transactions/test_set_oracle.py index 161c85006..fc948c16c 100644 --- a/tests/integration/transactions/test_set_oracle.py +++ b/tests/integration/transactions/test_set_oracle.py @@ -1,4 +1,3 @@ -import random import time from tests.integration.integration_test_case import IntegrationTestCase @@ -25,7 +24,9 @@ async def test_all_fields(self, client): # (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 - oracle_document_id=random.randint(100, 300), + # 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()), From 958ee1ac1d233e514a34b6a7230f26cd8106d6a5 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S <21219765+ckeshava@users.noreply.github.com> Date: Tue, 7 May 2024 13:32:52 -0700 Subject: [PATCH 21/29] Update tests/integration/transactions/test_delete_oracle.py Co-authored-by: pdp2121 <71317875+pdp2121@users.noreply.github.com> --- tests/integration/transactions/test_delete_oracle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/transactions/test_delete_oracle.py b/tests/integration/transactions/test_delete_oracle.py index 315192469..61cc591a3 100644 --- a/tests/integration/transactions/test_delete_oracle.py +++ b/tests/integration/transactions/test_delete_oracle.py @@ -25,7 +25,7 @@ async def test_basic(self, client): 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 th end of the test. + # 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, From 915ff350d1f0ab679d18df71b0b3a54fdebf61ee Mon Sep 17 00:00:00 2001 From: Chenna Keshava Date: Tue, 7 May 2024 14:04:27 -0700 Subject: [PATCH 22/29] address review comments --- .../transactions/test_delete_oracle.py | 2 +- .../models/transactions/test_set_oracle.py | 19 +-- xrpl/models/requests/get_aggregate_price.py | 23 ++-- xrpl/models/transactions/oracle_delete.py | 8 +- xrpl/models/transactions/oracle_set.py | 119 ++++++++++-------- 5 files changed, 97 insertions(+), 74 deletions(-) diff --git a/tests/integration/transactions/test_delete_oracle.py b/tests/integration/transactions/test_delete_oracle.py index 61cc591a3..702025af8 100644 --- a/tests/integration/transactions/test_delete_oracle.py +++ b/tests/integration/transactions/test_delete_oracle.py @@ -18,7 +18,7 @@ class TestDeleteOracle(IntegrationTestCase): @test_async_and_sync(globals()) async def test_basic(self, client): - oracle_id = 1 + oracle_id = self.value # Create PriceOracle, to be deleted later tx = OracleSet( diff --git a/tests/unit/models/transactions/test_set_oracle.py b/tests/unit/models/transactions/test_set_oracle.py index eacc319ed..3e7498c73 100644 --- a/tests/unit/models/transactions/test_set_oracle.py +++ b/tests/unit/models/transactions/test_set_oracle.py @@ -37,8 +37,7 @@ def test_valid(self): ) self.assertTrue(tx.is_valid()) - def validate_data_series(self): - # price_data_series is missing in the input + def test_missing_data_series(self): with self.assertRaises(XRPLModelException): OracleSet( account=_ACCOUNT, @@ -48,6 +47,7 @@ def validate_data_series(self): last_update_time=int(time.time()), ) + def test_exceed_length_price_data_series(self): # price_data_series exceeds the mandated length (10 elements) with self.assertRaises(XRPLModelException): OracleSet( @@ -93,7 +93,7 @@ def validate_data_series(self): ], ) - def validate_provider_field(self): + def test_valid_provider_field(self): tx = OracleSet( account=_ACCOUNT, oracle_document_id=1, @@ -112,7 +112,7 @@ def validate_provider_field(self): ) self.assertTrue(tx.is_valid()) - # empty provider + def test_empty_provider_field(self): with self.assertRaises(XRPLModelException): OracleSet( account=_ACCOUNT, @@ -122,6 +122,7 @@ def validate_provider_field(self): last_update_time=int(time.time()), ) + def test_long_provider_field(self): # provider exceeds MAX_ORACLE_PROVIDER characters with self.assertRaises(XRPLModelException): OracleSet( @@ -132,7 +133,7 @@ def validate_provider_field(self): last_update_time=int(time.time()), ) - def validate_uri_field(self): + def test_valid_uri_field(self): tx = OracleSet( account=_ACCOUNT, oracle_document_id=1, @@ -151,7 +152,7 @@ def validate_uri_field(self): ) self.assertTrue(tx.is_valid()) - # empty URI + def test_empty_uri_field(self): with self.assertRaises(XRPLModelException): OracleSet( account=_ACCOUNT, @@ -162,6 +163,7 @@ def validate_uri_field(self): uri="", ) + def test_long_uri_field(self): # URI exceeds MAX_ORACLE_URI characters with self.assertRaises(XRPLModelException): OracleSet( @@ -173,7 +175,7 @@ def validate_uri_field(self): uri=("x" * (MAX_ORACLE_URI + 1)), ) - def validate_asset_class_field(self): + def test_valid_asset_class_field(self): tx = OracleSet( account=_ACCOUNT, oracle_document_id=1, @@ -192,7 +194,7 @@ def validate_asset_class_field(self): ) self.assertTrue(tx.is_valid()) - # empty asset class + def test_empty_asset_class_field(self): with self.assertRaises(XRPLModelException): OracleSet( account=_ACCOUNT, @@ -202,6 +204,7 @@ def validate_asset_class_field(self): asset_class="", ) + def test_long_asset_class_field(self): # URI exceeds MAX_ORACLE_SYMBOL_CLASS characters with self.assertRaises(XRPLModelException): OracleSet( diff --git a/xrpl/models/requests/get_aggregate_price.py b/xrpl/models/requests/get_aggregate_price.py index 7f0013c0b..d8efdd017 100644 --- a/xrpl/models/requests/get_aggregate_price.py +++ b/xrpl/models/requests/get_aggregate_price.py @@ -18,25 +18,29 @@ @require_kwargs_on_init @dataclass(frozen=True) class GetAggregatePrice(Request): - """This request returns aggregate stats pertaining to the specified input oracles""" + """ + The get_aggregate_price method retrieves the aggregate price of specified Oracle + objects, returning three price statistics: mean, median, and trimmed mean. + """ method: RequestMethod = field(default=RequestMethod.GET_AGGREGATE_PRICE, init=False) base_asset: str = REQUIRED # type: ignore - """base_asset is the asset to be priced""" + """The currency code of the asset to be priced""" quote_asset: str = REQUIRED # type: ignore - """quote_asset is the denomination in which the prices are expressed""" + """The currency code of the asset to quote the price of the base asset""" oracles: List[Oracle] = REQUIRED # type: ignore - """oracles is an array of oracle objects to aggregate over""" + """The oracle identifier""" trim: Optional[int] = None - """percentage of outliers to trim""" + """The percentage of outliers to trim. Valid trim range is 1-25. If included, the + API returns statistics for the trimmed mean""" time_threshold: Optional[int] = None - """time_threshold : defines a range of prices to include based on the timestamp - range - {most recent, most recent - time_threshold}""" + """Defines a time range in seconds for filtering out older price data. Default + value is 0, which doesn't filter any data""" def _get_errors(self: GetAggregatePrice) -> Dict[str, str]: errors = super()._get_errors() @@ -53,8 +57,7 @@ class Oracle(TypedDict): """Represents one Oracle element. It is used in GetAggregatePrice request""" oracle_document_id: int = REQUIRED # type: ignore - """oracle_document_id is a unique identifier of the Price Oracle for the given - Account""" + """A unique identifier of the price oracle for the Account""" account: str = REQUIRED # type: ignore - """account is the Oracle's account.""" + """The XRPL account that controls the Oracle object""" diff --git a/xrpl/models/transactions/oracle_delete.py b/xrpl/models/transactions/oracle_delete.py index 54bf890b0..1e8def6c0 100644 --- a/xrpl/models/transactions/oracle_delete.py +++ b/xrpl/models/transactions/oracle_delete.py @@ -13,15 +13,13 @@ @require_kwargs_on_init @dataclass(frozen=True) class OracleDelete(Transaction): - """Represents an OracleDelete transaction.""" + """Delete an Oracle ledger entry.""" account: str = REQUIRED # type: ignore - """Account is the account that has the Oracle update and delete privileges. - This field corresponds to the Owner field on the PriceOracle ledger object.""" + """This account must match the account in the Owner field of the Oracle object.""" oracle_document_id: int = REQUIRED # type: ignore - """OracleDocumentID is a unique identifier of the Price Oracle for the given - Account.""" + """A unique identifier of the price oracle for the Account.""" transaction_type: TransactionType = field( default=TransactionType.ORACLE_DELETE, diff --git a/xrpl/models/transactions/oracle_set.py b/xrpl/models/transactions/oracle_set.py index b136cca1a..34bf92a4c 100644 --- a/xrpl/models/transactions/oracle_set.py +++ b/xrpl/models/transactions/oracle_set.py @@ -20,33 +20,51 @@ @require_kwargs_on_init @dataclass(frozen=True) class OracleSet(Transaction): - """Represents a OracleSet transaction.""" + """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: + + Create or own the XRPL account in the Owner field and have enough XRP to meet the + reserve and transaction fee requirements. + Publish the XRPL account public key, so it can be used for verification by dApps. + Publish a registry of available price oracles with their unique OracleDocumentID . + """ account: str = REQUIRED # type: ignore - """Account is the XRPL account that has update and delete privileges on the Oracle - being set. This field corresponds to the Owner field on the PriceOracle ledger - object.""" + """This account must match the account in the Owner field of the Oracle object.""" oracle_document_id: int = REQUIRED # type: ignore - """OracleDocumentID is a unique identifier of the Price Oracle for the given - Account.""" + """A unique identifier of the price oracle for the Account.""" provider: Optional[str] = None """ This field must be hex-encoded. You can use `xrpl.utils.str_to_hex` to convert a UTF-8 string to hex. + + 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. """ uri: Optional[str] = None """ This field must be hex-encoded. You can use `xrpl.utils.str_to_hex` to convert a UTF-8 string to hex. + + An optional Universal Resource Identifier to reference price data off-chain. This + field is limited to 256 bytes. """ asset_class: Optional[str] = None """ This field must be hex-encoded. You can use `xrpl.utils.str_to_hex` to convert a UTF-8 string to hex. + + 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. """ last_update_time: int = REQUIRED # type: ignore @@ -55,8 +73,8 @@ class OracleSet(Transaction): January 1, 1970 (00:00 UTC).""" price_data_series: List[PriceData] = REQUIRED # type: ignore - """PriceDataSeries is an array of up to ten PriceData objects, where PriceData - represents the price information for a token pair""" + """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.""" transaction_type: TransactionType = field( default=TransactionType.ORACLE_SET, @@ -67,41 +85,38 @@ def _get_errors(self: OracleSet) -> Dict[str, str]: errors = super()._get_errors() # If price_data_series is not set, do not perform further validation - if "price_data_series" not in errors and ( - len(self.price_data_series) == 0 - or len(self.price_data_series) > MAX_ORACLE_DATA_SERIES - ): - errors["OracleSet: price_data_series"] = ( - "The Price Data Series list must have a length of >0 and <" - + str(MAX_ORACLE_DATA_SERIES) - + "." - ) - - if self.asset_class and ( - len(self.asset_class) == 0 - or len(self.asset_class) > MAX_ORACLE_SYMBOL_CLASS - ): - errors["OracleSet: asset_class"] = ( - "The asset_class field must have a length of >0 and <" - + str(MAX_ORACLE_SYMBOL_CLASS) - + "." - ) - - if self.provider and ( - len(self.provider) == 0 or len(self.provider) > MAX_ORACLE_PROVIDER + if "price_data_series" not in errors and len(self.price_data_series) == 0: + errors["price_data_series"] = "Field must have a length greater than 0." + + if ( + "price_data_series" not in errors + and len(self.price_data_series) > MAX_ORACLE_DATA_SERIES ): - errors["OracleSet: provider"] = ( - "The provider field must have a length of >0 and <" - + str(MAX_ORACLE_PROVIDER) - + "." - ) - - if self.uri and (len(self.uri) == 0 or len(self.uri) > MAX_ORACLE_URI): - errors["OracleSet: uri"] = ( - "The uri field must have a length of >0 and <" - + str(MAX_ORACLE_URI) - + "." - ) + errors[ + "price_data_series" + ] = f"Field must have a length less than {MAX_ORACLE_DATA_SERIES}." + + if self.asset_class and len(self.asset_class) == 0: + errors["asset_class"] = "Field must have a length greater than 0." + + if self.asset_class and len(self.asset_class) > MAX_ORACLE_SYMBOL_CLASS: + errors[ + "asset_class" + ] = f"Field must have a length less than {MAX_ORACLE_SYMBOL_CLASS}." + + if self.provider and len(self.provider) == 0: + errors["provider"] = "Field must have a length greater than 0." + + if self.provider and len(self.provider) > MAX_ORACLE_PROVIDER: + errors[ + "provider" + ] = f"Field must have a length less than {MAX_ORACLE_PROVIDER}." + + if self.uri and len(self.uri) == 0: + errors["uri"] = "Field must have a length greater than 0." + + if self.uri and len(self.uri) > MAX_ORACLE_URI: + errors["uri"] = f"Field must have a length less than {MAX_ORACLE_URI}." return errors @@ -112,17 +127,21 @@ class PriceData(NestedModel): """Represents one PriceData element. It is used in OracleSet transaction""" base_asset: str = REQUIRED # type: ignore - """BaseAsset refers to the primary asset within a trading pair. It is the asset - against which the price of the quote asset is quoted.""" + """The primary asset in a trading pair. Any valid identifier, such as a stock + symbol, bond CUSIP, or currency code is allowed. For example, in the BTC/USD pair, + BTC is the base asset; in 912810RR9/BTC, 912810RR9 is the base asset.""" quote_asset: str = REQUIRED # type: ignore - """QuoteAsset represents the secondary or quote asset in a trading pair. It denotes - the price of one unit of the base asset.""" + """The quote asset in a trading pair. The quote asset denotes the price of one unit + of the base asset. For example, in the BTC/USD pair, BTC is the base asset; in + 912810RR9/BTC, 912810RR9 is the base asset.""" asset_price: Optional[int] = None - """AssetPrice is the scaled asset price, which is the price value after applying - the scaling factor.""" + """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.""" scale: Optional[int] = None - """Scale is the price's scaling factor. - It represents the price's precision level. """ + """The scaling factor to apply to an asset price. For example, if Scale is 6 and + original price is 0.155, then the scaled price is 155000. Valid scale ranges are + 0-10. It's not included if the last update transaction didn't include the + BaseAsset/QuoteAsset pair.""" From 3957867b12e9b76120b97bffd6a9d8d1b64f3cfa Mon Sep 17 00:00:00 2001 From: Chenna Keshava Date: Tue, 7 May 2024 14:38:09 -0700 Subject: [PATCH 23/29] use "is not None" to check for non-existence of an optional field --- xrpl/models/transactions/oracle_set.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/xrpl/models/transactions/oracle_set.py b/xrpl/models/transactions/oracle_set.py index 34bf92a4c..f7b905804 100644 --- a/xrpl/models/transactions/oracle_set.py +++ b/xrpl/models/transactions/oracle_set.py @@ -96,26 +96,29 @@ def _get_errors(self: OracleSet) -> Dict[str, str]: "price_data_series" ] = f"Field must have a length less than {MAX_ORACLE_DATA_SERIES}." - if self.asset_class and len(self.asset_class) == 0: + if self.asset_class is not None and len(self.asset_class) == 0: errors["asset_class"] = "Field must have a length greater than 0." - if self.asset_class and len(self.asset_class) > MAX_ORACLE_SYMBOL_CLASS: + if ( + self.asset_class is not None + and len(self.asset_class) > MAX_ORACLE_SYMBOL_CLASS + ): errors[ "asset_class" ] = f"Field must have a length less than {MAX_ORACLE_SYMBOL_CLASS}." - if self.provider and len(self.provider) == 0: + if self.provider is not None and len(self.provider) == 0: errors["provider"] = "Field must have a length greater than 0." - if self.provider and len(self.provider) > MAX_ORACLE_PROVIDER: + if self.provider is not None and len(self.provider) > MAX_ORACLE_PROVIDER: errors[ "provider" ] = f"Field must have a length less than {MAX_ORACLE_PROVIDER}." - if self.uri and len(self.uri) == 0: + if self.uri is not None and len(self.uri) == 0: errors["uri"] = "Field must have a length greater than 0." - if self.uri and len(self.uri) > MAX_ORACLE_URI: + if self.uri is not None and len(self.uri) > MAX_ORACLE_URI: errors["uri"] = f"Field must have a length less than {MAX_ORACLE_URI}." return errors From 8251981e2fa48ae404f90983c91249112dee7de4 Mon Sep 17 00:00:00 2001 From: Chenna Keshava Date: Wed, 8 May 2024 10:53:04 -0700 Subject: [PATCH 24/29] fixed the error message in OracleSet validation --- xrpl/models/transactions/oracle_set.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/xrpl/models/transactions/oracle_set.py b/xrpl/models/transactions/oracle_set.py index f7b905804..a5ae6b88e 100644 --- a/xrpl/models/transactions/oracle_set.py +++ b/xrpl/models/transactions/oracle_set.py @@ -92,10 +92,17 @@ def _get_errors(self: OracleSet) -> Dict[str, str]: "price_data_series" not in errors and len(self.price_data_series) > MAX_ORACLE_DATA_SERIES ): + errors["price_data_series"] = ( + "Field must have a length less than" + + " or equal to " + + str(MAX_ORACLE_DATA_SERIES) + ) + errors[ "price_data_series" - ] = f"Field must have a length less than {MAX_ORACLE_DATA_SERIES}." - + ] = f""" Field must have a length less than or + equal to {MAX_ORACLE_DATA_SERIES} + """ if self.asset_class is not None and len(self.asset_class) == 0: errors["asset_class"] = "Field must have a length greater than 0." @@ -105,7 +112,8 @@ def _get_errors(self: OracleSet) -> Dict[str, str]: ): errors[ "asset_class" - ] = f"Field must have a length less than {MAX_ORACLE_SYMBOL_CLASS}." + ] = f"""Field must have a length less than or equal to + {MAX_ORACLE_SYMBOL_CLASS}.""" if self.provider is not None and len(self.provider) == 0: errors["provider"] = "Field must have a length greater than 0." @@ -113,13 +121,15 @@ def _get_errors(self: OracleSet) -> Dict[str, str]: if self.provider is not None and len(self.provider) > MAX_ORACLE_PROVIDER: errors[ "provider" - ] = f"Field must have a length less than {MAX_ORACLE_PROVIDER}." + ] = f"Field must have a length less than or equal to {MAX_ORACLE_PROVIDER}." if self.uri is not None and len(self.uri) == 0: errors["uri"] = "Field must have a length greater than 0." if self.uri is not None and len(self.uri) > MAX_ORACLE_URI: - errors["uri"] = f"Field must have a length less than {MAX_ORACLE_URI}." + errors[ + "uri" + ] = f"Field must have a length less than or equal to {MAX_ORACLE_URI}." return errors From 56ff610c40dba9e9bdd0833cac63e094839b8f65 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S <21219765+ckeshava@users.noreply.github.com> Date: Wed, 8 May 2024 14:20:06 -0700 Subject: [PATCH 25/29] Update tests/unit/models/requests/test_ledger_entry.py Co-authored-by: Omar Khan --- tests/unit/models/requests/test_ledger_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/models/requests/test_ledger_entry.py b/tests/unit/models/requests/test_ledger_entry.py index d30626e97..367291b60 100644 --- a/tests/unit/models/requests/test_ledger_entry.py +++ b/tests/unit/models/requests/test_ledger_entry.py @@ -121,7 +121,7 @@ def test_has_multiple_query_params_is_invalid(self): ) # fetch a valid PriceOracle object - def test_get_priceoracle(self): + def test_get_price_oracle(self): # oracle_document_id is specified as uint req = LedgerEntry( oracle=Oracle( From 7f39f50557b79d76a321ec4fe6a3defa9da72ee3 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S <21219765+ckeshava@users.noreply.github.com> Date: Wed, 8 May 2024 14:20:21 -0700 Subject: [PATCH 26/29] Update tests/unit/models/requests/test_ledger_entry.py Co-authored-by: Omar Khan --- tests/unit/models/requests/test_ledger_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/models/requests/test_ledger_entry.py b/tests/unit/models/requests/test_ledger_entry.py index 367291b60..56ba0e84f 100644 --- a/tests/unit/models/requests/test_ledger_entry.py +++ b/tests/unit/models/requests/test_ledger_entry.py @@ -140,7 +140,7 @@ def test_get_price_oracle(self): ) self.assertTrue(req.is_valid()) - def test_invalid_priceoracle_object(self): + def test_invalid_price_oracle_object(self): # missing oracle_document_id with self.assertRaises(XRPLModelException): LedgerEntry( From db3d27f4458d2c22c84c877112715884a26e29a8 Mon Sep 17 00:00:00 2001 From: Chenna Keshava Date: Wed, 8 May 2024 15:26:14 -0700 Subject: [PATCH 27/29] rename test files to match transaction names --- .../{test_delete_oracle.py => test_oracle_delete.py} | 2 +- .../transactions/{test_set_oracle.py => test_oracle_set.py} | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) rename tests/unit/models/transactions/{test_delete_oracle.py => test_oracle_delete.py} (89%) rename tests/unit/models/transactions/{test_set_oracle.py => test_oracle_set.py} (98%) diff --git a/tests/unit/models/transactions/test_delete_oracle.py b/tests/unit/models/transactions/test_oracle_delete.py similarity index 89% rename from tests/unit/models/transactions/test_delete_oracle.py rename to tests/unit/models/transactions/test_oracle_delete.py index 48e259d06..84cf112c3 100644 --- a/tests/unit/models/transactions/test_delete_oracle.py +++ b/tests/unit/models/transactions/test_oracle_delete.py @@ -5,7 +5,7 @@ _ACCOUNT = "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW" -class TestSetOracle(TestCase): +class TestDeleteOracle(TestCase): def test_valid(self): tx = OracleDelete( account=_ACCOUNT, diff --git a/tests/unit/models/transactions/test_set_oracle.py b/tests/unit/models/transactions/test_oracle_set.py similarity index 98% rename from tests/unit/models/transactions/test_set_oracle.py rename to tests/unit/models/transactions/test_oracle_set.py index 3e7498c73..d7cf1cb94 100644 --- a/tests/unit/models/transactions/test_set_oracle.py +++ b/tests/unit/models/transactions/test_oracle_set.py @@ -122,7 +122,7 @@ def test_empty_provider_field(self): last_update_time=int(time.time()), ) - def test_long_provider_field(self): + def test_lengthy_provider_field(self): # provider exceeds MAX_ORACLE_PROVIDER characters with self.assertRaises(XRPLModelException): OracleSet( @@ -163,7 +163,7 @@ def test_empty_uri_field(self): uri="", ) - def test_long_uri_field(self): + def test_lengthy_uri_field(self): # URI exceeds MAX_ORACLE_URI characters with self.assertRaises(XRPLModelException): OracleSet( @@ -204,7 +204,7 @@ def test_empty_asset_class_field(self): asset_class="", ) - def test_long_asset_class_field(self): + def test_lengthy_asset_class_field(self): # URI exceeds MAX_ORACLE_SYMBOL_CLASS characters with self.assertRaises(XRPLModelException): OracleSet( From 72afddfcad7ece7e236653c3229c1416859a3f93 Mon Sep 17 00:00:00 2001 From: Chenna Keshava Date: Wed, 8 May 2024 18:00:27 -0700 Subject: [PATCH 28/29] validate the exception message in negative test cases --- .../models/transactions/test_oracle_set.py | 105 ++++++++++++++++-- xrpl/models/transactions/oracle_set.py | 16 +-- 2 files changed, 102 insertions(+), 19 deletions(-) diff --git a/tests/unit/models/transactions/test_oracle_set.py b/tests/unit/models/transactions/test_oracle_set.py index d7cf1cb94..db6db429b 100644 --- a/tests/unit/models/transactions/test_oracle_set.py +++ b/tests/unit/models/transactions/test_oracle_set.py @@ -38,7 +38,7 @@ def test_valid(self): self.assertTrue(tx.is_valid()) def test_missing_data_series(self): - with self.assertRaises(XRPLModelException): + with self.assertRaises(XRPLModelException) as err: OracleSet( account=_ACCOUNT, oracle_document_id=1, @@ -47,9 +47,14 @@ def test_missing_data_series(self): last_update_time=int(time.time()), ) + self.assertEqual( + err.exception.args[0], + "{'price_data_series': " + "'price_data_series is not set'}", + ) + def test_exceed_length_price_data_series(self): # price_data_series exceeds the mandated length (10 elements) - with self.assertRaises(XRPLModelException): + with self.assertRaises(XRPLModelException) as err: OracleSet( account=_ACCOUNT, oracle_document_id=1, @@ -93,6 +98,12 @@ def test_exceed_length_price_data_series(self): ], ) + self.assertEqual( + err.exception.args[0], + "{'price_data_series': 'Field must " + + "have a length less than or equal to 10'}", + ) + def test_valid_provider_field(self): tx = OracleSet( account=_ACCOUNT, @@ -113,26 +124,52 @@ def test_valid_provider_field(self): self.assertTrue(tx.is_valid()) def test_empty_provider_field(self): - with self.assertRaises(XRPLModelException): + with self.assertRaises(XRPLModelException) as err: OracleSet( account=_ACCOUNT, oracle_document_id=1, provider=_EMPTY_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 + ), + ], ) + self.assertEqual( + err.exception.args[0], + "{'provider': 'Field must have a " + "length greater than 0.'}", + ) + def test_lengthy_provider_field(self): # provider exceeds MAX_ORACLE_PROVIDER characters - with self.assertRaises(XRPLModelException): + with self.assertRaises(XRPLModelException) as err: OracleSet( account=_ACCOUNT, oracle_document_id=1, provider=_LENGTHY_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 + ), + ], ) + self.assertEqual( + err.exception.args[0], + "{'provider': 'Field must have a " + "length less than or equal to 256.'}", + ) + def test_valid_uri_field(self): tx = OracleSet( account=_ACCOUNT, @@ -153,7 +190,7 @@ def test_valid_uri_field(self): self.assertTrue(tx.is_valid()) def test_empty_uri_field(self): - with self.assertRaises(XRPLModelException): + with self.assertRaises(XRPLModelException) as err: OracleSet( account=_ACCOUNT, oracle_document_id=1, @@ -161,11 +198,24 @@ def test_empty_uri_field(self): asset_class=_ASSET_CLASS, last_update_time=int(time.time()), uri="", + 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 + ), + ], ) + self.assertEqual( + err.exception.args[0], + "{'uri': 'Field must have a" + " length greater than 0.'}", + ) + def test_lengthy_uri_field(self): # URI exceeds MAX_ORACLE_URI characters - with self.assertRaises(XRPLModelException): + with self.assertRaises(XRPLModelException) as err: OracleSet( account=_ACCOUNT, oracle_document_id=1, @@ -173,8 +223,21 @@ def test_lengthy_uri_field(self): asset_class=_ASSET_CLASS, last_update_time=int(time.time()), uri=("x" * (MAX_ORACLE_URI + 1)), + 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 + ), + ], ) + self.assertEqual( + err.exception.args[0], + "{'uri': 'Field must have a" + " length less than or equal to 256.'}", + ) + def test_valid_asset_class_field(self): tx = OracleSet( account=_ACCOUNT, @@ -195,22 +258,48 @@ def test_valid_asset_class_field(self): self.assertTrue(tx.is_valid()) def test_empty_asset_class_field(self): - with self.assertRaises(XRPLModelException): + with self.assertRaises(XRPLModelException) as err: OracleSet( account=_ACCOUNT, oracle_document_id=1, provider=_PROVIDER, last_update_time=int(time.time()), asset_class="", + 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 + ), + ], ) + self.assertEqual( + err.exception.args[0], + "{'asset_class': 'Field must have" + " a length greater than 0.'}", + ) + def test_lengthy_asset_class_field(self): # URI exceeds MAX_ORACLE_SYMBOL_CLASS characters - with self.assertRaises(XRPLModelException): + with self.assertRaises(XRPLModelException) as err: OracleSet( account=_ACCOUNT, oracle_document_id=1, provider=_PROVIDER, last_update_time=int(time.time()), asset_class=("x" * (MAX_ORACLE_SYMBOL_CLASS + 1)), + 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 + ), + ], ) + + self.assertEqual( + err.exception.args[0], + "{'asset_class': 'Field must have" + " a length less than or equal to 16'}", + ) diff --git a/xrpl/models/transactions/oracle_set.py b/xrpl/models/transactions/oracle_set.py index a5ae6b88e..5b378055b 100644 --- a/xrpl/models/transactions/oracle_set.py +++ b/xrpl/models/transactions/oracle_set.py @@ -94,15 +94,9 @@ def _get_errors(self: OracleSet) -> Dict[str, str]: ): errors["price_data_series"] = ( "Field must have a length less than" - + " or equal to " - + str(MAX_ORACLE_DATA_SERIES) + f" or equal to {MAX_ORACLE_DATA_SERIES}" ) - errors[ - "price_data_series" - ] = f""" Field must have a length less than or - equal to {MAX_ORACLE_DATA_SERIES} - """ if self.asset_class is not None and len(self.asset_class) == 0: errors["asset_class"] = "Field must have a length greater than 0." @@ -110,10 +104,10 @@ def _get_errors(self: OracleSet) -> Dict[str, str]: self.asset_class is not None and len(self.asset_class) > MAX_ORACLE_SYMBOL_CLASS ): - errors[ - "asset_class" - ] = f"""Field must have a length less than or equal to - {MAX_ORACLE_SYMBOL_CLASS}.""" + errors["asset_class"] = ( + "Field must have a length less than" + f" or equal to {MAX_ORACLE_SYMBOL_CLASS}" + ) if self.provider is not None and len(self.provider) == 0: errors["provider"] = "Field must have a length greater than 0." From 49370f1ba3b433ffd0def32524b1b486ffa4d1a2 Mon Sep 17 00:00:00 2001 From: Chenna Keshava Date: Thu, 9 May 2024 11:50:02 -0700 Subject: [PATCH 29/29] include validity checks on LastUpdateTime --- .../models/transactions/test_oracle_set.py | 47 +++++++++++++++++++ xrpl/models/transactions/oracle_set.py | 14 ++++++ 2 files changed, 61 insertions(+) diff --git a/tests/unit/models/transactions/test_oracle_set.py b/tests/unit/models/transactions/test_oracle_set.py index db6db429b..d99903311 100644 --- a/tests/unit/models/transactions/test_oracle_set.py +++ b/tests/unit/models/transactions/test_oracle_set.py @@ -4,6 +4,7 @@ from xrpl.models.exceptions import XRPLModelException from xrpl.models.transactions import OracleSet from xrpl.models.transactions.oracle_set import ( + EPOCH_OFFSET, MAX_ORACLE_PROVIDER, MAX_ORACLE_SYMBOL_CLASS, MAX_ORACLE_URI, @@ -303,3 +304,49 @@ def test_lengthy_asset_class_field(self): err.exception.args[0], "{'asset_class': 'Field must have" + " a length less than or equal to 16'}", ) + + def test_early_last_update_time_field(self): + with self.assertRaises(XRPLModelException) as err: + OracleSet( + account=_ACCOUNT, + oracle_document_id=1, + provider=_PROVIDER, + asset_class=_ASSET_CLASS, + last_update_time=EPOCH_OFFSET - 1, + 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 + ), + ], + ) + + self.assertEqual( + err.exception.args[0], + "{'last_update_time': 'LastUpdateTime" + + " must be greater than or equal to Ripple-Epoch 946684800.0 seconds'}", + ) + + # Validity depends on the time of the Last Closed Ledger. This test verifies the + # validity with respect to the Ripple Epoch time + def test_valid_last_update_time(self): + # Note: This test fails in an integration test because it's older than 300s + # with respect to the LastClosedLedger + tx = OracleSet( + account=_ACCOUNT, + oracle_document_id=1, + provider=_PROVIDER, + asset_class=_ASSET_CLASS, + last_update_time=EPOCH_OFFSET, + 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 + ), + ], + ) + self.assertTrue(tx.is_valid()) diff --git a/xrpl/models/transactions/oracle_set.py b/xrpl/models/transactions/oracle_set.py index 5b378055b..91619ce5a 100644 --- a/xrpl/models/transactions/oracle_set.py +++ b/xrpl/models/transactions/oracle_set.py @@ -2,6 +2,7 @@ from __future__ import annotations +import datetime from dataclasses import dataclass, field from typing import Dict, List, Optional @@ -16,6 +17,12 @@ MAX_ORACLE_URI = 256 MAX_ORACLE_SYMBOL_CLASS = 16 +# epoch offset must equal 946684800 seconds. It represents the diff between the +# genesis of Unix time and Ripple-Epoch time +EPOCH_OFFSET = ( + datetime.datetime(2000, 1, 1) - datetime.datetime(1970, 1, 1) +).total_seconds() + @require_kwargs_on_init @dataclass(frozen=True) @@ -125,6 +132,13 @@ def _get_errors(self: OracleSet) -> Dict[str, str]: "uri" ] = f"Field must have a length less than or equal to {MAX_ORACLE_URI}." + # check on the last_update_time + if self.last_update_time < EPOCH_OFFSET: + errors["last_update_time"] = ( + "LastUpdateTime must be greater than or equal" + f" to Ripple-Epoch {EPOCH_OFFSET} seconds" + ) + return errors