avalanche_types/wallet/p/
add_validator.rs

1use std::time::SystemTime;
2
3use crate::{
4    errors::{Error, Result},
5    formatting,
6    ids::{self, node},
7    jsonrpc::client::p as client_p,
8    key, platformvm, txs, units,
9};
10use chrono::{DateTime, NaiveDateTime, TimeZone, Utc};
11use tokio::time::{sleep, Duration, Instant};
12
13/// Represents P-chain "AddValidator" transaction.
14/// ref. <https://github.com/ava-labs/avalanchego/blob/v1.9.4/wallet/chain/p/builder.go#L325-L358> "NewAddValidatorTx"
15/// ref. <https://github.com/ava-labs/avalanchego/blob/v1.9.4/vms/platformvm/txs/builder/builder.go#L428> "NewAddValidatorTx"
16#[derive(Clone, Debug)]
17pub struct Tx<T>
18where
19    T: key::secp256k1::ReadOnly + key::secp256k1::SignOnly + Clone,
20{
21    pub inner: crate::wallet::p::P<T>,
22
23    pub node_id: node::Id,
24
25    /// Denominated in nano-AVAX.
26    /// On the X-Chain, one AVAX is 10^9  units.
27    /// On the P-Chain, one AVAX is 10^9  units.
28    /// On the C-Chain, one AVAX is 10^18 units.
29    /// ref. <https://snowtrace.io/unitconverter>
30    pub stake_amount: u64,
31
32    pub start_time: DateTime<Utc>,
33    pub end_time: DateTime<Utc>,
34
35    /// Validate reward fee in percent.
36    pub reward_fee_percent: u32,
37
38    /// Set "true" to poll transaction status after issuance for its acceptance.
39    pub check_acceptance: bool,
40
41    /// Initial wait duration before polling for acceptance.
42    pub poll_initial_wait: Duration,
43    /// Wait between each poll intervals for acceptance.
44    pub poll_interval: Duration,
45    /// Maximum duration for polling.
46    pub poll_timeout: Duration,
47
48    /// Set to true to return transaction Id for "issue" in dry mode.
49    pub dry_mode: bool,
50}
51
52impl<T> Tx<T>
53where
54    T: key::secp256k1::ReadOnly + key::secp256k1::SignOnly + Clone,
55{
56    pub fn new(p: &crate::wallet::p::P<T>) -> Self {
57        let now_unix = SystemTime::now()
58            .duration_since(SystemTime::UNIX_EPOCH)
59            .expect("unexpected None duration_since")
60            .as_secs();
61
62        let start_time = now_unix + 60;
63        let naive_dt = NaiveDateTime::from_timestamp_opt(start_time as i64, 0).unwrap();
64        let start_time = Utc.from_utc_datetime(&naive_dt);
65
66        // 15-day + 5-min
67        // must be smaller than the primary network default
68        // otherwise "staking period must be a subset of the primary network"
69        // 15 that subnet validator defaults can be bounded within
70        // ref. "Validator.BoundedBy"
71        let end_time = now_unix + 14 * 24 * 60 * 60 + 5 * 60;
72        let naive_dt = NaiveDateTime::from_timestamp_opt(end_time as i64, 0).unwrap();
73        let end_time = Utc.from_utc_datetime(&naive_dt);
74
75        Self {
76            inner: p.clone(),
77            node_id: node::Id::empty(),
78            stake_amount: 2 * units::KILO_AVAX,
79            start_time,
80            end_time,
81            reward_fee_percent: 2,
82            check_acceptance: false,
83            poll_initial_wait: Duration::from_secs(62), // enough to elapse validate start time
84            poll_interval: Duration::from_secs(1),
85            poll_timeout: Duration::from_secs(300),
86            dry_mode: false,
87        }
88    }
89
90    /// Sets the validator node Id.
91    #[must_use]
92    pub fn node_id(mut self, node_id: node::Id) -> Self {
93        self.node_id = node_id;
94        self
95    }
96
97    /// Sets the stake amount.
98    #[must_use]
99    pub fn stake_amount(mut self, stake_amount: u64) -> Self {
100        self.stake_amount = stake_amount;
101        self
102    }
103
104    /// Sets the validate start time.
105    #[must_use]
106    pub fn start_time(mut self, start_time: DateTime<Utc>) -> Self {
107        self.start_time = start_time;
108        self
109    }
110
111    /// Sets the validate start time.
112    #[must_use]
113    pub fn end_time(mut self, end_time: DateTime<Utc>) -> Self {
114        self.end_time = end_time;
115        self
116    }
117
118    /// Sets the validate start/end time in days from 'offset_seconds' later.
119    #[must_use]
120    pub fn validate_period_in_days(mut self, days: u64, offset_seconds: u64) -> Self {
121        let now_unix = SystemTime::now()
122            .duration_since(SystemTime::UNIX_EPOCH)
123            .expect("unexpected None duration_since")
124            .as_secs();
125
126        let start_time = now_unix + offset_seconds;
127        let naive_dt = NaiveDateTime::from_timestamp_opt(start_time as i64, 0).unwrap();
128        let start_time = Utc.from_utc_datetime(&naive_dt);
129
130        // must be smaller than the primary network default
131        // otherwise "staking period must be a subset of the primary network"
132        let end_time = now_unix + days * 24 * 60 * 60;
133        let naive_dt = NaiveDateTime::from_timestamp_opt(end_time as i64, 0).unwrap();
134        let end_time = Utc.from_utc_datetime(&naive_dt);
135
136        self.start_time = start_time;
137        self.end_time = end_time;
138        self
139    }
140
141    /// Sets the validate reward in percent.
142    #[must_use]
143    pub fn reward_fee_percent(mut self, reward_fee_percent: u32) -> Self {
144        self.reward_fee_percent = reward_fee_percent;
145        self
146    }
147
148    /// Sets the check acceptance boolean flag.
149    #[must_use]
150    pub fn check_acceptance(mut self, check_acceptance: bool) -> Self {
151        self.check_acceptance = check_acceptance;
152        self
153    }
154
155    /// Sets the initial poll wait time.
156    #[must_use]
157    pub fn poll_initial_wait(mut self, poll_initial_wait: Duration) -> Self {
158        self.poll_initial_wait = poll_initial_wait;
159        self
160    }
161
162    /// Sets the poll wait time between intervals.
163    #[must_use]
164    pub fn poll_interval(mut self, poll_interval: Duration) -> Self {
165        self.poll_interval = poll_interval;
166        self
167    }
168
169    /// Sets the poll timeout.
170    #[must_use]
171    pub fn poll_timeout(mut self, poll_timeout: Duration) -> Self {
172        self.poll_timeout = poll_timeout;
173        self
174    }
175
176    /// Sets the dry mode boolean flag.
177    #[must_use]
178    pub fn dry_mode(mut self, dry_mode: bool) -> Self {
179        self.dry_mode = dry_mode;
180        self
181    }
182
183    /// Issues the add validator transaction and returns the transaction Id.
184    /// The boolean return represents whether the "add_validator" request was
185    /// successfully issued or not (regardless of its acceptance).
186    /// If the validator is already a validator, it returns an empty Id and false.
187    pub async fn issue(&self) -> Result<(ids::Id, bool)> {
188        let picked_http_rpc = self.inner.inner.pick_base_http_url();
189        log::info!(
190            "adding primary network validator {} with stake amount {} AVAX ({} nAVAX) via {}",
191            self.node_id,
192            units::cast_xp_navax_to_avax(primitive_types::U256::from(self.stake_amount)),
193            self.stake_amount,
194            picked_http_rpc.1
195        );
196
197        let already_validator = self
198            .inner
199            .is_primary_network_validator(&self.node_id)
200            .await?;
201        if already_validator {
202            log::warn!(
203                "node Id {} is already a validator -- returning empty tx Id",
204                self.node_id
205            );
206            return Ok((ids::Id::empty(), false));
207        }
208
209        let cur_balance_p = self.inner.balance().await?;
210        if cur_balance_p < self.stake_amount + self.inner.inner.add_primary_network_validator_fee {
211            return Err(Error::Other {
212                message: 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),
213                retryable: false,
214            });
215        };
216        log::info!(
217            "{} current P-chain balance {}",
218            self.inner.inner.p_address,
219            cur_balance_p
220        );
221
222        let (ins, unstaked_outs, staked_outs, signers) = self
223            .inner
224            .spend(
225                self.stake_amount,
226                self.inner.inner.add_primary_network_validator_fee,
227            )
228            .await?;
229
230        let mut tx = platformvm::txs::add_validator::Tx {
231            base_tx: txs::Tx {
232                network_id: self.inner.inner.network_id,
233                blockchain_id: self.inner.inner.blockchain_id_p,
234                transferable_outputs: Some(unstaked_outs),
235                transferable_inputs: Some(ins),
236                ..Default::default()
237            },
238            validator: platformvm::txs::Validator {
239                node_id: self.node_id,
240                start: self.start_time.timestamp() as u64,
241                end: self.end_time.timestamp() as u64,
242                weight: self.stake_amount,
243            },
244            stake_transferable_outputs: Some(staked_outs),
245            rewards_owner: key::secp256k1::txs::OutputOwners {
246                locktime: 0,
247                threshold: 1,
248                addresses: vec![self.inner.inner.short_address.clone()],
249            },
250            shares: self.reward_fee_percent * 10000,
251            ..Default::default()
252        };
253        tx.sign(signers).await?;
254
255        if self.dry_mode {
256            return Ok((tx.base_tx.metadata.unwrap().id, false));
257        }
258
259        let tx_bytes_with_signatures = tx.base_tx.metadata.unwrap().tx_bytes_with_signatures;
260        let hex_tx = formatting::encode_hex_with_checksum(&tx_bytes_with_signatures);
261        let resp = client_p::issue_tx(&picked_http_rpc.1, &hex_tx).await?;
262
263        if let Some(e) = resp.error {
264            // handle duplicate validator
265            // ref. "avalanchego/vms/platformvm/txs/executor" "verifyAddValidatorTx"
266            let already_validator = e
267                .message
268                .contains("attempted to issue duplicate validation for");
269            if already_validator {
270                log::warn!(
271                    "node Id {} is already a validator -- returning empty tx Id ({})",
272                    self.node_id,
273                    e.message
274                );
275                return Ok((ids::Id::empty(), false));
276            }
277
278            return Err(Error::API {
279                message: format!("failed to issue add validator transaction {:?}", e),
280                retryable: false,
281            });
282        }
283
284        let tx_id = resp.result.unwrap().tx_id;
285        log::info!("{} successfully issued", tx_id);
286
287        if !self.check_acceptance {
288            log::debug!("skipping checking acceptance...");
289            return Ok((tx_id, true));
290        }
291
292        // enough time for txs processing
293        log::info!("initial waiting {:?}", self.poll_initial_wait);
294        sleep(self.poll_initial_wait).await;
295
296        log::info!("polling to confirm add validator transaction");
297        let (start, mut success) = (Instant::now(), false);
298        loop {
299            let elapsed = start.elapsed();
300            if elapsed.gt(&self.poll_timeout) {
301                break;
302            }
303
304            let resp = client_p::get_tx_status(&picked_http_rpc.1, &tx_id.to_string()).await?;
305
306            let status = resp.result.unwrap().status;
307            if status == platformvm::txs::status::Status::Committed {
308                log::info!("{} successfully committed", tx_id);
309                success = true;
310                break;
311            }
312
313            log::warn!(
314                "{} {} (not accepted yet in {}, elapsed {:?})",
315                tx_id,
316                status,
317                picked_http_rpc.1,
318                elapsed
319            );
320            sleep(self.poll_interval).await;
321        }
322        if !success {
323            return Err(Error::API {
324                message: "failed to check acceptance in time".to_string(),
325                retryable: true,
326            });
327        }
328
329        log::info!("polling to confirm validator");
330        success = false;
331        loop {
332            let elapsed = start.elapsed();
333            if elapsed.gt(&self.poll_timeout) {
334                break;
335            }
336
337            let already_validator = self
338                .inner
339                .is_primary_network_validator(&self.node_id)
340                .await?;
341            if already_validator {
342                log::info!("node Id {} is now a validator", self.node_id);
343                success = true;
344                break;
345            }
346
347            log::warn!(
348                "node Id {} is not a validator yet (elapsed {:?})",
349                self.node_id,
350                elapsed
351            );
352            sleep(self.poll_interval).await;
353        }
354        if !success {
355            return Err(Error::API {
356                message: "failed to check validator acceptance in time".to_string(),
357                retryable: true,
358            });
359        }
360
361        Ok((tx_id, true))
362    }
363}