From 79b5bc8fd3b060dda1970a97d44c080f5befd26a Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Mon, 10 Mar 2025 18:01:33 -0400 Subject: [PATCH 01/18] basics --- xrpl/asyncio/transaction/main.py | 3 +++ xrpl/models/transactions/escrow_create.py | 13 +++++++++++++ 2 files changed, 16 insertions(+) diff --git a/xrpl/asyncio/transaction/main.py b/xrpl/asyncio/transaction/main.py index a9f0f6f9c..029be2a15 100644 --- a/xrpl/asyncio/transaction/main.py +++ b/xrpl/asyncio/transaction/main.py @@ -479,6 +479,9 @@ async def _calculate_fee_per_transaction_type( # BaseFee × (33 + (Fulfillment size in bytes / 16)) base_fee = math.ceil(net_fee * (33 + (len(fulfillment_bytes) / 16))) + if transaction.transaction_type == TransactionType.ESCROW_CANCEL: + base_fee += 1000 + # AccountDelete Transaction if transaction.transaction_type in ( TransactionType.ACCOUNT_DELETE, diff --git a/xrpl/models/transactions/escrow_create.py b/xrpl/models/transactions/escrow_create.py index 3d564ed53..59bea4867 100644 --- a/xrpl/models/transactions/escrow_create.py +++ b/xrpl/models/transactions/escrow_create.py @@ -67,6 +67,10 @@ class EscrowCreate(Transaction): fulfilled. """ + finish_function: Optional[str] = None + + data: Optional[str] = None + transaction_type: TransactionType = field( default=TransactionType.ESCROW_CREATE, init=False, @@ -82,5 +86,14 @@ def _get_errors(self: Self) -> Dict[str, str]: errors["EscrowCreate"] = ( "The finish_after time must be before the cancel_after time." ) + if ( + self.cancel_after is None + and self.condition is None + and self.finish_function is None + ): + errors["EscrowCreate"] = ( + "At least one of cancel_after, condition, or finish_function must be " + "set." + ) return errors From db9234b0676db0f80ac3ccb5da9f9e5c1cd32e33 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Tue, 11 Mar 2025 14:02:18 -0400 Subject: [PATCH 02/18] add tests, fix issues, refactor escrow tests --- tests/integration/it_utils.py | 43 +----- tests/integration/transactions/test_escrow.py | 135 ++++++++++++++++++ .../transactions/test_escrow_cancel.py | 27 ---- .../transactions/test_escrow_create.py | 38 ----- .../transactions/test_escrow_finish.py | 38 ----- xrpl/asyncio/transaction/main.py | 7 +- .../binarycodec/definitions/definitions.json | 31 ++++ xrpl/models/transactions/escrow_create.py | 4 +- 8 files changed, 178 insertions(+), 145 deletions(-) create mode 100644 tests/integration/transactions/test_escrow.py delete mode 100644 tests/integration/transactions/test_escrow_cancel.py delete mode 100644 tests/integration/transactions/test_escrow_create.py delete mode 100644 tests/integration/transactions/test_escrow_finish.py diff --git a/tests/integration/it_utils.py b/tests/integration/it_utils.py index ebd2e2861..79856ffdc 100644 --- a/tests/integration/it_utils.py +++ b/tests/integration/it_utils.py @@ -3,7 +3,6 @@ import asyncio import importlib import inspect -from threading import Timer as ThreadingTimer from time import sleep from typing import Any, Dict, cast @@ -14,7 +13,7 @@ from xrpl.clients import Client, JsonRpcClient, WebsocketClient from xrpl.clients.sync_client import SyncClient from xrpl.constants import CryptoAlgorithm -from xrpl.models import GenericRequest, Payment, Request, Response, Transaction +from xrpl.models import GenericRequest, Payment, Response, Transaction from xrpl.models.amounts.issued_currency_amount import IssuedCurrencyAmount from xrpl.models.currencies.issued_currency import IssuedCurrency from xrpl.models.currencies.xrp import XRP @@ -69,40 +68,6 @@ LEDGER_ACCEPT_TIME = 0.1 -class AsyncTestTimer: - def __init__( - self, - client: AsyncClient, - delay: float = LEDGER_ACCEPT_TIME, - request: Request = LEDGER_ACCEPT_REQUEST, - ): - self._client = client - self._delay = delay - self._request = request - self._task = asyncio.ensure_future(self._job()) - - async def _job(self): - await asyncio.sleep(self._delay) - await self._client.request(self._request) - - def cancel(self): - self._task.cancel() - - -class SyncTestTimer: - def __init__( - self, - client: SyncClient, - delay: float = LEDGER_ACCEPT_TIME, - request: Request = LEDGER_ACCEPT_REQUEST, - ): - self._timer = ThreadingTimer(delay, client.request, (request,)) - self._timer.start() - - def cancel(self): - self._timer.cancel() - - def fund_wallet(wallet: Wallet) -> None: client = JSON_RPC_CLIENT payment = Payment( @@ -232,7 +197,8 @@ def accept_ledger( delay: float for how many seconds to wait before accepting ledger. """ client = _choose_client(use_json_client) - SyncTestTimer(client, delay) + sleep(delay) + client.request(LEDGER_ACCEPT_REQUEST) async def accept_ledger_async( @@ -247,7 +213,8 @@ async def accept_ledger_async( delay: float for how many seconds to wait before accepting ledger. """ client = _choose_client_async(use_json_client) - AsyncTestTimer(client, delay) + await asyncio.sleep(delay) + await client.request(LEDGER_ACCEPT_REQUEST) def _choose_client(use_json_client: bool) -> SyncClient: diff --git a/tests/integration/transactions/test_escrow.py b/tests/integration/transactions/test_escrow.py new file mode 100644 index 000000000..d99cfe302 --- /dev/null +++ b/tests/integration/transactions/test_escrow.py @@ -0,0 +1,135 @@ +from tests.integration.integration_test_case import IntegrationTestCase +from tests.integration.it_utils import ( + accept_ledger_async, + sign_and_reliable_submission_async, + test_async_and_sync, +) +from tests.integration.reusable_values import DESTINATION, WALLET +from xrpl.models import EscrowCancel, EscrowCreate, EscrowFinish, Ledger +from xrpl.models.response import ResponseStatus + +ACCOUNT = WALLET.address + +AMOUNT = "10000" +CONDITION = ( + "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100" +) +DESTINATION_TAG = 23480 +SOURCE_TAG = 11747 + +FINISH_FUNCTION = ( + "0061736d010000000105016000017f021b0108686f73745f6c69620e6765745f6c" + "65646765725f73716e0000030201000405017001010105030100100619037f0141" + "8080c0000b7f00418080c0000b7f00418080c0000b072d04066d656d6f72790200" + "05726561647900010a5f5f646174615f656e6403010b5f5f686561705f62617365" + "03020a0d010b0010808080800041044a0b006e046e616d65000e0d7761736d5f6c" + "69622e7761736d01430200395f5a4e387761736d5f6c696238686f73745f6c6962" + "31346765745f6c65646765725f73716e3137686663383539386237646539633036" + "64624501057265616479071201000f5f5f737461636b5f706f696e746572005509" + "70726f64756365727302086c616e6775616765010452757374000c70726f636573" + "7365642d62790105727573746325312e38332e302d6e696768746c792028633266" + "37346333663920323032342d30392d30392900490f7461726765745f6665617475" + "726573042b0a6d756c746976616c75652b0f6d757461626c652d676c6f62616c73" + "2b0f7265666572656e63652d74797065732b087369676e2d657874" +) + + +class TestEscrow(IntegrationTestCase): + @test_async_and_sync(globals()) + async def test_all_fields_cancel(self, client): + ledger = await client.request(Ledger(ledger_index="validated")) + close_time = ledger.result["ledger"]["close_time"] + escrow_create = EscrowCreate( + account=ACCOUNT, + amount=AMOUNT, + destination=DESTINATION.classic_address, + destination_tag=DESTINATION_TAG, + cancel_after=close_time + 3, + finish_after=close_time + 2, + source_tag=SOURCE_TAG, + ) + response = await sign_and_reliable_submission_async( + escrow_create, WALLET, client + ) + self.assertEqual(response.status, ResponseStatus.SUCCESS) + self.assertEqual(response.result["engine_result"], "tesSUCCESS") + sequence = response.result["tx_json"]["Sequence"] + # TODO: check account_objects + + for _ in range(4): + await accept_ledger_async() + + escrow_cancel = EscrowCancel( + account=ACCOUNT, + owner=ACCOUNT, + offer_sequence=sequence, + ) + response = await sign_and_reliable_submission_async( + escrow_cancel, WALLET, client + ) + self.assertEqual(response.status, ResponseStatus.SUCCESS) + self.assertEqual(response.result["engine_result"], "tesSUCCESS") + + @test_async_and_sync(globals()) + async def test_all_fields_finish(self, client): + ledger = await client.request(Ledger(ledger_index="validated")) + close_time = ledger.result["ledger"]["close_time"] + escrow_create = EscrowCreate( + account=ACCOUNT, + amount=AMOUNT, + destination=DESTINATION.classic_address, + destination_tag=DESTINATION_TAG, + finish_after=close_time + 2, + source_tag=SOURCE_TAG, + ) + response = await sign_and_reliable_submission_async( + escrow_create, WALLET, client + ) + self.assertEqual(response.status, ResponseStatus.SUCCESS) + self.assertEqual(response.result["engine_result"], "tesSUCCESS") + sequence = response.result["tx_json"]["Sequence"] + # TODO: check account_objects + + for _ in range(4): + await accept_ledger_async() + + escrow_finish = EscrowFinish( + account=ACCOUNT, + owner=ACCOUNT, + offer_sequence=sequence, + ) + response = await sign_and_reliable_submission_async( + escrow_finish, WALLET, client + ) + self.assertEqual(response.status, ResponseStatus.SUCCESS) + self.assertEqual(response.result["engine_result"], "tesSUCCESS") + + @test_async_and_sync(globals()) + async def test_finish_function(self, client): + ledger = await client.request(Ledger(ledger_index="validated")) + close_time = ledger.result["ledger"]["close_time"] + escrow_create = EscrowCreate( + account=ACCOUNT, + amount=AMOUNT, + destination=DESTINATION.classic_address, + finish_function=FINISH_FUNCTION, + cancel_after=close_time + 200, + ) + response = await sign_and_reliable_submission_async( + escrow_create, WALLET, client + ) + self.assertEqual(response.status, ResponseStatus.SUCCESS) + self.assertEqual(response.result["engine_result"], "tesSUCCESS") + sequence = response.result["tx_json"]["Sequence"] + # TODO: check account_objects + + escrow_finish = EscrowFinish( + account=ACCOUNT, + owner=ACCOUNT, + offer_sequence=sequence, + ) + response = await sign_and_reliable_submission_async( + escrow_finish, WALLET, client + ) + self.assertEqual(response.status, ResponseStatus.SUCCESS) + self.assertEqual(response.result["engine_result"], "tesSUCCESS") diff --git a/tests/integration/transactions/test_escrow_cancel.py b/tests/integration/transactions/test_escrow_cancel.py deleted file mode 100644 index 42cae81d6..000000000 --- a/tests/integration/transactions/test_escrow_cancel.py +++ /dev/null @@ -1,27 +0,0 @@ -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.response import ResponseStatus -from xrpl.models.transactions import EscrowCancel - -ACCOUNT = WALLET.address -OWNER = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn" -OFFER_SEQUENCE = 7 - - -class TestEscrowCancel(IntegrationTestCase): - @test_async_and_sync(globals()) - async def test_all_fields(self, client): - escrow_cancel = EscrowCancel( - account=ACCOUNT, - owner=OWNER, - offer_sequence=OFFER_SEQUENCE, - ) - response = await sign_and_reliable_submission_async( - escrow_cancel, WALLET, client - ) - # Actual engine_result is `tecNO_TARGET since OWNER account doesn't exist - self.assertEqual(response.status, ResponseStatus.SUCCESS) diff --git a/tests/integration/transactions/test_escrow_create.py b/tests/integration/transactions/test_escrow_create.py deleted file mode 100644 index 4e1df99ac..000000000 --- a/tests/integration/transactions/test_escrow_create.py +++ /dev/null @@ -1,38 +0,0 @@ -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 DESTINATION, WALLET -from xrpl.models import EscrowCreate, Ledger -from xrpl.models.response import ResponseStatus - -ACCOUNT = WALLET.address - -AMOUNT = "10000" -CONDITION = ( - "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100" -) -DESTINATION_TAG = 23480 -SOURCE_TAG = 11747 - - -class TestEscrowCreate(IntegrationTestCase): - @test_async_and_sync(globals()) - async def test_all_fields(self, client): - ledger = await client.request(Ledger(ledger_index="validated")) - close_time = ledger.result["ledger"]["close_time"] - escrow_create = EscrowCreate( - account=WALLET.classic_address, - amount=AMOUNT, - destination=DESTINATION.classic_address, - destination_tag=DESTINATION_TAG, - cancel_after=close_time + 3, - finish_after=close_time + 2, - source_tag=SOURCE_TAG, - ) - response = await sign_and_reliable_submission_async( - escrow_create, WALLET, client - ) - self.assertEqual(response.status, ResponseStatus.SUCCESS) - self.assertEqual(response.result["engine_result"], "tesSUCCESS") diff --git a/tests/integration/transactions/test_escrow_finish.py b/tests/integration/transactions/test_escrow_finish.py deleted file mode 100644 index a415cb3fa..000000000 --- a/tests/integration/transactions/test_escrow_finish.py +++ /dev/null @@ -1,38 +0,0 @@ -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.response import ResponseStatus -from xrpl.models.transactions import EscrowFinish - -# Special fee for EscrowFinish transactions that contain a fulfillment. -# See note here: https://xrpl.org/escrowfinish.html -FEE = "600000000" - -ACCOUNT = WALLET.address -OWNER = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn" -OFFER_SEQUENCE = 7 -CONDITION = ( - "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100" -) -FULFILLMENT = "A0028000" - - -class TestEscrowFinish(IntegrationTestCase): - @test_async_and_sync(globals()) - async def test_all_fields(self, client): - escrow_finish = EscrowFinish( - account=ACCOUNT, - owner=OWNER, - offer_sequence=OFFER_SEQUENCE, - condition=CONDITION, - fulfillment=FULFILLMENT, - ) - response = await sign_and_reliable_submission_async( - escrow_finish, WALLET, client - ) - # Actual engine_result will be 'tecNO_TARGET' since using non-extant - # account for OWNER - self.assertEqual(response.status, ResponseStatus.SUCCESS) diff --git a/xrpl/asyncio/transaction/main.py b/xrpl/asyncio/transaction/main.py index 029be2a15..2ea9e0f3e 100644 --- a/xrpl/asyncio/transaction/main.py +++ b/xrpl/asyncio/transaction/main.py @@ -22,6 +22,7 @@ SubmitOnly, Transaction, ) +from xrpl.models.transactions.escrow_create import EscrowCreate from xrpl.models.transactions.transaction import ( transaction_json_to_binary_codec_form as model_transaction_to_binary_codec, ) @@ -479,8 +480,10 @@ async def _calculate_fee_per_transaction_type( # BaseFee × (33 + (Fulfillment size in bytes / 16)) base_fee = math.ceil(net_fee * (33 + (len(fulfillment_bytes) / 16))) - if transaction.transaction_type == TransactionType.ESCROW_CANCEL: - base_fee += 1000 + if transaction.transaction_type == TransactionType.ESCROW_CREATE: + escrow_create = cast(EscrowCreate, transaction) + if escrow_create.finish_function is not None: + base_fee += 1000 # AccountDelete Transaction if transaction.transaction_type in ( diff --git a/xrpl/core/binarycodec/definitions/definitions.json b/xrpl/core/binarycodec/definitions/definitions.json index 92e90c8e6..f6c4984cd 100644 --- a/xrpl/core/binarycodec/definitions/definitions.json +++ b/xrpl/core/binarycodec/definitions/definitions.json @@ -670,6 +670,26 @@ "type": "UInt32" } ], + [ + "ExtensionComputeLimit", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 52, + "type": "UInt32" + } + ], + [ + "ExtensionSizeLimit", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 53, + "type": "UInt32" + } + ], [ "IndexNext", { @@ -1850,6 +1870,16 @@ "type": "Blob" } ], + [ + "FinishFunction", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": true, + "nth": 32, + "type": "Blob" + } + ], [ "Account", { @@ -2951,6 +2981,7 @@ "tecUNFUNDED_AMM": 162, "tecUNFUNDED_OFFER": 103, "tecUNFUNDED_PAYMENT": 104, + "tecWASM_REJECTED": 194, "tecXCHAIN_ACCOUNT_CREATE_PAST": 181, "tecXCHAIN_ACCOUNT_CREATE_TOO_MANY": 182, "tecXCHAIN_BAD_CLAIM_ID": 172, diff --git a/xrpl/models/transactions/escrow_create.py b/xrpl/models/transactions/escrow_create.py index 59bea4867..0ed8a85ab 100644 --- a/xrpl/models/transactions/escrow_create.py +++ b/xrpl/models/transactions/escrow_create.py @@ -87,12 +87,12 @@ def _get_errors(self: Self) -> Dict[str, str]: "The finish_after time must be before the cancel_after time." ) if ( - self.cancel_after is None + self.finish_after is None and self.condition is None and self.finish_function is None ): errors["EscrowCreate"] = ( - "At least one of cancel_after, condition, or finish_function must be " + "At least one of finish_after, condition, or finish_function must be " "set." ) From 09f98af7dcdebd9b5fec70bc09d3d5f2bdd3803e Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Tue, 11 Mar 2025 14:13:12 -0400 Subject: [PATCH 03/18] add EscrowCancel --- .../models/transactions/test_escrow_create.py | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/tests/unit/models/transactions/test_escrow_create.py b/tests/unit/models/transactions/test_escrow_create.py index d7f301a7a..b94c7878a 100644 --- a/tests/unit/models/transactions/test_escrow_create.py +++ b/tests/unit/models/transactions/test_escrow_create.py @@ -5,6 +5,30 @@ class TestEscrowCreate(TestCase): + def test_all_fields_valid(self): + account = "r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ" + amount = "amount" + cancel_after = 3 + destination = "destination" + destination_tag = 1 + fee = "0.00001" + finish_after = 2 + finish_function = "abcdef" + condition = "abcdef" + + escrow_create = EscrowCreate( + account=account, + amount=amount, + destination=destination, + destination_tag=destination_tag, + fee=fee, + cancel_after=cancel_after, + finish_after=finish_after, + finish_function=finish_function, + condition=condition, + ) + self.assertTrue(escrow_create.is_valid()) + def test_final_after_less_than_cancel_after(self): account = "r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ" amount = "amount" @@ -12,7 +36,6 @@ def test_final_after_less_than_cancel_after(self): finish_after = 2 destination = "destination" fee = "0.00001" - sequence = 19048 with self.assertRaises(XRPLModelException): EscrowCreate( @@ -22,5 +45,24 @@ def test_final_after_less_than_cancel_after(self): destination=destination, fee=fee, finish_after=finish_after, + ) + + def test_no_finish(self): + account = "r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ" + amount = "amount" + cancel_after = 1 + destination = "destination" + destination_tag = 1 + fee = "0.00001" + sequence = 19048 + + with self.assertRaises(XRPLModelException): + EscrowCreate( + account=account, + amount=amount, + destination=destination, + destination_tag=destination_tag, + fee=fee, + cancel_after=cancel_after, sequence=sequence, ) From c7b3b48544563e0b8d552c8802a536ca856a248d Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Tue, 11 Mar 2025 14:15:25 -0400 Subject: [PATCH 04/18] cleanup --- tests/unit/models/transactions/test_escrow_create.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/unit/models/transactions/test_escrow_create.py b/tests/unit/models/transactions/test_escrow_create.py index b94c7878a..5dd21eccd 100644 --- a/tests/unit/models/transactions/test_escrow_create.py +++ b/tests/unit/models/transactions/test_escrow_create.py @@ -11,7 +11,6 @@ def test_all_fields_valid(self): cancel_after = 3 destination = "destination" destination_tag = 1 - fee = "0.00001" finish_after = 2 finish_function = "abcdef" condition = "abcdef" @@ -21,7 +20,6 @@ def test_all_fields_valid(self): amount=amount, destination=destination, destination_tag=destination_tag, - fee=fee, cancel_after=cancel_after, finish_after=finish_after, finish_function=finish_function, @@ -35,7 +33,6 @@ def test_final_after_less_than_cancel_after(self): cancel_after = 1 finish_after = 2 destination = "destination" - fee = "0.00001" with self.assertRaises(XRPLModelException): EscrowCreate( @@ -43,7 +40,6 @@ def test_final_after_less_than_cancel_after(self): amount=amount, cancel_after=cancel_after, destination=destination, - fee=fee, finish_after=finish_after, ) @@ -53,8 +49,6 @@ def test_no_finish(self): cancel_after = 1 destination = "destination" destination_tag = 1 - fee = "0.00001" - sequence = 19048 with self.assertRaises(XRPLModelException): EscrowCreate( @@ -62,7 +56,5 @@ def test_no_finish(self): amount=amount, destination=destination, destination_tag=destination_tag, - fee=fee, cancel_after=cancel_after, - sequence=sequence, ) From d6614382f2fa8a91526d0162926cd4fc35436327 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Tue, 11 Mar 2025 17:42:16 -0400 Subject: [PATCH 05/18] remove repeat test --- tests/integration/sugar/test_wallet.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tests/integration/sugar/test_wallet.py b/tests/integration/sugar/test_wallet.py index 997f92072..87f5b1bc4 100644 --- a/tests/integration/sugar/test_wallet.py +++ b/tests/integration/sugar/test_wallet.py @@ -6,11 +6,9 @@ from xrpl.asyncio.clients import AsyncJsonRpcClient, AsyncWebsocketClient from xrpl.asyncio.wallet import generate_faucet_wallet from xrpl.clients import JsonRpcClient, WebsocketClient -from xrpl.core.addresscodec.main import classic_address_to_xaddress from xrpl.models.requests import AccountInfo from xrpl.models.transactions import Payment from xrpl.wallet import generate_faucet_wallet as sync_generate_faucet_wallet -from xrpl.wallet.main import Wallet def sync_generate_faucet_wallet_and_fund_again( @@ -156,8 +154,3 @@ async def _parallel_test_generate_faucet_wallet_devnet_async_websockets(self): "wss://s.devnet.rippletest.net:51233" ) as client: await generate_faucet_wallet_and_fund_again(self, client) - - def test_wallet_get_xaddress(self): - wallet = Wallet.create() - expected = classic_address_to_xaddress(wallet.address, None, False) - self.assertEqual(wallet.get_xaddress(), expected) From 139992960796b79567ae5d7691b256a163e579a1 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Wed, 26 Mar 2025 15:22:03 -0400 Subject: [PATCH 06/18] bump version for beta --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5c6e0806e..a99a73f20 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [tool.poetry] name = "xrpl-py" -version = "4.1.0" -description = "A complete Python library for interacting with the XRP ledger" +version = "4.2.0b0" +description = "A complete Python library for interacting with the XRP ledger (Smart Escrow beta)" readme = "README.md" repository = "https://github.com/XRPLF/xrpl-py" authors = [ From dc465b47171fd4704496d816da7b03fe2060372c Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Fri, 28 Mar 2025 12:53:44 -0400 Subject: [PATCH 07/18] add faucet info --- tests/integration/sugar/test_wallet.py | 6 ++++++ tests/unit/asyn/wallet/test_wallet.py | 10 ++++++---- xrpl/asyncio/wallet/wallet_generation.py | 16 ++++++++++++---- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/tests/integration/sugar/test_wallet.py b/tests/integration/sugar/test_wallet.py index 87f5b1bc4..b0036891d 100644 --- a/tests/integration/sugar/test_wallet.py +++ b/tests/integration/sugar/test_wallet.py @@ -154,3 +154,9 @@ async def _parallel_test_generate_faucet_wallet_devnet_async_websockets(self): "wss://s.devnet.rippletest.net:51233" ) as client: await generate_faucet_wallet_and_fund_again(self, client) + + async def _parallel_test_generate_faucet_wallet_wasm_devnet_async_websockets(self): + async with AsyncWebsocketClient( + "wss://wasm.devnet.rippletest.net:51233" + ) as client: + await generate_faucet_wallet_and_fund_again(self, client) diff --git a/tests/unit/asyn/wallet/test_wallet.py b/tests/unit/asyn/wallet/test_wallet.py index 19114aacd..c2501417e 100644 --- a/tests/unit/asyn/wallet/test_wallet.py +++ b/tests/unit/asyn/wallet/test_wallet.py @@ -1,8 +1,9 @@ from unittest import TestCase from xrpl.asyncio.wallet.wallet_generation import ( - _DEV_FAUCET_URL, - _TEST_FAUCET_URL, + _DEVNET_FAUCET_URL, + _TESTNET_FAUCET_URL, + _WASM_DEVNET_FAUCET_URL, XRPLFaucetException, get_faucet_url, process_faucet_host_url, @@ -18,8 +19,9 @@ def test_wallet_get_xaddress(self): self.assertEqual(wallet.get_xaddress(), expected) def test_get_faucet_wallet_valid(self): - self.assertEqual(get_faucet_url(1), _TEST_FAUCET_URL) - self.assertEqual(get_faucet_url(2), _DEV_FAUCET_URL) + self.assertEqual(get_faucet_url(1), _TESTNET_FAUCET_URL) + self.assertEqual(get_faucet_url(2), _DEVNET_FAUCET_URL) + self.assertEqual(get_faucet_url(2002), _WASM_DEVNET_FAUCET_URL) def test_get_faucet_wallet_invalid(self): with self.assertRaises(XRPLFaucetException): diff --git a/xrpl/asyncio/wallet/wallet_generation.py b/xrpl/asyncio/wallet/wallet_generation.py index c5bf7418c..d71afea12 100644 --- a/xrpl/asyncio/wallet/wallet_generation.py +++ b/xrpl/asyncio/wallet/wallet_generation.py @@ -13,11 +13,19 @@ from xrpl.constants import XRPLException from xrpl.wallet.main import Wallet -_TEST_FAUCET_URL: Final[str] = "https://faucet.altnet.rippletest.net/accounts" -_DEV_FAUCET_URL: Final[str] = "https://faucet.devnet.rippletest.net/accounts" +_TESTNET_FAUCET_URL: Final[str] = "https://faucet.altnet.rippletest.net/accounts" +_DEVNET_FAUCET_URL: Final[str] = "https://faucet.devnet.rippletest.net/accounts" +_WASM_DEVNET_FAUCET_URL: Final[str] = ( + "https://wasmfaucet.devnet.rippletest.net/accounts" +) + _TIMEOUT_SECONDS: Final[int] = 40 -_NETWORK_ID_URL_MAP: Dict[int, str] = {1: _TEST_FAUCET_URL, 2: _DEV_FAUCET_URL} +_NETWORK_ID_URL_MAP: Dict[int, str] = { + 1: _TESTNET_FAUCET_URL, + 2: _DEVNET_FAUCET_URL, + 2002: _WASM_DEVNET_FAUCET_URL, +} class XRPLFaucetException(XRPLException): @@ -35,7 +43,7 @@ async def generate_faucet_wallet( user_agent: Optional[str] = "xrpl-py", ) -> Wallet: """ - Generates a random wallet and funds it using the XRPL Testnet Faucet. + Generates a random wallet and funds it using an XRPL Testnet Faucet. Args: client: the network client used to make network calls. From 3a009c230b248f1db19bfcaecb321523f7b354d2 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Fri, 28 Mar 2025 15:38:15 -0400 Subject: [PATCH 08/18] easier running wallet tests --- tests/integration/sugar/test_wallet.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/integration/sugar/test_wallet.py b/tests/integration/sugar/test_wallet.py index b0036891d..6d1297cf2 100644 --- a/tests/integration/sugar/test_wallet.py +++ b/tests/integration/sugar/test_wallet.py @@ -1,7 +1,11 @@ import asyncio from threading import Thread -from tests.integration.integration_test_case import IntegrationTestCase +try: + from unittest import IsolatedAsyncioTestCase +except ImportError: + from aiounittest import AsyncTestCase as IsolatedAsyncioTestCase # type: ignore + from tests.integration.it_utils import submit_transaction_async from xrpl.asyncio.clients import AsyncJsonRpcClient, AsyncWebsocketClient from xrpl.asyncio.wallet import generate_faucet_wallet @@ -63,7 +67,7 @@ async def generate_faucet_wallet_and_fund_again( self.assertTrue(new_balance > balance) -class TestWallet(IntegrationTestCase): +class TestWallet(IsolatedAsyncioTestCase): async def test_run_faucet_tests(self): # run all the tests that start with `_test_` in parallel def run_test(test_name): From 468b29b7485b985c960dc06010d1c7beaa298bbe Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Fri, 28 Mar 2025 16:07:01 -0400 Subject: [PATCH 09/18] Update rippled.cfg --- .ci-config/rippled.cfg | 3 --- 1 file changed, 3 deletions(-) diff --git a/.ci-config/rippled.cfg b/.ci-config/rippled.cfg index c266a72fa..1561f9906 100644 --- a/.ci-config/rippled.cfg +++ b/.ci-config/rippled.cfg @@ -72,9 +72,6 @@ pool.ntp.org [ips] r.ripple.com 51235 -[validators_file] -validators.txt - [rpc_startup] { "command": "log_level", "severity": "info" } From cbeee6bffaa9ce13803dde039b0adf3989eb97d8 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Fri, 28 Mar 2025 16:13:44 -0400 Subject: [PATCH 10/18] if it ain't broke don't fix it --- tests/integration/it_utils.py | 43 +++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/tests/integration/it_utils.py b/tests/integration/it_utils.py index 79856ffdc..ebd2e2861 100644 --- a/tests/integration/it_utils.py +++ b/tests/integration/it_utils.py @@ -3,6 +3,7 @@ import asyncio import importlib import inspect +from threading import Timer as ThreadingTimer from time import sleep from typing import Any, Dict, cast @@ -13,7 +14,7 @@ from xrpl.clients import Client, JsonRpcClient, WebsocketClient from xrpl.clients.sync_client import SyncClient from xrpl.constants import CryptoAlgorithm -from xrpl.models import GenericRequest, Payment, Response, Transaction +from xrpl.models import GenericRequest, Payment, Request, Response, Transaction from xrpl.models.amounts.issued_currency_amount import IssuedCurrencyAmount from xrpl.models.currencies.issued_currency import IssuedCurrency from xrpl.models.currencies.xrp import XRP @@ -68,6 +69,40 @@ LEDGER_ACCEPT_TIME = 0.1 +class AsyncTestTimer: + def __init__( + self, + client: AsyncClient, + delay: float = LEDGER_ACCEPT_TIME, + request: Request = LEDGER_ACCEPT_REQUEST, + ): + self._client = client + self._delay = delay + self._request = request + self._task = asyncio.ensure_future(self._job()) + + async def _job(self): + await asyncio.sleep(self._delay) + await self._client.request(self._request) + + def cancel(self): + self._task.cancel() + + +class SyncTestTimer: + def __init__( + self, + client: SyncClient, + delay: float = LEDGER_ACCEPT_TIME, + request: Request = LEDGER_ACCEPT_REQUEST, + ): + self._timer = ThreadingTimer(delay, client.request, (request,)) + self._timer.start() + + def cancel(self): + self._timer.cancel() + + def fund_wallet(wallet: Wallet) -> None: client = JSON_RPC_CLIENT payment = Payment( @@ -197,8 +232,7 @@ def accept_ledger( delay: float for how many seconds to wait before accepting ledger. """ client = _choose_client(use_json_client) - sleep(delay) - client.request(LEDGER_ACCEPT_REQUEST) + SyncTestTimer(client, delay) async def accept_ledger_async( @@ -213,8 +247,7 @@ async def accept_ledger_async( delay: float for how many seconds to wait before accepting ledger. """ client = _choose_client_async(use_json_client) - await asyncio.sleep(delay) - await client.request(LEDGER_ACCEPT_REQUEST) + AsyncTestTimer(client, delay) def _choose_client(use_json_client: bool) -> SyncClient: From 445b233ade905ba068993923a1ce88e4b96e2740 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Fri, 28 Mar 2025 16:19:30 -0400 Subject: [PATCH 11/18] add more buffer --- tests/integration/transactions/test_escrow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/transactions/test_escrow.py b/tests/integration/transactions/test_escrow.py index d99cfe302..65ff38493 100644 --- a/tests/integration/transactions/test_escrow.py +++ b/tests/integration/transactions/test_escrow.py @@ -90,7 +90,7 @@ async def test_all_fields_finish(self, client): sequence = response.result["tx_json"]["Sequence"] # TODO: check account_objects - for _ in range(4): + for _ in range(6): await accept_ledger_async() escrow_finish = EscrowFinish( From 67a7775eecd1d32a00c554117eecbfb83925321f Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Fri, 28 Mar 2025 16:23:03 -0400 Subject: [PATCH 12/18] add 1s wait between snippet runs --- .github/workflows/snippet_test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/snippet_test.yml b/.github/workflows/snippet_test.yml index 619c29663..f7720b201 100644 --- a/.github/workflows/snippet_test.yml +++ b/.github/workflows/snippet_test.yml @@ -48,4 +48,4 @@ jobs: run: poetry install - name: Run Snippets - run: (for i in snippets/*.py; do echo "Running $i" && poetry run python $i || exit 1; done) + run: (for i in snippets/*.py; do echo "Running $i" && poetry run python $i && sleep 1 || exit 1; done) From 4443d6956f07297bcb516bb8e5ad799cd3336708 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Fri, 28 Mar 2025 16:30:18 -0400 Subject: [PATCH 13/18] more fixes --- tests/integration/transactions/test_escrow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/transactions/test_escrow.py b/tests/integration/transactions/test_escrow.py index 65ff38493..f8a214fc9 100644 --- a/tests/integration/transactions/test_escrow.py +++ b/tests/integration/transactions/test_escrow.py @@ -57,7 +57,7 @@ async def test_all_fields_cancel(self, client): # TODO: check account_objects for _ in range(4): - await accept_ledger_async() + await accept_ledger_async(delay=0.5) escrow_cancel = EscrowCancel( account=ACCOUNT, @@ -91,7 +91,7 @@ async def test_all_fields_finish(self, client): # TODO: check account_objects for _ in range(6): - await accept_ledger_async() + await accept_ledger_async(delay=0.5) escrow_finish = EscrowFinish( account=ACCOUNT, From 2a68fde5cc20c30e2a9272f8e16d566e9e7c9c46 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Fri, 28 Mar 2025 16:40:19 -0400 Subject: [PATCH 14/18] bump version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f5411bbe9..2073496f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "xrpl-py" -version = "4.2.0b0" +version = "4.2.0b1" description = "A complete Python library for interacting with the XRP ledger (Smart Escrow beta)" license = "ISC" readme = "README.md" From 34e6d8009c1e65f6dc37b8acf17790c518e1cc48 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Fri, 28 Mar 2025 16:51:21 -0400 Subject: [PATCH 15/18] increase faucet timeout --- xrpl/asyncio/wallet/wallet_generation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xrpl/asyncio/wallet/wallet_generation.py b/xrpl/asyncio/wallet/wallet_generation.py index d71afea12..eb6f10a29 100644 --- a/xrpl/asyncio/wallet/wallet_generation.py +++ b/xrpl/asyncio/wallet/wallet_generation.py @@ -228,7 +228,7 @@ async def _request_funding( json_body = {"destination": address, "userAgent": user_agent} if usage_context is not None: json_body["usageContext"] = usage_context - response = await http_client.post(url=url, json=json_body) + response = await http_client.post(url=url, json=json_body, timeout=10) if not response.status_code == httpx.codes.OK: response.raise_for_status() From cfa95f5db852edf1532367f15d0538701b48b476 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Tue, 29 Apr 2025 16:39:15 -0400 Subject: [PATCH 16/18] update to Devnet 2 --- xrpl/asyncio/transaction/main.py | 12 ++++++++++++ xrpl/models/transactions/escrow_finish.py | 2 ++ 2 files changed, 14 insertions(+) diff --git a/xrpl/asyncio/transaction/main.py b/xrpl/asyncio/transaction/main.py index 2ea9e0f3e..79658f9e1 100644 --- a/xrpl/asyncio/transaction/main.py +++ b/xrpl/asyncio/transaction/main.py @@ -38,6 +38,7 @@ # More context: https://github.com/XRPLF/rippled/pull/4370 _RESTRICTED_NETWORKS = 1024 _REQUIRED_NETWORKID_VERSION = "1.11.0" +_MICRO_DROPS_PER_DROP = 1_000_000 async def sign_and_submit( @@ -479,6 +480,11 @@ async def _calculate_fee_per_transaction_type( fulfillment_bytes = escrow_finish.fulfillment.encode("ascii") # BaseFee × (33 + (Fulfillment size in bytes / 16)) base_fee = math.ceil(net_fee * (33 + (len(fulfillment_bytes) / 16))) + if escrow_finish.computation_allowance is not None: + gas_price = await _fetch_gas_price(client) + base_fee += math.ceil( + gas_price * escrow_finish.computation_allowance / _MICRO_DROPS_PER_DROP + ) if transaction.transaction_type == TransactionType.ESCROW_CREATE: escrow_create = cast(EscrowCreate, transaction) @@ -504,3 +510,9 @@ async def _fetch_owner_reserve_fee(client: Client) -> int: server_state = await client._request_impl(ServerState()) fee = server_state.result["state"]["validated_ledger"]["reserve_inc"] return int(fee) + + +async def _fetch_gas_price(client: Client) -> int: + server_state = await client._request_impl(ServerState()) + fee = server_state.result["state"]["validated_ledger"]["gas_price"] + return int(fee) diff --git a/xrpl/models/transactions/escrow_finish.py b/xrpl/models/transactions/escrow_finish.py index 07bb87fa4..93176700f 100644 --- a/xrpl/models/transactions/escrow_finish.py +++ b/xrpl/models/transactions/escrow_finish.py @@ -63,6 +63,8 @@ class EscrowFinish(Transaction): """Credentials associated with sender of this transaction. The credentials included must not be expired.""" + computation_allowance: Optional[int] = None + def _get_errors(self: Self) -> Dict[str, str]: errors = super()._get_errors() if self.condition and not self.fulfillment: From fc3f5f6b012320333ff83708615f175b8cc3b42b Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Fri, 2 May 2025 11:42:40 -0400 Subject: [PATCH 17/18] update beta version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2073496f5..787ae4ae1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "xrpl-py" -version = "4.2.0b1" +version = "4.2.0b2" description = "A complete Python library for interacting with the XRP ledger (Smart Escrow beta)" license = "ISC" readme = "README.md" From 1f25b908ecafa8868c309fbf34a4db7c1b8ef2b1 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Sat, 3 May 2025 21:47:02 -0400 Subject: [PATCH 18/18] update definitions --- .../binarycodec/definitions/definitions.json | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/xrpl/core/binarycodec/definitions/definitions.json b/xrpl/core/binarycodec/definitions/definitions.json index f6c4984cd..d0f3376c0 100644 --- a/xrpl/core/binarycodec/definitions/definitions.json +++ b/xrpl/core/binarycodec/definitions/definitions.json @@ -690,6 +690,26 @@ "type": "UInt32" } ], + [ + "GasPrice", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 54, + "type": "UInt32" + } + ], + [ + "ComputationAllowance", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 55, + "type": "UInt32" + } + ], [ "IndexNext", { @@ -1880,6 +1900,16 @@ "type": "Blob" } ], + [ + "FinishFunction", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": true, + "nth": 32, + "type": "Blob" + } + ], [ "Account", { @@ -2571,7 +2601,8 @@ } ], [ - "AcceptedCredentials", { + "AcceptedCredentials", + { "nth": 28, "isVLEncoded": false, "isSerialized": true, @@ -3018,8 +3049,10 @@ "tefNOT_MULTI_SIGNING": -184, "tefNO_AUTH_REQUIRED": -191, "tefNO_TICKET": -180, + "tefNO_WASM": -177, "tefPAST_SEQ": -190, "tefTOO_BIG": -181, + "tefWASM_FIELD_NOT_INCLUDED": -176, "tefWRONG_PRIOR": -189, "telBAD_DOMAIN": -398, "telBAD_PATH_COUNT": -397,