fireblocks_solana_signer/
lib.rs

1#![doc = include_str!("../README.md")]
2mod asset;
3mod error;
4mod extensions;
5// mod multi;
6mod signer;
7use solana_sdk::pubkey::Pubkey;
8pub use {
9    asset::*,
10    error::Error,
11    extensions::*,
12    fireblocks_signer_transport::{
13        Client,
14        ClientBuilder,
15        FIREBLOCKS_API,
16        FIREBLOCKS_SANDBOX_API,
17        TransactionResponse,
18        TransactionStatus,
19    },
20    //    multi::*,
21    signer::*,
22    std::str::FromStr,
23};
24
25// pub type DynSigner = dyn multi::MultiSigner;
26
27/// Environment variables used by the FireblocksSigner.
28#[derive(Debug, Clone, Copy)]
29pub enum EnvVar {
30    Vault,
31    Secret,
32    ApiKey,
33    Endpoint,
34    Pubkey,
35    Testnet,
36    Devnet,
37    PollTimeout,
38    PollInterval,
39}
40
41impl std::fmt::Display for EnvVar {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        let name = match self {
44            EnvVar::Vault => "FIREBLOCKS_VAULT",
45            EnvVar::Secret => "FIREBLOCKS_SECRET",
46            EnvVar::ApiKey => "FIREBLOCKS_API_KEY",
47            EnvVar::Endpoint => "FIREBLOCKS_ENDPOINT",
48            EnvVar::Pubkey => "FIREBLOCKS_PUBKEY",
49            EnvVar::Testnet => "FIREBLOCKS_TESTNET",
50            EnvVar::Devnet => "FIREBLOCKS_DEVNET",
51            EnvVar::PollTimeout => "FIREBLOCKS_POLL_TIMEOUT",
52            EnvVar::PollInterval => "FIREBLOCKS_POLL_INTERVAL",
53        };
54        write!(f, "{name}")
55    }
56}
57
58impl AsRef<std::ffi::OsStr> for EnvVar {
59    fn as_ref(&self) -> &std::ffi::OsStr {
60        match self {
61            EnvVar::Vault => std::ffi::OsStr::new("FIREBLOCKS_VAULT"),
62            EnvVar::Secret => std::ffi::OsStr::new("FIREBLOCKS_SECRET"),
63            EnvVar::ApiKey => std::ffi::OsStr::new("FIREBLOCKS_API_KEY"),
64            EnvVar::Endpoint => std::ffi::OsStr::new("FIREBLOCKS_ENDPOINT"),
65            EnvVar::Pubkey => std::ffi::OsStr::new("FIREBLOCKS_PUBKEY"),
66            EnvVar::Testnet => std::ffi::OsStr::new("FIREBLOCKS_TESTNET"),
67            EnvVar::Devnet => std::ffi::OsStr::new("FIREBLOCKS_DEVNET"),
68            EnvVar::PollTimeout => std::ffi::OsStr::new("FIREBLOCKS_POLL_TIMEOUT"),
69            EnvVar::PollInterval => std::ffi::OsStr::new("FIREBLOCKS_POLL_INTERVAL"),
70        }
71    }
72}
73
74impl From<(EnvVar, std::env::VarError)> for Error {
75    fn from((env_var, _): (EnvVar, std::env::VarError)) -> Self {
76        Error::EnvMissing(env_var.to_string())
77    }
78}
79/// A type alias for [`std::result::Result`] with this crate's [`Error`] type.
80pub type Result<T> = std::result::Result<T, Error>;
81pub const DEFAULT_CLIENT_TIMEOUT: u8 = 15;
82
83/// See [`build_client_and_address_blocking_safe`]
84pub fn build_client_safe(builder: ClientBuilder) -> Result<Client> {
85    let (tx, rx) = std::sync::mpsc::channel();
86    std::thread::spawn(move || {
87        if tx.send(builder.build()).is_err() {
88            tracing::error!("Failed to send result back to main thread");
89        }
90    });
91    Ok(rx.recv()??)
92}
93
94/// Builds a Fireblocks client and retrieves the associated Solana address in a
95/// tokio-safe manner.
96///
97/// This function is specifically designed for applications running in a tokio
98/// runtime environment. The underlying `fireblocks_signer_transport::Client`
99/// uses blocking HTTP operations via `reqwest` that can cause panics when
100/// called directly from within a tokio async context. This function
101/// prevents such panics by executing the blocking operations in a separate OS
102/// thread.
103///
104/// # Tokio Runtime Safety
105///
106/// **Important**: This function is primarily intended for programs running
107/// under tokio runtime. The `reqwest` crate's blocking client will panic if
108/// used directly in an async tokio context because it attempts to create a new
109/// tokio runtime while one is already running. This function solves that
110/// problem by:
111///
112/// 1. Spawning a separate OS thread (not a tokio task)
113/// 2. Performing all blocking operations in that thread
114/// 3. Using a channel to safely communicate results back to the main thread
115/// 4. Including timeout handling to prevent indefinite blocking
116///
117/// # Parameters
118///
119/// * `builder` - A configured `ClientBuilder` for creating the Fireblocks
120///   client
121/// * `vault` - The Fireblocks vault ID to use
122/// * `asset` - The asset type (typically Solana) for address derivation
123/// * `address` - Optional pre-existing address string. If `None`, the address
124///   will be fetched from Fireblocks
125///
126/// # Returns
127///
128/// Returns a tuple containing:
129/// * `fireblocks_signer_transport::Client` - The configured Fireblocks client
130/// * `Pubkey` - The Solana public key/address associated with the vault and
131///   asset
132///
133/// # Errors
134///
135/// This function can return various errors:
136/// * `Error::Timeout` - If client initialization takes longer than the
137///   configured timeout
138/// * `Error::ThreadPanic` - If the worker thread panics during initialization
139/// * `Error::ChannelClosed` - If the communication channel closes unexpectedly
140/// * Other `Error` variants from the underlying Fireblocks client or address
141///   parsing
142///
143/// # Example
144///
145/// ```rust,no_run
146/// use fireblocks_solana_signer::{Asset, ClientBuilder, build_client_and_address_blocking_safe};
147///
148/// #[tokio::main]
149/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
150///     let builder = ClientBuilder::new("api_key", b"private_key");
151///     let vault_id = "vault_123".to_string();
152///     let asset = Asset::Sol;
153///
154///     // Safe to call from within tokio runtime
155///     let (client, pubkey) =
156///         build_client_and_address_blocking_safe(builder, vault_id, asset, None)?;
157///
158///     println!("Initialized client with address: {}", pubkey);
159///     Ok(())
160/// }
161/// ```
162pub fn build_client_and_address_blocking_safe(
163    builder: ClientBuilder,
164    vault: String,
165    asset: Asset,
166    address: Option<String>,
167) -> Result<(fireblocks_signer_transport::Client, Pubkey)> {
168    let client = build_client_safe(builder)?;
169    match address {
170        Some(pk) => Ok((client, Pubkey::from_str(&pk)?)),
171        None => {
172            let (tx, rx) = std::sync::mpsc::channel();
173            let handle = std::thread::spawn(move || {
174                let result = match client.address(&vault, &asset) {
175                    Err(e) => Err(crate::Error::from(e)),
176                    Ok(pk) => match Pubkey::from_str(&pk) {
177                        Err(e) => Err(crate::Error::from(e)),
178                        Ok(pk) => Ok((client, pk)),
179                    },
180                };
181                // Don't ignore send errors
182                if tx.send(result).is_err() {
183                    tracing::error!("Failed to send result back to main thread");
184                }
185            });
186            tracing::debug!("waiting for client builder response...");
187
188            // Add timeout to prevent infinite blocking
189            match rx.recv_timeout(std::time::Duration::from_secs(
190                (DEFAULT_CLIENT_TIMEOUT + 5).into(),
191            )) {
192                Ok(result) => Ok(result?),
193                Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
194                    tracing::error!("Client initialization timed out");
195                    Err(Error::Timeout(
196                        "Client initialization timed out".to_string(),
197                    ))
198                }
199                Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
200                    // Check if thread panicked
201                    if let Err(panic_err) = handle.join() {
202                        tracing::error!("Client initialization thread panicked: {panic_err:?}");
203                        Err(Error::ThreadPanic(
204                            "Client initialization thread panicked".to_string(),
205                        ))
206                    } else {
207                        Err(Error::ChannelClosed(
208                            "Channel disconnected unexpectedly".to_string(),
209                        ))
210                    }
211                }
212            }
213        }
214    }
215}