Skip to main content

avalanche_types/wallet/p/
mod.rs

1pub mod add_permissionless_validator;
2pub mod add_subnet_validator;
3pub mod add_validator;
4pub mod create_chain;
5pub mod create_subnet;
6pub mod export;
7pub mod import;
8
9use std::{cmp, time::SystemTime};
10
11use crate::{
12    errors::{Error, Result},
13    ids::{self, node},
14    jsonrpc::client::p as client_p,
15    key, platformvm, txs, wallet,
16};
17
18impl<T> wallet::Wallet<T>
19where
20    T: key::secp256k1::ReadOnly + key::secp256k1::SignOnly + Clone,
21{
22    #[must_use]
23    pub fn p(&self) -> P<T> {
24        P {
25            inner: self.clone(),
26        }
27    }
28}
29
30#[derive(Clone, Debug)]
31pub struct P<T>
32where
33    T: key::secp256k1::ReadOnly + key::secp256k1::SignOnly + Clone,
34{
35    pub inner: crate::wallet::Wallet<T>,
36}
37
38impl<T> P<T>
39where
40    T: key::secp256k1::ReadOnly + key::secp256k1::SignOnly + Clone,
41{
42    /// Fetches the current balance of the wallet owner from the specified HTTP endpoint.
43    pub async fn balance_with_endpoint(&self, http_rpc: &str) -> Result<u64> {
44        let resp = client_p::get_balance(http_rpc, &self.inner.p_address).await?;
45        let cur_balance = resp
46            .result
47            .expect("unexpected None GetBalanceResult")
48            .balance;
49        Ok(cur_balance)
50    }
51
52    /// Fetches the current balance of the wallet owner from all endpoints
53    /// in the same order of "self.http_rpcs".
54    pub async fn balances(&self) -> Result<Vec<u64>> {
55        let mut balances = Vec::new();
56        for http_rpc in self.inner.base_http_urls.iter() {
57            let balance = self.balance_with_endpoint(http_rpc).await?;
58            balances.push(balance);
59        }
60        Ok(balances)
61    }
62
63    /// Fetches the current balance of the wallet owner.
64    pub async fn balance(&self) -> Result<u64> {
65        self.balance_with_endpoint(&self.inner.pick_base_http_url().1)
66            .await
67    }
68
69    /// Fetches UTXOs for "P" chain.
70    /// TODO: cache this like avalanchego
71    pub async fn utxos(&self) -> Result<Vec<txs::utxo::Utxo>> {
72        let resp =
73            client_p::get_utxos(&self.inner.pick_base_http_url().1, &self.inner.p_address).await?;
74        let utxos = resp
75            .result
76            .expect("unexpected None GetUtxosResult")
77            .utxos
78            .expect("unexpected None Utxos");
79        Ok(utxos)
80    }
81
82    /// Returns "true" if the node_id is a current primary network validator.
83    pub async fn is_primary_network_validator(&self, node_id: &node::Id) -> Result<bool> {
84        let resp =
85            client_p::get_primary_network_validators(&self.inner.pick_base_http_url().1).await?;
86        let resp = resp
87            .result
88            .expect("unexpected None GetCurrentValidatorResult");
89        let validators = resp.validators.expect("unexpected None vaidators");
90        for validator in validators.iter() {
91            log::info!("listing primary network validator {}", node_id);
92            if validator.node_id.eq(node_id) {
93                return Ok(true);
94            }
95        }
96        Ok(false)
97    }
98
99    /// Returns "true" if the node_id is a current subnet validator.
100    pub async fn is_subnet_validator(
101        &self,
102        node_id: &node::Id,
103        subnet_id: &ids::Id,
104    ) -> Result<bool> {
105        let resp = client_p::get_subnet_validators(
106            &self.inner.pick_base_http_url().1,
107            &subnet_id.to_string(),
108        )
109        .await?;
110        let resp = resp
111            .result
112            .expect("unexpected None GetCurrentValidatorResult");
113        let validators = resp.validators.expect("unexpected None vaidators");
114        for validator in validators.iter() {
115            log::info!(
116                "listing subnet validator {} for subnet {}",
117                node_id,
118                subnet_id
119            );
120            if validator.node_id.eq(node_id) {
121                return Ok(true);
122            }
123        }
124        Ok(false)
125    }
126
127    /// ref. <https://github.com/ava-labs/avalanchego/blob/v1.9.4/vms/platformvm/utxo/handler.go#L169> "Spend"
128    /// ref. <https://github.com/ava-labs/avalanchego/blob/v1.9.4/wallet/chain/p/builder.go#L325-L358> "NewAddValidatorTx"
129    /// ref. <https://github.com/ava-labs/avalanchego/blob/v1.9.4/vms/platformvm/txs/builder/builder.go#L428> "NewAddValidatorTx"
130    async fn spend(
131        &self,
132        amount: u64,
133        fee: u64,
134    ) -> Result<(
135        Vec<txs::transferable::Input>,
136        Vec<txs::transferable::Output>,
137        Vec<txs::transferable::Output>,
138        Vec<Vec<T>>,
139    )> {
140        let utxos = self.utxos().await?;
141
142        let now_unix = SystemTime::now()
143            .duration_since(SystemTime::UNIX_EPOCH)
144            .expect("unexpected None duration_since")
145            .as_secs();
146
147        let mut ins: Vec<txs::transferable::Input> = Vec::new();
148        let mut returned_outputs: Vec<txs::transferable::Output> = Vec::new();
149        let mut staked_outputs: Vec<txs::transferable::Output> = Vec::new();
150        let mut signers: Vec<Vec<T>> = Vec::new();
151
152        // amount of AVAX that has been staked
153        let mut amount_staked: u64 = 0_u64;
154
155        // consume locked UTXOs
156        for utxo in utxos.iter() {
157            // no need to consume more locked AVAX
158            // because it already has consumed more than the target stake amount
159            if amount_staked >= amount {
160                break;
161            }
162
163            // only staking avax so ignore other assets
164            if utxo.asset_id != self.inner.avax_asset_id {
165                continue;
166            }
167
168            // check "*platformvm.StakeableLockOut"
169            if utxo.stakeable_lock_out.is_none() {
170                // output is not locked, thus handle this in the next iteration
171                continue;
172            }
173
174            // check locktime
175            let out = utxo.stakeable_lock_out.clone().unwrap();
176            if out.locktime <= now_unix {
177                // output is no longer locked, thus handle in the next iteration
178                continue;
179            }
180
181            // check "*secp256k1fx.TransferOutput"
182            let inner = out.clone().transfer_output;
183            let res = self.inner.keychain.spend(&inner, now_unix);
184            if res.is_none() {
185                // cannot spend the output, move onto next
186                continue;
187            }
188            let (transfer_input, in_signers) = res.unwrap();
189
190            let mut remaining_value = transfer_input.amount;
191            let amount_to_stake = cmp::min(
192                amount - amount_staked, // amount we still need to stake
193                remaining_value,        // amount available to stake
194            );
195            amount_staked += amount_to_stake;
196            remaining_value -= amount_to_stake;
197
198            // add input to the consumed inputs
199            ins.push(txs::transferable::Input {
200                utxo_id: utxo.utxo_id.clone(),
201                asset_id: utxo.asset_id,
202                stakeable_lock_in: Some(platformvm::txs::StakeableLockIn {
203                    locktime: out.locktime,
204                    transfer_input,
205                }),
206                ..txs::transferable::Input::default()
207            });
208
209            // add output to the staked outputs
210            staked_outputs.push(txs::transferable::Output {
211                asset_id: utxo.asset_id,
212                stakeable_lock_out: Some(platformvm::txs::StakeableLockOut {
213                    locktime: out.clone().locktime,
214                    transfer_output: key::secp256k1::txs::transfer::Output {
215                        amount: remaining_value,
216                        output_owners: out.clone().transfer_output.output_owners,
217                    },
218                }),
219                ..txs::transferable::Output::default()
220            });
221
222            if remaining_value > 0 {
223                // this input provided more value than was needed to be locked
224                // some must be returned
225                returned_outputs.push(txs::transferable::Output {
226                    asset_id: utxo.asset_id,
227                    stakeable_lock_out: Some(platformvm::txs::StakeableLockOut {
228                        locktime: out.clone().locktime,
229                        transfer_output: key::secp256k1::txs::transfer::Output {
230                            amount: amount_to_stake,
231                            output_owners: out.clone().transfer_output.output_owners,
232                        },
233                    }),
234                    ..txs::transferable::Output::default()
235                });
236            }
237
238            signers.push(in_signers);
239        }
240
241        // amount of AVAX that has been burned
242        let mut amount_burned = 0_u64;
243
244        for utxo in utxos.iter() {
245            // have staked/burned more AVAX than we need
246            // thus no need to consume more AVAX
247            if amount_burned >= fee && amount_staked >= amount {
248                break;
249            }
250
251            // only burn AVAX, thus ignore other assets
252            if utxo.asset_id != self.inner.avax_asset_id {
253                continue;
254            }
255
256            let (skip, out) = {
257                if utxo.transfer_output.is_some() {
258                    let out = utxo.transfer_output.clone().unwrap();
259                    (false, out)
260                } else {
261                    let inner = utxo.stakeable_lock_out.clone().unwrap();
262                    (inner.locktime > now_unix, inner.transfer_output)
263                }
264            };
265            // output is currently locked, so this output cannot be burned
266            // or it may have already been consumed above
267            if skip {
268                continue;
269            }
270
271            let res = self.inner.keychain.spend(&out, now_unix);
272            if res.is_none() {
273                // cannot spend the output, move onto next
274                continue;
275            }
276            let (transfer_input, in_signers) = res.unwrap();
277
278            // ref. https://github.com/ava-labs/subnet-cli/blob/6bbe9f4aff353b812822af99c08133af35dbc6bd/client/p.go#L763
279            let mut remaining_value = transfer_input.amount;
280            let amount_to_burn = cmp::min(
281                fee - amount_burned, // amount we still need to burn
282                remaining_value,     // amount available to burn
283            );
284            amount_burned += amount_to_burn;
285            remaining_value -= amount_to_burn;
286
287            let amount_to_stake = cmp::min(
288                amount - amount_staked, // amount we still need to stake
289                remaining_value,        // amount available to stake
290            );
291            amount_staked += amount_to_stake;
292            remaining_value -= amount_to_stake;
293
294            // add the input to the consumed inputs
295            ins.push(txs::transferable::Input {
296                utxo_id: utxo.utxo_id.clone(),
297                asset_id: utxo.asset_id,
298                transfer_input: Some(transfer_input),
299                ..txs::transferable::Input::default()
300            });
301
302            if amount_to_stake > 0 {
303                // some of this input was put for staking
304                staked_outputs.push(txs::transferable::Output {
305                    asset_id: utxo.asset_id,
306                    transfer_output: Some(key::secp256k1::txs::transfer::Output {
307                        amount: amount_to_stake,
308                        output_owners: key::secp256k1::txs::OutputOwners {
309                            locktime: 0,
310                            threshold: 1,
311                            addresses: vec![self.inner.short_address.clone()],
312                        },
313                    }),
314                    ..txs::transferable::Output::default()
315                });
316            }
317
318            if remaining_value > 0 {
319                // this input had extra value, so some must be returned
320                returned_outputs.push(txs::transferable::Output {
321                    asset_id: utxo.asset_id,
322                    transfer_output: Some(key::secp256k1::txs::transfer::Output {
323                        amount: remaining_value,
324                        output_owners: key::secp256k1::txs::OutputOwners {
325                            locktime: 0,
326                            threshold: 1,
327                            addresses: vec![self.inner.short_address.clone()],
328                        },
329                    }),
330                    ..txs::transferable::Output::default()
331                });
332            }
333
334            signers.push(in_signers);
335        }
336
337        log::info!(
338            "provided keys have balance (unlocked/burned amount so far, locked/staked amount so far) ({}, {}) and need ({}, {})",
339            amount_burned,
340            amount_staked,
341            fee,
342            amount
343        );
344        if amount_burned < fee || amount_staked < amount {
345            return Err(Error::Other {
346                message: format!(
347                    "provided keys have balance (unlocked/burned amount so far, locked/staked amount so far) ({}, {}) but need ({}, {})",
348                    amount_burned,
349                    amount_staked,
350                    fee,
351                    amount
352                ),
353                retryable: false,
354            });
355        }
356
357        // TODO: for now just ignore "signers" in the sorting
358        // since the wallet currently only supports one key
359        ins.sort();
360        returned_outputs.sort();
361        staked_outputs.sort();
362
363        Ok((ins, returned_outputs, staked_outputs, signers))
364    }
365
366    /// ref. <https://github.com/ava-labs/avalanchego/blob/v1.9.4/vms/platformvm/utxo/handler.go#L411> "Authorize"
367    /// ref. <https://github.com/ava-labs/avalanchego/blob/v1.9.4/wallet/chain/p/builder.go#L360-L390> "NewAddSubnetValidatorTx"
368    /// ref. <https://github.com/ava-labs/avalanchego/blob/v1.9.4/vms/platformvm/txs/builder/builder.go#L512> "NewAddSubnetValidatorTx"
369    async fn authorize(
370        &self,
371        subnet_id: ids::Id,
372    ) -> Result<(key::secp256k1::txs::Input, Vec<Vec<T>>)> {
373        log::info!("authorizing subnet {}", subnet_id);
374
375        let tx =
376            client_p::get_tx(&self.inner.pick_base_http_url().1, &subnet_id.to_string()).await?;
377        if let Some(tx_result) = tx.result {
378            let output_owners = tx_result.tx.unsigned_tx.output_owners;
379
380            let now_unix = SystemTime::now()
381                .duration_since(SystemTime::UNIX_EPOCH)
382                .expect("unexpected None duration_since")
383                .as_secs();
384
385            let res = self
386                .inner
387                .keychain
388                .match_threshold(&output_owners, now_unix);
389            let threshold_met = res.is_some();
390            if !threshold_met {
391                return Err(Error::Other {
392                    message: "no threshold met, can't sign".to_string(),
393                    retryable: false,
394                });
395            }
396            let (sig_indices, keys) = res.unwrap();
397
398            return Ok((
399                key::secp256k1::txs::Input {
400                    // if empty, it errors with "unauthorized subnet modification: input has less signers than expected"
401                    sig_indices,
402                },
403                vec![keys],
404            ));
405        }
406
407        Err(Error::Other {
408            message: "empty get tx result".to_string(),
409            retryable: false,
410        })
411    }
412
413    /// Subnet validators must validate the primary network.
414    #[must_use]
415    pub fn add_validator(&self) -> add_validator::Tx<T> {
416        add_validator::Tx::new(self)
417    }
418
419    #[must_use]
420    pub fn add_permissionless_validator(&self) -> add_permissionless_validator::Tx<T> {
421        add_permissionless_validator::Tx::new(self)
422    }
423
424    /// Once subnet is created, the avalanche node must whitelist the subnet Id
425    /// (the returned/confirmed transaction Id).
426    #[must_use]
427    pub fn create_subnet(&self) -> create_subnet::Tx<T> {
428        create_subnet::Tx::new(self)
429    }
430
431    /// Once the subnet is created, the subnet needs to add new validators
432    /// for the subnet itself.
433    #[must_use]
434    pub fn add_subnet_validator(&self) -> add_subnet_validator::Tx<T> {
435        add_subnet_validator::Tx::new(self)
436    }
437
438    /// Once the subnet validators are added, each virtual machine must create
439    /// its own blockchain and use the chain Id as the RPC endpoint.
440    #[must_use]
441    pub fn create_chain(&self) -> create_chain::Tx<T> {
442        create_chain::Tx::new(self)
443    }
444
445    #[must_use]
446    pub fn export(&self) -> export::Tx<T> {
447        export::Tx::new(self)
448    }
449
450    #[must_use]
451    pub fn import(&self) -> import::Tx<T> {
452        import::Tx::new(self)
453    }
454}