11import json
22import logging
3- import os
43from typing import Optional , Union
54
65from cytoolz import reduceby
3029
3130logger = logging .getLogger (__name__ )
3231
33- JUP_AG_BAN_LIST_URL = os .getenv (
34- 'BLOCKAPI_JUP_AG_BAN_LIST_URL' ,
35- 'https://raw.githubusercontent.com/jup-ag/token-list/main/banned-tokens.csv' ,
36- )
37-
3832
3933class SolanaApi (CustomizableBlockchainApi , BalanceMixin ):
4034 """Solana JSON-RPC client with DAS metadata integration.
@@ -59,11 +53,10 @@ class SolanaApi(CustomizableBlockchainApi, BalanceMixin):
5953
6054 Caching architecture
6155 --------------------
62- ``_das_cache`` and ``_ban_list`` are **class-level** attributes shared
63- across all instances. This is intentional: DAS metadata and the Jupiter
64- ban list are global and rarely change. Sharing them avoids redundant RPC/HTTP calls
65- when multiple ``SolanaApi`` instances coexist (e.g. one per user request
66- in a web service).
56+ ``_das_cache`` is a **class-level** attribute shared across all instances.
57+ This is intentional: DAS metadata is global and rarely changes. Sharing it
58+ avoids redundant RPC calls when multiple ``SolanaApi`` instances coexist
59+ (e.g. one per user request in a web service).
6760 """
6861
6962 # ── Configuration ──────────────────────────────────────────
@@ -90,9 +83,6 @@ class SolanaApi(CustomizableBlockchainApi, BalanceMixin):
9083 # Class-level cache: shared across instances to avoid redundant DAS RPCs.
9184 _das_cache : dict [str , dict ] = {}
9285
93- # Class-level ban list: shared across instances
94- _ban_list : set = set ()
95-
9686 # ── Initialization ─────────────────────────────────────────
9787
9888 def __init__ (self , base_url : Optional [str ] = None , include_nfts : bool = False ):
@@ -175,15 +165,6 @@ def get_coin(self, fetch_params: tuple[str, int]) -> Coin:
175165 self ._fetch_das_assets ([contract ])
176166 return self ._resolve_coin (contract , decimals )
177167
178- @property
179- def ban_list (self ) -> set [str ]:
180- """Return the set of banned token mints from Jupiter."""
181- if not self ._ban_list :
182- if response := self ._get_from_url (JUP_AG_BAN_LIST_URL ):
183- ban_list = response .text .strip ().split ('\n ' )
184- SolanaApi ._ban_list = set (i .split (',' )[0 ] for i in ban_list [1 :])
185- return self ._ban_list
186-
187168 @staticmethod
188169 def merge_balances_with_same_coin (
189170 token_balances : list [BalanceItem ],
@@ -233,9 +214,6 @@ def _parse_token_balance(self, raw: dict) -> Optional[BalanceItem]:
233214 return None
234215
235216 mint = info .get ('mint' )
236- if mint in self .ban_list :
237- return None
238-
239217 decimals = int (token_amount .get ('decimals' , 0 ))
240218 coin = self ._resolve_coin (mint , decimals )
241219
@@ -262,16 +240,17 @@ def _extract_token_info(account: dict) -> dict:
262240
263241 def _fetch_das_assets (self , mint_addresses : list [str ]) -> None :
264242 """Batch-fetch token metadata via DAS and populate cache."""
265- uncached = [m for m in mint_addresses if m not in self ._das_cache ]
243+ uncached = list (
244+ dict .fromkeys (m for m in mint_addresses if m not in self ._das_cache )
245+ )
266246 if not uncached :
267247 return
268248
269249 for i in range (0 , len (uncached ), self .DAS_BATCH_SIZE ):
270250 chunk = uncached [i : i + self .DAS_BATCH_SIZE ]
271251 try :
272252 response = self ._request (
273- # 'getAssetBatch',
274- 'getAssets' ,
253+ 'getAssetBatch' ,
275254 {'ids' : chunk , 'options' : {'showFungible' : True }},
276255 )
277256 except (ApiException , RequestException ) as e :
@@ -285,6 +264,11 @@ def _fetch_das_assets(self, mint_addresses: list[str]) -> None:
285264 if mint := asset .get ('id' ):
286265 self ._das_cache [mint ] = asset
287266
267+ returned = {asset ['id' ] for asset in results if asset and asset .get ('id' )}
268+ for mint in chunk :
269+ if mint not in self ._das_cache :
270+ self ._das_cache [mint ] = {}
271+
288272 def _build_coin_from_das_asset (self , asset : dict ) -> Optional [Coin ]:
289273 """Build a Coin from a DAS asset response."""
290274 content = asset .get ('content' , {})
@@ -364,7 +348,11 @@ def _parse_staked_balance(self, response: dict) -> Optional[BalanceItem]:
364348 return None
365349
366350 balance_raw = sum (
367- int (r ['account' ]['data' ]['parsed' ]['info' ]['stake' ]['delegation' ]['stake' ])
351+ int (
352+ (r ['account' ]['data' ]['parsed' ]['info' ].get ('stake' ) or {})
353+ .get ('delegation' , {})
354+ .get ('stake' , 0 )
355+ )
368356 for r in response ['result' ]
369357 )
370358
@@ -408,17 +396,6 @@ def _request(self, method: str, params: Union[list, dict]) -> dict:
408396 )
409397 return self .post (body = body , headers = {'Content-Type' : 'application/json' })
410398
411- def _get_from_url (self , url : str ) -> Optional [Response ]:
412- """Perform a GET request to an external URL."""
413- try :
414- response = self ._session .get (url )
415- response .raise_for_status ()
416- except RequestException as e :
417- logger .error (e )
418- return None
419-
420- return response
421-
422399 def _opt_raise_on_other_error (self , response : Response ) -> None :
423400 """Raise ApiException or InvalidAddressException on RPC errors."""
424401 json_response = response .json ()
0 commit comments