diff --git a/doc/wallet/usage.md b/doc/wallet/usage.md index 37482b33e..768f0ae3d 100644 --- a/doc/wallet/usage.md +++ b/doc/wallet/usage.md @@ -268,6 +268,15 @@ Other flags here are: [host]$ grin wallet send -f -d "http://192.168.0.10:13415" 60.00 ``` +* `-g` 'Message' - You can specify an optional message to include alongside your transaction data. This message is purely for informational +purposes between all transacting participants, and is not included in transaction data sent to the chain. Each participant message includes +a signature that can be verified with the participant's public key. A message can also be specified by the recipient during a `grin wallet receive` +command. + +```sh +[host]$ grin wallet send -f -d "http://192.168.0.10:13415" -g "This is from Dave" 60.00 +``` + ### outputs Simply displays all the the outputs in your wallet: e.g: @@ -322,7 +331,6 @@ Transaction Log - Account 'default' - Block Height: 49 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ 6 Received Tx 03715cf6-f29b-4a3a-bda5-b02cba6bf0d9 2018-07-20 19:46:46.120244904 UTC false None 0 1 60.000000000 0.000000000 None 60.000000000 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ->>>>>>> master To see the inputs/outputs associated with a particular transaction, use the `-i` switch providing the Id of the given transaction, e.g: diff --git a/servers/tests/framework/mod.rs b/servers/tests/framework/mod.rs index b01e6eb8d..43ebb3527 100644 --- a/servers/tests/framework/mod.rs +++ b/servers/tests/framework/mod.rs @@ -352,6 +352,7 @@ impl LocalServerContainer { max_outputs, change_outputs, selection_strategy == "all", + None, )?; slate = client_w.send_tx_sync(dest, &slate)?; api.finalize_tx(&mut slate)?; diff --git a/servers/tests/simulnet.rs b/servers/tests/simulnet.rs index a7c9a757d..444f2ddf6 100644 --- a/servers/tests/simulnet.rs +++ b/servers/tests/simulnet.rs @@ -969,6 +969,7 @@ fn replicate_tx_fluff_failure() { 500, // max outputs 1000, // num change outputs true, // select all outputs + None, )?; slate = client1_w.send_tx_sync(dest, &slate)?; api.finalize_tx(&mut slate)?; diff --git a/src/bin/cmd/wallet.rs b/src/bin/cmd/wallet.rs index 7329d18be..d2b633be2 100644 --- a/src/bin/cmd/wallet.rs +++ b/src/bin/cmd/wallet.rs @@ -355,6 +355,10 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) -> i e )) })?; + let message = match send_args.is_present("message") { + true => Some(send_args.value_of("message").unwrap().to_owned()), + false => None, + }; let minimum_confirmations: u64 = send_args .value_of("minimum_confirmations") .ok_or_else(|| { @@ -417,6 +421,7 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) -> i max_outputs, change_outputs, selection_strategy == "all", + message, ); let (mut slate, lock_fn) = match result { Ok(s) => { @@ -453,11 +458,15 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) -> i slate = adapter.send_tx_sync(dest, &slate)?; if method == "self" { controller::foreign_single_use(wallet, |api| { - api.receive_tx(&mut slate, Some(dest))?; + api.receive_tx(&mut slate, Some(dest), None)?; Ok(()) })?; } api.tx_lock_outputs(&slate, lock_fn)?; + if let Err(e) = api.verify_slate_messages(&slate) { + error!("Error validating participant messages: {}", e); + return Err(e); + } api.finalize_tx(&mut slate)?; } else { adapter.send_tx_async(dest, &slate)?; @@ -481,6 +490,10 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) -> i } ("receive", Some(send_args)) => { let mut receive_result: Result<(), grin_wallet::libwallet::Error> = Ok(()); + let message = match send_args.is_present("message") { + true => Some(send_args.value_of("message").unwrap().to_owned()), + false => None, + }; let tx_file = send_args.value_of("input").ok_or_else(|| { ErrorKind::GenericError("Transaction file required".to_string()) })?; @@ -492,7 +505,7 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) -> i let adapter = FileWalletCommAdapter::new(); let mut slate = adapter.receive_tx_async(tx_file)?; controller::foreign_single_use(wallet, |api| { - api.receive_tx(&mut slate, Some(account))?; + api.receive_tx(&mut slate, Some(account), message)?; Ok(()) })?; let send_tx = format!("{}.response", tx_file); @@ -515,6 +528,10 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) -> i } let adapter = FileWalletCommAdapter::new(); let mut slate = adapter.receive_tx_async(tx_file)?; + if let Err(e) = api.verify_slate_messages(&slate) { + error!("Error validating participant messages: {}", e); + return Err(e); + } let _ = api.finalize_tx(&mut slate).expect("Finalize failed"); let result = api.post_tx(&slate.tx, fluff); diff --git a/src/bin/grin.rs b/src/bin/grin.rs index e1272a8b3..dd141a551 100644 --- a/src/bin/grin.rs +++ b/src/bin/grin.rs @@ -246,15 +246,25 @@ fn real_main() -> i32 { .arg(Arg::with_name("fluff") .help("Fluff the transaction (ignore Dandelion relay protocol)") .short("f") - .long("fluff"))) + .long("fluff")) + .arg(Arg::with_name("message") + .help("Optional participant message to include") + .short("g") + .long("message") + .takes_value(true)) .arg(Arg::with_name("stored_tx") .help("If present, use the previously stored Unconfirmed transaction with given id.") .short("t") .long("stored_tx") - .takes_value(true)) + .takes_value(true))) .subcommand(SubCommand::with_name("receive") .about("Processes a transaction file to accept a transfer from a sender.") + .arg(Arg::with_name("message") + .help("Optional participant message to include") + .short("g") + .long("message") + .takes_value(true)) .arg(Arg::with_name("input") .help("Partial transaction to process, expects the sender's transaction file.") .short("i") diff --git a/wallet/src/controller.rs b/wallet/src/controller.rs index 363966342..d6a4f8d84 100644 --- a/wallet/src/controller.rs +++ b/wallet/src/controller.rs @@ -171,7 +171,7 @@ where } } - fn retrieve_outputs( + pub fn retrieve_outputs( &self, req: &Request, api: APIOwner, @@ -195,7 +195,7 @@ where api.retrieve_outputs(show_spent, update_from_node, id) } - fn retrieve_txs( + pub fn retrieve_txs( &self, req: &Request, api: APIOwner, @@ -222,7 +222,7 @@ where api.retrieve_txs(update_from_node, tx_id, tx_slate_id) } - fn retrieve_stored_tx( + pub fn retrieve_stored_tx( &self, req: &Request, api: APIOwner, @@ -251,7 +251,7 @@ where } } - fn retrieve_summary_info( + pub fn retrieve_summary_info( &self, req: &Request, mut api: APIOwner, @@ -269,7 +269,7 @@ where api.retrieve_summary_info(update_from_node, minimum_confirmations) } - fn node_height( + pub fn node_height( &self, _req: &Request, mut api: APIOwner, @@ -297,7 +297,7 @@ where }) } - fn issue_send_tx( + pub fn issue_send_tx( &self, req: Request, mut api: APIOwner, @@ -310,6 +310,7 @@ where args.max_outputs, args.num_change_outputs, args.selection_strategy_is_use_all, + args.message, ); let (mut slate, lock_fn) = match result { Ok(s) => { @@ -353,7 +354,7 @@ where })) } - fn finalize_tx( + pub fn finalize_tx( &self, req: Request, mut api: APIOwner, @@ -369,7 +370,7 @@ where ) } - fn cancel_tx( + pub fn cancel_tx( &self, req: Request, mut api: APIOwner, @@ -414,7 +415,7 @@ where } } - fn post_tx( + pub fn post_tx( &self, req: Request, api: APIOwner, @@ -548,7 +549,8 @@ where mut api: APIForeign, ) -> Box + Send> { Box::new(parse_body(req).and_then( - move |mut slate| match api.receive_tx(&mut slate, None) { + //TODO: No way to insert a message from the params + move |mut slate| match api.receive_tx(&mut slate, None, None) { Ok(_) => ok(slate.clone()), Err(e) => { error!("receive_tx: failed with error: {}", e); diff --git a/wallet/src/libtx/aggsig.rs b/wallet/src/libtx/aggsig.rs index f00bc6ee6..e87b920bd 100644 --- a/wallet/src/libtx/aggsig.rs +++ b/wallet/src/libtx/aggsig.rs @@ -420,6 +420,28 @@ pub fn verify_completed_sig( Ok(()) } +/// Adds signatures +pub fn add_signatures( + secp: &Secp256k1, + part_sigs: Vec<&Signature>, + nonce_sum: &PublicKey, +) -> Result { + // Add public nonces kR*G + kS*G + let sig = aggsig::add_signatures_single(&secp, part_sigs, &nonce_sum)?; + Ok(sig) +} + +/// Just a simple sig, creates its own nonce, etc +pub fn sign_single( + secp: &Secp256k1, + msg: &Message, + skey: &SecretKey, + pubkey_sum: Option<&PublicKey>, +) -> Result { + let sig = aggsig::sign_single(secp, &msg, skey, None, None, None, pubkey_sum, None)?; + Ok(sig) +} + /// Verifies an aggsig signature pub fn verify_single( secp: &Secp256k1, @@ -435,17 +457,6 @@ pub fn verify_single( ) } -/// Adds signatures -pub fn add_signatures( - secp: &Secp256k1, - part_sigs: Vec<&Signature>, - nonce_sum: &PublicKey, -) -> Result { - // Add public nonces kR*G + kS*G - let sig = aggsig::add_signatures_single(&secp, part_sigs, &nonce_sum)?; - Ok(sig) -} - /// Just a simple sig, creates its own nonce, etc pub fn sign_with_blinding( secp: &Secp256k1, diff --git a/wallet/src/libtx/slate.rs b/wallet/src/libtx/slate.rs index 6dc5c4095..d5c6f147a 100644 --- a/wallet/src/libtx/slate.rs +++ b/wallet/src/libtx/slate.rs @@ -32,6 +32,8 @@ use util::secp::key::{PublicKey, SecretKey}; use util::secp::Signature; use util::RwLock; +use blake2::blake2b::blake2b; + /// Public data for each participant in the slate #[derive(Serialize, Deserialize, Debug, Clone)] @@ -44,6 +46,10 @@ pub struct ParticipantData { pub public_nonce: PublicKey, /// Public partial signature pub part_sig: Option, + /// A message for other participants + pub message: Option, + /// Signature, created with private key corresponding to 'public_blind_excess' + pub message_sig: Option, } impl ParticipantData { @@ -133,6 +139,7 @@ impl Slate { sec_key: &mut SecretKey, sec_nonce: &SecretKey, participant_id: usize, + message: Option, ) -> Result<(), Error> where K: Keychain, @@ -141,7 +148,14 @@ impl Slate { if self.tx.offset == BlindingFactor::zero() { self.generate_offset(keychain, sec_key)?; } - self.add_participant_info(keychain, &sec_key, &sec_nonce, participant_id, None)?; + self.add_participant_info( + keychain, + &sec_key, + &sec_nonce, + participant_id, + None, + message, + )?; Ok(()) } @@ -234,6 +248,7 @@ impl Slate { sec_nonce: &SecretKey, id: usize, part_sig: Option, + message: Option, ) -> Result<(), Error> where K: Keychain, @@ -241,11 +256,24 @@ impl Slate { // Add our public key and nonce to the slate let pub_key = PublicKey::from_secret_key(keychain.secp(), &sec_key)?; let pub_nonce = PublicKey::from_secret_key(keychain.secp(), &sec_nonce)?; + // Sign the provided message + let message_sig = { + if let Some(m) = message.clone() { + let hashed = blake2b(secp::constants::MESSAGE_SIZE, &[], &m.as_bytes()[..]); + let m = secp::Message::from_slice(&hashed.as_bytes())?; + let res = aggsig::sign_single(&keychain.secp(), &m, &sec_key, None)?; + Some(res) + } else { + None + } + }; self.participant_data.push(ParticipantData { id: id as u64, public_blind_excess: pub_key, public_nonce: pub_nonce, part_sig: part_sig, + message: message, + message_sig: message_sig, }); Ok(()) @@ -321,6 +349,30 @@ impl Slate { Ok(()) } + /// Verifies any messages in the slate's participant data match their signatures + pub fn verify_messages(&self, secp: &secp::Secp256k1) -> Result<(), Error> { + for p in self.participant_data.iter() { + if let Some(m) = p.message.clone() { + let hashed = blake2b(secp::constants::MESSAGE_SIZE, &[], &m.as_bytes()[..]); + let m = secp::Message::from_slice(&hashed.as_bytes())?; + if !aggsig::verify_single( + secp, + &p.message_sig.as_ref().unwrap(), + &m, + None, + &p.public_blind_excess, + None, + false, + ) { + return Err(ErrorKind::Signature( + "Optional participant messages do not match signatures".to_owned(), + ))?; + } + } + } + Ok(()) + } + /// This should be callable by either the sender or receiver /// once phase 3 is done /// diff --git a/wallet/src/libwallet/api.rs b/wallet/src/libwallet/api.rs index 8e839157f..94775b059 100644 --- a/wallet/src/libwallet/api.rs +++ b/wallet/src/libwallet/api.rs @@ -462,6 +462,7 @@ where max_outputs: usize, num_change_outputs: usize, selection_strategy_is_use_all: bool, + message: Option, ) -> Result<(Slate, impl FnOnce(&mut W, &str) -> Result<(), Error>), Error> { let mut w = self.wallet.lock(); w.open_with_credentials()?; @@ -485,6 +486,7 @@ where selection_strategy_is_use_all, &parent_key_id, false, + message, )?; // Save the aggsig context in our DB for when we @@ -577,6 +579,13 @@ where } } + /// Verifies all messages in the slate match their public keys + pub fn verify_slate_messages(&mut self, slate: &Slate) -> Result<(), Error> { + let mut w = self.wallet.lock(); + slate.verify_messages(w.keychain().secp())?; + Ok(()) + } + /// Attempt to restore contents of wallet pub fn restore(&mut self) -> Result<(), Error> { let mut w = self.wallet.lock(); @@ -660,6 +669,7 @@ where &mut self, slate: &mut Slate, dest_acct_name: Option<&str>, + message: Option, ) -> Result<(), Error> { let mut w = self.wallet.lock(); w.open_with_credentials()?; @@ -673,7 +683,7 @@ where } None => w.parent_key_id(), }; - let res = tx::receive_tx(&mut *w, slate, &parent_key_id, false); + let res = tx::receive_tx(&mut *w, slate, &parent_key_id, false, message); w.close()?; if let Err(e) = res { diff --git a/wallet/src/libwallet/internal/tx.rs b/wallet/src/libwallet/internal/tx.rs index deae53479..329ef0612 100644 --- a/wallet/src/libwallet/internal/tx.rs +++ b/wallet/src/libwallet/internal/tx.rs @@ -31,6 +31,7 @@ pub fn receive_tx( slate: &mut Slate, parent_key_id: &Identifier, is_self: bool, + message: Option, ) -> Result<(), Error> where T: WalletBackend, @@ -51,6 +52,7 @@ where &mut context.sec_key, &context.sec_nonce, 1, + message, )?; // perform partial sig @@ -73,6 +75,7 @@ pub fn create_send_tx( selection_strategy_is_use_all: bool, parent_key_id: &Identifier, is_self: bool, + message: Option, ) -> Result< ( Slate, @@ -122,6 +125,7 @@ where &mut context.sec_key, &context.sec_nonce, 0, + message, )?; Ok((slate, context, sender_lock_fn)) diff --git a/wallet/src/libwallet/types.rs b/wallet/src/libwallet/types.rs index aa050a34e..972865120 100644 --- a/wallet/src/libwallet/types.rs +++ b/wallet/src/libwallet/types.rs @@ -700,4 +700,6 @@ pub struct SendTXArgs { pub num_change_outputs: usize, /// whether to use all outputs (combine) pub selection_strategy_is_use_all: bool, + /// Optional message, that will be signed + pub message: Option, } diff --git a/wallet/tests/accounts.rs b/wallet/tests/accounts.rs index 40c6fce9c..a13a0a64a 100644 --- a/wallet/tests/accounts.rs +++ b/wallet/tests/accounts.rs @@ -190,6 +190,7 @@ fn accounts_test_impl(test_dir: &str) -> Result<(), libwallet::Error> { 500, // max outputs 1, // num change outputs true, // select all outputs + None, )?; slate = client1.send_tx_slate_direct("wallet2", &slate)?; api.tx_lock_outputs(&slate, lock_fn)?; diff --git a/wallet/tests/common/testclient.rs b/wallet/tests/common/testclient.rs index 9a309144d..a47d5aea1 100644 --- a/wallet/tests/common/testclient.rs +++ b/wallet/tests/common/testclient.rs @@ -211,7 +211,7 @@ where let w = dest_wallet.unwrap().1.clone(); let mut slate = serde_json::from_str(&m.body).unwrap(); controller::foreign_single_use(w.clone(), |listener_api| { - listener_api.receive_tx(&mut slate, None)?; + listener_api.receive_tx(&mut slate, None, None)?; Ok(()) })?; Ok(WalletProxyMessage { diff --git a/wallet/tests/file.rs b/wallet/tests/file.rs index 03c0d6eac..d07f3da29 100644 --- a/wallet/tests/file.rs +++ b/wallet/tests/file.rs @@ -97,6 +97,9 @@ fn file_exchange_test_impl(test_dir: &str) -> Result<(), libwallet::Error> { let send_file = format!("{}/part_tx_1.tx", test_dir); let receive_file = format!("{}/part_tx_2.tx", test_dir); + // test optional message + let message = "sender test message, sender test message"; + // Should have 5 in account1 (5 spendable), 5 in account (2 spendable) wallet::controller::owner_single_use(wallet1.clone(), |api| { let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(true, 1)?; @@ -106,13 +109,12 @@ fn file_exchange_test_impl(test_dir: &str) -> Result<(), libwallet::Error> { // send to send let (mut slate, lock_fn) = api.initiate_tx( Some("mining"), - reward * 2, // amount - 2, // minimum confirmations - 500, // max outputs - 1, // num change outputs - true, // select all outputs - //"mining", - //"listener", + reward * 2, // amount + 2, // minimum confirmations + 500, // max outputs + 1, // num change outputs + true, // select all outputs + Some(message.to_owned()), // optional message )?; // output tx file let file_adapter = FileWalletCommAdapter::new(); @@ -127,11 +129,23 @@ fn file_exchange_test_impl(test_dir: &str) -> Result<(), libwallet::Error> { w.set_parent_key_id_by_name("account1")?; } + let adapter = FileWalletCommAdapter::new(); + let mut slate = adapter.receive_tx_async(&send_file)?; + let mut naughty_slate = slate.clone(); + naughty_slate.participant_data[0].message = Some("I changed the message".to_owned()); + + // verify messages on slate match + wallet::controller::owner_single_use(wallet1.clone(), |api| { + api.verify_slate_messages(&slate)?; + assert!(api.verify_slate_messages(&naughty_slate).is_err()); + Ok(()) + })?; + + let sender2_message = "And this is sender 2's message".to_owned(); + // wallet 2 receives file, completes, sends file back wallet::controller::foreign_single_use(wallet2.clone(), |api| { - let adapter = FileWalletCommAdapter::new(); - let mut slate = adapter.receive_tx_async(&send_file)?; - api.receive_tx(&mut slate, None)?; + api.receive_tx(&mut slate, None, Some(sender2_message))?; adapter.send_tx_async(&receive_file, &mut slate)?; Ok(()) })?; @@ -140,6 +154,7 @@ fn file_exchange_test_impl(test_dir: &str) -> Result<(), libwallet::Error> { wallet::controller::owner_single_use(wallet1.clone(), |api| { let adapter = FileWalletCommAdapter::new(); let mut slate = adapter.receive_tx_async(&receive_file)?; + api.verify_slate_messages(&slate)?; api.finalize_tx(&mut slate)?; api.post_tx(&slate.tx, false)?; bh += 1; diff --git a/wallet/tests/repost.rs b/wallet/tests/repost.rs index 0647ca0d5..152ea003f 100644 --- a/wallet/tests/repost.rs +++ b/wallet/tests/repost.rs @@ -112,6 +112,7 @@ fn file_repost_test_impl(test_dir: &str) -> Result<(), libwallet::Error> { 500, // max outputs 1, // num change outputs true, // select all outputs + None, )?; // output tx file let file_adapter = FileWalletCommAdapter::new(); @@ -132,7 +133,7 @@ fn file_repost_test_impl(test_dir: &str) -> Result<(), libwallet::Error> { wallet::controller::foreign_single_use(wallet1.clone(), |api| { let adapter = FileWalletCommAdapter::new(); let mut slate = adapter.receive_tx_async(&send_file)?; - api.receive_tx(&mut slate, None)?; + api.receive_tx(&mut slate, None, None)?; adapter.send_tx_async(&receive_file, &mut slate)?; Ok(()) })?; @@ -207,6 +208,7 @@ fn file_repost_test_impl(test_dir: &str) -> Result<(), libwallet::Error> { 500, // max outputs 1, // num change outputs true, // select all outputs + None, )?; slate = client1.send_tx_slate_direct("wallet2", &slate_i)?; sender_api.tx_lock_outputs(&slate, lock_fn)?; diff --git a/wallet/tests/restore.rs b/wallet/tests/restore.rs index ea782445e..ac94a31b5 100644 --- a/wallet/tests/restore.rs +++ b/wallet/tests/restore.rs @@ -236,6 +236,7 @@ fn setup_restore(test_dir: &str) -> Result<(), libwallet::Error> { 500, // max outputs 1, // num change outputs true, // select all outputs + None, )?; slate = client1.send_tx_slate_direct("wallet2", &slate_i)?; sender_api.tx_lock_outputs(&slate, lock_fn)?; @@ -257,6 +258,7 @@ fn setup_restore(test_dir: &str) -> Result<(), libwallet::Error> { 500, // max outputs 1, // num change outputs true, // select all outputs + None, )?; slate = client1.send_tx_slate_direct("wallet3", &slate_i)?; sender_api.tx_lock_outputs(&slate, lock_fn)?; @@ -278,6 +280,7 @@ fn setup_restore(test_dir: &str) -> Result<(), libwallet::Error> { 500, // max outputs 1, // num change outputs true, // select all outputs + None, )?; slate = client3.send_tx_slate_direct("wallet2", &slate_i)?; sender_api.tx_lock_outputs(&slate, lock_fn)?; @@ -305,6 +308,7 @@ fn setup_restore(test_dir: &str) -> Result<(), libwallet::Error> { 500, // max outputs 1, // num change outputs true, // select all outputs + None, )?; slate = client3.send_tx_slate_direct("wallet2", &slate_i)?; sender_api.tx_lock_outputs(&slate, lock_fn)?; diff --git a/wallet/tests/self_send.rs b/wallet/tests/self_send.rs index 5194fad34..1d9aec3b3 100644 --- a/wallet/tests/self_send.rs +++ b/wallet/tests/self_send.rs @@ -99,12 +99,11 @@ fn self_send_test_impl(test_dir: &str) -> Result<(), libwallet::Error> { 500, // max outputs 1, // num change outputs true, // select all outputs - //"mining", - //"listener", + None, )?; // Send directly to self wallet::controller::foreign_single_use(wallet1.clone(), |api| { - api.receive_tx(&mut slate, Some("listener"))?; + api.receive_tx(&mut slate, Some("listener"), None)?; Ok(()) })?; api.finalize_tx(&mut slate)?; diff --git a/wallet/tests/transaction.rs b/wallet/tests/transaction.rs index 30cc39f92..22ad40aaa 100644 --- a/wallet/tests/transaction.rs +++ b/wallet/tests/transaction.rs @@ -110,6 +110,7 @@ fn basic_transaction_api(test_dir: &str) -> Result<(), libwallet::Error> { 500, // max outputs 1, // num change outputs true, // select all outputs + None, )?; slate = client1.send_tx_slate_direct("wallet2", &slate_i)?; sender_api.tx_lock_outputs(&slate, lock_fn)?; @@ -244,6 +245,7 @@ fn basic_transaction_api(test_dir: &str) -> Result<(), libwallet::Error> { 500, // max outputs 1, // num change outputs true, // select all outputs + None, )?; slate = client1.send_tx_slate_direct("wallet2", &slate_i)?; sender_api.finalize_tx(&mut slate)?; @@ -337,6 +339,7 @@ fn tx_rollback(test_dir: &str) -> Result<(), libwallet::Error> { 500, // max outputs 1, // num change outputs true, // select all outputs + None, )?; slate = client1.send_tx_slate_direct("wallet2", &slate_i)?; sender_api.finalize_tx(&mut slate)?;