From ba7a2a081b49324ed034f682f32ad33d551035eb Mon Sep 17 00:00:00 2001 From: Omar Khan Date: Thu, 2 Nov 2023 13:38:55 -0400 Subject: [PATCH 1/2] test: add AMM integration tests (#645) --- .ci-config/rippled.cfg | 1 + .github/workflows/integration_test.yml | 9 +- CONTRIBUTING.md | 2 +- tests/integration/it_utils.py | 162 +++++++++- tests/integration/reqs/test_amm_info.py | 28 ++ tests/integration/reusable_values.py | 45 +++ .../integration/transactions/test_amm_bid.py | 162 ++++++++++ .../transactions/test_amm_create.py | 28 ++ .../transactions/test_amm_deposit.py | 287 +++++++++++++++++ .../integration/transactions/test_amm_vote.py | 67 ++++ .../transactions/test_amm_withdraw.py | 289 ++++++++++++++++++ 11 files changed, 1074 insertions(+), 6 deletions(-) create mode 100644 tests/integration/reqs/test_amm_info.py create mode 100644 tests/integration/transactions/test_amm_bid.py create mode 100644 tests/integration/transactions/test_amm_create.py create mode 100644 tests/integration/transactions/test_amm_deposit.py create mode 100644 tests/integration/transactions/test_amm_vote.py create mode 100644 tests/integration/transactions/test_amm_withdraw.py diff --git a/.ci-config/rippled.cfg b/.ci-config/rippled.cfg index 24e072709..8ac86fa55 100644 --- a/.ci-config/rippled.cfg +++ b/.ci-config/rippled.cfg @@ -163,6 +163,7 @@ XRPFees # 1.11.0 Amendments ExpandedSignerList # 1.12.0 Amendments +AMM Clawback fixReducedOffersV1 fixNFTokenRemint diff --git a/.github/workflows/integration_test.yml b/.github/workflows/integration_test.yml index 56d28f375..955d1175b 100644 --- a/.github/workflows/integration_test.yml +++ b/.github/workflows/integration_test.yml @@ -1,14 +1,15 @@ name: Integration test +env: + POETRY_VERSION: 1.4.2 + RIPPLED_DOCKER_IMAGE: rippleci/rippled:2.0.0-b3 + on: push: branches: [ master ] pull_request: workflow_dispatch: -env: - POETRY_VERSION: 1.4.2 - jobs: integration-test: name: Integration test @@ -31,7 +32,7 @@ jobs: - name: Run docker in background run: | - docker run --detach --rm --name rippled-service -p 5005:5005 -p 6006:6006 --volume "${{ github.workspace }}/.ci-config/":"/opt/ripple/etc/" --health-cmd="wget localhost:6006 || exit 1" --health-interval=5s --health-retries=10 --health-timeout=2s --env GITHUB_ACTIONS=true --env CI=true rippleci/rippled:2.0.0-b3 /opt/ripple/bin/rippled -a --conf /opt/ripple/etc/rippled.cfg + docker run --detach --rm --name rippled-service -p 5005:5005 -p 6006:6006 --volume "${{ github.workspace }}/.ci-config/":"/opt/ripple/etc/" --health-cmd="wget localhost:6006 || exit 1" --health-interval=5s --health-retries=10 --health-timeout=2s --env GITHUB_ACTIONS=true --env CI=true ${{ env.RIPPLED_DOCKER_IMAGE }} /opt/ripple/bin/rippled -a --conf /opt/ripple/etc/rippled.cfg - name: Install poetry if: steps.cache-poetry.outputs.cache-hit != 'true' diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index daf825db5..799e0624f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -98,7 +98,7 @@ Breaking down the command: * `--interactive` allows you to interact with the container. * `-t` starts a terminal in the container for you to send commands to. * `--volume $PWD/.ci-config:/config/` identifies the `rippled.cfg` and `validators.txt` to import. It must be an absolute path, so we use `$PWD` instead of `./`. -* `xrpllabsofficial/xrpld:1.12.0-b1` is an image that is regularly updated with the latest `rippled` releases and can be found here: https://github.com/WietseWind/docker-rippled +* `xrpllabsofficial/xrpld:1.12.0` is an image that is regularly updated with the latest `rippled` releases and can be found here: https://github.com/WietseWind/docker-rippled * `-a` starts `rippled` in standalone mode * `--start` signals to start `rippled` with the specified amendments in `rippled.cfg` enabled immediately instead of voting for 2 weeks on them. diff --git a/tests/integration/it_utils.py b/tests/integration/it_utils.py index 5de937c84..72a74e3e3 100644 --- a/tests/integration/it_utils.py +++ b/tests/integration/it_utils.py @@ -4,7 +4,7 @@ import inspect from threading import Timer as ThreadingTimer from time import sleep -from typing import cast +from typing import Any, Dict, cast import xrpl # noqa: F401 - needed for sync tests from xrpl.asyncio.clients import AsyncJsonRpcClient, AsyncWebsocketClient @@ -14,6 +14,12 @@ from xrpl.clients.sync_client import SyncClient from xrpl.constants import CryptoAlgorithm 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 +from xrpl.models.transactions.account_set import AccountSet, AccountSetAsfFlag +from xrpl.models.transactions.amm_create import AMMCreate +from xrpl.models.transactions.trust_set import TrustSet, TrustSetFlag from xrpl.transaction import sign_and_submit # noqa: F401 - needed for sync tests from xrpl.transaction import ( # noqa: F401 - needed for sync tests submit as submit_transaction_alias, @@ -302,3 +308,157 @@ def _get_non_decorator_code(function): if "def " in code_lines[line]: return code_lines[line:] line += 1 + + +def create_amm_pool( + client: SyncClient = JSON_RPC_CLIENT, +) -> Dict[str, Any]: + issuer_wallet = Wallet.create() + fund_wallet(issuer_wallet) + lp_wallet = Wallet.create() + fund_wallet(lp_wallet) + currency_code = "USD" + + # test prerequisites - create trustline and send funds + sign_and_reliable_submission( + AccountSet( + account=issuer_wallet.classic_address, + set_flag=AccountSetAsfFlag.ASF_DEFAULT_RIPPLE, + ), + issuer_wallet, + ) + + sign_and_reliable_submission( + TrustSet( + account=lp_wallet.classic_address, + flags=TrustSetFlag.TF_CLEAR_NO_RIPPLE, + limit_amount=IssuedCurrencyAmount( + issuer=issuer_wallet.classic_address, + currency=currency_code, + value="1000", + ), + ), + lp_wallet, + ) + + sign_and_reliable_submission( + Payment( + account=issuer_wallet.classic_address, + destination=lp_wallet.classic_address, + amount=IssuedCurrencyAmount( + currency=currency_code, + issuer=issuer_wallet.classic_address, + value="500", + ), + ), + issuer_wallet, + ) + + sign_and_reliable_submission( + AMMCreate( + account=lp_wallet.classic_address, + amount="250", + amount2=IssuedCurrencyAmount( + issuer=issuer_wallet.classic_address, + currency=currency_code, + value="250", + ), + trading_fee=12, + ), + lp_wallet, + client, + ) + + asset = XRP() + asset2 = IssuedCurrency( + currency=currency_code, + issuer=issuer_wallet.classic_address, + ) + + return { + "asset": asset, + "asset2": asset2, + "issuer_wallet": issuer_wallet, + } + + +async def create_amm_pool_async( + client: AsyncClient = ASYNC_JSON_RPC_CLIENT, +) -> Dict[str, Any]: + issuer_wallet = Wallet.create() + await fund_wallet_async(issuer_wallet) + lp_wallet = Wallet.create() + await fund_wallet_async(lp_wallet) + currency_code = "USD" + + # test prerequisites - create trustline and send funds + await sign_and_reliable_submission_async( + AccountSet( + account=issuer_wallet.classic_address, + set_flag=AccountSetAsfFlag.ASF_DEFAULT_RIPPLE, + ), + issuer_wallet, + ) + + await sign_and_reliable_submission_async( + TrustSet( + account=lp_wallet.classic_address, + flags=TrustSetFlag.TF_CLEAR_NO_RIPPLE, + limit_amount=IssuedCurrencyAmount( + issuer=issuer_wallet.classic_address, + currency=currency_code, + value="1000", + ), + ), + lp_wallet, + ) + + await sign_and_reliable_submission_async( + Payment( + account=issuer_wallet.classic_address, + destination=lp_wallet.classic_address, + amount=IssuedCurrencyAmount( + currency=currency_code, + issuer=issuer_wallet.classic_address, + value="500", + ), + ), + issuer_wallet, + ) + + await sign_and_reliable_submission_async( + AMMCreate( + account=lp_wallet.classic_address, + amount="250", + amount2=IssuedCurrencyAmount( + issuer=issuer_wallet.classic_address, + currency=currency_code, + value="250", + ), + trading_fee=12, + ), + lp_wallet, + client, + ) + + asset = XRP() + asset2 = IssuedCurrency( + currency=currency_code, + issuer=issuer_wallet.classic_address, + ) + + return { + "asset": asset, + "asset2": asset2, + "issuer_wallet": issuer_wallet, + } + + +def compare_amm_values(val, val2, round_buffer): + diff = abs(float(val) - float(val2)) + if diff > round_buffer: + raise ValueError( + f"Values [{val}, {val2}] with difference {diff} are too far apart " + f"with round_buffer {round_buffer}" + ) + return True diff --git a/tests/integration/reqs/test_amm_info.py b/tests/integration/reqs/test_amm_info.py new file mode 100644 index 000000000..d64ed5eb3 --- /dev/null +++ b/tests/integration/reqs/test_amm_info.py @@ -0,0 +1,28 @@ +from tests.integration.integration_test_case import IntegrationTestCase +from tests.integration.it_utils import test_async_and_sync +from tests.integration.reusable_values import AMM_ASSET, AMM_ASSET2 +from xrpl.models.requests.amm_info import AMMInfo + +asset = AMM_ASSET +asset2 = AMM_ASSET2 + + +class TestAMMInfo(IntegrationTestCase): + @test_async_and_sync(globals()) + async def test_basic_functionality(self, client): + amm_info = await client.request( + AMMInfo( + asset=asset, + asset2=asset2, + ) + ) + + self.assertEqual(float(amm_info.result["amm"]["amount"]), 1250) + self.assertEqual( + amm_info.result["amm"]["amount2"], + { + "currency": asset2.currency, + "issuer": asset2.issuer, + "value": "250", + }, + ) diff --git a/tests/integration/reusable_values.py b/tests/integration/reusable_values.py index 5aeb75ef0..a31c0fcee 100644 --- a/tests/integration/reusable_values.py +++ b/tests/integration/reusable_values.py @@ -1,10 +1,15 @@ import asyncio +from typing import Any, Dict from tests.integration.it_utils import ( + ASYNC_JSON_RPC_CLIENT, + create_amm_pool_async, fund_wallet_async, sign_and_reliable_submission_async, ) +from xrpl.asyncio.clients.async_client import AsyncClient from xrpl.models import IssuedCurrencyAmount, OfferCreate, PaymentChannelCreate +from xrpl.models.transactions.amm_deposit import AMMDeposit, AMMDepositFlag from xrpl.wallet import Wallet @@ -41,17 +46,57 @@ async def _set_up_reusable_values(): WALLET, ) + setup_amm_pool_res = await setup_amm_pool(wallet=WALLET) + AMM_ASSET = setup_amm_pool_res["asset"] + AMM_ASSET2 = setup_amm_pool_res["asset2"] + AMM_ISSUER_WALLET = setup_amm_pool_res["issuer_wallet"] + return ( WALLET, DESTINATION, OFFER, PAYMENT_CHANNEL, + AMM_ASSET, + AMM_ASSET2, + AMM_ISSUER_WALLET, ) +async def setup_amm_pool( + wallet: Wallet, + client: AsyncClient = ASYNC_JSON_RPC_CLIENT, +) -> Dict[str, Any]: + amm_pool = await create_amm_pool_async(client=client) + asset = amm_pool["asset"] + asset2 = amm_pool["asset2"] + issuer_wallet = amm_pool["issuer_wallet"] + + # Need to deposit (be an LP) to make bid/vote/withdraw eligible in tests for WALLET + await sign_and_reliable_submission_async( + AMMDeposit( + account=wallet.classic_address, + asset=asset, + asset2=asset2, + amount="1000", + flags=AMMDepositFlag.TF_SINGLE_ASSET, + ), + wallet, + client, + ) + + return { + "asset": asset, + "asset2": asset2, + "issuer_wallet": issuer_wallet, + } + + ( WALLET, DESTINATION, OFFER, PAYMENT_CHANNEL, + AMM_ASSET, + AMM_ASSET2, + AMM_ISSUER_WALLET, ) = asyncio.run(_set_up_reusable_values()) diff --git a/tests/integration/transactions/test_amm_bid.py b/tests/integration/transactions/test_amm_bid.py new file mode 100644 index 000000000..6ec0c222f --- /dev/null +++ b/tests/integration/transactions/test_amm_bid.py @@ -0,0 +1,162 @@ +from tests.integration.integration_test_case import IntegrationTestCase +from tests.integration.it_utils import ( + compare_amm_values, + sign_and_reliable_submission_async, + test_async_and_sync, +) +from tests.integration.reusable_values import ( + AMM_ASSET, + AMM_ASSET2, + AMM_ISSUER_WALLET, + WALLET, +) +from xrpl.models.amounts.issued_currency_amount import IssuedCurrencyAmount +from xrpl.models.auth_account import AuthAccount +from xrpl.models.requests.amm_info import AMMInfo +from xrpl.models.transactions.amm_bid import AMMBid + +asset = AMM_ASSET +asset2 = AMM_ASSET2 +issuer_wallet = AMM_ISSUER_WALLET + + +class TestAMMBid(IntegrationTestCase): + @test_async_and_sync(globals()) + async def test_basic_functionality(self, client): + pre_amm_info = await client.request( + AMMInfo( + asset=asset, + asset2=asset2, + ) + ) + + response = await sign_and_reliable_submission_async( + AMMBid( + account=WALLET.classic_address, + asset=asset, + asset2=asset2, + ), + WALLET, + client, + ) + + self.assertTrue(response.is_successful()) + self.assertEqual(response.result["engine_result"], "tesSUCCESS") + + amm_info = await client.request( + AMMInfo( + asset=asset, + asset2=asset2, + ) + ) + + before_price = float( + pre_amm_info.result["amm"]["auction_slot"]["price"]["value"] + ) + diff_price = -0.002891202555 + expected_price = before_price + diff_price + + before_lptoken = float(pre_amm_info.result["amm"]["lp_token"]["value"]) + diff_lptoken = -0.003099233161 + expected_lptoken = before_lptoken + diff_lptoken + + self.assertGreater( + float(amm_info.result["amm"]["auction_slot"]["price"]["value"]), + before_price, + ) + self.assertTrue( + compare_amm_values( + amm_info.result["amm"]["auction_slot"]["price"]["value"], + expected_price, + 0.31212452270475843, + ) + ) + self.assertLess( + float(amm_info.result["amm"]["lp_token"]["value"]), + before_lptoken, + ) + self.assertTrue( + compare_amm_values( + amm_info.result["amm"]["lp_token"]["value"], + expected_lptoken, + 0.6109616023928766, + ) + ) + + @test_async_and_sync(globals()) + async def test_with_auth_account_bidmin_bidmax(self, client): + pre_amm_info = await client.request( + AMMInfo( + asset=asset, + asset2=asset2, + ) + ) + lp_token = pre_amm_info.result["amm"]["lp_token"] + + response = await sign_and_reliable_submission_async( + AMMBid( + account=WALLET.classic_address, + asset=asset, + asset2=asset2, + auth_accounts=[AuthAccount(account=issuer_wallet.classic_address)], + bid_min=IssuedCurrencyAmount( + currency=lp_token["currency"], + issuer=lp_token["issuer"], + value="5", + ), + bid_max=IssuedCurrencyAmount( + currency=lp_token["currency"], + issuer=lp_token["issuer"], + value="10", + ), + ), + WALLET, + client, + ) + + self.assertTrue(response.is_successful()) + self.assertEqual(response.result["engine_result"], "tesSUCCESS") + + amm_info = await client.request( + AMMInfo( + asset=asset, + asset2=asset2, + ) + ) + + before_price_value = float( + pre_amm_info.result["amm"]["auction_slot"]["price"]["value"] + ) + diff_price_value = 1.4462339482 + expected_price = before_price_value + diff_price_value + + before_lptoken_value = float(pre_amm_info.result["amm"]["lp_token"]["value"]) + diff_lptoken_value = -1.6435111096 + expected_lptoken_value = before_lptoken_value + diff_lptoken_value + + self.assertGreater( + float(amm_info.result["amm"]["auction_slot"]["price"]["value"]), + before_price_value, + ) + self.assertTrue( + compare_amm_values( + amm_info.result["amm"]["auction_slot"]["price"]["value"], + expected_price, + 3.5493258038256785, + ) + ) + self.assertLess( + float(amm_info.result["amm"]["lp_token"]["value"]), + before_lptoken_value, + ) + self.assertTrue( + compare_amm_values( + amm_info.result["amm"]["lp_token"]["value"], + expected_lptoken_value, + 3.352270654824224, + ) + ) + self.assertEqual( + amm_info.result["amm"]["auction_slot"]["auth_accounts"], + [{"account": issuer_wallet.classic_address}], + ) diff --git a/tests/integration/transactions/test_amm_create.py b/tests/integration/transactions/test_amm_create.py new file mode 100644 index 000000000..abca68160 --- /dev/null +++ b/tests/integration/transactions/test_amm_create.py @@ -0,0 +1,28 @@ +from tests.integration.integration_test_case import IntegrationTestCase +from tests.integration.it_utils import create_amm_pool_async, test_async_and_sync +from xrpl.models.requests.amm_info import AMMInfo + + +class TestAMMCreate(IntegrationTestCase): + @test_async_and_sync(globals()) + async def test_basic_functionality(self, client): + amm_pool = await create_amm_pool_async(client) + asset = amm_pool["asset"] + asset2 = amm_pool["asset2"] + + amm_info = await client.request( + AMMInfo( + asset=asset, + asset2=asset2, + ) + ) + + self.assertEqual(float(amm_info.result["amm"]["amount"]), 250) + self.assertEqual( + amm_info.result["amm"]["amount2"], + { + "currency": asset2.currency, + "issuer": asset2.issuer, + "value": "250", + }, + ) diff --git a/tests/integration/transactions/test_amm_deposit.py b/tests/integration/transactions/test_amm_deposit.py new file mode 100644 index 000000000..78b4f5556 --- /dev/null +++ b/tests/integration/transactions/test_amm_deposit.py @@ -0,0 +1,287 @@ +from tests.integration.integration_test_case import IntegrationTestCase +from tests.integration.it_utils import ( + compare_amm_values, + sign_and_reliable_submission_async, + test_async_and_sync, +) +from tests.integration.reusable_values import ( + AMM_ASSET, + AMM_ASSET2, + AMM_ISSUER_WALLET, + WALLET, +) +from xrpl.models import AMMDeposit +from xrpl.models.amounts.issued_currency_amount import IssuedCurrencyAmount +from xrpl.models.requests.amm_info import AMMInfo +from xrpl.models.transactions.amm_deposit import AMMDepositFlag + +asset = AMM_ASSET +asset2 = AMM_ASSET2 +issuer_wallet = AMM_ISSUER_WALLET + + +class TestAMMDeposit(IntegrationTestCase): + @test_async_and_sync(globals()) + async def test_single_asset(self, client): + pre_amm_info = await client.request( + AMMInfo( + asset=asset, + asset2=asset2, + ) + ) + + response = await sign_and_reliable_submission_async( + AMMDeposit( + account=WALLET.classic_address, + asset=asset, + asset2=asset2, + amount="1000", + flags=AMMDepositFlag.TF_SINGLE_ASSET, + ), + WALLET, + client, + ) + + self.assertTrue(response.is_successful()) + self.assertEqual(response.result["engine_result"], "tesSUCCESS") + + amm_info = await client.request( + AMMInfo( + asset=asset, + asset2=asset2, + ) + ) + + before_amount = float(pre_amm_info.result["amm"]["amount"]) + diff_amount = 1000 + expected_amount = before_amount + diff_amount + + before_lptoken_value = float(pre_amm_info.result["amm"]["lp_token"]["value"]) + diff_lptoken_value = 143.850763914 + expected_lptoken_value = before_lptoken_value + diff_lptoken_value + + self.assertEqual(float(amm_info.result["amm"]["amount"]), expected_amount) + self.assertEqual( + float(amm_info.result["amm"]["amount2"]["value"]), + float(pre_amm_info.result["amm"]["amount2"]["value"]), + ) + self.assertGreater( + float(amm_info.result["amm"]["lp_token"]["value"]), + before_lptoken_value, + ) + self.assertTrue( + compare_amm_values( + amm_info.result["amm"]["lp_token"]["value"], + expected_lptoken_value, + 47.1144478672054, + ) + ) + + @test_async_and_sync(globals()) + async def test_two_assets(self, client): + pre_amm_info = await client.request( + AMMInfo( + asset=asset, + asset2=asset2, + ) + ) + + response = await sign_and_reliable_submission_async( + AMMDeposit( + account=issuer_wallet.classic_address, + asset=asset, + asset2=asset2, + amount="100", + amount2=IssuedCurrencyAmount( + currency=asset2.currency, + issuer=asset2.issuer, + value="100", + ), + flags=AMMDepositFlag.TF_TWO_ASSET, + ), + issuer_wallet, + client, + ) + + self.assertTrue(response.is_successful()) + self.assertEqual(response.result["engine_result"], "tesSUCCESS") + + amm_info = await client.request( + AMMInfo( + asset=asset, + asset2=asset2, + ) + ) + + before_amount = float(pre_amm_info.result["amm"]["amount"]) + diff_amount = 100 + expected_amount = before_amount + diff_amount + + before_amount2_value = float(pre_amm_info.result["amm"]["amount2"]["value"]) + diff_amount2 = 5 + expected_amount2_value = before_amount2_value + diff_amount2 + + before_lptoken_value = float(pre_amm_info.result["amm"]["lp_token"]["value"]) + diff_lptoken = 21.6824186193 + expected_lptoken_value = before_lptoken_value + diff_lptoken + + self.assertEqual( + float(amm_info.result["amm"]["amount"]), + expected_amount, + ) + self.assertGreater( + float(amm_info.result["amm"]["amount2"]["value"]), + before_amount2_value, + ) + self.assertTrue( + compare_amm_values( + amm_info.result["amm"]["amount2"]["value"], + expected_amount2_value, + 0.3192286088748233, + ) + ) + self.assertGreater( + float(amm_info.result["amm"]["lp_token"]["value"]), + before_lptoken_value, + ) + self.assertTrue( + compare_amm_values( + amm_info.result["amm"]["lp_token"]["value"], + expected_lptoken_value, + 0.25026967439612235, + ) + ) + + @test_async_and_sync(globals()) + async def test_one_asset_with_lptoken(self, client): + pre_amm_info = await client.request( + AMMInfo( + asset=asset, + asset2=asset2, + ) + ) + + lp_token = pre_amm_info.result["amm"]["lp_token"] + + response = await sign_and_reliable_submission_async( + AMMDeposit( + account=issuer_wallet.classic_address, + asset=asset, + asset2=asset2, + amount="100", + lp_token_out=IssuedCurrencyAmount( + currency=lp_token["currency"], + issuer=lp_token["issuer"], + value="5", + ), + flags=AMMDepositFlag.TF_ONE_ASSET_LP_TOKEN, + ), + issuer_wallet, + client, + ) + + self.assertTrue(response.is_successful()) + self.assertEqual(response.result["engine_result"], "tesSUCCESS") + + amm_info = await client.request( + AMMInfo( + asset=asset, + asset2=asset2, + ) + ) + + before_amount = float(pre_amm_info.result["amm"]["amount"]) + diff_amount = 23 + expected_amount = before_amount + diff_amount + + before_lptoken_value = float(pre_amm_info.result["amm"]["lp_token"]["value"]) + diff_lptoken = 5 + expected_lptoken_value = before_lptoken_value + diff_lptoken + + self.assertGreater( + float(amm_info.result["amm"]["amount"]), + before_amount, + ) + self.assertTrue( + compare_amm_values( + float(amm_info.result["amm"]["amount"]), + expected_amount, + 1, + ) + ) + self.assertEqual( + amm_info.result["amm"]["amount2"], + pre_amm_info.result["amm"]["amount2"], + ) + self.assertEqual( + float(amm_info.result["amm"]["lp_token"]["value"]), expected_lptoken_value + ) + + @test_async_and_sync(globals()) + async def test_lptoken(self, client): + pre_amm_info = await client.request( + AMMInfo( + asset=asset, + asset2=asset2, + ) + ) + + lp_token = pre_amm_info.result["amm"]["lp_token"] + + response = await sign_and_reliable_submission_async( + AMMDeposit( + account=issuer_wallet.classic_address, + asset=asset, + asset2=asset2, + lp_token_out=IssuedCurrencyAmount( + currency=lp_token["currency"], + issuer=lp_token["issuer"], + value="5", + ), + flags=AMMDepositFlag.TF_LP_TOKEN, + ), + issuer_wallet, + client, + ) + + self.assertTrue(response.is_successful()) + self.assertEqual(response.result["engine_result"], "tesSUCCESS") + + amm_info = await client.request( + AMMInfo( + asset=asset, + asset2=asset2, + ) + ) + + before_amount = float(pre_amm_info.result["amm"]["amount"]) + diff_amount = 11 + expected_amount = before_amount + diff_amount + + before_amount2_value = float(pre_amm_info.result["amm"]["amount2"]["value"]) + diff_amount2_value = 2.2628038035 + expected_amount2_value = before_amount2_value + diff_amount2_value + + before_lptoken_value = float(pre_amm_info.result["amm"]["lp_token"]["value"]) + diff_lptoken_value = 5 + expected_lptoken_value = before_lptoken_value + diff_lptoken_value + + self.assertEqual( + float(amm_info.result["amm"]["amount"]), + expected_amount, + ) + self.assertGreater( + float(amm_info.result["amm"]["amount2"]["value"]), + before_amount2_value, + ) + self.assertTrue( + compare_amm_values( + amm_info.result["amm"]["amount2"]["value"], + expected_amount2_value, + 0.0266616562412878, + ) + ) + self.assertEqual( + float(amm_info.result["amm"]["lp_token"]["value"]), + expected_lptoken_value, + ) diff --git a/tests/integration/transactions/test_amm_vote.py b/tests/integration/transactions/test_amm_vote.py new file mode 100644 index 000000000..82a752add --- /dev/null +++ b/tests/integration/transactions/test_amm_vote.py @@ -0,0 +1,67 @@ +from tests.integration.integration_test_case import IntegrationTestCase +from tests.integration.it_utils import ( + compare_amm_values, + sign_and_reliable_submission_async, + test_async_and_sync, +) +from tests.integration.reusable_values import ( + AMM_ASSET, + AMM_ASSET2, + AMM_ISSUER_WALLET, + WALLET, +) +from xrpl.models.requests.amm_info import AMMInfo +from xrpl.models.transactions.amm_vote import AMMVote + +asset = AMM_ASSET +asset2 = AMM_ASSET2 +issuer_wallet = AMM_ISSUER_WALLET + + +class TestAMMVote(IntegrationTestCase): + @test_async_and_sync(globals()) + async def test_basic_functionality(self, client): + pre_amm_info = await client.request( + AMMInfo( + asset=asset, + asset2=asset2, + ) + ) + + response = await sign_and_reliable_submission_async( + AMMVote( + account=WALLET.classic_address, + asset=asset, + asset2=asset2, + trading_fee=pre_amm_info.result["amm"]["trading_fee"] + 10, + ), + WALLET, + client, + ) + + self.assertTrue(response.is_successful()) + self.assertEqual(response.result["engine_result"], "tesSUCCESS") + + amm_info = await client.request( + AMMInfo( + asset=asset, + asset2=asset2, + ) + ) + + before_trading_fee = pre_amm_info.result["amm"]["trading_fee"] + diff_trading_fee = 5.75 + expected_trading_fee = before_trading_fee + diff_trading_fee + + self.assertGreater( + amm_info.result["amm"]["trading_fee"], + before_trading_fee, + ) + self.assertTrue( + compare_amm_values( + amm_info.result["amm"]["trading_fee"], + expected_trading_fee, + 3.75, + ) + ) + self.assertEqual(len(amm_info.result["amm"]["vote_slots"]), 2) diff --git a/tests/integration/transactions/test_amm_withdraw.py b/tests/integration/transactions/test_amm_withdraw.py new file mode 100644 index 000000000..779337139 --- /dev/null +++ b/tests/integration/transactions/test_amm_withdraw.py @@ -0,0 +1,289 @@ +from tests.integration.integration_test_case import IntegrationTestCase +from tests.integration.it_utils import ( + compare_amm_values, + sign_and_reliable_submission_async, + test_async_and_sync, +) +from tests.integration.reusable_values import ( + AMM_ASSET, + AMM_ASSET2, + AMM_ISSUER_WALLET, + WALLET, +) +from xrpl.models.amounts.issued_currency_amount import IssuedCurrencyAmount +from xrpl.models.requests.amm_info import AMMInfo +from xrpl.models.transactions.amm_withdraw import AMMWithdraw, AMMWithdrawFlag + +asset = AMM_ASSET +asset2 = AMM_ASSET2 +issuer_wallet = AMM_ISSUER_WALLET + + +class TestAMMWithdraw(IntegrationTestCase): + @test_async_and_sync(globals()) + async def test_single_asset(self, client): + pre_amm_info = await client.request( + AMMInfo( + asset=asset, + asset2=asset2, + ) + ) + + response = await sign_and_reliable_submission_async( + AMMWithdraw( + account=WALLET.classic_address, + asset=asset, + asset2=asset2, + amount="500", + flags=AMMWithdrawFlag.TF_SINGLE_ASSET, + ), + WALLET, + client, + ) + + self.assertTrue(response.is_successful()) + self.assertEqual(response.result["engine_result"], "tesSUCCESS") + + amm_info = await client.request( + AMMInfo( + asset=asset, + asset2=asset2, + ) + ) + + before_amount = float(pre_amm_info.result["amm"]["amount"]) + diff_amount = -500 + expected_amount = before_amount + diff_amount + + before_lptoken_value = float(pre_amm_info.result["amm"]["lp_token"]["value"]) + diff_lptoken_value = -61.2810042329 + expected_lptoken_value = before_lptoken_value + diff_lptoken_value + + self.assertEqual( + float(amm_info.result["amm"]["amount"]), + expected_amount, + ) + self.assertEqual( + amm_info.result["amm"]["amount2"], + pre_amm_info.result["amm"]["amount2"], + ) + self.assertLess( + float(amm_info.result["amm"]["lp_token"]["value"]), + before_lptoken_value, + ) + self.assertTrue( + compare_amm_values( + amm_info.result["amm"]["lp_token"]["value"], + expected_lptoken_value, + 6.206379545573327, + ) + ) + + @test_async_and_sync(globals()) + async def test_two_assets(self, client): + pre_amm_info = await client.request( + AMMInfo( + asset=asset, + asset2=asset2, + ) + ) + + response = await sign_and_reliable_submission_async( + AMMWithdraw( + account=WALLET.classic_address, + asset=asset, + asset2=asset2, + amount="50", + amount2=IssuedCurrencyAmount( + currency=asset2.currency, + issuer=asset2.issuer, + value="50", + ), + flags=AMMWithdrawFlag.TF_TWO_ASSET, + ), + WALLET, + client, + ) + + self.assertTrue(response.is_successful()) + self.assertEqual(response.result["engine_result"], "tesSUCCESS") + + amm_info = await client.request( + AMMInfo( + asset=asset, + asset2=asset2, + ) + ) + + before_amount = float(pre_amm_info.result["amm"]["amount"]) + diff_amount = -50 + expected_amount = before_amount + diff_amount + + before_amount2_value = float(pre_amm_info.result["amm"]["amount2"]["value"]) + diff_amount2_value = 3.8999367945 + expected_amount2_value = before_amount2_value + diff_amount2_value + + before_lptoken_value = float(pre_amm_info.result["amm"]["lp_token"]["value"]) + diff_lptoken_value = -13.805478843361811 + expected_lptoken_value = before_lptoken_value + diff_lptoken_value + + self.assertEqual( + float(amm_info.result["amm"]["amount"]), + expected_amount, + ) + self.assertLess( + float(amm_info.result["amm"]["amount2"]["value"]), + before_amount2_value, + ) + self.assertTrue( + compare_amm_values( + amm_info.result["amm"]["amount2"]["value"], + expected_amount2_value, + 7.799873589023093, + ) + ) + self.assertLess( + float(amm_info.result["amm"]["lp_token"]["value"]), + before_lptoken_value, + ) + self.assertTrue( + compare_amm_values( + float(amm_info.result["amm"]["lp_token"]["value"]), + expected_lptoken_value, + 0.24811570097392632, + ) + ) + + @test_async_and_sync(globals()) + async def test_one_asset_with_lptoken(self, client): + pre_amm_info = await client.request( + AMMInfo( + asset=asset, + asset2=asset2, + ) + ) + + lp_token = pre_amm_info.result["amm"]["lp_token"] + + response = await sign_and_reliable_submission_async( + AMMWithdraw( + account=WALLET.classic_address, + asset=asset, + asset2=asset2, + amount="5", + lp_token_in=IssuedCurrencyAmount( + currency=lp_token["currency"], + issuer=lp_token["issuer"], + value="5", + ), + flags=AMMWithdrawFlag.TF_ONE_ASSET_LP_TOKEN, + ), + WALLET, + client, + ) + + self.assertTrue(response.is_successful()) + self.assertEqual(response.result["engine_result"], "tesSUCCESS") + + amm_info = await client.request( + AMMInfo( + asset=asset, + asset2=asset2, + ) + ) + + diff_amount = -45 + expected_amount = float(pre_amm_info.result["amm"]["amount"]) + diff_amount + diff_lptoken = -5 + expected_lptoken = ( + float(pre_amm_info.result["amm"]["lp_token"]["value"]) + diff_lptoken + ) + + self.assertLess( + float(amm_info.result["amm"]["amount"]), + float(pre_amm_info.result["amm"]["amount"]), + ) + self.assertTrue( + compare_amm_values( + float(amm_info.result["amm"]["amount"]), + expected_amount, + 1, + ) + ) + self.assertEqual( + amm_info.result["amm"]["amount2"], + pre_amm_info.result["amm"]["amount2"], + ) + self.assertEqual( + float(amm_info.result["amm"]["lp_token"]["value"]), + expected_lptoken, + ) + + @test_async_and_sync(globals()) + async def test_lptoken(self, client): + pre_amm_info = await client.request( + AMMInfo( + asset=asset, + asset2=asset2, + ) + ) + + lp_token = pre_amm_info.result["amm"]["lp_token"] + + response = await sign_and_reliable_submission_async( + AMMWithdraw( + account=WALLET.classic_address, + asset=asset, + asset2=asset2, + lp_token_in=IssuedCurrencyAmount( + currency=lp_token["currency"], + issuer=lp_token["issuer"], + value="5", + ), + flags=AMMWithdrawFlag.TF_LP_TOKEN, + ), + WALLET, + client, + ) + + self.assertTrue(response.is_successful()) + self.assertEqual(response.result["engine_result"], "tesSUCCESS") + + amm_info = await client.request( + AMMInfo( + asset=asset, + asset2=asset2, + ) + ) + + before_amount = float(pre_amm_info.result["amm"]["amount"]) + diff_amount = -23 + expected_amount = before_amount + diff_amount + + before_amount2_value = float(pre_amm_info.result["amm"]["amount2"]["value"]) + diff_amount2_value = -1.1091277317 + expected_amount2_value = before_amount2_value + diff_amount2_value + + before_lptoken_value = float(pre_amm_info.result["amm"]["lp_token"]["value"]) + diff_lptoken_value = -5 + expected_lptoken_value = before_lptoken_value + diff_lptoken_value + + self.assertEqual( + float(amm_info.result["amm"]["amount"]), + expected_amount, + ) + self.assertLess( + float(amm_info.result["amm"]["amount2"]["value"]), + float(pre_amm_info.result["amm"]["amount2"]["value"]), + ) + self.assertTrue( + compare_amm_values( + float(amm_info.result["amm"]["amount2"]["value"]), + expected_amount2_value, + 0.012903907753866406, + ) + ) + self.assertEqual( + float(amm_info.result["amm"]["lp_token"]["value"]), + expected_lptoken_value, + ) From 132a94ad569eee335ea8f10e8aad019f94f8291c Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Fri, 3 Nov 2023 16:19:47 -0400 Subject: [PATCH 2/2] feat: add support for `server_definitions` RPC (#659) --- .github/workflows/integration_test.yml | 2 +- .../reqs/test_server_definitions.py | 10 ++++++++ xrpl/models/requests/__init__.py | 2 ++ xrpl/models/requests/request.py | 1 + xrpl/models/requests/server_definitions.py | 24 +++++++++++++++++++ 5 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 tests/integration/reqs/test_server_definitions.py create mode 100644 xrpl/models/requests/server_definitions.py diff --git a/.github/workflows/integration_test.yml b/.github/workflows/integration_test.yml index 955d1175b..75184131b 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-b3 + RIPPLED_DOCKER_IMAGE: rippleci/rippled:2.0.0-b4 on: push: diff --git a/tests/integration/reqs/test_server_definitions.py b/tests/integration/reqs/test_server_definitions.py new file mode 100644 index 000000000..c83049cfc --- /dev/null +++ b/tests/integration/reqs/test_server_definitions.py @@ -0,0 +1,10 @@ +from tests.integration.integration_test_case import IntegrationTestCase +from tests.integration.it_utils import test_async_and_sync +from xrpl.models.requests import ServerDefinitions + + +class TestServerDefinitions(IntegrationTestCase): + @test_async_and_sync(globals()) + async def test_basic_functionality(self, client): + response = await client.request(ServerDefinitions()) + self.assertTrue(response.is_successful()) diff --git a/xrpl/models/requests/__init__.py b/xrpl/models/requests/__init__.py index 111dbf23c..32b666a77 100644 --- a/xrpl/models/requests/__init__.py +++ b/xrpl/models/requests/__init__.py @@ -33,6 +33,7 @@ from xrpl.models.requests.random import Random from xrpl.models.requests.request import Request from xrpl.models.requests.ripple_path_find import RipplePathFind +from xrpl.models.requests.server_definitions import ServerDefinitions from xrpl.models.requests.server_info import ServerInfo from xrpl.models.requests.server_state import ServerState from xrpl.models.requests.sign import Sign @@ -85,6 +86,7 @@ "Random", "Request", "RipplePathFind", + "ServerDefinitions", "ServerInfo", "ServerState", "Sign", diff --git a/xrpl/models/requests/request.py b/xrpl/models/requests/request.py index a91c4e2b8..07da47849 100644 --- a/xrpl/models/requests/request.py +++ b/xrpl/models/requests/request.py @@ -68,6 +68,7 @@ class RequestMethod(str, Enum): # server info methods FEE = "fee" MANIFEST = "manifest" + SERVER_DEFINITIONS = "server_definitions" SERVER_INFO = "server_info" SERVER_STATE = "server_state" diff --git a/xrpl/models/requests/server_definitions.py b/xrpl/models/requests/server_definitions.py new file mode 100644 index 000000000..295706b6b --- /dev/null +++ b/xrpl/models/requests/server_definitions.py @@ -0,0 +1,24 @@ +""" +The server_info command asks the server for a +human-readable version of various information +about the rippled server being queried. +""" +from dataclasses import dataclass, field +from typing import Optional + +from xrpl.models.requests.request import Request, RequestMethod +from xrpl.models.utils import require_kwargs_on_init + + +@require_kwargs_on_init +@dataclass(frozen=True) +class ServerDefinitions(Request): + """ + The definitions command asks the server for a + human-readable version of various information + about the rippled server being queried. + """ + + method: RequestMethod = field(default=RequestMethod.SERVER_DEFINITIONS, init=False) + + hash: Optional[str] = None