diff --git a/crates/sp_client/Cargo.toml b/crates/sp_client/Cargo.toml index 25f7965..3999e06 100644 --- a/crates/sp_client/Cargo.toml +++ b/crates/sp_client/Cargo.toml @@ -8,7 +8,6 @@ name = "sdk_client" crate-type = ["cdylib"] [dependencies] -sp_backend = { git = "https://github.com/Sosthene00/sp-backend", branch = "sp_client" } anyhow = "1.0" serde = { version = "1.0.188", features = ["derive"] } serde_json = "1.0" @@ -18,8 +17,8 @@ wasm-logger = "0.2.0" rand = "0.8.5" log = "0.4.6" tsify = { git = "https://github.com/Sosthene00/tsify", branch = "next" } -aes-gcm = "0.10.3" -aes = "0.8.3" +sdk_common = { path = "../../../sdk_common" } +#sdk_common = { git = "https://git.4nkweb.com/4nk/sdk_common.git", branch = "demo" } shamir = { git = "https://github.com/Sosthene00/shamir", branch = "master" } img-parts = "0.3.0" diff --git a/crates/sp_client/src/Prd_list.rs b/crates/sp_client/src/Prd_list.rs deleted file mode 100644 index e8e8185..0000000 --- a/crates/sp_client/src/Prd_list.rs +++ /dev/null @@ -1,73 +0,0 @@ -use serde::{Deserialize, Serialize}; -use serde_json::{json, Value}; -use sp_backend::bitcoin::PublicKey; -//use sp_backend::silentpayments::sending::SilentPaymentAddress; -use std::marker::Copy; -use tsify::Tsify; -use wasm_bindgen::prelude::*; - -#[wasm_bindgen] -#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Copy)] -pub enum Role { - Manager, - #[default] - User, -} - -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] -pub struct SilentPaymentAddress { - version: u8, - scan_pubkey: PublicKey, - m_pubkey: PublicKey, - is_testnet: bool, -} - -#[derive(Debug, Copy, Clone)] -pub struct ItemMember { - pub role: Role, - pub sp_address: SilentPaymentAddress, - //pre_id: hash(password, part1) - //shard, - //priv_key_mainnet_spend, (enc) - //priv_key_mainnet_scan, - //priv_key_signet_scan, -} - -impl ItemMember { - pub fn new(role: Role, sp_address: SilentPaymentAddress) -> Self { - ItemMember { role, sp_address } - } -} - -#[derive(Debug, Clone)] -pub struct Prdlist { - //pub id: String, - //pub version: String, - pub gestionnaires: Vec, - // pub gestionnaires: Box>, -} - -#[derive(Serialize)] -#[wasm_bindgen] -struct RequestBody { - message: String, -} - -pub fn send_PrdRequest(prdlist: &Prdlist) -> Result<(), JsValue> { - let managers: Vec<&ItemMember> = prdlist - .gestionnaires - .iter() - .filter(|m| m.role == Role::Manager) - .collect(); - for manager in managers { - let request_body = RequestBody { - message: "Asking for the Prd list".to_string(), - }; - - let json_body = serde_json::to_string(&request_body).map_err(|e| { - JsValue::from_str(&format!("Failed to serialize request body: {:?}", e)) - })?; - println!("Sending request to manager {:?}", manager.sp_address); - } - Ok(()) -} diff --git a/crates/sp_client/src/api.rs b/crates/sp_client/src/api.rs index ea6bd9e..16ddacc 100644 --- a/crates/sp_client/src/api.rs +++ b/crates/sp_client/src/api.rs @@ -1,32 +1,57 @@ 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 rand::Rng; +use log::{debug, warn}; +use rand::{thread_rng, Fill, Rng, RngCore}; use anyhow::Error as AnyhowError; -use serde_json::Error as SerdeJsonError; +use sdk_common::crypto::{ + AeadCore, Aes256Decryption, Aes256Encryption, Aes256Gcm, AnkSharedSecret, KeyInit, Purpose, +}; +use sdk_common::sp_client::bitcoin::blockdata::fee_rate; +use sdk_common::sp_client::bitcoin::consensus::{deserialize, serialize}; +use sdk_common::sp_client::bitcoin::hashes::HashEngine; +use sdk_common::sp_client::bitcoin::hashes::{sha256, Hash}; +use sdk_common::sp_client::bitcoin::hex::{ + parse, DisplayHex, FromHex, HexToArrayError, HexToBytesError, +}; +use sdk_common::sp_client::bitcoin::key::Secp256k1; +use sdk_common::sp_client::bitcoin::secp256k1::ecdh::shared_secret_point; +use sdk_common::sp_client::bitcoin::secp256k1::{PublicKey, SecretKey}; +use sdk_common::sp_client::bitcoin::{Amount, Network, OutPoint, Psbt, Transaction, Txid}; +use sdk_common::sp_client::silentpayments::utils as sp_utils; +use sdk_common::sp_client::silentpayments::{Error as SpError, Network as SpNetwork}; +use serde_json::{Error as SerdeJsonError, Value}; use shamir::SecretData; -use sp_backend::bitcoin::consensus::deserialize; -use sp_backend::bitcoin::hex::{FromHex, HexToBytesError}; -use sp_backend::bitcoin::secp256k1::{PublicKey, SecretKey}; -use sp_backend::bitcoin::{Transaction, Txid}; -use sp_backend::silentpayments::Error as SpError; +use sdk_common::sp_client::silentpayments::sending::SilentPaymentAddress; use serde::{Deserialize, Serialize}; -use sp_backend::silentpayments::sending::SilentPaymentAddress; use tsify::Tsify; use wasm_bindgen::convert::FromWasmAbi; use wasm_bindgen::prelude::*; -use sp_backend::spclient::{derive_keys_from_seed, OutputList, SpClient}; -use sp_backend::spclient::{SpWallet, SpendKey}; +use sdk_common::network::{ + self, AnkFlag, AnkNetworkMsg, CachedMessage, CachedMessageStatus, CipherMessage, FaucetMessage, + NewTxMessage, +}; +use sdk_common::silentpayments::{ + create_transaction, create_transaction_for_address_with_shared_secret, + create_transaction_spend_outpoint, map_outputs_to_sp_address, +}; -use crate::images; -use crate::network::{BitcoinNetworkMsg, BitcoinTopic, AnkNetworkMsg, AnkTopic}; -use crate::silentpayments::check_transaction; -use crate::user::{lock_connected_users, User, UserWallets, CONNECTED_USERS}; +use sdk_common::sp_client::spclient::{ + derive_keys_from_seed, OutputList, OutputSpendStatus, OwnedOutput, Recipient, SpClient, +}; +use sdk_common::sp_client::spclient::{SpWallet, SpendKey}; + +use crate::user::{lock_connected_user, User, UserWallets, CONNECTED_USER}; +use crate::{images, lock_messages, CACHEDMESSAGES}; use crate::process::Process; @@ -71,16 +96,48 @@ impl From for ApiError { } } -impl From for ApiError { - fn from(value: sp_backend::bitcoin::secp256k1::Error) -> Self { +impl From for ApiError { + fn from(value: HexToArrayError) -> Self { ApiError { message: value.to_string(), } } } -impl From for ApiError { - fn from(value: sp_backend::bitcoin::consensus::encode::Error) -> Self { +impl From for ApiError { + fn from(value: sdk_common::sp_client::bitcoin::psbt::PsbtParseError) -> Self { + ApiError { + message: value.to_string(), + } + } +} + +impl From for ApiError { + fn from(value: sdk_common::sp_client::bitcoin::psbt::ExtractTxError) -> Self { + ApiError { + message: value.to_string(), + } + } +} + +impl From for ApiError { + fn from(value: sdk_common::sp_client::bitcoin::secp256k1::Error) -> Self { + ApiError { + message: value.to_string(), + } + } +} + +impl From for ApiError { + fn from(value: sdk_common::sp_client::bitcoin::consensus::encode::Error) -> Self { + ApiError { + message: value.to_string(), + } + } +} + +impl From for ApiError { + fn from(value: FromUtf8Error) -> Self { ApiError { message: value.to_string(), } @@ -134,9 +191,26 @@ pub fn generate_sp_wallet( } #[wasm_bindgen] -pub fn get_receiving_address(pre_id: String) -> ApiResult { - if let Some(my_wallets) = lock_connected_users()?.get(&pre_id) { - Ok(my_wallets.recover.get_client().get_receiving_address()) +pub fn get_recover_address() -> ApiResult { + if let Ok(my_wallets) = lock_connected_user() { + Ok(my_wallets + .try_get_recover()? + .get_client() + .get_receiving_address()) + } else { + Err(ApiError { + message: "Unknown user pre_id".to_owned(), + }) + } +} + +#[wasm_bindgen] +pub fn get_main_address() -> ApiResult { + if let Ok(my_wallets) = lock_connected_user() { + Ok(my_wallets + .try_get_main()? + .get_client() + .get_receiving_address()) } else { Err(ApiError { message: "Unknown user pre_id".to_owned(), @@ -161,7 +235,7 @@ pub fn create_user( let user_wallets = UserWallets::new( Some(sp_wallet_main), - sp_wallet_recover, + Some(sp_wallet_recover), Some(sp_wallet_revoke), ); @@ -169,7 +243,9 @@ pub fn create_user( let outputs = user_wallets.get_all_outputs(); - lock_connected_users()?.insert(user.pre_id.clone(), user_wallets); + // Setting CONNECTED_USER to user + let mut connected_user = lock_connected_user()?; + *connected_user = user_wallets; let generate_user = createUserReturn { user, @@ -197,60 +273,38 @@ pub struct get_process_return(Vec); #[wasm_bindgen] pub fn get_processes() -> ApiResult { - let number_managers: u8 = 5; - - let birthday_signet = 50000; - let mut members: Vec = Vec::with_capacity((number_managers) as usize); - - for _ in 0..number_managers { - //add sp_client - let sp_wallet = generate_sp_wallet(None, birthday_signet, true)?; - let sp_address = sp_wallet.get_client().get_receiving_address(); - members.push(sp_address); - } + let MEMBERS: [String;5] = [ + "tsp1qqdvmxycf3c3tf2qhpev0npx25rj05270d6j2pcsrfk2qn5gdy0rpwq6hd9u9sztl3fwmrzzqafzl3ymkq86aqfz5jl5egdkz72tqmhcnrswdz3pk".to_owned(), + "tsp1qqwafwn7dcr9d6ta0w8fjtd9s53u72x9qmmtgd8adqr7454xl90a5jq3vw23l2x8ypt55nrg7trl9lwz5xr5j357ucu4sf9rfmvc0zujcpqcps6rm".to_owned(), + "tsp1qqw02t5hmg5rxpjdkmjdnnmhvuc76wt6vlqdmn2zafnh6axxjd6e2gqcz04gzvnkzf572mur8spyx2a2s8sqzll2ymdpyz59cpl96j4zuvcdvrzxz".to_owned(), + "tsp1qqgpay2r5jswm7vcv24xd94shdf90w30vxtql9svw7qnlnrzd6xt02q7s7z57uw0sssh6c0xddcrryq4mxup93jsh3gfau3autrawl8umkgsyupkm".to_owned(), + "tsp1qqtsqmtgnxp0lsmnxyxcq52zpgxwugwlq8urlprs5pr5lwyqc789gjqhx5qra6g4rszsq43pms6nguee2l9trx905rk5sgntek05hnf7say4ru69y".to_owned(), + ]; //instances of process let process1 = Process { - id: 1, - name: String::from("CREATE_ID"), + id: 6, + name: String::from("Messaging"), version: String::from("1.0"), - members: members.clone(), - html: crate::process::HTML_CREATE_ID.to_owned(), + members: MEMBERS.to_vec(), + html: crate::process::HTML_MESSAGING.to_owned(), style: crate::process::CSS.to_owned(), script: "".to_owned(), }; let process2 = Process { - id: 2, - name: String::from("UPDATE_ID"), + id: 7, + name: String::from("Kotpart"), version: String::from("1.0"), - members: members.clone(), - html: crate::process::HTML_UPDATE_ID.to_owned(), - style: crate::process::CSSUPDATE.to_owned(), - script: crate::process::JSUPDATE.to_owned(), + members: MEMBERS.to_vec(), + html: crate::process::HTML_MESSAGING.to_owned(), + style: crate::process::CSS.to_owned(), + script: "".to_owned(), }; let process3 = Process { - id: 3, - name: String::from("RECOVER"), + id: 8, + name: String::from("Storage"), version: String::from("1.0"), - members: members.clone(), - html: crate::process::HTML_RECOVER.to_owned(), - style: crate::process::CSS.to_owned(), - script: "".to_owned(), - }; - let process4 = Process { - id: 4, - name: String::from("REVOKE_IMAGE"), - version: String::from("1.0"), - members: members.clone(), - html: crate::process::HTML_REVOKE_IMAGE.to_owned(), - style: crate::process::CSS.to_owned(), - script: "".to_owned(), - }; - let process5 = Process { - id: 5, - name: String::from("REVOKE"), - version: String::from("1.0"), - members: members.clone(), - html: crate::process::HTML_REVOKE.to_owned(), + members: MEMBERS.to_vec(), + html: crate::process::HTML_MESSAGING.to_owned(), style: crate::process::CSS.to_owned(), script: "".to_owned(), }; @@ -260,8 +314,6 @@ pub fn get_processes() -> ApiResult { data_process.push(process1); data_process.push(process2); data_process.push(process3); - data_process.push(process4); - data_process.push(process5); Ok(get_process_return(data_process)) } @@ -288,7 +340,7 @@ impl shamir_shares { } #[derive(Debug, Tsify, Serialize, Deserialize)] -#[tsify(from_wasm_abi)] +#[tsify(from_wasm_abi, into_wasm_abi)] #[allow(non_camel_case_types)] pub struct outputs_list(Vec); @@ -317,54 +369,665 @@ pub fn login_user( Ok(res) } -#[wasm_bindgen] -pub fn check_transaction_for_silent_payments( - tx_hex: String, - tweak_data_hex: String, -) -> ApiResult<()> { - let tx = deserialize::(&Vec::from_hex(&tx_hex)?)?; - let tweak_data = PublicKey::from_str(&tweak_data_hex)?; +fn handle_recover_transaction( + updated: HashMap, + tx: &Transaction, + sp_wallet: &mut SpWallet, + tweak_data: PublicKey, + fee_rate: u32, +) -> anyhow::Result { + 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(); - check_transaction(tx, tweak_data); + // If we got updates from a transaction, it means that it creates an output to us, spend an output we owned, or both + // Basically a transaction that destroyed utxo is a transaction we sent. + 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(); - Ok(()) -} + let mut messages = lock_messages()?; -#[wasm_bindgen] -pub fn parse_bitcoin_network_msg(msg: Vec) -> ApiResult<()> { - let parsed_msg = BitcoinNetworkMsg::new(&msg)?; - - match parsed_msg.topic { - BitcoinTopic::RawTx => { - let tx = deserialize::(parsed_msg.data)?; - let tweak_data = PublicKey::from_slice(parsed_msg.addon)?; - check_transaction(tx, tweak_data); - } - BitcoinTopic::RawBlock => (), - } - - Ok(()) -} - -#[wasm_bindgen] -pub fn parse_4nk_msg(raw: String) -> Option{ - if let Ok(msg) = AnkNetworkMsg::new(&raw) { - match msg.topic { - AnkTopic::Faucet => { - match Txid::from_str(msg.content) { - Ok(txid) => { - // return the txid for verification - Some(txid.to_string()) - }, - Err(e) => { - log::error!("Invalid txid with a \"faucet\" message: {}", e.to_string()); - None - } + // empty utxo_destroyed means we received this transaction + if utxo_destroyed.is_empty() { + // We first check for faucet transactions + if let Some(pos) = messages.iter().position(|m| { + if m.status == CachedMessageStatus::FaucetWaiting { + m.commitment.as_ref() == Some(&commitment_str) + } else { + false + } + }) { + let message = messages.get_mut(pos).unwrap(); + match message.status { + CachedMessageStatus::FaucetWaiting => { + message.status = CachedMessageStatus::FaucetComplete; + message.commited_in = utxo_created + .into_iter() + .next() + .map(|(outpoint, _)| *outpoint); + return Ok(message.clone()); } + // Actually this is unreachable + CachedMessageStatus::FaucetComplete => return Ok(message.clone()), + _ => (), } } + + // we inspect inputs looking for links with previous tx + for input in tx.input.iter() { + if let Some(pos) = messages.iter().position(|m| { + debug!("{:?}", Some(input.previous_output)); + m.confirmed_by == Some(input.previous_output) + }) { + let message = messages.get_mut(pos).unwrap(); + // If we are receiver, that's pretty much it, just set status to complete + message.status = CachedMessageStatus::Complete; + return Ok(message.clone()); + } else if let Some(pos) = messages + .iter() + .position(|m| m.commited_in == Some(input.previous_output)) + { + // sender needs to spent it back again to receiver + let (outpoint, output) = utxo_created.into_iter().next().unwrap(); + + let message = messages.get_mut(pos).unwrap(); + + message.confirmed_by = Some(outpoint.clone()); + message.status = CachedMessageStatus::MustSpendConfirmation; + + // Caller must interpret this message as "do spend confirmed_by outpoint to receiver" + return Ok(message.clone()); + } + } + + // if we've found nothing we are being notified + let shared_point = sp_utils::receiving::calculate_shared_point( + &tweak_data, + &sp_wallet.get_client().get_scan_key(), + ); + let shared_secret = AnkSharedSecret::new(PublicKey::from_slice(&shared_point)?); + + debug!( + "Shared secret: {}", + shared_secret.to_byte_array().to_lower_hex_string() + ); + + let mut plaintext: Vec = vec![]; + if let Some(cipher_pos) = messages.iter().position(|m| { + if m.status != CachedMessageStatus::CipherWaitingTx { + return false; + } + let res = m.try_decrypt_with_shared_secret(shared_secret.to_byte_array()); + if res.is_ok() { + plaintext = res.unwrap(); + return true; + } else { + return false; + } + }) { + let message = messages.get_mut(cipher_pos).unwrap(); + + let (outpoint, output) = utxo_created.into_iter().next().unwrap(); + + let cipher_msg: CipherMessage = serde_json::from_slice(&plaintext)?; + message.commited_in = Some(outpoint.clone()); + message.shared_secret = Some(shared_secret.to_byte_array().to_lower_hex_string()); + message.commitment = Some(commitment_str); + message.plaintext = Some(cipher_msg.message); + message.sender = Some(cipher_msg.sender); + message.recipient = Some(sp_wallet.get_client().get_receiving_address()); + message.status = CachedMessageStatus::ReceivedMustConfirm; + + return Ok(message.clone()); + } else { + // store it and wait for the message + let mut new_msg = CachedMessage::new(); + 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 = CachedMessageStatus::TxWaitingCipher; + messages.push(new_msg.clone()); + return Ok(new_msg.clone()); + } } else { - log::debug!("Can't parse message as a valid 4nk message: {}", raw); - None + // We are sender of a transaction + // We only need to return the message + // eiter this is notification, a challenge, or response to a challenge + // if notification, commitment is the same than in the message + // if challenge or response, commitment is H(commitment | b_scan), b_scan being different depending on who we are + if let Some(message) = messages.iter().find(|m| { + if commitment.is_empty() || m.commitment.is_none() { + return false; + } + match m.status { + CachedMessageStatus::SentWaitingConfirmation => { + // commitment we're looking for is simply what's in the message + m.commitment + .as_ref() + .map(|c| Vec::from_hex(&c).unwrap()) + .unwrap() + == commitment + } + CachedMessageStatus::MustSpendConfirmation + | CachedMessageStatus::ReceivedMustConfirm => { + // we compute the potential commitment + let m_commitment = m + .commitment + .as_ref() + .map(|c| Vec::from_hex(&c).unwrap()) + .unwrap(); + let mut buf = [0u8; 64]; + buf[..32].copy_from_slice(&m_commitment); + buf[32..] + .copy_from_slice(&sp_wallet.get_client().get_scan_key().secret_bytes()); + + let mut engine = sha256::HashEngine::default(); + engine.write_all(&buf).unwrap(); + let hash = sha256::Hash::from_engine(engine); + hash.to_byte_array().to_vec() == commitment + } + _ => return false, + } + }) { + return Ok(message.clone()); + } else { + return Err(anyhow::Error::msg( + "We spent a transaction for a commitment we don't know", + )); + } } } + +/// 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 { + let tx = deserialize::(&Vec::from_hex(&tx_hex)?)?; + + 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() { + 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() { + let updated = main.update_wallet_with_transaction(&tx, blockheight, tweak_data)?; + if updated.len() > 0 { + unimplemented!(); + } + } + + if let Ok(revoke) = connected_user.try_get_mut_revoke() { + let updated = revoke.update_wallet_with_transaction(&tx, blockheight, tweak_data)?; + if updated.len() > 0 { + unimplemented!(); + } + } + + Err(anyhow::Error::msg("No output found")) +} + +fn process_new_tx_error(msg: NewTxMessage) -> anyhow::Result { + // how do we match this error with the cached message? + unimplemented!(); +} + +#[wasm_bindgen] +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 => { + let tx_message = serde_json::from_str::(&ank_msg.content)?; + if let Some(ref error) = tx_message.error { + // Transaction failed to broadcast + // we can retry later or check the availability of our spent output, depending on the actual error + // we should probably look up the cached message and record the error + log::error!("{}", error); + // let updated = process_new_tx_error(tx_message)?; + let updated = CachedMessage::new(); + return Ok(updated); + } + if tx_message.tweak_data.is_none() { + return Err(ApiError { + message: "Missing tweak_data".to_owned(), + }); + } + let network_msg = process_transaction( + tx_message.transaction, + 0, + tx_message.tweak_data.unwrap(), + fee_rate, + )?; + return Ok(network_msg); + } + AnkFlag::Faucet => { + let faucet_msg = serde_json::from_str::(&ank_msg.content)?; + if let Some(error) = faucet_msg.error { + debug!("Faucet msg returned with an error: {}", error); + } + unimplemented!(); + } + AnkFlag::Cipher => { + // 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.trim_matches('\"'))?; + let cipher_pos = messages.iter().position(|m| { + debug!("Trying message: {:?}", m); + if m.status != CachedMessageStatus::TxWaitingCipher { + return false; + } + m.try_decrypt_cipher(cipher.clone()).is_ok() + }); + if cipher_pos.is_some() { + let mut message = messages.get_mut(cipher_pos.unwrap()).unwrap(); + let plain = message.try_decrypt_cipher(cipher).unwrap(); + let cipher_msg: CipherMessage = serde_json::from_slice(&plain)?; + message.plaintext = Some(cipher_msg.message); + message.sender = Some(cipher_msg.sender); + message.ciphertext = Some(ank_msg.content); + message.status = CachedMessageStatus::ReceivedMustConfirm; + return Ok(message.clone()); + } else { + // let's keep it in case we receive the transaction later + let mut new_msg = CachedMessage::new(); + new_msg.status = CachedMessageStatus::CipherWaitingTx; + new_msg.ciphertext = Some(ank_msg.content); + messages.push(new_msg.clone()); + return Ok(new_msg); + } + } + _ => unimplemented!(), + } + } else { + Err(ApiError { + message: format!("Can't parse message as a valid 4nk message: {}", raw), + }) + } +} + +#[wasm_bindgen] +pub fn get_outpoints_for_user() -> ApiResult { + let connected_user = lock_connected_user()?; + if connected_user.is_not_empty() { + Ok(outputs_list(connected_user.get_all_outputs())) + } else { + Err(ApiError { + message: "No user logged in".to_owned(), + }) + } +} + +#[wasm_bindgen] +pub fn get_available_amount_for_user(recover: bool) -> ApiResult { + let connected_user = lock_connected_user()?; + if recover { + if let Ok(recover_wallet) = connected_user.try_get_recover() { + Ok(recover_wallet.get_outputs().get_balance().to_sat()) + } else { + Err(ApiError { + message: "User doesn't have recover wallet available".to_owned(), + }) + } + } else { + Err(ApiError { + message: "No user logged in".to_owned(), + }) + } +} + +#[wasm_bindgen] +pub fn is_tx_owned_by_user(pre_id: String, tx: String) -> ApiResult { + let transaction = deserialize::(&Vec::from_hex(&tx)?)?; + let txid = transaction.txid(); + let connected_user = lock_connected_user()?; + + if let Some(_) = connected_user + .try_get_recover()? + .get_outputs() + .to_outpoints_list() + .iter() + .find(|(outpoint, output)| outpoint.txid == txid) + { + Ok(true) + } else { + Ok(false) + } +} + +#[derive(Tsify, Serialize, Deserialize)] +#[tsify(into_wasm_abi, from_wasm_abi)] +#[allow(non_camel_case_types)] +pub struct createTransactionReturn { + pub txid: String, + pub transaction: String, + 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_id: u32, + fee_rate: u32, +) -> ApiResult { + let mut messages = lock_messages()?; + let message: &mut CachedMessage; + if let Some(m) = messages.iter_mut().find(|m| m.id == message_id) { + if m.sender.is_none() || m.commited_in.is_none() { + return Err(ApiError { + message: "Invalid network message".to_owned(), + }); + } + + message = m; + } else { + return Err(ApiError { + message: format!("Can't find message for id {}", message_id), + }); + } + + 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.get_network() != SpNetwork::Mainnet { + 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 confirmed_by = message.confirmed_by.clone().unwrap(); + let commited_in = message.commited_in.clone().unwrap(); + + let signed_psbt = create_transaction_spend_outpoint( + &confirmed_by, + sp_wallet, + recipient, + &commited_in.txid, + Amount::from_sat(fee_rate.into()), + )?; + + let final_tx = signed_psbt.extract_tx()?; + + message.status = CachedMessageStatus::Complete; + + Ok(createTransactionReturn { + txid: final_tx.txid().to_string(), + transaction: serialize(&final_tx).to_lower_hex_string(), + new_network_msg: message.clone(), + }) +} + +/// This is what we call to confirm as a receiver +#[wasm_bindgen] +pub fn create_confirmation_transaction( + message_id: u32, + fee_rate: u32, +) -> ApiResult { + let mut messages = lock_messages()?; + let message: &mut CachedMessage; + if let Some(m) = messages.iter_mut().find(|m| m.id == message_id) { + if m.sender.is_none() || m.commited_in.is_none() { + return Err(ApiError { + message: "Invalid network message".to_owned(), + }); + } + + message = m; + } else { + return Err(ApiError { + message: format!("Can't find message for id {}", message_id), + }); + } + + let sp_address: SilentPaymentAddress = message.sender.as_ref().unwrap().as_str().try_into()?; + let connected_user = lock_connected_user()?; + + let sp_wallet: &SpWallet; + if sp_address.get_network() != SpNetwork::Mainnet { + 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), + nb_outputs: 1, + }; + + let commited_in = message.commited_in.clone().unwrap(); + + let signed_psbt = create_transaction_spend_outpoint( + &commited_in, + sp_wallet, + recipient, + &commited_in.txid, + Amount::from_sat(fee_rate.into()), + )?; + + // what's the vout of the output sent to sender? + let sp_address2vouts = map_outputs_to_sp_address(&signed_psbt.to_string())?; + let recipients_vouts = sp_address2vouts + .get::(&sp_address.into()) + .expect("recipients didn't change") + .as_slice(); + + let final_tx = signed_psbt.extract_tx()?; + + message.confirmed_by = Some(OutPoint { + txid: final_tx.txid(), + vout: recipients_vouts[0] as u32, + }); + + Ok(createTransactionReturn { + txid: final_tx.txid().to_string(), + transaction: serialize(&final_tx).to_lower_hex_string(), + new_network_msg: message.clone(), + }) +} + +#[wasm_bindgen] +pub fn create_notification_transaction( + address: String, + message: CipherMessage, + 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.get_network() != SpNetwork::Mainnet { + 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 commitment = create_commitment(serde_json::to_string(&message)?); + + let signed_psbt = create_transaction_for_address_with_shared_secret( + recipient, + sp_wallet, + Some(&commitment), + 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 = + sp_utils::sending::calculate_shared_point(&sp_address.get_scan_key(), &partial_secret); + + let shared_secret = AnkSharedSecret::new(PublicKey::from_slice(&shared_point)?); + + debug!( + "Created transaction with secret {}", + shared_secret.to_byte_array().to_lower_hex_string() + ); + + let cipher = encrypt_with_key( + serde_json::to_string(&message)?, + shared_secret.to_byte_array().to_lower_hex_string(), + )?; + + // update our cache + 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 = CachedMessage::new(); + new_msg.plaintext = Some(message.message); + new_msg.ciphertext = Some(cipher); + new_msg.commitment = Some(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; + lock_messages()?.push(new_msg.clone()); + + Ok(createTransactionReturn { + txid: final_tx.txid().to_string(), + transaction: serialize(&final_tx).to_lower_hex_string(), + new_network_msg: new_msg, + }) +} + +#[derive(Tsify, Serialize, Deserialize)] +#[tsify(into_wasm_abi, from_wasm_abi)] +#[allow(non_camel_case_types)] +pub struct encryptWithNewKeyResult { + pub cipher: String, + pub key: String, +} + +#[wasm_bindgen] +pub fn encrypt_with_key(plaintext: String, key: String) -> ApiResult { + let nonce = Aes256Gcm::generate_nonce(&mut rand::thread_rng()); + + let mut aes_key = [0u8; 32]; + aes_key.copy_from_slice(&Vec::from_hex(&key)?); + + // encrypt + let aes_enc = Aes256Encryption::import_key( + Purpose::Arbitrary, + plaintext.into_bytes(), + aes_key, + nonce.into(), + )?; + + let cipher = aes_enc.encrypt_with_aes_key()?; + + Ok(cipher.to_lower_hex_string()) +} + +#[wasm_bindgen] +pub fn encrypt_with_new_key(plaintext: String) -> ApiResult { + let mut rng = rand::thread_rng(); + + // generate new key + let aes_key = Aes256Gcm::generate_key(&mut rng); + let nonce = Aes256Gcm::generate_nonce(&mut rng); + + // encrypt + let aes_enc = Aes256Encryption::import_key( + Purpose::Arbitrary, + plaintext.into_bytes(), + aes_key.into(), + nonce.into(), + )?; + + let cipher = aes_enc.encrypt_with_aes_key()?; + + Ok(encryptWithNewKeyResult { + cipher: cipher.to_lower_hex_string(), + key: aes_key.to_lower_hex_string(), + }) +} + +#[wasm_bindgen] +pub fn try_decrypt_with_key(cipher: String, key: String) -> ApiResult { + let key_bin = Vec::from_hex(&key)?; + if key_bin.len() != 32 { + return Err(ApiError { + message: "key of invalid lenght".to_owned(), + }); + } + let mut aes_key = [0u8; 32]; + aes_key.copy_from_slice(&Vec::from_hex(&key)?); + let aes_dec = Aes256Decryption::new(Purpose::Arbitrary, Vec::from_hex(&cipher)?, aes_key)?; + + let plain = String::from_utf8(aes_dec.decrypt_with_key()?)?; + 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 mut commitment = [0u8; 64]; + thread_rng().fill_bytes(&mut commitment); + + let mut cached_msg = CachedMessage::new(); + cached_msg.recipient = Some(sp_address); + cached_msg.commitment = Some(commitment.to_lower_hex_string()); + cached_msg.status = CachedMessageStatus::FaucetWaiting; + lock_messages()?.push(cached_msg.clone()); + Ok(cached_msg) +} + +#[wasm_bindgen] +pub fn create_commitment(payload_to_hash: String) -> String { + let mut engine = sha256::HashEngine::default(); + engine.write_all(&payload_to_hash.as_bytes()); + let hash = sha256::Hash::from_engine(engine); + hash.to_byte_array().to_lower_hex_string() +} diff --git a/crates/sp_client/src/crypto.rs b/crates/sp_client/src/crypto.rs deleted file mode 100644 index 59ab295..0000000 --- a/crates/sp_client/src/crypto.rs +++ /dev/null @@ -1,447 +0,0 @@ -use std::collections::HashMap; - -use anyhow::{Error, Result}; -use sp_backend::{ - bitcoin::{ - consensus::serde::hex, - hex::DisplayHex, - key::constants::SECRET_KEY_SIZE, - secp256k1::{ecdh::SharedSecret, SecretKey}, - Txid, - }, - silentpayments::sending::SilentPaymentAddress, -}; -use wasm_bindgen::JsValue; - -use serde::{Deserialize, Serialize}; -use serde_json::{json, Value}; - -use aes::cipher::generic_array::GenericArray; -use aes::{ - cipher::consts::{U32, U8}, - Aes256, -}; -use aes_gcm::{ - aead::{Aead, AeadInPlace, KeyInit, Nonce}, - AeadCore, Aes256Gcm, AesGcm, Key, TagSize, -}; -use rand::{thread_rng, RngCore}; - -const HALFKEYSIZE: usize = SECRET_KEY_SIZE / 2; - -const THIRTYTWO: usize = 32; - -pub struct HalfKey([u8; HALFKEYSIZE]); - -impl TryFrom> for HalfKey { - type Error = anyhow::Error; - fn try_from(value: Vec) -> std::prelude::v1::Result { - if value.len() == HALFKEYSIZE { - let mut buf = [0u8; HALFKEYSIZE]; - buf.copy_from_slice(&value); - Ok(HalfKey(buf)) - } else { - Err(Error::msg("Invalid length for HalfKey")) - } - } -} - -impl HalfKey { - pub fn as_slice(&self) -> &[u8] { - &self.0 - } - - pub fn to_inner(&self) -> Vec { - self.0.to_vec() - } -} - -pub enum Purpose { - Login, - ThirtyTwoBytes, -} - -pub type CipherText = Vec; - -pub type EncryptedKey = Vec; - -pub struct Aes256Decryption { - pub purpose: Purpose, - cipher_text: CipherText, - aes_key: [u8; 32], - nonce: [u8; 12], -} - -impl Aes256Decryption { - pub fn new( - purpose: Purpose, - cipher_text: CipherText, - encrypted_aes_key: Vec, // If shared_secret is none this is actually the aes_key - shared_secret: Option, // We don't need that for certain purpose, like Login - ) -> Result { - let mut aes_key = [0u8; 32]; - if let Some(shared_secret) = shared_secret { - if encrypted_aes_key.len() <= 12 { - return Err(Error::msg("encrypted_aes_key is shorter than nonce length")); - } // Actually we could probably test that if the remnant is not a multiple of 32, something's wrong - // take the first 12 bytes form encrypted_aes_key as nonce - let (decrypt_key_nonce, encrypted_key) = encrypted_aes_key.split_at(12); - // decrypt key with shared_secret obtained from transaction - let decrypt_key_cipher = Aes256Gcm::new_from_slice(shared_secret.as_ref()) - .map_err(|e| Error::msg(format!("{}", e)))?; - let aes_key_plain = decrypt_key_cipher - .decrypt(decrypt_key_nonce.into(), encrypted_key) - .map_err(|e| Error::msg(format!("{}", e)))?; - if aes_key_plain.len() != 32 { - return Err(Error::msg("Invalid length for decrypted key")); - } - aes_key.copy_from_slice(&aes_key_plain); - } else { - if encrypted_aes_key.len() != 32 { - return Err(Error::msg("Invalid length for decrypted key")); - } - aes_key.copy_from_slice(&encrypted_aes_key); - } - if cipher_text.len() <= 12 { - return Err(Error::msg("cipher_text is shorter than nonce length")); - } - let (message_nonce, message_cipher) = cipher_text.split_at(12); - let mut nonce = [0u8; 12]; - nonce.copy_from_slice(message_nonce); - Ok(Self { - purpose, - cipher_text: message_cipher.to_vec(), - aes_key, - nonce, - }) - } - - pub fn decrypt_with_key(&self) -> Result> { - match self.purpose { - Purpose::Login => { - let half_key = self.decrypt_login()?; - Ok(half_key.to_inner()) - } - Purpose::ThirtyTwoBytes => { - let thirty_two_buf = self.decrypt_thirty_two()?; - Ok(thirty_two_buf.to_vec()) - } - } - } - - fn decrypt_login(&self) -> Result { - let cipher = Aes256Gcm::new(&self.aes_key.into()); - let plain = cipher - .decrypt(&self.nonce.into(), &*self.cipher_text) - .map_err(|e| Error::msg(format!("{}", e)))?; - if plain.len() != SECRET_KEY_SIZE / 2 { - return Err(Error::msg("Plain text of invalid lenght for a login")); - } - let mut key_half = [0u8; SECRET_KEY_SIZE / 2]; - key_half.copy_from_slice(&plain); - Ok(HalfKey(key_half)) - } - - fn decrypt_thirty_two(&self) -> Result<[u8; THIRTYTWO]> { - let cipher = Aes256Gcm::new(&self.aes_key.into()); - let plain = cipher - .decrypt(&self.nonce.into(), &*self.cipher_text) - .map_err(|e| Error::msg(format!("{}", e)))?; - if plain.len() != THIRTYTWO { - return Err(Error::msg("Plain text of invalid length, should be 32")); - } - let mut thirty_two = [0u8; THIRTYTWO]; - thirty_two.copy_from_slice(&plain); - Ok(thirty_two) - } -} - -pub struct Aes256Encryption { - pub purpose: Purpose, - plaintext: Vec, - aes_key: [u8; 32], - nonce: [u8; 12], - shared_secrets: HashMap>, -} - -impl Aes256Encryption { - pub fn new(purpose: Purpose, plaintext: Vec) -> Result { - let mut rng = thread_rng(); - let aes_key: [u8; 32] = Aes256Gcm::generate_key(&mut rng).into(); - let nonce: [u8; 12] = Aes256Gcm::generate_nonce(&mut rng).into(); - Self::import_key(purpose, plaintext, aes_key, nonce) - } - - pub fn set_shared_secret( - &mut self, - shared_secrets: HashMap>, - ) { - self.shared_secrets = shared_secrets; - } - - pub fn encrypt_keys_with_shared_secrets( - &self, - ) -> Result> { - let mut res = HashMap::new(); - let mut rng = thread_rng(); - - for (_, sp_address2shared_secret) in self.shared_secrets.iter() { - for (sp_address, shared_secret) in sp_address2shared_secret { - let cipher = Aes256Gcm::new_from_slice(shared_secret.as_ref()) - .map_err(|e| Error::msg(format!("{}", e)))?; - let nonce = Aes256Gcm::generate_nonce(&mut rng); - let encrypted_key = cipher - .encrypt(&nonce, self.aes_key.as_slice()) - .map_err(|e| Error::msg(format!("{}", e)))?; - - let mut ciphertext = Vec::::with_capacity(nonce.len() + encrypted_key.len()); - ciphertext.extend(nonce); - ciphertext.extend(encrypted_key); - - res.insert(sp_address.to_owned(), ciphertext); - } - } - Ok(res) - } - - pub fn import_key( - purpose: Purpose, - plaintext: Vec, - aes_key: [u8; 32], - nonce: [u8; 12], - ) -> Result { - if plaintext.len() == 0 { - return Err(Error::msg("Can't create encryption for an empty message")); - } - Ok(Self { - purpose, - plaintext, - aes_key, - nonce, - shared_secrets: HashMap::new(), - }) - } - - pub fn encrypt_with_aes_key(&self) -> Result { - match self.purpose { - Purpose::Login => self.encrypt_login(), - Purpose::ThirtyTwoBytes => self.encrypt_thirty_two(), - } - } - - fn encrypt_login(&self) -> Result { - let half_key: HalfKey = self.plaintext.clone().try_into()?; - let cipher = Aes256Gcm::new(&self.aes_key.into()); - let cipher_text = cipher - .encrypt(&self.nonce.into(), half_key.as_slice()) - .map_err(|e| Error::msg(format!("{}", e)))?; - let mut res = Vec::with_capacity(self.nonce.len() + cipher_text.len()); - res.extend_from_slice(&self.nonce); - res.extend_from_slice(&cipher_text); - Ok(res) - } - - fn encrypt_thirty_two(&self) -> Result { - if self.plaintext.len() != 32 { - return Err(Error::msg("Invalid length, should be 32")); - } - let mut thirty_two = [0u8; 32]; - thirty_two.copy_from_slice(&self.plaintext); - let cipher = Aes256Gcm::new(&self.aes_key.into()); - let cipher_text = cipher - .encrypt(&self.nonce.into(), thirty_two.as_slice()) - .map_err(|e| Error::msg(format!("{}", e)))?; - let mut res = Vec::with_capacity(self.nonce.len() + cipher_text.len()); - log::info!("{}", cipher_text.len()); - res.extend_from_slice(&self.nonce); - res.extend_from_slice(&cipher_text); - Ok(res) - } -} - -#[cfg(test)] -mod tests { - use std::str::FromStr; - - use super::*; - - const ALICE_SP_ADDRESS: &str = "tsp1qqw3lqr6xravz9nf8ntazgwwl0fqv47kfjdxsnxs6eutavqfwyv5q6qk97mmyf6dtkdyzqlu2zv6h9j2ggclk7vn705q5u2phglpq7yw3dg5rwpdz"; - const BOB_SP_ADDRESS: &str = "tsp1qq2hlsgrj0gz8kcfkf9flqw5llz0u2vr04telqndku9mcqm6dl4fhvq60t8r78srrf56w9yr7w9e9dusc2wjqc30up6fjwnh9mw3e3veqegdmtf08"; - const TRANSACTION: &str = "4e6d03dec558e1b6624f813bf2da7cd8d8fb1c2296684c08cf38724dcfd8d10b"; - const ALICE_SHARED_SECRET: &str = - "ccf02d364c2641ca129a3fdf49de57b705896e233f7ba6d738991993ea7e2106"; - const BOB_SHARED_SECRET: &str = - "15ef3e377fb842e81de52dbaaea8ba30aeb051a81043ee19264afd27353da521"; - - #[test] - fn new_aes_empty_plaintext() { - let plaintext = Vec::new(); - let aes_enc = Aes256Encryption::new(Purpose::Login, plaintext); - - assert!(aes_enc.is_err()); - } - - #[test] - fn aes_encrypt_login_invalid_length() { - let plaintext = "example"; - let aes_enc_short = Aes256Encryption::new(Purpose::Login, plaintext.as_bytes().to_vec()); - - assert!(aes_enc_short.is_ok()); - - let cipher = aes_enc_short.unwrap().encrypt_with_aes_key(); - - assert!(cipher.is_err()); - - let plaintext = [1u8; 64]; - let aes_enc_long = Aes256Encryption::new(Purpose::Login, plaintext.to_vec()); - - assert!(aes_enc_long.is_ok()); - - let cipher = aes_enc_long.unwrap().encrypt_with_aes_key(); - - assert!(cipher.is_err()); - } - - #[test] - fn aes_encrypt_login() { - let plaintext = [1u8; HALFKEYSIZE]; - let aes_key = Aes256Gcm::generate_key(&mut thread_rng()); - let nonce = Aes256Gcm::generate_nonce(&mut thread_rng()); - let aes_enc = Aes256Encryption::import_key( - Purpose::Login, - plaintext.to_vec(), - aes_key.into(), - nonce.into(), - ); - - assert!(aes_enc.is_ok()); - - let cipher = aes_enc.unwrap().encrypt_with_aes_key(); - - assert!(cipher.is_ok()); - - let mut plain_key = [0u8; 32]; - plain_key.copy_from_slice(&aes_key.to_vec()); - - let aes_dec = - Aes256Decryption::new(Purpose::Login, cipher.unwrap(), plain_key.to_vec(), None); - - assert!(aes_dec.is_ok()); - } - - #[test] - fn aes_encrypt_key() { - let plaintext = [1u8; HALFKEYSIZE]; - let mut aes_enc = Aes256Encryption::new(Purpose::Login, plaintext.to_vec()).unwrap(); - - let mut shared_secrets: HashMap = HashMap::new(); - let mut sp_address2shared_secrets: HashMap = - HashMap::new(); - sp_address2shared_secrets.insert( - ALICE_SP_ADDRESS.try_into().unwrap(), - SharedSecret::from_str(ALICE_SHARED_SECRET).unwrap(), - ); - shared_secrets.insert( - Txid::from_str(TRANSACTION).unwrap(), - sp_address2shared_secrets, - ); - - aes_enc.set_shared_secret(shared_secrets); - - let sp_address2encrypted_keys = aes_enc.encrypt_keys_with_shared_secrets(); - - assert!(sp_address2encrypted_keys.is_ok()); - - let encrypted_key = sp_address2encrypted_keys - .unwrap() - .get(&ALICE_SP_ADDRESS.try_into().unwrap()) - .cloned(); - - let ciphertext = aes_enc.encrypt_with_aes_key(); - - assert!(ciphertext.is_ok()); - - let aes_dec = Aes256Decryption::new( - Purpose::Login, - ciphertext.unwrap(), - encrypted_key.unwrap(), - Some(SharedSecret::from_str(ALICE_SHARED_SECRET).unwrap()), - ); - - assert!(aes_dec.is_ok()); - - let retrieved_plain = aes_dec.unwrap().decrypt_with_key(); - - assert!(retrieved_plain.is_ok()); - - assert!(retrieved_plain.unwrap() == plaintext); - } - - #[test] - fn aes_encrypt_key_many() { - let plaintext = [1u8; THIRTYTWO]; - let mut aes_enc = - Aes256Encryption::new(Purpose::ThirtyTwoBytes, plaintext.to_vec()).unwrap(); - - let mut shared_secrets: HashMap = HashMap::new(); - let mut sp_address2shared_secrets: HashMap = - HashMap::new(); - sp_address2shared_secrets.insert( - ALICE_SP_ADDRESS.try_into().unwrap(), - SharedSecret::from_str(ALICE_SHARED_SECRET).unwrap(), - ); - sp_address2shared_secrets.insert( - BOB_SP_ADDRESS.try_into().unwrap(), - SharedSecret::from_str(BOB_SHARED_SECRET).unwrap(), - ); - shared_secrets.insert( - Txid::from_str(TRANSACTION).unwrap(), - sp_address2shared_secrets, - ); - - aes_enc.set_shared_secret(shared_secrets); - - let mut sp_address2encrypted_keys = aes_enc.encrypt_keys_with_shared_secrets(); - - assert!(sp_address2encrypted_keys.is_ok()); - - // Alice - let encrypted_key = sp_address2encrypted_keys - .as_mut() - .unwrap() - .get(&ALICE_SP_ADDRESS.try_into().unwrap()) - .cloned(); - - let ciphertext = aes_enc.encrypt_with_aes_key(); - - let aes_dec = Aes256Decryption::new( - Purpose::ThirtyTwoBytes, - ciphertext.unwrap(), - encrypted_key.unwrap(), - Some(SharedSecret::from_str(ALICE_SHARED_SECRET).unwrap()), - ); - - let retrieved_plain = aes_dec.unwrap().decrypt_with_key(); - - assert!(retrieved_plain.unwrap() == plaintext); - - // Bob - let encrypted_key = sp_address2encrypted_keys - .unwrap() - .get(&BOB_SP_ADDRESS.try_into().unwrap()) - .cloned(); - - let ciphertext = aes_enc.encrypt_with_aes_key(); - - let aes_dec = Aes256Decryption::new( - Purpose::ThirtyTwoBytes, - ciphertext.unwrap(), - encrypted_key.unwrap(), - Some(SharedSecret::from_str(BOB_SHARED_SECRET).unwrap()), - ); - - let retrieved_plain = aes_dec.unwrap().decrypt_with_key(); - - assert!(retrieved_plain.unwrap() == plaintext); - } -} diff --git a/crates/sp_client/src/images.rs b/crates/sp_client/src/images.rs index f0e11ee..75f3a0d 100644 --- a/crates/sp_client/src/images.rs +++ b/crates/sp_client/src/images.rs @@ -1,7 +1,7 @@ use anyhow::{Error, Result}; use img_parts::{jpeg::Jpeg, Bytes, ImageEXIF}; +use sdk_common::sp_client::bitcoin::secp256k1::SecretKey; use serde::{Deserialize, Serialize}; -use sp_backend::bitcoin::secp256k1::SecretKey; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BackUpImage(Vec); diff --git a/crates/sp_client/src/lib.rs b/crates/sp_client/src/lib.rs index 95968cb..1168492 100644 --- a/crates/sp_client/src/lib.rs +++ b/crates/sp_client/src/lib.rs @@ -1,18 +1,27 @@ #![allow(warnings)] use anyhow::Error; +use sdk_common::crypto::AnkSharedSecret; +use sdk_common::network::CachedMessage; +use serde::{Deserialize, Serialize}; +use std::collections::{HashMap, HashSet}; use std::fmt::Debug; -use std::sync::{Mutex, MutexGuard}; +use std::sync::{Mutex, MutexGuard, OnceLock}; +use tsify::Tsify; -mod Prd_list; pub mod api; -mod crypto; mod images; -mod network; mod peers; mod process; -mod silentpayments; mod user; +pub static CACHEDMESSAGES: OnceLock>> = OnceLock::new(); + +pub fn lock_messages() -> Result>, Error> { + CACHEDMESSAGES + .get_or_init(|| Mutex::new(vec![])) + .lock_anyhow() +} + pub(crate) trait MutexExt { fn lock_anyhow(&self) -> Result, Error>; } diff --git a/crates/sp_client/src/network.rs b/crates/sp_client/src/network.rs deleted file mode 100644 index fdb16c6..0000000 --- a/crates/sp_client/src/network.rs +++ /dev/null @@ -1,94 +0,0 @@ -use anyhow::{Error, Result}; -use serde::{Deserialize, Serialize}; -use tsify::Tsify; - -const RAWTXTOPIC: &'static str = "rawtx"; -const RAWBLOCKTOPIC: &'static str = "rawblock"; - -#[derive(Debug, Serialize, Deserialize)] -pub enum BitcoinTopic { - RawTx, - RawBlock, -} - -impl BitcoinTopic { - pub fn as_str(&self) -> &str { - match self { - Self::RawTx => RAWTXTOPIC, - Self::RawBlock => RAWBLOCKTOPIC, - } - } -} - -#[derive(Debug, Serialize, Deserialize, Tsify)] -#[tsify(from_wasm_abi, into_wasm_abi)] -pub struct BitcoinNetworkMsg<'a> { - pub topic: BitcoinTopic, - pub data: &'a [u8], - pub sequence: &'a [u8], - pub addon: &'a [u8], -} - -impl<'a> BitcoinNetworkMsg<'a> { - pub fn new(raw_msg: &'a [u8]) -> Result { - let topic: BitcoinTopic; - let data: &[u8]; - let sequence: &[u8]; - let addon: &[u8]; - let addon_len: usize; - let raw_msg_len = raw_msg.len(); - - if raw_msg.starts_with(RAWTXTOPIC.as_bytes()) { - topic = BitcoinTopic::RawTx; - addon_len = 33; - } else if raw_msg.starts_with(RAWBLOCKTOPIC.as_bytes()) { - topic = BitcoinTopic::RawBlock; - addon_len = 0; - } else { - return Err(Error::msg("Unknown prefix")); - } - - data = &raw_msg[topic.as_str().as_bytes().len()..raw_msg_len - 4 - addon_len]; - sequence = &raw_msg[raw_msg_len - 4 - addon_len..]; - addon = &raw_msg[raw_msg_len - addon_len..]; - - Ok(Self { - topic, - data, - sequence, - addon, - }) - } -} - -#[derive(Debug)] -pub enum AnkTopic { - Faucet, -} - -impl AnkTopic { - pub fn as_str(&self) -> &str { - match self { - Self::Faucet => "faucet", - } - } -} - -#[derive(Debug)] -pub struct AnkNetworkMsg<'a> { - pub topic: AnkTopic, - pub content: &'a str, -} - -impl<'a> AnkNetworkMsg<'a> { - pub fn new(raw: &'a str) -> Result { - if raw.starts_with(AnkTopic::Faucet.as_str()) { - Ok(Self { - topic: AnkTopic::Faucet, - content: &raw[AnkTopic::Faucet.as_str().len()..], - }) - } else { - Err(Error::msg("Unknown 4nk message")) - } - } -} diff --git a/crates/sp_client/src/process.rs b/crates/sp_client/src/process.rs index 56f95d6..25eed24 100644 --- a/crates/sp_client/src/process.rs +++ b/crates/sp_client/src/process.rs @@ -1,149 +1,68 @@ use std::fmt::DebugStruct; +use sdk_common::sp_client::silentpayments::sending::SilentPaymentAddress; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; -use sp_backend::silentpayments::sending::SilentPaymentAddress; use tsify::Tsify; use wasm_bindgen::prelude::*; -pub const HTML_CREATE_ID: &str = " +pub const HTML_KOTPART: &str = "
-

Create an Id

- -
-
- -
- -
-
- -
- Recover -
-
-

-
-
- "; - -pub const HTML_UPDATE_ID: &str = " - -
-
-

Update an Id

-
-
-
- - - - - - - - - - - - - -
-
- - -
-
-
- -
- -
-
- - "; - -pub const HTML_RECOVER: &str = " -
-
-

Recover my Id

- -
-
- - - -
-
- +

Send encrypted messages

-

- Revoke -

-
-
- "; - -pub const HTML_REVOKE_IMAGE: &str = " -
-
-

Revoke image

- -
-
-
- - - - - -
- -
-
- "; - -pub const HTML_REVOKE: &str = " -
-
-

Revoke an Id

-
- - + +
-
- - + + +
+ + +
+ "; + +pub const HTML_STORAGE: &str = " +
+
+

Send encrypted messages

+ +
+
+ +
- + + +
+ +
+
+ "; + +pub const HTML_MESSAGING: &str = " +
+
+

Send encrypted messages

+ +
+
+
+ + +
+ + +
+
"; diff --git a/crates/sp_client/src/silentpayments.rs b/crates/sp_client/src/silentpayments.rs deleted file mode 100644 index 5ab45d4..0000000 --- a/crates/sp_client/src/silentpayments.rs +++ /dev/null @@ -1,76 +0,0 @@ -use std::collections::HashMap; - -use anyhow::Result; - -use sp_backend::silentpayments::utils::receiving::calculate_shared_secret; -use sp_backend::{ - bitcoin::{ - secp256k1::{PublicKey, Scalar, XOnlyPublicKey}, - Transaction, - }, - silentpayments::receiving::Label, -}; - -use crate::user::{lock_connected_users, CONNECTED_USERS}; - -type FoundOutputs = HashMap, HashMap>; - -pub fn check_transaction(tx: Transaction, tweak_data: PublicKey) -> Result { - let connected_users = lock_connected_users()?; - - let pubkeys_to_check: HashMap = (0u32..) - .zip(tx.output) - .filter_map(|(i, o)| { - if o.script_pubkey.is_p2tr() { - let xonly = XOnlyPublicKey::from_slice(&o.script_pubkey.as_bytes()[2..]) - .expect("Transaction is invalid"); - Some((xonly, i)) - } else { - None - } - }) - .collect(); - - // Check the transaction for all connected users - for (pre_id, keys) in connected_users.clone() { - let recover = keys.recover; - let shared_secret = - calculate_shared_secret(tweak_data, recover.get_client().get_scan_key())?; - let res = recover - .get_client() - .sp_receiver - .scan_transaction(&shared_secret, pubkeys_to_check.keys().cloned().collect())?; - - if res.len() > 0 { - return Ok(res); - } - - if let Some(main) = keys.main { - let shared_secret = - calculate_shared_secret(tweak_data, main.get_client().get_scan_key())?; - let res = main - .get_client() - .sp_receiver - .scan_transaction(&shared_secret, pubkeys_to_check.keys().cloned().collect())?; - - if res.len() > 0 { - return Ok(res); - } - } - - if let Some(revoke) = keys.revoke { - let shared_secret = - calculate_shared_secret(tweak_data, revoke.get_client().get_scan_key())?; - let res = revoke - .get_client() - .sp_receiver - .scan_transaction(&shared_secret, pubkeys_to_check.keys().cloned().collect())?; - - if res.len() > 0 { - return Ok(res); - } - } - } - - Ok(HashMap::new()) -} diff --git a/crates/sp_client/src/user.rs b/crates/sp_client/src/user.rs index 202ed8d..f7af595 100644 --- a/crates/sp_client/src/user.rs +++ b/crates/sp_client/src/user.rs @@ -1,18 +1,13 @@ -use aes::cipher::generic_array::GenericArray; -use aes_gcm::aead::Aead; -use aes_gcm::AeadCore; -use aes_gcm::KeyInit; -use aes_gcm::{aead::Buffer, Aes256Gcm, Key}; use anyhow::{Error, Result}; use rand::{self, thread_rng, Rng, RngCore}; +use sdk_common::sp_client::bitcoin::hashes::Hash; +use sdk_common::sp_client::bitcoin::hashes::HashEngine; +use sdk_common::sp_client::bitcoin::hex::{DisplayHex, FromHex}; +use sdk_common::sp_client::bitcoin::secp256k1::SecretKey; +use sdk_common::sp_client::bitcoin::secp256k1::ThirtyTwoByteHash; +use sdk_common::sp_client::spclient::SpClient; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; -use sp_backend::bitcoin::hashes::Hash; -use sp_backend::bitcoin::hashes::HashEngine; -use sp_backend::bitcoin::hex::{DisplayHex, FromHex}; -use sp_backend::bitcoin::secp256k1::SecretKey; -use sp_backend::bitcoin::secp256k1::ThirtyTwoByteHash; -use sp_backend::spclient::SpClient; use tsify::Tsify; use wasm_bindgen::prelude::*; @@ -23,40 +18,45 @@ use std::io::{Cursor, Read, Write}; use std::str::FromStr; use std::sync::{Mutex, MutexGuard, OnceLock}; -use sp_backend::bitcoin::secp256k1::constants::SECRET_KEY_SIZE; -use sp_backend::silentpayments::bitcoin_hashes::sha256; -use sp_backend::silentpayments::sending::SilentPaymentAddress; -use sp_backend::spclient::SpendKey; -use sp_backend::spclient::{OutputList, SpWallet}; +use sdk_common::sp_client::bitcoin::secp256k1::constants::SECRET_KEY_SIZE; +use sdk_common::sp_client::silentpayments::bitcoin_hashes::sha256; +use sdk_common::sp_client::silentpayments::sending::SilentPaymentAddress; +use sdk_common::sp_client::spclient::SpendKey; +use sdk_common::sp_client::spclient::{OutputList, SpWallet}; -use crate::crypto::{Aes256Decryption, Aes256Encryption, HalfKey, Purpose}; use crate::peers::Peer; use crate::user; use crate::MutexExt; +use sdk_common::crypto::{ + AeadCore, Aes256Decryption, Aes256Encryption, Aes256Gcm, HalfKey, KeyInit, Purpose, +}; type PreId = String; const MANAGERS_NUMBER: u8 = 10; const QUORUM_SHARD: f32 = 0.8; -type UsersMap = HashMap; -pub static CONNECTED_USERS: OnceLock> = OnceLock::new(); +pub static CONNECTED_USER: OnceLock> = OnceLock::new(); -pub fn lock_connected_users() -> Result> { - CONNECTED_USERS - .get_or_init(|| Mutex::new(HashMap::new())) +pub fn lock_connected_user() -> Result> { + CONNECTED_USER + .get_or_init(|| Mutex::new(UserWallets::default())) .lock_anyhow() } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct UserWallets { - pub main: Option, - pub recover: SpWallet, - pub revoke: Option, + main: Option, + recover: Option, + revoke: Option, } impl UserWallets { - pub fn new(main: Option, recover: SpWallet, revoke: Option) -> Self { + pub fn new( + main: Option, + recover: Option, + revoke: Option, + ) -> Self { Self { main, recover, @@ -64,8 +64,56 @@ impl UserWallets { } } - pub fn try_get_revoke(&self) -> Option<&SpWallet> { - self.revoke.as_ref() + pub fn try_get_revoke(&self) -> Result<&SpWallet> { + if let Some(revoke) = &self.revoke { + Ok(revoke) + } else { + Err(Error::msg("No revoke wallet available")) + } + } + + pub fn try_get_recover(&self) -> Result<&SpWallet> { + if let Some(recover) = &self.recover { + Ok(recover) + } else { + Err(Error::msg("No recover wallet available")) + } + } + + pub fn try_get_main(&self) -> Result<&SpWallet> { + if let Some(main) = &self.main { + Ok(main) + } else { + Err(Error::msg("No main wallet available")) + } + } + + pub fn try_get_mut_revoke(&mut self) -> Result<&mut SpWallet> { + if let Some(revoke) = &mut self.revoke { + Ok(revoke) + } else { + Err(Error::msg("No revoke wallet available")) + } + } + + pub fn try_get_mut_recover(&mut self) -> Result<&mut SpWallet> { + if let Some(recover) = &mut self.recover { + Ok(recover) + } else { + Err(Error::msg("No recover wallet available")) + } + } + + pub fn try_get_mut_main(&mut self) -> Result<&mut SpWallet> { + if let Some(main) = &mut self.main { + Ok(main) + } else { + Err(Error::msg("No main wallet available")) + } + } + + pub(crate) fn is_not_empty(&self) -> bool { + self.get_all_outputs().len() > 0 } pub(crate) fn get_all_outputs(&self) -> Vec { @@ -76,7 +124,9 @@ impl UserWallets { if let Some(revoke) = &self.revoke { res.push(revoke.get_outputs().clone()); } - res.push(self.recover.get_outputs().clone()); + if let Some(recover) = &self.recover { + res.push(recover.get_outputs().clone()); + } res } @@ -96,22 +146,24 @@ pub struct User { impl User { pub fn new(user_wallets: UserWallets, user_password: String, process: String) -> Result { + // if we are already logged in, abort + if lock_connected_user()?.is_not_empty() { + return Err(Error::msg("User already logged in")); + } + let mut rng = thread_rng(); // image revoke // We just take the 2 revoke keys let mut revoke_data = Vec::with_capacity(64); - if let Some(revoke) = user_wallets.try_get_revoke() { - revoke_data.extend_from_slice(revoke.get_client().get_scan_key().as_ref()); - revoke_data.extend_from_slice(revoke.get_client().try_get_secret_spend_key()?.as_ref()); - } else { - return Err(Error::msg("No revoke wallet available")); - } + let revoke = user_wallets.try_get_revoke()?; + revoke_data.extend_from_slice(revoke.get_client().get_scan_key().as_ref()); + revoke_data.extend_from_slice(revoke.get_client().try_get_secret_spend_key()?.as_ref()); // Take the 2 recover keys // split recover spend key let recover_spend_key = user_wallets - .recover + .try_get_recover()? .get_client() .try_get_secret_spend_key()? .clone(); @@ -189,7 +241,7 @@ impl User { let scan_key_encryption = Aes256Encryption::import_key( Purpose::ThirtyTwoBytes, user_wallets - .recover + .try_get_recover()? .get_client() .get_scan_key() .secret_bytes() @@ -203,6 +255,8 @@ impl User { recover_data.extend_from_slice(&cipher_scan_key); + let all_outputs = user_wallets.get_all_outputs(); + Ok(User { pre_id: pre_id.to_string(), processes: vec![process], @@ -210,10 +264,19 @@ impl User { recover_data, revoke_data: Some(revoke_data), shares, - outputs: user_wallets.get_all_outputs(), + outputs: all_outputs, }) } + pub fn logout() -> Result<()> { + if let Ok(mut user) = lock_connected_user() { + *user = UserWallets::default(); + Ok(()) + } else { + Err(Error::msg("Failed to lock CONNECTED_USER")) + } + } + pub fn login( pre_id: PreId, user_password: String, @@ -221,6 +284,11 @@ impl User { shares: &[Vec], outputs: &[OutputList], ) -> Result<()> { + // if we are already logged in, abort + if lock_connected_user()?.is_not_empty() { + return Err(Error::msg("User already logged in")); + } + let mut retrieved_spend_key = [0u8; 32]; let mut retrieved_scan_key = [0u8; 32]; let mut entropy1 = [0u8; 32]; @@ -244,21 +312,6 @@ impl User { return Err(Error::msg("pre_id and recover_data don't match")); } - // If we already have loaded a user with this pre_id, abort - if let Some(current_users) = CONNECTED_USERS.get() { - if current_users - .to_owned() - .lock() - .unwrap() - .contains_key(&pre_id) - { - return Err(Error::msg(format!( - "User with pre_id {} already logged in", - pre_id - ))); - } - } - retrieved_spend_key[..16].copy_from_slice(&Self::recover_part1( &user_password, &entropy1, @@ -293,26 +346,12 @@ impl User { let recover_wallet = SpWallet::new(recover_client, recover_outputs)?; - // Adding user to CONNECTED_USERS - if let Some(current_users) = CONNECTED_USERS.get() { - let mut lock = current_users.to_owned().lock().unwrap(); - if lock.contains_key(&pre_id) { - return Err(Error::msg(format!( - "User with pre_id {} already exists", - pre_id - ))); - } else { - lock.insert(pre_id.clone(), UserWallets::new(None, recover_wallet, None)); - } + let user_wallets = UserWallets::new(None, Some(recover_wallet), None); + + if let Ok(mut user) = lock_connected_user() { + *user = user_wallets; } else { - let mut user_map = HashMap::new(); - user_map.insert(pre_id, UserWallets::new(None, recover_wallet, None)); - let new_value = Mutex::new(user_map); - if let Err(error) = CONNECTED_USERS.set(new_value) { - return Err(Error::msg( - "Failed to set the CONNECTED_USERS static variable", - )); - } + return Err(Error::msg("Failed to lock CONNECTED_USER")); } Ok(()) @@ -324,12 +363,8 @@ impl User { engine.write_all(&entropy); let hash = sha256::Hash::from_engine(engine); - let aes_dec = Aes256Decryption::new( - Purpose::ThirtyTwoBytes, - ciphertext, - hash.to_byte_array().to_vec(), - None, - )?; + let aes_dec = + Aes256Decryption::new(Purpose::ThirtyTwoBytes, ciphertext, hash.to_byte_array())?; aes_dec.decrypt_with_key() } @@ -340,12 +375,7 @@ impl User { engine.write_all(&entropy); let hash = sha256::Hash::from_engine(engine); - let aes_dec = Aes256Decryption::new( - Purpose::Login, - ciphertext, - hash.to_byte_array().to_vec(), - None, - )?; + let aes_dec = Aes256Decryption::new(Purpose::Login, ciphertext, hash.to_byte_array())?; aes_dec.decrypt_with_key() } @@ -364,12 +394,7 @@ impl User { .ok_or_else(|| anyhow::Error::msg("Failed to retrieve the sharded secret"))?, )?; - let aes_dec = Aes256Decryption::new( - Purpose::Login, - part2_key_enc, - hash.to_byte_array().to_vec(), - None, - )?; + let aes_dec = Aes256Decryption::new(Purpose::Login, part2_key_enc, hash.to_byte_array())?; aes_dec.decrypt_with_key() } @@ -459,21 +484,26 @@ mod tests { .unwrap(); let user_wallets = UserWallets::new( Some(SpWallet::new(sp_main, None).unwrap()), - SpWallet::new(sp_recover, None).unwrap(), + Some(SpWallet::new(sp_recover, None).unwrap()), Some(SpWallet::new(sp_revoke, None).unwrap()), ); user_wallets } - // Test 1: Create User #[test] fn test_successful_creation() { let user_wallets = helper_create_user_wallets(); let result = User::new(user_wallets, USER_PASSWORD.to_owned(), PROCESS.to_owned()); assert!(result.is_ok()); - let user = result.unwrap(); + } + + #[test] + fn test_logout() { + let res = User::logout(); + + assert!(res.is_ok()); } #[test] @@ -496,9 +526,9 @@ mod tests { assert!(res.is_ok()); - let connected = CONNECTED_USERS.get().unwrap().lock().unwrap(); + let connected = lock_connected_user().unwrap(); - let recover = &connected.get(&user.pre_id).unwrap().recover; + let recover = connected.try_get_recover().unwrap(); assert!( format!( 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 d8d8041..81bb802 100644 --- a/src/database.ts +++ b/src/database.ts @@ -4,32 +4,10 @@ class Database { private dbName: string = '4nk'; private dbVersion: number = 1; private storeDefinitions = { - // SpClient: { - // name: "sp_client", - // options: {}, - // indices: [] - // }, - // SpOutputs: { - // name: "sp_outputs", - // options: {'autoIncrement': true}, - // indices: [{ - // name: 'by_wallet_fingerprint', - // keyPath: 'wallet_fingerprint', - // options: { - // 'unique': false - // } - // }] - // }, AnkUser: { name: "user", options: {'keyPath': 'pre_id'}, - indices: [{ - name: 'by_process', - keyPath: 'process', - options: { - 'unique': false - } - }] + indices: [] }, AnkSession: { name: "session", @@ -46,6 +24,11 @@ class Database { 'unique': true } }] + }, + AnkMessages: { + name: "messages", + options: {'keyPath': 'id'}, + indices: [] } } @@ -92,11 +75,11 @@ class Database { }); } - public getDb(): IDBDatabase { + public async getDb(): Promise { if (!this.db) { - throw new Error("Database not initialized"); + await this.init(); } - return this.db; + return this.db!; } public getStoreList(): {[key: string]: string} { @@ -134,6 +117,17 @@ class Database { }); } + public rmObject(db: IDBDatabase, storeName: string, key: IDBValidKey): Promise { + return new Promise((resolve, reject) => { + const transaction = db.transaction(storeName, 'readwrite'); + const store = transaction.objectStore(storeName); + const request = store.delete(key); + + request.onerror = () => reject(request.error); + request.onsuccess = () => resolve(request.result); + }); + } + public getFirstMatchWithIndex(db: IDBDatabase, storeName: string, indexName: string, lookup: string): Promise { return new Promise((resolve, reject) => { const transaction = db.transaction(storeName, 'readonly'); diff --git a/src/index.ts b/src/index.ts index f0304ea..d379dd8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ import Services from './services'; import { WebSocketClient } from './websockets'; -const wsurl = "ws://192.168.1.44:8090"; +const wsurl = "ws://localhost:8090"; document.addEventListener('DOMContentLoaded', async () => { try { const services = await Services.getInstance(); diff --git a/src/services.ts b/src/services.ts index 7160fbc..81298d3 100644 --- a/src/services.ts +++ b/src/services.ts @@ -1,4 +1,4 @@ -import { createUserReturn, User, Process } from '../dist/pkg/sdk_client'; +import { createUserReturn, User, Process, createTransactionReturn, outputs_list, FaucetMessage, AnkFlag, NewTxMessage, CipherMessage, CachedMessage } from '../dist/pkg/sdk_client'; import IndexedDB from './database' import { WebSocketClient } from './websockets'; @@ -7,6 +7,7 @@ class Services { private sdkClient: any; private current_process: string | null = null; private websocketConnection: WebSocketClient[] = []; + private sp_address: string | null = null; // Private constructor to prevent direct instantiation from outside private constructor() {} @@ -24,27 +25,9 @@ class Services { private async init(): Promise { this.sdkClient = await import("../dist/pkg/sdk_client"); this.sdkClient.setup(); + await this.updateProcesses(); } - // public async getSpAddressDefaultClient(): Promise { - // try { - // const indexedDB = await IndexedDB.getInstance(); - // const db = indexedDB.getDb(); - // const spClient = await indexedDB.getObject(db, indexedDB.getStoreList().SpClient, "default"); - - // if (spClient) { - // return this.sdkClient.get_receiving_address(spClient); - // } else { - // console.error("SP client not found"); - // return null; - // } - // } catch (error) { - // console.error("Failed to retrieve object or get sp address:", error); - // return null; - // } - - // } - public async addWebsocketConnection(url: string): Promise { const services = await Services.getInstance(); const newClient = new WebSocketClient(url, services); @@ -57,7 +40,7 @@ class Services { let isNew = false; try { const indexedDB = await IndexedDB.getInstance(); - const db = indexedDB.getDb(); + const db = await indexedDB.getDb(); let userListObject = await indexedDB.getAll(db, indexedDB.getStoreList().AnkUser); if (userListObject.length == 0) { isNew = true; @@ -70,21 +53,99 @@ class Services { public async displayCreateId(): Promise { const services = await Services.getInstance(); - await services.injectHtml('CREATE_ID'); + await services.createIdInjectHtml(); services.attachSubmitListener("form4nk", (event) => services.createId(event)); services.attachClickListener("displayrecover", services.displayRecover); await services.displayProcess(); } + public async displaySendMessage(): Promise { + const services = await Services.getInstance(); + await services.injectHtml('Messaging'); + services.attachSubmitListener("form4nk", (event) => services.sendMessage(event)); + // const ourAddress = document.getElementById('our_address'); + // if (ourAddress) { + // ourAddress.innerHTML = `Our Address: ${this.sp_address}` + // } + // services.attachClickListener("displaysendmessage", services.displaySendMessage); + // await services.displayProcess(); + } + + public async sendMessage(event: Event): Promise { + event.preventDefault(); + + const services = await Services.getInstance(); + let availableAmt: number = 0; + + // check available amount + try { + availableAmt = await services.sdkClient.get_available_amount_for_user(true); + } catch (error) { + console.error('Failed to get available amount'); + return; + } + + if (availableAmt < 2000) { + try { + await services.obtainTokenWithFaucet(); + } catch (error) { + console.error('Failed to obtain faucet token:', error); + return; + } + } + + const spAddressElement = document.getElementById("sp_address") as HTMLInputElement; + const messageElement = document.getElementById("message") as HTMLInputElement; + + if (!spAddressElement || !messageElement) { + console.error("One or more elements not found"); + return; + } + + const recipientSpAddress = spAddressElement.value; + const message = messageElement.value; + + const msg_payload: CipherMessage = {sender: this.sp_address!, message: message, error: null}; + + let notificationInfo = await services.notify_address_for_message(recipientSpAddress, msg_payload); + if (notificationInfo) { + let networkMsg = notificationInfo.new_network_msg; + console.debug(networkMsg); + + const connection = await services.pickWebsocketConnectionRandom(); + const flag: AnkFlag = 'Cipher'; + try { + // send message (transaction in envelope) + await services.updateMessages(networkMsg); + connection?.sendMessage(flag, networkMsg.ciphertext!); + } catch (error) { + throw error; + } + // add peers list + // add processes list + } + } + public async createId(event: Event): Promise { event.preventDefault(); + // verify we don't already have an user + const services = await Services.getInstance(); + try { + let user = await services.getUserInfo(); + if (user) { + console.error("User already exists, please recover"); + return; + } + } catch (error) { + throw error; + } + const passwordElement = document.getElementById("password") as HTMLInputElement; const processElement = document.getElementById("selectProcess") as HTMLSelectElement; if (!passwordElement || !processElement) { - console.error("One or more elements not found"); - return; + throw 'One or more elements not found'; } const password = passwordElement.value; @@ -97,47 +158,46 @@ class Services { const birthday_signet = 50000; const birthday_main = 500000; - const services = await Services.getInstance(); - let createUserReturn: createUserReturn = services.sdkClient.create_user(password, label, birthday_main, birthday_signet, this.current_process); + let createUserReturn: createUserReturn; + try { + createUserReturn = services.sdkClient.create_user(password, label, birthday_main, birthday_signet, this.current_process); + } catch (error) { + throw error; + } let user = createUserReturn.user; - const shares = user.shares; + // const shares = user.shares; // send the shares on the network const revokeData = user.revoke_data; if (!revokeData) { - console.error('Failed to get revoke data from wasm'); - return; + throw 'Failed to get revoke data from wasm'; } - user.shares = []; + // user.shares = []; user.revoke_data = null; try { const indexedDb = await IndexedDB.getInstance(); - const db = indexedDb.getDb(); + const db = await indexedDb.getDb(); await indexedDb.writeObject(db, indexedDb.getStoreList().AnkUser, user, null); } catch (error) { - console.error("Failed to write user object :", error); + throw `Failed to write user object: ${error}`; } - let sp_address = ""; try { - sp_address = services.sdkClient.get_receiving_address(user.pre_id); - console.info('Using sp_address:', sp_address); + await services.obtainTokenWithFaucet(); } catch (error) { - console.error(error); + throw error; } - await services.obtainTokenWithFaucet(sp_address); - await services.displayRevokeImage(new Uint8Array(revokeData)); } public async displayRecover(): Promise { const services = await Services.getInstance(); - await services.injectHtml('RECOVER'); - services.attachSubmitListener("form4nk", services.recover); + await services.recoverInjectHtml(); + services.attachSubmitListener("form4nk", (event) => services.recover(event)); services.attachClickListener("displaycreateid", services.displayCreateId); services.attachClickListener("displayrevoke", services.displayRevoke); services.attachClickListener("submitButtonRevoke", services.revoke); @@ -146,7 +206,6 @@ class Services { public async recover(event: Event) { event.preventDefault(); - console.log("JS recover submit "); const passwordElement = document.getElementById("password") as HTMLInputElement; const processElement = document.getElementById("selectProcess") as HTMLSelectElement; @@ -158,17 +217,36 @@ class Services { const password = passwordElement.value; const process = processElement.value; - console.log("JS password: " + password + " process: " + process); + // console.log("JS password: " + password + " process: " + process); // To comment if test // if (!Services.instance.isPasswordValid(password)) return; - // TODO - alert("Recover submit to do ..."); + // Get user in db + const services = await Services.getInstance(); + try { + const user = await services.getUserInfo(); + if (user) { + services.sdkClient.login_user(password, user.pre_id, user.recover_data, user.shares, user.outputs); + this.sp_address = services.sdkClient.get_recover_address(); + if (this.sp_address) { + console.info('Using sp_address:', this.sp_address); + await services.obtainTokenWithFaucet(); + } + } + } catch (error) { + console.error(error); + } + + console.info(this.sp_address); + + // TODO: check blocks since last_scan and update outputs + + await services.displaySendMessage(); } public async displayRevokeImage(revokeData: Uint8Array): Promise { const services = await Services.getInstance(); - await services.injectHtml('REVOKE_IMAGE'); + await services.revokeImageInjectHtml(); services.attachClickListener("displayupdateanid", services.displayUpdateAnId); let imageBytes = await services.getRecoverImage('assets/4nk_revoke.jpg'); @@ -176,6 +254,12 @@ class Services { var elem = document.getElementById("revoke") as HTMLAnchorElement; if (elem != null) { let imageWithData = services.sdkClient.add_data_to_image(imageBytes, revokeData, true); + const blob = new Blob([imageWithData], { type: 'image/jpeg' }); + const url = URL.createObjectURL(blob); + + // Set the href attribute for download + elem.href = url; + elem.download = 'revoke_4NK.jpg'; } } } @@ -195,21 +279,9 @@ class Services { return imageBytes; } - public async parseBitcoinMessage(raw: Blob): Promise { - try { - const buffer = await raw.arrayBuffer(); - const uint8Array = new Uint8Array(buffer); - const msg: string = this.sdkClient.parse_bitcoin_network_msg(uint8Array); - return msg; - } catch (error) { - console.error("Error processing the blob:", error); - return null; - } - } - public async displayRevoke(): Promise { const services = await Services.getInstance(); - services.injectHtml('REVOKE'); + await services.revokeInjectHtml(); services.attachClickListener("displayrecover", Services.instance.displayRecover); services.attachSubmitListener("form4nk", Services.instance.revoke); } @@ -224,51 +296,19 @@ class Services { public async displayUpdateAnId() { const services = await Services.getInstance(); - console.log("JS displayUpdateAnId process : "+services.current_process); - let body = ""; - let style = ""; - let script = ""; - try { - const processObject = await services.getProcessByName("UPDATE_ID"); - if (processObject) { - body = processObject.html; - style = processObject.style; - script = processObject.script; - console.log("JS displayUpdateAnId body : "+body); + await services.updateIdInjectHtml(); - } - } catch (error) { - console.error("Failed to retrieve process with Error:", error); - } - - services.injectUpdateAnIdHtml(body, style, script); services.attachSubmitListener("form4nk", services.updateAnId); } - public async parse4nkMessage(raw: string): Promise { - const msg: string = this.sdkClient.parse_4nk_msg(raw); + public async parseNetworkMessage(raw: string, feeRate: number): Promise { + const services = await Services.getInstance(); + try { + const msg: CachedMessage = services.sdkClient.parse_network_msg(raw, feeRate); return msg; - } - - public injectUpdateAnIdHtml(bodyToInject: string, styleToInject: string, scriptToInject: string) { - console.log("JS html : "+bodyToInject); - const body = document.getElementsByTagName('body')[0]; - - if (!body) { - console.error("No body tag"); - return; + } catch (error) { + throw error; } - body.innerHTML = styleToInject + bodyToInject; - - const script = document.createElement("script"); - script.innerHTML = scriptToInject; - document.body.appendChild(script); - script.onload = () => { - console.log('Script loaded successfuly'); - }; - script.onerror = () => { - console.log('Error loading script'); - }; } public async updateAnId(event: Event): Promise { @@ -304,7 +344,7 @@ class Services { public async addProcess(process: Process): Promise { try { const indexedDB = await IndexedDB.getInstance(); - const db = indexedDB.getDb(); + const db = await indexedDB.getDb(); await indexedDB.writeObject(db, indexedDB.getStoreList().AnkProcess, process, null); } catch (error) { console.log('addProcess failed: ',error); @@ -314,7 +354,7 @@ class Services { public async getAllProcess(): Promise { try { const indexedDB = await IndexedDB.getInstance(); - const db = indexedDB.getDb(); + const db = await indexedDB.getDb(); let processListObject = await indexedDB.getAll(db, indexedDB.getStoreList().AnkProcess); return processListObject; } catch (error) { @@ -322,24 +362,36 @@ class Services { return []; } } - - public async checkTransaction(tx: string): Promise { + + public async updateOwnedOutputsForUser(): Promise { const services = await Services.getInstance(); + let latest_outputs: outputs_list; try { - return services.sdkClient.check_network_transaction(tx); + latest_outputs = services.sdkClient.get_outpoints_for_user(); + } catch (error) { + console.error(error); + return; + } + + try { + let user = await services.getUserInfo(); + if (user) { + user.outputs = latest_outputs; + // console.warn(user); + await services.updateUser(user); + } } catch (error) { console.error(error); - return null; } } - + public async getAllProcessForUser(pre_id: string): Promise { const services = await Services.getInstance(); let user: User; let userProcessList: Process[] = []; try { const indexedDB = await IndexedDB.getInstance(); - const db = indexedDB.getDb(); + const db = await indexedDB.getDb(); user = await indexedDB.getObject(db, indexedDB.getStoreList().AnkUser, pre_id); } catch (error) { console.error('getAllUserProcess failed: ',error); @@ -363,29 +415,49 @@ class Services { public async getProcessByName(name: string): Promise { console.log('getProcessByName name: '+name); const indexedDB = await IndexedDB.getInstance(); - const db = indexedDB.getDb(); + const db = await indexedDB.getDb(); const process = await indexedDB.getFirstMatchWithIndex(db, indexedDB.getStoreList().AnkProcess, 'by_name', name); console.log('getProcessByName process: '+process); return process; } - public async loadProcesses(): Promise { + public async updateMessages(message: CachedMessage): 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 removeMessage(id: number): Promise { + const indexedDb = await IndexedDB.getInstance(); + const db = await indexedDb.getDb(); + + try { + await indexedDb.rmObject(db, indexedDb.getStoreList().AnkMessages, id); + } catch (error) { + throw error; + } + } + + public async updateProcesses(): Promise { const services = await Services.getInstance(); const processList: Process[] = services.sdkClient.get_processes(); - console.error('processList size: '+processList.length); processList.forEach(async (process: Process) => { const indexedDB = await IndexedDB.getInstance(); - const db = indexedDB.getDb(); + const db = await indexedDB.getDb(); try { const processStore = await indexedDB.getObject(db, indexedDB.getStoreList().AnkProcess, process.id); if (!processStore) { - console.error('Adding process.id : '+process.id); await indexedDB.writeObject(db, indexedDB.getStoreList().AnkProcess, process, null); } } catch (error) { - console.warn('Error while writing process', process.name, 'to indexedDB:', error); + console.error('Error while writing process', process.name, 'to indexedDB:', error); } }) } @@ -401,6 +473,190 @@ class Services { element?.removeEventListener("submit", callback); element?.addEventListener("submit", callback); } + public async revokeInjectHtml() { + const container = document.getElementById('containerId'); + + if (!container) { + console.error("No html container"); + return; + } + + container.innerHTML = + `
+
+

Revoke an Id

+
+ Recover +
+
+
+ + +
+
+ + +
+
+ +
+
+ `; + } + public async revokeImageInjectHtml() { + const container = document.getElementById('containerId'); + + if (!container) { + console.error("No html container"); + return; + } + + container.innerHTML = + `
+
+

Revoke image

+ +
+
+
+ + + + + +
+ +
+
`; + } + + public async recoverInjectHtml() { + const container = document.getElementById('containerId'); + + if (!container) { + console.error("No html container"); + return; + } + + const services = await Services.getInstance(); + await services.updateProcesses(); + + container.innerHTML = + `
+
+

Recover my Id

+ +
+
+ + + +
+
+ + +

+ Revoke +

+
+
`; + } + + public async createIdInjectHtml() { + const container = document.getElementById('containerId'); + + if (!container) { + console.error("No html container"); + return; + } + + container.innerHTML = + `
+
+

Create an Id

+ +
+
+ +
+ +
+
+ +
+ Recover +
+
+

+
+
`; + } + + public async updateIdInjectHtml() { + const container = document.getElementById('containerId'); + + if (!container) { + console.error("No html container"); + return; + } + + container.innerHTML = + ` +
+
+

Update an Id

+
+
+
+ + + + + + + + + + + + + +
+
+ + +
+
+
+ +
+ +
+
+ `; + } public async injectHtml(processName: string) { const container = document.getElementById('containerId'); @@ -492,19 +748,161 @@ class Services { } } - public async obtainTokenWithFaucet(spaddress: string): Promise { + public async obtainTokenWithFaucet(): Promise { const services = await Services.getInstance(); const connection = await services.pickWebsocketConnectionRandom(); if (!connection) { - return null; + throw 'no available relay connections'; } + + let cachedMsg: CachedMessage; try { - connection.sendMessage('faucet'+spaddress); + const flag: AnkFlag = 'Faucet'; + cachedMsg = services.sdkClient.create_faucet_msg(); + if (cachedMsg.commitment && cachedMsg.recipient) { + let faucetMsg: FaucetMessage = { + sp_address: cachedMsg.recipient, + commitment: cachedMsg.commitment, + error: null, + } + connection.sendMessage(flag, JSON.stringify(faucetMsg)); + } } catch (error) { - console.error("Failed to obtain tokens with relay ", connection.getUrl()); - return null; + throw `Failed to obtain tokens with relay ${connection.getUrl()}: ${error}`; + } + + try { + await services.updateMessages(cachedMsg); + } catch (error) { + throw error; + } + } + + public async updateUser(user: User): Promise { + try { + const indexedDB = await IndexedDB.getInstance(); + const db = await indexedDB.getDb(); + await indexedDB.setObject(db, indexedDB.getStoreList().AnkUser, user, null); + } catch (error) { + throw error; + } + } + + public async getUserInfo(): Promise { + try { + const indexedDB = await IndexedDB.getInstance(); + const db = await indexedDB.getDb(); + let user = await indexedDB.getAll(db, indexedDB.getStoreList().AnkUser); + // This should never happen + if (user.length > 1) { + throw "Multiple users in db"; + } else { + let res = user.pop(); + if (res === undefined) { + return null; + } else { + return res; + } + } + } catch (error) { + throw error; + } + } + + public async answer_confirmation_message(msg: CachedMessage): Promise { + const services = await Services.getInstance(); + const connection = await services.pickWebsocketConnectionRandom(); + if (!connection) { + throw new Error("No connection to relay"); + } + let user: User; + try { + let possibleUser = await services.getUserInfo(); + if (!possibleUser) { + throw new Error("No user loaded, please first create a new user or login"); + } else { + user = possibleUser; + } + } catch (error) { + throw error; + } + + let notificationInfo: createTransactionReturn; + try { + const feeRate = 1; + notificationInfo = services.sdkClient.answer_confirmation_transaction(msg.id, feeRate); + } catch (error) { + throw new Error(`Failed to create confirmation transaction: ${error}`); + } + const flag: AnkFlag = "NewTx"; + const newTxMsg: NewTxMessage = { + 'transaction': notificationInfo.transaction, + 'tweak_data': null, + 'error': null, + } + connection.sendMessage(flag, JSON.stringify(newTxMsg)); + await services.updateMessages(notificationInfo.new_network_msg); + return; + } + + public async confirm_sender_address(msg: CachedMessage): Promise { + const services = await Services.getInstance(); + const connection = await services.pickWebsocketConnectionRandom(); + if (!connection) { + throw new Error("No connection to relay"); + } + let user: User; + try { + let possibleUser = await services.getUserInfo(); + if (!possibleUser) { + throw new Error("No user loaded, please first create a new user or login"); + } else { + user = possibleUser; + } + } catch (error) { + throw error; + } + + let notificationInfo: createTransactionReturn; + try { + const feeRate = 1; + notificationInfo = services.sdkClient.create_confirmation_transaction(msg.id, feeRate); + } catch (error) { + throw new Error(`Failed to create confirmation transaction: ${error}`); + } + const flag: AnkFlag = "NewTx"; + const newTxMsg: NewTxMessage = { + 'transaction': notificationInfo.transaction, + 'tweak_data': null, + 'error': null, + } + connection.sendMessage(flag, JSON.stringify(newTxMsg)); + await services.updateMessages(notificationInfo.new_network_msg); + return; + } + + public async notify_address_for_message(sp_address: string, message: CipherMessage): Promise { + const services = await Services.getInstance(); + const connection = await services.pickWebsocketConnectionRandom(); + if (!connection) { + throw 'No available connection'; + } + + try { + const feeRate = 1; + let notificationInfo: createTransactionReturn = services.sdkClient.create_notification_transaction(sp_address, message, feeRate); + const flag: AnkFlag = "NewTx"; + const newTxMsg: NewTxMessage = { + 'transaction': notificationInfo.transaction, + 'tweak_data': null, + 'error': null, + } + connection.sendMessage(flag, JSON.stringify(newTxMsg)); + console.info('Successfully sent notification transaction'); + return notificationInfo; + } catch (error) { + throw 'Failed to create notification transaction:", error'; } - return null; } } diff --git a/src/websockets.ts b/src/websockets.ts index 72d8c80..6cc35c5 100644 --- a/src/websockets.ts +++ b/src/websockets.ts @@ -1,5 +1,5 @@ import Services from "./services"; -// import * as mempool from "./mempool"; +import { AnkFlag, AnkNetworkMsg, CachedMessage } from "../dist/pkg/sdk_client"; class WebSocketClient { private ws: WebSocket; @@ -22,31 +22,58 @@ class WebSocketClient { // Listen for messages this.ws.addEventListener('message', (event) => { const msgData = event.data; - console.log(msgData); (async () => { - if (msgData instanceof Blob) { - // bitcoin network msg is just bytes - let res = await services.parseBitcoinMessage(msgData); - if (res) { - let ours = await services.checkTransaction(res); - if (ours) { - console.log("Found our utxo in "+res); - } else { - console.log("No utxo found in tx "+res); - } - } else { - console.error("Faile to parse a bitcoin network msg"); - } - } else if (typeof(msgData) === 'string') { - // json strings are 4nk message + if (typeof(msgData) === 'string') { console.log("Received text message: "+msgData); - let res = await services.parse4nkMessage(msgData); - if (res) { + try { + const feeRate = 1; + // By parsing the message, we can link it with existing cached message and return the updated version of the message + let res: CachedMessage = await services.parseNetworkMessage(msgData, feeRate); console.debug(res); + if (res.status === 'FaucetComplete') { + // we received a faucet tx, there's nothing else to do + window.alert(`New faucet output\n${res.commited_in}`); + await services.updateMessages(res); + await services.updateOwnedOutputsForUser(); + } else if (res.status === 'TxWaitingCipher') { + // we received a tx but we don't have the cipher + console.debug(`received notification in output ${res.commited_in}, waiting for cipher message`); + await services.updateMessages(res); + await services.updateOwnedOutputsForUser(); + } else if (res.status === 'CipherWaitingTx') { + // we received a cipher but we don't have the key + console.debug(`received a cipher`); + await services.updateMessages(res); + } else if (res.status === 'SentWaitingConfirmation') { + // We are sender and we're waiting for the challenge that will confirm recipient got the transaction and the message + await services.updateMessages(res); + await services.updateOwnedOutputsForUser(); + } else if (res.status === 'MustSpendConfirmation') { + // we received a challenge for a notification we made + // that means we can stop rebroadcasting the tx and we must spend the challenge to confirm + window.alert(`Spending ${res.confirmed_by} to prove our identity`); + console.debug(`sending confirm message to ${res.recipient}`); + await services.updateMessages(res); + await services.answer_confirmation_message(res); + } else if (res.status === 'ReceivedMustConfirm') { + // we found a notification and decrypted the cipher + window.alert(`Received message from ${res.sender}\n${res.plaintext}`); + // we must spend the commited_in output to sender + await services.updateMessages(res); + await services.confirm_sender_address(res); + } else if (res.status === 'Complete') { + window.alert(`Received confirmation that ${res.sender} is the author of message ${res.plaintext}`) + await services.updateMessages(res); + await services.updateOwnedOutputsForUser(); + } else { + console.debug('Received an unimplemented valid message'); + } + } catch (error) { + console.error('Received an invalid message:', error); } } else { - console.error("Received an unknown message"); + console.error('Received a non-string message'); } })(); }); @@ -63,11 +90,16 @@ class WebSocketClient { } // Method to send messages - public sendMessage(message: string): void { + public sendMessage(flag: AnkFlag, message: string): void { if (this.ws.readyState === WebSocket.OPEN) { - this.ws.send(message); + const networkMessage: AnkNetworkMsg = { + 'flag': flag, + 'content': message + } + // console.debug("Sending message:", JSON.stringify(networkMessage)); + this.ws.send(JSON.stringify(networkMessage)); } else { - console.error('WebSocket is not open. ReadyState:', this.ws.readyState); + console.warn('WebSocket is not open. ReadyState:', this.ws.readyState); this.messageQueue.push(message); } }