1use std::{any::Any, fmt::Debug};
17
18use nautilus_common::{
19 config::{ConfigResult, check_unique_values},
20 factories::ClientConfig,
21};
22use nautilus_core::string::secret::{REDACTED, SecretString};
23use nautilus_infrastructure::sql::pg::PostgresConnectOptions;
24use nautilus_model::{
25 defi::{Chain, DexType, SharedChain},
26 identifiers::AccountId,
27};
28use nautilus_network::websocket::TransportBackend;
29use serde::{Deserialize, Serialize};
30
31#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
33#[serde(default, deny_unknown_fields)]
34#[cfg_attr(
35 feature = "python",
36 pyo3::pyclass(module = "nautilus_trader.adapters.blockchain", from_py_object)
37)]
38#[cfg_attr(
39 feature = "python",
40 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.blockchain")
41)]
42pub struct DexPoolFilters {
43 #[builder(default = true)]
45 pub remove_pools_with_empty_erc20fields: bool,
46}
47
48impl Default for DexPoolFilters {
49 fn default() -> Self {
50 Self::builder().build()
51 }
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
56#[serde(deny_unknown_fields)]
57#[cfg_attr(
58 feature = "python",
59 pyo3::pyclass(module = "nautilus_trader.adapters.blockchain", from_py_object)
60)]
61#[cfg_attr(
62 feature = "python",
63 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.blockchain")
64)]
65pub struct BlockchainDataClientConfig {
66 pub chain: SharedChain,
68 #[builder(default)]
70 #[serde(default)]
71 pub dex_ids: Vec<DexType>,
72 #[builder(default)]
74 #[serde(default)]
75 pub use_hypersync_for_live_data: bool,
76 pub http_rpc_url: SecretString,
78 pub wss_rpc_url: Option<SecretString>,
80 pub proxy_url: Option<SecretString>,
82 pub rpc_requests_per_second: Option<u32>,
84 #[builder(default = 200)]
86 #[serde(default = "default_multicall_calls_per_rpc_request")]
87 pub multicall_calls_per_rpc_request: u32,
88 pub from_block: Option<u64>,
90 #[builder(default)]
92 #[serde(default)]
93 pub pool_filters: DexPoolFilters,
94 pub postgres_cache_database_config: Option<PostgresConnectOptions>,
96 #[builder(default)]
98 #[serde(default)]
99 pub transport_backend: TransportBackend,
100}
101
102#[cfg(feature = "python")]
103nautilus_core::impl_pyo3_config_getters!(BlockchainDataClientConfig {
104 dex_ids: Vec<DexType>,
105 multicall_calls_per_rpc_request: u32,
106 pool_filters: DexPoolFilters,
107 transport_backend: TransportBackend,
108});
109
110const fn default_multicall_calls_per_rpc_request() -> u32 {
111 200
112}
113
114#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
116#[serde(deny_unknown_fields)]
117#[cfg_attr(
118 feature = "python",
119 pyo3::pyclass(module = "nautilus_trader.adapters.blockchain", from_py_object)
120)]
121#[cfg_attr(
122 feature = "python",
123 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.blockchain")
124)]
125pub struct BlockchainProviderIdentity {
126 pub provider_id: String,
128 pub operator_id: String,
130 pub failure_domain_ids: Vec<String>,
132}
133
134#[cfg(feature = "python")]
135nautilus_core::impl_pyo3_config_getters!(BlockchainProviderIdentity {
136 failure_domain_ids: Vec<String>,
137 operator_id: String,
138 provider_id: String,
139});
140
141#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
143#[serde(deny_unknown_fields)]
144#[cfg_attr(
145 feature = "python",
146 pyo3::pyclass(module = "nautilus_trader.adapters.blockchain", from_py_object)
147)]
148#[cfg_attr(
149 feature = "python",
150 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.blockchain")
151)]
152pub struct BlockchainVerificationProviderConfig {
153 pub identity: BlockchainProviderIdentity,
155 pub http_rpc_url: SecretString,
157}
158
159#[cfg(feature = "python")]
160nautilus_core::impl_pyo3_config_getters!(BlockchainVerificationProviderConfig {
161 identity: BlockchainProviderIdentity,
162});
163
164#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
166#[serde(deny_unknown_fields)]
167#[cfg_attr(
168 feature = "python",
169 pyo3::pyclass(module = "nautilus_trader.adapters.blockchain", from_py_object)
170)]
171#[cfg_attr(
172 feature = "python",
173 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.blockchain")
174)]
175pub struct BlockchainChainAnchorConfig {
176 pub chain_id: u32,
178 pub chain_name: String,
180 pub checkpoint_height: u64,
182 pub checkpoint_hash: String,
184 pub checkpoint_timestamp: u64,
186 pub max_head_skew_blocks: u64,
188 pub max_head_age_secs: u64,
190 pub max_future_drift_secs: u64,
192}
193
194#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
196#[serde(rename_all = "snake_case")]
197pub enum BlockchainContractRole {
198 Router,
199 Factory,
200 WrappedNative,
201 Quote,
202 Token,
203 Pool,
204 Implementation,
205}
206
207#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
209#[serde(deny_unknown_fields)]
210pub struct BlockchainContractProbe {
211 pub call_data: String,
213 pub expected_output: String,
215}
216
217#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
219#[serde(deny_unknown_fields)]
220pub struct BlockchainProxyManifest {
221 pub kind: String,
223 pub storage_slot: String,
225 pub storage_value: String,
227 pub target_address: String,
229 pub target_code_hash: String,
231}
232
233#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
235#[serde(deny_unknown_fields)]
236pub struct BlockchainContractManifest {
237 pub address: String,
239 pub role: BlockchainContractRole,
241 pub runtime_code_hash: String,
243 pub proxy: Option<BlockchainProxyManifest>,
245 #[serde(default)]
247 pub probes: Vec<BlockchainContractProbe>,
248}
249
250#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
252#[serde(deny_unknown_fields)]
253pub struct BlockchainTokenManifest {
254 pub address: String,
255 pub name: String,
256 pub symbol: String,
257 pub decimals: u8,
258 pub asset_role: String,
260}
261
262#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
264#[serde(deny_unknown_fields)]
265pub struct BlockchainPoolManifest {
266 pub address: String,
267 pub token0: String,
268 pub token1: String,
269 pub fee: u32,
270 pub factory: String,
271 pub quote_contract: String,
272}
273
274#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
276#[serde(deny_unknown_fields)]
277pub struct BlockchainCallEdgeManifest {
278 pub purpose: String,
280 pub caller: String,
281 pub target: String,
282 pub call_type: String,
284}
285
286#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
288#[serde(deny_unknown_fields)]
289pub struct BlockchainDeploymentManifest {
290 pub version: String,
291 pub chain_id: u32,
292 pub chain_name: String,
293 pub contracts: Vec<BlockchainContractManifest>,
294 pub tokens: Vec<BlockchainTokenManifest>,
295 pub pools: Vec<BlockchainPoolManifest>,
296 pub call_edges: Vec<BlockchainCallEdgeManifest>,
297}
298
299#[cfg(feature = "python")]
300nautilus_core::impl_pyo3_config_getters!(BlockchainChainAnchorConfig {
301 chain_id: u32,
302 chain_name: String,
303 checkpoint_hash: String,
304 checkpoint_height: u64,
305 checkpoint_timestamp: u64,
306 max_future_drift_secs: u64,
307 max_head_age_secs: u64,
308 max_head_skew_blocks: u64,
309});
310
311#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
313#[serde(deny_unknown_fields)]
314#[cfg_attr(
315 feature = "python",
316 pyo3::pyclass(module = "nautilus_trader.adapters.blockchain", from_py_object)
317)]
318#[cfg_attr(
319 feature = "python",
320 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.blockchain")
321)]
322pub struct BlockchainVerificationConfig {
323 pub authoritative: BlockchainProviderIdentity,
325 pub verifiers: Vec<BlockchainVerificationProviderConfig>,
327 pub chain_anchor: BlockchainChainAnchorConfig,
329 pub manifest_version: String,
331 pub manifest_digest: String,
333 pub deployment_manifest: BlockchainDeploymentManifest,
335}
336
337impl Debug for BlockchainVerificationConfig {
338 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
339 f.debug_struct(stringify!(BlockchainVerificationConfig))
340 .field("authoritative", &self.authoritative)
341 .field("verifiers", &self.verifiers)
342 .field("chain_anchor", &self.chain_anchor)
343 .field("manifest_version", &self.manifest_version)
344 .field("manifest_digest", &self.manifest_digest)
345 .field("deployment_manifest", &REDACTED)
346 .finish()
347 }
348}
349
350#[cfg(feature = "python")]
351nautilus_core::impl_pyo3_config_getters!(BlockchainVerificationConfig {
352 authoritative: BlockchainProviderIdentity,
353 chain_anchor: BlockchainChainAnchorConfig,
354 manifest_digest: String,
355 manifest_version: String,
356 verifiers: Vec<BlockchainVerificationProviderConfig>,
357});
358
359#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
361#[serde(deny_unknown_fields)]
362#[cfg_attr(
363 feature = "python",
364 pyo3::pyclass(module = "nautilus_trader.adapters.blockchain", from_py_object)
365)]
366#[cfg_attr(
367 feature = "python",
368 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.blockchain")
369)]
370pub struct QuoteSpendLimit {
371 pub token_in: String,
373 pub token_out: String,
375 pub spend_token: String,
377 pub spend_token_decimals: u8,
379 pub max_amount: String,
381}
382
383#[cfg(feature = "python")]
384nautilus_core::impl_pyo3_config_getters!(QuoteSpendLimit {
385 max_amount: String,
386 spend_token: String,
387 spend_token_decimals: u8,
388 token_in: String,
389 token_out: String,
390});
391
392#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
393#[serde(deny_unknown_fields)]
394#[cfg_attr(
395 feature = "python",
396 pyo3::pyclass(module = "nautilus_trader.adapters.blockchain", from_py_object)
397)]
398#[cfg_attr(
399 feature = "python",
400 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.blockchain")
401)]
402pub struct BlockchainExecutionClientConfig {
403 pub client_id: AccountId,
405 pub chain: Chain,
407 pub wallet_address: String,
409 pub tokens: Option<Vec<String>>,
411 pub http_rpc_url: SecretString,
413 #[serde(default)]
415 pub verification: Option<BlockchainVerificationConfig>,
416 pub rpc_requests_per_second: Option<u32>,
418 pub signer_private_key_env: String,
420 #[serde(default)]
422 pub payload_key_env: Option<String>,
423 #[builder(default)]
425 #[serde(default)]
426 pub payload_key_retired_env: Vec<String>,
427 #[serde(default)]
429 pub payload_deployment_id: Option<String>,
430 pub router_addresses: Vec<String>,
432 pub weth_address: String,
434 #[builder(default)]
436 #[serde(default)]
437 pub unlimited_approval: bool,
438 pub max_fee_per_gas_wei: u64,
441 pub base_fee_buffer_bps: u32,
443 pub gas_limit: u64,
446 pub gas_buffer_bps: u32,
448 pub allowed_token_pairs: Option<Vec<(String, String)>>,
450 pub quote_spend_limits: Option<Vec<QuoteSpendLimit>>,
452 pub slippage_bps: Option<u32>,
454 pub max_slippage_bps: Option<u32>,
456 pub max_order_amount: Option<u64>,
458 pub deadline_seconds: Option<u64>,
460 pub max_quote_age_blocks: Option<u64>,
462 pub receipt_timeout_secs: Option<u64>,
464 pub postgres_cache_database_config: Option<PostgresConnectOptions>,
467 #[builder(default)]
469 #[serde(default)]
470 pub transport_backend: TransportBackend,
471}
472
473impl ClientConfig for BlockchainExecutionClientConfig {
474 fn as_any(&self) -> &dyn Any {
475 self
476 }
477}
478
479#[cfg(feature = "python")]
480nautilus_core::impl_pyo3_config_getters!(BlockchainExecutionClientConfig {
481 base_fee_buffer_bps: u32,
482 deadline_seconds: Option<u64>,
483 gas_buffer_bps: u32,
484 gas_limit: u64,
485 verification: Option<BlockchainVerificationConfig>,
486 max_fee_per_gas_wei: u64,
487 max_order_amount: Option<u64>,
488 max_quote_age_blocks: Option<u64>,
489 max_slippage_bps: Option<u32>,
490 quote_spend_limits: Option<Vec<QuoteSpendLimit>>,
491 receipt_timeout_secs: Option<u64>,
492 router_addresses: Vec<String>,
493 payload_deployment_id: Option<String>,
494 payload_key_env: Option<String>,
495 payload_key_retired_env: Vec<String>,
496 signer_private_key_env: String,
497 slippage_bps: Option<u32>,
498 tokens: Option<Vec<String>>,
499 transport_backend: TransportBackend,
500 unlimited_approval: bool,
501 wallet_address: String,
502 weth_address: String,
503});
504
505impl BlockchainDataClientConfig {
506 pub fn validate(&self) -> ConfigResult<()> {
512 check_unique_values("dex_ids", &self.dex_ids)
513 }
514}
515
516impl BlockchainExecutionClientConfig {
517 pub fn validate(&self) -> ConfigResult<()> {
523 if let Some(values) = &self.tokens {
524 check_unique_values("tokens", values)?;
525 }
526
527 check_unique_values("payload_key_retired_env", &self.payload_key_retired_env)?;
528 check_unique_values("router_addresses", &self.router_addresses)?;
529
530 if let Some(values) = &self.allowed_token_pairs {
531 check_unique_values("allowed_token_pairs", values)?;
532 }
533
534 Ok(())
535 }
536}
537
538#[cfg(test)]
539mod tests {
540 use std::sync::Arc;
541
542 use nautilus_common::config::ConfigError;
543 use nautilus_model::defi::chain::chains;
544 #[cfg(feature = "hypersync")]
545 use nautilus_model::identifiers::ClientId;
546 use rstest::rstest;
547
548 use super::*;
549
550 #[rstest]
551 fn test_data_config_toml_minimal() {
552 let config: BlockchainDataClientConfig = toml::from_str(
553 r#"
554http_rpc_url = "https://eth-mainnet.example.com"
555
556[chain]
557name = "Ethereum"
558chain_id = 1
559hypersync_url = "https://1.hypersync.xyz"
560native_currency_decimals = 18
561"#,
562 )
563 .unwrap();
564
565 assert_eq!(
566 config.http_rpc_url.expose_secret(),
567 "https://eth-mainnet.example.com"
568 );
569 assert_eq!(config.chain.chain_id, 1);
570 assert!(config.dex_ids.is_empty());
571 assert!(!config.use_hypersync_for_live_data);
572 assert_eq!(config.multicall_calls_per_rpc_request, 200);
573 assert!(config.pool_filters.remove_pools_with_empty_erc20fields);
574 assert_eq!(config.transport_backend, TransportBackend::default());
575 }
576
577 #[rstest]
578 fn test_execution_config_toml_minimal() {
579 let config: BlockchainExecutionClientConfig = toml::from_str(
580 r#"
581client_id = "BLOCKCHAIN-001"
582wallet_address = "0x0000000000000000000000000000000000000000"
583http_rpc_url = "https://eth-mainnet.example.com"
584signer_private_key_env = "BLOCKCHAIN_PRIVATE_KEY"
585payload_key_env = "BLOCKCHAIN_PAYLOAD_KEY"
586payload_key_retired_env = ["BLOCKCHAIN_PAYLOAD_KEY_OLD"]
587payload_deployment_id = "primary-execution"
588router_addresses = ["0xE592427A0AEce92De3Edee1F18E0157C05861564"]
589weth_address = "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"
590max_fee_per_gas_wei = 1000000000
591base_fee_buffer_bps = 2000
592gas_limit = 1000000
593gas_buffer_bps = 2000
594allowed_token_pairs = [
595 ["0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", "0xaf88d065e77c8cC2239327C5EDb3A432268e5831"],
596 ["0xaf88d065e77c8cC2239327C5EDb3A432268e5831", "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"],
597]
598slippage_bps = 50
599max_slippage_bps = 200
600max_order_amount = 1000000000000000000
601deadline_seconds = 300
602max_quote_age_blocks = 100
603receipt_timeout_secs = 60
604
605[[quote_spend_limits]]
606token_in = "0xaf88d065e77c8cC2239327C5EDb3A432268e5831"
607token_out = "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"
608spend_token = "0xaf88d065e77c8cC2239327C5EDb3A432268e5831"
609spend_token_decimals = 6
610max_amount = "1000000000"
611
612[chain]
613name = "Ethereum"
614chain_id = 1
615hypersync_url = "https://1.hypersync.xyz"
616native_currency_decimals = 18
617"#,
618 )
619 .unwrap();
620
621 assert_eq!(
622 config.http_rpc_url.expose_secret(),
623 "https://eth-mainnet.example.com"
624 );
625 assert_eq!(config.chain.chain_id, 1);
626 assert_eq!(
627 config.wallet_address,
628 "0x0000000000000000000000000000000000000000",
629 );
630 assert!(config.tokens.is_none());
631 assert!(config.rpc_requests_per_second.is_none());
632 assert_eq!(config.signer_private_key_env, "BLOCKCHAIN_PRIVATE_KEY");
633 assert_eq!(
634 config.payload_key_env.as_deref(),
635 Some("BLOCKCHAIN_PAYLOAD_KEY")
636 );
637 assert_eq!(
638 config.payload_key_retired_env,
639 vec!["BLOCKCHAIN_PAYLOAD_KEY_OLD".to_string()]
640 );
641 assert_eq!(
642 config.payload_deployment_id.as_deref(),
643 Some("primary-execution")
644 );
645 assert_eq!(
646 config.router_addresses,
647 vec!["0xE592427A0AEce92De3Edee1F18E0157C05861564".to_string()],
648 );
649 assert_eq!(
650 config.weth_address,
651 "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
652 );
653 assert!(!config.unlimited_approval);
654 assert_eq!(config.max_fee_per_gas_wei, 1_000_000_000);
655 assert_eq!(config.base_fee_buffer_bps, 2_000);
656 assert_eq!(config.gas_limit, 1_000_000);
657 assert_eq!(config.gas_buffer_bps, 2_000);
658 assert_eq!(
659 config.allowed_token_pairs,
660 Some(vec![
661 (
662 "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1".to_string(),
663 "0xaf88d065e77c8cC2239327C5EDb3A432268e5831".to_string(),
664 ),
665 (
666 "0xaf88d065e77c8cC2239327C5EDb3A432268e5831".to_string(),
667 "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1".to_string(),
668 )
669 ]),
670 );
671 assert_eq!(
672 config.quote_spend_limits,
673 Some(vec![QuoteSpendLimit {
674 token_in: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831".to_string(),
675 token_out: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1".to_string(),
676 spend_token: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831".to_string(),
677 spend_token_decimals: 6,
678 max_amount: "1000000000".to_string(),
679 }]),
680 );
681 assert_eq!(config.slippage_bps, Some(50));
682 assert_eq!(config.max_slippage_bps, Some(200));
683 assert_eq!(config.max_order_amount, Some(1_000_000_000_000_000_000));
684 assert_eq!(config.deadline_seconds, Some(300));
685 assert_eq!(config.max_quote_age_blocks, Some(100));
686 assert_eq!(config.receipt_timeout_secs, Some(60));
687 assert!(config.postgres_cache_database_config.is_none());
688 assert_eq!(config.transport_backend, TransportBackend::default());
689 }
690
691 #[rstest]
692 fn test_execution_config_toml_accepts_legacy_shape_without_transaction_limits() {
693 let config: BlockchainExecutionClientConfig = toml::from_str(
694 r#"
695client_id = "BLOCKCHAIN-001"
696wallet_address = "0x0000000000000000000000000000000000000000"
697http_rpc_url = "https://eth-mainnet.example.com"
698signer_private_key_env = "BLOCKCHAIN_PRIVATE_KEY"
699router_addresses = ["0xE592427A0AEce92De3Edee1F18E0157C05861564"]
700weth_address = "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"
701max_fee_per_gas_wei = 1000000000
702base_fee_buffer_bps = 2000
703gas_limit = 1000000
704gas_buffer_bps = 2000
705
706[chain]
707name = "Ethereum"
708chain_id = 1
709hypersync_url = "https://1.hypersync.xyz"
710native_currency_decimals = 18
711"#,
712 )
713 .unwrap();
714
715 assert!(config.allowed_token_pairs.is_none());
716 assert!(config.payload_key_env.is_none());
717 assert!(config.payload_key_retired_env.is_empty());
718 assert!(config.payload_deployment_id.is_none());
719 assert!(config.quote_spend_limits.is_none());
720 assert!(config.slippage_bps.is_none());
721 assert!(config.max_slippage_bps.is_none());
722 assert!(config.max_order_amount.is_none());
723 assert!(config.deadline_seconds.is_none());
724 assert!(config.max_quote_age_blocks.is_none());
725 assert!(config.receipt_timeout_secs.is_none());
726 }
727
728 #[rstest]
729 fn test_execution_config_toml_rejects_unknown_fields() {
730 let result: Result<BlockchainExecutionClientConfig, _> = toml::from_str(
731 r#"
732client_id = "BLOCKCHAIN-001"
733wallet_address = "0x0000000000000000000000000000000000000000"
734http_rpc_url = "https://eth-mainnet.example.com"
735signer_private_key_env = "BLOCKCHAIN_PRIVATE_KEY"
736router_addresses = ["0xE592427A0AEce92De3Edee1F18E0157C05861564"]
737weth_address = "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"
738max_fee_per_gas_wei = 1000000000
739base_fee_buffer_bps = 2000
740gas_limit = 1000000
741gas_buffer_bps = 2000
742allowed_token_pairs = [["0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", "0xaf88d065e77c8cC2239327C5EDb3A432268e5831"]]
743slippage_bps = 50
744max_slippage_bps = 200
745max_order_amount = 1000000000000000000
746deadline_seconds = 300
747max_quote_age_blocks = 100
748receipt_timeout_secs = 60
749unknown_field = 1
750
751[chain]
752name = "Ethereum"
753chain_id = 1
754hypersync_url = "https://1.hypersync.xyz"
755native_currency_decimals = 18
756"#,
757 );
758
759 assert!(result.is_err());
760 }
761
762 #[rstest]
763 fn test_data_config_debug_redacts_rpc_urls() {
764 const HTTP_USERINFO_SECRET: &str = "data-http-userinfo-secret";
765 const WSS_QUERY_SECRET: &str = "data-wss-query-secret";
766 let http_rpc_url = format!(
767 "https://rpc-user:{HTTP_USERINFO_SECRET}@rpc.example.com/data-http-path-secret"
768 );
769 let wss_rpc_url = format!("wss://rpc.example.com/ws?api_key={WSS_QUERY_SECRET}");
770 let config = BlockchainDataClientConfig::builder()
771 .chain(Arc::new(chains::ETHEREUM.clone()))
772 .http_rpc_url(http_rpc_url.clone().into())
773 .wss_rpc_url(wss_rpc_url.clone().into())
774 .build();
775
776 let debug = format!("{config:?}");
777
778 assert!(debug.contains("http_rpc_url: <redacted>"));
779 assert!(debug.contains("wss_rpc_url: Some(<redacted>)"));
780 assert!(!debug.contains(HTTP_USERINFO_SECRET));
781 assert!(!debug.contains(WSS_QUERY_SECRET));
782 assert!(!debug.contains(&http_rpc_url));
783 assert!(!debug.contains(&wss_rpc_url));
784 }
785
786 #[rstest]
787 fn test_execution_config_debug_redacts_rpc_url() {
788 const PATH_SECRET: &str = "execution-http-path-secret";
789 const QUERY_SECRET: &str = "execution-http-query-secret";
790 let http_rpc_url = format!("https://rpc.example.com/{PATH_SECRET}?api_key={QUERY_SECRET}");
791 let config = BlockchainExecutionClientConfig::builder()
792 .client_id(AccountId::from("BLOCKCHAIN-001"))
793 .chain(chains::ETHEREUM.clone())
794 .wallet_address("0x0000000000000000000000000000000000000000".to_string())
795 .http_rpc_url(http_rpc_url.clone().into())
796 .signer_private_key_env("BLOCKCHAIN_PRIVATE_KEY".to_string())
797 .router_addresses(vec![
798 "0xE592427A0AEce92De3Edee1F18E0157C05861564".to_string(),
799 ])
800 .weth_address("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1".to_string())
801 .max_fee_per_gas_wei(1_000_000_000)
802 .base_fee_buffer_bps(2_000)
803 .gas_limit(1_000_000)
804 .gas_buffer_bps(2_000)
805 .allowed_token_pairs(vec![(
806 "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1".to_string(),
807 "0xaf88d065e77c8cC2239327C5EDb3A432268e5831".to_string(),
808 )])
809 .slippage_bps(50)
810 .max_slippage_bps(200)
811 .max_order_amount(1_000_000_000_000_000_000)
812 .deadline_seconds(300)
813 .max_quote_age_blocks(100)
814 .receipt_timeout_secs(60)
815 .build();
816
817 let debug = format!("{config:?}");
818
819 assert!(debug.contains("http_rpc_url: <redacted>"));
820 assert!(!debug.contains(PATH_SECRET));
821 assert!(!debug.contains(QUERY_SECRET));
822 assert!(!debug.contains(&http_rpc_url));
823 }
824
825 #[rstest]
826 fn test_data_config_rejects_duplicate_dex_ids() {
827 let config = BlockchainDataClientConfig::builder()
828 .chain(Arc::new(chains::ETHEREUM.clone()))
829 .http_rpc_url("https://rpc.example.com".into())
830 .dex_ids(vec![
831 DexType::UniswapV2,
832 DexType::UniswapV3,
833 DexType::UniswapV2,
834 ])
835 .build();
836
837 assert_eq!(
838 config.validate(),
839 Err(ConfigError::duplicate("dex_ids", None))
840 );
841 }
842
843 #[rstest]
844 #[case("tokens")]
845 #[case("payload_key_retired_env")]
846 #[case("router_addresses")]
847 #[case("allowed_token_pairs")]
848 fn test_execution_config_rejects_duplicate_selections(#[case] field: &str) {
849 let config = BlockchainExecutionClientConfig::builder()
850 .client_id(AccountId::from("BLOCKCHAIN-001"))
851 .chain(chains::ETHEREUM.clone())
852 .wallet_address("0x0000000000000000000000000000000000000000".to_string())
853 .http_rpc_url("https://rpc.example.com".into())
854 .signer_private_key_env("BLOCKCHAIN_PRIVATE_KEY".to_string())
855 .router_addresses(vec!["router-one".to_string()])
856 .weth_address("wrapped-native".to_string())
857 .max_fee_per_gas_wei(1)
858 .base_fee_buffer_bps(1)
859 .gas_limit(1)
860 .gas_buffer_bps(1)
861 .build();
862 let mut input = serde_json::to_value(config).unwrap();
863
864 input[field] = if field == "allowed_token_pairs" {
865 serde_json::json!([["token-one", "token-two"], ["token-one", "token-two"]])
866 } else {
867 serde_json::json!(["first", "second", "first"])
868 };
869
870 let config: BlockchainExecutionClientConfig = serde_json::from_value(input).unwrap();
871
872 assert_eq!(config.validate(), Err(ConfigError::duplicate(field, None)));
873 }
874
875 #[cfg(feature = "hypersync")]
876 #[rstest]
877 #[case::client(false)]
878 #[case::core(true)]
879 fn test_constructor_rejects_duplicate_dexes(#[case] core: bool) {
880 let config = BlockchainDataClientConfig::builder()
881 .chain(Arc::new(chains::ETHEREUM.clone()))
882 .http_rpc_url("https://rpc.example.com".into())
883 .dex_ids(vec![DexType::UniswapV2, DexType::UniswapV2])
884 .build();
885
886 let error = if core {
887 crate::data::core::BlockchainDataClientCore::new(
888 config,
889 None,
890 None,
891 tokio_util::sync::CancellationToken::new(),
892 )
893 .unwrap_err()
894 } else {
895 crate::data::client::BlockchainDataClient::new(ClientId::from("BLOCKCHAIN"), config)
896 .unwrap_err()
897 };
898
899 assert_eq!(
900 error.downcast_ref::<ConfigError>(),
901 Some(&ConfigError::duplicate("dex_ids", None)),
902 );
903 }
904}