diff --git a/src/api.rs b/src/api.rs index 6ee8900..3718dd2 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,6 +1,6 @@ use std::any::Any; use std::borrow::Borrow; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::io::Write; use std::ops::Index; use std::str::FromStr; @@ -15,7 +15,7 @@ use anyhow::Error as AnyhowError; use sdk_common::crypto::{ AeadCore, Aes256Decryption, Aes256Encryption, Aes256Gcm, AnkSharedSecret, KeyInit, Purpose, }; -use sdk_common::process::{Process, ValidationRules}; +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}; @@ -52,7 +52,7 @@ use wasm_bindgen::prelude::*; use sdk_common::network::{ self, AnkFlag, CachedMessage, CachedMessageStatus, Envelope, FaucetMessage, NewTxMessage}; -use sdk_common::pcd::{AnkPcdHash, Pcd, Member}; +use sdk_common::pcd::{AnkPcdHash, Member, Pcd, RoleDefinition, ValidationRule}; use sdk_common::prd::{AnkPrdHash, Prd, PrdType}; use sdk_common::silentpayments::{create_transaction, map_outputs_to_sp_address}; use sdk_common::sp_client::spclient::{ @@ -222,7 +222,7 @@ pub fn create_new_device(birthday: u32, network_str: String) -> ApiResult ApiResult<()> { +pub fn pair_device(uuid: String, mut sp_addresses: Vec) -> ApiResult<()> { let mut local_device = lock_local_device()?; if local_device.is_linked() { @@ -231,9 +231,12 @@ pub fn pair_device(uuid: String, new_device_address: String, state: String) -> A }); } - local_device.set_process_uuid(Uuid::parse_str(&uuid).map_err(|e| ApiError { message: e.to_string() })?); - local_device.push_paired_device(new_device_address.try_into().map_err(|_| ApiError { message: "Invalid address".to_owned() })?); - local_device.update_latest_state(serde_json::from_str(&state)?); + sp_addresses.push(local_device.get_wallet().get_client().get_receiving_address()); + + local_device.pair( + Uuid::parse_str(&uuid).unwrap(), + Member::new(sp_addresses.into_iter().map(|a| TryInto::::try_into(a).unwrap()).collect())? + ); Ok(()) } @@ -539,6 +542,20 @@ pub fn get_available_amount() -> ApiResult { Ok(device.get_wallet().get_outputs().get_balance().to_sat()) } +#[wasm_bindgen] +pub fn create_process_from_template(json: String) -> ApiResult { + let template_process: Process = serde_json::from_str(&json)?; + let mut new_process = Process::new( + template_process.html, + template_process.style, + template_process.script, + template_process.init_state, + template_process.commited_in + ); + + Ok(new_process) +} + #[derive(Debug, Tsify, Serialize, Deserialize, Default)] #[tsify(into_wasm_abi, from_wasm_abi)] #[allow(non_camel_case_types)] @@ -548,54 +565,38 @@ pub struct createTransactionReturn { pub new_messages: Vec } -#[derive(Debug, Serialize, Deserialize, Tsify)] -#[tsify(into_wasm_abi, from_wasm_abi)] -pub struct CreateProcessInitTransactionArguments{ - pub member2fields: HashMap>, - pub process: Process, - pub stringified_pcd: String, // must be valid json -} - #[wasm_bindgen] pub fn create_process_init_transaction( - args: CreateProcessInitTransactionArguments, + mut new_process: Process, fee_rate: u32, ) -> ApiResult { - if args.member2fields.len() == 0 { - return Err(ApiError { - message: "Must have at least one recipient".to_owned(), - }); - } - - let pcd: Map; - if let Some(object) = Value::from_str(&args.stringified_pcd)?.as_object() { - pcd = object.to_owned(); - } else { - return Err(ApiError { - message: "provided pcd is not a valid json".to_owned(), - }); + let pcd = new_process.init_state; + let roles = pcd["roles"].as_object().unwrap().clone(); + let mut all_members: HashMap> = HashMap::new(); + for (name, role_def) in roles { + let role: RoleDefinition = serde_json::from_str(&role_def.to_string()).unwrap(); + let fields: Vec = role.validation_rules.iter().flat_map(|rule| rule.fields.clone()).collect(); + for member in role.members { + if !all_members.contains_key(&member) { + all_members.insert(member.clone(), HashSet::new()); + } + all_members.get_mut(&member).unwrap().extend(fields.clone()); + } } - let mut process = args.process; - - // process doesn't have an initial state at this stage - if process.init_state != Value::Null { - return Err(ApiError { - message: "new process must have a null initial state".to_owned(), - }); - } - - // maybe the process script can embed some basic checks for the pcd, for example `pcd.members.len() == 1` - - let all_members: Vec<&Member> = args.member2fields.keys().collect(); let nb_recipients = all_members.len(); + if nb_recipients == 0 { + return Err(ApiError { + message: "Can't create a process with 0 member".to_owned(), + }); + } - let mut recipients: Vec = Vec::with_capacity(nb_recipients*2); - // we actually have 2 "recipients" in a technical sense for each social recipient + 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 { - let (address_a, address_b) = member.get_addresses(); - for sp_address in [address_a, address_b].into_iter() { + for member in all_members.keys() { + let addresses = member.get_addresses(); + for sp_address in addresses.into_iter() { recipients.push(Recipient { address: sp_address.into(), amount: DEFAULT_AMOUNT, @@ -606,35 +607,20 @@ pub fn create_process_init_transaction( let mut fields2keys = Map::new(); let mut fields2cipher = Map::new(); - Value::from_str(&args.stringified_pcd).unwrap().encrypt_fields(&mut fields2keys, &mut fields2cipher); + Value::Object(pcd.clone()).encrypt_fields(&mut fields2keys, &mut fields2cipher); - process.init_state = Value::Object(fields2cipher.clone()); + new_process.init_state = fields2cipher.clone(); let local_device = lock_local_device()?; let sp_wallet = local_device.get_wallet(); - let latest_state = local_device.get_latest_state(); - if latest_state == Value::Null { - return Err(ApiError { - message: "Device not paired, nor pairing".to_owned(), - }); - } - - let sender: Member = serde_json::from_value( - latest_state.get("members") - .unwrap() - .as_array() - .ok_or(ApiError { message: "members must be an array".to_owned() })? - .get(0) - .ok_or(ApiError { message: "empty array".to_owned() })? - .clone() - )?; + let sender: Member = local_device.to_member().unwrap(); // 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(&process.uuid).expect("We can trust process to have a valid uuid"), + Uuid::from_str(&new_process.uuid).expect("We can trust process to have a valid uuid"), serde_json::to_string(&sender)?, fields2cipher.clone(), fields2keys.clone() @@ -664,7 +650,7 @@ pub fn create_process_init_transaction( let mut new_messages = vec![]; let mut shared_secrets = vec![]; // This is a bit ugly, but this way we can update the process status - for (member, visible_fields) in args.member2fields { + 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)?; @@ -676,8 +662,8 @@ pub fn create_process_init_transaction( res.commitment = Some(prd_commitment.to_string()); res.status = CachedMessageStatus::Opened; - let (addresses1, addresses2) = member.get_addresses(); - for sp_address in [addresses1, addresses2].into_iter() { + 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).unwrap().get_scan_key(), &partial_secret, @@ -708,8 +694,8 @@ pub fn create_process_init_transaction( }; // We are initializing a process, so we shouldn't have it in our cache yet - processes.insert(Uuid::parse_str(&process.uuid).unwrap(), RelevantProcess { - process, + processes.insert(Uuid::parse_str(&new_process.uuid).unwrap(), RelevantProcess { + process: new_process, states: vec![init_state], current_status: ProcessStatus::Active(shared_secrets) }); diff --git a/tests/pairing.rs b/tests/pairing.rs index ca94b08..252471b 100644 --- a/tests/pairing.rs +++ b/tests/pairing.rs @@ -1,18 +1,17 @@ use std::collections::HashMap; use sdk_client::api::{ - create_device_from_sp_wallet, create_process_init_transaction, get_outputs, pair_device, reset_device, setup, CreateProcessInitTransactionArguments + create_device_from_sp_wallet, create_process_from_template, create_process_init_transaction, get_address, get_outputs, pair_device, reset_device, setup }; use sdk_client::lock_processes; use sdk_common::network::CachedMessage; -use sdk_common::pcd::{Member, Pcd, Role}; +use sdk_common::pcd::{Member, Pcd}; use sdk_common::prd::Prd; -use sdk_common::process::{Process, ValidationRules}; 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, Value}; +use serde_json::{self, json}; use tsify::JsValueSerdeExt; use wasm_bindgen_test::*; @@ -32,52 +31,60 @@ fn test_pairing() { reset_device().unwrap(); create_device_from_sp_wallet(ALICE_LOGIN_WALLET.to_owned()).unwrap(); + // we get our own address + let device_address = get_address().unwrap(); + + // we scan the qr code or get the address by any other means + let paired_device = helper_get_bob_address(); + // Alice creates the new member with Bob address let new_member = Member::new( - DEFAULT_NYM.to_owned(), - helper_get_alice_address().try_into().unwrap(), - helper_get_bob_address().try_into().unwrap(), - Role::User - ); + vec![ + device_address.as_str().try_into().unwrap(), + paired_device.as_str().try_into().unwrap(), + ] + ).unwrap(); - let initial_state = json!({ - "nym": DEFAULT_NYM, - "members": [ - new_member, - ], - "current_session_tx": null, + // 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": "", + "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": "", + }, + "commited_in": OutPoint::null() }); - let validation_rules = ValidationRules::new( - 1.0, - Role::Admin, - 1.0 - ); - - let mut member2fields: HashMap> = HashMap::new(); - member2fields.insert(new_member, initial_state.as_object().unwrap().keys().map(|k| k.to_owned()).collect()); - - // We define the process for pairing - let pairing_process = Process::new( - "pairing".to_owned(), - validation_rules, - String::default(), - String::default(), - String::default(), - Value::Null, - OutPoint::null() - ); + let new_process = create_process_from_template(pairing_template.to_string()).unwrap(); // we can update our local device now - pair_device(pairing_process.uuid.clone(), helper_get_bob_address(), initial_state.to_string()).unwrap(); + pair_device(new_process.uuid.clone(), vec![helper_get_bob_address()]).unwrap(); debug!("Alice sends a transaction commiting to an init prd to Bob"); - let args = CreateProcessInitTransactionArguments { - member2fields, - process: pairing_process, - stringified_pcd: initial_state.to_string() - }; - let alice_pairing_return = create_process_init_transaction(args, 1).unwrap(); + let alice_pairing_return = create_process_init_transaction(new_process, 1).unwrap(); let get_outputs_result = get_outputs().unwrap(); @@ -89,6 +96,8 @@ fn test_pairing() { // Alice parse her own transaction helper_parse_transaction(&alice_pairing_return.transaction, &alice_pairing_tweak_data).id; + // Notify user that we're waiting for confirmation from the other device + // ======================= Bob reset_device().unwrap(); create_device_from_sp_wallet(BOB_LOGIN_WALLET.to_owned()).unwrap(); @@ -138,10 +147,11 @@ fn test_pairing() { let keys = initial_state.keys; let mut pcd = initial_state.encrypted_pcd; pcd.decrypt_fields(&keys).unwrap(); - pair_device(relevant_process.get_process().uuid, helper_get_alice_address(), pcd.to_string()).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(); // Once we know this tx id, we can commit to the relay }