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, MutexGuard, OnceLock, PoisonError}; use std::time::{Duration, Instant}; use std::u32; use rand::{thread_rng, Fill, Rng, RngCore}; use sdk_common::aes_gcm::aead::generic_array::GenericArray; use sdk_common::aes_gcm::aes::cipher::ArrayLength; use sdk_common::aes_gcm::Nonce; use sdk_common::log::{debug, warn, info}; use anyhow::Context; use anyhow::Error as AnyhowError; use anyhow::Result as AnyhowResult; use sdk_common::aes_gcm::aead::{Aead, Payload}; use sdk_common::crypto::{ encrypt_with_key, AeadCore, Aes256Gcm, AnkSharedSecretHash, KeyInit, AAD, }; use sdk_common::process::{lock_processes, Process, ProcessState}; use sdk_common::signature::{AnkHash, AnkValidationNoHash, AnkValidationYesHash, Proof}; 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::{sha256, sha256t, Hash}; use sdk_common::sp_client::bitcoin::hashes::{FromSliceError, HashEngine}; use sdk_common::sp_client::bitcoin::hex::{ self, 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::{signature, MAX_PRD_PAYLOAD_SIZE}; 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::device::Device; use sdk_common::network::{ self, AnkFlag, CachedMessage, CachedMessageStatus, CommitMessage, Envelope, FaucetMessage, NewTxMessage, }; use sdk_common::pcd::{ compare_maps, AnkPcdHash, AnkPcdTag, 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 crate::user::{lock_local_device, set_new_device, LOCAL_DEVICE}; use crate::wallet::{generate_sp_wallet, lock_freezed_utxos}; use crate::{lock_messages, CACHEDMESSAGES}; #[derive(Debug, PartialEq, Tsify, Serialize, Deserialize, Default)] #[tsify(into_wasm_abi, from_wasm_abi)] #[allow(non_camel_case_types)] pub struct ApiReturn { pub updated_cached_msg: Vec, pub updated_process: Option<(String, Process)>, pub new_tx_to_send: Option, pub ciphers_to_send: Vec, pub commit_to_send: Option, } 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 ApiError { pub fn new(message: String) -> Self { ApiError { message } } } impl From for ApiError { fn from(value: AnyhowError) -> Self { ApiError::new(value.to_string()) } } impl From for ApiError { fn from(value: SpError) -> Self { ApiError::new(value.to_string()) } } impl From for ApiError { fn from(value: FromSliceError) -> Self { ApiError::new(value.to_string()) } } impl From for ApiError { fn from(value: SerdeJsonError) -> Self { ApiError::new(value.to_string()) } } impl From for ApiError { fn from(value: HexToBytesError) -> Self { ApiError::new(value.to_string()) } } impl From for ApiError { fn from(value: HexToArrayError) -> Self { ApiError::new(value.to_string()) } } impl From for ApiError { fn from(value: sdk_common::sp_client::bitcoin::psbt::PsbtParseError) -> Self { ApiError::new(value.to_string()) } } impl From for ApiError { fn from(value: sdk_common::sp_client::bitcoin::psbt::ExtractTxError) -> Self { ApiError::new(value.to_string()) } } impl From for ApiError { fn from(value: sdk_common::sp_client::bitcoin::secp256k1::Error) -> Self { ApiError::new(value.to_string()) } } impl From for ApiError { fn from(value: sdk_common::sp_client::bitcoin::consensus::encode::Error) -> Self { ApiError::new(value.to_string()) } } impl From for ApiError { fn from(value: FromUtf8Error) -> Self { ApiError::new(value.to_string()) } } impl From for ApiError { fn from(value: ParseNetworkError) -> Self { ApiError::new(value.to_string()) } } impl From for ApiError { fn from(value: ParseOutPointError) -> Self { ApiError::new(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(commitment_tx: String, mut sp_addresses: Vec) -> ApiResult<()> { let mut local_device = lock_local_device()?; if local_device.is_linked() { return Err(ApiError::new("Already paired".to_owned())); } sp_addresses.push( local_device .get_wallet() .get_client() .get_receiving_address(), ); local_device.pair( OutPoint::from_str(&commitment_tx)?.txid, Member::new( sp_addresses .into_iter() .map(|a| TryInto::::try_into(a).unwrap()) .collect(), )?, ); Ok(()) } #[wasm_bindgen] pub fn unpair_device() -> ApiResult<()> { let mut local_device = lock_local_device()?; local_device.unpair(); 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 login(previous_login_tx: String, fee_rate: u32) -> ApiResult { // We first create a transaction that spends both pairing tx outputs let previous_tx: Txid = deserialize(&Vec::from_hex(&previous_login_tx)?)?; let device = lock_local_device()?; if !device.is_linked() { return Err(ApiError::new("Device is not linked".to_owned())); } let member = device.to_member().unwrap(); let nb_outputs = member.get_addresses().len(); let other_addresses = device.get_other_addresses(); // We get the pairing process out of cache let commitment_txid = device.get_process_commitment().unwrap(); let commitment_outpoint = OutPoint::new(commitment_txid, 0); let process = lock_processes()?.get(&commitment_outpoint).unwrap().clone(); let state = process.get_latest_state().unwrap().clone(); let mut shared_secrets = Vec::new(); for address in other_addresses { let shared_secret = process.get_shared_secret_for_address(&SilentPaymentAddress::try_from(address)?); if let Some(shared_secret) = shared_secret { shared_secrets.push(shared_secret); } } let mut decrypted_pcd = Map::new(); state .encrypted_pcd .decrypt_fields(&state.keys, &mut decrypted_pcd)?; let pairing_tx = decrypted_pcd.get("pairing_tx").unwrap().as_str().unwrap(); let wallet = device.get_wallet(); let freezed_utxos = lock_freezed_utxos()?; let recipients: Vec = device .to_member() .unwrap() .get_addresses() .iter() .map(|a| Recipient { address: a.clone(), amount: DEFAULT_AMOUNT, nb_outputs: 1, }) .collect(); let mut mandatory_inputs = Vec::new(); for i in 0u32..nb_outputs.try_into().unwrap() { mandatory_inputs.push(OutPoint::new(previous_tx, i)); } let signed_psbt = create_transaction( mandatory_inputs, &freezed_utxos, wallet, recipients, None, Amount::from_sat(fee_rate.into()), None, )?; // We send it in a TxProposal prd let tx_proposal = Prd::new_tx_proposal(commitment_outpoint, member, signed_psbt); debug!("tx_proposal: {:?}", tx_proposal); // We encrypt the prd with the shared_secret for pairing process let prd_msg = tx_proposal.to_network_msg(wallet)?; debug!("prd_msg: {:?}", prd_msg); let mut ciphers = Vec::new(); for shared_secret in shared_secrets { let cipher = encrypt_with_key(shared_secret.as_byte_array(), prd_msg.as_bytes())?; ciphers.push(cipher.to_lower_hex_string()); } // We return the cipher Ok(ApiReturn { ciphers_to_send: ciphers, ..Default::default() }) } #[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_process_cache() -> ApiResult<()> { let mut cached_processes = lock_processes()?; *cached_processes = HashMap::new(); debug_assert!(cached_processes.is_empty()); Ok(()) } #[wasm_bindgen] pub fn dump_process_cache() -> ApiResult { let cached_processes = lock_processes()?; let serializable_cache = cached_processes .iter() .map(|(outpoint, process)| { ( outpoint.to_string(), serde_json::to_value(&process).unwrap(), ) }) .collect::>(); let json_string = serde_json::to_string(&serializable_cache) .map_err(|e| ApiError::new(format!("Failed to serialize process cache: {}", e)))?; Ok(json_string) } #[wasm_bindgen] pub fn set_process_cache(processes: String) -> ApiResult<()> { let processes: Map = serde_json::from_str(&processes)?; let parsed_processes: anyhow::Result> = processes.into_iter() .map(|(key, value)| { Ok((OutPoint::from_str(&key)?, serde_json::from_value(value)?)) }) .collect(); let mut cached_processes = lock_processes()?; *cached_processes = parsed_processes?; Ok(()) } #[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::new("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()?; reset_process_cache()?; Ok(()) } #[wasm_bindgen] pub fn get_txid(transaction: String) -> ApiResult { let tx: Transaction = deserialize(&Vec::from_hex(&transaction)?)?; Ok(tx.txid().to_string()) } fn handle_transaction( updated: HashMap, tx: &Transaction, tweak_data: PublicKey, ) -> AnyhowResult { let b_scan = lock_local_device()?.get_wallet().get_client().get_scan_key(); let op_return: Vec<&sdk_common::sp_client::bitcoin::TxOut> = tx .output .iter() .filter(|o| o.script_pubkey.is_op_return()) .collect(); if op_return.len() != 1 { return Err(AnyhowError::msg( "Transaction must have exactly one op_return output", )); } let commitment = AnkPrdHash::from_slice(&op_return.first().unwrap().script_pubkey.as_bytes()[2..])?; // 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 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, &b_scan, ); let shared_secret = AnkSharedSecretHash::from_shared_point(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; } }) { // Calling this check that the prd we found check with the hashed commitment in transaction // We also check the signed proof that is included in the prd let prd = Prd::extract_from_message_with_commitment(&plaintext, &commitment)?; // for now the previous method doesn't error if proof is missing, // We must define if there are cases where a valid prd doesn't have proof let outpoint = OutPoint::from_str(&prd.root_commitment)?; handle_decrypted_message(plaintext, Some(shared_secret), Some(outpoint)) } else { // store it and wait for the message let mut new_msg = CachedMessage::new(); new_msg.commitment = Some(commitment.as_byte_array().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(ApiReturn { updated_cached_msg: vec![new_msg.clone()], ..Default::default() }); } } else { // We're sender of the transaction, do nothing return Ok(ApiReturn { ..Default::default() }); } } /// If the transaction has anything to do with us, we create/update the relevant process /// 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 { return handle_transaction(updated, &tx, tweak_data); } Err(anyhow::Error::msg("Transaction is not our")) } #[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::new(format!( "NewTx returned with an error: {}", error ))); } if new_tx.tweak_data.is_none() { return Err(ApiError::new("Missing tweak_data".to_owned())); } Ok(process_transaction( new_tx.transaction, block_height, new_tx.tweak_data.unwrap(), )?) } fn try_decrypt_with_processes( cipher: &[u8], processes: MutexGuard>, ) -> Option<(Vec, SilentPaymentAddress, OutPoint)> { let nonce = Nonce::from_slice(&cipher[..12]); for (outpoint, process) in processes.iter() { for (address, secret) in process.get_all_secrets() { let engine = Aes256Gcm::new(&secret.to_byte_array().into()); if let Ok(plain) = engine.decrypt( &nonce, Payload { msg: &cipher[12..], aad: AAD, }, ) { return Some((plain, address, *outpoint)); } } } None } #[wasm_bindgen] /// Produce a proof and append it to a prd pub fn add_validation_token_to_prd( root_commitment: String, prd_commitment: String, approval: bool, ) -> ApiResult { let prd_hash = AnkPrdHash::from_str(&prd_commitment)?; let outpoint = OutPoint::from_str(&root_commitment)?; // find the prd in the registered process let mut processes = lock_processes()?; let process = processes .get_mut(&outpoint) .ok_or(ApiError::new("Unknown process".to_owned()))?; let prd_ref = process .get_impending_requests_mut() .into_iter() .find(|r| r.create_commitment() == prd_hash) .ok_or(ApiError::new( "Failed to find the prd in registered processes".to_owned(), ))?; let local_device = lock_local_device()?; let wallet = local_device.get_wallet(); let spend_key: SecretKey = wallet.get_client().get_spend_key().try_into()?; match prd_ref.prd_type { PrdType::Update => { let new_state = Value::from_str(&prd_ref.payload)?; let new_state_commitment = new_state.tagged_hash(); let message_hash = if approval { AnkHash::ValidationYes(AnkValidationYesHash::from_commitment(new_state_commitment)) } else { AnkHash::ValidationNo(AnkValidationNoHash::from_commitment(new_state_commitment)) }; let proof = Proof::new(message_hash, spend_key); prd_ref.validation_tokens.push(proof); Ok(ApiReturn { updated_process: Some((root_commitment, process.clone())), ..Default::default() }) } _ => return Err(ApiError::new("Can't validate that prd".to_owned())), } } #[wasm_bindgen] pub fn response_prd( root_commitment: String, prd_commitment: String, // The commitment to the Prd we respond to approval: bool, ) -> ApiResult { let prd_hash = AnkPrdHash::from_str(&prd_commitment)?; let outpoint = OutPoint::from_str(&root_commitment)?; let local_device = lock_local_device()?; let member = local_device .to_member() .ok_or(ApiError::new("Unpaired device".to_owned()))?; // find the prd in the registered process let mut processes = lock_processes()?; let process = processes .get_mut(&outpoint) .ok_or(ApiError::new("Unknown process".to_owned()))?; let prd_ref = process .get_impending_requests_mut() .into_iter() .find(|r| r.create_commitment() == prd_hash) .ok_or(ApiError::new( "Failed to find the prd in registered processes".to_owned(), ))?; match prd_ref.prd_type { PrdType::Update => { let pcd_hash: AnkPcdHash = AnkPcdHash::from_value(&Value::from_str(&prd_ref.payload)?); let prd_response = Prd::new_response( OutPoint::from_str(&root_commitment)?, serde_json::to_string(&member)?, prd_ref.validation_tokens.clone(), pcd_hash, ); let prd_msg = prd_response.to_network_msg(local_device.get_wallet())?; let mut ciphers = vec![]; for (sp_address, shared_secret) in process.get_all_secrets() { if sp_address.to_string() == local_device .get_wallet() .get_client() .get_receiving_address() { continue; } let cipher = encrypt_with_key(shared_secret.as_byte_array(), prd_msg.as_bytes())?; ciphers.push(cipher.to_lower_hex_string()); } return Ok(ApiReturn { ciphers_to_send: ciphers, ..Default::default() }); } _ => unimplemented!(), }; } fn confirm_prd(prd: Prd, shared_secret: &AnkSharedSecretHash) -> AnyhowResult { match prd.prd_type { PrdType::Confirm | PrdType::Response | PrdType::List => { return Err(AnyhowError::msg("Invalid prd type")); } _ => (), } let outpoint = OutPoint::from_str(&prd.root_commitment)?; let local_device = lock_local_device()?; let member = match local_device.to_member() { Some(member) => member, None => { // This might be because we're pairing, let's see if our address is part of sender of the initial prd let remote_member: Member = serde_json::from_str(&prd.sender)?; let addresses = remote_member.get_addresses(); let this_device_address = local_device .get_wallet() .get_client() .get_receiving_address(); if let Some(_) = addresses.into_iter().find(|a| *a == this_device_address) { remote_member } else { return Err(AnyhowError::msg("Must pair device first")); } } }; let pcd_commitment = AnkPcdHash::from_str(&prd.payload)?; let prd_confirm = Prd::new_confirm(outpoint, member, pcd_commitment); let prd_msg = prd_confirm.to_network_msg(local_device.get_wallet())?; Ok(encrypt_with_key(shared_secret.as_byte_array(), prd_msg.as_bytes())?.to_lower_hex_string()) } fn send_data(prd: &Prd, shared_secret: &AnkSharedSecretHash) -> AnyhowResult { let pcd = &prd.payload; let cipher = encrypt_with_key(shared_secret.as_byte_array(), pcd.as_bytes())?; Ok(ApiReturn { ciphers_to_send: vec![cipher.to_lower_hex_string()], ..Default::default() }) } fn decrypt_with_cached_messages( cipher: &[u8], messages: &mut MutexGuard> ) -> anyhow::Result, AnkSharedSecretHash)>> { let nonce = Nonce::from_slice(&cipher[..12]); for message in messages.iter_mut() { for shared_secret in message.shared_secrets.iter() { let aes_key = match AnkSharedSecretHash::from_str(shared_secret) { Ok(key) => key, Err(_) => { debug!( "Invalid shared secret for message{}: {}", message.id, shared_secret ); continue; } }; let engine = Aes256Gcm::new(aes_key.as_byte_array().into()); let plain = match engine.decrypt( &nonce, Payload { msg: &cipher[12..], aad: AAD, }, ) { Ok(plain) => plain, Err(_) => continue, }; let commitment = AnkPrdHash::from_str( message .commitment .as_ref() .ok_or(anyhow::Error::msg("Missing commitment".to_owned()))?, )?; // A message matched against a new transaction must be a prd // We just check the commitment while we're at it let _ = Prd::extract_from_message_with_commitment(&plain, &commitment)?; // Update the message status message.status = CachedMessageStatus::NoStatus; message.shared_secrets = vec![]; // this way we won't check it again return Ok(Some(( plain, aes_key, ))); } } Ok(None) } fn decrypt_with_known_processes(cipher: &[u8], processes: MutexGuard>) -> anyhow::Result, OutPoint)>> { let nonce = Nonce::from_slice(&cipher[..12]); for (outpoint, process) in processes.iter() { for (address, secret) in process.get_all_secrets() { debug!("Attempting decryption with key {} for {}", secret, address); let engine = Aes256Gcm::new(secret.as_byte_array().into()); if let Ok(plain) = engine.decrypt( &nonce, Payload { msg: &cipher[12..], aad: AAD, }, ) { return Ok(Some((plain, *outpoint))); } } } Ok(None) } /// Prd can come with an attached transaction or without /// It's useful to commit it in a transaction in the case there are multiple recipients (guarantee that everyone gets the same payload) /// Or if for some reasons we don't want to use the same shared secret again fn handle_prd( plain: &[u8], new_shared_secret: Option, ) -> AnyhowResult { // We already checked the commitment if any let prd = Prd::extract_from_message(plain)?; debug!("found prd: {:#?}", prd); let proof_key = prd.proof.unwrap().get_key(); let sp_address = serde_json::from_str::(&prd.sender)? .get_addresses() .into_iter() .find_map(|address| { let parsed_address = SilentPaymentAddress::try_from(address.as_str()).ok()?; let spend_key = parsed_address.get_spend_key().x_only_public_key().0; if spend_key == proof_key { Some(parsed_address) } else { None } }) .ok_or_else(|| anyhow::Error::msg("No matching address found for the proof key"))?; let outpoint = OutPoint::from_str(&prd.root_commitment)?; let mut processes = lock_processes()?; let relevant_process = match processes.entry(outpoint) { std::collections::hash_map::Entry::Occupied(entry) => entry.into_mut(), std::collections::hash_map::Entry::Vacant(entry) => { debug!("Creating new process for outpoint: {}", outpoint); let shared_secret = new_shared_secret .ok_or_else(|| anyhow::Error::msg("Missing shared secret for new process"))?; let mut shared_secrets = HashMap::new(); shared_secrets.insert(sp_address, shared_secret); entry.insert(Process::new(vec![], shared_secrets, vec![])) } }; match prd.prd_type { PrdType::Confirm => { // It must match a prd we sent previously // We send the whole data in a pcd debug!("Received confirm prd {:#?}", prd); let original_request = relevant_process .get_impending_requests() .into_iter() .find(|r| { if r.prd_type != PrdType::Update { return false; } let hash = Value::from_str(&r.payload).unwrap().tagged_hash(); hash.to_string() == prd.payload }) .ok_or(anyhow::Error::msg("Original request not found"))?; let shared_secret = relevant_process .get_shared_secret_for_address(&sp_address) .ok_or(anyhow::Error::msg( "Missing shared secret for address in original request", ))?; return send_data(original_request, &shared_secret); } PrdType::Update | PrdType::TxProposal | PrdType::Message => { // Those all have some new data we don't know about yet // We send a Confirm to get the pcd // Add the prd to our list of actions for this process relevant_process.insert_impending_request(prd.clone()); let shared_secret = relevant_process .get_shared_secret_for_address(&sp_address) .ok_or(anyhow::Error::msg( "Missing shared secret for address in original request", ))?; let cipher = confirm_prd(prd, &shared_secret)?; return Ok(ApiReturn { ciphers_to_send: vec![cipher], updated_process: Some((outpoint.to_string(), relevant_process.clone())), ..Default::default() }); } PrdType::Response => { // We must know of a prd update that the response answers to let original_request = relevant_process .get_impending_requests_mut() .into_iter() .find(|r| { if r.prd_type != PrdType::Update { return false; } let hash = Value::from_str(&r.payload).unwrap().tagged_hash(); hash.to_string() == prd.payload }) .ok_or(anyhow::Error::msg("Original request not found"))?; // Once we found the prd update, we can add the received proofs as validation tokens original_request .validation_tokens .extend(prd.validation_tokens); // We must return an update of the process let updated_process = (prd.root_commitment, relevant_process.clone()); return Ok(ApiReturn { updated_process: Some(updated_process), ..Default::default() }); } _ => unimplemented!(), } } fn handle_pcd(plain: Vec, root_commitment: OutPoint) -> AnyhowResult { let pcd = Value::from_str(&String::from_utf8(plain)?)?; let pcd_commitment = pcd.tagged_hash(); let mut processes = lock_processes()?; let relevant_process = processes.get_mut(&root_commitment).unwrap(); // We match the pcd with a prd and act accordingly let prd = relevant_process .get_impending_requests_mut() .into_iter() .find(|r| *r.payload == pcd_commitment.to_string()) .ok_or(AnyhowError::msg("Failed to retrieve the matching prd"))?; // We update the process and return it prd.payload = pcd.to_string(); return Ok(ApiReturn { updated_process: Some((root_commitment.to_string(), relevant_process.clone())), ..Default::default() }); } fn handle_decrypted_message( plain: Vec, shared_secret: Option, root_commitment: Option, ) -> anyhow::Result { // Try to handle as PRD first handle_prd(&plain, shared_secret) .or_else(|_| { // If PRD handling fails, try to handle as PCD handle_pcd( plain, root_commitment.ok_or(anyhow::Error::msg( "root_commitment must be known for a pcd", ))?, ) }) .map_err(|e| anyhow::Error::msg(format!("Failed to handle decrypted message: {}", e))) } #[wasm_bindgen] pub fn parse_cipher(cipher_msg: String) -> ApiResult { // We lock message cache and processes to prevent race conditions let mut messages = lock_messages()?; let processes = lock_processes()?; // Check that the cipher is not empty or too long if cipher_msg.is_empty() || cipher_msg.len() > MAX_PRD_PAYLOAD_SIZE { return Err(ApiError::new( "Invalid cipher: empty or too long".to_owned(), )); } let cipher = Vec::from_hex(cipher_msg.trim_matches('"'))?; // Try decrypting with cached messages first if let Ok(Some((plain, shared_secret))) = decrypt_with_cached_messages(&cipher, &mut messages) { return handle_decrypted_message(plain, Some(shared_secret), None) .map_err(|e| ApiError::new(format!("Failed to handle decrypted message: {}", e))); } // If that fails, try decrypting with known processes if let Ok(Some((plain, root_commitment))) = decrypt_with_known_processes(&cipher, processes) { return handle_decrypted_message(plain, None, Some(root_commitment)) .map_err(|e| ApiError::new(format!("Failed to handle decrypted message: {}", e))); } // If both decryption attempts fail, we keep it just in case we receive the transaction later let mut return_msg = CachedMessage::new(); return_msg.cipher = vec![cipher_msg]; return_msg.status = CachedMessageStatus::CipherWaitingTx; messages.push(return_msg.clone()); Ok(ApiReturn { updated_cached_msg: vec![return_msg], ..Default::default() }) } #[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] /// This takes a reference to a process and creates a commit msg for the latest state pub fn create_commit_message( init_commitment_outpoint: String, relay_address: String, fee_rate: u32, ) -> ApiResult { let outpoint = OutPoint::from_str(&init_commitment_outpoint)?; if let Some(process) = lock_processes()?.get(&outpoint) { match process.get_number_of_states() { 0 => Err(ApiError::new("Process has no states".to_owned())), 1 => { // This is a creation let state = process.get_latest_state().unwrap(); if state.commited_in.vout != u32::MAX { return Err(ApiError::new("Latest state is already commited".to_owned())); } let encrypted_pcd = state.encrypted_pcd.clone(); let keys = state.keys.clone(); let freezed_utxos = lock_freezed_utxos()?; let local_device = lock_local_device()?; let sp_wallet = local_device.get_wallet(); let signed_psbt = create_transaction( vec![], &freezed_utxos, sp_wallet, vec![Recipient { address: relay_address, amount: Amount::from_sat(1000), nb_outputs: 1, }], None, Amount::from_sat(fee_rate.into()), None, )?; let tx = signed_psbt.extract_tx()?; Ok(ApiReturn { commit_to_send: Some(CommitMessage::new_first_commitment( tx, encrypted_pcd.as_object().unwrap().clone(), keys, )), ..Default::default() }) } _ => { // We're updating an existing process // Check that initial outpoint is not a placeholder and that latest state has a commited_in of null if outpoint.vout != u32::MAX { return Err(ApiError::new( "Initial outpoint is a placeholder".to_owned(), )); } let state = process.get_latest_state().unwrap(); if state.commited_in != OutPoint::null() { return Err(ApiError::new("Latest state is already commited".to_owned())); } let encrypted_pcd = state.encrypted_pcd.clone(); let keys = state.keys.clone(); // We just send the message with the outpoint return Ok(ApiReturn { commit_to_send: Some(CommitMessage::new_update_commitment( outpoint, encrypted_pcd.as_object().unwrap().clone(), keys, )), ..Default::default() }); } } } else { return Err(ApiError::new("Process not found".to_owned())); } } #[wasm_bindgen] /// We assume that the provided tx outpoint exist pub fn create_update_transaction( init_commitment: Option, new_state: String, fee_rate: u32, ) -> ApiResult { let pcd = Value::from_str(&new_state)?; let pcd_map = pcd .as_object() .ok_or(ApiError::new("new_state must be an object".to_owned()))?; let mut processes = lock_processes()?; let commitment_outpoint: OutPoint; let relevant_process: &mut Process; if let Some(s) = init_commitment { // We're updating an existing contract let outpoint = OutPoint::from_str(&s)?; if let Some(p) = processes.get_mut(&outpoint) { // compare the provided new_state with the process defined template let previous_state = &p.get_state_at(0).unwrap().encrypted_pcd; if !compare_maps(previous_state.as_object().unwrap(), pcd_map) { return Err(ApiError::new( "Provided updated state is not consistent with the process template".to_owned(), )); } relevant_process = p; commitment_outpoint = outpoint; } else { // This is a process we don't know about, so we insert a new entry processes.insert(outpoint, Process::default()); relevant_process = processes.get_mut(&outpoint).unwrap(); commitment_outpoint = outpoint; } } else { // This is a creation with an init state, the commitment will come later // We need a placeholder to keep track of the process before it's commited on chain // We can take the hash of the init_state as a txid, and set the vout to the max as it is very unlikely to ever have a real commitment that will look like this let dummy = pcd.tagged_hash(); let dummy_outpoint = OutPoint::new(Txid::from_slice(dummy.as_byte_array())?, u32::MAX); processes.insert(dummy_outpoint, Process::default()); relevant_process = processes.get_mut(&dummy_outpoint).unwrap(); commitment_outpoint = dummy_outpoint; } // We assume that all processes must have a roles key let roles = pcd .get("roles") .ok_or(ApiError::new("No roles in new_state".to_owned()))?; let roles_map = roles .as_object() .ok_or(ApiError::new("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::new( "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(); let encrypted_pcd = pcd.clone(); let fields_to_encrypt: Vec = encrypted_pcd .as_object() .unwrap() .keys() .map(|k| k.clone()) .collect(); encrypted_pcd.encrypt_fields(&fields_to_encrypt, &mut fields2keys, &mut fields2cipher); let local_device = lock_local_device()?; let sp_wallet = local_device.get_wallet(); let sender: Member = local_device .to_member() .ok_or(ApiError::new("unpaired device".to_owned()))?; // We first generate the prd with all the keys that we will keep to ourselves let full_prd = Prd::new_update( commitment_outpoint, 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 ciphers = vec![]; for (member, visible_fields) in all_members { let mut prd = full_prd.clone(); prd.filter_keys(visible_fields); // we hash the payload prd.payload = Value::from_str(&prd.payload) .unwrap() .tagged_hash() .to_string(); let prd_msg = prd.to_network_msg(sp_wallet)?; 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.as_str())?.get_scan_key(), &partial_secret, ); let shared_secret = AnkSharedSecretHash::from_shared_point(shared_point); let cipher = encrypt_with_key(shared_secret.as_byte_array(), prd_msg.as_bytes())?; ciphers.push(cipher.to_lower_hex_string()); relevant_process .insert_shared_secret(SilentPaymentAddress::try_from(sp_address)?, shared_secret); } } relevant_process.insert_impending_request(full_prd); relevant_process.insert_state(ProcessState { commited_in: OutPoint::null(), encrypted_pcd: Value::Object(fields2cipher), keys: fields2keys, validation_tokens: vec![], }); // Create the new_tx message let new_tx_msg = NewTxMessage::new(serialize(&final_tx).to_lower_hex_string(), None); Ok(ApiReturn { new_tx_to_send: Some(new_tx_msg), updated_process: Some((commitment_outpoint.to_string(), relevant_process.clone())), ciphers_to_send: ciphers, ..Default::default() }) } #[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 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()); Ok(faucet_msg.to_string()) } /// Get active update proposals for a given process outpoint /// Returns a vector with the latest commited state first, if any, and all active proposals #[wasm_bindgen] pub fn get_update_proposals(process_outpoint: String) -> ApiResult> { let outpoint = OutPoint::from_str(&process_outpoint)?; let mut processes = lock_processes()?; // TODO: We clone the process to prevent double borrowing issue, this can certainly be improved let relevant_process = processes .get(&outpoint) .ok_or(ApiError::new("process not found".to_owned()))?; let mut updated_process = relevant_process.clone(); let update_proposals: Vec<&Prd> = relevant_process .get_impending_requests() .into_iter() .filter(|r| r.prd_type == PrdType::Update) .collect(); if update_proposals.is_empty() { return Err(ApiError::new(format!( "No active update proposals for process {}", process_outpoint ))); } let mut res = vec![]; // We first push the last commited state, if any match relevant_process.get_latest_commited_state() { Some(state) => res.push(serde_json::to_string(state)?), None => () } // Maybe that's the right place for adding a new state with what we've got from the prd update // We should probably iterate on every update proposals and see which one don't have a state yet let mut update_states = false; for proposal in update_proposals { // Is there a state that matches this proposal? If not, let's add it debug!("Trying proposal {:#?}", proposal); let pcd = match Value::from_str(&proposal.payload) { Ok(value) => value, Err(e) => continue }; debug!("found pcd {:#?}", pcd); let pcd_hash = AnkPcdHash::from_value(&pcd); // We look for a pending state for the exact same state as the one in the proposal if let None = relevant_process.get_latest_concurrent_states() .into_iter() .find(|state| { AnkPcdHash::from_value(&state.encrypted_pcd) == pcd_hash }) { // If not, we first add a new state updated_process.insert_state(ProcessState { commited_in: OutPoint::new(Txid::from_str(&pcd_hash.to_string())?, u32::MAX), encrypted_pcd: pcd.clone(), keys: proposal.keys.clone(), validation_tokens: proposal.validation_tokens.clone() }); update_states = true; } // We add the decrypted state to our return variable let mut decrypted_pcd = Map::new(); pcd.decrypt_fields(&proposal.keys, &mut decrypted_pcd)?; res.push(serde_json::to_string(&decrypted_pcd)?); } if update_states { // We replace the process processes.insert(outpoint, updated_process); } // else we do nothing Ok(res) }