Skip to content

Commit

Permalink
Changes pertaining to rippled API v2 -- xrpl-py v3 (#729)
Browse files Browse the repository at this point in the history
feat: add rippled API v2 support and use as default (#720)

---------

Co-authored-by: Zhiyuan Wang <[email protected]>
Co-authored-by: Kassaking <[email protected]>
Co-authored-by: Mayukha Vadari <[email protected]>
Co-authored-by: Jackson Mills <[email protected]>
Co-authored-by: Omar Khan <[email protected]>
Co-authored-by: Mayukha Vadari <[email protected]>
  • Loading branch information
7 people authored Jul 16, 2024
1 parent 6d87379 commit cf99d19
Show file tree
Hide file tree
Showing 14 changed files with 144 additions and 48 deletions.
4 changes: 4 additions & 0 deletions .ci-config/rippled.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ validators.txt
# Note: The version of rippled you use this config with must have an implementation for the amendments you attempt to enable or it will crash.
# If you need the version of rippled to be more up to date, you may need to make a comment on this repo: https://github.com/WietseWind/docker-rippled

# network_id is required otherwise it's read as None
[network_id]
63456

[features]
# Devnet amendments as of June 28th, 2023
NegativeUNL
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [[Unreleased]]

### BREAKING CHANGE
- Use rippled API v2 as default in requests

### Added
- Support for the DeliverMax field in Payment transactions
- Support for the `feature` RPC
Expand Down
10 changes: 5 additions & 5 deletions snippets/multisign.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Example of how we can multisign a transaction"""

from xrpl.clients import JsonRpcClient
from xrpl.models import AccountSet, SignerEntry, SignerListSet
from xrpl.transaction import autofill, multisign, sign, submit_and_wait
Expand Down Expand Up @@ -57,11 +58,10 @@
if multisigned_tx_response.result["validated"]:
print("The multisigned transaction was accepted by the ledger:")
print(multisigned_tx_response)
if multisigned_tx_response.result["Signers"]:
print(
"The transaction had "
f"{len(multisigned_tx_response.result['Signers'])} signatures"
)
signers_in_response = multisigned_tx_response.result["tx_json"].get("Signers")

if signers_in_response:
print("The transaction had " f"{len(signers_in_response)} signatures")
else:
print(
"The multisigned transaction was rejected by rippled."
Expand Down
3 changes: 2 additions & 1 deletion snippets/send_escrow.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Example of how we can set up an escrow"""

from datetime import datetime
from time import sleep

Expand Down Expand Up @@ -55,7 +56,7 @@
finish_tx = EscrowFinish(
account=wallet1.address,
owner=wallet1.address,
offer_sequence=create_escrow_response.result["Sequence"],
offer_sequence=create_escrow_response.result["tx_json"]["Sequence"],
)

submit_and_wait(finish_tx, client, wallet1)
Expand Down
42 changes: 42 additions & 0 deletions tests/integration/reqs/test_account_tx.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,45 @@ async def test_basic_functionality(self, client):
)
)
self.assertTrue(response.is_successful())

@test_async_and_sync(globals())
async def test_api_v2(self, client):
response = await client.request(
AccountTx(account=WALLET.address, api_version=2)
)

# use the below proxies to ensure that the correct API response is returned
# API v2 returns tx_json field
self.assertIn("tx_json", response.result["transactions"][0])

# API v2 does not return a tx field
self.assertNotIn("tx", response.result["transactions"][0])
self.assertTrue(response.is_successful())

@test_async_and_sync(globals())
async def test_api_v1(self, client):
response = await client.request(
AccountTx(account=WALLET.address, api_version=1)
)

# use the below proxies to ensure that the correct API response is returned
# API v1 does not contain a tx_json field
self.assertNotIn("tx_json", response.result["transactions"][0])

# API v1 returns a tx field
self.assertIn("tx", response.result["transactions"][0])
self.assertTrue(response.is_successful())

@test_async_and_sync(globals())
async def test_no_explicit_api_version(self, client):
response_without_version = await client.request(
AccountTx(account=WALLET.address)
)

response_with_version = await client.request(
AccountTx(account=WALLET.address, api_version=2)
)

# if api_version is not explicitly specified, xrpl-py inserts api_version:2
# inside the Requests
self.assertEqual(response_with_version.result, response_without_version.result)
24 changes: 16 additions & 8 deletions tests/integration/reqs/test_generic_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,11 @@


class TestGenericRequest(IntegrationTestCase):
# Note: Support for the tx_history command has been removed since rippled API v2
@test_async_and_sync(globals())
async def test_constructor(self, client):
response = await client.request(
GenericRequest(
method="tx_history",
start=0,
)
GenericRequest(method="tx_history", start=0, api_version=1)
)
self.assertTrue(response.is_successful())

Expand All @@ -23,6 +21,7 @@ async def test_json_formatting(self, client):
"params": {
"start": 0,
},
"api_version": 1,
}
)
)
Expand All @@ -32,10 +31,19 @@ async def test_json_formatting(self, client):
async def test_websocket_formatting(self, client):
response = await client.request(
GenericRequest.from_dict(
{
"command": "tx_history",
"start": 0,
}
{"command": "tx_history", "start": 0, "api_version": 1}
)
)
self.assertTrue(response.is_successful())

def test_from_dict_json_without_api_version_input(self):
with self.assertRaises(KeyError):
# tx_history is invalid in the default API version 2
GenericRequest.from_dict(
{
"method": "tx_history",
"params": {
"start": 0,
},
}
)
4 changes: 2 additions & 2 deletions tests/integration/sugar/test_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ async def test_get_latest_transaction(self, client):

response = await get_latest_transaction(WALLET.address, client)
self.assertEqual(len(response.result["transactions"]), 1)
transaction = response.result["transactions"][0]["tx"]
transaction = response.result["transactions"][0]["tx_json"]
self.assertEqual(transaction["TransactionType"], "Payment")
self.assertEqual(transaction["Amount"], amount)
self.assertEqual(transaction["DeliverMax"], amount)
self.assertEqual(transaction["Account"], WALLET.address)
24 changes: 0 additions & 24 deletions tests/integration/sugar/test_network_id.py

This file was deleted.

43 changes: 38 additions & 5 deletions tests/integration/sugar/test_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ async def test_none_as_destination_tag(self, client):
)

# AND we expect the result Account to be the same as the original payment Acct
self.assertEqual(payment.result["Account"], ACCOUNT)
self.assertEqual(payment.result["tx_json"]["Account"], ACCOUNT)
# AND we expect the response to be successful (200)
self.assertTrue(payment.is_successful())

Expand Down Expand Up @@ -216,6 +216,39 @@ async def test_calculate_payment_fee(self, client):
expected_fee = await get_fee(client)
self.assertEqual(payment_autofilled.fee, expected_fee)

@test_async_and_sync(
globals(),
["xrpl.transaction.autofill"],
)
async def test_networkid_non_reserved_networks(self, client):
tx = AccountSet(
account="rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
fee=FEE,
domain="www.example.com",
)
transaction = await autofill(tx, client)

# Autofill should populate the tx networkID and build_version from 1.11.0 or
# later. NetworkID field is populated only for networks where network_id > 1024
self.assertEqual(client.network_id, 63456)
self.assertEqual(transaction.network_id, 63456)

@test_async_and_sync(globals(), ["xrpl.transaction.autofill"], use_testnet=True)
async def test_networkid_reserved_networks(self, client):
tx = AccountSet(
account="rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
fee=FEE,
domain="www.example.com",
)
# The network_id is less than 1024 for the testnet.
# Hence network_id field is not set
transaction = await autofill(tx, client)

# Although the client network_id property is set,
# the corresponding field in transaction is not populated
self.assertIsNone(transaction.network_id)
self.assertEqual(client.network_id, 1)


class TestSubmitAndWait(IntegrationTestCase):
@test_async_and_sync(
Expand All @@ -235,7 +268,7 @@ async def test_submit_and_wait_simple(self, client):
self.assertTrue(response.result["validated"])
self.assertEqual(response.result["meta"]["TransactionResult"], "tesSUCCESS")
self.assertTrue(response.is_successful())
self.assertEqual(response.result["Fee"], await get_fee(client))
self.assertEqual(response.result["tx_json"]["Fee"], await get_fee(client))

@test_async_and_sync(
globals(),
Expand All @@ -255,7 +288,7 @@ async def test_submit_and_wait_payment(self, client):
self.assertTrue(response.result["validated"])
self.assertEqual(response.result["meta"]["TransactionResult"], "tesSUCCESS")
self.assertTrue(response.is_successful())
self.assertEqual(response.result["Fee"], await get_fee(client))
self.assertEqual(response.result["tx_json"]["Fee"], await get_fee(client))

@test_async_and_sync(
globals(),
Expand All @@ -279,7 +312,7 @@ async def test_submit_and_wait_signed(self, client):
self.assertTrue(response.result["validated"])
self.assertEqual(response.result["meta"]["TransactionResult"], "tesSUCCESS")
self.assertTrue(response.is_successful())
self.assertEqual(response.result["Fee"], await get_fee(client))
self.assertEqual(response.result["tx_json"]["Fee"], await get_fee(client))

@test_async_and_sync(
globals(),
Expand All @@ -304,7 +337,7 @@ async def test_submit_and_wait_blob(self, client):
self.assertTrue(response.result["validated"])
self.assertEqual(response.result["meta"]["TransactionResult"], "tesSUCCESS")
self.assertTrue(response.is_successful())
self.assertEqual(response.result["Fee"], await get_fee(client))
self.assertEqual(response.result["tx_json"]["Fee"], await get_fee(client))

@test_async_and_sync(
globals(),
Expand Down
9 changes: 9 additions & 0 deletions tests/unit/models/requests/test_amm_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,21 @@

from xrpl.models.currencies import XRP, IssuedCurrency
from xrpl.models.requests import AMMInfo
from xrpl.models.requests.request import _DEFAULT_API_VERSION

_ASSET = XRP()
_ASSET_2 = IssuedCurrency(currency="USD", issuer="rN6zcSynkRnf8zcgTVrRL8K7r4ovE7J4Zj")


class TestAMMInfo(TestCase):
def test_populate_api_version_field(self):
request = AMMInfo(
asset=_ASSET,
asset2=_ASSET_2,
)
self.assertEqual(request.api_version, _DEFAULT_API_VERSION)
self.assertTrue(request.is_valid())

def test_asset_asset2(self):
request = AMMInfo(
asset=_ASSET,
Expand Down
5 changes: 4 additions & 1 deletion tests/unit/models/requests/test_requests.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from unittest import TestCase

from xrpl.models.requests import Fee, GenericRequest
from xrpl.models.requests.request import _DEFAULT_API_VERSION


class TestRequest(TestCase):
Expand All @@ -12,4 +13,6 @@ def test_to_dict_includes_method_as_string(self):
def test_generic_request_to_dict_sets_command_as_method(self):
command = "validator_list_sites"
tx = GenericRequest(command=command).to_dict()
self.assertDictEqual(tx, {"method": command})
self.assertDictEqual(
tx, {"method": command, "api_version": _DEFAULT_API_VERSION}
)
4 changes: 4 additions & 0 deletions tests/unit/models/test_base_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
SubmitMultisigned,
SubmitOnly,
)
from xrpl.models.requests.request import _DEFAULT_API_VERSION
from xrpl.models.transactions import (
AMMBid,
AuthAccount,
Expand Down Expand Up @@ -117,6 +118,7 @@ def test_from_dict_recursive_currency(self):
**book_offers_dict,
"method": "book_offers",
"taker_gets": {"currency": "XRP"},
"api_version": _DEFAULT_API_VERSION,
}
self.assertEqual(expected_dict, book_offers.to_dict())

Expand All @@ -132,6 +134,7 @@ def test_from_dict_recursive_transaction(self):
"fee_mult_max": 10,
"fee_div_max": 1,
"offline": False,
"api_version": _DEFAULT_API_VERSION,
}
del expected_dict["transaction"]
self.assertEqual(expected_dict, sign.to_dict())
Expand All @@ -148,6 +151,7 @@ def test_from_dict_recursive_transaction_tx_json(self):
"fee_mult_max": 10,
"fee_div_max": 1,
"offline": False,
"api_version": _DEFAULT_API_VERSION,
}
self.assertEqual(expected_dict, sign.to_dict())

Expand Down
6 changes: 5 additions & 1 deletion xrpl/models/requests/generic_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,11 @@ def from_dict(cls: Type[Self], value: Dict[str, Any]) -> Self:

elif "method" in value: # JSON RPC formatting
if "params" in value: # actual JSON RPC formatting
value = {"method": value["method"], **value["params"]}
value = {
"api_version": value["api_version"],
"method": value["method"],
**value["params"],
}
# else is the internal request formatting

else:
Expand Down
11 changes: 10 additions & 1 deletion xrpl/models/requests/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@
from enum import Enum
from typing import Any, Dict, Optional, Type, Union, cast

from typing_extensions import Self
from typing_extensions import Final, Self

import xrpl.models.requests # bare import to get around circular dependency
from xrpl.models.base_model import BaseModel
from xrpl.models.exceptions import XRPLModelException
from xrpl.models.required import REQUIRED
from xrpl.models.utils import KW_ONLY_DATACLASS, require_kwargs_on_init

_DEFAULT_API_VERSION: Final[int] = 2


class RequestMethod(str, Enum):
"""Represents the different options for the ``method`` field in a request."""
Expand Down Expand Up @@ -108,6 +110,13 @@ class Request(BaseModel):

id: Optional[Union[str, int]] = None

api_version: int = _DEFAULT_API_VERSION
"""
The API version to use for the said Request. By default, api_version: 2 is used.
Docs:
https://xrpl.org/docs/references/http-websocket-apis/api-conventions/request-formatting/#api-versioning
"""

@classmethod
def from_dict(cls: Type[Self], value: Dict[str, Any]) -> Self:
"""
Expand Down

0 comments on commit cf99d19

Please sign in to comment.