mirror of
https://github.com/jwinterm/grin-wallet.git
synced 2026-10-17 07:11:36 +02:00
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:
+12
-1
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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!();
|
||||
}
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user