mod jwtclient;
mod types;
use types::{TransactionArguments, TransactionDetails, TransactionStatus};
mod api;
use api::FireblocksClient;
mod signer;
mod middleware;
pub use middleware::FireblocksMiddleware;
use ethers_core::types::Address;
use jsonwebtoken::EncodingKey;
use std::{collections::HashMap, time::Instant};
use thiserror::Error;
pub(crate) type Result<T> = std::result::Result<T, FireblocksError>;
#[derive(Debug, Error)]
pub enum FireblocksError {
#[error(transparent)]
JwtError(#[from] jwtclient::JwtError),
#[error(transparent)]
JwtParseError(#[from] jsonwebtoken::errors::Error),
#[error(transparent)]
IoError(#[from] std::io::Error),
#[error(transparent)]
ReqwestError(#[from] reqwest::Error),
#[error("Deserialization Error: {err}. Response: {text}")]
SerdeJson {
err: serde_json::Error,
text: String,
},
#[error(
"Transaction was not completed successfully. Final Status: {:?}. Sub status: {1}",
0
)]
TxError(TransactionStatus, String),
#[error("Could not parse data: {0}")]
ParseError(String),
#[error("Timed out while waiting for user to approve transaction")]
Timeout,
}
#[derive(Debug, Clone)]
pub struct FireblocksSigner {
fireblocks: FireblocksClient,
account_ids: HashMap<Address, String>,
chain_id: u64,
asset_id: String,
address: Address,
account_id: String,
timeout: u128,
}
pub struct Config {
pub key: EncodingKey,
pub api_key: String,
pub chain_id: u64,
pub account_id: String,
}
impl Config {
pub fn new<T: AsRef<str>>(
key: T,
api_key: &str,
account_id: &str,
chain_id: u64,
) -> Result<Self> {
let rsa_pem = std::fs::read(key.as_ref())?;
let key = EncodingKey::from_rsa_pem(&rsa_pem)?;
Ok(Self {
key,
chain_id,
api_key: api_key.to_string(),
account_id: account_id.to_string(),
})
}
}
impl AsRef<FireblocksClient> for FireblocksSigner {
fn as_ref(&self) -> &FireblocksClient {
&self.fireblocks
}
}
impl FireblocksSigner {
pub async fn new(cfg: Config) -> Self {
let fireblocks = FireblocksClient::new(cfg.key, &cfg.api_key);
let asset_id = match cfg.chain_id {
1 => "ETH",
3 => "ETH_TEST",
42 => "ETH_TEST2",
_ => panic!("Unsupported chain_id"),
};
let res = fireblocks
.vault_addresses(&cfg.account_id, asset_id)
.await
.expect("could not get vault addrs");
Self {
fireblocks,
account_ids: HashMap::new(),
chain_id: cfg.chain_id,
asset_id: asset_id.to_owned(),
address: res[0].address[2..]
.parse()
.expect("could not parse as address"),
account_id: cfg.account_id,
timeout: 60_000,
}
}
pub fn timeout(&mut self, timeout_ms: u128) {
self.timeout = timeout_ms;
}
pub fn add_account(&mut self, account_id: String, address: Address) {
self.account_ids.insert(address, account_id);
}
async fn handle_action<F, R>(&self, args: TransactionArguments, func: F) -> Result<R>
where
F: FnOnce(TransactionDetails) -> Result<R>,
{
let res = self.fireblocks.create_transaction(args).await?;
let start = Instant::now();
loop {
if Instant::now().duration_since(start).as_millis() >= self.timeout {
return Err(FireblocksError::Timeout);
}
let details = self.fireblocks.transaction(&res.id).await?;
use TransactionStatus::*;
match details.status {
BROADCASTING | COMPLETED => return func(details),
BLOCKED | CANCELLED | FAILED => {
return Err(FireblocksError::TxError(details.status, details.sub_status))
}
_ => {}
}
}
}
}
#[cfg(test)]
async fn test_signer() -> FireblocksSigner {
let config = Config::new(
std::env::var("FIREBLOCKS_API_SECRET_PATH").unwrap(),
&std::env::var("FIREBLOCKS_API_KEY").unwrap(),
&std::env::var("FIREBLOCKS_SOURCE_VAULT_ACCOUNT").unwrap(),
3,
)
.unwrap();
FireblocksSigner::new(config).await
}