From bc6516ed81723d58c4105cbf9fe965d35716d09b Mon Sep 17 00:00:00 2001 From: wiesche Date: Sat, 5 Sep 2026 11:26:24 +0200 Subject: [PATCH] add contract outfile --- controller/src/command.rs | 14 +++++------ controller/tests/contract_revoke.rs | 31 ++++++++++-------------- doc/contracts.md | 3 ++- libwallet/src/contract/actions/README.md | 1 - src/bin/grin-wallet.yml | 15 ++++++++++++ src/cmd/wallet_args.rs | 11 +++++---- tests/cmd_line_contract.rs | 28 ++++++++++++++++++++- 7 files changed, 70 insertions(+), 33 deletions(-) diff --git a/controller/src/command.rs b/controller/src/command.rs index baa5828..8fed82a 100644 --- a/controller/src/command.rs +++ b/controller/src/command.rs @@ -1694,12 +1694,10 @@ pub struct ContractNewArgs { pub ttl_blocks: Option, /// Fee rate in nanogrin per unit of transaction weight pub fee_rate: Option, + /// Override the output Slatepack file + pub outfile: Option, /// Select and lock outputs early pub add_outputs: bool, - - // Future features - /// Save slatepack to a specific filename - pub outfile: Option, } impl ContractNewArgs { @@ -1799,12 +1797,12 @@ pub struct ContractSetupArgs { pub minimum_confirmations: Option, /// Fee rate in nanogrin per unit of transaction weight pub fee_rate: Option, + /// Override the output Slatepack file + pub outfile: Option, // Future features /// Whether we should automatically sign a receive of any value // pub auto_receive: Option, - /// Save slatepack to a specific filename - pub outfile: Option, /// Add outputs pub add_outputs: bool, // lock outputs early } @@ -1937,6 +1935,8 @@ where pub struct ContractRevokeArgs { /// Id of a transaction we want to cancel pub tx_id: u32, + /// Override the output Slatepack file + pub outfile: Option, } pub fn contract_revoke( @@ -1961,7 +1961,7 @@ where )?; if let Some(slate) = slate_opt { // A revoke has no counterparty, so write the replacement as plaintext. - let slate_out = prepare_slatepack(api, keychain_mask, &slate, None, None)?; + let slate_out = prepare_slatepack(api, keychain_mask, &slate, None, args.outfile)?; println!("{}", slate_out); } else { println!("Contract revoked. No replacement transaction was created."); diff --git a/controller/tests/contract_revoke.rs b/controller/tests/contract_revoke.rs index 3bdb3be..8f6b50a 100644 --- a/controller/tests/contract_revoke.rs +++ b/controller/tests/contract_revoke.rs @@ -14,6 +14,7 @@ //! Test contract revoke, including when a different account is active than the //! one that contributed (and locked) the inputs. // #[macro_use] +extern crate grin_wallet_api as api; extern crate grin_wallet_controller as wallet; extern crate grin_wallet_impls as impls; extern crate log; @@ -29,7 +30,7 @@ use std::time::Duration; #[macro_use] mod common; use common::{clean_output_dir, create_wallets, setup}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; /// Revoke the transaction in the requested account when two accounts share a transaction id. fn contract_revoke_other_account_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { @@ -202,7 +203,7 @@ fn wallet_contract_revoke_other_account() -> Result<(), libwallet::Error> { /// built yet) must be resumable: a second revoke detects the now-Unspent inputs that are /// still tagged with the cancelled tx and still produces the self-spend, rather than /// silently doing nothing. -fn contract_revoke_resume_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { +fn contract_revoke_resume_impl(test_dir: &'static str) -> Result<(), wallet::Error> { let (wallets, _chain, stopper, _bh) = create_wallets(vec![vec![("default", 4)]], test_dir).unwrap(); let wallet1 = wallets[0].0.clone(); @@ -261,25 +262,19 @@ fn contract_revoke_resume_impl(test_dir: &'static str) -> Result<(), libwallet:: }, )?; - // Resume: revoke must still produce the self-spend from the now-Unspent input. - let mut revoked = None; - wallet::controller::owner_single_use( - wallet1.clone(), + // Resume through the command path and write the self-spend to the requested file. + let outfile = format!("{}/revoke.slatepack", test_dir); + let mut owner = api::Owner::new(wallet1.clone(), None, PathBuf::from(test_dir)); + wallet::command::contract_revoke( + &mut owner, mask1, - PathBuf::from(test_dir), - |api, m| { - revoked = api.contract_revoke( - m, - &ContractRevokeArgsAPI { - tx_id, - src_acct_name: None, - }, - )?; - Ok(()) + wallet::command::ContractRevokeArgs { + tx_id, + outfile: Some(outfile.clone()), }, )?; assert!( - revoked.is_some(), + Path::new(&outfile).is_file(), "interrupted revoke should resume and produce a self-spend slate" ); @@ -289,7 +284,7 @@ fn contract_revoke_resume_impl(test_dir: &'static str) -> Result<(), libwallet:: } #[test] -fn wallet_contract_revoke_resume() -> Result<(), libwallet::Error> { +fn wallet_contract_revoke_resume() -> Result<(), wallet::Error> { let test_dir = "test_output/contract_revoke_resume"; setup(test_dir); contract_revoke_resume_impl(test_dir)?; diff --git a/doc/contracts.md b/doc/contracts.md index 8f3a864..0723da5 100644 --- a/doc/contracts.md +++ b/doc/contracts.md @@ -13,7 +13,8 @@ API does not require manual confirmation, so a wallet using it still needs to sh transaction and ask the user before signing. The CLI exchanges Slatepacks. The last `sign` broadcasts the transaction unless -`--no-broadcast` is used. +`--no-broadcast` is used. `--outfile` overrides the output Slatepack file for `new`, `sign` +and `revoke`. ## Amounts and fees diff --git a/libwallet/src/contract/actions/README.md b/libwallet/src/contract/actions/README.md index 4497392..b2862bc 100644 --- a/libwallet/src/contract/actions/README.md +++ b/libwallet/src/contract/actions/README.md @@ -41,7 +41,6 @@ Separating side effects until the 'save_step' part would make these functions mu clearer to callers. - For a payjoin, `--use-inputs any` picks an input for us; naming the commitment would be clearer, for the API as much as the CLI. - - Support `--outfile` for contract slatepacks. - Raise the replacement fee when `revoke` races a transaction already in the mempool. - Merge invoice-proof retrieval with the existing payment-proof API. - Add a Grin node API for kernel lookup by MMR index, then use the witness index instead diff --git a/src/bin/grin-wallet.yml b/src/bin/grin-wallet.yml index f54cd9a..a7b6248 100644 --- a/src/bin/grin-wallet.yml +++ b/src/bin/grin-wallet.yml @@ -512,6 +512,11 @@ subcommands: short: b long: ttl_blocks takes_value: true + - outfile: + help: If present, overrides the filename and location of the output Slatepack file. + short: u + long: outfile + takes_value: true - sign: about: Sign a contract args: @@ -559,6 +564,11 @@ subcommands: help: Fee rate to use when first joining, in nanogrin per unit of transaction weight (defaults to accept_fee_base) long: fee_rate takes_value: true + - outfile: + help: If present, overrides the filename and location of the output Slatepack file. + short: u + long: outfile + takes_value: true - no-broadcast: help: Don't broadcast the transaction if it was finalized long: no-broadcast @@ -579,3 +589,8 @@ subcommands: short: i long: tx-id takes_value: true + - outfile: + help: If present, overrides the filename and location of the output Slatepack file. + short: u + long: outfile + takes_value: true diff --git a/src/cmd/wallet_args.rs b/src/cmd/wallet_args.rs index 7ae4c46..996fef5 100644 --- a/src/cmd/wallet_args.rs +++ b/src/cmd/wallet_args.rs @@ -1048,6 +1048,7 @@ pub fn parse_contract_new_args( "minimum_confirmations", )?; let fee_rate = parse_contract_fee_rate(args)?; + let outfile = parse_optional(args, "outfile")?; let ttl_blocks = match args.value_of("ttl_blocks") { Some(value) => { let blocks = parse_u64(value, "ttl_blocks")?; @@ -1117,8 +1118,7 @@ pub fn parse_contract_new_args( minimum_confirmations, fee_rate, ttl_blocks, - // Not yet wired to CLI flags: - outfile: None, + outfile, }) } @@ -1149,6 +1149,7 @@ pub fn parse_contract_setup_args( None => None, }; let fee_rate = parse_contract_fee_rate(args)?; + let outfile = parse_optional(args, "outfile")?; let no_payjoin = args.is_present("no-payjoin"); let use_inputs = match args.value_of("use-inputs") { Some(v) => { @@ -1192,8 +1193,7 @@ pub fn parse_contract_setup_args( make_outputs: make_outputs, minimum_confirmations, fee_rate, - // Not yet wired to CLI flags: - outfile: None, + outfile, }) } @@ -1233,8 +1233,9 @@ pub fn parse_contract_revoke_args( let tx_id = tx_id_str.parse::().map_err(|e| { ParseError::ArgumentError(format!("Could not parse tx-id '{}'. e={}", tx_id_str, e)) })?; + let outfile = parse_optional(args, "outfile")?; - Ok(command::ContractRevokeArgs { tx_id: tx_id }) + Ok(command::ContractRevokeArgs { tx_id, outfile }) } pub fn contract_json_output(args: &ArgMatches<'_>) -> bool { diff --git a/tests/cmd_line_contract.rs b/tests/cmd_line_contract.rs index 5fbcf82..be7c5cf 100644 --- a/tests/cmd_line_contract.rs +++ b/tests/cmd_line_contract.rs @@ -107,6 +107,7 @@ fn contract_command_test_impl(test_dir: &str) -> Result<(), grin_wallet_controll let expected_fee = Transaction::weight_by_iok(1, 1, 0) * fee_rate + (Transaction::weight_by_iok(0, 0, 1) * fee_rate).div_ceil(2); let fee_rate = fee_rate.to_string(); + let file_name = format!("{}/wallet1/contract.slatepack", test_dir); // Wallet 1 opens a contract sending 1 grin, leaving an unencrypted slatepack because // no --encrypt-for was given @@ -122,11 +123,13 @@ fn contract_command_test_impl(test_dir: &str) -> Result<(), grin_wallet_controll "1", "--fee_rate", &fee_rate, + "--outfile", + &file_name, ]; execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + assert!(std::path::Path::new(&file_name).is_file()); // Wallet 2 views the contract before signing it - let file_name = only_slatepack(test_dir, "wallet1"); grin_wallet_controller::controller::owner_single_use( wallet1.clone(), mask1, @@ -284,6 +287,8 @@ fn parses_contract_options() { "2", "--ttl_blocks", "20", + "--outfile", + "new.slatepack", ]) .unwrap(); let contract = args.subcommand_matches("contract").unwrap(); @@ -293,6 +298,7 @@ fn parses_contract_options() { assert_eq!(parsed.minimum_confirmations, 3); assert_eq!(parsed.fee_rate, Some(2)); assert_eq!(parsed.ttl_blocks, Some(20)); + assert_eq!(parsed.outfile.as_deref(), Some("new.slatepack")); let expected = grin_wallet_libwallet::contract::types::DEFAULT_MINIMUM_CONFIRMATIONS; let args = app @@ -363,6 +369,7 @@ fn parses_contract_options() { assert_eq!(parsed.fee_rate, None); let args = app + .clone() .get_matches_from_safe(vec![ "grin-wallet", "contract", @@ -371,6 +378,8 @@ fn parses_contract_options() { "3", "--fee_rate", "2", + "--outfile", + "sign.slatepack", ]) .unwrap(); let contract = args.subcommand_matches("contract").unwrap(); @@ -378,6 +387,23 @@ fn parses_contract_options() { let parsed = grin_wallet::cmd::wallet_args::parse_contract_setup_args(sign_args).unwrap(); assert_eq!(parsed.minimum_confirmations, Some(3)); assert_eq!(parsed.fee_rate, Some(2)); + assert_eq!(parsed.outfile.as_deref(), Some("sign.slatepack")); + + let args = app + .get_matches_from_safe(vec![ + "grin-wallet", + "contract", + "revoke", + "--tx-id", + "1", + "--outfile", + "revoke.slatepack", + ]) + .unwrap(); + let contract = args.subcommand_matches("contract").unwrap(); + let revoke_args = contract.subcommand_matches("revoke").unwrap(); + let parsed = grin_wallet::cmd::wallet_args::parse_contract_revoke_args(revoke_args).unwrap(); + assert_eq!(parsed.outfile.as_deref(), Some("revoke.slatepack")); } #[test]