11import logging
2- from typing import Optional , Dict , Generator
2+ from typing import Optional , Dict , Generator , Tuple
33from enum import Enum
44from datetime import datetime
55
@@ -41,6 +41,9 @@ class UnisatApi(BlockchainApi, INftParser, INftProvider):
4141
4242 coin = COIN_BTC
4343
44+ DEFAULT_CID = "uncategorized-ordinals"
45+ DEFAULT_CNAME = "Uncategorized Ordinals"
46+
4447 api_options = ApiOptions (
4548 blockchain = Blockchain .BITCOIN ,
4649 base_url = 'https://open-api.unisat.io/' ,
@@ -52,6 +55,7 @@ class UnisatApi(BlockchainApi, INftParser, INftProvider):
5255 'get_listings' : 'v3/market/collection/auction/list' ,
5356 'get_offers' : 'v3/market/collection/auction/actions' ,
5457 'get_collection_stats' : 'v3/market/collection/auction/collection_statistic' ,
58+ 'get_collection_summary' : '/v3/market/collection/auction/collection_summary' ,
5559 }
5660
5761 def __init__ (
@@ -77,6 +81,8 @@ def __init__(
7781 }
7882 self .limit = limit
7983
84+ self ._collection_map : Dict [str , Tuple [str , str ]] | None = None
85+
8086 def fetch_nfts (self , address : str ) -> FetchResult :
8187 """
8288 Fetch NFTs (inscriptions) owned by the address
@@ -96,13 +102,18 @@ def fetch_nfts(self, address: str) -> FetchResult:
96102 params = {'size' : self .limit , 'cursor' : 0 }
97103
98104 try :
99- return self .get_data (
105+ result = self .get_data (
100106 'get_nfts' ,
101107 headers = self .headers ,
102108 params = params ,
103109 address = address ,
104110 extra = dict (address = address ),
105111 )
112+
113+ if self ._collection_map is None :
114+ self ._collection_map = self ._build_collection_map (address )
115+
116+ return result
106117 except (HTTPError , ValueError , TypeError ) as e :
107118 logger .error (f"Error fetching NFTs for address { address } : { str (e )} " )
108119 return FetchResult (errors = [str (e )])
@@ -133,6 +144,10 @@ def parse_nfts(self, fetch_result: FetchResult) -> ParseResult:
133144 errors .append ("No data in API response" )
134145 return ParseResult (data = [], errors = errors )
135146
147+ if self ._collection_map is None :
148+ errors .append ("Collection map is not initialized." )
149+ return ParseResult (errors = [errors ])
150+
136151 for nft in self ._yield_parsed_nfts (inner_data ):
137152 data .append (nft )
138153
@@ -163,10 +178,15 @@ def _yield_parsed_nfts(self, data: Dict) -> Generator[NftToken, None, None]:
163178 logger .warning (f"Missing required fields in UTXO data: { utxo } " )
164179 continue
165180
181+ iid = item ["inscriptionId" ]
182+ cid , cname = self ._collection_map .get (
183+ iid , (self .DEFAULT_CID , self .DEFAULT_CNAME )
184+ )
185+
166186 yield NftToken .from_api (
167- ident = item [ "inscriptionId" ] ,
168- collection = "ordinals" ,
169- collection_name = "Bitcoin Ordinals" ,
187+ ident = iid ,
188+ collection = cid ,
189+ collection_name = cname ,
170190 contract = utxo ["txid" ],
171191 standard = "ordinals" ,
172192 name = f"Ordinal #{ item ['inscriptionNumber' ]} " ,
@@ -186,6 +206,37 @@ def _yield_parsed_nfts(self, data: Dict) -> Generator[NftToken, None, None]:
186206 logger .warning (f"Error parsing NFT item { item } : { str (e )} " )
187207 continue
188208
209+ def _build_collection_map (self , address : str ) -> Dict [str , Tuple [str , str ]]:
210+ """Return a mapping using a single collection_summary call.
211+
212+ Raises
213+ ------
214+ ValueError
215+ If UniSat returns a non‑zero `code` (e.g. -119 = address invalid).
216+ """
217+ try :
218+ resp = self .post (
219+ "get_collection_summary" ,
220+ json = {"address" : address },
221+ headers = self .headers ,
222+ )
223+ except Exception as exc :
224+ raise ValueError (f"UniSat request failed: { exc } " ) from exc
225+
226+ if not isinstance (resp , dict ):
227+ raise ValueError ("Unexpected response object from UniSat" )
228+
229+ if resp .get ("code" , - 1 ) != 0 :
230+ raise ValueError (f"Unisat error { resp .get ('code' )} : { resp .get ('msg' )} " )
231+
232+ mapping : Dict [str , Tuple [str , str ]] = {}
233+ for col in resp .get ("data" , {}).get ("list" , []):
234+ cid = col .get ("collectionId" , self .DEFAULT_CID )
235+ name = col .get ("name" , self .DEFAULT_CNAME )
236+ for iid in col .get ("ids" , []):
237+ mapping [iid ] = (cid , name )
238+ return mapping
239+
189240 def fetch_collection (self , collection : str ) -> FetchResult :
190241 """Fetch collection data from Unisat API."""
191242 try :
@@ -212,8 +263,23 @@ def parse_collection(self, fetch_result: FetchResult) -> ParseResult:
212263 Returns:
213264 ParseResult containing parsed collection data
214265 """
215- if not fetch_result or not fetch_result .data :
216- return ParseResult (errors = fetch_result .errors if fetch_result else None )
266+ if not fetch_result :
267+ return ParseResult (errors = ["Empty response from UniSat" ])
268+
269+ unisat_response = fetch_result .data
270+ code = unisat_response .get ("code" , 0 )
271+
272+ # ----- dummy-result case ---------------------------------------
273+ if (
274+ code == - 1
275+ and unisat_response .get ("msg" ) == "Internal Server Error"
276+ and unisat_response .get ("data" ) is None
277+ ):
278+ return self ._dummy_result ()
279+
280+ # ----- any other UniSat error ------------------------------------------
281+ if code != 0 :
282+ return ParseResult (errors = fetch_result .errors )
217283
218284 stats = fetch_result .data .get ("data" , {})
219285 if not stats :
@@ -263,6 +329,33 @@ def parse_collection(self, fetch_result: FetchResult) -> ParseResult:
263329
264330 return ParseResult (data = [collection ], errors = fetch_result .errors )
265331
332+ def _dummy_result (self ) -> ParseResult :
333+ dummy_stats = NftCollectionTotalStats .from_api (
334+ volume = "0" ,
335+ sales_count = "0" ,
336+ owners_count = "0" ,
337+ market_cap = "0" ,
338+ floor_price = "0" ,
339+ average_price = "0" ,
340+ coin = self .coin ,
341+ )
342+ dummy_col = NftCollection .from_api (
343+ ident = self .DEFAULT_CID ,
344+ name = self .DEFAULT_CNAME ,
345+ contracts = [
346+ ContractInfo .from_api (
347+ blockchain = Blockchain .BITCOIN , address = self .DEFAULT_CID
348+ )
349+ ],
350+ image = None ,
351+ is_disabled = False ,
352+ is_nsfw = False ,
353+ blockchain = Blockchain .BITCOIN ,
354+ total_stats = dummy_stats ,
355+ volumes = NftVolumes .from_api (coin = self .coin ),
356+ )
357+ return ParseResult (data = [dummy_col ])
358+
266359 def fetch_listings (
267360 self ,
268361 nft_type : BtcNftType = BtcNftType .COLLECTION ,
0 commit comments