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, AnkMessageHash, 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::{Keypair, 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, MutexExt, MAX_PRD_PAYLOAD_SIZE}; use serde_json::{Error as SerdeJsonError, Map, Value}; use serde::{de, 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::{ 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 sdk_common::secrets::SecretsStore; use crate::user::{lock_local_device, set_new_device, LOCAL_DEVICE}; use crate::wallet::{generate_sp_wallet, lock_freezed_utxos}; #[derive(Debug, PartialEq, Tsify, Serialize, Deserialize, Default)] #[tsify(into_wasm_abi)] #[allow(non_camel_case_types)] pub enum DiffStatus { #[default] None, Rejected, Validated, } #[derive(Debug, PartialEq, Tsify, Serialize, Deserialize, Default)] #[tsify(into_wasm_abi)] #[allow(non_camel_case_types)] pub struct UserDiff { pub process_id: String, pub new_state_merkle_root: String, // TODO add a merkle proof that the new_value belongs to that state pub value_commitment: String, pub field: String, pub description: Option, pub previous_value: Value, pub new_value: Value, pub notify_user: bool, pub need_validation: bool, pub validation_status: DiffStatus, } #[derive(Debug, PartialEq, Tsify, Serialize, Deserialize, Default)] #[tsify(into_wasm_abi)] #[allow(non_camel_case_types)] pub struct UpdatedProcess { pub commitment_tx: OutPoint, pub current_process: Process, pub up_to_date_roles: HashMap, pub new_diffs: Vec, // All diffs should have the same new_state_merkle_root pub modified_state: Option, // basically when we add/receive validation proofs for a state // I think we should never have both new_state and modified_state } #[derive(Debug, PartialEq, Tsify, Serialize, Deserialize, Default)] #[tsify(into_wasm_abi)] #[allow(non_camel_case_types)] pub struct ApiReturn { pub secrets: Option, pub updated_process: Option, pub new_tx_to_send: Option, pub ciphers_to_send: Vec, pub commit_to_send: Option, } pub type ApiResult = Result; #[derive(Debug, PartialEq, Tsify, Serialize, Deserialize, Default)] #[tsify(into_wasm_abi)] #[allow(non_camel_case_types)] pub struct NewKey { pub private_key: String, pub x_only_public_key: String, pub key_parity: bool } const IS_TESTNET: bool = true; const DEFAULT_AMOUNT: Amount = Amount::from_sat(1000); pub static SHAREDSECRETS: OnceLock> = OnceLock::new(); pub fn lock_shared_secrets() -> Result, anyhow::Error> { SHAREDSECRETS .get_or_init(|| Mutex::new(SecretsStore::new())) .lock_anyhow() } #[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 get_new_keypair() -> NewKey { let secp = Secp256k1::new(); let mut rng = thread_rng(); let keypair = Keypair::new(&secp, &mut rng); let secret_hex = keypair.secret_bytes().to_lower_hex_string(); let (xonly, parity) = keypair.x_only_public_key(); NewKey { private_key: secret_hex, x_only_public_key: xonly.to_string(), key_parity: if parity == Parity::Even { true } else { false } } } #[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 is_paired() -> ApiResult { let local_device = lock_local_device()?; Ok(local_device.get_pairing_commitment().is_some()) } #[wasm_bindgen] pub fn pair_device(commitment_tx: String, mut sp_addresses: Vec) -> ApiResult<()> { let mut local_device = lock_local_device()?; if local_device.get_pairing_commitment().is_some() { return Err(ApiError::new("Already paired".to_owned())); } let local_address = local_device .get_wallet() .get_client() .get_receiving_address(); if !sp_addresses.iter().any(|a| *a == local_address) { sp_addresses.push(local_address); } local_device.pair( OutPoint::from_str(&commitment_tx)?, 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_shared_secrets() -> ApiResult<()> { let mut shared_secrets = lock_shared_secrets()?; *shared_secrets = SecretsStore::new(); Ok(()) } #[wasm_bindgen] pub fn set_shared_secrets(secrets: String) -> ApiResult<()>{ let mut shared_secrets = lock_shared_secrets()?; *shared_secrets = serde_json::from_str(&secrets)?; Ok(()) } #[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_shared_secrets()?; 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, public_data: PublicKey, ) -> AnyhowResult { let b_scan: SecretKey; let local_member: Member; let sp_wallet: SpWallet; { let local_device = lock_local_device()?; sp_wallet = local_device.get_wallet().clone(); b_scan = local_device.get_wallet().get_client().get_scan_key(); local_member = local_device.to_member(); } 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 shared_secrets = lock_shared_secrets()?; // empty utxo_destroyed means we received this transaction if utxo_destroyed.is_empty() { let shared_point = sp_utils::receiving::calculate_ecdh_shared_secret( &public_data, &b_scan, ); let shared_secret = AnkSharedSecretHash::from_shared_point(shared_point); // We keep the shared_secret as unconfirmed shared_secrets.add_unconfirmed_secret(shared_secret); // We also return it let mut new_secret = SecretsStore::new(); new_secret.add_unconfirmed_secret(shared_secret); // We hash the shared secret to commit into the prd connect let secret_hash = AnkMessageHash::from_message(shared_secret.as_byte_array()); // We still don't know who sent it, so we reply with a `Connect` prd let prd_connect = Prd::new_connect(local_member, secret_hash, None); let msg = prd_connect.to_network_msg(&sp_wallet)?; // We encrypt the prd connect with the same secret let cipher = encrypt_with_key(shared_secret.as_byte_array(), msg.as_bytes())?; return Ok(ApiReturn { secrets: Some(new_secret), ciphers_to_send: vec![cipher.to_lower_hex_string()], ..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) -> 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 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 sender = local_device.to_member(); let prd_confirm = Prd::new_confirm(outpoint, sender, prd.pcd_commitments.clone()); // debug!("Sending confirm prd: {:?}", prd_confirm); 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 create_diffs(process: &Process, new_state: &ProcessState) -> AnyhowResult> { let new_state_commitments = new_state.pcd_commitment.as_object().ok_or(AnyhowError::msg("new_state commitments is not an object"))?; let device = lock_local_device()?; let our_id = device.to_member(); let is_pairing = device.get_pairing_commitment().is_none(); let fields_to_validate = if new_state.encrypted_pcd != Value::Null { new_state.get_fields_to_validate_for_member(&our_id)? } else { vec![] }; let new_state_root = &new_state.state_id; let new_state_decrypted = match new_state.decrypt_pcd() { Ok(val) => val, Err(_) => Map::new() }; let new_state_descriptions = &new_state.descriptions; let process_id = process.get_process_id()?.to_string(); let mut diffs = vec![]; if let Some(prev_state) = process.get_latest_commited_state() { // We first decrypt as much as we can of the prev_state let clear_prev_state = prev_state.decrypt_pcd()?; // We just make a diff for values that are different from previous state for (field, prev_hash) in prev_state.pcd_commitment.as_object().unwrap() { let description = new_state_descriptions.get(field).map(|d| d.to_string()); let new_value = if let Some(val) = new_state_decrypted.get(field.as_str()) { val.clone() } else { Value::Null }; let need_validation = if (is_pairing && field.as_str() == "roles" && new_value != Value::Null) || fields_to_validate.contains(field) { true } else { false }; if let Some(new_hash) = new_state_commitments.get(field.as_str()) { if new_hash.as_str() == prev_hash.as_str() { continue; } else { // There's a diff let previous_value = clear_prev_state.get(field.as_str()).unwrap().clone(); diffs.push(UserDiff { process_id: process_id.clone(), new_state_merkle_root: new_state_root.to_owned(), value_commitment: new_hash.as_str().unwrap().to_string(), field: field.to_owned(), description, previous_value, new_value, notify_user: false, need_validation, validation_status: DiffStatus::None, }); } } else { // We're missing a hash return Err(AnyhowError::msg(format!("No commitment for field {} in new state", field))); } } } else { // All fields need a diff for (field, hash) in new_state_commitments { let description = new_state_descriptions.get(field).map(|d| d.to_string()); let new_value = if let Some(val) = new_state_decrypted.get(field.as_str()) { val.clone() } else { Value::Null }; let need_validation = if (is_pairing && field.as_str() == "roles" && new_value != Value::Null) || fields_to_validate.contains(field) { true } else { false }; diffs.push(UserDiff { process_id: process_id.clone(), new_state_merkle_root: new_state_root.to_owned(), value_commitment: hash.as_str().unwrap().to_string(), field: field.to_owned(), description, previous_value: Value::Null, new_value, notify_user: false, need_validation, validation_status: DiffStatus::None, }); } } Ok(diffs) } fn handle_prd_connect(prd: Prd, secret: AnkSharedSecretHash) -> AnyhowResult { let local_device = lock_local_device()?; let local_member = local_device.to_member(); let sp_wallet = local_device.get_wallet(); let secret_hash = AnkMessageHash::from_message(secret.as_byte_array()); let mut shared_secrets = lock_shared_secrets()?; if let Some(prev_proof) = prd.validation_tokens.get(0) { // check that the proof is valid prev_proof.verify()?; // Check it's signed with our key let local_address = SilentPaymentAddress::try_from(sp_wallet.get_client().get_receiving_address())?; if prev_proof.get_key() != local_address.get_spend_key() { return Err(anyhow::Error::msg("Previous proof of a prd connect isn't signed by us")); } // Check it signs a prd connect that contains the commitment to the shared secret let empty_prd = Prd::new_connect(local_member, secret_hash, None); let msg = AnkMessageHash::from_message(empty_prd.to_string().as_bytes()); if *msg.as_byte_array() != prev_proof.get_message() { return Err(anyhow::Error::msg("Previous proof signs another message")); } // Now we can confirm the secret and link it to an address let sender = serde_json::from_str::(&prd.sender)?; let proof = prd.proof.unwrap(); let actual_sender = sender.get_address_for_key(&proof.get_key()) .ok_or(anyhow::Error::msg("Signer of the proof is not part of sender"))?; shared_secrets.confirm_secret_for_address(secret, actual_sender.clone().try_into()?); let mut secrets_return = SecretsStore::new(); secrets_return.confirm_secret_for_address(secret, actual_sender.try_into()?); return Ok(ApiReturn { secrets: Some(secrets_return), ..Default::default() }) } else { let proof = prd.proof.unwrap(); let sender = serde_json::from_str::(&prd.sender)?; let actual_sender = sender.get_address_for_key(&proof.get_key()) .ok_or(anyhow::Error::msg("Signer of the proof is not part of sender"))?; shared_secrets.confirm_secret_for_address(secret, actual_sender.clone().try_into()?); let mut secrets_return = SecretsStore::new(); secrets_return.confirm_secret_for_address(secret, actual_sender.try_into()?); let prd_connect = Prd::new_connect(local_member, secret_hash, prd.proof); let msg = prd_connect.to_network_msg(sp_wallet)?; let cipher = encrypt_with_key(secret.as_byte_array(), msg.as_bytes())?; return Ok(ApiReturn { ciphers_to_send: vec![cipher.to_lower_hex_string()], secrets: Some(secrets_return), ..Default::default() }) } } fn handle_prd( prd: Prd, secret: AnkSharedSecretHash ) -> AnyhowResult { debug!("handle_prd: {:#?}", prd); // Connect is a bit different here because there's no associated process // Let's handle that case separately if prd.prd_type == PrdType::Connect { return handle_prd_connect(prd, secret); } 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); entry.insert(Process::new(outpoint)) } }; match prd.prd_type { PrdType::Update => { // Compute the merkle tree root for the proposed new state to see if we already know about it let update_merkle_root = prd.pcd_commitments.create_merkle_tree()?.root().ok_or(AnyhowError::msg("Invalid merkle tree"))?.to_lower_hex_string(); if relevant_process.get_state_for_id(&update_merkle_root).is_ok() { // We already know about that state return Err(AnyhowError::msg("Received update for a state we already know")); } let commited_in = OutPoint::from_str(&prd.root_commitment)?; // Extract the roles from the payload let proposal_roles: HashMap = serde_json::from_str(&prd.payload)?; // TODO: check that the role in the prd has the right commitment let new_state = ProcessState { commited_in, pcd_commitment: prd.pcd_commitments, state_id: update_merkle_root.clone(), keys: prd.keys, ..Default::default() }; // Compute the diffs // At this point we don't have the encrypted values let diffs = create_diffs(&relevant_process, &new_state)?; // Take the roles from the last validated state let mut roles = HashMap::new(); if let Some(last_state) = relevant_process.get_latest_commited_state() { let decrypted_last_state = last_state.decrypt_pcd()?; roles = Value::Object(decrypted_last_state).extract_roles()?; } else { // We don't have commited state yet, let's take the current roles roles = proposal_roles; } relevant_process.insert_concurrent_state(new_state); let updated_process = UpdatedProcess { commitment_tx: outpoint, current_process: relevant_process.clone(), new_diffs: diffs, up_to_date_roles: roles, ..Default::default() }; return Ok(ApiReturn { updated_process: Some(updated_process), ..Default::default() }); } PrdType::Response => { let update_state_id = prd.pcd_commitments.create_merkle_tree()?.root().ok_or(AnyhowError::msg("Invalid merkle tree"))?.to_lower_hex_string(); let mut to_update = relevant_process.get_state_for_id_mut(&update_state_id)?; let new_validations = prd.validation_tokens; let mut to_add: Vec<&Proof> = vec![]; for token in &new_validations { let key = token.get_key(); // Check if the token key already exists let already_exists = to_update .validation_tokens .iter() .any(|existing_token| existing_token.get_key() == key); if !already_exists { debug!("Adding token with key {}", key); to_add.push(token); } else { debug!("Token with key {} already exists, skipping", key); } } // If there's no new proofs return early if to_add.is_empty() { return Err(AnyhowError::msg("No new validation tokens found in prd response")); } // We add the new tokens and return all that was updated to_update.validation_tokens.extend(to_add); let updated_state = to_update.clone(); let clear_state = to_update.decrypt_pcd()?; let roles = Value::Object(clear_state).extract_roles()?; let modified_state = Some(to_update.state_id.clone()); let mut commit_msg = CommitMessage::new_update_commitment( OutPoint::from_str(&prd.root_commitment)?, updated_state.pcd_commitment, roles ); commit_msg.set_validation_tokens(updated_state.validation_tokens); // We must return an update of the process let updated_process = UpdatedProcess { commitment_tx: OutPoint::from_str(&prd.root_commitment)?, current_process: relevant_process.clone(), modified_state, ..Default::default() }; return Ok(ApiReturn { updated_process: Some(updated_process), commit_to_send: Some(commit_msg), ..Default::default() }); } _ => unimplemented!(), } } fn handle_decrypted_message( secret: AnkSharedSecretHash, plain: Vec, ) -> anyhow::Result { let local_address: SilentPaymentAddress = lock_local_device()?.get_wallet().get_client().get_receiving_address().try_into()?; if let Ok(prd) = Prd::extract_from_message(&plain, local_address) { handle_prd(prd, secret) } else { Err(anyhow::Error::msg("Failed to handle decrypted message")) } } #[wasm_bindgen] /// Use the provided Map to update a state /// The map uses hash commitment as keys, as in storage pub fn update_process_state(init_commitment: String, state_id: String, hash2values: String) -> ApiResult { let hash2values_map = serde_json::from_str::(&hash2values)?.to_value_object()?; // Get the process let outpoint = OutPoint::from_str(&init_commitment)?; let mut processes = lock_processes()?; { // First a mutable borrow of the process let process = processes.get_mut(&outpoint) .ok_or(ApiError::new("Unknown process".to_owned()))?; // Get the state let state = process.get_latest_concurrent_states_mut()? .into_iter() .find(|state| state.state_id == state_id) .ok_or(ApiError::new("Unknown state".to_owned()))?; // Update each value // Check if there's already something // If we have the key, decrypt and compare to the commitment if state.encrypted_pcd.as_object().is_some() && !state.encrypted_pcd.as_object().unwrap().is_empty() { return Err(ApiError::new("State already existing".to_owned())); } let state_commitments = state.pcd_commitment.to_value_object()?; let mut new_encrypted_pcd: Map = Map::with_capacity(hash2values_map.len()); for (hash, value) in hash2values_map { // Check the hash in pcd_commitment, get the corresponding field name let (field, _) = state_commitments.iter().find(|(field, commitment)| *hash == **commitment) .ok_or(ApiError::new(format!("Failed to find the commitment {}", hash)))?; new_encrypted_pcd.insert(field.clone(), value); } // decrypt all we can and check it matches the commitment state.encrypted_pcd = Value::Object(new_encrypted_pcd); let commited_in = serialize(&state.commited_in); let clear_pcd = state.decrypt_pcd()?; for (i, (key, value)) in clear_pcd.iter().enumerate() { // hash each value, and check the result against commitments if let Some(expected) = state_commitments.get(key.as_str()) { // value can already be the commitment, if we don't have the encryption key if value.is_hex_string(Some(32)).is_ok() { // check if the clear value is the commitment if expected.as_str().unwrap() == value.as_str().unwrap() { continue; } } // Otherwise we hash the value whatever it is, it must match the commitment let mut value_bin = value.to_string().into_bytes(); value_bin.push(i.try_into().unwrap()); let tagged_hash = AnkPcdHash::from_value_with_outpoint(&value_bin, &commited_in); if tagged_hash.as_byte_array().to_lower_hex_string() != expected.as_str().unwrap() { // We set the encrypted pcd back to empty state.encrypted_pcd = Value::Object(Map::new()); return Err(ApiError::new(format!("Retrieved value for {} doesn't match the commitment", key))); } } else { // This shouldn't be possible state.encrypted_pcd = Value::Object(Map::new()); return Err(ApiError::new(format!("Missing commitment for key {}", key))); } } } // If every value we can decrypt is valid, then we return the new state and diffs // We borrow it again immutably let process = processes.get(&outpoint).unwrap(); let state = process.get_latest_concurrent_states()?.into_iter().find(|s| s.state_id == state_id).unwrap(); let diffs = create_diffs(&process, &state)?; let udpated_process = UpdatedProcess { commitment_tx: outpoint, current_process: process.clone(), new_diffs: diffs, ..Default::default() }; Ok(ApiReturn { updated_process: Some(udpated_process), ..Default::default() }) } #[wasm_bindgen] pub fn parse_cipher(cipher_msg: String) -> ApiResult { // 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('"'))?; let decrypt_res = lock_shared_secrets()?.try_decrypt(&cipher); if let Ok((secret, plain)) = decrypt_res { return handle_decrypted_message(secret, plain) .map_err(|e| ApiError::new(format!("Failed to handle decrypted message: {}", e))); } Err(ApiError::new("Failed to decrypt cipher".to_owned())) } #[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()) } fn get_shared_secrets_in_transaction( psbt: &Psbt, addresses: Vec ) -> anyhow::Result> { let local_device = lock_local_device()?; let sp_wallet = local_device.get_wallet(); let partial_secret = sp_wallet .get_client() .get_partial_secret_from_psbt(&psbt)?; let mut new_secrets = HashMap::new(); for address in addresses { let sp_address = SilentPaymentAddress::try_from(address.as_str())?; let shared_point = sp_utils::sending::calculate_ecdh_shared_secret( &sp_address.get_scan_key(), &partial_secret, ); let shared_secret = AnkSharedSecretHash::from_shared_point(shared_point); new_secrets.insert(sp_address, shared_secret); } Ok(new_secrets) } fn create_transaction_for_addresses(addresses: Vec, fee_rate: u32) -> anyhow::Result { let mut sp_addresses: Vec = Vec::with_capacity(addresses.len()); for address in &addresses { let sp_address = SilentPaymentAddress::try_from(address.as_str())?; sp_addresses.push(sp_address); } let local_device = lock_local_device()?; let sp_wallet = local_device.get_wallet(); let freezed_utxos = lock_freezed_utxos()?; let mut recipients = Vec::with_capacity(addresses.len()); for address in addresses { let recipient = Recipient { address: address, amount: DEFAULT_AMOUNT, nb_outputs: 1, }; recipients.push(recipient); } let signed_psbt = create_transaction( vec![], &freezed_utxos, sp_wallet, recipients, None, Amount::from_sat(fee_rate.into()), None, )?; Ok(signed_psbt) } #[wasm_bindgen] /// We send a transaction that pays at least one output to each address of each member /// The goal is to establish a shared_secret to be used as an encryption key for further communication pub fn create_connect_transaction(members_str: Vec, fee_rate: u32) -> ApiResult { let mut members: Vec = vec![]; for member in members_str { members.push(serde_json::from_str(&member)?) } let mut addresses = vec![]; for member in members { addresses.extend(member.get_addresses().into_iter()); } if addresses.is_empty() { return Err(ApiError::new("No addresses to connect to".to_owned())); } let psbt = create_transaction_for_addresses(addresses.clone(), fee_rate)?; let new_secrets = get_shared_secrets_in_transaction(&psbt, addresses)?; let transaction = psbt.extract_tx()?; let mut shared_secrets = lock_shared_secrets()?; let mut secrets_return = SecretsStore::new(); for (address, secret) in new_secrets { shared_secrets.confirm_secret_for_address(secret, address); secrets_return.confirm_secret_for_address(secret, address); } Ok(ApiReturn { new_tx_to_send: Some(NewTxMessage::new(serialize(&transaction).to_lower_hex_string(), None)), secrets: Some(secrets_return), ..Default::default() }) } #[wasm_bindgen] pub fn create_new_process( init_state_str: String, descriptions_str: Option, relay_address: String, fee_rate: u32, ) -> ApiResult { let init_state = ::new_from_string(&init_state_str)?; let descriptions = if let Some(d) = descriptions_str { ::new_from_string(&d)? } else { Value::Object(Map::new()) }; // check that we have a proper roles map let roles = init_state.extract_roles()?; // We create a transaction that spends to the relay address let psbt = create_transaction_for_addresses(vec![relay_address.clone()], fee_rate)?; // We take the secret out let new_secrets = get_shared_secrets_in_transaction(&psbt, vec![relay_address])?; let mut shared_secrets = lock_shared_secrets()?; let mut secrets_return = SecretsStore::new(); for (address, secret) in new_secrets { shared_secrets.confirm_secret_for_address(secret, address); secrets_return.confirm_secret_for_address(secret, address); } let transaction = psbt.extract_tx()?; // We now have the outpoint that will serve as id for the whole process let outpoint = OutPoint::new(transaction.txid(), 0); let new_state = ProcessState::new(outpoint, init_state.to_value_object()?, descriptions.to_value_object()?)?; let mut process = Process::new(outpoint); let diffs = create_diffs(&process, &new_state)?; process.insert_concurrent_state(new_state.clone())?; { let mut processes = lock_processes()?; // If we already have an entry with this outpoint, something's wrong if processes.contains_key(&outpoint) { return Err(ApiError::new("There's already a process for this outpoint".to_owned())); } processes.insert(outpoint.clone(), process.clone()); } let commit_msg = CommitMessage::new_first_commitment(transaction, new_state.pcd_commitment, roles.clone()); let updated_process = UpdatedProcess { commitment_tx: outpoint, current_process: process, up_to_date_roles: roles, new_diffs: diffs, ..Default::default() }; Ok(ApiReturn { secrets: Some(secrets_return), commit_to_send: Some(commit_msg), updated_process: Some(updated_process), ..Default::default() }) } #[wasm_bindgen] pub fn update_process( init_commitment: String, new_state_str: String, ) -> ApiResult { let outpoint = OutPoint::from_str(&init_commitment)?; let mut processes = lock_processes()?; let process = processes.get_mut(&outpoint) .ok_or(ApiError::new("Unknown process".to_owned()))?; let prev_state = process.get_latest_commited_state() .ok_or(ApiError::new("Process must have at least one state already commited".to_owned()))?; let last_state_commitments = &prev_state.pcd_commitment; let last_state_descriptions = &prev_state.descriptions; let clear_new_state = Value::from_str(&new_state_str)?; let roles = clear_new_state.extract_roles()?; let new_state = ProcessState::new(prev_state.commited_in, clear_new_state.to_value_object()?, last_state_descriptions.clone())?; // We compare the new state with the previous one let last_state_merkle_root = &prev_state.state_id; if *last_state_merkle_root == new_state.state_id { return Err(ApiError::new("new proposed state is identical to the previous commited state".to_owned())); } // We check that we don't have already a similar concurrent state let concurrent_processes = process.get_latest_concurrent_states()?; if concurrent_processes.iter().any(|p| p.state_id == new_state.state_id) { return Err(ApiError::new("New state already known".to_owned())); } let diffs = create_diffs(&process, &new_state)?; // Add the new state to the process process.insert_concurrent_state(new_state.clone())?; let updated_process = UpdatedProcess { commitment_tx: outpoint, current_process: process.clone(), up_to_date_roles: roles, new_diffs: diffs, ..Default::default() }; Ok(ApiReturn { updated_process: Some(updated_process), ..Default::default() }) } #[wasm_bindgen] pub fn create_update_message( init_commitment: String, merkle_root_hex: String, ) -> ApiResult { let mut processes = lock_processes()?; let outpoint = OutPoint::from_str(&init_commitment)?; let process = processes.get_mut(&outpoint) .ok_or(ApiError::new("Unknown process".to_owned()))?; let update_state = process.get_state_for_id(&merkle_root_hex)?; // We must have at least the key for the roles field, otherwise we don't know who to send the message to let clear_state = update_state.decrypt_pcd()?; let roles = Value::Object(clear_state).extract_roles()?; let local_device = lock_local_device()?; let sp_wallet = local_device.get_wallet(); let local_address = sp_wallet.get_client().get_receiving_address(); let mut all_members: HashMap> = HashMap::new(); let shared_secrets = lock_shared_secrets()?; for (name, role) in &roles { let fields: Vec = role .validation_rules .iter() .flat_map(|rule| rule.fields.clone()) .collect(); for member in &role.members { // Check that we have a shared_secret with all members if let Some(no_secret_address) = member.get_addresses().iter() .find(|a| shared_secrets.get_secret_for_address(a.as_str().try_into().unwrap()).is_none()) { // We ignore it if we don't have a secret with ourselves if *no_secret_address != local_address { // for now we return an error to keep it simple return Err(ApiError::new(format!("No shared secret for all addresses of {:?}\nPlease first connect", member))); } } if !all_members.contains_key(&member) { all_members.insert(member.clone(), HashSet::new()); } all_members.get_mut(&member).unwrap().extend(fields.clone()); } } let sender: Member = local_device .to_member(); let full_prd = Prd::new_update( outpoint, sender, roles, update_state.keys.clone(), update_state.pcd_commitment.clone(), ); let mut ciphers = vec![]; 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 addresses = member.get_addresses(); for sp_address in addresses.into_iter() { // We skip our own device address, no point sending ourself a cipher if sp_address == local_address { continue; } // We shouldn't ever have error here since we already checked above let shared_secret = shared_secrets.get_secret_for_address(sp_address.as_str().try_into()?).unwrap(); let cipher = encrypt_with_key(shared_secret.as_byte_array(), prd_msg.as_bytes())?; ciphers.push(cipher.to_lower_hex_string()); } } if ciphers.is_empty() { return Err(ApiError::new("Empty ciphers list".to_owned())); } Ok(ApiReturn { ciphers_to_send: ciphers, ..Default::default() }) } #[wasm_bindgen] pub fn validate_state(init_commitment: String, merkle_root_hex: String) -> ApiResult { add_validation_token(init_commitment, merkle_root_hex, true) } #[wasm_bindgen] pub fn refuse_state(init_commitment: String, merkle_root_hex: String) -> ApiResult { add_validation_token(init_commitment, merkle_root_hex, false) } #[wasm_bindgen] pub fn evaluate_state(init_commitment: String, previous_state: Option, state: String) -> ApiResult { let prev_state: Option = if let Some(s) = previous_state { Some(serde_json::from_str(&s)?) } else { None }; let process_state: ProcessState = serde_json::from_str(&state)?; process_state.is_valid(prev_state.as_ref())?; let clear_pcd = process_state.decrypt_pcd()?; let roles = Value::Object(clear_pcd).extract_roles()?; // We create a commit msg with the valid state let outpoint: OutPoint = OutPoint::from_str(&init_commitment)?; let commit_msg = CommitMessage::new_update_commitment(outpoint, process_state.pcd_commitment, roles); Ok(ApiReturn { commit_to_send: Some(commit_msg), ..Default::default() }) } fn add_validation_token(init_commitment: String, merkle_root_hex: String, approval: bool) -> ApiResult { let mut processes = lock_processes()?; let outpoint = OutPoint::from_str(&init_commitment)?; let process = processes.get_mut(&outpoint) .ok_or(ApiError::new("Unknown process".to_owned()))?; { let update_state: &mut ProcessState = process.get_state_for_id_mut(&merkle_root_hex)?; let mut merkle_root = [0u8; 32]; merkle_root.copy_from_slice(&Vec::from_hex(&merkle_root_hex)?); let message_hash = if approval { AnkHash::ValidationYes(AnkValidationYesHash::from_merkle_root(merkle_root)) } else { AnkHash::ValidationNo(AnkValidationNoHash::from_merkle_root(merkle_root)) }; let local_device = lock_local_device()?; let sp_wallet = local_device.get_wallet(); let proof = Proof::new(message_hash, sp_wallet.get_client().get_spend_key().try_into()?); update_state.validation_tokens.push(proof); } let updated_process = UpdatedProcess { commitment_tx: OutPoint::from_str(&init_commitment)?, current_process: process.clone(), modified_state: Some(merkle_root_hex), ..Default::default() }; Ok(ApiReturn { updated_process: Some(updated_process), ..Default::default() }) } #[wasm_bindgen] pub fn create_response_prd(init_commitment: String, merkle_root_hex: String) -> ApiResult { let mut processes = lock_processes()?; let outpoint = OutPoint::from_str(&init_commitment)?; let process = processes.get_mut(&outpoint) .ok_or(ApiError::new("Unknown process".to_owned()))?; let update_state: &mut ProcessState = process.get_state_for_id_mut(&merkle_root_hex)?; // We must have at least the key for the roles field, otherwise we don't know who to send the message to let clear_state = update_state.decrypt_pcd()?; let roles = Value::Object(clear_state).extract_roles()?; let local_device = lock_local_device()?; let sp_wallet = local_device.get_wallet(); let local_address = sp_wallet.get_client().get_receiving_address(); let mut all_members: HashMap> = HashMap::new(); let shared_secrets = lock_shared_secrets()?; for (name, role) in roles { let fields: Vec = role .validation_rules .iter() .flat_map(|rule| rule.fields.clone()) .collect(); for member in role.members { // Check that we have a shared_secret with all members if let Some(no_secret_address) = member.get_addresses().iter() .find(|a| shared_secrets.get_secret_for_address(a.as_str().try_into().unwrap()).is_none()) { // We ignore it if we don't have a secret with ourselves if *no_secret_address != local_address { // for now we return an error to keep it simple return Err(ApiError::new(format!("No shared secret for all addresses of {:?}\nPlease first connect", member))); } } if !all_members.contains_key(&member) { all_members.insert(member.clone(), HashSet::new()); } all_members.get_mut(&member).unwrap().extend(fields.clone()); } } let our_key = SilentPaymentAddress::try_from(local_address.as_str())?.get_spend_key(); let proof = update_state.validation_tokens.iter().find(|t| t.get_key() == our_key) .ok_or(ApiError::new("We haven't added our validation token yet".to_owned()))?; let sender: Member = local_device .to_member(); let response_prd = Prd::new_response( outpoint, sender, vec![*proof], update_state.pcd_commitment.clone(), ); let prd_msg = response_prd.to_network_msg(sp_wallet)?; let mut ciphers = vec![]; for (member, visible_fields) in all_members { let addresses = member.get_addresses(); for sp_address in addresses.into_iter() { // We skip our own device address, no point sending ourself a cipher if sp_address == local_address { continue; } // We shouldn't ever have error here since we already checked above let shared_secret = shared_secrets.get_secret_for_address(sp_address.as_str().try_into()?).unwrap(); let cipher = encrypt_with_key(shared_secret.as_byte_array(), prd_msg.as_bytes())?; ciphers.push(cipher.to_lower_hex_string()); } } if ciphers.is_empty() { return Err(ApiError::new("Empty ciphers list".to_owned())); } Ok(ApiReturn { 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()) } #[wasm_bindgen] pub fn get_storages(process_outpoint: String) -> ApiResult> { let outpoint = OutPoint::from_str(&process_outpoint)?; Ok(vec![]) }