Files
atomic-swap/cmd/swapcli/main.go

960 lines
23 KiB
Go

// Copyright 2023 The AthanorLabs/atomic-swap Authors
// SPDX-License-Identifier: LGPL-3.0-only
// Package main provides the entrypoint of swapcli, an executable for interacting with a
// local swapd instance from the command line.
package main
import (
"fmt"
"os"
"strconv"
"strings"
"time"
"github.com/cockroachdb/apd/v3"
ethcommon "github.com/ethereum/go-ethereum/common"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/skip2/go-qrcode"
"github.com/urfave/cli/v2"
"github.com/athanorlabs/atomic-swap/cliutil"
"github.com/athanorlabs/atomic-swap/coins"
"github.com/athanorlabs/atomic-swap/common"
"github.com/athanorlabs/atomic-swap/common/rpctypes"
"github.com/athanorlabs/atomic-swap/common/types"
"github.com/athanorlabs/atomic-swap/net"
"github.com/athanorlabs/atomic-swap/rpcclient"
"github.com/athanorlabs/atomic-swap/rpcclient/wsclient"
)
const (
defaultDiscoverSearchTimeSecs = 12
flagSwapdPort = "swapd-port"
flagMinAmount = "min-amount"
flagMaxAmount = "max-amount"
flagPeerID = "peer-id"
flagOfferID = "offer-id"
flagOfferIDs = "offer-ids"
flagExchangeRate = "exchange-rate"
flagProvides = "provides"
flagProvidesAmount = "provides-amount"
flagUseRelayer = "use-relayer"
flagSearchTime = "search-time"
flagToken = "token"
flagDetached = "detached"
)
func cliApp() *cli.App {
return &cli.App{
Name: "swapcli",
Usage: "Client for swapd",
Version: cliutil.GetVersion(),
EnableBashCompletion: true,
Suggest: true,
Commands: []*cli.Command{
{
Name: "addresses",
Aliases: []string{"a"},
Usage: "List our daemon's libp2p listening addresses",
Action: runAddresses,
Flags: []cli.Flag{
swapdPortFlag,
},
},
{
Name: "peers",
Aliases: []string{"p"},
Usage: "List peers that are currently connected",
Action: runPeers,
Flags: []cli.Flag{
swapdPortFlag,
},
},
{
Name: "balances",
Aliases: []string{"b"},
Usage: "Show our monero and ethereum account balances",
Action: runBalances,
Flags: []cli.Flag{
swapdPortFlag,
&cli.StringSliceFlag{
Name: flagToken,
Aliases: []string{"t"},
EnvVars: []string{"SWAPCLI_TOKENS"},
Usage: "Token address to include in the balance response",
},
},
},
{
Name: "eth-address",
Usage: "Show our ethereum address with its QR code",
Action: runETHAddress,
Flags: []cli.Flag{
swapdPortFlag,
},
},
{
Name: "xmr-address",
Usage: "Show our Monero address with its QR code",
Action: runXMRAddress,
Flags: []cli.Flag{
swapdPortFlag,
},
},
{
Name: "discover",
Aliases: []string{"d"},
Usage: "Discover peers who provide a certain coin",
Action: runDiscover,
Flags: []cli.Flag{
&cli.StringFlag{
Name: flagProvides,
Usage: fmt.Sprintf("Search for %q or %q providers",
coins.ProvidesXMR, net.RelayerProvidesStr),
Value: string(coins.ProvidesXMR),
},
&cli.Uint64Flag{
Name: flagSearchTime,
Usage: "Duration of time to search for, in seconds",
Value: defaultDiscoverSearchTimeSecs,
},
swapdPortFlag,
},
},
{
Name: "query",
Aliases: []string{"q"},
Usage: "Query a peer for details on what they provide",
Action: runQuery,
Flags: []cli.Flag{
&cli.StringFlag{
Name: flagPeerID,
Usage: "Peer's ID, as provided by discover",
Required: true,
},
swapdPortFlag,
},
},
{
Name: "query-all",
Aliases: []string{"qall"},
Usage: "discover peers that provide a certain coin and their offers",
Action: runQueryAll,
Flags: []cli.Flag{
&cli.StringFlag{
Name: flagProvides,
Usage: fmt.Sprintf("Coin to find providers for: one of [%s, %s]",
coins.ProvidesXMR, coins.ProvidesETH),
Value: string(coins.ProvidesXMR),
},
&cli.Uint64Flag{
Name: flagSearchTime,
Usage: "Duration of time to search for, in seconds",
Value: defaultDiscoverSearchTimeSecs,
},
swapdPortFlag,
},
},
{
Name: "make",
Aliases: []string{"m"},
Usage: "Make a swap offer; currently monero holders must be the makers",
Action: runMake,
Flags: []cli.Flag{
&cli.StringFlag{
Name: flagMinAmount,
Usage: "Minimum amount to be swapped, in XMR",
Required: true,
},
&cli.StringFlag{
Name: flagMaxAmount,
Usage: "Maximum amount to be swapped, in XMR",
Required: true,
},
&cli.StringFlag{
Name: flagExchangeRate,
Usage: "Desired exchange rate of XMR:ETH, eg. --exchange-rate=0.1 means 10XMR = 1ETH",
Required: true,
},
&cli.BoolFlag{
Name: flagDetached,
Usage: "Exit immediately instead of subscribing to notifications about the swap's status",
},
&cli.StringFlag{
Name: flagToken,
Usage: "Use to pass the ethereum ERC20 token address to receive instead of ETH",
},
&cli.BoolFlag{
Name: flagUseRelayer,
Usage: "Use the relayer even if the receiving account has enough ETH to claim",
},
swapdPortFlag,
},
},
{
Name: "take",
Aliases: []string{"t"},
Usage: "Initiate a swap by taking an offer; currently only eth holders can be the takers",
Action: runTake,
Flags: []cli.Flag{
&cli.StringFlag{
Name: flagPeerID,
Usage: "Peer's ID, as provided by discover",
Required: true,
},
&cli.StringFlag{
Name: flagOfferID,
Usage: "ID of the offer being taken",
Required: true,
},
&cli.StringFlag{
Name: flagProvidesAmount,
Usage: "Amount of coin to send in the swap",
Required: true,
},
&cli.BoolFlag{
Name: flagDetached,
Usage: "Exit immediately instead of subscribing to notifications about the swap's status",
},
swapdPortFlag,
},
},
{
Name: "ongoing",
Usage: "Get information about ongoing swap(s).",
Action: runGetOngoingSwap,
Flags: []cli.Flag{
&cli.StringFlag{
Name: flagOfferID,
Usage: "ID of swap to retrieve info for",
},
swapdPortFlag,
},
},
{
Name: "past",
Usage: "Get information about past swap(s)",
Action: runGetPastSwap,
Flags: []cli.Flag{
&cli.StringFlag{
Name: flagOfferID,
Usage: "ID of swap to retrieve info for",
},
swapdPortFlag,
},
},
{
Name: "cancel",
Usage: "Cancel a ongoing swap if possible. Depending on the swap stage, this may not be possible.",
Action: runCancel,
Flags: []cli.Flag{
&cli.StringFlag{
Name: flagOfferID,
Usage: "ID of swap to retrieve info for",
},
swapdPortFlag,
},
},
{
Name: "clear-offers",
Usage: "Clear current offers. If no offer IDs are provided, clears all current offers.",
Action: runClearOffers,
Flags: []cli.Flag{
&cli.StringFlag{
Name: flagOfferIDs,
Usage: "A comma-separated list of offer IDs to delete",
},
swapdPortFlag,
},
},
{
Name: "get-offers",
Usage: "Get all current offers.",
Action: runGetOffers,
Flags: []cli.Flag{
swapdPortFlag,
},
},
{
Name: "get-status",
Usage: "Get the status of a current swap.",
Action: runGetStatus,
Flags: []cli.Flag{
&cli.StringFlag{
Name: flagOfferID,
Usage: "ID of swap to retrieve info for",
Required: true,
},
swapdPortFlag,
},
},
{
Name: "set-swap-timeout",
Usage: "Set the duration between swap initiation and t0 and t0 and t1, in seconds",
Action: runSetSwapTimeout,
Flags: []cli.Flag{
&cli.UintFlag{
Name: "duration",
Usage: "Duration of timeout, in seconds",
Required: true,
},
swapdPortFlag,
},
},
{
Name: "suggested-exchange-rate",
Usage: "Returns the current mainnet exchange rate based on ETH/USD and XMR/USD price feeds.",
Action: runSuggestedExchangeRate,
Flags: []cli.Flag{swapdPortFlag},
},
{
Name: "get-swap-timeout",
Usage: "Get the duration between swap initiation and t0 and t0 and t1, in seconds",
Action: runGetSwapTimeout,
Flags: []cli.Flag{
swapdPortFlag,
},
},
{
Name: "version",
Usage: "Get the client and server versions",
Action: runGetVersions,
Flags: []cli.Flag{
swapdPortFlag,
},
},
{
Name: "shutdown",
Usage: "Shutdown swapd",
Action: runShutdown,
Flags: []cli.Flag{
swapdPortFlag,
},
},
},
}
}
var (
swapdPortFlag = &cli.UintFlag{
Name: flagSwapdPort,
Aliases: []string{"p"},
Usage: "RPC port of swap daemon",
Value: common.DefaultSwapdPort,
EnvVars: []string{"SWAPD_PORT"},
}
)
func main() {
if err := cliApp().Run(os.Args); err != nil {
_, _ = fmt.Fprintf(os.Stderr, "%s\n", err)
os.Exit(1)
}
}
func newRRPClient(ctx *cli.Context) *rpcclient.Client {
swapdPort := ctx.Uint(flagSwapdPort)
endpoint := fmt.Sprintf("http://127.0.0.1:%d", swapdPort)
return rpcclient.NewClient(ctx.Context, endpoint)
}
func newWSClient(ctx *cli.Context) (wsclient.WsClient, error) {
swapdPort := ctx.Uint(flagSwapdPort)
endpoint := fmt.Sprintf("ws://127.0.0.1:%d/ws", swapdPort)
return wsclient.NewWsClient(ctx.Context, endpoint)
}
func runAddresses(ctx *cli.Context) error {
c := newRRPClient(ctx)
resp, err := c.Addresses()
if err != nil {
return err
}
fmt.Println("Local listening multi-addresses:")
for i, a := range resp.Addrs {
fmt.Printf("%d: %s\n", i+1, a)
}
if len(resp.Addrs) == 0 {
fmt.Println("[none]")
}
return nil
}
func runPeers(ctx *cli.Context) error {
c := newRRPClient(ctx)
resp, err := c.Peers()
if err != nil {
return err
}
fmt.Println("Connected peer multi-addresses:")
for i, a := range resp.Addrs {
fmt.Printf("%d: %s\n", i+1, a)
}
if len(resp.Addrs) == 0 {
fmt.Println("[none]")
}
return nil
}
func runBalances(ctx *cli.Context) error {
c := newRRPClient(ctx)
request := &rpctypes.BalancesRequest{}
tokens := ctx.StringSlice(flagToken)
for _, tokenAddr := range tokens {
if !ethcommon.IsHexAddress(tokenAddr) {
return fmt.Errorf("invalid token address: %q", tokenAddr)
}
request.TokenAddrs = append(request.TokenAddrs, ethcommon.HexToAddress(tokenAddr))
}
balances, err := c.Balances(request)
if err != nil {
return err
}
fmt.Printf("Ethereum address: %s\n", balances.EthAddress)
fmt.Printf("ETH Balance: %s\n", balances.WeiBalance.AsEtherString())
fmt.Println()
for _, tokenBalance := range balances.TokenBalances {
fmt.Printf("Token: %s\n", tokenBalance.TokenInfo.Address)
fmt.Printf("Name: %q\n", tokenBalance.TokenInfo.Name)
fmt.Printf("Symbol: %q\n", tokenBalance.TokenInfo.Symbol)
fmt.Printf("Balance: %s\n", tokenBalance.AsStandard().Text('f'))
fmt.Println()
}
fmt.Printf("Monero address: %s\n", balances.MoneroAddress)
fmt.Printf("XMR Balance: %s\n", balances.PiconeroBalance.AsMoneroString())
fmt.Printf("Unlocked XMR balance: %s\n",
balances.PiconeroUnlockedBalance.AsMoneroString())
fmt.Printf("Blocks to unlock: %d\n", balances.BlocksToUnlock)
return nil
}
func runETHAddress(ctx *cli.Context) error {
c := newRRPClient(ctx)
balances, err := c.Balances(nil)
if err != nil {
return err
}
fmt.Printf("Ethereum address: %s\n", balances.EthAddress)
code, err := qrcode.New(balances.EthAddress.String(), qrcode.Medium)
if err != nil {
return err
}
fmt.Println(code.ToString(false))
return nil
}
func runXMRAddress(ctx *cli.Context) error {
c := newRRPClient(ctx)
balances, err := c.Balances(nil)
if err != nil {
return err
}
fmt.Printf("Monero address: %s\n", balances.MoneroAddress)
code, err := qrcode.New(balances.MoneroAddress.String(), qrcode.Medium)
if err != nil {
return err
}
fmt.Println(code.ToString(true))
return nil
}
func runDiscover(ctx *cli.Context) error {
c := newRRPClient(ctx)
provides := ctx.String(flagProvides)
peerIDs, err := c.Discover(provides, ctx.Uint64(flagSearchTime))
if err != nil {
return err
}
for i, peerID := range peerIDs {
fmt.Printf("Peer %d: %v\n", i, peerID)
}
if len(peerIDs) == 0 {
fmt.Println("[none]")
}
return nil
}
func runQuery(ctx *cli.Context) error {
peerID, err := peer.Decode(ctx.String(flagPeerID))
if err != nil {
return errInvalidFlagValue(flagPeerID, err)
}
c := newRRPClient(ctx)
res, err := c.Query(peerID)
if err != nil {
return err
}
for i, o := range res.Offers {
err = printOffer(c, o, i, "")
if err != nil {
return err
}
}
return nil
}
func runQueryAll(ctx *cli.Context) error {
provides, err := providesStrToVal(ctx.String(flagProvides))
if err != nil {
return err
}
searchTime := ctx.Uint64(flagSearchTime)
c := newRRPClient(ctx)
peerOffers, err := c.QueryAll(provides, searchTime)
if err != nil {
return err
}
for i, po := range peerOffers {
if i > 0 {
fmt.Println("---")
}
fmt.Printf("Peer %d:\n", i)
fmt.Printf(" Peer ID: %v\n", po.PeerID)
fmt.Printf(" Offers:\n")
for j, o := range po.Offers {
err = printOffer(c, o, j, " ")
if err != nil {
return err
}
}
}
return nil
}
func runMake(ctx *cli.Context) error {
c := newRRPClient(ctx)
min, err := cliutil.ReadUnsignedDecimalFlag(ctx, flagMinAmount)
if err != nil {
return err
}
max, err := cliutil.ReadUnsignedDecimalFlag(ctx, flagMaxAmount)
if err != nil {
return err
}
ethAssetStr := ctx.String(flagToken)
ethAsset := types.EthAssetETH
if ethAssetStr != "" {
ethAsset = types.EthAsset(ethcommon.HexToAddress(ethAssetStr))
}
exchangeRateDec, err := cliutil.ReadUnsignedDecimalFlag(ctx, flagExchangeRate)
if err != nil {
return err
}
exchangeRate := coins.ToExchangeRate(exchangeRateDec)
var otherMin, otherMax *apd.Decimal
var symbol string
if ethAsset.IsETH() {
symbol = "ETH"
if otherMin, err = exchangeRate.ToETH(min); err != nil {
return err
}
if otherMax, err = exchangeRate.ToETH(max); err != nil {
return err
}
} else {
tokenInfo, err := c.TokenInfo(ethAsset.Address()) //nolint:govet
if err != nil {
return err
}
symbol = strconv.Quote(tokenInfo.Symbol)
if otherMin, err = exchangeRate.ToERC20Amount(min, tokenInfo); err != nil {
return err
}
if otherMax, err = exchangeRate.ToERC20Amount(max, tokenInfo); err != nil {
return err
}
}
printOfferSummary := func(offerResp *rpctypes.MakeOfferResponse) {
fmt.Println("Published:")
fmt.Printf("\tOffer ID: %s\n", offerResp.OfferID)
fmt.Printf("\tPeer ID: %s\n", offerResp.PeerID)
fmt.Printf("\tTaker Min: %s %s\n", otherMin.Text('f'), symbol)
fmt.Printf("\tTaker Max: %s %s\n", otherMax.Text('f'), symbol)
}
alwaysUseRelayer := ctx.Bool(flagUseRelayer)
if !ctx.Bool(flagDetached) {
wsc, err := newWSClient(ctx) //nolint:govet
if err != nil {
return err
}
defer wsc.Close()
resp, statusCh, err := wsc.MakeOfferAndSubscribe(
min,
max,
exchangeRate,
ethAsset,
alwaysUseRelayer,
)
if err != nil {
return err
}
printOfferSummary(resp)
for stage := range statusCh {
fmt.Printf("%s > Stage updated: %s\n", time.Now().Format(common.TimeFmtSecs), stage)
if !stage.IsOngoing() {
return nil
}
}
return nil
}
resp, err := c.MakeOffer(min, max, exchangeRate, ethAsset, alwaysUseRelayer)
if err != nil {
return err
}
printOfferSummary(resp)
return nil
}
func runTake(ctx *cli.Context) error {
peerID, err := peer.Decode(ctx.String(flagPeerID))
if err != nil {
return errInvalidFlagValue(flagPeerID, err)
}
offerID, err := types.HexToHash(ctx.String(flagOfferID))
if err != nil {
return errInvalidFlagValue(flagOfferID, err)
}
providesAmount, err := cliutil.ReadUnsignedDecimalFlag(ctx, flagProvidesAmount)
if err != nil {
return err
}
if !ctx.Bool(flagDetached) {
wsc, err := newWSClient(ctx)
if err != nil {
return err
}
defer wsc.Close()
statusCh, err := wsc.TakeOfferAndSubscribe(peerID, offerID, providesAmount)
if err != nil {
return err
}
fmt.Printf("Initiated swap with offer ID %s\n", offerID)
for stage := range statusCh {
fmt.Printf("%s > Stage updated: %s\n", time.Now().Format(common.TimeFmtSecs), stage)
if !stage.IsOngoing() {
return nil
}
}
return nil
}
c := newRRPClient(ctx)
if err := c.TakeOffer(peerID, offerID, providesAmount); err != nil {
return err
}
fmt.Printf("Initiated swap with offer ID %s\n", offerID)
return nil
}
func runGetOngoingSwap(ctx *cli.Context) error {
var offerID *types.Hash
if ctx.IsSet(flagOfferID) {
hash, err := types.HexToHash(ctx.String(flagOfferID))
if err != nil {
return errInvalidFlagValue(flagOfferID, err)
}
offerID = &hash
}
c := newRRPClient(ctx)
resp, err := c.GetOngoingSwap(offerID)
if err != nil {
return err
}
fmt.Println("Ongoing swaps:")
if len(resp.Swaps) == 0 {
fmt.Println("[none]")
return nil
}
for i, info := range resp.Swaps {
if i > 0 {
fmt.Printf("---\n")
}
providedCoin, receivedCoin, err := providedAndReceivedSymbols(c, info.Provided, info.EthAsset)
if err != nil {
return err
}
fmt.Printf("ID: %s\n", info.ID)
fmt.Printf("Start time: %s\n", info.StartTime.Format(common.TimeFmtSecs))
fmt.Printf("Provided: %s %s\n", info.ProvidedAmount.Text('f'), providedCoin)
fmt.Printf("Receiving: %s %s\n", info.ExpectedAmount.Text('f'), receivedCoin)
fmt.Printf("Exchange Rate: %s ETH/XMR\n", info.ExchangeRate)
fmt.Printf("Status: %s\n", info.Status)
fmt.Printf("Time status was last updated: %s\n", info.LastStatusUpdateTime.Format(common.TimeFmtSecs))
if info.Timeout0 != nil && info.Timeout1 != nil {
fmt.Printf("First timeout: %s\n", info.Timeout0.Format(common.TimeFmtSecs))
fmt.Printf("Second timeout: %s\n", info.Timeout1.Format(common.TimeFmtSecs))
}
fmt.Printf("Estimated time to completion: %s\n", info.EstimatedTimeToCompletion)
}
return nil
}
func runGetPastSwap(ctx *cli.Context) error {
var offerID *types.Hash
if ctx.IsSet(flagOfferID) {
hash, err := types.HexToHash(ctx.String(flagOfferID))
if err != nil {
return errInvalidFlagValue(flagOfferID, err)
}
offerID = &hash
}
c := newRRPClient(ctx)
resp, err := c.GetPastSwap(offerID)
if err != nil {
return err
}
fmt.Println("Past swaps:")
if len(resp.Swaps) == 0 {
fmt.Println("[none]")
return nil
}
for i, info := range resp.Swaps {
if i > 0 {
fmt.Printf("---\n")
}
providedCoin, receivedCoin, err := providedAndReceivedSymbols(c, info.Provided, info.EthAsset)
if err != nil {
return err
}
endTime := "-"
if info.EndTime != nil {
endTime = info.EndTime.Format(common.TimeFmtSecs)
}
fmt.Printf("ID: %s\n", info.ID)
fmt.Printf("Start time: %s\n", info.StartTime.Format(common.TimeFmtSecs))
fmt.Printf("End time: %s\n", endTime)
fmt.Printf("Provided: %s %s\n", info.ProvidedAmount.Text('f'), providedCoin)
fmt.Printf("Received: %s %s\n", info.ExpectedAmount.Text('f'), receivedCoin)
fmt.Printf("Exchange Rate: %s ETH/XMR\n", info.ExchangeRate)
fmt.Printf("Status: %s\n", info.Status)
}
return nil
}
func runCancel(ctx *cli.Context) error {
offerID, err := types.HexToHash(ctx.String(flagOfferID))
if err != nil {
return errInvalidFlagValue(flagOfferID, err)
}
c := newRRPClient(ctx)
fmt.Printf("Attempting to exit swap with id %s\n", offerID)
resp, err := c.Cancel(offerID)
if err != nil {
return err
}
fmt.Printf("Cancelled successfully, exit status: %s\n", resp)
return nil
}
func runClearOffers(ctx *cli.Context) error {
c := newRRPClient(ctx)
ids := ctx.String(flagOfferIDs)
if ids == "" {
err := c.ClearOffers(nil)
if err != nil {
return err
}
fmt.Printf("Cleared all offers successfully.\n")
return nil
}
var offerIDs []types.Hash
for _, offerIDStr := range strings.Split(ids, ",") {
id, err := types.HexToHash(strings.TrimSpace(offerIDStr))
if err != nil {
return errInvalidFlagValue(flagOfferIDs, err)
}
offerIDs = append(offerIDs, id)
}
err := c.ClearOffers(offerIDs)
if err != nil {
return err
}
fmt.Printf("Cleared offers successfully: %s\n", ids)
return nil
}
func runGetOffers(ctx *cli.Context) error {
c := newRRPClient(ctx)
resp, err := c.GetOffers()
if err != nil {
return err
}
fmt.Println("Peer ID (self):", resp.PeerID)
fmt.Println("Offers:")
for i, offer := range resp.Offers {
err = printOffer(c, offer, i, " ")
if err != nil {
return err
}
}
if len(resp.Offers) == 0 {
fmt.Println("[no offers]")
}
return nil
}
func runGetStatus(ctx *cli.Context) error {
offerID, err := types.HexToHash(ctx.String(flagOfferID))
if err != nil {
return errInvalidFlagValue(flagOfferID, err)
}
c := newRRPClient(ctx)
resp, err := c.GetStatus(offerID)
if err != nil {
return err
}
fmt.Printf("Start time: %s\n", resp.StartTime.Format(common.TimeFmtSecs))
fmt.Printf("Status=%s: %s\n", resp.Status, resp.Description)
return nil
}
func runSetSwapTimeout(ctx *cli.Context) error {
duration := ctx.Uint("duration")
if duration == 0 {
return errNoDuration
}
c := newRRPClient(ctx)
err := c.SetSwapTimeout(uint64(duration))
if err != nil {
return err
}
fmt.Printf("Set timeout duration to %d seconds\n", duration)
return nil
}
func runGetSwapTimeout(ctx *cli.Context) error {
c := newRRPClient(ctx)
resp, err := c.GetSwapTimeout()
if err != nil {
return err
}
fmt.Printf("Swap timeout duration: %d seconds\n", resp.Timeout)
return nil
}
func runSuggestedExchangeRate(ctx *cli.Context) error {
c := newRRPClient(ctx)
resp, err := c.SuggestedExchangeRate()
if err != nil {
return err
}
fmt.Printf("Exchange rate: %s\n", resp.ExchangeRate)
fmt.Printf("XMR/USD Price: %-13s (%s)\n", resp.XMRPrice, resp.XMRUpdatedAt)
fmt.Printf("ETH/USD Price: %-13s (%s)\n", resp.ETHPrice, resp.ETHUpdatedAt)
return nil
}
func runGetVersions(ctx *cli.Context) error {
fmt.Printf("swapcli: %s\n", cliutil.GetVersion())
c := newRRPClient(ctx)
resp, err := c.Version()
if err != nil {
return err
}
fmt.Printf("swapd: %s\n", resp.SwapdVersion)
fmt.Printf("p2p version: %s\n", resp.P2PVersion)
fmt.Printf("env: %s\n", resp.Env)
fmt.Printf("swap creator address: %s\n", resp.SwapCreatorAddr)
return nil
}
func runShutdown(ctx *cli.Context) error {
c := newRRPClient(ctx)
err := c.Shutdown()
if err != nil {
return err
}
return nil
}
func providesStrToVal(providesStr string) (coins.ProvidesCoin, error) {
var provides coins.ProvidesCoin
// The provides flag value defaults to XMR, but the user can still specify the empty
// string explicitly, which they can do to search the empty DHT namespace for all
// peers. `NewProvidesCoin` gives an error if you pass the empty string, so we
// special case the empty string.
if providesStr == "" {
return provides, nil
}
return coins.NewProvidesCoin(providesStr)
}