pop_chains/call/
mod.rs

1// SPDX-License-Identifier: GPL-3.0
2
3use crate::{Function, Pallet, Param, errors::Error, find_callable_by_name};
4use pop_common::{
5	call::{DefaultEnvironment, DisplayEvents, TokenMetadata, Verbosity},
6	create_signer,
7};
8use sp_core::bytes::{from_hex, to_hex};
9use subxt::{
10	OnlineClient, SubstrateConfig,
11	blocks::ExtrinsicEvents,
12	dynamic::Value,
13	tx::{DynamicPayload, Payload, SubmittableTransaction},
14};
15pub mod metadata;
16
17/// Sets up an [OnlineClient] instance for connecting to a blockchain.
18///
19/// # Arguments
20/// * `url` - Endpoint of the node.
21pub async fn set_up_client(url: &str) -> Result<OnlineClient<SubstrateConfig>, Error> {
22	OnlineClient::<SubstrateConfig>::from_url(url)
23		.await
24		.map_err(|e| Error::ConnectionFailure(e.to_string()))
25}
26
27/// Constructs a dynamic extrinsic payload for a specified dispatchable function.
28///
29/// # Arguments
30/// * `function` - A dispatchable function.
31/// * `args` - A vector of string arguments to be passed to construct the extrinsic.
32pub fn construct_extrinsic(
33	function: &Function,
34	args: Vec<String>,
35) -> Result<DynamicPayload, Error> {
36	let parsed_args: Vec<Value> = metadata::parse_dispatchable_arguments(&function.params, args)?;
37	Ok(subxt::dynamic::tx(function.pallet.clone(), function.name.clone(), parsed_args))
38}
39
40/// Constructs a Sudo extrinsic.
41///
42/// # Arguments
43/// * `xt`: The extrinsic representing the dispatchable function call to be dispatched with `Root`
44///   privileges.
45pub fn construct_sudo_extrinsic(xt: DynamicPayload) -> DynamicPayload {
46	subxt::dynamic::tx("Sudo", "sudo", [xt.into_value()].to_vec())
47}
48
49/// Constructs a Proxy call extrinsic.
50///
51/// # Arguments
52/// * `pallets`: List of pallets available within the chain's runtime.
53/// * `proxied_account` - The account on whose behalf the proxy will act.
54/// * `xt`: The extrinsic representing the dispatchable function call to be dispatched using the
55///   proxy.
56pub fn construct_proxy_extrinsic(
57	pallets: &[Pallet],
58	proxied_account: String,
59	xt: DynamicPayload,
60) -> Result<DynamicPayload, Error> {
61	let proxy_function = find_callable_by_name(pallets, "Proxy", "proxy")?;
62	// `find_dispatchable_by_name` doesn't support parsing parameters that are calls.
63	// Therefore, we only parse the first two parameters for the proxy call
64	// using `parse_dispatchable_arguments`, while the last parameter (which is the call)
65	// must be manually added.
66	let required_params: Vec<Param> = match proxy_function {
67		metadata::CallItem::Function(ref function) =>
68			function.params.iter().take(2).cloned().collect(),
69		_ => return Err(Error::CallableNotSupported),
70	};
71	let mut parsed_args: Vec<Value> = metadata::parse_dispatchable_arguments(
72		&required_params,
73		vec![proxied_account, "None()".to_string()],
74	)?;
75	let real = parsed_args.remove(0);
76	let proxy_type = parsed_args.remove(0);
77	Ok(subxt::dynamic::tx("Proxy", "proxy", [real, proxy_type, xt.into_value()].to_vec()))
78}
79
80/// Signs and submits a given extrinsic.
81///
82/// # Arguments
83/// * `client` - The client used to interact with the chain.
84/// * `url` - Endpoint of the node.
85/// * `xt` - The (encoded) extrinsic to be signed and submitted.
86/// * `suri` - The secret URI (e.g., mnemonic or private key) for signing the extrinsic.
87pub async fn sign_and_submit_extrinsic<Xt: Payload>(
88	client: &OnlineClient<SubstrateConfig>,
89	url: &url::Url,
90	xt: Xt,
91	suri: &str,
92) -> Result<String, Error> {
93	let signer = create_signer(suri)?;
94	let result = client
95		.tx()
96		.sign_and_submit_then_watch_default(&xt, &signer)
97		.await
98		.map_err(|e| Error::ExtrinsicSubmissionError(format!("{:?}", e)))?
99		.wait_for_finalized_success()
100		.await
101		.map_err(|e| Error::ExtrinsicSubmissionError(format!("{:?}", e)))?;
102
103	let events = parse_and_format_events(client, url, &result).await?;
104
105	Ok(format!("Extrinsic Submitted with hash: {:?}\n\n{}", result.extrinsic_hash(), events))
106}
107
108/// Parses and formats the events from the extrinsic result.
109///
110/// # Arguments
111/// * `client` - The client used to interact with the chain.
112/// * `url` - Endpoint of the node.
113/// * `result` - The extrinsic result from which to extract events.
114pub async fn parse_and_format_events(
115	client: &OnlineClient<SubstrateConfig>,
116	url: &url::Url,
117	result: &ExtrinsicEvents<SubstrateConfig>,
118) -> Result<String, Error> {
119	// Obtain required metadata and parse events. The following is using existing logic from
120	// `cargo-contract`, also used in calling contracts, due to simplicity and can be refactored in
121	// the future.
122	let metadata = client.metadata();
123	let token_metadata = TokenMetadata::query::<SubstrateConfig>(url).await?;
124	let events =
125		DisplayEvents::from_events::<SubstrateConfig, DefaultEnvironment>(result, None, &metadata)?;
126	let events =
127		events.display_events::<DefaultEnvironment>(Verbosity::Default, &token_metadata)?;
128
129	Ok(events)
130}
131
132/// Submits a signed extrinsic.
133///
134/// # Arguments
135/// * `client` - The client used to interact with the chain.
136/// * `payload` - The signed payload string to be submitted.
137pub async fn submit_signed_extrinsic(
138	client: OnlineClient<SubstrateConfig>,
139	payload: String,
140) -> Result<ExtrinsicEvents<SubstrateConfig>, Error> {
141	let hex_encoded =
142		from_hex(&payload).map_err(|e| Error::CallDataDecodingError(e.to_string()))?;
143	let extrinsic = SubmittableTransaction::from_bytes(client, hex_encoded);
144	extrinsic
145		.submit_and_watch()
146		.await
147		.map_err(|e| Error::ExtrinsicSubmissionError(format!("{:?}", e)))?
148		.wait_for_finalized_success()
149		.await
150		.map_err(|e| Error::ExtrinsicSubmissionError(format!("{:?}", e)))
151}
152
153/// Encodes the call data for a given extrinsic into a hexadecimal string.
154///
155/// # Arguments
156/// * `client` - The client used to interact with the chain.
157/// * `xt` - The extrinsic whose call data will be encoded and returned.
158pub fn encode_call_data(
159	client: &OnlineClient<SubstrateConfig>,
160	xt: &DynamicPayload,
161) -> Result<String, Error> {
162	let call_data = xt
163		.encode_call_data(&client.metadata())
164		.map_err(|e| Error::CallDataEncodingError(e.to_string()))?;
165	Ok(to_hex(&call_data, false))
166}
167
168/// Decodes a hex-encoded string into a vector of bytes representing the call data.
169///
170/// # Arguments
171/// * `call_data` - The hex-encoded string representing call data.
172pub fn decode_call_data(call_data: &str) -> Result<Vec<u8>, Error> {
173	from_hex(call_data).map_err(|e| Error::CallDataDecodingError(e.to_string()))
174}
175
176/// This struct implements the [`Payload`] trait and is used to submit
177/// pre-encoded SCALE call data directly, without the dynamic construction of transactions.
178pub struct CallData(Vec<u8>);
179
180impl CallData {
181	/// Create a new instance of `CallData`.
182	pub fn new(data: Vec<u8>) -> CallData {
183		CallData(data)
184	}
185}
186
187impl Payload for CallData {
188	fn encode_call_data_to(
189		&self,
190		_: &subxt::Metadata,
191		out: &mut Vec<u8>,
192	) -> Result<(), subxt::ext::subxt_core::Error> {
193		out.extend_from_slice(&self.0);
194		Ok(())
195	}
196}
197
198#[cfg(test)]
199mod tests {
200	use super::*;
201	use crate::set_up_client;
202	use anyhow::Result;
203
204	const ALICE_SURI: &str = "//Alice";
205
206	#[tokio::test]
207	async fn set_up_client_fails_wrong_url() -> Result<()> {
208		assert!(matches!(
209			set_up_client("wss://wronguri.xyz").await,
210			Err(Error::ConnectionFailure(_))
211		));
212		Ok(())
213	}
214
215	#[tokio::test]
216	async fn construct_extrinsic_works() -> Result<()> {
217		let transfer_allow_death = Function {
218            pallet: "Balances".into(),
219            name: "transfer_allow_death".into(),
220            index: 0,
221            docs: ".".into(),
222            params: vec![
223                Param {
224                    name: "dest".into(),
225                    type_name: "MultiAddress<AccountId32 ([u8;32]),()>: Id(AccountId32 ([u8;32])), Index(Compact<()>), Raw([u8]), Address32([u8;32]), Address20([u8;20])".into(),
226                    sub_params: vec![
227                        Param {
228                            name: "Id".into(),
229                            type_name: "".into(),
230                            sub_params: vec![
231                                Param {
232                                    name: "Id".into(),
233                                    type_name: "AccountId32 ([u8;32])".into(),
234                                    sub_params: vec![
235                                        Param {
236                                            name: "Id".into(),
237                                            type_name: "[u8;32]".into(),
238                                            sub_params: vec![],
239                                            ..Default::default()
240                                        }
241                                    ],
242                                    ..Default::default()
243                                }
244                            ],
245                            ..Default::default()
246                        }],
247                    ..Default::default()
248                },
249                Param {
250                    name: "value".into(),
251                    type_name: "Compact<u128>".into(),
252                    sub_params: vec![],
253                    ..Default::default()
254                }
255            ],
256            is_supported: true,
257        };
258		// Wrong parameters
259		assert!(matches!(
260			construct_extrinsic(
261				&transfer_allow_death,
262				vec![ALICE_SURI.to_string(), "100".to_string()],
263			),
264			Err(Error::ParamProcessingError)
265		));
266		// Valid parameters
267		let xt = construct_extrinsic(
268			&transfer_allow_death,
269			vec![
270				"Id(5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty)".to_string(),
271				"100".to_string(),
272			],
273		)?;
274		assert_eq!(xt.call_name(), "transfer_allow_death");
275		assert_eq!(xt.pallet_name(), "Balances");
276		Ok(())
277	}
278
279	#[tokio::test]
280	async fn construct_sudo_extrinsic_works() -> Result<()> {
281		let xt = construct_extrinsic(
282			&Function::default(),
283			vec![
284				"Id(5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty)".to_string(),
285				"Id(5DAAnrj7VHTznn2AWBemMuyBwZWs6FNFjdyVXUeYum3PTXFy)".to_string(),
286				"100".to_string(),
287			],
288		)?;
289		let xt = construct_sudo_extrinsic(xt);
290		assert_eq!(xt.call_name(), "sudo");
291		assert_eq!(xt.pallet_name(), "Sudo");
292		Ok(())
293	}
294}