use std::any::Any; use std::borrow::Borrow; use std::collections::{BTreeMap, 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::hash::AnkPcdHash; use sdk_common::log::{self, debug, info, warn}; use anyhow::{anyhow, Context}; use anyhow::Error as AnyhowError; use anyhow::Result as AnyhowResult; use sdk_common::aes_gcm::aead::{Aead, Payload}; use sdk_common::crypto::{ decrypt_with_key, encrypt_with_key, generate_key, AeadCore, Aes256Gcm, AnkSharedSecretHash, KeyInit, AAD }; use sdk_common::process::{check_tx_for_process_updates, lock_processes, Process, ProcessState}; use sdk_common::serialization::{OutPointMemberMap, OutPointProcessMap, ciborium_deserialize as common_deserialize}; 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::{ SilentPaymentAddress, Error as SpError, }; use sdk_common::{js_sys::{Object, Reflect, Uint8Array}, signature, MutexExt, MAX_PRD_PAYLOAD_SIZE}; use serde_json::{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, CommitMessage, Envelope, FaucetMessage, NewTxMessage, }; use sdk_common::pcd::{ DataType, FileBlob, Member, Pcd, PcdCommitments, RoleDefinition, Roles, ValidationRule, PCD_VERSION, PcdSerializable }; use sdk_common::prd::{AnkPrdHash, Prd, PrdType}; use sdk_common::silentpayments::{create_transaction as internal_create_transaction, sign_transaction as internal_sign_tx, TsUnsignedTransaction}; use sdk_common::sp_client::{FeeRate, OutputSpendStatus, OwnedOutput, Recipient, RecipientAddress, SilentPaymentUnsignedTransaction, SpClient, 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, scan_blocks}; const EMPTYSTATEID: &str = "0000000000000000000000000000000000000000000000000000000000000000"; #[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 state_id: String, // TODO add a merkle proof that the new_value belongs to that state pub value_commitment: String, pub field: String, pub roles: Roles, pub description: Option, 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 process_id: OutPoint, pub current_process: Process, pub diffs: Vec, // All diffs should have the same state_id pub encrypted_data: BTreeMap, // hashes in pcd commitment to ciphers pub validated_state: Option<[u8; 32]>, // when we add/receive validation proofs for a 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 push_to_storage: Vec, // hash of the requested data, must be in db pub partial_tx: Option, } pub type ApiResult = Result; 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: serde_wasm_bindgen::Error) -> 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()?; let address = local_device .get_sp_client() .get_receiving_address() .to_string(); debug!("{}", address); Ok(address) } #[wasm_bindgen] pub fn get_member() -> ApiResult { let local_device = lock_local_device()?; let us = local_device.to_member(); Ok(us) } #[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: SpClient = 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(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(process_id: 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_sp_client() .get_receiving_address() .to_string(); if !sp_addresses.iter().any(|a| *a == local_address) { sp_addresses.push(local_address); } local_device.pair( OutPoint::from_str(&process_id)?, 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(()) } #[wasm_bindgen] pub fn dump_wallet() -> ApiResult { let device = lock_local_device()?; Ok(serde_json::to_string(device.get_sp_client()).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: JsValue) -> ApiResult<()> { let parsed_processes: HashMap = serde_wasm_bindgen::from_value(processes)?; let mut cached_processes = lock_processes()?; if !cached_processes.is_empty() { // Don't overwrite processes in memory return Err(ApiError::new("Processes cache is not empty".to_owned())); } *cached_processes = parsed_processes; Ok(()) } #[wasm_bindgen] pub fn add_to_process_cache(process_id: String, process: String) -> ApiResult<()> { let process_id = OutPoint::from_str(&process_id)?; let process: Process = serde_json::from_str(&process)?; let mut cached_processes = lock_processes()?; cached_processes.insert(process_id, process); 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 get_pairing_process_id() -> ApiResult { let local_device = lock_local_device()?; let pairing_commitment = local_device.get_pairing_commitment().ok_or(ApiError::new("Device is not paired".to_owned()))?; Ok(pairing_commitment.to_string()) } #[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 dump_neutered_device() -> ApiResult { let local_device = lock_local_device()?; if local_device.get_pairing_commitment().is_none() { return Err(ApiError::new("Device must be paired".to_owned())); } let client = local_device.get_sp_client(); let scan_key = client.get_scan_key(); let spend_pubkey: PublicKey = client.get_spend_key().into(); let neutered_client = SpClient::new(scan_key, SpendKey::Public(spend_pubkey), Network::Signet)?; let mut neutered_device = Device::new(neutered_client); neutered_device.pair(local_device.get_pairing_commitment().unwrap(), local_device.to_member()); Ok(neutered_device) } #[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: SpClient; { let local_device = lock_local_device()?; sp_wallet = local_device.get_sp_client().clone(); b_scan = local_device.get_sp_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, members_list: &OutPointMemberMap ) -> anyhow::Result { let tx = deserialize::(&Vec::from_hex(&tx_hex)?)?; // Before anything, check if this transaction spends the tip of a process we know about match check_tx_for_process_updates(&tx) { Ok(outpoint) => { let processes = lock_processes()?; let process = processes.get(&outpoint).unwrap(); let new_state = process.get_latest_commited_state().unwrap(); let diffs = if let Ok(diffs) = create_diffs(&lock_local_device()?, process, new_state, members_list) { diffs } else { vec![] }; let updated_process = UpdatedProcess { process_id: outpoint, current_process: process.clone(), diffs, ..Default::default() }; let api_return = ApiReturn { updated_process: Some(updated_process), ..Default::default() }; debug!("Found an update for process {:?}", api_return.updated_process.as_ref().unwrap().process_id); return Ok(api_return); } Err(e) => debug!("Failed to find process update: {}", e) } let tweak_data = PublicKey::from_str(&tweak_data_hex)?; let updated: HashMap; { let mut device = lock_local_device()?; updated = device.update_outputs_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, members_list: OutPointMemberMap) -> 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(), &members_list )?) } 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 = prd.process_id; let local_device = lock_local_device()?; let sender = local_device.to_member(); let prd_confirm = Prd::new_confirm(outpoint, sender, prd.pcd_commitments.clone()); let prd_msg = prd_confirm.to_network_msg(local_device.get_sp_client())?; Ok(encrypt_with_key(shared_secret.as_byte_array(), prd_msg.as_bytes())?.to_lower_hex_string()) } fn create_diffs(device: &MutexGuard, process: &Process, new_state: &ProcessState, members_list: &OutPointMemberMap) -> AnyhowResult> { let new_state_commitments = &new_state.pcd_commitment; let our_id = device.get_pairing_commitment(); let fields_to_validate = if let Some(our_id) = our_id { new_state.get_fields_to_validate_for_member(&our_id)? } else { // Device is unpaired, we just take all the fields in the `pairing` role if let Some(pairing_role) = new_state.roles.get("pairing") { pairing_role.validation_rules.iter().flat_map(|r| r.fields.clone()).collect() } else { return Err(AnyhowError::msg("Missing pairing role")) } }; let new_state_id = &new_state.state_id; let new_public_data = &new_state.public_data; let process_id = process.get_process_id()?.to_string(); let mut diffs = vec![]; for (field, hash) in new_state_commitments.iter() { let need_validation = fields_to_validate.contains(field); diffs.push(UserDiff { process_id: process_id.clone(), state_id: new_state_id.to_lower_hex_string(), value_commitment: hash.to_lower_hex_string(), field: field.to_owned(), description: None, // TODO we don't use that for now, we'll see later notify_user: false, need_validation, validation_status: DiffStatus::None, roles: new_state.roles.clone(), }); } 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_sp_client(); 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_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 proof = prd.proof.unwrap(); let actual_sender = prd.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 actual_sender = prd.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, members_list: &OutPointMemberMap, ) -> 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 = prd.process_id; 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"))?; 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 = relevant_process.get_process_tip()?; let new_state = ProcessState { commited_in, pcd_commitment: prd.pcd_commitments, state_id: update_merkle_root.clone(), keys: prd.keys, roles: prd.roles, public_data: prd.public_data, ..Default::default() }; // Compute the diffs let diffs = create_diffs(&lock_local_device()?, &relevant_process, &new_state, members_list)?; relevant_process.insert_concurrent_state(new_state)?; let updated_process = UpdatedProcess { process_id: outpoint, current_process: relevant_process.clone(), diffs, ..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"))?; 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 validated_state = Some(to_update.state_id); let mut commit_msg = CommitMessage::new( prd.process_id, updated_state.pcd_commitment, updated_state.roles, updated_state.public_data, updated_state.validation_tokens, ); // We must return an update of the process let updated_process = UpdatedProcess { process_id: prd.process_id, current_process: relevant_process.clone(), validated_state, ..Default::default() }; return Ok(ApiReturn { updated_process: Some(updated_process), commit_to_send: Some(commit_msg), ..Default::default() }); } PrdType::Request => { // We are being requested encrypted data for one or more states, to be uploaded on storage let states: Vec<[u8; 32]> = serde_json::from_str(&prd.payload)?; let requester = if let Some((requester_process_id, _)) = members_list.0.iter() .find(|(outpoint, member)| { **member == prd.sender }) { requester_process_id } else { return Err(AnyhowError::msg("Unknown requester")); }; // diffs will trigger upload of the encrypted data on storage let mut diffs = vec![]; // This will notify the requester and provide relevant information if needed let mut ciphers = vec![]; let mut push_to_storage = vec![]; for state_id in states { let state = match relevant_process.get_state_for_id(&state_id) { Ok(state) => state, Err(_) => { debug!("Ignoring request for unknown state {}", state_id.to_lower_hex_string()); continue; } }; let local_device = lock_local_device()?; let sp_wallet = local_device.get_sp_client(); let local_address = sp_wallet.get_receiving_address().to_string(); let mut relevant_fields: HashSet = HashSet::new(); let shared_secrets = lock_shared_secrets()?; for (name, role) in state.roles.iter() { if !role.members.contains(&requester) { // This role doesn't concern requester continue; } let fields: Vec = role .validation_rules .iter() .flat_map(|rule| rule.fields.clone()) .collect(); relevant_fields.extend(fields); } let sender: Member = local_device .to_member(); let mut full_prd = Prd::new_update( outpoint, sender, state.roles.clone(), state.public_data.clone(), state.keys.clone(), state.pcd_commitment.clone(), ); full_prd.filter_keys(&relevant_fields); let prd_msg = full_prd.to_network_msg(sp_wallet)?; let addresses = prd.sender.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're answering a cipher let shared_secret = shared_secrets.get_secret_for_address(sp_address.as_str().try_into()?) .ok_or(AnyhowError::msg(format!("No secret for address {}", sp_address.as_str())))?; let cipher = encrypt_with_key(shared_secret.as_byte_array(), prd_msg.as_bytes())?; ciphers.push(cipher.to_lower_hex_string()); } let pcd_commitment = &state.pcd_commitment; for (field, hash) in pcd_commitment.iter() { // We only need field that are visible by requester if !relevant_fields.contains(field.as_str()) { continue; } let diff = UserDiff { process_id: outpoint.to_string(), state_id: state_id.to_lower_hex_string(), value_commitment: hash.to_lower_hex_string(), field: field.to_owned(), ..Default::default() }; diffs.push(diff); push_to_storage.push(hash.to_lower_hex_string()); } } let updated_process = Some(UpdatedProcess { process_id: outpoint, current_process: relevant_process.clone(), diffs, ..Default::default() }); return Ok(ApiReturn { updated_process, ciphers_to_send: ciphers, push_to_storage, ..Default::default() }); } _ => unimplemented!(), } } fn handle_decrypted_message( secret: AnkSharedSecretHash, plain: Vec, members_list: &OutPointMemberMap ) -> anyhow::Result { let local_address: SilentPaymentAddress = lock_local_device()?.get_address(); if let Ok(prd) = Prd::extract_from_message(&plain, local_address) { handle_prd(prd, secret, members_list) } else { Err(anyhow::Error::msg("Failed to handle decrypted message")) } } #[wasm_bindgen] pub fn parse_cipher(cipher_msg: String, members_list: OutPointMemberMap) -> 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, &members_list) .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()?; Ok(JsValue::from_serde(device.get_outputs())?) } #[wasm_bindgen] pub fn get_available_amount() -> ApiResult { let device = lock_local_device()?; Ok(device.get_balance().to_sat()) } fn get_shared_secrets_in_transaction( unsigned_transaction: &SilentPaymentUnsignedTransaction, sp_addresses: &[SilentPaymentAddress] ) -> anyhow::Result> { let mut new_secrets = HashMap::new(); for sp_address in sp_addresses { let shared_point = sp_utils::sending::calculate_ecdh_shared_secret( &sp_address.get_scan_key(), &unsigned_transaction.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( device: &Device, freezed_utxos: &HashSet, sp_addresses: &[SilentPaymentAddress], fee_rate: FeeRate ) -> anyhow::Result { let mut recipients = Vec::with_capacity(sp_addresses.len()); for sp_address in sp_addresses { let recipient = Recipient { address: RecipientAddress::SpAddress(*sp_address), amount: DEFAULT_AMOUNT, }; recipients.push(recipient); } // If we had mandatory inputs, we would add them at the top of the list // Take spendable outputs and filter out the freezed utxos let candidates_inputs: Vec<(OutPoint, OwnedOutput)> = device.get_outputs() .into_iter() .filter_map(|(outpoint, output)| { if output.spend_status == OutputSpendStatus::Unspent && !freezed_utxos.contains(outpoint) { Some((*outpoint, output.clone())) } else { None } }) .collect(); let mut tx = internal_create_transaction( candidates_inputs, device.get_sp_client(), recipients, None, fee_rate, )?; let unsigned_transaction = SpClient::finalize_transaction(tx)?; log::debug!("{:#?}", unsigned_transaction.unsigned_tx); Ok(unsigned_transaction) } #[wasm_bindgen] /// We send a transaction that pays at least one output to each address /// The goal can be to establish a shared_secret to be used as an encryption key for further communication /// or if the recipient is a relay it can be the init transaction for a new process pub fn create_transaction(addresses: Vec, fee_rate: u32) -> ApiResult { if addresses.is_empty() { return Err(ApiError::new("No addresses to connect to".to_owned())); } let sp_addresses: anyhow::Result> = addresses.into_iter() .map(|a| { ::try_from(a).map_err(|e| anyhow::Error::new(e)) }) .collect(); let sp_addresses = sp_addresses?; let mut local_device = lock_local_device()?; let mut freezed_utxos = lock_freezed_utxos()?; let partial_tx = create_transaction_for_addresses(&local_device, &freezed_utxos, &sp_addresses, FeeRate::from_sat_per_vb(fee_rate as f32))?; let new_secrets = get_shared_secrets_in_transaction(&partial_tx, &sp_addresses)?; let unsigned_tx = SpClient::finalize_transaction(partial_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); } let outputs = local_device.get_mut_outputs(); let new_txid = unsigned_tx.unsigned_tx.as_ref().unwrap().txid(); // We mark the utxos in the inputs as spent to prevent accidental double spends for input in &unsigned_tx.unsigned_tx.as_ref().unwrap().input { if let Some(output) = outputs.get_mut(&input.previous_output) { output.spend_status = OutputSpendStatus::Spent(new_txid.to_byte_array()); } } Ok(ApiReturn { secrets: Some(secrets_return), partial_tx: Some(TsUnsignedTransaction::new(unsigned_tx)), ..Default::default() }) } #[wasm_bindgen] pub fn sign_transaction(partial_tx: TsUnsignedTransaction) -> ApiResult { let local_device = lock_local_device()?; let partial_tweak = partial_tx.as_inner().partial_secret; let tx = internal_sign_tx(local_device.get_sp_client(), partial_tx.to_inner())?; let res = ApiReturn { new_tx_to_send: Some(NewTxMessage::new(serialize(&tx).to_lower_hex_string(), Some(partial_tweak.secret_bytes().to_lower_hex_string()))), ..Default::default() }; Ok(res) } #[wasm_bindgen] pub fn create_new_process( private_data: Pcd, roles: Roles, public_data: Pcd, relay_address: String, fee_rate: u32, members_list: OutPointMemberMap, ) -> ApiResult { // At the very least we should have something in role if roles.is_empty() { return Err(ApiError { message: "Roles can't be empty".to_owned() }); } // We create a transaction that spends to the relay address let local_device = lock_local_device()?; let mut freezed_utxos = lock_freezed_utxos()?; let relay_address: SilentPaymentAddress = relay_address.try_into()?; let fee_rate_checked = FeeRate::from_sat_per_vb(fee_rate as f32); let tx = create_transaction_for_addresses(&local_device, &freezed_utxos, &vec![relay_address], fee_rate_checked)?; let unsigned_transaction = SpClient::finalize_transaction(tx)?; // We take the secret out let new_secrets = get_shared_secrets_in_transaction(&unsigned_transaction, &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); } // We now have the outpoint that will serve as id for the whole process let process_id = OutPoint::new(unsigned_transaction.unsigned_tx.as_ref().unwrap().txid(), 0); let mut new_state = ProcessState::new(process_id, private_data.clone(), public_data.clone(), roles.clone())?; let pcd_commitment = new_state.pcd_commitment.clone(); let mut process = Process::new(process_id); let diffs = create_diffs(&local_device, &process, &new_state, &members_list)?; let mut encrypted_data = BTreeMap::new(); let mut rng = thread_rng(); for (field, plain_value) in private_data.iter() { let hash = pcd_commitment.get(field).ok_or(anyhow::Error::msg("Missing commitment"))?; let key = generate_key(&mut rng); let cipher = encrypt_with_key(&key, plain_value.as_slice())?; new_state.keys.insert(field.to_owned(), key); encrypted_data.insert(hash.to_lower_hex_string(), cipher.to_lower_hex_string()); } 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(&process_id) { return Err(ApiError::new("There's already a process for this outpoint".to_owned())); } processes.insert(process_id, process.clone()); } let commit_msg = CommitMessage::new( process_id, pcd_commitment, roles, public_data, vec![], ); let updated_process = UpdatedProcess { process_id, current_process: process, diffs, encrypted_data, ..Default::default() }; Ok(ApiReturn { secrets: Some(secrets_return), commit_to_send: Some(commit_msg), updated_process: Some(updated_process), partial_tx: Some(TsUnsignedTransaction::new(unsigned_transaction)), ..Default::default() }) } #[wasm_bindgen] pub fn update_process( mut process: Process, new_attributes: Pcd, roles: Roles, new_public_data: Pcd, members_list: OutPointMemberMap, ) -> ApiResult { let process_id = process.get_process_id()?; let prev_state = process.get_latest_commited_state() .ok_or(ApiError::new("Process must have at least one state already commited".to_owned()))?; let mut prev_public_data = prev_state.public_data.clone(); for (field, value) in new_public_data.into_iter() { prev_public_data.insert(field, value); } let mut new_state = ProcessState::new( process.get_process_tip()?, new_attributes.clone(), prev_public_data, roles.clone() )?; // TODO duplicate check is broken // // 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(&lock_local_device()?, &process, &new_state, &members_list)?; let all_fields: Vec = new_attributes.iter().map(|(field, _)| field.clone()).collect(); let mut encrypted_data = BTreeMap::new(); let mut rng = thread_rng(); for (field, plain_value) in new_attributes.iter() { let hash = new_state.pcd_commitment.get(field).ok_or(anyhow::Error::msg("Missing commitment"))?; let key = generate_key(&mut rng); let cipher = encrypt_with_key(&key, plain_value.as_slice())?; new_state.keys.insert(field.to_owned(), key); encrypted_data.insert(hash.to_lower_hex_string(), cipher.to_lower_hex_string()); } // Add the new state to the process process.insert_concurrent_state(new_state.clone())?; { // save the process to wasm memory let mut processes = lock_processes()?; // To keep it simple, assume that db is true and replace what we have in memory processes.insert(process_id, process.clone()); } let updated_process = UpdatedProcess { process_id, current_process: process, diffs, encrypted_data, ..Default::default() }; let commit_msg = CommitMessage::new( process_id, new_state.pcd_commitment, roles, new_state.public_data, vec![] ); Ok(ApiReturn { updated_process: Some(updated_process), commit_to_send: Some(commit_msg), ..Default::default() }) } #[wasm_bindgen] pub fn request_data(process_id: String, state_ids_str: Vec, roles: JsValue, members_list: OutPointMemberMap) -> ApiResult { let process_id = OutPoint::from_str(&process_id)?; let local_device = lock_local_device()?; let sender_pairing_id = local_device.get_pairing_commitment().ok_or(ApiError::new("Device not paired".to_owned()))?; let local_address = local_device.get_address().to_string(); let roles: Vec = serde_wasm_bindgen::from_value(roles)?; let mut state_ids: Vec<[u8; 32]> = vec![]; for s in state_ids_str { if (s.len() == 0 || s == String::from_utf8(Vec::from([0u8; 32])).unwrap()) { continue; } let state_id: Result<[u8; 32], _> = Vec::from_hex(&s)?.try_into().map_err(|_| ApiError::new("Invalid state id".to_owned())); if let Ok(state_id) = state_id { state_ids.push(state_id); } } let mut send_to: HashSet = HashSet::new(); for role in roles { for (_, role_def) in role { let pairing_ids = &role_def.members; if !pairing_ids.contains(&sender_pairing_id) { continue; } for pairing_id in pairing_ids { if let Some(member) = members_list.0.get(pairing_id) { for address in member.get_addresses() { if address == local_address { continue }; send_to.insert(SilentPaymentAddress::try_from(address)?); } } } } } let prd_request = Prd::new_request( process_id, members_list.0.get(&sender_pairing_id).unwrap().clone(), state_ids ); let prd_msg = prd_request.to_network_msg(local_device.get_sp_client())?; // For now, we just send the request to all members we share the data with, but this could be refined let shared_secrets = lock_shared_secrets()?; let mut ciphers = vec![]; for address in send_to { if let Some(secret) = shared_secrets.get_secret_for_address(address) { let cipher = encrypt_with_key(secret.as_byte_array(), prd_msg.as_bytes())?; ciphers.push(cipher.to_lower_hex_string()); } else { debug!("No shared secret"); } } Ok(ApiReturn { ciphers_to_send: ciphers, ..Default::default() }) } #[wasm_bindgen] pub fn create_update_message( process: Process, state_id: String, members_list: OutPointMemberMap ) -> ApiResult { let process_id = process.get_process_id()?; let state_id: [u8; 32] = Vec::from_hex(&state_id)?.try_into().map_err(|_| ApiError::new("Invalid state_id".to_owned()))?; let update_state = process.get_state_for_id(&state_id)?; let local_device = lock_local_device()?; let local_address = local_device.get_address().to_string(); let mut all_members: HashMap> = HashMap::new(); let shared_secrets = lock_shared_secrets()?; for (name, role) in update_state.roles.iter() { let fields: Vec = role .validation_rules .iter() .flat_map(|rule| rule.fields.clone()) .collect(); for pairing_id in &role.members { let member = if let Some(member) = members_list.0.get(pairing_id) { member } else { continue; }; // 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", pairing_id))); } } 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( process_id, sender, update_state.roles.clone(), update_state.public_data.clone(), 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(local_device.get_sp_client())?; 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()); } } Ok(ApiReturn { ciphers_to_send: ciphers, ..Default::default() }) } #[wasm_bindgen] pub fn validate_state(process: Process, state_id: String, members_list: OutPointMemberMap) -> ApiResult { add_validation_token(process, state_id, true, &members_list) } #[wasm_bindgen] pub fn refuse_state(process: Process, state_id: String, members_list: OutPointMemberMap) -> ApiResult { add_validation_token(process, state_id, false, &members_list) } #[wasm_bindgen] pub fn evaluate_state(process: Process, state_id: String, members_list: OutPointMemberMap) -> ApiResult { let state_id: [u8; 32] = Vec::from_hex(&state_id)?.try_into().map_err(|_| ApiError::new("Invalid state id".to_owned()))?; let process_id = process.get_process_id()?; let process_state = process.get_state_for_id(&state_id)?; let previous_state = process.get_parent_state(&process_state.commited_in); process_state.is_valid(previous_state, &members_list)?; // We create a commit msg with the valid state let commit_msg = CommitMessage::new( process_id, process_state.pcd_commitment.clone(), process_state.roles.clone(), process_state.public_data.clone(), vec![] ); Ok(ApiReturn { commit_to_send: Some(commit_msg), ..Default::default() }) } fn add_validation_token(mut process: Process, state_id: String, approval: bool, members_list: &OutPointMemberMap) -> ApiResult { let process_id = process.get_process_id()?; let state_id: [u8; 32] = Vec::from_hex(&state_id)?.try_into().map_err(|_| ApiError::new("Invalid state_id".to_owned()))?; let update_state: &mut ProcessState = process.get_state_for_id_mut(&state_id)?; let message_hash = if approval { AnkHash::ValidationYes(AnkValidationYesHash::from_merkle_root(state_id)) } else { AnkHash::ValidationNo(AnkValidationNoHash::from_merkle_root(state_id)) }; { let local_device = lock_local_device()?; let proof = Proof::new(message_hash, local_device.get_sp_client().get_spend_key().try_into()?); update_state.validation_tokens.push(proof); } let mut commit_msg = CommitMessage::new( process_id, update_state.pcd_commitment.clone(), update_state.roles.clone(), update_state.public_data.clone(), update_state.validation_tokens.clone() ); let updated_process = UpdatedProcess { process_id, current_process: process.clone(), validated_state: Some(state_id.clone()), ..Default::default() }; let ciphers_to_send = new_response_prd(process_id, process.get_state_for_id(&state_id)?, members_list)?; Ok(ApiReturn { updated_process: Some(updated_process), commit_to_send: Some(commit_msg), ciphers_to_send, ..Default::default() }) } #[wasm_bindgen] pub fn create_response_prd(process: Process, state_id: String, members_list: OutPointMemberMap) -> ApiResult { let process_id = process.get_process_id()?; let state_id: [u8; 32] = Vec::from_hex(&state_id)?.try_into().map_err(|_| ApiError::new("Invalid state_id".to_owned()))?; let update_state: &ProcessState = process.get_state_for_id(&state_id)?; let ciphers = new_response_prd(process_id, update_state, &members_list)?; Ok(ApiReturn { ciphers_to_send: ciphers, ..Default::default() }) } fn new_response_prd(process_id: OutPoint, update_state: &ProcessState, members_list: &OutPointMemberMap) -> AnyhowResult> { let local_device = lock_local_device()?; let local_address = local_device.get_address().to_string(); let mut all_members: HashMap> = HashMap::new(); let shared_secrets = lock_shared_secrets()?; for (name, role) in update_state.roles.iter() { let fields: Vec = role .validation_rules .iter() .flat_map(|rule| rule.fields.clone()) .collect(); for pairing_id in &role.members { let member = if let Some(member) = members_list.0.get(pairing_id) { member } else { continue; }; // 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(AnyhowError::msg(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(AnyhowError::msg("We haven't added our validation token yet".to_owned()))?; let sender: Member = local_device .to_member(); let response_prd = Prd::new_response( process_id, sender, vec![*proof], update_state.pcd_commitment.clone(), ); let prd_msg = response_prd.to_network_msg(local_device.get_sp_client())?; 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()?) .ok_or(AnyhowError::msg("Failed to retrieve secret".to_owned()))?; let cipher = encrypt_with_key(shared_secret.as_byte_array(), prd_msg.as_bytes())?; ciphers.push(cipher.to_lower_hex_string()); } } Ok(ciphers) } #[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_sp_client() .get_receiving_address() .to_string(); let faucet_msg = FaucetMessage::new(sp_address); Ok(faucet_msg.to_string()) } #[wasm_bindgen] pub fn get_storages(process_outpoint: String) -> ApiResult> { let outpoint = OutPoint::from_str(&process_outpoint)?; Ok(vec![]) } #[wasm_bindgen] pub fn is_child_role(parent_roles: String, child_roles: String) -> ApiResult<()> { let parent_roles: BTreeMap = serde_json::from_str(&parent_roles)?; let child_roles: BTreeMap = serde_json::from_str(&child_roles)?; for (_, child_role) in &child_roles { for child_member in &child_role.members { let mut is_in_parent = false; for (_, parent_role) in &parent_roles { if parent_role.members.contains(&child_member) { is_in_parent = true; } if is_in_parent { break } } if !is_in_parent { return Err(ApiError::new("child role contains a member not in parent".to_owned())); } } } Ok(()) } #[wasm_bindgen] pub fn decrypt_data(key: &[u8], data: &[u8]) -> ApiResult> { let mut key_buf = [0u8; 32]; if key.len() != 32 { return Err(ApiError::new("key must be 32B long".to_owned())); } key_buf.copy_from_slice(key); // decrypt the data let clear = decrypt_with_key(&key_buf, data)?; Ok(clear) } #[wasm_bindgen] pub fn encode_binary(data: JsValue) -> ApiResult { let map: BTreeMap = serde_wasm_bindgen::from_value(data)?; let res = TryInto::::try_into(map)?; Ok(res) } #[wasm_bindgen] pub fn encode_json(json_data: JsValue) -> ApiResult { let value: Value = serde_wasm_bindgen::from_value(json_data)?; let res = TryInto::::try_into(value)?; Ok(res) } #[wasm_bindgen] pub fn decode_value(value: Vec) -> ApiResult { // Try FileBlob first if let Ok(file_blob) = sdk_common::pcd::FileBlob::deserialize_from_pcd(&value) { let u8IntArray = Uint8Array::from(file_blob.data.as_slice()); let res = Object::new(); Reflect::set(&res, &JsValue::from_str("type"), &JsValue::from_str(&file_blob.r#type)).unwrap(); Reflect::set(&res, &JsValue::from_str("data"), &JsValue::from(u8IntArray)).unwrap(); return Ok(JsValue::from(res)); } // Try JSON next if let Ok(json) = serde_json::Value::deserialize_from_pcd(&value) { return Ok(serde_wasm_bindgen::to_value(&json)?); } Err(ApiError::new("Invalid or unsupported PCD data".to_owned())) } #[wasm_bindgen] pub fn hash_value(value: JsValue, commited_in: String, label: String) -> ApiResult { let outpoint = OutPoint::from_str(&commited_in)?; let encoded_value = if let Ok(file_blob) = serde_wasm_bindgen::from_value::(value.clone()) { file_blob.serialize_to_pcd()? } else if let Ok(json) = serde_wasm_bindgen::from_value::(value) { json.serialize_to_pcd()? } else { return Err(ApiError::new("Invalid or unsupported PCD data".to_owned())); }; let hash = AnkPcdHash::from_pcd_value(encoded_value.as_slice(), label.as_bytes(), &outpoint); Ok(hash.as_byte_array().to_lower_hex_string()) }