1use std::{any::Any, fmt::Debug};
17
18use nautilus_common::factories::ClientConfig;
19use nautilus_core::string::secret::{REDACTED, SecretString};
20use nautilus_infrastructure::sql::pg::PostgresConnectOptions;
21use nautilus_model::{
22 defi::{Chain, DexType, SharedChain},
23 identifiers::AccountId,
24};
25use nautilus_network::websocket::TransportBackend;
26use serde::{Deserialize, Serialize};
27
28#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
30#[serde(default, deny_unknown_fields)]
31#[cfg_attr(
32 feature = "python",
33 pyo3::pyclass(module = "nautilus_trader.adapters.blockchain", from_py_object)
34)]
35#[cfg_attr(
36 feature = "python",
37 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.blockchain")
38)]
39pub struct DexPoolFilters {
40 #[builder(default = true)]
42 pub remove_pools_with_empty_erc20fields: bool,
43}
44
45impl Default for DexPoolFilters {
46 fn default() -> Self {
47 Self::builder().build()
48 }
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
53#[serde(deny_unknown_fields)]
54#[cfg_attr(
55 feature = "python",
56 pyo3::pyclass(module = "nautilus_trader.adapters.blockchain", from_py_object)
57)]
58#[cfg_attr(
59 feature = "python",
60 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.blockchain")
61)]
62pub struct BlockchainDataClientConfig {
63 pub chain: SharedChain,
65 #[builder(default)]
67 #[serde(default)]
68 pub dex_ids: Vec<DexType>,
69 #[builder(default)]
71 #[serde(default)]
72 pub use_hypersync_for_live_data: bool,
73 pub http_rpc_url: SecretString,
75 pub wss_rpc_url: Option<SecretString>,
77 pub proxy_url: Option<SecretString>,
79 pub rpc_requests_per_second: Option<u32>,
81 #[builder(default = 200)]
83 #[serde(default = "default_multicall_calls_per_rpc_request")]
84 pub multicall_calls_per_rpc_request: u32,
85 pub from_block: Option<u64>,
87 #[builder(default)]
89 #[serde(default)]
90 pub pool_filters: DexPoolFilters,
91 pub postgres_cache_database_config: Option<PostgresConnectOptions>,
93 #[builder(default)]
95 #[serde(default)]
96 pub transport_backend: TransportBackend,
97}
98
99#[cfg(feature = "python")]
100nautilus_core::impl_pyo3_config_getters!(BlockchainDataClientConfig {
101 dex_ids: Vec<DexType>,
102 multicall_calls_per_rpc_request: u32,
103 pool_filters: DexPoolFilters,
104 transport_backend: TransportBackend,
105});
106
107const fn default_multicall_calls_per_rpc_request() -> u32 {
108 200
109}
110
111#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
113#[serde(deny_unknown_fields)]
114#[cfg_attr(
115 feature = "python",
116 pyo3::pyclass(module = "nautilus_trader.adapters.blockchain", from_py_object)
117)]
118#[cfg_attr(
119 feature = "python",
120 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.blockchain")
121)]
122pub struct BlockchainProviderIdentity {
123 pub provider_id: String,
125 pub operator_id: String,
127 pub failure_domain_ids: Vec<String>,
129}
130
131#[cfg(feature = "python")]
132nautilus_core::impl_pyo3_config_getters!(BlockchainProviderIdentity {
133 failure_domain_ids: Vec<String>,
134 operator_id: String,
135 provider_id: String,
136});
137
138#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
140#[serde(deny_unknown_fields)]
141#[cfg_attr(
142 feature = "python",
143 pyo3::pyclass(module = "nautilus_trader.adapters.blockchain", from_py_object)
144)]
145#[cfg_attr(
146 feature = "python",
147 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.blockchain")
148)]
149pub struct BlockchainVerificationProviderConfig {
150 pub identity: BlockchainProviderIdentity,
152 pub http_rpc_url: SecretString,
154}
155
156#[cfg(feature = "python")]
157nautilus_core::impl_pyo3_config_getters!(BlockchainVerificationProviderConfig {
158 identity: BlockchainProviderIdentity,
159});
160
161#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
163#[serde(deny_unknown_fields)]
164#[cfg_attr(
165 feature = "python",
166 pyo3::pyclass(module = "nautilus_trader.adapters.blockchain", from_py_object)
167)]
168#[cfg_attr(
169 feature = "python",
170 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.blockchain")
171)]
172pub struct BlockchainChainAnchorConfig {
173 pub chain_id: u32,
175 pub chain_name: String,
177 pub checkpoint_height: u64,
179 pub checkpoint_hash: String,
181 pub checkpoint_timestamp: u64,
183 pub max_head_skew_blocks: u64,
185 pub max_head_age_secs: u64,
187 pub max_future_drift_secs: u64,
189}
190
191#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
193#[serde(rename_all = "snake_case")]
194pub enum BlockchainContractRole {
195 Router,
196 Factory,
197 WrappedNative,
198 Quote,
199 Token,
200 Pool,
201 Implementation,
202}
203
204#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
206#[serde(deny_unknown_fields)]
207pub struct BlockchainContractProbe {
208 pub call_data: String,
210 pub expected_output: String,
212}
213
214#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
216#[serde(deny_unknown_fields)]
217pub struct BlockchainProxyManifest {
218 pub kind: String,
220 pub storage_slot: String,
222 pub storage_value: String,
224 pub target_address: String,
226 pub target_code_hash: String,
228}
229
230#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
232#[serde(deny_unknown_fields)]
233pub struct BlockchainContractManifest {
234 pub address: String,
236 pub role: BlockchainContractRole,
238 pub runtime_code_hash: String,
240 pub proxy: Option<BlockchainProxyManifest>,
242 #[serde(default)]
244 pub probes: Vec<BlockchainContractProbe>,
245}
246
247#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
249#[serde(deny_unknown_fields)]
250pub struct BlockchainTokenManifest {
251 pub address: String,
252 pub name: String,
253 pub symbol: String,
254 pub decimals: u8,
255 pub asset_role: String,
257}
258
259#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
261#[serde(deny_unknown_fields)]
262pub struct BlockchainPoolManifest {
263 pub address: String,
264 pub token0: String,
265 pub token1: String,
266 pub fee: u32,
267 pub factory: String,
268 pub quote_contract: String,
269}
270
271#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
273#[serde(deny_unknown_fields)]
274pub struct BlockchainCallEdgeManifest {
275 pub purpose: String,
277 pub caller: String,
278 pub target: String,
279 pub call_type: String,
281}
282
283#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
285#[serde(deny_unknown_fields)]
286pub struct BlockchainDeploymentManifest {
287 pub version: String,
288 pub chain_id: u32,
289 pub chain_name: String,
290 pub contracts: Vec<BlockchainContractManifest>,
291 pub tokens: Vec<BlockchainTokenManifest>,
292 pub pools: Vec<BlockchainPoolManifest>,
293 pub call_edges: Vec<BlockchainCallEdgeManifest>,
294}
295
296#[cfg(feature = "python")]
297nautilus_core::impl_pyo3_config_getters!(BlockchainChainAnchorConfig {
298 chain_id: u32,
299 chain_name: String,
300 checkpoint_hash: String,
301 checkpoint_height: u64,
302 checkpoint_timestamp: u64,
303 max_future_drift_secs: u64,
304 max_head_age_secs: u64,
305 max_head_skew_blocks: u64,
306});
307
308#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
310#[serde(deny_unknown_fields)]
311#[cfg_attr(
312 feature = "python",
313 pyo3::pyclass(module = "nautilus_trader.adapters.blockchain", from_py_object)
314)]
315#[cfg_attr(
316 feature = "python",
317 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.blockchain")
318)]
319pub struct BlockchainVerificationConfig {
320 pub authoritative: BlockchainProviderIdentity,
322 pub verifiers: Vec<BlockchainVerificationProviderConfig>,
324 pub chain_anchor: BlockchainChainAnchorConfig,
326 pub manifest_version: String,
328 pub manifest_digest: String,
330 pub deployment_manifest: BlockchainDeploymentManifest,
332}
333
334impl Debug for BlockchainVerificationConfig {
335 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
336 f.debug_struct(stringify!(BlockchainVerificationConfig))
337 .field("authoritative", &self.authoritative)
338 .field("verifiers", &self.verifiers)
339 .field("chain_anchor", &self.chain_anchor)
340 .field("manifest_version", &self.manifest_version)
341 .field("manifest_digest", &self.manifest_digest)
342 .field("deployment_manifest", &REDACTED)
343 .finish()
344 }
345}
346
347#[cfg(feature = "python")]
348nautilus_core::impl_pyo3_config_getters!(BlockchainVerificationConfig {
349 authoritative: BlockchainProviderIdentity,
350 chain_anchor: BlockchainChainAnchorConfig,
351 manifest_digest: String,
352 manifest_version: String,
353 verifiers: Vec<BlockchainVerificationProviderConfig>,
354});
355
356#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
358#[serde(deny_unknown_fields)]
359#[cfg_attr(
360 feature = "python",
361 pyo3::pyclass(module = "nautilus_trader.adapters.blockchain", from_py_object)
362)]
363#[cfg_attr(
364 feature = "python",
365 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.blockchain")
366)]
367pub struct QuoteSpendLimit {
368 pub token_in: String,
370 pub token_out: String,
372 pub spend_token: String,
374 pub spend_token_decimals: u8,
376 pub max_amount: String,
378}
379
380#[cfg(feature = "python")]
381nautilus_core::impl_pyo3_config_getters!(QuoteSpendLimit {
382 max_amount: String,
383 spend_token: String,
384 spend_token_decimals: u8,
385 token_in: String,
386 token_out: String,
387});
388
389#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
390#[serde(deny_unknown_fields)]
391#[cfg_attr(
392 feature = "python",
393 pyo3::pyclass(module = "nautilus_trader.adapters.blockchain", from_py_object)
394)]
395#[cfg_attr(
396 feature = "python",
397 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.blockchain")
398)]
399pub struct BlockchainExecutionClientConfig {
400 pub client_id: AccountId,
402 pub chain: Chain,
404 pub wallet_address: String,
406 pub tokens: Option<Vec<String>>,
408 pub http_rpc_url: SecretString,
410 #[serde(default)]
412 pub verification: Option<BlockchainVerificationConfig>,
413 pub rpc_requests_per_second: Option<u32>,
415 pub signer_private_key_env: String,
417 #[serde(default)]
419 pub payload_key_env: Option<String>,
420 #[builder(default)]
422 #[serde(default)]
423 pub payload_key_retired_env: Vec<String>,
424 #[serde(default)]
426 pub payload_deployment_id: Option<String>,
427 pub router_addresses: Vec<String>,
429 pub weth_address: String,
431 #[builder(default)]
433 #[serde(default)]
434 pub unlimited_approval: bool,
435 pub max_fee_per_gas_wei: u64,
438 pub base_fee_buffer_bps: u32,
440 pub gas_limit: u64,
443 pub gas_buffer_bps: u32,
445 pub allowed_token_pairs: Option<Vec<(String, String)>>,
447 pub quote_spend_limits: Option<Vec<QuoteSpendLimit>>,
449 pub slippage_bps: Option<u32>,
451 pub max_slippage_bps: Option<u32>,
453 pub max_order_amount: Option<u64>,
455 pub deadline_seconds: Option<u64>,
457 pub max_quote_age_blocks: Option<u64>,
459 pub receipt_timeout_secs: Option<u64>,
461 pub postgres_cache_database_config: Option<PostgresConnectOptions>,
464 #[builder(default)]
466 #[serde(default)]
467 pub transport_backend: TransportBackend,
468}
469
470impl ClientConfig for BlockchainExecutionClientConfig {
471 fn as_any(&self) -> &dyn Any {
472 self
473 }
474}
475
476#[cfg(feature = "python")]
477nautilus_core::impl_pyo3_config_getters!(BlockchainExecutionClientConfig {
478 base_fee_buffer_bps: u32,
479 deadline_seconds: Option<u64>,
480 gas_buffer_bps: u32,
481 gas_limit: u64,
482 verification: Option<BlockchainVerificationConfig>,
483 max_fee_per_gas_wei: u64,
484 max_order_amount: Option<u64>,
485 max_quote_age_blocks: Option<u64>,
486 max_slippage_bps: Option<u32>,
487 quote_spend_limits: Option<Vec<QuoteSpendLimit>>,
488 receipt_timeout_secs: Option<u64>,
489 router_addresses: Vec<String>,
490 payload_deployment_id: Option<String>,
491 payload_key_env: Option<String>,
492 payload_key_retired_env: Vec<String>,
493 signer_private_key_env: String,
494 slippage_bps: Option<u32>,
495 tokens: Option<Vec<String>>,
496 transport_backend: TransportBackend,
497 unlimited_approval: bool,
498 wallet_address: String,
499 weth_address: String,
500});
501
502#[cfg(test)]
503mod tests {
504 use std::sync::Arc;
505
506 use nautilus_model::defi::chain::chains;
507 use rstest::rstest;
508
509 use super::*;
510
511 #[rstest]
512 fn test_data_config_toml_minimal() {
513 let config: BlockchainDataClientConfig = toml::from_str(
514 r#"
515http_rpc_url = "https://eth-mainnet.example.com"
516
517[chain]
518name = "Ethereum"
519chain_id = 1
520hypersync_url = "https://1.hypersync.xyz"
521native_currency_decimals = 18
522"#,
523 )
524 .unwrap();
525
526 assert_eq!(
527 config.http_rpc_url.expose_secret(),
528 "https://eth-mainnet.example.com"
529 );
530 assert_eq!(config.chain.chain_id, 1);
531 assert!(config.dex_ids.is_empty());
532 assert!(!config.use_hypersync_for_live_data);
533 assert_eq!(config.multicall_calls_per_rpc_request, 200);
534 assert!(config.pool_filters.remove_pools_with_empty_erc20fields);
535 assert_eq!(config.transport_backend, TransportBackend::default());
536 }
537
538 #[rstest]
539 fn test_execution_config_toml_minimal() {
540 let config: BlockchainExecutionClientConfig = toml::from_str(
541 r#"
542client_id = "BLOCKCHAIN-001"
543wallet_address = "0x0000000000000000000000000000000000000000"
544http_rpc_url = "https://eth-mainnet.example.com"
545signer_private_key_env = "BLOCKCHAIN_PRIVATE_KEY"
546payload_key_env = "BLOCKCHAIN_PAYLOAD_KEY"
547payload_key_retired_env = ["BLOCKCHAIN_PAYLOAD_KEY_OLD"]
548payload_deployment_id = "primary-execution"
549router_addresses = ["0xE592427A0AEce92De3Edee1F18E0157C05861564"]
550weth_address = "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"
551max_fee_per_gas_wei = 1000000000
552base_fee_buffer_bps = 2000
553gas_limit = 1000000
554gas_buffer_bps = 2000
555allowed_token_pairs = [
556 ["0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", "0xaf88d065e77c8cC2239327C5EDb3A432268e5831"],
557 ["0xaf88d065e77c8cC2239327C5EDb3A432268e5831", "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"],
558]
559slippage_bps = 50
560max_slippage_bps = 200
561max_order_amount = 1000000000000000000
562deadline_seconds = 300
563max_quote_age_blocks = 100
564receipt_timeout_secs = 60
565
566[[quote_spend_limits]]
567token_in = "0xaf88d065e77c8cC2239327C5EDb3A432268e5831"
568token_out = "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"
569spend_token = "0xaf88d065e77c8cC2239327C5EDb3A432268e5831"
570spend_token_decimals = 6
571max_amount = "1000000000"
572
573[chain]
574name = "Ethereum"
575chain_id = 1
576hypersync_url = "https://1.hypersync.xyz"
577native_currency_decimals = 18
578"#,
579 )
580 .unwrap();
581
582 assert_eq!(
583 config.http_rpc_url.expose_secret(),
584 "https://eth-mainnet.example.com"
585 );
586 assert_eq!(config.chain.chain_id, 1);
587 assert_eq!(
588 config.wallet_address,
589 "0x0000000000000000000000000000000000000000",
590 );
591 assert!(config.tokens.is_none());
592 assert!(config.rpc_requests_per_second.is_none());
593 assert_eq!(config.signer_private_key_env, "BLOCKCHAIN_PRIVATE_KEY");
594 assert_eq!(
595 config.payload_key_env.as_deref(),
596 Some("BLOCKCHAIN_PAYLOAD_KEY")
597 );
598 assert_eq!(
599 config.payload_key_retired_env,
600 vec!["BLOCKCHAIN_PAYLOAD_KEY_OLD".to_string()]
601 );
602 assert_eq!(
603 config.payload_deployment_id.as_deref(),
604 Some("primary-execution")
605 );
606 assert_eq!(
607 config.router_addresses,
608 vec!["0xE592427A0AEce92De3Edee1F18E0157C05861564".to_string()],
609 );
610 assert_eq!(
611 config.weth_address,
612 "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
613 );
614 assert!(!config.unlimited_approval);
615 assert_eq!(config.max_fee_per_gas_wei, 1_000_000_000);
616 assert_eq!(config.base_fee_buffer_bps, 2_000);
617 assert_eq!(config.gas_limit, 1_000_000);
618 assert_eq!(config.gas_buffer_bps, 2_000);
619 assert_eq!(
620 config.allowed_token_pairs,
621 Some(vec![
622 (
623 "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1".to_string(),
624 "0xaf88d065e77c8cC2239327C5EDb3A432268e5831".to_string(),
625 ),
626 (
627 "0xaf88d065e77c8cC2239327C5EDb3A432268e5831".to_string(),
628 "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1".to_string(),
629 )
630 ]),
631 );
632 assert_eq!(
633 config.quote_spend_limits,
634 Some(vec![QuoteSpendLimit {
635 token_in: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831".to_string(),
636 token_out: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1".to_string(),
637 spend_token: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831".to_string(),
638 spend_token_decimals: 6,
639 max_amount: "1000000000".to_string(),
640 }]),
641 );
642 assert_eq!(config.slippage_bps, Some(50));
643 assert_eq!(config.max_slippage_bps, Some(200));
644 assert_eq!(config.max_order_amount, Some(1_000_000_000_000_000_000));
645 assert_eq!(config.deadline_seconds, Some(300));
646 assert_eq!(config.max_quote_age_blocks, Some(100));
647 assert_eq!(config.receipt_timeout_secs, Some(60));
648 assert!(config.postgres_cache_database_config.is_none());
649 assert_eq!(config.transport_backend, TransportBackend::default());
650 }
651
652 #[rstest]
653 fn test_execution_config_toml_accepts_legacy_shape_without_transaction_limits() {
654 let config: BlockchainExecutionClientConfig = toml::from_str(
655 r#"
656client_id = "BLOCKCHAIN-001"
657wallet_address = "0x0000000000000000000000000000000000000000"
658http_rpc_url = "https://eth-mainnet.example.com"
659signer_private_key_env = "BLOCKCHAIN_PRIVATE_KEY"
660router_addresses = ["0xE592427A0AEce92De3Edee1F18E0157C05861564"]
661weth_address = "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"
662max_fee_per_gas_wei = 1000000000
663base_fee_buffer_bps = 2000
664gas_limit = 1000000
665gas_buffer_bps = 2000
666
667[chain]
668name = "Ethereum"
669chain_id = 1
670hypersync_url = "https://1.hypersync.xyz"
671native_currency_decimals = 18
672"#,
673 )
674 .unwrap();
675
676 assert!(config.allowed_token_pairs.is_none());
677 assert!(config.payload_key_env.is_none());
678 assert!(config.payload_key_retired_env.is_empty());
679 assert!(config.payload_deployment_id.is_none());
680 assert!(config.quote_spend_limits.is_none());
681 assert!(config.slippage_bps.is_none());
682 assert!(config.max_slippage_bps.is_none());
683 assert!(config.max_order_amount.is_none());
684 assert!(config.deadline_seconds.is_none());
685 assert!(config.max_quote_age_blocks.is_none());
686 assert!(config.receipt_timeout_secs.is_none());
687 }
688
689 #[rstest]
690 fn test_execution_config_toml_rejects_unknown_fields() {
691 let result: Result<BlockchainExecutionClientConfig, _> = toml::from_str(
692 r#"
693client_id = "BLOCKCHAIN-001"
694wallet_address = "0x0000000000000000000000000000000000000000"
695http_rpc_url = "https://eth-mainnet.example.com"
696signer_private_key_env = "BLOCKCHAIN_PRIVATE_KEY"
697router_addresses = ["0xE592427A0AEce92De3Edee1F18E0157C05861564"]
698weth_address = "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"
699max_fee_per_gas_wei = 1000000000
700base_fee_buffer_bps = 2000
701gas_limit = 1000000
702gas_buffer_bps = 2000
703allowed_token_pairs = [["0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", "0xaf88d065e77c8cC2239327C5EDb3A432268e5831"]]
704slippage_bps = 50
705max_slippage_bps = 200
706max_order_amount = 1000000000000000000
707deadline_seconds = 300
708max_quote_age_blocks = 100
709receipt_timeout_secs = 60
710unknown_field = 1
711
712[chain]
713name = "Ethereum"
714chain_id = 1
715hypersync_url = "https://1.hypersync.xyz"
716native_currency_decimals = 18
717"#,
718 );
719
720 assert!(result.is_err());
721 }
722
723 #[rstest]
724 fn test_data_config_debug_redacts_rpc_urls() {
725 const HTTP_USERINFO_SECRET: &str = "data-http-userinfo-secret";
726 const WSS_QUERY_SECRET: &str = "data-wss-query-secret";
727 let http_rpc_url = format!(
728 "https://rpc-user:{HTTP_USERINFO_SECRET}@rpc.example.com/data-http-path-secret"
729 );
730 let wss_rpc_url = format!("wss://rpc.example.com/ws?api_key={WSS_QUERY_SECRET}");
731 let config = BlockchainDataClientConfig::builder()
732 .chain(Arc::new(chains::ETHEREUM.clone()))
733 .http_rpc_url(http_rpc_url.clone().into())
734 .wss_rpc_url(wss_rpc_url.clone().into())
735 .build();
736
737 let debug = format!("{config:?}");
738
739 assert!(debug.contains("http_rpc_url: <redacted>"));
740 assert!(debug.contains("wss_rpc_url: Some(<redacted>)"));
741 assert!(!debug.contains(HTTP_USERINFO_SECRET));
742 assert!(!debug.contains(WSS_QUERY_SECRET));
743 assert!(!debug.contains(&http_rpc_url));
744 assert!(!debug.contains(&wss_rpc_url));
745 }
746
747 #[rstest]
748 fn test_execution_config_debug_redacts_rpc_url() {
749 const PATH_SECRET: &str = "execution-http-path-secret";
750 const QUERY_SECRET: &str = "execution-http-query-secret";
751 let http_rpc_url = format!("https://rpc.example.com/{PATH_SECRET}?api_key={QUERY_SECRET}");
752 let config = BlockchainExecutionClientConfig::builder()
753 .client_id(AccountId::from("BLOCKCHAIN-001"))
754 .chain(chains::ETHEREUM.clone())
755 .wallet_address("0x0000000000000000000000000000000000000000".to_string())
756 .http_rpc_url(http_rpc_url.clone().into())
757 .signer_private_key_env("BLOCKCHAIN_PRIVATE_KEY".to_string())
758 .router_addresses(vec![
759 "0xE592427A0AEce92De3Edee1F18E0157C05861564".to_string(),
760 ])
761 .weth_address("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1".to_string())
762 .max_fee_per_gas_wei(1_000_000_000)
763 .base_fee_buffer_bps(2_000)
764 .gas_limit(1_000_000)
765 .gas_buffer_bps(2_000)
766 .allowed_token_pairs(vec![(
767 "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1".to_string(),
768 "0xaf88d065e77c8cC2239327C5EDb3A432268e5831".to_string(),
769 )])
770 .slippage_bps(50)
771 .max_slippage_bps(200)
772 .max_order_amount(1_000_000_000_000_000_000)
773 .deadline_seconds(300)
774 .max_quote_age_blocks(100)
775 .receipt_timeout_secs(60)
776 .build();
777
778 let debug = format!("{config:?}");
779
780 assert!(debug.contains("http_rpc_url: <redacted>"));
781 assert!(!debug.contains(PATH_SECRET));
782 assert!(!debug.contains(QUERY_SECRET));
783 assert!(!debug.contains(&http_rpc_url));
784 }
785}