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 }