CLI cleanups (#185)

* Switches to current v2 version of urfave CLI
* Fixes incorrect port values in the help
* Provides defaults directly to urfave for clear `--help` messaging
* `--base-path` flag renamed to `--data-dir`. This matches `monerod`'s' `--data-dir` and almost matches the `geth` flag `--datadir`
* Bootnodes and contract address are provided for stagenet testing
* Bootnodes can be provided individually by repeating the `--bootnodes`/`--bn` flag, or as a comma separated block (original behavior), or even a combination of both. (Makes adding/removing bootnodes a lot easier.)
* More precise error messaging when XMR maker/taker values are off
* Fixes bug in the peer finder where XMR was hardcoded as the provides coin
* `cleanup-test-processes.sh` displays which processes are being killed
This commit is contained in:
Dmitry Holodov
2022-09-07 19:45:01 -05:00
committed by GitHub
parent 9e65c5890d
commit 476bc69b82
42 changed files with 717 additions and 538 deletions

View File

@@ -45,7 +45,7 @@ test-integration: init
.PHONY: install
install: init
cd cmd/ && go install
cd cmd/ && go install ./...
.PHONY: build
build: init

View File

@@ -5,10 +5,9 @@ import (
)
var (
errNoMultiaddr = errors.New("must provide peer's multiaddress with --multiaddr")
errNoMinAmount = errors.New("must provide non-zero --min-amount")
errNoMaxAmount = errors.New("must provide non-zero --max-amount")
errNoExchangeRate = errors.New("must provide non-zero --exchange-rate")
errNoOfferID = errors.New("must provide --offer-id")
errNoProvidesAmount = errors.New("must provide --provides-amount")
errNoProvidesAmount = errors.New("must provide non-zero --provides-amount")
errNoDuration = errors.New("must provide non-zero --duration")
)

View File

@@ -6,30 +6,28 @@ import (
"os"
"strings"
"github.com/ethereum/go-ethereum/common"
"github.com/urfave/cli/v2"
"github.com/athanorlabs/atomic-swap/common/types"
"github.com/athanorlabs/atomic-swap/rpcclient"
"github.com/athanorlabs/atomic-swap/rpcclient/wsclient"
"github.com/ethereum/go-ethereum/common"
logging "github.com/ipfs/go-log"
"github.com/urfave/cli"
)
const (
defaultSwapdAddress = "http://localhost:5001"
defaultSwapdAddress = "http://127.0.0.1:5001"
defaultDiscoverSearchTimeSecs = 12
)
var log = logging.Logger("cmd")
var (
app = &cli.App{
Name: "swapcli",
Usage: "Client for swapd",
Commands: []cli.Command{
Commands: []*cli.Command{
{
Name: "addresses",
Aliases: []string{"a"},
Usage: "list our daemon's libp2p listening addresses",
Usage: "List our daemon's libp2p listening addresses",
Action: runAddresses,
Flags: []cli.Flag{
daemonAddrFlag,
@@ -38,16 +36,19 @@ var (
{
Name: "discover",
Aliases: []string{"d"},
Usage: "discover peers who provide a certain coin",
Usage: "Discover peers who provide a certain coin",
Action: runDiscover,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "provides",
Usage: "coin to find providers for: one of [ETH, XMR]",
Name: "provides",
Usage: fmt.Sprintf("Coin to find providers for: one of [%s, %s]",
types.ProvidesXMR, types.ProvidesETH),
Value: string(types.ProvidesXMR),
},
&cli.UintFlag{
Name: "search-time",
Usage: "duration of time to search for, in seconds",
Usage: "Duration of time to search for, in seconds",
Value: defaultDiscoverSearchTimeSecs,
},
daemonAddrFlag,
},
@@ -55,12 +56,13 @@ var (
{
Name: "query",
Aliases: []string{"q"},
Usage: "query a peer for details on what they provide",
Usage: "Query a peer for details on what they provide",
Action: runQuery,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "multiaddr",
Usage: "peer's multiaddress, as provided by discover",
Name: "multiaddr",
Usage: "Peer's multiaddress, as provided by discover",
Required: true,
},
daemonAddrFlag,
},
@@ -72,12 +74,15 @@ var (
Action: runQueryAll,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "provides",
Usage: "coin to find providers for: one of [ETH, XMR]",
Name: "provides",
Usage: fmt.Sprintf("Coin to find providers for: one of [%s, %s]",
types.ProvidesXMR, types.ProvidesETH),
Value: string(types.ProvidesXMR),
},
&cli.UintFlag{
Name: "search-time",
Usage: "duration of time to search for, in seconds",
Usage: "Duration of time to search for, in seconds",
Value: defaultDiscoverSearchTimeSecs,
},
daemonAddrFlag,
},
@@ -85,24 +90,27 @@ var (
{
Name: "make",
Aliases: []string{"m"},
Usage: "mke a swap offer; currently monero holders must be the makers",
Usage: "Make a swap offer; currently monero holders must be the makers",
Action: runMake,
Flags: []cli.Flag{
&cli.Float64Flag{
Name: "min-amount",
Usage: "minimum amount to be swapped, in XMR",
Name: "min-amount",
Usage: "Minimum amount to be swapped, in XMR",
Required: true,
},
&cli.Float64Flag{
Name: "max-amount",
Usage: "maximum amount to be swapped, in XMR",
Name: "max-amount",
Usage: "Maximum amount to be swapped, in XMR",
Required: true,
},
&cli.Float64Flag{
Name: "exchange-rate",
Usage: "desired exchange rate of XMR:ETH, eg. --exchange-rate=0.1 means 10XMR = 1ETH",
Name: "exchange-rate",
Usage: "Desired exchange rate of XMR:ETH, eg. --exchange-rate=0.1 means 10XMR = 1ETH",
Required: true,
},
&cli.BoolFlag{
Name: "subscribe",
Usage: "subscribe to push notifications about the swap's status",
Usage: "Subscribe to push notifications about the swap's status",
},
&cli.StringFlag{
Name: "eth-asset",
@@ -114,73 +122,79 @@ var (
{
Name: "take",
Aliases: []string{"t"},
Usage: "initiate a swap by taking an offer; currently only eth holders can be the takers",
Usage: "Initiate a swap by taking an offer; currently only eth holders can be the takers",
Action: runTake,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "multiaddr",
Usage: "peer's multiaddress, as provided by discover",
Name: "multiaddr",
Usage: "Peer's multiaddress, as provided by discover",
Required: true,
},
&cli.StringFlag{
Name: "offer-id",
Usage: "ID of the offer being taken",
Name: "offer-id",
Usage: "ID of the offer being taken",
Required: true,
},
&cli.Float64Flag{
Name: "provides-amount",
Usage: "amount of coin to send in the swap",
Name: "provides-amount",
Usage: "Amount of coin to send in the swap",
Required: true,
},
&cli.BoolFlag{
Name: "subscribe",
Usage: "subscribe to push notifications about the swap's status",
Usage: "Subscribe to push notifications about the swap's status",
},
daemonAddrFlag,
},
},
{
Name: "get-past-swap-ids",
Usage: "get past swap IDs",
Usage: "Get past swap IDs",
Action: runGetPastSwapIDs,
Flags: []cli.Flag{daemonAddrFlag},
},
{
Name: "get-ongoing-swap",
Usage: "get information about ongoing swap, if there is one",
Usage: "Get information about ongoing swap, if there is one",
Action: runGetOngoingSwap,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "offer-id",
Usage: "ID of swap to retrieve info for",
Name: "offer-id",
Usage: "ID of swap to retrieve info for",
Required: true,
},
daemonAddrFlag,
},
},
{
Name: "get-past-swap",
Usage: "get information about a past swap with the given ID",
Usage: "Get information about a past swap with the given ID",
Action: runGetPastSwap,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "offer-id",
Usage: "ID of swap to retrieve info for",
Name: "offer-id",
Usage: "ID of swap to retrieve info for",
Required: true,
},
daemonAddrFlag,
},
},
{
Name: "refund",
Usage: "if we are the ETH provider for an ongoing swap, refund it if possible.",
Usage: "If we are the ETH provider for an ongoing swap, refund it if possible.",
Action: runRefund,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "offer-id",
Usage: "ID of swap to retrieve info for",
Name: "offer-id",
Usage: "ID of swap to retrieve info for",
Required: true,
},
daemonAddrFlag,
},
},
{
Name: "cancel",
Usage: "cancel a ongoing swap if possible.",
Usage: "Cancel a ongoing swap if possible.",
Action: runCancel,
Flags: []cli.Flag{
&cli.StringFlag{
@@ -192,36 +206,38 @@ var (
},
{
Name: "clear-offers",
Usage: "clear current offers. if no offer IDs are provided, clears all current offers.",
Usage: "Clear current offers. If no offer IDs are provided, clears all current offers.",
Action: runClearOffers,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "offer-ids",
Usage: "a comma-separated list of offer IDs to delete",
Usage: "A comma-separated list of offer IDs to delete",
},
daemonAddrFlag,
},
},
{
Name: "get-stage",
Usage: "get the stage of a current swap.",
Usage: "Get the stage of a current swap.",
Action: runGetStage,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "offer-id",
Usage: "ID of swap to retrieve info for",
Name: "offer-id",
Usage: "ID of swap to retrieve info for",
Required: true,
},
daemonAddrFlag,
},
},
{
Name: "set-swap-timeout",
Usage: "set the duration between swap initiation and t0 and t0 and t1, in seconds",
Usage: "Set the duration between swap initiation and t0 and t0 and t1, in seconds",
Action: runSetSwapTimeout,
Flags: []cli.Flag{
&cli.UintFlag{
Name: "duration",
Usage: "duration of timeout, in seconds",
Name: "duration",
Usage: "Duration of timeout, in seconds",
Required: true,
},
daemonAddrFlag,
},
@@ -232,13 +248,14 @@ var (
daemonAddrFlag = &cli.StringFlag{
Name: "daemon-addr",
Usage: "address of swap daemon; default http://localhost:5001",
Usage: "Address of swap daemon",
Value: defaultSwapdAddress,
}
)
func main() {
if err := app.Run(os.Args); err != nil {
log.Error(err)
_, _ = fmt.Fprintf(os.Stderr, "%s\n", err)
os.Exit(1)
}
}
@@ -265,15 +282,7 @@ func runDiscover(ctx *cli.Context) error {
return err
}
if provides == "" {
provides = types.ProvidesXMR
}
endpoint := ctx.String("daemon-addr")
if endpoint == "" {
endpoint = defaultSwapdAddress
}
searchTime := ctx.Uint("search-time")
c := rpcclient.NewClient(endpoint)
@@ -291,14 +300,7 @@ func runDiscover(ctx *cli.Context) error {
func runQuery(ctx *cli.Context) error {
maddr := ctx.String("multiaddr")
if maddr == "" {
return errNoMultiaddr
}
endpoint := ctx.String("daemon-addr")
if endpoint == "" {
endpoint = defaultSwapdAddress
}
c := rpcclient.NewClient(endpoint)
res, err := c.Query(maddr)
@@ -318,10 +320,6 @@ func runQueryAll(ctx *cli.Context) error {
return err
}
if provides == "" {
provides = types.ProvidesXMR
}
endpoint := ctx.String("daemon-addr")
if endpoint == "" {
endpoint = defaultSwapdAddress
@@ -364,9 +362,6 @@ func runMake(ctx *cli.Context) error {
}
endpoint := ctx.String("daemon-addr")
if endpoint == "" {
endpoint = defaultSwapdAddress
}
ethAssetStr := ctx.String("eth-asset")
ethAsset := types.EthAssetETH
@@ -404,29 +399,23 @@ func runMake(ctx *cli.Context) error {
}
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)
return nil
}
func runTake(ctx *cli.Context) error {
maddr := ctx.String("multiaddr")
if maddr == "" {
return errNoMultiaddr
}
offerID := ctx.String("offer-id")
if offerID == "" {
return errNoOfferID
}
providesAmount := ctx.Float64("provides-amount")
if providesAmount == 0 {
return errNoProvidesAmount
}
endpoint := ctx.String("daemon-addr")
if endpoint == "" {
endpoint = defaultSwapdAddress
}
if ctx.Bool("subscribe") {
c, err := wsclient.NewWsClient(context.Background(), endpoint)
@@ -479,14 +468,7 @@ func runGetPastSwapIDs(ctx *cli.Context) error {
func runGetOngoingSwap(ctx *cli.Context) error {
endpoint := ctx.String("daemon-addr")
if endpoint == "" {
endpoint = defaultSwapdAddress
}
offerID := ctx.String("offer-id")
if offerID == "" {
return errNoOfferID
}
c := rpcclient.NewClient(endpoint)
info, err := c.GetOngoingSwap(offerID)
@@ -506,14 +488,7 @@ func runGetOngoingSwap(ctx *cli.Context) error {
func runGetPastSwap(ctx *cli.Context) error {
endpoint := ctx.String("daemon-addr")
if endpoint == "" {
endpoint = defaultSwapdAddress
}
offerID := ctx.String("offer-id")
if offerID == "" {
return errNoOfferID
}
c := rpcclient.NewClient(endpoint)
info, err := c.GetPastSwap(offerID)
@@ -533,14 +508,7 @@ func runGetPastSwap(ctx *cli.Context) error {
func runRefund(ctx *cli.Context) error {
endpoint := ctx.String("daemon-addr")
if endpoint == "" {
endpoint = defaultSwapdAddress
}
offerID := ctx.String("offer-id")
if offerID == "" {
return errNoOfferID
}
c := rpcclient.NewClient(endpoint)
resp, err := c.Refund(offerID)
@@ -554,14 +522,7 @@ func runRefund(ctx *cli.Context) error {
func runCancel(ctx *cli.Context) error {
endpoint := ctx.String("daemon-addr")
if endpoint == "" {
endpoint = defaultSwapdAddress
}
offerID := ctx.String("offer-id")
if offerID == "" {
return errNoOfferID
}
c := rpcclient.NewClient(endpoint)
resp, err := c.Cancel(offerID)
@@ -575,10 +536,6 @@ func runCancel(ctx *cli.Context) error {
func runClearOffers(ctx *cli.Context) error {
endpoint := ctx.String("daemon-addr")
if endpoint == "" {
endpoint = defaultSwapdAddress
}
c := rpcclient.NewClient(endpoint)
ids := ctx.String("offer-ids")
@@ -603,14 +560,7 @@ func runClearOffers(ctx *cli.Context) error {
func runGetStage(ctx *cli.Context) error {
endpoint := ctx.String("daemon-addr")
if endpoint == "" {
endpoint = defaultSwapdAddress
}
offerID := ctx.String("offer-id")
if offerID == "" {
return errNoOfferID
}
c := rpcclient.NewClient(endpoint)
resp, err := c.GetStage(offerID)
@@ -624,11 +574,10 @@ func runGetStage(ctx *cli.Context) error {
func runSetSwapTimeout(ctx *cli.Context) error {
duration := ctx.Uint("duration")
endpoint := ctx.String("daemon-addr")
if endpoint == "" {
endpoint = defaultSwapdAddress
if duration == 0 {
return errNoDuration
}
endpoint := ctx.String("daemon-addr")
c := rpcclient.NewClient(endpoint)
err := c.SetSwapTimeout(uint64(duration))
@@ -636,6 +585,6 @@ func runSetSwapTimeout(ctx *cli.Context) error {
return err
}
fmt.Printf("Set timeout duration to %ds", duration)
fmt.Printf("Set timeout duration to %ds\n", duration)
return nil
}

View File

@@ -24,9 +24,15 @@ var (
errInvalidSwapContract = errors.New("given contract address does not contain correct code")
)
func getOrDeploySwapFactory(ctx context.Context, address ethcommon.Address, env common.Environment,
basePath string, chainID *big.Int, privkey *ecdsa.PrivateKey,
ec *ethclient.Client) (*swapfactory.SwapFactory, ethcommon.Address, error) {
func getOrDeploySwapFactory(
ctx context.Context,
address ethcommon.Address,
env common.Environment,
dataDir string,
chainID *big.Int,
privkey *ecdsa.PrivateKey,
ec *ethclient.Client,
) (*swapfactory.SwapFactory, ethcommon.Address, error) {
var (
sf *swapfactory.SwapFactory
)
@@ -51,7 +57,7 @@ func getOrDeploySwapFactory(ctx context.Context, address ethcommon.Address, env
log.Infof("deployed SwapFactory.sol: address=%s tx hash=%s", address, tx.Hash())
// store the contract address on disk
fp := path.Join(basePath, "contractaddress")
fp := path.Join(dataDir, "contractaddress")
if err = pcommon.WriteContractAddressToFile(fp, address.String()); err != nil {
return nil, ethcommon.Address{}, fmt.Errorf("failed to write contract address to file: %w", err)
}

View File

@@ -3,16 +3,15 @@ package main
import (
"context"
"crypto/ecdsa"
"errors"
"fmt"
"math/big"
"os"
"path"
"strings"
ethcommon "github.com/ethereum/go-ethereum/common"
ethcrypto "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/urfave/cli"
"github.com/urfave/cli/v2"
"github.com/athanorlabs/atomic-swap/cmd/utils"
"github.com/athanorlabs/atomic-swap/common"
@@ -50,15 +49,18 @@ const (
var (
log = logging.Logger("cmd")
// default dev basepaths
defaultXMRMakerBasepath = os.TempDir() + "/xmrmaker"
defaultXMRTakerBasepath = os.TempDir() + "/xmrtaker"
// Default dev base paths. If SWAP_TEST_DATA_DIR is not defined, it is
// still safe, there just won't be an intermediate directory and tests
// could fail from stale data.
testDataDir = os.Getenv("SWAP_TEST_DATA_DIR")
defaultXMRMakerDataDir = path.Join(os.TempDir(), testDataDir, "xmrmaker")
defaultXMRTakerDataDir = path.Join(os.TempDir(), testDataDir, "xmrtaker")
)
const (
flagRPCPort = "rpc-port"
flagWSPort = "ws-port"
flagBasepath = "basepath"
flagDataDir = "data-dir"
flagLibp2pKey = "libp2p-key"
flagLibp2pPort = "libp2p-port"
flagBootnodes = "bootnodes"
@@ -92,35 +94,41 @@ var (
Flags: []cli.Flag{
&cli.UintFlag{
Name: flagRPCPort,
Usage: "port for the daemon RPC server to run on; default 5001",
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; default 8080",
Usage: "Port for the daemon RPC websockets server to run on",
Value: defaultWSPort,
},
&cli.StringFlag{
Name: flagBasepath,
Usage: "path to store swap artefacts",
Name: flagDataDir,
Usage: "Path to store swap artifacts", //nolint:misspell
Value: "{HOME}/.atomicswap/{ENV}", // For --help only, actual default replaces variables
},
&cli.StringFlag{
Name: flagLibp2pKey,
Usage: "libp2p private key",
Value: defaultLibp2pKey,
},
&cli.UintFlag{
Name: flagLibp2pPort,
Usage: "libp2p port to listen on",
Value: defaultLibp2pPort,
},
&cli.StringFlag{
Name: flagWalletFile,
Usage: "filename of wallet file containing XMR to be swapped; required if running as XMR provider",
Usage: "Filename of wallet file containing XMR to be swapped; required if running as XMR provider",
},
&cli.StringFlag{
Name: flagWalletPassword,
Usage: "password of wallet file containing XMR to be swapped",
Usage: "Password of wallet file containing XMR to be swapped",
},
&cli.StringFlag{
Name: flagEnv,
Usage: "environment to use: one of mainnet, stagenet, or dev",
Usage: "Environment to use: one of mainnet, stagenet, or dev",
Value: "dev",
},
&cli.StringFlag{
Name: flagMoneroWalletEndpoint,
@@ -132,55 +140,57 @@ var (
},
&cli.StringFlag{
Name: flagEthereumEndpoint,
Usage: "ethereum client endpoint",
Usage: "Ethereum client endpoint",
},
&cli.StringFlag{
Name: flagEthereumPrivKey,
Usage: "file containing a private key hex string",
Usage: "File containing a private key as hex string",
},
&cli.UintFlag{
Name: flagEthereumChainID,
Usage: "ethereum chain ID; eg. mainnet=1, ropsten=3, rinkeby=4, goerli=5, ganache=1337",
Usage: "Ethereum chain ID; eg. mainnet=1, goerli=5, ganache=1337",
},
&cli.StringFlag{
Name: flagContractAddress,
Usage: "address of instance of SwapFactory.sol already deployed on-chain; required if running on mainnet",
Usage: "Address of instance of SwapFactory.sol already deployed on-chain; required if running on mainnet",
},
&cli.StringFlag{
Name: flagBootnodes,
Usage: "comma-separated string of libp2p bootnodes",
&cli.StringSliceFlag{
Name: flagBootnodes,
Aliases: []string{"bn"},
Usage: "libp2p bootnode, comma separated if passing multiple to a single flag",
},
&cli.UintFlag{
Name: flagGasPrice,
Usage: "ethereum gas price to use for transactions (in gwei). if not set, the gas price is set via oracle.",
Usage: "Ethereum gas price to use for transactions (in gwei). If not set, the gas price is set via oracle.",
},
&cli.UintFlag{
Name: flagGasLimit,
Usage: "ethereum gas limit to use for transactions. if not set, the gas limit is estimated for each transaction.",
Usage: "Ethereum gas limit to use for transactions. If not set, the gas limit is estimated for each transaction.",
},
&cli.BoolFlag{
Name: flagDevXMRTaker,
Usage: "run in development mode and use ETH provider default values",
Usage: "Run in development mode and use ETH provider default values",
},
&cli.BoolFlag{
Name: flagDevXMRMaker,
Usage: "run in development mode and use XMR provider default values",
Usage: "Run in development mode and use XMR provider default values",
},
&cli.BoolFlag{
Name: flagDeploy,
Usage: "deploy an instance of the swap contract; defaults to false",
Usage: "Deploy an instance of the swap contract",
},
&cli.BoolFlag{
Name: flagTransferBack,
Usage: "when receiving XMR in a swap, transfer it back to the original wallet.",
Usage: "When receiving XMR in a swap, transfer it back to the original wallet.",
},
&cli.StringFlag{
Name: flagLog,
Usage: "set log level: one of [error|warn|info|debug]",
Usage: "Set log level: one of [error|warn|info|debug]",
Value: "info",
},
&cli.BoolFlag{
Name: flagUseExternalSigner,
Usage: "use external signer, for usage with the swap UI",
Usage: "Use external signer, for usage with the swap UI",
},
},
}
@@ -216,14 +226,10 @@ func setLogLevels(c *cli.Context) error {
)
level := c.String(flagLog)
if level == "" {
level = levelInfo
}
switch level {
case levelError, levelWarn, levelInfo, levelDebug:
default:
return errors.New("invalid log level")
return fmt.Errorf("invalid log level %q", level)
}
_ = logging.SetLogLevel("xmrtaker", level)
@@ -258,86 +264,94 @@ func runDaemon(c *cli.Context) error {
return nil
}
func (d *daemon) make(c *cli.Context) error {
env, cfg, err := utils.GetEnvironment(c)
if err != nil {
return err
}
devXMRTaker := c.Bool(flagDevXMRTaker)
devXMRMaker := c.Bool(flagDevXMRMaker)
chainID := int64(c.Uint(flagEthereumChainID))
if chainID == 0 {
chainID = cfg.EthereumChainID
}
var bootnodes []string
if c.String(flagBootnodes) != "" {
bootnodes = strings.Split(c.String(flagBootnodes), ",")
}
k := c.String(flagLibp2pKey)
p := uint16(c.Uint(flagLibp2pPort))
var (
libp2pKey string
libp2pPort uint16
rpcPort uint16
)
switch {
case k != "":
libp2pKey = k
case devXMRTaker:
libp2pKey = defaultXMRTakerLibp2pKey
case devXMRMaker:
libp2pKey = defaultXMRMakerLibp2pKey
default:
libp2pKey = defaultLibp2pKey
}
switch {
case p != 0:
libp2pPort = p
case devXMRTaker:
libp2pPort = defaultXMRTakerLibp2pPort
case devXMRMaker:
libp2pPort = defaultXMRMakerLibp2pPort
default:
libp2pPort = defaultLibp2pPort
}
// basepath is already set in default case
basepath := c.String(flagBasepath)
switch {
case basepath != "":
cfg.Basepath = basepath
case devXMRTaker:
cfg.Basepath = defaultXMRTakerBasepath
case devXMRMaker:
cfg.Basepath = defaultXMRMakerBasepath
}
exists, err := common.Exists(cfg.Basepath)
if err != nil {
return err
}
if !exists {
err = common.MakeDir(cfg.Basepath)
if err != nil {
return err
// expandBootnodes expands the boot nodes passed on the command line that
// can be specified individually with multiple flags, but can also contain
// multiple boot nodes passed to single flag separated by commas.
func expandBootnodes(nodesCLI []string) []string {
var nodes []string
for _, n := range nodesCLI {
splitNodes := strings.Split(n, ",")
for _, ns := range splitNodes {
nodes = append(nodes, strings.TrimSpace(ns))
}
}
return nodes
}
func (d *daemon) make(c *cli.Context) error {
env, cfg, err := utils.GetEnvironment(c.String(flagEnv))
if err != nil {
return err
}
devXMRMaker := c.Bool(flagDevXMRMaker)
devXMRTaker := c.Bool(flagDevXMRTaker)
if devXMRMaker && devXMRTaker {
return errFlagsMutuallyExclusive(flagDevXMRMaker, flagDevXMRTaker)
}
// By default, the chain ID is derived from the `flagEnv` value, but it can be overridden if
// `flagEthereumChainID` is passed:
if c.Uint(flagEthereumChainID) != 0 {
cfg.EthereumChainID = int64(c.Uint(flagEthereumChainID))
}
if len(c.StringSlice(flagBootnodes)) > 0 {
cfg.Bootnodes = expandBootnodes(c.StringSlice(flagBootnodes))
}
//
// Note: Overrides for devXMRTaker/devXMRMaker use "IsSet" instead of checking the value so that
// the devXMRTaker/devXMRMaker configurations take precedence over normal default values,
// but will not override values explicitly set by the end user.
//
libp2pKey := c.String(flagLibp2pKey)
if !c.IsSet(flagLibp2pKey) {
switch {
case devXMRTaker:
libp2pKey = defaultXMRTakerLibp2pKey
case devXMRMaker:
libp2pKey = defaultXMRMakerLibp2pKey
}
}
libp2pPort := uint16(c.Uint(flagLibp2pPort))
if !c.IsSet(flagLibp2pPort) {
switch {
case devXMRTaker:
libp2pPort = defaultXMRTakerLibp2pPort
case devXMRMaker:
libp2pPort = defaultXMRMakerLibp2pPort
}
}
// cfg.DataDir was already defaulted from the `flagEnv` value and `flagDataDir` does
// not directly set a default value.
if c.IsSet(flagDataDir) {
cfg.DataDir = c.String(flagDataDir) // override the value derived from `flagEnv`
} else {
// Override in dev scenarios if the value was not explicitly set
switch {
case devXMRTaker:
cfg.DataDir = defaultXMRTakerDataDir
case devXMRMaker:
cfg.DataDir = defaultXMRMakerDataDir
}
}
if err = common.MakeDir(cfg.DataDir); err != nil {
return err
}
netCfg := &net.Config{
Ctx: d.ctx,
Environment: env,
Basepath: cfg.Basepath,
ChainID: chainID,
DataDir: cfg.DataDir,
EthChainID: cfg.EthereumChainID,
Port: libp2pPort,
KeyFile: libp2pKey,
Bootnodes: bootnodes,
Bootnodes: cfg.Bootnodes,
}
host, err := net.NewHost(netCfg)
@@ -346,7 +360,7 @@ func (d *daemon) make(c *cli.Context) error {
}
sm := swap.NewManager()
backend, err := newBackend(d.ctx, c, env, cfg, chainID, devXMRMaker, sm, host)
backend, err := newBackend(d.ctx, c, env, cfg, devXMRMaker, devXMRTaker, sm, host)
if err != nil {
return err
}
@@ -364,27 +378,24 @@ func (d *daemon) make(c *cli.Context) error {
return err
}
p = uint16(c.Uint(flagRPCPort))
switch {
case p != 0:
rpcPort = p
case devXMRTaker:
rpcPort = defaultXMRTakerRPCPort
case devXMRMaker:
rpcPort = defaultXMRMakerRPCPort
default:
rpcPort = defaultRPCPort
rpcPort := uint16(c.Uint(flagRPCPort))
if !c.IsSet(flagRPCPort) {
switch {
case devXMRTaker:
rpcPort = defaultXMRTakerRPCPort
case devXMRMaker:
rpcPort = defaultXMRMakerRPCPort
}
}
wsPort := uint16(c.Uint(flagWSPort))
switch {
case wsPort != 0:
case devXMRTaker:
wsPort = defaultXMRTakerWSPort
case devXMRMaker:
wsPort = defaultXMRMakerWSPort
default:
wsPort = defaultWSPort
if !c.IsSet(flagWSPort) {
switch {
case devXMRTaker:
wsPort = defaultXMRTakerWSPort
case devXMRMaker:
wsPort = defaultXMRMakerWSPort
}
}
rpcCfg := &rpc.Config{
@@ -414,24 +425,46 @@ func (d *daemon) make(c *cli.Context) error {
}
}()
log.Infof("started swapd with basepath %s",
cfg.Basepath,
)
log.Infof("started swapd with data-dir %s", cfg.DataDir)
return nil
}
func newBackend(ctx context.Context, c *cli.Context, env common.Environment, cfg common.Config,
chainID int64, devXMRMaker bool, sm swap.Manager, net net.Host) (backend.Backend, error) {
func errFlagsMutuallyExclusive(flag1, flag2 string) error {
return fmt.Errorf("flags %q and %q are mutually exclusive", flag1, flag2)
}
func errFlagRequired(flag string) error {
return fmt.Errorf("required flag %q not specified", flag)
}
func newBackend(
ctx context.Context,
c *cli.Context,
env common.Environment,
cfg common.Config,
devXMRMaker bool,
devXMRTaker bool,
sm swap.Manager,
net net.Host,
) (backend.Backend, error) {
var (
moneroEndpoint, daemonEndpoint, ethEndpoint string
moneroEndpoint string
daemonEndpoint string
ethEndpoint string
ethPrivKey *ecdsa.PrivateKey
)
if c.String(flagMoneroWalletEndpoint) != "" {
switch {
// flagMoneroWalletEndpoint doesn't have a default, so we don't have to use c.IsSet when
// doing the devXMRMaker/devXMRTaker overrides. We'll also be eliminating this flag soon.
case c.String(flagMoneroWalletEndpoint) != "":
moneroEndpoint = c.String(flagMoneroWalletEndpoint)
} else if devXMRMaker {
case devXMRMaker:
moneroEndpoint = common.DefaultXMRMakerMoneroEndpoint
} else {
case devXMRTaker:
moneroEndpoint = common.DefaultXMRTakerMoneroEndpoint
default:
return nil, errFlagRequired(flagMoneroWalletEndpoint)
}
if c.String(flagEthereumEndpoint) != "" {
@@ -440,15 +473,15 @@ func newBackend(ctx context.Context, c *cli.Context, env common.Environment, cfg
ethEndpoint = common.DefaultEthEndpoint
}
ethPrivKey, err := utils.GetEthereumPrivateKey(c, env, devXMRMaker, c.Bool(flagUseExternalSigner))
if err != nil {
return nil, err
useExternalSigner := c.Bool(flagUseExternalSigner)
ethPrivKeyFile := c.String(flagEthereumPrivKey)
if useExternalSigner && ethPrivKeyFile != "" {
return nil, errFlagsMutuallyExclusive(flagUseExternalSigner, flagEthereumPrivKey)
}
var pk *ecdsa.PrivateKey
if ethPrivKey != "" {
pk, err = ethcrypto.HexToECDSA(ethPrivKey)
if err != nil {
if !useExternalSigner {
var err error
if ethPrivKey, err = utils.GetEthereumPrivateKey(ethPrivKeyFile, env, devXMRMaker, devXMRTaker); err != nil {
return nil, err
}
}
@@ -465,12 +498,11 @@ func newBackend(ctx context.Context, c *cli.Context, env common.Environment, cfg
gasPrice = big.NewInt(int64(c.Uint(flagGasPrice)))
}
var contractAddr ethcommon.Address
contractAddrStr := c.String(flagContractAddress)
if contractAddrStr == "" {
contractAddr = ethcommon.Address{}
} else {
contractAddr = ethcommon.HexToAddress(contractAddrStr)
if contractAddrStr != "" {
// We check the contract code at the address later, so we don't need
// to tightly validate the address here.
cfg.ContractAddress = ethcommon.HexToAddress(contractAddrStr)
}
ec, err := ethclient.Dial(ethEndpoint)
@@ -480,11 +512,16 @@ func newBackend(ctx context.Context, c *cli.Context, env common.Environment, cfg
deploy := c.Bool(flagDeploy)
if deploy {
contractAddr = ethcommon.Address{}
if c.IsSet(flagContractAddress) {
return nil, errFlagsMutuallyExclusive(flagDeploy, flagContractAddress)
}
// Zero out any default contract address in the config, so we deploy
cfg.ContractAddress = ethcommon.Address{}
}
contract, contractAddr, err := getOrDeploySwapFactory(ctx, contractAddr, env, cfg.Basepath,
big.NewInt(chainID), pk, ec)
chainID := big.NewInt(cfg.EthereumChainID)
contract, contractAddr, err :=
getOrDeploySwapFactory(ctx, cfg.ContractAddress, env, cfg.DataDir, chainID, ethPrivKey, ec)
if err != nil {
return nil, err
}
@@ -494,9 +531,9 @@ func newBackend(ctx context.Context, c *cli.Context, env common.Environment, cfg
MoneroWalletEndpoint: moneroEndpoint,
MoneroDaemonEndpoint: daemonEndpoint,
EthereumClient: ec,
EthereumPrivateKey: pk,
EthereumPrivateKey: ethPrivKey,
Environment: env,
ChainID: big.NewInt(chainID),
ChainID: chainID,
GasPrice: gasPrice,
GasLimit: uint64(c.Uint(flagGasLimit)),
SwapManager: sm,
@@ -527,7 +564,7 @@ func getProtocolInstances(c *cli.Context, cfg common.Config,
xmrtakerCfg := &xmrtaker.Config{
Backend: b,
Basepath: cfg.Basepath,
DataDir: cfg.DataDir,
MoneroWalletFile: walletFile,
MoneroWalletPassword: walletPassword,
TransferBack: c.Bool(flagTransferBack),
@@ -540,7 +577,7 @@ func getProtocolInstances(c *cli.Context, cfg common.Config,
xmrmakerCfg := &xmrmaker.Config{
Backend: b,
Basepath: cfg.Basepath,
DataDir: cfg.DataDir,
WalletFile: walletFile,
WalletPassword: walletPassword,
}

View File

@@ -4,69 +4,57 @@ import (
"context"
"flag"
"fmt"
"strconv"
"testing"
"github.com/stretchr/testify/require"
"github.com/urfave/cli"
"github.com/urfave/cli/v2"
)
func newTestContext(t *testing.T, description string, flags []string, values []interface{}) *cli.Context {
require.Equal(t, len(flags), len(values))
func newTestContext(t *testing.T, description string, flags map[string]any) *cli.Context {
set := flag.NewFlagSet(description, 0)
for i := range values {
switch v := values[i].(type) {
for flag, value := range flags {
switch v := value.(type) {
case bool:
set.Bool(flags[i], v, "")
set.Bool(flag, v, "")
case string:
set.String(flags[i], v, "")
set.String(flag, v, "")
case uint:
set.Uint(flags[i], v, "")
set.Uint(flag, v, "")
case int64:
set.Int64(flags[i], v, "")
set.Int64(flag, v, "")
case []string:
set.Var(&cli.StringSlice{}, flags[i], "")
set.Var(&cli.StringSlice{}, flag, "")
default:
t.Fatalf("unexpected cli value type: %T", values[i])
t.Fatalf("unexpected cli value type: %T", value)
}
}
ctx := cli.NewContext(app, set, nil)
var (
err error
i int
)
for i = range values {
switch v := values[i].(type) {
case bool:
err = ctx.Set(flags[i], strconv.FormatBool(v))
case string:
err = ctx.Set(flags[i], values[i].(string))
case uint:
err = ctx.Set(flags[i], strconv.Itoa(int(values[i].(uint))))
case int64:
err = ctx.Set(flags[i], strconv.Itoa(int(values[i].(int64))))
for flag, value := range flags {
switch v := value.(type) {
case bool, uint, int64, string:
require.NoError(t, ctx.Set(flag, fmt.Sprintf("%v", v)))
case []string:
for _, str := range values[i].([]string) {
err = ctx.Set(flags[i], str)
require.NoError(t, err)
for _, str := range v {
require.NoError(t, ctx.Set(flag, str))
}
default:
t.Fatalf("unexpected cli value type: %T", values[i])
t.Fatalf("unexpected cli value type: %T", value)
}
}
require.NoError(t, err, fmt.Sprintf("failed to set cli flag: %T, err: %s", flags[i], err))
return ctx
}
func TestDaemon_DevXMRTaker(t *testing.T) {
c := newTestContext(t,
"test --dev-xmrtaker",
[]string{flagDevXMRTaker, flagBasepath},
[]interface{}{true, t.TempDir()},
map[string]any{
flagEnv: "dev",
flagDevXMRTaker: true,
flagDataDir: t.TempDir(),
},
)
ctx, cancel := context.WithCancel(context.Background())
@@ -84,8 +72,12 @@ func TestDaemon_DevXMRTaker(t *testing.T) {
func TestDaemon_DevXMRMaker(t *testing.T) {
c := newTestContext(t,
"test --dev-xmrmaker",
[]string{flagDevXMRMaker, flagDeploy, flagBasepath},
[]interface{}{true, true, t.TempDir()},
map[string]any{
flagEnv: "dev",
flagDevXMRMaker: true,
flagDeploy: true,
flagDataDir: t.TempDir(),
},
)
ctx, cancel := context.WithCancel(context.Background())
@@ -99,3 +91,23 @@ func TestDaemon_DevXMRMaker(t *testing.T) {
err := d.make(c)
require.NoError(t, err)
}
func Test_expandBootnodes(t *testing.T) {
cliNodes := []string{
" node1, node2 ,node3,node4 ",
"node5",
"\tnode6\n",
"node7,node8",
}
expected := []string{
"node1",
"node2",
"node3",
"node4",
"node5",
"node6",
"node7",
"node8",
}
require.EqualValues(t, expected, expandBootnodes(cliNodes))
}

View File

@@ -8,9 +8,9 @@ import (
"path/filepath"
ethcommon "github.com/ethereum/go-ethereum/common"
ethcrypto "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/urfave/cli"
logging "github.com/ipfs/go-log"
"github.com/urfave/cli/v2"
"github.com/athanorlabs/atomic-swap/cmd/utils"
"github.com/athanorlabs/atomic-swap/common"
@@ -21,15 +21,13 @@ import (
"github.com/athanorlabs/atomic-swap/protocol/xmrtaker"
recovery "github.com/athanorlabs/atomic-swap/recover"
"github.com/athanorlabs/atomic-swap/swapfactory"
logging "github.com/ipfs/go-log"
)
const (
flagEnv = "env"
flagMoneroWalletEndpoint = "monero-endpoint"
flagEthereumEndpoint = "ethereum-endpoint"
flagEthereumPrivateKey = "ethereum-privkey"
flagEthereumPrivKey = "ethereum-privkey"
flagEthereumChainID = "ethereum-chain-id"
flagGasPrice = "gas-price"
flagGasLimit = "gas-limit"
@@ -56,7 +54,8 @@ var (
Flags: []cli.Flag{
&cli.StringFlag{
Name: flagEnv,
Usage: "environment to use: one of mainnet, stagenet, or dev",
Usage: "Environment to use: one of mainnet, stagenet, or dev",
Value: "dev",
},
&cli.StringFlag{
Name: flagMoneroWalletEndpoint,
@@ -64,35 +63,35 @@ var (
},
&cli.StringFlag{
Name: flagEthereumEndpoint,
Usage: "ethereum client endpoint",
Usage: "Ethereum client endpoint",
},
&cli.StringFlag{
Name: flagEthereumPrivateKey,
Usage: "file containing a private key hex string",
Name: flagEthereumPrivKey,
Usage: "File containing a private key hex string",
},
&cli.UintFlag{
Name: flagEthereumChainID,
Usage: "ethereum chain ID; eg. mainnet=1, ropsten=3, rinkeby=4, goerli=5, ganache=1337",
Usage: "Ethereum chain ID; eg. mainnet=1, goerli=5, ganache=1337",
},
&cli.UintFlag{
Name: flagGasPrice,
Usage: "ethereum gas price to use for transactions (in gwei). if not set, the gas price is set via oracle.",
Usage: "Ethereum gas price to use for transactions (in gwei). If not set, the gas price is set via oracle.",
},
&cli.UintFlag{
Name: flagGasLimit,
Usage: "ethereum gas limit to use for transactions. if not set, the gas limit is estimated for each transaction.",
Usage: "Ethereum gas limit to use for transactions. if not set, the gas limit is estimated for each transaction.",
},
&cli.StringFlag{
Name: flagInfoFile,
Usage: "path to swap infofile",
Usage: "Path to swap infofile",
},
&cli.BoolFlag{
Name: flagXMRMaker,
Usage: "true if recovering as an xmr-maker",
Usage: "Use when recovering as an xmr-maker",
},
&cli.BoolFlag{
Name: flagXMRTaker,
Usage: "true if recovering as an xmr-taker",
Usage: "Use when recovering as an xmr-taker",
},
},
}
@@ -108,8 +107,8 @@ func main() {
// Recoverer is implemented by a backend which is able to recover swap funds
type Recoverer interface {
WalletFromSharedSecret(secret *mcrypto.PrivateKeyInfo) (mcrypto.Address, error)
RecoverFromXMRMakerSecretAndContract(b backend.Backend, basepath string, xmrmakerSecret, contractAddr string, swapID [32]byte, swap swapfactory.SwapFactorySwap) (*xmrmaker.RecoveryResult, error) //nolint:lll
RecoverFromXMRTakerSecretAndContract(b backend.Backend, basepath string, xmrtakerSecret string, swapID [32]byte, swap swapfactory.SwapFactorySwap) (*xmrtaker.RecoveryResult, error) //nolint:lll
RecoverFromXMRMakerSecretAndContract(b backend.Backend, dataDir string, xmrmakerSecret, contractAddr string, swapID [32]byte, swap swapfactory.SwapFactorySwap) (*xmrmaker.RecoveryResult, error) //nolint:lll
RecoverFromXMRTakerSecretAndContract(b backend.Backend, dataDir string, xmrtakerSecret string, swapID [32]byte, swap swapfactory.SwapFactorySwap) (*xmrtaker.RecoveryResult, error) //nolint:lll
}
type instance struct {
@@ -126,11 +125,12 @@ func runRecover(c *cli.Context) error {
func (inst *instance) recover(c *cli.Context) error {
xmrmaker := c.Bool(flagXMRMaker)
xmrtaker := c.Bool(flagXMRTaker)
if !xmrmaker && !xmrtaker {
// Either maker or taker must be specified, but not both, so their values must be opposite
if xmrmaker == xmrtaker {
return errMustSpecifyXMRMakerOrTaker
}
env, cfg, err := utils.GetEnvironment(c)
env, cfg, err := utils.GetEnvironment(c.String(flagEnv))
if err != nil {
return err
}
@@ -173,10 +173,10 @@ func (inst *instance) recover(c *cli.Context) error {
return err
}
basepath := filepath.Dir(filepath.Clean(infofilePath))
dataDir := filepath.Dir(filepath.Clean(infofilePath))
if xmrmaker {
res, err := r.RecoverFromXMRMakerSecretAndContract(b, basepath, infofile.PrivateKeyInfo.PrivateSpendKey,
res, err := r.RecoverFromXMRMakerSecretAndContract(b, dataDir, infofile.PrivateKeyInfo.PrivateSpendKey,
contractAddr, infofile.ContractSwapID, infofile.ContractSwap)
if err != nil {
return err
@@ -194,7 +194,7 @@ func (inst *instance) recover(c *cli.Context) error {
}
if xmrtaker {
res, err := r.RecoverFromXMRTakerSecretAndContract(b, basepath, infofile.PrivateKeyInfo.PrivateSpendKey,
res, err := r.RecoverFromXMRTakerSecretAndContract(b, dataDir, infofile.PrivateKeyInfo.PrivateSpendKey,
infofile.ContractSwapID, infofile.ContractSwap)
if err != nil {
return err
@@ -245,9 +245,8 @@ func createBackend(ctx context.Context, c *cli.Context, env common.Environment,
moneroEndpoint, ethEndpoint string
)
chainID := int64(c.Uint(flagEthereumChainID))
if chainID == 0 {
chainID = cfg.EthereumChainID
if c.IsSet(flagEthereumChainID) {
cfg.EthereumChainID = int64(c.Uint(flagEthereumChainID))
}
if c.String(flagMoneroWalletEndpoint) != "" {
@@ -263,7 +262,10 @@ func createBackend(ctx context.Context, c *cli.Context, env common.Environment,
}
// TODO: add --external-signer option to allow front-end integration (#124)
ethPrivKey, err := utils.GetEthereumPrivateKey(c, env, false, false)
ethPrivKeyFile := c.String(flagEthereumPrivKey)
devXMRMaker := false // Not directly supported, but you can put the Ganache key in a file
devXMRTaker := false
ethPrivKey, err := utils.GetEthereumPrivateKey(ethPrivKeyFile, env, devXMRMaker, devXMRTaker)
if err != nil {
return nil, err
}
@@ -274,11 +276,6 @@ func createBackend(ctx context.Context, c *cli.Context, env common.Environment,
gasPrice = big.NewInt(int64(c.Uint(flagGasPrice)))
}
pk, err := ethcrypto.HexToECDSA(ethPrivKey)
if err != nil {
return nil, err
}
ec, err := ethclient.Dial(ethEndpoint)
if err != nil {
return nil, err
@@ -293,9 +290,9 @@ func createBackend(ctx context.Context, c *cli.Context, env common.Environment,
Ctx: ctx,
MoneroWalletEndpoint: moneroEndpoint,
EthereumClient: ec,
EthereumPrivateKey: pk,
EthereumPrivateKey: ethPrivKey,
Environment: env,
ChainID: big.NewInt(chainID),
ChainID: big.NewInt(cfg.EthereumChainID),
GasPrice: gasPrice,
GasLimit: uint64(c.Uint(flagGasLimit)),
SwapContract: contract,

View File

@@ -6,7 +6,6 @@ import (
"fmt"
"os"
"path"
"strconv"
"testing"
"github.com/athanorlabs/atomic-swap/common"
@@ -19,57 +18,43 @@ import (
"github.com/athanorlabs/atomic-swap/tests"
"github.com/stretchr/testify/require"
"github.com/urfave/cli"
"github.com/urfave/cli/v2"
)
func newTestContext(t *testing.T, description string, flags []string, values []interface{}) *cli.Context {
require.Equal(t, len(flags), len(values))
func newTestContext(t *testing.T, description string, flags map[string]any) *cli.Context {
set := flag.NewFlagSet(description, 0)
for i := range values {
switch v := values[i].(type) {
for flag, value := range flags {
switch v := value.(type) {
case bool:
set.Bool(flags[i], v, "")
set.Bool(flag, v, "")
case string:
set.String(flags[i], v, "")
set.String(flag, v, "")
case uint:
set.Uint(flags[i], v, "")
set.Uint(flag, v, "")
case int64:
set.Int64(flags[i], v, "")
set.Int64(flag, v, "")
case []string:
set.Var(&cli.StringSlice{}, flags[i], "")
set.Var(&cli.StringSlice{}, flag, "")
default:
t.Fatalf("unexpected cli value type: %T", values[i])
t.Fatalf("unexpected cli value type: %T", value)
}
}
ctx := cli.NewContext(app, set, nil)
var (
err error
i int
)
for i = range values {
switch v := values[i].(type) {
case bool:
err = ctx.Set(flags[i], strconv.FormatBool(v))
case string:
err = ctx.Set(flags[i], values[i].(string))
case uint:
err = ctx.Set(flags[i], strconv.Itoa(int(values[i].(uint))))
case int64:
err = ctx.Set(flags[i], strconv.Itoa(int(values[i].(int64))))
for flag, value := range flags {
switch v := value.(type) {
case bool, uint, int64, string:
require.NoError(t, ctx.Set(flag, fmt.Sprintf("%v", v)))
case []string:
for _, str := range values[i].([]string) {
err = ctx.Set(flags[i], str)
require.NoError(t, err)
for _, str := range v {
require.NoError(t, ctx.Set(flag, str))
}
default:
t.Fatalf("unexpected cli value type: %T", values[i])
t.Fatalf("unexpected cli value type: %T", value)
}
}
require.NoError(t, err, fmt.Sprintf("failed to set cli flag: %T, err: %s", flags[i], err))
return ctx
}
@@ -122,11 +107,18 @@ func createInfoFile(t *testing.T, kpA, kpB *mcrypto.PrivateKeyPair, contractAddr
bz, err := json.MarshalIndent(infofile, "", "\t")
require.NoError(t, err)
filepath := path.Join(t.TempDir(), "test-infofile.txt")
err = os.WriteFile(filepath, bz, os.ModePerm)
err = os.WriteFile(filepath, bz, 0600)
require.NoError(t, err)
return filepath
}
func createEthPrivKeyFile(t *testing.T, ethKeyHex string) string {
fileName := path.Join(t.TempDir(), "eth.key")
err := os.WriteFile(fileName, []byte(ethKeyHex), 0600)
require.NoError(t, err)
return fileName
}
func TestRecover_sharedSwapSecret(t *testing.T) {
kpA, err := mcrypto.GenerateKeys()
require.NoError(t, err)
@@ -137,11 +129,11 @@ func TestRecover_sharedSwapSecret(t *testing.T) {
c := newTestContext(t,
"test --xmrtaker with shared swap secret",
[]string{flagXMRTaker, flagInfoFile, flagMoneroWalletEndpoint},
[]interface{}{
true,
infoFilePath,
tests.CreateWalletRPCService(t),
map[string]any{
flagEnv: "dev",
flagXMRTaker: true,
flagInfoFile: infoFilePath,
flagMoneroWalletEndpoint: tests.CreateWalletRPCService(t),
},
)
@@ -157,10 +149,12 @@ func TestRecover_withXMRMakerSecretAndContract(t *testing.T) {
c := newTestContext(t,
"test --xmrmaker with contract address and secret",
[]string{flagXMRMaker, flagInfoFile},
[]interface{}{
true,
infoFilePath,
map[string]any{
flagEnv: "dev",
flagXMRMaker: true,
flagInfoFile: infoFilePath,
flagEthereumPrivKey: createEthPrivKeyFile(t, common.DefaultPrivKeyXMRMaker),
flagMoneroWalletEndpoint: tests.CreateWalletRPCService(t),
},
)
@@ -179,10 +173,11 @@ func TestRecover_withXMRTakerSecretAndContract(t *testing.T) {
c := newTestContext(t,
"test --xmrtaker with contract address and secret",
[]string{flagXMRTaker, flagInfoFile},
[]interface{}{
true,
infoFilePath,
map[string]any{
flagEnv: "dev",
flagXMRTaker: true,
flagInfoFile: infoFilePath,
flagEthereumPrivKey: createEthPrivKeyFile(t, common.DefaultPrivKeyXMRTaker),
},
)

View File

@@ -13,7 +13,7 @@ import (
"sync"
"time"
"github.com/urfave/cli"
"github.com/urfave/cli/v2"
"github.com/athanorlabs/atomic-swap/common"
"github.com/athanorlabs/atomic-swap/common/types"

View File

@@ -1,63 +1,52 @@
package utils
import (
"crypto/ecdsa"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
logging "github.com/ipfs/go-log"
"github.com/urfave/cli"
ethcrypto "github.com/ethereum/go-ethereum/crypto"
"github.com/athanorlabs/atomic-swap/common"
)
const (
// TODO: just move all the flags to here, or their own package? there's a lot of duplicate ones
flagEthereumPrivKey = "ethereum-privkey"
flagEnv = "env"
)
var log = logging.Logger("cmd")
var defaultEnvironment = common.Development
var (
errNoEthereumPrivateKey = errors.New("must provide --ethereum-privkey file for non-development environment")
errInvalidEnv = errors.New("--env must be one of mainnet, stagenet, or dev")
)
// GetEthereumPrivateKey returns an ethereum private key hex string given the CLI options.
func GetEthereumPrivateKey(c *cli.Context, env common.Environment, devXMRMaker,
useExternal bool) (ethPrivKeyHex string, err error) {
if c.String(flagEthereumPrivKey) != "" {
ethPrivKeyFile := c.String(flagEthereumPrivKey)
key, err := os.ReadFile(filepath.Clean(ethPrivKeyFile))
// GetEthereumPrivateKey returns an ethereum private key for the given the CLI options.
func GetEthereumPrivateKey(ethPrivKeyFile string, env common.Environment, devXMRMaker, devXMRTaker bool) (
key *ecdsa.PrivateKey,
err error,
) {
if ethPrivKeyFile != "" {
fileData, err := os.ReadFile(filepath.Clean(ethPrivKeyFile))
if err != nil {
return "", fmt.Errorf("failed to read ethereum-privkey file: %w", err)
}
ethPrivKeyHex = strings.TrimSpace(string(key))
} else {
if env != common.Development || useExternal {
log.Warnf("%s", errNoEthereumPrivateKey)
return "", nil
return nil, fmt.Errorf("failed to read ethereum-privkey file: %w", err)
}
ethPrivKeyHex := strings.TrimSpace(string(fileData))
return ethcrypto.HexToECDSA(ethPrivKeyHex)
}
log.Warn("no ethereum private key file provided, using ganache deterministic key")
if devXMRMaker {
ethPrivKeyHex = common.DefaultPrivKeyXMRMaker
} else {
ethPrivKeyHex = common.DefaultPrivKeyXMRTaker
if env == common.Development {
switch {
case devXMRMaker:
return ethcrypto.HexToECDSA(common.DefaultPrivKeyXMRMaker)
case devXMRTaker:
return ethcrypto.HexToECDSA(common.DefaultPrivKeyXMRTaker)
}
}
return ethPrivKeyHex, nil
return nil, errNoEthereumPrivateKey
}
// GetEnvironment returns a common.Environment from the CLI options.
func GetEnvironment(c *cli.Context) (env common.Environment, cfg common.Config, err error) {
switch c.String(flagEnv) {
func GetEnvironment(envStr string) (env common.Environment, cfg common.Config, err error) {
switch envStr {
case "mainnet":
env = common.Mainnet
cfg = common.MainnetConfig
@@ -67,9 +56,6 @@ func GetEnvironment(c *cli.Context) (env common.Environment, cfg common.Config,
case "dev":
env = common.Development
cfg = common.DevelopmentConfig
case "":
env = defaultEnvironment
cfg = common.DevelopmentConfig
default:
return 0, common.Config{}, errInvalidEnv
}

77
cmd/utils/utils_test.go Normal file
View File

@@ -0,0 +1,77 @@
package utils
import (
"encoding/hex"
"fmt"
"os"
"path"
"testing"
ethcrypto "github.com/ethereum/go-ethereum/crypto"
"github.com/stretchr/testify/require"
"github.com/athanorlabs/atomic-swap/common"
)
func TestGetEthereumPrivateKey_devXMRMaker(t *testing.T) {
devXMRMaker := true
devXMRTaker := false
key, err := GetEthereumPrivateKey("", common.Development, devXMRMaker, devXMRTaker)
require.NoError(t, err)
require.Equal(t, common.DefaultPrivKeyXMRMaker, hex.EncodeToString(ethcrypto.FromECDSA(key)))
}
func TestGetEthereumPrivateKey_devXMRTaker(t *testing.T) {
devXMRMaker := false
devXMRTaker := true
key, err := GetEthereumPrivateKey("", common.Development, devXMRMaker, devXMRTaker)
require.NoError(t, err)
require.Equal(t, common.DefaultPrivKeyXMRTaker, hex.EncodeToString(ethcrypto.FromECDSA(key)))
}
func TestGetEthereumPrivateKey_devXMRMaker_nonDevEnv(t *testing.T) {
devXMRMaker := true
devXMRTaker := false
_, err := GetEthereumPrivateKey("", common.Stagenet, devXMRMaker, devXMRTaker)
require.ErrorIs(t, err, errNoEthereumPrivateKey)
}
func TestGetEthereumPrivateKey_fromFile(t *testing.T) {
keyHex := "87c546d6cb8ec705bea47e2ab40f42a768b1e5900686b0cecc68c0e8b74cd789"
fileData := []byte(fmt.Sprintf(" %s\n", keyHex)) // add whitespace that we should ignore
keyFile := path.Join(t.TempDir(), "eth.key")
require.NoError(t, os.WriteFile(keyFile, fileData, 0600))
key, err := GetEthereumPrivateKey(keyFile, common.Mainnet, false, false)
require.NoError(t, err)
require.Equal(t, keyHex, hex.EncodeToString(ethcrypto.FromECDSA(key)))
}
func TestGetEthereumPrivateKey_fromFileFail(t *testing.T) {
keyHex := "87c546d6cb8ec705bea47e2ab40f42a768b1e5900686b0cecc68c0e8b74cd789"
keyBytes, err := hex.DecodeString(keyHex)
require.NoError(t, err)
keyFile := path.Join(t.TempDir(), "eth.key")
require.NoError(t, os.WriteFile(keyFile, keyBytes, 0600)) // key is binary instead of hex
_, err = GetEthereumPrivateKey(keyFile, common.Mainnet, false, false)
require.ErrorContains(t, err, "invalid hex character")
}
func TestGetEnvironment(t *testing.T) {
expected := map[string]common.Environment{
"mainnet": common.Mainnet,
"stagenet": common.Stagenet,
"dev": common.Development,
}
for cliVal, expectedResult := range expected {
env, cfg, err := GetEnvironment(cliVal)
require.NoError(t, err)
require.Equal(t, expectedResult, env)
require.NotEmpty(t, cfg.DataDir)
}
}
func TestGetEnvironment_fail(t *testing.T) {
_, _, err := GetEnvironment("goerli")
require.ErrorIs(t, err, errInvalidEnv)
}

View File

@@ -3,35 +3,49 @@ package common
import (
"fmt"
"os"
ethcommon "github.com/ethereum/go-ethereum/common"
)
var homeDir, _ = os.UserHomeDir()
// Config contains constants that are defaults for various environments
type Config struct {
Basepath string
DataDir string
MoneroDaemonEndpoint string
EthereumChainID int64
Bootnodes []string // TODO: when it's ready for users to test, add some bootnodes (#153)
ContractAddress ethcommon.Address
Bootnodes []string
}
// MainnetConfig is the mainnet ethereum and monero configuration
var MainnetConfig = Config{
Basepath: fmt.Sprintf("%s/.atomicswap/mainnet", homeDir),
DataDir: fmt.Sprintf("%s/.atomicswap/mainnet", homeDir),
MoneroDaemonEndpoint: "http://127.0.0.1:18081/json_rpc",
EthereumChainID: MainnetChainID,
}
// StagenetConfig is the monero stagenet and ethereum ropsten configuration
// StagenetConfig is the monero stagenet and ethereum Gorli configuration
var StagenetConfig = Config{
Basepath: fmt.Sprintf("%s/.atomicswap/stagenet", homeDir),
DataDir: fmt.Sprintf("%s/.atomicswap/stagenet", homeDir),
MoneroDaemonEndpoint: "http://127.0.0.1:38081/json_rpc",
EthereumChainID: RopstenChainID,
EthereumChainID: GorliChainID,
ContractAddress: ethcommon.HexToAddress("0x64e902cD8A29bBAefb9D4e2e3A24d8250C606ee7"),
Bootnodes: []string{
"/ip4/134.122.115.208/tcp/9900/p2p/12D3KooWDqCzbjexHEa8Rut7bzxHFpRMZyDRW1L6TGkL1KY24JH5",
"/ip4/143.198.123.27/tcp/9900/p2p/12D3KooWSc4yFkPWBFmPToTMbhChH3FAgGH96DNzSg5fio1pQYoN",
"/ip4/67.207.89.83/tcp/9900/p2p/12D3KooWLbfkLZZvvn8Lxs1KDU3u7gyvBk88ZNtJBbugytBr5RCG",
"/ip4/134.122.115.208/tcp/9900/p2p/12D3KooWDqCzbjexHEa8Rut7bzxHFpRMZyDRW1L6TGkL1KY24JH5",
"/ip4/164.92.103.160/tcp/9900/p2p/12D3KooWAZtRECEv7zN69zU1e7sPrHbMgfqFUn7QTLh1pKGiMuaM",
"/ip4/164.92.103.159/tcp/9900/p2p/12D3KooWSNQF1eNyapxC2zA3jJExgLX7jWhEyw8B3k7zMW5ZRvQz",
"/ip4/164.92.123.10/tcp/9900/p2p/12D3KooWG8z9fXVTB72XL8hQbahpfEjutREL9vbBQ4FzqtDKzTBu",
"/ip4/161.35.110.210/tcp/9900/p2p/12D3KooWS8iKxqsGTiL3Yc1VaAfg99U5km1AE7bWYQiuavXj3Yz6",
},
}
// DevelopmentConfig is the monero and ethereum development environment configuration
var DevelopmentConfig = Config{
Basepath: fmt.Sprintf("%s/.atomicswap/dev", homeDir),
DataDir: fmt.Sprintf("%s/.atomicswap/dev", homeDir),
MoneroDaemonEndpoint: "http://127.0.0.1:18081/json_rpc",
EthereumChainID: GanacheChainID,
}

View File

@@ -2,7 +2,7 @@ package common
const (
MainnetChainID = 1 //nolint
RopstenChainID = 3
GorliChainID = 5
GanacheChainID = 1337
DefaultXMRTakerMoneroEndpoint = "http://127.0.0.1:18084/json_rpc"

View File

@@ -32,13 +32,10 @@ func GetTopic(sig string) ethcommon.Hash {
return b
}
// MakeDir makes a directory
// MakeDir creates a directory, including leading directories, if they don't already exist.
// File permissions of created directories are only granted to the current user.
func MakeDir(dir string) error {
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
return err
}
return nil
return os.MkdirAll(dir, 0700)
}
// Exists returns whether the given file or directory exists

View File

@@ -1,9 +1,12 @@
package common
import (
"os"
"path"
"testing"
ethcommon "github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -23,3 +26,12 @@ func TestGetTopic(t *testing.T) {
refundedTopic := ethcommon.HexToHash("0x007c875846b687732a7579c19bb1dade66cd14e9f4f809565e2b2b5e76c72b4f")
require.Equal(t, GetTopic(RefundedEventSignature), refundedTopic)
}
func TestMakeDir(t *testing.T) {
path := path.Join(t.TempDir(), "mainnet")
require.NoError(t, MakeDir(path))
assert.NoError(t, MakeDir(path)) // No error if the dir already exists
fileStats, err := os.Stat(path)
require.NoError(t, err)
assert.Equal(t, "drwx------", fileStats.Mode().String()) // only user has access
}

View File

@@ -61,7 +61,7 @@ make build
10. Copy `goerli.key` into this directory. If you are using an Infura Goerli endpoint, copy-paste your API key into the field below following the `--ethereum-endpoint` flag. Otherwise, change `--ethereum-endpoint` to point to your endpoint. Finally, start the `swapd` atomic swap daemon process:
```bash
./swapd --env stagenet --ethereum-privkey=goerli.key --monero-endpoint=http://localhost:18083/json_rpc --wallet-file=stagenet-wallet --ethereum-endpoint=https://goerli.infura.io/v3/<your-api-key> --ethereum-chain-id=5 --contract-address=0x2125320230096B33b55f6d7905Fef61A3a0906a0 --bootnodes /ip4/134.122.115.208/tcp/9900/p2p/12D3KooWDqCzbjexHEa8Rut7bzxHFpRMZyDRW1L6TGkL1KY24JH5,/ip4/143.198.123.27/tcp/9900/p2p/12D3KooWSc4yFkPWBFmPToTMbhChH3FAgGH96DNzSg5fio1pQYoN,/ip4/67.207.89.83/tcp/9900/p2p/12D3KooWLbfkLZZvvn8Lxs1KDU3u7gyvBk88ZNtJBbugytBr5RCG,/ip4/134.122.115.208/tcp/9900/p2p/12D3KooWDqCzbjexHEa8Rut7bzxHFpRMZyDRW1L6TGkL1KY24JH5,/ip4/164.92.103.160/tcp/9900/p2p/12D3KooWAZtRECEv7zN69zU1e7sPrHbMgfqFUn7QTLh1pKGiMuaM,/ip4/164.92.103.159/tcp/9900/p2p/12D3KooWSNQF1eNyapxC2zA3jJExgLX7jWhEyw8B3k7zMW5ZRvQz,/ip4/164.92.123.10/tcp/9900/p2p/12D3KooWG8z9fXVTB72XL8hQbahpfEjutREL9vbBQ4FzqtDKzTBu,/ip4/161.35.110.210/tcp/9900/p2p/12D3KooWS8iKxqsGTiL3Yc1VaAfg99U5km1AE7bWYQiuavXj3Yz6,/ip4/206.189.47.220/tcp/9900/p2p/12D3KooWGVzz2d2LSceVFFdqTYqmQXTqc5eWziw7PLRahCWGJhKB --rpc-port=5001
./swapd --env stagenet --ethereum-privkey=goerli.key --monero-endpoint=http://localhost:18083/json_rpc --wallet-file=stagenet-wallet --ethereum-endpoint=https://goerli.infura.io/v3/<your-api-key> --rpc-port=5001
```
> Note: please also see the [RPC documentation](./rpc.md) for complete documentation on available RPC calls and their parameters.

3
go.mod
View File

@@ -23,7 +23,7 @@ require (
github.com/multiformats/go-multiaddr v0.6.0
github.com/noot/cgo-dleq v0.0.0-20220726051627-d0716fb55684
github.com/stretchr/testify v1.8.0
github.com/urfave/cli v1.22.9
github.com/urfave/cli/v2 v2.10.2
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa
)
@@ -139,6 +139,7 @@ require (
github.com/tklauser/go-sysconf v0.3.10 // indirect
github.com/tklauser/numcpus v0.5.0 // indirect
github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect
go.opencensus.io v0.23.0 // indirect
go.uber.org/atomic v1.10.0 // indirect

4
go.sum
View File

@@ -741,9 +741,8 @@ github.com/tklauser/numcpus v0.5.0/go.mod h1:OGzpTxpcIMNGYQdit2BYL1pvk/dSOaJWjKo
github.com/tyler-smith/go-bip39 v1.0.1-0.20181017060643-dbb3b84ba2ef h1:wHSqTBrZW24CsNJDfeh9Ex6Pm0Rcpc7qrgKBiL44vF4=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli v1.22.9 h1:cv3/KhXGBGjEXLC4bH0sLuJ9BewaAbpk5oyMOveu4pw=
github.com/urfave/cli v1.22.9/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli/v2 v2.10.2 h1:x3p8awjp/2arX+Nl/G2040AZpOCHS/eMJJ1/a+mye4Y=
github.com/urfave/cli/v2 v2.10.2/go.mod h1:f8iq5LtQ/bLxafbdBSLPPNsgaW0l/2fYYEHhAyPlwvo=
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
github.com/warpfork/go-wish v0.0.0-20200122115046-b9ea61034e4a h1:G++j5e0OC488te356JvdhaM8YS6nMsjLAYF7JxCv07w=
@@ -754,6 +753,7 @@ github.com/whyrusleeping/go-logging v0.0.0-20170515211332-0457bb6b88fc/go.mod h1
github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=

View File

@@ -122,7 +122,7 @@ func (d *discovery) discover(provides types.ProvidesCoin,
searchTime.Seconds(),
)
peerCh, err := d.rd.FindPeers(d.ctx, string("XMR"))
peerCh, err := d.rd.FindPeers(d.ctx, string(provides))
if err != nil {
return nil, err
}

View File

@@ -71,8 +71,8 @@ type host struct {
type Config struct {
Ctx context.Context
Environment common.Environment
Basepath string
ChainID int64
DataDir string
EthChainID int64
Port uint16
KeyFile string
Bootnodes []string
@@ -111,7 +111,7 @@ func NewHost(cfg *Config) (*host, error) {
}
}
ds, err := badger.NewDatastore(path.Join(cfg.Basepath, "libp2p-datastore"), &badger.DefaultOptions)
ds, err := badger.NewDatastore(path.Join(cfg.DataDir, "libp2p-datastore"), &badger.DefaultOptions)
if err != nil {
return nil, err
}
@@ -165,7 +165,7 @@ func NewHost(cfg *Config) (*host, error) {
hst := &host{
ctx: ourCtx,
cancel: cancel,
protocolID: fmt.Sprintf("%s/%s/%d", protocolID, cfg.Environment, cfg.ChainID),
protocolID: fmt.Sprintf("%s/%s/%d", protocolID, cfg.Environment, cfg.EthChainID),
h: h,
handler: cfg.Handler,
ds: ds,
@@ -218,7 +218,7 @@ func (h *host) logPeers() {
}
}
// close closes host services and the libp2p host (host services first)
// Stop closes host services and the libp2p host (host services first)
func (h *host) Stop() error {
h.cancel()

View File

@@ -62,8 +62,8 @@ func newHost(t *testing.T, port uint16) *host {
cfg := &Config{
Ctx: context.Background(),
Environment: common.Development,
Basepath: t.TempDir(),
ChainID: common.GanacheChainID,
DataDir: t.TempDir(),
EthChainID: common.GanacheChainID,
Port: port,
KeyFile: path.Join(t.TempDir(), fmt.Sprintf("node-%d.key", port)),
Bootnodes: []string{},

View File

@@ -11,15 +11,15 @@ import (
)
// GetSwapInfoFilepath returns an info file path with the current timestamp.
func GetSwapInfoFilepath(basePath string) string {
func GetSwapInfoFilepath(dataDir string) string {
t := time.Now().Format(common.TimeFmtNSecs)
return path.Join(basePath, t)
return path.Join(dataDir, t)
}
// GetSwapRecoveryFilepath returns an info file path with the current timestamp.
func GetSwapRecoveryFilepath(basePath string) string {
func GetSwapRecoveryFilepath(dataDir string) string {
t := time.Now().Format(common.TimeFmtNSecs)
return path.Join(basePath, fmt.Sprintf("recovery-%s.txt", t))
return path.Join(dataDir, fmt.Sprintf("recovery-%s.txt", t))
}
// ConvertContractSwapToMsg converts a swapfactory.SwapFactorySwap to a *message.ContractSwap

View File

@@ -17,8 +17,9 @@ func (b *Instance) MakeOffer(o *types.Offer) (*types.OfferExtra, error) {
return nil, err
}
if common.MoneroAmount(balance.UnlockedBalance) < common.MoneroToPiconero(o.MaximumAmount) {
return nil, errUnlockedBalanceTooLow
unlockedBalance := common.MoneroAmount(balance.UnlockedBalance)
if unlockedBalance < common.MoneroToPiconero(o.MaximumAmount) {
return nil, errUnlockedBalanceTooLow{unlockedBalance.AsMonero(), o.MaximumAmount}
}
extra := b.offerManager.AddOffer(o)

View File

@@ -3,6 +3,7 @@ package xmrmaker
import (
"errors"
"fmt"
"strconv"
)
var (
@@ -28,11 +29,55 @@ var (
// protocol initiation errors
errProtocolAlreadyInProgress = errors.New("protocol already in progress")
errBalanceTooLow = errors.New("balance lower than amount to be provided")
errNoOfferWithID = errors.New("failed to find offer with given ID")
errOfferIDNotSet = errors.New("offer ID was not set")
errAmountProvidedTooLow = errors.New("amount provided by taker is too low for offer")
errAmountProvidedTooHigh = errors.New("amount provided by taker is too high for offer")
errUnlockedBalanceTooLow = errors.New("unlocked balance is less than maximum offer amount")
errSwapCompleted = errors.New("swap is already completed")
)
type errBalanceTooLow struct {
unlockedBalance float64
providedAmount float64
}
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),
)
}
type errAmountProvidedTooLow struct {
providedAmount float64
minAmount float64
}
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),
)
}
type errAmountProvidedTooHigh struct {
providedAmount float64
maxAmount float64
}
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),
)
}
type errUnlockedBalanceTooLow struct {
minAmount float64
unlockedBalance float64
}
func (e errUnlockedBalanceTooLow) Error() string {
return fmt.Sprintf("balance %s XMR is too low for maximum offer amount of %s XMR",
strconv.FormatFloat(e.minAmount, 'f', -1, 64),
strconv.FormatFloat(e.unlockedBalance, 'f', -1, 64),
)
}

View File

@@ -18,8 +18,8 @@ var (
// Instance implements the functionality that will be needed by a user who owns XMR
// and wishes to swap for ETH.
type Instance struct {
backend backend.Backend
basepath string
backend backend.Backend
dataDir string
walletFile, walletPassword string
@@ -32,7 +32,7 @@ type Instance struct {
// Config contains the configuration values for a new XMRMaker instance.
type Config struct {
Backend backend.Backend
Basepath string
DataDir string
WalletFile, WalletPassword string
ExternalSender bool
}
@@ -50,10 +50,10 @@ func NewInstance(cfg *Config) (*Instance, error) {
return &Instance{
backend: cfg.Backend,
basepath: cfg.Basepath,
dataDir: cfg.DataDir,
walletFile: cfg.WalletFile,
walletPassword: cfg.WalletPassword,
offerManager: offers.NewManager(cfg.Basepath),
offerManager: offers.NewManager(cfg.DataDir),
swapStates: make(map[types.Hash]*swapState),
}, nil
}

View File

@@ -34,7 +34,10 @@ func (b *Instance) initiate(
// check user's balance and that they actually have what they will provide
if balance.UnlockedBalance <= uint64(providesAmount) {
return nil, errBalanceTooLow
return nil, errBalanceTooLow{
unlockedBalance: common.MoneroAmount(balance.UnlockedBalance).AsMonero(),
providedAmount: providesAmount.AsMonero(),
}
}
s, err := newSwapState(b.backend, offer, b.offerManager, offerExtra.StatusCh,
@@ -86,11 +89,11 @@ func (b *Instance) HandleInitiateMessage(msg *net.SendKeysMessage) (net.SwapStat
providedAmount := offer.ExchangeRate.ToXMR(msg.ProvidedAmount)
if providedAmount < offer.MinimumAmount {
return nil, nil, errAmountProvidedTooLow
return nil, nil, errAmountProvidedTooLow{providedAmount, offer.MinimumAmount}
}
if providedAmount > offer.MaximumAmount {
return nil, nil, errAmountProvidedTooHigh
return nil, nil, errAmountProvidedTooHigh{providedAmount, offer.MaximumAmount}
}
providedPicoXMR := common.MoneroToPiconero(providedAmount)

View File

@@ -11,9 +11,9 @@ const statusChSize = 6 // the max number of stages a swap can potentially go thr
// Manager synchronises access to the offers map.
type Manager struct {
mu sync.Mutex // synchronises access to the offers map
offers map[types.Hash]*offerWithExtra
basePath string
mu sync.Mutex // synchronises access to the offers map
offers map[types.Hash]*offerWithExtra
dataDir string
}
type offerWithExtra struct {
@@ -21,12 +21,12 @@ type offerWithExtra struct {
extra *types.OfferExtra
}
// NewManager creates a new offers manager. The passed in basePath is the directory where the
// NewManager creates a new offers manager. The passed in dataDir is the directory where the
// recovery file is for each individual swap is stored.
func NewManager(basePath string) *Manager {
func NewManager(dataDir string) *Manager {
return &Manager{
offers: make(map[types.Hash]*offerWithExtra),
basePath: basePath,
offers: make(map[types.Hash]*offerWithExtra),
dataDir: dataDir,
}
}
@@ -56,7 +56,7 @@ func (m *Manager) AddOffer(o *types.Offer) *types.OfferExtra {
extra := &types.OfferExtra{
StatusCh: make(chan types.Status, statusChSize),
InfoFile: pcommon.GetSwapInfoFilepath(m.basePath),
InfoFile: pcommon.GetSwapInfoFilepath(m.dataDir),
}
m.offers[id] = &offerWithExtra{

View File

@@ -4,13 +4,14 @@ import (
"context"
"errors"
ethcommon "github.com/ethereum/go-ethereum/common"
"github.com/athanorlabs/atomic-swap/common/types"
mcrypto "github.com/athanorlabs/atomic-swap/crypto/monero"
"github.com/athanorlabs/atomic-swap/dleq"
pcommon "github.com/athanorlabs/atomic-swap/protocol"
"github.com/athanorlabs/atomic-swap/protocol/backend"
"github.com/athanorlabs/atomic-swap/swapfactory"
ethcommon "github.com/ethereum/go-ethereum/common"
)
type recoveryState struct {
@@ -19,7 +20,7 @@ type recoveryState struct {
// NewRecoveryState returns a new *xmrmaker.recoveryState,
// which has methods to either claim ether or reclaim monero from an initiated swap.
func NewRecoveryState(b backend.Backend, basePath string, secret *mcrypto.PrivateSpendKey,
func NewRecoveryState(b backend.Backend, dataDir string, secret *mcrypto.PrivateSpendKey,
contractAddr ethcommon.Address,
contractSwapID [32]byte, contractSwap swapfactory.SwapFactorySwap) (*recoveryState, error) {
kp, err := secret.AsPrivateKeyPair()
@@ -49,7 +50,7 @@ func NewRecoveryState(b backend.Backend, basePath string, secret *mcrypto.Privat
dleqProof: dleq.NewProofWithSecret(sc),
contractSwapID: contractSwapID,
contractSwap: contractSwap,
infoFile: pcommon.GetSwapRecoveryFilepath(basePath),
infoFile: pcommon.GetSwapRecoveryFilepath(dataDir),
}
if err := s.setContract(contractAddr); err != nil {

View File

@@ -23,8 +23,8 @@ func newTestRecoveryState(t *testing.T, timeout time.Duration) *recoveryState {
newSwap(t, s, [32]byte{}, sr, big.NewInt(1), timeout)
basePath := path.Join(t.TempDir(), "test-infofile")
rs, err := NewRecoveryState(inst.backend, basePath, s.privkeys.SpendKey(), s.ContractAddr(),
dataDir := path.Join(t.TempDir(), "test-infofile")
rs, err := NewRecoveryState(inst.backend, dataDir, s.privkeys.SpendKey(), s.ContractAddr(),
s.contractSwapID, s.contractSwap)
require.NoError(t, err)

View File

@@ -86,7 +86,7 @@ func newTestXMRMaker(t *testing.T) *Instance {
cfg := &Config{
Backend: b,
Basepath: path.Join(t.TempDir(), "xmrmaker"),
DataDir: path.Join(t.TempDir(), "xmrmaker"),
WalletFile: testWallet,
WalletPassword: "",
}

View File

@@ -27,8 +27,8 @@ var (
// Instance implements the functionality that will be used by a user who owns ETH
// and wishes to swap for XMR.
type Instance struct {
backend backend.Backend
basepath string
backend backend.Backend
dataDir string
walletFile, walletPassword string
transferBack bool // transfer xmr back to original account
@@ -42,7 +42,7 @@ type Instance struct {
// Config contains the configuration values for a new XMRTaker instance.
type Config struct {
Backend backend.Backend
Basepath string
DataDir string
MoneroWalletFile, MoneroWalletPassword string
TransferBack bool
ExternalSender bool
@@ -74,7 +74,7 @@ func NewInstance(cfg *Config) (*Instance, error) {
return &Instance{
backend: cfg.Backend,
basepath: cfg.Basepath,
dataDir: cfg.DataDir,
walletFile: cfg.MoneroWalletFile,
walletPassword: cfg.MoneroWalletPassword,
swapStates: make(map[types.Hash]*swapState),

View File

@@ -45,8 +45,16 @@ func (a *Instance) initiate(providesAmount common.EtherAmount, receivedAmount co
return nil, errBalanceTooLow
}
s, err := newSwapState(a.backend, offerID, pcommon.GetSwapInfoFilepath(a.basepath), a.transferBack,
providesAmount, receivedAmount, exchangeRate, ethAsset)
s, err := newSwapState(
a.backend,
offerID,
pcommon.GetSwapInfoFilepath(a.dataDir),
a.transferBack,
providesAmount,
receivedAmount,
exchangeRate,
ethAsset,
)
if err != nil {
return nil, err
}

View File

@@ -12,8 +12,8 @@ import (
func newTestXMRTaker(t *testing.T) *Instance {
b := newBackend(t)
cfg := &Config{
Backend: b,
Basepath: path.Join(t.TempDir(), "xmrtaker"),
Backend: b,
DataDir: path.Join(t.TempDir(), "xmrtaker"),
}
xmrtaker, err := NewInstance(cfg)

View File

@@ -27,7 +27,7 @@ type recoveryState struct {
// NewRecoveryState returns a new *xmrmaker.recoveryState,
// which has methods to either claim ether or reclaim monero from an initiated swap.
func NewRecoveryState(b backend.Backend, basePath string, secret *mcrypto.PrivateSpendKey,
func NewRecoveryState(b backend.Backend, dataDir string, secret *mcrypto.PrivateSpendKey,
contractSwapID [32]byte, contractSwap swapfactory.SwapFactorySwap) (*recoveryState, error) {
kp, err := secret.AsPrivateKeyPair()
if err != nil {
@@ -56,7 +56,7 @@ func NewRecoveryState(b backend.Backend, basePath string, secret *mcrypto.Privat
dleqProof: dleq.NewProofWithSecret(sc),
contractSwapID: contractSwapID,
contractSwap: contractSwap,
infoFile: pcommon.GetSwapRecoveryFilepath(basePath),
infoFile: pcommon.GetSwapRecoveryFilepath(dataDir),
claimedCh: make(chan struct{}),
info: pswap.NewEmptyInfo(),
}

View File

@@ -28,8 +28,8 @@ func newTestRecoveryState(t *testing.T, timeout time.Duration) *recoveryState {
_, err = s.lockETH(common.NewEtherAmount(1))
require.NoError(t, err)
basePath := path.Join(t.TempDir(), "test-infoFile")
rs, err := NewRecoveryState(s, basePath, s.privkeys.SpendKey(), s.contractSwapID, s.contractSwap)
dataDir := path.Join(t.TempDir(), "test-infoFile")
rs, err := NewRecoveryState(s, dataDir, s.privkeys.SpendKey(), s.contractSwapID, s.contractSwap)
require.NoError(t, err)
return rs
}

View File

@@ -89,7 +89,7 @@ func (r *recoverer) WalletFromSharedSecret(pk *mcrypto.PrivateKeyInfo) (mcrypto.
}
// RecoverFromXMRMakerSecretAndContract recovers funds by either claiming ether or reclaiming locked monero.
func (r *recoverer) RecoverFromXMRMakerSecretAndContract(b backend.Backend, basePath string,
func (r *recoverer) RecoverFromXMRMakerSecretAndContract(b backend.Backend, dataDir string,
xmrmakerSecret, contractAddr string, swapID [32]byte,
swap swapfactory.SwapFactorySwap) (*xmrmaker.RecoveryResult, error) {
bs, err := hex.DecodeString(xmrmakerSecret)
@@ -103,7 +103,7 @@ func (r *recoverer) RecoverFromXMRMakerSecretAndContract(b backend.Backend, base
}
addr := ethcommon.HexToAddress(contractAddr)
rs, err := xmrmaker.NewRecoveryState(b, basePath, bk, addr, swapID, swap)
rs, err := xmrmaker.NewRecoveryState(b, dataDir, bk, addr, swapID, swap)
if err != nil {
return nil, err
}
@@ -112,7 +112,7 @@ func (r *recoverer) RecoverFromXMRMakerSecretAndContract(b backend.Backend, base
}
// RecoverFromXMRTakerSecretAndContract recovers funds by either claiming locked monero or refunding ether.
func (r *recoverer) RecoverFromXMRTakerSecretAndContract(b backend.Backend, basePath string,
func (r *recoverer) RecoverFromXMRTakerSecretAndContract(b backend.Backend, dataDir string,
xmrtakerSecret string, swapID [32]byte, swap swapfactory.SwapFactorySwap) (*xmrtaker.RecoveryResult, error) {
as, err := hex.DecodeString(xmrtakerSecret)
if err != nil {
@@ -124,7 +124,7 @@ func (r *recoverer) RecoverFromXMRTakerSecretAndContract(b backend.Backend, base
return nil, err
}
rs, err := xmrtaker.NewRecoveryState(b, basePath, ak, swapID, swap)
rs, err := xmrtaker.NewRecoveryState(b, dataDir, ak, swapID, swap)
if err != nil {
return nil, err
}

View File

@@ -140,8 +140,8 @@ func TestRecoverer_RecoverFromXMRMakerSecretAndContract_Claim(t *testing.T) {
b := newBackend(t, addr, contract, tests.GetMakerTestKey(t))
r := newRecoverer(t)
basePath := path.Join(t.TempDir(), "test-infofile")
res, err := r.RecoverFromXMRMakerSecretAndContract(b, basePath, keys.PrivateKeyPair.SpendKey().Hex(),
dataDir := path.Join(t.TempDir(), "test-infofile")
res, err := r.RecoverFromXMRMakerSecretAndContract(b, dataDir, keys.PrivateKeyPair.SpendKey().Hex(),
addr.String(), swapID, swap)
require.NoError(t, err)
require.True(t, res.Claimed)
@@ -156,8 +156,8 @@ func TestRecoverer_RecoverFromXMRMakerSecretAndContract_Claim_afterTimeout(t *te
b := newBackend(t, addr, contract, tests.GetMakerTestKey(t))
r := newRecoverer(t)
basePath := path.Join(t.TempDir(), "test-infofile")
res, err := r.RecoverFromXMRMakerSecretAndContract(b, basePath, keys.PrivateKeyPair.SpendKey().Hex(),
dataDir := path.Join(t.TempDir(), "test-infofile")
res, err := r.RecoverFromXMRMakerSecretAndContract(b, dataDir, keys.PrivateKeyPair.SpendKey().Hex(),
addr.String(), swapID, swap)
require.NoError(t, err)
require.True(t, res.Claimed)
@@ -172,8 +172,8 @@ func TestRecoverer_RecoverFromXMRTakerSecretAndContract_Refund(t *testing.T) {
b := newBackend(t, addr, contract, tests.GetTakerTestKey(t))
r := newRecoverer(t)
basePath := path.Join(t.TempDir(), "test-infofile")
res, err := r.RecoverFromXMRTakerSecretAndContract(b, basePath, keys.PrivateKeyPair.SpendKey().Hex(),
dataDir := path.Join(t.TempDir(), "test-infofile")
res, err := r.RecoverFromXMRTakerSecretAndContract(b, dataDir, keys.PrivateKeyPair.SpendKey().Hex(),
swapID, swap)
require.NoError(t, err)
require.True(t, res.Refunded)

View File

@@ -2,13 +2,10 @@
# This script can be handy to kill test processes launched manually
# for debugging or still hanging around for other reasons.
pkill --uid "${UID}" --full '/monerod .* --regtest '
pkill --uid "${UID}" --full '/ganache.* --deterministic '
pkill --echo --uid "${UID}" --full '/monerod .* --regtest '
pkill --echo --uid "${UID}" --full '/ganache.* --deterministic '
# If you have monero-wallet-rpc or swapd processes owned by the current user
# that you don't want to kill, don't use this script!
pkill --uid "${UID}" --full '/monero-wallet-rpc '
pkill --uid "${UID}" --full '/swapd '
# These directories MUST be removed every time you start a fresh monerod instance
rm -rf alice-test-keys bob-test-keys
pkill --echo --uid "${UID}" --full '/monero-wallet-rpc '
pkill --echo --uid "${UID}" --full '/swapd '

View File

@@ -35,4 +35,4 @@ fi
--abi ethereum/abi/IERC20Metadata.abi \
--pkg swapfactory \
--type IERC20 \
--out swapfactory/ierc20.go
--out swapfactory/ierc20.go

View File

@@ -10,6 +10,10 @@ start-monerod-regtest
start-ganache
start-alice-wallet
start-bob-wallet
start-charlie-wallet
CHARLIE_ETH_KEY="${SWAP_TEST_DATA_DIR}/charlie-eth.key"
echo "87c546d6cb8ec705bea47e2ab40f42a768b1e5900686b0cecc68c0e8b74cd789" >"${CHARLIE_ETH_KEY}"
# wait for wallets to start
sleep 5
@@ -40,6 +44,8 @@ start-swapd bob \
--deploy
start-swapd charlie \
--monero-endpoint "http://127.0.0.1:${CHARLIE_WALLET_PORT}/json_rpc" \
--ethereum-privkey "${CHARLIE_ETH_KEY}" \
--libp2p-port 9955 \
--rpc-port 5003 \
--ws-port 8083 \
@@ -51,15 +57,26 @@ sleep 3 # Time for Bob and Charlie's swapd to be fully up
# run tests
echo "running integration tests..."
TESTS=integration go test ./tests -v -count=1
OK=$?
OK="${?}"
# If we failed, make a copy of the log files that won't get deleted
if [[ "${OK}" -ne 0 ]]; then
mkdir -p "${SWAP_TEST_DATA_DIR}/saved-logs"
cp "${SWAP_TEST_DATA_DIR}/"*.log "${SWAP_TEST_DATA_DIR}/saved-logs/"
echo "Logs saved to ${SWAP_TEST_DATA_DIR}/saved-logs/"
fi
stop-swapd alice
stop-swapd bob
stop-swapd charlie
stop-alice-wallet
stop-bob-wallet
stop-charlie-wallet
stop-monerod-regtest
stop-ganache
remove-test-data-dir
rm -f "${CHARLIE_ETH_KEY}"
if [[ "${OK}" -eq 0 ]]; then
remove-test-data-dir
fi
exit $OK
exit "${OK}"

View File

@@ -21,8 +21,11 @@
MONEROD_PORT=18081
GANACHE_PORT=8545
BOB_WALLET_PORT=18083
ALICE_WALLET_PORT=18084
CHARLIE_WALLET_PORT=18085
PROJECT_ROOT="$(dirname "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")")"
MONERO_BIN_DIR="${PROJECT_ROOT}/monero-bin"
@@ -80,6 +83,7 @@ stop-program() {
echo "ERROR: failed to kill ${name}"
return 1
fi
sleep 2 # let program flush data and exit so we delete all files below
# Remove the PID file, log file and any data subdirectory
rm -rf "${SWAP_TEST_DATA_DIR:?}/${name}"{.pid,.log,}
}
@@ -202,3 +206,11 @@ start-bob-wallet() {
stop-bob-wallet() {
stop-program bob-monero-wallet-rpc
}
start-charlie-wallet() {
start-monero-wallet-rpc charlie "${CHARLIE_WALLET_PORT}"
}
stop-charlie-wallet() {
stop-program charlie-monero-wallet-rpc
}

View File

@@ -67,7 +67,10 @@ func generateBlocks(num uint64) {
}
fmt.Println("> Generating blocks for test setup...")
_ = d.GenerateBlocks(xmrmakerAddr.Address, num)
err = d.GenerateBlocks(xmrmakerAddr.Address, num)
if err != nil {
panic(err)
}
err = c.Refresh()
if err != nil {
panic(err)
@@ -96,6 +99,9 @@ func generateBlocksAsync() {
}
func TestXMRTaker_Discover(t *testing.T) {
if os.Getenv(generateBlocksEnv) != falseStr {
generateBlocks(64)
}
bc := rpcclient.NewClient(defaultXMRMakerDaemonEndpoint)
_, err := bc.MakeOffer(xmrmakerProvideAmount, xmrmakerProvideAmount, exchangeRate, types.EthAssetETH)
require.NoError(t, err)
@@ -119,6 +125,9 @@ func TestXMRMaker_Discover(t *testing.T) {
}
func TestXMRTaker_Query(t *testing.T) {
if os.Getenv(generateBlocksEnv) != falseStr {
generateBlocks(64)
}
bc := rpcclient.NewClient(defaultXMRMakerDaemonEndpoint)
offerID, err := bc.MakeOffer(xmrmakerProvideAmount, xmrmakerProvideAmount, exchangeRate, types.EthAssetETH)
require.NoError(t, err)
@@ -150,6 +159,10 @@ func TestXMRTaker_Query(t *testing.T) {
}
func TestSuccess_OneSwap(t *testing.T) {
if os.Getenv(generateBlocksEnv) != falseStr {
generateBlocks(64)
}
const testTimeout = time.Second * 75
ctx, cancel := context.WithCancel(context.Background())