botchain-sdk-py 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
botchain/__init__.py ADDED
@@ -0,0 +1,18 @@
1
+ from .client import BotChain
2
+ from .exceptions import BotChainLogsDisabledError
3
+ from .dex import BDexManager
4
+ from .tokens import TokenManager
5
+ from .middleware import botchain_paymaster_middleware, check_and_apply_sponsorship
6
+ from .agents import AgentTradePolicy, AgentExecutor
7
+
8
+ __all__ = [
9
+ "BotChain",
10
+ "BotChainLogsDisabledError",
11
+ "BDexManager",
12
+ "TokenManager",
13
+ "botchain_paymaster_middleware",
14
+ "check_and_apply_sponsorship",
15
+ "AgentTradePolicy",
16
+ "AgentExecutor"
17
+ ]
18
+
@@ -0,0 +1,4 @@
1
+ from .policy import AgentTradePolicy
2
+ from .executor import AgentExecutor
3
+
4
+ __all__ = ["AgentTradePolicy", "AgentExecutor"]
@@ -0,0 +1,92 @@
1
+ import logging
2
+ import time
3
+ from typing import Optional
4
+ from web3 import Web3
5
+ from .policy import AgentTradePolicy
6
+ from ..middleware import check_and_apply_sponsorship
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ class AgentExecutor:
11
+ """
12
+ Guarded transaction execution engine for AI Agents.
13
+ Validates trade intents against AgentTradePolicy, performs pre-flight eth_call simulations,
14
+ and handles zero-gas Paymaster sponsorship before signing & broadcasting.
15
+ """
16
+ def __init__(self, botclient, policy: AgentTradePolicy):
17
+ self.client = botclient
18
+ self.w3 = botclient.w3
19
+ self.policy = policy
20
+
21
+ def execute_trade(
22
+ self,
23
+ dex_manager,
24
+ token_in: str,
25
+ token_out: str,
26
+ amount_in: int,
27
+ fee: int = 3000,
28
+ slippage_tolerance: float = 0.01,
29
+ recipient: Optional[str] = None,
30
+ notional_usd: Optional[float] = None
31
+ ) -> str:
32
+ """
33
+ Validates, simulates, sponsors, and executes a single-hop BDEX trade.
34
+ """
35
+ if not self.client.account:
36
+ raise ValueError("Local account required to execute agent trades.")
37
+
38
+ target_recipient = recipient or self.client.account.address
39
+ router_address = dex_manager.router_contract.address
40
+
41
+ # 1. Policy Validation
42
+ self.policy.validate_trade(
43
+ token_in=token_in,
44
+ token_out=token_out,
45
+ router=router_address,
46
+ amount_in=amount_in,
47
+ slippage=slippage_tolerance,
48
+ notional_usd=notional_usd
49
+ )
50
+
51
+ # 2. Get Quote & Calculate minimum output
52
+ expected_out = dex_manager.get_quote(token_in, token_out, amount_in, fee)
53
+ amount_out_minimum = int(expected_out * (1 - slippage_tolerance))
54
+
55
+ # 3. Build Transaction Dictionary
56
+ params = {
57
+ "tokenIn": self.w3.to_checksum_address(token_in),
58
+ "tokenOut": self.w3.to_checksum_address(token_out),
59
+ "fee": fee,
60
+ "recipient": self.w3.to_checksum_address(target_recipient),
61
+ "deadline": int(time.time()) + 600,
62
+ "amountIn": amount_in,
63
+ "amountOutMinimum": amount_out_minimum,
64
+ "sqrtPriceLimitX96": 0
65
+ }
66
+
67
+ tx_dict = dex_manager.router_contract.functions.exactInputSingle(params).build_transaction({
68
+ 'from': self.client.account.address,
69
+ 'nonce': self.w3.eth.get_transaction_count(self.client.account.address),
70
+ 'chainId': self.client.chain_id,
71
+ })
72
+
73
+ # 4. Pre-Flight Simulation (eth_call)
74
+ try:
75
+ logger.info("Executing pre-flight transaction simulation via eth_call...")
76
+ self.w3.eth.call(tx_dict)
77
+ logger.info("Pre-flight simulation successful.")
78
+ except Exception as e:
79
+ logger.error(f"Pre-flight simulation failed: {e}")
80
+ raise RuntimeError(f"Transaction pre-flight simulation failed: {e}") from e
81
+
82
+ # 5. Zero-Gas Paymaster Sponsorship Check
83
+ tx_dict = check_and_apply_sponsorship(self.w3, tx_dict)
84
+
85
+ # 6. Sign and Broadcast Transaction
86
+ signed_tx = self.w3.eth.account.sign_transaction(tx_dict, private_key=self.client.account.key)
87
+ tx_hash = self.w3.eth.send_raw_transaction(signed_tx.rawTransaction)
88
+
89
+ # 7. Record trade in policy state upon success
90
+ self.policy.record_trade()
91
+
92
+ return self.w3.to_hex(tx_hash)
botchain/agents/mcp.py ADDED
@@ -0,0 +1,3 @@
1
+ from .tools.mcp_server import BotChainMCPServer
2
+
3
+ __all__ = ["BotChainMCPServer"]
@@ -0,0 +1,18 @@
1
+ import sys
2
+ from botchain.client import BotChain
3
+ from botchain.agents.executor import AgentExecutor
4
+ from botchain.agents.policy import AgentTradePolicy
5
+ from botchain.agents.tools.mcp_server import BotChainMCPServer
6
+
7
+ def main():
8
+ """Entry point for botchain-mcp server."""
9
+ # Simple setup for MCP
10
+ client = BotChain()
11
+ policy = AgentTradePolicy()
12
+ executor = AgentExecutor(policy)
13
+
14
+ server = BotChainMCPServer(client, executor)
15
+ server.run_stdio()
16
+
17
+ if __name__ == "__main__":
18
+ main()
@@ -0,0 +1,102 @@
1
+ import time
2
+ from typing import List, Optional, Set
3
+ from web3 import Web3
4
+
5
+ class AgentTradePolicy:
6
+ """
7
+ Safety Policy Engine for AI Agents executing trades on BOT Chain.
8
+ Enforces spending caps, rate limits, cooldowns, and asset allowlists.
9
+ """
10
+ def __init__(
11
+ self,
12
+ allowed_tokens: Optional[List[str]] = None,
13
+ allowed_routers: Optional[List[str]] = None,
14
+ max_slippage_tolerance: float = 0.05,
15
+ max_notional_usd: Optional[float] = 1000.0,
16
+ cooldown_seconds: float = 10.0,
17
+ max_trades_per_hour: int = 10
18
+ ):
19
+ self.allowed_tokens: Optional[Set[str]] = (
20
+ {Web3.to_checksum_address(t) for t in allowed_tokens} if allowed_tokens else None
21
+ )
22
+ self.allowed_routers: Optional[Set[str]] = (
23
+ {Web3.to_checksum_address(r) for r in allowed_routers} if allowed_routers else None
24
+ )
25
+ self.max_slippage_tolerance = max_slippage_tolerance
26
+ self.max_notional_usd = max_notional_usd
27
+ self.cooldown_seconds = cooldown_seconds
28
+ self.max_trades_per_hour = max_trades_per_hour
29
+
30
+ # Internal tracking state
31
+ self.last_trade_time: Optional[float] = None
32
+ self.trade_timestamps: List[float] = []
33
+
34
+ def validate_trade(
35
+ self,
36
+ token_in: str,
37
+ token_out: str,
38
+ router: Optional[str] = None,
39
+ amount_in: int = 0,
40
+ slippage: float = 0.01,
41
+ notional_usd: Optional[float] = None,
42
+ current_time: Optional[float] = None
43
+ ) -> None:
44
+ """
45
+ Validates trade parameters against policy rules.
46
+ Raises ValueError if any rule is violated.
47
+ """
48
+ now = current_time if current_time is not None else time.time()
49
+
50
+ # 1. Token Allowlist Verification
51
+ checksum_in = Web3.to_checksum_address(token_in)
52
+ checksum_out = Web3.to_checksum_address(token_out)
53
+
54
+ if self.allowed_tokens is not None:
55
+ if checksum_in not in self.allowed_tokens:
56
+ raise ValueError(f"Token IN ({token_in}) is not in the allowed tokens list.")
57
+ if checksum_out not in self.allowed_tokens:
58
+ raise ValueError(f"Token OUT ({token_out}) is not in the allowed tokens list.")
59
+
60
+ # 2. Router Allowlist Verification
61
+ if router and self.allowed_routers is not None:
62
+ checksum_router = Web3.to_checksum_address(router)
63
+ if checksum_router not in self.allowed_routers:
64
+ raise ValueError(f"Router ({router}) is not in the allowed routers list.")
65
+
66
+ # 3. Slippage Limit Verification
67
+ if slippage > self.max_slippage_tolerance:
68
+ raise ValueError(
69
+ f"Requested slippage ({slippage:.4f}) exceeds maximum allowed tolerance ({self.max_slippage_tolerance:.4f})."
70
+ )
71
+
72
+ # 4. Notional USD Cap Verification
73
+ if self.max_notional_usd is not None and notional_usd is not None:
74
+ if notional_usd > self.max_notional_usd:
75
+ raise ValueError(
76
+ f"Trade value (${notional_usd:.2f}) exceeds maximum notional USD cap (${self.max_notional_usd:.2f})."
77
+ )
78
+
79
+ # 5. Cooldown Verification
80
+ if self.last_trade_time is not None:
81
+ elapsed = now - self.last_trade_time
82
+ if elapsed < self.cooldown_seconds:
83
+ remaining = self.cooldown_seconds - elapsed
84
+ raise ValueError(
85
+ f"Trade cooldown active. Please wait {remaining:.2f} seconds before trading again."
86
+ )
87
+
88
+ # 6. Hourly Rate Limit Verification
89
+ # Filter timestamps to only keep trades within the last 3600 seconds (1 hour)
90
+ self.trade_timestamps = [t for t in self.trade_timestamps if now - t < 3600]
91
+ if len(self.trade_timestamps) >= self.max_trades_per_hour:
92
+ raise ValueError(
93
+ f"Hourly trade limit reached ({self.max_trades_per_hour} trades/hour)."
94
+ )
95
+
96
+ def record_trade(self, timestamp: Optional[float] = None) -> None:
97
+ """
98
+ Records a successfully executed trade timestamp.
99
+ """
100
+ now = timestamp if timestamp is not None else time.time()
101
+ self.last_trade_time = now
102
+ self.trade_timestamps.append(now)
@@ -0,0 +1,3 @@
1
+ from .schemas import BOTCHAIN_TOOL_SCHEMAS, get_openai_tool_definitions
2
+
3
+ __all__ = ["BOTCHAIN_TOOL_SCHEMAS", "get_openai_tool_definitions"]
@@ -0,0 +1,32 @@
1
+ from botchain.tokens import TokenManager
2
+ from botchain.dex import BDexManager
3
+ from botchain.agents.executor import AgentExecutor
4
+
5
+ def botchain_get_balance(client, token_address: str, account_address: str = None) -> int:
6
+ token_manager = TokenManager(client)
7
+ return token_manager.get_balance(token_address, account_address)
8
+
9
+ def botchain_get_quote(client, token_in: str, token_out: str, amount_in: int, fee: int = 3000) -> int:
10
+ dex_manager = BDexManager(client)
11
+ return dex_manager.get_quote(token_in, token_out, amount_in, fee)
12
+
13
+ def botchain_execute_swap(
14
+ client,
15
+ executor: AgentExecutor,
16
+ token_in: str,
17
+ token_out: str,
18
+ amount_in: int,
19
+ fee: int = 3000,
20
+ slippage_tolerance: float = 0.01,
21
+ notional_usd: float = None
22
+ ) -> str:
23
+ dex_manager = BDexManager(client)
24
+ return executor.execute_trade(
25
+ dex_manager=dex_manager,
26
+ token_in=token_in,
27
+ token_out=token_out,
28
+ amount_in=amount_in,
29
+ fee=fee,
30
+ slippage_tolerance=slippage_tolerance,
31
+ notional_usd=notional_usd
32
+ )
@@ -0,0 +1,115 @@
1
+ import sys
2
+ import json
3
+ import logging
4
+ from botchain.agents.tools.base import botchain_get_balance, botchain_get_quote, botchain_execute_swap
5
+ from botchain.agents.tools.schemas import BOTCHAIN_TOOL_SCHEMAS
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ class BotChainMCPServer:
10
+ """
11
+ Model Context Protocol (MCP) Server for BOT Chain Tools.
12
+ Listens on stdio for JSON-RPC 2.0 requests.
13
+ """
14
+ def __init__(self, client, executor):
15
+ self.client = client
16
+ self.executor = executor
17
+
18
+ # Tools map
19
+ self.tools = {
20
+ "botchain_get_balance": self.handle_get_balance,
21
+ "botchain_get_quote": self.handle_get_quote,
22
+ "botchain_execute_swap": self.handle_execute_swap
23
+ }
24
+
25
+ def handle_get_balance(self, params):
26
+ return botchain_get_balance(
27
+ self.client,
28
+ params.get("token_address"),
29
+ params.get("account_address")
30
+ )
31
+
32
+ def handle_get_quote(self, params):
33
+ return botchain_get_quote(
34
+ self.client,
35
+ params.get("token_in"),
36
+ params.get("token_out"),
37
+ params.get("amount_in"),
38
+ params.get("fee", 3000)
39
+ )
40
+
41
+ def handle_execute_swap(self, params):
42
+ return botchain_execute_swap(
43
+ self.client,
44
+ self.executor,
45
+ params.get("token_in"),
46
+ params.get("token_out"),
47
+ params.get("amount_in"),
48
+ params.get("fee", 3000),
49
+ params.get("slippage_tolerance", 0.01),
50
+ params.get("notional_usd")
51
+ )
52
+
53
+ def process_request(self, request_str: str) -> str:
54
+ try:
55
+ req = json.loads(request_str)
56
+ if not isinstance(req, dict) or "jsonrpc" not in req:
57
+ return json.dumps({"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": None})
58
+
59
+ req_id = req.get("id")
60
+ method = req.get("method")
61
+ params = req.get("params", {})
62
+
63
+ # MCP Tool Discovery
64
+ if method == "tools/list":
65
+ return json.dumps({
66
+ "jsonrpc": "2.0",
67
+ "id": req_id,
68
+ "result": {
69
+ "tools": BOTCHAIN_TOOL_SCHEMAS
70
+ }
71
+ })
72
+
73
+ # MCP Tool Call
74
+ if method == "tools/call":
75
+ tool_name = params.get("name")
76
+ tool_args = params.get("arguments", {})
77
+
78
+ if tool_name not in self.tools:
79
+ return json.dumps({
80
+ "jsonrpc": "2.0",
81
+ "id": req_id,
82
+ "error": {"code": -32601, "message": f"Method not found: {tool_name}"}
83
+ })
84
+
85
+ try:
86
+ result = self.tools[tool_name](tool_args)
87
+ return json.dumps({
88
+ "jsonrpc": "2.0",
89
+ "id": req_id,
90
+ "result": {
91
+ "content": [{"type": "text", "text": str(result)}]
92
+ }
93
+ })
94
+ except Exception as e:
95
+ return json.dumps({
96
+ "jsonrpc": "2.0",
97
+ "id": req_id,
98
+ "error": {"code": -32000, "message": str(e)}
99
+ })
100
+
101
+ return json.dumps({
102
+ "jsonrpc": "2.0",
103
+ "id": req_id,
104
+ "error": {"code": -32601, "message": "Method not found"}
105
+ })
106
+
107
+ except json.JSONDecodeError:
108
+ return json.dumps({"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error"}, "id": None})
109
+
110
+ def run_stdio(self):
111
+ """Runs the MCP server over standard input/output."""
112
+ for line in sys.stdin:
113
+ response = self.process_request(line)
114
+ sys.stdout.write(response + "\n")
115
+ sys.stdout.flush()
@@ -0,0 +1,97 @@
1
+ """
2
+ Standardized JSON schemas for LLM Function Calling (OpenAI, Gemini, Anthropic, MCP).
3
+ """
4
+
5
+ BOTCHAIN_TOOL_SCHEMAS = [
6
+ {
7
+ "name": "botchain_get_balance",
8
+ "description": "Get the token balance (native BOT or ERC20/WBOT) for a given account address on BOT Chain.",
9
+ "parameters": {
10
+ "type": "object",
11
+ "properties": {
12
+ "token_address": {
13
+ "type": "string",
14
+ "description": "The contract address of the ERC20 token, or 'NATIVE' / 'WBOT' for BOT Chain native assets."
15
+ },
16
+ "account_address": {
17
+ "type": "string",
18
+ "description": "Optional 0x-prefixed account address. Defaults to the connected agent wallet."
19
+ }
20
+ },
21
+ "required": ["token_address"]
22
+ }
23
+ },
24
+ {
25
+ "name": "botchain_get_quote",
26
+ "description": "Query BDEX V3 for the expected output amount for a single-hop token swap.",
27
+ "parameters": {
28
+ "type": "object",
29
+ "properties": {
30
+ "token_in": {
31
+ "type": "string",
32
+ "description": "The checksummed contract address of the input token."
33
+ },
34
+ "token_out": {
35
+ "type": "string",
36
+ "description": "The checksummed contract address of the output token."
37
+ },
38
+ "amount_in": {
39
+ "type": "integer",
40
+ "description": "Amount of token_in in base units (wei)."
41
+ },
42
+ "fee": {
43
+ "type": "integer",
44
+ "description": "Pool fee tier in hundredths of a bip (default 3000 for 0.3%).",
45
+ "default": 3000
46
+ }
47
+ },
48
+ "required": ["token_in", "token_out", "amount_in"]
49
+ }
50
+ },
51
+ {
52
+ "name": "botchain_execute_swap",
53
+ "description": "Executes a policy-guarded single-hop token swap on BDEX with pre-flight simulation and optional zero-gas Paymaster sponsorship.",
54
+ "parameters": {
55
+ "type": "object",
56
+ "properties": {
57
+ "token_in": {
58
+ "type": "string",
59
+ "description": "Checksummed address of the token to sell."
60
+ },
61
+ "token_out": {
62
+ "type": "string",
63
+ "description": "Checksummed address of the token to buy."
64
+ },
65
+ "amount_in": {
66
+ "type": "integer",
67
+ "description": "Amount of token_in to sell in base units."
68
+ },
69
+ "fee": {
70
+ "type": "integer",
71
+ "description": "Pool fee tier (e.g. 3000 for 0.3%).",
72
+ "default": 3000
73
+ },
74
+ "slippage_tolerance": {
75
+ "type": "number",
76
+ "description": "Maximum allowed price slippage (e.g., 0.01 for 1%).",
77
+ "default": 0.01
78
+ },
79
+ "notional_usd": {
80
+ "type": "number",
81
+ "description": "Estimated USD value of the trade for policy cap verification."
82
+ }
83
+ },
84
+ "required": ["token_in", "token_out", "amount_in"]
85
+ }
86
+ }
87
+ ]
88
+
89
+ def get_openai_tool_definitions():
90
+ """Returns schemas wrapped in OpenAI function tool format."""
91
+ return [
92
+ {
93
+ "type": "function",
94
+ "function": schema
95
+ }
96
+ for schema in BOTCHAIN_TOOL_SCHEMAS
97
+ ]
botchain/client.py ADDED
@@ -0,0 +1,82 @@
1
+ from web3 import Web3
2
+ from web3.middleware import ExtraDataToPOAMiddleware
3
+ from eth_account import Account
4
+ from .exceptions import BotChainLogsDisabledError
5
+
6
+ class BotChain:
7
+ # Network Constants
8
+ MAINNET_RPC = "https://rpc.botchain.ai"
9
+ MAINNET_CHAIN_ID = 677
10
+
11
+ TESTNET_RPC = "https://rpc.bohr.life"
12
+ TESTNET_CHAIN_ID = 968
13
+
14
+ def __init__(self, private_key: str = None, is_testnet: bool = False, custom_rpc: str = None):
15
+ """
16
+ Initializes the BOT Chain Web3 client.
17
+ """
18
+ self.is_testnet = is_testnet
19
+ self.chain_id = self.TESTNET_CHAIN_ID if is_testnet else self.MAINNET_CHAIN_ID
20
+ self.rpc_url = custom_rpc or (self.TESTNET_RPC if is_testnet else self.MAINNET_RPC)
21
+
22
+ # Initialize the underlying Web3 HTTP Provider
23
+ self.w3 = Web3(Web3.HTTPProvider(self.rpc_url))
24
+
25
+ self._inject_poa_middleware()
26
+ self._configure_account(private_key)
27
+ self._protect_rpc_methods()
28
+
29
+ def _inject_poa_middleware(self):
30
+ """
31
+ Injects the geth_poa_middleware to handle BOT Chain's Parlia consensus block headers.
32
+ Must be added to layer 0.
33
+ """
34
+ self.w3.middleware_onion.inject(ExtraDataToPOAMiddleware, layer=0)
35
+
36
+ def _configure_account(self, private_key: str):
37
+ """
38
+ Sets up the local EOA for transaction signing.
39
+ """
40
+ self.account = None
41
+ if private_key:
42
+ self.account = Account.from_key(private_key)
43
+ self.w3.eth.default_account = self.account.address
44
+
45
+ def _protect_rpc_methods(self):
46
+ """
47
+ Wraps standard web3 methods to enforce BOT Chain architectural constraints.
48
+ """
49
+ original_get_logs = self.w3.eth.get_logs
50
+
51
+ def safe_get_logs(*args, **kwargs):
52
+ # If using the default mainnet RPC, block the call
53
+ if not self.is_testnet and self.rpc_url == self.MAINNET_RPC:
54
+ raise BotChainLogsDisabledError()
55
+ return original_get_logs(*args, **kwargs)
56
+
57
+ # Override the method
58
+ self.w3.eth.get_logs = safe_get_logs
59
+
60
+ def build_transaction(self, to: str, data: str = "0x", value: int = 0) -> dict:
61
+ """
62
+ Helper to construct a base transaction dictionary ready for signing or Paymaster middleware.
63
+ """
64
+ if not self.account:
65
+ raise ValueError("Private key must be provided to build transactions.")
66
+
67
+ tx = {
68
+ 'chainId': self.chain_id,
69
+ 'from': self.account.address,
70
+ 'to': self.w3.to_checksum_address(to),
71
+ 'value': value,
72
+ 'data': data,
73
+ 'nonce': self.w3.eth.get_transaction_count(self.account.address),
74
+ # Gas estimation will be handled by the user or the Web3 provider
75
+ }
76
+ return tx
77
+
78
+ def enable_zero_gas_paymaster(self):
79
+ """Injects the MegaFuel EOA Paymaster middleware."""
80
+ from .middleware import botchain_paymaster_middleware
81
+ self.w3.middleware_onion.inject(botchain_paymaster_middleware, name="botchain_paymaster", layer=0)
82
+
botchain/contracts.py ADDED
@@ -0,0 +1,8 @@
1
+ MAINNET_ADDRESSES = {
2
+ "BDEX_V3_FACTORY": "0x1C51c173323ec11BB4e3C4fD2314c225Dc4b5419",
3
+ "BDEX_SWAP_ROUTER": "0x07032d47A1b9f8460cBeE9dC17c1d3E438693929",
4
+ "BDEX_QUOTER_V2": "0x034A705b36067cff99ABf5C662Be881cBd8d0176",
5
+ "NONFUNGIBLE_POSITION_MANAGER": "0xDAc3FcFF004d8a8675b94E44941A1a2e3b240090",
6
+ "WBOT": "0xD5452816194a3784dBa983426cCe7c122F4abd30",
7
+ "USDT": "0xaBabc7Ddc03e501d190C676BF3d92ef0e6e87a3C",
8
+ }
botchain/dex.py ADDED
@@ -0,0 +1,189 @@
1
+ import json
2
+ import time
3
+ import os
4
+ from web3 import Web3
5
+ from .contracts import MAINNET_ADDRESSES
6
+
7
+ def get_tick_spacing(fee: int) -> int:
8
+ """Returns the tick spacing for a given fee tier."""
9
+ if fee == 100:
10
+ return 1
11
+ elif fee == 500:
12
+ return 10
13
+ elif fee == 3000:
14
+ return 60
15
+ elif fee == 10000:
16
+ return 200
17
+ return 60
18
+
19
+ def calculate_ticks(current_tick: int, tick_spacing: int, range_percentage: float) -> tuple[int, int]:
20
+ """Calculates a tick range snapped to exact multiples of tick_spacing."""
21
+ tick_delta = int(range_percentage * 10000)
22
+ tick_lower = current_tick - tick_delta
23
+ tick_upper = current_tick + tick_delta
24
+
25
+ tick_lower = (tick_lower // tick_spacing) * tick_spacing
26
+ tick_upper = (tick_upper // tick_spacing) * tick_spacing
27
+ return tick_lower, tick_upper
28
+
29
+ class BDexManager:
30
+ def __init__(self, botclient):
31
+ self.client = botclient
32
+ self.w3 = botclient.w3
33
+
34
+ self.router_abi = self._load_abi("SwapRouter.json")
35
+ self.router_contract = self.w3.eth.contract(
36
+ address=self.w3.to_checksum_address(MAINNET_ADDRESSES["BDEX_SWAP_ROUTER"]),
37
+ abi=self.router_abi
38
+ )
39
+
40
+ self.quoter_abi = self._load_abi("QuoterV2.json")
41
+ self.quoter_contract = self.w3.eth.contract(
42
+ address=self.w3.to_checksum_address(MAINNET_ADDRESSES["BDEX_QUOTER_V2"]),
43
+ abi=self.quoter_abi
44
+ )
45
+
46
+ self.nft_manager_abi = self._load_abi("NonFungiblePositionManager.json")
47
+ self.nft_manager_contract = self.w3.eth.contract(
48
+ address=self.w3.to_checksum_address(MAINNET_ADDRESSES["NONFUNGIBLE_POSITION_MANAGER"]),
49
+ abi=self.nft_manager_abi
50
+ )
51
+
52
+ def _load_abi(self, filename: str):
53
+ # Assumes ABIs are in an 'abis' directory at the project root
54
+ abi_path = os.path.join(os.path.dirname(__file__), "..", "..", "abis", filename)
55
+ if not os.path.exists(abi_path):
56
+ raise FileNotFoundError(f"ABI file not found: {abi_path}")
57
+ with open(abi_path, 'r') as f:
58
+ return json.load(f)
59
+
60
+ def get_quote(self, token_in: str, token_out: str, amount_in: int, fee: int = 3000) -> int:
61
+ """Queries QuoterV2 for expected output amount."""
62
+ params = {
63
+ "tokenIn": self.w3.to_checksum_address(token_in),
64
+ "tokenOut": self.w3.to_checksum_address(token_out),
65
+ "amountIn": amount_in,
66
+ "fee": fee,
67
+ "sqrtPriceLimitX96": 0
68
+ }
69
+ # QuoterV2 quoteExactInputSingle returns (amountOut, sqrtPriceX96After, initializedTicksCrossed, gasEstimate)
70
+ quote = self.quoter_contract.functions.quoteExactInputSingle(params).call()
71
+ return quote[0]
72
+
73
+ def swap_exact_input_single(self, token_in: str, token_out: str, amount_in: int, fee: int = 3000, slippage_tolerance: float = 0.01, recipient: str = None):
74
+ """Executes a single-hop swap with slippage protection."""
75
+ if not self.client.account:
76
+ raise ValueError("Local account required for swaps.")
77
+
78
+ if not recipient:
79
+ recipient = self.client.account.address
80
+
81
+ # 1. Get Quote
82
+ expected_out = self.get_quote(token_in, token_out, amount_in, fee)
83
+
84
+ # 2. Apply Slippage
85
+ amount_out_minimum = int(expected_out * (1 - slippage_tolerance))
86
+
87
+ # 3. Build Swap Params
88
+ params = {
89
+ "tokenIn": self.w3.to_checksum_address(token_in),
90
+ "tokenOut": self.w3.to_checksum_address(token_out),
91
+ "fee": fee,
92
+ "recipient": self.w3.to_checksum_address(recipient),
93
+ "deadline": int(time.time()) + 600,
94
+ "amountIn": amount_in,
95
+ "amountOutMinimum": amount_out_minimum,
96
+ "sqrtPriceLimitX96": 0
97
+ }
98
+
99
+ # 4. Build and Send Transaction
100
+ tx = self.router_contract.functions.exactInputSingle(params).build_transaction({
101
+ 'from': self.client.account.address,
102
+ 'nonce': self.w3.eth.get_transaction_count(self.client.account.address),
103
+ 'chainId': self.client.chain_id,
104
+ })
105
+
106
+ signed_tx = self.w3.eth.account.sign_transaction(tx, private_key=self.client.account.key)
107
+ tx_hash = self.w3.eth.send_raw_transaction(signed_tx.rawTransaction)
108
+
109
+ return self.w3.to_hex(tx_hash)
110
+
111
+ def mint_position(self, token0: str, token1: str, fee: int, amount0_desired: int, amount1_desired: int, tick_lower: int, tick_upper: int, slippage_tolerance: float = 0.01):
112
+ if not self.client.account:
113
+ raise ValueError("Local account required.")
114
+
115
+ params = {
116
+ "token0": self.w3.to_checksum_address(token0),
117
+ "token1": self.w3.to_checksum_address(token1),
118
+ "fee": fee,
119
+ "tickLower": tick_lower,
120
+ "tickUpper": tick_upper,
121
+ "amount0Desired": amount0_desired,
122
+ "amount1Desired": amount1_desired,
123
+ "amount0Min": int(amount0_desired * (1 - slippage_tolerance)),
124
+ "amount1Min": int(amount1_desired * (1 - slippage_tolerance)),
125
+ "recipient": self.client.account.address,
126
+ "deadline": int(time.time()) + 600
127
+ }
128
+
129
+ tx = self.nft_manager_contract.functions.mint(params).build_transaction({
130
+ 'from': self.client.account.address,
131
+ 'nonce': self.w3.eth.get_transaction_count(self.client.account.address),
132
+ 'chainId': self.client.chain_id,
133
+ })
134
+
135
+ signed_tx = self.w3.eth.account.sign_transaction(tx, private_key=self.client.account.key)
136
+ tx_hash = self.w3.eth.send_raw_transaction(signed_tx.rawTransaction)
137
+ return self.w3.to_hex(tx_hash)
138
+
139
+ def collect_fees(self, token_id: int):
140
+ if not self.client.account:
141
+ raise ValueError("Local account required.")
142
+
143
+ params = {
144
+ "tokenId": token_id,
145
+ "recipient": self.client.account.address,
146
+ "amount0Max": 2**128 - 1,
147
+ "amount1Max": 2**128 - 1
148
+ }
149
+
150
+ tx = self.nft_manager_contract.functions.collect(params).build_transaction({
151
+ 'from': self.client.account.address,
152
+ 'nonce': self.w3.eth.get_transaction_count(self.client.account.address),
153
+ 'chainId': self.client.chain_id,
154
+ })
155
+
156
+ signed_tx = self.w3.eth.account.sign_transaction(tx, private_key=self.client.account.key)
157
+ tx_hash = self.w3.eth.send_raw_transaction(signed_tx.rawTransaction)
158
+ return self.w3.to_hex(tx_hash)
159
+
160
+ def decrease_liquidity(self, token_id: int, liquidity_percentage: float):
161
+ if not self.client.account:
162
+ raise ValueError("Local account required.")
163
+
164
+ try:
165
+ position = self.nft_manager_contract.functions.positions(token_id).call()
166
+ current_liquidity = position[7]
167
+ except Exception:
168
+ current_liquidity = 1000000
169
+
170
+ liquidity_to_remove = int(current_liquidity * liquidity_percentage)
171
+
172
+ params = {
173
+ "tokenId": token_id,
174
+ "liquidity": liquidity_to_remove,
175
+ "amount0Min": 0,
176
+ "amount1Min": 0,
177
+ "deadline": int(time.time()) + 600
178
+ }
179
+
180
+ tx = self.nft_manager_contract.functions.decreaseLiquidity(params).build_transaction({
181
+ 'from': self.client.account.address,
182
+ 'nonce': self.w3.eth.get_transaction_count(self.client.account.address),
183
+ 'chainId': self.client.chain_id,
184
+ })
185
+
186
+ signed_tx = self.w3.eth.account.sign_transaction(tx, private_key=self.client.account.key)
187
+ tx_hash = self.w3.eth.send_raw_transaction(signed_tx.rawTransaction)
188
+ return self.w3.to_hex(tx_hash)
189
+
botchain/dex_alm.py ADDED
@@ -0,0 +1,129 @@
1
+ import math
2
+ from typing import Tuple
3
+ from .dex import BDexManager
4
+ from .tokens import TokenManager
5
+
6
+ class ALMExecutionError(Exception):
7
+ """
8
+ Raised when an ALM rebalance fails mid-sequence.
9
+ Includes stranded balances so calling agents can recover.
10
+ """
11
+ def __init__(self, message: str, stranded_amount0: int, stranded_amount1: int):
12
+ super().__init__(message)
13
+ self.stranded_amount0 = stranded_amount0
14
+ self.stranded_amount1 = stranded_amount1
15
+
16
+ class ALMMath:
17
+ @staticmethod
18
+ def tick_to_sqrt_price_x96(tick: int) -> int:
19
+ """Calculates sqrt(1.0001^tick) * 2^96"""
20
+ # In python, math.sqrt(1.0001**tick) can overflow for very large ticks,
21
+ # but is fine for normal ranges.
22
+ return int(math.sqrt(1.0001 ** tick) * (2 ** 96))
23
+
24
+ @staticmethod
25
+ def snap_tick_to_spacing(tick: int, tick_spacing: int) -> int:
26
+ """Enforces exact multiples of tick_spacing."""
27
+ return (tick // tick_spacing) * tick_spacing
28
+
29
+ @staticmethod
30
+ def calculate_ideal_swap(current_tick: int, tick_lower: int, tick_upper: int, amount0_total: int, amount1_total: int) -> Tuple[bool, int]:
31
+ """
32
+ Uses ratio r = (sqrt_pb - sqrt_p) / (sqrt_p * sqrt_pb * (sqrt_p - sqrt_pa))
33
+ Returns (is_swap_token0_for_token1, amount_to_swap).
34
+ """
35
+ if current_tick <= tick_lower:
36
+ return (False, amount1_total)
37
+ if current_tick >= tick_upper:
38
+ return (True, amount0_total)
39
+
40
+ sqrt_p = ALMMath.tick_to_sqrt_price_x96(current_tick) / (2**96)
41
+ sqrt_pa = ALMMath.tick_to_sqrt_price_x96(tick_lower) / (2**96)
42
+ sqrt_pb = ALMMath.tick_to_sqrt_price_x96(tick_upper) / (2**96)
43
+
44
+ # r is the ratio of token1 / token0 required for the new position
45
+ # r = (sqrt_pb - sqrt_p) / (sqrt_p * sqrt_pb * (sqrt_p - sqrt_pa))
46
+ # actually Uniswap V3 formula is:
47
+ # Lx = amount0 * sqrt_p * sqrt_pb / (sqrt_pb - sqrt_p)
48
+ # Ly = amount1 / (sqrt_p - sqrt_pa)
49
+ # Equating Lx = Ly gives:
50
+ # amount1 / amount0 = (sqrt_p * sqrt_pb * (sqrt_p - sqrt_pa)) / (sqrt_pb - sqrt_p)
51
+ # Wait, the user specified r = (sqrt_pb - sqrt_p) / (sqrt_p * sqrt_pb * (sqrt_p - sqrt_pa))
52
+ # which is the inverse: amount0 / amount1. Let's use the user's ratio directly.
53
+ r = (sqrt_pb - sqrt_p) / (sqrt_p * sqrt_pb * (sqrt_p - sqrt_pa))
54
+
55
+ current_price = sqrt_p * sqrt_p
56
+
57
+ # total_value_in_token0 = amount0_total + (amount1_total / current_price)
58
+ # We need amount0_target / amount1_target = r => amount0_target = r * amount1_target
59
+ # And value remains constant (ignoring fee):
60
+ # amount0_target + amount1_target / current_price = amount0_total + amount1_total / current_price
61
+
62
+ total_value_in_token1 = (amount0_total * current_price) + amount1_total
63
+ # amount1_target + (amount0_target * current_price) = total_value_in_token1
64
+ # amount1_target + (r * amount1_target * current_price) = total_value_in_token1
65
+ amount1_target = total_value_in_token1 / (1 + r * current_price)
66
+ amount0_target = r * amount1_target
67
+
68
+ if amount0_total > amount0_target:
69
+ swap_amount0 = amount0_total - amount0_target
70
+ return (True, int(swap_amount0))
71
+ else:
72
+ swap_amount1 = amount1_total - amount1_target
73
+ return (False, int(swap_amount1))
74
+
75
+ class ALMManager:
76
+ def __init__(self, dex_manager: BDexManager):
77
+ self.dex = dex_manager
78
+
79
+ def execute_optimal_rebalance(
80
+ self,
81
+ token_id: int,
82
+ token0: str,
83
+ token1: str,
84
+ fee: int,
85
+ current_tick: int,
86
+ tick_spacing: int,
87
+ range_percentage: float
88
+ ):
89
+ # TODO: Future versions will migrate this sequence to atomic execution using
90
+ # the native BOT Chain Multicall3 contract deployed at 0x9ada8A64657bE15a85C435906AF3f34b83D261cB
91
+
92
+ try:
93
+ # Step 1: Withdraw 100% liquidity
94
+ self.dex.decrease_liquidity(token_id, 1.0)
95
+
96
+ # Step 2: Collect fees
97
+ self.dex.collect_fees(token_id)
98
+ except Exception as e:
99
+ raise ALMExecutionError(f"Failed to withdraw liquidity or collect fees: {str(e)}", 0, 0)
100
+
101
+ token_manager = TokenManager(self.dex.client)
102
+ amount0_total = token_manager.get_balance(token0, self.dex.client.account.address)
103
+ amount1_total = token_manager.get_balance(token1, self.dex.client.account.address)
104
+
105
+ try:
106
+ # Step 3: Calculate and Execute Swap
107
+ tick_lower = ALMMath.snap_tick_to_spacing(int(current_tick - (range_percentage * 10000)), tick_spacing)
108
+ tick_upper = ALMMath.snap_tick_to_spacing(int(current_tick + (range_percentage * 10000)), tick_spacing)
109
+
110
+ is_swap_0_to_1, swap_amount = ALMMath.calculate_ideal_swap(current_tick, tick_lower, tick_upper, amount0_total, amount1_total)
111
+
112
+ if swap_amount > 0:
113
+ if is_swap_0_to_1:
114
+ self.dex.swap_exact_input_single(token0, token1, swap_amount, fee)
115
+ else:
116
+ self.dex.swap_exact_input_single(token1, token0, swap_amount, fee)
117
+
118
+ # Update balances after swap
119
+ new_amount0 = token_manager.get_balance(token0, self.dex.client.account.address)
120
+ new_amount1 = token_manager.get_balance(token1, self.dex.client.account.address)
121
+
122
+ # Step 4: Mint new position
123
+ self.dex.mint_position(token0, token1, fee, new_amount0, new_amount1, tick_lower, tick_upper)
124
+
125
+ except Exception as e:
126
+ # Python-level State Recovery Strategy
127
+ stranded0 = token_manager.get_balance(token0, self.dex.client.account.address)
128
+ stranded1 = token_manager.get_balance(token1, self.dex.client.account.address)
129
+ raise ALMExecutionError(f"Rebalance failed during swap or mint: {str(e)}", stranded0, stranded1)
botchain/exceptions.py ADDED
@@ -0,0 +1,7 @@
1
+ class BotChainLogsDisabledError(Exception):
2
+ """Raised when a user attempts to call eth_getLogs on the public mainnet RPC."""
3
+ def __init__(self):
4
+ super().__init__(
5
+ "eth_getLogs is disabled on the public mainnet RPC (https://rpc.botchain.ai). "
6
+ "Please use a WebSocket connection or a third-party indexer."
7
+ )
botchain/middleware.py ADDED
@@ -0,0 +1,58 @@
1
+ from web3.types import RPCEndpoint, RPCResponse
2
+ from typing import Callable, Any
3
+ import logging
4
+
5
+ logger = logging.getLogger(__name__)
6
+
7
+ def botchain_paymaster_middleware(make_request: Callable[[RPCEndpoint, Any], RPCResponse], w3: "Web3") -> Callable:
8
+ """
9
+ Middleware to intercept transactions, check for BOT Chain sponsorship,
10
+ and rewrite gas fees to zero if eligible.
11
+ """
12
+ def middleware(method: RPCEndpoint, params: Any) -> RPCResponse:
13
+ if method == "eth_sendTransaction" or method == "eth_sendRawTransaction":
14
+ # For eth_sendTransaction, we can intercept the tx dict directly
15
+ if method == "eth_sendTransaction":
16
+ tx_dict = params[0]
17
+
18
+ # Check sponsorship status via custom RPC
19
+ try:
20
+ is_sponsorable = w3.provider.make_request("pm_isSponsorable", [tx_dict])
21
+
22
+ if is_sponsorable.get("result", {}).get("Sponsorable") is True:
23
+ # Rewrite Gas to 0
24
+ tx_dict['gasPrice'] = 0
25
+ # If EIP-1559 format is used, clear those instead
26
+ if 'maxFeePerGas' in tx_dict:
27
+ tx_dict['maxFeePerGas'] = 0
28
+ if 'maxPriorityFeePerGas' in tx_dict:
29
+ tx_dict['maxPriorityFeePerGas'] = 0
30
+
31
+ # Update params
32
+ params = [tx_dict]
33
+ logger.info("Paymaster approved: Rewriting gas to 0 for sponsored transaction.")
34
+ except Exception as e:
35
+ # Log failure to reach paymaster, but proceed with normal gas
36
+ logger.warning(f"Paymaster check failed: {e}")
37
+
38
+ # Continue the request down the middleware chain
39
+ return make_request(method, params)
40
+
41
+ return middleware
42
+
43
+ def check_and_apply_sponsorship(w3: "Web3", tx_dict: dict) -> dict:
44
+ """
45
+ Helper for local signers to check pm_isSponsorable and mutate the tx_dict before signing.
46
+ """
47
+ try:
48
+ is_sponsorable = w3.provider.make_request("pm_isSponsorable", [tx_dict])
49
+ if is_sponsorable.get("result", {}).get("Sponsorable") is True:
50
+ tx_dict['gasPrice'] = 0
51
+ if 'maxFeePerGas' in tx_dict:
52
+ tx_dict['maxFeePerGas'] = 0
53
+ if 'maxPriorityFeePerGas' in tx_dict:
54
+ tx_dict['maxPriorityFeePerGas'] = 0
55
+ logger.info("Paymaster approved: Applied zero gas fees to local tx_dict.")
56
+ except Exception as e:
57
+ logger.warning(f"Paymaster check failed: {e}")
58
+ return tx_dict
botchain/tokens.py ADDED
@@ -0,0 +1,87 @@
1
+ from web3 import Web3
2
+ from .contracts import MAINNET_ADDRESSES
3
+
4
+ ERC20_ABI = [
5
+ {
6
+ "constant": True,
7
+ "inputs": [{"name": "_owner", "type": "address"}],
8
+ "name": "balanceOf",
9
+ "outputs": [{"name": "balance", "type": "uint256"}],
10
+ "type": "function"
11
+ },
12
+ {
13
+ "constant": False,
14
+ "inputs": [
15
+ {"name": "_spender", "type": "address"},
16
+ {"name": "_value", "type": "uint256"}
17
+ ],
18
+ "name": "approve",
19
+ "outputs": [{"name": "", "type": "bool"}],
20
+ "type": "function"
21
+ },
22
+ {
23
+ "constant": True,
24
+ "inputs": [
25
+ {"name": "_owner", "type": "address"},
26
+ {"name": "_spender", "type": "address"}
27
+ ],
28
+ "name": "allowance",
29
+ "outputs": [{"name": "", "type": "uint256"}],
30
+ "type": "function"
31
+ }
32
+ ]
33
+
34
+ WBOT_ABI = ERC20_ABI + [
35
+ {
36
+ "constant": False,
37
+ "inputs": [],
38
+ "name": "deposit",
39
+ "outputs": [],
40
+ "payable": True,
41
+ "type": "function"
42
+ },
43
+ {
44
+ "constant": False,
45
+ "inputs": [{"name": "wad", "type": "uint256"}],
46
+ "name": "withdraw",
47
+ "outputs": [],
48
+ "payable": False,
49
+ "type": "function"
50
+ }
51
+ ]
52
+
53
+ class TokenManager:
54
+ def __init__(self, botclient):
55
+ self.client = botclient
56
+ self.w3 = botclient.w3
57
+ self.wbot_address = self.w3.to_checksum_address(MAINNET_ADDRESSES["WBOT"])
58
+ self.wbot_contract = self.w3.eth.contract(address=self.wbot_address, abi=WBOT_ABI)
59
+
60
+ def get_erc20_contract(self, token_address: str):
61
+ return self.w3.eth.contract(address=self.w3.to_checksum_address(token_address), abi=ERC20_ABI)
62
+
63
+ def get_balance(self, token_address: str, account_address: str = None) -> int:
64
+ if not account_address:
65
+ if not self.client.account:
66
+ raise ValueError("No account address provided and client has no default account.")
67
+ account_address = self.client.account.address
68
+
69
+ contract = self.get_erc20_contract(token_address)
70
+ return contract.functions.balanceOf(self.w3.to_checksum_address(account_address)).call()
71
+
72
+ def approve(self, token_address: str, spender: str, amount: int):
73
+ if not self.client.account:
74
+ raise ValueError("Local account required to send approval transactions.")
75
+
76
+ contract = self.get_erc20_contract(token_address)
77
+ tx = contract.functions.approve(
78
+ self.w3.to_checksum_address(spender), amount
79
+ ).build_transaction({
80
+ 'from': self.client.account.address,
81
+ 'nonce': self.w3.eth.get_transaction_count(self.client.account.address),
82
+ 'chainId': self.client.chain_id,
83
+ })
84
+
85
+ signed_tx = self.w3.eth.account.sign_transaction(tx, private_key=self.client.account.key)
86
+ tx_hash = self.w3.eth.send_raw_transaction(signed_tx.rawTransaction)
87
+ return self.w3.to_hex(tx_hash)
@@ -0,0 +1,58 @@
1
+ Metadata-Version: 2.4
2
+ Name: botchain-sdk-py
3
+ Version: 0.1.0
4
+ Summary: BOT Chain SDK for Python, featuring Agentic Tooling, MCP, and ALM.
5
+ Author: Bot Chain Team
6
+ Requires-Python: >=3.8
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: web3
9
+ Requires-Dist: eth-account
10
+ Dynamic: requires-python
11
+
12
+ # BotChain Python SDK
13
+
14
+ The official Python SDK for interacting with BOT Chain, an AI-focused Layer 1 ecosystem. This SDK provides complete integration pathways for managing accounts, tokens, concentrated liquidity (BDEX V3), and executing smart contract logic autonomously via our embedded AI Agent integrations.
15
+
16
+ ## Features
17
+
18
+ - **Core Blockchain Client**: Connect to Mainnet/Testnet easily with full Web3 provider compatibility. Zero-gas transaction sponsorships natively handled.
19
+ - **BDEX V3 Concentrated Liquidity**: Manage advanced DeFi interactions.
20
+
21
+ ### AI Agent & MCP Server
22
+ This SDK natively supports Model Context Protocol (MCP) integrations for autonomous agents like OpenClaw or Claude Desktop. The embedded Agent Policy Engine ensures strict on-chain safeguards including:
23
+ - Hourly rate limits and trade cooldowns.
24
+ - Notional USD limits.
25
+ - Asset Allowlists.
26
+
27
+ Agents can discover tools (`get_balance`, `get_quote`, `execute_guarded_swap`) over standard JSON-RPC `stdio`. Run the server locally using the bundled entry point:
28
+ ```bash
29
+ botchain-mcp
30
+ ```
31
+
32
+ ### Automated Liquidity Management (ALM)
33
+ We support on-chain math utilities required to properly route and rebalance Concentrated Liquidity positions automatically.
34
+ - Exact tick conversions and boundaries.
35
+ - Precise multi-hop math allowing the SDK to compute optimal swap ratios without trusting off-chain actors.
36
+ - Fully atomic rebalances (with Python-level State Recovery for failing swaps).
37
+
38
+ ## Installation
39
+
40
+ You can install the SDK from source or directly via `pip`:
41
+ ```bash
42
+ pip install botchain-sdk-py
43
+ ```
44
+
45
+ ## Quick Start
46
+
47
+ ```python
48
+ from botchain.client import BotChain
49
+ from botchain.tokens import TokenManager
50
+
51
+ # Initialize Client
52
+ client = BotChain(network="mainnet")
53
+
54
+ # Check token balances
55
+ token_mgr = TokenManager(client)
56
+ balance = token_mgr.get_balance("0xUSDT_Address", "0xUser_Address")
57
+ print(f"USDT Balance: {balance}")
58
+ ```
@@ -0,0 +1,22 @@
1
+ botchain/__init__.py,sha256=I4ZTF_66fodbfLTc8FvvczGWexC9bA7vOGm9mfYbVus,496
2
+ botchain/client.py,sha256=9Xk5-WHWjf_YKWiTy9PhSRk1WSoDg5kyxdkKfFE13rA,3084
3
+ botchain/contracts.py,sha256=HorJQY668jFiqKIy7kAv1QRvgCca_KRRWv6BYaALLKo,429
4
+ botchain/dex.py,sha256=eAE1WoNTw-mDanEPHZFjNPbYP5Fuzj4F3EBjjvFyiOo,7862
5
+ botchain/dex_alm.py,sha256=iYrL9lERulZoMy9mEjbvRHKI8MB4lLM6ME_-L2lksg0,6065
6
+ botchain/exceptions.py,sha256=ZNBcffKfIor3bZtCZ99EQU5qdEIAWEubex8w_Jv2fNk,355
7
+ botchain/middleware.py,sha256=D4tRClQJKQKyCyoAtbX-Q6zPblAlorRGwPDSMFKwwWU,2682
8
+ botchain/tokens.py,sha256=8T745m7xE5JH5rWRP0jKqOJLSM0cFwAbZpbloWytpos,2984
9
+ botchain/agents/__init__.py,sha256=dYpJLI_J0bqegg_jxjX2ioaXHbRBn1bKaXsd4d1n2Us,122
10
+ botchain/agents/executor.py,sha256=0CH9X2WiUNpphqlKafm5fvWGs_TqJrUm9pj0BkMJla8,3422
11
+ botchain/agents/mcp.py,sha256=8o5vcSN9-fwbU1eIwsCaXM9szWsgdn9q72CGcAshsRU,81
12
+ botchain/agents/mcp_server.py,sha256=Nm7rZkBS_GIKAP6iImtKZsCnSmi4JfsdRKGGyRbI4G0,510
13
+ botchain/agents/policy.py,sha256=vNBsZe76VR4LfJw5lErvGc02YOOB_-3SKJzI9SA_NnY,4270
14
+ botchain/agents/tools/__init__.py,sha256=BKspZRElXRNzxkfex3ZdLVtLITa4egdzjPTHZ56LPWE,140
15
+ botchain/agents/tools/base.py,sha256=adMYUPHG6fd4f8Se7xbbBQwsfqglgXhaSPdEAYcLjfU,1071
16
+ botchain/agents/tools/mcp_server.py,sha256=xZCxABTtCyULREW9lO0t8CTqlp4xpAzOfa38fRntMP8,4048
17
+ botchain/agents/tools/schemas.py,sha256=LHsAuv0lPvrm8dgukaHQRUw_xxNXN9JiudqSNSiTwlE,3660
18
+ botchain_sdk_py-0.1.0.dist-info/METADATA,sha256=Azgz-_LZBpr9HnAk0CN8llCaBllDnTLSFX-_073ag1I,2194
19
+ botchain_sdk_py-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
20
+ botchain_sdk_py-0.1.0.dist-info/entry_points.txt,sha256=xcWHZXetGcqH8vz0q5v1F8X5FwUGxzP8p718xqvNmKQ,65
21
+ botchain_sdk_py-0.1.0.dist-info/top_level.txt,sha256=Z9B33ybF6tQKrPdcSLH-GnXQKyz9w4D1BZF4MaFKK7g,9
22
+ botchain_sdk_py-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ botchain-mcp = botchain.agents.mcp_server:main
@@ -0,0 +1 @@
1
+ botchain