diff --git a/api/src/foreign.rs b/api/src/foreign.rs index 0dc4933..ead6d18 100644 --- a/api/src/foreign.rs +++ b/api/src/foreign.rs @@ -44,6 +44,10 @@ pub enum ForeignCheckMiddlewareFn { VerifySlateMessages, /// receive_tx ReceiveTx, + /// contract_new + ContractNew, + /// contract_sign + ContractSign, /// finalize_tx FinalizeTx, } @@ -463,26 +467,35 @@ where // Below is a foreign wrapper around owner calls to 'new' and 'sign' which are only executed // if this is a receiving contract. This preserves the ability to receive on a foreign interface. /// Start a receiving contract through the foreign API. - pub fn contract_new( - &self, - keychain_mask: Option<&SecretKey>, - args: &ContractNewArgsAPI, - ) -> Result { + pub fn contract_new(&self, args: &ContractNewArgsAPI) -> Result { let mut w_lock = self.wallet_inst.lock(); let w = w_lock.lc_provider()?.wallet_inst()?; - foreign::contract_new(w, keychain_mask, args) + if let Some(m) = self.middleware.as_ref() { + m( + ForeignCheckMiddlewareFn::ContractNew, + w.w2n_client().get_version_info(), + None, + )?; + } + foreign::contract_new(w, (&self.keychain_mask).as_ref(), args) } /// Sign the receiving side of a contract through the foreign API. pub fn contract_sign( &self, - keychain_mask: Option<&SecretKey>, slate: &Slate, args: &ContractSetupArgsAPI, ) -> Result { let mut w_lock = self.wallet_inst.lock(); let w = w_lock.lc_provider()?.wallet_inst()?; - foreign::contract_sign(w, keychain_mask, args, slate) + if let Some(m) = self.middleware.as_ref() { + m( + ForeignCheckMiddlewareFn::ContractSign, + w.w2n_client().get_version_info(), + Some(slate), + )?; + } + foreign::contract_sign(w, (&self.keychain_mask).as_ref(), args, slate) } /// Verify an invoice payment proof against the chain. diff --git a/controller/tests/common/mod.rs b/controller/tests/common/mod.rs index 8a41e94..8d56436 100644 --- a/controller/tests/common/mod.rs +++ b/controller/tests/common/mod.rs @@ -22,6 +22,7 @@ use grin_core as core; use grin_keychain as keychain; use grin_util as util; +use self::core::core::{FeeFields, KernelFeatures}; use self::core::global; use self::core::global::ChainTypes; use self::keychain::{ExtKeychain, Keychain}; @@ -96,6 +97,81 @@ pub fn contract_selection_args() -> libwallet::contract::types::OutputSelectionA } } +#[allow(dead_code)] +pub struct ExpectedContractSlate { + pub amount: u64, + pub fee: u64, + pub inputs: usize, + pub outputs: usize, + pub kernels: usize, + pub num_participants: u8, + pub participant_data: usize, + pub signatures: usize, +} + +#[allow(dead_code)] +pub fn assert_basic_contract_slate(slate: &libwallet::Slate, expected: ExpectedContractSlate) { + let tx = slate.tx_or_err().unwrap(); + assert_ne!(expected.fee, 0, "expected fee must be non-zero"); + let expected_fee = FeeFields::new(0, expected.fee).unwrap(); + assert_eq!(slate.amount, expected.amount); + assert_eq!(slate.fee_fields, expected_fee); + assert_eq!(slate.kernel_features, 0); + assert!(slate.kernel_features_args.is_none()); + assert!(slate.payment_proof.is_none()); + assert_eq!(slate.num_participants, expected.num_participants); + assert_eq!(tx.inputs().len(), expected.inputs); + assert_eq!(tx.outputs().len(), expected.outputs); + assert_eq!(tx.kernels().len(), expected.kernels); + assert_eq!(slate.participant_data.len(), expected.participant_data); + assert_eq!( + slate + .participant_data + .iter() + .filter(|participant| participant.part_sig.is_some()) + .count(), + expected.signatures + ); + if expected.kernels != 0 { + assert_eq!( + tx.kernels()[0].features, + KernelFeatures::Plain { fee: expected_fee } + ); + } +} + +#[allow(dead_code)] +pub type TestOwner = grin_wallet_api::Owner< + DefaultLCProvider, + LocalWalletClient, + ExtKeychain, +>; + +#[allow(dead_code)] +#[derive(Debug, PartialEq, Eq)] +pub struct WalletProgress { + child_index: u32, + tx_count: usize, +} + +#[allow(dead_code)] +pub fn wallet_progress( + api: &TestOwner, + keychain_mask: Option<&SecretKey>, +) -> Result { + let child_index = { + let mut wallet = api.wallet_inst.lock(); + let wallet = wallet.lc_provider()?.wallet_inst()?; + let parent = wallet.parent_key_id(); + wallet.current_child_index(&parent)? + }; + let (_, txs) = api.retrieve_txs(keychain_mask, false, None, None, None)?; + Ok(WalletProgress { + child_index, + tx_count: txs.len(), + }) +} + pub fn setup(test_dir: &str) { util::init_test_logger(); clean_output_dir(test_dir); diff --git a/controller/tests/contract_rsr.rs b/controller/tests/contract_rsr.rs index 9243643..9cdf996 100644 --- a/controller/tests/contract_rsr.rs +++ b/controller/tests/contract_rsr.rs @@ -22,7 +22,7 @@ use grin_wallet_libwallet as libwallet; use impls::test_framework::{self}; use libwallet::contract::my_fee_contribution; use libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI}; -use libwallet::{Slate, SlateState, TxLogEntryType}; +use libwallet::{NodeVersionInfo, Slate, SlateState, TxLogEntryType}; use std::sync::atomic::Ordering; use std::thread; use std::time::Duration; @@ -32,6 +32,21 @@ mod common; use common::{clean_output_dir, create_wallets, setup}; use std::path::PathBuf; +fn reject_contract_new( + method: grin_wallet_api::ForeignCheckMiddlewareFn, + _node_version: Option, + slate: Option<&Slate>, +) -> Result<(), libwallet::Error> { + assert!(matches!( + method, + grin_wallet_api::ForeignCheckMiddlewareFn::ContractNew + )); + assert!(slate.is_none()); + Err(libwallet::Error::GenericError( + "Contract rejected by middleware".to_string(), + )) +} + /// contract RSR flow fn contract_rsr_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { // create two wallets and mine 4 blocks in each (we want both to have balance to get a payjoin) @@ -41,28 +56,68 @@ fn contract_rsr_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> let send_mask = wallets[0].1.as_ref(); let recv_wallet = wallets[1].0.clone(); let recv_mask = wallets[1].1.as_ref(); + let participant_fee = my_fee_contribution(1, 1, 1, 2)?.fee(); - let mut slate = Slate::blank(0, true); // this gets overriden below - - wallet::controller::owner_single_use( + // Receive wallet initiates an invoice transaction through the foreign API. + let args = &ContractNewArgsAPI { + setup_args: ContractSetupArgsAPI { + selection_args: common::contract_selection_args(), + net_change: Some(5_000_000_000), + ..Default::default() + }, + }; + { + let api = grin_wallet_api::Foreign::new( + recv_wallet.clone(), + PathBuf::from(test_dir), + recv_mask.cloned(), + Some(reject_contract_new), + false, + ); + let err = api.contract_new(args).unwrap_err(); + assert!(matches!( + err, + libwallet::Error::GenericError(ref msg) + if msg == "Contract rejected by middleware" + )); + } + let mut slate = None; + wallet::controller::foreign_single_use( recv_wallet.clone(), - recv_mask, PathBuf::from(test_dir), - |api, m| { - // Receive wallet inititates an invoice transaction with --receive=5 - let args = &mut ContractNewArgsAPI { + recv_mask.cloned(), + |api| { + let rejected = ContractNewArgsAPI { setup_args: ContractSetupArgsAPI { - selection_args: common::contract_selection_args(), - net_change: Some(5_000_000_000), - ..Default::default() + net_change: Some(-5_000_000_000), + ..args.setup_args.clone() }, - ..Default::default() }; - slate = api.contract_new(m, args)?; + let err = api.contract_new(&rejected).unwrap_err(); + assert!(matches!( + err, + libwallet::Error::GenericError(ref msg) + if msg == "Can't create a non-receiving contract from a foreign API." + )); + slate = Some(api.contract_new(args)?); Ok(()) }, )?; + let mut slate = slate.expect("foreign API returned no slate"); assert_eq!(slate.state, SlateState::Invoice1); + common::assert_basic_contract_slate( + &slate, + common::ExpectedContractSlate { + amount: 5_000_000_000, + fee: participant_fee, + inputs: 0, + outputs: 0, + kernels: 0, + num_participants: 2, + participant_data: 1, + signatures: 0, + }, + ); wallet::controller::owner_single_use( send_wallet.clone(), @@ -92,6 +147,19 @@ fn contract_rsr_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> }, )?; assert_eq!(slate.state, SlateState::Invoice2); + common::assert_basic_contract_slate( + &slate, + common::ExpectedContractSlate { + amount: 5_000_000_000, + fee: 2 * participant_fee, + inputs: 1, + outputs: 1, + kernels: 1, + num_participants: 2, + participant_data: 2, + signatures: 1, + }, + ); // Receive wallet finalizes and posts wallet::controller::owner_single_use( @@ -108,6 +176,19 @@ fn contract_rsr_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> }, )?; assert_eq!(slate.state, SlateState::Invoice3); + common::assert_basic_contract_slate( + &slate, + common::ExpectedContractSlate { + amount: 5_000_000_000, + fee: 2 * participant_fee, + inputs: 2, + outputs: 2, + kernels: 1, + num_participants: 2, + participant_data: 2, + signatures: 2, + }, + ); // Send wallet posts so receive wallet doesn't get the mined amount wallet::controller::owner_single_use( diff --git a/controller/tests/contract_srs.rs b/controller/tests/contract_srs.rs index ea4ef48..17e73c1 100644 --- a/controller/tests/contract_srs.rs +++ b/controller/tests/contract_srs.rs @@ -23,7 +23,7 @@ use grin_core::core::transaction::{CommitWrapper, Input, Inputs, OutputFeatures, use impls::test_framework::{self}; use libwallet::contract::my_fee_contribution; use libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI, OwnCommitmentStatus}; -use libwallet::{Slate, SlateState, TxLogEntryType}; +use libwallet::{NodeVersionInfo, Slate, SlateState, TxLogEntryType}; use std::sync::atomic::Ordering; use std::thread; use std::time::Duration; @@ -33,6 +33,21 @@ mod common; use common::{clean_output_dir, create_wallets, setup}; use std::path::PathBuf; +fn reject_contract_slate( + method: grin_wallet_api::ForeignCheckMiddlewareFn, + _node_version: Option, + slate: Option<&Slate>, +) -> Result<(), libwallet::Error> { + assert!(matches!( + method, + grin_wallet_api::ForeignCheckMiddlewareFn::ContractSign + )); + assert!(slate.is_some()); + Err(libwallet::Error::GenericError( + "Contract rejected by middleware".to_string(), + )) +} + /// contract SRS flow fn contract_srs_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { // create two wallets and mine 4 blocks in each (we want both to have balance to get a payjoin) @@ -42,8 +57,9 @@ fn contract_srs_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> let send_mask = wallets[0].1.as_ref(); let recv_wallet = wallets[1].0.clone(); let recv_mask = wallets[1].1.as_ref(); + let participant_fee = my_fee_contribution(1, 1, 1, 2)?.fee(); - let mut slate = Slate::blank(0, true); // this gets overriden below + let mut slate = Slate::blank(0, false); // this gets overriden below wallet::controller::owner_single_use( send_wallet.clone(), @@ -64,19 +80,26 @@ fn contract_srs_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> }, )?; assert_eq!(slate.state, SlateState::Standard1); + common::assert_basic_contract_slate( + &slate, + common::ExpectedContractSlate { + amount: 5_000_000_000, + fee: participant_fee, + inputs: 0, + outputs: 0, + kernels: 0, + num_participants: 2, + participant_data: 1, + signatures: 0, + }, + ); wallet::controller::owner_single_use( recv_wallet.clone(), recv_mask, PathBuf::from(test_dir), |api, m| { - let child_index = { - let mut wallet = api.wallet_inst.lock(); - let wallet = wallet.lc_provider()?.wallet_inst()?; - let parent = wallet.parent_key_id(); - wallet.current_child_index(&parent)? - }; - let (_, txs) = api.retrieve_txs(m, false, None, None, None)?; + let progress = common::wallet_progress(api, m)?; let mut invalid = slate.clone(); invalid.state = SlateState::Unknown; let err = api @@ -95,15 +118,7 @@ fn contract_srs_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> libwallet::Error::GenericError(ref msg) if msg == "Cannot advance a contract slate in state UN" )); - let current_child_index = { - let mut wallet = api.wallet_inst.lock(); - let wallet = wallet.lc_provider()?.wallet_inst()?; - let parent = wallet.parent_key_id(); - wallet.current_child_index(&parent)? - }; - let (_, current_txs) = api.retrieve_txs(m, false, None, None, None)?; - assert_eq!(current_child_index, child_index); - assert_eq!(current_txs.len(), txs.len()); + assert_eq!(common::wallet_progress(api, m)?, progress); let wrong_args = ContractSetupArgsAPI { selection_args: common::contract_selection_args(), @@ -117,41 +132,82 @@ fn contract_srs_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> if msg == "Expected net change 5000000000, got -5000000000 (did you mean --receive instead of --send?)" )); - // Receive wallet calls --receive=5 - let args = &mut ContractSetupArgsAPI { - selection_args: common::contract_selection_args(), - net_change: Some(5_000_000_000), - ..Default::default() - }; - let incoming = slate.clone(); - slate = api.contract_sign(m, &incoming, args)?; + Ok(()) + }, + )?; - let child_index = { - let mut wallet = api.wallet_inst.lock(); - let wallet = wallet.lc_provider()?.wallet_inst()?; - let parent = wallet.parent_key_id(); - wallet.current_child_index(&parent)? + // Receive wallet signs through the foreign API. + let args = ContractSetupArgsAPI { + selection_args: common::contract_selection_args(), + net_change: Some(5_000_000_000), + ..Default::default() + }; + let incoming = slate.clone(); + { + let api = grin_wallet_api::Foreign::new( + recv_wallet.clone(), + PathBuf::from(test_dir), + recv_mask.cloned(), + Some(reject_contract_slate), + false, + ); + let err = api.contract_sign(&incoming, &args).unwrap_err(); + assert!(matches!( + err, + libwallet::Error::GenericError(ref msg) + if msg == "Contract rejected by middleware" + )); + } + wallet::controller::foreign_single_use( + recv_wallet.clone(), + PathBuf::from(test_dir), + recv_mask.cloned(), + |api| { + let rejected = ContractSetupArgsAPI { + net_change: Some(-5_000_000_000), + ..args.clone() }; - let (_, txs) = api.retrieve_txs(m, false, None, None, None)?; - let err = api.contract_sign(m, &incoming, args).unwrap_err(); + let err = api.contract_sign(&incoming, &rejected).unwrap_err(); + assert!(matches!( + err, + libwallet::Error::GenericError(ref msg) + if msg == "Can't sign a non-receiving contract from a foreign API." + )); + slate = api.contract_sign(&incoming, &args)?; + Ok(()) + }, + )?; + + wallet::controller::owner_single_use( + recv_wallet.clone(), + recv_mask, + PathBuf::from(test_dir), + |api, m| { + let progress = common::wallet_progress(api, m)?; + let err = api.contract_sign(m, &incoming, &args).unwrap_err(); assert!(matches!( err, libwallet::Error::GenericError(ref msg) if msg == &format!("Slate with id:{} has already been signed.", incoming.id) )); - let current_child_index = { - let mut wallet = api.wallet_inst.lock(); - let wallet = wallet.lc_provider()?.wallet_inst()?; - let parent = wallet.parent_key_id(); - wallet.current_child_index(&parent)? - }; - let (_, current_txs) = api.retrieve_txs(m, false, None, None, None)?; - assert_eq!(current_child_index, child_index); - assert_eq!(current_txs.len(), txs.len()); + assert_eq!(common::wallet_progress(api, m)?, progress); Ok(()) }, )?; assert_eq!(slate.state, SlateState::Standard2); + common::assert_basic_contract_slate( + &slate, + common::ExpectedContractSlate { + amount: 5_000_000_000, + fee: 2 * participant_fee, + inputs: 1, + outputs: 1, + kernels: 1, + num_participants: 2, + participant_data: 2, + signatures: 1, + }, + ); wallet::controller::owner_single_use( recv_wallet.clone(), recv_mask, @@ -186,12 +242,7 @@ fn contract_srs_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> let (_, outputs) = api.retrieve_outputs(m, false, false, None)?; let tx = slate.tx_or_err()?; let input_commits = Vec::::from(&tx.inputs()); - let child_index = { - let mut wallet = api.wallet_inst.lock(); - let wallet = wallet.lc_provider()?.wallet_inst()?; - let parent = wallet.parent_key_id(); - wallet.current_child_index(&parent)? - }; + let progress = common::wallet_progress(api, m)?; let mut changed = slate.clone(); changed.tx = Some( Transaction::new( @@ -212,13 +263,7 @@ fn contract_srs_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> } let view = api.contract_view(m, &changed)?; assert_eq!(view.own_commitment_status, OwnCommitmentStatus::Unknown); - let current_child_index = { - let mut wallet = api.wallet_inst.lock(); - let wallet = wallet.lc_provider()?.wallet_inst()?; - let parent = wallet.parent_key_id(); - wallet.current_child_index(&parent)? - }; - assert_eq!(current_child_index, child_index); + assert_eq!(common::wallet_progress(api, m)?, progress); let (output, pos) = outputs .iter() @@ -333,6 +378,19 @@ fn contract_srs_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> }, )?; assert_eq!(slate.state, SlateState::Standard3); + common::assert_basic_contract_slate( + &slate, + common::ExpectedContractSlate { + amount: 5_000_000_000, + fee: 2 * participant_fee, + inputs: 2, + outputs: 2, + kernels: 1, + num_participants: 2, + participant_data: 2, + signatures: 2, + }, + ); wallet::controller::owner_single_use( recv_wallet.clone(), recv_mask, diff --git a/libwallet/src/api_impl/foreign.rs b/libwallet/src/api_impl/foreign.rs index bbc1c2c..7548100 100644 --- a/libwallet/src/api_impl/foreign.rs +++ b/libwallet/src/api_impl/foreign.rs @@ -259,6 +259,7 @@ where let net_change = args.setup_args.net_change.ok_or_else(|| { Error::GenericError("Contract requires a net change (--send or --receive)".to_string()) })?; + // The foreign API only handles the receiving side. if net_change <= 0 { return Err(Error::GenericError( "Can't create a non-receiving contract from a foreign API.".to_string(), @@ -281,6 +282,7 @@ where let net_change = args.net_change.ok_or_else(|| { Error::GenericError("Contract requires a net change (--send or --receive)".to_string()) })?; + // The foreign API only handles the receiving side. if net_change <= 0 { return Err(Error::GenericError( "Can't sign a non-receiving contract from a foreign API.".to_string(), diff --git a/libwallet/src/contract/actions/README.md b/libwallet/src/contract/actions/README.md index b14d775..aacb2ea 100644 --- a/libwallet/src/contract/actions/README.md +++ b/libwallet/src/contract/actions/README.md @@ -73,8 +73,6 @@ Separating side effects until the 'save_step' part would make these functions mu #### Tests - - slate contents at each step, not only the end state - - the foreign API entry points for `new` and `sign` - Slatepack version negotiation with the last supported wallet release #### save_step