This commit is contained in:
Victor Vobis 2025-09-11 22:42:44 +02:00
parent d8fd0dfb73
commit d9339d5c92
31 changed files with 2451 additions and 3182 deletions

1
lib/.gitignore vendored
View File

@ -1 +0,0 @@
/target

5166
native_client/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,18 @@
name = "native_client"
version = "0.1.0"
edition = "2024"
build = "build.rs"
[dependencies]
tauri = "=2.0.0-alpha.4"
tauri-egui = "0.3.0"
egui = "0.32.2"
eframe = { version = "0.32.2", default-features = false, features = [
"default_fonts", # Embed the default egui fonts.
"glow", # Use the glow rendering backend. Alternative: "wgpu".
"persistence", # Enable restoring app state when restarting the app.
"wayland", # To support Linux (and CI)
"x11", # To support older Linux distributions (restores one of the default features)
] }
serde = { version = "1.0.219", features = ["derive"] }
watchlet = { path = "../watchlet" }
hex = "0.4.3"
vlogger = { path = "../vlogger" }

6
native_client/build.rs Normal file
View File

@ -0,0 +1,6 @@
fn main() {
let dir = env!("CARGO_MANIFEST_DIR");
let data_path = format!("{dir}/data");
println!("cargo:rustc-env=DATA_DIR_PATH={}", data_path);
}

View File

@ -0,0 +1,91 @@
use std::sync::mpsc::{Receiver, RecvTimeoutError, Sender};
use std::time::Duration;
use vlogger::*;
use watchlet::{FileStorage, Wallet, WalletManager};
use crate::messages::error::SystemError;
use crate::messages::frontend::FrontendMessage;
use crate::messages::backend::BackendMessage;
use crate::constants::DATA_DIR_PATH;
#[derive(Debug)]
pub struct WalletState {
pub address: String,
pub balance: u64,
}
pub struct WalletService {
exit: bool,
receiver: Receiver<BackendMessage>,
sender: Sender<FrontendMessage>,
wallet_manager: WalletManager<FileStorage>,
}
impl WalletService {
pub fn new(
receiver: Receiver<BackendMessage>,
sender: Sender<FrontendMessage>,
) -> Self {
let wallet_path = format!("{}/wallet/wallet.age", DATA_DIR_PATH);
let storage = FileStorage::new(wallet_path.into());
let mut wallet_manager = WalletManager::new(storage);
wallet_manager.load_wallet("password".into()).unwrap();
Self {
exit: false,
receiver,
sender,
wallet_manager
}
}
pub fn wallet(&self) -> Option<&Wallet> {
self.wallet_manager.wallet()
}
pub fn update(&mut self) {
log!(DEBUG, "Dummy Update");
}
pub fn get_state(&self) -> Option<WalletState> {
if let Some(wallet) = self.wallet() {
let address = hex::encode(wallet.address());
let balance = wallet.balance();
Some(WalletState { address, balance })
} else {
None
}
}
pub fn handle_cmd(&mut self, cmd: BackendMessage) {
log!(DEBUG, "Received Command: {:#?}", cmd);
match cmd {
BackendMessage::StateRequest => {
let msg = match self.get_state() {
Some(state) => FrontendMessage::StateResponse(state),
None => FrontendMessage::Error(SystemError::WalletNotLoaded),
};
if let Err(e) = self.sender.send(msg) {
log!(ERROR, "Failed to send FrontendMessage: {e}");
}
},
BackendMessage::Transaction(_transaction) => {
},
BackendMessage::Shutdown => self.exit = true,
}
}
pub fn run(&mut self) {
while !self.exit {
match self.receiver.recv_timeout(Duration::from_secs(30)) {
Ok(cmd) => self.handle_cmd(cmd),
Err(e) => match e {
RecvTimeoutError::Timeout => {
self.update()
},
RecvTimeoutError::Disconnected => {},
}
}
}
}
}

View File

@ -0,0 +1 @@
pub const DATA_DIR_PATH: &str = env!("DATA_DIR_PATH");

View File

@ -0,0 +1,6 @@
use std::sync::mpsc::Sender;
use crate::messages::backend::BackendMessage;
pub trait DisplayPage {
fn show(&mut self, rx: &Sender<BackendMessage>, ctx: &egui::Context);
}

View File

View File

@ -0,0 +1,41 @@
use std::sync::mpsc::Sender;
use crate::{frontend::DisplayPage, messages::backend::BackendMessage};
#[derive(Default)]
#[derive(serde::Deserialize, serde::Serialize)]
pub struct HomePage {
label: String,
#[serde(skip)]
value: f32,
}
impl DisplayPage for HomePage {
fn show(&mut self, _rx: &Sender<BackendMessage>, ctx: &egui::Context) {
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("eframe template");
ui.horizontal(|ui| {
ui.label("Write something: ");
ui.text_edit_singleline(&mut self.label).request_focus();
});
ui.add(egui::Slider::new(&mut self.value, 0.0..=10.0).text("value"));
if ui.button("Increment").clicked() {
self.value += 1.0;
}
ui.separator();
ui.add(egui::github_link_file!(
"https://github.com/emilk/eframe_template/blob/main/",
"Source code."
));
ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| {
egui::warn_if_debug_build(ui);
});
});
}
}

View File

@ -0,0 +1,52 @@
use std::sync::mpsc::Sender;
use egui::{Align, Layout, RichText};
use crate::{frontend::DisplayPage, messages::backend::BackendMessage};
#[derive(Default)]
#[derive(serde::Serialize, serde::Deserialize)]
pub struct TransactionPage {
from_to_amount: [String; 3],
focused: usize,
}
#[derive(Debug)]
pub struct TransactionRequest {
}
const TX_FIELDS: &[&str] = &[
"From:",
"To:",
"Amount:",
];
impl DisplayPage for TransactionPage {
fn show(&mut self, tx: &Sender<BackendMessage>, ctx: &egui::Context) {
ctx.input(|input| {
if input.key_pressed(egui::Key::Tab) {
self.focused = (self.focused + 1) % TX_FIELDS.len()
}
});
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("Transaction Page");
ui.separator();
ui.with_layout(Layout::top_down_justified(Align::LEFT), |ui| {
for (i, f) in TX_FIELDS.iter().enumerate() {
ui.add_space(8.0);
ui.label(RichText::new(*f).size(14.0));
if i == self.focused {
ui.text_edit_singleline(&mut self.from_to_amount[i]).request_focus();
} else {
ui.text_edit_singleline(&mut self.from_to_amount[i]);
}
}
});
if ui.button("GetState").clicked() {
tx.send(BackendMessage::StateRequest).unwrap();
}
});
}
}

View File

@ -0,0 +1,119 @@
use egui::{Button, Color32, Key, RichText};
use crate::messages::{backend::BackendMessage, frontend::FrontendMessage};
use vlogger::*;
use super::{pages::HomePage, DisplayPage, TransactionPage};
use std::sync::mpsc::{self, Receiver, Sender};
#[derive(serde::Deserialize, serde::Serialize)]
#[serde(default)]
pub struct State {
page: Page,
#[serde(skip)]
receiver: Receiver<FrontendMessage>,
#[serde(skip)]
sender: Sender<BackendMessage>,
}
#[derive(serde::Deserialize, serde::Serialize)]
pub enum Page {
Home(HomePage),
Transaction(TransactionPage),
}
impl DisplayPage for Page {
fn show(&mut self, rx: &Sender<BackendMessage>, ctx: &egui::Context) {
match self {
Page::Home(home) => home.show(rx, ctx),
Page::Transaction(tx) => tx.show(rx, ctx),
}
}
}
impl Default for State {
fn default() -> Self {
let (_, front_rx) = mpsc::channel::<FrontendMessage>();
let (back_tx, _) = mpsc::channel::<BackendMessage>();
Self {
// Example stuff:
// page: Page::Home(HomePage::default()),
page: Page::Transaction(TransactionPage::default()),
receiver: front_rx,
sender: back_tx,
}
}
}
impl State {
pub fn new(
cc: &eframe::CreationContext<'_>,
receiver: Receiver<FrontendMessage>,
sender: Sender<BackendMessage>,
) -> Self {
if let Some(storage) = cc.storage {
let mut ret = eframe::get_value::<Self>(storage, eframe::APP_KEY).unwrap_or_default();
ret.receiver = receiver;
ret.sender = sender;
ret
} else {
Self {
receiver,
sender,
..Default::default()
}
}
}
}
impl eframe::App for State {
fn save(&mut self, storage: &mut dyn eframe::Storage) {
eframe::set_value(storage, eframe::APP_KEY, self);
}
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
if let Ok(msg) = self.receiver.try_recv() {
match msg {
FrontendMessage::StateResponse(state) => {
log!(DEBUG, "Recived: {:#?}", state);
}
FrontendMessage::Error(e) => {
log!(ERROR, "Received Error {e:?}")
}
}
}
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
ctx.input(|input| {
if input.key_pressed(Key::Escape) {
self.sender.send(BackendMessage::Shutdown).unwrap();
std::process::exit(1)
};
});
egui::MenuBar::new().ui(ui, |ui| {
egui::widgets::global_theme_preference_buttons(ui);
});
});
egui::SidePanel::left("menu panel").show(ctx, |ui| {
ui.heading("Navigation");
let available_width = ui.available_width();
if ui.add_sized(
[available_width, 24.0],
Button::new(RichText::new("Home").size(14.0))
.right_text("")
.fill(Color32::TRANSPARENT)
).clicked() {
self.page = Page::Home(HomePage::default())
}
if ui.add_sized(
[available_width, 24.0],
Button::new(RichText::new("Transaction").size(14.0))
.right_text("")
.fill(Color32::TRANSPARENT)
).clicked() {
self.page = Page::Transaction(TransactionPage::default())
}
});
self.page.show(&self.sender, ctx)
}
}

31
native_client/src/lib.rs Normal file
View File

@ -0,0 +1,31 @@
pub mod constants;
pub mod frontend {
pub mod state;
pub use state::*;
pub mod display;
pub use display::*;
pub mod message;
pub use pages::*;
pub mod pages {
pub mod tx_page;
pub use tx_page::*;
pub mod home;
pub use home::*;
}
}
pub mod messages {
pub mod frontend;
pub mod backend;
pub mod error;
}
pub mod backend {
pub mod wallet;
}

View File

@ -1,5 +1,37 @@
use tauri_egui;
#![warn(clippy::all, rust_2018_idioms)]
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
println!("Hello, world!");
use native_client::{backend::wallet::WalletService, frontend, messages::{backend::BackendMessage, frontend::FrontendMessage}};
use std::sync::mpsc::{self, Receiver, Sender};
fn dispatch_frontend(
receiver: Receiver<FrontendMessage>,
sender: Sender<BackendMessage>,
) -> eframe::Result {
let native_options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_inner_size([400.0, 300.0])
.with_min_inner_size([300.0, 220.0]),
..Default::default()
};
eframe::run_native(
"eframe template",
native_options,
Box::new(|cc| Ok(Box::new(frontend::State::new(cc, receiver, sender)))),
)
}
fn main() -> Result<(), std::io::Error> {
let (front_tx, front_rx) = mpsc::channel::<FrontendMessage>();
let (back_tx, back_rx) = mpsc::channel::<BackendMessage>();
let backend_handle = std::thread::spawn( || {
let mut wallet_service = WalletService::new(back_rx, front_tx);
wallet_service.run();
});
let _ = dispatch_frontend(front_rx, back_tx);
backend_handle.join().unwrap();
Ok(())
}

View File

@ -0,0 +1,8 @@
use crate::frontend::TransactionRequest;
#[derive(Debug)]
pub enum BackendMessage {
StateRequest,
Transaction(TransactionRequest),
Shutdown,
}

View File

@ -0,0 +1,4 @@
#[derive(Debug)]
pub enum SystemError {
WalletNotLoaded
}

View File

@ -0,0 +1,8 @@
use crate::backend::wallet::WalletState;
use super::error::SystemError;
pub enum FrontendMessage {
StateResponse(WalletState),
Error(SystemError)
}

View File

@ -14,7 +14,7 @@ thiserror = "2.0.16"
tokio = { version = "1.47.1", features = ["rt-multi-thread", "net", "sync", "time", "macros"] }
tokio-tungstenite = "0.27.0"
uuid = { version = "1.18.0", features = ["v4", "serde"] }
vlogger = { path = "../lib/logger-rs" }
vlogger = { path = "../vlogger" }
ratatui = "0.29.0"
crossterm = { version = "0.29.0", features = ["event-stream"] }
once_cell = "1.21.3"

View File

@ -1610,25 +1610,6 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "wallet"
version = "0.1.0"
dependencies = [
"age",
"bincode",
"getrandom",
"hex",
"js-sys",
"k256",
"serde",
"sha3",
"shared",
"thiserror 2.0.16",
"wasm-bindgen",
"wasm-bindgen-test",
"web-sys",
]
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
@ -1733,6 +1714,25 @@ dependencies = [
"syn",
]
[[package]]
name = "watchlet"
version = "0.1.0"
dependencies = [
"age",
"bincode",
"getrandom",
"hex",
"js-sys",
"k256",
"serde",
"sha3",
"shared",
"thiserror 2.0.16",
"wasm-bindgen",
"wasm-bindgen-test",
"web-sys",
]
[[package]]
name = "web-sys"
version = "0.3.78"

View File

@ -1,5 +1,5 @@
[package]
name = "wallet"
name = "watchlet"
version = "0.1.0"
edition = "2021"

View File

@ -134,6 +134,10 @@ impl Wallet {
self.nonce
}
pub fn balance(&self) -> u64 {
self.balance
}
pub fn public_key(&self) -> Result<VerifyingKey, WalletError> {
let pk = self.private_key()?;
Ok(*pk.verifying_key())

BIN
watchlet/wallet.age Normal file

Binary file not shown.