partner_chains_smart_contracts_commands/
lib.rs

1//! This crate provides types and functions that can be used to create a CLI
2//! for managing the mainchain smart contracts relevant for a given partner
3//! chain instance.
4//!
5//! ## Common Arguments
6//!
7//! Most type commands (usualy ending in "Cmd") take a [CommonArguments]
8//! struct as argument. It stores the information neccessary for connecting
9//! to the Ogmios server and retrying the operations like checking if a transaction
10//! is included in the blockchain.
11//!
12//! ## Subcommands
13//!
14//! Each subcommand has its own command type, which implements the [clap::Parser]
15//! trait. Each command type also has a `execute` method, which is used to execute
16//! the command.
17//!
18//! Subcommands can execute transactions on the mainchain, query the mainchain
19//! and also provide other utilities for managing the smart contracts.
20//!
21//! ## Result types
22//!
23//! Most commands return result of [serde_json::Value].
24//! The returned value is printed to the ouptut at the end of the command execution.
25use ogmios_client::jsonrpsee::{OgmiosClients, client_for_url};
26use partner_chains_cardano_offchain::{
27	await_tx::FixedDelayRetries,
28	cardano_keys::{CardanoKeyFileContent, CardanoPaymentSigningKey},
29	multisig::MultiSigSmartContractResult,
30};
31use serde::Serialize;
32use sidechain_domain::*;
33use std::time::Duration;
34
35pub mod assemble_tx;
36pub mod d_parameter;
37pub mod get_scripts;
38pub mod governance;
39pub mod governed_map;
40pub mod permissioned_candidates;
41pub mod register;
42pub mod reserve;
43pub mod sign_tx;
44
45#[derive(Clone, Debug, clap::Subcommand)]
46#[allow(clippy::large_enum_variant)]
47/// Commands for managing the mainchain smart contracts
48pub enum SmartContractsCmd {
49	/// Prints validator addresses and policy IDs of Partner Chain smart contracts
50	GetScripts(get_scripts::GetScripts),
51	/// Upsert DParameter
52	UpsertDParameter(d_parameter::UpsertDParameterCmd),
53	/// Upsert Permissioned Candidates
54	UpsertPermissionedCandidates(permissioned_candidates::UpsertPermissionedCandidatesCmd),
55	/// Register candidate
56	Register(register::RegisterCmd),
57	/// Deregister candidate
58	Deregister(register::DeregisterCmd),
59	#[command(subcommand)]
60	/// Commands for management of rewards reserve
61	Reserve(reserve::ReserveCmd),
62	#[command(subcommand)]
63	/// Commands for management of on-chain governance
64	Governance(governance::GovernanceCmd),
65	/// Assemble and submit a transaction
66	AssembleAndSubmitTx(assemble_tx::AssembleAndSubmitCmd),
67	/// Sign a transaction CBOR using a payment signing key
68	SignTx(sign_tx::SignTxCmd),
69	#[command(subcommand)]
70	/// Manage the Governed Map key-value store on Cardano
71	GovernedMap(governed_map::GovernedMapCmd),
72}
73
74#[derive(Clone, Debug, clap::Parser)]
75#[command(author, version, about, long_about = None)]
76/// Common command arguments
77pub struct CommonArguments {
78	#[arg(default_value = "ws://localhost:1337", long, short = 'O', env)]
79	/// URL of the Ogmios server
80	ogmios_url: String,
81	#[arg(default_value = "180", long, env)]
82	/// Timeout in seconds for Ogmios requests.
83	ogmios_requests_timeout_seconds: u64,
84	#[arg(default_value = "5", long)]
85	/// Delay between retries in seconds. System will wait this long between
86	/// queries checking if transaction is included in the blockchain.
87	retry_delay_seconds: u64,
88	#[arg(default_value = "59", long)]
89	/// Number of retries. After transaction is submitted, system will try to check
90	/// if it's included in the blockchain this many times.
91	retry_count: usize,
92}
93
94impl CommonArguments {
95	/// Connects to the Ogmios server and returns a client
96	pub async fn get_ogmios_client(&self) -> crate::CmdResult<OgmiosClients> {
97		Ok(client_for_url(
98			&self.ogmios_url,
99			Duration::from_secs(self.ogmios_requests_timeout_seconds),
100		)
101		.await
102		.map_err(|e| format!("Failed to connect to Ogmios at {} with: {}", &self.ogmios_url, e))?)
103	}
104
105	/// Builds a `FixedDelayRetries` instance for retrying failed operations
106	pub fn retries(&self) -> FixedDelayRetries {
107		FixedDelayRetries::new(Duration::from_secs(self.retry_delay_seconds), self.retry_count)
108	}
109}
110
111/// Result type for commands
112type CmdResult<T> = Result<T, Box<dyn std::error::Error + Send + Sync>>;
113/// Result type for subcommands
114type SubCmdResult = Result<serde_json::Value, Box<dyn std::error::Error + Send + Sync>>;
115
116impl SmartContractsCmd {
117	/// Executes the internal command, and prints the result
118	pub async fn execute(self) -> CmdResult<()> {
119		let result: serde_json::Value = match self {
120			Self::Governance(cmd) => cmd.execute().await,
121			Self::GetScripts(cmd) => cmd.execute().await,
122			Self::UpsertDParameter(cmd) => cmd.execute().await,
123			Self::UpsertPermissionedCandidates(cmd) => cmd.execute().await,
124			Self::Register(cmd) => cmd.execute().await,
125			Self::Deregister(cmd) => cmd.execute().await,
126			Self::Reserve(cmd) => cmd.execute().await,
127			Self::AssembleAndSubmitTx(cmd) => cmd.execute().await,
128			Self::SignTx(cmd) => cmd.execute().await,
129			Self::GovernedMap(cmd) => cmd.execute().await,
130		}?;
131		println!("{}", result);
132		Ok(())
133	}
134
135	/// Executes the internal command in a blocking manner
136	pub fn execute_blocking(self) -> CmdResult<()> {
137		tokio::runtime::Runtime::new()?.block_on(self.execute())
138	}
139}
140
141/// Make a JSON object for a transaction hash. By default [McTxHash] is serialized
142/// to a JSONString.
143pub(crate) fn transaction_submitted_json(tx_hash: McTxHash) -> serde_json::Value {
144	serde_json::json!(MultiSigSmartContractResult::TransactionSubmitted(tx_hash))
145}
146
147/// Converts an optional value to a JSON object. None values are converted to an empty object.
148pub(crate) fn option_to_json<T: Serialize>(value_opt: Option<T>) -> serde_json::Value {
149	match value_opt {
150		Some(value) => serde_json::json!(value),
151		None => serde_json::json!({}),
152	}
153}
154
155#[derive(Clone, Debug, clap::Parser)]
156pub(crate) struct PaymentFilePath {
157	#[arg(long, short = 'k')]
158	/// Path to the Cardano Signing Key file used to sign transaction(s) and pay for them
159	payment_key_file: String,
160}
161
162impl PaymentFilePath {
163	/// Reads the Cardano Signing Key file from the given path and returns a [CardanoPaymentSigningKey]
164	pub(crate) fn read_key(&self) -> CmdResult<CardanoPaymentSigningKey> {
165		let key_file = CardanoKeyFileContent::parse_file(&self.payment_key_file)?;
166		Ok(CardanoPaymentSigningKey::try_from(key_file)?)
167	}
168}
169
170#[derive(Clone, Debug, clap::Parser)]
171pub(crate) struct GenesisUtxo {
172	#[arg(long, short = 'c')]
173	/// Genesis UTXO that identifies the partner chain
174	genesis_utxo: UtxoId,
175}
176
177impl From<GenesisUtxo> for UtxoId {
178	fn from(value: GenesisUtxo) -> Self {
179		value.genesis_utxo
180	}
181}
182
183#[cfg(test)]
184mod test {
185	use std::str::FromStr;
186
187	use hex_literal::hex;
188	use sidechain_domain::{
189		AuraPublicKey, CandidateKey, CandidateKeys, GrandpaPublicKey, PermissionedCandidateData,
190		SidechainPublicKey,
191	};
192
193	#[test]
194	fn parse_partnerchain_public_keys_legacy_format_without_0x_prefix() {
195		let input = "039799ff93d184146deacaa455dade51b13ed16f23cdad11d1ad6af20103391180:e85534c93315d60f808568d1dce5cb9e8ba6ed0b204209c5cc8f3bec56c10b73:cdf3e5b33f53c8b541bbaea383225c45654f24de38c585725f3cff25b2802f55";
196		assert_eq!(PermissionedCandidateData::from_str(input).unwrap(), expected_public_keys())
197	}
198
199	#[test]
200	fn parse_partnerchain_public_keys_legacy_format_with_0x_prefix() {
201		let input = "0x039799ff93d184146deacaa455dade51b13ed16f23cdad11d1ad6af20103391180:0xe85534c93315d60f808568d1dce5cb9e8ba6ed0b204209c5cc8f3bec56c10b73:0xcdf3e5b33f53c8b541bbaea383225c45654f24de38c585725f3cff25b2802f55";
202		assert_eq!(PermissionedCandidateData::from_str(input).unwrap(), expected_public_keys())
203	}
204
205	#[test]
206	fn parse_partnerchain_public_keys_generic_format_without_0x_prefix() {
207		let input = "039799ff93d184146deacaa455dade51b13ed16f23cdad11d1ad6af20103391180,aura:e85534c93315d60f808568d1dce5cb9e8ba6ed0b204209c5cc8f3bec56c10b73,gran:cdf3e5b33f53c8b541bbaea383225c45654f24de38c585725f3cff25b2802f55";
208		assert_eq!(PermissionedCandidateData::from_str(input).unwrap(), expected_public_keys())
209	}
210
211	#[test]
212	fn parse_partnerchain_public_keys_generic_format_with_0x_prefix() {
213		let input = "0x039799ff93d184146deacaa455dade51b13ed16f23cdad11d1ad6af20103391180,aura:0xe85534c93315d60f808568d1dce5cb9e8ba6ed0b204209c5cc8f3bec56c10b73,gran:0xcdf3e5b33f53c8b541bbaea383225c45654f24de38c585725f3cff25b2802f55";
214		assert_eq!(PermissionedCandidateData::from_str(input).unwrap(), expected_public_keys())
215	}
216
217	#[test]
218	fn key_id_can_contain_0x() {
219		let input = "0x0102,0xxd:0xffff";
220		assert_eq!(
221			PermissionedCandidateData::from_str(input).unwrap(),
222			PermissionedCandidateData {
223				sidechain_public_key: SidechainPublicKey([1, 2].to_vec()),
224				keys: CandidateKeys(vec![CandidateKey {
225					id: *b"0xxd",
226					bytes: [255, 255].to_vec()
227				}])
228			}
229		)
230	}
231
232	fn expected_public_keys() -> PermissionedCandidateData {
233		PermissionedCandidateData {
234			sidechain_public_key: SidechainPublicKey(
235				hex!("039799ff93d184146deacaa455dade51b13ed16f23cdad11d1ad6af20103391180").to_vec(),
236			),
237			keys: CandidateKeys(vec![
238				AuraPublicKey(
239					hex!("e85534c93315d60f808568d1dce5cb9e8ba6ed0b204209c5cc8f3bec56c10b73")
240						.to_vec(),
241				)
242				.into(),
243				GrandpaPublicKey(
244					hex!("cdf3e5b33f53c8b541bbaea383225c45654f24de38c585725f3cff25b2802f55")
245						.to_vec(),
246				)
247				.into(),
248			]),
249		}
250	}
251}