Skip to content

Commit 5779ba6

Browse files
authored
Merge pull request #30 from helloissariel/main
[solana] Add Solana crypto toolkit
2 parents a59e24f + 77d6fe6 commit 5779ba6

File tree

11 files changed

+4222
-0
lines changed

11 files changed

+4222
-0
lines changed
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
"""Solana toolkit exports for SpoonAI."""
2+
3+
# Transfer tools
4+
from .transfer import SolanaTransferTool
5+
6+
# Swap tools
7+
from .swap import SolanaSwapTool
8+
9+
# Wallet management tools
10+
from .wallet import (
11+
SolanaWalletInfoTool,
12+
)
13+
14+
# Blockchain service tools and helpers
15+
from .service import (
16+
create_request_headers,
17+
detect_private_keys_from_string,
18+
detect_pubkeys_from_string,
19+
format_token_amount,
20+
get_api_key,
21+
get_rpc_url,
22+
get_wallet_cache_scheduler,
23+
is_native_sol,
24+
lamports_to_sol,
25+
parse_token_amount,
26+
parse_transaction_error,
27+
sol_to_lamports,
28+
truncate_address,
29+
validate_private_key,
30+
validate_solana_address,
31+
verify_solana_signature,
32+
)
33+
34+
# Keypair utilities
35+
from .keypairUtils import get_wallet_keypair, get_wallet_key, get_private_key, get_public_key
36+
37+
# Plugin integration
38+
from .index import solana_plugin, PluginManifest, ProviderDefinition, wallet_provider, init_plugin
39+
40+
# Environment helpers
41+
from .environment import load_solana_config, SolanaConfig
42+
43+
# Shared constants
44+
from .constants import NATIVE_SOL_ADDRESS, TOKEN_ADDRESSES, DEFAULT_SLIPPAGE_BPS, JUPITER_PRIORITY_LEVELS
45+
46+
# Typed models
47+
from .types import (
48+
WalletPortfolio,
49+
Item,
50+
Prices,
51+
TransferContent,
52+
SwapContent,
53+
KeypairResult,
54+
TokenMetadata,
55+
)
56+
57+
__all__ = [
58+
# Transfer tools
59+
"SolanaTransferTool",
60+
# Swap tools
61+
"SolanaSwapTool",
62+
# Wallet tools
63+
"SolanaWalletInfoTool",
64+
# Service tools & helpers
65+
"create_request_headers",
66+
"detect_private_keys_from_string",
67+
"detect_pubkeys_from_string",
68+
"format_token_amount",
69+
"get_api_key",
70+
"get_rpc_url",
71+
"get_wallet_cache_scheduler",
72+
"is_native_sol",
73+
"lamports_to_sol",
74+
"parse_token_amount",
75+
"parse_transaction_error",
76+
"sol_to_lamports",
77+
"truncate_address",
78+
"validate_private_key",
79+
"validate_solana_address",
80+
"verify_solana_signature",
81+
# Key utilities
82+
"get_wallet_keypair",
83+
"get_wallet_key",
84+
"get_private_key",
85+
"get_public_key",
86+
# Plugin integration
87+
"solana_plugin",
88+
"PluginManifest",
89+
"ProviderDefinition",
90+
"wallet_provider",
91+
"init_plugin",
92+
# Environment helpers
93+
"load_solana_config",
94+
"SolanaConfig",
95+
# Constants
96+
"NATIVE_SOL_ADDRESS",
97+
"TOKEN_ADDRESSES",
98+
"DEFAULT_SLIPPAGE_BPS",
99+
"JUPITER_PRIORITY_LEVELS",
100+
# Typed models
101+
"WalletPortfolio",
102+
"Item",
103+
"Prices",
104+
"TransferContent",
105+
"SwapContent",
106+
"KeypairResult",
107+
"TokenMetadata",
108+
]
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from decimal import Decimal, getcontext
2+
from typing import Union
3+
4+
getcontext().prec = 10
5+
6+
class BigNumber(Decimal):
7+
def __new__(cls, value: Union[str, float, int, Decimal, "BigNumber"]) -> "BigNumber":
8+
return super().__new__(cls, str(value))
9+
10+
def plus(self, other: Union[str, float, int, Decimal, "BigNumber"]) -> "BigNumber":
11+
return BigNumber(self + Decimal(str(other)))
12+
13+
def minus(self, other: Union[str, float, int, Decimal, "BigNumber"]) -> "BigNumber":
14+
return BigNumber(self - Decimal(str(other)))
15+
16+
def multiplied_by(self, other: Union[str, float, int, Decimal, "BigNumber"]) -> "BigNumber":
17+
return BigNumber(self * Decimal(str(other)))
18+
19+
def divided_by(self, other: Union[str, float, int, Decimal, "BigNumber"]) -> "BigNumber":
20+
return BigNumber(self / Decimal(str(other)))
21+
22+
def pow(self, exponent: int) -> "BigNumber":
23+
return BigNumber(self ** Decimal(exponent))
24+
25+
def to_number(self) -> float:
26+
return float(self)
27+
28+
def to_string(self) -> str:
29+
return format(self, "f")
30+
31+
BN = BigNumber
32+
33+
34+
def toBN(value: Union[str, float, int, Decimal, BigNumber]) -> BigNumber:
35+
"""Convert a value to a BigNumber (Decimal) object."""
36+
return BigNumber(value)
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"""Solana toolkit constants,This module defines constants used throughout the Solana toolkit"""
2+
3+
# Service and Cache Configuration
4+
SOLANA_SERVICE_NAME = "chain_solana"
5+
SOLANA_WALLET_DATA_CACHE_KEY = "solana/walletData"
6+
7+
# Token Program IDs
8+
TOKEN_PROGRAM_ID = None
9+
TOKEN_2022_PROGRAM_ID = None
10+
ASSOCIATED_TOKEN_PROGRAM_ID = None
11+
12+
# System Program ID
13+
SYSTEM_PROGRAM_ID = None
14+
15+
# Metadata Program ID (Metaplex)
16+
METADATA_PROGRAM_ID = None
17+
18+
TOKEN_ADDRESSES = {
19+
"SOL": None,
20+
"USDC": None,
21+
"USDT": None,
22+
"BTC": None,
23+
"ETH": None,
24+
}
25+
26+
# Native SOL placeholder address (used for swaps)
27+
NATIVE_SOL_ADDRESS = None
28+
29+
# Transaction Configuration
30+
DEFAULT_PRIORITY_FEE = 5 # micro-lamports per compute unit
31+
MAX_RETRIES = 3
32+
RETRY_DELAY = 2.0 # seconds
33+
34+
# Slippage Configuration
35+
DEFAULT_SLIPPAGE_BPS = 100 # 1%
36+
MAX_SLIPPAGE_BPS = 3000 # 30%
37+
38+
# Cache Configuration
39+
UPDATE_INTERVAL = 120 # seconds
40+
CACHE_TTL = 300 # seconds
41+
42+
# Account Data Lengths
43+
TOKEN_ACCOUNT_DATA_LENGTH = 165
44+
TOKEN_MINT_DATA_LENGTH = 82
45+
46+
# Environment Variable Keys
47+
ENV_KEYS = {
48+
"RPC_URL": ["SOLANA_RPC_URL", "RPC_URL"],
49+
"PRIVATE_KEY": ["SOLANA_PRIVATE_KEY", "WALLET_PRIVATE_KEY"],
50+
"PUBLIC_KEY": ["SOLANA_PUBLIC_KEY", "WALLET_PUBLIC_KEY"],
51+
"HELIUS_API_KEY": ["HELIUS_API_KEY"],
52+
"BIRDEYE_API_KEY": ["BIRDEYE_API_KEY"],
53+
}
54+
55+
# Priority Levels for Jupiter
56+
JUPITER_PRIORITY_LEVELS = {
57+
"low": 50,
58+
"medium": 200,
59+
"high": 1000,
60+
"veryHigh": 4_000_000,
61+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
"""Solana environment configuration validation."""
2+
3+
from __future__ import annotations
4+
5+
import os
6+
import logging
7+
from typing import Any, Optional
8+
9+
from pydantic import BaseModel, Field, ValidationError, model_validator, ConfigDict
10+
11+
logger = logging.getLogger(__name__)
12+
13+
14+
class SolanaConfig(BaseModel):
15+
"""Validated configuration required for Solana toolkit operations."""
16+
17+
wallet_secret_salt: Optional[str] = Field(default=None, alias="WALLET_SECRET_SALT")
18+
wallet_secret_key: Optional[str] = Field(default=None, alias="WALLET_SECRET_KEY")
19+
wallet_public_key: Optional[str] = Field(default=None, alias="WALLET_PUBLIC_KEY")
20+
21+
sol_address: str = Field(alias="SOL_ADDRESS")
22+
slippage: str = Field(alias="SLIPPAGE")
23+
solana_rpc_url: str = Field(alias="SOLANA_RPC_URL")
24+
helius_api_key: str = Field(alias="HELIUS_API_KEY")
25+
birdeye_api_key: str = Field(alias="BIRDEYE_API_KEY")
26+
27+
model_config = ConfigDict(populate_by_name=True, extra="forbid")
28+
29+
@model_validator(mode="after")
30+
def _validate_key_material(cls, values: "SolanaConfig") -> "SolanaConfig":
31+
"""Ensure either a secret salt or keypair credentials are present."""
32+
has_salt = bool(values.wallet_secret_salt)
33+
has_keypair = bool(values.wallet_secret_key and values.wallet_public_key)
34+
35+
if not (has_salt or has_keypair):
36+
raise ValueError(
37+
"Provide WALLET_SECRET_SALT or both WALLET_SECRET_KEY and WALLET_PUBLIC_KEY."
38+
)
39+
return values
40+
41+
42+
def _runtime_get(runtime: Any, key: str) -> Optional[str]:
43+
"""Attempt to read a setting from a runtime object if available."""
44+
if runtime is None:
45+
return None
46+
47+
for attr in ("get_setting", "getSetting", "get"):
48+
getter = getattr(runtime, attr, None)
49+
if callable(getter):
50+
try:
51+
value = getter(key)
52+
except TypeError:
53+
# Getter signature mismatch – try next option
54+
continue
55+
if value is not None:
56+
return value
57+
58+
# Common pattern: runtime.settings dict
59+
settings = getattr(runtime, "settings", None)
60+
if isinstance(settings, dict):
61+
return settings.get(key)
62+
63+
return None
64+
65+
66+
def _read_config_value(runtime: Any, *keys: str) -> Optional[str]:
67+
"""Return the first non-empty value from runtime or environment for the provided keys."""
68+
for key in keys:
69+
value = _runtime_get(runtime, key)
70+
if value is None:
71+
value = os.getenv(key)
72+
73+
if isinstance(value, str):
74+
value = value.strip()
75+
76+
if value:
77+
return value
78+
79+
return None
80+
81+
82+
def load_solana_config(runtime: Any = None) -> SolanaConfig:
83+
"""Validate and return Solana configuration based on runtime settings and environment variables.
84+
85+
Args:
86+
runtime: Optional runtime object providing a ``get_setting``-style API.
87+
88+
Returns:
89+
SolanaConfig: Validated configuration object.
90+
91+
Raises:
92+
ValueError: When validation fails or required fields are missing.
93+
"""
94+
config_payload = {
95+
"WALLET_SECRET_SALT": _read_config_value(runtime, "WALLET_SECRET_SALT"),
96+
"WALLET_SECRET_KEY": _read_config_value(runtime, "WALLET_SECRET_KEY"),
97+
"WALLET_PUBLIC_KEY": _read_config_value(
98+
runtime,
99+
"SOLANA_PUBLIC_KEY",
100+
"WALLET_PUBLIC_KEY",
101+
),
102+
"SOL_ADDRESS": _read_config_value(runtime, "SOL_ADDRESS"),
103+
"SLIPPAGE": _read_config_value(runtime, "SLIPPAGE"),
104+
"SOLANA_RPC_URL": _read_config_value(runtime, "SOLANA_RPC_URL"),
105+
"HELIUS_API_KEY": _read_config_value(runtime, "HELIUS_API_KEY"),
106+
"BIRDEYE_API_KEY": _read_config_value(runtime, "BIRDEYE_API_KEY"),
107+
}
108+
109+
try:
110+
return SolanaConfig(**config_payload)
111+
except ValidationError as exc:
112+
messages = []
113+
for error in exc.errors():
114+
location = ".".join(str(part) for part in error.get("loc", ()))
115+
messages.append(f"{location or 'configuration'}: {error.get('msg')}")
116+
117+
detail = "\n".join(messages) or str(exc)
118+
logger.error("Solana configuration validation failed:\n%s", detail)
119+
raise ValueError(f"Solana configuration validation failed:\n{detail}") from exc
120+

0 commit comments

Comments
 (0)