feat: implement swap relayer cmd package (#282)

This commit is contained in:
noot
2023-01-25 01:17:55 -05:00
committed by GitHub
parent af08415254
commit ea43b6a86c
21 changed files with 885 additions and 102 deletions

View File

@@ -9,9 +9,11 @@ import (
"runtime/debug"
"strings"
"github.com/cockroachdb/apd/v3"
"github.com/ethereum/go-ethereum/common/hexutil"
ethcrypto "github.com/ethereum/go-ethereum/crypto"
logging "github.com/ipfs/go-log"
"github.com/urfave/cli/v2"
"github.com/athanorlabs/atomic-swap/common"
)
@@ -104,3 +106,42 @@ func GetVersion() string {
info.GoVersion,
)
}
// ReadUnsignedDecimalFlag reads a string flag and parses it into an *apd.Decimal.
func ReadUnsignedDecimalFlag(ctx *cli.Context, flagName string) (*apd.Decimal, error) {
s := ctx.String(flagName)
if s == "" {
return nil, fmt.Errorf("flag --%s cannot be empty", flagName)
}
bf, _, err := new(apd.Decimal).SetString(s)
if err != nil {
return nil, fmt.Errorf("invalid value %q for flag --%s", s, flagName)
}
if bf.IsZero() {
return nil, fmt.Errorf("value of flag --%s cannot be zero", flagName)
}
if bf.Negative {
return nil, fmt.Errorf("value of flag --%s cannot be negative", flagName)
}
return bf, 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 // nodes from all flag values combined
for _, flagVal := range nodesCLI {
splitNodes := strings.Split(flagVal, ",")
for _, n := range splitNodes {
n = strings.TrimSpace(n)
// Handle the empty string to not use default bootnodes. Doing it here after
// the split has the arguably positive side effect of skipping empty entries.
if len(n) > 0 {
nodes = append(nodes, strings.TrimSpace(n))
}
}
}
return nodes
}

View File

@@ -81,3 +81,31 @@ func TestGetVersion(t *testing.T) {
require.NotEmpty(t, GetVersion())
t.Log(GetVersion())
}
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))
}
func Test_expandBootnodes_noNodes(t *testing.T) {
// This can happen when the user specifies a single `--bootnodes ""` flag
// to not use the default bootnodes for an environment.
cliNodes := []string{""}
nodes := ExpandBootnodes(cliNodes)
require.Zero(t, len(nodes))
}

67
cmd/relayer/contract.go Normal file
View File

@@ -0,0 +1,67 @@
package main
import (
"context"
"fmt"
"math/big"
rcommon "github.com/athanorlabs/go-relayer/common"
rcontracts "github.com/athanorlabs/go-relayer/impls/gsnforwarder"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
ethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
contracts "github.com/athanorlabs/atomic-swap/ethereum"
)
func deployOrGetForwarder(
ctx context.Context,
addressString string,
ec *ethclient.Client,
key *rcommon.Key,
chainID *big.Int,
) (*rcontracts.IForwarder, ethcommon.Address, error) {
txOpts, err := bind.NewKeyedTransactorWithChainID(key.PrivateKey(), chainID)
if err != nil {
return nil, ethcommon.Address{}, fmt.Errorf("failed to make transactor: %w", err)
}
if addressString == "" {
address, tx, _, err := rcontracts.DeployForwarder(txOpts, ec) //nolint:govet
if err != nil {
return nil, ethcommon.Address{}, err
}
_, err = bind.WaitMined(ctx, ec, tx)
if err != nil {
return nil, ethcommon.Address{}, err
}
log.Infof("deployed Forwarder.sol to %s", address)
f, err := rcontracts.NewIForwarder(address, ec)
if err != nil {
return nil, ethcommon.Address{}, err
}
return f, address, nil
}
ok := ethcommon.IsHexAddress(addressString)
if !ok {
return nil, ethcommon.Address{}, errInvalidAddress
}
address := ethcommon.HexToAddress(addressString)
err = contracts.CheckForwarderContractCode(context.Background(), ec, address)
if err != nil {
return nil, ethcommon.Address{}, err
}
log.Infof("loaded Forwarder.sol at %s", address)
f, err := rcontracts.NewIForwarder(address, ec)
if err != nil {
return nil, ethcommon.Address{}, err
}
return f, address, nil
}

388
cmd/relayer/main.go Normal file
View File

@@ -0,0 +1,388 @@
// Package main is the entrypoint for the swap-specific Ethereum transaction relayer.
// Its purpose is to allow swaps users (ETH-takers in particular) to submit calls to the
// swap contract to claim their ETH from an account that does not have any ETH in it.
// This improves the swap UX by allowing users to obtain ETH without already having any.
// In this case, the relayer submits the transaction on the user's behalf, paying their
// gas fees, and (optionally) receiving a small percentage of the swap's value as payment.
package main
import (
"context"
"errors"
"fmt"
"net/http"
"os"
"path"
"path/filepath"
"strings"
"github.com/cockroachdb/apd/v3"
ethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/urfave/cli/v2"
p2pnet "github.com/athanorlabs/go-p2p-net"
rcommon "github.com/athanorlabs/go-relayer/common"
rcontracts "github.com/athanorlabs/go-relayer/impls/gsnforwarder"
net "github.com/athanorlabs/go-relayer/net"
"github.com/athanorlabs/go-relayer/relayer"
rrpc "github.com/athanorlabs/go-relayer/rpc"
"github.com/athanorlabs/atomic-swap/cliutil"
"github.com/athanorlabs/atomic-swap/common"
swapnet "github.com/athanorlabs/atomic-swap/net"
logging "github.com/ipfs/go-log"
)
const (
flagDataDir = "data-dir"
flagEthereumEndpoint = "ethereum-endpoint"
flagForwarderAddress = "forwarder-address"
flagKey = "key"
flagRPC = "rpc"
flagRPCPort = "rpc-port"
flagDeploy = "deploy"
flagLog = "log-level"
// TODO: do we need this, or can we assume all swap relayers will need to be on the p2p network?
flagWithNetwork = "with-network"
flagLibp2pKey = "libp2p-key"
flagLibp2pPort = "libp2p-port"
flagBootnodes = "bootnodes"
flagRelayerCommission = "relayer-commission"
defaultLibp2pPort = 10900
)
var (
log = logging.Logger("main")
flags = []cli.Flag{
&cli.StringFlag{
Name: flagDataDir,
Usage: "Path to store swap artifacts",
Value: "{HOME}/.atomicswap/{ENV}", // For --help only, actual default replaces variables
},
&cli.StringFlag{
Name: flagEthereumEndpoint,
Value: "http://localhost:8545",
Usage: "Ethereum RPC endpoint",
},
&cli.StringFlag{
Name: flagKey,
Value: fmt.Sprintf("{DATA-DIR}/%s", common.DefaultEthKeyFileName),
Usage: "Path to file containing Ethereum private key",
},
&cli.StringFlag{
Name: flagForwarderAddress,
Usage: "Address of the forwarder contract to use. Defaults to the forwarder address in the chain's swap contract.",
},
&cli.UintFlag{
Name: flagRPCPort,
Value: 7799,
Usage: "Relayer RPC server port",
},
&cli.BoolFlag{
Name: flagRPC,
Value: false,
Usage: "Run the relayer HTTP-RPC server on localhost. Defaults to false",
},
&cli.BoolFlag{
Name: flagDeploy,
Usage: "Deploy an instance of the forwarder contract",
},
&cli.StringFlag{
Name: flagLog,
Value: "info",
Usage: "Set log level: one of [error|warn|info|debug]",
},
&cli.BoolFlag{
Name: flagWithNetwork,
Value: true,
Usage: "Run the relayer with p2p network capabilities",
},
&cli.StringFlag{
Name: flagLibp2pKey,
Usage: "libp2p private key",
Value: fmt.Sprintf("{DATA_DIR}/%s", common.DefaultLibp2pKeyFileName),
},
&cli.UintFlag{
Name: flagLibp2pPort,
Usage: "libp2p port to listen on",
Value: defaultLibp2pPort,
},
&cli.StringSliceFlag{
Name: flagBootnodes,
Aliases: []string{"bn"},
Usage: "libp2p bootnode, comma separated if passing multiple to a single flag",
EnvVars: []string{"SWAPD_BOOTNODES"},
},
&cli.StringFlag{
Name: flagRelayerCommission,
Usage: "Minimum commission percentage (of the swap value) to receive:" +
" eg. --relayer-commission=0.01 for 1% commission",
Value: common.DefaultRelayerCommission.Text('f'),
},
}
errInvalidAddress = errors.New("invalid forwarder address")
errNoEthereumPrivateKey = errors.New("must provide ethereum private key with --key")
)
func main() {
app := &cli.App{
Name: "relayer",
Usage: "Ethereum transaction relayer",
Version: cliutil.GetVersion(),
Flags: flags,
Action: run,
EnableBashCompletion: true,
Suggest: true,
}
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}
}
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("main", level)
_ = logging.SetLogLevel("relayer", level)
_ = logging.SetLogLevel("rpc", level)
_ = logging.SetLogLevel("p2pnet", "debug")
return nil
}
func run(c *cli.Context) error {
err := setLogLevels(c)
if err != nil {
return err
}
port := uint16(c.Uint(flagRPCPort))
endpoint := c.String(flagEthereumEndpoint)
ec, err := ethclient.Dial(endpoint)
if err != nil {
return err
}
ctx, cancel := context.WithCancel(c.Context)
defer cancel()
chainID, err := ec.ChainID(ctx)
if err != nil {
return err
}
log.Infof("starting relayer with ethereum endpoint %s and chain ID %s", endpoint, chainID)
config, err := common.ConfigFromChainID(chainID)
if err != nil {
return err
}
// cfg.DataDir already has a default set, so only override if the user explicitly set the flag
var datadir string
if c.IsSet(flagDataDir) {
datadir = c.String(flagDataDir) // override the value derived from `flagEnv`
} else {
datadir = config.DataDir
}
datadir = path.Join(datadir, "relayer")
if err = common.MakeDir(datadir); err != nil {
return err
}
keyFile := path.Join(datadir, common.DefaultEthKeyFileName)
if c.IsSet(flagKey) {
keyFile = c.String(flagKey)
}
key, err := getPrivateKey(keyFile)
if err != nil {
return err
}
// set the forwarder address from the config, if it exists on that network
// however, if the --forwarder-address flag is set, that address takes precedence
var contractAddr string
if (config.ContractAddress != ethcommon.Address{}) {
contractAddr = config.ForwarderContractAddress.String()
}
addrFromFlag := c.String(flagForwarderAddress)
if addrFromFlag != "" {
contractAddr = addrFromFlag
}
deploy := c.Bool(flagDeploy)
if deploy && (addrFromFlag != "") {
return fmt.Errorf("flags --%s and --%s are mutually exclusive", flagDeploy, flagForwarderAddress)
}
if !deploy && (contractAddr == "") {
return fmt.Errorf("either --%s or --%s is required", flagDeploy, flagForwarderAddress)
}
forwarder, forwarderAddr, err := deployOrGetForwarder(
ctx,
contractAddr,
ec,
key,
chainID,
)
if err != nil {
return err
}
relayerCommission, err := cliutil.ReadUnsignedDecimalFlag(c, flagRelayerCommission)
if err != nil {
return err
}
if relayerCommission.Cmp(apd.New(1, -1)) > 0 {
return errors.New("relayer commission is too high: must be less than 0.1 (10%)")
}
// TODO: do we need to restrict potential commission values? eg. 1%, 1.25%, 1.5%, etc
// or should we just require a fixed value for now?
v := &validator{
ctx: ctx,
ec: ec,
relayerCommission: relayerCommission,
forwarderAddress: forwarderAddr,
}
// the forwarder contract is fixed here; thus it needs to be the same
// as what's hardcoded in the swap contract addr for that network.
rcfg := &relayer.Config{
Ctx: ctx,
EthClient: ec,
Forwarder: rcontracts.NewIForwarderWrapped(forwarder),
Key: key,
ValidateTransactionFunc: v.validateTransactionFunc,
}
r, err := relayer.NewRelayer(rcfg)
if err != nil {
return err
}
if c.Bool(flagWithNetwork) {
h, err := setupNetwork(ctx, c, ec, r, datadir) //nolint:govet
if err != nil {
return err
}
defer func() {
_ = h.Stop()
}()
}
if c.Bool(flagRPC) {
go signalHandler(ctx, cancel)
rpcCfg := &rrpc.Config{
Ctx: ctx,
Address: fmt.Sprintf("127.0.0.1:%d", port),
Relayer: r,
}
server, err := rrpc.NewServer(rpcCfg) //nolint:govet
if err != nil {
return err
}
err = server.Start()
if errors.Is(err, context.Canceled) || errors.Is(err, http.ErrServerClosed) {
return nil
}
} else {
signalHandler(ctx, cancel)
}
return err
}
func setupNetwork(
ctx context.Context,
c *cli.Context,
ec *ethclient.Client,
r *relayer.Relayer,
datadir string,
) (*net.Host, error) {
chainID, err := ec.ChainID(ctx)
if err != nil {
return nil, err
}
var bootnodes []string
if c.IsSet(flagBootnodes) {
bootnodes = cliutil.ExpandBootnodes(c.StringSlice(flagBootnodes))
}
libp2pKey := path.Join(datadir, common.DefaultLibp2pKeyFileName)
if c.IsSet(flagLibp2pKey) {
libp2pKey = c.String(flagLibp2pKey)
if libp2pKey == "" {
return nil, errFlagValueEmpty(flagLibp2pKey)
}
}
listenIP := "0.0.0.0"
netCfg := &p2pnet.Config{
Ctx: ctx,
DataDir: datadir,
Port: uint16(c.Uint(flagLibp2pPort)),
KeyFile: libp2pKey,
Bootnodes: bootnodes,
ProtocolID: fmt.Sprintf("/%s/%d/%s", swapnet.ProtocolID, chainID.Int64(), net.ProtocolID),
ListenIP: listenIP,
}
cfg := &net.Config{
Context: ctx,
P2pConfig: netCfg,
TransactionSubmitter: r,
IsRelayer: true,
}
h, err := net.NewHost(cfg)
if err != nil {
return nil, err
}
err = h.Start()
if err != nil {
return nil, err
}
return h, nil
}
func getPrivateKey(keyFile string) (*rcommon.Key, error) {
if keyFile != "" {
fileData, err := os.ReadFile(filepath.Clean(keyFile))
if err != nil {
return nil, fmt.Errorf("failed to read private key file: %w", err)
}
keyHex := strings.TrimSpace(string(fileData))
return rcommon.NewKeyFromPrivateKeyString(keyHex)
}
return nil, errNoEthereumPrivateKey
}
func errFlagValueEmpty(flag string) error {
return fmt.Errorf("flag %q requires a non-empty value", flag)
}

View File

@@ -0,0 +1,28 @@
package main
import (
"context"
"os"
"os/signal"
"syscall"
"time"
)
func signalHandler(ctx context.Context, cancel context.CancelFunc) {
sigc := make(chan os.Signal, 1)
signal.Ignore(syscall.SIGHUP)
signal.Notify(sigc, syscall.SIGINT, syscall.SIGTERM)
defer func() {
// Hopefully, we'll exit our main() before this sleep ends, but if not we allow
// the default signal behavior to kill us after this function exits.
time.Sleep(1 * time.Second)
signal.Stop(sigc)
}()
select {
case s := <-sigc:
log.Infof("Received signal %s(%d), shutting down...", s, s)
cancel()
case <-ctx.Done():
}
}

165
cmd/relayer/validate.go Normal file
View File

@@ -0,0 +1,165 @@
package main
import (
"bytes"
"context"
"errors"
"fmt"
"math/big"
"github.com/athanorlabs/atomic-swap/coins"
contracts "github.com/athanorlabs/atomic-swap/ethereum"
rcommon "github.com/athanorlabs/go-relayer/common"
"github.com/cockroachdb/apd/v3"
"github.com/ethereum/go-ethereum/accounts/abi"
ethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
)
var (
uint256Ty, _ = abi.NewType("uint256", "", nil)
bytes32Ty, _ = abi.NewType("bytes32", "", nil)
addressTy, _ = abi.NewType("address", "", nil)
arguments = abi.Arguments{
{
Name: "owner",
Type: addressTy,
},
{
Name: "claimer",
Type: addressTy,
},
{
Name: "pubKeyClaim",
Type: bytes32Ty,
},
{
Name: "pubKeyRefund",
Type: bytes32Ty,
},
{
Name: "timeout0",
Type: uint256Ty,
},
{
Name: "timeout1",
Type: uint256Ty,
},
{
Name: "asset",
Type: addressTy,
},
{
Name: "value",
Type: uint256Ty,
},
{
Name: "nonce",
Type: uint256Ty,
},
{
Name: "_s",
Type: bytes32Ty,
},
{
Name: "fee",
Type: uint256Ty,
},
}
)
type validator struct {
ctx context.Context
ec *ethclient.Client
relayerCommission *apd.Decimal
forwarderAddress ethcommon.Address
}
func (v *validator) validateTransactionFunc(req *rcommon.SubmitTransactionRequest) error {
// validate that:
// 1. the `to` address is a swap contract;
// 2. the function being called is `claimRelayer`;
// 3. the fee passed to `claimRelayer` is equal to or greater
// than our desired commission percentage.
forwarderAddr, err := contracts.CheckSwapFactoryContractCode(
v.ctx, v.ec, req.To,
)
if err != nil {
return err
}
if forwarderAddr != v.forwarderAddress {
return fmt.Errorf("swap contract does not have expected forwarder address: got %s, expected %s",
forwarderAddr,
v.forwarderAddress,
)
}
// hardcoded, from swap_factory.go bindings
claimRelayerSig := ethcommon.FromHex("0x73e4771c")
if !bytes.Equal(claimRelayerSig, req.Data[:4]) {
return fmt.Errorf("call must be to claimRelayer(); got call to function with sig 0x%x", req.Data[:4])
}
args, err := unpackData(req.Data[4:])
if err != nil {
return err
}
err = validateRelayerFee(args, v.relayerCommission)
if err != nil {
return err
}
return nil
}
func unpackData(data []byte) (map[string]interface{}, error) {
args := make(map[string]interface{})
err := arguments.UnpackIntoMap(args, data)
if err != nil {
return nil, err
}
return args, nil
}
func validateRelayerFee(args map[string]interface{}, minFeePercentage *apd.Decimal) error {
value, ok := args["value"].(*big.Int)
if !ok {
// this shouldn't happen afaik
return errors.New("value argument was not marshalled into a *big.Int")
}
fee, ok := args["fee"].(*big.Int)
if !ok {
// this shouldn't happen afaik
return errors.New("fee argument was not marshalled into a *big.Int")
}
valueD := apd.NewWithBigInt(
new(apd.BigInt).SetMathBigInt(value), // swap value, in wei
0,
)
feeD := apd.NewWithBigInt(
new(apd.BigInt).SetMathBigInt(fee), // fee, in wei
0,
)
percentage := new(apd.Decimal)
_, err := coins.DecimalCtx().Quo(percentage, feeD, valueD)
if err != nil {
return err
}
if percentage.Cmp(minFeePercentage) < 0 {
return fmt.Errorf("fee too low: percentage is %s, expected minimum %s",
percentage,
minFeePercentage,
)
}
return nil
}

View File

@@ -0,0 +1,96 @@
package main
import (
"math/big"
"strings"
"testing"
contracts "github.com/athanorlabs/atomic-swap/ethereum"
"github.com/cockroachdb/apd/v3"
"github.com/ethereum/go-ethereum/accounts/abi"
ethcommon "github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"
)
func TestValidateRelayerFee(t *testing.T) {
swapABI, err := abi.JSON(strings.NewReader(contracts.SwapFactoryMetaData.ABI))
require.NoError(t, err)
type testCase struct {
value, fee *big.Int
minFeePercentage *apd.Decimal
expectErr bool
}
testCases := []testCase{
{
value: big.NewInt(100),
fee: big.NewInt(1),
minFeePercentage: apd.New(1, -2),
},
{
value: big.NewInt(100),
fee: big.NewInt(2),
minFeePercentage: apd.New(1, -2),
},
{
value: big.NewInt(1000),
fee: big.NewInt(1),
minFeePercentage: apd.New(1, -2),
expectErr: true,
},
{
value: big.NewInt(100),
fee: big.NewInt(100),
minFeePercentage: apd.New(1, 0),
},
{
value: big.NewInt(100),
fee: big.NewInt(10),
minFeePercentage: apd.New(1, -1),
},
{
value: big.NewInt(10000),
fee: big.NewInt(99),
minFeePercentage: apd.New(1, -2),
expectErr: true,
},
{
value: big.NewInt(10000),
fee: big.NewInt(101),
minFeePercentage: apd.New(1, -2),
},
}
for _, tc := range testCases {
args := []interface{}{
&contracts.SwapFactorySwap{
Owner: ethcommon.Address{},
Claimer: ethcommon.Address{},
PubKeyClaim: [32]byte{},
PubKeyRefund: [32]byte{},
Timeout0: new(big.Int),
Timeout1: new(big.Int),
Asset: ethcommon.Address{},
Value: tc.value,
Nonce: new(big.Int),
},
[32]byte{},
tc.fee,
}
data, err := swapABI.Pack("claimRelayer", args...)
require.NoError(t, err)
unpacked, err := unpackData(data[4:])
require.NoError(t, err)
require.Equal(t, unpacked["value"], tc.value)
require.Equal(t, unpacked["fee"], tc.fee)
err = validateRelayerFee(unpacked, tc.minFeePercentage)
if tc.expectErr {
require.Error(t, err)
continue
}
require.NoError(t, err)
}
}

View File

@@ -473,17 +473,17 @@ func runQueryAll(ctx *cli.Context) error {
}
func runMake(ctx *cli.Context) error {
min, err := readUnsignedDecimalFlag(ctx, flagMinAmount)
min, err := cliutil.ReadUnsignedDecimalFlag(ctx, flagMinAmount)
if err != nil {
return err
}
max, err := readUnsignedDecimalFlag(ctx, flagMaxAmount)
max, err := cliutil.ReadUnsignedDecimalFlag(ctx, flagMaxAmount)
if err != nil {
return err
}
exchangeRateDec, err := readUnsignedDecimalFlag(ctx, flagExchangeRate)
exchangeRateDec, err := cliutil.ReadUnsignedDecimalFlag(ctx, flagExchangeRate)
if err != nil {
return err
}
@@ -509,7 +509,7 @@ func runMake(ctx *cli.Context) error {
relayerEndpoint := ctx.String(flagRelayerEndpoint)
relayerCommission := new(apd.Decimal)
if relayerEndpoint != "" {
if relayerCommission, err = readUnsignedDecimalFlag(ctx, flagRelayerCommission); err != nil {
if relayerCommission, err = cliutil.ReadUnsignedDecimalFlag(ctx, flagRelayerCommission); err != nil {
return err
}
} else if ctx.IsSet(flagRelayerCommission) {
@@ -577,7 +577,7 @@ func runTake(ctx *cli.Context) error {
return errInvalidFlagValue(flagOfferID, err)
}
providesAmount, err := readUnsignedDecimalFlag(ctx, flagProvidesAmount)
providesAmount, err := cliutil.ReadUnsignedDecimalFlag(ctx, flagProvidesAmount)
if err != nil {
return err
}
@@ -850,22 +850,3 @@ func providesStrToVal(providesStr string) (coins.ProvidesCoin, error) {
}
return coins.NewProvidesCoin(providesStr)
}
func readUnsignedDecimalFlag(ctx *cli.Context, flagName string) (*apd.Decimal, error) {
s := ctx.String(flagName)
if s == "" {
return nil, fmt.Errorf("flag --%s cannot be empty", flagName)
}
bf, _, err := new(apd.Decimal).SetString(s)
if err != nil {
return nil, fmt.Errorf("invalid value %q for flag --%s", s, flagName)
}
if bf.IsZero() {
return nil, fmt.Errorf("value of flag --%s cannot be zero", flagName)
}
if bf.Negative {
return nil, fmt.Errorf("value of flag --%s cannot be negative", flagName)
}
return bf, nil
}

View File

@@ -11,7 +11,6 @@ import (
"net/http"
"os"
"path"
"strings"
"github.com/ChainSafe/chaindb"
p2pnet "github.com/athanorlabs/go-p2p-net"
@@ -347,25 +346,6 @@ func (d *daemon) stop() error {
}
}
// 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 // nodes from all flag values combined
for _, flagVal := range nodesCLI {
splitNodes := strings.Split(flagVal, ",")
for _, n := range splitNodes {
n = strings.TrimSpace(n)
// Handle the empty string to not use default bootnodes. Doing it here after
// the split has the arguably positive side effect of skipping empty entries.
if len(n) > 0 {
nodes = append(nodes, strings.TrimSpace(n))
}
}
}
return nodes
}
func (d *daemon) make(c *cli.Context) error { //nolint:gocyclo
env, err := common.NewEnv(c.String(flagEnv))
if err != nil {
@@ -399,7 +379,7 @@ func (d *daemon) make(c *cli.Context) error { //nolint:gocyclo
}
if c.IsSet(flagBootnodes) {
cfg.Bootnodes = expandBootnodes(c.StringSlice(flagBootnodes))
cfg.Bootnodes = cliutil.ExpandBootnodes(c.StringSlice(flagBootnodes))
}
libp2pKey := cfg.LibP2PKeyFile()

View File

@@ -133,34 +133,6 @@ func TestDaemon_DevXMRMaker(t *testing.T) {
wg.Wait()
}
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))
}
func Test_expandBootnodes_noNodes(t *testing.T) {
// This can happen when the user specifies a single `--bootnodes ""` flag
// to not use the default bootnodes for an environment.
cliNodes := []string{""}
nodes := expandBootnodes(cliNodes)
require.Zero(t, len(nodes))
}
func TestDaemon_PersistOffers(t *testing.T) {
startupTimeout := time.Millisecond * 100

View File

@@ -18,7 +18,7 @@ const (
)
var (
// DecimalCtx is the apd context used for math operations on our coins
// decimalCtx is the apd context used for math operations on our coins
decimalCtx = apd.BaseContext.WithPrecision(MaxCoinPrecision)
log = logging.Logger("coins")

View File

@@ -1,6 +1,8 @@
package common
import (
"fmt"
"math/big"
"os"
"path"
"time"
@@ -30,10 +32,11 @@ type MoneroNode struct {
// Config contains constants that are defaults for various environments
type Config struct {
DataDir string
MoneroNodes []*MoneroNode
ContractAddress ethcommon.Address
Bootnodes []string
DataDir string
MoneroNodes []*MoneroNode
ContractAddress ethcommon.Address
ForwarderContractAddress ethcommon.Address
Bootnodes []string
}
// MainnetConfig is the mainnet ethereum and monero configuration
@@ -76,7 +79,8 @@ var StagenetConfig = Config{
Port: 38081,
},
},
ContractAddress: ethcommon.HexToAddress("0x01EeB71A63853fc89Ef26493bbdB7829F72b40d4"),
ContractAddress: ethcommon.HexToAddress("0x55c29ed1D31FC511c425308A03c060238b7aC35A"),
ForwarderContractAddress: ethcommon.HexToAddress("0xbb6a65B366251D98cDe86692ff16B54d3d9e804d"),
Bootnodes: []string{
"/ip4/134.122.115.208/tcp/9900/p2p/12D3KooWDqCzbjexHEa8Rut7bzxHFpRMZyDRW1L6TGkL1KY24JH5",
"/ip4/143.198.123.27/tcp/9900/p2p/12D3KooWSc4yFkPWBFmPToTMbhChH3FAgGH96DNzSg5fio1pQYoN",
@@ -158,3 +162,17 @@ func DefaultMoneroPortFromEnv(env Environment) uint {
panic("invalid environment")
}
}
// ConfigFromChainID returns the *Config corresponding to the given chain ID.
func ConfigFromChainID(chainID *big.Int) (Config, error) {
switch chainID.Uint64() {
case MainnetChainID:
return MainnetConfig, nil
case GoerliChainID:
return StagenetConfig, nil
case GanacheChainID, HardhatChainID:
return DevelopmentConfig, nil
default:
return Config{}, fmt.Errorf("no config for chain ID %d", chainID)
}
}

View File

@@ -1,6 +1,8 @@
// Package common is for miscellaneous constants, types and interfaces used by many packages.
package common
import "github.com/cockroachdb/apd/v3"
const (
DefaultMoneroDaemonMainnetPort = 18081 //nolint
DefaultMoneroDaemonDevPort = DefaultMoneroDaemonMainnetPort
@@ -30,3 +32,7 @@ const (
GanacheChainID = 1337
HardhatChainID = 31337
)
// DefaultRelayerCommission is the default commission percentage for swap relayers.
// It's set to 0.01 or 1%.
var DefaultRelayerCommission = apd.New(1, -2)

View File

@@ -40,7 +40,7 @@ file above. More information on what the individual files contain can be
### {DATA_DIR}/eth.key
This is the default location of your ethereum private key used by swaps. Alternate
This is the default location of your Ethereum private key used by swaps. Alternate
locations can be configured with `--ethereum-privkey`. If the file does not
exist, a new random key will be created and placed in this location.
@@ -63,3 +63,19 @@ Stores information on a swap when it reaches the stage where ethereum is locked.
Only written when `--deploy` is passed to swapd. This file stores the address
that the contract was deployed to along with other data.
## Relayer default file locations
### {DATA_DIR}/relayer
By default, all relayer-related files will be placed in the `relayer` directory within the data dir.
### {DATA_DIR}/relayer/eth.key
The location of the Ethereum private key used by the relayer to submit transactions. Fees received by the relayer will also go into this account. Alternate locations can be configured with `--ethereum-privkey`. If the file does not exist, the relayer will error on startup.
### {DATA_DIR}/relayer/net.key
The private key to the relayer's libp2p identity. If the file does not exist, a new
random key will be generated and placed in this location. Alternate locations can be
configured with `--libp2p-key`. It does not necessarily need to be a different key than that used by swapd.

View File

@@ -85,7 +85,7 @@ func CheckSwapFactoryContractCode(
return forwarderAddress, nil
}
err = checkForwarderContractCode(ctx, ec, forwarderAddress)
err = CheckForwarderContractCode(ctx, ec, forwarderAddress)
if err != nil {
return ethcommon.Address{}, err
}
@@ -94,9 +94,9 @@ func CheckSwapFactoryContractCode(
return forwarderAddress, nil
}
// checkSwapFactoryForwarder checks that the trusted forwarder contract used by
// CheckForwarderContractCode checks that the trusted forwarder contract used by
// the given swap contract has the expected bytecode.
func checkForwarderContractCode(
func CheckForwarderContractCode(
ctx context.Context,
ec *ethclient.Client,
contractAddr ethcommon.Address,

View File

@@ -52,7 +52,7 @@ func TestCheckForwarderContractCode(t *testing.T) {
ec, _ := tests.NewEthClient(t)
pk := tests.GetMakerTestKey(t)
trustedForwarder := deployForwarder(t, ec, pk)
err := checkForwarderContractCode(context.Background(), ec, trustedForwarder)
err := CheckForwarderContractCode(context.Background(), ec, trustedForwarder)
require.NoError(t, err)
}

2
go.mod
View File

@@ -9,7 +9,7 @@ require (
github.com/Masterminds/semver/v3 v3.1.1
github.com/athanorlabs/go-dleq v0.0.0-20221228030413-8ac300febf46
github.com/athanorlabs/go-p2p-net v0.0.0-20230109212235-d22ff2447282
github.com/athanorlabs/go-relayer v0.0.4
github.com/athanorlabs/go-relayer v0.0.5
github.com/athanorlabs/go-relayer-client v0.0.0-20221103041240-2aad2e8fc742
github.com/btcsuite/btcd/btcutil v1.1.2
github.com/cockroachdb/apd/v3 v3.1.2

4
go.sum
View File

@@ -67,8 +67,8 @@ github.com/athanorlabs/go-dleq v0.0.0-20221228030413-8ac300febf46 h1:4N2dClTJzeo
github.com/athanorlabs/go-dleq v0.0.0-20221228030413-8ac300febf46/go.mod h1:DWry6jSD7A13MKmeZA0AX3/xBeQCXDoygX99VPwL3yU=
github.com/athanorlabs/go-p2p-net v0.0.0-20230109212235-d22ff2447282 h1:jDOEysh6BxVc+EWWEG02vtdKsZWlo15DB4i8JnjUt+8=
github.com/athanorlabs/go-p2p-net v0.0.0-20230109212235-d22ff2447282/go.mod h1:U++jd4bNvI6qMDysd0gLWVst4DtHDEYvnoQAk4n7cx4=
github.com/athanorlabs/go-relayer v0.0.4 h1:KcibOFqXZJ6D/BzYAUpvtlG2LNu5NiN1MaCLxVWb4Z4=
github.com/athanorlabs/go-relayer v0.0.4/go.mod h1:7zbS8EWdZaMqfvi/mMxN+f0gqczwBHeOWhEu80d6/Uk=
github.com/athanorlabs/go-relayer v0.0.5 h1:DRpEcwM5C5zmpa9HxLh0fWILySgrzN8bLA77rmd5teo=
github.com/athanorlabs/go-relayer v0.0.5/go.mod h1:0Kbt5AKMMNBI6cdx3zB7+xon+NESveiJHFUlAfyxugc=
github.com/athanorlabs/go-relayer-client v0.0.0-20221103041240-2aad2e8fc742 h1:8XIlVAZ5K40kQw8qpQW6fb9rbYr4yzyajhHkMw9rMV0=
github.com/athanorlabs/go-relayer-client v0.0.0-20221103041240-2aad2e8fc742/go.mod h1:LzKxSadMjZ9ku9Nxt11AcVgoRzZtv8yE+tUsdszXKUo=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=

View File

@@ -39,7 +39,6 @@ func runRelayer(
ec *ethclient.Client,
forwarderAddress ethcommon.Address,
sk *ecdsa.PrivateKey,
chainID *big.Int,
) string {
iforwarder, err := gsnforwarder.NewIForwarder(forwarderAddress, ec)
require.NoError(t, err)
@@ -48,12 +47,13 @@ func runRelayer(
key := rcommon.NewKeyFromPrivateKey(sk)
cfg := &relayer.Config{
Ctx: ctx,
EthClient: ec,
Forwarder: fw,
Key: key,
ChainID: chainID,
NewForwardRequestFunc: gsnforwarder.NewIForwarderForwardRequest,
Ctx: ctx,
EthClient: ec,
Forwarder: fw,
Key: key,
ValidateTransactionFunc: func(_ *rcommon.SubmitTransactionRequest) error {
return nil
},
}
r, err := relayer.NewRelayer(cfg)
@@ -152,7 +152,7 @@ func testSwapStateClaimRelayer(t *testing.T, sk *ecdsa.PrivateKey, asset types.E
t.Logf("gas cost to call RegisterDomainSeparator: %d", receipt.GasUsed)
// start relayer
relayerEndpoint := runRelayer(t, ctx, conn, forwarderAddress, relayerSk, chainID)
relayerEndpoint := runRelayer(t, ctx, conn, forwarderAddress, relayerSk)
// deploy swap contract with claim key hash
contractAddr, tx, contract, err := contracts.DeploySwapFactory(txOpts, conn, forwarderAddress)

View File

@@ -20,8 +20,3 @@ if [[ -n "${ALL}" ]]; then
else
go install -tags=prod ./cmd/swapd ./cmd/swapcli
fi
# Since we are inside a project using go modules when performing this
# install, the version installed will match the go-relayer dependency in
# our go.mod file. To override, add a @version suffix on the end.
go install github.com/athanorlabs/go-relayer/cmd/relayer

View File

@@ -3,7 +3,7 @@
PROJECT_ROOT="$(dirname "$(dirname "$(realpath "$0")")")"
cd "${PROJECT_ROOT}" || exit 1
./scripts/build.sh || exit 1
ALL=true ./scripts/build.sh || exit 1
source "scripts/testlib.sh"
check-set-swap-test-data-dir
@@ -46,8 +46,10 @@ start-relayer() {
echo "Starting relayer with logs in ${log_file}"
./bin/relayer \
--deploy \
--endpoint="http://localhost:${GANACHE_PORT}" \
--data-dir="${SWAP_TEST_DATA_DIR}" \
--ethereum-endpoint="http://localhost:${GANACHE_PORT}" \
--log-level=debug \
--rpc \
--rpc-port="${RELAYER_PORT}" \
--key="${SWAP_TEST_DATA_DIR}/relayer/eth.key" \
&>"${log_file}" &