1use std::{env, str::FromStr, time::Duration};
41
42use nautilus_dydx::{
43 common::{
44 consts::{DYDX_GRPC_URLS, DYDX_HTTP_URL, DYDX_TESTNET_GRPC_URLS, DYDX_TESTNET_HTTP_URL},
45 enums::DydxOrderStatus,
46 },
47 grpc::{
48 TxBuilder,
49 client::DydxGrpcClient,
50 order::{
51 OrderBuilder, OrderGoodUntil, OrderMarketParams, SHORT_TERM_ORDER_MAXIMUM_LIFETIME,
52 },
53 types::ChainId,
54 wallet::{Account, Wallet},
55 },
56 http::{
57 client::{DydxHttpClient, DydxRawHttpClient},
58 models::Order as DydxOrder,
59 },
60 proto::{
61 ToAny,
62 dydxprotocol::{
63 clob::{
64 MsgBatchCancel, MsgCancelOrder, MsgPlaceOrder, OrderBatch, OrderId,
65 msg_cancel_order::GoodTilOneof,
66 order::{Side as DydxSide, TimeInForce},
67 },
68 subaccounts::SubaccountId,
69 },
70 },
71};
72use nautilus_model::{enums::OrderSide, identifiers::InstrumentId, types::Quantity};
73use rust_decimal::Decimal;
74use serde::Deserialize;
75use tracing::level_filters::LevelFilter;
76
77const DEFAULT_SUBACCOUNT: u32 = 0;
78const DEFAULT_INSTRUMENT: &str = "BTC-USD-PERP.DYDX";
79const DEFAULT_SIDE: &str = "buy";
80const DEFAULT_PRICE: &str = "10000.0";
81const DEFAULT_QUANTITY: &str = "0.001";
82
83#[derive(Debug, Deserialize)]
84struct Credentials {
85 mnemonic: String,
86 #[serde(default)]
87 subaccount: u32,
88}
89
90fn load_credentials() -> Result<Credentials, Box<dyn std::error::Error>> {
91 if let Ok(mnemonic) = env::var("DYDX_MNEMONIC") {
92 tracing::info!("Loaded credentials from DYDX_MNEMONIC environment variable");
93 return Ok(Credentials {
94 mnemonic,
95 subaccount: DEFAULT_SUBACCOUNT,
96 });
97 }
98
99 Err(
100 "No credentials found. Please set DYDX_MNEMONIC environment variable"
101 .to_string()
102 .into(),
103 )
104}
105
106#[tokio::main]
107async fn main() -> Result<(), Box<dyn std::error::Error>> {
108 tracing_subscriber::fmt()
109 .with_max_level(LevelFilter::INFO)
110 .init();
111
112 let args: Vec<String> = env::args().collect();
113
114 let has_network_flag = args.iter().any(|a| a == "--mainnet" || a == "--testnet");
115 let has_instrument_flag = args.iter().any(|a| a == "--instrument");
116
117 if has_network_flag && !has_instrument_flag {
118 return run_all_edge_case_tests(&args).await;
119 }
120
121 let instrument_str = args
122 .iter()
123 .position(|a| a == "--instrument")
124 .and_then(|i| args.get(i + 1))
125 .map_or(DEFAULT_INSTRUMENT, |s| s.as_str());
126
127 let side_str = args
128 .iter()
129 .position(|a| a == "--side")
130 .and_then(|i| args.get(i + 1))
131 .map_or(DEFAULT_SIDE, |s| s.as_str());
132
133 let price_str = args
134 .iter()
135 .position(|a| a == "--price")
136 .and_then(|i| args.get(i + 1))
137 .map_or(DEFAULT_PRICE, |s| s.as_str());
138
139 let quantity_str = args
140 .iter()
141 .position(|a| a == "--quantity")
142 .and_then(|i| args.get(i + 1))
143 .map_or(DEFAULT_QUANTITY, |s| s.as_str());
144
145 let subaccount_arg = args
146 .iter()
147 .position(|a| a == "--subaccount")
148 .and_then(|i| args.get(i + 1))
149 .and_then(|s| s.parse::<u32>().ok());
150
151 let is_mainnet = args.iter().any(|a| a == "--mainnet");
152
153 rustls::crypto::aws_lc_rs::default_provider()
155 .install_default()
156 .expect("Failed to install rustls crypto provider");
157
158 let creds = load_credentials()?;
160 let grpc_urls = if is_mainnet {
161 DYDX_GRPC_URLS
162 } else {
163 DYDX_TESTNET_GRPC_URLS
164 };
165 let http_url = env::var("DYDX_HTTP_URL").unwrap_or_else(|_| {
166 if is_mainnet {
167 DYDX_HTTP_URL.to_string()
168 } else {
169 DYDX_TESTNET_HTTP_URL.to_string()
170 }
171 });
172 let subaccount_number = subaccount_arg.unwrap_or(creds.subaccount);
173
174 tracing::info!("dYdX gRPC Order Submission Test");
175 tracing::info!("================================");
176 tracing::info!(
177 "Network: {}",
178 if is_mainnet { "MAINNET" } else { "TESTNET" }
179 );
180 tracing::info!("Instrument: {}", instrument_str);
181 tracing::info!("Side: {}", side_str);
182 tracing::info!("Price: {}", price_str);
183 tracing::info!("Quantity: {}", quantity_str);
184 tracing::info!("Subaccount: {}", subaccount_number);
185 tracing::info!("");
186
187 let wallet = Wallet::from_mnemonic(&creds.mnemonic)?;
189 let mut account = wallet.account_offline(subaccount_number)?;
190 let wallet_address = account.address.clone();
191 tracing::info!("Wallet address: {}", wallet_address);
192
193 tracing::info!("Connecting to gRPC endpoints (with fallback):");
195 for url in grpc_urls {
196 tracing::info!(" - {}", url);
197 }
198 let mut grpc_client = DydxGrpcClient::new_with_fallback(grpc_urls).await?;
199
200 tracing::info!("Querying account info from chain...");
202 let (account_number, sequence) = grpc_client.query_address(&wallet_address).await?;
203 account.set_account_info(account_number, sequence);
204 tracing::info!("Account number: {}, sequence: {}", account_number, sequence);
205
206 tracing::info!("Connecting to HTTP API: {}", http_url);
208 let http_client = DydxHttpClient::new(
209 Some(http_url.clone()),
210 Some(30), None, !is_mainnet, None, )
215 .expect("Failed to create HTTP client");
216
217 let raw_http_client = DydxRawHttpClient::new(Some(http_url), Some(30), None, !is_mainnet, None)
219 .expect("Failed to create raw HTTP client");
220
221 tracing::info!("Fetching instruments...");
223 http_client.fetch_and_cache_instruments().await?;
224 tracing::info!("Instruments cached");
225
226 let height = grpc_client.latest_block_height().await?;
228 tracing::info!("Current block height: {}", height.0);
229
230 let instrument_id = InstrumentId::from(instrument_str);
231 let client_order_id: u32 = (std::time::SystemTime::now()
232 .duration_since(std::time::UNIX_EPOCH)?
233 .as_millis() as u32)
234 % 1_000_000_000;
235 let side = match side_str.to_lowercase().as_str() {
236 "buy" => OrderSide::Buy,
237 "sell" => OrderSide::Sell,
238 _ => return Err(format!("Invalid side: {side_str}").into()),
239 };
240 let quantity = Quantity::from(quantity_str);
241 let price = Decimal::from_str(price_str)?;
242
243 tracing::info!("Placing limit order for {}", instrument_id);
244 tracing::info!("Client order ID: {}", client_order_id);
245 tracing::info!("Side: {:?}", side);
246 tracing::info!("Price: ${}", price);
247 tracing::info!("Quantity: {}", quantity);
248
249 let market_params = http_client
251 .get_market_params(&instrument_id)
252 .ok_or("Market params not found in cache")?;
253
254 let params = OrderMarketParams {
255 atomic_resolution: market_params.atomic_resolution,
256 clob_pair_id: market_params.clob_pair_id,
257 oracle_price: None,
258 quantum_conversion_exponent: market_params.quantum_conversion_exponent,
259 step_base_quantums: market_params.step_base_quantums,
260 subticks_per_tick: market_params.subticks_per_tick,
261 };
262
263 let mut builder = OrderBuilder::new(
265 params,
266 wallet_address.clone(),
267 subaccount_number,
268 client_order_id,
269 );
270
271 let proto_side = DydxSide::Buy;
272 let size_decimal = Decimal::from_str(&quantity.to_string())?;
273
274 builder = builder.limit(proto_side, price, size_decimal);
275 builder = builder.short_term();
276 builder = builder.time_in_force(TimeInForce::PostOnly);
277 builder = builder.until(OrderGoodUntil::Block(
278 height.0 + SHORT_TERM_ORDER_MAXIMUM_LIFETIME,
279 ));
280
281 let order = builder
282 .build()
283 .map_err(|e| format!("Failed to build order: {e}"))?;
284
285 tracing::info!("Order built successfully");
286
287 let chain_id = if is_mainnet {
289 ChainId::Mainnet1
290 } else {
291 ChainId::Testnet4
292 };
293 let tx_builder = TxBuilder::new(chain_id, "adydx".to_string())
294 .map_err(|e| format!("TxBuilder init failed: {e}"))?;
295
296 let msg_place_order = MsgPlaceOrder { order: Some(order) };
297 let any_msg = msg_place_order.to_any();
298
299 let tx_raw = tx_builder
300 .build_transaction(&account, vec![any_msg], None, None)
301 .map_err(|e| format!("Failed to build tx: {e}"))?;
302
303 let tx_bytes = tx_raw
304 .to_bytes()
305 .map_err(|e| format!("Failed to serialize tx: {e}"))?;
306
307 tracing::info!("Broadcasting transaction...");
308 let tx_hash = grpc_client
309 .broadcast_tx(tx_bytes)
310 .await
311 .map_err(|e| format!("Broadcast failed: {e}"))?;
312
313 tracing::info!("Order placed successfully, tx_hash: {}", tx_hash);
314
315 tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
317
318 tracing::info!("Fetching open orders to cancel...");
320 let orders_response = raw_http_client
321 .get_orders(&wallet_address, subaccount_number, None, None)
322 .await?;
323
324 let open_orders: Vec<_> = orders_response
326 .into_iter()
327 .filter(|o| matches!(o.status, DydxOrderStatus::Open))
328 .collect();
329
330 if open_orders.is_empty() {
331 tracing::info!("No open orders to cancel");
332 } else {
333 tracing::info!(
334 "Found {} open order(s), canceling all...",
335 open_orders.len()
336 );
337
338 for order in open_orders {
339 let client_id: u32 = order
341 .client_id
342 .parse()
343 .map_err(|e| format!("Failed to parse client_id: {e}"))?;
344
345 let msg_cancel = MsgCancelOrder {
346 order_id: Some(OrderId {
347 subaccount_id: Some(SubaccountId {
348 owner: wallet_address.clone(),
349 number: subaccount_number,
350 }),
351 client_id,
352 order_flags: 0, clob_pair_id: market_params.clob_pair_id,
354 }),
355 good_til_oneof: Some(GoodTilOneof::GoodTilBlock(
356 height.0 + SHORT_TERM_ORDER_MAXIMUM_LIFETIME,
357 )),
358 };
359
360 let any_cancel = msg_cancel.to_any();
361 account.increment_sequence();
362 let tx_raw = tx_builder
363 .build_transaction(&account, vec![any_cancel], None, None)
364 .map_err(|e| format!("Failed to build cancel tx: {e}"))?;
365 let tx_bytes = tx_raw
366 .to_bytes()
367 .map_err(|e| format!("Failed to serialize cancel tx: {e}"))?;
368
369 tracing::info!("Canceling order client_id={}", client_id);
370 let cancel_tx_hash = grpc_client
371 .broadcast_tx(tx_bytes)
372 .await
373 .map_err(|e| format!("Cancel broadcast failed: {e}"))?;
374
375 tracing::info!("Order canceled, tx_hash: {}", cancel_tx_hash);
376 }
377 }
378
379 Ok(())
380}
381
382async fn run_all_edge_case_tests(args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
383 let is_mainnet = args.iter().any(|a| a == "--mainnet");
384 let creds = load_credentials()?;
385 let wallet = Wallet::from_mnemonic(&creds.mnemonic)?;
386 let mut account = wallet.account_offline(0)?;
387 let wallet_address = account.address.clone();
388
389 rustls::crypto::aws_lc_rs::default_provider()
390 .install_default()
391 .expect("Failed to install rustls crypto provider");
392
393 let grpc_urls = if is_mainnet {
394 DYDX_GRPC_URLS
395 } else {
396 DYDX_TESTNET_GRPC_URLS
397 };
398 let http_url = if is_mainnet {
399 DYDX_HTTP_URL
400 } else {
401 DYDX_TESTNET_HTTP_URL
402 };
403
404 let mut grpc_client = DydxGrpcClient::new_with_fallback(grpc_urls).await?;
405 let (account_number, sequence) = grpc_client.query_address(&wallet_address).await?;
406 account.set_account_info(account_number, sequence);
407
408 let http_client = DydxHttpClient::new(
409 Some(http_url.to_string()),
410 Some(30),
411 None,
412 !is_mainnet,
413 None,
414 )?;
415 let raw_http = DydxRawHttpClient::new(
416 Some(http_url.to_string()),
417 Some(30),
418 None,
419 !is_mainnet,
420 None,
421 )?;
422
423 http_client.fetch_and_cache_instruments().await?;
424 tracing::info!("Setup complete - wallet: {}", wallet_address);
425
426 run_all_edge_tests(
427 &mut grpc_client,
428 &mut account,
429 &wallet_address,
430 &http_client,
431 &raw_http,
432 is_mainnet,
433 )
434 .await
435}
436
437async fn run_all_edge_tests(
438 grpc: &mut DydxGrpcClient,
439 account: &mut Account,
440 address: &str,
441 http: &DydxHttpClient,
442 raw_http: &DydxRawHttpClient,
443 is_mainnet: bool,
444) -> Result<(), Box<dyn std::error::Error>> {
445 tracing::info!("\n=== Running All Edge Case Tests ===\n");
446
447 test_cancel_specific(grpc, account, http, raw_http, address, is_mainnet).await?;
448 tokio::time::sleep(Duration::from_secs(2)).await;
449
450 test_cancel_by_market(grpc, account, http, raw_http, address, is_mainnet).await?;
451 tokio::time::sleep(Duration::from_secs(2)).await;
452
453 test_replace_order(grpc, account, http, raw_http, address, is_mainnet).await?;
454 tokio::time::sleep(Duration::from_secs(2)).await;
455
456 test_duplicate_cancel(grpc, account, http, raw_http, address, is_mainnet).await?;
457 tokio::time::sleep(Duration::from_secs(2)).await;
458
459 test_rapid_sequence(grpc, account, http, raw_http, address, is_mainnet).await?;
460 tokio::time::sleep(Duration::from_secs(2)).await;
461
462 test_batch_cancel(grpc, account, http, raw_http, address, is_mainnet).await?;
463
464 tracing::info!("\n=== All Tests Complete ===");
465 Ok(())
466}
467
468async fn test_cancel_specific(
469 grpc: &mut DydxGrpcClient,
470 account: &Account,
471 http: &DydxHttpClient,
472 raw_http: &DydxRawHttpClient,
473 address: &str,
474 is_mainnet: bool,
475) -> Result<(), Box<dyn std::error::Error>> {
476 tracing::info!("\n--- Test: Cancel Specific Order ---");
477
478 let client_id = generate_client_id();
479 let tx_hash = place_edge_test_order(
480 grpc,
481 account,
482 http,
483 client_id,
484 "BTC-USD-PERP.DYDX",
485 10000.0,
486 is_mainnet,
487 )
488 .await?;
489 tracing::info!("Placed order {} (tx: {})", client_id, tx_hash);
490
491 tokio::time::sleep(Duration::from_secs(3)).await;
492
493 let orders = fetch_open_orders(raw_http, address).await?;
494 let target = orders.iter().find(|o| o.client_id == client_id.to_string());
495
496 match target {
497 Some(order) => {
498 tracing::info!(
499 "Order found: client_id={}, status={:?}",
500 order.client_id,
501 order.status
502 );
503
504 cancel_order_by_client_id(
505 grpc,
506 account,
507 http,
508 address,
509 client_id,
510 order.ticker.as_deref().unwrap_or("BTC-USD"),
511 is_mainnet,
512 )
513 .await?;
514 tracing::info!("Canceled order {}", client_id);
515 }
516 None => tracing::warn!("Order {} not yet indexed or already filled", client_id),
517 }
518
519 Ok(())
520}
521
522async fn test_cancel_by_market(
523 grpc: &mut DydxGrpcClient,
524 account: &Account,
525 http: &DydxHttpClient,
526 raw_http: &DydxRawHttpClient,
527 address: &str,
528 is_mainnet: bool,
529) -> Result<(), Box<dyn std::error::Error>> {
530 tracing::info!("\n--- Test: Cancel All Orders for Market ---");
531
532 for _ in 0..3 {
533 let client_id = generate_client_id();
534 place_edge_test_order(
535 grpc,
536 account,
537 http,
538 client_id,
539 "BTC-USD-PERP.DYDX",
540 10000.0,
541 is_mainnet,
542 )
543 .await?;
544 tokio::time::sleep(Duration::from_millis(500)).await;
545 }
546 tracing::info!("Placed 3 BTC orders");
547
548 tokio::time::sleep(Duration::from_secs(3)).await;
549
550 let orders = fetch_open_orders(raw_http, address).await?;
551 let btc_orders: Vec<_> = orders
552 .iter()
553 .filter(|o| o.ticker.as_deref() == Some("BTC-USD"))
554 .collect();
555
556 tracing::info!("Found {} open BTC orders", btc_orders.len());
557
558 for order in btc_orders {
559 let client_id: u32 = order.client_id.parse()?;
560 cancel_order_by_client_id(
561 grpc,
562 account,
563 http,
564 address,
565 client_id,
566 order.ticker.as_deref().unwrap_or("BTC-USD"),
567 is_mainnet,
568 )
569 .await?;
570 }
571
572 tracing::info!("Canceled all BTC orders");
573 Ok(())
574}
575
576async fn test_replace_order(
577 grpc: &mut DydxGrpcClient,
578 account: &Account,
579 http: &DydxHttpClient,
580 _raw_http: &DydxRawHttpClient,
581 address: &str,
582 is_mainnet: bool,
583) -> Result<(), Box<dyn std::error::Error>> {
584 tracing::info!("\n--- Test: Replace Order (Cancel + Place) ---");
585
586 let old_client_id = generate_client_id();
587 place_edge_test_order(
588 grpc,
589 account,
590 http,
591 old_client_id,
592 "BTC-USD-PERP.DYDX",
593 10000.0,
594 is_mainnet,
595 )
596 .await?;
597 tracing::info!("Placed original order {}", old_client_id);
598
599 tokio::time::sleep(Duration::from_secs(3)).await;
600
601 let new_client_id = generate_client_id();
602
603 cancel_order_by_client_id(
604 grpc,
605 account,
606 http,
607 address,
608 old_client_id,
609 "BTC-USD",
610 is_mainnet,
611 )
612 .await
613 .ok();
614 tracing::info!("Canceled old order {}", old_client_id);
615
616 place_edge_test_order(
617 grpc,
618 account,
619 http,
620 new_client_id,
621 "BTC-USD-PERP.DYDX",
622 11000.0,
623 is_mainnet,
624 )
625 .await?;
626 tracing::info!("Placed new order {} at $11,000", new_client_id);
627
628 Ok(())
629}
630
631async fn test_duplicate_cancel(
632 grpc: &mut DydxGrpcClient,
633 account: &Account,
634 http: &DydxHttpClient,
635 _raw_http: &DydxRawHttpClient,
636 address: &str,
637 is_mainnet: bool,
638) -> Result<(), Box<dyn std::error::Error>> {
639 tracing::info!("\n--- Test: Duplicate Cancellation ---");
640
641 let client_id = generate_client_id();
642 place_edge_test_order(
643 grpc,
644 account,
645 http,
646 client_id,
647 "BTC-USD-PERP.DYDX",
648 10000.0,
649 is_mainnet,
650 )
651 .await?;
652 tracing::info!("Placed order {}", client_id);
653
654 tokio::time::sleep(Duration::from_secs(3)).await;
655
656 cancel_order_by_client_id(
657 grpc, account, http, address, client_id, "BTC-USD", is_mainnet,
658 )
659 .await?;
660 tracing::info!("First cancel succeeded");
661
662 tokio::time::sleep(Duration::from_secs(1)).await;
663 match cancel_order_by_client_id(
664 grpc, account, http, address, client_id, "BTC-USD", is_mainnet,
665 )
666 .await
667 {
668 Ok(_) => tracing::info!("Second cancel succeeded (order may have been re-indexed)"),
669 Err(e) => tracing::info!("Second cancel failed as expected: {}", e),
670 }
671
672 Ok(())
673}
674
675async fn test_rapid_sequence(
676 grpc: &mut DydxGrpcClient,
677 account: &mut Account,
678 http: &DydxHttpClient,
679 _raw_http: &DydxRawHttpClient,
680 address: &str,
681 is_mainnet: bool,
682) -> Result<(), Box<dyn std::error::Error>> {
683 tracing::info!("\n--- Test: Rapid Order Sequence ---");
684
685 let mut client_ids = Vec::new();
686
687 for i in 0..5 {
688 let client_id = generate_client_id();
689 client_ids.push(client_id);
690
691 match place_edge_test_order(
692 grpc,
693 account,
694 http,
695 client_id,
696 "BTC-USD-PERP.DYDX",
697 10000.0 + (i as f64 * 100.0),
698 is_mainnet,
699 )
700 .await
701 {
702 Ok(tx) => tracing::info!("Order {} placed (tx: {})", i + 1, tx),
703 Err(e) => tracing::warn!("Order {} failed: {}", i + 1, e),
704 }
705
706 tokio::time::sleep(Duration::from_millis(200)).await;
707 }
708
709 tokio::time::sleep(Duration::from_secs(3)).await;
710
711 for (i, client_id) in client_ids.iter().enumerate() {
712 match cancel_order_by_client_id(
713 grpc, account, http, address, *client_id, "BTC-USD", is_mainnet,
714 )
715 .await
716 {
717 Ok(_) => tracing::info!("Order {} canceled", i + 1),
718 Err(e) => tracing::warn!("Cancel {} failed: {}", i + 1, e),
719 }
720 }
721
722 tracing::info!("Rapid sequence test complete");
723 Ok(())
724}
725
726async fn test_batch_cancel(
727 grpc: &mut DydxGrpcClient,
728 account: &mut Account,
729 http: &DydxHttpClient,
730 raw_http: &DydxRawHttpClient,
731 address: &str,
732 is_mainnet: bool,
733) -> Result<(), Box<dyn std::error::Error>> {
734 tracing::info!("\n--- Test: Batch Cancel Orders ---");
735
736 let mut client_ids = Vec::new();
738 for i in 0..5 {
739 let client_id = generate_client_id();
740 client_ids.push(client_id);
741 place_edge_test_order(
742 grpc,
743 account,
744 http,
745 client_id,
746 "BTC-USD-PERP.DYDX",
747 10000.0 + (i as f64 * 50.0),
748 is_mainnet,
749 )
750 .await?;
751 tokio::time::sleep(Duration::from_millis(300)).await;
752 }
753 tracing::info!("Placed {} BTC orders", client_ids.len());
754
755 tokio::time::sleep(Duration::from_secs(3)).await;
756
757 let instrument_id = InstrumentId::from("BTC-USD-PERP.DYDX");
759 let market_params = http
760 .get_market_params(&instrument_id)
761 .ok_or("Market params not found")?;
762
763 let height = grpc.latest_block_height().await?;
765 let order_batch = OrderBatch {
766 clob_pair_id: market_params.clob_pair_id,
767 client_ids: client_ids.clone(),
768 };
769
770 let msg_batch_cancel = MsgBatchCancel {
771 subaccount_id: Some(SubaccountId {
772 owner: address.to_string(),
773 number: 0,
774 }),
775 short_term_cancels: vec![order_batch],
776 good_til_block: height.0 + SHORT_TERM_ORDER_MAXIMUM_LIFETIME,
777 };
778
779 let chain_id = if is_mainnet {
781 ChainId::Mainnet1
782 } else {
783 ChainId::Testnet4
784 };
785 let tx_builder = TxBuilder::new(chain_id, "adydx".to_string())?;
786 let tx_raw =
787 tx_builder.build_transaction(account, vec![msg_batch_cancel.to_any()], None, None)?;
788 let tx_hash = grpc.broadcast_tx(tx_raw.to_bytes()?).await?;
789
790 tracing::info!(
791 "Batch canceled {} orders in single transaction: {}",
792 client_ids.len(),
793 tx_hash
794 );
795
796 tokio::time::sleep(Duration::from_secs(2)).await;
798 let orders = fetch_open_orders(raw_http, address).await?;
799 let remaining = orders
800 .iter()
801 .filter(|o| client_ids.contains(&o.client_id.parse::<u32>().unwrap_or(0)))
802 .count();
803 tracing::info!(
804 "Batch cancel complete - {} orders remaining (expected 0)",
805 remaining
806 );
807
808 Ok(())
809}
810
811fn generate_client_id() -> u32 {
812 (std::time::SystemTime::now()
813 .duration_since(std::time::UNIX_EPOCH)
814 .unwrap()
815 .as_millis() as u32)
816 % 1_000_000_000
817}
818
819async fn place_edge_test_order(
820 grpc: &mut DydxGrpcClient,
821 account: &Account,
822 http: &DydxHttpClient,
823 client_id: u32,
824 instrument: &str,
825 price: f64,
826 is_mainnet: bool,
827) -> Result<String, Box<dyn std::error::Error>> {
828 let instrument_id = InstrumentId::from(instrument);
829 let market_params = http
830 .get_market_params(&instrument_id)
831 .ok_or("Market params not found")?;
832
833 let params = OrderMarketParams {
834 atomic_resolution: market_params.atomic_resolution,
835 clob_pair_id: market_params.clob_pair_id,
836 oracle_price: None,
837 quantum_conversion_exponent: market_params.quantum_conversion_exponent,
838 step_base_quantums: market_params.step_base_quantums,
839 subticks_per_tick: market_params.subticks_per_tick,
840 };
841
842 let height = grpc.latest_block_height().await?;
843
844 let mut builder = OrderBuilder::new(params, account.address.clone(), 0, client_id);
845
846 builder = builder.limit(
847 DydxSide::Buy,
848 Decimal::from_str(&price.to_string())?,
849 Decimal::from_str("0.001")?,
850 );
851 builder = builder.short_term();
852 builder = builder.time_in_force(TimeInForce::PostOnly);
853 builder = builder.until(OrderGoodUntil::Block(
854 height.0 + SHORT_TERM_ORDER_MAXIMUM_LIFETIME,
855 ));
856
857 let order = builder.build()?;
858 let msg = MsgPlaceOrder { order: Some(order) };
859
860 let chain_id = if is_mainnet {
861 ChainId::Mainnet1
862 } else {
863 ChainId::Testnet4
864 };
865 let tx_builder = TxBuilder::new(chain_id, "adydx".to_string())?;
866 let tx_raw = tx_builder.build_transaction(account, vec![msg.to_any()], None, None)?;
867 let tx_hash = grpc.broadcast_tx(tx_raw.to_bytes()?).await?;
868
869 Ok(tx_hash)
870}
871
872async fn cancel_order_by_client_id(
873 grpc: &mut DydxGrpcClient,
874 account: &Account,
875 http: &DydxHttpClient,
876 address: &str,
877 client_id: u32,
878 ticker: &str,
879 is_mainnet: bool,
880) -> Result<String, Box<dyn std::error::Error>> {
881 let instrument_id = InstrumentId::from(format!("{ticker}-PERP.DYDX"));
882 let market_params = http
883 .get_market_params(&instrument_id)
884 .ok_or("Market params not found")?;
885
886 let height = grpc.latest_block_height().await?;
887
888 let msg_cancel = MsgCancelOrder {
889 order_id: Some(OrderId {
890 subaccount_id: Some(SubaccountId {
891 owner: address.to_string(),
892 number: 0,
893 }),
894 client_id,
895 order_flags: 0,
896 clob_pair_id: market_params.clob_pair_id,
897 }),
898 good_til_oneof: Some(GoodTilOneof::GoodTilBlock(
899 height.0 + SHORT_TERM_ORDER_MAXIMUM_LIFETIME,
900 )),
901 };
902
903 let chain_id = if is_mainnet {
904 ChainId::Mainnet1
905 } else {
906 ChainId::Testnet4
907 };
908 let tx_builder = TxBuilder::new(chain_id, "adydx".to_string())?;
909 let tx_raw = tx_builder.build_transaction(account, vec![msg_cancel.to_any()], None, None)?;
910 let tx_hash = grpc.broadcast_tx(tx_raw.to_bytes()?).await?;
911
912 Ok(tx_hash)
913}
914
915async fn fetch_open_orders(
916 raw_http: &DydxRawHttpClient,
917 address: &str,
918) -> Result<Vec<DydxOrder>, Box<dyn std::error::Error>> {
919 let orders = raw_http.get_orders(address, 0, None, None).await?;
920 Ok(orders
921 .into_iter()
922 .filter(|o| matches!(o.status, DydxOrderStatus::Open))
923 .collect())
924}