From a2f52cea445de95d576a9041d2b6d51b4bb60bbe Mon Sep 17 00:00:00 2001 From: danenright Date: Mon, 2 Feb 2026 10:39:25 +1100 Subject: [PATCH 1/7] feat: add LightLink Phoenix Mainnet support Add LightLink L2 network configuration: - Chain ID: 1890 - Native token: ETH - Explorer: phoenix.lightlink.io - Primary RPC: replicator.phoenix.lightlink.io/rpc/v1 - Fallback RPCs: thirdweb, omniatech public endpoints Update README with LightLink in supported chains table. --- README.md | 1 + src/lib/chains.js | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/README.md b/README.md index a94c11f8..7199586b 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ All commands support `--json` for machine-readable output. | 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) | ## Architecture diff --git a/src/lib/chains.js b/src/lib/chains.js index 0d0c3c55..a3dcd72d 100644 --- a/src/lib/chains.js +++ b/src/lib/chains.js @@ -113,6 +113,24 @@ export const chains = { // See: https://docs.megaeth.com/realtime-api // EIP-7966: https://ethereum-magicians.org/t/eip-7966-eth-sendrawtransactionsync-method/24640 syncRpc: "eth_sendRawTransactionSync" + }, + + lightlink: { + chainId: 1890, + name: "LightLink", + nativeToken: { + symbol: "ETH", + decimals: 18 + }, + explorer: { + name: "LightLink Explorer", + url: "https://phoenix.lightlink.io" + }, + rpcs: [ + "https://replicator.phoenix.lightlink.io/rpc/v1", + "https://1890.rpc.thirdweb.com", + "https://endpoints.omniatech.io/v1/lightlink/phoenix/public" + ] } }; From 99d9b967ae016e5ff22976b1f4616cddb010429a Mon Sep 17 00:00:00 2001 From: danenright Date: Mon, 2 Feb 2026 11:02:40 +1100 Subject: [PATCH 2/7] docs: add LightLink, MegaETH to all chain lists Update help text and documentation to include LightLink and MegaETH: - transfer.js: add megaeth, lightlink to chain list in help - contract.js: add megaeth, lightlink to chain list in help - SKILL-clawdhub.md: update description and supported chains table --- SKILL-clawdhub.md | 4 +++- src/contract.js | 2 +- src/transfer.js | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/SKILL-clawdhub.md b/SKILL-clawdhub.md index 1e4e319a..94ad4bb4 100644 --- a/SKILL-clawdhub.md +++ b/SKILL-clawdhub.md @@ -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"]}}} --- @@ -149,6 +149,8 @@ cd "$SKILL_DIR" && git pull && npm install | polygon | POL | Low fees | | arbitrum | ETH | Low fees | | optimism | ETH | Low fees | +| megaeth | ETH | Ultra-fast transactions | +| lightlink | ETH | Enterprise L2, low fees | **Always recommend Base** for first-time users (lowest gas fees). diff --git a/src/contract.js b/src/contract.js index ea294475..873086d4 100755 --- a/src/contract.js +++ b/src/contract.js @@ -33,7 +33,7 @@ EVM Contract Interaction Usage: node src/contract.js [options]
[args...] [--value ] 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) diff --git a/src/transfer.js b/src/transfer.js index 5e9362ae..7f1e4af8 100755 --- a/src/transfer.js +++ b/src/transfer.js @@ -36,7 +36,7 @@ EVM Wallet Transfer Usage: node src/transfer.js [options] [tokenAddress] Arguments: - chain Chain name (base, ethereum, polygon, arbitrum, optimism) + chain Chain name (base, ethereum, polygon, arbitrum, optimism, megaeth, lightlink) to Recipient address amount Amount to send tokenAddress ERC20 token contract address (optional, for token transfers) From bf1217017808267fefad9bf7dcd2a3ad7f775362 Mon Sep 17 00:00:00 2001 From: danenright Date: Mon, 2 Feb 2026 11:40:05 +1100 Subject: [PATCH 3/7] feat: support legacy gas pricing and gasless transactions Add legacy gas pricing support for chains like LightLink, with --gas-price flag for custom overrides including 0 for gasless transactions. Auto-detects chain type and applies correct pricing model (EIP-1559 vs legacy). --- README.md | 37 +++++++++++---- SKILL-clawdhub.md | 74 ++++++++++++++++++++++++++---- src/contract.js | 64 ++++++++++++++++++++------ src/lib/chains.js | 5 +- src/lib/gas.js | 114 ++++++++++++++++++++++++++++++++++++++++++---- src/transfer.js | 112 ++++++++++++++++++++++++++++++--------------- 6 files changed, 327 insertions(+), 79 deletions(-) diff --git a/README.md b/README.md index 7199586b..59e672af 100644 --- a/README.md +++ b/README.md @@ -55,15 +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) | +| LightLink | ETH | 1890 | [phoenix.lightlink.io](https://phoenix.lightlink.io) | Legacy gas, supports gasless txs | ## Architecture @@ -92,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 @@ -127,11 +126,29 @@ 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 diff --git a/SKILL-clawdhub.md b/SKILL-clawdhub.md index 94ad4bb4..9788b07d 100644 --- a/SKILL-clawdhub.md +++ b/SKILL-clawdhub.md @@ -74,6 +74,9 @@ node src/transfer.js --yes --json # ERC20 token node src/transfer.js --yes --json + +# With custom gas price (for legacy chains) +node src/transfer.js --gas-price 0 --yes --json ``` **āš ļø ALWAYS confirm with the user before executing transfers.** Show them: @@ -84,6 +87,11 @@ node src/transfer.js --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 ` to override (e.g., `--gas-price 0` for gasless transactions on LightLink) + ### Swap Tokens When user wants to swap, trade, buy, or sell tokens: @@ -114,6 +122,10 @@ node src/contract.js \ # Write (costs gas — confirm first) node src/contract.js \ "" [args...] --yes --json + +# Write with custom gas price (e.g., gasless on LightLink) +node src/contract.js \ + "" [args...] --gas-price 0 --yes --json ``` Examples: @@ -142,18 +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 | -| megaeth | ETH | Ultra-fast transactions | -| lightlink | ETH | Enterprise L2, 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 @@ -164,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 `: + +```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** @@ -179,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 diff --git a/src/contract.js b/src/contract.js index 873086d4..c9e275a1 100755 --- a/src/contract.js +++ b/src/contract.js @@ -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); @@ -26,6 +26,17 @@ 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 @@ -40,6 +51,7 @@ Arguments: Options: --value 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 @@ -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 `); } @@ -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) { @@ -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({ @@ -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} @@ -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 @@ -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}`); @@ -351,7 +381,7 @@ Proceed with transaction?`; const explorerUrl = getExplorerTxUrl(chainName, txHash); if (jsonFlag) { - console.log(JSON.stringify({ + const result = { success: true, txHash, explorerUrl, @@ -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}`); @@ -387,4 +425,4 @@ Proceed with transaction?`; main().then(() => printUpdateNag()).catch(error => { exitWithError(`Unexpected error: ${error.message}`); -}); \ No newline at end of file +}); diff --git a/src/lib/chains.js b/src/lib/chains.js index a3dcd72d..29a7bc6d 100644 --- a/src/lib/chains.js +++ b/src/lib/chains.js @@ -130,7 +130,10 @@ export const chains = { "https://replicator.phoenix.lightlink.io/rpc/v1", "https://1890.rpc.thirdweb.com", "https://endpoints.omniatech.io/v1/lightlink/phoenix/public" - ] + ], + // LightLink uses legacy gas pricing (not EIP-1559) + // Supports gasless transactions (gasPrice = 0) + legacyGas: true } }; diff --git a/src/lib/gas.js b/src/lib/gas.js index 3a342757..2575d50c 100644 --- a/src/lib/gas.js +++ b/src/lib/gas.js @@ -1,24 +1,59 @@ /** - * Smart EIP-1559 Gas Estimation - * Calculates optimal gas parameters for transactions + * Smart Gas Estimation + * Supports both EIP-1559 and legacy gas pricing */ import { createPublicClientWithRetry } from './rpc.js'; +import { getChain } from './chains.js'; /** - * Get smart gas estimation for EIP-1559 transaction + * Check if chain uses legacy gas pricing (non-EIP-1559) + * @param {string} chainName - Chain name + * @returns {boolean} True if chain uses legacy gas + */ +export function isLegacyGasChain(chainName) { + try { + const chain = getChain(chainName); + return chain.legacyGas === true; + } catch { + return false; + } +} + +/** + * Get smart gas estimation for transaction + * Automatically detects chain type (EIP-1559 vs legacy) * @param {string} chainName - Chain name * @param {Object} [options] - Gas estimation options * @param {number} [options.safetyMargin] - Safety margin multiplier (default: 2) * @param {number} [options.priorityFeePercentile] - Priority fee percentile from recent blocks (default: 75) - * @returns {Object} Gas parameters + * @param {bigint} [options.gasPrice] - Override gas price for legacy chains (optional) + * @returns {Object} Gas parameters with `type` field ('eip1559' or 'legacy') */ export async function estimateGas(chainName, options = {}) { const { safetyMargin = 2, - priorityFeePercentile = 75 + priorityFeePercentile = 75, + gasPrice: gasPriceOverride } = options; + // Check if this chain uses legacy gas pricing + if (isLegacyGasChain(chainName)) { + return estimateLegacyGas(chainName, { gasPrice: gasPriceOverride }); + } + + // Use EIP-1559 estimation + return estimateEIP1559Gas(chainName, { safetyMargin, priorityFeePercentile }); +} + +/** + * Estimate gas using EIP-1559 (modern chains) + * @param {string} chainName - Chain name + * @param {Object} options - Estimation options + * @returns {Object} EIP-1559 gas parameters with type 'eip1559' + */ +async function estimateEIP1559Gas(chainName, options) { + const { safetyMargin, priorityFeePercentile } = options; const client = createPublicClientWithRetry(chainName); try { @@ -27,7 +62,8 @@ export async function estimateGas(chainName, options = {}) { const baseFeePerGas = latestBlock.baseFeePerGas; if (!baseFeePerGas) { - throw new Error('Chain does not support EIP-1559 (no base fee found)'); + // Chain doesn't support EIP-1559, fall back to legacy + return estimateLegacyGas(chainName, {}); } // Estimate priority fee from recent blocks @@ -41,16 +77,57 @@ export async function estimateGas(chainName, options = {}) { const maxFeePerGas = baseFeePerGas * BigInt(safetyMargin) + maxPriorityFeePerGas; return { + type: 'eip1559', maxFeePerGas, maxPriorityFeePerGas, baseFeePerGas, - gasPrice: maxFeePerGas, // For legacy compatibility + gasPrice: maxFeePerGas, // For backward compatibility estimatedCostGwei: formatGwei(maxFeePerGas), baseFeeGwei: formatGwei(baseFeePerGas), priorityFeeGwei: formatGwei(maxPriorityFeePerGas) }; } catch (error) { - throw new Error(`Failed to estimate gas: ${error.message}`); + // If EIP-1559 estimation fails, try legacy as fallback + try { + return estimateLegacyGas(chainName, {}); + } catch { + throw new Error(`Failed to estimate gas: ${error.message}`); + } + } +} + +/** + * Estimate gas using legacy pricing (pre-EIP-1559 chains) + * @param {string} chainName - Chain name + * @param {Object} options - Estimation options + * @param {bigint} [options.gasPrice] - Override gas price (optional) + * @returns {Object} Legacy gas parameters with type 'legacy' + */ +async function estimateLegacyGas(chainName, options) { + const { gasPrice: gasPriceOverride } = options; + const client = createPublicClientWithRetry(chainName); + + try { + let gasPrice; + + if (gasPriceOverride !== undefined) { + // Use provided gas price override + gasPrice = gasPriceOverride; + } else { + // Get gas price from RPC + gasPrice = await client.getGasPrice(); + } + + return { + type: 'legacy', + gasPrice, + // For backward compatibility + maxFeePerGas: gasPrice, + maxPriorityFeePerGas: BigInt(0), + estimatedCostGwei: formatGwei(gasPrice) + }; + } catch (error) { + throw new Error(`Failed to estimate legacy gas: ${error.message}`); } } @@ -183,4 +260,23 @@ export async function getCurrentGasPrices(chainNames = null) { ); return results; -} \ No newline at end of file +} + +/** + * Build gas parameters for transaction based on gas type + * @param {Object} gasEstimate - Gas estimate from estimateGas() + * @returns {Object} Transaction gas parameters + */ +export function buildGasParams(gasEstimate) { + if (gasEstimate.type === 'legacy') { + return { + gasPrice: gasEstimate.gasPrice + }; + } + + // EIP-1559 + return { + maxFeePerGas: gasEstimate.maxFeePerGas, + maxPriorityFeePerGas: gasEstimate.maxPriorityFeePerGas + }; +} diff --git a/src/transfer.js b/src/transfer.js index 7f1e4af8..d259f643 100755 --- a/src/transfer.js +++ b/src/transfer.js @@ -5,6 +5,7 @@ * Usage: * node src/transfer.js # Send native ETH * node src/transfer.js # Send ERC20 + * node src/transfer.js --gas-price 0 # Send with custom gas price */ import { parseEther, parseUnits, formatEther, parseAbi, isAddress } from 'viem'; @@ -12,7 +13,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'; // Standard ERC20 ABI const ERC20_ABI = parseAbi([ @@ -29,6 +30,16 @@ const jsonFlag = args.includes('--json'); const yesFlag = args.includes('--yes') || args.includes('-y'); const helpFlag = args.includes('--help') || args.includes('-h'); +// Parse --gas-price flag +let customGasPrice = null; +const gasPriceIdx = args.indexOf('--gas-price'); +if (gasPriceIdx !== -1 && args[gasPriceIdx + 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 + } +} + function showHelp() { console.log(` EVM Wallet Transfer @@ -44,12 +55,14 @@ Arguments: Options: --yes Skip confirmation prompt --json Output in JSON format + --gas-price Custom gas price in gwei (for legacy chains, e.g., --gas-price 0 for gasless) --help Show this help message Examples: node src/transfer.js base 0x123... 0.01 # Send 0.01 ETH on Base node src/transfer.js base 0x123... 100 0x833589fcd... # Send 100 USDC on Base node src/transfer.js ethereum 0x123... 0.5 --yes # Send 0.5 ETH, skip confirmation + node src/transfer.js lightlink 0x123... 0.01 --gas-price 0 # Send with 0 gas price on LightLink `); } @@ -139,8 +152,18 @@ async function main() { } // Parse arguments - const filteredArgs = args.filter(arg => !arg.startsWith('--')); - const [chainName, to, amount, tokenAddress] = filteredArgs; + const filteredArgs = args.filter(arg => !arg.startsWith('--') && !arg.match(/^\d+\.?\d*$/)); + // Also need to filter out the gas price value + const positionalArgs = []; + for (let i = 0; i < args.length; i++) { + if (args[i].startsWith('--')) { + if (args[i] === '--gas-price') i++; // Skip the value too + continue; + } + positionalArgs.push(args[i]); + } + + const [chainName, to, amount, tokenAddress] = positionalArgs; if (!chainName || !to || !amount) { exitWithError('Missing required arguments. Use --help for usage information.'); @@ -199,36 +222,47 @@ async function main() { // Estimate gas let gasEstimate; try { - if (isNativeTransfer) { - gasEstimate = await estimateGas(chainName); - const gasLimit = await estimateGasLimit(publicClient, { - to, - value: transferAmount, - account: walletAddress - }); - gasEstimate.gasLimit = gasLimit; - } else { - gasEstimate = await estimateGas(chainName); - const gasLimit = await estimateGasLimit(publicClient, { - to: tokenAddress, - data: walletClient.encodeFunctionData({ - abi: ERC20_ABI, - functionName: 'transfer', - args: [to, transferAmount] - }), - account: walletAddress - }); - gasEstimate.gasLimit = gasLimit; + const gasOptions = {}; + if (customGasPrice !== null) { + gasOptions.gasPrice = customGasPrice; } + gasEstimate = await estimateGas(chainName, gasOptions); + + const gasLimitTx = isNativeTransfer ? { + to, + value: transferAmount, + account: walletAddress + } : { + to: tokenAddress, + data: walletClient.encodeFunctionData({ + abi: ERC20_ABI, + functionName: 'transfer', + args: [to, transferAmount] + }), + account: walletAddress + }; + + const gasLimit = await estimateGasLimit(publicClient, gasLimitTx); + gasEstimate.gasLimit = gasLimit; } catch (error) { exitWithError(`Gas estimation failed: ${error.message}`); } // Calculate total cost for native transfers - const estimatedGasCost = gasEstimate.maxFeePerGas * gasEstimate.gasLimit; + 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 = ` šŸš€ Transfer Details: From: ${walletAddress} @@ -238,7 +272,7 @@ async function main() { ⛽ Gas Estimate: Gas Limit: ${gasEstimate.gasLimit.toLocaleString()} - Max Fee: ${formatGwei(gasEstimate.maxFeePerGas)} gwei + ${gasInfoText} Est. Cost: ${estimatedGasCostEth} ETH ${isNativeTransfer ? `šŸ’° Total Deduction: ${(parseFloat(amount) + parseFloat(estimatedGasCostEth)).toFixed(6)} ETH` : `šŸ’° Gas Cost: ${estimatedGasCostEth} ETH (separate from token transfer)`} @@ -267,9 +301,8 @@ Proceed with transfer?`; txHash = await walletClient.sendTransaction({ to, value: transferAmount, - maxFeePerGas: gasEstimate.maxFeePerGas, - maxPriorityFeePerGas: gasEstimate.maxPriorityFeePerGas, - gas: gasEstimate.gasLimit + gas: gasEstimate.gasLimit, + ...gasParams }); } else { // Send ERC20 token @@ -278,9 +311,8 @@ Proceed with transfer?`; abi: ERC20_ABI, functionName: 'transfer', args: [to, transferAmount], - maxFeePerGas: gasEstimate.maxFeePerGas, - maxPriorityFeePerGas: gasEstimate.maxPriorityFeePerGas, - gas: gasEstimate.gasLimit + gas: gasEstimate.gasLimit, + ...gasParams }); } } catch (error) { @@ -290,7 +322,7 @@ Proceed with transfer?`; const explorerUrl = getExplorerTxUrl(chainName, txHash); if (jsonFlag) { - console.log(JSON.stringify({ + const result = { success: true, txHash, explorerUrl, @@ -300,13 +332,21 @@ Proceed with transfer?`; symbol, chain: chainName, tokenAddress: tokenAddress || null, + 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āœ… Transfer successful!'); console.log(`Tx Hash: ${txHash}`); @@ -323,4 +363,4 @@ Proceed with transfer?`; main().then(() => printUpdateNag()).catch(error => { exitWithError(`Unexpected error: ${error.message}`); -}); \ No newline at end of file +}); From 84d29625c921f1016315dd096e71c53ed78ad558 Mon Sep 17 00:00:00 2001 From: danenright Date: Mon, 2 Feb 2026 11:52:49 +1100 Subject: [PATCH 4/7] Fix ERC20 gas estimation encoding --- src/transfer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/transfer.js b/src/transfer.js index d259f643..e615ba8f 100755 --- a/src/transfer.js +++ b/src/transfer.js @@ -8,7 +8,7 @@ * node src/transfer.js --gas-price 0 # Send with custom gas price */ -import { parseEther, parseUnits, formatEther, parseAbi, isAddress } from 'viem'; +import { parseEther, parseUnits, formatEther, parseAbi, isAddress, encodeFunctionData } from 'viem'; import { printUpdateNag } from './check-update.js'; import { getWalletClient, exists } from './lib/wallet.js'; import { createPublicClientWithRetry } from './lib/rpc.js'; @@ -234,7 +234,7 @@ async function main() { account: walletAddress } : { to: tokenAddress, - data: walletClient.encodeFunctionData({ + data: encodeFunctionData({ abi: ERC20_ABI, functionName: 'transfer', args: [to, transferAmount] From 84c1cad70c8ea16800d42aabf680889ff7f402ab Mon Sep 17 00:00:00 2001 From: HK47-droid Date: Tue, 3 Feb 2026 09:50:53 +1100 Subject: [PATCH 5/7] feat: add SODAX EVM chains (Sonic, Avalanche, BSC, HyperEVM) Adds support for all SODAX-compatible EVM chains: - Sonic (chainId 146) - SODAX hub chain - Avalanche C-Chain (chainId 43114) - BNB Smart Chain (chainId 56) - HyperEVM (chainId 998) Total supported chains: 11 (was 7) --- src/lib/chains.js | 85 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 77 insertions(+), 8 deletions(-) diff --git a/src/lib/chains.js b/src/lib/chains.js index 29a7bc6d..edfbaa81 100644 --- a/src/lib/chains.js +++ b/src/lib/chains.js @@ -1,6 +1,7 @@ /** * EVM Chain Configurations * Includes chainId, native token, block explorers, and default public RPCs + * Updated to support all SODAX EVM chains */ export const chains = { @@ -52,7 +53,7 @@ export const chains = { url: "https://polygonscan.com" }, rpcs: [ - "https://polygon.llamarpc.com", + "https://polygon-rpc.com", "https://polygon.publicnode.com", "https://rpc.ankr.com/polygon" ] @@ -109,9 +110,6 @@ export const chains = { "https://mainnet.megaeth.com/rpc", "https://rpc-megaeth-mainnet.globalstake.io" ], - // MegaETH supports eth_sendRawTransactionSync (EIP-7966) — returns tx receipt directly in response (<10ms) - // See: https://docs.megaeth.com/realtime-api - // EIP-7966: https://ethereum-magicians.org/t/eip-7966-eth-sendrawtransactionsync-method/24640 syncRpc: "eth_sendRawTransactionSync" }, @@ -131,15 +129,86 @@ export const chains = { "https://1890.rpc.thirdweb.com", "https://endpoints.omniatech.io/v1/lightlink/phoenix/public" ], - // LightLink uses legacy gas pricing (not EIP-1559) - // Supports gasless transactions (gasPrice = 0) legacyGas: true + }, + + // === SODAX Additional Chains === + + sonic: { + chainId: 146, + name: "Sonic", + nativeToken: { + symbol: "S", + decimals: 18 + }, + explorer: { + name: "Sonic Explorer", + url: "https://sonicscan.org" + }, + rpcs: [ + "https://rpc.soniclabs.com", + "https://sonic.drpc.org", + "https://rpc.ankr.com/sonic" + ] + }, + + avalanche: { + chainId: 43114, + name: "Avalanche C-Chain", + nativeToken: { + symbol: "AVAX", + decimals: 18 + }, + explorer: { + name: "Snowtrace", + url: "https://snowtrace.io" + }, + rpcs: [ + "https://api.avax.network/ext/bc/C/rpc", + "https://avalanche.publicnode.com", + "https://rpc.ankr.com/avalanche" + ] + }, + + bsc: { + chainId: 56, + name: "BNB Smart Chain", + nativeToken: { + symbol: "BNB", + decimals: 18 + }, + explorer: { + name: "BscScan", + url: "https://bscscan.com" + }, + rpcs: [ + "https://bsc-dataseed.binance.org", + "https://bsc.publicnode.com", + "https://rpc.ankr.com/bsc" + ] + }, + + hyper: { + chainId: 998, + name: "HyperEVM", + nativeToken: { + symbol: "HYPE", + decimals: 18 + }, + explorer: { + name: "Hyperliquid Explorer", + url: "https://explorer.hyperliquid.xyz" + }, + rpcs: [ + "https://rpc.hyperliquid.xyz/evm", + "https://api.hyperliquid.xyz/evm" + ] } }; /** * Get chain config by name - * @param {string} chainName - Chain name (e.g., "base", "ethereum") + * @param {string} chainName - Chain name (e.g., "base", "ethereum", "sonic") * @returns {Object} Chain configuration */ export function getChain(chainName) { @@ -178,4 +247,4 @@ export function getExplorerTxUrl(chainName, txHash) { export function getExplorerAddressUrl(chainName, address) { const chain = getChain(chainName); return `${chain.explorer.url}/address/${address}`; -} \ No newline at end of file +} From 6fce2a8edcad6ba14ecfc26044eaaf8d9bcd75e1 Mon Sep 17 00:00:00 2001 From: HK47-droid Date: Tue, 3 Feb 2026 19:24:28 +1100 Subject: [PATCH 6/7] feat: add Kaia chain support for SODAX compatibility --- src/lib/chains.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/lib/chains.js b/src/lib/chains.js index edfbaa81..86b2ef50 100644 --- a/src/lib/chains.js +++ b/src/lib/chains.js @@ -248,3 +248,22 @@ export function getExplorerAddressUrl(chainName, address) { const chain = getChain(chainName); return `${chain.explorer.url}/address/${address}`; } + +// Appending missing SODAX chain +chains.kaia = { + chainId: 8217, + name: "Kaia", + nativeToken: { + symbol: "KAIA", + decimals: 18 + }, + explorer: { + name: "KaiaScan", + url: "https://kaiascan.io" + }, + rpcs: [ + "https://public-en.node.kaia.io", + "https://kaia.blockpi.network/v1/rpc/public", + "https://rpc.ankr.com/kaia" + ] +}; From 92468f98e16acce4fdbdf58c5fed5c5e9e2fa324 Mon Sep 17 00:00:00 2001 From: HK47-droid Date: Wed, 4 Feb 2026 19:52:01 +1100 Subject: [PATCH 7/7] docs: add custom RPC configuration section --- README.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/README.md b/README.md index 59e672af..2a1b1a9d 100644 --- a/README.md +++ b/README.md @@ -156,3 +156,38 @@ node src/contract.js lightlink 0x... "transfer(address,uint256)" 0x... 1000 --ga ## 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 | +