aleo_rust/program/
transfer.rs

1// Copyright (C) 2019-2023 Aleo Systems Inc.
2// This file is part of the Aleo SDK library.
3
4// The Aleo SDK library is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// The Aleo SDK library is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with the Aleo SDK library. If not, see <https://www.gnu.org/licenses/>.
16
17use super::*;
18
19impl<N: Network> ProgramManager<N> {
20    /// Executes a transfer to the specified recipient_address with the specified amount and fee.
21    /// Specify 0 for no fee.
22    #[allow(clippy::too_many_arguments)]
23    pub fn transfer(
24        &self,
25        amount: u64,
26        fee: u64,
27        recipient_address: Address<N>,
28        transfer_type: TransferType,
29        password: Option<&str>,
30        amount_record: Option<Record<N, Plaintext<N>>>,
31        fee_record: Option<Record<N, Plaintext<N>>>,
32    ) -> Result<String> {
33        // Ensure records provided have enough credits to cover the transfer amount and fee
34        if let Some(amount_record) = amount_record.as_ref() {
35            ensure!(
36                amount_record.microcredits()? >= amount,
37                "Credits in amount record must greater than transfer amount specified"
38            );
39        }
40        if let Some(fee_record) = fee_record.as_ref() {
41            ensure!(fee_record.microcredits()? >= fee, "Credits in fee record must greater than fee specified");
42        }
43
44        // Specify the network state query
45        let query = Query::from(self.api_client.as_ref().unwrap().base_url());
46
47        // Retrieve the private key.
48        let private_key = self.get_private_key(password)?;
49
50        // Generate the execution transaction
51        let execution = {
52            let rng = &mut rand::thread_rng();
53
54            // Initialize a VM
55            let store = ConsensusStore::<N, ConsensusMemory<N>>::open(None)?;
56            let vm = VM::from(store)?;
57
58            // Prepare the inputs for a transfer.
59            let (transfer_function, inputs) = match transfer_type {
60                TransferType::Public => {
61                    let inputs = vec![
62                        Value::from_str(&recipient_address.to_string())?,
63                        Value::from_str(&format!("{}u64", amount))?,
64                    ];
65                    ("transfer_public", inputs)
66                }
67                TransferType::Private => {
68                    if amount_record.is_none() {
69                        bail!("Amount record must be specified for private transfers");
70                    } else {
71                        let inputs = vec![
72                            Value::Record(amount_record.unwrap()),
73                            Value::from_str(&recipient_address.to_string())?,
74                            Value::from_str(&format!("{}u64", amount))?,
75                        ];
76                        ("transfer_private", inputs)
77                    }
78                }
79                TransferType::PublicToPrivate => {
80                    let inputs = vec![
81                        Value::from_str(&recipient_address.to_string())?,
82                        Value::from_str(&format!("{}u64", amount))?,
83                    ];
84                    ("transfer_public_to_private", inputs)
85                }
86                TransferType::PrivateToPublic => {
87                    if amount_record.is_none() {
88                        bail!("Amount record must be specified for private transfers");
89                    } else {
90                        let inputs = vec![
91                            Value::Record(amount_record.unwrap()),
92                            Value::from_str(&recipient_address.to_string())?,
93                            Value::from_str(&format!("{}u64", amount))?,
94                        ];
95                        ("transfer_private_to_public", inputs)
96                    }
97                }
98            };
99
100            // Create a new transaction.
101            vm.execute(
102                &private_key,
103                ("credits.aleo", transfer_function),
104                inputs.iter(),
105                fee_record,
106                fee,
107                Some(query),
108                rng,
109            )?
110        };
111
112        self.broadcast_transaction(execution)
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119    use crate::{test_utils::BEACON_PRIVATE_KEY, AleoAPIClient, RecordFinder};
120    use snarkvm_console::network::Testnet3;
121
122    use std::{str::FromStr, thread};
123
124    // Attempt to transfer the specified amount from the sender to the recipient.
125    fn try_transfer(
126        sender: &PrivateKey<Testnet3>,
127        recipient: &Address<Testnet3>,
128        amount: u64,
129        visibility: TransferType,
130    ) {
131        println!("Attempting to transfer of type: {visibility:?} of {amount} to {recipient:?}");
132        let api_client = AleoAPIClient::<Testnet3>::local_testnet3("3033");
133        let program_manager =
134            ProgramManager::<Testnet3>::new(Some(*sender), None, Some(api_client.clone()), None, false).unwrap();
135        let record_finder = RecordFinder::new(api_client);
136        let fee = 5_000_000;
137        for i in 0..10 {
138            let (amount_record, fee_record) = match &visibility {
139                TransferType::Public => {
140                    let fee_record = record_finder.find_one_record(sender, fee, None);
141                    if fee_record.is_err() {
142                        println!("Record not found: {} - retrying", fee_record.unwrap_err());
143                        thread::sleep(std::time::Duration::from_secs(3));
144                        if i == 9 {
145                            panic!("Transfer failed after 10 attempts");
146                        }
147                        continue;
148                    }
149                    (None, fee_record.unwrap())
150                }
151                TransferType::PublicToPrivate => {
152                    let fee_record = record_finder.find_one_record(sender, fee, None);
153                    if fee_record.is_err() {
154                        println!("Record not found: {} - retrying", fee_record.unwrap_err());
155                        thread::sleep(std::time::Duration::from_secs(3));
156                        if i == 9 {
157                            panic!("Transfer failed after 10 attempts");
158                        }
159                        continue;
160                    }
161                    (None, fee_record.unwrap())
162                }
163                _ => {
164                    let record = record_finder.find_amount_and_fee_records(amount, fee, sender);
165                    if record.is_err() {
166                        println!("Record not found: {} - retrying", record.unwrap_err());
167                        thread::sleep(std::time::Duration::from_secs(3));
168                        continue;
169                    }
170                    let (amount_record, fee_record) = record.unwrap();
171                    (Some(amount_record), fee_record)
172                }
173            };
174
175            let result =
176                program_manager.transfer(amount, fee, *recipient, visibility, None, amount_record, Some(fee_record));
177            if result.is_err() {
178                println!("Transfer error: {} - retrying", result.unwrap_err());
179                if i == 9 {
180                    panic!("Transfer failed after 10 attempts");
181                }
182            } else {
183                break;
184            }
185        }
186    }
187
188    // Check that the specified amount has been transferred from the sender to the recipient.
189    fn verify_transfer(
190        amount: u64,
191        api_client: &AleoAPIClient<Testnet3>,
192        recipient_private_key: &PrivateKey<Testnet3>,
193        visibility: TransferType,
194    ) {
195        for i in 0..10 {
196            println!("Attempting to verify transfer of visibility: {visibility:?} for amount: {amount}");
197            let height = api_client.latest_height().unwrap();
198            let records = api_client.get_unspent_records(recipient_private_key, 0..height, None, None).unwrap();
199            let mut is_verified = false;
200            if !records.is_empty() {
201                for record in records.iter() {
202                    let (_, record) = record;
203                    let record_amount = record.microcredits().unwrap();
204                    println!("Found amount: {record_amount} - expected: {amount}");
205                    if amount == record_amount {
206                        println!("✅ Transfer of {amount} verified for transfer type: {visibility:?}");
207                        is_verified = true;
208                        break;
209                    }
210                }
211                if is_verified {
212                    break;
213                }
214            }
215            thread::sleep(std::time::Duration::from_secs(3));
216            if i > 8 {
217                let error =
218                    format!("❌ Failed to verify transfer of visibility: {visibility:?} after 8 transfer errors");
219                panic!("{error}");
220            }
221        }
222    }
223
224    #[test]
225    #[ignore]
226    fn test_transfer_roundtrip() {
227        // Initialize necessary key material
228        // Use the beacon private key to make the initial transfer
229        let beacon_private_key = PrivateKey::<Testnet3>::from_str(BEACON_PRIVATE_KEY).unwrap();
230        let amount = 16_666_666;
231        let amount_str = "16666666u64";
232        let fee = 26_666_666;
233        // Create a unique recipient for each transfer type so we can unique identify each transfer
234        let private_recipient_private_key =
235            PrivateKey::<Testnet3>::from_str("APrivateKey1zkpCF24vGLohfHWNZam1z7qDhDq5Bp1u8WPFhh9q2wWoNh8").unwrap();
236        let private_recipient_view_key = ViewKey::try_from(&private_recipient_private_key).unwrap();
237        let private_recipient_address = Address::try_from(&private_recipient_view_key).unwrap();
238        let private_to_public_recipient_private_key =
239            PrivateKey::<Testnet3>::from_str("APrivateKey1zkpFkojCEFsw3LaozkqaVkMuxdTq2DEsUV88WGexXoWGuNA").unwrap();
240        let private_to_public_recipient_view_key = ViewKey::try_from(&private_to_public_recipient_private_key).unwrap();
241        let private_to_public_recipient_address = Address::try_from(&private_to_public_recipient_view_key).unwrap();
242        let public_recipient_private_key =
243            PrivateKey::<Testnet3>::from_str("APrivateKey1zkp6rwhE5Jxz16cv32kM8Z8VMsLe78BCK8LU3FM7htmSsis").unwrap();
244        let public_recipient_view_key = ViewKey::try_from(&public_recipient_private_key).unwrap();
245        let public_recipient_address = Address::try_from(&public_recipient_view_key).unwrap();
246        let public_to_private_recipient_private_key =
247            PrivateKey::<Testnet3>::from_str("APrivateKey1zkp3NchSbrypyf2UoJSGyag58biAFPvtd1WtpM5M9pqoifK").unwrap();
248        let public_to_private_recipient_view_key = ViewKey::try_from(&public_to_private_recipient_private_key).unwrap();
249        let public_to_private_recipient_address = Address::try_from(&public_to_private_recipient_view_key).unwrap();
250        let api_client = AleoAPIClient::<Testnet3>::local_testnet3("3033");
251        let public_address_literal = Literal::<Testnet3>::from_str(&public_recipient_address.to_string()).unwrap();
252        let private_to_public_address_literal =
253            Literal::<Testnet3>::from_str(&private_to_public_recipient_address.to_string()).unwrap();
254        let expected_value = Value::from(Plaintext::<Testnet3>::from_str(amount_str).unwrap());
255        let zero_value = Value::from(Plaintext::<Testnet3>::from_str("0u64").unwrap());
256
257        println!("Private recipient private_key: {}", private_recipient_private_key);
258        println!("Private recipient address: {}", private_recipient_address);
259        println!("Private to public recipient private_key: {}", private_to_public_recipient_private_key);
260        println!("Private to public recipient address: {}", private_to_public_recipient_address);
261        println!("Public recipient private_key: {}", public_recipient_private_key);
262        println!("Public recipient address: {}", public_recipient_address);
263        println!("Public to private recipient private_key: {}", public_to_private_recipient_private_key);
264        println!("Public to private recipient address: {}", public_to_private_recipient_address);
265
266        // Transfer funds to the private recipient and confirm that the transaction is on chain
267        try_transfer(&beacon_private_key, &private_recipient_address, amount, TransferType::Private);
268
269        // Transfer funds to each of the other recipients to pay the fee with
270        try_transfer(&beacon_private_key, &private_recipient_address, fee, TransferType::Private);
271        try_transfer(&beacon_private_key, &private_to_public_recipient_address, fee, TransferType::Private);
272        try_transfer(&beacon_private_key, &public_recipient_address, fee, TransferType::Private);
273        try_transfer(&beacon_private_key, &public_to_private_recipient_address, fee, TransferType::Private);
274        thread::sleep(std::time::Duration::from_secs(20));
275
276        // Verify private transfer
277        verify_transfer(amount, &api_client, &private_recipient_private_key, TransferType::Private);
278
279        // Transfer funds to the private_to_public recipient
280        try_transfer(
281            &private_recipient_private_key,
282            &private_to_public_recipient_address,
283            amount,
284            TransferType::PrivateToPublic,
285        );
286        thread::sleep(std::time::Duration::from_secs(25));
287        let value =
288            api_client.get_mapping_value("credits.aleo", "account", &private_to_public_address_literal).unwrap();
289        assert!(value.eq(&expected_value));
290
291        try_transfer(&private_to_public_recipient_private_key, &public_recipient_address, amount, TransferType::Public);
292        thread::sleep(std::time::Duration::from_secs(25));
293        let value = api_client.get_mapping_value("credits.aleo", "account", public_address_literal).unwrap();
294        assert!(value.eq(&expected_value));
295        let value =
296            api_client.get_mapping_value("credits.aleo", "account", &private_to_public_address_literal).unwrap();
297        assert!(value.eq(&zero_value));
298
299        // Transfer funds to the public_to_private recipient and ensure the funds made the entire journey
300        try_transfer(
301            &public_recipient_private_key,
302            &public_to_private_recipient_address,
303            amount,
304            TransferType::PublicToPrivate,
305        );
306        thread::sleep(std::time::Duration::from_secs(25));
307        verify_transfer(amount, &api_client, &public_to_private_recipient_private_key, TransferType::PublicToPrivate);
308    }
309}