From 0c44549eda0b772f04049f1e306d1e6e86ff1d3f Mon Sep 17 00:00:00 2001 From: alexstone Date: Tue, 29 Oct 2024 14:52:12 -0700 Subject: [PATCH] feat: Support fetching faucet transaction status --- CHANGELOG.md | 2 + README.md | 6 ++ cdp/address.py | 5 +- cdp/faucet_transaction.py | 51 ++++++++++ tests/factories/faucet_transaction_factory.py | 8 +- tests/factories/transaction_factory.py | 2 +- tests/test_address.py | 18 ++-- tests/test_faucet_transaction.py | 95 ++++++++++++++++++- 8 files changed, 174 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c7e0caa..1d36bbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Migrate faucet transactions to use `wait` syntax. + ## [0.0.9] - 2024-10-29 ### Fixed diff --git a/README.md b/README.md index 9fc8dae..f46bd48 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,9 @@ testnet ETH. You are allowed one faucet claim per 24-hour window. # Fund the wallet with a faucet transaction. faucet_tx = wallet1.faucet() +# Wait for the faucet transaction to complete. +faucet_tx.wait() + print(f"Faucet transaction successfully completed: {faucet_tx}") ``` @@ -135,6 +138,9 @@ print(f"Wallet successfully created: {wallet3}") # Fund the wallet with USDC with a faucet transaction. usdc_faucet_tx = wallet1.faucet("usdc") +# Wait for the faucet transaction to complete. +usdc_faucet_tx.wait() + print(f"Faucet transaction successfully completed: {usdc_faucet_tx}") transfer = wallet1.transfer(0.00001, "usdc", wallet3, gasless=True).wait() diff --git a/cdp/address.py b/cdp/address.py index da5a652..e8a24a9 100644 --- a/cdp/address.py +++ b/cdp/address.py @@ -65,7 +65,10 @@ def faucet(self, asset_id=None) -> FaucetTransaction: """ model = Cdp.api_clients.external_addresses.request_external_faucet_funds( - network_id=self.network_id, address_id=self.address_id, asset_id=asset_id + network_id=self.network_id, + address_id=self.address_id, + asset_id=asset_id, + skip_wait=True ) return FaucetTransaction(model) diff --git a/cdp/faucet_transaction.py b/cdp/faucet_transaction.py index 2ffdb0e..8c8a08d 100644 --- a/cdp/faucet_transaction.py +++ b/cdp/faucet_transaction.py @@ -1,3 +1,6 @@ +import time + +from cdp.cdp import Cdp from cdp.client.models.faucet_transaction import ( FaucetTransaction as FaucetTransactionModel, ) @@ -76,6 +79,54 @@ def status(self) -> str: """ return self.transaction.status + def wait(self, interval_seconds: float = 0.2, timeout_seconds: float = 20) -> "FaucetTransaction": + """Wait for the faucet transaction to complete. + + Args: + interval_seconds (float): The interval seconds. + timeout_seconds (float): The timeout seconds. + + Returns: + FaucetTransaction: The faucet transaction. + + """ + start_time = time.time() + + while not self.transaction.terminal_state: + self.reload() + + if time.time() - start_time > timeout_seconds: + raise TimeoutError("Timed out waiting for FaucetTransaction to land onchain") + + time.sleep(interval_seconds) + + return self + + + def reload(self) -> "FaucetTransaction": + """Reload the faucet transaction. + + Returns: + None + + """ + model = Cdp.api_clients.external_addresses.get_faucet_transaction( + self.network_id, + self.address_id, + self.transaction_hash + ) + self._model = model + + if model.transaction is None: + raise ValueError("Faucet transaction is required.") + + print("SUP DAWG") + print(type(model)) + + # Update the transaction + self._transaction = Transaction(model.transaction) + + return self def __str__(self) -> str: """Return a string representation of the FaucetTransaction.""" diff --git a/tests/factories/faucet_transaction_factory.py b/tests/factories/faucet_transaction_factory.py index b12948c..fcb2875 100644 --- a/tests/factories/faucet_transaction_factory.py +++ b/tests/factories/faucet_transaction_factory.py @@ -11,10 +11,16 @@ def faucet_transaction_model_factory(transaction_model_factory): def _create_faucet_tx_model(status="complete"): transaction_model = transaction_model_factory(status) + if transaction_model.transaction_hash is None: + raise ValueError("Faucet transaction must have a hash.") + + if transaction_model.transaction_link is None: + raise ValueError("Faucet transaction must have a link.") + return FaucetTransactionModel( transaction=transaction_model, transaction_hash=transaction_model.transaction_hash, - transaction_link=transaction_model.transaction_link + transaction_link=transaction_model.transaction_link, ) return _create_faucet_tx_model diff --git a/tests/factories/transaction_factory.py b/tests/factories/transaction_factory.py index 5fd9712..51ac3a5 100644 --- a/tests/factories/transaction_factory.py +++ b/tests/factories/transaction_factory.py @@ -20,7 +20,7 @@ def _create_transaction_model(status="complete"): else None, status=status, transaction_link="https://sepolia.basescan.org/tx/0xtransactionlink" - if status == "complete" + if status in ["broadcast", "complete"] else None, block_hash="0xblockhash" if status == "complete" else None, block_height="123456" if status == "complete" else None, diff --git a/tests/test_address.py b/tests/test_address.py index c605bd8..a6241f8 100644 --- a/tests/test_address.py +++ b/tests/test_address.py @@ -29,36 +29,42 @@ def test_address_can_sign(address_factory): @patch("cdp.Cdp.api_clients") -def test_address_faucet(mock_api_clients, address_factory): +def test_address_faucet(mock_api_clients, address_factory, faucet_transaction_model_factory): """Test the faucet method of an Address.""" address = address_factory() mock_request_faucet = Mock() - mock_request_faucet.return_value = Mock(spec=FaucetTransaction) + mock_request_faucet.return_value = faucet_transaction_model_factory() mock_api_clients.external_addresses.request_external_faucet_funds = mock_request_faucet faucet_tx = address.faucet() assert isinstance(faucet_tx, FaucetTransaction) mock_request_faucet.assert_called_once_with( - network_id=address.network_id, address_id=address.address_id, asset_id=None + network_id=address.network_id, + address_id=address.address_id, + asset_id=None, + skip_wait=True ) @patch("cdp.Cdp.api_clients") -def test_address_faucet_with_asset_id(mock_api_clients, address_factory): +def test_address_faucet_with_asset_id(mock_api_clients, address_factory, faucet_transaction_model_factory): """Test the faucet method of an Address with an asset_id.""" address = address_factory() mock_request_faucet = Mock() - mock_request_faucet.return_value = Mock(spec=FaucetTransaction) + mock_request_faucet.return_value = faucet_transaction_model_factory() mock_api_clients.external_addresses.request_external_faucet_funds = mock_request_faucet faucet_tx = address.faucet(asset_id="usdc") assert isinstance(faucet_tx, FaucetTransaction) mock_request_faucet.assert_called_once_with( - network_id=address.network_id, address_id=address.address_id, asset_id="usdc" + network_id=address.network_id, + address_id=address.address_id, + asset_id="usdc", + skip_wait=True ) diff --git a/tests/test_faucet_transaction.py b/tests/test_faucet_transaction.py index c61168f..341ccab 100644 --- a/tests/test_faucet_transaction.py +++ b/tests/test_faucet_transaction.py @@ -1,11 +1,8 @@ -from unittest.mock import Mock, patch +from unittest.mock import Mock, call, patch import pytest -from cdp.client.exceptions import ApiException -from cdp.errors import ApiError from cdp.faucet_transaction import FaucetTransaction -from cdp.transaction import Transaction def test_faucet_tx_initialization(faucet_transaction_factory): @@ -16,3 +13,93 @@ def test_faucet_tx_initialization(faucet_transaction_factory): assert faucet_transaction.transaction_hash == "0xtransactionhash" assert faucet_transaction.network_id == "base-sepolia" assert faucet_transaction.address_id == "0xdestination" + assert faucet_transaction.status.value == "complete" + +@patch("cdp.Cdp.api_clients") +def test_reload_faucet_tx(mock_api_clients, faucet_transaction_factory): + """Test the reloading of a FaucetTransaction object.""" + faucet_tx = faucet_transaction_factory(status="broadcast") + complete_faucet_tx = faucet_transaction_factory(status="complete") + + # Mock the GetFaucetTransaction API returning a complete faucet transaction. + mock_get_faucet_tx = Mock() + mock_get_faucet_tx.return_value = complete_faucet_tx._model + mock_api_clients.external_addresses.get_faucet_transaction = mock_get_faucet_tx + + reloaded_faucet_tx = faucet_tx.reload() + + mock_get_faucet_tx.assert_called_once_with( + "base-sepolia", + "0xdestination", + "0xtransactionhash" + ) + assert faucet_tx.status.value == "complete" + assert reloaded_faucet_tx.status.value == "complete" + + +@patch("cdp.Cdp.api_clients") +@patch("cdp.faucet_transaction.time.sleep") +@patch("cdp.faucet_transaction.time.time") +def test_wait_for_faucet_transaction( + mock_time, + mock_sleep, + mock_api_clients, + faucet_transaction_factory +): + """Test the waiting for a FaucetTransaction object to complete.""" + faucet_tx = faucet_transaction_factory(status="broadcast") + complete_faucet_tx = faucet_transaction_factory(status="complete") + + # Mock GetFaucetTransaction returning a `broadcast` and then a `complete` + # faucet transaction. + mock_get_faucet_tx = Mock() + mock_api_clients.external_addresses.get_faucet_transaction = mock_get_faucet_tx + mock_get_faucet_tx.side_effect = [faucet_tx._model, complete_faucet_tx._model] + + mock_time.side_effect = [0, 0.2, 0.4] + + result = faucet_tx.wait(interval_seconds=0.2, timeout_seconds=1) + + assert result.status.value == "complete" + + mock_get_faucet_tx.assert_called_with( + "base-sepolia", + "0xdestination", + "0xtransactionhash" + ) + assert mock_get_faucet_tx.call_count == 2 + mock_sleep.assert_has_calls([call(0.2)] * 2) + assert mock_time.call_count == 3 + + +@patch("cdp.Cdp.api_clients") +@patch("cdp.faucet_transaction.time.sleep") +@patch("cdp.faucet_transaction.time.time") +def test_wait_for_faucet_transaction_timeout( + mock_time, + mock_sleep, + mock_api_clients, + faucet_transaction_factory +): + """Test the waiting for a FaucetTransaction object to complete with a timeout.""" + faucet_tx = faucet_transaction_factory(status="broadcast") + + mock_get_faucet_tx = Mock() + mock_get_faucet_tx.return_value = faucet_tx._model + mock_api_clients.external_addresses.get_faucet_transaction = mock_get_faucet_tx + + mock_time.side_effect = [0, 0.5, 1.0, 1.5, 2.0, 2.5] + + with pytest.raises(TimeoutError, match="Timed out waiting for FaucetTransaction to land onchain"): + faucet_tx.wait(interval_seconds=0.5, timeout_seconds=2) + + mock_get_faucet_tx.assert_called_with( + "base-sepolia", + "0xdestination", + "0xtransactionhash" + ) + + assert mock_get_faucet_tx.call_count == 5 + mock_sleep.assert_has_calls([call(0.5)] * 4) + assert mock_time.call_count == 6 +