quantus_cli/cli/
referenda_decode.rs

1//! Decoding utilities for referendum proposals
2
3use crate::error::QuantusError;
4use codec::Decode;
5use colored::Colorize;
6
7/// Decode preimage call data into human-readable format
8pub async fn decode_preimage(
9	quantus_client: &crate::chain::client::QuantusClient,
10	hash: &subxt::utils::H256,
11	len: u32,
12) -> crate::error::Result<String> {
13	// Fetch preimage from storage
14	let latest_block_hash = quantus_client.get_latest_block().await?;
15	let storage_at = quantus_client.client().storage().at(latest_block_hash);
16
17	let preimage_addr = crate::chain::quantus_subxt::api::storage()
18		.preimage()
19		.preimage_for((*hash, len));
20
21	let preimage_result = storage_at.fetch(&preimage_addr).await;
22
23	let content = match preimage_result {
24		Ok(Some(bounded_vec)) => bounded_vec.0,
25		Ok(None) =>
26			return Err(QuantusError::Generic(format!("Preimage not found for hash {:?}", hash))),
27		Err(e) => return Err(QuantusError::Generic(format!("Error fetching preimage: {:?}", e))),
28	};
29
30	// Decode using direct Decode trait (RuntimeCall implements it via DecodeAsType derive)
31	decode_runtime_call_direct(&content)
32}
33
34/// Decode RuntimeCall directly using Decode trait
35fn decode_runtime_call_direct(data: &[u8]) -> crate::error::Result<String> {
36	// First, let's try to understand the call structure by reading indices
37	if data.len() < 3 {
38		return Err(QuantusError::Generic("Call data too short".to_string()));
39	}
40
41	let pallet_index = data[0];
42	let inner_index = data[1];
43	let call_index = data[2];
44
45	match (pallet_index, inner_index, call_index) {
46		// System pallet (0, 0, X)
47		// Special case: if call_index looks like Compact (high value like 0xe8),
48		// it might be remark (call 0) where the call index byte is omitted
49		(0, 0, idx) if idx > 100 => {
50			// Likely remark (call 0) with Compact-encoded Vec starting at byte 2
51			decode_system_remark_no_index(&data[2..])
52		},
53		(0, 0, _) => decode_system_call(&data[2..]),
54
55		// TreasuryPallet (18, 5, X) where X is any spend variant (11, 15, 19, etc.)
56		// Different indices represent different value ranges/encodings
57		(18, 5, _) => decode_treasury_spend_call(&data[3..]),
58
59		// Unknown
60		_ => Ok(format!(
61			"   {}  {} {} {}\n   {}  {} bytes\n   {}:\n   {}",
62			"Call Indices:".dimmed(),
63			pallet_index,
64			inner_index,
65			call_index,
66			"Args:".dimmed(),
67			data.len() - 3,
68			"Raw Hex".dimmed(),
69			hex::encode(&data[3..]).bright_green()
70		)),
71	}
72}
73
74/// Decode System::remark when call index byte is omitted (call 0)
75fn decode_system_remark_no_index(args: &[u8]) -> crate::error::Result<String> {
76	// args starts directly with Compact-encoded Vec<u8>
77	let mut cursor = args;
78	let remark_bytes: Vec<u8> = Vec::decode(&mut cursor)
79		.map_err(|e| QuantusError::Generic(format!("Failed to decode remark: {:?}", e)))?;
80	let remark_str = String::from_utf8_lossy(&remark_bytes);
81
82	Ok(format!(
83		"   {}  {}\n   {}  {}\n   {}:\n     {} \"{}\"",
84		"Pallet:".dimmed(),
85		"System".bright_cyan(),
86		"Call:".dimmed(),
87		"remark".bright_yellow(),
88		"Parameters".dimmed(),
89		"message:".dimmed(),
90		remark_str.bright_green()
91	))
92}
93
94/// Decode System pallet calls
95fn decode_system_call(data_from_call: &[u8]) -> crate::error::Result<String> {
96	if data_from_call.is_empty() {
97		return Err(QuantusError::Generic("Empty system call data".to_string()));
98	}
99
100	let call_index = data_from_call[0];
101	let args = &data_from_call[1..];
102
103	match call_index {
104		0 => {
105			// remark - standard Vec<u8>
106			let mut cursor = args;
107			let remark_bytes: Vec<u8> = Vec::decode(&mut cursor)
108				.map_err(|e| QuantusError::Generic(format!("Failed to decode remark: {:?}", e)))?;
109			let remark_str = String::from_utf8_lossy(&remark_bytes);
110
111			Ok(format!(
112				"   {}  {}\n   {}  {}\n   {}:\n     {} \"{}\"",
113				"Pallet:".dimmed(),
114				"System".bright_cyan(),
115				"Call:".dimmed(),
116				"remark".bright_yellow(),
117				"Parameters".dimmed(),
118				"message:".dimmed(),
119				remark_str.bright_green()
120			))
121		},
122		1 => {
123			// remark_with_event - has different encoding, try decoding from byte 1
124			let remark_str = if args.len() > 1 {
125				String::from_utf8_lossy(&args[1..])
126			} else {
127				String::from_utf8_lossy(args)
128			};
129
130			Ok(format!(
131				"   {}  {}\n   {}  {}\n   {}:\n     {} \"{}\"",
132				"Pallet:".dimmed(),
133				"System".bright_cyan(),
134				"Call:".dimmed(),
135				"remark_with_event".bright_yellow(),
136				"Parameters".dimmed(),
137				"message:".dimmed(),
138				remark_str.bright_green()
139			))
140		},
141		7 => {
142			// set_code
143			Ok(format!(
144				"   {}  {}\n   {}  {} {}\n   {}  {}",
145				"Pallet:".dimmed(),
146				"System".bright_cyan(),
147				"Call:".dimmed(),
148				"set_code".bright_yellow(),
149				"(Runtime Upgrade)".dimmed(),
150				"Parameters:".dimmed(),
151				"<WASM binary>".bright_green()
152			))
153		},
154		_ => Ok(format!(
155			"   {}  {}\n   {}  {} (index {})",
156			"Pallet:".dimmed(),
157			"System".bright_cyan(),
158			"Call:".dimmed(),
159			"unknown".yellow(),
160			call_index
161		)),
162	}
163}
164
165/// Decode TreasuryPallet::spend call arguments
166/// The amount is stored as variable-length u128 in little-endian
167fn decode_treasury_spend_call(args: &[u8]) -> crate::error::Result<String> {
168	use sp_core::crypto::Ss58Codec;
169
170	crate::log_verbose!("Decoding treasury spend, args length: {} bytes", args.len());
171	crate::log_verbose!("Args hex: {}", hex::encode(args));
172
173	if args.len() < 34 {
174		return Err(QuantusError::Generic(format!(
175			"Args too short for treasury spend: {} bytes (expected 40-42)",
176			args.len()
177		)));
178	}
179
180	// Structure (discovered through empirical analysis):
181	// - asset_kind: Box<()> = 0 bytes (unit type has no encoding)
182	// - amount: u128 = variable bytes (7-8 bytes typically) as little-endian
183	// - beneficiary: Box<MultiAddress::Id(AccountId32)> = 32 bytes (no variant byte!)
184	// - valid_from: Option<u32> = 1 byte (0x00 for None)
185
186	// The amount length varies based on the value:
187	// - Small values (< 256TB): 7 bytes
188	// - Larger values: 8+ bytes
189	// Total length is typically 40 bytes (7+32+1) or 42 bytes (8+32+1) or similar
190
191	// Calculate amount bytes length: total - 32 (beneficiary) - 1 (valid_from)
192	let amount_bytes_len = args.len() - 32 - 1;
193	if !(1..=16).contains(&amount_bytes_len) {
194		return Err(QuantusError::Generic(format!(
195			"Invalid amount bytes length: {}",
196			amount_bytes_len
197		)));
198	}
199
200	// Decode amount: first N bytes as little-endian u128
201	let mut amount_bytes_extended = [0u8; 16];
202	amount_bytes_extended[..amount_bytes_len].copy_from_slice(&args[..amount_bytes_len]);
203	let amount = u128::from_le_bytes(amount_bytes_extended);
204
205	// Decode beneficiary: starts after amount bytes, 32 bytes
206	let beneficiary_start = amount_bytes_len;
207	let account_bytes: [u8; 32] = args[beneficiary_start..beneficiary_start + 32]
208		.try_into()
209		.map_err(|_| QuantusError::Generic("Failed to extract beneficiary bytes".to_string()))?;
210	let sp_account = sp_core::crypto::AccountId32::from(account_bytes);
211	let ss58 = sp_account.to_ss58check_with_version(sp_core::crypto::Ss58AddressFormat::custom(42));
212	let beneficiary_str = format!("{} ({}...{})", ss58, &ss58[..8], &ss58[ss58.len() - 6..]);
213
214	// Decode valid_from: last byte
215	let valid_from_byte = args[args.len() - 1];
216	let valid_from_str = if valid_from_byte == 0 {
217		"None (immediate)".to_string()
218	} else {
219		format!("Some (byte: 0x{:02x})", valid_from_byte)
220	};
221
222	// Format amount in QUAN (1 QUAN = 10^12)
223	let quan = amount as f64 / 1_000_000_000_000.0;
224
225	Ok(format!(
226		"   {}  {}\n   {}  {}\n   {}:\n     {} {} {} ({} raw)\n     {} {}\n     {} {}\n\n   {}  {}",
227		"Pallet:".dimmed(),
228		"TreasuryPallet".bright_cyan(),
229		"Call:".dimmed(),
230		"spend".bright_yellow(),
231		"Parameters".dimmed(),
232		"amount:".dimmed(),
233		quan.to_string().bright_green().bold(),
234		"QUAN".bright_green(),
235		amount,
236		"beneficiary:".dimmed(),
237		beneficiary_str.bright_green(),
238		"valid_from:".dimmed(),
239		valid_from_str.bright_green(),
240		"💡 Info:".cyan(),
241		"Vote YES if you approve this Treasury spend, NO to reject.".cyan()
242	))
243}