Refactor the whole api for the new 2fa model + various improvements

This commit is contained in:
Sosthene 2024-07-19 23:11:16 +02:00
parent d737242bb5
commit 30759027b0
5 changed files with 817 additions and 147 deletions

View File

@ -5,7 +5,7 @@ edition = "2021"
[lib] [lib]
name = "sdk_client" name = "sdk_client"
crate-type = ["cdylib"] crate-type = ["lib", "cdylib"]
[dependencies] [dependencies]
anyhow = "1.0" anyhow = "1.0"
@ -17,7 +17,7 @@ wasm-logger = "0.2.0"
rand = "0.8.5" rand = "0.8.5"
log = "0.4.6" log = "0.4.6"
tsify = { git = "https://github.com/Sosthene00/tsify", branch = "next" } tsify = { git = "https://github.com/Sosthene00/tsify", branch = "next" }
#sdk_common = { path = "../../../sdk_common" } # sdk_common = { path = "../sdk_common" }
sdk_common = { git = "https://git.4nkweb.com/4nk/sdk_common.git", branch = "dev" } sdk_common = { git = "https://git.4nkweb.com/4nk/sdk_common.git", branch = "dev" }
shamir = { git = "https://github.com/Sosthene00/shamir", branch = "master" } shamir = { git = "https://github.com/Sosthene00/shamir", branch = "master" }
img-parts = "0.3.0" img-parts = "0.3.0"

View File

@ -35,7 +35,7 @@ use sdk_common::sp_client::silentpayments::{
use serde_json::{Error as SerdeJsonError, Value}; use serde_json::{Error as SerdeJsonError, Value};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tsify::Tsify; use tsify::{JsValueSerdeExt, Tsify};
use wasm_bindgen::convert::FromWasmAbi; use wasm_bindgen::convert::FromWasmAbi;
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
@ -43,16 +43,13 @@ use sdk_common::network::{
self, AnkFlag, AnkNetworkMsg, CachedMessage, CachedMessageStatus, CipherMessage, FaucetMessage, self, AnkFlag, AnkNetworkMsg, CachedMessage, CachedMessageStatus, CipherMessage, FaucetMessage,
NewTxMessage, NewTxMessage,
}; };
use sdk_common::silentpayments::{ use sdk_common::silentpayments::{create_transaction, map_outputs_to_sp_address};
create_transaction, create_transaction_for_address_with_shared_secret,
create_transaction_spend_outpoint, map_outputs_to_sp_address,
};
use crate::wallet::generate_sp_wallet;
use sdk_common::sp_client::spclient::{ use sdk_common::sp_client::spclient::{
derive_keys_from_seed, OutputList, OutputSpendStatus, OwnedOutput, Recipient, SpClient, derive_keys_from_seed, OutputList, OutputSpendStatus, OwnedOutput, Recipient, SpClient,
}; };
use sdk_common::sp_client::spclient::{SpWallet, SpendKey}; use sdk_common::sp_client::spclient::{SpWallet, SpendKey};
use crate::wallet::generate_sp_wallet;
use crate::user::{ use crate::user::{
lock_local_device, lock_spending_client, Device, RevokeOutput, LOCAL_DEVICE, SPENDING_CLIENT, lock_local_device, lock_spending_client, Device, RevokeOutput, LOCAL_DEVICE, SPENDING_CLIENT,
@ -61,13 +58,13 @@ use crate::{images, lock_messages, CACHEDMESSAGES};
use crate::process::Process; use crate::process::Process;
type ApiResult<T: FromWasmAbi> = Result<T, ApiError>; pub type ApiResult<T: FromWasmAbi> = Result<T, ApiError>;
const IS_TESTNET: bool = true; const IS_TESTNET: bool = true;
#[derive(Debug)] #[derive(Debug, PartialEq, Eq)]
struct ApiError { pub struct ApiError {
message: String, pub message: String,
} }
impl From<AnyhowError> for ApiError { impl From<AnyhowError> for ApiError {
@ -188,7 +185,55 @@ pub fn get_address() -> ApiResult<String> {
} }
#[wasm_bindgen] #[wasm_bindgen]
pub fn create_new_wallet(birthday: u32, network_str: String) -> ApiResult<String> { pub fn restore_device_from_sp_wallet(sp_wallet: String) -> ApiResult<String> {
let wallet: SpWallet = serde_json::from_str(&sp_wallet)?;
let mut device = Device::new(
wallet.get_client().get_scan_key(),
wallet.get_client().get_spend_key().into(),
wallet.get_client().get_network(),
);
let birthday = wallet.get_outputs().get_birthday();
let outputs = wallet.get_outputs().to_outpoints_list();
device
.get_watch_only_mut()
.get_mut_outputs()
.set_birthday(birthday);
device
.get_watch_only_mut()
.get_mut_outputs()
.extend_from(outputs);
let json_return = serde_json::to_string(&device)?;
// Set the LOCAL_DEVICE const with the new value
let mut local_device = lock_local_device()?;
if *local_device.get_watch_only().get_client() == SpClient::default() {
*local_device = device;
} else {
return Err(ApiError {
message: "device is already initialized".to_owned(),
});
}
// Set the LOGGED_WALLET with the new wallet to keep it in memory while we wait for the linking
let mut spending_client = lock_spending_client()?;
if *spending_client == SpClient::default() {
*spending_client = wallet.get_client().clone();
} else {
return Err(ApiError {
message: "device is already initialized".to_owned(),
});
}
Ok(json_return)
}
#[wasm_bindgen]
pub fn create_new_device(birthday: u32, network_str: String) -> ApiResult<String> {
let network = Network::from_core_arg(&network_str)?; let network = Network::from_core_arg(&network_str)?;
let wallet = generate_sp_wallet(None, Network::Regtest)?; let wallet = generate_sp_wallet(None, Network::Regtest)?;
@ -207,20 +252,32 @@ pub fn create_new_wallet(birthday: u32, network_str: String) -> ApiResult<String
let our_address = device.get_watch_only().get_client().get_receiving_address(); let our_address = device.get_watch_only().get_client().get_receiving_address();
// Set the LOCAL_DEVICE const with the new value // Set the LOCAL_DEVICE const with the new value
LOCAL_DEVICE let mut local_device = lock_local_device()?;
.set(Mutex::new(device))
.expect("We shouldn't already have initialized LOCAL_DEVICE now"); if *local_device.get_watch_only().get_client() == SpClient::default() {
*local_device = device;
} else {
return Err(ApiError {
message: "device is already initialized".to_owned(),
});
}
// Set the LOGGED_WALLET with the new wallet to keep it in memory while we wait for the linking // Set the LOGGED_WALLET with the new wallet to keep it in memory while we wait for the linking
SPENDING_CLIENT let mut spending_client = lock_spending_client()?;
.set(Mutex::new(wallet.get_client().clone()))
.expect("We shouldn't already have initialized SPENDING_CLIENT now"); if *spending_client == SpClient::default() {
*spending_client = wallet.get_client().clone();
} else {
return Err(ApiError {
message: "device is already initialized".to_owned(),
});
}
Ok(our_address) Ok(our_address)
} }
#[wasm_bindgen] #[wasm_bindgen]
pub fn update_linked_address( pub fn pair_device(
spend_sk_cipher: Vec<u8>, spend_sk_cipher: Vec<u8>,
linked_with: String, linked_with: String,
revokation_output: String, revokation_output: String,
@ -327,6 +384,104 @@ pub fn login_user(fee_rate: u32) -> ApiResult<()> {
Ok(()) Ok(())
} }
#[wasm_bindgen]
pub fn logout() -> ApiResult<()> {
let mut spending_client = lock_spending_client()?;
*spending_client = SpClient::default();
Ok(())
}
#[wasm_bindgen]
pub fn dump_watch_only_wallet() -> ApiResult<String> {
let device = lock_local_device()?;
Ok(serde_json::to_string(device.get_watch_only()).unwrap())
}
#[wasm_bindgen]
pub fn reset_message_cache() -> ApiResult<()> {
let mut cached_msg = lock_messages()?;
*cached_msg = vec![];
debug_assert!(cached_msg.is_empty());
Ok(())
}
#[wasm_bindgen]
pub fn set_message_cache(msg_cache: Vec<String>) -> ApiResult<()> {
let mut cached_msg = lock_messages()?;
if !cached_msg.is_empty() {
return Err(ApiError {
message: "Message cache not empty".to_owned(),
});
}
let new_cache: Result<Vec<CachedMessage>, serde_json::Error> =
msg_cache.iter().map(|m| serde_json::from_str(m)).collect();
*cached_msg = new_cache?;
Ok(())
}
#[wasm_bindgen]
pub fn dump_message_cache() -> ApiResult<Vec<String>> {
let cached_msg = lock_messages()?;
let res: Vec<String> = cached_msg
.iter()
.map(|m| serde_json::to_string(m).unwrap())
.collect();
Ok(res)
}
#[wasm_bindgen]
pub fn reset_device() -> ApiResult<()> {
let mut spending_client = lock_spending_client()?;
*spending_client = SpClient::default();
let mut device = lock_local_device()?;
*device = Device::default();
reset_message_cache()?;
Ok(())
}
#[wasm_bindgen]
pub fn encrypt_remote_key(
remote_key: String,
aes_key: Option<String>,
nonce: Option<String>,
) -> ApiResult<String> {
let mut device = lock_local_device()?;
let encrypted_key: Vec<u8>;
if let (Some(key_hex), Some(nonce_hex)) = (aes_key, nonce) {
// check that the hex we got for key and nonce is valid
let mut key = [0u8; 32];
let mut nonce = [0u8; 12];
key.copy_from_slice(&Vec::from_hex(&key_hex)?);
nonce.copy_from_slice(&Vec::from_hex(&nonce_hex)?);
encrypted_key = device.encrypt_for_remote_device(
SecretKey::from_str(&remote_key)?,
Some(key),
Some(nonce),
)?;
} else {
encrypted_key =
device.encrypt_for_remote_device(SecretKey::from_str(&remote_key)?, None, None)?;
}
Ok(encrypted_key.to_lower_hex_string())
}
fn handle_recover_transaction( fn handle_recover_transaction(
updated: HashMap<OutPoint, OwnedOutput>, updated: HashMap<OutPoint, OwnedOutput>,
tx: &Transaction, tx: &Transaction,
@ -357,48 +512,37 @@ fn handle_recover_transaction(
// empty utxo_destroyed means we received this transaction // empty utxo_destroyed means we received this transaction
if utxo_destroyed.is_empty() { if utxo_destroyed.is_empty() {
// We first check for faucet transactions // We first check for faucet transactions
if let Some(pos) = messages.iter().position(|m| { if let Some(message) = messages.iter_mut().find(|m| {
if m.status == CachedMessageStatus::FaucetWaiting { if m.status == CachedMessageStatus::FaucetWaiting {
m.commitment.as_ref() == Some(&commitment_str) m.commitment.as_ref() == Some(&commitment_str)
} else { } else {
false false
} }
}) { }) {
let message = messages.get_mut(pos).unwrap(); message.status = CachedMessageStatus::Closed;
match message.status { message.commited_in = utxo_created
CachedMessageStatus::FaucetWaiting => { .into_iter()
message.status = CachedMessageStatus::FaucetComplete; .next()
message.commited_in = utxo_created .map(|(outpoint, _)| *outpoint);
.into_iter() return Ok(message.clone());
.next()
.map(|(outpoint, _)| *outpoint);
return Ok(message.clone());
}
// Actually this is unreachable
CachedMessageStatus::FaucetComplete => return Ok(message.clone()),
_ => (),
}
} }
// we inspect inputs looking for links with previous tx // we inspect inputs looking for links with previous tx
for input in tx.input.iter() { for input in tx.input.iter() {
if let Some(pos) = messages.iter().position(|m| { if let Some(message) = messages
debug!("{:?}", Some(input.previous_output)); .iter_mut()
m.confirmed_by == Some(input.previous_output) .find(|m| m.confirmed_by == Some(input.previous_output))
}) { {
let message = messages.get_mut(pos).unwrap(); // If we are receiver, that's pretty much it
// If we are receiver, that's pretty much it, just set status to complete message.status = CachedMessageStatus::Trusted;
message.status = CachedMessageStatus::Complete;
return Ok(message.clone()); return Ok(message.clone());
} else if let Some(pos) = messages } else if let Some(message) = messages
.iter() .iter_mut()
.position(|m| m.commited_in == Some(input.previous_output)) .find(|m| m.commited_in == Some(input.previous_output))
{ {
// sender needs to spent it back again to receiver // sender needs to spent it back again to receiver
let (outpoint, output) = utxo_created.into_iter().next().unwrap(); let (outpoint, output) = utxo_created.into_iter().next().unwrap();
let message = messages.get_mut(pos).unwrap();
message.confirmed_by = Some(outpoint.clone()); message.confirmed_by = Some(outpoint.clone());
message.status = CachedMessageStatus::MustSpendConfirmation; message.status = CachedMessageStatus::MustSpendConfirmation;
@ -420,7 +564,7 @@ fn handle_recover_transaction(
); );
let mut plaintext: Vec<u8> = vec![]; let mut plaintext: Vec<u8> = vec![];
if let Some(cipher_pos) = messages.iter().position(|m| { if let Some(message) = messages.iter_mut().find(|m| {
if m.status != CachedMessageStatus::CipherWaitingTx { if m.status != CachedMessageStatus::CipherWaitingTx {
return false; return false;
} }
@ -432,15 +576,14 @@ fn handle_recover_transaction(
return false; return false;
} }
}) { }) {
let message = messages.get_mut(cipher_pos).unwrap();
let (outpoint, output) = utxo_created.into_iter().next().unwrap(); let (outpoint, output) = utxo_created.into_iter().next().unwrap();
let cipher_msg: CipherMessage = serde_json::from_slice(&plaintext)?; let cipher_msg: CipherMessage = serde_json::from_slice(&plaintext)?;
message.commited_in = Some(outpoint.clone()); message.commited_in = Some(outpoint.clone());
message.shared_secret = Some(shared_secret.to_byte_array().to_lower_hex_string()); message.shared_secret = Some(shared_secret.to_byte_array().to_lower_hex_string());
message.commitment = Some(commitment_str); message.commitment = Some(commitment_str);
message.plaintext = Some(cipher_msg.message); message.plaintext.push(cipher_msg.message);
message.ciphertext = None;
message.sender = Some(cipher_msg.sender); message.sender = Some(cipher_msg.sender);
message.recipient = Some(sp_wallet.get_client().get_receiving_address()); message.recipient = Some(sp_wallet.get_client().get_receiving_address());
message.status = CachedMessageStatus::ReceivedMustConfirm; message.status = CachedMessageStatus::ReceivedMustConfirm;
@ -466,7 +609,6 @@ fn handle_recover_transaction(
// We only need to return the message // We only need to return the message
// eiter this is notification, a challenge, or response to a challenge // eiter this is notification, a challenge, or response to a challenge
// if notification, commitment is the same than in the message // if notification, commitment is the same than in the message
// if challenge or response, commitment is H(commitment | b_scan), b_scan being different depending on who we are
if let Some(message) = messages.iter().find(|m| { if let Some(message) = messages.iter().find(|m| {
if commitment.is_empty() || m.commitment.is_none() { if commitment.is_empty() || m.commitment.is_none() {
return false; return false;
@ -480,23 +622,29 @@ fn handle_recover_transaction(
.unwrap() .unwrap()
== commitment == commitment
} }
CachedMessageStatus::MustSpendConfirmation CachedMessageStatus::ReceivedMustConfirm => {
| CachedMessageStatus::ReceivedMustConfirm => { // we look for a message that has commited_in as input of the transaction
// we compute the potential commitment if let Some(_) = tx
let m_commitment = m .input
.commitment .iter()
.as_ref() .find(|i| Some(i.previous_output) == m.commited_in)
.map(|c| Vec::from_hex(&c).unwrap()) {
.unwrap(); return true;
let mut buf = [0u8; 64]; } else {
buf[..32].copy_from_slice(&m_commitment); return false;
buf[32..] }
.copy_from_slice(&sp_wallet.get_client().get_scan_key().secret_bytes()); }
CachedMessageStatus::MustSpendConfirmation | CachedMessageStatus::Trusted => {
let mut engine = sha256::HashEngine::default(); // we look for a message that has confirm_by as input
engine.write_all(&buf).unwrap(); if let Some(_) = tx
let hash = sha256::Hash::from_engine(engine); .input
hash.to_byte_array().to_vec() == commitment .iter()
.find(|i| Some(i.previous_output) == m.confirmed_by)
{
return true;
} else {
return false;
}
} }
_ => return false, _ => return false,
} }
@ -504,7 +652,7 @@ fn handle_recover_transaction(
return Ok(message.clone()); return Ok(message.clone());
} else { } else {
return Err(anyhow::Error::msg( return Err(anyhow::Error::msg(
"We spent a transaction for a commitment we don't know", "Failed to find the message for one of our transactions",
)); ));
} }
} }
@ -573,21 +721,35 @@ pub fn parse_network_msg(raw: String, fee_rate: u32) -> ApiResult<CachedMessage>
// let's try to decrypt with keys we found in transactions but haven't used yet // let's try to decrypt with keys we found in transactions but haven't used yet
let mut messages = lock_messages()?; let mut messages = lock_messages()?;
let cipher = Vec::from_hex(&ank_msg.content.trim_matches('\"'))?; let cipher = Vec::from_hex(&ank_msg.content.trim_matches('\"'))?;
let cipher_pos = messages.iter().position(|m| { if let Some(message) = messages.iter_mut().find(|m| {
debug!("Trying message: {:?}", m); // debug!("Trying message: {:?}", m);
if m.status != CachedMessageStatus::TxWaitingCipher { match m.status {
return false; CachedMessageStatus::TxWaitingCipher | CachedMessageStatus::Trusted => {
m.try_decrypt_cipher(cipher.clone()).is_ok()
}
_ => return false,
} }
m.try_decrypt_cipher(cipher.clone()).is_ok() }) {
});
if cipher_pos.is_some() {
let mut message = messages.get_mut(cipher_pos.unwrap()).unwrap();
let plain = message.try_decrypt_cipher(cipher).unwrap(); let plain = message.try_decrypt_cipher(cipher).unwrap();
let cipher_msg: CipherMessage = serde_json::from_slice(&plain)?; let cipher_msg: CipherMessage = serde_json::from_slice(&plain)?;
message.plaintext = Some(cipher_msg.message); if message.status == CachedMessageStatus::TxWaitingCipher {
message.sender = Some(cipher_msg.sender); // does the retrieved message match with the commited hash?
message.ciphertext = Some(ank_msg.content); let hash = create_commitment(serde_json::to_string(&cipher_msg)?);
message.status = CachedMessageStatus::ReceivedMustConfirm; if Some(hash) != message.commitment {
return Err(ApiError {
message: "Message doesn't match commitment".to_owned(),
});
}
message.plaintext.push(cipher_msg.message);
message.sender = Some(cipher_msg.sender);
message.ciphertext = None;
message.status = CachedMessageStatus::ReceivedMustConfirm;
} else {
// We're receiving a message for some action already engaged
// We could check the sender, but do we really care since we alredy trust it?
// Let's update the message by pushing what we just found out
message.plaintext.push(cipher_msg.message);
}
return Ok(message.clone()); return Ok(message.clone());
} else { } else {
// let's keep it in case we receive the transaction later // let's keep it in case we receive the transaction later
@ -608,20 +770,20 @@ pub fn parse_network_msg(raw: String, fee_rate: u32) -> ApiResult<CachedMessage>
} }
#[wasm_bindgen] #[wasm_bindgen]
pub fn get_outpoints_for_user() -> ApiResult<outputs_list> { pub fn get_outputs() -> ApiResult<JsValue> {
let device = lock_local_device()?; let device = lock_local_device()?;
let outputs = device.get_watch_only().get_outputs().clone(); let outputs = device.get_watch_only().get_outputs().clone();
Ok(outputs_list(outputs)) Ok(JsValue::from_serde(&outputs.to_outpoints_list())?)
} }
#[wasm_bindgen] #[wasm_bindgen]
pub fn get_available_amount_for_user() -> ApiResult<u64> { pub fn get_available_amount() -> ApiResult<u64> {
let device = lock_local_device()?; let device = lock_local_device()?;
Ok(device.get_watch_only().get_outputs().get_balance().to_sat()) Ok(device.get_watch_only().get_outputs().get_balance().to_sat())
} }
#[derive(Tsify, Serialize, Deserialize, Default)] #[derive(Debug, Tsify, Serialize, Deserialize, Default)]
#[tsify(into_wasm_abi, from_wasm_abi)] #[tsify(into_wasm_abi, from_wasm_abi)]
#[allow(non_camel_case_types)] #[allow(non_camel_case_types)]
pub struct createTransactionReturn { pub struct createTransactionReturn {
@ -661,7 +823,7 @@ pub fn answer_confirmation_transaction(
let spending_client = lock_spending_client()?.clone(); let spending_client = lock_spending_client()?.clone();
let sp_wallet = SpWallet::new(spending_client, Some(current_outputs))?; let sp_wallet = SpWallet::new(spending_client, Some(current_outputs), vec![])?;
let recipient = Recipient { let recipient = Recipient {
address: sp_address.into(), address: sp_address.into(),
@ -669,21 +831,20 @@ pub fn answer_confirmation_transaction(
nb_outputs: 1, nb_outputs: 1,
}; };
let confirmed_by = message.confirmed_by.clone().unwrap(); let confirmed_by = message.confirmed_by.as_ref().unwrap();
let commited_in = message.commited_in.clone().unwrap();
let signed_psbt = create_transaction_spend_outpoint( let signed_psbt = create_transaction(
&confirmed_by, &vec![confirmed_by],
&sp_wallet, &sp_wallet,
recipient, recipient,
&commited_in.txid,
None, None,
Amount::from_sat(fee_rate.into()), Amount::from_sat(fee_rate.into()),
message.recipient.clone(),
)?; )?;
let final_tx = signed_psbt.extract_tx()?; let final_tx = signed_psbt.extract_tx()?;
message.status = CachedMessageStatus::Complete; message.status = CachedMessageStatus::Trusted;
Ok(createTransactionReturn { Ok(createTransactionReturn {
txid: final_tx.txid().to_string(), txid: final_tx.txid().to_string(),
@ -701,7 +862,7 @@ pub fn create_confirmation_transaction(
let mut messages = lock_messages()?; let mut messages = lock_messages()?;
let message: &mut CachedMessage; let message: &mut CachedMessage;
if let Some(m) = messages.iter_mut().find(|m| m.id == message_id) { if let Some(m) = messages.iter_mut().find(|m| m.id == message_id) {
if m.sender.is_none() || m.commited_in.is_none() { if m.sender.is_none() || m.commited_in.is_none() || m.plaintext.is_empty() {
return Err(ApiError { return Err(ApiError {
message: "Invalid network message".to_owned(), message: "Invalid network message".to_owned(),
}); });
@ -716,22 +877,11 @@ pub fn create_confirmation_transaction(
let sp_address: SilentPaymentAddress = message.sender.as_ref().unwrap().as_str().try_into()?; let sp_address: SilentPaymentAddress = message.sender.as_ref().unwrap().as_str().try_into()?;
let mut local_device = lock_local_device()?; let current_outputs = lock_local_device()?.get_watch_only().get_outputs().clone();
// Are we waiting for pairing?
let remote_key_cipher: Option<Vec<u8>>;
if !local_device.is_linked() {
let remote_spend_sk = SecretKey::from_str(message.plaintext.as_deref().unwrap())?;
remote_key_cipher = Some(local_device.encrypt_for_remote_device(remote_spend_sk)?);
} else {
remote_key_cipher = None;
}
let current_outputs = local_device.get_watch_only().get_outputs().clone();
let spending_client = lock_spending_client()?.clone(); let spending_client = lock_spending_client()?.clone();
let sp_wallet = SpWallet::new(spending_client, Some(current_outputs))?; let sp_wallet = SpWallet::new(spending_client, Some(current_outputs), vec![])?;
let recipient = Recipient { let recipient = Recipient {
address: sp_address.into(), address: sp_address.into(),
@ -739,15 +889,15 @@ pub fn create_confirmation_transaction(
nb_outputs: 1, nb_outputs: 1,
}; };
let commited_in = message.commited_in.clone().unwrap(); let commited_in = message.commited_in.as_ref().unwrap();
let signed_psbt = create_transaction_spend_outpoint( let signed_psbt = create_transaction(
&commited_in, &vec![commited_in],
&sp_wallet, &sp_wallet,
recipient, recipient,
&commited_in.txid, None,
remote_key_cipher,
Amount::from_sat(fee_rate.into()), Amount::from_sat(fee_rate.into()),
message.sender.clone(),
)?; )?;
// what's the vout of the output sent to sender? // what's the vout of the output sent to sender?
@ -778,18 +928,37 @@ pub fn create_pairing_transaction(
) -> ApiResult<createTransactionReturn> { ) -> ApiResult<createTransactionReturn> {
let message: CipherMessage; let message: CipherMessage;
{ {
let mut spending_wallet = lock_spending_client()?; let mut spending_client = lock_spending_client()?;
let our_address = spending_wallet.get_receiving_address(); let our_address = spending_client.get_receiving_address();
let spend_sk: SecretKey = spending_wallet.get_spend_key().try_into()?; let spend_sk: SecretKey = spending_client.get_spend_key().try_into()?;
message = CipherMessage::new(our_address, format!("{}", spend_sk.display_secret())); message = CipherMessage::new(our_address, format!("{}", spend_sk.display_secret()));
// we forget our own wallet
*spending_wallet = SpClient::default();
} }
create_notification_transaction(address, message, fee_rate) let mut res = create_notification_transaction(address, message, fee_rate);
// we slightly alter the message
if let Ok(ref mut new_msg) = res {
new_msg.new_network_msg.plaintext = vec![]; // we don't want to let that information available
new_msg.new_network_msg.status = CachedMessageStatus::Trusted;
} else {
return res;
}
// Update the msg in cache
let mut messages = lock_messages()?;
let mut updated = res.as_ref().unwrap().new_network_msg.clone();
let pos = messages.iter().position(|m| m.id == updated.id).unwrap();
// clone the cached message and erase the ciphertext
updated.ciphertext = None;
messages[pos] = updated;
// we forget our own wallet
let mut spending_client = lock_spending_client()?;
*spending_client = SpClient::default();
res
} }
#[wasm_bindgen] #[wasm_bindgen]
@ -815,7 +984,7 @@ pub fn create_login_transaction(fee_rate: u32) -> ApiResult<createTransactionRet
#[wasm_bindgen] #[wasm_bindgen]
pub fn create_notification_transaction( pub fn create_notification_transaction(
address: String, address: String,
message: CipherMessage, cipher_message: CipherMessage,
fee_rate: u32, fee_rate: u32,
) -> ApiResult<createTransactionReturn> { ) -> ApiResult<createTransactionReturn> {
let sp_address: SilentPaymentAddress = address.as_str().try_into()?; let sp_address: SilentPaymentAddress = address.as_str().try_into()?;
@ -826,7 +995,7 @@ pub fn create_notification_transaction(
let spending_client = lock_spending_client()?.clone(); let spending_client = lock_spending_client()?.clone();
let sp_wallet = SpWallet::new(spending_client, Some(current_outputs))?; let sp_wallet = SpWallet::new(spending_client, Some(current_outputs), vec![])?;
let recipient = Recipient { let recipient = Recipient {
address: sp_address.into(), address: sp_address.into(),
@ -834,18 +1003,20 @@ pub fn create_notification_transaction(
nb_outputs: 1, nb_outputs: 1,
}; };
let commitment = create_commitment(serde_json::to_string(&message)?); let commitment = create_commitment(serde_json::to_string(&cipher_message)?);
let signed_psbt = create_transaction_for_address_with_shared_secret( let signed_psbt = create_transaction(
recipient, &vec![],
&sp_wallet, &sp_wallet,
Some(&commitment), recipient,
Some(Vec::from_hex(&commitment)?),
Amount::from_sat(fee_rate.into()), Amount::from_sat(fee_rate.into()),
None,
)?; )?;
let psbt = Psbt::from_str(&signed_psbt)?; let partial_secret = sp_wallet
.get_client()
let partial_secret = sp_wallet.get_client().get_partial_secret_from_psbt(&psbt)?; .get_partial_secret_from_psbt(&signed_psbt)?;
let shared_point = sp_utils::sending::calculate_ecdh_shared_secret( let shared_point = sp_utils::sending::calculate_ecdh_shared_secret(
&sp_address.get_scan_key(), &sp_address.get_scan_key(),
@ -854,26 +1025,26 @@ pub fn create_notification_transaction(
let shared_secret = AnkSharedSecret::new(shared_point); let shared_secret = AnkSharedSecret::new(shared_point);
debug!( // debug!(
"Created transaction with secret {}", // "Created transaction with secret {}",
shared_secret.to_byte_array().to_lower_hex_string() // shared_secret.to_byte_array().to_lower_hex_string()
); // );
let cipher = encrypt_with_key( let cipher = encrypt_with_key(
serde_json::to_string(&message)?, serde_json::to_string(&cipher_message)?,
shared_secret.to_byte_array().to_lower_hex_string(), shared_secret.to_byte_array().to_lower_hex_string(),
)?; )?;
// update our cache // update our cache
let sp_address2vouts = map_outputs_to_sp_address(&signed_psbt)?; let sp_address2vouts = map_outputs_to_sp_address(&signed_psbt.to_string())?;
let recipients_vouts = sp_address2vouts let recipients_vouts = sp_address2vouts
.get::<String>(&address) .get::<String>(&address)
.expect("recipients didn't change") .expect("recipients didn't change")
.as_slice(); .as_slice();
// for now let's just take the smallest vout that belongs to the recipient // for now let's just take the smallest vout that belongs to the recipient
let final_tx = psbt.extract_tx()?; let final_tx = signed_psbt.extract_tx()?;
let mut new_msg = CachedMessage::new(); let mut new_msg = CachedMessage::new();
new_msg.plaintext = Some(message.message); new_msg.plaintext.push(cipher_message.message);
new_msg.ciphertext = Some(cipher); new_msg.ciphertext = Some(cipher);
new_msg.commitment = Some(commitment); new_msg.commitment = Some(commitment);
new_msg.commited_in = Some(OutPoint { new_msg.commited_in = Some(OutPoint {
@ -882,7 +1053,7 @@ pub fn create_notification_transaction(
}); });
new_msg.shared_secret = Some(shared_secret.to_byte_array().to_lower_hex_string()); new_msg.shared_secret = Some(shared_secret.to_byte_array().to_lower_hex_string());
new_msg.recipient = Some(address); new_msg.recipient = Some(address);
new_msg.sender = Some(sp_wallet.get_client().get_receiving_address()); new_msg.sender = Some(cipher_message.sender);
new_msg.status = CachedMessageStatus::SentWaitingConfirmation; new_msg.status = CachedMessageStatus::SentWaitingConfirmation;
lock_messages()?.push(new_msg.clone()); lock_messages()?.push(new_msg.clone());

View File

@ -28,8 +28,6 @@ use sdk_common::crypto::{
AeadCore, Aes256Decryption, Aes256Encryption, Aes256Gcm, HalfKey, KeyInit, Purpose, AeadCore, Aes256Decryption, Aes256Encryption, Aes256Gcm, HalfKey, KeyInit, Purpose,
}; };
pub static LOCAL_DEVICE: OnceLock<Mutex<Device>> = OnceLock::new(); pub static LOCAL_DEVICE: OnceLock<Mutex<Device>> = OnceLock::new();
pub fn lock_local_device() -> Result<MutexGuard<'static, Device>> { pub fn lock_local_device() -> Result<MutexGuard<'static, Device>> {
@ -82,7 +80,7 @@ impl Device {
.expect("watch_only_client creation failed"); .expect("watch_only_client creation failed");
let mut watch_only_wallet = let mut watch_only_wallet =
SpWallet::new(watch_only_client, None).expect("watch_only_wallet creation failed"); SpWallet::new(watch_only_client, None, vec![]).expect("watch_only_wallet creation failed");
let spend_sk_cipher = vec![]; let spend_sk_cipher = vec![];
let remote_device_key = [0; 32]; let remote_device_key = [0; 32];
@ -116,12 +114,34 @@ impl Device {
self.spend_sk_cipher.clone() self.spend_sk_cipher.clone()
} }
pub fn encrypt_for_remote_device(&mut self, remote_spend_sk: SecretKey) -> Result<Vec<u8>> { pub fn encrypt_for_remote_device(
// encrypt with the remote_device_key &mut self,
let encryption = Aes256Encryption::new( remote_spend_sk: SecretKey,
Purpose::ThirtyTwoBytes, aes_key: Option<[u8; 32]>,
remote_spend_sk.secret_bytes().to_vec(), nonce: Option<[u8; 12]>,
)?; ) -> Result<Vec<u8>> {
let encryption: Aes256Encryption;
match (aes_key, nonce) {
(Some(key), Some(nonce)) => {
encryption = Aes256Encryption::import_key(
Purpose::ThirtyTwoBytes,
remote_spend_sk.secret_bytes().to_vec(),
key,
nonce,
)?;
}
(None, None) => {
encryption = Aes256Encryption::new(
Purpose::ThirtyTwoBytes,
remote_spend_sk.secret_bytes().to_vec(),
)?;
}
_ => {
return Err(Error::msg(
"aes_key and nonce must either both be set or empty",
))
}
}
let remote_spend_sk_cipher = encryption.encrypt_with_aes_key(); let remote_spend_sk_cipher = encryption.encrypt_with_aes_key();
self.remote_device_key = encryption.export_key(); self.remote_device_key = encryption.export_key();
remote_spend_sk_cipher remote_spend_sk_cipher

View File

@ -22,7 +22,7 @@ pub fn generate_sp_wallet(label: Option<String>, network: Network) -> anyhow::Re
our_address.to_string() our_address.to_string()
); );
let res = SpWallet::new(sp_client, None)?; let res = SpWallet::new(sp_client, None, vec![])?;
Ok(res) Ok(res)
} }

479
tests/browser.rs Normal file
View File

@ -0,0 +1,479 @@
use std::collections::HashMap;
use log::debug;
use sdk_client::api::{
answer_confirmation_transaction, create_confirmation_transaction,
create_notification_transaction, encrypt_remote_key, get_outputs, pair_device,
parse_network_msg, reset_device, restore_device_from_sp_wallet, set_message_cache, setup,
};
use sdk_common::network::{
AnkNetworkMsg, CachedMessage, CipherMessage, NewTxMessage, TrustedChannel,
};
use sdk_common::sp_client::bitcoin::consensus::deserialize;
use sdk_common::sp_client::bitcoin::hex::FromHex;
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 serde_json;
use tsify::JsValueSerdeExt;
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
// We're using alice and bob for clarity, but it's important to remember that here Alice and Bob are the same person
const ALICE_START_WALLET: &str = "{\"client\":{\"network\":\"regtest\",\"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]]]}},\"outputs\":{\"wallet_fingerprint\":[187,119,108,230,171,125,106,11],\"birthday\":1620,\"last_scan\":2146,\"outputs\":{\"9a4a67cc5a40bf882d8b300d91024d7c97024b3b68b2df7745a5b9ea1df1888c:1\":{\"blockheight\":1620,\"tweak\":\"b8b63b3ed97d297b744135cfac2fb4a344c881a77543b71f1fcd16bc67514f26\",\"amount\":3938643,\"script\":\"51205b7b324bb71d411e32f2c61fda5d1db23f5c7d6d416a77fab87c913a1b120be1\",\"label\":\"ac14a827e2d023b8f7804303a47259366117d99ed932b641d4a8eaf1b82cc992\",\"spend_status\":\"Unspent\"}}}}";
const ALICE_CHALLENGE_WALLET: &str = "{\"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\":0,\"outputs\":{\"d5edbf1f60ae4fd7d698626b44ed1e85edddf65b45f62e5f82928a9e12759db9:1\":{\"blockheight\":0,\"tweak\":\"28994b2f2ee8e5f35d6d2dcdee1580d0455fe3dc37f81e0a36864473ee86f5c4\",\"amount\":3937246,\"script\":\"51207d06144e982b6fd38a85d6152f1f95746b059553258a31e04df97fe6b5f19ea1\",\"label\":\"ac14a827e2d023b8f7804303a47259366117d99ed932b641d4a8eaf1b82cc992\",\"spend_status\":\"Unspent\"},\"9a4a67cc5a40bf882d8b300d91024d7c97024b3b68b2df7745a5b9ea1df1888c:1\":{\"blockheight\":1620,\"tweak\":\"b8b63b3ed97d297b744135cfac2fb4a344c881a77543b71f1fcd16bc67514f26\",\"amount\":3938643,\"script\":\"51205b7b324bb71d411e32f2c61fda5d1db23f5c7d6d416a77fab87c913a1b120be1\",\"label\":\"ac14a827e2d023b8f7804303a47259366117d99ed932b641d4a8eaf1b82cc992\",\"spend_status\":{\"Spent\":\"d5edbf1f60ae4fd7d698626b44ed1e85edddf65b45f62e5f82928a9e12759db9\"}}}}}";
const ALICE_ANSWER_WALLET: &str = "{\"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\":0,\"outputs\":{\"57ca073676fa6130397ce1e4738278952483cee943daa1664b9a1807d4700066:0\":{\"blockheight\":0,\"tweak\":\"b99660cd873026aebeb378ae2ff32aa1c79a5946c462a36a01247f8afcfc5dba\",\"amount\":1046,\"script\":\"51209b80b6e8b3e93437c82a26c977860fb4d2bef9f27f6332f811a41187592502ed\",\"label\":null,\"spend_status\":\"Unspent\"},\"d5edbf1f60ae4fd7d698626b44ed1e85edddf65b45f62e5f82928a9e12759db9:1\":{\"blockheight\":0,\"tweak\":\"28994b2f2ee8e5f35d6d2dcdee1580d0455fe3dc37f81e0a36864473ee86f5c4\",\"amount\":3937246,\"script\":\"51207d06144e982b6fd38a85d6152f1f95746b059553258a31e04df97fe6b5f19ea1\",\"label\":\"ac14a827e2d023b8f7804303a47259366117d99ed932b641d4a8eaf1b82cc992\",\"spend_status\":\"Unspent\"},\"9a4a67cc5a40bf882d8b300d91024d7c97024b3b68b2df7745a5b9ea1df1888c:1\":{\"blockheight\":1620,\"tweak\":\"b8b63b3ed97d297b744135cfac2fb4a344c881a77543b71f1fcd16bc67514f26\",\"amount\":3938643,\"script\":\"51205b7b324bb71d411e32f2c61fda5d1db23f5c7d6d416a77fab87c913a1b120be1\",\"label\":\"ac14a827e2d023b8f7804303a47259366117d99ed932b641d4a8eaf1b82cc992\",\"spend_status\":{\"Spent\":\"d5edbf1f60ae4fd7d698626b44ed1e85edddf65b45f62e5f82928a9e12759db9\"}}}}}";
const BOB_START_WALLET: &str = "{\"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\":{\"fbd9c63e0dd08c2569b51a0d6095a79ee2acfcac66acdb594328a095f1fadb63:1\":{\"blockheight\":2146,\"tweak\":\"678dbcbdb40cd3733c8dbbd508761a0937009cf75a9f466e3c98877e79037cbc\",\"amount\":99896595,\"script\":\"5120deab0c5a3d23de30477b0b5a95a261c96e29afdd9813c665d2bf025ad2b3f919\",\"label\":null,\"spend_status\":\"Unspent\"}}}}";
const BOB_CHALLENGE_WALLET: &str = "{\"client\":{\"network\":\"regtest\",\"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]]]}},\"outputs\":{\"wallet_fingerprint\":[203,200,4,248,139,36,241,232],\"birthday\":2146,\"last_scan\":0,\"outputs\":{\"fbd9c63e0dd08c2569b51a0d6095a79ee2acfcac66acdb594328a095f1fadb63:1\":{\"blockheight\":2146,\"tweak\":\"678dbcbdb40cd3733c8dbbd508761a0937009cf75a9f466e3c98877e79037cbc\",\"amount\":99896595,\"script\":\"5120deab0c5a3d23de30477b0b5a95a261c96e29afdd9813c665d2bf025ad2b3f919\",\"label\":null,\"spend_status\":\"Unspent\"},\"d5edbf1f60ae4fd7d698626b44ed1e85edddf65b45f62e5f82928a9e12759db9:0\":{\"blockheight\":0,\"tweak\":\"0e3395ff27bde9187ffaeeb2521f6277d3b83911f16ccbaf59a1a68d99a0ab93\",\"amount\":1200,\"script\":\"512010f06f764cbc923ec3198db946307bf0c06a1b4f09206055e47a6fec0a33d52c\",\"label\":null,\"spend_status\":\"Unspent\"}}}}";
const BOB_ANSWER_WALLET: &str = "{\"client\":{\"network\":\"regtest\",\"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]]]}},\"outputs\":{\"wallet_fingerprint\":[203,200,4,248,139,36,241,232],\"birthday\":2146,\"last_scan\":0,\"outputs\":{\"wallet_fingerprint\":[203,200,4,248,139,36,241,232],\"birthday\":2146,\"last_scan\":0,\"outputs\":{\"d5edbf1f60ae4fd7d698626b44ed1e85edddf65b45f62e5f82928a9e12759db9:0\":{\"blockheight\":0,\"tweak\":\"0e3395ff27bde9187ffaeeb2521f6277d3b83911f16ccbaf59a1a68d99a0ab93\",\"amount\":1200,\"script\":\"512010f06f764cbc923ec3198db946307bf0c06a1b4f09206055e47a6fec0a33d52c\",\"label\":null,\"spend_status\":{\"Spent\":\"57ca073676fa6130397ce1e4738278952483cee943daa1664b9a1807d4700066\"}},\"fbd9c63e0dd08c2569b51a0d6095a79ee2acfcac66acdb594328a095f1fadb63:1\":{\"blockheight\":2146,\"tweak\":\"678dbcbdb40cd3733c8dbbd508761a0937009cf75a9f466e3c98877e79037cbc\",\"amount\":99896595,\"script\":\"5120deab0c5a3d23de30477b0b5a95a261c96e29afdd9813c665d2bf025ad2b3f919\",\"label\":null,\"spend_status\":\"Unspent\"}}}}";
const ALICE_SPK: &str = "51205b7b324bb71d411e32f2c61fda5d1db23f5c7d6d416a77fab87c913a1b120be1";
const REVOKATION_OUTPUT: &str =
"3dd51588af6cc2e4ff8e405fd2620f19c8f4e09e05692ad57a9208a061687295:3";
const ALICE_CHALLENGE_CACHE: &str = "{\"id\":1283337801,\"status\":\"SentWaitingConfirmation\",\"ciphertext\":\"d53ec574b09068cc661a76e6934cb9131df1b8d84b0ec791cb2c2789c42aae53bf2e7d514a00b2c27a78a786fee97f55dd022a99551770c4f05edfc4296993b2f82104933f61cd0d7c2e02a21188c58b492ac0a190e711ddc697505e106df087f8f9d3591aa30798608c2a0eb1e7c124d54e88dd9271884a3fe7f24f628a150d0dc90ded4753f9aa4e2730c7a339bfd7ef0faa02ee6d4cd3b1de36f8796485bd6bd8eee0e78c1e6d6c822dd9532df4c963835ea412bb36827cd89d3466\",\"plaintext\":[\"TEST\"],\"commited_in\":\"d5edbf1f60ae4fd7d698626b44ed1e85edddf65b45f62e5f82928a9e12759db9:0\",\"tie_by\":null,\"commitment\":\"d12f3c5b37240bc3abf2976f41fdf9a594f0680aafd2781ac448f80440fbeb99\",\"sender\":\"sprt1qqf50y6deqe7dqg3vj562xf3lmktqe3sct78ha6e9jh54sa3q2m5esq7x2tz0xrpm0ekclyyaszc2sh3ksm5gkumxpwegpa80lv5wgsuefqaufx8q\",\"recipient\":\"sprt1qqf2kqh8n7lkupngfk2fxtmvyaq8sy3v2ramyeryweadqmsz7l6tg2qc9n4dl42ff8klz28nznr7mfzh6263xv555stwzextl2v4lvhg3aqq7ru8u\",\"shared_secret\":\"5a28562161f939bf77983df807dd914e73f02ea67a21ed976d214452887ae43e\",\"key\":null,\"confirmed_by\":null,\"timestamp\":1720966687785,\"error\":null}";
const ALICE_ANSWER_CACHE: &str = "{\"id\":1283337801,\"status\":\"MustSpendConfirmation\",\"ciphertext\":\"d53ec574b09068cc661a76e6934cb9131df1b8d84b0ec791cb2c2789c42aae53bf2e7d514a00b2c27a78a786fee97f55dd022a99551770c4f05edfc4296993b2f82104933f61cd0d7c2e02a21188c58b492ac0a190e711ddc697505e106df087f8f9d3591aa30798608c2a0eb1e7c124d54e88dd9271884a3fe7f24f628a150d0dc90ded4753f9aa4e2730c7a339bfd7ef0faa02ee6d4cd3b1de36f8796485bd6bd8eee0e78c1e6d6c822dd9532df4c963835ea412bb36827cd89d3466\",\"plaintext\":[\"TEST\"],\"commited_in\":\"d5edbf1f60ae4fd7d698626b44ed1e85edddf65b45f62e5f82928a9e12759db9:0\",\"tie_by\":null,\"commitment\":\"d12f3c5b37240bc3abf2976f41fdf9a594f0680aafd2781ac448f80440fbeb99\",\"sender\":\"sprt1qqf50y6deqe7dqg3vj562xf3lmktqe3sct78ha6e9jh54sa3q2m5esq7x2tz0xrpm0ekclyyaszc2sh3ksm5gkumxpwegpa80lv5wgsuefqaufx8q\",\"recipient\":\"sprt1qqf2kqh8n7lkupngfk2fxtmvyaq8sy3v2ramyeryweadqmsz7l6tg2qc9n4dl42ff8klz28nznr7mfzh6263xv555stwzextl2v4lvhg3aqq7ru8u\",\"shared_secret\":\"5a28562161f939bf77983df807dd914e73f02ea67a21ed976d214452887ae43e\",\"key\":null,\"confirmed_by\":\"57ca073676fa6130397ce1e4738278952483cee943daa1664b9a1807d4700066:0\",\"timestamp\":1720966687785,\"error\":null}";
const BOB_CHALLENGE_CACHE: &str = "{\"id\":2639151119,\"status\":\"ReceivedMustConfirm\",\"ciphertext\":null,\"plaintext\":[\"TEST\"],\"commited_in\":\"d5edbf1f60ae4fd7d698626b44ed1e85edddf65b45f62e5f82928a9e12759db9:0\",\"tie_by\":null,\"commitment\":\"d12f3c5b37240bc3abf2976f41fdf9a594f0680aafd2781ac448f80440fbeb99\",\"sender\":\"sprt1qqf50y6deqe7dqg3vj562xf3lmktqe3sct78ha6e9jh54sa3q2m5esq7x2tz0xrpm0ekclyyaszc2sh3ksm5gkumxpwegpa80lv5wgsuefqaufx8q\",\"recipient\":\"sprt1qqf2kqh8n7lkupngfk2fxtmvyaq8sy3v2ramyeryweadqmsz7l6tg2qc9n4dl42ff8klz28nznr7mfzh6263xv555stwzextl2v4lvhg3aqq7ru8u\",\"shared_secret\":\"5a28562161f939bf77983df807dd914e73f02ea67a21ed976d214452887ae43e\",\"key\":null,\"confirmed_by\":null,\"timestamp\":1721315780531,\"error\":null}";
const BOB_ANSWER_CACHE: &str = "{\"id\":2639151119,\"status\":\"ReceivedMustConfirm\",\"ciphertext\":null,\"plaintext\":[\"TEST\"],\"commited_in\":\"d5edbf1f60ae4fd7d698626b44ed1e85edddf65b45f62e5f82928a9e12759db9:0\",\"tie_by\":null,\"commitment\":\"d12f3c5b37240bc3abf2976f41fdf9a594f0680aafd2781ac448f80440fbeb99\",\"sender\":\"sprt1qqf50y6deqe7dqg3vj562xf3lmktqe3sct78ha6e9jh54sa3q2m5esq7x2tz0xrpm0ekclyyaszc2sh3ksm5gkumxpwegpa80lv5wgsuefqaufx8q\",\"recipient\":\"sprt1qqf2kqh8n7lkupngfk2fxtmvyaq8sy3v2ramyeryweadqmsz7l6tg2qc9n4dl42ff8klz28nznr7mfzh6263xv555stwzextl2v4lvhg3aqq7ru8u\",\"shared_secret\":\"5a28562161f939bf77983df807dd914e73f02ea67a21ed976d214452887ae43e\",\"key\":null,\"confirmed_by\":\"57ca073676fa6130397ce1e4738278952483cee943daa1664b9a1807d4700066:0\",\"timestamp\":1721315780531,\"error\":null}";
fn helper_switch_device(wallet: String) {
reset_device().unwrap();
// debug!("{}", wallet);
restore_device_from_sp_wallet(wallet.clone()).unwrap();
// debug!("Restored device with wallet {}", wallet);
}
fn helper_get_alice_address() -> String {
let wallet: SpWallet = serde_json::from_str(ALICE_START_WALLET).unwrap();
wallet.get_client().get_receiving_address()
}
fn helper_get_bob_address() -> String {
let wallet: SpWallet = serde_json::from_str(BOB_START_WALLET).unwrap();
wallet.get_client().get_receiving_address()
}
fn helper_get_tweak_data(transaction: &str, spk: ScriptBuf) -> String {
let tx = deserialize::<Transaction>(&Vec::from_hex(transaction).unwrap()).unwrap();
let prevout = tx.input.get(0).unwrap().to_owned();
let outpoint_data = (
prevout.previous_output.txid.to_string(),
prevout.previous_output.vout,
);
let input_pubkey =
get_pubkey_from_input(&vec![], &prevout.witness.to_vec(), spk.as_bytes()).unwrap();
let tweak_data =
calculate_tweak_data(&vec![&input_pubkey.unwrap()], &vec![outpoint_data]).unwrap();
tweak_data.to_string()
}
fn helper_parse_transaction(transaction: &str, tweak_data: String) -> CachedMessage {
let new_tx_msg =
serde_json::to_string(&NewTxMessage::new(transaction.to_owned(), Some(tweak_data)))
.unwrap();
let network_msg = serde_json::to_string(&AnkNetworkMsg::new(
sdk_common::network::AnkFlag::NewTx,
&new_tx_msg,
))
.unwrap();
let result = parse_network_msg(network_msg, 1);
match result {
Ok(m) => m,
Err(e) => panic!("Unexpected error: {}", e.message),
}
}
fn helper_parse_ank_msg(ank_network_msg: String) -> CachedMessage {
let result = parse_network_msg(ank_network_msg, 1);
match result {
Ok(r) => return r,
Err(e) => panic!("Unexpected error: {}", e.message),
};
}
fn helper_link_alice() {
const ALICE_SPEND_KEY: &str =
"93292e5b21042c6cfc742ba30e9d2a1e01609b12d154a1825184ed12c7b9631b";
let encrypted_alice_key = encrypt_remote_key(
ALICE_SPEND_KEY.to_owned(),
Some("073ec0cd90cd77f5486cbfb586ed639c4d435d39bb038dc905d7a65762d29ff9".to_owned()),
Some("46b4e8e9cfb1b6fd139d480f".to_owned()),
)
.unwrap();
pair_device(
Vec::from_hex(&encrypted_alice_key).unwrap(),
helper_get_alice_address(),
REVOKATION_OUTPUT.to_owned(),
)
.unwrap();
}
fn helper_link_bob() {
const BOB_ENCRYPTED_KEY: &str = "62f10cffb1ed127ef88826c0e65d21dc337c763db53a3d814e1bf21460b53d84d612995ab3b272ac395221bf49cb54df6250502fc8941ba06d85bd58";
pair_device(
Vec::from_hex(BOB_ENCRYPTED_KEY).unwrap(),
helper_get_alice_address(),
REVOKATION_OUTPUT.to_owned(),
)
.unwrap();
}
// #[wasm_bindgen_test]
// fn test_pairing_transaction() {
// setup();
// helper_switch_bob();
// // bob sends pairing transaction to alice
// let result = create_pairing_transaction(helper_get_alice_address(), 1);
// let bob_pairing_transaction = match result {
// Ok(t) => t,
// Err(e) => {
// panic!("create_pairing_transaction failed with {}", e.message);
// }
// };
// // get the tweak data to add to the transaction
// let bob_pairing_tweak_data = helper_get_tweak_data(
// &bob_pairing_transaction.transaction,
// ScriptBuf::from_hex(BOB_SPK).unwrap(),
// );
// // dump bob message cache
// let bob_msg_cache = dump_message_cache().unwrap();
// reset_message_cache().unwrap();
// // switch to alice
// helper_switch_alice();
// // alice receives and parses that transaction
// helper_parse_transaction(&bob_pairing_transaction.transaction, bob_pairing_tweak_data);
// // alice receives and parses the message
// let bob_sent_ank_msg = AnkNetworkMsg::new(
// sdk_common::network::AnkFlag::Cipher,
// &bob_pairing_transaction.new_network_msg.ciphertext.unwrap(),
// );
// let mut pairing_msg = helper_parse_ank_msg(bob_sent_ank_msg.to_string());
// // Alice takes the key out of the plaintext cached message and returns it encrypted to Bob
// let bob_key = pairing_msg.plaintext.pop().unwrap();
// let encrypted_bob_key = encrypt_remote_key(
// bob_key,
// Some("d8ac223089c8f5e575a06bc0e7071a0dc1124688722ecb34ccf9b3c95e5baaa8".to_owned()),
// Some("62f10cffb1ed127ef88826c0".to_owned()),
// )
// .unwrap();
// let cipher_msg = CipherMessage::new(helper_get_alice_address(), encrypted_bob_key.clone());
// let encrypted_payload =
// encrypt_with_key(cipher_msg.to_string(), pairing_msg.shared_secret.unwrap()).unwrap();
// let alice_response_msg =
// AnkNetworkMsg::new(sdk_common::network::AnkFlag::Cipher, &encrypted_payload).to_string();
// // same switch back to Bob
// // let alice_msg_cache = dump_message_cache().unwrap();
// reset_message_cache().unwrap();
// helper_switch_bob();
// set_message_cache(bob_msg_cache).unwrap();
// let bob_received_msg = helper_parse_ank_msg(alice_response_msg);
// assert!(*bob_received_msg.plaintext.get(0).unwrap() == encrypted_bob_key);
// // take the revokation output
// let keypair = Keypair::from_seckey_str(
// &Secp256k1::signing_only(),
// &bob_received_msg.shared_secret.clone().unwrap(),
// )
// .unwrap();
// let spk = ScriptBuf::new_p2tr_tweaked(keypair.x_only_public_key().0.dangerous_assume_tweaked());
// let pairing_transaction =
// deserialize::<Transaction>(&Vec::from_hex(&bob_pairing_transaction.transaction).unwrap())
// .unwrap();
// let vout = pairing_transaction
// .output
// .iter()
// .position(|o| o.script_pubkey == spk)
// .unwrap();
// let revokation_outpoint =
// OutPoint::new(pairing_transaction.txid(), vout.try_into().unwrap()).to_string();
// pair_device(
// Vec::from_hex(bob_received_msg.plaintext.get(0).unwrap()).unwrap(),
// bob_received_msg.recipient.clone().unwrap(),
// revokation_outpoint.clone(),
// )
// .unwrap();
// // Now we can set a trusted channel, which is basically the pairing
// let mut trusted_channel = TrustedChannel::new(bob_received_msg.recipient.unwrap()).unwrap();
// trusted_channel
// .set_revokation(revokation_outpoint, bob_received_msg.shared_secret.unwrap())
// .unwrap();
// }
#[wasm_bindgen_test]
fn test_alice_notifies_bob() {
setup();
debug!("==============================================\nStarting test_alice_notifies_bob\n==============================================");
// Alice notifies Bob
helper_switch_device(ALICE_START_WALLET.to_owned());
helper_link_alice();
let cipher_msg = CipherMessage::new(helper_get_alice_address(), "TEST".to_owned());
debug!("Alice notifies Bob");
let notification_tx =
create_notification_transaction(helper_get_bob_address(), cipher_msg, 1).unwrap();
// debug!("new_network_msg: {:?}", notification_tx.new_network_msg);
// debug!("notification_tx: {}", serde_json::to_string(&notification_tx).unwrap());
let notification_tweak_data = helper_get_tweak_data(
&notification_tx.transaction,
ScriptBuf::from_hex(ALICE_SPK).unwrap(),
);
let ank_msg = AnkNetworkMsg::new(
sdk_common::network::AnkFlag::Cipher,
&notification_tx.new_network_msg.ciphertext.unwrap(),
);
debug!("Alice parse her own notification transaction");
// Alice parse her own transaction to update her utxos
helper_parse_transaction(
&notification_tx.transaction,
notification_tweak_data.clone(),
);
// debug!("Alice parsed transaction {}", notification_tx.txid);
// debug!("alice_wallet_dump: {:?}", dump_watch_only_wallet().unwrap());
reset_device().unwrap();
helper_switch_device(BOB_START_WALLET.to_owned());
helper_link_bob();
debug!(
"Bob parses notification transaction {}",
notification_tx.txid
);
// bob parse the transaction and the message
helper_parse_transaction(&notification_tx.transaction, notification_tweak_data);
helper_parse_ank_msg(ank_msg.to_string());
}
#[wasm_bindgen_test]
fn test_bob_challenges_alice() {
setup();
debug!("==============================================\nStarting test_bob_challenges_alice\n==============================================");
helper_switch_device(BOB_CHALLENGE_WALLET.to_owned());
helper_link_bob();
set_message_cache(vec![BOB_CHALLENGE_CACHE.to_owned()]).unwrap();
let notification_msg: CachedMessage = serde_json::from_str(BOB_CHALLENGE_CACHE).unwrap();
let commited_in = notification_msg.commited_in.unwrap();
let get_outputs_result = get_outputs().unwrap();
let bob_outputs: HashMap<OutPoint, OwnedOutput> = get_outputs_result.into_serde().unwrap();
let bob_spk = &bob_outputs.get(&commited_in).unwrap().script;
debug!("Bob sends a challenge transaction back to Alice");
let confirmation_tx = create_confirmation_transaction(notification_msg.id, 1).unwrap();
let confirmation_tweak_data = helper_get_tweak_data(
&confirmation_tx.transaction,
ScriptBuf::from_hex(&bob_spk).unwrap(),
);
debug!("Bob parsing its own confirmation tx");
helper_parse_transaction(
&confirmation_tx.transaction,
confirmation_tweak_data.clone(),
);
reset_device().unwrap();
helper_switch_device(ALICE_CHALLENGE_WALLET.to_owned());
helper_link_alice();
set_message_cache(vec![ALICE_CHALLENGE_CACHE.to_owned()]).unwrap();
debug!("Alice parsing confirmation tx");
helper_parse_transaction(&confirmation_tx.transaction, confirmation_tweak_data);
}
#[wasm_bindgen_test]
fn test_alice_answers_bob() {
setup();
debug!("==============================================\nStarting test_alice_answers_bob\n==============================================");
helper_switch_device(ALICE_ANSWER_WALLET.to_owned());
helper_link_alice();
set_message_cache(vec![ALICE_ANSWER_CACHE.to_owned()]).unwrap();
let challenge_msg: CachedMessage = serde_json::from_str(ALICE_ANSWER_CACHE).unwrap();
debug!("Alice answers bob's challenge");
let answer_tx = answer_confirmation_transaction(challenge_msg.id, 1).unwrap();
// debug!("answer_tx: {:?}", answer_tx.new_network_msg);
let confirmed_by = challenge_msg.confirmed_by.unwrap();
debug!("confirmed_by: {}", confirmed_by);
let get_outputs_result = get_outputs().unwrap();
let alice_outputs: HashMap<OutPoint, OwnedOutput> = get_outputs_result.into_serde().unwrap();
let alice_spk = &alice_outputs.get(&confirmed_by).unwrap().script;
let answer_tweak_data = helper_get_tweak_data(
&answer_tx.transaction,
ScriptBuf::from_hex(&alice_spk).unwrap(),
);
debug!("Alice parsing its own answer tx");
debug!(
"message: {}",
serde_json::to_string(&answer_tx.new_network_msg).unwrap()
);
helper_parse_transaction(&answer_tx.transaction, answer_tweak_data.clone());
reset_device().unwrap();
helper_switch_device(BOB_ANSWER_WALLET.to_owned());
helper_link_bob();
set_message_cache(vec![BOB_ANSWER_CACHE.to_owned()]).unwrap();
debug!("Bob parses answer transaction {}", answer_tx.txid);
let answer_msg = helper_parse_transaction(&answer_tx.transaction, answer_tweak_data);
debug!(
"bob's message: {}",
serde_json::to_string(&answer_msg).unwrap()
);
}
// #[wasm_bindgen_test]
// fn test_linking_devices() {
// setup();
// helper_switch_bob();
// // bob sends pairing transaction to alice
// let result = create_pairing_transaction(helper_get_alice_address(), 1);
// let bob_pairing_transaction = match result {
// Ok(t) => t,
// Err(e) => {
// panic!("create_pairing_transaction failed with {}", e.message);
// }
// };
// // get the tweak data to add to the transaction
// let bob_pairing_tweak_data = helper_get_tweak_data(
// &bob_pairing_transaction.transaction,
// ScriptBuf::from_hex(BOB_SPK).unwrap(),
// );
// // alice receives and parses that transaction
// helper_switch_alice();
// helper_parse_transaction(&bob_pairing_transaction.transaction, bob_pairing_tweak_data);
// // alice receives and parses the message
// let pairing_msg =
// helper_parse_cipher_msg(bob_pairing_transaction.new_network_msg.ciphertext.unwrap());
// // and sends the encrypted key back in a confirm transaction
// let result = create_confirmation_transaction(pairing_msg.id, 1);
// let alice_confirm_transaction = match result {
// Ok(r) => r,
// Err(e) => {
// panic!("create_pairing_transaction failed with {}", e.message);
// }
// };
// // get the tweak data to add to the transaction
// let alice_confirm_tweak_data = helper_get_tweak_data(
// &alice_confirm_transaction.transaction,
// helper_get_spk(
// bob_pairing_transaction.transaction,
// alice_confirm_transaction
// .new_network_msg
// .commited_in
// .unwrap(),
// ),
// );
// // alice parses her own transaction to update her utxoset
// helper_parse_transaction(
// &alice_confirm_transaction.transaction,
// alice_confirm_tweak_data.clone(),
// );
// // alice sends her own pairing transaction
// let result = create_pairing_transaction(helper_get_bob_address(), 1);
// let alice_pairing_transaction = match result {
// Ok(t) => t,
// Err(e) => {
// panic!("create_pairing_transaction failed with {}", e.message);
// }
// };
// // get the tweak data to add to the transaction
// let alice_pairing_tweak_data = helper_get_tweak_data(
// &alice_pairing_transaction.transaction,
// ScriptBuf::from_hex(ALICE_SPK).unwrap(),
// );
// helper_switch_bob();
// // bob receives the spend_key from alice and does the same thing
// helper_parse_transaction(
// &alice_pairing_transaction.transaction,
// alice_pairing_tweak_data,
// );
// let pairing_msg = helper_parse_cipher_msg(
// alice_pairing_transaction
// .new_network_msg
// .ciphertext
// .unwrap(),
// );
// // let result = create_confirmation_transaction(pairing_msg.id, 1);
// // let bob_confirm_transaction = match result {
// // Ok(r) => r,
// // Err(e) => {
// // panic!("create_pairing_transaction failed with {}", e.message);
// // }
// // };
// // bob also received the confirmation transaction from alice, containing his own encrypted key
// log::debug!("Parsing alice confirmation transaction");
// helper_parse_transaction(
// &alice_confirm_transaction.transaction,
// alice_confirm_tweak_data,
// );
// log::debug!("Parsing the message that goes with it");
// let confirmation_msg = helper_parse_cipher_msg(
// alice_confirm_transaction
// .new_network_msg
// .ciphertext
// .unwrap(),
// );
// log::debug!("{:?}", confirmation_msg);
// // bob can now update its device with the linking information
// pair_device(
// confirmation_msg.plaintext.get(0).unwrap().to_owned().into(),
// confirmation_msg.recipient.unwrap(),
// OutPoint::null().to_string(),
// )
// .unwrap();
// }