cryptoagent-ai 0.1.0__tar.gz
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.
- cryptoagent_ai-0.1.0/.claude/settings.local.json +15 -0
- cryptoagent_ai-0.1.0/.env.example +25 -0
- cryptoagent_ai-0.1.0/LICENSE +21 -0
- cryptoagent_ai-0.1.0/PKG-INFO +96 -0
- cryptoagent_ai-0.1.0/README.md +58 -0
- cryptoagent_ai-0.1.0/chain/__init__.py +0 -0
- cryptoagent_ai-0.1.0/chain/abi_registry.py +57 -0
- cryptoagent_ai-0.1.0/chain/client.py +207 -0
- cryptoagent_ai-0.1.0/chain/constants.py +122 -0
- cryptoagent_ai-0.1.0/chain/multicall.py +128 -0
- cryptoagent_ai-0.1.0/chain/provider.py +134 -0
- cryptoagent_ai-0.1.0/chain/transaction.py +158 -0
- cryptoagent_ai-0.1.0/cli/__init__.py +0 -0
- cryptoagent_ai-0.1.0/cli/main.py +271 -0
- cryptoagent_ai-0.1.0/config/__init__.py +0 -0
- cryptoagent_ai-0.1.0/config/agents.yaml +69 -0
- cryptoagent_ai-0.1.0/config/chains.yaml +64 -0
- cryptoagent_ai-0.1.0/config/protocols.yaml +44 -0
- cryptoagent_ai-0.1.0/config/settings.py +100 -0
- cryptoagent_ai-0.1.0/core/__init__.py +0 -0
- cryptoagent_ai-0.1.0/core/agent.py +284 -0
- cryptoagent_ai-0.1.0/core/brain.py +171 -0
- cryptoagent_ai-0.1.0/core/exceptions.py +153 -0
- cryptoagent_ai-0.1.0/core/tool_registry.py +159 -0
- cryptoagent_ai-0.1.0/core/types.py +148 -0
- cryptoagent_ai-0.1.0/events/__init__.py +0 -0
- cryptoagent_ai-0.1.0/events/bus.py +93 -0
- cryptoagent_ai-0.1.0/events/monitor.py +73 -0
- cryptoagent_ai-0.1.0/events/price_feed.py +110 -0
- cryptoagent_ai-0.1.0/events/triggers.py +82 -0
- cryptoagent_ai-0.1.0/memory/__init__.py +0 -0
- cryptoagent_ai-0.1.0/memory/models.py +87 -0
- cryptoagent_ai-0.1.0/memory/portfolio.py +88 -0
- cryptoagent_ai-0.1.0/memory/store.py +185 -0
- cryptoagent_ai-0.1.0/orchestration/__init__.py +0 -0
- cryptoagent_ai-0.1.0/orchestration/coordinator.py +84 -0
- cryptoagent_ai-0.1.0/orchestration/scheduler.py +88 -0
- cryptoagent_ai-0.1.0/orchestration/strategies.py +100 -0
- cryptoagent_ai-0.1.0/protocols/__init__.py +0 -0
- cryptoagent_ai-0.1.0/protocols/aave/__init__.py +205 -0
- cryptoagent_ai-0.1.0/protocols/base.py +34 -0
- cryptoagent_ai-0.1.0/protocols/erc20/__init__.py +87 -0
- cryptoagent_ai-0.1.0/protocols/registry.py +60 -0
- cryptoagent_ai-0.1.0/protocols/uniswap/__init__.py +195 -0
- cryptoagent_ai-0.1.0/pyproject.toml +78 -0
- cryptoagent_ai-0.1.0/safety/__init__.py +0 -0
- cryptoagent_ai-0.1.0/safety/approval.py +89 -0
- cryptoagent_ai-0.1.0/safety/audit.py +107 -0
- cryptoagent_ai-0.1.0/safety/guardian.py +119 -0
- cryptoagent_ai-0.1.0/safety/limits.py +72 -0
- cryptoagent_ai-0.1.0/safety/rules.py +159 -0
- cryptoagent_ai-0.1.0/safety/slippage.py +58 -0
- cryptoagent_ai-0.1.0/tools/__init__.py +0 -0
- cryptoagent_ai-0.1.0/tools/blockchain_tools.py +88 -0
- cryptoagent_ai-0.1.0/tools/defi_tools.py +127 -0
- cryptoagent_ai-0.1.0/tools/memory_tools.py +62 -0
- cryptoagent_ai-0.1.0/tools/portfolio_tools.py +45 -0
- cryptoagent_ai-0.1.0/tools/wallet_tools.py +45 -0
- cryptoagent_ai-0.1.0/wallet/__init__.py +0 -0
- cryptoagent_ai-0.1.0/wallet/hd.py +60 -0
- cryptoagent_ai-0.1.0/wallet/keystore.py +104 -0
- cryptoagent_ai-0.1.0/wallet/manager.py +135 -0
- cryptoagent_ai-0.1.0/wallet/signer.py +68 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"WebSearch",
|
|
5
|
+
"Bash(pip install:*)",
|
|
6
|
+
"Bash(python -c:*)",
|
|
7
|
+
"Bash(python -m cli.main:*)",
|
|
8
|
+
"Bash(python -m build:*)",
|
|
9
|
+
"Bash(twine check:*)",
|
|
10
|
+
"Bash(twine upload:*)",
|
|
11
|
+
"Bash(TWINE_USERNAME=__token__ TWINE_PASSWORD=pypi-AgEIcHlwaS5vcmcCJDk1YThkNjc3LWNjNTUtNGU1Mi05YTkzLTgwYWVkOGM2OGQzMgACKlszLCI3YjVlODQzZC01M2E3LTRkMTQtOGNjMS0wZDg5NDU3OWEzMzUiXQAABiDC0ffAMKgCvZqXdhEztxeHys83JcIz6R--q-mJPGzwCw twine upload:*)",
|
|
12
|
+
"Bash(pip index:*)"
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Claude API
|
|
2
|
+
ANTHROPIC_API_KEY=sk-ant-...
|
|
3
|
+
|
|
4
|
+
# RPC Endpoints (override defaults in chains.yaml)
|
|
5
|
+
ETH_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY
|
|
6
|
+
BASE_RPC_URL=https://base-mainnet.g.alchemy.com/v2/YOUR_KEY
|
|
7
|
+
ARBITRUM_RPC_URL=https://arb-mainnet.g.alchemy.com/v2/YOUR_KEY
|
|
8
|
+
POLYGON_RPC_URL=https://polygon-mainnet.g.alchemy.com/v2/YOUR_KEY
|
|
9
|
+
|
|
10
|
+
# Wallet encryption password
|
|
11
|
+
WALLET_PASSWORD=change-me-to-a-strong-password
|
|
12
|
+
|
|
13
|
+
# Safety
|
|
14
|
+
MAX_TX_VALUE_USD=100.0
|
|
15
|
+
DAILY_SPENDING_LIMIT_USD=1000.0
|
|
16
|
+
REQUIRE_HUMAN_APPROVAL_ABOVE_USD=50.0
|
|
17
|
+
|
|
18
|
+
# Price API (optional, falls back to Chainlink on-chain)
|
|
19
|
+
COINGECKO_API_KEY=
|
|
20
|
+
|
|
21
|
+
# Database
|
|
22
|
+
DATABASE_URL=sqlite+aiosqlite:///data/agent.db
|
|
23
|
+
|
|
24
|
+
# Logging
|
|
25
|
+
LOG_LEVEL=INFO
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 coins
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cryptoagent-ai
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Autonomous AI agents that own wallets and transact on EVM chains
|
|
5
|
+
Project-URL: Homepage, https://github.com/coins/cryptoagent-ai
|
|
6
|
+
Project-URL: Repository, https://github.com/coins/cryptoagent-ai
|
|
7
|
+
Author-email: coins <gobeyondfj@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: agent,ai,claude,crypto,defi,ethereum,web3
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Requires-Dist: aiosqlite>=0.20.0
|
|
19
|
+
Requires-Dist: anthropic>=0.40.0
|
|
20
|
+
Requires-Dist: cryptography>=44.0.0
|
|
21
|
+
Requires-Dist: eth-account>=0.13.0
|
|
22
|
+
Requires-Dist: httpx>=0.28.0
|
|
23
|
+
Requires-Dist: mnemonic>=0.21
|
|
24
|
+
Requires-Dist: pydantic-settings>=2.6.0
|
|
25
|
+
Requires-Dist: pyyaml>=6.0.2
|
|
26
|
+
Requires-Dist: rich>=13.9.0
|
|
27
|
+
Requires-Dist: sqlmodel>=0.0.22
|
|
28
|
+
Requires-Dist: structlog>=24.4.0
|
|
29
|
+
Requires-Dist: tenacity>=9.0.0
|
|
30
|
+
Requires-Dist: typer[all]>=0.12.0
|
|
31
|
+
Requires-Dist: web3>=7.0.0
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: pytest-asyncio>=0.24.0; extra == 'dev'
|
|
34
|
+
Requires-Dist: pytest-cov>=6.0; extra == 'dev'
|
|
35
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
36
|
+
Requires-Dist: ruff>=0.8.0; extra == 'dev'
|
|
37
|
+
Description-Content-Type: text/markdown
|
|
38
|
+
|
|
39
|
+
# CryptoAgent AI
|
|
40
|
+
|
|
41
|
+
Autonomous AI agents that own wallets and independently transact on EVM chains (Ethereum, Base, Arbitrum, Polygon), powered by Claude API.
|
|
42
|
+
|
|
43
|
+
## Features
|
|
44
|
+
|
|
45
|
+
- **HD Wallets** — BIP-44 derivation with Fernet+PBKDF2 encrypted key storage
|
|
46
|
+
- **Multi-chain** — Ethereum, Base, Arbitrum, Polygon with failover RPC
|
|
47
|
+
- **DeFi Protocols** — Uniswap V3 swaps, Aave V3 lending
|
|
48
|
+
- **Safety First** — Spending limits, slippage guards, human approval, audit log
|
|
49
|
+
- **AI Agent Loop** — Perceive → Reason (Claude) → Act → Reflect
|
|
50
|
+
- **16 Tools** — Balances, swaps, lending, portfolio, memory — all exposed to Claude
|
|
51
|
+
|
|
52
|
+
## Install
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install cryptoagent-ai
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Quick Start
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# Show config
|
|
62
|
+
cryptoagent info
|
|
63
|
+
|
|
64
|
+
# Create a wallet
|
|
65
|
+
cryptoagent wallet create my-wallet -p "strong-password"
|
|
66
|
+
|
|
67
|
+
# Start an agent
|
|
68
|
+
export ANTHROPIC_API_KEY=sk-ant-...
|
|
69
|
+
cryptoagent agent start --wallet my-wallet -p "strong-password"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Architecture
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
EventMonitor → PERCEIVE (balances, events, portfolio)
|
|
76
|
+
↓
|
|
77
|
+
REASON (Claude + tool-use)
|
|
78
|
+
↓
|
|
79
|
+
ACT (Guardian safety checks → execute → audit)
|
|
80
|
+
↓
|
|
81
|
+
REFLECT (update portfolio, log metrics)
|
|
82
|
+
↓
|
|
83
|
+
sleep → loop
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Security
|
|
87
|
+
|
|
88
|
+
- Keys encrypted at rest (Fernet + PBKDF2, 600k iterations)
|
|
89
|
+
- Per-transaction, hourly, and daily USD spending caps
|
|
90
|
+
- Human-in-the-loop approval for high-value transactions
|
|
91
|
+
- Slippage protection on all swaps
|
|
92
|
+
- Append-only audit log
|
|
93
|
+
|
|
94
|
+
## License
|
|
95
|
+
|
|
96
|
+
MIT
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# CryptoAgent AI
|
|
2
|
+
|
|
3
|
+
Autonomous AI agents that own wallets and independently transact on EVM chains (Ethereum, Base, Arbitrum, Polygon), powered by Claude API.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **HD Wallets** — BIP-44 derivation with Fernet+PBKDF2 encrypted key storage
|
|
8
|
+
- **Multi-chain** — Ethereum, Base, Arbitrum, Polygon with failover RPC
|
|
9
|
+
- **DeFi Protocols** — Uniswap V3 swaps, Aave V3 lending
|
|
10
|
+
- **Safety First** — Spending limits, slippage guards, human approval, audit log
|
|
11
|
+
- **AI Agent Loop** — Perceive → Reason (Claude) → Act → Reflect
|
|
12
|
+
- **16 Tools** — Balances, swaps, lending, portfolio, memory — all exposed to Claude
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install cryptoagent-ai
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# Show config
|
|
24
|
+
cryptoagent info
|
|
25
|
+
|
|
26
|
+
# Create a wallet
|
|
27
|
+
cryptoagent wallet create my-wallet -p "strong-password"
|
|
28
|
+
|
|
29
|
+
# Start an agent
|
|
30
|
+
export ANTHROPIC_API_KEY=sk-ant-...
|
|
31
|
+
cryptoagent agent start --wallet my-wallet -p "strong-password"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Architecture
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
EventMonitor → PERCEIVE (balances, events, portfolio)
|
|
38
|
+
↓
|
|
39
|
+
REASON (Claude + tool-use)
|
|
40
|
+
↓
|
|
41
|
+
ACT (Guardian safety checks → execute → audit)
|
|
42
|
+
↓
|
|
43
|
+
REFLECT (update portfolio, log metrics)
|
|
44
|
+
↓
|
|
45
|
+
sleep → loop
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Security
|
|
49
|
+
|
|
50
|
+
- Keys encrypted at rest (Fernet + PBKDF2, 600k iterations)
|
|
51
|
+
- Per-transaction, hourly, and daily USD spending caps
|
|
52
|
+
- Human-in-the-loop approval for high-value transactions
|
|
53
|
+
- Slippage protection on all swaps
|
|
54
|
+
- Append-only audit log
|
|
55
|
+
|
|
56
|
+
## License
|
|
57
|
+
|
|
58
|
+
MIT
|
|
File without changes
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""ABI loading and caching registry."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from chain.constants import ERC20_ABI
|
|
10
|
+
|
|
11
|
+
# Built-in ABIs
|
|
12
|
+
_BUILTIN_ABIS: dict[str, list[dict[str, Any]]] = {
|
|
13
|
+
"erc20": ERC20_ABI,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
# File-based ABI cache
|
|
17
|
+
_ABI_DIR = Path("config/abis")
|
|
18
|
+
_cache: dict[str, list[dict[str, Any]]] = {}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_abi(name: str) -> list[dict[str, Any]]:
|
|
22
|
+
"""Get an ABI by name.
|
|
23
|
+
|
|
24
|
+
Checks built-in ABIs first, then file cache, then config/abis/ directory.
|
|
25
|
+
"""
|
|
26
|
+
# Built-in
|
|
27
|
+
if name in _BUILTIN_ABIS:
|
|
28
|
+
return _BUILTIN_ABIS[name]
|
|
29
|
+
|
|
30
|
+
# Memory cache
|
|
31
|
+
if name in _cache:
|
|
32
|
+
return _cache[name]
|
|
33
|
+
|
|
34
|
+
# Load from file
|
|
35
|
+
path = _ABI_DIR / f"{name}.json"
|
|
36
|
+
if path.exists():
|
|
37
|
+
with open(path) as f:
|
|
38
|
+
abi = json.load(f)
|
|
39
|
+
_cache[name] = abi
|
|
40
|
+
return abi
|
|
41
|
+
|
|
42
|
+
raise ValueError(f"ABI not found: {name}")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def register_abi(name: str, abi: list[dict[str, Any]]) -> None:
|
|
46
|
+
"""Register an ABI in the runtime cache."""
|
|
47
|
+
_cache[name] = abi
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def save_abi(name: str, abi: list[dict[str, Any]]) -> Path:
|
|
51
|
+
"""Save an ABI to the config/abis directory."""
|
|
52
|
+
_ABI_DIR.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
path = _ABI_DIR / f"{name}.json"
|
|
54
|
+
with open(path, "w") as f:
|
|
55
|
+
json.dump(abi, f, indent=2)
|
|
56
|
+
_cache[name] = abi
|
|
57
|
+
return path
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""High-level blockchain client for read/write operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import structlog
|
|
8
|
+
from web3 import AsyncWeb3
|
|
9
|
+
|
|
10
|
+
from chain.constants import ERC20_ABI, NATIVE_TOKEN
|
|
11
|
+
from chain.provider import ChainProvider, MultiChainProvider
|
|
12
|
+
from chain.transaction import NonceManager, TransactionBuilder
|
|
13
|
+
from core.exceptions import (
|
|
14
|
+
ChainError,
|
|
15
|
+
InsufficientFundsError,
|
|
16
|
+
TransactionError,
|
|
17
|
+
TransactionRevertedError,
|
|
18
|
+
)
|
|
19
|
+
from core.types import TokenInfo, TransactionReceipt, TransactionRequest
|
|
20
|
+
from wallet.signer import TransactionSigner
|
|
21
|
+
|
|
22
|
+
logger = structlog.get_logger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ChainClient:
|
|
26
|
+
"""High-level async client for reading and writing to EVM chains."""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
providers: MultiChainProvider,
|
|
31
|
+
signer: TransactionSigner,
|
|
32
|
+
nonce_manager: NonceManager | None = None,
|
|
33
|
+
) -> None:
|
|
34
|
+
self._providers = providers
|
|
35
|
+
self._signer = signer
|
|
36
|
+
self._nonce_manager = nonce_manager or NonceManager()
|
|
37
|
+
|
|
38
|
+
def _get_provider(self, chain_id: int) -> ChainProvider:
|
|
39
|
+
return self._providers.get(chain_id)
|
|
40
|
+
|
|
41
|
+
# ── Read Operations ────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
async def get_eth_balance(self, chain_id: int, address: str | None = None) -> int:
|
|
44
|
+
"""Get native token balance in wei."""
|
|
45
|
+
address = address or self._signer.address
|
|
46
|
+
provider = self._get_provider(chain_id)
|
|
47
|
+
return await provider.get_balance(address)
|
|
48
|
+
|
|
49
|
+
async def get_token_balance(
|
|
50
|
+
self, chain_id: int, token_address: str, address: str | None = None
|
|
51
|
+
) -> int:
|
|
52
|
+
"""Get ERC-20 token balance (raw units)."""
|
|
53
|
+
address = address or self._signer.address
|
|
54
|
+
provider = self._get_provider(chain_id)
|
|
55
|
+
contract = provider.w3.eth.contract(
|
|
56
|
+
address=AsyncWeb3.to_checksum_address(token_address),
|
|
57
|
+
abi=ERC20_ABI,
|
|
58
|
+
)
|
|
59
|
+
return await contract.functions.balanceOf(
|
|
60
|
+
AsyncWeb3.to_checksum_address(address)
|
|
61
|
+
).call()
|
|
62
|
+
|
|
63
|
+
async def get_token_info(self, chain_id: int, token_address: str) -> TokenInfo:
|
|
64
|
+
"""Get token metadata (symbol, decimals, name)."""
|
|
65
|
+
provider = self._get_provider(chain_id)
|
|
66
|
+
contract = provider.w3.eth.contract(
|
|
67
|
+
address=AsyncWeb3.to_checksum_address(token_address),
|
|
68
|
+
abi=ERC20_ABI,
|
|
69
|
+
)
|
|
70
|
+
symbol, decimals, name = await asyncio.gather(
|
|
71
|
+
contract.functions.symbol().call(),
|
|
72
|
+
contract.functions.decimals().call(),
|
|
73
|
+
contract.functions.name().call(),
|
|
74
|
+
)
|
|
75
|
+
return TokenInfo(
|
|
76
|
+
address=token_address,
|
|
77
|
+
symbol=symbol,
|
|
78
|
+
decimals=decimals,
|
|
79
|
+
chain_id=chain_id,
|
|
80
|
+
name=name,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
async def get_allowance(
|
|
84
|
+
self,
|
|
85
|
+
chain_id: int,
|
|
86
|
+
token_address: str,
|
|
87
|
+
spender: str,
|
|
88
|
+
owner: str | None = None,
|
|
89
|
+
) -> int:
|
|
90
|
+
"""Get ERC-20 allowance."""
|
|
91
|
+
owner = owner or self._signer.address
|
|
92
|
+
provider = self._get_provider(chain_id)
|
|
93
|
+
contract = provider.w3.eth.contract(
|
|
94
|
+
address=AsyncWeb3.to_checksum_address(token_address),
|
|
95
|
+
abi=ERC20_ABI,
|
|
96
|
+
)
|
|
97
|
+
return await contract.functions.allowance(
|
|
98
|
+
AsyncWeb3.to_checksum_address(owner),
|
|
99
|
+
AsyncWeb3.to_checksum_address(spender),
|
|
100
|
+
).call()
|
|
101
|
+
|
|
102
|
+
async def get_gas_price(self, chain_id: int) -> int:
|
|
103
|
+
"""Get current gas price in wei."""
|
|
104
|
+
provider = self._get_provider(chain_id)
|
|
105
|
+
return await provider.get_gas_price()
|
|
106
|
+
|
|
107
|
+
async def get_block_number(self, chain_id: int) -> int:
|
|
108
|
+
provider = self._get_provider(chain_id)
|
|
109
|
+
return await provider.get_block_number()
|
|
110
|
+
|
|
111
|
+
# ── Write Operations ───────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
async def send_transaction(self, request: TransactionRequest) -> TransactionReceipt:
|
|
114
|
+
"""Build, sign, send, and wait for a transaction."""
|
|
115
|
+
provider = self._get_provider(request.chain_id)
|
|
116
|
+
builder = TransactionBuilder(provider, self._nonce_manager)
|
|
117
|
+
|
|
118
|
+
# Build tx dict
|
|
119
|
+
tx_dict = await builder.build(request, self._signer.address)
|
|
120
|
+
|
|
121
|
+
# Check balance
|
|
122
|
+
balance = await provider.get_balance(self._signer.address)
|
|
123
|
+
total_cost = request.value + (tx_dict["gas"] * tx_dict["maxFeePerGas"])
|
|
124
|
+
if balance < total_cost:
|
|
125
|
+
raise InsufficientFundsError(
|
|
126
|
+
f"Balance {balance} wei < required {total_cost} wei"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Sign
|
|
130
|
+
raw_tx = self._signer.sign_transaction(tx_dict)
|
|
131
|
+
|
|
132
|
+
# Send
|
|
133
|
+
try:
|
|
134
|
+
tx_hash = await provider.call("eth.send_raw_transaction", raw_tx)
|
|
135
|
+
self._nonce_manager.confirm_nonce(
|
|
136
|
+
request.chain_id, self._signer.address, tx_dict["nonce"]
|
|
137
|
+
)
|
|
138
|
+
except Exception as e:
|
|
139
|
+
self._nonce_manager.reset(request.chain_id, self._signer.address)
|
|
140
|
+
raise TransactionError(f"Failed to send transaction: {e}") from e
|
|
141
|
+
|
|
142
|
+
# Wait for receipt
|
|
143
|
+
logger.info("transaction_sent", tx_hash=tx_hash.hex(), chain_id=request.chain_id)
|
|
144
|
+
receipt = await provider.call("eth.wait_for_transaction_receipt", tx_hash, timeout=120)
|
|
145
|
+
|
|
146
|
+
result = TransactionReceipt(
|
|
147
|
+
tx_hash=receipt["transactionHash"].hex(),
|
|
148
|
+
chain_id=request.chain_id,
|
|
149
|
+
block_number=receipt["blockNumber"],
|
|
150
|
+
gas_used=receipt["gasUsed"],
|
|
151
|
+
status=receipt["status"],
|
|
152
|
+
logs=[dict(log) for log in receipt.get("logs", [])],
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
if not result.success:
|
|
156
|
+
raise TransactionRevertedError(result.tx_hash, "Transaction reverted")
|
|
157
|
+
|
|
158
|
+
logger.info(
|
|
159
|
+
"transaction_confirmed",
|
|
160
|
+
tx_hash=result.tx_hash,
|
|
161
|
+
block=result.block_number,
|
|
162
|
+
gas_used=result.gas_used,
|
|
163
|
+
)
|
|
164
|
+
return result
|
|
165
|
+
|
|
166
|
+
async def send_eth(
|
|
167
|
+
self, chain_id: int, to: str, value_wei: int, description: str = ""
|
|
168
|
+
) -> TransactionReceipt:
|
|
169
|
+
"""Send native ETH/MATIC."""
|
|
170
|
+
request = TransactionRequest(
|
|
171
|
+
chain_id=chain_id,
|
|
172
|
+
to=to,
|
|
173
|
+
value=value_wei,
|
|
174
|
+
description=description or f"Send {value_wei} wei to {to}",
|
|
175
|
+
)
|
|
176
|
+
return await self.send_transaction(request)
|
|
177
|
+
|
|
178
|
+
async def approve_token(
|
|
179
|
+
self,
|
|
180
|
+
chain_id: int,
|
|
181
|
+
token_address: str,
|
|
182
|
+
spender: str,
|
|
183
|
+
amount: int | None = None,
|
|
184
|
+
) -> TransactionReceipt:
|
|
185
|
+
"""Approve an ERC-20 token for spending."""
|
|
186
|
+
max_uint256 = 2**256 - 1
|
|
187
|
+
amount = amount if amount is not None else max_uint256
|
|
188
|
+
provider = self._get_provider(chain_id)
|
|
189
|
+
contract = provider.w3.eth.contract(
|
|
190
|
+
address=AsyncWeb3.to_checksum_address(token_address),
|
|
191
|
+
abi=ERC20_ABI,
|
|
192
|
+
)
|
|
193
|
+
data = contract.functions.approve(
|
|
194
|
+
AsyncWeb3.to_checksum_address(spender), amount
|
|
195
|
+
)._encode_transaction_data()
|
|
196
|
+
|
|
197
|
+
request = TransactionRequest(
|
|
198
|
+
chain_id=chain_id,
|
|
199
|
+
to=token_address,
|
|
200
|
+
data=bytes.fromhex(data[2:]) if isinstance(data, str) else data,
|
|
201
|
+
description=f"Approve {spender} to spend token {token_address}",
|
|
202
|
+
)
|
|
203
|
+
return await self.send_transaction(request)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
# Missing import
|
|
207
|
+
import asyncio
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Well-known contract addresses per chain."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from core.types import ChainId
|
|
6
|
+
|
|
7
|
+
# Native ETH sentinel address (used by protocols to represent native gas token)
|
|
8
|
+
NATIVE_TOKEN = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
|
|
9
|
+
|
|
10
|
+
# WETH addresses per chain
|
|
11
|
+
WETH: dict[int, str] = {
|
|
12
|
+
ChainId.ETHEREUM: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
|
13
|
+
ChainId.BASE: "0x4200000000000000000000000000000000000006",
|
|
14
|
+
ChainId.ARBITRUM: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
|
|
15
|
+
ChainId.POLYGON: "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270", # WMATIC
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
# USDC addresses per chain
|
|
19
|
+
USDC: dict[int, str] = {
|
|
20
|
+
ChainId.ETHEREUM: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
|
21
|
+
ChainId.BASE: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
22
|
+
ChainId.ARBITRUM: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
|
|
23
|
+
ChainId.POLYGON: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
# USDT addresses per chain
|
|
27
|
+
USDT: dict[int, str] = {
|
|
28
|
+
ChainId.ETHEREUM: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
|
29
|
+
ChainId.ARBITRUM: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9",
|
|
30
|
+
ChainId.POLYGON: "0xc2132D05D31c914a87C6611C10748AEb04B58e8F",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# DAI addresses per chain
|
|
34
|
+
DAI: dict[int, str] = {
|
|
35
|
+
ChainId.ETHEREUM: "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
|
36
|
+
ChainId.BASE: "0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb",
|
|
37
|
+
ChainId.ARBITRUM: "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1",
|
|
38
|
+
ChainId.POLYGON: "0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Multicall3 — same address on all supported chains
|
|
42
|
+
MULTICALL3 = "0xcA11bde05977b3631167028862bE2a173976CA11"
|
|
43
|
+
|
|
44
|
+
# ERC-20 minimal ABI for balance/approve/transfer
|
|
45
|
+
ERC20_ABI = [
|
|
46
|
+
{
|
|
47
|
+
"constant": True,
|
|
48
|
+
"inputs": [{"name": "account", "type": "address"}],
|
|
49
|
+
"name": "balanceOf",
|
|
50
|
+
"outputs": [{"name": "", "type": "uint256"}],
|
|
51
|
+
"type": "function",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"constant": True,
|
|
55
|
+
"inputs": [],
|
|
56
|
+
"name": "decimals",
|
|
57
|
+
"outputs": [{"name": "", "type": "uint8"}],
|
|
58
|
+
"type": "function",
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"constant": True,
|
|
62
|
+
"inputs": [],
|
|
63
|
+
"name": "symbol",
|
|
64
|
+
"outputs": [{"name": "", "type": "string"}],
|
|
65
|
+
"type": "function",
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"constant": True,
|
|
69
|
+
"inputs": [],
|
|
70
|
+
"name": "name",
|
|
71
|
+
"outputs": [{"name": "", "type": "string"}],
|
|
72
|
+
"type": "function",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"constant": True,
|
|
76
|
+
"inputs": [
|
|
77
|
+
{"name": "owner", "type": "address"},
|
|
78
|
+
{"name": "spender", "type": "address"},
|
|
79
|
+
],
|
|
80
|
+
"name": "allowance",
|
|
81
|
+
"outputs": [{"name": "", "type": "uint256"}],
|
|
82
|
+
"type": "function",
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"constant": False,
|
|
86
|
+
"inputs": [
|
|
87
|
+
{"name": "spender", "type": "address"},
|
|
88
|
+
{"name": "amount", "type": "uint256"},
|
|
89
|
+
],
|
|
90
|
+
"name": "approve",
|
|
91
|
+
"outputs": [{"name": "", "type": "bool"}],
|
|
92
|
+
"type": "function",
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"constant": False,
|
|
96
|
+
"inputs": [
|
|
97
|
+
{"name": "to", "type": "address"},
|
|
98
|
+
{"name": "amount", "type": "uint256"},
|
|
99
|
+
],
|
|
100
|
+
"name": "transfer",
|
|
101
|
+
"outputs": [{"name": "", "type": "bool"}],
|
|
102
|
+
"type": "function",
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"constant": False,
|
|
106
|
+
"inputs": [
|
|
107
|
+
{"name": "from", "type": "address"},
|
|
108
|
+
{"name": "to", "type": "address"},
|
|
109
|
+
{"name": "amount", "type": "uint256"},
|
|
110
|
+
],
|
|
111
|
+
"name": "transferFrom",
|
|
112
|
+
"outputs": [{"name": "", "type": "bool"}],
|
|
113
|
+
"type": "function",
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"constant": True,
|
|
117
|
+
"inputs": [],
|
|
118
|
+
"name": "totalSupply",
|
|
119
|
+
"outputs": [{"name": "", "type": "uint256"}],
|
|
120
|
+
"type": "function",
|
|
121
|
+
},
|
|
122
|
+
]
|