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());
}
}