mirror of
https://github.com/jwinterm/grin-wallet.git
synced 2026-11-13 02:53:53 +01:00
598 lines
20 KiB
Rust
598 lines
20 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 building utility functions
|
|
|
|
use crate::backend::WalletBackend;
|
|
use crate::contract::selection::verify_selection_consistency;
|
|
use crate::contract::types::ContractSetupArgsAPI;
|
|
use crate::grin_core::libtx::tx_fee;
|
|
use crate::grin_keychain::{Identifier, Keychain};
|
|
use crate::grin_util::secp::key::SecretKey;
|
|
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, Transaction};
|
|
use uuid::Uuid;
|
|
|
|
/// Creates an initial TxLogEntry without input/output or kernel information
|
|
pub fn create_tx_log_entry(
|
|
slate: &Slate,
|
|
net_change: i64,
|
|
parent_key_id: Identifier,
|
|
log_id: u32,
|
|
) -> Result<TxLogEntry, Error> {
|
|
let log_type = if slate.num_participants == 1 {
|
|
TxLogEntryType::TxSelfSpend
|
|
} else {
|
|
if net_change > 0 {
|
|
TxLogEntryType::TxReceived
|
|
} else {
|
|
TxLogEntryType::TxSent
|
|
}
|
|
};
|
|
let mut t = TxLogEntry::new(parent_key_id.clone(), log_type, log_id);
|
|
// stored_tx is set in save_step, once we have signed and the transaction is written.
|
|
|
|
t.tx_slate_id = Some(slate.id);
|
|
if net_change > 0 {
|
|
t.amount_credited = net_change as u64;
|
|
} else {
|
|
t.amount_debited = -net_change as u64;
|
|
}
|
|
t.ttl_cutoff_height = match slate.ttl_cutoff_height {
|
|
0 => None,
|
|
n => Some(n),
|
|
};
|
|
|
|
Ok(t)
|
|
}
|
|
|
|
/// Update TxLogEntry with data from the sign step
|
|
/// `participant_index` is our entry in `slate.participant_data`
|
|
pub fn update_tx_log_entry<C, K>(
|
|
wallet: &mut WalletBackend<C, K>,
|
|
keychain: &K,
|
|
slate: &Slate,
|
|
context: &Context,
|
|
participant_index: usize,
|
|
tx_log_entry: &mut TxLogEntry,
|
|
) -> Result<(), Error>
|
|
where
|
|
C: NodeClient,
|
|
K: Keychain,
|
|
{
|
|
// This is expected to be called when we are signing the contract and have already contributed inputs & outputs
|
|
let parent_key_id = context.parent_key_id.clone();
|
|
let current_height = wallet.w2n_client().get_chain_tip()?.0;
|
|
// We have already contributed inputs and outputs so we know how much of each we contribute
|
|
tx_log_entry.num_outputs = context.output_ids.len();
|
|
tx_log_entry.num_inputs = context.input_ids.len();
|
|
tx_log_entry.fee = context.fee;
|
|
// Set kernel information
|
|
tx_log_entry.kernel_excess = Some(slate.calc_excess(keychain.secp())?);
|
|
tx_log_entry.kernel_lookup_min_height = Some(current_height);
|
|
|
|
// If we're sending and there's payment proof info in the slate added by recipient, store as well
|
|
if let Some(ref p) = slate.payment_proof {
|
|
if tx_log_entry.amount_debited > 0 {
|
|
let timestamp = p
|
|
.timestamp
|
|
.ok_or_else(|| Error::PaymentProof("Missing proof timestamp".to_string()))?;
|
|
// note we only use a single path for now
|
|
let sender_address_path = 0u32;
|
|
let sender_key = address::address_from_derivation_path(
|
|
keychain,
|
|
&parent_key_id,
|
|
sender_address_path,
|
|
)?;
|
|
let sender_address = OnionV3Address::from_private(&sender_key.0)?;
|
|
|
|
// We're looking for the OTHER party here, the recipient. The 'xor 1' pairing
|
|
// only holds for a two-party slate, so check that before indexing: a slate
|
|
// with a shorter participant list would otherwise panic here.
|
|
if slate.participant_data.len() != 2 {
|
|
return Err(Error::GenericError(format!(
|
|
"Expected 2 participants for a payment proof, found {}",
|
|
slate.participant_data.len()
|
|
)));
|
|
}
|
|
let sender = slate
|
|
.participant_data
|
|
.get(participant_index)
|
|
.ok_or(Error::ContextToIndex)?;
|
|
let recipient = slate
|
|
.participant_data
|
|
.get(participant_index ^ 1)
|
|
.ok_or(Error::ContextToIndex)?;
|
|
|
|
tx_log_entry.payment_proof = Some(StoredProofInfo {
|
|
receiver_address: p.receiver_address,
|
|
receiver_signature: p.promise_signature,
|
|
sender_address: sender_address.to_ed25519()?,
|
|
sender_address_path,
|
|
sender_signature: None,
|
|
// Filled as separate steps for now; could be merged into a general case
|
|
// once we know which nonces here belong to the recipient.
|
|
proof_type: Some(p.proof_type.as_u8()),
|
|
receiver_public_nonce: Some(recipient.public_nonce),
|
|
receiver_public_excess: Some(recipient.public_blind_excess),
|
|
timestamp: Some(timestamp),
|
|
memo: p.memo.clone(),
|
|
promise_signature: p.promise_signature,
|
|
sender_part_sig: sender.part_sig,
|
|
});
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Get net_change value. This is obtained either from the Context.net_change or the setup_args.net_change
|
|
pub fn get_net_change(
|
|
context: Option<&Context>,
|
|
setup_args_net_change: Option<i64>,
|
|
) -> Result<i64, Error> {
|
|
let mut expected_net_change: Option<i64> = setup_args_net_change;
|
|
if let Some(context) = context {
|
|
debug!("contract::sign => context found");
|
|
// We have a context so we must have agreed on a certain net_change value in Context.net_change.
|
|
// If we have both Context.net_change and setup_args.net_change, then they must be equal.
|
|
let ctx_net_change = context.get_net_change()?;
|
|
match expected_net_change {
|
|
Some(args_net_change) => {
|
|
if ctx_net_change != args_net_change {
|
|
return Err(Error::GenericError(format!(
|
|
"Expected net change mismatch! Context.net_change: {}, setup_args.net_change: {}",
|
|
ctx_net_change, args_net_change
|
|
)));
|
|
}
|
|
}
|
|
None => (),
|
|
}
|
|
expected_net_change = Some(ctx_net_change);
|
|
} else {
|
|
debug!("contract::utils::get_net_change => context not found")
|
|
}
|
|
|
|
// Fail if net_change was not passed to setup_args and was also not present in the context.
|
|
// This means it has not been explicitly agreed on and we require the user to pass it.
|
|
if expected_net_change.is_none() {
|
|
return Err(Error::GenericError(
|
|
"You did not agree on the expected net difference.".into(),
|
|
)
|
|
.into());
|
|
}
|
|
debug!(
|
|
"contract::utils::get_net_change => expected_net_change: {}",
|
|
expected_net_change.unwrap()
|
|
);
|
|
|
|
Ok(expected_net_change.unwrap())
|
|
}
|
|
|
|
/// Lock inputs and store the Context, TxLogEntry and OutputData atomically
|
|
/// Consumes the context and derives the signed state from our participant data
|
|
/// Signed transactions are written outside the database batch
|
|
pub fn save_step<C, K>(
|
|
w: &mut WalletBackend<C, K>,
|
|
keychain_mask: Option<&SecretKey>,
|
|
slate: &Slate,
|
|
mut context: Context,
|
|
step_added_outputs: bool,
|
|
) -> Result<(), Error>
|
|
where
|
|
C: NodeClient,
|
|
K: Keychain,
|
|
{
|
|
debug!(
|
|
"contract::utils::save_step => performing atomic update for slate_id: {}",
|
|
slate.id
|
|
);
|
|
// Phase 1 - precompute the data needed for atomic update
|
|
let parent_key_id = &context.parent_key_id;
|
|
let keychain = w.keychain(keychain_mask)?;
|
|
let participant_index = slate.find_index_matching_context(&keychain, &context)?;
|
|
let is_signed = slate.participant_data[participant_index].is_complete();
|
|
if is_signed {
|
|
// Verify part_sig before using it as signed state
|
|
slate.verify_part_sigs(keychain.secp())?;
|
|
}
|
|
let current_height = w.w2n_client().get_chain_tip()?.0;
|
|
// We are at step2 if we don't have context.log_id and we have signed the slate
|
|
let is_step2 = context.log_id.is_none() && is_signed;
|
|
|
|
let mut tx_log_entry = {
|
|
if context.log_id.is_none() {
|
|
// We create a new entry with log_id=0 and but replace it with the real id before committing
|
|
create_tx_log_entry(slate, context.get_net_change()?, parent_key_id.clone(), 0)?
|
|
} else {
|
|
let log_id = context.log_id.unwrap();
|
|
w.get_tx_log_entry_by_id(parent_key_id.clone(), log_id)?
|
|
.ok_or_else(|| Error::NotFoundErr(format!("Transaction log entry {}", log_id)))?
|
|
}
|
|
};
|
|
|
|
// Update TxLogEntry if we have signed the contract (we have data about the kernel)
|
|
if is_signed {
|
|
update_tx_log_entry(
|
|
w,
|
|
&keychain,
|
|
slate,
|
|
&context,
|
|
participant_index,
|
|
&mut tx_log_entry,
|
|
)?;
|
|
// Record where the transaction is stored, as internal::selection does for a
|
|
// standard send. store_tx below writes it under this name, and 'txs' reads the
|
|
// field to report whether the transaction data is held.
|
|
tx_log_entry.stored_tx = Some(format!("{}.grintx", slate.id));
|
|
}
|
|
// If we added outputs in this step, we have to create OutputData here because 'batch'
|
|
// takes the mutable ref and we can no longer call calc_commit_for_cache for output
|
|
let added_outputs = if !step_added_outputs {
|
|
vec![]
|
|
} else {
|
|
let mut output_data_xs: Vec<OutputData> = vec![];
|
|
// Create an OutputData entry for every created output
|
|
for (key_id, _, amount) in context.get_outputs() {
|
|
let commit = w.calc_commit_for_cache(keychain_mask, amount, &key_id)?;
|
|
let output_data = OutputData {
|
|
root_key_id: parent_key_id.clone(),
|
|
key_id: key_id.clone(),
|
|
mmr_index: None,
|
|
n_child: key_id.to_path().last_path_index(),
|
|
commit: commit,
|
|
value: amount,
|
|
status: OutputStatus::Unconfirmed,
|
|
height: current_height,
|
|
lock_height: 0,
|
|
is_coinbase: false,
|
|
tx_log_entry: None,
|
|
};
|
|
output_data_xs.push(output_data);
|
|
}
|
|
output_data_xs
|
|
};
|
|
|
|
// Phase 2 - atomically update Context, OutputData and TxLogEntry
|
|
let mut batch = w.batch(keychain_mask)?;
|
|
|
|
// Update TxLogEntry
|
|
if context.log_id.is_none() {
|
|
// If we just created the TxLogEntry, we have to assign it an id
|
|
let log_id = batch.next_tx_log_id(&parent_key_id)?;
|
|
tx_log_entry.id = log_id;
|
|
context.log_id = Some(log_id);
|
|
}
|
|
batch.save_tx_log_entry(tx_log_entry.clone(), &parent_key_id)?;
|
|
// Create OutputData entries and lock inputs if we added outputs at this step
|
|
if step_added_outputs {
|
|
// Create an OutputData entry for every created output
|
|
for mut output_data in added_outputs {
|
|
output_data.tx_log_entry = context.log_id;
|
|
batch.save(output_data)?;
|
|
}
|
|
// Lock inputs
|
|
for id in context.get_inputs() {
|
|
let mut coin = batch.get(&id.0, &id.1)?;
|
|
// At this point we already have context.log_id set
|
|
coin.tx_log_entry = context.log_id;
|
|
batch.lock_output(&mut coin)?;
|
|
}
|
|
}
|
|
|
|
// Update context
|
|
if is_signed && !is_step2 {
|
|
// NOTE: We MUST forget the context when we sign. Ideally, these two would be atomic or perhaps
|
|
// when we call slate::sigadd_partial_signaturen we could swap the secret key with a temporary one just to be safe.
|
|
// Keep the step2 context for contract view
|
|
batch.delete_private_context(slate.id.as_bytes())?;
|
|
} else {
|
|
batch.save_private_context(slate.id.as_bytes(), &context)?;
|
|
}
|
|
|
|
batch.commit()?;
|
|
|
|
// Confirm sec_key/sec_nonce are gone after signing, except for step2
|
|
if is_signed && !is_step2 {
|
|
match w.get_private_context(keychain_mask, slate.id.as_bytes()) {
|
|
Err(Error::NotFoundErr(_)) => {}
|
|
Err(e) => return Err(e),
|
|
Ok(_) => {
|
|
return Err(Error::GenericError(
|
|
"signing context was not removed after signing".into(),
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Store the signed transaction only after the wallet-state batch has committed (and
|
|
// the signing context is confirmed gone), so a DB failure can't leave the stored tx
|
|
// out of sync with wallet state. Matches the core convention (internal::selection).
|
|
//
|
|
// store_tx writes a file outside LMDB, so this cannot be part of the batch above and
|
|
// a window remains: the write can fail with the tx log entry and the input locks
|
|
// already committed. Making it atomic would mean holding stored transactions in the
|
|
// database, which is how every transaction in the wallet is kept, not just contracts.
|
|
// Until then the way out is to cancel the transaction, which releases the inputs
|
|
// without reading the stored tx. Nothing has been broadcast at this point: the error
|
|
// returns to the caller before it posts. Covered by
|
|
// wallet_contract_self_spend_cancel_missing_stored_tx.
|
|
if is_signed {
|
|
w.store_tx(&format!("{}", slate.id), slate.tx_or_err()?)?;
|
|
}
|
|
debug!("contract::utils::save_step => Atomic updated done");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Computes fees contribution for a participant
|
|
pub fn my_fee_contribution(
|
|
n_inputs: usize,
|
|
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 = 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 = 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.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
|
|
// 1/num_participants split so the contributions together cover the kernel cost.
|
|
let my_fee_fields = FeeFields::new(0, fee)?;
|
|
Ok(my_fee_fields)
|
|
}
|
|
|
|
/// Contracts need at least one participant for fee splitting
|
|
/// More than two remain disabled because of the known multi-party attack
|
|
pub(super) fn verify_num_participants(num_participants: u8) -> Result<(), Error> {
|
|
if !(1..=2).contains(&num_participants) {
|
|
return Err(Error::GenericError(format!(
|
|
"Unsupported num_participants: {} (expected 1 or 2)",
|
|
num_participants
|
|
)));
|
|
}
|
|
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);
|
|
if expected != slate.ttl_cutoff_height {
|
|
return Err(Error::GenericError(format!(
|
|
"Contract TTL changed from {} to {}",
|
|
expected, slate.ttl_cutoff_height
|
|
)));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Whether a transaction log entry shows that this wallet signed the slate.
|
|
pub(super) fn is_signed_tx(tx: &TxLogEntry, slate_id: Uuid) -> bool {
|
|
tx.tx_slate_id == Some(slate_id) && tx.kernel_excess.is_some()
|
|
}
|
|
|
|
/// Returns an error if the slate has already been signed (in our local database). Even if the
|
|
/// result is Ok, it's still possible it was signed but we don't have the data about it locally.
|
|
pub fn verify_not_signed<C, K>(w: &mut WalletBackend<C, K>, slate_id: Uuid) -> Result<(), Error>
|
|
where
|
|
C: NodeClient,
|
|
K: Keychain,
|
|
{
|
|
// If we have a transaction log entry for that slatepack that has a kernel value, then
|
|
// we have already signed this slate.
|
|
for tx in w.tx_log_iter()? {
|
|
let tx = tx?;
|
|
if is_signed_tx(&tx, slate_id) {
|
|
debug!("contract::utils::verify_not_signed => The slate has already been signed.");
|
|
return Err(Error::GenericError(format!(
|
|
"Slate with id:{} has already been signed.",
|
|
slate_id
|
|
)));
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Compares the setup args provided at call with those in the Context and checks whether they conflict.
|
|
/// This is relevant to see if there's any conflict in the arguments provided at step1 with step3.
|
|
pub fn verify_setup_args_consistency(
|
|
ctx_setup_args: &ContractSetupArgsAPI,
|
|
cur_setup_args: &ContractSetupArgsAPI,
|
|
) -> Result<(), Error> {
|
|
// Compare net_change
|
|
if ctx_setup_args.net_change.unwrap() != cur_setup_args.net_change.unwrap() {
|
|
return Err(Error::GenericError(format!(
|
|
"Inconsistent net change. Ctx net_change:{}, Current net_change: {}",
|
|
ctx_setup_args.net_change.unwrap(),
|
|
cur_setup_args.net_change.unwrap()
|
|
)));
|
|
}
|
|
// Compare num_participants
|
|
if ctx_setup_args.num_participants != cur_setup_args.num_participants {
|
|
return Err(Error::GenericError(format!(
|
|
"Inconsistent num_participants. Ctx num_participants:{}, Current num_participants: {}",
|
|
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.
|
|
|
|
// Compare OutputSelectionArgs
|
|
verify_selection_consistency(
|
|
&ctx_setup_args.selection_args,
|
|
&cur_setup_args.selection_args,
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Get the parent_key_id for a given wallet instance and src_acct_name.
|
|
/// Errors on an unknown account name rather than silently falling back to the
|
|
/// active account.
|
|
pub fn parent_key_for<C, K>(
|
|
w: &mut WalletBackend<C, K>,
|
|
src_acct_name: Option<&String>,
|
|
) -> Result<Identifier, Error>
|
|
where
|
|
C: NodeClient,
|
|
K: Keychain,
|
|
{
|
|
let parent_key_id = match src_acct_name {
|
|
Some(d) => match w.get_acct_path(d.clone())? {
|
|
Some(p) => p.path,
|
|
None => return Err(Error::UnknownAccountLabel(d.clone())),
|
|
},
|
|
None => w.parent_key_id(),
|
|
};
|
|
Ok(parent_key_id)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn fee_contribution_kernel_split() {
|
|
let (n_inputs, n_outputs, n_kernels) = (1, 1, 1);
|
|
let base = tx_fee(n_inputs, n_outputs, 0);
|
|
let kernel_cost = tx_fee(0, 0, n_kernels);
|
|
|
|
// A single participant (self-spend) pays the whole kernel cost.
|
|
let solo = my_fee_contribution(n_inputs, n_outputs, n_kernels, 1)
|
|
.unwrap()
|
|
.fee();
|
|
assert_eq!(solo, base + kernel_cost);
|
|
|
|
// Two participants each pay ceil(kernel_cost / 2) ...
|
|
let half = my_fee_contribution(n_inputs, n_outputs, n_kernels, 2)
|
|
.unwrap()
|
|
.fee();
|
|
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());
|
|
assert!(verify_num_participants(2).is_ok());
|
|
for count in [0, 3] {
|
|
let err = verify_num_participants(count).unwrap_err();
|
|
assert!(matches!(
|
|
err,
|
|
Error::GenericError(ref msg)
|
|
if msg == &format!(
|
|
"Unsupported num_participants: {} (expected 1 or 2)",
|
|
count
|
|
)
|
|
));
|
|
assert!(my_fee_contribution(1, 1, 1, count).is_err());
|
|
}
|
|
}
|
|
}
|