Skip to content

Commit 68fcd4e

Browse files
committed
fix: remove Jupiter ban list logic from Solana API, clean up related tests and fixtures
1 parent 4daa034 commit 68fcd4e

3 files changed

Lines changed: 86 additions & 93 deletions

File tree

blockapi/test/v2/api/data/solana/ban-list-jup-ag.csv

Lines changed: 0 additions & 42 deletions
This file was deleted.

blockapi/test/v2/api/test_solana.py

Lines changed: 68 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
from blockapi.test.v2.api.conftest import read_file
88
from blockapi.v2.api import SolanaApi, SolscanApi
9-
from blockapi.v2.api.solana import JUP_AG_BAN_LIST_URL
109
from blockapi.v2.models import (
1110
AssetType,
1211
BalanceItem,
@@ -20,10 +19,8 @@
2019
@pytest.fixture(autouse=True)
2120
def _reset_caches():
2221
SolanaApi._das_cache = {}
23-
SolanaApi._ban_list = set()
2422
yield
2523
SolanaApi._das_cache = {}
26-
SolanaApi._ban_list = set()
2724

2825

2926
def test_merge_balances_with_different_coins(solana_api, balances_with_different_coins):
@@ -82,7 +79,6 @@ def test_use_base_url_in_post(
8279
token_accounts_response,
8380
das_asset_batch_response,
8481
staked_solana_response,
85-
ban_list_jup_ag_response,
8682
):
8783
test_addr = '5PjMxaijeVVQtuEzxK2NxyJeWwUbpTsi2uXuZ653WoHu'
8884
empty_token_accounts = '{"jsonrpc":"2.0","result":{"context":{"apiVersion":"1.17.34","slot":268207149},"value":[]},"id":1}'
@@ -103,7 +99,6 @@ def get_text(*args, **kwargs):
10399
return data
104100

105101
with Mocker() as m:
106-
m.get(JUP_AG_BAN_LIST_URL, text=ban_list_jup_ag_response)
107102
m.post(ANY, text=get_text),
108103
api = SolanaApi(base_url='https://proxy/solana/')
109104
api.get_balance(test_addr)
@@ -189,6 +184,74 @@ def test_nft_included_when_include_nfts_true():
189184
assert 'V1_NFT' in coin.standards
190185

191186

187+
def test_parse_staked_balance_skips_undelegated():
188+
api = SolanaApi()
189+
response = {
190+
'result': [
191+
# Delegated account with stake
192+
{
193+
'account': {
194+
'lamports': 2282880,
195+
'data': {
196+
'parsed': {
197+
'info': {
198+
'stake': {
199+
'delegation': {
200+
'stake': '1000000000',
201+
}
202+
}
203+
}
204+
}
205+
},
206+
}
207+
},
208+
# Undelegated account: stake key is null
209+
{
210+
'account': {
211+
'lamports': 2282880,
212+
'data': {
213+
'parsed': {
214+
'info': {
215+
'stake': None,
216+
}
217+
}
218+
},
219+
}
220+
},
221+
# Undelegated account: stake key absent
222+
{
223+
'account': {
224+
'lamports': 2282880,
225+
'data': {
226+
'parsed': {
227+
'info': {},
228+
}
229+
},
230+
}
231+
},
232+
]
233+
}
234+
result = api._parse_staked_balance(response)
235+
assert result is not None
236+
assert result.balance_raw == 1000000000
237+
assert result.asset_type == AssetType.STAKED
238+
239+
240+
def test_das_cache_stores_sentinel_for_unknown_mint():
241+
api = SolanaApi()
242+
unknown_mint = 'UnknownMint111111111111111111111111111111111'
243+
244+
with patch.object(
245+
api,
246+
'_request',
247+
return_value={'result': []},
248+
):
249+
api._fetch_das_assets([unknown_mint])
250+
251+
assert unknown_mint in api._das_cache
252+
assert api._das_cache[unknown_mint] == {}
253+
254+
192255
def test_das_cache_prevents_refetch():
193256
api = SolanaApi()
194257
# Pre-populate cache
@@ -381,11 +444,6 @@ def staked_solana_response():
381444
return read_file('data/solana/staked_solana_response.json')
382445

383446

384-
@pytest.fixture
385-
def ban_list_jup_ag_response():
386-
return read_file('data/solana/ban-list-jup-ag.csv')
387-
388-
389447
@pytest.fixture
390448
def solana_api():
391449
return SolanaApi()

blockapi/v2/api/solana.py

Lines changed: 18 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import json
22
import logging
3-
import os
43
from typing import Optional, Union
54

65
from cytoolz import reduceby
@@ -30,11 +29,6 @@
3029

3130
logger = 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

3933
class 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

Comments
 (0)