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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Environment Variables
.env

# Per-skill local config (use config.yaml.example as template)
skills/**/config.yaml

# Python
__pycache__/
*.pyc
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ Contributors add user-facing entries under `[Unreleased]` in the same PR. Mainta

## [Unreleased]

### Added
- **`defi/evm_tx_handler`** (#142): Structured EVM agent wallet skill on Ethereum and Base — `resolve`, Uni V2 `quote`/`preview`/`execute` (approve + swap), `transfer`, `balances`, `wallet_info`, YAML registries, optional CoinGecko USD preview, `max_trade_usd` fail-closed cap, balance pre-flight checks, and mocked Web3 tests. Examples: `examples/gemini_evm_tx_handler.py`, `examples/claude_evm_tx_handler.py`.
### Changed
- **CI**: GitHub Actions installs dependencies from `pyproject.toml` only (`pip install -e ".[dev,all]"`); removed redundant manual pip pins. CI runs `pytest tests/` only; co-located `skills/**/test_skill.py` remains a local pre-PR step (#151).
- **Documentation**: [TESTING.md](docs/TESTING.md) and [CONTRIBUTING.md](CONTRIBUTING.md) aligned with CI scope and local skill-test workflow (#151).
Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ Place each skill under one top-level directory under `skills/`. Use an existing
| `compliance` | Privacy, policy, regulatory guardrails | `pii_masker`, `mica_module`, `tos_evaluator` |
| `data_engineering` | Datasets, generation, ETL-style tooling | `synthetic_generator` |
| `finance` | Blockchain, risk, financial analysis | `wallet_screening` |
| `defi` | On-chain trading and agent wallet execution | `evm_tx_handler` |
| `office` | Documents, productivity | `pdf_form_filler` |
| `optimization` | Middleware, compression, efficiency | `prompt_rewriter` |

Expand Down
6 changes: 6 additions & 0 deletions docs/skills/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ Tools for financial analysis, blockchain interaction, and regulatory compliance.
| :--- | :--- | :--- | :--- |
| **[Wallet Screening](wallet_screening.md)** | `finance/wallet_screening` | [@rosspeili](https://github.com/rosspeili) ([@ARPAHLS](https://github.com/ARPAHLS)) | Comprehensive risk assessment for Ethereum wallets. Checks sanctions lists (OFAC, FBI) and identifies interactions with malicious contracts (Mixers, Scams). |

## DeFi
On-chain execution and trading for dedicated agent wallets (structured intent, previews, confirmations).

| Skill | ID | Issuer | Description |
| :--- | :--- | :--- | :--- |
| **[EVM Transaction Handler](evm_tx_handler.md)** | `defi/evm_tx_handler` | [@Hendobox](https://github.com/Hendobox) | Uni V2 quote, preview, execute, and transfer on Ethereum/Base from structured intent (#142). |

## Optimization
Middleware skills that operate on text or state to increase performance, security, or efficiency.
Expand Down
137 changes: 137 additions & 0 deletions docs/skills/evm_tx_handler.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# EVM Transaction Handler

**ID**: `defi/evm_tx_handler`
**Issuer**: [@Hendobox](https://github.com/Hendobox) ([@ARPAHLS](https://github.com/ARPAHLS))

[Skill Library](README.md) · [Testing](../TESTING.md)

Structured EVM operations for a **dedicated agent wallet**: resolve trade intent, quote Uniswap V2 swaps, preview outcomes, execute swaps, and send native/ERC20 transfers on Ethereum and Base.

## Capabilities

| Action | Description |
|--------|-------------|
| `resolve` | Merge intent with config and YAML registries; surface missing fields |
| `quote` / `preview` | On-chain Uni V2 quote (buy/sell); optional CoinGecko USD in preview |
| `execute` | Approve (if needed) + Uni V2 swap; **re-quotes on-chain at broadcast** |
| `transfer` | Sign and send native or ERC20 (with optional confirmation gate) |
| `balances` | Wallet balances for registered tokens |
| `wallet_info` | Address, supported chains, preferences (no secrets) |
| `update_preferences` | Persist allowed keys to `config.yaml` |

## Environment

| Variable | Required | Purpose |
| :--- | :--- | :--- |
| `AGENT_WALLET_PRIVATE_KEY` | Yes (for signing) | **Dedicated agent wallet only** — hex private key in `.env`, never in tool args or YAML |
| `ETHEREUM_RPC_URL` | If using `ethereum` | JSON-RPC |
| `BASE_RPC_URL` | If using `base` | JSON-RPC |
| `COINGECKO_API_KEY` | No | USD preview and `max_trade_usd` enforcement |

### Dedicated agent wallet (required for signing)

1. Create a **new wallet** used only for this agent (limited funds).
2. Add the private key to `.env` as `AGENT_WALLET_PRIVATE_KEY` (or the name in `private_key_env` inside `config.yaml`).
3. Load `.env` via `skillware.core.env.load_env_file()` before calling the skill.
4. **Never** use a personal, treasury, or exchange hot wallet. **Never** pass the key in chat or tool arguments.

If the key is missing, the skill returns `status: missing_config` with setup guidance (see `setup` in the JSON and [API keys for skills](../usage/api_keys.md)).

Copy `skills/defi/evm_tx_handler/config.yaml.example` to `config.yaml` in the same folder for long-term defaults.

## Agent notes

- **Preview drift:** `execute` builds a fresh on-chain quote at broadcast; amounts may differ from the last preview.
- **Quote before confirm:** Call `quote` / `preview` immediately before user approval and `execute`.
- **ERC20 two-step:** Swaps may require `approve` then `swap` (see `approve_tx_hash` in the response).
- **Balances:** Insufficient funds return `status: insufficient_balance` with `agent_hint` before any transaction is sent.

## Registry data

- `data/chains.yaml` — chain IDs, RPC env keys, explorers, Uni V2 routers
- `data/tokens.yaml` — symbol → contract per chain
- `data/addressbook.yaml` — label → address (replace placeholders before mainnet use)

## Usage Examples

Guides: [Usage index](../usage/README.md) · [Agent loops](../usage/agent_loops.md) · [API keys](../usage/api_keys.md).

Sample user message: *“Buy 10 DEGEN on Base with USDC”* → `resolve` → `quote` → `preview` → user confirms → `execute` with `confirmed: true`.

### Runnable examples

See [examples/README.md](../../examples/README.md).

| Provider | Script |
| :--- | :--- |
| Gemini | `examples/gemini_evm_tx_handler.py` |
| Claude | `examples/claude_evm_tx_handler.py` |

**Demo mode (no live keys):** `EVM_TX_HANDLER_EXAMPLE_DEMO=1 python examples/gemini_evm_tx_handler.py`

### Gemini

```python
import os
import google.genai as genai
from google.genai import types
from skillware.core.env import load_env_file
from skillware.core.loader import SkillLoader

load_env_file()
bundle = SkillLoader.load_skill("defi/evm_tx_handler")
skill = bundle["module"].EvmTxHandlerSkill()
client = genai.Client()
tool = SkillLoader.to_gemini_tool(bundle)

intent = {
"side": "buy",
"chain": "base",
"target_asset": "degen",
"spend_asset": "usdc",
"amount": 10,
"amount_kind": "target_out",
}

response = client.models.generate_content(
model="gemini-2.5-flash",
contents="Resolve and quote a buy of 10 DEGEN on Base with USDC.",
config=types.GenerateContentConfig(
tools=[tool],
system_instruction=bundle["instructions"],
),
)
# On function_call (evm_tx_handler): skill.execute({"action": ..., "intent": ...})
# After preview + user approval: skill.execute({"action": "execute", "intent": intent, "confirmed": True})
```

### Claude

```python
import os
import anthropic
from skillware.core.env import load_env_file
from skillware.core.loader import SkillLoader

load_env_file()
bundle = SkillLoader.load_skill("defi/evm_tx_handler")
skill = bundle["module"].EvmTxHandlerSkill()
client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
tools = [SkillLoader.to_claude_tool(bundle)]

# On tool_use (evm_tx_handler): skill.execute(tool_use.input)
# execute example:
# skill.execute({"action": "execute", "intent": intent, "confirmed": True})
```

## Limitations

- Uniswap V2 only; Ethereum + Base.
- No cross-chain bridges or aggregators.
- Create and fund the agent wallet outside the skill; key only in `.env`.

## Security

- Fail closed on missing RPC, registry entries, missing wallet key, or USD price when `max_trade_usd` is set.
- `confirm_before_send` blocks execute/transfer until `confirmed: true`.
- Not financial or legal advice; agents can mis-parse NL — always preview.
2 changes: 2 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ with editable install: `pip install -e ".[gemini]"`.
| `gemini_pdf_form_filler.py` | `office/pdf_form_filler` | Gemini | `[gemini]`, `[office]` | `GOOGLE_API_KEY`, `ANTHROPIC_API_KEY` | Uses Gemini as the agent while the PDF skill calls Anthropic for form filling. |
| `gemini_tos_evaluator.py` | `compliance/tos_evaluator` | Gemini | `[gemini]` | `GOOGLE_API_KEY` | Runs the terms-of-service evaluator with a Gemini function-calling loop. |
| `gemini_wallet_check.py` | `finance/wallet_screening` | Gemini | `[gemini]` | `GOOGLE_API_KEY`, `ETHERSCAN_API_KEY` | Screens an Ethereum wallet with Gemini orchestration and Etherscan data. |
| `gemini_evm_tx_handler.py` | `defi/evm_tx_handler` | Gemini | `[gemini]` | `GOOGLE_API_KEY`; for live swaps also `AGENT_WALLET_PRIVATE_KEY`, `BASE_RPC_URL` or `ETHEREUM_RPC_URL`. Set `EVM_TX_HANDLER_EXAMPLE_DEMO=1` for mocked flow without keys. | Resolve → quote → preview → execute buy flow via Gemini tool loop (or demo mode). |
| `claude_evm_tx_handler.py` | `defi/evm_tx_handler` | Claude | `[claude]` | `ANTHROPIC_API_KEY`; for live swaps also `AGENT_WALLET_PRIVATE_KEY`, RPC URLs. Demo: `EVM_TX_HANDLER_EXAMPLE_DEMO=1`. | Claude tool loop for structured DeFi intent and optional execute after confirmation. |
| `mica_claude_flow.py` | `compliance/mica_module` | Claude | `[claude]` | `ANTHROPIC_API_KEY` | Runs a MiCA compliance agent loop through Claude. |
| `mica_ollama_flow.py` | `compliance/mica_module` | Ollama | No Skillware extra; install `ollama` separately | None | Runs a local Ollama MiCA flow with prompt-mode tool calling. |
| `mica_rag_flow.py` | `compliance/mica_module` | Gemini | `[gemini]` | `GOOGLE_API_KEY` | Runs the MiCA RAG flow with Gemini. |
Expand Down
106 changes: 106 additions & 0 deletions examples/claude_evm_tx_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""
Claude agent loop for defi/evm_tx_handler.

Environment (live mode):
ANTHROPIC_API_KEY
AGENT_WALLET_PRIVATE_KEY
BASE_RPC_URL and/or ETHEREUM_RPC_URL
optional COINGECKO_API_KEY

Demo mode (mocked Web3, no live keys):
EVM_TX_HANDLER_EXAMPLE_DEMO=1 python examples/claude_evm_tx_handler.py
"""

import json
import os
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))

from evm_tx_handler_common import ( # noqa: E402
SKILL_ID,
demo_mode_enabled,
handle_tool_call,
load_skill,
run_scripted_flow,
)
from skillware.core.env import load_env_file # noqa: E402
from skillware.core.loader import SkillLoader # noqa: E402


def main() -> None:
load_env_file()

if demo_mode_enabled():
print("DEMO MODE: mocked Web3 — no live RPC or signing.\n")
with load_skill() as skill:
run_scripted_flow(skill, execute=True)
return

import anthropic

bundle = SkillLoader.load_skill(SKILL_ID)
skill = bundle["module"].EvmTxHandlerSkill()
client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
tools = [SkillLoader.to_claude_tool(bundle)]
tool_name = bundle["manifest"]["name"]

user_query = (
"Plan a buy of 10 DEGEN on Base with USDC: resolve, quote, preview, "
"then execute only after explicit user confirmation."
)
print(f"User: {user_query}\n")

message = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=2048,
system=bundle["instructions"],
messages=[{"role": "user", "content": user_query}],
tools=tools,
)

messages = [{"role": "user", "content": user_query}]

while message.stop_reason == "tool_use":
tool_use = next(block for block in message.content if block.type == "tool_use")
print(f"Claude tool call: {tool_use.name} -> {json.dumps(tool_use.input)}")

if tool_use.name != tool_name:
print(f"Unknown tool: {tool_use.name}")
break

result = handle_tool_call(skill, tool_use.input)
print(f"Skill result: {json.dumps(result, indent=2)[:2000]}...\n")

messages = [
*messages,
{"role": "assistant", "content": message.content},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": tool_use.id,
"content": json.dumps(result),
}
],
},
]

message = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=2048,
system=bundle["instructions"],
tools=tools,
messages=messages,
)

print("\nAgent final response:")
for block in message.content:
if hasattr(block, "text"):
print(block.text)


if __name__ == "__main__":
main()
Loading
Loading