use std::collections::HashMap; use std::str::FromStr; use sdk_client::api::{ 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, response_prd, restore_device, set_process_cache, setup, ApiReturn, }; use sdk_common::log::debug; use sdk_common::pcd::{Member, Pcd, RoleDefinition}; use sdk_common::sp_client::bitcoin::OutPoint; use sdk_common::sp_client::spclient::OwnedOutput; use serde_json::{json, Value}; use tsify::JsValueSerdeExt; use wasm_bindgen_test::*; mod utils; use utils::*; wasm_bindgen_test_configure!(run_in_browser); #[wasm_bindgen_test] fn test_pairing() { setup(); debug!("==============================================\nStarting test_pairing\n=============================================="); // ========================= Alice reset_device().unwrap(); create_device_from_sp_wallet(ALICE_LOGIN_WALLET.to_owned()).unwrap(); // we get our own address let alice_address = get_address().unwrap(); // we scan the qr code or get the address by any other means let bob_address = helper_get_bob_address(); // Alice creates the new member with Bob address let new_member = Member::new(vec![ alice_address.as_str().try_into().unwrap(), bob_address.as_str().try_into().unwrap(), ]) .unwrap(); let pairing_init_state = json!({ "html": "", "style": "", "script": "", "description": "AliceBob", "roles": { "owner": { "members": [ new_member ], "validation_rules": [ { "quorum": 1.0, "fields": [ "roles", "pairing_tx" ], "min_sig_member": 1.0 } ] } }, "pairing_tx": OutPoint::null(), }); 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(); 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(); let (root_outpoint, alice_init_process) = alice_pairing_return.updated_process.unwrap(); let alice_pcd_commitment = Value::from_str( &alice_init_process .get_impending_requests() .get(0) .unwrap() .payload, ) .unwrap() .tagged_hash(); let pairing_tx = alice_pairing_return.new_tx_to_send.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(&pairing_tx, alice_outputs); // 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 // 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(); // TODO unpair device // We can produce the prd response now even if we can't use it yet let alice_prd_response = response_prd( root_outpoint, alice_init_process .get_impending_requests() .get(0) .unwrap() .to_string(), true, ) .unwrap() .ciphers_to_send .get(0) .unwrap() .clone(); // We can also create the first login transaction and sign it // this is only for testing, as we're playing both parts let alice_device = dump_device().unwrap(); let alice_processes = dump_process_cache().unwrap(); // ======================= 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(&pairing_tx, &alice_pairing_tweak_data); debug!("Bob receives the prd"); 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; } } } assert!(bob_retrieved_prd.ciphers_to_send.len() == 1); assert!(bob_retrieved_prd.updated_process.is_some()); debug!("Bob retrieved prd: {:#?}", bob_retrieved_prd); let (root_commitment, relevant_process) = bob_retrieved_prd.updated_process.unwrap(); let pcd_commitment = relevant_process .get_impending_requests() .get(0) .unwrap() .payload .clone(); let prd_confirm_cipher = bob_retrieved_prd.ciphers_to_send.iter().next().unwrap(); debug!("Bob sends a Confirm Prd to Alice"); // this is only for testing, as we're playing both parts 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(prd_confirm_cipher.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(); debug!("Bob parses Alice's pcd"); 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!("Alice proposal: {:#?}", alice_proposal); // get the pairing tx from the proposal let proposal = Value::from_str(&alice_proposal.get(0).unwrap()).unwrap(); debug!("proposal: {:#?}", proposal); let pairing_tx = proposal .get("pairing_tx") .unwrap() .as_str() .unwrap() .trim_matches('"'); let roles = proposal .get("roles") .and_then(|v| Value::from_str(v.as_str().unwrap()).ok()) .unwrap() .as_object() .unwrap() .iter() .map(|(role_name, role_value)| { let role_def: RoleDefinition = serde_json::from_value(role_value.clone())?; Ok((role_name.clone(), role_def)) }) .collect::, anyhow::Error>>(); let roles = roles.unwrap(); // we check that the proposal contains only one member assert!(roles.len() == 1); assert!(roles["owner"].members.len() == 1); // we get all the addresses of the members of the proposal let proposal_members = roles .iter() .flat_map(|(_, members)| members.members.iter().flat_map(|m| m.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(&alice_address)); assert!(proposal_members.contains(&bob_address)); assert!(proposal_members.len() == 2); // no free riders // We remove the local address, but maybe that's the responsibility of the Member type let proposal_members = proposal_members .into_iter() .filter(|m| m != &bob_address) .collect::>(); debug!("proposal_members: {:?}", proposal_members); // we can now show all the addresses + pairing tx to the user on device to prompt confirmation debug!("Bob pairs device with Alice"); pair_device(pairing_tx.to_owned(), proposal_members).unwrap(); // Bob signs the proposal and sends a prd response too let bob_prd_response = response_prd(root_commitment, pcd_commitment, true) .unwrap() .ciphers_to_send .get(0) .unwrap(); // To make the pairing effective, alice and bob must now creates a new transaction where they both control one output // login logic: user must have access to both devices to login // user must still have access to both devices in case an action needs both devices (as defined in the roles) // process can define that acting on some fields of the state only needs a fraction of the devices to sign // Problem: we need both devices to sign the next login transaction. // Simplest solution: device A creates the transaction with both inputs and outputs, signs its input and sends to device B // device B notify user that a login is underway, user either accepts (sign the transaction and broadcast), or refuses (actually we should think of some revokation step from here) // login(); // Once we know this tx id, we can commit to the relay }