github.com/nspcc-dev/neo-go@v0.105.2-0.20240517133400-6be757af3eba/cli/cmdargs/parser.go (about)

     1  package cmdargs
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"strings"
     7  
     8  	"github.com/nspcc-dev/neo-go/cli/flags"
     9  	"github.com/nspcc-dev/neo-go/pkg/core/transaction"
    10  	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
    11  	"github.com/nspcc-dev/neo-go/pkg/encoding/address"
    12  	"github.com/nspcc-dev/neo-go/pkg/rpcclient/actor"
    13  	"github.com/nspcc-dev/neo-go/pkg/smartcontract"
    14  	"github.com/nspcc-dev/neo-go/pkg/wallet"
    15  	"github.com/urfave/cli"
    16  )
    17  
    18  const (
    19  	// CosignersSeparator marks the start of cosigners cli args.
    20  	CosignersSeparator = "--"
    21  	// ArrayStartSeparator marks the start of array cli arg.
    22  	ArrayStartSeparator = "["
    23  	// ArrayEndSeparator marks the end of array cli arg.
    24  	ArrayEndSeparator = "]"
    25  )
    26  
    27  const (
    28  	// ParamsParsingDoc is a documentation for parameters parsing.
    29  	ParamsParsingDoc = `   Arguments always do have regular Neo smart contract parameter types, either
    30     specified explicitly or being inferred from the value. To specify the type
    31     manually use "type:value" syntax where the type is one of the following:
    32     'signature', 'bool', 'int', 'hash160', 'hash256', 'bytes', 'key' or 'string'.
    33     Array types are also supported: use special space-separated '[' and ']'
    34     symbols around array values to denote array bounds. Nested arrays are also
    35     supported. Null parameter is supported via 'nil' keyword without additional
    36     type specification.
    37  
    38     There is ability to provide an argument of 'bytearray' type via file. Use a
    39     special 'filebytes' argument type for this with a filepath specified after
    40     the colon, e.g. 'filebytes:my_file.txt'.
    41  
    42     Given values are type-checked against given types with the following
    43     restrictions applied:
    44      * 'signature' type values should be hex-encoded and have a (decoded)
    45        length of 64 bytes.
    46      * 'bool' type values are 'true' and 'false'.
    47      * 'int' values are decimal integers that can be successfully converted
    48        from the string.
    49      * 'hash160' values are Neo addresses and hex-encoded 20-bytes long (after
    50        decoding) strings.
    51      * 'hash256' type values should be hex-encoded and have a (decoded)
    52        length of 32 bytes.
    53      * 'bytes' type values are any hex-encoded things.
    54      * 'filebytes' type values are filenames with the argument value inside.
    55      * 'key' type values are hex-encoded marshalled public keys.
    56      * 'string' type values are any valid UTF-8 strings. In the value's part of
    57        the string the colon looses it's special meaning as a separator between
    58        type and value and is taken literally.
    59  
    60     If no type is explicitly specified, it is inferred from the value using the
    61     following logic:
    62      - anything that can be interpreted as a decimal integer gets
    63        an 'int' type
    64      - 'nil' string gets 'Any' NEP-14 parameter type and nil value which corresponds
    65        to Null stackitem
    66      - 'true' and 'false' strings get 'bool' type
    67      - valid Neo addresses and 20 bytes long hex-encoded strings get 'hash160'
    68        type
    69      - valid hex-encoded public keys get 'key' type
    70      - 32 bytes long hex-encoded values get 'hash256' type
    71      - 64 bytes long hex-encoded values get 'signature' type
    72      - any other valid hex-encoded values get 'bytes' type
    73      - anything else is a 'string'
    74  
    75     Backslash character is used as an escape character and allows to use colon in
    76     an implicitly typed string. For any other characters it has no special
    77     meaning, to get a literal backslash in the string use the '\\' sequence.
    78  
    79     Examples:
    80      * 'int:42' is an integer with a value of 42
    81      * '42' is an integer with a value of 42
    82      * 'nil' is a parameter with Any NEP-14 type and nil value (corresponds to Null stackitem)
    83      * 'bad' is a string with a value of 'bad'
    84      * 'dead' is a byte array with a value of 'dead'
    85      * 'string:dead' is a string with a value of 'dead'
    86      * 'filebytes:my_data.txt' is bytes decoded from a content of my_data.txt
    87      * 'NSiVJYZej4XsxG5CUpdwn7VRQk8iiiDMPM' is a hash160 with a value
    88        of '682cca3ebdc66210e5847d7f8115846586079d4a'
    89      * '\4\2' is an integer with a value of 42
    90      * '\\4\2' is a string with a value of '\42'
    91      * 'string:string' is a string with a value of 'string'
    92      * 'string\:string' is a string with a value of 'string:string'
    93      * '03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c' is a
    94        key with a value of '03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c'
    95      * '[ a b c ]' is an array with strings values 'a', 'b' and 'c'
    96      * '[ a b [ c d ] e ]' is an array with 4 values: string 'a', string 'b',
    97        array of two strings 'c' and 'd', string 'e'
    98      * '[ ]' is an empty array`
    99  
   100  	// SignersParsingDoc is a documentation for signers parsing.
   101  	SignersParsingDoc = `   Signers represent a set of Uint160 hashes with witness scopes and are used
   102     to verify hashes in System.Runtime.CheckWitness syscall. First signer is treated
   103     as a sender. To specify signers use signer[:scope] syntax where
   104      * 'signer' is a signer's address (as Neo address or hex-encoded 160 bit (20 byte)
   105                 LE value with or without '0x' prefix).
   106      * 'scope' is a comma-separated set of cosigner's scopes, which could be:
   107          - 'None' - default witness scope which may be used for the sender
   108  			       to only pay fee for the transaction.
   109          - 'Global' - allows this witness in all contexts. This cannot be combined
   110                       with other flags.
   111          - 'CalledByEntry' - means that this condition must hold: EntryScriptHash
   112                              == CallingScriptHash. The witness/permission/signature
   113                              given on first invocation will automatically expire if
   114                              entering deeper internal invokes. This can be default
   115                              safe choice for native NEO/GAS.
   116          - 'CustomContracts' - define valid custom contract hashes for witness check.
   117                                Hashes are be provided as hex-encoded LE value string.
   118                                At lest one hash must be provided. Multiple hashes
   119                                are separated by ':'.
   120          - 'CustomGroups' - define custom public keys for group members. Public keys are
   121                             provided as short-form (1-byte prefix + 32 bytes) hex-encoded
   122                             values. At least one key must be provided. Multiple keys
   123                             are separated by ':'.
   124  
   125     If no scopes were specified, 'CalledByEntry' used as default. If no signers were
   126     specified, no array is passed. Note that scopes are properly handled by
   127     neo-go RPC server only. C# implementation does not support scopes capability.
   128  
   129     Examples:
   130      * 'NNQk4QXsxvsrr3GSozoWBUxEmfag7B6hz5'
   131      * 'NVquyZHoPirw6zAEPvY1ZezxM493zMWQqs:Global'
   132      * '0x0000000009070e030d0f0e020d0c06050e030c02'
   133      * '0000000009070e030d0f0e020d0c06050e030c02:CalledByEntry,` +
   134  		`CustomGroups:0206d7495ceb34c197093b5fc1cccf1996ada05e69ef67e765462a7f5d88ee14d0'
   135      * '0000000009070e030d0f0e020d0c06050e030c02:CalledByEntry,` +
   136  		`CustomContracts:1011120009070e030d0f0e020d0c06050e030c02:0x1211100009070e030d0f0e020d0c06050e030c02'`
   137  )
   138  
   139  // GetSignersFromContext returns signers parsed from context args starting
   140  // from the specified offset.
   141  func GetSignersFromContext(ctx *cli.Context, offset int) ([]transaction.Signer, *cli.ExitError) {
   142  	args := ctx.Args()
   143  	var (
   144  		signers []transaction.Signer
   145  		err     error
   146  	)
   147  	if args.Present() && len(args) > offset {
   148  		signers, err = ParseSigners(args[offset:])
   149  		if err != nil {
   150  			return nil, cli.NewExitError(err, 1)
   151  		}
   152  	}
   153  	return signers, nil
   154  }
   155  
   156  // ParseSigners returns array of signers parsed from their string representation.
   157  func ParseSigners(args []string) ([]transaction.Signer, error) {
   158  	var signers []transaction.Signer
   159  	for i, c := range args {
   160  		cosigner, err := parseCosigner(c)
   161  		if err != nil {
   162  			return nil, fmt.Errorf("failed to parse signer #%d: %w", i, err)
   163  		}
   164  		signers = append(signers, cosigner)
   165  	}
   166  	return signers, nil
   167  }
   168  
   169  func parseCosigner(c string) (transaction.Signer, error) {
   170  	var (
   171  		err error
   172  		res = transaction.Signer{
   173  			Scopes: transaction.CalledByEntry,
   174  		}
   175  	)
   176  	data := strings.SplitN(c, ":", 2)
   177  	s := data[0]
   178  	res.Account, err = flags.ParseAddress(s)
   179  	if err != nil {
   180  		return res, err
   181  	}
   182  
   183  	if len(data) == 1 {
   184  		return res, nil
   185  	}
   186  
   187  	res.Scopes = 0
   188  	scopes := strings.Split(data[1], ",")
   189  	for _, s := range scopes {
   190  		sub := strings.Split(s, ":")
   191  		scope, err := transaction.ScopesFromString(sub[0])
   192  		if err != nil {
   193  			return transaction.Signer{}, err
   194  		}
   195  		if scope == transaction.Global && res.Scopes&^transaction.Global != 0 ||
   196  			scope != transaction.Global && res.Scopes&transaction.Global != 0 {
   197  			return transaction.Signer{}, errors.New("Global scope can not be combined with other scopes")
   198  		}
   199  
   200  		res.Scopes |= scope
   201  
   202  		switch scope {
   203  		case transaction.CustomContracts:
   204  			if len(sub) == 1 {
   205  				return transaction.Signer{}, errors.New("CustomContracts scope must refer to at least one contract")
   206  			}
   207  			for _, s := range sub[1:] {
   208  				addr, err := flags.ParseAddress(s)
   209  				if err != nil {
   210  					return transaction.Signer{}, err
   211  				}
   212  
   213  				res.AllowedContracts = append(res.AllowedContracts, addr)
   214  			}
   215  		case transaction.CustomGroups:
   216  			if len(sub) == 1 {
   217  				return transaction.Signer{}, errors.New("CustomGroups scope must refer to at least one group")
   218  			}
   219  			for _, s := range sub[1:] {
   220  				pub, err := keys.NewPublicKeyFromString(s)
   221  				if err != nil {
   222  					return transaction.Signer{}, err
   223  				}
   224  
   225  				res.AllowedGroups = append(res.AllowedGroups, pub)
   226  			}
   227  		}
   228  	}
   229  	return res, nil
   230  }
   231  
   232  // GetDataFromContext returns data parameter from context args.
   233  func GetDataFromContext(ctx *cli.Context) (int, any, *cli.ExitError) {
   234  	var (
   235  		data   any
   236  		offset int
   237  		params []smartcontract.Parameter
   238  		err    error
   239  	)
   240  	args := ctx.Args()
   241  	if args.Present() {
   242  		offset, params, err = ParseParams(args, true)
   243  		if err != nil {
   244  			return offset, nil, cli.NewExitError(fmt.Errorf("unable to parse 'data' parameter: %w", err), 1)
   245  		}
   246  		if len(params) > 1 {
   247  			return offset, nil, cli.NewExitError("'data' should be represented as a single parameter", 1)
   248  		}
   249  		if len(params) != 0 {
   250  			data, err = smartcontract.ExpandParameterToEmitable(params[0])
   251  			if err != nil {
   252  				return offset, nil, cli.NewExitError(fmt.Sprintf("failed to convert 'data' to emitable type: %s", err.Error()), 1)
   253  			}
   254  		}
   255  	}
   256  	return offset, data, nil
   257  }
   258  
   259  // EnsureNone returns an error if there are any positional arguments present.
   260  // It can be used to check for them in commands that don't accept arguments.
   261  func EnsureNone(ctx *cli.Context) *cli.ExitError {
   262  	if ctx.Args().Present() {
   263  		return cli.NewExitError("additional arguments given while this command expects none", 1)
   264  	}
   265  	return nil
   266  }
   267  
   268  // ParseParams extracts array of smartcontract.Parameter from the given args and
   269  // returns the number of handled words, the array itself and an error.
   270  // `calledFromMain` denotes whether the method was called from the outside or
   271  // recursively and used to check if CosignersSeparator and ArrayEndSeparator are
   272  // allowed to be in `args` sequence.
   273  func ParseParams(args []string, calledFromMain bool) (int, []smartcontract.Parameter, error) {
   274  	res := []smartcontract.Parameter{}
   275  	for k := 0; k < len(args); {
   276  		s := args[k]
   277  		switch s {
   278  		case CosignersSeparator:
   279  			if calledFromMain {
   280  				return k + 1, res, nil // `1` to convert index to numWordsRead
   281  			}
   282  			return 0, []smartcontract.Parameter{}, errors.New("invalid array syntax: missing closing bracket")
   283  		case ArrayStartSeparator:
   284  			numWordsRead, array, err := ParseParams(args[k+1:], false)
   285  			if err != nil {
   286  				return 0, nil, fmt.Errorf("failed to parse array: %w", err)
   287  			}
   288  			res = append(res, smartcontract.Parameter{
   289  				Type:  smartcontract.ArrayType,
   290  				Value: array,
   291  			})
   292  			k += 1 + numWordsRead // `1` for opening bracket
   293  		case ArrayEndSeparator:
   294  			if calledFromMain {
   295  				return 0, nil, errors.New("invalid array syntax: missing opening bracket")
   296  			}
   297  			return k + 1, res, nil // `1`to convert index to numWordsRead
   298  		default:
   299  			param, err := smartcontract.NewParameterFromString(s)
   300  			if err != nil {
   301  				// '--' argument is skipped by urfave/cli library, which leads
   302  				// to [--, addr:scope] being transformed to [addr:scope] and
   303  				// interpreted as a parameter if other positional arguments are not present.
   304  				// Here we fallback to parsing cosigners in this specific case to
   305  				// create a better user experience ('-- addr:scope' vs '-- -- addr:scope').
   306  				if k == 0 {
   307  					if _, err := parseCosigner(s); err == nil {
   308  						return 0, nil, nil
   309  					}
   310  				}
   311  				return 0, nil, fmt.Errorf("failed to parse argument #%d: %w", k+1, err)
   312  			}
   313  			res = append(res, *param)
   314  			k++
   315  		}
   316  	}
   317  	if calledFromMain {
   318  		return len(args), res, nil
   319  	}
   320  	return 0, []smartcontract.Parameter{}, errors.New("invalid array syntax: missing closing bracket")
   321  }
   322  
   323  // GetSignersAccounts returns the list of signers combined with the corresponding
   324  // accounts from the provided wallet.
   325  func GetSignersAccounts(senderAcc *wallet.Account, wall *wallet.Wallet, signers []transaction.Signer, accScope transaction.WitnessScope) ([]actor.SignerAccount, error) {
   326  	signersAccounts := make([]actor.SignerAccount, 0, len(signers)+1)
   327  	sender := senderAcc.ScriptHash()
   328  	signersAccounts = append(signersAccounts, actor.SignerAccount{
   329  		Signer: transaction.Signer{
   330  			Account: sender,
   331  			Scopes:  accScope,
   332  		},
   333  		Account: senderAcc,
   334  	})
   335  	for i, s := range signers {
   336  		if s.Account == sender {
   337  			signersAccounts[0].Signer = s
   338  			continue
   339  		}
   340  		signerAcc := wall.GetAccount(s.Account)
   341  		if signerAcc == nil {
   342  			return nil, fmt.Errorf("no account was found in the wallet for signer #%d (%s)", i, address.Uint160ToString(s.Account))
   343  		}
   344  		signersAccounts = append(signersAccounts, actor.SignerAccount{
   345  			Signer:  s,
   346  			Account: signerAcc,
   347  		})
   348  	}
   349  	return signersAccounts, nil
   350  }