Skip to content

Commit

Permalink
feat: add transfer methods to swapcli (#462)
Browse files Browse the repository at this point in the history
  • Loading branch information
noot authored May 4, 2023
1 parent f95e916 commit 8f566b6
Show file tree
Hide file tree
Showing 7 changed files with 367 additions and 0 deletions.
159 changes: 159 additions & 0 deletions cmd/swapcli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -44,6 +46,9 @@ const (
flagSearchTime = "search-time"
flagToken = "token"
flagDetached = "detached"
flagTo = "to"
flagAmount = "amount"
flagEnv = "env"
)

func cliApp() *cli.App {
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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

Expand Down
48 changes: 48 additions & 0 deletions ethereum/extethclient/eth_wallet_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down
34 changes: 34 additions & 0 deletions protocol/backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
12 changes: 12 additions & 0 deletions rpc/mocks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
69 changes: 69 additions & 0 deletions rpc/personal.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
Loading

0 comments on commit 8f566b6

Please sign in to comment.