1use super::*;
18
19impl<N: Network> ProgramManager<N> {
20 #[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 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 let query = Query::from(self.api_client.as_ref().unwrap().base_url());
46
47 let private_key = self.get_private_key(password)?;
49
50 let execution = {
52 let rng = &mut rand::thread_rng();
53
54 let store = ConsensusStore::<N, ConsensusMemory<N>>::open(None)?;
56 let vm = VM::from(store)?;
57
58 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 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 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 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 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 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 try_transfer(&beacon_private_key, &private_recipient_address, amount, TransferType::Private);
268
269 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_transfer(amount, &api_client, &private_recipient_private_key, TransferType::Private);
278
279 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 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}