build + wallet + ui: fix dependencies connection setup, mnemonic validation, wallet creation, methods to work with wallet, update translations
This commit is contained in:
parent
f461f27e4c
commit
2e12b17663
19 changed files with 3243 additions and 590 deletions
749
Cargo.lock
generated
749
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
33
Cargo.toml
33
Cargo.toml
|
@ -16,42 +16,47 @@ log = "0.4"
|
|||
|
||||
## node
|
||||
openssl-sys = { version = "0.9.82", features = ["vendored"] }
|
||||
grin_api = { path = "../grin/node/api" }
|
||||
grin_chain = { path = "../grin/node/chain" }
|
||||
grin_config = { path = "../grin/node/config" }
|
||||
grin_core = { path = "../grin/node/core" }
|
||||
grin_keychain = { path = "../grin/node/keychain" }
|
||||
grin_p2p = { path = "../grin/node/p2p" }
|
||||
grin_servers = { path = "../grin/node/servers" }
|
||||
grin_util = { path = "../grin/node/util" }
|
||||
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" }
|
||||
|
||||
## wallet
|
||||
grin_wallet_impls = { path = "../grin/wallet/impls" }
|
||||
#grin_wallet_api = "5.1.0"
|
||||
#grin_wallet_libwallet = "5.1.0"
|
||||
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_util = "5.1.0"
|
||||
|
||||
## ui
|
||||
pollster = "0.3.0"
|
||||
wgpu = "0.16.1"
|
||||
egui = { version = "0.22.0", default-features = false }
|
||||
egui_extras = { version = "0.22.0", features = ["image"] }
|
||||
rust-i18n = "2.1.0"
|
||||
|
||||
## other
|
||||
futures = "0.3"
|
||||
dirs = "5.0.1"
|
||||
once_cell = "1.10.0"
|
||||
rust-i18n = "2.0.0"
|
||||
sys-locale = "0.3.0"
|
||||
chrono = "0.4.23"
|
||||
lazy_static = "1.4.0"
|
||||
toml = "0.7.4"
|
||||
serde = "1"
|
||||
pnet = "0.33.0"
|
||||
pnet = "0.34.0"
|
||||
zeroize = "1.6.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"
|
||||
|
|
|
@ -16,6 +16,7 @@ wallets:
|
|||
words_count: 'Words count:'
|
||||
enter_word: 'Enter word #%{number}:'
|
||||
not_valid_word: Entered word is not valid
|
||||
not_valid_phrase: Entered phrase is not valid
|
||||
create_phrase_desc: Safely write down and save your recovery phrase.
|
||||
restore_phrase_desc: Enter words from your saved recovery phrase.
|
||||
setup_conn_desc: Choose how your wallet connects to the network.
|
||||
|
|
|
@ -16,6 +16,7 @@ wallets:
|
|||
words_count: 'Количество слов:'
|
||||
enter_word: 'Введите слово #%{number}:'
|
||||
not_valid_word: Введено недопустимое слово
|
||||
not_valid_phrase: Введена недопустимая фраза восстановления
|
||||
create_phrase_desc: Безопасно запишите и сохраните вашу фразу восстановления.
|
||||
restore_phrase_desc: Введите слова из вашей сохранённой фразы восстановления.
|
||||
setup_conn_desc: Выберите способ подключения вашего кошелька к сети.
|
||||
|
|
|
@ -168,7 +168,7 @@ impl View {
|
|||
let size = egui::Vec2::splat(2.0 * r + 5.0);
|
||||
let (rect, br) = ui.allocate_at_least(size, Sense::click_and_drag());
|
||||
|
||||
let mut icon_size = 22.0;
|
||||
let mut icon_size = 24.0;
|
||||
let mut icon_color = Colors::TEXT_BUTTON;
|
||||
|
||||
// Increase radius and change icon size and color on-hover.
|
||||
|
|
|
@ -23,6 +23,7 @@ use crate::gui::views::{Modal, ModalPosition, View};
|
|||
use crate::gui::views::wallets::creation::MnemonicSetup;
|
||||
use crate::gui::views::wallets::creation::types::{PhraseMode, Step};
|
||||
use crate::gui::views::wallets::setup::ConnectionSetup;
|
||||
use crate::wallet::WalletList;
|
||||
|
||||
/// Wallet creation content.
|
||||
pub struct WalletCreation {
|
||||
|
@ -70,11 +71,12 @@ impl WalletCreation {
|
|||
pub const NAME_PASS_MODAL: &'static str = "name_pass_modal";
|
||||
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
// Show wallet creation step description and confirmation bottom panel.
|
||||
// Show wallet creation step description and confirmation panel.
|
||||
if self.step.is_some() {
|
||||
egui::TopBottomPanel::bottom("wallet_creation_step_panel")
|
||||
.frame(egui::Frame {
|
||||
stroke: View::DEFAULT_STROKE,
|
||||
fill: Colors::FILL_DARK,
|
||||
inner_margin: Margin {
|
||||
left: View::far_left_inset_margin(ui) + 4.0,
|
||||
right: View::get_right_inset() + 4.0,
|
||||
|
@ -85,65 +87,15 @@ impl WalletCreation {
|
|||
})
|
||||
.show_inside(ui, |ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
if let Some(step) = &self.step {
|
||||
// Setup step description text and availability.
|
||||
let (step_text, step_available) = match step {
|
||||
Step::EnterMnemonic => {
|
||||
let mode = &self.mnemonic_setup.mnemonic.mode;
|
||||
let text = if mode == &PhraseMode::Generate {
|
||||
t!("wallets.create_phrase_desc")
|
||||
} else {
|
||||
t!("wallets.restore_phrase_desc")
|
||||
};
|
||||
let available = !self
|
||||
.mnemonic_setup
|
||||
.mnemonic
|
||||
.words
|
||||
.contains(&String::from(""));
|
||||
(text, available)
|
||||
}
|
||||
Step::ConfirmMnemonic => {
|
||||
let text = t!("wallets.restore_phrase_desc");
|
||||
let available = !self
|
||||
.mnemonic_setup
|
||||
.mnemonic
|
||||
.confirm_words
|
||||
.contains(&String::from(""));
|
||||
(text, available)
|
||||
},
|
||||
Step::SetupConnection => (t!("wallets.setup_conn_desc"), true)
|
||||
};
|
||||
// Show step description.
|
||||
ui.label(RichText::new(step_text).size(16.0).color(Colors::GRAY));
|
||||
|
||||
// Show next step button if there are no empty words.
|
||||
|
||||
if step_available {
|
||||
// Setup button text.
|
||||
let (next_text, color) = if step == &Step::SetupConnection {
|
||||
(format!("{} {}", CHECK, t!("complete")), Colors::GOLD)
|
||||
} else {
|
||||
let text = format!("{} {}", SHARE_FAT, t!("continue"));
|
||||
(text, Colors::WHITE)
|
||||
};
|
||||
|
||||
ui.add_space(4.0);
|
||||
// Show button.
|
||||
View::button(ui, next_text.to_uppercase(), color, || {
|
||||
self.forward();
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
}
|
||||
}
|
||||
self.step_control_ui(ui);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Show wallet creation step content.
|
||||
// Show wallet creation step content panel.
|
||||
egui::CentralPanel::default()
|
||||
.frame(egui::Frame {
|
||||
stroke: View::DEFAULT_STROKE,
|
||||
fill: if self.step.is_none() { Colors::FILL_DARK } else { Colors::WHITE },
|
||||
inner_margin: Margin {
|
||||
left: View::far_left_inset_margin(ui) + 4.0,
|
||||
right: View::get_right_inset() + 4.0,
|
||||
|
@ -153,12 +105,74 @@ impl WalletCreation {
|
|||
..Default::default()
|
||||
})
|
||||
.show_inside(ui, |ui| {
|
||||
self.step_ui(ui, cb);
|
||||
self.step_content_ui(ui, cb);
|
||||
});
|
||||
}
|
||||
|
||||
/// Draw [`Step`] description and confirmation control.
|
||||
fn step_control_ui(&mut self, ui: &mut egui::Ui) {
|
||||
if let Some(step) = &self.step {
|
||||
// Setup step description text and availability.
|
||||
let (step_text, mut step_available) = match step {
|
||||
Step::EnterMnemonic => {
|
||||
let mode = &self.mnemonic_setup.mnemonic.mode;
|
||||
let text = if mode == &PhraseMode::Generate {
|
||||
t!("wallets.create_phrase_desc")
|
||||
} else {
|
||||
t!("wallets.restore_phrase_desc")
|
||||
};
|
||||
let available = !self
|
||||
.mnemonic_setup
|
||||
.mnemonic
|
||||
.words
|
||||
.contains(&String::from(""));
|
||||
(text, available)
|
||||
}
|
||||
Step::ConfirmMnemonic => {
|
||||
let text = t!("wallets.restore_phrase_desc");
|
||||
let available = !self
|
||||
.mnemonic_setup
|
||||
.mnemonic
|
||||
.confirm_words
|
||||
.contains(&String::from(""));
|
||||
(text, available)
|
||||
},
|
||||
Step::SetupConnection => (t!("wallets.setup_conn_desc"), true)
|
||||
};
|
||||
// Show step description.
|
||||
ui.label(RichText::new(step_text).size(16.0).color(Colors::GRAY));
|
||||
|
||||
// Show error if entered phrase is not valid.
|
||||
if !self.mnemonic_setup.valid_phrase {
|
||||
step_available = false;
|
||||
ui.label(RichText::new(t!("wallets.not_valid_phrase"))
|
||||
.size(16.0)
|
||||
.color(Colors::RED));
|
||||
ui.add_space(2.0);
|
||||
}
|
||||
|
||||
// Show next step button if there are no empty words.
|
||||
if step_available {
|
||||
// Setup button text.
|
||||
let (next_text, color) = if step == &Step::SetupConnection {
|
||||
(format!("{} {}", CHECK, t!("complete")), Colors::GOLD)
|
||||
} else {
|
||||
let text = format!("{} {}", SHARE_FAT, t!("continue"));
|
||||
(text, Colors::WHITE)
|
||||
};
|
||||
|
||||
ui.add_space(4.0);
|
||||
// Show button.
|
||||
View::button(ui, next_text.to_uppercase(), color, || {
|
||||
self.forward();
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw wallet creation [`Step`] content.
|
||||
fn step_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
fn step_content_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
match &self.step {
|
||||
None => {
|
||||
// Show wallet creation message if step is empty.
|
||||
|
@ -233,12 +247,23 @@ impl WalletCreation {
|
|||
if self.mnemonic_setup.mnemonic.mode == PhraseMode::Generate {
|
||||
Some(Step::ConfirmMnemonic)
|
||||
} else {
|
||||
Some(Step::SetupConnection)
|
||||
// Check if entered phrase was valid.
|
||||
if self.mnemonic_setup.valid_phrase {
|
||||
Some(Step::SetupConnection)
|
||||
} else {
|
||||
Some(Step::EnterMnemonic)
|
||||
}
|
||||
}
|
||||
}
|
||||
Step::ConfirmMnemonic => Some(Step::SetupConnection),
|
||||
Step::SetupConnection => {
|
||||
//TODO: Confirm mnemonic
|
||||
// Create wallet at last step.
|
||||
WalletList::create_wallet(
|
||||
self.name_edit.clone(),
|
||||
self.pass_edit.clone(),
|
||||
self.mnemonic_setup.mnemonic.get_phrase(),
|
||||
self.network_setup.get_ext_conn_url()
|
||||
).unwrap();
|
||||
None
|
||||
}
|
||||
}
|
||||
|
@ -249,7 +274,7 @@ impl WalletCreation {
|
|||
/// Start wallet creation from showing [`Modal`] to enter name and password.
|
||||
pub fn show_name_pass_modal(&mut self) {
|
||||
// Reset modal values.
|
||||
self.hide_pass = false;
|
||||
self.hide_pass = true;
|
||||
self.modal_just_opened = true;
|
||||
self.name_edit = String::from("");
|
||||
self.pass_edit = String::from("");
|
||||
|
|
|
@ -25,6 +25,9 @@ pub struct MnemonicSetup {
|
|||
/// Current mnemonic phrase.
|
||||
pub(crate) mnemonic: Mnemonic,
|
||||
|
||||
/// Flag to check if entered phrase was valid.
|
||||
pub(crate) valid_phrase: bool,
|
||||
|
||||
/// Current word number to edit at [`Modal`].
|
||||
word_num_edit: usize,
|
||||
/// Entered word value for [`Modal`].
|
||||
|
@ -37,6 +40,7 @@ impl Default for MnemonicSetup {
|
|||
fn default() -> Self {
|
||||
Self {
|
||||
mnemonic: Mnemonic::default(),
|
||||
valid_phrase: true,
|
||||
word_num_edit: 0,
|
||||
word_edit: String::from(""),
|
||||
valid_word_edit: true
|
||||
|
@ -290,6 +294,10 @@ impl MnemonicSetup {
|
|||
let close_modal = words.len() == self.word_num_edit
|
||||
|| !words.get(self.word_num_edit).unwrap().is_empty();
|
||||
if close_modal {
|
||||
// Check if entered phrase was valid when all words were entered.
|
||||
if !self.mnemonic.words.contains(&String::from("")) {
|
||||
self.valid_phrase = self.mnemonic.is_valid_phrase();
|
||||
}
|
||||
cb.hide_keyboard();
|
||||
modal.close();
|
||||
} else {
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use grin_keychain::mnemonic::{from_entropy, search};
|
||||
use grin_keychain::mnemonic::{from_entropy, search, to_entropy};
|
||||
use rand::{Rng, thread_rng};
|
||||
use zeroize::{Zeroize, ZeroizeOnDrop};
|
||||
|
||||
|
@ -124,6 +124,16 @@ impl Mnemonic {
|
|||
valid && equal
|
||||
}
|
||||
|
||||
/// Check if current phrase is valid.
|
||||
pub fn is_valid_phrase(&self) -> bool {
|
||||
to_entropy(self.get_phrase().as_str()).is_ok()
|
||||
}
|
||||
|
||||
/// Get phrase from words.
|
||||
pub fn get_phrase(&self) -> String {
|
||||
self.words.iter().map(|x| x.to_string() + " ").collect::<String>()
|
||||
}
|
||||
|
||||
/// Generate list of words based on provided [`PhraseMode`] and [`PhraseSize`].
|
||||
fn generate_words(mode: &PhraseMode, size: &PhraseSize) -> Vec<String> {
|
||||
match mode {
|
||||
|
|
|
@ -52,6 +52,14 @@ impl ConnectionSetup {
|
|||
// Self { method: ConnectionMethod::Integrated }
|
||||
// }
|
||||
|
||||
/// Get external node connection URL.
|
||||
pub fn get_ext_conn_url(&self) -> Option<String> {
|
||||
match &self.method {
|
||||
ConnectionMethod::Integrated => None,
|
||||
ConnectionMethod::External(url) => Some(url.clone())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
ScrollArea::vertical()
|
||||
.id_source("wallet_connection_setup")
|
||||
|
@ -99,11 +107,6 @@ impl ConnectionSetup {
|
|||
});
|
||||
}
|
||||
|
||||
/// Draw external connections setup.
|
||||
fn external_conn_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
|
||||
}
|
||||
|
||||
/// Draw modal content.
|
||||
pub fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||
ui.add_space(6.0);
|
||||
|
|
|
@ -23,13 +23,10 @@ use crate::gui::views::{Modal, ModalContainer, Root, TitlePanel, TitleType, View
|
|||
use crate::gui::views::wallets::creation::{MnemonicSetup, WalletCreation};
|
||||
use crate::gui::views::wallets::setup::ConnectionSetup;
|
||||
use crate::gui::views::wallets::wallet::WalletContent;
|
||||
use crate::wallet::{Wallet, WalletList};
|
||||
use crate::wallet::WalletList;
|
||||
|
||||
/// Wallets content.
|
||||
pub struct WalletsContent {
|
||||
/// List of wallets.
|
||||
list: Vec<Wallet>,
|
||||
|
||||
/// Selected list item content.
|
||||
item_content: Option<WalletContent>,
|
||||
/// Wallet creation content.
|
||||
|
@ -42,7 +39,6 @@ pub struct WalletsContent {
|
|||
impl Default for WalletsContent {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
list: WalletList::list(),
|
||||
item_content: None,
|
||||
creation_content: WalletCreation::default(),
|
||||
modal_ids: vec![
|
||||
|
@ -83,14 +79,22 @@ impl WalletsContent {
|
|||
// Show title panel.
|
||||
self.title_ui(ui, frame);
|
||||
|
||||
let is_wallet_panel_open = Self::is_dual_panel_mode(ui, frame) || self.list.is_empty();
|
||||
let wallets = WalletList::list();
|
||||
|
||||
let is_dual_panel = Self::is_dual_panel_mode(ui, frame);
|
||||
let is_wallet_creation = self.creation_content.can_go_back();
|
||||
let is_wallet_panel_open = is_dual_panel || is_wallet_creation || wallets.is_empty();
|
||||
let wallet_panel_width = self.wallet_panel_width(ui, frame);
|
||||
// Show wallet content.
|
||||
egui::SidePanel::right("wallet_panel")
|
||||
.resizable(false)
|
||||
.min_width(wallet_panel_width)
|
||||
.frame(egui::Frame {
|
||||
fill: if self.list.is_empty() { Colors::FILL_DARK } else { Colors::WHITE },
|
||||
fill: if !wallets.is_empty() || is_wallet_creation {
|
||||
Colors::WHITE
|
||||
} else {
|
||||
Colors::FILL_DARK
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.show_animated_inside(ui, is_wallet_panel_open, |ui| {
|
||||
|
@ -98,7 +102,7 @@ impl WalletsContent {
|
|||
});
|
||||
|
||||
// Show list of wallets.
|
||||
if !self.list.is_empty() {
|
||||
if !is_wallet_creation && !wallets.is_empty() {
|
||||
egui::CentralPanel::default()
|
||||
.frame(egui::Frame {
|
||||
stroke: View::DEFAULT_STROKE,
|
||||
|
@ -113,6 +117,12 @@ impl WalletsContent {
|
|||
})
|
||||
.show_inside(ui, |ui| {
|
||||
//TODO: wallets list
|
||||
for w in WalletList::list() {
|
||||
ui.label(w.config.get_name());
|
||||
View::button(ui, "get info".to_string(), Colors::GOLD, || {
|
||||
println!("12345 amount {}", w.get_txs_info(10).unwrap().2.total);
|
||||
});
|
||||
}
|
||||
});
|
||||
// Show wallet creation button if wallet panel is not open.
|
||||
if !is_wallet_panel_open {
|
||||
|
@ -154,7 +164,7 @@ impl WalletsContent {
|
|||
ui: &mut egui::Ui,
|
||||
frame: &mut eframe::Frame,
|
||||
cb: &dyn PlatformCallbacks) {
|
||||
if self.list.is_empty() || self.item_content.is_none() {
|
||||
if WalletList::list().is_empty() || self.item_content.is_none() {
|
||||
self.creation_content.ui(ui, cb)
|
||||
} else {
|
||||
self.item_content.as_mut().unwrap().ui(ui, frame, cb);
|
||||
|
@ -163,18 +173,19 @@ impl WalletsContent {
|
|||
|
||||
/// Get [`WalletContent`] panel width.
|
||||
fn wallet_panel_width(&self, ui: &mut egui::Ui, frame: &mut eframe::Frame) -> f32 {
|
||||
let is_wallet_creation = self.creation_content.can_go_back();
|
||||
let available_width = if WalletList::list().is_empty() || is_wallet_creation {
|
||||
ui.available_width()
|
||||
} else {
|
||||
ui.available_width() - Root::SIDE_PANEL_MIN_WIDTH
|
||||
};
|
||||
if Self::is_dual_panel_mode(ui, frame) {
|
||||
let min_width = (Root::SIDE_PANEL_MIN_WIDTH + View::get_right_inset()) as i64;
|
||||
let available_width = if self.list.is_empty() {
|
||||
ui.available_width()
|
||||
} else {
|
||||
ui.available_width() - Root::SIDE_PANEL_MIN_WIDTH
|
||||
} as i64;
|
||||
max(min_width, available_width) as f32
|
||||
max(min_width, available_width as i64) as f32
|
||||
} else {
|
||||
let dual_panel_root = Root::is_dual_panel_mode(frame);
|
||||
if dual_panel_root {
|
||||
ui.available_width()
|
||||
available_width
|
||||
} else {
|
||||
frame.info().window_info.size.x
|
||||
}
|
||||
|
|
14
src/lib.rs
14
src/lib.rs
|
@ -27,17 +27,17 @@ use crate::node::Node;
|
|||
|
||||
i18n!("locales");
|
||||
|
||||
mod node;
|
||||
mod wallet;
|
||||
mod settings;
|
||||
|
||||
pub mod gui;
|
||||
|
||||
// Include build information.
|
||||
pub mod built_info {
|
||||
include!(concat!(env!("OUT_DIR"), "/built.rs"));
|
||||
}
|
||||
|
||||
mod node;
|
||||
mod wallet;
|
||||
pub mod gui;
|
||||
|
||||
mod settings;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[cfg(target_os = "android")]
|
||||
#[no_mangle]
|
||||
|
@ -100,7 +100,7 @@ pub fn start(mut options: eframe::NativeOptions, app_creator: eframe::AppCreator
|
|||
Node::start();
|
||||
}
|
||||
// Launch graphical interface.
|
||||
let _ = eframe::run_native("Grim", options, app_creator);
|
||||
eframe::run_native("Grim", options, app_creator).unwrap();
|
||||
}
|
||||
|
||||
/// Setup application [`egui::Style`] and [`egui::Visuals`].
|
||||
|
|
|
@ -12,8 +12,9 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::ffi::OsString;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use grin_core::global::ChainTypes;
|
||||
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use crate::{AppConfig, Settings};
|
||||
|
@ -22,8 +23,10 @@ use crate::wallet::WalletList;
|
|||
/// Wallet configuration.
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct WalletConfig {
|
||||
/// Chain type for current wallet.
|
||||
chain_type: ChainTypes,
|
||||
/// Identifier for a wallet.
|
||||
id: OsString,
|
||||
id: i64,
|
||||
/// Readable wallet name.
|
||||
name: String,
|
||||
/// External node connection URL.
|
||||
|
@ -35,13 +38,12 @@ const CONFIG_FILE_NAME: &'static str = "grim-wallet.toml";
|
|||
|
||||
impl WalletConfig {
|
||||
/// Create wallet config.
|
||||
pub fn create(id: OsString, name: String) -> WalletConfig {
|
||||
let config_path = Self::get_config_path(&id);
|
||||
let config = WalletConfig {
|
||||
id,
|
||||
name,
|
||||
external_node_url: None,
|
||||
};
|
||||
pub fn create(name: String, external_node_url: Option<String>) -> WalletConfig {
|
||||
let id = chrono::Utc::now().timestamp();
|
||||
let chain_type = AppConfig::chain_type();
|
||||
let config_path = Self::get_config_file_path(&chain_type, id);
|
||||
|
||||
let config = WalletConfig { chain_type, id, name, external_node_url };
|
||||
Settings::write_to_file(&config, config_path);
|
||||
config
|
||||
}
|
||||
|
@ -56,18 +58,29 @@ impl WalletConfig {
|
|||
None
|
||||
}
|
||||
|
||||
/// Get config file path for provided wallet identifier.
|
||||
fn get_config_path(id: &OsString) -> PathBuf {
|
||||
let chain_type = AppConfig::chain_type();
|
||||
let mut config_path = WalletList::get_wallets_base_dir(&chain_type);
|
||||
config_path.push(id);
|
||||
/// Get config file path for provided [`ChainTypes`] and wallet identifier.
|
||||
fn get_config_file_path(chain_type: &ChainTypes, id: i64) -> PathBuf {
|
||||
let mut config_path = WalletList::get_base_path(chain_type);
|
||||
config_path.push(id.to_string());
|
||||
// Create if the config path doesn't exist.
|
||||
if !config_path.exists() {
|
||||
let _ = fs::create_dir_all(config_path.clone());
|
||||
}
|
||||
config_path.push(CONFIG_FILE_NAME);
|
||||
config_path
|
||||
}
|
||||
|
||||
/// Get current wallet data path.
|
||||
pub fn get_data_path(&self) -> String {
|
||||
let chain_type = AppConfig::chain_type();
|
||||
let mut config_path = WalletList::get_base_path(&chain_type);
|
||||
config_path.push(self.id.to_string());
|
||||
config_path.to_str().unwrap().to_string()
|
||||
}
|
||||
|
||||
/// Save wallet config.
|
||||
fn save(&self) {
|
||||
let config_path = Self::get_config_path(&self.id);
|
||||
let config_path = Self::get_config_file_path(&self.chain_type, self.id);
|
||||
Settings::write_to_file(self, config_path);
|
||||
}
|
||||
|
||||
|
|
128
src/wallet/keys.rs
Normal file
128
src/wallet/keys.rs
Normal file
|
@ -0,0 +1,128 @@
|
|||
// 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(())
|
||||
}
|
|
@ -16,6 +16,7 @@ use std::fs;
|
|||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use grin_core::global::ChainTypes;
|
||||
use grin_wallet_libwallet::Error;
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
|
@ -24,43 +25,58 @@ use crate::wallet::Wallet;
|
|||
|
||||
lazy_static! {
|
||||
/// Global wallets state.
|
||||
static ref WALLETS_STATE: Arc<RwLock<WalletList >> = Arc::new(RwLock::new(WalletList::load()));
|
||||
static ref WALLETS_STATE: Arc<RwLock<WalletList>> = Arc::new(RwLock::new(WalletList::init()));
|
||||
}
|
||||
|
||||
/// List of created wallets.
|
||||
/// Wallets manager.
|
||||
pub struct WalletList {
|
||||
list: Vec<Wallet>
|
||||
pub(crate) list: Vec<Wallet>
|
||||
}
|
||||
|
||||
/// Base wallets directory name.
|
||||
pub const BASE_DIR_NAME: &'static str = "wallets";
|
||||
|
||||
impl WalletList {
|
||||
/// Load list of wallets.
|
||||
fn load() -> Self {
|
||||
/// Initialize manager by loading list of wallets into state.
|
||||
fn init() -> Self {
|
||||
Self { list: Self::load_wallets(&AppConfig::chain_type()) }
|
||||
}
|
||||
|
||||
/// Create new wallet and add it to state.
|
||||
pub fn create_wallet(
|
||||
name: String,
|
||||
password: String,
|
||||
mnemonic: String,
|
||||
external_node_url: Option<String>
|
||||
)-> Result<(), Error> {
|
||||
let wallet = Wallet::create(name, password, mnemonic, external_node_url)?;
|
||||
let mut w_state = WALLETS_STATE.write().unwrap();
|
||||
w_state.list.push(wallet);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Load wallets for provided [`ChainType`].
|
||||
fn load_wallets(chain_type: &ChainTypes) -> Vec<Wallet> {
|
||||
let mut wallets = Vec::new();
|
||||
let wallets_dir = Self::get_wallets_base_dir(chain_type);
|
||||
// Load wallets from directory.
|
||||
let wallets_dir = Self::get_base_path(chain_type);
|
||||
// Load wallets from base directory.
|
||||
for dir in wallets_dir.read_dir().unwrap() {
|
||||
let wallet = Wallet::load(dir.unwrap().path());
|
||||
if let Some(w) = wallet {
|
||||
wallets.push(w);
|
||||
let wallet_dir = dir.unwrap().path();
|
||||
if wallet_dir.is_dir() {
|
||||
let wallet = Wallet::init(wallet_dir);
|
||||
if let Some(w) = wallet {
|
||||
wallets.push(w);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
wallets
|
||||
}
|
||||
|
||||
/// Get wallets base directory for provided [`ChainTypes`].
|
||||
pub fn get_wallets_base_dir(chain_type: &ChainTypes) -> PathBuf {
|
||||
/// Get wallets base directory path for provided [`ChainTypes`].
|
||||
pub fn get_base_path(chain_type: &ChainTypes) -> PathBuf {
|
||||
let mut wallets_path = Settings::get_base_path(Some(chain_type.shortname()));
|
||||
wallets_path.push(BASE_DIR_NAME);
|
||||
// Create wallets directory if it doesn't exist.
|
||||
// Create wallets base directory if it doesn't exist.
|
||||
if !wallets_path.exists() {
|
||||
let _ = fs::create_dir_all(wallets_path.clone());
|
||||
}
|
||||
|
|
|
@ -12,6 +12,11 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
pub mod updater;
|
||||
pub mod selection;
|
||||
pub mod tx;
|
||||
pub mod keys;
|
||||
|
||||
mod wallet;
|
||||
pub use wallet::Wallet;
|
||||
|
||||
|
|
714
src/wallet/selection.rs
Normal file
714
src/wallet/selection.rs
Normal file
|
@ -0,0 +1,714 @@
|
|||
// 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(())
|
||||
}
|
547
src/wallet/tx.rs
Normal file
547
src/wallet/tx.rs
Normal file
|
@ -0,0 +1,547 @@
|
|||
// 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};
|
||||
|
||||
/// Static value to increment UUIDs of slates.
|
||||
lazy_static! {
|
||||
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(())
|
||||
}
|
837
src/wallet/updater.rs
Normal file
837
src/wallet/updater.rs
Normal file
|
@ -0,0 +1,837 @@
|
|||
// 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(())
|
||||
}
|
|
@ -12,51 +12,499 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::ffi::OsString;
|
||||
use std::path::PathBuf;
|
||||
use crate::node::NodeConfig;
|
||||
use std::sync::Arc;
|
||||
|
||||
use grin_core::global;
|
||||
use grin_keychain::{ExtKeychain, Identifier, Keychain};
|
||||
use grin_util::types::ZeroingString;
|
||||
use grin_wallet_api::{Foreign, ForeignCheckMiddlewareFn, Owner};
|
||||
use grin_wallet_impls::{DefaultLCProvider, DefaultWalletImpl, HTTPNodeClient};
|
||||
use grin_wallet_libwallet::{Error, NodeClient, NodeVersionInfo, OutputStatus, Slate, slate_versions, SlatepackArmor, Slatepacker, SlatepackerArgs, TxLogEntry, wallet_lock, WalletBackend, WalletInfo, WalletInst, WalletLCProvider};
|
||||
use grin_wallet_libwallet::Error::GenericError;
|
||||
use log::debug;
|
||||
use parking_lot::Mutex;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::AppConfig;
|
||||
use crate::node::NodeConfig;
|
||||
use crate::wallet::selection::lock_tx_context;
|
||||
use crate::wallet::tx::{add_inputs_to_slate, new_tx_slate};
|
||||
use crate::wallet::updater::{cancel_tx, refresh_output_state, retrieve_txs};
|
||||
use crate::wallet::WalletConfig;
|
||||
|
||||
/// Wallet loaded from config.
|
||||
/// Wallet instance and config wrapper.
|
||||
#[derive(Clone)]
|
||||
pub struct Wallet {
|
||||
/// Identifier for a wallet, name of wallet directory.
|
||||
id: OsString,
|
||||
/// Base path for wallet data.
|
||||
pub(crate) path: String,
|
||||
/// Loaded file config.
|
||||
/// Wallet instance, exists when wallet is open.
|
||||
instance: Option<WalletInstance>,
|
||||
/// Wallet data path.
|
||||
path: String,
|
||||
/// Wallet configuration.
|
||||
pub(crate) config: WalletConfig,
|
||||
}
|
||||
|
||||
impl Wallet {
|
||||
/// Create new wallet from provided name.
|
||||
pub fn create(name: String ) {
|
||||
/// Wallet instance type.
|
||||
type WalletInstance = Arc<
|
||||
Mutex<
|
||||
Box<
|
||||
dyn WalletInst<
|
||||
'static,
|
||||
DefaultLCProvider<'static, HTTPNodeClient, ExtKeychain>,
|
||||
HTTPNodeClient,
|
||||
ExtKeychain,
|
||||
>,
|
||||
>,
|
||||
>,
|
||||
>;
|
||||
|
||||
impl Wallet {
|
||||
/// Create and open new wallet.
|
||||
pub fn create(
|
||||
name: String,
|
||||
password: String,
|
||||
mnemonic: String,
|
||||
external_node_url: Option<String>
|
||||
) -> Result<Wallet, Error> {
|
||||
let config = WalletConfig::create(name.clone(), external_node_url);
|
||||
let wallet = Self::create_wallet_instance(config.clone())?;
|
||||
let mut w_lock = wallet.lock();
|
||||
let p = w_lock.lc_provider()?;
|
||||
|
||||
// Create wallet.
|
||||
p.create_wallet(None,
|
||||
Some(ZeroingString::from(mnemonic.clone())),
|
||||
mnemonic.len(),
|
||||
ZeroingString::from(password.clone()),
|
||||
false,
|
||||
)?;
|
||||
|
||||
// Open wallet.
|
||||
p.open_wallet(None, ZeroingString::from(password), false, false)?;
|
||||
|
||||
let w = Wallet {
|
||||
instance: Some(wallet.clone()),
|
||||
path: config.get_data_path(),
|
||||
config,
|
||||
};
|
||||
Ok(w)
|
||||
}
|
||||
|
||||
/// Load wallet from provided data path.
|
||||
pub fn load(data_path: PathBuf) -> Option<Wallet> {
|
||||
if !data_path.is_dir() {
|
||||
return None;
|
||||
}
|
||||
/// Initialize wallet from provided data path.
|
||||
pub fn init(data_path: PathBuf) -> Option<Wallet> {
|
||||
let wallet_config = WalletConfig::load(data_path.clone());
|
||||
if let Some(config) = wallet_config {
|
||||
// Set id as wallet directory name.
|
||||
let id = data_path.file_name().unwrap().to_os_string();
|
||||
let path = data_path.to_str().unwrap().to_string();
|
||||
return Some(Self { id, path, config });
|
||||
return Some(Self { instance: None, path, config });
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Get wallet node connection URL.
|
||||
pub fn get_connection_url(&self) -> String {
|
||||
match self.config.get_external_node_url() {
|
||||
None => {
|
||||
format!("http://{}", NodeConfig::get_api_address())
|
||||
/// Check if wallet is open (instance exists).
|
||||
pub fn is_open(&self) -> bool {
|
||||
self.instance.is_some()
|
||||
}
|
||||
|
||||
/// Create wallet instance from provided config.
|
||||
fn create_wallet_instance(config: WalletConfig) -> Result<WalletInstance, Error> {
|
||||
// Assume global chain type has already been initialized.
|
||||
let chain_type = AppConfig::chain_type();
|
||||
if !global::GLOBAL_CHAIN_TYPE.is_init() {
|
||||
global::init_global_chain_type(chain_type);
|
||||
} else {
|
||||
global::set_global_chain_type(chain_type);
|
||||
global::set_local_chain_type(chain_type);
|
||||
}
|
||||
|
||||
// Setup node client.
|
||||
let (node_api_url, node_secret) = if let Some(url) = config.get_external_node_url() {
|
||||
(url.to_string(), None)
|
||||
} else {
|
||||
(NodeConfig::get_api_address(), NodeConfig::get_api_secret())
|
||||
};
|
||||
let node_client = HTTPNodeClient::new(&node_api_url, node_secret)?;
|
||||
|
||||
// Create wallet instance.
|
||||
let wallet = Self::inst_wallet::<
|
||||
DefaultLCProvider<HTTPNodeClient, ExtKeychain>,
|
||||
HTTPNodeClient,
|
||||
ExtKeychain,
|
||||
>(config, node_client)?;
|
||||
Ok(wallet)
|
||||
}
|
||||
|
||||
/// Instantiate wallet from provided node client and config.
|
||||
fn inst_wallet<L, C, K>(
|
||||
config: WalletConfig,
|
||||
node_client: C,
|
||||
) -> Result<Arc<Mutex<Box<dyn WalletInst<'static, L, C, K>>>>, Error>
|
||||
where
|
||||
DefaultWalletImpl<'static, C>: WalletInst<'static, L, C, K>,
|
||||
L: WalletLCProvider<'static, C, K>,
|
||||
C: NodeClient + 'static,
|
||||
K: Keychain + 'static,
|
||||
{
|
||||
let mut wallet = Box::new(DefaultWalletImpl::<'static, C>::new(node_client).unwrap())
|
||||
as Box<dyn WalletInst<'static, L, C, K>>;
|
||||
let lc = wallet.lc_provider()?;
|
||||
lc.set_top_level_directory(config.get_data_path().as_str())?;
|
||||
Ok(Arc::new(Mutex::new(wallet)))
|
||||
}
|
||||
|
||||
/// Open wallet.
|
||||
pub fn open_wallet(&mut self, password: ZeroingString) -> Result<(), Error> {
|
||||
if let None = self.instance {
|
||||
let wallet = Self::create_wallet_instance(self.config.clone())?;
|
||||
let mut wallet_lock = wallet.lock();
|
||||
let lc = wallet_lock.lc_provider()?;
|
||||
lc.open_wallet(None, password, false, false)?;
|
||||
self.instance = Some(wallet.clone());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Close wallet.
|
||||
pub fn close_wallet(&self) -> Result<(), Error> {
|
||||
if let Some(wallet) = &self.instance {
|
||||
let mut wallet_lock = wallet.lock();
|
||||
let lc = wallet_lock.lc_provider()?;
|
||||
lc.close_wallet(None)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create transaction.
|
||||
fn tx_create(
|
||||
&self,
|
||||
amount: u64,
|
||||
minimum_confirmations: u64,
|
||||
selection_strategy_is_use_all: bool,
|
||||
) -> Result<(Vec<TxLogEntry>, String), Error> {
|
||||
let wallet = self.instance.clone().ok_or(GenericError("Wallet was not open".to_string()))?;
|
||||
let parent_key_id = {
|
||||
wallet_lock!(wallet.clone(), w);
|
||||
w.parent_key_id().clone()
|
||||
};
|
||||
|
||||
let slate = {
|
||||
wallet_lock!(wallet, w);
|
||||
let mut slate = new_tx_slate(&mut **w, amount, false, 2, false, None)?;
|
||||
let height = w.w2n_client().get_chain_tip()?.0;
|
||||
|
||||
let context = add_inputs_to_slate(
|
||||
&mut **w,
|
||||
None,
|
||||
&mut slate,
|
||||
height,
|
||||
minimum_confirmations,
|
||||
500,
|
||||
1,
|
||||
selection_strategy_is_use_all,
|
||||
&parent_key_id,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
)?;
|
||||
|
||||
{
|
||||
let mut batch = w.batch(None)?;
|
||||
batch.save_private_context(slate.id.as_bytes(), &context)?;
|
||||
batch.commit()?;
|
||||
}
|
||||
|
||||
lock_tx_context(&mut **w, None, &slate, height, &context, None)?;
|
||||
slate.compact()?;
|
||||
slate
|
||||
};
|
||||
|
||||
let packer = Slatepacker::new(SlatepackerArgs {
|
||||
sender: None, // sender
|
||||
recipients: vec![],
|
||||
dec_key: None,
|
||||
});
|
||||
let slatepack = packer.create_slatepack(&slate)?;
|
||||
let api = Owner::new(self.instance.clone().unwrap(), None);
|
||||
let txs = api.retrieve_txs(None, false, None, Some(slate.id), None)?;
|
||||
let result = (
|
||||
txs.1,
|
||||
SlatepackArmor::encode(&slatepack)?,
|
||||
);
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Callback to check slate compatibility at current node.
|
||||
fn check_middleware(
|
||||
name: ForeignCheckMiddlewareFn,
|
||||
node_version_info: Option<NodeVersionInfo>,
|
||||
slate: Option<&Slate>,
|
||||
) -> Result<(), Error> {
|
||||
match name {
|
||||
ForeignCheckMiddlewareFn::BuildCoinbase => Ok(()),
|
||||
_ => {
|
||||
let mut bhv = 3;
|
||||
if let Some(n) = node_version_info {
|
||||
bhv = n.block_header_version;
|
||||
}
|
||||
if let Some(s) = slate {
|
||||
if bhv > 4
|
||||
&& s.version_info.block_header_version
|
||||
< slate_versions::GRIN_BLOCK_HEADER_VERSION
|
||||
{
|
||||
Err(Error::Compatibility(
|
||||
"Incoming Slate is not compatible with this wallet. \
|
||||
Please upgrade the node or use a different one."
|
||||
.into(),
|
||||
))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Some(url) => url.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Receive transaction.
|
||||
fn tx_receive(
|
||||
&self,
|
||||
account: &str,
|
||||
slate_armored: &str
|
||||
) -> Result<(Vec<TxLogEntry>, String), Error> {
|
||||
let wallet = self.instance.clone().ok_or(GenericError("Wallet was not open".to_string()))?;
|
||||
let foreign_api = Foreign::new(wallet.clone(), None, Some(Self::check_middleware), false);
|
||||
let owner_api = Owner::new(wallet, None);
|
||||
|
||||
let mut slate =
|
||||
owner_api.slate_from_slatepack_message(None, slate_armored.to_owned(), vec![0])?;
|
||||
let slatepack = owner_api.decode_slatepack_message(None, slate_armored.to_owned(), vec![0])?;
|
||||
|
||||
let _ret_address = slatepack.sender;
|
||||
|
||||
slate = foreign_api.receive_tx(&slate, Some(&account), None)?;
|
||||
let txs = owner_api.retrieve_txs(None, false, None, Some(slate.id), None)?;
|
||||
let packer = Slatepacker::new(SlatepackerArgs {
|
||||
sender: None, // sender
|
||||
recipients: vec![],
|
||||
dec_key: None,
|
||||
});
|
||||
let slatepack = packer.create_slatepack(&slate)?;
|
||||
let result = (
|
||||
txs.1,
|
||||
SlatepackArmor::encode(&slatepack)?,
|
||||
);
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Cancel transaction.
|
||||
fn tx_cancel(&self, id: u32) -> Result<String, Error> {
|
||||
let wallet = self.instance.clone().ok_or(GenericError("Wallet was not open".to_string()))?;
|
||||
wallet_lock!(wallet, w);
|
||||
let parent_key_id = w.parent_key_id();
|
||||
cancel_tx(&mut **w, None, &parent_key_id, Some(id), None)?;
|
||||
Ok("".to_owned())
|
||||
}
|
||||
|
||||
/// Get transaction info.
|
||||
pub fn get_tx(&self, tx_slate_id: &str) -> Result<(bool, Vec<TxLogEntry>), Error> {
|
||||
let api = Owner::new(self.instance.clone().unwrap(), None);
|
||||
let uuid = Uuid::parse_str(tx_slate_id).unwrap();
|
||||
let txs = api.retrieve_txs(None, true, None, Some(uuid), None)?;
|
||||
Ok(txs)
|
||||
}
|
||||
|
||||
/// Finalize transaction.
|
||||
fn tx_finalize(&self, slate_armored: &str) -> Result<(bool, Vec<TxLogEntry>), Error> {
|
||||
let wallet = self.instance.clone().ok_or(GenericError("Wallet was not open".to_string()))?;
|
||||
let owner_api = Owner::new(wallet, None);
|
||||
let mut slate =
|
||||
owner_api.slate_from_slatepack_message(None, slate_armored.to_owned(), vec![0])?;
|
||||
let slatepack = owner_api.decode_slatepack_message(None, slate_armored.to_owned(), vec![0])?;
|
||||
|
||||
let _ret_address = slatepack.sender;
|
||||
|
||||
slate = owner_api.finalize_tx(None, &slate)?;
|
||||
let txs = owner_api.retrieve_txs(None, false, None, Some(slate.id), None)?;
|
||||
Ok(txs)
|
||||
}
|
||||
|
||||
/// Post transaction to node for broadcasting.
|
||||
fn tx_post(&self, tx_slate_id: &str) -> Result<(), Error> {
|
||||
let wallet = self.instance.clone().ok_or(GenericError("Wallet was not open".to_string()))?;
|
||||
let api = Owner::new(wallet, None);
|
||||
let tx_uuid = Uuid::parse_str(tx_slate_id).unwrap();
|
||||
let (_, txs) = api.retrieve_txs(None, true, None, Some(tx_uuid.clone()), None)?;
|
||||
if txs[0].confirmed {
|
||||
return Err(Error::from(GenericError(format!(
|
||||
"Transaction with id {} is already confirmed. Not posting.",
|
||||
tx_slate_id
|
||||
))));
|
||||
}
|
||||
let stored_tx = api.get_stored_tx(None, None, Some(&tx_uuid))?;
|
||||
match stored_tx {
|
||||
Some(stored_tx) => {
|
||||
api.post_tx(None, &stored_tx, true)?;
|
||||
Ok(())
|
||||
}
|
||||
None => Err(Error::from(GenericError(format!(
|
||||
"Transaction with id {} does not have transaction data. Not posting.",
|
||||
tx_slate_id
|
||||
)))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get transactions and base wallet info.
|
||||
pub fn get_txs_info(
|
||||
&self,
|
||||
minimum_confirmations: u64
|
||||
) -> Result<(bool, Vec<TxLogEntry>, WalletInfo), Error> {
|
||||
let wallet = self.instance.clone().ok_or(GenericError("Wallet was not open".to_string()))?;
|
||||
let refreshed = Self::update_state(wallet.clone()).unwrap_or(false);
|
||||
let wallet_info = {
|
||||
wallet_lock!(wallet, w);
|
||||
let parent_key_id = w.parent_key_id();
|
||||
Self::get_info(&mut **w, &parent_key_id, minimum_confirmations)?
|
||||
};
|
||||
let api = Owner::new(wallet, None);
|
||||
|
||||
let txs = api.retrieve_txs(None, false, None, None, None)?;
|
||||
Ok((refreshed, txs.1, wallet_info))
|
||||
}
|
||||
|
||||
/// Update wallet instance state.
|
||||
fn update_state<'a, L, C, K>(
|
||||
wallet_inst: Arc<Mutex<Box<dyn WalletInst<'a, L, C, K>>>>,
|
||||
) -> Result<bool, Error>
|
||||
where
|
||||
L: WalletLCProvider<'a, C, K>,
|
||||
C: NodeClient + 'a,
|
||||
K: Keychain + 'a,
|
||||
{
|
||||
let parent_key_id = {
|
||||
wallet_lock!(wallet_inst, w);
|
||||
w.parent_key_id().clone()
|
||||
};
|
||||
let mut client = {
|
||||
wallet_lock!(wallet_inst, w);
|
||||
w.w2n_client().clone()
|
||||
};
|
||||
let tip = client.get_chain_tip()?;
|
||||
|
||||
// Step1: Update outputs and transactions purely based on UTXO state.
|
||||
{
|
||||
wallet_lock!(wallet_inst, w);
|
||||
if !match refresh_output_state(&mut **w, None, tip.0, &parent_key_id, true) {
|
||||
Ok(_) => true,
|
||||
Err(_) => false,
|
||||
} {
|
||||
// We are unable to contact the node.
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
let mut txs = {
|
||||
wallet_lock!(wallet_inst, w);
|
||||
retrieve_txs(&mut **w, None, None, None, Some(&parent_key_id), true)?
|
||||
};
|
||||
|
||||
for tx in txs.iter_mut() {
|
||||
// Step 2: Cancel any transactions with an expired TTL.
|
||||
if let Some(e) = tx.ttl_cutoff_height {
|
||||
if tip.0 >= e {
|
||||
wallet_lock!(wallet_inst, w);
|
||||
let parent_key_id = w.parent_key_id();
|
||||
cancel_tx(&mut **w, None, &parent_key_id, Some(tx.id), None)?;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Step 3: Update outstanding transactions with no change outputs by kernel.
|
||||
if tx.confirmed {
|
||||
continue;
|
||||
}
|
||||
if tx.amount_debited != 0 && tx.amount_credited != 0 {
|
||||
continue;
|
||||
}
|
||||
if let Some(e) = tx.kernel_excess {
|
||||
let res = client.get_kernel(&e, tx.kernel_lookup_min_height, Some(tip.0));
|
||||
let kernel = match res {
|
||||
Ok(k) => k,
|
||||
Err(_) => return Ok(false),
|
||||
};
|
||||
if let Some(k) = kernel {
|
||||
debug!("Kernel Retrieved: {:?}", k);
|
||||
wallet_lock!(wallet_inst, w);
|
||||
let mut batch = w.batch(None)?;
|
||||
tx.confirmed = true;
|
||||
tx.update_confirmation_ts();
|
||||
batch.save_tx_log_entry(tx.clone(), &parent_key_id)?;
|
||||
batch.commit()?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
/// Get summary info about the wallet.
|
||||
pub fn get_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,
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue