rusk_wallet/wallet/
transaction.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4//
5// Copyright (c) DUSK NETWORK. All rights reserved.
6
7use std::fmt::Debug;
8
9use dusk_core::signatures::bls::PublicKey as BlsPublicKey;
10use dusk_core::stake::StakeFundOwner;
11use dusk_core::transfer::data::TransactionData;
12use dusk_core::transfer::phoenix::PublicKey as PhoenixPublicKey;
13use dusk_core::transfer::Transaction;
14use rand::rngs::StdRng;
15use rand::SeedableRng;
16use wallet_core::transaction::{
17    moonlight, moonlight_deployment, moonlight_stake, moonlight_stake_reward,
18    moonlight_to_phoenix, moonlight_unstake, phoenix, phoenix_deployment,
19    phoenix_stake, phoenix_stake_reward, phoenix_to_moonlight, phoenix_unstake,
20};
21use zeroize::Zeroize;
22
23use super::file::SecureWalletFile;
24use super::{Address, Wallet};
25use crate::clients::Prover;
26use crate::currency::Dusk;
27use crate::gas::Gas;
28use crate::Error;
29
30impl<F: SecureWalletFile + Debug> Wallet<F> {
31    /// Transfers funds between shielded accounts.
32    pub async fn phoenix_transfer(
33        &self,
34        sender_idx: u8,
35        receiver_pk: &PhoenixPublicKey,
36        memo: Option<String>,
37        amt: Dusk,
38        gas: Gas,
39    ) -> Result<Transaction, Error> {
40        // make sure amount is positive
41        if amt == 0 && memo.is_none() {
42            return Err(Error::AmountIsZero);
43        }
44        // check gas limits
45        if !gas.is_enough() {
46            return Err(Error::NotEnoughGas);
47        }
48
49        let state = self.state()?;
50
51        let mut rng = StdRng::from_entropy();
52        let amt = *amt;
53
54        let mut sender_sk = self.derive_phoenix_sk(sender_idx);
55        let refund_pk = self.shielded_key(sender_idx)?;
56
57        let tx_cost = amt + gas.limit * gas.price;
58        let inputs = state
59            .tx_input_notes(sender_idx, tx_cost)
60            .await?
61            .into_iter()
62            .map(|(note, opening, _nullifier)| (note, opening))
63            .collect();
64
65        let root = state.fetch_root().await?;
66        let chain_id = state.fetch_chain_id().await?;
67
68        let tx = phoenix(
69            &mut rng,
70            &sender_sk,
71            refund_pk,
72            receiver_pk,
73            inputs,
74            root,
75            amt,
76            true,
77            0,
78            gas.limit,
79            gas.price,
80            chain_id,
81            memo,
82            &Prover,
83        )?;
84
85        sender_sk.zeroize();
86
87        let tx = state.prove(tx).await?;
88        state.propagate(tx).await
89    }
90
91    /// Transfers funds between public accounts.
92    pub async fn moonlight_transfer(
93        &self,
94        sender_idx: u8,
95        rcvr: &BlsPublicKey,
96        memo: Option<String>,
97        amt: Dusk,
98        gas: Gas,
99    ) -> Result<Transaction, Error> {
100        // make sure amount is positive
101        if amt == 0 && memo.is_none() {
102            return Err(Error::AmountIsZero);
103        }
104        // check gas limits
105        if !gas.is_enough() {
106            return Err(Error::NotEnoughGas);
107        }
108
109        let mut sender_sk = self.derive_bls_sk(sender_idx);
110        let sender_pk = self.public_key(sender_idx)?;
111        let amt = *amt;
112
113        let state = self.state()?;
114        let nonce = state.fetch_account(sender_pk).await?.nonce + 1;
115        let chain_id = state.fetch_chain_id().await?;
116
117        let tx = moonlight(
118            &sender_sk,
119            Some(*rcvr),
120            amt,
121            0,
122            gas.limit,
123            gas.price,
124            nonce,
125            chain_id,
126            memo,
127        )?;
128
129        sender_sk.zeroize();
130
131        state.propagate(tx).await
132    }
133
134    /// Executes a generic contract call, paying gas with a shielded account.
135    pub async fn phoenix_execute(
136        &self,
137        sender_idx: u8,
138        deposit: Dusk,
139        gas: Gas,
140        data: TransactionData,
141    ) -> Result<Transaction, Error> {
142        // check gas limits
143        if !gas.is_enough() {
144            return Err(Error::NotEnoughGas);
145        }
146
147        let state = self.state()?;
148        let deposit = *deposit;
149
150        let mut rng = StdRng::from_entropy();
151        let mut sender_sk = self.derive_phoenix_sk(sender_idx);
152        // in a contract execution or deployment, the sender and receiver are
153        // the same
154        let receiver_pk = self.shielded_key(sender_idx)?;
155
156        let tx_cost = deposit + gas.limit * gas.price;
157        let inputs = state
158            .tx_input_notes(sender_idx, tx_cost)
159            .await?
160            .into_iter()
161            .map(|(a, b, _)| (a, b))
162            .collect();
163
164        let root = state.fetch_root().await?;
165        let chain_id = state.fetch_chain_id().await?;
166
167        let tx = phoenix(
168            &mut rng,
169            &sender_sk,
170            self.shielded_key(sender_idx)?,
171            receiver_pk,
172            inputs,
173            root,
174            0,
175            true,
176            deposit,
177            gas.limit,
178            gas.price,
179            chain_id,
180            Some(data),
181            &Prover,
182        )?;
183
184        sender_sk.zeroize();
185
186        let tx = state.prove(tx).await?;
187        state.propagate(tx).await
188    }
189
190    /// Executes a generic contract call, paying gas from a public account.
191    #[allow(clippy::too_many_arguments)]
192    pub async fn moonlight_execute(
193        &self,
194        sender_idx: u8,
195        transfer_value: Dusk,
196        deposit: Dusk,
197        gas: Gas,
198        exec: Option<impl Into<TransactionData>>,
199    ) -> Result<Transaction, Error> {
200        // check gas limits
201        if !gas.is_enough() {
202            return Err(Error::NotEnoughGas);
203        }
204
205        let state = self.state()?;
206        let deposit = *deposit;
207
208        let mut sender_sk = self.derive_bls_sk(sender_idx);
209        let sender = self.public_key(sender_idx)?;
210
211        let account = state.fetch_account(sender).await?;
212
213        // technically this check is not necessary, but it's nice to not spam
214        // the network with transactions that are unspendable.
215        let nonce = account.nonce + 1;
216
217        let chain_id = state.fetch_chain_id().await?;
218
219        let tx = moonlight(
220            &sender_sk,
221            None,
222            *transfer_value,
223            deposit,
224            gas.limit,
225            gas.price,
226            nonce,
227            chain_id,
228            exec,
229        )?;
230
231        sender_sk.zeroize();
232
233        state.propagate(tx).await
234    }
235
236    /// Stakes Dusk using shielded notes.
237    pub async fn phoenix_stake(
238        &self,
239        profile_idx: u8,
240        owner_idx: Option<u8>,
241        amt: Dusk,
242        gas: Gas,
243    ) -> Result<Transaction, Error> {
244        // make sure amount is positive
245        if amt == 0 {
246            return Err(Error::AmountIsZero);
247        }
248        // check if the gas is enough
249        if !gas.is_enough() {
250            return Err(Error::NotEnoughGas);
251        }
252
253        let state = self.state()?;
254
255        let mut rng = StdRng::from_entropy();
256        let amt = *amt;
257        let mut sender_sk = self.derive_phoenix_sk(profile_idx);
258        let mut stake_sk = self.derive_bls_sk(profile_idx);
259
260        let stake_pk = self.public_key(profile_idx)?;
261        let stake_owner_idx = match self.find_stake_owner_idx(stake_pk).await {
262            Ok(state_idx) => {
263                if let Some(owner_idx) = owner_idx {
264                    if state_idx != owner_idx {
265                        return Err(Error::Unauthorized);
266                    }
267                }
268                state_idx
269            }
270            Err(Error::NotStaked) => owner_idx.unwrap_or(profile_idx),
271            Err(e) => {
272                return Err(e);
273            }
274        };
275        let mut stake_owner_sk = self.derive_bls_sk(stake_owner_idx);
276
277        let tx_cost = amt + gas.limit * gas.price;
278        let inputs = state
279            .tx_input_notes(profile_idx, tx_cost)
280            .await?
281            .into_iter()
282            .map(|(a, b, _)| (a, b))
283            .collect();
284
285        let root = state.fetch_root().await?;
286        let chain_id = state.fetch_chain_id().await?;
287
288        let stake = phoenix_stake(
289            &mut rng,
290            &sender_sk,
291            &stake_sk,
292            &stake_owner_sk,
293            inputs,
294            root,
295            gas.limit,
296            gas.price,
297            chain_id,
298            amt,
299            &Prover,
300        )?;
301
302        sender_sk.zeroize();
303        stake_sk.zeroize();
304        stake_owner_sk.zeroize();
305
306        let stake = state.prove(stake).await?;
307        state.propagate(stake).await
308    }
309
310    /// Stakes Dusk using a public account.
311    pub async fn moonlight_stake(
312        &self,
313        profile_idx: u8,
314        owner_idx: Option<u8>,
315        amt: Dusk,
316        gas: Gas,
317    ) -> Result<Transaction, Error> {
318        // make sure amount is positive
319        if amt == 0 {
320            return Err(Error::AmountIsZero);
321        }
322        // check if the gas is enough
323        if !gas.is_enough() {
324            return Err(Error::NotEnoughGas);
325        }
326
327        let state = self.state()?;
328        let amt = *amt;
329        let mut stake_sk = self.derive_bls_sk(profile_idx);
330        let stake_pk = self.public_key(profile_idx)?;
331        let chain_id = state.fetch_chain_id().await?;
332        let moonlight_current_nonce =
333            state.fetch_account(stake_pk).await?.nonce + 1;
334
335        let stake_owner_idx = match self.find_stake_owner_idx(stake_pk).await {
336            Ok(state_idx) => {
337                if let Some(owner_idx) = owner_idx {
338                    if state_idx != owner_idx {
339                        return Err(Error::Unauthorized);
340                    }
341                }
342                state_idx
343            }
344            Err(Error::NotStaked) => owner_idx.unwrap_or(profile_idx),
345            Err(e) => {
346                return Err(e);
347            }
348        };
349        let mut stake_owner_sk = self.derive_bls_sk(stake_owner_idx);
350
351        let stake = moonlight_stake(
352            &stake_sk,
353            &stake_sk,
354            &stake_owner_sk,
355            amt,
356            gas.limit,
357            gas.price,
358            moonlight_current_nonce,
359            chain_id,
360        )?;
361
362        stake_sk.zeroize();
363        stake_owner_sk.zeroize();
364
365        state.propagate(stake).await
366    }
367
368    /// Unstakes Dusk into shielded notes.
369    pub async fn phoenix_unstake(
370        &self,
371        profile_idx: u8,
372        gas: Gas,
373    ) -> Result<Transaction, Error> {
374        let mut rng = StdRng::from_entropy();
375
376        let state = self.state()?;
377
378        let mut sender_sk = self.derive_phoenix_sk(profile_idx);
379        let mut stake_sk = self.derive_bls_sk(profile_idx);
380        let stake_pk = BlsPublicKey::from(&stake_sk);
381
382        let stake_owner_idx = self.find_stake_owner_idx(&stake_pk).await?;
383        let mut stake_owner_sk = self.derive_bls_sk(stake_owner_idx);
384
385        let unstake_value = state
386            .fetch_stake(&stake_pk)
387            .await?
388            .and_then(|s| s.amount)
389            .map(|s| s.total_funds())
390            .unwrap_or_default();
391
392        if unstake_value == 0 {
393            return Err(Error::NotStaked);
394        }
395
396        let tx_cost = gas.limit * gas.price;
397        let inputs = state.tx_input_notes(profile_idx, tx_cost).await?;
398
399        let root = state.fetch_root().await?;
400        let chain_id = state.fetch_chain_id().await?;
401
402        let unstake = phoenix_unstake(
403            &mut rng,
404            &sender_sk,
405            &stake_sk,
406            &stake_owner_sk,
407            inputs,
408            root,
409            unstake_value,
410            gas.limit,
411            gas.price,
412            chain_id,
413            &Prover,
414        )?;
415
416        sender_sk.zeroize();
417        stake_sk.zeroize();
418        stake_owner_sk.zeroize();
419
420        let unstake = state.prove(unstake).await?;
421        state.propagate(unstake).await
422    }
423
424    /// Unstakes Dusk onto a public account.
425    pub async fn moonlight_unstake(
426        &self,
427        profile_idx: u8,
428        gas: Gas,
429    ) -> Result<Transaction, Error> {
430        let mut rng = StdRng::from_entropy();
431        let state = self.state()?;
432        let mut stake_sk = self.derive_bls_sk(profile_idx);
433
434        let stake_pk = self.public_key(profile_idx)?;
435
436        let chain_id = state.fetch_chain_id().await?;
437        let account_nonce = state.fetch_account(stake_pk).await?.nonce + 1;
438
439        let unstake_value = state
440            .fetch_stake(stake_pk)
441            .await?
442            .and_then(|s| s.amount)
443            .map(|s| s.total_funds())
444            .unwrap_or_default();
445
446        if unstake_value == 0 {
447            return Err(Error::NotStaked);
448        }
449
450        let stake_owner_idx = self.find_stake_owner_idx(stake_pk).await?;
451        let mut stake_owner_sk = self.derive_bls_sk(stake_owner_idx);
452
453        let unstake = moonlight_unstake(
454            &mut rng,
455            &stake_sk,
456            &stake_sk,
457            &stake_owner_sk,
458            unstake_value,
459            gas.limit,
460            gas.price,
461            account_nonce,
462            chain_id,
463        )?;
464
465        stake_sk.zeroize();
466        stake_owner_sk.zeroize();
467
468        state.propagate(unstake).await
469    }
470
471    /// Withdraws accumulated staking to a shielded account.
472    pub async fn phoenix_stake_withdraw(
473        &self,
474        sender_idx: u8,
475        gas: Gas,
476    ) -> Result<Transaction, Error> {
477        let state = self.state()?;
478        let mut rng = StdRng::from_entropy();
479
480        let mut sender_sk = self.derive_phoenix_sk(sender_idx);
481        let mut stake_sk = self.derive_bls_sk(sender_idx);
482
483        let tx_cost = gas.limit * gas.price;
484        let inputs = state.tx_input_notes(sender_idx, tx_cost).await?;
485
486        let root = state.fetch_root().await?;
487        let chain_id = state.fetch_chain_id().await?;
488
489        let stake_pk = BlsPublicKey::from(&stake_sk);
490
491        let reward_amount = state
492            .fetch_stake(&stake_pk)
493            .await?
494            .map(|s| s.reward)
495            .unwrap_or(0);
496
497        let stake_owner_idx = self.find_stake_owner_idx(&stake_pk).await?;
498        let mut stake_owner_sk = self.derive_bls_sk(stake_owner_idx);
499
500        let withdraw = phoenix_stake_reward(
501            &mut rng,
502            &sender_sk,
503            &stake_sk,
504            &stake_owner_sk,
505            inputs,
506            root,
507            reward_amount,
508            gas.limit,
509            gas.price,
510            chain_id,
511            &Prover,
512        )?;
513
514        sender_sk.zeroize();
515        stake_sk.zeroize();
516        stake_owner_sk.zeroize();
517
518        let withdraw = state.prove(withdraw).await?;
519        state.propagate(withdraw).await
520    }
521
522    /// Withdraws accumulated staking reward to a public account.
523    pub async fn moonlight_stake_withdraw(
524        &self,
525        sender_idx: u8,
526        gas: Gas,
527    ) -> Result<Transaction, Error> {
528        let mut rng = StdRng::from_entropy();
529        let state = self.state()?;
530
531        let pk = self.public_key(sender_idx)?;
532        let nonce = state.fetch_account(pk).await?.nonce + 1;
533        let chain_id = state.fetch_chain_id().await?;
534        let stake_info = state.fetch_stake(pk).await?;
535        let reward = stake_info.map(|s| s.reward).ok_or(Error::NoReward)?;
536        let reward = Dusk::from(reward);
537
538        let mut sender_sk = self.derive_bls_sk(sender_idx);
539
540        let stake_pk = self.public_key(sender_idx)?;
541        let stake_owner_idx = self.find_stake_owner_idx(stake_pk).await?;
542        let mut stake_owner_sk = self.derive_bls_sk(stake_owner_idx);
543
544        let withdraw = moonlight_stake_reward(
545            &mut rng,
546            &sender_sk,
547            &sender_sk,
548            &stake_owner_sk,
549            *reward,
550            gas.limit,
551            gas.price,
552            nonce,
553            chain_id,
554        )?;
555
556        sender_sk.zeroize();
557        stake_owner_sk.zeroize();
558
559        state.propagate(withdraw).await
560    }
561
562    /// Converts Dusk from a shielded account to a public account.
563    pub async fn phoenix_to_moonlight(
564        &self,
565        profile_idx: u8,
566        amt: Dusk,
567        gas: Gas,
568    ) -> Result<Transaction, Error> {
569        let mut rng = StdRng::from_entropy();
570        let state = self.state()?;
571        let tx_cost = *amt + gas.limit * gas.price;
572        let inputs = state.tx_input_notes(profile_idx, tx_cost).await?;
573
574        let root = state.fetch_root().await?;
575        let chain_id = state.fetch_chain_id().await?;
576
577        let mut phoenix_sk = self.derive_phoenix_sk(profile_idx);
578        let mut moonlight_sk = self.derive_bls_sk(profile_idx);
579
580        let convert = phoenix_to_moonlight(
581            &mut rng,
582            &phoenix_sk,
583            &moonlight_sk,
584            inputs,
585            root,
586            *amt,
587            gas.limit,
588            gas.price,
589            chain_id,
590            &Prover,
591        )?;
592
593        phoenix_sk.zeroize();
594        moonlight_sk.zeroize();
595
596        let convert = state.prove(convert).await?;
597        state.propagate(convert).await
598    }
599
600    /// Converts Dusk from a public account to a shielded account.
601    pub async fn moonlight_to_phoenix(
602        &self,
603        profile_idx: u8,
604        amt: Dusk,
605        gas: Gas,
606    ) -> Result<Transaction, Error> {
607        let mut rng = StdRng::from_entropy();
608        let state = self.state()?;
609
610        let moonlight_pk = self.public_key(profile_idx)?;
611
612        let nonce = state.fetch_account(moonlight_pk).await?.nonce + 1;
613        let chain_id = state.fetch_chain_id().await?;
614
615        let mut phoenix_sk = self.derive_phoenix_sk(profile_idx);
616        let mut moonlight_sk = self.derive_bls_sk(profile_idx);
617
618        let convert = moonlight_to_phoenix(
619            &mut rng,
620            &moonlight_sk,
621            &phoenix_sk,
622            *amt,
623            gas.limit,
624            gas.price,
625            nonce,
626            chain_id,
627        )?;
628
629        phoenix_sk.zeroize();
630        moonlight_sk.zeroize();
631
632        state.propagate(convert).await
633    }
634
635    /// Deploys a contract using shielded notes to pay gas.
636    pub async fn phoenix_deploy(
637        &self,
638        sender_idx: u8,
639        bytes_code: Vec<u8>,
640        init_args: Vec<u8>,
641        deploy_nonce: u64,
642        gas: Gas,
643    ) -> Result<Transaction, Error> {
644        let mut rng = StdRng::from_entropy();
645        let state = self.state()?;
646
647        let chain_id = state.fetch_chain_id().await?;
648        let root = state.fetch_root().await?;
649
650        let tx_cost = gas.limit * gas.price;
651        let inputs = state.tx_input_notes(sender_idx, tx_cost).await?;
652
653        let mut sender_sk = self.derive_phoenix_sk(sender_idx);
654        let owner_pk = self.public_key(sender_idx)?;
655
656        let deploy = phoenix_deployment(
657            &mut rng,
658            &sender_sk,
659            inputs,
660            root,
661            bytes_code,
662            owner_pk,
663            init_args,
664            deploy_nonce,
665            gas.limit,
666            gas.price,
667            chain_id,
668            &Prover,
669        )?;
670
671        sender_sk.zeroize();
672
673        let deploy = state.prove(deploy).await?;
674        state.propagate(deploy).await
675    }
676
677    /// Deploys a contract using a public account to pay gas.
678    pub async fn moonlight_deploy(
679        &self,
680        sender_idx: u8,
681        bytes_code: Vec<u8>,
682        init_args: Vec<u8>,
683        deploy_nonce: u64,
684        gas: Gas,
685    ) -> Result<Transaction, Error> {
686        let state = self.state()?;
687
688        let pk = self.public_key(sender_idx)?;
689        let moonlight_nonce = state.fetch_account(pk).await?.nonce + 1;
690        let chain_id = state.fetch_chain_id().await?;
691
692        let mut sender_sk = self.derive_bls_sk(sender_idx);
693
694        let deploy = moonlight_deployment(
695            &sender_sk,
696            bytes_code,
697            pk,
698            init_args,
699            gas.limit,
700            gas.price,
701            moonlight_nonce,
702            deploy_nonce,
703            chain_id,
704        )?;
705
706        sender_sk.zeroize();
707
708        state.propagate(deploy).await
709    }
710
711    /// Finds the index of the stake owner account.
712    pub async fn find_stake_owner_idx(
713        &self,
714        stake_pk: &BlsPublicKey,
715    ) -> Result<u8, Error> {
716        self.find_index(&self.find_stake_owner_account(stake_pk).await?)
717    }
718
719    /// Finds the address of the stake owner account.
720    pub async fn find_stake_owner_account(
721        &self,
722        stake_pk: &BlsPublicKey,
723    ) -> Result<Address, Error> {
724        let stake_owner = self
725            .state()?
726            .fetch_stake_owner(stake_pk)
727            .await?
728            .ok_or(Error::NotStaked)?;
729
730        match stake_owner {
731            StakeFundOwner::Account(public_key) => {
732                Ok(Address::Public(public_key))
733            }
734            StakeFundOwner::Contract(_) => Err(Error::Unauthorized),
735        }
736    }
737}