quantus_cli/cli/
treasury.rs

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