Files
grin-wallet/libwallet/src/contract/selection.rs
T
Yeastplume cdd63fb565 [CONTRACTS] Contracts branch update for 1.80 (#718)
* Simple contracts restructured v3 (#675)

* Add prototype contract implementation

Lacks:
- Payment proofs (add early payment proofs)
- Tests
- Better structure

* Separate contract utilities

* Simplify the main setup/sign code flow

* Remove commented initial_sec_key assignment

* Simplify sign::compute function

* Add logic for "removal" of secret keys from the context

* Further simplify a bit sign and setup

* No need for mutable context when adding outputs

* Refactor the commented code (lol)

* Refactor a bit

* tmp

---------

Co-authored-by: oryhp <gtrphyro@gmail.com>

* contract test fixes (#676)

* [Contracts] Experimental Slate v5 (#677)

* beginning to add and modify slate version 5

* completion of conversions from V5 to V4 Slate

* timestamp and memo fields

* upgrade/downgrade serialization of v5 slates

* add v5 binary slate versions + start of tests

* add bin slate ser/deser to tests

* ensure serialization of timestamp always excludes milliseconds

* start to update v5 tests, update v5 documentation

* add fn to generate populated internal slate for conversion testing

* add basic tests to convert all slate versions

* update

* commit cargo.lock

* [WIP] [Contracts] Early payment proofs (#681)

* add types and beginnings of signature utils

* add proof serialization

* serialisation of proof data + signature operation

* add serialization type for invoice proof + separate bin wrapper version

* add witness data + serializion to invoice payment proof, insert verfication functions in place in order to begin verification testing

* tests and infrastructure in place for validation

* verification of promise sig

* added verification of promise signature, infrastructure up to the point where a signature must be subtracted

* attempting to figure out differences between recipient nonce that's getting stored and calculated recipient nonce

* implementation of witness verification function, retrieve relevant values and re-validate derived recipient partial signature

* move stored portion of invoice proof into core types for storage, need to rename invoice proof

* define/refine the stored portion of payment proofs type 2?

* Folding all proof data into tx log entry storage

* back to importing master

* remove cargo files from diffs

* remove a lot of extra debug output

* return proof witness as part of proof retrieval, define json serialization of invoice proof + witness fields

* finish adding verification steps to foreign API

* remove redundant promise sig field

* move lcation of sign/verify calls

* Replace Azure Pipelines with Github Actions (#688)

* Update CI Badge on README.MD (#690)

* Trigger CI on push and pull request (#693)

* Update versioning to 5.2.0-beta.1 against grin 5.2.0-beta.3 (#691)

* update versioning to 5.2.0-beta.1 against grin 5.2.0-beta.3

* tweak for CI trigger

---------

Co-authored-by: Quentin Le Sceller <q.lesceller@gmail.com>

---------

Co-authored-by: Quentin Le Sceller <q.lesceller@gmail.com>

* fix Cargo.lock

* Contracts - Test fixes + warning cleanups (#694)

* clean up warnings in libwallet crate

* clean up warnings in controller crate

* update all contract tests with awareness of new proof structure

* added explicit payment proof tests for both rsr and srs (#695)

* Basic mwmixnet send (#696)

* integrating onion library

* updates and changes to support newly included mwmixnet types

* add (incorrect) owner api function

* turn off test for now

* switch working grin branch to master

* fix doctests for build

* update cargo lock in attempt to fix croaring build on CI server

* update cargo lock with upstream thiserror crate

* update test dependency for croaring

* [Contracts] Add self spend transaction state (#697)

* Add self spend transaction state

* subtle errors with output states and tx lookups - fixes

* [Contracts] Slatepack v5 Deserialization fix (#698)

* add V5 deserialization test + fixes

* clarify comment

* upwrap fix during v4 deserialization

* further unwrap removal

* update cargo lock, use latest grin packages

* addition of self-spend cancel + tests (#699)

* add tests + legacy self send cancel (#702)

* [Contracts] Cancel self-spend TX Pt. 2 (#703)

* add tests + legacy self send cancel

* add missing file

* Update grin to latest master, change versioning label to include contracts (#712)

* Add owner_api_listen_interface as hidden configuration field (#714)

* updates for rust 1.80

* updates for rust 1.80

---------

Co-authored-by: oryhp <gtrphyro@gmail.com>
Co-authored-by: Quentin Le Sceller <q.lesceller@gmail.com>
2024-08-27 12:30:22 +01:00

597 lines
22 KiB
Rust

// 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.
//! Contract coin selection functions
use crate::contract::types::{ContractSetupArgsAPI, OutputSelectionArgs};
use crate::contract::utils::my_fee_contribution;
use crate::grin_core::core::amount_to_hr_string;
use crate::grin_keychain::{Identifier, Keychain};
use crate::types::{NodeClient, WalletBackend};
use crate::{Error, OutputData};
use grin_core::core::FeeFields;
/// Prepares inputs & outputs that satisfy `Σmy_inputs >= Σmy_outputs + my_fee_cost` taking into account selection args
pub fn prepare_outputs<'a, T: ?Sized, C, K>(
w: &mut T,
parent_key_id: &Identifier,
current_height: u64,
setup_args: &ContractSetupArgsAPI,
committed_fee: Option<FeeFields>,
) -> Result<(Vec<OutputData>, Vec<u64>, FeeFields), Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
// Find available inputs
let mut eligible_inputs = find_eligible(w, parent_key_id, current_height)?;
// Select which inputs to use to satisfy the equation
compute(setup_args, committed_fee, &mut eligible_inputs)
}
/// Find all inputs eligible to spend
pub fn find_eligible<'a, T: ?Sized, C, K>(
w: &mut T,
parent_key_id: &Identifier,
current_height: u64,
) -> Result<Vec<OutputData>, Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
// Find eligible inputs in the wallet
let eligible_inputs = w
.iter()
.filter(|out| out.root_key_id == *parent_key_id && out.eligible_to_spend(current_height, 1))
.collect::<Vec<OutputData>>();
Ok(eligible_inputs)
}
// Given a list of inputs, an optional committed fee and setup args, compute which inputs to use, what amount outputs to make and fee cost
pub fn compute(
setup_args: &ContractSetupArgsAPI,
committed_fee: Option<FeeFields>,
inputs: &mut Vec<OutputData>,
) -> Result<(Vec<OutputData>, Vec<u64>, FeeFields), Error>
where
{
let (inputs, fee) = select_inputs(setup_args, committed_fee, inputs)?;
let output_amounts = build_output_amount_list(
inputs.clone().iter().map(|out| out.value).sum::<u64>(),
fee.fee(),
setup_args,
);
Ok((inputs, output_amounts, fee))
}
// Given a list of inputs, an optional committed fee and setup args, compute which inputs to use
fn select_inputs(
setup_args: &ContractSetupArgsAPI,
committed_fee: Option<FeeFields>,
inputs: &mut Vec<OutputData>,
) -> Result<(Vec<OutputData>, FeeFields), Error>
where
{
// We use 'lhs' and 'rhs' to denote the amounts on the left/right-hand side of the equation.
// To simulate receive/payment value we:
// - add positive net_change to the 'lhs' for the receiver (to simulate sender's input)
// - add positive net_change to the 'rhs' for the sender (to simulate receiver's output)
// For either party, the following MUST hold for the inputs the function returns:
// Σmy_inputs >= Σmy_outputs + my_fee_cost
// Each party later balances the equation by adding an additional output (change output or receiver output)
let net_change = setup_args.net_change.unwrap();
let custom_outputs_amount_sum = setup_args.selection_args.sum_output_amounts();
let pay_amount = if net_change < 0 {
net_change.abs() as u64
} else {
0
};
// Add the amount we pay and the custom outputs to rhs of the equation
let rhs = pay_amount + custom_outputs_amount_sum;
let required_inputs = setup_args.selection_args.required_inputs();
let is_payjoin = setup_args.selection_args.is_payjoin();
let is_self_spend = setup_args.num_participants == 1;
debug!(
"contract::selection::selecting inputs: num_participants: {}, min_input_amount: {}, is_payjoin: {}",
setup_args.num_participants, rhs, is_payjoin
);
// We don't try to contribute an input only in the case where we have multiple participants
// where we are on the receiving end and we don't want to do a payjoin
if !is_self_spend && (pay_amount == 0 && !is_payjoin) {
return Ok((
vec![],
my_fee_contribution(
0,
setup_args.selection_args.num_custom_outputs() + 1,
1,
setup_args.num_participants,
)?,
));
}
// NOTE: that these are inputs that MUST be selected. We should lock the inputs if they're
// required to minimize any potential race conditions.
let must_use_list = required_inputs.unwrap_or(vec![]);
if must_use_list.len() > 0 {
// Sort the inputs first by the ones listed in the use_inputs and then by value
inputs.sort_by_key(|out| {
(
// We have to negate the boolean to prioritize truthy values because
// false is 0 and hence would be sorted before truthy entries
!(out.commit.is_some()
&& must_use_list.contains(&&out.commit.as_ref().unwrap()[..])),
out.value,
)
});
} else {
// Sort the inputs only by value
inputs.sort_by_key(|out| out.value);
}
// NOTE: Since we sort by value increasingly, if we hold any 0-value inputs, they will all be used if we're the sender
// or a single one if we're the receiver doing a payjoin.
// If we are the receiver, we pretend we have a virtual input from the sender (for which we don't pay the fees) so we can easily
// test that lhs >= rhs and see if we will be able to satisfy equation. We simulate this by starting with lhs = net_change.
let mut lhs = 0;
if net_change > 0 {
lhs = net_change as u64; // TODO: check bounds
}
// We want to count how many inputs we've picked _so far_. This is used to prevent picking
// all 0*H +r*G outputs when we call with min_input_amount=0 and want just a payjoin.
let mut n_inputs = 0;
let mut must_use_list_cnt: u32 = 0;
let my_num_outputs = setup_args.selection_args.num_custom_outputs() + 1;
// If we have already committed to a fee (context.fee) then set this as our "minimum" fee. The reason we have to
// do this is to avoid solving the equation for less than the committed fee. We have to guarantee the inputs we take
// are enough to cover the committed fee. At the end of selection, we check that the fees for the selection were not
// higher than the fee value we committed to.
let mut my_fee = if committed_fee.is_some() {
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).unwrap()
// FeeFields::zero()
};
// NOTE: This always takes at least one input if it is available. We take the inputs we must take and then we take
// inputs until we fulfill Σmy_inputs >= Σmy_outputs + my_fee_cost
let selected_inputs = inputs
.iter()
.take_while(|out| {
// Take the commitment if it is listed as one of those we MUST take
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)
.unwrap();
// 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)
.unwrap();
// 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;
}
// If we don't have a "must take" input, have contributed an input and have enough to balance the equation, we can stop
let can_finish = lhs >= (rhs + fee_without.fee()) && n_inputs > 0 && !must_take;
if can_finish {
return false;
}
// Take the commitment if `lhs < rhs+fees_with` (or if we have not yet taken an input - payjoins)
let should_take = lhs < (rhs + fee_with.fee()) || n_inputs == 0;
let res = must_take || should_take;
if res {
lhs += out.value;
n_inputs += 1;
// Update the fee cost if we decided to take the input
my_fee = fee_with;
if must_take {
must_use_list_cnt += 1;
}
}
debug!(
"contract::selection::select_inputs => out_value:{}, new my_inputs_sum:{}",
out.value, lhs
);
res
})
.cloned()
.collect::<Vec<OutputData>>();
// Return an error if the fee computed is larger than the committed fee
if committed_fee.is_some() && my_fee.fee() > committed_fee.unwrap().fee() {
// TODO: Return a specific Fee estimation error and suggest the user to cancel the transaction
let msg = format!(
"Fee computed ({}) is larger than the committed fee ({})",
my_fee.fee(),
committed_fee.unwrap().fee()
);
return Err(Error::GenericError(msg.into()).into());
}
// Check that the inputs we picked are enough to cover all our output amounts and fees
// asserts that Σmy_inputs >= Σmy_outputs + my_fee_cost
if lhs < rhs + my_fee.fee() {
let total = inputs.iter().fold(0, |acc, x| acc + x.value);
debug!("Not enough funds. Total funds eligible to spend: {}, needed: {}. Fee cost for this transaction: {}", total, rhs+my_fee.fee(), my_fee.fee());
return Err(Error::NotEnoughFunds {
available: total,
available_disp: amount_to_hr_string(total, false),
needed: rhs + my_fee.fee(),
needed_disp: amount_to_hr_string(rhs + my_fee.fee(), false),
}
.into());
// return Err(ErrorKind::GenericError(msg.into()).into());
}
// Assert that all the use_inputs have been selected
if must_use_list.len() != must_use_list_cnt as usize {
let msg = format!(
"We have not found all the inputs that have been requested. {}, found only: {}",
setup_args.selection_args.use_inputs.as_ref().unwrap(),
must_use_list_cnt
);
return Err(Error::GenericError(msg.into()).into());
}
debug!(
"contract::selection::select_inputs => selected_inputs: {:#?}",
selected_inputs
);
// We are returning a set of inputs for which `Σmy_inputs >= Σmy_outputs + my_fee_cost` holds
Ok((selected_inputs, my_fee))
}
fn build_output_amount_list(
my_input_sum: u64,
my_fee_cost: u64,
setup_args: &ContractSetupArgsAPI,
) -> Vec<u64> {
let expected_net_change = setup_args.net_change.unwrap();
let mut my_output_amounts = setup_args.selection_args.output_amounts();
let custom_outputs_sum = my_output_amounts.iter().sum::<u64>();
// We know that `Σmy_inputs >= Σmy_outputs + my_fee_cost` holds so we balance the equation by adding
// an additional output holding the missing amount (change output or receiver output)
// TODO: check bounds when casting.
let my_change_output_amount =
(my_input_sum - custom_outputs_sum) as i64 + expected_net_change - my_fee_cost as i64;
// TODO: Check if it's even possible for change output to be negative (it shouldn't be if the equation is correct)
if my_change_output_amount < 0 {
panic!(
"contract::selection::build_output_amount_list => ERROR: This should never happen!!! Values: my_input_sum: {}, expected_net_change: {}, my_fee_cost: {}",
my_input_sum as i64, expected_net_change, my_fee_cost as i64
);
}
// Add our change/receiver output (which can be a zero-value output) to the list of outputs
my_output_amounts.push(my_change_output_amount as u64);
debug!(
"contract::selection::build_output_amount_list => inputs sum: {}, my_output_amounts:{:#?}",
my_input_sum, my_output_amounts
);
my_output_amounts
}
/// Compares the output selection args provided at call with those from Context and checks whether they conflict
pub fn verify_selection_consistency(
ctx_args: &OutputSelectionArgs,
cur_args: &OutputSelectionArgs,
) -> Result<(), Error> {
// We can't define a selection strategy if we've already done the setup phase. We only allow to pass either the
// default or exactly the same strategy we defined when doing the setup phase.
// TODO: Test that this works. Perhaps we'd have to define how to compare the two?
if cur_args != ctx_args && cur_args != &OutputSelectionArgs::default() {
panic!("Can't define selection args now because we've already done the setup phase. ctx_selection_args:{:#?}, cur_selection_args:{:#?}", ctx_args, cur_args);
}
// NOTE: The logic above isn't perfect. This is because the user could define arguments that are the default. In this case
// we'd simply silently use the arguments provided in the setup phase. This could be confusing for the user.
Ok(())
}
// Tests
#[cfg(test)]
mod tests {
use super::*;
use crate::grin_keychain::{Identifier, IDENTIFIER_SIZE};
use crate::OutputStatus;
fn _create_output_data_for(amounts: Vec<u64>) -> Vec<OutputData> {
let mut rv: Vec<OutputData> = vec![];
for (idx, amount) in amounts.iter().enumerate() {
let identifier = [0u8; IDENTIFIER_SIZE];
let key_id = Identifier::from_bytes(&identifier);
rv.push(OutputData {
// The identifiers here don't make sense, but they're not needed for testing
root_key_id: key_id.clone(),
key_id: key_id.clone(),
n_child: key_id.clone().to_path().last_path_index(),
mmr_index: None,
commit: Some(format!("{}{}", "abc", idx.to_string())),
value: *amount,
status: OutputStatus::Unspent,
height: 1,
lock_height: 0,
is_coinbase: false,
tx_log_entry: None,
});
}
rv
}
#[test]
fn sender_no_inputs() {
// net_change=-1, no inputs, no fee committed => NotEnoughFunds
let setup_args = ContractSetupArgsAPI {
net_change: Some(-1_000_000_000),
..Default::default()
};
let expected = Error::NotEnoughFunds {
available: 0,
available_disp: amount_to_hr_string(0, false),
needed: 1_000_000_000 + my_fee_contribution(0, 1, 1, 2).unwrap().fee(),
needed_disp: amount_to_hr_string(
1_000_000_000 + my_fee_contribution(0, 1, 1, 2).unwrap().fee(),
false,
),
};
let result = compute(&setup_args, None, &mut vec![]);
assert_eq!(result.err().unwrap(), expected);
}
#[test]
fn sender_not_enough_funds_for_fee() {
// net_change=-3, inputs=[2, 1], no fee committed => NotEnoughFunds because we can't pay for fees
let setup_args = ContractSetupArgsAPI {
net_change: Some(-3_000_000_000),
..Default::default()
};
let expected = Error::NotEnoughFunds {
available: 3_000_000_000,
available_disp: amount_to_hr_string(3_000_000_000, false),
needed: 3_000_000_000 + my_fee_contribution(2, 1, 1, 2).unwrap().fee(),
needed_disp: amount_to_hr_string(
3_000_000_000 + my_fee_contribution(2, 1, 1, 2).unwrap().fee(),
false,
),
};
let mut inputs = _create_output_data_for(vec![2_000_000_000, 1_000_000_000]);
let result = compute(&setup_args, None, &mut inputs);
assert_eq!(result.err().unwrap(), expected);
}
#[test]
fn sender_happy_path() {
// net_change=-3, inputs=[3, 2, 1, 2], no fee committed => Ok([1, 2, 2], fees)
let setup_args = ContractSetupArgsAPI {
net_change: Some(-3_000_000_000),
..Default::default()
};
let inputs = _create_output_data_for(vec![
3_000_000_000,
2_000_000_000,
1_000_000_000,
2_000_000_000,
]);
// We expect 3 inputs with amounts 1, 2, 2
let expected_inputs = vec![&inputs[2], &inputs[1], &inputs[3]];
let expected_fee = my_fee_contribution(3, 1, 1, 2).unwrap();
let expected_output_amounts = vec![
// we expect a single output with change holding 5 - 3 - fees
(5_000_000_000 as i64 + (setup_args.net_change.unwrap())) as u64 - expected_fee.fee(),
];
let result = compute(&setup_args, None, &mut inputs.clone()).unwrap();
let result_ref = (
result.0.iter().collect::<Vec<&OutputData>>(),
result.1,
result.2,
);
assert_eq!(
result_ref,
(expected_inputs, expected_output_amounts, 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)
let setup_args = ContractSetupArgsAPI {
net_change: Some(-3_000_000_000),
..Default::default()
};
let inputs = _create_output_data_for(vec![
my_fee_contribution(2, 1, 1, 2).unwrap().fee(),
3_000_000_000,
]);
let expected_inputs = inputs.clone(); // we expect both inputs in the same order
let expected_fee = my_fee_contribution(2, 1, 1, 2).unwrap();
let expected_output_amounts = vec![0]; // we expect a change output of 0-value
let result = compute(&setup_args, None, &mut inputs.clone()).unwrap();
assert_eq!(
result,
(expected_inputs, expected_output_amounts, expected_fee)
);
}
#[test]
fn receiver_payjoin_exact() {
// net_change=-my_fees(1, 1), inputs=[my_fees(1, 0)], no fee committed => Ok([3, my_fees(2, 1)], fees)
let setup_args = ContractSetupArgsAPI {
// we expect to receive exactly our fee contribution my_fees(1, 1)
net_change: Some(my_fee_contribution(1, 1, 1, 2).unwrap().fee() as i64),
..Default::default()
};
let inputs = _create_output_data_for(vec![0, 1_000_000_000]); // we have a 0-value and 1 grin input
let expected_inputs = vec![&inputs[0]]; // we expect to use the 0-value input
let expected_fee = my_fee_contribution(1, 1, 1, 2).unwrap();
let expected_output_amounts = vec![0]; // we expect a change output of 0-value
let result = compute(&setup_args, None, &mut inputs.clone()).unwrap();
let result_ref = (
result.0.iter().collect::<Vec<&OutputData>>(),
result.1,
result.2,
);
assert_eq!(
result_ref,
(expected_inputs, expected_output_amounts, expected_fee)
);
}
#[test]
fn receiver_no_payjoin() {
let setup_args = ContractSetupArgsAPI {
// we expect to receive exactly our fee contribution my_fees(1, 1)
net_change: Some(3_000_000_000),
selection_args: OutputSelectionArgs {
use_inputs: None,
..Default::default()
},
..Default::default()
};
let inputs = _create_output_data_for(vec![1_000_000_000]);
let expected_inputs = vec![];
let expected_fee = my_fee_contribution(0, 1, 1, 2).unwrap();
let expected_output_amounts = vec![
// we expect a single output with change holding 3 - fees
(setup_args.net_change.unwrap() as u64) - expected_fee.fee(),
];
let result = compute(&setup_args, None, &mut inputs.clone()).unwrap();
assert_eq!(
result,
(expected_inputs, expected_output_amounts, expected_fee)
);
}
#[test]
fn sender_use_inputs_ok() {
let setup_args = ContractSetupArgsAPI {
// we expect to receive exactly our fee contribution my_fees(1, 1)
net_change: Some(-2_000_000_000),
selection_args: OutputSelectionArgs {
use_inputs: Some(String::from("abc0,abc2,abc3")),
..Default::default()
},
..Default::default()
};
let inputs = _create_output_data_for(vec![
1_000_000_000, // abc0
2_000_000_000,
3_000_000_000, // abc2
4_000_000_000, // abc3
]);
let expected_inputs = vec![&inputs[0], &inputs[2], &inputs[3]];
let expected_fee = my_fee_contribution(3, 1, 1, 2).unwrap();
let expected_output_amounts = vec![
// we expect a single output with change holding 3 - fees
(8_000_000_000 as i64 + setup_args.net_change.unwrap()) as u64 - expected_fee.fee(),
];
let result = compute(&setup_args, None, &mut inputs.clone()).unwrap();
let result_ref = (
result.0.iter().collect::<Vec<&OutputData>>(),
result.1,
result.2,
);
assert_eq!(
result_ref,
(expected_inputs, expected_output_amounts, expected_fee)
);
}
#[test]
fn sender_use_inputs_happy_err() {
let setup_args = ContractSetupArgsAPI {
net_change: Some(-2_000_000_000),
selection_args: OutputSelectionArgs {
// there is no abc5 input
use_inputs: Some(String::from("abc0,abc2,abc5")),
..Default::default()
},
..Default::default()
};
let inputs = _create_output_data_for(vec![
1_000_000_000, // abc0
2_000_000_000,
3_000_000_000, // abc2
4_000_000_000,
]);
let msg = format!(
"We have not found all the inputs that have been requested. abc0,abc2,abc5, found only: 2"
);
let expected_err = Error::GenericError(msg.into()).into();
let result = compute(&setup_args, None, &mut inputs.clone());
assert_eq!(result.err().unwrap(), expected_err);
}
#[test]
fn sender_make_outputs_ok() {
let setup_args = ContractSetupArgsAPI {
net_change: Some(-2_000_000_000),
selection_args: OutputSelectionArgs {
// there is no abc5 input
make_outputs: Some(String::from("1,3")),
..Default::default()
},
..Default::default()
};
let inputs = _create_output_data_for(vec![
1_000_000_000,
2_000_000_000,
3_000_000_000,
4_000_000_000,
]);
// we expect all to be used (2+1+3+fees)
let expected_inputs = inputs.clone();
let expected_fee = my_fee_contribution(4, 3, 1, 2).unwrap();
let expected_output_amounts = vec![
1_000_000_000u64,
3_000_000_000u64,
// change output
((10_000_000_000u64 - 4_000_000_000u64) as i64 + setup_args.net_change.unwrap()) as u64
- expected_fee.fee(),
];
let result = compute(&setup_args, None, &mut inputs.clone()).unwrap();
assert_eq!(
result,
(expected_inputs, expected_output_amounts, expected_fee)
);
}
/*
Tests to add:
- compute_receiver_invariant - test that receiving_amount - my_fees >= 0 (to prevent going into negative accidentally)
- compute_sender_invariant - test that -send_amount - my_fees < 0 (do you need this one and is it correct?)
- compute_receiver_payjoin_negative_fee - could the receiver receive a negative amount through fees? but the thing
would go through because they made a payjoin so they could pay for the fees?
- compute_receiver_omit_payjoin - we can't contribute an input, but have enough for other fees
- compute_make_outputs_fee_err - fail due to not enough funds for fees
- compute_make_outputs_sum_err - is this even possible?
- compute_zero_value_outputs_sender - sender uses all 0-value outputs when sending
- compute_zero_value_inputs_receiver - receiver uses 0-value inputs in payjoin
- test_fee_committed_err - we have already committed to a certain fee which we no longer satisfy
- think if we should have sender/receiver separate testing
- sender_use_all_features
- coinbase output cases
- validate --make-outputs has all positive u64 numbers
*/
}