use std::{
io::{self, Error, ErrorKind},
time::SystemTime,
};
use crate::p as api_p;
use avalanche_types::{
formatting,
ids::{self, node},
key, platformvm, txs, units,
};
use chrono::{DateTime, NaiveDateTime, Utc};
use tokio::time::{sleep, Duration, Instant};
#[derive(Clone, Debug)]
pub struct Tx<T>
where
T: key::secp256k1::ReadOnly + key::secp256k1::SignOnly + Clone,
{
pub inner: crate::wallet::p::P<T>,
pub node_id: node::Id,
pub stake_amount: u64,
pub start_time: DateTime<Utc>,
pub end_time: DateTime<Utc>,
pub reward_fee_percent: u32,
pub check_acceptance: bool,
pub poll_initial_wait: Duration,
pub poll_interval: Duration,
pub poll_timeout: Duration,
}
impl<T> Tx<T>
where
T: key::secp256k1::ReadOnly + key::secp256k1::SignOnly + Clone,
{
pub fn new(p: &crate::wallet::p::P<T>) -> Self {
let now_unix = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.expect("unexpected None duration_since")
.as_secs();
let start_time = now_unix + 60;
let native_dt = NaiveDateTime::from_timestamp(start_time as i64, 0);
let start_time = DateTime::<Utc>::from_utc(native_dt, Utc);
let end_time = now_unix + 100 * 24 * 60 * 60;
let native_dt = NaiveDateTime::from_timestamp(end_time as i64, 0);
let end_time = DateTime::<Utc>::from_utc(native_dt, Utc);
Self {
inner: p.clone(),
node_id: node::Id::empty(),
stake_amount: 2 * units::KILO_AVAX,
start_time,
end_time,
reward_fee_percent: 2,
check_acceptance: false,
poll_initial_wait: Duration::from_secs(62), poll_interval: Duration::from_secs(1),
poll_timeout: Duration::from_secs(300),
}
}
#[must_use]
pub fn node_id(mut self, node_id: node::Id) -> Self {
self.node_id = node_id;
self
}
#[must_use]
pub fn stake_amount(mut self, stake_amount: u64) -> Self {
self.stake_amount = stake_amount;
self
}
#[must_use]
pub fn start_time(mut self, start_time: DateTime<Utc>) -> Self {
self.start_time = start_time;
self
}
#[must_use]
pub fn end_time(mut self, end_time: DateTime<Utc>) -> Self {
self.end_time = end_time;
self
}
#[must_use]
pub fn reward_fee_percent(mut self, reward_fee_percent: u32) -> Self {
self.reward_fee_percent = reward_fee_percent;
self
}
#[must_use]
pub fn check_acceptance(mut self, check_acceptance: bool) -> Self {
self.check_acceptance = check_acceptance;
self
}
#[must_use]
pub fn poll_initial_wait(mut self, poll_initial_wait: Duration) -> Self {
self.poll_initial_wait = poll_initial_wait;
self
}
#[must_use]
pub fn poll_interval(mut self, poll_interval: Duration) -> Self {
self.poll_interval = poll_interval;
self
}
#[must_use]
pub fn poll_timeout(mut self, poll_timeout: Duration) -> Self {
self.poll_timeout = poll_timeout;
self
}
pub async fn issue(&self) -> io::Result<(ids::Id, bool)> {
let picked_http_rpc = self.inner.inner.pick_http_rpc();
log::info!(
"adding primary network validator {} with stake amount {} AVAX ({} nAVAX) via {}",
self.node_id,
units::convert_navax_for_x_and_p(self.stake_amount),
self.stake_amount,
picked_http_rpc.1
);
let already_validator = self
.inner
.is_primary_network_validator(&self.node_id)
.await?;
if already_validator {
log::warn!(
"node Id {} is already a validator -- returning empty tx Id",
self.node_id
);
return Ok((ids::Id::empty(), false));
}
let cur_balance_p = self.inner.balance().await?;
if cur_balance_p < self.stake_amount + self.inner.inner.add_primary_network_validator_fee {
return Err(Error::new(
ErrorKind::InvalidInput,
format!("key address {} (balance {} nano-AVAX, network {}) does not have enough to cover stake amount + fee {}", self.inner.inner.p_address, cur_balance_p, self.inner.inner.network_name, self.stake_amount + self.inner.inner.add_primary_network_validator_fee),
));
};
log::info!(
"{} current P-chain balance {}",
self.inner.inner.p_address,
cur_balance_p
);
let (ins, unstaked_outs, staked_outs, signers) = self
.inner
.spend(
self.stake_amount,
self.inner.inner.add_primary_network_validator_fee,
)
.await?;
let mut tx = platformvm::txs::add_validator::Tx {
base_tx: txs::Tx {
network_id: self.inner.inner.network_id,
blockchain_id: self.inner.inner.p_chain_id,
transferable_outputs: Some(unstaked_outs),
transferable_inputs: Some(ins),
..Default::default()
},
validator: platformvm::txs::Validator {
node_id: self.node_id.clone(),
start: self.start_time.timestamp() as u64,
end: self.end_time.timestamp() as u64,
weight: self.stake_amount,
},
stake_transferable_outputs: Some(staked_outs),
rewards_owner: key::secp256k1::txs::OutputOwners {
locktime: 0,
threshold: 1,
addresses: vec![self.inner.inner.short_address.clone()],
},
shares: self.reward_fee_percent * 10000,
..Default::default()
};
tx.sign(signers)?;
let signed_bytes = tx.base_tx.metadata.unwrap().bytes;
let hex_tx = formatting::encode_hex_with_checksum(&signed_bytes);
let resp = api_p::issue_tx(&picked_http_rpc.1, &hex_tx).await?;
if let Some(e) = resp.error {
let already_validator = e
.message
.contains("attempted to issue duplicate validation for");
if already_validator {
log::warn!(
"node Id {} is already a validator -- returning empty tx Id ({})",
self.node_id,
e.message
);
return Ok((ids::Id::empty(), false));
}
return Err(Error::new(
ErrorKind::Other,
format!("failed to issue add validator transaction {:?}", e),
));
}
let tx_id = resp.result.unwrap().tx_id;
log::info!("{} successfully issued", tx_id);
if !self.check_acceptance {
log::debug!("skipping checking acceptance...");
return Ok((tx_id, true));
}
sleep(self.poll_initial_wait).await;
log::info!("polling to confirm add validator transaction");
let (start, mut success) = (Instant::now(), false);
loop {
let elapsed = start.elapsed();
if elapsed.gt(&self.poll_timeout) {
break;
}
let resp = api_p::get_tx_status(&picked_http_rpc.1, &tx_id.to_string()).await?;
let status = resp.result.unwrap().status;
if status == platformvm::txs::status::Status::Committed {
log::info!("{} successfully committed", tx_id);
success = true;
break;
}
log::warn!(
"{} {} (not accepted yet in {}, elapsed {:?})",
tx_id,
status,
picked_http_rpc.1,
elapsed
);
sleep(self.poll_interval).await;
}
if !success {
return Err(Error::new(
ErrorKind::Other,
"failed to check acceptance in time",
));
}
log::info!("polling to confirm validator");
success = false;
loop {
let elapsed = start.elapsed();
if elapsed.gt(&self.poll_timeout) {
break;
}
let already_validator = self
.inner
.is_primary_network_validator(&self.node_id)
.await?;
if already_validator {
log::info!("node Id {} is now a validator", self.node_id);
success = true;
break;
}
log::warn!(
"node Id {} is not a validator yet (elapsed {:?})",
self.node_id,
elapsed
);
sleep(self.poll_interval).await;
}
if !success {
return Err(Error::new(
ErrorKind::Other,
"failed to check validator acceptance in time",
));
}
Ok((tx_id, true))
}
}