From 236e8d354e1a198b1737f95ef3d793142d3279d3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Dec 2025 20:54:22 +0000 Subject: [PATCH 1/4] Initial plan From 1f3cd7743e335c9cd8e99a52825958e14ba27381 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Dec 2025 21:09:56 +0000 Subject: [PATCH 2/4] Add signing_hash method and wallet GUI transaction implementation Co-authored-by: Steake <530040+Steake@users.noreply.github.com> --- crates/bitcell-consensus/src/block.rs | 18 ++- crates/bitcell-node/src/rpc.rs | 7 +- crates/bitcell-wallet-gui/Cargo.toml | 2 + crates/bitcell-wallet-gui/src/main.rs | 201 +++++++++++++++++++++++--- crates/bitcell-wallet/src/wallet.rs | 14 ++ 5 files changed, 217 insertions(+), 25 deletions(-) diff --git a/crates/bitcell-consensus/src/block.rs b/crates/bitcell-consensus/src/block.rs index a6d72d6..74f85ea 100644 --- a/crates/bitcell-consensus/src/block.rs +++ b/crates/bitcell-consensus/src/block.rs @@ -106,12 +106,28 @@ pub struct Transaction { } impl Transaction { - /// Compute transaction hash + /// Compute transaction hash (includes signature for uniqueness) pub fn hash(&self) -> Hash256 { // Note: bincode serialization to Vec cannot fail for this structure let serialized = bincode::serialize(self).expect("transaction serialization should never fail"); Hash256::hash(&serialized) } + + /// Compute hash for signing (excludes signature) + /// + /// This is the message that should be signed when creating a transaction. + /// The signature field is excluded to avoid circular dependency. + pub fn signing_hash(&self) -> Hash256 { + let mut data = Vec::new(); + data.extend_from_slice(&self.nonce.to_le_bytes()); + data.extend_from_slice(self.from.as_bytes()); + data.extend_from_slice(self.to.as_bytes()); + data.extend_from_slice(&self.amount.to_le_bytes()); + data.extend_from_slice(&self.gas_limit.to_le_bytes()); + data.extend_from_slice(&self.gas_price.to_le_bytes()); + data.extend_from_slice(&self.data); + Hash256::hash(&data) + } } /// Battle proof (placeholder for ZK proof) diff --git a/crates/bitcell-node/src/rpc.rs b/crates/bitcell-node/src/rpc.rs index 8b92b74..2c00dbb 100644 --- a/crates/bitcell-node/src/rpc.rs +++ b/crates/bitcell-node/src/rpc.rs @@ -515,8 +515,8 @@ async fn eth_send_raw_transaction(state: &RpcState, params: Option) -> Re })?; // Validate transaction signature - let tx_hash = tx.hash(); - if tx.signature.verify(&tx.from, tx_hash.as_bytes()).is_err() { + let signing_hash = tx.signing_hash(); + if tx.signature.verify(&tx.from, signing_hash.as_bytes()).is_err() { return Err(JsonRpcError { code: -32602, message: "Invalid transaction signature".to_string(), @@ -612,7 +612,8 @@ async fn eth_send_raw_transaction(state: &RpcState, params: Option) -> Re }); } - // Return transaction hash + // Return transaction hash (full hash including signature for uniqueness) + let tx_hash = tx.hash(); Ok(json!(format!("0x{}", hex::encode(tx_hash.as_bytes())))) } diff --git a/crates/bitcell-wallet-gui/Cargo.toml b/crates/bitcell-wallet-gui/Cargo.toml index e2ed9c6..b082f7b 100644 --- a/crates/bitcell-wallet-gui/Cargo.toml +++ b/crates/bitcell-wallet-gui/Cargo.toml @@ -15,6 +15,7 @@ path = "src/main.rs" [dependencies] bitcell-wallet = { path = "../bitcell-wallet" } bitcell-crypto = { path = "../bitcell-crypto" } +bitcell-consensus = { path = "../bitcell-consensus" } # Slint UI framework - native rendering, no WebView slint = "1.9" @@ -22,6 +23,7 @@ slint = "1.9" # Serialization serde.workspace = true serde_json = "1.0" +bincode = "1.3" # Error handling thiserror.workspace = true diff --git a/crates/bitcell-wallet-gui/src/main.rs b/crates/bitcell-wallet-gui/src/main.rs index 8a1308b..0814ff6 100644 --- a/crates/bitcell-wallet-gui/src/main.rs +++ b/crates/bitcell-wallet-gui/src/main.rs @@ -5,6 +5,7 @@ //! Features: 60fps smooth interactions, accessibility support, no WebView use bitcell_wallet::{Chain, Mnemonic, Wallet, WalletConfig}; +use bitcell_crypto::PublicKey; use std::cell::RefCell; use std::rc::Rc; @@ -45,6 +46,25 @@ fn parse_chain(chain: &str) -> Chain { } } +/// Parse a PublicKey from hex address string +/// +/// Addresses are stored as hex-encoded compressed public keys (0x prefix optional) +fn parse_public_key(address: &str) -> Result { + let hex_str = address.strip_prefix("0x").unwrap_or(address); + let bytes = hex::decode(hex_str) + .map_err(|e| format!("Invalid hex: {}", e))?; + + if bytes.len() != 33 { + return Err(format!("Invalid public key length: expected 33 bytes, got {}", bytes.len())); + } + + let mut pk_bytes = [0u8; 33]; + pk_bytes.copy_from_slice(&bytes); + + PublicKey::from_bytes(pk_bytes) + .map_err(|e| format!("Invalid public key: {}", e)) +} + /// Format chain for display fn chain_display_name(chain: Chain) -> &'static str { match chain { @@ -460,6 +480,7 @@ fn setup_callbacks(window: &MainWindow, state: Rc>) { let to_address = to_address.to_string(); // Async nonce fetch and transaction preparation + let state_clone = state.clone(); tokio::spawn(async move { // Get nonce from node let nonce = match rpc_client.get_transaction_count(&from_address).await { @@ -482,30 +503,168 @@ fn setup_callbacks(window: &MainWindow, state: Rc>) { Err(_) => DEFAULT_GAS_PRICE, // Use default if unavailable }; - // Calculate fee (simple estimate) - let fee = gas_price.saturating_mul(21000); + // Gas limit for simple transfer + let gas_limit = 21000u64; - // For now, display transaction details and inform user signing requires wallet unlock - // In production, this would integrate with hardware wallet or secure key management - let tx_info = format!( - "Transaction prepared:\n\ - From: {}\n\ - To: {}\n\ - Amount: {} units\n\ - Fee: {} units\n\ - Nonce: {}\n\n\ - Hardware wallet signing coming soon. \ - Use the CLI or Admin console with HSM for secure signing.", - from_address, to_address, amount_units, fee, nonce - ); + // Parse addresses as PublicKeys + let from_pk = match parse_public_key(&from_address) { + Ok(pk) => pk, + Err(e) => { + let _ = slint::invoke_from_event_loop(move || { + if let Some(window) = window_weak.upgrade() { + let ws = window.global::(); + ws.set_is_loading(false); + ws.set_status_message(format!("Invalid sender address: {}", e).into()); + } + }); + return; + } + }; - let _ = slint::invoke_from_event_loop(move || { - if let Some(window) = window_weak.upgrade() { - let ws = window.global::(); - ws.set_is_loading(false); - ws.set_status_message(tx_info.into()); + let to_pk = match parse_public_key(&to_address) { + Ok(pk) => pk, + Err(e) => { + let _ = slint::invoke_from_event_loop(move || { + if let Some(window) = window_weak.upgrade() { + let ws = window.global::(); + ws.set_is_loading(false); + ws.set_status_message(format!("Invalid recipient address: {}", e).into()); + } + }); + return; } - }); + }; + + // Get wallet for signing + let mut app_state = state_clone.borrow_mut(); + let wallet = match &mut app_state.wallet { + Some(w) => w, + None => { + drop(app_state); + let _ = slint::invoke_from_event_loop(move || { + if let Some(window) = window_weak.upgrade() { + let ws = window.global::(); + ws.set_is_loading(false); + ws.set_status_message("Wallet not loaded".into()); + } + }); + return; + } + }; + + // Check wallet is unlocked + if !wallet.is_unlocked() { + drop(app_state); + let _ = slint::invoke_from_event_loop(move || { + if let Some(window) = window_weak.upgrade() { + let ws = window.global::(); + ws.set_is_loading(false); + ws.set_status_message("Wallet is locked. Please unlock to send transactions.".into()); + } + }); + return; + } + + // Find the wallet address to get the secret key + let wallet_addr = match wallet.all_addresses().iter().find(|a| a.to_string_formatted() == from_address) { + Some(a) => a.clone(), + None => { + drop(app_state); + let _ = slint::invoke_from_event_loop(move || { + if let Some(window) = window_weak.upgrade() { + let ws = window.global::(); + ws.set_is_loading(false); + ws.set_status_message("Sender address not found in wallet".into()); + } + }); + return; + } + }; + + // Get the secret key for signing + let secret_key = match wallet.get_secret_key(&wallet_addr) { + Ok(sk) => sk, + Err(e) => { + drop(app_state); + let _ = slint::invoke_from_event_loop(move || { + if let Some(window) = window_weak.upgrade() { + let ws = window.global::(); + ws.set_is_loading(false); + ws.set_status_message(format!("Failed to get signing key: {}", e).into()); + } + }); + return; + } + }; + + drop(app_state); // Release borrow + + // Create transaction with placeholder signature + let placeholder_sig = bitcell_crypto::Signature::from_bytes([0u8; 64]) + .expect("placeholder signature"); + + let mut tx = bitcell_consensus::Transaction { + nonce, + from: from_pk, + to: to_pk, + amount: amount_units, + gas_limit, + gas_price, + data: Vec::new(), + signature: placeholder_sig, + }; + + // Compute signing hash and sign + let signing_hash = tx.signing_hash(); + let signature = secret_key.sign(signing_hash.as_bytes()); + tx.signature = signature; + + // Serialize transaction + let tx_bytes = match bincode::serialize(&tx) { + Ok(bytes) => bytes, + Err(e) => { + let _ = slint::invoke_from_event_loop(move || { + if let Some(window) = window_weak.upgrade() { + let ws = window.global::(); + ws.set_is_loading(false); + ws.set_status_message(format!("Failed to serialize transaction: {}", e).into()); + } + }); + return; + } + }; + + // Send transaction via RPC + match rpc_client.send_raw_transaction_bytes(&tx_bytes).await { + Ok(tx_hash) => { + let success_msg = format!( + "Transaction sent successfully!\n\ + Transaction hash: {}\n\ + Amount: {} CELL\n\ + Fee: {} CELL", + tx_hash, + amount_units as f64 / 100_000_000.0, + (gas_limit * gas_price) as f64 / 100_000_000.0 + ); + + let _ = slint::invoke_from_event_loop(move || { + if let Some(window) = window_weak.upgrade() { + let ws = window.global::(); + ws.set_is_loading(false); + ws.set_status_message(success_msg.into()); + } + }); + } + Err(e) => { + let _ = slint::invoke_from_event_loop(move || { + if let Some(window) = window_weak.upgrade() { + let ws = window.global::(); + ws.set_is_loading(false); + ws.set_status_message(format!("Failed to send transaction: {}", e).into()); + } + }); + } + } }); }); } diff --git a/crates/bitcell-wallet/src/wallet.rs b/crates/bitcell-wallet/src/wallet.rs index 7dc3aa2..bf4a57f 100644 --- a/crates/bitcell-wallet/src/wallet.rs +++ b/crates/bitcell-wallet/src/wallet.rs @@ -398,6 +398,20 @@ impl Wallet { self.sign_transaction(tx, from) } + /// Get the secret key for an address + /// + /// This allows signing transactions outside the wallet's transaction system. + /// The wallet must be unlocked to access secret keys. + pub fn get_secret_key(&mut self, address: &Address) -> Result { + if !self.is_unlocked() { + return Err(Error::WalletLocked); + } + + let path = DerivationPath::for_chain(address.chain(), address.index()); + let key = self.derive_key(&path)?; + Ok(key.secret_key.clone()) + } + /// Get transaction history pub fn history(&self) -> &TransactionHistory { &self.history From 80cdfb3ab3d55aa0c0b7e03bd135fbada7ad4758 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Dec 2025 21:15:49 +0000 Subject: [PATCH 3/4] Add transaction signing tests and fix Signature initialization Co-authored-by: Steake <530040+Steake@users.noreply.github.com> --- crates/bitcell-consensus/src/block.rs | 69 +++++++++++++++++++++++++++ crates/bitcell-wallet-gui/src/main.rs | 3 +- 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/crates/bitcell-consensus/src/block.rs b/crates/bitcell-consensus/src/block.rs index 74f85ea..68e7d3a 100644 --- a/crates/bitcell-consensus/src/block.rs +++ b/crates/bitcell-consensus/src/block.rs @@ -189,4 +189,73 @@ mod tests { let hash = tx.hash(); assert_ne!(hash, Hash256::zero()); } + + #[test] + fn test_transaction_signing_hash() { + let sk = SecretKey::generate(); + let pk = sk.public_key(); + + // Create transaction with placeholder signature + let placeholder_sig = bitcell_crypto::Signature::from_bytes([0u8; 64]); + let mut tx = Transaction { + nonce: 1, + from: pk.clone(), + to: pk.clone(), + amount: 100, + gas_limit: 21000, + gas_price: 1000, + data: vec![], + signature: placeholder_sig, + }; + + // Get signing hash and sign + let signing_hash = tx.signing_hash(); + let signature = sk.sign(signing_hash.as_bytes()); + tx.signature = signature; + + // Verify signature using signing_hash (not full hash) + assert!(tx.signature.verify(&pk, signing_hash.as_bytes()).is_ok()); + + // The full hash should be different from signing hash (because it includes signature) + let full_hash = tx.hash(); + assert_ne!(full_hash, signing_hash); + } + + #[test] + fn test_signing_hash_excludes_signature() { + let sk = SecretKey::generate(); + let pk = sk.public_key(); + + // Create two identical transactions with different signatures + let sig1 = sk.sign(b"different1"); + let sig2 = sk.sign(b"different2"); + + let tx1 = Transaction { + nonce: 1, + from: pk.clone(), + to: pk.clone(), + amount: 100, + gas_limit: 21000, + gas_price: 1000, + data: vec![], + signature: sig1, + }; + + let tx2 = Transaction { + nonce: 1, + from: pk.clone(), + to: pk.clone(), + amount: 100, + gas_limit: 21000, + gas_price: 1000, + data: vec![], + signature: sig2, + }; + + // Signing hashes should be identical (signature not included) + assert_eq!(tx1.signing_hash(), tx2.signing_hash()); + + // Full hashes should be different (signature included) + assert_ne!(tx1.hash(), tx2.hash()); + } } diff --git a/crates/bitcell-wallet-gui/src/main.rs b/crates/bitcell-wallet-gui/src/main.rs index 0814ff6..31b1a29 100644 --- a/crates/bitcell-wallet-gui/src/main.rs +++ b/crates/bitcell-wallet-gui/src/main.rs @@ -600,8 +600,7 @@ fn setup_callbacks(window: &MainWindow, state: Rc>) { drop(app_state); // Release borrow // Create transaction with placeholder signature - let placeholder_sig = bitcell_crypto::Signature::from_bytes([0u8; 64]) - .expect("placeholder signature"); + let placeholder_sig = bitcell_crypto::Signature::from_bytes([0u8; 64]); let mut tx = bitcell_consensus::Transaction { nonce, From c81e2f039bacc90873bdda1135f00a8b7c37edaf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Dec 2025 21:24:13 +0000 Subject: [PATCH 4/4] Add PLACEHOLDER_SIGNATURE constants and improve code clarity Co-authored-by: Steake <530040+Steake@users.noreply.github.com> --- crates/bitcell-consensus/src/block.rs | 7 +++++-- crates/bitcell-wallet-gui/src/main.rs | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/crates/bitcell-consensus/src/block.rs b/crates/bitcell-consensus/src/block.rs index 68e7d3a..ca2a81a 100644 --- a/crates/bitcell-consensus/src/block.rs +++ b/crates/bitcell-consensus/src/block.rs @@ -152,6 +152,9 @@ mod tests { use super::*; use bitcell_crypto::SecretKey; + /// Placeholder signature for tests (before actual signing) + const PLACEHOLDER_SIGNATURE: [u8; 64] = [0u8; 64]; + #[test] fn test_block_header_hash() { let sk = SecretKey::generate(); @@ -195,8 +198,8 @@ mod tests { let sk = SecretKey::generate(); let pk = sk.public_key(); - // Create transaction with placeholder signature - let placeholder_sig = bitcell_crypto::Signature::from_bytes([0u8; 64]); + // Create transaction with placeholder signature (will be replaced after signing) + let placeholder_sig = bitcell_crypto::Signature::from_bytes(PLACEHOLDER_SIGNATURE); let mut tx = Transaction { nonce: 1, from: pk.clone(), diff --git a/crates/bitcell-wallet-gui/src/main.rs b/crates/bitcell-wallet-gui/src/main.rs index 31b1a29..30ae807 100644 --- a/crates/bitcell-wallet-gui/src/main.rs +++ b/crates/bitcell-wallet-gui/src/main.rs @@ -20,6 +20,9 @@ mod game_viz; /// Default gas price when RPC call fails const DEFAULT_GAS_PRICE: u64 = 1000; +/// Placeholder signature for transaction creation (before signing) +const PLACEHOLDER_SIGNATURE: [u8; 64] = [0u8; 64]; + /// Wallet application state struct AppState { wallet: Option, @@ -599,8 +602,8 @@ fn setup_callbacks(window: &MainWindow, state: Rc>) { drop(app_state); // Release borrow - // Create transaction with placeholder signature - let placeholder_sig = bitcell_crypto::Signature::from_bytes([0u8; 64]); + // Create transaction with placeholder signature (will be replaced after signing) + let placeholder_sig = bitcell_crypto::Signature::from_bytes(PLACEHOLDER_SIGNATURE); let mut tx = bitcell_consensus::Transaction { nonce,