add contract fee rates

This commit is contained in:
wiesche
2026-09-05 11:00:03 +02:00
parent fb99cd6c9b
commit fb0d0493bc
16 changed files with 343 additions and 29 deletions
+23 -4
View File
@@ -1692,12 +1692,12 @@ pub struct ContractNewArgs {
pub minimum_confirmations: u64,
/// Blocks until wallets should stop signing the contract
pub ttl_blocks: Option<u64>,
/// Fee rate in nanogrin per unit of transaction weight
pub fee_rate: Option<u32>,
/// Select and lock outputs early
pub add_outputs: bool,
// Future features
/// Custom fee contribution
pub fee_rate: Option<u32>,
/// Save slatepack to a specific filename
pub outfile: Option<String>,
}
@@ -1724,6 +1724,7 @@ impl ContractNewArgs {
Ok(ContractNewArgsAPI {
ttl_blocks: self.ttl_blocks,
setup_args: ContractSetupArgsAPI {
fee_rate: self.fee_rate,
src_acct_name: match self.src_acct_name.as_ref() {
Some(v) => Some(v.to_string()),
None => None,
@@ -1796,12 +1797,12 @@ pub struct ContractSetupArgs {
pub make_outputs: Option<Vec<u64>>,
/// Optional minimum number of confirmations required for an input
pub minimum_confirmations: Option<u64>,
/// Fee rate in nanogrin per unit of transaction weight
pub fee_rate: Option<u32>,
// Future features
/// Whether we should automatically sign a receive of any value
// pub auto_receive: Option<bool>,
/// Custom fee contribution
pub fee_rate: Option<u32>,
/// Save slatepack to a specific filename
pub outfile: Option<String>,
/// Add outputs
@@ -1830,6 +1831,7 @@ impl ContractSetupArgs {
fn to_api_args(&self) -> Result<ContractSetupArgsAPI, Error> {
let net_change = self.get_net_change()?;
Ok(ContractSetupArgsAPI {
fee_rate: self.fee_rate,
net_change: net_change,
add_outputs: self.add_outputs,
selection_args: OutputSelectionArgs {
@@ -2061,4 +2063,21 @@ mod contract_tests {
if message.contains("Unsupported Slatepack mode: 2")
));
}
#[test]
fn contract_sign_fee_rate() {
let args = ContractSetupArgs {
counterparty_addr: None,
receive: None,
send: None,
as_json: false,
use_inputs: None,
make_outputs: None,
minimum_confirmations: None,
fee_rate: Some(2),
outfile: None,
add_outputs: false,
};
assert_eq!(args.to_api_args().unwrap().fee_rate, Some(2));
}
}
+55
View File
@@ -124,6 +124,52 @@ fn contract_self_spend_tx_impl(test_dir: &'static str) -> Result<(), libwallet::
Ok(())
}
fn contract_fee_floor_impl(test_dir: &'static str) -> Result<(), libwallet::Error> {
let (wallets, _, stopper, _) = create_wallets(vec![vec![("default", 4)]], test_dir).unwrap();
let wallet = wallets[0].0.clone();
let mask = wallets[0].1.as_ref();
wallet::controller::owner_single_use(wallet, mask, PathBuf::from(test_dir), |api, m| {
let slate = api.contract_new(
m,
&ContractNewArgsAPI {
setup_args: ContractSetupArgsAPI {
selection_args: common::contract_selection_args(),
net_change: Some(0),
num_participants: 1,
fee_rate: Some(1),
..Default::default()
},
..Default::default()
},
)?;
let (_, txs) = api.retrieve_txs(m, false, None, Some(slate.id), None)?;
let (_, outputs) = api.retrieve_outputs(m, false, false, None)?;
let err = api
.contract_sign(m, &slate, &ContractSetupArgsAPI::default())
.unwrap_err();
assert!(matches!(
err,
libwallet::Error::Fee(ref message) if message.starts_with("Fee Dispute Error:")
));
let (_, current_txs) = api.retrieve_txs(m, false, None, Some(slate.id), None)?;
let (_, current_outputs) = api.retrieve_outputs(m, false, false, None)?;
assert_eq!(
serde_json::to_value(current_txs).unwrap(),
serde_json::to_value(txs).unwrap()
);
assert_eq!(
serde_json::to_value(current_outputs).unwrap(),
serde_json::to_value(outputs).unwrap()
);
Ok(())
})?;
stopper.store(false, Ordering::Relaxed);
thread::sleep(Duration::from_millis(200));
Ok(())
}
#[test]
fn wallet_contract_self_spend_tx() -> Result<(), libwallet::Error> {
let test_dir = "test_output/contract_self_spend_tx";
@@ -132,3 +178,12 @@ fn wallet_contract_self_spend_tx() -> Result<(), libwallet::Error> {
clean_output_dir(test_dir);
Ok(())
}
#[test]
fn contract_fee_floor() -> Result<(), libwallet::Error> {
let test_dir = "test_output/contract_fee_floor";
setup(test_dir);
contract_fee_floor_impl(test_dir)?;
clean_output_dir(test_dir);
Ok(())
}
+19 -7
View File
@@ -70,6 +70,9 @@ fn contract_srs_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error>
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 receiver_rate = 2 * u32::try_from(grin_core::global::get_accept_fee_base()).unwrap();
let receiver_fee = Transaction::weight_by_iok(1, 1, 0) * u64::from(receiver_rate)
+ (Transaction::weight_by_iok(0, 0, 1) * u64::from(receiver_rate)).div_ceil(2);
let ttl_cutoff = bh + 20;
let ttl_error = format!(
"Contract TTL changed from {} to {}",
@@ -111,6 +114,15 @@ fn contract_srs_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error>
if msg == "Contract TTL must be at least 1 block"
));
args.ttl_blocks = Some(20);
args.setup_args.fee_rate = Some(0);
let err = api.contract_new(m, &mut args).unwrap_err();
assert!(matches!(
err,
libwallet::Error::GenericError(ref msg)
if msg == "Contract fee rate must be at least 1"
));
args.setup_args.fee_rate = None;
assert_eq!(common::wallet_progress(api, m)?, progress);
slate = api.contract_new(m, &mut args)?;
let (_, txs) = api.retrieve_txs(m, false, None, Some(slate.id), None)?;
assert_eq!(txs[0].ttl_cutoff_height, Some(ttl_cutoff));
@@ -253,6 +265,7 @@ fn contract_srs_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error>
let args = ContractSetupArgsAPI {
selection_args: common::contract_selection_args(),
net_change: Some(5_000_000_000),
fee_rate: Some(receiver_rate),
..Default::default()
};
let incoming = slate.clone();
@@ -325,7 +338,7 @@ fn contract_srs_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error>
&slate,
common::ExpectedContractSlate {
amount: 5_000_000_000,
fee: 2 * participant_fee,
fee: participant_fee + receiver_fee,
inputs: 1,
coinbase_inputs: 1,
outputs: 1,
@@ -342,10 +355,10 @@ fn contract_srs_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error>
|api, m| {
let view = api.contract_view(m, &slate)?;
assert_eq!(view.own_commitment_status, OwnCommitmentStatus::Clean);
assert_eq!(view.own_fee, Some(participant_fee));
assert_eq!(view.own_fee, Some(receiver_fee));
assert_eq!(
view.balance_change,
Some(5_000_000_000 - participant_fee as i64)
Some(5_000_000_000 - receiver_fee as i64)
);
Ok(())
},
@@ -545,7 +558,7 @@ fn contract_srs_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error>
&slate,
common::ExpectedContractSlate {
amount: 5_000_000_000,
fee: 2 * participant_fee,
fee: participant_fee + receiver_fee,
inputs: 2,
coinbase_inputs: 2,
outputs: 2,
@@ -623,11 +636,10 @@ fn contract_srs_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error>
assert_eq!(tx_log.amount_debited, 0);
assert_eq!(tx_log.num_inputs, 1);
assert_eq!(tx_log.num_outputs, 1);
let expected_fees_paid = Some(my_fee_contribution(1, 1, 1, 2)?);
assert_eq!(tx_log.fee, expected_fees_paid);
assert_eq!(tx_log.fee.map(|fee| fee.fee()), Some(receiver_fee));
assert_eq!(
wallet_info.amount_currently_spendable,
4 * 60_000_000_000 + 5_000_000_000 - expected_fees_paid.unwrap().fee() // we expect the balance of 4 mined blocks + 5 Grin - fees paid
4 * 60_000_000_000 + 5_000_000_000 - receiver_fee // we expect the balance of 4 mined blocks + 5 Grin - fees paid
);
Ok(())
},
+5 -1
View File
@@ -41,6 +41,11 @@ so a late-locked contract can fail if another transaction spends the available o
`contract new --ttl_blocks N` stops contract signing after `N` blocks. Finalized transactions
can still be posted afterward.
`--fee_rate N` uses `N` nanogrin per weight unit for this wallet's fee contribution. Each
wallet chooses its rate when it first joins. Without it, the wallet uses `accept_fee_base`
(500000 by default). The wallet only finalizes when the combined fee meets that minimum. If it
does not, revoke the contract and create a new one.
Each side's account is fixed when it first joins the contract. Changing the active account
later does not affect setup or signing; they use the account stored in the context.
@@ -72,7 +77,6 @@ account selected with `--account`.
the API. Only invoice proofs are supported and they require the experimental Slate V5
format. The full proposal is described in
[Early Payment Proofs](https://github.com/mimblewimble/grin-rfcs/pull/70)
* Custom fee rates are not supported
* There are no contract-specific history, lookup or transport commands. Payment proof
memos cannot be set through the contract API or CLI. The current proof stores a memo
type and 32 bytes instead of the text and hash described by the early payment proofs
-1
View File
@@ -41,7 +41,6 @@ Separating side effects until the 'save_step' part would make these functions mu
clearer to callers.
- For a payjoin, `--use-inputs any` picks an input for us; naming the commitment would be
clearer, for the API as much as the CLI.
- Add custom contract fee rates.
- Support `--outfile` for contract slatepacks.
- Raise the replacement fee when `revoke` races a transaction already in the mempool.
- Merge invoice-proof retrieval with the existing payment-proof API.
+1
View File
@@ -40,6 +40,7 @@ where
K: Keychain,
{
contract::utils::verify_num_participants(setup_args.num_participants)?;
contract::utils::verify_fee_rate(setup_args.fee_rate)?;
// Compute state for 'new'
let (slate, context) = compute(w, keychain_mask, setup_args, ttl_blocks, slate_id)?;
+2
View File
@@ -129,6 +129,7 @@ where
src_acct_name: src_acct_name.clone(),
net_change: Some(0), // self-spend
num_participants: 1,
fee_rate: None,
add_outputs: false,
selection_args: OutputSelectionArgs {
// Keep revoke at one confirmation
@@ -149,6 +150,7 @@ where
src_acct_name,
net_change: None, // we already have it in the context as 0 now
num_participants: 1,
fee_rate: None,
add_outputs: false,
selection_args: OutputSelectionArgs {
minimum_confirmations: Some(1),
+1
View File
@@ -48,6 +48,7 @@ where
K: Keychain,
{
contract::utils::verify_num_participants(slate.num_participants)?;
contract::utils::verify_fee_rate(setup_args.fee_rate)?;
if slate.num_participants != setup_args.num_participants {
return Err(Error::GenericError(format!(
"Inconsistent num_participants. Slate num_participants:{}, Setup num_participants: {}",
+1
View File
@@ -59,6 +59,7 @@ where
K: Keychain,
{
contract::utils::verify_num_participants(slate.num_participants)?;
contract::utils::verify_fee_rate(setup_args.fee_rate)?;
// Compute if we will add outputs at this step. Only a missing context means we
// still need to add them; propagate any other error.
let existing_context = match w.get_private_context(keychain_mask, slate.id.as_bytes()) {
+1
View File
@@ -80,6 +80,7 @@ where
{
debug!("Creating a new contract context");
contract_utils::verify_num_participants(setup_args.num_participants)?;
contract_utils::verify_fee_rate(setup_args.fee_rate)?;
// sender should always refresh outputs
updater::refresh_outputs(w, keychain_mask, parent_key_id, false)?;
+33 -7
View File
@@ -16,7 +16,7 @@
use crate::backend::WalletBackend;
use crate::contract::types::{ContractSetupArgsAPI, OutputSelectionArgs};
use crate::contract::utils::my_fee_contribution;
use crate::contract::utils::fee_contribution;
use crate::grin_core::core::amount_to_hr_string;
use crate::grin_keychain::{Identifier, Keychain};
use crate::types::NodeClient;
@@ -130,11 +130,12 @@ where
if !is_self_spend && (pay_amount == 0 && !is_payjoin) {
return Ok((
vec![],
my_fee_contribution(
fee_contribution(
0,
setup_args.selection_args.num_custom_outputs() + 1,
1,
setup_args.num_participants,
setup_args.fee_rate,
)?,
));
}
@@ -179,7 +180,7 @@ where
committed_fee.unwrap()
} else {
// We start with a fee of 1 output and a shared kernel which is minimum for both parties
my_fee_contribution(0, 1, 1, setup_args.num_participants)?
fee_contribution(0, 1, 1, setup_args.num_participants, setup_args.fee_rate)?
// FeeFields::zero()
};
@@ -191,11 +192,21 @@ where
let must_take =
out.commit.is_some() && must_use_list.contains(&&out.commit.as_ref().unwrap()[..]);
// Compute the fee without this input
let fee_without =
my_fee_contribution(n_inputs, my_num_outputs, 1, setup_args.num_participants)?;
let fee_without = fee_contribution(
n_inputs,
my_num_outputs,
1,
setup_args.num_participants,
setup_args.fee_rate,
)?;
// Compute the total fee cost if we took this input
let mut fee_with =
my_fee_contribution(n_inputs + 1, my_num_outputs, 1, setup_args.num_participants)?;
let mut fee_with = fee_contribution(
n_inputs + 1,
my_num_outputs,
1,
setup_args.num_participants,
setup_args.fee_rate,
)?;
// If the current fee is lower than the committed fee (my_fee) then set it to committed fee
if my_fee.fee() > fee_with.fee() {
fee_with = my_fee;
@@ -370,6 +381,7 @@ pub fn verify_selection_consistency(
mod tests {
use super::*;
use crate::contract::utils::my_fee_contribution;
use crate::grin_keychain::{Identifier, IDENTIFIER_SIZE};
use crate::OutputStatus;
@@ -537,6 +549,20 @@ mod tests {
);
}
#[test]
fn custom_fee_rate() {
let setup_args = ContractSetupArgsAPI {
net_change: Some(-3_000_000_000),
fee_rate: Some(2),
..Default::default()
};
let mut inputs = _create_output_data_for(vec![4_000_000_000]);
let (_, outputs, fee) = compute(&setup_args, None, &mut inputs).unwrap();
let expected = fee_contribution(1, 1, 1, 2, Some(2)).unwrap();
assert_eq!(fee, expected);
assert_eq!(outputs, vec![1_000_000_000 - expected.fee()]);
}
#[test]
fn sender_exact() {
// net_change=-3, inputs=[3, my_fees(2, 1)], no fee committed => Ok([3, my_fees(2, 1)], fees)
+25
View File
@@ -127,6 +127,10 @@ pub struct ContractSetupArgsAPI {
pub net_change: Option<i64>,
/// The number of participants in a contract. Used for computing our kernel fee contribution
pub num_participants: u8,
/// Fee rate in nanogrin per unit of transaction weight
/// `None` uses the wallet default. The rate is fixed when the local context is created
/// and may be omitted in later steps
pub fee_rate: Option<u32>,
/// Should we perform an early lock of outputs
pub add_outputs: bool,
/// Output selection arguments
@@ -141,6 +145,7 @@ impl Default for ContractSetupArgsAPI {
src_acct_name: None,
net_change: None,
num_participants: 2,
fee_rate: None,
add_outputs: false,
selection_args: OutputSelectionArgs {
..Default::default()
@@ -169,6 +174,7 @@ impl Default for ContractNewArgsAPI {
src_acct_name: None,
net_change: None,
num_participants: 2,
fee_rate: None,
add_outputs: false,
selection_args: OutputSelectionArgs {
..Default::default()
@@ -295,6 +301,25 @@ mod tests {
);
}
#[test]
fn fee_rate_json() {
let mut json = serde_json::to_value(ContractSetupArgsAPI::default()).unwrap();
json["fee_rate"] = serde_json::json!(2);
assert_eq!(
serde_json::from_value::<ContractSetupArgsAPI>(json.clone())
.unwrap()
.fee_rate,
Some(2)
);
json.as_object_mut().unwrap().remove("fee_rate");
assert_eq!(
serde_json::from_value::<ContractSetupArgsAPI>(json)
.unwrap()
.fee_rate,
None
);
}
#[test]
fn own_commitment_status_json() {
for (status, value) in [
+88 -6
View File
@@ -24,7 +24,7 @@ use crate::slate::Slate;
use crate::types::{Context, NodeClient, StoredProofInfo, TxLogEntryType};
use crate::util::OnionV3Address;
use crate::{address, Error, OutputData, OutputStatus, TxLogEntry};
use grin_core::core::FeeFields;
use grin_core::core::{FeeFields, Transaction};
use uuid::Uuid;
/// Creates an initial TxLogEntry without input/output or kernel information
@@ -345,16 +345,35 @@ pub fn my_fee_contribution(
n_outputs: usize,
n_kernels: usize,
num_participants: u8,
) -> Result<FeeFields, Error> {
fee_contribution(n_inputs, n_outputs, n_kernels, num_participants, None)
}
pub(super) fn fee_contribution(
n_inputs: usize,
n_outputs: usize,
n_kernels: usize,
num_participants: u8,
fee_rate: Option<u32>,
) -> Result<FeeFields, Error> {
verify_num_participants(num_participants)?;
verify_fee_rate(fee_rate)?;
let fee_for = |inputs: usize, outputs: usize, kernels: usize| match fee_rate {
Some(rate) => Transaction::weight_by_iok(inputs as u64, outputs as u64, kernels as u64)
.checked_mul(u64::from(rate))
.ok_or_else(|| Error::GenericError("Contract fee overflow".to_string())),
None => Ok(tx_fee(inputs, outputs, kernels)),
};
// Add our fee costs for our inputs and a single output
let mut fee = tx_fee(n_inputs, n_outputs, 0);
let mut fee = fee_for(n_inputs, n_outputs, 0)?;
// Add out fee costs for kernel. We pay 1/num_participants of a kernel cost
let kernel_cost = tx_fee(0, 0, n_kernels);
let kernel_cost = fee_for(0, 0, n_kernels)?;
// Round each participant's kernel share up, so the participants together never underpay;
// the overpay is bounded to under one fee unit per participant.
let my_kernel_cost = (kernel_cost as f64 / (num_participants as f64)).ceil();
fee += my_kernel_cost as u64;
let my_kernel_cost = kernel_cost.div_ceil(u64::from(num_participants));
fee = fee
.checked_add(my_kernel_cost)
.ok_or_else(|| Error::GenericError("Contract fee overflow".to_string()))?;
// Add my fee contribution to the slate total fee. Uses the standard FeeFields
// encoding; contracts rely on every participant applying the same
@@ -375,6 +394,15 @@ pub(super) fn verify_num_participants(num_participants: u8) -> Result<(), Error>
Ok(())
}
pub(super) fn verify_fee_rate(fee_rate: Option<u32>) -> Result<(), Error> {
if fee_rate == Some(0) {
return Err(Error::GenericError(
"Contract fee rate must be at least 1".to_string(),
));
}
Ok(())
}
/// Ensure the slate still carries the stored contract deadline
pub(super) fn verify_ttl(expected: Option<u64>, slate: &Slate) -> Result<(), Error> {
let expected = expected.unwrap_or(0);
@@ -436,6 +464,18 @@ pub fn verify_setup_args_consistency(
ctx_setup_args.num_participants, cur_setup_args.num_participants
)));
}
if let Some(fee_rate) = cur_setup_args.fee_rate {
if Some(fee_rate) != ctx_setup_args.fee_rate {
let setup = ctx_setup_args
.fee_rate
.map(|rate| rate.to_string())
.unwrap_or_else(|| "default".to_string());
return Err(Error::GenericError(format!(
"Can't change fee rate after contract setup. setup:{}, current:{}",
setup, fee_rate
)));
}
}
// add_outputs is intentionally forced true at the sign step (late lock), so it is not
// part of this consistency check. parent_key_id is taken from the stored Context, so
// later steps always derive under the contract's account regardless of the active one.
@@ -489,12 +529,54 @@ mod tests {
let half = my_fee_contribution(n_inputs, n_outputs, n_kernels, 2)
.unwrap()
.fee();
let my_share = ((kernel_cost as f64) / 2.0).ceil() as u64;
let my_share = kernel_cost.div_ceil(2);
assert_eq!(half, base + my_share);
// ... and applying the same split, the participants together cover the kernel cost.
assert!(2 * my_share >= kernel_cost);
}
#[test]
fn custom_fee_rate() {
let rate = 2;
let fee = fee_contribution(1, 1, 1, 2, Some(rate)).unwrap();
let base = Transaction::weight_by_iok(1, 1, 0) * u64::from(rate);
let kernel = Transaction::weight_by_iok(0, 0, 1) * u64::from(rate);
assert_eq!(fee.fee(), base + kernel.div_ceil(2));
assert!(fee_contribution(1, 1, 1, 2, Some(0)).is_err());
}
#[test]
fn fee_rate_consistency() {
let setup = ContractSetupArgsAPI {
net_change: Some(-1),
fee_rate: Some(2),
..Default::default()
};
assert!(verify_setup_args_consistency(&setup, &setup).is_ok());
assert!(verify_setup_args_consistency(
&setup,
&ContractSetupArgsAPI {
net_change: Some(-1),
..Default::default()
}
)
.is_ok());
let err = verify_setup_args_consistency(
&setup,
&ContractSetupArgsAPI {
net_change: Some(-1),
fee_rate: Some(3),
..Default::default()
},
)
.unwrap_err();
assert!(matches!(
err,
Error::GenericError(ref msg)
if msg == "Can't change fee rate after contract setup. setup:2, current:3"
));
}
#[test]
fn participant_count() {
assert!(verify_num_participants(1).is_ok());
+8
View File
@@ -503,6 +503,10 @@ subcommands:
long: min_conf
default_value: "10"
takes_value: true
- fee_rate:
help: Fee rate in nanogrin per unit of transaction weight (defaults to accept_fee_base)
long: fee_rate
takes_value: true
- ttl_blocks:
help: Positive number of blocks until wallets stop signing the contract
short: b
@@ -551,6 +555,10 @@ subcommands:
short: c
long: min_conf
takes_value: true
- fee_rate:
help: Fee rate to use when first joining, in nanogrin per unit of transaction weight (defaults to accept_fee_base)
long: fee_rate
takes_value: true
- no-broadcast:
help: Don't broadcast the transaction if it was finalized
long: no-broadcast
+19 -2
View File
@@ -1001,6 +1001,21 @@ pub fn parse_verify_proof_args(args: &ArgMatches) -> Result<command::ProofVerify
})
}
fn parse_contract_fee_rate(args: &ArgMatches) -> Result<Option<u32>, ParseError> {
let Some(value) = args.value_of("fee_rate") else {
return Ok(None);
};
let rate = parse_u64(value, "fee_rate")?;
if rate == 0 {
return Err(ParseError::ArgumentError(
"Contract fee rate must be at least 1".to_string(),
));
}
u32::try_from(rate)
.map(Some)
.map_err(|_| ParseError::ArgumentError("Contract fee rate is too large".to_string()))
}
pub fn parse_contract_new_args(
args: &ArgMatches,
account: &String,
@@ -1032,6 +1047,7 @@ pub fn parse_contract_new_args(
parse_required(args, "minimum_confirmations")?,
"minimum_confirmations",
)?;
let fee_rate = parse_contract_fee_rate(args)?;
let ttl_blocks = match args.value_of("ttl_blocks") {
Some(value) => {
let blocks = parse_u64(value, "ttl_blocks")?;
@@ -1099,9 +1115,9 @@ pub fn parse_contract_new_args(
use_inputs: use_inputs,
make_outputs: make_outputs,
minimum_confirmations,
fee_rate,
ttl_blocks,
// Not yet wired to CLI flags:
fee_rate: None,
outfile: None,
})
}
@@ -1132,6 +1148,7 @@ pub fn parse_contract_setup_args(
Some(value) => Some(parse_u64(value, "minimum_confirmations")?),
None => None,
};
let fee_rate = parse_contract_fee_rate(args)?;
let no_payjoin = args.is_present("no-payjoin");
let use_inputs = match args.value_of("use-inputs") {
Some(v) => {
@@ -1174,8 +1191,8 @@ pub fn parse_contract_setup_args(
use_inputs: use_inputs,
make_outputs: make_outputs,
minimum_confirmations,
fee_rate,
// Not yet wired to CLI flags:
fee_rate: None,
outfile: None,
})
}
+62 -1
View File
@@ -26,6 +26,7 @@ use clap::App;
use std::thread;
use std::time::Duration;
use grin_core::core::Transaction;
use grin_keychain::ExtKeychain;
use grin_wallet_impls::DefaultLCProvider;
@@ -102,6 +103,10 @@ fn contract_command_test_impl(test_dir: &str) -> Result<(), grin_wallet_controll
let mask1 = (&mask1_i).as_ref();
let mask2 = (&mask2_i).as_ref();
let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 5, false);
let fee_rate = 2 * grin_core::global::get_accept_fee_base();
let expected_fee = Transaction::weight_by_iok(1, 1, 0) * fee_rate
+ (Transaction::weight_by_iok(0, 0, 1) * fee_rate).div_ceil(2);
let fee_rate = fee_rate.to_string();
// Wallet 1 opens a contract sending 1 grin, leaving an unencrypted slatepack because
// no --encrypt-for was given
@@ -115,11 +120,24 @@ fn contract_command_test_impl(test_dir: &str) -> Result<(), grin_wallet_controll
"1",
"--min_conf",
"1",
"--fee_rate",
&fee_rate,
];
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");
grin_wallet_controller::controller::owner_single_use(
wallet1.clone(),
mask1,
std::path::PathBuf::from(test_dir),
|api, m| {
let message = std::fs::read_to_string(&file_name)?;
let slate = api.slate_from_slatepack_message(m, message, vec![0])?;
assert_eq!(slate.fee_fields.fee(), expected_fee);
Ok(())
},
)?;
let arg_vec = vec![
"grin-wallet",
"-p",
@@ -262,6 +280,8 @@ fn parses_contract_options() {
"1",
"--min_conf",
"3",
"--fee_rate",
"2",
"--ttl_blocks",
"20",
])
@@ -271,6 +291,7 @@ fn parses_contract_options() {
let parsed =
grin_wallet::cmd::wallet_args::parse_contract_new_args(new_args, &account).unwrap();
assert_eq!(parsed.minimum_confirmations, 3);
assert_eq!(parsed.fee_rate, Some(2));
assert_eq!(parsed.ttl_blocks, Some(20));
let expected = grin_wallet_libwallet::contract::types::DEFAULT_MINIMUM_CONFIRMATIONS;
@@ -283,6 +304,7 @@ fn parses_contract_options() {
let parsed =
grin_wallet::cmd::wallet_args::parse_contract_new_args(new_args, &account).unwrap();
assert_eq!(parsed.minimum_confirmations, expected);
assert_eq!(parsed.fee_rate, None);
assert_eq!(parsed.ttl_blocks, None);
let args = app
.clone()
@@ -300,6 +322,35 @@ fn parses_contract_options() {
"Contract TTL must be at least 1 block".to_string()
)
);
for (rate, message) in [
("0", "Contract fee rate must be at least 1"),
("4294967296", "Contract fee rate is too large"),
] {
let args = app
.clone()
.get_matches_from_safe(vec![
"grin-wallet",
"contract",
"new",
"-s",
"1",
"--fee_rate",
rate,
])
.unwrap();
let new_args = args
.subcommand_matches("contract")
.unwrap()
.subcommand_matches("new")
.unwrap();
let err = grin_wallet::cmd::wallet_args::parse_contract_new_args(new_args, &account)
.err()
.expect("invalid contract fee rate accepted");
assert_eq!(
err,
grin_wallet::cmd::wallet_args::ParseError::ArgumentError(message.to_string())
);
}
let args = app
.clone()
@@ -309,14 +360,24 @@ fn parses_contract_options() {
let sign_args = contract.subcommand_matches("sign").unwrap();
let parsed = grin_wallet::cmd::wallet_args::parse_contract_setup_args(sign_args).unwrap();
assert_eq!(parsed.minimum_confirmations, None);
assert_eq!(parsed.fee_rate, None);
let args = app
.get_matches_from_safe(vec!["grin-wallet", "contract", "sign", "--min_conf", "3"])
.get_matches_from_safe(vec![
"grin-wallet",
"contract",
"sign",
"--min_conf",
"3",
"--fee_rate",
"2",
])
.unwrap();
let contract = args.subcommand_matches("contract").unwrap();
let sign_args = contract.subcommand_matches("sign").unwrap();
let parsed = grin_wallet::cmd::wallet_args::parse_contract_setup_args(sign_args).unwrap();
assert_eq!(parsed.minimum_confirmations, Some(3));
assert_eq!(parsed.fee_rate, Some(2));
}
#[test]