1use std::{any::Any, fmt::Debug};
17
18use nautilus_common::factories::ClientConfig;
19use nautilus_core::string::secret::REDACTED;
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(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: String,
75 pub rpc_requests_per_second: Option<u32>,
77 #[builder(default = 200)]
79 #[serde(default = "default_multicall_calls_per_rpc_request")]
80 pub multicall_calls_per_rpc_request: u32,
81 pub wss_rpc_url: Option<String>,
83 pub proxy_url: Option<String>,
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
99impl Debug for BlockchainDataClientConfig {
100 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101 f.debug_struct(stringify!(BlockchainDataClientConfig))
102 .field("chain", &self.chain)
103 .field("dex_ids", &self.dex_ids)
104 .field(
105 "use_hypersync_for_live_data",
106 &self.use_hypersync_for_live_data,
107 )
108 .field("http_rpc_url", &REDACTED)
109 .field("rpc_requests_per_second", &self.rpc_requests_per_second)
110 .field(
111 "multicall_calls_per_rpc_request",
112 &self.multicall_calls_per_rpc_request,
113 )
114 .field("wss_rpc_url", &self.wss_rpc_url.as_ref().map(|_| REDACTED))
115 .field("proxy_url", &self.proxy_url.as_ref().map(|_| REDACTED))
116 .field("from_block", &self.from_block)
117 .field("pool_filters", &self.pool_filters)
118 .field(
119 "postgres_cache_database_config",
120 &self.postgres_cache_database_config,
121 )
122 .field("transport_backend", &self.transport_backend)
123 .finish()
124 }
125}
126
127#[cfg(feature = "python")]
128nautilus_core::impl_pyo3_config_getters!(BlockchainDataClientConfig {
129 dex_ids: Vec<DexType>,
130 multicall_calls_per_rpc_request: u32,
131 pool_filters: DexPoolFilters,
132 transport_backend: TransportBackend,
133});
134
135const fn default_multicall_calls_per_rpc_request() -> u32 {
136 200
137}
138
139#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
141#[serde(deny_unknown_fields)]
142#[cfg_attr(
143 feature = "python",
144 pyo3::pyclass(module = "nautilus_trader.adapters.blockchain", from_py_object)
145)]
146#[cfg_attr(
147 feature = "python",
148 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.blockchain")
149)]
150pub struct BlockchainProviderIdentity {
151 pub provider_id: String,
153 pub operator_id: String,
155 pub failure_domain_ids: Vec<String>,
157}
158
159#[cfg(feature = "python")]
160nautilus_core::impl_pyo3_config_getters!(BlockchainProviderIdentity {
161 failure_domain_ids: Vec<String>,
162 operator_id: String,
163 provider_id: String,
164});
165
166#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
168#[serde(deny_unknown_fields)]
169#[cfg_attr(
170 feature = "python",
171 pyo3::pyclass(module = "nautilus_trader.adapters.blockchain", from_py_object)
172)]
173#[cfg_attr(
174 feature = "python",
175 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.blockchain")
176)]
177pub struct BlockchainVerificationProviderConfig {
178 pub identity: BlockchainProviderIdentity,
180 pub http_rpc_url: String,
182}
183
184impl Debug for BlockchainVerificationProviderConfig {
185 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
186 f.debug_struct(stringify!(BlockchainVerificationProviderConfig))
187 .field("identity", &self.identity)
188 .field("http_rpc_url", &REDACTED)
189 .finish()
190 }
191}
192
193#[cfg(feature = "python")]
194nautilus_core::impl_pyo3_config_getters!(BlockchainVerificationProviderConfig {
195 http_rpc_url: String,
196 identity: BlockchainProviderIdentity,
197});
198
199#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
201#[serde(deny_unknown_fields)]
202#[cfg_attr(
203 feature = "python",
204 pyo3::pyclass(module = "nautilus_trader.adapters.blockchain", from_py_object)
205)]
206#[cfg_attr(
207 feature = "python",
208 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.blockchain")
209)]
210pub struct BlockchainChainAnchorConfig {
211 pub chain_id: u32,
213 pub chain_name: String,
215 pub checkpoint_height: u64,
217 pub checkpoint_hash: String,
219 pub checkpoint_timestamp: u64,
221 pub max_head_skew_blocks: u64,
223 pub max_head_age_secs: u64,
225 pub max_future_drift_secs: u64,
227}
228
229#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
231#[serde(rename_all = "snake_case")]
232pub enum BlockchainContractRole {
233 Router,
234 Factory,
235 WrappedNative,
236 Quote,
237 Token,
238 Pool,
239 Implementation,
240}
241
242#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
244#[serde(deny_unknown_fields)]
245pub struct BlockchainContractProbe {
246 pub call_data: String,
248 pub expected_output: String,
250}
251
252#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
254#[serde(deny_unknown_fields)]
255pub struct BlockchainProxyManifest {
256 pub kind: String,
258 pub storage_slot: String,
260 pub storage_value: String,
262 pub target_address: String,
264 pub target_code_hash: String,
266}
267
268#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
270#[serde(deny_unknown_fields)]
271pub struct BlockchainContractManifest {
272 pub address: String,
274 pub role: BlockchainContractRole,
276 pub runtime_code_hash: String,
278 pub proxy: Option<BlockchainProxyManifest>,
280 #[serde(default)]
282 pub probes: Vec<BlockchainContractProbe>,
283}
284
285#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
287#[serde(deny_unknown_fields)]
288pub struct BlockchainTokenManifest {
289 pub address: String,
290 pub name: String,
291 pub symbol: String,
292 pub decimals: u8,
293 pub asset_role: String,
295}
296
297#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
299#[serde(deny_unknown_fields)]
300pub struct BlockchainPoolManifest {
301 pub address: String,
302 pub token0: String,
303 pub token1: String,
304 pub fee: u32,
305 pub factory: String,
306 pub quote_contract: String,
307}
308
309#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
311#[serde(deny_unknown_fields)]
312pub struct BlockchainCallEdgeManifest {
313 pub purpose: String,
315 pub caller: String,
316 pub target: String,
317 pub call_type: String,
319}
320
321#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
323#[serde(deny_unknown_fields)]
324pub struct BlockchainDeploymentManifest {
325 pub version: String,
326 pub chain_id: u32,
327 pub chain_name: String,
328 pub contracts: Vec<BlockchainContractManifest>,
329 pub tokens: Vec<BlockchainTokenManifest>,
330 pub pools: Vec<BlockchainPoolManifest>,
331 pub call_edges: Vec<BlockchainCallEdgeManifest>,
332}
333
334#[cfg(feature = "python")]
335nautilus_core::impl_pyo3_config_getters!(BlockchainChainAnchorConfig {
336 chain_id: u32,
337 chain_name: String,
338 checkpoint_hash: String,
339 checkpoint_height: u64,
340 checkpoint_timestamp: u64,
341 max_future_drift_secs: u64,
342 max_head_age_secs: u64,
343 max_head_skew_blocks: u64,
344});
345
346#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
348#[serde(deny_unknown_fields)]
349#[cfg_attr(
350 feature = "python",
351 pyo3::pyclass(module = "nautilus_trader.adapters.blockchain", from_py_object)
352)]
353#[cfg_attr(
354 feature = "python",
355 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.blockchain")
356)]
357pub struct BlockchainVerificationConfig {
358 pub authoritative: BlockchainProviderIdentity,
360 pub verifiers: Vec<BlockchainVerificationProviderConfig>,
362 pub chain_anchor: BlockchainChainAnchorConfig,
364 pub manifest_version: String,
366 pub manifest_digest: String,
368 pub deployment_manifest: BlockchainDeploymentManifest,
370}
371
372impl Debug for BlockchainVerificationConfig {
373 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
374 f.debug_struct(stringify!(BlockchainVerificationConfig))
375 .field("authoritative", &self.authoritative)
376 .field("verifiers", &self.verifiers)
377 .field("chain_anchor", &self.chain_anchor)
378 .field("manifest_version", &self.manifest_version)
379 .field("manifest_digest", &self.manifest_digest)
380 .field("deployment_manifest", &REDACTED)
381 .finish()
382 }
383}
384
385#[cfg(feature = "python")]
386nautilus_core::impl_pyo3_config_getters!(BlockchainVerificationConfig {
387 authoritative: BlockchainProviderIdentity,
388 chain_anchor: BlockchainChainAnchorConfig,
389 manifest_digest: String,
390 manifest_version: String,
391 verifiers: Vec<BlockchainVerificationProviderConfig>,
392});
393
394#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
396#[serde(deny_unknown_fields)]
397#[cfg_attr(
398 feature = "python",
399 pyo3::pyclass(module = "nautilus_trader.adapters.blockchain", from_py_object)
400)]
401#[cfg_attr(
402 feature = "python",
403 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.blockchain")
404)]
405pub struct QuoteSpendLimit {
406 pub token_in: String,
408 pub token_out: String,
410 pub spend_token: String,
412 pub spend_token_decimals: u8,
414 pub max_amount: String,
416}
417
418#[cfg(feature = "python")]
419nautilus_core::impl_pyo3_config_getters!(QuoteSpendLimit {
420 max_amount: String,
421 spend_token: String,
422 spend_token_decimals: u8,
423 token_in: String,
424 token_out: String,
425});
426
427#[derive(Clone, Serialize, Deserialize, bon::Builder)]
428#[serde(deny_unknown_fields)]
429#[cfg_attr(
430 feature = "python",
431 pyo3::pyclass(module = "nautilus_trader.adapters.blockchain", from_py_object)
432)]
433#[cfg_attr(
434 feature = "python",
435 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.blockchain")
436)]
437pub struct BlockchainExecutionClientConfig {
438 pub client_id: AccountId,
440 pub chain: Chain,
442 pub wallet_address: String,
444 pub tokens: Option<Vec<String>>,
446 pub http_rpc_url: String,
448 #[serde(default)]
450 pub verification: Option<BlockchainVerificationConfig>,
451 pub rpc_requests_per_second: Option<u32>,
453 pub signer_private_key_env: String,
455 #[serde(default)]
457 pub payload_key_env: Option<String>,
458 #[builder(default)]
460 #[serde(default)]
461 pub payload_key_retired_env: Vec<String>,
462 #[serde(default)]
464 pub payload_deployment_id: Option<String>,
465 pub router_addresses: Vec<String>,
467 pub weth_address: String,
469 #[builder(default)]
471 #[serde(default)]
472 pub unlimited_approval: bool,
473 pub max_fee_per_gas_wei: u64,
476 pub base_fee_buffer_bps: u32,
478 pub gas_limit: u64,
481 pub gas_buffer_bps: u32,
483 pub allowed_token_pairs: Option<Vec<(String, String)>>,
485 pub quote_spend_limits: Option<Vec<QuoteSpendLimit>>,
487 pub slippage_bps: Option<u32>,
489 pub max_slippage_bps: Option<u32>,
491 pub max_order_amount: Option<u64>,
493 pub deadline_seconds: Option<u64>,
495 pub max_quote_age_blocks: Option<u64>,
497 pub receipt_timeout_secs: Option<u64>,
499 pub postgres_cache_database_config: Option<PostgresConnectOptions>,
502 #[builder(default)]
504 #[serde(default)]
505 pub transport_backend: TransportBackend,
506}
507
508impl Debug for BlockchainExecutionClientConfig {
509 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
510 f.debug_struct(stringify!(BlockchainExecutionClientConfig))
511 .field("client_id", &self.client_id)
512 .field("chain", &self.chain)
513 .field("wallet_address", &self.wallet_address)
514 .field("tokens", &self.tokens)
515 .field("http_rpc_url", &REDACTED)
516 .field("verification", &self.verification)
517 .field("rpc_requests_per_second", &self.rpc_requests_per_second)
518 .field("signer_private_key_env", &self.signer_private_key_env)
519 .field("payload_key_env", &self.payload_key_env)
520 .field("payload_key_retired_env", &self.payload_key_retired_env)
521 .field("payload_deployment_id", &self.payload_deployment_id)
522 .field("router_addresses", &self.router_addresses)
523 .field("weth_address", &self.weth_address)
524 .field("unlimited_approval", &self.unlimited_approval)
525 .field("max_fee_per_gas_wei", &self.max_fee_per_gas_wei)
526 .field("base_fee_buffer_bps", &self.base_fee_buffer_bps)
527 .field("gas_limit", &self.gas_limit)
528 .field("gas_buffer_bps", &self.gas_buffer_bps)
529 .field("allowed_token_pairs", &self.allowed_token_pairs)
530 .field("quote_spend_limits", &self.quote_spend_limits)
531 .field("slippage_bps", &self.slippage_bps)
532 .field("max_slippage_bps", &self.max_slippage_bps)
533 .field("max_order_amount", &self.max_order_amount)
534 .field("deadline_seconds", &self.deadline_seconds)
535 .field("max_quote_age_blocks", &self.max_quote_age_blocks)
536 .field("receipt_timeout_secs", &self.receipt_timeout_secs)
537 .field(
538 "postgres_cache_database_config",
539 &self.postgres_cache_database_config,
540 )
541 .field("transport_backend", &self.transport_backend)
542 .finish()
543 }
544}
545
546impl ClientConfig for BlockchainExecutionClientConfig {
547 fn as_any(&self) -> &dyn Any {
548 self
549 }
550}
551
552#[cfg(feature = "python")]
553nautilus_core::impl_pyo3_config_getters!(BlockchainExecutionClientConfig {
554 base_fee_buffer_bps: u32,
555 deadline_seconds: Option<u64>,
556 gas_buffer_bps: u32,
557 gas_limit: u64,
558 http_rpc_url: String,
559 verification: Option<BlockchainVerificationConfig>,
560 max_fee_per_gas_wei: u64,
561 max_order_amount: Option<u64>,
562 max_quote_age_blocks: Option<u64>,
563 max_slippage_bps: Option<u32>,
564 quote_spend_limits: Option<Vec<QuoteSpendLimit>>,
565 receipt_timeout_secs: Option<u64>,
566 router_addresses: Vec<String>,
567 payload_deployment_id: Option<String>,
568 payload_key_env: Option<String>,
569 payload_key_retired_env: Vec<String>,
570 signer_private_key_env: String,
571 slippage_bps: Option<u32>,
572 tokens: Option<Vec<String>>,
573 transport_backend: TransportBackend,
574 unlimited_approval: bool,
575 wallet_address: String,
576 weth_address: String,
577});
578
579#[cfg(test)]
580mod tests {
581 use std::sync::Arc;
582
583 use nautilus_model::defi::chain::chains;
584 use rstest::rstest;
585
586 use super::*;
587
588 #[rstest]
589 fn test_data_config_toml_minimal() {
590 let config: BlockchainDataClientConfig = toml::from_str(
591 r#"
592http_rpc_url = "https://eth-mainnet.example.com"
593
594[chain]
595name = "Ethereum"
596chain_id = 1
597hypersync_url = "https://1.hypersync.xyz"
598native_currency_decimals = 18
599"#,
600 )
601 .unwrap();
602
603 assert_eq!(config.http_rpc_url, "https://eth-mainnet.example.com");
604 assert_eq!(config.chain.chain_id, 1);
605 assert!(config.dex_ids.is_empty());
606 assert!(!config.use_hypersync_for_live_data);
607 assert_eq!(config.multicall_calls_per_rpc_request, 200);
608 assert!(config.pool_filters.remove_pools_with_empty_erc20fields);
609 assert_eq!(config.transport_backend, TransportBackend::default());
610 }
611
612 #[rstest]
613 fn test_execution_config_toml_minimal() {
614 let config: BlockchainExecutionClientConfig = toml::from_str(
615 r#"
616client_id = "BLOCKCHAIN-001"
617wallet_address = "0x0000000000000000000000000000000000000000"
618http_rpc_url = "https://eth-mainnet.example.com"
619signer_private_key_env = "BLOCKCHAIN_PRIVATE_KEY"
620payload_key_env = "BLOCKCHAIN_PAYLOAD_KEY"
621payload_key_retired_env = ["BLOCKCHAIN_PAYLOAD_KEY_OLD"]
622payload_deployment_id = "primary-execution"
623router_addresses = ["0xE592427A0AEce92De3Edee1F18E0157C05861564"]
624weth_address = "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"
625max_fee_per_gas_wei = 1000000000
626base_fee_buffer_bps = 2000
627gas_limit = 1000000
628gas_buffer_bps = 2000
629allowed_token_pairs = [
630 ["0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", "0xaf88d065e77c8cC2239327C5EDb3A432268e5831"],
631 ["0xaf88d065e77c8cC2239327C5EDb3A432268e5831", "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"],
632]
633slippage_bps = 50
634max_slippage_bps = 200
635max_order_amount = 1000000000000000000
636deadline_seconds = 300
637max_quote_age_blocks = 100
638receipt_timeout_secs = 60
639
640[[quote_spend_limits]]
641token_in = "0xaf88d065e77c8cC2239327C5EDb3A432268e5831"
642token_out = "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"
643spend_token = "0xaf88d065e77c8cC2239327C5EDb3A432268e5831"
644spend_token_decimals = 6
645max_amount = "1000000000"
646
647[chain]
648name = "Ethereum"
649chain_id = 1
650hypersync_url = "https://1.hypersync.xyz"
651native_currency_decimals = 18
652"#,
653 )
654 .unwrap();
655
656 assert_eq!(config.http_rpc_url, "https://eth-mainnet.example.com");
657 assert_eq!(config.chain.chain_id, 1);
658 assert_eq!(
659 config.wallet_address,
660 "0x0000000000000000000000000000000000000000",
661 );
662 assert!(config.tokens.is_none());
663 assert!(config.rpc_requests_per_second.is_none());
664 assert_eq!(config.signer_private_key_env, "BLOCKCHAIN_PRIVATE_KEY");
665 assert_eq!(
666 config.payload_key_env.as_deref(),
667 Some("BLOCKCHAIN_PAYLOAD_KEY")
668 );
669 assert_eq!(
670 config.payload_key_retired_env,
671 vec!["BLOCKCHAIN_PAYLOAD_KEY_OLD".to_string()]
672 );
673 assert_eq!(
674 config.payload_deployment_id.as_deref(),
675 Some("primary-execution")
676 );
677 assert_eq!(
678 config.router_addresses,
679 vec!["0xE592427A0AEce92De3Edee1F18E0157C05861564".to_string()],
680 );
681 assert_eq!(
682 config.weth_address,
683 "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
684 );
685 assert!(!config.unlimited_approval);
686 assert_eq!(config.max_fee_per_gas_wei, 1_000_000_000);
687 assert_eq!(config.base_fee_buffer_bps, 2_000);
688 assert_eq!(config.gas_limit, 1_000_000);
689 assert_eq!(config.gas_buffer_bps, 2_000);
690 assert_eq!(
691 config.allowed_token_pairs,
692 Some(vec![
693 (
694 "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1".to_string(),
695 "0xaf88d065e77c8cC2239327C5EDb3A432268e5831".to_string(),
696 ),
697 (
698 "0xaf88d065e77c8cC2239327C5EDb3A432268e5831".to_string(),
699 "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1".to_string(),
700 )
701 ]),
702 );
703 assert_eq!(
704 config.quote_spend_limits,
705 Some(vec![QuoteSpendLimit {
706 token_in: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831".to_string(),
707 token_out: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1".to_string(),
708 spend_token: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831".to_string(),
709 spend_token_decimals: 6,
710 max_amount: "1000000000".to_string(),
711 }]),
712 );
713 assert_eq!(config.slippage_bps, Some(50));
714 assert_eq!(config.max_slippage_bps, Some(200));
715 assert_eq!(config.max_order_amount, Some(1_000_000_000_000_000_000));
716 assert_eq!(config.deadline_seconds, Some(300));
717 assert_eq!(config.max_quote_age_blocks, Some(100));
718 assert_eq!(config.receipt_timeout_secs, Some(60));
719 assert!(config.postgres_cache_database_config.is_none());
720 assert_eq!(config.transport_backend, TransportBackend::default());
721 }
722
723 #[rstest]
724 fn test_execution_config_toml_accepts_legacy_shape_without_transaction_limits() {
725 let config: BlockchainExecutionClientConfig = toml::from_str(
726 r#"
727client_id = "BLOCKCHAIN-001"
728wallet_address = "0x0000000000000000000000000000000000000000"
729http_rpc_url = "https://eth-mainnet.example.com"
730signer_private_key_env = "BLOCKCHAIN_PRIVATE_KEY"
731router_addresses = ["0xE592427A0AEce92De3Edee1F18E0157C05861564"]
732weth_address = "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"
733max_fee_per_gas_wei = 1000000000
734base_fee_buffer_bps = 2000
735gas_limit = 1000000
736gas_buffer_bps = 2000
737
738[chain]
739name = "Ethereum"
740chain_id = 1
741hypersync_url = "https://1.hypersync.xyz"
742native_currency_decimals = 18
743"#,
744 )
745 .unwrap();
746
747 assert!(config.allowed_token_pairs.is_none());
748 assert!(config.payload_key_env.is_none());
749 assert!(config.payload_key_retired_env.is_empty());
750 assert!(config.payload_deployment_id.is_none());
751 assert!(config.quote_spend_limits.is_none());
752 assert!(config.slippage_bps.is_none());
753 assert!(config.max_slippage_bps.is_none());
754 assert!(config.max_order_amount.is_none());
755 assert!(config.deadline_seconds.is_none());
756 assert!(config.max_quote_age_blocks.is_none());
757 assert!(config.receipt_timeout_secs.is_none());
758 }
759
760 #[rstest]
761 fn test_execution_config_toml_rejects_unknown_fields() {
762 let result: Result<BlockchainExecutionClientConfig, _> = toml::from_str(
763 r#"
764client_id = "BLOCKCHAIN-001"
765wallet_address = "0x0000000000000000000000000000000000000000"
766http_rpc_url = "https://eth-mainnet.example.com"
767signer_private_key_env = "BLOCKCHAIN_PRIVATE_KEY"
768router_addresses = ["0xE592427A0AEce92De3Edee1F18E0157C05861564"]
769weth_address = "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"
770max_fee_per_gas_wei = 1000000000
771base_fee_buffer_bps = 2000
772gas_limit = 1000000
773gas_buffer_bps = 2000
774allowed_token_pairs = [["0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", "0xaf88d065e77c8cC2239327C5EDb3A432268e5831"]]
775slippage_bps = 50
776max_slippage_bps = 200
777max_order_amount = 1000000000000000000
778deadline_seconds = 300
779max_quote_age_blocks = 100
780receipt_timeout_secs = 60
781unknown_field = 1
782
783[chain]
784name = "Ethereum"
785chain_id = 1
786hypersync_url = "https://1.hypersync.xyz"
787native_currency_decimals = 18
788"#,
789 );
790
791 assert!(result.is_err());
792 }
793
794 #[rstest]
795 fn test_data_config_debug_redacts_rpc_urls() {
796 const HTTP_USERINFO_SECRET: &str = "data-http-userinfo-secret";
797 const WSS_QUERY_SECRET: &str = "data-wss-query-secret";
798 let http_rpc_url = format!(
799 "https://rpc-user:{HTTP_USERINFO_SECRET}@rpc.example.com/data-http-path-secret"
800 );
801 let wss_rpc_url = format!("wss://rpc.example.com/ws?api_key={WSS_QUERY_SECRET}");
802 let config = BlockchainDataClientConfig::builder()
803 .chain(Arc::new(chains::ETHEREUM.clone()))
804 .http_rpc_url(http_rpc_url.clone())
805 .wss_rpc_url(wss_rpc_url.clone())
806 .build();
807
808 let debug = format!("{config:?}");
809
810 assert!(debug.contains("http_rpc_url: \"<redacted>\""));
811 assert!(debug.contains("wss_rpc_url: Some(\"<redacted>\")"));
812 assert!(!debug.contains(HTTP_USERINFO_SECRET));
813 assert!(!debug.contains(WSS_QUERY_SECRET));
814 assert!(!debug.contains(&http_rpc_url));
815 assert!(!debug.contains(&wss_rpc_url));
816 }
817
818 #[rstest]
819 fn test_execution_config_debug_redacts_rpc_url() {
820 const PATH_SECRET: &str = "execution-http-path-secret";
821 const QUERY_SECRET: &str = "execution-http-query-secret";
822 let http_rpc_url = format!("https://rpc.example.com/{PATH_SECRET}?api_key={QUERY_SECRET}");
823 let config = BlockchainExecutionClientConfig::builder()
824 .client_id(AccountId::from("BLOCKCHAIN-001"))
825 .chain(chains::ETHEREUM.clone())
826 .wallet_address("0x0000000000000000000000000000000000000000".to_string())
827 .http_rpc_url(http_rpc_url.clone())
828 .signer_private_key_env("BLOCKCHAIN_PRIVATE_KEY".to_string())
829 .router_addresses(vec![
830 "0xE592427A0AEce92De3Edee1F18E0157C05861564".to_string(),
831 ])
832 .weth_address("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1".to_string())
833 .max_fee_per_gas_wei(1_000_000_000)
834 .base_fee_buffer_bps(2_000)
835 .gas_limit(1_000_000)
836 .gas_buffer_bps(2_000)
837 .allowed_token_pairs(vec![(
838 "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1".to_string(),
839 "0xaf88d065e77c8cC2239327C5EDb3A432268e5831".to_string(),
840 )])
841 .slippage_bps(50)
842 .max_slippage_bps(200)
843 .max_order_amount(1_000_000_000_000_000_000)
844 .deadline_seconds(300)
845 .max_quote_age_blocks(100)
846 .receipt_timeout_secs(60)
847 .build();
848
849 let debug = format!("{config:?}");
850
851 assert!(debug.contains("http_rpc_url: \"<redacted>\""));
852 assert!(!debug.contains(PATH_SECRET));
853 assert!(!debug.contains(QUERY_SECRET));
854 assert!(!debug.contains(&http_rpc_url));
855 }
856}