From 8fd89f1e006aadab767c76345c58c20e2cce1e0c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 28 Nov 2025 14:48:19 +0000 Subject: [PATCH 1/4] Initial plan From 0bdf94689bf786778dd2f2e879ef8e65b3721ea8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 28 Nov 2025 15:00:57 +0000 Subject: [PATCH 2/4] Add wallet CLI binary and update release workflow to include bitcell-wallet Co-authored-by: Steake <530040+Steake@users.noreply.github.com> --- .github/workflows/release.yml | 4 +- crates/bitcell-wallet/Cargo.toml | 7 ++ crates/bitcell-wallet/src/main.rs | 174 ++++++++++++++++++++++++++++++ 3 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 crates/bitcell-wallet/src/main.rs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5649f23..0d690ad 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -52,7 +52,7 @@ jobs: ${{ runner.os }}-${{ matrix.target }}-cargo- - name: Build release binaries - run: cargo build --release --target ${{ matrix.target }} -p bitcell-node -p bitcell-admin + run: cargo build --release --target ${{ matrix.target }} -p bitcell-node -p bitcell-admin -p bitcell-wallet - name: Create artifact directory shell: bash @@ -64,6 +64,7 @@ jobs: run: | cp target/${{ matrix.target }}/release/bitcell-node artifacts/ cp target/${{ matrix.target }}/release/bitcell-admin artifacts/ + cp target/${{ matrix.target }}/release/bitcell-wallet artifacts/ - name: Copy binaries (Windows) if: runner.os == 'Windows' @@ -71,6 +72,7 @@ jobs: run: | cp target/${{ matrix.target }}/release/bitcell-node.exe artifacts/ cp target/${{ matrix.target }}/release/bitcell-admin.exe artifacts/ + cp target/${{ matrix.target }}/release/bitcell-wallet.exe artifacts/ - name: Create archive (Unix) if: runner.os != 'Windows' diff --git a/crates/bitcell-wallet/Cargo.toml b/crates/bitcell-wallet/Cargo.toml index 53b8a5f..5c2c51d 100644 --- a/crates/bitcell-wallet/Cargo.toml +++ b/crates/bitcell-wallet/Cargo.toml @@ -8,6 +8,10 @@ license.workspace = true repository.workspace = true description = "Modular wallet for BitCell blockchain with multi-chain support" +[[bin]] +name = "bitcell-wallet" +path = "src/main.rs" + [dependencies] bitcell-crypto = { path = "../bitcell-crypto" } bitcell-state = { path = "../bitcell-state" } @@ -36,6 +40,9 @@ bincode.workspace = true # Error handling thiserror.workspace = true +# CLI +clap = { version = "4", features = ["derive"] } + # Utilities zeroize.workspace = true parking_lot.workspace = true diff --git a/crates/bitcell-wallet/src/main.rs b/crates/bitcell-wallet/src/main.rs new file mode 100644 index 0000000..c6f9ca7 --- /dev/null +++ b/crates/bitcell-wallet/src/main.rs @@ -0,0 +1,174 @@ +//! BitCell Wallet CLI +//! +//! Command-line interface for the BitCell wallet. + +use bitcell_wallet::{Chain, Mnemonic, Wallet, WalletConfig}; +use clap::{Parser, Subcommand}; + +#[derive(Parser)] +#[command(name = "bitcell-wallet")] +#[command(about = "BitCell blockchain wallet", long_about = None)] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + /// Create a new wallet with a fresh mnemonic + Create { + /// Wallet name + #[arg(short, long, default_value = "Default Wallet")] + name: String, + }, + /// Restore a wallet from a mnemonic phrase + Restore { + /// Mnemonic phrase (24 words) + #[arg(short, long)] + mnemonic: String, + /// Optional passphrase + #[arg(short, long, default_value = "")] + passphrase: String, + }, + /// Generate a new address + Address { + /// Chain to generate address for + #[arg(short, long, default_value = "bitcell")] + chain: String, + }, + /// Show wallet balance + Balance { + /// Chain to show balance for + #[arg(short, long)] + chain: Option, + }, + /// Show version information + Version, +} + +fn parse_chain(chain: &str) -> Result { + match chain.to_lowercase().as_str() { + "bitcell" | "cell" => Ok(Chain::BitCell), + "bitcoin" | "btc" => Ok(Chain::Bitcoin), + "bitcoin-testnet" | "btc-testnet" => Ok(Chain::BitcoinTestnet), + "ethereum" | "eth" => Ok(Chain::Ethereum), + "ethereum-sepolia" | "eth-sepolia" => Ok(Chain::EthereumSepolia), + _ => Err(format!("Unknown chain: {}", chain)), + } +} + +fn main() { + let cli = Cli::parse(); + + match cli.command { + Commands::Create { name } => { + println!("💰 BitCell Wallet"); + println!("================="); + println!(); + + let config = WalletConfig { + name: name.clone(), + ..WalletConfig::default() + }; + + let (wallet, mnemonic) = Wallet::create_new(config); + + println!("✅ Wallet '{}' created successfully!", name); + println!(); + println!("⚠️ IMPORTANT: Write down your recovery phrase and store it safely!"); + println!(" Anyone with this phrase can access your funds."); + println!(); + println!("Recovery Phrase ({} words):", mnemonic.word_count()); + println!("─────────────────────────────────────────────────────────"); + for (i, word) in mnemonic.words().iter().enumerate() { + print!("{:2}. {:<12}", i + 1, word); + if (i + 1) % 4 == 0 { + println!(); + } + } + println!("─────────────────────────────────────────────────────────"); + println!(); + + // Show generated addresses + println!("Generated Addresses:"); + for addr in wallet.all_addresses() { + println!(" {:?}: {}", addr.chain(), addr.to_string_formatted()); + } + } + Commands::Restore { + mnemonic, + passphrase, + } => { + println!("💰 BitCell Wallet - Restore"); + println!("==========================="); + println!(); + + match Mnemonic::from_phrase(&mnemonic) { + Ok(mnemonic) => { + let wallet = + Wallet::from_mnemonic(&mnemonic, &passphrase, WalletConfig::default()); + + println!("✅ Wallet restored successfully!"); + println!(); + println!("Generated Addresses:"); + for addr in wallet.all_addresses() { + println!(" {:?}: {}", addr.chain(), addr.to_string_formatted()); + } + } + Err(e) => { + eprintln!("❌ Error: Invalid mnemonic phrase - {}", e); + std::process::exit(1); + } + } + } + Commands::Address { chain } => { + match parse_chain(&chain) { + Ok(chain) => { + // For demo purposes, create a temporary wallet + let (mut wallet, _) = Wallet::create_new(WalletConfig::default()); + match wallet.next_address(chain) { + Ok(addr) => { + println!("New {:?} address: {}", chain, addr.to_string_formatted()); + } + Err(e) => { + eprintln!("❌ Error generating address: {}", e); + std::process::exit(1); + } + } + } + Err(e) => { + eprintln!("❌ Error: {}", e); + std::process::exit(1); + } + } + } + Commands::Balance { chain } => { + println!("💰 BitCell Wallet - Balance"); + println!("==========================="); + println!(); + + // For demo purposes, show zero balances + let chains = if let Some(chain_str) = chain { + match parse_chain(&chain_str) { + Ok(c) => vec![c], + Err(e) => { + eprintln!("❌ Error: {}", e); + std::process::exit(1); + } + } + } else { + vec![Chain::BitCell, Chain::Bitcoin, Chain::Ethereum] + }; + + for chain in chains { + println!("{:?}: 0.00", chain); + } + println!(); + println!("Note: Connect to a node to fetch actual balances."); + } + Commands::Version => { + println!("bitcell-wallet v0.1.0"); + println!("BitCell blockchain wallet"); + } + } +} From e4fed6578ac57022bdb52e023ddbb1c5ba875bd6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 28 Nov 2025 15:02:00 +0000 Subject: [PATCH 3/4] Use CARGO_PKG_VERSION macro for wallet version command Co-authored-by: Steake <530040+Steake@users.noreply.github.com> --- crates/bitcell-wallet/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bitcell-wallet/src/main.rs b/crates/bitcell-wallet/src/main.rs index c6f9ca7..e39cc2c 100644 --- a/crates/bitcell-wallet/src/main.rs +++ b/crates/bitcell-wallet/src/main.rs @@ -167,7 +167,7 @@ fn main() { println!("Note: Connect to a node to fetch actual balances."); } Commands::Version => { - println!("bitcell-wallet v0.1.0"); + println!("bitcell-wallet v{}", env!("CARGO_PKG_VERSION")); println!("BitCell blockchain wallet"); } } From 5ea92ea4fa94fbfab0f3375e18e4e108f704c275 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 28 Nov 2025 15:43:43 +0000 Subject: [PATCH 4/4] Add cross-platform native wallet GUI using Slint Co-authored-by: Steake <530040+Steake@users.noreply.github.com> --- .github/workflows/release.yml | 10 +- Cargo.toml | 1 + crates/bitcell-wallet-gui/Cargo.toml | 34 + crates/bitcell-wallet-gui/build.rs | 3 + crates/bitcell-wallet-gui/src/main.rs | 373 ++++++++ crates/bitcell-wallet-gui/ui/main.slint | 1098 +++++++++++++++++++++++ 6 files changed, 1518 insertions(+), 1 deletion(-) create mode 100644 crates/bitcell-wallet-gui/Cargo.toml create mode 100644 crates/bitcell-wallet-gui/build.rs create mode 100644 crates/bitcell-wallet-gui/src/main.rs create mode 100644 crates/bitcell-wallet-gui/ui/main.slint diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0d690ad..e76335c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -51,8 +51,14 @@ jobs: restore-keys: | ${{ runner.os }}-${{ matrix.target }}-cargo- + - name: Install Linux dependencies + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y libfontconfig1-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev + - name: Build release binaries - run: cargo build --release --target ${{ matrix.target }} -p bitcell-node -p bitcell-admin -p bitcell-wallet + run: cargo build --release --target ${{ matrix.target }} -p bitcell-node -p bitcell-admin -p bitcell-wallet -p bitcell-wallet-gui - name: Create artifact directory shell: bash @@ -65,6 +71,7 @@ jobs: cp target/${{ matrix.target }}/release/bitcell-node artifacts/ cp target/${{ matrix.target }}/release/bitcell-admin artifacts/ cp target/${{ matrix.target }}/release/bitcell-wallet artifacts/ + cp target/${{ matrix.target }}/release/bitcell-wallet-gui artifacts/ - name: Copy binaries (Windows) if: runner.os == 'Windows' @@ -73,6 +80,7 @@ jobs: cp target/${{ matrix.target }}/release/bitcell-node.exe artifacts/ cp target/${{ matrix.target }}/release/bitcell-admin.exe artifacts/ cp target/${{ matrix.target }}/release/bitcell-wallet.exe artifacts/ + cp target/${{ matrix.target }}/release/bitcell-wallet-gui.exe artifacts/ - name: Create archive (Unix) if: runner.os != 'Windows' diff --git a/Cargo.toml b/Cargo.toml index 8be7d13..0839832 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ members = [ "crates/bitcell-admin", "crates/bitcell-simulation", "crates/bitcell-wallet", + "crates/bitcell-wallet-gui", ] resolver = "2" diff --git a/crates/bitcell-wallet-gui/Cargo.toml b/crates/bitcell-wallet-gui/Cargo.toml new file mode 100644 index 0000000..c62dd6b --- /dev/null +++ b/crates/bitcell-wallet-gui/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "bitcell-wallet-gui" +version.workspace = true +authors.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +description = "Cross-platform native GUI for BitCell wallet using Slint" + +[[bin]] +name = "bitcell-wallet-gui" +path = "src/main.rs" + +[dependencies] +bitcell-wallet = { path = "../bitcell-wallet" } +bitcell-crypto = { path = "../bitcell-crypto" } + +# Slint UI framework - native rendering, no WebView +slint = "1.9" + +# Serialization +serde.workspace = true +serde_json = "1.0" + +# Error handling +thiserror.workspace = true +anyhow.workspace = true + +[build-dependencies] +slint-build = "1.9" + +[features] +default = [] diff --git a/crates/bitcell-wallet-gui/build.rs b/crates/bitcell-wallet-gui/build.rs new file mode 100644 index 0000000..51fb26a --- /dev/null +++ b/crates/bitcell-wallet-gui/build.rs @@ -0,0 +1,3 @@ +fn main() { + slint_build::compile("ui/main.slint").expect("Slint compile failed"); +} diff --git a/crates/bitcell-wallet-gui/src/main.rs b/crates/bitcell-wallet-gui/src/main.rs new file mode 100644 index 0000000..40eadae --- /dev/null +++ b/crates/bitcell-wallet-gui/src/main.rs @@ -0,0 +1,373 @@ +//! BitCell Wallet GUI +//! +//! Cross-platform native GUI for the BitCell wallet using Slint. +//! Targets: macOS, Linux, Windows +//! Features: 60fps smooth interactions, accessibility support, no WebView + +use bitcell_wallet::{Chain, Mnemonic, Wallet, WalletConfig}; +use std::cell::RefCell; +use std::rc::Rc; + +slint::include_modules!(); + +/// Wallet application state +struct AppState { + wallet: Option, + mnemonic: Option, +} + +impl AppState { + fn new() -> Self { + Self { + wallet: None, + mnemonic: None, + } + } +} + +/// Convert chain string to Chain enum +fn parse_chain(chain: &str) -> Chain { + match chain.to_lowercase().as_str() { + "bitcoin" | "btc" => Chain::Bitcoin, + "ethereum" | "eth" => Chain::Ethereum, + _ => Chain::BitCell, + } +} + +/// Format chain for display +fn chain_display_name(chain: Chain) -> &'static str { + match chain { + Chain::BitCell => "BitCell", + Chain::Bitcoin => "Bitcoin", + Chain::BitcoinTestnet => "Bitcoin Testnet", + Chain::Ethereum => "Ethereum", + Chain::EthereumSepolia => "Ethereum Sepolia", + Chain::Custom(_) => "Custom", + } +} + +fn main() -> Result<(), slint::PlatformError> { + // Create the main window + let main_window = MainWindow::new()?; + + // Create shared application state + let state = Rc::new(RefCell::new(AppState::new())); + + // Get global wallet state handle + let wallet_state = main_window.global::(); + + // Setup callback handlers + setup_callbacks(&main_window, state.clone()); + + // Initialize with welcome view + wallet_state.set_current_tab(0); + wallet_state.set_wallet_exists(false); + wallet_state.set_wallet_locked(true); + + // Run the event loop + main_window.run() +} + +/// Setup all callback handlers for the UI +fn setup_callbacks(window: &MainWindow, state: Rc>) { + let wallet_state = window.global::(); + + // Create wallet callback + { + let state = state.clone(); + let window_weak = window.as_weak(); + + wallet_state.on_create_wallet(move || { + let window = window_weak.unwrap(); + let wallet_state = window.global::(); + + // Create new wallet + let config = WalletConfig { + name: "BitCell Wallet".to_string(), + ..WalletConfig::default() + }; + + let (wallet, mnemonic) = Wallet::create_new(config); + + // Format mnemonic for display + let mnemonic_words: Vec<&str> = mnemonic.words(); + let mnemonic_display = mnemonic_words + .chunks(4) + .enumerate() + .map(|(row, chunk)| { + chunk + .iter() + .enumerate() + .map(|(col, word)| format!("{}. {}", row * 4 + col + 1, word)) + .collect::>() + .join(" ") + }) + .collect::>() + .join("\n"); + + // Update state + { + let mut app_state = state.borrow_mut(); + app_state.wallet = Some(wallet); + app_state.mnemonic = Some(mnemonic); + } + + // Update UI + wallet_state.set_mnemonic_display(mnemonic_display.into()); + wallet_state.set_show_mnemonic(true); + wallet_state.set_current_tab(2); + wallet_state.set_wallet_exists(true); + wallet_state.set_wallet_locked(false); + wallet_state.set_status_message("Wallet created successfully!".into()); + + // Update addresses + update_addresses(&wallet_state, &state); + }); + } + + // Restore wallet callback + { + let state = state.clone(); + let window_weak = window.as_weak(); + + wallet_state.on_restore_wallet(move |mnemonic_str, passphrase| { + let window = window_weak.unwrap(); + let wallet_state = window.global::(); + + match Mnemonic::from_phrase(&mnemonic_str) { + Ok(mnemonic) => { + let wallet = Wallet::from_mnemonic( + &mnemonic, + &passphrase, + WalletConfig::default(), + ); + + // Update state + { + let mut app_state = state.borrow_mut(); + app_state.wallet = Some(wallet); + app_state.mnemonic = Some(mnemonic); + } + + // Update UI + wallet_state.set_wallet_exists(true); + wallet_state.set_wallet_locked(false); + wallet_state.set_current_tab(3); + wallet_state.set_status_message("Wallet restored successfully!".into()); + + // Update addresses + update_addresses(&wallet_state, &state); + } + Err(e) => { + wallet_state.set_status_message(format!("Error: {}", e).into()); + } + } + }); + } + + // Lock wallet callback + { + let state = state.clone(); + let window_weak = window.as_weak(); + + wallet_state.on_lock_wallet(move || { + let window = window_weak.unwrap(); + let wallet_state = window.global::(); + + if let Some(ref mut wallet) = state.borrow_mut().wallet { + wallet.lock(); + } + + wallet_state.set_wallet_locked(true); + wallet_state.set_status_message("Wallet locked".into()); + }); + } + + // Unlock wallet callback + { + let state = state.clone(); + let window_weak = window.as_weak(); + + wallet_state.on_unlock_wallet(move |passphrase| { + let window = window_weak.unwrap(); + let wallet_state = window.global::(); + + let mut app_state = state.borrow_mut(); + + // Clone mnemonic to avoid borrowing issues + let mnemonic_clone = app_state.mnemonic.clone(); + + if let (Some(ref mut wallet), Some(ref mnemonic)) = + (&mut app_state.wallet, &mnemonic_clone) + { + match wallet.unlock(mnemonic, &passphrase) { + Ok(()) => { + wallet_state.set_wallet_locked(false); + wallet_state.set_current_tab(3); + wallet_state.set_status_message("Wallet unlocked".into()); + } + Err(e) => { + wallet_state.set_status_message(format!("Error: {}", e).into()); + } + } + } + }); + } + + // Generate address callback + { + let state = state.clone(); + let window_weak = window.as_weak(); + + wallet_state.on_generate_address(move |chain_str| { + let window = window_weak.unwrap(); + let wallet_state = window.global::(); + + let chain = parse_chain(&chain_str); + + if let Some(ref mut wallet) = state.borrow_mut().wallet { + match wallet.next_address(chain) { + Ok(_addr) => { + wallet_state.set_status_message( + format!("New {} address generated", chain_display_name(chain)).into() + ); + update_addresses(&wallet_state, &state); + } + Err(e) => { + wallet_state.set_status_message(format!("Error: {}", e).into()); + } + } + } + }); + } + + // Send transaction callback + { + let _state = state.clone(); + let window_weak = window.as_weak(); + + wallet_state.on_send_transaction(move |to_address, amount, chain_str| { + let window = window_weak.unwrap(); + let wallet_state = window.global::(); + + // Parse amount + let amount: f64 = amount.parse().unwrap_or(0.0); + if amount <= 0.0 { + wallet_state.set_status_message("Invalid amount".into()); + return; + } + + if to_address.is_empty() { + wallet_state.set_status_message("Invalid recipient address".into()); + return; + } + + // TODO: Implement actual transaction sending + // This is a placeholder that will be implemented when network integration is complete + // For now, show a message indicating the feature is not yet available + + wallet_state.set_status_message( + format!("Transaction prepared (offline): {} {} to {} - Connect to node to broadcast", + amount, chain_str, + if to_address.len() > 16 { + format!("{}...", &to_address[..16]) + } else { + to_address.to_string() + } + ).into() + ); + wallet_state.set_current_tab(3); + }); + } + + // Refresh balances callback + { + let window_weak = window.as_weak(); + + wallet_state.on_refresh_balances(move || { + let window = window_weak.unwrap(); + let wallet_state = window.global::(); + + // In a real implementation, this would fetch balances from nodes + wallet_state.set_status_message("Balances refreshed".into()); + }); + } + + // Copy to clipboard callback + { + let window_weak = window.as_weak(); + + wallet_state.on_copy_to_clipboard(move |text| { + let window = window_weak.unwrap(); + let wallet_state = window.global::(); + + // Platform-specific clipboard handling + #[cfg(target_os = "linux")] + { + if let Ok(mut child) = std::process::Command::new("xclip") + .args(["-selection", "clipboard"]) + .stdin(std::process::Stdio::piped()) + .spawn() + { + use std::io::Write; + if let Some(ref mut stdin) = child.stdin { + let _ = stdin.write_all(text.as_bytes()); + } + } + } + + #[cfg(target_os = "macos")] + { + if let Ok(mut child) = std::process::Command::new("pbcopy") + .stdin(std::process::Stdio::piped()) + .spawn() + { + use std::io::Write; + if let Some(ref mut stdin) = child.stdin { + let _ = stdin.write_all(text.as_bytes()); + } + } + } + + #[cfg(target_os = "windows")] + { + // Windows clipboard via PowerShell using stdin to avoid injection + if let Ok(mut child) = std::process::Command::new("powershell") + .args(["-Command", "Set-Clipboard -Value $input"]) + .stdin(std::process::Stdio::piped()) + .spawn() + { + use std::io::Write; + if let Some(ref mut stdin) = child.stdin { + let _ = stdin.write_all(text.as_bytes()); + } + } + } + + wallet_state.set_status_message("Copied to clipboard".into()); + }); + } +} + +/// Update addresses in the UI from wallet state +fn update_addresses(wallet_state: &WalletState, state: &Rc>) { + let app_state = state.borrow(); + + if let Some(ref wallet) = app_state.wallet { + let addresses: Vec = wallet + .all_addresses() + .iter() + .map(|addr| { + let balance = wallet.get_balance(addr); + WalletAddress { + chain: chain_display_name(addr.chain()).into(), + address: addr.to_string_formatted().into(), + balance: format!("{:.8}", balance.amount() as f64 / 100_000_000.0).into(), + } + }) + .collect(); + + let model = std::rc::Rc::new(slint::VecModel::from(addresses)); + wallet_state.set_addresses(model.into()); + } +} diff --git a/crates/bitcell-wallet-gui/ui/main.slint b/crates/bitcell-wallet-gui/ui/main.slint new file mode 100644 index 0000000..fbfbba2 --- /dev/null +++ b/crates/bitcell-wallet-gui/ui/main.slint @@ -0,0 +1,1098 @@ +// BitCell Wallet GUI - Main UI +// Cross-platform native GUI using Slint +// Targets: macOS, Linux, Windows +// Features: 60fps smooth interactions, accessibility support + +import { Button, LineEdit, ComboBox, ScrollView, VerticalBox, HorizontalBox, GridBox, TabWidget, Palette, StyleMetrics } from "std-widgets.slint"; + +// Custom color scheme for BitCell brand +global Theme { + // Brand colors + in-out property primary: #6366f1; + in-out property primary-hover: #4f46e5; + in-out property secondary: #10b981; + in-out property accent: #f59e0b; + + // Background colors + in-out property background: #0f172a; + in-out property surface: #1e293b; + in-out property surface-elevated: #334155; + + // Text colors + in-out property text-primary: #f8fafc; + in-out property text-secondary: #94a3b8; + in-out property text-muted: #64748b; + + // Status colors + in-out property success: #22c55e; + in-out property warning: #eab308; + in-out property error: #ef4444; + + // Spacing + in-out property spacing-xs: 4px; + in-out property spacing-sm: 8px; + in-out property spacing-md: 16px; + in-out property spacing-lg: 24px; + in-out property spacing-xl: 32px; + + // Border radius + in-out property radius-sm: 4px; + in-out property radius-md: 8px; + in-out property radius-lg: 12px; + in-out property radius-xl: 16px; +} + +// Wallet data model +struct WalletAddress { + chain: string, + address: string, + balance: string, +} + +struct TransactionItem { + tx-hash: string, + direction: string, + amount: string, + timestamp: string, + status: string, +} + +// Global wallet state +export global WalletState { + // Wallet info + in-out property wallet-name: "BitCell Wallet"; + in-out property wallet-locked: true; + in-out property wallet-exists: false; + + // Current view + in-out property current-tab: 0; + + // Balances + in-out property total-balance-usd: "$0.00"; + in-out property bitcell-balance: "0.00 CELL"; + in-out property bitcoin-balance: "0.00 BTC"; + in-out property ethereum-balance: "0.00 ETH"; + + // Addresses + in-out property <[WalletAddress]> addresses: []; + + // Transactions + in-out property <[TransactionItem]> transactions: []; + + // Mnemonic display + in-out property mnemonic-display: ""; + in-out property show-mnemonic: false; + + // Input fields + in-out property restore-mnemonic: ""; + in-out property restore-passphrase: ""; + in-out property send-to-address: ""; + in-out property send-amount: ""; + in-out property selected-chain: "BitCell"; + + // Status + in-out property status-message: ""; + in-out property is-loading: false; + + // Callbacks for Rust backend + callback create-wallet(); + callback restore-wallet(string, string); + callback lock-wallet(); + callback unlock-wallet(string); + callback generate-address(string); + callback send-transaction(string, string, string); + callback refresh-balances(); + callback copy-to-clipboard(string); +} + +// Logo component +component BitCellLogo inherits Rectangle { + width: 48px; + height: 48px; + border-radius: Theme.radius-lg; + background: Theme.primary; + + Text { + text: "₿C"; + font-size: 20px; + font-weight: 700; + color: white; + horizontal-alignment: center; + vertical-alignment: center; + } +} + +// Primary button component +component PrimaryButton inherits Rectangle { + in property text; + in property enabled: true; + callback clicked; + + height: 44px; + min-width: 120px; + border-radius: Theme.radius-md; + background: enabled ? (touch.has-hover ? Theme.primary-hover : Theme.primary) : Theme.surface-elevated; + + animate background { duration: 150ms; easing: ease-out; } + + touch := TouchArea { + enabled: root.enabled; + clicked => { root.clicked(); } + } + + Text { + text: root.text; + font-size: 14px; + font-weight: 600; + color: root.enabled ? white : Theme.text-muted; + horizontal-alignment: center; + vertical-alignment: center; + } +} + +// Secondary button component +component SecondaryButton inherits Rectangle { + in property text; + in property enabled: true; + callback clicked; + + height: 44px; + min-width: 120px; + border-radius: Theme.radius-md; + border-width: 1px; + border-color: Theme.primary; + background: touch.has-hover ? Theme.surface-elevated : transparent; + + animate background { duration: 150ms; easing: ease-out; } + + touch := TouchArea { + enabled: root.enabled; + clicked => { root.clicked(); } + } + + Text { + text: root.text; + font-size: 14px; + font-weight: 600; + color: root.enabled ? Theme.primary : Theme.text-muted; + horizontal-alignment: center; + vertical-alignment: center; + } +} + +// Card component +component Card inherits Rectangle { + in property title: ""; + + border-radius: Theme.radius-lg; + background: Theme.surface; + + VerticalLayout { + padding: Theme.spacing-lg; + spacing: Theme.spacing-md; + + if root.title != "" : Text { + text: root.title; + font-size: 18px; + font-weight: 600; + color: Theme.text-primary; + } + + @children + } +} + +// Balance card component +component BalanceCard inherits Rectangle { + in property chain-name; + in property balance; + in property chain-color: Theme.primary; + + height: 80px; + border-radius: Theme.radius-lg; + background: Theme.surface; + + HorizontalLayout { + padding: Theme.spacing-md; + spacing: Theme.spacing-md; + alignment: start; + + Rectangle { + width: 48px; + height: 48px; + border-radius: Theme.radius-md; + background: root.chain-color; + + Text { + text: "•"; + font-size: 24px; + font-weight: 700; + color: white; + horizontal-alignment: center; + vertical-alignment: center; + } + } + + VerticalLayout { + alignment: center; + spacing: Theme.spacing-xs; + + Text { + text: root.chain-name; + font-size: 14px; + color: Theme.text-secondary; + } + + Text { + text: root.balance; + font-size: 18px; + font-weight: 600; + color: Theme.text-primary; + } + } + } +} + +// Address row component +component AddressRow inherits Rectangle { + in property addr; + + height: 60px; + border-radius: Theme.radius-md; + background: touch.has-hover ? Theme.surface-elevated : Theme.surface; + + animate background { duration: 100ms; } + + touch := TouchArea { + clicked => { WalletState.copy-to-clipboard(root.addr.address); } + } + + HorizontalLayout { + padding-left: Theme.spacing-md; + padding-right: Theme.spacing-md; + spacing: Theme.spacing-md; + alignment: space-between; + + HorizontalLayout { + spacing: Theme.spacing-md; + alignment: start; + + Text { + text: root.addr.chain; + font-size: 14px; + font-weight: 600; + color: Theme.primary; + vertical-alignment: center; + } + + Text { + text: root.addr.address; + font-size: 12px; + font-family: "monospace"; + color: Theme.text-secondary; + vertical-alignment: center; + overflow: elide; + } + } + + Text { + text: root.addr.balance; + font-size: 14px; + font-weight: 500; + color: Theme.text-primary; + vertical-alignment: center; + } + } +} + +// Transaction row component +component TransactionRow inherits Rectangle { + in property tx; + + height: 64px; + border-radius: Theme.radius-md; + background: touch.has-hover ? Theme.surface-elevated : Theme.surface; + + animate background { duration: 100ms; } + + touch := TouchArea { + clicked => { WalletState.copy-to-clipboard(root.tx.tx-hash); } + } + + HorizontalLayout { + padding-left: Theme.spacing-md; + padding-right: Theme.spacing-md; + spacing: Theme.spacing-md; + alignment: space-between; + + HorizontalLayout { + spacing: Theme.spacing-md; + alignment: start; + + Rectangle { + width: 32px; + height: 32px; + border-radius: 16px; + background: root.tx.direction == "received" ? Theme.success : Theme.warning; + + Text { + text: root.tx.direction == "received" ? "↓" : "↑"; + font-size: 16px; + color: white; + horizontal-alignment: center; + vertical-alignment: center; + } + } + + VerticalLayout { + alignment: center; + spacing: 2px; + + Text { + text: root.tx.direction == "received" ? "Received" : "Sent"; + font-size: 14px; + font-weight: 500; + color: Theme.text-primary; + } + + Text { + text: root.tx.timestamp; + font-size: 12px; + color: Theme.text-muted; + } + } + } + + VerticalLayout { + alignment: center; + spacing: 2px; + + Text { + text: root.tx.amount; + font-size: 14px; + font-weight: 600; + color: root.tx.direction == "received" ? Theme.success : Theme.text-primary; + horizontal-alignment: right; + } + + Text { + text: root.tx.status; + font-size: 12px; + color: Theme.text-muted; + horizontal-alignment: right; + } + } + } +} + +// Welcome/Setup view +component WelcomeView inherits Rectangle { + background: Theme.background; + + VerticalLayout { + alignment: center; + spacing: Theme.spacing-xl; + padding: Theme.spacing-xl; + + // Logo and title + VerticalLayout { + alignment: center; + spacing: Theme.spacing-md; + + BitCellLogo { + width: 80px; + height: 80px; + } + + Text { + text: "BitCell Wallet"; + font-size: 32px; + font-weight: 700; + color: Theme.text-primary; + horizontal-alignment: center; + } + + Text { + text: "Secure, cross-platform cryptocurrency wallet"; + font-size: 16px; + color: Theme.text-secondary; + horizontal-alignment: center; + } + } + + // Action buttons + VerticalLayout { + alignment: center; + spacing: Theme.spacing-md; + + PrimaryButton { + text: "Create New Wallet"; + width: 240px; + clicked => { WalletState.create-wallet(); } + } + + SecondaryButton { + text: "Restore Wallet"; + width: 240px; + clicked => { WalletState.current-tab = 1; } + } + } + + // Version info + Text { + text: "v0.1.0 - Native • Secure • Open Source"; + font-size: 12px; + color: Theme.text-muted; + horizontal-alignment: center; + } + } +} + +// Restore wallet view +component RestoreView inherits Rectangle { + background: Theme.background; + + VerticalLayout { + alignment: center; + spacing: Theme.spacing-lg; + padding: Theme.spacing-xl; + + Text { + text: "Restore Wallet"; + font-size: 24px; + font-weight: 700; + color: Theme.text-primary; + horizontal-alignment: center; + } + + Text { + text: "Enter your 24-word recovery phrase"; + font-size: 14px; + color: Theme.text-secondary; + horizontal-alignment: center; + } + + Card { + width: 480px; + + VerticalLayout { + spacing: Theme.spacing-md; + + Text { + text: "Recovery Phrase"; + font-size: 14px; + font-weight: 500; + color: Theme.text-secondary; + } + + LineEdit { + placeholder-text: "Enter your 24-word mnemonic phrase..."; + text <=> WalletState.restore-mnemonic; + } + + Text { + text: "Passphrase (optional)"; + font-size: 14px; + font-weight: 500; + color: Theme.text-secondary; + } + + LineEdit { + placeholder-text: "Enter passphrase if you used one..."; + text <=> WalletState.restore-passphrase; + input-type: password; + } + } + } + + HorizontalLayout { + alignment: center; + spacing: Theme.spacing-md; + + SecondaryButton { + text: "Back"; + clicked => { WalletState.current-tab = 0; } + } + + PrimaryButton { + text: "Restore"; + clicked => { + WalletState.restore-wallet( + WalletState.restore-mnemonic, + WalletState.restore-passphrase + ); + } + } + } + } +} + +// Mnemonic display view +component MnemonicView inherits Rectangle { + background: Theme.background; + + VerticalLayout { + alignment: center; + spacing: Theme.spacing-lg; + padding: Theme.spacing-xl; + + Text { + text: "⚠️ Save Your Recovery Phrase"; + font-size: 24px; + font-weight: 700; + color: Theme.warning; + horizontal-alignment: center; + } + + Text { + text: "Write down these 24 words and store them safely.\nAnyone with this phrase can access your funds."; + font-size: 14px; + color: Theme.text-secondary; + horizontal-alignment: center; + } + + Card { + width: 520px; + + Rectangle { + background: Theme.surface-elevated; + border-radius: Theme.radius-md; + + Text { + text: WalletState.mnemonic-display; + font-size: 14px; + font-family: "monospace"; + color: Theme.text-primary; + wrap: word-wrap; + horizontal-alignment: center; + vertical-alignment: center; + } + + height: 120px; + } + } + + HorizontalLayout { + alignment: center; + spacing: Theme.spacing-md; + + SecondaryButton { + text: "Copy to Clipboard"; + clicked => { WalletState.copy-to-clipboard(WalletState.mnemonic-display); } + } + + PrimaryButton { + text: "I've Saved It"; + clicked => { + WalletState.show-mnemonic = false; + WalletState.current-tab = 3; + } + } + } + } +} + +// Main dashboard view +component DashboardView inherits Rectangle { + background: Theme.background; + + VerticalLayout { + padding: Theme.spacing-lg; + spacing: Theme.spacing-lg; + + // Header + HorizontalLayout { + alignment: space-between; + + HorizontalLayout { + spacing: Theme.spacing-md; + + BitCellLogo { } + + VerticalLayout { + alignment: center; + + Text { + text: WalletState.wallet-name; + font-size: 20px; + font-weight: 600; + color: Theme.text-primary; + } + + Text { + text: WalletState.wallet-locked ? "🔒 Locked" : "🔓 Unlocked"; + font-size: 12px; + color: Theme.text-secondary; + } + } + } + + HorizontalLayout { + spacing: Theme.spacing-sm; + + SecondaryButton { + text: "Refresh"; + width: 100px; + clicked => { WalletState.refresh-balances(); } + } + + SecondaryButton { + text: WalletState.wallet-locked ? "Unlock" : "Lock"; + width: 100px; + clicked => { + if WalletState.wallet-locked { + WalletState.current-tab = 5; + } else { + WalletState.lock-wallet(); + } + } + } + } + } + + // Total balance + Card { + VerticalLayout { + spacing: Theme.spacing-sm; + + Text { + text: "Total Balance"; + font-size: 14px; + color: Theme.text-secondary; + } + + Text { + text: WalletState.total-balance-usd; + font-size: 36px; + font-weight: 700; + color: Theme.text-primary; + } + } + } + + // Balance cards + HorizontalLayout { + spacing: Theme.spacing-md; + + BalanceCard { + horizontal-stretch: 1; + chain-name: "BitCell"; + balance: WalletState.bitcell-balance; + chain-color: Theme.primary; + } + + BalanceCard { + horizontal-stretch: 1; + chain-name: "Bitcoin"; + balance: WalletState.bitcoin-balance; + chain-color: #f7931a; + } + + BalanceCard { + horizontal-stretch: 1; + chain-name: "Ethereum"; + balance: WalletState.ethereum-balance; + chain-color: #627eea; + } + } + + // Quick actions + HorizontalLayout { + spacing: Theme.spacing-md; + + PrimaryButton { + text: "Send"; + horizontal-stretch: 1; + clicked => { WalletState.current-tab = 4; } + } + + SecondaryButton { + text: "Receive"; + horizontal-stretch: 1; + clicked => { WalletState.current-tab = 6; } + } + + SecondaryButton { + text: "History"; + horizontal-stretch: 1; + clicked => { WalletState.current-tab = 7; } + } + } + + // Recent transactions + Card { + title: "Recent Activity"; + vertical-stretch: 1; + + if WalletState.transactions.length == 0 : VerticalLayout { + alignment: center; + + Text { + text: "No transactions yet"; + font-size: 14px; + color: Theme.text-muted; + horizontal-alignment: center; + } + } + + if WalletState.transactions.length > 0 : ScrollView { + VerticalLayout { + spacing: Theme.spacing-sm; + + for tx in WalletState.transactions : TransactionRow { + tx: tx; + } + } + } + } + } +} + +// Send transaction view +component SendView inherits Rectangle { + background: Theme.background; + + VerticalLayout { + alignment: center; + spacing: Theme.spacing-lg; + padding: Theme.spacing-xl; + + Text { + text: "Send Crypto"; + font-size: 24px; + font-weight: 700; + color: Theme.text-primary; + horizontal-alignment: center; + } + + Card { + width: 480px; + + VerticalLayout { + spacing: Theme.spacing-md; + + Text { + text: "Chain"; + font-size: 14px; + font-weight: 500; + color: Theme.text-secondary; + } + + ComboBox { + model: ["BitCell", "Bitcoin", "Ethereum"]; + current-value <=> WalletState.selected-chain; + } + + Text { + text: "Recipient Address"; + font-size: 14px; + font-weight: 500; + color: Theme.text-secondary; + } + + LineEdit { + placeholder-text: "Enter recipient address..."; + text <=> WalletState.send-to-address; + } + + Text { + text: "Amount"; + font-size: 14px; + font-weight: 500; + color: Theme.text-secondary; + } + + LineEdit { + placeholder-text: "0.00"; + text <=> WalletState.send-amount; + input-type: decimal; + } + } + } + + HorizontalLayout { + alignment: center; + spacing: Theme.spacing-md; + + SecondaryButton { + text: "Cancel"; + clicked => { WalletState.current-tab = 3; } + } + + PrimaryButton { + text: "Send"; + enabled: !WalletState.wallet-locked; + clicked => { + WalletState.send-transaction( + WalletState.send-to-address, + WalletState.send-amount, + WalletState.selected-chain + ); + } + } + } + + if WalletState.wallet-locked : Text { + text: "⚠️ Unlock wallet to send transactions"; + font-size: 14px; + color: Theme.warning; + horizontal-alignment: center; + } + } +} + +// Unlock wallet view +component UnlockView inherits Rectangle { + in-out property passphrase: ""; + + background: Theme.background; + + VerticalLayout { + alignment: center; + spacing: Theme.spacing-lg; + padding: Theme.spacing-xl; + + BitCellLogo { + width: 64px; + height: 64px; + } + + Text { + text: "Unlock Wallet"; + font-size: 24px; + font-weight: 700; + color: Theme.text-primary; + horizontal-alignment: center; + } + + Card { + width: 400px; + + VerticalLayout { + spacing: Theme.spacing-md; + + Text { + text: "Enter your passphrase"; + font-size: 14px; + font-weight: 500; + color: Theme.text-secondary; + } + + LineEdit { + placeholder-text: "Passphrase..."; + text <=> root.passphrase; + input-type: password; + } + } + } + + HorizontalLayout { + alignment: center; + spacing: Theme.spacing-md; + + SecondaryButton { + text: "Cancel"; + clicked => { WalletState.current-tab = 3; } + } + + PrimaryButton { + text: "Unlock"; + clicked => { WalletState.unlock-wallet(root.passphrase); } + } + } + } +} + +// Receive/Addresses view +component ReceiveView inherits Rectangle { + background: Theme.background; + + VerticalLayout { + padding: Theme.spacing-lg; + spacing: Theme.spacing-lg; + + HorizontalLayout { + alignment: space-between; + + Text { + text: "Receive"; + font-size: 24px; + font-weight: 700; + color: Theme.text-primary; + } + + SecondaryButton { + text: "Back"; + width: 80px; + clicked => { WalletState.current-tab = 3; } + } + } + + Card { + title: "Your Addresses"; + vertical-stretch: 1; + + VerticalLayout { + spacing: Theme.spacing-sm; + + HorizontalLayout { + spacing: Theme.spacing-md; + + ComboBox { + horizontal-stretch: 1; + model: ["BitCell", "Bitcoin", "Ethereum"]; + current-value <=> WalletState.selected-chain; + } + + PrimaryButton { + text: "New Address"; + width: 120px; + clicked => { WalletState.generate-address(WalletState.selected-chain); } + } + } + + ScrollView { + vertical-stretch: 1; + + VerticalLayout { + spacing: Theme.spacing-sm; + + for addr in WalletState.addresses : AddressRow { + addr: addr; + } + } + } + } + } + + Text { + text: "Click an address to copy it to clipboard"; + font-size: 12px; + color: Theme.text-muted; + horizontal-alignment: center; + } + } +} + +// Transaction history view +component HistoryView inherits Rectangle { + background: Theme.background; + + VerticalLayout { + padding: Theme.spacing-lg; + spacing: Theme.spacing-lg; + + HorizontalLayout { + alignment: space-between; + + Text { + text: "Transaction History"; + font-size: 24px; + font-weight: 700; + color: Theme.text-primary; + } + + HorizontalLayout { + spacing: Theme.spacing-sm; + + SecondaryButton { + text: "Refresh"; + width: 80px; + clicked => { WalletState.refresh-balances(); } + } + + SecondaryButton { + text: "Back"; + width: 80px; + clicked => { WalletState.current-tab = 3; } + } + } + } + + Card { + vertical-stretch: 1; + + if WalletState.transactions.length == 0 : VerticalLayout { + alignment: center; + vertical-stretch: 1; + + Text { + text: "No transactions yet"; + font-size: 16px; + color: Theme.text-muted; + horizontal-alignment: center; + } + + Text { + text: "Your transaction history will appear here"; + font-size: 14px; + color: Theme.text-muted; + horizontal-alignment: center; + } + } + + if WalletState.transactions.length > 0 : ScrollView { + VerticalLayout { + spacing: Theme.spacing-sm; + + for tx in WalletState.transactions : TransactionRow { + tx: tx; + } + } + } + } + } +} + +// Main application window +export component MainWindow inherits Window { + title: "BitCell Wallet"; + icon: @image-url(""); + min-width: 800px; + min-height: 600px; + preferred-width: 960px; + preferred-height: 720px; + background: Theme.background; + + // View router based on current tab + if WalletState.current-tab == 0 && !WalletState.wallet-exists : WelcomeView { } + if WalletState.current-tab == 1 : RestoreView { } + if WalletState.current-tab == 2 || WalletState.show-mnemonic : MnemonicView { } + if WalletState.current-tab == 3 && WalletState.wallet-exists : DashboardView { } + if WalletState.current-tab == 4 : SendView { } + if WalletState.current-tab == 5 : UnlockView { } + if WalletState.current-tab == 6 : ReceiveView { } + if WalletState.current-tab == 7 : HistoryView { } + + // Status bar + if WalletState.status-message != "" : Rectangle { + y: parent.height - 40px; + width: 100%; + height: 40px; + background: Theme.surface; + + Text { + text: WalletState.status-message; + font-size: 14px; + color: Theme.text-secondary; + horizontal-alignment: center; + vertical-alignment: center; + } + } + + // Loading overlay + if WalletState.is-loading : Rectangle { + background: #00000080; + + VerticalLayout { + alignment: center; + + Text { + text: "Loading..."; + font-size: 18px; + color: white; + horizontal-alignment: center; + } + } + } +}