Files
atomic-swap/cmd/daemon/main.go
Dmitry Holodov 5698e25239 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.
2022-09-10 14:54:43 -05:00

561 lines
14 KiB
Go

package main
import (
"context"
"crypto/ecdsa"
"fmt"
"math/big"
"os"
"path"
"strings"
ethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/urfave/cli/v2"
"github.com/athanorlabs/atomic-swap/cmd/utils"
"github.com/athanorlabs/atomic-swap/common"
"github.com/athanorlabs/atomic-swap/net"
"github.com/athanorlabs/atomic-swap/protocol/backend"
"github.com/athanorlabs/atomic-swap/protocol/swap"
"github.com/athanorlabs/atomic-swap/protocol/xmrmaker"
"github.com/athanorlabs/atomic-swap/protocol/xmrtaker"
"github.com/athanorlabs/atomic-swap/rpc"
logging "github.com/ipfs/go-log"
)
const (
// default libp2p ports
defaultLibp2pPort = 9900
defaultXMRTakerLibp2pPort = 9933
defaultXMRMakerLibp2pPort = 9934
// default libp2p key files
defaultLibp2pKey = "node.key"
defaultXMRTakerLibp2pKey = "xmrtaker.key"
defaultXMRMakerLibp2pKey = "xmrmaker.key"
// default RPC port
defaultRPCPort = 5005
defaultXMRTakerRPCPort = 5001
defaultXMRMakerRPCPort = 5002
)
var (
log = logging.Logger("cmd")
// 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"
flagDataDir = "data-dir"
flagLibp2pKey = "libp2p-key"
flagLibp2pPort = "libp2p-port"
flagBootnodes = "bootnodes"
flagWalletFile = "wallet-file"
flagWalletPassword = "wallet-password"
flagEnv = "env"
flagMoneroWalletEndpoint = "monero-endpoint"
flagMoneroDaemonEndpoint = "monero-daemon-endpoint"
flagEthereumEndpoint = "ethereum-endpoint"
flagEthereumPrivKey = "ethereum-privkey"
flagEthereumChainID = "ethereum-chain-id"
flagContractAddress = "contract-address"
flagGasPrice = "gas-price"
flagGasLimit = "gas-limit"
flagUseExternalSigner = "external-signer"
flagDevXMRTaker = "dev-xmrtaker"
flagDevXMRMaker = "dev-xmrmaker"
flagDeploy = "deploy"
flagTransferBack = "transfer-back"
flagLog = "log"
)
var (
app = &cli.App{
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.StringFlag{
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",
},
&cli.StringFlag{
Name: flagWalletPassword,
Usage: "Password of wallet file containing XMR to be swapped",
},
&cli.StringFlag{
Name: flagEnv,
Usage: "Environment to use: one of mainnet, stagenet, or dev",
Value: "dev",
},
&cli.StringFlag{
Name: flagMoneroWalletEndpoint,
Usage: "monero-wallet-rpc endpoint",
},
&cli.StringFlag{
Name: flagMoneroDaemonEndpoint,
Usage: "monerod RPC endpoint; only used if running in development mode",
},
&cli.StringFlag{
Name: flagEthereumEndpoint,
Usage: "Ethereum client endpoint",
},
&cli.StringFlag{
Name: flagEthereumPrivKey,
Usage: "File containing a private key as hex string",
},
&cli.UintFlag{
Name: flagEthereumChainID,
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",
},
&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.",
},
&cli.UintFlag{
Name: flagGasLimit,
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",
},
&cli.BoolFlag{
Name: flagDevXMRMaker,
Usage: "Run in development mode and use XMR provider default values",
},
&cli.BoolFlag{
Name: flagDeploy,
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.",
},
&cli.StringFlag{
Name: flagLog,
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",
},
},
}
)
func main() {
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}
}
type xmrtakerHandler interface {
rpc.XMRTaker
}
type xmrmakerHandler interface {
net.Handler
rpc.XMRMaker
}
type daemon struct {
ctx context.Context
cancel context.CancelFunc
}
func setLogLevels(c *cli.Context) error {
const (
levelError = "error"
levelWarn = "warn"
levelInfo = "info"
levelDebug = "debug"
)
level := c.String(flagLog)
switch level {
case levelError, levelWarn, levelInfo, levelDebug:
default:
return fmt.Errorf("invalid log level %q", level)
}
_ = logging.SetLogLevel("xmrtaker", level)
_ = logging.SetLogLevel("xmrmaker", level)
_ = logging.SetLogLevel("common", level)
_ = logging.SetLogLevel("cmd", level)
_ = logging.SetLogLevel("net", level)
_ = logging.SetLogLevel("rpc", level)
return nil
}
func runDaemon(c *cli.Context) error {
if err := setLogLevels(c); err != nil {
return err
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
d := &daemon{
ctx: ctx,
cancel: cancel,
}
err := d.make(c)
if err != nil {
return err
}
d.wait()
os.Exit(0)
return nil
}
// 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,
DataDir: cfg.DataDir,
EthChainID: cfg.EthereumChainID,
Port: libp2pPort,
KeyFile: libp2pKey,
Bootnodes: cfg.Bootnodes,
}
host, err := net.NewHost(netCfg)
if err != nil {
return err
}
sm := swap.NewManager()
backend, err := newBackend(d.ctx, c, env, cfg, devXMRMaker, devXMRTaker, sm, host)
if err != nil {
return err
}
a, b, err := getProtocolInstances(c, cfg, backend)
if err != nil {
return err
}
// connect network to protocol handler
// handler handles initiated ("taken") swap
host.SetHandler(b)
if err = host.Start(); err != nil {
return err
}
rpcPort := uint16(c.Uint(flagRPCPort))
if !c.IsSet(flagRPCPort) {
switch {
case devXMRTaker:
rpcPort = defaultXMRTakerRPCPort
case devXMRMaker:
rpcPort = defaultXMRMakerRPCPort
}
}
listenAddr := fmt.Sprintf("127.0.0.1:%d", rpcPort)
rpcCfg := &rpc.Config{
Ctx: d.ctx,
Address: listenAddr,
Net: host,
XMRTaker: a,
XMRMaker: b,
ProtocolBackend: backend,
}
s, err := rpc.NewServer(rpcCfg)
if err != nil {
return err
}
log.Infof("starting swapd with data-dir %s", cfg.DataDir)
return s.Start()
}
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 string
daemonEndpoint string
ethEndpoint string
ethPrivKey *ecdsa.PrivateKey
)
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)
case devXMRMaker:
moneroEndpoint = common.DefaultXMRMakerMoneroEndpoint
case devXMRTaker:
moneroEndpoint = common.DefaultXMRTakerMoneroEndpoint
default:
return nil, errFlagRequired(flagMoneroWalletEndpoint)
}
if c.String(flagEthereumEndpoint) != "" {
ethEndpoint = c.String(flagEthereumEndpoint)
} else {
ethEndpoint = common.DefaultEthEndpoint
}
useExternalSigner := c.Bool(flagUseExternalSigner)
ethPrivKeyFile := c.String(flagEthereumPrivKey)
if useExternalSigner && ethPrivKeyFile != "" {
return nil, errFlagsMutuallyExclusive(flagUseExternalSigner, flagEthereumPrivKey)
}
if !useExternalSigner {
var err error
if ethPrivKey, err = utils.GetEthereumPrivateKey(ethPrivKeyFile, env, devXMRMaker, devXMRTaker); err != nil {
return nil, err
}
}
if c.String(flagMoneroDaemonEndpoint) != "" {
daemonEndpoint = c.String(flagMoneroDaemonEndpoint)
} else {
daemonEndpoint = cfg.MoneroDaemonEndpoint
}
// TODO: add configs for different eth testnets + L2 and set gas limit based on those, if not set (#153)
var gasPrice *big.Int
if c.Uint(flagGasPrice) != 0 {
gasPrice = big.NewInt(int64(c.Uint(flagGasPrice)))
}
contractAddrStr := c.String(flagContractAddress)
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)
if err != nil {
return nil, err
}
deploy := c.Bool(flagDeploy)
if deploy {
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{}
}
chainID := big.NewInt(cfg.EthereumChainID)
contract, contractAddr, err :=
getOrDeploySwapFactory(ctx, cfg.ContractAddress, env, cfg.DataDir, chainID, ethPrivKey, ec)
if err != nil {
return nil, err
}
bcfg := &backend.Config{
Ctx: ctx,
MoneroWalletEndpoint: moneroEndpoint,
MoneroDaemonEndpoint: daemonEndpoint,
EthereumClient: ec,
EthereumPrivateKey: ethPrivKey,
Environment: env,
ChainID: chainID,
GasPrice: gasPrice,
GasLimit: uint64(c.Uint(flagGasLimit)),
SwapManager: sm,
SwapContract: contract,
SwapContractAddress: contractAddr,
Net: net,
}
b, err := backend.NewBackend(bcfg)
if err != nil {
return nil, fmt.Errorf("failed to make backend: %w", err)
}
log.Infof("created backend with monero endpoint %s and ethereum endpoint %s",
moneroEndpoint,
ethEndpoint,
)
return b, nil
}
func getProtocolInstances(c *cli.Context, cfg common.Config,
b backend.Backend) (xmrtakerHandler, xmrmakerHandler, error) {
walletFile := c.String("wallet-file")
// empty password is ok
walletPassword := c.String("wallet-password")
xmrtakerCfg := &xmrtaker.Config{
Backend: b,
DataDir: cfg.DataDir,
MoneroWalletFile: walletFile,
MoneroWalletPassword: walletPassword,
TransferBack: c.Bool(flagTransferBack),
}
xmrtaker, err := xmrtaker.NewInstance(xmrtakerCfg)
if err != nil {
return nil, nil, err
}
xmrmakerCfg := &xmrmaker.Config{
Backend: b,
DataDir: cfg.DataDir,
WalletFile: walletFile,
WalletPassword: walletPassword,
}
xmrmaker, err := xmrmaker.NewInstance(xmrmakerCfg)
if err != nil {
return nil, nil, err
}
return xmrtaker, xmrmaker, nil
}