Skip to main content

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