bolt-cw-sdk 1.0.0

SDK for the BOLT protocol, providing utilities for interacting with the BOLT contracts.
Documentation
use crate::tx_builder::derivation_path::CosmosDerivationPath;
use crate::tx_builder::error::TxBuilderError;
use bip32::{Mnemonic, XPrv};
use cosmrs::crypto::secp256k1::SigningKey;
use cosmrs::crypto::PublicKey;
use cosmrs::tendermint::block;
use cosmrs::tx::{BodyBuilder, Fee, SignDoc, SignerInfo};
use cosmrs::{AccountId, Any, Coin, Gas};
use k256::ecdsa::SigningKey as RootSigningKey;

pub mod derivation_path;
pub mod error;

pub struct TxBuilder {
    signing_key: SigningKey,
    public_key: PublicKey,
    pub(crate) account_id: AccountId,
    body_builder: BodyBuilder,
    sequence_number: u64,
    chain_id: String,
    account_number: u64,

    pub(crate) fees: Option<Fee>,
}

impl TxBuilder {
    pub fn new(
        mnemonic: String,
        chain_prefix: String,
        chain_id: String,
        derivation_path: CosmosDerivationPath,
        account_sequence_number: u64,
        account_number: u64,
    ) -> Result<Self, TxBuilderError> {
        let signing_key = Self::generate_signing_key_from_mnemonic(mnemonic, derivation_path)?;
        let public_key = signing_key.public_key();
        let account_id = public_key.account_id(&chain_prefix)?;
        let body_builder = BodyBuilder::new();
        Ok(TxBuilder {
            signing_key,
            public_key,
            account_id,
            body_builder,
            sequence_number: account_sequence_number,
            chain_id,
            account_number,
            fees: None,
        })
    }

    pub fn get_signer_address(&self) -> String {
        self.account_id.to_string()
    }

    pub fn get_signed_bytes(&self) -> Result<Vec<u8>, TxBuilderError> {
        let fee = self.fees.clone().ok_or(TxBuilderError::FeesNotSet)?;
        let tx_body = self.body_builder.finish();
        let auth_info =
            SignerInfo::single_direct(Some(self.public_key), self.sequence_number).auth_info(fee);
        let sign_doc = SignDoc::new(
            &tx_body,
            &auth_info,
            &self.chain_id.parse().unwrap(),
            self.account_number,
        )?;
        let tx_raw = sign_doc.sign(&self.signing_key)?;
        Ok(tx_raw.to_bytes()?)
    }

    pub fn add_msg(&mut self, msg: impl Into<Any>) -> &mut Self {
        self.body_builder.msg(msg);
        self
    }

    pub fn add_msgs(&mut self, msgs: impl IntoIterator<Item = Any>) -> &mut Self {
        self.body_builder.msgs(msgs);
        self
    }

    pub fn set_memo(&mut self, memo: impl Into<String>) -> &mut Self {
        self.body_builder.memo(memo);
        self
    }

    pub fn set_fee(&mut self, amount: u128, denom: &str, gas_limit: impl Into<Gas>) -> &mut Self {
        let amount = Coin::new(amount, denom).unwrap();
        self.fees = Some(Fee::from_amount_and_gas(amount, gas_limit));
        self
    }

    pub fn set_timeout_height(&mut self, height: impl Into<block::Height>) -> &mut Self {
        self.body_builder.timeout_height(height);
        self
    }

    pub fn set_extension_option(&mut self, option: impl Into<Any>) -> &mut Self {
        self.body_builder.extension_option(option);
        self
    }

    pub fn set_non_critical_extension_option(&mut self, option: impl Into<Any>) -> &mut Self {
        self.body_builder.non_critical_extension_option(option);
        self
    }

    fn generate_signing_key_from_mnemonic(
        mnemonic: String,
        derivation_path: CosmosDerivationPath,
    ) -> Result<SigningKey, TxBuilderError> {
        let mnemonic = Mnemonic::new(mnemonic, Default::default())?;
        let seed = mnemonic.to_seed("");
        let path: String = derivation_path.into();
        let private = XPrv::derive_from_path(seed, &path.parse()?)?;
        let key = RootSigningKey::from(private);
        Ok(SigningKey::new(Box::new(key)))
    }
}

#[cfg(test)]
mod tests {
    use serial_test::serial;

    use super::*;
    use crate::test_utils::test_scenario::TestScenario;

    #[tokio::test]
    #[serial]
    async fn test_tx_builder_success() {
        let test_scenario = TestScenario::new_from_config("config.json".to_string()).await;

        let tx_builder = TxBuilder::new(
            test_scenario.admin_mnemonic,
            test_scenario.chain_prefix,
            test_scenario.chain_id,
            test_scenario.derivation_path,
            0,
            0,
        )
        .expect("Failed to create tx builder");
        assert_eq!(
            tx_builder.account_id.to_string(),
            test_scenario.admin_address
        );
    }

    #[test]
    fn test_tx_builder_invalid_mnemonic() {
        let derivation_path = CosmosDerivationPath::new();
        let result = TxBuilder::new(
            "invalid mnemonic".to_string(),
            "testprefix".to_string(),
            "testchainid".to_string(),
            derivation_path,
            0,
            0,
        );
        assert!(result.is_err());
    }
}