From 5698e252395559dbdfaa38207eb6eeb1cceaa203 Mon Sep 17 00:00:00 2001 From: Dmitry Holodov Date: Sat, 10 Sep 2022 14:54:43 -0500 Subject: [PATCH] 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. --- cmd/client/main.go | 163 +++++++++--------- cmd/daemon/main.go | 51 ++---- cmd/daemon/main_test.go | 28 +++- cmd/recover/main.go | 8 +- common/coins.go | 7 + common/coins_test.go | 4 +- docs/local.md | 8 +- docs/rpc.md | 8 +- docs/stagenet.md | 8 +- monero/wallet_client_test.go | 12 +- net/discovery.go | 13 ++ protocol/xmrmaker/errors.go | 14 +- rpc/mocks_test.go | 208 +++++++++++++++++++++++ rpc/net.go | 3 +- rpc/server.go | 100 +++++------ rpc/ws_test.go | 211 +++++------------------- rpcclient/wsclient/wsclient.go | 7 +- scripts/run-integration-tests.sh | 1 - testerconfig.json | 8 +- tests/integration_test.go | 6 +- ui/README.md | 2 +- ui/src/components/TakeDealDialog.svelte | 2 +- 22 files changed, 484 insertions(+), 388 deletions(-) create mode 100644 rpc/mocks_test.go diff --git a/cmd/client/main.go b/cmd/client/main.go index 8d43e274..2c433931 100644 --- a/cmd/client/main.go +++ b/cmd/client/main.go @@ -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 diff --git a/cmd/daemon/main.go b/cmd/daemon/main.go index ef3fea8c..50a6bb79 100644 --- a/cmd/daemon/main.go +++ b/cmd/daemon/main.go @@ -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 { diff --git a/cmd/daemon/main_test.go b/cmd/daemon/main_test.go index 011eba2b..5918f3ee 100644 --- a/cmd/daemon/main_test.go +++ b/cmd/daemon/main_test.go @@ -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) { diff --git a/cmd/recover/main.go b/cmd/recover/main.go index 8e162e2b..0e98c8df 100644 --- a/cmd/recover/main.go +++ b/cmd/recover/main.go @@ -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, diff --git a/common/coins.go b/common/coins.go index 291b4c40..a60d2e81 100644 --- a/common/coins.go +++ b/common/coins.go @@ -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) +} diff --git a/common/coins_test.go b/common/coins_test.go index 49f9f00f..9142e0d5 100644 --- a/common/coins_test.go +++ b/common/coins_test.go @@ -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))) } diff --git a/docs/local.md b/docs/local.md index 659f7065..376901ac 100644 --- a/docs/local.md +++ b/docs/local.md @@ -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. diff --git a/docs/rpc.md b/docs/rpc.md index a5b9290e..2faf0371 100644 --- a/docs/rpc.md +++ b/docs/rpc.md @@ -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} -``` \ No newline at end of file +``` diff --git a/docs/stagenet.md b/docs/stagenet.md index 278998a1..358482f3 100644 --- a/docs/stagenet.md +++ b/docs/stagenet.md @@ -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 ``` diff --git a/monero/wallet_client_test.go b/monero/wallet_client_test.go index 2b3123d4..a4c0773f 100644 --- a/monero/wallet_client_test.go +++ b/monero/wallet_client_test.go @@ -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()) diff --git a/net/discovery.go b/net/discovery.go index 9f8c56c4..58796bff 100644 --- a/net/discovery.go +++ b/net/discovery.go @@ -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 diff --git a/protocol/xmrmaker/errors.go b/protocol/xmrmaker/errors.go index a05d79d1..3637f506 100644 --- a/protocol/xmrmaker/errors.go +++ b/protocol/xmrmaker/errors.go @@ -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), ) } diff --git a/rpc/mocks_test.go b/rpc/mocks_test.go new file mode 100644 index 00000000..4dd7856f --- /dev/null +++ b/rpc/mocks_test.go @@ -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") +} diff --git a/rpc/net.go b/rpc/net.go index 84ed36d7..dfe9f92a 100644 --- a/rpc/net.go +++ b/rpc/net.go @@ -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" ) diff --git a/rpc/server.go b/rpc/server.go index 5db0c656..e06c89ba 100644 --- a/rpc/server.go +++ b/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. diff --git a/rpc/ws_test.go b/rpc/ws_test.go index 1f778608..c10c96c6 100644 --- a/rpc/ws_test.go +++ b/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") } } diff --git a/rpcclient/wsclient/wsclient.go b/rpcclient/wsclient/wsclient.go index 2ba7e519..ebe7bcb4 100644 --- a/rpcclient/wsclient/wsclient.go +++ b/rpcclient/wsclient/wsclient.go @@ -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 { diff --git a/scripts/run-integration-tests.sh b/scripts/run-integration-tests.sh index 091a1342..2f9f6a1d 100755 --- a/scripts/run-integration-tests.sh +++ b/scripts/run-integration-tests.sh @@ -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 diff --git a/testerconfig.json b/testerconfig.json index fed6da6f..3bd44667 100644 --- a/testerconfig.json +++ b/testerconfig.json @@ -1,5 +1,5 @@ [ - "ws://localhost:8081", - "ws://localhost:8082", - "ws://localhost:8080" -] \ No newline at end of file + "ws://localhost:5001/ws", + "ws://localhost:5002/ws", + "ws://localhost:5000/ws" +] diff --git a/tests/integration_test.go b/tests/integration_test.go index 218f4877..78eb1c1d 100644 --- a/tests/integration_test.go +++ b/tests/integration_test.go @@ -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 diff --git a/ui/README.md b/ui/README.md index af0e9506..82725304 100644 --- a/ui/README.md +++ b/ui/README.md @@ -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 diff --git a/ui/src/components/TakeDealDialog.svelte b/ui/src/components/TakeDealDialog.svelte index 80aa1d28..24e2ad75 100644 --- a/ui/src/components/TakeDealDialog.svelte +++ b/ui/src/components/TakeDealDialog.svelte @@ -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 = ''