mirror of
https://github.com/AthanorLabs/atomic-swap.git
synced 2026-01-07 21:34:05 -05:00
RPC and WS endpoints share the same port (#187)
Combine our HTTP RPC and Websocket services into a single HTTP server, eliminating the --ws-port flag to swapd and using --swapd-port flag for swapcli.
This commit is contained in:
@@ -1,28 +1,32 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
ethcommon "github.com/ethereum/go-ethereum/common"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/athanorlabs/atomic-swap/common"
|
||||
"github.com/athanorlabs/atomic-swap/common/types"
|
||||
"github.com/athanorlabs/atomic-swap/rpcclient"
|
||||
"github.com/athanorlabs/atomic-swap/rpcclient/wsclient"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultSwapdAddress = "http://127.0.0.1:5001"
|
||||
defaultSwapdPort = 5001
|
||||
defaultDiscoverSearchTimeSecs = 12
|
||||
|
||||
flagSwapdPort = "swapd-port"
|
||||
)
|
||||
|
||||
var (
|
||||
app = &cli.App{
|
||||
Name: "swapcli",
|
||||
Usage: "Client for swapd",
|
||||
Name: "swapcli",
|
||||
Usage: "Client for swapd",
|
||||
EnableBashCompletion: true,
|
||||
Suggest: true,
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "addresses",
|
||||
@@ -30,7 +34,7 @@ var (
|
||||
Usage: "List our daemon's libp2p listening addresses",
|
||||
Action: runAddresses,
|
||||
Flags: []cli.Flag{
|
||||
daemonAddrFlag,
|
||||
swapdPortFlag,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -50,7 +54,7 @@ var (
|
||||
Usage: "Duration of time to search for, in seconds",
|
||||
Value: defaultDiscoverSearchTimeSecs,
|
||||
},
|
||||
daemonAddrFlag,
|
||||
swapdPortFlag,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -64,7 +68,7 @@ var (
|
||||
Usage: "Peer's multiaddress, as provided by discover",
|
||||
Required: true,
|
||||
},
|
||||
daemonAddrFlag,
|
||||
swapdPortFlag,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -84,7 +88,7 @@ var (
|
||||
Usage: "Duration of time to search for, in seconds",
|
||||
Value: defaultDiscoverSearchTimeSecs,
|
||||
},
|
||||
daemonAddrFlag,
|
||||
swapdPortFlag,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -116,7 +120,7 @@ var (
|
||||
Name: "eth-asset",
|
||||
Usage: "Ethereum ERC-20 token address to receive, or the zero address for regular ETH",
|
||||
},
|
||||
daemonAddrFlag,
|
||||
swapdPortFlag,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -144,14 +148,14 @@ var (
|
||||
Name: "subscribe",
|
||||
Usage: "Subscribe to push notifications about the swap's status",
|
||||
},
|
||||
daemonAddrFlag,
|
||||
swapdPortFlag,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "get-past-swap-ids",
|
||||
Usage: "Get past swap IDs",
|
||||
Action: runGetPastSwapIDs,
|
||||
Flags: []cli.Flag{daemonAddrFlag},
|
||||
Flags: []cli.Flag{swapdPortFlag},
|
||||
},
|
||||
{
|
||||
Name: "get-ongoing-swap",
|
||||
@@ -163,7 +167,7 @@ var (
|
||||
Usage: "ID of swap to retrieve info for",
|
||||
Required: true,
|
||||
},
|
||||
daemonAddrFlag,
|
||||
swapdPortFlag,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -176,7 +180,7 @@ var (
|
||||
Usage: "ID of swap to retrieve info for",
|
||||
Required: true,
|
||||
},
|
||||
daemonAddrFlag,
|
||||
swapdPortFlag,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -189,7 +193,7 @@ var (
|
||||
Usage: "ID of swap to retrieve info for",
|
||||
Required: true,
|
||||
},
|
||||
daemonAddrFlag,
|
||||
swapdPortFlag,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -201,7 +205,7 @@ var (
|
||||
Name: "offer-id",
|
||||
Usage: "ID of swap to retrieve info for",
|
||||
},
|
||||
daemonAddrFlag,
|
||||
swapdPortFlag,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -213,7 +217,7 @@ var (
|
||||
Name: "offer-ids",
|
||||
Usage: "A comma-separated list of offer IDs to delete",
|
||||
},
|
||||
daemonAddrFlag,
|
||||
swapdPortFlag,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -226,7 +230,7 @@ var (
|
||||
Usage: "ID of swap to retrieve info for",
|
||||
Required: true,
|
||||
},
|
||||
daemonAddrFlag,
|
||||
swapdPortFlag,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -239,17 +243,17 @@ var (
|
||||
Usage: "Duration of timeout, in seconds",
|
||||
Required: true,
|
||||
},
|
||||
daemonAddrFlag,
|
||||
swapdPortFlag,
|
||||
},
|
||||
},
|
||||
},
|
||||
Flags: []cli.Flag{daemonAddrFlag},
|
||||
}
|
||||
|
||||
daemonAddrFlag = &cli.StringFlag{
|
||||
Name: "daemon-addr",
|
||||
Usage: "Address of swap daemon",
|
||||
Value: defaultSwapdAddress,
|
||||
swapdPortFlag = &cli.UintFlag{
|
||||
Name: flagSwapdPort,
|
||||
Usage: "RPC port of swap daemon",
|
||||
Value: defaultSwapdPort,
|
||||
EnvVars: []string{"SWAPD_PORT"},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -260,13 +264,20 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
func runAddresses(ctx *cli.Context) error {
|
||||
endpoint := ctx.String("daemon-addr")
|
||||
if endpoint == "" {
|
||||
endpoint = defaultSwapdAddress
|
||||
}
|
||||
func newRRPClient(ctx *cli.Context) *rpcclient.Client {
|
||||
swapdPort := ctx.Uint(flagSwapdPort)
|
||||
endpoint := fmt.Sprintf("http://127.0.0.1:%d", swapdPort)
|
||||
return rpcclient.NewClient(endpoint)
|
||||
}
|
||||
|
||||
c := rpcclient.NewClient(endpoint)
|
||||
func newWSClient(ctx *cli.Context) (wsclient.WsClient, error) {
|
||||
swapdPort := ctx.Uint(flagSwapdPort)
|
||||
endpoint := fmt.Sprintf("ws://127.0.0.1:%d/ws", swapdPort)
|
||||
return wsclient.NewWsClient(ctx.Context, endpoint)
|
||||
}
|
||||
|
||||
func runAddresses(ctx *cli.Context) error {
|
||||
c := newRRPClient(ctx)
|
||||
addrs, err := c.Addresses()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -282,10 +293,9 @@ func runDiscover(ctx *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
endpoint := ctx.String("daemon-addr")
|
||||
searchTime := ctx.Uint("search-time")
|
||||
|
||||
c := rpcclient.NewClient(endpoint)
|
||||
c := newRRPClient(ctx)
|
||||
peers, err := c.Discover(provides, uint64(searchTime))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -300,9 +310,8 @@ func runDiscover(ctx *cli.Context) error {
|
||||
|
||||
func runQuery(ctx *cli.Context) error {
|
||||
maddr := ctx.String("multiaddr")
|
||||
endpoint := ctx.String("daemon-addr")
|
||||
|
||||
c := rpcclient.NewClient(endpoint)
|
||||
c := newRRPClient(ctx)
|
||||
res, err := c.Query(maddr)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -320,14 +329,9 @@ func runQueryAll(ctx *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
endpoint := ctx.String("daemon-addr")
|
||||
if endpoint == "" {
|
||||
endpoint = defaultSwapdAddress
|
||||
}
|
||||
|
||||
searchTime := ctx.Uint("search-time")
|
||||
|
||||
c := rpcclient.NewClient(endpoint)
|
||||
c := newRRPClient(ctx)
|
||||
peers, err := c.QueryAll(provides, uint64(searchTime))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -360,27 +364,41 @@ func runMake(ctx *cli.Context) error {
|
||||
if exchangeRate == 0 {
|
||||
return errNoExchangeRate
|
||||
}
|
||||
|
||||
endpoint := ctx.String("daemon-addr")
|
||||
otherMin := min * exchangeRate
|
||||
otherMax := max * exchangeRate
|
||||
|
||||
ethAssetStr := ctx.String("eth-asset")
|
||||
ethAsset := types.EthAssetETH
|
||||
if ethAssetStr != "" {
|
||||
ethAsset = types.EthAsset(common.HexToAddress(ethAssetStr))
|
||||
ethAsset = types.EthAsset(ethcommon.HexToAddress(ethAssetStr))
|
||||
}
|
||||
|
||||
c := newRRPClient(ctx)
|
||||
ourAddresses, err := c.Addresses()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printOfferSummary := func(offerID string) {
|
||||
fmt.Printf("Published offer with ID: %s\n", offerID)
|
||||
fmt.Printf("On addresses: %v\n", ourAddresses)
|
||||
fmt.Printf("Takers can provide between %s to %s %s\n",
|
||||
common.FmtFloat(otherMin), common.FmtFloat(otherMax), ethAsset)
|
||||
}
|
||||
|
||||
if ctx.Bool("subscribe") {
|
||||
c, err := wsclient.NewWsClient(context.Background(), endpoint)
|
||||
wsc, err := newWSClient(ctx) //nolint:govet
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer wsc.Close()
|
||||
|
||||
id, statusCh, err := wsc.MakeOfferAndSubscribe(min, max, types.ExchangeRate(exchangeRate), ethAsset)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
id, statusCh, err := c.MakeOfferAndSubscribe(min, max, types.ExchangeRate(exchangeRate), ethAsset)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Made offer with ID %s\n", id)
|
||||
printOfferSummary(id)
|
||||
|
||||
for stage := range statusCh {
|
||||
fmt.Printf("> Stage updated: %s\n", stage)
|
||||
@@ -392,18 +410,12 @@ func runMake(ctx *cli.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
c := rpcclient.NewClient(endpoint)
|
||||
id, err := c.MakeOffer(min, max, exchangeRate, ethAsset)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Published offer with ID %s\n", id)
|
||||
addrs, err := c.Addresses()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("On addresses: %v\n", addrs)
|
||||
printOfferSummary(id)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -415,15 +427,14 @@ func runTake(ctx *cli.Context) error {
|
||||
return errNoProvidesAmount
|
||||
}
|
||||
|
||||
endpoint := ctx.String("daemon-addr")
|
||||
|
||||
if ctx.Bool("subscribe") {
|
||||
c, err := wsclient.NewWsClient(context.Background(), endpoint)
|
||||
wsc, err := newWSClient(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer wsc.Close()
|
||||
|
||||
statusCh, err := c.TakeOfferAndSubscribe(maddr, offerID, providesAmount)
|
||||
statusCh, err := wsc.TakeOfferAndSubscribe(maddr, offerID, providesAmount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -440,7 +451,7 @@ func runTake(ctx *cli.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
c := rpcclient.NewClient(endpoint)
|
||||
c := newRRPClient(ctx)
|
||||
err := c.TakeOffer(maddr, offerID, providesAmount)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -451,12 +462,7 @@ func runTake(ctx *cli.Context) error {
|
||||
}
|
||||
|
||||
func runGetPastSwapIDs(ctx *cli.Context) error {
|
||||
endpoint := ctx.String("daemon-addr")
|
||||
if endpoint == "" {
|
||||
endpoint = defaultSwapdAddress
|
||||
}
|
||||
|
||||
c := rpcclient.NewClient(endpoint)
|
||||
c := newRRPClient(ctx)
|
||||
ids, err := c.GetPastSwapIDs()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -467,10 +473,9 @@ func runGetPastSwapIDs(ctx *cli.Context) error {
|
||||
}
|
||||
|
||||
func runGetOngoingSwap(ctx *cli.Context) error {
|
||||
endpoint := ctx.String("daemon-addr")
|
||||
offerID := ctx.String("offer-id")
|
||||
|
||||
c := rpcclient.NewClient(endpoint)
|
||||
c := newRRPClient(ctx)
|
||||
info, err := c.GetOngoingSwap(offerID)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -487,10 +492,9 @@ func runGetOngoingSwap(ctx *cli.Context) error {
|
||||
}
|
||||
|
||||
func runGetPastSwap(ctx *cli.Context) error {
|
||||
endpoint := ctx.String("daemon-addr")
|
||||
offerID := ctx.String("offer-id")
|
||||
|
||||
c := rpcclient.NewClient(endpoint)
|
||||
c := newRRPClient(ctx)
|
||||
info, err := c.GetPastSwap(offerID)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -507,10 +511,9 @@ func runGetPastSwap(ctx *cli.Context) error {
|
||||
}
|
||||
|
||||
func runRefund(ctx *cli.Context) error {
|
||||
endpoint := ctx.String("daemon-addr")
|
||||
offerID := ctx.String("offer-id")
|
||||
|
||||
c := rpcclient.NewClient(endpoint)
|
||||
c := newRRPClient(ctx)
|
||||
resp, err := c.Refund(offerID)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -521,10 +524,9 @@ func runRefund(ctx *cli.Context) error {
|
||||
}
|
||||
|
||||
func runCancel(ctx *cli.Context) error {
|
||||
endpoint := ctx.String("daemon-addr")
|
||||
offerID := ctx.String("offer-id")
|
||||
|
||||
c := rpcclient.NewClient(endpoint)
|
||||
c := newRRPClient(ctx)
|
||||
resp, err := c.Cancel(offerID)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -535,8 +537,7 @@ func runCancel(ctx *cli.Context) error {
|
||||
}
|
||||
|
||||
func runClearOffers(ctx *cli.Context) error {
|
||||
endpoint := ctx.String("daemon-addr")
|
||||
c := rpcclient.NewClient(endpoint)
|
||||
c := newRRPClient(ctx)
|
||||
|
||||
ids := ctx.String("offer-ids")
|
||||
if ids == "" {
|
||||
@@ -559,10 +560,9 @@ func runClearOffers(ctx *cli.Context) error {
|
||||
}
|
||||
|
||||
func runGetStage(ctx *cli.Context) error {
|
||||
endpoint := ctx.String("daemon-addr")
|
||||
offerID := ctx.String("offer-id")
|
||||
|
||||
c := rpcclient.NewClient(endpoint)
|
||||
c := newRRPClient(ctx)
|
||||
resp, err := c.GetStage(offerID)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -577,9 +577,8 @@ func runSetSwapTimeout(ctx *cli.Context) error {
|
||||
if duration == 0 {
|
||||
return errNoDuration
|
||||
}
|
||||
endpoint := ctx.String("daemon-addr")
|
||||
|
||||
c := rpcclient.NewClient(endpoint)
|
||||
c := newRRPClient(ctx)
|
||||
err := c.SetSwapTimeout(uint64(duration))
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -40,10 +40,6 @@ const (
|
||||
defaultRPCPort = 5005
|
||||
defaultXMRTakerRPCPort = 5001
|
||||
defaultXMRMakerRPCPort = 5002
|
||||
|
||||
defaultWSPort = 6005
|
||||
defaultXMRTakerWSPort = 8081
|
||||
defaultXMRMakerWSPort = 8082
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -59,7 +55,6 @@ var (
|
||||
|
||||
const (
|
||||
flagRPCPort = "rpc-port"
|
||||
flagWSPort = "ws-port"
|
||||
flagDataDir = "data-dir"
|
||||
flagLibp2pKey = "libp2p-key"
|
||||
flagLibp2pPort = "libp2p-port"
|
||||
@@ -88,20 +83,17 @@ const (
|
||||
|
||||
var (
|
||||
app = &cli.App{
|
||||
Name: "swapd",
|
||||
Usage: "A program for doing atomic swaps between ETH and XMR",
|
||||
Action: runDaemon,
|
||||
Name: "swapd",
|
||||
Usage: "A program for doing atomic swaps between ETH and XMR",
|
||||
Action: runDaemon,
|
||||
EnableBashCompletion: true,
|
||||
Suggest: true,
|
||||
Flags: []cli.Flag{
|
||||
&cli.UintFlag{
|
||||
Name: flagRPCPort,
|
||||
Usage: "Port for the daemon RPC server to run on",
|
||||
Value: defaultRPCPort,
|
||||
},
|
||||
&cli.UintFlag{
|
||||
Name: flagWSPort,
|
||||
Usage: "Port for the daemon RPC websockets server to run on",
|
||||
Value: defaultWSPort,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flagDataDir,
|
||||
Usage: "Path to store swap artifacts", //nolint:misspell
|
||||
@@ -198,8 +190,7 @@ var (
|
||||
|
||||
func main() {
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
log.Error(err)
|
||||
os.Exit(1)
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -387,21 +378,11 @@ func (d *daemon) make(c *cli.Context) error {
|
||||
rpcPort = defaultXMRMakerRPCPort
|
||||
}
|
||||
}
|
||||
|
||||
wsPort := uint16(c.Uint(flagWSPort))
|
||||
if !c.IsSet(flagWSPort) {
|
||||
switch {
|
||||
case devXMRTaker:
|
||||
wsPort = defaultXMRTakerWSPort
|
||||
case devXMRMaker:
|
||||
wsPort = defaultXMRMakerWSPort
|
||||
}
|
||||
}
|
||||
listenAddr := fmt.Sprintf("127.0.0.1:%d", rpcPort)
|
||||
|
||||
rpcCfg := &rpc.Config{
|
||||
Ctx: d.ctx,
|
||||
Port: rpcPort,
|
||||
WsPort: wsPort,
|
||||
Address: listenAddr,
|
||||
Net: host,
|
||||
XMRTaker: a,
|
||||
XMRMaker: b,
|
||||
@@ -413,20 +394,8 @@ func (d *daemon) make(c *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
errCh := s.Start()
|
||||
go func() {
|
||||
select {
|
||||
case <-d.ctx.Done():
|
||||
return
|
||||
case err := <-errCh:
|
||||
log.Errorf("failed to start RPC server: %s", err)
|
||||
d.cancel()
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
|
||||
log.Infof("started swapd with data-dir %s", cfg.DataDir)
|
||||
return nil
|
||||
log.Infof("starting swapd with data-dir %s", cfg.DataDir)
|
||||
return s.Start()
|
||||
}
|
||||
|
||||
func errFlagsMutuallyExclusive(flag1, flag2 string) error {
|
||||
|
||||
@@ -4,7 +4,9 @@ import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/urfave/cli/v2"
|
||||
@@ -65,8 +67,17 @@ func TestDaemon_DevXMRTaker(t *testing.T) {
|
||||
cancel: cancel,
|
||||
}
|
||||
|
||||
err := d.make(c)
|
||||
require.NoError(t, err)
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
|
||||
go func() {
|
||||
err := d.make(c) // blocks on RPC server start
|
||||
require.ErrorIs(t, err, context.Canceled)
|
||||
wg.Done()
|
||||
}()
|
||||
time.Sleep(500 * time.Millisecond) // let the server start
|
||||
cancel()
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestDaemon_DevXMRMaker(t *testing.T) {
|
||||
@@ -88,8 +99,17 @@ func TestDaemon_DevXMRMaker(t *testing.T) {
|
||||
cancel: cancel,
|
||||
}
|
||||
|
||||
err := d.make(c)
|
||||
require.NoError(t, err)
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
|
||||
go func() {
|
||||
err := d.make(c) // blocks on RPC server start
|
||||
require.ErrorIs(t, err, context.Canceled)
|
||||
wg.Done()
|
||||
}()
|
||||
time.Sleep(500 * time.Millisecond) // let the server start
|
||||
cancel()
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func Test_expandBootnodes(t *testing.T) {
|
||||
|
||||
@@ -48,9 +48,11 @@ var (
|
||||
|
||||
var (
|
||||
app = &cli.App{
|
||||
Name: "swaprecover",
|
||||
Usage: "A program for recovering swap funds due to unexpected shutdowns",
|
||||
Action: runRecover,
|
||||
Name: "swaprecover",
|
||||
Usage: "A program for recovering swap funds due to unexpected shutdowns",
|
||||
Action: runRecover,
|
||||
EnableBashCompletion: true,
|
||||
Suggest: true,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: flagEnv,
|
||||
|
||||
@@ -3,6 +3,7 @@ package common
|
||||
import (
|
||||
"math"
|
||||
"math/big"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -73,3 +74,9 @@ func (a EtherAmount) ToDecimals(decimals uint8) float64 {
|
||||
func (a EtherAmount) String() string {
|
||||
return a.BigInt().String()
|
||||
}
|
||||
|
||||
// FmtFloat creates a string from a floating point value that keeps maximum precision,
|
||||
// does not use exponent notation, and has no trailing zeros after the decimal point.
|
||||
func FmtFloat(f float64) string {
|
||||
return strconv.FormatFloat(f, 'f', -1, 64)
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ func TestEtherAmount(t *testing.T) {
|
||||
|
||||
func TestToDecimals(t *testing.T) {
|
||||
val := NewEtherAmount(123456)
|
||||
require.Equal(t, fmt.Sprint(val.ToDecimals(5)), "1.23456")
|
||||
require.Equal(t, "1.23456", FmtFloat(val.ToDecimals(5)))
|
||||
val = NewEtherAmount(1234567890)
|
||||
require.Equal(t, fmt.Sprint(val.ToDecimals(6)), "1234.56789")
|
||||
require.Equal(t, "1234.56789", FmtFloat(val.ToDecimals(6)))
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ Create a wallet for "Bob", who will own XMR later on:
|
||||
./monero-wallet-cli // you will be prompted to create a wallet. In the next steps, we will go with "Bob", without password. Remember the name and optionally the password for the upcoming steps
|
||||
```
|
||||
|
||||
You do not need to mine blocks, and you can exit the the wallet-cli once Bob's account has been created by typing "exit".
|
||||
You do not need to mine blocks, and you can exit the wallet-cli once Bob's account has been created by typing "exit".
|
||||
|
||||
Start monero-wallet-rpc for Bob on port 18083. Make sure `--wallet-dir` corresponds to the directory the wallet from the previous step is in:
|
||||
```bash
|
||||
@@ -98,13 +98,13 @@ In terminal 3, we will interact with the swap daemon using `swapcli`.
|
||||
|
||||
Firstly, we need Bob to make an offer and advertise it, so that Alice can take it:
|
||||
```bash
|
||||
./swapcli make --min-amount 0.1 --max-amount 1 --exchange-rate 0.05 --daemon-addr=http://localhost:5002
|
||||
./swapcli make --min-amount 0.1 --max-amount 1 --exchange-rate 0.05 --swapd-port 5002
|
||||
# Published offer with ID cf4bf01a0775a0d13fa41b14516e4b89034300707a1754e0d99b65f6cb6fffb9
|
||||
```
|
||||
|
||||
Alternatively, you can make the offer via websockets and get notified when the swap is taken:
|
||||
```bash
|
||||
./swapcli make --min-amount 0.1 --max-amount 1 --exchange-rate 0.05 --daemon-addr=ws://localhost:8082 --subscribe
|
||||
./swapcli make --min-amount 0.1 --max-amount 1 --exchange-rate 0.05 --swapd-port 5002 --subscribe
|
||||
```
|
||||
|
||||
Now, we can have Alice begin discovering peers who have offers advertised.
|
||||
@@ -127,7 +127,7 @@ Now, we can tell Alice to initiate the protocol w/ the peer (Bob), the offer (co
|
||||
|
||||
Alternatively, you can take the offer via websockets and get notified when the swap status updates:
|
||||
```bash
|
||||
./swapcli take --multiaddr /ip4/127.0.0.1/tcp/9934/p2p/12D3KooWHLUrLnJtUbaGzTSi6azZavKhNgUZTtSiUZ9Uy12v1eZ7 --offer-id cf4bf01a0775a0d13fa41b14516e4b89034300707a1754e0d99b65f6cb6fffb9 --provides-amount 0.05 --subscribe --daemon-addr=ws://localhost:8081
|
||||
./swapcli take --multiaddr /ip4/127.0.0.1/tcp/9934/p2p/12D3KooWHLUrLnJtUbaGzTSi6azZavKhNgUZTtSiUZ9Uy12v1eZ7 --offer-id cf4bf01a0775a0d13fa41b14516e4b89034300707a1754e0d99b65f6cb6fffb9 --provides-amount 0.05 --subscribe --swapd-port 5001
|
||||
```
|
||||
|
||||
If all goes well, you should see Alice and Bob successfully exchange messages and execute the swap protocol. The result is that Alice now owns the private key to a Monero account (and is the only owner of that key) and Bob has the ETH transferred to him. On Alice's side, a Monero wallet will be generated in the `--wallet-dir` provided in the `monero-wallet-rpc` step for Alice.
|
||||
|
||||
@@ -258,7 +258,7 @@ Returns:
|
||||
|
||||
Example:
|
||||
```bash
|
||||
wscat -c ws://localhost:8081
|
||||
wscat -c ws://localhost:5001/ws
|
||||
# Connected (press CTRL+C to quit)
|
||||
# > {"jsonrpc":"2.0", "method":"swap_subscribeStatus", "params": {"id": "7492ceb4d0f5f45ecd5d06923b35cae406d1406cd685ce1ba184f2a40c683ac2"}, "id": 0}
|
||||
# < {"jsonrpc":"2.0","result":{"stage":"ETHLocked"},"error":null,"id":null}
|
||||
@@ -282,7 +282,7 @@ Returns:
|
||||
|
||||
Example (including notifications when swap is taken):
|
||||
```bash
|
||||
wscat -c ws://localhost:8082
|
||||
wscat -c ws://localhost:5002/ws
|
||||
# Connected (press CTRL+C to quit)
|
||||
# > {"jsonrpc":"2.0", "method":"net_makeOfferAndSubscribe", "params": {"minimumAmount": 0.1, "maximumAmount": 1, "exchangeRate": 0.05}, "id": 0}
|
||||
# < {"jsonrpc":"2.0","result":{"offerID":"cf4bf01a0775a0d13fa41b14516e4b89034300707a1754e0d99b65f6cb6fffb9"},"error":null,"id":null}
|
||||
@@ -308,7 +308,7 @@ Returns:
|
||||
|
||||
Example:
|
||||
```bash
|
||||
wscat -c ws://localhost:8081
|
||||
wscat -c ws://localhost:5001/ws
|
||||
# Connected (press CTRL+C to quit)
|
||||
# > {"jsonrpc":"2.0", "method":"net_takeOfferAndSubscribe", "params": {"multiaddr": "/ip4/192.168.0.101/tcp/9934/p2p/12D3KooWHLUrLnJtUbaGzTSi6azZavKhNgUZTtSiUZ9Uy12v1eZ7", "offerID": "cf4bf01a0775a0d13fa41b14516e4b89034300707a1754e0d99b65f6cb6fffb9", "providesAmount": 0.05}, "id": 0}
|
||||
# < {"jsonrpc":"2.0","result":{"id":0},"error":null,"id":null}
|
||||
@@ -316,4 +316,4 @@ wscat -c ws://localhost:8081
|
||||
# < {"jsonrpc":"2.0","result":{"stage":"ETHLocked"},"error":null,"id":null}
|
||||
# < {"jsonrpc":"2.0","result":{"stage":"ContractReady"},"error":null,"id":null}
|
||||
# < {"jsonrpc":"2.0","result":{"stage":"Success"},"error":null,"id":null}
|
||||
```
|
||||
```
|
||||
|
||||
@@ -93,7 +93,7 @@ yarn start
|
||||
|
||||
1. Search for existing XMR offers using `swapcli`:
|
||||
```bash
|
||||
./swapcli discover --provides XMR --search-time 3 --daemon-addr=http://localhost:5005
|
||||
./swapcli discover --provides XMR --search-time 3 --swapd-port 5001
|
||||
# [[/ip4/127.0.0.1/tcp/9934/p2p/12D3KooWC547RfLcveQi1vBxACjnT6Uv15V11ortDTuxRWuhubGv /ip4/127.0.0.1/tcp/9934/p2p/12D3KooWC547RfLcveQi1vBxACjnT6Uv15V11ortDTuxRWuhubGv]]
|
||||
```
|
||||
|
||||
@@ -113,7 +113,7 @@ yarn start
|
||||
|
||||
3. b. Alternatively, you can take the offer via websockets and get notified when the swap status updates:
|
||||
```bash
|
||||
./swapcli take --multiaddr /ip4/127.0.0.1/tcp/9934/p2p/12D3KooWHLUrLnJtUbaGzTSi6azZavKhNgUZTtSiUZ9Uy12v1eZ7 --offer-id cf4bf01a0775a0d13fa41b14516e4b89034300707a1754e0d99b65f6cb6fffb9 --provides-amount 0.05 --subscribe --daemon-addr=ws://localhost:8081
|
||||
./swapcli take --multiaddr /ip4/127.0.0.1/tcp/9934/p2p/12D3KooWHLUrLnJtUbaGzTSi6azZavKhNgUZTtSiUZ9Uy12v1eZ7 --offer-id cf4bf01a0775a0d13fa41b14516e4b89034300707a1754e0d99b65f6cb6fffb9 --provides-amount 0.05 --subscribe --swapd-port 5001
|
||||
```
|
||||
|
||||
If all goes well, you should see the node execute the swap protocol. If the swap ends successfully, a Monero wallet will be generated in the `--wallet-dir` provided in the `monero-wallet-rpc` step (so `./node-keys`) named `swap-deposit-wallet`. This wallet will contained the received XMR.
|
||||
@@ -140,13 +140,13 @@ If you don't have any luck with these, please message me on twitter/reddit (@eli
|
||||
|
||||
4. a. Make an offer with `swapcli`:
|
||||
```bash
|
||||
./swapcli make --min-amount 0.1 --max-amount 1 --exchange-rate 0.5 --daemon-addr http://localhost:5005
|
||||
./swapcli make --min-amount 0.1 --max-amount 1 --exchange-rate 0.5 --swapd-port 5001
|
||||
# Published offer with ID cf4bf01a0775a0d13fa41b14516e4b89034300707a1754e0d99b65f6cb6fffb9
|
||||
```
|
||||
|
||||
4. b. Alternatively, make an offer and subscribe to updates on it with `swapcli`:
|
||||
```bash
|
||||
./swapcli make --min-amount 0.1 --max-amount 1 --exchange-rate 0.5 --daemon-addr ws://localhost:6005 --subscribe
|
||||
./swapcli make --min-amount 0.1 --max-amount 1 --exchange-rate 0.5 --swapd-port 5001 --subscribe
|
||||
# Published offer with ID cf4bf01a0775a0d13fa41b14516e4b89034300707a1754e0d99b65f6cb6fffb9
|
||||
```
|
||||
|
||||
|
||||
@@ -25,9 +25,9 @@ func TestClient_Transfer(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
daemon := NewDaemonClient(common.DefaultMoneroDaemonEndpoint)
|
||||
_ = daemon.GenerateBlocks(xmrmakerAddr.Address, 512)
|
||||
|
||||
time.Sleep(time.Second * 10)
|
||||
err = daemon.GenerateBlocks(xmrmakerAddr.Address, 512)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, cXMRMaker.Refresh())
|
||||
|
||||
balance, err := cXMRMaker.GetBalance(0)
|
||||
require.NoError(t, err)
|
||||
@@ -76,12 +76,14 @@ func TestClient_Transfer(t *testing.T) {
|
||||
break
|
||||
}
|
||||
|
||||
_ = daemon.GenerateBlocks(xmrmakerAddr.Address, 1)
|
||||
time.Sleep(time.Second)
|
||||
err = daemon.GenerateBlocks(xmrmakerAddr.Address, 1)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, cXMRMaker.Refresh())
|
||||
}
|
||||
|
||||
err = daemon.GenerateBlocks(xmrmakerAddr.Address, 16)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, cXMRMaker.Refresh())
|
||||
|
||||
// generate spend account for A+B
|
||||
skAKPriv := mcrypto.SumPrivateSpendKeys(kpA.SpendKey(), kpB.SpendKey())
|
||||
|
||||
@@ -37,6 +37,19 @@ func newDiscovery(ctx context.Context, h libp2phost.Host, bnsFunc func() []peer.
|
||||
dual.DHTOption(kaddht.Mode(kaddht.ModeAutoServer)),
|
||||
}
|
||||
|
||||
//
|
||||
// There is libp2p bug when calling `dual.New` with a cancelled context creating a panic,
|
||||
// so we added the extra guard below:
|
||||
// Panic: https://github.com/jbenet/goprocess/blob/v0.1.4/impl-mutex.go#L99
|
||||
// Caller: https://github.com/libp2p/go-libp2p-kad-dht/blob/v0.17.0/dht.go#L222
|
||||
//
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
default:
|
||||
// not cancelled, continue on
|
||||
}
|
||||
|
||||
dht, err := dual.New(ctx, h, dhtOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/athanorlabs/atomic-swap/common"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -41,8 +43,8 @@ type errBalanceTooLow struct {
|
||||
|
||||
func (e errBalanceTooLow) Error() string {
|
||||
return fmt.Sprintf("balance of %s XMR is below provided %s XMR",
|
||||
strconv.FormatFloat(e.unlockedBalance, 'f', -1, 64),
|
||||
strconv.FormatFloat(e.providedAmount, 'f', -1, 64),
|
||||
common.FmtFloat(e.unlockedBalance),
|
||||
common.FmtFloat(e.providedAmount),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -53,8 +55,8 @@ type errAmountProvidedTooLow struct {
|
||||
|
||||
func (e errAmountProvidedTooLow) Error() string {
|
||||
return fmt.Sprintf("%s XMR provided by taker is under offer minimum of %s XMR",
|
||||
strconv.FormatFloat(e.providedAmount, 'f', -1, 64),
|
||||
strconv.FormatFloat(e.minAmount, 'f', -1, 64),
|
||||
common.FmtFloat(e.providedAmount),
|
||||
common.FmtFloat(e.minAmount),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -65,8 +67,8 @@ type errAmountProvidedTooHigh struct {
|
||||
|
||||
func (e errAmountProvidedTooHigh) Error() string {
|
||||
return fmt.Sprintf("%s XMR provided by taker is over offer maximum of %s XMR",
|
||||
strconv.FormatFloat(e.providedAmount, 'f', -1, 64),
|
||||
strconv.FormatFloat(e.maxAmount, 'f', -1, 64),
|
||||
common.FmtFloat(e.providedAmount),
|
||||
common.FmtFloat(e.maxAmount),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
208
rpc/mocks_test.go
Normal file
208
rpc/mocks_test.go
Normal file
@@ -0,0 +1,208 @@
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
ethcommon "github.com/ethereum/go-ethereum/common"
|
||||
"github.com/libp2p/go-libp2p-core/peer"
|
||||
|
||||
"github.com/athanorlabs/atomic-swap/common"
|
||||
"github.com/athanorlabs/atomic-swap/common/types"
|
||||
mcrypto "github.com/athanorlabs/atomic-swap/crypto/monero"
|
||||
"github.com/athanorlabs/atomic-swap/net"
|
||||
"github.com/athanorlabs/atomic-swap/net/message"
|
||||
"github.com/athanorlabs/atomic-swap/protocol/swap"
|
||||
"github.com/athanorlabs/atomic-swap/protocol/txsender"
|
||||
)
|
||||
|
||||
//
|
||||
// This file only contains mock definitions used by other test files
|
||||
//
|
||||
|
||||
type mockNet struct{}
|
||||
|
||||
func (*mockNet) Addresses() []string {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (*mockNet) Advertise() {
|
||||
}
|
||||
|
||||
func (*mockNet) Discover(provides types.ProvidesCoin, searchTime time.Duration) ([]peer.AddrInfo, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (*mockNet) Query(who peer.AddrInfo) (*net.QueryResponse, error) {
|
||||
var offer types.Offer
|
||||
offerJSON := fmt.Sprintf(`{"ID":%q}`, testSwapID.String())
|
||||
if err := json.Unmarshal([]byte(offerJSON), &offer); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return &net.QueryResponse{Offers: []*types.Offer{&offer}}, nil
|
||||
}
|
||||
|
||||
func (*mockNet) Initiate(who peer.AddrInfo, msg *net.SendKeysMessage, s common.SwapStateNet) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*mockNet) CloseProtocolStream(types.Hash) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
type mockSwapManager struct{}
|
||||
|
||||
func (*mockSwapManager) GetPastIDs() []types.Hash {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (*mockSwapManager) GetPastSwap(id types.Hash) *swap.Info {
|
||||
return &swap.Info{}
|
||||
}
|
||||
|
||||
func (*mockSwapManager) GetOngoingSwap(id types.Hash) *swap.Info {
|
||||
statusCh := make(chan types.Status, 1)
|
||||
statusCh <- types.CompletedSuccess
|
||||
|
||||
return swap.NewInfo(
|
||||
id,
|
||||
types.ProvidesETH,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
types.EthAssetETH,
|
||||
types.CompletedSuccess,
|
||||
statusCh,
|
||||
)
|
||||
}
|
||||
|
||||
func (*mockSwapManager) AddSwap(*swap.Info) error {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (*mockSwapManager) CompleteOngoingSwap(types.Hash) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
type mockXMRTaker struct{}
|
||||
|
||||
func (*mockXMRTaker) Provides() types.ProvidesCoin {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (*mockXMRTaker) SetGasPrice(gasPrice uint64) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (*mockXMRTaker) GetOngoingSwapState(types.Hash) common.SwapState {
|
||||
return new(mockSwapState)
|
||||
}
|
||||
|
||||
func (*mockXMRTaker) InitiateProtocol(providesAmount float64, _ *types.Offer) (common.SwapState, error) {
|
||||
return new(mockSwapState), nil
|
||||
}
|
||||
|
||||
func (*mockXMRTaker) Refund(types.Hash) (ethcommon.Hash, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (*mockXMRTaker) SetSwapTimeout(_ time.Duration) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (*mockXMRTaker) ExternalSender(_ types.Hash) (*txsender.ExternalSender, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
type mockXMRMaker struct{}
|
||||
|
||||
func (m *mockXMRMaker) Provides() types.ProvidesCoin {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (m *mockXMRMaker) GetOngoingSwapState(hash types.Hash) common.SwapState {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (*mockXMRMaker) MakeOffer(offer *types.Offer) (*types.OfferExtra, error) {
|
||||
offerExtra := &types.OfferExtra{
|
||||
StatusCh: make(chan types.Status, 1),
|
||||
InfoFile: "/dev/null",
|
||||
}
|
||||
offerExtra.StatusCh <- types.CompletedSuccess
|
||||
return offerExtra, nil
|
||||
}
|
||||
|
||||
func (*mockXMRMaker) SetMoneroWalletFile(file string, password string) error {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (*mockXMRMaker) GetOffers() []*types.Offer {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (*mockXMRMaker) ClearOffers([]string) error {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
type mockSwapState struct{}
|
||||
|
||||
func (*mockSwapState) HandleProtocolMessage(msg message.Message) (resp message.Message, done bool, err error) {
|
||||
return nil, true, nil
|
||||
}
|
||||
|
||||
func (*mockSwapState) Exit() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*mockSwapState) SendKeysMessage() (*message.SendKeysMessage, error) {
|
||||
return &message.SendKeysMessage{}, nil
|
||||
}
|
||||
|
||||
func (*mockSwapState) ID() types.Hash {
|
||||
return testSwapID
|
||||
}
|
||||
|
||||
func (*mockSwapState) InfoFile() string {
|
||||
return os.TempDir() + "test.infofile"
|
||||
}
|
||||
|
||||
type mockProtocolBackend struct {
|
||||
sm *mockSwapManager
|
||||
}
|
||||
|
||||
func newMockProtocolBackend() *mockProtocolBackend {
|
||||
return &mockProtocolBackend{
|
||||
sm: new(mockSwapManager),
|
||||
}
|
||||
}
|
||||
|
||||
func (*mockProtocolBackend) Env() common.Environment {
|
||||
return common.Development
|
||||
}
|
||||
|
||||
func (*mockProtocolBackend) SetGasPrice(uint64) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (*mockProtocolBackend) SetSwapTimeout(timeout time.Duration) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (b *mockProtocolBackend) SwapManager() swap.Manager {
|
||||
return b.sm
|
||||
}
|
||||
|
||||
func (*mockProtocolBackend) SetEthAddress(ethcommon.Address) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (*mockProtocolBackend) SetXMRDepositAddress(mcrypto.Address, types.Hash) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (*mockProtocolBackend) ClearXMRDepositAddress(types.Hash) {
|
||||
panic("not implemented")
|
||||
}
|
||||
@@ -5,11 +5,12 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
ethcommon "github.com/ethereum/go-ethereum/common"
|
||||
|
||||
"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"
|
||||
ethcommon "github.com/ethereum/go-ethereum/common"
|
||||
|
||||
"github.com/libp2p/go-libp2p-core/peer"
|
||||
)
|
||||
|
||||
100
rpc/server.go
100
rpc/server.go
@@ -3,6 +3,7 @@ package rpc
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@@ -24,17 +25,15 @@ var log = logging.Logger("rpc")
|
||||
|
||||
// Server represents the JSON-RPC server
|
||||
type Server struct {
|
||||
s *rpc.Server
|
||||
wsServer *wsServer
|
||||
port uint16
|
||||
wsPort uint16
|
||||
ctx context.Context
|
||||
listener net.Listener
|
||||
httpServer *http.Server
|
||||
}
|
||||
|
||||
// Config ...
|
||||
type Config struct {
|
||||
Ctx context.Context
|
||||
Port uint16
|
||||
WsPort uint16
|
||||
Address string // "IP:port"
|
||||
Net Net
|
||||
XMRTaker XMRTaker
|
||||
XMRMaker XMRMaker
|
||||
@@ -43,67 +42,70 @@ type Config struct {
|
||||
|
||||
// NewServer ...
|
||||
func NewServer(cfg *Config) (*Server, error) {
|
||||
s := rpc.NewServer()
|
||||
s.RegisterCodec(NewCodec(), "application/json")
|
||||
rpcServer := rpc.NewServer()
|
||||
rpcServer.RegisterCodec(NewCodec(), "application/json")
|
||||
|
||||
ns := NewNetService(cfg.Net, cfg.XMRTaker, cfg.XMRMaker, cfg.ProtocolBackend.SwapManager())
|
||||
if err := s.RegisterService(ns, "net"); err != nil {
|
||||
if err := rpcServer.RegisterService(ns, "net"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.RegisterService(NewPersonalService(cfg.XMRMaker, cfg.ProtocolBackend), "personal"); err != nil {
|
||||
if err := rpcServer.RegisterService(NewPersonalService(cfg.XMRMaker, cfg.ProtocolBackend), "personal"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.RegisterService(NewSwapService(cfg.ProtocolBackend.SwapManager(), cfg.XMRTaker, cfg.XMRMaker, cfg.Net), "swap"); err != nil { //nolint:lll
|
||||
if err := rpcServer.RegisterService(NewSwapService(cfg.ProtocolBackend.SwapManager(), cfg.XMRTaker, cfg.XMRMaker, cfg.Net), "swap"); err != nil { //nolint:lll
|
||||
return nil, err
|
||||
}
|
||||
|
||||
wsServer := newWsServer(cfg.Ctx, cfg.ProtocolBackend.SwapManager(), ns, cfg.ProtocolBackend, cfg.XMRTaker)
|
||||
|
||||
lc := net.ListenConfig{}
|
||||
ln, err := lc.Listen(cfg.Ctx, "tcp", cfg.Address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r := mux.NewRouter()
|
||||
r.Handle("/", rpcServer)
|
||||
r.Handle("/ws", wsServer)
|
||||
|
||||
headersOk := handlers.AllowedHeaders([]string{"content-type", "username", "password"})
|
||||
methodsOk := handlers.AllowedMethods([]string{"GET", "HEAD", "POST", "PUT", "OPTIONS"})
|
||||
originsOk := handlers.AllowedOrigins([]string{"*"})
|
||||
server := &http.Server{
|
||||
Addr: ln.Addr().String(),
|
||||
ReadHeaderTimeout: time.Second,
|
||||
Handler: handlers.CORS(headersOk, methodsOk, originsOk)(r),
|
||||
BaseContext: func(listener net.Listener) context.Context {
|
||||
return cfg.Ctx
|
||||
},
|
||||
}
|
||||
|
||||
return &Server{
|
||||
s: s,
|
||||
wsServer: newWsServer(cfg.Ctx, cfg.ProtocolBackend.SwapManager(), ns, cfg.ProtocolBackend, cfg.XMRTaker),
|
||||
port: cfg.Port,
|
||||
wsPort: cfg.WsPort,
|
||||
ctx: cfg.Ctx,
|
||||
listener: ln,
|
||||
httpServer: server,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Start starts the JSON-RPC server.
|
||||
func (s *Server) Start() <-chan error {
|
||||
errCh := make(chan error)
|
||||
// HttpURL returns the URL used for HTTP requests
|
||||
func (s *Server) HttpURL() string { //nolint:revive
|
||||
return fmt.Sprintf("http://%s", s.httpServer.Addr)
|
||||
}
|
||||
|
||||
go func() {
|
||||
r := mux.NewRouter()
|
||||
r.Handle("/", s.s)
|
||||
// WsURL returns the URL used for websocket requests
|
||||
func (s *Server) WsURL() string {
|
||||
return fmt.Sprintf("ws://%s/ws", s.httpServer.Addr)
|
||||
}
|
||||
|
||||
headersOk := handlers.AllowedHeaders([]string{"content-type", "username", "password"})
|
||||
methodsOk := handlers.AllowedMethods([]string{"GET", "HEAD", "POST", "PUT", "OPTIONS"})
|
||||
originsOk := handlers.AllowedOrigins([]string{"*"})
|
||||
// Start starts the JSON-RPC and Websocket server.
|
||||
func (s *Server) Start() error {
|
||||
log.Infof("Starting RPC server on %s", s.HttpURL())
|
||||
log.Infof("Starting websockets server on %s", s.WsURL())
|
||||
|
||||
log.Infof("starting RPC server on http://127.0.0.1:%d", s.port)
|
||||
|
||||
if err := http.ListenAndServe(fmt.Sprintf("127.0.0.1:%d", s.port), handlers.CORS(headersOk, methodsOk, originsOk)(r)); err != nil { //nolint:lll
|
||||
log.Errorf("failed to start http RPC server: %s", err)
|
||||
errCh <- err
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
r := mux.NewRouter()
|
||||
r.Handle("/", s.wsServer)
|
||||
|
||||
headersOk := handlers.AllowedHeaders([]string{"content-type", "username", "password"})
|
||||
methodsOk := handlers.AllowedMethods([]string{"GET", "HEAD", "POST", "PUT", "OPTIONS"})
|
||||
originsOk := handlers.AllowedOrigins([]string{"*"})
|
||||
|
||||
log.Infof("starting websockets server on ws://127.0.0.1:%d", s.wsPort)
|
||||
|
||||
if err := http.ListenAndServe(fmt.Sprintf("127.0.0.1:%d", s.wsPort), handlers.CORS(headersOk, methodsOk, originsOk)(r)); err != nil { //nolint:lll
|
||||
log.Errorf("failed to start websockets RPC server: %s", err)
|
||||
errCh <- err
|
||||
}
|
||||
}()
|
||||
|
||||
return errCh
|
||||
err := s.httpServer.Serve(s.listener) // Serve never returns nil
|
||||
return fmt.Errorf("RPC server failed: %w", err)
|
||||
}
|
||||
|
||||
// Protocol represents the functions required by the rpc service into the protocol handler.
|
||||
|
||||
211
rpc/ws_test.go
211
rpc/ws_test.go
@@ -2,23 +2,14 @@ package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"net/http"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/athanorlabs/atomic-swap/common"
|
||||
"github.com/athanorlabs/atomic-swap/common/types"
|
||||
mcrypto "github.com/athanorlabs/atomic-swap/crypto/monero"
|
||||
"github.com/athanorlabs/atomic-swap/net"
|
||||
"github.com/athanorlabs/atomic-swap/net/message"
|
||||
"github.com/athanorlabs/atomic-swap/protocol/swap"
|
||||
"github.com/athanorlabs/atomic-swap/protocol/txsender"
|
||||
"github.com/athanorlabs/atomic-swap/rpcclient/wsclient"
|
||||
|
||||
ethcommon "github.com/ethereum/go-ethereum/common"
|
||||
"github.com/libp2p/go-libp2p-core/peer"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -27,163 +18,50 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
testSwapID = types.Hash{99}
|
||||
testTImeout = time.Second * 5
|
||||
defaultRPCPort uint16 = 3001
|
||||
defaultWSPort uint16 = 4002
|
||||
testSwapID = types.Hash{99}
|
||||
testTimeout = time.Second * 5
|
||||
)
|
||||
|
||||
func defaultWSEndpoint() string {
|
||||
return fmt.Sprintf("ws://localhost:%d", defaultWSPort)
|
||||
}
|
||||
|
||||
type mockNet struct{}
|
||||
|
||||
func (*mockNet) Addresses() []string {
|
||||
return nil
|
||||
}
|
||||
func (*mockNet) Advertise() {}
|
||||
func (*mockNet) Discover(provides types.ProvidesCoin, searchTime time.Duration) ([]peer.AddrInfo, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (*mockNet) Query(who peer.AddrInfo) (*net.QueryResponse, error) {
|
||||
var offer types.Offer
|
||||
offerJSON := fmt.Sprintf(`{"ID":%q}`, testSwapID.String())
|
||||
if err := json.Unmarshal([]byte(offerJSON), &offer); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return &net.QueryResponse{Offers: []*types.Offer{&offer}}, nil
|
||||
}
|
||||
func (*mockNet) Initiate(who peer.AddrInfo, msg *net.SendKeysMessage, s common.SwapStateNet) error {
|
||||
return nil
|
||||
}
|
||||
func (*mockNet) CloseProtocolStream(types.Hash) {}
|
||||
|
||||
type mockSwapManager struct{}
|
||||
|
||||
func (*mockSwapManager) GetPastIDs() []types.Hash {
|
||||
return []types.Hash{}
|
||||
}
|
||||
func (*mockSwapManager) GetPastSwap(id types.Hash) *swap.Info {
|
||||
return &swap.Info{}
|
||||
}
|
||||
func (*mockSwapManager) GetOngoingSwap(id types.Hash) *swap.Info {
|
||||
statusCh := make(chan types.Status, 1)
|
||||
statusCh <- types.CompletedSuccess
|
||||
|
||||
return swap.NewInfo(
|
||||
id,
|
||||
types.ProvidesETH,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
types.EthAssetETH,
|
||||
types.CompletedSuccess,
|
||||
statusCh,
|
||||
)
|
||||
}
|
||||
func (*mockSwapManager) AddSwap(*swap.Info) error {
|
||||
return nil
|
||||
}
|
||||
func (*mockSwapManager) CompleteOngoingSwap(types.Hash) {}
|
||||
|
||||
type mockXMRTaker struct{}
|
||||
|
||||
func (*mockXMRTaker) Provides() types.ProvidesCoin {
|
||||
return types.ProvidesETH
|
||||
}
|
||||
func (*mockXMRTaker) SetGasPrice(gasPrice uint64) {}
|
||||
func (*mockXMRTaker) GetOngoingSwapState(types.Hash) common.SwapState {
|
||||
return new(mockSwapState)
|
||||
}
|
||||
func (*mockXMRTaker) InitiateProtocol(providesAmount float64, _ *types.Offer) (common.SwapState, error) {
|
||||
return new(mockSwapState), nil
|
||||
}
|
||||
func (*mockXMRTaker) Refund(types.Hash) (ethcommon.Hash, error) {
|
||||
return ethcommon.Hash{}, nil
|
||||
}
|
||||
func (*mockXMRTaker) SetSwapTimeout(_ time.Duration) {}
|
||||
func (*mockXMRTaker) ExternalSender(_ types.Hash) (*txsender.ExternalSender, error) {
|
||||
return nil, fmt.Errorf("unimplemented")
|
||||
}
|
||||
|
||||
type mockSwapState struct{}
|
||||
|
||||
func (*mockSwapState) HandleProtocolMessage(msg message.Message) (resp message.Message, done bool, err error) {
|
||||
return nil, true, nil
|
||||
}
|
||||
func (*mockSwapState) Exit() error {
|
||||
return nil
|
||||
}
|
||||
func (*mockSwapState) SendKeysMessage() (*message.SendKeysMessage, error) {
|
||||
return &message.SendKeysMessage{}, nil
|
||||
}
|
||||
func (*mockSwapState) ID() types.Hash {
|
||||
return testSwapID
|
||||
}
|
||||
func (*mockSwapState) InfoFile() string {
|
||||
return os.TempDir() + "test.infofile"
|
||||
}
|
||||
|
||||
type mockProtocolBackend struct {
|
||||
sm *mockSwapManager
|
||||
}
|
||||
|
||||
func newMockProtocolBackend() *mockProtocolBackend {
|
||||
return &mockProtocolBackend{
|
||||
sm: new(mockSwapManager),
|
||||
}
|
||||
}
|
||||
func (*mockProtocolBackend) Env() common.Environment {
|
||||
return common.Development
|
||||
}
|
||||
func (*mockProtocolBackend) SetGasPrice(uint64) {}
|
||||
func (*mockProtocolBackend) SetSwapTimeout(timeout time.Duration) {}
|
||||
func (b *mockProtocolBackend) SwapManager() swap.Manager {
|
||||
return b.sm
|
||||
}
|
||||
func (*mockProtocolBackend) SetEthAddress(ethcommon.Address) {}
|
||||
func (*mockProtocolBackend) SetXMRDepositAddress(mcrypto.Address, types.Hash) {}
|
||||
func (*mockProtocolBackend) ClearXMRDepositAddress(types.Hash) {}
|
||||
|
||||
func newServer(t *testing.T) *Server {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(func() {
|
||||
cancel()
|
||||
})
|
||||
|
||||
defaultRPCPort++
|
||||
defaultWSPort++
|
||||
|
||||
cfg := &Config{
|
||||
Ctx: ctx,
|
||||
Port: defaultRPCPort,
|
||||
WsPort: defaultWSPort,
|
||||
Address: "127.0.0.1:0", // OS assigned port
|
||||
Net: new(mockNet),
|
||||
ProtocolBackend: newMockProtocolBackend(),
|
||||
XMRTaker: new(mockXMRTaker),
|
||||
XMRMaker: new(mockXMRMaker),
|
||||
}
|
||||
|
||||
s, err := NewServer(cfg)
|
||||
require.NoError(t, err)
|
||||
errCh := s.Start()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
|
||||
go func() {
|
||||
err := <-errCh
|
||||
require.NoError(t, err)
|
||||
err := s.Start()
|
||||
require.ErrorIs(t, err, http.ErrServerClosed)
|
||||
wg.Done()
|
||||
}()
|
||||
time.Sleep(time.Millisecond * 300) // let server start up
|
||||
|
||||
t.Cleanup(func() {
|
||||
cancel()
|
||||
// Using non-cancelled context, so shutdown waits for clients to disconnect before unblocking
|
||||
err := s.httpServer.Shutdown(context.Background())
|
||||
require.NoError(t, err)
|
||||
wg.Wait() // unblocks when server exits
|
||||
})
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func TestSubscribeSwapStatus(t *testing.T) {
|
||||
_ = newServer(t)
|
||||
s := newServer(t)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(func() {
|
||||
cancel()
|
||||
})
|
||||
c, err := wsclient.NewWsClient(ctx, defaultWSEndpoint())
|
||||
c, err := wsclient.NewWsClient(s.ctx, s.WsURL())
|
||||
require.NoError(t, err)
|
||||
|
||||
ch, err := c.SubscribeSwapStatus(testSwapID)
|
||||
@@ -192,41 +70,36 @@ func TestSubscribeSwapStatus(t *testing.T) {
|
||||
select {
|
||||
case status := <-ch:
|
||||
require.Equal(t, types.CompletedSuccess, status)
|
||||
case <-time.After(testTImeout):
|
||||
case <-time.After(testTimeout):
|
||||
t.Fatal("test timed out")
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: add unit test
|
||||
// func TestSubscribeMakeOffer(t *testing.T) {
|
||||
// _ = newServer(t)
|
||||
func TestSubscribeMakeOffer(t *testing.T) {
|
||||
s := newServer(t)
|
||||
|
||||
// ctx, cancel := context.WithCancel(context.Background())
|
||||
// t.Cleanup(func() {
|
||||
// cancel()
|
||||
// })
|
||||
// c, err := rpcclient.NewWsClient(ctx, defaultWSEndpoint())
|
||||
// require.NoError(t, err)
|
||||
c, err := wsclient.NewWsClient(s.ctx, s.WsURL())
|
||||
require.NoError(t, err)
|
||||
|
||||
// id, ch, err := c.MakeOfferAndSubscribe(0.1, 1, 0.05)
|
||||
// require.NoError(t, err)
|
||||
// require.Equal(t, id, testSwapID)
|
||||
// select {
|
||||
// case status := <-ch:
|
||||
// require.Equal(t, types.CompletedSuccess, status)
|
||||
// case <-time.After(testTImeout):
|
||||
// t.Fatal("test timed out")
|
||||
// }
|
||||
// }
|
||||
id, ch, err := c.MakeOfferAndSubscribe(0.1, 1, 0.05, types.EthAssetETH)
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, id, testSwapID)
|
||||
select {
|
||||
case status := <-ch:
|
||||
require.Equal(t, types.CompletedSuccess, status)
|
||||
case <-time.After(testTimeout):
|
||||
t.Fatal("test timed out")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubscribeTakeOffer(t *testing.T) {
|
||||
_ = newServer(t)
|
||||
s := newServer(t)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cliCtx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(func() {
|
||||
cancel()
|
||||
})
|
||||
c, err := wsclient.NewWsClient(ctx, defaultWSEndpoint())
|
||||
c, err := wsclient.NewWsClient(cliCtx, s.WsURL())
|
||||
require.NoError(t, err)
|
||||
|
||||
ch, err := c.TakeOfferAndSubscribe(testMultiaddr, testSwapID.String(), 1)
|
||||
@@ -235,7 +108,7 @@ func TestSubscribeTakeOffer(t *testing.T) {
|
||||
select {
|
||||
case status := <-ch:
|
||||
require.Equal(t, types.CompletedSuccess, status)
|
||||
case <-time.After(testTImeout):
|
||||
case <-time.After(testTimeout):
|
||||
t.Fatal("test timed out")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,9 +20,8 @@ type WsClient interface {
|
||||
Close()
|
||||
Discover(provides types.ProvidesCoin, searchTime uint64) ([][]string, error)
|
||||
Query(maddr string) (*rpctypes.QueryPeerResponse, error)
|
||||
SubscribeSwapStatus(id uint64) (<-chan types.Status, error)
|
||||
TakeOfferAndSubscribe(multiaddr, offerID string,
|
||||
providesAmount float64) (id uint64, ch <-chan types.Status, err error)
|
||||
SubscribeSwapStatus(id types.Hash) (<-chan types.Status, error)
|
||||
TakeOfferAndSubscribe(multiaddr, offerID string, providesAmount float64) (ch <-chan types.Status, err error)
|
||||
MakeOfferAndSubscribe(min, max float64,
|
||||
exchangeRate types.ExchangeRate, ethAsset types.EthAsset) (string, <-chan types.Status, error)
|
||||
}
|
||||
@@ -37,7 +36,7 @@ type wsClient struct {
|
||||
func NewWsClient(ctx context.Context, endpoint string) (*wsClient, error) { ///nolint:revive
|
||||
conn, resp, err := websocket.DefaultDialer.DialContext(ctx, endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to dial endpoint: %w", err)
|
||||
return nil, fmt.Errorf("failed to dial WS endpoint: %w", err)
|
||||
}
|
||||
|
||||
if err = resp.Body.Close(); err != nil {
|
||||
|
||||
@@ -48,7 +48,6 @@ start-swapd charlie \
|
||||
--ethereum-privkey "${CHARLIE_ETH_KEY}" \
|
||||
--libp2p-port 9955 \
|
||||
--rpc-port 5003 \
|
||||
--ws-port 8083 \
|
||||
--bootnodes /ip4/127.0.0.1/tcp/9933/p2p/12D3KooWAYn1T8Lu122Pav4zAogjpeU61usLTNZpLRNh9gCqY6X2 \
|
||||
--deploy
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[
|
||||
"ws://localhost:8081",
|
||||
"ws://localhost:8082",
|
||||
"ws://localhost:8080"
|
||||
]
|
||||
"ws://localhost:5001/ws",
|
||||
"ws://localhost:5002/ws",
|
||||
"ws://localhost:5000/ws"
|
||||
]
|
||||
|
||||
@@ -28,10 +28,10 @@ const (
|
||||
falseStr = "false"
|
||||
|
||||
defaultXMRTakerDaemonEndpoint = "http://localhost:5001"
|
||||
defaultXMRTakerDaemonWSEndpoint = "ws://localhost:8081"
|
||||
defaultXMRTakerDaemonWSEndpoint = "ws://localhost:5001/ws"
|
||||
defaultXMRMakerDaemonEndpoint = "http://localhost:5002"
|
||||
defaultXMRMakerDaemonWSEndpoint = "ws://localhost:8082"
|
||||
defaultCharlieDaemonWSEndpoint = "ws://localhost:8083"
|
||||
defaultXMRMakerDaemonWSEndpoint = "ws://localhost:5002/ws"
|
||||
defaultCharlieDaemonWSEndpoint = "ws://localhost:5003/ws"
|
||||
|
||||
defaultDiscoverTimeout = 2 // 2 seconds
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Atomic swap UI
|
||||
|
||||
This is a draft UI to interract with the atomic swap nodes
|
||||
This is a draft UI to interact with the atomic swap nodes
|
||||
|
||||
## Get started
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
import HelperText from '@smui/textfield/helper-text'
|
||||
import { currentAccount, sign } from '../stores/metamask'
|
||||
|
||||
const WS_ADDRESS = 'ws://127.0.0.1:8081'
|
||||
const WS_ADDRESS = 'ws://127.0.0.1:5001/ws'
|
||||
|
||||
let amountProvided: number | null = null
|
||||
let xmrAddress = ''
|
||||
|
||||
Reference in New Issue
Block a user