diff --git a/clients/apps/rust/src/main.rs b/clients/apps/rust/src/main.rs index 7b15138e..05e72264 100644 --- a/clients/apps/rust/src/main.rs +++ b/clients/apps/rust/src/main.rs @@ -163,7 +163,7 @@ async fn main() -> Result<()> { let force_send = force_send.unwrap_or(false); - mercuryrustlib::transfer_sender::execute(&client_config, &to_address, &wallet_name, &statechain_id, force_send, batch_id).await?; + mercuryrustlib::transfer_sender::execute(&client_config, &to_address, &wallet_name, &statechain_id, None, force_send, batch_id).await?; let obj = json!({"Transfer": "sent"}); diff --git a/clients/libs/rust/src/coin_status.rs b/clients/libs/rust/src/coin_status.rs index 2f811434..ef926592 100644 --- a/clients/libs/rust/src/coin_status.rs +++ b/clients/libs/rust/src/coin_status.rs @@ -59,7 +59,16 @@ async fn check_deposit(client_config: &ClientConfig, coin: &mut Coin, wallet_net let utxo_txid = utxo.tx_hash.to_string(); let utxo_vout = utxo.tx_pos as u32; - let backup_tx = create_tx1(client_config, coin, wallet_netwotk, &utxo_txid, utxo_vout).await?; + if coin.status != CoinStatus::INITIALISED { + return Err(anyhow!("The coin with the public key {} is not in the INITIALISED state", coin.user_pubkey.to_string())); + } + + coin.utxo_txid = Some(utxo_txid.to_string()); + coin.utxo_vout = Some(utxo_vout); + + coin.status = CoinStatus::IN_MEMPOOL; + + let backup_tx = create_tx1(client_config, coin, wallet_netwotk, 1u32).await?; let activity_utxo = format!("{}:{}", utxo.tx_hash.to_string(), utxo.tx_pos); @@ -257,6 +266,20 @@ pub async fn update_coins(client_config: &ClientConfig, wallet_name: &str) -> Re wallet.coins.extend(duplicated_coins); + // invalidate duplicated coins that were not transferred + for i in 0..wallet.coins.len() { + if wallet.coins[i].status == CoinStatus::DUPLICATED { + let is_transferred = (0..wallet.coins.len()).any(|j| + i != j && // Skip comparing with self + wallet.coins[j].statechain_id == wallet.coins[i].statechain_id && + wallet.coins[j].status == CoinStatus::TRANSFERRED + ); + if is_transferred { + wallet.coins[i].status = CoinStatus::INVALIDATED; + } + } + } + update_wallet(&client_config.pool, &wallet).await?; Ok(()) diff --git a/clients/libs/rust/src/deposit.rs b/clients/libs/rust/src/deposit.rs index 3ed62f31..71be26c4 100644 --- a/clients/libs/rust/src/deposit.rs +++ b/clients/libs/rust/src/deposit.rs @@ -1,5 +1,5 @@ use anyhow::{anyhow, Result, Ok}; -use mercurylib::{deposit::{create_deposit_msg1, create_aggregated_address}, wallet::{Wallet, BackupTx, CoinStatus, Coin}, transaction:: get_user_backup_address, utils::get_blockheight}; +use mercurylib::{deposit::{create_deposit_msg1, create_aggregated_address}, wallet::{Wallet, BackupTx, Coin}, transaction:: get_user_backup_address, utils::get_blockheight}; use crate::{client_config::ClientConfig, sqlite_manager::{get_wallet, update_wallet}, transaction::new_transaction, utils::info_config}; @@ -23,19 +23,8 @@ pub async fn get_deposit_bitcoin_address(client_config: &ClientConfig, wallet_na Ok(aggregated_public_key.aggregate_address) } -pub async fn create_tx1(client_config: &ClientConfig, coin: &mut Coin, wallet_netwotk: &str, tx0_hash: &str, tx0_vout: u32) -> Result { - - if coin.status != CoinStatus::INITIALISED { - return Err(anyhow!("The coin with the public key {} is not in the INITIALISED state", coin.user_pubkey.to_string())); - } - - if coin.utxo_txid.is_some() && coin.utxo_vout.is_some() { - return Err(anyhow!("The coin with the public key {} has already been deposited", coin.user_pubkey.to_string())); - } - coin.utxo_txid = Some(tx0_hash.to_string()); - coin.utxo_vout = Some(tx0_vout); - - coin.status = CoinStatus::IN_MEMPOOL; +// When sending duplicated coins, the tx_n of the backup_tx must be different +pub async fn create_tx1(client_config: &ClientConfig, coin: &mut Coin, wallet_netwotk: &str, tx_n: u32) -> Result { let to_address = get_user_backup_address(&coin, wallet_netwotk.to_string())?; @@ -73,7 +62,7 @@ pub async fn create_tx1(client_config: &ClientConfig, coin: &mut Coin, wallet_ne } let backup_tx = BackupTx { - tx_n: 1, + tx_n, tx: signed_tx, client_public_nonce: coin.public_nonce.as_ref().unwrap().to_string(), server_public_nonce: coin.server_public_nonce.as_ref().unwrap().to_string(), diff --git a/clients/libs/rust/src/lib.rs b/clients/libs/rust/src/lib.rs index 17004821..20a409ec 100644 --- a/clients/libs/rust/src/lib.rs +++ b/clients/libs/rust/src/lib.rs @@ -16,6 +16,7 @@ pub use mercurylib::wallet::CoinStatus; pub use mercurylib::wallet::Coin; pub use mercurylib::wallet::BackupTx; pub use mercurylib::wallet::Activity; +pub use mercurylib::wallet::get_previous_outpoint; pub use mercurylib::transfer::sender::{TransferSenderRequestPayload, TransferSenderResponsePayload, create_transfer_signature, create_transfer_update_msg}; pub use mercurylib::transaction::{SignFirstRequestPayload, SignFirstResponsePayload, create_and_commit_nonces}; diff --git a/clients/libs/rust/src/transfer_receiver.rs b/clients/libs/rust/src/transfer_receiver.rs index 3af84631..8a5ac4f0 100644 --- a/clients/libs/rust/src/transfer_receiver.rs +++ b/clients/libs/rust/src/transfer_receiver.rs @@ -5,7 +5,7 @@ use anyhow::{anyhow, Ok, Result}; use bitcoin::{Txid, Address}; use chrono::Utc; use electrum_client::ElectrumApi; -use mercurylib::{utils::{get_network, InfoConfig}, wallet::{Activity, Coin, CoinStatus}}; +use mercurylib::{utils::{get_network, InfoConfig}, wallet::{get_previous_outpoint, Activity, BackupTx, Coin, CoinStatus}}; use reqwest::StatusCode; pub async fn new_transfer_address(client_config: &ClientConfig, wallet_name: &str) -> Result{ @@ -28,6 +28,49 @@ pub struct TransferReceiveResult { pub received_statechain_ids: Vec, } +pub struct DuplicatedCoinData { + pub txid: String, + pub vout: u32, + pub amount: u64, + pub index: u32, +} + +pub struct MessageResult { + pub is_batch_locked: bool, + pub statechain_id: Option, + pub duplicated_coins: Vec, +} + +pub fn sort_coins_by_statechain(coins: &mut Vec) { + // Create a map to store the position of first occurrence of each statechain_id + let mut first_positions: HashMap = HashMap::new(); + + // Record the position of the first occurrence of each statechain_id + for (idx, coin) in coins.iter().enumerate() { + if let Some(id) = &coin.statechain_id { + first_positions.entry(id.clone()).or_insert(idx); + } + } + + // Sort the vector maintaining original order of different statechain_ids + coins.sort_by(|a, b| { + match (&a.statechain_id, &b.statechain_id) { + (None, None) => a.duplicate_index.cmp(&b.duplicate_index), + (None, Some(_)) => std::cmp::Ordering::Greater, + (Some(_), None) => std::cmp::Ordering::Less, + (Some(id_a), Some(id_b)) => { + if id_a == id_b { + // Same statechain_id: sort by duplicate_index + a.duplicate_index.cmp(&b.duplicate_index) + } else { + // Different statechain_ids: compare their first positions + first_positions[id_a].cmp(&first_positions[id_b]) + } + } + } + }); +} + pub async fn execute(client_config: &ClientConfig, wallet_name: &str) -> Result{ let mut wallet = get_wallet(&client_config.pool, &wallet_name).await?; @@ -59,6 +102,8 @@ pub async fn execute(client_config: &ClientConfig, wallet_name: &str) -> Result< let mut temp_coins = wallet.coins.clone(); let mut temp_activities = wallet.activities.clone(); + let mut duplicated_coins: Vec = Vec::new(); + let block_header = client_config.electrum_client.block_headers_subscribe_raw()?; let blockheight = block_header.height as u32; @@ -74,10 +119,17 @@ pub async fn execute(client_config: &ClientConfig, wallet_name: &str) -> Result< let mut coin = coin.unwrap(); - let message_result = process_encrypted_message(client_config, &mut coin, enc_message, &wallet.network, &wallet.name, &info_config, blockheight, &mut temp_activities).await; + let is_msg_valid = validate_encrypted_message(client_config, &coin, enc_message, &wallet.network, &info_config, blockheight).await; + + if is_msg_valid.is_err() { + println!("Validation error: {}", is_msg_valid.err().unwrap().to_string()); + continue; + } + + let message_result = process_encrypted_message(client_config, &mut coin, enc_message, &wallet.network, &wallet.name, &mut temp_activities).await; if message_result.is_err() { - println!("Error: {}", message_result.err().unwrap().to_string()); + println!("Processing error: {}", message_result.err().unwrap().to_string()); continue; } @@ -91,6 +143,21 @@ pub async fn execute(client_config: &ClientConfig, wallet_name: &str) -> Result< received_statechain_ids.push(message_result.statechain_id.unwrap()); } + if message_result.duplicated_coins.len() > 0 { + + assert!(!message_result.is_batch_locked); + + for duplicated_coin_data in message_result.duplicated_coins { + let mut duplicated_coin = coin.clone(); + duplicated_coin.status = CoinStatus::DUPLICATED; + duplicated_coin.utxo_txid = Some(duplicated_coin_data.txid); + duplicated_coin.utxo_vout = Some(duplicated_coin_data.vout); + duplicated_coin.amount = Some(duplicated_coin_data.amount as u32); + duplicated_coin.duplicate_index = duplicated_coin_data.index; + duplicated_coins.push(duplicated_coin); + } + } + } else { let new_coin = mercurylib::transfer::receiver::duplicate_coin_to_initialized_state(&wallet, &auth_pubkey); @@ -102,14 +169,21 @@ pub async fn execute(client_config: &ClientConfig, wallet_name: &str) -> Result< let mut new_coin = new_coin.unwrap(); - let message_result = process_encrypted_message(client_config, &mut new_coin, enc_message, &wallet.network, &wallet.name, &info_config, blockheight, &mut temp_activities).await; + let is_msg_valid = validate_encrypted_message(client_config, &new_coin, enc_message, &wallet.network, &info_config, blockheight).await; + + if is_msg_valid.is_err() { + println!("Validation error: {}", is_msg_valid.err().unwrap().to_string()); + continue; + } + + let message_result = process_encrypted_message(client_config, &mut new_coin, enc_message, &wallet.network, &wallet.name, &mut temp_activities).await; if message_result.is_err() { - println!("Error: {}", message_result.err().unwrap().to_string()); + println!("Processing error: {}", message_result.err().unwrap().to_string()); continue; } - temp_coins.push(new_coin); + temp_coins.push(new_coin.clone()); let message_result = message_result.unwrap(); @@ -120,10 +194,28 @@ pub async fn execute(client_config: &ClientConfig, wallet_name: &str) -> Result< if message_result.statechain_id.is_some() { received_statechain_ids.push(message_result.statechain_id.unwrap()); } + + if message_result.duplicated_coins.len() > 0 { + + assert!(!message_result.is_batch_locked); + + for duplicated_coin_data in message_result.duplicated_coins { + let mut duplicated_coin = new_coin.clone(); + duplicated_coin.status = CoinStatus::DUPLICATED; + duplicated_coin.utxo_txid = Some(duplicated_coin_data.txid); + duplicated_coin.utxo_vout = Some(duplicated_coin_data.vout); + duplicated_coin.amount = Some(duplicated_coin_data.amount as u32); + duplicated_coin.duplicate_index = duplicated_coin_data.index; + // temp_coins.push(duplicated_coin); + duplicated_coins.push(duplicated_coin); + } + } } } } + temp_coins.extend(duplicated_coins); + sort_coins_by_statechain(&mut temp_coins); wallet.coins = temp_coins.clone(); wallet.activities = temp_activities.clone(); @@ -149,143 +241,235 @@ async fn get_msg_addr(auth_pubkey: &str, client_config: &ClientConfig) -> Result Ok(response.list_enc_transfer_msg) } -pub struct MessageResult { - pub is_batch_locked: bool, - pub statechain_id: Option, +pub fn split_backup_transactions(backup_transactions: &Vec) -> Vec> { + // HashMap to store grouped transactions + let mut grouped_txs: HashMap<(String, u32), Vec> = HashMap::new(); + + // Vector to keep track of order of appearance of outpoints + let mut order_of_appearance: Vec<(String, u32)> = Vec::new(); + // HashSet to track which outpoints we've seen + let mut seen_outpoints: HashSet<(String, u32)> = HashSet::new(); + + // Process each transaction + for tx in backup_transactions { + // Get the outpoint for this transaction + let outpoint = get_previous_outpoint(&tx).expect("Valid outpoint"); + + // Create a key tuple from txid and vout + let key = (outpoint.txid, outpoint.vout); + + // If we haven't seen this outpoint before, record its order + if seen_outpoints.insert(key.clone()) { + order_of_appearance.push(key.clone()); + } + + // Add the transaction to its group + grouped_txs.entry(key).or_insert_with(Vec::new).push(tx.clone()); + } + + // Create result vector maintaining order of first appearance + let mut result: Vec> = Vec::with_capacity(order_of_appearance.len()); + + // Add vectors to result in order of first appearance + for key in order_of_appearance { + if let Some(mut transactions) = grouped_txs.remove(&key) { + // Sort each group by tx_n + transactions.sort_by_key(|tx| tx.tx_n); + result.push(transactions); + } + } + + result } -async fn process_encrypted_message(client_config: &ClientConfig, coin: &mut Coin, enc_message: &str, network: &str, wallet_name: &str, info_config: &InfoConfig, blockheight: u32, activities: &mut Vec) -> Result { +async fn validate_encrypted_message(client_config: &ClientConfig, coin: &Coin, enc_message: &str, network: &str, info_config: &InfoConfig, blockheight: u32) -> Result<()> { let client_auth_key = coin.auth_privkey.clone(); let new_user_pubkey = coin.user_pubkey.clone(); let transfer_msg = mercurylib::transfer::receiver::decrypt_transfer_msg(enc_message, &client_auth_key)?; - let tx0_outpoint = mercurylib::transfer::receiver::get_tx0_outpoint(&transfer_msg.backup_transactions)?; - - let tx0_hex = get_tx0(&client_config.electrum_client, &tx0_outpoint.txid).await?; + let grouped_backup_transactions = split_backup_transactions(&transfer_msg.backup_transactions); - let is_transfer_signature_valid = mercurylib::transfer::receiver::verify_transfer_signature(&new_user_pubkey, &tx0_outpoint, &transfer_msg)?; + for (index, backup_transactions) in grouped_backup_transactions.iter().enumerate() { + + let tx0_outpoint = mercurylib::transfer::receiver::get_tx0_outpoint(backup_transactions)?; - if !is_transfer_signature_valid { - return Err(anyhow::anyhow!("Invalid transfer signature".to_string())); - } + let tx0_hex = get_tx0(&client_config.electrum_client, &tx0_outpoint.txid).await?; - let statechain_info = utils::get_statechain_info(&transfer_msg.statechain_id, &client_config).await?; + if index == 0 { + let is_transfer_signature_valid = mercurylib::transfer::receiver::verify_transfer_signature(&new_user_pubkey, &tx0_outpoint, &transfer_msg)?; - if statechain_info.is_none() { - return Err(anyhow::anyhow!("Statechain info not found".to_string())); - } + if !is_transfer_signature_valid { + return Err(anyhow::anyhow!("Invalid transfer signature".to_string())); + } + } - let statechain_info = statechain_info.unwrap(); + let statechain_info = utils::get_statechain_info(&transfer_msg.statechain_id, &client_config).await?; - let is_tx0_output_pubkey_valid = mercurylib::transfer::receiver::validate_tx0_output_pubkey(&statechain_info.enclave_public_key, &transfer_msg, &tx0_outpoint, &tx0_hex, network)?; + if statechain_info.is_none() { + return Err(anyhow::anyhow!("Statechain info not found".to_string())); + } - if !is_tx0_output_pubkey_valid { - return Err(anyhow::anyhow!("Invalid tx0 output pubkey".to_string())); - } + let statechain_info = statechain_info.unwrap(); - let latest_backup_tx_pays_to_user_pubkey = mercurylib::transfer::receiver::verify_latest_backup_tx_pays_to_user_pubkey(&transfer_msg, &new_user_pubkey, network)?; + let is_tx0_output_pubkey_valid = mercurylib::transfer::receiver::validate_tx0_output_pubkey(&statechain_info.enclave_public_key, &transfer_msg, &tx0_outpoint, &tx0_hex, network)?; - if !latest_backup_tx_pays_to_user_pubkey { - return Err(anyhow::anyhow!("Latest Backup Tx does not pay to the expected public key".to_string())); - } + if !is_tx0_output_pubkey_valid { + return Err(anyhow::anyhow!("Invalid tx0 output pubkey".to_string())); + } - if statechain_info.num_sigs != transfer_msg.backup_transactions.len() as u32 { - return Err(anyhow::anyhow!("num_sigs is not correct".to_string())); - } + let latest_backup_tx_pays_to_user_pubkey = mercurylib::transfer::receiver::verify_latest_backup_tx_pays_to_user_pubkey(&transfer_msg, &new_user_pubkey, network)?; - let (is_tx0_output_unspent, tx0_status) = verify_tx0_output_is_unspent_and_confirmed(&client_config.electrum_client, &tx0_outpoint, &tx0_hex, &network, client_config.confirmation_target).await?; + if !latest_backup_tx_pays_to_user_pubkey { + return Err(anyhow::anyhow!("Latest Backup Tx does not pay to the expected public key".to_string())); + } - if !is_tx0_output_unspent { - return Err(anyhow::anyhow!("tx0 output is spent or not confirmed".to_string())); + if statechain_info.num_sigs != transfer_msg.backup_transactions.len() as u32 { + return Err(anyhow::anyhow!("num_sigs is not correct".to_string())); + } + + let (is_tx0_output_unspent, _) = verify_tx0_output_is_unspent_and_confirmed(&client_config.electrum_client, &tx0_outpoint, &tx0_hex, &network, client_config.confirmation_target).await?; + + if !is_tx0_output_unspent { + return Err(anyhow::anyhow!("tx0 output is spent or not confirmed".to_string())); + } + + let current_fee_rate_sats_per_byte = if info_config.fee_rate_sats_per_byte > client_config.max_fee_rate { + client_config.max_fee_rate + } else { + info_config.fee_rate_sats_per_byte + }; + + let previous_lock_time = mercurylib::transfer::receiver::validate_signature_scheme( + backup_transactions, + &statechain_info, + &tx0_hex, + blockheight, + client_config.fee_rate_tolerance, + current_fee_rate_sats_per_byte, + info_config.initlock, + info_config.interval); + + if previous_lock_time.is_err() { + let error = previous_lock_time.err().unwrap(); + return Err(anyhow!("Signature scheme validation failed. Error {}", error.to_string())); + } } - let current_fee_rate_sats_per_byte = if info_config.fee_rate_sats_per_byte > client_config.max_fee_rate { - client_config.max_fee_rate - } else { - info_config.fee_rate_sats_per_byte + Ok(()) +} + +async fn process_encrypted_message(client_config: &ClientConfig, coin: &mut Coin, enc_message: &str, network: &str, wallet_name: &str, activities: &mut Vec) -> Result { + + let mut transfer_receive_result = MessageResult { + is_batch_locked: false, + statechain_id: None, + duplicated_coins: Vec::new(), }; - let previous_lock_time = mercurylib::transfer::receiver::validate_signature_scheme( - &transfer_msg, - &statechain_info, - &tx0_hex, - blockheight, - client_config.fee_rate_tolerance, - current_fee_rate_sats_per_byte, - info_config.initlock, - info_config.interval); - - if previous_lock_time.is_err() { - let error = previous_lock_time.err().unwrap(); - return Err(anyhow!("Signature scheme validation failed. Error {}", error.to_string())); - } + let client_auth_key = coin.auth_privkey.clone(); - let previous_lock_time = previous_lock_time.unwrap(); + let transfer_msg = mercurylib::transfer::receiver::decrypt_transfer_msg(enc_message, &client_auth_key)?; - let transfer_receiver_request_payload = mercurylib::transfer::receiver::create_transfer_receiver_request_payload(&statechain_info, &transfer_msg, &coin)?; + let grouped_backup_transactions = split_backup_transactions(&transfer_msg.backup_transactions); - // unlock the statecoin - it might be part of a batch + for (index, backup_transactions) in grouped_backup_transactions.iter().enumerate() { - // the pub_auth_key has not been updated yet in the server (it will be updated after the transfer/receive call) - // So we need to manually sign the statechain_id with the client_auth_key - let signed_statechain_id_for_unlock = mercurylib::transfer::receiver::sign_message(&transfer_msg.statechain_id, &coin)?; + if index == 0 { - unlock_statecoin(&client_config, &transfer_msg.statechain_id, &signed_statechain_id_for_unlock, &coin.auth_pubkey).await?; + let tx0_outpoint = mercurylib::transfer::receiver::get_tx0_outpoint(backup_transactions)?; + let tx0_hex = get_tx0(&client_config.electrum_client, &tx0_outpoint.txid).await?; - let transfer_receiver_result = send_transfer_receiver_request_payload(&client_config, &transfer_receiver_request_payload).await; + let statechain_info = utils::get_statechain_info(&transfer_msg.statechain_id, &client_config).await?; + let statechain_info = statechain_info.unwrap(); - let server_public_key_hex = match transfer_receiver_result { - std::result::Result::Ok(server_public_key_hex) => { + let (_, tx0_status) = verify_tx0_output_is_unspent_and_confirmed(&client_config.electrum_client, &tx0_outpoint, &tx0_hex, &network, client_config.confirmation_target).await?; - if server_public_key_hex.is_batch_locked { - return Ok(MessageResult { - is_batch_locked: true, - statechain_id: None, - }); - } + let backup_tx = backup_transactions.last().unwrap(); - server_public_key_hex.server_pubkey.unwrap() - }, - Err(err) => { - return Err(anyhow::anyhow!("Error: {}", err.to_string())); - } - }; + let last_tx_lock_time = mercurylib::utils::get_blockheight(&backup_tx)?; - let new_key_info = mercurylib::transfer::receiver::get_new_key_info(&server_public_key_hex, &coin, &transfer_msg.statechain_id, &tx0_outpoint, &tx0_hex, network)?; - - coin.server_pubkey = Some(server_public_key_hex); - coin.aggregated_pubkey = Some(new_key_info.aggregate_pubkey); - coin.aggregated_address = Some(new_key_info.aggregate_address); - coin.statechain_id = Some(transfer_msg.statechain_id.clone()); - coin.signed_statechain_id = Some(new_key_info.signed_statechain_id.clone()); - coin.amount = Some(new_key_info.amount); - coin.utxo_txid = Some(tx0_outpoint.txid.clone()); - coin.utxo_vout = Some(tx0_outpoint.vout); - coin.locktime = Some(previous_lock_time); - coin.status = tx0_status; - - let date = Utc::now(); // This will get the current date and time in UTC - let iso_string = date.to_rfc3339(); // Converts the date to an ISO 8601 string - - let activity = Activity { - utxo: tx0_outpoint.txid.clone(), - amount: new_key_info.amount, - action: "Receive".to_string(), - date: iso_string - }; + let transfer_receiver_request_payload = mercurylib::transfer::receiver::create_transfer_receiver_request_payload(&statechain_info, &transfer_msg, &coin)?; - activities.push(activity); + // unlock the statecoin - it might be part of a batch - insert_or_update_backup_txs(&client_config.pool, wallet_name, &transfer_msg.statechain_id, &transfer_msg.backup_transactions).await?; + // the pub_auth_key has not been updated yet in the server (it will be updated after the transfer/receive call) + // So we need to manually sign the statechain_id with the client_auth_key + let signed_statechain_id_for_unlock = mercurylib::transfer::receiver::sign_message(&transfer_msg.statechain_id, &coin)?; - Ok(MessageResult { - is_batch_locked: false, - statechain_id: Some(transfer_msg.statechain_id.clone()), - }) + unlock_statecoin(&client_config, &transfer_msg.statechain_id, &signed_statechain_id_for_unlock, &coin.auth_pubkey).await?; + + let transfer_receiver_result = send_transfer_receiver_request_payload(&client_config, &transfer_receiver_request_payload).await; + + let server_public_key_hex = match transfer_receiver_result { + std::result::Result::Ok(server_public_key_hex) => { + + if server_public_key_hex.is_batch_locked { + return Ok(MessageResult { + is_batch_locked: true, + statechain_id: None, + duplicated_coins: Vec::new(), + }); + } + + server_public_key_hex.server_pubkey.unwrap() + }, + Err(err) => { + return Err(anyhow::anyhow!("Error: {}", err.to_string())); + } + }; + + let new_key_info = mercurylib::transfer::receiver::get_new_key_info(&server_public_key_hex, &coin, &transfer_msg.statechain_id, &tx0_outpoint, &tx0_hex, network)?; + + coin.server_pubkey = Some(server_public_key_hex); + coin.aggregated_pubkey = Some(new_key_info.aggregate_pubkey); + coin.aggregated_address = Some(new_key_info.aggregate_address); + coin.statechain_id = Some(transfer_msg.statechain_id.clone()); + coin.signed_statechain_id = Some(new_key_info.signed_statechain_id.clone()); + coin.amount = Some(new_key_info.amount); + coin.utxo_txid = Some(tx0_outpoint.txid.clone()); + coin.utxo_vout = Some(tx0_outpoint.vout); + coin.locktime = Some(last_tx_lock_time); + coin.status = tx0_status; + + let date = Utc::now(); // This will get the current date and time in UTC + let iso_string = date.to_rfc3339(); // Converts the date to an ISO 8601 string + + let activity = Activity { + utxo: tx0_outpoint.txid.clone(), + amount: new_key_info.amount, + action: "Receive".to_string(), + date: iso_string + }; + + activities.push(activity); - // Ok(transfer_msg.statechain_id.clone()) + insert_or_update_backup_txs(&client_config.pool, wallet_name, &transfer_msg.statechain_id, &transfer_msg.backup_transactions).await?; + + transfer_receive_result.is_batch_locked = false; + transfer_receive_result.statechain_id = Some(transfer_msg.statechain_id.clone()); + } else { + + let tx0_outpoint = mercurylib::transfer::receiver::get_tx0_outpoint(backup_transactions)?; + let tx0_hex = get_tx0(&client_config.electrum_client, &tx0_outpoint.txid).await?; + + let first_backup_tx = backup_transactions.first().unwrap(); + + let tx_outpoint = get_previous_outpoint(&first_backup_tx)?; + + let amount = mercurylib::transfer::receiver::get_amount_from_tx0(&tx0_hex, &tx_outpoint)?; + + transfer_receive_result.duplicated_coins.push(DuplicatedCoinData { + txid: tx_outpoint.txid, + vout: tx_outpoint.vout, + amount, + index: index as u32, + }); + } + } + + Ok(transfer_receive_result) } async fn get_tx0(electrum_client: &electrum_client::Client, tx0_txid: &str) -> Result { @@ -297,14 +481,12 @@ async fn get_tx0(electrum_client: &electrum_client::Client, tx0_txid: &str) -> R return Err(anyhow!("tx0 not found")); } - // let tx0 = bitcoin::consensus::encode::deserialize(&tx_bytes[0])?; - let tx0_hex = hex::encode(&tx_bytes[0]); Ok(tx0_hex) } -async fn verify_tx0_output_is_unspent_and_confirmed(electrum_client: &electrum_client::Client, tx0_outpoint: &mercurylib::transfer::receiver::TxOutpoint, tx0_hex: &str, network: &str, confirmation_target: u32) -> Result<(bool, CoinStatus)> { +async fn verify_tx0_output_is_unspent_and_confirmed(electrum_client: &electrum_client::Client, tx0_outpoint: &mercurylib::transfer::TxOutpoint, tx0_hex: &str, network: &str, confirmation_target: u32) -> Result<(bool, CoinStatus)> { let output_address = mercurylib::transfer::receiver::get_output_address_from_tx0(&tx0_outpoint, &tx0_hex, &network)?; let network = get_network(&network)?; diff --git a/clients/libs/rust/src/transfer_sender.rs b/clients/libs/rust/src/transfer_sender.rs index 1dcfd1da..bff9502a 100644 --- a/clients/libs/rust/src/transfer_sender.rs +++ b/clients/libs/rust/src/transfer_sender.rs @@ -1,18 +1,179 @@ -use crate::{client_config::ClientConfig, sqlite_manager::{get_backup_txs, get_wallet, update_backup_txs, update_wallet}, transaction::new_transaction, utils::info_config}; +use std::cmp::Ordering; + +use crate::{client_config::ClientConfig, deposit::create_tx1, sqlite_manager::{get_backup_txs, get_wallet, update_backup_txs, update_wallet}, transaction::new_transaction, utils::info_config}; use anyhow::{anyhow, Result}; use chrono::Utc; -use mercurylib::{wallet::{Coin, BackupTx, Activity, CoinStatus}, utils::get_blockheight, decode_transfer_address, transfer::sender::{TransferSenderRequestPayload, TransferSenderResponsePayload, create_transfer_signature, create_transfer_update_msg}}; +use mercurylib::{decode_transfer_address, transfer::sender::{create_transfer_signature, create_transfer_update_msg, TransferSenderRequestPayload, TransferSenderResponsePayload}, utils::get_blockheight, wallet::{get_previous_outpoint, Activity, BackupTx, Coin, CoinStatus, Wallet}}; use electrum_client::ElectrumApi; +pub async fn create_backup_transactions( + client_config: &ClientConfig, + recipient_address: &str, + wallet: &mut Wallet, + statechain_id: &str, + duplicated_indexes: Option>, +) -> Result> { + + // throw error if duplicated_indexes contains an index that does not exist in wallet.coins + // this can be moved to the caller function + if duplicated_indexes.is_some() { + for index in duplicated_indexes.as_ref().unwrap() { + if *index as usize >= wallet.coins.len() { + return Err(anyhow!("Index {} does not exist in wallet.coins", index)); + } + } + } + + let mut coin_list: Vec<&mut Coin> = Vec::new(); + + let backup_transactions = get_backup_txs(&client_config.pool, &wallet.name, &statechain_id).await?; + + // Get coins that already have a backup transaction + for coin in wallet.coins.iter_mut() { + // Check if coin matches any backup transaction + let has_matching_tx = backup_transactions.iter().any(|backup_tx| { + if let Ok(tx_outpoint) = get_previous_outpoint(backup_tx) { + if let (Some(utxo_txid), Some(utxo_vout)) = (coin.utxo_txid.clone(), coin.utxo_vout) { + tx_outpoint.txid == utxo_txid && tx_outpoint.vout == utxo_vout + } else { + false + } + } else { + false + } + }); + + let mut coin_to_add = false; + + if duplicated_indexes.is_some() { + if coin.statechain_id == Some(statechain_id.to_string()) && + (coin.status == CoinStatus::CONFIRMED || coin.status == CoinStatus::IN_TRANSFER) { + coin_to_add = true; + } + + if coin.statechain_id == Some(statechain_id.to_string()) && coin.status == CoinStatus::DUPLICATED && + duplicated_indexes.is_some() && duplicated_indexes.as_ref().unwrap().contains(&coin.duplicate_index) { + coin_to_add = true; + } + } + + if has_matching_tx || coin_to_add { + if coin.locktime.is_none() { + return Err(anyhow::anyhow!("coin.locktime is None")); + } + + let block_header = client_config.electrum_client.block_headers_subscribe_raw()?; + let current_blockheight = block_header.height as u32; + + if current_blockheight > coin.locktime.unwrap() { + return Err(anyhow::anyhow!("The coin is expired. Coin locktime is {} and current blockheight is {}", coin.locktime.unwrap(), current_blockheight)); + } + + coin_list.push(coin); + } + } + + // The backup transaction for the CONFIRMED coin is created when it is detected in the mempool + // So it is exepcted that the coin with duplicate_index == 0 is in the list since it must have at least one backup transaction + let coins_with_zero_index = coin_list + .iter() + .filter(|coin| coin.duplicate_index == 0 && (coin.status == CoinStatus::CONFIRMED || coin.status == CoinStatus::IN_TRANSFER)) + .collect::>(); + + if coins_with_zero_index.len() != 1 { + return Err(anyhow!("There must be at least one coin with duplicate_index == 0")); + } + + // Move the coin with CONFIRMED status to the last position + // coin_list.sort_by(|a, b| { + // match (&a.status, &b.status) { + // (CoinStatus::CONFIRMED, _) => Ordering::Greater, + // (_, CoinStatus::CONFIRMED) => Ordering::Less, + // _ => Ordering::Equal, + // } + // }); + + // Move the coin with CONFIRMED status to the first position + coin_list.sort_by(|a, b| { + match (&a.status, &b.status) { + (CoinStatus::CONFIRMED, _) => Ordering::Less, + (_, CoinStatus::CONFIRMED) => Ordering::Greater, + _ => Ordering::Equal, + } + }); + + let mut new_backup_transactions = Vec::new(); + + // create backup transaction for every coin + let backup_transactions = get_backup_txs(&client_config.pool, &wallet.name, &statechain_id).await?; + + let mut new_tx_n = backup_transactions.len() as u32; + + for coin in coin_list { + + let mut filtered_transactions: Vec = Vec::new(); + + for backup_tx in &backup_transactions { + if let Ok(tx_outpoint) = get_previous_outpoint(&backup_tx) { + if let (Some(utxo_txid), Some(utxo_vout)) = (coin.utxo_txid.clone(), coin.utxo_vout) { + if tx_outpoint.txid == utxo_txid && tx_outpoint.vout == utxo_vout { + filtered_transactions.push(backup_tx.clone()); + } + } + } + } + + filtered_transactions.sort_by(|a, b| a.tx_n.cmp(&b.tx_n)); + + if filtered_transactions.len() == 0 { + new_tx_n = new_tx_n + 1; + let bkp_tx1 = create_tx1(client_config, coin, &wallet.network, new_tx_n).await?; + filtered_transactions.push(bkp_tx1); + } + + let qt_backup_tx = filtered_transactions.len() as u32; + + new_tx_n = new_tx_n + 1; + + let bkp_tx1 = &filtered_transactions[0]; + + let signed_tx = create_backup_tx_to_receiver(client_config, coin, bkp_tx1, recipient_address, qt_backup_tx, &wallet.network).await?; + + let backup_tx = BackupTx { + tx_n: new_tx_n, + tx: signed_tx.clone(), + client_public_nonce: coin.public_nonce.as_ref().unwrap().to_string(), + server_public_nonce: coin.server_public_nonce.as_ref().unwrap().to_string(), + client_public_key: coin.user_pubkey.clone(), + server_public_key: coin.server_pubkey.as_ref().unwrap().to_string(), + blinding_factor: coin.blinding_factor.as_ref().unwrap().to_string(), + }; + + filtered_transactions.push(backup_tx); + + if coin.duplicate_index == 0 { + new_backup_transactions.splice(0..0, filtered_transactions); + } else { + new_backup_transactions.extend(filtered_transactions); + } + + coin.status = CoinStatus::IN_TRANSFER; + } + + new_backup_transactions.sort_by(|a, b| a.tx_n.cmp(&b.tx_n)); + + Ok(new_backup_transactions) +} + pub async fn execute( client_config: &ClientConfig, recipient_address: &str, wallet_name: &str, statechain_id: &str, + duplicated_indexes: Option>, force_send: bool, batch_id: Option) -> Result<()> { - let mut wallet: mercurylib::wallet::Wallet = get_wallet(&client_config.pool, &wallet_name).await?; let is_address_valid = mercurylib::validate_address(recipient_address, &wallet.network)?; @@ -21,23 +182,16 @@ pub async fn execute( return Err(anyhow!("Invalid address")); } - let mut backup_transactions = get_backup_txs(&client_config.pool, &wallet.name, &statechain_id).await?; - - if backup_transactions.len() == 0 { - return Err(anyhow!("No backup transaction associated with this statechain ID were found")); - } - - let qt_backup_tx = backup_transactions.len() as u32; - - backup_transactions.sort_by(|a, b| a.tx_n.cmp(&b.tx_n)); - - let new_tx_n = backup_transactions.len() as u32 + 1; - let is_coin_duplicated = wallet.coins.iter().any(|c| { c.statechain_id == Some(statechain_id.to_string()) && c.status == CoinStatus::DUPLICATED }); + if is_coin_duplicated && !force_send { + return Err(anyhow::anyhow!("Coin is duplicated. If you want to proceed, use the command '--force, -f' option. \ + You will no longer be able to move other duplicate coins with the same statechain_id and this will cause PERMANENT LOSS of these duplicate coin funds.")); + } + let are_there_duplicate_coins_withdrawn = wallet.coins.iter().any(|c| { c.statechain_id == Some(statechain_id.to_string()) && (c.status == CoinStatus::WITHDRAWING || c.status == CoinStatus::WITHDRAWING) && @@ -50,70 +204,35 @@ pub async fn execute( This coin can be withdrawn, however.")); } - // If the user sends to himself, he will have two coins with same statechain_id - // In this case, we need to find the one with the lowest locktime - let coin = wallet.coins - .iter_mut() - .filter(|c| c.statechain_id == Some(statechain_id.to_string()) && c.status != CoinStatus::DUPLICATED) // Filter coins with the specified statechain_id + let coin = &wallet.coins + .iter() + .filter(|c| + c.statechain_id == Some(statechain_id.to_string()) && + (c.status == CoinStatus::CONFIRMED || c.status == CoinStatus::IN_TRANSFER) && + c.duplicate_index == 0) // Filter coins with the specified statechain_id .min_by_key(|c| c.locktime.unwrap_or(u32::MAX)); // Find the one with the lowest locktime if coin.is_none() { - return Err(anyhow!("No coins associated with this statechain ID were found")); - } - - let coin = coin.unwrap(); - - if is_coin_duplicated && !force_send { - return Err(anyhow::anyhow!("Coin is duplicated. If you want to proceed, use the command '--force, -f' option. \ - You will no longer be able to move other duplicate coins with the same statechain_id and this will cause PERMANENT LOSS of these duplicate coin funds.")); - } - - if coin.amount.is_none() { - return Err(anyhow::anyhow!("coin.amount is None")); + return Err(anyhow!("No coins with status CONFIRMED or IN_TRANSFER associated with this statechain ID were found")); } - if coin.status != CoinStatus::CONFIRMED && coin.status != CoinStatus::IN_TRANSFER { - return Err(anyhow::anyhow!("Coin status must be CONFIRMED or IN_TRANSFER to transfer it. The current status is {}", coin.status)); - } + let coin = coin.unwrap().clone(); - if coin.locktime.is_none() { - return Err(anyhow::anyhow!("coin.locktime is None")); - } - - let block_header = client_config.electrum_client.block_headers_subscribe_raw()?; - let current_blockheight = block_header.height as u32; - - if current_blockheight > coin.locktime.unwrap() { - return Err(anyhow::anyhow!("The coin is expired. Coin locktime is {} and current blockheight is {}", coin.locktime.unwrap(), current_blockheight)); - } - - let statechain_id = coin.statechain_id.as_ref().unwrap(); - let signed_statechain_id = coin.signed_statechain_id.as_ref().unwrap(); + let statechain_id = coin.statechain_id.as_ref().unwrap().clone(); + let signed_statechain_id = coin.signed_statechain_id.as_ref().unwrap().clone(); let (_, _, recipient_auth_pubkey) = decode_transfer_address(recipient_address)?; - let x1 = get_new_x1(&client_config, statechain_id, signed_statechain_id, &recipient_auth_pubkey.to_string(), batch_id).await?; - - let bkp_tx1 = &backup_transactions[0]; - - let signed_tx = create_backup_tx_to_receiver(client_config, coin, bkp_tx1, recipient_address, qt_backup_tx, &wallet.network).await?; - - let backup_tx = BackupTx { - tx_n: new_tx_n, - tx: signed_tx.clone(), - client_public_nonce: coin.public_nonce.as_ref().unwrap().to_string(), - server_public_nonce: coin.server_public_nonce.as_ref().unwrap().to_string(), - client_public_key: coin.user_pubkey.clone(), - server_public_key: coin.server_pubkey.as_ref().unwrap().to_string(), - blinding_factor: coin.blinding_factor.as_ref().unwrap().to_string(), - }; - - backup_transactions.push(backup_tx); + let x1 = get_new_x1(&client_config, &statechain_id, &signed_statechain_id, &recipient_auth_pubkey.to_string(), batch_id).await?; let input_txid = coin.utxo_txid.as_ref().unwrap(); let input_vout = coin.utxo_vout.unwrap(); let client_seckey = coin.user_privkey.as_ref(); - let transfer_signature = create_transfer_signature(recipient_address, input_txid, input_vout, client_seckey)?; + let coin_amount = coin.amount.unwrap(); + + let transfer_signature = create_transfer_signature(recipient_address, &input_txid, input_vout, &client_seckey)?; + + let backup_transactions = create_backup_transactions(client_config, recipient_address, &mut wallet, &statechain_id, duplicated_indexes).await?; let transfer_update_msg_request_payload = create_transfer_update_msg(&x1, recipient_address, &coin, &transfer_signature, &backup_transactions)?; @@ -138,13 +257,12 @@ pub async fn execute( let activity = Activity { utxo, - amount: coin.amount.unwrap(), + amount: coin_amount, action: "Transfer".to_string(), date: iso_string }; wallet.activities.push(activity); - coin.status = CoinStatus::IN_TRANSFER; update_wallet(&client_config.pool, &wallet).await?; diff --git a/clients/tests/rust/src/main.rs b/clients/tests/rust/src/main.rs index 692f76b7..90556314 100644 --- a/clients/tests/rust/src/main.rs +++ b/clients/tests/rust/src/main.rs @@ -3,6 +3,7 @@ pub mod electrs; pub mod bitcoin_core; pub mod ta01_sign_second_not_called; pub mod ta02_duplicate_deposits; +pub mod ta03_multiple_deposits; pub mod tb01_simple_transfer; pub mod tb02_transfer_address_reuse; pub mod tb03_simple_atomic_transfer; @@ -22,6 +23,8 @@ async fn main() -> Result<()> { ta01_sign_second_not_called::execute().await?; ta02_duplicate_deposits::execute().await?; tv05::execute().await?; + + ta03_multiple_deposits::execute().await?; Ok(()) } diff --git a/clients/tests/rust/src/ta01_sign_second_not_called.rs b/clients/tests/rust/src/ta01_sign_second_not_called.rs index 40e5112d..69ddd50b 100644 --- a/clients/tests/rust/src/ta01_sign_second_not_called.rs +++ b/clients/tests/rust/src/ta01_sign_second_not_called.rs @@ -158,7 +158,7 @@ async fn ta01(client_config: &ClientConfig, wallet1: &Wallet, wallet2: &Wallet) let force_send = false; - let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet1.name, &statechain_id, force_send, batch_id).await; + let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet1.name, &statechain_id, None, force_send, batch_id).await; assert!(result.is_ok()); diff --git a/clients/tests/rust/src/ta02_duplicate_deposits.rs b/clients/tests/rust/src/ta02_duplicate_deposits.rs index c179d747..816d1466 100644 --- a/clients/tests/rust/src/ta02_duplicate_deposits.rs +++ b/clients/tests/rust/src/ta02_duplicate_deposits.rs @@ -69,7 +69,7 @@ async fn withdraw_flow(client_config: &ClientConfig, wallet1: &Wallet, wallet2: let force_send = false; - let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet1.name, statechain_id, force_send, batch_id.clone()).await; + let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet1.name, statechain_id, None, force_send, batch_id.clone()).await; assert!(result.is_err()); @@ -86,7 +86,7 @@ async fn withdraw_flow(client_config: &ClientConfig, wallet1: &Wallet, wallet2: mercuryrustlib::coin_status::update_coins(&client_config, &wallet1.name).await?; - let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet1.name, statechain_id, force_send, batch_id).await; + let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet1.name, statechain_id, None, force_send, batch_id).await; assert!(result.is_err()); @@ -167,7 +167,7 @@ async fn transfer_flow(client_config: &ClientConfig, wallet1: &Wallet, wallet2: let force_send = true; - let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet1.name, statechain_id, force_send, batch_id.clone()).await; + let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet1.name, statechain_id, None, force_send, batch_id.clone()).await; assert!(result.is_ok()); @@ -181,7 +181,7 @@ async fn transfer_flow(client_config: &ClientConfig, wallet1: &Wallet, wallet2: let wallet1: mercuryrustlib::Wallet = mercuryrustlib::sqlite_manager::get_wallet(&client_config.pool, &wallet1.name).await?; let transferred_coin = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.status == CoinStatus::TRANSFERRED); - let duplicated_coin = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.status == CoinStatus::DUPLICATED); + let duplicated_coin = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.status == CoinStatus::INVALIDATED); assert!(transferred_coin.is_some()); assert!(duplicated_coin.is_some()); @@ -200,10 +200,10 @@ async fn transfer_flow(client_config: &ClientConfig, wallet1: &Wallet, wallet2: let error_msg = result.err().unwrap().to_string(); - assert!(error_msg == "Signature does not match authentication key."); - - + // assert!(error_msg == "Signature does not match authentication key."); + assert!(error_msg == "No duplicated coins associated with this statechain ID and index 1 were found"); + Ok(()) } @@ -230,7 +230,7 @@ pub async fn execute() -> Result<()> { withdraw_flow(&client_config, &wallet1, &wallet2).await?; transfer_flow(&client_config, &wallet1, &wallet2).await?; - println!("TA02 - Test \"Multiple Deposits in the Same Adress\" completed successfully"); + println!("TA02 - Test \"Duplicate Deposits in the Same Adress\" completed successfully"); Ok(()) } \ No newline at end of file diff --git a/clients/tests/rust/src/ta03_multiple_deposits.rs b/clients/tests/rust/src/ta03_multiple_deposits.rs new file mode 100644 index 00000000..6f3572e9 --- /dev/null +++ b/clients/tests/rust/src/ta03_multiple_deposits.rs @@ -0,0 +1,260 @@ +use std::{env, process::Command, thread, time::Duration}; + +use anyhow::{Result, Ok}; +use mercuryrustlib::{client_config::ClientConfig, BackupTx, CoinStatus, Wallet}; + +use crate::{bitcoin_core, electrs}; + +async fn deposit(amount_in_sats: u32, client_config: &ClientConfig, deposit_address: &str) -> Result<()> { + + let _ = bitcoin_core::sendtoaddress(amount_in_sats, &deposit_address)?; + + let core_wallet_address = bitcoin_core::getnewaddress()?; + let remaining_blocks = client_config.confirmation_target; + let _ = bitcoin_core::generatetoaddress(remaining_blocks, &core_wallet_address)?; + + // It appears that Electrs takes a few seconds to index the transaction + let mut is_tx_indexed = false; + + while !is_tx_indexed { + is_tx_indexed = electrs::check_address(client_config, &deposit_address, 1000).await?; + thread::sleep(Duration::from_secs(1)); + } + + Ok(()) +} + +fn validate_backup_transactions(backup_transactions: &Vec, interval: u32) -> Result<()> { + let mut current_txid: Option = None; + let mut current_vout: Option = None; + let mut current_tx_n = 0u32; + let mut previous_lock_time = 0u32; + + for backup_tx in backup_transactions { + let outpoint = mercuryrustlib::get_previous_outpoint(&backup_tx)?; + + let current_lock_time = mercuryrustlib::get_blockheight(&backup_tx)?; + + if current_txid.is_some() && current_vout.is_some() { + assert!(current_txid.unwrap() == outpoint.txid && current_vout.unwrap() == outpoint.vout); + assert!(current_lock_time == previous_lock_time - interval); + assert!(backup_tx.tx_n > current_tx_n); + } + + current_txid = Some(outpoint.txid); + current_vout = Some(outpoint.vout); + current_tx_n = backup_tx.tx_n; + previous_lock_time = current_lock_time; + } + + Ok(()) +} + +async fn basic_workflow(client_config: &ClientConfig, wallet1: &Wallet, wallet2: &Wallet) -> Result<()> { + + let amount = 1000; + + let token_id = mercuryrustlib::deposit::get_token(client_config).await?; + + let deposit_address = mercuryrustlib::deposit::get_deposit_bitcoin_address(&client_config, &wallet1.name, &token_id, amount).await?; + + deposit(amount, &client_config, &deposit_address).await?; + + let amount = 2000; + + deposit(amount, &client_config, &deposit_address).await?; + + deposit(amount, &client_config, &deposit_address).await?; + + let amount = 1000; + + deposit(amount, &client_config, &deposit_address).await?; + + mercuryrustlib::coin_status::update_coins(&client_config, &wallet1.name).await?; + let wallet1: mercuryrustlib::Wallet = mercuryrustlib::sqlite_manager::get_wallet(&client_config.pool, &wallet1.name).await?; + + let new_coin = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 0 && coin.status == CoinStatus::CONFIRMED); + let duplicated_coin_1 = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 1 && coin.status == CoinStatus::DUPLICATED); + let duplicated_coin_2 = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 2 && coin.status == CoinStatus::DUPLICATED); + let duplicated_coin_3 = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 3 && coin.status == CoinStatus::DUPLICATED); + + assert!(new_coin.is_some()); + assert!(duplicated_coin_1.is_some()); + assert!(duplicated_coin_2.is_some()); + assert!(duplicated_coin_3.is_some()); + + let new_coin = new_coin.unwrap(); + // let duplicated_coin_1 = duplicated_coin_1.unwrap(); + // let duplicated_coin_2 = duplicated_coin_2.unwrap(); + // let duplicated_coin_3 = duplicated_coin_3.unwrap(); + + let statechain_id = new_coin.statechain_id.as_ref().unwrap(); + + let wallet2_transfer_adress = mercuryrustlib::transfer_receiver::new_transfer_address(&client_config, &wallet2.name).await?; + + let batch_id = None; + + let force_send = true; + + let duplicated_indexes = vec![1, 3]; + + let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet1.name, &statechain_id, Some(duplicated_indexes), force_send, batch_id).await; + + // result.unwrap(); + assert!(result.is_ok()); + + let wallet1 = mercuryrustlib::sqlite_manager::get_wallet(&client_config.pool, &wallet1.name).await?; + + let new_coin = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 0 && coin.status == CoinStatus::IN_TRANSFER); + let duplicated_coin_1 = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 1 && coin.status == CoinStatus::IN_TRANSFER); + let duplicated_coin_2 = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 2 && coin.status == CoinStatus::DUPLICATED); + let duplicated_coin_3 = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 3 && coin.status == CoinStatus::IN_TRANSFER); + + assert!(new_coin.is_some()); + assert!(duplicated_coin_1.is_some()); + assert!(duplicated_coin_2.is_some()); + assert!(duplicated_coin_3.is_some()); + + let new_coin = new_coin.unwrap(); + + let backup_transactions = mercuryrustlib::sqlite_manager::get_backup_txs(&client_config.pool, &wallet1.name, &statechain_id).await?; + + /* for backup_tx in backup_transactions.iter() { + let tx_outpoint = mercuryrustlib::get_previous_outpoint(&backup_tx)?; + println!("statechain_id: {}", statechain_id); + println!("txid: {} vout: {}", tx_outpoint.txid, tx_outpoint.vout); + println!("tx_n: {}", backup_tx.tx_n); + // println!("client_public_nonce: {}", backup_tx.client_public_nonce); + // println!("server_public_nonce: {}", backup_tx.server_public_nonce); + // println!("client_public_key: {}", backup_tx.client_public_key); + // println!("server_public_key: {}", backup_tx.server_public_key); + // println!("blinding_factor: {}", backup_tx.blinding_factor); + println!("----------------------"); + } */ + + let info_config = mercuryrustlib::utils::info_config(&client_config).await?; + + let split_backup_transactions = mercuryrustlib::transfer_receiver::split_backup_transactions(&backup_transactions); + + for (index, bk_txs) in split_backup_transactions.iter().enumerate() { + if index == 0 { + let first_bkp_tx = bk_txs.first().unwrap(); + let first_backup_outpoint = mercuryrustlib::get_previous_outpoint(&first_bkp_tx)?; + assert!(first_backup_outpoint.txid == new_coin.utxo_txid.clone().unwrap() && first_backup_outpoint.vout == new_coin.utxo_vout.unwrap()); + } + validate_backup_transactions(&bk_txs, info_config.interval)?; + } + + /* println!("Grouped Backup Transactions:"); + + for backup_txs in grouped_backup_transactions.iter() { + for backup_tx in backup_txs.iter() { + let tx_outpoint = mercuryrustlib::get_previous_outpoint(&backup_tx)?; + println!("statechain_id: {}", statechain_id); + println!("txid: {} vout: {}", tx_outpoint.txid, tx_outpoint.vout); + println!("tx_n: {}", backup_tx.tx_n); + // println!("client_public_nonce: {}", backup_tx.client_public_nonce); + // println!("server_public_nonce: {}", backup_tx.server_public_nonce); + // println!("client_public_key: {}", backup_tx.client_public_key); + // println!("server_public_key: {}", backup_tx.server_public_key); + // println!("blinding_factor: {}", backup_tx.blinding_factor); + println!("----------------------"); + } + } */ + + + + + // for backup_tx in backup_transactions.iter() { + // let tx_outpoint = mercuryrustlib::get_previous_outpoint(&backup_tx)?; + // println!("txid: {} vout: {}", tx_outpoint.txid, tx_outpoint.vout); + // println!("tx_n: {}", backup_tx.tx_n); + // println!("client_public_nonce: {}", backup_tx.client_public_nonce); + // println!("server_public_nonce: {}", backup_tx.server_public_nonce); + // println!("client_public_key: {}", backup_tx.client_public_key); + // println!("server_public_key: {}", backup_tx.server_public_key); + // println!("blinding_factor: {}", backup_tx.blinding_factor); + // println!("----------------------"); + // } + + let transfer_receive_result = mercuryrustlib::transfer_receiver::execute(&client_config, &wallet2.name).await?; + + let received_statechain_ids = transfer_receive_result.received_statechain_ids; + + assert!(received_statechain_ids.contains(&statechain_id.to_string())); + assert!(received_statechain_ids.len() == 1); + + let wallet2: mercuryrustlib::Wallet = mercuryrustlib::sqlite_manager::get_wallet(&client_config.pool, &wallet2.name).await?; + + let w2_new_coin = wallet2.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 0 && coin.status == CoinStatus::CONFIRMED); + let w2_duplicated_coin_1 = wallet2.coins.iter().find(|&coin| coin.statechain_id == Some(statechain_id.to_string()) && coin.duplicate_index == 1 && coin.status == CoinStatus::DUPLICATED); + let w2_duplicated_coin_2 = wallet2.coins.iter().find(|&coin| coin.statechain_id == Some(statechain_id.to_string()) && coin.duplicate_index == 2 && coin.status == CoinStatus::DUPLICATED); + + assert!(w2_new_coin.is_some()); + assert!(w2_duplicated_coin_1.is_some()); + assert!(w2_duplicated_coin_2.is_some()); + + /* let mut coins_json = Vec::new(); + + for coin in wallet2.coins.iter() { + let obj = json!({ + "coin.user_pubkey": coin.user_pubkey, + "coin.aggregated_address": coin.aggregated_address.as_ref().unwrap_or(&"".to_string()), + "coin.address": coin.address, + "coin.statechain_id": coin.statechain_id.as_ref().unwrap_or(&"".to_string()), + "coin.amount": coin.amount.unwrap_or(0), + "coin.status": coin.status, + "coin.locktime": coin.locktime.unwrap_or(0), + "coin.duplicate_index": coin.duplicate_index, + }); + + coins_json.push(obj); + } + + let coins_json_string = serde_json::to_string_pretty(&coins_json).unwrap(); + println!("{}", coins_json_string); */ + + mercuryrustlib::coin_status::update_coins(&client_config, &wallet1.name).await?; + let wallet1: mercuryrustlib::Wallet = mercuryrustlib::sqlite_manager::get_wallet(&client_config.pool, &wallet1.name).await?; + + let new_coin = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 0 && coin.status == CoinStatus::TRANSFERRED); + let duplicated_coin_1 = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 1 && coin.status == CoinStatus::TRANSFERRED); + let duplicated_coin_2 = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 2 && coin.status == CoinStatus::INVALIDATED); + let duplicated_coin_3 = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 3 && coin.status == CoinStatus::TRANSFERRED); + + assert!(new_coin.is_some()); + assert!(duplicated_coin_1.is_some()); + assert!(duplicated_coin_2.is_some()); + assert!(duplicated_coin_3.is_some()); + + // TODO: DUPLICATED coins should be invalidated after the other with same statechain_id is transferred + + Ok(()) +} + +pub async fn execute() -> Result<()> { + + let _ = Command::new("rm").arg("wallet.db").arg("wallet.db-shm").arg("wallet.db-wal").output().expect("failed to execute process"); + + env::set_var("ML_NETWORK", "regtest"); + + let client_config = mercuryrustlib::client_config::load().await; + + let wallet1 = mercuryrustlib::wallet::create_wallet( + "wallet1", + &client_config).await?; + + mercuryrustlib::sqlite_manager::insert_wallet(&client_config.pool, &wallet1).await?; + + let wallet2 = mercuryrustlib::wallet::create_wallet( + "wallet2", + &client_config).await?; + + mercuryrustlib::sqlite_manager::insert_wallet(&client_config.pool, &wallet2).await?; + + basic_workflow(&client_config, &wallet1, &wallet2).await?; + + println!("TA03 - Test \"Multiple Deposits in the Same Adress\" completed successfully"); + + Ok(()) +} \ No newline at end of file diff --git a/clients/tests/rust/src/tb01_simple_transfer.rs b/clients/tests/rust/src/tb01_simple_transfer.rs index e3c7d98d..ff5736aa 100644 --- a/clients/tests/rust/src/tb01_simple_transfer.rs +++ b/clients/tests/rust/src/tb01_simple_transfer.rs @@ -6,19 +6,19 @@ use mercuryrustlib::{client_config::ClientConfig, CoinStatus, Wallet}; use crate::{bitcoin_core, electrs}; -async fn try_to_send_unconfirmed_coin(client_config: &ClientConfig, to_address: &str, wallet: &Wallet, statechain_id: &str, current_status: &str) -> Result<()> { +async fn try_to_send_unconfirmed_coin(client_config: &ClientConfig, to_address: &str, wallet: &Wallet, statechain_id: &str) -> Result<()> { let batch_id = None; let force_send = false; - let result = mercuryrustlib::transfer_sender::execute(&client_config, to_address, &wallet.name, &statechain_id, force_send, batch_id).await; + let result = mercuryrustlib::transfer_sender::execute(&client_config, to_address, &wallet.name, &statechain_id, None, force_send, batch_id).await; assert!(result.is_err()); let error = result.err().unwrap(); - let error_message = format!("Coin status must be CONFIRMED or IN_TRANSFER to transfer it. The current status is {}", current_status); + let error_message = format!("No coins with status CONFIRMED or IN_TRANSFER associated with this statechain ID were found"); assert!(error.to_string() == error_message); @@ -90,7 +90,7 @@ async fn sucessfully_transfer(client_config: &ClientConfig, wallet1: &Wallet, wa let statechain_id = new_coin.statechain_id.as_ref().unwrap(); - try_to_send_unconfirmed_coin(&client_config, &wallet2_transfer_adress, &wallet1, statechain_id, &CoinStatus::IN_MEMPOOL.to_string()).await?; + try_to_send_unconfirmed_coin(&client_config, &wallet2_transfer_adress, &wallet1, statechain_id).await?; let core_wallet_address = bitcoin_core::getnewaddress()?; let _ = bitcoin_core::generatetoaddress(1, &core_wallet_address)?; @@ -103,7 +103,7 @@ async fn sucessfully_transfer(client_config: &ClientConfig, wallet1: &Wallet, wa assert!(new_coin.status == CoinStatus::UNCONFIRMED); - try_to_send_unconfirmed_coin(&client_config, &wallet2_transfer_adress, &wallet1, statechain_id, &CoinStatus::UNCONFIRMED.to_string()).await?; + try_to_send_unconfirmed_coin(&client_config, &wallet2_transfer_adress, &wallet1, statechain_id).await?; let remaining_blocks = client_config.confirmation_target - 1; let _ = bitcoin_core::generatetoaddress(remaining_blocks, &core_wallet_address)?; @@ -120,7 +120,7 @@ async fn sucessfully_transfer(client_config: &ClientConfig, wallet1: &Wallet, wa let force_send = false; - let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet.name, &statechain_id, force_send, batch_id).await; + let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet.name, &statechain_id, None, force_send, batch_id).await; assert!(result.is_ok()); diff --git a/clients/tests/rust/src/tb02_transfer_address_reuse.rs b/clients/tests/rust/src/tb02_transfer_address_reuse.rs index e02ed726..e7478254 100644 --- a/clients/tests/rust/src/tb02_transfer_address_reuse.rs +++ b/clients/tests/rust/src/tb02_transfer_address_reuse.rs @@ -56,7 +56,7 @@ async fn tb02(client_config: &ClientConfig, wallet1: &Wallet, wallet2: &Wallet) let statechain_id = coin.statechain_id.as_ref().unwrap(); - let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet1.name, &statechain_id, force_send, batch_id).await; + let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet1.name, &statechain_id, None, force_send, batch_id).await; assert!(result.is_ok()); } @@ -112,7 +112,7 @@ async fn tb02(client_config: &ClientConfig, wallet1: &Wallet, wallet2: &Wallet) let force_send = false; - let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet1_transfer_adress, &wallet2.name, &statechain_id, force_send, batch_id).await; + let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet1_transfer_adress, &wallet2.name, &statechain_id, None, force_send, batch_id).await; assert!(result.is_ok()); diff --git a/clients/tests/rust/src/tb03_simple_atomic_transfer.rs b/clients/tests/rust/src/tb03_simple_atomic_transfer.rs index 4bbfb346..1a064f6d 100644 --- a/clients/tests/rust/src/tb03_simple_atomic_transfer.rs +++ b/clients/tests/rust/src/tb03_simple_atomic_transfer.rs @@ -48,7 +48,7 @@ pub async fn tb03(client_config: &ClientConfig, wallet1: &Wallet, wallet2: &Wall let force_send = false; - let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet3_transfer_adress, &wallet1.name, &statechain_id_1, force_send, batch_id.clone()).await; + let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet3_transfer_adress, &wallet1.name, &statechain_id_1, None, force_send, batch_id.clone()).await; assert!(result.is_ok()); @@ -57,7 +57,7 @@ pub async fn tb03(client_config: &ClientConfig, wallet1: &Wallet, wallet2: &Wall let new_coin = wallet2.coins.iter().find(|&coin| coin.aggregated_address == Some(wallet2_address.clone()) && coin.status == CoinStatus::CONFIRMED).unwrap(); let statechain_id_2 = new_coin.statechain_id.as_ref().unwrap(); - let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet4_transfer_adress, &wallet2.name, &statechain_id_2, force_send, batch_id).await; + let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet4_transfer_adress, &wallet2.name, &statechain_id_2, None, force_send, batch_id).await; assert!(result.is_ok()); diff --git a/clients/tests/rust/src/tb04_simple_lightning_latch.rs b/clients/tests/rust/src/tb04_simple_lightning_latch.rs index eb4f1e27..3dcc6a1e 100644 --- a/clients/tests/rust/src/tb04_simple_lightning_latch.rs +++ b/clients/tests/rust/src/tb04_simple_lightning_latch.rs @@ -44,7 +44,7 @@ pub async fn tb04(client_config: &ClientConfig, wallet1: &Wallet, wallet2: &Wall let force_send = false; - mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet1.name, statechain_id, force_send, Some(batch_id.clone())).await?; + mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet1.name, statechain_id, None, force_send, Some(batch_id.clone())).await?; // receiver confirms that payment hash was generated by the server let response = mercuryrustlib::lightning_latch::get_payment_hash(&client_config, &batch_id).await?; diff --git a/clients/tests/rust/src/tm01_sender_double_spends.rs b/clients/tests/rust/src/tm01_sender_double_spends.rs index 113a3fc2..513b7bfb 100644 --- a/clients/tests/rust/src/tm01_sender_double_spends.rs +++ b/clients/tests/rust/src/tm01_sender_double_spends.rs @@ -40,7 +40,7 @@ async fn tm01(client_config: &ClientConfig, wallet1: &Wallet, wallet2: &Wallet, let wallet2_transfer_adress = mercuryrustlib::transfer_receiver::new_transfer_address(&client_config, &wallet2.name).await?; - let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet1.name, &statechain_id, force_send, batch_id).await; + let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet1.name, &statechain_id, None, force_send, batch_id).await; assert!(result.is_ok()); @@ -49,7 +49,7 @@ async fn tm01(client_config: &ClientConfig, wallet1: &Wallet, wallet2: &Wallet, let batch_id = None; // this first "double spend" is legitimate, as it will overwrite the previous transaction - let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet3_transfer_adress, &wallet1.name, &statechain_id, force_send, batch_id).await; + let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet3_transfer_adress, &wallet1.name, &statechain_id, None, force_send, batch_id).await; assert!(result.is_ok()); @@ -63,7 +63,7 @@ async fn tm01(client_config: &ClientConfig, wallet1: &Wallet, wallet2: &Wallet, let batch_id = None; // this second "double spend" is not legitimate, as the statecoin has already been received by wallet3 - let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet1.name, &statechain_id, force_send, batch_id).await; + let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet1.name, &statechain_id, None, force_send, batch_id).await; assert!(result.is_err()); @@ -77,11 +77,11 @@ async fn tm01(client_config: &ClientConfig, wallet1: &Wallet, wallet2: &Wallet, let batch_id = None; - let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet1.name, &statechain_id, force_send, batch_id).await; + let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet1.name, &statechain_id, None, force_send, batch_id).await; assert!(result.is_err()); - assert!(result.err().unwrap().to_string().contains("Coin status must be CONFIRMED or IN_TRANSFER to transfer it. The current status is TRANSFERRED")); + assert!(result.err().unwrap().to_string().contains("No coins with status CONFIRMED or IN_TRANSFER associated with this statechain ID were found")); Ok(()) } diff --git a/clients/tests/rust/src/tv05.rs b/clients/tests/rust/src/tv05.rs index 29bff91f..43cd8e35 100644 --- a/clients/tests/rust/src/tv05.rs +++ b/clients/tests/rust/src/tv05.rs @@ -39,7 +39,7 @@ async fn w1_transfer_to_w2(client_config: &ClientConfig, wallet1: &Wallet, walle let batch_id = None; let force_send = false; - let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet1.name, &statechain_id, force_send, batch_id).await; + let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet1.name, &statechain_id, None, force_send, batch_id).await; assert!(result.is_ok()); let transfer_receive_result = mercuryrustlib::transfer_receiver::execute(&client_config, &wallet2.name).await?; diff --git a/lib/src/transfer/mod.rs b/lib/src/transfer/mod.rs index 03be674d..aeaf48b2 100644 --- a/lib/src/transfer/mod.rs +++ b/lib/src/transfer/mod.rs @@ -98,3 +98,10 @@ pub struct TransferMsg { pub t1: [u8; 32], pub user_public_key: String, } + +#[derive(Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "bindings", derive(uniffi::Record))] +pub struct TxOutpoint { + pub txid: String, + pub vout: u32, +} diff --git a/lib/src/transfer/receiver.rs b/lib/src/transfer/receiver.rs index 541aa0c4..ebf76ed9 100644 --- a/lib/src/transfer/receiver.rs +++ b/lib/src/transfer/receiver.rs @@ -4,9 +4,9 @@ use bitcoin::{hashes::{sha256, Hash}, sighash::{self, SighashCache, TapSighashTy use secp256k1_zkp::{PublicKey, schnorr::Signature, Secp256k1, Message, XOnlyPublicKey, musig::{MusigPubNonce, BlindingFactor, blinded_musig_pubkey_xonly_tweak_add, MusigAggNonce, MusigSession}, SecretKey, Scalar, KeyPair}; use serde::{Serialize, Deserialize}; -use crate::{error::MercuryError, utils::get_network, wallet::{BackupTx, Coin, CoinStatus, Wallet}}; +use crate::{error::MercuryError, utils::get_network, wallet::{get_previous_outpoint, BackupTx, Coin, CoinStatus, Wallet}}; -use super::TransferMsg; +use super::{TransferMsg, TxOutpoint}; #[derive(Debug, Serialize, Deserialize)] #[cfg_attr(feature = "bindings", derive(uniffi::Record))] @@ -77,13 +77,6 @@ pub struct StatechainInfoResponsePayload { pub x1_pub: Option, } -#[derive(Debug, Serialize, Deserialize)] -#[cfg_attr(feature = "bindings", derive(uniffi::Record))] -pub struct TxOutpoint { - pub txid: String, - pub vout: u32, -} - #[derive(Debug, Serialize, Deserialize)] #[cfg_attr(feature = "bindings", derive(uniffi::Record))] pub struct NewKeyInfo { @@ -159,20 +152,7 @@ pub fn get_tx0_outpoint(backup_transactions: &Vec) -> Result 1 { - return Err(MercuryError::Tx1HasMoreThanOneInput); - } - - if tx1.output.len() > 1 { - return Err(MercuryError::Tx1HasMoreThanOneInput); - } - - let tx0_txid = tx1.input[0].previous_output.txid; - let tx0_vout = tx1.input[0].previous_output.vout as u32; - - Ok(TxOutpoint{ txid: tx0_txid.to_string(), vout: tx0_vout }) + get_previous_outpoint(bkp_tx1) } pub fn verify_transfer_signature(new_user_pubkey: &str, tx0_outpoint: &TxOutpoint, transfer_msg: &TransferMsg) -> Result { @@ -261,8 +241,19 @@ pub fn get_output_address_from_tx0(tx0_outpoint: &TxOutpoint, tx0_hex: &str, net Ok(address.to_string()) } +#[cfg_attr(feature = "bindings", uniffi::export)] +pub fn get_amount_from_tx0(tx0_hex: &str, tx0_outpoint: &TxOutpoint,) -> Result { + + let tx0: Transaction = bitcoin::consensus::encode::deserialize(&hex::decode(&tx0_hex)?)?; + + assert!(tx0_outpoint.txid == tx0.txid().to_string()); + + Ok(tx0.output[tx0_outpoint.vout as usize].value) +} + +#[cfg_attr(feature = "bindings", uniffi::export)] pub fn validate_signature_scheme( - transfer_msg: &TransferMsg, + backup_transactions: &Vec, statechain_info: &StatechainInfoResponsePayload, tx0_hex: &str, current_blockheight: u32, @@ -275,9 +266,19 @@ pub fn validate_signature_scheme( let mut sig_scheme_validation = true; - for (index, backup_tx) in transfer_msg.backup_transactions.iter().enumerate() { + for backup_tx in backup_transactions.iter() { - let statechain_info = statechain_info.statechain_info.get(index).unwrap(); + let statechain_info = statechain_info.statechain_info + .iter() + .find(|info| info.tx_n == backup_tx.tx_n); + + if statechain_info.is_none() { + println!("statechain_info not found"); + sig_scheme_validation = false; + break; + } + + let statechain_info = statechain_info.unwrap(); let is_signature_valid = verify_transaction_signature(&backup_tx.tx, &tx0_hex, fee_rate_tolerance, current_fee_rate_sats_per_byte); if is_signature_valid.is_err() { @@ -483,7 +484,8 @@ fn get_tx_hash(tx_0: &Transaction, tx_n: &Transaction) -> Result "TRANSFERRED", Self::WITHDRAWN => "WITHDRAWN", Self::DUPLICATED => "DUPLICATED", + Self::INVALIDATED => "INVALIDATED", }) } } @@ -165,6 +168,7 @@ impl FromStr for CoinStatus { "TRANSFERRED" => Ok(CoinStatus::TRANSFERRED), "WITHDRAWN" => Ok(CoinStatus::WITHDRAWN), "DUPLICATED" => Ok(CoinStatus::DUPLICATED), + "INVALIDATED" => Ok(CoinStatus::INVALIDATED), _ => Err(CoinStatusParseError {}), } } @@ -186,7 +190,26 @@ pub struct BackupTx { pub client_public_key: String, pub server_public_key: String, pub blinding_factor: String, -} +} + +#[cfg_attr(feature = "bindings", uniffi::export)] +pub fn get_previous_outpoint(backup_tx: &BackupTx) -> Result { + + let tx1: Transaction = bitcoin::consensus::encode::deserialize(&hex::decode(backup_tx.tx.clone())?)?; + + if tx1.input.len() > 1 { + return Err(MercuryError::Tx1HasMoreThanOneInput); + } + + if tx1.output.len() > 1 { + return Err(MercuryError::Tx1HasMoreThanOneInput); + } + + let tx0_txid = tx1.input[0].previous_output.txid; + let tx0_vout = tx1.input[0].previous_output.vout as u32; + + Ok(TxOutpoint{ txid: tx0_txid.to_string(), vout: tx0_vout }) +} pub fn set_config(wallet: &mut Wallet, config: &ServerConfig) { wallet.initlock = config.initlock; diff --git a/wasm/src/lib.rs b/wasm/src/lib.rs index 7feafe55..57083a00 100644 --- a/wasm/src/lib.rs +++ b/wasm/src/lib.rs @@ -2,7 +2,7 @@ mod utils; -use mercurylib::{decode_transfer_address, deposit::DepositMsg1Response, transfer::{receiver::{create_transfer_receiver_request_payload, decrypt_transfer_msg, get_new_key_info, StatechainInfo, StatechainInfoResponsePayload, TxOutpoint}, sender::create_transfer_signature, TransferMsg}, utils::ServerConfig, wallet::{Activity, BackupTx, Coin, Settings, Token, Wallet}}; +use mercurylib::{decode_transfer_address, deposit::DepositMsg1Response, transfer::{receiver::{create_transfer_receiver_request_payload, decrypt_transfer_msg, get_new_key_info, StatechainInfo, StatechainInfoResponsePayload}, sender::create_transfer_signature, TransferMsg, TxOutpoint}, utils::ServerConfig, wallet::{Activity, BackupTx, Coin, Settings, Token, Wallet}}; use wasm_bindgen::prelude::*; use serde::{Serialize, Deserialize}; use bip39::Mnemonic; @@ -473,7 +473,7 @@ pub fn validateSignatureScheme(transfer_msg: JsValue, statechain_info: JsValue, let statechain_info: StatechainInfoResponsePayload = serde_wasm_bindgen::from_value(statechain_info).unwrap(); let transfer_msg: TransferMsg = serde_wasm_bindgen::from_value(transfer_msg).unwrap(); - let result = mercurylib::transfer::receiver::validate_signature_scheme(&transfer_msg, &statechain_info, &tx0_hex, current_blockheight, fee_rate_tolerance as f64, current_fee_rate_sats_per_byte as f64, lockheight_init, interval); + let result = mercurylib::transfer::receiver::validate_signature_scheme(&transfer_msg.backup_transactions, &statechain_info, &tx0_hex, current_blockheight, fee_rate_tolerance as f64, current_fee_rate_sats_per_byte as f64, lockheight_init, interval); #[derive(Serialize, Deserialize)] struct ValidationResult {