mirror of
https://github.com/AthanorLabs/atomic-swap.git
synced 2026-01-08 21:58:07 -05:00
309 lines
8.6 KiB
Go
309 lines
8.6 KiB
Go
// Copyright 2023 Athanor Labs (ON)
|
|
// SPDX-License-Identifier: LGPL-3.0-only
|
|
|
|
package xmrmaker
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
"time"
|
|
|
|
"github.com/ethereum/go-ethereum"
|
|
"github.com/ethereum/go-ethereum/accounts/abi/bind"
|
|
ethcommon "github.com/ethereum/go-ethereum/common"
|
|
ethtypes "github.com/ethereum/go-ethereum/core/types"
|
|
"github.com/ethereum/go-ethereum/ethclient"
|
|
|
|
"github.com/athanorlabs/atomic-swap/coins"
|
|
"github.com/athanorlabs/atomic-swap/common"
|
|
"github.com/athanorlabs/atomic-swap/common/types"
|
|
"github.com/athanorlabs/atomic-swap/ethereum/block"
|
|
"github.com/athanorlabs/atomic-swap/net/message"
|
|
"github.com/athanorlabs/atomic-swap/relayer"
|
|
)
|
|
|
|
// claimFunds redeems XMRMaker's ETH funds by calling Claim() on the contract
|
|
func (s *swapState) claimFunds() (*ethtypes.Receipt, error) {
|
|
var (
|
|
symbol string
|
|
decimals uint8
|
|
err error
|
|
)
|
|
if types.EthAsset(s.contractSwap.Asset) != types.EthAssetETH {
|
|
_, symbol, decimals, err = s.ETHClient().ERC20Info(s.ctx, s.contractSwap.Asset)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get ERC20 info: %w", err)
|
|
}
|
|
}
|
|
|
|
weiBalance, err := s.ETHClient().Balance(s.ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if types.EthAsset(s.contractSwap.Asset) == types.EthAssetETH {
|
|
log.Infof("balance before claim: %s ETH", coins.NewWeiAmount(weiBalance).AsEther())
|
|
} else {
|
|
balance, err := s.ETHClient().ERC20Balance(s.ctx, s.contractSwap.Asset) //nolint:govet
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
log.Infof("balance before claim: %v %s",
|
|
coins.NewERC20TokenAmountFromBigInt(balance, decimals).AsStandard().Text('f'),
|
|
symbol,
|
|
)
|
|
}
|
|
|
|
var receipt *ethtypes.Receipt
|
|
|
|
// call swap.Swap.Claim() w/ b.privkeys.sk, revealing XMRMaker's secret spend key
|
|
if s.offerExtra.UseRelayer || weiBalance.Cmp(big.NewInt(0)) == 0 {
|
|
// relayer fee was set or we had insufficient funds to claim without a relayer
|
|
// TODO: Sufficient funds check above should be more specific
|
|
receipt, err = s.claimWithRelay()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to claim using relayers: %w", err)
|
|
}
|
|
log.Infof("claim transaction was relayed: %s", common.ReceiptInfo(receipt))
|
|
} else {
|
|
// claim and wait for tx to be included
|
|
sc := s.getSecret()
|
|
receipt, err = s.sender.Claim(s.contractSwap, sc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
log.Infof("claim transaction %s", common.ReceiptInfo(receipt))
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if types.EthAsset(s.contractSwap.Asset) == types.EthAssetETH {
|
|
balance, err := s.ETHClient().Balance(s.ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
log.Infof("balance after claim: %s ETH", coins.FmtWeiAsETH(balance))
|
|
} else {
|
|
balance, err := s.ETHClient().ERC20Balance(s.ctx, s.contractSwap.Asset)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
log.Infof("balance after claim: %s %s",
|
|
coins.NewERC20TokenAmountFromBigInt(balance, decimals).AsStandard().Text('f'),
|
|
symbol,
|
|
)
|
|
}
|
|
|
|
return receipt, nil
|
|
}
|
|
|
|
// relayClaimWithXMRTaker relays the claim to the swap's XMR taker, who should
|
|
// process the claim even if they are not relaying claims for everyone.
|
|
func (s *swapState) relayClaimWithXMRTaker(request *message.RelayClaimRequest) (*ethtypes.Receipt, error) {
|
|
// only requests to the XMR taker set the offerID field
|
|
request.OfferID = &s.offer.ID
|
|
defer func() { request.OfferID = nil }()
|
|
|
|
response, err := s.Backend.SubmitClaimToRelayer(s.info.PeerID, request)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
receipt, err := waitForClaimReceipt(
|
|
s.ctx,
|
|
s.ETHClient().Raw(),
|
|
response.TxHash,
|
|
s.contractAddr,
|
|
s.contractSwapID,
|
|
s.getSecret(),
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get receipt of relayer's tx: %s", err)
|
|
}
|
|
|
|
log.Infof("relayer's claim via counterparty included and validated %s", common.ReceiptInfo(receipt))
|
|
|
|
return receipt, nil
|
|
}
|
|
|
|
// claimWithAdvertisedRelayers relays the claim to nodes that advertise
|
|
// themselves as relayers in the DHT until the claim succeeds, all relayers have
|
|
// been tried, or the context is cancelled.
|
|
func (s *swapState) claimWithAdvertisedRelayers(request *message.RelayClaimRequest) (*ethtypes.Receipt, error) {
|
|
relayers, err := s.Backend.DiscoverRelayers()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(relayers) == 0 {
|
|
return nil, errors.New("no relayers found to submit claim to")
|
|
}
|
|
log.Debugf("Found %d relayers to submit claim to", len(relayers))
|
|
for _, relayerPeerID := range relayers {
|
|
if relayerPeerID == s.info.PeerID {
|
|
log.Debugf("skipping DHT-advertised relayer that is our swap counterparty")
|
|
continue
|
|
}
|
|
log.Debugf("submitting claim to relayer with peer ID %s", relayerPeerID)
|
|
resp, err := s.Backend.SubmitClaimToRelayer(relayerPeerID, request)
|
|
if err != nil {
|
|
log.Warnf("failed to submit tx to relayer: %s", err)
|
|
continue
|
|
}
|
|
|
|
receipt, err := waitForClaimReceipt(
|
|
s.ctx,
|
|
s.ETHClient().Raw(),
|
|
resp.TxHash,
|
|
s.contractAddr,
|
|
s.contractSwapID,
|
|
s.getSecret(),
|
|
)
|
|
if err != nil {
|
|
log.Warnf("failed to get receipt of relayer's tx: %s", err)
|
|
continue
|
|
}
|
|
|
|
log.Infof("DHT relayer's claim included and validated %s", common.ReceiptInfo(receipt))
|
|
|
|
return receipt, nil
|
|
}
|
|
|
|
return nil, errors.New("failed to relay claim with any non-counterparty relayer")
|
|
}
|
|
|
|
// claimWithRelay first tries to relay sequentially with all relayers
|
|
// advertising in the DHT that are not the XMR taker and, if that fails, falls
|
|
// back to the XMR taker who, if using our software, will act as a relayer of
|
|
// last resort for their own swap, even if they are not performing relay
|
|
// operations more generally. Note that the receipt returned is for a
|
|
// transaction created by the remote relayer, not by us.
|
|
func (s *swapState) claimWithRelay() (*ethtypes.Receipt, error) {
|
|
forwarderAddress, err := s.Contract().TrustedForwarder(&bind.CallOpts{Context: s.ctx})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
secret := s.getSecret()
|
|
|
|
request, err := relayer.CreateRelayClaimRequest(
|
|
s.ctx,
|
|
s.ETHClient().PrivateKey(),
|
|
s.ETHClient().Raw(),
|
|
s.contractAddr,
|
|
forwarderAddress,
|
|
s.contractSwap,
|
|
&secret,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
receipt, err := s.claimWithAdvertisedRelayers(request)
|
|
if err != nil {
|
|
log.Warnf("failed to relay with DHT-advertised relayers: %s", err)
|
|
log.Infof("falling back to swap counterparty as relayer")
|
|
return s.relayClaimWithXMRTaker(request)
|
|
}
|
|
return receipt, nil
|
|
}
|
|
|
|
func waitForClaimReceipt(
|
|
ctx context.Context,
|
|
ec *ethclient.Client,
|
|
txHash ethcommon.Hash,
|
|
contractAddr ethcommon.Address,
|
|
contractSwapID [32]byte,
|
|
secret [32]byte,
|
|
) (*ethtypes.Receipt, error) {
|
|
const (
|
|
checkInterval = time.Second // time between transaction polls
|
|
maxWait = time.Minute // max wait for the tx to be included in a block
|
|
maxNotFound = 10 // max failures where the tx is not even found in the mempool
|
|
)
|
|
|
|
start := time.Now()
|
|
|
|
var notFoundCount int
|
|
// wait for inclusion
|
|
for {
|
|
// sleep before the first check, b/c we want to give the tx some time to propagate
|
|
// into the node we're using
|
|
err := common.SleepWithContext(ctx, checkInterval)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
_, isPending, err := ec.TransactionByHash(ctx, txHash)
|
|
if err != nil {
|
|
// allow up to 5 NotFound errors, in case there's some network problems
|
|
if errors.Is(err, ethereum.NotFound) && notFoundCount >= maxNotFound {
|
|
notFoundCount++
|
|
continue
|
|
}
|
|
|
|
return nil, err
|
|
}
|
|
|
|
if time.Since(start) > maxWait {
|
|
// the tx is taking too long, return an error so we try with another relayer
|
|
return nil, errRelayedTransactionTimeout
|
|
}
|
|
|
|
if !isPending {
|
|
break
|
|
}
|
|
}
|
|
|
|
receipt, err := ec.TransactionReceipt(ctx, txHash)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if receipt.Status != ethtypes.ReceiptStatusSuccessful {
|
|
err = fmt.Errorf("relayer's claim transaction failed (gas-lost=%d tx=%s block=%d), %w",
|
|
receipt.GasUsed, txHash, receipt.BlockNumber, block.ErrorFromBlock(ctx, ec, receipt))
|
|
return nil, err
|
|
}
|
|
|
|
if len(receipt.Logs) == 0 {
|
|
return nil, fmt.Errorf("relayer's claim transaction had no logs (tx=%s block=%d)",
|
|
txHash, receipt.BlockNumber)
|
|
}
|
|
|
|
if err = checkClaimedLog(receipt.Logs[0], contractAddr, contractSwapID, secret); err != nil {
|
|
return nil, fmt.Errorf("relayer's claim had logs error (tx=%s block=%d): %w",
|
|
txHash, receipt.BlockNumber, err)
|
|
}
|
|
|
|
return receipt, nil
|
|
}
|
|
|
|
func checkClaimedLog(log *ethtypes.Log, contractAddr ethcommon.Address, contractSwapID, secret [32]byte) error {
|
|
if log.Address != contractAddr {
|
|
return errClaimedLogInvalidContractAddr
|
|
}
|
|
|
|
if len(log.Topics) != 3 {
|
|
return errClaimedLogWrongTopicLength
|
|
}
|
|
|
|
if log.Topics[0] != claimedTopic {
|
|
return errClaimedLogWrongEvent
|
|
}
|
|
|
|
if log.Topics[1] != contractSwapID {
|
|
return errClaimedLogWrongSwapID
|
|
}
|
|
|
|
if log.Topics[2] != secret {
|
|
return errClaimedLogWrongSecret
|
|
}
|
|
|
|
return nil
|
|
}
|