diff --git a/CHANGELOG.md b/CHANGELOG.md index b9905b0..ab92b77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # CDP Python SDK Changelog ## Unreleased +- Add support for fetching address reputation + - Add `reputation` method to `Address` to fetch the reputation of the address. ### Added diff --git a/cdp/address.py b/cdp/address.py index abe7002..c3e848f 100644 --- a/cdp/address.py +++ b/cdp/address.py @@ -1,6 +1,7 @@ from collections.abc import Iterator from decimal import Decimal +from cdp.address_reputation import AddressReputation from cdp.asset import Asset from cdp.balance import Balance from cdp.balance_map import BalanceMap @@ -23,6 +24,7 @@ def __init__(self, network_id: str, address_id: str) -> None: """ self._network_id = network_id self._id = address_id + self._reputation: AddressReputation | None = None @property def address_id(self) -> str: @@ -133,6 +135,22 @@ def transactions(self) -> Iterator[Transaction]: """ return Transaction.list(network_id=self.network_id, address_id=self.address_id) + def reputation(self) -> AddressReputation: + """Get the reputation of the address. + + Returns: + AddressReputation: The reputation of the address. + + """ + if self._reputation is not None: + return self._reputation + + response = Cdp.api_clients.reputation.get_address_reputation( + network_id=self.network_id, address_id=self.address_id + ) + self._reputation = AddressReputation(response) + return self._reputation + def __str__(self) -> str: """Return a string representation of the Address.""" return f"Address: (address_id: {self.address_id}, network_id: {self.network_id})" diff --git a/cdp/address_reputation.py b/cdp/address_reputation.py new file mode 100644 index 0000000..48d8a63 --- /dev/null +++ b/cdp/address_reputation.py @@ -0,0 +1,38 @@ +from cdp.client import AddressReputationMetadata +from cdp.client.models.address_reputation import AddressReputation as AddressReputationModel + + +class AddressReputation: + """A representation of the reputation of a blockchain address.""" + + def __init__(self, model: AddressReputationModel) -> None: + """Initialize the AddressReputation class.""" + if not model: + raise ValueError("model is required") + + self._score = model.score + self._metadata = model.metadata + + @property + def metadata(self) -> AddressReputationMetadata: + """Return the metadata of the address.""" + return self._metadata + + @property + def score(self) -> int: + """Return the score of the address.""" + return self._score + + @property + def risky(self) -> bool: + """Return whether the address is risky.""" + return self.score < 0 + + def __str__(self) -> str: + """Return a string representation of the AddressReputation.""" + metadata = ", ".join(f"{key}={getattr(self.metadata, key)}" for key in vars(self.metadata)) + return f"Address Reputation: (score={self.score}, metadata=({metadata}))" + + def __repr__(self) -> str: + """Return a string representation of the AddressReputation.""" + return str(self) diff --git a/cdp/api_clients.py b/cdp/api_clients.py index e60e17b..8b37942 100644 --- a/cdp/api_clients.py +++ b/cdp/api_clients.py @@ -1,4 +1,5 @@ from cdp.cdp_api_client import CdpApiClient +from cdp.client import ReputationApi from cdp.client.api.addresses_api import AddressesApi from cdp.client.api.assets_api import AssetsApi from cdp.client.api.balance_history_api import BalanceHistoryApi @@ -55,6 +56,7 @@ def __init__(self, cdp_client: CdpApiClient) -> None: self._balance_history: BalanceHistoryApi | None = None self._transaction_history: TransactionHistoryApi | None = None self._fund: FundApi | None = None + self._reputation: ReputationApi | None = None @property def wallets(self) -> WalletsApi: @@ -250,3 +252,18 @@ def fund(self) -> FundApi: if self._fund is None: self._fund = FundApi(api_client=self._cdp_client) return self._fund + + @property + def reputation(self) -> ReputationApi: + """Get the ReputationApi client instance. + + Returns: + ReputationApi: The ReputationApi client instance. + + Note: + This property lazily initializes the ReputationApi client on first access. + + """ + if self._reputation is None: + self._reputation = ReputationApi(api_client=self._cdp_client) + return self._reputation diff --git a/tests/factories/address_reputation_factory.py b/tests/factories/address_reputation_factory.py new file mode 100644 index 0000000..d06ecc1 --- /dev/null +++ b/tests/factories/address_reputation_factory.py @@ -0,0 +1,50 @@ +import pytest + +from cdp.address_reputation import AddressReputation +from cdp.client.models.address_reputation import AddressReputation as AddressReputationModel +from cdp.client.models.address_reputation_metadata import AddressReputationMetadata + + +@pytest.fixture +def address_reputation_model_factory(): + """Create and return a factory for creating AddressReputation fixtures.""" + + def _create_address_reputation_model( + score=1, + total_transactions=2, + unique_days_active=3, + longest_active_streak=4, + current_active_streak=5, + activity_period_days=6, + token_swaps_performed=7, + bridge_transactions_performed=8, + lend_borrow_stake_transactions=9, + ens_contract_interactions=10, + smart_contract_deployments=10, + ): + metadata = AddressReputationMetadata( + total_transactions=total_transactions, + unique_days_active=unique_days_active, + longest_active_streak=longest_active_streak, + current_active_streak=current_active_streak, + activity_period_days=activity_period_days, + token_swaps_performed=token_swaps_performed, + bridge_transactions_performed=bridge_transactions_performed, + lend_borrow_stake_transactions=lend_borrow_stake_transactions, + ens_contract_interactions=ens_contract_interactions, + smart_contract_deployments=smart_contract_deployments, + ) + return AddressReputationModel(score=score, metadata=metadata) + + return _create_address_reputation_model + + +@pytest.fixture +def address_reputation_factory(address_reputation_model_factory): + """Create and return a factory for creating AddressReputation fixtures.""" + + def _create_address_reputation(score=10): + reputation_model = address_reputation_model_factory(score=score) + return AddressReputation(reputation_model) + + return _create_address_reputation diff --git a/tests/test_address.py b/tests/test_address.py index 4c255cd..a247728 100644 --- a/tests/test_address.py +++ b/tests/test_address.py @@ -4,6 +4,7 @@ import pytest from cdp.address import Address +from cdp.address_reputation import AddressReputation from cdp.balance_map import BalanceMap from cdp.client.exceptions import ApiException from cdp.errors import ApiError @@ -240,6 +241,28 @@ def test_address_transactions_error(mock_api_clients, address_factory): next(transactions) +@patch("cdp.Cdp.api_clients") +def test_address_reputation(mock_api_clients, address_factory, address_reputation_factory): + """Test the reputation property of an Address.""" + address = address_factory() + address_reputation_data = address_reputation_factory(score=-10) + + mock_address_reputation = Mock() + mock_address_reputation.return_value = address_reputation_data + mock_api_clients.reputation.get_address_reputation = mock_address_reputation + + reputation = address.reputation() + + assert isinstance(reputation, AddressReputation) + + assert reputation.metadata.activity_period_days == 6 + assert reputation.risky is True + + mock_address_reputation.assert_called_once_with( + network_id=address.network_id, address_id=address.address_id + ) + + def test_address_str_representation(address_factory): """Test the str representation of an Address.""" address = address_factory() diff --git a/tests/test_address_reputation.py b/tests/test_address_reputation.py new file mode 100644 index 0000000..c57049a --- /dev/null +++ b/tests/test_address_reputation.py @@ -0,0 +1,48 @@ +from cdp.address_reputation import AddressReputation + + +def test_address_reputation_initialization(address_reputation_factory): + """Test address reputation initialization.""" + address_reputation = address_reputation_factory() + assert isinstance(address_reputation, AddressReputation) + + +def test_address_reputation_score(address_reputation_factory): + """Test address reputation score.""" + address_reputation = address_reputation_factory() + + assert address_reputation.score == 10 + + +def test_address_reputation_metadata(address_reputation_factory): + """Test address reputation metadata.""" + address_reputation = address_reputation_factory() + + assert address_reputation.metadata.total_transactions == 2 + assert address_reputation.metadata.unique_days_active == 3 + + +def test_address_reputation_risky(address_reputation_factory): + """Test address reputation risky.""" + address_reputation = address_reputation_factory(score=-5) + assert address_reputation.risky is True + + address_reputation = address_reputation_factory(score=0) + assert address_reputation.risky is False + + address_reputation = address_reputation_factory(score=5) + assert address_reputation.risky is False + + +def test_address_reputation_str(address_reputation_factory): + """Test address reputation str.""" + address_reputation = address_reputation_factory(score=10) + expected_str = "Address Reputation: (score=10, metadata=(total_transactions=2, unique_days_active=3, longest_active_streak=4, current_active_streak=5, activity_period_days=6, token_swaps_performed=7, bridge_transactions_performed=8, lend_borrow_stake_transactions=9, ens_contract_interactions=10, smart_contract_deployments=10))" + assert str(address_reputation) == expected_str + + +def test_address_reputation_repr(address_reputation_factory): + """Test address reputation repr.""" + address_reputation = address_reputation_factory(score=10) + expected_repr = "Address Reputation: (score=10, metadata=(total_transactions=2, unique_days_active=3, longest_active_streak=4, current_active_streak=5, activity_period_days=6, token_swaps_performed=7, bridge_transactions_performed=8, lend_borrow_stake_transactions=9, ens_contract_interactions=10, smart_contract_deployments=10))" + assert repr(address_reputation) == expected_repr