1use std::{
24 collections::HashMap,
25 env,
26 io::{self, Write},
27 str::FromStr,
28 sync::{
29 Arc,
30 atomic::{AtomicBool, Ordering},
31 mpsc,
32 },
33 thread,
34 time::{Duration, SystemTime},
35};
36
37use alloy_primitives::{Address, B256, keccak256};
38use alloy_signer::SignerSync;
39use alloy_signer_local::PrivateKeySigner;
40use alloy_sol_types::Eip712Domain;
41use nautilus_network::http::{HttpClient, Method};
42use serde::{Deserialize, Serialize};
43
44use super::consts::{
45 HYPERLIQUID_CHAIN_ID, NAUTILUS_BUILDER_FEE_ADDRESS, NAUTILUS_BUILDER_FEE_MAKER_TENTHS_BP,
46 NAUTILUS_BUILDER_FEE_TAKER_TENTHS_BP, exchange_url, info_url,
47};
48use crate::{
49 common::credential::EvmPrivateKey,
50 http::{
51 error::Result,
52 models::{HyperliquidExecBuilderFee, RESPONSE_STATUS_OK},
53 },
54};
55
56const BUILDER_CODES_APPROVAL_FEE_RATE: &str = "0.01%";
58
59#[must_use]
64pub fn resolve_builder_fee(symbol: &str, post_only: bool) -> Option<HyperliquidExecBuilderFee> {
65 if symbol.ends_with("-SPOT") {
66 return None;
67 }
68
69 let fee_tenths_bp = if post_only {
70 NAUTILUS_BUILDER_FEE_MAKER_TENTHS_BP
71 } else {
72 NAUTILUS_BUILDER_FEE_TAKER_TENTHS_BP
73 };
74
75 Some(HyperliquidExecBuilderFee {
76 address: NAUTILUS_BUILDER_FEE_ADDRESS.to_string(),
77 fee_tenths_bp,
78 })
79}
80
81#[must_use]
91pub fn resolve_builder_fee_batch(orders: &[(&str, bool)]) -> Option<HyperliquidExecBuilderFee> {
92 let mut min: Option<HyperliquidExecBuilderFee> = None;
93
94 for &(symbol, post_only) in orders {
95 let fee = resolve_builder_fee(symbol, post_only)?;
96 min = Some(match min {
97 Some(current) if current.fee_tenths_bp <= fee.fee_tenths_bp => current,
98 _ => fee,
99 });
100 }
101
102 min
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct BuilderFeeInfo {
108 pub address: String,
110 pub perp_taker_tenths_bp: u32,
112 pub perp_maker_tenths_bp: u32,
114 pub approval_rate: String,
116}
117
118impl Default for BuilderFeeInfo {
119 fn default() -> Self {
120 Self::new()
121 }
122}
123
124impl BuilderFeeInfo {
125 #[must_use]
127 pub fn new() -> Self {
128 Self {
129 address: NAUTILUS_BUILDER_FEE_ADDRESS.to_string(),
130 perp_taker_tenths_bp: NAUTILUS_BUILDER_FEE_TAKER_TENTHS_BP,
131 perp_maker_tenths_bp: NAUTILUS_BUILDER_FEE_MAKER_TENTHS_BP,
132 approval_rate: BUILDER_CODES_APPROVAL_FEE_RATE.to_string(),
133 }
134 }
135
136 pub fn print(&self) {
138 let separator = "=".repeat(60);
139
140 println!("{separator}");
141 println!("NautilusTrader Hyperliquid Builder Fee Configuration");
142 println!("{separator}");
143 println!();
144 println!("Builder address: {}", self.address);
145 println!();
146 println!("Fee rates charged per fill (perpetuals only, no fee on spot):");
147 println!(
148 " - Taker: {:.3}% ({} tenths of a basis point)",
149 self.perp_taker_tenths_bp as f64 / 1000.0,
150 self.perp_taker_tenths_bp,
151 );
152 println!(
153 " - Maker: {:.3}% ({} tenths of a basis point)",
154 self.perp_maker_tenths_bp as f64 / 1000.0,
155 self.perp_maker_tenths_bp,
156 );
157 println!();
158 println!("These fees are charged in addition to Hyperliquid's standard fees.");
159 println!();
160 println!("This is at the low end of ecosystem norms.");
161 println!("Hyperliquid allows up to 0.1% (10 bps) for perps and 1% (100 bps) for spot.");
162 println!();
163 println!("Source: crates/adapters/hyperliquid/src/common/consts.rs");
164 println!("{separator}");
165 }
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct BuilderFeeApprovalResult {
171 pub success: bool,
173 pub status: String,
175 pub message: Option<String>,
177 pub wallet_address: String,
179 pub builder_address: String,
181 pub is_testnet: bool,
183}
184
185#[allow(clippy::missing_panics_doc)]
204pub async fn approve_builder_fee(
205 private_key: &str,
206 is_testnet: bool,
207) -> Result<BuilderFeeApprovalResult> {
208 let pk = EvmPrivateKey::new(private_key.to_string())?;
209 let wallet_address = derive_address(&pk)?;
210
211 let nonce = SystemTime::now()
212 .duration_since(SystemTime::UNIX_EPOCH)
213 .map_err(|e| crate::http::error::Error::transport(format!("Time error: {e}")))?
214 .as_millis() as u64;
215
216 let signature =
217 sign_approve_builder_fee(&pk, is_testnet, nonce, BUILDER_CODES_APPROVAL_FEE_RATE)?;
218
219 let action = serde_json::json!({
220 "type": "approveBuilderFee",
221 "hyperliquidChain": if is_testnet { "Testnet" } else { "Mainnet" },
222 "signatureChainId": "0x66eee",
223 "maxFeeRate": BUILDER_CODES_APPROVAL_FEE_RATE,
224 "builder": NAUTILUS_BUILDER_FEE_ADDRESS,
225 "nonce": nonce,
226 });
227
228 let payload = serde_json::json!({
229 "action": action,
230 "nonce": nonce,
231 "signature": signature,
232 });
233
234 let url = exchange_url(is_testnet);
235 let client =
236 HttpClient::new(HashMap::new(), vec![], vec![], None, None, None).map_err(|e| {
237 crate::http::error::Error::transport(format!("Failed to create client: {e}"))
238 })?;
239
240 let body_bytes = serde_json::to_vec(&payload)
241 .map_err(|e| crate::http::error::Error::transport(format!("Failed to serialize: {e}")))?;
242
243 let headers = HashMap::from([("Content-Type".to_string(), "application/json".to_string())]);
244 let response = client
245 .request(
246 Method::POST,
247 url.to_string(),
248 None,
249 Some(headers),
250 Some(body_bytes),
251 None,
252 None,
253 )
254 .await
255 .map_err(|e| crate::http::error::Error::transport(format!("HTTP request failed: {e}")))?;
256
257 if !response.status.is_success() {
258 let body_str = String::from_utf8_lossy(&response.body);
259 return Err(crate::http::error::Error::transport(format!(
260 "HTTP {} from {url}: {}",
261 response.status.as_u16(),
262 if body_str.is_empty() {
263 "(empty response)"
264 } else {
265 &body_str
266 }
267 )));
268 }
269
270 let response_json: serde_json::Value = serde_json::from_slice(&response.body).map_err(|e| {
271 let body_str = String::from_utf8_lossy(&response.body);
272 crate::http::error::Error::transport(format!(
273 "Failed to parse JSON response from {url}: {e}. Body: {}",
274 if body_str.is_empty() {
275 "(empty)"
276 } else if body_str.len() > 200 {
277 &body_str[..200]
278 } else {
279 &body_str
280 }
281 ))
282 })?;
283
284 let status = response_json
285 .get("status")
286 .and_then(|v| v.as_str())
287 .unwrap_or("unknown")
288 .to_string();
289
290 let success = status == RESPONSE_STATUS_OK;
291 let message = response_json.get("response").map(|v: &serde_json::Value| {
292 if v.is_string() {
293 v.as_str().unwrap().to_string()
294 } else {
295 v.to_string()
296 }
297 });
298
299 Ok(BuilderFeeApprovalResult {
300 success,
301 status,
302 message,
303 wallet_address,
304 builder_address: NAUTILUS_BUILDER_FEE_ADDRESS.to_string(),
305 is_testnet,
306 })
307}
308
309pub async fn approve_from_env(non_interactive: bool) -> bool {
327 let is_testnet = env::var("HYPERLIQUID_TESTNET").is_ok_and(|v| v.to_lowercase() == "true");
328
329 let env_var = if is_testnet {
330 "HYPERLIQUID_TESTNET_PK"
331 } else {
332 "HYPERLIQUID_PK"
333 };
334
335 let private_key = match env::var(env_var) {
336 Ok(pk) => pk,
337 Err(_) => {
338 println!("Error: {env_var} environment variable not set");
339 return false;
340 }
341 };
342
343 let info = BuilderFeeInfo::new();
344 let network = if is_testnet { "testnet" } else { "mainnet" };
345
346 println!("Approving Nautilus builder fee on {network}");
347 println!("Builder address: {}", info.address);
348 println!(
349 "Approval rate: {} (covers perpetual taker and maker fills)",
350 info.approval_rate
351 );
352 println!(" - Taker: 1 bp (0.01%) on perpetual fills");
353 println!(" - Maker: 0.5 bp (0.005%) on perpetual post-only fills");
354 println!(" - Spot: no builder fee");
355 println!();
356 println!("This is at the low end of ecosystem norms.");
357 println!("Hyperliquid allows up to 0.1% (10 bps) for perps and 1% (100 bps) for spot.");
358 println!();
359
360 if !non_interactive && !wait_for_confirmation("Press Enter to approve or Ctrl+C to cancel... ")
361 {
362 return false;
363 }
364
365 println!("Approving builder fee...");
366
367 match approve_builder_fee(&private_key, is_testnet).await {
368 Ok(result) => {
369 println!();
370 println!("Wallet address: {}", result.wallet_address);
371 println!("Status: {}", result.status);
372 if let Some(msg) = &result.message {
373 println!("Response: {msg}");
374 }
375 println!();
376
377 if result.success {
378 println!("Builder fee approved successfully!");
379 println!("You can now trade on Hyperliquid via NautilusTrader.");
380 println!();
381 println!("To verify approval status at any time, run:");
382 println!(
383 " python nautilus_trader/adapters/hyperliquid/scripts/builder_fee_verify.py"
384 );
385 } else {
386 println!("Approval may have failed. Check the response above.");
387 }
388
389 result.success
390 }
391 Err(e) => {
392 println!("Error: {e}");
393 false
394 }
395 }
396}
397
398const REVOKE_FEE_RATE: &str = "0%";
400
401#[allow(clippy::missing_panics_doc)]
417pub async fn revoke_builder_fee(
418 private_key: &str,
419 is_testnet: bool,
420) -> Result<BuilderFeeApprovalResult> {
421 let pk = EvmPrivateKey::new(private_key.to_string())?;
422 let wallet_address = derive_address(&pk)?;
423
424 let nonce = SystemTime::now()
425 .duration_since(SystemTime::UNIX_EPOCH)
426 .map_err(|e| crate::http::error::Error::transport(format!("Time error: {e}")))?
427 .as_millis() as u64;
428
429 let signature = sign_approve_builder_fee(&pk, is_testnet, nonce, REVOKE_FEE_RATE)?;
430
431 let action = serde_json::json!({
432 "type": "approveBuilderFee",
433 "hyperliquidChain": if is_testnet { "Testnet" } else { "Mainnet" },
434 "signatureChainId": "0x66eee",
435 "maxFeeRate": REVOKE_FEE_RATE,
436 "builder": NAUTILUS_BUILDER_FEE_ADDRESS,
437 "nonce": nonce,
438 });
439
440 let payload = serde_json::json!({
441 "action": action,
442 "nonce": nonce,
443 "signature": signature,
444 });
445
446 let url = exchange_url(is_testnet);
447 let client =
448 HttpClient::new(HashMap::new(), vec![], vec![], None, None, None).map_err(|e| {
449 crate::http::error::Error::transport(format!("Failed to create client: {e}"))
450 })?;
451
452 let body_bytes = serde_json::to_vec(&payload)
453 .map_err(|e| crate::http::error::Error::transport(format!("Failed to serialize: {e}")))?;
454
455 let headers = HashMap::from([("Content-Type".to_string(), "application/json".to_string())]);
456 let response = client
457 .request(
458 Method::POST,
459 url.to_string(),
460 None,
461 Some(headers),
462 Some(body_bytes),
463 None,
464 None,
465 )
466 .await
467 .map_err(|e| crate::http::error::Error::transport(format!("HTTP request failed: {e}")))?;
468
469 if !response.status.is_success() {
470 let body_str = String::from_utf8_lossy(&response.body);
471 return Err(crate::http::error::Error::transport(format!(
472 "HTTP {} from {url}: {}",
473 response.status.as_u16(),
474 if body_str.is_empty() {
475 "(empty response)"
476 } else {
477 &body_str
478 }
479 )));
480 }
481
482 let response_json: serde_json::Value = serde_json::from_slice(&response.body).map_err(|e| {
483 let body_str = String::from_utf8_lossy(&response.body);
484 crate::http::error::Error::transport(format!(
485 "Failed to parse JSON response from {url}: {e}. Body: {}",
486 if body_str.is_empty() {
487 "(empty)"
488 } else if body_str.len() > 200 {
489 &body_str[..200]
490 } else {
491 &body_str
492 }
493 ))
494 })?;
495
496 let status = response_json
497 .get("status")
498 .and_then(|v| v.as_str())
499 .unwrap_or("unknown")
500 .to_string();
501
502 let success = status == RESPONSE_STATUS_OK;
503 let message = response_json.get("response").map(|v: &serde_json::Value| {
504 if v.is_string() {
505 v.as_str().unwrap().to_string()
506 } else {
507 v.to_string()
508 }
509 });
510
511 Ok(BuilderFeeApprovalResult {
512 success,
513 status,
514 message,
515 wallet_address,
516 builder_address: NAUTILUS_BUILDER_FEE_ADDRESS.to_string(),
517 is_testnet,
518 })
519}
520
521pub async fn revoke_from_env(non_interactive: bool) -> bool {
539 let is_testnet = env::var("HYPERLIQUID_TESTNET").is_ok_and(|v| v.to_lowercase() == "true");
540
541 let env_var = if is_testnet {
542 "HYPERLIQUID_TESTNET_PK"
543 } else {
544 "HYPERLIQUID_PK"
545 };
546
547 let private_key = match env::var(env_var) {
548 Ok(pk) => pk,
549 Err(_) => {
550 println!("Error: {env_var} environment variable not set");
551 return false;
552 }
553 };
554
555 let network = if is_testnet { "testnet" } else { "mainnet" };
556
557 println!("Revoking Nautilus builder fee on {network}");
558 println!("Builder address: {NAUTILUS_BUILDER_FEE_ADDRESS}");
559 println!();
560 println!("WARNING: After revoking, you will not be able to trade on");
561 println!("Hyperliquid via NautilusTrader until you re-approve.");
562 println!();
563
564 if !non_interactive && !wait_for_confirmation("Press Enter to revoke or Ctrl+C to cancel... ") {
565 return false;
566 }
567
568 println!("Revoking builder fee...");
569
570 match revoke_builder_fee(&private_key, is_testnet).await {
571 Ok(result) => {
572 println!();
573 println!("Wallet address: {}", result.wallet_address);
574 println!("Status: {}", result.status);
575 if let Some(msg) = &result.message {
576 println!("Response: {msg}");
577 }
578 println!();
579
580 if result.success {
581 println!("Builder fee revoked successfully.");
582 println!("You will need to re-approve to trade via NautilusTrader.");
583 } else {
584 println!("Revocation may have failed. Check the response above.");
585 }
586
587 result.success
588 }
589 Err(e) => {
590 println!("Error: {e}");
591 false
592 }
593 }
594}
595
596#[derive(Debug, Clone, Serialize, Deserialize)]
598pub struct BuilderFeeVerifyResult {
599 pub wallet_address: String,
601 pub builder_address: String,
603 pub approved_rate: Option<String>,
605 pub required_rate: String,
607 pub is_approved: bool,
609 pub is_testnet: bool,
611}
612
613pub async fn verify_builder_fee(
627 wallet_address: &str,
628 is_testnet: bool,
629) -> Result<BuilderFeeVerifyResult> {
630 let url = info_url(is_testnet);
631 let client =
632 HttpClient::new(HashMap::new(), vec![], vec![], None, None, None).map_err(|e| {
633 crate::http::error::Error::transport(format!("Failed to create client: {e}"))
634 })?;
635
636 let payload = serde_json::json!({
637 "type": "maxBuilderFee",
638 "user": wallet_address,
639 "builder": NAUTILUS_BUILDER_FEE_ADDRESS,
640 });
641
642 let body_bytes = serde_json::to_vec(&payload)
643 .map_err(|e| crate::http::error::Error::transport(format!("Failed to serialize: {e}")))?;
644
645 let headers = HashMap::from([("Content-Type".to_string(), "application/json".to_string())]);
646 let response = client
647 .request(
648 Method::POST,
649 url.to_string(),
650 None,
651 Some(headers),
652 Some(body_bytes),
653 None,
654 None,
655 )
656 .await
657 .map_err(|e| crate::http::error::Error::transport(format!("HTTP request failed: {e}")))?;
658
659 if !response.status.is_success() {
660 let body_str = String::from_utf8_lossy(&response.body);
661 return Err(crate::http::error::Error::transport(format!(
662 "HTTP {} from {url}: {}",
663 response.status.as_u16(),
664 if body_str.is_empty() {
665 "(empty response)"
666 } else {
667 &body_str
668 }
669 )));
670 }
671
672 let response_text = String::from_utf8_lossy(&response.body).trim().to_string();
674 let approved_tenths_bp: Option<u32> = if response_text == "null" {
675 None
676 } else {
677 response_text.parse().ok()
678 };
679
680 let approved_rate = approved_tenths_bp.map(|tenths| {
681 let bps = tenths as f64 / 10.0;
682 let percent = bps / 100.0;
683 format!("{percent}%")
684 });
685 let is_approved = approved_tenths_bp.is_some_and(|tenths| tenths >= 10);
686
687 Ok(BuilderFeeVerifyResult {
688 wallet_address: wallet_address.to_string(),
689 builder_address: NAUTILUS_BUILDER_FEE_ADDRESS.to_string(),
690 approved_rate,
691 required_rate: BUILDER_CODES_APPROVAL_FEE_RATE.to_string(),
692 is_approved,
693 is_testnet,
694 })
695}
696
697pub async fn verify_from_env_or_address(wallet_address: Option<String>) -> bool {
712 let is_testnet = env::var("HYPERLIQUID_TESTNET").is_ok_and(|v| v.to_lowercase() == "true");
713
714 let wallet_address = match wallet_address {
715 Some(addr) => addr,
716 None => {
717 let env_var = if is_testnet {
719 "HYPERLIQUID_TESTNET_PK"
720 } else {
721 "HYPERLIQUID_PK"
722 };
723
724 let private_key = match env::var(env_var) {
725 Ok(pk) => pk,
726 Err(_) => {
727 println!("Error: No wallet address provided and {env_var} not set");
728 return false;
729 }
730 };
731
732 let pk = match EvmPrivateKey::new(private_key) {
733 Ok(pk) => pk,
734 Err(e) => {
735 println!("Error: Invalid private key: {e}");
736 return false;
737 }
738 };
739
740 match derive_address(&pk) {
741 Ok(addr) => addr,
742 Err(e) => {
743 println!("Error: Failed to derive address: {e}");
744 return false;
745 }
746 }
747 }
748 };
749
750 let network = if is_testnet { "testnet" } else { "mainnet" };
751 let separator = "=".repeat(60);
752
753 println!("{separator}");
754 println!("Hyperliquid Builder Fee Verification");
755 println!("{separator}");
756 println!();
757 println!("Checking approval status on {network}...");
758 println!();
759
760 match verify_builder_fee(&wallet_address, is_testnet).await {
761 Ok(result) => {
762 println!("Wallet: {}", result.wallet_address);
763 println!("Builder: {}", result.builder_address);
764 println!("Network: {network}");
765 println!(
766 "Approved: {}",
767 result.approved_rate.as_deref().unwrap_or("(none)")
768 );
769 println!();
770
771 if result.is_approved {
772 println!("Status: APPROVED");
773 println!();
774 println!("NautilusTrader builder fee rates (perpetuals only, no fee on spot):");
775 println!(" - Taker: 1 bp (0.01%) per fill");
776 println!(" - Maker: 0.5 bp (0.005%) per fill");
777 println!();
778 println!("This is at the low end of ecosystem norms.");
779 println!(
780 "(Hyperliquid allows up to 0.1% (10 bps) for perps and 1% (100 bps) for spot)"
781 );
782 println!();
783 println!("You can trade on Hyperliquid via NautilusTrader.");
784 } else {
785 println!("Status: NOT APPROVED");
786 println!();
787 println!("Run the approval script:");
788 println!(
789 " python nautilus_trader/adapters/hyperliquid/scripts/builder_fee_approve.py"
790 );
791 println!();
792 println!("See: docs/integrations/hyperliquid.md#approving-builder-fees");
793 }
794
795 println!("{separator}");
796 result.is_approved
797 }
798 Err(e) => {
799 println!("Error: {e}");
800 false
801 }
802 }
803}
804
805fn sign_approve_builder_fee(
806 pk: &EvmPrivateKey,
807 is_testnet: bool,
808 nonce: u64,
809 fee_rate: &str,
810) -> Result<serde_json::Value> {
811 let domain_hash = compute_domain_hash();
813
814 let type_hash = keccak256(
816 b"HyperliquidTransaction:ApproveBuilderFee(string hyperliquidChain,string maxFeeRate,address builder,uint64 nonce)",
817 );
818
819 let chain_str = if is_testnet { "Testnet" } else { "Mainnet" };
821 let chain_hash = keccak256(chain_str.as_bytes());
822 let fee_rate_hash = keccak256(fee_rate.as_bytes());
823
824 let builder_addr = Address::from_str(NAUTILUS_BUILDER_FEE_ADDRESS).map_err(|e| {
826 crate::http::error::Error::transport(format!("Invalid builder address: {e}"))
827 })?;
828
829 let mut struct_data = Vec::with_capacity(32 * 5);
831 struct_data.extend_from_slice(type_hash.as_slice());
832 struct_data.extend_from_slice(chain_hash.as_slice());
833 struct_data.extend_from_slice(fee_rate_hash.as_slice());
834
835 let mut addr_bytes = [0u8; 32];
837 addr_bytes[12..].copy_from_slice(builder_addr.as_slice());
838 struct_data.extend_from_slice(&addr_bytes);
839
840 let mut nonce_bytes = [0u8; 32];
842 nonce_bytes[24..].copy_from_slice(&nonce.to_be_bytes());
843 struct_data.extend_from_slice(&nonce_bytes);
844
845 let struct_hash = keccak256(&struct_data);
846
847 let mut final_data = Vec::with_capacity(66);
849 final_data.extend_from_slice(b"\x19\x01");
850 final_data.extend_from_slice(&domain_hash);
851 final_data.extend_from_slice(struct_hash.as_slice());
852
853 let signing_hash = keccak256(&final_data);
854
855 let key_hex = pk.as_hex();
857 let key_hex = key_hex.strip_prefix("0x").unwrap_or(key_hex);
858
859 let signer = PrivateKeySigner::from_str(key_hex).map_err(|e| {
860 crate::http::error::Error::transport(format!("Failed to create signer: {e}"))
861 })?;
862
863 let hash_b256 = B256::from(signing_hash);
864 let signature = signer
865 .sign_hash_sync(&hash_b256)
866 .map_err(|e| crate::http::error::Error::transport(format!("Failed to sign: {e}")))?;
867
868 let r = format!("0x{:064x}", signature.r());
870 let s = format!("0x{:064x}", signature.s());
871 let v = if signature.v() { 28u8 } else { 27u8 };
872
873 Ok(serde_json::json!({
874 "r": r,
875 "s": s,
876 "v": v,
877 }))
878}
879
880fn get_eip712_domain() -> Eip712Domain {
881 Eip712Domain {
882 name: Some("HyperliquidSignTransaction".into()),
883 version: Some("1".into()),
884 chain_id: Some(alloy_primitives::U256::from(HYPERLIQUID_CHAIN_ID)),
885 verifying_contract: Some(Address::ZERO),
886 salt: None,
887 }
888}
889
890fn compute_domain_hash() -> [u8; 32] {
891 *get_eip712_domain().hash_struct()
892}
893
894fn derive_address(pk: &EvmPrivateKey) -> Result<String> {
895 let key_hex = pk.as_hex();
896 let key_hex = key_hex.strip_prefix("0x").unwrap_or(key_hex);
897
898 let signer = PrivateKeySigner::from_str(key_hex).map_err(|e| {
899 crate::http::error::Error::transport(format!("Failed to create signer: {e}"))
900 })?;
901
902 Ok(format!("{:#x}", signer.address()))
903}
904
905fn wait_for_confirmation(prompt: &str) -> bool {
906 let cancelled = Arc::new(AtomicBool::new(false));
907 let cancelled_clone = cancelled.clone();
908
909 if ctrlc::set_handler(move || {
910 cancelled_clone.store(true, Ordering::SeqCst);
911 })
912 .is_err()
913 {
914 }
916
917 print!("{prompt}");
918 io::stdout().flush().ok();
919
920 let (tx, rx) = mpsc::channel();
922 thread::spawn(move || {
923 let mut input = String::new();
924 let result = io::stdin().read_line(&mut input);
925 let _ = tx.send(result);
926 });
927
928 loop {
930 if cancelled.load(Ordering::SeqCst) {
931 println!();
932 println!("Aborted.");
933 return false;
934 }
935
936 match rx.recv_timeout(Duration::from_millis(100)) {
937 Ok(Ok(0) | Err(_)) => {
938 println!();
939 println!("Aborted.");
940 return false;
941 }
942 Ok(Ok(_)) => {
943 println!();
944 return true;
945 }
946 Err(mpsc::RecvTimeoutError::Timeout) => continue,
947 Err(mpsc::RecvTimeoutError::Disconnected) => {
948 println!();
949 println!("Aborted.");
950 return false;
951 }
952 }
953 }
954}
955
956#[cfg(test)]
957mod tests {
958 use rstest::rstest;
959
960 use super::*;
961
962 #[rstest]
963 fn test_builder_fee_info() {
964 let info = BuilderFeeInfo::new();
965 assert_eq!(info.address, NAUTILUS_BUILDER_FEE_ADDRESS);
966 assert_eq!(info.perp_taker_tenths_bp, 10);
967 assert_eq!(info.perp_maker_tenths_bp, 5);
968 assert_eq!(info.approval_rate, "0.01%");
969 }
970
971 #[rstest]
972 fn test_derive_address() {
973 let pk = EvmPrivateKey::new(
974 "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef".to_string(),
975 )
976 .unwrap();
977 let addr = derive_address(&pk).unwrap();
978 assert!(addr.starts_with("0x"));
979 assert_eq!(addr.len(), 42);
980 }
981
982 #[rstest]
983 fn test_compute_domain_hash() {
984 let hash = compute_domain_hash();
985 assert_eq!(hash.len(), 32);
986 }
987
988 #[rstest]
989 fn test_sign_approve_builder_fee() {
990 let pk = EvmPrivateKey::new(
991 "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef".to_string(),
992 )
993 .unwrap();
994 let nonce = 1640995200000u64;
995
996 let signature =
997 sign_approve_builder_fee(&pk, false, nonce, BUILDER_CODES_APPROVAL_FEE_RATE).unwrap();
998
999 assert!(signature.get("r").is_some());
1000 assert!(signature.get("s").is_some());
1001 assert!(signature.get("v").is_some());
1002
1003 let r = signature["r"].as_str().unwrap();
1004 let s = signature["s"].as_str().unwrap();
1005
1006 assert!(r.starts_with("0x"));
1007 assert!(s.starts_with("0x"));
1008 assert_eq!(r.len(), 66); assert_eq!(s.len(), 66);
1010 }
1011}