Skip to content
Open
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
71 changes: 62 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,15 @@ All commands support `--json` for machine-readable output.

## Supported Chains

| Chain | Native Token | Chain ID | Explorer |
|-------|-------------|----------|----------|
| Chain | Native Token | Chain ID | Explorer | Notes |
|-------|-------------|----------|----------|-------|
| Base | ETH | 8453 | [basescan.org](https://basescan.org) |
| Ethereum | ETH | 1 | [etherscan.io](https://etherscan.io) |
| Polygon | POL | 137 | [polygonscan.com](https://polygonscan.com) |
| Arbitrum | ETH | 42161 | [arbiscan.io](https://arbiscan.io) |
| Optimism | ETH | 10 | [optimistic.etherscan.io](https://optimistic.etherscan.io) |
| MegaETH | ETH | 4326 | [mega.etherscan.io](https://mega.etherscan.io) |
| LightLink | ETH | 1890 | [phoenix.lightlink.io](https://phoenix.lightlink.io) | Legacy gas, supports gasless txs |

## Architecture

Expand Down Expand Up @@ -91,12 +92,11 @@ evm-wallet-skill/

**`wallet.js`** — Handles wallet lifecycle. Generates a new private key via viem's `generatePrivateKey()`, stores it at `~/.evm-wallet.json` with `chmod 600` permissions. Loads the key and returns viem account/client objects for signing transactions.

**`gas.js`** — Smart EIP-1559 gas estimation. Analyzes the last 20 blocks to calculate optimal `maxFeePerGas` and `maxPriorityFeePerGas`:
- Fetches current `baseFeePerGas` from the latest block
- Samples priority fees from recent transactions (75th percentile)
- Applies 2x safety margin: `maxFee = 2 × baseFee + priorityFee`
- 20% gas limit buffer on all transactions
- Falls back to sensible defaults if estimation fails
**`gas.js`** — Smart gas estimation supporting both EIP-1559 and legacy gas pricing:
- **EIP-1559 chains** (Base, Ethereum, Polygon, etc.): Analyzes last 20 blocks for optimal `maxFeePerGas` and `maxPriorityFeePerGas`
- **Legacy chains** (LightLink): Uses `gasPrice` with support for custom overrides (including 0 for gasless transactions)
- Auto-detects chain type and applies correct pricing model
- Applies 2x safety margin and 20% gas limit buffer

### Transaction Flow

Expand Down Expand Up @@ -126,15 +126,68 @@ User request
- **DEX aggregator:** [Odos](https://odos.xyz) — multi-hop, multi-source routing
- **RPCs:** Public endpoints (no API keys)

## Gas Pricing

The skill automatically handles gas pricing based on the chain type:

- **EIP-1559 chains** (Base, Ethereum, Polygon, Arbitrum, Optimism, MegaETH): Uses modern gas pricing with `maxFeePerGas` and `maxPriorityFeePerGas`
- **Legacy chains** (LightLink): Uses traditional `gasPrice` format

For legacy chains, you can override the gas price:

```bash
# Gasless transaction on LightLink (0 gas price)
node src/transfer.js lightlink 0x... 0.01 --gas-price 0 --yes

# Custom gas price on any chain
node src/transfer.js base 0x... 0.01 --gas-price 20
node src/contract.js lightlink 0x... "transfer(address,uint256)" 0x... 1000 --gas-price 0
```

## Roadmap

- [ ] **Token swaps** via Matcha/0x aggregator (Uniswap V2/V3/V4 + more)
- [ ] **Chainlist auto-refresh** — periodically fetch fresh RPCs
- [ ] **ENS resolution** — send to `vitalik.eth`
- [x] **ENS resolution** — send to `vitalik.eth`
- [ ] **Passphrase encryption** for key storage
- [ ] **Multi-wallet support**
- [ ] **Transaction history** tracking

## License

MIT

## Custom RPC URLs

By default, the skill uses public RPCs. For better reliability and speed, you can configure your own RPC endpoints (e.g., Alchemy, Infura, QuickNode).

Add a `rpcUrls` object to your `~/.evm-wallet.json`:

```json
{
"privateKey": "0x...",
"rpcUrls": {
"ethereum": "https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY",
"polygon": "https://polygon-mainnet.g.alchemy.com/v2/YOUR_KEY",
"base": "https://base-mainnet.g.alchemy.com/v2/YOUR_KEY",
"arbitrum": "https://arb-mainnet.g.alchemy.com/v2/YOUR_KEY"
}
}
```

**Priority order for RPC resolution:**
1. Custom RPCs in `~/.evm-wallet.json` (highest priority)
2. Environment variable: `EVM_RPC_URLS_JSON`
3. Built-in public defaults

You only need to specify RPCs for chains you want to override — others will fall back to defaults.

### Recommended Providers

| Provider | Free Tier | Notes |
|----------|-----------|-------|
| [Alchemy](https://www.alchemy.com/) | 300M compute units/mo | Best reliability |
| [Infura](https://www.infura.io/) | 100K requests/day | Good coverage |
| [QuickNode](https://www.quicknode.com/) | 10M credits/mo | Fast |
| [PublicNode](https://publicnode.com/) | Unlimited | Free, no signup |

74 changes: 65 additions & 9 deletions SKILL-clawdhub.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: evm-wallet-skill
description: Self-sovereign EVM wallet for AI agents. Use when the user wants to create a crypto wallet, check balances, send ETH or ERC20 tokens, swap tokens, or interact with smart contracts. Supports Base, Ethereum, Polygon, Arbitrum, and Optimism. Private keys stored locally — no cloud custody, no API keys required.
description: Self-sovereign EVM wallet for AI agents. Use when the user wants to create a crypto wallet, check balances, send ETH or ERC20 tokens, swap tokens, or interact with smart contracts. Supports Base, Ethereum, Polygon, Arbitrum, Optimism, MegaETH, and LightLink. Private keys stored locally — no cloud custody, no API keys required.
metadata: {"clawdbot":{"emoji":"💰","homepage":"https://github.com/surfer77/evm-wallet-skill","requires":{"bins":["node","git"]}}}
---

Expand Down Expand Up @@ -74,6 +74,9 @@ node src/transfer.js <chain> <to_address> <amount> --yes --json

# ERC20 token
node src/transfer.js <chain> <to_address> <amount> <token_address> --yes --json

# With custom gas price (for legacy chains)
node src/transfer.js <chain> <to_address> <amount> --gas-price 0 --yes --json
```

**⚠️ ALWAYS confirm with the user before executing transfers.** Show them:
Expand All @@ -84,6 +87,11 @@ node src/transfer.js <chain> <to_address> <amount> <token_address> --yes --json

Only add `--yes` after the user explicitly confirms.

**Gas Price Options:**
- Most chains use EIP-1559 gas pricing automatically
- Legacy chains (like LightLink) use legacy gas pricing automatically
- Use `--gas-price <gwei>` to override (e.g., `--gas-price 0` for gasless transactions on LightLink)

### Swap Tokens

When user wants to swap, trade, buy, or sell tokens:
Expand Down Expand Up @@ -114,6 +122,10 @@ node src/contract.js <chain> <contract_address> \
# Write (costs gas — confirm first)
node src/contract.js <chain> <contract_address> \
"<function_signature>" [args...] --yes --json

# Write with custom gas price (e.g., gasless on LightLink)
node src/contract.js <chain> <contract_address> \
"<function_signature>" [args...] --gas-price 0 --yes --json
```

Examples:
Expand Down Expand Up @@ -142,16 +154,29 @@ cd "$SKILL_DIR" && git pull && npm install

## Supported Chains

| Chain | Native Token | Use For |
|-------|-------------|---------|
| base | ETH | Cheapest fees — default for testing |
| ethereum | ETH | Mainnet, highest fees |
| polygon | POL | Low fees |
| arbitrum | ETH | Low fees |
| optimism | ETH | Low fees |
| Chain | Native Token | Gas Type | Use For |
|-------|-------------|----------|---------|
| base | ETH | EIP-1559 | Cheapest fees — default for testing |
| ethereum | ETH | EIP-1559 | Mainnet, highest fees |
| polygon | POL | EIP-1559 | Low fees |
| arbitrum | ETH | EIP-1559 | Low fees |
| optimism | ETH | EIP-1559 | Low fees |
| megaeth | ETH | EIP-1559 | Ultra-fast transactions |
| lightlink | ETH | Legacy | Enterprise L2, supports gasless txs |

**Always recommend Base** for first-time users (lowest gas fees).

### LightLink Notes

LightLink uses **legacy gas pricing** (not EIP-1559). The skill automatically detects this and uses the correct gas format.

LightLink also supports **gasless transactions** — set `--gas-price 0` for zero-fee transfers:

```bash
# Gasless transfer on LightLink
node src/transfer.js lightlink 0xRECIPIENT 0.01 --gas-price 0 --yes --json
```

## Common Token Addresses

### Base
Expand All @@ -162,6 +187,36 @@ cd "$SKILL_DIR" && git pull && npm install
- **USDC:** `0xA0b86a33E6441b8a46a59DE4c4C5E8F5a6a7A8d0`
- **WETH:** `0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2`

### LightLink
- **USDC:** `0x5bE26527c30aB557B82375c4Df8b8EEd898Ccef1`
- **USDT:** `0x603611F9a92D0Ca57E91026879d040D5d1aFe9Ce`

## Gas Pricing

The skill automatically handles gas pricing based on the chain:

### EIP-1559 Chains (Base, Ethereum, Polygon, Arbitrum, Optimism, MegaETH)
- Automatically estimates `maxFeePerGas` and `maxPriorityFeePerGas`
- Uses 2x safety margin on base fee
- Samples priority fees from recent blocks (75th percentile)
- Adds 20% buffer to gas limit estimates

### Legacy Chains (LightLink)
- Uses `gasPrice` instead of EIP-1559 parameters
- Fetches current gas price from RPC
- Supports custom gas price overrides (including 0 for gasless)

### Custom Gas Price
Override gas price for any chain with `--gas-price <gwei>`:

```bash
# Fast transaction with high gas price
node src/transfer.js base 0x... 0.01 --gas-price 50 --yes

# Gasless transaction on LightLink
node src/transfer.js lightlink 0x... 0.01 --gas-price 0 --yes
```

## Safety Rules

1. **Never execute transfers or swaps without user confirmation**
Expand All @@ -177,4 +232,5 @@ cd "$SKILL_DIR" && git pull && npm install
- **"Insufficient balance"** → Show current balance, suggest funding
- **"RPC error"** → Retry once, automatic failover built in
- **"No route found"** (swap) → Token pair may lack liquidity
- **"Gas estimation failed"** → May need more ETH for gas
- **"Gas estimation failed"** → May need more ETH for gas, or try `--gas-price` override
- **"Chain does not support EIP-1559"** → The skill now auto-detects this, update your skill if you see this error
66 changes: 52 additions & 14 deletions src/contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { printUpdateNag } from './check-update.js';
import { getWalletClient, exists } from './lib/wallet.js';
import { createPublicClientWithRetry } from './lib/rpc.js';
import { getChain, getExplorerTxUrl } from './lib/chains.js';
import { estimateGas, estimateGasLimit, formatGwei } from './lib/gas.js';
import { estimateGas, estimateGasLimit, formatGwei, buildGasParams } from './lib/gas.js';

// Parse command line arguments
const args = process.argv.slice(2);
Expand All @@ -26,20 +26,32 @@ if (valueIndex !== -1 && valueIndex < args.length - 1) {
args.splice(valueIndex, 2); // Remove --value and its parameter
}

// Parse --gas-price flag
let customGasPrice = null;
const gasPriceIdx = args.indexOf('--gas-price');
if (gasPriceIdx !== -1 && gasPriceIdx < args.length - 1) {
const gweiValue = parseFloat(args[gasPriceIdx + 1]);
if (!isNaN(gweiValue) && gweiValue >= 0) {
customGasPrice = BigInt(Math.floor(gweiValue * 1_000_000_000)); // Convert gwei to wei
}
args.splice(gasPriceIdx, 2); // Remove --gas-price and its parameter
}

function showHelp() {
console.log(`
EVM Contract Interaction

Usage: node src/contract.js [options] <chain> <address> <functionSig> [args...] [--value <eth>]

Arguments:
chain Chain name (base, ethereum, polygon, arbitrum, optimism)
chain Chain name (base, ethereum, polygon, arbitrum, optimism, megaeth, lightlink)
address Contract address
functionSig Function signature (e.g., "balanceOf(address)" or "transfer(address,uint256)")
args Function arguments (space-separated)

Options:
--value <eth> ETH value to send with transaction (for payable functions)
--gas-price Custom gas price in gwei (for legacy chains, e.g., --gas-price 0 for gasless)
--yes Skip confirmation prompt (for write operations)
--json Output in JSON format
--help Show this help message
Expand All @@ -57,6 +69,9 @@ Examples:
# Payable functions
node src/contract.js ethereum 0x789... "deposit()" --value 0.1
node src/contract.js base 0x456... "mint(address)" 0x123... --value 0.01 --yes

# Gasless transactions (LightLink)
node src/contract.js lightlink 0x... "transfer(address,uint256)" 0x123... 1000 --gas-price 0 --yes
`);
}

Expand Down Expand Up @@ -194,7 +209,7 @@ async function main() {
}

// Parse arguments (exclude flags)
const filteredArgs = args.filter(arg => !arg.startsWith('--') && arg !== valueInEth);
const filteredArgs = args.filter(arg => !arg.startsWith('--'));
const [chainName, contractAddress, functionSig, ...functionArgs] = filteredArgs;

if (!chainName || !contractAddress || !functionSig) {
Expand Down Expand Up @@ -280,7 +295,12 @@ async function main() {
// Estimate gas
let gasEstimate;
try {
gasEstimate = await estimateGas(chainName);
const gasOptions = {};
if (customGasPrice !== null) {
gasOptions.gasPrice = customGasPrice;
}
gasEstimate = await estimateGas(chainName, gasOptions);

const gasLimit = await estimateGasLimit(publicClient, {
to: contractAddress,
data: encodeFunctionData({
Expand All @@ -296,10 +316,21 @@ async function main() {
exitWithError(`Gas estimation failed: ${error.message}`);
}

const estimatedGasCost = gasEstimate.maxFeePerGas * gasEstimate.gasLimit;
// Calculate total cost
const gasPriceForCalc = gasEstimate.type === 'legacy'
? gasEstimate.gasPrice
: gasEstimate.maxFeePerGas;
const estimatedGasCost = gasPriceForCalc * gasEstimate.gasLimit;
const estimatedGasCostEth = formatEther(estimatedGasCost);

// Build gas params for transaction
const gasParams = buildGasParams(gasEstimate);

// Show confirmation details
const gasInfoText = gasEstimate.type === 'legacy'
? `Gas Price: ${formatGwei(gasEstimate.gasPrice)} gwei (legacy)`
: `Max Fee: ${formatGwei(gasEstimate.maxFeePerGas)} gwei (EIP-1559)`;

const confirmationMessage = `
🔧 Contract Call Details:
Contract: ${contractAddress}
Expand All @@ -310,7 +341,7 @@ async function main() {

⛽ Gas Estimate:
Gas Limit: ${gasEstimate.gasLimit.toLocaleString()}
Max Fee: ${formatGwei(gasEstimate.maxFeePerGas)} gwei
${gasInfoText}
Est. Cost: ${estimatedGasCostEth} ETH

💰 Total Cost: ${(parseFloat(valueInEth) + parseFloat(estimatedGasCostEth)).toFixed(6)} ETH
Expand Down Expand Up @@ -340,9 +371,8 @@ Proceed with transaction?`;
functionName: parsedFunction.functionName,
args: parsedArgs,
value,
maxFeePerGas: gasEstimate.maxFeePerGas,
maxPriorityFeePerGas: gasEstimate.maxPriorityFeePerGas,
gas: gasEstimate.gasLimit
gas: gasEstimate.gasLimit,
...gasParams
});
} catch (error) {
exitWithError(`Transaction failed: ${error.message}`);
Expand All @@ -351,7 +381,7 @@ Proceed with transaction?`;
const explorerUrl = getExplorerTxUrl(chainName, txHash);

if (jsonFlag) {
console.log(JSON.stringify({
const result = {
success: true,
txHash,
explorerUrl,
Expand All @@ -360,13 +390,21 @@ Proceed with transaction?`;
args: functionArgs,
value: valueInEth,
chain: chainName,
gasType: gasEstimate.type,
gasUsed: {
maxFeePerGas: gasEstimate.maxFeePerGas.toString(),
maxPriorityFeePerGas: gasEstimate.maxPriorityFeePerGas.toString(),
gasLimit: gasEstimate.gasLimit.toString(),
estimatedCostEth: estimatedGasCostEth
}
}, null, 2));
};

if (gasEstimate.type === 'legacy') {
result.gasUsed.gasPrice = gasEstimate.gasPrice.toString();
} else {
result.gasUsed.maxFeePerGas = gasEstimate.maxFeePerGas.toString();
result.gasUsed.maxPriorityFeePerGas = gasEstimate.maxPriorityFeePerGas.toString();
}

console.log(JSON.stringify(result, null, 2));
} else {
console.log('\n✅ Transaction successful!');
console.log(`Tx Hash: ${txHash}`);
Expand All @@ -387,4 +425,4 @@ Proceed with transaction?`;

main().then(() => printUpdateNag()).catch(error => {
exitWithError(`Unexpected error: ${error.message}`);
});
});
Loading