taceo_nodes_common/lib.rs
1#![deny(missing_docs)]
2#![deny(clippy::all, clippy::pedantic)]
3#![deny(
4 clippy::allow_attributes_without_reason,
5 clippy::assertions_on_result_states,
6 clippy::dbg_macro,
7 clippy::decimal_literal_representation,
8 clippy::exhaustive_enums,
9 clippy::exhaustive_structs,
10 clippy::iter_over_hash_type,
11 clippy::let_underscore_must_use,
12 clippy::missing_assert_message,
13 clippy::print_stderr,
14 clippy::print_stdout,
15 clippy::undocumented_unsafe_blocks,
16 clippy::unnecessary_safety_comment,
17 clippy::unwrap_used
18)]
19#![allow(clippy::needless_pass_by_value, reason = "Needed for axum")]
20//! Common utilities for MPC-node services.
21//!
22//! This crate provides building blocks shared across nodes in the MPC network.
23//!
24//! * [`Environment`] – represents the deployment environment (prod / staging / test).
25//! * [`StartedServices`] – tracks whether all async background services have started,
26//! used to drive the `/health` endpoint.
27//! * [`spawn_shutdown_task`] / [`default_shutdown_signal`] – wiring for graceful shutdown
28//! via `CTRL+C` or `SIGTERM`.
29//! * [`version_info!`] – macro that returns a version string containing the crate name,
30//! semver version, and git hash.
31//!
32//! # Optional Features
33//!
34//! * `api` (enabled by default) – exposes `/health` and `/version` Axum endpoints.
35//! * `serde` (enabled by default) – ser/de implementation for [`Environment`].
36//! * `aws` (enabled by default) – adds a method to create a localstack configuration used for testing.
37//! * `postgres` (enabled by default) – [`postgres::PostgresConfig`] and [`postgres::pg_pool_with_schema`] for creating a `sqlx` connection pool pinned to a schema, with configurable retry behaviour."
38//! * `alloy` (enabled by default) – [`web3::RpcProvider`], [`web3::RpcProviderBuilder`], and [`web3::RpcProviderConfig`] for building HTTP + WebSocket Ethereum RPC providers with automatic retry and failover, plus ERC-165 interface detection utilities.
39
40use core::fmt;
41use std::sync::{
42 Arc, Mutex,
43 atomic::{AtomicBool, Ordering},
44};
45
46#[cfg(feature = "serde")]
47use serde::{Deserialize, Serialize};
48use tokio::signal;
49use tokio_util::sync::CancellationToken;
50
51pub use git_version;
52
53#[cfg(feature = "api")]
54/// See [`api::routes`] and [`api::routes_with_services`].
55pub mod api;
56#[cfg(feature = "postgres")]
57pub mod postgres;
58#[cfg(feature = "alloy")]
59pub mod web3;
60
61/// The environment the service is running in.
62///
63/// Main usage for the `Environment` is to call
64/// [`Environment::assert_is_dev`]. Services that are intended
65/// for `dev` only (like local secret-manager,...)
66/// shall assert that they are called from the `dev` environment.
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
68#[allow(
69 clippy::exhaustive_enums,
70 reason = "We only expect those four environments at the moment. Changing that is a breaking change."
71)]
72#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
73#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
74pub enum Environment {
75 /// Production environment.
76 Prod,
77 /// Staging environment.
78 Stage,
79 /// Test environment. Used for deployed test nets not for local testing. Use `Dev` instead for local testing.
80 Test,
81 /// Local dev environment.
82 Dev,
83}
84
85impl fmt::Display for Environment {
86 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87 let str = match self {
88 Environment::Prod => "prod",
89 Environment::Stage => "stage",
90 Environment::Test => "test",
91 Environment::Dev => "dev",
92 };
93 f.write_str(str)
94 }
95}
96
97impl Environment {
98 /// Asserts that the environment is the dev environment.
99 ///
100 /// # Panics
101 ///
102 /// Panics with `"Is not dev environment"` if `self` is not `Environment::Dev`.
103 pub fn assert_is_dev(&self) {
104 assert!(self.is_dev(), "Is not dev environment");
105 }
106
107 /// Returns `true` if the environment is the test environment.
108 #[must_use]
109 pub fn is_dev(&self) -> bool {
110 matches!(self, Environment::Dev)
111 }
112
113 /// Returns `true` if the environment is not the test environment.
114 #[must_use]
115 pub fn is_not_dev(&self) -> bool {
116 !self.is_dev()
117 }
118}
119
120/// Macro to generate version information including the crate name, version, and git hash.
121#[macro_export]
122macro_rules! version_info {
123 () => {
124 format!(
125 "{} {} ({})",
126 env!("CARGO_PKG_NAME"),
127 env!("CARGO_PKG_VERSION"),
128 option_env!("GIT_HASH")
129 .unwrap_or($crate::git_version::git_version!(fallback = "UNKNOWN"))
130 )
131 };
132}
133
134/// A struct that keeps track of the health of all async services started by the service.
135///
136/// Relevant for the `/health` route. Implementations should call [`StartedServices::new_service`] for their services and set the bool to `true` if the service started successfully.
137#[derive(Debug, Clone, Default)]
138pub struct StartedServices {
139 external_service: Arc<Mutex<Vec<Arc<AtomicBool>>>>,
140}
141
142impl StartedServices {
143 /// Initializes all services as not started.
144 #[must_use]
145 pub fn new() -> Self {
146 Self::default()
147 }
148
149 /// Adds a new external service to the bookkeeping struct.
150 ///
151 /// Implementations should call this method for every async task that they start. The returned `AtomicBool` should then be set to `true` if the service is ready.
152 #[must_use]
153 #[allow(clippy::missing_panics_doc, reason = "Ok to panic for lock poisoning")]
154 pub fn new_service(&self) -> Arc<AtomicBool> {
155 let service = Arc::new(AtomicBool::default());
156 self.external_service
157 .lock()
158 .expect("Not poisoned")
159 .push(Arc::clone(&service));
160 service
161 }
162
163 /// Returns `true` if all services did start. If there are no services started, this will also return `true`.
164 #[must_use]
165 #[allow(clippy::missing_panics_doc, reason = "Ok to panic for lock poisoning")]
166 pub fn all_started(&self) -> bool {
167 self.external_service
168 .lock()
169 .expect("Not poisoned")
170 .iter()
171 .all(|service| service.load(Ordering::Relaxed))
172 }
173}
174
175/// Spawns a shutdown task and creates an associated [`CancellationToken`](https://docs.rs/tokio-util/latest/tokio_util/sync/struct.CancellationToken.html). This task will complete when either the provided `shutdown_signal` futures completes or if some other tasks cancels the shutdown token. The associated shutdown token will be cancelled either way.
176///
177/// Waiting for the shutdown token is the preferred way to wait for termination.
178pub fn spawn_shutdown_task(
179 shutdown_signal: impl Future<Output = ()> + Send + 'static,
180) -> (CancellationToken, Arc<AtomicBool>) {
181 let cancellation_token = CancellationToken::new();
182 let is_graceful = Arc::new(AtomicBool::new(false));
183 let task_token = cancellation_token.clone();
184 tokio::task::spawn({
185 let is_graceful = Arc::clone(&is_graceful);
186 async move {
187 let _drop_guard = task_token.drop_guard_ref();
188 tokio::select! {
189 () = shutdown_signal => {
190 tracing::info!("received graceful shutdown");
191 is_graceful.store(true, Ordering::Relaxed);
192 task_token.cancel();
193 }
194 () = task_token.cancelled() => {}
195 }
196 }
197 });
198 (cancellation_token, is_graceful)
199}
200
201/// Returns a future that completes when the application should shut down.
202///
203/// On most systems, it completes when the user presses `CTRL+C`.
204/// On Unix platforms, it also responds to the `SIGTERM` signal.
205///
206/// # Panics
207///
208/// Panics if the `CTRL+C` or `SIGTERM` signal handlers cannot be installed.
209pub async fn default_shutdown_signal() {
210 let ctrl_c = async {
211 signal::ctrl_c()
212 .await
213 .expect("failed to install Ctrl+C handler");
214 };
215
216 #[cfg(unix)]
217 let terminate = async {
218 signal::unix::signal(signal::unix::SignalKind::terminate())
219 .expect("failed to install signal handler")
220 .recv()
221 .await;
222 };
223
224 #[cfg(not(unix))]
225 let terminate = std::future::pending::<()>();
226
227 tokio::select! {
228 () = ctrl_c => {},
229 () = terminate => {},
230 }
231}
232
233#[cfg(feature = "aws")]
234/// Creates an AWS SDK configuration for connecting to a `LocalStack` instance.
235///
236/// This function is designed to facilitate testing and development by configuring
237/// an AWS SDK client to connect to a `LocalStack` instance. It sets the region to
238/// `us-east-1` and uses static test credentials. The endpoint URL can be customized
239/// via the `TEST_AWS_ENDPOINT_URL` environment variable; if not set, it defaults
240/// to `http://localhost:4566`.
241pub async fn localstack_aws_config() -> aws_config::SdkConfig {
242 use aws_config::Region;
243 use aws_sdk_secretsmanager::config::Credentials;
244 let region_provider = Region::new("us-east-1");
245 let credentials = Credentials::new("test", "test", None, None, "Static");
246 // in case we don't want the standard url, we can configure it via the environment
247 aws_config::from_env()
248 .region(region_provider)
249 .endpoint_url(
250 std::env::var("TEST_AWS_ENDPOINT_URL").unwrap_or("http://localhost:4566".to_string()),
251 )
252 .credentials_provider(credentials)
253 .load()
254 .await
255}