github.com/mysteriumnetwork/node@v0.0.0-20240516044423-365054f76801/cmd/commands/account/command.go (about)

     1  /*
     2   * Copyright (C) 2021 The "MysteriumNetwork/node" Authors.
     3   *
     4   * This program is free software: you can redistribute it and/or modify
     5   * it under the terms of the GNU General Public License as published by
     6   * the Free Software Foundation, either version 3 of the License, or
     7   * (at your option) any later version.
     8   *
     9   * This program is distributed in the hope that it will be useful,
    10   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    11   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    12   * GNU General Public License for more details.
    13   *
    14   * You should have received a copy of the GNU General Public License
    15   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    16   */
    17  
    18  package account
    19  
    20  import (
    21  	"encoding/json"
    22  	"errors"
    23  	"fmt"
    24  	"strconv"
    25  	"strings"
    26  
    27  	"github.com/mysteriumnetwork/payments/exchange"
    28  	"github.com/urfave/cli/v2"
    29  
    30  	"github.com/mysteriumnetwork/node/cmd/commands/cli/clio"
    31  	"github.com/mysteriumnetwork/node/config"
    32  	"github.com/mysteriumnetwork/node/config/remote"
    33  	"github.com/mysteriumnetwork/node/identity"
    34  	"github.com/mysteriumnetwork/node/identity/registry"
    35  	"github.com/mysteriumnetwork/node/money"
    36  	tequilapi_client "github.com/mysteriumnetwork/node/tequilapi/client"
    37  	"github.com/mysteriumnetwork/node/tequilapi/contract"
    38  )
    39  
    40  // CommandName is the name of the main command.
    41  const CommandName = "account"
    42  
    43  var (
    44  	flagAmount = cli.Float64Flag{
    45  		Name:  "amount",
    46  		Usage: "Amount of MYST for the top-up",
    47  	}
    48  
    49  	flagCurrency = cli.StringFlag{
    50  		Name:  "currency",
    51  		Usage: "Currency you want to use when paying for your top-up",
    52  	}
    53  
    54  	flagGateway = cli.StringFlag{
    55  		Name:  "gateway",
    56  		Usage: "Gateway to use",
    57  	}
    58  
    59  	flagGwData = cli.StringFlag{
    60  		Name:  "data",
    61  		Usage: "Data required to use the gateway",
    62  	}
    63  
    64  	flagLastTopup = cli.BoolFlag{
    65  		Name:  "last-topup",
    66  		Usage: "Include last top-up information",
    67  	}
    68  
    69  	flagToken = cli.StringFlag{
    70  		Name:  "token",
    71  		Usage: "Either a referral or affiliate token which can be used when registering",
    72  	}
    73  
    74  	flagCountry = cli.StringFlag{
    75  		Name:  "country",
    76  		Usage: "Country code",
    77  	}
    78  
    79  	flagState = cli.StringFlag{
    80  		Name:  "state",
    81  		Usage: "State code",
    82  	}
    83  )
    84  
    85  // NewCommand function creates license command.
    86  func NewCommand() *cli.Command {
    87  	var cmd *command
    88  
    89  	return &cli.Command{
    90  		Name:        CommandName,
    91  		Usage:       "Manage your account",
    92  		Description: "Using account subcommands you can manage your account details and get information about it",
    93  		Flags:       []cli.Flag{&config.FlagTequilapiAddress, &config.FlagTequilapiPort},
    94  		Before: func(ctx *cli.Context) error {
    95  			tc, err := clio.NewTequilApiClient(ctx)
    96  			if err != nil {
    97  				return err
    98  			}
    99  
   100  			cfg, err := remote.NewConfig(tc)
   101  			if err != nil {
   102  				return err
   103  			}
   104  
   105  			cmd = &command{
   106  				tequilapi: tc,
   107  				cfg:       cfg,
   108  			}
   109  			return nil
   110  		},
   111  		Subcommands: []*cli.Command{
   112  			{
   113  				Name:  "register",
   114  				Usage: "Submit a registration request",
   115  				Flags: []cli.Flag{&flagToken},
   116  				Action: func(ctx *cli.Context) error {
   117  					cmd.register(ctx)
   118  					return nil
   119  				},
   120  			},
   121  			{
   122  				Name:  "topup",
   123  				Usage: "Create a new top-up for your account",
   124  				Flags: []cli.Flag{&flagAmount, &flagCurrency, &flagGateway, &flagGwData, &flagCountry, &flagState},
   125  				Action: func(ctx *cli.Context) error {
   126  					cmd.topup(ctx)
   127  					return nil
   128  				},
   129  			},
   130  			{
   131  				Name:  "info",
   132  				Usage: "Display information about identity account currently in use",
   133  				Flags: []cli.Flag{&flagLastTopup},
   134  				Action: func(ctx *cli.Context) error {
   135  					cmd.info(ctx)
   136  					return nil
   137  				},
   138  			},
   139  			{
   140  				Name:      "set-identity",
   141  				Usage:     "Sets a new identity for your account which will be used in commands that require it",
   142  				ArgsUsage: "[IdentityAddress]",
   143  				Action: func(ctx *cli.Context) error {
   144  					cmd.setIdentity(ctx)
   145  					return nil
   146  				},
   147  			},
   148  		},
   149  	}
   150  }
   151  
   152  type command struct {
   153  	tequilapi *tequilapi_client.Client
   154  	cfg       *remote.Config
   155  }
   156  
   157  func (c *command) setIdentity(ctx *cli.Context) {
   158  	givenID := ctx.Args().First()
   159  	if givenID == "" {
   160  		clio.Warn("No identity provided")
   161  		return
   162  	}
   163  
   164  	if _, err := c.tequilapi.CurrentIdentity(givenID, ""); err != nil {
   165  		clio.Error("Failed to set identity as default")
   166  		return
   167  	}
   168  
   169  	clio.Success(fmt.Sprintf("Identity %s set as default", givenID))
   170  }
   171  
   172  func (c *command) info(ctx *cli.Context) {
   173  	id, err := c.tequilapi.CurrentIdentity("", "")
   174  	if err != nil {
   175  		clio.Error("Failed to display information: could not get current identity")
   176  		return
   177  	}
   178  
   179  	clio.Status("SECTION", "General account information:")
   180  	c.infoGeneral(id.Address)
   181  
   182  	if ctx.Bool(flagLastTopup.Name) {
   183  		clio.Status("SECTION", "Last top-up information:")
   184  		c.infoTopUp(id.Address)
   185  	}
   186  }
   187  
   188  func (c *command) topup(ctx *cli.Context) {
   189  	id, err := c.tequilapi.CurrentIdentity("", "")
   190  	if err != nil {
   191  		clio.Warn("Could not get your identity")
   192  		return
   193  	}
   194  
   195  	ok, err := c.identityIsUnregistered(id.Address)
   196  	if err != nil {
   197  		clio.Error(err.Error())
   198  		return
   199  	}
   200  
   201  	if ok {
   202  		clio.Warn("Your identity is not registered, please execute `myst account register` first")
   203  		return
   204  	}
   205  
   206  	gws, err := c.tequilapi.PaymentOrderGateways(exchange.CurrencyMYST)
   207  	if err != nil {
   208  		clio.Info("Failed to get enabled gateways and their information")
   209  	}
   210  
   211  	gatewayName := ctx.String(flagGateway.Name)
   212  
   213  	gw, ok := findGateway(gatewayName, gws)
   214  	if !ok {
   215  		clio.Error("Can't continue, no such gateway:", gatewayName)
   216  		return
   217  	}
   218  
   219  	amount := ctx.String(flagAmount.Name)
   220  
   221  	amountF, err := strconv.ParseFloat(amount, 64)
   222  	if err != nil || amountF <= 0 {
   223  		clio.Warn("Top-up amount is required and must be greater than 0")
   224  		return
   225  	}
   226  
   227  	if gw.OrderOptions.Minimum != 0 && amountF <= gw.OrderOptions.Minimum {
   228  		clio.Error(
   229  			fmt.Sprintf(
   230  				"Top-up amount must be greater than %v%s",
   231  				gw.OrderOptions.Minimum,
   232  				config.GetString(config.FlagDefaultCurrency)))
   233  		return
   234  	}
   235  
   236  	currency := ctx.String(flagCurrency.Name)
   237  	if !contains(currency, gw.Currencies) {
   238  		clio.Warn("Given currency cannot be used")
   239  		clio.Info("Supported currencies are:", strings.Join(gw.Currencies, ", "))
   240  		return
   241  	}
   242  
   243  	country := ctx.String(flagCountry.Name)
   244  	if country == "" {
   245  		clio.Warn("Country is required")
   246  		return
   247  	} else if len(country) != 2 {
   248  		clio.Warn("Country code must be 2 characters long")
   249  		return
   250  	}
   251  
   252  	state := ctx.String(flagState.Name)
   253  	if state != "" && len(state) != 2 {
   254  		clio.Warn("State code must be 2 characters long")
   255  		return
   256  	}
   257  
   258  	callerData := json.RawMessage("{}")
   259  	if gwData := ctx.String(flagGwData.Name); len(gwData) > 0 {
   260  		data := map[string]interface{}{}
   261  		parts := strings.Split(gwData, ",")
   262  		for _, part := range parts {
   263  			kv := strings.Split(part, "=")
   264  			if len(kv) != 2 {
   265  				clio.Error("Gateway data wrong, example: lightning_network=true,custom_id=123")
   266  				return
   267  			}
   268  
   269  			if b, err := strconv.ParseBool(kv[1]); err == nil {
   270  				data[kv[0]] = b
   271  				continue
   272  			}
   273  
   274  			if b, err := strconv.ParseFloat(kv[1], 64); err == nil {
   275  				data[kv[0]] = b
   276  				continue
   277  			}
   278  
   279  			data[kv[0]] = kv[1]
   280  		}
   281  
   282  		callerData, err = json.Marshal(data)
   283  		if err != nil {
   284  			clio.Error("Failed to make caller data")
   285  			return
   286  		}
   287  	}
   288  
   289  	resp, err := c.tequilapi.OrderCreate(identity.FromAddress(id.Address), gatewayName, contract.PaymentOrderRequest{
   290  		MystAmount:  amount,
   291  		PayCurrency: currency,
   292  		CallerData:  callerData,
   293  		Country:     country,
   294  		State:       state,
   295  	})
   296  	if err != nil {
   297  		clio.Error("Failed to create a top-up request, make sure your requested amount is equal or more than 0.0001 BTC")
   298  		return
   299  	}
   300  
   301  	clio.Info(fmt.Sprintf("New top-up request for identity %s has been created: ", id.Address))
   302  	printOrder(resp)
   303  }
   304  
   305  func (c *command) register(ctx *cli.Context) {
   306  	id, err := c.tequilapi.CurrentIdentity("", "")
   307  	if err != nil {
   308  		clio.Warn("Could not get or create identity")
   309  		return
   310  	}
   311  
   312  	ok, err := c.identityIsUnregistered(id.Address)
   313  	if err != nil {
   314  		clio.Error(err.Error())
   315  		return
   316  	}
   317  	if !ok {
   318  		clio.Infof("Already have an identity: %s\n", id.Address)
   319  		return
   320  	}
   321  
   322  	if err := c.tequilapi.Unlock(id.Address, ""); err != nil {
   323  		clio.Warn("Failed to unlock the identity")
   324  		return
   325  	}
   326  
   327  	c.registerIdentity(id.Address, c.parseToken(ctx))
   328  }
   329  
   330  func (c *command) parseToken(ctx *cli.Context) *string {
   331  	if val := ctx.String(flagToken.Name); val != "" {
   332  		return &val
   333  	}
   334  
   335  	return nil
   336  }
   337  
   338  func (c *command) registerIdentity(identity string, token *string) {
   339  	err := c.tequilapi.RegisterIdentity(identity, "", token)
   340  	if err != nil {
   341  		clio.Error("Failed to register the identity")
   342  		return
   343  	}
   344  
   345  	msg := "Registration started. Top up the identities channel to finish it."
   346  	clio.Success(msg)
   347  }
   348  
   349  func (c *command) identityIsUnregistered(identityAddress string) (bool, error) {
   350  	identityStatus, err := c.tequilapi.Identity(identityAddress)
   351  	if err != nil {
   352  		return false, errors.New("failed to get identity status")
   353  	}
   354  
   355  	return identityStatus.RegistrationStatus == registry.Unregistered.String(), nil
   356  }
   357  
   358  func (c *command) infoGeneral(identityAddress string) {
   359  	identityStatus, err := c.tequilapi.Identity(identityAddress)
   360  	if err != nil {
   361  		clio.Error("Failed to display general account information: failed to fetch data")
   362  		return
   363  	}
   364  
   365  	clio.Info("Using identity:", identityAddress)
   366  	clio.Info("Registration Status:", identityStatus.RegistrationStatus)
   367  	clio.Info("Channel address:", identityStatus.ChannelAddress)
   368  	clio.Info(fmt.Sprintf("Balance: %s", money.New(identityStatus.Balance)))
   369  }
   370  
   371  func (c *command) infoTopUp(identityAddress string) {
   372  	resp, err := c.tequilapi.OrderGetAll(identity.FromAddress(identityAddress))
   373  	if err != nil {
   374  		clio.Error("Failed to get top-up information")
   375  		return
   376  	}
   377  
   378  	if len(resp) == 0 {
   379  		clio.Info("You have no top-up requests")
   380  		return
   381  	}
   382  	printOrder(resp[len(resp)-1])
   383  }