mirror of
https://github.com/AthanorLabs/atomic-swap.git
synced 2026-01-07 21:34:05 -05:00
331 lines
7.8 KiB
Go
331 lines
7.8 KiB
Go
// Copyright 2023 The AthanorLabs/atomic-swap Authors
|
|
// SPDX-License-Identifier: LGPL-3.0-only
|
|
|
|
package rpc
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/cockroachdb/apd/v3"
|
|
"github.com/libp2p/go-libp2p/core/peer"
|
|
ma "github.com/multiformats/go-multiaddr"
|
|
|
|
"github.com/athanorlabs/atomic-swap/coins"
|
|
"github.com/athanorlabs/atomic-swap/common"
|
|
"github.com/athanorlabs/atomic-swap/common/rpctypes"
|
|
"github.com/athanorlabs/atomic-swap/common/types"
|
|
"github.com/athanorlabs/atomic-swap/net/message"
|
|
"github.com/athanorlabs/atomic-swap/protocol/swap"
|
|
|
|
ethcommon "github.com/ethereum/go-ethereum/common"
|
|
)
|
|
|
|
const defaultSearchTime = time.Second * 12
|
|
|
|
// Net contains the network-related functions required by the rpc service.
|
|
type Net interface {
|
|
PeerID() peer.ID
|
|
ConnectedPeers() []string
|
|
Addresses() []ma.Multiaddr
|
|
Discover(provides string, searchTime time.Duration) ([]peer.ID, error)
|
|
Query(who peer.ID) (*message.QueryResponse, error)
|
|
Initiate(who peer.AddrInfo, sendKeysMessage common.Message, s common.SwapStateNet) error
|
|
CloseProtocolStream(types.Hash)
|
|
}
|
|
|
|
// NetService is the RPC service prefixed by net_.
|
|
type NetService struct {
|
|
ctx context.Context
|
|
net Net
|
|
xmrtaker XMRTaker
|
|
xmrmaker XMRMaker
|
|
pb ProtocolBackend
|
|
sm swap.Manager
|
|
isBootnode bool
|
|
}
|
|
|
|
// NewNetService ...
|
|
func NewNetService(
|
|
ctx context.Context,
|
|
net Net,
|
|
xmrtaker XMRTaker,
|
|
xmrmaker XMRMaker,
|
|
pb ProtocolBackend,
|
|
sm swap.Manager,
|
|
isBootnode bool,
|
|
) *NetService {
|
|
return &NetService{
|
|
ctx: ctx,
|
|
net: net,
|
|
xmrtaker: xmrtaker,
|
|
xmrmaker: xmrmaker,
|
|
pb: pb,
|
|
sm: sm,
|
|
isBootnode: isBootnode,
|
|
}
|
|
}
|
|
|
|
// Addresses returns the local listening multi-addresses. Note that local listening
|
|
// addresses do not correspond to what remote peers connect to unless your host has a
|
|
// public IP directly attached to a local interface.
|
|
func (s *NetService) Addresses(_ *http.Request, _ *interface{}, resp *rpctypes.AddressesResponse) error {
|
|
// Multiaddr is an interface that you can serialize, but you need a concrete
|
|
// type to deserialize, so we just use strings in the AddressesResponse.
|
|
addresses := s.net.Addresses()
|
|
resp.Addrs = make([]string, 0, len(addresses))
|
|
for _, a := range addresses {
|
|
resp.Addrs = append(resp.Addrs, a.String())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Peers returns the peers that this node is currently connected to.
|
|
func (s *NetService) Peers(_ *http.Request, _ *interface{}, resp *rpctypes.PeersResponse) error {
|
|
resp.Addrs = s.net.ConnectedPeers()
|
|
return nil
|
|
}
|
|
|
|
// Pairs returns all currently available pairs from offers of all peers
|
|
func (s *NetService) Pairs(_ *http.Request, req *rpctypes.PairsRequest, resp *rpctypes.PairsResponse) error {
|
|
if s.isBootnode {
|
|
return errUnsupportedForBootnode
|
|
}
|
|
|
|
peerIDs, err := s.discover(&rpctypes.DiscoverRequest{
|
|
Provides: "",
|
|
SearchTime: req.SearchTime,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
pairs := make(map[ethcommon.Address]*types.Pair)
|
|
|
|
for _, p := range peerIDs {
|
|
msg, err := s.net.Query(p)
|
|
if err != nil {
|
|
log.Debugf("Failed to query peer ID %s", p)
|
|
continue
|
|
}
|
|
|
|
if len(msg.Offers) == 0 {
|
|
continue
|
|
}
|
|
|
|
for _, o := range msg.Offers {
|
|
address := o.EthAsset.Address()
|
|
pair, exists := pairs[address]
|
|
|
|
if !exists {
|
|
pair = types.NewPair(o.EthAsset)
|
|
if pair.EthAsset.IsToken() {
|
|
tokenInfo, tokenInfoErr := s.pb.ETHClient().ERC20Info(s.ctx, address)
|
|
if tokenInfoErr != nil {
|
|
log.Debugf("Error while reading token info: %s", tokenInfoErr)
|
|
continue
|
|
}
|
|
pair.Token = *tokenInfo
|
|
} else {
|
|
pair.Token.Name = "Ether"
|
|
pair.Token.Symbol = "ETH"
|
|
pair.Token.NumDecimals = 18
|
|
pair.Verified = true
|
|
}
|
|
pairs[address] = pair
|
|
}
|
|
|
|
err = pair.AddOffer(o)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
pairsArray := make([]*types.Pair, 0, len(pairs))
|
|
for _, pair := range pairs {
|
|
if pair.EthAsset.IsETH() {
|
|
pairsArray = append([]*types.Pair{pair}, pairsArray...)
|
|
} else {
|
|
pairsArray = append(pairsArray, pair)
|
|
}
|
|
}
|
|
|
|
resp.Pairs = pairsArray
|
|
return nil
|
|
}
|
|
|
|
// QueryAll discovers peers who provide a certain coin and queries all of them for their current offers.
|
|
func (s *NetService) QueryAll(_ *http.Request, req *rpctypes.QueryAllRequest, resp *rpctypes.QueryAllResponse) error {
|
|
if s.isBootnode {
|
|
return errUnsupportedForBootnode
|
|
}
|
|
|
|
peerIDs, err := s.discover(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
resp.PeersWithOffers = make([]*rpctypes.PeerWithOffers, 0, len(peerIDs))
|
|
for _, p := range peerIDs {
|
|
msg, err := s.net.Query(p)
|
|
if err != nil {
|
|
log.Debugf("Failed to query peer ID %s", p)
|
|
continue
|
|
}
|
|
if len(msg.Offers) > 0 {
|
|
resp.PeersWithOffers = append(resp.PeersWithOffers, &rpctypes.PeerWithOffers{
|
|
PeerID: p,
|
|
Offers: msg.Offers,
|
|
})
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *NetService) discover(req *rpctypes.DiscoverRequest) ([]peer.ID, error) {
|
|
searchTime, err := time.ParseDuration(fmt.Sprintf("%ds", req.SearchTime))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if searchTime == 0 {
|
|
searchTime = defaultSearchTime
|
|
}
|
|
|
|
return s.net.Discover(req.Provides, searchTime)
|
|
}
|
|
|
|
// Discover discovers peers over the network that provide a certain coin up for `SearchTime` duration of time.
|
|
func (s *NetService) Discover(_ *http.Request, req *rpctypes.DiscoverRequest, resp *rpctypes.DiscoverResponse) error {
|
|
searchTime, err := time.ParseDuration(fmt.Sprintf("%ds", req.SearchTime))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if searchTime == 0 {
|
|
searchTime = defaultSearchTime
|
|
}
|
|
|
|
resp.PeerIDs, err = s.net.Discover(req.Provides, searchTime)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// QueryPeer queries a peer for the coins they provide, their maximum amounts, and desired exchange rate.
|
|
func (s *NetService) QueryPeer(
|
|
_ *http.Request,
|
|
req *rpctypes.QueryPeerRequest,
|
|
resp *rpctypes.QueryPeerResponse,
|
|
) error {
|
|
if s.isBootnode {
|
|
return errUnsupportedForBootnode
|
|
}
|
|
|
|
msg, err := s.net.Query(req.PeerID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
resp.Offers = msg.Offers
|
|
return nil
|
|
}
|
|
|
|
// TakeOffer initiates a swap with the given peer by taking an offer they've made.
|
|
func (s *NetService) TakeOffer(
|
|
_ *http.Request,
|
|
req *rpctypes.TakeOfferRequest,
|
|
_ *interface{},
|
|
) error {
|
|
if s.isBootnode {
|
|
return errUnsupportedForBootnode
|
|
}
|
|
|
|
err := s.takeOffer(req.PeerID, req.OfferID, req.ProvidesAmount)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *NetService) takeOffer(makerPeerID peer.ID, offerID types.Hash, providesAmount *apd.Decimal) error {
|
|
queryResp, err := s.net.Query(makerPeerID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var offer *types.Offer
|
|
for _, maybeOffer := range queryResp.Offers {
|
|
if offerID == maybeOffer.ID {
|
|
offer = maybeOffer
|
|
break
|
|
}
|
|
}
|
|
if offer == nil {
|
|
return errNoOfferWithID
|
|
}
|
|
|
|
swapState, err := s.xmrtaker.InitiateProtocol(makerPeerID, providesAmount, offer)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
skm := swapState.SendKeysMessage().(*message.SendKeysMessage)
|
|
skm.OfferID = offerID
|
|
skm.ProvidedAmount = providesAmount
|
|
|
|
if err = s.net.Initiate(peer.AddrInfo{ID: makerPeerID}, skm, swapState); err != nil {
|
|
if err = swapState.Exit(); err != nil {
|
|
log.Warnf("Swap exit failure: %s", err)
|
|
}
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// MakeOffer creates and advertises a new swap offer.
|
|
func (s *NetService) MakeOffer(
|
|
_ *http.Request,
|
|
req *rpctypes.MakeOfferRequest,
|
|
resp *rpctypes.MakeOfferResponse,
|
|
) error {
|
|
if s.isBootnode {
|
|
return errUnsupportedForBootnode
|
|
}
|
|
|
|
offerResp, err := s.makeOffer(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
*resp = *offerResp
|
|
return nil
|
|
}
|
|
|
|
func (s *NetService) makeOffer(req *rpctypes.MakeOfferRequest) (*rpctypes.MakeOfferResponse, error) {
|
|
offer := types.NewOffer(
|
|
coins.ProvidesXMR,
|
|
req.MinAmount,
|
|
req.MaxAmount,
|
|
req.ExchangeRate,
|
|
req.EthAsset,
|
|
)
|
|
|
|
_, err := s.xmrmaker.MakeOffer(offer, req.UseRelayer)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &rpctypes.MakeOfferResponse{
|
|
PeerID: s.net.PeerID(),
|
|
OfferID: offer.ID,
|
|
}, nil
|
|
}
|