Skip to main content

pop_contracts/utils/
metadata.rs

1// SPDX-License-Identifier: GPL-3.0
2
3//! Functionality for processing and extracting metadata from ink! smart contracts.
4
5use crate::{DefaultEnvironment, errors::Error};
6use contract_extrinsics::{ContractArtifacts, ContractStorageRpc, TrieId};
7use contract_transcode::{
8	ContractMessageTranscoder,
9	ink_metadata::{MessageParamSpec, layout::Layout},
10};
11use ink_env::call::utils::EncodeArgsWith;
12use pop_common::{
13	DefaultConfig, find_contract_artifact_path, format_type, manifest::from_path,
14	parse_h160_account,
15};
16use scale_info::{PortableRegistry, Type, form::PortableForm};
17use sp_core::blake2_128;
18use std::path::Path;
19use url::Url;
20
21const MAPPING_TYPE_PATH: &str = "ink_storage::lazy::mapping::Mapping";
22
23/// Represents a callable entity within a smart contract, either a function or storage item.
24#[derive(Clone, Eq, PartialEq, Debug)]
25pub enum ContractCallable {
26	/// A callable function (message or constructor).
27	Function(ContractFunction),
28	/// A storage item that can be queried.
29	Storage(ContractStorage),
30}
31
32impl ContractCallable {
33	/// Returns the name/label of the callable entity.
34	///
35	/// For functions, returns the function label.
36	/// For storage items, returns the storage field name.
37	///
38	/// # Returns
39	/// A string containing the name of the callable entity.
40	pub fn name(&self) -> String {
41		match self {
42			ContractCallable::Function(f) => f.label.clone(),
43			ContractCallable::Storage(s) => s.name.clone(),
44		}
45	}
46
47	/// Returns a descriptive hint string indicating the type of this callable entity.
48	pub fn hint(&self) -> String {
49		match self {
50			ContractCallable::Function(f) => {
51				let prelude = if f.mutates { "📝 [MUTATES] " } else { "[READS] " };
52				format!("{}{}", prelude, f.label)
53			},
54			ContractCallable::Storage(s) => {
55				format!("[STORAGE] {}", &s.name)
56			},
57		}
58	}
59
60	/// Returns a descriptive documentation string for this callable entity.
61	pub fn docs(&self) -> String {
62		match self {
63			ContractCallable::Function(f) => f.docs.clone(),
64			ContractCallable::Storage(s) => s.type_name.clone(),
65		}
66	}
67}
68
69/// Describes a parameter.
70#[derive(Debug, Clone, PartialEq, Eq)]
71pub struct Param {
72	/// The label of the parameter.
73	pub label: String,
74	/// The type name of the parameter.
75	pub type_name: String,
76}
77
78/// Describes a contract function.
79#[derive(Clone, PartialEq, Eq, Debug)]
80pub struct ContractFunction {
81	/// The label of the function.
82	pub label: String,
83	/// If the function accepts any `value` from the caller.
84	pub payable: bool,
85	/// The parameters of the deployment handler.
86	pub args: Vec<Param>,
87	/// The function documentation.
88	pub docs: String,
89	/// If the message/constructor is the default for off-chain consumers (e.g UIs).
90	pub default: bool,
91	/// If the message is allowed to mutate the contract state. true for constructors.
92	pub mutates: bool,
93}
94
95/// Describes a contract storage item.
96#[derive(Debug, Clone, PartialEq, Eq)]
97pub struct ContractStorage {
98	/// The name of the storage field.
99	pub name: String,
100	/// The type name of the storage field.
101	pub type_name: String,
102	/// The storage key used to fetch the value from the contract.
103	pub storage_key: u32,
104	/// The type ID from the metadata registry, used for decoding storage values.
105	pub type_id: u32,
106	/// The type name of the mapping key, when this storage is a mapping. None otherwise.
107	pub key_type_name: Option<String>,
108}
109
110/// Specifies the type of contract function, either a constructor or a message.
111#[derive(Clone, PartialEq, Eq)]
112pub enum FunctionType {
113	/// Function that initializes and creates a new contract instance.
114	Constructor,
115	/// Function that can be called on an instantiated contract.
116	Message,
117}
118
119/// Extracts a list of smart contract messages parsing the contract artifact.
120///
121/// # Arguments
122/// * `path` -  Location path of the project or contract artifact.
123pub fn get_messages<P>(path: P) -> Result<Vec<ContractFunction>, Error>
124where
125	P: AsRef<Path>,
126{
127	get_contract_functions(path.as_ref(), FunctionType::Message)
128}
129
130/// Extracts a list of smart contract contructors parsing the contract artifact.
131///
132/// # Arguments
133/// * `path` -  Location path of the project or contract artifact.
134pub fn get_constructors<P>(path: P) -> Result<Vec<ContractFunction>, Error>
135where
136	P: AsRef<Path>,
137{
138	get_contract_functions(path.as_ref(), FunctionType::Constructor)
139}
140
141fn collapse_docs(docs: &[String]) -> String {
142	docs.iter()
143		.map(|s| if s.is_empty() { " " } else { s })
144		.collect::<Vec<_>>()
145		.join("")
146		.trim()
147		.to_string()
148}
149
150fn get_contract_transcoder(path: &Path) -> anyhow::Result<ContractMessageTranscoder> {
151	let contract_artifacts = if path.is_dir() || path.ends_with("Cargo.toml") {
152		let cargo_toml_path =
153			if path.ends_with("Cargo.toml") { path.to_path_buf() } else { path.join("Cargo.toml") };
154		let artifact_from_manifest =
155			|| ContractArtifacts::from_manifest_or_file(Some(&cargo_toml_path), None);
156
157		let artifact = from_path(&cargo_toml_path)
158			.ok()
159			.and_then(|manifest| manifest.package)
160			.and_then(|package| {
161				let project_root = cargo_toml_path.parent().unwrap_or(&cargo_toml_path);
162				find_contract_artifact_path(project_root, package.name())
163			});
164
165		if let Some(contract_path) = artifact {
166			ContractArtifacts::from_manifest_or_file(None, Some(&contract_path))?
167		} else {
168			artifact_from_manifest()?
169		}
170	} else {
171		ContractArtifacts::from_manifest_or_file(None, Some(&path.to_path_buf()))?
172	};
173	contract_artifacts.contract_transcoder()
174}
175
176async fn decode_mapping(
177	storage: &ContractStorage,
178	rpc: &ContractStorageRpc<DefaultConfig>,
179	trie_id: &TrieId,
180	ty: &Type<PortableForm>,
181	transcoder: &ContractMessageTranscoder,
182	key_filter: Option<&str>,
183) -> anyhow::Result<String> {
184	// Fetch ALL contract keys, then filter to those belonging to this mapping's root_key.
185	// This mirrors contract-extrinsics behavior and is robust across hashing strategies.
186	let mut all_keys = Vec::new();
187	let mut start_key: Option<Vec<u8>> = None;
188	// Page size is chosen to be large enough to cover all mappings in a single trie.
189	const PAGE: u32 = 1000;
190	loop {
191		let page_keys = rpc
192			.fetch_storage_keys_paged(
193				trie_id,
194				None, // no prefix: page through entire child trie
195				PAGE,
196				start_key.as_deref(),
197				None,
198			)
199			.await?;
200		let count = page_keys.len();
201		if count == 0 {
202			break;
203		}
204		start_key = page_keys.last().map(|b| b.0.clone());
205		all_keys.extend(page_keys);
206		if (count as u32) < PAGE {
207			break;
208		}
209	}
210
211	// Filter keys by matching the embedded root key at bytes [16..20].
212	//
213	// Storage key format in ink!:
214	// - Bytes [0..16]:  Blake2-128 hash of the root key (used for key distribution)
215	// - Bytes [16..20]: Root key as u32 in little-endian format (identifies the storage field)
216	// - Bytes [20..]:   SCALE-encoded mapping key (the user's key for this mapping entry)
217	//
218	// This format is defined by ink!'s storage layout and is stable across ink! v4 and v5.
219	// If future versions change this format, this validation check should catch it.
220	let keys: Vec<_> = all_keys
221		.into_iter()
222		.filter(|k| {
223			// Validate minimum key length: must contain hash (16) + root_key (4) = 20 bytes minimum
224			if k.0.len() < 20 {
225				return false;
226			}
227			// Extract the root key from bytes [16..20] and compare with expected storage key
228			let mut rk = [0u8; 4];
229			rk.copy_from_slice(&k.0[16..20]);
230			let root = u32::from_le_bytes(rk);
231			root == storage.storage_key
232		})
233		.collect();
234
235	if keys.is_empty() {
236		return Ok("Mapping is empty".to_string());
237	}
238
239	// Fetch values for all keys in a single batch
240	let values = rpc.fetch_storage_entries(trie_id, &keys, None).await?;
241
242	// Determine K and V type ids from the Mapping<K, V> type
243	let (key_type_id, value_type_id) = match (param_type_id(ty, "K"), param_type_id(ty, "V")) {
244		(Some(k), Some(v)) => (k, v),
245		_ => {
246			// Fallback: cannot determine generics; show raw count
247			return Ok(format!("Mapping {{ {} entries }}", values.len()));
248		},
249	};
250
251	// Zip keys and values into a simple Vec for decoding/formatting
252	let pairs: Vec<(Vec<u8>, Option<Vec<u8>>)> = keys
253		.into_iter()
254		.zip(values.into_iter())
255		.map(|(k, v)| (k.0, v.map(|b| b.0)))
256		.collect();
257
258	decode_mapping_impl(pairs, key_type_id, value_type_id, transcoder, key_filter)
259}
260
261// A small helper to make mapping decoding logic unit-testable without RPC.
262// It expects full storage keys (including the 20-byte prefix) paired with optional values.
263pub(crate) fn decode_mapping_impl(
264	pairs: Vec<(Vec<u8>, Option<Vec<u8>>)>,
265	key_type_id: u32,
266	value_type_id: u32,
267	transcoder: &ContractMessageTranscoder,
268	key_filter: Option<&str>,
269) -> anyhow::Result<String> {
270	// Prepare optional filter string (trimmed) for comparison with decoded key rendering
271	let key_filter = key_filter.map(|s| s.trim()).filter(|s| !s.is_empty());
272
273	if pairs.is_empty() {
274		return Ok("Mapping is empty".to_string());
275	}
276
277	let mut rendered_pairs: Vec<String> = Vec::new();
278	for (key, val_opt) in pairs.into_iter() {
279		if let Some(val) = val_opt {
280			// Extract the SCALE-encoded mapping key bytes following the 20-byte prefix
281			let key_bytes = if key.len() > 20 { &key[20..] } else { &[] };
282			let k_decoded = transcoder.decode(key_type_id, &mut &key_bytes[..])?;
283			let v_decoded = transcoder.decode(value_type_id, &mut &val[..])?;
284			let k_str = k_decoded.to_string();
285			if let Some(filter) = key_filter {
286				if k_str == filter {
287					// Found the requested key; stop early and return only the value
288					return Ok(v_decoded.to_string());
289				}
290			} else {
291				rendered_pairs.push(format!("{{ {k_str} => {v_decoded} }}"));
292			}
293		}
294	}
295	if rendered_pairs.is_empty() {
296		if key_filter.is_some() {
297			Ok("No value found for the provided key".to_string())
298		} else {
299			Ok("Mapping is empty".to_string())
300		}
301	} else {
302		Ok(rendered_pairs.join("\n"))
303	}
304}
305
306/// Fetches and decodes a storage value from a deployed smart contract.
307///
308/// # Arguments
309/// * `storage` - Storage item descriptor containing key and type information
310/// * `account` - Contract address as string
311/// * `rpc_url` - URL of the RPC endpoint to connect to
312/// * `path` - Path to contract artifacts for metadata access
313///
314/// # Returns
315/// * `Ok(String)` - The decoded storage value as a string
316/// * `Err(anyhow::Error)` - If any step fails
317pub async fn fetch_contract_storage(
318	storage: &ContractStorage,
319	account: &str,
320	rpc_url: &Url,
321	path: &Path,
322) -> anyhow::Result<String> {
323	fetch_contract_storage_with_param(storage, account, rpc_url, path, None).await
324}
325
326/// Fetches and decodes a storage value from a deployed smart contract,
327/// with optional filtering for mappings.
328///
329/// This function retrieves the value of a storage item from a deployed smart contract.
330/// For regular storage items, it returns the decoded value.
331/// For mapping types (Mapping<K,V>), it can either return all key-value pairs or filter for a
332/// specific key if provided.
333///
334/// # Arguments
335/// * `storage` - Storage item descriptor containing key and type information
336/// * `account` - Contract address as string (typically in H160 format)
337/// * `rpc_url` - URL of the RPC endpoint to connect to
338/// * `path` - Path to contract artifacts for metadata access
339/// * `mapping_key` - Optional key string for filtering mapping entries. Only used if the storage
340///   item is a Mapping<K,V>. The key string must be compatible with the mapping's key type K.
341pub async fn fetch_contract_storage_with_param(
342	storage: &ContractStorage,
343	account: &str,
344	rpc_url: &Url,
345	path: &Path,
346	mapping_key: Option<&str>,
347) -> anyhow::Result<String> {
348	// Get the transcoder to decode the storage value
349	let transcoder = get_contract_transcoder(path)?;
350
351	// Create RPC client
352	let rpc = ContractStorageRpc::<DefaultConfig>::new(rpc_url).await?;
353
354	// Parse account address to AccountId
355	let account_id = parse_h160_account(account)?;
356
357	// Fetch contract info to get the trie_id
358	let contract_info = rpc.fetch_contract_info::<DefaultEnvironment>(&account_id).await?;
359	let trie_id = contract_info.trie_id();
360
361	// Detect if this storage item is a Mapping<K, V> from its type information
362	let registry = transcoder.metadata().registry();
363	if let Some(ty) = registry.resolve(storage.type_id) {
364		let path = ty.path.to_string();
365		if path == MAPPING_TYPE_PATH {
366			return decode_mapping(storage, &rpc, trie_id, ty, &transcoder, mapping_key).await;
367		}
368	}
369
370	// Non-mapping storage: fetch a single value by its root key
371	// Encode the storage key as bytes: blake2_128 hash (16 bytes) + root_key (4 bytes)
372	let root_key_bytes = storage.storage_key.encode();
373	let mut full_key = blake2_128(&root_key_bytes).to_vec();
374	full_key.extend_from_slice(&root_key_bytes);
375
376	// Fetch the storage value
377	let bytes = full_key.into();
378	let value = rpc.fetch_contract_storage(trie_id, &bytes, None).await?;
379
380	match value {
381		Some(data) => {
382			// Decode the raw bytes using the type_id from storage
383			let decoded_value = transcoder.decode(storage.type_id, &mut &data.0[..])?;
384			Ok(decoded_value.to_string())
385		},
386		None => Ok("No value found".to_string()),
387	}
388}
389
390/// Extracts a list of smart contract storage items parsing the contract artifact.
391///
392/// # Arguments
393/// * `path` - Location path of the project or contract artifact.
394pub fn get_contract_storage_info(path: &Path) -> Result<Vec<ContractStorage>, Error> {
395	let transcoder = get_contract_transcoder(path)?;
396	let metadata = transcoder.metadata();
397	let layout = metadata.layout();
398	let registry = metadata.registry();
399
400	let mut storage_items = Vec::new();
401	extract_storage_fields(layout, registry, &mut storage_items);
402
403	Ok(storage_items)
404}
405
406// Recursively extracts storage fields from the layout
407fn extract_storage_fields(
408	layout: &Layout<PortableForm>,
409	registry: &PortableRegistry,
410	storage_items: &mut Vec<ContractStorage>,
411) {
412	match layout {
413		Layout::Root(root_layout) => {
414			// For root layout, capture the root key and traverse into the nested layout
415			let root_key = *root_layout.root_key().key();
416			extract_storage_fields_with_key(
417				root_layout.layout(),
418				registry,
419				storage_items,
420				root_key,
421				Some(root_layout.ty().id),
422			);
423		},
424		Layout::Struct(struct_layout) => {
425			// For struct layout at the top level (no root key yet), skip it
426			// This shouldn't normally happen as Root should be the outermost layout
427			for field in struct_layout.fields() {
428				extract_storage_fields(field.layout(), registry, storage_items);
429			}
430		},
431		Layout::Leaf(_) => {
432			// Leaf nodes represent individual storage items but without a name at this level
433			// They are typically accessed through their parent (struct field)
434		},
435		Layout::Hash(_) | Layout::Array(_) | Layout::Enum(_) => {
436			// For complex layouts (hash maps, arrays, enums), we could expand this
437			// but for now we focus on simple struct fields
438		},
439	}
440}
441
442// Helper function to extract storage fields with a known root key
443fn extract_storage_fields_with_key(
444	layout: &Layout<PortableForm>,
445	registry: &PortableRegistry,
446	storage_items: &mut Vec<ContractStorage>,
447	root_key: u32,
448	root_type_id: Option<u32>,
449) {
450	match layout {
451		Layout::Root(root_layout) => {
452			// Nested root layout, update the root key
453			let new_root_key = *root_layout.root_key().key();
454			extract_storage_fields_with_key(
455				root_layout.layout(),
456				registry,
457				storage_items,
458				new_root_key,
459				Some(root_layout.ty().id),
460			);
461		},
462		Layout::Struct(struct_layout) => {
463			// For struct layout, extract all fields with the current root key
464			for field in struct_layout.fields() {
465				extract_field(
466					field.name(),
467					field.layout(),
468					registry,
469					storage_items,
470					root_key,
471					root_type_id,
472				);
473			}
474		},
475		Layout::Leaf(_) => {
476			// Leaf nodes represent individual storage items but without a name at this level
477		},
478		Layout::Hash(_) | Layout::Array(_) | Layout::Enum(_) => {
479			// For complex layouts, we could expand this later
480		},
481	}
482}
483
484fn try_extract_mapping(
485	name: &str,
486	tid: u32,
487	root_key: u32,
488	registry: &PortableRegistry,
489	storage_items: &mut Vec<ContractStorage>,
490) -> bool {
491	if let Some(ty) = registry.resolve(tid) &&
492		ty.path.to_string() == MAPPING_TYPE_PATH
493	{
494		let type_name = format_type(ty, registry);
495		let key_type_name = param_type_id(ty, "K")
496			.and_then(|kid| registry.resolve(kid))
497			.map(|kty| format_type(kty, registry));
498		storage_items.push(ContractStorage {
499			name: name.to_string(),
500			type_name,
501			storage_key: root_key,
502			type_id: tid,
503			key_type_name,
504		});
505		return true;
506	}
507	false
508}
509
510// Extracts a single field and recursively processes nested layouts
511fn extract_field(
512	name: &str,
513	layout: &Layout<PortableForm>,
514	registry: &PortableRegistry,
515	storage_items: &mut Vec<ContractStorage>,
516	root_key: u32,
517	root_type_id: Option<u32>,
518) {
519	match layout {
520		Layout::Leaf(leaf_layout) => {
521			// Get the type ID and resolve it to get the type name
522			let type_id = leaf_layout.ty();
523			if let Some(ty) = registry.resolve(type_id.id) {
524				let type_name = format_type(ty, registry);
525				storage_items.push(ContractStorage {
526					name: name.to_string(),
527					type_name,
528					storage_key: root_key,
529					type_id: type_id.id,
530					key_type_name: None,
531				});
532			}
533		},
534		Layout::Struct(struct_layout) => {
535			// Nested struct - recursively extract its fields with qualified names
536			for field in struct_layout.fields() {
537				let qualified_name = format!("{}.{}", name, field.name());
538				extract_field(
539					&qualified_name,
540					field.layout(),
541					registry,
542					storage_items,
543					root_key,
544					root_type_id,
545				);
546			}
547		},
548		Layout::Array(array_layout) => {
549			// For arrays, iterate over indices and recurse into element layout
550			let len = array_layout.len();
551			for i in 0..len {
552				let qualified_name = format!("{}[{}]", name, i);
553				extract_field(
554					&qualified_name,
555					array_layout.layout(),
556					registry,
557					storage_items,
558					root_key,
559					root_type_id,
560				);
561			}
562		},
563		Layout::Enum(enum_layout) => {
564			// For enums, iterate over variants and their fields
565			for variant_layout in enum_layout.variants().values() {
566				let variant_prefix = format!("{}::{}", name, variant_layout.name());
567				for field in variant_layout.fields() {
568					let qualified_name = format!("{}.{}", variant_prefix, field.name());
569					extract_field(
570						&qualified_name,
571						field.layout(),
572						registry,
573						storage_items,
574						root_key,
575						root_type_id,
576					);
577				}
578			}
579		},
580		Layout::Hash(hash_layout) => {
581			// Hash maps (e.g., Mapping) don't have statically enumerable keys.
582			// If this Root represents a Mapping<K,V>, create a single storage entry for the mapping
583			// itself.
584			if let Some(tid) = root_type_id &&
585				try_extract_mapping(name, tid, root_key, registry, storage_items)
586			{
587				return;
588			}
589			// Otherwise, recurse into the value layout to capture leaf type information.
590			extract_field(
591				name,
592				hash_layout.layout(),
593				registry,
594				storage_items,
595				root_key,
596				root_type_id,
597			);
598		},
599		Layout::Root(root_layout) => {
600			// Nested root updates the storage key; keep the same field name prefix
601			let new_root_key = *root_layout.root_key().key();
602			let tid = root_layout.ty().id;
603			// Some contracts represent Mapping as a Root whose inner layout is a Leaf (value type).
604			// Detect Mapping here and emit a single storage entry for the mapping container.
605			if try_extract_mapping(name, tid, new_root_key, registry, storage_items) {
606				return;
607			}
608			extract_field(
609				name,
610				root_layout.layout(),
611				registry,
612				storage_items,
613				new_root_key,
614				Some(tid),
615			);
616		},
617	}
618}
619
620// Helper to extract a generic parameter type id by name (e.g., "K" or "V")
621fn param_type_id(type_def: &Type<PortableForm>, param_name: &str) -> Option<u32> {
622	type_def
623		.type_params
624		.iter()
625		.find(|p| p.name == param_name)
626		.and_then(|p| p.ty.as_ref())
627		.map(|pt| pt.id)
628}
629
630/// Extracts a list of smart contract functions (messages or constructors) parsing the contract
631/// artifact.
632///
633/// # Arguments
634/// * `path` - Location path of the project or contract artifact.
635/// * `function_type` - Specifies whether to extract messages or constructors.
636fn get_contract_functions(
637	path: &Path,
638	function_type: FunctionType,
639) -> Result<Vec<ContractFunction>, Error> {
640	let transcoder = get_contract_transcoder(path)?;
641	let metadata = transcoder.metadata();
642
643	Ok(match function_type {
644		FunctionType::Message => metadata
645			.spec()
646			.messages()
647			.iter()
648			.map(|message| ContractFunction {
649				label: message.label().to_string(),
650				mutates: message.mutates(),
651				payable: message.payable(),
652				args: process_args(message.args(), metadata.registry()),
653				docs: collapse_docs(message.docs()),
654				default: message.default(),
655			})
656			.collect(),
657		FunctionType::Constructor => metadata
658			.spec()
659			.constructors()
660			.iter()
661			.map(|constructor| ContractFunction {
662				label: constructor.label().to_string(),
663				payable: constructor.payable(),
664				args: process_args(constructor.args(), metadata.registry()),
665				docs: collapse_docs(constructor.docs()),
666				default: constructor.default(),
667				mutates: true,
668			})
669			.collect(),
670	})
671}
672
673/// Extracts the information of a smart contract message parsing the contract artifact.
674///
675/// # Arguments
676/// * `path` -  Location path of the project or contract artifact.
677/// * `message` - The label of the contract message.
678pub fn get_message<P>(path: P, message: &str) -> Result<ContractFunction, Error>
679where
680	P: AsRef<Path>,
681{
682	get_messages(path.as_ref())?
683		.into_iter()
684		.find(|msg| msg.label == message)
685		.ok_or_else(|| Error::InvalidMessageName(message.to_string()))
686}
687
688/// Extracts the information of a smart contract constructor parsing the contract artifact.
689///
690/// # Arguments
691/// * `path` -  Location path of the project or contract artifact.
692/// * `constructor` - The label of the constructor.
693fn get_constructor<P>(path: P, constructor: &str) -> Result<ContractFunction, Error>
694where
695	P: AsRef<Path>,
696{
697	get_constructors(path.as_ref())?
698		.into_iter()
699		.find(|c| c.label == constructor)
700		.ok_or_else(|| Error::InvalidConstructorName(constructor.to_string()))
701}
702
703// Parse the parameters into a vector of argument labels.
704fn process_args(
705	params: &[MessageParamSpec<PortableForm>],
706	registry: &PortableRegistry,
707) -> Vec<Param> {
708	let mut args: Vec<Param> = Vec::new();
709	for arg in params {
710		// Resolve type from registry to provide full type representation.
711		let type_name =
712			format_type(registry.resolve(arg.ty().ty().id).expect("type not found"), registry);
713		args.push(Param { label: arg.label().to_string(), type_name });
714	}
715	args
716}
717
718/// Extracts the information of a smart contract function (message or constructor) parsing the
719/// contract artifact.
720///
721/// # Arguments
722/// * `path` - Location path of the project or contract artifact.
723/// * `label` - The label of the contract function.
724/// * `function_type` - Specifies whether to extract a message or constructor.
725pub fn extract_function<P>(
726	path: P,
727	label: &str,
728	function_type: FunctionType,
729) -> Result<ContractFunction, Error>
730where
731	P: AsRef<Path>,
732{
733	match function_type {
734		FunctionType::Message => get_message(path.as_ref(), label),
735		FunctionType::Constructor => get_constructor(path.as_ref(), label),
736	}
737}
738
739/// Processes a list of argument values for a specified contract function,
740/// wrapping each value in `Some(...)` or replacing it with `None` if the argument is optional.
741///
742/// # Arguments
743/// * `function` - The contract function to process.
744/// * `args` - Argument values provided by the user.
745pub fn process_function_args(
746	function: &ContractFunction,
747	args: Vec<String>,
748) -> Result<Vec<String>, Error> {
749	if args.len() != function.args.len() {
750		return Err(Error::IncorrectArguments {
751			expected: function.args.len(),
752			provided: args.len(),
753		});
754	}
755	Ok(args
756		.into_iter()
757		.zip(&function.args)
758		.map(|(arg, param)| match (param.type_name.starts_with("Option<"), arg.is_empty()) {
759			// If the argument is Option and empty, replace it with `None`
760			(true, true) => "None".to_string(),
761			// If the argument is Option and not empty, wrap it in `Some(...)`
762			(true, false) => format!("Some({})", arg),
763			// If the argument is not Option, return it as is
764			_ => arg,
765		})
766		.collect())
767}
768
769#[cfg(test)]
770mod tests {
771	use std::env;
772
773	use super::*;
774	use crate::{mock_build_process, new_environment};
775	use anyhow::Result;
776	use scale_info::{Registry, TypeDef, TypeDefPrimitive, TypeInfo};
777	use std::{
778		marker::PhantomData,
779		path::PathBuf,
780		sync::{LazyLock, Mutex},
781	};
782	use temp_env;
783	// No need for SCALE encoding helpers in tests; for u8 values the SCALE encoding is the byte
784	// itself.
785
786	const CONTRACT_FILE: &str = "./tests/files/testing.contract";
787	static ENV_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
788
789	/// Returns a shared mock contract directory that persists across nextest
790	/// processes. The fixture is created once (using an atomic directory lock)
791	/// and reused by every test process that calls this function.
792	fn shared_contract_dir() -> PathBuf {
793		let base = std::env::temp_dir().join("pop-test-contract-fixture");
794		let contract_dir = base.join("testing");
795		let ready_marker = base.join(".ready");
796		let lock_dir = base.join(".creating");
797
798		// Fast path: fixture already exists.
799		if ready_marker.exists() {
800			return contract_dir;
801		}
802
803		std::fs::create_dir_all(&base).ok();
804
805		// Try to acquire creation lock (atomic directory creation).
806		match std::fs::create_dir(&lock_dir) {
807			Ok(_) => {
808				// We won the race, create the fixture.
809				let temp_dir =
810					new_environment("testing").expect("Failed to create test environment");
811				let current_dir = env::current_dir().expect("Failed to get current directory");
812				mock_build_process(
813					temp_dir.path().join("testing"),
814					current_dir.join(CONTRACT_FILE),
815					current_dir.join("./tests/files/testing.json"),
816				)
817				.expect("Failed to mock build process");
818
819				// Copy into the shared location.
820				if contract_dir.exists() {
821					std::fs::remove_dir_all(&contract_dir).ok();
822				}
823				copy_dir_recursive(temp_dir.path().join("testing"), &contract_dir);
824
825				// Signal completion and release lock.
826				std::fs::write(&ready_marker, "").unwrap();
827				std::fs::remove_dir(&lock_dir).ok();
828			},
829			Err(_) => {
830				// Another process is creating the fixture, wait for it.
831				while !ready_marker.exists() {
832					std::thread::sleep(std::time::Duration::from_millis(50));
833				}
834			},
835		}
836
837		contract_dir
838	}
839
840	fn copy_dir_recursive(src: impl AsRef<std::path::Path>, dst: &std::path::Path) {
841		std::fs::create_dir_all(dst).unwrap();
842		for entry in std::fs::read_dir(src).unwrap() {
843			let entry = entry.unwrap();
844			let ty = entry.file_type().unwrap();
845			let dest_path = dst.join(entry.file_name());
846			if ty.is_dir() {
847				copy_dir_recursive(entry.path(), &dest_path);
848			} else {
849				std::fs::copy(entry.path(), dest_path).unwrap();
850			}
851		}
852	}
853
854	#[test]
855	fn get_messages_work() -> Result<()> {
856		let contract_dir = shared_contract_dir();
857		let current_dir = env::current_dir().expect("Failed to get current directory");
858
859		// Helper function to avoid duplicated code
860		fn assert_contract_metadata_parsed(message: Vec<ContractFunction>) -> Result<()> {
861			assert_eq!(message.len(), 3);
862			assert_eq!(message[0].label, "flip");
863			assert_eq!(
864				message[0].docs,
865				"A message that can be called on instantiated contracts. This one flips the value of the stored `bool` from `true` to `false` and vice versa."
866			);
867			assert_eq!(message[1].label, "get");
868			assert_eq!(message[1].docs, "Simply returns the current value of our `bool`.");
869			assert_eq!(message[2].label, "specific_flip");
870			assert_eq!(
871				message[2].docs,
872				"A message for testing, flips the value of the stored `bool` with `new_value` and is payable"
873			);
874			// assert parsed arguments
875			assert_eq!(message[2].args.len(), 2);
876			assert_eq!(message[2].args[0].label, "new_value".to_string());
877			assert_eq!(message[2].args[0].type_name, "bool".to_string());
878			assert_eq!(message[2].args[1].label, "number".to_string());
879			assert_eq!(message[2].args[1].type_name, "Option<u32>: None, Some(u32)".to_string());
880			Ok(())
881		}
882
883		// Test with a directory path
884		let message = get_messages(&contract_dir)?;
885		assert_contract_metadata_parsed(message)?;
886
887		// Test with a metadata file path
888		let message = get_messages(current_dir.join(CONTRACT_FILE))?;
889		assert_contract_metadata_parsed(message)?;
890
891		Ok(())
892	}
893
894	#[test]
895	fn get_messages_uses_artifact_when_cargo_unavailable() -> Result<()> {
896		let temp_dir = new_environment("testing")?;
897		let current_dir = env::current_dir().expect("Failed to get current directory");
898		mock_build_process(
899			temp_dir.path().join("testing"),
900			current_dir.join(CONTRACT_FILE),
901			current_dir.join("./tests/files/testing.json"),
902		)?;
903
904		// Ensure the fallback uses the existing artifact instead of invoking cargo.
905		let _env_guard = ENV_LOCK.lock().expect("env lock poisoned");
906		let message = temp_env::with_var("CARGO", Some("/nonexistent/cargo"), || {
907			get_messages(temp_dir.path().join("testing"))
908		});
909
910		let message = message?;
911		assert_eq!(message.len(), 3);
912		assert_eq!(message[0].label, "flip");
913		Ok(())
914	}
915
916	#[test]
917	fn get_message_work() -> Result<()> {
918		let contract_dir = shared_contract_dir();
919		assert!(matches!(
920			get_message(&contract_dir, "wrong_flip"),
921			Err(Error::InvalidMessageName(name)) if name == *"wrong_flip"));
922		let message = get_message(&contract_dir, "specific_flip")?;
923		assert_eq!(message.label, "specific_flip");
924		assert_eq!(
925			message.docs,
926			"A message for testing, flips the value of the stored `bool` with `new_value` and is payable"
927		);
928		// assert parsed arguments
929		assert_eq!(message.args.len(), 2);
930		assert_eq!(message.args[0].label, "new_value".to_string());
931		assert_eq!(message.args[0].type_name, "bool".to_string());
932		assert_eq!(message.args[1].label, "number".to_string());
933		assert_eq!(message.args[1].type_name, "Option<u32>: None, Some(u32)".to_string());
934		Ok(())
935	}
936
937	#[test]
938	fn get_constructors_work() -> Result<()> {
939		let contract_dir = shared_contract_dir();
940		let constructor = get_constructors(&contract_dir)?;
941		assert_eq!(constructor.len(), 2);
942		assert_eq!(constructor[0].label, "new");
943		assert_eq!(
944			constructor[0].docs,
945			"Constructor that initializes the `bool` value to the given `init_value`."
946		);
947		assert_eq!(constructor[1].label, "default");
948		assert_eq!(
949			constructor[1].docs,
950			"Constructor that initializes the `bool` value to `false`. Constructors can delegate to other constructors."
951		);
952		// assert parsed arguments
953		assert_eq!(constructor[0].args.len(), 1);
954		assert_eq!(constructor[0].args[0].label, "init_value".to_string());
955		assert_eq!(constructor[0].args[0].type_name, "bool".to_string());
956		assert_eq!(constructor[1].args.len(), 2);
957		assert_eq!(constructor[1].args[0].label, "init_value".to_string());
958		assert_eq!(constructor[1].args[0].type_name, "bool".to_string());
959		assert_eq!(constructor[1].args[1].label, "number".to_string());
960		assert_eq!(constructor[1].args[1].type_name, "Option<u32>: None, Some(u32)".to_string());
961		Ok(())
962	}
963
964	#[test]
965	fn get_constructor_work() -> Result<()> {
966		let contract_dir = shared_contract_dir();
967		assert!(matches!(
968			get_constructor(&contract_dir, "wrong_constructor"),
969			Err(Error::InvalidConstructorName(name)) if name == *"wrong_constructor"));
970		let constructor = get_constructor(&contract_dir, "default")?;
971		assert_eq!(constructor.label, "default");
972		assert_eq!(
973			constructor.docs,
974			"Constructor that initializes the `bool` value to `false`. Constructors can delegate to other constructors."
975		);
976		// assert parsed arguments
977		assert_eq!(constructor.args.len(), 2);
978		assert_eq!(constructor.args[0].label, "init_value".to_string());
979		assert_eq!(constructor.args[0].type_name, "bool".to_string());
980		assert_eq!(constructor.args[1].label, "number".to_string());
981		assert_eq!(constructor.args[1].type_name, "Option<u32>: None, Some(u32)".to_string());
982		Ok(())
983	}
984
985	#[test]
986	fn process_function_args_work() -> Result<()> {
987		let contract_dir = shared_contract_dir();
988
989		// Test messages
990		assert!(matches!(
991			extract_function(&contract_dir, "wrong_flip", FunctionType::Message),
992			Err(Error::InvalidMessageName(error)) if error == *"wrong_flip"));
993
994		let specific_flip =
995			extract_function(&contract_dir, "specific_flip", FunctionType::Message)?;
996
997		assert!(matches!(
998			process_function_args(&specific_flip, Vec::new()),
999			Err(Error::IncorrectArguments {expected, provided }) if expected == 2 && provided == 0
1000		));
1001
1002		assert_eq!(
1003			process_function_args(&specific_flip, ["true".to_string(), "2".to_string()].to_vec())?,
1004			["true".to_string(), "Some(2)".to_string()]
1005		);
1006
1007		assert_eq!(
1008			process_function_args(&specific_flip, ["true".to_string(), "".to_string()].to_vec())?,
1009			["true".to_string(), "None".to_string()]
1010		);
1011
1012		// Test constructors
1013		assert!(matches!(
1014			extract_function(&contract_dir, "wrong_constructor", FunctionType::Constructor),
1015			Err(Error::InvalidConstructorName(error)) if error == *"wrong_constructor"));
1016
1017		let default_constructor =
1018			extract_function(&contract_dir, "default", FunctionType::Constructor)?;
1019		assert!(matches!(
1020			process_function_args(&default_constructor, Vec::new()),
1021			Err(Error::IncorrectArguments {expected, provided }) if expected == 2 && provided == 0
1022		));
1023
1024		assert_eq!(
1025			process_function_args(
1026				&default_constructor,
1027				["true".to_string(), "2".to_string()].to_vec()
1028			)?,
1029			["true".to_string(), "Some(2)".to_string()]
1030		);
1031
1032		assert_eq!(
1033			process_function_args(
1034				&default_constructor,
1035				["true".to_string(), "".to_string()].to_vec()
1036			)?,
1037			["true".to_string(), "None".to_string()]
1038		);
1039		Ok(())
1040	}
1041
1042	#[test]
1043	fn get_contract_storage_work() -> Result<()> {
1044		let contract_dir = shared_contract_dir();
1045		let current_dir = env::current_dir().expect("Failed to get current directory");
1046
1047		// Test with a directory path
1048		let storage = get_contract_storage_info(contract_dir.as_path())?;
1049		assert_eq!(storage.len(), 2);
1050		assert_eq!(storage[0].name, "value");
1051		assert_eq!(storage[0].type_name, "bool");
1052		assert_eq!(storage[1].name, "number");
1053		// The exact type name may vary, but it should contain u32
1054		assert!(storage[1].type_name.contains("u32"));
1055
1056		// Test with a metadata file path
1057		let storage = get_contract_storage_info(
1058			current_dir.join("./tests/files/testing.contract").as_path(),
1059		)?;
1060		assert_eq!(storage.len(), 2);
1061		assert_eq!(storage[0].name, "value");
1062		assert_eq!(storage[0].type_name, "bool");
1063
1064		Ok(())
1065	}
1066
1067	#[derive(TypeInfo)]
1068	struct DummyKV<K, V>(PhantomData<(K, V)>);
1069
1070	#[test]
1071	fn param_type_id_resolves_generic_k_v() -> Result<()> {
1072		// Build a registry that includes a dummy generic type with params named K and V
1073		let mut reg = Registry::new();
1074		let _ = reg.register_type(&scale_info::meta_type::<DummyKV<u32, bool>>());
1075		let portable: PortableRegistry = reg.into();
1076		// Find our dummy type by its last path segment
1077		let type_id = portable
1078			.types
1079			.iter()
1080			.find(|t| t.ty.path.segments.last().map(|s| s == "DummyKV").unwrap_or(false))
1081			.map(|t| t.id)
1082			.expect("dummy type must exist");
1083		let ty = portable.resolve(type_id).unwrap();
1084		// Ensure helper extracts K and V type ids
1085		let k_id = param_type_id(ty, "K").expect("K param must exist");
1086		let v_id = param_type_id(ty, "V").expect("V param must exist");
1087		let k_ty = portable.resolve(k_id).unwrap();
1088		let v_ty = portable.resolve(v_id).unwrap();
1089		match &k_ty.type_def {
1090			TypeDef::Primitive(p) => assert_eq!(*p, TypeDefPrimitive::U32),
1091			other => panic!("Expected primitive u32 for K, got {:?}", other),
1092		}
1093		match &v_ty.type_def {
1094			TypeDef::Primitive(p) => assert_eq!(*p, TypeDefPrimitive::Bool),
1095			other => panic!("Expected primitive bool for V, got {:?}", other),
1096		}
1097		Ok(())
1098	}
1099
1100	// Helper to build a transcoder from the bundled testing.contract file
1101	fn test_transcoder() -> Result<ContractMessageTranscoder> {
1102		let current_dir = env::current_dir().expect("Failed to get current directory");
1103		get_contract_transcoder(current_dir.join("./tests/files/testing.contract").as_path())
1104	}
1105
1106	// Helper to find the type id for a primitive in the registry
1107	fn find_primitive(reg: &PortableRegistry, prim: TypeDefPrimitive) -> u32 {
1108		reg.types
1109			.iter()
1110			.find_map(|t| match &t.ty.type_def {
1111				TypeDef::Primitive(p) if *p == prim => Some(t.id),
1112				_ => None,
1113			})
1114			.expect("primitive type must exist in registry")
1115	}
1116
1117	#[test]
1118	fn decode_mapping_impl_empty_returns_message() -> Result<()> {
1119		let transcoder = test_transcoder()?;
1120		let reg = transcoder.metadata().registry();
1121		let u8_id = find_primitive(reg, TypeDefPrimitive::U8);
1122
1123		let out = decode_mapping_impl(Vec::new(), u8_id, u8_id, &transcoder, None)?;
1124		assert_eq!(out, "Mapping is empty");
1125		Ok(())
1126	}
1127
1128	#[test]
1129	fn decode_mapping_impl_renders_single_entry() -> Result<()> {
1130		let transcoder = test_transcoder()?;
1131		let reg = transcoder.metadata().registry();
1132		let u8_id = find_primitive(reg, TypeDefPrimitive::U8);
1133
1134		// Build a full storage key: 16-byte hash + 4-byte root + SCALE(key)
1135		let mut full_key = vec![0u8; 16];
1136		full_key.extend_from_slice(&1u32.to_le_bytes());
1137		full_key.push(4u8); // SCALE encoding of u8 is itself
1138		let value_bytes = vec![8u8];
1139
1140		let out = decode_mapping_impl(
1141			vec![(full_key, Some(value_bytes))],
1142			u8_id,
1143			u8_id,
1144			&transcoder,
1145			None,
1146		)?;
1147
1148		assert_eq!(out, "{ 4 => 8 }");
1149		Ok(())
1150	}
1151
1152	#[test]
1153	fn decode_mapping_impl_filter_match_returns_value_only() -> Result<()> {
1154		let transcoder = test_transcoder()?;
1155		let reg = transcoder.metadata().registry();
1156		let u8_id = find_primitive(reg, TypeDefPrimitive::U8);
1157
1158		let mut full_key = vec![0u8; 16];
1159		full_key.extend_from_slice(&1u32.to_le_bytes());
1160		full_key.push(4u8);
1161		let value_bytes = vec![8u8];
1162
1163		let out = decode_mapping_impl(
1164			vec![(full_key, Some(value_bytes))],
1165			u8_id,
1166			u8_id,
1167			&transcoder,
1168			Some("4"),
1169		)?;
1170		assert_eq!(out, "8");
1171		Ok(())
1172	}
1173
1174	#[test]
1175	fn decode_mapping_impl_filter_no_match() -> Result<()> {
1176		let transcoder = test_transcoder()?;
1177		let reg = transcoder.metadata().registry();
1178		let u8_id = find_primitive(reg, TypeDefPrimitive::U8);
1179
1180		let mut full_key = vec![0u8; 16];
1181		full_key.extend_from_slice(&1u32.to_le_bytes());
1182		full_key.push(4u8);
1183		let value_bytes = vec![8u8];
1184
1185		let out = decode_mapping_impl(
1186			vec![(full_key, Some(value_bytes))],
1187			u8_id,
1188			u8_id,
1189			&transcoder,
1190			Some("5"),
1191		)?;
1192		assert_eq!(out, "No value found for the provided key");
1193		Ok(())
1194	}
1195}