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
}