Skip to content

Commit

Permalink
feat: Support fetching faucet transaction status
Browse files Browse the repository at this point in the history
  • Loading branch information
alex-stone committed Oct 29, 2024
1 parent 0edce16 commit 0c44549
Show file tree
Hide file tree
Showing 8 changed files with 174 additions and 13 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

- Migrate faucet transactions to use `wait` syntax.

## [0.0.9] - 2024-10-29

### Fixed
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
```

Expand Down Expand Up @@ -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()
Expand Down
5 changes: 4 additions & 1 deletion cdp/address.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
51 changes: 51 additions & 0 deletions cdp/faucet_transaction.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import time

from cdp.cdp import Cdp
from cdp.client.models.faucet_transaction import (
FaucetTransaction as FaucetTransactionModel,
)
Expand Down Expand Up @@ -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."""
Expand Down
8 changes: 7 additions & 1 deletion tests/factories/faucet_transaction_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion tests/factories/transaction_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
18 changes: 12 additions & 6 deletions tests/test_address.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)


Expand Down
95 changes: 91 additions & 4 deletions tests/test_faucet_transaction.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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

0 comments on commit 0c44549

Please sign in to comment.