From d2e889c32a5d87f6889d57d6872d2780d6ab2c91 Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Sat, 6 Sep 2025 13:28:37 +0300 Subject: [PATCH 01/10] relax a couple of derive_htlc_key_pair to derive_htlc_pubkey used in swaps v2 --- mm2src/coins/utxo/utxo_common.rs | 36 +++++++++++++++++------------- mm2src/coins/utxo/utxo_standard.rs | 3 ++- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index 7024d3fa03..0974c8e5c1 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -1852,17 +1852,19 @@ pub fn create_maker_payment_spend_preimage( drop_mutability!(prev_transaction); let payment_value = try_tx_fus!(prev_transaction.first_output()).value; - let key_pair = coin.derive_htlc_key_pair(swap_unique_data); + let taker_pub = coin.derive_htlc_pubkey(swap_unique_data); let script_data = Builder::default().into_script(); let redeem_script = payment_script( time_lock, secret_hash, &try_tx_fus!(Public::from_slice(maker_pub)), - key_pair.public(), + &try_tx_fus!(Public::from_slice(&taker_pub)), ) .into(); + let coin = coin.clone(); + let swap_unique_data = swap_unique_data.to_vec(); let fut = async move { let my_address = try_tx_s!(coin.as_ref().derivation_method.single_addr_or_err().await); let fee = try_tx_s!( @@ -1890,7 +1892,7 @@ pub fn create_maker_payment_spend_preimage( script_data, sequence: SEQUENCE_FINAL, lock_time: time_lock, - signer: P2SHSigner::KeyPair(key_pair), + signer: try_tx_s!(P2SHSigner::try_from_coin(&coin, &swap_unique_data)), }; let transaction = try_tx_s!(coin.p2sh_spending_tx(input).await); @@ -1907,7 +1909,6 @@ pub fn create_taker_payment_refund_preimage( secret_hash: &[u8], swap_unique_data: &[u8], ) -> TransactionFut { - let coin = coin.clone(); let mut prev_transaction: UtxoTx = try_tx_fus!(deserialize(taker_payment_tx).map_err(|e| TransactionErr::Plain(format!( "Failed to deserialize transaction (coin={}, hex={}) : {}", @@ -1919,15 +1920,18 @@ pub fn create_taker_payment_refund_preimage( drop_mutability!(prev_transaction); let payment_value = try_tx_fus!(prev_transaction.first_output()).value; - let key_pair = coin.derive_htlc_key_pair(swap_unique_data); + let taker_pub = coin.derive_htlc_pubkey(swap_unique_data); let script_data = Builder::default().push_opcode(Opcode::OP_1).into_script(); let redeem_script = payment_script( time_lock, secret_hash, - key_pair.public(), + &try_tx_fus!(Public::from_slice(&taker_pub)), &try_tx_fus!(Public::from_slice(maker_pub)), ) .into(); + + let coin = coin.clone(); + let swap_unique_data = swap_unique_data.to_vec(); let fut = async move { let my_address = try_tx_s!(coin.as_ref().derivation_method.single_addr_or_err().await); let fee = try_tx_s!( @@ -1954,7 +1958,7 @@ pub fn create_taker_payment_refund_preimage( script_data, sequence: SEQUENCE_FINAL - 1, lock_time: time_lock, - signer: P2SHSigner::KeyPair(key_pair), + signer: try_tx_s!(P2SHSigner::try_from_coin(&coin, &swap_unique_data)), }; let transaction = try_tx_s!(coin.p2sh_spending_tx(input).await); @@ -5385,7 +5389,7 @@ where let my_address = try_tx_s!(coin.as_ref().derivation_method.single_addr_or_err().await).clone(); let payment_value = try_tx_s!(args.funding_tx.first_output()).value; - let key_pair = coin.derive_htlc_key_pair(args.swap_unique_data); + let taker_pubkey = coin.derive_htlc_pubkey(args.swap_unique_data); let script_data = Builder::default() .push_data(args.taker_secret) .push_opcode(Opcode::OP_0) @@ -5396,7 +5400,7 @@ where let redeem_script = swap_proto_v2_scripts::taker_funding_script( time_lock, args.taker_secret_hash, - key_pair.public(), + &try_tx_s!(Public::from_slice(&taker_pubkey)), args.maker_pubkey, ) .into(); @@ -5424,7 +5428,7 @@ where script_data, sequence: SEQUENCE_FINAL, lock_time: time_lock, - signer: P2SHSigner::KeyPair(key_pair), + signer: try_tx_s!(P2SHSigner::try_from_coin(&coin, args.swap_unique_data)), }; let transaction = try_tx_s!(coin.p2sh_spending_tx(input).await); @@ -5569,7 +5573,7 @@ pub async fn spend_maker_payment_v2( let my_address = try_tx_s!(coin.as_ref().derivation_method.single_addr_or_err().await).clone(); let payment_value = try_tx_s!(args.maker_payment_tx.first_output()).value; - let key_pair = coin.derive_htlc_key_pair(args.swap_unique_data); + let taker_pub = coin.derive_htlc_pubkey(args.swap_unique_data); let script_data = Builder::default() .push_data(&args.maker_secret) .push_opcode(Opcode::OP_1) @@ -5582,7 +5586,7 @@ pub async fn spend_maker_payment_v2( args.maker_secret_hash, args.taker_secret_hash, args.maker_pub, - key_pair.public(), + &try_tx_s!(Public::from_slice(&taker_pub)), ) .into(); @@ -5610,7 +5614,7 @@ pub async fn spend_maker_payment_v2( script_data, sequence: SEQUENCE_FINAL, lock_time: time_lock, - signer: P2SHSigner::KeyPair(key_pair), + signer: try_tx_s!(P2SHSigner::try_from_coin(coin, args.swap_unique_data)), }; let transaction = try_tx_s!(coin.p2sh_spending_tx(input).await); @@ -5631,7 +5635,7 @@ where let my_address = try_tx_s!(coin.as_ref().derivation_method.single_addr_or_err().await).clone(); let payment_value = try_tx_s!(args.maker_payment_tx.first_output()).value; - let key_pair = coin.derive_htlc_key_pair(args.swap_unique_data); + let maker_pub = coin.derive_htlc_pubkey(args.swap_unique_data); let script_data = Builder::default() .push_data(args.taker_secret) .push_opcode(Opcode::OP_0) @@ -5643,7 +5647,7 @@ where time_lock, args.maker_secret_hash, args.taker_secret_hash, - key_pair.public(), + &try_tx_s!(Public::from_slice(&maker_pub)), args.taker_pub, ) .into(); @@ -5671,7 +5675,7 @@ where script_data, sequence: SEQUENCE_FINAL, lock_time: time_lock, - signer: P2SHSigner::KeyPair(key_pair), + signer: try_tx_s!(P2SHSigner::try_from_coin(&coin, args.swap_unique_data)), }; let transaction = try_tx_s!(coin.p2sh_spending_tx(input).await); diff --git a/mm2src/coins/utxo/utxo_standard.rs b/mm2src/coins/utxo/utxo_standard.rs index c8b8c256b6..a975220453 100644 --- a/mm2src/coins/utxo/utxo_standard.rs +++ b/mm2src/coins/utxo/utxo_standard.rs @@ -853,7 +853,8 @@ impl TakerCoinSwapOpsV2 for UtxoStandardCoin { impl CommonSwapOpsV2 for UtxoStandardCoin { fn derive_htlc_pubkey_v2(&self, swap_unique_data: &[u8]) -> Self::Pubkey { - *self.derive_htlc_key_pair(swap_unique_data).public() + let pubkey_bytes = self.derive_htlc_pubkey(swap_unique_data); + Public::Compressed(pubkey_bytes.into()) } #[inline(always)] From 74a54cec2dc1e641a6206773b5b66a43c91cba92 Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Sat, 6 Sep 2025 13:31:55 +0300 Subject: [PATCH 02/10] simplify error handling by using try_tx_s! --- mm2src/coins/utxo/utxo_common.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index 0974c8e5c1..025a6f0872 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -1766,8 +1766,7 @@ pub async fn send_maker_spends_taker_payment( script_pubkey, }; - let signer = P2SHSigner::try_from_coin(&coin, args.swap_unique_data) - .map_err(|e| TransactionErr::Plain(ERRL!("Failed to create P2SHSigner: {}", e)))?; + let signer = try_tx_s!(P2SHSigner::try_from_coin(&coin, args.swap_unique_data)); let input = P2SHSpendingTxInput { prev_transaction, @@ -2015,8 +2014,7 @@ pub async fn send_taker_spends_maker_payment( script_pubkey, }; - let signer = P2SHSigner::try_from_coin(&coin, args.swap_unique_data) - .map_err(|e| TransactionErr::Plain(ERRL!("Failed to create P2SHSigner: {}", e)))?; + let signer = try_tx_s!(P2SHSigner::try_from_coin(&coin, args.swap_unique_data)); let input = P2SHSpendingTxInput { prev_transaction, @@ -2077,8 +2075,7 @@ pub async fn refund_htlc_payment( script_pubkey, }; - let signer = P2SHSigner::try_from_coin(&coin, args.swap_unique_data) - .map_err(|e| TransactionErr::Plain(ERRL!("Failed to create P2SHSigner: {}", e)))?; + let signer = try_tx_s!(P2SHSigner::try_from_coin(&coin, args.swap_unique_data)); let input = P2SHSpendingTxInput { prev_transaction, From ed6ffd176ee3f73fd2154b95204899bf4b2c49f5 Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Wed, 10 Sep 2025 14:12:50 +0300 Subject: [PATCH 03/10] put fixme lables and use P2SHSigner --- mm2src/coins/utxo/utxo_common.rs | 39 ++++++++++++++++++++++++++---- mm2src/coins/utxo/utxo_standard.rs | 19 ++++++++------- 2 files changed, 44 insertions(+), 14 deletions(-) diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index 025a6f0872..bc79d3bec1 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -916,7 +916,7 @@ pub enum P2SHSigner { } impl P2SHSigner { - fn try_from_coin(coin: &Coin, swap_unique_data: &[u8]) -> Result + pub fn try_from_coin(coin: &Coin, swap_unique_data: &[u8]) -> Result where Coin: UtxoCommonOps + SwapOps, { @@ -1111,11 +1111,19 @@ async fn gen_taker_funding_spend_preimage( .map_to_mm(TxGenError::Legacy) } +// fixme: funding spend = taker payment +// this func is run by the maker after the taker has sent the funding tx +// the maker generates and signs the preimage of the taker payment and send it to the taker pub async fn gen_and_sign_taker_funding_spend_preimage( coin: &T, args: &GenTakerFundingSpendArgs<'_, T>, - htlc_keypair: &KeyPair, + htlc_keypair: &P2SHSigner, ) -> GenPreimageResult { + let htlc_keypair = match htlc_keypair { + P2SHSigner::KeyPair(key_pair) => key_pair, + P2SHSigner::WalletConnect(_) => panic!("no walletconnect here"), + }; + let funding_time_lock = args .funding_time_lock .try_into() @@ -1231,13 +1239,20 @@ pub async fn validate_taker_funding_spend_preimage( Ok(()) } +// fixme: funding spend = taker payment +// this func is run by the taker after receiving the preimage from the maker and (confirming the maker payment; optional) +// the taker signs and finalizes the taker payment and broadcasts it. /// Common implementation of taker funding spend finalization and broadcast for UTXO coins. pub async fn sign_and_send_taker_funding_spend( coin: &T, preimage: &TxPreimageWithSig, gen_args: &GenTakerFundingSpendArgs<'_, T>, - htlc_keypair: &KeyPair, + htlc_keypair: &P2SHSigner, ) -> Result { + let htlc_keypair = match htlc_keypair { + P2SHSigner::KeyPair(key_pair) => key_pair, + P2SHSigner::WalletConnect(_) => panic!("no walletconnect here"), + }; let redeem_script = swap_proto_v2_scripts::taker_funding_script( try_tx_s!(gen_args.funding_time_lock.try_into()), gen_args.taker_secret_hash, @@ -1395,11 +1410,18 @@ async fn gen_taker_payment_spend_preimage( .map_to_mm(TxGenError::Legacy) } +// fixme: taker payment spend = preimage/swap-finalizing tx +// this func is run by the taker after confirming the taker payment (that this tx spends). +// the taker generates and signs the preimage of this tx and sends it to the maker. pub async fn gen_and_sign_taker_payment_spend_preimage( coin: &T, args: &GenTakerPaymentSpendArgs<'_, T>, - htlc_keypair: &KeyPair, + htlc_keypair: &P2SHSigner, ) -> GenPreimageResult { + let htlc_keypair = match htlc_keypair { + P2SHSigner::KeyPair(key_pair) => key_pair, + P2SHSigner::WalletConnect(_) => panic!("no walletconnect here"), + }; let time_lock = args .time_lock .try_into() @@ -1487,6 +1509,9 @@ pub async fn validate_taker_payment_spend_preimage( Ok(()) } +// fixme: taker payment spend = preimage/swap-finalizing tx +// this func is run by the maker after recieving the preimage of the finalizing tx from the taker +// the maker signs and broadcasts the finalizing tx. /// Common implementation of taker payment spend finalization and broadcast for UTXO coins. /// Appends maker output to the preimage, signs it with SIGHASH_ALL and submits the resulting tx to coin's RPC. pub async fn sign_and_broadcast_taker_payment_spend( @@ -1494,8 +1519,12 @@ pub async fn sign_and_broadcast_taker_payment_spend( preimage: &TxPreimageWithSig, gen_args: &GenTakerPaymentSpendArgs<'_, T>, secret: &[u8], - htlc_keypair: &KeyPair, + htlc_keypair: &P2SHSigner, ) -> Result { + let htlc_keypair = match htlc_keypair { + P2SHSigner::KeyPair(key_pair) => key_pair, + P2SHSigner::WalletConnect(_) => panic!("no walletconnect here"), + }; let secret_hash = dhash160(secret); let redeem_script = swap_proto_v2_scripts::taker_payment_script( try_tx_s!(gen_args.time_lock.try_into()), diff --git a/mm2src/coins/utxo/utxo_standard.rs b/mm2src/coins/utxo/utxo_standard.rs index a975220453..196a7dc0f4 100644 --- a/mm2src/coins/utxo/utxo_standard.rs +++ b/mm2src/coins/utxo/utxo_standard.rs @@ -26,6 +26,7 @@ use crate::rpc_command::init_withdraw::{InitWithdrawCoin, WithdrawTaskHandleShar use crate::tx_history_storage::{GetTxHistoryFilters, WalletId}; use crate::utxo::rpc_clients::BlockHashOrHeight; use crate::utxo::utxo_builder::{UtxoArcBuilder, UtxoCoinBuilder}; +use crate::utxo::utxo_common::P2SHSigner; use crate::utxo::utxo_hd_wallet::{UtxoHDAccount, UtxoHDAddress}; use crate::utxo::utxo_tx_history_v2::{ UtxoMyAddressesHistoryError, UtxoTxDetailsError, UtxoTxDetailsParams, UtxoTxHistoryOps, @@ -40,7 +41,7 @@ use crate::{ SearchForSwapTxSpendInput, SendMakerPaymentArgs, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SendTakerFundingArgs, SignRawTransactionRequest, SignatureResult, SpendMakerPaymentArgs, SpendPaymentArgs, SwapOps, SwapTxTypeWithSecretHash, TakerCoinSwapOpsV2, ToBytes, TradePreimageValue, TransactionFut, TransactionResult, - TxMarshalingErr, TxPreimageWithSig, ValidateAddressResult, ValidateFeeArgs, ValidateMakerPaymentArgs, + TxGenError, TxMarshalingErr, TxPreimageWithSig, ValidateAddressResult, ValidateFeeArgs, ValidateMakerPaymentArgs, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, ValidateSwapV2TxResult, ValidateTakerFundingArgs, ValidateTakerFundingSpendPreimageResult, ValidateTakerPaymentSpendPreimageResult, ValidateWatcherSpendInput, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, @@ -759,8 +760,8 @@ impl TakerCoinSwapOpsV2 for UtxoStandardCoin { args: &GenTakerFundingSpendArgs<'_, Self>, swap_unique_data: &[u8], ) -> GenPreimageResult { - let htlc_keypair = self.derive_htlc_key_pair(swap_unique_data); - utxo_common::gen_and_sign_taker_funding_spend_preimage(self, args, &htlc_keypair).await + let signer = P2SHSigner::try_from_coin(self, swap_unique_data).map_err(TxGenError::Signing)?; + utxo_common::gen_and_sign_taker_funding_spend_preimage(self, args, &signer).await } async fn validate_taker_funding_spend_preimage( @@ -777,8 +778,8 @@ impl TakerCoinSwapOpsV2 for UtxoStandardCoin { args: &GenTakerFundingSpendArgs<'_, Self>, swap_unique_data: &[u8], ) -> Result { - let htlc_keypair = self.derive_htlc_key_pair(swap_unique_data); - utxo_common::sign_and_send_taker_funding_spend(self, preimage, args, &htlc_keypair).await + let signer = try_tx_s!(P2SHSigner::try_from_coin(self, swap_unique_data)); + utxo_common::sign_and_send_taker_funding_spend(self, preimage, args, &signer).await } async fn refund_combined_taker_payment( @@ -802,8 +803,8 @@ impl TakerCoinSwapOpsV2 for UtxoStandardCoin { args: &GenTakerPaymentSpendArgs<'_, Self>, swap_unique_data: &[u8], ) -> GenPreimageResult { - let key_pair = self.derive_htlc_key_pair(swap_unique_data); - utxo_common::gen_and_sign_taker_payment_spend_preimage(self, args, &key_pair).await + let signer = P2SHSigner::try_from_coin(self, swap_unique_data).map_err(TxGenError::Signing)?; + utxo_common::gen_and_sign_taker_payment_spend_preimage(self, args, &signer).await } async fn validate_taker_payment_spend_preimage( @@ -823,8 +824,8 @@ impl TakerCoinSwapOpsV2 for UtxoStandardCoin { ) -> Result { let preimage = preimage .ok_or_else(|| TransactionErr::Plain(ERRL!("taker_payment_spend_preimage must be Some for UTXO coin")))?; - let htlc_keypair = self.derive_htlc_key_pair(swap_unique_data); - utxo_common::sign_and_broadcast_taker_payment_spend(self, preimage, gen_args, secret, &htlc_keypair).await + let signer = try_tx_s!(P2SHSigner::try_from_coin(self, swap_unique_data)); + utxo_common::sign_and_broadcast_taker_payment_spend(self, preimage, gen_args, secret, &signer).await } async fn find_taker_payment_spend_tx( From 552416455e3187db0309548e8ff4d341b7114065 Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Wed, 10 Sep 2025 15:11:35 +0300 Subject: [PATCH 04/10] return the bare p2sh signature from wallet connect and configure sighash type this makes it so we return the bare p2sh siganture (bare as in, no redeem or unlocking script were added to the signature. i.e. the signature is exactly the one walletconnect returned with no modifications). this also adds one extra configuration parameter to change the sighash type of the transaction. this is to be used later when signing with SIGHASH_SINGLE vs SIGHASH_ALL in the TPU. --- mm2src/coins/utxo/utxo_common.rs | 25 +++++++++++++++---------- mm2src/coins/utxo/wallet_connect.rs | 18 +++++++++++++----- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index bc79d3bec1..63a64b6b96 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -1040,16 +1040,21 @@ pub async fn p2sh_spending_tx(coin: &T, input: P2SHSpendingTxI )); Ok(complete_tx(unsigned, vec![signed_input])) }, - P2SHSigner::WalletConnect(session_topic) => wallet_connect::sign_p2sh( - coin, - &session_topic, - &unsigned, - input.prev_transaction, - input.redeem_script, - input.script_data.into(), - ) - .await - .map_err(|e| format!("WalletConnect P2SH signing error: {e}")), + P2SHSigner::WalletConnect(session_topic) => { + let (tx, _p2sh_sig) = wallet_connect::sign_p2sh( + coin, + &session_topic, + &unsigned, + input.prev_transaction, + input.redeem_script, + input.script_data.into(), + SIGHASH_ALL, + ) + .await + .map_err(|e| format!("WalletConnect P2SH signing error: {e}"))?; + + Ok(tx) + }, } } diff --git a/mm2src/coins/utxo/wallet_connect.rs b/mm2src/coins/utxo/wallet_connect.rs index 217d494962..c7d2c5e073 100644 --- a/mm2src/coins/utxo/wallet_connect.rs +++ b/mm2src/coins/utxo/wallet_connect.rs @@ -218,6 +218,8 @@ async fn sign_psbt( /// `prev_tx` is the previous transaction that contains the P2SH output being spent. /// `redeem_script` is the redeem script that is used to spend the P2SH output. /// `unlocking_script` is the unlocking script that picks the appropriate spending path (normal spend (with secret hash) vs refund) +/// +/// Returns the signed transaction and the bare P2SH signature returned by WalletConnect. #[expect(clippy::too_many_arguments)] pub async fn sign_p2sh_with_walletconnect( wc: &WalletConnectCtx, @@ -228,7 +230,8 @@ pub async fn sign_p2sh_with_walletconnect( prev_tx: UtxoTx, redeem_script: Bytes, unlocking_script: Bytes, -) -> MmResult { + sighash_type: EcdsaSighashType, +) -> MmResult<(UtxoTx, Bytes), WalletConnectError> { let signing_address = signing_address.display_address().map_to_mm(|e| { WalletConnectError::InternalError(format!("Failed to convert the signing address to a string: {e}")) })?; @@ -249,13 +252,13 @@ pub async fn sign_p2sh_with_walletconnect( // We need to provide the redeem script as it's used in the signing process. psbt.inputs[DEFAULT_SWAP_VIN].redeem_script = Some(redeem_script.take().into()); // TODO: Check whether we should put `fork_id` here or not. When we support a `fork_id`-based chain in WalletConnect. - psbt.inputs[DEFAULT_SWAP_VIN].sighash_type = Some(EcdsaSighashType::All.into()); + psbt.inputs[DEFAULT_SWAP_VIN].sighash_type = Some(sighash_type.into()); // Ask WalletConnect to sign the PSBT for us. let inputs = vec![InputSigningParams { index: DEFAULT_SWAP_VIN as u32, address: signing_address.clone(), - sighash_types: vec![EcdsaSighashType::All as u8], + sighash_types: vec![sighash_type as u8], }]; let signed_psbt = sign_psbt(wc, session_topic, chain_id, psbt, inputs, false).await?; @@ -284,7 +287,7 @@ pub async fn sign_p2sh_with_walletconnect( tx_to_sign.inputs[DEFAULT_SWAP_VIN].script_sig = final_script_sig; tx_to_sign.inputs[DEFAULT_SWAP_VIN].script_witness = vec![]; - Ok(tx_to_sign) + Ok((tx_to_sign, p2sh_signature)) } /// Signs a P2SH transaction that has a single input using WalletConnect. @@ -298,7 +301,8 @@ pub async fn sign_p2sh( prev_tx: UtxoTx, redeem_script: Bytes, unlocking_script: Bytes, -) -> MmResult { + sighash_type: u32, +) -> MmResult<(UtxoTx, Bytes), WalletConnectError> { let ctx = MmArc::from_weak(&coin.as_ref().ctx) .ok_or_else(|| WalletConnectError::InternalError("Couldn't get access to MmArc".to_string()))?; let wc_ctx = WalletConnectCtx::from_ctx(&ctx)?; @@ -316,6 +320,9 @@ pub async fn sign_p2sh( .as_ref() .ok_or_else(|| WalletConnectError::InternalError("Chain ID is not set".to_string()))?; + let sighash_type = EcdsaSighashType::from_standard(sighash_type) + .map_err(|e| WalletConnectError::InternalError(format!("Bad sighash type: {e}")))?; + sign_p2sh_with_walletconnect( &wc_ctx, session_topic, @@ -325,6 +332,7 @@ pub async fn sign_p2sh( prev_tx, redeem_script, unlocking_script, + sighash_type, ) .await } From 7be460f289ad0536c2058a14c524cf4116d9e61d Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Wed, 10 Sep 2025 17:29:21 +0300 Subject: [PATCH 05/10] refactor and edit the visibility of utxo::wallet_connect methods remove the unneeded `pub` for some methods (until they are needed in the future) and refactor sign_p2sh into two different functions. one function that returns a complete finalized transaction without the bare signature (used in swaps v1 since these are simple, non-cooperative p2sh spends) one function that returns the bare p2sh signature (i.e. no modifications to the signature what so ever). this function doesn't require an unlocking_script since that's only needed to finalize a p2sh transaction and we are not trying to finalize it here --- mm2src/coins/utxo/utxo_common.rs | 26 ++++++--------- mm2src/coins/utxo/wallet_connect.rs | 52 +++++++++++++++++++++++++++-- 2 files changed, 60 insertions(+), 18 deletions(-) diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index 63a64b6b96..80f04d8872 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -1040,21 +1040,17 @@ pub async fn p2sh_spending_tx(coin: &T, input: P2SHSpendingTxI )); Ok(complete_tx(unsigned, vec![signed_input])) }, - P2SHSigner::WalletConnect(session_topic) => { - let (tx, _p2sh_sig) = wallet_connect::sign_p2sh( - coin, - &session_topic, - &unsigned, - input.prev_transaction, - input.redeem_script, - input.script_data.into(), - SIGHASH_ALL, - ) - .await - .map_err(|e| format!("WalletConnect P2SH signing error: {e}"))?; - - Ok(tx) - }, + P2SHSigner::WalletConnect(session_topic) => wallet_connect::sign_p2sh( + coin, + &session_topic, + &unsigned, + input.prev_transaction, + input.redeem_script, + input.script_data.into(), + SIGHASH_ALL, + ) + .await + .map_err(|e| format!("WalletConnect P2SH signing error: {e}")), } } diff --git a/mm2src/coins/utxo/wallet_connect.rs b/mm2src/coins/utxo/wallet_connect.rs index c7d2c5e073..ddc6d064fc 100644 --- a/mm2src/coins/utxo/wallet_connect.rs +++ b/mm2src/coins/utxo/wallet_connect.rs @@ -221,7 +221,7 @@ async fn sign_psbt( /// /// Returns the signed transaction and the bare P2SH signature returned by WalletConnect. #[expect(clippy::too_many_arguments)] -pub async fn sign_p2sh_with_walletconnect( +async fn sign_p2sh_with_walletconnect( wc: &WalletConnectCtx, session_topic: &WcTopic, chain_id: &WcChainId, @@ -294,7 +294,7 @@ pub async fn sign_p2sh_with_walletconnect( /// /// This is just another wrapper around `sign_p2sh_with_walletconnect` to avoid some boilerplate given /// that there is an accessible `coin`. -pub async fn sign_p2sh( +async fn sign_p2sh_get_tx_and_sig( coin: &impl AsRef, session_topic: &WcTopic, tx_input_signer: &TransactionInputSigner, @@ -337,11 +337,57 @@ pub async fn sign_p2sh( .await } +/// Signs a P2SH transaction that has a single input using WalletConnect and returns the signed transaction. +pub async fn sign_p2sh( + coin: &impl AsRef, + session_topic: &WcTopic, + tx_input_signer: &TransactionInputSigner, + prev_tx: UtxoTx, + redeem_script: Bytes, + unlocking_script: Bytes, + sighash_type: u32, +) -> MmResult { + sign_p2sh_get_tx_and_sig( + coin, + session_topic, + tx_input_signer, + prev_tx, + redeem_script, + unlocking_script, + sighash_type, + ) + .await + .map(|(tx, _p2sh_sig)| tx) +} + +/// Signs a P2SH transaction that has a single input using WalletConnect and returns only the bare P2SH signature. +pub async fn sign_p2sh_get_sig_only( + coin: &impl AsRef, + session_topic: &WcTopic, + tx_input_signer: &TransactionInputSigner, + prev_tx: UtxoTx, + redeem_script: Bytes, + sighash_type: u32, +) -> MmResult { + sign_p2sh_get_tx_and_sig( + coin, + session_topic, + tx_input_signer, + prev_tx, + redeem_script, + // Since we will not use the resulting tx, we can put any dummy unlocking script here. + Bytes::new(), + sighash_type, + ) + .await + .map(|(_tx, p2sh_sig)| p2sh_sig) +} + /// Signs a P2PKH/P2WPKH spending transaction using WalletConnect. /// /// Contrary to what the function name might suggest, this function can sign both P2PKH and **P2WPKH** inputs. /// `prev_txs` is a map of previous transactions that contain the P2PKH inputs being spent. P2WPKH inputs don't need their previous transactions. -pub async fn sign_p2pkh_with_walletconnect( +async fn sign_p2pkh_with_walletconnect( wc: &WalletConnectCtx, session_topic: &WcTopic, chain_id: &WcChainId, From 8ecd7b9e5ea9c0f0f5ba21d94b9b7f00e1ff1a7f Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Wed, 10 Sep 2025 19:15:51 +0300 Subject: [PATCH 06/10] support walletconnect utxo signing for swaps v2 --- mm2src/coins/utxo/utxo_common.rs | 158 +++++++++++++++++++------------ 1 file changed, 98 insertions(+), 60 deletions(-) diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index 80f04d8872..9ad818d4b4 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -1118,13 +1118,8 @@ async fn gen_taker_funding_spend_preimage( pub async fn gen_and_sign_taker_funding_spend_preimage( coin: &T, args: &GenTakerFundingSpendArgs<'_, T>, - htlc_keypair: &P2SHSigner, + p2sh_signer: &P2SHSigner, ) -> GenPreimageResult { - let htlc_keypair = match htlc_keypair { - P2SHSigner::KeyPair(key_pair) => key_pair, - P2SHSigner::WalletConnect(_) => panic!("no walletconnect here"), - }; - let funding_time_lock = args .funding_time_lock .try_into() @@ -1139,16 +1134,30 @@ pub async fn gen_and_sign_taker_funding_spend_preimage( args.taker_pub, args.maker_pub, ); - let signature = calc_and_sign_sighash( - &preimage, - DEFAULT_SWAP_VOUT, - &redeem_script, - htlc_keypair, - coin.as_ref().conf.signature_version, - SIGHASH_ALL, - coin.as_ref().conf.fork_id, - ) - .map_mm_err()?; + + let signature = match p2sh_signer { + P2SHSigner::KeyPair(htlc_keypair) => calc_and_sign_sighash( + &preimage, + DEFAULT_SWAP_VOUT, + &redeem_script, + htlc_keypair, + coin.as_ref().conf.signature_version, + SIGHASH_ALL, + coin.as_ref().conf.fork_id, + ) + .map_mm_err()?, + P2SHSigner::WalletConnect(session_topic) => wallet_connect::sign_p2sh_get_sig_only( + coin, + session_topic, + &preimage, + args.funding_tx.clone(), + redeem_script.into(), + SIGHASH_ALL, + ) + .await + .mm_err(|e| TxGenError::Signing(format!("WalletConnect P2SH signing error: {e}")))?, + }; + Ok(TxPreimageWithSig { preimage: preimage.into(), signature: signature.take().into(), @@ -1248,12 +1257,8 @@ pub async fn sign_and_send_taker_funding_spend( coin: &T, preimage: &TxPreimageWithSig, gen_args: &GenTakerFundingSpendArgs<'_, T>, - htlc_keypair: &P2SHSigner, + p2sh_signer: &P2SHSigner, ) -> Result { - let htlc_keypair = match htlc_keypair { - P2SHSigner::KeyPair(key_pair) => key_pair, - P2SHSigner::WalletConnect(_) => panic!("no walletconnect here"), - }; let redeem_script = swap_proto_v2_scripts::taker_funding_script( try_tx_s!(gen_args.funding_time_lock.try_into()), gen_args.taker_secret_hash, @@ -1267,15 +1272,29 @@ pub async fn sign_and_send_taker_funding_spend( payment_input.amount = funding_output.value; signer.consensus_branch_id = coin.as_ref().conf.consensus_branch_id; - let taker_signature = try_tx_s!(calc_and_sign_sighash( - &signer, - DEFAULT_SWAP_VOUT, - &redeem_script, - htlc_keypair, - coin.as_ref().conf.signature_version, - SIGHASH_ALL, - coin.as_ref().conf.fork_id - )); + let taker_signature = match p2sh_signer { + P2SHSigner::KeyPair(htlc_keypair) => try_tx_s!(calc_and_sign_sighash( + &signer, + DEFAULT_SWAP_VOUT, + &redeem_script, + htlc_keypair, + coin.as_ref().conf.signature_version, + SIGHASH_ALL, + coin.as_ref().conf.fork_id + )), + P2SHSigner::WalletConnect(session_topic) => try_tx_s!( + wallet_connect::sign_p2sh_get_sig_only( + coin, + session_topic, + &signer, + gen_args.funding_tx.clone(), + redeem_script.clone().into(), + SIGHASH_ALL, + ) + .await + ), + }; + let sig_hash_all_fork_id = (SIGHASH_ALL | coin.as_ref().conf.fork_id) as u8; let mut maker_signature_with_sighash = preimage.signature.to_vec(); @@ -1417,12 +1436,8 @@ async fn gen_taker_payment_spend_preimage( pub async fn gen_and_sign_taker_payment_spend_preimage( coin: &T, args: &GenTakerPaymentSpendArgs<'_, T>, - htlc_keypair: &P2SHSigner, + p2sh_signer: &P2SHSigner, ) -> GenPreimageResult { - let htlc_keypair = match htlc_keypair { - P2SHSigner::KeyPair(key_pair) => key_pair, - P2SHSigner::WalletConnect(_) => panic!("no walletconnect here"), - }; let time_lock = args .time_lock .try_into() @@ -1438,16 +1453,29 @@ pub async fn gen_and_sign_taker_payment_spend_preimage SIGHASH_ALL, }; - let signature = calc_and_sign_sighash( - &preimage, - DEFAULT_SWAP_VOUT, - &redeem_script, - htlc_keypair, - coin.as_ref().conf.signature_version, - sig_hash_type, - coin.as_ref().conf.fork_id, - ) - .map_mm_err()?; + let signature = match p2sh_signer { + P2SHSigner::KeyPair(htlc_keypair) => calc_and_sign_sighash( + &preimage, + DEFAULT_SWAP_VOUT, + &redeem_script, + htlc_keypair, + coin.as_ref().conf.signature_version, + sig_hash_type, + coin.as_ref().conf.fork_id, + ) + .map_mm_err()?, + P2SHSigner::WalletConnect(session_topic) => wallet_connect::sign_p2sh_get_sig_only( + coin, + session_topic, + &preimage, + args.taker_tx.clone(), + redeem_script.into(), + sig_hash_type, + ) + .await + .mm_err(|e| TxGenError::Signing(format!("WalletConnect P2SH signing error: {e}")))?, + }; + Ok(TxPreimageWithSig { preimage: preimage.into(), signature: signature.take().into(), @@ -1520,18 +1548,14 @@ pub async fn sign_and_broadcast_taker_payment_spend( preimage: &TxPreimageWithSig, gen_args: &GenTakerPaymentSpendArgs<'_, T>, secret: &[u8], - htlc_keypair: &P2SHSigner, + p2sh_signer: &P2SHSigner, ) -> Result { - let htlc_keypair = match htlc_keypair { - P2SHSigner::KeyPair(key_pair) => key_pair, - P2SHSigner::WalletConnect(_) => panic!("no walletconnect here"), - }; let secret_hash = dhash160(secret); let redeem_script = swap_proto_v2_scripts::taker_payment_script( try_tx_s!(gen_args.time_lock.try_into()), secret_hash.as_slice(), gen_args.taker_pub, - htlc_keypair.public(), + gen_args.maker_pub, ); let mut signer: TransactionInputSigner = preimage.preimage.clone().into(); @@ -1563,15 +1587,29 @@ pub async fn sign_and_broadcast_taker_payment_spend( } drop_mutability!(signer); - let maker_signature = try_tx_s!(calc_and_sign_sighash( - &signer, - DEFAULT_SWAP_VOUT, - &redeem_script, - htlc_keypair, - coin.as_ref().conf.signature_version, - SIGHASH_ALL, - coin.as_ref().conf.fork_id - )); + let maker_signature = match p2sh_signer { + P2SHSigner::KeyPair(htlc_keypair) => try_tx_s!(calc_and_sign_sighash( + &signer, + DEFAULT_SWAP_VOUT, + &redeem_script, + htlc_keypair, + coin.as_ref().conf.signature_version, + SIGHASH_ALL, + coin.as_ref().conf.fork_id + )), + P2SHSigner::WalletConnect(session_topic) => try_tx_s!( + wallet_connect::sign_p2sh_get_sig_only( + coin, + session_topic, + &signer, + gen_args.taker_tx.clone(), + redeem_script.clone().into(), + SIGHASH_ALL, + ) + .await + ), + }; + let mut taker_signature_with_sighash = preimage.signature.to_vec(); let taker_sig_hash = match gen_args.dex_fee { DexFee::Standard(_) => (SIGHASH_SINGLE | coin.as_ref().conf.fork_id) as u8, From 2393ee7989dbe9d8c2ffbe008119973b8db96c50 Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Wed, 10 Sep 2025 20:37:21 +0300 Subject: [PATCH 07/10] fix walletconnect malformed sig was due to the wrap inside push_data and also that an extra sighash types byte was being added --- mm2src/coins/utxo/wallet_connect.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/mm2src/coins/utxo/wallet_connect.rs b/mm2src/coins/utxo/wallet_connect.rs index ddc6d064fc..dae9f5b008 100644 --- a/mm2src/coins/utxo/wallet_connect.rs +++ b/mm2src/coins/utxo/wallet_connect.rs @@ -287,7 +287,7 @@ async fn sign_p2sh_with_walletconnect( tx_to_sign.inputs[DEFAULT_SWAP_VIN].script_sig = final_script_sig; tx_to_sign.inputs[DEFAULT_SWAP_VIN].script_witness = vec![]; - Ok((tx_to_sign, p2sh_signature)) + Ok((tx_to_sign, Bytes::from(walletconnect_sig.to_vec()))) } /// Signs a P2SH transaction that has a single input using WalletConnect. @@ -380,7 +380,12 @@ pub async fn sign_p2sh_get_sig_only( sighash_type, ) .await - .map(|(_tx, p2sh_sig)| p2sh_sig) + .map(|(_tx, p2sh_sig)| { + let mut p2sh_sig = p2sh_sig.take(); + // Remove the sighash byte at the end so to align with the output of `calc_and_sign_sighash`. + p2sh_sig.pop(); + Bytes::from(p2sh_sig) + }) } /// Signs a P2PKH/P2WPKH spending transaction using WalletConnect. From 1ecae08b617c514b4c28d7ac59ccc832226116ad Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Wed, 10 Sep 2025 21:40:55 +0300 Subject: [PATCH 08/10] fix walletconnect not signing sighash_single the sighash_types should be sighashTypes for walletconnect to understand it also the doc comment was updated based on the returned error message by walletconnect. the returned error message state that sighashTypes field is a whitelist of possible sighash_types to allow in the psbt signing process. such thing is not mentioned explicitly in reown's docs (though i would say reown's docs is the vauge one here, esp since they take sighashTypes as an array). reown's docs about sighashTypes (weird because: 1- it's a list, 2- this field already exists in the psbt): sighashTypes : Integer[] - (Optional) Specifies which part(s) of the transaction the signature commits to. Default is [1]. walletconnect error message about missing sighashTypes (happens when you try to sign using SIGHASH_SINGLE while a missing sighashTypes defaults to [SIGHASH_ALL]): Sighash type is not allowed. Retry the sign method passing the sighashTypes array of whitelisted types. Sighash type: SIGHASH_SINGLE --- mm2src/coins/utxo/wallet_connect.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mm2src/coins/utxo/wallet_connect.rs b/mm2src/coins/utxo/wallet_connect.rs index dae9f5b008..cf9ecfb4f7 100644 --- a/mm2src/coins/utxo/wallet_connect.rs +++ b/mm2src/coins/utxo/wallet_connect.rs @@ -162,12 +162,13 @@ struct SignedPsbt { /// /// An **array** of this struct is sent to WalletConnect in `SignPsbt` request. #[derive(Serialize)] +#[serde(rename_all = "camelCase")] struct InputSigningParams { /// The index of the input to sign. index: u32, /// The address to sign the input with. address: String, - /// The sighash types to use for signing. + /// An array of whitelisted sighash types that the wallet is allowed to use when signing this input. sighash_types: Vec, } From 51ac7e00a20f12f0810d63e6ec2c78f9fceb2864 Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Wed, 10 Sep 2025 21:52:49 +0300 Subject: [PATCH 09/10] add a manual test for v2 swaps via walletconnect --- .../tests/mm2_tests/wallet_connect_tests.rs | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/mm2src/mm2_main/tests/mm2_tests/wallet_connect_tests.rs b/mm2src/mm2_main/tests/mm2_tests/wallet_connect_tests.rs index bafea5a608..c518f72e08 100644 --- a/mm2src/mm2_main/tests/mm2_tests/wallet_connect_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/wallet_connect_tests.rs @@ -10,7 +10,7 @@ use serde_json::json; #[cfg(not(target_arch = "wasm32"))] /// Perform a swap using WalletConnect protocol with two tBTC (testnet4) coins. -async fn perform_walletconnect_swap() { +async fn perform_walletconnect_swap(use_swaps_proto_v2: bool) { let walletconnect_namespaces = json!({ "required_namespaces": { "bip122": { @@ -66,7 +66,10 @@ async fn perform_walletconnect_swap() { let trading_pair = (coins[0]["coin"].as_str().unwrap(), coins[1]["coin"].as_str().unwrap()); let coins = json!(coins); - let bob_conf = Mm2TestConfForSwap::bob_conf_with_policy(&Mm2InitPrivKeyPolicy::GlobalHDAccount, &coins); + let mut bob_conf = Mm2TestConfForSwap::bob_conf_with_policy(&Mm2InitPrivKeyPolicy::GlobalHDAccount, &coins); + if use_swaps_proto_v2 { + bob_conf.conf["use_trading_proto_v2"] = true.into(); + } // Uncomment to test the refund case. The quickest way to test both refunds is to reject signing TakerPaymentSpend (the 4th signing prompt). // This will force the taker to refund himself and after sometime the maker will also refund himself because he can't spend the TakerPayment anymore (as it's already refunded). // Note that you need to run the test with `--features custom-swap-locktime` to enable the custom `payment_locktime` feature. @@ -79,11 +82,14 @@ async fn perform_walletconnect_swap() { log!("Bob log path: {}", mm_bob.log_path.display()); Timer::sleep(2.).await; - let alice_conf = Mm2TestConfForSwap::alice_conf_with_policy( + let mut alice_conf = Mm2TestConfForSwap::alice_conf_with_policy( &Mm2InitPrivKeyPolicy::GlobalHDAccount, &coins, &mm_bob.my_seed_addr(), ); + if use_swaps_proto_v2 { + alice_conf.conf["use_trading_proto_v2"] = true.into(); + } // Uncomment to test the refund case // alice_conf.conf["payment_locktime"] = (1 * 60).into(); let mut mm_alice = MarketMakerIt::start_async(alice_conf.conf, alice_conf.rpc_password, None) @@ -138,6 +144,12 @@ async fn perform_walletconnect_swap() { #[test] #[ignore] -fn test_walletconnect_swap() { - block_on(perform_walletconnect_swap()); +fn test_walletconnect_swap_v1() { + block_on(perform_walletconnect_swap(false)); +} + +#[test] +#[ignore] +fn test_walletconnect_swap_v2() { + block_on(perform_walletconnect_swap(true)); } From 45d8f17c8790a41d17ae2695a5e04771c06136f0 Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Wed, 10 Sep 2025 23:13:12 +0300 Subject: [PATCH 10/10] remove leftover fixmes --- mm2src/coins/utxo/utxo_common.rs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index 9ad818d4b4..bd41621eae 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -1112,9 +1112,6 @@ async fn gen_taker_funding_spend_preimage( .map_to_mm(TxGenError::Legacy) } -// fixme: funding spend = taker payment -// this func is run by the maker after the taker has sent the funding tx -// the maker generates and signs the preimage of the taker payment and send it to the taker pub async fn gen_and_sign_taker_funding_spend_preimage( coin: &T, args: &GenTakerFundingSpendArgs<'_, T>, @@ -1249,9 +1246,6 @@ pub async fn validate_taker_funding_spend_preimage( Ok(()) } -// fixme: funding spend = taker payment -// this func is run by the taker after receiving the preimage from the maker and (confirming the maker payment; optional) -// the taker signs and finalizes the taker payment and broadcasts it. /// Common implementation of taker funding spend finalization and broadcast for UTXO coins. pub async fn sign_and_send_taker_funding_spend( coin: &T, @@ -1430,9 +1424,6 @@ async fn gen_taker_payment_spend_preimage( .map_to_mm(TxGenError::Legacy) } -// fixme: taker payment spend = preimage/swap-finalizing tx -// this func is run by the taker after confirming the taker payment (that this tx spends). -// the taker generates and signs the preimage of this tx and sends it to the maker. pub async fn gen_and_sign_taker_payment_spend_preimage( coin: &T, args: &GenTakerPaymentSpendArgs<'_, T>, @@ -1538,9 +1529,6 @@ pub async fn validate_taker_payment_spend_preimage( Ok(()) } -// fixme: taker payment spend = preimage/swap-finalizing tx -// this func is run by the maker after recieving the preimage of the finalizing tx from the taker -// the maker signs and broadcasts the finalizing tx. /// Common implementation of taker payment spend finalization and broadcast for UTXO coins. /// Appends maker output to the preimage, signs it with SIGHASH_ALL and submits the resulting tx to coin's RPC. pub async fn sign_and_broadcast_taker_payment_spend(