From 8f566b6fbe0a4d032cefca8dbdb6771070b6f51d Mon Sep 17 00:00:00 2001 From: noot <36753753+noot@users.noreply.github.com> Date: Thu, 4 May 2023 14:26:26 -0400 Subject: [PATCH] feat: add transfer methods to swapcli (#462) --- cmd/swapcli/main.go | 159 +++++++++++++++++++++ ethereum/extethclient/eth_wallet_client.go | 48 +++++++ protocol/backend/backend.go | 34 +++++ rpc/mocks_test.go | 12 ++ rpc/personal.go | 69 +++++++++ rpc/server.go | 3 + rpcclient/personal.go | 42 ++++++ 7 files changed, 367 insertions(+) diff --git a/cmd/swapcli/main.go b/cmd/swapcli/main.go index 8610d29e9..b63103662 100644 --- a/cmd/swapcli/main.go +++ b/cmd/swapcli/main.go @@ -23,7 +23,9 @@ import ( "github.com/athanorlabs/atomic-swap/common" "github.com/athanorlabs/atomic-swap/common/rpctypes" "github.com/athanorlabs/atomic-swap/common/types" + mcrypto "github.com/athanorlabs/atomic-swap/crypto/monero" "github.com/athanorlabs/atomic-swap/net" + "github.com/athanorlabs/atomic-swap/rpc" "github.com/athanorlabs/atomic-swap/rpcclient" "github.com/athanorlabs/atomic-swap/rpcclient/wsclient" ) @@ -44,6 +46,9 @@ const ( flagSearchTime = "search-time" flagToken = "token" flagDetached = "detached" + flagTo = "to" + flagAmount = "amount" + flagEnv = "env" ) func cliApp() *cli.App { @@ -318,6 +323,65 @@ func cliApp() *cli.App { swapdPortFlag, }, }, + { + Name: "transfer-xmr", + Usage: "Transfer XMR from the swap wallet to another address.", + Action: runTransferXMR, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: flagTo, + Usage: "Address to send XMR to", + Required: true, + }, + &cli.StringFlag{ + Name: flagAmount, + Usage: "Amount of XMR to send", + Required: true, + }, + &cli.StringFlag{ + Name: flagEnv, + Usage: "Environment to use. Options are [mainnet, stagenet, dev]. Default = mainnet.", + Value: "mainnet", + }, + swapdPortFlag, + }, + }, + { + Name: "sweep-xmr", + Usage: "Sweep all XMR from the swap wallet to another address.", + Action: runSweepXMR, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: flagTo, + Usage: "Address to send XMR to", + Required: true, + }, + &cli.StringFlag{ + Name: flagEnv, + Usage: "Environment to use. Options are [mainnet, stagenet, dev]. Default = mainnet.", + Value: "mainnet", + }, + swapdPortFlag, + }, + }, + { + Name: "transfer-eth", + Usage: "Transfer ETH from the swap wallet to another address.", + Action: runTransferETH, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: flagTo, + Usage: "Address to send ETH to", + Required: true, + }, + &cli.StringFlag{ + Name: flagAmount, + Usage: "Amount of ETH to send", + Required: true, + }, + swapdPortFlag, + }, + }, { Name: "version", Usage: "Get the client and server versions", @@ -1090,6 +1154,101 @@ func runGetSwapSecret(ctx *cli.Context) error { return nil } +func runTransferXMR(ctx *cli.Context) error { + env, err := common.NewEnv(ctx.String(flagEnv)) + if err != nil { + return err + } + + to, err := mcrypto.NewAddress(ctx.String(flagTo), env) + if err != nil { + return err + } + + amount, err := cliutil.ReadUnsignedDecimalFlag(ctx, flagAmount) + if err != nil { + return err + } + + c := newRRPClient(ctx) + req := &rpc.TransferXMRRequest{ + To: to, + Amount: amount, + } + + fmt.Printf("Transferring %s XMR to %s, waiting 1 block for confirmation\n", amount, to) + resp, err := c.TransferXMR(req) + if err != nil { + return err + } + + fmt.Printf("Transferred %s XMR to %s\n", amount, to) + fmt.Printf("Transaction ID: %s\n", resp.TxID) + return nil +} + +func runSweepXMR(ctx *cli.Context) error { + env, err := common.NewEnv(ctx.String(flagEnv)) + if err != nil { + return err + } + + to, err := mcrypto.NewAddress(ctx.String(flagTo), env) + if err != nil { + return err + } + + c := newRRPClient(ctx) + request := &rpctypes.BalancesRequest{} + balances, err := c.Balances(request) + if err != nil { + return err + } + + req := &rpc.SweepXMRRequest{ + To: to, + } + + fmt.Printf("Sweeping %s XMR to %s, waiting 1 block for confirmation\n", balances.PiconeroBalance.AsMoneroString(), to) + resp, err := c.SweepXMR(req) + if err != nil { + return err + } + + fmt.Printf("Transferred %s XMR to %s\n", balances.PiconeroBalance.AsMoneroString(), to) + fmt.Printf("Transaction IDs: %s\n", resp.TxIDs) + return nil +} + +func runTransferETH(ctx *cli.Context) error { + ok := ethcommon.IsHexAddress(ctx.String(flagTo)) + if !ok { + return fmt.Errorf("invalid address: %s", ctx.String(flagTo)) + } + + to := ethcommon.HexToAddress(ctx.String(flagTo)) + amount, err := cliutil.ReadUnsignedDecimalFlag(ctx, flagAmount) + if err != nil { + return err + } + + c := newRRPClient(ctx) + req := &rpc.TransferETHRequest{ + To: to, + Amount: amount, + } + + fmt.Printf("Transferring %s ETH to %s\n", amount, to) + resp, err := c.TransferETH(req) + if err != nil { + return err + } + + fmt.Printf("Transferred %s ETH to %s\n", amount, to) + fmt.Printf("Transaction ID: %s\n", resp.TxHash) + return nil +} + func providesStrToVal(providesStr string) (coins.ProvidesCoin, error) { var provides coins.ProvidesCoin diff --git a/ethereum/extethclient/eth_wallet_client.go b/ethereum/extethclient/eth_wallet_client.go index cbf5f0132..fc5f86060 100644 --- a/ethereum/extethclient/eth_wallet_client.go +++ b/ethereum/extethclient/eth_wallet_client.go @@ -51,6 +51,10 @@ type EthClient interface { Lock() // Lock the wallet so only one transaction runs at at time Unlock() // Unlock the wallet after a transaction is complete + // transfers ETH to the given address + // does not need locking, as it locks internally + Transfer(ctx context.Context, to ethcommon.Address, amount *coins.WeiAmount) (ethcommon.Hash, error) + WaitForReceipt(ctx context.Context, txHash ethcommon.Hash) (*ethtypes.Receipt, error) WaitForTimestamp(ctx context.Context, ts time.Time) error LatestBlockTimestamp(ctx context.Context) (time.Time, error) @@ -294,6 +298,50 @@ func (c *ethClient) Raw() *ethclient.Client { return c.ec } +func (c *ethClient) Transfer( + ctx context.Context, + to ethcommon.Address, + amount *coins.WeiAmount, +) (ethcommon.Hash, error) { + c.mu.Lock() + defer c.mu.Unlock() + + nonce, err := c.ec.NonceAt(ctx, c.ethAddress, nil) + if err != nil { + return ethcommon.Hash{}, fmt.Errorf("failed to get nonce: %w", err) + } + + // TODO: why does this type not implement ethtypes.TxData? seems like a bug in geth + // txData := ethtypes.DynamicFeeTx{ + // ChainID: c.chainID, + // Nonce: nonce, + // Gas: 21000, + // To: &to, + // Value: amount.BigInt(), + // } + // tx := ethtypes.NewTx(txData) + + gasPrice, err := c.ec.SuggestGasPrice(ctx) + if err != nil { + return ethcommon.Hash{}, fmt.Errorf("failed to get gas price: %w", err) + } + + tx := ethtypes.NewTransaction(nonce, to, amount.BigInt(), 21000, gasPrice, nil) + + signer := ethtypes.LatestSignerForChainID(c.chainID) + signedTx, err := ethtypes.SignTx(tx, signer, c.ethPrivKey) + if err != nil { + return ethcommon.Hash{}, fmt.Errorf("failed to sign tx: %w", err) + } + + err = c.ec.SendTransaction(ctx, signedTx) + if err != nil { + return ethcommon.Hash{}, fmt.Errorf("failed to send transaction: %w", err) + } + + return signedTx.Hash(), nil +} + func validateChainID(env common.Environment, chainID *big.Int) error { switch env { case common.Mainnet: diff --git a/protocol/backend/backend.go b/protocol/backend/backend.go index 393a7300f..ae9bf4782 100644 --- a/protocol/backend/backend.go +++ b/protocol/backend/backend.go @@ -17,6 +17,7 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/libp2p/go-libp2p/core/peer" + "github.com/athanorlabs/atomic-swap/coins" "github.com/athanorlabs/atomic-swap/common" "github.com/athanorlabs/atomic-swap/common/types" mcrypto "github.com/athanorlabs/atomic-swap/crypto/monero" @@ -92,6 +93,11 @@ type Backend interface { SetSwapTimeout(timeout time.Duration) SetXMRDepositAddress(*mcrypto.Address, types.Hash) ClearXMRDepositAddress(types.Hash) + + // transfer helpers + TransferXMR(to *mcrypto.Address, amount *coins.PiconeroAmount) (string, error) + SweepXMR(to *mcrypto.Address) ([]string, error) + TransferETH(to ethcommon.Address, amount *coins.WeiAmount) (types.Hash, error) } type backend struct { @@ -378,3 +384,31 @@ func (b *backend) SubmitClaimToRelayer( return b.SubmitRelayRequest(relayerID, req) } + +func (b *backend) TransferXMR(to *mcrypto.Address, amount *coins.PiconeroAmount) (string, error) { + res, err := b.moneroWallet.Transfer(b.ctx, to, 0, amount, 1) + if err != nil { + return "", err + } + + return res.TxID, nil + +} + +func (b *backend) SweepXMR(to *mcrypto.Address) ([]string, error) { + res, err := b.moneroWallet.SweepAll(b.ctx, to, 0, 1) + if err != nil { + return nil, err + } + + txIDs := make([]string, len(res)) + for i, transfer := range res { + txIDs[i] = transfer.TxID + } + + return txIDs, nil +} + +func (b *backend) TransferETH(to ethcommon.Address, amount *coins.WeiAmount) (types.Hash, error) { + return b.ethClient.Transfer(b.ctx, to, amount) +} diff --git a/rpc/mocks_test.go b/rpc/mocks_test.go index 840e47dd1..589fff831 100644 --- a/rpc/mocks_test.go +++ b/rpc/mocks_test.go @@ -237,3 +237,15 @@ func (*mockProtocolBackend) ETHClient() extethclient.EthClient { func (*mockProtocolBackend) SwapCreatorAddr() ethcommon.Address { panic("not implemented") } + +func (*mockProtocolBackend) TransferXMR(_ *mcrypto.Address, _ *coins.PiconeroAmount) (string, error) { + panic("not implemented") +} + +func (*mockProtocolBackend) SweepXMR(_ *mcrypto.Address) ([]string, error) { + panic("not implemented") +} + +func (*mockProtocolBackend) TransferETH(_ ethcommon.Address, _ *coins.WeiAmount) (ethcommon.Hash, error) { + panic("not implemented") +} diff --git a/rpc/personal.go b/rpc/personal.go index 911b72794..2e10bdeaf 100644 --- a/rpc/personal.go +++ b/rpc/personal.go @@ -11,6 +11,10 @@ import ( "github.com/athanorlabs/atomic-swap/coins" "github.com/athanorlabs/atomic-swap/common/rpctypes" + mcrypto "github.com/athanorlabs/atomic-swap/crypto/monero" + + "github.com/cockroachdb/apd/v3" + ethcommon "github.com/ethereum/go-ethereum/common" ) // PersonalService handles private keys and wallets. @@ -119,3 +123,68 @@ func (s *PersonalService) Balances( } return nil } + +// TransferXMRRequest ... +type TransferXMRRequest struct { + To *mcrypto.Address `json:"to" validate:"required"` + Amount *apd.Decimal `json:"amount" validate:"required"` +} + +// TransferXMRResponse ... +type TransferXMRResponse struct { + TxID string `json:"txID"` +} + +// TransferXMR transfers XMR from the swapd wallet. +func (s *PersonalService) TransferXMR(_ *http.Request, req *TransferXMRRequest, resp *TransferXMRResponse) error { + txID, err := s.pb.TransferXMR(req.To, coins.MoneroToPiconero(req.Amount)) + if err != nil { + return err + } + + resp.TxID = txID + return nil +} + +// SweepXMRRequest ... +type SweepXMRRequest struct { + To *mcrypto.Address `json:"to" validate:"required"` +} + +// SweepXMRResponse ... +type SweepXMRResponse struct { + TxIDs []string `json:"txIds"` +} + +// SweepXMR sweeps XMR from the swapd wallet. +func (s *PersonalService) SweepXMR(_ *http.Request, req *SweepXMRRequest, resp *SweepXMRResponse) error { + txIDs, err := s.pb.SweepXMR(req.To) + if err != nil { + return err + } + + resp.TxIDs = txIDs + return nil +} + +// TransferETHRequest ... +type TransferETHRequest struct { + To ethcommon.Address `json:"to" validate:"required"` + Amount *apd.Decimal `json:"amount" validate:"required"` +} + +// TransferETHResponse ... +type TransferETHResponse struct { + TxHash ethcommon.Hash `json:"txHash"` +} + +// TransferETH transfers ETH from the swapd wallet. +func (s *PersonalService) TransferETH(_ *http.Request, req *TransferETHRequest, resp *TransferETHResponse) error { + txHash, err := s.pb.TransferETH(req.To, coins.EtherToWei(req.Amount)) + if err != nil { + return err + } + + resp.TxHash = txHash + return nil +} diff --git a/rpc/server.go b/rpc/server.go index b823499bd..f7afd71b6 100644 --- a/rpc/server.go +++ b/rpc/server.go @@ -225,6 +225,9 @@ type ProtocolBackend interface { SetXMRDepositAddress(*mcrypto.Address, types.Hash) ClearXMRDepositAddress(types.Hash) ETHClient() extethclient.EthClient + TransferXMR(to *mcrypto.Address, amount *coins.PiconeroAmount) (string, error) + SweepXMR(to *mcrypto.Address) ([]string, error) + TransferETH(to ethcommon.Address, amount *coins.WeiAmount) (types.Hash, error) } // XMRTaker ... diff --git a/rpcclient/personal.go b/rpcclient/personal.go index b5e736f41..9b36f618f 100644 --- a/rpcclient/personal.go +++ b/rpcclient/personal.go @@ -72,3 +72,45 @@ func (c *Client) Balances(request *rpctypes.BalancesRequest) (*rpctypes.Balances return balances, nil } + +// TransferXMR calls personal_transferXMR +func (c *Client) TransferXMR(request *rpc.TransferXMRRequest) (*rpc.TransferXMRResponse, error) { + const ( + method = "personal_transferXMR" + ) + + resp := new(rpc.TransferXMRResponse) + if err := c.Post(method, request, resp); err != nil { + return nil, err + } + + return resp, nil +} + +// SweepXMR calls personal_sweepXMR +func (c *Client) SweepXMR(request *rpc.SweepXMRRequest) (*rpc.SweepXMRResponse, error) { + const ( + method = "personal_sweepXMR" + ) + + resp := new(rpc.SweepXMRResponse) + if err := c.Post(method, request, resp); err != nil { + return nil, err + } + + return resp, nil +} + +// TransferETH calls personal_transferETH +func (c *Client) TransferETH(request *rpc.TransferETHRequest) (*rpc.TransferETHResponse, error) { + const ( + method = "personal_transferETH" + ) + + resp := new(rpc.TransferETHResponse) + if err := c.Post(method, request, resp); err != nil { + return nil, err + } + + return resp, nil +}