diff --git a/api/src/owner.rs b/api/src/owner.rs index ec7a9e1..cf333f5 100644 --- a/api/src/owner.rs +++ b/api/src/owner.rs @@ -25,7 +25,7 @@ use crate::libwallet::api_impl::types::update_tx_slate_state; use crate::libwallet::api_impl::{owner, owner_updater}; use crate::libwallet::contract::proofs::InvoiceProof; use crate::libwallet::contract::types::{ - ContractNewArgsAPI, ContractRevokeArgsAPI, ContractSetupArgsAPI, + ContractNewArgsAPI, ContractRevokeArgsAPI, ContractSetupArgsAPI, ContractView, }; use crate::libwallet::{ AcctPathMapping, BuiltOutput, Error, InitTxArgs, IssueInvoiceTxArgs, NodeClient, @@ -815,6 +815,17 @@ where owner::contract_new(w, keychain_mask, args) } + /// Summarise a contract slate, including its participants, signatures and net change. + pub fn contract_view( + &self, + keychain_mask: Option<&SecretKey>, + slate: &Slate, + ) -> Result { + let mut w_lock = self.wallet_inst.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + owner::contract_view(w, keychain_mask, slate) + } + /// Sign a contract, running setup first if it has not been done yet. pub fn contract_sign( &self, diff --git a/api/src/owner_rpc.rs b/api/src/owner_rpc.rs index 288d38e..461a016 100644 --- a/api/src/owner_rpc.rs +++ b/api/src/owner_rpc.rs @@ -22,7 +22,7 @@ use crate::core::core::OutputFeatures; use crate::core::global; use crate::keychain::{Identifier, Keychain}; use crate::libwallet::contract::types::{ - ContractNewArgsAPI, ContractRevokeArgsAPI, ContractSetupArgsAPI, + ContractNewArgsAPI, ContractRevokeArgsAPI, ContractSetupArgsAPI, ContractView, }; use crate::libwallet::{ mwixnet::MixnetReqCreationParams, AcctPathMapping, Amount, BuiltOutput, Error, InitTxArgs, @@ -547,6 +547,12 @@ pub trait OwnerRpc { TODO: Full docs once API has stabilised */ + fn contract_view(&self, token: Token, slate: VersionedSlate) -> Result; + + /** + TODO: Full docs once API has stabilised + */ + fn contract_revoke( &self, token: Token, @@ -2187,6 +2193,10 @@ where VersionedSlate::into_version(slate, version) } + fn contract_view(&self, token: Token, slate: VersionedSlate) -> Result { + Owner::contract_view(self, (&token.keychain_mask).as_ref(), &Slate::from(slate)) + } + fn contract_revoke( &self, token: Token, diff --git a/controller/src/command.rs b/controller/src/command.rs index ac65b3b..ca98c1e 100644 --- a/controller/src/command.rs +++ b/controller/src/command.rs @@ -1814,6 +1814,42 @@ where Ok(()) } +#[derive(Clone)] +pub struct ContractViewArgs { + /// Slatepack file to read the contract from + pub input_file: Option, + /// Slatepack message to read the contract from + pub input_slatepack_message: Option, +} + +pub fn contract_view( + owner_api: &mut Owner, + keychain_mask: Option<&SecretKey>, + args: ContractViewArgs, +) -> Result<(), Error> +where + L: WalletLCProvider<'static, C, K>, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + let (slate, _) = parse_slatepack( + owner_api, + keychain_mask, + args.input_file, + args.input_slatepack_message, + )?; + + let wallet_inst = owner_api.wallet_inst.clone(); + let config_path = owner_api.config_path(); + controller::owner_single_use(wallet_inst, keychain_mask, config_path, |api, m| { + let view = api.contract_view(m, &slate)?; + display::contract_view(&slate, &view); + Ok(()) + })?; + + Ok(()) +} + #[derive(Clone)] pub struct ContractRevokeArgs { /// Id of a transaction we want to cancel diff --git a/controller/src/display.rs b/controller/src/display.rs index e5f2f88..05e2ce0 100644 --- a/controller/src/display.rs +++ b/controller/src/display.rs @@ -15,8 +15,10 @@ use crate::core::core::FeeFields; use crate::core::core::{self, amount_to_hr_string}; use crate::core::global; +use crate::libwallet::contract::types::ContractView; use crate::libwallet::{ - AcctPathMapping, Error, OutputCommitMapping, OutputStatus, TxLogEntry, ViewWallet, WalletInfo, + AcctPathMapping, Error, OutputCommitMapping, OutputStatus, Slate, TxLogEntry, ViewWallet, + WalletInfo, }; use crate::util::ToHex; use grin_wallet_util::OnionV3Address; @@ -634,3 +636,29 @@ pub fn payment_proof(tx: &TxLogEntry) -> Result<(), Error> { Ok(()) } + +/// Display a summary of a contract slate +pub fn contract_view(slate: &Slate, view: &ContractView) { + println!("\n____ Contract ____\n"); + let mut table = table!(); + + table.add_row(row![bFC->"Slate Id", bGC->slate.id]); + table.add_row(row![bFC->"State", bGC->slate.state]); + table.add_row(row![bFC->"Participants", bGC->view.num_participants]); + table.add_row(row![bFC->"Signatures", bGC->view.num_sigs]); + let suggested = match view.suggested_net_change { + Some(v) => amount_to_hr_string(v.unsigned_abs(), false), + None => String::from("None"), + }; + table.add_row(row![bFC->"Suggested Net Change", bGC->suggested]); + let agreed = match view.agreed_net_change { + Some(v) => amount_to_hr_string(v.unsigned_abs(), false), + None => String::from("None"), + }; + table.add_row(row![bFC->"Agreed Net Change", bGC->agreed]); + table.add_row(row![bFC->"Executed", bGC->view.is_executed]); + + table.set_format(*prettytable::format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); + table.printstd(); + println!(); +} diff --git a/libwallet/src/api_impl/owner.rs b/libwallet/src/api_impl/owner.rs index 7700891..0241de2 100644 --- a/libwallet/src/api_impl/owner.rs +++ b/libwallet/src/api_impl/owner.rs @@ -29,7 +29,9 @@ use crate::grin_util::{Mutex, ToHex}; use crate::util::OnionV3Address; use crate::api_impl::owner_updater::StatusMessage; -use crate::contract::types::{ContractNewArgsAPI, ContractRevokeArgsAPI, ContractSetupArgsAPI}; +use crate::contract::types::{ + ContractNewArgsAPI, ContractRevokeArgsAPI, ContractSetupArgsAPI, ContractView, +}; use crate::grin_keychain::{BlindingFactor, Identifier, Keychain, SwitchCommitmentType}; use crate::internal::{keys, scan, selection, tx, updater}; @@ -1514,6 +1516,20 @@ where contract::new(&mut *w, keychain_mask, &args.setup_args, None) } +/// View a transaction contract +pub fn contract_view( + w: &mut WalletBackend, + keychain_mask: Option<&SecretKey>, + slate: &Slate, +) -> Result +where + C: NodeClient, + K: Keychain, +{ + let mut slate = slate.clone(); + contract::view(&mut *w, keychain_mask, &mut slate, "") +} + /// Sign transaction contract pub fn contract_sign( w: &mut WalletBackend, diff --git a/libwallet/src/contract/actions/view.rs b/libwallet/src/contract/actions/view.rs index 1c3ff60..72908f7 100644 --- a/libwallet/src/contract/actions/view.rs +++ b/libwallet/src/contract/actions/view.rs @@ -19,6 +19,7 @@ use crate::contract::types::ContractView; use crate::error::Error; use crate::grin_keychain::Keychain; use crate::grin_util::secp::key::SecretKey; +use crate::internal::updater; use crate::slate::{Slate, SlateState}; use crate::types::NodeClient; @@ -61,7 +62,12 @@ where })?), _ => None, }; - let is_executed = false; + // A contract is executed once the transaction it produced has confirmed. That is + // recorded on our own tx log entry for this slate, so no chain lookup is needed; a + // slate we have never signed simply has no entry and is not executed. + let is_executed = updater::retrieve_txs(w, None, Some(slate.id), None, None, false)? + .iter() + .any(|tx| tx.confirmed); // Count signatures present (a participant is "complete" once it has a partial sig). let num_sigs = slate .participant_data diff --git a/src/bin/grin-wallet.yml b/src/bin/grin-wallet.yml index 56c9559..98c24c9 100644 --- a/src/bin/grin-wallet.yml +++ b/src/bin/grin-wallet.yml @@ -541,6 +541,12 @@ subcommands: takes_value: false - view: about: View a contract + args: + - input: + help: Slatepack file containing the contract to view. + short: i + long: input + takes_value: true - revoke: about: Attempt to revoke a contract args: diff --git a/src/cmd/wallet_args.rs b/src/cmd/wallet_args.rs index a65b24f..7be382d 100644 --- a/src/cmd/wallet_args.rs +++ b/src/cmd/wallet_args.rs @@ -1140,6 +1140,35 @@ pub fn parse_contract_setup_args( }) } +pub fn parse_contract_view_args( + args: &ArgMatches, +) -> Result { + let input_file = match args.is_present("input") { + true => { + let file = parse_required(args, "input")?.to_owned(); + if !Path::new(&file).is_file() { + return Err(ParseError::ArgumentError(format!( + "File {} not found.", + &file + ))); + } + Some(file) + } + false => None, + }; + + // As for receive, prompt for the slatepack when no file was given + let mut input_slatepack_message = None; + if input_file.is_none() { + input_slatepack_message = Some(prompt_slatepack()?); + } + + Ok(command::ContractViewArgs { + input_file, + input_slatepack_message, + }) +} + pub fn parse_contract_revoke_args( args: &ArgMatches, ) -> Result { @@ -1489,8 +1518,9 @@ where let broadcast_tx = !sign_args.is_present("no-broadcast"); command::contract_sign(owner_api, km, setup_args, broadcast_tx) } - ("view", Some(_view_args)) => { - Err(Error::ArgumentError(String::from("Not implemented")).into()) + ("view", Some(view_args)) => { + let a = arg_parse!(parse_contract_view_args(&view_args)); + command::contract_view(owner_api, km, a) } ("revoke", Some(revoke_args)) => { let a = arg_parse!(parse_contract_revoke_args(&revoke_args)); diff --git a/tests/cmd_line_contract.rs b/tests/cmd_line_contract.rs new file mode 100644 index 0000000..67c5caf --- /dev/null +++ b/tests/cmd_line_contract.rs @@ -0,0 +1,167 @@ +// Copyright 2022 The Grin Developers +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Test the contract command line works as expected +#[macro_use] +extern crate clap; + +#[macro_use] +extern crate log; + +extern crate grin_wallet; + +use grin_wallet_impls::test_framework::{self, LocalWalletClient, WalletProxy}; + +use clap::App; +use std::thread; +use std::time::Duration; + +use grin_keychain::ExtKeychain; +use grin_wallet_impls::DefaultLCProvider; + +mod common; +use common::{clean_output_dir, execute_command, initial_setup_wallet, instantiate_wallet, setup}; + +/// Return the single slatepack file a wallet has written +fn only_slatepack(test_dir: &str, wallet_name: &str) -> String { + let dir = format!("{}/{}/slatepack", test_dir, wallet_name); + let mut found: Vec = std::fs::read_dir(&dir) + .unwrap_or_else(|e| panic!("no slatepack dir {}: {}", dir, e)) + .filter_map(|e| e.ok()) + .map(|e| e.path().to_string_lossy().to_string()) + .filter(|p| p.ends_with(".slatepack")) + .collect(); + assert_eq!(found.len(), 1, "expected one slatepack in {}", dir); + found.pop().unwrap() +} + +fn contract_command_test_impl(test_dir: &str) -> Result<(), grin_wallet_controller::Error> { + setup(test_dir); + let mut wallet_proxy: WalletProxy< + DefaultLCProvider, + LocalWalletClient, + ExtKeychain, + > = WalletProxy::new(test_dir); + let chain = wallet_proxy.chain.clone(); + + let yml = load_yaml!("../src/bin/grin-wallet.yml"); + let app = App::from_yaml(yml); + + // Two wallets, as a contract has two parties + let arg_vec = vec!["grin-wallet", "-p", "password1", "init", "-h"]; + let client1 = LocalWalletClient::new("wallet1", wallet_proxy.tx.clone()); + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + let config1 = initial_setup_wallet(test_dir, "wallet1"); + let (wallet1, mask1_i) = instantiate_wallet( + config1.clone().members.wallet, + client1.clone(), + "password1", + "default", + )?; + wallet_proxy.add_wallet( + "wallet1", + client1.get_send_instance(), + wallet1.clone(), + mask1_i.clone(), + ); + + let arg_vec = vec!["grin-wallet", "-p", "password2", "init", "-h"]; + let client2 = LocalWalletClient::new("wallet2", wallet_proxy.tx.clone()); + execute_command(&app, test_dir, "wallet2", &client2, arg_vec)?; + let config2 = initial_setup_wallet(test_dir, "wallet2"); + let (wallet2, mask2_i) = instantiate_wallet( + config2.clone().members.wallet, + client2.clone(), + "password2", + "default", + )?; + wallet_proxy.add_wallet( + "wallet2", + client2.get_send_instance(), + wallet2.clone(), + mask2_i.clone(), + ); + + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + + // Mine into wallet 1 so it has something to send + let mask1 = (&mask1_i).as_ref(); + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 5, false); + + // Wallet 1 opens a contract sending 1 grin, leaving an unencrypted slatepack because + // no --encrypt-for was given + let arg_vec = vec![ + "grin-wallet", + "-p", + "password1", + "contract", + "new", + "--send", + "1", + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + // Wallet 2 views the contract before signing it + let file_name = only_slatepack(test_dir, "wallet1"); + let arg_vec = vec![ + "grin-wallet", + "-p", + "password2", + "contract", + "view", + "-i", + &file_name, + ]; + execute_command(&app, test_dir, "wallet2", &client2, arg_vec)?; + + // Wallet 1 can view its own contract too + let arg_vec = vec![ + "grin-wallet", + "-p", + "password1", + "contract", + "view", + "-i", + &file_name, + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + // A file that isn't there is reported, not panicked on + let arg_vec = vec![ + "grin-wallet", + "-p", + "password1", + "contract", + "view", + "-i", + "no/such/file.slatepack", + ]; + assert!(execute_command(&app, test_dir, "wallet1", &client1, arg_vec).is_err()); + + // let logging finish + thread::sleep(Duration::from_millis(200)); + Ok(()) +} + +#[test] +fn wallet_contract_command_line() { + let test_dir = "target/test_output/contract_command_line"; + if let Err(e) = contract_command_test_impl(test_dir) { + panic!("Libwallet Error: {}", e); + } + clean_output_dir(test_dir); +}