1#[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
44pub 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#[derive(Debug, EnumProperty, PartialEq, VariantArray)]
66pub(super) enum Chain {
67 #[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 #[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 fn source(&self) -> Result<Source, Error> {
90 Ok(match self {
91 &Chain::ContractsNode => {
92 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
111pub 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
132pub 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 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 ArchiveFileSpec::new(
202 "artifacts/substrate-contracts-node-mac/substrate-contracts-node".into(),
203 Some(binary.into()),
204 false,
205 ),
206 ArchiveFileSpec::new(
208 "substrate-contracts-node-mac/substrate-contracts-node".into(),
209 Some(binary.into()),
210 false,
211 ),
212 ]),
213 "linux" => Ok(vec![
214 ArchiveFileSpec::new(
216 "artifacts/substrate-contracts-node-linux/substrate-contracts-node".into(),
217 Some(binary.into()),
218 false,
219 ),
220 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}