Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/integration_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Integration test

env:
POETRY_VERSION: 1.8.3
RIPPLED_DOCKER_IMAGE: rippleci/rippled:2.3.0
RIPPLED_DOCKER_IMAGE: rippleci/rippled:develop

on:
push:
Expand Down
7 changes: 6 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,10 @@
"source.organizeImports": "always"
}
},
"isort.args": ["--profile", "black"]
"isort.args": [
"--profile",
"black"
],
"python.testing.pytestEnabled": false,
"python.testing.unittestEnabled": true
}
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [[Unreleased]]

### Added
- Support for the `simulate` RPC ([XLS-69](https://github.com/XRPLF/XRPL-Standards/tree/master/XLS-0069d-simulate))

### Fixed
- `Sign`, `SignFor`, and `SignAndSubmit` methods now properly handle WebSocket clients

## [4.0.0] - 2024-12-23

### Added
Expand Down
32 changes: 19 additions & 13 deletions tests/integration/it_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ def test_async_and_sync(
websockets_only=False,
num_retries=1,
use_testnet=False,
async_only=False,
):
def decorator(test_function):
lines = _get_non_decorator_code(test_function)
Expand All @@ -235,15 +236,18 @@ def decorator(test_function):
first_line = lines[0]
sync_code += first_line.replace(" async def ", "").replace(":", "")

sync_modules_to_import = {}
if modules is not None:
for module_str in modules:
function = module_str.split(".")[-1]
location = module_str[: -1 * len(function) - 1]
module = getattr(importlib.import_module(location), function)
sync_modules_to_import[function] = module

all_modules = {**original_globals, **globals(), **sync_modules_to_import}
if not async_only:
sync_modules_to_import = {}
if modules is not None:
for module_str in modules:
function = module_str.split(".")[-1]
location = module_str[: -1 * len(function) - 1]
module = getattr(importlib.import_module(location), function)
sync_modules_to_import[function] = module

all_modules = {**original_globals, **globals(), **sync_modules_to_import}
else:
all_modules = {**original_globals, **globals()}
# 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

Expand Down Expand Up @@ -290,14 +294,16 @@ def modified_test(self):
asyncio.run(
_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), 2)
if not async_only:
with self.subTest(version="sync", client="json"):
_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), 3)
)
with self.subTest(version="sync", client="websocket"):
_run_sync_test(self, _get_client(False, False, use_testnet), 4)
if not async_only:
with self.subTest(version="sync", client="websocket"):
_run_sync_test(self, _get_client(False, False, use_testnet), 4)

return modified_test

Expand Down
23 changes: 23 additions & 0 deletions tests/integration/reqs/test_simulate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from tests.integration.integration_test_case import IntegrationTestCase
from tests.integration.it_utils import test_async_and_sync
from tests.integration.reusable_values import WALLET
from xrpl.models import AccountSet, Simulate


class TestSimulate(IntegrationTestCase):
@test_async_and_sync(globals())
async def test_basic_functionality(self, client):
response = await client.request(
Simulate(transaction=AccountSet(account=WALLET.classic_address))
)

self.assertEqual(response.type, "response")
self.assertIn(
"meta", response.result, "Key 'meta' not found in simulate response."
)
self.assertIsInstance(
response.result["meta"], dict, "'meta' should be a dictionary."
)
self.assertEqual(response.result["engine_result"], "tesSUCCESS")
self.assertEqual(response.result["engine_result_code"], 0)
self.assertFalse(response.result["applied"])
137 changes: 76 additions & 61 deletions tests/integration/sugar/test_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,15 @@
from xrpl.asyncio.ledger import get_fee, get_latest_validated_ledger_sequence
from xrpl.asyncio.transaction import (
XRPLReliableSubmissionException,
_calculate_fee_per_transaction_type,
autofill,
autofill_and_sign,
sign,
sign_and_submit,
simulate,
)
from xrpl.asyncio.transaction import submit as submit_transaction_alias_async
from xrpl.asyncio.transaction import submit_and_wait
from xrpl.asyncio.transaction.main import (
_calculate_fee_per_transaction_type,
sign_and_submit,
)
from xrpl.clients import XRPLRequestFailureException
from xrpl.core.addresscodec import classic_address_to_xaddress
from xrpl.core.binarycodec.main import encode
Expand Down Expand Up @@ -254,6 +253,79 @@ async def test_networkid_reserved_networks(self, client):
self.assertIsNone(transaction.network_id)
self.assertEqual(client.network_id, 1)

@test_async_and_sync(
globals(),
[
"xrpl.transaction.sign_and_submit",
],
)
async def test_sign_and_submit(self, client):
payment_dict = {
"account": ACCOUNT,
"fee": "10",
"amount": "100",
"destination": DESTINATION,
}
payment_transaction = Payment.from_dict(payment_dict)
response = await sign_and_submit(payment_transaction, client, WALLET)
self.assertTrue(response.is_successful())

@test_async_and_sync(globals(), async_only=True)
async def test_basic_calculate_fee_per_transaction_type(self, client):
fee = await _calculate_fee_per_transaction_type(
Payment(
account="rweYz56rfmQ98cAdRaeTxQS9wVMGnrdsFp",
amount=IssuedCurrencyAmount(
currency="USD",
issuer="rweYz56rfmQ98cAdRaeTxQS9wVMGnrdsFp",
value="0.0001",
),
destination="rweYz56rfmQ98cAdRaeTxQS9wVMGnrdsFp",
send_max=IssuedCurrencyAmount(
currency="BTC",
issuer="rweYz56rfmQ98cAdRaeTxQS9wVMGnrdsFp",
value="0.0000002831214446",
),
),
client,
)

# The expected fee is read from the below-specified config file
expected_fee = ""
with open(".ci-config/rippled.cfg", "r", encoding="utf-8") as file:
lines = file.readlines() # Read all lines into a list

for value in lines:
kv_pairs = value.split()
# This step assumes that no non-`voting` section in the config file
# uses the reference_fee key-value pair.
if "reference_fee" in kv_pairs:
expected_fee = kv_pairs[2]
break

self.assertEqual(fee, expected_fee)

@test_async_and_sync(
globals(),
[
"xrpl.transaction.simulate",
],
)
async def test_simulate(self, client):
response = await simulate(AccountSet(account=WALLET.address), client)

self.assertTrue(response.is_successful())
self.assertEqual(response.type, "response")
self.assertIn(
"meta", response.result, "Key 'meta' not found in simulate response."
)
self.assertIsInstance(
response.result["meta"], dict, "'meta' should be a dictionary."
)
self.assertEqual(response.result["engine_result"], "tesSUCCESS")
self.assertEqual(response.result["engine_result_code"], 0)
self.assertFalse(response.result["applied"])


class TestSubmitAndWait(IntegrationTestCase):
@test_async_and_sync(
Expand Down Expand Up @@ -419,60 +491,3 @@ async def test_reliable_submission_no_last_ledger_sequence(self, client):
signed_payment_transaction = sign(payment_transaction, WALLET)
with self.assertRaises(XRPLReliableSubmissionException):
await submit_and_wait(signed_payment_transaction, client)

@test_async_and_sync(
globals(),
[
"xrpl.transaction.sign_and_submit",
],
)
async def test_sign_and_submit(self, client):
payment_dict = {
"account": ACCOUNT,
"fee": "10",
"amount": "100",
"destination": DESTINATION,
}
payment_transaction = Payment.from_dict(payment_dict)
response = await sign_and_submit(payment_transaction, client, WALLET)
self.assertTrue(response.is_successful())

@test_async_and_sync(
globals(),
[
"xrpl.transaction._calculate_fee_per_transaction_type",
],
)
async def test_basic_calculate_fee_per_transaction_type(self, client):
fee = await _calculate_fee_per_transaction_type(
Payment(
account="rweYz56rfmQ98cAdRaeTxQS9wVMGnrdsFp",
amount=IssuedCurrencyAmount(
currency="USD",
issuer="rweYz56rfmQ98cAdRaeTxQS9wVMGnrdsFp",
value="0.0001",
),
destination="rweYz56rfmQ98cAdRaeTxQS9wVMGnrdsFp",
send_max=IssuedCurrencyAmount(
currency="BTC",
issuer="rweYz56rfmQ98cAdRaeTxQS9wVMGnrdsFp",
value="0.0000002831214446",
),
),
client,
)

# The expected fee is read from the below-specified config file
expected_fee = ""
with open(".ci-config/rippled.cfg", "r", encoding="utf-8") as file:
lines = file.readlines() # Read all lines into a list

for value in lines:
kv_pairs = value.split()
# This step assumes that no non-`voting` section in the config file
# uses the reference_fee key-value pair.
if "reference_fee" in kv_pairs:
expected_fee = kv_pairs[2]
break

self.assertEqual(fee, expected_fee)
65 changes: 65 additions & 0 deletions tests/unit/models/requests/test_simulate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import unittest

from xrpl.models import AccountSet, Simulate
from xrpl.models.exceptions import XRPLModelException
from xrpl.transaction import sign
from xrpl.wallet import Wallet

_WALLET = Wallet.create()
_TRANSACTION = AccountSet(account=_WALLET.address, sequence=1)


class TestSimulate(unittest.TestCase):
def test_simulate_with_both_tx_blob_and_transaction(self):
with self.assertRaises(XRPLModelException) as e:
Simulate(
tx_blob=_TRANSACTION.blob(),
transaction=_TRANSACTION,
)
self.assertEqual(
e.exception.args[0],
"{'tx': 'Must have exactly one of `tx_blob` and `transaction` fields.'}",
)

def test_simulate_with_neither_tx_blob_nor_transaction(self):
with self.assertRaises(XRPLModelException) as e:
Simulate()
self.assertEqual(
e.exception.args[0],
"{'tx': 'Must have exactly one of `tx_blob` and `transaction` fields.'}",
)

def test_simulate_with_signed_transaction(self):
signed_tx = sign(_TRANSACTION, _WALLET)
with self.assertRaises(XRPLModelException) as e:
Simulate(transaction=signed_tx)
self.assertEqual(
e.exception.args[0],
"{'transaction': 'Cannot simulate a signed transaction.'}",
)

def test_simulate_with_valid_tx_blob(self):
tx_blob = _TRANSACTION.blob()
simulate = Simulate(tx_blob=tx_blob)
self.assertEqual(simulate.tx_blob, tx_blob)
self.assertIsNone(simulate.transaction)
self.assertIsNone(simulate.binary)
self.assertEqual(simulate.method, "simulate")
self.assertTrue(simulate.is_valid())

def test_simulate_with_valid_transaction(self):
simulate = Simulate(transaction=_TRANSACTION)
self.assertIsNone(simulate.tx_blob)
self.assertEqual(simulate.transaction, _TRANSACTION)
self.assertIsNone(simulate.binary)
self.assertEqual(simulate.method, "simulate")
self.assertTrue(simulate.is_valid())

def test_simulate_with_binary(self):
tx_blob = _TRANSACTION.blob()
simulate = Simulate(tx_blob=tx_blob, binary=True)
self.assertEqual(simulate.tx_blob, tx_blob)
self.assertIsNone(simulate.transaction)
self.assertTrue(simulate.binary)
self.assertEqual(simulate.method, "simulate")
self.assertTrue(simulate.is_valid())
5 changes: 2 additions & 3 deletions tests/unit/models/test_base_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ def test_from_dict_recursive_currency(self):

def test_from_dict_recursive_transaction(self):
transaction = CheckCreate.from_dict(check_create_dict)
sign_dict = {"secret": secret, "transaction": transaction.to_dict()}
sign_dict = {"secret": secret, "tx_json": transaction.to_xrpl()}
sign = Sign.from_dict(sign_dict)

expected_dict = {
Expand All @@ -136,12 +136,11 @@ def test_from_dict_recursive_transaction(self):
"offline": False,
"api_version": _DEFAULT_API_VERSION,
}
del expected_dict["transaction"]
self.assertEqual(expected_dict, sign.to_dict())

def test_from_dict_recursive_transaction_tx_json(self):
transaction = CheckCreate.from_dict(check_create_dict)
sign_dict = {"secret": secret, "tx_json": transaction.to_dict()}
sign_dict = {"secret": secret, "tx_json": transaction.to_xrpl()}
sign = Sign.from_dict(sign_dict)

expected_dict = {
Expand Down
2 changes: 2 additions & 0 deletions xrpl/asyncio/transaction/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
autofill_and_sign,
sign,
sign_and_submit,
simulate,
submit,
transaction_json_to_binary_codec_form,
)
Expand All @@ -19,6 +20,7 @@
"autofill_and_sign",
"sign",
"sign_and_submit",
"simulate",
"submit",
"submit_and_wait",
"transaction_json_to_binary_codec_form",
Expand Down
Loading