-
Notifications
You must be signed in to change notification settings - Fork 1
[WIP] Implement real transaction system for wallet and RPC #90
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -57,6 +57,36 @@ fn chain_display_name(chain: Chain) -> &'static str { | |||||
| } | ||||||
| } | ||||||
|
|
||||||
| /// Parse address string to PublicKey | ||||||
| /// For BitCell addresses, the address is the hex-encoded public key with optional prefix | ||||||
|
||||||
| fn parse_address_to_pubkey(address: &str) -> Result<bitcell_crypto::PublicKey, String> { | ||||||
| // Remove common prefixes | ||||||
| let address = address.trim(); | ||||||
| let address = if address.starts_with("0x") { | ||||||
| &address[2..] | ||||||
| } else if address.starts_with("BC1") || address.starts_with("bc1") { | ||||||
| // BitCell address format - for now, just strip prefix | ||||||
| // In a real implementation, this would decode the address properly | ||||||
| &address[3..] | ||||||
| } else { | ||||||
| address | ||||||
| }; | ||||||
|
Comment on lines
+67
to
+73
|
||||||
|
|
||||||
| // Decode hex to bytes | ||||||
| let bytes = hex::decode(address) | ||||||
| .map_err(|e| format!("Invalid hex in address: {}", e))?; | ||||||
|
|
||||||
| if bytes.len() != 33 { | ||||||
| return Err(format!("Address must be 33 bytes (compressed public key), got {}", bytes.len())); | ||||||
| } | ||||||
|
|
||||||
| let mut key_bytes = [0u8; 33]; | ||||||
| key_bytes.copy_from_slice(&bytes); | ||||||
|
|
||||||
| bitcell_crypto::PublicKey::from_bytes(key_bytes) | ||||||
| .map_err(|e| format!("Invalid public key: {}", e)) | ||||||
| } | ||||||
|
|
||||||
| #[tokio::main] | ||||||
| async fn main() -> Result<(), Box<dyn std::error::Error>> { | ||||||
| // Initialize logging | ||||||
|
|
@@ -416,10 +446,10 @@ fn setup_callbacks(window: &MainWindow, state: Rc<RefCell<AppState>>) { | |||||
| // Convert to smallest units (1 CELL = 100_000_000 units) | ||||||
| let amount_units = (amount * 100_000_000.0) as u64; | ||||||
|
|
||||||
| // Get wallet and RPC client | ||||||
| let app_state = state.borrow(); | ||||||
|
|
||||||
| let (from_address, rpc_client) = { | ||||||
| // Get wallet info and secret key before async operation | ||||||
| let (from_addr_formatted, secret_key, rpc_client) = { | ||||||
| let app_state = state.borrow(); | ||||||
| let wallet = match &app_state.wallet { | ||||||
| Some(w) => w, | ||||||
| None => { | ||||||
|
|
@@ -428,16 +458,30 @@ fn setup_callbacks(window: &MainWindow, state: Rc<RefCell<AppState>>) { | |||||
| } | ||||||
| }; | ||||||
|
|
||||||
| if !wallet.is_unlocked() { | ||||||
| wallet_state.set_status_message("Wallet is locked. Please unlock it first.".into()); | ||||||
| return; | ||||||
| } | ||||||
|
|
||||||
| // Get the first address as sender | ||||||
| let addresses = wallet.all_addresses(); | ||||||
| let from_addr = match addresses.iter().find(|a| a.chain() == chain) { | ||||||
| Some(a) => a.to_string_formatted(), | ||||||
| let from_addr_obj = match addresses.iter().find(|a| a.chain() == chain) { | ||||||
| Some(a) => a, | ||||||
| None => { | ||||||
| wallet_state.set_status_message(format!("No {} address available", chain_display_name(chain)).into()); | ||||||
| return; | ||||||
| } | ||||||
| }; | ||||||
|
|
||||||
| // Get secret key for signing | ||||||
| let sk = match wallet.get_secret_key_for_address(from_addr_obj) { | ||||||
| Ok(sk) => sk, | ||||||
| Err(e) => { | ||||||
| wallet_state.set_status_message(format!("Failed to get secret key: {}", e).into()); | ||||||
| return; | ||||||
| } | ||||||
| }; | ||||||
|
|
||||||
| let rpc = match &app_state.rpc_client { | ||||||
| Some(c) => c.clone(), | ||||||
| None => { | ||||||
|
|
@@ -446,23 +490,20 @@ fn setup_callbacks(window: &MainWindow, state: Rc<RefCell<AppState>>) { | |||||
| } | ||||||
| }; | ||||||
|
|
||||||
| (from_addr, rpc) | ||||||
| (from_addr_obj.to_string_formatted(), sk, rpc) | ||||||
| }; | ||||||
|
|
||||||
| // Drop app_state borrow before the async operation | ||||||
| drop(app_state); | ||||||
|
|
||||||
| // Set loading state | ||||||
| wallet_state.set_is_loading(true); | ||||||
| wallet_state.set_status_message("Preparing transaction...".into()); | ||||||
|
|
||||||
| let window_weak = window.as_weak(); | ||||||
| let to_address = to_address.to_string(); | ||||||
|
|
||||||
| // Async nonce fetch and transaction preparation | ||||||
| // Async nonce fetch and transaction creation | ||||||
| tokio::spawn(async move { | ||||||
| // Get nonce from node | ||||||
| let nonce = match rpc_client.get_transaction_count(&from_address).await { | ||||||
| let nonce = match rpc_client.get_transaction_count(&from_addr_formatted).await { | ||||||
| Ok(n) => n, | ||||||
| Err(e) => { | ||||||
| let _ = slint::invoke_from_event_loop(move || { | ||||||
|
|
@@ -482,30 +523,95 @@ fn setup_callbacks(window: &MainWindow, state: Rc<RefCell<AppState>>) { | |||||
| 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 to PublicKey format | ||||||
| let from_pk = match parse_address_to_pubkey(&from_addr_formatted) { | ||||||
| Ok(pk) => pk, | ||||||
| Err(e) => { | ||||||
| let _ = slint::invoke_from_event_loop(move || { | ||||||
| if let Some(window) = window_weak.upgrade() { | ||||||
| let ws = window.global::<WalletState>(); | ||||||
| ws.set_is_loading(false); | ||||||
| ws.set_status_message(format!("Invalid from address: {}", e).into()); | ||||||
| } | ||||||
| }); | ||||||
| return; | ||||||
| } | ||||||
| }; | ||||||
|
|
||||||
| let _ = slint::invoke_from_event_loop(move || { | ||||||
| if let Some(window) = window_weak.upgrade() { | ||||||
| let ws = window.global::<WalletState>(); | ||||||
| ws.set_is_loading(false); | ||||||
| ws.set_status_message(tx_info.into()); | ||||||
| let to_pk = match parse_address_to_pubkey(&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::<WalletState>(); | ||||||
| ws.set_is_loading(false); | ||||||
| ws.set_status_message(format!("Invalid to address: {}", e).into()); | ||||||
| } | ||||||
| }); | ||||||
| return; | ||||||
| } | ||||||
| }); | ||||||
| }; | ||||||
|
|
||||||
| // Create consensus transaction (without signature initially) | ||||||
| let mut tx = bitcell_consensus::Transaction { | ||||||
| nonce, | ||||||
| from: from_pk, | ||||||
| to: to_pk, | ||||||
| amount: amount_units, | ||||||
| gas_limit, | ||||||
| gas_price, | ||||||
| data: vec![], | ||||||
| signature: bitcell_crypto::Signature::from_bytes([0u8; 64]), // Placeholder | ||||||
|
||||||
| signature: bitcell_crypto::Signature::from_bytes([0u8; 64]), // Placeholder | |
| signature: bitcell_crypto::Signature::from_bytes([0xFFu8; 64]), // Invalid placeholder |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -398,6 +398,31 @@ impl Wallet { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.sign_transaction(tx, from) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Get the secret key for an address (for advanced use cases like consensus transaction signing) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// This method should be used with caution as it exposes the raw secret key. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Prefer using sign_transaction when possible. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pub fn get_secret_key_for_address(&self, address: &Address) -> Result<SecretKey> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if !self.is_unlocked() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Err(Error::WalletLocked); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let path = DerivationPath::for_chain(address.chain(), address.index()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // We need to derive the key without caching (since self is immutable) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let seed = self.master_seed.as_ref().ok_or(Error::WalletLocked)?; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let path_str = path.to_string(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let mut derivation_data = Vec::new(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| derivation_data.extend_from_slice(seed.as_bytes()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| derivation_data.extend_from_slice(path_str.as_bytes()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let derived_hash = Hash256::hash(&derivation_data); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let secret_key = SecretKey::from_bytes(derived_hash.as_bytes())?; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Ok(secret_key) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+415
to
+423
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let path_str = path.to_string(); | |
| let mut derivation_data = Vec::new(); | |
| derivation_data.extend_from_slice(seed.as_bytes()); | |
| derivation_data.extend_from_slice(path_str.as_bytes()); | |
| let derived_hash = Hash256::hash(&derivation_data); | |
| let secret_key = SecretKey::from_bytes(derived_hash.as_bytes())?; | |
| Ok(secret_key) | |
| Self::derive_secret_key_from_seed(seed, &path) | |
| } | |
| /// Helper to derive a secret key from seed and derivation path | |
| fn derive_secret_key_from_seed(seed: &SeedBytes, path: &DerivationPath) -> Result<SecretKey> { | |
| let path_str = path.to_string(); | |
| let mut derivation_data = Vec::new(); | |
| derivation_data.extend_from_slice(seed.as_bytes()); | |
| derivation_data.extend_from_slice(path_str.as_bytes()); | |
| let derived_hash = Hash256::hash(&derivation_data); | |
| SecretKey::from_bytes(derived_hash.as_bytes()) |
Copilot
AI
Dec 9, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The get_secret_key_for_address method exposes raw secret keys, which is a significant security risk. While the documentation warns about this, the API design could be improved to reduce the attack surface.
Consider alternative approaches:
- Accept a callback function that receives the secret key temporarily:
fn with_secret_key_for_address<F, R>(&self, address: &Address, f: F) -> Result<R> where F: FnOnce(&SecretKey) -> R - Return a guard object that automatically zeros the key when dropped
- Provide a more specific
sign_consensus_transactionmethod that handles the signing internally without exposing the raw key
The callback approach is particularly good as it ensures the secret key never leaves the wallet's control and is automatically dropped after use:
pub fn with_secret_key_for_address<F, R>(&self, address: &Address, f: F) -> Result<R>
where
F: FnOnce(&SecretKey) -> R
{
if !self.is_unlocked() {
return Err(Error::WalletLocked);
}
let path = DerivationPath::for_chain(address.chain(), address.index());
let seed = self.master_seed.as_ref().ok_or(Error::WalletLocked)?;
let path_str = path.to_string();
let mut derivation_data = Vec::new();
derivation_data.extend_from_slice(seed.as_bytes());
derivation_data.extend_from_slice(path_str.as_bytes());
let derived_hash = Hash256::hash(&derivation_data);
let secret_key = SecretKey::from_bytes(derived_hash.as_bytes())?;
Ok(f(&secret_key))
}| /// Get the secret key for an address (for advanced use cases like consensus transaction signing) | |
| /// | |
| /// This method should be used with caution as it exposes the raw secret key. | |
| /// Prefer using sign_transaction when possible. | |
| pub fn get_secret_key_for_address(&self, address: &Address) -> Result<SecretKey> { | |
| if !self.is_unlocked() { | |
| return Err(Error::WalletLocked); | |
| } | |
| let path = DerivationPath::for_chain(address.chain(), address.index()); | |
| // We need to derive the key without caching (since self is immutable) | |
| let seed = self.master_seed.as_ref().ok_or(Error::WalletLocked)?; | |
| let path_str = path.to_string(); | |
| let mut derivation_data = Vec::new(); | |
| derivation_data.extend_from_slice(seed.as_bytes()); | |
| derivation_data.extend_from_slice(path_str.as_bytes()); | |
| let derived_hash = Hash256::hash(&derivation_data); | |
| let secret_key = SecretKey::from_bytes(derived_hash.as_bytes())?; | |
| Ok(secret_key) | |
| /// Perform an operation with the secret key for an address (for advanced use cases like consensus transaction signing) | |
| /// | |
| /// This method is safer than exposing the raw secret key. The key is only available inside the callback and is dropped immediately after. | |
| /// Prefer using sign_transaction when possible. | |
| pub fn with_secret_key_for_address<F, R>(&self, address: &Address, f: F) -> Result<R> | |
| where | |
| F: FnOnce(&SecretKey) -> R, | |
| { | |
| if !self.is_unlocked() { | |
| return Err(Error::WalletLocked); | |
| } | |
| let path = DerivationPath::for_chain(address.chain(), address.index()); | |
| let seed = self.master_seed.as_ref().ok_or(Error::WalletLocked)?; | |
| let path_str = path.to_string(); | |
| let mut derivation_data = Vec::new(); | |
| derivation_data.extend_from_slice(seed.as_bytes()); | |
| derivation_data.extend_from_slice(path_str.as_bytes()); | |
| let derived_hash = Hash256::hash(&derivation_data); | |
| let secret_key = SecretKey::from_bytes(derived_hash.as_bytes())?; | |
| Ok(f(&secret_key)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new
signing_hash()method lacks test coverage. Given the critical nature of transaction signing (incorrect implementation could lead to funds loss or security vulnerabilities), this method should have comprehensive tests that verify:Consider adding a test like: