use std::any::Any; use std::borrow::Borrow; use std::collections::{HashMap, HashSet}; use std::io::Write; use std::ops::Index; use std::str::FromStr; use std::string::FromUtf8Error; use std::sync::{Mutex, OnceLock, PoisonError}; use std::time::{Duration, Instant}; use sdk_common::log::{debug, warn}; use rand::{thread_rng, Fill, Rng, RngCore}; use anyhow::Error as AnyhowError; use sdk_common::crypto::{ AeadCore, Aes256Decryption, Aes256Encryption, Aes256Gcm, AnkSharedSecret, KeyInit, Purpose, }; use sdk_common::process::Process; use sdk_common::signature; 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::{Parity, Secp256k1}; use sdk_common::sp_client::bitcoin::network::ParseNetworkError; use sdk_common::sp_client::bitcoin::p2p::message::NetworkMessage; use sdk_common::sp_client::bitcoin::psbt::raw; use sdk_common::sp_client::bitcoin::secp256k1::ecdh::shared_secret_point; use sdk_common::sp_client::bitcoin::secp256k1::{PublicKey, Scalar, SecretKey}; use sdk_common::sp_client::bitcoin::transaction::ParseOutPointError; use sdk_common::sp_client::bitcoin::{ Amount, Network, OutPoint, Psbt, Transaction, Txid, XOnlyPublicKey, }; use sdk_common::sp_client::constants::{ DUST_THRESHOLD, PSBT_SP_ADDRESS_KEY, PSBT_SP_PREFIX, PSBT_SP_SUBTYPE, }; use sdk_common::sp_client::silentpayments::utils as sp_utils; use sdk_common::sp_client::silentpayments::{ utils::{Network as SpNetwork, SilentPaymentAddress}, Error as SpError, }; use sdk_common::uuid::Uuid; use serde_json::{Error as SerdeJsonError, Map, Value}; use serde::{Deserialize, Serialize}; use tsify::{JsValueSerdeExt, Tsify}; use wasm_bindgen::convert::{FromWasmAbi, VectorFromWasmAbi}; use wasm_bindgen::prelude::*; use sdk_common::network::{ self, AnkFlag, CachedMessage, CachedMessageStatus, Envelope, FaucetMessage, NewTxMessage}; use sdk_common::pcd::{AnkPcdHash, Member, Pcd, RoleDefinition, ValidationRule}; use sdk_common::prd::{AnkPrdHash, Prd, PrdType}; use sdk_common::silentpayments::{create_transaction, map_outputs_to_sp_address}; use sdk_common::sp_client::spclient::{ derive_keys_from_seed, OutputList, OutputSpendStatus, OwnedOutput, Recipient, SpClient, }; use sdk_common::sp_client::spclient::{SpWallet, SpendKey}; use sdk_common::device::Device; use crate::user::{lock_local_device, set_new_device, LOCAL_DEVICE}; use crate::{lock_messages, lock_processes, ProcessState, ProcessStatus, RelevantProcess, CACHEDMESSAGES, CACHEDPROCESSES}; use crate::wallet::{generate_sp_wallet, lock_freezed_utxos}; pub type ApiResult = Result; const IS_TESTNET: bool = true; const DEFAULT_AMOUNT: Amount = Amount::from_sat(1000); #[derive(Debug, PartialEq, Eq)] pub struct ApiError { pub message: String, } impl From for ApiError { fn from(value: AnyhowError) -> Self { ApiError { message: value.to_string(), } } } impl From for ApiError { fn from(value: SpError) -> Self { ApiError { message: value.to_string(), } } } impl From for ApiError { fn from(value: SerdeJsonError) -> Self { ApiError { message: value.to_string(), } } } impl From for ApiError { fn from(value: HexToBytesError) -> Self { ApiError { message: value.to_string(), } } } impl From for ApiError { fn from(value: HexToArrayError) -> Self { ApiError { message: value.to_string(), } } } impl From for ApiError { fn from(value: sdk_common::sp_client::bitcoin::psbt::PsbtParseError) -> Self { ApiError { message: value.to_string(), } } } impl From for ApiError { fn from(value: sdk_common::sp_client::bitcoin::psbt::ExtractTxError) -> Self { ApiError { message: value.to_string(), } } } impl From for ApiError { fn from(value: sdk_common::sp_client::bitcoin::secp256k1::Error) -> Self { ApiError { message: value.to_string(), } } } impl From for ApiError { fn from(value: sdk_common::sp_client::bitcoin::consensus::encode::Error) -> Self { ApiError { message: value.to_string(), } } } impl From for ApiError { fn from(value: FromUtf8Error) -> Self { ApiError { message: value.to_string(), } } } impl From for ApiError { fn from(value: ParseNetworkError) -> Self { ApiError { message: value.to_string(), } } } impl From for ApiError { fn from(value: ParseOutPointError) -> Self { ApiError { message: value.to_string(), } } } impl Into for ApiError { fn into(self) -> JsValue { JsValue::from_str(&self.message) } } #[wasm_bindgen] pub fn setup() { wasm_logger::init(wasm_logger::Config::default()); } #[wasm_bindgen] pub fn get_address() -> ApiResult { let local_device = lock_local_device()?; Ok(local_device .get_wallet() .get_client() .get_receiving_address()) } #[wasm_bindgen] pub fn restore_device(device_str: String) -> ApiResult<()> { let device: Device = serde_json::from_str(&device_str)?; let mut local_device = lock_local_device()?; *local_device = device; Ok(()) } #[wasm_bindgen] pub fn create_device_from_sp_wallet(sp_wallet: String) -> ApiResult { let sp_wallet: SpWallet = serde_json::from_str(&sp_wallet)?; let our_address = set_new_device(sp_wallet)?; Ok(our_address) } #[wasm_bindgen] pub fn create_new_device(birthday: u32, network_str: String) -> ApiResult { let sp_wallet = generate_sp_wallet(None, Network::from_core_arg(&network_str)?)?; let our_address = set_new_device(sp_wallet)?; Ok(our_address) } #[wasm_bindgen] pub fn pair_device(uuid: String, mut sp_addresses: Vec) -> ApiResult<()> { let mut local_device = lock_local_device()?; if local_device.is_linked() { return Err(ApiError { message: "Already paired".to_owned(), }); } sp_addresses.push(local_device.get_wallet().get_client().get_receiving_address()); local_device.pair( Uuid::parse_str(&uuid).unwrap(), Member::new(sp_addresses.into_iter().map(|a| TryInto::::try_into(a).unwrap()).collect())? ); Ok(()) } #[derive(Debug, Tsify, Serialize, Deserialize)] #[tsify(from_wasm_abi, into_wasm_abi)] #[allow(non_camel_case_types)] pub struct outputs_list(OutputList); impl outputs_list { fn as_inner(&self) -> &OutputList { &self.0 } } #[wasm_bindgen] pub fn logout() -> ApiResult<()> { unimplemented!(); } #[wasm_bindgen] pub fn dump_wallet() -> ApiResult { let device = lock_local_device()?; Ok(serde_json::to_string(device.get_wallet()).unwrap()) } #[wasm_bindgen] pub fn reset_message_cache() -> ApiResult<()> { let mut cached_msg = lock_messages()?; *cached_msg = vec![]; debug_assert!(cached_msg.is_empty()); Ok(()) } #[wasm_bindgen] pub fn set_message_cache(msg_cache: Vec) -> ApiResult<()> { let mut cached_msg = lock_messages()?; if !cached_msg.is_empty() { return Err(ApiError { message: "Message cache not empty".to_owned(), }); } let new_cache: Result, serde_json::Error> = msg_cache.iter().map(|m| serde_json::from_str(m)).collect(); *cached_msg = new_cache?; Ok(()) } #[wasm_bindgen] pub fn dump_message_cache() -> ApiResult> { let cached_msg = lock_messages()?; let res: Vec = cached_msg .iter() .map(|m| serde_json::to_string(m).unwrap()) .collect(); Ok(res) } #[wasm_bindgen] pub fn dump_device() -> ApiResult { let local_device = lock_local_device()?; Ok(serde_json::to_string(&local_device.clone())?) } #[wasm_bindgen] pub fn reset_device() -> ApiResult<()> { let mut device = lock_local_device()?; *device = Device::default(); reset_message_cache()?; Ok(()) } fn handle_transaction( updated: HashMap, tx: &Transaction, tweak_data: PublicKey, ) -> anyhow::Result { let device = lock_local_device()?; let sp_wallet = device.get_wallet(); let op_return = tx.output.iter().find(|o| o.script_pubkey.is_op_return()); let mut commitment = [0u8; 32]; if op_return.is_some() { commitment.copy_from_slice(&op_return.unwrap().script_pubkey.as_bytes()[2..]); }; let commitment_str = commitment.to_lower_hex_string(); // If we got updates from a transaction, it means that it creates an output to us, spend an output we owned, or both // Basically a transaction that destroyed utxo is a transaction we sent. let utxo_destroyed: HashMap<&OutPoint, &OwnedOutput> = updated .iter() .filter(|(outpoint, output)| output.spend_status != OutputSpendStatus::Unspent) .collect(); let utxo_created: HashMap<&OutPoint, &OwnedOutput> = updated .iter() .filter(|(outpoint, output)| output.spend_status == OutputSpendStatus::Unspent) .collect(); let mut messages = lock_messages()?; // empty utxo_destroyed means we received this transaction if utxo_destroyed.is_empty() { let shared_point = sp_utils::receiving::calculate_ecdh_shared_secret( &tweak_data, &sp_wallet.get_client().get_scan_key(), ); let shared_secret = AnkSharedSecret::new(shared_point); let mut plaintext: Vec = vec![]; if let Some(message) = messages.iter_mut().find(|m| { if m.status != CachedMessageStatus::CipherWaitingTx { return false; } let res = m.try_decrypt_with_shared_secret(shared_secret.to_byte_array()); if res.is_ok() { plaintext = res.unwrap(); return true; } else { return false; } }) { let (outpoint, output) = utxo_created.into_iter().next().unwrap(); let prd = Prd::extract_from_message(&plaintext, commitment)?; message.prd = Some(prd.to_string()); message.shared_secrets.push(shared_secret.to_byte_array().to_lower_hex_string()); message.commitment = Some(commitment_str); message.sender = Some(serde_json::from_str(&prd.sender)?); message.status = CachedMessageStatus::Opened; 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.commitment = Some(commitment.to_lower_hex_string()); new_msg.shared_secrets.push(shared_secret.to_byte_array().to_lower_hex_string()); new_msg.status = CachedMessageStatus::TxWaitingPrd; messages.push(new_msg.clone()); return Ok(new_msg.clone()); } } else { // We're sender of the transaction, do nothing return Ok(CachedMessage::new()); } } /// 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, ) -> anyhow::Result> { let tx = deserialize::(&Vec::from_hex(&tx_hex)?)?; let tweak_data = PublicKey::from_str(&tweak_data_hex)?; let updated: HashMap; { let mut device = lock_local_device()?; let wallet = device.get_mut_wallet(); updated = wallet.update_wallet_with_transaction(&tx, blockheight, tweak_data)?; } if updated.len() > 0 { let updated_msg = handle_transaction(updated, &tx, tweak_data)?; return Ok(Some(updated_msg)); } Ok(None) } #[wasm_bindgen] pub fn parse_new_tx(new_tx_msg: String, block_height: u32, fee_rate: u32) -> ApiResult> { let new_tx: NewTxMessage = serde_json::from_str(&new_tx_msg)?; if let Some(error) = new_tx.error { return Err(ApiError { message: format!("NewTx returned with an error: {}", error), }); } if new_tx.tweak_data.is_none() { return Err(ApiError { message: "Missing tweak_data".to_owned(), }); } let msg = process_transaction(new_tx.transaction, block_height, new_tx.tweak_data.unwrap())?; Ok(msg) } #[wasm_bindgen] pub fn parse_cipher(cipher_msg: String, fee_rate: u32) -> ApiResult { // 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(&cipher_msg.trim_matches('\"'))?; let mut plain = vec![]; if let Some(message) = messages.iter_mut().find(|m| match m.status { CachedMessageStatus::TxWaitingPrd | CachedMessageStatus::Opened => { if let Ok(m) = m.try_decrypt_message(cipher.clone()) { plain = m; return true; } else { return false } } _ => return false, }) { if message.status == CachedMessageStatus::TxWaitingPrd { // debug!("Found message {}", String::from_utf8(plain.clone())?); let mut commitment = [0u8; 32]; commitment.copy_from_slice(&Vec::from_hex(message.commitment.as_ref().unwrap())?); let prd = Prd::extract_from_message(&plain, commitment)?; message.prd = Some(prd.to_string()); message.sender = Some(serde_json::from_str(&prd.sender)?); message.status = CachedMessageStatus::Opened; } else { // debug!("Found message {}", String::from_utf8(plain.clone())?); // we're receiving a pcd for a prd we already have let mut pcd = Value::from_str(&String::from_utf8(plain)?)?; // check that the hash of the pcd is the same than commited in the prd let pcd_commitment = AnkPcdHash::from_value(&pcd); let prd: Prd = serde_json::from_str(message.prd.as_ref().unwrap()).unwrap(); if pcd_commitment.to_string() != prd.pcd_commitment { return Err(ApiError { message: format!("Pcd doesn't match commitment: expected {:?}\ngot {}", prd.pcd_commitment, pcd_commitment), }); } message.pcd = Some(pcd.clone()); // Now we decrypt all we can from the pcd pcd.decrypt_fields(&prd.keys)?; // we take a few mandatory informations let process = pcd["process"].take(); let uuid = Uuid::parse_str(&prd.process_uuid).expect("prd can't have an invalid uuid"); // todo check that the uuid in the process we took from pcd is consistent // we complete the process we keep in storage let mut processes = lock_processes()?; match prd.prd_type { PrdType::Init => { let relevant_process = RelevantProcess { process: serde_json::from_value(process)?, states: vec![ProcessState { commited_in: OutPoint::null(), // At this point process is not commited yet encrypted_pcd: message.pcd.clone().unwrap(), keys: prd.keys.clone(), validation_token: vec![], }], current_status: ProcessStatus::Active(message.shared_secrets.clone()) }; processes.insert(uuid, relevant_process); }, _ => unimplemented!() } } 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.cipher.push(cipher_msg); messages.push(new_msg.clone()); return Ok(new_msg); } } #[wasm_bindgen] pub fn get_outputs() -> ApiResult { let device = lock_local_device()?; let outputs = device.get_wallet().get_outputs().clone(); Ok(JsValue::from_serde(&outputs.to_outpoints_list())?) } #[wasm_bindgen] pub fn get_available_amount() -> ApiResult { let device = lock_local_device()?; Ok(device.get_wallet().get_outputs().get_balance().to_sat()) } #[wasm_bindgen] pub fn create_process_from_template(json: String) -> ApiResult { let template_process: Process = serde_json::from_str(&json)?; let mut new_process = Process::new( template_process.html, template_process.style, template_process.script, template_process.init_state, template_process.commited_in ); Ok(new_process) } #[derive(Debug, Tsify, Serialize, Deserialize, Default)] #[tsify(into_wasm_abi, from_wasm_abi)] #[allow(non_camel_case_types)] pub struct createTransactionReturn { pub txid: String, pub transaction: String, pub new_messages: Vec } #[wasm_bindgen] pub fn create_process_init_transaction( mut new_process: Process, fee_rate: u32, ) -> ApiResult { let pcd = new_process.init_state; let roles = pcd.get("roles").ok_or(ApiError { message: "No roles in init_state".to_owned()})?; let roles_map = roles.as_object().ok_or(ApiError { message: "roles is not an object".to_owned()})?.clone(); let mut all_members: HashMap> = HashMap::new(); for (name, role_def) in roles_map { let role: RoleDefinition = serde_json::from_str(&role_def.to_string())?; let fields: Vec = role.validation_rules.iter().flat_map(|rule| rule.fields.clone()).collect(); for member in role.members { if !all_members.contains_key(&member) { all_members.insert(member.clone(), HashSet::new()); } all_members.get_mut(&member).unwrap().extend(fields.clone()); } } let nb_recipients = all_members.len(); if nb_recipients == 0 { return Err(ApiError { message: "Can't create a process with 0 member".to_owned(), }); } let mut recipients: Vec = Vec::with_capacity(nb_recipients*2); // We suppose that will work most of the time // we actually have multiple "recipients" in a technical sense for each social recipient // that's necessary because we don't want to miss a notification because we don't have a device atm for member in all_members.keys() { let addresses = member.get_addresses(); for sp_address in addresses.into_iter() { recipients.push(Recipient { address: sp_address.into(), amount: DEFAULT_AMOUNT, nb_outputs: 1, }); } } let mut fields2keys = Map::new(); let mut fields2cipher = Map::new(); Value::Object(pcd.clone()).encrypt_fields(&mut fields2keys, &mut fields2cipher); new_process.init_state = fields2cipher.clone(); let local_device = lock_local_device()?; let sp_wallet = local_device.get_wallet(); let sender: Member = local_device.to_member().ok_or(ApiError { message: "unpaired device".to_owned() })?; // We first generate the prd with all the keys that we will keep to ourselves let full_prd = Prd::new( PrdType::Init, Uuid::from_str(&new_process.uuid).expect("We can trust process to have a valid uuid"), serde_json::to_string(&sender)?, fields2cipher.clone(), fields2keys.clone() )?; let prd_commitment = full_prd.create_commitment(); let freezed_utxos = lock_freezed_utxos()?; let signed_psbt = create_transaction( &vec![], &freezed_utxos, sp_wallet, recipients, Some(prd_commitment.as_byte_array().to_vec()), Amount::from_sat(fee_rate.into()), None, )?; let sp_address2vouts = map_outputs_to_sp_address(&signed_psbt.to_string())?; let partial_secret = sp_wallet .get_client() .get_partial_secret_from_psbt(&signed_psbt)?; let final_tx = signed_psbt.extract_tx()?; let mut new_messages = vec![]; let mut shared_secrets = vec![]; // This is a bit ugly, but this way we can update the process status for (member, visible_fields) in all_members { let mut prd = full_prd.clone(); prd.filter_keys(visible_fields); let prd_msg = prd.to_network_msg(sp_wallet)?; let mut res = CachedMessage::new(); res.recipient = Some(member.clone()); res.prd = Some(prd_msg.clone()); res.sender = Some(sender.clone()); res.pcd = Some(Value::Object(pcd.clone())); res.commitment = Some(prd_commitment.to_string()); res.status = CachedMessageStatus::Opened; let addresses = member.get_addresses(); for sp_address in addresses.into_iter() { let shared_point = sp_utils::sending::calculate_ecdh_shared_secret( &::try_from(sp_address)?.get_scan_key(), &partial_secret, ); let shared_secret = AnkSharedSecret::new(shared_point).to_byte_array().to_lower_hex_string(); let cipher = encrypt_with_key( prd_msg.clone(), shared_secret.clone(), )?; res.cipher.push(cipher); res.shared_secrets.push(shared_secret.clone()); shared_secrets.push(shared_secret); } new_messages.push(res); } lock_messages()?.extend(new_messages.clone()); let mut processes = lock_processes()?; let init_state = ProcessState { commited_in: OutPoint::null(), encrypted_pcd: Value::Object(fields2cipher), keys: fields2keys, validation_token: vec![] }; // We are initializing a process, so we shouldn't have it in our cache yet processes.insert(Uuid::parse_str(&new_process.uuid).unwrap(), RelevantProcess { process: new_process, states: vec![init_state], current_status: ProcessStatus::Active(shared_secrets) }); Ok(createTransactionReturn { txid: final_tx.txid().to_string(), transaction: serialize(&final_tx).to_lower_hex_string(), new_messages }) } #[derive(Tsify, Serialize, Deserialize)] #[tsify(into_wasm_abi, from_wasm_abi)] #[allow(non_camel_case_types)] pub struct encryptWithNewKeyResult { pub cipher: String, pub key: String, } #[wasm_bindgen] pub fn encrypt_with_key(plaintext: String, key: String) -> ApiResult { let nonce = Aes256Gcm::generate_nonce(&mut rand::thread_rng()); let mut aes_key = [0u8; 32]; aes_key.copy_from_slice(&Vec::from_hex(&key)?); // encrypt let aes_enc = Aes256Encryption::import_key( Purpose::Arbitrary, plaintext.into_bytes(), aes_key, nonce.into(), )?; let cipher = aes_enc.encrypt_with_aes_key()?; Ok(cipher.to_lower_hex_string()) } #[wasm_bindgen] pub fn encrypt_with_new_key(plaintext: String) -> ApiResult { let mut rng = rand::thread_rng(); // generate new key let aes_key = Aes256Gcm::generate_key(&mut rng); let nonce = Aes256Gcm::generate_nonce(&mut rng); // encrypt let aes_enc = Aes256Encryption::import_key( Purpose::Arbitrary, plaintext.into_bytes(), aes_key.into(), nonce.into(), )?; let cipher = aes_enc.encrypt_with_aes_key()?; Ok(encryptWithNewKeyResult { cipher: cipher.to_lower_hex_string(), key: aes_key.to_lower_hex_string(), }) } #[wasm_bindgen] pub fn try_decrypt_with_key(cipher: String, key: String) -> ApiResult { let key_bin = Vec::from_hex(&key)?; if key_bin.len() != 32 { return Err(ApiError { message: "key of invalid lenght".to_owned(), }); } let mut aes_key = [0u8; 32]; aes_key.copy_from_slice(&Vec::from_hex(&key)?); let aes_dec = Aes256Decryption::new(Purpose::Arbitrary, Vec::from_hex(&cipher)?, aes_key)?; let plain = String::from_utf8(aes_dec.decrypt_with_key()?)?; Ok(plain) } #[wasm_bindgen] pub fn create_faucet_msg() -> ApiResult { let sp_address = lock_local_device()? .get_wallet() .get_client() .get_receiving_address(); let faucet_msg = FaucetMessage::new(sp_address.clone()); let network_msg = Envelope::new(AnkFlag::Faucet, &faucet_msg.to_string()); Ok(network_msg.to_string()) }