Skip to main content

pop_chains/call/metadata/
mod.rs

1// SPDX-License-Identifier: GPL-3.0
2
3use crate::errors::Error;
4use params::Param;
5use scale_value::{Composite, ValueDef, stringify::custom_parsers};
6use std::fmt::{Display, Formatter, Write};
7use subxt::{
8	Metadata, OnlineClient, SubstrateConfig,
9	dynamic::Value,
10	ext::futures::TryStreamExt,
11	metadata::types::{PalletMetadata, StorageEntryType},
12	utils::to_hex,
13};
14
15pub mod action;
16pub mod params;
17
18pub type RawValue = Value<u32>;
19
20fn format_single_tuples<T, W: Write>(value: &Value<T>, mut writer: W) -> Option<core::fmt::Result> {
21	if let ValueDef::Composite(Composite::Unnamed(vals)) = &value.value &&
22		vals.len() == 1
23	{
24		let val = &vals[0];
25		return match raw_value_to_string(val, "") {
26			Ok(r) => match writer.write_str(&r) {
27				Ok(_) => Some(Ok(())),
28				Err(_) => None,
29			},
30			Err(_) => None,
31		};
32	}
33	None
34}
35
36// Formats to hexadecimal in lowercase
37fn format_hex<T, W: Write>(value: &Value<T>, mut writer: W) -> Option<core::fmt::Result> {
38	let mut result = String::new();
39	match scale_value::stringify::custom_formatters::format_hex(value, &mut result) {
40		Some(res) => match res {
41			Ok(_) => match writer.write_str(&result.to_lowercase()) {
42				Ok(_) => Some(Ok(())),
43				Err(_) => None,
44			},
45			Err(_) => None,
46		},
47		None => None,
48	}
49}
50
51/// Converts a raw SCALE value to a human-readable string representation.
52///
53/// This function takes a raw SCALE value and formats it into a string using custom formatters:
54/// - Formats byte sequences as hex strings.
55/// - Unwraps single-element tuples.
56/// - Uses pretty printing for better readability.
57///
58/// # Arguments
59/// * `value` - The raw SCALE value to convert to string.
60///
61/// # Returns
62/// * `Ok(String)` - The formatted string representation of the value.
63/// * `Err(_)` - If the value cannot be converted to string.
64pub fn raw_value_to_string<T>(value: &Value<T>, indent: &str) -> anyhow::Result<String> {
65	let mut result = String::new();
66	scale_value::stringify::to_writer_custom()
67		.compact()
68		.pretty()
69		.add_custom_formatter(|v, w| format_hex(v, w))
70		.add_custom_formatter(|v, w| format_single_tuples(v, w))
71		.write(value, &mut result)?;
72
73	// Add indentation to each line
74	let indented = result
75		.lines()
76		.map(|line| format!("{indent}{line}"))
77		.collect::<Vec<_>>()
78		.join("\n");
79	Ok(indented)
80}
81
82/// Renders storage key-value pairs into a human-readable string format.
83///
84/// Takes a slice of tuples containing storage keys and their associated values and formats them
85/// into a readable string representation. Each key-value pair is rendered on separate lines within
86/// square brackets.
87///
88/// # Arguments
89/// * `key_value_pairs` - A slice of tuples where each tuple contains:
90///   - A vector of storage keys.
91///   - The associated storage value.
92///
93/// # Returns
94/// * `Ok(String)` - A formatted string containing the rendered key-value pairs.
95/// * `Err(_)` - If there's an error converting the values to strings.
96pub fn render_storage_key_values(
97	key_value_pairs: &[(Vec<Value>, RawValue)],
98) -> anyhow::Result<String> {
99	let mut result = String::new();
100	let indent = "  ";
101	for (keys, value) in key_value_pairs {
102		result.push_str("[\n");
103		if !keys.is_empty() {
104			for key in keys {
105				result.push_str(&raw_value_to_string(key, indent)?);
106				result.push_str(",\n");
107			}
108		}
109		result.push_str(&raw_value_to_string(value, indent)?);
110		result.push_str("\n]\n");
111	}
112	Ok(result)
113}
114
115/// Represents different types of callable items that can be interacted with in the runtime.
116#[derive(Clone, Debug, Eq, PartialEq)]
117pub enum CallItem {
118	/// A dispatchable function (extrinsic) that can be called.
119	Function(Function),
120	/// A constant value defined in the runtime.
121	Constant(Constant),
122	/// A storage item that can be queried.
123	Storage(Storage),
124}
125
126impl Default for CallItem {
127	fn default() -> Self {
128		Self::Function(Function::default())
129	}
130}
131
132impl Display for CallItem {
133	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
134		match self {
135			CallItem::Function(function) => function.fmt(f),
136			CallItem::Constant(constant) => constant.fmt(f),
137			CallItem::Storage(storage) => storage.fmt(f),
138		}
139	}
140}
141
142impl CallItem {
143	/// Returns a reference to the [`Function`] if this is a function call item.
144	pub fn as_function(&self) -> Option<&Function> {
145		match self {
146			CallItem::Function(f) => Some(f),
147			_ => None,
148		}
149	}
150
151	/// Returns a reference to the [`Constant`] if this is a constant call item.
152	pub fn as_constant(&self) -> Option<&Constant> {
153		match self {
154			CallItem::Constant(c) => Some(c),
155			_ => None,
156		}
157	}
158
159	/// Returns a reference to the [`Storage`] if this is a storage call item.
160	pub fn as_storage(&self) -> Option<&Storage> {
161		match self {
162			CallItem::Storage(s) => Some(s),
163			_ => None,
164		}
165	}
166
167	/// Returns the name of this call item.
168	pub fn name(&self) -> &str {
169		match self {
170			CallItem::Function(function) => &function.name,
171			CallItem::Constant(constant) => &constant.name,
172			CallItem::Storage(storage) => &storage.name,
173		}
174	}
175	/// Returns a descriptive hint string indicating the type of this call item.
176	pub fn hint(&self) -> &str {
177		match self {
178			CallItem::Function(_) => "📝 [EXTRINSIC]",
179			CallItem::Constant(_) => "[CONSTANT]",
180			CallItem::Storage(_) => "[STORAGE]",
181		}
182	}
183
184	/// Returns the documentation string associated with this call item.
185	pub fn docs(&self) -> &str {
186		match self {
187			CallItem::Function(function) => &function.docs,
188			CallItem::Constant(constant) => &constant.docs,
189			CallItem::Storage(storage) => &storage.docs,
190		}
191	}
192
193	/// Returns the name of the pallet containing this call item.
194	pub fn pallet(&self) -> &str {
195		match self {
196			CallItem::Function(function) => &function.pallet,
197			CallItem::Constant(constant) => &constant.pallet,
198			CallItem::Storage(storage) => &storage.pallet,
199		}
200	}
201}
202
203/// Represents a pallet in the blockchain, including its dispatchable functions.
204#[derive(Clone, Debug, Default, Eq, PartialEq)]
205pub struct Pallet {
206	/// The name of the pallet.
207	pub name: String,
208	/// The index of the pallet within the runtime.
209	pub index: u8,
210	/// The documentation of the pallet.
211	pub docs: String,
212	/// The dispatchable functions of the pallet.
213	pub functions: Vec<Function>,
214	/// The constants of the pallet.
215	pub constants: Vec<Constant>,
216	/// The storage items of the pallet.
217	pub state: Vec<Storage>,
218}
219
220impl Display for Pallet {
221	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
222		write!(f, "{}", self.name)
223	}
224}
225
226impl Pallet {
227	/// Returns a vector containing all callable items (functions, constants, and storage) defined
228	/// in this pallet.
229	///
230	/// This method collects and returns all available callable items from the pallet:
231	/// - Dispatchable functions (extrinsics)
232	/// - Constants
233	/// - Storage items
234	///
235	/// # Returns
236	/// A `Vec<CallItem>` containing all callable items from this pallet.
237	pub fn get_all_callables(&self) -> Vec<CallItem> {
238		let mut callables = Vec::new();
239		for function in &self.functions {
240			callables.push(CallItem::Function(function.clone()));
241		}
242		for constant in &self.constants {
243			callables.push(CallItem::Constant(constant.clone()));
244		}
245		for storage in &self.state {
246			callables.push(CallItem::Storage(storage.clone()));
247		}
248		callables
249	}
250}
251
252/// Represents a dispatchable function.
253#[derive(Clone, Debug, Default, Eq, PartialEq)]
254pub struct Function {
255	/// The pallet containing the dispatchable function.
256	pub pallet: String,
257	/// The name of the function.
258	pub name: String,
259	/// The index of the function within the pallet.
260	pub index: u8,
261	/// The documentation of the function.
262	pub docs: String,
263	/// The parameters of the function.
264	pub params: Vec<Param>,
265	/// Whether this function is supported (no recursive or unsupported types like `RuntimeCall`).
266	pub is_supported: bool,
267}
268
269impl Display for Function {
270	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
271		write!(f, "{}", self.name)
272	}
273}
274
275/// Represents a runtime constant.
276#[derive(Clone, Debug, Eq, PartialEq)]
277pub struct Constant {
278	/// The pallet containing the dispatchable function.
279	pub pallet: String,
280	/// The name of the constant.
281	pub name: String,
282	/// The documentation of the constant.
283	pub docs: String,
284	/// The value of the constant.
285	pub value: RawValue,
286}
287
288impl Display for Constant {
289	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
290		write!(f, "{}", self.name)
291	}
292}
293
294/// Represents a storage item.
295#[derive(Clone, Debug, Eq, PartialEq)]
296pub struct Storage {
297	/// The pallet containing the storage item.
298	pub pallet: String,
299	/// The name of the storage item.
300	pub name: String,
301	/// The documentation of the storage item.
302	pub docs: String,
303	/// The type ID for decoding the storage value.
304	pub type_id: u32,
305	/// Optional type ID for map-type storage items. Usually a tuple.
306	pub key_id: Option<u32>,
307	/// Whether to query all values for a storage item, optionally filtered by provided keys.
308	pub query_all: bool,
309}
310
311impl Display for Storage {
312	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
313		write!(f, "{}", self.name)
314	}
315}
316
317impl Storage {
318	/// Queries all values for a storage item, optionally filtered by provided keys.
319	///
320	/// This method allows retrieving multiple values from storage by iterating through all entries
321	/// that match the provided keys. For map-type storage items, keys can be used to filter
322	/// the results.
323	///
324	/// # Arguments
325	/// * `client` - The client to interact with the chain.
326	/// * `keys` - Optional storage keys for map-type storage items to filter results.
327	pub async fn query_all(
328		&self,
329		client: &OnlineClient<SubstrateConfig>,
330		keys: Vec<Value>,
331	) -> Result<Vec<(Vec<Value>, RawValue)>, Error> {
332		let mut elements = Vec::new();
333		let metadata = client.metadata();
334		let types = metadata.types();
335		let storage_address = subxt::dynamic::storage(&self.pallet, &self.name, keys);
336		let mut stream = client
337			.storage()
338			.at_latest()
339			.await
340			.map_err(|e| Error::MetadataParsingError(format!("Failed to get storage: {}", e)))?
341			.iter(storage_address)
342			.await
343			.map_err(|e| {
344				Error::MetadataParsingError(format!("Failed to fetch storage value: {}", e))
345			})?;
346
347		while let Some(storage_data) = stream.try_next().await.map_err(|e| {
348			Error::MetadataParsingError(format!("Failed to fetch storage value: {}", e))
349		})? {
350			let keys = storage_data.keys;
351			let mut bytes = storage_data.value.encoded();
352			let decoded_value = scale_value::scale::decode_as_type(&mut bytes, self.type_id, types)
353				.map_err(|e| {
354					Error::MetadataParsingError(format!("Failed to decode storage value: {}", e))
355				})?;
356			elements.push((keys, decoded_value));
357		}
358		Ok(elements)
359	}
360	/// Query the storage value from the chain and return it as a formatted string.
361	///
362	/// # Arguments
363	/// * `client` - The client to interact with the chain.
364	/// * `keys` - Optional storage keys for map-type storage items.
365	pub async fn query(
366		&self,
367		client: &OnlineClient<SubstrateConfig>,
368		keys: Vec<Value>,
369	) -> Result<Option<RawValue>, Error> {
370		let metadata = client.metadata();
371		let types = metadata.types();
372		let storage_address = subxt::dynamic::storage(&self.pallet, &self.name, keys);
373		let storage_data = client
374			.storage()
375			.at_latest()
376			.await
377			.map_err(|e| Error::MetadataParsingError(format!("Failed to get storage: {}", e)))?
378			.fetch(&storage_address)
379			.await
380			.map_err(|e| {
381				Error::MetadataParsingError(format!("Failed to fetch storage value: {}", e))
382			})?;
383
384		// Decode the value if it exists
385		match storage_data {
386			Some(value) => {
387				// Try to decode using the type information
388				let mut bytes = value.encoded();
389				let decoded_value = scale_value::scale::decode_as_type(
390					&mut bytes,
391					self.type_id,
392					types,
393				)
394				.map_err(|e| {
395					Error::MetadataParsingError(format!("Failed to decode storage value: {}", e))
396				})?;
397
398				Ok(Some(decoded_value))
399			},
400			None => Ok(None),
401		}
402	}
403}
404
405fn extract_chain_state_from_pallet_metadata(
406	pallet: &PalletMetadata,
407) -> anyhow::Result<Vec<Storage>> {
408	pallet
409		.storage()
410		.map(|storage_metadata| {
411			storage_metadata
412				.entries()
413				.iter()
414				.map(|entry| {
415					Ok(Storage {
416						pallet: pallet.name().to_string(),
417						name: entry.name().to_string(),
418						docs: entry
419							.docs()
420							.iter()
421							.filter(|l| !l.is_empty())
422							.cloned()
423							.collect::<Vec<_>>()
424							.join("")
425							.trim()
426							.to_string(),
427						type_id: entry.entry_type().value_ty(),
428						key_id: match entry.entry_type() {
429							StorageEntryType::Plain(_) => None,
430							StorageEntryType::Map { key_ty, .. } => Some(*key_ty),
431						},
432						query_all: false,
433					})
434				})
435				.collect::<Result<Vec<Storage>, Error>>()
436		})
437		.unwrap_or_else(|| Ok(vec![]))
438		.map_err(|e| anyhow::Error::msg(e.to_string()))
439}
440
441fn extract_constants_from_pallet_metadata(
442	pallet: &PalletMetadata,
443	metadata: &Metadata,
444) -> anyhow::Result<Vec<Constant>> {
445	let types = metadata.types();
446	pallet
447		.constants()
448		.map(|constant| {
449			// Decode the SCALE-encoded constant value using its type information
450			let mut value_bytes = constant.value();
451			let decoded_value =
452				scale_value::scale::decode_as_type(&mut value_bytes, constant.ty(), types)
453					.map_err(|e| {
454						Error::MetadataParsingError(format!(
455							"Failed to decode constant {}: {}",
456							constant.name(),
457							e
458						))
459					})?;
460
461			Ok(Constant {
462				pallet: pallet.name().to_string(),
463				name: constant.name().to_string(),
464				docs: constant
465					.docs()
466					.iter()
467					.filter(|l| !l.is_empty())
468					.cloned()
469					.collect::<Vec<_>>()
470					.join("")
471					.trim()
472					.to_string(),
473				value: decoded_value,
474			})
475		})
476		.collect::<Result<Vec<Constant>, Error>>()
477		.map_err(|e| anyhow::Error::msg(e.to_string()))
478}
479
480fn extract_functions_from_pallet_metadata(
481	pallet: &PalletMetadata,
482	metadata: &Metadata,
483) -> anyhow::Result<Vec<Function>> {
484	pallet
485		.call_variants()
486		.map(|variants| {
487			variants
488				.iter()
489				.map(|variant| {
490					let mut is_supported = true;
491
492					// Parse parameters for the dispatchable function.
493					let params = {
494						let mut parsed_params = Vec::new();
495						for field in &variant.fields {
496							match params::field_to_param(metadata, field) {
497								Ok(param) => parsed_params.push(param),
498								Err(_) => {
499									// If an error occurs while parsing the values, mark the
500									// dispatchable function as unsupported rather than
501									// error.
502									is_supported = false;
503									break;
504								},
505							}
506						}
507						parsed_params
508					};
509
510					Ok(Function {
511						pallet: pallet.name().to_string(),
512						name: variant.name.clone(),
513						index: variant.index,
514						docs: if is_supported {
515							// Filter out blank lines and then flatten into a single value.
516							variant
517								.docs
518								.iter()
519								.filter(|l| !l.is_empty())
520								.cloned()
521								.collect::<Vec<_>>()
522								.join(" ")
523								.trim()
524								.to_string()
525						} else {
526							// To display the message in the UI
527							"Function Not Supported".to_string()
528						},
529						params,
530						is_supported,
531					})
532				})
533				.collect::<Result<Vec<Function>, Error>>()
534		})
535		.unwrap_or_else(|| Ok(vec![]))
536		.map_err(|e| anyhow::Error::msg(e.to_string()))
537}
538
539/// Parses the chain metadata to extract information about pallets and their dispatchable functions.
540///
541/// # Arguments
542/// * `client`: The client to interact with the chain.
543///
544/// NOTE: pallets are ordered by their index within the runtime by default.
545pub fn parse_chain_metadata(client: &OnlineClient<SubstrateConfig>) -> Result<Vec<Pallet>, Error> {
546	let metadata: Metadata = client.metadata();
547
548	let pallets = metadata
549		.pallets()
550		.map(|pallet| {
551			Ok(Pallet {
552				name: pallet.name().to_string(),
553				index: pallet.index(),
554				docs: pallet.docs().join("").trim().to_string(),
555				functions: extract_functions_from_pallet_metadata(&pallet, &metadata)?,
556				constants: extract_constants_from_pallet_metadata(&pallet, &metadata)?,
557				state: extract_chain_state_from_pallet_metadata(&pallet)?,
558			})
559		})
560		.collect::<Result<Vec<Pallet>, Error>>()?;
561
562	Ok(pallets)
563}
564
565/// Finds a specific pallet by name and retrieves its details from metadata.
566///
567/// # Arguments
568/// * `pallets`: List of pallets available within the chain's runtime.
569/// * `pallet_name`: The name of the pallet to find.
570pub fn find_pallet_by_name<'a>(
571	pallets: &'a [Pallet],
572	pallet_name: &str,
573) -> Result<&'a Pallet, Error> {
574	if let Some(pallet) = pallets.iter().find(|p| p.name == pallet_name) {
575		Ok(pallet)
576	} else {
577		Err(Error::PalletNotFound(pallet_name.to_string()))
578	}
579}
580
581/// Finds a specific dispatchable function by name and retrieves its details from metadata.
582///
583/// # Arguments
584/// * `pallets`: List of pallets available within the chain's runtime.
585/// * `pallet_name`: The name of the pallet.
586/// * `function_name`: Name of the dispatchable function to locate.
587pub fn find_callable_by_name(
588	pallets: &[Pallet],
589	pallet_name: &str,
590	function_name: &str,
591) -> Result<CallItem, Error> {
592	let pallet = find_pallet_by_name(pallets, pallet_name)?;
593	if let Some(function) = pallet.functions.iter().find(|&e| e.name == function_name) {
594		return Ok(CallItem::Function(function.clone()));
595	}
596	if let Some(constant) = pallet.constants.iter().find(|&e| e.name == function_name) {
597		return Ok(CallItem::Constant(constant.clone()));
598	}
599	if let Some(storage) = pallet.state.iter().find(|&e| e.name == function_name) {
600		return Ok(CallItem::Storage(storage.clone()));
601	}
602	Err(Error::FunctionNotFound(format!(
603		"Could not find a function, constant or storage with the name \"{function_name}\""
604	)))
605}
606
607/// Parses and processes raw string parameter values for a dispatchable function, mapping them to
608/// `Value` types.
609///
610/// # Arguments
611/// * `params`: The metadata definition for each parameter of the corresponding dispatchable
612///   function.
613/// * `raw_params`: A vector of raw string arguments for the dispatchable function.
614pub fn parse_dispatchable_arguments(
615	params: &[Param],
616	raw_params: Vec<String>,
617) -> Result<Vec<Value>, Error> {
618	params
619		.iter()
620		.zip(raw_params)
621		.map(|(param, raw_param)| {
622			let processed_param = if param.is_sequence && !raw_param.starts_with("0x") {
623				if param.type_name == "[u8]" {
624					// Convert byte sequence parameters to hex
625					to_hex(&raw_param)
626				} else {
627					// For other sequences (e.g., Vec<AccountId32>), convert bracket syntax
628					// [a, b, c] to parentheses (a, b, c) since scale_value uses parentheses
629					// for unnamed composites. This allows SS58 parsing inside arrays.
630					convert_brackets_to_parens(&raw_param)
631				}
632			} else {
633				raw_param
634			};
635			scale_value::stringify::from_str_custom()
636				.add_custom_parser(custom_parsers::parse_hex)
637				.add_custom_parser(custom_parsers::parse_ss58)
638				.parse(&processed_param)
639				.0
640				.map_err(|_| Error::ParamProcessingError)
641		})
642		.collect()
643}
644
645/// Converts bracket array syntax `[a, b, c]` to parentheses `(a, b, c)` for scale_value parsing.
646/// Only converts outermost brackets when the string starts with `[` and ends with `]`.
647fn convert_brackets_to_parens(input: &str) -> String {
648	let trimmed = input.trim();
649	if trimmed.starts_with('[') && trimmed.ends_with(']') {
650		format!("({})", &trimmed[1..trimmed.len() - 1])
651	} else {
652		input.to_string()
653	}
654}
655
656#[cfg(test)]
657mod tests {
658	use super::*;
659	use anyhow::Result;
660	use sp_core::bytes::from_hex;
661	use subxt::ext::scale_bits;
662
663	#[test]
664	fn parse_dispatchable_arguments_works() -> Result<()> {
665		// Values for testing from: https://docs.rs/scale-value/0.18.0/scale_value/stringify/fn.from_str.html
666		// and https://docs.rs/scale-value/0.18.0/scale_value/stringify/fn.from_str_custom.html
667		let args = [
668			"1".to_string(),
669			"-1".to_string(),
670			"true".to_string(),
671			"'a'".to_string(),
672			"\"hi\"".to_string(),
673			"{ a: true, b: \"hello\" }".to_string(),
674			"MyVariant { a: true, b: \"hello\" }".to_string(),
675			"<0101>".to_string(),
676			"(1,2,0x030405)".to_string(),
677			r#"{
678				name: "Alice",
679				address: 5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty
680			}"#
681			.to_string(),
682		]
683		.to_vec();
684		let addr: Vec<_> =
685			from_hex("8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48")?
686				.into_iter()
687				.map(|b| Value::u128(b as u128))
688				.collect();
689		// Define mock dispatchable function parameters for testing.
690		let params = vec![
691			Param { type_name: "u128".to_string(), ..Default::default() },
692			Param { type_name: "i128".to_string(), ..Default::default() },
693			Param { type_name: "bool".to_string(), ..Default::default() },
694			Param { type_name: "char".to_string(), ..Default::default() },
695			Param { type_name: "string".to_string(), ..Default::default() },
696			Param { type_name: "composite".to_string(), ..Default::default() },
697			Param { type_name: "variant".to_string(), is_variant: true, ..Default::default() },
698			Param { type_name: "bit_sequence".to_string(), ..Default::default() },
699			Param { type_name: "tuple".to_string(), is_tuple: true, ..Default::default() },
700			Param { type_name: "composite".to_string(), ..Default::default() },
701		];
702		assert_eq!(
703			parse_dispatchable_arguments(&params, args)?,
704			[
705				Value::u128(1),
706				Value::i128(-1),
707				Value::bool(true),
708				Value::char('a'),
709				Value::string("hi"),
710				Value::named_composite(vec![
711					("a", Value::bool(true)),
712					("b", Value::string("hello"))
713				]),
714				Value::named_variant(
715					"MyVariant",
716					vec![("a", Value::bool(true)), ("b", Value::string("hello"))]
717				),
718				Value::bit_sequence(scale_bits::Bits::from_iter([false, true, false, true])),
719				Value::unnamed_composite(vec![
720					Value::u128(1),
721					Value::u128(2),
722					Value::unnamed_composite(vec![Value::u128(3), Value::u128(4), Value::u128(5),])
723				]),
724				Value::named_composite(vec![
725					("name", Value::string("Alice")),
726					("address", Value::unnamed_composite(addr))
727				])
728			]
729		);
730		Ok(())
731	}
732
733	#[test]
734	fn parse_vec_account_id() -> Result<()> {
735		// Test case from issue #906: Vec<AccountId32> should parse SS58 addresses
736		let params = vec![Param {
737			name: "who".into(),
738			type_name: "[AccountId32 ([u8;32])]".into(),
739			is_sequence: true,
740			..Default::default()
741		}];
742		let args = vec!["[5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty]".into()];
743		let result = parse_dispatchable_arguments(&params, args);
744		assert!(result.is_ok(), "Failed to parse: {:?}", result);
745
746		// Verify the parsed value is a composite containing the decoded SS58 address
747		let values = result?;
748		assert_eq!(values.len(), 1);
749
750		// The expected AccountId bytes for 5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty
751		let addr: Vec<_> =
752			from_hex("8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48")?
753				.into_iter()
754				.map(|b| Value::u128(b as u128))
755				.collect();
756
757		assert_eq!(values[0], Value::unnamed_composite(vec![Value::unnamed_composite(addr)]));
758		Ok(())
759	}
760
761	#[test]
762	fn parse_vec_multiple_account_ids() -> Result<()> {
763		// Test multiple AccountIds in a Vec
764		let params = vec![Param {
765			name: "who".into(),
766			type_name: "[AccountId32 ([u8;32])]".into(),
767			is_sequence: true,
768			..Default::default()
769		}];
770		let args = vec![
771			"[5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty, 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY]".into(),
772		];
773		let result = parse_dispatchable_arguments(&params, args);
774		assert!(result.is_ok());
775
776		let values = result?;
777		assert_eq!(values.len(), 1);
778
779		// Both addresses should be parsed
780		if let ValueDef::Composite(composite) = &values[0].value {
781			match composite {
782				Composite::Unnamed(items) => assert_eq!(items.len(), 2),
783				_ => panic!("Expected unnamed composite"),
784			}
785		} else {
786			panic!("Expected composite value");
787		}
788		Ok(())
789	}
790
791	#[test]
792	fn parse_byte_sequence_still_works() -> Result<()> {
793		// Ensure [u8] sequences still work (regression test)
794		let params = vec![Param {
795			name: "remark".into(),
796			type_name: "[u8]".into(),
797			is_sequence: true,
798			..Default::default()
799		}];
800		let args = vec!["hello".into()];
801		let result = parse_dispatchable_arguments(&params, args);
802		assert!(result.is_ok());
803		Ok(())
804	}
805
806	#[test]
807	fn convert_brackets_to_parens_works() {
808		// Standard array conversion
809		assert_eq!(convert_brackets_to_parens("[a, b, c]"), "(a, b, c)");
810		// Single element
811		assert_eq!(convert_brackets_to_parens("[x]"), "(x)");
812		// With whitespace
813		assert_eq!(convert_brackets_to_parens("  [a, b]  "), "(a, b)");
814		// Empty array
815		assert_eq!(convert_brackets_to_parens("[]"), "()");
816		// Non-array input unchanged
817		assert_eq!(convert_brackets_to_parens("hello"), "hello");
818		assert_eq!(convert_brackets_to_parens("(a, b)"), "(a, b)");
819		// Nested brackets preserved in content
820		assert_eq!(convert_brackets_to_parens("[[1, 2], [3, 4]]"), "([1, 2], [3, 4])");
821	}
822
823	#[test]
824	fn constant_display_works() {
825		let value = Value::u128(250).map_context(|_| 0u32);
826		let constant = Constant {
827			pallet: "System".to_string(),
828			name: "BlockHashCount".to_string(),
829			docs: "Maximum number of block number to block hash mappings to keep.".to_string(),
830			value,
831		};
832		assert_eq!(format!("{constant}"), "BlockHashCount");
833	}
834
835	#[test]
836	fn constant_struct_fields_work() {
837		let value = Value::u128(100).map_context(|_| 0u32);
838		let constant = Constant {
839			pallet: "Balances".to_string(),
840			name: "ExistentialDeposit".to_string(),
841			docs: "The minimum amount required to keep an account open.".to_string(),
842			value: value.clone(),
843		};
844		assert_eq!(constant.pallet, "Balances");
845		assert_eq!(constant.name, "ExistentialDeposit");
846		assert_eq!(constant.docs, "The minimum amount required to keep an account open.");
847		assert_eq!(constant.value, value);
848	}
849
850	#[test]
851	fn storage_display_works() {
852		let storage = Storage {
853			pallet: "System".to_string(),
854			name: "Account".to_string(),
855			docs: "The full account information for a particular account ID.".to_string(),
856			type_id: 42,
857			key_id: None,
858			query_all: false,
859		};
860		assert_eq!(format!("{storage}"), "Account");
861	}
862
863	#[test]
864	fn pallet_with_constants_and_storage() {
865		// Create a test value using map_context to convert Value<()> to Value<u32>
866		let value = Value::u128(250).map_context(|_| 0u32);
867		let pallet = Pallet {
868			name: "System".to_string(),
869			index: 0,
870			docs: "System pallet".to_string(),
871			functions: vec![],
872			constants: vec![Constant {
873				pallet: "System".to_string(),
874				name: "BlockHashCount".to_string(),
875				docs: "Maximum number of block number to block hash mappings to keep.".to_string(),
876				value,
877			}],
878			state: vec![Storage {
879				pallet: "System".to_string(),
880				name: "Account".to_string(),
881				docs: "The full account information for a particular account ID.".to_string(),
882				type_id: 42,
883				key_id: None,
884				query_all: false,
885			}],
886		};
887		assert_eq!(pallet.constants.len(), 1);
888		assert_eq!(pallet.state.len(), 1);
889		assert_eq!(pallet.constants[0].name, "BlockHashCount");
890		assert_eq!(pallet.state[0].name, "Account");
891	}
892
893	#[test]
894	fn storage_struct_with_key_id_works() {
895		// Test storage without key_id (plain storage)
896		let plain_storage = Storage {
897			pallet: "Timestamp".to_string(),
898			name: "Now".to_string(),
899			docs: "Current time for the current block.".to_string(),
900			type_id: 10,
901			key_id: None,
902			query_all: false,
903		};
904		assert_eq!(plain_storage.pallet, "Timestamp");
905		assert_eq!(plain_storage.name, "Now");
906		assert!(plain_storage.key_id.is_none());
907
908		// Test storage with key_id (storage map)
909		let map_storage = Storage {
910			pallet: "System".to_string(),
911			name: "Account".to_string(),
912			docs: "The full account information for a particular account ID.".to_string(),
913			type_id: 42,
914			key_id: Some(100),
915			query_all: false,
916		};
917		assert_eq!(map_storage.pallet, "System");
918		assert_eq!(map_storage.name, "Account");
919		assert_eq!(map_storage.key_id, Some(100));
920	}
921
922	#[test]
923	fn raw_value_to_string_works() -> Result<()> {
924		// Test simple integer value
925		let value = Value::u128(250).map_context(|_| 0u32);
926		let result = raw_value_to_string(&value, "")?;
927		assert_eq!(result, "250");
928
929		// Test boolean value
930		let value = Value::bool(true).map_context(|_| 0u32);
931		let result = raw_value_to_string(&value, "")?;
932		assert_eq!(result, "true");
933
934		// Test string value
935		let value = Value::string("hello").map_context(|_| 0u32);
936		let result = raw_value_to_string(&value, "")?;
937		assert_eq!(result, "\"hello\"");
938
939		// Test single-element tuple (should unwrap) - demonstrates format_single_tuples
940		let inner = Value::u128(42);
941		let value = Value::unnamed_composite(vec![inner]).map_context(|_| 0u32);
942		let result = raw_value_to_string(&value, "")?;
943		assert_eq!(result, "0x2a"); // 42 in hex - unwrapped from tuple
944
945		// Test multi-element composite - hex formatted
946		let value =
947			Value::unnamed_composite(vec![Value::u128(1), Value::u128(2)]).map_context(|_| 0u32);
948		let result = raw_value_to_string(&value, "")?;
949		assert_eq!(result, "0x0102"); // Formatted as hex bytes
950
951		Ok(())
952	}
953
954	#[test]
955	fn call_item_default_works() {
956		let item = CallItem::default();
957		assert!(matches!(item, CallItem::Function(_)));
958		if let CallItem::Function(f) = item {
959			assert_eq!(f, Function::default());
960		}
961	}
962
963	#[test]
964	fn call_item_display_works() {
965		let function = Function {
966			pallet: "System".to_string(),
967			name: "remark".to_string(),
968			..Default::default()
969		};
970		let item = CallItem::Function(function);
971		assert_eq!(format!("{item}"), "remark");
972
973		let constant = Constant {
974			pallet: "System".to_string(),
975			name: "BlockHashCount".to_string(),
976			docs: "docs".to_string(),
977			value: Value::u128(250).map_context(|_| 0u32),
978		};
979		let item = CallItem::Constant(constant);
980		assert_eq!(format!("{item}"), "BlockHashCount");
981
982		let storage = Storage {
983			pallet: "System".to_string(),
984			name: "Account".to_string(),
985			docs: "docs".to_string(),
986			type_id: 42,
987			key_id: None,
988			query_all: false,
989		};
990		let item = CallItem::Storage(storage);
991		assert_eq!(format!("{item}"), "Account");
992	}
993
994	#[test]
995	fn call_item_as_methods_work() {
996		let function = Function {
997			pallet: "System".to_string(),
998			name: "remark".to_string(),
999			..Default::default()
1000		};
1001		let item = CallItem::Function(function.clone());
1002		assert_eq!(item.as_function(), Some(&function));
1003		assert_eq!(item.as_constant(), None);
1004		assert_eq!(item.as_storage(), None);
1005
1006		let constant = Constant {
1007			pallet: "System".to_string(),
1008			name: "BlockHashCount".to_string(),
1009			docs: "docs".to_string(),
1010			value: Value::u128(250).map_context(|_| 0u32),
1011		};
1012		let item = CallItem::Constant(constant.clone());
1013		assert_eq!(item.as_function(), None);
1014		assert_eq!(item.as_constant(), Some(&constant));
1015		assert_eq!(item.as_storage(), None);
1016
1017		let storage = Storage {
1018			pallet: "System".to_string(),
1019			name: "Account".to_string(),
1020			docs: "docs".to_string(),
1021			type_id: 42,
1022			key_id: None,
1023			query_all: false,
1024		};
1025		let item = CallItem::Storage(storage.clone());
1026		assert_eq!(item.as_function(), None);
1027		assert_eq!(item.as_constant(), None);
1028		assert_eq!(item.as_storage(), Some(&storage));
1029	}
1030
1031	#[test]
1032	fn call_item_name_works() {
1033		let function = Function {
1034			pallet: "System".to_string(),
1035			name: "remark".to_string(),
1036			..Default::default()
1037		};
1038		let item = CallItem::Function(function);
1039		assert_eq!(item.name(), "remark");
1040
1041		let constant = Constant {
1042			pallet: "System".to_string(),
1043			name: "BlockHashCount".to_string(),
1044			docs: "docs".to_string(),
1045			value: Value::u128(250).map_context(|_| 0u32),
1046		};
1047		let item = CallItem::Constant(constant);
1048		assert_eq!(item.name(), "BlockHashCount");
1049
1050		let storage = Storage {
1051			pallet: "System".to_string(),
1052			name: "Account".to_string(),
1053			docs: "docs".to_string(),
1054			type_id: 42,
1055			key_id: None,
1056			query_all: false,
1057		};
1058		let item = CallItem::Storage(storage);
1059		assert_eq!(item.name(), "Account");
1060	}
1061
1062	#[test]
1063	fn call_item_hint_works() {
1064		let function = Function {
1065			pallet: "System".to_string(),
1066			name: "remark".to_string(),
1067			..Default::default()
1068		};
1069		let item = CallItem::Function(function);
1070		assert_eq!(item.hint(), "📝 [EXTRINSIC]");
1071
1072		let constant = Constant {
1073			pallet: "System".to_string(),
1074			name: "BlockHashCount".to_string(),
1075			docs: "docs".to_string(),
1076			value: Value::u128(250).map_context(|_| 0u32),
1077		};
1078		let item = CallItem::Constant(constant);
1079		assert_eq!(item.hint(), "[CONSTANT]");
1080
1081		let storage = Storage {
1082			pallet: "System".to_string(),
1083			name: "Account".to_string(),
1084			docs: "docs".to_string(),
1085			type_id: 42,
1086			key_id: None,
1087			query_all: false,
1088		};
1089		let item = CallItem::Storage(storage);
1090		assert_eq!(item.hint(), "[STORAGE]");
1091	}
1092
1093	#[test]
1094	fn call_item_docs_works() {
1095		let function = Function {
1096			pallet: "System".to_string(),
1097			name: "remark".to_string(),
1098			docs: "Make some on-chain remark.".to_string(),
1099			..Default::default()
1100		};
1101		let item = CallItem::Function(function);
1102		assert_eq!(item.docs(), "Make some on-chain remark.");
1103
1104		let constant = Constant {
1105			pallet: "System".to_string(),
1106			name: "BlockHashCount".to_string(),
1107			docs: "Maximum number of block number to block hash mappings to keep.".to_string(),
1108			value: Value::u128(250).map_context(|_| 0u32),
1109		};
1110		let item = CallItem::Constant(constant);
1111		assert_eq!(item.docs(), "Maximum number of block number to block hash mappings to keep.");
1112
1113		let storage = Storage {
1114			pallet: "System".to_string(),
1115			name: "Account".to_string(),
1116			docs: "The full account information for a particular account ID.".to_string(),
1117			type_id: 42,
1118			key_id: None,
1119			query_all: false,
1120		};
1121		let item = CallItem::Storage(storage);
1122		assert_eq!(item.docs(), "The full account information for a particular account ID.");
1123	}
1124
1125	#[test]
1126	fn call_item_pallet_works() {
1127		let function = Function {
1128			pallet: "System".to_string(),
1129			name: "remark".to_string(),
1130			..Default::default()
1131		};
1132		let item = CallItem::Function(function);
1133		assert_eq!(item.pallet(), "System");
1134
1135		let constant = Constant {
1136			pallet: "Balances".to_string(),
1137			name: "ExistentialDeposit".to_string(),
1138			docs: "docs".to_string(),
1139			value: Value::u128(100).map_context(|_| 0u32),
1140		};
1141		let item = CallItem::Constant(constant);
1142		assert_eq!(item.pallet(), "Balances");
1143
1144		let storage = Storage {
1145			pallet: "Timestamp".to_string(),
1146			name: "Now".to_string(),
1147			docs: "docs".to_string(),
1148			type_id: 10,
1149			key_id: None,
1150			query_all: false,
1151		};
1152		let item = CallItem::Storage(storage);
1153		assert_eq!(item.pallet(), "Timestamp");
1154	}
1155
1156	#[test]
1157	fn pallet_get_all_callables_works() {
1158		let function = Function {
1159			pallet: "System".to_string(),
1160			name: "remark".to_string(),
1161			..Default::default()
1162		};
1163		let constant = Constant {
1164			pallet: "System".to_string(),
1165			name: "BlockHashCount".to_string(),
1166			docs: "docs".to_string(),
1167			value: Value::u128(250).map_context(|_| 0u32),
1168		};
1169		let storage = Storage {
1170			pallet: "System".to_string(),
1171			name: "Account".to_string(),
1172			docs: "docs".to_string(),
1173			type_id: 42,
1174			key_id: None,
1175			query_all: false,
1176		};
1177
1178		let pallet = Pallet {
1179			name: "System".to_string(),
1180			index: 0,
1181			docs: "System pallet".to_string(),
1182			functions: vec![function.clone()],
1183			constants: vec![constant.clone()],
1184			state: vec![storage.clone()],
1185		};
1186
1187		let callables = pallet.get_all_callables();
1188		assert_eq!(callables.len(), 3);
1189		assert!(matches!(callables[0], CallItem::Function(_)));
1190		assert!(matches!(callables[1], CallItem::Constant(_)));
1191		assert!(matches!(callables[2], CallItem::Storage(_)));
1192
1193		// Verify the items match
1194		if let CallItem::Function(f) = &callables[0] {
1195			assert_eq!(f, &function);
1196		}
1197		if let CallItem::Constant(c) = &callables[1] {
1198			assert_eq!(c, &constant);
1199		}
1200		if let CallItem::Storage(s) = &callables[2] {
1201			assert_eq!(s, &storage);
1202		}
1203	}
1204
1205	#[test]
1206	fn find_callable_by_name_works() {
1207		let function = Function {
1208			pallet: "System".to_string(),
1209			name: "remark".to_string(),
1210			..Default::default()
1211		};
1212		let constant = Constant {
1213			pallet: "System".to_string(),
1214			name: "BlockHashCount".to_string(),
1215			docs: "docs".to_string(),
1216			value: Value::u128(250).map_context(|_| 0u32),
1217		};
1218		let storage = Storage {
1219			pallet: "System".to_string(),
1220			name: "Account".to_string(),
1221			docs: "docs".to_string(),
1222			type_id: 42,
1223			key_id: None,
1224			query_all: false,
1225		};
1226
1227		let pallets = vec![Pallet {
1228			name: "System".to_string(),
1229			index: 0,
1230			docs: "System pallet".to_string(),
1231			functions: vec![function.clone()],
1232			constants: vec![constant.clone()],
1233			state: vec![storage.clone()],
1234		}];
1235
1236		// Test finding a function
1237		let result = find_callable_by_name(&pallets, "System", "remark");
1238		assert!(result.is_ok());
1239		if let Ok(CallItem::Function(f)) = result {
1240			assert_eq!(f.name, "remark");
1241		}
1242
1243		// Test finding a constant
1244		let result = find_callable_by_name(&pallets, "System", "BlockHashCount");
1245		assert!(result.is_ok());
1246		if let Ok(CallItem::Constant(c)) = result {
1247			assert_eq!(c.name, "BlockHashCount");
1248		}
1249
1250		// Test finding a storage item
1251		let result = find_callable_by_name(&pallets, "System", "Account");
1252		assert!(result.is_ok());
1253		if let Ok(CallItem::Storage(s)) = result {
1254			assert_eq!(s.name, "Account");
1255		}
1256
1257		// Test not finding a callable
1258		let result = find_callable_by_name(&pallets, "System", "NonExistent");
1259		assert!(result.is_err());
1260		assert!(matches!(result.unwrap_err(), Error::FunctionNotFound(_)));
1261
1262		// Test pallet not found
1263		let result = find_callable_by_name(&pallets, "NonExistent", "remark");
1264		assert!(result.is_err());
1265		assert!(matches!(result.unwrap_err(), Error::PalletNotFound(_)));
1266	}
1267
1268	#[test]
1269	fn format_single_tuples_single_element_works() -> Result<()> {
1270		// Create a single-element tuple
1271		let inner_value = Value::u128(42);
1272		let single_tuple = Value::unnamed_composite(vec![inner_value]).map_context(|_| 0u32);
1273
1274		let mut output = String::new();
1275		let result = format_single_tuples(&single_tuple, &mut output);
1276
1277		// Should return Some(Ok(())) and unwrap the tuple
1278		assert!(result.is_some());
1279		assert!(result.unwrap().is_ok());
1280		assert_eq!(output, "42");
1281		Ok(())
1282	}
1283
1284	#[test]
1285	fn format_single_tuples_multi_element_returns_none() -> Result<()> {
1286		// Create a multi-element tuple
1287		let tuple =
1288			Value::unnamed_composite(vec![Value::u128(1), Value::u128(2)]).map_context(|_| 0u32);
1289
1290		let mut output = String::new();
1291		let result = format_single_tuples(&tuple, &mut output);
1292
1293		// Should return None for multi-element tuples
1294		assert!(result.is_none());
1295		assert_eq!(output, "");
1296		Ok(())
1297	}
1298
1299	#[test]
1300	fn format_single_tuples_empty_tuple_returns_none() -> Result<()> {
1301		// Create an empty tuple
1302		let empty_tuple = Value::unnamed_composite(vec![]).map_context(|_| 0u32);
1303
1304		let mut output = String::new();
1305		let result = format_single_tuples(&empty_tuple, &mut output);
1306
1307		// Should return None for empty tuples
1308		assert!(result.is_none());
1309		assert_eq!(output, "");
1310		Ok(())
1311	}
1312
1313	#[test]
1314	fn format_single_tuples_non_composite_returns_none() -> Result<()> {
1315		// Create a non-composite value (not a tuple)
1316		let simple_value = Value::u128(42).map_context(|_| 0u32);
1317
1318		let mut output = String::new();
1319		let result = format_single_tuples(&simple_value, &mut output);
1320
1321		// Should return None for non-composite values
1322		assert!(result.is_none());
1323		assert_eq!(output, "");
1324		Ok(())
1325	}
1326
1327	#[test]
1328	fn format_single_tuples_named_composite_returns_none() -> Result<()> {
1329		// Create a named composite (not an unnamed tuple)
1330		let named_composite =
1331			Value::named_composite(vec![("field", Value::u128(42))]).map_context(|_| 0u32);
1332
1333		let mut output = String::new();
1334		let result = format_single_tuples(&named_composite, &mut output);
1335
1336		// Should return None for named composites
1337		assert!(result.is_none());
1338		assert_eq!(output, "");
1339		Ok(())
1340	}
1341
1342	#[tokio::test]
1343	async fn query_storage_works() -> Result<()> {
1344		use crate::{parse_chain_metadata, set_up_client};
1345		use pop_common::test_env::shared_substrate_ws_url;
1346		use scale_value::stringify::custom_parsers;
1347
1348		let node_url = shared_substrate_ws_url().await;
1349		let client = set_up_client(&node_url).await?;
1350		let pallets = parse_chain_metadata(&client)?;
1351
1352		// Find a map storage item (System::Account exists from genesis with funded dev accounts)
1353		let storage = pallets
1354			.iter()
1355			.find(|p| p.name == "System")
1356			.and_then(|p| p.state.iter().find(|s| s.name == "Account"))
1357			.expect("System::Account storage should exist");
1358
1359		// Query Alice's account (funded at genesis in --dev mode)
1360		let alice_address = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY";
1361		let account_key = scale_value::stringify::from_str_custom()
1362			.add_custom_parser(custom_parsers::parse_ss58)
1363			.parse(alice_address)
1364			.0
1365			.expect("Should parse Alice's address");
1366
1367		let result = storage.query(&client, vec![account_key]).await?;
1368		assert!(result.is_some(), "Alice's account should exist in dev chain from genesis");
1369		Ok(())
1370	}
1371
1372	#[tokio::test]
1373	async fn query_storage_with_key_works() -> Result<()> {
1374		use crate::{parse_chain_metadata, set_up_client};
1375		use pop_common::test_env::shared_substrate_ws_url;
1376
1377		let node_url = shared_substrate_ws_url().await;
1378		let client = set_up_client(&node_url).await?;
1379		let pallets = parse_chain_metadata(&client)?;
1380
1381		// Find a map storage item (System::Account requires a key)
1382		let storage = pallets
1383			.iter()
1384			.find(|p| p.name == "System")
1385			.and_then(|p| p.state.iter().find(|s| s.name == "Account"))
1386			.expect("System::Account storage should exist");
1387
1388		// Use Alice's account as the key
1389		let alice_address = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY";
1390		let account_key = scale_value::stringify::from_str_custom()
1391			.add_custom_parser(custom_parsers::parse_ss58)
1392			.parse(alice_address)
1393			.0
1394			.expect("Should parse Alice's address");
1395
1396		// Query the storage with the account key
1397		let result = storage.query(&client, vec![account_key]).await?;
1398
1399		// Should return Some value for Alice's account (which should exist in a test chain)
1400		assert!(result.is_some());
1401		Ok(())
1402	}
1403
1404	#[test]
1405	fn render_storage_key_values_with_keys_works() -> Result<()> {
1406		// Create test data with keys
1407		let key1 = Value::u128(42);
1408		let key2 = Value::string("test_key");
1409		let value = Value::bool(true).map_context(|_| 0u32);
1410
1411		let key_value_pairs = vec![(vec![key1, key2], value)];
1412
1413		let result = render_storage_key_values(&key_value_pairs)?;
1414
1415		// Expected format with keys
1416		let expected = "[\n  42,\n  \"test_key\",\n  true\n]\n";
1417		assert_eq!(result, expected);
1418		Ok(())
1419	}
1420
1421	#[test]
1422	fn render_storage_key_values_without_keys_works() -> Result<()> {
1423		// Create test data without keys (empty key vector)
1424		let value = Value::u128(100).map_context(|_| 0u32);
1425
1426		let key_value_pairs = vec![(vec![], value)];
1427
1428		let result = render_storage_key_values(&key_value_pairs)?;
1429
1430		// Expected format without keys
1431		let expected = "[\n  100\n]\n";
1432		assert_eq!(result, expected);
1433		Ok(())
1434	}
1435}