Skip to content

Commit aa1ebce

Browse files
authored
Merge pull request #146 from GeoGu360/fix/gmx-v2-preflight-v0.2.1
fix(gmx-v2): pre-flight checks for balance, collateral, ETH fee (v0.2.1)
2 parents 4086af8 + 59ee040 commit aa1ebce

8 files changed

Lines changed: 130 additions & 14 deletions

File tree

skills/gmx-v2/.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "gmx-v2",
33
"description": "Trade perpetuals and spot on GMX V2 — open/close leveraged positions, place limit/stop orders, add/remove GM pool liquidity, query markets and positions",
4-
"version": "0.2.0",
4+
"version": "0.2.1",
55
"author": {"name": "GeoGu360", "github": "GeoGu360"},
66
"homepage": "https://github.com/GeoGu360/plugin-store/tree/main/skills/gmx-v2",
77
"repository": "https://github.com/GeoGu360/plugin-store",

skills/gmx-v2/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

skills/gmx-v2/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "gmx-v2"
3-
version = "0.2.0"
3+
version = "0.2.1"
44
edition = "2021"
55

66
[[bin]]

skills/gmx-v2/SKILL.md

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
name: gmx-v2
33
description: "Trade perpetuals and spot on GMX V2 — open/close leveraged positions, place limit/stop orders, add/remove GM pool liquidity, query markets and positions. Trigger phrases: open position GMX, close position GMX, GMX trade, GMX leverage, GMX liquidity, deposit GM pool, withdraw GM pool, GMX stop loss, GMX take profit, cancel order GMX, claim funding fees GMX."
4-
version: 0.2.0
4+
version: 0.2.1
55
author: "GeoGu360"
66
tags:
77
- perpetuals
@@ -49,7 +49,7 @@ if ! command -v gmx-v2 >/dev/null 2>&1; then
4949
mingw*_aarch64|msys*_aarch64|cygwin*_aarch64) TARGET="aarch64-pc-windows-msvc"; EXT=".exe" ;;
5050
esac
5151
mkdir -p ~/.local/bin
52-
curl -fsSL "https://github.com/okx/plugin-store/releases/download/plugins/gmx-v2@0.2.0/gmx-v2-${TARGET}${EXT}" -o ~/.local/bin/gmx-v2${EXT}
52+
curl -fsSL "https://github.com/okx/plugin-store/releases/download/plugins/gmx-v2@0.2.1/gmx-v2-${TARGET}${EXT}" -o ~/.local/bin/gmx-v2${EXT}
5353
chmod +x ~/.local/bin/gmx-v2${EXT}
5454
fi
5555
```
@@ -71,7 +71,7 @@ if [ ! -f "$REPORT_FLAG" ]; then
7171
# Report to Vercel stats
7272
curl -s -X POST "https://plugin-store-dun.vercel.app/install" \
7373
-H "Content-Type: application/json" \
74-
-d '{"name":"gmx-v2","version":"0.2.0"}' >/dev/null 2>&1 || true
74+
-d '{"name":"gmx-v2","version":"0.2.1"}' >/dev/null 2>&1 || true
7575
# Report to OKX API (with HMAC-signed device token)
7676
curl -s -X POST "https://www.okx.com/priapi/v1/wallet/plugins/download/report" \
7777
-H "Content-Type: application/json" \
@@ -250,10 +250,24 @@ gmx-v2 --chain arbitrum open-position \
250250

251251
**Flow:**
252252
1. Run `--dry-run` to preview calldata and estimated leverage
253-
2. **Ask user to confirm** market, direction, size, slippage, and execution fee
254-
3. If collateral allowance is insufficient, the binary prints a NOTE — re-run with `--confirm` flag to approve and open in one step
255-
4. Submits multicall via `onchainos wallet contract-call`
256-
5. Keeper executes position within 1–30 seconds
253+
2. Pre-flight: checks ERC-20 collateral token balance — returns `{"ok":false,"error":"INSUFFICIENT_TOKEN_BALANCE"}` JSON if wallet balance < `--collateral-amount`
254+
3. Pre-flight: checks GMX `minCollateralUsd` on-chain — returns `{"ok":false,"error":"INSUFFICIENT_COLLATERAL"}` JSON if post-fee collateral would fall below GMX minimum (keeper would cancel immediately)
255+
4. Pre-flight: checks wallet ETH balance — returns `{"ok":false,"error":"INSUFFICIENT_ETH_FOR_EXECUTION"}` JSON if ETH < execution fee + gas buffer
256+
5. **Ask user to confirm** market, direction, size, slippage, and execution fee
257+
6. If collateral allowance is insufficient, the binary prints a NOTE — re-run with `--confirm` flag to approve and open in one step
258+
7. Submits multicall via `onchainos wallet contract-call`
259+
8. Keeper executes position within 1–30 seconds
260+
261+
**Pre-flight error JSON examples:**
262+
```json
263+
{"ok":false,"error":"INSUFFICIENT_TOKEN_BALANCE","reason":"Wallet collateral token balance is less than the requested collateral amount.","collateral_token":"0xaf88...","wallet_balance":"500000","wallet_balance_usd":"0.5000","required_amount":"1000000","required_amount_usd":"1.0000","suggestion":"Reduce --collateral-amount to at most 500000 or top up the collateral token."}
264+
```
265+
```json
266+
{"ok":false,"error":"INSUFFICIENT_COLLATERAL","reason":"Post-fee collateral is below GMX minimum. Keeper will cancel the order immediately.","collateral_usd":"1.0000","estimated_open_fee_usd":"0.0050","collateral_after_fee_usd":"0.9950","min_collateral_usd":"1.0000","suggestion":"Increase --collateral-amount so that collateral_after_fee_usd >= min_collateral_usd, or reduce --size-usd to lower the fee."}
267+
```
268+
```json
269+
{"ok":false,"error":"INSUFFICIENT_ETH_FOR_EXECUTION","reason":"Wallet does not have enough ETH to cover execution fee + gas.","eth_balance":"0.00050000","execution_fee_eth":"0.00100000","gas_buffer_eth":"0.00020000","eth_required":"0.00120000","suggestion":"Top up wallet 0xYourWallet with at least 0.000700 ETH on Arbitrum."}
270+
```
257271

258272
---
259273

skills/gmx-v2/plugin.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
schema_version: 1
22
name: gmx-v2
3-
version: "0.2.0"
3+
version: "0.2.1"
44
description: "Trade perpetuals and spot on GMX V2 — open/close leveraged positions, place limit/stop orders, add/remove GM pool liquidity on Arbitrum and Avalanche"
55
author:
66
name: GeoGu360

skills/gmx-v2/src/commands/get_orders.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,7 @@ pub async fn run(chain: &str, args: GetOrdersArgs) -> anyhow::Result<()> {
5656
"chain": chain,
5757
"wallet": wallet,
5858
"count": orders.len(),
59-
"orders": orders,
60-
"raw": raw
59+
"orders": orders
6160
}))?
6261
);
6362
Ok(())

skills/gmx-v2/src/commands/open_position.rs

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,12 +171,79 @@ pub async fn run(chain: &str, dry_run: bool, confirm: bool, args: OpenPositionAr
171171
0.0
172172
};
173173

174+
// Pre-flight check 1 — ERC-20 token balance
175+
let token_balance = crate::rpc::check_erc20_balance(
176+
cfg.rpc_url, &args.collateral_token, &wallet,
177+
).await.unwrap_or(u128::MAX);
178+
if token_balance < args.collateral_amount {
179+
let collateral_price_for_check = crate::api::find_price(&tickers, &args.collateral_token)
180+
.and_then(|t| t.min_price.as_deref().and_then(|p| p.parse::<u128>().ok()))
181+
.unwrap_or(0);
182+
let collateral_usd_have = token_balance as f64 * collateral_price_for_check as f64 / 1e30;
183+
let collateral_usd_need = args.collateral_amount as f64 * collateral_price_for_check as f64 / 1e30;
184+
println!("{}", serde_json::to_string_pretty(&serde_json::json!({
185+
"ok": false,
186+
"error": "INSUFFICIENT_TOKEN_BALANCE",
187+
"reason": "Wallet collateral token balance is less than the requested collateral amount.",
188+
"collateral_token": args.collateral_token,
189+
"wallet_balance": token_balance.to_string(),
190+
"wallet_balance_usd": format!("{:.4}", collateral_usd_have),
191+
"required_amount": args.collateral_amount.to_string(),
192+
"required_amount_usd": format!("{:.4}", collateral_usd_need),
193+
"suggestion": format!("Reduce --collateral-amount to at most {} or top up the collateral token.", token_balance)
194+
}))?);
195+
return Ok(());
196+
}
197+
198+
// Pre-flight check 2 — GMX minimum collateral
199+
let min_collateral_usd_key = "6497f0f2c47edc68f06ede1c06d3475f939eb1a8341362460277bcd8ee7419f4";
200+
let min_collateral_usd_30 = crate::rpc::datastore_get_uint(
201+
cfg.datastore, min_collateral_usd_key, cfg.rpc_url,
202+
).await;
203+
let collateral_price_raw = crate::api::find_price(&tickers, &args.collateral_token)
204+
.and_then(|t| t.min_price.as_deref().and_then(|p| p.parse::<u128>().ok()))
205+
.unwrap_or(0);
206+
let collateral_usd_30 = (args.collateral_amount as u128).saturating_mul(collateral_price_raw);
207+
let estimated_fee_30 = size_delta_usd / 1000; // 0.1% conservative open fee
208+
if min_collateral_usd_30 > 0 && collateral_usd_30 < min_collateral_usd_30.saturating_add(estimated_fee_30) {
209+
println!("{}", serde_json::to_string_pretty(&serde_json::json!({
210+
"ok": false,
211+
"error": "INSUFFICIENT_COLLATERAL",
212+
"reason": "Post-fee collateral is below GMX minimum. Keeper will cancel the order immediately.",
213+
"collateral_usd": format!("{:.4}", collateral_usd_30 as f64 / 1e30),
214+
"estimated_open_fee_usd": format!("{:.4}", estimated_fee_30 as f64 / 1e30),
215+
"collateral_after_fee_usd": format!("{:.4}", collateral_usd_30.saturating_sub(estimated_fee_30) as f64 / 1e30),
216+
"min_collateral_usd": format!("{:.4}", min_collateral_usd_30 as f64 / 1e30),
217+
"suggestion": "Increase --collateral-amount so that collateral_after_fee_usd >= min_collateral_usd, or reduce --size-usd to lower the fee."
218+
}))?);
219+
return Ok(());
220+
}
221+
222+
// Pre-flight check 3 — ETH execution fee
223+
let eth_balance = crate::rpc::get_eth_balance(&wallet, cfg.rpc_url).await;
224+
let gas_margin: u128 = 200_000_000_000_000; // 0.0002 ETH conservative gas buffer
225+
let eth_required = execution_fee.saturating_add(gas_margin);
226+
if eth_balance < eth_required {
227+
println!("{}", serde_json::to_string_pretty(&serde_json::json!({
228+
"ok": false,
229+
"error": "INSUFFICIENT_ETH_FOR_EXECUTION",
230+
"reason": "Wallet does not have enough ETH to cover execution fee + gas.",
231+
"eth_balance": format!("{:.8}", eth_balance as f64 / 1e18),
232+
"execution_fee_eth": format!("{:.8}", execution_fee as f64 / 1e18),
233+
"gas_buffer_eth": format!("{:.8}", gas_margin as f64 / 1e18),
234+
"eth_required": format!("{:.8}", eth_required as f64 / 1e18),
235+
"suggestion": format!("Top up wallet {} with at least {:.6} ETH on Arbitrum.",
236+
wallet, (eth_required.saturating_sub(eth_balance)) as f64 / 1e18)
237+
}))?);
238+
return Ok(());
239+
}
240+
174241
// Preview
175242
eprintln!("=== Open Position Preview ===");
176243
eprintln!("Market: {}", market.name.as_deref().unwrap_or("?"));
177244
eprintln!("Direction: {}", if args.long { "LONG" } else { "SHORT" });
178245
eprintln!("Size: ${:.2} USD", args.size_usd);
179-
eprintln!("Collateral: {} units", args.collateral_amount);
246+
eprintln!("Collateral: {} units (${:.4} USD)", args.collateral_amount, collateral_usd_30 as f64 / 1e30);
180247
eprintln!("Current price: ${:.4}", mid_price_usd);
181248
eprintln!("Acceptable price: {}", acceptable_price);
182249
eprintln!("Execution fee: {} wei", execution_fee);

skills/gmx-v2/src/rpc.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,42 @@ pub async fn eth_call(to: &str, data: &str, rpc_url: &str) -> anyhow::Result<Str
3333
Ok(result)
3434
}
3535

36+
/// Query ERC-20 balanceOf(address) — returns None on RPC failure
37+
pub async fn check_erc20_balance(rpc_url: &str, token: &str, owner: &str) -> Option<u128> {
38+
let owner_clean = owner.trim_start_matches("0x");
39+
let calldata = format!("0x70a08231{:0>64}", owner_clean);
40+
let result = eth_call(token, &calldata, rpc_url).await.ok()?;
41+
let hex = result.trim_start_matches("0x");
42+
u128::from_str_radix(hex, 16).ok()
43+
}
44+
45+
/// Query GMX DataStore getUint(bytes32) — returns 0 on failure
46+
pub async fn datastore_get_uint(datastore: &str, key: &str, rpc_url: &str) -> u128 {
47+
let calldata = format!("0xbd02d0f5{:0>64}", key);
48+
match eth_call(datastore, &calldata, rpc_url).await {
49+
Ok(result) => {
50+
let hex = result.trim_start_matches("0x");
51+
u128::from_str_radix(&hex[hex.len().saturating_sub(32)..], 16).unwrap_or(0)
52+
}
53+
Err(_) => 0,
54+
}
55+
}
56+
57+
/// Query native ETH balance (wei) for an address — returns 0 on failure
58+
pub async fn get_eth_balance(address: &str, rpc_url: &str) -> u128 {
59+
let client = reqwest::Client::new();
60+
let body = serde_json::json!({
61+
"jsonrpc": "2.0", "method": "eth_getBalance",
62+
"params": [address, "latest"], "id": 1
63+
});
64+
let resp: serde_json::Value = match client.post(rpc_url).json(&body).send().await {
65+
Ok(r) => match r.json().await { Ok(v) => v, Err(_) => return 0 },
66+
Err(_) => return 0,
67+
};
68+
let hex = resp["result"].as_str().unwrap_or("0x0").trim_start_matches("0x");
69+
u128::from_str_radix(hex, 16).unwrap_or(0)
70+
}
71+
3672
/// Decode a bytes32 from eth_call result at a given 32-byte slot offset
3773
pub fn decode_bytes32(hex_data: &str, slot: usize) -> Option<String> {
3874
let data = hex_data.trim_start_matches("0x");

0 commit comments

Comments
 (0)