diff --git a/src/api.rs b/src/api.rs index 221156d..0043cc8 100644 --- a/src/api.rs +++ b/src/api.rs @@ -5,24 +5,31 @@ use std::io::Write; use std::ops::Index; use std::str::FromStr; use std::string::FromUtf8Error; -use std::sync::{Mutex, OnceLock, PoisonError}; +use std::sync::{Mutex, MutexGuard, OnceLock, PoisonError}; use std::time::{Duration, Instant}; +use std::u32; -use sdk_common::log::{debug, warn}; 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}; +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::{ - AeadCore, Aes256Decryption, Aes256Encryption, Aes256Gcm, AnkSharedSecret, KeyInit, Purpose, + AeadCore, Aes256Decryption, Aes256Encryption, Aes256Gcm, AnkSharedSecret, AnkSharedSecretHash, + KeyInit, Purpose, AAD, }; use sdk_common::process::Process; -use sdk_common::signature; use sdk_common::sp_client::bitcoin::blockdata::fee_rate; use sdk_common::sp_client::bitcoin::consensus::{deserialize, serialize}; -use sdk_common::sp_client::bitcoin::hashes::HashEngine; use sdk_common::sp_client::bitcoin::hashes::{sha256, Hash}; +use sdk_common::sp_client::bitcoin::hashes::{FromSliceError, HashEngine}; use sdk_common::sp_client::bitcoin::hex::{ - parse, DisplayHex, FromHex, HexToArrayError, HexToBytesError, + self, parse, DisplayHex, FromHex, HexToArrayError, HexToBytesError, }; use sdk_common::sp_client::bitcoin::key::{Parity, Secp256k1}; use sdk_common::sp_client::bitcoin::network::ParseNetworkError; @@ -43,6 +50,7 @@ use sdk_common::sp_client::silentpayments::{ Error as SpError, }; use sdk_common::uuid::Uuid; +use sdk_common::{signature, MAX_PRD_PAYLOAD_SIZE}; use serde_json::{Error as SerdeJsonError, Map, Value}; use serde::{Deserialize, Serialize}; @@ -50,20 +58,35 @@ 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, Envelope, FaucetMessage, NewTxMessage}; -use sdk_common::pcd::{AnkPcdHash, Member, Pcd, RoleDefinition, ValidationRule}; + self, AnkFlag, CachedMessage, CachedMessageStatus, CommitMessage, Envelope, FaucetMessage, + NewTxMessage, +}; +use sdk_common::pcd::{compare_maps, AnkPcdHash, Member, Pcd, RoleDefinition, ValidationRule}; use sdk_common::prd::{AnkPrdHash, Prd, PrdType}; use sdk_common::silentpayments::{create_transaction, map_outputs_to_sp_address}; use sdk_common::sp_client::spclient::{ derive_keys_from_seed, OutputList, OutputSpendStatus, OwnedOutput, Recipient, SpClient, }; use sdk_common::sp_client::spclient::{SpWallet, SpendKey}; -use sdk_common::device::Device; use crate::user::{lock_local_device, set_new_device, LOCAL_DEVICE}; -use crate::{lock_messages, lock_processes, ProcessState, ProcessStatus, RelevantProcess, CACHEDMESSAGES, CACHEDPROCESSES}; use crate::wallet::{generate_sp_wallet, lock_freezed_utxos}; +use crate::{ + lock_messages, lock_processes, ProcessState, RelevantProcess, CACHEDMESSAGES, CACHEDPROCESSES, +}; + +#[derive(Debug, PartialEq, Tsify, Serialize, Deserialize, Default)] +#[tsify(into_wasm_abi, from_wasm_abi)] +#[allow(non_camel_case_types)] +pub struct ApiReturn { + pub updated_cached_msg: Vec, + pub updated_process: Option<(String, RelevantProcess)>, + pub new_tx_to_send: Option, + pub ciphers_to_send: Vec, + pub commit_to_send: Option, +} pub type ApiResult = Result; @@ -75,99 +98,87 @@ 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 { - message: value.to_string(), - } + ApiError::new(value.to_string()) } } impl From for ApiError { fn from(value: SpError) -> Self { - ApiError { - message: value.to_string(), - } + 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 { - message: value.to_string(), - } + ApiError::new(value.to_string()) } } impl From for ApiError { fn from(value: HexToBytesError) -> Self { - ApiError { - message: value.to_string(), - } + ApiError::new(value.to_string()) } } impl From for ApiError { fn from(value: HexToArrayError) -> Self { - ApiError { - message: value.to_string(), - } + ApiError::new(value.to_string()) } } impl From for ApiError { fn from(value: sdk_common::sp_client::bitcoin::psbt::PsbtParseError) -> Self { - ApiError { - message: value.to_string(), - } + ApiError::new(value.to_string()) } } impl From for ApiError { fn from(value: sdk_common::sp_client::bitcoin::psbt::ExtractTxError) -> Self { - ApiError { - message: value.to_string(), - } + ApiError::new(value.to_string()) } } impl From for ApiError { fn from(value: sdk_common::sp_client::bitcoin::secp256k1::Error) -> Self { - ApiError { - message: value.to_string(), - } + ApiError::new(value.to_string()) } } impl From for ApiError { fn from(value: sdk_common::sp_client::bitcoin::consensus::encode::Error) -> Self { - ApiError { - message: value.to_string(), - } + ApiError::new(value.to_string()) } } impl From for ApiError { fn from(value: FromUtf8Error) -> Self { - ApiError { - message: value.to_string(), - } + ApiError::new(value.to_string()) } } impl From for ApiError { fn from(value: ParseNetworkError) -> Self { - ApiError { - message: value.to_string(), - } + ApiError::new(value.to_string()) } } impl From for ApiError { fn from(value: ParseOutPointError) -> Self { - ApiError { - message: value.to_string(), - } + ApiError::new(value.to_string()) } } @@ -222,20 +233,28 @@ pub fn create_new_device(birthday: u32, network_str: String) -> ApiResult) -> ApiResult<()> { +pub fn pair_device(commitment_tx: String, mut sp_addresses: Vec) -> ApiResult<()> { let mut local_device = lock_local_device()?; if local_device.is_linked() { - return Err(ApiError { - message: "Already paired".to_owned(), - }); + return Err(ApiError::new("Already paired".to_owned())); } - sp_addresses.push(local_device.get_wallet().get_client().get_receiving_address()); + sp_addresses.push( + local_device + .get_wallet() + .get_client() + .get_receiving_address(), + ); local_device.pair( - Uuid::parse_str(&uuid).unwrap(), - Member::new(sp_addresses.into_iter().map(|a| TryInto::::try_into(a).unwrap()).collect())? + OutPoint::from_str(&commitment_tx)?.txid, + Member::new( + sp_addresses + .into_iter() + .map(|a| TryInto::::try_into(a).unwrap()) + .collect(), + )?, ); Ok(()) @@ -252,6 +271,11 @@ impl outputs_list { } } +#[wasm_bindgen] +pub fn login() -> ApiResult<()> { + unimplemented!(); +} + #[wasm_bindgen] pub fn logout() -> ApiResult<()> { unimplemented!(); @@ -264,6 +288,48 @@ pub fn dump_wallet() -> ApiResult { 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: HashMap = serde_json::from_str(&processes)?; + + let mut cached_processes = lock_processes()?; + + *cached_processes = processes; + + Ok(()) +} + #[wasm_bindgen] pub fn reset_message_cache() -> ApiResult<()> { let mut cached_msg = lock_messages()?; @@ -280,9 +346,7 @@ pub fn set_message_cache(msg_cache: Vec) -> ApiResult<()> { let mut cached_msg = lock_messages()?; if !cached_msg.is_empty() { - return Err(ApiError { - message: "Message cache not empty".to_owned(), - }); + return Err(ApiError::new("Message cache not empty".to_owned())); } let new_cache: Result, serde_json::Error> = @@ -319,35 +383,44 @@ pub fn reset_device() -> ApiResult<()> { *device = Device::default(); reset_message_cache()?; + reset_process_cache()?; Ok(()) } +#[wasm_bindgen] +pub fn get_txid(transaction: String) -> ApiResult { + let tx: Transaction = deserialize(&Vec::from_hex(&transaction)?)?; + + Ok(tx.txid().to_string()) +} + fn handle_transaction( updated: HashMap, tx: &Transaction, tweak_data: PublicKey, -) -> anyhow::Result { +) -> AnyhowResult { let device = lock_local_device()?; let sp_wallet = device.get_wallet(); - let op_return = tx.output.iter().find(|o| o.script_pubkey.is_op_return()); - let mut commitment = [0u8; 32]; - if op_return.is_some() { - commitment.copy_from_slice(&op_return.unwrap().script_pubkey.as_bytes()[2..]); - }; - let commitment_str = commitment.to_lower_hex_string(); + 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..])?; - // If we got updates from a transaction, it means that it creates an output to us, spend an output we owned, or both // Basically a transaction that destroyed utxo is a transaction we sent. let utxo_destroyed: HashMap<&OutPoint, &OwnedOutput> = updated .iter() .filter(|(outpoint, output)| output.spend_status != OutputSpendStatus::Unspent) .collect(); - let utxo_created: HashMap<&OutPoint, &OwnedOutput> = updated - .iter() - .filter(|(outpoint, output)| output.spend_status == OutputSpendStatus::Unspent) - .collect(); let mut messages = lock_messages()?; @@ -372,33 +445,81 @@ fn handle_transaction( return false; } }) { - let (outpoint, output) = utxo_created.into_iter().next().unwrap(); + let prd = Prd::extract_from_message_with_commitment(&plaintext, &commitment)?; - let prd = Prd::extract_from_message(&plaintext, commitment)?; + let proof_key = prd.proof.unwrap().get_key(); + let mut actual_sender = String::default(); + for sp_address in serde_json::from_str::(&prd.sender)?.get_addresses() { + if proof_key + == SilentPaymentAddress::try_from(sp_address.as_str()) + .unwrap() + .get_spend_key() + .x_only_public_key() + .0 + { + actual_sender = sp_address; + } else { + continue; + } + break; + } - message.prd = Some(prd.to_string()); - message.shared_secrets.push(shared_secret.to_byte_array().to_lower_hex_string()); - message.commitment = Some(commitment_str); - message.sender = Some(serde_json::from_str(&prd.sender)?); - message.status = CachedMessageStatus::Opened; + let outpoint = OutPoint::from_str(&prd.root_commitment)?; - return Ok(message.clone()); + let updated_process: RelevantProcess; + if let Some(process) = lock_processes()?.get_mut(&outpoint) { + process.shared_secrets.insert( + actual_sender, + shared_secret.to_byte_array().to_lower_hex_string(), + ); + updated_process = process.clone(); + } else { + let mut shared_secrets = HashMap::new(); + shared_secrets.insert( + actual_sender, + shared_secret.to_byte_array().to_lower_hex_string(), + ); + let new_process = RelevantProcess { + shared_secrets, + ..Default::default() + }; + lock_processes()?.insert(outpoint, new_process.clone()); + updated_process = new_process; + } + + // We don't need the cached message anymore + let id_to_rm = message.id; + *message = CachedMessage { + id: id_to_rm, + ..Default::default() + }; + + return Ok(ApiReturn { + updated_cached_msg: vec![message.clone()], + updated_process: Some((outpoint.to_string(), updated_process)), + ..Default::default() + }); } else { // store it and wait for the message let mut new_msg = CachedMessage::new(); - let (outpoint, output) = utxo_created - .into_iter() - .next() - .expect("utxo_created shouldn't be empty"); - new_msg.commitment = Some(commitment.to_lower_hex_string()); - new_msg.shared_secrets.push(shared_secret.to_byte_array().to_lower_hex_string()); + new_msg.commitment = Some(commitment.as_byte_array().to_lower_hex_string()); + new_msg + .shared_secrets + .push(shared_secret.to_byte_array().to_lower_hex_string()); new_msg.status = CachedMessageStatus::TxWaitingPrd; + messages.push(new_msg.clone()); - return Ok(new_msg.clone()); + + return Ok(ApiReturn { + updated_cached_msg: vec![new_msg.clone()], + ..Default::default() + }); } } else { // We're sender of the transaction, do nothing - return Ok(CachedMessage::new()); + return Ok(ApiReturn { + ..Default::default() + }); } } @@ -408,7 +529,7 @@ fn process_transaction( tx_hex: String, blockheight: u32, tweak_data_hex: String, -) -> anyhow::Result> { +) -> anyhow::Result { let tx = deserialize::(&Vec::from_hex(&tx_hex)?)?; let tweak_data = PublicKey::from_str(&tweak_data_hex)?; @@ -421,113 +542,492 @@ fn process_transaction( } if updated.len() > 0 { - let updated_msg = handle_transaction(updated, &tx, tweak_data)?; - return Ok(Some(updated_msg)); + return handle_transaction(updated, &tx, tweak_data); + } + + Err(anyhow::Error::msg("Transaction is not our")) +} + +#[wasm_bindgen] +pub fn parse_new_tx(new_tx_msg: String, block_height: u32, fee_rate: u32) -> ApiResult { + let new_tx: NewTxMessage = serde_json::from_str(&new_tx_msg)?; + + if let Some(error) = new_tx.error { + return Err(ApiError::new(format!( + "NewTx returned with an error: {}", + error + ))); + } + + if new_tx.tweak_data.is_none() { + return Err(ApiError::new("Missing tweak_data".to_owned())); + } + + Ok(process_transaction( + new_tx.transaction, + block_height, + new_tx.tweak_data.unwrap(), + )?) +} + +fn try_decrypt_with_processes( + cipher: &[u8], + processes: MutexGuard>, +) -> Option<(Vec, SilentPaymentAddress, OutPoint)> { + let nonce = Nonce::from_slice(&cipher[..12]); + + for (outpoint, process) in processes.iter() { + for (address, secret) in &process.shared_secrets { + let aes_key = match AnkSharedSecretHash::from_str(secret) { + Ok(key) => key, + Err(_) => continue, + }; + + let engine = Aes256Gcm::new(aes_key.as_byte_array().into()); + if let Ok(plain) = engine.decrypt( + &nonce, + Payload { + msg: &cipher[12..], + aad: AAD, + }, + ) { + return Some(( + plain, + SilentPaymentAddress::try_from(address.as_str()).unwrap(), + *outpoint, + )); + } + } + } + None +} + +fn confirm_prd(prd: Prd, shared_secret: &str) -> AnyhowResult { + match prd.prd_type { + PrdType::Confirm | PrdType::Response | PrdType::List => { + return Err(AnyhowError::msg("Invalid prd type")); + } + _ => (), + } + + let outpoint = OutPoint::from_str(&prd.root_commitment)?; + + let local_device = lock_local_device()?; + let member = match local_device.to_member() { + Some(member) => member, + None => { + // This might be because we're pairing, let's see if our address is part of sender of the initial prd + let remote_member: Member = serde_json::from_str(&prd.sender)?; + let addresses = remote_member.get_addresses(); + let this_device_address = local_device + .get_wallet() + .get_client() + .get_receiving_address(); + if let Some(_) = addresses.into_iter().find(|a| *a == this_device_address) { + remote_member + } else { + return Err(AnyhowError::msg("Must pair device first")); + } + } + }; + + let payload = Value::from_str(&prd.payload)?; + + let prd_confirm = Prd::new_confirm(outpoint, member, payload.tagged_hash()); + + let prd_msg = prd_confirm.to_network_msg(local_device.get_wallet())?; + + debug!("encrypting with key {}", shared_secret); + Ok(encrypt_with_key(prd_msg, shared_secret.to_owned()).unwrap()) +} + +fn send_data(prd: &Prd, shared_secret: &str) -> AnyhowResult { + let pcd = &prd.payload; + + debug!("encrypting with key: {:#?}", shared_secret); + let cipher = encrypt_with_key(pcd.clone(), shared_secret.to_owned()).unwrap(); + + Ok(ApiReturn { + ciphers_to_send: vec![cipher], + ..Default::default() + }) +} + +fn decrypt_with_cached_messages( + cipher: &[u8], +) -> anyhow::Result, AnkSharedSecretHash)>> { + let mut messages = lock_messages()?; + + let nonce = Nonce::from_slice(&cipher[..12]); + + for message in messages.iter_mut() { + debug!("Attempting decryption with cached message {:#?}", message); + for shared_secret in message.shared_secrets.iter() { + let aes_key = match AnkSharedSecretHash::from_str(shared_secret) { + Ok(key) => key, + Err(_) => { + debug!( + "Invalid shared secret for message{}: {}", + message.id, shared_secret + ); + continue; + } + }; + + let engine = Aes256Gcm::new(aes_key.as_byte_array().into()); + + let plain = match engine.decrypt( + &nonce, + Payload { + msg: &cipher[12..], + aad: AAD, + }, + ) { + Ok(plain) => plain, + Err(_) => continue, + }; + + let commitment = AnkPrdHash::from_str( + message + .commitment + .as_ref() + .ok_or(anyhow::Error::msg("Missing commitment".to_owned()))?, + )?; + // A message matched against a new transaction must be a prd + // We just check the commitment while we're at it + let _ = Prd::extract_from_message_with_commitment(&plain, &commitment)?; + // Update the message status + message.status = CachedMessageStatus::NoStatus; + + return Ok(Some(( + plain, + AnkSharedSecretHash::from_str(message.shared_secrets.first().unwrap())?, + ))); + } } Ok(None) } -#[wasm_bindgen] -pub fn parse_new_tx(new_tx_msg: String, block_height: u32, fee_rate: u32) -> ApiResult> { - let new_tx: NewTxMessage = serde_json::from_str(&new_tx_msg)?; +fn decrypt_with_known_processes(cipher: &[u8]) -> anyhow::Result, OutPoint)>> { + let processes = lock_processes()?; - if let Some(error) = new_tx.error { - return Err(ApiError { - message: format!("NewTx returned with an error: {}", error), - }); + debug!("Known processes: {:#?}", processes); + + let nonce = Nonce::from_slice(&cipher[..12]); + + for (outpoint, process) in processes.iter() { + for (address, secret) in &process.shared_secrets { + debug!("Attempting decryption with key {} for {}", secret, address); + let aes_key = match AnkSharedSecretHash::from_str(secret) { + Ok(key) => key, + Err(_) => { + debug!("Invalid shared secret for process {}: {}", outpoint, secret); + continue; + } + }; + + let engine = Aes256Gcm::new(aes_key.as_byte_array().into()); + + if let Ok(plain) = engine.decrypt( + &nonce, + Payload { + msg: &cipher[12..], + aad: AAD, + }, + ) { + return Ok(Some((plain, *outpoint))); + } + } } - - if new_tx.tweak_data.is_none() { - return Err(ApiError { - message: "Missing tweak_data".to_owned(), - }); - } - - let msg = - process_transaction(new_tx.transaction, block_height, new_tx.tweak_data.unwrap())?; - - Ok(msg) + Ok(None) } -#[wasm_bindgen] -pub fn parse_cipher(cipher_msg: String, fee_rate: u32) -> ApiResult { - // let's try to decrypt with keys we found in transactions but haven't used yet - let mut messages = lock_messages()?; - let cipher = Vec::from_hex(&cipher_msg.trim_matches('\"'))?; - let mut plain = vec![]; - if let Some(message) = messages.iter_mut().find(|m| match m.status { - CachedMessageStatus::TxWaitingPrd | CachedMessageStatus::Opened => { - if let Ok(m) = m.try_decrypt_message(cipher.clone()) { - plain = m; - return true; - } else { return false } - } - _ => return false, - }) { - if message.status == CachedMessageStatus::TxWaitingPrd { - // debug!("Found message {}", String::from_utf8(plain.clone())?); - let mut commitment = [0u8; 32]; - commitment.copy_from_slice(&Vec::from_hex(message.commitment.as_ref().unwrap())?); - let prd = Prd::extract_from_message(&plain, commitment)?; +/// Prd can come with an attached transaction or without +/// It's useful to commit it in a transaction in the case there are multiple recipients (guarantee that everyone gets the same payload) +/// Or if for some reasons we don't want to use the same shared secret again +fn handle_prd( + plain: &[u8], + new_shared_secret: Option, +) -> AnyhowResult { + // We already checked the commitment if any + let prd = Prd::extract_from_message(plain)?; - message.prd = Some(prd.to_string()); - message.sender = Some(serde_json::from_str(&prd.sender)?); - message.status = CachedMessageStatus::Opened; - } else { - // debug!("Found message {}", String::from_utf8(plain.clone())?); - // we're receiving a pcd for a prd we already have - let mut pcd = Value::from_str(&String::from_utf8(plain)?)?; - // check that the hash of the pcd is the same than commited in the prd - let pcd_commitment = AnkPcdHash::from_value(&pcd); - let prd: Prd = serde_json::from_str(message.prd.as_ref().unwrap()).unwrap(); - if pcd_commitment.to_string() != prd.pcd_commitment { - return Err(ApiError { - message: format!("Pcd doesn't match commitment: expected {:?}\ngot {}", prd.pcd_commitment, pcd_commitment), - }); + debug!("found prd: {:#?}", prd); + + let proof_key = prd.proof.unwrap().get_key(); + + let sp_address = serde_json::from_str::(&prd.sender)? + .get_addresses() + .into_iter() + .find_map(|address| { + let parsed_address = SilentPaymentAddress::try_from(address.as_str()).ok()?; + let spend_key = parsed_address.get_spend_key().x_only_public_key().0; + if spend_key == proof_key { + Some(parsed_address) + } else { + None } - message.pcd = Some(pcd.clone()); + }) + .ok_or_else(|| anyhow::Error::msg("No matching address found for the proof key"))?; - // Now we decrypt all we can from the pcd - pcd.decrypt_fields(&prd.keys)?; - - // we take a few mandatory informations - let process = pcd["process"].take(); - let uuid = Uuid::parse_str(&prd.process_uuid).expect("prd can't have an invalid uuid"); - // todo check that the uuid in the process we took from pcd is consistent - - // we complete the process we keep in storage - let mut processes = lock_processes()?; - - match prd.prd_type { - PrdType::Init => { - let relevant_process = RelevantProcess { - process: serde_json::from_value(process)?, - states: vec![ProcessState { - commited_in: OutPoint::null(), // At this point process is not commited yet - encrypted_pcd: message.pcd.clone().unwrap(), - keys: prd.keys.clone(), - validation_token: vec![], - }], - current_status: ProcessStatus::Active(message.shared_secrets.clone()) - }; - processes.insert(uuid, relevant_process); - }, - _ => unimplemented!() - } + let outpoint = OutPoint::from_str(&prd.root_commitment)?; + let mut processes = lock_processes()?; + let relevant_process = match processes.entry(outpoint) { + std::collections::hash_map::Entry::Occupied(entry) => entry.into_mut(), + std::collections::hash_map::Entry::Vacant(entry) => { + debug!("Creating new process for outpoint: {}", outpoint); + let shared_secret = new_shared_secret + .ok_or_else(|| anyhow::Error::msg("Missing shared secret for new process"))?; + let mut shared_secrets = HashMap::new(); + shared_secrets.insert( + sp_address.to_string(), + shared_secret.to_byte_array().to_lower_hex_string(), + ); + entry.insert(RelevantProcess { + shared_secrets, + ..Default::default() + }) } - return Ok(message.clone()); - } else { - // let's keep it in case we receive the transaction later - let mut new_msg = CachedMessage::new(); - new_msg.status = CachedMessageStatus::CipherWaitingTx; - new_msg.cipher.push(cipher_msg); - messages.push(new_msg.clone()); - return Ok(new_msg); + }; + + match prd.prd_type { + PrdType::Confirm => { + // It must match a prd we sent previously + // We send the whole data in a pcd + debug!("Received confirm prd {:#?}", prd); + let original_request = relevant_process + .impending_requests + .iter() + .find(|r| { + if r.prd_type != PrdType::Update { + return false; + } + let hash = Value::from_str(&r.payload).unwrap().tagged_hash(); + hash.to_string() == prd.payload + }) + .ok_or(anyhow::Error::msg("Original request not found"))?; + let shared_secret = relevant_process + .shared_secrets + .get(&sp_address.to_string()) + .ok_or(anyhow::Error::msg( + "Missing shared secret for address in original request", + ))?; + + return send_data(original_request, shared_secret); + } + PrdType::Update | PrdType::TxProposal | PrdType::Message => { + // Those all have some new data we don't know about yet + // We send a Confirm to get the pcd + // Add the prd to our list of actions for this process + relevant_process.impending_requests.push(prd.clone()); + let shared_secret = relevant_process + .shared_secrets + .get(&sp_address.to_string()) + .ok_or(anyhow::Error::msg( + "Missing shared secret for address in original request", + ))?; + + let cipher = confirm_prd(prd, shared_secret)?; + + return Ok(ApiReturn { + ciphers_to_send: vec![cipher], + updated_process: Some((outpoint.to_string(), relevant_process.clone())), + ..Default::default() + }); + } + _ => unimplemented!(), } } +fn handle_pcd(plain: Vec, root_commitment: OutPoint) -> AnyhowResult { + let pcd = Value::from_str(&String::from_utf8(plain)?)?; + + // debug!("Found pcd: {:#?}", pcd); + let pcd_commitment = pcd.tagged_hash(); + + let mut processes = lock_processes()?; + let relevant_process = processes.get_mut(&root_commitment).unwrap(); + + // We match the pcd with a prd and act accordingly + let prd = relevant_process + .impending_requests + .iter_mut() + .find(|r| Value::from_str(&r.payload).unwrap().tagged_hash() == pcd_commitment) + .ok_or(AnyhowError::msg("Failed to retrieve the matching prd"))?; + + // We update the process and return it + prd.payload = pcd.to_string(); + + debug!("New state of the process: {:#?}", prd); + + return Ok(ApiReturn { + updated_process: Some((root_commitment.to_string(), relevant_process.clone())), + ..Default::default() + }); +} + +fn handle_decrypted_message( + plain: Vec, + shared_secret: Option, + root_commitment: Option, +) -> anyhow::Result { + // Try to handle as PRD first + handle_prd(&plain, shared_secret) + .or_else(|_| { + // If PRD handling fails, try to handle as PCD + handle_pcd( + plain, + root_commitment.ok_or(anyhow::Error::msg( + "root_commitment must be known for a pcd", + ))?, + ) + }) + .map_err(|e| anyhow::Error::msg(format!("Failed to handle decrypted message: {}", e))) +} + +// fn match_with_active_processes(cipher: &[u8]) -> anyhow::Result { +// // Try decrypting the cipher with available processes and shared secrets +// let (plain, sp_address, process_root_commitment) = +// try_decrypt_with_processes(cipher, lock_processes()?) +// .ok_or_else(|| anyhow::Error::msg("No match in active processes"))?; + +// debug!("matched with an active process"); + +// // Try to handle the decrypted payload as PRD first +// if let Ok(res) = handle_prd(&plain, sp_address, None) { +// return Ok(res); +// } + +// // If not PRD, try to handle as PCD +// if let Ok(res) = handle_pcd(plain, sp_address, process_root_commitment) { +// return Ok(ApiReturn { +// updated_process: Some((process_root_commitment.to_string(), res)), +// ..Default::default() +// }); +// } + +// // If neither PRD nor PCD, return an error +// Err(anyhow::Error::msg("payload can't be parsed")) +// } + +// /// Go through cached messages for tx waiting for a prd +// /// We update the process so that the +// fn match_cipher_to_cached_messages(cipher: &[u8]) -> anyhow::Result { +// // let's try to decrypt with keys we found in transactions but haven't used yet +// let mut messages = lock_messages()?; +// let mut plain = vec![]; +// // debug!("messages: {:?}", messages); +// if let Some(message) = messages.iter_mut().find(|m| match m.status { +// CachedMessageStatus::TxWaitingPrd => { +// if let Ok(m) = m.try_decrypt_message(&cipher) { +// plain = m; +// return true; +// } else { +// return false; +// } +// } +// _ => return false, +// }) { +// // debug!("Found message {}", String::from_utf8(plain.clone())?); +// let commitment = AnkPrdHash::from_str(message.commitment.as_ref().unwrap())?; +// let prd = Prd::extract_from_message_with_commitment(&plain, &commitment)?; + +// let proof_key = prd.proof.unwrap().get_key(); +// let mut actual_sender = String::default(); +// for sp_address in serde_json::from_str::(&prd.sender)?.get_addresses() { +// if proof_key +// == SilentPaymentAddress::try_from(sp_address.as_str()) +// .unwrap() +// .get_spend_key() +// .x_only_public_key() +// .0 +// { +// actual_sender = sp_address; +// break; +// } else { +// continue; +// } +// } + +// let shared_secret = message.shared_secrets.first().unwrap(); +// let root_outpoint = OutPoint::from_str(&prd.root_commitment)?; + +// let mut processes = lock_processes()?; + +// let updated_process: RelevantProcess; +// if let Some(process) = processes.get_mut(&root_outpoint) { +// // we're actually replacing the shared_secret for that process and that sender if it exists +// process.shared_secrets.insert( +// actual_sender, +// Vec::from_hex(shared_secret)?.to_lower_hex_string(), +// ); +// updated_process = process.clone(); +// } else { +// let mut shared_secrets = HashMap::new(); +// shared_secrets.insert( +// actual_sender, +// Vec::from_hex(shared_secret)?.to_lower_hex_string(), +// ); +// let new_process = RelevantProcess { +// shared_secrets, +// ..Default::default() +// }; +// processes.insert(root_outpoint, new_process.clone()); +// updated_process = new_process; +// } + +// return Ok(ApiReturn { +// updated_cached_msg: vec![CachedMessage { +// id: message.id, +// ..Default::default() +// }], +// updated_process: Some((root_outpoint.to_string(), updated_process)), +// ..Default::default() +// }); +// } else { +// // let's keep it in case we receive the transaction later +// let mut new_msg = CachedMessage::new(); +// new_msg.status = CachedMessageStatus::CipherWaitingTx; +// new_msg.cipher.push(cipher.to_lower_hex_string()); +// messages.push(new_msg.clone()); +// return Ok(ApiReturn { +// updated_cached_msg: vec![new_msg], +// ..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('"'))?; + + // Try decrypting with cached messages first + if let Ok(Some((plain, shared_secret))) = decrypt_with_cached_messages(&cipher) { + return handle_decrypted_message(plain, Some(shared_secret), None) + .map_err(|e| ApiError::new(format!("Failed to handle decrypted message: {}", e))); + } + + // If that fails, try decrypting with known processes + if let Ok(Some((plain, root_commitment))) = decrypt_with_known_processes(&cipher) { + return handle_decrypted_message(plain, None, Some(root_commitment)) + .map_err(|e| ApiError::new(format!("Failed to handle decrypted message: {}", e))); + } + + // If both decryption attempts fail, return an error + Err(ApiError::new( + "Failed to decrypt cipher with any known method".to_owned(), + )) +} + #[wasm_bindgen] pub fn get_outputs() -> ApiResult { let device = lock_local_device()?; @@ -543,40 +1043,128 @@ pub fn get_available_amount() -> ApiResult { } #[wasm_bindgen] -pub fn create_process_from_template(json: String) -> ApiResult { - let template_process: Process = serde_json::from_str(&json)?; - let mut new_process = Process::new( - template_process.html, - template_process.style, - template_process.script, - template_process.init_state, - template_process.commited_in - ); +/// This takes some commitment and creates a commit msg so that relay commits it on chain +pub fn create_commit_message( + to_commit: String, + relay_address: String, + init_commitment_outpoint: Option, // if None, we're initializing a new commitment chain + fee_rate: u32, +) -> ApiResult { + let value = Value::from_str(&to_commit)?; + let commitment = value.tagged_hash(); - Ok(new_process) -} + // if the transaction exists, we don't need to create a transaction and just put the outpoint in the msg + if let Some(outpoint) = init_commitment_outpoint { + // Todo : if we have an init tx, look into cached processes and confirm that it exists + // We just send the message with the outpoint + return Ok(ApiReturn { + commit_to_send: Some(CommitMessage::new_update_commitment( + OutPoint::from_str(&outpoint)?, + 0, + commitment.to_byte_array(), + )), + ..Default::default() + }); + } else { + let freezed_utxos = lock_freezed_utxos()?; -#[derive(Debug, Tsify, Serialize, Deserialize, Default)] -#[tsify(into_wasm_abi, from_wasm_abi)] -#[allow(non_camel_case_types)] -pub struct createTransactionReturn { - pub txid: String, - pub transaction: String, - pub new_messages: Vec + let local_device = lock_local_device()?; + + let sp_wallet = local_device.get_wallet(); + + let signed_psbt = create_transaction( + &vec![], + &freezed_utxos, + sp_wallet, + vec![Recipient { + address: relay_address, + amount: Amount::from_sat(1000), + nb_outputs: 1, + }], + None, + Amount::from_sat(fee_rate.into()), + None, + )?; + + let tx = signed_psbt.extract_tx()?; + + Ok(ApiReturn { + commit_to_send: Some(CommitMessage::new_first_commitment( + tx, + 0, + commitment.to_byte_array(), + )), + ..Default::default() + }) + } } #[wasm_bindgen] -pub fn create_process_init_transaction( - mut new_process: Process, +/// We assume that the provided tx outpoint exist +pub fn create_update_transaction( + init_commitment: Option, + new_state: String, fee_rate: u32, -) -> ApiResult { - let pcd = new_process.init_state; - let roles = pcd.get("roles").ok_or(ApiError { message: "No roles in init_state".to_owned()})?; - let roles_map = roles.as_object().ok_or(ApiError { message: "roles is not an object".to_owned()})?.clone(); +) -> ApiResult { + let pcd = Value::from_str(&new_state)?; + let pcd_map = pcd + .as_object() + .ok_or(ApiError::new("new_state must be an object".to_owned()))?; + + let mut processes = lock_processes()?; + + let commitment_outpoint: OutPoint; + let relevant_process: &mut RelevantProcess; + if let Some(s) = init_commitment { + // We're updating an existing contract + let outpoint = OutPoint::from_str(&s)?; + + if let Some(p) = processes.get_mut(&outpoint) { + // compare the provided new_state with the process defined template + let previous_state = &p.states.first().unwrap().encrypted_pcd; + if !compare_maps(previous_state.as_object().unwrap(), pcd_map) { + return Err(ApiError::new( + "Provided updated state is not consistent with the process template".to_owned(), + )); + } + relevant_process = p; + commitment_outpoint = outpoint; + } else { + // This is a process we don't know about, so we insert a new entry + processes.insert(outpoint, RelevantProcess::default()); + relevant_process = processes.get_mut(&outpoint).unwrap(); + commitment_outpoint = outpoint; + } + } else { + // This is a creation with an init state, the commitment will come later + // We need a placeholder to keep track of the process before it's commited on chain + // We can take the hash of the init_state as a txid, and set the vout to the max as it is very unlikely to ever have a real commitment that will look like this + let dummy = pcd.tagged_hash(); + + let dummy_outpoint = OutPoint::new(Txid::from_slice(dummy.as_byte_array())?, u32::MAX); + + processes.insert(dummy_outpoint, RelevantProcess::default()); + + relevant_process = processes.get_mut(&dummy_outpoint).unwrap(); + commitment_outpoint = dummy_outpoint; + } + + // We assume that all processes must have a roles key + let roles = pcd + .get("roles") + .ok_or(ApiError::new("No roles in new_state".to_owned()))?; + let roles_map = roles + .as_object() + .ok_or(ApiError::new("roles is not an object".to_owned()))? + .clone(); let mut all_members: HashMap> = HashMap::new(); for (name, role_def) in roles_map { let role: RoleDefinition = serde_json::from_str(&role_def.to_string())?; - let fields: Vec = role.validation_rules.iter().flat_map(|rule| rule.fields.clone()).collect(); + let fields: Vec = role + .validation_rules + .iter() + .flat_map(|rule| rule.fields.clone()) + .collect(); for member in role.members { if !all_members.contains_key(&member) { all_members.insert(member.clone(), HashSet::new()); @@ -587,14 +1175,14 @@ pub fn create_process_init_transaction( let nb_recipients = all_members.len(); if nb_recipients == 0 { - return Err(ApiError { - message: "Can't create a process with 0 member".to_owned(), - }); + return Err(ApiError::new( + "Can't create a process with 0 member".to_owned(), + )); } - let mut recipients: Vec = Vec::with_capacity(nb_recipients*2); // We suppose that will work most of the time - // we actually have multiple "recipients" in a technical sense for each social recipient - // that's necessary because we don't want to miss a notification because we don't have a device atm + let mut recipients: Vec = Vec::with_capacity(nb_recipients * 2); // We suppose that will work most of the time + // we actually have multiple "recipients" in a technical sense for each social recipient + // that's necessary because we don't want to miss a notification because we don't have a device atm for member in all_members.keys() { let addresses = member.get_addresses(); for sp_address in addresses.into_iter() { @@ -608,24 +1196,24 @@ pub fn create_process_init_transaction( let mut fields2keys = Map::new(); let mut fields2cipher = Map::new(); - Value::Object(pcd.clone()).encrypt_fields(&mut fields2keys, &mut fields2cipher); - - new_process.init_state = fields2cipher.clone(); + let encrypted_pcd = pcd.clone(); + encrypted_pcd.encrypt_fields(&mut fields2keys, &mut fields2cipher); let local_device = lock_local_device()?; let sp_wallet = local_device.get_wallet(); - let sender: Member = local_device.to_member().ok_or(ApiError { message: "unpaired device".to_owned() })?; - + let sender: Member = local_device + .to_member() + .ok_or(ApiError::new("unpaired device".to_owned()))?; + // We first generate the prd with all the keys that we will keep to ourselves - let full_prd = Prd::new( - PrdType::Init, - Uuid::from_str(&new_process.uuid).expect("We can trust process to have a valid uuid"), + let full_prd = Prd::new_update( + commitment_outpoint, serde_json::to_string(&sender)?, fields2cipher.clone(), - fields2keys.clone() - )?; + fields2keys.clone(), + ); let prd_commitment = full_prd.create_commitment(); @@ -645,66 +1233,49 @@ pub fn create_process_init_transaction( let partial_secret = sp_wallet .get_client() - .get_partial_secret_from_psbt(&signed_psbt)?; + .get_partial_secret_from_psbt(&signed_psbt)?; let final_tx = signed_psbt.extract_tx()?; - let mut new_messages = vec![]; - let mut shared_secrets = vec![]; // This is a bit ugly, but this way we can update the process status + let mut ciphers = vec![]; + let mut shared_secrets = HashMap::new(); for (member, visible_fields) in all_members { let mut prd = full_prd.clone(); prd.filter_keys(visible_fields); let prd_msg = prd.to_network_msg(sp_wallet)?; - let mut res = CachedMessage::new(); - res.recipient = Some(member.clone()); - res.prd = Some(prd_msg.clone()); - res.sender = Some(sender.clone()); - res.pcd = Some(Value::Object(pcd.clone())); - res.commitment = Some(prd_commitment.to_string()); - res.status = CachedMessageStatus::Opened; let addresses = member.get_addresses(); for sp_address in addresses.into_iter() { let shared_point = sp_utils::sending::calculate_ecdh_shared_secret( - &::try_from(sp_address)?.get_scan_key(), + &::try_from(sp_address.as_str())?.get_scan_key(), &partial_secret, ); - let shared_secret = AnkSharedSecret::new(shared_point).to_byte_array().to_lower_hex_string(); + let shared_secret = AnkSharedSecret::new(shared_point) + .to_byte_array() + .to_lower_hex_string(); - let cipher = encrypt_with_key( - prd_msg.clone(), - shared_secret.clone(), - )?; - res.cipher.push(cipher); - res.shared_secrets.push(shared_secret.clone()); - shared_secrets.push(shared_secret); + let cipher = encrypt_with_key(prd_msg.clone(), shared_secret.clone())?; + ciphers.push(cipher); + shared_secrets.insert(sp_address, shared_secret); } - - new_messages.push(res); } - - lock_messages()?.extend(new_messages.clone()); - - let mut processes = lock_processes()?; - let init_state = ProcessState { + relevant_process.impending_requests.push(full_prd); + relevant_process.shared_secrets.extend(shared_secrets); + relevant_process.states.push(ProcessState { commited_in: OutPoint::null(), encrypted_pcd: Value::Object(fields2cipher), keys: fields2keys, - validation_token: vec![] - }; - - // We are initializing a process, so we shouldn't have it in our cache yet - processes.insert(Uuid::parse_str(&new_process.uuid).unwrap(), RelevantProcess { - process: new_process, - states: vec![init_state], - current_status: ProcessStatus::Active(shared_secrets) + validation_token: vec![], }); - Ok(createTransactionReturn { - txid: final_tx.txid().to_string(), - transaction: serialize(&final_tx).to_lower_hex_string(), - new_messages + debug!("updated_process: {:#?}", relevant_process); + + Ok(ApiReturn { + new_tx_to_send: Some(final_tx), + updated_process: Some((commitment_outpoint.to_string(), relevant_process.clone())), + ciphers_to_send: ciphers, + ..Default::default() }) } @@ -764,9 +1335,7 @@ pub fn encrypt_with_new_key(plaintext: String) -> ApiResult ApiResult { let key_bin = Vec::from_hex(&key)?; if key_bin.len() != 32 { - return Err(ApiError { - message: "key of invalid lenght".to_owned(), - }); + return Err(ApiError::new("key of invalid lenght".to_owned())); } let mut aes_key = [0u8; 32]; aes_key.copy_from_slice(&Vec::from_hex(&key)?); @@ -789,3 +1358,45 @@ pub fn create_faucet_msg() -> ApiResult { Ok(network_msg.to_string()) } + +/// Get active update proposals for a given process outpoint +/// Returns a vector with the latest commited state first and all active proposals +#[wasm_bindgen] +pub fn get_update_proposals(process_outpoint: String) -> ApiResult> { + let outpoint = OutPoint::from_str(&process_outpoint)?; + + let mut processes = lock_processes()?; + + let relevant_process = processes + .get(&outpoint) + .ok_or(ApiError::new("process not found".to_owned()))?; + + let update_proposals: Vec<&Prd> = relevant_process + .impending_requests + .iter() + .filter(|r| r.prd_type == PrdType::Update) + .collect(); + + if update_proposals.is_empty() { + return Err(ApiError::new(format!( + "No active update proposals for process {}", + process_outpoint + ))); + } + + let mut res = vec![]; + + // If we don't have a latest state, it simply means this update is a creation + if let Some(latest_state) = relevant_process.get_latest_state() { + res.push(serde_json::to_string(&latest_state)?); + } + + for proposal in update_proposals { + let pcd = Value::from_str(&proposal.payload)?; + let mut decrypted_pcd = Map::new(); + pcd.decrypt_fields(&proposal.keys, &mut decrypted_pcd)?; + res.push(serde_json::to_string(&decrypted_pcd)?); + } + + Ok(res) +} diff --git a/src/lib.rs b/src/lib.rs index 6e38363..a1d15e6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,12 +2,13 @@ use anyhow::Error; use sdk_common::crypto::AnkSharedSecret; use sdk_common::network::CachedMessage; +use sdk_common::pcd::AnkPcdHash; +use sdk_common::prd::{Prd, ValidationToken}; use sdk_common::process::Process; use sdk_common::sp_client::bitcoin::OutPoint; use sdk_common::uuid::Uuid; -use sdk_common::prd::ValidationToken; use serde::{Deserialize, Serialize}; -use serde_json::{Value, Map}; +use serde_json::{Map, Value}; use std::collections::{HashMap, HashSet}; use std::fmt::Debug; use std::ops::Index; @@ -27,40 +28,34 @@ pub fn lock_messages() -> Result>, Error> .lock_anyhow() } -#[derive(Debug)] -pub enum ProcessStatus { - Sealed, - Active(Vec), // shared_secrets used to communicate on current session -} - -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] pub struct ProcessState { pub commited_in: OutPoint, - pub encrypted_pcd: Value, + pub encrypted_pcd: Value, pub keys: Map, // We may not always have all the keys - pub validation_token: Vec // This signs the encrypted pcd + pub validation_token: Vec, // This signs the encrypted pcd } -#[derive(Debug)] +#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] pub struct RelevantProcess { - process: Process, states: Vec, - current_status: ProcessStatus, + shared_secrets: HashMap, + impending_requests: Vec, } impl RelevantProcess { - pub fn get_process(&self) -> Process { - self.process.clone() - } - pub fn get_status_at(&self, index: usize) -> Option { self.states.get(index).cloned() } + + pub fn get_latest_state(&self) -> Option { + self.states.last().cloned() + } } -pub static CACHEDPROCESSES: OnceLock>> = OnceLock::new(); +pub static CACHEDPROCESSES: OnceLock>> = OnceLock::new(); -pub fn lock_processes() -> Result>, Error> { +pub fn lock_processes() -> Result>, Error> { CACHEDPROCESSES .get_or_init(|| Mutex::new(HashMap::new())) .lock_anyhow() diff --git a/src/user.rs b/src/user.rs index 360f02f..bab7706 100644 --- a/src/user.rs +++ b/src/user.rs @@ -22,11 +22,11 @@ use std::io::{Cursor, Read, Write}; use std::str::FromStr; use std::sync::{Mutex, MutexGuard, OnceLock}; +use sdk_common::device::Device; use sdk_common::sp_client::bitcoin::secp256k1::constants::SECRET_KEY_SIZE; use sdk_common::sp_client::silentpayments::bitcoin_hashes::sha256; use sdk_common::sp_client::silentpayments::utils::{Network as SpNetwork, SilentPaymentAddress}; use sdk_common::sp_client::spclient::{OutputList, SpWallet, SpendKey}; -use sdk_common::device::Device; use crate::peers::Peer; use crate::wallet::generate_sp_wallet; diff --git a/tests/pairing.rs b/tests/pairing.rs index 252471b..335ff34 100644 --- a/tests/pairing.rs +++ b/tests/pairing.rs @@ -1,17 +1,16 @@ use std::collections::HashMap; +use std::str::FromStr; use sdk_client::api::{ - create_device_from_sp_wallet, create_process_from_template, create_process_init_transaction, get_address, get_outputs, pair_device, reset_device, setup + create_device_from_sp_wallet, create_update_transaction, dump_device, dump_process_cache, + get_address, get_outputs, get_update_proposals, pair_device, parse_cipher, reset_device, + restore_device, set_process_cache, setup, ApiReturn, }; -use sdk_client::lock_processes; -use sdk_common::network::CachedMessage; -use sdk_common::pcd::{Member, Pcd}; -use sdk_common::prd::Prd; +use sdk_common::log::debug; +use sdk_common::pcd::{Member, RoleDefinition}; use sdk_common::sp_client::bitcoin::OutPoint; use sdk_common::sp_client::spclient::OwnedOutput; -use sdk_common::uuid::Uuid; -use sdk_common::log::debug; -use serde_json::{self, json}; +use serde_json::{json, Value}; use tsify::JsValueSerdeExt; use wasm_bindgen_test::*; @@ -38,117 +37,181 @@ fn test_pairing() { let paired_device = helper_get_bob_address(); // Alice creates the new member with Bob address - let new_member = Member::new( - vec![ - device_address.as_str().try_into().unwrap(), - paired_device.as_str().try_into().unwrap(), - ] - ).unwrap(); + let new_member = Member::new(vec![ + device_address.as_str().try_into().unwrap(), + paired_device.as_str().try_into().unwrap(), + ]) + .unwrap(); - // We get the template for the pairing - // We don't really care how we get it, we can even imagine user writing it himself - // It just have to respect the basic Process struct, i.e. have all the fields below and the right type for the value - let pairing_template = json!({ - "uuid": "", + let pairing_init_state = json!({ "html": "", - "script": "", "style": "", - "init_state": { - "roles": { - "owner": { - "members": - [ - new_member - ], - "validation_rules": - [ - { - "quorum": 0.0, - "fields": [ - "roles", - "pairing_tx" - ], - "min_sig_member": 0.0 - } - ] - } - }, - "pairing_tx": "", + "script": "", + "roles": { + "owner": { + "members": + [ + new_member + ], + "validation_rules": + [ + { + "quorum": 0.0, + "fields": [ + "roles", + "pairing_tx" + ], + "min_sig_member": 0.0 + } + ] + } }, - "commited_in": OutPoint::null() + "pairing_tx": OutPoint::null(), }); - let new_process = create_process_from_template(pairing_template.to_string()).unwrap(); + debug!("Alice pairs her device"); + // we can update our local device now, first with an empty txid + pair_device(OutPoint::null().to_string(), vec![helper_get_bob_address()]).unwrap(); - // we can update our local device now - pair_device(new_process.uuid.clone(), vec![helper_get_bob_address()]).unwrap(); + debug!("Alice sends a transaction commiting to an update prd to Bob"); + let alice_pairing_return = + create_update_transaction(None, pairing_init_state.to_string(), 1).unwrap(); - debug!("Alice sends a transaction commiting to an init prd to Bob"); - let alice_pairing_return = create_process_init_transaction(new_process, 1).unwrap(); + // debug!("{:?}", alice_pairing_return); + // todo must take all the necessary validation before commiting + + // debug!("Alice prepares the commit message for the relay"); + // let (outpoint, process) = alice_pairing_return.updated_process.unwrap(); + // let to_commit = process.get_status_at(0).unwrap().encrypted_pcd; + // let init_return = create_commit_message(serde_json::to_string(&to_commit).unwrap(), RELAY_ADDRESS.to_owned(), None, 1).unwrap(); + + // todo send the commit message to the relay + + let pairing_tx = alice_pairing_return.new_tx_to_send.unwrap(); + + // We can update the local device with the actual pairing outpoint + pair_device( + OutPoint::new(pairing_tx.txid(), 0).to_string(), + vec![helper_get_bob_address()], + ) + .unwrap(); + + // This is only for testing, the relay takes care of that in prod let get_outputs_result = get_outputs().unwrap(); let alice_outputs: HashMap = get_outputs_result.into_serde().unwrap(); - let alice_pairing_tweak_data = - helper_get_tweak_data(&alice_pairing_return.transaction, alice_outputs); + let alice_pairing_tweak_data = helper_get_tweak_data(&pairing_tx, alice_outputs); - // Alice parse her own transaction - helper_parse_transaction(&alice_pairing_return.transaction, &alice_pairing_tweak_data).id; + // End of the test only part + + // Alice parses her own transaction + helper_parse_transaction(&pairing_tx, &alice_pairing_tweak_data); // Notify user that we're waiting for confirmation from the other device + let alice_device = dump_device().unwrap(); + let alice_processes = dump_process_cache().unwrap(); + debug!("Alice processes: {:#?}", alice_processes); + // ======================= Bob reset_device().unwrap(); create_device_from_sp_wallet(BOB_LOGIN_WALLET.to_owned()).unwrap(); // Bob receives Alice pairing transaction debug!("Bob parses Alice pairing transaction"); - helper_parse_transaction(&alice_pairing_return.transaction, &alice_pairing_tweak_data); + helper_parse_transaction(&pairing_tx, &alice_pairing_tweak_data); debug!("Bob receives the prd"); - let mut bob_retrieved_prd = CachedMessage::default(); - for message in alice_pairing_return.new_messages.iter() { - for cipher in message.cipher.iter() { - match helper_parse_cipher(cipher.clone()) { - Ok(res) => bob_retrieved_prd = res, - Err(_) => continue + let mut bob_retrieved_prd: ApiReturn = ApiReturn::default(); + for cipher in alice_pairing_return.ciphers_to_send.iter() { + // debug!("Parsing cipher: {:#?}", cipher); + match parse_cipher(cipher.clone()) { + Ok(res) => bob_retrieved_prd = res, + Err(e) => { + debug!("Error parsing cipher: {:#?}", e); + continue; } } } - if bob_retrieved_prd == CachedMessage::default() { - panic!("Bob failed to retrieve Alice message"); - } + assert!(bob_retrieved_prd != ApiReturn::default()); - debug!("Bob receives the pcd"); - let mut bob_retrieved_pcd = CachedMessage::default(); - for message in alice_pairing_return.new_messages { - for cipher in message.cipher { - match helper_parse_cipher(cipher) { - Ok(res) => bob_retrieved_pcd = res, - Err(_) => continue - } - } - } + debug!("Bob retrieved prd: {:#?}", bob_retrieved_prd); - if bob_retrieved_pcd == CachedMessage::default() { - panic!("Bob failed to retrieve Alice message"); - } + debug!("Bob sends a Confirm Prd to Alice"); + + let bob_device = dump_device().unwrap(); + let bob_processes = dump_process_cache().unwrap(); + + // ======================= Alice + reset_device().unwrap(); + restore_device(alice_device).unwrap(); + set_process_cache(alice_processes).unwrap(); + + debug!("Alice receives the Confirm Prd"); + let alice_parsed_prd = parse_cipher(bob_retrieved_prd.ciphers_to_send[0].clone()).unwrap(); + + debug!("Alice parsed Bob's Confirm Prd: {:#?}", alice_parsed_prd); + + // ======================= Bob + reset_device().unwrap(); + restore_device(bob_device).unwrap(); + set_process_cache(bob_processes).unwrap(); + + let bob_parsed_pcd_return = parse_cipher(alice_parsed_prd.ciphers_to_send[0].clone()).unwrap(); + + debug!("bob_parsed_pcd: {:#?}", bob_parsed_pcd_return); // At this point, user must validate the pairing proposal received from Alice + // We decrypt the content of the pcd so that we can display to user what matters + let alice_proposal = + get_update_proposals(bob_parsed_pcd_return.updated_process.unwrap().0).unwrap(); - debug!("Bob pairs device with Alice"); - let process = lock_processes().unwrap(); - let prd: Prd = serde_json::from_str(&bob_retrieved_prd.prd.unwrap()).unwrap(); - let relevant_process = process.get(&Uuid::parse_str(&prd.process_uuid).unwrap()).unwrap(); - // decrypt the pcd and update bob device - if let Some(initial_state) = relevant_process.get_status_at(0) { - let keys = initial_state.keys; - let mut pcd = initial_state.encrypted_pcd; - pcd.decrypt_fields(&keys).unwrap(); - pair_device(relevant_process.get_process().uuid, vec![device_address]).unwrap(); - } + debug!("Alice proposal: {:#?}", alice_proposal); + + // get the pairing tx from the proposal + let proposal = Value::from_str(&alice_proposal.get(0).unwrap()).unwrap(); + let pairing_tx = proposal.get("pairing_tx").unwrap().as_str().unwrap(); + let roles: RoleDefinition = + serde_json::from_str(proposal.get("roles").unwrap().as_str().unwrap()).unwrap(); + + // we check that the proposal contains only one member + assert!(roles.members.len() == 1); + + // we get all the addresses of the members of the proposal + let proposal_members = roles + .members + .iter() + .flat_map(|member| member.get_addresses()) + .collect::>(); + + // we can automatically check that a pairing member contains local device address + the one that sent the proposal + assert!(proposal_members.contains(&device_address)); + assert!(proposal_members.contains(&paired_device)); + assert!(proposal_members.len() == 2); // no free riders + + // we can now show all the addresses + pairing tx to the user on device to prompt confirmation + + pair_device(pairing_tx.to_owned(), proposal_members).unwrap(); + + // Bob signs the proposal and send a prd response to Alice + + // debug!("Bob pairs device with Alice"); + // let process = lock_processes().unwrap(); + // let prd: Prd = serde_json::from_str(&bob_retrieved_prd.prd.unwrap()).unwrap(); + // let relevant_process = process.get(&Uuid::parse_str(&prd.process_uuid).unwrap()).unwrap(); + // // decrypt the pcd and update bob device + // let pairing_tx: Txid; + // if let Some(initial_state) = relevant_process.get_status_at(0) { + // let keys = initial_state.keys; + // let mut pcd = initial_state.encrypted_pcd; + // pcd.decrypt_fields(&keys).unwrap(); + // debug!("decrypted pcd: {:?}", pcd); + // pairing_tx = Txid::from_str(pcd.get("pairing_tx").unwrap().as_str().unwrap()).unwrap(); + // pair_device(relevant_process.get_process().uuid, vec![device_address]).unwrap(); + // } // To make the pairing effective, alice and bob must now spend their respective output into a new transaction // login(); diff --git a/tests/utils.rs b/tests/utils.rs index 262c209..63e5ce5 100644 --- a/tests/utils.rs +++ b/tests/utils.rs @@ -1,18 +1,15 @@ use std::collections::HashMap; -use sdk_client::api::{parse_cipher, parse_new_tx, reset_device, ApiResult}; -use sdk_common::network::{ - CachedMessage, NewTxMessage, -}; -use sdk_common::sp_client::bitcoin::consensus::deserialize; -use sdk_common::sp_client::bitcoin::hex::FromHex; +use sdk_client::api::{parse_cipher, parse_new_tx, ApiResult, ApiReturn}; +use sdk_common::network::NewTxMessage; +use sdk_common::sp_client::bitcoin::consensus::serialize; +use sdk_common::sp_client::bitcoin::hex::DisplayHex; use sdk_common::sp_client::bitcoin::secp256k1::PublicKey; use sdk_common::sp_client::bitcoin::{OutPoint, ScriptBuf, Transaction}; use sdk_common::sp_client::silentpayments::utils::receiving::{ calculate_tweak_data, get_pubkey_from_input, }; use sdk_common::sp_client::spclient::{OwnedOutput, SpWallet}; -use sdk_common::log::debug; use serde_json; // We're using alice and bob for clarity, but it's important to remember that for pairing and login Alice and Bob are the same person @@ -37,6 +34,7 @@ pub const ALICE_ANSWER_DEVICE: &str = "{\"sp_wallet\":{\"client\":{\"label\":\"d pub const BOB_ANSWER_DEVICE: &str = "{\"sp_wallet\":{\"client\":{\"label\":\"default\",\"scan_sk\":\"0de90b7195c1380d5fde13de3f1d66d53423a9896314839e36ba672653af60b4\",\"spend_key\":{\"Secret\":\"affe686075ecbe17b8ce7de45ec31314804259d0a4bc1c863de21ffd6dc598f8\"},\"mnemonic\":null,\"sp_receiver\":{\"version\":0,\"network\":\"Regtest\",\"scan_pubkey\":[2,85,96,92,243,247,237,192,205,9,178,146,101,237,132,232,15,2,69,138,31,118,76,140,142,207,90,13,192,94,254,150,133],\"spend_pubkey\":[3,5,157,91,250,169,41,61,190,37,30,98,152,253,180,138,250,86,162,102,82,148,130,220,44,153,127,83,43,246,93,17,232],\"change_label\":\"56572fc770b52096879662f97f98263d3e126f5a4a38f00f2895a9dde4c47c1c\",\"labels\":[[\"56572fc770b52096879662f97f98263d3e126f5a4a38f00f2895a9dde4c47c1c\",[2,237,237,247,213,154,87,34,239,168,235,87,122,152,94,41,35,101,184,201,58,201,6,185,58,157,52,208,129,167,2,224,198]]]},\"network\":\"regtest\"},\"outputs\":{\"wallet_fingerprint\":[203,200,4,248,139,36,241,232],\"birthday\":2146,\"last_scan\":2146,\"outputs\":{\"23a09cb1af6a2d350f02c38e9e8727f8ec0e75181f19f781ef6985862577792d:2\":{\"blockheight\":0,\"tweak\":\"e02bb8059b6c787d22715be95de87ea75c120e0f4e0507233bb3e1547b50d98b\",\"amount\":99895292,\"script\":\"51203207d027bb48df9a7c5798091b8bd4912c436c19ea41f04b48ea5dad31118183\",\"label\":\"56572fc770b52096879662f97f98263d3e126f5a4a38f00f2895a9dde4c47c1c\",\"spend_status\":{\"Spent\":\"4b7a78bb8a715fa4875543025182b3f0bb1bc5ca44b852cab57c2929231ddf3f\"}},\"78e97c940d3353718864c251489da99be99441323cabe619522169d22b030fb8:0\":{\"blockheight\":0,\"tweak\":\"898284108952bd8b0e74030e00b81c1b387ee151a810ca3b40143da227707df2\",\"amount\":1200,\"script\":\"5120c0ff94a1318f0061960a713c927ff175a0d71a791497e94e70601c3baae739b8\",\"label\":null,\"spend_status\":{\"Spent\":\"4b7a78bb8a715fa4875543025182b3f0bb1bc5ca44b852cab57c2929231ddf3f\"}},\"4b7a78bb8a715fa4875543025182b3f0bb1bc5ca44b852cab57c2929231ddf3f:2\":{\"blockheight\":0,\"tweak\":\"b13107995241e7daaf13e89ff62a007dd592173c02284a46c9d40778e9fd8165\",\"amount\":99894746,\"script\":\"5120fc7226dc54ed54144d7f6081726fa1d6b5d0fc6aca09a0cc689cc9552e81db14\",\"label\":\"56572fc770b52096879662f97f98263d3e126f5a4a38f00f2895a9dde4c47c1c\",\"spend_status\":\"Unspent\"},\"5e18963dcdd5e94b4b5dfaa147a6e62889a367d9fc1a016973f84c07a192982c:0\":{\"blockheight\":0,\"tweak\":\"ac6c259ddf88509b3663a412f4d40403cafb3a89c8a6922770384ba1806ecda0\",\"amount\":1200,\"script\":\"512010bb317e1ee70b2be456609f5c7aeaa8106b615326220f0e73ad5e78d0322f22\",\"label\":null,\"spend_status\":\"Unspent\"},\"650088ebc28e4d5d7b4ee2e0337b4907a02537f5be6c28c63da88a6b67b9af1d:1\":{\"blockheight\":0,\"tweak\":\"281ea54f74e5f6582a7462bdd9397b9a786f97455e858e7901a6a4fa95f39192\",\"amount\":654,\"script\":\"5120aa4395e2be9cbce580e94f4308d64422da251b7df12fb8c571675d40cc50cf00\",\"label\":\"56572fc770b52096879662f97f98263d3e126f5a4a38f00f2895a9dde4c47c1c\",\"spend_status\":\"Unspent\"},\"576c2e53fe924d68deb7262cfc0e4023b5889652dec35671e1e7cf255d61c28f:1\":{\"blockheight\":0,\"tweak\":\"f0ad83734cdc7d73575e5a32651390cf30b92cc7e44cf94ec37da46900ecaf71\",\"amount\":654,\"script\":\"5120230cc1e85829be238e666f469653cbc2f1c0e3675a9bf33e1d1f91115f5dd306\",\"label\":\"56572fc770b52096879662f97f98263d3e126f5a4a38f00f2895a9dde4c47c1c\",\"spend_status\":\"Unspent\"},\"e2c6ff9927c8a5f7a60087117732c07ab7cd82c0c65462e9c780eb5ce9c35292:0\":{\"blockheight\":0,\"tweak\":\"0e3395ff27bde9187ffaeeb2521f6277d3b83911f16ccbaf59a1a68d99a0ab93\",\"amount\":1200,\"script\":\"512010f06f764cbc923ec3198db946307bf0c06a1b4f09206055e47a6fec0a33d52c\",\"label\":null,\"spend_status\":{\"Spent\":\"576c2e53fe924d68deb7262cfc0e4023b5889652dec35671e1e7cf255d61c28f\"}},\"23a09cb1af6a2d350f02c38e9e8727f8ec0e75181f19f781ef6985862577792d:1\":{\"blockheight\":0,\"tweak\":\"a68aed757eb3aca6bcbd0deb749eb26767963401e07d568b19f05f9945fce9b3\",\"amount\":248,\"script\":\"5120d3a39aed25fccd3338aa8c9d40c015febfb4361cffb6cfdcede3032317dc26ff\",\"label\":null,\"spend_status\":\"Unspent\"},\"4b7a78bb8a715fa4875543025182b3f0bb1bc5ca44b852cab57c2929231ddf3f:1\":{\"blockheight\":0,\"tweak\":\"73cd0a0ebe8e61c3f5ec6b414107934238c4642924272823fb630ef828c14a97\",\"amount\":248,\"script\":\"51202c1f1be3cff73416dc34de0d5f7760095a51f6b4a7bfa118441221718394beea\",\"label\":null,\"spend_status\":\"Unspent\"},\"93722ea2fb9b74954210b4cdd1360e280b7ff1bc156d6b75f847e62411c588fb:0\":{\"blockheight\":0,\"tweak\":\"da5e3aa2378e3a257f99eb1e0ae4c672916f6a2f32a8ed9a8e146f2074da981b\",\"amount\":443,\"script\":\"51209eb9e950127b6a7d81668f25b4d5b164b42dafe59ce40b80e6c489ec983540d7\",\"label\":null,\"spend_status\":{\"Spent\":\"23a09cb1af6a2d350f02c38e9e8727f8ec0e75181f19f781ef6985862577792d\"}},\"fbd9c63e0dd08c2569b51a0d6095a79ee2acfcac66acdb594328a095f1fadb63:1\":{\"blockheight\":2146,\"tweak\":\"678dbcbdb40cd3733c8dbbd508761a0937009cf75a9f466e3c98877e79037cbc\",\"amount\":99896595,\"script\":\"5120deab0c5a3d23de30477b0b5a95a261c96e29afdd9813c665d2bf025ad2b3f919\",\"label\":null,\"spend_status\":{\"Spent\":\"23a09cb1af6a2d350f02c38e9e8727f8ec0e75181f19f781ef6985862577792d\"}},\"494aa6088c4ff368c04ee9dcacad8ab20d91587bacd48e030ab510b012131fe2:0\":{\"blockheight\":0,\"tweak\":\"21fc32bded971dd15387671436232f17d932e18758f69b476219e2d6e688767f\",\"amount\":1200,\"script\":\"5120230d06322d10a5838634df3c55eef9959a1c0c144108e0416b9687eaf22e6baa\",\"label\":null,\"spend_status\":{\"Spent\":\"650088ebc28e4d5d7b4ee2e0337b4907a02537f5be6c28c63da88a6b67b9af1d\"}}}},\"tx_history\":[]},\"current_session_outpoint\":\"5e18963dcdd5e94b4b5dfaa147a6e62889a367d9fc1a016973f84c07a192982c:0\",\"current_session_key\":[92,106,141,254,85,117,14,178,239,50,33,247,83,151,23,25,144,142,183,115,190,26,14,113,238,72,13,18,29,254,37,87],\"current_session_revokation_outpoint\":\"4b7a78bb8a715fa4875543025182b3f0bb1bc5ca44b852cab57c2929231ddf3f:1\",\"paired_device\":{\"address\":\"sprt1qqf50y6deqe7dqg3vj562xf3lmktqe3sct78ha6e9jh54sa3q2m5esq7x2tz0xrpm0ekclyyaszc2sh3ksm5gkumxpwegpa80lv5wgsuefqaufx8q\",\"outgoing_pairing_transaction\":[45,121,119,37,134,133,105,239,129,247,25,31,24,117,14,236,248,39,135,158,142,195,2,15,53,45,106,175,177,156,160,35],\"revokation_index\":1,\"incoming_pairing_transaction\":[184,15,3,43,210,105,33,82,25,230,171,60,50,65,148,233,155,169,157,72,81,194,100,136,113,83,51,13,148,124,233,120],\"current_remote_key\":[156,58,113,25,63,127,197,36,130,253,81,106,26,54,244,223,85,246,70,161,95,21,236,196,245,84,151,38,43,6,231,200],\"current_session_outpoint\":\"4b7a78bb8a715fa4875543025182b3f0bb1bc5ca44b852cab57c2929231ddf3f:0\",\"current_session_revokation_outpoint\":\"5e18963dcdd5e94b4b5dfaa147a6e62889a367d9fc1a016973f84c07a192982c:1\"}}"; pub const ALICE_FINAL_DEVICE: &str = "{\"sp_wallet\":{\"client\":{\"label\":\"default\",\"scan_sk\":\"e3d8922a41a7cb1a84a90f4334e987bb5ea2df6a1fdf44f789b5302de119f9e2\",\"spend_key\":{\"Secret\":\"93292e5b21042c6cfc742ba30e9d2a1e01609b12d154a1825184ed12c7b9631b\"},\"mnemonic\":null,\"sp_receiver\":{\"version\":0,\"network\":\"Regtest\",\"scan_pubkey\":[2,104,242,105,185,6,124,208,34,44,149,52,163,38,63,221,150,12,198,24,95,143,126,235,37,149,233,88,118,32,86,233,152],\"spend_pubkey\":[3,198,82,196,243,12,59,126,109,143,144,157,128,176,168,94,54,134,232,139,115,102,11,178,128,244,239,251,40,228,67,153,72],\"change_label\":\"ac14a827e2d023b8f7804303a47259366117d99ed932b641d4a8eaf1b82cc992\",\"labels\":[[\"ac14a827e2d023b8f7804303a47259366117d99ed932b641d4a8eaf1b82cc992\",[2,244,223,255,57,50,216,27,133,112,138,69,120,126,85,110,6,242,141,33,136,191,82,164,241,54,179,115,84,161,145,174,154]]]},\"network\":\"regtest\"},\"outputs\":{\"wallet_fingerprint\":[187,119,108,230,171,125,106,11],\"birthday\":1620,\"last_scan\":2146,\"outputs\":{\"78e97c940d3353718864c251489da99be99441323cabe619522169d22b030fb8:1\":{\"blockheight\":0,\"tweak\":\"cfea493360d6ffe2bfd13e4c5c2f677351e0c038f1ce0cd5383fadad6adc79f1\",\"amount\":191,\"script\":\"51200f008ff13f876bf328c69f10ad8621c9770ff8d5eb1861cb67199189a6308bb9\",\"label\":null,\"spend_status\":{\"Spent\":\"5e18963dcdd5e94b4b5dfaa147a6e62889a367d9fc1a016973f84c07a192982c\"}},\"650088ebc28e4d5d7b4ee2e0337b4907a02537f5be6c28c63da88a6b67b9af1d:0\":{\"blockheight\":0,\"tweak\":\"de507c854cee16d2e157fbf6c6dbfd2e721b861efc7f657bb724790fc7829665\",\"amount\":349,\"script\":\"5120028143243ed5d4554e60d96ee61fdf6f6e359a9083311c210645c0a7fef51248\",\"label\":null,\"spend_status\":{\"Spent\":\"f07c116303c88e7b22169f0872c67e3196ad8312170313643a8f47482c6bd308\"}},\"494aa6088c4ff368c04ee9dcacad8ab20d91587bacd48e030ab510b012131fe2:1\":{\"blockheight\":0,\"tweak\":\"97b06ded6644c6ae2a861342db3375d10a002da72f367a0acd5b8890c277df1e\",\"amount\":248,\"script\":\"5120902e75b33f6a80f20edd2574f02f5eaf3fc196aaad9c494263465519606a47a7\",\"label\":null,\"spend_status\":{\"Spent\":\"f07c116303c88e7b22169f0872c67e3196ad8312170313643a8f47482c6bd308\"}},\"5e18963dcdd5e94b4b5dfaa147a6e62889a367d9fc1a016973f84c07a192982c:1\":{\"blockheight\":0,\"tweak\":\"49471984d232f62afb7421c904a26ec073b968e76e3e1b8f4371f2f730ba627f\",\"amount\":191,\"script\":\"51203b12839f99ca62bd09eb277bab720b9bb506003ed5b3fa6654f37a42f3cf1825\",\"label\":null,\"spend_status\":{\"Spent\":\"494aa6088c4ff368c04ee9dcacad8ab20d91587bacd48e030ab510b012131fe2\"}},\"e2c6ff9927c8a5f7a60087117732c07ab7cd82c0c65462e9c780eb5ce9c35292:2\":{\"blockheight\":0,\"tweak\":\"e3e21d551e933199f8c977bc0362616bdfb128da7ca9728bd7254977541c39cc\",\"amount\":3936897,\"script\":\"5120df3af55a63bd056c5d6d09f47a3a19c382d938c08db8bc41a59c5235970209ad\",\"label\":\"ac14a827e2d023b8f7804303a47259366117d99ed932b641d4a8eaf1b82cc992\",\"spend_status\":{\"Spent\":\"78e97c940d3353718864c251489da99be99441323cabe619522169d22b030fb8\"}},\"78e97c940d3353718864c251489da99be99441323cabe619522169d22b030fb8:2\":{\"blockheight\":0,\"tweak\":\"6ebffdcaf1da7b160b167caed039e795f8b6d5168903972f9436aaedef574f28\",\"amount\":3935806,\"script\":\"5120fe0ec587ba0a8f8b1ef7c708a8b67723e55d45fb8e5c12ffc8874d61dedc3c16\",\"label\":\"ac14a827e2d023b8f7804303a47259366117d99ed932b641d4a8eaf1b82cc992\",\"spend_status\":{\"Spent\":\"5e18963dcdd5e94b4b5dfaa147a6e62889a367d9fc1a016973f84c07a192982c\"}},\"9a4a67cc5a40bf882d8b300d91024d7c97024b3b68b2df7745a5b9ea1df1888c:1\":{\"blockheight\":1620,\"tweak\":\"b8b63b3ed97d297b744135cfac2fb4a344c881a77543b71f1fcd16bc67514f26\",\"amount\":3938643,\"script\":\"51205b7b324bb71d411e32f2c61fda5d1db23f5c7d6d416a77fab87c913a1b120be1\",\"label\":\"ac14a827e2d023b8f7804303a47259366117d99ed932b641d4a8eaf1b82cc992\",\"spend_status\":{\"Spent\":\"e2c6ff9927c8a5f7a60087117732c07ab7cd82c0c65462e9c780eb5ce9c35292\"}},\"23a09cb1af6a2d350f02c38e9e8727f8ec0e75181f19f781ef6985862577792d:0\":{\"blockheight\":0,\"tweak\":\"dac814c1ceff86cac3476e028ffa6eda0d9858bd74288a0e6e9e5cef271f723c\",\"amount\":1200,\"script\":\"51205e349be9fff1dc0c8e6bae3d35679029c57d9e554f0bb9705b71491d9b7569f1\",\"label\":null,\"spend_status\":{\"Spent\":\"5e18963dcdd5e94b4b5dfaa147a6e62889a367d9fc1a016973f84c07a192982c\"}},\"576c2e53fe924d68deb7262cfc0e4023b5889652dec35671e1e7cf255d61c28f:0\":{\"blockheight\":0,\"tweak\":\"6e5c1d4690b9ff4fa26e5185ada25af5c2987650e629a518db8a43fdaad7a26c\",\"amount\":349,\"script\":\"512088ac90e180c87b7fa6aa33db4c72a8620cd08fda3ba1c71430b904eb068c587f\",\"label\":null,\"spend_status\":{\"Spent\":\"78e97c940d3353718864c251489da99be99441323cabe619522169d22b030fb8\"}},\"4b7a78bb8a715fa4875543025182b3f0bb1bc5ca44b852cab57c2929231ddf3f:0\":{\"blockheight\":0,\"tweak\":\"2e3636950174a42842da254d7936f589e8c9ded6e17c90656bb759467fff9ea0\",\"amount\":1200,\"script\":\"51209c3a71193f7fc52482fd516a1a36f4df55f646a15f15ecc4f55497262b06e7c8\",\"label\":null,\"spend_status\":\"Unspent\"},\"e2c6ff9927c8a5f7a60087117732c07ab7cd82c0c65462e9c780eb5ce9c35292:1\":{\"blockheight\":0,\"tweak\":\"7c84a3074c18c23a65eceaca49a327989ef6e7240e0e080421afb80f06906d73\",\"amount\":306,\"script\":\"5120eb78084d7a2ccbdb7eb7e9bba7cf875c4e54a5aba0beac57c5d3e41133bd8fdd\",\"label\":null,\"spend_status\":{\"Spent\":\"78e97c940d3353718864c251489da99be99441323cabe619522169d22b030fb8\"}},\"5e18963dcdd5e94b4b5dfaa147a6e62889a367d9fc1a016973f84c07a192982c:2\":{\"blockheight\":0,\"tweak\":\"86a78efb4f522790d46096df9261435e3f7cab5f5129efe4ff77562fb4834a0c\",\"amount\":3935451,\"script\":\"5120c314279d078acaa41b5d897f98b81f09546001b6422e90e0b3f902ac7442a6b7\",\"label\":\"ac14a827e2d023b8f7804303a47259366117d99ed932b641d4a8eaf1b82cc992\",\"spend_status\":{\"Spent\":\"494aa6088c4ff368c04ee9dcacad8ab20d91587bacd48e030ab510b012131fe2\"}},\"494aa6088c4ff368c04ee9dcacad8ab20d91587bacd48e030ab510b012131fe2:2\":{\"blockheight\":0,\"tweak\":\"6493d306b4650a36321d05270e2067b58839637da52f0fc29cd2f05817fff0d6\",\"amount\":3933896,\"script\":\"51206666fc11233881b1c831e032e9b06cf05a90ed93fef4f35c78583b9eec845ab5\",\"label\":\"ac14a827e2d023b8f7804303a47259366117d99ed932b641d4a8eaf1b82cc992\",\"spend_status\":\"Unspent\"}}},\"tx_history\":[]},\"current_session_outpoint\":\"4b7a78bb8a715fa4875543025182b3f0bb1bc5ca44b852cab57c2929231ddf3f:0\",\"current_session_key\":[193,95,100,240,34,120,208,149,63,78,80,240,135,212,31,167,234,42,121,233,178,209,49,231,189,60,70,89,71,185,1,187],\"current_session_revokation_outpoint\":\"5e18963dcdd5e94b4b5dfaa147a6e62889a367d9fc1a016973f84c07a192982c:1\",\"paired_device\":{\"address\":\"sprt1qqf2kqh8n7lkupngfk2fxtmvyaq8sy3v2ramyeryweadqmsz7l6tg2qc9n4dl42ff8klz28nznr7mfzh6263xv555stwzextl2v4lvhg3aqq7ru8u\",\"outgoing_pairing_transaction\":[184,15,3,43,210,105,33,82,25,230,171,60,50,65,148,233,155,169,157,72,81,194,100,136,113,83,51,13,148,124,233,120],\"revokation_index\":1,\"incoming_pairing_transaction\":[45,121,119,37,134,133,105,239,129,247,25,31,24,117,14,236,248,39,135,158,142,195,2,15,53,45,106,175,177,156,160,35],\"current_remote_key\":[16,187,49,126,30,231,11,43,228,86,96,159,92,122,234,168,16,107,97,83,38,34,15,14,115,173,94,120,208,50,47,34],\"current_session_outpoint\":\"5e18963dcdd5e94b4b5dfaa147a6e62889a367d9fc1a016973f84c07a192982c:0\",\"current_session_revokation_outpoint\":\"4b7a78bb8a715fa4875543025182b3f0bb1bc5ca44b852cab57c2929231ddf3f:1\"}}"; +pub const RELAY_ADDRESS: &str = "sprt1qqfmqt0ngq99y8t4ke6uhtm2a2vc2zxvhj7hjrqu599kn30d4cs9rwqn6n079mdr4dfqg72yrtvuxf43yswscw86nvvl09mc5ljx65vfh75fkza35"; pub const DEFAULT_NYM: &str = "AliceBob"; pub fn helper_get_alice_address() -> String { @@ -49,17 +47,19 @@ pub fn helper_get_bob_address() -> String { wallet.get_client().get_receiving_address() } -pub fn helper_get_tweak_data(transaction: &str, outpoints: HashMap) -> String { - let tx = deserialize::(&Vec::from_hex(transaction).unwrap()).unwrap(); +pub fn helper_get_tweak_data( + tx: &Transaction, + outpoints: HashMap, +) -> String { let mut outpoint_data = vec![]; let mut witnesses = vec![]; let mut spks = vec![]; - for prevout in tx.input { + for prevout in tx.input.iter() { outpoint_data.push(( prevout.previous_output.txid.to_string(), prevout.previous_output.vout, )); - witnesses.push(prevout.witness); + witnesses.push(prevout.witness.clone()); if let Some(output) = outpoints.get(&prevout.previous_output) { spks.push(ScriptBuf::from_hex(&output.script).unwrap()); } @@ -75,26 +75,16 @@ pub fn helper_get_tweak_data(transaction: &str, outpoints: HashMap CachedMessage { +pub fn helper_parse_transaction(transaction: &Transaction, tweak_data: &str) -> ApiReturn { let new_tx_msg = serde_json::to_string(&NewTxMessage::new( - transaction.to_owned(), + serialize(transaction).to_lower_hex_string(), Some(tweak_data.to_owned()), )) .unwrap(); - debug!("new_tx_msg: {:?}", new_tx_msg); + // debug!("new_tx_msg: {:?}", new_tx_msg); let result = parse_new_tx(new_tx_msg, 0, 1); match result { - Ok(m) => { - if m.is_some() { - m.unwrap() - } else { - panic!("Failed to find our tx"); - } - }, + Ok(m) => m, Err(e) => panic!("Unexpected error: {}", e.message), } } - -pub fn helper_parse_cipher(cipher_msg: String) -> ApiResult { - parse_cipher(cipher_msg, 1) -}