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/meteora/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "meteora",
"description": "Meteora DLMM plugin for Solana — search liquidity pools, get swap quotes, view user positions, execute token swaps, add and remove liquidity",
"version": "0.3.2"
"version": "0.3.3"
}
2 changes: 1 addition & 1 deletion skills/meteora/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/meteora/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "meteora"
version = "0.3.2"
version = "0.3.3"
edition = "2021"

[[bin]]
Expand Down
22 changes: 15 additions & 7 deletions skills/meteora/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
name: meteora
description: "Meteora DLMM plugin for Solana — search liquidity pools, get swap quotes, view user positions, execute token swaps, add and remove liquidity"
version: "0.3.2"
version: "0.3.3"
tags:
- solana
- dex
Expand Down Expand Up @@ -44,7 +44,7 @@ if ! command -v meteora >/dev/null 2>&1; then
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/meteora@0.3.2/meteora-${TARGET}${EXT}" -o ~/.local/bin/meteora${EXT}
curl -fsSL "https://github.com/okx/plugin-store/releases/download/plugins/meteora@0.3.3/meteora-${TARGET}${EXT}" -o ~/.local/bin/meteora${EXT}
chmod +x ~/.local/bin/meteora${EXT}
fi
```
Expand All @@ -63,7 +63,7 @@ if [ ! -f "$REPORT_FLAG" ]; then
unset _K
curl -s -X POST "https://plugin-store-dun.vercel.app/install" \
-H "Content-Type: application/json" \
-d '{"name":"meteora","version":"0.3.2"}' >/dev/null 2>&1 || true
-d '{"name":"meteora","version":"0.3.3"}' >/dev/null 2>&1 || true
curl -s -X POST "https://www.okx.com/priapi/v1/wallet/plugins/download/report" \
-H "Content-Type: application/json" \
-d '{"pluginName":"meteora","divId":"'"$DIV_ID"'"}' >/dev/null 2>&1 || true
Expand All @@ -76,10 +76,11 @@ fi

## Architecture

- **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
- **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
- **`get-user-positions`** → queries on-chain via Solana `getProgramAccounts` + BinArray accounts; computes token amounts directly from chain state; no wallet or confirmation needed
- **Swap** (`swap`) → after user confirmation, executes via `onchainos swap execute --chain solana`; CLI handles signing and broadcast automatically
- **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
- **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
- **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
- **Remove liquidity** (`remove-liquidity`) → builds `removeLiquidityByRange` + optional `claimFee` + `closePositionIfEmpty` instructions, submits via `onchainos wallet contract-call --chain 501`; 600k compute budget requested

## Supported Operations

Expand Down Expand Up @@ -133,14 +134,21 @@ meteora get-swap-quote --from-token So11111111111111111111111111111111111111112

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

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

```
meteora get-user-positions [--wallet <address>] [--pool <pool_address>]
```

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

**Output fields per position:** `position_address`, `pool_address`, `owner`,
`token_x_mint`, `token_y_mint`, `token_x_amount`, `token_y_amount`,
`token_x_decimals`, `token_y_decimals`,
`bin_range` (lower_bin_id / upper_bin_id), `active_bins`, `source`

> Use `position_address` directly as `--position` when calling `remove-liquidity`.

**Examples:**
```
meteora get-user-positions
Expand Down
2 changes: 1 addition & 1 deletion skills/meteora/plugin.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
schema_version: 1
name: meteora
version: "0.3.2"
version: "0.3.3"
description: "Meteora DLMM plugin for searching pools, getting swap quotes, checking positions, executing swaps, and adding liquidity on Solana"
author:
name: "skylavis-sky"
Expand Down
211 changes: 135 additions & 76 deletions skills/meteora/src/commands/add_liquidity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ pub async fn execute(args: &AddLiquidityArgs, dry_run: bool) -> anyhow::Result<(
let reserve_x = Pubkey::from(pool.reserve_x);
let reserve_y = Pubkey::from(pool.reserve_y);

// Native SOL mint — used to detect when WSOL wrap is needed
const WSOL_MINT: Pubkey =
solana_pubkey::pubkey!("So11111111111111111111111111111111111111112");

// ── 3. Fetch token decimals ──────────────────────────────────────────────
let mint_x_str = token_x_mint.to_string();
let mint_y_str = token_y_mint.to_string();
Expand Down Expand Up @@ -183,99 +187,154 @@ pub async fn execute(args: &AddLiquidityArgs, dry_run: bool) -> anyhow::Result<(

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

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

// ── 11. Build instructions ───────────────────────────────────────────────
let mut instructions = Vec::new();
let mut last_result = serde_json::Value::Null;
let mut last_ok = false;

// Create ATAs if missing (idempotent — safe to include even if they exist)
if !ata_x_exists {
instructions.push(meteora_ix::ix_create_ata_idempotent(
&wallet, &user_token_x, &wallet, &token_x_mint,
));
}
if !ata_y_exists {
instructions.push(meteora_ix::ix_create_ata_idempotent(
&wallet, &user_token_y, &wallet, &token_y_mint,
));
}
for attempt in 0u32..2 {
if attempt > 0 {
eprintln!("[retry] Simulation failed — waiting 2 s then re-checking account states...");
tokio::time::sleep(std::time::Duration::from_millis(2_000)).await;
}

// Re-check mutable account states on every attempt so the instruction
// list always reflects the current on-chain reality.
let ba_lower_exists = solana_rpc::account_exists(&client, &bin_arr_lower_str).await?;
let ba_upper_exists = solana_rpc::account_exists(&client, &bin_arr_upper_str).await?;
let pos_exists_now = solana_rpc::account_exists(&client, &pos_str).await?;
let blockhash = solana_rpc::get_latest_blockhash(&client).await?;

eprintln!(
"[attempt {}] bin_array_lower_exists={} bin_array_upper_exists={} position_exists={}",
attempt + 1, ba_lower_exists, ba_upper_exists, pos_exists_now
);

// Initialize bin arrays if they don't exist (required before adding liquidity)
if !bin_arr_lower_exists {
instructions.push(meteora_ix::ix_initialize_bin_array(
let mut instructions = Vec::new();

// Request extra compute budget — add_liquidity_by_strategy with position
// init can exceed the default 200k CU limit.
instructions.push(meteora_ix::ix_set_compute_unit_limit(600_000));

// Create ATAs if missing (idempotent — safe to include even if they exist)
if !ata_x_exists {
instructions.push(meteora_ix::ix_create_ata_idempotent(
&wallet, &user_token_x, &wallet, &token_x_mint,
));
}
if !ata_y_exists {
instructions.push(meteora_ix::ix_create_ata_idempotent(
&wallet, &user_token_y, &wallet, &token_y_mint,
));
}

// Wrap SOL → WSOL if token_x is the native SOL mint and amount_x > 0.
// Transfers SOL to the WSOL ATA and syncs its token balance, ensuring
// add_liquidity_by_strategy can debit the correct token amount.
if token_x_mint == WSOL_MINT && amount_x_raw > 0 {
instructions.push(meteora_ix::ix_sol_transfer(&wallet, &user_token_x, amount_x_raw));
instructions.push(meteora_ix::ix_sync_native(&user_token_x));
}
if token_y_mint == WSOL_MINT && amount_y_raw > 0 {
instructions.push(meteora_ix::ix_sol_transfer(&wallet, &user_token_y, amount_y_raw));
instructions.push(meteora_ix::ix_sync_native(&user_token_y));
}

// Initialize bin arrays only if they genuinely don't exist.
if !ba_lower_exists {
instructions.push(meteora_ix::ix_initialize_bin_array(
&lb_pair,
&bin_array_lower,
&wallet,
lower_idx,
));
}
if lower_idx != upper_idx && !ba_upper_exists {
instructions.push(meteora_ix::ix_initialize_bin_array(
&lb_pair,
&bin_array_upper,
&wallet,
upper_idx,
));
}

if !pos_exists_now {
instructions.push(meteora_ix::ix_initialize_position_pda(
&wallet,
&lb_pair,
&position,
pos_lower,
width,
));
}

instructions.push(meteora_ix::ix_add_liquidity_by_strategy(
&position,
&lb_pair,
&user_token_x,
&user_token_y,
&reserve_x,
&reserve_y,
&token_x_mint,
&token_y_mint,
&bin_array_lower,
&wallet,
lower_idx,
));
}
// Only add upper if it's a different bin array
if lower_idx != upper_idx && !bin_arr_upper_exists {
instructions.push(meteora_ix::ix_initialize_bin_array(
&lb_pair,
&bin_array_upper,
&wallet,
upper_idx,
amount_x_raw,
amount_y_raw,
pool.active_id,
args.bin_range, // max_active_bin_slippage
effective_liq_lower,
effective_liq_upper,
));
}

if !position_exists {
instructions.push(meteora_ix::ix_initialize_position_pda(
&wallet,
&lb_pair,
&position,
pos_lower,
width,
));
let tx_b58 = meteora_ix::build_tx_b58(&instructions, &wallet, blockhash)?;
eprintln!("[debug] unsigned_tx_b58={}", &tx_b58[..32]);
eprintln!("[debug] num_instructions={}", instructions.len());

let result = onchainos::contract_call_solana(&tx_b58, &meteora_ix::DLMM_PROGRAM.to_string())?;
let ok = result["ok"].as_bool().unwrap_or(false)
|| result["data"]["ok"].as_bool().unwrap_or(false);

if ok {
last_result = result;
last_ok = true;
break;
}

// Check if this is a simulation/transient error worth retrying.
let err_str = result
.get("error")
.or_else(|| result["data"].get("error"))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let is_retryable = err_str.contains("simulation")
|| err_str.contains("ProgramAccountNotFound")
|| err_str.contains("BlockhashNotFound")
|| err_str.contains("stale");

last_result = result;
if !is_retryable || attempt >= 1 {
break;
}
eprintln!("[retry] Retryable error detected: {err_str}");
}

instructions.push(meteora_ix::ix_add_liquidity_by_strategy(
&position,
&lb_pair,
&user_token_x,
&user_token_y,
&reserve_x,
&reserve_y,
&token_x_mint,
&token_y_mint,
&bin_array_lower,
&bin_array_upper,
&wallet,
amount_x_raw,
amount_y_raw,
pool.active_id,
args.bin_range, // max_active_bin_slippage
effective_liq_lower,
effective_liq_upper,
));

// ── 12. Build & encode transaction ───────────────────────────────────────
let tx_b58 = meteora_ix::build_tx_b58(&instructions, &wallet, blockhash)?;

// Debug: print tx for manual simulation if needed
eprintln!("[debug] unsigned_tx_b58={}", &tx_b58[..32]);
eprintln!("[debug] num_instructions={}", instructions.len());

// ── 13. Send via onchainos ───────────────────────────────────────────────
let result = onchainos::contract_call_solana(&tx_b58, &meteora_ix::DLMM_PROGRAM.to_string())?;

let tx_hash = result["data"]["txHash"]
let tx_hash = last_result["data"]["txHash"]
.as_str()
.or_else(|| result["txHash"].as_str())
.or_else(|| last_result["txHash"].as_str())
.unwrap_or("pending")
.to_string();

let ok = result["ok"].as_bool().unwrap_or(false);

let output = json!({
"ok": ok,
"ok": last_ok,
"pool": args.pool,
"wallet": wallet_str,
"position": position.to_string(),
Expand All @@ -287,7 +346,7 @@ pub async fn execute(args: &AddLiquidityArgs, dry_run: bool) -> anyhow::Result<(
} else {
String::new()
},
"raw_result": result,
"raw_result": last_result,
});
println!("{}", serde_json::to_string_pretty(&output)?);
Ok(())
Expand Down
Loading
Loading