diff --git a/api/src/foreign.rs b/api/src/foreign.rs index 77c65754..db46b1a5 100644 --- a/api/src/foreign.rs +++ b/api/src/foreign.rs @@ -17,6 +17,7 @@ use crate::config::TorConfig; use crate::keychain::Keychain; use crate::libwallet::api_impl::foreign; +use crate::libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI}; use crate::libwallet::{ BlockFees, CbData, Error, NodeClient, NodeVersionInfo, Slate, VersionInfo, WalletInst, WalletLCProvider, @@ -450,6 +451,32 @@ where post_automatically, ) } + + // Below is a foreign wrapper around owner calls to 'new' and 'sign' which are only executed + // if this is a receiving contract. This preserves the ability to receive on a foreign interface. + /// TODO + pub fn contract_new( + &self, + keychain_mask: Option<&SecretKey>, + args: &ContractNewArgsAPI, + ) -> Result { + let mut w_lock = self.wallet_inst.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + // TODO: self.doctest_mode ? + foreign::contract_new(&mut **w, keychain_mask, &args) + } + + /// TODO + pub fn contract_sign( + &self, + keychain_mask: Option<&SecretKey>, + slate: &Slate, + args: &ContractSetupArgsAPI, + ) -> Result { + let mut w_lock = self.wallet_inst.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + foreign::contract_sign(&mut **w, keychain_mask, &args, &slate) + } } #[doc(hidden)] diff --git a/api/src/owner.rs b/api/src/owner.rs index ac80d787..d18df0f6 100644 --- a/api/src/owner.rs +++ b/api/src/owner.rs @@ -27,6 +27,9 @@ use crate::impls::SlateSender as _; use crate::keychain::{Identifier, Keychain}; use crate::libwallet::api_impl::owner_updater::{start_updater_log_thread, StatusMessage}; use crate::libwallet::api_impl::{owner, owner_updater}; +use crate::libwallet::contract::types::{ + ContractNewArgsAPI, ContractRevokeArgsAPI, ContractSetupArgsAPI, +}; use crate::libwallet::{ AcctPathMapping, BuiltOutput, Error, InitTxArgs, IssueInvoiceTxArgs, NodeClient, NodeHeightResult, OutputCommitMapping, PaymentProof, Slate, Slatepack, SlatepackAddress, @@ -766,6 +769,54 @@ where owner::issue_invoice_tx(&mut **w, keychain_mask, args, self.doctest_mode) } + /// TODO + pub fn contract_new( + &self, + keychain_mask: Option<&SecretKey>, + args: &ContractNewArgsAPI, + ) -> Result { + let mut w_lock = self.wallet_inst.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + // TODO: self.doctest_mode ? + owner::contract_new(&mut **w, keychain_mask, &args) + } + + // /// TODO + // pub fn contract_setup( + // &self, + // keychain_mask: Option<&SecretKey>, + // slate: &Slate, + // args: &ContractSetupArgsAPI, + // ) -> Result { + // let mut w_lock = self.wallet_inst.lock(); + // let w = w_lock.lc_provider()?.wallet_inst()?; + // // TODO: self.doctest_mode ? + // owner::contract_setup(&mut **w, keychain_mask, &args, &slate) + // } + + /// TODO + pub fn contract_sign( + &self, + keychain_mask: Option<&SecretKey>, + slate: &Slate, + args: &ContractSetupArgsAPI, + ) -> Result { + let mut w_lock = self.wallet_inst.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + owner::contract_sign(&mut **w, keychain_mask, &args, &slate) + } + + /// TODO + pub fn contract_revoke( + &self, + keychain_mask: Option<&SecretKey>, + args: &ContractRevokeArgsAPI, + ) -> Result, Error> { + let mut w_lock = self.wallet_inst.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + owner::contract_revoke(&mut **w, keychain_mask, &args) + } + /// Processes an invoice tranaction created by another party, essentially /// a `request for payment`. The incoming slate should contain a requested /// amount, an output created by the invoicer convering the amount, and diff --git a/api/src/owner_rpc.rs b/api/src/owner_rpc.rs index 23fc6b63..c8e6f136 100644 --- a/api/src/owner_rpc.rs +++ b/api/src/owner_rpc.rs @@ -20,6 +20,9 @@ use crate::config::{TorConfig, WalletConfig}; use crate::core::core::OutputFeatures; use crate::core::global; use crate::keychain::{Identifier, Keychain}; +use crate::libwallet::contract::types::{ + ContractNewArgsAPI, ContractRevokeArgsAPI, ContractSetupArgsAPI, +}; use crate::libwallet::{ AcctPathMapping, Amount, BuiltOutput, Error, InitTxArgs, IssueInvoiceTxArgs, NodeClient, NodeHeightResult, OutputCommitMapping, PaymentProof, Slate, SlateVersion, Slatepack, @@ -511,6 +514,25 @@ pub trait OwnerRpc { */ fn init_send_tx(&self, token: Token, args: InitTxArgs) -> Result; + fn contract_new(&self, token: Token, args: ContractNewArgsAPI) + -> Result; + // fn contract_setup( + // &self, + // token: Token, + // slate: VersionedSlate, + // args: ContractSetupArgsAPI, + // ) -> Result; + fn contract_sign( + &self, + token: Token, + slate: VersionedSlate, + args: ContractSetupArgsAPI, + ) -> Result; + fn contract_revoke( + &self, + token: Token, + args: ContractRevokeArgsAPI, + ) -> Result, Error>; /** ;Networked version of [Owner::issue_invoice_tx](struct.Owner.html#method.issue_invoice_tx). @@ -2052,6 +2074,65 @@ where VersionedSlate::into_version(slate, version) } + fn contract_new( + &self, + token: Token, + args: ContractNewArgsAPI, + ) -> Result { + let slate = Owner::contract_new(self, (&token.keychain_mask).as_ref(), &args)?; + let version = SlateVersion::V4; + VersionedSlate::into_version(slate, version) + } + + // fn contract_setup( + // &self, + // token: Token, + // in_slate: VersionedSlate, + // args: ContractSetupArgsAPI, + // ) -> Result { + // let slate = Owner::contract_setup( + // self, + // (&token.keychain_mask).as_ref(), + // &Slate::from(in_slate), + // &args, + // )?; + // let version = SlateVersion::V4; + // VersionedSlate::into_version(slate, version) + // } + + fn contract_sign( + &self, + token: Token, + in_slate: VersionedSlate, + args: ContractSetupArgsAPI, + ) -> Result { + let slate = Owner::contract_sign( + self, + (&token.keychain_mask).as_ref(), + &Slate::from(in_slate), + &args, + )?; + let version = SlateVersion::V4; + VersionedSlate::into_version(slate, version) + } + + fn contract_revoke( + &self, + token: Token, + args: ContractRevokeArgsAPI, + ) -> Result, Error> { + let slate_opt = Owner::contract_revoke(self, (&token.keychain_mask).as_ref(), &args)?; + let version = SlateVersion::V4; + // We return a slate only when we had to perform a self-spend safe cancel + if slate_opt.is_some() { + return Ok(Some(VersionedSlate::into_version( + slate_opt.unwrap(), + version, + )?)); + } + Ok(None) + } + fn issue_invoice_tx( &self, token: Token, diff --git a/controller/src/command.rs b/controller/src/command.rs index 938eb829..efa4a6be 100644 --- a/controller/src/command.rs +++ b/controller/src/command.rs @@ -13,7 +13,6 @@ // limitations under the License. //! Grin wallet command-line function implementations - use crate::api::TLSConfig; use crate::apiwallet::{try_slatepack_sync_workflow, Owner}; use crate::config::{TorConfig, WalletConfig, WALLET_CONFIG_FILE_NAME}; @@ -22,6 +21,11 @@ use crate::error::Error; use crate::impls::PathToSlatepack; use crate::impls::SlateGetter as _; use crate::keychain; +use crate::libwallet::api_impl::owner; +use crate::libwallet::contract::can_finalize; +use crate::libwallet::contract::types::{ + ContractNewArgsAPI, ContractRevokeArgsAPI, ContractSetupArgsAPI, OutputSelectionArgs, +}; use crate::libwallet::{ self, InitTxArgs, IssueInvoiceTxArgs, NodeClient, PaymentProof, Slate, SlateState, Slatepack, SlatepackAddress, Slatepacker, SlatepackerArgs, WalletLCProvider, @@ -31,9 +35,12 @@ use crate::util::{Mutex, ZeroingString}; use crate::{controller, display}; use ::core::time; use qr_code::QrCode; +use serde::{Deserialize, Serialize}; use serde_json as json; use std::convert::TryFrom; +use std::fmt; use std::fs::File; +use std::io; use std::io::{Read, Write}; use std::sync::atomic::Ordering; use std::sync::Arc; @@ -558,6 +565,140 @@ where Ok(()) } +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct SlatepackOut { + /// Is slatepack encrypted + pub is_encrypted: bool, + /// Is slatepack finalized + pub is_finalized: bool, + /// File where slatepack is saved + pub out_file: String, + /// Slatepack message. Encrypted or not. + pub message: String, +} + +impl fmt::Display for SlatepackOut { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let start_meta = "--------------- SLATEPACK METADATA --------------"; + let meta = format!( + "Slate encrypted: {}\nSlate finalized: {}\nSlate saved to file: {}", + self.is_encrypted, self.is_finalized, self.out_file + ); + let start_slatepack = "-------------- CUT BELOW THIS LINE --------------"; + let end_slatepack = "-------------- CUT ABOVE THIS LINE --------------"; + write!( + f, + "{start_meta}\n\n{meta}\n\n{start_slatepack}\n\n{}\n\n{end_slatepack}", + self.message + ) + } +} + +impl SlatepackOut { + fn as_json(&self) -> String { + serde_json::to_string_pretty(&self).unwrap() + } + + pub fn print(&self, as_json: bool) -> () { + if !self.is_finalized { + if as_json { + println!("{}", self.as_json()); + } else { + println!("{}", self); + } + } else { + println!("Transaction was broadcasted."); // TODO: as_json makes no sense here, fix later. + } + } +} + +pub fn print_slatepack( + api: &mut Owner, + keychain_mask: Option<&SecretKey>, + slate: &Slate, + counterparty_addr: &str, + out_file: Option, + as_json: bool, +) -> () +where + L: WalletLCProvider<'static, C, K> + 'static, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + // For now, we don't compact slates with sl.compact(). We first make them work without compaction. + let slate_out = + prepare_slatepack(api, keychain_mask, &slate, &counterparty_addr, out_file).unwrap(); + slate_out.print(as_json); +} + +pub fn prepare_slatepack( + owner_api: &mut Owner, + keychain_mask: Option<&SecretKey>, + slate: &Slate, + dest: &str, + out_file_override: Option, +) -> Result +where + L: WalletLCProvider<'static, C, K> + 'static, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + // Same as output_slatepack except that we don't write to stdout, care about locking or whether the slate was finalized. + + // Output the slatepack file to stdout and to a file + let mut message = String::from(""); + let mut address = None; + let mut tld = String::from(""); + controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { + address = match SlatepackAddress::try_from(dest) { + Ok(a) => Some(a), + Err(_) => None, + }; + // encrypt for recipient by default + let recipients = match address.clone() { + Some(a) => vec![a], + None => vec![], + }; + // TODO: what is sender_index? + message = api.create_slatepack_message(m, &slate, Some(0), recipients)?; + // Trim the \n at the end. + let len_withoutcrlf = message.trim_end().len(); + message.truncate(len_withoutcrlf); + + tld = api.get_top_level_directory()?; + Ok(()) + })?; + + // create a directory to which files will be output + let slate_dir = format!("{}/{}", tld, "slatepack"); + let _ = std::fs::create_dir_all(slate_dir.clone()); + let out_file_name = match out_file_override { + None => format!("{}/{}.{}.slatepack", slate_dir, slate.id, slate.state), + Some(f) => f, + }; + + let mut output = File::create(out_file_name.clone())?; + output.write_all(&message.as_bytes())?; + output.sync_all()?; + + // Since we always finalize if we can, we can also use this to know if the tx is finalized + let is_finalized = can_finalize(slate); + + let slate_out = SlatepackOut { + is_encrypted: address.is_some(), + is_finalized: is_finalized, + out_file: out_file_name, + message: message, + }; + + // TODO: We save the slatepack, but it is encrypted for the counterparty. It seems hard to + // know which slatepack is which if we can't decrypt them. Either add some more metadata + // to slatepacks e.g. timestamp, counterparty address or save also a version that is encrypted with + // our own address so we can view it. + + Ok(slate_out) +} + // Parse a slate and slatepack from a message pub fn parse_slatepack( owner_api: &mut Owner, @@ -1474,3 +1615,340 @@ where })?; Ok(()) } + +/// Create new contract command arguments +#[derive(Clone)] +pub struct ContractNewArgs { + /// Address of the counterparty + pub counterparty_addr: String, + /// Receive amount + pub receive: Option, + /// Send amount + pub send: Option, + /// The human readable account name from which to draw outputs + /// for the transaction, overriding whatever the active account is as set via the + /// [`set_active_account`](../grin_wallet_api/owner/struct.Owner.html#method.set_active_account) method. + pub src_acct_name: Option, + /// Number of participants in a contract (either 1 or 2) + pub num_participants: u8, + /// Show the resulting slatepack as JSON + pub as_json: bool, + /// Use the specified inputs (comma separated input commitments) + pub use_inputs: Option, + /// How to separate outputs (command separated amounts) + pub make_outputs: Option, + + // Future features + /// Custom fee contribution + pub fee_rate: Option, + /// Save slatepack to a specific filename + pub outfile: Option, + /// Select outputs early + pub add_outputs: bool, +} + +impl ContractNewArgs { + fn get_net_change(&self) -> i64 { + // TODO: could the cast 'as i64' overflow or something? + match self.receive { + None => match self.send { + None => panic!("Send or receive not specified."), + Some(v) => -(v as i64), // negative net change on send + }, + Some(v) => v as i64, // positive net change on receive + } + } + + // Create a ContractNewArgsAPI from the ContractNewArgs + fn to_api_args(&self) -> ContractNewArgsAPI { + let net_change = self.get_net_change(); + ContractNewArgsAPI { + setup_args: ContractSetupArgsAPI { + src_acct_name: match self.src_acct_name.as_ref() { + Some(v) => Some(v.to_string()), + None => None, + }, + net_change: Some(net_change), + num_participants: self.num_participants, + add_outputs: self.add_outputs, + selection_args: OutputSelectionArgs { + use_inputs: match self.use_inputs.as_ref() { + Some(v) => Some(v.to_string()), + None => None, + }, + make_outputs: match self.make_outputs.as_ref() { + Some(v) => Some(v.to_string()), + None => None, + }, + ..Default::default() + }, + }, + ..Default::default() + } + } +} + +pub fn contract_new( + owner_api: &mut Owner, + keychain_mask: Option<&SecretKey>, + args: ContractNewArgs, +) -> Result<(), Error> +where + L: WalletLCProvider<'static, C, K>, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { + let contract_new_args = args.to_api_args(); + + let slate = api.contract_new(m, &contract_new_args)?; + + print_slatepack( + api, + keychain_mask, + &slate, + &args.counterparty_addr, + args.outfile, + args.as_json, + ); + + Ok(()) + })?; + + Ok(()) +} + +/// Sign contract command argument +#[derive(Clone)] +pub struct ContractSetupArgs { + /// Address of the counterparty + pub counterparty_addr: Option, + /// Receive amount + pub receive: Option, + /// Send amount + pub send: Option, + /// Show the resulting slatepack as JSON + pub as_json: bool, + /// Use the specified inputs (comma separated input commitments) + pub use_inputs: Option, + /// How to separate outputs (command separated amounts) + pub make_outputs: Option, + + // Future features + /// Whether we should automatically sign a receive of any value + // pub auto_receive: Option, + /// Custom fee contribution + pub fee_rate: Option, + /// Save slatepack to a specific filename + pub outfile: Option, + /// Add outputs + pub add_outputs: bool, // lock outputs early +} + +impl ContractSetupArgs { + fn get_net_change(&self) -> Option { + let mut net_change: Option = None; + // TODO: Check bounds before casting to i64. + if self.receive.is_some() && self.send.is_some() { + panic!("Can't pass both --receive and --send parameters."); + } + if self.receive.is_some() { + net_change = Some(self.receive.unwrap() as i64); + } + if self.send.is_some() { + net_change = Some(-(self.send.unwrap() as i64)); + } + net_change + } + + // Create a ContractSetupArgsAPI from the ContractSetupArgs + fn to_api_args(&self) -> ContractSetupArgsAPI { + let net_change = self.get_net_change(); + ContractSetupArgsAPI { + // TODO: num_participants is derived here. It should be an Option and read from the slate. + // Need to check no attack are possible regarding kernel fee contribution. + net_change: net_change, + add_outputs: self.add_outputs, + selection_args: OutputSelectionArgs { + use_inputs: match self.use_inputs.as_ref() { + Some(v) => Some(v.to_string()), + None => None, + }, + make_outputs: match self.make_outputs.as_ref() { + Some(v) => Some(v.to_string()), + None => None, + }, + ..Default::default() + }, + ..Default::default() + } + } +} + +// pub fn contract_setup( +// owner_api: &mut Owner, +// keychain_mask: Option<&SecretKey>, +// args: ContractSetupArgs, +// ) -> Result<(), Error> +// where +// L: WalletLCProvider<'static, C, K>, +// C: NodeClient + 'static, +// K: keychain::Keychain + 'static, +// { +// controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { +// // Read the slatepack from stdin +// println!("Paste slatepack:"); +// let mut slatepack_msg = String::new(); +// io::stdin() +// .read_line(&mut slatepack_msg) +// .expect("Failed to read from stdin"); + +// let mut contract_setup_args = args.to_api_args(); + +// // Decrypt the slate, perform setup on it and encrypt it for the next party +// // TODO: Make sure you get the counterparty_addr and slate with 1 call. +// let slatepack = owner::decode_slatepack_message( +// api.wallet_inst.clone(), +// keychain_mask, +// String::from(slatepack_msg.clone()), +// vec![0], +// )?; + +// let counterparty_addr = +// if args.counterparty_addr.is_some() { +// args.counterparty_addr.unwrap() +// } else { +// if !slatepack.sender.is_some() { +// panic!("No address to encrypt for. Contracts only support encrypted slates right now."); +// } +// String::try_from(&slatepack.sender.unwrap())? +// }; +// let mut slate = owner::slate_from_slatepack_message( +// api.wallet_inst.clone(), +// keychain_mask, +// String::from(slatepack_msg), +// vec![0], +// )?; +// // We read the number of participants from the slate that was already created. We need this to +// // avoid taking the default value of 2 in case of 3-party computation and thus incorrectly computing +// // our kernel cost contribution. +// contract_setup_args.num_participants = slate.num_participants; + +// slate = api.contract_setup(m, &slate, &contract_setup_args)?; + +// print_slatepack( +// api, +// keychain_mask, +// &slate, +// &counterparty_addr, +// args.outfile, +// args.as_json, +// ); + +// Ok(()) +// })?; + +// Ok(()) +// } + +pub fn contract_sign( + owner_api: &mut Owner, + keychain_mask: Option<&SecretKey>, + args: ContractSetupArgs, + broadcast_tx: bool, +) -> Result<(), Error> +where + L: WalletLCProvider<'static, C, K>, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { + // Read the slatepack from stdin + println!("Paste slatepack:"); + let mut slatepack_msg = String::new(); + io::stdin() + .read_line(&mut slatepack_msg) + .expect("Failed to read from stdin"); + + // Args for signing are just setup args + let contract_sign_args = args.to_api_args(); + + // Decrypt the slate, sign it and encrypt it for the next party + // TODO: Make sure you get the counterparty_addr and slate with 1 call. + let slatepack = owner::decode_slatepack_message( + api.wallet_inst.clone(), + keychain_mask, + String::from(slatepack_msg.clone()), + vec![0], + )?; + + let counterparty_addr = + if args.counterparty_addr.is_some() { + args.counterparty_addr.unwrap() + } else { + if !slatepack.sender.is_some() { + panic!("No address to encrypt for. Contracts only support encrypted slates right now."); + } + String::try_from(&slatepack.sender.unwrap())? + }; + let mut slate = owner::slate_from_slatepack_message( + api.wallet_inst.clone(), + keychain_mask, + String::from(slatepack_msg), + vec![0], + )?; + + slate = api.contract_sign(m, &slate, &contract_sign_args)?; + + print_slatepack( + api, + keychain_mask, + &slate, + &counterparty_addr, + args.outfile, + args.as_json, + ); + + if broadcast_tx { + let is_finalized = can_finalize(&slate); + if is_finalized { + api.post_tx(keychain_mask, &slate, true)?; + } + } + + Ok(()) + })?; + + Ok(()) +} + +#[derive(Clone)] +pub struct ContractRevokeArgs { + /// Id of a transaction we want to cancel + pub tx_id: u32, +} + +pub fn contract_revoke( + owner_api: &mut Owner, + keychain_mask: Option<&SecretKey>, + args: ContractRevokeArgs, +) -> Result<(), Error> +where + L: WalletLCProvider<'static, C, K>, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { + let slate_opt = api.contract_revoke(m, &ContractRevokeArgsAPI { tx_id: args.tx_id })?; + // TODO: replace dest="nope" with our own address and add --as-json support + if slate_opt.is_some() { + let slate_out = + prepare_slatepack(api, keychain_mask, &slate_opt.unwrap(), "nope", None)?; + println!("{}", slate_out); + } + + Ok(()) + })?; + + Ok(()) +} diff --git a/controller/tests/common/mod.rs b/controller/tests/common/mod.rs index 114667a0..42a4524d 100644 --- a/controller/tests/common/mod.rs +++ b/controller/tests/common/mod.rs @@ -15,18 +15,24 @@ extern crate grin_wallet_controller as wallet; extern crate grin_wallet_impls as impls; extern crate grin_wallet_libwallet as libwallet; +extern crate log; +use grin_chain as chain; use grin_core as core; use grin_keychain as keychain; use grin_util as util; use self::core::global; use self::core::global::ChainTypes; -use self::keychain::ExtKeychain; +use self::keychain::{ExtKeychain, Keychain}; use self::libwallet::WalletInst; -use impls::test_framework::{LocalWalletClient, WalletProxy}; +use chain::Chain; +use grin_wallet_controller::Error; +use impls::test_framework::{self, LocalWalletClient, WalletProxy}; use impls::{DefaultLCProvider, DefaultWalletImpl}; +use std::sync::atomic::AtomicBool; use std::sync::Arc; +use std::thread; use util::secp::key::SecretKey; use util::{Mutex, ZeroingString}; @@ -176,3 +182,132 @@ pub fn open_local_wallet( .unwrap(); (Arc::new(Mutex::new(wallet)), mask) } + +// Creates the given number of wallets and spawns a thread that runs the wallet proxy +pub fn create_wallets( + wallets_def: Vec>, // a vector of boolean that represent whether we mine into a wallet + test_dir: &'static str, +) -> Result< + ( + Vec<( + Arc< + Mutex< + Box< + dyn WalletInst< + 'static, + DefaultLCProvider<'static, LocalWalletClient, ExtKeychain>, + LocalWalletClient, + ExtKeychain, + >, + >, + >, + >, + Option, + )>, // wallets + Arc, // chain + Arc, // stopper + u64, // block height + ), + Error, +> { + // Create a new proxy to simulate server and wallet responses + let mut wallet_proxy = create_wallet_proxy(test_dir); + let chain = wallet_proxy.chain.clone(); + let stopper = wallet_proxy.running.clone(); + + let mut wallets = vec![]; + for i in 0..wallets_def.len() { + let name = format!("wallet{}", i + 1); + let wclient = LocalWalletClient::new(&name, wallet_proxy.tx.clone()); + let (wallet1, mask1) = create_local_wallet( + test_dir, + &name, + None, + // $seed_phrase.clone(), + wclient.clone(), + true, + ); + wallet_proxy.add_wallet( + &name, + wclient.get_send_instance(), + wallet1.clone(), + mask1.clone(), + ); + wallets.push((wallet1, mask1)); + } + // Set the wallet proxy listener running + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + log::error!("Wallet Proxy error: {}", e); + } + }); + + // // Mine values into wallets + // // few values to keep things shorter + let reward = core::consensus::REWARD; + let mut bh = 0u64; + + for (idx, accs) in wallets_def.iter().enumerate() { + let wallet1 = wallets[idx].0.clone(); + let mask1 = wallets[idx].1.as_ref(); + + for (acc_idx, (acc_name, num_mined_blocks)) in accs.iter().enumerate() { + // create the account + if acc_name.to_string() != "default" { + wallet::controller::owner_single_use( + Some(wallet1.clone()), + mask1, + None, + |api, m| { + let new_path = api.create_account_path(m, acc_name)?; + assert_eq!( + new_path, + ExtKeychain::derive_key_id(2, acc_idx as u32, 0, 0, 0) // NOTE: default should always be at 0 and is already created + ); + Ok(()) + }, + )?; + } + + // Get some mining done + if *num_mined_blocks == 0 { + continue; + } + { + wallet_inst!(wallet1, w); + w.set_parent_key_id_by_name(acc_name)?; + } + let _ = test_framework::award_blocks_to_wallet( + &chain, + wallet1.clone(), + mask1, + *num_mined_blocks as usize, + false, + ); + bh += num_mined_blocks; + + // Sanity check wallet 1 contents + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, bh); + assert_eq!(wallet1_info.total, num_mined_blocks * reward); + Ok(()) + })?; + } + + // Sanity check the number of accounts + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let accounts = api.accounts(m)?; + assert_eq!(accounts.len(), accs.len()); + Ok(()) + })?; + // Set the account on the wallet to "default" + { + wallet_inst!(wallet1, w); + w.set_parent_key_id_by_name("default")?; + } + } + + Ok((wallets, chain, stopper, bh)) +} diff --git a/controller/tests/contract/mod.rs b/controller/tests/contract/mod.rs new file mode 100644 index 00000000..f0ca31a6 --- /dev/null +++ b/controller/tests/contract/mod.rs @@ -0,0 +1,310 @@ +// 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. + +//! Test contract utils +#[macro_use] +extern crate log; +extern crate grin_wallet_controller as wallet; +extern crate grin_wallet_impls as impls; + +use grin_wallet_libwallet as libwallet; +use grin_wallet_util::grin_core as core; +use grin_wallet_util::grin_keychain as keychain; +use grin_wallet_util::grin_util as util; + +use self::keychain::ExtKeychain; +use self::libwallet::WalletInst; +// use impls::test_framework::{LocalWalletClient, WalletProxy}; +use crate::chain::Chain; +use grin_wallet_util::grin_chain as chain; +use impls::{DefaultLCProvider, DefaultWalletImpl}; +use std::sync::Arc; +use util::secp::key::SecretKey; +use util::{Mutex, ZeroingString}; + +use impls::test_framework::{self, LocalWalletClient, WalletProxy}; +use libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI}; +use libwallet::{Slate, SlateState}; +use std::sync::atomic::AtomicBool; +use std::sync::atomic::Ordering; +use std::thread; +use std::time::Duration; + +// #[macro_use] +mod common; +use common::{clean_output_dir, create_local_wallet, create_wallet_proxy, setup}; + +pub fn create_wallets( + test_dir: &'static str, +) -> ( + Vec<( + Arc< + Mutex< + Box< + dyn WalletInst< + 'static, + DefaultLCProvider<'static, LocalWalletClient, ExtKeychain>, + LocalWalletClient, + ExtKeychain, + >, + >, + >, + >, + Option, + )>, + Arc, // chain + Arc, // stopper +) { + // Create a new proxy to simulate server and wallet responses + let mut wallet_proxy = common::create_wallet_proxy(test_dir); + let chain = wallet_proxy.chain.clone(); + let stopper = wallet_proxy.running.clone(); + + let mut wallets = vec![]; + for i in 0..2 { + let name = format!("wallet{}", i + 1); + let wclient = LocalWalletClient::new(&name, wallet_proxy.tx.clone()); + let (wallet1, mask1) = common::create_local_wallet( + test_dir, + &name, + None, + // $seed_phrase.clone(), + wclient.clone(), + true, + ); + wallet_proxy.add_wallet( + &name, + wclient.get_send_instance(), + wallet1.clone(), + mask1.clone(), + ); + // create_wallet_and_add!( + // client1, + // wallet1, + // mask1_i, + // test_dir, + // "wallet1", + // None, + // &mut wallet_proxy, + // true + // ); + wallets.push((wallet1, mask1)); + } + // Set the wallet proxy listener running + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + (wallets, chain, stopper) + + // let $client = LocalWalletClient::new($name, $proxy.tx.clone()); + // let ($wallet, $mask) = common::create_local_wallet( + // $test_dir, + // $name, + // $seed_phrase.clone(), + // $client.clone(), + // $create_mask, + // ); + // $proxy.add_wallet( + // $name, + // $client.get_send_instance(), + // $wallet.clone(), + // $mask.clone(), + // ); +} + +// #[macro_export] +// macro_rules! create_wallets { +// ($client:ident, $wallet: ident, $mask: ident, $test_dir: expr, $name: expr, $seed_phrase: expr, $proxy: expr, $create_mask: expr) => { +// // Create a new proxy to simulate server and wallet responses +// let mut wallet_proxy = create_wallet_proxy(test_dir); +// let chain = wallet_proxy.chain.clone(); +// let stopper = wallet_proxy.running.clone(); + +// let rv = vec![]; +// for i in 0..wallets.len() { +// let name = format!("wallet{}", i + 1); +// let wclient = LocalWalletClient::new(name, wallet_proxy.tx.clone()); +// let (wallet1, mask1) = common::create_local_wallet( +// $test_dir, +// name, +// None, +// // $seed_phrase.clone(), +// wallet_proxy.clone(), +// true, +// ); +// wallet_proxy.add_wallet( +// name, +// wclient.get_send_instance(), +// wallet1.clone(), +// mask1.clone(), +// ); +// // create_wallet_and_add!( +// // client1, +// // wallet1, +// // mask1_i, +// // test_dir, +// // "wallet1", +// // None, +// // &mut wallet_proxy, +// // true +// // ); +// rv.push((wallet1, mask1)); +// } +// rv + +// // let $client = LocalWalletClient::new($name, $proxy.tx.clone()); +// // let ($wallet, $mask) = common::create_local_wallet( +// // $test_dir, +// // $name, +// // $seed_phrase.clone(), +// // $client.clone(), +// // $create_mask, +// // ); +// // $proxy.add_wallet( +// // $name, +// // $client.get_send_instance(), +// // $wallet.clone(), +// // $mask.clone(), +// // ); +// }; +// } + +// prepare wallets +// fn create_wallets( +// wallets: Vec, +// test_dir: &'static str, +// // wallet_proxy: WalletProxy< +// // 'static, +// // DefaultLCProvider<'static, LocalWalletClient, ExtKeychain>, +// // LocalWalletClient, +// // ExtKeychain, +// // >, +// ) -> Vec<( +// Arc< +// Mutex< +// Box< +// dyn WalletInst< +// 'static, +// DefaultLCProvider<'static, LocalWalletClient, ExtKeychain>, +// LocalWalletClient, +// ExtKeychain, +// >, +// >, +// >, +// >, +// Option, +// )> { +// // Create a new proxy to simulate server and wallet responses +// let mut wallet_proxy = create_wallet_proxy(test_dir); +// let chain = wallet_proxy.chain.clone(); +// let stopper = wallet_proxy.running.clone(); + +// let rv = vec![]; +// for _ in 0..wallets.len() { +// create_wallet_and_add!( +// client1, +// wallet1, +// mask1_i, +// test_dir, +// "wallet1", +// None, +// &mut wallet_proxy, +// true +// ); +// rv.push((wallet1, mask1_i)); +// } +// rv +// } + +// /// prepare two wallets for testing +// fn prepare_wallets(n_wallets: u8, test_dir: &'static str) -> Result, libwallet::Error> { +// // Create a new proxy to simulate server and wallet responses +// let mut wallet_proxy = create_wallet_proxy(test_dir); +// let chain = wallet_proxy.chain.clone(); +// let stopper = wallet_proxy.running.clone(); + +// create_wallet_and_add!( +// client1, +// wallet1, +// mask1_i, +// test_dir, +// "wallet1", +// None, +// &mut wallet_proxy, +// true +// ); +// let mask1 = (&mask1_i).as_ref(); +// create_wallet_and_add!( +// client2, +// wallet2, +// mask2_i, +// test_dir, +// "wallet2", +// None, +// &mut wallet_proxy, +// true +// ); +// let mask2 = (&mask2_i).as_ref(); + +// // Set the wallet proxy listener running +// thread::spawn(move || { +// if let Err(e) = wallet_proxy.run() { +// error!("Wallet Proxy error: {}", e); +// } +// }); + +// // few values to keep things shorter +// let reward = core::consensus::REWARD; + +// // add some accounts +// wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { +// api.create_account_path(m, "mining")?; +// api.create_account_path(m, "listener")?; +// Ok(()) +// })?; + +// // Get some mining done +// { +// wallet_inst!(wallet1, w); +// w.set_parent_key_id_by_name("mining")?; +// } +// let mut bh = 10u64; +// let _ = +// test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, bh as usize, false); + +// // Sanity check wallet 1 contents +// wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { +// let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; +// assert!(wallet1_refreshed); +// assert_eq!(wallet1_info.last_confirmed_height, bh); +// assert_eq!(wallet1_info.total, bh * reward); +// Ok(()) +// })?; + +// // let logging finish +// stopper.store(false, Ordering::Relaxed); +// thread::sleep(Duration::from_millis(200)); + +// Ok(()) +// } + +// #[test] +// fn wallet_contract_rsr_tx() -> Result<(), libwallet::Error> { +// let test_dir = "test_output/contract_rsr_tx"; +// setup(test_dir); +// contract_rsr_tx_impl(test_dir)?; +// clean_output_dir(test_dir); +// Ok(()) +// } diff --git a/controller/tests/contract_accounts.rs b/controller/tests/contract_accounts.rs new file mode 100644 index 00000000..25ea5bc7 --- /dev/null +++ b/controller/tests/contract_accounts.rs @@ -0,0 +1,256 @@ +// 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. + +//! Test a wallet doing contracts with different accounts +// #[macro_use] +extern crate grin_wallet_controller as wallet; +extern crate grin_wallet_impls as impls; +extern crate log; + +use grin_core as core; +use grin_keychain as keychain; + +use self::core::global; +use self::keychain::{ExtKeychain, Keychain}; +use grin_wallet_libwallet as libwallet; +use impls::test_framework::{self}; +use libwallet::contract::my_fee_contribution; +use libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI}; +use libwallet::{Slate, SlateState, TxLogEntryType}; +use std::sync::atomic::Ordering; +use std::thread; +use std::time::Duration; + +#[macro_use] +mod common; +use common::{clean_output_dir, create_wallets, setup}; + +/// contract accounts testing (mostly the same as accounts.rs) +fn contract_accounts_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, mut bh) = create_wallets( + vec![ + vec![ + ("default", 0), + ("account1", 0), + ("account2", 0), + ("account3", 0), + ], + vec![("default", 0), ("listener_account", 0)], + ], + test_dir, + ) + .unwrap(); + let wallet1 = wallets[0].0.clone(); + let mask1 = wallets[0].1.as_ref(); + let wallet2 = wallets[1].0.clone(); + let mask2 = wallets[1].1.as_ref(); + + // few values to keep things shorter + let reward = core::consensus::REWARD; + let cm = global::coinbase_maturity(); // assume all testing precedes soft fork height + + // Default wallet 2 to listen on that account + { + wallet_inst!(wallet2, w); + w.set_parent_key_id_by_name("listener_account")?; + } + + // Mine into two different accounts in the same wallet + { + wallet_inst!(wallet1, w); + w.set_parent_key_id_by_name("account1")?; + assert_eq!(w.parent_key_id(), ExtKeychain::derive_key_id(2, 1, 0, 0, 0)); + } + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 7, false); + + { + wallet_inst!(wallet1, w); + w.set_parent_key_id_by_name("account2")?; + assert_eq!(w.parent_key_id(), ExtKeychain::derive_key_id(2, 2, 0, 0, 0)); + } + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 5, false); + + // Should have 5 in account1 (5 spendable), 5 in account (2 spendable) + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, 12); + assert_eq!(wallet1_info.total, 5 * reward); + assert_eq!(wallet1_info.amount_currently_spendable, (5 - cm) * reward); + // check tx log as well + let (_, txs) = api.retrieve_txs(m, true, None, None)?; + assert_eq!(txs.len(), 5); + Ok(()) + })?; + + // now check second account + { + wallet_inst!(wallet1, w); + w.set_parent_key_id_by_name("account1")?; + } + + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + // check last confirmed height on this account is different from above (should be 0) + let (_, wallet1_info) = api.retrieve_summary_info(m, false, 1)?; + assert_eq!(wallet1_info.last_confirmed_height, 0); + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, 12); + assert_eq!(wallet1_info.total, 7 * reward); + assert_eq!(wallet1_info.amount_currently_spendable, 7 * reward); + // check tx log as well + let (_, txs) = api.retrieve_txs(m, true, None, None)?; + assert_eq!(txs.len(), 7); + Ok(()) + })?; + + // should be nothing in default account + { + wallet_inst!(wallet1, w); + w.set_parent_key_id_by_name("default")?; + } + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (_, wallet1_info) = api.retrieve_summary_info(m, false, 1)?; + assert_eq!(wallet1_info.last_confirmed_height, 0); + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, 12); + assert_eq!(wallet1_info.total, 0,); + assert_eq!(wallet1_info.amount_currently_spendable, 0,); + // check tx log as well + let (_, txs) = api.retrieve_txs(m, true, None, None)?; + assert_eq!(txs.len(), 0); + Ok(()) + })?; + + // TODO: check what send_tx_slate_direct call does in accounts.rs test + // TODO: check that you can't call send on the default account because you have no funds + + // Send a tx from wallet1::account1 -> wallet2::listener_account + { + wallet_inst!(wallet1, w); + w.set_parent_key_id_by_name("account1")?; + } + + let mut slate = Slate::blank(0, true); // this gets overriden below + + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + // Send wallet inititates a standard transaction with --send=5 + let args = &ContractNewArgsAPI { + setup_args: ContractSetupArgsAPI { + net_change: Some(-5_000_000_000), + ..Default::default() + }, + ..Default::default() + }; + slate = api.contract_new(m, args)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Standard1); + + wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| { + // Receive wallet calls --receive=5 + let args = &ContractSetupArgsAPI { + net_change: Some(5_000_000_000), + ..Default::default() + }; + slate = api.contract_sign(m, &slate, args)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Standard2); + + // Send wallet finalizes and posts + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let args = &ContractSetupArgsAPI { + ..Default::default() + }; + slate = api.contract_sign(m, &slate, args)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Standard3); + + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + api.post_tx(m, &slate, false)?; + Ok(()) + })?; + bh += 1; + + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, 13); + let (_, txs) = api.retrieve_txs(m, true, None, None)?; + assert_eq!(txs.len(), 9); + Ok(()) + })?; + + // other account should be untouched + { + wallet_inst!(wallet1, w); + w.set_parent_key_id_by_name("account2")?; + } + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (_, wallet1_info) = api.retrieve_summary_info(m, false, 1)?; + assert_eq!(wallet1_info.last_confirmed_height, 12); + let (_, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + assert_eq!(wallet1_info.last_confirmed_height, 13); + let (_, txs) = api.retrieve_txs(m, true, None, None)?; + println!("{:?}", txs); + assert_eq!(txs.len(), 5); + Ok(()) + })?; + + // wallet 2 should only have this tx on the listener account + wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| { + let (wallet2_refreshed, wallet2_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet2_refreshed); + assert_eq!(wallet2_info.last_confirmed_height, 13); + let (_, txs) = api.retrieve_txs(m, true, None, None)?; + assert_eq!(txs.len(), 1); + Ok(()) + })?; + // Default account on wallet 2 should be untouched + { + wallet_inst!(wallet2, w); + w.set_parent_key_id_by_name("default")?; + } + wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| { + let (_, wallet2_info) = api.retrieve_summary_info(m, false, 1)?; + assert_eq!(wallet2_info.last_confirmed_height, 0); + let (wallet2_refreshed, wallet2_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet2_refreshed); + assert_eq!(wallet2_info.last_confirmed_height, 13); + assert_eq!(wallet2_info.total, 0,); + assert_eq!(wallet2_info.amount_currently_spendable, 0,); + // check tx log as well + let (_, txs) = api.retrieve_txs(m, true, None, None)?; + assert_eq!(txs.len(), 0); + Ok(()) + })?; + + // let logging finish + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(200)); + + Ok(()) +} + +#[test] +fn wallet_contract_accounts() -> Result<(), libwallet::Error> { + let test_dir = "test_output/contract_accounts"; + setup(test_dir); + contract_accounts_impl(test_dir)?; + clean_output_dir(test_dir); + Ok(()) +} diff --git a/controller/tests/contract_accounts_switch.rs b/controller/tests/contract_accounts_switch.rs new file mode 100644 index 00000000..74d94f9e --- /dev/null +++ b/controller/tests/contract_accounts_switch.rs @@ -0,0 +1,216 @@ +// 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. + +//! Test a wallet doing contracts with different accounts and switching between them +// #[macro_use] +extern crate grin_wallet_controller as wallet; +extern crate grin_wallet_impls as impls; +extern crate log; + +use grin_core as core; +use grin_keychain as keychain; + +use self::core::global; +use self::keychain::{ExtKeychain, Keychain}; +use grin_wallet_libwallet as libwallet; +use impls::test_framework::{self}; +use libwallet::contract::my_fee_contribution; +use libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI}; +use libwallet::{Slate, SlateState, TxLogEntryType}; +use std::sync::atomic::Ordering; +use std::thread; +use std::time::Duration; + +#[macro_use] +mod common; +use common::{clean_output_dir, create_wallets, setup}; + +/// 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, mut bh) = create_wallets( + vec![ + vec![("default", 0), ("account1", 1), ("account2", 2)], + vec![("default", 0), ("account1", 3), ("account2", 4)], + ], + test_dir, + ) + .unwrap(); + let wallet1 = wallets[0].0.clone(); + let mask1 = wallets[0].1.as_ref(); + let wallet2 = wallets[1].0.clone(); + let mask2 = wallets[1].1.as_ref(); + + let reward = core::consensus::REWARD; + + // wallet1::account1 should have 1 in account1 (1 spendable) + { + wallet_inst!(wallet1, w); + w.set_parent_key_id_by_name("account1")?; + } + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, 10); + assert_eq!(wallet1_info.total, 1 * reward); + assert_eq!(wallet1_info.amount_currently_spendable, 1 * reward); + let (_, txs) = api.retrieve_txs(m, true, None, None)?; + assert_eq!(txs.len(), 1); + Ok(()) + })?; + + // wallet1::account2 should have 2 in account1 (2 spendable) + { + wallet_inst!(wallet1, w); + w.set_parent_key_id_by_name("account2")?; + } + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + // check last confirmed height on this account is different from above (should be 0) + let (_, wallet1_info) = api.retrieve_summary_info(m, false, 1)?; + assert_eq!(wallet1_info.last_confirmed_height, 3); + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, 10); + assert_eq!(wallet1_info.total, 2 * reward); + assert_eq!(wallet1_info.amount_currently_spendable, 2 * reward); + // check tx log as well + let (_, txs) = api.retrieve_txs(m, true, None, None)?; + assert_eq!(txs.len(), 2); + Ok(()) + })?; + + // Make a transaction by sending 5 coins from wallet1::account1 -> wallet2::account2 + { + wallet_inst!(wallet1, w); + w.set_parent_key_id_by_name("account1")?; + } + + let mut slate = Slate::blank(0, true); // this gets overriden below + + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + // Send wallet inititates a standard transaction with --send=5 + let args = &ContractNewArgsAPI { + setup_args: ContractSetupArgsAPI { + net_change: Some(-5_000_000_000), + ..Default::default() + }, + ..Default::default() + }; + slate = api.contract_new(m, args)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Standard1); + + // Receiver gets their coins on account2 where they can payjoin + { + wallet_inst!(wallet2, w); + w.set_parent_key_id_by_name("account2")?; + } + wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| { + // Receive wallet calls --receive=5 + let args = &ContractSetupArgsAPI { + net_change: Some(5_000_000_000), + ..Default::default() + }; + slate = api.contract_sign(m, &slate, args)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Standard2); + + // Switch account for wallet1 to account2 and finish the transaction (should use account1 to complete) + { + wallet_inst!(wallet1, w); + w.set_parent_key_id_by_name("account2")?; + } + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let args = &ContractSetupArgsAPI { + ..Default::default() + }; + slate = api.contract_sign(m, &slate, args)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Standard3); + + // post tx and mine a block to wallet1::account2 + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + api.post_tx(m, &slate, false)?; + Ok(()) + })?; + + // The currently set account (account2) should not be affected by this transaction because they weren't a part of it, + // but it did mine a block and pick the transaction fees + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, 11); + assert_eq!( + wallet1_info.total, + 3 * reward + core::libtx::tx_fee(2, 2, 1) // we have received a block reward and the tx fee (payjoin) + ); + assert_eq!(wallet1_info.amount_currently_spendable, 2 * reward); + let (_, txs) = api.retrieve_txs(m, true, None, None)?; + assert_eq!(txs.len(), 3); + Ok(()) + })?; + + // Switch to wallet1::account1 and check that it sent 5 coins + { + wallet_inst!(wallet1, w); + w.set_parent_key_id_by_name("account1")?; + } + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, 11); + assert_eq!( + wallet1_info.total, + 1 * reward - 5_000_000_000 - my_fee_contribution(1, 1, 1, 2)?.fee() // we subtract also our fee contribution + ); + let (_, txs) = api.retrieve_txs(m, true, None, None)?; + assert_eq!(txs.len(), 2); + Ok(()) + })?; + + // Switch to wallet2::account2 and check that it received 5 coins + { + wallet_inst!(wallet2, w); + w.set_parent_key_id_by_name("account2")?; + } + wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| { + let (wallet2_refreshed, wallet2_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet2_refreshed); + assert_eq!(wallet2_info.last_confirmed_height, 11); + assert_eq!( + wallet2_info.total, + 4 * reward + 5_000_000_000 - my_fee_contribution(1, 1, 1, 2)?.fee() // we subtract also our fee contribution for a payjoin + ); + let (_, txs) = api.retrieve_txs(m, true, None, None)?; + assert_eq!(txs.len(), 5); + Ok(()) + })?; + + // let logging finish + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(200)); + + Ok(()) +} + +#[test] +fn wallet_contract_accounts_switch() -> Result<(), libwallet::Error> { + let test_dir = "test_output/contract_accounts_switch"; + setup(test_dir); + contract_accounts_switch_impl(test_dir)?; + clean_output_dir(test_dir); + Ok(()) +} diff --git a/controller/tests/contract_early_lock.rs b/controller/tests/contract_early_lock.rs new file mode 100644 index 00000000..8ad3bdc0 --- /dev/null +++ b/controller/tests/contract_early_lock.rs @@ -0,0 +1,104 @@ +// 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. + +//! Test a wallet doing contract early lock when using --add-outputs +// #[macro_use] +extern crate grin_wallet_controller as wallet; +extern crate grin_wallet_impls as impls; +extern crate log; + +use grin_wallet_libwallet as libwallet; + +use grin_core::consensus; +use impls::test_framework::{self}; +use libwallet::contract::my_fee_contribution; +use libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI, OutputSelectionArgs}; +use libwallet::{OutputCommitMapping, OutputStatus, Slate, SlateState, TxLogEntryType}; +use std::sync::atomic::Ordering; +use std::thread; +use std::time::Duration; + +#[macro_use] +mod common; +use common::{clean_output_dir, create_wallets, setup}; + +/// contract new with --add-outputs +fn contract_early_lock_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { + // create a single wallet and mine 5 blocks + let (wallets, chain, stopper, mut bh) = + create_wallets(vec![vec![("default", 5)]], test_dir).unwrap(); + let send_wallet = wallets[0].0.clone(); + let send_mask = wallets[0].1.as_ref(); + + // Confirm all our outputs are unspent + wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| { + let (_, commits) = api.retrieve_outputs(m, true, false, None)?; + for commit in commits.iter() { + assert_eq!(commit.output.status, OutputStatus::Unspent); + } + Ok(()) + })?; + + let mut slate = Slate::blank(0, false); // this gets overriden below + + // Call contract 'new' with --add-outputs + wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| { + // Send wallet inititates a standard transaction with --send=80 + let args = &ContractNewArgsAPI { + setup_args: ContractSetupArgsAPI { + net_change: Some(-80_000_000_000), + num_participants: 2, + add_outputs: true, + ..Default::default() + }, + ..Default::default() + }; + slate = api.contract_new(m, args)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Standard1); + + // Assert we locked 2 inputs and prepared an unconfirmed change output + wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| { + let (_, commits) = api.retrieve_outputs(m, true, false, None)?; + // we locked the first two coinbase outputs + assert_eq!(commits[0].output.status, OutputStatus::Locked); + assert_eq!(commits[1].output.status, OutputStatus::Locked); + // we added a new unconfirmed change output + let new_output_idx = commits.len() - 1; + assert_eq!( + commits[new_output_idx].output.status, + OutputStatus::Unconfirmed + ); + assert_eq!( + commits[new_output_idx].output.value, + 2 * consensus::REWARD - 80_000_000_000 - my_fee_contribution(2, 1, 1, 2)?.fee() + ); + Ok(()) + })?; + + // let logging finish + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(200)); + + Ok(()) +} + +#[test] +fn wallet_contract_early_lock_tx() -> Result<(), libwallet::Error> { + let test_dir = "test_output/contract_early_lock_tx"; + setup(test_dir); + contract_early_lock_tx_impl(test_dir)?; + clean_output_dir(test_dir); + Ok(()) +} diff --git a/controller/tests/contract_rsr.rs b/controller/tests/contract_rsr.rs new file mode 100644 index 00000000..91ad6940 --- /dev/null +++ b/controller/tests/contract_rsr.rs @@ -0,0 +1,150 @@ +// 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. + +//! Test a wallet doing contract RSR flow +// #[macro_use] +extern crate grin_wallet_controller as wallet; +extern crate grin_wallet_impls as impls; +extern crate log; + +use grin_wallet_libwallet as libwallet; + +use impls::test_framework::{self}; +use libwallet::contract::my_fee_contribution; +use libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI}; +use libwallet::{Slate, SlateState, TxLogEntryType}; +use std::sync::atomic::Ordering; +use std::thread; +use std::time::Duration; + +#[macro_use] +mod common; +use common::{clean_output_dir, create_wallets, setup}; + +/// contract RSR flow +fn contract_rsr_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { + // create two wallets and mine 4 blocks in each (we want both to have balance to get a payjoin) + let (wallets, chain, stopper, mut bh) = + create_wallets(vec![vec![("default", 4)], vec![("default", 4)]], test_dir).unwrap(); + let send_wallet = wallets[0].0.clone(); + let send_mask = wallets[0].1.as_ref(); + let recv_wallet = wallets[1].0.clone(); + let recv_mask = wallets[1].1.as_ref(); + + let mut slate = Slate::blank(0, true); // this gets overriden below + + wallet::controller::owner_single_use(Some(recv_wallet.clone()), recv_mask, None, |api, m| { + // Receive wallet inititates an invoice transaction with --receive=5 + let args = &ContractNewArgsAPI { + setup_args: ContractSetupArgsAPI { + net_change: Some(5_000_000_000), + ..Default::default() + }, + ..Default::default() + }; + slate = api.contract_new(m, args)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Invoice1); + + wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| { + // Send Wallet calls --send=5 + let args = &ContractSetupArgsAPI { + net_change: Some(-5_000_000_000), + ..Default::default() + }; + slate = api.contract_sign(m, &slate, args)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Invoice2); + + // Receive wallet finalizes and posts + wallet::controller::owner_single_use(Some(recv_wallet.clone()), recv_mask, None, |api, m| { + let args = &ContractSetupArgsAPI { + ..Default::default() + }; + slate = api.contract_sign(m, &slate, args)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Invoice3); + + // Send wallet posts so receive wallet doesn't get the mined amount + wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| { + api.post_tx(m, &slate, false)?; + Ok(()) + })?; + bh += 1; + + let _ = + test_framework::award_blocks_to_wallet(&chain, send_wallet.clone(), send_mask, 3, false); + bh += 3; + + // Assert changes in receive wallet + wallet::controller::owner_single_use(Some(recv_wallet.clone()), recv_mask, None, |api, m| { + let (_, wallet_info) = api.retrieve_summary_info(m, true, 1)?; + let (refreshed, txs) = api.retrieve_txs(m, true, None, None)?; + assert_eq!(wallet_info.last_confirmed_height, bh); + assert!(refreshed); + assert_eq!(txs.len(), 5); // 4 mined and 1 received + let tx_log = txs[4].clone(); + assert_eq!(tx_log.tx_type, TxLogEntryType::TxReceived); + assert_eq!(tx_log.amount_credited, 5_000_000_000); + 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!( + 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 + ); + // println!("txlogentry: {:#?}", tx_log); + // println!("wallet info: {:#?}", wallet_info); + // let (validated, commits) = api.retrieve_outputs(m, true, false, Some(tx_log.id))?; + // println!("commits: {:#?}", commits); + // panic!("lala"); + Ok(()) + })?; + + // Assert changes in send wallet + wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| { + let (_, wallet_info) = api.retrieve_summary_info(m, true, 1)?; + let (refreshed, txs) = api.retrieve_txs(m, true, None, None)?; + assert_eq!(wallet_info.last_confirmed_height, bh); + assert!(refreshed); + assert_eq!(txs.len() as u64, bh - 4 + 1); // send_wallet didn't mine 4 blocks and made 1 tx + let tx_log = txs[txs.len() - 5].clone(); // TODO: why -5 and not -4? + assert_eq!(tx_log.tx_type, TxLogEntryType::TxSent); + assert_eq!(tx_log.amount_credited, 0); + assert_eq!(tx_log.amount_debited, 5_000_000_000); + assert_eq!(tx_log.num_inputs, 1); + assert_eq!(tx_log.num_outputs, 1); + assert_eq!(tx_log.fee, Some(my_fee_contribution(1, 1, 1, 2)?)); + Ok(()) + })?; + + // let logging finish + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(200)); + + Ok(()) +} + +#[test] +fn wallet_contract_rsr_tx() -> Result<(), libwallet::Error> { + let test_dir = "test_output/contract_rsr_tx"; + setup(test_dir); + contract_rsr_tx_impl(test_dir)?; + clean_output_dir(test_dir); + Ok(()) +} diff --git a/controller/tests/contract_self_spend.rs b/controller/tests/contract_self_spend.rs new file mode 100644 index 00000000..dddaf209 --- /dev/null +++ b/controller/tests/contract_self_spend.rs @@ -0,0 +1,111 @@ +// 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. + +//! Test a wallet doing contract self-spend flow +// #[macro_use] +extern crate grin_wallet_controller as wallet; +extern crate grin_wallet_impls as impls; +extern crate log; + +use grin_wallet_libwallet as libwallet; + +use impls::test_framework::{self}; +use libwallet::contract::my_fee_contribution; +use libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI}; +use libwallet::{Slate, SlateState, TxLogEntryType}; +use std::sync::atomic::Ordering; +use std::thread; +use std::time::Duration; + +#[macro_use] +mod common; +use common::{clean_output_dir, create_wallets, setup}; + +/// contract self-spend flow +fn contract_self_spend_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { + // create a single wallet and mine 4 blocks + let (wallets, chain, stopper, mut bh) = + create_wallets(vec![vec![("default", 4)]], test_dir).unwrap(); + let send_wallet = wallets[0].0.clone(); + let send_mask = wallets[0].1.as_ref(); + + let mut slate = Slate::blank(0, true); // this gets overriden below + + wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| { + // Send wallet inititates a standard transaction with --send=0 + let args = &ContractNewArgsAPI { + setup_args: ContractSetupArgsAPI { + net_change: Some(0), + num_participants: 1, + ..Default::default() + }, + ..Default::default() + }; + slate = api.contract_new(m, args)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Standard1); + + // Send wallet finalizes and posts + wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| { + let args = &ContractSetupArgsAPI { + ..Default::default() + }; + slate = api.contract_sign(m, &slate, args)?; + Ok(()) + })?; + // In the case of a self-spend, we just finish the slate when it's in the Standard2 state + assert_eq!(slate.state, SlateState::Standard2); + + wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| { + api.post_tx(m, &slate, false)?; + Ok(()) + })?; + bh += 1; + + let _ = + test_framework::award_blocks_to_wallet(&chain, send_wallet.clone(), send_mask, 3, false); + bh += 3; + + // Assert changes in send wallet + wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| { + let (_, wallet_info) = api.retrieve_summary_info(m, true, 1)?; + let (refreshed, txs) = api.retrieve_txs(m, true, None, None)?; + assert_eq!(wallet_info.last_confirmed_height, bh); + assert!(refreshed); + assert_eq!(txs.len() as u64, bh + 1); // send wallet didn't mine 4 blocks and made 1 tx + let tx_log = txs[txs.len() - 5].clone(); // TODO: why -5 and not -4? + assert_eq!(tx_log.tx_type, TxLogEntryType::TxSent); + assert_eq!(tx_log.amount_credited, 0); + assert_eq!(tx_log.amount_debited, 0); + assert_eq!(tx_log.num_inputs, 1); + assert_eq!(tx_log.num_outputs, 1); + assert_eq!(tx_log.fee, Some(my_fee_contribution(1, 1, 1, 1)?)); + Ok(()) + })?; + + // let logging finish + 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"; + setup(test_dir); + contract_self_spend_tx_impl(test_dir)?; + clean_output_dir(test_dir); + Ok(()) +} diff --git a/controller/tests/contract_self_spend_custom.rs b/controller/tests/contract_self_spend_custom.rs new file mode 100644 index 00000000..fc3f601b --- /dev/null +++ b/controller/tests/contract_self_spend_custom.rs @@ -0,0 +1,159 @@ +// 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. + +//! Test a wallet doing contract self-spend flow by using custom inputs and creating custom outputs +// #[macro_use] +extern crate grin_wallet_controller as wallet; +extern crate grin_wallet_impls as impls; +extern crate log; + +use grin_wallet_libwallet as libwallet; + +use grin_core::consensus; +use impls::test_framework::{self}; +use libwallet::contract::my_fee_contribution; +use libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI, OutputSelectionArgs}; +use libwallet::{OutputStatus, Slate, SlateState, TxLogEntryType}; +use std::sync::atomic::Ordering; +use std::thread; +use std::time::Duration; + +#[macro_use] +mod common; +use common::{clean_output_dir, create_wallets, setup}; + +/// contract self-spend flow with custom picked inputs and outputs +fn contract_self_spend_custom_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { + // create a single wallet and mine 4 blocks + let (wallets, chain, stopper, mut bh) = + create_wallets(vec![vec![("default", 10)]], test_dir).unwrap(); + let send_wallet = wallets[0].0.clone(); + let send_mask = wallets[0].1.as_ref(); + + let mut use_inputs = String::from(""); + + wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| { + let (_, commits) = api.retrieve_outputs(m, true, false, None)?; + use_inputs = format!( + "{},{}", + commits[0].output.commit.as_ref().unwrap(), + commits[1].output.commit.as_ref().unwrap() + ); + Ok(()) + })?; + + let mut slate = Slate::blank(0, true); // this gets overriden below + + let selection_args = OutputSelectionArgs { + min_input_confirmation: 0, + use_inputs: Some(use_inputs.clone()), // we will use two coinbase inputs + make_outputs: Some(String::from("88,35,3,0.2,15")), // the sum is such that it will need to pick another input making total of 3 inputs + }; + wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| { + // Send wallet inititates a standard transaction with --send=0 + let args = &ContractNewArgsAPI { + setup_args: ContractSetupArgsAPI { + net_change: Some(0), + num_participants: 1, + selection_args: selection_args.clone(), + ..Default::default() + }, + ..Default::default() + }; + slate = api.contract_new(m, args)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Standard1); + + // Send wallet finalizes and posts + wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| { + let args = &ContractSetupArgsAPI { + ..Default::default() + }; + slate = api.contract_sign(m, &slate, args)?; + Ok(()) + })?; + // In the case of a self-spend, we just finish the slate when it's in the Standard2 state + assert_eq!(slate.state, SlateState::Standard2); + + wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| { + api.post_tx(m, &slate, false)?; + Ok(()) + })?; + bh += 1; + + let _ = + test_framework::award_blocks_to_wallet(&chain, send_wallet.clone(), send_mask, 3, false); + bh += 3; + + // Assert changes in send wallet + wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| { + let (_, wallet_info) = api.retrieve_summary_info(m, true, 1)?; + let (refreshed, txs) = api.retrieve_txs(m, true, None, None)?; + assert_eq!(wallet_info.last_confirmed_height, bh); + assert!(refreshed); + assert_eq!(txs.len() as u64, bh + 1); // send wallet didn't mine 4 blocks and made 1 tx + let tx_log = txs[txs.len() - 5].clone(); // TODO: why -5 and not -4? + assert_eq!(tx_log.tx_type, TxLogEntryType::TxSent); + assert_eq!(tx_log.amount_credited, 0); + assert_eq!(tx_log.amount_debited, 0); + assert_eq!(tx_log.num_inputs, 3); + assert_eq!(tx_log.num_outputs, 6); + assert_eq!(tx_log.fee, Some(my_fee_contribution(3, 6, 1, 1)?)); + Ok(()) + })?; + + wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| { + let (_, commits) = api.retrieve_outputs(m, true, false, None)?; + // Assert used inputs are the ones we specified + let used_inputs = use_inputs.split(",").collect::>(); + assert_eq!(commits[0].output.status, OutputStatus::Spent); + assert_eq!(commits[0].output.commit.as_ref().unwrap(), used_inputs[0]); + assert_eq!(commits[1].output.status, OutputStatus::Spent); + assert_eq!(commits[1].output.commit.as_ref().unwrap(), used_inputs[1]); + assert_eq!(commits[2].output.status, OutputStatus::Spent); + // Assert expected outputs were created + // 88, 35, 3, 0.2, 15 and a change output + assert_eq!(commits[10].output.value, 88 * consensus::GRIN_BASE); + assert_eq!(commits[11].output.value, 35 * consensus::GRIN_BASE); + assert_eq!(commits[12].output.value, 3 * consensus::GRIN_BASE); + assert_eq!( + commits[13].output.value, + (0.2 * consensus::GRIN_BASE as f64) as u64 + ); + assert_eq!(commits[14].output.value, 15 * consensus::GRIN_BASE); + // change output is 3*reward - (88-35-3-0.2-15) - my_fees + assert_eq!( + commits[15].output.value, + 3 * consensus::REWARD + - selection_args.sum_output_amounts() + - my_fee_contribution(3, 6, 1, 1)?.fee() + ); + Ok(()) + })?; + + // let logging finish + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(200)); + + Ok(()) +} + +#[test] +fn wallet_contract_self_spend_custom_tx() -> Result<(), libwallet::Error> { + let test_dir = "test_output/contract_self_spend_custom_tx"; + setup(test_dir); + contract_self_spend_custom_tx_impl(test_dir)?; + clean_output_dir(test_dir); + Ok(()) +} diff --git a/controller/tests/contract_srs.rs b/controller/tests/contract_srs.rs new file mode 100644 index 00000000..ce0148f7 --- /dev/null +++ b/controller/tests/contract_srs.rs @@ -0,0 +1,144 @@ +// 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. + +//! Test a wallet doing contract SRS flow +// #[macro_use] +extern crate grin_wallet_controller as wallet; +extern crate grin_wallet_impls as impls; +extern crate log; + +use grin_wallet_libwallet as libwallet; + +use impls::test_framework::{self}; +use libwallet::contract::my_fee_contribution; +use libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI}; +use libwallet::{Slate, SlateState, TxLogEntryType}; +use std::sync::atomic::Ordering; +use std::thread; +use std::time::Duration; + +#[macro_use] +mod common; +use common::{clean_output_dir, create_wallets, setup}; + +/// contract SRS flow +fn contract_srs_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { + // create two wallets and mine 4 blocks in each (we want both to have balance to get a payjoin) + let (wallets, chain, stopper, mut bh) = + create_wallets(vec![vec![("default", 4)], vec![("default", 4)]], test_dir).unwrap(); + let send_wallet = wallets[0].0.clone(); + let send_mask = wallets[0].1.as_ref(); + let recv_wallet = wallets[1].0.clone(); + let recv_mask = wallets[1].1.as_ref(); + + let mut slate = Slate::blank(0, true); // this gets overriden below + + wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| { + // Send wallet inititates a standard transaction with --send=5 + let args = &ContractNewArgsAPI { + setup_args: ContractSetupArgsAPI { + net_change: Some(-5_000_000_000), + ..Default::default() + }, + ..Default::default() + }; + slate = api.contract_new(m, args)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Standard1); + + wallet::controller::owner_single_use(Some(recv_wallet.clone()), recv_mask, None, |api, m| { + // Receive wallet calls --receive=5 + let args = &ContractSetupArgsAPI { + net_change: Some(5_000_000_000), + ..Default::default() + }; + slate = api.contract_sign(m, &slate, args)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Standard2); + + // Send wallet finalizes and posts + wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| { + let args = &ContractSetupArgsAPI { + ..Default::default() + }; + slate = api.contract_sign(m, &slate, args)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Standard3); + + wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| { + api.post_tx(m, &slate, false)?; + Ok(()) + })?; + bh += 1; + + let _ = + test_framework::award_blocks_to_wallet(&chain, send_wallet.clone(), send_mask, 3, false); + bh += 3; + + // Assert changes in receive wallet + wallet::controller::owner_single_use(Some(recv_wallet.clone()), recv_mask, None, |api, m| { + let (_, wallet_info) = api.retrieve_summary_info(m, true, 1)?; + let (refreshed, txs) = api.retrieve_txs(m, true, None, None)?; + assert_eq!(wallet_info.last_confirmed_height, bh); + assert!(refreshed); + assert_eq!(txs.len(), 5); // 4 mined and 1 received + let tx_log = txs[4].clone(); + assert_eq!(tx_log.tx_type, TxLogEntryType::TxReceived); + assert_eq!(tx_log.amount_credited, 5_000_000_000); + 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!( + 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 + ); + Ok(()) + })?; + + // Assert changes in send wallet + wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| { + let (_, wallet_info) = api.retrieve_summary_info(m, true, 1)?; + let (refreshed, txs) = api.retrieve_txs(m, true, None, None)?; + assert_eq!(wallet_info.last_confirmed_height, bh); + assert!(refreshed); + assert_eq!(txs.len() as u64, bh - 4 + 1); // send wallet didn't mine 4 blocks and made 1 tx + let tx_log = txs[txs.len() - 5].clone(); // TODO: why -5 and not -4? + assert_eq!(tx_log.tx_type, TxLogEntryType::TxSent); + assert_eq!(tx_log.amount_credited, 0); + assert_eq!(tx_log.amount_debited, 5_000_000_000); + assert_eq!(tx_log.num_inputs, 1); + assert_eq!(tx_log.num_outputs, 1); + assert_eq!(tx_log.fee, Some(my_fee_contribution(1, 1, 1, 2)?)); + Ok(()) + })?; + + // let logging finish + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(200)); + + Ok(()) +} + +#[test] +fn wallet_contract_srs_tx() -> Result<(), libwallet::Error> { + let test_dir = "test_output/contract_srs_tx"; + setup(test_dir); + contract_srs_tx_impl(test_dir)?; + clean_output_dir(test_dir); + Ok(()) +} diff --git a/default.nix b/default.nix new file mode 100644 index 00000000..6e40b0a7 --- /dev/null +++ b/default.nix @@ -0,0 +1,18 @@ +{ pkgs ? import {} }: + + pkgs.mkShell { + nativeBuildInputs = [ pkgs.clang ]; + buildInputs = with pkgs; [ + glibc + rustup + openssl + pkgconfig + llvmPackages.libclang + ncurses + glibcLocales + tor + ]; + shellHook = '' + export LIBCLANG_PATH="${pkgs.llvmPackages.libclang.lib}/lib"; + ''; + } \ No newline at end of file diff --git a/impls/src/backends/lmdb.rs b/impls/src/backends/lmdb.rs index 6a94d057..0ce30a5b 100644 --- a/impls/src/backends/lmdb.rs +++ b/impls/src/backends/lmdb.rs @@ -318,9 +318,41 @@ where Box::new(iter) } - fn get_tx_log_entry(&self, u: &Uuid) -> Result, Error> { - let key = to_key(TX_LOG_ENTRY_PREFIX, &mut u.as_bytes().to_vec()); - self.db.get_ser(&key, None).map_err(|e| e.into()) + // TODO: I think this can be deleted + // fn get_tx_log_entry(&self, u: &Uuid) -> Result, Error> { + // let key = to_key(TX_LOG_ENTRY_PREFIX, &mut u.as_bytes().to_vec()); + + // self.db.get_ser(&key, None).map_err(|e| e.into()) + // } + + fn get_tx_log_entry( + &self, + parent_id: Identifier, + log_id: u32, + ) -> Result, Error> { + let tx_log_key = to_key_u64( + TX_LOG_ENTRY_PREFIX, + &mut parent_id.to_bytes().to_vec(), + log_id as u64, + ); + self.db.get_ser(&tx_log_key, None).map_err(|e| e.into()) + /* + fn save_tx_log_entry( + &mut self, + tx_in: TxLogEntry, + parent_id: &Identifier, + ) -> Result<(), Error> { + let tx_log_key = to_key_u64( + TX_LOG_ENTRY_PREFIX, + &mut parent_id.to_bytes().to_vec(), + tx_in.id as u64, + ); + self.db + .borrow() + .as_ref() + .unwrap() + .put_ser(&tx_log_key, &tx_in)?; + */ } // TODO - fix this awkward conversion between PrefixIterator and our Box diff --git a/libwallet/src/api_impl/foreign.rs b/libwallet/src/api_impl/foreign.rs index be08950f..dd2abb65 100644 --- a/libwallet/src/api_impl/foreign.rs +++ b/libwallet/src/api_impl/foreign.rs @@ -15,8 +15,11 @@ //! Generic implementation of owner API functions use strum::IntoEnumIterator; +use crate::api_impl::owner::contract_new as owner_contract_new; +use crate::api_impl::owner::contract_sign as owner_contract_sign; use crate::api_impl::owner::finalize_tx as owner_finalize; use crate::api_impl::owner::{check_ttl, post_tx}; +use crate::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI}; use crate::grin_core::core::FeeFields; use crate::grin_keychain::Keychain; use crate::grin_util::secp::key::SecretKey; @@ -173,3 +176,48 @@ where } Ok(sl) } + +/// Initialize a receive transaction contract +pub fn contract_new<'a, T: ?Sized, C, K>( + w: &mut T, + keychain_mask: Option<&SecretKey>, + args: &ContractNewArgsAPI, + // use_test_rng: bool, +) -> Result +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let net_change = args.setup_args.net_change.unwrap(); + if net_change <= 0 { + return Err(Error::GenericError( + "Can't create a non-receiving contract from a foreign API.".to_string(), + ) + .into()); + } + owner_contract_new(&mut *w, keychain_mask, args) +} + +/// Sign a receive transaction contract +pub fn contract_sign<'a, T: ?Sized, C, K>( + w: &mut T, + keychain_mask: Option<&SecretKey>, + args: &ContractSetupArgsAPI, + slate: &Slate, + // use_test_rng: bool, +) -> Result +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let net_change = args.net_change.unwrap(); + if net_change <= 0 { + return Err(Error::GenericError( + "Can't sign a non-receiving contract from a foreign API.".to_string(), + ) + .into()); + } + owner_contract_sign(&mut *w, keychain_mask, args, slate) +} diff --git a/libwallet/src/api_impl/owner.rs b/libwallet/src/api_impl/owner.rs index 0ab87e47..3ed6cdd9 100644 --- a/libwallet/src/api_impl/owner.rs +++ b/libwallet/src/api_impl/owner.rs @@ -26,13 +26,14 @@ use crate::grin_util::ToHex; use crate::util::{OnionV3Address, OnionV3AddressError}; use crate::api_impl::owner_updater::StatusMessage; +use crate::contract::types::{ContractNewArgsAPI, ContractRevokeArgsAPI, ContractSetupArgsAPI}; use crate::grin_keychain::{BlindingFactor, Identifier, Keychain, SwitchCommitmentType}; use crate::internal::{keys, scan, selection, tx, updater}; use crate::slate::{PaymentInfo, Slate, SlateState}; use crate::types::{AcctPathMapping, NodeClient, TxLogEntry, WalletBackend, WalletInfo}; use crate::Error; use crate::{ - address, wallet_lock, BuiltOutput, InitTxArgs, IssueInvoiceTxArgs, NodeHeightResult, + address, contract, wallet_lock, BuiltOutput, InitTxArgs, IssueInvoiceTxArgs, NodeHeightResult, OutputCommitMapping, PaymentProof, RetrieveTxQueryArgs, ScannedBlockInfo, Slatepack, SlatepackAddress, Slatepacker, SlatepackerArgs, TxLogEntryType, ViewWallet, WalletInitStatus, WalletInst, WalletLCProvider, @@ -1426,3 +1427,67 @@ where output: output, }) } + +// Contract implementation + +/// Initialize transaction contract +pub fn contract_new<'a, T: ?Sized, C, K>( + w: &mut T, + keychain_mask: Option<&SecretKey>, + args: &ContractNewArgsAPI, + // use_test_rng: bool, +) -> Result +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + contract::new(&mut *w, keychain_mask, &args.setup_args) +} + +// /// Setup transaction contract +// pub fn contract_setup<'a, T: ?Sized, C, K>( +// w: &mut T, +// keychain_mask: Option<&SecretKey>, +// args: &ContractSetupArgsAPI, +// slate: &Slate, +// // use_test_rng: bool, +// ) -> Result +// where +// T: WalletBackend<'a, C, K>, +// C: NodeClient + 'a, +// K: Keychain + 'a, +// { +// contract::setup(&mut *w, keychain_mask, slate, &args) +// } + +/// Sign transaction contract +pub fn contract_sign<'a, T: ?Sized, C, K>( + w: &mut T, + keychain_mask: Option<&SecretKey>, + args: &ContractSetupArgsAPI, + slate: &Slate, + // use_test_rng: bool, +) -> Result +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + contract::sign(&mut *w, keychain_mask, slate, &args) +} + +/// Revoke transaction contract +pub fn contract_revoke<'a, T: ?Sized, C, K>( + w: &mut T, + keychain_mask: Option<&SecretKey>, + args: &ContractRevokeArgsAPI, + // use_test_rng: bool, +) -> Result, Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + contract::revoke(&mut *w, keychain_mask, &args) +} diff --git a/libwallet/src/contract/actions/README.md b/libwallet/src/contract/actions/README.md new file mode 100644 index 00000000..a4906690 --- /dev/null +++ b/libwallet/src/contract/actions/README.md @@ -0,0 +1,116 @@ +# Contract actions + +### API endpoints + +We introduce 3 new API endpoints `/new, /setup, /sign` each corresponding to a specific action on a contract. In the future we'll add the ability to also `view` a contract and to `revoke` it. + +### Rust implementation + +Every contract action on a slate is divided in 3 parts: +1. compute the new state +2. save the new state +3. return slate + +Putting this into code, it looks like the following: +```rust +// Compute the new state (both of the Slate and the Context) +let (slate, context) = compute(slate, args); +// Atomically commit the new state +contract_utils::save_step(slate, context, ...); +// Return the newly produced slate +return slate; +``` + +We only allow contribution of custom inputs/outputs when we're doing the setup phase. Once the setup phase is done, +we no longer allow any customization of inputs. This means that the customization can only happen at contract setup phase which is the first time we see the contract.Additionally, if we customize output selection, we immediately pick the inputs/outputs which means it's an early lock. These are however not added to the slate until we reach the 'sign' phase of the contract. Counterparties don't need to see our inputs/outputs before that. This means we always add inputs/outputs only when we have to and never before. + +Ideally we'd also separate side effects out of these functions e.g. computing the current_height +or refreshing the outputs with updater::refresh_outputs(...). The current_height could be +communicated through a &ChainState parameter which would collect these values before the call. +Additionally, we could fetch the existing Context before the call to avoid doing db fetch. +Separating side effects until the 'save_step' part would make these functions much easier to test. + +#### TODOs + + - for payjoins, instead of doing Some("any"), find an actual input and put the actual commitment in --use-inputs (only do that if none is added). Think about how to do that in a way that would be nice also for the API. Maybe the API should just take "any" which gets transformed into one of the random inputs. Maybe the "any" is ok. It seems to work fine, might be better if we selected the input though. + - make sure to forget the Context when we sign or at least forget the secret keys for it to avoid signing with the same nonce twice + - make_outputs api should receive nanogrins rather than grin. We have to make the conversion before calling the API + - sometimes the slatepack outputs with a \n for some reason which makes pasting it register \n as the end of paste and crashes? + - remove casting decimals, we should accept value in nanogrins (including --make-outputs option), never as 0.1 Grin through the interface. Casting should be left to the gui wallet logic + - Check casts to/from i64/u64 etc. consider using saturating methods. Make sure conversions are safe. + - separate side-effects out from the main computations + - is keys::next_available_key(..) safe from race-conditions? (do we lock?) + - function add_output_to_ctx has a comment around next_available_key + - add support for different accounts not just main (check parent_id, parent_key_id, etc. usage) + - ensure counterparty can't make you overpay fees through num_participants param + - make sure the stored transaction is saved correctly at each step (TxLogEntry has stored_tx field, check other fields as well) + - make sure the transaction log contains all the necessary data (check TODO comments on tx log entry functions) + - graceful error handling + - setup.rs# TODO: verify that the parent_key_id is consistent + - replace mutable objects with immutable when possible + - make sure we lock the wallet when needed (check wallet_lock!() macro that is used in api/owner.rs) + - add support for more than 2 parties (includes a new 'setup' API endpoint and command) + - do we avoid using "too recent" outputs? e.g. though with depth < 10 + - remove unneeded imports + - think if Context.setup_args.net_change type should be u64. If you make it i64, you divide it's size by 2. Perhaps it would be + better to have a u64 field and another field called 'positive' of type bool. + - add --no-setup to 'new' command + - add early-payment proofs. Make sure we have a symmetric variant of a payment proof to avoid having different proofs based on which position you are in the contract signing. Ideally, the position would be irrelevant. + - what happens if you call contract sign on some slate that was not initiated as a contract slate and has different context values? + - make sure you handle all the flows with coinbase outputs as well + - check if they can trick you by providing a slate with different inputs/outputs that are yours + - remove 'setup' API/CLI + - move the contract test utilities to a separate contract_utilities file instead of having it in 'common/mod.rs' + - we have --add-outputs, but we should also lock if we use the --use-inputs param + - check if contract_accounts_switch.rs is a legit scenario. It might need to return an error if the wallet is trying to sign with a different account + +#### Tests + - test contract_fee (various test around fee contribution with 1 or 2 parties) + - test save_step functionality (stored tx, context, logs,..) + - test different output selection in step1 and step3 + - test foreign API for contract new and sign + - test a case where the receiver doesn't have an input available (either not enough confirmations or no inputs) + - contract_rsr.rs asserts that you get amount_credited=5, should it subtract the fees? + - test using more than a single input + - test that sending then again the same slatepack doesn't produce a new signature (to avoid leaking key) + - test locking: + * test that outputs are locked after you sign + * test early locking when using --make-outputs or --use-inputs etc. + - test --no-payjoin + - test accounts + - test 0-value outputs + - test that if --no-payjoin is used, this doesn't mean that we early lock (we shouldn't). Same for --make-outputs + - test slate content through steps + - test negative cases (not enough funds, using input that doesn't exist, make outputs that go over the value, sign twice,...) + +#### DONE + - Always "late-add" inputs/outputs to the slate + - _Always_ add a change output, even if the change output ends up being a 0-value output + + +#### save_step + + // TODO: + // - is_signed should be derived from the slate + // - Check what happens if the batch fails. Also think about possible race conditions because + // of the time delay between the id was picked and saved. + // - Consider taking ownership of Context here. It should not be used after this is called. + + +### Side-effects + +#### Setup + // Side-effects: + // - height = w.w2n_client().get_chain_tip()?.0; + // - maybe_context = w.get_private_context(keychain_mask, sl.id.as_bytes()) + // - create_contract_ctx -> updater::refresh_outputs(wallet, keychain_mask, parent_key_id, false)?; + // - add_outputs -> let current_height = w.w2n_client().get_chain_tip()?.0; + // - add_outputs -> contribute_output -> let key_id = keys::next_available_key(wallet, keychain_mask).unwrap(); + // - TODO: would we need to compute keys::next_available_key for as many outputs as we plan to contribute and pass + // them as a param to keep this without side effects? + +#### Sign + // Side-effects: + // - contract_utils::check_already_signed -> tx_log_iter + // - contract_utils::get_net_change -> context and net_change + // - everything from 'setup' \ No newline at end of file diff --git a/libwallet/src/contract/actions/mod.rs b/libwallet/src/contract/actions/mod.rs new file mode 100644 index 00000000..5681e2ad --- /dev/null +++ b/libwallet/src/contract/actions/mod.rs @@ -0,0 +1,27 @@ +// 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. + +//! This module contains contract related actions. + +mod new; +mod revoke; +mod setup; +mod sign; +mod view; + +pub use self::new::new; +pub use self::revoke::revoke; +pub use self::setup::setup; +pub use self::sign::sign; +pub use self::view::view; diff --git a/libwallet/src/contract/actions/new.rs b/libwallet/src/contract/actions/new.rs new file mode 100644 index 00000000..ce1e5a53 --- /dev/null +++ b/libwallet/src/contract/actions/new.rs @@ -0,0 +1,76 @@ +// 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. + +//! Implementation of contract new + +use crate::contract; +use crate::contract::actions::setup; +use crate::contract::types::ContractSetupArgsAPI; +use crate::error::Error; +use crate::grin_keychain::Keychain; +use crate::grin_util::secp::key::SecretKey; +use crate::slate::Slate; +use crate::types::{Context, NodeClient, WalletBackend}; + +/// Create a new contract with initial setup done by the initiator +pub fn new<'a, T: ?Sized, C, K>( + w: &mut T, + keychain_mask: Option<&SecretKey>, + setup_args: &ContractSetupArgsAPI, +) -> Result +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + // Compute state for 'new' + let (slate, mut context) = compute(w, keychain_mask, setup_args)?; + + // Atomically commit state + contract::utils::save_step( + w, + keychain_mask, + &slate, + &mut context, + setup_args.add_outputs, + false, + )?; + + Ok(slate) +} + +/// Compute logic for new +pub fn compute<'a, T: ?Sized, C, K>( + w: &mut T, + keychain_mask: Option<&SecretKey>, + setup_args: &ContractSetupArgsAPI, +) -> Result<(Slate, Context), Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let net_change = setup_args.net_change.unwrap(); + debug!("contract::new => net_change passed: {}", net_change); + + // Initialize a new contract (if net_change is positive, I'm the receiver meaning this is invoice flow) + let num_participants = setup_args.num_participants; + let mut slate = Slate::blank(num_participants, net_change > 0); + // We set slate.amount to contain the _positive_ net_change for the other party so they can derive expectations. + slate.amount = net_change.abs() as u64; + debug!("contract::new => slate amount: {}", slate.amount); + + // Perform setup for the slate + setup::compute(w, keychain_mask, &mut slate, setup_args) +} diff --git a/libwallet/src/contract/actions/revoke.rs b/libwallet/src/contract/actions/revoke.rs new file mode 100644 index 00000000..659a4049 --- /dev/null +++ b/libwallet/src/contract/actions/revoke.rs @@ -0,0 +1,102 @@ +// 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. + +//! Implementation of contract revoke + +use crate::contract::types::{ContractRevokeArgsAPI, ContractSetupArgsAPI, OutputSelectionArgs}; +use crate::contract::{new, sign}; +use crate::error::Error; +use crate::grin_keychain::Keychain; +use crate::grin_util::secp::key::SecretKey; +use crate::internal::tx; +use crate::slate::Slate; +use crate::types::{NodeClient, OutputData, OutputStatus, WalletBackend}; + +/// Contract revocation is done by double-spending the input +pub fn revoke<'a, T: ?Sized, C, K>( + w: &mut T, + keychain_mask: Option<&SecretKey>, + args: &ContractRevokeArgsAPI, +) -> Result, Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + // TODO: check the correctness of this. This is essentially old cancel + self-spend. + // FUTURE: we may want to boost fees in case we notice something in the mempool. There + // are also race conditions possible. We may not want to label txlogenry as Canceled + // until the new tx gets on the chain. + // NOTE: We should not care about deleting the context because as soon as we sign + // a contract, the context is deleted. + + // If we contributed inputs, we must have locked them at which point we also set the + // OutputData.tx_log_entry which is the tx_id. + let tx_id = args.tx_id; + + // Find my outputs that have been Locked and refer to the given tx_id + let my_contributed_inputs = w + .batch(keychain_mask)? + .iter() + .filter(|out| { + // Find an output that is Locked and is in the tx_input_commit + out.status == OutputStatus::Locked + && (out.tx_log_entry.is_some() && out.tx_log_entry.as_ref().unwrap() == &tx_id) + }) + .collect::>(); + + // 1. Unlock the input by calling cancel_tx + let parent_key_id = w.parent_key_id(); + tx::cancel_tx(&mut *w, keychain_mask, &parent_key_id, Some(tx_id), None)?; + + if my_contributed_inputs.len() == 0 { + return Ok(None); + } + let input_commit = my_contributed_inputs[0].commit.as_ref().unwrap(); + // 2. Create a 1-1 self-spend transaction using this input + let ct_slate = new( + w, + keychain_mask, + &ContractSetupArgsAPI { + // TODO: Check the src_acct_name below. This would use the currently active account + src_acct_name: None, + net_change: Some(0), // self-spend + num_participants: 1, + add_outputs: false, + selection_args: OutputSelectionArgs { + use_inputs: Some(String::from(input_commit)), + ..Default::default() + }, + }, + )?; + let finished_slate = sign( + w, + keychain_mask, + &ct_slate, + &ContractSetupArgsAPI { + // TODO: Check the src_acct_name below. This would use the currently active account + src_acct_name: None, + net_change: None, // we already have it in the context as 0 now + num_participants: 1, + add_outputs: false, + selection_args: OutputSelectionArgs { + use_inputs: Some(String::from(input_commit)), + ..Default::default() + }, + }, + )?; + // TODO: Think about what to do with transaction context of the cancelled slate. It should probably get deleted. + + Ok(Some(finished_slate)) +} diff --git a/libwallet/src/contract/actions/setup.rs b/libwallet/src/contract/actions/setup.rs new file mode 100644 index 00000000..5d814d60 --- /dev/null +++ b/libwallet/src/contract/actions/setup.rs @@ -0,0 +1,86 @@ +// 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. + +//! Implementation of contract setup + +use crate::api_impl::owner::check_ttl; +use crate::contract; +use crate::contract::types::ContractSetupArgsAPI; +use crate::error::Error; +use crate::grin_keychain::Keychain; +use crate::grin_util::secp::key::SecretKey; +use crate::slate::Slate; +use crate::types::{Context, NodeClient, WalletBackend}; + +/// Perform a contract setup +pub fn setup<'a, T: ?Sized, C, K>( + w: &mut T, + keychain_mask: Option<&SecretKey>, + slate: &Slate, + setup_args: &ContractSetupArgsAPI, +) -> Result +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + // Compute state for 'setup' + let (slate, mut context) = compute(w, keychain_mask, slate, setup_args)?; + + // Atomically commit state + contract::utils::save_step( + w, + keychain_mask, + &slate, + &mut context, + setup_args.add_outputs, + false, + )?; + + Ok(slate) +} + +/// Compute logic for setup +pub fn compute<'a, T: ?Sized, C, K>( + w: &mut T, + keychain_mask: Option<&SecretKey>, + slate: &Slate, + setup_args: &ContractSetupArgsAPI, +) -> Result<(Slate, Context), Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let mut sl = slate.clone(); + check_ttl(w, &sl)?; + + // Get or create a transaction Context and verify consistency of setup arguments + let mut context = contract::context::get_or_create(w, keychain_mask, &mut sl, setup_args)?; + contract::utils::verify_setup_args_consistency( + &context.setup_args.as_ref().unwrap(), + &setup_args, + )?; + + // Add keys and payment proof to slate (both are idempotent operations) + contract::slate::add_keys(&mut sl, &w.keychain(keychain_mask)?, &mut context)?; + contract::slate::add_payment_proof(&mut sl)?; // noop for the sender + + // Add inputs/outputs to the Context if needed. No locking is done here. This happens at save_step. + if setup_args.add_outputs { + contract::context::add_outputs(&mut *w, keychain_mask, &mut context)?; + } + + Ok((sl, context)) +} diff --git a/libwallet/src/contract/actions/sign.rs b/libwallet/src/contract/actions/sign.rs new file mode 100644 index 00000000..38fd02ac --- /dev/null +++ b/libwallet/src/contract/actions/sign.rs @@ -0,0 +1,91 @@ +// 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. + +//! Implementation of contract sign + +use crate::contract; +use crate::contract::actions::setup; +use crate::contract::types::ContractSetupArgsAPI; +use crate::error::Error; +use crate::grin_keychain::Keychain; +use crate::grin_util::secp::key::SecretKey; +use crate::slate::Slate; +use crate::types::{Context, NodeClient, WalletBackend}; + +/// Sign a contract +pub fn sign<'a, T: ?Sized, C, K>( + w: &mut T, + keychain_mask: Option<&SecretKey>, + slate: &Slate, + setup_args: &ContractSetupArgsAPI, +) -> Result +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + // Compute if we will add outputs at this step + let will_add_outputs = match w.get_private_context(keychain_mask, slate.id.as_bytes()) { + Ok(ctx) => ctx.get_inputs().len() + ctx.get_outputs().len() == 0, + Err(_) => true, + }; + // Compute state for 'sign' + let (sl, mut context) = compute(w, keychain_mask, slate, setup_args)?; + + // Atomically commit state + contract::utils::save_step(w, keychain_mask, &sl, &mut context, will_add_outputs, true)?; + + Ok(sl) +} + +/// Compute logic for sign +pub fn compute<'a, T: ?Sized, C, K>( + w: &mut T, + keychain_mask: Option<&SecretKey>, + slate: &Slate, + setup_args: &ContractSetupArgsAPI, +) -> Result<(Slate, Context), Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let mut sl = slate.clone(); + contract::utils::verify_not_signed(w, sl.id)?; + + // Ensure net_change has been provided + let expected_net_change = + contract::utils::get_net_change(w, keychain_mask, &sl, setup_args.net_change)?; + + // Define the values that must be provided in the setup phase at the sign step + let mut setup_args = setup_args.clone(); + setup_args.net_change = Some(expected_net_change); + setup_args.num_participants = sl.num_participants; + setup_args.add_outputs = true; // we add outputs to the Context in case we haven't done that yet + + // Ensure Setup phase is done and that inputs/outputs have been added to the Context + let (mut sl, mut context) = setup::compute(w, keychain_mask, &mut sl, &setup_args)?; + // Add outputs to the slate, verify the payment proof and sign the slate + contract::slate::add_outputs(w, keychain_mask, &mut sl, &context)?; + contract::slate::verify_payment_proof(&sl)?; // noop for the receiver + contract::slate::sign(w, keychain_mask, &mut sl, &mut context)?; + contract::slate::transition_state(&mut sl)?; + + // If we have all the partial signatures, finalize the tx + if contract::slate::can_finalize(&sl) { + contract::slate::finalize(w, keychain_mask, &mut sl)?; + } + + Ok((sl, context)) +} diff --git a/libwallet/src/contract/actions/view.rs b/libwallet/src/contract/actions/view.rs new file mode 100644 index 00000000..744ac6e0 --- /dev/null +++ b/libwallet/src/contract/actions/view.rs @@ -0,0 +1,65 @@ +// 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. + +//! Implementation of contract view + +use crate::contract::types::ContractView; +use crate::error::Error; +use crate::grin_keychain::Keychain; +use crate::grin_util::secp::key::SecretKey; +use crate::slate::{Slate, SlateState}; +use crate::types::{NodeClient, WalletBackend}; + +/// View contract +pub fn view<'a, T: ?Sized, C, K>( + w: &mut T, + keychain_mask: Option<&SecretKey>, + slate: &mut Slate, + encrypted_for: &str, +) -> Result +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + // NOTE: This should only be run on slates that we received and were signed for us. + // Otherwise, you can't really predict who the party doing the next step should be. + + // TODO: Do we need to do any slate verification here? + let suggested_net_change: Option = match slate.state { + // TODO: Check bounds against overflow/underflow + SlateState::Invoice1 => Some(slate.amount as i64), + SlateState::Standard1 => Some(-(slate.amount as i64)), + _ => None, + }; + let is_executed = false; + let num_sigs = slate + .participant_data + .clone() + .into_iter() + .filter(|v| !v.is_complete()) + .count(); + + // TODO: Maybe we can know if the slate was meant for us if it was encrypted for us. + // A possible issue is that one can encrypt the same slate for 10 people. + let ct_view = ContractView { + num_participants: slate.num_participants, + suggested_net_change: suggested_net_change, + agreed_net_change: None, // TODO + num_sigs: num_sigs as u8, + is_executed: is_executed, + ..Default::default() + }; + Ok(ct_view) +} diff --git a/libwallet/src/contract/context.rs b/libwallet/src/contract/context.rs new file mode 100644 index 00000000..07764073 --- /dev/null +++ b/libwallet/src/contract/context.rs @@ -0,0 +1,204 @@ +// 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 functions on the Context + +use crate::contract::selection::prepare_outputs; +use crate::contract::types::ContractSetupArgsAPI; +use crate::contract::utils as contract_utils; +use crate::grin_keychain::{Identifier, Keychain}; +use crate::grin_util::secp::key::SecretKey; +use crate::internal::{keys, updater}; +use crate::slate::Slate; +use crate::types::{Context, NodeClient, WalletBackend}; +use crate::{Error, OutputData}; +use grin_core::core::FeeFields; + +/// Get or create transaction Context for the given slate +pub fn get_or_create<'a, T: ?Sized, C, K>( + w: &mut T, + keychain_mask: Option<&SecretKey>, + slate: &mut Slate, + setup_args: &ContractSetupArgsAPI, +) -> Result +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + debug!("contract::context::get_or_create => called"); + let maybe_context = w.get_private_context(keychain_mask, slate.id.as_bytes()); + + let context = match maybe_context { + Err(_) => { + // Get data required for creating a context + let height = w.w2n_client().get_chain_tip()?.0; + let parent_key_id = + contract_utils::parent_key_for(w, setup_args.src_acct_name.as_ref()); + self::create( + w, + keychain_mask, + slate, + height, + // &args, + setup_args, + &parent_key_id, + false, + )? + } + Ok(ctx) => ctx, + }; + Ok(context) +} + +/// Creates a context for a contract +fn create<'a, T: ?Sized, C, K>( + w: &mut T, + keychain_mask: Option<&SecretKey>, + slate: &mut Slate, + current_height: u64, + // TODO: compare with &InitTxArgs to see if any information is missing + setup_args: &ContractSetupArgsAPI, + parent_key_id: &Identifier, + use_test_rng: bool, +) -> Result +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + debug!("Creating a new contract context"); + // sender should always refresh outputs + updater::refresh_outputs(w, keychain_mask, parent_key_id, false)?; + + // Fee contribution estimation + let net_change = setup_args.net_change.unwrap(); + // select inputs to estimate fee cost + let (inputs, _, my_fee) = + prepare_outputs(w, &parent_key_id, current_height, &setup_args, None)?; + // The number of outputs we expect is the number of custom outputs plus one change output + debug!( + "My fee contribution estimation: {} for n_inputs: {}, n_outputs: {}, n_kernels: {}, num_participants: {}", + my_fee.fee(), inputs.len(), setup_args.selection_args.num_custom_outputs() + 1, 1, setup_args.num_participants + ); + // Make sure `my_fee < net_change` holds for the receiver. This can't be true for a self-spend, because nobody + // has a net_change > 0 which makes a self-spend ok to be a net negative when fees are included. + if net_change > 0 && my_fee.fee() > net_change.abs() as u64 { + panic!( + "My contribution as a receiver would be net negative. my_fee: {}, net_change: {}", + my_fee.fee(), + net_change + ); + } + // Add my share of fee contribution to the slate fees + slate.fee_fields = FeeFields::new(0, slate.fee_fields.fee() + my_fee.fee())?; + debug!("Slate.fee: {}", slate.fee_fields.fee()); + + // Create a Context for this slate + let keychain = w.keychain(keychain_mask)?; + // TODO: it seems 'is_initiator: true' is only used in test_rng. Do we care about this? + let mut context = Context::new(keychain.secp(), &parent_key_id, use_test_rng, true); + // Context.fee will hold _our_ fee contribution and not the total slate fee + context.fee = my_fee.as_opt(); + // Context.amount is not used in contracts, but we set it anyway. + context.amount = slate.amount; + // TODO: looking at what uses Context.late_lock_args, it seems only the args in SelectionArgs are used except + // for args.ttl_blocks. Is this needed? Can we refactor this? + context.setup_args = Some(setup_args.clone()); + debug!( + "Setting Context.net_change as: {}", + context.get_net_change() + ); + + Ok(context) +} + +/// Add outputs to a contract context (including spent outputs which get locked) +pub fn add_outputs<'a, T: ?Sized, C, K>( + w: &mut T, + keychain_mask: Option<&SecretKey>, + context: &mut Context, +) -> Result<(), Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + debug!("contract::utils::add_outputs => called"); + // Do nothing if we have already contributed our outputs. The assumption is that if this was done, + // our output contribution is complete. + if context.output_ids.len() > 0 || context.input_ids.len() > 0 { + debug!("contract::utils::add_outputs => outputs have already been added, returning."); + return Ok(()); + } + let setup_args = context.setup_args.as_ref().unwrap(); + debug!("contract::utils::add_outputs => adding outputs"); + let current_height = w.w2n_client().get_chain_tip()?.0; + let parent_key_id = &context.parent_key_id; + + // Select inputs for which `Σmy_inputs >= Σmy_outputs + my_fee_cost` holds. Uses committed fee if present. + let (inputs, my_output_amounts, my_fee) = prepare_outputs( + &mut *w, + parent_key_id, + current_height, + &setup_args, + context.fee, + )?; + assert_eq!(my_fee.fee(), context.fee.unwrap().fee(), "my_fee!=ctx.fee"); + // Add selected/created inputs/outputs to the context + add_inputs_to_ctx(context, &inputs)?; + add_outputs_to_ctx(w, keychain_mask, context, my_output_amounts)?; + + Ok(()) +} + +/// Add inputs to Context +fn add_inputs_to_ctx(context: &mut Context, inputs: &Vec) -> Result<(), Error> { + debug!("contract::utils::add_inputs_to_ctx => adding inputs to context"); + for input in inputs { + context.add_input(&input.key_id, &input.mmr_index, input.value); + debug!( + "contract::utils::add_inputs_to_ctx => input id: {}, value:{}", + &input.key_id, input.value + ); + } + + Ok(()) +} + +/// Add outputs to Context +fn add_outputs_to_ctx<'a, T: ?Sized, C, K>( + w: &mut T, + keychain_mask: Option<&SecretKey>, + context: &mut Context, + amounts: Vec, +) -> Result<(), Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + for amount in amounts { + // TODO: it seems like next_available_key does not respect the parent_key_id. Check if it does, it probably should? + // A late-lock might have a different account set to active than the one that was set to the Context + let key_id = keys::next_available_key(w, keychain_mask).unwrap(); + context.add_output(&key_id, &None, amount); + debug!( + "contract::utils::add_output_to_ctx => added output to context. Output id: {}, amount: {}", + key_id.clone(), + amount + ); + } + Ok(()) +} diff --git a/libwallet/src/contract/mod.rs b/libwallet/src/contract/mod.rs new file mode 100644 index 00000000..7f9d656f --- /dev/null +++ b/libwallet/src/contract/mod.rs @@ -0,0 +1,27 @@ +// 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. + +//! This module contains contract related actions. + +mod actions; +mod context; +mod selection; +mod slate; +pub mod types; +mod utils; + +pub use self::actions::{new, revoke, setup, sign, view}; + +pub use self::slate::can_finalize; +pub use self::utils::my_fee_contribution; diff --git a/libwallet/src/contract/selection.rs b/libwallet/src/contract/selection.rs new file mode 100644 index 00000000..6b98afe1 --- /dev/null +++ b/libwallet/src/contract/selection.rs @@ -0,0 +1,596 @@ +// 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, +) -> Result<(Vec, Vec, 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, 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::>(); + 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, + inputs: &mut Vec, +) -> Result<(Vec, Vec, 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::(), + 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, + inputs: &mut Vec, +) -> Result<(Vec, 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::>(); + + // 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 { + 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::(); + // 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) -> Vec { + let mut rv: Vec = 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::>(), + 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::>(), + 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::>(), + 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 + + */ +} diff --git a/libwallet/src/contract/slate.rs b/libwallet/src/contract/slate.rs new file mode 100644 index 00000000..02c06769 --- /dev/null +++ b/libwallet/src/contract/slate.rs @@ -0,0 +1,237 @@ +// 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 functions on the Slate + +use crate::grin_core::libtx::build; +use crate::grin_core::libtx::proof::ProofBuilder; +use crate::grin_keychain::Keychain; +use crate::grin_util::secp::key::SecretKey; +use crate::slate::{Slate, SlateState}; +use crate::types::{Context, NodeClient, WalletBackend}; +use crate::Error; + +/// The secret key we replace the actual key with after we have signed with the Context keys. This is +/// to prevent possibility of signing with the same key twice. +pub const SEC_KEY_FAKE: [u8; 32] = [0; 32]; + +/// Add payment proof data to slate +pub fn add_payment_proof(slate: &mut Slate) -> Result<(), Error> { + // TODO: Implement. Consider adding this function to the Slate itself so they can easily be versioned + // e.g. slate.add_payment_proof_data() + debug!("contract::slate::add_payment_proof => called (not implemented yet)"); + Ok(()) +} + +/// Verify payment proof signature +pub fn verify_payment_proof(slate: &Slate) -> Result<(), Error> { + // TODO: Implement. Consider adding this function to the Slate itself so they can easily be versioned + // e.g. slate.verify_payment_proof_sig() + debug!("contract::slate::verify_payment_proof => called (not implemented yet)"); + Ok(()) +} + +/// Adds inputs and outputs to slate +pub fn add_outputs<'a, T: ?Sized, C, K>( + w: &mut T, + keychain_mask: Option<&SecretKey>, + slate: &mut Slate, + context: &Context, +) -> Result<(), Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + add_inputs_to_slate(w, keychain_mask, slate, context)?; + add_outputs_to_slate(w, keychain_mask, slate, context)?; + // Adjust the offset for the added input and outputs + let keychain = &w.keychain(keychain_mask)?; + slate.adjust_offset(keychain, &context)?; + + Ok(()) +} + +/// Contribute inputs to slate +fn add_inputs_to_slate<'a, T: ?Sized, C, K>( + w: &mut T, + keychain_mask: Option<&SecretKey>, + slate: &mut Slate, + context: &Context, +) -> Result<(), Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + debug!("contract::slate::add_inputs_to_slate => adding inputs to slate"); + let keychain = w.keychain(keychain_mask)?; + let batch = w.batch(keychain_mask)?; + for (key_id, mmr_index, _) in context.get_inputs() { + // We have no information if the input is a coinbase or not, so we fetch the data from DB + let coin = batch.get(&key_id, &mmr_index).unwrap(); + if coin.is_coinbase { + slate.add_transaction_elements( + &keychain, + &ProofBuilder::new(&keychain), + vec![build::coinbase_input(coin.value, coin.key_id.clone())], + )?; + debug!( + "contract::slate::add_inputs_to_slate => added coinbase input id: {}, value: {}", + coin.key_id.clone(), + coin.value + ); + } else { + slate.add_transaction_elements( + &keychain, + &ProofBuilder::new(&keychain), + vec![build::input(coin.value, coin.key_id.clone())], + )?; + debug!( + "contract::slate::add_inputs_to_slate => added regular input id: {}, value: {}", + coin.key_id.clone(), + coin.value + ); + } + } + + Ok(()) +} + +/// Contribute outputs to slate +fn add_outputs_to_slate<'a, T: ?Sized, C, K>( + w: &mut T, + keychain_mask: Option<&SecretKey>, + slate: &mut Slate, + context: &Context, +) -> Result<(), Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + debug!("contract::slate::add_outputs_to_slate => start"); + let keychain = w.keychain(keychain_mask)?; + // Iterate over outputs in the Context and add the same output to the slate + for (key_id, _, amount) in context.get_outputs() { + slate.add_transaction_elements( + &keychain, + &ProofBuilder::new(&keychain), + vec![build::output(amount, key_id.clone())], + )?; + debug!( + "contract::slate::add_outputs_to_slate => added output to slate. Output id: {}, amount: {}", + key_id.clone(), + amount + ); + } + + Ok(()) +} + +/// Transition the slate state to the next one +pub fn transition_state(slate: &mut Slate) -> Result<(), Error> { + // We don't really use these states right now apart from leaving it to derive expected net_change. + // This suggests these can't be used for manipulation. It doesn't hurt to think a bit more if that's the case. + let new_state = match slate.state { + SlateState::Invoice1 => SlateState::Invoice2, + SlateState::Invoice2 => SlateState::Invoice3, + SlateState::Standard1 => SlateState::Standard2, + SlateState::Standard2 => SlateState::Standard3, + _ => { + debug!("Slate.state: {}", slate.state); + SlateState::Standard3 + } + }; + slate.state = new_state; + // NOTE: It's possible to never reach the step3. A self-spend has only 2 steps: new -> sign. + Ok(()) +} + +/// Add partial signature to the slate. +// TODO: Should be a sign & forget pubkey+nonce implementation. +pub fn sign<'a, T: ?Sized, C, K>( + w: &mut T, + keychain_mask: Option<&SecretKey>, + slate: &mut Slate, + context: &mut Context, +) -> Result<(), Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + debug!("contract::slate::sign => called"); + let keychain = w.keychain(keychain_mask)?; + slate.fill_round_2(&keychain, &context.sec_key, &context.sec_nonce)?; + debug!( + "contract::sign => signed for slate fees: {}", + slate.fee_fields + ); + debug!("contract::slate::sign => done"); + + // TODO: This produces a secp error, probably need a valid key. Verify that this is what we want to do. + // let fake_key = SecretKey::from_slice(keychain.secp(), &SEC_KEY_FAKE)?; + // context.sec_key = fake_key.clone(); + // context.sec_nonce = fake_key.clone(); + // context.initial_sec_key = fake_key.clone(); + // context.initial_sec_nonce = fake_key.clone(); + + Ok(()) +} + +/// We can finalize if all partial sigs are present +pub fn can_finalize(slate: &Slate) -> bool { + let res = slate + .participant_data + .clone() + .into_iter() + .filter(|v| !v.is_complete()) + .count(); + + // We can finalize if the number of partial sigs is the same as the number of participants + res == 0 && slate.participant_data.len() == slate.num_participants as usize +} + +/// Finalize slate +pub fn finalize<'a, T: ?Sized, C, K>( + w: &mut T, + keychain_mask: Option<&SecretKey>, + slate: &mut Slate, +) -> Result<(), Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + debug!("contract::slate::finalize => called"); + // Final transaction can be built by anyone at this stage + trace!("Slate to finalize is: {}", slate); + // At this point, everyone adjusted their offset, so we update the offset on the tx + slate.tx_or_err_mut()?.offset = slate.offset.clone(); + slate.finalize(&w.keychain(keychain_mask)?)?; + + Ok(()) +} + +/// Perform 'setup' step for a contract. This adds our public key and nonce to the slate +/// The operation should be idempotent. +pub fn add_keys(slate: &mut Slate, keychain: &K, context: &mut Context) -> Result<(), Error> +where + K: Keychain, +{ + debug!("contract::slate::add_keys => called"); + // TODO: Is this safe from manipulation? + slate.add_participant_info(keychain, context, None) +} diff --git a/libwallet/src/contract/types.rs b/libwallet/src/contract/types.rs new file mode 100644 index 00000000..905c9c4b --- /dev/null +++ b/libwallet/src/contract/types.rs @@ -0,0 +1,184 @@ +// 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. + +//! Types related to a contract + +use crate::grin_core::consensus; + +/// Output selection args +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct OutputSelectionArgs { + /// Constraint on how many confirmations used inputs must have + pub min_input_confirmation: u64, + /// Which inputs we want to use - default to payjoin if available with Some("any") + pub use_inputs: Option, + /// Change output specification (comma separated amounts which don't include fee subtraction) + /// e.g. "3,1,4,0,0" describes 5 outputs two of which hold 0 value + pub make_outputs: Option, +} + +impl OutputSelectionArgs { + /// We try to make a payjoin if use_inputs has a value (either commitments or Some("any")) + pub fn is_payjoin(&self) -> bool { + self.use_inputs.is_some() + } + /// Return a list of commitments we must use + pub fn required_inputs(&self) -> Option> { + if self.use_inputs.is_some() { + Some( + self.use_inputs.as_ref().unwrap()[..] + .split(",") + .filter(|x| *x != "any") + .collect(), + ) + } else { + None + } + } + /// Returns the outputs we have to create + pub fn output_amounts(&self) -> Vec { + if self.make_outputs.is_some() { + let output_amounts: Vec = self.make_outputs.as_ref().unwrap()[..] + .split(",") + // TODO: move consensus code outside of here. Consider turning make_outputs to Vec + .map(|amt| (amt.parse::().unwrap() * consensus::GRIN_BASE as f64) as u64) + .collect(); + output_amounts + } else { + vec![] + } + } + /// Returns the sum of our output amounts + pub fn sum_output_amounts(&self) -> u64 { + self.output_amounts().iter().sum() + } + /// Returns the number of custom outputs + pub fn num_custom_outputs(&self) -> usize { + self.output_amounts().len() + } + + // TODO: make sure to validate this: if custom outputs are specified, it has to be a payjoin. +} + +impl Default for OutputSelectionArgs { + fn default() -> OutputSelectionArgs { + OutputSelectionArgs { + min_input_confirmation: 10, + use_inputs: Some(String::from("any")), + make_outputs: None, + } + } +} + +/// Contract Setup - defines how we pick inputs/outputs and what we expect from a contract. Both +/// 'new' and 'sign' actions perform a setup phase which is why their endpoints take these parameters. +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct ContractSetupArgsAPI { + /// The human readable account name from which to draw outputs + /// for the transaction, overriding whatever the active account is as set via the + /// [`set_active_account`](../grin_wallet_api/owner/struct.Owner.html#method.set_active_account) method. + pub src_acct_name: Option, + /// The net change we will agree on. The amount is in nanogrins (`1 G = 1_000_000_000nG`). + /// The value is positive when we are on the receiving end and negative when we are the sender. + /// It is optional because we could have agreed on it before we reach the sign e.g. when we create new contract + pub net_change: Option, + /// The number of participants in a contract. Used for computing our kernel fee contribution + pub num_participants: u8, + /// Should we perform an early lock of outputs + pub add_outputs: bool, + /// Output selection arguments + pub selection_args: OutputSelectionArgs, +} + +impl Default for ContractSetupArgsAPI { + fn default() -> ContractSetupArgsAPI { + ContractSetupArgsAPI { + src_acct_name: None, + net_change: None, + num_participants: 2, + add_outputs: false, + selection_args: OutputSelectionArgs { + ..Default::default() + }, + } + } +} + +/// Contract New +#[derive(Clone, Serialize, Deserialize)] +pub struct ContractNewArgsAPI { + /// TODO: do we need the target_slate_version? + /// Optionally set the output target slate version (acceptable + /// down to the minimum slate version compatible with the current. If `None` the slate + /// is generated with the latest version. + pub target_slate_version: Option, + /// Setup args - contract new also initiates the setup by default + pub setup_args: ContractSetupArgsAPI, +} + +impl Default for ContractNewArgsAPI { + fn default() -> ContractNewArgsAPI { + ContractNewArgsAPI { + target_slate_version: None, + setup_args: ContractSetupArgsAPI { + src_acct_name: None, + net_change: None, + num_participants: 2, + add_outputs: false, + selection_args: OutputSelectionArgs { + ..Default::default() + }, + }, + } + } +} + +/// ContractView +#[derive(Clone, Serialize, Deserialize)] +pub struct ContractView { + /// TODO: do we need the target_slate_version? + pub target_slate_version: Option, + /// Every slatepack has a number of participants + pub num_participants: u8, + /// Suggested value for the party at step2 (only provided if slatepack is at step1) + pub suggested_net_change: Option, + /// Agreed net_change if we've agreed on it (the context must exist for this) + // NOTE: we drop the Context once we've signed. Perhaps we should think about dropping + // only the private keys associated with it to prevent double-signing with the same + // (pubkey, nonce) pair. This way, we'd retain the history on that wallet instance. + // There might also be value in forgetting the whole context. + pub agreed_net_change: Option, + /// Number of singatures on the contract + pub num_sigs: u8, + /// Has the contract been executed on chain + pub is_executed: bool, +} + +impl Default for ContractView { + fn default() -> ContractView { + ContractView { + target_slate_version: None, + num_participants: 2, + suggested_net_change: None, + agreed_net_change: None, + num_sigs: 0, + is_executed: false, + } + } +} +#[derive(Clone, Serialize, Deserialize)] +pub struct ContractRevokeArgsAPI { + /// Tx id to cancel + pub tx_id: u32, +} diff --git a/libwallet/src/contract/utils.rs b/libwallet/src/contract/utils.rs new file mode 100644 index 00000000..02aeef69 --- /dev/null +++ b/libwallet/src/contract/utils.rs @@ -0,0 +1,352 @@ +// 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::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, TxLogEntryType, WalletBackend}; +use crate::{Error, OutputData, OutputStatus, TxLogEntry}; +use grin_core::core::FeeFields; +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 { + let log_type = if net_change > 0 { + TxLogEntryType::TxReceived + } else { + TxLogEntryType::TxSent + }; + let mut t = TxLogEntry::new(parent_key_id.clone(), log_type, log_id); + // TODO: TxLogEntry has stored_tx field. Check what this needs to be set to and check other fields as well + + 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) +} + +/// Updates TxLogEntry for a contract with information available in the 'sign' step +pub fn update_tx_log_entry<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + slate: &Slate, + context: &Context, + tx_log_entry: &mut TxLogEntry, +) -> Result<(), Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + // This is expected to be called when we are signing the contract and have already contributed inputs & outputs + let keychain = wallet.keychain(keychain_mask)?; + 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 + match slate.calc_excess(keychain.secp()) { + Ok(e) => tx_log_entry.kernel_excess = Some(e), + Err(_) => panic!("We can't update tx log entry. Excess could not be computed."), + }; + tx_log_entry.kernel_lookup_min_height = Some(current_height); + + 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<'a, T: ?Sized, C, K>( + w: &mut T, + keychain_mask: Option<&SecretKey>, + // TODO: make this receive only slate.id instead of passing the whole slate + slate: &Slate, + setup_args_net_change: Option, +) -> Result +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let mut expected_net_change: Option = setup_args_net_change; + match w.get_private_context(keychain_mask, slate.id.as_bytes()) { + Ok(ctx) => { + 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. + match expected_net_change { + Some(args_net_change) => { + if ctx.get_net_change() != args_net_change { + panic!( + "Expected net change mismatch! Context.net_change: {}, setup_args.net_change: {}", + ctx.get_net_change(), args_net_change + ); + } + } + None => (), + } + expected_net_change = Some(ctx.get_net_change()); + } + Err(_) => 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()) +} + +/// Atomically locks the inputs and saves the changes of Context, TxLogEntry and OutputData. +/// Additionally, the transaction is saved in a file in case we signed it. +pub fn save_step<'a, T: ?Sized, C, K>( + w: &mut T, + keychain_mask: Option<&SecretKey>, + slate: &Slate, + context: &mut Context, + step_added_outputs: bool, + is_signed: bool, +) -> Result<(), Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + 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 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_some() && is_signed; + + let mut tx_log_entry = { + if !context.log_id.is_some() { + // 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 { + w.get_tx_log_entry(parent_key_id.clone(), context.log_id.unwrap())? + .unwrap() + } + }; + // Update TxLogEntry if we have signed the contract (we have data about the kernel) + if is_signed { + update_tx_log_entry(w, keychain_mask, &slate, &context, &mut tx_log_entry)?; + // TODO: It's possible to store the transaction in a file while and the atomic commit below fails + // In this case, we should revert to the previous stored tx to avoid having discrepancy + w.store_tx(&format!("{}", slate.id), slate.tx_or_err()?)?; + } + // 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 = 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_some() { + // 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).unwrap(); + // 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. + // The reason we don't delete if we are at step2 is because in case we want to do safe cancel, + // we need to know which inputs are in the context to know which input we have to double-spend. + batch.delete_private_context(slate.id.as_bytes())?; + } else { + batch.save_private_context(slate.id.as_bytes(), &context)?; + } + + batch.commit()?; + + // TODO: Assert we don't have the context to avoid potentially leaking it! Also write tests around this. + 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 { + // Add our fee costs for our inputs and a single output + let mut fee = tx_fee(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); + // TODO: we slightly overpay. Make sure to cover all the cases + let my_kernel_cost = (kernel_cost as f64 / (num_participants as f64)).ceil(); + fee += my_kernel_cost as u64; + + // Add my fee contribution to the slate total fee. + // TODO: Does this break compatibility with existing slates? + let my_fee_fields = FeeFields::new(0, fee)?; + Ok(my_fee_fields) +} + +/// 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<'a, T: ?Sized, C, K>(w: &mut T, slate_id: Uuid) -> Result<(), Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + // If we have a transaction log entry for that slatepack that has a kernel value, then + // we have already signed this slate. + let tx = w + .tx_log_iter() + .find(|t| t.tx_slate_id.is_some() && t.tx_slate_id.unwrap() == slate_id); + let already_signed = tx.is_some() && tx.unwrap().kernel_excess.is_some(); + if already_signed { + 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).into(), + ) + .into()); + } + + 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() { + panic!( + "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 { + panic!( + "Inconsistent num_participants. Ctx num_participants:{}, Current num_participants: {}", + ctx_setup_args.num_participants, cur_setup_args.num_participants + ); + } + // TODO: Should we verify add_outputs? + // TODO: verify that the parent_key_id is consistent, perhaps even with the active_account set? + + // 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 +pub fn parent_key_for<'a, T: ?Sized, C, K>(w: &mut T, src_acct_name: Option<&String>) -> Identifier +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + // TODO: does it matter what api.set_active_account is set? also check LMDB set_parent_key_id etc. methods + // - Does it matter what api.set_active_account is set? I think w.parent_key_id() already takes the active one + // but the verify_consistency may need to verify this or perhaps give a warning that active is different than + // the one that was set at the first setup phase. + let parent_key_id = match src_acct_name { + Some(d) => { + let pm = w.get_acct_path(d.clone()).unwrap(); + match pm { + Some(p) => p.path, + // TODO: should we error if the path is not found? + None => w.parent_key_id(), + } + } + None => w.parent_key_id(), + }; + parent_key_id +} diff --git a/libwallet/src/lib.rs b/libwallet/src/lib.rs index 3c663a9c..5862469b 100644 --- a/libwallet/src/lib.rs +++ b/libwallet/src/lib.rs @@ -45,6 +45,7 @@ extern crate strum_macros; pub mod address; pub mod api_impl; +pub mod contract; mod error; mod internal; mod slate; @@ -77,6 +78,8 @@ pub use types::{ WalletOutputBatch, }; +pub use contract::can_finalize; + /// Helper for taking a lock on the wallet instance #[macro_export] macro_rules! wallet_lock { diff --git a/libwallet/src/slate.rs b/libwallet/src/slate.rs index 43a7ba90..2673af5e 100644 --- a/libwallet/src/slate.rs +++ b/libwallet/src/slate.rs @@ -276,8 +276,12 @@ impl Slate { kernel_features_args: None, } } + /// Removes any signature data that isn't mine, for compacting /// slates for a return journey + // TODO: Check if this is a noop when we have only 2 parties. The first sig appears at + // step2 and removing everything except your sig means you remove nothing. For more than + // 2 parties, we should probably never remove the part_sigs so that everyone can verify them. pub fn remove_other_sigdata( &mut self, keychain: &K, @@ -310,13 +314,22 @@ impl Slate { K: Keychain, B: ProofBuild, { + debug!("slate::add_transaction_elements => start"); self.update_kernel()?; + + debug!("slate::add_transaction_elements => kernel updated"); if elems.is_empty() { + debug!("slate::add_transaction_elements => elems is empty, returning"); return Ok(BlindingFactor::zero()); } + let (tx, blind) = build::partial_transaction(self.tx_or_err()?.clone(), &elems, keychain, builder)?; + + debug!("slate::add_transaction_elements => built partial transaction"); self.tx = Some(tx); + + debug!("slate::add_transaction_elements => slate.tx is set"); Ok(blind) } diff --git a/libwallet/src/types.rs b/libwallet/src/types.rs index 3a63b7aa..9bffe94d 100644 --- a/libwallet/src/types.rs +++ b/libwallet/src/types.rs @@ -16,6 +16,7 @@ //! implementation use crate::config::{TorConfig, WalletConfig}; +use crate::contract::types::ContractSetupArgsAPI; use crate::error::Error; use crate::grin_core::core::hash::Hash; use crate::grin_core::core::FeeFields; @@ -189,7 +190,15 @@ where fn get(&self, id: &Identifier, mmr_index: &Option) -> Result; /// Get an (Optional) tx log entry by uuid - fn get_tx_log_entry(&self, uuid: &Uuid) -> Result, Error>; + // TODO: I think this can be deleted + // fn get_tx_log_entry(&self, uuid: &Uuid) -> Result, Error>; + + /// Get an (Optional) tx log entry by uuid + fn get_tx_log_entry( + &self, + parent_id: Identifier, + log_id: u32, + ) -> Result, Error>; /// Retrieves the private context associated with a given slate id fn get_private_context( @@ -563,6 +572,13 @@ pub struct Context { /// for invoice I2 Only, store the tx excess so we can /// remove it from the slate on return pub calculated_excess: Option, + /// Arguments that define which outputs to pick for a contract + pub setup_args: Option, + /// TxLogEntry id (needed to avoid a linear scan) + // Services that keep a long history might need to search + // through a list when they need to update a txlogentry. + // This is why we keep the id in the context. + pub log_id: Option, } impl Context { @@ -612,6 +628,8 @@ impl Context { payment_proof_derivation_index: None, late_lock_args: None, calculated_excess: None, + setup_args: None, + log_id: None, } } } @@ -652,6 +670,11 @@ impl Context { PublicKey::from_secret_key(secp, &self.sec_nonce).unwrap(), ) } + + /// Returns net_change for the contract + pub fn get_net_change(&self) -> i64 { + self.setup_args.as_ref().unwrap().net_change.unwrap() + } } impl ser::Writeable for Context { diff --git a/src/bin/grin-wallet.yml b/src/bin/grin-wallet.yml index 39a8962c..a88efb4b 100644 --- a/src/bin/grin-wallet.yml +++ b/src/bin/grin-wallet.yml @@ -446,3 +446,149 @@ subcommands: - input: help: Filename of a proof file index: 1 + - contract: + subcommands: + - new: + about: Create a new contract with initial setup + args: + - encrypt-for: + help: The counter party grin address + short: e + long: encrypt-for + takes_value: true + # index: 1 + - receive: + help: How much you want to receive + short: r + long: receive + takes_value: true + - send: + help: How much you want to send + short: s + long: send + takes_value: true + - num-participants: + help: How many participants are involved? (can be 1 or 2) + short: n + long: num-participants + takes_value: true + default_value: "2" + - as-json: + help: Show result as JSON + short: j + long: as-json + takes_value: false + - no-payjoin: + help: Don't make it a payjoin (if receiver) + long: no-payjoin + takes_value: false + - add-outputs: + help: Defines whether we should pick inputs/outputs in the first step. + short: a + long: add-outputs + takes_value: false + - use-inputs: + help: Which inputs you want to use (provide comma separated commitments) + short: i + long: use-inputs + takes_value: true + - make-outputs: + help: Which outputs should we create? (provide comma separated amounts) + short: o + long: make-outputs + takes_value: true + # - setup: + # about: Perform a key setup on a contract + # args: + # - encrypt-for: + # help: The counter party grin address + # short: e + # long: encrypt-for + # takes_value: true + # # index: 1 + # - receive: + # help: How much you want to receive + # short: r + # long: receive + # takes_value: true + # - send: + # help: How much you want to send + # short: s + # long: send + # takes_value: true + # - as-json: + # help: Show result as JSON + # short: j + # long: as-json + # takes_value: false + # - no-payjoin: + # help: Don't make it a payjoin (if receiver) + # long: no-payjoin + # takes_value: false + # - add-outputs: + # help: Defines whether we should pick inputs/outputs in the setup step. + # short: a + # long: add-outputs + # takes_value: false + # - use-inputs: + # help: Which inputs you want to use (provide comma separated commitments) + # short: i + # long: use-inputs + # takes_value: true + # - make-outputs: + # help: Which outputs should we create? (provide comma separated amounts) + # short: o + # long: make-outputs + # takes_value: true + - sign: + about: Sign a contract + args: + - encrypt-for: + help: The counter party grin address + short: e + long: encrypt-for + takes_value: true + # index: 1 + - receive: + help: How much you want to receive + short: r + long: receive + takes_value: true + - send: + help: How much you want to send + short: s + long: send + takes_value: true + - as-json: + help: Show result as JSON + short: j + long: as-json + takes_value: false + - no-payjoin: + help: Don't make it a payjoin (if receiver) + long: no-payjoin + takes_value: false + - use-inputs: + help: Which inputs you want to use (provide comma separated commitments) + short: i + long: use-inputs + takes_value: true + - make-outputs: + help: Which outputs should we create? (provide comma separated amounts) + short: o + long: make-outputs + takes_value: true + - no-broadcast: + help: Don't broadcast the transaction if it was finalized + long: no-broadcast + takes_value: false + - view: + about: View a contract + - revoke: + about: Attempt to revoke a contract + args: + - tx-id: + help: Id of a transaction we want to cancel + short: i + long: tx-id + takes_value: true diff --git a/src/cmd/wallet_args.rs b/src/cmd/wallet_args.rs index 866db6ca..a0f48d2b 100644 --- a/src/cmd/wallet_args.rs +++ b/src/cmd/wallet_args.rs @@ -12,13 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. +/// Argument parsing and error handling for wallet commands +use self::core::consensus; use crate::api::TLSConfig; use crate::cli::command_loop; use crate::config::GRIN_WALLET_DIR; use crate::util::file::get_first_line; use crate::util::secp::key::SecretKey; use crate::util::{Mutex, ZeroingString}; -/// Argument parsing and error handling for wallet commands use clap::ArgMatches; use grin_core as core; use grin_core::core::amount_to_hr_string; @@ -965,6 +966,148 @@ pub fn parse_verify_proof_args(args: &ArgMatches) -> Result Result { + let counterparty_addr = args.value_of("encrypt-for").unwrap(); + + // TODO: Make sure the values are in some expected bounds. + // TODO: How to deal with decimals and precision? we probably want users to express the value in Grin. + // Parse receive and send params and convert them to nano grin + let receive = match args.value_of("receive") { + Some(g) => Some((g.parse::().unwrap() * consensus::GRIN_BASE as f64) as u64), + None => None, + }; + let send = match args.value_of("send") { + Some(g) => Some((g.parse::().unwrap() * consensus::GRIN_BASE as f64) as u64), + None => None, + }; + if receive.is_some() && send.is_some() { + return Err(ParseError::ArgumentError(String::from( + "You can only specify receive or send, not both.", + ))); + }; + // TODO: verify this is correct e.g. which values are passed here by default etc. + let src_acct_name = Some(String::from(account)); + let add_outputs = args.is_present("add-outputs"); + 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") { + Some(v) => { + if no_payjoin { + panic!("Can't use --no-payjoin with --use-inputs."); + } + Some(String::from(v)) + } + None => { + if no_payjoin { + None + } else { + // Some("any") means pick 1 random input to contribute (payjoin) + Some(String::from("any")) + } + } + }; + let make_outputs = match args.value_of("make-outputs") { + Some(v) => Some(String::from(v)), + None => None, + }; + + let num_participants = args + .value_of("num-participants") + .unwrap() + .parse::() + .unwrap(); + + Ok(command::ContractNewArgs { + counterparty_addr: String::from(counterparty_addr), + receive: receive, + send: send, + src_acct_name: src_acct_name, + num_participants: num_participants, + as_json: as_json, + add_outputs: add_outputs, + use_inputs: use_inputs, + make_outputs: make_outputs, + // TODO: Future features below + fee_rate: None, + outfile: None, + }) +} + +// TODO: parse args +pub fn parse_contract_setup_args( + args: &ArgMatches, +) -> Result { + // TODO: Make sure the values are in some expected bounds. + // TODO: How to deal with decimals and precision? we probably want users to express the value in Grin. + let counterparty_addr = match args.value_of("encrypt-for") { + Some(v) => Some(String::from(v)), + None => None, + }; + // Parse receive and send params and convert them to nano grin + let receive = match args.value_of("receive") { + Some(g) => Some((g.parse::().unwrap() * consensus::GRIN_BASE as f64) as u64), + None => None, + }; + let send = match args.value_of("send") { + Some(g) => Some((g.parse::().unwrap() * consensus::GRIN_BASE as f64) as u64), + None => None, + }; + if receive.is_some() && send.is_some() { + return Err(ParseError::ArgumentError(String::from( + "You can only specify receive or send, not both.", + ))); + }; + 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") { + Some(v) => { + if no_payjoin { + panic!("Can't use --no-payjoin with --use-inputs."); + } + Some(String::from(v)) + } + None => { + if no_payjoin { + None + } else { + // Some("any") means pick 1 random input to contribute (payjoin) + Some(String::from("any")) + } + } + }; + let make_outputs = match args.value_of("make-outputs") { + Some(v) => Some(String::from(v)), + None => None, + }; + // TODO: should we catch if the person calls "--receive=5" when it should be "--send=5"? + // Perhaps we could detect this from the slate state e.g. S1 -> receive, I1 -> send? + + Ok(command::ContractSetupArgs { + counterparty_addr: counterparty_addr, + receive: receive, + send: send, + as_json: as_json, + add_outputs: false, + use_inputs: use_inputs, + make_outputs: make_outputs, + // TODO: Future features below + fee_rate: None, + outfile: None, + }) +} + +pub fn parse_contract_revoke_args( + args: &ArgMatches, +) -> Result { + let tx_id = args.value_of("tx-id").unwrap().parse::().unwrap(); + + Ok(command::ContractRevokeArgs { tx_id: tx_id }) +} + pub fn wallet_command( wallet_args: &ArgMatches, mut wallet_config: WalletConfig, @@ -1295,6 +1438,31 @@ where // for CLI mode only, should be handled externally Ok(()) } + ("contract", Some(args)) => match args.subcommand() { + ("new", Some(new_args)) => { + let account = &global_wallet_args.account; + let a = arg_parse!(parse_contract_new_args(&new_args, account)); + command::contract_new(owner_api, km, a) + } + // ("setup", Some(setup_args)) => { + // let a = arg_parse!(parse_contract_setup_args(&setup_args)); + // command::contract_setup(owner_api, km, a) + // } + ("sign", Some(sign_args)) => { + // Sign command takes setup_args so we use the same parser + let setup_args = arg_parse!(parse_contract_setup_args(&sign_args)); + let broadcast_tx = !sign_args.is_present("no-broadcast"); + command::contract_sign(owner_api, km, setup_args, broadcast_tx) + } + ("view", Some(view_args)) => { + Err(Error::ArgumentError(String::from("Not implemented")).into()) + } + ("revoke", Some(revoke_args)) => { + let a = arg_parse!(parse_contract_revoke_args(&revoke_args)); + command::contract_revoke(owner_api, km, a) + } + _ => Err(Error::ArgumentError(String::from("Unknown contract subcommand.")).into()), + }, _ => { let msg = format!("Unknown wallet command, use 'grin-wallet help' for details"); return Err(Error::ArgumentError(msg));