From 8d0a58356a4806f88e695e5541ff870daffddba8 Mon Sep 17 00:00:00 2001 From: Artem Vitae Date: Mon, 30 Oct 2023 13:31:25 +0700 Subject: [PATCH] feat(trading-proto-upgrade): sql storage wip + protocol enhancement (#1980) This commit: - Removes access by index for UTXO transactions handling - Changes OP_CHECKMULTISIG to OP_CHECKSIGVERIFY OP_CHECKSIG - Stores upgraded swaps data to SQLite DB (still WIP) - Implements protocol enhancement for UTXO coins by adding one more funding tx for taker, which can be reclaimed immediately if maker back-outs. - Adds CoinAssocTypes trait representing coin associated types. #1895 --- mm2src/coins/lp_coins.rs | 230 +++-- mm2src/coins/test_coin.rs | 123 ++- mm2src/coins/utxo.rs | 44 +- mm2src/coins/utxo/swap_proto_v2_scripts.rs | 70 +- mm2src/coins/utxo/utxo_common.rs | 612 +++++++++--- mm2src/coins/utxo/utxo_standard.rs | 90 +- mm2src/mm2_bitcoin/chain/src/transaction.rs | 15 + mm2src/mm2_main/src/database.rs | 5 + mm2src/mm2_main/src/database/my_swaps.rs | 177 +++- mm2src/mm2_main/src/lp_ordermatch.rs | 153 +-- mm2src/mm2_main/src/lp_swap.rs | 69 +- .../src/lp_swap/komodefi.swap_v2.pb.rs | 32 +- mm2src/mm2_main/src/lp_swap/maker_swap.rs | 10 +- mm2src/mm2_main/src/lp_swap/maker_swap_v2.rs | 618 ++++++++---- mm2src/mm2_main/src/lp_swap/swap_v2.proto | 27 +- mm2src/mm2_main/src/lp_swap/taker_swap.rs | 4 +- mm2src/mm2_main/src/lp_swap/taker_swap_v2.rs | 887 +++++++++++++----- .../tests/docker_tests/swap_proto_v2_tests.rs | 202 ++-- .../tests/docker_tests/swap_watcher_tests.rs | 26 +- mm2src/mm2_number/src/mm_number.rs | 21 +- mm2src/mm2_state_machine/src/state_machine.rs | 2 +- 21 files changed, 2585 insertions(+), 832 deletions(-) diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index ac063c946c..5eb13daf6a 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -313,10 +313,12 @@ pub type RawTransactionResult = Result = Box> + Send + 'a>; pub type RefundResult = Result>; -/// Helper type used for taker payment's spend preimage generation result -pub type GenTakerPaymentSpendResult = MmResult; -/// Helper type used for taker payment's validation result -pub type ValidateTakerPaymentResult = MmResult<(), ValidateTakerPaymentError>; +/// Helper type used for swap transactions' spend preimage generation result +pub type GenPreimageResult = MmResult, TxGenError>; +/// Helper type used for taker funding's validation result +pub type ValidateTakerFundingResult = MmResult<(), ValidateTakerFundingError>; +/// Helper type used for taker funding's spend preimage validation result +pub type ValidateTakerFundingSpendPreimageResult = MmResult<(), ValidateTakerFundingSpendPreimageError>; /// Helper type used for taker payment's spend preimage validation result pub type ValidateTakerPaymentSpendPreimageResult = MmResult<(), ValidateTakerPaymentSpendPreimageError>; @@ -1081,14 +1083,14 @@ pub trait WatcherOps { ) -> Result, MmError>; } -/// Helper struct wrapping arguments for [SwapOpsV2::send_combined_taker_payment] -pub struct SendCombinedTakerPaymentArgs<'a> { +/// Helper struct wrapping arguments for [SwapOpsV2::send_taker_funding] +pub struct SendTakerFundingArgs<'a> { /// Taker will be able to refund the payment after this timestamp pub time_lock: u64, - /// The hash of the secret generated by maker - pub secret_hash: &'a [u8], + /// The hash of the secret generated by taker, this needs to be revealed for immediate refund + pub taker_secret_hash: &'a [u8], /// Maker's pubkey - pub other_pub: &'a [u8], + pub maker_pub: &'a [u8], /// DEX fee amount pub dex_fee_amount: BigDecimal, /// Additional reward for maker (premium) @@ -1099,16 +1101,46 @@ pub struct SendCombinedTakerPaymentArgs<'a> { pub swap_unique_data: &'a [u8], } -/// Helper struct wrapping arguments for [SwapOpsV2::validate_combined_taker_payment] -pub struct ValidateTakerPaymentArgs<'a> { +/// Helper struct wrapping arguments for [SwapOpsV2::refund_taker_funding_secret] +pub struct RefundFundingSecretArgs<'a, Coin: CoinAssocTypes + ?Sized> { + pub funding_tx: &'a Coin::Tx, + pub time_lock: u64, + pub maker_pubkey: &'a Coin::Pubkey, + pub taker_secret: &'a [u8], + pub taker_secret_hash: &'a [u8], + pub swap_contract_address: &'a Option, + pub swap_unique_data: &'a [u8], + pub watcher_reward: bool, +} + +/// Helper struct wrapping arguments for [SwapOpsV2::gen_taker_funding_spend_preimage] +pub struct GenTakerFundingSpendArgs<'a, Coin: CoinAssocTypes + ?Sized> { /// Taker payment transaction serialized to raw bytes - pub taker_tx: &'a [u8], + pub funding_tx: &'a Coin::Tx, + /// Maker's pubkey + pub maker_pub: &'a Coin::Pubkey, + /// Taker's pubkey + pub taker_pub: &'a Coin::Pubkey, + /// Timelock of the funding tx + pub funding_time_lock: u64, + /// The hash of the secret generated by taker + pub taker_secret_hash: &'a [u8], + /// Timelock of the taker payment + pub taker_payment_time_lock: u64, + /// The hash of the secret generated by maker + pub maker_secret_hash: &'a [u8], +} + +/// Helper struct wrapping arguments for [SwapOpsV2::validate_taker_funding] +pub struct ValidateTakerFundingArgs<'a, Coin: CoinAssocTypes + ?Sized> { + /// Taker funding transaction + pub funding_tx: &'a Coin::Tx, /// Taker will be able to refund the payment after this timestamp pub time_lock: u64, - /// The hash of the secret generated by maker - pub secret_hash: &'a [u8], + /// The hash of the secret generated by taker + pub taker_secret_hash: &'a [u8], /// Taker's pubkey - pub other_pub: &'a [u8], + pub other_pub: &'a Coin::Pubkey, /// DEX fee amount pub dex_fee_amount: BigDecimal, /// Additional reward for maker (premium) @@ -1122,17 +1154,17 @@ pub struct ValidateTakerPaymentArgs<'a> { /// Helper struct wrapping arguments for taker payment's spend generation, used in /// [SwapOpsV2::gen_taker_payment_spend_preimage], [SwapOpsV2::validate_taker_payment_spend_preimage] and /// [SwapOpsV2::sign_and_broadcast_taker_payment_spend] -pub struct GenTakerPaymentSpendArgs<'a> { +pub struct GenTakerPaymentSpendArgs<'a, Coin: CoinAssocTypes + ?Sized> { /// Taker payment transaction serialized to raw bytes - pub taker_tx: &'a [u8], + pub taker_tx: &'a Coin::Tx, /// Taker will be able to refund the payment after this timestamp pub time_lock: u64, /// The hash of the secret generated by maker pub secret_hash: &'a [u8], /// Maker's pubkey - pub maker_pub: &'a [u8], + pub maker_pub: &'a Coin::Pubkey, /// Taker's pubkey - pub taker_pub: &'a [u8], + pub taker_pub: &'a Coin::Pubkey, /// Pubkey of address, receiving DEX fees pub dex_fee_pub: &'a [u8], /// DEX fee amount @@ -1144,14 +1176,14 @@ pub struct GenTakerPaymentSpendArgs<'a> { } /// Taker payment spend preimage with taker's signature -pub struct TxPreimageWithSig { - /// The preimage tx serialized to raw bytes, might be empty for certain coin protocols - pub preimage: Vec, +pub struct TxPreimageWithSig { + /// The preimage, might be () for certain coin types (only signature might be used) + pub preimage: Coin::Preimage, /// Taker's signature - pub signature: Vec, + pub signature: Coin::Sig, } -/// Enum covering error cases that can happen during taker payment spend preimage generation. +/// Enum covering error cases that can happen during transaction preimage generation. #[derive(Debug, Display)] pub enum TxGenError { /// RPC error @@ -1160,16 +1192,16 @@ pub enum TxGenError { NumConversion(String), /// Address derivation error. AddressDerivation(String), - /// Error during transaction raw bytes deserialization. - TxDeserialization(String), - /// Error during pubkey deserialization. - InvalidPubkey(String), /// Problem with tx preimage signing. Signing(String), /// Legacy error produced by usage of try_s/try_fus and other similar macros. Legacy(String), /// Input payment timelock overflows the type used by specific coin. LocktimeOverflow(String), + /// Transaction fee is too high + TxFeeTooHigh(String), + /// Previous tx is not valid + PrevTxIsNotValid(String), } impl From for TxGenError { @@ -1184,48 +1216,76 @@ impl From for TxGenError { fn from(err: UtxoSignWithKeyPairError) -> Self { TxGenError::Signing(err.to_string()) } } -/// Enum covering error cases that can happen during taker payment validation. -#[derive(Debug)] -pub enum ValidateTakerPaymentError { +/// Enum covering error cases that can happen during taker funding validation. +#[derive(Debug, Display)] +pub enum ValidateTakerFundingError { /// Payment sent to wrong address or has invalid amount. InvalidDestinationOrAmount(String), - /// Error during pubkey deserialization. - InvalidPubkey(String), /// Error during conversion of BigDecimal amount to coin's specific monetary units (satoshis, wei, etc.). NumConversion(String), /// RPC error. Rpc(String), - /// Serialized tx bytes doesn't match ones received from coin's RPC. + /// Serialized tx bytes don't match ones received from coin's RPC. + #[display(fmt = "Tx bytes {:02x} don't match ones received from rpc {:02x}", actual, from_rpc)] TxBytesMismatch { from_rpc: BytesJson, actual: BytesJson }, - /// Error during transaction raw bytes deserialization. - TxDeserialization(String), /// Provided transaction doesn't have output with specific index TxLacksOfOutputs, /// Input payment timelock overflows the type used by specific coin. LocktimeOverflow(String), } -impl From for ValidateTakerPaymentError { - fn from(err: NumConversError) -> Self { ValidateTakerPaymentError::NumConversion(err.to_string()) } +impl From for ValidateTakerFundingError { + fn from(err: NumConversError) -> Self { ValidateTakerFundingError::NumConversion(err.to_string()) } +} + +impl From for ValidateTakerFundingError { + fn from(err: UtxoRpcError) -> Self { ValidateTakerFundingError::Rpc(err.to_string()) } +} + +/// Enum covering error cases that can happen during taker funding spend preimage validation. +#[derive(Debug, Display)] +pub enum ValidateTakerFundingSpendPreimageError { + /// Funding tx has no outputs + FundingTxNoOutputs, + /// Actual preimage fee is either too high or too small + UnexpectedPreimageFee(String), + /// Error during signature deserialization. + InvalidMakerSignature, + /// Error during preimage comparison to an expected one. + InvalidPreimage(String), + /// Error during taker's signature check. + SignatureVerificationFailure(String), + /// Error during generation of an expected preimage. + TxGenError(String), + /// Input payment timelock overflows the type used by specific coin. + LocktimeOverflow(String), + /// Coin's RPC error + Rpc(String), +} + +impl From for ValidateTakerFundingSpendPreimageError { + fn from(err: UtxoSignWithKeyPairError) -> Self { + ValidateTakerFundingSpendPreimageError::SignatureVerificationFailure(err.to_string()) + } +} + +impl From for ValidateTakerFundingSpendPreimageError { + fn from(err: TxGenError) -> Self { ValidateTakerFundingSpendPreimageError::TxGenError(format!("{:?}", err)) } } -impl From for ValidateTakerPaymentError { - fn from(err: UtxoRpcError) -> Self { ValidateTakerPaymentError::Rpc(err.to_string()) } +impl From for ValidateTakerFundingSpendPreimageError { + fn from(err: UtxoRpcError) -> Self { ValidateTakerFundingSpendPreimageError::Rpc(err.to_string()) } } /// Enum covering error cases that can happen during taker payment spend preimage validation. #[derive(Debug, Display)] pub enum ValidateTakerPaymentSpendPreimageError { - /// Error during pubkey deserialization. - InvalidPubkey(String), /// Error during signature deserialization. InvalidTakerSignature, /// Error during preimage comparison to an expected one. InvalidPreimage(String), /// Error during taker's signature check. SignatureVerificationFailure(String), - /// Error during preimage raw bytes deserialization. - TxDeserialization(String), /// Error during generation of an expected preimage. TxGenError(String), /// Input payment timelock overflows the type used by specific coin. @@ -1242,14 +1302,71 @@ impl From for ValidateTakerPaymentSpendPreimageError { fn from(err: TxGenError) -> Self { ValidateTakerPaymentSpendPreimageError::TxGenError(format!("{:?}", err)) } } +/// Helper trait used for various types serialization to bytes +pub trait ToBytes { + fn to_bytes(&self) -> Vec; +} + +/// Defines associated types specific to each coin (Pubkey, Address, etc.) +pub trait CoinAssocTypes { + type Pubkey: ToBytes + Send + Sync; + type PubkeyParseError: Send + std::fmt::Display; + type Tx: Transaction + Send + Sync; + type TxParseError: Send + std::fmt::Display; + type Preimage: ToBytes + Send + Sync; + type PreimageParseError: Send + std::fmt::Display; + type Sig: ToBytes + Send + Sync; + type SigParseError: Send + std::fmt::Display; + + fn parse_pubkey(&self, pubkey: &[u8]) -> Result; + + fn parse_tx(&self, tx: &[u8]) -> Result; + + fn parse_preimage(&self, tx: &[u8]) -> Result; + + fn parse_signature(&self, sig: &[u8]) -> Result; +} + /// Operations specific to the [Trading Protocol Upgrade implementation](https://github.com/KomodoPlatform/komodo-defi-framework/issues/1895) #[async_trait] -pub trait SwapOpsV2: Send + Sync + 'static { - /// Generate and broadcast taker payment transaction that includes dex fee, maker premium and actual trading volume. - async fn send_combined_taker_payment(&self, args: SendCombinedTakerPaymentArgs<'_>) -> TransactionResult; +pub trait SwapOpsV2: CoinAssocTypes + Send + Sync + 'static { + /// Generate and broadcast taker funding transaction that includes dex fee, maker premium and actual trading volume. + /// Funding tx can be reclaimed immediately if maker back-outs (doesn't send maker payment) + async fn send_taker_funding(&self, args: SendTakerFundingArgs<'_>) -> Result; + + /// Validates taker funding transaction. + async fn validate_taker_funding(&self, args: ValidateTakerFundingArgs<'_, Self>) -> ValidateTakerFundingResult; + + /// Refunds taker funding transaction using time-locked path without secret reveal. + async fn refund_taker_funding_timelock(&self, args: RefundPaymentArgs<'_>) -> TransactionResult; + + /// Reclaims taker funding transaction using immediate refund path with secret reveal. + async fn refund_taker_funding_secret( + &self, + args: RefundFundingSecretArgs<'_, Self>, + ) -> Result; + + /// Generates and signs a preimage spending funding tx to the combined taker payment + async fn gen_taker_funding_spend_preimage( + &self, + args: &GenTakerFundingSpendArgs<'_, Self>, + swap_unique_data: &[u8], + ) -> GenPreimageResult; + + /// Validates taker funding spend preimage generated and signed by maker + async fn validate_taker_funding_spend_preimage( + &self, + gen_args: &GenTakerFundingSpendArgs<'_, Self>, + preimage: &TxPreimageWithSig, + ) -> ValidateTakerFundingSpendPreimageResult; - /// Validates taker payment transaction. - async fn validate_combined_taker_payment(&self, args: ValidateTakerPaymentArgs<'_>) -> ValidateTakerPaymentResult; + /// Generates and signs a preimage spending funding tx to the combined taker payment + async fn sign_and_send_taker_funding_spend( + &self, + preimage: &TxPreimageWithSig, + args: &GenTakerFundingSpendArgs<'_, Self>, + swap_unique_data: &[u8], + ) -> Result; /// Refunds taker payment transaction. async fn refund_combined_taker_payment(&self, args: RefundPaymentArgs<'_>) -> TransactionResult; @@ -1258,25 +1375,28 @@ pub trait SwapOpsV2: Send + Sync + 'static { /// shared with maker to proceed with protocol execution. async fn gen_taker_payment_spend_preimage( &self, - args: &GenTakerPaymentSpendArgs<'_>, + args: &GenTakerPaymentSpendArgs<'_, Self>, swap_unique_data: &[u8], - ) -> GenTakerPaymentSpendResult; + ) -> GenPreimageResult; /// Validate taker payment spend preimage on maker's side. async fn validate_taker_payment_spend_preimage( &self, - gen_args: &GenTakerPaymentSpendArgs<'_>, - preimage: &TxPreimageWithSig, + gen_args: &GenTakerPaymentSpendArgs<'_, Self>, + preimage: &TxPreimageWithSig, ) -> ValidateTakerPaymentSpendPreimageResult; /// Sign and broadcast taker payment spend on maker's side. async fn sign_and_broadcast_taker_payment_spend( &self, - preimage: &TxPreimageWithSig, - gen_args: &GenTakerPaymentSpendArgs<'_>, + preimage: &TxPreimageWithSig, + gen_args: &GenTakerPaymentSpendArgs<'_, Self>, secret: &[u8], swap_unique_data: &[u8], ) -> TransactionResult; + + /// Derives an HTLC key-pair and returns a public key corresponding to that key. + fn derive_htlc_pubkey_v2(&self, swap_unique_data: &[u8]) -> Self::Pubkey; } /// Operations that coins have independently from the MarketMaker. diff --git a/mm2src/coins/test_coin.rs b/mm2src/coins/test_coin.rs index 3991d73e17..d21b438f58 100644 --- a/mm2src/coins/test_coin.rs +++ b/mm2src/coins/test_coin.rs @@ -2,20 +2,21 @@ use super::{CoinBalance, HistorySyncState, MarketCoinOps, MmCoin, RawTransactionFut, RawTransactionRequest, SwapOps, TradeFee, TransactionEnum, TransactionFut}; -use crate::ValidateWatcherSpendInput; -use crate::{coin_errors::MyAddressError, BalanceFut, CanRefundHtlc, CheckIfMyPaymentSentArgs, CoinFutSpawner, - ConfirmPaymentInput, FeeApproxStage, FoundSwapTxSpend, GenTakerPaymentSpendArgs, - GenTakerPaymentSpendResult, MakerSwapTakerCoin, MmCoinEnum, NegotiateSwapContractAddrErr, - PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, RefundPaymentArgs, RefundResult, - SearchForSwapTxSpendInput, SendCombinedTakerPaymentArgs, SendMakerPaymentSpendPreimageInput, - SendPaymentArgs, SignatureResult, SpendPaymentArgs, SwapOpsV2, TakerSwapMakerCoin, TradePreimageFut, - TradePreimageResult, TradePreimageValue, TransactionResult, TxMarshalingErr, TxPreimageWithSig, +use crate::{coin_errors::MyAddressError, BalanceFut, CanRefundHtlc, CheckIfMyPaymentSentArgs, CoinAssocTypes, + CoinFutSpawner, ConfirmPaymentInput, FeeApproxStage, FoundSwapTxSpend, GenPreimageResult, + GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, MakerSwapTakerCoin, MmCoinEnum, + NegotiateSwapContractAddrErr, PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, + RefundFundingSecretArgs, RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, + SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SendTakerFundingArgs, SignatureResult, + SpendPaymentArgs, SwapOpsV2, TakerSwapMakerCoin, TradePreimageFut, TradePreimageResult, + TradePreimageValue, Transaction, TransactionErr, TransactionResult, TxMarshalingErr, TxPreimageWithSig, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, - ValidateTakerPaymentArgs, ValidateTakerPaymentResult, ValidateTakerPaymentSpendPreimageResult, - VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, - WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawFut, - WithdrawRequest}; + ValidateTakerFundingArgs, ValidateTakerFundingResult, ValidateTakerFundingSpendPreimageResult, + ValidateTakerPaymentSpendPreimageResult, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, + WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, + WatcherValidateTakerFeeInput, WithdrawFut, WithdrawRequest}; +use crate::{ToBytes, ValidateWatcherSpendInput}; use async_trait::async_trait; use common::executor::AbortedError; use futures01::Future; @@ -387,42 +388,122 @@ impl MmCoin for TestCoin { fn on_token_deactivated(&self, _ticker: &str) { () } } +pub struct TestPubkey {} + +impl ToBytes for TestPubkey { + fn to_bytes(&self) -> Vec { vec![] } +} + +#[derive(Debug)] +pub struct TestTx {} + +impl Transaction for TestTx { + fn tx_hex(&self) -> Vec { todo!() } + + fn tx_hash(&self) -> BytesJson { todo!() } +} + +pub struct TestPreimage {} + +impl ToBytes for TestPreimage { + fn to_bytes(&self) -> Vec { vec![] } +} + +pub struct TestSig {} + +impl ToBytes for TestSig { + fn to_bytes(&self) -> Vec { vec![] } +} + +impl CoinAssocTypes for TestCoin { + type Pubkey = TestPubkey; + type PubkeyParseError = String; + type Tx = TestTx; + type TxParseError = String; + type Preimage = TestPreimage; + type PreimageParseError = String; + type Sig = TestSig; + type SigParseError = String; + + fn parse_pubkey(&self, pubkey: &[u8]) -> Result { unimplemented!() } + + fn parse_tx(&self, tx: &[u8]) -> Result { unimplemented!() } + + fn parse_preimage(&self, preimage: &[u8]) -> Result { todo!() } + + fn parse_signature(&self, sig: &[u8]) -> Result { todo!() } +} + #[async_trait] #[mockable] impl SwapOpsV2 for TestCoin { - async fn send_combined_taker_payment(&self, args: SendCombinedTakerPaymentArgs<'_>) -> TransactionResult { + async fn send_taker_funding(&self, args: SendTakerFundingArgs<'_>) -> Result { todo!() } + + async fn validate_taker_funding(&self, args: ValidateTakerFundingArgs<'_, Self>) -> ValidateTakerFundingResult { unimplemented!() } - async fn validate_combined_taker_payment(&self, args: ValidateTakerPaymentArgs<'_>) -> ValidateTakerPaymentResult { - unimplemented!() + async fn refund_taker_funding_timelock(&self, args: RefundPaymentArgs<'_>) -> TransactionResult { todo!() } + + async fn refund_taker_funding_secret( + &self, + args: RefundFundingSecretArgs<'_, Self>, + ) -> Result { + todo!() + } + + async fn gen_taker_funding_spend_preimage( + &self, + args: &GenTakerFundingSpendArgs<'_, Self>, + swap_unique_data: &[u8], + ) -> GenPreimageResult { + todo!() + } + + async fn validate_taker_funding_spend_preimage( + &self, + gen_args: &GenTakerFundingSpendArgs<'_, Self>, + preimage: &TxPreimageWithSig, + ) -> ValidateTakerFundingSpendPreimageResult { + todo!() + } + + async fn sign_and_send_taker_funding_spend( + &self, + preimage: &TxPreimageWithSig, + args: &GenTakerFundingSpendArgs<'_, Self>, + swap_unique_data: &[u8], + ) -> Result { + todo!() } async fn refund_combined_taker_payment(&self, args: RefundPaymentArgs<'_>) -> TransactionResult { unimplemented!() } async fn gen_taker_payment_spend_preimage( &self, - args: &GenTakerPaymentSpendArgs<'_>, + args: &GenTakerPaymentSpendArgs<'_, Self>, swap_unique_data: &[u8], - ) -> GenTakerPaymentSpendResult { + ) -> GenPreimageResult { unimplemented!() } async fn validate_taker_payment_spend_preimage( &self, - gen_args: &GenTakerPaymentSpendArgs<'_>, - preimage: &TxPreimageWithSig, + gen_args: &GenTakerPaymentSpendArgs<'_, Self>, + preimage: &TxPreimageWithSig, ) -> ValidateTakerPaymentSpendPreimageResult { unimplemented!() } async fn sign_and_broadcast_taker_payment_spend( &self, - preimage: &TxPreimageWithSig, - gen_args: &GenTakerPaymentSpendArgs<'_>, + preimage: &TxPreimageWithSig, + gen_args: &GenTakerPaymentSpendArgs<'_, Self>, secret: &[u8], swap_unique_data: &[u8], ) -> TransactionResult { unimplemented!() } + + fn derive_htlc_pubkey_v2(&self, swap_unique_data: &[u8]) -> Self::Pubkey { todo!() } } diff --git a/mm2src/coins/utxo.rs b/mm2src/coins/utxo.rs index ab1fee5bde..19a9d9cd7d 100644 --- a/mm2src/coins/utxo.rs +++ b/mm2src/coins/utxo.rs @@ -60,6 +60,7 @@ use futures::compat::Future01CompatExt; use futures::lock::{Mutex as AsyncMutex, MutexGuard as AsyncMutexGuard}; use futures01::Future; use keys::bytes::Bytes; +use keys::Signature; pub use keys::{Address, AddressFormat as UtxoAddressFormat, AddressHashEnum, KeyPair, Private, Public, Secret, Type as ScriptType}; #[cfg(not(target_arch = "wasm32"))] @@ -74,8 +75,9 @@ use num_traits::ToPrimitive; use primitives::hash::{H160, H256, H264}; use rpc::v1::types::{Bytes as BytesJson, Transaction as RpcTransaction, H256 as H256Json}; use script::{Builder, Script, SignatureVersion, TransactionInputSigner}; +use secp256k1::Signature as SecpSignature; use serde_json::{self as json, Value as Json}; -use serialization::{serialize, serialize_with_flags, Error as SerError, SERIALIZE_TRANSACTION_WITNESS}; +use serialization::{deserialize, serialize, serialize_with_flags, Error as SerError, SERIALIZE_TRANSACTION_WITNESS}; use spv_validation::conf::SPVConf; use spv_validation::helpers_validation::SPVError; use spv_validation::storage::BlockHeaderStorageError; @@ -110,6 +112,7 @@ use crate::hd_wallet::{HDAccountOps, HDAccountsMutex, HDAddress, HDAddressId, HD InvalidBip44ChainError}; use crate::hd_wallet_storage::{HDAccountStorageItem, HDWalletCoinStorage, HDWalletStorageError, HDWalletStorageResult}; use crate::utxo::tx_cache::UtxoVerboseCacheShared; +use crate::{CoinAssocTypes, ToBytes}; pub mod tx_cache; @@ -1011,6 +1014,45 @@ pub trait UtxoCommonOps: } } +impl ToBytes for UtxoTx { + fn to_bytes(&self) -> Vec { self.tx_hex() } +} + +impl ToBytes for Signature { + fn to_bytes(&self) -> Vec { self.to_vec() } +} + +impl CoinAssocTypes for T { + type Pubkey = Public; + type PubkeyParseError = MmError; + type Tx = UtxoTx; + type TxParseError = MmError; + type Preimage = UtxoTx; + type PreimageParseError = MmError; + type Sig = Signature; + type SigParseError = MmError; + + #[inline] + fn parse_pubkey(&self, pubkey: &[u8]) -> Result { + Ok(Public::from_slice(pubkey)?) + } + + #[inline] + fn parse_tx(&self, tx: &[u8]) -> Result { + let mut tx: UtxoTx = deserialize(tx)?; + tx.tx_hash_algo = self.as_ref().tx_hash_algo; + Ok(tx) + } + + #[inline] + fn parse_preimage(&self, tx: &[u8]) -> Result { self.parse_tx(tx) } + + fn parse_signature(&self, sig: &[u8]) -> Result { + SecpSignature::from_der(sig)?; + Ok(sig.into()) + } +} + #[async_trait] #[cfg_attr(test, mockable)] pub trait GetUtxoListOps { diff --git a/mm2src/coins/utxo/swap_proto_v2_scripts.rs b/mm2src/coins/utxo/swap_proto_v2_scripts.rs index 153f0bc4bb..f0b5231e04 100644 --- a/mm2src/coins/utxo/swap_proto_v2_scripts.rs +++ b/mm2src/coins/utxo/swap_proto_v2_scripts.rs @@ -4,37 +4,79 @@ use bitcrypto::ripemd160; use keys::Public; use script::{Builder, Opcode, Script}; -/// Builds a script for refundable dex_fee + premium taker transaction -pub fn taker_payment_script(time_lock: u32, secret_hash: &[u8], pub_0: &Public, pub_1: &Public) -> Script { +/// Builds a script for taker funding transaction +pub fn taker_funding_script( + time_lock: u32, + taker_secret_hash: &[u8], + taker_pub: &Public, + maker_pub: &Public, +) -> Script { let mut builder = Builder::default() - // Dex fee refund path, same lock time as for taker payment .push_opcode(Opcode::OP_IF) .push_bytes(&time_lock.to_le_bytes()) .push_opcode(Opcode::OP_CHECKLOCKTIMEVERIFY) .push_opcode(Opcode::OP_DROP) - .push_bytes(pub_0) + .push_bytes(taker_pub) + .push_opcode(Opcode::OP_CHECKSIG) + .push_opcode(Opcode::OP_ELSE) + .push_opcode(Opcode::OP_IF) + .push_bytes(taker_pub) + .push_opcode(Opcode::OP_CHECKSIGVERIFY) + .push_bytes(maker_pub) .push_opcode(Opcode::OP_CHECKSIG) - // Dex fee redeem path, Maker needs to reveal the secret to prevent case of getting - // the premium but not proceeding with spending the taker payment .push_opcode(Opcode::OP_ELSE) .push_opcode(Opcode::OP_SIZE) .push_bytes(&[32]) .push_opcode(Opcode::OP_EQUALVERIFY) .push_opcode(Opcode::OP_HASH160); - if secret_hash.len() == 32 { - builder = builder.push_bytes(ripemd160(secret_hash).as_slice()); + if taker_secret_hash.len() == 32 { + builder = builder.push_bytes(ripemd160(taker_secret_hash).as_slice()); } else { - builder = builder.push_bytes(secret_hash); + builder = builder.push_bytes(taker_secret_hash); } builder .push_opcode(Opcode::OP_EQUALVERIFY) - .push_opcode(Opcode::OP_2) - .push_bytes(pub_0) - .push_bytes(pub_1) - .push_opcode(Opcode::OP_2) - .push_opcode(Opcode::OP_CHECKMULTISIG) + .push_bytes(taker_pub) + .push_opcode(Opcode::OP_CHECKSIG) + .push_opcode(Opcode::OP_ENDIF) + .push_opcode(Opcode::OP_ENDIF) + .into_script() +} + +/// Builds a script for combined trading_volume + dex_fee + premium taker transaction +pub fn taker_payment_script( + time_lock: u32, + maker_secret_hash: &[u8], + taker_pub: &Public, + maker_pub: &Public, +) -> Script { + let mut builder = Builder::default() + .push_opcode(Opcode::OP_IF) + .push_bytes(&time_lock.to_le_bytes()) + .push_opcode(Opcode::OP_CHECKLOCKTIMEVERIFY) + .push_opcode(Opcode::OP_DROP) + .push_bytes(taker_pub) + .push_opcode(Opcode::OP_CHECKSIG) + .push_opcode(Opcode::OP_ELSE) + .push_opcode(Opcode::OP_SIZE) + .push_bytes(&[32]) + .push_opcode(Opcode::OP_EQUALVERIFY) + .push_opcode(Opcode::OP_HASH160); + + if maker_secret_hash.len() == 32 { + builder = builder.push_bytes(ripemd160(maker_secret_hash).as_slice()); + } else { + builder = builder.push_bytes(maker_secret_hash); + } + + builder + .push_opcode(Opcode::OP_EQUALVERIFY) + .push_bytes(taker_pub) + .push_opcode(Opcode::OP_CHECKSIGVERIFY) + .push_bytes(maker_pub) + .push_opcode(Opcode::OP_CHECKSIG) .push_opcode(Opcode::OP_ENDIF) .into_script() } diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index 9f657d02d2..82e33e4b36 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -15,18 +15,20 @@ use crate::utxo::spv::SimplePaymentVerification; use crate::utxo::tx_cache::TxCacheResult; use crate::utxo::utxo_withdraw::{InitUtxoWithdraw, StandardUtxoWithdraw, UtxoWithdraw}; use crate::watcher_common::validate_watcher_reward; -use crate::{CanRefundHtlc, CoinBalance, CoinWithDerivationMethod, ConfirmPaymentInput, GenTakerPaymentSpendArgs, - GenTakerPaymentSpendResult, GetWithdrawSenderAddress, HDAccountAddressId, RawTransactionError, - RawTransactionRequest, RawTransactionRes, RefundPaymentArgs, RewardTarget, SearchForSwapTxSpendInput, - SendCombinedTakerPaymentArgs, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignatureError, - SignatureResult, SpendPaymentArgs, SwapOps, TradePreimageValue, TransactionFut, TransactionResult, - TxFeeDetails, TxGenError, TxMarshalingErr, TxPreimageWithSig, ValidateAddressResult, - ValidateOtherPubKeyErr, ValidatePaymentFut, ValidatePaymentInput, ValidateTakerPaymentArgs, - ValidateTakerPaymentError, ValidateTakerPaymentResult, ValidateTakerPaymentSpendPreimageError, - ValidateTakerPaymentSpendPreimageResult, ValidateWatcherSpendInput, VerificationError, VerificationResult, - WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawFrom, - WithdrawResult, WithdrawSenderAddress, EARLY_CONFIRMATION_ERR_LOG, INVALID_RECEIVER_ERR_LOG, - INVALID_REFUND_TX_ERR_LOG, INVALID_SCRIPT_ERR_LOG, INVALID_SENDER_ERR_LOG, OLD_TRANSACTION_ERR_LOG}; +use crate::{CanRefundHtlc, CoinBalance, CoinWithDerivationMethod, ConfirmPaymentInput, GenPreimageResult, + GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, GetWithdrawSenderAddress, HDAccountAddressId, + RawTransactionError, RawTransactionRequest, RawTransactionRes, RefundFundingSecretArgs, RefundPaymentArgs, + RewardTarget, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, + SendTakerFundingArgs, SignatureError, SignatureResult, SpendPaymentArgs, SwapOps, TradePreimageValue, + TransactionFut, TransactionResult, TxFeeDetails, TxGenError, TxMarshalingErr, TxPreimageWithSig, + ValidateAddressResult, ValidateOtherPubKeyErr, ValidatePaymentFut, ValidatePaymentInput, + ValidateTakerFundingArgs, ValidateTakerFundingError, ValidateTakerFundingResult, + ValidateTakerFundingSpendPreimageError, ValidateTakerFundingSpendPreimageResult, + ValidateTakerPaymentSpendPreimageError, ValidateTakerPaymentSpendPreimageResult, + ValidateWatcherSpendInput, VerificationError, VerificationResult, WatcherSearchForSwapTxSpendInput, + WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawFrom, WithdrawResult, + WithdrawSenderAddress, EARLY_CONFIRMATION_ERR_LOG, INVALID_RECEIVER_ERR_LOG, INVALID_REFUND_TX_ERR_LOG, + INVALID_SCRIPT_ERR_LOG, INVALID_SENDER_ERR_LOG, OLD_TRANSACTION_ERR_LOG}; use crate::{MmCoinEnum, WatcherReward, WatcherRewardError}; pub use bitcrypto::{dhash160, sha256, ChecksumType}; use bitcrypto::{dhash256, ripemd160}; @@ -50,7 +52,7 @@ use mm2_number::{BigDecimal, MmNumber}; use primitives::hash::H512; use rpc::v1::types::{Bytes as BytesJson, ToTxHash, TransactionInputEnum, H256 as H256Json}; use script::{Builder, Opcode, Script, ScriptAddress, TransactionInputSigner, UnsignedTransactionInput}; -use secp256k1::{PublicKey, Signature}; +use secp256k1::{PublicKey, Signature as SecpSignature}; use serde_json::{self as json}; use serialization::{deserialize, serialize, serialize_with_flags, CoinVariant, CompactInteger, Serializable, Stream, SERIALIZE_TRANSACTION_WITNESS}; @@ -1114,22 +1116,34 @@ enum LocktimeSetting { UseExact(u32), } +enum NTimeSetting { + UseNow, + UseValue(Option), +} + +enum FundingSpendFeeSetting { + GetFromCoin, + UseExact(u64), +} + async fn p2sh_spending_tx_preimage( coin: &T, prev_tx: &UtxoTx, lock_time: LocktimeSetting, + set_n_time: NTimeSetting, sequence: u32, outputs: Vec, ) -> Result { - if prev_tx.outputs.is_empty() { - return ERR!("Previous transaction doesn't have any output"); - } + let amount = try_s!(prev_tx.first_output()).value; let lock_time = match lock_time { LocktimeSetting::CalcByHtlcLocktime(lock) => try_s!(coin.p2sh_tx_locktime(lock).await), LocktimeSetting::UseExact(lock) => lock, }; let n_time = if coin.as_ref().conf.is_pos { - Some(now_sec_u32()) + match set_n_time { + NTimeSetting::UseNow => Some(now_sec_u32()), + NTimeSetting::UseValue(value) => value, + } } else { None }; @@ -1150,7 +1164,7 @@ async fn p2sh_spending_tx_preimage( hash: prev_tx.hash(), index: DEFAULT_SWAP_VOUT as u32, }, - amount: prev_tx.outputs[0].value, + amount, witness: Vec::new(), }], outputs, @@ -1174,6 +1188,7 @@ pub async fn p2sh_spending_tx(coin: &T, input: P2SHSpendingTxI coin, &input.prev_transaction, LocktimeSetting::CalcByHtlcLocktime(input.lock_time), + NTimeSetting::UseNow, input.sequence, input.outputs ) @@ -1211,17 +1226,267 @@ pub async fn p2sh_spending_tx(coin: &T, input: P2SHSpendingTxI }) } -pub type GenDexFeeSpendResult = MmResult; +type GenPreimageResInner = MmResult; -async fn gen_taker_payment_spend_preimage( +async fn gen_taker_funding_spend_preimage( coin: &T, - args: &GenTakerPaymentSpendArgs<'_>, - lock_time: LocktimeSetting, -) -> GenDexFeeSpendResult { - let mut prev_tx: UtxoTx = deserialize(args.taker_tx).map_to_mm(|e| TxGenError::TxDeserialization(e.to_string()))?; - prev_tx.tx_hash_algo = coin.as_ref().tx_hash_algo; - drop_mutability!(prev_tx); + args: &GenTakerFundingSpendArgs<'_, T>, + n_time: NTimeSetting, + fee: FundingSpendFeeSetting, +) -> GenPreimageResInner { + let payment_time_lock = args + .taker_payment_time_lock + .try_into() + .map_to_mm(|e: TryFromIntError| TxGenError::LocktimeOverflow(e.to_string()))?; + + let payment_redeem_script = swap_proto_v2_scripts::taker_payment_script( + payment_time_lock, + args.maker_secret_hash, + args.taker_pub, + args.maker_pub, + ); + + let funding_amount = args + .funding_tx + .first_output() + .map_to_mm(|_| TxGenError::PrevTxIsNotValid("Funding tx has no outputs".into()))? + .value; + + let fee = match fee { + FundingSpendFeeSetting::GetFromCoin => { + coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WithoutApprox) + .await? + }, + FundingSpendFeeSetting::UseExact(f) => f, + }; + + let fee_plus_dust = fee + coin.as_ref().dust_amount; + if funding_amount < fee_plus_dust { + return MmError::err(TxGenError::TxFeeTooHigh(format!( + "Fee + dust {} is larger than funding amount {}", + fee_plus_dust, funding_amount + ))); + } + + let payment_output = TransactionOutput { + value: funding_amount - fee, + script_pubkey: Builder::build_p2sh(&AddressHashEnum::AddressHash(dhash160(&payment_redeem_script))).to_bytes(), + }; + + p2sh_spending_tx_preimage( + coin, + args.funding_tx, + LocktimeSetting::UseExact(0), + n_time, + SEQUENCE_FINAL, + vec![payment_output], + ) + .await + .map_to_mm(TxGenError::Legacy) +} + +pub async fn gen_and_sign_taker_funding_spend_preimage( + coin: &T, + args: &GenTakerFundingSpendArgs<'_, T>, + htlc_keypair: &KeyPair, +) -> GenPreimageResult { + let funding_time_lock = args + .funding_time_lock + .try_into() + .map_to_mm(|e: TryFromIntError| TxGenError::LocktimeOverflow(e.to_string()))?; + + let preimage = + gen_taker_funding_spend_preimage(coin, args, NTimeSetting::UseNow, FundingSpendFeeSetting::GetFromCoin).await?; + + let redeem_script = swap_proto_v2_scripts::taker_funding_script( + funding_time_lock, + args.taker_secret_hash, + 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, + )?; + Ok(TxPreimageWithSig { + preimage: preimage.into(), + signature: signature.take().into(), + }) +} + +/// Common implementation of taker funding spend preimage validation for UTXO coins. +/// Checks maker's signature and compares received preimage with the expected tx. +pub async fn validate_taker_funding_spend_preimage( + coin: &T, + gen_args: &GenTakerFundingSpendArgs<'_, T>, + preimage: &TxPreimageWithSig, +) -> ValidateTakerFundingSpendPreimageResult { + let funding_amount = gen_args + .funding_tx + .first_output() + .map_to_mm(|_| ValidateTakerFundingSpendPreimageError::FundingTxNoOutputs)? + .value; + + let payment_amount = preimage + .preimage + .first_output() + .map_to_mm(|_| ValidateTakerFundingSpendPreimageError::InvalidPreimage("Preimage has no outputs".into()))? + .value; + + if payment_amount > funding_amount { + return MmError::err(ValidateTakerFundingSpendPreimageError::InvalidPreimage(format!( + "Preimage output {} larger than funding input {}", + payment_amount, funding_amount + ))); + } + + let expected_fee = coin + .get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WithoutApprox) + .await?; + + let actual_fee = funding_amount - payment_amount; + + let fee_div = expected_fee as f64 / actual_fee as f64; + + if !(0.9..=1.1).contains(&fee_div) { + return MmError::err(ValidateTakerFundingSpendPreimageError::UnexpectedPreimageFee(format!( + "Too large difference between expected {} and actual {} fees", + expected_fee, actual_fee + ))); + } + + let expected_preimage = gen_taker_funding_spend_preimage( + coin, + gen_args, + NTimeSetting::UseValue(preimage.preimage.n_time), + FundingSpendFeeSetting::UseExact(actual_fee), + ) + .await?; + + let funding_time_lock = gen_args + .funding_time_lock + .try_into() + .map_to_mm(|e: TryFromIntError| ValidateTakerFundingSpendPreimageError::LocktimeOverflow(e.to_string()))?; + let redeem_script = swap_proto_v2_scripts::taker_funding_script( + funding_time_lock, + gen_args.taker_secret_hash, + gen_args.taker_pub, + gen_args.maker_pub, + ); + let sig_hash = signature_hash_to_sign( + &expected_preimage, + DEFAULT_SWAP_VOUT, + &redeem_script, + coin.as_ref().conf.signature_version, + SIGHASH_ALL, + coin.as_ref().conf.fork_id, + )?; + + if !gen_args + .maker_pub + .verify(&sig_hash, &preimage.signature) + .map_to_mm(|e| ValidateTakerFundingSpendPreimageError::SignatureVerificationFailure(e.to_string()))? + { + return MmError::err(ValidateTakerFundingSpendPreimageError::InvalidMakerSignature); + }; + let expected_preimage_tx: UtxoTx = expected_preimage.into(); + if expected_preimage_tx != preimage.preimage { + return MmError::err(ValidateTakerFundingSpendPreimageError::InvalidPreimage( + "Preimage is not equal to expected".into(), + )); + } + Ok(()) +} + +/// 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, +) -> Result { + 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, + gen_args.taker_pub, + gen_args.maker_pub, + ); + + let mut signer: TransactionInputSigner = preimage.preimage.clone().into(); + let payment_input = try_tx_s!(signer.inputs.first_mut().ok_or("Preimage doesn't have inputs")); + let funding_output = try_tx_s!(gen_args.funding_tx.first_output()); + 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 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(); + maker_signature_with_sighash.push(sig_hash_all_fork_id); + drop_mutability!(maker_signature_with_sighash); + + let mut taker_signature_with_sighash: Vec = taker_signature.take(); + taker_signature_with_sighash.push(sig_hash_all_fork_id); + drop_mutability!(taker_signature_with_sighash); + + let script_sig = Builder::default() + .push_data(&maker_signature_with_sighash) + .push_data(&taker_signature_with_sighash) + .push_opcode(Opcode::OP_1) + .push_opcode(Opcode::OP_0) + .push_data(&redeem_script) + .into_bytes(); + let mut final_tx: UtxoTx = signer.into(); + let final_tx_input = try_tx_s!(final_tx.inputs.first_mut().ok_or("Final tx doesn't have inputs")); + final_tx_input.script_sig = script_sig; + drop_mutability!(final_tx); + if let UtxoRpcClientEnum::Native(client) = &coin.as_ref().rpc_client { + let payment_redeem_script = swap_proto_v2_scripts::taker_payment_script( + try_tx_s!(gen_args.taker_payment_time_lock.try_into()), + gen_args.maker_secret_hash, + gen_args.taker_pub, + gen_args.maker_pub, + ); + let payment_address = Address { + checksum_type: coin.as_ref().conf.checksum_type, + hash: AddressHashEnum::AddressHash(dhash160(&payment_redeem_script)), + prefix: coin.as_ref().conf.p2sh_addr_prefix, + t_addr_prefix: coin.as_ref().conf.p2sh_t_addr_prefix, + hrp: coin.as_ref().conf.bech32_hrp.clone(), + addr_format: UtxoAddressFormat::Standard, + }; + let payment_address_str = payment_address.to_string(); + try_tx_s!( + client + .import_address(&payment_address_str, &payment_address_str, false) + .compat() + .await + ); + } + + try_tx_s!(coin.broadcast_tx(&final_tx).await, final_tx); + Ok(final_tx) +} + +async fn gen_taker_payment_spend_preimage( + coin: &T, + args: &GenTakerPaymentSpendArgs<'_, T>, + n_time: NTimeSetting, +) -> GenPreimageResInner { let dex_fee_sat = sat_from_big_decimal(&args.dex_fee_amount, coin.as_ref().decimals)?; let dex_fee_address = address_from_raw_pubkey( @@ -1238,27 +1503,32 @@ async fn gen_taker_payment_spend_preimage( script_pubkey: Builder::build_p2pkh(&dex_fee_address.hash).to_bytes(), }; - p2sh_spending_tx_preimage(coin, &prev_tx, lock_time, SEQUENCE_FINAL, vec![dex_fee_output]) - .await - .map_to_mm(TxGenError::Legacy) + p2sh_spending_tx_preimage( + coin, + args.taker_tx, + LocktimeSetting::UseExact(0), + n_time, + SEQUENCE_FINAL, + vec![dex_fee_output], + ) + .await + .map_to_mm(TxGenError::Legacy) } pub async fn gen_and_sign_taker_payment_spend_preimage( coin: &T, - args: &GenTakerPaymentSpendArgs<'_>, + args: &GenTakerPaymentSpendArgs<'_, T>, htlc_keypair: &KeyPair, -) -> GenTakerPaymentSpendResult { - let maker_pub = Public::from_slice(args.maker_pub).map_to_mm(|e| TxGenError::InvalidPubkey(e.to_string()))?; - let taker_pub = Public::from_slice(args.taker_pub).map_to_mm(|e| TxGenError::InvalidPubkey(e.to_string()))?; +) -> GenPreimageResult { let time_lock = args .time_lock .try_into() .map_to_mm(|e: TryFromIntError| TxGenError::LocktimeOverflow(e.to_string()))?; - let preimage = gen_taker_payment_spend_preimage(coin, args, LocktimeSetting::CalcByHtlcLocktime(time_lock)).await?; + let preimage = gen_taker_payment_spend_preimage(coin, args, NTimeSetting::UseNow).await?; let redeem_script = - swap_proto_v2_scripts::taker_payment_script(time_lock, args.secret_hash, &taker_pub, &maker_pub); + swap_proto_v2_scripts::taker_payment_script(time_lock, args.secret_hash, args.taker_pub, args.maker_pub); let signature = calc_and_sign_sighash( &preimage, DEFAULT_SWAP_VOUT, @@ -1268,10 +1538,9 @@ pub async fn gen_and_sign_taker_payment_spend_preimage( SIGHASH_SINGLE, coin.as_ref().conf.fork_id, )?; - let preimage_tx: UtxoTx = preimage.into(); Ok(TxPreimageWithSig { - preimage: serialize(&preimage_tx).take(), - signature: signature.take(), + preimage: preimage.into(), + signature: signature.take().into(), }) } @@ -1279,33 +1548,24 @@ pub async fn gen_and_sign_taker_payment_spend_preimage( /// Checks taker's signature and compares received preimage with the expected tx. pub async fn validate_taker_payment_spend_preimage( coin: &T, - gen_args: &GenTakerPaymentSpendArgs<'_>, - preimage: &TxPreimageWithSig, + gen_args: &GenTakerPaymentSpendArgs<'_, T>, + preimage: &TxPreimageWithSig, ) -> ValidateTakerPaymentSpendPreimageResult { - // TODO validate that preimage has exactly 2 outputs - let actual_preimage_tx: UtxoTx = deserialize(preimage.preimage.as_slice()) - .map_to_mm(|e| ValidateTakerPaymentSpendPreimageError::TxDeserialization(e.to_string()))?; - - let maker_pub = Public::from_slice(gen_args.maker_pub) - .map_to_mm(|e| ValidateTakerPaymentSpendPreimageError::InvalidPubkey(e.to_string()))?; - let taker_pub = Public::from_slice(gen_args.taker_pub) - .map_to_mm(|e| ValidateTakerPaymentSpendPreimageError::InvalidPubkey(e.to_string()))?; - - // TODO validate premium amount. Might be a bit tricky in the case of dynamic miner fee - // TODO validate that output amounts are larger than dust - // Here, we have to use the exact lock time from the preimage because maker // can get different values (e.g. if MTP advances during preimage exchange/fee rate changes) let expected_preimage = - gen_taker_payment_spend_preimage(coin, gen_args, LocktimeSetting::UseExact(actual_preimage_tx.lock_time)) - .await?; + gen_taker_payment_spend_preimage(coin, gen_args, NTimeSetting::UseValue(preimage.preimage.n_time)).await?; let time_lock = gen_args .time_lock .try_into() .map_to_mm(|e: TryFromIntError| ValidateTakerPaymentSpendPreimageError::LocktimeOverflow(e.to_string()))?; - let redeem_script = - swap_proto_v2_scripts::taker_payment_script(time_lock, gen_args.secret_hash, &taker_pub, &maker_pub); + let redeem_script = swap_proto_v2_scripts::taker_payment_script( + time_lock, + gen_args.secret_hash, + gen_args.taker_pub, + gen_args.maker_pub, + ); let sig_hash = signature_hash_to_sign( &expected_preimage, DEFAULT_SWAP_VOUT, @@ -1315,14 +1575,15 @@ pub async fn validate_taker_payment_spend_preimage( coin.as_ref().conf.fork_id, )?; - if !taker_pub - .verify(&sig_hash, &preimage.signature.clone().into()) + if !gen_args + .taker_pub + .verify(&sig_hash, &preimage.signature) .map_to_mm(|e| ValidateTakerPaymentSpendPreimageError::SignatureVerificationFailure(e.to_string()))? { return MmError::err(ValidateTakerPaymentSpendPreimageError::InvalidTakerSignature); }; let expected_preimage_tx: UtxoTx = expected_preimage.into(); - if expected_preimage_tx != actual_preimage_tx { + if expected_preimage_tx != preimage.preimage { return MmError::err(ValidateTakerPaymentSpendPreimageError::InvalidPreimage( "Preimage is not equal to expected".into(), )); @@ -1334,31 +1595,23 @@ pub async fn validate_taker_payment_spend_preimage( /// 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( coin: &T, - preimage: &TxPreimageWithSig, - gen_args: &GenTakerPaymentSpendArgs<'_>, + preimage: &TxPreimageWithSig, + gen_args: &GenTakerPaymentSpendArgs<'_, T>, secret: &[u8], htlc_keypair: &KeyPair, ) -> TransactionResult { - let taker_pub = try_tx_s!(Public::from_slice(gen_args.taker_pub)); - - let mut taker_tx: UtxoTx = try_tx_s!(deserialize(gen_args.taker_tx)); - taker_tx.tx_hash_algo = coin.as_ref().tx_hash_algo; - drop_mutability!(taker_tx); - - let mut preimage_tx: UtxoTx = try_tx_s!(deserialize(preimage.preimage.as_slice())); - preimage_tx.tx_hash_algo = coin.as_ref().tx_hash_algo; - drop_mutability!(preimage_tx); - 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(), - &taker_pub, + gen_args.taker_pub, htlc_keypair.public(), ); - let mut signer: TransactionInputSigner = preimage_tx.clone().into(); - signer.inputs[0].amount = taker_tx.outputs[0].value; + let mut signer: TransactionInputSigner = preimage.preimage.clone().into(); + let payment_input = try_tx_s!(signer.inputs.first_mut().ok_or("Preimage doesn't have inputs")); + let payment_output = try_tx_s!(gen_args.taker_tx.first_output()); + payment_input.amount = payment_output.value; signer.consensus_branch_id = coin.as_ref().conf.consensus_branch_id; let miner_fee = try_tx_s!( @@ -1369,7 +1622,7 @@ pub async fn sign_and_broadcast_taker_payment_spend( let maker_amount = &gen_args.trading_amount + &gen_args.premium_amount; let maker_sat = try_tx_s!(sat_from_big_decimal(&maker_amount, coin.as_ref().decimals)); if miner_fee + coin.as_ref().dust_amount > maker_sat { - return TX_PLAIN_ERR!("Maker amount is too small to cover miner fee"); + return TX_PLAIN_ERR!("Maker amount is too small to cover miner fee + dust"); } let maker_address = try_tx_s!(coin.as_ref().derivation_method.single_addr_or_err()); @@ -1390,7 +1643,7 @@ pub async fn sign_and_broadcast_taker_payment_spend( coin.as_ref().conf.fork_id )); let sig_hash_single_fork_id = (SIGHASH_SINGLE | coin.as_ref().conf.fork_id) as u8; - let mut taker_signature_with_sighash = preimage.signature.clone(); + let mut taker_signature_with_sighash = preimage.signature.to_vec(); taker_signature_with_sighash.push(sig_hash_single_fork_id); drop_mutability!(taker_signature_with_sighash); @@ -1400,15 +1653,15 @@ pub async fn sign_and_broadcast_taker_payment_spend( drop_mutability!(maker_signature_with_sighash); let script_sig = Builder::default() - .push_opcode(Opcode::OP_0) - .push_data(&taker_signature_with_sighash) .push_data(&maker_signature_with_sighash) + .push_data(&taker_signature_with_sighash) .push_data(secret) .push_opcode(Opcode::OP_0) .push_data(&redeem_script) .into_bytes(); let mut final_tx: UtxoTx = signer.into(); - final_tx.inputs[0].script_sig = script_sig; + let final_tx_input = try_tx_s!(final_tx.inputs.first_mut().ok_or("Final tx doesn't have inputs")); + final_tx_input.script_sig = script_sig; drop_mutability!(final_tx); try_tx_s!(coin.broadcast_tx(&final_tx).await, final_tx); @@ -1510,9 +1763,8 @@ pub fn send_maker_spends_taker_payment(coin: T, args let mut prev_transaction: UtxoTx = try_tx_fus!(deserialize(args.other_payment_tx).map_err(|e| ERRL!("{:?}", e))); prev_transaction.tx_hash_algo = coin.as_ref().tx_hash_algo; drop_mutability!(prev_transaction); - if prev_transaction.outputs.is_empty() { - return try_tx_fus!(TX_PLAIN_ERR!("Transaction doesn't have any output")); - } + + let payment_value = try_tx_fus!(prev_transaction.first_output()).value; let key_pair = coin.derive_htlc_key_pair(args.swap_unique_data); let script_data = Builder::default() @@ -1533,16 +1785,16 @@ pub fn send_maker_spends_taker_payment(coin: T, args coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WithoutApprox) .await ); - if fee >= prev_transaction.outputs[0].value { + if fee >= payment_value { return TX_PLAIN_ERR!( "HTLC spend fee {} is greater than transaction output {}", fee, - prev_transaction.outputs[0].value + payment_value ); } let script_pubkey = output_script(&my_address, ScriptType::P2PKH).to_bytes(); let output = TransactionOutput { - value: prev_transaction.outputs[0].value - fee, + value: payment_value - fee, script_pubkey, }; @@ -1620,9 +1872,7 @@ pub fn create_maker_payment_spend_preimage( let mut prev_transaction: UtxoTx = try_tx_fus!(deserialize(maker_payment_tx).map_err(|e| ERRL!("{:?}", e))); prev_transaction.tx_hash_algo = coin.as_ref().tx_hash_algo; drop_mutability!(prev_transaction); - if prev_transaction.outputs.is_empty() { - return try_tx_fus!(TX_PLAIN_ERR!("Transaction doesn't have any output")); - } + let payment_value = try_tx_fus!(prev_transaction.first_output()).value; let key_pair = coin.derive_htlc_key_pair(swap_unique_data); @@ -1641,16 +1891,16 @@ pub fn create_maker_payment_spend_preimage( .await ); - if fee >= prev_transaction.outputs[0].value { + if fee >= payment_value { return TX_PLAIN_ERR!( "HTLC spend fee {} is greater than transaction output {}", fee, - prev_transaction.outputs[0].value + payment_value ); } let script_pubkey = output_script(&my_address, ScriptType::P2PKH).to_bytes(); let output = TransactionOutput { - value: prev_transaction.outputs[0].value - fee, + value: payment_value - fee, script_pubkey, }; @@ -1684,9 +1934,7 @@ pub fn create_taker_payment_refund_preimage( try_tx_fus!(deserialize(taker_payment_tx).map_err(|e| TransactionErr::Plain(format!("{:?}", e)))); prev_transaction.tx_hash_algo = coin.as_ref().tx_hash_algo; drop_mutability!(prev_transaction); - if prev_transaction.outputs.is_empty() { - return try_tx_fus!(TX_PLAIN_ERR!("Transaction doesn't have any output")); - } + let payment_value = try_tx_fus!(prev_transaction.first_output()).value; let key_pair = coin.derive_htlc_key_pair(swap_unique_data); let script_data = Builder::default().push_opcode(Opcode::OP_1).into_script(); @@ -1702,16 +1950,16 @@ pub fn create_taker_payment_refund_preimage( coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WatcherPreimage) .await ); - if fee >= prev_transaction.outputs[0].value { + if fee >= payment_value { return TX_PLAIN_ERR!( "HTLC spend fee {} is greater than transaction output {}", fee, - prev_transaction.outputs[0].value + payment_value ); } let script_pubkey = output_script(&my_address, ScriptType::P2PKH).to_bytes(); let output = TransactionOutput { - value: prev_transaction.outputs[0].value - fee, + value: payment_value - fee, script_pubkey, }; @@ -1736,9 +1984,7 @@ pub fn send_taker_spends_maker_payment(coin: T, args let mut prev_transaction: UtxoTx = try_tx_fus!(deserialize(args.other_payment_tx).map_err(|e| ERRL!("{:?}", e))); prev_transaction.tx_hash_algo = coin.as_ref().tx_hash_algo; drop_mutability!(prev_transaction); - if prev_transaction.outputs.is_empty() { - return try_tx_fus!(TX_PLAIN_ERR!("Transaction doesn't have any output")); - } + let payment_value = try_tx_fus!(prev_transaction.first_output()).value; let key_pair = coin.derive_htlc_key_pair(args.swap_unique_data); @@ -1761,16 +2007,16 @@ pub fn send_taker_spends_maker_payment(coin: T, args coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WithoutApprox) .await ); - if fee >= prev_transaction.outputs[0].value { + if fee >= payment_value { return TX_PLAIN_ERR!( "HTLC spend fee {} is greater than transaction output {}", fee, - prev_transaction.outputs[0].value + payment_value ); } let script_pubkey = output_script(&my_address, ScriptType::P2PKH).to_bytes(); let output = TransactionOutput { - value: prev_transaction.outputs[0].value - fee, + value: payment_value - fee, script_pubkey, }; @@ -1803,9 +2049,7 @@ async fn refund_htlc_payment( try_tx_s!(deserialize(args.payment_tx).map_err(|e| TransactionErr::Plain(format!("{:?}", e)))); prev_transaction.tx_hash_algo = coin.as_ref().tx_hash_algo; drop_mutability!(prev_transaction); - if prev_transaction.outputs.is_empty() { - return try_tx_s!(TX_PLAIN_ERR!("Transaction doesn't have any output")); - } + let payment_value = try_tx_s!(prev_transaction.first_output()).value; let other_public = try_tx_s!(Public::from_slice(args.other_pubkey)); let key_pair = coin.derive_htlc_key_pair(args.swap_unique_data); @@ -1816,6 +2060,10 @@ async fn refund_htlc_payment( SwapPaymentType::TakerOrMakerPayment => { payment_script(time_lock, args.secret_hash, key_pair.public(), &other_public).into() }, + SwapPaymentType::TakerFunding => { + swap_proto_v2_scripts::taker_funding_script(time_lock, args.secret_hash, key_pair.public(), &other_public) + .into() + }, SwapPaymentType::TakerPaymentV2 => { swap_proto_v2_scripts::taker_payment_script(time_lock, args.secret_hash, key_pair.public(), &other_public) .into() @@ -1825,16 +2073,16 @@ async fn refund_htlc_payment( coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WithoutApprox) .await ); - if fee >= prev_transaction.outputs[0].value { + if fee >= payment_value { return TX_PLAIN_ERR!( "HTLC spend fee {} is greater than transaction output {}", fee, - prev_transaction.outputs[0].value + payment_value ); } let script_pubkey = output_script(&my_address, ScriptType::P2PKH).to_bytes(); let output = TransactionOutput { - value: prev_transaction.outputs[0].value - fee, + value: payment_value - fee, script_pubkey, }; @@ -1895,7 +2143,7 @@ fn pubkey_from_script_sig(script: &Script) -> Result { match script.get_instruction(0) { Some(Ok(instruction)) => match instruction.opcode { Opcode::OP_PUSHBYTES_70 | Opcode::OP_PUSHBYTES_71 | Opcode::OP_PUSHBYTES_72 => match instruction.data { - Some(bytes) => try_s!(Signature::from_der(&bytes[..bytes.len() - 1])), + Some(bytes) => try_s!(SecpSignature::from_der(&bytes[..bytes.len() - 1])), None => return ERR!("No data at instruction 0 of script {:?}", script), }, _ => return ERR!("Unexpected opcode {:?}", instruction.opcode), @@ -1932,7 +2180,7 @@ fn pubkey_from_witness_script(witness_script: &[Bytes]) -> Result if signature.is_empty() { return ERR!("Empty signature data in witness script"); } - try_s!(Signature::from_der(&signature[..signature.len() - 1])); + try_s!(SecpSignature::from_der(&signature[..signature.len() - 1])); let pubkey = try_s!(PublicKey::from_slice(&witness_script[1])); @@ -2630,10 +2878,16 @@ pub fn wait_for_output_spend( let tx_hash_algo = coin.tx_hash_algo; let fut = async move { loop { + let script_pubkey = &try_tx_s!(tx + .outputs + .get(output_index) + .ok_or(ERRL!("No output with index {}", output_index))) + .script_pubkey; + match client .find_output_spend( tx.hash(), - &tx.outputs[output_index].script_pubkey, + script_pubkey, output_index, BlockHashOrHeight::Height(from_block as i64), ) @@ -4146,10 +4400,17 @@ async fn search_for_swap_output_spend( } let script = payment_script(time_lock, secret_hash, first_pub, second_pub); let expected_script_pubkey = Builder::build_p2sh(&dhash160(&script).into()).to_bytes(); - if tx.outputs[0].script_pubkey != expected_script_pubkey { + let script_pubkey = &tx + .outputs + .get(output_index) + .ok_or(ERRL!("No output with index {}", output_index))? + .script_pubkey; + + if *script_pubkey != expected_script_pubkey { return ERR!( - "Transaction {:?} output 0 script_pubkey doesn't match expected {:?}", + "Transaction {:?} output {} script_pubkey doesn't match expected {:?}", tx, + output_index, expected_script_pubkey ); } @@ -4158,7 +4419,7 @@ async fn search_for_swap_output_spend( coin.rpc_client .find_output_spend( tx.hash(), - &tx.outputs[output_index].script_pubkey, + script_pubkey, output_index, BlockHashOrHeight::Height(search_from_block as i64) ) @@ -4198,6 +4459,7 @@ struct SwapPaymentOutputsResult { enum SwapPaymentType { TakerOrMakerPayment, + TakerFunding, TakerPaymentV2, } @@ -4217,6 +4479,9 @@ where let other_public = try_s!(Public::from_slice(other_pub)); let redeem_script = match payment_type { SwapPaymentType::TakerOrMakerPayment => payment_script(time_lock, secret_hash, &my_public, &other_public), + SwapPaymentType::TakerFunding => { + swap_proto_v2_scripts::taker_funding_script(time_lock, secret_hash, &my_public, &other_public) + }, SwapPaymentType::TakerPaymentV2 => { swap_proto_v2_scripts::taker_payment_script(time_lock, secret_hash, &my_public, &other_public) }, @@ -4574,8 +4839,8 @@ where .collect() } -/// Common implementation of combined taker payment generation and broadcast for UTXO coins. -pub async fn send_combined_taker_payment(coin: T, args: SendCombinedTakerPaymentArgs<'_>) -> TransactionResult +/// Common implementation of taker funding generation and broadcast for UTXO coins. +pub async fn send_taker_funding(coin: T, args: SendTakerFundingArgs<'_>) -> Result where T: UtxoCommonOps + GetUtxoListOps + SwapOps, { @@ -4589,10 +4854,10 @@ where &coin, try_tx_s!(args.time_lock.try_into()), taker_htlc_key_pair.public_slice(), - args.other_pub, - args.secret_hash, + args.maker_pub, + args.taker_secret_hash, total_amount, - SwapPaymentType::TakerPaymentV2, + SwapPaymentType::TakerFunding, )); if let UtxoRpcClientEnum::Native(client) = &coin.as_ref().rpc_client { let addr_string = try_tx_s!(payment_address.display_address()); @@ -4602,25 +4867,82 @@ where .compat() .await?; } - send_outputs_from_my_address(coin, outputs).compat().await + send_outputs_from_my_address_impl(coin, outputs).await } -/// Common implementation of combined taker payment validation for UTXO coins. -pub async fn validate_combined_taker_payment( - coin: &T, - args: ValidateTakerPaymentArgs<'_>, -) -> ValidateTakerPaymentResult +/// Common implementation of taker funding reclaim for UTXO coins using time-locked path. +pub async fn refund_taker_funding_timelock(coin: T, args: RefundPaymentArgs<'_>) -> TransactionResult where - T: UtxoCommonOps + SwapOps, + T: UtxoCommonOps + GetUtxoListOps + SwapOps, +{ + refund_htlc_payment(coin, args, SwapPaymentType::TakerFunding).await +} + +/// Common implementation of taker funding reclaim for UTXO coins using immediate refund path with secret reveal. +pub async fn refund_taker_funding_secret( + coin: T, + args: RefundFundingSecretArgs<'_, T>, +) -> Result +where + T: UtxoCommonOps + GetUtxoListOps + SwapOps, { - let dex_fee_tx: UtxoTx = - deserialize(args.taker_tx).map_to_mm(|e| ValidateTakerPaymentError::TxDeserialization(e.to_string()))?; - if dex_fee_tx.outputs.len() < 2 { - return MmError::err(ValidateTakerPaymentError::TxLacksOfOutputs); + let my_address = try_tx_s!(coin.as_ref().derivation_method.single_addr_or_err()).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 script_data = Builder::default() + .push_data(args.taker_secret) + .push_opcode(Opcode::OP_0) + .push_opcode(Opcode::OP_0) + .into_script(); + let time_lock = try_tx_s!(args.time_lock.try_into()); + + let redeem_script = swap_proto_v2_scripts::taker_funding_script( + time_lock, + args.taker_secret_hash, + key_pair.public(), + args.maker_pubkey, + ) + .into(); + let fee = try_tx_s!( + coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WithoutApprox) + .await + ); + if fee >= payment_value { + return TX_PLAIN_ERR!( + "HTLC spend fee {} is greater than transaction output {}", + fee, + payment_value + ); } + let script_pubkey = output_script(&my_address, ScriptType::P2PKH).to_bytes(); + let output = TransactionOutput { + value: payment_value - fee, + script_pubkey, + }; + + let input = P2SHSpendingTxInput { + prev_transaction: args.funding_tx.clone(), + redeem_script, + outputs: vec![output], + script_data, + sequence: SEQUENCE_FINAL, + lock_time: time_lock, + keypair: &key_pair, + }; + let transaction = try_tx_s!(coin.p2sh_spending_tx(input).await); + + let tx_fut = coin.as_ref().rpc_client.send_transaction(&transaction).compat(); + try_tx_s!(tx_fut.await, transaction); + + Ok(transaction) +} - let taker_pub = - Public::from_slice(args.other_pub).map_to_mm(|e| ValidateTakerPaymentError::InvalidPubkey(e.to_string()))?; +/// Common implementation of taker funding validation for UTXO coins. +pub async fn validate_taker_funding(coin: &T, args: ValidateTakerFundingArgs<'_, T>) -> ValidateTakerFundingResult +where + T: UtxoCommonOps + SwapOps, +{ let maker_htlc_key_pair = coin.derive_htlc_key_pair(args.swap_unique_data); let total_expected_amount = &args.dex_fee_amount + &args.premium_amount + &args.trading_amount; @@ -4629,12 +4951,12 @@ where let time_lock = args .time_lock .try_into() - .map_to_mm(|e: TryFromIntError| ValidateTakerPaymentError::LocktimeOverflow(e.to_string()))?; + .map_to_mm(|e: TryFromIntError| ValidateTakerFundingError::LocktimeOverflow(e.to_string()))?; - let redeem_script = swap_proto_v2_scripts::taker_payment_script( + let redeem_script = swap_proto_v2_scripts::taker_funding_script( time_lock, - args.secret_hash, - &taker_pub, + args.taker_secret_hash, + args.other_pub, maker_htlc_key_pair.public(), ); let expected_output = TransactionOutput { @@ -4642,23 +4964,25 @@ where script_pubkey: Builder::build_p2sh(&AddressHashEnum::AddressHash(dhash160(&redeem_script))).into(), }; - if dex_fee_tx.outputs[0] != expected_output { - return MmError::err(ValidateTakerPaymentError::InvalidDestinationOrAmount(format!( + if args.funding_tx.outputs.get(0) != Some(&expected_output) { + return MmError::err(ValidateTakerFundingError::InvalidDestinationOrAmount(format!( "Expected {:?}, got {:?}", - expected_output, dex_fee_tx.outputs[0] + expected_output, + args.funding_tx.outputs.get(0) ))); } let tx_bytes_from_rpc = coin .as_ref() .rpc_client - .get_transaction_bytes(&dex_fee_tx.hash().reversed().into()) + .get_transaction_bytes(&args.funding_tx.hash().reversed().into()) .compat() .await?; - if tx_bytes_from_rpc.0 != args.taker_tx { - return MmError::err(ValidateTakerPaymentError::TxBytesMismatch { + let actual_tx_bytes = serialize(args.funding_tx).take(); + if tx_bytes_from_rpc.0 != actual_tx_bytes { + return MmError::err(ValidateTakerFundingError::TxBytesMismatch { from_rpc: tx_bytes_from_rpc, - actual: args.taker_tx.into(), + actual: actual_tx_bytes.into(), }); } Ok(()) diff --git a/mm2src/coins/utxo/utxo_standard.rs b/mm2src/coins/utxo/utxo_standard.rs index a9b5e3aa32..26bc6bdc88 100644 --- a/mm2src/coins/utxo/utxo_standard.rs +++ b/mm2src/coins/utxo/utxo_standard.rs @@ -23,18 +23,18 @@ use crate::utxo::utxo_builder::{UtxoArcBuilder, UtxoCoinBuilder}; use crate::utxo::utxo_tx_history_v2::{UtxoMyAddressesHistoryError, UtxoTxDetailsError, UtxoTxDetailsParams, UtxoTxHistoryOps}; use crate::{CanRefundHtlc, CheckIfMyPaymentSentArgs, CoinBalance, CoinWithDerivationMethod, ConfirmPaymentInput, - GenTakerPaymentSpendArgs, GenTakerPaymentSpendResult, GetWithdrawSenderAddress, IguanaPrivKey, - MakerSwapTakerCoin, MmCoinEnum, NegotiateSwapContractAddrErr, PaymentInstructionArgs, PaymentInstructions, - PaymentInstructionsErr, PrivKeyBuildPolicy, RefundError, RefundPaymentArgs, RefundResult, - SearchForSwapTxSpendInput, SendCombinedTakerPaymentArgs, SendMakerPaymentSpendPreimageInput, - SendPaymentArgs, SignatureResult, SpendPaymentArgs, SwapOps, SwapOpsV2, TakerSwapMakerCoin, - TradePreimageValue, TransactionFut, TransactionResult, TxMarshalingErr, TxPreimageWithSig, - ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, - ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, ValidateTakerPaymentArgs, - ValidateTakerPaymentResult, ValidateTakerPaymentSpendPreimageResult, ValidateWatcherSpendInput, - VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, - WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawFut, - WithdrawSenderAddress}; + GenPreimageResult, GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, GetWithdrawSenderAddress, + IguanaPrivKey, MakerSwapTakerCoin, MmCoinEnum, NegotiateSwapContractAddrErr, PaymentInstructionArgs, + PaymentInstructions, PaymentInstructionsErr, PrivKeyBuildPolicy, RefundError, RefundFundingSecretArgs, + RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, + SendPaymentArgs, SendTakerFundingArgs, SignatureResult, SpendPaymentArgs, SwapOps, SwapOpsV2, + TakerSwapMakerCoin, ToBytes, TradePreimageValue, TransactionFut, TransactionResult, TxMarshalingErr, + TxPreimageWithSig, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, + ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, + ValidateTakerFundingArgs, ValidateTakerFundingResult, ValidateTakerFundingSpendPreimageResult, + ValidateTakerPaymentSpendPreimageResult, ValidateWatcherSpendInput, VerificationResult, + WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, + WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawFut, WithdrawSenderAddress}; use common::executor::{AbortableSystem, AbortedError}; use crypto::Bip44Chain; use futures::{FutureExt, TryFutureExt}; @@ -588,14 +588,56 @@ impl WatcherOps for UtxoStandardCoin { } } +impl ToBytes for Public { + fn to_bytes(&self) -> Vec { self.to_vec() } +} + #[async_trait] impl SwapOpsV2 for UtxoStandardCoin { - async fn send_combined_taker_payment(&self, args: SendCombinedTakerPaymentArgs<'_>) -> TransactionResult { - utxo_common::send_combined_taker_payment(self.clone(), args).await + async fn send_taker_funding(&self, args: SendTakerFundingArgs<'_>) -> Result { + utxo_common::send_taker_funding(self.clone(), args).await + } + + async fn validate_taker_funding(&self, args: ValidateTakerFundingArgs<'_, Self>) -> ValidateTakerFundingResult { + utxo_common::validate_taker_funding(self, args).await + } + + async fn refund_taker_funding_timelock(&self, args: RefundPaymentArgs<'_>) -> TransactionResult { + utxo_common::refund_taker_funding_timelock(self.clone(), args).await + } + + async fn refund_taker_funding_secret( + &self, + args: RefundFundingSecretArgs<'_, Self>, + ) -> Result { + utxo_common::refund_taker_funding_secret(self.clone(), args).await + } + + async fn gen_taker_funding_spend_preimage( + &self, + 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 } - async fn validate_combined_taker_payment(&self, args: ValidateTakerPaymentArgs<'_>) -> ValidateTakerPaymentResult { - utxo_common::validate_combined_taker_payment(self, args).await + async fn validate_taker_funding_spend_preimage( + &self, + gen_args: &GenTakerFundingSpendArgs<'_, Self>, + preimage: &TxPreimageWithSig, + ) -> ValidateTakerFundingSpendPreimageResult { + utxo_common::validate_taker_funding_spend_preimage(self, gen_args, preimage).await + } + + async fn sign_and_send_taker_funding_spend( + &self, + preimage: &TxPreimageWithSig, + 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 } async fn refund_combined_taker_payment(&self, args: RefundPaymentArgs<'_>) -> TransactionResult { @@ -604,31 +646,35 @@ impl SwapOpsV2 for UtxoStandardCoin { async fn gen_taker_payment_spend_preimage( &self, - args: &GenTakerPaymentSpendArgs<'_>, + args: &GenTakerPaymentSpendArgs<'_, Self>, swap_unique_data: &[u8], - ) -> GenTakerPaymentSpendResult { + ) -> 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 } async fn validate_taker_payment_spend_preimage( &self, - gen_args: &GenTakerPaymentSpendArgs<'_>, - preimage: &TxPreimageWithSig, + gen_args: &GenTakerPaymentSpendArgs<'_, Self>, + preimage: &TxPreimageWithSig, ) -> ValidateTakerPaymentSpendPreimageResult { utxo_common::validate_taker_payment_spend_preimage(self, gen_args, preimage).await } async fn sign_and_broadcast_taker_payment_spend( &self, - preimage: &TxPreimageWithSig, - gen_args: &GenTakerPaymentSpendArgs<'_>, + preimage: &TxPreimageWithSig, + gen_args: &GenTakerPaymentSpendArgs<'_, Self>, secret: &[u8], swap_unique_data: &[u8], ) -> TransactionResult { 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 } + + fn derive_htlc_pubkey_v2(&self, swap_unique_data: &[u8]) -> Self::Pubkey { + *self.derive_htlc_key_pair(swap_unique_data).public() + } } impl MarketCoinOps for UtxoStandardCoin { diff --git a/mm2src/mm2_bitcoin/chain/src/transaction.rs b/mm2src/mm2_bitcoin/chain/src/transaction.rs index ca75eaa1ad..0490447d29 100644 --- a/mm2src/mm2_bitcoin/chain/src/transaction.rs +++ b/mm2src/mm2_bitcoin/chain/src/transaction.rs @@ -14,6 +14,7 @@ use hash::{CipherText, EncCipherText, OutCipherText, ZkProof, ZkProofSapling, H2 use hex::FromHex; use ser::{deserialize, serialize, serialize_with_flags, SERIALIZE_TRANSACTION_WITNESS}; use ser::{CompactInteger, Deserializable, Error, Reader, Serializable, Stream}; +use std::fmt::Formatter; use std::io; use std::io::Read; @@ -257,6 +258,14 @@ impl Default for TxHashAlgo { fn default() -> Self { TxHashAlgo::DSHA256 } } +/// Represents the error returned when transaction has no outputs +#[derive(Debug)] +pub struct TxHasNoOutputs {} + +impl std::fmt::Display for TxHasNoOutputs { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.write_str("Tx has no outputs") } +} + impl Transaction { pub fn hash(&self) -> H256 { let serialized = &serialize(self); @@ -318,6 +327,12 @@ impl Transaction { } result } + + /// Returns reference to first output of the transaction or error if outputs are empty + #[inline] + pub fn first_output(&self) -> Result<&TransactionOutput, TxHasNoOutputs> { + self.outputs.first().ok_or(TxHasNoOutputs {}) + } } impl Serializable for TransactionInput { diff --git a/mm2src/mm2_main/src/database.rs b/mm2src/mm2_main/src/database.rs index 47b1241cb3..8ce87db0e1 100644 --- a/mm2src/mm2_main/src/database.rs +++ b/mm2src/mm2_main/src/database.rs @@ -101,6 +101,10 @@ fn migration_8() -> Vec<(&'static str, Vec)> { db_common::sqlite::execute_batch(stats_swaps::ADD_MAKER_TAKER_PUBKEYS) } +fn migration_9() -> Vec<(&'static str, Vec)> { + db_common::sqlite::execute_batch(my_swaps::TRADING_PROTO_UPGRADE_MIGRATION) +} + async fn statements_for_migration(ctx: &MmArc, current_migration: i64) -> Option)>> { match current_migration { 1 => Some(migration_1(ctx).await), @@ -111,6 +115,7 @@ async fn statements_for_migration(ctx: &MmArc, current_migration: i64) -> Option 6 => Some(migration_6()), 7 => Some(migration_7()), 8 => Some(migration_8()), + 9 => Some(migration_9()), _ => None, } } diff --git a/mm2src/mm2_main/src/database/my_swaps.rs b/mm2src/mm2_main/src/database/my_swaps.rs index ab5a08b84b..4b852db835 100644 --- a/mm2src/mm2_main/src/database/my_swaps.rs +++ b/mm2src/mm2_main/src/database/my_swaps.rs @@ -5,7 +5,7 @@ use crate::mm2::lp_swap::{MyRecentSwapsUuids, MySwapsFilter, SavedSwap, SavedSwa use common::log::debug; use common::PagingOptions; use db_common::sqlite::offset_by_uuid; -use db_common::sqlite::rusqlite::{Connection, Error as SqlError, Result as SqlResult, ToSql}; +use db_common::sqlite::rusqlite::{Connection, Error as SqlError, Result as SqlResult, Row, ToSql}; use db_common::sqlite::sql_builder::SqlBuilder; use mm2_core::mm_ctx::MmArc; use std::convert::TryInto; @@ -27,6 +27,31 @@ macro_rules! CREATE_MY_SWAPS_TABLE { );" }; } + +/// Adds new fields required for trading protocol upgrade implementation (swap v2) +pub const TRADING_PROTO_UPGRADE_MIGRATION: &[&str] = &[ + "ALTER TABLE my_swaps ADD COLUMN is_finished BOOLEAN NOT NULL DEFAULT 0;", + "ALTER TABLE my_swaps ADD COLUMN events_json TEXT NOT NULL DEFAULT '[]';", + "ALTER TABLE my_swaps ADD COLUMN swap_type INTEGER NOT NULL DEFAULT 0;", + // Storing rational numbers as text to maintain precision + "ALTER TABLE my_swaps ADD COLUMN maker_volume TEXT;", + // Storing rational numbers as text to maintain precision + "ALTER TABLE my_swaps ADD COLUMN taker_volume TEXT;", + // Storing rational numbers as text to maintain precision + "ALTER TABLE my_swaps ADD COLUMN premium TEXT;", + // Storing rational numbers as text to maintain precision + "ALTER TABLE my_swaps ADD COLUMN dex_fee TEXT;", + "ALTER TABLE my_swaps ADD COLUMN secret BLOB;", + "ALTER TABLE my_swaps ADD COLUMN secret_hash BLOB;", + "ALTER TABLE my_swaps ADD COLUMN secret_hash_algo INTEGER;", + "ALTER TABLE my_swaps ADD COLUMN p2p_privkey BLOB;", + "ALTER TABLE my_swaps ADD COLUMN lock_duration INTEGER;", + "ALTER TABLE my_swaps ADD COLUMN maker_coin_confs INTEGER;", + "ALTER TABLE my_swaps ADD COLUMN maker_coin_nota BOOLEAN;", + "ALTER TABLE my_swaps ADD COLUMN taker_coin_confs INTEGER;", + "ALTER TABLE my_swaps ADD COLUMN taker_coin_nota BOOLEAN;", +]; + const INSERT_MY_SWAP: &str = "INSERT INTO my_swaps (my_coin, other_coin, uuid, started_at) VALUES (?1, ?2, ?3, ?4)"; pub fn insert_new_swap(ctx: &MmArc, my_coin: &str, other_coin: &str, uuid: &str, started_at: &str) -> SqlResult<()> { @@ -36,6 +61,51 @@ pub fn insert_new_swap(ctx: &MmArc, my_coin: &str, other_coin: &str, uuid: &str, conn.execute(INSERT_MY_SWAP, params).map(|_| ()) } +const INSERT_MY_SWAP_V2: &str = r#"INSERT INTO my_swaps ( + my_coin, + other_coin, + uuid, + started_at, + swap_type, + maker_volume, + taker_volume, + premium, + dex_fee, + secret, + secret_hash, + secret_hash_algo, + p2p_privkey, + lock_duration, + maker_coin_confs, + maker_coin_nota, + taker_coin_confs, + taker_coin_nota +) VALUES ( + :my_coin, + :other_coin, + :uuid, + :started_at, + :swap_type, + :maker_volume, + :taker_volume, + :premium, + :dex_fee, + :secret, + :secret_hash, + :secret_hash_algo, + :p2p_privkey, + :lock_duration, + :maker_coin_confs, + :maker_coin_nota, + :taker_coin_confs, + :taker_coin_nota +);"#; + +pub fn insert_new_swap_v2(ctx: &MmArc, params: &[(&str, &dyn ToSql)]) -> SqlResult<()> { + let conn = ctx.sqlite_connection(); + conn.execute(INSERT_MY_SWAP_V2, params).map(|_| ()) +} + /// Returns SQL statements to initially fill my_swaps table using existing DB with JSON files pub async fn fill_my_swaps_from_json_statements(ctx: &MmArc) -> Vec<(&'static str, Vec)> { let swaps = SavedSwap::load_all_my_swaps_from_db(ctx).await.unwrap_or_default(); @@ -154,3 +224,108 @@ pub fn select_uuids_by_my_swaps_filter( skipped, }) } + +/// Queries swap type by uuid +pub fn get_swap_type(conn: &Connection, uuid: &str) -> SqlResult { + const SELECT_SWAP_TYPE_BY_UUID: &str = "SELECT swap_type FROM my_swaps WHERE uuid = :uuid;"; + let mut stmt = conn.prepare(SELECT_SWAP_TYPE_BY_UUID)?; + let swap_type = stmt.query_row(&[(":uuid", uuid)], |row| row.get(0))?; + Ok(swap_type) +} + +/// Queries swap events by uuid +pub fn get_swap_events(conn: &Connection, uuid: &str) -> SqlResult { + const SELECT_SWAP_EVENTS_BY_UUID: &str = "SELECT events_json FROM my_swaps WHERE uuid = :uuid;"; + let mut stmt = conn.prepare(SELECT_SWAP_EVENTS_BY_UUID)?; + let swap_type = stmt.query_row(&[(":uuid", uuid)], |row| row.get(0))?; + Ok(swap_type) +} + +/// Updates swap events by uuid +pub fn update_swap_events(conn: &Connection, uuid: &str, events_json: &str) -> SqlResult<()> { + const UPDATE_SWAP_EVENTS_BY_UUID: &str = "UPDATE my_swaps SET events_json = :events_json WHERE uuid = :uuid;"; + let mut stmt = conn.prepare(UPDATE_SWAP_EVENTS_BY_UUID)?; + stmt.execute(&[(":uuid", uuid), (":events_json", events_json)]) + .map(|_| ()) +} + +pub fn set_swap_is_finished(conn: &Connection, uuid: &str) -> SqlResult<()> { + const UPDATE_SWAP_IS_FINISHED_BY_UUID: &str = "UPDATE my_swaps SET is_finished = 1 WHERE uuid = :uuid;"; + let mut stmt = conn.prepare(UPDATE_SWAP_IS_FINISHED_BY_UUID)?; + stmt.execute(&[(":uuid", uuid)]).map(|_| ()) +} + +const SELECT_MY_SWAP_V2_FOR_RPC_BY_UUID: &str = r#"SELECT + my_coin, + other_coin, + uuid, + started_at, + is_finished, + events_json, + maker_volume, + taker_volume, + premium, + dex_fee, + secret_hash, + secret_hash_algo, + lock_duration, + maker_coin_confs, + maker_coin_nota, + taker_coin_confs, + taker_coin_nota +FROM my_swaps +WHERE uuid = :uuid; +"#; + +/// Represents data of the swap used for RPC, omits fields that should be kept in secret +#[derive(Debug, Serialize)] +pub struct MySwapForRpc { + my_coin: String, + other_coin: String, + uuid: String, + started_at: i64, + is_finished: bool, + events_json: String, + maker_volume: String, + taker_volume: String, + premium: String, + dex_fee: String, + secret_hash: Vec, + secret_hash_algo: i64, + lock_duration: i64, + maker_coin_confs: i64, + maker_coin_nota: bool, + taker_coin_confs: i64, + taker_coin_nota: bool, +} + +impl MySwapForRpc { + fn from_row(row: &Row) -> SqlResult { + Ok(Self { + my_coin: row.get(0)?, + other_coin: row.get(1)?, + uuid: row.get(2)?, + started_at: row.get(3)?, + is_finished: row.get(4)?, + events_json: row.get(5)?, + maker_volume: row.get(6)?, + taker_volume: row.get(7)?, + premium: row.get(8)?, + dex_fee: row.get(9)?, + secret_hash: row.get(10)?, + secret_hash_algo: row.get(11)?, + lock_duration: row.get(12)?, + maker_coin_confs: row.get(13)?, + maker_coin_nota: row.get(14)?, + taker_coin_confs: row.get(15)?, + taker_coin_nota: row.get(16)?, + }) + } +} + +/// Queries `MySwapForRpc` by uuid +pub fn get_swap_data_for_rpc(conn: &Connection, uuid: &str) -> SqlResult { + let mut stmt = conn.prepare(SELECT_MY_SWAP_V2_FOR_RPC_BY_UUID)?; + let swap_data = stmt.query_row(&[(":uuid", uuid)], MySwapForRpc::from_row)?; + Ok(swap_data) +} diff --git a/mm2src/mm2_main/src/lp_ordermatch.rs b/mm2src/mm2_main/src/lp_ordermatch.rs index d40ac478f2..2456a2f38b 100644 --- a/mm2src/mm2_main/src/lp_ordermatch.rs +++ b/mm2src/mm2_main/src/lp_ordermatch.rs @@ -71,15 +71,19 @@ use uuid::Uuid; use crate::mm2::lp_network::{broadcast_p2p_msg, request_any_relay, request_one_peer, subscribe_to_topic, P2PRequest, P2PRequestError}; +#[cfg(not(target_arch = "wasm32"))] +use crate::mm2::lp_swap::detect_secret_hash_algo; +#[cfg(not(target_arch = "wasm32"))] use crate::mm2::lp_swap::maker_swap_v2::{self, DummyMakerSwapStorage, MakerSwapStateMachine}; +#[cfg(not(target_arch = "wasm32"))] use crate::mm2::lp_swap::taker_swap_v2::{self, DummyTakerSwapStorage, TakerSwapStateMachine}; use crate::mm2::lp_swap::{calc_max_maker_vol, check_balance_for_maker_swap, check_balance_for_taker_swap, - check_other_coin_balance_for_swap, dex_fee_amount_from_taker_coin, get_max_maker_vol, - insert_new_swap_to_db, is_pubkey_banned, lp_atomic_locktime, + check_other_coin_balance_for_swap, dex_fee_amount_from_taker_coin, generate_secret, + get_max_maker_vol, insert_new_swap_to_db, is_pubkey_banned, lp_atomic_locktime, p2p_keypair_and_peer_id_to_broadcast, p2p_private_and_peer_id_to_broadcast, run_maker_swap, run_taker_swap, swap_v2_topic, AtomicLocktimeVersion, CheckBalanceError, CheckBalanceResult, - CoinVolumeInfo, MakerSwap, RunMakerSwapInput, RunTakerSwapInput, SecretHashAlgo, - SwapConfirmationsSettings, TakerSwap}; + CoinVolumeInfo, MakerSwap, RunMakerSwapInput, RunTakerSwapInput, SwapConfirmationsSettings, + TakerSwap}; #[cfg(any(test, feature = "run-docker-tests"))] use crate::mm2::lp_swap::taker_swap::FailAt; @@ -2947,11 +2951,8 @@ fn lp_connect_start_bob(ctx: MmArc, maker_match: MakerMatch, maker_order: MakerO ); let now = now_sec(); - if let Err(e) = insert_new_swap_to_db(ctx.clone(), maker_coin.ticker(), taker_coin.ticker(), uuid, now).await { - error!("Error {} on new swap insertion", e); - } - let secret = match MakerSwap::generate_secret() { + let secret = match generate_secret() { Ok(s) => s.into(), Err(e) => { error!("Error {} on secret generation", e); @@ -2960,35 +2961,44 @@ fn lp_connect_start_bob(ctx: MmArc, maker_match: MakerMatch, maker_order: MakerO }; if ctx.use_trading_proto_v2() { - match (maker_coin, taker_coin) { - (MmCoinEnum::UtxoCoin(m), MmCoinEnum::UtxoCoin(t)) => { - let mut maker_swap_state_machine = MakerSwapStateMachine { - ctx, - storage: DummyMakerSwapStorage::default(), - started_at: now_sec(), - maker_coin: m.clone(), - maker_volume: maker_amount, - secret, - taker_coin: t.clone(), - dex_fee_amount: dex_fee_amount_from_taker_coin(&t, m.ticker(), &taker_amount), - taker_volume: taker_amount, - taker_premium: Default::default(), - conf_settings: my_conf_settings, - p2p_topic: swap_v2_topic(&uuid), - uuid, - p2p_keypair: maker_order.p2p_privkey.map(SerializableSecp256k1Keypair::into_inner), - secret_hash_algo: SecretHashAlgo::DHASH160, - lock_duration: lock_time, - }; - #[allow(clippy::box_default)] - maker_swap_state_machine - .run(Box::new(maker_swap_v2::Initialize::default())) - .await - .error_log(); - }, - _ => todo!("implement fallback to the old protocol here"), + #[cfg(not(target_arch = "wasm32"))] + { + let secret_hash_algo = detect_secret_hash_algo(&maker_coin, &taker_coin); + match (maker_coin, taker_coin) { + (MmCoinEnum::UtxoCoin(m), MmCoinEnum::UtxoCoin(t)) => { + let mut maker_swap_state_machine = MakerSwapStateMachine { + storage: DummyMakerSwapStorage::new(ctx.clone()), + ctx, + started_at: now_sec(), + maker_coin: m.clone(), + maker_volume: maker_amount, + secret, + taker_coin: t.clone(), + dex_fee_amount: dex_fee_amount_from_taker_coin(&t, m.ticker(), &taker_amount), + taker_volume: taker_amount, + taker_premium: Default::default(), + conf_settings: my_conf_settings, + p2p_topic: swap_v2_topic(&uuid), + uuid, + p2p_keypair: maker_order.p2p_privkey.map(SerializableSecp256k1Keypair::into_inner), + secret_hash_algo, + lock_duration: lock_time, + }; + #[allow(clippy::box_default)] + maker_swap_state_machine + .run(Box::new(maker_swap_v2::Initialize::default())) + .await + .error_log(); + }, + _ => todo!("implement fallback to the old protocol here"), + } } } else { + if let Err(e) = + insert_new_swap_to_db(ctx.clone(), maker_coin.ticker(), taker_coin.ticker(), uuid, now).await + { + error!("Error {} on new swap insertion", e); + } let maker_swap = MakerSwap::new( ctx.clone(), alice, @@ -3090,41 +3100,56 @@ fn lp_connected_alice(ctx: MmArc, taker_order: TakerOrder, taker_match: TakerMat ); let now = now_sec(); - if let Err(e) = insert_new_swap_to_db(ctx.clone(), taker_coin.ticker(), maker_coin.ticker(), uuid, now).await { - error!("Error {} on new swap insertion", e); - } - if ctx.use_trading_proto_v2() { - match (maker_coin, taker_coin) { - (MmCoinEnum::UtxoCoin(m), MmCoinEnum::UtxoCoin(t)) => { - let mut taker_swap_state_machine = TakerSwapStateMachine { - ctx, - storage: DummyTakerSwapStorage::default(), - started_at: now_sec(), - lock_duration: locktime, - maker_coin: m.clone(), - maker_volume: maker_amount, - taker_coin: t.clone(), - dex_fee: dex_fee_amount_from_taker_coin(&t, maker_coin_ticker, &taker_amount), - taker_volume: taker_amount, - taker_premium: Default::default(), - conf_settings: my_conf_settings, - p2p_topic: swap_v2_topic(&uuid), - uuid, - p2p_keypair: taker_order.p2p_privkey.map(SerializableSecp256k1Keypair::into_inner), - }; - #[allow(clippy::box_default)] - taker_swap_state_machine - .run(Box::new(taker_swap_v2::Initialize::default())) - .await - .error_log(); - }, - _ => todo!("implement fallback to the old protocol here"), + #[cfg(not(target_arch = "wasm32"))] + { + let taker_secret = match generate_secret() { + Ok(s) => s.into(), + Err(e) => { + error!("Error {} on secret generation", e); + return; + }, + }; + let secret_hash_algo = detect_secret_hash_algo(&maker_coin, &taker_coin); + match (maker_coin, taker_coin) { + (MmCoinEnum::UtxoCoin(m), MmCoinEnum::UtxoCoin(t)) => { + let mut taker_swap_state_machine = TakerSwapStateMachine { + storage: DummyTakerSwapStorage::new(ctx.clone()), + ctx, + started_at: now, + lock_duration: locktime, + maker_coin: m.clone(), + maker_volume: maker_amount, + taker_coin: t.clone(), + dex_fee: dex_fee_amount_from_taker_coin(&t, maker_coin_ticker, &taker_amount), + taker_volume: taker_amount, + taker_premium: Default::default(), + secret_hash_algo, + conf_settings: my_conf_settings, + p2p_topic: swap_v2_topic(&uuid), + uuid, + p2p_keypair: taker_order.p2p_privkey.map(SerializableSecp256k1Keypair::into_inner), + taker_secret, + }; + #[allow(clippy::box_default)] + taker_swap_state_machine + .run(Box::new(taker_swap_v2::Initialize::default())) + .await + .error_log(); + }, + _ => todo!("implement fallback to the old protocol here"), + } } } else { #[cfg(any(test, feature = "run-docker-tests"))] let fail_at = std::env::var("TAKER_FAIL_AT").map(FailAt::from).ok(); + if let Err(e) = + insert_new_swap_to_db(ctx.clone(), taker_coin.ticker(), maker_coin.ticker(), uuid, now).await + { + error!("Error {} on new swap insertion", e); + } + let taker_swap = TakerSwap::new( ctx.clone(), maker, diff --git a/mm2src/mm2_main/src/lp_swap.rs b/mm2src/mm2_main/src/lp_swap.rs index c2620814a6..5cacc9458a 100644 --- a/mm2src/mm2_main/src/lp_swap.rs +++ b/mm2src/mm2_main/src/lp_swap.rs @@ -92,7 +92,9 @@ use std::sync::atomic::{AtomicU64, Ordering}; #[path = "lp_swap/check_balance.rs"] mod check_balance; #[path = "lp_swap/maker_swap.rs"] mod maker_swap; -#[path = "lp_swap/maker_swap_v2.rs"] pub mod maker_swap_v2; +#[cfg(not(target_arch = "wasm32"))] +#[path = "lp_swap/maker_swap_v2.rs"] +pub mod maker_swap_v2; #[path = "lp_swap/max_maker_vol_rpc.rs"] mod max_maker_vol_rpc; #[path = "lp_swap/my_swaps_storage.rs"] mod my_swaps_storage; #[path = "lp_swap/pubkey_banning.rs"] mod pubkey_banning; @@ -106,13 +108,17 @@ mod swap_v2_pb; #[path = "lp_swap/taker_restart.rs"] pub(crate) mod taker_restart; #[path = "lp_swap/taker_swap.rs"] pub(crate) mod taker_swap; -#[path = "lp_swap/taker_swap_v2.rs"] pub mod taker_swap_v2; +#[cfg(not(target_arch = "wasm32"))] +#[path = "lp_swap/taker_swap_v2.rs"] +pub mod taker_swap_v2; #[path = "lp_swap/trade_preimage.rs"] mod trade_preimage; #[cfg(target_arch = "wasm32")] #[path = "lp_swap/swap_wasm_db.rs"] mod swap_wasm_db; +#[cfg(not(target_arch = "wasm32"))] +use crate::mm2::database::my_swaps::{get_swap_data_for_rpc, get_swap_type}; pub use check_balance::{check_other_coin_balance_for_swap, CheckBalanceError, CheckBalanceResult}; use crypto::CryptoCtx; use keys::{KeyPair, SECP_SIGN, SECP_VERIFY}; @@ -142,6 +148,11 @@ pub const SWAP_V2_PREFIX: TopicPrefix = "swapv2"; pub const SWAP_FINISHED_LOG: &str = "Swap finished: "; pub const TX_HELPER_PREFIX: TopicPrefix = "txhlp"; +const LEGACY_SWAP_TYPE: u8 = 0; +const MAKER_SWAP_V2_TYPE: u8 = 1; +const TAKER_SWAP_V2_TYPE: u8 = 2; +const MAX_STARTED_AT_DIFF: u64 = 60; + const NEGOTIATE_SEND_INTERVAL: f64 = 30.; /// If a certain P2P message is not received, swap will be aborted after this time expires. @@ -190,8 +201,9 @@ pub struct SwapV2MsgStore { maker_negotiation: Option, taker_negotiation: Option, maker_negotiated: Option, - taker_payment: Option, + taker_funding: Option, maker_payment: Option, + taker_payment: Option, taker_payment_spend_preimage: Option, #[allow(dead_code)] accept_only_from: bits256, @@ -1000,6 +1012,35 @@ impl From for MySwapStatusResponse { } /// Returns the status of swap performed on `my` node +#[cfg(not(target_arch = "wasm32"))] +pub async fn my_swap_status(ctx: MmArc, req: Json) -> Result>, String> { + let uuid: Uuid = try_s!(json::from_value(req["params"]["uuid"].clone())); + let uuid_str = uuid.to_string(); + let swap_type = try_s!(get_swap_type(&ctx.sqlite_connection(), &uuid_str)); + + match swap_type { + LEGACY_SWAP_TYPE => { + let status = match SavedSwap::load_my_swap_from_db(&ctx, uuid).await { + Ok(Some(status)) => status, + Ok(None) => return Err("swap data is not found".to_owned()), + Err(e) => return ERR!("{}", e), + }; + + let res_js = json!({ "result": MySwapStatusResponse::from(status) }); + let res = try_s!(json::to_vec(&res_js)); + Ok(try_s!(Response::builder().body(res))) + }, + MAKER_SWAP_V2_TYPE | TAKER_SWAP_V2_TYPE => { + let swap_data = try_s!(get_swap_data_for_rpc(&ctx.sqlite_connection(), &uuid_str)); + let res_js = json!({ "result": swap_data }); + let res = try_s!(json::to_vec(&res_js)); + Ok(try_s!(Response::builder().body(res))) + }, + unsupported_type => ERR!("Got unsupported swap type from DB: {}", unsupported_type), + } +} + +#[cfg(target_arch = "wasm32")] pub async fn my_swap_status(ctx: MmArc, req: Json) -> Result>, String> { let uuid: Uuid = try_s!(json::from_value(req["params"]["uuid"].clone())); let status = match SavedSwap::load_my_swap_from_db(&ctx, uuid).await { @@ -1406,11 +1447,12 @@ pub async fn active_swaps_rpc(ctx: MmArc, req: Json) -> Result> } /// Algorithm used to hash swap secret. +#[derive(Clone, Copy)] pub enum SecretHashAlgo { /// ripemd160(sha256(secret)) - DHASH160, + DHASH160 = 1, /// sha256(secret) - SHA256, + SHA256 = 2, } impl Default for SecretHashAlgo { @@ -1427,8 +1469,9 @@ impl SecretHashAlgo { } // Todo: Maybe add a secret_hash_algo method to the SwapOps trait instead +/// Selects secret hash algorithm depending on types of coins being swapped #[cfg(not(target_arch = "wasm32"))] -fn detect_secret_hash_algo(maker_coin: &MmCoinEnum, taker_coin: &MmCoinEnum) -> SecretHashAlgo { +pub fn detect_secret_hash_algo(maker_coin: &MmCoinEnum, taker_coin: &MmCoinEnum) -> SecretHashAlgo { match (maker_coin, taker_coin) { (MmCoinEnum::Tendermint(_) | MmCoinEnum::TendermintToken(_) | MmCoinEnum::LightningCoin(_), _) => { SecretHashAlgo::SHA256 @@ -1439,6 +1482,7 @@ fn detect_secret_hash_algo(maker_coin: &MmCoinEnum, taker_coin: &MmCoinEnum) -> } } +/// Selects secret hash algorithm depending on types of coins being swapped #[cfg(target_arch = "wasm32")] fn detect_secret_hash_algo(maker_coin: &MmCoinEnum, taker_coin: &MmCoinEnum) -> SecretHashAlgo { match (maker_coin, taker_coin) { @@ -1535,12 +1579,15 @@ pub fn process_swap_v2_msg(ctx: MmArc, topic: &str, msg: &[u8]) -> P2PProcessRes Some(swap_v2_pb::swap_message::Inner::MakerNegotiated(maker_negotiated)) => { msg_store.maker_negotiated = Some(maker_negotiated) }, - Some(swap_v2_pb::swap_message::Inner::TakerPaymentInfo(taker_payment)) => { - msg_store.taker_payment = Some(taker_payment) + Some(swap_v2_pb::swap_message::Inner::TakerFundingInfo(taker_funding)) => { + msg_store.taker_funding = Some(taker_funding) }, Some(swap_v2_pb::swap_message::Inner::MakerPaymentInfo(maker_payment)) => { msg_store.maker_payment = Some(maker_payment) }, + Some(swap_v2_pb::swap_message::Inner::TakerPaymentInfo(taker_payment)) => { + msg_store.taker_payment = Some(taker_payment) + }, Some(swap_v2_pb::swap_message::Inner::TakerPaymentSpendPreimage(preimage)) => { msg_store.taker_payment_spend_preimage = Some(preimage) }, @@ -1575,6 +1622,12 @@ async fn recv_swap_v2_msg( } } +pub fn generate_secret() -> Result<[u8; 32], rand::Error> { + let mut sec = [0u8; 32]; + common::os_rng(&mut sec)?; + Ok(sec) +} + #[cfg(all(test, not(target_arch = "wasm32")))] mod lp_swap_tests { use super::*; diff --git a/mm2src/mm2_main/src/lp_swap/komodefi.swap_v2.pb.rs b/mm2src/mm2_main/src/lp_swap/komodefi.swap_v2.pb.rs index d677f903a0..761fabd0e3 100644 --- a/mm2src/mm2_main/src/lp_swap/komodefi.swap_v2.pb.rs +++ b/mm2src/mm2_main/src/lp_swap/komodefi.swap_v2.pb.rs @@ -34,15 +34,18 @@ pub struct TakerNegotiationData { #[prost(uint64, tag="1")] pub started_at: u64, #[prost(uint64, tag="2")] + pub funding_locktime: u64, + #[prost(uint64, tag="3")] pub payment_locktime: u64, - /// add bytes secret_hash = 3 if required #[prost(bytes="vec", tag="4")] - pub maker_coin_htlc_pub: ::prost::alloc::vec::Vec, + pub taker_secret_hash: ::prost::alloc::vec::Vec, #[prost(bytes="vec", tag="5")] + pub maker_coin_htlc_pub: ::prost::alloc::vec::Vec, + #[prost(bytes="vec", tag="6")] pub taker_coin_htlc_pub: ::prost::alloc::vec::Vec, - #[prost(bytes="vec", optional, tag="6")] - pub maker_coin_swap_contract: ::core::option::Option<::prost::alloc::vec::Vec>, #[prost(bytes="vec", optional, tag="7")] + pub maker_coin_swap_contract: ::core::option::Option<::prost::alloc::vec::Vec>, + #[prost(bytes="vec", optional, tag="8")] pub taker_coin_swap_contract: ::core::option::Option<::prost::alloc::vec::Vec>, } #[derive(Clone, PartialEq, ::prost::Message)] @@ -69,6 +72,13 @@ pub struct MakerNegotiated { pub reason: ::core::option::Option<::prost::alloc::string::String>, } #[derive(Clone, PartialEq, ::prost::Message)] +pub struct TakerFundingInfo { + #[prost(bytes="vec", tag="1")] + pub tx_bytes: ::prost::alloc::vec::Vec, + #[prost(bytes="vec", optional, tag="2")] + pub next_step_instructions: ::core::option::Option<::prost::alloc::vec::Vec>, +} +#[derive(Clone, PartialEq, ::prost::Message)] pub struct TakerPaymentInfo { #[prost(bytes="vec", tag="1")] pub tx_bytes: ::prost::alloc::vec::Vec, @@ -81,17 +91,21 @@ pub struct MakerPaymentInfo { pub tx_bytes: ::prost::alloc::vec::Vec, #[prost(bytes="vec", optional, tag="2")] pub next_step_instructions: ::core::option::Option<::prost::alloc::vec::Vec>, + #[prost(bytes="vec", tag="3")] + pub funding_preimage_sig: ::prost::alloc::vec::Vec, + #[prost(bytes="vec", tag="4")] + pub funding_preimage_tx: ::prost::alloc::vec::Vec, } #[derive(Clone, PartialEq, ::prost::Message)] pub struct TakerPaymentSpendPreimage { #[prost(bytes="vec", tag="1")] pub signature: ::prost::alloc::vec::Vec, - #[prost(bytes="vec", optional, tag="2")] - pub tx_preimage: ::core::option::Option<::prost::alloc::vec::Vec>, + #[prost(bytes="vec", tag="2")] + pub tx_preimage: ::prost::alloc::vec::Vec, } #[derive(Clone, PartialEq, ::prost::Message)] pub struct SwapMessage { - #[prost(oneof="swap_message::Inner", tags="1, 2, 3, 4, 5, 6")] + #[prost(oneof="swap_message::Inner", tags="1, 2, 3, 4, 5, 6, 7")] pub inner: ::core::option::Option, } /// Nested message and enum types in `SwapMessage`. @@ -105,10 +119,12 @@ pub mod swap_message { #[prost(message, tag="3")] MakerNegotiated(super::MakerNegotiated), #[prost(message, tag="4")] - TakerPaymentInfo(super::TakerPaymentInfo), + TakerFundingInfo(super::TakerFundingInfo), #[prost(message, tag="5")] MakerPaymentInfo(super::MakerPaymentInfo), #[prost(message, tag="6")] + TakerPaymentInfo(super::TakerPaymentInfo), + #[prost(message, tag="7")] TakerPaymentSpendPreimage(super::TakerPaymentSpendPreimage), } } diff --git a/mm2src/mm2_main/src/lp_swap/maker_swap.rs b/mm2src/mm2_main/src/lp_swap/maker_swap.rs index 7c51fe0c30..67f1b6285a 100644 --- a/mm2src/mm2_main/src/lp_swap/maker_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/maker_swap.rs @@ -13,7 +13,7 @@ use super::{broadcast_my_swap_status, broadcast_p2p_tx_msg, broadcast_swap_msg_e use crate::mm2::lp_dispatcher::{DispatcherContext, LpEvents}; use crate::mm2::lp_network::subscribe_to_topic; use crate::mm2::lp_ordermatch::MakerOrderBuilder; -use crate::mm2::lp_swap::{broadcast_swap_message, taker_payment_spend_duration}; +use crate::mm2::lp_swap::{broadcast_swap_message, taker_payment_spend_duration, MAX_STARTED_AT_DIFF}; use coins::lp_price::fetch_swap_coins_price; use coins::{CanRefundHtlc, CheckIfMyPaymentSentArgs, ConfirmPaymentInput, FeeApproxStage, FoundSwapTxSpend, MmCoin, MmCoinEnum, PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, RefundPaymentArgs, @@ -239,12 +239,6 @@ impl MakerSwap { #[inline] fn r(&self) -> RwLockReadGuard { self.mutable.read().unwrap() } - pub fn generate_secret() -> Result<[u8; 32], rand::Error> { - let mut sec = [0u8; 32]; - common::os_rng(&mut sec)?; - Ok(sec) - } - #[inline] fn secret_hash(&self) -> Vec { self.r() @@ -613,7 +607,7 @@ impl MakerSwap { }; drop(send_abort_handle); let time_dif = self.r().data.started_at.abs_diff(taker_data.started_at()); - if time_dif > 60 { + if time_dif > MAX_STARTED_AT_DIFF { self.broadcast_negotiated_false(); return Ok((Some(MakerSwapCommand::Finish), vec![MakerSwapEvent::NegotiateFailed( ERRL!("The time difference between you and the taker cannot be longer than 60 seconds. Current difference: {}. Please make sure that your system clock is synced to the correct time before starting another swap!", time_dif).into(), diff --git a/mm2src/mm2_main/src/lp_swap/maker_swap_v2.rs b/mm2src/mm2_main/src/lp_swap/maker_swap_v2.rs index eb601df2e2..097e71d9c3 100644 --- a/mm2src/mm2_main/src/lp_swap/maker_swap_v2.rs +++ b/mm2src/mm2_main/src/lp_swap/maker_swap_v2.rs @@ -1,58 +1,93 @@ use super::{NEGOTIATE_SEND_INTERVAL, NEGOTIATION_TIMEOUT_SEC}; +use crate::mm2::database::my_swaps::{get_swap_events, insert_new_swap_v2, set_swap_is_finished, update_swap_events}; use crate::mm2::lp_network::subscribe_to_topic; use crate::mm2::lp_swap::swap_v2_pb::*; use crate::mm2::lp_swap::{broadcast_swap_v2_msg_every, check_balance_for_maker_swap, recv_swap_v2_msg, SecretHashAlgo, - SwapConfirmationsSettings, SwapsContext, TransactionIdentifier}; + SwapConfirmationsSettings, SwapsContext, TransactionIdentifier, MAKER_SWAP_V2_TYPE, + MAX_STARTED_AT_DIFF}; use async_trait::async_trait; use bitcrypto::{dhash160, sha256}; -use coins::{ConfirmPaymentInput, FeeApproxStage, GenTakerPaymentSpendArgs, MarketCoinOps, MmCoin, SendPaymentArgs, - SwapOpsV2, TxPreimageWithSig}; +use coins::{CoinAssocTypes, ConfirmPaymentInput, FeeApproxStage, GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, + MarketCoinOps, MmCoin, SendPaymentArgs, SwapOpsV2, ToBytes, Transaction, TxPreimageWithSig, + ValidateTakerFundingArgs}; use common::log::{debug, info, warn}; use common::{bits256, Future01CompatExt, DEX_FEE_ADDR_RAW_PUBKEY}; +use db_common::sqlite::rusqlite::named_params; use keys::KeyPair; use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; use mm2_number::MmNumber; use mm2_state_machine::prelude::*; use mm2_state_machine::storable_state_machine::*; use primitives::hash::H256; -use std::collections::HashMap; +use rpc::v1::types::Bytes as BytesJson; use std::marker::PhantomData; use uuid::Uuid; // This is needed to have Debug on messages #[allow(unused_imports)] use prost::Message; +/// Negotiation data representation to be stored in DB. +#[derive(Debug, Deserialize, Serialize)] +pub struct StoredNegotiationData { + taker_payment_locktime: u64, + maker_coin_htlc_pub_from_taker: BytesJson, + taker_coin_htlc_pub_from_taker: BytesJson, + maker_coin_swap_contract: Option, + taker_coin_swap_contract: Option, + taker_secret_hash: BytesJson, +} + /// Represents events produced by maker swap states. -#[derive(Debug, PartialEq)] +#[derive(Debug, Deserialize, Serialize)] pub enum MakerSwapEvent { /// Swap has been successfully initialized. Initialized { maker_coin_start_block: u64, taker_coin_start_block: u64, }, - /// Started waiting for taker payment. - WaitingForTakerPayment { + /// Started waiting for taker funding tx. + WaitingForTakerFunding { maker_coin_start_block: u64, taker_coin_start_block: u64, + negotiation_data: StoredNegotiationData, }, - /// Received taker payment info. - TakerPaymentReceived { + /// Received taker funding info. + TakerFundingReceived { maker_coin_start_block: u64, taker_coin_start_block: u64, - taker_payment: TransactionIdentifier, + negotiation_data: StoredNegotiationData, + taker_funding: TransactionIdentifier, }, /// Sent maker payment. MakerPaymentSent { maker_coin_start_block: u64, taker_coin_start_block: u64, + negotiation_data: StoredNegotiationData, maker_payment: TransactionIdentifier, }, + /// Received funding spend preimage. + TakerFundingSpendReceived { + maker_coin_start_block: u64, + taker_coin_start_block: u64, + negotiation_data: StoredNegotiationData, + taker_funding: TransactionIdentifier, + maker_payment: TransactionIdentifier, + taker_funding_preimage: BytesJson, + taker_funding_spend_signature: BytesJson, + }, /// Something went wrong, so maker payment refund is required. - MakerPaymentRefundRequired { maker_payment: TransactionIdentifier }, + MakerPaymentRefundRequired { + maker_coin_start_block: u64, + taker_coin_start_block: u64, + negotiation_data: StoredNegotiationData, + maker_payment: TransactionIdentifier, + }, /// Taker payment has been confirmed on-chain. TakerPaymentConfirmed { maker_coin_start_block: u64, taker_coin_start_block: u64, + negotiation_data: StoredNegotiationData, maker_payment: TransactionIdentifier, taker_payment: TransactionIdentifier, }, @@ -65,37 +100,54 @@ pub enum MakerSwapEvent { taker_payment_spend: TransactionIdentifier, }, /// Swap has been aborted before maker payment was sent. - Aborted { reason: String }, + Aborted { reason: AbortReason }, /// Swap completed successfully. Completed, } /// Represents errors that can be produced by [`MakerSwapStateMachine`] run. #[derive(Debug, Display)] -pub enum MakerSwapStateMachineError {} +pub enum MakerSwapStateMachineError { + StorageError(String), + SerdeError(String), +} /// Dummy storage for maker swap events (used temporary). -#[derive(Default)] pub struct DummyMakerSwapStorage { - events: HashMap>, + ctx: MmArc, +} + +impl DummyMakerSwapStorage { + pub fn new(ctx: MmArc) -> Self { DummyMakerSwapStorage { ctx } } } #[async_trait] impl StateMachineStorage for DummyMakerSwapStorage { type MachineId = Uuid; type Event = MakerSwapEvent; - type Error = MakerSwapStateMachineError; + type Error = MmError; async fn store_event(&mut self, id: Self::MachineId, event: Self::Event) -> Result<(), Self::Error> { - self.events.entry(id).or_insert_with(Vec::new).push(event); + let id_str = id.to_string(); + let events_json = get_swap_events(&self.ctx.sqlite_connection(), &id_str) + .map_to_mm(|e| MakerSwapStateMachineError::StorageError(e.to_string()))?; + let mut events: Vec = + serde_json::from_str(&events_json).map_to_mm(|e| MakerSwapStateMachineError::SerdeError(e.to_string()))?; + events.push(event); + drop_mutability!(events); + let serialized_events = + serde_json::to_string(&events).map_to_mm(|e| MakerSwapStateMachineError::SerdeError(e.to_string()))?; + update_swap_events(&self.ctx.sqlite_connection(), &id_str, &serialized_events) + .map_to_mm(|e| MakerSwapStateMachineError::StorageError(e.to_string()))?; Ok(()) } - async fn get_unfinished(&self) -> Result, Self::Error> { - Ok(self.events.keys().copied().collect()) - } + async fn get_unfinished(&self) -> Result, Self::Error> { todo!() } - async fn mark_finished(&mut self, _id: Self::MachineId) -> Result<(), Self::Error> { Ok(()) } + async fn mark_finished(&mut self, id: Self::MachineId) -> Result<(), Self::Error> { + set_swap_is_finished(&self.ctx.sqlite_connection(), &id.to_string()) + .map_to_mm(|e| MakerSwapStateMachineError::StorageError(e.to_string())) + } } /// Represents the state machine for maker's side of the Trading Protocol Upgrade swap (v2). @@ -194,24 +246,52 @@ impl InitialState for Init } #[async_trait] -impl State - for Initialize -{ +impl State for Initialize { type StateMachine = MakerSwapStateMachine; async fn on_changed(self: Box, state_machine: &mut Self::StateMachine) -> StateResult { + { + let sql_params = named_params! { + ":my_coin": state_machine.maker_coin.ticker(), + ":other_coin": state_machine.taker_coin.ticker(), + ":uuid": state_machine.uuid.to_string(), + ":started_at": state_machine.started_at, + ":swap_type": MAKER_SWAP_V2_TYPE, + ":maker_volume": state_machine.maker_volume.to_fraction_string(), + ":taker_volume": state_machine.taker_volume.to_fraction_string(), + ":premium": state_machine.taker_premium.to_fraction_string(), + ":dex_fee": state_machine.dex_fee_amount.to_fraction_string(), + ":secret": state_machine.secret.take(), + ":secret_hash": state_machine.secret_hash(), + ":secret_hash_algo": state_machine.secret_hash_algo as u8, + ":p2p_privkey": state_machine.p2p_keypair.map(|k| k.private_bytes()).unwrap_or_default(), + ":lock_duration": state_machine.lock_duration, + ":maker_coin_confs": state_machine.conf_settings.maker_coin_confs, + ":maker_coin_nota": state_machine.conf_settings.maker_coin_nota, + ":taker_coin_confs": state_machine.conf_settings.taker_coin_confs, + ":taker_coin_nota": state_machine.conf_settings.taker_coin_nota + }; + insert_new_swap_v2(&state_machine.ctx, sql_params).unwrap(); + } + subscribe_to_topic(&state_machine.ctx, state_machine.p2p_topic.clone()); let swap_ctx = SwapsContext::from_ctx(&state_machine.ctx).expect("SwapsContext::from_ctx should not fail"); swap_ctx.init_msg_v2_store(state_machine.uuid, bits256::default()); let maker_coin_start_block = match state_machine.maker_coin.current_block().compat().await { Ok(b) => b, - Err(e) => return Self::change_state(Aborted::new(e), state_machine).await, + Err(e) => { + let reason = AbortReason::FailedToGetMakerCoinBlock(e); + return Self::change_state(Aborted::new(reason), state_machine).await; + }, }; let taker_coin_start_block = match state_machine.taker_coin.current_block().compat().await { Ok(b) => b, - Err(e) => return Self::change_state(Aborted::new(e), state_machine).await, + Err(e) => { + let reason = AbortReason::FailedToGetTakerCoinBlock(e); + return Self::change_state(Aborted::new(reason), state_machine).await; + }, }; if let Err(e) = check_balance_for_maker_swap( @@ -225,7 +305,8 @@ impl StorableState for Ini } #[async_trait] -impl State for Initialized { +impl State for Initialized { type StateMachine = MakerSwapStateMachine; async fn on_changed(self: Box, state_machine: &mut Self::StateMachine) -> StateResult { @@ -296,8 +377,8 @@ impl State for Initialized d, Err(e) => { - let next_state = Aborted::new(format!("Failed to receive TakerNegotiation: {}", e)); - return Self::change_state(next_state, state_machine).await; + let reason = AbortReason::DidNotReceiveTakerNegotiation(e); + return Self::change_state(Aborted::new(reason), state_machine).await; }, }; drop(abort_handle); @@ -306,49 +387,106 @@ impl State for Initialized data, Some(taker_negotiation::Action::Abort(abort)) => { - let next_state = Aborted::new(abort.reason); - return Self::change_state(next_state, state_machine).await; + let reason = AbortReason::TakerAbortedNegotiation(abort.reason); + return Self::change_state(Aborted::new(reason), state_machine).await; }, None => { - let next_state = Aborted::new("received invalid negotiation message from taker".into()); - return Self::change_state(next_state, state_machine).await; + let reason = AbortReason::ReceivedInvalidTakerNegotiation; + return Self::change_state(Aborted::new(reason), state_machine).await; }, }; - let next_state = WaitingForTakerPayment { - maker_coin: Default::default(), - taker_coin: Default::default(), + let started_at_diff = state_machine.started_at.abs_diff(taker_data.started_at); + if started_at_diff > MAX_STARTED_AT_DIFF { + let reason = AbortReason::TooLargeStartedAtDiff(started_at_diff); + return Self::change_state(Aborted::new(reason), state_machine).await; + } + + let expected_taker_funding_locktime = taker_data.started_at + 3 * state_machine.lock_duration; + if taker_data.funding_locktime != expected_taker_funding_locktime { + let reason = AbortReason::TakerProvidedInvalidFundingLocktime(taker_data.funding_locktime); + return Self::change_state(Aborted::new(reason), state_machine).await; + } + + let expected_taker_payment_locktime = taker_data.started_at + state_machine.lock_duration; + if taker_data.payment_locktime != expected_taker_payment_locktime { + let reason = AbortReason::TakerProvidedInvalidPaymentLocktime(taker_data.payment_locktime); + return Self::change_state(Aborted::new(reason), state_machine).await; + } + + let taker_coin_htlc_pub_from_taker = + match state_machine.taker_coin.parse_pubkey(&taker_data.taker_coin_htlc_pub) { + Ok(p) => p, + Err(e) => { + let reason = AbortReason::FailedToParsePubkey(e.to_string()); + return Self::change_state(Aborted::new(reason), state_machine).await; + }, + }; + + let maker_coin_htlc_pub_from_taker = + match state_machine.maker_coin.parse_pubkey(&taker_data.maker_coin_htlc_pub) { + Ok(p) => p, + Err(e) => { + let reason = AbortReason::FailedToParsePubkey(e.to_string()); + return Self::change_state(Aborted::new(reason), state_machine).await; + }, + }; + + let next_state = WaitingForTakerFunding { maker_coin_start_block: self.maker_coin_start_block, taker_coin_start_block: self.taker_coin_start_block, - taker_payment_locktime: taker_data.payment_locktime, - maker_coin_htlc_pub_from_taker: taker_data.maker_coin_htlc_pub, - taker_coin_htlc_pub_from_taker: taker_data.taker_coin_htlc_pub, - maker_coin_swap_contract: taker_data.maker_coin_swap_contract, - taker_coin_swap_contract: taker_data.taker_coin_swap_contract, + negotiation_data: NegotiationData { + taker_payment_locktime: expected_taker_payment_locktime, + taker_funding_locktime: expected_taker_funding_locktime, + maker_coin_htlc_pub_from_taker, + taker_coin_htlc_pub_from_taker, + maker_coin_swap_contract: taker_data.maker_coin_swap_contract, + taker_coin_swap_contract: taker_data.taker_coin_swap_contract, + taker_secret_hash: taker_data.taker_secret_hash, + }, }; Self::change_state(next_state, state_machine).await } } -struct WaitingForTakerPayment { - maker_coin: PhantomData, - taker_coin: PhantomData, - maker_coin_start_block: u64, - taker_coin_start_block: u64, +struct NegotiationData { taker_payment_locktime: u64, - maker_coin_htlc_pub_from_taker: Vec, - taker_coin_htlc_pub_from_taker: Vec, + taker_funding_locktime: u64, + maker_coin_htlc_pub_from_taker: MakerCoin::Pubkey, + taker_coin_htlc_pub_from_taker: TakerCoin::Pubkey, maker_coin_swap_contract: Option>, taker_coin_swap_contract: Option>, + taker_secret_hash: Vec, +} + +impl NegotiationData { + fn to_stored_data(&self) -> StoredNegotiationData { + StoredNegotiationData { + taker_payment_locktime: self.taker_payment_locktime, + maker_coin_htlc_pub_from_taker: self.maker_coin_htlc_pub_from_taker.to_bytes().into(), + taker_coin_htlc_pub_from_taker: self.taker_coin_htlc_pub_from_taker.to_bytes().into(), + maker_coin_swap_contract: self.maker_coin_swap_contract.clone().map(|b| b.into()), + taker_coin_swap_contract: self.taker_coin_swap_contract.clone().map(|b| b.into()), + taker_secret_hash: self.taker_secret_hash.clone().into(), + } + } +} + +struct WaitingForTakerFunding { + maker_coin_start_block: u64, + taker_coin_start_block: u64, + negotiation_data: NegotiationData, } -impl TransitionFrom> - for WaitingForTakerPayment +impl TransitionFrom> + for WaitingForTakerFunding { } #[async_trait] -impl State for WaitingForTakerPayment { +impl State + for WaitingForTakerFunding +{ type StateMachine = MakerSwapStateMachine; async fn on_changed(self: Box, state_machine: &mut Self::StateMachine) -> StateResult { @@ -370,83 +508,117 @@ impl State for WaitingForTaker let recv_fut = recv_swap_v2_msg( state_machine.ctx.clone(), - |store| store.taker_payment.take(), + |store| store.taker_funding.take(), &state_machine.uuid, NEGOTIATION_TIMEOUT_SEC, ); - let taker_payment = match recv_fut.await { + let taker_funding_info = match recv_fut.await { Ok(p) => p, Err(e) => { - let next_state = Aborted::new(format!("Failed to receive TakerPaymentInfo: {}", e)); - return Self::change_state(next_state, state_machine).await; + let reason = AbortReason::DidNotReceiveTakerFundingInfo(e); + return Self::change_state(Aborted::new(reason), state_machine).await; }, }; drop(abort_handle); - debug!("Received taker payment info message {:?}", taker_payment); - let next_state = TakerPaymentReceived { - maker_coin: Default::default(), - taker_coin: Default::default(), + debug!("Received taker funding info message {:?}", taker_funding_info); + let taker_funding = match state_machine.taker_coin.parse_tx(&taker_funding_info.tx_bytes) { + Ok(tx) => tx, + Err(e) => { + let reason = AbortReason::FailedToParseTakerFunding(e.to_string()); + return Self::change_state(Aborted::new(reason), state_machine).await; + }, + }; + let next_state = TakerFundingReceived { maker_coin_start_block: self.maker_coin_start_block, taker_coin_start_block: self.taker_coin_start_block, - taker_payment_locktime: self.taker_payment_locktime, - maker_coin_htlc_pub_from_taker: self.maker_coin_htlc_pub_from_taker, - taker_coin_htlc_pub_from_taker: self.taker_coin_htlc_pub_from_taker, - maker_coin_swap_contract: self.maker_coin_swap_contract, - taker_coin_swap_contract: self.taker_coin_swap_contract, - taker_payment: TransactionIdentifier { - tx_hex: taker_payment.tx_bytes.into(), - tx_hash: Default::default(), - }, + negotiation_data: self.negotiation_data, + taker_funding, }; Self::change_state(next_state, state_machine).await } } -impl StorableState - for WaitingForTakerPayment +impl StorableState + for WaitingForTakerFunding { type StateMachine = MakerSwapStateMachine; fn get_event(&self) -> <::Storage as StateMachineStorage>::Event { - MakerSwapEvent::WaitingForTakerPayment { + MakerSwapEvent::WaitingForTakerFunding { maker_coin_start_block: self.maker_coin_start_block, taker_coin_start_block: self.taker_coin_start_block, + negotiation_data: self.negotiation_data.to_stored_data(), } } } -struct TakerPaymentReceived { - maker_coin: PhantomData, - taker_coin: PhantomData, +struct TakerFundingReceived { maker_coin_start_block: u64, taker_coin_start_block: u64, - taker_payment_locktime: u64, - maker_coin_htlc_pub_from_taker: Vec, - taker_coin_htlc_pub_from_taker: Vec, - maker_coin_swap_contract: Option>, - taker_coin_swap_contract: Option>, - taker_payment: TransactionIdentifier, + negotiation_data: NegotiationData, + taker_funding: TakerCoin::Tx, } -impl TransitionFrom> - for TakerPaymentReceived +impl TransitionFrom> + for TakerFundingReceived { } #[async_trait] -impl State for TakerPaymentReceived { +impl State + for TakerFundingReceived +{ type StateMachine = MakerSwapStateMachine; async fn on_changed(self: Box, state_machine: &mut Self::StateMachine) -> StateResult { + let unique_data = state_machine.unique_data(); + + let validation_args = ValidateTakerFundingArgs { + funding_tx: &self.taker_funding, + time_lock: self.negotiation_data.taker_funding_locktime, + taker_secret_hash: &self.negotiation_data.taker_secret_hash, + other_pub: &self.negotiation_data.taker_coin_htlc_pub_from_taker, + dex_fee_amount: state_machine.dex_fee_amount.to_decimal(), + premium_amount: state_machine.taker_premium.to_decimal(), + trading_amount: state_machine.taker_volume.to_decimal(), + swap_unique_data: &unique_data, + }; + + if let Err(e) = state_machine.taker_coin.validate_taker_funding(validation_args).await { + let reason = AbortReason::TakerFundingValidationFailed(e.to_string()); + return Self::change_state(Aborted::new(reason), state_machine).await; + } + + let args = GenTakerFundingSpendArgs { + funding_tx: &self.taker_funding, + maker_pub: &state_machine.taker_coin.derive_htlc_pubkey_v2(&unique_data), + taker_pub: &self.negotiation_data.taker_coin_htlc_pub_from_taker, + funding_time_lock: self.negotiation_data.taker_funding_locktime, + taker_secret_hash: &self.negotiation_data.taker_secret_hash, + taker_payment_time_lock: self.negotiation_data.taker_payment_locktime, + maker_secret_hash: &state_machine.secret_hash(), + }; + let funding_spend_preimage = match state_machine + .taker_coin + .gen_taker_funding_spend_preimage(&args, &unique_data) + .await + { + Ok(p) => p, + Err(e) => { + let reason = AbortReason::FailedToGenerateFundingSpend(e.to_string()); + return Self::change_state(Aborted::new(reason), state_machine).await; + }, + }; + let args = SendPaymentArgs { time_lock_duration: state_machine.lock_duration, time_lock: state_machine.maker_payment_locktime(), - other_pubkey: &self.maker_coin_htlc_pub_from_taker, + other_pubkey: &self.negotiation_data.maker_coin_htlc_pub_from_taker.to_bytes(), secret_hash: &state_machine.secret_hash(), amount: state_machine.maker_volume.to_decimal(), swap_contract_address: &None, - swap_unique_data: &state_machine.unique_data(), + swap_unique_data: &unique_data, payment_instructions: &None, watcher_reward: None, wait_for_confirmation_until: 0, @@ -454,8 +626,8 @@ impl State for TakerPaymentRec let maker_payment = match state_machine.maker_coin.send_maker_payment(args).compat().await { Ok(tx) => tx, Err(e) => { - let next_state = Aborted::new(format!("Failed to send maker payment {:?}", e)); - return Self::change_state(next_state, state_machine).await; + let reason = AbortReason::FailedToSendMakerPayment(format!("{:?}", e)); + return Self::change_state(Aborted::new(reason), state_machine).await; }, }; info!( @@ -464,17 +636,11 @@ impl State for TakerPaymentRec maker_payment.tx_hash(), state_machine.uuid ); - let next_state = MakerPaymentSent { - maker_coin: Default::default(), - taker_coin: Default::default(), + let next_state = MakerPaymentSentFundingSpendGenerated { maker_coin_start_block: self.maker_coin_start_block, taker_coin_start_block: self.taker_coin_start_block, - taker_payment_locktime: self.taker_payment_locktime, - maker_coin_htlc_pub_from_taker: self.maker_coin_htlc_pub_from_taker, - taker_coin_htlc_pub_from_taker: self.taker_coin_htlc_pub_from_taker, - maker_coin_swap_contract: self.maker_coin_swap_contract, - taker_coin_swap_contract: self.taker_coin_swap_contract, - taker_payment: self.taker_payment, + negotiation_data: self.negotiation_data, + funding_spend_preimage, maker_payment: TransactionIdentifier { tx_hex: maker_payment.tx_hex().into(), tx_hash: maker_payment.tx_hash(), @@ -485,62 +651,100 @@ impl State for TakerPaymentRec } } -impl StorableState - for TakerPaymentReceived +impl StorableState + for TakerFundingReceived { type StateMachine = MakerSwapStateMachine; fn get_event(&self) -> <::Storage as StateMachineStorage>::Event { - MakerSwapEvent::TakerPaymentReceived { + MakerSwapEvent::TakerFundingReceived { maker_coin_start_block: self.maker_coin_start_block, taker_coin_start_block: self.taker_coin_start_block, - taker_payment: self.taker_payment.clone(), + negotiation_data: self.negotiation_data.to_stored_data(), + taker_funding: TransactionIdentifier { + tx_hex: self.taker_funding.tx_hex().into(), + tx_hash: self.taker_funding.tx_hash(), + }, } } } -struct MakerPaymentSent { - maker_coin: PhantomData, - taker_coin: PhantomData, +struct MakerPaymentSentFundingSpendGenerated { maker_coin_start_block: u64, taker_coin_start_block: u64, - taker_payment_locktime: u64, - maker_coin_htlc_pub_from_taker: Vec, - taker_coin_htlc_pub_from_taker: Vec, - maker_coin_swap_contract: Option>, - taker_coin_swap_contract: Option>, - taker_payment: TransactionIdentifier, + negotiation_data: NegotiationData, + funding_spend_preimage: TxPreimageWithSig, maker_payment: TransactionIdentifier, } -impl TransitionFrom> - for MakerPaymentSent +impl TransitionFrom> + for MakerPaymentSentFundingSpendGenerated { } #[async_trait] -impl State for MakerPaymentSent { +impl State + for MakerPaymentSentFundingSpendGenerated +{ type StateMachine = MakerSwapStateMachine; async fn on_changed(self: Box, state_machine: &mut Self::StateMachine) -> StateResult { let maker_payment_info = MakerPaymentInfo { tx_bytes: self.maker_payment.tx_hex.0.clone(), next_step_instructions: None, + funding_preimage_sig: self.funding_spend_preimage.signature.to_bytes(), + funding_preimage_tx: self.funding_spend_preimage.preimage.to_bytes(), }; let swap_msg = SwapMessage { inner: Some(swap_message::Inner::MakerPaymentInfo(maker_payment_info)), }; debug!("Sending maker payment info message {:?}", swap_msg); - let _abort_handle = broadcast_swap_v2_msg_every( + let abort_handle = broadcast_swap_v2_msg_every( state_machine.ctx.clone(), state_machine.p2p_topic.clone(), swap_msg, 600., state_machine.p2p_keypair, ); + + let recv_fut = recv_swap_v2_msg( + state_machine.ctx.clone(), + |store| store.taker_payment.take(), + &state_machine.uuid, + NEGOTIATION_TIMEOUT_SEC, + ); + let taker_payment_info = match recv_fut.await { + Ok(p) => p, + Err(e) => { + let next_state = MakerPaymentRefundRequired { + maker_coin_start_block: self.maker_coin_start_block, + taker_coin_start_block: self.taker_coin_start_block, + negotiation_data: self.negotiation_data, + maker_payment: self.maker_payment, + reason: MakerPaymentRefundReason::DidNotGetTakerPayment(e), + }; + return Self::change_state(next_state, state_machine).await; + }, + }; + drop(abort_handle); + + let taker_payment = match state_machine.taker_coin.parse_tx(&taker_payment_info.tx_bytes) { + Ok(tx) => tx, + Err(e) => { + let next_state = MakerPaymentRefundRequired { + maker_coin_start_block: self.maker_coin_start_block, + taker_coin_start_block: self.taker_coin_start_block, + negotiation_data: self.negotiation_data, + maker_payment: self.maker_payment, + reason: MakerPaymentRefundReason::FailedToParseTakerPayment(e.to_string()), + }; + return Self::change_state(next_state, state_machine).await; + }, + }; + let input = ConfirmPaymentInput { - payment_tx: self.taker_payment.tx_hex.0.clone(), + payment_tx: taker_payment.tx_hex(), confirmations: state_machine.conf_settings.taker_coin_confs, requires_nota: state_machine.conf_settings.taker_coin_nota, wait_until: state_machine.taker_payment_conf_timeout(), @@ -548,8 +752,9 @@ impl State for MakerPaymentSen }; if let Err(e) = state_machine.taker_coin.wait_for_confirmations(input).compat().await { let next_state = MakerPaymentRefundRequired { - maker_coin: Default::default(), - taker_coin: Default::default(), + maker_coin_start_block: self.maker_coin_start_block, + taker_coin_start_block: self.taker_coin_start_block, + negotiation_data: self.negotiation_data, maker_payment: self.maker_payment, reason: MakerPaymentRefundReason::TakerPaymentNotConfirmedInTime(e), }; @@ -557,29 +762,26 @@ impl State for MakerPaymentSen } let next_state = TakerPaymentConfirmed { - maker_coin: Default::default(), - taker_coin: Default::default(), maker_coin_start_block: self.maker_coin_start_block, taker_coin_start_block: self.taker_coin_start_block, maker_payment: self.maker_payment, - taker_payment: self.taker_payment, - taker_payment_locktime: self.taker_payment_locktime, - maker_coin_htlc_pub_from_taker: self.maker_coin_htlc_pub_from_taker, - taker_coin_htlc_pub_from_taker: self.taker_coin_htlc_pub_from_taker, - maker_coin_swap_contract: self.maker_coin_swap_contract, - taker_coin_swap_contract: self.taker_coin_swap_contract, + taker_payment, + negotiation_data: self.negotiation_data, }; Self::change_state(next_state, state_machine).await } } -impl StorableState for MakerPaymentSent { +impl StorableState + for MakerPaymentSentFundingSpendGenerated +{ type StateMachine = MakerSwapStateMachine; fn get_event(&self) -> <::Storage as StateMachineStorage>::Event { MakerSwapEvent::MakerPaymentSent { maker_coin_start_block: self.maker_coin_start_block, taker_coin_start_block: self.taker_coin_start_block, + negotiation_data: self.negotiation_data.to_stored_data(), maker_payment: self.maker_payment.clone(), } } @@ -587,31 +789,39 @@ impl StorableState for Mak #[derive(Debug)] enum MakerPaymentRefundReason { + DidNotGetTakerPayment(String), + FailedToParseTakerPayment(String), TakerPaymentNotConfirmedInTime(String), DidNotGetTakerPaymentSpendPreimage(String), TakerPaymentSpendPreimageIsNotValid(String), + FailedToParseTakerPreimage(String), + FailedToParseTakerSignature(String), TakerPaymentSpendBroadcastFailed(String), } -struct MakerPaymentRefundRequired { - maker_coin: PhantomData, - taker_coin: PhantomData, +struct MakerPaymentRefundRequired { + maker_coin_start_block: u64, + taker_coin_start_block: u64, + negotiation_data: NegotiationData, maker_payment: TransactionIdentifier, reason: MakerPaymentRefundReason, } -impl TransitionFrom> +impl + TransitionFrom> for MakerPaymentRefundRequired { } -impl TransitionFrom> +impl TransitionFrom> for MakerPaymentRefundRequired { } #[async_trait] -impl State - for MakerPaymentRefundRequired +impl< + MakerCoin: CoinAssocTypes + Send + Sync + 'static, + TakerCoin: MarketCoinOps + CoinAssocTypes + Send + Sync + 'static, + > State for MakerPaymentRefundRequired { type StateMachine = MakerSwapStateMachine; @@ -624,40 +834,40 @@ impl StorableState +impl StorableState for MakerPaymentRefundRequired { type StateMachine = MakerSwapStateMachine; fn get_event(&self) -> <::Storage as StateMachineStorage>::Event { MakerSwapEvent::MakerPaymentRefundRequired { + maker_coin_start_block: self.maker_coin_start_block, + taker_coin_start_block: self.taker_coin_start_block, + negotiation_data: self.negotiation_data.to_stored_data(), maker_payment: self.maker_payment.clone(), } } } #[allow(dead_code)] -struct TakerPaymentConfirmed { - maker_coin: PhantomData, - taker_coin: PhantomData, +struct TakerPaymentConfirmed { maker_coin_start_block: u64, taker_coin_start_block: u64, maker_payment: TransactionIdentifier, - taker_payment: TransactionIdentifier, - taker_payment_locktime: u64, - maker_coin_htlc_pub_from_taker: Vec, - taker_coin_htlc_pub_from_taker: Vec, - maker_coin_swap_contract: Option>, - taker_coin_swap_contract: Option>, + taker_payment: TakerCoin::Tx, + negotiation_data: NegotiationData, } -impl TransitionFrom> +impl + TransitionFrom> for TakerPaymentConfirmed { } #[async_trait] -impl State for TakerPaymentConfirmed { +impl State + for TakerPaymentConfirmed +{ type StateMachine = MakerSwapStateMachine; async fn on_changed(self: Box, state_machine: &mut Self::StateMachine) -> StateResult { @@ -667,45 +877,72 @@ impl State for TakerPaymentCon &state_machine.uuid, state_machine.taker_payment_conf_timeout(), ); - let preimage = match recv_fut.await { + let preimage_data = match recv_fut.await { Ok(preimage) => preimage, Err(e) => { let next_state = MakerPaymentRefundRequired { - maker_coin: Default::default(), - taker_coin: Default::default(), + maker_coin_start_block: self.maker_coin_start_block, + taker_coin_start_block: self.taker_coin_start_block, + negotiation_data: self.negotiation_data, maker_payment: self.maker_payment, reason: MakerPaymentRefundReason::DidNotGetTakerPaymentSpendPreimage(e), }; return Self::change_state(next_state, state_machine).await; }, }; - debug!("Received taker payment spend preimage message {:?}", preimage); + debug!("Received taker payment spend preimage message {:?}", preimage_data); let unique_data = state_machine.unique_data(); let gen_args = GenTakerPaymentSpendArgs { - taker_tx: &self.taker_payment.tx_hex.0, - time_lock: self.taker_payment_locktime, + taker_tx: &self.taker_payment, + time_lock: self.negotiation_data.taker_payment_locktime, secret_hash: &state_machine.secret_hash(), - maker_pub: &state_machine.maker_coin.derive_htlc_pubkey(&unique_data), - taker_pub: &self.taker_coin_htlc_pub_from_taker, + maker_pub: &state_machine.taker_coin.derive_htlc_pubkey_v2(&unique_data), + taker_pub: &self.negotiation_data.taker_coin_htlc_pub_from_taker, dex_fee_amount: state_machine.dex_fee_amount.to_decimal(), premium_amount: Default::default(), trading_amount: state_machine.taker_volume.to_decimal(), dex_fee_pub: &DEX_FEE_ADDR_RAW_PUBKEY, }; - let tx_preimage = TxPreimageWithSig { - preimage: preimage.tx_preimage.unwrap_or_default(), - signature: preimage.signature, + + let preimage = match state_machine.taker_coin.parse_preimage(&preimage_data.tx_preimage) { + Ok(p) => p, + Err(e) => { + let next_state = MakerPaymentRefundRequired { + maker_coin_start_block: self.maker_coin_start_block, + taker_coin_start_block: self.taker_coin_start_block, + negotiation_data: self.negotiation_data, + maker_payment: self.maker_payment, + reason: MakerPaymentRefundReason::FailedToParseTakerPreimage(e.to_string()), + }; + return Self::change_state(next_state, state_machine).await; + }, + }; + let signature = match state_machine.taker_coin.parse_signature(&preimage_data.signature) { + Ok(s) => s, + Err(e) => { + let next_state = MakerPaymentRefundRequired { + maker_coin_start_block: self.maker_coin_start_block, + taker_coin_start_block: self.taker_coin_start_block, + negotiation_data: self.negotiation_data, + maker_payment: self.maker_payment, + reason: MakerPaymentRefundReason::FailedToParseTakerSignature(e.to_string()), + }; + return Self::change_state(next_state, state_machine).await; + }, }; + + let tx_preimage = TxPreimageWithSig { preimage, signature }; if let Err(e) = state_machine .taker_coin .validate_taker_payment_spend_preimage(&gen_args, &tx_preimage) .await { let next_state = MakerPaymentRefundRequired { - maker_coin: Default::default(), - taker_coin: Default::default(), + maker_coin_start_block: self.maker_coin_start_block, + taker_coin_start_block: self.taker_coin_start_block, + negotiation_data: self.negotiation_data, maker_payment: self.maker_payment, reason: MakerPaymentRefundReason::TakerPaymentSpendPreimageIsNotValid(e.to_string()), }; @@ -725,8 +962,9 @@ impl State for TakerPaymentCon Ok(tx) => tx, Err(e) => { let next_state = MakerPaymentRefundRequired { - maker_coin: Default::default(), - taker_coin: Default::default(), + maker_coin_start_block: self.maker_coin_start_block, + taker_coin_start_block: self.taker_coin_start_block, + negotiation_data: self.negotiation_data, maker_payment: self.maker_payment, reason: MakerPaymentRefundReason::TakerPaymentSpendBroadcastFailed(format!("{:?}", e)), }; @@ -741,7 +979,6 @@ impl State for TakerPaymentCon ); let next_state = TakerPaymentSpent { maker_coin: Default::default(), - taker_coin: Default::default(), maker_coin_start_block: self.maker_coin_start_block, taker_coin_start_block: self.taker_coin_start_block, maker_payment: self.maker_payment, @@ -755,7 +992,7 @@ impl State for TakerPaymentCon } } -impl StorableState +impl StorableState for TakerPaymentConfirmed { type StateMachine = MakerSwapStateMachine; @@ -764,29 +1001,32 @@ impl StorableState MakerSwapEvent::TakerPaymentConfirmed { maker_coin_start_block: self.maker_coin_start_block, taker_coin_start_block: self.taker_coin_start_block, + negotiation_data: self.negotiation_data.to_stored_data(), maker_payment: self.maker_payment.clone(), - taker_payment: self.taker_payment.clone(), + taker_payment: TransactionIdentifier { + tx_hex: self.taker_payment.tx_hex().into(), + tx_hash: self.taker_payment.tx_hash(), + }, } } } -struct TakerPaymentSpent { +struct TakerPaymentSpent { maker_coin: PhantomData, - taker_coin: PhantomData, maker_coin_start_block: u64, taker_coin_start_block: u64, maker_payment: TransactionIdentifier, - taker_payment: TransactionIdentifier, + taker_payment: TakerCoin::Tx, taker_payment_spend: TransactionIdentifier, } -impl TransitionFrom> +impl TransitionFrom> for TakerPaymentSpent { } #[async_trait] -impl State +impl State for TakerPaymentSpent { type StateMachine = MakerSwapStateMachine; @@ -796,7 +1036,9 @@ impl State } } -impl StorableState for TakerPaymentSpent { +impl StorableState + for TakerPaymentSpent +{ type StateMachine = MakerSwapStateMachine; fn get_event(&self) -> <::Storage as StateMachineStorage>::Event { @@ -804,20 +1046,43 @@ impl StorableState for Tak maker_coin_start_block: self.maker_coin_start_block, taker_coin_start_block: self.taker_coin_start_block, maker_payment: self.maker_payment.clone(), - taker_payment: self.taker_payment.clone(), + taker_payment: TransactionIdentifier { + tx_hex: self.taker_payment.tx_hex().into(), + tx_hash: self.taker_payment.tx_hash(), + }, taker_payment_spend: self.taker_payment_spend.clone(), } } } +/// Represents possible reasons of maker swap being aborted +#[derive(Clone, Debug, Deserialize, Display, Serialize)] +pub enum AbortReason { + FailedToGetMakerCoinBlock(String), + FailedToGetTakerCoinBlock(String), + BalanceCheckFailure(String), + DidNotReceiveTakerNegotiation(String), + TakerAbortedNegotiation(String), + ReceivedInvalidTakerNegotiation, + DidNotReceiveTakerFundingInfo(String), + FailedToParseTakerFunding(String), + TakerFundingValidationFailed(String), + FailedToGenerateFundingSpend(String), + FailedToSendMakerPayment(String), + TooLargeStartedAtDiff(u64), + TakerProvidedInvalidFundingLocktime(u64), + TakerProvidedInvalidPaymentLocktime(u64), + FailedToParsePubkey(String), +} + struct Aborted { maker_coin: PhantomData, taker_coin: PhantomData, - reason: String, + reason: AbortReason, } impl Aborted { - fn new(reason: String) -> Aborted { + fn new(reason: AbortReason) -> Aborted { Aborted { maker_coin: Default::default(), taker_coin: Default::default(), @@ -850,11 +1115,11 @@ impl StorableState for Abo impl TransitionFrom> for Aborted {} impl TransitionFrom> for Aborted {} -impl TransitionFrom> +impl TransitionFrom> for Aborted { } -impl TransitionFrom> +impl TransitionFrom> for Aborted { } @@ -893,4 +1158,7 @@ impl LastSta } } -impl TransitionFrom> for Completed {} +impl TransitionFrom> + for Completed +{ +} diff --git a/mm2src/mm2_main/src/lp_swap/swap_v2.proto b/mm2src/mm2_main/src/lp_swap/swap_v2.proto index 5ef75bc241..9bbaa87e5d 100644 --- a/mm2src/mm2_main/src/lp_swap/swap_v2.proto +++ b/mm2src/mm2_main/src/lp_swap/swap_v2.proto @@ -24,12 +24,13 @@ message Abort { message TakerNegotiationData { uint64 started_at = 1; - uint64 payment_locktime = 2; - // add bytes secret_hash = 3 if required - bytes maker_coin_htlc_pub = 4; - bytes taker_coin_htlc_pub = 5; - optional bytes maker_coin_swap_contract = 6; - optional bytes taker_coin_swap_contract = 7; + uint64 funding_locktime = 2; + uint64 payment_locktime = 3; + bytes taker_secret_hash = 4; + bytes maker_coin_htlc_pub = 5; + bytes taker_coin_htlc_pub = 6; + optional bytes maker_coin_swap_contract = 7; + optional bytes taker_coin_swap_contract = 8; } message TakerNegotiation { @@ -45,6 +46,11 @@ message MakerNegotiated { optional string reason = 2; } +message TakerFundingInfo { + bytes tx_bytes = 1; + optional bytes next_step_instructions = 2; +} + message TakerPaymentInfo { bytes tx_bytes = 1; optional bytes next_step_instructions = 2; @@ -53,11 +59,13 @@ message TakerPaymentInfo { message MakerPaymentInfo { bytes tx_bytes = 1; optional bytes next_step_instructions = 2; + bytes funding_preimage_sig = 3; + bytes funding_preimage_tx = 4; } message TakerPaymentSpendPreimage { bytes signature = 1; - optional bytes tx_preimage = 2; + bytes tx_preimage = 2; } message SwapMessage { @@ -65,8 +73,9 @@ message SwapMessage { MakerNegotiation maker_negotiation = 1; TakerNegotiation taker_negotiation = 2; MakerNegotiated maker_negotiated = 3; - TakerPaymentInfo taker_payment_info = 4; + TakerFundingInfo taker_funding_info = 4; MakerPaymentInfo maker_payment_info = 5; - TakerPaymentSpendPreimage taker_payment_spend_preimage = 6; + TakerPaymentInfo taker_payment_info = 6; + TakerPaymentSpendPreimage taker_payment_spend_preimage = 7; } } diff --git a/mm2src/mm2_main/src/lp_swap/taker_swap.rs b/mm2src/mm2_main/src/lp_swap/taker_swap.rs index 1afdcbb5f8..f16bdd129d 100644 --- a/mm2src/mm2_main/src/lp_swap/taker_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/taker_swap.rs @@ -14,7 +14,7 @@ use crate::mm2::lp_network::subscribe_to_topic; use crate::mm2::lp_ordermatch::TakerOrderBuilder; use crate::mm2::lp_swap::taker_restart::get_command_based_on_watcher_activity; use crate::mm2::lp_swap::{broadcast_p2p_tx_msg, broadcast_swap_msg_every_delayed, tx_helper_topic, - wait_for_maker_payment_conf_duration, TakerSwapWatcherData}; + wait_for_maker_payment_conf_duration, TakerSwapWatcherData, MAX_STARTED_AT_DIFF}; use coins::lp_price::fetch_swap_coins_price; use coins::{lp_coinfind, CanRefundHtlc, CheckIfMyPaymentSentArgs, ConfirmPaymentInput, FeeApproxStage, FoundSwapTxSpend, MmCoin, MmCoinEnum, PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, @@ -1128,7 +1128,7 @@ impl TakerSwap { debug!("Received maker negotiation data {:?}", maker_data); let time_dif = self.r().data.started_at.abs_diff(maker_data.started_at()); - if time_dif > 60 { + if time_dif > MAX_STARTED_AT_DIFF { return Ok((Some(TakerSwapCommand::Finish), vec![TakerSwapEvent::NegotiateFailed( ERRL!("The time difference between you and the maker cannot be longer than 60 seconds. Current difference: {}. Please make sure that your system clock is synced to the correct time before starting another swap!", time_dif).into(), )])); diff --git a/mm2src/mm2_main/src/lp_swap/taker_swap_v2.rs b/mm2src/mm2_main/src/lp_swap/taker_swap_v2.rs index e4ae68b89b..0e825d5e9c 100644 --- a/mm2src/mm2_main/src/lp_swap/taker_swap_v2.rs +++ b/mm2src/mm2_main/src/lp_swap/taker_swap_v2.rs @@ -1,28 +1,45 @@ use super::{NEGOTIATE_SEND_INTERVAL, NEGOTIATION_TIMEOUT_SEC}; +use crate::mm2::database::my_swaps::{get_swap_events, insert_new_swap_v2, set_swap_is_finished, update_swap_events}; use crate::mm2::lp_network::subscribe_to_topic; use crate::mm2::lp_swap::swap_v2_pb::*; -use crate::mm2::lp_swap::{broadcast_swap_v2_msg_every, check_balance_for_taker_swap, recv_swap_v2_msg, - SwapConfirmationsSettings, SwapsContext, TransactionIdentifier}; +use crate::mm2::lp_swap::{broadcast_swap_v2_msg_every, check_balance_for_taker_swap, recv_swap_v2_msg, SecretHashAlgo, + SwapConfirmationsSettings, SwapsContext, TransactionIdentifier, MAX_STARTED_AT_DIFF, + TAKER_SWAP_V2_TYPE}; use async_trait::async_trait; -use coins::{ConfirmPaymentInput, FeeApproxStage, GenTakerPaymentSpendArgs, MmCoin, SendCombinedTakerPaymentArgs, - SpendPaymentArgs, SwapOpsV2, WaitForHTLCTxSpendArgs}; +use bitcrypto::{dhash160, sha256}; +use coins::{CoinAssocTypes, ConfirmPaymentInput, FeeApproxStage, GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, + MmCoin, SendTakerFundingArgs, SpendPaymentArgs, SwapOps, SwapOpsV2, ToBytes, Transaction, + TxPreimageWithSig, ValidatePaymentInput, WaitForHTLCTxSpendArgs}; use common::log::{debug, info, warn}; use common::{bits256, Future01CompatExt, DEX_FEE_ADDR_RAW_PUBKEY}; +use db_common::sqlite::rusqlite::named_params; use keys::KeyPair; use mm2_core::mm_ctx::MmArc; -use mm2_number::{BigDecimal, MmNumber}; +use mm2_err_handle::prelude::*; +use mm2_number::MmNumber; use mm2_state_machine::prelude::*; use mm2_state_machine::storable_state_machine::*; +use primitives::hash::H256; use rpc::v1::types::Bytes as BytesJson; -use std::collections::HashMap; use std::marker::PhantomData; use uuid::Uuid; // This is needed to have Debug on messages #[allow(unused_imports)] use prost::Message; +/// Negotiation data representation to be stored in DB. +#[derive(Debug, Deserialize, Serialize)] +pub struct StoredNegotiationData { + maker_payment_locktime: u64, + maker_secret_hash: BytesJson, + maker_coin_htlc_pub_from_maker: BytesJson, + taker_coin_htlc_pub_from_maker: BytesJson, + maker_coin_swap_contract: Option, + taker_coin_swap_contract: Option, +} + /// Represents events produced by taker swap states. -#[derive(Debug, PartialEq)] +#[derive(Debug, Deserialize, Serialize)] pub enum TakerSwapEvent { /// Swap has been successfully initialized. Initialized { @@ -33,27 +50,50 @@ pub enum TakerSwapEvent { Negotiated { maker_coin_start_block: u64, taker_coin_start_block: u64, - secret_hash: BytesJson, + negotiation_data: StoredNegotiationData, + }, + /// Sent taker funding tx. + TakerFundingSent { + maker_coin_start_block: u64, + taker_coin_start_block: u64, + negotiation_data: StoredNegotiationData, + taker_funding: TransactionIdentifier, + }, + /// Taker funding tx refund is required. + TakerFundingRefundRequired { + maker_coin_start_block: u64, + taker_coin_start_block: u64, + negotiation_data: StoredNegotiationData, + taker_funding: TransactionIdentifier, + reason: TakerFundingRefundReason, + }, + /// Received maker payment + MakerPaymentReceived { + maker_coin_start_block: u64, + taker_coin_start_block: u64, + negotiation_data: StoredNegotiationData, + taker_funding: TransactionIdentifier, + maker_payment: TransactionIdentifier, }, /// Sent taker payment. TakerPaymentSent { maker_coin_start_block: u64, taker_coin_start_block: u64, taker_payment: TransactionIdentifier, - secret_hash: BytesJson, + negotiation_data: StoredNegotiationData, }, /// Something went wrong, so taker payment refund is required. TakerPaymentRefundRequired { taker_payment: TransactionIdentifier, - secret_hash: BytesJson, + negotiation_data: StoredNegotiationData, }, - /// Both payments are confirmed on-chain - BothPaymentsSentAndConfirmed { + /// Maker payment is confirmed on-chain + MakerPaymentConfirmed { maker_coin_start_block: u64, taker_coin_start_block: u64, maker_payment: TransactionIdentifier, taker_payment: TransactionIdentifier, - secret_hash: BytesJson, + negotiation_data: StoredNegotiationData, }, /// Maker spent taker's payment and taker discovered the tx on-chain. TakerPaymentSpent { @@ -62,7 +102,7 @@ pub enum TakerSwapEvent { maker_payment: TransactionIdentifier, taker_payment: TransactionIdentifier, taker_payment_spend: TransactionIdentifier, - secret: BytesJson, + negotiation_data: StoredNegotiationData, }, /// Taker spent maker's payment. MakerPaymentSpent { @@ -74,37 +114,54 @@ pub enum TakerSwapEvent { maker_payment_spend: TransactionIdentifier, }, /// Swap has been aborted before taker payment was sent. - Aborted { reason: String }, + Aborted { reason: AbortReason }, /// Swap completed successfully. Completed, } /// Represents errors that can be produced by [`TakerSwapStateMachine`] run. #[derive(Debug, Display)] -pub enum TakerSwapStateMachineError {} +pub enum TakerSwapStateMachineError { + StorageError(String), + SerdeError(String), +} /// Dummy storage for taker swap events (used temporary). -#[derive(Default)] pub struct DummyTakerSwapStorage { - events: HashMap>, + ctx: MmArc, +} + +impl DummyTakerSwapStorage { + pub fn new(ctx: MmArc) -> Self { DummyTakerSwapStorage { ctx } } } #[async_trait] impl StateMachineStorage for DummyTakerSwapStorage { type MachineId = Uuid; type Event = TakerSwapEvent; - type Error = TakerSwapStateMachineError; + type Error = MmError; async fn store_event(&mut self, id: Self::MachineId, event: Self::Event) -> Result<(), Self::Error> { - self.events.entry(id).or_insert_with(Vec::new).push(event); + let id_str = id.to_string(); + let events_json = get_swap_events(&self.ctx.sqlite_connection(), &id_str) + .map_to_mm(|e| TakerSwapStateMachineError::StorageError(e.to_string()))?; + let mut events: Vec = + serde_json::from_str(&events_json).map_to_mm(|e| TakerSwapStateMachineError::SerdeError(e.to_string()))?; + events.push(event); + drop_mutability!(events); + let serialized_events = + serde_json::to_string(&events).map_to_mm(|e| TakerSwapStateMachineError::SerdeError(e.to_string()))?; + update_swap_events(&self.ctx.sqlite_connection(), &id_str, &serialized_events) + .map_to_mm(|e| TakerSwapStateMachineError::StorageError(e.to_string()))?; Ok(()) } - async fn get_unfinished(&self) -> Result, Self::Error> { - Ok(self.events.keys().copied().collect()) - } + async fn get_unfinished(&self) -> Result, Self::Error> { todo!() } - async fn mark_finished(&mut self, _id: Self::MachineId) -> Result<(), Self::Error> { Ok(()) } + async fn mark_finished(&mut self, id: Self::MachineId) -> Result<(), Self::Error> { + set_swap_is_finished(&self.ctx.sqlite_connection(), &id.to_string()) + .map_to_mm(|e| TakerSwapStateMachineError::StorageError(e.to_string())) + } } /// Represents the state machine for taker's side of the Trading Protocol Upgrade swap (v2). @@ -129,6 +186,8 @@ pub struct TakerSwapStateMachine { pub dex_fee: MmNumber, /// Premium amount, which might be paid to maker as additional reward. pub taker_premium: MmNumber, + /// Algorithm used to hash swap secrets. + pub secret_hash_algo: SecretHashAlgo, /// Swap transactions' confirmations settings. pub conf_settings: SwapConfirmationsSettings, /// UUID of the swap. @@ -137,14 +196,26 @@ pub struct TakerSwapStateMachine { pub p2p_topic: String, /// If Some, used to sign P2P messages of this swap. pub p2p_keypair: Option, + /// The secret used for immediate taker funding tx reclaim if maker back-outs + pub taker_secret: H256, } impl TakerSwapStateMachine { fn maker_payment_conf_timeout(&self) -> u64 { self.started_at + self.lock_duration * 2 / 3 } + fn taker_funding_locktime(&self) -> u64 { self.started_at + self.lock_duration * 3 } + fn taker_payment_locktime(&self) -> u64 { self.started_at + self.lock_duration } fn unique_data(&self) -> Vec { self.uuid.as_bytes().to_vec() } + + /// Returns secret hash generated using selected [SecretHashAlgo]. + fn taker_secret_hash(&self) -> Vec { + match self.secret_hash_algo { + SecretHashAlgo::DHASH160 => dhash160(self.taker_secret.as_slice()).take().into(), + SecretHashAlgo::SHA256 => sha256(self.taker_secret.as_slice()).take().into(), + } + } } impl StorableStateMachine @@ -191,18 +262,48 @@ impl; async fn on_changed(self: Box, state_machine: &mut Self::StateMachine) -> StateResult { + { + let sql_params = named_params! { + ":my_coin": state_machine.taker_coin.ticker(), + ":other_coin": state_machine.maker_coin.ticker(), + ":uuid": state_machine.uuid.to_string(), + ":started_at": state_machine.started_at, + ":swap_type": TAKER_SWAP_V2_TYPE, + ":maker_volume": state_machine.maker_volume.to_fraction_string(), + ":taker_volume": state_machine.taker_volume.to_fraction_string(), + ":premium": state_machine.taker_premium.to_fraction_string(), + ":dex_fee": state_machine.dex_fee.to_fraction_string(), + ":secret": state_machine.taker_secret.take(), + ":secret_hash": state_machine.taker_secret_hash(), + ":secret_hash_algo": state_machine.secret_hash_algo as u8, + ":p2p_privkey": state_machine.p2p_keypair.map(|k| k.private_bytes()).unwrap_or_default(), + ":lock_duration": state_machine.lock_duration, + ":maker_coin_confs": state_machine.conf_settings.maker_coin_confs, + ":maker_coin_nota": state_machine.conf_settings.maker_coin_nota, + ":taker_coin_confs": state_machine.conf_settings.taker_coin_confs, + ":taker_coin_nota": state_machine.conf_settings.taker_coin_nota + }; + insert_new_swap_v2(&state_machine.ctx, sql_params).unwrap(); + } + subscribe_to_topic(&state_machine.ctx, state_machine.p2p_topic.clone()); let swap_ctx = SwapsContext::from_ctx(&state_machine.ctx).expect("SwapsContext::from_ctx should not fail"); swap_ctx.init_msg_v2_store(state_machine.uuid, bits256::default()); let maker_coin_start_block = match state_machine.maker_coin.current_block().compat().await { Ok(b) => b, - Err(e) => return Self::change_state(Aborted::new(e), state_machine).await, + Err(e) => { + let reason = AbortReason::FailedToGetMakerCoinBlock(e); + return Self::change_state(Aborted::new(reason), state_machine).await; + }, }; let taker_coin_start_block = match state_machine.taker_coin.current_block().compat().await { Ok(b) => b, - Err(e) => return Self::change_state(Aborted::new(e), state_machine).await, + Err(e) => { + let reason = AbortReason::FailedToGetTakerCoinBlock(e); + return Self::change_state(Aborted::new(reason), state_machine).await; + }, }; if let Err(e) = check_balance_for_taker_swap( @@ -216,7 +317,8 @@ impl StorableState for Ini } #[async_trait] -impl State - for Initialized -{ +impl State for Initialized { type StateMachine = TakerSwapStateMachine; async fn on_changed(self: Box, state_machine: &mut Self::StateMachine) -> StateResult { @@ -267,18 +367,59 @@ impl d, Err(e) => { - let next_state = Aborted::new(format!("Failed to receive MakerNegotiation: {}", e)); - return Self::change_state(next_state, state_machine).await; + let reason = AbortReason::DidNotReceiveMakerNegotiation(e); + return Self::change_state(Aborted::new(reason), state_machine).await; }, }; debug!("Received maker negotiation message {:?}", maker_negotiation); + let started_at_diff = state_machine.started_at.abs_diff(maker_negotiation.started_at); + if started_at_diff > MAX_STARTED_AT_DIFF { + let reason = AbortReason::TooLargeStartedAtDiff(started_at_diff); + return Self::change_state(Aborted::new(reason), state_machine).await; + } + + if !(maker_negotiation.secret_hash.len() == 20 || maker_negotiation.secret_hash.len() == 32) { + let reason = AbortReason::SecretHashUnexpectedLen(maker_negotiation.secret_hash.len()); + return Self::change_state(Aborted::new(reason), state_machine).await; + } + + let expected_maker_payment_locktime = maker_negotiation.started_at + 2 * state_machine.lock_duration; + if maker_negotiation.payment_locktime != expected_maker_payment_locktime { + let reason = AbortReason::MakerProvidedInvalidLocktime(maker_negotiation.payment_locktime); + return Self::change_state(Aborted::new(reason), state_machine).await; + } + + let maker_coin_htlc_pub_from_maker = match state_machine + .maker_coin + .parse_pubkey(&maker_negotiation.maker_coin_htlc_pub) + { + Ok(p) => p, + Err(e) => { + let reason = AbortReason::FailedToParsePubkey(e.to_string()); + return Self::change_state(Aborted::new(reason), state_machine).await; + }, + }; + + let taker_coin_htlc_pub_from_maker = match state_machine + .taker_coin + .parse_pubkey(&maker_negotiation.taker_coin_htlc_pub) + { + Ok(p) => p, + Err(e) => { + let reason = AbortReason::FailedToParsePubkey(e.to_string()); + return Self::change_state(Aborted::new(reason), state_machine).await; + }, + }; + let unique_data = state_machine.unique_data(); let taker_negotiation = TakerNegotiation { action: Some(taker_negotiation::Action::Continue(TakerNegotiationData { started_at: state_machine.started_at, + funding_locktime: state_machine.taker_funding_locktime(), payment_locktime: state_machine.taker_payment_locktime(), + taker_secret_hash: state_machine.taker_secret_hash(), maker_coin_htlc_pub: state_machine.maker_coin.derive_htlc_pubkey(&unique_data), taker_coin_htlc_pub: state_machine.taker_coin.derive_htlc_pubkey(&unique_data), maker_coin_swap_contract: state_machine.maker_coin.swap_contract_address().map(|bytes| bytes.0), @@ -307,140 +448,142 @@ impl d, Err(e) => { - let next_state = Aborted::new(format!("Failed to receive MakerNegotiated: {}", e)); - return Self::change_state(next_state, state_machine).await; + let reason = AbortReason::DidNotReceiveMakerNegotiated(e); + return Self::change_state(Aborted::new(reason), state_machine).await; }, }; drop(abort_handle); debug!("Received maker negotiated message {:?}", maker_negotiated); if !maker_negotiated.negotiated { - let next_state = Aborted::new(format!( - "Maker did not negotiate with the reason: {}", - maker_negotiated.reason.unwrap_or_default() - )); - return Self::change_state(next_state, state_machine).await; + let reason = AbortReason::MakerDidNotNegotiate(maker_negotiated.reason.unwrap_or_default()); + return Self::change_state(Aborted::new(reason), state_machine).await; } let next_state = Negotiated { - maker_coin: Default::default(), - taker_coin: Default::default(), maker_coin_start_block: self.maker_coin_start_block, taker_coin_start_block: self.taker_coin_start_block, - secret_hash: maker_negotiation.secret_hash, - maker_payment_locktime: maker_negotiation.payment_locktime, - maker_coin_htlc_pub_from_maker: maker_negotiation.maker_coin_htlc_pub, - taker_coin_htlc_pub_from_maker: maker_negotiation.taker_coin_htlc_pub, - maker_coin_swap_contract: maker_negotiation.maker_coin_swap_contract, - taker_coin_swap_contract: maker_negotiation.taker_coin_swap_contract, + negotiation_data: NegotiationData { + maker_secret_hash: maker_negotiation.secret_hash, + maker_payment_locktime: expected_maker_payment_locktime, + maker_coin_htlc_pub_from_maker, + taker_coin_htlc_pub_from_maker, + maker_coin_swap_contract: maker_negotiation.maker_coin_swap_contract, + taker_coin_swap_contract: maker_negotiation.taker_coin_swap_contract, + }, }; Self::change_state(next_state, state_machine).await } } -struct Negotiated { - maker_coin: PhantomData, - taker_coin: PhantomData, - maker_coin_start_block: u64, - taker_coin_start_block: u64, - secret_hash: Vec, +struct NegotiationData { + maker_secret_hash: Vec, maker_payment_locktime: u64, - maker_coin_htlc_pub_from_maker: Vec, - taker_coin_htlc_pub_from_maker: Vec, + maker_coin_htlc_pub_from_maker: MakerCoin::Pubkey, + taker_coin_htlc_pub_from_maker: TakerCoin::Pubkey, maker_coin_swap_contract: Option>, taker_coin_swap_contract: Option>, } -impl TransitionFrom> for Negotiated {} +impl NegotiationData { + fn to_stored_data(&self) -> StoredNegotiationData { + StoredNegotiationData { + maker_payment_locktime: self.maker_payment_locktime, + maker_secret_hash: self.maker_secret_hash.clone().into(), + maker_coin_htlc_pub_from_maker: self.maker_coin_htlc_pub_from_maker.to_bytes().into(), + taker_coin_htlc_pub_from_maker: self.taker_coin_htlc_pub_from_maker.to_bytes().into(), + maker_coin_swap_contract: self.maker_coin_swap_contract.clone().map(|b| b.into()), + taker_coin_swap_contract: self.taker_coin_swap_contract.clone().map(|b| b.into()), + } + } +} + +struct Negotiated { + maker_coin_start_block: u64, + taker_coin_start_block: u64, + negotiation_data: NegotiationData, +} + +impl TransitionFrom> + for Negotiated +{ +} #[async_trait] -impl State for Negotiated { +impl State for Negotiated { type StateMachine = TakerSwapStateMachine; async fn on_changed(self: Box, state_machine: &mut Self::StateMachine) -> StateResult { - let args = SendCombinedTakerPaymentArgs { - time_lock: state_machine.taker_payment_locktime(), - secret_hash: &self.secret_hash, - other_pub: &self.taker_coin_htlc_pub_from_maker, + let args = SendTakerFundingArgs { + time_lock: state_machine.taker_funding_locktime(), + taker_secret_hash: &state_machine.taker_secret_hash(), + maker_pub: &self.negotiation_data.taker_coin_htlc_pub_from_maker.to_bytes(), dex_fee_amount: state_machine.dex_fee.to_decimal(), - premium_amount: BigDecimal::from(0), + premium_amount: state_machine.taker_premium.to_decimal(), trading_amount: state_machine.taker_volume.to_decimal(), swap_unique_data: &state_machine.unique_data(), }; - let taker_payment = match state_machine.taker_coin.send_combined_taker_payment(args).await { + let taker_funding = match state_machine.taker_coin.send_taker_funding(args).await { Ok(tx) => tx, Err(e) => { - let next_state = Aborted::new(format!("Failed to send taker payment {:?}", e)); - return Self::change_state(next_state, state_machine).await; + let reason = AbortReason::FailedToSendTakerFunding(format!("{:?}", e)); + return Self::change_state(Aborted::new(reason), state_machine).await; }, }; + info!( - "Sent combined taker payment {} tx {:02x} during swap {}", + "Sent taker funding {} tx {:02x} during swap {}", state_machine.taker_coin.ticker(), - taker_payment.tx_hash(), + taker_funding.tx_hash(), state_machine.uuid ); - let next_state = TakerPaymentSent { - maker_coin: Default::default(), - taker_coin: Default::default(), + let next_state = TakerFundingSent { maker_coin_start_block: self.maker_coin_start_block, taker_coin_start_block: self.taker_coin_start_block, - taker_payment: TransactionIdentifier { - tx_hex: taker_payment.tx_hex().into(), - tx_hash: taker_payment.tx_hash(), - }, - secret_hash: self.secret_hash, - maker_payment_locktime: self.maker_payment_locktime, - maker_coin_htlc_pub_from_maker: self.maker_coin_htlc_pub_from_maker, - taker_coin_htlc_pub_from_maker: self.taker_coin_htlc_pub_from_maker, - maker_coin_swap_contract: self.maker_coin_swap_contract, - taker_coin_swap_contract: self.taker_coin_swap_contract, + taker_funding, + negotiation_data: self.negotiation_data, }; Self::change_state(next_state, state_machine).await } } -impl StorableState for Negotiated { +impl StorableState + for Negotiated +{ type StateMachine = TakerSwapStateMachine; fn get_event(&self) -> <::Storage as StateMachineStorage>::Event { TakerSwapEvent::Negotiated { maker_coin_start_block: self.maker_coin_start_block, taker_coin_start_block: self.taker_coin_start_block, - secret_hash: Default::default(), + negotiation_data: self.negotiation_data.to_stored_data(), } } } -struct TakerPaymentSent { - maker_coin: PhantomData, - taker_coin: PhantomData, +struct TakerFundingSent { maker_coin_start_block: u64, taker_coin_start_block: u64, - taker_payment: TransactionIdentifier, - secret_hash: Vec, - maker_payment_locktime: u64, - maker_coin_htlc_pub_from_maker: Vec, - taker_coin_htlc_pub_from_maker: Vec, - maker_coin_swap_contract: Option>, - taker_coin_swap_contract: Option>, + taker_funding: TakerCoin::Tx, + negotiation_data: NegotiationData, } -impl TransitionFrom> for TakerPaymentSent {} - #[async_trait] -impl State for TakerPaymentSent { +impl State + for TakerFundingSent +{ type StateMachine = TakerSwapStateMachine; async fn on_changed(self: Box, state_machine: &mut Self::StateMachine) -> StateResult { - let taker_payment_info = TakerPaymentInfo { - tx_bytes: self.taker_payment.tx_hex.clone().0, + let taker_funding_info = TakerFundingInfo { + tx_bytes: self.taker_funding.tx_hex(), next_step_instructions: None, }; + let swap_msg = SwapMessage { - inner: Some(swap_message::Inner::TakerPaymentInfo(taker_payment_info)), + inner: Some(swap_message::Inner::TakerFundingInfo(taker_funding_info)), }; let abort_handle = broadcast_swap_v2_msg_every( state_machine.ctx.clone(), @@ -460,21 +603,260 @@ impl State for TakerPaymentSen let maker_payment_info = match recv_fut.await { Ok(p) => p, Err(e) => { - let next_state = TakerPaymentRefundRequired { - maker_coin: Default::default(), - taker_coin: Default::default(), - taker_payment: self.taker_payment, - secret_hash: self.secret_hash, - reason: TakerPaymentRefundReason::DidNotReceiveMakerPayment(e), + let next_state = TakerFundingRefundRequired { + maker_coin_start_block: self.maker_coin_start_block, + taker_coin_start_block: self.taker_coin_start_block, + taker_funding: self.taker_funding, + negotiation_data: self.negotiation_data, + reason: TakerFundingRefundReason::DidNotReceiveMakerPayment(e), }; return Self::change_state(next_state, state_machine).await; }, }; drop(abort_handle); + debug!("Received maker payment info message {:?}", maker_payment_info); + let preimage_tx = match state_machine + .taker_coin + .parse_preimage(&maker_payment_info.funding_preimage_tx) + { + Ok(p) => p, + Err(e) => { + let next_state = TakerFundingRefundRequired { + maker_coin_start_block: self.maker_coin_start_block, + taker_coin_start_block: self.taker_coin_start_block, + taker_funding: self.taker_funding, + negotiation_data: self.negotiation_data, + reason: TakerFundingRefundReason::FailedToParseFundingSpendPreimg(e.to_string()), + }; + return Self::change_state(next_state, state_machine).await; + }, + }; + + let preimage_sig = match state_machine + .taker_coin + .parse_signature(&maker_payment_info.funding_preimage_sig) + { + Ok(p) => p, + Err(e) => { + let next_state = TakerFundingRefundRequired { + maker_coin_start_block: self.maker_coin_start_block, + taker_coin_start_block: self.taker_coin_start_block, + taker_funding: self.taker_funding, + negotiation_data: self.negotiation_data, + reason: TakerFundingRefundReason::FailedToParseFundingSpendSig(e.to_string()), + }; + return Self::change_state(next_state, state_machine).await; + }, + }; + + let next_state = MakerPaymentAndFundingSpendPreimgReceived { + maker_coin_start_block: self.maker_coin_start_block, + taker_coin_start_block: self.taker_coin_start_block, + negotiation_data: self.negotiation_data, + taker_funding: self.taker_funding, + funding_spend_preimage: TxPreimageWithSig { + preimage: preimage_tx, + signature: preimage_sig, + }, + maker_payment: TransactionIdentifier { + tx_hex: maker_payment_info.tx_bytes.into(), + tx_hash: Default::default(), + }, + }; + Self::change_state(next_state, state_machine).await + } +} + +impl TransitionFrom> + for TakerFundingSent +{ +} + +impl StorableState + for TakerFundingSent +{ + type StateMachine = TakerSwapStateMachine; + + fn get_event(&self) -> <::Storage as StateMachineStorage>::Event { + TakerSwapEvent::TakerFundingSent { + maker_coin_start_block: self.maker_coin_start_block, + taker_coin_start_block: self.taker_coin_start_block, + taker_funding: TransactionIdentifier { + tx_hex: self.taker_funding.tx_hex().into(), + tx_hash: self.taker_funding.tx_hash(), + }, + negotiation_data: self.negotiation_data.to_stored_data(), + } + } +} + +struct MakerPaymentAndFundingSpendPreimgReceived { + maker_coin_start_block: u64, + taker_coin_start_block: u64, + negotiation_data: NegotiationData, + taker_funding: TakerCoin::Tx, + funding_spend_preimage: TxPreimageWithSig, + maker_payment: TransactionIdentifier, +} + +impl TransitionFrom> + for MakerPaymentAndFundingSpendPreimgReceived +{ +} + +impl StorableState + for MakerPaymentAndFundingSpendPreimgReceived +{ + type StateMachine = TakerSwapStateMachine; + + fn get_event(&self) -> <::Storage as StateMachineStorage>::Event { + TakerSwapEvent::MakerPaymentReceived { + maker_coin_start_block: self.maker_coin_start_block, + taker_coin_start_block: self.taker_coin_start_block, + negotiation_data: self.negotiation_data.to_stored_data(), + taker_funding: TransactionIdentifier { + tx_hex: self.taker_funding.tx_hex().into(), + tx_hash: self.taker_funding.tx_hash(), + }, + maker_payment: self.maker_payment.clone(), + } + } +} + +#[async_trait] +impl State + for MakerPaymentAndFundingSpendPreimgReceived +{ + type StateMachine = TakerSwapStateMachine; + + async fn on_changed(self: Box, state_machine: &mut Self::StateMachine) -> StateResult { + let unique_data = state_machine.unique_data(); + + let input = ValidatePaymentInput { + payment_tx: self.maker_payment.tx_hex.0.clone(), + time_lock_duration: state_machine.lock_duration, + time_lock: self.negotiation_data.maker_payment_locktime, + other_pub: self.negotiation_data.maker_coin_htlc_pub_from_maker.to_bytes(), + secret_hash: self.negotiation_data.maker_secret_hash.clone(), + amount: state_machine.maker_volume.to_decimal(), + swap_contract_address: None, + try_spv_proof_until: state_machine.maker_payment_conf_timeout(), + confirmations: state_machine.conf_settings.maker_coin_confs, + unique_swap_data: unique_data.clone(), + watcher_reward: None, + }; + if let Err(e) = state_machine.maker_coin.validate_maker_payment(input).compat().await { + let next_state = TakerFundingRefundRequired { + maker_coin_start_block: self.maker_coin_start_block, + taker_coin_start_block: self.taker_coin_start_block, + taker_funding: self.taker_funding, + negotiation_data: self.negotiation_data, + reason: TakerFundingRefundReason::MakerPaymentValidationFailed(e.to_string()), + }; + return Self::change_state(next_state, state_machine).await; + }; + + let args = GenTakerFundingSpendArgs { + funding_tx: &self.taker_funding, + maker_pub: &self.negotiation_data.taker_coin_htlc_pub_from_maker, + taker_pub: &state_machine.taker_coin.derive_htlc_pubkey_v2(&unique_data), + funding_time_lock: state_machine.taker_funding_locktime(), + taker_secret_hash: &state_machine.taker_secret_hash(), + taker_payment_time_lock: state_machine.taker_payment_locktime(), + maker_secret_hash: &self.negotiation_data.maker_secret_hash, + }; + + if let Err(e) = state_machine + .taker_coin + .validate_taker_funding_spend_preimage(&args, &self.funding_spend_preimage) + .await + { + let next_state = TakerFundingRefundRequired { + maker_coin_start_block: self.maker_coin_start_block, + taker_coin_start_block: self.taker_coin_start_block, + taker_funding: self.taker_funding, + negotiation_data: self.negotiation_data, + reason: TakerFundingRefundReason::FundingSpendPreimageValidationFailed(format!("{:?}", e)), + }; + return Self::change_state(next_state, state_machine).await; + } + + let taker_payment = match state_machine + .taker_coin + .sign_and_send_taker_funding_spend(&self.funding_spend_preimage, &args, &unique_data) + .await + { + Ok(tx) => tx, + Err(e) => { + let next_state = TakerFundingRefundRequired { + maker_coin_start_block: self.maker_coin_start_block, + taker_coin_start_block: self.taker_coin_start_block, + taker_funding: self.taker_funding, + negotiation_data: self.negotiation_data, + reason: TakerFundingRefundReason::FailedToSendTakerPayment(format!("{:?}", e)), + }; + return Self::change_state(next_state, state_machine).await; + }, + }; + + info!( + "Sent taker payment {} tx {:02x} during swap {}", + state_machine.taker_coin.ticker(), + taker_payment.tx_hash(), + state_machine.uuid + ); + + let next_state = TakerPaymentSent { + maker_coin_start_block: self.maker_coin_start_block, + taker_coin_start_block: self.taker_coin_start_block, + taker_payment, + maker_payment: self.maker_payment, + negotiation_data: self.negotiation_data, + }; + Self::change_state(next_state, state_machine).await + } +} + +struct TakerPaymentSent { + maker_coin_start_block: u64, + taker_coin_start_block: u64, + taker_payment: TakerCoin::Tx, + maker_payment: TransactionIdentifier, + negotiation_data: NegotiationData, +} + +impl + TransitionFrom> + for TakerPaymentSent +{ +} + +#[async_trait] +impl State + for TakerPaymentSent +{ + type StateMachine = TakerSwapStateMachine; + + async fn on_changed(self: Box, state_machine: &mut Self::StateMachine) -> StateResult { + let taker_payment_info = TakerPaymentInfo { + tx_bytes: self.taker_payment.tx_hex(), + next_step_instructions: None, + }; + let swap_msg = SwapMessage { + inner: Some(swap_message::Inner::TakerPaymentInfo(taker_payment_info)), + }; + let _abort_handle = broadcast_swap_v2_msg_every( + state_machine.ctx.clone(), + state_machine.p2p_topic.clone(), + swap_msg, + 600., + state_machine.p2p_keypair, + ); + let input = ConfirmPaymentInput { - payment_tx: maker_payment_info.tx_bytes.clone(), + payment_tx: self.maker_payment.tx_hex.0.clone(), confirmations: state_machine.conf_settings.taker_coin_confs, requires_nota: state_machine.conf_settings.taker_coin_nota, wait_until: state_machine.maker_payment_conf_timeout(), @@ -483,76 +865,125 @@ impl State for TakerPaymentSen if let Err(e) = state_machine.maker_coin.wait_for_confirmations(input).compat().await { let next_state = TakerPaymentRefundRequired { - maker_coin: Default::default(), - taker_coin: Default::default(), taker_payment: self.taker_payment, - secret_hash: self.secret_hash, + negotiation_data: self.negotiation_data, reason: TakerPaymentRefundReason::MakerPaymentNotConfirmedInTime(e), }; return Self::change_state(next_state, state_machine).await; } let next_state = MakerPaymentConfirmed { - maker_coin: Default::default(), - taker_coin: Default::default(), maker_coin_start_block: self.maker_coin_start_block, taker_coin_start_block: self.taker_coin_start_block, - maker_payment: TransactionIdentifier { - tx_hex: maker_payment_info.tx_bytes.into(), - tx_hash: Default::default(), - }, + maker_payment: self.maker_payment, taker_payment: self.taker_payment, - secret_hash: self.secret_hash, - maker_payment_locktime: self.maker_payment_locktime, - maker_coin_htlc_pub_from_maker: self.maker_coin_htlc_pub_from_maker, - taker_coin_htlc_pub_from_maker: self.taker_coin_htlc_pub_from_maker, - maker_coin_swap_contract: self.maker_coin_swap_contract, - taker_coin_swap_contract: self.taker_coin_swap_contract, + negotiation_data: self.negotiation_data, }; Self::change_state(next_state, state_machine).await } } -impl StorableState for TakerPaymentSent { +impl StorableState + for TakerPaymentSent +{ type StateMachine = TakerSwapStateMachine; fn get_event(&self) -> <::Storage as StateMachineStorage>::Event { TakerSwapEvent::TakerPaymentSent { maker_coin_start_block: self.maker_coin_start_block, taker_coin_start_block: self.taker_coin_start_block, - taker_payment: self.taker_payment.clone(), - secret_hash: self.secret_hash.clone().into(), + taker_payment: TransactionIdentifier { + tx_hex: self.taker_payment.tx_hex().into(), + tx_hash: self.taker_payment.tx_hash(), + }, + negotiation_data: self.negotiation_data.to_stored_data(), + } + } +} + +/// Represents the reason taker funding refund +#[derive(Clone, Debug, Deserialize, Serialize)] +pub enum TakerFundingRefundReason { + DidNotReceiveMakerPayment(String), + FailedToParseFundingSpendPreimg(String), + FailedToParseFundingSpendSig(String), + FailedToSendTakerPayment(String), + MakerPaymentValidationFailed(String), + FundingSpendPreimageValidationFailed(String), +} + +struct TakerFundingRefundRequired { + maker_coin_start_block: u64, + taker_coin_start_block: u64, + taker_funding: TakerCoin::Tx, + negotiation_data: NegotiationData, + reason: TakerFundingRefundReason, +} + +impl TransitionFrom> + for TakerFundingRefundRequired +{ +} +impl + TransitionFrom> + for TakerFundingRefundRequired +{ +} + +#[async_trait] +impl State + for TakerFundingRefundRequired +{ + type StateMachine = TakerSwapStateMachine; + + async fn on_changed(self: Box, _state_machine: &mut Self::StateMachine) -> StateResult { + todo!() + } +} + +impl StorableState + for TakerFundingRefundRequired +{ + type StateMachine = TakerSwapStateMachine; + + fn get_event(&self) -> <::Storage as StateMachineStorage>::Event { + TakerSwapEvent::TakerFundingRefundRequired { + maker_coin_start_block: self.maker_coin_start_block, + taker_coin_start_block: self.taker_coin_start_block, + taker_funding: TransactionIdentifier { + tx_hex: self.taker_funding.tx_hex().into(), + tx_hash: self.taker_funding.tx_hash(), + }, + negotiation_data: self.negotiation_data.to_stored_data(), + reason: self.reason.clone(), } } } #[derive(Debug)] enum TakerPaymentRefundReason { - DidNotReceiveMakerPayment(String), MakerPaymentNotConfirmedInTime(String), FailedToGenerateSpendPreimage(String), MakerDidNotSpendInTime(String), } -struct TakerPaymentRefundRequired { - maker_coin: PhantomData, - taker_coin: PhantomData, - taker_payment: TransactionIdentifier, - secret_hash: Vec, +struct TakerPaymentRefundRequired { + taker_payment: TakerCoin::Tx, + negotiation_data: NegotiationData, reason: TakerPaymentRefundReason, } -impl TransitionFrom> +impl TransitionFrom> for TakerPaymentRefundRequired { } -impl TransitionFrom> +impl TransitionFrom> for TakerPaymentRefundRequired { } #[async_trait] -impl State +impl State for TakerPaymentRefundRequired { type StateMachine = TakerSwapStateMachine; @@ -566,52 +997,50 @@ impl State } } -impl StorableState +impl StorableState for TakerPaymentRefundRequired { type StateMachine = TakerSwapStateMachine; fn get_event(&self) -> <::Storage as StateMachineStorage>::Event { TakerSwapEvent::TakerPaymentRefundRequired { - taker_payment: self.taker_payment.clone(), - secret_hash: self.secret_hash.clone().into(), + taker_payment: TransactionIdentifier { + tx_hex: self.taker_payment.tx_hex().into(), + tx_hash: self.taker_payment.tx_hash(), + }, + negotiation_data: self.negotiation_data.to_stored_data(), } } } -struct MakerPaymentConfirmed { - maker_coin: PhantomData, - taker_coin: PhantomData, +struct MakerPaymentConfirmed { maker_coin_start_block: u64, taker_coin_start_block: u64, maker_payment: TransactionIdentifier, - taker_payment: TransactionIdentifier, - secret_hash: Vec, - maker_payment_locktime: u64, - maker_coin_htlc_pub_from_maker: Vec, - taker_coin_htlc_pub_from_maker: Vec, - maker_coin_swap_contract: Option>, - taker_coin_swap_contract: Option>, + taker_payment: TakerCoin::Tx, + negotiation_data: NegotiationData, } -impl TransitionFrom> +impl TransitionFrom> for MakerPaymentConfirmed { } #[async_trait] -impl State for MakerPaymentConfirmed { +impl State + for MakerPaymentConfirmed +{ type StateMachine = TakerSwapStateMachine; async fn on_changed(self: Box, state_machine: &mut Self::StateMachine) -> StateResult { let unique_data = state_machine.unique_data(); let args = GenTakerPaymentSpendArgs { - taker_tx: &self.taker_payment.tx_hex.0, + taker_tx: &self.taker_payment, time_lock: state_machine.taker_payment_locktime(), - secret_hash: &self.secret_hash, - maker_pub: &self.maker_coin_htlc_pub_from_maker, - taker_pub: &state_machine.taker_coin.derive_htlc_pubkey(&unique_data), + secret_hash: &self.negotiation_data.maker_secret_hash, + maker_pub: &self.negotiation_data.taker_coin_htlc_pub_from_maker, + taker_pub: &state_machine.taker_coin.derive_htlc_pubkey_v2(&unique_data), dex_fee_pub: &DEX_FEE_ADDR_RAW_PUBKEY, dex_fee_amount: state_machine.dex_fee.to_decimal(), premium_amount: Default::default(), @@ -626,10 +1055,8 @@ impl State for MakerPaymentCon Ok(p) => p, Err(e) => { let next_state = TakerPaymentRefundRequired { - maker_coin: Default::default(), - taker_coin: Default::default(), taker_payment: self.taker_payment, - secret_hash: self.secret_hash, + negotiation_data: self.negotiation_data, reason: TakerPaymentRefundReason::FailedToGenerateSpendPreimage(e.to_string()), }; return Self::change_state(next_state, state_machine).await; @@ -637,12 +1064,8 @@ impl State for MakerPaymentCon }; let preimage_msg = TakerPaymentSpendPreimage { - signature: preimage.signature, - tx_preimage: if !preimage.preimage.is_empty() { - Some(preimage.preimage) - } else { - None - }, + signature: preimage.signature.to_bytes(), + tx_preimage: preimage.preimage.to_bytes(), }; let swap_msg = SwapMessage { inner: Some(swap_message::Inner::TakerPaymentSpendPreimage(preimage_msg)), @@ -657,11 +1080,15 @@ impl State for MakerPaymentCon ); let wait_args = WaitForHTLCTxSpendArgs { - tx_bytes: &self.taker_payment.tx_hex.0, - secret_hash: &self.secret_hash, + tx_bytes: &self.taker_payment.tx_hex(), + secret_hash: &self.negotiation_data.maker_secret_hash, wait_until: state_machine.taker_payment_locktime(), from_block: self.taker_coin_start_block, - swap_contract_address: &self.taker_coin_swap_contract.clone().map(|bytes| bytes.into()), + swap_contract_address: &self + .negotiation_data + .taker_coin_swap_contract + .clone() + .map(|bytes| bytes.into()), check_every: 10.0, watcher_reward: false, }; @@ -674,10 +1101,8 @@ impl State for MakerPaymentCon Ok(tx) => tx, Err(e) => { let next_state = TakerPaymentRefundRequired { - maker_coin: Default::default(), - taker_coin: Default::default(), taker_payment: self.taker_payment, - secret_hash: self.secret_hash, + negotiation_data: self.negotiation_data, reason: TakerPaymentRefundReason::MakerDidNotSpendInTime(format!("{:?}", e)), }; return Self::change_state(next_state, state_machine).await; @@ -691,8 +1116,6 @@ impl State for MakerPaymentCon ); let next_state = TakerPaymentSpent { - maker_coin: Default::default(), - taker_coin: Default::default(), maker_coin_start_block: self.maker_coin_start_block, taker_coin_start_block: self.taker_coin_start_block, maker_payment: self.maker_payment, @@ -701,57 +1124,48 @@ impl State for MakerPaymentCon tx_hex: taker_payment_spend.tx_hex().into(), tx_hash: taker_payment_spend.tx_hash(), }, - secret_hash: self.secret_hash, - maker_payment_locktime: self.maker_payment_locktime, - maker_coin_htlc_pub_from_maker: self.maker_coin_htlc_pub_from_maker, - taker_coin_htlc_pub_from_maker: self.taker_coin_htlc_pub_from_maker, - maker_coin_swap_contract: self.maker_coin_swap_contract, - taker_coin_swap_contract: self.taker_coin_swap_contract, + negotiation_data: self.negotiation_data, }; Self::change_state(next_state, state_machine).await } } -impl StorableState +impl StorableState for MakerPaymentConfirmed { type StateMachine = TakerSwapStateMachine; fn get_event(&self) -> <::Storage as StateMachineStorage>::Event { - TakerSwapEvent::BothPaymentsSentAndConfirmed { + TakerSwapEvent::MakerPaymentConfirmed { maker_coin_start_block: self.maker_coin_start_block, taker_coin_start_block: self.taker_coin_start_block, maker_payment: self.maker_payment.clone(), - taker_payment: self.taker_payment.clone(), - secret_hash: self.secret_hash.clone().into(), + taker_payment: TransactionIdentifier { + tx_hex: self.taker_payment.tx_hex().into(), + tx_hash: self.taker_payment.tx_hash(), + }, + negotiation_data: self.negotiation_data.to_stored_data(), } } } #[allow(dead_code)] -struct TakerPaymentSpent { - maker_coin: PhantomData, - taker_coin: PhantomData, +struct TakerPaymentSpent { maker_coin_start_block: u64, taker_coin_start_block: u64, maker_payment: TransactionIdentifier, - taker_payment: TransactionIdentifier, + taker_payment: TakerCoin::Tx, taker_payment_spend: TransactionIdentifier, - secret_hash: Vec, - maker_payment_locktime: u64, - maker_coin_htlc_pub_from_maker: Vec, - taker_coin_htlc_pub_from_maker: Vec, - maker_coin_swap_contract: Option>, - taker_coin_swap_contract: Option>, + negotiation_data: NegotiationData, } -impl TransitionFrom> +impl TransitionFrom> for TakerPaymentSpent { } #[async_trait] -impl State +impl State for TakerPaymentSpent { type StateMachine = TakerSwapStateMachine; @@ -759,23 +1173,31 @@ impl S async fn on_changed(self: Box, state_machine: &mut Self::StateMachine) -> StateResult { let secret = match state_machine .taker_coin - .extract_secret(&self.secret_hash, &self.taker_payment_spend.tx_hex.0, false) + .extract_secret( + &self.negotiation_data.maker_secret_hash, + &self.taker_payment_spend.tx_hex.0, + false, + ) .await { Ok(s) => s, Err(e) => { - let next_state = Aborted::new(format!("Couldn't extract secret from taker payment spend {}", e)); - return Self::change_state(next_state, state_machine).await; + let reason = AbortReason::CouldNotExtractSecret(e); + return Self::change_state(Aborted::new(reason), state_machine).await; }, }; let args = SpendPaymentArgs { other_payment_tx: &self.maker_payment.tx_hex.0, - time_lock: self.maker_payment_locktime, - other_pubkey: &self.maker_coin_htlc_pub_from_maker, + time_lock: self.negotiation_data.maker_payment_locktime, + other_pubkey: &self.negotiation_data.maker_coin_htlc_pub_from_maker.to_bytes(), secret: &secret, - secret_hash: &self.secret_hash, - swap_contract_address: &self.maker_coin_swap_contract.clone().map(|bytes| bytes.into()), + secret_hash: &self.negotiation_data.maker_secret_hash, + swap_contract_address: &self + .negotiation_data + .maker_coin_swap_contract + .clone() + .map(|bytes| bytes.into()), swap_unique_data: &state_machine.unique_data(), watcher_reward: false, }; @@ -787,8 +1209,8 @@ impl S { Ok(tx) => tx, Err(e) => { - let next_state = Aborted::new(format!("Failed to spend maker payment {:?}", e)); - return Self::change_state(next_state, state_machine).await; + let reason = AbortReason::FailedToSpendMakerPayment(format!("{:?}", e)); + return Self::change_state(Aborted::new(reason), state_machine).await; }, }; info!( @@ -799,7 +1221,6 @@ impl S ); let next_state = MakerPaymentSpent { maker_coin: Default::default(), - taker_coin: Default::default(), maker_coin_start_block: self.maker_coin_start_block, taker_coin_start_block: self.taker_coin_start_block, maker_payment: self.maker_payment, @@ -814,7 +1235,9 @@ impl S } } -impl StorableState for TakerPaymentSpent { +impl StorableState + for TakerPaymentSpent +{ type StateMachine = TakerSwapStateMachine; fn get_event(&self) -> <::Storage as StateMachineStorage>::Event { @@ -822,30 +1245,34 @@ impl StorableState for Tak maker_coin_start_block: self.maker_coin_start_block, taker_coin_start_block: self.taker_coin_start_block, maker_payment: self.maker_payment.clone(), - taker_payment: self.taker_payment.clone(), + taker_payment: TransactionIdentifier { + tx_hex: self.taker_payment.tx_hex().into(), + tx_hash: self.taker_payment.tx_hash(), + }, taker_payment_spend: self.taker_payment_spend.clone(), - secret: Vec::new().into(), + negotiation_data: self.negotiation_data.to_stored_data(), } } } -struct MakerPaymentSpent { +struct MakerPaymentSpent { maker_coin: PhantomData, - taker_coin: PhantomData, maker_coin_start_block: u64, taker_coin_start_block: u64, maker_payment: TransactionIdentifier, - taker_payment: TransactionIdentifier, + taker_payment: TakerCoin::Tx, taker_payment_spend: TransactionIdentifier, maker_payment_spend: TransactionIdentifier, } -impl TransitionFrom> +impl TransitionFrom> for MakerPaymentSpent { } -impl StorableState for MakerPaymentSpent { +impl StorableState + for MakerPaymentSpent +{ type StateMachine = TakerSwapStateMachine; fn get_event(&self) -> <::Storage as StateMachineStorage>::Event { @@ -853,7 +1280,10 @@ impl StorableState for Mak maker_coin_start_block: self.maker_coin_start_block, taker_coin_start_block: self.taker_coin_start_block, maker_payment: self.maker_payment.clone(), - taker_payment: self.taker_payment.clone(), + taker_payment: TransactionIdentifier { + tx_hex: self.taker_payment.tx_hex().into(), + tx_hash: self.taker_payment.tx_hash(), + }, taker_payment_spend: self.taker_payment_spend.clone(), maker_payment_spend: self.maker_payment_spend.clone(), } @@ -861,7 +1291,7 @@ impl StorableState for Mak } #[async_trait] -impl State +impl State for MakerPaymentSpent { type StateMachine = TakerSwapStateMachine; @@ -871,14 +1301,32 @@ impl State } } +/// Represents possible reasons of taker swap being aborted +#[derive(Clone, Debug, Deserialize, Display, Serialize)] +pub enum AbortReason { + FailedToGetMakerCoinBlock(String), + FailedToGetTakerCoinBlock(String), + BalanceCheckFailure(String), + DidNotReceiveMakerNegotiation(String), + TooLargeStartedAtDiff(u64), + FailedToParsePubkey(String), + MakerProvidedInvalidLocktime(u64), + SecretHashUnexpectedLen(usize), + DidNotReceiveMakerNegotiated(String), + MakerDidNotNegotiate(String), + FailedToSendTakerFunding(String), + CouldNotExtractSecret(String), + FailedToSpendMakerPayment(String), +} + struct Aborted { maker_coin: PhantomData, taker_coin: PhantomData, - reason: String, + reason: AbortReason, } impl Aborted { - fn new(reason: String) -> Aborted { + fn new(reason: AbortReason) -> Aborted { Aborted { maker_coin: Default::default(), taker_coin: Default::default(), @@ -911,8 +1359,14 @@ impl StorableState for Abo impl TransitionFrom> for Aborted {} impl TransitionFrom> for Aborted {} -impl TransitionFrom> for Aborted {} -impl TransitionFrom> for Aborted {} +impl TransitionFrom> + for Aborted +{ +} +impl TransitionFrom> + for Aborted +{ +} struct Completed { maker_coin: PhantomData, @@ -948,4 +1402,7 @@ impl LastSta } } -impl TransitionFrom> for Completed {} +impl TransitionFrom> + for Completed +{ +} diff --git a/mm2src/mm2_main/tests/docker_tests/swap_proto_v2_tests.rs b/mm2src/mm2_main/tests/docker_tests/swap_proto_v2_tests.rs index 331b5b918b..7276fd1847 100644 --- a/mm2src/mm2_main/tests/docker_tests/swap_proto_v2_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/swap_proto_v2_tests.rs @@ -1,65 +1,60 @@ use crate::{generate_utxo_coin_with_random_privkey, MYCOIN, MYCOIN1}; use bitcrypto::dhash160; use coins::utxo::UtxoCommonOps; -use coins::{GenTakerPaymentSpendArgs, RefundPaymentArgs, SendCombinedTakerPaymentArgs, SwapOpsV2, Transaction, - TransactionEnum, ValidateTakerPaymentArgs}; -use common::{block_on, now_sec, DEX_FEE_ADDR_RAW_PUBKEY}; -use mm2_test_helpers::for_tests::{enable_native, mm_dump, mycoin1_conf, mycoin_conf, start_swaps, MarketMakerIt, - Mm2TestConf}; +use coins::{GenTakerFundingSpendArgs, RefundFundingSecretArgs, RefundPaymentArgs, SendTakerFundingArgs, SwapOpsV2, + Transaction, ValidateTakerFundingArgs}; +use common::{block_on, now_sec}; +use mm2_test_helpers::for_tests::{enable_native, mm_dump, my_swap_status, mycoin1_conf, mycoin_conf, start_swaps, + MarketMakerIt, Mm2TestConf}; use script::{Builder, Opcode}; +use serialization::serialize; #[test] -fn send_and_refund_taker_payment() { +fn send_and_refund_taker_funding_timelock() { let (_mm_arc, coin, _privkey) = generate_utxo_coin_with_random_privkey(MYCOIN, 1000.into()); let time_lock = now_sec() - 1000; - let secret_hash = &[0; 20]; - let other_pub = coin.my_public_key().unwrap(); + let taker_secret_hash = &[0; 20]; + let maker_pub = coin.my_public_key().unwrap(); - let send_args = SendCombinedTakerPaymentArgs { + let send_args = SendTakerFundingArgs { time_lock, - secret_hash, - other_pub, + taker_secret_hash, + maker_pub, dex_fee_amount: "0.01".parse().unwrap(), premium_amount: "0.1".parse().unwrap(), trading_amount: 1.into(), swap_unique_data: &[], }; - let taker_payment_tx = block_on(coin.send_combined_taker_payment(send_args)).unwrap(); - println!("{:02x}", taker_payment_tx.tx_hash()); - let taker_payment_utxo_tx = match taker_payment_tx { - TransactionEnum::UtxoTx(tx) => tx, - unexpected => panic!("Unexpected tx {:?}", unexpected), - }; - // tx must have 3 outputs: actual payment, OP_RETURN containing the secret hash and change - assert_eq!(3, taker_payment_utxo_tx.outputs.len()); + let taker_funding_utxo_tx = block_on(coin.send_taker_funding(send_args)).unwrap(); + println!("{:02x}", taker_funding_utxo_tx.tx_hash()); + // tx must have 3 outputs: actual funding, OP_RETURN containing the secret hash and change + assert_eq!(3, taker_funding_utxo_tx.outputs.len()); // dex_fee_amount + premium_amount + trading_amount let expected_amount = 111000000u64; - assert_eq!(expected_amount, taker_payment_utxo_tx.outputs[0].value); + assert_eq!(expected_amount, taker_funding_utxo_tx.outputs[0].value); let expected_op_return = Builder::default() .push_opcode(Opcode::OP_RETURN) .push_data(&[0; 20]) .into_bytes(); - assert_eq!(expected_op_return, taker_payment_utxo_tx.outputs[1].script_pubkey); - - let taker_payment_bytes = taker_payment_utxo_tx.tx_hex(); + assert_eq!(expected_op_return, taker_funding_utxo_tx.outputs[1].script_pubkey); - let validate_args = ValidateTakerPaymentArgs { - taker_tx: &taker_payment_bytes, + let validate_args = ValidateTakerFundingArgs { + funding_tx: &taker_funding_utxo_tx, time_lock, - secret_hash, - other_pub, + taker_secret_hash, + other_pub: maker_pub, dex_fee_amount: "0.01".parse().unwrap(), premium_amount: "0.1".parse().unwrap(), trading_amount: 1.into(), swap_unique_data: &[], }; - block_on(coin.validate_combined_taker_payment(validate_args)).unwrap(); + block_on(coin.validate_taker_funding(validate_args)).unwrap(); let refund_args = RefundPaymentArgs { - payment_tx: &taker_payment_bytes, + payment_tx: &serialize(&taker_funding_utxo_tx).take(), time_lock, other_pubkey: coin.my_public_key().unwrap(), secret_hash: &[0; 20], @@ -68,71 +63,130 @@ fn send_and_refund_taker_payment() { watcher_reward: false, }; - let refund_tx = block_on(coin.refund_combined_taker_payment(refund_args)).unwrap(); + let refund_tx = block_on(coin.refund_taker_funding_timelock(refund_args)).unwrap(); println!("{:02x}", refund_tx.tx_hash()); } #[test] -fn send_and_spend_taker_payment() { - let (_, taker_coin, _) = generate_utxo_coin_with_random_privkey(MYCOIN, 1000.into()); - let (_, maker_coin, _) = generate_utxo_coin_with_random_privkey(MYCOIN, 1000.into()); +fn send_and_refund_taker_funding_secret() { + let (_mm_arc, coin, _privkey) = generate_utxo_coin_with_random_privkey(MYCOIN, 1000.into()); let time_lock = now_sec() - 1000; - let secret = [1; 32]; - let secret_hash = dhash160(&secret); - let send_args = SendCombinedTakerPaymentArgs { + let taker_secret = [0; 32]; + let taker_secret_hash = dhash160(&taker_secret); + let maker_pub = coin.my_public_key().unwrap(); + + let send_args = SendTakerFundingArgs { time_lock, - secret_hash: secret_hash.as_slice(), - other_pub: maker_coin.my_public_key().unwrap(), + taker_secret_hash: taker_secret_hash.as_slice(), + maker_pub, dex_fee_amount: "0.01".parse().unwrap(), premium_amount: "0.1".parse().unwrap(), trading_amount: 1.into(), swap_unique_data: &[], }; - let taker_payment_tx = block_on(taker_coin.send_combined_taker_payment(send_args)).unwrap(); - println!("taker_payment_tx hash {:02x}", taker_payment_tx.tx_hash()); - let taker_payment_utxo_tx = match taker_payment_tx { - TransactionEnum::UtxoTx(tx) => tx, - unexpected => panic!("Unexpected tx {:?}", unexpected), - }; + let taker_funding_utxo_tx = block_on(coin.send_taker_funding(send_args)).unwrap(); + println!("{:02x}", taker_funding_utxo_tx.tx_hash()); + // tx must have 3 outputs: actual funding, OP_RETURN containing the secret hash and change + assert_eq!(3, taker_funding_utxo_tx.outputs.len()); + + // dex_fee_amount + premium_amount + trading_amount + let expected_amount = 111000000u64; + assert_eq!(expected_amount, taker_funding_utxo_tx.outputs[0].value); + + let expected_op_return = Builder::default() + .push_opcode(Opcode::OP_RETURN) + .push_data(taker_secret_hash.as_slice()) + .into_bytes(); + assert_eq!(expected_op_return, taker_funding_utxo_tx.outputs[1].script_pubkey); - let taker_payment_bytes = taker_payment_utxo_tx.tx_hex(); - let validate_args = ValidateTakerPaymentArgs { - taker_tx: &taker_payment_bytes, + let validate_args = ValidateTakerFundingArgs { + funding_tx: &taker_funding_utxo_tx, time_lock, - secret_hash: secret_hash.as_slice(), - other_pub: taker_coin.my_public_key().unwrap(), + taker_secret_hash: taker_secret_hash.as_slice(), + other_pub: maker_pub, dex_fee_amount: "0.01".parse().unwrap(), premium_amount: "0.1".parse().unwrap(), trading_amount: 1.into(), swap_unique_data: &[], }; - block_on(maker_coin.validate_combined_taker_payment(validate_args)).unwrap(); + block_on(coin.validate_taker_funding(validate_args)).unwrap(); - let gen_preimage_args = GenTakerPaymentSpendArgs { - taker_tx: &taker_payment_utxo_tx.tx_hex(), + let refund_args = RefundFundingSecretArgs { + funding_tx: &taker_funding_utxo_tx, time_lock, - secret_hash: secret_hash.as_slice(), - maker_pub: maker_coin.my_public_key().unwrap(), - taker_pub: taker_coin.my_public_key().unwrap(), - dex_fee_pub: &DEX_FEE_ADDR_RAW_PUBKEY, + maker_pubkey: maker_pub, + taker_secret: &taker_secret, + taker_secret_hash: taker_secret_hash.as_slice(), + swap_unique_data: &[], + swap_contract_address: &None, + watcher_reward: false, + }; + + let refund_tx = block_on(coin.refund_taker_funding_secret(refund_args)).unwrap(); + println!("{:02x}", refund_tx.tx_hash()); +} + +#[test] +fn send_and_spend_taker_funding() { + let (_mm_arc, taker_coin, _privkey) = generate_utxo_coin_with_random_privkey(MYCOIN, 1000.into()); + let (_mm_arc, maker_coin, _privkey) = generate_utxo_coin_with_random_privkey(MYCOIN, 1000.into()); + + let funding_time_lock = now_sec() - 1000; + let taker_secret_hash = &[0; 20]; + + let taker_pub = taker_coin.my_public_key().unwrap(); + let maker_pub = maker_coin.my_public_key().unwrap(); + + let send_args = SendTakerFundingArgs { + time_lock: funding_time_lock, + taker_secret_hash, + maker_pub, dex_fee_amount: "0.01".parse().unwrap(), premium_amount: "0.1".parse().unwrap(), trading_amount: 1.into(), + swap_unique_data: &[], }; - let preimage_with_taker_sig = - block_on(taker_coin.gen_taker_payment_spend_preimage(&gen_preimage_args, &[])).unwrap(); - - block_on(maker_coin.validate_taker_payment_spend_preimage(&gen_preimage_args, &preimage_with_taker_sig)).unwrap(); - - let taker_payment_spend = block_on(maker_coin.sign_and_broadcast_taker_payment_spend( - &preimage_with_taker_sig, - &gen_preimage_args, - &secret, - &[], - )) - .unwrap(); - println!("taker_payment_spend hash {:02x}", taker_payment_spend.tx_hash()); + let taker_funding_utxo_tx = block_on(taker_coin.send_taker_funding(send_args)).unwrap(); + println!("Funding tx {:02x}", taker_funding_utxo_tx.tx_hash()); + // tx must have 3 outputs: actual funding, OP_RETURN containing the secret hash and change + assert_eq!(3, taker_funding_utxo_tx.outputs.len()); + + // dex_fee_amount + premium_amount + trading_amount + let expected_amount = 111000000u64; + assert_eq!(expected_amount, taker_funding_utxo_tx.outputs[0].value); + + let expected_op_return = Builder::default() + .push_opcode(Opcode::OP_RETURN) + .push_data(&[0; 20]) + .into_bytes(); + assert_eq!(expected_op_return, taker_funding_utxo_tx.outputs[1].script_pubkey); + + let validate_args = ValidateTakerFundingArgs { + funding_tx: &taker_funding_utxo_tx, + time_lock: funding_time_lock, + taker_secret_hash, + other_pub: taker_pub, + dex_fee_amount: "0.01".parse().unwrap(), + premium_amount: "0.1".parse().unwrap(), + trading_amount: 1.into(), + swap_unique_data: &[], + }; + block_on(maker_coin.validate_taker_funding(validate_args)).unwrap(); + + let preimage_args = GenTakerFundingSpendArgs { + funding_tx: &taker_funding_utxo_tx, + maker_pub, + taker_pub, + funding_time_lock, + taker_secret_hash, + taker_payment_time_lock: 0, + maker_secret_hash: &[0; 20], + }; + let preimage = block_on(maker_coin.gen_taker_funding_spend_preimage(&preimage_args, &[])).unwrap(); + + let payment_tx = block_on(taker_coin.sign_and_send_taker_funding_spend(&preimage, &preimage_args, &[])).unwrap(); + println!("Taker payment tx {:02x}", payment_tx.tx_hash()); } #[test] @@ -144,6 +198,7 @@ fn test_v2_swap_utxo_utxo() { let bob_conf = Mm2TestConf::seednode_trade_v2(&format!("0x{}", hex::encode(bob_priv_key)), &coins); let mut mm_bob = MarketMakerIt::start(bob_conf.conf, bob_conf.rpc_password, None).unwrap(); let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); + log!("Bob log path: {}", mm_bob.log_path.display()); let alice_conf = Mm2TestConf::light_node_trade_v2(&format!("0x{}", hex::encode(alice_priv_key)), &coins, &[&mm_bob @@ -151,6 +206,7 @@ fn test_v2_swap_utxo_utxo() { .to_string()]); let mut mm_alice = MarketMakerIt::start(alice_conf.conf, alice_conf.rpc_password, None).unwrap(); let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); + log!("Alice log path: {}", mm_alice.log_path.display()); log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); @@ -170,6 +226,12 @@ fn test_v2_swap_utxo_utxo() { for uuid in uuids { let expected_msg = format!("Swap {} has been completed", uuid); block_on(mm_bob.wait_for_log(60., |log| log.contains(&expected_msg))).unwrap(); - block_on(mm_alice.wait_for_log(60., |log| log.contains(&expected_msg))).unwrap(); + block_on(mm_alice.wait_for_log(30., |log| log.contains(&expected_msg))).unwrap(); + + let maker_swap_status = block_on(my_swap_status(&mm_bob, &uuid)); + println!("{:?}", maker_swap_status); + + let taker_swap_status = block_on(my_swap_status(&mm_alice, &uuid)); + println!("{:?}", taker_swap_status); } } diff --git a/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs b/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs index 3fce082f0f..4f3a10593f 100644 --- a/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs @@ -12,7 +12,7 @@ use coins::{ConfirmPaymentInput, FoundSwapTxSpend, MarketCoinOps, MmCoin, MmCoin use common::{block_on, now_sec, wait_until_sec, DEX_FEE_ADDR_RAW_PUBKEY}; use crypto::privkey::{key_pair_from_secret, key_pair_from_seed}; use futures01::Future; -use mm2_main::mm2::lp_swap::{dex_fee_amount, dex_fee_amount_from_taker_coin, get_payment_locktime, MakerSwap, +use mm2_main::mm2::lp_swap::{dex_fee_amount, dex_fee_amount_from_taker_coin, generate_secret, get_payment_locktime, MAKER_PAYMENT_SENT_LOG, MAKER_PAYMENT_SPEND_FOUND_LOG, MAKER_PAYMENT_SPEND_SENT_LOG, REFUND_TEST_FAILURE_LOG, SWAP_FINISHED_LOG, TAKER_PAYMENT_REFUND_SENT_LOG, WATCHER_MESSAGE_SENT_LOG}; @@ -1457,7 +1457,7 @@ fn test_watcher_validate_taker_payment_utxo() { let (_ctx, maker_coin, _) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000u64.into()); let maker_pubkey = maker_coin.my_public_key().unwrap(); - let secret_hash = dhash160(&MakerSwap::generate_secret().unwrap()); + let secret_hash = dhash160(&generate_secret().unwrap()); let taker_payment = taker_coin .send_taker_payment(SendPaymentArgs { @@ -1535,7 +1535,7 @@ fn test_watcher_validate_taker_payment_utxo() { } // Used to get wrong swap id - let wrong_secret_hash = dhash160(&MakerSwap::generate_secret().unwrap()); + let wrong_secret_hash = dhash160(&generate_secret().unwrap()); let error = taker_coin .watcher_validate_taker_payment(WatcherValidatePaymentInput { payment_tx: taker_payment.tx_hex(), @@ -1672,7 +1672,7 @@ fn test_watcher_validate_taker_payment_eth() { let time_lock = wait_for_confirmation_until; let taker_amount = BigDecimal::from_str("0.01").unwrap(); let maker_amount = BigDecimal::from_str("0.01").unwrap(); - let secret_hash = dhash160(&MakerSwap::generate_secret().unwrap()); + let secret_hash = dhash160(&generate_secret().unwrap()); let watcher_reward = Some( block_on(taker_coin.get_taker_watcher_reward( &MmCoinEnum::from(taker_coin.clone()), @@ -1793,7 +1793,7 @@ fn test_watcher_validate_taker_payment_eth() { } // Used to get wrong swap id - let wrong_secret_hash = dhash160(&MakerSwap::generate_secret().unwrap()); + let wrong_secret_hash = dhash160(&generate_secret().unwrap()); let error = taker_coin .watcher_validate_taker_payment(coins::WatcherValidatePaymentInput { payment_tx: taker_payment.tx_hex(), @@ -1915,7 +1915,7 @@ fn test_watcher_validate_taker_payment_erc20() { let wait_for_confirmation_until = wait_until_sec(time_lock_duration); let time_lock = wait_for_confirmation_until; - let secret_hash = dhash160(&MakerSwap::generate_secret().unwrap()); + let secret_hash = dhash160(&generate_secret().unwrap()); let taker_amount = BigDecimal::from_str("0.01").unwrap(); let maker_amount = BigDecimal::from_str("0.01").unwrap(); @@ -2040,7 +2040,7 @@ fn test_watcher_validate_taker_payment_erc20() { } // Used to get wrong swap id - let wrong_secret_hash = dhash160(&MakerSwap::generate_secret().unwrap()); + let wrong_secret_hash = dhash160(&generate_secret().unwrap()); let error = taker_coin .watcher_validate_taker_payment(WatcherValidatePaymentInput { payment_tx: taker_payment.tx_hex(), @@ -2156,7 +2156,7 @@ fn test_taker_validates_taker_payment_refund_utxo() { let (_ctx, maker_coin, _) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000u64.into()); let maker_pubkey = maker_coin.my_public_key().unwrap(); - let secret_hash = dhash160(&MakerSwap::generate_secret().unwrap()); + let secret_hash = dhash160(&generate_secret().unwrap()); let taker_payment = taker_coin .send_taker_payment(SendPaymentArgs { @@ -2243,7 +2243,7 @@ fn test_taker_validates_taker_payment_refund_eth() { let time_lock = now_sec() - 10; let taker_amount = BigDecimal::from_str("0.001").unwrap(); let maker_amount = BigDecimal::from_str("0.001").unwrap(); - let secret_hash = dhash160(&MakerSwap::generate_secret().unwrap()); + let secret_hash = dhash160(&generate_secret().unwrap()); let watcher_reward = block_on(taker_coin.get_taker_watcher_reward( &MmCoinEnum::from(taker_coin.clone()), @@ -2562,7 +2562,7 @@ fn test_taker_validates_taker_payment_refund_erc20() { let wait_for_confirmation_until = wait_until_sec(time_lock_duration); let time_lock = now_sec() - 10; - let secret_hash = dhash160(&MakerSwap::generate_secret().unwrap()); + let secret_hash = dhash160(&generate_secret().unwrap()); let taker_amount = BigDecimal::from_str("0.001").unwrap(); let maker_amount = BigDecimal::from_str("0.001").unwrap(); @@ -2684,7 +2684,7 @@ fn test_taker_validates_maker_payment_spend_utxo() { let taker_pubkey = taker_coin.my_public_key().unwrap(); let maker_pubkey = maker_coin.my_public_key().unwrap(); - let secret = MakerSwap::generate_secret().unwrap(); + let secret = generate_secret().unwrap(); let secret_hash = dhash160(&secret); let maker_payment = maker_coin @@ -2771,7 +2771,7 @@ fn test_taker_validates_maker_payment_spend_eth() { let time_lock = wait_for_confirmation_until; let maker_amount = BigDecimal::from_str("0.001").unwrap(); - let secret = MakerSwap::generate_secret().unwrap(); + let secret = generate_secret().unwrap(); let secret_hash = dhash160(&secret); let watcher_reward = block_on(maker_coin.get_maker_watcher_reward( @@ -3092,7 +3092,7 @@ fn test_taker_validates_maker_payment_spend_erc20() { let time_lock = wait_for_confirmation_until; let maker_amount = BigDecimal::from_str("0.001").unwrap(); - let secret = MakerSwap::generate_secret().unwrap(); + let secret = generate_secret().unwrap(); let secret_hash = dhash160(&secret); let watcher_reward = block_on(maker_coin.get_maker_watcher_reward( diff --git a/mm2src/mm2_number/src/mm_number.rs b/mm2src/mm2_number/src/mm_number.rs index 3934f02391..6e3c5896a6 100644 --- a/mm2src/mm2_number/src/mm_number.rs +++ b/mm2src/mm2_number/src/mm_number.rs @@ -3,7 +3,7 @@ use crate::{from_dec_to_ratio, from_ratio_to_dec}; use bigdecimal::BigDecimal; use core::ops::{Add, AddAssign, Div, Mul, Sub}; use num_bigint::BigInt; -use num_rational::BigRational; +use num_rational::{BigRational, ParseRatioError}; use num_traits::CheckedDiv; use num_traits::Zero; use serde::Serialize; @@ -228,11 +228,20 @@ impl MmNumber { /// Get BigDecimal representation pub fn to_decimal(&self) -> BigDecimal { from_ratio_to_dec(&self.0) } + /// Returns the numerator of the internal BigRational pub fn numer(&self) -> &BigInt { self.0.numer() } + /// Returns the denominator of the internal BigRational pub fn denom(&self) -> &BigInt { self.0.denom() } + /// Returns whether the number is zero pub fn is_zero(&self) -> bool { self.0.is_zero() } + + /// Returns the stringified representation of a number in a format like "1/3". + pub fn to_fraction_string(&self) -> String { self.0.to_string() } + + /// Attempts to parse a number from string, expects input to have fraction format like "1/3". + pub fn from_fraction_string(input: &str) -> Result { Ok(MmNumber(input.parse()?)) } } impl From for MmNumber { @@ -399,4 +408,14 @@ mod tests { assert_eq!(actual.num, expected); } + + #[test] + fn test_from_to_fraction_string() { + let input = "1000/999"; + let mm_num = MmNumber::from_fraction_string(input).unwrap(); + assert_eq!(*mm_num.numer(), BigInt::from(1000)); + assert_eq!(*mm_num.denom(), BigInt::from(999)); + + assert_eq!(input, mm_num.to_fraction_string()); + } } diff --git a/mm2src/mm2_state_machine/src/state_machine.rs b/mm2src/mm2_state_machine/src/state_machine.rs index 63ce6a96a3..9deae81120 100644 --- a/mm2src/mm2_state_machine/src/state_machine.rs +++ b/mm2src/mm2_state_machine/src/state_machine.rs @@ -63,7 +63,7 @@ pub trait State: Send + Sync + 'static { /// ```rust /// return Self::change_state(next_state); /// ``` - async fn on_changed(self: Box, ctx: &mut Self::StateMachine) -> StateResult; + async fn on_changed(self: Box, state_machine: &mut Self::StateMachine) -> StateResult; } /// A trait for transitioning between states in the state machine.