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