From 7d22c033bbb6e7ace03a2bd05a97bd0535c81147 Mon Sep 17 00:00:00 2001 From: Sosthene00 <674694@protonmail.ch> Date: Wed, 17 Apr 2024 08:31:55 +0200 Subject: [PATCH 01/40] mv crypto and network to common --- crates/sp_client/Cargo.toml | 3 +- crates/sp_client/src/crypto.rs | 447 -------------------------------- crates/sp_client/src/lib.rs | 2 - crates/sp_client/src/network.rs | 94 ------- crates/sp_client/src/user.rs | 9 +- 5 files changed, 4 insertions(+), 551 deletions(-) delete mode 100644 crates/sp_client/src/crypto.rs delete mode 100644 crates/sp_client/src/network.rs diff --git a/crates/sp_client/Cargo.toml b/crates/sp_client/Cargo.toml index 25f7965..1a725bc 100644 --- a/crates/sp_client/Cargo.toml +++ b/crates/sp_client/Cargo.toml @@ -18,8 +18,7 @@ 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 = { 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/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/lib.rs b/crates/sp_client/src/lib.rs index 95968cb..c0056a6 100644 --- a/crates/sp_client/src/lib.rs +++ b/crates/sp_client/src/lib.rs @@ -5,9 +5,7 @@ use std::sync::{Mutex, MutexGuard}; mod Prd_list; pub mod api; -mod crypto; mod images; -mod network; mod peers; mod process; mod silentpayments; 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/user.rs b/crates/sp_client/src/user.rs index 202ed8d..d1720c1 100644 --- a/crates/sp_client/src/user.rs +++ b/crates/sp_client/src/user.rs @@ -1,8 +1,3 @@ -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 serde::{Deserialize, Serialize}; @@ -29,10 +24,12 @@ use sp_backend::silentpayments::sending::SilentPaymentAddress; use sp_backend::spclient::SpendKey; use sp_backend::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; From 0d42c289cb4cb9ef9a2a2750d1b0729b421e6096 Mon Sep 17 00:00:00 2001 From: Sosthene00 <674694@protonmail.ch> Date: Wed, 17 Apr 2024 08:41:38 +0200 Subject: [PATCH 02/40] mv html to ts, update process acquisition --- crates/sp_client/src/api.rs | 66 +++------ crates/sp_client/src/process.rs | 177 +++++++---------------- src/services.ts | 240 ++++++++++++++++++++++++++------ 3 files changed, 263 insertions(+), 220 deletions(-) diff --git a/crates/sp_client/src/api.rs b/crates/sp_client/src/api.rs index ea6bd9e..2788999 100644 --- a/crates/sp_client/src/api.rs +++ b/crates/sp_client/src/api.rs @@ -197,60 +197,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 +238,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)) } diff --git a/crates/sp_client/src/process.rs b/crates/sp_client/src/process.rs index 56f95d6..2e6af74 100644 --- a/crates/sp_client/src/process.rs +++ b/crates/sp_client/src/process.rs @@ -6,144 +6,63 @@ 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/src/services.ts b/src/services.ts index 7160fbc..e7de153 100644 --- a/src/services.ts +++ b/src/services.ts @@ -24,27 +24,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); @@ -70,7 +52,7 @@ 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(); @@ -136,8 +118,8 @@ class Services { 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); @@ -168,7 +150,7 @@ class Services { 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'); @@ -209,7 +191,7 @@ class Services { 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,24 +206,8 @@ 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); } @@ -370,22 +336,20 @@ class Services { return process; } - public async loadProcesses(): Promise { + 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 +365,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'); From 8fb517c1072498f6b946d6353539a6b5c9bafd9f Mon Sep 17 00:00:00 2001 From: Sosthene00 <674694@protonmail.ch> Date: Wed, 17 Apr 2024 08:58:28 +0200 Subject: [PATCH 03/40] Update faucet --- crates/sp_client/src/api.rs | 74 ++++++++++++++++++-------- crates/sp_client/src/silentpayments.rs | 60 ++++++--------------- src/services.ts | 55 ++++++------------- src/websockets.ts | 37 ++++++------- 4 files changed, 103 insertions(+), 123 deletions(-) diff --git a/crates/sp_client/src/api.rs b/crates/sp_client/src/api.rs index 2788999..c9b80b6 100644 --- a/crates/sp_client/src/api.rs +++ b/crates/sp_client/src/api.rs @@ -3,15 +3,18 @@ use std::collections::HashMap; use std::str::FromStr; use std::sync::{Mutex, OnceLock, PoisonError}; +use log::debug; use rand::Rng; use anyhow::Error as AnyhowError; +use sdk_common::crypto::AnkSharedSecret; use serde_json::Error as SerdeJsonError; use shamir::SecretData; -use sp_backend::bitcoin::consensus::deserialize; -use sp_backend::bitcoin::hex::{FromHex, HexToBytesError}; +use sp_backend::bitcoin::consensus::{deserialize, serialize}; +use sp_backend::bitcoin::hex::{parse, DisplayHex, FromHex, HexToBytesError}; +use sp_backend::bitcoin::secp256k1::ecdh::SharedSecret; use sp_backend::bitcoin::secp256k1::{PublicKey, SecretKey}; -use sp_backend::bitcoin::{Transaction, Txid}; +use sp_backend::bitcoin::{OutPoint, Transaction, Txid}; use sp_backend::silentpayments::Error as SpError; use serde::{Deserialize, Serialize}; @@ -20,12 +23,13 @@ use tsify::Tsify; use wasm_bindgen::convert::FromWasmAbi; use wasm_bindgen::prelude::*; -use sp_backend::spclient::{derive_keys_from_seed, OutputList, SpClient}; +use sdk_common::network::{AnkFlag, AnkNetworkMsg, NewTxMessage}; + +use sp_backend::spclient::{derive_keys_from_seed, OutputList, OwnedOutput, SpClient}; use sp_backend::spclient::{SpWallet, SpendKey}; use crate::images; -use crate::network::{BitcoinNetworkMsg, BitcoinTopic, AnkNetworkMsg, AnkTopic}; -use crate::silentpayments::check_transaction; +use crate::silentpayments::{check_transaction, create_transaction_for_address}; use crate::user::{lock_connected_users, User, UserWallets, CONNECTED_USERS}; use crate::process::Process; @@ -264,7 +268,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); @@ -296,30 +300,58 @@ pub fn login_user( #[wasm_bindgen] pub fn check_transaction_for_silent_payments( tx_hex: String, + blockheight: u32, tweak_data_hex: String, -) -> ApiResult<()> { +) -> ApiResult { let tx = deserialize::(&Vec::from_hex(&tx_hex)?)?; let tweak_data = PublicKey::from_str(&tweak_data_hex)?; - check_transaction(tx, tweak_data); + let updated_user = check_transaction(&tx, blockheight, tweak_data)?; - Ok(()) + Ok(updated_user) +} + +#[derive(Tsify, Serialize, Deserialize)] +#[tsify(into_wasm_abi, from_wasm_abi)] +#[allow(non_camel_case_types)] +pub struct parseNetworkMsgReturn { + topic: String, + message: String, } #[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); +pub fn parse_network_msg(raw: String) -> 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)?; + let tx = deserialize::(&Vec::from_hex(&tx_message.transaction)?)?; + if tx_message.tweak_data.is_none() { + return Err(ApiError { + message: "Missing tweak_data".to_owned(), + }); + } + let partial_tweak = PublicKey::from_str(&tx_message.tweak_data.unwrap())?; + let txid = check_transaction(&tx, 0, partial_tweak)?; + return Ok(parseNetworkMsgReturn { + topic: AnkFlag::NewTx.as_str().to_owned(), + message: txid, + }); + } + AnkFlag::Faucet => unimplemented!(), + AnkFlag::Error => { + return Ok(parseNetworkMsgReturn { + topic: AnkFlag::Error.as_str().to_owned(), + message: ank_msg.content.to_owned(), + }) + } + _ => unimplemented!(), } - BitcoinTopic::RawBlock => (), + } else { + Err(ApiError { + message: format!("Can't parse message as a valid 4nk message: {}", raw), + }) } - - Ok(()) } #[wasm_bindgen] diff --git a/crates/sp_client/src/silentpayments.rs b/crates/sp_client/src/silentpayments.rs index 5ab45d4..14964f6 100644 --- a/crates/sp_client/src/silentpayments.rs +++ b/crates/sp_client/src/silentpayments.rs @@ -18,59 +18,33 @@ 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(); +pub fn check_transaction( + tx: &Transaction, + blockheight: u32, + tweak_data: PublicKey, +) -> Result { + let connected_users = lock_connected_users()?; + let txid = tx.txid().to_string(); // 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); + let mut recover = keys.recover; + if recover.update_wallet_with_transaction(tx, blockheight, tweak_data)? > 0 { + return Ok(txid); } - 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(mut main) = keys.main { + if main.update_wallet_with_transaction(tx, blockheight, tweak_data)? > 0 { + return Ok(txid); } } - 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); + if let Some(mut revoke) = keys.revoke { + if revoke.update_wallet_with_transaction(tx, blockheight, tweak_data)? > 0 { + return Ok(txid); } } } - Ok(HashMap::new()) + return Err(Error::msg("No new outputs found")); } diff --git a/src/services.ts b/src/services.ts index e7de153..b7952d4 100644 --- a/src/services.ts +++ b/src/services.ts @@ -177,18 +177,6 @@ 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(); await services.revokeInjectHtml(); @@ -211,30 +199,15 @@ class Services { 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): Promise { + const services = await Services.getInstance(); + try { + const msg: parseNetworkMsgReturn = services.sdkClient.parse_network_msg(raw); 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) { + console.error(error); + return null; } - 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 { @@ -288,11 +261,13 @@ class Services { return []; } } - - public async checkTransaction(tx: string): Promise { + public async checkTransaction(tx: string, tweak_data: string, blkheight: number): Promise { const services = await Services.getInstance(); + try { - return services.sdkClient.check_network_transaction(tx); + const updated_user: string = services.sdkClient.check_transaction_for_silent_payments(tx, blkheight, tweak_data); + await services.updateOwnedOutputsForUser(updated_user); + return updated_user; } catch (error) { console.error(error); return null; @@ -647,7 +622,11 @@ class Services { return null; } try { - connection.sendMessage('faucet'+spaddress); + const flag: AnkFlag = "Faucet"; + const faucetMsg: FaucetMessage = { + 'sp_address': spaddress + } + connection.sendMessage(flag, JSON.stringify(faucetMsg)); } catch (error) { console.error("Failed to obtain tokens with relay ", connection.getUrl()); return null; diff --git a/src/websockets.ts b/src/websockets.ts index 72d8c80..c73e7dd 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, parseNetworkMsgReturn } from "../dist/pkg/sdk_client"; class WebSocketClient { private ws: WebSocket; @@ -25,28 +25,18 @@ class WebSocketClient { 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); + let res = await services.parseNetworkMessage(msgData); if (res) { - console.debug(res); + if (res.topic === 'new_tx') { + // we received a tx + window.alert(`New tx\n${res.message}`); + await services.updateOwnedOutputsForUser(res.message); + } } } else { - console.error("Received an unknown message"); + console.error("Received an invalid message"); } })(); }); @@ -63,9 +53,14 @@ 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); this.messageQueue.push(message); From d6a473f6e917fb120aa7c261eb85c34dc3207356 Mon Sep 17 00:00:00 2001 From: Sosthene00 <674694@protonmail.ch> Date: Wed, 17 Apr 2024 09:12:12 +0200 Subject: [PATCH 04/40] fix indexeddb --- src/database.ts | 30 ++++-------------------------- src/services.ts | 12 ++++++------ 2 files changed, 10 insertions(+), 32 deletions(-) diff --git a/src/database.ts b/src/database.ts index d8d8041..3c2fc30 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", @@ -92,11 +70,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} { diff --git a/src/services.ts b/src/services.ts index b7952d4..f159552 100644 --- a/src/services.ts +++ b/src/services.ts @@ -39,7 +39,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; @@ -97,7 +97,7 @@ class Services { 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); @@ -243,7 +243,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); @@ -253,7 +253,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) { @@ -280,7 +280,7 @@ class Services { 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); @@ -304,7 +304,7 @@ 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); From 18f159696b32cd4d8062a084601a83da6c2475a7 Mon Sep 17 00:00:00 2001 From: Sosthene00 <674694@protonmail.ch> Date: Wed, 17 Apr 2024 09:13:19 +0200 Subject: [PATCH 05/40] fix image download --- src/services.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/services.ts b/src/services.ts index f159552..6e7a86c 100644 --- a/src/services.ts +++ b/src/services.ts @@ -158,6 +158,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'; } } } From 8e60ec6ef0c2b4204c66d8fa9065d0d1d56a5c5b Mon Sep 17 00:00:00 2001 From: Sosthene00 <674694@protonmail.ch> Date: Wed, 17 Apr 2024 09:14:00 +0200 Subject: [PATCH 06/40] recover --- src/services.ts | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/services.ts b/src/services.ts index 6e7a86c..ee5176a 100644 --- a/src/services.ts +++ b/src/services.ts @@ -128,7 +128,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; @@ -140,12 +139,25 @@ 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_receiving_address(user?.pre_id); + } + } catch (error) { + console.error(error); + } + + // TODO: check blocks since last_scan and update outputs + + await services.displaySendMessage(); } public async displayRevokeImage(revokeData: Uint8Array): Promise { From 51465902bd4c60580e717aca1684c42d91bb53d3 Mon Sep 17 00:00:00 2001 From: Sosthene00 <674694@protonmail.ch> Date: Wed, 17 Apr 2024 09:14:36 +0200 Subject: [PATCH 07/40] update create id --- src/services.ts | 50 +++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/src/services.ts b/src/services.ts index ee5176a..f703aa6 100644 --- a/src/services.ts +++ b/src/services.ts @@ -61,6 +61,19 @@ class Services { 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) { + console.error(error); + return; + } + const passwordElement = document.getElementById("password") as HTMLInputElement; const processElement = document.getElementById("selectProcess") as HTMLSelectElement; @@ -79,12 +92,11 @@ 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 user = createUserReturn.user; - const shares = user.shares; + // const shares = user.shares; // send the shares on the network const revokeData = user.revoke_data; if (!revokeData) { @@ -92,7 +104,7 @@ class Services { return; } - user.shares = []; + // user.shares = []; user.revoke_data = null; try { @@ -103,16 +115,16 @@ class Services { console.error("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); + this.sp_address = services.sdkClient.get_receiving_address(user.pre_id); + if (this.sp_address) { + console.info('Using sp_address:', this.sp_address); + await services.obtainTokenWithFaucet(this.sp_address); + } } catch (error) { console.error(error); } - await services.obtainTokenWithFaucet(sp_address); - await services.displayRevokeImage(new Uint8Array(revokeData)); } @@ -651,6 +663,28 @@ class Services { } return null; } + + 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) { + console.error("Can't get user from db"); + return null; + } + } } export default Services; From 3330baefc17b947961bd0f4823fd71c2fdab4869 Mon Sep 17 00:00:00 2001 From: Sosthene00 <674694@protonmail.ch> Date: Wed, 17 Apr 2024 09:15:15 +0200 Subject: [PATCH 08/40] Add send message screen --- src/services.ts | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/services.ts b/src/services.ts index f703aa6..6005be8 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, createNotificationTransactionReturn, parse_network_msg, outputs_list, parseNetworkMsgReturn, FaucetMessage, AnkFlag } 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() {} @@ -58,6 +59,37 @@ class Services { 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 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 services = await Services.getInstance(); + + let notificationInfo = services.notify_address_for_message(recipientSpAddress, message); + console.log(notificationInfo); + } + public async createId(event: Event): Promise { event.preventDefault(); From a6f4a5122cd4d44155f38636d8ffe96329b644a4 Mon Sep 17 00:00:00 2001 From: Sosthene00 <674694@protonmail.ch> Date: Wed, 17 Apr 2024 09:15:30 +0200 Subject: [PATCH 09/40] update user outputs --- src/services.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/services.ts b/src/services.ts index 6005be8..593557b 100644 --- a/src/services.ts +++ b/src/services.ts @@ -323,6 +323,29 @@ class Services { return []; } } + + public async updateOwnedOutputsForUser(preId: string): Promise { + const services = await Services.getInstance(); + let latest_outputs: outputs_list; + try { + latest_outputs = services.sdkClient.get_outpoints_for_user(preId); + } catch (error) { + console.error(error); + return; + } + + try { + const indexedDB = await IndexedDB.getInstance(); + const db = await indexedDB.getDb(); + const storeName = indexedDB.getStoreList().AnkUser; + let user = await indexedDB.getObject(db, storeName, preId); + user.outputs = latest_outputs; + await indexedDB.setObject(db, storeName, user, null); + } catch (error) { + console.error(error); + } + } + public async checkTransaction(tx: string, tweak_data: string, blkheight: number): Promise { const services = await Services.getInstance(); From 8f6918748dcad0897facb361d0d9c6b434f2109c Mon Sep 17 00:00:00 2001 From: Sosthene00 <674694@protonmail.ch> Date: Wed, 17 Apr 2024 09:15:44 +0200 Subject: [PATCH 10/40] add notify address for message --- crates/sp_client/src/api.rs | 81 ++++++++++++++++++++------ crates/sp_client/src/silentpayments.rs | 80 ++++++++++++++++++++++++- src/services.ts | 24 ++++++++ 3 files changed, 165 insertions(+), 20 deletions(-) diff --git a/crates/sp_client/src/api.rs b/crates/sp_client/src/api.rs index c9b80b6..3182318 100644 --- a/crates/sp_client/src/api.rs +++ b/crates/sp_client/src/api.rs @@ -355,24 +355,69 @@ pub fn parse_network_msg(raw: String) -> ApiResult { } #[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 - } - } - } - } +pub fn get_outpoints_for_user(pre_id: String) -> ApiResult { + let connected_users = lock_connected_users()?; + let user = connected_users.get(&pre_id).ok_or(ApiError { + message: "Can't find user".to_owned(), + })?; + Ok(outputs_list(user.get_all_outputs())) +} + +#[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_users = lock_connected_users()?; + let user = connected_users.get(&pre_id).ok_or(ApiError { + message: "Can't find user".to_owned(), + })?; + + if let Some(_) = user + .recover + .get_outputs() + .to_outpoints_list() + .iter() + .find(|(outpoint, output)| outpoint.txid == txid) + { + Ok(true) } else { - log::debug!("Can't parse message as a valid 4nk message: {}", raw); - None + Ok(false) } } + +#[derive(Tsify, Serialize, Deserialize)] +#[tsify(into_wasm_abi, from_wasm_abi)] +#[allow(non_camel_case_types)] +pub struct createNotificationTransactionReturn { + pub transaction: String, + pub spaddress2secret: HashMap, +} + +#[wasm_bindgen] +pub fn create_notification_transaction( + user_pre_id: String, + recipient: String, + message: String, +) -> ApiResult { + let sp_address: SilentPaymentAddress = recipient.try_into()?; + + let (transaction, notification_information) = + create_transaction_for_address(user_pre_id, sp_address, message)?; + + // The secret is an ecc point and *must* be hashed to produce a proper ecdh secret + // For now we propose to implement a tagged hash for it + // It could be interesting to add some piece of data to allow for the derivation of multiple secrets + + let spaddress2secret = notification_information + .into_iter() + .map(|(address, shared_pubkey)| { + let shared_secret = AnkSharedSecret::new_from_public_key(shared_pubkey); + (address.into(), shared_secret.to_string()) + }) + .collect(); + + Ok(createNotificationTransactionReturn { + transaction: serialize(&transaction).to_lower_hex_string(), + spaddress2secret, + }) +} diff --git a/crates/sp_client/src/silentpayments.rs b/crates/sp_client/src/silentpayments.rs index 14964f6..cee9658 100644 --- a/crates/sp_client/src/silentpayments.rs +++ b/crates/sp_client/src/silentpayments.rs @@ -1,8 +1,14 @@ use std::collections::HashMap; -use anyhow::Result; +use anyhow::{Error, Result}; +use rand::Rng; +use sp_backend::bitcoin::policy::DUST_RELAY_TX_FEE; +use sp_backend::bitcoin::secp256k1::ecdh::SharedSecret; +use sp_backend::bitcoin::{block, Amount, OutPoint}; +use sp_backend::silentpayments::sending::SilentPaymentAddress; use sp_backend::silentpayments::utils::receiving::calculate_shared_secret; +use sp_backend::spclient::{OutputList, OwnedOutput, Recipient, SpClient}; use sp_backend::{ bitcoin::{ secp256k1::{PublicKey, Scalar, XOnlyPublicKey}, @@ -15,9 +21,79 @@ use crate::user::{lock_connected_users, CONNECTED_USERS}; type FoundOutputs = HashMap, HashMap>; -pub fn check_transaction(tx: Transaction, tweak_data: PublicKey) -> Result { +type NotificationInformation = (Transaction, Vec<(SilentPaymentAddress, PublicKey)>); + +pub fn create_transaction_for_address( + send_as: String, + sp_address: SilentPaymentAddress, + message: String, +) -> Result { let connected_users = lock_connected_users()?; + let sender = connected_users + .get(&send_as) + .ok_or(Error::msg("Unknown sender"))?; + + let sp_wallet = if sp_address.is_testnet() { + &sender.recover + } else { + if let Some(main) = &sender.main { + main + } else { + return Err(Error::msg("Can't spend on mainnet")); + } + }; + + let available_outpoints = sender.recover.get_outputs().to_spendable_list(); + + // Here we need to add more heuristics about which outpoint we spend + // For now let's keep it simple + + let mut inputs: HashMap = HashMap::new(); + + let total_available = + available_outpoints + .into_iter() + .try_fold(Amount::from_sat(0), |acc, (outpoint, output)| { + let new_total = acc + output.amount; + inputs.insert(outpoint, output); + if new_total > Amount::from_sat(1000) { + Err(new_total) + } else { + Ok(new_total) + } + }); + + match total_available { + Err(total) => log::debug!("Spending {} outputs totaling {} sats", inputs.len(), total), + Ok(_) => return Err(Error::msg("Not enought fund available")), + } + + let recipient = Recipient { + address: sp_address.into(), + amount: Amount::from_sat(1000), + nb_outputs: 1, + }; + + let mut new_psbt = sp_wallet + .get_client() + .create_new_psbt(inputs, vec![recipient], None)?; + log::debug!("Created psbt: {}", new_psbt); + SpClient::set_fees(&mut new_psbt, Amount::from_sat(1000), sp_address.into())?; + let shared_secrets: Vec<(SilentPaymentAddress, PublicKey)> = + sp_wallet.get_client().fill_sp_outputs(&mut new_psbt)?; + log::debug!("Definitive psbt: {}", new_psbt); + let mut aux_rand = [0u8; 32]; + rand::thread_rng().fill(&mut aux_rand); + let mut signed = sp_wallet.get_client().sign_psbt(new_psbt, &aux_rand)?; + log::debug!("signed psbt: {}", signed); + SpClient::finalize_psbt(&mut signed)?; + + let final_tx = signed.extract_tx()?; + + Ok((final_tx, shared_secrets)) +} + pub fn check_transaction( tx: &Transaction, blockheight: u32, diff --git a/src/services.ts b/src/services.ts index 593557b..1643b09 100644 --- a/src/services.ts +++ b/src/services.ts @@ -740,6 +740,30 @@ class Services { return null; } } + + public async notify_address_for_message(sp_address: string, message: string): Promise { + const services = await Services.getInstance(); + 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; + } + + try { + let notificationInfo: createNotificationTransactionReturn = services.sdkClient.create_notification_transaction(user, sp_address, message); + return notificationInfo; + } catch { + console.error("Failed to create notification transaction for user", user); + return null + } + } } export default Services; From 1ac419edb7bab4faa75100c55d7577a5d730d74d Mon Sep 17 00:00:00 2001 From: Sosthene00 <674694@protonmail.ch> Date: Wed, 17 Apr 2024 21:53:29 +0200 Subject: [PATCH 11/40] Update sp_backend to sp_client --- crates/sp_client/Cargo.toml | 2 +- crates/sp_client/src/Prd_list.rs | 4 ++-- crates/sp_client/src/api.rs | 26 +++++++++++++------------- crates/sp_client/src/images.rs | 2 +- crates/sp_client/src/process.rs | 2 +- crates/sp_client/src/silentpayments.rs | 14 +++++++------- crates/sp_client/src/user.rs | 22 +++++++++++----------- 7 files changed, 36 insertions(+), 36 deletions(-) diff --git a/crates/sp_client/Cargo.toml b/crates/sp_client/Cargo.toml index 1a725bc..2155567 100644 --- a/crates/sp_client/Cargo.toml +++ b/crates/sp_client/Cargo.toml @@ -8,7 +8,7 @@ name = "sdk_client" crate-type = ["cdylib"] [dependencies] -sp_backend = { git = "https://github.com/Sosthene00/sp-backend", branch = "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/Prd_list.rs b/crates/sp_client/src/Prd_list.rs index e8e8185..9ed63d0 100644 --- a/crates/sp_client/src/Prd_list.rs +++ b/crates/sp_client/src/Prd_list.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; -use sp_backend::bitcoin::PublicKey; -//use sp_backend::silentpayments::sending::SilentPaymentAddress; +use sp_client::bitcoin::PublicKey; +//use sp_client::silentpayments::sending::SilentPaymentAddress; use std::marker::Copy; use tsify::Tsify; use wasm_bindgen::prelude::*; diff --git a/crates/sp_client/src/api.rs b/crates/sp_client/src/api.rs index 3182318..213a6b0 100644 --- a/crates/sp_client/src/api.rs +++ b/crates/sp_client/src/api.rs @@ -10,23 +10,23 @@ use anyhow::Error as AnyhowError; use sdk_common::crypto::AnkSharedSecret; use serde_json::Error as SerdeJsonError; use shamir::SecretData; -use sp_backend::bitcoin::consensus::{deserialize, serialize}; -use sp_backend::bitcoin::hex::{parse, DisplayHex, FromHex, HexToBytesError}; -use sp_backend::bitcoin::secp256k1::ecdh::SharedSecret; -use sp_backend::bitcoin::secp256k1::{PublicKey, SecretKey}; -use sp_backend::bitcoin::{OutPoint, Transaction, Txid}; -use sp_backend::silentpayments::Error as SpError; +use sp_client::bitcoin::consensus::{deserialize, serialize}; +use sp_client::bitcoin::hex::{parse, DisplayHex, FromHex, HexToBytesError}; +use sp_client::bitcoin::secp256k1::ecdh::SharedSecret; +use sp_client::bitcoin::secp256k1::{PublicKey, SecretKey}; +use sp_client::bitcoin::{OutPoint, Transaction, Txid}; +use sp_client::silentpayments::Error as SpError; use serde::{Deserialize, Serialize}; -use sp_backend::silentpayments::sending::SilentPaymentAddress; +use sp_client::silentpayments::sending::SilentPaymentAddress; use tsify::Tsify; use wasm_bindgen::convert::FromWasmAbi; use wasm_bindgen::prelude::*; use sdk_common::network::{AnkFlag, AnkNetworkMsg, NewTxMessage}; -use sp_backend::spclient::{derive_keys_from_seed, OutputList, OwnedOutput, SpClient}; -use sp_backend::spclient::{SpWallet, SpendKey}; +use sp_client::spclient::{derive_keys_from_seed, OutputList, OwnedOutput, SpClient}; +use sp_client::spclient::{SpWallet, SpendKey}; use crate::images; use crate::silentpayments::{check_transaction, create_transaction_for_address}; @@ -75,16 +75,16 @@ impl From for ApiError { } } -impl From for ApiError { - fn from(value: sp_backend::bitcoin::secp256k1::Error) -> Self { +impl From for ApiError { + fn from(value: sp_client::bitcoin::secp256k1::Error) -> 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: sp_client::bitcoin::consensus::encode::Error) -> Self { ApiError { message: value.to_string(), } diff --git a/crates/sp_client/src/images.rs b/crates/sp_client/src/images.rs index f0e11ee..f0e0698 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 serde::{Deserialize, Serialize}; -use sp_backend::bitcoin::secp256k1::SecretKey; +use sp_client::bitcoin::secp256k1::SecretKey; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BackUpImage(Vec); diff --git a/crates/sp_client/src/process.rs b/crates/sp_client/src/process.rs index 2e6af74..36e1c47 100644 --- a/crates/sp_client/src/process.rs +++ b/crates/sp_client/src/process.rs @@ -2,7 +2,7 @@ use std::fmt::DebugStruct; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; -use sp_backend::silentpayments::sending::SilentPaymentAddress; +use sp_client::silentpayments::sending::SilentPaymentAddress; use tsify::Tsify; use wasm_bindgen::prelude::*; diff --git a/crates/sp_client/src/silentpayments.rs b/crates/sp_client/src/silentpayments.rs index cee9658..a82f99e 100644 --- a/crates/sp_client/src/silentpayments.rs +++ b/crates/sp_client/src/silentpayments.rs @@ -3,13 +3,13 @@ use std::collections::HashMap; use anyhow::{Error, Result}; use rand::Rng; -use sp_backend::bitcoin::policy::DUST_RELAY_TX_FEE; -use sp_backend::bitcoin::secp256k1::ecdh::SharedSecret; -use sp_backend::bitcoin::{block, Amount, OutPoint}; -use sp_backend::silentpayments::sending::SilentPaymentAddress; -use sp_backend::silentpayments::utils::receiving::calculate_shared_secret; -use sp_backend::spclient::{OutputList, OwnedOutput, Recipient, SpClient}; -use sp_backend::{ +use sp_client::bitcoin::policy::DUST_RELAY_TX_FEE; +use sp_client::bitcoin::secp256k1::ecdh::SharedSecret; +use sp_client::bitcoin::{block, Amount, OutPoint}; +use sp_client::silentpayments::sending::SilentPaymentAddress; +use sp_client::silentpayments::utils::receiving::calculate_shared_secret; +use sp_client::spclient::{OutputList, OwnedOutput, Recipient, SpClient}; +use sp_client::{ bitcoin::{ secp256k1::{PublicKey, Scalar, XOnlyPublicKey}, Transaction, diff --git a/crates/sp_client/src/user.rs b/crates/sp_client/src/user.rs index d1720c1..30a0119 100644 --- a/crates/sp_client/src/user.rs +++ b/crates/sp_client/src/user.rs @@ -2,12 +2,12 @@ use anyhow::{Error, Result}; use rand::{self, thread_rng, Rng, RngCore}; 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 sp_client::bitcoin::hashes::Hash; +use sp_client::bitcoin::hashes::HashEngine; +use sp_client::bitcoin::hex::{DisplayHex, FromHex}; +use sp_client::bitcoin::secp256k1::SecretKey; +use sp_client::bitcoin::secp256k1::ThirtyTwoByteHash; +use sp_client::spclient::SpClient; use tsify::Tsify; use wasm_bindgen::prelude::*; @@ -18,11 +18,11 @@ 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 sp_client::bitcoin::secp256k1::constants::SECRET_KEY_SIZE; +use sp_client::silentpayments::bitcoin_hashes::sha256; +use sp_client::silentpayments::sending::SilentPaymentAddress; +use sp_client::spclient::SpendKey; +use sp_client::spclient::{OutputList, SpWallet}; use crate::peers::Peer; use crate::user; From c986324c6899f260e3bce66cad4e2690922eb0f0 Mon Sep 17 00:00:00 2001 From: Sosthene00 <674694@protonmail.ch> Date: Wed, 17 Apr 2024 21:56:48 +0200 Subject: [PATCH 12/40] check_transaction_for_silent_payments returns txid --- crates/sp_client/src/api.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/sp_client/src/api.rs b/crates/sp_client/src/api.rs index 213a6b0..0eb9bf7 100644 --- a/crates/sp_client/src/api.rs +++ b/crates/sp_client/src/api.rs @@ -306,9 +306,9 @@ pub fn check_transaction_for_silent_payments( let tx = deserialize::(&Vec::from_hex(&tx_hex)?)?; let tweak_data = PublicKey::from_str(&tweak_data_hex)?; - let updated_user = check_transaction(&tx, blockheight, tweak_data)?; + let txid = check_transaction(&tx, blockheight, tweak_data)?; - Ok(updated_user) + Ok(txid) } #[derive(Tsify, Serialize, Deserialize)] From 0f185c693fc277d8ecef7b65f225e803b557fb94 Mon Sep 17 00:00:00 2001 From: Sosthene00 <674694@protonmail.ch> Date: Thu, 18 Apr 2024 00:35:16 +0200 Subject: [PATCH 13/40] fix update outputs for user --- src/services.ts | 34 +++++++++++++++++++++------------- src/websockets.ts | 2 +- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/services.ts b/src/services.ts index 1643b09..77a23ca 100644 --- a/src/services.ts +++ b/src/services.ts @@ -324,23 +324,22 @@ class Services { } } - public async updateOwnedOutputsForUser(preId: string): Promise { + public async updateOwnedOutputsForUser(): Promise { const services = await Services.getInstance(); let latest_outputs: outputs_list; try { - latest_outputs = services.sdkClient.get_outpoints_for_user(preId); + latest_outputs = services.sdkClient.get_outpoints_for_user(); } catch (error) { console.error(error); return; } try { - const indexedDB = await IndexedDB.getInstance(); - const db = await indexedDB.getDb(); - const storeName = indexedDB.getStoreList().AnkUser; - let user = await indexedDB.getObject(db, storeName, preId); - user.outputs = latest_outputs; - await indexedDB.setObject(db, storeName, user, null); + let user = await services.getUserInfo(); + if (user) { + user.outputs = latest_outputs; + await services.updateUser(user); + } } catch (error) { console.error(error); } @@ -350,9 +349,9 @@ class Services { const services = await Services.getInstance(); try { - const updated_user: string = services.sdkClient.check_transaction_for_silent_payments(tx, blkheight, tweak_data); - await services.updateOwnedOutputsForUser(updated_user); - return updated_user; + 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; @@ -719,6 +718,16 @@ class Services { return null; } + 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(); @@ -736,8 +745,7 @@ class Services { } } } catch (error) { - console.error("Can't get user from db"); - return null; + throw error; } } diff --git a/src/websockets.ts b/src/websockets.ts index c73e7dd..c94b4af 100644 --- a/src/websockets.ts +++ b/src/websockets.ts @@ -32,7 +32,7 @@ class WebSocketClient { if (res.topic === 'new_tx') { // we received a tx window.alert(`New tx\n${res.message}`); - await services.updateOwnedOutputsForUser(res.message); + await services.updateOwnedOutputsForUser(); } } } else { From b5f3c821927b25781ca01a0b0a884ca8a19f19b8 Mon Sep 17 00:00:00 2001 From: Sosthene00 <674694@protonmail.ch> Date: Thu, 18 Apr 2024 00:36:38 +0200 Subject: [PATCH 14/40] Get the shared secret from a transaction --- crates/sp_client/src/api.rs | 28 +++++++++++------------ crates/sp_client/src/silentpayments.rs | 31 ++++++++++++++------------ 2 files changed, 31 insertions(+), 28 deletions(-) diff --git a/crates/sp_client/src/api.rs b/crates/sp_client/src/api.rs index 0eb9bf7..6046111 100644 --- a/crates/sp_client/src/api.rs +++ b/crates/sp_client/src/api.rs @@ -1,5 +1,6 @@ use std::any::Any; use std::collections::HashMap; +use std::io::Write; use std::str::FromStr; use std::sync::{Mutex, OnceLock, PoisonError}; @@ -11,6 +12,7 @@ use sdk_common::crypto::AnkSharedSecret; use serde_json::Error as SerdeJsonError; use shamir::SecretData; use sp_client::bitcoin::consensus::{deserialize, serialize}; +use sp_client::silentpayments::bitcoin_hashes::{HashEngine, sha256, Hash}; use sp_client::bitcoin::hex::{parse, DisplayHex, FromHex, HexToBytesError}; use sp_client::bitcoin::secp256k1::ecdh::SharedSecret; use sp_client::bitcoin::secp256k1::{PublicKey, SecretKey}; @@ -355,9 +357,9 @@ pub fn parse_network_msg(raw: String) -> ApiResult { } #[wasm_bindgen] -pub fn get_outpoints_for_user(pre_id: String) -> ApiResult { +pub fn get_outpoints_for_user() -> ApiResult { let connected_users = lock_connected_users()?; - let user = connected_users.get(&pre_id).ok_or(ApiError { + let (_, user) = connected_users.iter().last().ok_or(ApiError { message: "Can't find user".to_owned(), })?; Ok(outputs_list(user.get_all_outputs())) @@ -395,26 +397,24 @@ pub struct createNotificationTransactionReturn { #[wasm_bindgen] pub fn create_notification_transaction( - user_pre_id: String, recipient: String, message: String, ) -> ApiResult { let sp_address: SilentPaymentAddress = recipient.try_into()?; - let (transaction, notification_information) = - create_transaction_for_address(user_pre_id, sp_address, message)?; + let (transaction, shared_point) = + create_transaction_for_address_with_shared_secret(sp_address, message)?; - // The secret is an ecc point and *must* be hashed to produce a proper ecdh secret + // The shared_point *must* be hashed to produce a proper ecdh secret // For now we propose to implement a tagged hash for it - // It could be interesting to add some piece of data to allow for the derivation of multiple secrets + // It could be interesting to add some piece of data to allow for the derivation of multiple secrets + let mut eng = sha256::HashEngine::default(); + eng.write_all(&shared_point); + let shared_secret = sha256::Hash::from_engine(eng); - let spaddress2secret = notification_information - .into_iter() - .map(|(address, shared_pubkey)| { - let shared_secret = AnkSharedSecret::new_from_public_key(shared_pubkey); - (address.into(), shared_secret.to_string()) - }) - .collect(); + let mut spaddress2secret: HashMap = HashMap::new(); + + spaddress2secret.insert(sp_address.into(), shared_secret.as_byte_array().to_lower_hex_string()); Ok(createNotificationTransactionReturn { transaction: serialize(&transaction).to_lower_hex_string(), diff --git a/crates/sp_client/src/silentpayments.rs b/crates/sp_client/src/silentpayments.rs index a82f99e..ac2a223 100644 --- a/crates/sp_client/src/silentpayments.rs +++ b/crates/sp_client/src/silentpayments.rs @@ -4,11 +4,11 @@ use anyhow::{Error, Result}; use rand::Rng; use sp_client::bitcoin::policy::DUST_RELAY_TX_FEE; -use sp_client::bitcoin::secp256k1::ecdh::SharedSecret; +use sp_client::bitcoin::secp256k1::ecdh::shared_secret_point; use sp_client::bitcoin::{block, Amount, OutPoint}; use sp_client::silentpayments::sending::SilentPaymentAddress; use sp_client::silentpayments::utils::receiving::calculate_shared_secret; -use sp_client::spclient::{OutputList, OwnedOutput, Recipient, SpClient}; +use sp_client::spclient::{OutputList, OwnedOutput, Recipient, SpClient, SpWallet}; use sp_client::{ bitcoin::{ secp256k1::{PublicKey, Scalar, XOnlyPublicKey}, @@ -21,30 +21,30 @@ use crate::user::{lock_connected_users, CONNECTED_USERS}; type FoundOutputs = HashMap, HashMap>; -type NotificationInformation = (Transaction, Vec<(SilentPaymentAddress, PublicKey)>); +type SharedPoint = [u8;64]; -pub fn create_transaction_for_address( - send_as: String, +pub fn create_transaction_for_address_with_shared_secret( sp_address: SilentPaymentAddress, message: String, -) -> Result { +) -> Result<(Transaction, SharedPoint)> { let connected_users = lock_connected_users()?; - let sender = connected_users - .get(&send_as) + let (_, wallets) = connected_users + .iter() + .last() .ok_or(Error::msg("Unknown sender"))?; let sp_wallet = if sp_address.is_testnet() { - &sender.recover + &wallets.recover } else { - if let Some(main) = &sender.main { + if let Some(main) = &wallets.main { main } else { return Err(Error::msg("Can't spend on mainnet")); } }; - let available_outpoints = sender.recover.get_outputs().to_spendable_list(); + let available_outpoints = wallets.recover.get_outputs().to_spendable_list(); // Here we need to add more heuristics about which outpoint we spend // For now let's keep it simple @@ -80,8 +80,8 @@ pub fn create_transaction_for_address( .create_new_psbt(inputs, vec![recipient], None)?; log::debug!("Created psbt: {}", new_psbt); SpClient::set_fees(&mut new_psbt, Amount::from_sat(1000), sp_address.into())?; - let shared_secrets: Vec<(SilentPaymentAddress, PublicKey)> = - sp_wallet.get_client().fill_sp_outputs(&mut new_psbt)?; + let partial_secret = sp_wallet.get_client().get_partial_secret_from_psbt(&new_psbt)?; + sp_wallet.get_client().fill_sp_outputs(&mut new_psbt, partial_secret)?; log::debug!("Definitive psbt: {}", new_psbt); let mut aux_rand = [0u8; 32]; rand::thread_rng().fill(&mut aux_rand); @@ -91,7 +91,10 @@ pub fn create_transaction_for_address( let final_tx = signed.extract_tx()?; - Ok((final_tx, shared_secrets)) + // This should not be directly used without hashing + let shared_point = shared_secret_point(&sp_address.get_scan_key(), &partial_secret); + + Ok((final_tx, shared_point)) } pub fn check_transaction( From c57330d389883f4254ca521d86487ad48fc711da Mon Sep 17 00:00:00 2001 From: Sosthene00 Date: Fri, 19 Apr 2024 00:20:34 +0200 Subject: [PATCH 15/40] Hide our_address --- src/services.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/services.ts b/src/services.ts index 77a23ca..56d26a2 100644 --- a/src/services.ts +++ b/src/services.ts @@ -63,10 +63,10 @@ class Services { 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}` - } + // const ourAddress = document.getElementById('our_address'); + // if (ourAddress) { + // ourAddress.innerHTML = `Our Address: ${this.sp_address}` + // } // services.attachClickListener("displaysendmessage", services.displaySendMessage); // await services.displayProcess(); } From 5cddf0566a8911a9378e7d86564fba5ee5fb1e83 Mon Sep 17 00:00:00 2001 From: Sosthene00 Date: Fri, 19 Apr 2024 00:23:09 +0200 Subject: [PATCH 16/40] WIP --- crates/sp_client/src/api.rs | 22 ++--- crates/sp_client/src/silentpayments.rs | 125 ++++++++++++++++++++++--- src/services.ts | 21 ++++- 3 files changed, 140 insertions(+), 28 deletions(-) diff --git a/crates/sp_client/src/api.rs b/crates/sp_client/src/api.rs index 6046111..14c43cc 100644 --- a/crates/sp_client/src/api.rs +++ b/crates/sp_client/src/api.rs @@ -12,7 +12,6 @@ use sdk_common::crypto::AnkSharedSecret; use serde_json::Error as SerdeJsonError; use shamir::SecretData; use sp_client::bitcoin::consensus::{deserialize, serialize}; -use sp_client::silentpayments::bitcoin_hashes::{HashEngine, sha256, Hash}; use sp_client::bitcoin::hex::{parse, DisplayHex, FromHex, HexToBytesError}; use sp_client::bitcoin::secp256k1::ecdh::SharedSecret; use sp_client::bitcoin::secp256k1::{PublicKey, SecretKey}; @@ -31,7 +30,9 @@ use sp_client::spclient::{derive_keys_from_seed, OutputList, OwnedOutput, SpClie use sp_client::spclient::{SpWallet, SpendKey}; use crate::images; -use crate::silentpayments::{check_transaction, create_transaction_for_address}; +use crate::silentpayments::{ + check_transaction, create_transaction_for_address_with_shared_secret, ScannedTransaction, +}; use crate::user::{lock_connected_users, User, UserWallets, CONNECTED_USERS}; use crate::process::Process; @@ -392,7 +393,7 @@ pub fn is_tx_owned_by_user(pre_id: String, tx: String) -> ApiResult { #[allow(non_camel_case_types)] pub struct createNotificationTransactionReturn { pub transaction: String, - pub spaddress2secret: HashMap, + pub transaction2secret: ScannedTransaction, } #[wasm_bindgen] @@ -402,22 +403,17 @@ pub fn create_notification_transaction( ) -> ApiResult { let sp_address: SilentPaymentAddress = recipient.try_into()?; - let (transaction, shared_point) = + let (transaction, shared_secret) = create_transaction_for_address_with_shared_secret(sp_address, message)?; - // The shared_point *must* be hashed to produce a proper ecdh secret - // For now we propose to implement a tagged hash for it - // It could be interesting to add some piece of data to allow for the derivation of multiple secrets - let mut eng = sha256::HashEngine::default(); - eng.write_all(&shared_point); - let shared_secret = sha256::Hash::from_engine(eng); - let mut spaddress2secret: HashMap = HashMap::new(); + // update our cache - spaddress2secret.insert(sp_address.into(), shared_secret.as_byte_array().to_lower_hex_string()); + let mut transaction2secret = ScannedTransaction::new(); + transaction2secret.get_mut().insert(transaction.txid(), vec![shared_secret]); Ok(createNotificationTransactionReturn { transaction: serialize(&transaction).to_lower_hex_string(), - spaddress2secret, + transaction2secret, }) } diff --git a/crates/sp_client/src/silentpayments.rs b/crates/sp_client/src/silentpayments.rs index ac2a223..1b5d35e 100644 --- a/crates/sp_client/src/silentpayments.rs +++ b/crates/sp_client/src/silentpayments.rs @@ -1,14 +1,23 @@ -use std::collections::HashMap; +// This file should move to common + +use std::collections::{HashMap, HashSet}; +use std::io::Write; +use std::iter::Once; +use std::sync::{Mutex, MutexGuard, OnceLock}; use anyhow::{Error, Result}; +use log::debug; use rand::Rng; +use sdk_common::crypto::{Aes256Encryption, Aes256Gcm, AeadCore, Purpose}; +use serde::{Deserialize, Serialize}; use sp_client::bitcoin::policy::DUST_RELAY_TX_FEE; use sp_client::bitcoin::secp256k1::ecdh::shared_secret_point; -use sp_client::bitcoin::{block, Amount, OutPoint}; +use sp_client::bitcoin::{block, Amount, OutPoint, Txid}; use sp_client::silentpayments::sending::SilentPaymentAddress; use sp_client::silentpayments::utils::receiving::calculate_shared_secret; use sp_client::spclient::{OutputList, OwnedOutput, Recipient, SpClient, SpWallet}; +use sp_client::silentpayments::bitcoin_hashes::{sha256, Hash, HashEngine}; use sp_client::{ bitcoin::{ secp256k1::{PublicKey, Scalar, XOnlyPublicKey}, @@ -16,17 +25,75 @@ use sp_client::{ }, silentpayments::receiving::Label, }; +use tsify::Tsify; use crate::user::{lock_connected_users, CONNECTED_USERS}; +use crate::MutexExt; + +#[derive(Debug, Serialize, Deserialize, Tsify)] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub struct ScannedTransaction(HashMap>); + +impl ScannedTransaction { + pub fn new() -> Self { + Self(HashMap::new()) + } + + pub fn get_mut(&mut self) -> &mut HashMap> { + &mut self.0 + } +} + +pub static TRANSACTIONCACHE: OnceLock> = OnceLock::new(); + +pub fn lock_scanned_transactions() -> Result> { + TRANSACTIONCACHE + .get_or_init(|| Mutex::new(ScannedTransaction::new())) + .lock_anyhow() +} type FoundOutputs = HashMap, HashMap>; -type SharedPoint = [u8;64]; +#[derive(Debug)] +pub struct SharedPoint([u8; 64]); + +impl SharedPoint { + pub fn as_inner(&self) -> &[u8; 64] { + &self.0 + } +} + +#[derive(Debug, Serialize, Deserialize, Tsify)] +#[tsify(from_wasm_abi, into_wasm_abi)] +pub struct SharedSecret { + secret: [u8; 32], + shared_with: Option, // SilentPaymentAddress +} + +impl SharedSecret { + pub fn new(secret: [u8;32], shared_with: Option) -> Result { + if let Some(ref shared) = shared_with { + if let Ok(_) = SilentPaymentAddress::try_from(shared.as_str()) { + Ok(Self { + secret, + shared_with + }) + } else { + Err(Error::msg("Invalid silent payment address")) + } + } else { + Ok(Self { + secret, + shared_with: None + }) + } + } +} pub fn create_transaction_for_address_with_shared_secret( sp_address: SilentPaymentAddress, message: String, -) -> Result<(Transaction, SharedPoint)> { +) -> Result<(Transaction, SharedSecret)> { let connected_users = lock_connected_users()?; let (_, wallets) = connected_users @@ -34,6 +101,8 @@ pub fn create_transaction_for_address_with_shared_secret( .last() .ok_or(Error::msg("Unknown sender"))?; + debug!("Got user wallets"); + let sp_wallet = if sp_address.is_testnet() { &wallets.recover } else { @@ -77,11 +146,38 @@ pub fn create_transaction_for_address_with_shared_secret( let mut new_psbt = sp_wallet .get_client() - .create_new_psbt(inputs, vec![recipient], None)?; + .create_new_psbt(inputs, vec![recipient], Some(message.as_bytes()))?; log::debug!("Created psbt: {}", new_psbt); SpClient::set_fees(&mut new_psbt, Amount::from_sat(1000), sp_address.into())?; - let partial_secret = sp_wallet.get_client().get_partial_secret_from_psbt(&new_psbt)?; - sp_wallet.get_client().fill_sp_outputs(&mut new_psbt, partial_secret)?; + + let partial_secret = sp_wallet + .get_client() + .get_partial_secret_from_psbt(&new_psbt)?; + + // This wouldn't work with many recipients in the same transaction + // each address (or more precisely each scan public key) would have its own point + let shared_point = shared_secret_point(&sp_address.get_scan_key(), &partial_secret); + + // The shared_point *must* be hashed to produce a proper ecdh secret + let mut eng = sha256::HashEngine::default(); + eng.write_all(&shared_point); + let shared_secret = sha256::Hash::from_engine(eng); + + // encrypt the message with the new shared_secret + let message_encryption = Aes256Encryption::import_key( + Purpose::Arbitrary, + message.into_bytes(), + shared_secret.to_byte_array(), + Aes256Gcm::generate_nonce(&mut rand::thread_rng()).into(), + )?; + + let cipher = message_encryption.encrypt_with_aes_key()?; + + sp_client::spclient::SpClient::replace_op_return_with(&mut new_psbt, &cipher)?; + + sp_wallet + .get_client() + .fill_sp_outputs(&mut new_psbt, partial_secret)?; log::debug!("Definitive psbt: {}", new_psbt); let mut aux_rand = [0u8; 32]; rand::thread_rng().fill(&mut aux_rand); @@ -91,17 +187,24 @@ pub fn create_transaction_for_address_with_shared_secret( let final_tx = signed.extract_tx()?; - // This should not be directly used without hashing - let shared_point = shared_secret_point(&sp_address.get_scan_key(), &partial_secret); - - Ok((final_tx, shared_point)) + Ok((final_tx, SharedSecret { + secret: shared_secret.to_byte_array(), + shared_with: Some(sp_address.into()) + })) } +// This need to go pub fn check_transaction( tx: &Transaction, blockheight: u32, tweak_data: PublicKey, ) -> Result { + // first check that we haven't scanned this transaction yet + if let Some((txid, _)) = lock_scanned_transactions()?.0.get_key_value(&tx.txid()) { + let err_msg = format!("Already scanned tx {}", txid); + return Err(Error::msg(err_msg)); + } + let connected_users = lock_connected_users()?; let txid = tx.txid().to_string(); diff --git a/src/services.ts b/src/services.ts index 56d26a2..5a3344e 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 } from '../dist/pkg/sdk_client'; +import { createUserReturn, User, Process, createNotificationTransactionReturn, parse_network_msg, outputs_list, parseNetworkMsgReturn, FaucetMessage, AnkFlag, NewTxMessage } from '../dist/pkg/sdk_client'; import IndexedDB from './database' import { WebSocketClient } from './websockets'; @@ -86,7 +86,10 @@ class Services { const message = messageElement.value; const services = await Services.getInstance(); - let notificationInfo = services.notify_address_for_message(recipientSpAddress, message); + let notificationInfo = await services.notify_address_for_message(recipientSpAddress, message); + if (notificationInfo) { + notificationInfo.transaction + } console.log(notificationInfo); } @@ -751,6 +754,10 @@ class Services { 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(); @@ -766,9 +773,15 @@ class Services { try { let notificationInfo: createNotificationTransactionReturn = services.sdkClient.create_notification_transaction(user, sp_address, message); + const flag: AnkFlag = "NewTx"; + const newTxMsg: NewTxMessage = { + 'transaction': notificationInfo.transaction, + 'tweak_data': null + } + connection.sendMessage(flag, JSON.stringify(newTxMsg)); return notificationInfo; - } catch { - console.error("Failed to create notification transaction for user", user); + } catch (error) { + console.error("Failed to create notification transaction:", error); return null } } From 26bf0c08652a2f92b91a618370e498fd13838361 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Mon, 22 Apr 2024 11:52:57 +0200 Subject: [PATCH 17/40] ws mono user --- crates/sp_client/src/api.rs | 45 ++++--- crates/sp_client/src/silentpayments.rs | 43 +++---- crates/sp_client/src/user.rs | 165 ++++++++++++++++--------- 3 files changed, 146 insertions(+), 107 deletions(-) diff --git a/crates/sp_client/src/api.rs b/crates/sp_client/src/api.rs index 14c43cc..9d0d59c 100644 --- a/crates/sp_client/src/api.rs +++ b/crates/sp_client/src/api.rs @@ -33,7 +33,7 @@ use crate::images; use crate::silentpayments::{ check_transaction, create_transaction_for_address_with_shared_secret, ScannedTransaction, }; -use crate::user::{lock_connected_users, User, UserWallets, CONNECTED_USERS}; +use crate::user::{lock_connected_user, User, UserWallets, CONNECTED_USER}; use crate::process::Process; @@ -141,9 +141,20 @@ 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(), @@ -168,7 +179,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), ); @@ -176,7 +187,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, @@ -359,24 +372,22 @@ pub fn parse_network_msg(raw: String) -> ApiResult { #[wasm_bindgen] pub fn get_outpoints_for_user() -> ApiResult { - let connected_users = lock_connected_users()?; - let (_, user) = connected_users.iter().last().ok_or(ApiError { - message: "Can't find user".to_owned(), - })?; - Ok(outputs_list(user.get_all_outputs())) + 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 is_tx_owned_by_user(pre_id: String, tx: String) -> ApiResult { let transaction = deserialize::(&Vec::from_hex(&tx)?)?; let txid = transaction.txid(); - let connected_users = lock_connected_users()?; - let user = connected_users.get(&pre_id).ok_or(ApiError { - message: "Can't find user".to_owned(), - })?; + let connected_user = lock_connected_user()?; - if let Some(_) = user - .recover + if let Some(_) = connected_user + .try_get_recover()? .get_outputs() .to_outpoints_list() .iter() diff --git a/crates/sp_client/src/silentpayments.rs b/crates/sp_client/src/silentpayments.rs index 1b5d35e..aee9616 100644 --- a/crates/sp_client/src/silentpayments.rs +++ b/crates/sp_client/src/silentpayments.rs @@ -27,7 +27,7 @@ use sp_client::{ }; use tsify::Tsify; -use crate::user::{lock_connected_users, CONNECTED_USERS}; +use crate::user::{lock_connected_user, CONNECTED_USER}; use crate::MutexExt; #[derive(Debug, Serialize, Deserialize, Tsify)] @@ -94,26 +94,19 @@ pub fn create_transaction_for_address_with_shared_secret( sp_address: SilentPaymentAddress, message: String, ) -> Result<(Transaction, SharedSecret)> { - let connected_users = lock_connected_users()?; - - let (_, wallets) = connected_users - .iter() - .last() - .ok_or(Error::msg("Unknown sender"))?; - - debug!("Got user wallets"); + let connected_user = lock_connected_user()?; let sp_wallet = if sp_address.is_testnet() { - &wallets.recover + connected_user.try_get_recover()? } else { - if let Some(main) = &wallets.main { + if let Ok(main) = connected_user.try_get_main() { main } else { return Err(Error::msg("Can't spend on mainnet")); } }; - let available_outpoints = wallets.recover.get_outputs().to_spendable_list(); + let available_outpoints = sp_wallet.get_outputs().to_spendable_list(); // Here we need to add more heuristics about which outpoint we spend // For now let's keep it simple @@ -205,27 +198,19 @@ pub fn check_transaction( return Err(Error::msg(err_msg)); } - let connected_users = lock_connected_users()?; + let mut connected_user = lock_connected_user()?; let txid = tx.txid().to_string(); - // Check the transaction for all connected users - for (pre_id, keys) in connected_users.clone() { - let mut recover = keys.recover; - if recover.update_wallet_with_transaction(tx, blockheight, tweak_data)? > 0 { - return Ok(txid); - } + if connected_user.try_get_mut_recover()?.update_wallet_with_transaction(tx, blockheight, tweak_data)? > 0 { + return Ok(txid); + } - if let Some(mut main) = keys.main { - if main.update_wallet_with_transaction(tx, blockheight, tweak_data)? > 0 { - return Ok(txid); - } - } + if connected_user.try_get_mut_main()?.update_wallet_with_transaction(tx, blockheight, tweak_data)? > 0 { + return Ok(txid); + } - if let Some(mut revoke) = keys.revoke { - if revoke.update_wallet_with_transaction(tx, blockheight, tweak_data)? > 0 { - return Ok(txid); - } - } + if connected_user.try_get_mut_revoke()?.update_wallet_with_transaction(tx, blockheight, tweak_data)? > 0 { + return Ok(txid); } return Err(Error::msg("No new outputs found")); diff --git a/crates/sp_client/src/user.rs b/crates/sp_client/src/user.rs index 30a0119..37de212 100644 --- a/crates/sp_client/src/user.rs +++ b/crates/sp_client/src/user.rs @@ -36,24 +36,23 @@ 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, @@ -61,8 +60,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 { @@ -73,7 +120,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 } @@ -93,22 +142,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(); @@ -186,7 +237,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() @@ -200,6 +251,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], @@ -207,10 +260,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, @@ -218,6 +280,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]; @@ -241,21 +308,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, @@ -290,26 +342,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(()) @@ -456,21 +494,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] @@ -493,9 +536,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!( From fcdfeeaecca6c71bf1ff6afa58b702148a64f6e2 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Mon, 22 Apr 2024 17:17:12 +0200 Subject: [PATCH 18/40] WIP --- crates/sp_client/Cargo.toml | 4 +- crates/sp_client/src/api.rs | 192 +++++++++++++++++++--- crates/sp_client/src/lib.rs | 19 ++- crates/sp_client/src/silentpayments.rs | 217 ------------------------- crates/sp_client/src/user.rs | 6 +- src/index.ts | 2 +- src/services.ts | 74 ++++++++- src/websockets.ts | 1 - 8 files changed, 263 insertions(+), 252 deletions(-) delete mode 100644 crates/sp_client/src/silentpayments.rs diff --git a/crates/sp_client/Cargo.toml b/crates/sp_client/Cargo.toml index 2155567..f64543a 100644 --- a/crates/sp_client/Cargo.toml +++ b/crates/sp_client/Cargo.toml @@ -8,6 +8,7 @@ name = "sdk_client" crate-type = ["cdylib"] [dependencies] +# 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"] } @@ -18,7 +19,8 @@ wasm-logger = "0.2.0" rand = "0.8.5" log = "0.4.6" tsify = { git = "https://github.com/Sosthene00/tsify", branch = "next" } -sdk_common = { git = "https://git.4nkweb.com/4nk/sdk_common.git", branch = "demo" } +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/api.rs b/crates/sp_client/src/api.rs index 9d0d59c..73bedb8 100644 --- a/crates/sp_client/src/api.rs +++ b/crates/sp_client/src/api.rs @@ -2,20 +2,22 @@ use std::any::Any; use std::collections::HashMap; use std::io::Write; use std::str::FromStr; +use std::string::FromUtf8Error; use std::sync::{Mutex, OnceLock, PoisonError}; use log::debug; -use rand::Rng; +use rand::{Fill, Rng}; use anyhow::Error as AnyhowError; -use sdk_common::crypto::AnkSharedSecret; +use sdk_common::crypto::{ + AeadCore, Aes256Decryption, Aes256Encryption, Aes256Gcm, AnkSharedSecret, KeyInit, Purpose, +}; use serde_json::Error as SerdeJsonError; use shamir::SecretData; use sp_client::bitcoin::consensus::{deserialize, serialize}; use sp_client::bitcoin::hex::{parse, DisplayHex, FromHex, HexToBytesError}; -use sp_client::bitcoin::secp256k1::ecdh::SharedSecret; use sp_client::bitcoin::secp256k1::{PublicKey, SecretKey}; -use sp_client::bitcoin::{OutPoint, Transaction, Txid}; +use sp_client::bitcoin::{Amount, OutPoint, Transaction, Txid}; use sp_client::silentpayments::Error as SpError; use serde::{Deserialize, Serialize}; @@ -25,15 +27,15 @@ use wasm_bindgen::convert::FromWasmAbi; use wasm_bindgen::prelude::*; use sdk_common::network::{AnkFlag, AnkNetworkMsg, NewTxMessage}; +use sdk_common::silentpayments::{ + check_transaction, create_transaction_for_address_with_shared_secret, +}; use sp_client::spclient::{derive_keys_from_seed, OutputList, OwnedOutput, SpClient}; use sp_client::spclient::{SpWallet, SpendKey}; -use crate::images; -use crate::silentpayments::{ - check_transaction, create_transaction_for_address_with_shared_secret, ScannedTransaction, -}; use crate::user::{lock_connected_user, User, UserWallets, CONNECTED_USER}; +use crate::{images, lock_scanned_transactions, Txid2Secrets}; use crate::process::Process; @@ -94,6 +96,14 @@ impl From for ApiError { } } +impl From for ApiError { + fn from(value: FromUtf8Error) -> Self { + ApiError { + message: value.to_string(), + } + } +} + impl Into for ApiError { fn into(self) -> JsValue { JsValue::from_str(&self.message) @@ -143,7 +153,10 @@ pub fn generate_sp_wallet( #[wasm_bindgen] 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()) + Ok(my_wallets + .try_get_recover()? + .get_client() + .get_receiving_address()) } else { Err(ApiError { message: "Unknown user pre_id".to_owned(), @@ -154,7 +167,10 @@ pub fn get_recover_address() -> ApiResult { #[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()) + Ok(my_wallets + .try_get_main()? + .get_client() + .get_receiving_address()) } else { Err(ApiError { message: "Unknown user pre_id".to_owned(), @@ -322,9 +338,28 @@ pub fn check_transaction_for_silent_payments( let tx = deserialize::(&Vec::from_hex(&tx_hex)?)?; let tweak_data = PublicKey::from_str(&tweak_data_hex)?; - let txid = check_transaction(&tx, blockheight, tweak_data)?; + 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) { + return Ok(txid); + } + } - Ok(txid) + if let Ok(main) = connected_user.try_get_mut_main() { + if let Ok(txid) = check_transaction(&tx, main, blockheight, tweak_data) { + return Ok(txid); + } + } + + if let Ok(revoke) = connected_user.try_get_mut_revoke() { + if let Ok(txid) = check_transaction(&tx, revoke, blockheight, tweak_data) { + return Ok(txid); + } + } + + Err(ApiError { + message: "No output found".to_owned(), + }) } #[derive(Tsify, Serialize, Deserialize)] @@ -341,14 +376,16 @@ pub fn parse_network_msg(raw: String) -> ApiResult { match ank_msg.flag { AnkFlag::NewTx => { let tx_message = serde_json::from_str::(&ank_msg.content)?; - let tx = deserialize::(&Vec::from_hex(&tx_message.transaction)?)?; if tx_message.tweak_data.is_none() { return Err(ApiError { message: "Missing tweak_data".to_owned(), }); } - let partial_tweak = PublicKey::from_str(&tx_message.tweak_data.unwrap())?; - let txid = check_transaction(&tx, 0, partial_tweak)?; + let txid = check_transaction_for_silent_payments( + tx_message.transaction, + 0, + tx_message.tweak_data.unwrap(), + )?; return Ok(parseNetworkMsgReturn { topic: AnkFlag::NewTx.as_str().to_owned(), message: txid, @@ -376,7 +413,27 @@ pub fn get_outpoints_for_user() -> ApiResult { if connected_user.is_not_empty() { Ok(outputs_list(connected_user.get_all_outputs())) } else { - Err(ApiError { message: "No user logged in".to_owned() }) + 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(), + }) } } @@ -403,28 +460,117 @@ pub fn is_tx_owned_by_user(pre_id: String, tx: String) -> ApiResult { #[tsify(into_wasm_abi, from_wasm_abi)] #[allow(non_camel_case_types)] pub struct createNotificationTransactionReturn { + pub txid: String, pub transaction: String, - pub transaction2secret: ScannedTransaction, + pub address2secret: HashMap, } #[wasm_bindgen] pub fn create_notification_transaction( recipient: String, message: String, + fee_rate: u32, ) -> ApiResult { let sp_address: SilentPaymentAddress = recipient.try_into()?; - let (transaction, shared_secret) = - create_transaction_for_address_with_shared_secret(sp_address, message)?; + 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 (transaction, shared_secret) = create_transaction_for_address_with_shared_secret( + sp_address, + sp_wallet, + message, + Amount::from_sat(fee_rate.into()), + )?; + + let mut address2secret: Vec<(String, AnkSharedSecret)> = vec![]; + address2secret.push((sp_address.into(), shared_secret)); // update our cache - - let mut transaction2secret = ScannedTransaction::new(); - transaction2secret.get_mut().insert(transaction.txid(), vec![shared_secret]); + lock_scanned_transactions()? + .insert(transaction.txid(), address2secret.clone()); Ok(createNotificationTransactionReturn { + txid: transaction.txid().to_string(), transaction: serialize(&transaction).to_lower_hex_string(), - transaction2secret, + address2secret: address2secret.into_iter().collect() }) } + +#[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(String::from_utf8(cipher)?) +} + +#[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) +} diff --git a/crates/sp_client/src/lib.rs b/crates/sp_client/src/lib.rs index c0056a6..8d7155c 100644 --- a/crates/sp_client/src/lib.rs +++ b/crates/sp_client/src/lib.rs @@ -1,16 +1,31 @@ #![allow(warnings)] use anyhow::Error; +use sdk_common::crypto::AnkSharedSecret; +use serde::{Deserialize, Serialize}; +use sp_client::bitcoin::Txid; +use sp_client::silentpayments::sending::SilentPaymentAddress; +use std::collections::HashMap; 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 images; mod peers; mod process; -mod silentpayments; mod user; +pub type Txid2Secrets = HashMap>; + +pub static TRANSACTIONCACHE: OnceLock> = OnceLock::new(); + +pub fn lock_scanned_transactions() -> Result, Error> { + TRANSACTIONCACHE + .get_or_init(|| Mutex::new(Txid2Secrets::new())) + .lock_anyhow() +} + pub(crate) trait MutexExt { fn lock_anyhow(&self) -> Result, Error>; } diff --git a/crates/sp_client/src/silentpayments.rs b/crates/sp_client/src/silentpayments.rs deleted file mode 100644 index aee9616..0000000 --- a/crates/sp_client/src/silentpayments.rs +++ /dev/null @@ -1,217 +0,0 @@ -// This file should move to common - -use std::collections::{HashMap, HashSet}; -use std::io::Write; -use std::iter::Once; -use std::sync::{Mutex, MutexGuard, OnceLock}; - -use anyhow::{Error, Result}; - -use log::debug; -use rand::Rng; -use sdk_common::crypto::{Aes256Encryption, Aes256Gcm, AeadCore, Purpose}; -use serde::{Deserialize, Serialize}; -use sp_client::bitcoin::policy::DUST_RELAY_TX_FEE; -use sp_client::bitcoin::secp256k1::ecdh::shared_secret_point; -use sp_client::bitcoin::{block, Amount, OutPoint, Txid}; -use sp_client::silentpayments::sending::SilentPaymentAddress; -use sp_client::silentpayments::utils::receiving::calculate_shared_secret; -use sp_client::spclient::{OutputList, OwnedOutput, Recipient, SpClient, SpWallet}; -use sp_client::silentpayments::bitcoin_hashes::{sha256, Hash, HashEngine}; -use sp_client::{ - bitcoin::{ - secp256k1::{PublicKey, Scalar, XOnlyPublicKey}, - Transaction, - }, - silentpayments::receiving::Label, -}; -use tsify::Tsify; - -use crate::user::{lock_connected_user, CONNECTED_USER}; -use crate::MutexExt; - -#[derive(Debug, Serialize, Deserialize, Tsify)] -#[tsify(into_wasm_abi, from_wasm_abi)] -pub struct ScannedTransaction(HashMap>); - -impl ScannedTransaction { - pub fn new() -> Self { - Self(HashMap::new()) - } - - pub fn get_mut(&mut self) -> &mut HashMap> { - &mut self.0 - } -} - -pub static TRANSACTIONCACHE: OnceLock> = OnceLock::new(); - -pub fn lock_scanned_transactions() -> Result> { - TRANSACTIONCACHE - .get_or_init(|| Mutex::new(ScannedTransaction::new())) - .lock_anyhow() -} - -type FoundOutputs = HashMap, HashMap>; - -#[derive(Debug)] -pub struct SharedPoint([u8; 64]); - -impl SharedPoint { - pub fn as_inner(&self) -> &[u8; 64] { - &self.0 - } -} - -#[derive(Debug, Serialize, Deserialize, Tsify)] -#[tsify(from_wasm_abi, into_wasm_abi)] -pub struct SharedSecret { - secret: [u8; 32], - shared_with: Option, // SilentPaymentAddress -} - -impl SharedSecret { - pub fn new(secret: [u8;32], shared_with: Option) -> Result { - if let Some(ref shared) = shared_with { - if let Ok(_) = SilentPaymentAddress::try_from(shared.as_str()) { - Ok(Self { - secret, - shared_with - }) - } else { - Err(Error::msg("Invalid silent payment address")) - } - } else { - Ok(Self { - secret, - shared_with: None - }) - } - } -} - -pub fn create_transaction_for_address_with_shared_secret( - sp_address: SilentPaymentAddress, - message: String, -) -> Result<(Transaction, SharedSecret)> { - let connected_user = lock_connected_user()?; - - let sp_wallet = if sp_address.is_testnet() { - connected_user.try_get_recover()? - } else { - if let Ok(main) = connected_user.try_get_main() { - main - } else { - return Err(Error::msg("Can't spend on mainnet")); - } - }; - - let available_outpoints = sp_wallet.get_outputs().to_spendable_list(); - - // Here we need to add more heuristics about which outpoint we spend - // For now let's keep it simple - - let mut inputs: HashMap = HashMap::new(); - - let total_available = - available_outpoints - .into_iter() - .try_fold(Amount::from_sat(0), |acc, (outpoint, output)| { - let new_total = acc + output.amount; - inputs.insert(outpoint, output); - if new_total > Amount::from_sat(1000) { - Err(new_total) - } else { - Ok(new_total) - } - }); - - match total_available { - Err(total) => log::debug!("Spending {} outputs totaling {} sats", inputs.len(), total), - Ok(_) => return Err(Error::msg("Not enought fund available")), - } - - let recipient = Recipient { - address: sp_address.into(), - amount: Amount::from_sat(1000), - nb_outputs: 1, - }; - - let mut new_psbt = sp_wallet - .get_client() - .create_new_psbt(inputs, vec![recipient], Some(message.as_bytes()))?; - log::debug!("Created psbt: {}", new_psbt); - SpClient::set_fees(&mut new_psbt, Amount::from_sat(1000), sp_address.into())?; - - let partial_secret = sp_wallet - .get_client() - .get_partial_secret_from_psbt(&new_psbt)?; - - // This wouldn't work with many recipients in the same transaction - // each address (or more precisely each scan public key) would have its own point - let shared_point = shared_secret_point(&sp_address.get_scan_key(), &partial_secret); - - // The shared_point *must* be hashed to produce a proper ecdh secret - let mut eng = sha256::HashEngine::default(); - eng.write_all(&shared_point); - let shared_secret = sha256::Hash::from_engine(eng); - - // encrypt the message with the new shared_secret - let message_encryption = Aes256Encryption::import_key( - Purpose::Arbitrary, - message.into_bytes(), - shared_secret.to_byte_array(), - Aes256Gcm::generate_nonce(&mut rand::thread_rng()).into(), - )?; - - let cipher = message_encryption.encrypt_with_aes_key()?; - - sp_client::spclient::SpClient::replace_op_return_with(&mut new_psbt, &cipher)?; - - sp_wallet - .get_client() - .fill_sp_outputs(&mut new_psbt, partial_secret)?; - log::debug!("Definitive psbt: {}", new_psbt); - let mut aux_rand = [0u8; 32]; - rand::thread_rng().fill(&mut aux_rand); - let mut signed = sp_wallet.get_client().sign_psbt(new_psbt, &aux_rand)?; - log::debug!("signed psbt: {}", signed); - SpClient::finalize_psbt(&mut signed)?; - - let final_tx = signed.extract_tx()?; - - Ok((final_tx, SharedSecret { - secret: shared_secret.to_byte_array(), - shared_with: Some(sp_address.into()) - })) -} - -// This need to go -pub fn check_transaction( - tx: &Transaction, - blockheight: u32, - tweak_data: PublicKey, -) -> Result { - // first check that we haven't scanned this transaction yet - if let Some((txid, _)) = lock_scanned_transactions()?.0.get_key_value(&tx.txid()) { - let err_msg = format!("Already scanned tx {}", txid); - return Err(Error::msg(err_msg)); - } - - let mut connected_user = lock_connected_user()?; - - let txid = tx.txid().to_string(); - if connected_user.try_get_mut_recover()?.update_wallet_with_transaction(tx, blockheight, tweak_data)? > 0 { - return Ok(txid); - } - - if connected_user.try_get_mut_main()?.update_wallet_with_transaction(tx, blockheight, tweak_data)? > 0 { - return Ok(txid); - } - - if connected_user.try_get_mut_revoke()?.update_wallet_with_transaction(tx, blockheight, tweak_data)? > 0 { - return Ok(txid); - } - - return Err(Error::msg("No new outputs found")); -} diff --git a/crates/sp_client/src/user.rs b/crates/sp_client/src/user.rs index 37de212..758c4f2 100644 --- a/crates/sp_client/src/user.rs +++ b/crates/sp_client/src/user.rs @@ -52,7 +52,11 @@ pub struct UserWallets { } impl UserWallets { - pub fn new(main: Option, recover: Option, revoke: Option) -> Self { + pub fn new( + main: Option, + recover: Option, + revoke: Option, + ) -> Self { Self { main, recover, 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 5a3344e..802c089 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 } from '../dist/pkg/sdk_client'; +import { createUserReturn, User, Process, createNotificationTransactionReturn, parse_network_msg, outputs_list, parseNetworkMsgReturn, FaucetMessage, AnkFlag, NewTxMessage, encryptWithNewKeyResult, AnkSharedSecret } from '../dist/pkg/sdk_client'; import IndexedDB from './database' import { WebSocketClient } from './websockets'; @@ -74,6 +74,26 @@ class Services { 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(this.sp_address!); + } 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; @@ -84,11 +104,18 @@ class Services { const recipientSpAddress = spAddressElement.value; const message = messageElement.value; - const services = await Services.getInstance(); let notificationInfo = await services.notify_address_for_message(recipientSpAddress, message); if (notificationInfo) { - notificationInfo.transaction + console.info('Successfully sent notification transaction'); + // Save the secret to db + // encrypt the message(s) + services.encryptData(message, notificationInfo.address2secret); + // encrypt the key + + // add peers list + // add processes list + // send message (transaction in envelope) } console.log(notificationInfo); } @@ -151,7 +178,7 @@ class Services { } try { - this.sp_address = services.sdkClient.get_receiving_address(user.pre_id); + this.sp_address = services.sdkClient.get_recover_address(); if (this.sp_address) { console.info('Using sp_address:', this.sp_address); await services.obtainTokenWithFaucet(this.sp_address); @@ -196,12 +223,14 @@ class Services { 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_receiving_address(user?.pre_id); + this.sp_address = services.sdkClient.get_recover_address(); } } catch (error) { console.error(error); } + console.info(this.sp_address); + // TODO: check blocks since last_scan and update outputs await services.displaySendMessage(); @@ -341,6 +370,7 @@ class Services { let user = await services.getUserInfo(); if (user) { user.outputs = latest_outputs; + // console.warn(user); await services.updateUser(user); } } catch (error) { @@ -772,7 +802,8 @@ class Services { } try { - let notificationInfo: createNotificationTransactionReturn = services.sdkClient.create_notification_transaction(user, sp_address, message); + const feeRate = 1000; + let notificationInfo: createNotificationTransactionReturn = services.sdkClient.create_notification_transaction(sp_address, message, feeRate); const flag: AnkFlag = "NewTx"; const newTxMsg: NewTxMessage = { 'transaction': notificationInfo.transaction, @@ -785,6 +816,37 @@ class Services { return null } } + + public async encryptData(data: string, sharedSecret: Record): Promise { + const services = await Services.getInstance(); + let msg_cipher: encryptWithNewKeyResult; + try { + msg_cipher = services.sdkClient.encrypt_with_new_key(data); + } catch (error) { + throw error; + } + + let res = new Map(); + for (const [recipient, secret] of Object.entries(sharedSecret)) { + try { + const key = secret.secret; + const encryptedKey = await services.sdkClient.encrypt_with_key(msg_cipher.key, key); + res.set(recipient, encryptedKey); + } catch (error) { + throw new Error(`Failed to encrypt key for recipient ${recipient}: ${error}`); + } + } +} + + public async decryptData(cipher: string, key: string): Promise { + const services = await Services.getInstance(); + try { + let res = services.sdkClient.try_decrypt_with_key(cipher, key); + return res; + } catch (error) { + throw error; + } + } } export default Services; diff --git a/src/websockets.ts b/src/websockets.ts index c94b4af..c4fc134 100644 --- a/src/websockets.ts +++ b/src/websockets.ts @@ -22,7 +22,6 @@ class WebSocketClient { // Listen for messages this.ws.addEventListener('message', (event) => { const msgData = event.data; - console.log(msgData); (async () => { if (typeof(msgData) === 'string') { From e26ea1068d405ebb47d3744efca811f4abe5d704 Mon Sep 17 00:00:00 2001 From: Alex Silva Date: Fri, 3 May 2024 13:02:11 +0200 Subject: [PATCH 19/40] Add create commit --- crates/sp_client/src/api.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/sp_client/src/api.rs b/crates/sp_client/src/api.rs index 73bedb8..1b1b0a9 100644 --- a/crates/sp_client/src/api.rs +++ b/crates/sp_client/src/api.rs @@ -574,3 +574,11 @@ pub fn try_decrypt_with_key( let plain = String::from_utf8(aes_dec.decrypt_with_key()?)?; Ok(plain) } + +#[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); + String::from_utf8_lossy(hash.to_bytes()) +} From 46fded3791600bf014bb33144c2085a0d8654c0b Mon Sep 17 00:00:00 2001 From: Sosthene Date: Fri, 3 May 2024 13:52:39 +0200 Subject: [PATCH 20/40] bug fix --- crates/sp_client/src/user.rs | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/crates/sp_client/src/user.rs b/crates/sp_client/src/user.rs index 758c4f2..5038554 100644 --- a/crates/sp_client/src/user.rs +++ b/crates/sp_client/src/user.rs @@ -363,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() } @@ -379,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() } @@ -403,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() } From f1c2f0e4ed68d244e4751c30c2009888e422ae69 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Fri, 3 May 2024 13:53:14 +0200 Subject: [PATCH 21/40] Decrypt unknown message --- crates/sp_client/src/api.rs | 68 ++++++++++++++++++++++++++----------- 1 file changed, 48 insertions(+), 20 deletions(-) diff --git a/crates/sp_client/src/api.rs b/crates/sp_client/src/api.rs index 1b1b0a9..e118698 100644 --- a/crates/sp_client/src/api.rs +++ b/crates/sp_client/src/api.rs @@ -398,6 +398,40 @@ pub fn parse_network_msg(raw: String) -> ApiResult { message: ank_msg.content.to_owned(), }) } + AnkFlag::Unknown => { + let transaction_cache = lock_scanned_transactions()?; + // try to decrypt the cipher with all available keys + let mut plaintext: String = "".to_owned(); + for (txid, secret_vec) in transaction_cache.iter() { + for (shared_with, ank_secret) in secret_vec.iter() { + let shared_secret = ank_secret.to_byte_array(); + if let Ok(msg_decrypt) = Aes256Decryption::new( + Purpose::Arbitrary, + ank_msg.content.as_bytes().to_vec(), + shared_secret, + ) { + if let Ok(plain) = msg_decrypt.decrypt_with_key() { + plaintext = String::from_utf8(plain)?; + break; + } + } + continue; + } + } + if plaintext.is_empty() { + // keep the message in cache, just in case + // return an error + return Err(ApiError { + message: "No key found".to_owned(), + }); + } else { + // return the plain text + return Ok(parseNetworkMsgReturn { + topic: AnkFlag::Unknown.as_str().to_owned(), + message: plaintext, + }); + } + } _ => unimplemented!(), } } else { @@ -493,13 +527,12 @@ pub fn create_notification_transaction( address2secret.push((sp_address.into(), shared_secret)); // update our cache - lock_scanned_transactions()? - .insert(transaction.txid(), address2secret.clone()); + lock_scanned_transactions()?.insert(transaction.txid(), address2secret.clone()); Ok(createNotificationTransactionReturn { txid: transaction.txid().to_string(), transaction: serialize(&transaction).to_lower_hex_string(), - address2secret: address2secret.into_iter().collect() + address2secret: address2secret.into_iter().collect(), }) } @@ -515,7 +548,7 @@ pub struct encryptWithNewKeyResult { 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]; + let mut aes_key = [0u8; 32]; aes_key.copy_from_slice(&Vec::from_hex(&key)?); // encrypt @@ -555,30 +588,25 @@ pub fn encrypt_with_new_key(plaintext: String) -> ApiResult ApiResult { +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() }); + return Err(ApiError { + message: "key of invalid lenght".to_owned(), + }); } - let mut aes_key = [0u8;32]; + 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 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_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); - String::from_utf8_lossy(hash.to_bytes()) +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() } From 870fdc831f59abab0e7649e0795b0a0c854827a4 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Sat, 4 May 2024 01:18:05 +0200 Subject: [PATCH 22/40] Send and receive messages --- crates/sp_client/src/api.rs | 40 +++++++++++++++++----- src/services.ts | 68 ++++++++++++++++++++++++++----------- src/websockets.ts | 2 ++ 3 files changed, 83 insertions(+), 27 deletions(-) diff --git a/crates/sp_client/src/api.rs b/crates/sp_client/src/api.rs index e118698..eb6b1aa 100644 --- a/crates/sp_client/src/api.rs +++ b/crates/sp_client/src/api.rs @@ -16,6 +16,7 @@ use serde_json::Error as SerdeJsonError; use shamir::SecretData; use sp_client::bitcoin::consensus::{deserialize, serialize}; use sp_client::bitcoin::hex::{parse, DisplayHex, FromHex, HexToBytesError}; +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::silentpayments::Error as SpError; @@ -336,27 +337,40 @@ pub fn check_transaction_for_silent_payments( tweak_data_hex: String, ) -> ApiResult { let tx = deserialize::(&Vec::from_hex(&tx_hex)?)?; + // check that we don't already have scanned the tx + if lock_scanned_transactions()?.contains_key(&tx.txid()) { + return Err(ApiError { message: "Transaction already scanned".to_owned()}); + } + 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) { + let shared_point = shared_secret_point(&tweak_data, &recover.get_client().get_scan_key()); + lock_scanned_transactions()?.insert(tx.txid(), vec![("".to_owned(), AnkSharedSecret::new(shared_point, false))]); return Ok(txid); } } if let Ok(main) = connected_user.try_get_mut_main() { if let Ok(txid) = check_transaction(&tx, main, blockheight, tweak_data) { + let shared_point = shared_secret_point(&tweak_data, &main.get_client().get_scan_key()); + lock_scanned_transactions()?.insert(tx.txid(), vec![("".to_owned(), AnkSharedSecret::new(shared_point, false))]); return Ok(txid); } } if let Ok(revoke) = connected_user.try_get_mut_revoke() { if let Ok(txid) = check_transaction(&tx, revoke, blockheight, tweak_data) { + let shared_point = shared_secret_point(&tweak_data, &revoke.get_client().get_scan_key()); + lock_scanned_transactions()?.insert(tx.txid(), vec![("".to_owned(), AnkSharedSecret::new(shared_point, false))]); return Ok(txid); } } + // we still want to insert an empty entry in our cache to make sure we don't scan the transaction again + lock_scanned_transactions()?.insert(tx.txid(), vec![("".to_owned(), AnkSharedSecret::default())]); Err(ApiError { message: "No output found".to_owned(), }) @@ -399,23 +413,30 @@ pub fn parse_network_msg(raw: String) -> ApiResult { }) } AnkFlag::Unknown => { - let transaction_cache = lock_scanned_transactions()?; // try to decrypt the cipher with all available keys let mut plaintext: String = "".to_owned(); - for (txid, secret_vec) in transaction_cache.iter() { + for (txid, secret_vec) in lock_scanned_transactions()?.iter() { for (shared_with, ank_secret) in secret_vec.iter() { + if *ank_secret == AnkSharedSecret::default() { + continue; + } let shared_secret = ank_secret.to_byte_array(); if let Ok(msg_decrypt) = Aes256Decryption::new( Purpose::Arbitrary, - ank_msg.content.as_bytes().to_vec(), + Vec::from_hex(&ank_msg.content.trim_matches('\"'))?, shared_secret, ) { - if let Ok(plain) = msg_decrypt.decrypt_with_key() { - plaintext = String::from_utf8(plain)?; - break; + match msg_decrypt.decrypt_with_key() { + Ok(plain) => { + plaintext = String::from_utf8(plain)?; + break; + }, + Err(e) => { + debug!("{}", e); + debug!("Failed to decrypt message {} with key {}", ank_msg.content, shared_secret.to_lower_hex_string()); + } } } - continue; } } if plaintext.is_empty() { @@ -523,6 +544,8 @@ pub fn create_notification_transaction( Amount::from_sat(fee_rate.into()), )?; + 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)); @@ -560,7 +583,8 @@ pub fn encrypt_with_key(plaintext: String, key: String) -> ApiResult { )?; let cipher = aes_enc.encrypt_with_aes_key()?; - Ok(String::from_utf8(cipher)?) + + Ok(cipher.to_lower_hex_string()) } #[wasm_bindgen] diff --git a/src/services.ts b/src/services.ts index 802c089..f975b67 100644 --- a/src/services.ts +++ b/src/services.ts @@ -107,17 +107,31 @@ class Services { let notificationInfo = await services.notify_address_for_message(recipientSpAddress, message); if (notificationInfo) { + let ciphers: string[] = []; console.info('Successfully sent notification transaction'); // Save the secret to db // encrypt the message(s) - services.encryptData(message, notificationInfo.address2secret); - // encrypt the key - + for (const [address, ankSharedSecret] of Object.entries(notificationInfo.address2secret)) { + try { + let cipher = await services.encryptData(message, ankSharedSecret.secret); + 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); } - console.log(notificationInfo); } public async createId(event: Event): Promise { @@ -224,6 +238,10 @@ class Services { 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(this.sp_address); + } } } catch (error) { console.error(error); @@ -802,7 +820,7 @@ class Services { } try { - const feeRate = 1000; + const feeRate = 1; let notificationInfo: createNotificationTransactionReturn = services.sdkClient.create_notification_transaction(sp_address, message, feeRate); const flag: AnkFlag = "NewTx"; const newTxMsg: NewTxMessage = { @@ -817,26 +835,38 @@ class Services { } } - public async encryptData(data: string, sharedSecret: Record): Promise { + // public async encryptData(data: string, sharedSecret: Record): Promise> { + // const services = await Services.getInstance(); + // let msg_cipher: encryptWithNewKeyResult; + // try { + // msg_cipher = services.sdkClient.encrypt_with_new_key(data); + // } catch (error) { + // throw error; + // } + + // let res = new Map(); + // for (const [recipient, secret] of Object.entries(sharedSecret)) { + // try { + // const key = secret.secret; + // const encryptedKey: string = await services.sdkClient.encrypt_with_key(msg_cipher.key, key); + // res.set(recipient, encryptedKey); + // } catch (error) { + // throw new Error(`Failed to encrypt key for recipient ${recipient}: ${error}`); + // } + // } + + // return res; + // } + public async encryptData(data: string, key: string): Promise { const services = await Services.getInstance(); - let msg_cipher: encryptWithNewKeyResult; + try { - msg_cipher = services.sdkClient.encrypt_with_new_key(data); + let res: string = services.sdkClient.encrypt_with_key(data, key); + return res; } catch (error) { throw error; } - - let res = new Map(); - for (const [recipient, secret] of Object.entries(sharedSecret)) { - try { - const key = secret.secret; - const encryptedKey = await services.sdkClient.encrypt_with_key(msg_cipher.key, key); - res.set(recipient, encryptedKey); - } catch (error) { - throw new Error(`Failed to encrypt key for recipient ${recipient}: ${error}`); - } } -} public async decryptData(cipher: string, key: string): Promise { const services = await Services.getInstance(); diff --git a/src/websockets.ts b/src/websockets.ts index c4fc134..79b9872 100644 --- a/src/websockets.ts +++ b/src/websockets.ts @@ -32,6 +32,8 @@ class WebSocketClient { // we received a tx window.alert(`New tx\n${res.message}`); await services.updateOwnedOutputsForUser(); + } else if (res.topic === 'unknown') { + window.alert(`new message: ${res.message}`); } } } else { From bcabeb9a0f0f163d577ba087d0c62ab30ba90fe2 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Tue, 7 May 2024 21:21:57 +0200 Subject: [PATCH 23/40] Add the sender address in message --- src/services.ts | 9 ++++++--- src/websockets.ts | 11 ++++++++++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/services.ts b/src/services.ts index f975b67..c328cd3 100644 --- a/src/services.ts +++ b/src/services.ts @@ -105,7 +105,9 @@ class Services { const recipientSpAddress = spAddressElement.value; const message = messageElement.value; - let notificationInfo = await services.notify_address_for_message(recipientSpAddress, message); + const msg_payload = JSON.stringify({sender: this.sp_address, payload: message}); + + let notificationInfo = await services.notify_address_for_message(recipientSpAddress, msg_payload); if (notificationInfo) { let ciphers: string[] = []; console.info('Successfully sent notification transaction'); @@ -113,7 +115,7 @@ class Services { // encrypt the message(s) for (const [address, ankSharedSecret] of Object.entries(notificationInfo.address2secret)) { try { - let cipher = await services.encryptData(message, ankSharedSecret.secret); + let cipher = await services.encryptData(msg_payload, ankSharedSecret.secret); ciphers.push(cipher); } catch (error) { throw error; @@ -821,7 +823,8 @@ class Services { try { const feeRate = 1; - let notificationInfo: createNotificationTransactionReturn = services.sdkClient.create_notification_transaction(sp_address, message, feeRate); + const commitment = services.sdkClient.create_commitment(message); + let notificationInfo: createNotificationTransactionReturn = services.sdkClient.create_notification_transaction(sp_address, commitment, feeRate); const flag: AnkFlag = "NewTx"; const newTxMsg: NewTxMessage = { 'transaction': notificationInfo.transaction, diff --git a/src/websockets.ts b/src/websockets.ts index 79b9872..3342c31 100644 --- a/src/websockets.ts +++ b/src/websockets.ts @@ -33,7 +33,16 @@ class WebSocketClient { window.alert(`New tx\n${res.message}`); await services.updateOwnedOutputsForUser(); } else if (res.topic === 'unknown') { - window.alert(`new message: ${res.message}`); + // Do we have a json with a sender? + try { + let parsed = JSON.parse(res.message); + if (parsed.sender !== undefined) { + console.info(`Message sent by ${parsed.sender}`); + } + window.alert(`new message: ${parsed.payload}`); + } catch (_) { + window.alert(`new message: ${res.message}`); + } } } } else { From fcafcff69e616301231e3520af8b2b7ad097ba90 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Mon, 13 May 2024 16:18:17 +0200 Subject: [PATCH 24/40] Return transaction to confirm identity --- crates/sp_client/src/api.rs | 181 ++++++++++++++++++++++++++---------- crates/sp_client/src/lib.rs | 34 ++++++- src/services.ts | 43 ++++++++- src/websockets.ts | 27 +++--- 4 files changed, 212 insertions(+), 73 deletions(-) diff --git a/crates/sp_client/src/api.rs b/crates/sp_client/src/api.rs index eb6b1aa..0eb433c 100644 --- a/crates/sp_client/src/api.rs +++ b/crates/sp_client/src/api.rs @@ -5,15 +5,16 @@ use std::str::FromStr; use std::string::FromUtf8Error; use std::sync::{Mutex, OnceLock, PoisonError}; -use log::debug; +use log::{debug, warn}; use rand::{Fill, Rng}; use anyhow::Error as AnyhowError; use sdk_common::crypto::{ AeadCore, Aes256Decryption, Aes256Encryption, Aes256Gcm, AnkSharedSecret, KeyInit, Purpose, }; -use serde_json::Error as SerdeJsonError; +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::secp256k1::ecdh::shared_secret_point; @@ -27,16 +28,16 @@ use tsify::Tsify; use wasm_bindgen::convert::FromWasmAbi; use wasm_bindgen::prelude::*; -use sdk_common::network::{AnkFlag, AnkNetworkMsg, NewTxMessage}; +use sdk_common::network::{AnkFlag, AnkNetworkMsg, NewTxMessage, UnknownMessage}; use sdk_common::silentpayments::{ - check_transaction, create_transaction_for_address_with_shared_secret, + check_transaction, create_transaction, create_transaction_for_address_with_shared_secret, }; use sp_client::spclient::{derive_keys_from_seed, OutputList, OwnedOutput, SpClient}; use sp_client::spclient::{SpWallet, SpendKey}; use crate::user::{lock_connected_user, User, UserWallets, CONNECTED_USER}; -use crate::{images, lock_scanned_transactions, Txid2Secrets}; +use crate::{images, lock_scanned_transactions, lock_secrets, lock_watched, Txid2Secrets}; use crate::process::Process; @@ -330,16 +331,80 @@ 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)?)?; + + 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()); + } + } + } + } + + Err(anyhow::Error::msg("Not spending a watched output")) +} + +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")); + } + // 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)); + } else { + return Err(anyhow::Error::msg("Received a confirmation from an umapped output")); + } + } + } + } + // 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( tx_hex: String, blockheight: u32, tweak_data_hex: String, + fee_rate: u32, ) -> ApiResult { let tx = deserialize::(&Vec::from_hex(&tx_hex)?)?; - // check that we don't already have scanned the tx - if lock_scanned_transactions()?.contains_key(&tx.txid()) { - return Err(ApiError { message: "Transaction already scanned".to_owned()}); + + // 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(), + }); } let tweak_data = PublicKey::from_str(&tweak_data_hex)?; @@ -347,30 +412,28 @@ pub fn check_transaction_for_silent_payments( 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) { - let shared_point = shared_secret_point(&tweak_data, &recover.get_client().get_scan_key()); - lock_scanned_transactions()?.insert(tx.txid(), vec![("".to_owned(), AnkSharedSecret::new(shared_point, false))]); + 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); } } if let Ok(main) = connected_user.try_get_mut_main() { if let Ok(txid) = check_transaction(&tx, main, blockheight, tweak_data) { - let shared_point = shared_secret_point(&tweak_data, &main.get_client().get_scan_key()); - lock_scanned_transactions()?.insert(tx.txid(), vec![("".to_owned(), AnkSharedSecret::new(shared_point, false))]); + // TODO return Ok(txid); } } if let Ok(revoke) = connected_user.try_get_mut_revoke() { if let Ok(txid) = check_transaction(&tx, revoke, blockheight, tweak_data) { - let shared_point = shared_secret_point(&tweak_data, &revoke.get_client().get_scan_key()); - lock_scanned_transactions()?.insert(tx.txid(), vec![("".to_owned(), AnkSharedSecret::new(shared_point, false))]); + // TODO return Ok(txid); } } - // we still want to insert an empty entry in our cache to make sure we don't scan the transaction again - lock_scanned_transactions()?.insert(tx.txid(), vec![("".to_owned(), AnkSharedSecret::default())]); Err(ApiError { message: "No output found".to_owned(), }) @@ -385,7 +448,7 @@ pub struct parseNetworkMsgReturn { } #[wasm_bindgen] -pub fn parse_network_msg(raw: String) -> ApiResult { +pub fn parse_network_msg(raw: String, fee_rate: u32) -> ApiResult { if let Ok(ank_msg) = serde_json::from_str::(&raw) { match ank_msg.flag { AnkFlag::NewTx => { @@ -399,6 +462,7 @@ pub fn parse_network_msg(raw: String) -> ApiResult { tx_message.transaction, 0, tx_message.tweak_data.unwrap(), + fee_rate, )?; return Ok(parseNetworkMsgReturn { topic: AnkFlag::NewTx.as_str().to_owned(), @@ -414,44 +478,60 @@ pub fn parse_network_msg(raw: String) -> ApiResult { } AnkFlag::Unknown => { // try to decrypt the cipher with all available keys - let mut plaintext: String = "".to_owned(); - for (txid, secret_vec) in lock_scanned_transactions()?.iter() { - for (shared_with, ank_secret) in secret_vec.iter() { - if *ank_secret == AnkSharedSecret::default() { - continue; - } + 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(); - if let Ok(msg_decrypt) = Aes256Decryption::new( + 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(plain) => { - plaintext = String::from_utf8(plain)?; - break; - }, - Err(e) => { - debug!("{}", e); - debug!("Failed to decrypt message {} with key {}", ank_msg.content, shared_secret.to_lower_hex_string()); + )?; + 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() + ); } } } } - if plaintext.is_empty() { - // keep the message in cache, just in case - // return an error - return Err(ApiError { - message: "No key found".to_owned(), - }); - } else { - // return the plain text - return Ok(parseNetworkMsgReturn { - topic: AnkFlag::Unknown.as_str().to_owned(), - message: plaintext, - }); - } + // keep the message in cache, just in case? + // return an error + return Err(ApiError { + message: "No key found".to_owned(), + }); } _ => unimplemented!(), } @@ -523,7 +603,7 @@ pub struct createNotificationTransactionReturn { #[wasm_bindgen] pub fn create_notification_transaction( recipient: String, - message: String, + message: Option, fee_rate: u32, ) -> ApiResult { let sp_address: SilentPaymentAddress = recipient.try_into()?; @@ -544,13 +624,16 @@ pub fn create_notification_transaction( Amount::from_sat(fee_rate.into()), )?; - debug!("Created transaction with secret {}", shared_secret.to_byte_array().to_lower_hex_string()); + 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_scanned_transactions()?.insert(transaction.txid(), address2secret.clone()); + lock_secrets()?.insert(transaction.txid(), address2secret.clone()); Ok(createNotificationTransactionReturn { txid: transaction.txid().to_string(), diff --git a/crates/sp_client/src/lib.rs b/crates/sp_client/src/lib.rs index 8d7155c..4802a9f 100644 --- a/crates/sp_client/src/lib.rs +++ b/crates/sp_client/src/lib.rs @@ -2,9 +2,9 @@ use anyhow::Error; use sdk_common::crypto::AnkSharedSecret; use serde::{Deserialize, Serialize}; -use sp_client::bitcoin::Txid; +use sp_client::bitcoin::{OutPoint, Txid}; use sp_client::silentpayments::sending::SilentPaymentAddress; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::fmt::Debug; use std::sync::{Mutex, MutexGuard, OnceLock}; use tsify::Tsify; @@ -16,16 +16,40 @@ 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 TRANSACTIONCACHE: OnceLock> = OnceLock::new(); +pub static SECRETCACHE: OnceLock> = OnceLock::new(); -pub fn lock_scanned_transactions() -> Result, Error> { - TRANSACTIONCACHE +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())) + .lock_anyhow() +} + pub(crate) trait MutexExt { fn lock_anyhow(&self) -> Result, Error>; } diff --git a/src/services.ts b/src/services.ts index c328cd3..4d95df8 100644 --- a/src/services.ts +++ b/src/services.ts @@ -105,7 +105,7 @@ class Services { const recipientSpAddress = spAddressElement.value; const message = messageElement.value; - const msg_payload = JSON.stringify({sender: this.sp_address, payload: message}); + const msg_payload = JSON.stringify({sender: this.sp_address, message: message}); let notificationInfo = await services.notify_address_for_message(recipientSpAddress, msg_payload); if (notificationInfo) { @@ -313,14 +313,13 @@ class Services { services.attachSubmitListener("form4nk", services.updateAnId); } - public async parseNetworkMessage(raw: string): Promise { + public async parseNetworkMessage(raw: string, feeRate: number): Promise { const services = await Services.getInstance(); try { - const msg: parseNetworkMsgReturn = services.sdkClient.parse_network_msg(raw); + const msg: parseNetworkMsgReturn = services.sdkClient.parse_network_msg(raw, feeRate); return msg; } catch (error) { - console.error(error); - return null; + throw error; } } @@ -802,6 +801,40 @@ class Services { } } + public async confirm_sender_address(sp_address: string): 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: createNotificationTransactionReturn; + try { + const feeRate = 1; + notificationInfo = services.sdkClient.create_notification_transaction(sp_address, undefined, 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 + } + connection.sendMessage(flag, JSON.stringify(newTxMsg)); + return; + } + public async notify_address_for_message(sp_address: string, message: string): Promise { const services = await Services.getInstance(); const connection = await services.pickWebsocketConnectionRandom(); diff --git a/src/websockets.ts b/src/websockets.ts index 3342c31..0234f56 100644 --- a/src/websockets.ts +++ b/src/websockets.ts @@ -25,28 +25,27 @@ class WebSocketClient { (async () => { if (typeof(msgData) === 'string') { - console.log("Received text message: "+msgData); - let res = await services.parseNetworkMessage(msgData); - if (res) { + // console.log("Received text message: "+msgData); + try { + const feeRate = 1; + let res = await services.parseNetworkMessage(msgData, feeRate); if (res.topic === 'new_tx') { // we received a tx window.alert(`New tx\n${res.message}`); await services.updateOwnedOutputsForUser(); } else if (res.topic === 'unknown') { - // Do we have a json with a sender? - try { - let parsed = JSON.parse(res.message); - if (parsed.sender !== undefined) { - console.info(`Message sent by ${parsed.sender}`); - } - window.alert(`new message: ${parsed.payload}`); - } catch (_) { - window.alert(`new message: ${res.message}`); - } + let parsed = JSON.parse(res.message); + let message = parsed['message']; + let sender = parsed['sender']; + window.alert(`new message: ${message}\nAsking sender ${sender} to confirm identity...`); + console.debug(`sending confirm message to ${sender}`); + await services.confirm_sender_address(sender); } + } catch (error) { + console.error('Received an invalid message:', error); } } else { - console.error("Received an invalid message"); + console.error('Received a non-string message'); } })(); }); From 0f7bc644c8375c2d47715f4ca755beb2d4433a97 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Wed, 22 May 2024 10:15:42 +0200 Subject: [PATCH 25/40] Heavy refactor of the caching and message structure --- crates/sp_client/Cargo.toml | 4 +- crates/sp_client/src/api.rs | 581 ++++++++++++++++++++++++++---------- crates/sp_client/src/lib.rs | 35 +-- package.json | 2 +- src/database.ts | 5 + src/services.ts | 90 +++--- src/websockets.ts | 9 +- 7 files changed, 488 insertions(+), 238 deletions(-) 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) { From a93aed0edef42745c75c6f65fb81b98d48b1761e Mon Sep 17 00:00:00 2001 From: Sosthene Date: Wed, 22 May 2024 20:17:53 +0200 Subject: [PATCH 26/40] [bug fix] Working faucet + small fixes --- crates/sp_client/src/api.rs | 255 +++++++++++++----------------------- crates/sp_client/src/lib.rs | 8 +- 2 files changed, 95 insertions(+), 168 deletions(-) diff --git a/crates/sp_client/src/api.rs b/crates/sp_client/src/api.rs index 19c348c..91f8c67 100644 --- a/crates/sp_client/src/api.rs +++ b/crates/sp_client/src/api.rs @@ -34,7 +34,7 @@ use wasm_bindgen::convert::FromWasmAbi; use wasm_bindgen::prelude::*; use sdk_common::network::{ - self, AnkFlag, AnkNetworkMsg, FaucetMessage, NewTxMessage, UnknownMessage, + self, AnkFlag, AnkNetworkMsg, CachedMessage, CachedMessageStatus, FaucetMessage, NewTxMessage, UnknownMessage }; use sdk_common::silentpayments::{ create_transaction, create_transaction_for_address_with_shared_secret, @@ -371,10 +371,10 @@ fn handle_recover_transaction( sp_wallet: &mut SpWallet, tweak_data: PublicKey, fee_rate: u32, -) -> anyhow::Result { +) -> anyhow::Result { // We need to look for different case: // 1) faucet - // This one is the simplest, we only care about finding the commitment + // This one is the simplest, we only care about finding the commitment.clone() let op_return = tx.output.iter().find(|o| o.script_pubkey.is_op_return()); let commitment = if op_return.is_none() { vec![] @@ -382,14 +382,18 @@ fn handle_recover_transaction( op_return.unwrap().script_pubkey.as_bytes()[2..].to_vec() }; let commitment_str = commitment.to_lower_hex_string(); - let pos = lock_messages()? - .iter() - .position(|m| m.commitment.as_ref() == Some(&commitment_str)); + { + let mut messages = lock_messages()?; + let pos = messages + .iter() + .position(|m| m.commitment.as_ref() == Some(&commitment_str)); - if pos.is_some() { - let messages = lock_messages()?; - let message = messages.get(pos.unwrap()); - return Ok(message.cloned().unwrap()); + if pos.is_some() { + let mut message = messages.swap_remove(pos.unwrap()); + message.commited_in = updated.into_iter().next().map(|(outpoint, _)| outpoint); + message.status = CachedMessageStatus::FaucetComplete; + return Ok(message); + } } // If we got updates from a transaction, it means that it creates an output to us, spend an output we owned, or both @@ -423,7 +427,7 @@ fn handle_recover_transaction( // If we are receiver, that's pretty much it, just set status to complete if message.recipient == Some(sp_wallet.get_client().get_receiving_address()) { debug_assert!(message.confirmed_by == Some(input.previous_output)); - message.status = NetworkMessageStatus::Complete; + message.status = CachedMessageStatus::Complete; return Ok(message.clone()); } @@ -443,11 +447,11 @@ fn handle_recover_transaction( let mut messages = lock_messages()?; let cipher_pos = messages.iter().position(|m| { - if m.status != NetworkMessageStatus::CipherWaitingTx { + if m.status != CachedMessageStatus::CipherWaitingTx { return false; } m.try_decrypt_with_shared_secret(shared_secret.to_byte_array()) - .is_some() + .is_ok() }); if cipher_pos.is_some() { @@ -468,15 +472,17 @@ fn handle_recover_transaction( return Ok(message.clone()) } else { // store it and wait for the message - let mut new_msg = NetworkMessage::default(); - let (outpoint, output) = utxo_created.iter().next().unwrap(); - new_msg.commited_in = Some(**outpoint); + let mut new_msg = CachedMessage::new(); + debug!("{:?}", utxo_created); + let (outpoint, output) = utxo_created.into_iter().next().expect("utxo_created shouldn't be empty"); + new_msg.commited_in = Some(outpoint.clone()); new_msg.commitment = Some(commitment.to_lower_hex_string()); new_msg.recipient = Some(sp_wallet.get_client().get_receiving_address()); new_msg.shared_secret = Some(shared_secret.to_byte_array().to_lower_hex_string()); - new_msg.status = NetworkMessageStatus::TxWaitingCipher; + new_msg.status = CachedMessageStatus::TxWaitingCipher; lock_messages()?.push(new_msg.clone()); + debug!("returning {:?}", new_msg); return Ok(new_msg.clone()); } } @@ -497,14 +503,14 @@ fn handle_recover_transaction( } } -/// If the transaction has anything to do with us, we create/update the relevant `NetworkMessage` +/// If the transaction has anything to do with us, we create/update the relevant `CachedMessage` /// and return it to caller for persistent storage fn process_transaction( tx_hex: String, blockheight: u32, tweak_data_hex: String, fee_rate: u32, -) -> anyhow::Result { +) -> anyhow::Result { let tx = deserialize::(&Vec::from_hex(&tx_hex)?)?; // check that we don't already have scanned the tx @@ -545,16 +551,8 @@ fn process_transaction( Err(anyhow::Error::msg("No output found")) } -#[derive(Tsify, Serialize, Deserialize)] -#[tsify(into_wasm_abi, from_wasm_abi)] -#[allow(non_camel_case_types)] -pub struct parseNetworkMsgReturn { - topic: String, - message: NetworkMessage, -} - #[wasm_bindgen] -pub fn parse_network_msg(raw: String, fee_rate: u32) -> ApiResult { +pub fn parse_network_msg(raw: String, fee_rate: u32) -> ApiResult { if let Ok(ank_msg) = serde_json::from_str::(&raw) { match ank_msg.flag { AnkFlag::NewTx => { @@ -570,29 +568,23 @@ pub fn parse_network_msg(raw: String, fee_rate: u32) -> ApiResult unimplemented!(), AnkFlag::Error => { - let error_msg = NetworkMessage::new_error(ank_msg.content); - return Ok(parseNetworkMsgReturn { - topic: AnkFlag::Error.as_str().to_owned(), - message: error_msg, - }) + let error_msg = CachedMessage::new_error(ank_msg.content); + return Ok(error_msg) } AnkFlag::Unknown => { // let's try to decrypt with keys we found in transactions but haven't used yet let mut messages = lock_messages()?; - let cipher = Vec::from_hex(&ank_msg.content)?; + let cipher = Vec::from_hex(&ank_msg.content.trim_matches('\"'))?; let cipher_pos = messages.iter().position(|m| { - if m.status != NetworkMessageStatus::TxWaitingCipher { + debug!("Trying message: {:?}", m); + if m.status != CachedMessageStatus::TxWaitingCipher { return false; } - m.try_decrypt_cipher(cipher.clone()).is_some() + m.try_decrypt_cipher(cipher.clone()).is_ok() }); if cipher_pos.is_some() { let mut message = messages.get_mut(cipher_pos.unwrap()).unwrap(); @@ -601,14 +593,11 @@ pub fn parse_network_msg(raw: String, fee_rate: u32) -> ApiResult ApiResult { #[derive(Tsify, Serialize, Deserialize)] #[tsify(into_wasm_abi, from_wasm_abi)] #[allow(non_camel_case_types)] -pub struct createNotificationTransactionReturn { +pub struct createTransactionReturn { pub txid: String, pub transaction: String, - pub new_network_msg: NetworkMessage, + pub new_network_msg: CachedMessage, +} + +/// This is what we call to answer a confirmation as a sender +#[wasm_bindgen] +pub fn answer_confirmation_transaction( + message: CachedMessage, + fee_rate: u32, +) -> ApiResult { + if message.recipient.is_none() || message.confirmed_by.is_none() { + return Err(ApiError { message: "Invalid network message".to_owned() }); + } + + let sp_address: SilentPaymentAddress = message.recipient.as_ref().unwrap().as_str().try_into()?; + + let connected_user = lock_connected_user()?; + + let sp_wallet: &SpWallet; + if sp_address.is_testnet() { + sp_wallet = connected_user.try_get_recover()?; + } else { + sp_wallet = connected_user.try_get_main()?; + } + + let recipient = Recipient { + address: sp_address.into(), + amount: Amount::from_sat(0), // we'll set amount to what's available in the confirmed_by output we don't want change + nb_outputs: 1, + }; + + let signed_psbt = create_transaction_spend_outpoint( + &message.confirmed_by.unwrap(), + sp_wallet, + recipient, + Amount::from_sat(fee_rate.into()) + )?; + + let final_tx = signed_psbt.extract_tx()?; + + Ok(createTransactionReturn { + txid: final_tx.txid().to_string(), + transaction: serialize(&final_tx).to_lower_hex_string(), + new_network_msg: message + }) } /// This is what we call to confirm as a receiver #[wasm_bindgen] pub fn create_confirmation_transaction( - message: NetworkMessage, + message: CachedMessage, fee_rate: u32, -) -> ApiResult { +) -> ApiResult { if message.sender.is_none() || message.confirmed_by.is_none() { return Err(ApiError { message: "Invalid network message".to_owned() }); } @@ -706,7 +738,7 @@ pub fn create_confirmation_transaction( let recipient = Recipient { address: sp_address.into(), - amount: Amount::from_sat(1200), + amount: Amount::from_sat(0), nb_outputs: 1, }; @@ -719,7 +751,7 @@ pub fn create_confirmation_transaction( let final_tx = signed_psbt.extract_tx()?; - Ok(createNotificationTransactionReturn { + Ok(createTransactionReturn { txid: final_tx.txid().to_string(), transaction: serialize(&final_tx).to_lower_hex_string(), new_network_msg: message @@ -731,7 +763,7 @@ pub fn create_notification_transaction( address: String, commitment: Option, fee_rate: u32, -) -> ApiResult { +) -> ApiResult { let sp_address: SilentPaymentAddress = address.as_str().try_into()?; let connected_user = lock_connected_user()?; @@ -782,16 +814,17 @@ pub fn create_notification_transaction( let recipients_vouts = sp_address2vouts.get::(&address).expect("recipients didn't change").as_slice(); // for now let's just take the smallest vout that belongs to the recipient let final_tx = psbt.extract_tx()?; - let mut new_msg = NetworkMessage::default(); + let mut new_msg = CachedMessage::default(); new_msg.commitment = commitment; new_msg.commited_in = Some(OutPoint { txid: final_tx.txid(), vout: recipients_vouts[0] as u32 }); new_msg.shared_secret = Some(shared_secret.to_byte_array().to_lower_hex_string()); new_msg.recipient = Some(address); new_msg.sender = Some(sp_wallet.get_client().get_receiving_address()); + new_msg.status = CachedMessageStatus::SentWaitingConfirmation; // plaintext and ciphertext to be added later when sending the encrypted message lock_messages()?.push(new_msg.clone()); - Ok(createNotificationTransactionReturn { + Ok(createTransactionReturn { txid: final_tx.txid().to_string(), transaction: serialize(&final_tx).to_lower_hex_string(), new_network_msg: new_msg, @@ -872,8 +905,9 @@ pub fn create_faucet_msg() -> ApiResult { let sp_address = user.try_get_recover()?.get_client().get_receiving_address(); let faucet_msg = FaucetMessage::new(sp_address); // we write the commitment in a networkmessage so that we can keep track - let mut network_msg = NetworkMessage::default(); + let mut network_msg = CachedMessage::new(); network_msg.commitment = Some(faucet_msg.commitment.clone()); + network_msg.status = CachedMessageStatus::FaucetWaiting; lock_messages()?.push(network_msg); Ok(faucet_msg) } @@ -885,110 +919,3 @@ pub fn create_commitment(payload_to_hash: String) -> String { let hash = sha256::Hash::from_engine(engine); hash.to_byte_array().to_lower_hex_string() } - -#[derive(Debug, Serialize, Deserialize, PartialEq, Tsify, Clone)] -pub enum NetworkMessageStatus { - NoStatus, // Default - CipherWaitingTx, - TxWaitingCipher, - SentWaitingConfirmation, - MustSpentConfirmation, - Complete, -} - -impl Default for NetworkMessageStatus { - fn default() -> Self { - Self::NoStatus - } -} - -/// Unique struct for both 4nk messages and notification/key exchange, both rust and ts -/// 1. Faucet: commited_in with nothing else, status is NoStatus -/// 2. notification: -/// 1. sender: ciphertext, plaintext, commited_in, sender, recipient, shared_secret, key -/// 2. receiver (without tx): ciphertext -/// 3. receiver (tx without msg): commited_in, commitment, recipient, shared_secret -/// 4. receiver (receive tx after msg): plaintext, key, sender, commited_in, commitment, recipient, shared_secret -/// 5. receiver (msg after tx): ciphertext, key, plaintext, sender -/// 3. confirmation: -/// 1. receiver (spend the smallest vout that pays him in the first tx): confirmed_by -/// 2. sender (detect a transaction that pays him and spend commited_by): confirmed_by -/// 3. sender toggle status to complete when it spent confirmed_by, receiver when it detects the confirmed_by is spent -#[derive(Debug, Default, Serialize, Deserialize, Tsify, Clone)] -#[tsify(into_wasm_abi, from_wasm_abi)] -#[allow(non_camel_case_types)] -pub struct NetworkMessage { - pub id: u32, - pub status: NetworkMessageStatus, - pub ciphertext: Option, // When we receive message we can't decrypt we only have this and commited_in_tx - pub plaintext: Option, // Never None when message sent - pub commited_in: Option, - pub commitment: Option, // content of the op_return - pub sender: Option, // Never None when message sent - pub recipient: Option, // Never None when message sent - pub shared_secret: Option, // Never None when message sent - pub key: Option, // Never None when message sent - pub confirmed_by: Option, // If this None, Sender keeps sending - pub timestamp: u64, - pub error: Option, -} - -impl NetworkMessage { - pub fn new() -> Self { - let mut new = NetworkMessage::default(); - let mut buf = [0u8;4]; - thread_rng().fill_bytes(&mut buf); - new.id = u32::from_be_bytes(buf); - new - } - - pub fn new_error(error: String) -> Self { - let mut new = NetworkMessage::default(); - new.error = Some(error); - new - } - - pub fn try_decrypt_cipher(&self, cipher: Vec) -> Option> { - if self.ciphertext.is_some() || self.shared_secret.is_none() { - log::error!( - "Can't try decrypt this message, there's already a ciphertext or no shared secret" - ); - return None; - } - let mut shared_secret = [0u8; 32]; - shared_secret - .copy_from_slice(&Vec::from_hex(self.shared_secret.as_ref().unwrap()).unwrap()); - let aes_decrypt = Aes256Decryption::new(Purpose::Arbitrary, cipher, shared_secret); - - if aes_decrypt.is_err() { - log::error!("Failed to create decrypt object"); - return None; - } - - aes_decrypt.unwrap().decrypt_with_key().ok() - } - - pub fn try_decrypt_with_shared_secret(&self, shared_secret: [u8; 32]) -> Option> { - if self.ciphertext.is_none() || self.shared_secret.is_some() { - log::error!( - "Can't try decrypt this message, ciphertext is none or shared_secret already found" - ); - return None; - } - let cipher_bin = Vec::from_hex(self.ciphertext.as_ref().unwrap()); - if cipher_bin.is_err() { - let error = cipher_bin.unwrap_err(); - log::error!("Invalid hex in ciphertext: {}", error.to_string()); - return None; - } - let aes_decrypt = - Aes256Decryption::new(Purpose::Arbitrary, cipher_bin.unwrap(), shared_secret); - - if aes_decrypt.is_err() { - log::error!("Failed to create decrypt object"); - return None; - } - - aes_decrypt.unwrap().decrypt_with_key().ok() - } -} diff --git a/crates/sp_client/src/lib.rs b/crates/sp_client/src/lib.rs index 4980f18..3643833 100644 --- a/crates/sp_client/src/lib.rs +++ b/crates/sp_client/src/lib.rs @@ -1,7 +1,7 @@ #![allow(warnings)] use anyhow::Error; -use api::NetworkMessage; use sdk_common::crypto::AnkSharedSecret; +use sdk_common::network::CachedMessage; use serde::{Deserialize, Serialize}; use sp_client::bitcoin::{OutPoint, Txid}; use sp_client::silentpayments::sending::SilentPaymentAddress; @@ -17,10 +17,10 @@ mod peers; mod process; mod user; -pub static NETWORKMESSAGES: OnceLock>> = OnceLock::new(); +pub static CACHEDMESSAGES: OnceLock>> = OnceLock::new(); -pub fn lock_messages() -> Result>, Error> { - NETWORKMESSAGES +pub fn lock_messages() -> Result>, Error> { + CACHEDMESSAGES .get_or_init(|| Mutex::new(vec![])) .lock_anyhow() } From 29c1db586991625adc8825a3d44d42861b7f057d Mon Sep 17 00:00:00 2001 From: Sosthene Date: Wed, 22 May 2024 20:21:59 +0200 Subject: [PATCH 27/40] database add rmObject --- src/database.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/database.ts b/src/database.ts index d5eaa83..81bb802 100644 --- a/src/database.ts +++ b/src/database.ts @@ -117,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'); From 2f4cd4fb9b320e83a845101dc57ddf162ba162c7 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Wed, 22 May 2024 20:43:31 +0200 Subject: [PATCH 28/40] Reformat websocket message parsing --- src/websockets.ts | 46 ++++++++++++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/src/websockets.ts b/src/websockets.ts index 5067254..67d8434 100644 --- a/src/websockets.ts +++ b/src/websockets.ts @@ -1,5 +1,5 @@ import Services from "./services"; -import { AnkFlag, AnkNetworkMsg, parseNetworkMsgReturn } from "../dist/pkg/sdk_client"; +import { AnkFlag, AnkNetworkMsg, CachedMessage } from "../dist/pkg/sdk_client"; class WebSocketClient { private ws: WebSocket; @@ -25,24 +25,38 @@ class WebSocketClient { (async () => { if (typeof(msgData) === 'string') { - // console.log("Received text message: "+msgData); + console.log("Received text message: "+msgData); try { const feeRate = 1; - let res = await services.parseNetworkMessage(msgData, feeRate); - if (res.topic === 'new_tx') { - // we received a tx - window.alert(`New tx\n${res.message}`); + // 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.removeMessage(res.id); await services.updateOwnedOutputsForUser(); - } else if (res.topic === 'unknown') { - 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); + } 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.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.confirm_sender_address(res); + } else { + console.debug('Received an unimplemented valid message'); } } catch (error) { console.error('Received an invalid message:', error); From a251458f04f32228349feacc1a44564ab960a134 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Wed, 22 May 2024 20:53:40 +0200 Subject: [PATCH 29/40] services various improvements --- src/services.ts | 49 +++++++++++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/src/services.ts b/src/services.ts index b5ca897..16b5a93 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, NetworkMessage } from '../dist/pkg/sdk_client'; +import { createUserReturn, User, Process, createTransactionReturn, parse_network_msg, outputs_list, FaucetMessage, AnkFlag, NewTxMessage, encryptWithNewKeyResult, AnkSharedSecret, CachedMessage } from '../dist/pkg/sdk_client'; import IndexedDB from './database' import { WebSocketClient } from './websockets'; @@ -110,38 +110,31 @@ 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(); - + + const connection = await services.pickWebsocketConnectionRandom(); + const flag: AnkFlag = "Unknown"; // encrypt the message(s) + // TODO we'd rather do that in the wasm as part of notify_address_for_message 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); + await services.updateMessages(updated_msg); + connection?.sendMessage(flag, cipher); } catch (error) { throw error; } - const connection = await services.pickWebsocketConnectionRandom(); - const flag: AnkFlag = "Unknown"; // add peers list // add processes list // send message (transaction in envelope) - for (const payload of ciphers) { - connection?.sendMessage(flag, payload); - } } } @@ -322,10 +315,10 @@ class Services { services.attachSubmitListener("form4nk", services.updateAnId); } - public async parseNetworkMessage(raw: string, feeRate: number): Promise { + public async parseNetworkMessage(raw: string, feeRate: number): Promise { const services = await Services.getInstance(); try { - const msg: parseNetworkMsgReturn = services.sdkClient.parse_network_msg(raw, feeRate); + const msg: CachedMessage = services.sdkClient.parse_network_msg(raw, feeRate); return msg; } catch (error) { throw error; @@ -443,7 +436,7 @@ class Services { return process; } - public async updateMessages(message: NetworkMessage): Promise { + public async updateMessages(message: CachedMessage): Promise { const indexedDb = await IndexedDB.getInstance(); const db = await indexedDb.getDb(); @@ -454,6 +447,17 @@ class Services { } } + 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(); @@ -768,6 +772,7 @@ class Services { const flag: AnkFlag = 'Faucet'; const faucetMsg = services.sdkClient.create_faucet_msg(); connection.sendMessage(flag, JSON.stringify(faucetMsg)); + await services.updateMessages(faucetMsg); } catch (error) { console.error("Failed to obtain tokens with relay ", connection.getUrl()); return null; @@ -806,7 +811,7 @@ class Services { } } - public async confirm_sender_address(sp_address: string): Promise { + public async confirm_sender_address(msg: CachedMessage): Promise { const services = await Services.getInstance(); const connection = await services.pickWebsocketConnectionRandom(); if (!connection) { @@ -824,10 +829,10 @@ class Services { throw error; } - let notificationInfo: createNotificationTransactionReturn; + let notificationInfo: createTransactionReturn; try { const feeRate = 1; - notificationInfo = services.sdkClient.create_notification_transaction(sp_address, null, feeRate); + notificationInfo = services.sdkClient.answer_confirmation_transaction(msg, feeRate); } catch (error) { throw new Error(`Failed to create confirmation transaction: ${error}`); } @@ -840,7 +845,7 @@ 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) { @@ -850,7 +855,7 @@ class Services { try { const feeRate = 1; const commitment = services.sdkClient.create_commitment(message); - let notificationInfo: createNotificationTransactionReturn = services.sdkClient.create_notification_transaction(sp_address, commitment, feeRate); + let notificationInfo: createTransactionReturn = services.sdkClient.create_notification_transaction(sp_address, commitment, feeRate); const flag: AnkFlag = "NewTx"; const newTxMsg: NewTxMessage = { 'transaction': notificationInfo.transaction, From 30c3ec9673a9e8de66ec733bf68b80c1b3400f92 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Wed, 22 May 2024 23:36:18 +0200 Subject: [PATCH 30/40] obtainTokenWithFaucet slight improvement --- crates/sp_client/src/api.rs | 19 +++++++------ src/services.ts | 57 +++++++++++++++++++++---------------- 2 files changed, 44 insertions(+), 32 deletions(-) diff --git a/crates/sp_client/src/api.rs b/crates/sp_client/src/api.rs index 91f8c67..1f54f16 100644 --- a/crates/sp_client/src/api.rs +++ b/crates/sp_client/src/api.rs @@ -900,16 +900,19 @@ pub fn try_decrypt_with_key(cipher: String, key: String) -> ApiResult { } #[wasm_bindgen] -pub fn create_faucet_msg() -> ApiResult { +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 = CachedMessage::new(); - network_msg.commitment = Some(faucet_msg.commitment.clone()); - network_msg.status = CachedMessageStatus::FaucetWaiting; - lock_messages()?.push(network_msg); - Ok(faucet_msg) + + 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] diff --git a/src/services.ts b/src/services.ts index 16b5a93..7ad857d 100644 --- a/src/services.ts +++ b/src/services.ts @@ -87,7 +87,7 @@ class Services { if (availableAmt < 2000) { try { - await services.obtainTokenWithFaucet(this.sp_address!); + await services.obtainTokenWithFaucet(); } catch (error) { console.error('Failed to obtain faucet token:', error); return; @@ -150,16 +150,14 @@ class Services { return; } } catch (error) { - console.error(error); - return; + 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; @@ -172,7 +170,12 @@ class Services { const birthday_signet = 50000; const birthday_main = 500000; - 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; @@ -180,8 +183,7 @@ class Services { // 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 = []; @@ -192,17 +194,13 @@ class Services { 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}`; } try { - this.sp_address = services.sdkClient.get_recover_address(); - if (this.sp_address) { - console.info('Using sp_address:', this.sp_address); - await services.obtainTokenWithFaucet(this.sp_address); - } + await services.obtainTokenWithFaucet(); } catch (error) { - console.error(error); + throw error; } await services.displayRevokeImage(new Uint8Array(revokeData)); @@ -244,7 +242,7 @@ class Services { this.sp_address = services.sdkClient.get_recover_address(); if (this.sp_address) { console.info('Using sp_address:', this.sp_address); - await services.obtainTokenWithFaucet(this.sp_address); + await services.obtainTokenWithFaucet(); } } } catch (error) { @@ -762,22 +760,33 @@ 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 { const flag: AnkFlag = 'Faucet'; - const faucetMsg = services.sdkClient.create_faucet_msg(); - connection.sendMessage(flag, JSON.stringify(faucetMsg)); - await services.updateMessages(faucetMsg); + cachedMsg = services.sdkClient.create_faucet_msg(); + if (cachedMsg.commitment && cachedMsg.recipient) { + let faucetMsg: FaucetMessage = { + sp_address: cachedMsg.recipient, + commitment: cachedMsg.commitment, + } + 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; } - return null; } public async updateUser(user: User): Promise { From f03abf15b9d3ff414c29f21f0bef3d7f23fac059 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Wed, 22 May 2024 23:37:54 +0200 Subject: [PATCH 31/40] [bug] fix duplicate transactions issue in handle_recover_transaction + refactor --- crates/sp_client/src/api.rs | 192 ++++++++++++++++++------------------ 1 file changed, 96 insertions(+), 96 deletions(-) diff --git a/crates/sp_client/src/api.rs b/crates/sp_client/src/api.rs index 1f54f16..a6f2a66 100644 --- a/crates/sp_client/src/api.rs +++ b/crates/sp_client/src/api.rs @@ -372,9 +372,6 @@ fn handle_recover_transaction( 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.clone() let op_return = tx.output.iter().find(|o| o.script_pubkey.is_op_return()); let commitment = if op_return.is_none() { vec![] @@ -382,24 +379,9 @@ fn handle_recover_transaction( op_return.unwrap().script_pubkey.as_bytes()[2..].to_vec() }; let commitment_str = commitment.to_lower_hex_string(); - { - let mut messages = lock_messages()?; - let pos = messages - .iter() - .position(|m| m.commitment.as_ref() == Some(&commitment_str)); - - if pos.is_some() { - let mut message = messages.swap_remove(pos.unwrap()); - message.commited_in = updated.into_iter().next().map(|(outpoint, _)| outpoint); - message.status = CachedMessageStatus::FaucetComplete; - return Ok(message); - } - } // If we got updates from a transaction, it means that it creates an output to us, spend an output we owned, or both - // 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 + // 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) @@ -409,89 +391,107 @@ fn handle_recover_transaction( .filter(|(outpoint, output)| output.spend_status == OutputSpendStatus::Unspent) .collect(); - // 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 = CachedMessageStatus::Complete; - return Ok(message.clone()); - } + let mut messages = lock_messages()?; - // 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()); + // 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 { - // 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 != CachedMessageStatus::CipherWaitingTx { - return false; - } - m.try_decrypt_with_shared_secret(shared_secret.to_byte_array()) - .is_ok() - }); - - 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 { - // store it and wait for the message - let mut new_msg = CachedMessage::new(); - debug!("{:?}", utxo_created); - let (outpoint, output) = utxo_created.into_iter().next().expect("utxo_created shouldn't be empty"); - new_msg.commited_in = Some(outpoint.clone()); - new_msg.commitment = Some(commitment.to_lower_hex_string()); - new_msg.recipient = Some(sp_wallet.get_client().get_receiving_address()); - new_msg.shared_secret = - Some(shared_secret.to_byte_array().to_lower_hex_string()); - new_msg.status = CachedMessageStatus::TxWaitingCipher; - lock_messages()?.push(new_msg.clone()); - debug!("returning {:?}", new_msg); - return Ok(new_msg.clone()); - } + 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()); + }, + CachedMessageStatus::FaucetComplete => return Ok(message.clone()), + _ => () } } - unreachable!("Transaction with no inputs"); + + // we inspect inputs looking for links with previous tx + for input in tx.input.iter() { + if let Some(pos) = messages.iter().position(|m| { + 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 = + shared_secret_point(&tweak_data, &sp_wallet.get_client().get_scan_key()); + let shared_secret = AnkSharedSecret::new(shared_point); + + if let Some(cipher_pos) = messages.iter().position(|m| { + if m.status != CachedMessageStatus::CipherWaitingTx { + return false; + } + m.try_decrypt_with_shared_secret(shared_secret.to_byte_array()) + .is_ok() + }) + { + let message = messages.get_mut(cipher_pos).unwrap(); + + let (outpoint, output) = utxo_created.into_iter().next().unwrap(); + + message.commited_in = Some(outpoint.clone()); + message.shared_secret = + Some(shared_secret.to_byte_array().to_lower_hex_string()); + message.commitment = Some(commitment_str); + + 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()); + 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 { - // We are sender of a notification transaction + // We are sender of a transaction // We only need to return the message - if let Some(message) = lock_messages()?.iter() + if let Some(message) = messages.iter() .find(|m| { m.commitment.as_ref() == Some(&commitment_str) }) From c3e435a22854f0f0a83f981aa32d979d703c7886 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Thu, 23 May 2024 15:36:40 +0200 Subject: [PATCH 32/40] [bug] don't panic if there's a main/revoke wallet loaded --- crates/sp_client/src/api.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/crates/sp_client/src/api.rs b/crates/sp_client/src/api.rs index a6f2a66..b3b4b34 100644 --- a/crates/sp_client/src/api.rs +++ b/crates/sp_client/src/api.rs @@ -540,12 +540,16 @@ fn process_transaction( if let Ok(main) = connected_user.try_get_mut_main() { let updated = main.update_wallet_with_transaction(&tx, blockheight, tweak_data)?; - unimplemented!(); + 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)?; - unimplemented!(); + if updated.len() > 0 { + unimplemented!(); + } } Err(anyhow::Error::msg("No output found")) @@ -596,7 +600,7 @@ pub fn parse_network_msg(raw: String, fee_rate: u32) -> ApiResult return Ok(message.clone()); } else { // let's keep it in case we receive the transaction later - let mut new_msg = CachedMessage::default(); + let mut new_msg = CachedMessage::new(); new_msg.status = CachedMessageStatus::CipherWaitingTx; new_msg.ciphertext = Some(ank_msg.content); messages.push(new_msg); From bc6f95a98fc9933eb0fe8dd1f3732ed3c16676d8 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Fri, 24 May 2024 22:40:40 +0200 Subject: [PATCH 33/40] Message processing heavy refactoring --- crates/sp_client/src/api.rs | 206 +++++++++++++++++++++++------------- src/services.ts | 111 ++++++++----------- src/websockets.ts | 15 ++- 3 files changed, 191 insertions(+), 141 deletions(-) diff --git a/crates/sp_client/src/api.rs b/crates/sp_client/src/api.rs index b3b4b34..d1edc07 100644 --- a/crates/sp_client/src/api.rs +++ b/crates/sp_client/src/api.rs @@ -25,7 +25,8 @@ 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, Network, OutPoint, Psbt, Transaction, Txid}; -use sp_client::silentpayments::Error as SpError; +use sp_client::silentpayments::utils as sp_utils; +use sp_client::silentpayments::{Error as SpError, Network as SpNetwork}; use serde::{Deserialize, Serialize}; use sp_client::silentpayments::sending::SilentPaymentAddress; @@ -34,11 +35,12 @@ use wasm_bindgen::convert::FromWasmAbi; use wasm_bindgen::prelude::*; use sdk_common::network::{ - self, AnkFlag, AnkNetworkMsg, CachedMessage, CachedMessageStatus, FaucetMessage, NewTxMessage, UnknownMessage + self, AnkFlag, AnkNetworkMsg, CachedMessage, CachedMessageStatus, FaucetMessage, NewTxMessage, + UnknownMessage, }; use sdk_common::silentpayments::{ create_transaction, create_transaction_for_address_with_shared_secret, - create_transaction_spend_outpoint, map_outputs_to_sp_address + create_transaction_spend_outpoint, map_outputs_to_sp_address, }; use sp_client::spclient::{ @@ -402,23 +404,29 @@ fn handle_recover_transaction( } 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); + 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| { + if let Some(pos) = messages + .iter() + .position(|m| { + debug!("{:?}", Some(input.previous_output)); m.confirmed_by == Some(input.previous_output) }) { @@ -426,9 +434,9 @@ fn handle_recover_transaction( // 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) - }) + } 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(); @@ -444,9 +452,16 @@ fn handle_recover_transaction( } // if we've found nothing 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 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() + ); if let Some(cipher_pos) = messages.iter().position(|m| { if m.status != CachedMessageStatus::CipherWaitingTx { @@ -454,15 +469,13 @@ fn handle_recover_transaction( } m.try_decrypt_with_shared_secret(shared_secret.to_byte_array()) .is_ok() - }) - { + }) { let message = messages.get_mut(cipher_pos).unwrap(); let (outpoint, output) = utxo_created.into_iter().next().unwrap(); message.commited_in = Some(outpoint.clone()); - message.shared_secret = - Some(shared_secret.to_byte_array().to_lower_hex_string()); + message.shared_secret = Some(shared_secret.to_byte_array().to_lower_hex_string()); message.commitment = Some(commitment_str); let plaintext = message @@ -474,16 +487,18 @@ fn handle_recover_transaction( message.recipient = Some(sp_wallet.get_client().get_receiving_address()); message.status = CachedMessageStatus::ReceivedMustConfirm; - return Ok(message.clone()) + 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"); + 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.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()); @@ -491,14 +506,15 @@ fn handle_recover_transaction( } else { // We are sender of a transaction // We only need to return the message - if let Some(message) = messages.iter() - .find(|m| { - m.commitment.as_ref() == Some(&commitment_str) - }) + if let Some(message) = 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")); + return Err(anyhow::Error::msg( + "We spent a transaction for a commitment we don't know", + )); } } } @@ -577,7 +593,7 @@ pub fn parse_network_msg(raw: String, fee_rate: u32) -> ApiResult AnkFlag::Faucet => unimplemented!(), AnkFlag::Error => { let error_msg = CachedMessage::new_error(ank_msg.content); - return Ok(error_msg) + return Ok(error_msg); } AnkFlag::Unknown => { // let's try to decrypt with keys we found in transactions but haven't used yet @@ -597,16 +613,15 @@ pub fn parse_network_msg(raw: String, fee_rate: u32) -> ApiResult message.plaintext = Some(unknown_msg.message); message.sender = Some(unknown_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); - return Err(ApiError { - message: "Can't decrypt message".to_owned(), - }); + messages.push(new_msg.clone()); + return Ok(new_msg); } } _ => unimplemented!(), @@ -676,22 +691,33 @@ pub struct createTransactionReturn { pub new_network_msg: CachedMessage, } -/// This is what we call to answer a confirmation as a sender +/// This is what we call to answer a confirmation as a sender #[wasm_bindgen] pub fn answer_confirmation_transaction( - message: CachedMessage, + message_id: u32, fee_rate: u32, ) -> ApiResult { - if message.recipient.is_none() || message.confirmed_by.is_none() { - return Err(ApiError { message: "Invalid network message".to_owned() }); + 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 sp_address: SilentPaymentAddress = + message.recipient.as_ref().unwrap().as_str().try_into()?; let connected_user = lock_connected_user()?; let sp_wallet: &SpWallet; - if sp_address.is_testnet() { + if sp_address.get_network() != SpNetwork::Mainnet { sp_wallet = connected_user.try_get_recover()?; } else { sp_wallet = connected_user.try_get_main()?; @@ -703,38 +729,53 @@ pub fn answer_confirmation_transaction( 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( - &message.confirmed_by.unwrap(), - sp_wallet, - recipient, - Amount::from_sat(fee_rate.into()) + &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 + new_network_msg: message.clone(), }) } /// This is what we call to confirm as a receiver #[wasm_bindgen] pub fn create_confirmation_transaction( - message: CachedMessage, + message_id: u32, fee_rate: u32, ) -> ApiResult { - if message.sender.is_none() || message.confirmed_by.is_none() { - return Err(ApiError { message: "Invalid network message".to_owned() }); + 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.is_testnet() { + if sp_address.get_network() != SpNetwork::Mainnet { sp_wallet = connected_user.try_get_recover()?; } else { sp_wallet = connected_user.try_get_main()?; @@ -746,26 +787,38 @@ pub fn create_confirmation_transaction( nb_outputs: 1, }; + let commited_in = message.commited_in.clone().unwrap(); + let signed_psbt = create_transaction_spend_outpoint( - &message.confirmed_by.unwrap(), - sp_wallet, - recipient, - Amount::from_sat(fee_rate.into()) + &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 + new_network_msg: message.clone(), }) } #[wasm_bindgen] pub fn create_notification_transaction( address: String, - commitment: Option, + message: UnknownMessage, fee_rate: u32, ) -> ApiResult { let sp_address: SilentPaymentAddress = address.as_str().try_into()?; @@ -773,7 +826,7 @@ pub fn create_notification_transaction( let connected_user = lock_connected_user()?; let sp_wallet: &SpWallet; - if sp_address.is_testnet() { + if sp_address.get_network() != SpNetwork::Mainnet { sp_wallet = connected_user.try_get_recover()?; } else { sp_wallet = connected_user.try_get_main()?; @@ -785,42 +838,47 @@ pub fn create_notification_transaction( 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, - commitment.as_deref(), + 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 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_point = + sp_utils::sending::calculate_shared_point(&sp_address.get_scan_key(), &partial_secret); - let shared_secret = AnkSharedSecret::new(shared_point); + 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(); + 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::default(); - new_msg.commitment = commitment; - new_msg.commited_in = Some(OutPoint { txid: final_tx.txid(), vout: recipients_vouts[0] as u32 }); + 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()); @@ -908,11 +966,11 @@ 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]; + 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.recipient = Some(sp_address); cached_msg.commitment = Some(commitment.to_lower_hex_string()); cached_msg.status = CachedMessageStatus::FaucetWaiting; lock_messages()?.push(cached_msg.clone()); diff --git a/src/services.ts b/src/services.ts index 7ad857d..d02fb70 100644 --- a/src/services.ts +++ b/src/services.ts @@ -1,4 +1,4 @@ -import { createUserReturn, User, Process, createTransactionReturn, parse_network_msg, outputs_list, FaucetMessage, AnkFlag, NewTxMessage, encryptWithNewKeyResult, AnkSharedSecret, CachedMessage } from '../dist/pkg/sdk_client'; +import { createUserReturn, User, Process, createTransactionReturn, parse_network_msg, outputs_list, FaucetMessage, AnkFlag, NewTxMessage, encryptWithNewKeyResult, AnkSharedSecret, CachedMessage, UnknownMessage } from '../dist/pkg/sdk_client'; import IndexedDB from './database' import { WebSocketClient } from './websockets'; @@ -105,36 +105,24 @@ class Services { const recipientSpAddress = spAddressElement.value; const message = messageElement.value; - const msg_payload = JSON.stringify({sender: this.sp_address, message: message}); + const msg_payload: UnknownMessage = {sender: this.sp_address!, message: message}; let notificationInfo = await services.notify_address_for_message(recipientSpAddress, msg_payload); if (notificationInfo) { let networkMsg = notificationInfo.new_network_msg; - let shared_secret = ''; - if (networkMsg.shared_secret) { - shared_secret = networkMsg.shared_secret; - } else { - throw 'no shared_secret'; - } - console.info('Successfully sent notification transaction'); + console.debug(networkMsg); const connection = await services.pickWebsocketConnectionRandom(); const flag: AnkFlag = "Unknown"; - // encrypt the message(s) - // TODO we'd rather do that in the wasm as part of notify_address_for_message 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 services.updateMessages(updated_msg); - connection?.sendMessage(flag, cipher); + // send message (transaction in envelope) + await services.updateMessages(networkMsg); + connection?.sendMessage(flag, networkMsg.ciphertext!); } catch (error) { throw error; } // add peers list // add processes list - // send message (transaction in envelope) } } @@ -820,6 +808,41 @@ class Services { } } + 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 + } + 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(); @@ -841,7 +864,7 @@ class Services { let notificationInfo: createTransactionReturn; try { const feeRate = 1; - notificationInfo = services.sdkClient.answer_confirmation_transaction(msg, feeRate); + notificationInfo = services.sdkClient.create_confirmation_transaction(msg.id, feeRate); } catch (error) { throw new Error(`Failed to create confirmation transaction: ${error}`); } @@ -851,10 +874,11 @@ class Services { 'tweak_data': 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: string): Promise { + public async notify_address_for_message(sp_address: string, message: UnknownMessage): Promise { const services = await Services.getInstance(); const connection = await services.pickWebsocketConnectionRandom(); if (!connection) { @@ -863,62 +887,19 @@ class Services { try { const feeRate = 1; - const commitment = services.sdkClient.create_commitment(message); - let notificationInfo: createTransactionReturn = services.sdkClient.create_notification_transaction(sp_address, commitment, feeRate); + 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 } connection.sendMessage(flag, JSON.stringify(newTxMsg)); + console.info('Successfully sent notification transaction'); return notificationInfo; } catch (error) { throw 'Failed to create notification transaction:", error'; } } - - // public async encryptData(data: string, sharedSecret: Record): Promise> { - // const services = await Services.getInstance(); - // let msg_cipher: encryptWithNewKeyResult; - // try { - // msg_cipher = services.sdkClient.encrypt_with_new_key(data); - // } catch (error) { - // throw error; - // } - - // let res = new Map(); - // for (const [recipient, secret] of Object.entries(sharedSecret)) { - // try { - // const key = secret.secret; - // const encryptedKey: string = await services.sdkClient.encrypt_with_key(msg_cipher.key, key); - // res.set(recipient, encryptedKey); - // } catch (error) { - // throw new Error(`Failed to encrypt key for recipient ${recipient}: ${error}`); - // } - // } - - // return res; - // } - public async encryptData(data: string, key: string): Promise { - const services = await Services.getInstance(); - - try { - let res: string = services.sdkClient.encrypt_with_key(data, key); - return res; - } catch (error) { - throw error; - } - } - - public async decryptData(cipher: string, key: string): Promise { - const services = await Services.getInstance(); - try { - let res = services.sdkClient.try_decrypt_with_key(cipher, key); - return res; - } catch (error) { - throw error; - } - } } export default Services; diff --git a/src/websockets.ts b/src/websockets.ts index 67d8434..6cc35c5 100644 --- a/src/websockets.ts +++ b/src/websockets.ts @@ -34,7 +34,7 @@ class WebSocketClient { 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.removeMessage(res.id); + await services.updateMessages(res); await services.updateOwnedOutputsForUser(); } else if (res.status === 'TxWaitingCipher') { // we received a tx but we don't have the cipher @@ -47,6 +47,7 @@ class WebSocketClient { 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 @@ -54,7 +55,17 @@ class WebSocketClient { 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'); } @@ -88,7 +99,7 @@ class WebSocketClient { // 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); } } From cca4e742c055ff4da7e0f20886b5a30399e726f6 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Mon, 27 May 2024 12:14:53 +0200 Subject: [PATCH 34/40] websocket check for error message --- src/websockets.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/websockets.ts b/src/websockets.ts index 6cc35c5..04d47ef 100644 --- a/src/websockets.ts +++ b/src/websockets.ts @@ -31,7 +31,11 @@ class WebSocketClient { // 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') { + if (res.status === 'Error') { + if (res.error) { + console.error(res.error); + } + } else 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); From b875b64a0a9cc44330d637f6181d2a536354d050 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Mon, 27 May 2024 12:17:39 +0200 Subject: [PATCH 35/40] adapt api to new AnkMsg types --- crates/sp_client/src/api.rs | 58 ++++++++++++++++++++----------------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/crates/sp_client/src/api.rs b/crates/sp_client/src/api.rs index d1edc07..19c49ba 100644 --- a/crates/sp_client/src/api.rs +++ b/crates/sp_client/src/api.rs @@ -36,7 +36,7 @@ use wasm_bindgen::prelude::*; use sdk_common::network::{ self, AnkFlag, AnkNetworkMsg, CachedMessage, CachedMessageStatus, FaucetMessage, NewTxMessage, - UnknownMessage, + CipherMessage, }; use sdk_common::silentpayments::{ create_transaction, create_transaction_for_address_with_shared_secret, @@ -49,7 +49,7 @@ use sp_client::spclient::{ use sp_client::spclient::{SpWallet, SpendKey}; use crate::user::{lock_connected_user, User, UserWallets, CONNECTED_USER}; -use crate::{images, lock_messages}; +use crate::{images, lock_messages, CACHEDMESSAGES}; use crate::process::Process; @@ -481,9 +481,9 @@ fn handle_recover_transaction( 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); + let cipher_msg: CipherMessage = serde_json::from_slice(&plaintext)?; + 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; @@ -529,18 +529,6 @@ fn process_transaction( ) -> anyhow::Result { let tx = deserialize::(&Vec::from_hex(&tx_hex)?)?; - // 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()?; @@ -571,12 +559,26 @@ fn process_transaction( 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(), @@ -590,12 +592,14 @@ pub fn parse_network_msg(raw: String, fee_rate: u32) -> ApiResult )?; return Ok(network_msg); } - AnkFlag::Faucet => unimplemented!(), - AnkFlag::Error => { - let error_msg = CachedMessage::new_error(ank_msg.content); - return Ok(error_msg); - } - AnkFlag::Unknown => { + 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('\"'))?; @@ -609,9 +613,9 @@ pub fn parse_network_msg(raw: String, fee_rate: u32) -> ApiResult 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); + 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()); @@ -818,7 +822,7 @@ pub fn create_confirmation_transaction( #[wasm_bindgen] pub fn create_notification_transaction( address: String, - message: UnknownMessage, + message: CipherMessage, fee_rate: u32, ) -> ApiResult { let sp_address: SilentPaymentAddress = address.as_str().try_into()?; From 184d4af31fc3ebfdbd76d3bdba3e00275cec997f Mon Sep 17 00:00:00 2001 From: Sosthene Date: Mon, 27 May 2024 14:41:33 +0200 Subject: [PATCH 36/40] [bug] false negative when decrypting message --- crates/sp_client/src/api.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/crates/sp_client/src/api.rs b/crates/sp_client/src/api.rs index 19c49ba..c2ea56e 100644 --- a/crates/sp_client/src/api.rs +++ b/crates/sp_client/src/api.rs @@ -463,25 +463,27 @@ fn handle_recover_transaction( 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; } - m.try_decrypt_with_shared_secret(shared_secret.to_byte_array()) - .is_ok() + 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); - - let plaintext = message - .try_decrypt_with_shared_secret(shared_secret.to_byte_array()) - .unwrap(); - let cipher_msg: CipherMessage = serde_json::from_slice(&plaintext)?; message.plaintext = Some(cipher_msg.message); message.sender = Some(cipher_msg.sender); message.recipient = Some(sp_wallet.get_client().get_receiving_address()); From 1f689d33c38afd0331ab79b28132890a496f9750 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Mon, 27 May 2024 14:42:32 +0200 Subject: [PATCH 37/40] conform service.ts to new message format --- src/services.ts | 18 +++++++++++------- src/websockets.ts | 6 +----- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/services.ts b/src/services.ts index d02fb70..81298d3 100644 --- a/src/services.ts +++ b/src/services.ts @@ -1,4 +1,4 @@ -import { createUserReturn, User, Process, createTransactionReturn, parse_network_msg, outputs_list, FaucetMessage, AnkFlag, NewTxMessage, encryptWithNewKeyResult, AnkSharedSecret, CachedMessage, UnknownMessage } 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'; @@ -105,7 +105,7 @@ class Services { const recipientSpAddress = spAddressElement.value; const message = messageElement.value; - const msg_payload: UnknownMessage = {sender: this.sp_address!, message: message}; + 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) { @@ -113,7 +113,7 @@ class Services { console.debug(networkMsg); const connection = await services.pickWebsocketConnectionRandom(); - const flag: AnkFlag = "Unknown"; + const flag: AnkFlag = 'Cipher'; try { // send message (transaction in envelope) await services.updateMessages(networkMsg); @@ -763,6 +763,7 @@ class Services { let faucetMsg: FaucetMessage = { sp_address: cachedMsg.recipient, commitment: cachedMsg.commitment, + error: null, } connection.sendMessage(flag, JSON.stringify(faucetMsg)); } @@ -836,7 +837,8 @@ class Services { const flag: AnkFlag = "NewTx"; const newTxMsg: NewTxMessage = { 'transaction': notificationInfo.transaction, - 'tweak_data': null + 'tweak_data': null, + 'error': null, } connection.sendMessage(flag, JSON.stringify(newTxMsg)); await services.updateMessages(notificationInfo.new_network_msg); @@ -871,14 +873,15 @@ class Services { const flag: AnkFlag = "NewTx"; const newTxMsg: NewTxMessage = { 'transaction': notificationInfo.transaction, - 'tweak_data': null + '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: UnknownMessage): Promise { + public async notify_address_for_message(sp_address: string, message: CipherMessage): Promise { const services = await Services.getInstance(); const connection = await services.pickWebsocketConnectionRandom(); if (!connection) { @@ -891,7 +894,8 @@ class Services { const flag: AnkFlag = "NewTx"; const newTxMsg: NewTxMessage = { 'transaction': notificationInfo.transaction, - 'tweak_data': null + 'tweak_data': null, + 'error': null, } connection.sendMessage(flag, JSON.stringify(newTxMsg)); console.info('Successfully sent notification transaction'); diff --git a/src/websockets.ts b/src/websockets.ts index 04d47ef..6cc35c5 100644 --- a/src/websockets.ts +++ b/src/websockets.ts @@ -31,11 +31,7 @@ class WebSocketClient { // 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 === 'Error') { - if (res.error) { - console.error(res.error); - } - } else if (res.status === 'FaucetComplete') { + 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); From de133cc0a3ec71e281709fa3a0908d162793cd1a Mon Sep 17 00:00:00 2001 From: Sosthene Date: Mon, 27 May 2024 15:59:29 +0200 Subject: [PATCH 38/40] [experimental] identify transaction we sent according to commitment value --- crates/sp_client/src/api.rs | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/crates/sp_client/src/api.rs b/crates/sp_client/src/api.rs index c2ea56e..9a5d7c1 100644 --- a/crates/sp_client/src/api.rs +++ b/crates/sp_client/src/api.rs @@ -508,9 +508,33 @@ fn handle_recover_transaction( } else { // 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| m.commitment.as_ref() == Some(&commitment_str)) + .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 { From 7da19aa28c5570c2017c56310c33c5c8f0c70444 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Tue, 28 May 2024 11:31:18 +0200 Subject: [PATCH 39/40] Import sp_client through sdk_common --- crates/sp_client/Cargo.toml | 2 - crates/sp_client/src/api.rs | 139 ++++++++++++++++++-------------- crates/sp_client/src/images.rs | 2 +- crates/sp_client/src/process.rs | 2 +- crates/sp_client/src/user.rs | 22 ++--- 5 files changed, 91 insertions(+), 76 deletions(-) diff --git a/crates/sp_client/Cargo.toml b/crates/sp_client/Cargo.toml index 264bd2f..3999e06 100644 --- a/crates/sp_client/Cargo.toml +++ b/crates/sp_client/Cargo.toml @@ -8,8 +8,6 @@ name = "sdk_client" crate-type = ["cdylib"] [dependencies] -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 9a5d7c1..16ddacc 100644 --- a/crates/sp_client/src/api.rs +++ b/crates/sp_client/src/api.rs @@ -14,39 +14,41 @@ use anyhow::Error as AnyhowError; 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_client::bitcoin::blockdata::fee_rate; -use sp_client::bitcoin::consensus::{deserialize, serialize}; -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, Network, OutPoint, Psbt, Transaction, Txid}; -use sp_client::silentpayments::utils as sp_utils; -use sp_client::silentpayments::{Error as SpError, Network as SpNetwork}; +use sdk_common::sp_client::silentpayments::sending::SilentPaymentAddress; use serde::{Deserialize, Serialize}; -use sp_client::silentpayments::sending::SilentPaymentAddress; use tsify::Tsify; use wasm_bindgen::convert::FromWasmAbi; use wasm_bindgen::prelude::*; use sdk_common::network::{ - self, AnkFlag, AnkNetworkMsg, CachedMessage, CachedMessageStatus, FaucetMessage, NewTxMessage, - CipherMessage, + 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 sp_client::spclient::{ +use sdk_common::sp_client::spclient::{ derive_keys_from_seed, OutputList, OutputSpendStatus, OwnedOutput, Recipient, SpClient, }; -use sp_client::spclient::{SpWallet, SpendKey}; +use sdk_common::sp_client::spclient::{SpWallet, SpendKey}; use crate::user::{lock_connected_user, User, UserWallets, CONNECTED_USER}; use crate::{images, lock_messages, CACHEDMESSAGES}; @@ -102,32 +104,32 @@ impl From for ApiError { } } -impl From for ApiError { - fn from(value: sp_client::bitcoin::psbt::PsbtParseError) -> 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: sp_client::bitcoin::psbt::ExtractTxError) -> Self { +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: sp_client::bitcoin::secp256k1::Error) -> Self { +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: sp_client::bitcoin::consensus::encode::Error) -> Self { +impl From for ApiError { + fn from(value: sdk_common::sp_client::bitcoin::consensus::encode::Error) -> Self { ApiError { message: value.to_string(), } @@ -423,13 +425,10 @@ fn handle_recover_transaction( // 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) - }) - { + 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; @@ -511,31 +510,40 @@ fn handle_recover_transaction( // 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 + 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( @@ -624,7 +632,7 @@ pub fn parse_network_msg(raw: String, fee_rate: u32) -> ApiResult 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()?; @@ -738,7 +746,9 @@ pub fn answer_confirmation_transaction( message = m; } else { - return Err(ApiError { message: format!("Can't find message for id {}", message_id) }); + return Err(ApiError { + message: format!("Can't find message for id {}", message_id), + }); } let sp_address: SilentPaymentAddress = @@ -798,7 +808,9 @@ pub fn create_confirmation_transaction( message = m; } else { - return Err(ApiError { message: format!("Can't find message for id {}", message_id) }); + 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()?; @@ -836,7 +848,10 @@ pub fn create_confirmation_transaction( let final_tx = signed_psbt.extract_tx()?; - message.confirmed_by = Some(OutPoint { txid: final_tx.txid(), vout: recipients_vouts[0] as u32 }); + message.confirmed_by = Some(OutPoint { + txid: final_tx.txid(), + vout: recipients_vouts[0] as u32, + }); Ok(createTransactionReturn { txid: final_tx.txid().to_string(), @@ -891,7 +906,10 @@ pub fn create_notification_transaction( 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())?; + 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)?; @@ -913,7 +931,6 @@ pub fn create_notification_transaction( new_msg.recipient = Some(address); new_msg.sender = Some(sp_wallet.get_client().get_receiving_address()); new_msg.status = CachedMessageStatus::SentWaitingConfirmation; - // plaintext and ciphertext to be added later when sending the encrypted message lock_messages()?.push(new_msg.clone()); Ok(createTransactionReturn { diff --git a/crates/sp_client/src/images.rs b/crates/sp_client/src/images.rs index f0e0698..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_client::bitcoin::secp256k1::SecretKey; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BackUpImage(Vec); diff --git a/crates/sp_client/src/process.rs b/crates/sp_client/src/process.rs index 36e1c47..25eed24 100644 --- a/crates/sp_client/src/process.rs +++ b/crates/sp_client/src/process.rs @@ -1,8 +1,8 @@ use std::fmt::DebugStruct; +use sdk_common::sp_client::silentpayments::sending::SilentPaymentAddress; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; -use sp_client::silentpayments::sending::SilentPaymentAddress; use tsify::Tsify; use wasm_bindgen::prelude::*; diff --git a/crates/sp_client/src/user.rs b/crates/sp_client/src/user.rs index 5038554..f7af595 100644 --- a/crates/sp_client/src/user.rs +++ b/crates/sp_client/src/user.rs @@ -1,13 +1,13 @@ 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_client::bitcoin::hashes::Hash; -use sp_client::bitcoin::hashes::HashEngine; -use sp_client::bitcoin::hex::{DisplayHex, FromHex}; -use sp_client::bitcoin::secp256k1::SecretKey; -use sp_client::bitcoin::secp256k1::ThirtyTwoByteHash; -use sp_client::spclient::SpClient; use tsify::Tsify; use wasm_bindgen::prelude::*; @@ -18,11 +18,11 @@ use std::io::{Cursor, Read, Write}; use std::str::FromStr; use std::sync::{Mutex, MutexGuard, OnceLock}; -use sp_client::bitcoin::secp256k1::constants::SECRET_KEY_SIZE; -use sp_client::silentpayments::bitcoin_hashes::sha256; -use sp_client::silentpayments::sending::SilentPaymentAddress; -use sp_client::spclient::SpendKey; -use sp_client::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::peers::Peer; use crate::user; From 834e6b3749f8c118230869290675dfbb4566c5c7 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Tue, 28 May 2024 11:51:30 +0200 Subject: [PATCH 40/40] rm dead code --- crates/sp_client/src/Prd_list.rs | 73 -------------------------------- crates/sp_client/src/lib.rs | 3 -- 2 files changed, 76 deletions(-) delete mode 100644 crates/sp_client/src/Prd_list.rs diff --git a/crates/sp_client/src/Prd_list.rs b/crates/sp_client/src/Prd_list.rs deleted file mode 100644 index 9ed63d0..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_client::bitcoin::PublicKey; -//use sp_client::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/lib.rs b/crates/sp_client/src/lib.rs index 3643833..1168492 100644 --- a/crates/sp_client/src/lib.rs +++ b/crates/sp_client/src/lib.rs @@ -3,14 +3,11 @@ use anyhow::Error; use sdk_common::crypto::AnkSharedSecret; use sdk_common::network::CachedMessage; use serde::{Deserialize, Serialize}; -use sp_client::bitcoin::{OutPoint, Txid}; -use sp_client::silentpayments::sending::SilentPaymentAddress; use std::collections::{HashMap, HashSet}; use std::fmt::Debug; use std::sync::{Mutex, MutexGuard, OnceLock}; use tsify::Tsify; -mod Prd_list; pub mod api; mod images; mod peers;