Files
atomic-swap/protocol/xmrmaker/net.go
2023-05-30 21:49:22 -05:00

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
}