build: update dependencies, remove unused wallet files and dependencies

This commit is contained in:
ardocrat 2023-08-15 21:23:53 +03:00
parent f54993483f
commit 4ea93407a7
9 changed files with 349 additions and 2467 deletions

550
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -16,22 +16,21 @@ log = "0.4"
## node
openssl-sys = { version = "0.9.82", features = ["vendored"] }
grin_api = { git = "https://github.com/mimblewimble/grin", branch = "master" }
grin_chain = { git = "https://github.com/mimblewimble/grin", branch = "master" }
grin_config = { git = "https://github.com/mimblewimble/grin", branch = "master" }
grin_core = { git = "https://github.com/mimblewimble/grin", branch = "master" }
grin_p2p = { git = "https://github.com/mimblewimble/grin", branch = "master" }
grin_servers = { git = "https://github.com/mimblewimble/grin", branch = "master" }
grin_keychain = { git = "https://github.com/mimblewimble/grin", branch = "master" }
grin_util = { git = "https://github.com/mimblewimble/grin", branch = "master" }
grin_api = { git = "https://github.com/mimblewimble/grin", tag = "v5.2.0-beta.3" }
grin_chain = { git = "https://github.com/mimblewimble/grin", tag = "v5.2.0-beta.3" }
grin_config = { git = "https://github.com/mimblewimble/grin", tag = "v5.2.0-beta.3" }
grin_core = { git = "https://github.com/mimblewimble/grin", tag = "v5.2.0-beta.3" }
grin_p2p = { git = "https://github.com/mimblewimble/grin", tag = "v5.2.0-beta.3" }
grin_servers = { git = "https://github.com/mimblewimble/grin", tag = "v5.2.0-beta.3" }
grin_keychain = { git = "https://github.com/mimblewimble/grin", tag = "v5.2.0-beta.3" }
grin_util = { git = "https://github.com/mimblewimble/grin", tag = "v5.2.0-beta.3" }
## wallet
grin_wallet_impls = { git = "https://github.com/mimblewimble/grin-wallet", branch = "master" }
grin_wallet_api = { git = "https://github.com/mimblewimble/grin-wallet", branch = "master" }
grin_wallet_libwallet = { git = "https://github.com/mimblewimble/grin-wallet", branch = "master" }
grin_wallet_util = { git = "https://github.com/mimblewimble/grin-wallet", branch = "master" }
#grin_wallet_controller = "5.1.0"
#grin_wallet_config = "5.1.0"
grin_wallet_impls = { git = "https://github.com/mimblewimble/grin-wallet", tag = "v5.2.0-beta.1" }
grin_wallet_api = { git = "https://github.com/mimblewimble/grin-wallet", tag = "v5.2.0-beta.1" }
grin_wallet_libwallet = { git = "https://github.com/mimblewimble/grin-wallet", tag = "v5.2.0-beta.1" }
grin_wallet_util = { git = "https://github.com/mimblewimble/grin-wallet", tag = "v5.2.0-beta.1" }
grin_wallet_controller = { git = "https://github.com/mimblewimble/grin-wallet", tag = "v5.2.0-beta.1" }
## ui
pollster = "0.3.0"
@ -51,11 +50,6 @@ toml = "0.7.4"
serde = "1"
pnet = "0.34.0"
url = "2.4.0"
parking_lot = "0.10.2"
uuid = { version = "0.8.2", features = ["serde", "v4"] }
num-bigint = "0.4.3"
byteorder = "1.3"
ed25519-dalek = "1.0.0-pre.4"
# stratum server
serde_derive = "1"

View file

@ -1,128 +0,0 @@
// Copyright 2023 The Grim 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.
use grin_keychain::{ChildNumber, ExtKeychain, Identifier, Keychain};
use grin_util::secp::key::SecretKey;
use grin_wallet_libwallet::{AcctPathMapping, NodeClient, WalletBackend};
use grin_wallet_libwallet::Error;
/// Get next available key in the wallet for a given parent
pub fn next_available_key<'a, T: ?Sized, C, K>(
wallet: &mut T,
keychain_mask: Option<&SecretKey>,
) -> Result<Identifier, Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
let child = wallet.next_child(keychain_mask)?;
Ok(child)
}
/// Retrieve an existing key from a wallet
pub fn retrieve_existing_key<'a, T: ?Sized, C, K>(
wallet: &T,
key_id: Identifier,
mmr_index: Option<u64>,
) -> Result<(Identifier, u32), Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
let existing = wallet.get(&key_id, &mmr_index)?;
let key_id = existing.key_id.clone();
let derivation = existing.n_child;
Ok((key_id, derivation))
}
/// Returns a list of account to BIP32 path mappings
pub fn accounts<'a, T: ?Sized, C, K>(wallet: &mut T) -> Result<Vec<AcctPathMapping>, Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
Ok(wallet.acct_path_iter().collect())
}
/// Adds an new parent account path with a given label
pub fn new_acct_path<'a, T: ?Sized, C, K>(
wallet: &mut T,
keychain_mask: Option<&SecretKey>,
label: &str,
) -> Result<Identifier, Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
let label = label.to_owned();
if wallet.acct_path_iter().any(|l| l.label == label) {
return Err(Error::AccountLabelAlreadyExists(label));
}
// We're always using paths at m/k/0 for parent keys for output derivations
// so find the highest of those, then increment (to conform with external/internal
// derivation chains in BIP32 spec)
let highest_entry = wallet.acct_path_iter().max_by(|a, b| {
<u32>::from(a.path.to_path().path[0]).cmp(&<u32>::from(b.path.to_path().path[0]))
});
let return_id = {
if let Some(e) = highest_entry {
let mut p = e.path.to_path();
p.path[0] = ChildNumber::from(<u32>::from(p.path[0]) + 1);
p.to_identifier()
} else {
ExtKeychain::derive_key_id(2, 0, 0, 0, 0)
}
};
let save_path = AcctPathMapping {
label,
path: return_id.clone(),
};
let mut batch = wallet.batch(keychain_mask)?;
batch.save_acct_path(save_path)?;
batch.commit()?;
Ok(return_id)
}
/// Adds/sets a particular account path with a given label
pub fn set_acct_path<'a, T: ?Sized, C, K>(
wallet: &mut T,
keychain_mask: Option<&SecretKey>,
label: &str,
path: &Identifier,
) -> Result<(), Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
let label = label.to_owned();
let save_path = AcctPathMapping {
label,
path: path.clone(),
};
let mut batch = wallet.batch(keychain_mask)?;
batch.save_acct_path(save_path)?;
batch.commit()?;
Ok(())
}

View file

@ -13,10 +13,6 @@
// limitations under the License.
pub mod types;
pub mod updater;
pub mod selection;
pub mod tx;
pub mod keys;
mod mnemonic;
pub use mnemonic::Mnemonic;

View file

@ -1,714 +0,0 @@
// Copyright 2023 The Grim 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.
use std::collections::HashMap;
use std::convert::TryInto;
use grin_core::core::amount_to_hr_string;
use grin_core::libtx::{
build,
proof::{ProofBuild, ProofBuilder},
tx_fee,
};
use grin_keychain::{Identifier, Keychain};
use grin_util::secp::key::SecretKey;
use grin_util::secp::pedersen;
use grin_wallet_libwallet::{address, Context, NodeClient, OutputData, OutputStatus, StoredProofInfo, TxLogEntry, TxLogEntryType, WalletBackend};
use grin_wallet_libwallet::Error;
use grin_wallet_libwallet::Slate;
use grin_wallet_util::OnionV3Address;
use log::debug;
use crate::wallet::keys::next_available_key;
/// Initialize a transaction on the sender side, returns a corresponding
/// libwallet transaction slate with the appropriate inputs selected,
/// and saves the private wallet identifiers of our selected outputs
/// into our transaction context
pub fn build_send_tx<'a, T: ?Sized, C, K>(
wallet: &mut T,
keychain: &K,
keychain_mask: Option<&SecretKey>,
slate: &mut Slate,
current_height: u64,
minimum_confirmations: u64,
max_outputs: usize,
change_outputs: usize,
selection_strategy_is_use_all: bool,
fixed_fee: Option<u64>,
parent_key_id: Identifier,
use_test_nonce: bool,
is_initiator: bool,
amount_includes_fee: bool,
) -> Result<Context, Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
let (elems, inputs, change_amounts_derivations, fee) = select_send_tx(
wallet,
keychain_mask,
slate.amount,
amount_includes_fee,
current_height,
minimum_confirmations,
max_outputs,
change_outputs,
selection_strategy_is_use_all,
&parent_key_id,
false,
)?;
if amount_includes_fee {
slate.amount = slate.amount.checked_sub(fee).ok_or(Error::GenericError(
format!("Transaction amount is too small to include fee").into(),
))?;
};
if fixed_fee.map(|f| fee != f).unwrap_or(false) {
return Err(Error::Fee(
"The initially selected fee is not sufficient".into(),
));
}
// Update the fee on the slate so we account for this when building the tx.
slate.fee_fields = fee.try_into().unwrap();
slate.add_transaction_elements(keychain, &ProofBuilder::new(keychain), elems)?;
// Create our own private context
let mut context = Context::new(
keychain.secp(),
&parent_key_id,
use_test_nonce,
is_initiator,
);
context.fee = Some(slate.fee_fields);
context.amount = slate.amount;
// Store our private identifiers for each input
for input in inputs {
context.add_input(&input.key_id, &input.mmr_index, input.value);
}
let mut commits: HashMap<Identifier, Option<String>> = HashMap::new();
// Store change output(s) and cached commits
for (change_amount, id, mmr_index) in &change_amounts_derivations {
context.add_output(&id, &mmr_index, *change_amount);
commits.insert(
id.clone(),
wallet.calc_commit_for_cache(keychain_mask, *change_amount, &id)?,
);
}
Ok(context)
}
/// Locks all corresponding outputs in the context, creates
/// change outputs and tx log entry
pub fn lock_tx_context<'a, T: ?Sized, C, K>(
wallet: &mut T,
keychain_mask: Option<&SecretKey>,
slate: &Slate,
current_height: u64,
context: &Context,
excess_override: Option<pedersen::Commitment>,
) -> Result<(), Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
let mut output_commits: HashMap<Identifier, (Option<String>, u64)> = HashMap::new();
// Store cached commits before locking wallet
let mut total_change = 0;
for (id, _, change_amount) in &context.get_outputs() {
output_commits.insert(
id.clone(),
(
wallet.calc_commit_for_cache(keychain_mask, *change_amount, &id)?,
*change_amount,
),
);
total_change += change_amount;
}
debug!("Change amount is: {}", total_change);
let keychain = wallet.keychain(keychain_mask)?;
let tx_entry = {
let lock_inputs = context.get_inputs();
let slate_id = slate.id;
let height = current_height;
let parent_key_id = context.parent_key_id.clone();
let mut batch = wallet.batch(keychain_mask)?;
let log_id = batch.next_tx_log_id(&parent_key_id)?;
let mut t = TxLogEntry::new(parent_key_id.clone(), TxLogEntryType::TxSent, log_id);
t.tx_slate_id = Some(slate_id);
let filename = format!("{}.grintx", slate_id);
t.stored_tx = Some(filename);
t.fee = context.fee;
t.ttl_cutoff_height = match slate.ttl_cutoff_height {
0 => None,
n => Some(n),
};
if let Ok(e) = slate.calc_excess(keychain.secp()) {
t.kernel_excess = Some(e)
}
if let Some(e) = excess_override {
t.kernel_excess = Some(e)
}
t.kernel_lookup_min_height = Some(current_height);
let mut amount_debited = 0;
t.num_inputs = lock_inputs.len();
for id in lock_inputs {
let mut coin = batch.get(&id.0, &id.1).unwrap();
coin.tx_log_entry = Some(log_id);
amount_debited += coin.value;
batch.lock_output(&mut coin)?;
}
t.amount_debited = amount_debited;
// store extra payment proof info, if required
if let Some(ref p) = slate.payment_proof {
let sender_address_path = match context.payment_proof_derivation_index {
Some(p) => p,
None => {
return Err(Error::PaymentProof(
"Payment proof derivation index required".to_owned(),
)
.into());
}
};
let sender_key = address::address_from_derivation_path(
&keychain,
&parent_key_id,
sender_address_path,
)?;
let sender_address = OnionV3Address::from_private(&sender_key.0)?;
t.payment_proof = Some(StoredProofInfo {
receiver_address: p.receiver_address,
receiver_signature: p.receiver_signature,
sender_address: sender_address.to_ed25519()?,
sender_address_path,
sender_signature: None,
});
};
// write the output representing our change
for (id, _, _) in &context.get_outputs() {
t.num_outputs += 1;
let (commit, change_amount) = output_commits.get(&id).unwrap().clone();
t.amount_credited += change_amount;
batch.save(OutputData {
root_key_id: parent_key_id.clone(),
key_id: id.clone(),
n_child: id.to_path().last_path_index(),
commit,
mmr_index: None,
value: change_amount,
status: OutputStatus::Unconfirmed,
height,
lock_height: 0,
is_coinbase: false,
tx_log_entry: Some(log_id),
})?;
}
batch.save_tx_log_entry(t.clone(), &parent_key_id)?;
batch.commit()?;
t
};
wallet.store_tx(
&format!("{}", tx_entry.tx_slate_id.unwrap()),
slate.tx_or_err()?,
)?;
Ok(())
}
/// Creates a new output in the wallet for the recipient,
/// returning the key of the fresh output
/// Also creates a new transaction containing the output
pub fn build_recipient_output<'a, T: ?Sized, C, K>(
wallet: &mut T,
keychain_mask: Option<&SecretKey>,
slate: &mut Slate,
current_height: u64,
parent_key_id: Identifier,
use_test_rng: bool,
is_initiator: bool,
) -> Result<(Identifier, Context, TxLogEntry), Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
// Create a potential output for this transaction
let key_id = next_available_key(wallet, keychain_mask).unwrap();
let keychain = wallet.keychain(keychain_mask)?;
let key_id_inner = key_id.clone();
let amount = slate.amount;
let height = current_height;
let slate_id = slate.id;
slate.add_transaction_elements(
&keychain,
&ProofBuilder::new(&keychain),
vec![build::output(amount, key_id.clone())],
)?;
// Add blinding sum to our context
let mut context = Context::new(keychain.secp(), &parent_key_id, use_test_rng, is_initiator);
context.add_output(&key_id, &None, amount);
context.amount = amount;
context.fee = slate.fee_fields.as_opt();
let commit = wallet.calc_commit_for_cache(keychain_mask, amount, &key_id_inner)?;
let mut batch = wallet.batch(keychain_mask)?;
let log_id = batch.next_tx_log_id(&parent_key_id)?;
let mut t = TxLogEntry::new(parent_key_id.clone(), TxLogEntryType::TxReceived, log_id);
t.tx_slate_id = Some(slate_id);
t.amount_credited = amount;
t.num_outputs = 1;
t.ttl_cutoff_height = match slate.ttl_cutoff_height {
0 => None,
n => Some(n),
};
// when invoicing, this will be invalid
if let Ok(e) = slate.calc_excess(keychain.secp()) {
t.kernel_excess = Some(e)
}
t.kernel_lookup_min_height = Some(current_height);
batch.save(OutputData {
root_key_id: parent_key_id.clone(),
key_id: key_id_inner.clone(),
mmr_index: None,
n_child: key_id_inner.to_path().last_path_index(),
commit,
value: amount,
status: OutputStatus::Unconfirmed,
height,
lock_height: 0,
is_coinbase: false,
tx_log_entry: Some(log_id),
})?;
batch.save_tx_log_entry(t.clone(), &parent_key_id)?;
batch.commit()?;
Ok((key_id, context, t))
}
/// Builds a transaction to send to someone from the HD seed associated with the
/// wallet and the amount to send. Handles reading through the wallet data file,
/// selecting outputs to spend and building the change.
pub fn select_send_tx<'a, T: ?Sized, C, K, B>(
wallet: &mut T,
keychain_mask: Option<&SecretKey>,
amount: u64,
amount_includes_fee: bool,
current_height: u64,
minimum_confirmations: u64,
max_outputs: usize,
change_outputs: usize,
selection_strategy_is_use_all: bool,
parent_key_id: &Identifier,
include_inputs_in_sum: bool,
) -> Result<
(
Vec<Box<build::Append<K, B>>>,
Vec<OutputData>,
Vec<(u64, Identifier, Option<u64>)>, // change amounts and derivations
u64, // fee
),
Error,
>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
B: ProofBuild,
{
let (coins, _total, amount, fee) = select_coins_and_fee(
wallet,
amount,
amount_includes_fee,
current_height,
minimum_confirmations,
max_outputs,
change_outputs,
selection_strategy_is_use_all,
&parent_key_id,
)?;
// build transaction skeleton with inputs and change
let (parts, change_amounts_derivations) = inputs_and_change(
&coins,
wallet,
keychain_mask,
amount,
fee,
change_outputs,
include_inputs_in_sum,
)?;
Ok((parts, coins, change_amounts_derivations, fee))
}
/// Select outputs and calculating fee.
pub fn select_coins_and_fee<'a, T: ?Sized, C, K>(
wallet: &mut T,
amount: u64,
amount_includes_fee: bool,
current_height: u64,
minimum_confirmations: u64,
max_outputs: usize,
change_outputs: usize,
selection_strategy_is_use_all: bool,
parent_key_id: &Identifier,
) -> Result<
(
Vec<OutputData>,
u64, // total
u64, // amount
u64, // fee
),
Error,
>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
// select some spendable coins from the wallet
let (max_outputs, mut coins) = select_coins(
wallet,
amount,
current_height,
minimum_confirmations,
max_outputs,
selection_strategy_is_use_all,
parent_key_id,
);
// sender is responsible for setting the fee on the partial tx
// recipient should double check the fee calculation and not blindly trust the
// sender
// First attempt to spend without change
let mut fee = tx_fee(coins.len(), 1, 1);
let mut total: u64 = coins.iter().map(|c| c.value).sum();
let mut amount_with_fee = match amount_includes_fee {
true => amount,
false => amount + fee,
};
if total == 0 {
return Err(Error::NotEnoughFunds {
available: 0,
available_disp: amount_to_hr_string(0, false),
needed: amount_with_fee,
needed_disp: amount_to_hr_string(amount_with_fee, false),
});
}
// The amount with fee is more than the total values of our max outputs
if total < amount_with_fee && coins.len() == max_outputs {
return Err(Error::NotEnoughFunds {
available: total,
available_disp: amount_to_hr_string(total, false),
needed: amount_with_fee,
needed_disp: amount_to_hr_string(amount_with_fee, false),
});
}
let num_outputs = change_outputs + 1;
// We need to add a change address or amount with fee is more than total
if total != amount_with_fee {
fee = tx_fee(coins.len(), num_outputs, 1);
amount_with_fee = match amount_includes_fee {
true => amount,
false => amount + fee,
};
// Here check if we have enough outputs for the amount including fee otherwise
// look for other outputs and check again
while total < amount_with_fee {
// End the loop if we have selected all the outputs and still not enough funds
if coins.len() == max_outputs {
return Err(Error::NotEnoughFunds {
available: total,
available_disp: amount_to_hr_string(total, false),
needed: amount_with_fee,
needed_disp: amount_to_hr_string(amount_with_fee, false),
});
}
// select some spendable coins from the wallet
coins = select_coins(
wallet,
amount_with_fee,
current_height,
minimum_confirmations,
max_outputs,
selection_strategy_is_use_all,
parent_key_id,
)
.1;
fee = tx_fee(coins.len(), num_outputs, 1);
total = coins.iter().map(|c| c.value).sum();
amount_with_fee = match amount_includes_fee {
true => amount,
false => amount + fee,
};
}
}
// If original amount includes fee, the new amount should
// be reduced, to accommodate the fee.
let new_amount = match amount_includes_fee {
true => amount.checked_sub(fee).ok_or(Error::GenericError(
format!("Transaction amount is too small to include fee").into(),
))?,
false => amount,
};
Ok((coins, total, new_amount, fee))
}
/// Selects inputs and change for a transaction
pub fn inputs_and_change<'a, T: ?Sized, C, K, B>(
coins: &[OutputData],
wallet: &mut T,
keychain_mask: Option<&SecretKey>,
amount: u64,
fee: u64,
num_change_outputs: usize,
include_inputs_in_sum: bool,
) -> Result<
(
Vec<Box<build::Append<K, B>>>,
Vec<(u64, Identifier, Option<u64>)>,
),
Error,
>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
B: ProofBuild,
{
let mut parts = vec![];
// calculate the total across all inputs, and how much is left
let total: u64 = coins.iter().map(|c| c.value).sum();
// if we are spending 10,000 coins to send 1,000 then our change will be 9,000
// if the fee is 80 then the recipient will receive 1000 and our change will be
// 8,920
let change = total - amount - fee;
// build inputs using the appropriate derived key_ids
if include_inputs_in_sum {
for coin in coins {
if coin.is_coinbase {
parts.push(build::coinbase_input(coin.value, coin.key_id.clone()));
} else {
parts.push(build::input(coin.value, coin.key_id.clone()));
}
}
}
let mut change_amounts_derivations = vec![];
if change == 0 {
debug!("No change (sending exactly amount + fee), no change outputs to build");
} else {
debug!(
"Building change outputs: total change: {} ({} outputs)",
change, num_change_outputs
);
let part_change = change / num_change_outputs as u64;
let remainder_change = change % part_change;
for x in 0..num_change_outputs {
// n-1 equal change_outputs and a final one accounting for any remainder
let change_amount = if x == (num_change_outputs - 1) {
part_change + remainder_change
} else {
part_change
};
let change_key = wallet.next_child(keychain_mask).unwrap();
change_amounts_derivations.push((change_amount, change_key.clone(), None));
parts.push(build::output(change_amount, change_key));
}
}
Ok((parts, change_amounts_derivations))
}
/// Select spendable coins from a wallet.
/// Default strategy is to spend the maximum number of outputs (up to
/// max_outputs). Alternative strategy is to spend smallest outputs first
/// but only as many as necessary. When we introduce additional strategies
/// we should pass something other than a bool in.
pub fn select_coins<'a, T: ?Sized, C, K>(
wallet: &mut T,
amount: u64,
current_height: u64,
minimum_confirmations: u64,
max_outputs: usize,
select_all: bool,
parent_key_id: &Identifier,
) -> (usize, Vec<OutputData>)
// max_outputs_available, Outputs
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
// first find all eligible outputs based on number of confirmations
let mut eligible = wallet
.iter()
.filter(|out| {
out.root_key_id == *parent_key_id
&& out.eligible_to_spend(current_height, minimum_confirmations)
})
.collect::<Vec<OutputData>>();
let max_available = eligible.len();
// sort eligible outputs by increasing value
eligible.sort_by_key(|out| out.value);
// use a sliding window to identify potential sets of possible outputs to spend
// Case of amount > total amount of max_outputs(500):
// The limit exists because by default, we always select as many inputs as
// possible in a transaction, to reduce both the Output set and the fees.
// But that only makes sense up to a point, hence the limit to avoid being too
// greedy. But if max_outputs(500) is actually not enough to cover the whole
// amount, the wallet should allow going over it to satisfy what the user
// wants to send. So the wallet considers max_outputs more of a soft limit.
if eligible.len() > max_outputs {
for window in eligible.windows(max_outputs) {
let windowed_eligibles = window.to_vec();
if let Some(outputs) = select_from(amount, select_all, windowed_eligibles) {
return (max_available, outputs);
}
}
// Not exist in any window of which total amount >= amount.
// Then take coins from the smallest one up to the total amount of selected
// coins = the amount.
if let Some(outputs) = select_from(amount, false, eligible.clone()) {
debug!(
"Extending maximum number of outputs. {} outputs selected.",
outputs.len()
);
return (max_available, outputs);
}
} else if let Some(outputs) = select_from(amount, select_all, eligible.clone()) {
return (max_available, outputs);
}
// we failed to find a suitable set of outputs to spend,
// so return the largest amount we can so we can provide guidance on what is
// possible
eligible.reverse();
(
max_available,
eligible.iter().take(max_outputs).cloned().collect(),
)
}
fn select_from(amount: u64, select_all: bool, outputs: Vec<OutputData>) -> Option<Vec<OutputData>> {
let total = outputs.iter().fold(0, |acc, x| acc + x.value);
if total >= amount {
if select_all {
Some(outputs.to_vec())
} else {
let mut selected_amount = 0;
Some(
outputs
.iter()
.take_while(|out| {
let res = selected_amount < amount;
selected_amount += out.value;
res
})
.cloned()
.collect(),
)
}
} else {
None
}
}
/// Repopulates output in the slate's tranacstion
/// with outputs from the stored context
/// change outputs and tx log entry
/// Remove the explicitly stored excess
pub fn repopulate_tx<'a, T: ?Sized, C, K>(
wallet: &mut T,
keychain_mask: Option<&SecretKey>,
slate: &mut Slate,
context: &Context,
update_fee: bool,
) -> Result<(), Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
// restore the original amount, fee
slate.amount = context.amount;
if update_fee {
slate.fee_fields = context
.fee
.ok_or_else(|| Error::Fee("Missing fee fields".into()))?;
}
let keychain = wallet.keychain(keychain_mask)?;
// restore my signature data
slate.add_participant_info(&keychain, &context, None)?;
let mut parts = vec![];
for (id, _, value) in &context.get_inputs() {
let input = wallet.iter().find(|out| out.key_id == *id);
if let Some(i) = input {
if i.is_coinbase {
parts.push(build::coinbase_input(*value, i.key_id.clone()));
} else {
parts.push(build::input(*value, i.key_id.clone()));
}
}
}
for (id, _, value) in &context.get_outputs() {
let output = wallet.iter().find(|out| out.key_id == *id);
if let Some(i) = output {
parts.push(build::output(*value, i.key_id.clone()));
}
}
let _ = slate.add_transaction_elements(&keychain, &ProofBuilder::new(&keychain), parts)?;
// restore the original offset
slate.tx_or_err_mut()?.offset = slate.offset.clone();
Ok(())
}

View file

@ -1,547 +0,0 @@
// Copyright 2023 The Grim 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.
use std::io::Cursor;
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
use ed25519_dalek::{Signer, Verifier};
use ed25519_dalek::Keypair as DalekKeypair;
use ed25519_dalek::PublicKey as DalekPublicKey;
use ed25519_dalek::SecretKey as DalekSecretKey;
use ed25519_dalek::Signature as DalekSignature;
use grin_core::consensus::valid_header_version;
use grin_core::core::FeeFields;
use grin_core::core::HeaderVersion;
use grin_keychain::{Identifier, Keychain};
use grin_util::Mutex;
use grin_util::secp::key::SecretKey;
use grin_util::secp::pedersen;
use grin_wallet_libwallet::{Context, NodeClient, StoredProofInfo, TxLogEntryType, WalletBackend};
use grin_wallet_libwallet::{address, Error};
use grin_wallet_libwallet::InitTxArgs;
use grin_wallet_libwallet::Slate;
use grin_wallet_util::OnionV3Address;
use lazy_static::lazy_static;
use log::trace;
use uuid::Uuid;
use crate::wallet::selection::{build_recipient_output, build_send_tx, select_coins_and_fee};
use crate::wallet::updater::{cancel_tx_and_outputs, refresh_outputs, retrieve_outputs, retrieve_txs};
lazy_static! {
/// Static value to increment UUIDs of slates.
static ref SLATE_COUNTER: Mutex<u8> = Mutex::new(0);
}
/// Creates a new slate for a transaction, can be called by anyone involved in
/// the transaction (sender(s), receiver(s)).
pub fn new_tx_slate<'a, T: ?Sized, C, K>(
wallet: &mut T,
amount: u64,
is_invoice: bool,
num_participants: u8,
use_test_rng: bool,
ttl_blocks: Option<u64>,
) -> Result<Slate, Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
let current_height = wallet.w2n_client().get_chain_tip()?.0;
let mut slate = Slate::blank(num_participants, is_invoice);
if let Some(b) = ttl_blocks {
slate.ttl_cutoff_height = current_height + b;
}
if use_test_rng {
{
let sc = SLATE_COUNTER.lock();
let bytes = [4, 54, 67, 12, 43, 2, 98, 76, 32, 50, 87, 5, 1, 33, 43, *sc];
slate.id = Uuid::from_slice(&bytes).unwrap();
}
*SLATE_COUNTER.lock() += 1;
}
slate.amount = amount;
if valid_header_version(current_height, HeaderVersion(1)) {
slate.version_info.block_header_version = 1;
}
if valid_header_version(current_height, HeaderVersion(2)) {
slate.version_info.block_header_version = 2;
}
if valid_header_version(current_height, HeaderVersion(3)) {
slate.version_info.block_header_version = 3;
}
// Set the features explicitly to 0 here.
// This will generate a Plain kernel (rather than a HeightLocked kernel).
slate.kernel_features = 0;
Ok(slate)
}
/// Add inputs to the slate (effectively becoming the sender).
pub fn add_inputs_to_slate<'a, T: ?Sized, C, K>(
wallet: &mut T,
keychain_mask: Option<&SecretKey>,
slate: &mut Slate,
current_height: u64,
minimum_confirmations: u64,
max_outputs: usize,
num_change_outputs: usize,
selection_strategy_is_use_all: bool,
parent_key_id: &Identifier,
is_initiator: bool,
use_test_rng: bool,
amount_includes_fee: bool,
) -> Result<Context, Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
// sender should always refresh outputs
refresh_outputs(wallet, keychain_mask, parent_key_id, false)?;
// Sender selects outputs into a new slate and save our corresponding keys in
// a transaction context. The secret key in our transaction context will be
// randomly selected. This returns the public slate, and a closure that locks
// our inputs and outputs once we're convinced the transaction exchange went
// according to plan
// This function is just a big helper to do all of that, in theory
// this process can be split up in any way
let mut context = build_send_tx(
wallet,
&wallet.keychain(keychain_mask)?,
keychain_mask,
slate,
current_height,
minimum_confirmations,
max_outputs,
num_change_outputs,
selection_strategy_is_use_all,
None,
parent_key_id.clone(),
use_test_rng,
is_initiator,
amount_includes_fee,
)?;
// Generate a kernel offset and subtract from our context's secret key. Store
// the offset in the slate's transaction kernel, and adds our public key
// information to the slate
slate.fill_round_1(&wallet.keychain(keychain_mask)?, &mut context)?;
context.initial_sec_key = context.sec_key.clone();
if !is_initiator {
// perform partial sig
slate.fill_round_2(
&wallet.keychain(keychain_mask)?,
&context.sec_key,
&context.sec_nonce,
)?;
}
Ok(context)
}
/// Add receiver output to the slate.
pub fn add_output_to_slate<'a, T: ?Sized, C, K>(
wallet: &mut T,
keychain_mask: Option<&SecretKey>,
slate: &mut Slate,
current_height: u64,
parent_key_id: &Identifier,
is_initiator: bool,
use_test_rng: bool,
) -> Result<Context, Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
let keychain = wallet.keychain(keychain_mask)?;
// create an output using the amount in the slate
let (_, mut context, mut tx) = build_recipient_output(
wallet,
keychain_mask,
slate,
current_height,
parent_key_id.clone(),
use_test_rng,
is_initiator,
)?;
// fill public keys
slate.fill_round_1(&keychain, &mut context)?;
context.initial_sec_key = context.sec_key.clone();
if !is_initiator {
// perform partial sig
slate.fill_round_2(&keychain, &context.sec_key, &context.sec_nonce)?;
// update excess in stored transaction
let mut batch = wallet.batch(keychain_mask)?;
tx.kernel_excess = Some(slate.calc_excess(keychain.secp())?);
batch.save_tx_log_entry(tx.clone(), &parent_key_id)?;
batch.commit()?;
}
Ok(context)
}
/// Create context, without adding inputs to slate.
pub fn create_late_lock_context<'a, T: ?Sized, C, K>(
wallet: &mut T,
keychain_mask: Option<&SecretKey>,
slate: &mut Slate,
current_height: u64,
init_tx_args: &InitTxArgs,
parent_key_id: &Identifier,
use_test_rng: bool,
) -> Result<Context, Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
// sender should always refresh outputs
refresh_outputs(wallet, keychain_mask, parent_key_id, false)?;
// we're just going to run a selection to get the potential fee,
// but this won't be locked
let (_coins, _total, _amount, fee) = select_coins_and_fee(
wallet,
init_tx_args.amount,
init_tx_args.amount_includes_fee.unwrap_or(false),
current_height,
init_tx_args.minimum_confirmations,
init_tx_args.max_outputs as usize,
init_tx_args.num_change_outputs as usize,
init_tx_args.selection_strategy_is_use_all,
&parent_key_id,
)?;
slate.fee_fields = FeeFields::new(0, fee)?;
let keychain = wallet.keychain(keychain_mask)?;
// Create our own private context
let mut context = Context::new(keychain.secp(), &parent_key_id, use_test_rng, true);
context.fee = Some(slate.fee_fields);
context.amount = slate.amount;
context.late_lock_args = Some(init_tx_args.clone());
// Generate a blinding factor for the tx and add
// public key info to the slate
slate.fill_round_1(&wallet.keychain(keychain_mask)?, &mut context)?;
Ok(context)
}
/// Complete a transaction.
pub fn complete_tx<'a, T: ?Sized, C, K>(
wallet: &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,
{
// when self sending invoice tx, use initiator nonce to finalize
let (sec_key, sec_nonce) = {
if context.initial_sec_key != context.sec_key
&& context.initial_sec_nonce != context.sec_nonce
{
(
context.initial_sec_key.clone(),
context.initial_sec_nonce.clone(),
)
} else {
(context.sec_key.clone(), context.sec_nonce.clone())
}
};
slate.fill_round_2(&wallet.keychain(keychain_mask)?, &sec_key, &sec_nonce)?;
// Final transaction can be built by anyone at this stage
trace!("Slate to finalize is: {}", slate);
slate.finalize(&wallet.keychain(keychain_mask)?)?;
Ok(())
}
/// Rollback outputs associated with a transaction in the wallet.
pub fn cancel_tx<'a, T: ?Sized, C, K>(
wallet: &mut T,
keychain_mask: Option<&SecretKey>,
parent_key_id: &Identifier,
tx_id: Option<u32>,
tx_slate_id: Option<Uuid>,
) -> Result<(), Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
let mut tx_id_string = String::new();
if let Some(tx_id) = tx_id {
tx_id_string = tx_id.to_string();
} else if let Some(tx_slate_id) = tx_slate_id {
tx_id_string = tx_slate_id.to_string();
}
let tx_vec = retrieve_txs(
wallet,
tx_id,
tx_slate_id,
None,
Some(&parent_key_id),
false,
)?;
if tx_vec.len() != 1 {
return Err(Error::TransactionDoesntExist(tx_id_string));
}
let tx = tx_vec[0].clone();
match tx.tx_type {
TxLogEntryType::TxSent | TxLogEntryType::TxReceived | TxLogEntryType::TxReverted => {}
_ => return Err(Error::TransactionNotCancellable(tx_id_string)),
}
if tx.confirmed {
return Err(Error::TransactionNotCancellable(tx_id_string));
}
// get outputs associated with tx
let res = retrieve_outputs(
wallet,
keychain_mask,
false,
Some(tx.id),
Some(&parent_key_id),
)?;
let outputs = res.iter().map(|m| m.output.clone()).collect();
cancel_tx_and_outputs(wallet, keychain_mask, tx, outputs, parent_key_id)?;
Ok(())
}
/// Update the stored transaction (this update needs to happen when the TX is finalised).
pub fn update_stored_tx<'a, T: ?Sized, C, K>(
wallet: &mut T,
keychain_mask: Option<&SecretKey>,
context: &Context,
slate: &Slate,
is_invoiced: bool,
) -> Result<(), Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
// finalize command
let tx_vec = retrieve_txs(wallet, None, Some(slate.id), None, None, false)?;
let mut tx = None;
// don't want to assume this is the right tx, in case of self-sending
for t in tx_vec {
if t.tx_type == TxLogEntryType::TxSent && !is_invoiced {
tx = Some(t);
break;
}
if t.tx_type == TxLogEntryType::TxReceived && is_invoiced {
tx = Some(t);
break;
}
}
let mut tx = match tx {
Some(t) => t,
None => return Err(Error::TransactionDoesntExist(slate.id.to_string())),
};
let parent_key = tx.parent_key_id.clone();
{
let keychain = wallet.keychain(keychain_mask)?;
tx.kernel_excess = Some(slate.calc_excess(keychain.secp())?);
}
if let Some(ref p) = slate.clone().payment_proof {
let derivation_index = match context.payment_proof_derivation_index {
Some(i) => i,
None => 0,
};
let keychain = wallet.keychain(keychain_mask)?;
let parent_key_id = wallet.parent_key_id();
let excess = slate.calc_excess(keychain.secp())?;
let sender_key =
address::address_from_derivation_path(&keychain, &parent_key_id, derivation_index)?;
let sender_address = OnionV3Address::from_private(&sender_key.0)?;
let sig =
create_payment_proof_signature(slate.amount, &excess, p.sender_address, sender_key)?;
tx.payment_proof = Some(StoredProofInfo {
receiver_address: p.receiver_address,
receiver_signature: p.receiver_signature,
sender_address_path: derivation_index,
sender_address: sender_address.to_ed25519()?,
sender_signature: Some(sig),
})
}
wallet.store_tx(&format!("{}", tx.tx_slate_id.unwrap()), slate.tx_or_err()?)?;
let mut batch = wallet.batch(keychain_mask)?;
batch.save_tx_log_entry(tx, &parent_key)?;
batch.commit()?;
Ok(())
}
pub fn payment_proof_message(
amount: u64,
kernel_commitment: &pedersen::Commitment,
sender_address: DalekPublicKey,
) -> Result<Vec<u8>, Error> {
let mut msg = Vec::new();
msg.write_u64::<BigEndian>(amount)?;
msg.append(&mut kernel_commitment.0.to_vec());
msg.append(&mut sender_address.to_bytes().to_vec());
Ok(msg)
}
pub fn _decode_payment_proof_message(
msg: &[u8],
) -> Result<(u64, pedersen::Commitment, DalekPublicKey), Error> {
let mut rdr = Cursor::new(msg);
let amount = rdr.read_u64::<BigEndian>()?;
let mut commit_bytes = [0u8; 33];
for i in 0..33 {
commit_bytes[i] = rdr.read_u8()?;
}
let mut sender_address_bytes = [0u8; 32];
for i in 0..32 {
sender_address_bytes[i] = rdr.read_u8()?;
}
Ok((
amount,
pedersen::Commitment::from_vec(commit_bytes.to_vec()),
DalekPublicKey::from_bytes(&sender_address_bytes).unwrap(),
))
}
/// Create a payment proof.
pub fn create_payment_proof_signature(
amount: u64,
kernel_commitment: &pedersen::Commitment,
sender_address: DalekPublicKey,
sec_key: SecretKey,
) -> Result<DalekSignature, Error> {
let msg = payment_proof_message(amount, kernel_commitment, sender_address)?;
let d_skey = match DalekSecretKey::from_bytes(&sec_key.0) {
Ok(k) => k,
Err(e) => {
return Err(Error::ED25519Key(format!("{}", e)));
}
};
let pub_key: DalekPublicKey = (&d_skey).into();
let keypair = DalekKeypair {
public: pub_key,
secret: d_skey,
};
Ok(keypair.sign(&msg))
}
/// Verify all aspects of a completed payment proof on the current slate.
pub fn verify_slate_payment_proof<'a, T: ?Sized, C, K>(
wallet: &mut T,
keychain_mask: Option<&SecretKey>,
parent_key_id: &Identifier,
context: &Context,
slate: &Slate,
) -> Result<(), Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
let tx_vec = retrieve_txs(
wallet,
None,
Some(slate.id),
None,
Some(parent_key_id),
false,
)?;
if tx_vec.is_empty() {
return Err(Error::PaymentProof(
"TxLogEntry with original proof info not found (is account correct?)".to_owned(),
));
}
let orig_proof_info = tx_vec[0].clone().payment_proof;
if orig_proof_info.is_some() && slate.payment_proof.is_none() {
return Err(Error::PaymentProof(
"Expected Payment Proof for this Transaction is not present".to_owned(),
));
}
if let Some(ref p) = slate.clone().payment_proof {
let orig_proof_info = match orig_proof_info {
Some(p) => p.clone(),
None => {
return Err(Error::PaymentProof(
"Original proof info not stored in tx".to_owned(),
));
}
};
let keychain = wallet.keychain(keychain_mask)?;
let index = match context.payment_proof_derivation_index {
Some(i) => i,
None => {
return Err(Error::PaymentProof(
"Payment proof derivation index required".to_owned(),
));
}
};
let orig_sender_sk =
address::address_from_derivation_path(&keychain, parent_key_id, index)?;
let orig_sender_address = OnionV3Address::from_private(&orig_sender_sk.0)?;
if p.sender_address != orig_sender_address.to_ed25519()? {
return Err(Error::PaymentProof(
"Sender address on slate does not match original sender address".to_owned(),
));
}
if orig_proof_info.receiver_address != p.receiver_address {
return Err(Error::PaymentProof(
"Recipient address on slate does not match original recipient address".to_owned(),
));
}
let msg = payment_proof_message(
slate.amount,
&slate.calc_excess(&keychain.secp())?,
orig_sender_address.to_ed25519()?,
)?;
let sig = match p.receiver_signature {
Some(s) => s,
None => {
return Err(Error::PaymentProof(
"Recipient did not provide requested proof signature".to_owned(),
));
}
};
if p.receiver_address.verify(&msg, &sig).is_err() {
return Err(Error::PaymentProof("Invalid proof signature".to_owned()));
};
}
Ok(())
}

View file

@ -15,9 +15,9 @@
use std::sync::Arc;
use grin_keychain::ExtKeychain;
use grin_util::Mutex;
use grin_wallet_impls::{DefaultLCProvider, HTTPNodeClient};
use grin_wallet_libwallet::{TxLogEntry, WalletInfo, WalletInst};
use parking_lot::Mutex;
/// Mnemonic phrase setup mode.
#[derive(PartialEq, Clone)]

View file

@ -1,837 +0,0 @@
// Copyright 2023 The Grim 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.
use std::collections::{HashMap, HashSet};
use grin_keychain::{Identifier, Keychain, SwitchCommitmentType};
use grin_util as util;
use grin_util::secp::key::SecretKey;
use grin_util::secp::pedersen;
use grin_util::static_secp_instance;
use grin_wallet_libwallet::{
NodeClient, OutputData, OutputStatus, TxLogEntry, TxLogEntryType, WalletBackend, WalletInfo,
};
use grin_wallet_libwallet::{
OutputCommitMapping, RetrieveTxQueryArgs, RetrieveTxQuerySortField,
RetrieveTxQuerySortOrder,
};
use grin_wallet_libwallet::Error;
use log::{debug, warn};
use num_bigint::BigInt;
use uuid::Uuid;
/// Retrieve all of the outputs (doesn't attempt to update from node)
pub fn retrieve_outputs<'a, T: ?Sized, C, K>(
wallet: &mut T,
keychain_mask: Option<&SecretKey>,
show_spent: bool,
tx_id: Option<u32>,
parent_key_id: Option<&Identifier>,
) -> Result<Vec<OutputCommitMapping>, Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
// just read the wallet here, no need for a write lock
let mut outputs = wallet
.iter()
.filter(|out| show_spent || out.status != OutputStatus::Spent)
.collect::<Vec<_>>();
// only include outputs with a given tx_id if provided
if let Some(id) = tx_id {
outputs = outputs
.into_iter()
.filter(|out| out.tx_log_entry == Some(id))
.collect::<Vec<_>>();
}
if let Some(k) = parent_key_id {
outputs = outputs
.iter()
.filter(|o| o.root_key_id == *k)
.cloned()
.collect()
}
outputs.sort_by_key(|out| out.n_child);
let keychain = wallet.keychain(keychain_mask)?;
let res = outputs
.into_iter()
.map(|output| {
let commit = match output.commit.clone() {
Some(c) => pedersen::Commitment::from_vec(util::from_hex(&c).unwrap()),
None => keychain
.commit(output.value, &output.key_id, SwitchCommitmentType::Regular)
.unwrap(), // TODO: proper support for different switch commitment schemes
};
OutputCommitMapping { output, commit }
})
.collect();
Ok(res)
}
/// Apply advanced filtering to resultset from retrieve_txs below
pub fn apply_advanced_tx_list_filtering<'a, T: ?Sized, C, K>(
wallet: &mut T,
query_args: &RetrieveTxQueryArgs,
) -> Vec<TxLogEntry>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
// Apply simple bool, GTE or LTE fields
let txs_iter: Box<dyn Iterator<Item = TxLogEntry>> = Box::new(
wallet
.tx_log_iter()
.filter(|tx_entry| {
if let Some(v) = query_args.exclude_cancelled {
if v {
tx_entry.tx_type != TxLogEntryType::TxReceivedCancelled
&& tx_entry.tx_type != TxLogEntryType::TxSentCancelled
} else {
true
}
} else {
true
}
})
.filter(|tx_entry| {
if let Some(v) = query_args.include_outstanding_only {
if v {
!tx_entry.confirmed
} else {
true
}
} else {
true
}
})
.filter(|tx_entry| {
if let Some(v) = query_args.include_confirmed_only {
if v {
tx_entry.confirmed
} else {
true
}
} else {
true
}
})
.filter(|tx_entry| {
if let Some(v) = query_args.include_sent_only {
if v {
tx_entry.tx_type == TxLogEntryType::TxSent
|| tx_entry.tx_type == TxLogEntryType::TxSentCancelled
} else {
true
}
} else {
true
}
})
.filter(|tx_entry| {
if let Some(v) = query_args.include_received_only {
if v {
tx_entry.tx_type == TxLogEntryType::TxReceived
|| tx_entry.tx_type == TxLogEntryType::TxReceivedCancelled
} else {
true
}
} else {
true
}
})
.filter(|tx_entry| {
if let Some(v) = query_args.include_coinbase_only {
if v {
tx_entry.tx_type == TxLogEntryType::ConfirmedCoinbase
} else {
true
}
} else {
true
}
})
.filter(|tx_entry| {
if let Some(v) = query_args.include_reverted_only {
if v {
tx_entry.tx_type == TxLogEntryType::TxReverted
} else {
true
}
} else {
true
}
})
.filter(|tx_entry| {
if let Some(v) = query_args.min_id {
tx_entry.id >= v
} else {
true
}
})
.filter(|tx_entry| {
if let Some(v) = query_args.max_id {
tx_entry.id <= v
} else {
true
}
})
.filter(|tx_entry| {
if let Some(v) = query_args.min_amount {
if tx_entry.tx_type == TxLogEntryType::TxSent
|| tx_entry.tx_type == TxLogEntryType::TxSentCancelled
{
BigInt::from(tx_entry.amount_debited)
- BigInt::from(tx_entry.amount_credited)
>= BigInt::from(v)
} else {
BigInt::from(tx_entry.amount_credited)
- BigInt::from(tx_entry.amount_debited)
>= BigInt::from(v)
}
} else {
true
}
})
.filter(|tx_entry| {
if let Some(v) = query_args.max_amount {
if tx_entry.tx_type == TxLogEntryType::TxSent
|| tx_entry.tx_type == TxLogEntryType::TxSentCancelled
{
BigInt::from(tx_entry.amount_debited)
- BigInt::from(tx_entry.amount_credited)
<= BigInt::from(v)
} else {
BigInt::from(tx_entry.amount_credited)
- BigInt::from(tx_entry.amount_debited)
<= BigInt::from(v)
}
} else {
true
}
})
.filter(|tx_entry| {
if let Some(v) = query_args.min_creation_timestamp {
tx_entry.creation_ts >= v
} else {
true
}
})
.filter(|tx_entry| {
if let Some(v) = query_args.min_confirmed_timestamp {
tx_entry.creation_ts <= v
} else {
true
}
})
.filter(|tx_entry| {
if let Some(v) = query_args.min_confirmed_timestamp {
if let Some(t) = tx_entry.confirmation_ts {
t >= v
} else {
true
}
} else {
true
}
})
.filter(|tx_entry| {
if let Some(v) = query_args.max_confirmed_timestamp {
if let Some(t) = tx_entry.confirmation_ts {
t <= v
} else {
true
}
} else {
true
}
}),
);
let mut return_txs: Vec<TxLogEntry> = txs_iter.collect();
// Now apply requested sorting
if let Some(ref s) = query_args.sort_field {
match s {
RetrieveTxQuerySortField::Id => {
return_txs.sort_by_key(|tx| tx.id);
}
RetrieveTxQuerySortField::CreationTimestamp => {
return_txs.sort_by_key(|tx| tx.creation_ts);
}
RetrieveTxQuerySortField::ConfirmationTimestamp => {
return_txs.sort_by_key(|tx| tx.confirmation_ts);
}
RetrieveTxQuerySortField::TotalAmount => {
return_txs.sort_by_key(|tx| {
if tx.tx_type == TxLogEntryType::TxSent
|| tx.tx_type == TxLogEntryType::TxSentCancelled
{
BigInt::from(tx.amount_debited) - BigInt::from(tx.amount_credited)
} else {
BigInt::from(tx.amount_credited) - BigInt::from(tx.amount_debited)
}
});
}
RetrieveTxQuerySortField::AmountCredited => {
return_txs.sort_by_key(|tx| tx.amount_credited);
}
RetrieveTxQuerySortField::AmountDebited => {
return_txs.sort_by_key(|tx| tx.amount_debited);
}
}
} else {
return_txs.sort_by_key(|tx| tx.id);
}
if let Some(ref s) = query_args.sort_order {
match s {
RetrieveTxQuerySortOrder::Desc => return_txs.reverse(),
_ => {}
}
}
// Apply limit if requested
if let Some(l) = query_args.limit {
return_txs = return_txs.into_iter().take(l as usize).collect()
}
return_txs
}
/// Retrieve all of the transaction entries, or a particular entry
/// if `parent_key_id` is set, only return entries from that key
pub fn retrieve_txs<'a, T: ?Sized, C, K>(
wallet: &mut T,
tx_id: Option<u32>,
tx_slate_id: Option<Uuid>,
query_args: Option<RetrieveTxQueryArgs>,
parent_key_id: Option<&Identifier>,
outstanding_only: bool,
) -> Result<Vec<TxLogEntry>, Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
let mut txs;
// Adding in new transaction list query logic. If `tx_id` or `tx_slate_id`
// is provided, then `query_args` is ignored and old logic is followed.
if query_args.is_some() && tx_id.is_none() && tx_slate_id.is_none() {
txs = apply_advanced_tx_list_filtering(wallet, &query_args.unwrap())
} else {
txs = wallet
.tx_log_iter()
.filter(|tx_entry| {
let f_pk = match parent_key_id {
Some(k) => tx_entry.parent_key_id == *k,
None => true,
};
let f_tx_id = match tx_id {
Some(i) => tx_entry.id == i,
None => true,
};
let f_txs = match tx_slate_id {
Some(t) => tx_entry.tx_slate_id == Some(t),
None => true,
};
let f_outstanding = match outstanding_only {
true => {
!tx_entry.confirmed
&& (tx_entry.tx_type == TxLogEntryType::TxReceived
|| tx_entry.tx_type == TxLogEntryType::TxSent
|| tx_entry.tx_type == TxLogEntryType::TxReverted)
}
false => true,
};
f_pk && f_tx_id && f_txs && f_outstanding
})
.collect();
txs.sort_by_key(|tx| tx.creation_ts);
}
Ok(txs)
}
/// Refreshes the outputs in a wallet with the latest information
/// from a node
pub fn refresh_outputs<'a, T: ?Sized, C, K>(
wallet: &mut T,
keychain_mask: Option<&SecretKey>,
parent_key_id: &Identifier,
update_all: bool,
) -> Result<(), Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
let height = wallet.w2n_client().get_chain_tip()?.0;
refresh_output_state(wallet, keychain_mask, height, parent_key_id, update_all)?;
Ok(())
}
/// build a local map of wallet outputs keyed by commit
/// and a list of outputs we want to query the node for
pub fn map_wallet_outputs<'a, T: ?Sized, C, K>(
wallet: &mut T,
keychain_mask: Option<&SecretKey>,
parent_key_id: &Identifier,
update_all: bool,
) -> Result<HashMap<pedersen::Commitment, (Identifier, Option<u64>, Option<u32>, bool)>, Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
let mut wallet_outputs = HashMap::new();
let keychain = wallet.keychain(keychain_mask)?;
let unspents: Vec<OutputData> = wallet
.iter()
.filter(|x| x.root_key_id == *parent_key_id && x.status != OutputStatus::Spent)
.collect();
let tx_entries = retrieve_txs(wallet, None, None, None, Some(&parent_key_id), true)?;
// Only select outputs that are actually involved in an outstanding transaction
let unspents = match update_all {
false => unspents
.into_iter()
.filter(|x| match x.tx_log_entry.as_ref() {
Some(t) => tx_entries.iter().any(|te| te.id == *t),
None => true,
})
.collect(),
true => unspents,
};
for out in unspents {
let commit = match out.commit.clone() {
Some(c) => pedersen::Commitment::from_vec(util::from_hex(&c).unwrap()),
None => keychain
.commit(out.value, &out.key_id, SwitchCommitmentType::Regular)
.unwrap(), // TODO: proper support for different switch commitment schemes
};
let val = (
out.key_id.clone(),
out.mmr_index,
out.tx_log_entry,
out.status == OutputStatus::Unspent,
);
wallet_outputs.insert(commit, val);
}
Ok(wallet_outputs)
}
/// Cancel transaction and associated outputs
pub fn cancel_tx_and_outputs<'a, T: ?Sized, C, K>(
wallet: &mut T,
keychain_mask: Option<&SecretKey>,
mut tx: TxLogEntry,
outputs: Vec<OutputData>,
parent_key_id: &Identifier,
) -> Result<(), Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
let mut batch = wallet.batch(keychain_mask)?;
for mut o in outputs {
// unlock locked outputs
if o.status == OutputStatus::Unconfirmed || o.status == OutputStatus::Reverted {
batch.delete(&o.key_id, &o.mmr_index)?;
}
if o.status == OutputStatus::Locked {
o.status = OutputStatus::Unspent;
batch.save(o)?;
}
}
match tx.tx_type {
TxLogEntryType::TxSent => tx.tx_type = TxLogEntryType::TxSentCancelled,
TxLogEntryType::TxReceived | TxLogEntryType::TxReverted => {
tx.tx_type = TxLogEntryType::TxReceivedCancelled
}
_ => {}
}
batch.save_tx_log_entry(tx, parent_key_id)?;
batch.commit()?;
Ok(())
}
/// Apply refreshed API output data to the wallet
pub fn apply_api_outputs<'a, T: ?Sized, C, K>(
wallet: &mut T,
keychain_mask: Option<&SecretKey>,
wallet_outputs: &HashMap<pedersen::Commitment, (Identifier, Option<u64>, Option<u32>, bool)>,
api_outputs: &HashMap<pedersen::Commitment, (String, u64, u64)>,
reverted_kernels: HashSet<u32>,
height: u64,
parent_key_id: &Identifier,
) -> Result<(), Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
// now for each commit, find the output in the wallet and the corresponding
// api output (if it exists) and refresh it in-place in the wallet.
// Note: minimizing the time we spend holding the wallet lock.
{
let last_confirmed_height = wallet.last_confirmed_height()?;
// If the server height is less than our confirmed height, don't apply
// these changes as the chain is syncing, incorrect or forking
if height < last_confirmed_height {
warn!(
"Not updating outputs as the height of the node's chain \
is less than the last reported wallet update height."
);
warn!("Please wait for sync on node to complete or fork to resolve and try again.");
return Ok(());
}
let mut batch = wallet.batch(keychain_mask)?;
for (commit, (id, mmr_index, _, _)) in wallet_outputs.iter() {
if let Ok(mut output) = batch.get(id, mmr_index) {
match api_outputs.get(&commit) {
Some(o) => {
// if this is a coinbase tx being confirmed, it's recordable in tx log
if output.is_coinbase && output.status == OutputStatus::Unconfirmed {
let log_id = batch.next_tx_log_id(parent_key_id)?;
let mut t = TxLogEntry::new(
parent_key_id.clone(),
TxLogEntryType::ConfirmedCoinbase,
log_id,
);
t.confirmed = true;
t.amount_credited = output.value;
t.amount_debited = 0;
t.num_outputs = 1;
// calculate kernel excess for coinbase
{
let secp = static_secp_instance();
let secp = secp.lock();
let over_commit = secp.commit_value(output.value)?;
let excess = secp.commit_sum(vec![*commit], vec![over_commit])?;
t.kernel_excess = Some(excess);
t.kernel_lookup_min_height = Some(height);
}
t.update_confirmation_ts();
output.tx_log_entry = Some(log_id);
batch.save_tx_log_entry(t, &parent_key_id)?;
}
// also mark the transaction in which this output is involved as confirmed
// note that one involved input/output confirmation SHOULD be enough
// to reliably confirm the tx
if !output.is_coinbase
&& (output.status == OutputStatus::Unconfirmed
|| output.status == OutputStatus::Reverted)
{
let tx = batch.tx_log_iter().find(|t| {
Some(t.id) == output.tx_log_entry
&& t.parent_key_id == *parent_key_id
});
if let Some(mut t) = tx {
if t.tx_type == TxLogEntryType::TxReverted {
t.tx_type = TxLogEntryType::TxReceived;
t.reverted_after = None;
}
t.update_confirmation_ts();
t.confirmed = true;
batch.save_tx_log_entry(t, &parent_key_id)?;
}
}
output.height = o.1;
output.mark_unspent();
}
None => {
if !output.is_coinbase
&& output
.tx_log_entry
.map(|i| reverted_kernels.contains(&i))
.unwrap_or(false)
{
output.mark_reverted();
} else {
output.mark_spent();
}
}
}
batch.save(output)?;
}
}
for mut tx in batch.tx_log_iter() {
if reverted_kernels.contains(&tx.id) && tx.parent_key_id == *parent_key_id {
tx.tx_type = TxLogEntryType::TxReverted;
tx.reverted_after = tx.confirmation_ts.clone().and_then(|t| {
let now = chrono::Utc::now();
(now - t).to_std().ok()
});
tx.confirmed = false;
batch.save_tx_log_entry(tx, &parent_key_id)?;
}
}
{
batch.save_last_confirmed_height(parent_key_id, height)?;
}
batch.commit()?;
}
Ok(())
}
/// Builds a single api query to retrieve the latest output data from the node.
/// So we can refresh the local wallet outputs.
pub fn refresh_output_state<'a, T: ?Sized, C, K>(
wallet: &mut T,
keychain_mask: Option<&SecretKey>,
height: u64,
parent_key_id: &Identifier,
update_all: bool,
) -> Result<(), Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
debug!("Refreshing wallet outputs");
// build a local map of wallet outputs keyed by commit
// and a list of outputs we want to query the node for
let wallet_outputs = map_wallet_outputs(wallet, keychain_mask, parent_key_id, update_all)?;
let wallet_output_keys = wallet_outputs.keys().copied().collect();
let api_outputs = wallet
.w2n_client()
.get_outputs_from_node(wallet_output_keys)?;
// For any disappeared output, check the on-chain status of the corresponding transaction kernel
// If it is no longer present, the transaction was reverted due to a re-org
let reverted_kernels =
find_reverted_kernels(wallet, &wallet_outputs, &api_outputs, parent_key_id)?;
apply_api_outputs(
wallet,
keychain_mask,
&wallet_outputs,
&api_outputs,
reverted_kernels,
height,
parent_key_id,
)?;
clean_old_unconfirmed(wallet, keychain_mask, height)?;
Ok(())
}
fn find_reverted_kernels<'a, T: ?Sized, C, K>(
wallet: &mut T,
wallet_outputs: &HashMap<pedersen::Commitment, (Identifier, Option<u64>, Option<u32>, bool)>,
api_outputs: &HashMap<pedersen::Commitment, (String, u64, u64)>,
parent_key_id: &Identifier,
) -> Result<HashSet<u32>, Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
let mut client = wallet.w2n_client().clone();
let mut ids = HashSet::new();
// Get transaction IDs for outputs that are no longer unspent
for (commit, (_, _, tx_id, was_unspent)) in wallet_outputs {
if let Some(tx_id) = *tx_id {
if *was_unspent && !api_outputs.contains_key(commit) {
ids.insert(tx_id);
}
}
}
// Get corresponding kernels
let kernels = wallet
.tx_log_iter()
.filter(|t| {
ids.contains(&t.id)
&& t.parent_key_id == *parent_key_id
&& t.tx_type == TxLogEntryType::TxReceived
})
.filter_map(|t| {
t.kernel_excess
.map(|e| (t.id, e, t.kernel_lookup_min_height))
});
// Check each of the kernels on-chain
let mut reverted = HashSet::new();
for (id, excess, min_height) in kernels {
if client.get_kernel(&excess, min_height, None)?.is_none() {
reverted.insert(id);
}
}
Ok(reverted)
}
fn clean_old_unconfirmed<'a, T: ?Sized, C, K>(
wallet: &mut T,
keychain_mask: Option<&SecretKey>,
height: u64,
) -> Result<(), Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
if height < 50 {
return Ok(());
}
let mut ids_to_del = vec![];
for out in wallet.iter() {
if out.status == OutputStatus::Unconfirmed
&& out.height > 0
&& out.height < height - 50
&& out.is_coinbase
{
ids_to_del.push(out.key_id.clone())
}
}
let mut batch = wallet.batch(keychain_mask)?;
for id in ids_to_del {
batch.delete(&id, &None)?;
}
batch.commit()?;
Ok(())
}
/// Retrieve summary info about the wallet
/// caller should refresh first if desired
pub fn retrieve_info<'a, T: ?Sized, C, K>(
wallet: &mut T,
parent_key_id: &Identifier,
minimum_confirmations: u64,
) -> Result<WalletInfo, Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
let current_height = wallet.last_confirmed_height()?;
let outputs = wallet
.iter()
.filter(|out| out.root_key_id == *parent_key_id);
let mut unspent_total = 0;
let mut immature_total = 0;
let mut awaiting_finalization_total = 0;
let mut unconfirmed_total = 0;
let mut locked_total = 0;
let mut reverted_total = 0;
for out in outputs {
match out.status {
OutputStatus::Unspent => {
if out.is_coinbase && out.lock_height > current_height {
immature_total += out.value;
} else if out.num_confirmations(current_height) < minimum_confirmations {
// Treat anything less than minimum confirmations as "unconfirmed".
unconfirmed_total += out.value;
} else {
unspent_total += out.value;
}
}
OutputStatus::Unconfirmed => {
// We ignore unconfirmed coinbase outputs completely.
if !out.is_coinbase {
if minimum_confirmations == 0 {
unconfirmed_total += out.value;
} else {
awaiting_finalization_total += out.value;
}
}
}
OutputStatus::Locked => {
locked_total += out.value;
}
OutputStatus::Reverted => reverted_total += out.value,
OutputStatus::Spent => {}
}
}
Ok(WalletInfo {
last_confirmed_height: current_height,
minimum_confirmations,
total: unspent_total + unconfirmed_total + immature_total,
amount_awaiting_finalization: awaiting_finalization_total,
amount_awaiting_confirmation: unconfirmed_total,
amount_immature: immature_total,
amount_locked: locked_total,
amount_currently_spendable: unspent_total,
amount_reverted: reverted_total,
})
}
/// Rollback outputs associated with a transaction in the wallet
pub fn cancel_tx<'a, T: ?Sized, C, K>(
wallet: &mut T,
keychain_mask: Option<&SecretKey>,
parent_key_id: &Identifier,
tx_id: Option<u32>,
tx_slate_id: Option<Uuid>,
) -> Result<(), Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
let mut tx_id_string = String::new();
if let Some(tx_id) = tx_id {
tx_id_string = tx_id.to_string();
} else if let Some(tx_slate_id) = tx_slate_id {
tx_id_string = tx_slate_id.to_string();
}
let tx_vec = retrieve_txs(
wallet,
tx_id,
tx_slate_id,
None,
Some(&parent_key_id),
false,
)?;
if tx_vec.len() != 1 {
return Err(Error::TransactionDoesntExist(tx_id_string));
}
let tx = tx_vec[0].clone();
match tx.tx_type {
TxLogEntryType::TxSent | TxLogEntryType::TxReceived | TxLogEntryType::TxReverted => {}
_ => return Err(Error::TransactionNotCancellable(tx_id_string)),
}
if tx.confirmed {
return Err(Error::TransactionNotCancellable(tx_id_string));
}
// get outputs associated with tx
let res = retrieve_outputs(
wallet,
keychain_mask,
false,
Some(tx.id),
Some(&parent_key_id),
)?;
let outputs = res.iter().map(|m| m.output.clone()).collect();
cancel_tx_and_outputs(wallet, keychain_mask, tx, outputs, parent_key_id)?;
Ok(())
}

View file

@ -22,13 +22,13 @@ use std::time::Duration;
use grin_chain::SyncStatus;
use grin_core::global;
use grin_keychain::{ExtKeychain, Keychain};
use grin_util::Mutex;
use grin_util::secp::SecretKey;
use grin_util::types::ZeroingString;
use grin_wallet_api::Owner;
use grin_wallet_impls::{DefaultLCProvider, DefaultWalletImpl, HTTPNodeClient};
use grin_wallet_libwallet::{Error, NodeClient, StatusMessage, WalletInst, WalletLCProvider};
use grin_wallet_libwallet::api_impl::owner::{retrieve_summary_info, retrieve_txs};
use parking_lot::Mutex;
use crate::node::{Node, NodeConfig};
use crate::wallet::{ConnectionsConfig, ExternalConnection, WalletConfig};