Skip to content
Closed
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: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ module github.com/ObolNetwork/obol-stack
go 1.25.1

require (
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0
github.com/dustinkirkland/golang-petname v0.0.0-20240428194347-eebcea082ee0
github.com/mark3labs/x402-go v0.13.0
github.com/urfave/cli/v2 v2.27.7
golang.org/x/crypto v0.45.0
gopkg.in/yaml.v3 v3.0.1
)

Expand Down Expand Up @@ -37,7 +39,6 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/ratelimit v0.3.1 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/term v0.37.0 // indirect
golang.org/x/time v0.14.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6N
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/dustinkirkland/golang-petname v0.0.0-20240428194347-eebcea082ee0 h1:aYo8nnk3ojoQkP5iErif5Xxv0Mo0Ga/FR5+ffl/7+Nk=
github.com/dustinkirkland/golang-petname v0.0.0-20240428194347-eebcea082ee0/go.mod h1:8AuBTZBRSFqEYBPYULd+NN474/zZBLP+6WeT5S9xlAc=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
Expand Down
21 changes: 10 additions & 11 deletions internal/embed/embed_skills_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package embed
import (
"os"
"path/filepath"
"sort"
"testing"
)

Expand All @@ -13,15 +12,15 @@ func TestGetEmbeddedSkillNames(t *testing.T) {
t.Fatalf("unexpected error: %v", err)
}

want := []string{"distributed-validators", "ethereum-networks", "ethereum-wallet", "obol-stack"}
sort.Strings(names)

if len(names) != len(want) {
t.Fatalf("got %d skills %v, want %d %v", len(names), names, len(want), want)
// Core skills that must always be present
required := []string{"distributed-validators", "ethereum-networks", "local-wallet", "obol-stack"}
nameSet := make(map[string]bool, len(names))
for _, n := range names {
nameSet[n] = true
}
for i := range want {
if names[i] != want[i] {
t.Errorf("skill[%d] = %q, want %q", i, names[i], want[i])
for _, r := range required {
if !nameSet[r] {
t.Errorf("required skill %q not found in embedded skills %v", r, names)
}
}
}
Expand All @@ -34,7 +33,7 @@ func TestCopySkills(t *testing.T) {
}

// Every skill must have a SKILL.md
skills := []string{"distributed-validators", "ethereum-networks", "ethereum-wallet", "obol-stack"}
skills := []string{"distributed-validators", "ethereum-networks", "local-wallet", "obol-stack"}
for _, skill := range skills {
skillMD := filepath.Join(destDir, skill, "SKILL.md")
info, err := os.Stat(skillMD)
Expand Down Expand Up @@ -73,7 +72,7 @@ func TestCopySkillsSkipsExisting(t *testing.T) {
destDir := t.TempDir()

// Pre-create a skill directory with custom content
customDir := filepath.Join(destDir, "ethereum-wallet")
customDir := filepath.Join(destDir, "local-wallet")
if err := os.MkdirAll(customDir, 0755); err != nil {
t.Fatalf("mkdir: %v", err)
}
Expand Down
2 changes: 1 addition & 1 deletion internal/embed/skills/ethereum-networks/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Query Ethereum blockchain data through the local eRPC gateway. Supports any JSON

## When NOT to Use

- Sending transactions or signing (read-only, no private keys)
- Sending transactions, signing, or deploying contracts — use `ethereum-wallet`
- Validator monitoring — use `distributed-validators`
- Kubernetes pod diagnostics — use `obol-stack`

Expand Down
121 changes: 100 additions & 21 deletions internal/embed/skills/ethereum-wallet/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,38 +1,117 @@
---
name: ethereum-wallet
description: "Sign and send Ethereum transactions via a remote Web3Signer. This skill is not yet implemented — it will connect to a Web3Signer instance using a URI and auth token to sign transactions, deploy contracts, and manage validator operations."
metadata: { "openclaw": { "emoji": "🔐", "requires": { "bins": ["curl"] } } }
description: "Sign and send Ethereum transactions via the local Web3Signer. Use when asked to send ETH, sign messages, or interact with contracts that modify state."
metadata: { "openclaw": { "emoji": "🔐", "requires": { "bins": ["python3"] } } }
---

# Ethereum Wallet

> **This skill is coming soon.** It is not yet functional — the instructions below describe what it will do when complete.
Sign and send Ethereum transactions through the local Web3Signer instance.
Keys are pre-generated during setup — this skill signs and submits only.

## What This Will Do
## When to Use

The ethereum-wallet skill will let you sign and send Ethereum transactions through a remote [Web3Signer](https://docs.web3signer.consensys.io/) instance. Web3Signer is a remote signing service that keeps private keys secure and separate from the application.
- Listing available signing addresses (wallets)
- Sending ETH to an address
- Signing messages or typed data (EIP-712)
- Signing transactions for later broadcast
- Calling contract functions that modify state (write operations)
- Deploying smart contracts

### Planned capabilities
## When NOT to Use

- **Send ETH** to any address
- **Call contract functions** that modify state (not just read — that's what `ethereum-networks` does)
- **Deploy contracts** from bytecode
- **Sign messages** for off-chain verification
- **Manage validator operations** like voluntary exits
- Reading blockchain data (balances, blocks, transactions) — use `ethereum-networks`
- Creating new keys — keys are managed by the `obol` CLI, not this skill
- Monitoring validators — use `distributed-validators`
- Kubernetes diagnostics — use `obol-stack`

### Configuration
## Quick Start

When ready, this skill will need two things:
```bash
# List signing addresses
python3 scripts/signer.py accounts

1. **Web3Signer URI** — the URL of your Web3Signer instance (e.g. `http://web3signer.svc.cluster.local:9000`)
2. **Auth token** — a bearer token for authenticating with the signer
# Check web3signer health
python3 scripts/signer.py health

These will be provided during setup via `obol openclaw setup` or environment variables.
# Sign a message
python3 scripts/signer.py sign 0xYourAddress 0xdeadbeef

## Current Status
# Sign a transaction (returns raw signed tx hex)
python3 scripts/signer.py sign-tx \
--from 0xYourAddress --to 0xRecipient --value 0xDE0B6B3A7640000

This skill is a placeholder. If you need to:
# Sign AND submit a transaction via eRPC
python3 scripts/signer.py send-tx \
--from 0xYourAddress --to 0xRecipient --value 0xDE0B6B3A7640000

- **Read** blockchain data (balances, blocks, transactions) — use the `ethereum-networks` skill
- **Monitor** distributed validators — use the `distributed-validators` skill
- **Sign transactions** — this will need to wait until the ethereum-wallet skill is implemented
# Sign EIP-712 typed data
python3 scripts/signer.py sign-typed 0xYourAddress '{"types":{...},"primaryType":"...","domain":{...},"message":{...}}'
```

## Available Commands

| Command | Params | Description |
|---------|--------|-------------|
| `accounts` | none | List signing addresses from web3signer |
| `health` | none | Check web3signer `/upcheck` endpoint |
| `sign` | `address data` | Sign arbitrary hex data (`eth_sign`) |
| `sign-tx` | `--from --to [--value] [--data] [--gas] [--nonce] [--network]` | Sign a tx, return raw signed hex |
| `sign-typed` | `address typed-data-json` | Sign EIP-712 typed data |
| `send-tx` | `--from --to [--value] [--data] [--network]` | Sign AND broadcast via eRPC |

## Transaction Submission Flow

`send-tx` does the following:

1. Fetches nonce, gas price, chain ID from eRPC (unless provided)
2. Calls `eth_signTransaction` on web3signer — returns RLP-encoded signed tx
3. Calls `eth_sendRawTransaction` on eRPC — returns tx hash
4. Reports the tx hash (use `ethereum-networks` skill to check receipt later)

## Multi-Network Support

By default, transactions target `mainnet`. Use `--network` to change:

```bash
python3 scripts/signer.py send-tx --network hoodi \
--from 0xYourAddress --to 0xRecipient --value 0xDE0B6B3A7640000
```

The signing key is chain-agnostic — the same address works on any EVM network.
Network routing goes through eRPC at `/rpc/{network}`.

## Values Are in Hex Wei

All `--value` amounts are hex-encoded wei, matching the JSON-RPC standard:

| Amount | Hex Wei |
|--------|---------|
| 1 ETH | `0xDE0B6B3A7640000` |
| 0.1 ETH | `0x16345785D8A0000` |
| 0.01 ETH | `0x2386F26FC10000` |
| 1 Gwei | `0x3B9ACA00` |

The script does NOT auto-convert from ETH decimal notation.

## Constraints

- **Shell is `sh`, not `bash`** — do not use bashisms like `${var//pattern}`, `${var:offset}`, `[[ ]]`, or arrays. Use POSIX-compatible syntax only
- **Python stdlib only** — only the Python 3.11 standard library is available. Do not import `web3`, `eth_abi`, `rlp`, `pysha3`, or any third-party package
- **No key creation** — keys are managed by the `obol` CLI. If no keys exist, tell the user to run `obol agent init`
- **Local only** — always use the in-cluster web3signer at `$WEB3SIGNER_URL`, never call external signing services
- **Always check for null** — RPC methods may return `null` for unknown hashes or pending state. Always check `if result is not None` before accessing fields
- **Confirm before sending** — always show the user what will be signed before executing `send-tx`

## Environment Variables

| Variable | Default | Description |
|----------|---------|-------------|
| `WEB3SIGNER_URL` | `http://web3signer:9000` | Web3Signer service URL |
| `ERPC_URL` | `http://erpc.erpc.svc.cluster.local:4000/rpc` | eRPC gateway base URL |
| `ERPC_NETWORK` | `mainnet` | Default network for eRPC routing |

## See Also

- `references/web3signer-api.md` — ETH1 JSON-RPC and REST API reference
- `ethereum-networks` skill — read-only blockchain queries via eRPC
97 changes: 97 additions & 0 deletions internal/embed/skills/ethereum-wallet/references/web3signer-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Web3Signer ETH1 API Reference

Base URL: `$WEB3SIGNER_URL` (default: `http://web3signer:9000`)

## JSON-RPC Methods

All methods use `POST` to the base URL with `Content-Type: application/json`.

Request format:
```json
{"jsonrpc": "2.0", "method": "<method>", "params": [...], "id": 1}
```

| Method | Params | Returns | Description |
|--------|--------|---------|-------------|
| `eth_accounts` | `[]` | `["0x..."]` | List signer addresses |
| `eth_sign` | `[address, data]` | `"0x..."` (65-byte signature) | Sign with Ethereum prefix |
| `eth_signTransaction` | `[txObject]` | `"0x..."` (signed RLP) | Sign tx for later broadcast |
| `eth_signTypedData` | `[address, typedData]` | `"0x..."` (65-byte signature) | EIP-712 typed data signing |
| `eth_sendTransaction` | `[txObject]` | `"0x..."` (tx hash) | Sign + submit (needs downstream config) |

## REST API Endpoints

| Method | Path | Description | Response |
|--------|------|-------------|----------|
| `GET` | `/upcheck` | Health check | `"OK"` (200) or 500 |
| `GET` | `/api/v1/eth1/publicKeys` | List SECP256K1 public keys | `["0x04..."]` (JSON array) |
| `POST` | `/api/v1/eth1/sign/{pubkey}` | Sign raw data | signature hex string |
| `POST` | `/reload` | Reload key configurations | 202 Accepted |
| `GET` | `/reload` | Check reload status | `idle`, `running`, `completed`, `failed` |

## Transaction Object Fields

Used with `eth_signTransaction` and `eth_sendTransaction`:

| Field | Required | Type | Description |
|-------|----------|------|-------------|
| `from` | yes | `DATA` (20 bytes) | Signer address |
| `to` | yes* | `DATA` (20 bytes) | Recipient (* omit for contract deploy) |
| `value` | no | `QUANTITY` (hex) | Wei to send |
| `data` | no | `DATA` (hex) | Calldata or contract bytecode |
| `gas` | no | `QUANTITY` (hex) | Gas limit |
| `gasPrice` | no | `QUANTITY` (hex) | Gas price (legacy tx) |
| `maxFeePerGas` | no | `QUANTITY` (hex) | EIP-1559 max fee |
| `maxPriorityFeePerGas` | no | `QUANTITY` (hex) | EIP-1559 priority fee |
| `nonce` | no | `QUANTITY` (hex) | Sender nonce |
| `chainId` | no | `QUANTITY` (hex) | Chain ID (prevents replay) |

## eth_sign Details

Signs data with the Ethereum-specific prefix: `"\x19Ethereum Signed Message:\n" + len(message) + message`.

**Params**: `[address, data]`
- `address`: `"0x..."` — 20-byte signer address
- `data`: `"0x..."` — hex-encoded data to sign

**Returns**: `"0x..."` — 65-byte signature (r + s + v)

## eth_signTypedData Details

Signs structured data per [EIP-712](https://eips.ethereum.org/EIPS/eip-712).

**Params**: `[address, typedData]`
- `address`: `"0x..."` — 20-byte signer address
- `typedData`: EIP-712 object with `types`, `primaryType`, `domain`, `message`

**Returns**: `"0x..."` — 65-byte signature (r + s + v)

## Error Responses

| HTTP Code | Meaning |
|-----------|---------|
| 400 | Bad request (malformed params) |
| 404 | Public key not found in keystore |
| 500 | Internal server error |

## Key Configuration (TOML)

Web3Signer loads keys from TOML configuration files in the key store directory.

### Raw hex key
```toml
[metadata]
description = "my-key"

[signing]
type = "file-raw"
filename = "/data/mykey.hex"
```

### Encrypted keystore (V3)
```toml
[signing]
type = "file-keystore"
keystoreFile = "/data/keystore.json"
keystorePasswordFile = "/data/password.txt"
```
Loading
Loading