Skip to content

Commit

Permalink
New Client methods GetTx and WaitTx (#25)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
Tkd-Alex and ctrl-Felix committed Feb 13, 2024
1 parent acff3aa commit b66cbdd
Show file tree
Hide file tree
Showing 6 changed files with 184 additions and 29 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "[email protected]" },
Expand Down
48 changes: 48 additions & 0 deletions src/mospy/clients/GRPCClient.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import base64
import time
import importlib

import grpc
from google.protobuf.json_format import MessageToDict
from mospy.Account import Account
from mospy.Transaction import Transaction

from mospy.exceptions.clients import TransactionNotFound, TransactionTimeout


class GRPCClient:
"""
Expand Down Expand Up @@ -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,
Expand Down
78 changes: 75 additions & 3 deletions src/mospy/clients/HTTPClient.py
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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:
Expand All @@ -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.
Expand Down Expand Up @@ -116,4 +138,54 @@ def estimate_gas(self,
if update:
transaction.set_gas(int(gas_used * multiplier))

return gas_used
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.")
12 changes: 12 additions & 0 deletions src/mospy/exceptions/clients.py
Original file line number Diff line number Diff line change
@@ -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
37 changes: 26 additions & 11 deletions tests/clients/test_grpcclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
}




36 changes: 22 additions & 14 deletions tests/clients/test_httpclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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,
Expand All @@ -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"
}
]
}

0 comments on commit b66cbdd

Please sign in to comment.