diff --git a/skills/etherfi-plugin/.claude-plugin/plugin.json b/skills/etherfi-plugin/.claude-plugin/plugin.json index 048b0488a..10fd228ee 100644 --- a/skills/etherfi-plugin/.claude-plugin/plugin.json +++ b/skills/etherfi-plugin/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "etherfi", "description": "Liquid restaking on Ethereum — deposit ETH to receive eETH, wrap eETH to weETH (ERC-4626), and check positions with APY", - "version": "0.2.3", + "version": "0.2.7", "author": { "name": "GeoGu360", "github": "GeoGu360" diff --git a/skills/etherfi-plugin/Cargo.lock b/skills/etherfi-plugin/Cargo.lock index 55258179c..afa220b70 100644 --- a/skills/etherfi-plugin/Cargo.lock +++ b/skills/etherfi-plugin/Cargo.lock @@ -213,8 +213,8 @@ dependencies = [ ] [[package]] -name = "etherfi" -version = "0.2.3" +name = "etherfi-plugin" +version = "0.2.7" dependencies = [ "anyhow", "clap", diff --git a/skills/etherfi-plugin/Cargo.toml b/skills/etherfi-plugin/Cargo.toml index b83347009..959769bd2 100644 --- a/skills/etherfi-plugin/Cargo.toml +++ b/skills/etherfi-plugin/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "etherfi-plugin" -version = "0.2.6" +version = "0.2.7" edition = "2021" [[bin]] diff --git a/skills/etherfi-plugin/SKILL.md b/skills/etherfi-plugin/SKILL.md index cfb6a1f90..50c884642 100644 --- a/skills/etherfi-plugin/SKILL.md +++ b/skills/etherfi-plugin/SKILL.md @@ -4,7 +4,7 @@ description: > Liquid restaking on Ethereum. Deposit ETH into ether.fi LiquidityPool to receive eETH, wrap eETH into weETH (ERC-4626 yield-bearing token) to earn staking + EigenLayer restaking rewards, unstake eETH back to ETH, check balances, and view current APY. -version: "0.2.6" +version: "0.2.7" author: GeoGu360 tags: - liquid-staking @@ -29,7 +29,7 @@ tags: # Check for skill updates (1-hour cache) UPDATE_CACHE="$HOME/.plugin-store/update-cache/etherfi-plugin" CACHE_MAX=3600 -LOCAL_VER="0.2.6" +LOCAL_VER="0.2.7" DO_CHECK=true if [ -f "$UPDATE_CACHE" ]; then @@ -102,7 +102,7 @@ case "${OS}_${ARCH}" in mingw*_aarch64|msys*_aarch64|cygwin*_aarch64) TARGET="aarch64-pc-windows-msvc"; EXT=".exe" ;; esac mkdir -p ~/.local/bin -curl -fsSL "https://github.com/okx/plugin-store/releases/download/plugins/etherfi-plugin@0.2.6/etherfi-plugin-${TARGET}${EXT}" -o ~/.local/bin/.etherfi-plugin-core${EXT} +curl -fsSL "https://github.com/okx/plugin-store/releases/download/plugins/etherfi-plugin@0.2.7/etherfi-plugin-${TARGET}${EXT}" -o ~/.local/bin/.etherfi-plugin-core${EXT} chmod +x ~/.local/bin/.etherfi-plugin-core${EXT} # Symlink CLI name to universal launcher @@ -110,7 +110,7 @@ ln -sf "$LAUNCHER" ~/.local/bin/etherfi-plugin # Register version mkdir -p "$HOME/.plugin-store/managed" -echo "0.2.6" > "$HOME/.plugin-store/managed/etherfi-plugin" +echo "0.2.7" > "$HOME/.plugin-store/managed/etherfi-plugin" ``` ### Report install (auto-injected, runs once) @@ -130,7 +130,7 @@ if [ ! -f "$REPORT_FLAG" ]; then # Report to Vercel stats curl -s -X POST "https://plugin-store-dun.vercel.app/install" \ -H "Content-Type: application/json" \ - -d '{"name":"etherfi-plugin","version":"0.2.6"}' >/dev/null 2>&1 || true + -d '{"name":"etherfi-plugin","version":"0.2.7"}' >/dev/null 2>&1 || true # Report to OKX API (with HMAC-signed device token) curl -s -X POST "https://www.okx.com/priapi/v1/wallet/plugins/download/report" \ -H "Content-Type: application/json" \ @@ -199,28 +199,14 @@ etherfi positions etherfi positions --owner 0xYourWalletAddress ``` -**Output (human-readable table):** -``` -ether.fi Positions - Wallet: 0x... -───────────────────────────────────────────────────────────── -Token Balance As eETH USD Value -───────────────────────────────────────────────────────────── -eETH 1.500000 1.500000 $3,321.60 -weETH 0.980000 1.070534 $2,372.02 -───────────────────────────────────────────────────────────── -Total 2.570534 $5,693.62 - -Protocol Stats: - weETH/eETH rate: 1.09238163 - APY: 2.30% - TVL: $5825437011 - ETH price: $2214.40 +**Output:** +```json +{"ok":true,"wallet":"0x...","eeth_balance":"1.500000","eeth_balance_raw":"1500000000000000000","weeth_balance":"0.980000","weeth_balance_raw":"980000000000000000","weeth_as_eeth":"1.070534","total_eeth":"2.570534","total_usd":"5693.62","rate":"1.09238163","apy_pct":"2.30","tvl_usd":"5825437011","eth_price_usd":"2214.40"} ``` -USD column is omitted if the ETH price API is unavailable. +`total_usd`, `apy_pct`, `tvl_usd`, `eth_price_usd` are `null` if the external price/stats API is unavailable. Balance and rate errors fail-fast with a clear message (RPC failure should not silently show 0). -**Display fields:** Token balances, eETH-equivalent totals, USD valuations (when available), APY, TVL, exchange rate. +**Output fields:** `ok`, `wallet`, `eeth_balance`, `eeth_balance_raw`, `weeth_balance`, `weeth_balance_raw`, `weeth_as_eeth`, `total_eeth`, `total_usd`, `rate`, `apy_pct`, `tvl_usd`, `eth_price_usd` --- @@ -291,7 +277,7 @@ etherfi unstake --amount 1.0 --dry-run 1. Parse eETH amount to wei (18 decimals) 2. Resolve wallet address via `onchainos wallet addresses` 3. Validate eETH balance is sufficient -4. Check eETH allowance for LiquidityPool; if insufficient, approve `u128::MAX` first (selector `0x095ea7b3`) — **displays explicit warning before proceeding** (3-second delay after approve) +4. Check eETH allowance for LiquidityPool; if insufficient, approve `u128::MAX` first — **waits for on-chain confirmation before proceeding** (polls `onchainos wallet history`, up to 90s) 5. **Requires `--confirm`** — without it, prints preview JSON and exits 6. Call `LiquidityPool.requestWithdraw(recipient, amountOfEEth)` (selector `0x397a1b28`) 7. WithdrawRequestNFT is minted — token ID is in the tx receipt (check Etherscan) @@ -345,17 +331,18 @@ etherfi wrap --amount 1.0 --dry-run **Output:** ```json -{"ok":true,"txHash":"0xdef...","action":"wrap","eETHWrapped":"1.0","eETHWei":"1000000000000000000","weETHBalance":"0.96"} +{"ok":true,"txHash":"0xdef...","action":"wrap","eETHWrapped":"1.0","eETHWei":"1000000000000000000","weETHExpected":"0.915226","weETHBalance":"0.915226"} ``` -**Display:** `txHash` (abbreviated), `eETHWrapped`, `weETHBalance` (updated balance). +**Display:** `txHash` (abbreviated), `eETHWrapped`, `weETHExpected` (preview of weETH to receive), `weETHBalance` (updated balance after tx). **Flow:** 1. Parse eETH amount to wei -2. Resolve wallet; check eETH balance is sufficient -3. Check eETH allowance for weETH contract; approve `u128::MAX` if needed — **displays an explicit warning about unlimited approval before proceeding** (3-second delay) -4. **Requires `--confirm`** for each step (approve + wrap) -5. Call `weETH.wrap(uint256)` via `onchainos wallet contract-call` (selector `0xea598cb0`) +2. Fetch `weETH.getRate()` and compute `weETHExpected = eETH / rate` — shown in preview before confirm +3. Resolve wallet; check eETH balance is sufficient +4. Check eETH allowance for weETH contract; approve `u128::MAX` if needed — **waits for on-chain confirmation before proceeding** (polls `onchainos wallet history`, up to 90s) +5. **Requires `--confirm`** for each step (approve + wrap) +6. Call `weETH.wrap(uint256)` via `onchainos wallet contract-call` (selector `0xea598cb0`) --- diff --git a/skills/etherfi-plugin/plugin.yaml b/skills/etherfi-plugin/plugin.yaml index e0fbb198a..dbce4f711 100644 --- a/skills/etherfi-plugin/plugin.yaml +++ b/skills/etherfi-plugin/plugin.yaml @@ -1,6 +1,6 @@ schema_version: 1 name: etherfi-plugin -version: "0.2.6" +version: "0.2.7" description: Liquid restaking on Ethereum — deposit ETH to receive eETH, wrap/unwrap eETH/weETH (ERC-4626), unstake eETH back to ETH, and check positions with APY author: name: GeoGu360 diff --git a/skills/etherfi-plugin/src/commands/positions.rs b/skills/etherfi-plugin/src/commands/positions.rs index 33024541f..6da6e9d85 100644 --- a/skills/etherfi-plugin/src/commands/positions.rs +++ b/skills/etherfi-plugin/src/commands/positions.rs @@ -22,88 +22,58 @@ pub async fn run(args: PositionsArgs) -> anyhow::Result<()> { None => resolve_wallet(CHAIN_ID)?, }; - println!("Fetching ether.fi positions for wallet: {}", owner); - - // Parallel fetch: balances - let (eeth_balance, weeth_balance) = tokio::join!( + // Parallel fetch: balances — fail-fast, 0 would be misleading if RPC is down + let (eeth_result, weeth_result) = tokio::join!( get_balance(eeth, &owner, rpc), get_balance(weeth, &owner, rpc), ); - let eeth_balance = eeth_balance.unwrap_or(0); - let weeth_balance = weeth_balance.unwrap_or(0); + let eeth_balance = eeth_result + .map_err(|e| anyhow::anyhow!("Failed to fetch eETH balance: {}", e))?; + let weeth_balance = weeth_result + .map_err(|e| anyhow::anyhow!("Failed to fetch weETH balance: {}", e))?; - // Exchange rate: weETH → eETH (getRate()) - let exchange_rate = crate::rpc::weeth_get_rate(weeth, rpc).await.ok(); - let rate = exchange_rate.unwrap_or(0.0); + // Exchange rate: weETH → eETH — required for meaningful totals + let rate = crate::rpc::weeth_get_rate(weeth, rpc).await + .map_err(|e| anyhow::anyhow!("Failed to fetch weETH exchange rate: {}", e))?; + if rate == 0.0 { + anyhow::bail!( + "weETH exchange rate returned 0 — RPC may be unavailable. \ + Check https://ethereum-rpc.publicnode.com connectivity." + ); + } - // Protocol stats + ETH price (non-fatal) + // Protocol stats + ETH price (non-fatal — external API may be unavailable) let (stats, eth_price_usd) = tokio::join!( fetch_stats(), crate::api::fetch_eth_price(), ); let stats = stats.unwrap_or(crate::api::EtherFiStats { apy: None, tvl: None }); - // --- Derived values --- - let eeth_f64 = eeth_balance as f64 / 1e18; + // Derived values + let eeth_f64 = eeth_balance as f64 / 1e18; let weeth_f64 = weeth_balance as f64 / 1e18; let weeth_as_eeth = weeth_f64 * rate; let total_eeth = eeth_f64 + weeth_as_eeth; + let total_usd = eth_price_usd.map(|p| total_eeth * p); - // --- Human-readable output --- - const SEP_W_USD: &str = "─────────────────────────────────────────────────────────────"; - const SEP_NO_USD: &str = "────────────────────────────────────────────────"; - - println!("\nether.fi Positions"); - println!(" Wallet: {}", owner); - - if let Some(price) = eth_price_usd { - // Full table with USD column - println!("{}", SEP_W_USD); - println!("{:<10} {:>14} {:>14} {:>14}", "Token", "Balance", "As eETH", "USD Value"); - println!("{}", SEP_W_USD); - println!( - "{:<10} {:>14.6} {:>14.6} {:>14}", - "eETH", eeth_f64, eeth_f64, - format!("${:.2}", eeth_f64 * price) - ); - println!( - "{:<10} {:>14.6} {:>14.6} {:>14}", - "weETH", weeth_f64, weeth_as_eeth, - format!("${:.2}", weeth_as_eeth * price) - ); - println!("{}", SEP_W_USD); - println!( - "{:<10} {:>14} {:>14.6} {:>14}", - "Total", "", total_eeth, - format!("${:.2}", total_eeth * price) - ); - } else { - // Narrower table without USD column - println!("{}", SEP_NO_USD); - println!("{:<10} {:>14} {:>14}", "Token", "Balance", "As eETH"); - println!("{}", SEP_NO_USD); - println!("{:<10} {:>14.6} {:>14.6}", "eETH", eeth_f64, eeth_f64); - println!("{:<10} {:>14.6} {:>14.6}", "weETH", weeth_f64, weeth_as_eeth); - println!("{}", SEP_NO_USD); - println!("{:<10} {:>14} {:>14.6}", "Total", "", total_eeth); - } - - println!("\nProtocol Stats:"); - match exchange_rate { - Some(r) => println!(" weETH/eETH rate: {:.8}", r), - None => println!(" weETH/eETH rate: N/A"), - } - match stats.apy { - Some(v) => println!(" APY: {:.2}%", v), - None => println!(" APY: N/A"), - } - match stats.tvl { - Some(v) => println!(" TVL: ${:.0}", v), - None => println!(" TVL: N/A"), - } - if let Some(p) = eth_price_usd { - println!(" ETH price: ${:.2}", p); - } + println!( + "{}", + serde_json::json!({ + "ok": true, + "wallet": owner, + "eeth_balance": format!("{:.6}", eeth_f64), + "eeth_balance_raw": eeth_balance.to_string(), + "weeth_balance": format!("{:.6}", weeth_f64), + "weeth_balance_raw": weeth_balance.to_string(), + "weeth_as_eeth": format!("{:.6}", weeth_as_eeth), + "total_eeth": format!("{:.6}", total_eeth), + "total_usd": total_usd.map(|v| format!("{:.2}", v)), + "rate": format!("{:.8}", rate), + "apy_pct": stats.apy.map(|v| format!("{:.2}", v)), + "tvl_usd": stats.tvl.map(|v| format!("{:.0}", v)), + "eth_price_usd": eth_price_usd.map(|v| format!("{:.2}", v)), + }) + ); Ok(()) } diff --git a/skills/etherfi-plugin/src/commands/unstake.rs b/skills/etherfi-plugin/src/commands/unstake.rs index b5ea0b517..ab602a6cf 100644 --- a/skills/etherfi-plugin/src/commands/unstake.rs +++ b/skills/etherfi-plugin/src/commands/unstake.rs @@ -1,11 +1,10 @@ use clap::Args; -use tokio::time::{sleep, Duration}; use crate::calldata::{build_request_withdraw_calldata, build_claim_withdraw_calldata}; use crate::config::{ build_approve_calldata, eeth_address, format_units, liquidity_pool_address, parse_units, rpc_url, withdraw_request_nft_address, CHAIN_ID, }; -use crate::onchainos::{extract_tx_hash, resolve_wallet, wallet_contract_call}; +use crate::onchainos::{extract_tx_hash, resolve_wallet, wait_for_tx, wallet_contract_call}; use crate::rpc::{get_allowance, get_balance, is_withdrawal_finalized}; #[derive(Args)] @@ -111,10 +110,11 @@ async fn run_request(args: UnstakeArgs) -> anyhow::Result<()> { return Ok(()); } - let approve_tx = extract_tx_hash(&approve_result); - println!("Approve tx: {}", approve_tx); - // Wait for approve to be mined before requestWithdraw (Ethereum ~12s per block) - sleep(Duration::from_secs(15)).await; + let approve_tx = extract_tx_hash(&approve_result).to_string(); + println!("Approve tx: {} — waiting for confirmation...", approve_tx); + wait_for_tx(approve_tx, wallet.clone()).await + .map_err(|e| anyhow::anyhow!("Approve tx did not confirm: {}", e))?; + println!("Approve confirmed."); } } diff --git a/skills/etherfi-plugin/src/commands/unwrap.rs b/skills/etherfi-plugin/src/commands/unwrap.rs index 82cd2786d..34912a478 100644 --- a/skills/etherfi-plugin/src/commands/unwrap.rs +++ b/skills/etherfi-plugin/src/commands/unwrap.rs @@ -105,12 +105,16 @@ pub async fn run(args: UnwrapArgs) -> anyhow::Result<()> { }; println!( - "{{\"ok\":true,\"txHash\":\"{}\",\"action\":\"unwrap\",\"weETHRedeemed\":\"{}\",\"weETHWei\":\"{}\",\"eETHExpected\":\"{}\",\"eETHBalance\":\"{}\"}}", - tx_hash, - args.amount, - weeth_wei, - format_units(eeth_expected, 18), - eeth_balance_str + "{}", + serde_json::json!({ + "ok": true, + "txHash": tx_hash, + "action": "unwrap", + "weETHRedeemed": args.amount, + "weETHWei": weeth_wei.to_string(), + "eETHExpected": format!("{:.6}", eeth_expected as f64 / 1e18), + "eETHBalance": eeth_balance_str, + }) ); Ok(()) diff --git a/skills/etherfi-plugin/src/commands/wrap.rs b/skills/etherfi-plugin/src/commands/wrap.rs index 9353a9412..461649dad 100644 --- a/skills/etherfi-plugin/src/commands/wrap.rs +++ b/skills/etherfi-plugin/src/commands/wrap.rs @@ -1,11 +1,10 @@ use clap::Args; -use tokio::time::{sleep, Duration}; use crate::calldata::build_wrap_calldata; use crate::config::{ build_approve_calldata, eeth_address, format_units, parse_units, rpc_url, weeth_address, CHAIN_ID, }; -use crate::onchainos::{extract_tx_hash, resolve_wallet, wallet_contract_call}; +use crate::onchainos::{extract_tx_hash, resolve_wallet, wait_for_tx, wallet_contract_call}; use crate::rpc::{get_allowance, get_balance}; #[derive(Args)] @@ -36,6 +35,15 @@ pub async fn run(args: WrapArgs) -> anyhow::Result<()> { // Resolve wallet address let wallet = resolve_wallet(CHAIN_ID)?; + // Preview: expected weETH output via getRate() — 1 weETH = rate eETH → weETH = eETH / rate + let weeth_expected_str = match crate::rpc::weeth_get_rate(weeth, rpc).await { + Ok(rate) if rate > 0.0 => { + let expected = (eeth_wei as f64 / rate) as u128; + format!("{:.6}", expected as f64 / 1e18) + } + _ => "N/A".to_string(), + }; + println!( "Wrapping {} eETH ({} wei) → weETH", args.amount, eeth_wei @@ -43,6 +51,7 @@ pub async fn run(args: WrapArgs) -> anyhow::Result<()> { println!(" eETH contract: {}", eeth); println!(" weETH contract: {}", weeth); println!(" Wallet: {}", wallet); + println!(" Expected weETH to receive: {}", weeth_expected_str); println!(" Run with --confirm to broadcast. (Proceeding automatically in non-interactive mode.)"); // Step 1: Check eETH balance @@ -84,10 +93,11 @@ pub async fn run(args: WrapArgs) -> anyhow::Result<()> { return Ok(()); } - let approve_tx = extract_tx_hash(&approve_result); - println!("Approve tx: {}", approve_tx); - // Wait for approve nonce to clear before wrapping - sleep(Duration::from_secs(3)).await; + let approve_tx = extract_tx_hash(&approve_result).to_string(); + println!("Approve tx: {} — waiting for confirmation...", approve_tx); + wait_for_tx(approve_tx, wallet.clone()).await + .map_err(|e| anyhow::anyhow!("Approve tx did not confirm: {}", e))?; + println!("Approve confirmed."); } } @@ -122,8 +132,16 @@ pub async fn run(args: WrapArgs) -> anyhow::Result<()> { }; println!( - "{{\"ok\":true,\"txHash\":\"{}\",\"action\":\"wrap\",\"eETHWrapped\":\"{}\",\"eETHWei\":\"{}\",\"weETHBalance\":\"{}\"}}", - tx_hash, args.amount, eeth_wei, weeth_balance_str + "{}", + serde_json::json!({ + "ok": true, + "txHash": tx_hash, + "action": "wrap", + "eETHWrapped": args.amount, + "eETHWei": eeth_wei.to_string(), + "weETHExpected": weeth_expected_str, + "weETHBalance": weeth_balance_str, + }) ); Ok(()) diff --git a/skills/etherfi-plugin/src/onchainos.rs b/skills/etherfi-plugin/src/onchainos.rs index 92b080e08..204979d20 100644 --- a/skills/etherfi-plugin/src/onchainos.rs +++ b/skills/etherfi-plugin/src/onchainos.rs @@ -1,6 +1,46 @@ use std::process::Command; use serde_json::Value; +/// Poll onchainos wallet history until txStatus is SUCCESS or FAILED (or 90s timeout). +/// Uses spawn_blocking so Command::output() doesn't block the Tokio runtime thread. +pub async fn wait_for_tx(tx_hash: String, wallet_addr: String) -> anyhow::Result<()> { + tokio::task::spawn_blocking(move || wait_for_tx_sync(&tx_hash, &wallet_addr)) + .await + .map_err(|e| anyhow::anyhow!("spawn_blocking error: {}", e))? +} + +fn wait_for_tx_sync(tx_hash: &str, wallet_addr: &str) -> anyhow::Result<()> { + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(90); + loop { + if std::time::Instant::now() > deadline { + anyhow::bail!("Timeout (90s) waiting for tx {} to confirm", tx_hash); + } + let output = Command::new("onchainos") + .args([ + "wallet", "history", + "--tx-hash", tx_hash, + "--address", wallet_addr, + "--chain", "1", + ]) + .output(); + if let Ok(out) = output { + if let Ok(v) = serde_json::from_str::(&String::from_utf8_lossy(&out.stdout)) { + if let Some(entry) = v["data"].as_array().and_then(|a| a.first()) { + match entry["txStatus"].as_str() { + Some("SUCCESS") => return Ok(()), + Some("FAILED") => { + let reason = entry["failReason"].as_str().unwrap_or(""); + anyhow::bail!("approve tx {} failed on-chain: {}", tx_hash, reason); + } + _ => {} // PENDING — keep polling + } + } + } + } + std::thread::sleep(std::time::Duration::from_secs(3)); + } +} + /// Resolve the EVM wallet address for Ethereum (chain_id=1) from the onchainos CLI. /// Parses `onchainos wallet addresses` JSON and returns the first matching EVM address. pub fn resolve_wallet(chain_id: u64) -> anyhow::Result {