Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(tpu-v2): fix tpu-v2 wait for payment spend and extract secret #2261

Open
wants to merge 29 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
9d7f476
add wait_for_maker_payment_spend to MakerPaymentSpent, add wait_for_t…
laruh Oct 16, 2024
de3a970
support Eth in lp bob/alice and use start_taker/maker_swap_state_machine
laruh Oct 19, 2024
ea5475b
use wait_for_confirmations in MakerPaymentSpent and in TakerPaymentSpent
laruh Oct 29, 2024
934a2f0
rename wait_for_taker_payment_spend to find_taker_payment_spend_tx
laruh Oct 30, 2024
ca0ee56
EthCoin find_taker_payment_spend_tx_impl function now tries to find m…
laruh Oct 30, 2024
9449cca
provide extract_secret_v2 in TakerSwapOpsV2, implement EthCoin extrac…
laruh Oct 31, 2024
2e65abf
reuse utxo extract_secret_v2 in legacy extract_secret
laruh Nov 4, 2024
43a1baf
eth extract_secret_v2 tests
laruh Nov 4, 2024
c700b59
cargo fmt
laruh Nov 8, 2024
8d5ed46
review: remove unnecessary param from extract_secret_v2_impl, add eve…
laruh Nov 12, 2024
707946a
review: simplify "swap_version" field type, make it non-optional
laruh Nov 14, 2024
1e7a197
review: simplify `if let Some(tx_hash) = event.transaction_hash` block
laruh Nov 14, 2024
fb383fb
review: make spendTakerPayment transaction log message more informative
laruh Nov 14, 2024
9ec0bab
review: provide LEGACY_SWAP_V const in tests
laruh Nov 14, 2024
6c8b2c1
review: skip retrieving events if tx_hash is already found in find_ta…
laruh Nov 14, 2024
6dcb1c3
Merge remote-tracking branch 'origin/dev' into fix-tpu-v2-wait-for-pa…
laruh Nov 14, 2024
f817f5c
review: combine legacy fallback conditions
laruh Nov 14, 2024
ddac3c8
remove unnecessary quotation marks
laruh Nov 14, 2024
f437dde
review: remove as_ref, map, remove Timer::sleep from the end
laruh Nov 14, 2024
02736e4
return Timer sleep at the end of loop
laruh Nov 15, 2024
a08b500
leave todo above coin tuple pattern matching in ordermatch
laruh Nov 15, 2024
24cb4c3
use `events_from_block` function in `wait_for_htlc_tx_spend`
laruh Nov 15, 2024
5f666f3
review: use logs_block_range in find_taker_payment_spend_tx
laruh Nov 17, 2024
47f904c
use logs_block_range in legacy wait_for_htlc_tx_spend function
laruh Nov 18, 2024
2056f9b
remove time sleep from the end of loop
laruh Nov 18, 2024
0b3ab9a
remove legacy_spend_events function and use new one instead
laruh Nov 18, 2024
cf76cab
Merge remote-tracking branch 'origin/dev' into fix-tpu-v2-wait-for-pa…
laruh Nov 26, 2024
c1a5063
impl detect_secret_hash_algo_v2 function
laruh Nov 27, 2024
43c2d94
Add new addr_to_string function to ParseCoinAssocTypes trait and use …
laruh Nov 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions mm2src/coins/eth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7375,14 +7375,19 @@ impl TakerCoinSwapOpsV2 for EthCoin {
self.sign_and_broadcast_taker_payment_spend_impl(gen_args, secret).await
}

/// Wrapper for [EthCoin::wait_for_taker_payment_spend_impl]
async fn wait_for_taker_payment_spend(
/// Wrapper for [EthCoin::find_taker_payment_spend_tx_impl]
async fn find_taker_payment_spend_tx(
&self,
taker_payment: &Self::Tx,
_from_block: u64,
from_block: u64,
wait_until: u64,
) -> MmResult<Self::Tx, WaitForPaymentSpendError> {
self.wait_for_taker_payment_spend_impl(taker_payment, wait_until).await
) -> MmResult<Self::Tx, FindPaymentSpendError> {
self.find_taker_payment_spend_tx_impl(taker_payment, from_block, wait_until, 10.)
.await
}

async fn extract_secret_v2(&self, _secret_hash: &[u8], spend_tx: &Self::Tx) -> Result<Vec<u8>, String> {
self.extract_secret_v2_impl(spend_tx).await
}
}

Expand Down
178 changes: 149 additions & 29 deletions mm2src/coins/eth/eth_swap_v2/eth_taker_swap_v2.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
use super::{check_decoded_length, validate_amount, validate_from_to_and_status, validate_payment_state,
EthPaymentType, PaymentMethod, PrepareTxDataError, ZERO_VALUE};
use crate::eth::{decode_contract_call, get_function_input_data, wei_from_big_decimal, EthCoin, EthCoinType,
ParseCoinAssocTypes, RefundFundingSecretArgs, RefundTakerPaymentArgs, SendTakerFundingArgs,
SignedEthTx, SwapTxTypeWithSecretHash, TakerPaymentStateV2, TransactionErr, ValidateSwapV2TxError,
ValidateSwapV2TxResult, ValidateTakerFundingArgs, TAKER_SWAP_V2};
use crate::{FundingTxSpend, GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, SearchForFundingSpendErr,
WaitForPaymentSpendError};
use crate::eth::{decode_contract_call, get_function_input_data, signed_tx_from_web3_tx, wei_from_big_decimal, EthCoin,
EthCoinType, ParseCoinAssocTypes, RefundFundingSecretArgs, RefundTakerPaymentArgs,
SendTakerFundingArgs, SignedEthTx, SwapTxTypeWithSecretHash, TakerPaymentStateV2, TransactionErr,
ValidateSwapV2TxError, ValidateSwapV2TxResult, ValidateTakerFundingArgs, TAKER_SWAP_V2};
use crate::{FindPaymentSpendError, FundingTxSpend, GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, MarketCoinOps,
SearchForFundingSpendErr};
use common::executor::Timer;
use common::log::{error, info};
use common::now_sec;
use ethabi::{Function, Token};
use ethcore_transaction::Action;
Expand All @@ -15,7 +16,7 @@ use ethkey::public_to_address;
use futures::compat::Future01CompatExt;
use mm2_err_handle::prelude::{MapToMmResult, MmError, MmResult};
use std::convert::TryInto;
use web3::types::TransactionId;
use web3::types::{BlockNumber, FilterBuilder, Log, TransactionId};

const ETH_TAKER_PAYMENT: &str = "ethTakerPayment";
const ERC20_TAKER_PAYMENT: &str = "erc20TakerPayment";
Expand Down Expand Up @@ -457,37 +458,120 @@ impl EthCoin {
Ok(spend_payment_tx)
}

/// Checks that taker payment state is `MakerSpent`.
/// Accepts maker spent payment transaction and returns it if payment status is correct.
pub(crate) async fn wait_for_taker_payment_spend_impl(
pub(crate) async fn find_taker_payment_spend_tx_impl(
&self,
taker_payment: &SignedEthTx,
taker_payment: &SignedEthTx, // it's approve_tx in Eth case, as in sign_and_send_taker_funding_spend we return approve_tx tx for it
from_block: u64,
wait_until: u64,
) -> MmResult<SignedEthTx, WaitForPaymentSpendError> {
let (decoded, taker_swap_v2_contract) = self
.get_decoded_and_swap_contract(taker_payment, "spendTakerPayment")
.await?;
check_every: f64,
) -> MmResult<SignedEthTx, FindPaymentSpendError> {
let taker_swap_v2_contract = self
.swap_v2_contracts
.as_ref()
.map(|contracts| contracts.taker_swap_v2_contract)
.ok_or_else(|| {
FindPaymentSpendError::Internal("Expected swap_v2_contracts to be Some, but found None".to_string())
})?;
borngraced marked this conversation as resolved.
Show resolved Hide resolved
let approve_func = TAKER_SWAP_V2.function(TAKER_PAYMENT_APPROVE)?;
let decoded = decode_contract_call(approve_func, taker_payment.unsigned().data())?;
let id = match decoded.first() {
Some(Token::FixedBytes(bytes)) => bytes,
invalid_token => {
return MmError::err(FindPaymentSpendError::InvalidData(format!(
"Expected Token::FixedBytes, got {:?}",
invalid_token
)))
},
};
// loop to find maker's spendTakerPayment transaction
loop {
let taker_status = self
.payment_status_v2(
taker_swap_v2_contract,
decoded[0].clone(), // id from spendTakerPayment
&TAKER_SWAP_V2,
EthPaymentType::TakerPayments,
TAKER_PAYMENT_STATE_INDEX,
)
.await?;
if taker_status == U256::from(TakerPaymentStateV2::MakerSpent as u8) {
return Ok(taker_payment.clone());
}
let now = now_sec();
if now > wait_until {
return MmError::err(WaitForPaymentSpendError::Timeout { wait_until, now });
return MmError::err(FindPaymentSpendError::Timeout { wait_until, now });
}
Timer::sleep(10.).await;

let current_block = match self.current_block().compat().await {
Ok(b) => b,
Err(e) => {
error!("Error getting block number: {}", e);
Timer::sleep(5.).await;
continue;
},
};

// get all logged TakerPaymentSpent events from `from_block` till current block
let events = match self
.events_from_block(taker_swap_v2_contract, "TakerPaymentSpent", from_block, current_block)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't we advance from_block if the event wasn't found?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't we advance from_block if the event wasn't found?

Shouldn't change blocks range, It introduces risks to skip necessary block.

Receiving empty events list does not necessarily indicate that there are no events, network latency can cause delays in the propagation and indexing of event logs even after a transaction is mined.
After a transaction is mined, the logs related to it need to be extracted and made available for querying. This process is not instantaneous.

Also we dont know all the nuances and differences of all blockchains. It is much safer to keep block range starting from swap start block.

Copy link
Member Author

@laruh laruh Nov 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UPD: l suggest to add if events.is_empty check to continue loop without moving forward
UPD2: added is empty check 8d5ed46

dimxy marked this conversation as resolved.
Show resolved Hide resolved
.await
{
Ok(events) => events,
Err(e) => {
error!(
"Error getting TakerPaymentSpent events from {} block: {}",
from_block, e
);
Timer::sleep(5.).await;
continue;
},
};

if events.is_empty() {
info!(
"No events found yet for the block range {} to {}",
from_block, current_block
);
Timer::sleep(5.).await;
continue;
}

// this is how spent event looks like in EtomicSwapTakerV2: event TakerPaymentSpent(bytes32 id, bytes32 secret)
let found_event = events.into_iter().find(|event| &event.data.0[..32] == id.as_slice());
dimxy marked this conversation as resolved.
Show resolved Hide resolved

if let Some(event) = found_event {
if let Some(tx_hash) = event.transaction_hash {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will this ever be None? will this be a recoverable state then? otherwise we can terminate early.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will this ever be None?

are you talking about event.transaction_hash? This type is from a dependency, we should handle it as it is. The transaction_hash could be None if the log is emitted by a transaction in a pending state. Once the transaction is included in a mined block, the value should be Some.

will this be a recoverable state then?

For TPU V2 we aim to have automatic recover process, if find_taker_payment_spend_tx return error then refund process will be started

let taker_payment_spend = match state_machine
.taker_coin
.find_taker_payment_spend_tx(
&self.taker_payment,
self.taker_coin_start_block,
state_machine.taker_payment_locktime(),
)
.await
{
Ok(tx) => tx,
Err(e) => {
let next_state = TakerPaymentRefundRequired {
taker_payment: self.taker_payment,
negotiation_data: self.negotiation_data,
reason: TakerPaymentRefundReason::MakerDidNotSpendInTime(format!("{}", e)),
};
return Self::change_state(next_state, state_machine).await;
},
};

otherwise we can terminate early

Could clarify what do you mean by termination? You want to return error and break loop?
We should try to find transaction in the loop until time is out

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The transaction_hash could be None if the log is emitted by a transaction in a pending state. Once the transaction is included in a mined block, the value should be Some.

aren't we getting events till the current mined block? so this tx shouldn't be pending?

For TPU V2 we aim to have automatic recover process

I just meant we can not do nothing about the fact that tx hash is none.
nothing to do with swap recovery.

how I was thinking (which might be wrong) is that some event types don't have a tx hash which means we supplied a bad event id from the beginning meaning that we can't proceed further. this might not be the case though, gotta read more about this, excuse my eth illiteracy 😂

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aren't we getting events till the current mined block? so this tx shouldn't be pending?

yeah, you are right as we are using from and to block filters for eth_getLogs API, tx should be confirmed

    async fn events_from_block(
        &self,
        swap_contract_address: Address,
        event_name: &str,
        from_block: u64,
        to_block: u64,
    ) -> MmResult<Vec<Log>, FindPaymentSpendError> {
        let contract_event = TAKER_SWAP_V2.event(event_name)?;
        let filter = FilterBuilder::default()
            .topics(Some(vec![contract_event.signature()]), None, None, None)
            .from_block(BlockNumber::Number(from_block.into()))
            .to_block(BlockNumber::Number(to_block.into()))
            .address(vec![swap_contract_address])
            .build();
        let events_logs = self
            .logs(filter)
            .await
            .map_err(|e| FindPaymentSpendError::Transport(e.to_string()))?;
        Ok(events_logs)
    }

some event types don't have a tx hash

I think you are confused a bit. Events/logs themselves don't necessarily contain the transaction hash. Instead, the transaction hash is associated with the transaction that emitted the event. So Log type from web3 just contains info about tx which emitted this log.

Note about event and log words. event in Solidity is a way for a contract to emit logs during the execution of a function.
So they are close words, just events refer to the Solidity construct used in the smart contract to emit logs, while logs refer to the actual data that is recorded in the blockchain when events are emitted.

So using from to block range we are looking for Log which was emitted by spend taker payment transaction.

As for empty tx hash I would like to refer to previous comment #2261 (comment) empty event list or none transaction_hash are not 100% that there is no tx which we are looking for, it could be just blockchain indexation delays or other reasons.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i will read further regarding the truncated (not necessarily empty? right?) event list issue, any resourced regarding the indexaction delay issues and such would be so helpful!

regarding an event not having a transaction_hash thought, how would we successfully get the event which has None for the transaction_hash and then try again and all of a sudden we get a Some transaction_hash! is that possible? are these event logs mutable?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i will read further regarding the truncated (not necessarily empty? right?) event list issue, any resourced regarding the indexaction delay issues and such would be so helpful!

I would say that chain reorganization, network latency, node synchronization lag issues could cause missing information issues. These problems usually temporarily, as I understand. You should wait for more confirmed blocks also try to fetch the info from different nodes.

regarding an event not having a transaction_hash thought, how would we successfully get the event which has None for the transaction_hash and then try again and all of a sudden we get a Some transaction_hash! is that possible? are these event logs mutable?

Logs are not mutable. Also they are tied to transactions. When a transaction calls a smart contract function that emits an event, this event generates log, which is permanently recorded in the blockchain.

But there’s a nuance. Lest check doc. According to the documentation https://www.chainnodes.org/docs/ethereum/eth_getLogs, a log from a pending transaction might lack a transaction hash, but when the transaction is confirmed, the log should include it.

Therefore, ideally, when we request a list of logs using the events_from_block function with from_block and to_block filters, it should return only logs from confirmed blocks, which means confirmed transactions. In this case, event.transaction_hash should ideally always be Some.

if let Some(event) = found_event {
if let Some(tx_hash) = event.transaction_hash {

We dont need to change from_block. However, if Log has transaction_hash:None, it doesn't mean Log doesn't have transaction (actually its not correct by itself, as logs are not owners of txs), it means smth went wrong as eth_getLogs API with fromBlock and toBlock filters will use confirmed blocks.

Copy link
Member Author

@laruh laruh Nov 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some additional info https://docs.alchemy.com/docs/deep-dive-into-eth_getlogs#what-are-logs-or-events

Logs and events are used synonymously—smart contracts generate logs by firing off events, so logs provide insights into events that occur within the smart contract. Logs can be found on transaction receipts.

Anytime a transaction is mined, we can see event logs for that transaction by making a request to eth_getLogs and then take actions based off those results. For example, if a purchase is being made using crypto payments, we can use eth_getLogs to see if the sender successfully made the payment before providing the item purchased.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, if Log has transaction_hash:None, it doesn't mean Log doesn't have transaction, it means smth went wrong as eth_getLogs API with fromBlock and toBlock filters will use confirmed blocks.

im missing u between the lines here so let me repeat that to you and see if i got it correctly. transaction_hash MUST always be Some eventually, if it was None this is a temporary reporting/mining/etc... thing.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, if Log has transaction_hash:None, it doesn't mean Log doesn't have transaction, it means smth went wrong as eth_getLogs API with fromBlock and toBlock filters will use confirmed blocks.

im missing u between the lines here so let me repeat that to you and see if i got it correctly. transaction_hash MUST always be Some eventually, if it was None this is a temporary reporting/mining/etc... thing.

Yes, in the context of not using "pending" tag in "eth_getLogs" API, we expect transaction_hash always be Some.
The best we can do is to repeat loop cycle until time is out, if None occurred.

dimxy marked this conversation as resolved.
Show resolved Hide resolved
let transaction = match self.transaction(TransactionId::Hash(tx_hash)).await {
Ok(Some(t)) => t,
Ok(None) => {
info!("Tx {} not found yet", tx_hash);
dimxy marked this conversation as resolved.
Show resolved Hide resolved
Timer::sleep(check_every).await;
continue;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now that we have the tx_hash, we shouldn't keep looping right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm yes

            if let Some(event) = found_event {
                if let Some(tx_hash) = event.transaction_hash {
                    let transaction = match self.transaction(TransactionId::Hash(tx_hash)).await {
                        Ok(Some(t)) => t,
                        Ok(None) => {
                            info!("Tx {} not found yet", tx_hash);
                            Timer::sleep(check_every).await;
                            continue;
                        },
                        Err(e) => {
                            error!("Get tx {} error: {}", tx_hash, e);
                            Timer::sleep(check_every).await;
                            continue;
                        },
                    };
                    let result = signed_tx_from_web3_tx(transaction).map_err(FindPaymentSpendError::Internal)?;
                    return Ok(result);
                }
            }

            Timer::sleep(5.).await;
        }

once we got Some(tx_hash) it means event we found had necessary swap id.
But if we got None or error in next step after Some(tx_hash), we dont have to repeat the whole process from the beginning.
Next loop cycle we can start right from requesting transaction from tx_hash. I will work on it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done 6c8b2c1

Copy link
Member

@borngraced borngraced Nov 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should abort if we got error as this can lead to a potential infinite loop. self.transaction return error only if the tx data can't be deserialize which means bad tx data so we should break

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should abort if we got error as this can lead to a potential infinite loop.

There is no infinite loop, we have if now > wait_until check

        loop {
            let now = now_sec();
            if now > wait_until {
                return MmError::err(FindPaymentSpendError::Timeout { wait_until, now });
            }

self.transaction return error only if the tx data can't be deserialize which means bad tx data so we should break

no, it has try_rpc_send function, it could return web3::Error
image

Copy link
Member

@borngraced borngraced Nov 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok.. then you should still handle the error

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok.. then you should still handle the error

I think you confused transaction and signed_tx_from_web3_tx functions

// Proceed to check transaction if we have a tx_hash
if let Some(tx_hash) = tx_hash {
match self.transaction(TransactionId::Hash(tx_hash)).await {
Ok(Some(t)) => {
let transaction = signed_tx_from_web3_tx(t).map_err(FindPaymentSpendError::Internal)?;
return Ok(transaction);
},
Ok(None) => info!("spendTakerPayment transaction {} not found yet", tx_hash),
Err(e) => error!("Get spendTakerPayment transaction {} error: {}", tx_hash, e),
};
Timer::sleep(check_every).await;
continue;
}
}

transaction uses web3 calls.
signed_tx_from_web3_tx is one which doesnt have web3 requests and tries to build ethcore transaction from web3 transaction. If it fails then it is useless to repeat it, we can return error instantly and we do it

let transaction = signed_tx_from_web3_tx(t).map_err(FindPaymentSpendError::Internal)?;

},
Err(e) => {
error!("Get tx {} error: {}", tx_hash, e);
Timer::sleep(check_every).await;
continue;
},
};
let result = signed_tx_from_web3_tx(transaction).map_err(FindPaymentSpendError::Internal)?;
return Ok(result);
}
}

Timer::sleep(5.).await;
}
}

async fn events_from_block(
&self,
swap_contract_address: Address,
event_name: &str,
from_block: u64,
to_block: u64,
) -> MmResult<Vec<Log>, FindPaymentSpendError> {
let contract_event = TAKER_SWAP_V2.event(event_name)?;
let filter = FilterBuilder::default()
.topics(Some(vec![contract_event.signature()]), None, None, None)
.from_block(BlockNumber::Number(from_block.into()))
.to_block(BlockNumber::Number(to_block.into()))
.address(vec![swap_contract_address])
.build();
let events_logs = self
.logs(filter)
.await
.map_err(|e| FindPaymentSpendError::Transport(e.to_string()))?;
Ok(events_logs)
}

/// Prepares data for EtomicSwapTakerV2 contract [ethTakerPayment](https://github.com/KomodoPlatform/etomic-swap/blob/5e15641cbf41766cd5b37b4d71842c270773f788/contracts/EtomicSwapTakerV2.sol#L44) method
async fn prepare_taker_eth_funding_data(&self, args: &TakerFundingArgs) -> Result<Vec<u8>, PrepareTxDataError> {
let function = TAKER_SWAP_V2.function(ETH_TAKER_PAYMENT)?;
Expand Down Expand Up @@ -692,6 +776,42 @@ impl EthCoin {

Ok((decoded, taker_swap_v2_contract))
}

/// Extracts the maker's secret from the input of transaction that calls the `spendTakerPayment` smart contract method.
///
/// function spendTakerPayment(
/// bytes32 id,
/// uint256 amount,
/// uint256 dexFee,
/// address taker,
/// bytes32 takerSecretHash,
/// bytes32 makerSecret,
/// address tokenAddress
/// )
pub(crate) async fn extract_secret_v2_impl(&self, spend_tx: &SignedEthTx) -> Result<Vec<u8>, String> {
let function = try_s!(TAKER_SWAP_V2.function("spendTakerPayment"));
// should be 0xcc90c199
let expected_signature = function.short_signature();
let signature = &spend_tx.unsigned().data()[0..4];
if signature != expected_signature {
return ERR!(
"Expected 'spendTakerPayment' contract call signature: {:?}, found {:?}",
expected_signature,
signature
);
};
let decoded = try_s!(decode_contract_call(function, spend_tx.unsigned().data()));
if decoded.len() < 7 {
return ERR!("Invalid arguments in 'spendTakerPayment' call: {:?}", decoded);
}
match &decoded[5] {
Token::FixedBytes(secret) => Ok(secret.to_vec()),
_ => ERR!(
"Expected secret to be fixed bytes, but decoded function data is {:?}",
decoded
),
}
}
}

/// Validation function for ETH taker payment data
Expand Down
20 changes: 11 additions & 9 deletions mm2src/coins/lp_coins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1783,7 +1783,7 @@ pub trait MakerNftSwapOpsV2: ParseCoinAssocTypes + ParseNftAssocTypes + Send + S

/// Enum representing errors that can occur while waiting for taker payment spend.
#[derive(Display, Debug, EnumFromStringify)]
pub enum WaitForPaymentSpendError {
pub enum FindPaymentSpendError {
/// Timeout error variant, indicating that the wait for taker payment spend has timed out.
#[display(
fmt = "Timed out waiting for taker payment spend, wait_until {}, now {}",
Expand All @@ -1806,18 +1806,18 @@ pub enum WaitForPaymentSpendError {
Transport(String),
}

impl From<WaitForOutputSpendErr> for WaitForPaymentSpendError {
impl From<WaitForOutputSpendErr> for FindPaymentSpendError {
fn from(err: WaitForOutputSpendErr) -> Self {
match err {
WaitForOutputSpendErr::Timeout { wait_until, now } => WaitForPaymentSpendError::Timeout { wait_until, now },
WaitForOutputSpendErr::Timeout { wait_until, now } => FindPaymentSpendError::Timeout { wait_until, now },
WaitForOutputSpendErr::NoOutputWithIndex(index) => {
WaitForPaymentSpendError::InvalidInputTx(format!("Tx doesn't have output with index {}", index))
FindPaymentSpendError::InvalidInputTx(format!("Tx doesn't have output with index {}", index))
},
}
}
}

impl From<PaymentStatusErr> for WaitForPaymentSpendError {
impl From<PaymentStatusErr> for FindPaymentSpendError {
fn from(e: PaymentStatusErr) -> Self {
match e {
PaymentStatusErr::ABIError(e) => Self::ABIError(e),
Expand All @@ -1828,7 +1828,7 @@ impl From<PaymentStatusErr> for WaitForPaymentSpendError {
}
}

impl From<PrepareTxDataError> for WaitForPaymentSpendError {
impl From<PrepareTxDataError> for FindPaymentSpendError {
fn from(e: PrepareTxDataError) -> Self {
match e {
PrepareTxDataError::ABIError(e) => Self::ABIError(e),
Expand Down Expand Up @@ -1963,13 +1963,15 @@ pub trait TakerCoinSwapOpsV2: ParseCoinAssocTypes + CommonSwapOpsV2 + Send + Syn
swap_unique_data: &[u8],
) -> Result<Self::Tx, TransactionErr>;

/// Wait until taker payment spend is found on-chain
async fn wait_for_taker_payment_spend(
/// Wait until taker payment spend transaction is found on-chain
async fn find_taker_payment_spend_tx(
&self,
taker_payment: &Self::Tx,
from_block: u64,
wait_until: u64,
) -> MmResult<Self::Tx, WaitForPaymentSpendError>;
) -> MmResult<Self::Tx, FindPaymentSpendError>;

async fn extract_secret_v2(&self, secret_hash: &[u8], spend_tx: &Self::Tx) -> Result<Vec<u8>, String>;
}

#[async_trait]
Expand Down
14 changes: 9 additions & 5 deletions mm2src/coins/test_coin.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#![allow(clippy::all)]

use super::{CoinBalance, CommonSwapOpsV2, FundingTxSpend, HistorySyncState, MarketCoinOps, MmCoin, RawTransactionFut,
RawTransactionRequest, RefundTakerPaymentArgs, SearchForFundingSpendErr, SwapOps, TradeFee,
TransactionEnum, TransactionFut, WaitForPaymentSpendError};
use super::{CoinBalance, CommonSwapOpsV2, FindPaymentSpendError, FundingTxSpend, HistorySyncState, MarketCoinOps,
MmCoin, RawTransactionFut, RawTransactionRequest, RefundTakerPaymentArgs, SearchForFundingSpendErr,
SwapOps, TradeFee, TransactionEnum, TransactionFut};
use crate::coin_errors::ValidatePaymentResult;
use crate::{coin_errors::MyAddressError, BalanceFut, CanRefundHtlc, CheckIfMyPaymentSentArgs, CoinFutSpawner,
ConfirmPaymentInput, FeeApproxStage, FoundSwapTxSpend, GenPreimageResult, GenTakerFundingSpendArgs,
Expand Down Expand Up @@ -557,12 +557,16 @@ impl TakerCoinSwapOpsV2 for TestCoin {
unimplemented!()
}

async fn wait_for_taker_payment_spend(
async fn find_taker_payment_spend_tx(
&self,
taker_payment: &Self::Tx,
from_block: u64,
wait_until: u64,
) -> MmResult<Self::Tx, WaitForPaymentSpendError> {
) -> MmResult<Self::Tx, FindPaymentSpendError> {
unimplemented!()
}

async fn extract_secret_v2(&self, secret_hash: &[u8], spend_tx: &Self::Tx) -> Result<Vec<u8>, String> {
unimplemented!()
}
}
Expand Down
8 changes: 7 additions & 1 deletion mm2src/coins/utxo/utxo_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2584,12 +2584,18 @@ pub async fn get_taker_watcher_reward<T: UtxoCommonOps + SwapOps + MarketCoinOps
/// Note spender could generate the spend with several inputs where the only one input is the p2sh script.
pub fn extract_secret(secret_hash: &[u8], spend_tx: &[u8]) -> Result<Vec<u8>, String> {
let spend_tx: UtxoTx = try_s!(deserialize(spend_tx).map_err(|e| ERRL!("{:?}", e)));
extract_secret_v2(secret_hash, &spend_tx)
}

/// Extract a secret from the `spend_tx`.
/// Note spender could generate the spend with several inputs where the only one input is the p2sh script.
pub fn extract_secret_v2(secret_hash: &[u8], spend_tx: &UtxoTx) -> Result<Vec<u8>, String> {
let expected_secret_hash = if secret_hash.len() == 32 {
ripemd160(secret_hash)
} else {
H160::from(secret_hash)
};
for input in spend_tx.inputs.into_iter() {
for input in spend_tx.inputs.iter() {
let script: Script = input.script_sig.clone().into();
for instruction in script.iter().flatten() {
if instruction.opcode == Opcode::OP_PUSHBYTES_32 {
Expand Down
Loading
Loading