diff --git a/.ci-config/rippled.cfg b/.ci-config/rippled.cfg index b6b907c8d..5700bbe9a 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" } diff --git a/pyproject.toml b/pyproject.toml index 13946abca..2310b2240 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "xrpl-py" -version = "4.3.0" -description = "A complete Python library for interacting with the XRP ledger" +version = "4.4.0b2" +description = "A complete Python library for interacting with the XRP ledger (Smart Escrow beta)" license = "ISC" readme = "README.md" authors = [ diff --git a/tests/faucet/test_faucet_wallet.py b/tests/faucet/test_faucet_wallet.py index c9c42f4c5..c3b555c79 100644 --- a/tests/faucet/test_faucet_wallet.py +++ b/tests/faucet/test_faucet_wallet.py @@ -1,7 +1,11 @@ import asyncio import time from threading import Thread -from unittest import TestCase + +try: + from unittest import IsolatedAsyncioTestCase +except ImportError: + from aiounittest import AsyncTestCase as IsolatedAsyncioTestCase # type: ignore import httpx @@ -9,11 +13,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 # Add retry logic for wallet funding to handle newly introduced faucet rate limiting. MAX_RETRY_DURATION = 600 # 10 minutes @@ -107,8 +109,8 @@ async def generate_faucet_wallet_and_fund_again( self.assertTrue(new_balance > balance) -class TestWallet(TestCase): - def test_run_faucet_tests(self): +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): with self.subTest(method=test_name): @@ -199,7 +201,8 @@ async def _parallel_test_generate_faucet_wallet_devnet_async_websockets(self): ) 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) + 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/integration/it_utils.py b/tests/integration/it_utils.py index 8652f19f5..488e12d2b 100644 --- a/tests/integration/it_utils.py +++ b/tests/integration/it_utils.py @@ -246,7 +246,7 @@ async def sign_and_reliable_submission_async( def accept_ledger( - use_json_client: bool = True, delay: float = LEDGER_ACCEPT_TIME + use_json_client: bool = True, delay: float = LEDGER_ACCEPT_TIME, wait: bool = False ) -> None: """ Allows integration tests for sync clients to send a `ledger_accept` request @@ -257,11 +257,13 @@ def accept_ledger( delay: float for how many seconds to wait before accepting ledger. """ client = _choose_client(use_json_client) - SyncTestTimer(client, delay) + timer = SyncTestTimer(client, delay) + if wait: + timer._timer.join() # Wait for the timer to finish async def accept_ledger_async( - use_json_client: bool = True, delay: float = LEDGER_ACCEPT_TIME + use_json_client: bool = True, delay: float = LEDGER_ACCEPT_TIME, wait: bool = False ) -> None: """ Allows integration tests for async clients to send a `ledger_accept` request @@ -272,7 +274,9 @@ 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) + timer = AsyncTestTimer(client, delay) + if wait: + await timer._job() # The _choose_client(_async)? methods are only used to send LEDGER_ACCEPT_REQUEST. diff --git a/tests/integration/transactions/test_escrow_create.py b/tests/integration/transactions/test_escrow.py similarity index 60% rename from tests/integration/transactions/test_escrow_create.py rename to tests/integration/transactions/test_escrow.py index 04ea95a59..dcda2d6d9 100644 --- a/tests/integration/transactions/test_escrow_create.py +++ b/tests/integration/transactions/test_escrow.py @@ -1,18 +1,24 @@ from tests.integration.integration_test_case import IntegrationTestCase from tests.integration.it_utils import ( JSON_RPC_CLIENT, - LEDGER_ACCEPT_REQUEST, + accept_ledger_async, create_mpt_token_and_authorize_source, fund_wallet, sign_and_reliable_submission_async, test_async_and_sync, ) from tests.integration.reusable_values import DESTINATION, WALLET -from xrpl.models import EscrowCreate, EscrowFinish, Ledger, MPTokenIssuanceCreateFlag -from xrpl.models.amounts import MPTAmount +from xrpl.models import ( + EscrowCancel, + EscrowCreate, + EscrowFinish, + Ledger, + MPTokenIssuanceCreateFlag, +) +from xrpl.models.amounts.mpt_amount import MPTAmount from xrpl.models.requests.account_objects import AccountObjects, AccountObjectType from xrpl.models.response import ResponseStatus -from xrpl.wallet.main import Wallet +from xrpl.wallet import Wallet ACCOUNT = WALLET.address @@ -23,9 +29,32 @@ DESTINATION_TAG = 23480 SOURCE_TAG = 11747 +FINISH_FUNCTION = ( + "0061736d010000000108026000017f60000002160103656e760e6765745f6c65646765725f" + "73716e000003030201000503010002063e0a7f004180080b7f004180080b7f004180100b7f" + "004180100b7f00418090040b7f004180080b7f00418090040b7f00418080080b7f0041000b" + "7f0041010b07b0010d066d656d6f72790200115f5f7761736d5f63616c6c5f63746f727300" + "010666696e69736800020362756603000c5f5f64736f5f68616e646c6503010a5f5f646174" + "615f656e6403020b5f5f737461636b5f6c6f7703030c5f5f737461636b5f6869676803040d" + "5f5f676c6f62616c5f6261736503050b5f5f686561705f6261736503060a5f5f686561705f" + "656e6403070d5f5f6d656d6f72795f6261736503080c5f5f7461626c655f6261736503090a" + "150202000b1001017f100022004100200041044b1b0b007f0970726f647563657273010c70" + "726f6365737365642d62790105636c616e675f31392e312e352d776173692d73646b202868" + "747470733a2f2f6769746875622e636f6d2f6c6c766d2f6c6c766d2d70726f6a6563742061" + "62346235613264623538323935386166316565333038613739306366646234326264323437" + "32302900490f7461726765745f6665617475726573042b0f6d757461626c652d676c6f6261" + "6c732b087369676e2d6578742b0f7265666572656e63652d74797065732b0a6d756c746976" + "616c7565" +) + +issuer = Wallet.create() +source = Wallet.create() +destination = Wallet.create() +good_mpt_issuance_id = "" +bad_mpt_issuance_id = "" -class TestEscrowCreate(IntegrationTestCase): +class TestEscrow(IntegrationTestCase): @classmethod def setUpClass(cls): super().setUpClass() @@ -58,11 +87,11 @@ def setUpClass(cls): ) @test_async_and_sync(globals()) - async def test_all_fields(self, client): + 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=WALLET.classic_address, + account=ACCOUNT, amount=AMOUNT, destination=DESTINATION.classic_address, destination_tag=DESTINATION_TAG, @@ -75,6 +104,56 @@ async def test_all_fields(self, 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(3): + await accept_ledger_async(wait=True) + + 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(2): + await accept_ledger_async(wait=True) + + 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_mpt_based_escrow(self, client): @@ -151,7 +230,7 @@ async def test_mpt_based_escrow(self, client): # Wait for the finish_after time to pass before finishing the escrow. close_time = 0 while close_time <= finish_after: - await client.request(LEDGER_ACCEPT_REQUEST) + await accept_ledger_async(wait=True) ledger = await client.request(Ledger(ledger_index="validated")) close_time = ledger.result["ledger"]["close_time"] @@ -212,3 +291,34 @@ async def test_mpt_based_escrow_failure(self, client): self.assertEqual(response.status, ResponseStatus.SUCCESS) self.assertEqual(response.result["engine_result"], "tecNO_PERMISSION") + + @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, + computation_allowance=20000, + ) + 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_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/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/tests/unit/core/binarycodec/fixtures/data/codec-fixtures.json b/tests/unit/core/binarycodec/fixtures/data/codec-fixtures.json index 18ca9dcaa..510477f3b 100644 --- a/tests/unit/core/binarycodec/fixtures/data/codec-fixtures.json +++ b/tests/unit/core/binarycodec/fixtures/data/codec-fixtures.json @@ -5039,6 +5039,33 @@ "TransactionType":"VaultCreate", "TxnSignature":"14B5CAC538F7094C2CCF1FB8A1E05068727F7A10441AA68C23CC50843EB558DA61E191430925890420EAE0F97E345F37F10D6FB89AA1E173EBF3C50B1095570D" } + }, + { + "binary": "201C0000000020390000197CA1FFFFFF00F8E511006125002C6B905563F4B97FFBA6D65B7C507DD24E157ADF1BED3AFF2B8EC73741B8E1BEE9F1A0A55698DC3588FBDB6A91CCF5DB14E8523E8DB7E4574C92818E87A2236A5906C71021E624002C6B8F624000000005F5E100E1E7220000000024002C6B902D00000000624000000005E69EB48114F69B176FEEA1193FE503C3D229BCBA1126916B02E1E1F10310C7", + "json": { + "AffectedNodes": [ + { + "ModifiedNode": { + "FinalFields": { + "Account": "rP7Ap93CjnFxzcGXPszoD9CQHniSJYCboN", + "Balance": "98999988", + "Flags": 0, + "OwnerCount": 0, + "Sequence": 2911120 + }, + "LedgerEntryType": "AccountRoot", + "LedgerIndex": "98DC3588FBDB6A91CCF5DB14E8523E8DB7E4574C92818E87A2236A5906C71021", + "PreviousFields": { "Balance": "100000000", "Sequence": 2911119 }, + "PreviousTxnID": "63F4B97FFBA6D65B7C507DD24E157ADF1BED3AFF2B8EC73741B8E1BEE9F1A0A5", + "PreviousTxnLgrSeq": 2911120 + } + } + ], + "GasUsed": 6524, + "TransactionIndex": 0, + "TransactionResult": "tecWASM_REJECTED", + "WasmReturnCode": -256 + } } ], "ledgerData": [ diff --git a/tests/unit/models/transactions/test_escrow_create.py b/tests/unit/models/transactions/test_escrow_create.py index 45fee8819..16bf4e25c 100644 --- a/tests/unit/models/transactions/test_escrow_create.py +++ b/tests/unit/models/transactions/test_escrow_create.py @@ -9,14 +9,34 @@ class TestEscrowCreate(TestCase): + def test_all_fields_valid(self): + account = _SOURCE + amount = "1000" + cancel_after = 3 + destination = _DESTINATION + destination_tag = 1 + finish_after = 2 + finish_function = "abcdef" + condition = "abcdef" + + escrow_create = EscrowCreate( + account=account, + amount=amount, + destination=destination, + destination_tag=destination_tag, + 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 = _SOURCE amount = "10.890" cancel_after = 1 finish_after = 2 destination = _DESTINATION - fee = "0.00001" - sequence = 19048 with self.assertRaises(XRPLModelException) as error: EscrowCreate( @@ -24,9 +44,7 @@ def test_final_after_less_than_cancel_after(self): amount=amount, cancel_after=cancel_after, destination=destination, - fee=fee, finish_after=finish_after, - sequence=sequence, ) self.assertEqual( error.exception.args[0], @@ -34,6 +52,28 @@ def test_final_after_less_than_cancel_after(self): "'The finish_after time must be before the cancel_after time.'}", ) + def test_no_finish(self): + account = _SOURCE + amount = "1000" + cancel_after = 1 + destination = _DESTINATION + destination_tag = 1 + + with self.assertRaises(XRPLModelException) as error: + EscrowCreate( + account=account, + amount=amount, + destination=destination, + destination_tag=destination_tag, + cancel_after=cancel_after, + ) + self.assertEqual( + error.exception.args[0], + "{'EscrowCreate': " + "'At least one of finish_after, condition, or finish_function must be set.'" + "}", + ) + def test_amount_not_positive(self): with self.assertRaises(XRPLModelException) as error: EscrowCreate( @@ -44,7 +84,7 @@ def test_amount_not_positive(self): currency="USD", value="0.00", ), - cancel_after=10, + finish_after=10, ) self.assertEqual( error.exception.args[0], @@ -59,6 +99,6 @@ def test_valid_escrow_create(self): mpt_issuance_id="rHxTJLqdVUxjJuZEZvajXYYQJ7q8p4DhHy", value="10.20", ), - cancel_after=10, + finish_after=10, ) self.assertTrue(tx.is_valid()) diff --git a/tools/generate_definitions.py b/tools/generate_definitions.py index 8355559cd..a5aa581f6 100644 --- a/tools/generate_definitions.py +++ b/tools/generate_definitions.py @@ -321,7 +321,7 @@ def _unhex(x: str) -> str: # Example line: # TRANSACTION(ttCHECK_CREATE, 16, CheckCreate, ({ tx_hits = re.findall( - r"^ *TRANSACTION\(tt[A-Z_]+ *,* ([0-9]+) *, *([A-Za-z]+).*$", + r"^ *TRANSACTION\(tt[A-Z_]+[ \n]*,* ([0-9]+)[ \n]*,[ \n]*([A-Za-z]+).*$", transactions_file, re.MULTILINE, ) diff --git a/xrpl/asyncio/transaction/main.py b/xrpl/asyncio/transaction/main.py index 4ec1dda28..08e6887e1 100644 --- a/xrpl/asyncio/transaction/main.py +++ b/xrpl/asyncio/transaction/main.py @@ -23,6 +23,7 @@ Transaction, TransactionFlag, ) +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, ) @@ -38,6 +39,11 @@ # 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 + + +_WASM_FIXED_UPLOAD_COST = 100 +_WASM_DROPS_PER_BYTE = 5 T = TypeVar("T", bound=Transaction, default=Transaction) @@ -491,6 +497,15 @@ async def _calculate_fee_per_transaction_type( base_fee = net_fee + if transaction.transaction_type == TransactionType.ESCROW_CREATE: + escrow_create = cast(EscrowCreate, transaction) + if escrow_create.finish_function is not None: + base_fee += _WASM_FIXED_UPLOAD_COST + base_fee += ( + _WASM_DROPS_PER_BYTE + * len(escrow_create.finish_function.encode("utf-8")) + ) // 2 + # EscrowFinish Transaction with Fulfillment # https://xrpl.org/escrowfinish.html#escrowfinish-fields if transaction.transaction_type == TransactionType.ESCROW_FINISH: @@ -499,6 +514,16 @@ 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) + if escrow_create.finish_function is not None: + base_fee += 1000 # AccountDelete Transaction elif transaction.transaction_type in ( @@ -592,3 +617,9 @@ def _validate_field(field_name: str, expected_value: Union[str, None]) -> None: inner_txs.append(raw_txn_dict) return inner_txs + + +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/asyncio/wallet/wallet_generation.py b/xrpl/asyncio/wallet/wallet_generation.py index c5bf7418c..eb6f10a29 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. @@ -220,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() diff --git a/xrpl/core/binarycodec/definitions/definitions.json b/xrpl/core/binarycodec/definitions/definitions.json index 9fdd5ff6a..f63c27a4b 100644 --- a/xrpl/core/binarycodec/definitions/definitions.json +++ b/xrpl/core/binarycodec/definitions/definitions.json @@ -680,6 +680,66 @@ "type": "UInt32" } ], + [ + "MutableFlags", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 53, + "type": "UInt32" + } + ], + [ + "ExtensionComputeLimit", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 54, + "type": "UInt32" + } + ], + [ + "ExtensionSizeLimit", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 55, + "type": "UInt32" + } + ], + [ + "GasPrice", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 56, + "type": "UInt32" + } + ], + [ + "ComputationAllowance", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 57, + "type": "UInt32" + } + ], + [ + "GasUsed", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 58, + "type": "UInt32" + } + ], [ "IndexNext", { @@ -1890,6 +1950,16 @@ "type": "Blob" } ], + [ + "FinishFunction", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": true, + "nth": 32, + "type": "Blob" + } + ], [ "Account", { @@ -2130,6 +2200,16 @@ "type": "Number" } ], + [ + "WasmReturnCode", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 1, + "type": "Int32" + } + ], [ "TransactionMetaData", { @@ -3159,6 +3239,7 @@ "tecUNFUNDED_AMM": 162, "tecUNFUNDED_OFFER": 103, "tecUNFUNDED_PAYMENT": 104, + "tecWASM_REJECTED": 199, "tecWRONG_ASSET": 194, "tecXCHAIN_ACCOUNT_CREATE_PAST": 181, "tecXCHAIN_ACCOUNT_CREATE_TOO_MANY": 182, @@ -3197,8 +3278,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, @@ -3246,6 +3329,7 @@ "temBAD_TICK_SIZE": -269, "temBAD_TRANSFER_FEE": -251, "temBAD_TRANSFER_RATE": -280, + "temBAD_WASM": -249, "temBAD_WEIGHT": -270, "temCANNOT_PREAUTH_SELF": -267, "temDISABLED": -273, @@ -3276,7 +3360,6 @@ "terLAST": -91, "terNO_ACCOUNT": -96, "terNO_AMM": -87, - "tedADDRESS_COLLISION": -86, "terNO_AUTH": -95, "terNO_LINE": -94, "terNO_RIPPLE": -90, @@ -3342,10 +3425,10 @@ "TicketCreate": 10, "TrustSet": 20, "UNLModify": 102, - "VaultCreate": 65, "VaultClawback": 70, - "VaultDeposit": 68, + "VaultCreate": 65, "VaultDelete": 67, + "VaultDeposit": 68, "VaultSet": 66, "VaultWithdraw": 69, "XChainAccountCreateCommit": 44, @@ -3367,6 +3450,8 @@ "Hash160": 17, "Hash192": 21, "Hash256": 5, + "Int32": 10, + "Int64": 11, "Issue": 24, "LedgerEntry": 10002, "Metadata": 10004, diff --git a/xrpl/core/binarycodec/types/__init__.py b/xrpl/core/binarycodec/types/__init__.py index 66e14cc0d..958d6e6fa 100644 --- a/xrpl/core/binarycodec/types/__init__.py +++ b/xrpl/core/binarycodec/types/__init__.py @@ -9,6 +9,7 @@ from xrpl.core.binarycodec.types.hash160 import Hash160 from xrpl.core.binarycodec.types.hash192 import Hash192 from xrpl.core.binarycodec.types.hash256 import Hash256 +from xrpl.core.binarycodec.types.int32 import Int32 from xrpl.core.binarycodec.types.issue import Issue from xrpl.core.binarycodec.types.number import Number from xrpl.core.binarycodec.types.path_set import PathSet @@ -32,6 +33,7 @@ "Hash160", "Hash192", "Hash256", + "Int32", "Issue", "Number", "PathSet", diff --git a/xrpl/core/binarycodec/types/int.py b/xrpl/core/binarycodec/types/int.py new file mode 100644 index 000000000..e9ae5fd65 --- /dev/null +++ b/xrpl/core/binarycodec/types/int.py @@ -0,0 +1,27 @@ +"""Base class for serializing and deserializing signed integers. +See `UInt Fields `_ +""" + +from __future__ import annotations + +from typing_extensions import Final, Self + +from xrpl.core.binarycodec.types.uint import UInt + +_WIDTH: Final[int] = 4 # 32 / 8 + + +class Int(UInt): + """Base class for serializing and deserializing unsigned integers. + See `UInt Fields `_ + """ + + @property + def value(self: Self) -> int: + """ + Get the value of the Int represented by `self.buffer`. + + Returns: + The int value of the Int. + """ + return int.from_bytes(self.buffer, byteorder="big", signed=True) diff --git a/xrpl/core/binarycodec/types/int32.py b/xrpl/core/binarycodec/types/int32.py new file mode 100644 index 000000000..3488c49cc --- /dev/null +++ b/xrpl/core/binarycodec/types/int32.py @@ -0,0 +1,71 @@ +"""Class for serializing and deserializing a 32-bit Int. +See `UInt Fields `_ +""" + +from __future__ import annotations + +from typing import Optional, Type, Union + +from typing_extensions import Final, Self + +from xrpl.core.binarycodec.binary_wrappers.binary_parser import BinaryParser +from xrpl.core.binarycodec.exceptions import XRPLBinaryCodecException +from xrpl.core.binarycodec.types.int import Int + +_WIDTH: Final[int] = 4 # 32 / 8 + + +class Int32(Int): + """ + Class for serializing and deserializing a 32-bit Int. + See `UInt Fields `_ + """ + + def __init__(self: Self, buffer: bytes = bytes(_WIDTH)) -> None: + """Construct a new Int32 type from a ``bytes`` value.""" + super().__init__(buffer) + + @classmethod + def from_parser( + cls: Type[Self], parser: BinaryParser, _length_hint: Optional[int] = None + ) -> Self: + """ + Construct a new Int32 type from a BinaryParser. + + Args: + parser: A BinaryParser to construct a Int32 from. + + Returns: + The Int32 constructed from parser. + """ + return cls(parser.read(_WIDTH)) + + @classmethod + def from_value(cls: Type[Self], value: Union[str, int]) -> Self: + """ + Construct a new Int32 type from a number. + + Args: + value: The number to construct a Int32 from. + + Returns: + The Int32 constructed from value. + + Raises: + XRPLBinaryCodecException: If a Int32 could not be constructed from value. + """ + if not isinstance(value, (str, int)): + raise XRPLBinaryCodecException( + "Invalid type to construct a Int32: expected str or int," + " received {value.__class__.__name__}." + ) + + if isinstance(value, int): + value_bytes = (value).to_bytes(_WIDTH, byteorder="big", signed=True) + return cls(value_bytes) + + if isinstance(value, str) and value.isdigit(): + value_bytes = (int(value)).to_bytes(_WIDTH, byteorder="big", signed=True) + return cls(value_bytes) + + raise XRPLBinaryCodecException("Cannot construct Int32 from given value") diff --git a/xrpl/models/transactions/escrow_create.py b/xrpl/models/transactions/escrow_create.py index 3bc23ebe0..48c47f7b5 100644 --- a/xrpl/models/transactions/escrow_create.py +++ b/xrpl/models/transactions/escrow_create.py @@ -68,6 +68,10 @@ class EscrowCreate(Transaction): fulfilled. """ + finish_function: Optional[str] = None + + data: Optional[str] = None + transaction_type: TransactionType = field( default=TransactionType.ESCROW_CREATE, init=False, @@ -83,6 +87,15 @@ def _get_errors(self: Self) -> Dict[str, str]: errors["EscrowCreate"] = ( "The finish_after time must be before the cancel_after time." ) + if ( + self.finish_after is None + and self.condition is None + and self.finish_function is None + ): + errors["EscrowCreate"] = ( + "At least one of finish_after, condition, or finish_function must be " + "set." + ) if get_amount_value(self.amount) <= 0: errors["amount"] = "amount must be positive." diff --git a/xrpl/models/transactions/escrow_finish.py b/xrpl/models/transactions/escrow_finish.py index 1339b4ee0..4556526d5 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: diff --git a/xrpl/models/transactions/pseudo_transactions/set_fee.py b/xrpl/models/transactions/pseudo_transactions/set_fee.py index 37b58c98e..c411bcdf7 100644 --- a/xrpl/models/transactions/pseudo_transactions/set_fee.py +++ b/xrpl/models/transactions/pseudo_transactions/set_fee.py @@ -90,6 +90,12 @@ class SetFee(PseudoTransaction): :meta hide-value: """ + extension_compute_limit: Optional[int] = None + + extension_size_limit: Optional[int] = None + + gas_price: Optional[int] = None + transaction_type: PseudoTransactionType = field( default=PseudoTransactionType.SET_FEE, init=False,