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
2 changes: 1 addition & 1 deletion skills/etherfi-plugin/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
4 changes: 2 additions & 2 deletions skills/etherfi-plugin/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion skills/etherfi-plugin/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "etherfi-plugin"
version = "0.2.6"
version = "0.2.7"
edition = "2021"

[[bin]]
Expand Down
49 changes: 18 additions & 31 deletions skills/etherfi-plugin/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -102,15 +102,15 @@ 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
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)
Expand All @@ -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" \
Expand Down Expand Up @@ -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`

---

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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`)

---

Expand Down
2 changes: 1 addition & 1 deletion skills/etherfi-plugin/plugin.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
104 changes: 37 additions & 67 deletions skills/etherfi-plugin/src/commands/positions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}
12 changes: 6 additions & 6 deletions skills/etherfi-plugin/src/commands/unstake.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand Down Expand Up @@ -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.");
}
}

Expand Down
16 changes: 10 additions & 6 deletions skills/etherfi-plugin/src/commands/unwrap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
Expand Down
34 changes: 26 additions & 8 deletions skills/etherfi-plugin/src/commands/wrap.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand Down Expand Up @@ -36,13 +35,23 @@ 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
);
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
Expand Down Expand Up @@ -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.");
}
}

Expand Down Expand Up @@ -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(())
Expand Down
Loading
Loading