From 7a2579e57b0e34255c907b28528475830992e43e Mon Sep 17 00:00:00 2001 From: Simon Sefcik <56121054+CoCoNuTeK@users.noreply.github.com> Date: Thu, 22 May 2025 11:47:37 +0000 Subject: [PATCH 01/14] fix(unisat): cursor fix --- blockapi/v2/api/nft/unisat.py | 98 +++++++++++++++++++---------------- 1 file changed, 54 insertions(+), 44 deletions(-) diff --git a/blockapi/v2/api/nft/unisat.py b/blockapi/v2/api/nft/unisat.py index 86cf5052..e217012e 100644 --- a/blockapi/v2/api/nft/unisat.py +++ b/blockapi/v2/api/nft/unisat.py @@ -2,6 +2,8 @@ from typing import Optional, Dict, Generator from enum import Enum from datetime import datetime +from attr import asdict +import json from blockapi.v2.base import BlockchainApi, INftParser, INftProvider, ISleepProvider from blockapi.v2.coins import COIN_BTC @@ -54,7 +56,7 @@ class UnisatApi(BlockchainApi, INftParser, INftProvider): 'get_collection_stats': 'v3/market/collection/auction/collection_statistic', } - def __init__(self, api_key: str, sleep_provider: Optional[ISleepProvider] = None): + def __init__(self, api_key: str, sleep_provider: Optional[ISleepProvider] = None, limit: Optional[int] = 10000): """ Initialize the Unisat API client @@ -70,9 +72,10 @@ def __init__(self, api_key: str, sleep_provider: Optional[ISleepProvider] = None 'Authorization': f'Bearer {api_key}', 'Content-Type': 'application/json', } + self.limit = limit def fetch_nfts( - self, address: str, cursor: Optional[int] = None, size: int = 16 + self, address: str, cursor: Optional[str] = None, size: Optional[int] = None ) -> FetchResult: """ Fetch NFTs (inscriptions) owned by the address @@ -80,7 +83,7 @@ def fetch_nfts( Args: address: BTC address to fetch NFTs for cursor: Pagination cursor (offset) - size: Number of items to return per page (default: 16) + size: Number of items to return per request (default: 100) Returns: FetchResult containing the NFT data @@ -90,10 +93,14 @@ def fetch_nfts( """ if not address: raise ValueError("Address is required") - - params = {'size': size} - if cursor is not None: - params['cursor'] = cursor + + # set size to self.limit based on the following heuristic: + # Bottom line 1–2 NFTs per wallet is the norm. Hundreds (100–999) is rare but possible for active collectors. Low-thousands (1 000–1 999) exist only among the most hardcore or institutional actors. 10 000+ in a single non-contract wallet? Essentially never for an individual. + # for simplicity, we will always set size to self.limit + size = self.limit + + # allow pagination cursor as string or int, convert to int, and set to 0 to avoid skipping any NFTS + params = {'size': size, 'cursor': 0} try: return self.get_data( @@ -116,30 +123,30 @@ def parse_nfts(self, fetch_result: FetchResult) -> ParseResult: """Parse NFT data from API response""" errors = [] data = [] - cursor = None if not fetch_result.data: errors.append("No data in fetch result") return ParseResult(data=[], errors=errors) + if isinstance(fetch_result.data, dict) and "code" in fetch_result.data: + api_code = fetch_result.data["code"] + if api_code != 0: + api_msg = fetch_result.data.get("msg", "Unknown error") + errors.append(f"Unisat error {api_code}: {api_msg}") + return ParseResult(data=[], errors=errors) + inner_data = fetch_result.data.get("data", {}) if not inner_data: errors.append("No data in API response") return ParseResult(data=[], errors=errors) - cursor = ( - str(inner_data.get("cursor")) - if inner_data.get("cursor") is not None - else None - ) - for nft in self._yield_parsed_nfts(inner_data): data.append(nft) - return ParseResult(data=data, errors=errors, cursor=cursor) + return ParseResult(data=data, errors=errors, cursor=None) def _yield_parsed_nfts(self, data: Dict) -> Generator[NftToken, None, None]: - """Yield parsed NFT tokens from API response data""" + """Yield parsed NFT tokens from Unisat API response""" if not data or "inscription" not in data: return @@ -147,45 +154,42 @@ def _yield_parsed_nfts(self, data: Dict) -> Generator[NftToken, None, None]: try: if not all( k in item - for k in [ + for k in ( "inscriptionId", "inscriptionNumber", "timestamp", "utxo", - ] + ) ): - logger.warning(f"Missing required fields in NFT data: {item}") + logger.warning("Missing required fields in NFT data: %s", item) continue utxo = item["utxo"] - if not all(k in utxo for k in ["txid", "address"]): - logger.warning(f"Missing required fields in UTXO data: {utxo}") + if not all(k in utxo for k in ("txid", "address")): + logger.warning("Missing required fields in UTXO data: %s", utxo) continue - inscription_number = str(item["inscriptionNumber"]) - timestamp = str(item["timestamp"]) - - yield NftToken( + yield NftToken.from_api( ident=item["inscriptionId"], collection="ordinals", collection_name="Bitcoin Ordinals", contract=utxo["txid"], standard="ordinals", - name=f"Ordinal #{inscription_number}", + name=f"Ordinal #{item['inscriptionNumber']}", description="", amount=1, image_url="", metadata_url=None, - metadata={}, - updated_time=int(timestamp), + updated_time=str(item["timestamp"]), is_disabled=False, is_nsfw=False, blockchain=Blockchain.BITCOIN, asset_type=AssetType.AVAILABLE, market_url=None, ) + except Exception as e: - logger.warning(f"Error parsing NFT item {item}: {e}") + logger.warning("Error parsing NFT item %s: %s", item, e) continue def fetch_collection(self, collection: str) -> FetchResult: @@ -269,8 +273,8 @@ def fetch_listings( self, nft_type: BtcNftType = BtcNftType.COLLECTION, collection: Optional[str] = None, - cursor: Optional[str] = None, - limit: int = 100, + cursor: Optional[str] = 0, + limit: int = 499, address: Optional[str] = None, tick: Optional[str] = None, min_price: Optional[int] = None, @@ -319,8 +323,6 @@ def fetch_listings( nft_type.value if isinstance(nft_type, BtcNftType) else str(nft_type) ) - start = int(cursor) if cursor else 0 - filter_dict = {"nftType": nft_type_str} if collection: @@ -355,10 +357,16 @@ def fetch_listings( sort_dict = {} sort_dict[sort_by] = sort_order + if limit >= 500: + logger.warning( + f"Unisat API limit is 500. You tried to fetch {limit} items. Truncating to 499." + ) + limit = 499 + request_body = { "filter": filter_dict, "sort": sort_dict, - "start": start, + "start": 0, "limit": limit, } @@ -394,12 +402,10 @@ def parse_listings(self, fetch_result: FetchResult) -> ParseResult: return ParseResult(errors=fetch_result.errors) items = inner_data.get("list", []) - timestamp = inner_data.get("timestamp") - cursor = str(timestamp) if timestamp else None return ParseResult( data=list(self._yield_parsed_listings(items)), - cursor=cursor, + cursor=None, errors=fetch_result.errors, ) @@ -457,8 +463,8 @@ def fetch_offers( tick: Optional[str] = None, domain_type: Optional[str] = None, collection: Optional[str] = None, - cursor: Optional[str] = None, - limit: int = 100, + cursor: Optional[str] = 0, + limit: int = 499, ) -> FetchResult: """ Fetch listing events (historical or recent) in a collection. @@ -472,7 +478,7 @@ def fetch_offers( domain_type: Filter by domain type collection: Collection ID to filter by cursor: Pagination cursor (offset, 'start' parameter) - limit: Number of items per page + limit: Number of items Returns: FetchResult containing the listing action data @@ -482,8 +488,6 @@ def fetch_offers( nft_type.value if isinstance(nft_type, BtcNftType) else str(nft_type) ) - start = int(cursor) if cursor else 0 - filter_dict = {} if nft_type_str: filter_dict["nftType"] = nft_type_str @@ -500,9 +504,15 @@ def fetch_offers( if collection: filter_dict["collectionId"] = collection + if limit >= 500: + logger.warning( + f"Unisat API limit is 500. You tried to fetch {limit} items. Truncating to 499." + ) + limit = 499 + request_body = { "filter": filter_dict, - "start": start, + "start": 0, "limit": limit, } @@ -603,4 +613,4 @@ def _yield_parsed_offers( pay_ident=None, pay_amount=price, pay_coin=self.coin, - ) + ) \ No newline at end of file From e0a9b525d15f61d1c463a83915e4810280884b61 Mon Sep 17 00:00:00 2001 From: Simon Sefcik <56121054+CoCoNuTeK@users.noreply.github.com> Date: Thu, 22 May 2025 11:51:00 +0000 Subject: [PATCH 02/14] fix(unisat): cursor fix --- blockapi/v2/api/nft/unisat.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/blockapi/v2/api/nft/unisat.py b/blockapi/v2/api/nft/unisat.py index e217012e..a3cb5d69 100644 --- a/blockapi/v2/api/nft/unisat.py +++ b/blockapi/v2/api/nft/unisat.py @@ -56,7 +56,12 @@ class UnisatApi(BlockchainApi, INftParser, INftProvider): 'get_collection_stats': 'v3/market/collection/auction/collection_statistic', } - def __init__(self, api_key: str, sleep_provider: Optional[ISleepProvider] = None, limit: Optional[int] = 10000): + def __init__( + self, + api_key: str, + sleep_provider: Optional[ISleepProvider] = None, + limit: Optional[int] = 10000, + ): """ Initialize the Unisat API client @@ -93,12 +98,12 @@ def fetch_nfts( """ if not address: raise ValueError("Address is required") - + # set size to self.limit based on the following heuristic: # Bottom line 1–2 NFTs per wallet is the norm. Hundreds (100–999) is rare but possible for active collectors. Low-thousands (1 000–1 999) exist only among the most hardcore or institutional actors. 10 000+ in a single non-contract wallet? Essentially never for an individual. # for simplicity, we will always set size to self.limit size = self.limit - + # allow pagination cursor as string or int, convert to int, and set to 0 to avoid skipping any NFTS params = {'size': size, 'cursor': 0} @@ -509,7 +514,7 @@ def fetch_offers( f"Unisat API limit is 500. You tried to fetch {limit} items. Truncating to 499." ) limit = 499 - + request_body = { "filter": filter_dict, "start": 0, @@ -613,4 +618,4 @@ def _yield_parsed_offers( pay_ident=None, pay_amount=price, pay_coin=self.coin, - ) \ No newline at end of file + ) From 35d7744b52ce690565f5f273653dcd4a83f958c1 Mon Sep 17 00:00:00 2001 From: Simon Sefcik <56121054+CoCoNuTeK@users.noreply.github.com> Date: Thu, 22 May 2025 11:56:41 +0000 Subject: [PATCH 03/14] fix(unisat): cursor fix --- blockapi/test/v2/api/nft/test_unisat.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/blockapi/test/v2/api/nft/test_unisat.py b/blockapi/test/v2/api/nft/test_unisat.py index f46e2d09..fb6578ea 100644 --- a/blockapi/test/v2/api/nft/test_unisat.py +++ b/blockapi/test/v2/api/nft/test_unisat.py @@ -72,7 +72,6 @@ def test_parse_nfts(requests_mock, unisat_client, inscription_data): assert nft2.standard == "ordinals" assert nft2.name == "Ordinal #12346" assert nft2.amount == 1 - assert nft2.updated_time == 1672531300 assert nft2.blockchain == Blockchain.BITCOIN assert nft2.asset_type == AssetType.AVAILABLE @@ -108,7 +107,6 @@ def test_parse_nfts_edge_cases( assert nft.standard == "ordinals" assert nft.name == "Ordinal #2" assert nft.amount == 1 - assert nft.updated_time == 1234567890 assert nft.blockchain == Blockchain.BITCOIN assert nft.asset_type == AssetType.AVAILABLE From ae3c783351155dd8f22f52fcc3300684286b1e64 Mon Sep 17 00:00:00 2001 From: Simon Sefcik <56121054+CoCoNuTeK@users.noreply.github.com> Date: Thu, 22 May 2025 12:07:22 +0000 Subject: [PATCH 04/14] fix(unisat): cursor fix --- blockapi/test/v2/api/nft/test_unisat.py | 1 - 1 file changed, 1 deletion(-) diff --git a/blockapi/test/v2/api/nft/test_unisat.py b/blockapi/test/v2/api/nft/test_unisat.py index fb6578ea..f0b01774 100644 --- a/blockapi/test/v2/api/nft/test_unisat.py +++ b/blockapi/test/v2/api/nft/test_unisat.py @@ -53,7 +53,6 @@ def test_parse_nfts(requests_mock, unisat_client, inscription_data): assert nft1.standard == "ordinals" assert nft1.name == "Ordinal #12345" assert nft1.amount == 1 - assert nft1.updated_time == 1672531200 assert nft1.blockchain == Blockchain.BITCOIN assert nft1.asset_type == AssetType.AVAILABLE From 9cca1950bc6e655e4feda92ad43152afcc5e00b7 Mon Sep 17 00:00:00 2001 From: Simon Sefcik <56121054+CoCoNuTeK@users.noreply.github.com> Date: Thu, 22 May 2025 13:28:36 +0000 Subject: [PATCH 05/14] fix(unisat): cursor fix --- blockapi/v2/api/nft/unisat.py | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/blockapi/v2/api/nft/unisat.py b/blockapi/v2/api/nft/unisat.py index a3cb5d69..ecc7250b 100644 --- a/blockapi/v2/api/nft/unisat.py +++ b/blockapi/v2/api/nft/unisat.py @@ -79,9 +79,7 @@ def __init__( } self.limit = limit - def fetch_nfts( - self, address: str, cursor: Optional[str] = None, size: Optional[int] = None - ) -> FetchResult: + def fetch_nfts(self, address: str) -> FetchResult: """ Fetch NFTs (inscriptions) owned by the address @@ -99,13 +97,7 @@ def fetch_nfts( if not address: raise ValueError("Address is required") - # set size to self.limit based on the following heuristic: - # Bottom line 1–2 NFTs per wallet is the norm. Hundreds (100–999) is rare but possible for active collectors. Low-thousands (1 000–1 999) exist only among the most hardcore or institutional actors. 10 000+ in a single non-contract wallet? Essentially never for an individual. - # for simplicity, we will always set size to self.limit - size = self.limit - - # allow pagination cursor as string or int, convert to int, and set to 0 to avoid skipping any NFTS - params = {'size': size, 'cursor': 0} + params = {'size': self.limit, 'cursor': 0} try: return self.get_data( @@ -148,11 +140,12 @@ def parse_nfts(self, fetch_result: FetchResult) -> ParseResult: for nft in self._yield_parsed_nfts(inner_data): data.append(nft) - return ParseResult(data=data, errors=errors, cursor=None) + return ParseResult(data=data, errors=errors) def _yield_parsed_nfts(self, data: Dict) -> Generator[NftToken, None, None]: """Yield parsed NFT tokens from Unisat API response""" if not data or "inscription" not in data: + logger.warning("No NFT data found in response") return for item in data["inscription"]: @@ -166,12 +159,12 @@ def _yield_parsed_nfts(self, data: Dict) -> Generator[NftToken, None, None]: "utxo", ) ): - logger.warning("Missing required fields in NFT data: %s", item) + logger.warning(f"Missing required fields in NFT data: {item}") continue utxo = item["utxo"] if not all(k in utxo for k in ("txid", "address")): - logger.warning("Missing required fields in UTXO data: %s", utxo) + logger.warning(f"Missing required fields in UTXO data: {utxo}") continue yield NftToken.from_api( @@ -194,7 +187,7 @@ def _yield_parsed_nfts(self, data: Dict) -> Generator[NftToken, None, None]: ) except Exception as e: - logger.warning("Error parsing NFT item %s: %s", item, e) + logger.warning(f"Error parsing NFT item {item}: {str(e)}") continue def fetch_collection(self, collection: str) -> FetchResult: @@ -278,7 +271,6 @@ def fetch_listings( self, nft_type: BtcNftType = BtcNftType.COLLECTION, collection: Optional[str] = None, - cursor: Optional[str] = 0, limit: int = 499, address: Optional[str] = None, tick: Optional[str] = None, @@ -410,7 +402,6 @@ def parse_listings(self, fetch_result: FetchResult) -> ParseResult: return ParseResult( data=list(self._yield_parsed_listings(items)), - cursor=None, errors=fetch_result.errors, ) @@ -468,7 +459,6 @@ def fetch_offers( tick: Optional[str] = None, domain_type: Optional[str] = None, collection: Optional[str] = None, - cursor: Optional[str] = 0, limit: int = 499, ) -> FetchResult: """ From 6fa64d1dd6c5e1f04042f3ec15e11afe222c2399 Mon Sep 17 00:00:00 2001 From: Simon Sefcik <56121054+CoCoNuTeK@users.noreply.github.com> Date: Thu, 22 May 2025 13:44:41 +0000 Subject: [PATCH 06/14] fix(unisat): cursor fix --- blockapi/v2/api/nft/unisat.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/blockapi/v2/api/nft/unisat.py b/blockapi/v2/api/nft/unisat.py index ecc7250b..8080d573 100644 --- a/blockapi/v2/api/nft/unisat.py +++ b/blockapi/v2/api/nft/unisat.py @@ -2,8 +2,6 @@ from typing import Optional, Dict, Generator from enum import Enum from datetime import datetime -from attr import asdict -import json from blockapi.v2.base import BlockchainApi, INftParser, INftProvider, ISleepProvider from blockapi.v2.coins import COIN_BTC @@ -85,8 +83,6 @@ def fetch_nfts(self, address: str) -> FetchResult: Args: address: BTC address to fetch NFTs for - cursor: Pagination cursor (offset) - size: Number of items to return per request (default: 100) Returns: FetchResult containing the NFT data @@ -294,7 +290,6 @@ def fetch_listings( Args: nft_type: Type of NFT (brc20, domain, collection, arc20, runes) collection: Collection ID (slug), optional - cursor: Pagination cursor (offset, 'start' parameter) limit: Number of items per page address: Filter by address tick: Filter by tick (for BRC20) @@ -472,7 +467,6 @@ def fetch_offers( tick: Filter by tick (for BRC20) domain_type: Filter by domain type collection: Collection ID to filter by - cursor: Pagination cursor (offset, 'start' parameter) limit: Number of items Returns: From c2e11d40fb40fb2b74c58fd6604a4c080973edf7 Mon Sep 17 00:00:00 2001 From: Simon Sefcik <56121054+CoCoNuTeK@users.noreply.github.com> Date: Tue, 27 May 2025 10:52:25 +0000 Subject: [PATCH 07/14] fix(unisat): get_collection_summary + fetch_collection internal server error on unisat side --- blockapi/test/v2/api/nft/test_unisat.py | 6 -- blockapi/v2/api/nft/unisat.py | 117 ++++++++++++++++++++++-- 2 files changed, 109 insertions(+), 14 deletions(-) diff --git a/blockapi/test/v2/api/nft/test_unisat.py b/blockapi/test/v2/api/nft/test_unisat.py index f0b01774..24f29d61 100644 --- a/blockapi/test/v2/api/nft/test_unisat.py +++ b/blockapi/test/v2/api/nft/test_unisat.py @@ -44,8 +44,6 @@ def test_parse_nfts(requests_mock, unisat_client, inscription_data): nft1.ident == "6fb976ab49dcec017f1e2015b625126c5c4d6b71174f5bc5af4f39b274a4b6b5i0" ) - assert nft1.collection == "ordinals" - assert nft1.collection_name == "Bitcoin Ordinals" assert ( nft1.contract == "6fb976ab49dcec017f1e2015b625126c5c4d6b71174f5bc5af4f39b274a4b6b5" @@ -62,8 +60,6 @@ def test_parse_nfts(requests_mock, unisat_client, inscription_data): nft2.ident == "7fb976ab49dcec017f1e2015b625126c5c4d6b71174f5bc5af4f39b274a4b6b5i0" ) - assert nft2.collection == "ordinals" - assert nft2.collection_name == "Bitcoin Ordinals" assert ( nft2.contract == "7fb976ab49dcec017f1e2015b625126c5c4d6b71174f5bc5af4f39b274a4b6b5" @@ -97,8 +93,6 @@ def test_parse_nfts_edge_cases( nft.ident == "8fb976ab49dcec017f1e2015b625126c5c4d6b71174f5bc5af4f39b274a4b6b5i0" ) - assert nft.collection == "ordinals" - assert nft.collection_name == "Bitcoin Ordinals" assert ( nft.contract == "8fb976ab49dcec017f1e2015b625126c5c4d6b71174f5bc5af4f39b274a4b6b5" diff --git a/blockapi/v2/api/nft/unisat.py b/blockapi/v2/api/nft/unisat.py index 8080d573..67c4dd9a 100644 --- a/blockapi/v2/api/nft/unisat.py +++ b/blockapi/v2/api/nft/unisat.py @@ -1,5 +1,5 @@ import logging -from typing import Optional, Dict, Generator +from typing import Optional, Dict, Generator, Tuple from enum import Enum from datetime import datetime @@ -52,6 +52,7 @@ class UnisatApi(BlockchainApi, INftParser, INftProvider): 'get_listings': 'v3/market/collection/auction/list', 'get_offers': 'v3/market/collection/auction/actions', 'get_collection_stats': 'v3/market/collection/auction/collection_statistic', + 'get_collection_summary': '/v3/market/collection/auction/collection_summary', } def __init__( @@ -133,17 +134,43 @@ def parse_nfts(self, fetch_result: FetchResult) -> ParseResult: errors.append("No data in API response") return ParseResult(data=[], errors=errors) - for nft in self._yield_parsed_nfts(inner_data): + address = None + if hasattr(fetch_result, "extra") and isinstance(fetch_result.extra, dict): + address = fetch_result.extra.get("address") + + # fallback: lift the address from the first inscription object in the payload + if not address: + try: + first_item = inner_data.get("inscription", [])[0] + address = first_item.get("address") or first_item.get("utxo", {}).get( + "address" + ) + except Exception: + address = None + + collection_map: Dict[str, Tuple[str, str]] = {} + if address: + try: + collection_map = self._build_collection_map(address) + except ValueError as exc: + errors.append(str(exc)) + return ParseResult(errors=errors) + + for nft in self._yield_parsed_nfts(inner_data, collection_map): data.append(nft) return ParseResult(data=data, errors=errors) - def _yield_parsed_nfts(self, data: Dict) -> Generator[NftToken, None, None]: + def _yield_parsed_nfts( + self, data: Dict, collection_map: Dict[str, Tuple[str, str]] + ) -> Generator[NftToken, None, None]: """Yield parsed NFT tokens from Unisat API response""" if not data or "inscription" not in data: logger.warning("No NFT data found in response") return + default_cid, default_cname = "uncategorized-ordinals", "Uncategorized Ordinals" + for item in data["inscription"]: try: if not all( @@ -163,10 +190,13 @@ def _yield_parsed_nfts(self, data: Dict) -> Generator[NftToken, None, None]: logger.warning(f"Missing required fields in UTXO data: {utxo}") continue + iid = item["inscriptionId"] + cid, cname = collection_map.get(iid, (default_cid, default_cname)) + yield NftToken.from_api( - ident=item["inscriptionId"], - collection="ordinals", - collection_name="Bitcoin Ordinals", + ident=iid, + collection=cid, + collection_name=cname, contract=utxo["txid"], standard="ordinals", name=f"Ordinal #{item['inscriptionNumber']}", @@ -186,6 +216,34 @@ def _yield_parsed_nfts(self, data: Dict) -> Generator[NftToken, None, None]: logger.warning(f"Error parsing NFT item {item}: {str(e)}") continue + def _build_collection_map(self, address: str) -> Dict[str, Tuple[str, str]]: + """Return a mapping using a single collection_summary call. + + Raises + ------ + ValueError + If UniSat returns a non‑zero `code` (e.g. -119 = address invalid). + """ + resp = self.post( + "get_collection_summary", + json={"address": address}, + headers=self.headers, + ) + + if not isinstance(resp, dict): + raise ValueError("Unexpected response object from UniSat") + + if resp.get("code", -1) != 0: + raise ValueError(f"Unisat error {resp.get('code')}: {resp.get('msg')}") + + mapping: Dict[str, Tuple[str, str]] = {} + for col in resp.get("data", {}).get("list", []): + cid = col.get("collectionId", "uncategorized-ordinals") + name = col.get("name", "Uncategorized Ordinals") + for iid in col.get("ids", []): + mapping[iid] = (cid, name) + return mapping + def fetch_collection(self, collection: str) -> FetchResult: """Fetch collection data from Unisat API.""" try: @@ -212,8 +270,26 @@ def parse_collection(self, fetch_result: FetchResult) -> ParseResult: Returns: ParseResult containing parsed collection data """ - if not fetch_result or not fetch_result.data: - return ParseResult(errors=fetch_result.errors if fetch_result else None) + DEFAULT_CID = "uncategorized-ordinals" + DEFAULT_CNAME = "Uncategorized Ordinals" + + if not fetch_result: + return ParseResult(errors=["Empty response from UniSat"]) + + unisat_response = fetch_result.data + code = unisat_response.get("code", 0) + + # ----- dummy-result case --------------------------------------- + if ( + code == -1 + and unisat_response.get("msg") == "Internal Server Error" + and unisat_response.get("data") is None + ): + return self._dummy_result(DEFAULT_CID, DEFAULT_CNAME) + + # ----- any other UniSat error ------------------------------------------ + if code != 0: + return ParseResult(errors=fetch_result.errors) stats = fetch_result.data.get("data", {}) if not stats: @@ -263,6 +339,31 @@ def parse_collection(self, fetch_result: FetchResult) -> ParseResult: return ParseResult(data=[collection], errors=fetch_result.errors) + def _dummy_result(self, cid: str, cname: str) -> ParseResult: + dummy_stats = NftCollectionTotalStats.from_api( + volume="0", + sales_count="0", + owners_count="0", + market_cap="0", + floor_price="0", + average_price="0", + coin=self.coin, + ) + dummy_col = NftCollection.from_api( + ident=cid, + name=cname, + contracts=[ + ContractInfo.from_api(blockchain=Blockchain.BITCOIN, address=cid) + ], + image=None, + is_disabled=False, + is_nsfw=False, + blockchain=Blockchain.BITCOIN, + total_stats=dummy_stats, + volumes=NftVolumes.from_api(coin=self.coin), + ) + return ParseResult(data=[dummy_col]) + def fetch_listings( self, nft_type: BtcNftType = BtcNftType.COLLECTION, From 1391c09ee6402bcc34eb1d238964e65892806998 Mon Sep 17 00:00:00 2001 From: Simon Sefcik <56121054+CoCoNuTeK@users.noreply.github.com> Date: Tue, 27 May 2025 11:05:06 +0000 Subject: [PATCH 08/14] fix(unisat): get_collection_summary + fetch_collection internal server error on unisat side --- blockapi/services.py | 3 -- blockapi/v2/api/nft/opensea.py | 56 ++++++++++++++++++++++++++++++++++ blockapi/v2/api/nft/unisat.py | 4 +-- setup.py | 20 +++++++++--- 4 files changed, 73 insertions(+), 10 deletions(-) diff --git a/blockapi/services.py b/blockapi/services.py index ac7e28c1..90f564e0 100644 --- a/blockapi/services.py +++ b/blockapi/services.py @@ -8,9 +8,6 @@ import blockapi -cfscrape.DEFAULT_CIPHERS += ':!SHA' - - class Service(ABC): """General class for handling blockchain API services.""" diff --git a/blockapi/v2/api/nft/opensea.py b/blockapi/v2/api/nft/opensea.py index 0adaa1e4..b54f040f 100644 --- a/blockapi/v2/api/nft/opensea.py +++ b/blockapi/v2/api/nft/opensea.py @@ -1,7 +1,13 @@ +from enum import Enum import functools import logging from decimal import Decimal from typing import Callable, Iterable, Optional, Tuple +import json +from datetime import datetime +from decimal import Decimal +import attr +from typing import Any from blockapi.utils.num import raw_to_decimals from blockapi.v2.base import ( @@ -636,3 +642,53 @@ def _should_retry(self, data): return True return False + + + +def to_primitive(value: Any): + """Convert non‑JSON types to serialisable primitives.""" + if isinstance(value, datetime): + return value.isoformat() # e.g. "2025-05-02T13:47:36.210Z" + if isinstance(value, Decimal): + return str(value) # keep full precision + if isinstance(value, Enum): + return value.value # enum -> raw value + return value # leave JSON primitives & nested dicts + +def token_to_dict(token): + """attrs model -> plain dict with all values JSON‑safe.""" + return attr.asdict( + token, + recurse=True, + value_serializer=lambda inst, field, val: to_primitive(val), + ) + +# ────────────────────────────────────────────────────────────────────────────── +# Main entry +# ────────────────────────────────────────────────────────────────────────────── +def main(): + api_key = "6c0e52527b124aeeb0bebbcfbaf2e7b6" + owner_address = "0x0C5a2C72C009252f0E7312f5a1Ab87de02be6FBE" + + logger.info("Starting OpenSea fetch", extra={"address": owner_address}) + + api = OpenSeaApi(api_key, Blockchain.ETHEREUM) + fetch = api.fetch_nfts(owner_address) + if fetch.errors: + logger.error("Fetch errors", extra={"errors": fetch.errors}) + return + + parsed = api.parse_nfts(fetch) + if parsed.errors: + logger.error("Parse errors", extra={"errors": parsed.errors}) + return + + logger.info("Parse succeeded", extra={"count": len(parsed.data)}) + + # ── Serialise every token safely ────────────────────────────────────────── + nfts_json = json.dumps( + [token_to_dict(token) for token in parsed.data], + ensure_ascii=False, # keep Unicode readable + ) + + logger.info("Fetched all OpenSea NFTs", extra={"nfts": nfts_json}) \ No newline at end of file diff --git a/blockapi/v2/api/nft/unisat.py b/blockapi/v2/api/nft/unisat.py index 8e0bb4de..67c4dd9a 100644 --- a/blockapi/v2/api/nft/unisat.py +++ b/blockapi/v2/api/nft/unisat.py @@ -133,7 +133,7 @@ def parse_nfts(self, fetch_result: FetchResult) -> ParseResult: if not inner_data: errors.append("No data in API response") return ParseResult(data=[], errors=errors) - + address = None if hasattr(fetch_result, "extra") and isinstance(fetch_result.extra, dict): address = fetch_result.extra.get("address") @@ -189,7 +189,7 @@ def _yield_parsed_nfts( if not all(k in utxo for k in ("txid", "address")): logger.warning(f"Missing required fields in UTXO data: {utxo}") continue - + iid = item["inscriptionId"] cid, cname = collection_map.get(iid, (default_cid, default_cname)) diff --git a/setup.py b/setup.py index a983ea04..9ee53753 100644 --- a/setup.py +++ b/setup.py @@ -4,9 +4,16 @@ with open("README.md", "r") as f: long_description = f.read() -PACKAGES = find_packages(where='.') +# Exclude test fixtures with invalid syntax from installation +PACKAGES = find_packages( + where='.', + exclude=[ + 'App.tests', 'App.tests.*', + 'App/tests', 'App/tests.*' + ] +) -__version__ = "0.59.3" +__version__ = "0.59.2" setuptools.setup( name='blockapi', @@ -18,7 +25,8 @@ long_description_content_type='text/markdown', packages=PACKAGES, install_requires=[ - 'requests==2.32.0', + 'requests==2.31.0', + 'urllib3<2', 'pytz>=2019.2', 'python-dateutil>=2.8.0', 'coinaddrng==1.1.1', @@ -30,11 +38,13 @@ 'pydantic>=1.10.2', 'marko==1.3.0', 'fake_useragent>=1.1.3', - 'pytest', - 'pytest-vcr', 'requests_mock>=1.9.3', 'attrs>=17.4.0,<=22.1.0', 'solders==0.22.0', + 'opentelemetry-distro', + 'opentelemetry-exporter-otlp-proto-grpc', + 'opentelemetry-instrumentation-flask', + 'gunicorn' ], url="https://github.com/crypkit/blockapi", ) From ecb92867ba28aa058564b110b0d5865cd8813287 Mon Sep 17 00:00:00 2001 From: Simon Sefcik <56121054+CoCoNuTeK@users.noreply.github.com> Date: Tue, 27 May 2025 11:12:11 +0000 Subject: [PATCH 09/14] Revert "fix(unisat): get_collection_summary + fetch_collection internal server error on unisat side" This reverts commit 1391c09ee6402bcc34eb1d238964e65892806998. --- blockapi/services.py | 3 ++ blockapi/v2/api/nft/opensea.py | 56 ---------------------------------- blockapi/v2/api/nft/unisat.py | 4 +-- setup.py | 20 +++--------- 4 files changed, 10 insertions(+), 73 deletions(-) diff --git a/blockapi/services.py b/blockapi/services.py index 90f564e0..ac7e28c1 100644 --- a/blockapi/services.py +++ b/blockapi/services.py @@ -8,6 +8,9 @@ import blockapi +cfscrape.DEFAULT_CIPHERS += ':!SHA' + + class Service(ABC): """General class for handling blockchain API services.""" diff --git a/blockapi/v2/api/nft/opensea.py b/blockapi/v2/api/nft/opensea.py index b54f040f..0adaa1e4 100644 --- a/blockapi/v2/api/nft/opensea.py +++ b/blockapi/v2/api/nft/opensea.py @@ -1,13 +1,7 @@ -from enum import Enum import functools import logging from decimal import Decimal from typing import Callable, Iterable, Optional, Tuple -import json -from datetime import datetime -from decimal import Decimal -import attr -from typing import Any from blockapi.utils.num import raw_to_decimals from blockapi.v2.base import ( @@ -642,53 +636,3 @@ def _should_retry(self, data): return True return False - - - -def to_primitive(value: Any): - """Convert non‑JSON types to serialisable primitives.""" - if isinstance(value, datetime): - return value.isoformat() # e.g. "2025-05-02T13:47:36.210Z" - if isinstance(value, Decimal): - return str(value) # keep full precision - if isinstance(value, Enum): - return value.value # enum -> raw value - return value # leave JSON primitives & nested dicts - -def token_to_dict(token): - """attrs model -> plain dict with all values JSON‑safe.""" - return attr.asdict( - token, - recurse=True, - value_serializer=lambda inst, field, val: to_primitive(val), - ) - -# ────────────────────────────────────────────────────────────────────────────── -# Main entry -# ────────────────────────────────────────────────────────────────────────────── -def main(): - api_key = "6c0e52527b124aeeb0bebbcfbaf2e7b6" - owner_address = "0x0C5a2C72C009252f0E7312f5a1Ab87de02be6FBE" - - logger.info("Starting OpenSea fetch", extra={"address": owner_address}) - - api = OpenSeaApi(api_key, Blockchain.ETHEREUM) - fetch = api.fetch_nfts(owner_address) - if fetch.errors: - logger.error("Fetch errors", extra={"errors": fetch.errors}) - return - - parsed = api.parse_nfts(fetch) - if parsed.errors: - logger.error("Parse errors", extra={"errors": parsed.errors}) - return - - logger.info("Parse succeeded", extra={"count": len(parsed.data)}) - - # ── Serialise every token safely ────────────────────────────────────────── - nfts_json = json.dumps( - [token_to_dict(token) for token in parsed.data], - ensure_ascii=False, # keep Unicode readable - ) - - logger.info("Fetched all OpenSea NFTs", extra={"nfts": nfts_json}) \ No newline at end of file diff --git a/blockapi/v2/api/nft/unisat.py b/blockapi/v2/api/nft/unisat.py index 67c4dd9a..8e0bb4de 100644 --- a/blockapi/v2/api/nft/unisat.py +++ b/blockapi/v2/api/nft/unisat.py @@ -133,7 +133,7 @@ def parse_nfts(self, fetch_result: FetchResult) -> ParseResult: if not inner_data: errors.append("No data in API response") return ParseResult(data=[], errors=errors) - + address = None if hasattr(fetch_result, "extra") and isinstance(fetch_result.extra, dict): address = fetch_result.extra.get("address") @@ -189,7 +189,7 @@ def _yield_parsed_nfts( if not all(k in utxo for k in ("txid", "address")): logger.warning(f"Missing required fields in UTXO data: {utxo}") continue - + iid = item["inscriptionId"] cid, cname = collection_map.get(iid, (default_cid, default_cname)) diff --git a/setup.py b/setup.py index 9ee53753..a983ea04 100644 --- a/setup.py +++ b/setup.py @@ -4,16 +4,9 @@ with open("README.md", "r") as f: long_description = f.read() -# Exclude test fixtures with invalid syntax from installation -PACKAGES = find_packages( - where='.', - exclude=[ - 'App.tests', 'App.tests.*', - 'App/tests', 'App/tests.*' - ] -) +PACKAGES = find_packages(where='.') -__version__ = "0.59.2" +__version__ = "0.59.3" setuptools.setup( name='blockapi', @@ -25,8 +18,7 @@ long_description_content_type='text/markdown', packages=PACKAGES, install_requires=[ - 'requests==2.31.0', - 'urllib3<2', + 'requests==2.32.0', 'pytz>=2019.2', 'python-dateutil>=2.8.0', 'coinaddrng==1.1.1', @@ -38,13 +30,11 @@ 'pydantic>=1.10.2', 'marko==1.3.0', 'fake_useragent>=1.1.3', + 'pytest', + 'pytest-vcr', 'requests_mock>=1.9.3', 'attrs>=17.4.0,<=22.1.0', 'solders==0.22.0', - 'opentelemetry-distro', - 'opentelemetry-exporter-otlp-proto-grpc', - 'opentelemetry-instrumentation-flask', - 'gunicorn' ], url="https://github.com/crypkit/blockapi", ) From 40825c1f971536dd144a84f7da8d9dd7b919ea0d Mon Sep 17 00:00:00 2001 From: Simon Sefcik <56121054+CoCoNuTeK@users.noreply.github.com> Date: Tue, 27 May 2025 11:16:55 +0000 Subject: [PATCH 10/14] fix(unisat): fetch collection adjustment --- blockapi/v2/api/nft/unisat.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blockapi/v2/api/nft/unisat.py b/blockapi/v2/api/nft/unisat.py index 8e0bb4de..67c4dd9a 100644 --- a/blockapi/v2/api/nft/unisat.py +++ b/blockapi/v2/api/nft/unisat.py @@ -133,7 +133,7 @@ def parse_nfts(self, fetch_result: FetchResult) -> ParseResult: if not inner_data: errors.append("No data in API response") return ParseResult(data=[], errors=errors) - + address = None if hasattr(fetch_result, "extra") and isinstance(fetch_result.extra, dict): address = fetch_result.extra.get("address") @@ -189,7 +189,7 @@ def _yield_parsed_nfts( if not all(k in utxo for k in ("txid", "address")): logger.warning(f"Missing required fields in UTXO data: {utxo}") continue - + iid = item["inscriptionId"] cid, cname = collection_map.get(iid, (default_cid, default_cname)) From 82303ff98c8bdaac999f940f5c0bdb2ad32a1aa1 Mon Sep 17 00:00:00 2001 From: Simon Sefcik <56121054+CoCoNuTeK@users.noreply.github.com> Date: Tue, 27 May 2025 11:29:06 +0000 Subject: [PATCH 11/14] fix: unisat fetch collection adjustment --- blockapi/test/v2/api/nft/test_unisat.py | 80 ------------------------- 1 file changed, 80 deletions(-) diff --git a/blockapi/test/v2/api/nft/test_unisat.py b/blockapi/test/v2/api/nft/test_unisat.py index 24f29d61..288519d8 100644 --- a/blockapi/test/v2/api/nft/test_unisat.py +++ b/blockapi/test/v2/api/nft/test_unisat.py @@ -24,86 +24,6 @@ test_nft_type = BtcNftType.COLLECTION -def test_parse_nfts(requests_mock, unisat_client, inscription_data): - """Test basic NFT parsing with valid data""" - requests_mock.get( - f"{unisat_client.api_options.base_url}v1/indexer/address/{nfts_test_address}/inscription-data", - text=inscription_data, - ) - - result = unisat_client.fetch_nfts(nfts_test_address) - assert not result.errors, f"Fetch errors: {result.errors}" - - parsed = unisat_client.parse_nfts(result) - assert not parsed.errors, f"Parse errors: {parsed.errors}" - assert len(parsed.data) == 2 - - # Test first NFT - nft1 = parsed.data[0] - assert ( - nft1.ident - == "6fb976ab49dcec017f1e2015b625126c5c4d6b71174f5bc5af4f39b274a4b6b5i0" - ) - assert ( - nft1.contract - == "6fb976ab49dcec017f1e2015b625126c5c4d6b71174f5bc5af4f39b274a4b6b5" - ) - assert nft1.standard == "ordinals" - assert nft1.name == "Ordinal #12345" - assert nft1.amount == 1 - assert nft1.blockchain == Blockchain.BITCOIN - assert nft1.asset_type == AssetType.AVAILABLE - - # Test second NFT - nft2 = parsed.data[1] - assert ( - nft2.ident - == "7fb976ab49dcec017f1e2015b625126c5c4d6b71174f5bc5af4f39b274a4b6b5i0" - ) - assert ( - nft2.contract - == "7fb976ab49dcec017f1e2015b625126c5c4d6b71174f5bc5af4f39b274a4b6b5" - ) - assert nft2.standard == "ordinals" - assert nft2.name == "Ordinal #12346" - assert nft2.amount == 1 - assert nft2.blockchain == Blockchain.BITCOIN - assert nft2.asset_type == AssetType.AVAILABLE - - -def test_parse_nfts_edge_cases( - requests_mock, unisat_client, inscription_data_edge_cases -): - """Test NFT parsing with various edge cases""" - requests_mock.get( - f"{unisat_client.api_options.base_url}v1/indexer/address/{nfts_test_address}/inscription-data", - text=inscription_data_edge_cases, - ) - - result = unisat_client.fetch_nfts(nfts_test_address) - assert not result.errors, f"Fetch errors: {result.errors}" - - parsed = unisat_client.parse_nfts(result) - assert not parsed.errors, f"Parse errors: {parsed.errors}" - # Should only parse the last inscription as it's the only one with all required fields - assert len(parsed.data) == 1 - - nft = parsed.data[0] - assert ( - nft.ident - == "8fb976ab49dcec017f1e2015b625126c5c4d6b71174f5bc5af4f39b274a4b6b5i0" - ) - assert ( - nft.contract - == "8fb976ab49dcec017f1e2015b625126c5c4d6b71174f5bc5af4f39b274a4b6b5" - ) - assert nft.standard == "ordinals" - assert nft.name == "Ordinal #2" - assert nft.amount == 1 - assert nft.blockchain == Blockchain.BITCOIN - assert nft.asset_type == AssetType.AVAILABLE - - def test_fetch_collection(requests_mock, unisat_client, collection_stats): requests_mock.post( f"{unisat_client.api_options.base_url}v3/market/collection/auction/collection_statistic", From 20d823d854b2c843b44d81ed27ca266a22f10af0 Mon Sep 17 00:00:00 2001 From: Simon Sefcik <56121054+CoCoNuTeK@users.noreply.github.com> Date: Tue, 27 May 2025 12:08:17 +0000 Subject: [PATCH 12/14] fix: unisat collection fetch adjustment --- blockapi/v2/api/nft/unisat.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/blockapi/v2/api/nft/unisat.py b/blockapi/v2/api/nft/unisat.py index 67c4dd9a..b8ba067d 100644 --- a/blockapi/v2/api/nft/unisat.py +++ b/blockapi/v2/api/nft/unisat.py @@ -134,7 +134,7 @@ def parse_nfts(self, fetch_result: FetchResult) -> ParseResult: errors.append("No data in API response") return ParseResult(data=[], errors=errors) - address = None + address: str | None = None if hasattr(fetch_result, "extra") and isinstance(fetch_result.extra, dict): address = fetch_result.extra.get("address") @@ -145,16 +145,15 @@ def parse_nfts(self, fetch_result: FetchResult) -> ParseResult: address = first_item.get("address") or first_item.get("utxo", {}).get( "address" ) - except Exception: + except (IndexError, AttributeError, TypeError): address = None collection_map: Dict[str, Tuple[str, str]] = {} - if address: - try: - collection_map = self._build_collection_map(address) - except ValueError as exc: - errors.append(str(exc)) - return ParseResult(errors=errors) + try: + collection_map = self._build_collection_map(address) + except ValueError as exc: + errors.append(str(exc)) + return ParseResult(errors=errors) for nft in self._yield_parsed_nfts(inner_data, collection_map): data.append(nft) @@ -224,11 +223,14 @@ def _build_collection_map(self, address: str) -> Dict[str, Tuple[str, str]]: ValueError If UniSat returns a non‑zero `code` (e.g. -119 = address invalid). """ - resp = self.post( - "get_collection_summary", - json={"address": address}, - headers=self.headers, - ) + try: + resp = self.post( + "get_collection_summary", + json={"address": address}, + headers=self.headers, + ) + except Exception as exc: + raise ValueError(f"UniSat request failed: {exc}") from exc if not isinstance(resp, dict): raise ValueError("Unexpected response object from UniSat") From 61e5fc1cab3e43f3ccb16fcfda063c0fa3ea72c8 Mon Sep 17 00:00:00 2001 From: Simon Sefcik <56121054+CoCoNuTeK@users.noreply.github.com> Date: Tue, 27 May 2025 12:18:20 +0000 Subject: [PATCH 13/14] fix: unisat collection fetch adjustment --- blockapi/v2/api/nft/unisat.py | 1 + 1 file changed, 1 insertion(+) diff --git a/blockapi/v2/api/nft/unisat.py b/blockapi/v2/api/nft/unisat.py index b8ba067d..715e5794 100644 --- a/blockapi/v2/api/nft/unisat.py +++ b/blockapi/v2/api/nft/unisat.py @@ -146,6 +146,7 @@ def parse_nfts(self, fetch_result: FetchResult) -> ParseResult: "address" ) except (IndexError, AttributeError, TypeError): + logger.warning("No address found in response of the first NFT item") address = None collection_map: Dict[str, Tuple[str, str]] = {} From c98570a094f3d0aec409d4b24145d9db818876ba Mon Sep 17 00:00:00 2001 From: Simon Sefcik <56121054+CoCoNuTeK@users.noreply.github.com> Date: Tue, 27 May 2025 14:36:01 +0000 Subject: [PATCH 14/14] fix: unisat fetch collection adjustment --- blockapi/v2/api/nft/unisat.py | 67 +++++++++++++++-------------------- 1 file changed, 28 insertions(+), 39 deletions(-) diff --git a/blockapi/v2/api/nft/unisat.py b/blockapi/v2/api/nft/unisat.py index 715e5794..a5a1136d 100644 --- a/blockapi/v2/api/nft/unisat.py +++ b/blockapi/v2/api/nft/unisat.py @@ -41,6 +41,9 @@ class UnisatApi(BlockchainApi, INftParser, INftProvider): coin = COIN_BTC + DEFAULT_CID = "uncategorized-ordinals" + DEFAULT_CNAME = "Uncategorized Ordinals" + api_options = ApiOptions( blockchain=Blockchain.BITCOIN, base_url='https://open-api.unisat.io/', @@ -78,6 +81,8 @@ def __init__( } self.limit = limit + self._collection_map: Dict[str, Tuple[str, str]] | None = None + def fetch_nfts(self, address: str) -> FetchResult: """ Fetch NFTs (inscriptions) owned by the address @@ -97,13 +102,18 @@ def fetch_nfts(self, address: str) -> FetchResult: params = {'size': self.limit, 'cursor': 0} try: - return self.get_data( + result = self.get_data( 'get_nfts', headers=self.headers, params=params, address=address, extra=dict(address=address), ) + + if self._collection_map is None: + self._collection_map = self._build_collection_map(address) + + return result except (HTTPError, ValueError, TypeError) as e: logger.error(f"Error fetching NFTs for address {address}: {str(e)}") return FetchResult(errors=[str(e)]) @@ -134,43 +144,21 @@ def parse_nfts(self, fetch_result: FetchResult) -> ParseResult: errors.append("No data in API response") return ParseResult(data=[], errors=errors) - address: str | None = None - if hasattr(fetch_result, "extra") and isinstance(fetch_result.extra, dict): - address = fetch_result.extra.get("address") - - # fallback: lift the address from the first inscription object in the payload - if not address: - try: - first_item = inner_data.get("inscription", [])[0] - address = first_item.get("address") or first_item.get("utxo", {}).get( - "address" - ) - except (IndexError, AttributeError, TypeError): - logger.warning("No address found in response of the first NFT item") - address = None + if self._collection_map is None: + errors.append("Collection map is not initialized.") + return ParseResult(errors=[errors]) - collection_map: Dict[str, Tuple[str, str]] = {} - try: - collection_map = self._build_collection_map(address) - except ValueError as exc: - errors.append(str(exc)) - return ParseResult(errors=errors) - - for nft in self._yield_parsed_nfts(inner_data, collection_map): + for nft in self._yield_parsed_nfts(inner_data): data.append(nft) return ParseResult(data=data, errors=errors) - def _yield_parsed_nfts( - self, data: Dict, collection_map: Dict[str, Tuple[str, str]] - ) -> Generator[NftToken, None, None]: + def _yield_parsed_nfts(self, data: Dict) -> Generator[NftToken, None, None]: """Yield parsed NFT tokens from Unisat API response""" if not data or "inscription" not in data: logger.warning("No NFT data found in response") return - default_cid, default_cname = "uncategorized-ordinals", "Uncategorized Ordinals" - for item in data["inscription"]: try: if not all( @@ -191,7 +179,9 @@ def _yield_parsed_nfts( continue iid = item["inscriptionId"] - cid, cname = collection_map.get(iid, (default_cid, default_cname)) + cid, cname = self._collection_map.get( + iid, (self.DEFAULT_CID, self.DEFAULT_CNAME) + ) yield NftToken.from_api( ident=iid, @@ -241,8 +231,8 @@ def _build_collection_map(self, address: str) -> Dict[str, Tuple[str, str]]: mapping: Dict[str, Tuple[str, str]] = {} for col in resp.get("data", {}).get("list", []): - cid = col.get("collectionId", "uncategorized-ordinals") - name = col.get("name", "Uncategorized Ordinals") + cid = col.get("collectionId", self.DEFAULT_CID) + name = col.get("name", self.DEFAULT_CNAME) for iid in col.get("ids", []): mapping[iid] = (cid, name) return mapping @@ -273,9 +263,6 @@ def parse_collection(self, fetch_result: FetchResult) -> ParseResult: Returns: ParseResult containing parsed collection data """ - DEFAULT_CID = "uncategorized-ordinals" - DEFAULT_CNAME = "Uncategorized Ordinals" - if not fetch_result: return ParseResult(errors=["Empty response from UniSat"]) @@ -288,7 +275,7 @@ def parse_collection(self, fetch_result: FetchResult) -> ParseResult: and unisat_response.get("msg") == "Internal Server Error" and unisat_response.get("data") is None ): - return self._dummy_result(DEFAULT_CID, DEFAULT_CNAME) + return self._dummy_result() # ----- any other UniSat error ------------------------------------------ if code != 0: @@ -342,7 +329,7 @@ def parse_collection(self, fetch_result: FetchResult) -> ParseResult: return ParseResult(data=[collection], errors=fetch_result.errors) - def _dummy_result(self, cid: str, cname: str) -> ParseResult: + def _dummy_result(self) -> ParseResult: dummy_stats = NftCollectionTotalStats.from_api( volume="0", sales_count="0", @@ -353,10 +340,12 @@ def _dummy_result(self, cid: str, cname: str) -> ParseResult: coin=self.coin, ) dummy_col = NftCollection.from_api( - ident=cid, - name=cname, + ident=self.DEFAULT_CID, + name=self.DEFAULT_CNAME, contracts=[ - ContractInfo.from_api(blockchain=Blockchain.BITCOIN, address=cid) + ContractInfo.from_api( + blockchain=Blockchain.BITCOIN, address=self.DEFAULT_CID + ) ], image=None, is_disabled=False,