ethers-fireblocks 0.1.1

ethers-rs middleware and signer for Fireblocks' APIs
Documentation
use crate::{
    types::{
        ExtraParameters, PeerType, RawMessageData, TransactionArguments, TransactionOperation,
        TransferPeerPath, UnsignedMessage,
    },
    FireblocksError, FireblocksSigner,
};
use async_trait::async_trait;
use ethers_core::{
    types::{transaction::eip2718::TypedTransaction, Address, Signature, H256, U256},
    utils::hash_message,
};
use ethers_signers::{to_eip155_v, Signer};
use rustc_hex::ToHex;

#[async_trait]
impl Signer for FireblocksSigner {
    type Error = FireblocksError;

    async fn sign_transaction(&self, tx: &TypedTransaction) -> Result<Signature, FireblocksError> {
        let sighash = tx.sighash(self.chain_id);
        self.sign(tx, sighash, true).await
    }

    async fn sign_message<S: Send + Sync + AsRef<[u8]>>(
        &self,
        message: S,
    ) -> Result<Signature, Self::Error> {
        let hash = hash_message(&message);
        self.sign(message.as_ref(), hash, false).await
    }

    fn address(&self) -> Address {
        self.address
    }

    fn with_chain_id<T: Into<u64>>(mut self, chain_id: T) -> Self {
        self.chain_id = chain_id.into();
        self
    }

    fn chain_id(&self) -> u64 {
        self.chain_id
    }
}

impl FireblocksSigner {
    async fn sign<S: serde::Serialize>(
        &self,
        preimage: S,
        hash: H256,
        is_eip155: bool,
    ) -> Result<Signature, FireblocksError> {
        // send the hash for signing - this will NOT take advantage
        // of the policy engine
        let args = TransactionArguments {
            operation: TransactionOperation::RAW,
            source: TransferPeerPath {
                peer_type: Some(PeerType::VAULT_ACCOUNT),
                id: Some(self.account_id.clone()),
            },
            extra_parameters: Some(ExtraParameters::RawMessageData(RawMessageData {
                messages: vec![UnsignedMessage {
                    content: hash.as_ref().to_hex::<String>(),
                }],
            })),

            // rest is unnecessary
            asset_id: self.asset_id.clone(),
            amount: "".to_owned(),
            destination: None,
            gas_price: None,
            gas_limit: None,
            note: serde_json::to_string(&preimage).map_err(|err| FireblocksError::SerdeJson {
                err,
                text: "failed to serialize tx/message".to_owned(),
            })?,
        };

        // Parse the signature returned from the API
        self.handle_action(args, |details| {
            let sig = &details.signed_messages[0].signature;
            let r = sig
                .r
                .parse::<U256>()
                .map_err(|err| FireblocksError::ParseError(err.to_string()))?;
            let s = sig
                .s
                .parse::<U256>()
                .map_err(|err| FireblocksError::ParseError(err.to_string()))?;
            let v = if is_eip155 {
                to_eip155_v(sig.v as u8, self.chain_id)
            } else {
                sig.v + 27
            };
            Ok(Signature { r, s, v })
        })
        .await
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::test_signer;
    use ethers_core::types::TransactionRequest;
    use rustc_hex::FromHex;

    #[tokio::test]
    async fn can_sign_transaction() {
        let signer = test_signer().await;
        let address: Address = "cbe74e21b070a979b9d6426b11e876d4cb618daf".parse().unwrap();
        let tx = TransactionRequest::new()
            .to(address)
            .data("ead710c40000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000548656c6c6f000000000000000000000000000000000000000000000000000000".from_hex::<Vec<u8>>().unwrap());
        let sighash = tx.sighash(3);
        let sig = signer.sign_transaction(&tx.into()).await.unwrap();
        sig.verify(sighash, signer.address()).unwrap();
    }

    #[tokio::test]
    async fn can_sign_msg() {
        let signer = test_signer().await;
        let msg = "Hello World 2";
        let sig = signer.sign_message(msg).await.unwrap();
        sig.verify(msg, signer.address()).unwrap();
    }
}