zera-sdk 0.1.0

Rust SDK for ZERA transactions, validator APIs, and bridge workflows
Documentation
use zera_proto::zera_txn::GovernanceVote;

use crate::error::{Result, ZeraError};
use crate::fees::{FeeConfigHelper, UniversalFeeCalculator};
use crate::grpc::{submit_transaction, UnaryTransport, ValidatorApiClient};
use crate::sign::sign_with_key;
use crate::tx::{
    build_standard_base_txn, get_address_and_nonce_with_client, BuildStandardBaseTxnParams,
};
use crate::types::RpcConfig;

#[derive(Debug, Clone, Default, PartialEq)]
pub struct BuildVoteTxnOptions {
    pub support: Option<bool>,
    pub support_option: Option<u32>,
    pub memo: Option<String>,
    pub grpc_config: Option<RpcConfig>,
    pub overestimate_percent: Option<f64>,
    pub nonce: Option<u64>,
    pub fee_id: Option<String>,
    pub fee_amount_parts: Option<String>,
}

pub type CreateVoteTxnOptions = BuildVoteTxnOptions;

pub async fn build_vote_txn(
    contract_id: &str,
    proposal_id_hex: &str,
    public_key_base58_identifier: &str,
    options: BuildVoteTxnOptions,
) -> Result<GovernanceVote> {
    let client = ValidatorApiClient::new(options.grpc_config.clone().unwrap_or_default())?;
    build_vote_txn_with_client(
        contract_id,
        proposal_id_hex,
        public_key_base58_identifier,
        options,
        &client,
    )
    .await
}

pub async fn build_vote_txn_with_client<T>(
    contract_id: &str,
    proposal_id_hex: &str,
    public_key_base58_identifier: &str,
    options: BuildVoteTxnOptions,
    client: &ValidatorApiClient<T>,
) -> Result<GovernanceVote>
where
    T: UnaryTransport,
{
    if contract_id.is_empty() {
        return Err(ZeraError::Validation("contractId is required".to_string()));
    }
    if proposal_id_hex.is_empty() {
        return Err(ZeraError::Validation(
            "proposalId (hex) is required".to_string(),
        ));
    }
    if public_key_base58_identifier.is_empty() {
        return Err(ZeraError::Validation(
            "publicKey identifier is required".to_string(),
        ));
    }

    let has_support = options.support.is_some();
    let has_support_option = options.support_option.is_some();
    if has_support == has_support_option {
        return Err(ZeraError::Validation(
            "Specify exactly one of: support (boolean) OR supportOption (number)".to_string(),
        ));
    }

    if proposal_id_hex.len() % 2 != 0
        || !proposal_id_hex.chars().all(|char| char.is_ascii_hexdigit())
    {
        return Err(ZeraError::Validation(
            "Invalid proposalId: must be hex-encoded".to_string(),
        ));
    }

    let nonce = if let Some(nonce) = options.nonce {
        nonce
    } else {
        get_address_and_nonce_with_client(public_key_base58_identifier, client)
            .await?
            .1
    };

    let base = build_standard_base_txn(BuildStandardBaseTxnParams {
        public_key_id: public_key_base58_identifier.to_string(),
        fee_id: options.fee_id.clone(),
        fee_amount_parts: options.fee_amount_parts.clone(),
        nonce,
        memo: options.memo.clone(),
    })?;

    let mut vote_txn = GovernanceVote {
        base: Some(base),
        contract_id: contract_id.to_string(),
        proposal_id: hex::decode(proposal_id_hex).map_err(|error| {
            ZeraError::Validation(format!("Invalid proposalId: must be hex-encoded ({error})"))
        })?,
        support: options.support,
        support_option: options.support_option,
    };

    vote_txn = UniversalFeeCalculator::calculate_fee_with_client(
        FeeConfigHelper {
            contract_id: Some(contract_id.to_string()),
            proto_object: vote_txn,
            token_info_map: std::collections::HashMap::new(),
            base_fee_id: options.fee_id.clone(),
            base_fee: options.fee_amount_parts.clone(),
            contract_fee_id: None,
            contract_fee: None,
            interface_fee_id: None,
            interface_fee: None,
            interface_address: None,
            overestimate_percent: options.overestimate_percent,
            gas_fee_in_usd: None,
            grpc_config: None,
            needs_initialization: None,
        },
        client,
    )
    .await?;

    Ok(vote_txn)
}

pub async fn create_vote_txn(
    contract_id: &str,
    proposal_id_hex: &str,
    public_key_base58_identifier: &str,
    private_key_base58: &str,
    options: CreateVoteTxnOptions,
) -> Result<GovernanceVote> {
    if private_key_base58.is_empty() {
        return Err(ZeraError::Validation("privateKey is required".to_string()));
    }

    let mut vote_txn = build_vote_txn(
        contract_id,
        proposal_id_hex,
        public_key_base58_identifier,
        options,
    )
    .await?;
    sign_with_key(
        &mut vote_txn,
        private_key_base58,
        public_key_base58_identifier,
    )?;
    Ok(vote_txn)
}

pub async fn send_vote_txn(
    vote: &GovernanceVote,
    grpc_config: Option<RpcConfig>,
) -> Result<String> {
    submit_transaction(vote, grpc_config.unwrap_or_default()).await
}