Skip to content

Commit 7a2579e

Browse files
authored
fix(unisat): cursor fix
1 parent fa81b72 commit 7a2579e

1 file changed

Lines changed: 54 additions & 44 deletions

File tree

blockapi/v2/api/nft/unisat.py

Lines changed: 54 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
from typing import Optional, Dict, Generator
33
from enum import Enum
44
from datetime import datetime
5+
from attr import asdict
6+
import json
57

68
from blockapi.v2.base import BlockchainApi, INftParser, INftProvider, ISleepProvider
79
from 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

Comments
 (0)