Skip to content

Commit 8f566b6

Browse files
authored
feat: add transfer methods to swapcli (#462)
1 parent f95e916 commit 8f566b6

File tree

7 files changed

+367
-0
lines changed

7 files changed

+367
-0
lines changed

cmd/swapcli/main.go

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ import (
2323
"github.com/athanorlabs/atomic-swap/common"
2424
"github.com/athanorlabs/atomic-swap/common/rpctypes"
2525
"github.com/athanorlabs/atomic-swap/common/types"
26+
mcrypto "github.com/athanorlabs/atomic-swap/crypto/monero"
2627
"github.com/athanorlabs/atomic-swap/net"
28+
"github.com/athanorlabs/atomic-swap/rpc"
2729
"github.com/athanorlabs/atomic-swap/rpcclient"
2830
"github.com/athanorlabs/atomic-swap/rpcclient/wsclient"
2931
)
@@ -44,6 +46,9 @@ const (
4446
flagSearchTime = "search-time"
4547
flagToken = "token"
4648
flagDetached = "detached"
49+
flagTo = "to"
50+
flagAmount = "amount"
51+
flagEnv = "env"
4752
)
4853

4954
func cliApp() *cli.App {
@@ -318,6 +323,65 @@ func cliApp() *cli.App {
318323
swapdPortFlag,
319324
},
320325
},
326+
{
327+
Name: "transfer-xmr",
328+
Usage: "Transfer XMR from the swap wallet to another address.",
329+
Action: runTransferXMR,
330+
Flags: []cli.Flag{
331+
&cli.StringFlag{
332+
Name: flagTo,
333+
Usage: "Address to send XMR to",
334+
Required: true,
335+
},
336+
&cli.StringFlag{
337+
Name: flagAmount,
338+
Usage: "Amount of XMR to send",
339+
Required: true,
340+
},
341+
&cli.StringFlag{
342+
Name: flagEnv,
343+
Usage: "Environment to use. Options are [mainnet, stagenet, dev]. Default = mainnet.",
344+
Value: "mainnet",
345+
},
346+
swapdPortFlag,
347+
},
348+
},
349+
{
350+
Name: "sweep-xmr",
351+
Usage: "Sweep all XMR from the swap wallet to another address.",
352+
Action: runSweepXMR,
353+
Flags: []cli.Flag{
354+
&cli.StringFlag{
355+
Name: flagTo,
356+
Usage: "Address to send XMR to",
357+
Required: true,
358+
},
359+
&cli.StringFlag{
360+
Name: flagEnv,
361+
Usage: "Environment to use. Options are [mainnet, stagenet, dev]. Default = mainnet.",
362+
Value: "mainnet",
363+
},
364+
swapdPortFlag,
365+
},
366+
},
367+
{
368+
Name: "transfer-eth",
369+
Usage: "Transfer ETH from the swap wallet to another address.",
370+
Action: runTransferETH,
371+
Flags: []cli.Flag{
372+
&cli.StringFlag{
373+
Name: flagTo,
374+
Usage: "Address to send ETH to",
375+
Required: true,
376+
},
377+
&cli.StringFlag{
378+
Name: flagAmount,
379+
Usage: "Amount of ETH to send",
380+
Required: true,
381+
},
382+
swapdPortFlag,
383+
},
384+
},
321385
{
322386
Name: "version",
323387
Usage: "Get the client and server versions",
@@ -1090,6 +1154,101 @@ func runGetSwapSecret(ctx *cli.Context) error {
10901154
return nil
10911155
}
10921156

1157+
func runTransferXMR(ctx *cli.Context) error {
1158+
env, err := common.NewEnv(ctx.String(flagEnv))
1159+
if err != nil {
1160+
return err
1161+
}
1162+
1163+
to, err := mcrypto.NewAddress(ctx.String(flagTo), env)
1164+
if err != nil {
1165+
return err
1166+
}
1167+
1168+
amount, err := cliutil.ReadUnsignedDecimalFlag(ctx, flagAmount)
1169+
if err != nil {
1170+
return err
1171+
}
1172+
1173+
c := newRRPClient(ctx)
1174+
req := &rpc.TransferXMRRequest{
1175+
To: to,
1176+
Amount: amount,
1177+
}
1178+
1179+
fmt.Printf("Transferring %s XMR to %s, waiting 1 block for confirmation\n", amount, to)
1180+
resp, err := c.TransferXMR(req)
1181+
if err != nil {
1182+
return err
1183+
}
1184+
1185+
fmt.Printf("Transferred %s XMR to %s\n", amount, to)
1186+
fmt.Printf("Transaction ID: %s\n", resp.TxID)
1187+
return nil
1188+
}
1189+
1190+
func runSweepXMR(ctx *cli.Context) error {
1191+
env, err := common.NewEnv(ctx.String(flagEnv))
1192+
if err != nil {
1193+
return err
1194+
}
1195+
1196+
to, err := mcrypto.NewAddress(ctx.String(flagTo), env)
1197+
if err != nil {
1198+
return err
1199+
}
1200+
1201+
c := newRRPClient(ctx)
1202+
request := &rpctypes.BalancesRequest{}
1203+
balances, err := c.Balances(request)
1204+
if err != nil {
1205+
return err
1206+
}
1207+
1208+
req := &rpc.SweepXMRRequest{
1209+
To: to,
1210+
}
1211+
1212+
fmt.Printf("Sweeping %s XMR to %s, waiting 1 block for confirmation\n", balances.PiconeroBalance.AsMoneroString(), to)
1213+
resp, err := c.SweepXMR(req)
1214+
if err != nil {
1215+
return err
1216+
}
1217+
1218+
fmt.Printf("Transferred %s XMR to %s\n", balances.PiconeroBalance.AsMoneroString(), to)
1219+
fmt.Printf("Transaction IDs: %s\n", resp.TxIDs)
1220+
return nil
1221+
}
1222+
1223+
func runTransferETH(ctx *cli.Context) error {
1224+
ok := ethcommon.IsHexAddress(ctx.String(flagTo))
1225+
if !ok {
1226+
return fmt.Errorf("invalid address: %s", ctx.String(flagTo))
1227+
}
1228+
1229+
to := ethcommon.HexToAddress(ctx.String(flagTo))
1230+
amount, err := cliutil.ReadUnsignedDecimalFlag(ctx, flagAmount)
1231+
if err != nil {
1232+
return err
1233+
}
1234+
1235+
c := newRRPClient(ctx)
1236+
req := &rpc.TransferETHRequest{
1237+
To: to,
1238+
Amount: amount,
1239+
}
1240+
1241+
fmt.Printf("Transferring %s ETH to %s\n", amount, to)
1242+
resp, err := c.TransferETH(req)
1243+
if err != nil {
1244+
return err
1245+
}
1246+
1247+
fmt.Printf("Transferred %s ETH to %s\n", amount, to)
1248+
fmt.Printf("Transaction ID: %s\n", resp.TxHash)
1249+
return nil
1250+
}
1251+
10931252
func providesStrToVal(providesStr string) (coins.ProvidesCoin, error) {
10941253
var provides coins.ProvidesCoin
10951254

ethereum/extethclient/eth_wallet_client.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ type EthClient interface {
5151
Lock() // Lock the wallet so only one transaction runs at at time
5252
Unlock() // Unlock the wallet after a transaction is complete
5353

54+
// transfers ETH to the given address
55+
// does not need locking, as it locks internally
56+
Transfer(ctx context.Context, to ethcommon.Address, amount *coins.WeiAmount) (ethcommon.Hash, error)
57+
5458
WaitForReceipt(ctx context.Context, txHash ethcommon.Hash) (*ethtypes.Receipt, error)
5559
WaitForTimestamp(ctx context.Context, ts time.Time) error
5660
LatestBlockTimestamp(ctx context.Context) (time.Time, error)
@@ -294,6 +298,50 @@ func (c *ethClient) Raw() *ethclient.Client {
294298
return c.ec
295299
}
296300

301+
func (c *ethClient) Transfer(
302+
ctx context.Context,
303+
to ethcommon.Address,
304+
amount *coins.WeiAmount,
305+
) (ethcommon.Hash, error) {
306+
c.mu.Lock()
307+
defer c.mu.Unlock()
308+
309+
nonce, err := c.ec.NonceAt(ctx, c.ethAddress, nil)
310+
if err != nil {
311+
return ethcommon.Hash{}, fmt.Errorf("failed to get nonce: %w", err)
312+
}
313+
314+
// TODO: why does this type not implement ethtypes.TxData? seems like a bug in geth
315+
// txData := ethtypes.DynamicFeeTx{
316+
// ChainID: c.chainID,
317+
// Nonce: nonce,
318+
// Gas: 21000,
319+
// To: &to,
320+
// Value: amount.BigInt(),
321+
// }
322+
// tx := ethtypes.NewTx(txData)
323+
324+
gasPrice, err := c.ec.SuggestGasPrice(ctx)
325+
if err != nil {
326+
return ethcommon.Hash{}, fmt.Errorf("failed to get gas price: %w", err)
327+
}
328+
329+
tx := ethtypes.NewTransaction(nonce, to, amount.BigInt(), 21000, gasPrice, nil)
330+
331+
signer := ethtypes.LatestSignerForChainID(c.chainID)
332+
signedTx, err := ethtypes.SignTx(tx, signer, c.ethPrivKey)
333+
if err != nil {
334+
return ethcommon.Hash{}, fmt.Errorf("failed to sign tx: %w", err)
335+
}
336+
337+
err = c.ec.SendTransaction(ctx, signedTx)
338+
if err != nil {
339+
return ethcommon.Hash{}, fmt.Errorf("failed to send transaction: %w", err)
340+
}
341+
342+
return signedTx.Hash(), nil
343+
}
344+
297345
func validateChainID(env common.Environment, chainID *big.Int) error {
298346
switch env {
299347
case common.Mainnet:

protocol/backend/backend.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/ethereum/go-ethereum/crypto"
1818
"github.com/libp2p/go-libp2p/core/peer"
1919

20+
"github.com/athanorlabs/atomic-swap/coins"
2021
"github.com/athanorlabs/atomic-swap/common"
2122
"github.com/athanorlabs/atomic-swap/common/types"
2223
mcrypto "github.com/athanorlabs/atomic-swap/crypto/monero"
@@ -92,6 +93,11 @@ type Backend interface {
9293
SetSwapTimeout(timeout time.Duration)
9394
SetXMRDepositAddress(*mcrypto.Address, types.Hash)
9495
ClearXMRDepositAddress(types.Hash)
96+
97+
// transfer helpers
98+
TransferXMR(to *mcrypto.Address, amount *coins.PiconeroAmount) (string, error)
99+
SweepXMR(to *mcrypto.Address) ([]string, error)
100+
TransferETH(to ethcommon.Address, amount *coins.WeiAmount) (types.Hash, error)
95101
}
96102

97103
type backend struct {
@@ -378,3 +384,31 @@ func (b *backend) SubmitClaimToRelayer(
378384

379385
return b.SubmitRelayRequest(relayerID, req)
380386
}
387+
388+
func (b *backend) TransferXMR(to *mcrypto.Address, amount *coins.PiconeroAmount) (string, error) {
389+
res, err := b.moneroWallet.Transfer(b.ctx, to, 0, amount, 1)
390+
if err != nil {
391+
return "", err
392+
}
393+
394+
return res.TxID, nil
395+
396+
}
397+
398+
func (b *backend) SweepXMR(to *mcrypto.Address) ([]string, error) {
399+
res, err := b.moneroWallet.SweepAll(b.ctx, to, 0, 1)
400+
if err != nil {
401+
return nil, err
402+
}
403+
404+
txIDs := make([]string, len(res))
405+
for i, transfer := range res {
406+
txIDs[i] = transfer.TxID
407+
}
408+
409+
return txIDs, nil
410+
}
411+
412+
func (b *backend) TransferETH(to ethcommon.Address, amount *coins.WeiAmount) (types.Hash, error) {
413+
return b.ethClient.Transfer(b.ctx, to, amount)
414+
}

rpc/mocks_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,3 +237,15 @@ func (*mockProtocolBackend) ETHClient() extethclient.EthClient {
237237
func (*mockProtocolBackend) SwapCreatorAddr() ethcommon.Address {
238238
panic("not implemented")
239239
}
240+
241+
func (*mockProtocolBackend) TransferXMR(_ *mcrypto.Address, _ *coins.PiconeroAmount) (string, error) {
242+
panic("not implemented")
243+
}
244+
245+
func (*mockProtocolBackend) SweepXMR(_ *mcrypto.Address) ([]string, error) {
246+
panic("not implemented")
247+
}
248+
249+
func (*mockProtocolBackend) TransferETH(_ ethcommon.Address, _ *coins.WeiAmount) (ethcommon.Hash, error) {
250+
panic("not implemented")
251+
}

rpc/personal.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ import (
1111

1212
"github.com/athanorlabs/atomic-swap/coins"
1313
"github.com/athanorlabs/atomic-swap/common/rpctypes"
14+
mcrypto "github.com/athanorlabs/atomic-swap/crypto/monero"
15+
16+
"github.com/cockroachdb/apd/v3"
17+
ethcommon "github.com/ethereum/go-ethereum/common"
1418
)
1519

1620
// PersonalService handles private keys and wallets.
@@ -119,3 +123,68 @@ func (s *PersonalService) Balances(
119123
}
120124
return nil
121125
}
126+
127+
// TransferXMRRequest ...
128+
type TransferXMRRequest struct {
129+
To *mcrypto.Address `json:"to" validate:"required"`
130+
Amount *apd.Decimal `json:"amount" validate:"required"`
131+
}
132+
133+
// TransferXMRResponse ...
134+
type TransferXMRResponse struct {
135+
TxID string `json:"txID"`
136+
}
137+
138+
// TransferXMR transfers XMR from the swapd wallet.
139+
func (s *PersonalService) TransferXMR(_ *http.Request, req *TransferXMRRequest, resp *TransferXMRResponse) error {
140+
txID, err := s.pb.TransferXMR(req.To, coins.MoneroToPiconero(req.Amount))
141+
if err != nil {
142+
return err
143+
}
144+
145+
resp.TxID = txID
146+
return nil
147+
}
148+
149+
// SweepXMRRequest ...
150+
type SweepXMRRequest struct {
151+
To *mcrypto.Address `json:"to" validate:"required"`
152+
}
153+
154+
// SweepXMRResponse ...
155+
type SweepXMRResponse struct {
156+
TxIDs []string `json:"txIds"`
157+
}
158+
159+
// SweepXMR sweeps XMR from the swapd wallet.
160+
func (s *PersonalService) SweepXMR(_ *http.Request, req *SweepXMRRequest, resp *SweepXMRResponse) error {
161+
txIDs, err := s.pb.SweepXMR(req.To)
162+
if err != nil {
163+
return err
164+
}
165+
166+
resp.TxIDs = txIDs
167+
return nil
168+
}
169+
170+
// TransferETHRequest ...
171+
type TransferETHRequest struct {
172+
To ethcommon.Address `json:"to" validate:"required"`
173+
Amount *apd.Decimal `json:"amount" validate:"required"`
174+
}
175+
176+
// TransferETHResponse ...
177+
type TransferETHResponse struct {
178+
TxHash ethcommon.Hash `json:"txHash"`
179+
}
180+
181+
// TransferETH transfers ETH from the swapd wallet.
182+
func (s *PersonalService) TransferETH(_ *http.Request, req *TransferETHRequest, resp *TransferETHResponse) error {
183+
txHash, err := s.pb.TransferETH(req.To, coins.EtherToWei(req.Amount))
184+
if err != nil {
185+
return err
186+
}
187+
188+
resp.TxHash = txHash
189+
return nil
190+
}

0 commit comments

Comments
 (0)