quantus_cli/cli/
treasury.rs

1//! `quantus treasury` subcommand - manage Treasury
2use crate::{
3	chain::quantus_subxt,
4	cli::{common::submit_transaction, progress_spinner::wait_for_tx_confirmation},
5	log_print, log_success,
6};
7use clap::Subcommand;
8use colored::Colorize;
9
10/// Treasury management commands
11#[derive(Subcommand, Debug)]
12pub enum TreasuryCommands {
13	/// Check current Treasury balance
14	Balance,
15
16	/// Get Treasury configuration  
17	Config,
18
19	/// Show Treasury information and how to spend from it
20	Info,
21
22	/// Submit a Treasury spend proposal via referendum (requires specific track)
23	/// This creates a referendum that, if approved, will approve a treasury spend
24	SubmitSpend {
25		/// Beneficiary address (will receive the funds)
26		#[arg(long)]
27		beneficiary: String,
28
29		/// Amount to spend (e.g., "100.0" for 100 QUAN)
30		#[arg(long)]
31		amount: String,
32
33		/// Track to use: "small", "medium", "big", or "treasurer"
34		/// - small: < 100 QUAN (Track 2)
35		/// - medium: < 1000 QUAN (Track 3)
36		/// - big: < 10000 QUAN (Track 4)
37		/// - treasurer: any amount (Track 5)
38		#[arg(long)]
39		track: String,
40
41		/// Wallet name to sign the transaction
42		#[arg(long)]
43		from: String,
44
45		/// Password for the wallet
46		#[arg(long)]
47		password: Option<String>,
48
49		/// Read password from file
50		#[arg(long)]
51		password_file: Option<String>,
52	},
53
54	/// Payout an approved Treasury spend (anyone can call this)
55	Payout {
56		/// Spend index to payout
57		#[arg(long)]
58		index: u32,
59
60		/// Wallet name to sign the transaction
61		#[arg(long)]
62		from: String,
63
64		/// Password for the wallet
65		#[arg(long)]
66		password: Option<String>,
67
68		/// Read password from file
69		#[arg(long)]
70		password_file: Option<String>,
71	},
72
73	/// Check and cleanup a Treasury spend status
74	CheckStatus {
75		/// Spend index to check
76		#[arg(long)]
77		index: u32,
78
79		/// Wallet name to sign the transaction
80		#[arg(long)]
81		from: String,
82
83		/// Password for the wallet
84		#[arg(long)]
85		password: Option<String>,
86
87		/// Read password from file
88		#[arg(long)]
89		password_file: Option<String>,
90	},
91
92	/// List active Treasury spends
93	ListSpends,
94
95	/// Directly create a Treasury spend via sudo (root only, for testing)
96	SpendSudo {
97		/// Beneficiary address (will receive the funds)
98		#[arg(long)]
99		beneficiary: String,
100
101		/// Amount to spend (e.g., "50.0" for 50 QUAN)
102		#[arg(long)]
103		amount: String,
104
105		/// Wallet name to sign with (must have sudo)
106		#[arg(long)]
107		from: String,
108
109		/// Password for the wallet
110		#[arg(long)]
111		password: Option<String>,
112
113		/// Read password from file
114		#[arg(long)]
115		password_file: Option<String>,
116	},
117}
118
119/// Handle treasury commands
120pub async fn handle_treasury_command(
121	command: TreasuryCommands,
122	node_url: &str,
123) -> crate::error::Result<()> {
124	let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
125
126	match command {
127		TreasuryCommands::Balance => get_treasury_balance(&quantus_client).await,
128		TreasuryCommands::Config => get_config(&quantus_client).await,
129		TreasuryCommands::Info => show_treasury_info().await,
130		TreasuryCommands::SubmitSpend {
131			beneficiary,
132			amount,
133			track,
134			from,
135			password,
136			password_file,
137		} =>
138			submit_spend_referendum(
139				&quantus_client,
140				&beneficiary,
141				&amount,
142				&track,
143				&from,
144				password,
145				password_file,
146			)
147			.await,
148		TreasuryCommands::Payout { index, from, password, password_file } =>
149			payout_spend(&quantus_client, index, &from, password, password_file).await,
150		TreasuryCommands::CheckStatus { index, from, password, password_file } =>
151			check_spend_status(&quantus_client, index, &from, password, password_file).await,
152		TreasuryCommands::ListSpends => list_spends(&quantus_client).await,
153		TreasuryCommands::SpendSudo { beneficiary, amount, from, password, password_file } =>
154			spend_sudo(&quantus_client, &beneficiary, &amount, &from, password, password_file).await,
155	}
156}
157
158/// Get current Treasury balance
159async fn get_treasury_balance(
160	quantus_client: &crate::chain::client::QuantusClient,
161) -> crate::error::Result<()> {
162	log_print!("💰 Treasury Balance");
163	log_print!("");
164
165	// Get Treasury account ID
166	// PalletId("py/trsry") converts to account using "modl" prefix
167	let mut full_data = [0u8; 32];
168	full_data[0..4].copy_from_slice(b"modl");
169	full_data[4..12].copy_from_slice(b"py/trsry");
170	let treasury_account = subxt::utils::AccountId32(full_data);
171
172	// Query balance
173	let addr = quantus_subxt::api::storage().system().account(treasury_account.clone());
174
175	let latest_block_hash = quantus_client.get_latest_block().await?;
176	let storage_at = quantus_client.client().storage().at(latest_block_hash);
177
178	let account_info = storage_at.fetch(&addr).await?.ok_or_else(|| {
179		crate::error::QuantusError::Generic("Treasury account not found".to_string())
180	})?;
181
182	let free_balance = account_info.data.free;
183	let reserved_balance = account_info.data.reserved;
184
185	let formatted_free_balance =
186		crate::cli::send::format_balance_with_symbol(quantus_client, free_balance).await?;
187	let formatted_reserved_balance =
188		crate::cli::send::format_balance_with_symbol(quantus_client, reserved_balance).await?;
189
190	log_print!("💰 Free Balance: {}", formatted_free_balance);
191	log_print!("💰 Reserved: {}", formatted_reserved_balance);
192	log_print!("📍 Treasury Account: {}", treasury_account.to_string().bright_yellow());
193
194	Ok(())
195}
196
197/// Get Treasury configuration
198async fn get_config(
199	quantus_client: &crate::chain::client::QuantusClient,
200) -> crate::error::Result<()> {
201	log_print!("⚙️  Treasury Configuration");
202	log_print!("");
203
204	let constants = quantus_client.client().constants();
205
206	// Get SpendPeriod
207	if let Ok(spend_period) =
208		constants.at(&quantus_subxt::api::constants().treasury_pallet().spend_period())
209	{
210		log_print!("⏰ Spend Period: {} blocks", spend_period.to_string().bright_cyan());
211		let hours = spend_period as f64 * 3.0 / 3600.0; // Assuming 3 sec blocks
212		log_print!("   (~{:.1} hours)", hours);
213	}
214
215	// Get Burn percentage
216	if let Ok(burn) = constants.at(&quantus_subxt::api::constants().treasury_pallet().burn()) {
217		log_print!("🔥 Burn: {:?}", burn);
218	}
219
220	// Get MaxApprovals
221	if let Ok(max_approvals) =
222		constants.at(&quantus_subxt::api::constants().treasury_pallet().max_approvals())
223	{
224		log_print!("📊 Max Approvals: {}", max_approvals.to_string().bright_yellow());
225	}
226
227	// Get PayoutPeriod
228	if let Ok(payout_period) =
229		constants.at(&quantus_subxt::api::constants().treasury_pallet().payout_period())
230	{
231		log_print!("💸 Payout Period: {} blocks", payout_period.to_string().bright_green());
232		let days = payout_period as f64 * 3.0 / 86400.0; // Assuming 3 sec blocks
233		log_print!("   (~{:.1} days)", days);
234	}
235
236	Ok(())
237}
238
239/// Show Treasury information
240async fn show_treasury_info() -> crate::error::Result<()> {
241	log_print!("💰 Treasury Information");
242	log_print!("");
243	log_print!("The Treasury is a pot of funds collected through:");
244	log_print!("   • Transaction fees");
245	log_print!("   • Slashing");
246	log_print!("   • Other network mechanisms");
247	log_print!("");
248	log_print!("📋 {} To spend from Treasury:", "HOW TO USE".bright_cyan().bold());
249	log_print!("");
250	log_print!(
251		"1. {} Create a spending proposal using Referenda:",
252		"Treasury Tracks".bright_yellow().bold()
253	);
254	log_print!("   • Track 2: Treasury Small Spender (< certain amount)");
255	log_print!("   • Track 3: Treasury Medium Spender");
256	log_print!("   • Track 4: Treasury Big Spender");
257	log_print!("   • Track 5: Treasury Treasurer (highest amounts)");
258	log_print!("");
259	log_print!(
260		"2. {} Submit referendum with Treasury spend call:",
261		"Example".bright_green().bold()
262	);
263	log_print!(
264		"   quantus referenda submit-remark --message \"Treasury spend: 1000 QUAN to Alice\""
265	);
266	log_print!("   --from <YOUR_WALLET> --password <PASSWORD>");
267	log_print!("");
268	log_print!("   Note: Use appropriate origin for treasury tracks");
269	log_print!("");
270	log_print!("3. {} Community votes on the proposal", "Voting".bright_magenta().bold());
271	log_print!("");
272	log_print!("4. {} If approved, funds are paid automatically", "Execution".bright_blue().bold());
273	log_print!("");
274	log_print!("💡 {}", "Useful Commands:".bright_cyan().bold());
275	log_print!("   quantus treasury balance     - Check Treasury balance");
276	log_print!("   quantus treasury config      - View Treasury configuration");
277	log_print!("   quantus referenda config     - View available tracks");
278	log_print!("");
279
280	Ok(())
281}
282
283/// Submit a Treasury spend proposal via referendum
284async fn submit_spend_referendum(
285	quantus_client: &crate::chain::client::QuantusClient,
286	beneficiary: &str,
287	amount: &str,
288	track: &str,
289	from: &str,
290	password: Option<String>,
291	password_file: Option<String>,
292) -> crate::error::Result<()> {
293	use qp_poseidon::PoseidonHasher;
294	use sp_core::crypto::{AccountId32 as SpAccountId32, Ss58Codec};
295	use subxt::tx::Payload;
296
297	log_print!("💰 Submitting Treasury Spend Referendum");
298	log_print!("   📍 Beneficiary: {}", beneficiary.bright_yellow());
299	log_print!("   💵 Amount: {}", amount.bright_green());
300	log_print!("   🛤️  Track: {}", track.bright_cyan());
301
302	// Parse amount
303	let amount_value = crate::cli::send::parse_amount(quantus_client, amount).await?;
304
305	// Parse beneficiary address
306	let beneficiary_resolved = crate::cli::common::resolve_address(beneficiary)?;
307	let (beneficiary_sp, _) = SpAccountId32::from_ss58check_with_version(&beneficiary_resolved)
308		.map_err(|e| {
309			crate::error::QuantusError::Generic(format!(
310				"Invalid beneficiary address '{beneficiary}': {e:?}"
311			))
312		})?;
313	let bytes: [u8; 32] = *beneficiary_sp.as_ref();
314	let beneficiary_account = subxt::utils::AccountId32::from(bytes);
315
316	// Create the treasury spend call using the transaction API
317	let beneficiary_multi = subxt::utils::MultiAddress::Id(beneficiary_account.clone());
318
319	let treasury_spend_call =
320		quantus_subxt::api::tx()
321			.treasury_pallet()
322			.spend((), amount_value, beneficiary_multi, None);
323
324	// Encode call_data
325	let encoded_call = treasury_spend_call
326		.encode_call_data(&quantus_client.client().metadata())
327		.map_err(|e| {
328			crate::error::QuantusError::Generic(format!("Failed to encode call: {:?}", e))
329		})?;
330
331	log_print!("📝 Creating preimage...");
332
333	// Load wallet keypair
334	let keypair =
335		crate::wallet::load_keypair_from_wallet(from, password.clone(), password_file.clone())?;
336
337	// Calculate preimage hash using Poseidon (runtime uses PoseidonHasher)
338	let preimage_hash: sp_core::H256 =
339		<PoseidonHasher as sp_runtime::traits::Hash>::hash(&encoded_call);
340
341	log_print!("🔗 Preimage hash: {:?}", preimage_hash);
342
343	// Submit preimage
344	let preimage_call = quantus_subxt::api::tx().preimage().note_preimage(encoded_call.clone());
345	let preimage_tx_hash =
346		submit_transaction(quantus_client, &keypair, preimage_call, None).await?;
347	let _ = wait_for_tx_confirmation(quantus_client.client(), preimage_tx_hash).await?;
348
349	log_print!("✅ Preimage created");
350
351	// Determine the origin based on track
352	let origin_caller = match track.to_lowercase().as_str() {
353		"small" => quantus_subxt::api::runtime_types::quantus_runtime::OriginCaller::Origins(
354			quantus_subxt::api::runtime_types::quantus_runtime::governance::origins::pallet_custom_origins::Origin::SmallSpender,
355		),
356		"medium" => quantus_subxt::api::runtime_types::quantus_runtime::OriginCaller::Origins(
357			quantus_subxt::api::runtime_types::quantus_runtime::governance::origins::pallet_custom_origins::Origin::MediumSpender,
358		),
359		"big" => quantus_subxt::api::runtime_types::quantus_runtime::OriginCaller::Origins(
360			quantus_subxt::api::runtime_types::quantus_runtime::governance::origins::pallet_custom_origins::Origin::BigSpender,
361		),
362		"treasurer" => quantus_subxt::api::runtime_types::quantus_runtime::OriginCaller::Origins(
363			quantus_subxt::api::runtime_types::quantus_runtime::governance::origins::pallet_custom_origins::Origin::Treasurer,
364		),
365		_ => {
366			return Err(crate::error::QuantusError::Generic(format!(
367				"Invalid track: {}. Must be 'small', 'medium', 'big', or 'treasurer'",
368				track
369			)))
370		},
371	};
372
373	// Create the bounded proposal
374	let proposal =
375		quantus_subxt::api::runtime_types::frame_support::traits::preimages::Bounded::Lookup {
376			hash: preimage_hash,
377			len: encoded_call.len() as u32,
378		};
379
380	log_print!("📜 Submitting referendum...");
381
382	// Submit referendum with DispatchTime::After
383	let enactment =
384		quantus_subxt::api::runtime_types::frame_support::traits::schedule::DispatchTime::After(
385			1u32,
386		);
387	let submit_call =
388		quantus_subxt::api::tx().referenda().submit(origin_caller, proposal, enactment);
389	let submit_tx_hash = submit_transaction(quantus_client, &keypair, submit_call, None).await?;
390	let _ = wait_for_tx_confirmation(quantus_client.client(), submit_tx_hash).await?;
391
392	log_print!("✅ {} Treasury spend referendum submitted!", "SUCCESS".bright_green().bold());
393	log_print!("💡 Next steps:");
394	log_print!("   1. Place decision deposit: quantus referenda place-decision-deposit --index <INDEX> --from {}", from);
395	log_print!(
396		"   2. Vote on the referendum: quantus referenda vote --index <INDEX> --aye --from <VOTER>"
397	);
398	log_print!(
399		"   3. After approval, payout: quantus treasury payout --index <SPEND_INDEX> --from {}",
400		from
401	);
402
403	Ok(())
404}
405
406/// Payout an approved Treasury spend
407async fn payout_spend(
408	quantus_client: &crate::chain::client::QuantusClient,
409	index: u32,
410	from: &str,
411	password: Option<String>,
412	password_file: Option<String>,
413) -> crate::error::Result<()> {
414	log_print!("💸 Paying out Treasury Spend #{}", index);
415
416	// Load wallet keypair
417	let keypair = crate::wallet::load_keypair_from_wallet(from, password, password_file)?;
418
419	// Create payout call
420	let payout_call = quantus_subxt::api::tx().treasury_pallet().payout(index);
421
422	let tx_hash = submit_transaction(quantus_client, &keypair, payout_call, None).await?;
423	log_print!(
424		"✅ {} Payout transaction submitted! Hash: {:?}",
425		"SUCCESS".bright_green().bold(),
426		tx_hash
427	);
428
429	let _ = wait_for_tx_confirmation(quantus_client.client(), tx_hash).await?;
430	log_success!("🎉 {} Treasury spend paid out!", "FINISHED".bright_green().bold());
431	log_print!("💡 Use 'quantus treasury check-status --index {}' to cleanup", index);
432
433	Ok(())
434}
435
436/// Check and cleanup a Treasury spend status
437async fn check_spend_status(
438	quantus_client: &crate::chain::client::QuantusClient,
439	index: u32,
440	from: &str,
441	password: Option<String>,
442	password_file: Option<String>,
443) -> crate::error::Result<()> {
444	log_print!("🔍 Checking Treasury Spend #{} status", index);
445
446	// Load wallet keypair
447	let keypair = crate::wallet::load_keypair_from_wallet(from, password, password_file)?;
448
449	// Create check_status call
450	let check_call = quantus_subxt::api::tx().treasury_pallet().check_status(index);
451
452	let tx_hash = submit_transaction(quantus_client, &keypair, check_call, None).await?;
453	log_print!(
454		"✅ {} Check status transaction submitted! Hash: {:?}",
455		"SUCCESS".bright_green().bold(),
456		tx_hash
457	);
458
459	let _ = wait_for_tx_confirmation(quantus_client.client(), tx_hash).await?;
460	log_success!("🎉 {} Spend status checked and cleaned up!", "FINISHED".bright_green().bold());
461
462	Ok(())
463}
464
465/// List active Treasury spends
466async fn list_spends(
467	quantus_client: &crate::chain::client::QuantusClient,
468) -> crate::error::Result<()> {
469	log_print!("📋 Active Treasury Spends");
470	log_print!("");
471
472	let latest_block_hash = quantus_client.get_latest_block().await?;
473	let storage_at = quantus_client.client().storage().at(latest_block_hash);
474
475	// Iterate through spend storage indices (0 to 100 for example)
476	let mut count = 0;
477	for spend_index in 0..100 {
478		let spend_addr = quantus_subxt::api::storage().treasury_pallet().spends(spend_index);
479		if let Some(spend_status) = storage_at.fetch(&spend_addr).await? {
480			log_print!("💰 Spend #{}", spend_index.to_string().bright_yellow().bold());
481			log_print!("   Amount: {} (raw)", spend_status.amount.to_string().bright_green());
482			log_print!(
483				"   Beneficiary: {}",
484				format!("{:?}", spend_status.beneficiary).bright_cyan()
485			);
486			log_print!("   Valid From: Block #{}", spend_status.valid_from);
487			log_print!("   Expires At: Block #{}", spend_status.expire_at);
488			log_print!(
489				"   Status: {}",
490				match spend_status.status {
491					quantus_subxt::api::runtime_types::pallet_treasury::PaymentState::Pending =>
492						"Pending".bright_yellow(),
493					quantus_subxt::api::runtime_types::pallet_treasury::PaymentState::Attempted { .. } =>
494						"Attempted".bright_blue(),
495					quantus_subxt::api::runtime_types::pallet_treasury::PaymentState::Failed =>
496						"Failed".bright_red(),
497				}
498			);
499			log_print!("");
500			count += 1;
501		}
502	}
503
504	if count == 0 {
505		log_print!("📭 No active Treasury spends found");
506	} else {
507		log_print!("Total: {} active spend(s)", count.to_string().bright_green().bold());
508	}
509
510	Ok(())
511}
512
513/// Directly create a Treasury spend via sudo (testing/root only)
514async fn spend_sudo(
515	quantus_client: &crate::chain::client::QuantusClient,
516	beneficiary: &str,
517	amount: &str,
518	from: &str,
519	password: Option<String>,
520	password_file: Option<String>,
521) -> crate::error::Result<()> {
522	use sp_core::crypto::{AccountId32 as SpAccountId32, Ss58Codec};
523
524	log_print!("💰 Creating Treasury Spend via Sudo (Root)");
525	log_print!("   📍 Beneficiary: {}", beneficiary.bright_yellow());
526	log_print!("   💵 Amount: {}", amount.bright_green());
527	log_print!("   ⚠️  Using ROOT permissions (sudo)");
528
529	// Parse amount
530	let amount_value = crate::cli::send::parse_amount(quantus_client, amount).await?;
531
532	// Parse beneficiary address
533	let beneficiary_resolved = crate::cli::common::resolve_address(beneficiary)?;
534	let (beneficiary_sp, _) = SpAccountId32::from_ss58check_with_version(&beneficiary_resolved)
535		.map_err(|e| {
536			crate::error::QuantusError::Generic(format!(
537				"Invalid beneficiary address '{beneficiary}': {e:?}"
538			))
539		})?;
540	let bytes: [u8; 32] = *beneficiary_sp.as_ref();
541	let beneficiary_account = subxt::utils::AccountId32::from(bytes);
542	let beneficiary_multi = subxt::utils::MultiAddress::Id(beneficiary_account.clone());
543
544	// Create the treasury spend call
545	let spend_call = quantus_subxt::api::Call::TreasuryPallet(
546		quantus_subxt::api::treasury_pallet::Call::spend {
547			asset_kind: Box::new(()),
548			amount: amount_value,
549			beneficiary: Box::new(beneficiary_multi),
550			valid_from: None,
551		},
552	);
553
554	// Wrap with sudo
555	let sudo_call = quantus_subxt::api::tx().sudo().sudo(spend_call);
556
557	// Load wallet keypair
558	let keypair = crate::wallet::load_keypair_from_wallet(from, password, password_file)?;
559
560	// Submit transaction
561	log_print!("📡 Submitting sudo transaction...");
562	let tx_hash = submit_transaction(quantus_client, &keypair, sudo_call, None).await?;
563	log_print!(
564		"✅ {} Sudo transaction submitted! Hash: {:?}",
565		"SUCCESS".bright_green().bold(),
566		tx_hash
567	);
568
569	let _ = wait_for_tx_confirmation(quantus_client.client(), tx_hash).await?;
570	log_success!("🎉 {} Treasury spend created via sudo!", "FINISHED".bright_green().bold());
571	log_print!("💡 Next step: quantus treasury list-spends");
572	log_print!("💡 Then payout: quantus treasury payout --index <INDEX> --from {}", beneficiary);
573
574	Ok(())
575}