From f748b3dd25c6c37f439703524126546b7ab598fd Mon Sep 17 00:00:00 2001 From: 1yam Date: Wed, 21 Jan 2026 11:34:57 +0100 Subject: [PATCH 1/3] feature: new method in AlephHttpClient `get_address_stats` --- src/aleph/sdk/client/http.py | 34 ++++++++++++++++- src/aleph/sdk/query/filters.py | 58 +++++++++++++++++++++++++++++ src/aleph/sdk/query/responses.py | 25 +++++++++++++ tests/unit/conftest.py | 54 +++++++++++++++++++++++++++ tests/unit/test_asynchronous_get.py | 58 ++++++++++++++++++++++++++++- 5 files changed, 226 insertions(+), 3 deletions(-) diff --git a/src/aleph/sdk/client/http.py b/src/aleph/sdk/client/http.py index 6fa6d4ac..7a554564 100644 --- a/src/aleph/sdk/client/http.py +++ b/src/aleph/sdk/client/http.py @@ -52,8 +52,9 @@ RemovedMessageError, ResourceNotFoundError, ) -from ..query.filters import BalanceFilter, MessageFilter, PostFilter +from ..query.filters import AddressesFilter, BalanceFilter, MessageFilter, PostFilter from ..query.responses import ( + AddressStatsResponse, BalanceResponse, CreditsHistoryResponse, MessagesResponse, @@ -727,3 +728,34 @@ async def get_balances( resp.raise_for_status() result = await resp.json() return BalanceResponse.model_validate(result) + + async def get_address_stats( + self, + page_size: int = 200, + page: int = 1, + filter: Optional[AddressesFilter] = None, + ) -> AddressStatsResponse: + """ + Get address statistics with optional filtering and sorting. + + :param page_size: Number of results per page + :param page: Page number starting at 1 + :param filter: Query parameters for filtering and sorting + :return: Address statistics response with pagination + """ + if not filter: + params = { + "page": str(page), + "pagination": str(page_size), + } + else: + params = filter.as_http_params() + params["page"] = str(page) + params["pagination"] = str(page_size) + + async with self.http_session.get( + "/api/v1/addresses/stats.json", params=params + ) as resp: + resp.raise_for_status() + result = await resp.json() + return AddressStatsResponse.model_validate(result) diff --git a/src/aleph/sdk/query/filters.py b/src/aleph/sdk/query/filters.py index 18f8b3f7..d601fac0 100644 --- a/src/aleph/sdk/query/filters.py +++ b/src/aleph/sdk/query/filters.py @@ -21,6 +21,18 @@ class SortOrder(str, Enum): DESCENDING = "-1" +class SortByMessageType(str, Enum): + """Supported SortByMessageType types for address stats""" + + AGGREGATE = "aggregate" + FORGET = "forget" + INSTANCE = "instance" + POST = "post" + PROGRAM = "program" + STORE = "store" + TOTAL = "total" + + class MessageFilter: """ A collection of filters that can be applied on message queries. @@ -228,3 +240,49 @@ def as_http_params(self) -> Dict[str, str]: result[key] = value return result + + +class AddressesFilter: + """ + A collection of query parameters for address stats queries. + + :param address_contains: Case-insensitive substring to filter addresses + :param sort_by: Message type to sort by (aggregate, forget, instance, post, program, store, total) + :param sort_order: Sort order (ascending or descending) + """ + + address_contains: Optional[str] + sort_by: Optional[SortByMessageType] + sort_order: Optional[SortOrder] + + def __init__( + self, + address_contains: Optional[str] = None, + sort_by: Optional[SortByMessageType] = None, + sort_order: Optional[SortOrder] = None, + ): + self.address_contains = address_contains + self.sort_by = sort_by + self.sort_order = sort_order + + def as_http_params(self) -> Dict[str, str]: + """Convert the filters into a dict that can be used by an `aiohttp` client + as `params` to build the HTTP query string. + """ + + partial_result = { + "addressContains": self.address_contains, + "sortBy": enum_as_str(self.sort_by), + "sortOrder": enum_as_str(self.sort_order), + } + + # Ensure all values are strings. + result: Dict[str, str] = {} + + # Drop empty values + for key, value in partial_result.items(): + if value: + assert isinstance(value, str), f"Value must be a string: `{value}`" + result[key] = value + + return result diff --git a/src/aleph/sdk/query/responses.py b/src/aleph/sdk/query/responses.py index 6efade14..0bd95830 100644 --- a/src/aleph/sdk/query/responses.py +++ b/src/aleph/sdk/query/responses.py @@ -114,3 +114,28 @@ class BalanceResponse(BaseModel): details: Optional[Dict[str, Decimal]] = None locked_amount: Decimal credit_balance: int = 0 + + +class AddressStats(BaseModel): + """ + Statistics for a single address showing message counts by type. + """ + + messages: int = Field(description="Total number of messages") + post: int = Field(description="Number of POST messages") + aggregate: int = Field(description="Number of AGGREGATE messages") + store: int = Field(description="Number of STORE messages") + forget: int = Field(description="Number of FORGET messages") + program: int = Field(description="Number of PROGRAM messages") + instance: int = Field(description="Number of INSTANCE messages") + + model_config = ConfigDict(extra="forbid") + + +class AddressStatsResponse(PaginationResponse): + """Response from an aleph.im node API on the path /api/v1/addresses/stats.json""" + + data: Dict[str, AddressStats] = Field( + description="Dictionary mapping addresses to their statistics" + ) + pagination_item: str = "addresses" diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 5086703b..93c71053 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -157,6 +157,60 @@ def raw_posts_response(json_post) -> Callable[[int], Dict[str, Any]]: } +@pytest.fixture +def address_stats_data() -> List[Dict[str, Any]]: + return [ + { + "address": "0xa1B3bb7d2332383D96b7796B908fB7f7F3c2Be10", + "post": 10, + "aggregate": 5, + "store": 3, + "forget": 0, + "program": 2, + "instance": 1, + "total": 21, + }, + { + "address": "0x51A58800b26AA1451aaA803d1746687cB88E0501", + "post": 15, + "aggregate": 8, + "store": 6, + "forget": 1, + "program": 3, + "instance": 2, + "total": 35, + }, + ] + + +@pytest.fixture +def raw_address_stats_response( + address_stats_data, +) -> Callable[[int], Dict[str, Any]]: + # Convert list of address stats to dict format as returned by API + data_dict = {} + if int(1) == 1: # page 1 + for item in address_stats_data: + address = item["address"] + data_dict[address] = { + "messages": item["total"], + "post": item["post"], + "aggregate": item["aggregate"], + "store": item["store"], + "forget": item["forget"], + "program": item["program"], + "instance": item["instance"], + } + + return lambda page: { + "data": data_dict if int(page) == 1 else {}, + "pagination_item": "addresses", + "pagination_page": int(page), + "pagination_per_page": max(len(address_stats_data), 20), + "pagination_total": len(address_stats_data) if page == 1 else 0, + } + + class MockResponse: def __init__(self, sync: bool): self.sync = sync diff --git a/tests/unit/test_asynchronous_get.py b/tests/unit/test_asynchronous_get.py index 674becf7..64fb95ed 100644 --- a/tests/unit/test_asynchronous_get.py +++ b/tests/unit/test_asynchronous_get.py @@ -5,8 +5,14 @@ from aleph_message.models import MessagesResponse, MessageType from aleph.sdk.exceptions import ForgottenMessageError -from aleph.sdk.query.filters import MessageFilter, PostFilter -from aleph.sdk.query.responses import PostsResponse +from aleph.sdk.query.filters import ( + AddressesFilter, + MessageFilter, + PostFilter, + SortByMessageType, + SortOrder, +) +from aleph.sdk.query.responses import AddressStatsResponse, PostsResponse from tests.unit.conftest import make_mock_get_session @@ -124,5 +130,53 @@ async def test_get_message_error(rejected_message): assert error["details"] == rejected_message["details"] +@pytest.mark.asyncio +async def test_get_address_stats(raw_address_stats_response, address_stats_data): + mock_session = make_mock_get_session(raw_address_stats_response(1)) + async with mock_session as session: + response: AddressStatsResponse = await session.get_address_stats( + page_size=20, + page=1, + filter=AddressesFilter( + address_contains="0xa1", + sort_by=SortByMessageType.TOTAL, + sort_order=SortOrder.DESCENDING, + ), + ) + + address_stats = response.data + assert len(address_stats) == 2 + + # Get the first address from the stats data + first_address = address_stats_data[0]["address"] + assert first_address in address_stats + assert address_stats[first_address].messages == address_stats_data[0]["total"] + assert address_stats[first_address].post == address_stats_data[0]["post"] + assert ( + address_stats[first_address].aggregate == address_stats_data[0]["aggregate"] + ) + assert address_stats[first_address].store == address_stats_data[0]["store"] + assert address_stats[first_address].forget == address_stats_data[0]["forget"] + assert address_stats[first_address].program == address_stats_data[0]["program"] + assert ( + address_stats[first_address].instance == address_stats_data[0]["instance"] + ) + + +@pytest.mark.asyncio +async def test_get_address_stats_without_filter(raw_address_stats_response): + mock_session = make_mock_get_session(raw_address_stats_response(1)) + async with mock_session as session: + response: AddressStatsResponse = await session.get_address_stats( + page_size=20, + page=1, + ) + + address_stats = response.data + assert len(address_stats) == 2 + assert response.pagination_page == 1 + assert response.pagination_item == "addresses" + + if __name__ == "__main __": unittest.main() From ceda0205c9638b283241c0765900be34402c2f81 Mon Sep 17 00:00:00 2001 From: 1yam Date: Wed, 21 Jan 2026 15:10:29 +0100 Subject: [PATCH 2/3] feature: new method in AlephHttpClient `get_account_files` --- src/aleph/sdk/client/http.py | 43 ++++++++++++++++++++- src/aleph/sdk/query/filters.py | 43 +++++++++++++++++++++ src/aleph/sdk/query/responses.py | 29 ++++++++++++++ tests/unit/conftest.py | 28 ++++++++++++++ tests/unit/test_asynchronous_get.py | 59 ++++++++++++++++++++++++++++- 5 files changed, 200 insertions(+), 2 deletions(-) diff --git a/src/aleph/sdk/client/http.py b/src/aleph/sdk/client/http.py index 7a554564..9177b505 100644 --- a/src/aleph/sdk/client/http.py +++ b/src/aleph/sdk/client/http.py @@ -52,8 +52,15 @@ RemovedMessageError, ResourceNotFoundError, ) -from ..query.filters import AddressesFilter, BalanceFilter, MessageFilter, PostFilter +from ..query.filters import ( + AccountFilesFilter, + AddressesFilter, + BalanceFilter, + MessageFilter, + PostFilter, +) from ..query.responses import ( + AccountFilesResponse, AddressStatsResponse, BalanceResponse, CreditsHistoryResponse, @@ -759,3 +766,37 @@ async def get_address_stats( resp.raise_for_status() result = await resp.json() return AddressStatsResponse.model_validate(result) + + async def get_account_files( + self, + address: str, + page_size: int = 100, + page: int = 1, + filter: Optional[AccountFilesFilter] = None, + ) -> AccountFilesResponse: + """ + Get files stored by a specific address. + + :param address: The account address to query files for + :param page_size: Number of results per page (default 100) + :param page: Page number starting at 1 + :param filter: Query parameters for filtering and sorting + :return: Account files response with pagination + :raises aiohttp.ClientResponseError: If the address has no files (HTTP 404) + """ + if not filter: + params = { + "page": str(page), + "pagination": str(page_size), + } + else: + params = filter.as_http_params() + params["page"] = str(page) + params["pagination"] = str(page_size) + + async with self.http_session.get( + f"/api/v0/addresses/{address}/files", params=params + ) as resp: + resp.raise_for_status() + result = await resp.json() + return AccountFilesResponse.model_validate(result) diff --git a/src/aleph/sdk/query/filters.py b/src/aleph/sdk/query/filters.py index d601fac0..e5e9d5d8 100644 --- a/src/aleph/sdk/query/filters.py +++ b/src/aleph/sdk/query/filters.py @@ -33,6 +33,13 @@ class SortByMessageType(str, Enum): TOTAL = "total" +class FileType(str, Enum): + """Supported file types""" + + FILE = "file" + DIRECTORY = "directory" + + class MessageFilter: """ A collection of filters that can be applied on message queries. @@ -286,3 +293,39 @@ def as_http_params(self) -> Dict[str, str]: result[key] = value return result + + +class AccountFilesFilter: + """ + A collection of query parameters for account files queries. + + :param sort_order: Sort order (ascending or descending by creation date) + """ + + sort_order: Optional[SortOrder] + + def __init__( + self, + sort_order: Optional[SortOrder] = None, + ): + self.sort_order = sort_order + + def as_http_params(self) -> Dict[str, str]: + """Convert the filters into a dict that can be used by an `aiohttp` client + as `params` to build the HTTP query string. + """ + + partial_result = { + "sort_order": enum_as_str(self.sort_order), + } + + # Ensure all values are strings. + result: Dict[str, str] = {} + + # Drop empty values + for key, value in partial_result.items(): + if value: + assert isinstance(value, str), f"Value must be a string: `{value}`" + result[key] = value + + return result diff --git a/src/aleph/sdk/query/responses.py b/src/aleph/sdk/query/responses.py index 0bd95830..f36763b2 100644 --- a/src/aleph/sdk/query/responses.py +++ b/src/aleph/sdk/query/responses.py @@ -13,6 +13,8 @@ ) from pydantic import BaseModel, ConfigDict, Field +from aleph.sdk.query.filters import FileType + class Post(BaseModel): """ @@ -139,3 +141,30 @@ class AddressStatsResponse(PaginationResponse): description="Dictionary mapping addresses to their statistics" ) pagination_item: str = "addresses" + + +class AccountFilesResponseItem(BaseModel): + """ + A single file entry in an account's file list. + """ + + file_hash: str = Field(description="Hash of the file content") + size: int = Field(description="Size of the file in bytes") + type: FileType = Field(description="Type of the file (FILE or DIRECTORY)") + created: dt.datetime = Field(description="Timestamp when the file was created") + item_hash: str = Field(description="Hash of the message that created this file") + + model_config = ConfigDict(extra="forbid") + + +class AccountFilesResponse(PaginationResponse): + """Response from an aleph.im node API on the path /api/v0/addresses/{address}/files""" + + address: str = Field(description="The account address") + total_size: int = Field(description="Total size of all files in bytes") + files: List[AccountFilesResponseItem] = Field( + description="List of files owned by the address" + ) + pagination_item: str = "files" + + model_config = ConfigDict(extra="forbid") diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 93c71053..e41d7512 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -211,6 +211,34 @@ def raw_address_stats_response( } +@pytest.fixture +def address_files_data() -> Dict[str, Any]: + return { + "address": "0xd463495a6FEaC9921FD0C3a595B81E7B2C02B24d", + "total_size": 2048000, + "files": [ + { + "file_hash": "QmX1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r9s0t1", + "size": 1024000, + "type": "file", + "created": "2024-01-15T10:30:00.000000", + "item_hash": "abc123def456", + }, + { + "file_hash": "QmY9z8y7x6w5v4u3t2s1r0q9p8o7n6m5l4k3j2i1h0g9", + "size": 1024000, + "type": "directory", + "created": "2024-01-16T14:45:00.000000", + "item_hash": "xyz789uvw012", + }, + ], + "pagination_page": 1, + "pagination_total": 2, + "pagination_per_page": 100, + "pagination_item": "files", + } + + class MockResponse: def __init__(self, sync: bool): self.sync = sync diff --git a/tests/unit/test_asynchronous_get.py b/tests/unit/test_asynchronous_get.py index 64fb95ed..3d49276b 100644 --- a/tests/unit/test_asynchronous_get.py +++ b/tests/unit/test_asynchronous_get.py @@ -6,13 +6,18 @@ from aleph.sdk.exceptions import ForgottenMessageError from aleph.sdk.query.filters import ( + AccountFilesFilter, AddressesFilter, MessageFilter, PostFilter, SortByMessageType, SortOrder, ) -from aleph.sdk.query.responses import AddressStatsResponse, PostsResponse +from aleph.sdk.query.responses import ( + AccountFilesResponse, + AddressStatsResponse, + PostsResponse, +) from tests.unit.conftest import make_mock_get_session @@ -178,5 +183,57 @@ async def test_get_address_stats_without_filter(raw_address_stats_response): assert response.pagination_item == "addresses" +@pytest.mark.asyncio +async def test_get_account_files(address_files_data): + mock_session = make_mock_get_session(address_files_data) + async with mock_session as session: + response: AccountFilesResponse = await session.get_account_files( + address="0xd463495a6FEaC9921FD0C3a595B81E7B2C02B24d", + page_size=100, + page=1, + filter=AccountFilesFilter(sort_order=SortOrder.DESCENDING), + ) + + files = response.files + assert len(files) >= 1 + assert response.address + assert response.total_size >= 0 + assert response.pagination_item == "files" + + +@pytest.mark.asyncio +async def test_get_account_files_without_filter(): + address = "0xd463495a6FEaC9921FD0C3a595B81E7B2C02B24d" + files_data = { + "address": address, + "total_size": 1024000, + "files": [ + { + "file_hash": "QmTest", + "size": 1024000, + "type": "file", + "created": "2024-01-15T10:30:00", + "item_hash": "testitem", + } + ], + "pagination_page": 1, + "pagination_total": 1, + "pagination_per_page": 100, + "pagination_item": "files", + } + + mock_session = make_mock_get_session(files_data) + async with mock_session as session: + response: AccountFilesResponse = await session.get_account_files( + address=address, + page_size=100, + page=1, + ) + + assert len(response.files) == 1 + assert response.pagination_page == 1 + assert response.pagination_item == "files" + + if __name__ == "__main __": unittest.main() From 17a8b0f718b8eaaa1eaf3c43e15c5ef90a8de665 Mon Sep 17 00:00:00 2001 From: 1yam Date: Wed, 21 Jan 2026 16:17:50 +0100 Subject: [PATCH 3/3] feature: new method in AlephHttpClient `get_chain_balances` --- src/aleph/sdk/client/http.py | 31 ++++++++++++++++++++ src/aleph/sdk/query/filters.py | 45 +++++++++++++++++++++++++++++ src/aleph/sdk/query/responses.py | 23 +++++++++++++++ tests/unit/conftest.py | 37 ++++++++++++++++++++++++ tests/unit/test_asynchronous_get.py | 43 ++++++++++++++++++++++++++- 5 files changed, 178 insertions(+), 1 deletion(-) diff --git a/src/aleph/sdk/client/http.py b/src/aleph/sdk/client/http.py index 9177b505..c3a37da6 100644 --- a/src/aleph/sdk/client/http.py +++ b/src/aleph/sdk/client/http.py @@ -56,6 +56,7 @@ AccountFilesFilter, AddressesFilter, BalanceFilter, + ChainBalancesFilter, MessageFilter, PostFilter, ) @@ -63,6 +64,7 @@ AccountFilesResponse, AddressStatsResponse, BalanceResponse, + ChainBalancesResponse, CreditsHistoryResponse, MessagesResponse, Post, @@ -736,6 +738,35 @@ async def get_balances( result = await resp.json() return BalanceResponse.model_validate(result) + async def get_chain_balances( + self, + page_size: int = 100, + page: int = 1, + filter: Optional[ChainBalancesFilter] = None, + ) -> ChainBalancesResponse: + """ + Get balances across multiple addresses and chains. + + :param page_size: Number of results per page (default 100) + :param page: Page number starting at 1 + :param filter: Query parameters for filtering by chains and minimum balance + :return: Chain balances response with pagination + """ + if not filter: + params = { + "page": str(page), + "pagination": str(page_size), + } + else: + params = filter.as_http_params() + params["page"] = str(page) + params["pagination"] = str(page_size) + + async with self.http_session.get("/api/v0/balances", params=params) as resp: + resp.raise_for_status() + result = await resp.json() + return ChainBalancesResponse.model_validate(result) + async def get_address_stats( self, page_size: int = 200, diff --git a/src/aleph/sdk/query/filters.py b/src/aleph/sdk/query/filters.py index e5e9d5d8..6a09bf36 100644 --- a/src/aleph/sdk/query/filters.py +++ b/src/aleph/sdk/query/filters.py @@ -329,3 +329,48 @@ def as_http_params(self) -> Dict[str, str]: result[key] = value return result + + +class ChainBalancesFilter: + """ + A collection of query parameters for chain balances queries. + + :param chains: Filter by specific blockchain chains + :param min_balance: Minimum balance required (must be >= 1) + """ + + chains: Optional[Iterable[Chain]] + min_balance: Optional[int] + + def __init__( + self, + chains: Optional[Iterable[Chain]] = None, + min_balance: Optional[int] = None, + ): + self.chains = chains + self.min_balance = min_balance + + def as_http_params(self) -> Dict[str, str]: + """Convert the filters into a dict that can be used by an `aiohttp` client + as `params` to build the HTTP query string. + """ + + partial_result = { + "chains": serialize_list( + [chain.value for chain in self.chains] if self.chains else None + ), + "min_balance": ( + str(self.min_balance) if self.min_balance is not None else None + ), + } + + # Ensure all values are strings. + result: Dict[str, str] = {} + + # Drop empty values + for key, value in partial_result.items(): + if value: + assert isinstance(value, str), f"Value must be a string: `{value}`" + result[key] = value + + return result diff --git a/src/aleph/sdk/query/responses.py b/src/aleph/sdk/query/responses.py index f36763b2..1bde9b4b 100644 --- a/src/aleph/sdk/query/responses.py +++ b/src/aleph/sdk/query/responses.py @@ -168,3 +168,26 @@ class AccountFilesResponse(PaginationResponse): pagination_item: str = "files" model_config = ConfigDict(extra="forbid") + + +class AddressBalanceResponseItem(BaseModel): + """ + A single balance entry for an address on a specific chain. + """ + + address: str = Field(description="The account address") + balance: Decimal = Field(description="Balance amount") + chain: Chain = Field(description="Blockchain the balance is on") + + model_config = ConfigDict(extra="forbid") + + +class ChainBalancesResponse(PaginationResponse): + """Response from an aleph.im node API on the path /api/v0/balances""" + + balances: List[AddressBalanceResponseItem] = Field( + description="List of address balances across different chains" + ) + pagination_item: str = "balances" + + model_config = ConfigDict(extra="forbid") diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index e41d7512..0588ab27 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -239,6 +239,43 @@ def address_files_data() -> Dict[str, Any]: } +@pytest.fixture +def chain_balances_data() -> List[Dict[str, Any]]: + """Return mock data representing chain balances.""" + return [ + { + "address": "0x1234567890123456789012345678901234567890", + "balance": 1000.5, + "chain": "ETH", + }, + { + "address": "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd", + "balance": 2500.75, + "chain": "ETH", + }, + { + "address": "0x9876543210987654321098765432109876543210", + "balance": 500.0, + "chain": "AVAX", + }, + ] + + +@pytest.fixture +def raw_chain_balances_response( + chain_balances_data: List[Dict[str, Any]], +) -> Callable[[int], Dict[str, Any]]: + """Return a function that generates paginated chain balances API responses.""" + + return lambda page: { + "balances": chain_balances_data if int(page) == 1 else [], + "pagination_item": "balances", + "pagination_page": int(page), + "pagination_per_page": 100, + "pagination_total": len(chain_balances_data) if page == 1 else 0, + } + + class MockResponse: def __init__(self, sync: bool): self.sync = sync diff --git a/tests/unit/test_asynchronous_get.py b/tests/unit/test_asynchronous_get.py index 3d49276b..7e019035 100644 --- a/tests/unit/test_asynchronous_get.py +++ b/tests/unit/test_asynchronous_get.py @@ -2,12 +2,13 @@ from datetime import datetime import pytest -from aleph_message.models import MessagesResponse, MessageType +from aleph_message.models import Chain, MessagesResponse, MessageType from aleph.sdk.exceptions import ForgottenMessageError from aleph.sdk.query.filters import ( AccountFilesFilter, AddressesFilter, + ChainBalancesFilter, MessageFilter, PostFilter, SortByMessageType, @@ -16,6 +17,7 @@ from aleph.sdk.query.responses import ( AccountFilesResponse, AddressStatsResponse, + ChainBalancesResponse, PostsResponse, ) from tests.unit.conftest import make_mock_get_session @@ -235,5 +237,44 @@ async def test_get_account_files_without_filter(): assert response.pagination_item == "files" +@pytest.mark.asyncio +async def test_get_chain_balances(raw_chain_balances_response, chain_balances_data): + """Test the get_chain_balances endpoint with filters applied.""" + mock_session = make_mock_get_session(raw_chain_balances_response(1)) + async with mock_session as session: + response: ChainBalancesResponse = await session.get_chain_balances( + page_size=100, + page=1, + filter=ChainBalancesFilter( + chains=[Chain.ETH, Chain.AVAX], + min_balance=100, + ), + ) + + balances = response.balances + assert len(balances) == 3 + assert balances[0].address == chain_balances_data[0]["address"] + assert float(balances[0].balance) == chain_balances_data[0]["balance"] + assert balances[0].chain == Chain.ETH + assert response.pagination_item == "balances" + assert response.pagination_page == 1 + assert response.pagination_total == 3 + + +@pytest.mark.asyncio +async def test_get_chain_balances_without_filter(raw_chain_balances_response): + """Test the get_chain_balances endpoint without filters.""" + mock_session = make_mock_get_session(raw_chain_balances_response(1)) + async with mock_session as session: + response: ChainBalancesResponse = await session.get_chain_balances( + page_size=100, + page=1, + ) + + assert len(response.balances) >= 0 + assert response.pagination_page == 1 + assert response.pagination_item == "balances" + + if __name__ == "__main __": unittest.main()