Simple contracts restructured v3 (#675)

* Add prototype contract implementation

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

* Separate contract utilities

* Simplify the main setup/sign code flow

* Remove commented initial_sec_key assignment

* Simplify sign::compute function

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

* Further simplify a bit sign and setup

* No need for mutable context when adding outputs

* Refactor the commented code (lol)

* Refactor a bit

* tmp

---------

Co-authored-by: oryhp <gtrphyro@gmail.com>
This commit is contained in:
Yeastplume 2023-03-20 10:09:17 +00:00 committed by GitHub
parent 477d903df4
commit f0cf8b0a7d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 4910 additions and 9 deletions

View file

@ -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<Slate, Error> {
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<Slate, Error> {
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)]

View file

@ -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<Slate, Error> {
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<Slate, Error> {
// 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<Slate, Error> {
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<Option<Slate>, 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

View file

@ -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<VersionedSlate, Error>;
fn contract_new(&self, token: Token, args: ContractNewArgsAPI)
-> Result<VersionedSlate, Error>;
// fn contract_setup(
// &self,
// token: Token,
// slate: VersionedSlate,
// args: ContractSetupArgsAPI,
// ) -> Result<VersionedSlate, Error>;
fn contract_sign(
&self,
token: Token,
slate: VersionedSlate,
args: ContractSetupArgsAPI,
) -> Result<VersionedSlate, Error>;
fn contract_revoke(
&self,
token: Token,
args: ContractRevokeArgsAPI,
) -> Result<Option<VersionedSlate>, 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<VersionedSlate, Error> {
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<VersionedSlate, Error> {
// 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<VersionedSlate, Error> {
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<Option<VersionedSlate>, 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,

View file

@ -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<L, C, K>(
api: &mut Owner<L, C, K>,
keychain_mask: Option<&SecretKey>,
slate: &Slate,
counterparty_addr: &str,
out_file: Option<String>,
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<L, C, K>(
owner_api: &mut Owner<L, C, K>,
keychain_mask: Option<&SecretKey>,
slate: &Slate,
dest: &str,
out_file_override: Option<String>,
) -> Result<SlatepackOut, libwallet::Error>
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<L, C, K>(
owner_api: &mut Owner<L, C, K>,
@ -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<u64>,
/// Send amount
pub send: Option<u64>,
/// 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<String>,
/// 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<String>,
/// How to separate outputs (command separated amounts)
pub make_outputs: Option<String>,
// Future features
/// Custom fee contribution
pub fee_rate: Option<u32>,
/// Save slatepack to a specific filename
pub outfile: Option<String>,
/// Select outputs early
pub add_outputs: bool,
}
impl ContractNewArgs {
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<L, C, K>(
owner_api: &mut Owner<L, C, K>,
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<String>,
/// Receive amount
pub receive: Option<u64>,
/// Send amount
pub send: Option<u64>,
/// Show the resulting slatepack as JSON
pub as_json: bool,
/// Use the specified inputs (comma separated input commitments)
pub use_inputs: Option<String>,
/// How to separate outputs (command separated amounts)
pub make_outputs: Option<String>,
// Future features
/// Whether we should automatically sign a receive of any value
// pub auto_receive: Option<bool>,
/// Custom fee contribution
pub fee_rate: Option<u32>,
/// Save slatepack to a specific filename
pub outfile: Option<String>,
/// Add outputs
pub add_outputs: bool, // lock outputs early
}
impl ContractSetupArgs {
fn get_net_change(&self) -> Option<i64> {
let mut net_change: Option<i64> = 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<L, C, K>(
// owner_api: &mut Owner<L, C, K>,
// 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<L, C, K>(
owner_api: &mut Owner<L, C, K>,
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<L, C, K>(
owner_api: &mut Owner<L, C, K>,
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(())
}

View file

@ -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<Vec<(&'static str, u64)>>, // 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<SecretKey>,
)>, // wallets
Arc<Chain>, // chain
Arc<AtomicBool>, // 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))
}

View file

@ -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<SecretKey>,
)>,
Arc<Chain>, // chain
Arc<AtomicBool>, // 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<u64>,
// 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<SecretKey>,
// )> {
// // 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<Vec<()>, 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(())
// }

View file

@ -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(())
}

View file

@ -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(())
}

View file

@ -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(())
}

View file

@ -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(())
}

View file

@ -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(())
}

View file

@ -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::<Vec<&str>>();
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(())
}

View file

@ -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(())
}

18
default.nix Normal file
View file

@ -0,0 +1,18 @@
{ pkgs ? import <nixpkgs> {} }:
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";
'';
}

View file

@ -318,9 +318,41 @@ where
Box::new(iter)
}
fn get_tx_log_entry(&self, u: &Uuid) -> Result<Option<TxLogEntry>, 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<Option<TxLogEntry>, 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<Option<TxLogEntry>, 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<dyn Iterator>

View file

@ -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<Slate, Error>
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<Slate, Error>
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)
}

View file

@ -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<Slate, Error>
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<Slate, Error>
// 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<Slate, Error>
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<Option<Slate>, Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
contract::revoke(&mut *w, keychain_mask, &args)
}

View file

@ -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'

View file

@ -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;

View file

@ -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<Slate, Error>
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)
}

View file

@ -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<Option<Slate>, 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::<Vec<OutputData>>();
// 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))
}

View file

@ -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<Slate, Error>
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))
}

View file

@ -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<Slate, Error>
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))
}

View file

@ -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<ContractView, Error>
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<i64> = 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)
}

View file

@ -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<Context, Error>
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<Context, Error>
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<OutputData>) -> 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<u64>,
) -> 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(())
}

View file

@ -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;

View file

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

View file

@ -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<K>(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)
}

View file

@ -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<String>,
/// 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<String>,
}
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<Vec<&str>> {
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<u64> {
if self.make_outputs.is_some() {
let output_amounts: Vec<u64> = self.make_outputs.as_ref().unwrap()[..]
.split(",")
// TODO: move consensus code outside of here. Consider turning make_outputs to Vec<u64>
.map(|amt| (amt.parse::<f64>().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<String>,
/// 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<i64>,
/// 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<u16>,
/// 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<u16>,
/// 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<i64>,
/// 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<i64>,
/// 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,
}

View file

@ -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<TxLogEntry, Error> {
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<i64>,
) -> Result<i64, Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
let mut expected_net_change: Option<i64> = 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<OutputData> = vec![];
// Create an OutputData entry for every created output
for (key_id, _, amount) in context.get_outputs() {
let commit = w.calc_commit_for_cache(keychain_mask, amount, &key_id)?;
let output_data = OutputData {
root_key_id: parent_key_id.clone(),
key_id: key_id.clone(),
mmr_index: None,
n_child: key_id.to_path().last_path_index(),
commit: commit,
value: amount,
status: OutputStatus::Unconfirmed,
height: current_height,
lock_height: 0,
is_coinbase: false,
tx_log_entry: None,
};
output_data_xs.push(output_data);
}
output_data_xs
};
// Phase 2 - atomically update Context, OutputData and TxLogEntry
let mut batch = w.batch(keychain_mask)?;
// Update TxLogEntry
if !context.log_id.is_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<FeeFields, Error> {
// 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
}

View file

@ -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 {

View file

@ -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<K>(
&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)
}

View file

@ -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<u64>) -> Result<OutputData, Error>;
/// Get an (Optional) tx log entry by uuid
fn get_tx_log_entry(&self, uuid: &Uuid) -> Result<Option<TxLogEntry>, Error>;
// TODO: I think this can be deleted
// fn get_tx_log_entry(&self, uuid: &Uuid) -> Result<Option<TxLogEntry>, Error>;
/// Get an (Optional) tx log entry by uuid
fn get_tx_log_entry(
&self,
parent_id: Identifier,
log_id: u32,
) -> Result<Option<TxLogEntry>, 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<pedersen::Commitment>,
/// Arguments that define which outputs to pick for a contract
pub setup_args: Option<ContractSetupArgsAPI>,
/// 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<u32>,
}
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 {

View file

@ -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

View file

@ -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<command::ProofVerify
})
}
// TODO: parse args
pub fn parse_contract_new_args(
args: &ArgMatches,
account: &String,
) -> Result<command::ContractNewArgs, ParseError> {
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::<f64>().unwrap() * consensus::GRIN_BASE as f64) as u64),
None => None,
};
let send = match args.value_of("send") {
Some(g) => Some((g.parse::<f64>().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::<u8>()
.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<command::ContractSetupArgs, ParseError> {
// 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::<f64>().unwrap() * consensus::GRIN_BASE as f64) as u64),
None => None,
};
let send = match args.value_of("send") {
Some(g) => Some((g.parse::<f64>().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<command::ContractRevokeArgs, ParseError> {
let tx_id = args.value_of("tx-id").unwrap().parse::<u32>().unwrap();
Ok(command::ContractRevokeArgs { tx_id: tx_id })
}
pub fn wallet_command<C, F>(
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));