tendermint_config/
config.rs

1//! Tendermint configuration file types (with serde parsers/serializers)
2//!
3//! This module contains types which correspond to the following config files:
4//!
5//! - `config.toml`: `config::TendermintConfig`
6//! - `node_key.rs`: `config::node_key::NodeKey`
7//! - `priv_validator_key.rs`: `config::priv_validator_key::PrivValidatorKey`
8
9use alloc::collections::{btree_map, BTreeMap};
10use core::{fmt, str::FromStr};
11use std::{
12    fs,
13    path::{Path, PathBuf},
14};
15
16use serde::{de, de::Error as _, ser, Deserialize, Serialize};
17use tendermint::{genesis::Genesis, node, Moniker, Timeout};
18
19use crate::{net, node_key::NodeKey, prelude::*, Error};
20
21/// Tendermint `config.toml` file
22#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
23pub struct TendermintConfig {
24    /// TCP or UNIX socket address of the ABCI application,
25    /// or the name of an ABCI application compiled in with the Tendermint binary.
26    pub proxy_app: net::Address,
27
28    /// A custom human readable name for this node
29    pub moniker: Moniker,
30
31    /// If this node is many blocks behind the tip of the chain, FastSync
32    /// allows them to catchup quickly by downloading blocks in parallel
33    /// and verifying their commits
34    pub fast_sync: bool,
35
36    /// Database backend: `goleveldb | cleveldb | boltdb | rocksdb | badgerdb`
37    pub db_backend: DbBackend,
38
39    /// Database directory
40    pub db_dir: PathBuf,
41
42    /// Output level for logging, including package level options
43    pub log_level: LogLevel,
44
45    /// Output format: 'plain' (colored text) or 'json'
46    pub log_format: LogFormat,
47
48    /// Path to the JSON file containing the initial validator set and other meta data
49    pub genesis_file: PathBuf,
50
51    /// Path to the JSON file containing the private key to use as a validator in the consensus
52    /// protocol
53    pub priv_validator_key_file: Option<PathBuf>,
54
55    /// Path to the JSON file containing the last sign state of a validator
56    pub priv_validator_state_file: PathBuf,
57
58    /// TCP or UNIX socket address for Tendermint to listen on for
59    /// connections from an external PrivValidator process
60    #[serde(
61        deserialize_with = "deserialize_optional_value",
62        serialize_with = "serialize_optional_value"
63    )]
64    pub priv_validator_laddr: Option<net::Address>,
65
66    /// Path to the JSON file containing the private key to use for node authentication in the p2p
67    /// protocol
68    pub node_key_file: PathBuf,
69
70    /// Mechanism to connect to the ABCI application: socket | grpc
71    pub abci: AbciMode,
72
73    /// If `true`, query the ABCI app on connecting to a new peer
74    /// so the app can decide if we should keep the connection or not
75    pub filter_peers: bool,
76
77    /// rpc server configuration options
78    pub rpc: RpcConfig,
79
80    /// peer to peer configuration options
81    pub p2p: P2PConfig,
82
83    /// mempool configuration options
84    pub mempool: MempoolConfig,
85
86    /// consensus configuration options
87    pub consensus: ConsensusConfig,
88
89    /// Storage configuration options. This section was only first made
90    /// available in Tendermint Core v0.34.21.
91    #[serde(default)]
92    pub storage: StorageConfig,
93
94    /// transactions indexer configuration options
95    pub tx_index: TxIndexConfig,
96
97    /// instrumentation configuration options
98    pub instrumentation: InstrumentationConfig,
99
100    /// statesync configuration options
101    pub statesync: StatesyncConfig,
102
103    /// fastsync configuration options
104    pub fastsync: FastsyncConfig,
105}
106
107impl TendermintConfig {
108    /// Parse Tendermint `config.toml`
109    pub fn parse_toml<T: AsRef<str>>(toml_string: T) -> Result<Self, Error> {
110        let res = toml::from_str(toml_string.as_ref()).map_err(Error::toml)?;
111
112        Ok(res)
113    }
114
115    /// Load `config.toml` from a file
116    pub fn load_toml_file<P>(path: &P) -> Result<Self, Error>
117    where
118        P: AsRef<Path>,
119    {
120        let toml_string = fs::read_to_string(path)
121            .map_err(|e| Error::file_io(format!("{}", path.as_ref().display()), e))?;
122
123        Self::parse_toml(toml_string)
124    }
125
126    /// Load `genesis.json` file from the configured location
127    pub fn load_genesis_file(&self, home: impl AsRef<Path>) -> Result<Genesis, Error> {
128        let path = home.as_ref().join(&self.genesis_file);
129        let genesis_json = fs::read_to_string(&path)
130            .map_err(|e| Error::file_io(format!("{}", path.display()), e))?;
131
132        let res = serde_json::from_str(genesis_json.as_ref()).map_err(Error::serde_json)?;
133
134        Ok(res)
135    }
136
137    /// Load `node_key.json` file from the configured location
138    pub fn load_node_key(&self, home: impl AsRef<Path>) -> Result<NodeKey, Error> {
139        let path = home.as_ref().join(&self.node_key_file);
140        NodeKey::load_json_file(&path)
141    }
142}
143
144/// Database backend
145#[derive(Copy, Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
146pub enum DbBackend {
147    /// GoLevelDB backend
148    #[serde(rename = "goleveldb")]
149    GoLevelDb,
150
151    /// CLevelDB backend
152    #[serde(rename = "cleveldb")]
153    CLevelDb,
154
155    /// BoltDB backend
156    #[serde(rename = "boltdb")]
157    BoltDb,
158
159    /// RocksDB backend
160    #[serde(rename = "rocksdb")]
161    RocksDb,
162
163    /// BadgerDB backend
164    #[serde(rename = "badgerdb")]
165    BadgerDb,
166}
167
168/// Loglevel configuration
169#[derive(Clone, Debug, Eq, PartialEq)]
170pub struct LogLevel {
171    /// A global log level
172    pub global: Option<String>,
173    components: BTreeMap<String, String>,
174}
175
176impl LogLevel {
177    /// Get the setting for the given key. If not found, returns the global setting, if any.
178    pub fn get<S>(&self, key: S) -> Option<&str>
179    where
180        S: AsRef<str>,
181    {
182        self.components
183            .get(key.as_ref())
184            .or(self.global.as_ref())
185            .map(AsRef::as_ref)
186    }
187
188    /// Iterate over the levels. This doesn't include the global setting, if any.
189    pub fn iter(&self) -> LogLevelIter<'_> {
190        self.components.iter()
191    }
192}
193
194/// Iterator over log levels
195pub type LogLevelIter<'a> = btree_map::Iter<'a, String, String>;
196
197impl FromStr for LogLevel {
198    type Err = Error;
199
200    fn from_str(s: &str) -> Result<Self, Self::Err> {
201        let mut global = None;
202        let mut components = BTreeMap::new();
203
204        for level in s.split(',') {
205            let parts = level.split(':').collect::<Vec<_>>();
206
207            if parts.len() == 1 {
208                global = Some(parts[0].to_owned());
209                continue;
210            } else if parts.len() != 2 {
211                return Err(Error::parse(format!("error parsing log level: {level}")));
212            }
213
214            let key = parts[0].to_owned();
215            let value = parts[1].to_owned();
216
217            if components.insert(key, value).is_some() {
218                return Err(Error::parse(format!(
219                    "duplicate log level setting for: {level}"
220                )));
221            }
222        }
223
224        Ok(LogLevel { global, components })
225    }
226}
227
228impl fmt::Display for LogLevel {
229    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
230        if let Some(global) = &self.global {
231            write!(f, "{global}")?;
232            if !self.components.is_empty() {
233                write!(f, ",")?;
234            }
235        }
236        for (i, (k, v)) in self.components.iter().enumerate() {
237            write!(f, "{k}:{v}")?;
238
239            if i < self.components.len() - 1 {
240                write!(f, ",")?;
241            }
242        }
243
244        Ok(())
245    }
246}
247
248impl<'de> Deserialize<'de> for LogLevel {
249    fn deserialize<D: de::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
250        let levels = String::deserialize(deserializer)?;
251        Self::from_str(&levels).map_err(|e| D::Error::custom(format!("{e}")))
252    }
253}
254
255impl Serialize for LogLevel {
256    fn serialize<S: ser::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
257        self.to_string().serialize(serializer)
258    }
259}
260
261/// Logging format
262#[derive(Copy, Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
263pub enum LogFormat {
264    /// Plain (colored text)
265    #[serde(rename = "plain")]
266    Plain,
267
268    /// JSON
269    #[serde(rename = "json")]
270    Json,
271}
272
273/// Mechanism to connect to the ABCI application: socket | grpc
274#[derive(Copy, Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
275pub enum AbciMode {
276    /// Socket
277    #[serde(rename = "socket")]
278    Socket,
279
280    /// GRPC
281    #[serde(rename = "grpc")]
282    Grpc,
283}
284
285/// Tendermint `config.toml` file's `[rpc]` section
286#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
287pub struct RpcConfig {
288    /// TCP or UNIX socket address for the RPC server to listen on
289    pub laddr: net::Address,
290
291    /// A list of origins a cross-domain request can be executed from
292    /// Default value `[]` disables cors support
293    /// Use `["*"]` to allow any origin
294    pub cors_allowed_origins: Vec<CorsOrigin>,
295
296    /// A list of methods the client is allowed to use with cross-domain requests
297    pub cors_allowed_methods: Vec<CorsMethod>,
298
299    /// A list of non simple headers the client is allowed to use with cross-domain requests
300    pub cors_allowed_headers: Vec<CorsHeader>,
301
302    /// TCP or UNIX socket address for the gRPC server to listen on
303    /// NOTE: This server only supports `/broadcast_tx_commit`
304    #[serde(
305        deserialize_with = "deserialize_optional_value",
306        serialize_with = "serialize_optional_value"
307    )]
308    pub grpc_laddr: Option<net::Address>,
309
310    /// Maximum number of simultaneous GRPC connections.
311    /// Does not include RPC (HTTP&WebSocket) connections. See `max_open_connections`.
312    pub grpc_max_open_connections: u64,
313
314    /// Activate unsafe RPC commands like `/dial_seeds` and `/unsafe_flush_mempool`
315    #[serde(rename = "unsafe")]
316    pub unsafe_commands: bool,
317
318    /// Maximum number of simultaneous connections (including WebSocket).
319    /// Does not include gRPC connections. See `grpc_max_open_connections`.
320    pub max_open_connections: u64,
321
322    /// Maximum number of unique clientIDs that can `/subscribe`.
323    pub max_subscription_clients: u64,
324
325    /// Maximum number of unique queries a given client can `/subscribe` to.
326    pub max_subscriptions_per_client: u64,
327
328    /// How long to wait for a tx to be committed during `/broadcast_tx_commit`.
329    pub timeout_broadcast_tx_commit: Timeout,
330
331    /// Maximum size of request body, in bytes
332    pub max_body_bytes: u64,
333
334    /// Maximum size of request header, in bytes
335    pub max_header_bytes: u64,
336
337    /// The name of a file containing certificate that is used to create the HTTPS server.
338    #[serde(
339        deserialize_with = "deserialize_optional_value",
340        serialize_with = "serialize_optional_value"
341    )]
342    pub tls_cert_file: Option<PathBuf>,
343
344    /// The name of a file containing matching private key that is used to create the HTTPS server.
345    #[serde(
346        deserialize_with = "deserialize_optional_value",
347        serialize_with = "serialize_optional_value"
348    )]
349    pub tls_key_file: Option<PathBuf>,
350
351    /// pprof listen address <https://golang.org/pkg/net/http/pprof>
352    #[serde(
353        deserialize_with = "deserialize_optional_value",
354        serialize_with = "serialize_optional_value"
355    )]
356    pub pprof_laddr: Option<net::Address>,
357}
358
359/// Origin hosts allowed with CORS requests to the RPC API
360// TODO(tarcieri): parse and validate this string
361#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
362pub struct CorsOrigin(String);
363
364impl AsRef<str> for CorsOrigin {
365    fn as_ref(&self) -> &str {
366        self.0.as_ref()
367    }
368}
369
370impl fmt::Display for CorsOrigin {
371    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
372        write!(f, "{}", &self.0)
373    }
374}
375
376/// HTTP methods allowed with CORS requests to the RPC API
377// TODO(tarcieri): parse and validate this string
378#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
379pub struct CorsMethod(String);
380
381impl AsRef<str> for CorsMethod {
382    fn as_ref(&self) -> &str {
383        self.0.as_ref()
384    }
385}
386
387impl fmt::Display for CorsMethod {
388    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
389        write!(f, "{}", &self.0)
390    }
391}
392
393/// HTTP headers allowed to be sent via CORS to the RPC API
394// TODO(tarcieri): parse and validate this string
395#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
396pub struct CorsHeader(String);
397
398impl AsRef<str> for CorsHeader {
399    fn as_ref(&self) -> &str {
400        self.0.as_ref()
401    }
402}
403
404impl fmt::Display for CorsHeader {
405    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
406        write!(f, "{}", &self.0)
407    }
408}
409
410/// peer to peer configuration options
411#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
412pub struct P2PConfig {
413    /// Address to listen for incoming connections
414    pub laddr: net::Address,
415
416    /// Address to advertise to peers for them to dial
417    /// If empty, will use the same port as the laddr,
418    /// and will introspect on the listener or use UPnP
419    /// to figure out the address.
420    #[serde(
421        deserialize_with = "deserialize_optional_value",
422        serialize_with = "serialize_optional_value"
423    )]
424    pub external_address: Option<net::Address>,
425
426    /// Comma separated list of seed nodes to connect to
427    #[serde(
428        serialize_with = "serialize_comma_separated_list",
429        deserialize_with = "deserialize_comma_separated_list"
430    )]
431    pub seeds: Vec<net::Address>,
432
433    /// Comma separated list of nodes to keep persistent connections to
434    #[serde(
435        serialize_with = "serialize_comma_separated_list",
436        deserialize_with = "deserialize_comma_separated_list"
437    )]
438    pub persistent_peers: Vec<net::Address>,
439
440    /// UPNP port forwarding
441    pub upnp: bool,
442
443    /// Path to address book
444    pub addr_book_file: PathBuf,
445
446    /// Set `true` for strict address routability rules
447    /// Set `false` for private or local networks
448    pub addr_book_strict: bool,
449
450    /// Maximum number of inbound peers
451    pub max_num_inbound_peers: u64,
452
453    /// Maximum number of outbound peers to connect to, excluding persistent peers
454    pub max_num_outbound_peers: u64,
455
456    /// List of node IDs, to which a connection will be (re)established ignoring any existing
457    /// limits
458    #[serde(
459        serialize_with = "serialize_comma_separated_list",
460        deserialize_with = "deserialize_comma_separated_list"
461    )]
462    pub unconditional_peer_ids: Vec<node::Id>,
463
464    /// Maximum pause when redialing a persistent peer (if zero, exponential backoff is used)
465    pub persistent_peers_max_dial_period: Timeout,
466
467    /// Time to wait before flushing messages out on the connection
468    pub flush_throttle_timeout: Timeout,
469
470    /// Maximum size of a message packet payload, in bytes
471    pub max_packet_msg_payload_size: u64,
472
473    /// Rate at which packets can be sent, in bytes/second
474    pub send_rate: TransferRate,
475
476    /// Rate at which packets can be received, in bytes/second
477    pub recv_rate: TransferRate,
478
479    /// Set `true` to enable the peer-exchange reactor
480    pub pex: bool,
481
482    /// Seed mode, in which node constantly crawls the network and looks for
483    /// peers. If another node asks it for addresses, it responds and disconnects.
484    ///
485    /// Does not work if the peer-exchange reactor is disabled.
486    pub seed_mode: bool,
487
488    /// Comma separated list of peer IDs to keep private (will not be gossiped to other peers)
489    #[serde(
490        serialize_with = "serialize_comma_separated_list",
491        deserialize_with = "deserialize_comma_separated_list"
492    )]
493    pub private_peer_ids: Vec<node::Id>,
494
495    /// Toggle to disable guard against peers connecting from the same ip.
496    pub allow_duplicate_ip: bool,
497
498    /// Handshake timeout
499    pub handshake_timeout: Timeout,
500
501    /// Timeout when dialing other peers
502    pub dial_timeout: Timeout,
503}
504
505/// mempool configuration options
506#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
507pub struct MempoolConfig {
508    /// Recheck enabled
509    pub recheck: bool,
510
511    /// Broadcast enabled
512    pub broadcast: bool,
513
514    /// WAL dir
515    #[serde(
516        deserialize_with = "deserialize_optional_value",
517        serialize_with = "serialize_optional_value"
518    )]
519    pub wal_dir: Option<PathBuf>,
520
521    /// Maximum number of transactions in the mempool
522    pub size: u64,
523
524    /// Limit the total size of all txs in the mempool.
525    /// This only accounts for raw transactions (e.g. given 1MB transactions and
526    /// `max_txs_bytes`=5MB, mempool will only accept 5 transactions).
527    pub max_txs_bytes: u64,
528
529    /// Size of the cache (used to filter transactions we saw earlier) in transactions
530    pub cache_size: u64,
531
532    /// Do not remove invalid transactions from the cache (default: false)
533    /// Set to true if it's not possible for any invalid transaction to become valid
534    /// again in the future.
535    #[serde(rename = "keep-invalid-txs-in-cache")]
536    pub keep_invalid_txs_in_cache: bool,
537
538    /// Maximum size of a single transaction.
539    /// NOTE: the max size of a tx transmitted over the network is {max-tx-bytes}.
540    pub max_tx_bytes: u64,
541
542    /// Maximum size of a batch of transactions to send to a peer
543    /// Including space needed by encoding (one varint per transaction).
544    /// XXX: Unused due to <https://github.com/tendermint/tendermint/issues/5796>
545    pub max_batch_bytes: u64,
546}
547
548/// consensus configuration options
549#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
550pub struct ConsensusConfig {
551    /// Path to WAL file
552    pub wal_file: PathBuf,
553
554    /// Propose timeout
555    pub timeout_propose: Timeout,
556
557    /// Propose timeout delta
558    pub timeout_propose_delta: Timeout,
559
560    /// Prevote timeout
561    pub timeout_prevote: Timeout,
562
563    /// Prevote timeout delta
564    pub timeout_prevote_delta: Timeout,
565
566    /// Precommit timeout
567    pub timeout_precommit: Timeout,
568
569    /// Precommit timeout delta
570    pub timeout_precommit_delta: Timeout,
571
572    /// Commit timeout
573    pub timeout_commit: Timeout,
574
575    /// How many blocks to look back to check existence of the node's consensus votes before
576    /// joining consensus When non-zero, the node will panic upon restart
577    /// if the same consensus key was used to sign {double-sign-check-height} last blocks.
578    /// So, validators should stop the state machine, wait for some blocks, and then restart the
579    /// state machine to avoid panic.
580    pub double_sign_check_height: u64,
581
582    /// Make progress as soon as we have all the precommits (as if TimeoutCommit = 0)
583    pub skip_timeout_commit: bool,
584
585    /// EmptyBlocks mode
586    pub create_empty_blocks: bool,
587
588    /// Interval between empty blocks
589    pub create_empty_blocks_interval: Timeout,
590
591    /// Reactor sleep duration
592    pub peer_gossip_sleep_duration: Timeout,
593
594    /// Reactor query sleep duration
595    pub peer_query_maj23_sleep_duration: Timeout,
596}
597
598/// Storage configuration options.
599#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Default)]
600pub struct StorageConfig {
601    #[serde(default)]
602    pub discard_abci_responses: bool,
603}
604
605/// transactions indexer configuration options
606#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
607pub struct TxIndexConfig {
608    /// What indexer to use for transactions
609    #[serde(default)]
610    pub indexer: TxIndexer,
611}
612
613/// What indexer to use for transactions
614#[derive(Copy, Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, Default)]
615pub enum TxIndexer {
616    /// "null"
617    // TODO(tarcieri): use an `Option` type here?
618    #[serde(rename = "null")]
619    Null,
620
621    /// "kv" (default) - the simplest possible indexer, backed by key-value storage (defaults to
622    /// levelDB; see DBBackend).
623    #[serde(rename = "kv")]
624    #[default]
625    Kv,
626}
627
628/// instrumentation configuration options
629#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
630pub struct InstrumentationConfig {
631    /// When `true`, Prometheus metrics are served under /metrics on
632    /// PrometheusListenAddr.
633    pub prometheus: bool,
634
635    /// Address to listen for Prometheus collector(s) connections
636    // TODO(tarcieri): parse to `tendermint::net::Addr`
637    pub prometheus_listen_addr: String,
638
639    /// Maximum number of simultaneous connections.
640    pub max_open_connections: u64,
641
642    /// Instrumentation namespace
643    pub namespace: String,
644}
645
646/// statesync configuration options
647#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
648pub struct StatesyncConfig {
649    /// State sync rapidly bootstraps a new node by discovering, fetching, and restoring a state
650    /// machine snapshot from peers instead of fetching and replaying historical blocks.
651    /// Requires some peers in the network to take and serve state machine snapshots. State
652    /// sync is not attempted if the node has any local state (LastBlockHeight > 0). The node
653    /// will have a truncated block history, starting from the height of the snapshot.
654    pub enable: bool,
655
656    /// RPC servers (comma-separated) for light client verification of the synced state machine and
657    /// retrieval of state data for node bootstrapping. Also needs a trusted height and
658    /// corresponding header hash obtained from a trusted source, and a period during which
659    /// validators can be trusted.
660    ///
661    /// For Cosmos SDK-based chains, trust-period should usually be about 2/3 of the unbonding time
662    /// (~2 weeks) during which they can be financially punished (slashed) for misbehavior.
663    #[serde(
664        serialize_with = "serialize_comma_separated_list",
665        deserialize_with = "deserialize_comma_separated_list"
666    )]
667    pub rpc_servers: Vec<String>,
668
669    /// Trust height. See `rpc_servers` above.
670    pub trust_height: u64,
671
672    /// Trust hash. See `rpc_servers` above.
673    pub trust_hash: String,
674
675    /// Trust period. See `rpc_servers` above.
676    pub trust_period: String,
677
678    /// Time to spend discovering snapshots before initiating a restore.
679    pub discovery_time: Timeout,
680
681    /// Temporary directory for state sync snapshot chunks, defaults to the OS tempdir (typically
682    /// /tmp). Will create a new, randomly named directory within, and remove it when done.
683    pub temp_dir: String,
684}
685
686/// fastsync configuration options
687#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
688pub struct FastsyncConfig {
689    /// Fast Sync version to use:
690    ///   1) "v0" (default) - the legacy fast sync implementation
691    ///   2) "v2" - complete redesign of v0, optimized for testability & readability
692    pub version: String,
693}
694
695/// Rate at which bytes can be sent/received
696#[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
697pub struct TransferRate(u64);
698
699impl TransferRate {
700    /// Get the transfer rate in bytes per second
701    pub fn bytes_per_sec(self) -> u64 {
702        self.0
703    }
704}
705
706/// Deserialize `Option<T: FromStr>` where an empty string indicates `None`
707fn deserialize_optional_value<'de, D, T, E>(deserializer: D) -> Result<Option<T>, D::Error>
708where
709    D: de::Deserializer<'de>,
710    T: FromStr<Err = E>,
711    E: fmt::Display,
712{
713    let string = Option::<String>::deserialize(deserializer).map(|str| str.unwrap_or_default())?;
714
715    if string.is_empty() {
716        return Ok(None);
717    }
718
719    string
720        .parse()
721        .map(Some)
722        .map_err(|e| D::Error::custom(format!("{e}")))
723}
724
725fn serialize_optional_value<S, T>(value: &Option<T>, serializer: S) -> Result<S::Ok, S::Error>
726where
727    S: ser::Serializer,
728    T: Serialize,
729{
730    match value {
731        Some(value) => value.serialize(serializer),
732        None => "".serialize(serializer),
733    }
734}
735
736/// Deserialize a comma separated list of types that impl `FromStr` as a `Vec`
737fn deserialize_comma_separated_list<'de, D, T, E>(deserializer: D) -> Result<Vec<T>, D::Error>
738where
739    D: de::Deserializer<'de>,
740    T: FromStr<Err = E>,
741    E: fmt::Display,
742{
743    let mut result = vec![];
744    let string = String::deserialize(deserializer)?;
745
746    if string.is_empty() {
747        return Ok(result);
748    }
749
750    for item in string.split(',') {
751        result.push(item.parse().map_err(|e| D::Error::custom(format!("{e}")))?);
752    }
753
754    Ok(result)
755}
756
757/// Serialize a comma separated list types that impl `ToString`
758fn serialize_comma_separated_list<S, T>(list: &[T], serializer: S) -> Result<S::Ok, S::Error>
759where
760    S: ser::Serializer,
761    T: ToString,
762{
763    let str_list = list.iter().map(|addr| addr.to_string()).collect::<Vec<_>>();
764    str_list.join(",").serialize(serializer)
765}