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}