Skip to main content

soil_cli/commands/
utils.rs

1// This file is part of Soil.
2
3// Copyright (C) Soil contributors.
4// Copyright (C) Parity Technologies (UK) Ltd.
5// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
6
7//! subcommand utilities
8use crate::{
9	error::{self, Error},
10	OutputType,
11};
12use serde_json::json;
13use std::path::PathBuf;
14use subsoil::core::{
15	crypto::{
16		unwrap_or_default_ss58_version, ExposeSecret, SecretString, Ss58AddressFormat, Ss58Codec,
17		Zeroize,
18	},
19	hexdisplay::HexDisplay,
20	Pair,
21};
22use subsoil::runtime::{traits::IdentifyAccount, MultiSigner};
23
24/// Public key type for Runtime
25pub type PublicFor<P> = <P as subsoil::core::Pair>::Public;
26/// Seed type for Runtime
27pub type SeedFor<P> = <P as subsoil::core::Pair>::Seed;
28
29/// helper method to fetch uri from `Option<String>` either as a file or read from stdin
30pub fn read_uri(uri: Option<&String>) -> error::Result<String> {
31	let uri = if let Some(uri) = uri {
32		let file = PathBuf::from(&uri);
33		if file.is_file() {
34			std::fs::read_to_string(uri)?.trim_end().to_owned()
35		} else {
36			uri.into()
37		}
38	} else {
39		rpassword::prompt_password("URI: ")?
40	};
41
42	Ok(uri)
43}
44
45/// Try to parse given `uri` and print relevant information.
46///
47/// 1. Try to construct the `Pair` while using `uri` as input for [`subsoil::core::Pair::from_phrase`].
48///
49/// 2. Try to construct the `Pair` while using `uri` as input for
50/// [`subsoil::core::Pair::from_string_with_seed`].
51///
52/// 3. Try to construct the `Pair::Public` while using `uri` as input for
53///    [`subsoil::core::crypto::Ss58Codec::from_string_with_version`].
54pub fn print_from_uri<Pair>(
55	uri: &str,
56	password: Option<SecretString>,
57	network_override: Option<Ss58AddressFormat>,
58	output: OutputType,
59) where
60	Pair: subsoil::core::Pair,
61	Pair::Public: Into<MultiSigner>,
62{
63	let password = password.as_ref().map(|s| s.expose_secret().as_str());
64	let network_id = String::from(unwrap_or_default_ss58_version(network_override));
65	if let Ok((pair, seed)) = Pair::from_phrase(uri, password) {
66		let public_key = pair.public();
67		let network_override = unwrap_or_default_ss58_version(network_override);
68
69		match output {
70			OutputType::Json => {
71				let json = json!({
72					"secretPhrase": uri,
73					"networkId": network_id,
74					"secretSeed": format_seed::<Pair>(seed),
75					"publicKey": format_public_key::<Pair>(public_key.clone()),
76					"ss58PublicKey": public_key.to_ss58check_with_version(network_override),
77					"accountId": format_account_id::<Pair>(public_key),
78					"ss58Address": pair.public().into().into_account().to_ss58check_with_version(network_override),
79				});
80				println!(
81					"{}",
82					serde_json::to_string_pretty(&json).expect("Json pretty print failed")
83				);
84			},
85			OutputType::Text => {
86				println!(
87					"Secret phrase:       {}\n  \
88					Network ID:        {}\n  \
89					Secret seed:       {}\n  \
90					Public key (hex):  {}\n  \
91					Account ID:        {}\n  \
92					Public key (SS58): {}\n  \
93					SS58 Address:      {}",
94					uri,
95					network_id,
96					format_seed::<Pair>(seed),
97					format_public_key::<Pair>(public_key.clone()),
98					format_account_id::<Pair>(public_key.clone()),
99					public_key.to_ss58check_with_version(network_override),
100					pair.public().into().into_account().to_ss58check_with_version(network_override),
101				);
102			},
103		}
104	} else if let Ok((pair, seed)) = Pair::from_string_with_seed(uri, password) {
105		let public_key = pair.public();
106		let network_override = unwrap_or_default_ss58_version(network_override);
107
108		match output {
109			OutputType::Json => {
110				let json = json!({
111					"secretKeyUri": uri,
112					"networkId": network_id,
113					"secretSeed": if let Some(seed) = seed { format_seed::<Pair>(seed) } else { "n/a".into() },
114					"publicKey": format_public_key::<Pair>(public_key.clone()),
115					"ss58PublicKey": public_key.to_ss58check_with_version(network_override),
116					"accountId": format_account_id::<Pair>(public_key),
117					"ss58Address": pair.public().into().into_account().to_ss58check_with_version(network_override),
118				});
119				println!(
120					"{}",
121					serde_json::to_string_pretty(&json).expect("Json pretty print failed")
122				);
123			},
124			OutputType::Text => {
125				println!(
126					"Secret Key URI `{}` is account:\n  \
127					Network ID:        {}\n  \
128					Secret seed:       {}\n  \
129					Public key (hex):  {}\n  \
130					Account ID:        {}\n  \
131					Public key (SS58): {}\n  \
132					SS58 Address:      {}",
133					uri,
134					network_id,
135					if let Some(seed) = seed { format_seed::<Pair>(seed) } else { "n/a".into() },
136					format_public_key::<Pair>(public_key.clone()),
137					format_account_id::<Pair>(public_key.clone()),
138					public_key.to_ss58check_with_version(network_override),
139					pair.public().into().into_account().to_ss58check_with_version(network_override),
140				);
141			},
142		}
143	} else if let Ok((public_key, network)) = Pair::Public::from_string_with_version(uri) {
144		let network_override = network_override.unwrap_or(network);
145
146		match output {
147			OutputType::Json => {
148				let json = json!({
149					"publicKeyUri": uri,
150					"networkId": String::from(network_override),
151					"publicKey": format_public_key::<Pair>(public_key.clone()),
152					"accountId": format_account_id::<Pair>(public_key.clone()),
153					"ss58PublicKey": public_key.to_ss58check_with_version(network_override),
154					"ss58Address": public_key.to_ss58check_with_version(network_override),
155				});
156
157				println!(
158					"{}",
159					serde_json::to_string_pretty(&json).expect("Json pretty print failed")
160				);
161			},
162			OutputType::Text => {
163				println!(
164					"Public Key URI `{}` is account:\n  \
165					 Network ID/Version: {}\n  \
166					 Public key (hex):   {}\n  \
167					 Account ID:         {}\n  \
168					 Public key (SS58):  {}\n  \
169					 SS58 Address:       {}",
170					uri,
171					String::from(network_override),
172					format_public_key::<Pair>(public_key.clone()),
173					format_account_id::<Pair>(public_key.clone()),
174					public_key.to_ss58check_with_version(network_override),
175					public_key.to_ss58check_with_version(network_override),
176				);
177			},
178		}
179	} else {
180		println!("Invalid phrase/URI given");
181	}
182}
183
184/// Try to parse given `public` as hex encoded public key and print relevant information.
185pub fn print_from_public<Pair>(
186	public_str: &str,
187	network_override: Option<Ss58AddressFormat>,
188	output: OutputType,
189) -> Result<(), Error>
190where
191	Pair: subsoil::core::Pair,
192	Pair::Public: Into<MultiSigner>,
193{
194	let public = array_bytes::hex2bytes(public_str)?;
195
196	let public_key = Pair::Public::try_from(&public)
197		.map_err(|_| "Failed to construct public key from given hex")?;
198
199	let network_override = unwrap_or_default_ss58_version(network_override);
200
201	match output {
202		OutputType::Json => {
203			let json = json!({
204				"networkId": String::from(network_override),
205				"publicKey": format_public_key::<Pair>(public_key.clone()),
206				"accountId": format_account_id::<Pair>(public_key.clone()),
207				"ss58PublicKey": public_key.to_ss58check_with_version(network_override),
208				"ss58Address": public_key.to_ss58check_with_version(network_override),
209			});
210
211			println!("{}", serde_json::to_string_pretty(&json).expect("Json pretty print failed"));
212		},
213		OutputType::Text => {
214			println!(
215				"Network ID/Version: {}\n  \
216				 Public key (hex):   {}\n  \
217				 Account ID:         {}\n  \
218				 Public key (SS58):  {}\n  \
219				 SS58 Address:       {}",
220				String::from(network_override),
221				format_public_key::<Pair>(public_key.clone()),
222				format_account_id::<Pair>(public_key.clone()),
223				public_key.to_ss58check_with_version(network_override),
224				public_key.to_ss58check_with_version(network_override),
225			);
226		},
227	}
228
229	Ok(())
230}
231
232/// generate a pair from suri
233pub fn pair_from_suri<P: Pair>(suri: &str, password: Option<SecretString>) -> Result<P, Error> {
234	let result = if let Some(pass) = password {
235		let mut pass_str = pass.expose_secret().clone();
236		let pair = P::from_string(suri, Some(&pass_str));
237		pass_str.zeroize();
238		pair
239	} else {
240		P::from_string(suri, None)
241	};
242
243	Ok(result.map_err(|err| format!("Invalid phrase {:?}", err))?)
244}
245
246/// formats seed as hex
247pub fn format_seed<P: subsoil::core::Pair>(seed: SeedFor<P>) -> String {
248	format!("0x{}", HexDisplay::from(&seed.as_ref()))
249}
250
251/// formats public key as hex
252fn format_public_key<P: subsoil::core::Pair>(public_key: PublicFor<P>) -> String {
253	format!("0x{}", HexDisplay::from(&public_key.as_ref()))
254}
255
256/// formats public key as accountId as hex
257fn format_account_id<P: subsoil::core::Pair>(public_key: PublicFor<P>) -> String
258where
259	PublicFor<P>: Into<MultiSigner>,
260{
261	format!("0x{}", HexDisplay::from(&public_key.into().into_account().as_ref()))
262}
263
264/// Allows for calling $method with appropriate crypto impl.
265#[macro_export]
266macro_rules! with_crypto_scheme {
267	(
268		$scheme:expr,
269		$method:ident ( $($params:expr),* $(,)?) $(,)?
270	) => {
271		$crate::with_crypto_scheme!($scheme, $method<>($($params),*))
272	};
273	(
274		$scheme:expr,
275		$method:ident<$($generics:ty),*>( $( $params:expr ),* $(,)?) $(,)?
276	) => {
277		match $scheme {
278			$crate::CryptoScheme::Ecdsa => {
279				$method::<subsoil::core::ecdsa::Pair, $($generics),*>($($params),*)
280			}
281			$crate::CryptoScheme::Sr25519 => {
282				$method::<subsoil::core::sr25519::Pair, $($generics),*>($($params),*)
283			}
284			$crate::CryptoScheme::Ed25519 => {
285				$method::<subsoil::core::ed25519::Pair, $($generics),*>($($params),*)
286			}
287		}
288	};
289}