test contract foreign api

This commit is contained in:
wiesche
2026-09-02 20:24:39 +02:00
parent daa40683e9
commit 2fc8d89add
6 changed files with 306 additions and 78 deletions
+21 -8
View File
@@ -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<Slate, Error> {
pub fn contract_new(&self, args: &ContractNewArgsAPI) -> Result<Slate, Error> {
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<Slate, Error> {
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.
+76
View File
@@ -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>,
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<WalletProgress, libwallet::Error> {
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);
+94 -13
View File
@@ -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<NodeVersionInfo>,
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(
+113 -55
View File
@@ -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<NodeVersionInfo>,
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::<CommitWrapper>::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,
+2
View File
@@ -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(),
-2
View File
@@ -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