avalanche_types/wallet/p/
add_validator.rs1use 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#[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 pub stake_amount: u64,
31
32 pub start_time: DateTime<Utc>,
33 pub end_time: DateTime<Utc>,
34
35 pub reward_fee_percent: u32,
37
38 pub check_acceptance: bool,
40
41 pub poll_initial_wait: Duration,
43 pub poll_interval: Duration,
45 pub poll_timeout: Duration,
47
48 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 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), poll_interval: Duration::from_secs(1),
85 poll_timeout: Duration::from_secs(300),
86 dry_mode: false,
87 }
88 }
89
90 #[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 #[must_use]
99 pub fn stake_amount(mut self, stake_amount: u64) -> Self {
100 self.stake_amount = stake_amount;
101 self
102 }
103
104 #[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 #[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 #[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 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 #[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 #[must_use]
150 pub fn check_acceptance(mut self, check_acceptance: bool) -> Self {
151 self.check_acceptance = check_acceptance;
152 self
153 }
154
155 #[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 #[must_use]
164 pub fn poll_interval(mut self, poll_interval: Duration) -> Self {
165 self.poll_interval = poll_interval;
166 self
167 }
168
169 #[must_use]
171 pub fn poll_timeout(mut self, poll_timeout: Duration) -> Self {
172 self.poll_timeout = poll_timeout;
173 self
174 }
175
176 #[must_use]
178 pub fn dry_mode(mut self, dry_mode: bool) -> Self {
179 self.dry_mode = dry_mode;
180 self
181 }
182
183 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 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 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}