forked from TrueCloudLab/neoneo-go
26eff7110c
By providing `--out` flag.
926 lines
29 KiB
Go
926 lines
29 KiB
Go
package smartcontract
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/nspcc-dev/neo-go/cli/flags"
|
|
"github.com/nspcc-dev/neo-go/cli/input"
|
|
"github.com/nspcc-dev/neo-go/cli/options"
|
|
"github.com/nspcc-dev/neo-go/cli/paramcontext"
|
|
"github.com/nspcc-dev/neo-go/pkg/compiler"
|
|
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
|
|
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
|
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
|
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
|
|
"github.com/nspcc-dev/neo-go/pkg/encoding/fixedn"
|
|
"github.com/nspcc-dev/neo-go/pkg/io"
|
|
"github.com/nspcc-dev/neo-go/pkg/rpc/client"
|
|
"github.com/nspcc-dev/neo-go/pkg/rpc/response/result"
|
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
|
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/nef"
|
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
|
"github.com/nspcc-dev/neo-go/pkg/vm"
|
|
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
|
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
|
"github.com/urfave/cli"
|
|
"gopkg.in/yaml.v2"
|
|
)
|
|
|
|
var (
|
|
errNoInput = errors.New("no input file was found, specify an input file with the '--in or -i' flag")
|
|
errNoConfFile = errors.New("no config file was found, specify a config file with the '--config' or '-c' flag")
|
|
errNoManifestFile = errors.New("no manifest file was found, specify manifest file with '--manifest' or '-m' flag")
|
|
errNoMethod = errors.New("no method specified for function invocation command")
|
|
errNoWallet = errors.New("no wallet parameter found, specify it with the '--wallet or -w' flag")
|
|
errNoScriptHash = errors.New("no smart contract hash was provided, specify one as the first argument")
|
|
errNoSmartContractName = errors.New("no name was provided, specify the '--name or -n' flag")
|
|
errFileExist = errors.New("A file with given smart-contract name already exists")
|
|
|
|
walletFlag = cli.StringFlag{
|
|
Name: "wallet, w",
|
|
Usage: "wallet to use to get the key for transaction signing",
|
|
}
|
|
addressFlag = flags.AddressFlag{
|
|
Name: "address, a",
|
|
Usage: "address to use as transaction signee (and gas source)",
|
|
}
|
|
gasFlag = flags.Fixed8Flag{
|
|
Name: "gas, g",
|
|
Usage: "gas to add to the transaction",
|
|
}
|
|
outFlag = cli.StringFlag{
|
|
Name: "out",
|
|
Usage: "file to put JSON transaction to",
|
|
}
|
|
forceFlag = cli.BoolFlag{
|
|
Name: "force",
|
|
Usage: "force-push the transaction in case of bad VM state after test script invocation",
|
|
}
|
|
)
|
|
|
|
const (
|
|
// smartContractTmpl is written to a file when used with `init` command.
|
|
// %s is parsed to be the smartContractName
|
|
smartContractTmpl = `package %s
|
|
|
|
import "github.com/nspcc-dev/neo-go/pkg/interop/runtime"
|
|
|
|
var notificationName string
|
|
|
|
// init initializes notificationName before calling any other smart-contract method
|
|
func init() {
|
|
notificationName = "Hello world!"
|
|
}
|
|
|
|
// RuntimeNotify sends runtime notification with "Hello world!" name
|
|
func RuntimeNotify(args []interface{}) {
|
|
runtime.Notify(notificationName, args)
|
|
}`
|
|
// cosignersSeparator is a special value which is used to distinguish
|
|
// parameters and cosigners for invoke* commands
|
|
cosignersSeparator = "--"
|
|
arrayStartSeparator = "["
|
|
arrayEndSeparator = "]"
|
|
)
|
|
|
|
// NewCommands returns 'contract' command.
|
|
func NewCommands() []cli.Command {
|
|
testInvokeScriptFlags := []cli.Flag{
|
|
cli.StringFlag{
|
|
Name: "in, i",
|
|
Usage: "Input location of the .nef file that needs to be invoked",
|
|
},
|
|
}
|
|
testInvokeScriptFlags = append(testInvokeScriptFlags, options.RPC...)
|
|
deployFlags := []cli.Flag{
|
|
cli.StringFlag{
|
|
Name: "in, i",
|
|
Usage: "Input file for the smart contract (*.nef)",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "manifest, m",
|
|
Usage: "Manifest input file (*.manifest.json)",
|
|
},
|
|
walletFlag,
|
|
addressFlag,
|
|
outFlag,
|
|
gasFlag,
|
|
}
|
|
deployFlags = append(deployFlags, options.RPC...)
|
|
invokeFunctionFlags := []cli.Flag{
|
|
walletFlag,
|
|
addressFlag,
|
|
gasFlag,
|
|
outFlag,
|
|
forceFlag,
|
|
}
|
|
invokeFunctionFlags = append(invokeFunctionFlags, options.RPC...)
|
|
return []cli.Command{{
|
|
Name: "contract",
|
|
Usage: "compile - debug - deploy smart contracts",
|
|
Subcommands: []cli.Command{
|
|
{
|
|
Name: "compile",
|
|
Usage: "compile a smart contract to a .nef file",
|
|
Action: contractCompile,
|
|
Flags: []cli.Flag{
|
|
cli.StringFlag{
|
|
Name: "in, i",
|
|
Usage: "Input file for the smart contract to be compiled",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "out, o",
|
|
Usage: "Output of the compiled contract",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "verbose, v",
|
|
Usage: "Print out additional information after a compiling",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "debug, d",
|
|
Usage: "Emit debug info in a separate file",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "manifest, m",
|
|
Usage: "Emit contract manifest (*.manifest.json) file into separate file using configuration input file (*.yml)",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "config, c",
|
|
Usage: "Configuration input file (*.yml)",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "no-standards",
|
|
Usage: "do not check compliance with supported standards",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "no-events",
|
|
Usage: "do not check emitted events with the manifest",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "deploy",
|
|
Usage: "deploy a smart contract (.nef with description)",
|
|
Description: `Deploys given contract into the chain. The gas parameter is for additional
|
|
gas to be added as a network fee to prioritize the transaction.
|
|
`,
|
|
Action: contractDeploy,
|
|
Flags: deployFlags,
|
|
},
|
|
{
|
|
Name: "invokefunction",
|
|
Usage: "invoke deployed contract on the blockchain",
|
|
UsageText: "neo-go contract invokefunction -r endpoint -w wallet [-a address] [-g gas] [--out file] scripthash [method] [arguments...] [--] [signers...]",
|
|
Description: `Executes given (as a script hash) deployed script with the given method,
|
|
arguments and signers. Sender is included in the list of signers by default
|
|
with None witness scope. If you'd like to change default sender's scope,
|
|
specify it via signers parameter. See testinvokefunction documentation for
|
|
the details about parameters. It differs from testinvokefunction in that this
|
|
command sends an invocation transaction to the network.
|
|
`,
|
|
Action: invokeFunction,
|
|
Flags: invokeFunctionFlags,
|
|
},
|
|
{
|
|
Name: "testinvokefunction",
|
|
Usage: "invoke deployed contract on the blockchain (test mode)",
|
|
UsageText: "neo-go contract testinvokefunction -r endpoint scripthash [method] [arguments...] [--] [signers...]",
|
|
Description: `Executes given (as a script hash) deployed script with the given method,
|
|
arguments and signers (sender is not included by default). If no method is given
|
|
"" is passed to the script, if no arguments are given, an empty array is
|
|
passed, if no signers are given no array is passed. If signers are specified,
|
|
the first one of them is treated as a sender. All of the given arguments are
|
|
encapsulated into array before invoking the script. The script thus should
|
|
follow the regular convention of smart contract arguments (method string and
|
|
an array of other arguments).
|
|
|
|
Arguments always do have regular Neo smart contract parameter types, either
|
|
specified explicitly or being inferred from the value. To specify the type
|
|
manually use "type:value" syntax where the type is one of the following:
|
|
'signature', 'bool', 'int', 'hash160', 'hash256', 'bytes', 'key' or 'string'.
|
|
Array types are also supported: use special space-separated '[' and ']'
|
|
symbols around array values to denote array bounds. Nested arrays are also
|
|
supported.
|
|
|
|
There is ability to provide an argument of 'bytearray' type via file. Use a
|
|
special 'filebytes' argument type for this with a filepath specified after
|
|
the colon, e.g. 'filebytes:my_file.txt'.
|
|
|
|
Given values are type-checked against given types with the following
|
|
restrictions applied:
|
|
* 'signature' type values should be hex-encoded and have a (decoded)
|
|
length of 64 bytes.
|
|
* 'bool' type values are 'true' and 'false'.
|
|
* 'int' values are decimal integers that can be successfully converted
|
|
from the string.
|
|
* 'hash160' values are Neo addresses and hex-encoded 20-bytes long (after
|
|
decoding) strings.
|
|
* 'hash256' type values should be hex-encoded and have a (decoded)
|
|
length of 32 bytes.
|
|
* 'bytes' type values are any hex-encoded things.
|
|
* 'filebytes' type values are filenames with the argument value inside.
|
|
* 'key' type values are hex-encoded marshalled public keys.
|
|
* 'string' type values are any valid UTF-8 strings. In the value's part of
|
|
the string the colon looses it's special meaning as a separator between
|
|
type and value and is taken literally.
|
|
|
|
If no type is explicitly specified, it is inferred from the value using the
|
|
following logic:
|
|
- anything that can be interpreted as a decimal integer gets
|
|
an 'int' type
|
|
- 'true' and 'false' strings get 'bool' type
|
|
- valid Neo addresses and 20 bytes long hex-encoded strings get 'hash160'
|
|
type
|
|
- valid hex-encoded public keys get 'key' type
|
|
- 32 bytes long hex-encoded values get 'hash256' type
|
|
- 64 bytes long hex-encoded values get 'signature' type
|
|
- any other valid hex-encoded values get 'bytes' type
|
|
- anything else is a 'string'
|
|
|
|
Backslash character is used as an escape character and allows to use colon in
|
|
an implicitly typed string. For any other characters it has no special
|
|
meaning, to get a literal backslash in the string use the '\\' sequence.
|
|
|
|
Examples:
|
|
* 'int:42' is an integer with a value of 42
|
|
* '42' is an integer with a value of 42
|
|
* 'bad' is a string with a value of 'bad'
|
|
* 'dead' is a byte array with a value of 'dead'
|
|
* 'string:dead' is a string with a value of 'dead'
|
|
* 'filebytes:my_data.txt' is bytes decoded from a content of my_data.txt
|
|
* 'AK2nJJpJr6o664CWJKi1QRXjqeic2zRp8y' is a hash160 with a value
|
|
of '23ba2703c53263e8d6e522dc32203339dcd8eee9'
|
|
* '\4\2' is an integer with a value of 42
|
|
* '\\4\2' is a string with a value of '\42'
|
|
* 'string:string' is a string with a value of 'string'
|
|
* 'string\:string' is a string with a value of 'string:string'
|
|
* '03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c' is a
|
|
key with a value of '03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c'
|
|
* '[ a b c ]' is an array with strings values 'a', 'b' and 'c'
|
|
* '[ a b [ c d ] e ]' is an array with 4 values: string 'a', string 'b',
|
|
array of two strings 'c' and 'd', string 'e'
|
|
* '[ ]' is an empty array
|
|
|
|
Signers represent a set of Uint160 hashes with witness scopes and are used
|
|
to verify hashes in System.Runtime.CheckWitness syscall. First signer is treated
|
|
as a sender. To specify signers use signer[:scope] syntax where
|
|
* 'signer' is a signer's address (as Neo address or hex-encoded 160 bit (20 byte)
|
|
LE value with or without '0x' prefix).
|
|
* 'scope' is a comma-separated set of cosigner's scopes, which could be:
|
|
- 'None' - default witness scope which may be used for the sender
|
|
to only pay fee for the transaction.
|
|
- 'Global' - allows this witness in all contexts. This cannot be combined
|
|
with other flags.
|
|
- 'CalledByEntry' - means that this condition must hold: EntryScriptHash
|
|
== CallingScriptHash. The witness/permission/signature
|
|
given on first invocation will automatically expire if
|
|
entering deeper internal invokes. This can be default
|
|
safe choice for native NEO/GAS.
|
|
- 'CustomContracts' - define valid custom contract hashes for witness check.
|
|
- 'CustomGroups' - define custom pubkey for group members.
|
|
|
|
If no scopes were specified, 'Global' used as default. If no signers were
|
|
specified, no array is passed. Note that scopes are properly handled by
|
|
neo-go RPC server only. C# implementation does not support scopes capability.
|
|
|
|
Examples:
|
|
* 'NNQk4QXsxvsrr3GSozoWBUxEmfag7B6hz5'
|
|
* 'NVquyZHoPirw6zAEPvY1ZezxM493zMWQqs:Global'
|
|
* '0x0000000009070e030d0f0e020d0c06050e030c02'
|
|
* '0000000009070e030d0f0e020d0c06050e030c02:CalledByEntry,CustomGroups'
|
|
`,
|
|
Action: testInvokeFunction,
|
|
Flags: options.RPC,
|
|
},
|
|
{
|
|
Name: "testinvokescript",
|
|
Usage: "Invoke compiled AVM code in NEF format on the blockchain (test mode, not creating a transaction for it)",
|
|
UsageText: "neo-go contract testinvokescript -r endpoint -i input.nef [signers...]",
|
|
Description: `Executes given compiled AVM instructions in NEF format with the given set of
|
|
signers not included sender by default. See testinvokefunction documentation
|
|
for the details about parameters.
|
|
`,
|
|
Action: testInvokeScript,
|
|
Flags: testInvokeScriptFlags,
|
|
},
|
|
{
|
|
Name: "init",
|
|
Usage: "initialize a new smart-contract in a directory with boiler plate code",
|
|
Action: initSmartContract,
|
|
Flags: []cli.Flag{
|
|
cli.StringFlag{
|
|
Name: "name, n",
|
|
Usage: "name of the smart-contract to be initialized",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "skip-details, skip",
|
|
Usage: "skip filling in the projects and contract details",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "inspect",
|
|
Usage: "creates a user readable dump of the program instructions",
|
|
Action: inspect,
|
|
Flags: []cli.Flag{
|
|
cli.BoolFlag{
|
|
Name: "compile, c",
|
|
Usage: "compile input file (it should be go code then)",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "in, i",
|
|
Usage: "input file of the program (either .go or .nef)",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "calc-hash",
|
|
Usage: "calculates hash of a contract after deployment",
|
|
Action: calcHash,
|
|
Flags: []cli.Flag{
|
|
cli.StringFlag{
|
|
Name: "sender, s",
|
|
Usage: "sender script hash or address",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "in",
|
|
Usage: "path to NEF file",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "manifest, m",
|
|
Usage: "path to manifest file",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}}
|
|
}
|
|
|
|
// initSmartContract initializes a given directory with some boiler plate code.
|
|
func initSmartContract(ctx *cli.Context) error {
|
|
contractName := ctx.String("name")
|
|
if contractName == "" {
|
|
return cli.NewExitError(errNoSmartContractName, 1)
|
|
}
|
|
|
|
// Check if the file already exists, if yes, exit
|
|
if _, err := os.Stat(contractName); err == nil {
|
|
return cli.NewExitError(errFileExist, 1)
|
|
}
|
|
|
|
basePath := contractName
|
|
contractName = path.Base(contractName)
|
|
fileName := "main.go"
|
|
|
|
// create base directory
|
|
if err := os.Mkdir(basePath, os.ModePerm); err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
|
|
m := ProjectConfig{
|
|
Name: contractName,
|
|
SupportedStandards: []string{},
|
|
SafeMethods: []string{},
|
|
Events: []manifest.Event{
|
|
{
|
|
Name: "Hello world!",
|
|
Parameters: []manifest.Parameter{
|
|
{
|
|
Name: "args",
|
|
Type: smartcontract.ArrayType,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
b, err := yaml.Marshal(m)
|
|
if err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
if err := ioutil.WriteFile(filepath.Join(basePath, "neo-go.yml"), b, 0644); err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
|
|
data := []byte(fmt.Sprintf(smartContractTmpl, contractName))
|
|
if err := ioutil.WriteFile(filepath.Join(basePath, fileName), data, 0644); err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
|
|
fmt.Fprintf(ctx.App.Writer, "Successfully initialized smart contract [%s]\n", contractName)
|
|
|
|
return nil
|
|
}
|
|
|
|
func contractCompile(ctx *cli.Context) error {
|
|
src := ctx.String("in")
|
|
if len(src) == 0 {
|
|
return cli.NewExitError(errNoInput, 1)
|
|
}
|
|
manifestFile := ctx.String("manifest")
|
|
confFile := ctx.String("config")
|
|
debugFile := ctx.String("debug")
|
|
if len(confFile) == 0 && (len(manifestFile) != 0 || len(debugFile) != 0) {
|
|
return cli.NewExitError(errNoConfFile, 1)
|
|
}
|
|
|
|
o := &compiler.Options{
|
|
Outfile: ctx.String("out"),
|
|
|
|
DebugInfo: debugFile,
|
|
ManifestFile: manifestFile,
|
|
|
|
NoStandardCheck: ctx.Bool("no-standards"),
|
|
NoEventsCheck: ctx.Bool("no-events"),
|
|
}
|
|
|
|
if len(confFile) != 0 {
|
|
conf, err := ParseContractConfig(confFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
o.Name = conf.Name
|
|
o.ContractEvents = conf.Events
|
|
o.ContractSupportedStandards = conf.SupportedStandards
|
|
o.SafeMethods = conf.SafeMethods
|
|
}
|
|
|
|
result, err := compiler.CompileAndSave(src, o)
|
|
if err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
if ctx.Bool("verbose") {
|
|
fmt.Fprintln(ctx.App.Writer, hex.EncodeToString(result))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func calcHash(ctx *cli.Context) error {
|
|
s := ctx.String("sender")
|
|
u, err := flags.ParseAddress(s)
|
|
if err != nil {
|
|
return cli.NewExitError(errors.New("invalid sender: must be either address or Uint160 in LE form"), 1)
|
|
}
|
|
|
|
p := ctx.String("in")
|
|
if p == "" {
|
|
return cli.NewExitError(errors.New("no .nef file was provided"), 1)
|
|
}
|
|
mpath := ctx.String("manifest")
|
|
if mpath == "" {
|
|
return cli.NewExitError(errors.New("no manifest file provided"), 1)
|
|
}
|
|
f, err := ioutil.ReadFile(p)
|
|
if err != nil {
|
|
return cli.NewExitError(fmt.Errorf("can't read .nef file: %w", err), 1)
|
|
}
|
|
nefFile, err := nef.FileFromBytes(f)
|
|
if err != nil {
|
|
return cli.NewExitError(fmt.Errorf("can't unmarshal .nef file: %w", err), 1)
|
|
}
|
|
manifestBytes, err := ioutil.ReadFile(mpath)
|
|
if err != nil {
|
|
return cli.NewExitError(fmt.Errorf("failed to read manifest file: %w", err), 1)
|
|
}
|
|
m := &manifest.Manifest{}
|
|
err = json.Unmarshal(manifestBytes, m)
|
|
if err != nil {
|
|
return cli.NewExitError(fmt.Errorf("failed to restore manifest file: %w", err), 1)
|
|
}
|
|
fmt.Fprintln(ctx.App.Writer, "Contract hash:", state.CreateContractHash(u, nefFile.Checksum, m.Name).StringLE())
|
|
return nil
|
|
}
|
|
|
|
func testInvokeFunction(ctx *cli.Context) error {
|
|
return invokeInternal(ctx, false)
|
|
}
|
|
|
|
func invokeFunction(ctx *cli.Context) error {
|
|
return invokeInternal(ctx, true)
|
|
}
|
|
|
|
func invokeInternal(ctx *cli.Context, signAndPush bool) error {
|
|
var (
|
|
err error
|
|
gas fixedn.Fixed8
|
|
operation string
|
|
params = make([]smartcontract.Parameter, 0)
|
|
paramsStart = 1
|
|
cosigners []transaction.Signer
|
|
cosignersAccounts []client.SignerAccount
|
|
cosignersOffset = 0
|
|
resp *result.Invoke
|
|
acc *wallet.Account
|
|
wall *wallet.Wallet
|
|
)
|
|
|
|
args := ctx.Args()
|
|
if !args.Present() {
|
|
return cli.NewExitError(errNoScriptHash, 1)
|
|
}
|
|
script, err := flags.ParseAddress(args[0])
|
|
if err != nil {
|
|
return cli.NewExitError(fmt.Errorf("incorrect script hash: %w", err), 1)
|
|
}
|
|
if len(args) <= 1 {
|
|
return cli.NewExitError(errNoMethod, 1)
|
|
}
|
|
operation = args[1]
|
|
paramsStart++
|
|
|
|
if len(args) > paramsStart {
|
|
cosignersOffset, params, err = parseParams(args[paramsStart:], true)
|
|
if err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
}
|
|
|
|
cosignersStart := paramsStart + cosignersOffset
|
|
if len(args) > cosignersStart {
|
|
for i, c := range args[cosignersStart:] {
|
|
cosigner, err := parseCosigner(c)
|
|
if err != nil {
|
|
return cli.NewExitError(fmt.Errorf("failed to parse cosigner #%d: %w", i+1, err), 1)
|
|
}
|
|
cosigners = append(cosigners, cosigner)
|
|
}
|
|
}
|
|
|
|
if signAndPush {
|
|
gas = flags.Fixed8FromContext(ctx, "gas")
|
|
acc, wall, err = getAccFromContext(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for i := range cosigners {
|
|
cosignerAcc := wall.GetAccount(cosigners[i].Account)
|
|
if cosignerAcc == nil {
|
|
return cli.NewExitError(fmt.Errorf("can't calculate network fee: no account was found in the wallet for cosigner #%d", i), 1)
|
|
}
|
|
cosignersAccounts = append(cosignersAccounts, client.SignerAccount{
|
|
Signer: cosigners[i],
|
|
Account: cosignerAcc,
|
|
})
|
|
}
|
|
}
|
|
gctx, cancel := options.GetTimeoutContext(ctx)
|
|
defer cancel()
|
|
|
|
c, err := options.GetRPCClient(gctx, ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
resp, err = c.InvokeFunction(script, operation, params, cosigners)
|
|
if err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
if signAndPush && resp.State != "HALT" {
|
|
errText := fmt.Sprintf("Warning: %s VM state returned from the RPC node: %s\n", resp.State, resp.FaultException)
|
|
if !ctx.Bool("force") {
|
|
return cli.NewExitError(errText+". Use --force flag to send the transaction anyway.", 1)
|
|
}
|
|
fmt.Fprintln(ctx.App.Writer, errText+". Sending transaction...")
|
|
}
|
|
if out := ctx.String("out"); out != "" {
|
|
tx, err := c.CreateTxFromScript(resp.Script, acc, resp.GasConsumed, int64(gas), cosignersAccounts)
|
|
if err != nil {
|
|
return cli.NewExitError(fmt.Errorf("failed to create tx: %w", err), 1)
|
|
}
|
|
if err := paramcontext.InitAndSave(c.GetNetwork(), tx, acc, out); err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
fmt.Fprintln(ctx.App.Writer, tx.Hash().StringLE())
|
|
return nil
|
|
}
|
|
if signAndPush {
|
|
if len(resp.Script) == 0 {
|
|
return cli.NewExitError(errors.New("no script returned from the RPC node"), 1)
|
|
}
|
|
txHash, err := c.SignAndPushInvocationTx(resp.Script, acc, resp.GasConsumed, gas, cosignersAccounts)
|
|
if err != nil {
|
|
return cli.NewExitError(fmt.Errorf("failed to push invocation tx: %w", err), 1)
|
|
}
|
|
fmt.Fprintf(ctx.App.Writer, "Sent invocation transaction %s\n", txHash.StringLE())
|
|
} else {
|
|
b, err := json.MarshalIndent(resp, "", " ")
|
|
if err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
|
|
fmt.Fprintln(ctx.App.Writer, string(b))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// parseParams extracts array of smartcontract.Parameter from the given args and
|
|
// returns the number of handled words, the array itself and an error.
|
|
// `calledFromMain` denotes whether the method was called from the outside or
|
|
// recursively and used to check if cosignersSeparator and closing bracket are
|
|
// allowed to be in `args` sequence.
|
|
func parseParams(args []string, calledFromMain bool) (int, []smartcontract.Parameter, error) {
|
|
res := []smartcontract.Parameter{}
|
|
for k := 0; k < len(args); {
|
|
s := args[k]
|
|
switch s {
|
|
case cosignersSeparator:
|
|
if calledFromMain {
|
|
return k + 1, res, nil // `1` to convert index to numWordsRead
|
|
}
|
|
return 0, []smartcontract.Parameter{}, errors.New("invalid array syntax: missing closing bracket")
|
|
case arrayStartSeparator:
|
|
numWordsRead, array, err := parseParams(args[k+1:], false)
|
|
if err != nil {
|
|
return 0, nil, fmt.Errorf("failed to parse array: %w", err)
|
|
}
|
|
res = append(res, smartcontract.Parameter{
|
|
Type: smartcontract.ArrayType,
|
|
Value: array,
|
|
})
|
|
k += 1 + numWordsRead // `1` for opening bracket
|
|
case arrayEndSeparator:
|
|
if calledFromMain {
|
|
return 0, nil, errors.New("invalid array syntax: missing opening bracket")
|
|
}
|
|
return k + 1, res, nil // `1`to convert index to numWordsRead
|
|
default:
|
|
param, err := smartcontract.NewParameterFromString(s)
|
|
if err != nil {
|
|
return 0, nil, fmt.Errorf("failed to parse argument #%d: %w", k+1, err)
|
|
}
|
|
res = append(res, *param)
|
|
k++
|
|
}
|
|
}
|
|
if calledFromMain {
|
|
return len(args), res, nil
|
|
}
|
|
return 0, []smartcontract.Parameter{}, errors.New("invalid array syntax: missing closing bracket")
|
|
|
|
}
|
|
|
|
func testInvokeScript(ctx *cli.Context) error {
|
|
src := ctx.String("in")
|
|
if len(src) == 0 {
|
|
return cli.NewExitError(errNoInput, 1)
|
|
}
|
|
|
|
b, err := ioutil.ReadFile(src)
|
|
if err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
nefFile, err := nef.FileFromBytes(b)
|
|
if err != nil {
|
|
return cli.NewExitError(fmt.Errorf("failed to restore .nef file: %w", err), 1)
|
|
}
|
|
|
|
args := ctx.Args()
|
|
var signers []transaction.Signer
|
|
if args.Present() {
|
|
for i, c := range args[:] {
|
|
cosigner, err := parseCosigner(c)
|
|
if err != nil {
|
|
return cli.NewExitError(fmt.Errorf("failed to parse signer #%d: %w", i+1, err), 1)
|
|
}
|
|
signers = append(signers, cosigner)
|
|
}
|
|
}
|
|
|
|
gctx, cancel := options.GetTimeoutContext(ctx)
|
|
defer cancel()
|
|
|
|
c, err := options.GetRPCClient(gctx, ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
resp, err := c.InvokeScript(nefFile.Script, signers)
|
|
if err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
|
|
b, err = json.MarshalIndent(resp, "", " ")
|
|
if err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
|
|
fmt.Fprintln(ctx.App.Writer, string(b))
|
|
|
|
return nil
|
|
}
|
|
|
|
// ProjectConfig contains project metadata.
|
|
type ProjectConfig struct {
|
|
Name string
|
|
SafeMethods []string
|
|
SupportedStandards []string
|
|
Events []manifest.Event
|
|
}
|
|
|
|
func inspect(ctx *cli.Context) error {
|
|
in := ctx.String("in")
|
|
compile := ctx.Bool("compile")
|
|
if len(in) == 0 {
|
|
return cli.NewExitError(errNoInput, 1)
|
|
}
|
|
var (
|
|
b []byte
|
|
err error
|
|
)
|
|
if compile {
|
|
b, err = compiler.Compile(in, nil)
|
|
if err != nil {
|
|
return cli.NewExitError(fmt.Errorf("failed to compile: %w", err), 1)
|
|
}
|
|
} else {
|
|
f, err := ioutil.ReadFile(in)
|
|
if err != nil {
|
|
return cli.NewExitError(fmt.Errorf("failed to read .nef file: %w", err), 1)
|
|
}
|
|
nefFile, err := nef.FileFromBytes(f)
|
|
if err != nil {
|
|
return cli.NewExitError(fmt.Errorf("failed to restore .nef file: %w", err), 1)
|
|
}
|
|
b = nefFile.Script
|
|
}
|
|
v := vm.New()
|
|
v.LoadScript(b)
|
|
v.PrintOps(ctx.App.Writer)
|
|
|
|
return nil
|
|
}
|
|
|
|
func getAccFromContext(ctx *cli.Context) (*wallet.Account, *wallet.Wallet, error) {
|
|
var addr util.Uint160
|
|
|
|
wPath := ctx.String("wallet")
|
|
if len(wPath) == 0 {
|
|
return nil, nil, cli.NewExitError(errNoWallet, 1)
|
|
}
|
|
|
|
wall, err := wallet.NewWalletFromFile(wPath)
|
|
if err != nil {
|
|
return nil, nil, cli.NewExitError(err, 1)
|
|
}
|
|
addrFlag := ctx.Generic("address").(*flags.Address)
|
|
if addrFlag.IsSet {
|
|
addr = addrFlag.Uint160()
|
|
} else {
|
|
addr = wall.GetChangeAddress()
|
|
}
|
|
acc := wall.GetAccount(addr)
|
|
if acc == nil {
|
|
return nil, nil, cli.NewExitError(fmt.Errorf("wallet contains no account for '%s'", address.Uint160ToString(addr)), 1)
|
|
}
|
|
|
|
rawPass, err := input.ReadPassword(
|
|
fmt.Sprintf("Enter account %s password > ", address.Uint160ToString(addr)))
|
|
if err != nil {
|
|
return nil, nil, cli.NewExitError(err, 1)
|
|
}
|
|
pass := strings.TrimRight(string(rawPass), "\n")
|
|
err = acc.Decrypt(pass)
|
|
if err != nil {
|
|
return nil, nil, cli.NewExitError(err, 1)
|
|
}
|
|
return acc, wall, nil
|
|
}
|
|
|
|
// contractDeploy deploys contract.
|
|
func contractDeploy(ctx *cli.Context) error {
|
|
in := ctx.String("in")
|
|
if len(in) == 0 {
|
|
return cli.NewExitError(errNoInput, 1)
|
|
}
|
|
manifestFile := ctx.String("manifest")
|
|
if len(manifestFile) == 0 {
|
|
return cli.NewExitError(errNoManifestFile, 1)
|
|
}
|
|
gas := flags.Fixed8FromContext(ctx, "gas")
|
|
|
|
acc, _, err := getAccFromContext(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
sender, err := address.StringToUint160(acc.Address)
|
|
if err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
f, err := ioutil.ReadFile(in)
|
|
if err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
// Check the file.
|
|
nefFile, err := nef.FileFromBytes(f)
|
|
if err != nil {
|
|
return cli.NewExitError(fmt.Errorf("failed to read .nef file: %w", err), 1)
|
|
}
|
|
|
|
manifestBytes, err := ioutil.ReadFile(manifestFile)
|
|
if err != nil {
|
|
return cli.NewExitError(fmt.Errorf("failed to read manifest file: %w", err), 1)
|
|
}
|
|
m := &manifest.Manifest{}
|
|
err = json.Unmarshal(manifestBytes, m)
|
|
if err != nil {
|
|
return cli.NewExitError(fmt.Errorf("failed to restore manifest file: %w", err), 1)
|
|
}
|
|
|
|
gctx, cancel := options.GetTimeoutContext(ctx)
|
|
defer cancel()
|
|
|
|
c, err := options.GetRPCClient(gctx, ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
mgmtHash, err := c.GetNativeContractHash(nativenames.Management)
|
|
if err != nil {
|
|
return cli.NewExitError(fmt.Errorf("failed to get management contract's hash: %w", err), 1)
|
|
}
|
|
buf := io.NewBufBinWriter()
|
|
emit.AppCall(buf.BinWriter, mgmtHash, "deploy",
|
|
callflag.ReadStates|callflag.WriteStates|callflag.AllowNotify,
|
|
f, manifestBytes)
|
|
if buf.Err != nil {
|
|
return cli.NewExitError(fmt.Errorf("failed to create deployment script: %w", buf.Err), 1)
|
|
}
|
|
txScript := buf.Bytes()
|
|
// It doesn't require any signers.
|
|
invRes, err := c.InvokeScript(txScript, nil)
|
|
if err == nil && invRes.FaultException != "" {
|
|
err = errors.New(invRes.FaultException)
|
|
}
|
|
if err != nil {
|
|
return cli.NewExitError(fmt.Errorf("failed to test-invoke deployment script: %w", err), 1)
|
|
}
|
|
|
|
var txHash util.Uint256
|
|
if out := ctx.String("out"); out != "" {
|
|
tx, err := c.CreateTxFromScript(txScript, acc, invRes.GasConsumed, int64(gas), nil)
|
|
if err != nil {
|
|
return cli.NewExitError(fmt.Errorf("failed to create tx: %w", err), 1)
|
|
}
|
|
if err := paramcontext.InitAndSave(c.GetNetwork(), tx, acc, out); err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
txHash = tx.Hash()
|
|
} else {
|
|
txHash, err = c.SignAndPushInvocationTx(txScript, acc, invRes.GasConsumed, gas, nil)
|
|
if err != nil {
|
|
return cli.NewExitError(fmt.Errorf("failed to push invocation tx: %w", err), 1)
|
|
}
|
|
}
|
|
hash := state.CreateContractHash(sender, nefFile.Checksum, m.Name)
|
|
fmt.Fprintf(ctx.App.Writer, "Contract: %s\n", hash.StringLE())
|
|
fmt.Fprintln(ctx.App.Writer, txHash.StringLE())
|
|
return nil
|
|
}
|
|
|
|
// ParseContractConfig reads contract configuration file (.yaml) and returns unmarshalled ProjectConfig.
|
|
func ParseContractConfig(confFile string) (ProjectConfig, error) {
|
|
conf := ProjectConfig{}
|
|
confBytes, err := ioutil.ReadFile(confFile)
|
|
if err != nil {
|
|
return conf, cli.NewExitError(err, 1)
|
|
}
|
|
|
|
err = yaml.Unmarshal(confBytes, &conf)
|
|
if err != nil {
|
|
return conf, cli.NewExitError(fmt.Errorf("bad config: %w", err), 1)
|
|
}
|
|
return conf, nil
|
|
}
|
|
|
|
func parseCosigner(c string) (transaction.Signer, error) {
|
|
var (
|
|
err error
|
|
res = transaction.Signer{
|
|
Scopes: transaction.Global,
|
|
}
|
|
)
|
|
data := strings.SplitN(c, ":", 2)
|
|
s := data[0]
|
|
res.Account, err = flags.ParseAddress(s)
|
|
if err != nil {
|
|
return res, err
|
|
}
|
|
if len(data) > 1 {
|
|
res.Scopes, err = transaction.ScopesFromString(data[1])
|
|
if err != nil {
|
|
return transaction.Signer{}, err
|
|
}
|
|
}
|
|
return res, nil
|
|
}
|