Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 38 additions & 6 deletions blockrun_llm/solana_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,10 +365,18 @@ 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() # disk: newest ~/.*/solana-wallet.json, else ~/.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 have a Solana wallet on disk "
"(~/.<provider>/solana-wallet.json or ~/.blockrun/.solana-session)."
)
self._private_key = key
validate_api_url(api_url)
Expand Down Expand Up @@ -398,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
Expand Down Expand Up @@ -1871,10 +1887,18 @@ 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() # disk: newest ~/.*/solana-wallet.json, else ~/.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 have a Solana wallet on disk "
"(~/.<provider>/solana-wallet.json or ~/.blockrun/.solana-session)."
)
self._private_key = key
validate_api_url(api_url)
Expand Down Expand Up @@ -1903,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
Expand Down
5 changes: 4 additions & 1 deletion blockrun_llm/solana_wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
39 changes: 34 additions & 5 deletions tests/unit/test_solana_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -20,12 +20,41 @@ 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

@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)
Expand Down
Loading