pub mod errors;
pub mod gas;
pub mod search;
pub mod track;
pub use errors::{decode_tx_error, QoreTxError};
pub use gas::{
calculate_fee, estimate_fee, estimate_gas, GasPrice, DEFAULT_GAS_MULTIPLIER, DEFAULT_GAS_PRICE,
GAS_AUTO,
};
pub use search::{
build_event_query, get_block, get_latest_block, get_tx, search_txs, TxSearchResult,
};
pub use track::{broadcast_and_wait, wait_for_tx, with_retry, TxResult, WaitOptions};
use crate::error::{Error, Result};
use crate::pqc::{
build_hybrid_signature_extension, pqc_sign, ALGORITHM_DILITHIUM5, HYBRID_SIG_TYPE_URL,
};
use base64::engine::general_purpose::STANDARD as BASE64;
use base64::Engine;
use cosmrs::crypto::secp256k1::SigningKey;
use cosmrs::proto::cosmos::bank::v1beta1::MsgSend;
use cosmrs::proto::cosmos::base::v1beta1::Coin as ProtoCoin;
use cosmrs::proto::cosmos::tx::signing::v1beta1::SignMode;
use cosmrs::proto::cosmos::tx::v1beta1::{
mode_info::{Single, Sum},
AuthInfo, Fee as ProtoFee, ModeInfo, SignDoc, SignerInfo, TxBody, TxRaw,
};
use cosmrs::proto::traits::Message as ProstMessage;
use cosmrs::Any;
use serde::Serialize;
use serde_json::Value;
pub const MSG_SEND_TYPE_URL: &str = "/cosmos.bank.v1beta1.MsgSend";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Coin {
pub denom: String,
pub amount: String,
}
#[derive(Debug, Clone, Default)]
pub struct Fee {
pub amount: Vec<Coin>,
pub gas: String,
pub granter: String,
pub payer: String,
}
#[derive(Debug, Clone)]
pub struct Message {
pub type_url: String,
pub value: Vec<u8>,
}
#[derive(Debug, Clone)]
pub struct BuiltTx {
pub tx_raw_bytes: Vec<u8>,
pub auth_info_bytes: Vec<u8>,
pub body_bytes: Vec<u8>,
pub pqc_signed_message: Vec<u8>,
pub pqc_signature: Vec<u8>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BroadcastMode {
Sync,
Async,
Block,
}
impl BroadcastMode {
fn wire(self) -> &'static str {
match self {
BroadcastMode::Sync => "BROADCAST_MODE_SYNC",
BroadcastMode::Async => "BROADCAST_MODE_ASYNC",
BroadcastMode::Block => "BROADCAST_MODE_BLOCK",
}
}
}
pub fn fee_from_estimate(estimate: &Value, gas: &str) -> Result<Fee> {
let raw = &estimate["suggested_fee_uqor"];
let amount = match raw {
Value::String(s) => s.clone(),
Value::Number(n) => n.to_string(),
Value::Null => {
return Err(Error::InvalidResponse(
"fee estimate has no suggested_fee_uqor".into(),
))
}
other => {
return Err(Error::InvalidResponse(format!(
"fee estimate suggested_fee_uqor has unexpected type: {other}"
)))
}
};
if amount.is_empty() || amount == "0" {
return Err(Error::InvalidResponse(
"fee estimate suggested_fee_uqor is empty/zero".into(),
));
}
if amount.contains(['.', 'e', 'E']) {
return Err(Error::InvalidResponse(format!(
"fee estimate suggested_fee_uqor is not an integer: {amount}"
)));
}
Ok(Fee {
amount: vec![Coin {
denom: "uqor".into(),
amount,
}],
gas: gas.into(),
granter: String::new(),
payer: String::new(),
})
}
#[derive(Debug, Clone)]
pub struct BankSendParams {
pub private_key: Vec<u8>,
pub public_key: Vec<u8>,
pub from_address: String,
pub to_address: String,
pub amount: Vec<Coin>,
pub chain_id: String,
pub account_number: u64,
pub sequence: u64,
pub fee: Fee,
pub memo: String,
pub timeout_height: u64,
}
pub fn bank_send(params: BankSendParams) -> Result<BuiltTx> {
let msg = MsgSend {
from_address: params.from_address,
to_address: params.to_address,
amount: to_proto_coins(¶ms.amount)?,
};
let messages = vec![Any {
type_url: MSG_SEND_TYPE_URL.to_string(),
value: msg.encode_to_vec(),
}];
let body = TxBody {
messages,
memo: params.memo,
timeout_height: params.timeout_height,
extension_options: vec![],
non_critical_extension_options: vec![],
};
let body_bytes = body.encode_to_vec();
let auth_info_bytes = build_auth_info_bytes(¶ms.public_key, params.sequence, ¶ms.fee)?;
let sig = sign_direct(
¶ms.private_key,
&body_bytes,
&auth_info_bytes,
¶ms.chain_id,
params.account_number,
)?;
let tx_raw = TxRaw {
body_bytes: body_bytes.clone(),
auth_info_bytes: auth_info_bytes.clone(),
signatures: vec![sig],
};
Ok(BuiltTx {
tx_raw_bytes: tx_raw.encode_to_vec(),
auth_info_bytes,
body_bytes,
pqc_signed_message: vec![],
pqc_signature: vec![],
})
}
#[derive(Debug, Clone)]
pub struct SendMessagesParams {
pub private_key: Vec<u8>,
pub public_key: Vec<u8>,
pub messages: Vec<Any>,
pub chain_id: String,
pub account_number: u64,
pub sequence: u64,
pub fee: Fee,
pub memo: String,
pub timeout_height: u64,
}
pub fn send_messages(params: SendMessagesParams) -> Result<BuiltTx> {
let body = TxBody {
messages: params.messages,
memo: params.memo,
timeout_height: params.timeout_height,
extension_options: vec![],
non_critical_extension_options: vec![],
};
let body_bytes = body.encode_to_vec();
let auth_info_bytes = build_auth_info_bytes(¶ms.public_key, params.sequence, ¶ms.fee)?;
let sig = sign_direct(
¶ms.private_key,
&body_bytes,
&auth_info_bytes,
¶ms.chain_id,
params.account_number,
)?;
let tx_raw = TxRaw {
body_bytes: body_bytes.clone(),
auth_info_bytes: auth_info_bytes.clone(),
signatures: vec![sig],
};
Ok(BuiltTx {
tx_raw_bytes: tx_raw.encode_to_vec(),
auth_info_bytes,
body_bytes,
pqc_signed_message: vec![],
pqc_signature: vec![],
})
}
#[derive(Debug, Clone)]
pub struct BuildHybridTxParams {
pub private_key: Vec<u8>,
pub public_key: Vec<u8>,
pub pqc_secret_key: Vec<u8>,
pub pqc_public_key: Vec<u8>,
pub messages: Vec<Message>,
pub fee: Fee,
pub chain_id: String,
pub account_number: u64,
pub sequence: u64,
pub memo: String,
pub timeout_height: u64,
pub include_pqc_public_key: bool,
}
pub fn build_hybrid_tx(params: BuildHybridTxParams) -> Result<BuiltTx> {
let messages = encode_messages(¶ms.messages);
let base_body = TxBody {
messages: messages.clone(),
memo: params.memo.clone(),
timeout_height: params.timeout_height,
extension_options: vec![],
non_critical_extension_options: vec![],
};
let b0 = base_body.encode_to_vec();
let auth_info_bytes = build_auth_info_bytes(¶ms.public_key, params.sequence, ¶ms.fee)?;
let pqc_signed_message = frame_sign_bytes(&b0, &auth_info_bytes);
let pqc_signature = pqc_sign(¶ms.pqc_secret_key, &pqc_signed_message)?;
let public_key: Option<&[u8]> = if params.include_pqc_public_key {
Some(params.pqc_public_key.as_slice())
} else {
None
};
let ext = build_hybrid_signature_extension(ALGORITHM_DILITHIUM5, &pqc_signature, public_key)?;
let ext_value = to_canonical_json(&ext)?;
let ext_any = Any {
type_url: HYBRID_SIG_TYPE_URL.to_string(),
value: ext_value,
};
let final_body = TxBody {
messages,
memo: params.memo,
timeout_height: params.timeout_height,
extension_options: vec![ext_any],
non_critical_extension_options: vec![],
};
let body_bytes = final_body.encode_to_vec();
let classical_sig = sign_direct(
¶ms.private_key,
&body_bytes,
&auth_info_bytes,
¶ms.chain_id,
params.account_number,
)?;
let tx_raw = TxRaw {
body_bytes: body_bytes.clone(),
auth_info_bytes: auth_info_bytes.clone(),
signatures: vec![classical_sig],
};
Ok(BuiltTx {
tx_raw_bytes: tx_raw.encode_to_vec(),
auth_info_bytes,
body_bytes,
pqc_signed_message,
pqc_signature,
})
}
pub async fn broadcast(rest_url: &str, tx_bytes: &[u8], mode: BroadcastMode) -> Result<Value> {
let url = format!("{}/cosmos/tx/v1beta1/txs", rest_url.trim_end_matches('/'));
let payload = serde_json::json!({
"tx_bytes": BASE64.encode(tx_bytes),
"mode": mode.wire(),
});
let resp = reqwest::Client::new()
.post(&url)
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.json(&payload)
.send()
.await?;
let status = resp.status();
let body = resp.text().await?;
if !status.is_success() {
return Err(Error::Http {
status: status.as_u16(),
url,
body,
});
}
serde_json::from_str(&body).map_err(|e| Error::InvalidResponse(e.to_string()))
}
fn be32(n: u32) -> [u8; 4] {
n.to_be_bytes()
}
fn frame_sign_bytes(b0: &[u8], a: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(8 + b0.len() + a.len());
out.extend_from_slice(&be32(b0.len() as u32));
out.extend_from_slice(b0);
out.extend_from_slice(&be32(a.len() as u32));
out.extend_from_slice(a);
out
}
fn to_canonical_json<T: Serialize>(value: &T) -> Result<Vec<u8>> {
serde_json::to_vec(value).map_err(|e| Error::Pqc(format!("serialize hybrid extension: {e}")))
}
fn to_proto_coins(coins: &[Coin]) -> Result<Vec<ProtoCoin>> {
for c in coins {
validate_amount(&c.amount)?;
}
Ok(coins
.iter()
.map(|c| ProtoCoin {
denom: c.denom.clone(),
amount: c.amount.clone(),
})
.collect())
}
fn validate_amount(amount: &str) -> Result<()> {
if amount.is_empty() || !amount.bytes().all(|b| b.is_ascii_digit()) {
return Err(Error::Denom(format!("invalid coin amount: {amount:?}")));
}
Ok(())
}
fn encode_messages(messages: &[Message]) -> Vec<Any> {
messages
.iter()
.map(|m| Any {
type_url: m.type_url.clone(),
value: m.value.clone(),
})
.collect()
}
fn fee_to_proto(fee: &Fee) -> Result<ProtoFee> {
let amount = to_proto_coins(&fee.amount)?;
let gas_limit = if fee.gas.is_empty() {
0
} else {
fee.gas
.parse::<u64>()
.map_err(|_| Error::Denom(format!("invalid gas: {:?}", fee.gas)))?
};
Ok(ProtoFee {
amount,
gas_limit,
payer: fee.payer.clone(),
granter: fee.granter.clone(),
})
}
fn build_auth_info_bytes(public_key: &[u8], sequence: u64, fee: &Fee) -> Result<Vec<u8>> {
let pubkey_any = secp256k1_pubkey_any(public_key)?;
let auth_info = AuthInfo {
signer_infos: vec![SignerInfo {
public_key: Some(pubkey_any),
mode_info: Some(ModeInfo {
sum: Some(Sum::Single(Single {
mode: SignMode::Direct as i32,
})),
}),
sequence,
}],
fee: Some(fee_to_proto(fee)?),
..Default::default()
};
Ok(auth_info.encode_to_vec())
}
fn secp256k1_pubkey_any(compressed: &[u8]) -> Result<Any> {
let pubkey = cosmrs::proto::cosmos::crypto::secp256k1::PubKey {
key: compressed.to_vec(),
};
Ok(Any {
type_url: "/cosmos.crypto.secp256k1.PubKey".to_string(),
value: pubkey.encode_to_vec(),
})
}
fn sign_direct(
private_key: &[u8],
body_bytes: &[u8],
auth_info_bytes: &[u8],
chain_id: &str,
account_number: u64,
) -> Result<Vec<u8>> {
let sign_doc = SignDoc {
body_bytes: body_bytes.to_vec(),
auth_info_bytes: auth_info_bytes.to_vec(),
chain_id: chain_id.to_string(),
account_number,
};
let sign_bytes = sign_doc.encode_to_vec();
let signing = SigningKey::from_slice(private_key)
.map_err(|e| Error::Derivation(format!("invalid signing key: {e}")))?;
let sig = signing
.sign(&sign_bytes)
.map_err(|e| Error::Derivation(format!("secp256k1 sign: {e}")))?;
Ok(sig.to_bytes().to_vec())
}