Skip to content

Commit ea9436d

Browse files
authored
Merge pull request #153 from GeoGu360/fix/meteora-v0.3.3
fix(meteora): on-chain positions, WSOL wrap, close fix, retry logic (v0.3.3)
2 parents b74287b + e01ddad commit ea9436d

10 files changed

Lines changed: 386 additions & 175 deletions

File tree

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"name": "meteora",
33
"description": "Meteora DLMM plugin for Solana — search liquidity pools, get swap quotes, view user positions, execute token swaps, add and remove liquidity",
4-
"version": "0.3.2"
4+
"version": "0.3.3"
55
}

skills/meteora/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/meteora/Cargo.toml

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

66
[[bin]]

skills/meteora/SKILL.md

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
name: meteora
33
description: "Meteora DLMM plugin for Solana — search liquidity pools, get swap quotes, view user positions, execute token swaps, add and remove liquidity"
4-
version: "0.3.2"
4+
version: "0.3.3"
55
tags:
66
- solana
77
- dex
@@ -44,7 +44,7 @@ if ! command -v meteora >/dev/null 2>&1; then
4444
mingw*_aarch64|msys*_aarch64|cygwin*_aarch64) TARGET="aarch64-pc-windows-msvc"; EXT=".exe" ;;
4545
esac
4646
mkdir -p ~/.local/bin
47-
curl -fsSL "https://github.com/okx/plugin-store/releases/download/plugins/meteora@0.3.2/meteora-${TARGET}${EXT}" -o ~/.local/bin/meteora${EXT}
47+
curl -fsSL "https://github.com/okx/plugin-store/releases/download/plugins/meteora@0.3.3/meteora-${TARGET}${EXT}" -o ~/.local/bin/meteora${EXT}
4848
chmod +x ~/.local/bin/meteora${EXT}
4949
fi
5050
```
@@ -63,7 +63,7 @@ if [ ! -f "$REPORT_FLAG" ]; then
6363
unset _K
6464
curl -s -X POST "https://plugin-store-dun.vercel.app/install" \
6565
-H "Content-Type: application/json" \
66-
-d '{"name":"meteora","version":"0.3.2"}' >/dev/null 2>&1 || true
66+
-d '{"name":"meteora","version":"0.3.3"}' >/dev/null 2>&1 || true
6767
curl -s -X POST "https://www.okx.com/priapi/v1/wallet/plugins/download/report" \
6868
-H "Content-Type: application/json" \
6969
-d '{"pluginName":"meteora","divId":"'"$DIV_ID"'"}' >/dev/null 2>&1 || true
@@ -76,10 +76,11 @@ fi
7676

7777
## Architecture
7878

79-
- **Read operations** (`get-pools`, `get-pool-detail`, `get-swap-quote`, `get-user-positions`) → direct REST API calls to `https://dlmm.datapi.meteora.ag`; no wallet or confirmation needed
79+
- **Read operations** (`get-pools`, `get-pool-detail`, `get-swap-quote`) → direct REST API calls to `https://dlmm.datapi.meteora.ag`; no wallet or confirmation needed
80+
- **`get-user-positions`** → queries on-chain via Solana `getProgramAccounts` + BinArray accounts; computes token amounts directly from chain state; no wallet or confirmation needed
8081
- **Swap** (`swap`) → after user confirmation, executes via `onchainos swap execute --chain solana`; CLI handles signing and broadcast automatically
81-
- **Add liquidity** (`add-liquidity`) → builds a Solana transaction natively in Rust (initialize position + add liquidity instructions), submits via `onchainos wallet contract-call --chain 501`; uses SpotBalanced strategy distributing tokens across 70-bin position centered at active bin
82-
- **Remove liquidity** (`remove-liquidity`) → builds a `removeLiquidityByRange` instruction and optionally a `closePosition` instruction, submits via `onchainos wallet contract-call --chain 501`; 600k compute budget requested
82+
- **Add liquidity** (`add-liquidity`) → builds a Solana transaction natively in Rust (initialize position + add liquidity instructions), submits via `onchainos wallet contract-call --chain 501`; uses SpotBalanced strategy distributing tokens across 70-bin position centered at active bin; auto-wraps SOL to WSOL when needed; retries once on simulation errors
83+
- **Remove liquidity** (`remove-liquidity`) → builds `removeLiquidityByRange` + optional `claimFee` + `closePositionIfEmpty` instructions, submits via `onchainos wallet contract-call --chain 501`; 600k compute budget requested
8384

8485
## Supported Operations
8586

@@ -133,14 +134,21 @@ meteora get-swap-quote --from-token So11111111111111111111111111111111111111112
133134

134135
### get-user-positions — View LP positions
135136

136-
View a user's DLMM LP positions including token amounts, bin ranges, and unclaimed fees.
137+
View a user's DLMM LP positions with token amounts computed from on-chain BinArray data.
137138

138139
```
139140
meteora get-user-positions [--wallet <address>] [--pool <pool_address>]
140141
```
141142

142143
If `--wallet` is omitted, uses the currently logged-in onchainos wallet.
143144

145+
**Output fields per position:** `position_address`, `pool_address`, `owner`,
146+
`token_x_mint`, `token_y_mint`, `token_x_amount`, `token_y_amount`,
147+
`token_x_decimals`, `token_y_decimals`,
148+
`bin_range` (lower_bin_id / upper_bin_id), `active_bins`, `source`
149+
150+
> Use `position_address` directly as `--position` when calling `remove-liquidity`.
151+
144152
**Examples:**
145153
```
146154
meteora get-user-positions

skills/meteora/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: meteora
3-
version: "0.3.2"
3+
version: "0.3.3"
44
description: "Meteora DLMM plugin for searching pools, getting swap quotes, checking positions, executing swaps, and adding liquidity on Solana"
55
author:
66
name: "skylavis-sky"

skills/meteora/src/commands/add_liquidity.rs

Lines changed: 135 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ pub async fn execute(args: &AddLiquidityArgs, dry_run: bool) -> anyhow::Result<(
6060
let reserve_x = Pubkey::from(pool.reserve_x);
6161
let reserve_y = Pubkey::from(pool.reserve_y);
6262

63+
// Native SOL mint — used to detect when WSOL wrap is needed
64+
const WSOL_MINT: Pubkey =
65+
solana_pubkey::pubkey!("So11111111111111111111111111111111111111112");
66+
6367
// ── 3. Fetch token decimals ──────────────────────────────────────────────
6468
let mint_x_str = token_x_mint.to_string();
6569
let mint_y_str = token_y_mint.to_string();
@@ -183,99 +187,154 @@ pub async fn execute(args: &AddLiquidityArgs, dry_run: bool) -> anyhow::Result<(
183187

184188
// ATAs are created on-the-fly in the instruction list if missing.
185189

186-
// ── 10. Check bin array existence & get blockhash ───────────────────────
187-
// Sequential to avoid saturating the public RPC rate limit
188-
// (steps 3 and 7 already fired 5+ concurrent calls).
190+
// ── 10-13. Build + submit with one automatic retry ───────────────────────
191+
// After closing a position, the Solana RPC may briefly return stale account
192+
// states (e.g. position still exists, or a bin array incorrectly missing).
193+
// If the first attempt fails with a simulation error, we wait 2 s, re-check
194+
// all mutable account states, rebuild the instruction list, and retry once.
189195
let bin_arr_lower_str = bin_array_lower.to_string();
190196
let bin_arr_upper_str = bin_array_upper.to_string();
191-
let bin_arr_lower_exists = solana_rpc::account_exists(&client, &bin_arr_lower_str).await?;
192-
let bin_arr_upper_exists = solana_rpc::account_exists(&client, &bin_arr_upper_str).await?;
193-
let blockhash = solana_rpc::get_latest_blockhash(&client).await?;
194197

195-
// ── 11. Build instructions ───────────────────────────────────────────────
196-
let mut instructions = Vec::new();
198+
let mut last_result = serde_json::Value::Null;
199+
let mut last_ok = false;
197200

198-
// Create ATAs if missing (idempotent — safe to include even if they exist)
199-
if !ata_x_exists {
200-
instructions.push(meteora_ix::ix_create_ata_idempotent(
201-
&wallet, &user_token_x, &wallet, &token_x_mint,
202-
));
203-
}
204-
if !ata_y_exists {
205-
instructions.push(meteora_ix::ix_create_ata_idempotent(
206-
&wallet, &user_token_y, &wallet, &token_y_mint,
207-
));
208-
}
201+
for attempt in 0u32..2 {
202+
if attempt > 0 {
203+
eprintln!("[retry] Simulation failed — waiting 2 s then re-checking account states...");
204+
tokio::time::sleep(std::time::Duration::from_millis(2_000)).await;
205+
}
206+
207+
// Re-check mutable account states on every attempt so the instruction
208+
// list always reflects the current on-chain reality.
209+
let ba_lower_exists = solana_rpc::account_exists(&client, &bin_arr_lower_str).await?;
210+
let ba_upper_exists = solana_rpc::account_exists(&client, &bin_arr_upper_str).await?;
211+
let pos_exists_now = solana_rpc::account_exists(&client, &pos_str).await?;
212+
let blockhash = solana_rpc::get_latest_blockhash(&client).await?;
213+
214+
eprintln!(
215+
"[attempt {}] bin_array_lower_exists={} bin_array_upper_exists={} position_exists={}",
216+
attempt + 1, ba_lower_exists, ba_upper_exists, pos_exists_now
217+
);
209218

210-
// Initialize bin arrays if they don't exist (required before adding liquidity)
211-
if !bin_arr_lower_exists {
212-
instructions.push(meteora_ix::ix_initialize_bin_array(
219+
let mut instructions = Vec::new();
220+
221+
// Request extra compute budget — add_liquidity_by_strategy with position
222+
// init can exceed the default 200k CU limit.
223+
instructions.push(meteora_ix::ix_set_compute_unit_limit(600_000));
224+
225+
// Create ATAs if missing (idempotent — safe to include even if they exist)
226+
if !ata_x_exists {
227+
instructions.push(meteora_ix::ix_create_ata_idempotent(
228+
&wallet, &user_token_x, &wallet, &token_x_mint,
229+
));
230+
}
231+
if !ata_y_exists {
232+
instructions.push(meteora_ix::ix_create_ata_idempotent(
233+
&wallet, &user_token_y, &wallet, &token_y_mint,
234+
));
235+
}
236+
237+
// Wrap SOL → WSOL if token_x is the native SOL mint and amount_x > 0.
238+
// Transfers SOL to the WSOL ATA and syncs its token balance, ensuring
239+
// add_liquidity_by_strategy can debit the correct token amount.
240+
if token_x_mint == WSOL_MINT && amount_x_raw > 0 {
241+
instructions.push(meteora_ix::ix_sol_transfer(&wallet, &user_token_x, amount_x_raw));
242+
instructions.push(meteora_ix::ix_sync_native(&user_token_x));
243+
}
244+
if token_y_mint == WSOL_MINT && amount_y_raw > 0 {
245+
instructions.push(meteora_ix::ix_sol_transfer(&wallet, &user_token_y, amount_y_raw));
246+
instructions.push(meteora_ix::ix_sync_native(&user_token_y));
247+
}
248+
249+
// Initialize bin arrays only if they genuinely don't exist.
250+
if !ba_lower_exists {
251+
instructions.push(meteora_ix::ix_initialize_bin_array(
252+
&lb_pair,
253+
&bin_array_lower,
254+
&wallet,
255+
lower_idx,
256+
));
257+
}
258+
if lower_idx != upper_idx && !ba_upper_exists {
259+
instructions.push(meteora_ix::ix_initialize_bin_array(
260+
&lb_pair,
261+
&bin_array_upper,
262+
&wallet,
263+
upper_idx,
264+
));
265+
}
266+
267+
if !pos_exists_now {
268+
instructions.push(meteora_ix::ix_initialize_position_pda(
269+
&wallet,
270+
&lb_pair,
271+
&position,
272+
pos_lower,
273+
width,
274+
));
275+
}
276+
277+
instructions.push(meteora_ix::ix_add_liquidity_by_strategy(
278+
&position,
213279
&lb_pair,
280+
&user_token_x,
281+
&user_token_y,
282+
&reserve_x,
283+
&reserve_y,
284+
&token_x_mint,
285+
&token_y_mint,
214286
&bin_array_lower,
215-
&wallet,
216-
lower_idx,
217-
));
218-
}
219-
// Only add upper if it's a different bin array
220-
if lower_idx != upper_idx && !bin_arr_upper_exists {
221-
instructions.push(meteora_ix::ix_initialize_bin_array(
222-
&lb_pair,
223287
&bin_array_upper,
224288
&wallet,
225-
upper_idx,
289+
amount_x_raw,
290+
amount_y_raw,
291+
pool.active_id,
292+
args.bin_range, // max_active_bin_slippage
293+
effective_liq_lower,
294+
effective_liq_upper,
226295
));
227-
}
228296

229-
if !position_exists {
230-
instructions.push(meteora_ix::ix_initialize_position_pda(
231-
&wallet,
232-
&lb_pair,
233-
&position,
234-
pos_lower,
235-
width,
236-
));
297+
let tx_b58 = meteora_ix::build_tx_b58(&instructions, &wallet, blockhash)?;
298+
eprintln!("[debug] unsigned_tx_b58={}", &tx_b58[..32]);
299+
eprintln!("[debug] num_instructions={}", instructions.len());
300+
301+
let result = onchainos::contract_call_solana(&tx_b58, &meteora_ix::DLMM_PROGRAM.to_string())?;
302+
let ok = result["ok"].as_bool().unwrap_or(false)
303+
|| result["data"]["ok"].as_bool().unwrap_or(false);
304+
305+
if ok {
306+
last_result = result;
307+
last_ok = true;
308+
break;
309+
}
310+
311+
// Check if this is a simulation/transient error worth retrying.
312+
let err_str = result
313+
.get("error")
314+
.or_else(|| result["data"].get("error"))
315+
.and_then(|v| v.as_str())
316+
.unwrap_or("")
317+
.to_string();
318+
let is_retryable = err_str.contains("simulation")
319+
|| err_str.contains("ProgramAccountNotFound")
320+
|| err_str.contains("BlockhashNotFound")
321+
|| err_str.contains("stale");
322+
323+
last_result = result;
324+
if !is_retryable || attempt >= 1 {
325+
break;
326+
}
327+
eprintln!("[retry] Retryable error detected: {err_str}");
237328
}
238329

239-
instructions.push(meteora_ix::ix_add_liquidity_by_strategy(
240-
&position,
241-
&lb_pair,
242-
&user_token_x,
243-
&user_token_y,
244-
&reserve_x,
245-
&reserve_y,
246-
&token_x_mint,
247-
&token_y_mint,
248-
&bin_array_lower,
249-
&bin_array_upper,
250-
&wallet,
251-
amount_x_raw,
252-
amount_y_raw,
253-
pool.active_id,
254-
args.bin_range, // max_active_bin_slippage
255-
effective_liq_lower,
256-
effective_liq_upper,
257-
));
258-
259-
// ── 12. Build & encode transaction ───────────────────────────────────────
260-
let tx_b58 = meteora_ix::build_tx_b58(&instructions, &wallet, blockhash)?;
261-
262-
// Debug: print tx for manual simulation if needed
263-
eprintln!("[debug] unsigned_tx_b58={}", &tx_b58[..32]);
264-
eprintln!("[debug] num_instructions={}", instructions.len());
265-
266-
// ── 13. Send via onchainos ───────────────────────────────────────────────
267-
let result = onchainos::contract_call_solana(&tx_b58, &meteora_ix::DLMM_PROGRAM.to_string())?;
268-
269-
let tx_hash = result["data"]["txHash"]
330+
let tx_hash = last_result["data"]["txHash"]
270331
.as_str()
271-
.or_else(|| result["txHash"].as_str())
332+
.or_else(|| last_result["txHash"].as_str())
272333
.unwrap_or("pending")
273334
.to_string();
274335

275-
let ok = result["ok"].as_bool().unwrap_or(false);
276-
277336
let output = json!({
278-
"ok": ok,
337+
"ok": last_ok,
279338
"pool": args.pool,
280339
"wallet": wallet_str,
281340
"position": position.to_string(),
@@ -287,7 +346,7 @@ pub async fn execute(args: &AddLiquidityArgs, dry_run: bool) -> anyhow::Result<(
287346
} else {
288347
String::new()
289348
},
290-
"raw_result": result,
349+
"raw_result": last_result,
291350
});
292351
println!("{}", serde_json::to_string_pretty(&output)?);
293352
Ok(())

0 commit comments

Comments
 (0)