diff --git a/crates/sp_client/Cargo.toml b/crates/sp_client/Cargo.toml index f64543a..264bd2f 100644 --- a/crates/sp_client/Cargo.toml +++ b/crates/sp_client/Cargo.toml @@ -8,8 +8,8 @@ name = "sdk_client" crate-type = ["cdylib"] [dependencies] -# sp_client= { path = "../../../sp-client" } -sp_client= { git = "https://github.com/Sosthene00/sp-client", branch = "sp_client" } +sp_client= { path = "../../../sp-client" } +# sp_client= { git = "https://github.com/Sosthene00/sp-client", branch = "sp_client" } anyhow = "1.0" serde = { version = "1.0.188", features = ["derive"] } serde_json = "1.0" diff --git a/crates/sp_client/src/api.rs b/crates/sp_client/src/api.rs index 0eb433c..19c348c 100644 --- a/crates/sp_client/src/api.rs +++ b/crates/sp_client/src/api.rs @@ -1,12 +1,14 @@ use std::any::Any; +use std::borrow::Borrow; use std::collections::HashMap; use std::io::Write; use std::str::FromStr; use std::string::FromUtf8Error; use std::sync::{Mutex, OnceLock, PoisonError}; +use std::time::{Duration, Instant}; use log::{debug, warn}; -use rand::{Fill, Rng}; +use rand::{thread_rng, Fill, Rng, RngCore}; use anyhow::Error as AnyhowError; use sdk_common::crypto::{ @@ -16,10 +18,13 @@ use serde_json::{Error as SerdeJsonError, Value}; use shamir::SecretData; use sp_client::bitcoin::blockdata::fee_rate; use sp_client::bitcoin::consensus::{deserialize, serialize}; -use sp_client::bitcoin::hex::{parse, DisplayHex, FromHex, HexToBytesError}; +use sp_client::bitcoin::hashes::HashEngine; +use sp_client::bitcoin::hashes::{sha256, Hash}; +use sp_client::bitcoin::hex::{parse, DisplayHex, FromHex, HexToArrayError, HexToBytesError}; +use sp_client::bitcoin::key::Secp256k1; use sp_client::bitcoin::secp256k1::ecdh::shared_secret_point; use sp_client::bitcoin::secp256k1::{PublicKey, SecretKey}; -use sp_client::bitcoin::{Amount, OutPoint, Transaction, Txid}; +use sp_client::bitcoin::{Amount, Network, OutPoint, Psbt, Transaction, Txid}; use sp_client::silentpayments::Error as SpError; use serde::{Deserialize, Serialize}; @@ -28,16 +33,21 @@ use tsify::Tsify; use wasm_bindgen::convert::FromWasmAbi; use wasm_bindgen::prelude::*; -use sdk_common::network::{AnkFlag, AnkNetworkMsg, NewTxMessage, UnknownMessage}; +use sdk_common::network::{ + self, AnkFlag, AnkNetworkMsg, FaucetMessage, NewTxMessage, UnknownMessage, +}; use sdk_common::silentpayments::{ - check_transaction, create_transaction, create_transaction_for_address_with_shared_secret, + create_transaction, create_transaction_for_address_with_shared_secret, + create_transaction_spend_outpoint, map_outputs_to_sp_address }; -use sp_client::spclient::{derive_keys_from_seed, OutputList, OwnedOutput, SpClient}; +use sp_client::spclient::{ + derive_keys_from_seed, OutputList, OutputSpendStatus, OwnedOutput, Recipient, SpClient, +}; use sp_client::spclient::{SpWallet, SpendKey}; use crate::user::{lock_connected_user, User, UserWallets, CONNECTED_USER}; -use crate::{images, lock_scanned_transactions, lock_secrets, lock_watched, Txid2Secrets}; +use crate::{images, lock_messages}; use crate::process::Process; @@ -82,6 +92,30 @@ impl From for ApiError { } } +impl From for ApiError { + fn from(value: HexToArrayError) -> Self { + ApiError { + message: value.to_string(), + } + } +} + +impl From for ApiError { + fn from(value: sp_client::bitcoin::psbt::PsbtParseError) -> Self { + ApiError { + message: value.to_string(), + } + } +} + +impl From for ApiError { + fn from(value: sp_client::bitcoin::psbt::ExtractTxError) -> Self { + ApiError { + message: value.to_string(), + } + } +} + impl From for ApiError { fn from(value: sp_client::bitcoin::secp256k1::Error) -> Self { ApiError { @@ -331,112 +365,184 @@ pub fn login_user( Ok(res) } -pub fn scan_for_confirmation_transaction(tx_hex: String) -> anyhow::Result { - let tx = deserialize::(&Vec::from_hex(&tx_hex)?)?; +fn handle_recover_transaction( + updated: HashMap, + tx: &Transaction, + sp_wallet: &mut SpWallet, + tweak_data: PublicKey, + fee_rate: u32, +) -> anyhow::Result { + // We need to look for different case: + // 1) faucet + // This one is the simplest, we only care about finding the commitment + let op_return = tx.output.iter().find(|o| o.script_pubkey.is_op_return()); + let commitment = if op_return.is_none() { + vec![] + } else { + 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)); - for i in tx.input.iter() { - if let Some(waiting) = lock_watched()?.remove(&i.previous_output) { - match lock_secrets()?.get_mut(&waiting) { - None => return Err(anyhow::Error::msg("No secret match for an error we're waiting for")), - Some(secret) => { - // for now we only handle the case of one secret for one transaction - let res = secret.get_mut(0).unwrap(); - res.1.trusted = true; - return Ok(res.0.clone()); - } - } - } + if pos.is_some() { + let messages = lock_messages()?; + let message = messages.get(pos.unwrap()); + return Ok(message.cloned().unwrap()); } - Err(anyhow::Error::msg("Not spending a watched output")) -} + // If we got updates from a transaction, it means that it creates an output to us, spend an output we owned, or both + // If we destroyed outputs it means we either notified others, or ask confirmation, or confirm + // We probably creates outputs too in this case because of change + // If we only created outputs it means we are being notified + let utxo_destroyed: HashMap<&OutPoint, &OwnedOutput> = updated + .iter() + .filter(|(outpoint, output)| output.spend_status != OutputSpendStatus::Unspent) + .collect(); + let utxo_created: HashMap<&OutPoint, &OwnedOutput> = updated + .iter() + .filter(|(outpoint, output)| output.spend_status == OutputSpendStatus::Unspent) + .collect(); -fn handle_recover_transaction(tx: Transaction, sp_wallet: &mut SpWallet, tweak_data: PublicKey, fee_rate: u32) -> anyhow::Result> { - // does this transaction spent a txid and output we're waiting confirmation for? - let scan_sk = sp_wallet.get_client().get_scan_key(); - let txid = tx.txid(); - for input in tx.input { - let prevout = input.previous_output; - match lock_secrets()?.get_mut(&prevout.txid) { - None => { - continue; - } - Some(secret) => { - // We found an input spending a notification transaction we sent - if let Some(res) = secret.get_mut(prevout.vout as usize) { - // This is a challenge from a previous message we sent - // we toggle the trusted value - if !res.1.trusted { - res.1.trusted = true; - } else { - return Err(anyhow::Error::msg("Received a confirmation for a transaction we already confirmed")); + // 2) confirmation + // If the transaction spends one outpoint in `commited_in`, it means we are receiving a confirmation for a notification + // if we are receiver, then we must look for `confirmed_by` + // if we owned at least one input or no outputs, we can skip the check + if utxo_destroyed.is_empty() && !utxo_created.is_empty() { + for input in tx.input.iter() { + // Check for each input if it match a known commitment we made as a sender + // OR a confirmation for the receiver + let pos = lock_messages()?.iter().position(|m| { + m.commited_in == Some(input.previous_output) + || m.confirmed_by == Some(input.previous_output) + }); + if pos.is_some() { + let mut messages = lock_messages()?; + let message = messages.get_mut(pos.unwrap()).unwrap(); + // 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; + return Ok(message.clone()); + } + + // sender needs to spent it back again to receiver + let (outpoint, output) = utxo_created.iter().next().unwrap(); + + // If we are sender, then we must update the confirmed_by field + message.confirmed_by = Some(**outpoint); + + // Caller must interpret this message as "spend confirmed_by outpoint to receiver" + return Ok(message.clone()); + } else { + // we are being notified + let shared_point = + shared_secret_point(&tweak_data, &sp_wallet.get_client().get_scan_key()); + let shared_secret = AnkSharedSecret::new(shared_point); + + let mut messages = lock_messages()?; + let cipher_pos = messages.iter().position(|m| { + if m.status != NetworkMessageStatus::CipherWaitingTx { + return false; } - // We spend the output back to the receiver - let sp_address = SilentPaymentAddress::try_from(res.0.as_str()).expect("Invalid silent payment address"); - let response_tx = create_transaction(sp_address, sp_wallet, Amount::from_sat(fee_rate.into()))?; - return Ok(Some(response_tx)); + m.try_decrypt_with_shared_secret(shared_secret.to_byte_array()) + .is_some() + }); + + if cipher_pos.is_some() { + let message = messages.get_mut(cipher_pos.unwrap()).unwrap(); + let (outpoint, output) = utxo_created.iter().next().unwrap(); + message.commited_in = Some(**outpoint); + message.shared_secret = + Some(shared_secret.to_byte_array().to_lower_hex_string()); + message.commitment = Some(commitment.to_lower_hex_string()); + + let plaintext = message + .try_decrypt_with_shared_secret(shared_secret.to_byte_array()) + .unwrap(); + let unknown_msg: UnknownMessage = serde_json::from_slice(&plaintext)?; + message.plaintext = Some(unknown_msg.message); + message.sender = Some(unknown_msg.sender); + message.recipient = Some(sp_wallet.get_client().get_receiving_address()); + return Ok(message.clone()) } else { - return Err(anyhow::Error::msg("Received a confirmation from an umapped output")); + // 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); + 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; + lock_messages()?.push(new_msg.clone()); + return Ok(new_msg.clone()); } } } + unreachable!("Transaction with no inputs"); + } else { + // We are sender of a notification transaction + // We only need to return the message + if let Some(message) = lock_messages()?.iter() + .find(|m| { + m.commitment.as_ref() == Some(&commitment_str) + }) + { + return Ok(message.clone()); + } else { + return Err(anyhow::Error::msg("We spent a transaction for a commitment we don't know")); + } } - // If we exhausted all inputs without finding one of our transaction, it means it's a notification - let shared_point = - shared_secret_point(&tweak_data, &scan_sk); - lock_secrets()?.insert( - txid, - vec![("".to_owned(), AnkSharedSecret::new(shared_point, false))], - ); - Ok(None) } -#[wasm_bindgen] -pub fn check_transaction_for_silent_payments( +/// If the transaction has anything to do with us, we create/update the relevant `NetworkMessage` +/// and return it to caller for persistent storage +fn process_transaction( tx_hex: String, blockheight: u32, tweak_data_hex: String, fee_rate: u32, -) -> ApiResult { +) -> anyhow::Result { let tx = deserialize::(&Vec::from_hex(&tx_hex)?)?; - // check that we don't already have scanned the tx, and insert it if we don't - if !lock_scanned_transactions()?.insert(tx.txid()) { - return Err(ApiError { - message: "Transaction already scanned".to_owned(), - }); + // check that we don't already have scanned the tx + if let Some(_) = lock_messages()?.iter().find(|message| { + if let Some(outpoint) = message.commited_in { + if outpoint.txid == tx.txid() { + return true; + } + } + return false; + }) { + return Err(anyhow::Error::msg("Transaction already scanned")); } let tweak_data = PublicKey::from_str(&tweak_data_hex)?; let mut connected_user = lock_connected_user()?; if let Ok(recover) = connected_user.try_get_mut_recover() { - if let Ok(txid) = check_transaction(&tx, recover, blockheight, tweak_data) { - if let Err(e) = scan_for_confirmation_transaction(tx_hex) { - log::error!("{}", e); - handle_recover_transaction(tx, recover, tweak_data, fee_rate)?; - } - return Ok(txid); + let updated = recover.update_wallet_with_transaction(&tx, blockheight, tweak_data)?; + + if updated.len() > 0 { + let updated_msg = + handle_recover_transaction(updated, &tx, recover, tweak_data, fee_rate)?; + return Ok(updated_msg); } } if let Ok(main) = connected_user.try_get_mut_main() { - if let Ok(txid) = check_transaction(&tx, main, blockheight, tweak_data) { - // TODO - return Ok(txid); - } + let updated = main.update_wallet_with_transaction(&tx, blockheight, tweak_data)?; + unimplemented!(); } if let Ok(revoke) = connected_user.try_get_mut_revoke() { - if let Ok(txid) = check_transaction(&tx, revoke, blockheight, tweak_data) { - // TODO - return Ok(txid); - } + let updated = revoke.update_wallet_with_transaction(&tx, blockheight, tweak_data)?; + unimplemented!(); } - Err(ApiError { - message: "No output found".to_owned(), - }) + Err(anyhow::Error::msg("No output found")) } #[derive(Tsify, Serialize, Deserialize)] @@ -444,7 +550,7 @@ pub fn check_transaction_for_silent_payments( #[allow(non_camel_case_types)] pub struct parseNetworkMsgReturn { topic: String, - message: String, + message: NetworkMessage, } #[wasm_bindgen] @@ -458,80 +564,57 @@ 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: ank_msg.content.to_owned(), + message: error_msg, }) } AnkFlag::Unknown => { - // try to decrypt the cipher with all available keys - for (txid, secret_vec) in lock_secrets()?.iter_mut() { - // Actually we probably will ever have only one secret in the case we're receiver - for (shared_with, ank_secret) in secret_vec.iter_mut() { - // if we already have shared_with, that means we already used that key for another message - if !shared_with.is_empty() { continue } - let shared_secret = ank_secret.to_byte_array(); - debug!("{} {}", shared_with, shared_secret.to_lower_hex_string()); - let msg_decrypt = Aes256Decryption::new( - Purpose::Arbitrary, - Vec::from_hex(&ank_msg.content.trim_matches('\"'))?, - shared_secret, - )?; - match msg_decrypt.decrypt_with_key() { - Ok(plaintext) => { - let unknown_msg = serde_json::from_slice::(&plaintext); - if unknown_msg.is_err() { - // The message we were sent is invalid, drop everything - // for now let's just fill the shared_with with garbage - *shared_with = "a".to_owned(); - return Err(ApiError { message: "Invalid msg".to_owned() }) - } - let sender: Result = unknown_msg.unwrap().sender.try_into(); - if sender.is_err() { - // The sender is invalid address - *shared_with = "a".to_owned(); - return Err(ApiError { message: "Invalid sp address".to_owned() }) - } - - // we update our list with the sender address - *shared_with = sender.unwrap().into(); - - // We return the whole message - // ts is responsible for sending the confirmation message - return Ok(parseNetworkMsgReturn { - topic: AnkFlag::Unknown.as_str().to_owned(), - message: String::from_utf8(plaintext)?, - }); - } - Err(e) => { - debug!("{}", e); - debug!( - "Failed to decrypt message {} with key {}", - ank_msg.content, - shared_secret.to_lower_hex_string() - ); - } - } + // 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_pos = messages.iter().position(|m| { + if m.status != NetworkMessageStatus::TxWaitingCipher { + return false; } - } - // keep the message in cache, just in case? - // return an error - return Err(ApiError { - message: "No key found".to_owned(), + m.try_decrypt_cipher(cipher.clone()).is_some() }); + if cipher_pos.is_some() { + let mut message = messages.get_mut(cipher_pos.unwrap()).unwrap(); + let plain = message.try_decrypt_cipher(cipher).unwrap(); + let unknown_msg: UnknownMessage = serde_json::from_slice(&plain)?; + message.plaintext = Some(unknown_msg.message); + message.sender = Some(unknown_msg.sender); + message.ciphertext = Some(ank_msg.content); + return Ok(parseNetworkMsgReturn { + topic: AnkFlag::Unknown.as_str().to_owned(), + message: message.clone(), + }); + } else { + // let's keep it in case we receive the transaction later + let mut new_msg = NetworkMessage::default(); + new_msg.status = NetworkMessageStatus::CipherWaitingTx; + new_msg.ciphertext = Some(ank_msg.content); + messages.push(new_msg); + return Err(ApiError { + message: "Can't decrypt message".to_owned(), + }); + } } _ => unimplemented!(), } @@ -597,16 +680,20 @@ pub fn is_tx_owned_by_user(pre_id: String, tx: String) -> ApiResult { pub struct createNotificationTransactionReturn { pub txid: String, pub transaction: String, - pub address2secret: HashMap, + pub new_network_msg: NetworkMessage, } +/// This is what we call to confirm as a receiver #[wasm_bindgen] -pub fn create_notification_transaction( - recipient: String, - message: Option, +pub fn create_confirmation_transaction( + message: NetworkMessage, fee_rate: u32, ) -> ApiResult { - let sp_address: SilentPaymentAddress = recipient.try_into()?; + if message.sender.is_none() || message.confirmed_by.is_none() { + return Err(ApiError { message: "Invalid network message".to_owned() }); + } + + let sp_address: SilentPaymentAddress = message.sender.as_ref().unwrap().as_str().try_into()?; let connected_user = lock_connected_user()?; @@ -617,28 +704,97 @@ pub fn create_notification_transaction( sp_wallet = connected_user.try_get_main()?; } - let (transaction, shared_secret) = create_transaction_for_address_with_shared_secret( - sp_address, + let recipient = Recipient { + address: sp_address.into(), + amount: Amount::from_sat(1200), + 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(createNotificationTransactionReturn { + txid: final_tx.txid().to_string(), + transaction: serialize(&final_tx).to_lower_hex_string(), + new_network_msg: message + }) +} + +#[wasm_bindgen] +pub fn create_notification_transaction( + address: String, + commitment: Option, + fee_rate: u32, +) -> ApiResult { + let sp_address: SilentPaymentAddress = address.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(1200), + nb_outputs: 1, + }; + + let signed_psbt = create_transaction_for_address_with_shared_secret( + recipient, sp_wallet, - message, + commitment.as_deref(), Amount::from_sat(fee_rate.into()), )?; + let psbt = Psbt::from_str(&signed_psbt)?; + + let partial_secret = sp_wallet + .get_client() + .get_partial_secret_from_psbt(&psbt)?; + + let shared_point = shared_secret_point( + &sp_wallet + .get_client() + .get_scan_key() + .public_key(&Secp256k1::signing_only()), + &partial_secret, + ); + + let shared_secret = AnkSharedSecret::new(shared_point); + debug!( "Created transaction with secret {}", shared_secret.to_byte_array().to_lower_hex_string() ); - let mut address2secret: Vec<(String, AnkSharedSecret)> = vec![]; - address2secret.push((sp_address.into(), shared_secret)); - // update our cache - lock_secrets()?.insert(transaction.txid(), address2secret.clone()); + let sp_address2vouts = map_outputs_to_sp_address(&signed_psbt)?; + 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(); + 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()); + // plaintext and ciphertext to be added later when sending the encrypted message + lock_messages()?.push(new_msg.clone()); Ok(createNotificationTransactionReturn { - txid: transaction.txid().to_string(), - transaction: serialize(&transaction).to_lower_hex_string(), - address2secret: address2secret.into_iter().collect(), + txid: final_tx.txid().to_string(), + transaction: serialize(&final_tx).to_lower_hex_string(), + new_network_msg: new_msg, }) } @@ -710,6 +866,18 @@ pub fn try_decrypt_with_key(cipher: String, key: String) -> ApiResult { Ok(plain) } +#[wasm_bindgen] +pub fn create_faucet_msg() -> ApiResult { + let user = lock_connected_user()?; + 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(); + network_msg.commitment = Some(faucet_msg.commitment.clone()); + lock_messages()?.push(network_msg); + Ok(faucet_msg) +} + #[wasm_bindgen] pub fn create_commitment(payload_to_hash: String) -> String { let mut engine = sha256::HashEngine::default(); @@ -717,3 +885,110 @@ 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 4802a9f..4980f18 100644 --- a/crates/sp_client/src/lib.rs +++ b/crates/sp_client/src/lib.rs @@ -1,5 +1,6 @@ #![allow(warnings)] use anyhow::Error; +use api::NetworkMessage; use sdk_common::crypto::AnkSharedSecret; use serde::{Deserialize, Serialize}; use sp_client::bitcoin::{OutPoint, Txid}; @@ -16,37 +17,11 @@ mod peers; mod process; mod user; -/// We map txid with one or n secrets -/// Each secret match one sp address -/// When we first detect a transaction, we can't tell who's the sender, so we like sp address empty -/// When we receive the corresponding message, we get a sp address declaration, we complete here -/// Then when we send the confirmation transaction and got the response we can flip the secret to trusted -pub type Txid2Secrets = HashMap>; +pub static NETWORKMESSAGES: OnceLock>> = OnceLock::new(); -pub static SECRETCACHE: OnceLock> = OnceLock::new(); - -pub fn lock_secrets() -> Result, Error> { - SECRETCACHE - .get_or_init(|| Mutex::new(Txid2Secrets::new())) - .lock_anyhow() -} - -/// this is to keep track of transaction we already analysed without finding notification -/// This is not critical and there's no need to keep that in persistent storage, as most transactions would only show up twice -/// Worst case is we will scan again transactions when they got into a block -pub static TRANSACTIONCACHE: OnceLock>> = OnceLock::new(); - -pub fn lock_scanned_transactions() -> Result>, Error> { - TRANSACTIONCACHE - .get_or_init(|| Mutex::new(HashSet::new())) - .lock_anyhow() -} - -pub static WATCHEDUTXO: OnceLock>> = OnceLock::new(); - -pub fn lock_watched() -> Result>, Error> { - WATCHEDUTXO - .get_or_init(|| Mutex::new(HashMap::new())) +pub fn lock_messages() -> Result>, Error> { + NETWORKMESSAGES + .get_or_init(|| Mutex::new(vec![])) .lock_anyhow() } diff --git a/package.json b/package.json index 6dabb0b..ed3c7b3 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "build_wasm": "wasm-pack build --out-dir ../../dist/pkg ./crates/sp_client --target bundler", + "build_wasm": "wasm-pack build --out-dir ../../dist/pkg ./crates/sp_client --target bundler --dev", "start": "webpack serve", "build": "webpack" }, diff --git a/src/database.ts b/src/database.ts index 3c2fc30..d5eaa83 100644 --- a/src/database.ts +++ b/src/database.ts @@ -24,6 +24,11 @@ class Database { 'unique': true } }] + }, + AnkMessages: { + name: "messages", + options: {'keyPath': 'id'}, + indices: [] } } diff --git a/src/services.ts b/src/services.ts index 4d95df8..b5ca897 100644 --- a/src/services.ts +++ b/src/services.ts @@ -1,4 +1,4 @@ -import { createUserReturn, User, Process, createNotificationTransactionReturn, parse_network_msg, outputs_list, parseNetworkMsgReturn, FaucetMessage, AnkFlag, NewTxMessage, encryptWithNewKeyResult, AnkSharedSecret } from '../dist/pkg/sdk_client'; +import { createUserReturn, User, Process, createNotificationTransactionReturn, parse_network_msg, outputs_list, parseNetworkMsgReturn, FaucetMessage, AnkFlag, NewTxMessage, encryptWithNewKeyResult, AnkSharedSecret, NetworkMessage } from '../dist/pkg/sdk_client'; import IndexedDB from './database' import { WebSocketClient } from './websockets'; @@ -109,30 +109,39 @@ class Services { let notificationInfo = await services.notify_address_for_message(recipientSpAddress, msg_payload); if (notificationInfo) { + let networkMsg = notificationInfo.new_network_msg; + const msgId = notificationInfo.new_network_msg.id; + let shared_secret = ''; + if (networkMsg.shared_secret) { + shared_secret = networkMsg.shared_secret; + } else { + throw 'no shared_secret'; + } let ciphers: string[] = []; console.info('Successfully sent notification transaction'); // Save the secret to db + const indexedDb = await IndexedDB.getInstance(); + const db = await indexedDb.getDb(); + // encrypt the message(s) - for (const [address, ankSharedSecret] of Object.entries(notificationInfo.address2secret)) { - try { - let cipher = await services.encryptData(msg_payload, ankSharedSecret.secret); - ciphers.push(cipher); - } catch (error) { - throw error; - } + try { + const cipher = await services.encryptData(msg_payload, shared_secret); + let updated_msg = notificationInfo.new_network_msg; + updated_msg.plaintext = msg_payload; + updated_msg.ciphertext = cipher; + await indexedDb.writeObject(db, indexedDb.getStoreList().AnkMessages, updated_msg, null); + ciphers.push(cipher); + } catch (error) { + throw error; } const connection = await services.pickWebsocketConnectionRandom(); const flag: AnkFlag = "Unknown"; - // for testing we only take the first cipher - const payload = ciphers.at(0); - if (!payload) { - console.error("No payload"); - return; - } // add peers list // add processes list // send message (transaction in envelope) - connection?.sendMessage(flag, payload); + for (const payload of ciphers) { + connection?.sendMessage(flag, payload); + } } } @@ -397,19 +406,6 @@ class Services { } } - public async checkTransaction(tx: string, tweak_data: string, blkheight: number): Promise { - const services = await Services.getInstance(); - - try { - const txid = services.sdkClient.check_transaction_for_silent_payments(tx, blkheight, tweak_data); - await services.updateOwnedOutputsForUser(); - return txid; - } catch (error) { - console.error(error); - return null; - } - } - public async getAllProcessForUser(pre_id: string): Promise { const services = await Services.getInstance(); let user: User; @@ -447,6 +443,17 @@ class Services { return process; } + public async updateMessages(message: NetworkMessage): Promise { + const indexedDb = await IndexedDB.getInstance(); + const db = await indexedDb.getDb(); + + try { + await indexedDb.setObject(db, indexedDb.getStoreList().AnkMessages, message, null); + } catch (error) { + throw error; + } + } + public async updateProcesses(): Promise { const services = await Services.getInstance(); const processList: Process[] = services.sdkClient.get_processes(); @@ -758,10 +765,8 @@ class Services { return null; } try { - const flag: AnkFlag = "Faucet"; - const faucetMsg: FaucetMessage = { - 'sp_address': spaddress - } + const flag: AnkFlag = 'Faucet'; + const faucetMsg = services.sdkClient.create_faucet_msg(); connection.sendMessage(flag, JSON.stringify(faucetMsg)); } catch (error) { console.error("Failed to obtain tokens with relay ", connection.getUrl()); @@ -822,7 +827,7 @@ class Services { let notificationInfo: createNotificationTransactionReturn; try { const feeRate = 1; - notificationInfo = services.sdkClient.create_notification_transaction(sp_address, undefined, feeRate); + notificationInfo = services.sdkClient.create_notification_transaction(sp_address, null, feeRate); } catch (error) { throw new Error(`Failed to create confirmation transaction: ${error}`); } @@ -835,23 +840,11 @@ class Services { return; } - public async notify_address_for_message(sp_address: string, message: string): Promise { + public async notify_address_for_message(sp_address: string, message: string): Promise { const services = await Services.getInstance(); const connection = await services.pickWebsocketConnectionRandom(); if (!connection) { - return null; - } - let user: User; - try { - let possibleUser = await services.getUserInfo(); - if (!possibleUser) { - console.error("No user loaded, please first create a new user or login"); - return null; - } else { - user = possibleUser; - } - } catch (error) { - throw error; + throw 'No available connection'; } try { @@ -866,8 +859,7 @@ class Services { connection.sendMessage(flag, JSON.stringify(newTxMsg)); return notificationInfo; } catch (error) { - console.error("Failed to create notification transaction:", error); - return null + throw 'Failed to create notification transaction:", error'; } } diff --git a/src/websockets.ts b/src/websockets.ts index 0234f56..5067254 100644 --- a/src/websockets.ts +++ b/src/websockets.ts @@ -34,11 +34,14 @@ class WebSocketClient { window.alert(`New tx\n${res.message}`); await services.updateOwnedOutputsForUser(); } else if (res.topic === 'unknown') { - let parsed = JSON.parse(res.message); - let message = parsed['message']; - let sender = parsed['sender']; + let message = res.message['plaintext']; + let sender = res.message['sender']; + if (!message || !sender) { + throw 'Message missing plaintext and/or sender'; + } window.alert(`new message: ${message}\nAsking sender ${sender} to confirm identity...`); console.debug(`sending confirm message to ${sender}`); + await services.updateMessages(res.message); await services.confirm_sender_address(sender); } } catch (error) {