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 bridge;
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	#[command(subcommand)]
74	/// Send token to bridge contract
75	Bridge(bridge::BridgeCmd),
76}
77
78#[derive(Clone, Debug, clap::Parser)]
79#[command(author, version, about, long_about = None)]
80/// Common command arguments
81pub struct CommonArguments {
82	#[arg(default_value = "ws://localhost:1337", long, short = 'O', env)]
83	/// URL of the Ogmios server
84	ogmios_url: String,
85	#[arg(default_value = "180", long, env)]
86	/// Timeout in seconds for Ogmios requests.
87	ogmios_requests_timeout_seconds: u64,
88	#[arg(default_value = "5", long)]
89	/// Delay between retries in seconds. System will wait this long between
90	/// queries checking if transaction is included in the blockchain.
91	retry_delay_seconds: u64,
92	#[arg(default_value = "59", long)]
93	/// Number of retries. After transaction is submitted, system will try to check
94	/// if it's included in the blockchain this many times.
95	retry_count: usize,
96}
97
98impl CommonArguments {
99	/// Connects to the Ogmios server and returns a client
100	pub async fn get_ogmios_client(&self) -> crate::CmdResult<OgmiosClients> {
101		Ok(client_for_url(
102			&self.ogmios_url,
103			Duration::from_secs(self.ogmios_requests_timeout_seconds),
104		)
105		.await
106		.map_err(|e| format!("Failed to connect to Ogmios at {} with: {}", &self.ogmios_url, e))?)
107	}
108
109	/// Builds a `FixedDelayRetries` instance for retrying failed operations
110	pub fn retries(&self) -> FixedDelayRetries {
111		FixedDelayRetries::new(Duration::from_secs(self.retry_delay_seconds), self.retry_count)
112	}
113}
114
115/// Result type for commands
116type CmdResult<T> = Result<T, Box<dyn std::error::Error + Send + Sync>>;
117/// Result type for subcommands
118type SubCmdResult = Result<serde_json::Value, Box<dyn std::error::Error + Send + Sync>>;
119
120impl SmartContractsCmd {
121	/// Executes the internal command, and prints the result
122	pub async fn execute(self) -> CmdResult<()> {
123		let result: serde_json::Value = match self {
124			Self::Governance(cmd) => cmd.execute().await,
125			Self::GetScripts(cmd) => cmd.execute().await,
126			Self::UpsertDParameter(cmd) => cmd.execute().await,
127			Self::UpsertPermissionedCandidates(cmd) => cmd.execute().await,
128			Self::Register(cmd) => cmd.execute().await,
129			Self::Deregister(cmd) => cmd.execute().await,
130			Self::Reserve(cmd) => cmd.execute().await,
131			Self::AssembleAndSubmitTx(cmd) => cmd.execute().await,
132			Self::SignTx(cmd) => cmd.execute().await,
133			Self::GovernedMap(cmd) => cmd.execute().await,
134			Self::Bridge(cmd) => cmd.execute().await,
135		}?;
136		println!("{}", result);
137		Ok(())
138	}
139
140	/// Executes the internal command in a blocking manner
141	pub fn execute_blocking(self) -> CmdResult<()> {
142		tokio::runtime::Runtime::new()?.block_on(self.execute())
143	}
144}
145
146/// Make a JSON object for a transaction hash. By default [McTxHash] is serialized
147/// to a JSONString.
148pub(crate) fn transaction_submitted_json(tx_hash: McTxHash) -> serde_json::Value {
149	serde_json::json!(MultiSigSmartContractResult::TransactionSubmitted(tx_hash))
150}
151
152/// Converts an optional value to a JSON object. None values are converted to an empty object.
153pub(crate) fn option_to_json<T: Serialize>(value_opt: Option<T>) -> serde_json::Value {
154	match value_opt {
155		Some(value) => serde_json::json!(value),
156		None => serde_json::json!({}),
157	}
158}
159
160#[derive(Clone, Debug, clap::Parser)]
161pub(crate) struct PaymentFilePath {
162	#[arg(long, short = 'k')]
163	/// Path to the Cardano Signing Key file used to sign transaction(s) and pay for them
164	payment_key_file: String,
165}
166
167impl PaymentFilePath {
168	/// Reads the Cardano Signing Key file from the given path and returns a [CardanoPaymentSigningKey]
169	pub(crate) fn read_key(&self) -> CmdResult<CardanoPaymentSigningKey> {
170		let key_file = CardanoKeyFileContent::parse_file(&self.payment_key_file)?;
171		Ok(CardanoPaymentSigningKey::try_from(key_file)?)
172	}
173}
174
175#[derive(Clone, Debug, clap::Parser)]
176pub(crate) struct GenesisUtxo {
177	#[arg(long, short = 'c')]
178	/// Genesis UTXO that identifies the partner chain
179	genesis_utxo: UtxoId,
180}
181
182impl From<GenesisUtxo> for UtxoId {
183	fn from(value: GenesisUtxo) -> Self {
184		value.genesis_utxo
185	}
186}
187
188#[cfg(test)]
189mod test {
190	use std::str::FromStr;
191
192	use hex_literal::hex;
193	use sidechain_domain::{
194		AuraPublicKey, CandidateKey, CandidateKeys, GrandpaPublicKey, PermissionedCandidateData,
195		SidechainPublicKey,
196	};
197
198	#[test]
199	fn parse_partnerchain_public_keys_legacy_format_without_0x_prefix() {
200		let input = "039799ff93d184146deacaa455dade51b13ed16f23cdad11d1ad6af20103391180:e85534c93315d60f808568d1dce5cb9e8ba6ed0b204209c5cc8f3bec56c10b73:cdf3e5b33f53c8b541bbaea383225c45654f24de38c585725f3cff25b2802f55";
201		assert_eq!(PermissionedCandidateData::from_str(input).unwrap(), expected_public_keys())
202	}
203
204	#[test]
205	fn parse_partnerchain_public_keys_legacy_format_with_0x_prefix() {
206		let input = "0x039799ff93d184146deacaa455dade51b13ed16f23cdad11d1ad6af20103391180:0xe85534c93315d60f808568d1dce5cb9e8ba6ed0b204209c5cc8f3bec56c10b73:0xcdf3e5b33f53c8b541bbaea383225c45654f24de38c585725f3cff25b2802f55";
207		assert_eq!(PermissionedCandidateData::from_str(input).unwrap(), expected_public_keys())
208	}
209
210	#[test]
211	fn parse_partnerchain_public_keys_generic_format_without_0x_prefix() {
212		let input = "039799ff93d184146deacaa455dade51b13ed16f23cdad11d1ad6af20103391180,aura:e85534c93315d60f808568d1dce5cb9e8ba6ed0b204209c5cc8f3bec56c10b73,gran:cdf3e5b33f53c8b541bbaea383225c45654f24de38c585725f3cff25b2802f55";
213		assert_eq!(PermissionedCandidateData::from_str(input).unwrap(), expected_public_keys())
214	}
215
216	#[test]
217	fn parse_partnerchain_public_keys_generic_format_with_0x_prefix() {
218		let input = "0x039799ff93d184146deacaa455dade51b13ed16f23cdad11d1ad6af20103391180,aura:0xe85534c93315d60f808568d1dce5cb9e8ba6ed0b204209c5cc8f3bec56c10b73,gran:0xcdf3e5b33f53c8b541bbaea383225c45654f24de38c585725f3cff25b2802f55";
219		assert_eq!(PermissionedCandidateData::from_str(input).unwrap(), expected_public_keys())
220	}
221
222	#[test]
223	fn key_id_can_contain_0x() {
224		let input = "0x0102,0xxd:0xffff";
225		assert_eq!(
226			PermissionedCandidateData::from_str(input).unwrap(),
227			PermissionedCandidateData {
228				sidechain_public_key: SidechainPublicKey([1, 2].to_vec()),
229				keys: CandidateKeys(vec![CandidateKey {
230					id: *b"0xxd",
231					bytes: [255, 255].to_vec()
232				}])
233			}
234		)
235	}
236
237	fn expected_public_keys() -> PermissionedCandidateData {
238		PermissionedCandidateData {
239			sidechain_public_key: SidechainPublicKey(
240				hex!("039799ff93d184146deacaa455dade51b13ed16f23cdad11d1ad6af20103391180").to_vec(),
241			),
242			keys: CandidateKeys(vec![
243				AuraPublicKey(
244					hex!("e85534c93315d60f808568d1dce5cb9e8ba6ed0b204209c5cc8f3bec56c10b73")
245						.to_vec(),
246				)
247				.into(),
248				GrandpaPublicKey(
249					hex!("cdf3e5b33f53c8b541bbaea383225c45654f24de38c585725f3cff25b2802f55")
250						.to_vec(),
251				)
252				.into(),
253			]),
254		}
255	}
256}