mirror of
https://github.com/AthanorLabs/atomic-swap.git
synced 2026-01-08 21:58:07 -05:00
175 lines
4.6 KiB
Go
175 lines
4.6 KiB
Go
// Copyright 2023 The AthanorLabs/atomic-swap Authors
|
|
// SPDX-License-Identifier: LGPL-3.0-only
|
|
|
|
package xmrmaker
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/libp2p/go-libp2p/core/peer"
|
|
|
|
"github.com/athanorlabs/atomic-swap/coins"
|
|
"github.com/athanorlabs/atomic-swap/common/types"
|
|
"github.com/athanorlabs/atomic-swap/net"
|
|
"github.com/athanorlabs/atomic-swap/net/message"
|
|
pcommon "github.com/athanorlabs/atomic-swap/protocol"
|
|
|
|
"github.com/fatih/color"
|
|
)
|
|
|
|
// Provides returns types.ProvidesXMR
|
|
func (inst *Instance) Provides() coins.ProvidesCoin {
|
|
return coins.ProvidesXMR
|
|
}
|
|
|
|
func (inst *Instance) initiate(
|
|
takerPeerID peer.ID,
|
|
offer *types.Offer,
|
|
offerExtra *types.OfferExtra,
|
|
providesAmount *coins.PiconeroAmount,
|
|
desiredAmount coins.EthAssetAmount,
|
|
) (*swapState, error) {
|
|
if inst.swapStates[offer.ID] != nil {
|
|
return nil, errProtocolAlreadyInProgress
|
|
}
|
|
|
|
balance, err := inst.backend.XMRClient().GetBalance(0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// check that the user's monero balance is sufficient for their max swap amount (strictly
|
|
// greater check, since they need to cover chain fees).
|
|
unlockedBal := coins.NewPiconeroAmount(balance.UnlockedBalance)
|
|
if unlockedBal.Decimal().Cmp(providesAmount.Decimal()) <= 0 {
|
|
return nil, errBalanceTooLow{
|
|
unlockedBalance: unlockedBal.AsMonero(),
|
|
providedAmount: providesAmount.AsMonero(),
|
|
}
|
|
}
|
|
|
|
// checks passed, delete the offer from memory for now
|
|
_, _, err = inst.offerManager.TakeOffer(offer.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
s, err := newSwapStateFromStart(
|
|
inst.backend,
|
|
takerPeerID,
|
|
offer,
|
|
offerExtra,
|
|
inst.offerManager,
|
|
providesAmount,
|
|
desiredAmount,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
go func() {
|
|
<-s.done
|
|
inst.swapMu.Lock()
|
|
defer inst.swapMu.Unlock()
|
|
delete(inst.swapStates, offer.ID)
|
|
}()
|
|
|
|
symbol, err := pcommon.AssetSymbol(inst.backend, offer.EthAsset)
|
|
if err != nil {
|
|
_ = s.Exit()
|
|
return nil, err
|
|
}
|
|
|
|
log.Info(color.New(color.Bold).Sprintf("**initiated swap with offer ID=%s**", s.info.OfferID))
|
|
log.Info(color.New(color.Bold).Sprint("DO NOT EXIT THIS PROCESS OR THE SWAP MAY BE CANCELLED!"))
|
|
log.Infof(color.New(color.Bold).Sprintf("receiving %v %s for %v XMR",
|
|
s.info.ExpectedAmount,
|
|
symbol,
|
|
s.info.ProvidedAmount),
|
|
)
|
|
inst.swapStates[offer.ID] = s
|
|
return s, nil
|
|
}
|
|
|
|
// HandleInitiateMessage is called when we receive a network message from a peer that they wish to initiate a swap.
|
|
func (inst *Instance) HandleInitiateMessage(
|
|
takerPeerID peer.ID,
|
|
msg *message.SendKeysMessage,
|
|
) (net.SwapState, error) {
|
|
inst.swapMu.Lock()
|
|
defer inst.swapMu.Unlock()
|
|
|
|
str := color.New(color.Bold).Sprintf("**incoming take of offer %s with provided amount %s**",
|
|
msg.OfferID,
|
|
msg.ProvidedAmount,
|
|
)
|
|
log.Info(str)
|
|
|
|
// get offer and determine expected amount
|
|
if types.IsHashZero(msg.OfferID) {
|
|
return nil, errOfferIDNotSet
|
|
}
|
|
|
|
offer, offerExtra, err := inst.offerManager.GetOffer(msg.OfferID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
maxDecimals := uint8(coins.NumEtherDecimals)
|
|
var token *coins.ERC20TokenInfo
|
|
if offer.EthAsset.IsToken() {
|
|
token, err = inst.backend.ETHClient().ERC20Info(inst.backend.Ctx(), offer.EthAsset.Address())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
maxDecimals = token.NumDecimals
|
|
}
|
|
|
|
err = coins.ValidatePositive("providedAmount", maxDecimals, msg.ProvidedAmount)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
expectedAmount := coins.NewEthAssetAmount(msg.ProvidedAmount, token)
|
|
|
|
// The calculation below will return an error if the provided amount, when
|
|
// represented in XMR, would require fractional piconeros. This can happen
|
|
// more easily than one might expect, as ToXMR is doing a division by the
|
|
// exchange rate. The taker also verifies that their provided amount will
|
|
// not result in fractional piconeros, so the issue will normally be caught
|
|
// before the taker ever contacts us.
|
|
providedAmtAsXMR, err := offer.ExchangeRate.ToXMR(expectedAmount)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if providedAmtAsXMR.Cmp(offer.MinAmount) < 0 {
|
|
return nil, errAmountProvidedTooLow{msg.ProvidedAmount, offer.MinAmount}
|
|
}
|
|
|
|
if providedAmtAsXMR.Cmp(offer.MaxAmount) > 0 {
|
|
return nil, errAmountProvidedTooHigh{msg.ProvidedAmount, offer.MaxAmount}
|
|
}
|
|
|
|
providedPiconero := coins.MoneroToPiconero(providedAmtAsXMR)
|
|
|
|
state, err := inst.initiate(takerPeerID, offer, offerExtra, providedPiconero, expectedAmount)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err = state.handleSendKeysMessage(msg); err != nil {
|
|
_ = state.Exit()
|
|
return nil, err
|
|
}
|
|
|
|
resp := state.SendKeysMessage()
|
|
err = inst.backend.SendSwapMessage(resp, offer.ID)
|
|
if err != nil {
|
|
_ = state.Exit()
|
|
return nil, fmt.Errorf("failed to send SendKeysMessage to remote peer: %w", err)
|
|
}
|
|
|
|
return state, nil
|
|
}
|