From 2b3b89eab850c878be3ddd857153372772aaa116 Mon Sep 17 00:00:00 2001 From: Fsocietyhhh <1211904451@qq.com> Date: Sun, 14 Jun 2026 14:37:37 -0700 Subject: [PATCH 1/2] fix(solana): auto-load wallet from ~/.blockrun/.solana-session MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SolanaLLMClient resolved its key from `private_key` or the SOLANA_WALLET_KEY env var only — unlike the Base LLMClient, which also falls back to load_wallet() (~/.blockrun/.session). So a host with a Solana wallet session on disk still had to export SOLANA_WALLET_KEY, and the blockrun-litellm sidecar refused to start (its fail-fast wallet check) without it. Add the missing load_solana_wallet() fallback to both SolanaLLMClient and AsyncSolanaLLMClient, mirroring the Base clients. Now the env var is optional when a session file exists. Tests: test_raises_without_key now patches load_solana_wallet -> None so it's deterministic regardless of the host's session file; new test_init_from_session_file covers the fallback. Full unit suite: 256 passed. --- blockrun_llm/solana_client.py | 22 ++++++++++++++++++---- tests/unit/test_solana_client.py | 18 ++++++++++++++---- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/blockrun_llm/solana_client.py b/blockrun_llm/solana_client.py index cd53c13..099930b 100644 --- a/blockrun_llm/solana_client.py +++ b/blockrun_llm/solana_client.py @@ -365,10 +365,17 @@ def __init__( "Solana payment requires the x402 SDK. " "Install with: pip install blockrun-llm[solana]" ) - key = private_key or os.environ.get("SOLANA_WALLET_KEY") + from .solana_wallet import load_solana_wallet + + key = ( + private_key + or os.environ.get("SOLANA_WALLET_KEY") + or load_solana_wallet() # Loads from ~/.blockrun/.solana-session + ) if not key: raise ValueError( - "Private key required. Pass private_key or set SOLANA_WALLET_KEY env var." + "Private key required. Pass private_key, set SOLANA_WALLET_KEY, " + "or create a wallet at ~/.blockrun/.solana-session." ) self._private_key = key validate_api_url(api_url) @@ -1871,10 +1878,17 @@ def __init__( "Solana payment requires the x402 SDK. " "Install with: pip install blockrun-llm[solana]" ) - key = private_key or os.environ.get("SOLANA_WALLET_KEY") + from .solana_wallet import load_solana_wallet + + key = ( + private_key + or os.environ.get("SOLANA_WALLET_KEY") + or load_solana_wallet() # Loads from ~/.blockrun/.solana-session + ) if not key: raise ValueError( - "Private key required. Pass private_key or set SOLANA_WALLET_KEY env var." + "Private key required. Pass private_key, set SOLANA_WALLET_KEY, " + "or create a wallet at ~/.blockrun/.solana-session." ) self._private_key = key validate_api_url(api_url) diff --git a/tests/unit/test_solana_client.py b/tests/unit/test_solana_client.py index e2d0b7e..8a67803 100644 --- a/tests/unit/test_solana_client.py +++ b/tests/unit/test_solana_client.py @@ -20,12 +20,22 @@ def test_init_from_env(self): assert client is not None del os.environ["SOLANA_WALLET_KEY"] - def test_raises_without_key(self): - saved = os.environ.pop("SOLANA_WALLET_KEY", None) + def test_raises_without_key(self, monkeypatch): + # No env var AND no wallet session on disk → must still raise. Patch the + # session loader so the test is deterministic regardless of whether the + # machine running it happens to have ~/.blockrun/.solana-session. + monkeypatch.delenv("SOLANA_WALLET_KEY", raising=False) + monkeypatch.setattr("blockrun_llm.solana_wallet.load_solana_wallet", lambda: None) with pytest.raises(ValueError, match="[Pp]rivate key required"): SolanaLLMClient() - if saved: - os.environ["SOLANA_WALLET_KEY"] = saved + + def test_init_from_session_file(self, monkeypatch): + # No env var, but a wallet session exists on disk → auto-load it (parity + # with the Base LLMClient, which already falls back to load_wallet()). + monkeypatch.delenv("SOLANA_WALLET_KEY", raising=False) + monkeypatch.setattr("blockrun_llm.solana_wallet.load_solana_wallet", lambda: TEST_BS58_KEY) + client = SolanaLLMClient() + assert client is not None def test_default_api_url(self): client = SolanaLLMClient(private_key=TEST_BS58_KEY) From 678ec41dca9b9754d3956113561d71cc5df1cc9a Mon Sep 17 00:00:00 2001 From: 1bcMax Date: Sun, 14 Jun 2026 19:12:35 -0400 Subject: [PATCH 2/2] harden(solana): accurate wallet-source docs, validate resolved key, cover async + bad-key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review follow-ups to the session-file auto-load: - Fix misleading comment/error: load_solana_wallet() scans the newest ~/.*/solana-wallet.json from ANY provider first, then ~/.blockrun/.solana-session — the old text named only the session file, hiding which key is actually used. - Validate the resolved key for true Base parity: wrap _create_signer so a malformed key (incl. one auto-loaded from disk) raises a clean ValueError instead of a raw base58/solders exception. Both sync and async clients. - Guard the legacy session-file read against OSError (unreadable file → no wallet, not a crash), matching the provider-scan path. - Tests: add async session-file fallback + invalid-key coverage. --- blockrun_llm/solana_client.py | 30 ++++++++++++++++++++++++------ blockrun_llm/solana_wallet.py | 5 ++++- tests/unit/test_solana_client.py | 21 ++++++++++++++++++++- 3 files changed, 48 insertions(+), 8 deletions(-) diff --git a/blockrun_llm/solana_client.py b/blockrun_llm/solana_client.py index 099930b..25d0628 100644 --- a/blockrun_llm/solana_client.py +++ b/blockrun_llm/solana_client.py @@ -370,12 +370,13 @@ def __init__( key = ( private_key or os.environ.get("SOLANA_WALLET_KEY") - or load_solana_wallet() # Loads from ~/.blockrun/.solana-session + or load_solana_wallet() # disk: newest ~/.*/solana-wallet.json, else ~/.blockrun/.solana-session ) if not key: raise ValueError( "Private key required. Pass private_key, set SOLANA_WALLET_KEY, " - "or create a wallet at ~/.blockrun/.solana-session." + "or have a Solana wallet on disk " + "(~/./solana-wallet.json or ~/.blockrun/.solana-session)." ) self._private_key = key validate_api_url(api_url) @@ -405,7 +406,15 @@ def __init__( # Initialize x402 SDK client for Solana payment signing. self._x402_client = x402ClientSync() - signer = _create_signer(self._private_key) + try: + signer = _create_signer(self._private_key) + except Exception as e: + # Parity with the Base client, which validates the resolved key up + # front: turn a malformed key (incl. one auto-loaded from disk) into + # a clean error instead of a raw base58/solders exception. + raise ValueError( + "Invalid Solana private key (expected a base58-encoded keypair " "or 32-byte seed)." + ) from e _register_svm_with_headers(self._x402_client, signer, resolved_url, resolved_headers) # x402ClientSync is NOT thread-safe: concurrent payment signing on one # shared client races on nonce/authorization state. This lock serializes @@ -1883,12 +1892,13 @@ def __init__( key = ( private_key or os.environ.get("SOLANA_WALLET_KEY") - or load_solana_wallet() # Loads from ~/.blockrun/.solana-session + or load_solana_wallet() # disk: newest ~/.*/solana-wallet.json, else ~/.blockrun/.solana-session ) if not key: raise ValueError( "Private key required. Pass private_key, set SOLANA_WALLET_KEY, " - "or create a wallet at ~/.blockrun/.solana-session." + "or have a Solana wallet on disk " + "(~/./solana-wallet.json or ~/.blockrun/.solana-session)." ) self._private_key = key validate_api_url(api_url) @@ -1917,7 +1927,15 @@ def __init__( from x402 import x402Client # local import to keep optional dep clean self._x402_client = x402Client() - signer = _create_signer(self._private_key) + try: + signer = _create_signer(self._private_key) + except Exception as e: + # Parity with the Base client, which validates the resolved key up + # front: turn a malformed key (incl. one auto-loaded from disk) into + # a clean error instead of a raw base58/solders exception. + raise ValueError( + "Invalid Solana private key (expected a base58-encoded keypair " "or 32-byte seed)." + ) from e _register_svm_with_headers(self._x402_client, signer, resolved_url, resolved_headers) # Lazily created on first sign (avoids binding asyncio.Lock to a loop at # construction time). Serializes the async signing critical section so a diff --git a/blockrun_llm/solana_wallet.py b/blockrun_llm/solana_wallet.py index d055f93..ee6322a 100644 --- a/blockrun_llm/solana_wallet.py +++ b/blockrun_llm/solana_wallet.py @@ -201,7 +201,10 @@ def load_solana_wallet() -> Optional[str]: # Legacy session file if SOLANA_WALLET_FILE.exists(): - key = SOLANA_WALLET_FILE.read_text().strip() + try: + key = SOLANA_WALLET_FILE.read_text().strip() + except OSError: + return None # unreadable (bad perms/ownership) → treat as "no wallet" if key: return key return None diff --git a/tests/unit/test_solana_client.py b/tests/unit/test_solana_client.py index 8a67803..c427c63 100644 --- a/tests/unit/test_solana_client.py +++ b/tests/unit/test_solana_client.py @@ -2,7 +2,7 @@ import pytest import os -from blockrun_llm.solana_client import SolanaLLMClient +from blockrun_llm.solana_client import SolanaLLMClient, AsyncSolanaLLMClient TEST_BS58_KEY = ( "433C7KFcM4y1ZEVdZYSH7wheSNAM384UcbgXEyD5FV7Q2HsQ1BwjEDx4GbBZUqPkZTVhFPyLyuZnzK8wCeAkU7wG" @@ -37,6 +37,25 @@ def test_init_from_session_file(self, monkeypatch): client = SolanaLLMClient() assert client is not None + @pytest.mark.asyncio + async def test_async_init_from_session_file(self, monkeypatch): + # Same disk fallback on the async client (identical code path). + monkeypatch.delenv("SOLANA_WALLET_KEY", raising=False) + monkeypatch.setattr("blockrun_llm.solana_wallet.load_solana_wallet", lambda: TEST_BS58_KEY) + client = AsyncSolanaLLMClient() + assert client is not None + await client.close() + + def test_raises_on_invalid_key(self, monkeypatch): + # A malformed key (here from the disk fallback) must surface a clean + # ValueError, not a raw base58/solders exception. Parity with Base. + monkeypatch.delenv("SOLANA_WALLET_KEY", raising=False) + monkeypatch.setattr( + "blockrun_llm.solana_wallet.load_solana_wallet", lambda: "not-a-valid-key" + ) + with pytest.raises(ValueError, match="[Ii]nvalid Solana private key"): + SolanaLLMClient() + def test_default_api_url(self): client = SolanaLLMClient(private_key=TEST_BS58_KEY) assert client.is_solana()