pop_contracts/
node.rs

1// SPDX-License-Identifier: GPL-3.0
2
3#[cfg(feature = "v6")]
4use crate::utils::map_account::MapAccount;
5
6#[cfg(feature = "v5")]
7use contract_extrinsics::{RawParams, RpcRequest};
8#[cfg(feature = "v6")]
9use contract_extrinsics_inkv6::{RawParams, RpcRequest};
10use pop_common::{
11	polkadot_sdk::sort_by_latest_semantic_version,
12	sourcing::{
13		traits::{
14			enums::{Source as _, *},
15			Source as SourceT,
16		},
17		Binary,
18		GitHub::ReleaseArchive,
19		Source,
20	},
21	Error, GitHub,
22};
23use strum_macros::{EnumProperty, VariantArray};
24
25use pop_common::sourcing::{filters::prefix, ArchiveFileSpec};
26use std::{
27	env::consts::{ARCH, OS},
28	fs::File,
29	path::PathBuf,
30	process::{Child, Command, Stdio},
31	time::Duration,
32};
33#[cfg(feature = "v5")]
34use subxt::dynamic::Value;
35use subxt::SubstrateConfig;
36use tokio::time::sleep;
37
38#[cfg(feature = "v5")]
39const BIN_NAME: &str = "substrate-contracts-node";
40#[cfg(feature = "v6")]
41const BIN_NAME: &str = "ink-node";
42const STARTUP: Duration = Duration::from_millis(20_000);
43
44/// Checks if the specified node is alive and responsive.
45///
46/// # Arguments
47///
48/// * `url` - Endpoint of the node.
49pub async fn is_chain_alive(url: url::Url) -> Result<bool, Error> {
50	let request = RpcRequest::new(&url).await;
51	match request {
52		Ok(request) => {
53			let params = RawParams::new(&[])?;
54			let result = request.raw_call("system_health", params).await;
55			match result {
56				Ok(_) => Ok(true),
57				Err(_) => Ok(false),
58			}
59		},
60		Err(_) => Ok(false),
61	}
62}
63
64/// A supported chain.
65#[derive(Debug, EnumProperty, PartialEq, VariantArray)]
66pub(super) enum Chain {
67	/// Minimal Substrate node configured for smart contracts via pallet-contracts.
68	#[strum(props(
69		Repository = "https://github.com/paritytech/substrate-contracts-node",
70		Binary = "substrate-contracts-node",
71		Fallback = "v0.41.0"
72	))]
73	#[cfg(feature = "v5")]
74	ContractsNode,
75	/// Minimal ink node configured for smart contracts via pallet-revive.
76	#[strum(props(
77		Repository = "https://github.com/use-ink/ink-node",
78		Binary = "ink-node",
79		Fallback = "v0.43.0"
80	))]
81	#[cfg(feature = "v6")]
82	ContractsNode,
83}
84
85#[cfg(any(feature = "v5", feature = "v6"))]
86impl SourceT for Chain {
87	type Error = Error;
88	/// Defines the source of a binary for the chain.
89	fn source(&self) -> Result<Source, Error> {
90		Ok(match self {
91			&Chain::ContractsNode => {
92				// Source from GitHub release asset
93				let repo = GitHub::parse(self.repository())?;
94				Source::GitHub(ReleaseArchive {
95					owner: repo.org,
96					repository: repo.name,
97					tag: None,
98					tag_pattern: self.tag_pattern().map(|t| t.into()),
99					prerelease: false,
100					version_comparator: sort_by_latest_semantic_version,
101					fallback: self.fallback().into(),
102					archive: archive_name_by_target()?,
103					contents: release_directory_by_target(self.binary())?,
104					latest: None,
105				})
106			},
107		})
108	}
109}
110
111/// Retrieves the latest release of the contracts node binary, resolves its version, and constructs
112/// a `Binary::Source` with the specified cache path.
113///
114/// # Arguments
115/// * `cache` - The cache directory path.
116/// * `version` - The specific version used for the substrate-contracts-node (`None` will use the
117///   latest available version).
118pub async fn contracts_node_generator(
119	cache: PathBuf,
120	version: Option<&str>,
121) -> Result<Binary, Error> {
122	let chain = &Chain::ContractsNode;
123	let name = chain.binary().to_string();
124	let source = chain
125		.source()?
126		.resolve(&name, version, &cache, |f| prefix(f, &name))
127		.await
128		.into();
129	Ok(Binary::Source { name, source, cache })
130}
131
132/// Runs the latest version of the `substrate-contracts-node` in the background.
133///
134/// # Arguments
135///
136/// * `binary_path` - The path where the binary is stored. Can be the binary name itself if in PATH.
137/// * `output` - The optional log file for node output.
138/// * `port` - The WebSocket port on which the node will listen for connections.
139pub async fn run_contracts_node(
140	binary_path: PathBuf,
141	output: Option<&File>,
142	port: u16,
143) -> Result<Child, Error> {
144	let mut command = Command::new(binary_path);
145	command.arg("-linfo,runtime::contracts=debug");
146	command.arg(format!("--rpc-port={}", port));
147	if let Some(output) = output {
148		command.stdout(Stdio::from(output.try_clone()?));
149		command.stderr(Stdio::from(output.try_clone()?));
150	}
151
152	let process = command.spawn()?;
153
154	// Wait until the node is ready
155	sleep(STARTUP).await;
156
157	#[cfg(feature = "v5")]
158	let data = Value::from_bytes(subxt::utils::to_hex("initialize contracts node"));
159	#[cfg(feature = "v5")]
160	let payload = subxt::dynamic::tx("System", "remark", [data].to_vec());
161	#[cfg(feature = "v6")]
162	let payload = MapAccount::new().build();
163
164	let client = subxt::client::OnlineClient::<SubstrateConfig>::from_url(format!(
165		"ws://127.0.0.1:{}",
166		port
167	))
168	.await
169	.map_err(|e| Error::AnyhowError(e.into()))?;
170	client
171		.tx()
172		.sign_and_submit_default(&payload, &subxt_signer::sr25519::dev::alice())
173		.await
174		.map_err(|e| Error::AnyhowError(e.into()))?;
175
176	Ok(process)
177}
178
179fn archive_name_by_target() -> Result<String, Error> {
180	match OS {
181		"macos" => Ok(format!("{}-mac-universal.tar.gz", BIN_NAME)),
182		"linux" => Ok(format!("{}-linux.tar.gz", BIN_NAME)),
183		_ => Err(Error::UnsupportedPlatform { arch: ARCH, os: OS }),
184	}
185}
186#[cfg(feature = "v6")]
187fn release_directory_by_target(binary: &str) -> Result<Vec<ArchiveFileSpec>, Error> {
188	match OS {
189		"macos" => Ok("ink-node-mac/ink-node"),
190		"linux" => Ok("ink-node-linux/ink-node"),
191		_ => Err(Error::UnsupportedPlatform { arch: ARCH, os: OS }),
192	}
193	.map(|name| vec![ArchiveFileSpec::new(name.into(), Some(binary.into()), true)])
194}
195
196#[cfg(feature = "v5")]
197fn release_directory_by_target(binary: &str) -> Result<Vec<ArchiveFileSpec>, Error> {
198	match OS {
199		"macos" => Ok(vec![
200			// < v0.42.0
201			ArchiveFileSpec::new(
202				"artifacts/substrate-contracts-node-mac/substrate-contracts-node".into(),
203				Some(binary.into()),
204				false,
205			),
206			// >=v0.42.0
207			ArchiveFileSpec::new(
208				"substrate-contracts-node-mac/substrate-contracts-node".into(),
209				Some(binary.into()),
210				false,
211			),
212		]),
213		"linux" => Ok(vec![
214			// < v0.42.0
215			ArchiveFileSpec::new(
216				"artifacts/substrate-contracts-node-linux/substrate-contracts-node".into(),
217				Some(binary.into()),
218				false,
219			),
220			// >=v0.42.0
221			ArchiveFileSpec::new(
222				"substrate-contracts-node-linux/substrate-contracts-node".into(),
223				Some(binary.into()),
224				false,
225			),
226		]),
227		_ => Err(Error::UnsupportedPlatform { arch: ARCH, os: OS }),
228	}
229}
230
231#[cfg(test)]
232mod tests {
233	use super::*;
234	use anyhow::{Error, Result};
235
236	const POLKADOT_NETWORK_URL: &str = "wss://polkadot-rpc.publicnode.com";
237
238	#[tokio::test]
239	async fn directory_path_by_target() -> Result<()> {
240		let archive = archive_name_by_target();
241		if cfg!(target_os = "macos") {
242			assert_eq!(archive?, format!("{BIN_NAME}-mac-universal.tar.gz"));
243		} else if cfg!(target_os = "linux") {
244			assert_eq!(archive?, format!("{BIN_NAME}-linux.tar.gz"));
245		} else {
246			assert!(archive.is_err())
247		}
248		Ok(())
249	}
250
251	#[tokio::test]
252	async fn is_chain_alive_works() -> Result<(), Error> {
253		let local_url = url::Url::parse("ws://wrong")?;
254		assert!(!is_chain_alive(local_url).await?);
255		let polkadot_url = url::Url::parse(POLKADOT_NETWORK_URL)?;
256		assert!(is_chain_alive(polkadot_url).await?);
257		Ok(())
258	}
259
260	#[tokio::test]
261	async fn contracts_node_generator_works() -> anyhow::Result<()> {
262		let expected = Chain::ContractsNode;
263		let archive = archive_name_by_target()?;
264		let contents = release_directory_by_target(BIN_NAME)?;
265		#[cfg(feature = "v5")]
266		let owner = "paritytech";
267		#[cfg(feature = "v5")]
268		let versions = ["v0.41.0", "v0.42.0"];
269		#[cfg(feature = "v6")]
270		let owner = "use-ink";
271		#[cfg(feature = "v6")]
272		let versions = ["v0.43.0"];
273		for version in versions {
274			let temp_dir = tempfile::tempdir().expect("Could not create temp dir");
275			let cache = temp_dir.path().join("cache");
276			let binary = contracts_node_generator(cache.clone(), Some(version)).await?;
277
278			assert!(matches!(binary, Binary::Source { name, source, cache}
279				if name == expected.binary() &&
280					*source == Source::GitHub(ReleaseArchive {
281						owner: owner.to_string(),
282						repository: BIN_NAME.to_string(),
283							tag: Some(version.to_string()),
284							tag_pattern: expected.tag_pattern().map(|t| t.into()),
285							prerelease: false,
286							version_comparator: sort_by_latest_semantic_version,
287							fallback: expected.fallback().into(),
288							archive: archive.clone(),
289							contents: contents.clone(),
290							latest: None,
291						})
292					&&
293				cache == cache
294			));
295		}
296		Ok(())
297	}
298}