github.com/mysteriumnetwork/node@v0.0.0-20240516044423-365054f76801/cmd/commands/cli/command_orders.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 cli
    19  
    20  import (
    21  	"encoding/json"
    22  	"errors"
    23  	"fmt"
    24  	"os"
    25  	"strconv"
    26  	"strings"
    27  
    28  	"github.com/mysteriumnetwork/node/config/remote"
    29  	"github.com/mysteriumnetwork/payments/exchange"
    30  
    31  	"github.com/mysteriumnetwork/node/cmd/commands/cli/clio"
    32  	"github.com/mysteriumnetwork/node/config"
    33  	"github.com/mysteriumnetwork/node/identity"
    34  	"github.com/mysteriumnetwork/node/tequilapi/contract"
    35  )
    36  
    37  func (c *cliApp) order(args []string) (err error) {
    38  	var usage = strings.Join([]string{
    39  		"Usage: order <action> [args]",
    40  		"Available actions:",
    41  		"  " + usageOrderGateways,
    42  		"  " + usageOrderCreate,
    43  		"  " + usageOrderGet,
    44  		"  " + usageOrderGetAll,
    45  	}, "\n")
    46  
    47  	if len(args) == 0 {
    48  		clio.Info(usage)
    49  		return errWrongArgumentCount
    50  	}
    51  
    52  	action := args[0]
    53  	actionArgs := args[1:]
    54  
    55  	switch action {
    56  	case "create":
    57  		return c.orderCreate(actionArgs)
    58  	case "get":
    59  		return c.orderGet(actionArgs)
    60  	case "get-all":
    61  		return c.orderGetAll(actionArgs)
    62  	case "invoice":
    63  		return c.invoice(actionArgs)
    64  	case "gateways":
    65  		return c.gateways(actionArgs)
    66  	default:
    67  		fmt.Println(usage)
    68  		return errUnknownSubCommand(args[0])
    69  	}
    70  }
    71  
    72  const usageOrderGateways = "gateways"
    73  
    74  func (c *cliApp) gateways(args []string) (err error) {
    75  	if len(args) > 0 {
    76  		clio.Info("Usage: " + usageOrderGateways)
    77  		return
    78  	}
    79  
    80  	resp, err := c.tequilapi.PaymentOrderGateways(exchange.CurrencyMYST)
    81  	if err != nil {
    82  		return fmt.Errorf("could not get currencies: %w", err)
    83  	}
    84  
    85  	for _, gw := range resp {
    86  		clio.Info("Gateway:", gw.Name)
    87  		clio.Info("Suggested minimum order:", gw.OrderOptions.Minimum)
    88  		clio.Info("Supported currencies:", strings.Join(gw.Currencies, ", "))
    89  	}
    90  
    91  	return nil
    92  }
    93  
    94  const usageOrderCreate = "create <identity> <amount> <pay currency> <gateway> <country> [gw data: lightning_network=true,order=123]"
    95  
    96  func (c *cliApp) orderCreate(args []string) (err error) {
    97  	if len(args) != 5 && len(args) != 6 {
    98  		clio.Info("Usage: " + usageOrderCreate)
    99  		return errWrongArgumentCount
   100  	}
   101  
   102  	argId := args[0]
   103  	argAmount := args[1]
   104  	argPayCurrency := args[2]
   105  	argGateway := args[3]
   106  	argCountry := args[4]
   107  	var argCallerData string
   108  	if len(args) > 5 {
   109  		argCallerData = args[5]
   110  	}
   111  
   112  	f, err := strconv.ParseFloat(argAmount, 64)
   113  	if err != nil {
   114  		return fmt.Errorf("could not parse amount: %w", err)
   115  	}
   116  	if f <= 0 {
   117  		return errors.New("top-up amount is required and must be greater than 0")
   118  	}
   119  
   120  	gws, err := c.tequilapi.PaymentOrderGateways(exchange.CurrencyMYST)
   121  	if err != nil {
   122  		clio.Info("Failed to get enabled gateways and their information")
   123  		return
   124  	}
   125  
   126  	if len(gws) == 0 {
   127  		clio.Warn("No payment gateways are enabled, can't create new orders")
   128  		return
   129  	}
   130  
   131  	gw, ok := findGateway(argGateway, gws)
   132  	if !ok {
   133  		clio.Error("Can't continue, no such gateway:", argGateway)
   134  		return
   135  	}
   136  	if gw.OrderOptions.Minimum != 0 && f <= gw.OrderOptions.Minimum {
   137  		return fmt.Errorf(
   138  			"top-up amount must be greater than %v%s",
   139  			gw.OrderOptions.Minimum,
   140  			config.GetString(config.FlagDefaultCurrency))
   141  	}
   142  
   143  	data := map[string]interface{}{}
   144  	parts := strings.Split(argCallerData, ",")
   145  	for _, part := range parts {
   146  		kv := strings.Split(part, "=")
   147  		if len(kv) != 2 {
   148  			clio.Error("Gateway data wrong, example: lightning_network=true,custom_id=\"123 11\"")
   149  			return
   150  		}
   151  
   152  		if b, err := strconv.ParseBool(kv[1]); err == nil {
   153  			data[kv[0]] = b
   154  			continue
   155  		}
   156  
   157  		if b, err := strconv.ParseFloat(kv[1], 64); err == nil {
   158  			data[kv[0]] = b
   159  			continue
   160  		}
   161  
   162  		data[kv[0]] = kv[1]
   163  	}
   164  
   165  	callerData, err := json.Marshal(data)
   166  	if err != nil {
   167  		clio.Error("Failed to make caller data")
   168  		return
   169  	}
   170  
   171  	resp, err := c.tequilapi.OrderCreate(
   172  		identity.FromAddress(argId),
   173  		argGateway,
   174  		contract.PaymentOrderRequest{
   175  			MystAmount:  argAmount,
   176  			PayCurrency: argPayCurrency,
   177  			Country:     argCountry,
   178  			CallerData:  callerData,
   179  		})
   180  	if err != nil {
   181  		return fmt.Errorf("could not create an order: %w", err)
   182  	}
   183  
   184  	printOrder(resp, c.config)
   185  	return nil
   186  }
   187  
   188  const usageOrderGet = "get <identity> <orderID>"
   189  
   190  func (c *cliApp) orderGet(args []string) (err error) {
   191  	if len(args) != 2 {
   192  		clio.Info("Usage: " + usageOrderGet)
   193  		return
   194  	}
   195  
   196  	resp, err := c.tequilapi.OrderGet(identity.FromAddress(args[0]), args[1])
   197  	if err != nil {
   198  		return fmt.Errorf("could not get an order: %w", err)
   199  	}
   200  	printOrder(resp, c.config)
   201  	return nil
   202  }
   203  
   204  const usageOrderGetAll = "get-all <identity>"
   205  
   206  func (c *cliApp) orderGetAll(args []string) (err error) {
   207  	if len(args) != 1 {
   208  		clio.Info("Usage: " + usageOrderGetAll)
   209  		return errWrongArgumentCount
   210  	}
   211  
   212  	resp, err := c.tequilapi.OrderGetAll(identity.FromAddress(args[0]))
   213  	if err != nil {
   214  		return fmt.Errorf("could not get orders: %w", err)
   215  	}
   216  
   217  	if len(resp) == 0 {
   218  		clio.Info("No orders found")
   219  		return nil
   220  	}
   221  
   222  	for _, r := range resp {
   223  		clio.Info(fmt.Sprintf("Order ID '%s' is in state: '%s'", r.ID, r.Status))
   224  	}
   225  	clio.Info(
   226  		fmt.Sprintf("To explore additional order information use: orders '%s'", usageOrderGet),
   227  	)
   228  	return nil
   229  }
   230  
   231  const usageOrderInvoice = "invoice <identity> <orderID>"
   232  
   233  func (c *cliApp) invoice(args []string) (err error) {
   234  	if len(args) != 2 {
   235  		clio.Info("Usage: " + usageOrderInvoice)
   236  		return
   237  	}
   238  
   239  	resp, err := c.tequilapi.OrderInvoice(identity.FromAddress(args[0]), args[1])
   240  	if err != nil {
   241  		return fmt.Errorf("could not get an order invoice: %w", err)
   242  	}
   243  	filename := fmt.Sprintf("invoice-%v.pdf", args[1])
   244  	clio.Info("Writing invoice to", filename)
   245  	return os.WriteFile(filename, resp, 0644)
   246  }
   247  
   248  func printOrder(o contract.PaymentOrderResponse, rc *remote.Config) {
   249  	clio.Info(fmt.Sprintf("Order ID '%s' is in state: '%s'", o.ID, o.Status))
   250  	clio.Info(fmt.Sprintf("Pay: %s %s", o.PayAmount, o.PayCurrency))
   251  	clio.Info(fmt.Sprintf("Receive MYST: %s", o.ReceiveMYST))
   252  	clio.Info("Order details:")
   253  	clio.Info(fmt.Sprintf(" + MYST (%v): %s %s", o.ReceiveMYST, o.ItemsSubTotal, o.Currency))
   254  	clio.Info(fmt.Sprintf(" + Tax (%s %%): %s %s", o.TaxRate, o.TaxSubTotal, o.Currency))
   255  	clio.Info(fmt.Sprintf(" = Total: %s %s", o.OrderTotal, o.Currency))
   256  	clio.Info("Data:", string(o.PublicGatewayData))
   257  }
   258  
   259  func findGateway(name string, gws []contract.GatewaysResponse) (*contract.GatewaysResponse, bool) {
   260  	for _, gw := range gws {
   261  		if gw.Name == name {
   262  			return &gw, true
   263  		}
   264  	}
   265  	return nil, false
   266  }