diff --git a/changelog.md b/changelog.md index 633c567994..9026dc5d1a 100644 --- a/changelog.md +++ b/changelog.md @@ -82,6 +82,8 @@ Partner Chain separately for use in `pallet_session_validator_management` and other pallets that use cross-chain keys. * `sp_sidechain::GetEpochDurationApi` runtime API * Reusable migration `AuthorityKeysMigration` in `pallet_session_validator_management` +* Added `--out-file` parameter to governed-map commands (`insert`, `update`, `remove`) to save transaction CBOR hex directly to a file, simplifying multisig workflows +* Enhanced `sign-tx` command to accept multiple input formats: file path to JSON, JSON string, or direct CBOR hex, improving flexibility in multisig transaction workflows # v1.8.0 diff --git a/docs/intro.md b/docs/intro.md index 15ca26a76a..7e167e8b4f 100644 --- a/docs/intro.md +++ b/docs/intro.md @@ -983,12 +983,16 @@ $ pc-node sign-tx --payment-key-file ``` -`TRANSACTION` should be a CBOR-encoded transaction. The command will output a CBOR-encoded -witness that can be passed to `assemble-and-submit-tx`. +`TRANSACTION` can be: +- A file path to a JSON file containing the transaction +- A JSON string with the transaction +- A CBOR-encoded transaction hex string + +The command will output a CBOR-encoded witness that can be passed to `assemble-and-submit-tx`. ##### governed-map -Set of subcommands for managing the Governed Map key-value store on Cardano +Set of subcommands for managing the Governed Map key-value store on Cardano. When multisig governance is configured, these commands support an optional `--out-file` parameter to save the transaction CBOR hex directly to a file for easier sharing and signing. ###### insert @@ -1000,9 +1004,10 @@ $ pc-node governed-map insert --value --payment-key-file --genesis-utxo + [ --out-file ] ``` -If the value for the key already exists it won't be updated. +If the value for the key already exists it won't be updated. The optional `--out-file` parameter saves the transaction CBOR hex to the specified file when multisig governance is in use. ###### update @@ -1015,11 +1020,12 @@ $ pc-node governed-map update --payment-key-file --genesis-utxo [ --current-value ] + [ --out-file ] ``` If the key does not already exist it won't be inserted. If the optional `--current-value` argument is passed, the transaction will fail if the current -value doesn't match the argument. +value doesn't match the argument. The optional `--out-file` parameter saves the transaction CBOR hex to the specified file when multisig governance is in use. ###### remove @@ -1030,8 +1036,11 @@ $ pc-node governed-map remove --key --payment-key-file --genesis-utxo + [ --out-file ] ``` +The optional `--out-file` parameter saves the transaction CBOR hex to the specified file when multisig governance is in use. + ###### list Lists all key-value pairs currently stored in the Governed Map. diff --git a/docs/user-guides/governance/governance.md b/docs/user-guides/governance/governance.md index 522b86bfb6..823ff05823 100644 --- a/docs/user-guides/governance/governance.md +++ b/docs/user-guides/governance/governance.md @@ -79,7 +79,9 @@ In version v1.4 this functionality is available in the smart contracts CLI appli ## Multi Signature Governance -All the `smart-contracts` sub-commands that require Governance: `governance update`, `upsert-d-parameter`, `upsert-permissioned-candidates`, `reserve init|create|deposit|handover|update-settings`, and `governed-map insert|update|remove` will now submit the transaction only if the governance is "1 of 1". Otherwise these commands return a transaction CBOR that can be submitted with the new command `assemble-and-submit-tx`. Signatures can be obtained using `sign-tx`. Example of executed commands, invoked by owners of `key1` and `key2` are: +All the `smart-contracts` sub-commands that require Governance: `governance update`, `upsert-d-parameter`, `upsert-permissioned-candidates`, `reserve init|create|deposit|handover|update-settings`, and `governed-map insert|update|remove` will now submit the transaction only if the governance is "1 of 1". Otherwise these commands return a transaction CBOR that can be submitted with the new command `assemble-and-submit-tx`. Signatures can be obtained using `sign-tx`. + +For convenience, governance commands support an optional `--out-file` parameter that saves the transaction CBOR hex directly to a file, eliminating the need for manual extraction using tools like `jq`. Example of executed commands, invoked by owners of `key1` and `key2` are: Owner of `key1` initialized Governance with two key hashes `e8c300330fe315531ca89d4a2e7d0c80211bc70b473b1ed4979dff2b` and `7fa48bb8fb5d6804fad26237738ce490d849e4567161e38ab8415ff3`(that are hashes of `key1` and `key2`), and sets requried number of signatures to `2`. ``` @@ -100,15 +102,17 @@ Owner of `key1` wants to set D-parameter to (7, 5). -c f8fbe7316561e57de9ecd1c86ee8f8b512a314ba86499ba9a584bfa8fe2edc8d#0 \ --permissioned-candidates-count 7 \ --registered-candidates-count 5 \ --k key1.skey | jq . +-k key1.skey \ +--out-file tx.cbor ... +✓ Wrote transaction CBOR hex to: tx.cbor {"transaction_to_sign":{"temporary_wallet":{"address":"addr_test1vzeg2g6gcnlvnemk9hgvsaxxktf8suxwd63hm54w9erxuwc49exyq","funded_by_tx":"0xed99c5eb6d12053c514915fcb0445c9ce9839b65570db042fcd1c9d9cc9fbcf8","private_key":"0x730f9c6f26666da41dedbe596f6b2f7d36a98ce768591010b537e4f48417448f"},"tx":{"cborHex":"84aa00..transaction bytes redacted..f6","description":"","type":"Tx ConwayEra"},"tx_name":"Insert D-parameter"}} ``` -The user gets the transaction data. It already contains the `key1` signature. Transaction requires signature of `key2` owner, before it can be submitted. +The user gets the transaction data. It already contains the `key1` signature. The transaction CBOR hex has been saved to `tx.cbor`. Transaction requires signature of `key2` owner, before it can be submitted. -`key2` owner has to get this transaction data, perhaps from `key1` owner, *inspect the transaction* and then sign `cborHex` value from the previous output: +`key2` owner has to get this transaction data, perhaps from `key1` owner, *inspect the transaction* and then sign it. The `sign-tx` command can accept the CBOR hex as a file path, JSON string, or direct hex: ``` -./partner-chains-node smart-contracts sign-tx -k key2.skey --transaction 84aa00....f6 +./partner-chains-node smart-contracts sign-tx -k key2.skey --transaction tx.cbor ... {"cborHex":"82008258202bebcb7fbc74a6e0fd6e00a311698b047b7b659f0e047ff5349dbd984aefc52c58409dfff5d837ec7b864502c7acac5ad5885f74d94cb68458413ee4565ff52f6dcb1ff3df272566662b4f00766fc9586a12532bfce68e56280f93dd57d6e22b9705","description":"","type":"TxWitness ConwayEra"} ``` diff --git a/toolkit/smart-contracts/commands/src/governed_map.rs b/toolkit/smart-contracts/commands/src/governed_map.rs index c6896f96a7..bd42df0941 100644 --- a/toolkit/smart-contracts/commands/src/governed_map.rs +++ b/toolkit/smart-contracts/commands/src/governed_map.rs @@ -5,6 +5,7 @@ use partner_chains_cardano_offchain::governed_map::{ use serde_json::json; use sidechain_domain::byte_string::ByteString; use std::collections::HashMap; +use std::path::PathBuf; #[derive(Clone, Debug, clap::Subcommand)] #[allow(clippy::large_enum_variant)] @@ -55,6 +56,9 @@ pub struct InsertCmd { #[clap(flatten)] /// Genesis UTXO genesis_utxo: GenesisUtxo, + #[arg(long, value_hint = clap::ValueHint::FilePath)] + /// Optional file path to save the transaction CBOR hex (for multisig flow) + out_file: Option, } impl InsertCmd { @@ -73,7 +77,7 @@ impl InsertCmd { &self.common_arguments.retries(), ) .await; - print_result_json(result) + print_result_json(result, self.out_file) } } @@ -97,6 +101,9 @@ pub struct UpdateCmd { #[clap(flatten)] /// Genesis UTXO genesis_utxo: GenesisUtxo, + #[arg(long, value_hint = clap::ValueHint::FilePath)] + /// Optional file path to save the transaction CBOR hex (for multisig flow) + out_file: Option, } impl UpdateCmd { @@ -116,7 +123,7 @@ impl UpdateCmd { &self.common_arguments.retries(), ) .await; - print_result_json(result) + print_result_json(result, self.out_file) } } @@ -134,6 +141,9 @@ pub struct RemoveCmd { #[clap(flatten)] /// Genesis UTXO genesis_utxo: GenesisUtxo, + #[arg(long, value_hint = clap::ValueHint::FilePath)] + /// Optional file path to save the transaction CBOR hex (for multisig flow) + out_file: Option, } impl RemoveCmd { @@ -151,7 +161,7 @@ impl RemoveCmd { &self.common_arguments.retries(), ) .await; - print_result_json(result) + print_result_json(result, self.out_file) } } @@ -205,12 +215,34 @@ impl GetCmd { } /// Converts the result of a command into a JSON object. +/// If out_file is provided and the result is a TransactionToSign, saves the cborHex to the file. fn print_result_json( result: anyhow::Result>, + out_file: Option, ) -> crate::SubCmdResult { match result { Err(err) => Err(err)?, - Ok(Some(res)) => Ok(json!(res)), + Ok(Some(res)) => { + // If out_file is specified and this is a multisig transaction, save cborHex + if let (Some(path), crate::MultiSigSmartContractResult::TransactionToSign(tx_data)) = + (&out_file, &res) + { + // Extract cborHex from the serialized transaction + let tx_json = serde_json::to_value(&tx_data.tx)?; + if let Some(cbor_hex) = tx_json.get("cborHex").and_then(|v| v.as_str()) { + std::fs::write(path, cbor_hex).map_err(|e| { + anyhow::anyhow!("Failed to write CBOR to file {}: {}", path.display(), e) + })?; + eprintln!("✓ Wrote transaction CBOR hex to: {}", path.display()); + } else { + return Err(anyhow::anyhow!( + "--out-file specified but could not extract cborHex from transaction" + ) + .into()); + } + } + Ok(json!(res)) + }, Ok(None) => Ok(json!({})), } } diff --git a/toolkit/smart-contracts/commands/src/sign_tx.rs b/toolkit/smart-contracts/commands/src/sign_tx.rs index faf32cee45..55730446a0 100644 --- a/toolkit/smart-contracts/commands/src/sign_tx.rs +++ b/toolkit/smart-contracts/commands/src/sign_tx.rs @@ -6,9 +6,12 @@ use sidechain_domain::TransactionCbor; #[derive(Clone, Debug, clap::Parser)] /// Command for signing a cardano transaction pub struct SignTxCmd { - #[arg(long)] - /// Hex-encoded transaction CBOR (with or without 0x prefix) - transaction: TransactionCbor, + #[arg( + long, + value_hint = clap::ValueHint::AnyPath, + help = "Transaction input: hex string, JSON with transaction_to_sign, or file path to JSON" + )] + transaction: String, #[clap(flatten)] /// Path to the Cardano Signing Key file that you want to sign the transaction with payment_key_file: PaymentFilePath, @@ -19,7 +22,13 @@ impl SignTxCmd { pub async fn execute(self) -> crate::SubCmdResult { let payment_key = self.payment_key_file.read_key()?; - let vkey_witness = sign_tx(self.transaction.0, &payment_key)?; + // Try to extract cborHex from the input + let cbor_hex = extract_cbor_hex(&self.transaction)?; + let transaction_cbor: TransactionCbor = cbor_hex + .parse() + .map_err(|e| anyhow::anyhow!("Failed to parse transaction CBOR: {}", e))?; + + let vkey_witness = sign_tx(transaction_cbor.0, &payment_key)?; let json = json!( { @@ -31,3 +40,49 @@ impl SignTxCmd { Ok(json) } } + +/// Extracts cborHex from the input string. +/// Uses deterministic order: file path -> JSON parsing -> hex string +fn extract_cbor_hex(input: &str) -> Result> { + let trimmed = input.trim(); + + // Case 1: Check if it's a file path that exists + let json_str = if std::path::Path::new(trimmed).exists() { + std::fs::read_to_string(trimmed) + .map_err(|e| format!("Failed to read file '{}': {}", trimmed, e))? + } else if let Ok(json_value) = serde_json::from_str::(trimmed) { + // Case 2: Try parsing as JSON directly + return extract_cbor_from_json(&json_value); + } else { + // Case 3: Treat as direct hex string + return Ok(trimmed.to_string()); + }; + + // If we read from file, parse the JSON and extract cborHex + let json_value: serde_json::Value = serde_json::from_str(&json_str) + .map_err(|e| format!("Failed to parse JSON from file: {}", e))?; + + extract_cbor_from_json(&json_value) +} + +/// Extracts cborHex from a JSON value +fn extract_cbor_from_json( + json_value: &serde_json::Value, +) -> Result> { + // Try to extract cborHex from transaction_to_sign format + if let Some(cbor_hex) = json_value + .get("transaction_to_sign") + .and_then(|v| v.get("tx")) + .and_then(|v| v.get("cborHex")) + .and_then(|v| v.as_str()) + { + return Ok(cbor_hex.to_string()); + } + + // Try to extract cborHex from direct tx format + if let Some(cbor_hex) = json_value.get("cborHex").and_then(|v| v.as_str()) { + return Ok(cbor_hex.to_string()); + } + + Err("Could not extract cborHex from JSON. Expected 'transaction_to_sign.tx.cborHex' or 'cborHex' field.".into()) +}