diff --git a/crates/sp_client/src/api.rs b/crates/sp_client/src/api.rs index 19c348c..91f8c67 100644 --- a/crates/sp_client/src/api.rs +++ b/crates/sp_client/src/api.rs @@ -34,7 +34,7 @@ use wasm_bindgen::convert::FromWasmAbi; use wasm_bindgen::prelude::*; use sdk_common::network::{ - self, AnkFlag, AnkNetworkMsg, FaucetMessage, NewTxMessage, UnknownMessage, + self, AnkFlag, AnkNetworkMsg, CachedMessage, CachedMessageStatus, FaucetMessage, NewTxMessage, UnknownMessage }; use sdk_common::silentpayments::{ create_transaction, create_transaction_for_address_with_shared_secret, @@ -371,10 +371,10 @@ fn handle_recover_transaction( sp_wallet: &mut SpWallet, tweak_data: PublicKey, fee_rate: u32, -) -> anyhow::Result { +) -> anyhow::Result { // We need to look for different case: // 1) faucet - // This one is the simplest, we only care about finding the commitment + // This one is the simplest, we only care about finding the commitment.clone() let op_return = tx.output.iter().find(|o| o.script_pubkey.is_op_return()); let commitment = if op_return.is_none() { vec![] @@ -382,14 +382,18 @@ fn handle_recover_transaction( op_return.unwrap().script_pubkey.as_bytes()[2..].to_vec() }; let commitment_str = commitment.to_lower_hex_string(); - let pos = lock_messages()? - .iter() - .position(|m| m.commitment.as_ref() == Some(&commitment_str)); + { + let mut messages = lock_messages()?; + let pos = messages + .iter() + .position(|m| m.commitment.as_ref() == Some(&commitment_str)); - if pos.is_some() { - let messages = lock_messages()?; - let message = messages.get(pos.unwrap()); - return Ok(message.cloned().unwrap()); + if pos.is_some() { + let mut message = messages.swap_remove(pos.unwrap()); + message.commited_in = updated.into_iter().next().map(|(outpoint, _)| outpoint); + message.status = CachedMessageStatus::FaucetComplete; + return Ok(message); + } } // If we got updates from a transaction, it means that it creates an output to us, spend an output we owned, or both @@ -423,7 +427,7 @@ fn handle_recover_transaction( // If we are receiver, that's pretty much it, just set status to complete if message.recipient == Some(sp_wallet.get_client().get_receiving_address()) { debug_assert!(message.confirmed_by == Some(input.previous_output)); - message.status = NetworkMessageStatus::Complete; + message.status = CachedMessageStatus::Complete; return Ok(message.clone()); } @@ -443,11 +447,11 @@ fn handle_recover_transaction( let mut messages = lock_messages()?; let cipher_pos = messages.iter().position(|m| { - if m.status != NetworkMessageStatus::CipherWaitingTx { + if m.status != CachedMessageStatus::CipherWaitingTx { return false; } m.try_decrypt_with_shared_secret(shared_secret.to_byte_array()) - .is_some() + .is_ok() }); if cipher_pos.is_some() { @@ -468,15 +472,17 @@ fn handle_recover_transaction( return Ok(message.clone()) } else { // store it and wait for the message - let mut new_msg = NetworkMessage::default(); - let (outpoint, output) = utxo_created.iter().next().unwrap(); - new_msg.commited_in = Some(**outpoint); + let mut new_msg = CachedMessage::new(); + debug!("{:?}", utxo_created); + let (outpoint, output) = utxo_created.into_iter().next().expect("utxo_created shouldn't be empty"); + new_msg.commited_in = Some(outpoint.clone()); new_msg.commitment = Some(commitment.to_lower_hex_string()); new_msg.recipient = Some(sp_wallet.get_client().get_receiving_address()); new_msg.shared_secret = Some(shared_secret.to_byte_array().to_lower_hex_string()); - new_msg.status = NetworkMessageStatus::TxWaitingCipher; + new_msg.status = CachedMessageStatus::TxWaitingCipher; lock_messages()?.push(new_msg.clone()); + debug!("returning {:?}", new_msg); return Ok(new_msg.clone()); } } @@ -497,14 +503,14 @@ fn handle_recover_transaction( } } -/// If the transaction has anything to do with us, we create/update the relevant `NetworkMessage` +/// If the transaction has anything to do with us, we create/update the relevant `CachedMessage` /// and return it to caller for persistent storage fn process_transaction( tx_hex: String, blockheight: u32, tweak_data_hex: String, fee_rate: u32, -) -> anyhow::Result { +) -> anyhow::Result { let tx = deserialize::(&Vec::from_hex(&tx_hex)?)?; // check that we don't already have scanned the tx @@ -545,16 +551,8 @@ fn process_transaction( Err(anyhow::Error::msg("No output found")) } -#[derive(Tsify, Serialize, Deserialize)] -#[tsify(into_wasm_abi, from_wasm_abi)] -#[allow(non_camel_case_types)] -pub struct parseNetworkMsgReturn { - topic: String, - message: NetworkMessage, -} - #[wasm_bindgen] -pub fn parse_network_msg(raw: String, fee_rate: u32) -> ApiResult { +pub fn parse_network_msg(raw: String, fee_rate: u32) -> ApiResult { if let Ok(ank_msg) = serde_json::from_str::(&raw) { match ank_msg.flag { AnkFlag::NewTx => { @@ -570,29 +568,23 @@ pub fn parse_network_msg(raw: String, fee_rate: u32) -> ApiResult unimplemented!(), AnkFlag::Error => { - let error_msg = NetworkMessage::new_error(ank_msg.content); - return Ok(parseNetworkMsgReturn { - topic: AnkFlag::Error.as_str().to_owned(), - message: error_msg, - }) + let error_msg = CachedMessage::new_error(ank_msg.content); + return Ok(error_msg) } AnkFlag::Unknown => { // let's try to decrypt with keys we found in transactions but haven't used yet let mut messages = lock_messages()?; - let cipher = Vec::from_hex(&ank_msg.content)?; + let cipher = Vec::from_hex(&ank_msg.content.trim_matches('\"'))?; let cipher_pos = messages.iter().position(|m| { - if m.status != NetworkMessageStatus::TxWaitingCipher { + debug!("Trying message: {:?}", m); + if m.status != CachedMessageStatus::TxWaitingCipher { return false; } - m.try_decrypt_cipher(cipher.clone()).is_some() + m.try_decrypt_cipher(cipher.clone()).is_ok() }); if cipher_pos.is_some() { let mut message = messages.get_mut(cipher_pos.unwrap()).unwrap(); @@ -601,14 +593,11 @@ pub fn parse_network_msg(raw: String, fee_rate: u32) -> ApiResult ApiResult { #[derive(Tsify, Serialize, Deserialize)] #[tsify(into_wasm_abi, from_wasm_abi)] #[allow(non_camel_case_types)] -pub struct createNotificationTransactionReturn { +pub struct createTransactionReturn { pub txid: String, pub transaction: String, - pub new_network_msg: NetworkMessage, + pub new_network_msg: CachedMessage, +} + +/// This is what we call to answer a confirmation as a sender +#[wasm_bindgen] +pub fn answer_confirmation_transaction( + message: CachedMessage, + fee_rate: u32, +) -> ApiResult { + if message.recipient.is_none() || message.confirmed_by.is_none() { + return Err(ApiError { message: "Invalid network message".to_owned() }); + } + + let sp_address: SilentPaymentAddress = message.recipient.as_ref().unwrap().as_str().try_into()?; + + let connected_user = lock_connected_user()?; + + let sp_wallet: &SpWallet; + if sp_address.is_testnet() { + sp_wallet = connected_user.try_get_recover()?; + } else { + sp_wallet = connected_user.try_get_main()?; + } + + let recipient = Recipient { + address: sp_address.into(), + amount: Amount::from_sat(0), // we'll set amount to what's available in the confirmed_by output we don't want change + nb_outputs: 1, + }; + + let signed_psbt = create_transaction_spend_outpoint( + &message.confirmed_by.unwrap(), + sp_wallet, + recipient, + Amount::from_sat(fee_rate.into()) + )?; + + let final_tx = signed_psbt.extract_tx()?; + + Ok(createTransactionReturn { + txid: final_tx.txid().to_string(), + transaction: serialize(&final_tx).to_lower_hex_string(), + new_network_msg: message + }) } /// This is what we call to confirm as a receiver #[wasm_bindgen] pub fn create_confirmation_transaction( - message: NetworkMessage, + message: CachedMessage, fee_rate: u32, -) -> ApiResult { +) -> ApiResult { if message.sender.is_none() || message.confirmed_by.is_none() { return Err(ApiError { message: "Invalid network message".to_owned() }); } @@ -706,7 +738,7 @@ pub fn create_confirmation_transaction( let recipient = Recipient { address: sp_address.into(), - amount: Amount::from_sat(1200), + amount: Amount::from_sat(0), nb_outputs: 1, }; @@ -719,7 +751,7 @@ pub fn create_confirmation_transaction( let final_tx = signed_psbt.extract_tx()?; - Ok(createNotificationTransactionReturn { + Ok(createTransactionReturn { txid: final_tx.txid().to_string(), transaction: serialize(&final_tx).to_lower_hex_string(), new_network_msg: message @@ -731,7 +763,7 @@ pub fn create_notification_transaction( address: String, commitment: Option, fee_rate: u32, -) -> ApiResult { +) -> ApiResult { let sp_address: SilentPaymentAddress = address.as_str().try_into()?; let connected_user = lock_connected_user()?; @@ -782,16 +814,17 @@ pub fn create_notification_transaction( let recipients_vouts = sp_address2vouts.get::(&address).expect("recipients didn't change").as_slice(); // for now let's just take the smallest vout that belongs to the recipient let final_tx = psbt.extract_tx()?; - let mut new_msg = NetworkMessage::default(); + let mut new_msg = CachedMessage::default(); new_msg.commitment = commitment; new_msg.commited_in = Some(OutPoint { txid: final_tx.txid(), vout: recipients_vouts[0] as u32 }); new_msg.shared_secret = Some(shared_secret.to_byte_array().to_lower_hex_string()); new_msg.recipient = Some(address); new_msg.sender = Some(sp_wallet.get_client().get_receiving_address()); + new_msg.status = CachedMessageStatus::SentWaitingConfirmation; // plaintext and ciphertext to be added later when sending the encrypted message lock_messages()?.push(new_msg.clone()); - Ok(createNotificationTransactionReturn { + Ok(createTransactionReturn { txid: final_tx.txid().to_string(), transaction: serialize(&final_tx).to_lower_hex_string(), new_network_msg: new_msg, @@ -872,8 +905,9 @@ pub fn create_faucet_msg() -> ApiResult { let sp_address = user.try_get_recover()?.get_client().get_receiving_address(); let faucet_msg = FaucetMessage::new(sp_address); // we write the commitment in a networkmessage so that we can keep track - let mut network_msg = NetworkMessage::default(); + let mut network_msg = CachedMessage::new(); network_msg.commitment = Some(faucet_msg.commitment.clone()); + network_msg.status = CachedMessageStatus::FaucetWaiting; lock_messages()?.push(network_msg); Ok(faucet_msg) } @@ -885,110 +919,3 @@ pub fn create_commitment(payload_to_hash: String) -> String { let hash = sha256::Hash::from_engine(engine); hash.to_byte_array().to_lower_hex_string() } - -#[derive(Debug, Serialize, Deserialize, PartialEq, Tsify, Clone)] -pub enum NetworkMessageStatus { - NoStatus, // Default - CipherWaitingTx, - TxWaitingCipher, - SentWaitingConfirmation, - MustSpentConfirmation, - Complete, -} - -impl Default for NetworkMessageStatus { - fn default() -> Self { - Self::NoStatus - } -} - -/// Unique struct for both 4nk messages and notification/key exchange, both rust and ts -/// 1. Faucet: commited_in with nothing else, status is NoStatus -/// 2. notification: -/// 1. sender: ciphertext, plaintext, commited_in, sender, recipient, shared_secret, key -/// 2. receiver (without tx): ciphertext -/// 3. receiver (tx without msg): commited_in, commitment, recipient, shared_secret -/// 4. receiver (receive tx after msg): plaintext, key, sender, commited_in, commitment, recipient, shared_secret -/// 5. receiver (msg after tx): ciphertext, key, plaintext, sender -/// 3. confirmation: -/// 1. receiver (spend the smallest vout that pays him in the first tx): confirmed_by -/// 2. sender (detect a transaction that pays him and spend commited_by): confirmed_by -/// 3. sender toggle status to complete when it spent confirmed_by, receiver when it detects the confirmed_by is spent -#[derive(Debug, Default, Serialize, Deserialize, Tsify, Clone)] -#[tsify(into_wasm_abi, from_wasm_abi)] -#[allow(non_camel_case_types)] -pub struct NetworkMessage { - pub id: u32, - pub status: NetworkMessageStatus, - pub ciphertext: Option, // When we receive message we can't decrypt we only have this and commited_in_tx - pub plaintext: Option, // Never None when message sent - pub commited_in: Option, - pub commitment: Option, // content of the op_return - pub sender: Option, // Never None when message sent - pub recipient: Option, // Never None when message sent - pub shared_secret: Option, // Never None when message sent - pub key: Option, // Never None when message sent - pub confirmed_by: Option, // If this None, Sender keeps sending - pub timestamp: u64, - pub error: Option, -} - -impl NetworkMessage { - pub fn new() -> Self { - let mut new = NetworkMessage::default(); - let mut buf = [0u8;4]; - thread_rng().fill_bytes(&mut buf); - new.id = u32::from_be_bytes(buf); - new - } - - pub fn new_error(error: String) -> Self { - let mut new = NetworkMessage::default(); - new.error = Some(error); - new - } - - pub fn try_decrypt_cipher(&self, cipher: Vec) -> Option> { - if self.ciphertext.is_some() || self.shared_secret.is_none() { - log::error!( - "Can't try decrypt this message, there's already a ciphertext or no shared secret" - ); - return None; - } - let mut shared_secret = [0u8; 32]; - shared_secret - .copy_from_slice(&Vec::from_hex(self.shared_secret.as_ref().unwrap()).unwrap()); - let aes_decrypt = Aes256Decryption::new(Purpose::Arbitrary, cipher, shared_secret); - - if aes_decrypt.is_err() { - log::error!("Failed to create decrypt object"); - return None; - } - - aes_decrypt.unwrap().decrypt_with_key().ok() - } - - pub fn try_decrypt_with_shared_secret(&self, shared_secret: [u8; 32]) -> Option> { - if self.ciphertext.is_none() || self.shared_secret.is_some() { - log::error!( - "Can't try decrypt this message, ciphertext is none or shared_secret already found" - ); - return None; - } - let cipher_bin = Vec::from_hex(self.ciphertext.as_ref().unwrap()); - if cipher_bin.is_err() { - let error = cipher_bin.unwrap_err(); - log::error!("Invalid hex in ciphertext: {}", error.to_string()); - return None; - } - let aes_decrypt = - Aes256Decryption::new(Purpose::Arbitrary, cipher_bin.unwrap(), shared_secret); - - if aes_decrypt.is_err() { - log::error!("Failed to create decrypt object"); - return None; - } - - aes_decrypt.unwrap().decrypt_with_key().ok() - } -} diff --git a/crates/sp_client/src/lib.rs b/crates/sp_client/src/lib.rs index 4980f18..3643833 100644 --- a/crates/sp_client/src/lib.rs +++ b/crates/sp_client/src/lib.rs @@ -1,7 +1,7 @@ #![allow(warnings)] use anyhow::Error; -use api::NetworkMessage; use sdk_common::crypto::AnkSharedSecret; +use sdk_common::network::CachedMessage; use serde::{Deserialize, Serialize}; use sp_client::bitcoin::{OutPoint, Txid}; use sp_client::silentpayments::sending::SilentPaymentAddress; @@ -17,10 +17,10 @@ mod peers; mod process; mod user; -pub static NETWORKMESSAGES: OnceLock>> = OnceLock::new(); +pub static CACHEDMESSAGES: OnceLock>> = OnceLock::new(); -pub fn lock_messages() -> Result>, Error> { - NETWORKMESSAGES +pub fn lock_messages() -> Result>, Error> { + CACHEDMESSAGES .get_or_init(|| Mutex::new(vec![])) .lock_anyhow() }