sdk_client/tests/pairing.rs
2024-09-23 12:33:32 +02:00

270 lines
8.9 KiB
Rust

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": 0.0,
"fields": [
"roles",
"pairing_tx"
],
"min_sig_member": 0.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<OutPoint, OwnedOutput> = 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
// TODO pass the process as argument
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();
// 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::<Result<HashMap<String, RoleDefinition>, 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::<Vec<String>>();
// 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::<Vec<String>>();
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();
// Once we know this tx id, we can commit to the relay
}