mirror of
https://github.com/jwinterm/grin-wallet.git
synced 2026-10-17 15:21:39 +02:00
add contract ttl
This commit is contained in:
@@ -1690,14 +1690,16 @@ pub struct ContractNewArgs {
|
||||
pub make_outputs: Option<Vec<u64>>,
|
||||
/// Minimum number of confirmations required for an input
|
||||
pub minimum_confirmations: u64,
|
||||
/// Blocks until wallets should stop signing the contract
|
||||
pub ttl_blocks: Option<u64>,
|
||||
/// 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>,
|
||||
/// Select outputs early
|
||||
pub add_outputs: bool,
|
||||
}
|
||||
|
||||
impl ContractNewArgs {
|
||||
@@ -1720,6 +1722,7 @@ impl ContractNewArgs {
|
||||
fn to_api_args(&self) -> Result<ContractNewArgsAPI, Error> {
|
||||
let net_change = self.get_net_change()?;
|
||||
Ok(ContractNewArgsAPI {
|
||||
ttl_blocks: self.ttl_blocks,
|
||||
setup_args: ContractSetupArgsAPI {
|
||||
src_acct_name: match self.src_acct_name.as_ref() {
|
||||
Some(v) => Some(v.to_string()),
|
||||
|
||||
@@ -654,6 +654,11 @@ fn contract_view_table(
|
||||
table.add_row(row![bFC->"Slate Id", bGC->slate.id]);
|
||||
table.add_row(row![bFC->"State", bGC->slate.state]);
|
||||
table.add_row(row![bFC->"Encrypted for This Wallet", bGC->yes_no(was_encrypted)]);
|
||||
let deadline = match slate.ttl_cutoff_height {
|
||||
0 => "None".to_string(),
|
||||
height => height.to_string(),
|
||||
};
|
||||
table.add_row(row![bFC->"Signing Deadline Height", bGC->deadline]);
|
||||
table.add_row(row![bFC->"Participants", bGC->view.num_participants]);
|
||||
table.add_row(row![bFC->"Signatures", bGC->view.num_sigs]);
|
||||
table.add_row(row![bFC->"Transfer Amount", bGC->amount_to_hr_string(slate.amount, false)]);
|
||||
@@ -787,6 +792,7 @@ mod tests {
|
||||
global::set_local_chain_type(global::ChainTypes::AutomatedTesting);
|
||||
let mut slate = Slate::blank(2, false);
|
||||
slate.amount = 1_000_000_000;
|
||||
slate.ttl_cutoff_height = 42;
|
||||
slate.fee_fields = FeeFields::new(0, 4_000_000).unwrap();
|
||||
let input_commit = Commitment::from_vec(vec![3]);
|
||||
let output_commit = Commitment::from_vec(vec![4]);
|
||||
@@ -810,6 +816,7 @@ mod tests {
|
||||
let table = contract_view_table(&slate, &view, true);
|
||||
|
||||
assert_eq!(table_value(&table, "Encrypted for This Wallet"), "Yes");
|
||||
assert_eq!(table_value(&table, "Signing Deadline Height"), "42");
|
||||
assert_eq!(table_value(&table, "Transfer Amount"), "1.000000000");
|
||||
assert_eq!(
|
||||
table_value(&table, "Current Transaction Fee"),
|
||||
@@ -825,6 +832,9 @@ mod tests {
|
||||
|
||||
let table = contract_view_table(&slate, &view, false);
|
||||
assert_eq!(table_value(&table, "Encrypted for This Wallet"), "No");
|
||||
slate.ttl_cutoff_height = 0;
|
||||
let table = contract_view_table(&slate, &view, false);
|
||||
assert_eq!(table_value(&table, "Signing Deadline Height"), "None");
|
||||
|
||||
slate.tx = Some(Transaction::new(
|
||||
Inputs::CommitOnly(vec![input_commit.into()]),
|
||||
|
||||
@@ -34,7 +34,7 @@ use std::path::PathBuf;
|
||||
/// contract accounts testing when switching between accounts during transaction building
|
||||
fn contract_accounts_switch_impl(test_dir: &'static str) -> Result<(), libwallet::Error> {
|
||||
// create two wallets with some extra accounts and don't mine anything in them
|
||||
let (wallets, _chain, stopper, _bh) = create_wallets(
|
||||
let (wallets, _chain, stopper, bh) = create_wallets(
|
||||
vec![
|
||||
vec![("default", 0), ("account1", 1), ("account2", 2)],
|
||||
vec![("default", 0), ("account1", 3), ("account2", 4)],
|
||||
@@ -110,6 +110,7 @@ fn contract_accounts_switch_impl(test_dir: &'static str) -> Result<(), libwallet
|
||||
|api, m| {
|
||||
// Send wallet inititates a standard transaction with --send=5
|
||||
let args = &ContractNewArgsAPI {
|
||||
ttl_blocks: Some(20),
|
||||
setup_args: ContractSetupArgsAPI {
|
||||
selection_args: common::contract_selection_args(),
|
||||
net_change: Some(-5_000_000_000),
|
||||
@@ -122,6 +123,7 @@ fn contract_accounts_switch_impl(test_dir: &'static str) -> Result<(), libwallet
|
||||
},
|
||||
)?;
|
||||
assert_eq!(slate.state, SlateState::Standard1);
|
||||
assert_eq!(slate.ttl_cutoff_height, bh + 20);
|
||||
|
||||
// Receiver gets their coins on account2 where they can payjoin
|
||||
{
|
||||
@@ -146,16 +148,24 @@ fn contract_accounts_switch_impl(test_dir: &'static str) -> Result<(), libwallet
|
||||
)?;
|
||||
assert_eq!(slate.state, SlateState::Standard2);
|
||||
|
||||
// Switch account for wallet1 to account2 and finish the transaction (should use account1 to complete)
|
||||
// Switch accounts and finish the transaction (should use account1 to complete)
|
||||
{
|
||||
wallet_inst!(wallet1, w);
|
||||
w.set_parent_key_id_by_name("account2")?;
|
||||
w.set_parent_key_id_by_name("default")?;
|
||||
}
|
||||
wallet::controller::owner_single_use(
|
||||
wallet1.clone(),
|
||||
mask1,
|
||||
PathBuf::from(test_dir),
|
||||
|api, m| {
|
||||
let (_, wallet_info) = api.retrieve_summary_info(m, false, 1)?;
|
||||
assert_eq!(wallet_info.last_confirmed_height, 0);
|
||||
let mut expired = slate.clone();
|
||||
expired.ttl_cutoff_height = bh;
|
||||
assert!(matches!(
|
||||
api.contract_sign(m, &expired, &ContractSetupArgsAPI::default()),
|
||||
Err(libwallet::Error::TransactionExpired)
|
||||
));
|
||||
let args = &ContractSetupArgsAPI {
|
||||
selection_args: common::contract_selection_args(),
|
||||
..Default::default()
|
||||
@@ -165,6 +175,10 @@ fn contract_accounts_switch_impl(test_dir: &'static str) -> Result<(), libwallet
|
||||
},
|
||||
)?;
|
||||
assert_eq!(slate.state, SlateState::Standard3);
|
||||
{
|
||||
wallet_inst!(wallet1, w);
|
||||
w.set_parent_key_id_by_name("account2")?;
|
||||
}
|
||||
|
||||
// post tx and mine a block to wallet1::account2
|
||||
wallet::controller::owner_single_use(
|
||||
|
||||
@@ -60,6 +60,7 @@ fn contract_rsr_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error>
|
||||
|
||||
// Receive wallet initiates an invoice transaction through the foreign API.
|
||||
let args = &ContractNewArgsAPI {
|
||||
ttl_blocks: None,
|
||||
setup_args: ContractSetupArgsAPI {
|
||||
selection_args: common::contract_selection_args(),
|
||||
net_change: Some(5_000_000_000),
|
||||
@@ -88,6 +89,7 @@ fn contract_rsr_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error>
|
||||
recv_mask.cloned(),
|
||||
|api| {
|
||||
let rejected = ContractNewArgsAPI {
|
||||
ttl_blocks: args.ttl_blocks,
|
||||
setup_args: ContractSetupArgsAPI {
|
||||
net_change: Some(-5_000_000_000),
|
||||
..args.setup_args.clone()
|
||||
|
||||
@@ -279,6 +279,7 @@ fn contract_min_confirmations_impl(test_dir: &'static str) -> Result<(), libwall
|
||||
slate = api.contract_new(
|
||||
m,
|
||||
&ContractNewArgsAPI {
|
||||
ttl_blocks: None,
|
||||
setup_args: ContractSetupArgsAPI {
|
||||
net_change: Some(-(consensus::GRIN_BASE as i64)),
|
||||
selection_args: OutputSelectionArgs {
|
||||
|
||||
@@ -70,6 +70,12 @@ 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 ttl_cutoff = bh + 20;
|
||||
let ttl_error = format!(
|
||||
"Contract TTL changed from {} to {}",
|
||||
ttl_cutoff,
|
||||
ttl_cutoff + 1
|
||||
);
|
||||
|
||||
let mut slate = Slate::blank(0, false); // this gets overriden below
|
||||
|
||||
@@ -80,6 +86,7 @@ fn contract_srs_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error>
|
||||
|api, m| {
|
||||
// Send wallet inititates a standard transaction with --send=5
|
||||
let mut args = ContractNewArgsAPI {
|
||||
ttl_blocks: Some(20),
|
||||
setup_args: ContractSetupArgsAPI {
|
||||
selection_args: common::contract_selection_args(),
|
||||
net_change: Some(-5_000_000_000),
|
||||
@@ -96,11 +103,22 @@ fn contract_srs_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error>
|
||||
assert_eq!(common::wallet_progress(api, m)?, progress);
|
||||
|
||||
args.setup_args.num_participants = 2;
|
||||
args.ttl_blocks = Some(0);
|
||||
let err = api.contract_new(m, &mut args).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
libwallet::Error::GenericError(ref msg)
|
||||
if msg == "Contract TTL must be at least 1 block"
|
||||
));
|
||||
args.ttl_blocks = Some(20);
|
||||
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));
|
||||
Ok(())
|
||||
},
|
||||
)?;
|
||||
assert_eq!(slate.state, SlateState::Standard1);
|
||||
assert_eq!(slate.ttl_cutoff_height, ttl_cutoff);
|
||||
common::assert_basic_contract_slate(
|
||||
&slate,
|
||||
common::ExpectedContractSlate {
|
||||
@@ -302,6 +320,7 @@ fn contract_srs_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error>
|
||||
},
|
||||
)?;
|
||||
assert_eq!(slate.state, SlateState::Standard2);
|
||||
assert_eq!(slate.ttl_cutoff_height, ttl_cutoff);
|
||||
common::assert_basic_contract_slate(
|
||||
&slate,
|
||||
common::ExpectedContractSlate {
|
||||
@@ -487,6 +506,32 @@ fn contract_srs_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error>
|
||||
send_mask,
|
||||
PathBuf::from(test_dir),
|
||||
|api, m| {
|
||||
let progress = common::wallet_progress(api, m)?;
|
||||
let mut expired = slate.clone();
|
||||
expired.ttl_cutoff_height = bh;
|
||||
let err = api
|
||||
.contract_sign(m, &expired, &ContractSetupArgsAPI::default())
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, libwallet::Error::TransactionExpired));
|
||||
|
||||
let mut changed = slate.clone();
|
||||
changed.ttl_cutoff_height += 1;
|
||||
let err = match api.contract_view(m, &changed) {
|
||||
Err(err) => err,
|
||||
Ok(_) => panic!("changed contract TTL accepted"),
|
||||
};
|
||||
assert!(matches!(
|
||||
err,
|
||||
libwallet::Error::GenericError(ref msg) if msg == &ttl_error
|
||||
));
|
||||
let err = api
|
||||
.contract_sign(m, &changed, &ContractSetupArgsAPI::default())
|
||||
.unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
libwallet::Error::GenericError(ref msg) if msg == &ttl_error
|
||||
));
|
||||
assert_eq!(common::wallet_progress(api, m)?, progress);
|
||||
let args = &mut ContractSetupArgsAPI {
|
||||
selection_args: common::contract_selection_args(),
|
||||
..Default::default()
|
||||
@@ -525,6 +570,16 @@ fn contract_srs_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error>
|
||||
send_mask,
|
||||
PathBuf::from(test_dir),
|
||||
|api, m| {
|
||||
let mut changed = slate.clone();
|
||||
changed.ttl_cutoff_height += 1;
|
||||
let err = match api.contract_view(m, &changed) {
|
||||
Err(err) => err,
|
||||
Ok(_) => panic!("changed contract TTL accepted"),
|
||||
};
|
||||
assert!(matches!(
|
||||
err,
|
||||
libwallet::Error::GenericError(ref msg) if msg == &ttl_error
|
||||
));
|
||||
let view = api.contract_view(m, &slate)?;
|
||||
assert_eq!(view.own_commitment_status, OwnCommitmentStatus::Unknown);
|
||||
assert_eq!(view.own_fee, Some(participant_fee));
|
||||
|
||||
+4
-1
@@ -38,6 +38,9 @@ is used for the fee estimate, so any required inputs need enough confirmations a
|
||||
A value of `0` allows unconfirmed non-coinbase outputs. Open contracts do not reserve funds,
|
||||
so a late-locked contract can fail if another transaction spends the available outputs first.
|
||||
|
||||
`contract new --ttl_blocks N` stops contract signing after `N` blocks. Finalized transactions
|
||||
can still be posted afterward.
|
||||
|
||||
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.
|
||||
|
||||
@@ -69,7 +72,7 @@ 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 and contract TTLs are not supported
|
||||
* 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
|
||||
|
||||
@@ -1383,12 +1383,13 @@ where
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
// Refuse if TTL is expired
|
||||
let last_confirmed_height = w.last_confirmed_height()?;
|
||||
if slate.ttl_cutoff_height != 0 {
|
||||
if last_confirmed_height >= slate.ttl_cutoff_height {
|
||||
return Err(Error::TransactionExpired);
|
||||
}
|
||||
check_ttl_at_height(slate, w.last_confirmed_height()?)
|
||||
}
|
||||
|
||||
/// Check a slate TTL against the given block height
|
||||
pub(crate) fn check_ttl_at_height(slate: &Slate, height: u64) -> Result<(), Error> {
|
||||
if slate.ttl_cutoff_height != 0 && height >= slate.ttl_cutoff_height {
|
||||
return Err(Error::TransactionExpired);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1587,7 +1588,13 @@ where
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
contract::new(&mut *w, keychain_mask, &args.setup_args, None)
|
||||
contract::new(
|
||||
&mut *w,
|
||||
keychain_mask,
|
||||
&args.setup_args,
|
||||
args.ttl_blocks,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
/// View a transaction contract
|
||||
|
||||
@@ -41,7 +41,8 @@ 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 fee rates and contract TTLs, or document them as unsupported.
|
||||
- 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.
|
||||
- Add a Grin node API for kernel lookup by MMR index, then use the witness index instead
|
||||
|
||||
@@ -32,6 +32,7 @@ pub fn new<C, K>(
|
||||
w: &mut WalletBackend<C, K>,
|
||||
keychain_mask: Option<&SecretKey>,
|
||||
setup_args: &ContractSetupArgsAPI,
|
||||
ttl_blocks: Option<u64>,
|
||||
slate_id: Option<Uuid>,
|
||||
) -> Result<Slate, Error>
|
||||
where
|
||||
@@ -40,7 +41,7 @@ where
|
||||
{
|
||||
contract::utils::verify_num_participants(setup_args.num_participants)?;
|
||||
// Compute state for 'new'
|
||||
let (slate, context) = compute(w, keychain_mask, setup_args, slate_id)?;
|
||||
let (slate, context) = compute(w, keychain_mask, setup_args, ttl_blocks, slate_id)?;
|
||||
|
||||
// Atomically commit state
|
||||
contract::utils::save_step(w, keychain_mask, &slate, context, setup_args.add_outputs)?;
|
||||
@@ -53,6 +54,7 @@ pub fn compute<C, K>(
|
||||
w: &mut WalletBackend<C, K>,
|
||||
keychain_mask: Option<&SecretKey>,
|
||||
setup_args: &ContractSetupArgsAPI,
|
||||
ttl_blocks: Option<u64>,
|
||||
slate_id: Option<Uuid>,
|
||||
) -> Result<(Slate, Context), Error>
|
||||
where
|
||||
@@ -68,8 +70,30 @@ where
|
||||
let num_participants = setup_args.num_participants;
|
||||
let mut slate = Slate::blank(num_participants, net_change > 0);
|
||||
// Use a caller-supplied id when given, so a retried creation reuses the same context.
|
||||
let mut reused_context = false;
|
||||
if let Some(id) = slate_id {
|
||||
slate.id = id;
|
||||
match w.get_private_context(keychain_mask, slate.id.as_bytes()) {
|
||||
Ok(context) => {
|
||||
slate.ttl_cutoff_height = context.contract_ttl_cutoff_height.unwrap_or(0);
|
||||
reused_context = true;
|
||||
}
|
||||
Err(Error::NotFoundErr(_)) => {}
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
if !reused_context {
|
||||
if let Some(blocks) = ttl_blocks {
|
||||
if blocks == 0 {
|
||||
return Err(Error::GenericError(
|
||||
"Contract TTL must be at least 1 block".to_string(),
|
||||
));
|
||||
}
|
||||
let height = w.w2n_client().get_chain_tip()?.0;
|
||||
slate.ttl_cutoff_height = height.checked_add(blocks).ok_or_else(|| {
|
||||
Error::GenericError("Contract TTL exceeds the maximum block height".to_string())
|
||||
})?;
|
||||
}
|
||||
}
|
||||
// We set slate.amount to contain the _positive_ net_change for the other party so they can derive expectations.
|
||||
// unsigned_abs avoids the i64::MIN overflow panic of abs().
|
||||
|
||||
@@ -138,6 +138,7 @@ where
|
||||
},
|
||||
proof_args: Default::default(),
|
||||
},
|
||||
None,
|
||||
self_spend_id,
|
||||
)?;
|
||||
let finished_slate = sign(
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
//! Implementation of contract setup
|
||||
|
||||
use crate::api_impl::owner::check_ttl;
|
||||
use crate::api_impl::owner::check_ttl_at_height;
|
||||
use crate::backend::WalletBackend;
|
||||
use crate::contract;
|
||||
use crate::contract::types::ContractSetupArgsAPI;
|
||||
@@ -24,6 +24,18 @@ use crate::grin_util::secp::key::SecretKey;
|
||||
use crate::slate::Slate;
|
||||
use crate::types::{Context, NodeClient};
|
||||
|
||||
// Contract deadlines use the node tip because the active account may not own the context
|
||||
fn check_contract_ttl<C, K>(w: &mut WalletBackend<C, K>, slate: &Slate) -> Result<(), Error>
|
||||
where
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
if slate.ttl_cutoff_height == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
check_ttl_at_height(slate, w.w2n_client().get_chain_tip()?.0)
|
||||
}
|
||||
|
||||
/// Perform a contract setup
|
||||
pub fn setup<C, K>(
|
||||
w: &mut WalletBackend<C, K>,
|
||||
@@ -64,10 +76,11 @@ where
|
||||
K: Keychain,
|
||||
{
|
||||
let mut sl = slate.clone();
|
||||
check_ttl(w, &sl)?;
|
||||
check_contract_ttl(w, &sl)?;
|
||||
|
||||
// Get or create the Context and check the setup arguments
|
||||
let mut context = contract::context::get_or_create(w, keychain_mask, &mut sl, setup_args)?;
|
||||
contract::utils::verify_ttl(context.contract_ttl_cutoff_height, &sl)?;
|
||||
let context_args = context
|
||||
.setup_args
|
||||
.as_ref()
|
||||
|
||||
@@ -92,6 +92,13 @@ where
|
||||
.find(|tx| contract::utils::is_signed_tx(tx, slate.id));
|
||||
let has_signed = signed_tx.is_some();
|
||||
let tx = signed_tx.or_else(|| txs.first());
|
||||
let stored_ttl = context
|
||||
.as_ref()
|
||||
.map(|context| context.contract_ttl_cutoff_height)
|
||||
.or_else(|| tx.map(|tx| tx.ttl_cutoff_height));
|
||||
if let Some(expected) = stored_ttl {
|
||||
contract::utils::verify_ttl(expected, slate)?;
|
||||
}
|
||||
// Once we have signed, the context identifies the commitments we added. If it has
|
||||
// already been removed, the slate no longer carries enough information to do that.
|
||||
let own_commitment_status = if slate.tx.is_none() || (context.is_none() && !txs.is_empty()) {
|
||||
|
||||
@@ -119,6 +119,10 @@ where
|
||||
// Store the full setup args on the context; later steps (add_outputs) read the
|
||||
// selection arguments from here.
|
||||
context.setup_args = Some(setup_args.clone());
|
||||
context.contract_ttl_cutoff_height = match slate.ttl_cutoff_height {
|
||||
0 => None,
|
||||
height => Some(height),
|
||||
};
|
||||
debug!(
|
||||
"Setting Context.net_change as: {}",
|
||||
context.get_net_change()?
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
//! Types related to a contract
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::grin_core::libtx::secp_ser;
|
||||
pub use crate::slate::PaymentProofType as ProofType;
|
||||
use crate::slate_versions::ser as dalek_ser;
|
||||
use ed25519_dalek::VerifyingKey as DalekPublicKey;
|
||||
@@ -154,11 +155,16 @@ impl Default for ContractSetupArgsAPI {
|
||||
pub struct ContractNewArgsAPI {
|
||||
/// Setup args - contract new also initiates the setup by default
|
||||
pub setup_args: ContractSetupArgsAPI,
|
||||
/// Blocks until wallets should stop signing the contract
|
||||
/// Only used when creating a new contract context
|
||||
#[serde(with = "secp_ser::opt_string_or_u64", default)]
|
||||
pub ttl_blocks: Option<u64>,
|
||||
}
|
||||
|
||||
impl Default for ContractNewArgsAPI {
|
||||
fn default() -> ContractNewArgsAPI {
|
||||
ContractNewArgsAPI {
|
||||
ttl_blocks: None,
|
||||
setup_args: ContractSetupArgsAPI {
|
||||
src_acct_name: None,
|
||||
net_change: None,
|
||||
@@ -265,6 +271,30 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contract_ttl_json() {
|
||||
let mut args = ContractNewArgsAPI::default();
|
||||
args.ttl_blocks = Some(20);
|
||||
let mut json = serde_json::to_value(&args).unwrap();
|
||||
assert_eq!(json["ttl_blocks"], "20");
|
||||
|
||||
json["ttl_blocks"] = serde_json::json!(20);
|
||||
assert_eq!(
|
||||
serde_json::from_value::<ContractNewArgsAPI>(json.clone())
|
||||
.unwrap()
|
||||
.ttl_blocks,
|
||||
Some(20)
|
||||
);
|
||||
|
||||
json.as_object_mut().unwrap().remove("ttl_blocks");
|
||||
assert_eq!(
|
||||
serde_json::from_value::<ContractNewArgsAPI>(json)
|
||||
.unwrap()
|
||||
.ttl_blocks,
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn own_commitment_status_json() {
|
||||
for (status, value) in [
|
||||
|
||||
@@ -375,6 +375,18 @@ pub(super) fn verify_num_participants(num_participants: u8) -> Result<(), Error>
|
||||
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()
|
||||
|
||||
@@ -403,6 +403,8 @@ pub struct Context {
|
||||
/// history might need to search through a list to update a txlogentry, so we
|
||||
/// keep the id in the context.
|
||||
pub log_id: Option<u32>,
|
||||
/// Contract signing deadline copied from the slate
|
||||
pub contract_ttl_cutoff_height: Option<u64>,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
@@ -454,6 +456,7 @@ impl Context {
|
||||
calculated_excess: None,
|
||||
setup_args: None,
|
||||
log_id: None,
|
||||
contract_ttl_cutoff_height: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -503,6 +503,11 @@ subcommands:
|
||||
long: min_conf
|
||||
default_value: "10"
|
||||
takes_value: true
|
||||
- ttl_blocks:
|
||||
help: Positive number of blocks until wallets stop signing the contract
|
||||
short: b
|
||||
long: ttl_blocks
|
||||
takes_value: true
|
||||
- sign:
|
||||
about: Sign a contract
|
||||
args:
|
||||
|
||||
@@ -1032,6 +1032,18 @@ pub fn parse_contract_new_args(
|
||||
parse_required(args, "minimum_confirmations")?,
|
||||
"minimum_confirmations",
|
||||
)?;
|
||||
let ttl_blocks = match args.value_of("ttl_blocks") {
|
||||
Some(value) => {
|
||||
let blocks = parse_u64(value, "ttl_blocks")?;
|
||||
if blocks == 0 {
|
||||
return Err(ParseError::ArgumentError(
|
||||
"Contract TTL must be at least 1 block".to_string(),
|
||||
));
|
||||
}
|
||||
Some(blocks)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let as_json = args.is_present("as-json");
|
||||
let no_payjoin = args.is_present("no-payjoin");
|
||||
let use_inputs = match args.value_of("use-inputs") {
|
||||
@@ -1087,6 +1099,7 @@ pub fn parse_contract_new_args(
|
||||
use_inputs: use_inputs,
|
||||
make_outputs: make_outputs,
|
||||
minimum_confirmations,
|
||||
ttl_blocks,
|
||||
// Not yet wired to CLI flags:
|
||||
fee_rate: None,
|
||||
outfile: None,
|
||||
|
||||
@@ -248,7 +248,7 @@ fn rejects_conflicting_input_options() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_contract_min_confirmations() {
|
||||
fn parses_contract_options() {
|
||||
let yml = load_yaml!("../src/bin/grin-wallet.yml");
|
||||
let app = App::from_yaml(yml);
|
||||
let account = "default".to_string();
|
||||
@@ -262,6 +262,8 @@ fn parses_contract_min_confirmations() {
|
||||
"1",
|
||||
"--min_conf",
|
||||
"3",
|
||||
"--ttl_blocks",
|
||||
"20",
|
||||
])
|
||||
.unwrap();
|
||||
let contract = args.subcommand_matches("contract").unwrap();
|
||||
@@ -269,6 +271,7 @@ fn parses_contract_min_confirmations() {
|
||||
let parsed =
|
||||
grin_wallet::cmd::wallet_args::parse_contract_new_args(new_args, &account).unwrap();
|
||||
assert_eq!(parsed.minimum_confirmations, 3);
|
||||
assert_eq!(parsed.ttl_blocks, Some(20));
|
||||
|
||||
let expected = grin_wallet_libwallet::contract::types::DEFAULT_MINIMUM_CONFIRMATIONS;
|
||||
let args = app
|
||||
@@ -280,6 +283,23 @@ fn parses_contract_min_confirmations() {
|
||||
let parsed =
|
||||
grin_wallet::cmd::wallet_args::parse_contract_new_args(new_args, &account).unwrap();
|
||||
assert_eq!(parsed.minimum_confirmations, expected);
|
||||
assert_eq!(parsed.ttl_blocks, None);
|
||||
let args = app
|
||||
.clone()
|
||||
.get_matches_from_safe(vec!["grin-wallet", "contract", "new", "-s", "1", "-b", "0"])
|
||||
.unwrap();
|
||||
let contract = args.subcommand_matches("contract").unwrap();
|
||||
let new_args = contract.subcommand_matches("new").unwrap();
|
||||
let err = match grin_wallet::cmd::wallet_args::parse_contract_new_args(new_args, &account) {
|
||||
Err(err) => err,
|
||||
Ok(_) => panic!("zero contract TTL accepted"),
|
||||
};
|
||||
assert_eq!(
|
||||
err,
|
||||
grin_wallet::cmd::wallet_args::ParseError::ArgumentError(
|
||||
"Contract TTL must be at least 1 block".to_string()
|
||||
)
|
||||
);
|
||||
|
||||
let args = app
|
||||
.clone()
|
||||
|
||||
Reference in New Issue
Block a user