22from typing import Optional , Dict , Generator
33from enum import Enum
44from datetime import datetime
5+ from attr import asdict
6+ import json
57
68from blockapi .v2 .base import BlockchainApi , INftParser , INftProvider , ISleepProvider
79from blockapi .v2 .coins import COIN_BTC
@@ -54,7 +56,7 @@ class UnisatApi(BlockchainApi, INftParser, INftProvider):
5456 'get_collection_stats' : 'v3/market/collection/auction/collection_statistic' ,
5557 }
5658
57- def __init__ (self , api_key : str , sleep_provider : Optional [ISleepProvider ] = None ):
59+ def __init__ (self , api_key : str , sleep_provider : Optional [ISleepProvider ] = None , limit : Optional [ int ] = 10000 ):
5860 """
5961 Initialize the Unisat API client
6062
@@ -70,17 +72,18 @@ def __init__(self, api_key: str, sleep_provider: Optional[ISleepProvider] = None
7072 'Authorization' : f'Bearer { api_key } ' ,
7173 'Content-Type' : 'application/json' ,
7274 }
75+ self .limit = limit
7376
7477 def fetch_nfts (
75- self , address : str , cursor : Optional [int ] = None , size : int = 16
78+ self , address : str , cursor : Optional [str ] = None , size : Optional [ int ] = None
7679 ) -> FetchResult :
7780 """
7881 Fetch NFTs (inscriptions) owned by the address
7982
8083 Args:
8184 address: BTC address to fetch NFTs for
8285 cursor: Pagination cursor (offset)
83- size: Number of items to return per page (default: 16 )
86+ size: Number of items to return per request (default: 100 )
8487
8588 Returns:
8689 FetchResult containing the NFT data
@@ -90,10 +93,14 @@ def fetch_nfts(
9093 """
9194 if not address :
9295 raise ValueError ("Address is required" )
93-
94- params = {'size' : size }
95- if cursor is not None :
96- params ['cursor' ] = cursor
96+
97+ # set size to self.limit based on the following heuristic:
98+ # 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.
99+ # for simplicity, we will always set size to self.limit
100+ size = self .limit
101+
102+ # allow pagination cursor as string or int, convert to int, and set to 0 to avoid skipping any NFTS
103+ params = {'size' : size , 'cursor' : 0 }
97104
98105 try :
99106 return self .get_data (
@@ -116,76 +123,73 @@ def parse_nfts(self, fetch_result: FetchResult) -> ParseResult:
116123 """Parse NFT data from API response"""
117124 errors = []
118125 data = []
119- cursor = None
120126
121127 if not fetch_result .data :
122128 errors .append ("No data in fetch result" )
123129 return ParseResult (data = [], errors = errors )
124130
131+ if isinstance (fetch_result .data , dict ) and "code" in fetch_result .data :
132+ api_code = fetch_result .data ["code" ]
133+ if api_code != 0 :
134+ api_msg = fetch_result .data .get ("msg" , "Unknown error" )
135+ errors .append (f"Unisat error { api_code } : { api_msg } " )
136+ return ParseResult (data = [], errors = errors )
137+
125138 inner_data = fetch_result .data .get ("data" , {})
126139 if not inner_data :
127140 errors .append ("No data in API response" )
128141 return ParseResult (data = [], errors = errors )
129142
130- cursor = (
131- str (inner_data .get ("cursor" ))
132- if inner_data .get ("cursor" ) is not None
133- else None
134- )
135-
136143 for nft in self ._yield_parsed_nfts (inner_data ):
137144 data .append (nft )
138145
139- return ParseResult (data = data , errors = errors , cursor = cursor )
146+ return ParseResult (data = data , errors = errors , cursor = None )
140147
141148 def _yield_parsed_nfts (self , data : Dict ) -> Generator [NftToken , None , None ]:
142- """Yield parsed NFT tokens from API response data """
149+ """Yield parsed NFT tokens from Unisat API response"""
143150 if not data or "inscription" not in data :
144151 return
145152
146153 for item in data ["inscription" ]:
147154 try :
148155 if not all (
149156 k in item
150- for k in [
157+ for k in (
151158 "inscriptionId" ,
152159 "inscriptionNumber" ,
153160 "timestamp" ,
154161 "utxo" ,
155- ]
162+ )
156163 ):
157- logger .warning (f "Missing required fields in NFT data: { item } " )
164+ logger .warning ("Missing required fields in NFT data: %s" , item )
158165 continue
159166
160167 utxo = item ["utxo" ]
161- if not all (k in utxo for k in [ "txid" , "address" ] ):
162- logger .warning (f "Missing required fields in UTXO data: { utxo } " )
168+ if not all (k in utxo for k in ( "txid" , "address" ) ):
169+ logger .warning ("Missing required fields in UTXO data: %s" , utxo )
163170 continue
164171
165- inscription_number = str (item ["inscriptionNumber" ])
166- timestamp = str (item ["timestamp" ])
167-
168- yield NftToken (
172+ yield NftToken .from_api (
169173 ident = item ["inscriptionId" ],
170174 collection = "ordinals" ,
171175 collection_name = "Bitcoin Ordinals" ,
172176 contract = utxo ["txid" ],
173177 standard = "ordinals" ,
174- name = f"Ordinal #{ inscription_number } " ,
178+ name = f"Ordinal #{ item [ 'inscriptionNumber' ] } " ,
175179 description = "" ,
176180 amount = 1 ,
177181 image_url = "" ,
178182 metadata_url = None ,
179- metadata = {},
180- updated_time = int (timestamp ),
183+ updated_time = str (item ["timestamp" ]),
181184 is_disabled = False ,
182185 is_nsfw = False ,
183186 blockchain = Blockchain .BITCOIN ,
184187 asset_type = AssetType .AVAILABLE ,
185188 market_url = None ,
186189 )
190+
187191 except Exception as e :
188- logger .warning (f "Error parsing NFT item { item } : { e } " )
192+ logger .warning ("Error parsing NFT item %s: %s" , item , e )
189193 continue
190194
191195 def fetch_collection (self , collection : str ) -> FetchResult :
@@ -269,8 +273,8 @@ def fetch_listings(
269273 self ,
270274 nft_type : BtcNftType = BtcNftType .COLLECTION ,
271275 collection : Optional [str ] = None ,
272- cursor : Optional [str ] = None ,
273- limit : int = 100 ,
276+ cursor : Optional [str ] = 0 ,
277+ limit : int = 499 ,
274278 address : Optional [str ] = None ,
275279 tick : Optional [str ] = None ,
276280 min_price : Optional [int ] = None ,
@@ -319,8 +323,6 @@ def fetch_listings(
319323 nft_type .value if isinstance (nft_type , BtcNftType ) else str (nft_type )
320324 )
321325
322- start = int (cursor ) if cursor else 0
323-
324326 filter_dict = {"nftType" : nft_type_str }
325327
326328 if collection :
@@ -355,10 +357,16 @@ def fetch_listings(
355357 sort_dict = {}
356358 sort_dict [sort_by ] = sort_order
357359
360+ if limit >= 500 :
361+ logger .warning (
362+ f"Unisat API limit is 500. You tried to fetch { limit } items. Truncating to 499."
363+ )
364+ limit = 499
365+
358366 request_body = {
359367 "filter" : filter_dict ,
360368 "sort" : sort_dict ,
361- "start" : start ,
369+ "start" : 0 ,
362370 "limit" : limit ,
363371 }
364372
@@ -394,12 +402,10 @@ def parse_listings(self, fetch_result: FetchResult) -> ParseResult:
394402 return ParseResult (errors = fetch_result .errors )
395403
396404 items = inner_data .get ("list" , [])
397- timestamp = inner_data .get ("timestamp" )
398- cursor = str (timestamp ) if timestamp else None
399405
400406 return ParseResult (
401407 data = list (self ._yield_parsed_listings (items )),
402- cursor = cursor ,
408+ cursor = None ,
403409 errors = fetch_result .errors ,
404410 )
405411
@@ -457,8 +463,8 @@ def fetch_offers(
457463 tick : Optional [str ] = None ,
458464 domain_type : Optional [str ] = None ,
459465 collection : Optional [str ] = None ,
460- cursor : Optional [str ] = None ,
461- limit : int = 100 ,
466+ cursor : Optional [str ] = 0 ,
467+ limit : int = 499 ,
462468 ) -> FetchResult :
463469 """
464470 Fetch listing events (historical or recent) in a collection.
@@ -472,7 +478,7 @@ def fetch_offers(
472478 domain_type: Filter by domain type
473479 collection: Collection ID to filter by
474480 cursor: Pagination cursor (offset, 'start' parameter)
475- limit: Number of items per page
481+ limit: Number of items
476482
477483 Returns:
478484 FetchResult containing the listing action data
@@ -482,8 +488,6 @@ def fetch_offers(
482488 nft_type .value if isinstance (nft_type , BtcNftType ) else str (nft_type )
483489 )
484490
485- start = int (cursor ) if cursor else 0
486-
487491 filter_dict = {}
488492 if nft_type_str :
489493 filter_dict ["nftType" ] = nft_type_str
@@ -500,9 +504,15 @@ def fetch_offers(
500504 if collection :
501505 filter_dict ["collectionId" ] = collection
502506
507+ if limit >= 500 :
508+ logger .warning (
509+ f"Unisat API limit is 500. You tried to fetch { limit } items. Truncating to 499."
510+ )
511+ limit = 499
512+
503513 request_body = {
504514 "filter" : filter_dict ,
505- "start" : start ,
515+ "start" : 0 ,
506516 "limit" : limit ,
507517 }
508518
@@ -603,4 +613,4 @@ def _yield_parsed_offers(
603613 pay_ident = None ,
604614 pay_amount = price ,
605615 pay_coin = self .coin ,
606- )
616+ )
0 commit comments