Skip to main content

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, TxStatus},
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 mut tx = client
95		.tx()
96		.sign_and_submit_then_watch_default(&xt, &signer)
97		.await
98		.map_err(|e| Error::ExtrinsicSubmissionError(format!("{:?}", e)))?;
99
100	let tx_hash = tx.extrinsic_hash();
101
102	while let Some(status) = tx.next().await {
103		match status.map_err(|e| Error::ExtrinsicSubmissionError(format!("{:?}", e)))? {
104			TxStatus::InFinalizedBlock(tx_in_block) => {
105				let events = tx_in_block
106					.wait_for_success()
107					.await
108					.map_err(|e| Error::ExtrinsicSubmissionError(format!("{:?}", e)))?;
109
110				let parsed_events = parse_and_format_events(client, url, &events).await?;
111
112				return Ok(format!(
113					"Extrinsic Submitted with hash: {:?}\n\n{}",
114					tx_hash, parsed_events
115				));
116			},
117			TxStatus::InBestBlock(tx_in_block) => {
118				let events = tx_in_block
119					.wait_for_success()
120					.await
121					.map_err(|e| Error::ExtrinsicSubmissionError(format!("{:?}", e)))?;
122
123				let parsed_events = parse_and_format_events(client, url, &events).await?;
124
125				return Ok(format!(
126					"Extrinsic Submitted with hash: {:?}\n\n{}",
127					tx_hash, parsed_events
128				));
129			},
130			TxStatus::Error { message } => {
131				return Err(Error::ExtrinsicSubmissionError(format!("{:?}", message)));
132			},
133			TxStatus::Invalid { message } => {
134				return Err(Error::ExtrinsicSubmissionError(format!("{:?}", message)));
135			},
136			TxStatus::Dropped { message } => {
137				return Err(Error::ExtrinsicSubmissionError(format!("{:?}", message)));
138			},
139			_ => continue,
140		}
141	}
142
143	Err(Error::ExtrinsicSubmissionError(
144		"Transaction stream ended without finalization".to_string(),
145	))
146}
147
148/// Parses and formats the events from the extrinsic result.
149///
150/// # Arguments
151/// * `client` - The client used to interact with the chain.
152/// * `url` - Endpoint of the node.
153/// * `result` - The extrinsic result from which to extract events.
154pub async fn parse_and_format_events(
155	client: &OnlineClient<SubstrateConfig>,
156	url: &url::Url,
157	result: &ExtrinsicEvents<SubstrateConfig>,
158) -> Result<String, Error> {
159	// Obtain required metadata and parse events. The following is using existing logic from
160	// `cargo-contract`, also used in calling contracts, due to simplicity and can be refactored in
161	// the future.
162	let metadata = client.metadata();
163	let token_metadata = TokenMetadata::query::<SubstrateConfig>(url).await?;
164	let events =
165		DisplayEvents::from_events::<SubstrateConfig, DefaultEnvironment>(result, None, &metadata)?;
166	let events =
167		events.display_events::<DefaultEnvironment>(Verbosity::Default, &token_metadata)?;
168
169	Ok(events)
170}
171
172/// Submits a signed extrinsic.
173///
174/// # Arguments
175/// * `client` - The client used to interact with the chain.
176/// * `payload` - The signed payload string to be submitted.
177pub async fn submit_signed_extrinsic(
178	client: OnlineClient<SubstrateConfig>,
179	payload: String,
180) -> Result<ExtrinsicEvents<SubstrateConfig>, Error> {
181	let hex_encoded =
182		from_hex(&payload).map_err(|e| Error::CallDataDecodingError(e.to_string()))?;
183	let extrinsic = SubmittableTransaction::from_bytes(client, hex_encoded);
184	extrinsic
185		.submit_and_watch()
186		.await
187		.map_err(|e| Error::ExtrinsicSubmissionError(format!("{:?}", e)))?
188		.wait_for_finalized_success()
189		.await
190		.map_err(|e| Error::ExtrinsicSubmissionError(format!("{:?}", e)))
191}
192
193/// Encodes the call data for a given extrinsic into a hexadecimal string.
194///
195/// # Arguments
196/// * `client` - The client used to interact with the chain.
197/// * `xt` - The extrinsic whose call data will be encoded and returned.
198pub fn encode_call_data(
199	client: &OnlineClient<SubstrateConfig>,
200	xt: &DynamicPayload,
201) -> Result<String, Error> {
202	let call_data = xt
203		.encode_call_data(&client.metadata())
204		.map_err(|e| Error::CallDataEncodingError(e.to_string()))?;
205	Ok(to_hex(&call_data, false))
206}
207
208/// Decodes a hex-encoded string into a vector of bytes representing the call data.
209///
210/// # Arguments
211/// * `call_data` - The hex-encoded string representing call data.
212pub fn decode_call_data(call_data: &str) -> Result<Vec<u8>, Error> {
213	from_hex(call_data).map_err(|e| Error::CallDataDecodingError(e.to_string()))
214}
215
216/// This struct implements the [`Payload`] trait and is used to submit
217/// pre-encoded SCALE call data directly, without the dynamic construction of transactions.
218pub struct CallData(Vec<u8>);
219
220impl CallData {
221	/// Create a new instance of `CallData`.
222	pub fn new(data: Vec<u8>) -> CallData {
223		CallData(data)
224	}
225}
226
227impl Payload for CallData {
228	fn encode_call_data_to(
229		&self,
230		_: &subxt::Metadata,
231		out: &mut Vec<u8>,
232	) -> Result<(), subxt::ext::subxt_core::Error> {
233		out.extend_from_slice(&self.0);
234		Ok(())
235	}
236}
237
238#[cfg(test)]
239mod tests {
240	use super::*;
241	use crate::set_up_client;
242	use anyhow::Result;
243
244	const ALICE_SURI: &str = "//Alice";
245
246	#[tokio::test]
247	async fn set_up_client_fails_wrong_url() -> Result<()> {
248		assert!(matches!(
249			set_up_client("wss://wronguri.xyz").await,
250			Err(Error::ConnectionFailure(_))
251		));
252		Ok(())
253	}
254
255	#[tokio::test]
256	async fn construct_extrinsic_works() -> Result<()> {
257		let transfer_allow_death = Function {
258            pallet: "Balances".into(),
259            name: "transfer_allow_death".into(),
260            index: 0,
261            docs: ".".into(),
262            params: vec![
263                Param {
264                    name: "dest".into(),
265                    type_name: "MultiAddress<AccountId32 ([u8;32]),()>: Id(AccountId32 ([u8;32])), Index(Compact<()>), Raw([u8]), Address32([u8;32]), Address20([u8;20])".into(),
266                    sub_params: vec![
267                        Param {
268                            name: "Id".into(),
269                            type_name: "".into(),
270                            sub_params: vec![
271                                Param {
272                                    name: "Id".into(),
273                                    type_name: "AccountId32 ([u8;32])".into(),
274                                    sub_params: vec![
275                                        Param {
276                                            name: "Id".into(),
277                                            type_name: "[u8;32]".into(),
278                                            sub_params: vec![],
279                                            ..Default::default()
280                                        }
281                                    ],
282                                    ..Default::default()
283                                }
284                            ],
285                            ..Default::default()
286                        }],
287                    ..Default::default()
288                },
289                Param {
290                    name: "value".into(),
291                    type_name: "Compact<u128>".into(),
292                    sub_params: vec![],
293                    ..Default::default()
294                }
295            ],
296            is_supported: true,
297        };
298		// Wrong parameters
299		assert!(matches!(
300			construct_extrinsic(
301				&transfer_allow_death,
302				vec![ALICE_SURI.to_string(), "100".to_string()],
303			),
304			Err(Error::ParamProcessingError)
305		));
306		// Valid parameters
307		let xt = construct_extrinsic(
308			&transfer_allow_death,
309			vec![
310				"Id(5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty)".to_string(),
311				"100".to_string(),
312			],
313		)?;
314		assert_eq!(xt.call_name(), "transfer_allow_death");
315		assert_eq!(xt.pallet_name(), "Balances");
316		Ok(())
317	}
318
319	#[tokio::test]
320	async fn construct_sudo_extrinsic_works() -> Result<()> {
321		let xt = construct_extrinsic(
322			&Function::default(),
323			vec![
324				"Id(5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty)".to_string(),
325				"Id(5DAAnrj7VHTznn2AWBemMuyBwZWs6FNFjdyVXUeYum3PTXFy)".to_string(),
326				"100".to_string(),
327			],
328		)?;
329		let xt = construct_sudo_extrinsic(xt);
330		assert_eq!(xt.call_name(), "sudo");
331		assert_eq!(xt.pallet_name(), "Sudo");
332		Ok(())
333	}
334}