@@ -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