Contracts: wire up the contract view command

contract view was in the CLI help but the command returned Not
implemented, and there was no owner API entry point for the existing
libwallet implementation. Add owner::contract_view through to the CLI
and the owner RPC, with a display for the result.

The slate is taken from --input or prompted for, as receive does.

is_executed was hardcoded to false. Derive it from our own tx log entry
for the slate, which is confirmed once the contract has executed.
This commit is contained in:
jwinterm
2026-07-28 11:13:53 -04:00
parent cf82e2c94b
commit 6d6e19832c
9 changed files with 317 additions and 7 deletions
+12 -1
View File
@@ -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<ContractView, Error> {
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,
+11 -1
View File
@@ -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<ContractView, Error>;
/**
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<ContractView, Error> {
Owner::contract_view(self, (&token.keychain_mask).as_ref(), &Slate::from(slate))
}
fn contract_revoke(
&self,
token: Token,
+36
View File
@@ -1814,6 +1814,42 @@ where
Ok(())
}
#[derive(Clone)]
pub struct ContractViewArgs {
/// Slatepack file to read the contract from
pub input_file: Option<String>,
/// Slatepack message to read the contract from
pub input_slatepack_message: Option<String>,
}
pub fn contract_view<L, C, K>(
owner_api: &mut Owner<L, C, K>,
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
+29 -1
View File
@@ -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!();
}
+17 -1
View File
@@ -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<C, K>(
w: &mut WalletBackend<C, K>,
keychain_mask: Option<&SecretKey>,
slate: &Slate,
) -> Result<ContractView, Error>
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<C, K>(
w: &mut WalletBackend<C, K>,
+7 -1
View File
@@ -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
+6
View File
@@ -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:
+32 -2
View File
@@ -1140,6 +1140,35 @@ pub fn parse_contract_setup_args(
})
}
pub fn parse_contract_view_args(
args: &ArgMatches,
) -> Result<command::ContractViewArgs, ParseError> {
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<command::ContractRevokeArgs, ParseError> {
@@ -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));
+167
View File
@@ -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<String> = 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>,
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);
}