From b66cbdd09eaeb9098f73ee6e9e290ca1b12c0359 Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Tue, 13 Feb 2024 18:19:52 +0100 Subject: [PATCH] New Client methods GetTx and WaitTx (#25) * get_tx method for GRPCClient * wait_tx method for GRPCClient * Stash commits * feat: wait for tx and get tx for HTTPClient --------- Co-authored-by: Felix <62290842+ctrl-Felix@users.noreply.github.com> --- pyproject.toml | 2 +- src/mospy/clients/GRPCClient.py | 48 ++++++++++++++++++++ src/mospy/clients/HTTPClient.py | 78 ++++++++++++++++++++++++++++++-- src/mospy/exceptions/clients.py | 12 +++++ tests/clients/test_grpcclient.py | 37 ++++++++++----- tests/clients/test_httpclient.py | 36 +++++++++------ 6 files changed, 184 insertions(+), 29 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b6950f0..022fa8f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "mospy-wallet" -version = "0.5.2" +version = "0.5.3" description = "This package is a fork of cosmospy and is a light framework for the cosmos ecosystem" authors = [ { name = "ctrl-felix", email = "dev@ctrl-felix.de" }, diff --git a/src/mospy/clients/GRPCClient.py b/src/mospy/clients/GRPCClient.py index c6733cc..b55e66c 100644 --- a/src/mospy/clients/GRPCClient.py +++ b/src/mospy/clients/GRPCClient.py @@ -1,4 +1,5 @@ import base64 +import time import importlib import grpc @@ -6,6 +7,8 @@ from mospy.Account import Account from mospy.Transaction import Transaction +from mospy.exceptions.clients import TransactionNotFound, TransactionTimeout + class GRPCClient: """ @@ -131,6 +134,51 @@ def broadcast_transaction(self, return {"hash": hash, "code": code, "log": log} + def get_tx(self, *, tx_hash: str): + """ + Query a transaction by passing the hash + + Note: + Takes only positional arguments. + + Args: + tx_hash (Transaction): The transaction hash + + Returns: + transaction (dict): Transaction dict as returned by the chain + + + """ + con = self._connect() + tx_stub = self._cosmos_tx_service_pb2_grpc.ServiceStub(con) + try: + return MessageToDict(tx_stub.GetTx(self._cosmos_tx_service_pb2.GetTxRequest(hash=tx_hash))) + except grpc.RpcError: + raise TransactionNotFound(f"The transaction {tx_hash} couldn't be found on chain.") + + def wait_for_tx(self, *, tx_hash: str, timeout: float = 60, poll_period: float = 10): + """ + Waits for a transaction hash to hit the chain. + + Note: + Takes only positional arguments + + Args: + tx_hash (Transaction): The transaction hash + timeout (bool): Time to wait before throwing a TransactionTimeout. Defaults to 60 + poll_period (float): Time to wait between each check. Defaults to 10 + + Returns: + transaction (dict): Transaction dict as returned by the chain + """ + start = time.time() + while time.time() < (start + timeout): + try: + return self.get_tx(tx_hash=tx_hash) + except TransactionNotFound: + time.sleep(poll_period) + + raise TransactionTimeout(f"The transaction {tx_hash} couldn't be found on chain within {timeout} seconds.") def estimate_gas(self, *, transaction: Transaction, diff --git a/src/mospy/clients/HTTPClient.py b/src/mospy/clients/HTTPClient.py index 84e3606..4fc69a4 100644 --- a/src/mospy/clients/HTTPClient.py +++ b/src/mospy/clients/HTTPClient.py @@ -1,10 +1,12 @@ import copy +import time import httpx from mospy.Account import Account from mospy.Transaction import Transaction from mospy.exceptions.clients import NodeException +from mospy.exceptions.clients import NodeTimeoutException, TransactionNotFound, TransactionTimeout class HTTPClient: @@ -15,11 +17,14 @@ class HTTPClient: api (str): URL to a Api node """ - def __init__(self, *, api: str = "https://api.cosmos.interbloc.org"): + def __init__(self, *, api: str = "https://rest.cosmos.directory/cosmoshub"): self._api = api def _make_post_request(self, path, payload, timeout): - req = httpx.post(self._api + path, json=payload, timeout=timeout) + try: + req = httpx.post(self._api + path, json=payload, timeout=timeout) + except httpx.TimeoutException: + raise NodeTimeoutException(f"Node {self._api} timed out after {timeout} seconds") if req.status_code != 200: try: @@ -32,6 +37,23 @@ def _make_post_request(self, path, payload, timeout): data = req.json() return data + def _make_get_request(self, path, timeout): + try: + req = httpx.get(self._api + path, timeout=timeout) + except httpx.TimeoutException: + raise NodeTimeoutException(f"Node {self._api} timed out after {timeout} seconds") + if req.status_code != 200: + try: + data = req.json() + message = f"({data['message']}" + except: + message = "" + raise NodeException(f"Error while doing request to api endpoint {message}") + + data = req.json() + return data + + def load_account_data(self, account: Account): """ Load the ``next_sequence`` and ``account_number`` into the account object. @@ -116,4 +138,54 @@ def estimate_gas(self, if update: transaction.set_gas(int(gas_used * multiplier)) - return gas_used \ No newline at end of file + return gas_used + + def get_tx(self, *, tx_hash: str, timeout: int = 5): + """ + Query a transaction by passing the hash + + Note: + Takes only positional arguments. + + Args: + tx_hash (Transaction): The transaction hash + timeout (int): Timeout for the request before throwing a NodeException + + Returns: + transaction (dict): Transaction dict as returned by the chain + + + """ + path = "/cosmos/tx/v1beta1/txs/" + tx_hash + + try: + data = self._make_get_request(path=path, timeout=timeout) + except NodeException: + raise TransactionNotFound(f"The transaction {tx_hash} couldn't be found") + + return data + + + def wait_for_tx(self, *, tx_hash: str, timeout: float = 60, poll_period: float = 10): + """ + Waits for a transaction hash to hit the chain. + + Note: + Takes only positional arguments + + Args: + tx_hash (Transaction): The transaction hash + timeout (bool): Time to wait before throwing a TransactionTimeout. Defaults to 60 + poll_period (float): Time to wait between each check. Defaults to 10 + + Returns: + transaction (dict): Transaction dict as returned by the chain + """ + start = time.time() + while time.time() < (start + timeout): + try: + return self.get_tx(tx_hash=tx_hash) + except TransactionNotFound: + time.sleep(poll_period) + + raise TransactionTimeout(f"The transaction {tx_hash} couldn't be found on chain within {timeout} seconds.") \ No newline at end of file diff --git a/src/mospy/exceptions/clients.py b/src/mospy/exceptions/clients.py index 467565e..644e32b 100644 --- a/src/mospy/exceptions/clients.py +++ b/src/mospy/exceptions/clients.py @@ -1,3 +1,15 @@ class NodeException(Exception): """Raised when a node returns an error to a request.""" + pass + +class NodeTimeoutException(Exception): + """Raised when a request to a node times out.""" + pass + +class TransactionTimeout(Exception): + """Raised when the transaction didn't hit the chain within the provided timeout.""" + pass + +class TransactionNotFound(Exception): + """Raised when the transaction couldn't be found on chain.""" pass \ No newline at end of file diff --git a/tests/clients/test_grpcclient.py b/tests/clients/test_grpcclient.py index aef103f..7b392aa 100644 --- a/tests/clients/test_grpcclient.py +++ b/tests/clients/test_grpcclient.py @@ -31,34 +31,49 @@ def test_transaction_submitting(self): port=443, ssl=True) - fee = Coin(denom="uatom", amount="1000") + client.load_account_data(account=account) + + fee = Coin(denom="uatom", amount="1500") tx = Transaction( account=account, fee=fee, gas=10000000000, + memo="This is a mospy test transaction" ) tx.add_msg( tx_type="transfer", sender=account, - receipient="cosmos1tkv9rquxr88r7snrg42kxdj9gsnfxxg028kuh9", + receipient=account.address, amount=1000, denom="uatom", ) - tx_data = client.broadcast_transaction(transaction=tx) + expected_gas = client.estimate_gas(transaction=tx) - assert ( - tx_data["hash"] == - "54B845AEB1523803D4EAF2330AE5759A83458CB5F0211159D04CC257428503C4") + assert expected_gas > 0 + tx_data = client.broadcast_transaction(transaction=tx) - client.load_account_data(account=account) + assert tx_data["code"] == 0 - gas_used = client.estimate_gas( - transaction=tx, - update=False, + transaction_dict = client.wait_for_tx( + tx_hash=tx_data["hash"] ) - assert gas_used > 0 + assert "tx" in transaction_dict and transaction_dict["tx"]["body"]["messages"][0] == { + "@type": "/cosmos.bank.v1beta1.MsgSend", + "fromAddress": account.address, + "toAddress": account.address, + "amount": [ + { + "denom": "uatom", + "amount": "1000" + } + ] + } + + + + diff --git a/tests/clients/test_httpclient.py b/tests/clients/test_httpclient.py index bb1f851..fe97818 100644 --- a/tests/clients/test_httpclient.py +++ b/tests/clients/test_httpclient.py @@ -5,7 +5,7 @@ from mospy import Transaction from mospy.clients import HTTPClient -API = "https://rest-cosmoshub.ecostake.com" +API = "https://cosmos-rest.publicnode.com" class TestHTTPClientClass: seed_phrase = "law grab theory better athlete submit awkward hawk state wedding wave monkey audit blame fury wood tag rent furnace exotic jeans drift destroy style" @@ -29,8 +29,9 @@ def test_transaction_submitting(self): ) client = HTTPClient(api=API) + client.load_account_data(account=account) - fee = Coin(denom="uatom", amount="1000") + fee = Coin(denom="uatom", amount="1500") tx = Transaction( account=account, @@ -41,24 +42,31 @@ def test_transaction_submitting(self): tx.add_msg( tx_type="transfer", sender=account, - receipient="cosmos1tkv9rquxr88r7snrg42kxdj9gsnfxxg028kuh9", + receipient=account.address, amount=1000, denom="uatom", ) - copied_transaction = copy.copy(tx) - tx_data = client.broadcast_transaction(transaction=copied_transaction) - assert ( - tx_data["hash"] == - "54B845AEB1523803D4EAF2330AE5759A83458CB5F0211159D04CC257428503C4") + expected_gas = client.estimate_gas(transaction=tx) - client.load_account_data(account=account) + assert expected_gas > 0 - gas_used = client.estimate_gas( - transaction=tx, - update=False, - ) + tx_data = client.broadcast_transaction(transaction=tx) + assert tx_data["code"] == 0 - assert gas_used > 0 + transaction_dict = client.wait_for_tx( + tx_hash=tx_data["hash"] + ) + assert "tx" in transaction_dict and transaction_dict["tx"]["body"]["messages"][0] == { + "@type": "/cosmos.bank.v1beta1.MsgSend", + "from_address": account.address, + "to_address": account.address, + "amount": [ + { + "denom": "uatom", + "amount": "1000" + } + ] + }