Merge pull request #747 from nspcc-dev/fix-contract-deploy-gas-parameter

Use wallet in smartcontract deploy/invoke commands and calculate GAS properly
This commit is contained in:
Roman Khimov 2020-03-12 17:00:04 +03:00 committed by GitHub
commit 734778c1f9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 120 additions and 59 deletions

View file

@ -10,26 +10,31 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"syscall"
"github.com/go-yaml/yaml" "github.com/go-yaml/yaml"
"github.com/nspcc-dev/neo-go/cli/flags"
"github.com/nspcc-dev/neo-go/pkg/compiler" "github.com/nspcc-dev/neo-go/pkg/compiler"
"github.com/nspcc-dev/neo-go/pkg/crypto/hash" "github.com/nspcc-dev/neo-go/pkg/crypto/hash"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/encoding/address"
"github.com/nspcc-dev/neo-go/pkg/rpc/client" "github.com/nspcc-dev/neo-go/pkg/rpc/client"
"github.com/nspcc-dev/neo-go/pkg/rpc/request" "github.com/nspcc-dev/neo-go/pkg/rpc/request"
"github.com/nspcc-dev/neo-go/pkg/rpc/response/result" "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"
"github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/vm"
"github.com/nspcc-dev/neo-go/pkg/wallet"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/urfave/cli" "github.com/urfave/cli"
"golang.org/x/crypto/ssh/terminal"
) )
var ( var (
errNoEndpoint = errors.New("no RPC endpoint specified, use option '--endpoint' or '-e'") errNoEndpoint = errors.New("no RPC endpoint specified, use option '--endpoint' or '-e'")
errNoInput = errors.New("no input file was found, specify an input file with the '--in or -i' flag") 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") errNoConfFile = errors.New("no config file was found, specify a config file with the '--config' or '-c' flag")
errNoWIF = errors.New("no WIF parameter found, specify it with the '--wif or -w' flag") 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") 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") 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") errFileExist = errors.New("A file with given smart-contract name already exists")
@ -38,13 +43,17 @@ var (
Name: "endpoint, e", Name: "endpoint, e",
Usage: "trusted RPC endpoint address (like 'http://localhost:20331')", Usage: "trusted RPC endpoint address (like 'http://localhost:20331')",
} }
wifFlag = cli.StringFlag{ walletFlag = cli.StringFlag{
Name: "wif, w", Name: "wallet, w",
Usage: "key to sign deployed transaction (in wif format)", 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 = cli.Float64Flag{ gasFlag = cli.Float64Flag{
Name: "gas, g", Name: "gas, g",
Usage: "gas to pay for transaction", Usage: "gas to add to the transaction",
} }
) )
@ -86,8 +95,14 @@ func NewCommands() []cli.Command {
}, },
}, },
{ {
Name: "deploy", Name: "deploy",
Usage: "deploy a smart contract (.avm with description)", Usage: "deploy a smart contract (.avm 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. It may also
be required to add that to satisfy chain's policy regarding transaction size
and the minimum size fee (so if transaction send fails, try adding 0.001 GAS
to it).
`,
Action: contractDeploy, Action: contractDeploy,
Flags: []cli.Flag{ Flags: []cli.Flag{
cli.StringFlag{ cli.StringFlag{
@ -99,14 +114,15 @@ func NewCommands() []cli.Command {
Usage: "configuration input file (*.yml)", Usage: "configuration input file (*.yml)",
}, },
endpointFlag, endpointFlag,
wifFlag, walletFlag,
addressFlag,
gasFlag, gasFlag,
}, },
}, },
{ {
Name: "invoke", Name: "invoke",
Usage: "invoke deployed contract on the blockchain", Usage: "invoke deployed contract on the blockchain",
UsageText: "neo-go contract invoke -e endpoint -w wif [-g gas] scripthash [arguments...]", UsageText: "neo-go contract invoke -e endpoint -w wallet [-a address] [-g gas] scripthash [arguments...]",
Description: `Executes given (as a script hash) deployed script with the given arguments. Description: `Executes given (as a script hash) deployed script with the given arguments.
See testinvoke documentation for the details about parameters. It differs See testinvoke documentation for the details about parameters. It differs
from testinvoke in that this command sends an invocation transaction to from testinvoke in that this command sends an invocation transaction to
@ -115,14 +131,15 @@ func NewCommands() []cli.Command {
Action: invoke, Action: invoke,
Flags: []cli.Flag{ Flags: []cli.Flag{
endpointFlag, endpointFlag,
wifFlag, walletFlag,
addressFlag,
gasFlag, gasFlag,
}, },
}, },
{ {
Name: "invokefunction", Name: "invokefunction",
Usage: "invoke deployed contract on the blockchain", Usage: "invoke deployed contract on the blockchain",
UsageText: "neo-go contract invokefunction -e endpoint -w wif [-g gas] scripthash [method] [arguments...]", UsageText: "neo-go contract invokefunction -e endpoint -w wallet [-a address] [-g gas] scripthash [method] [arguments...]",
Description: `Executes given (as a script hash) deployed script with the given method and Description: `Executes given (as a script hash) deployed script with the given method and
and arguments. See testinvokefunction documentation for the details about and arguments. See testinvokefunction documentation for the details about
parameters. It differs from testinvokefunction in that this command sends an parameters. It differs from testinvokefunction in that this command sends an
@ -131,7 +148,8 @@ func NewCommands() []cli.Command {
Action: invokeFunction, Action: invokeFunction,
Flags: []cli.Flag{ Flags: []cli.Flag{
endpointFlag, endpointFlag,
wifFlag, walletFlag,
addressFlag,
gasFlag, gasFlag,
}, },
}, },
@ -365,7 +383,7 @@ func invokeInternal(ctx *cli.Context, withMethod bool, signAndPush bool) error {
params = make([]smartcontract.Parameter, 0) params = make([]smartcontract.Parameter, 0)
paramsStart = 1 paramsStart = 1
resp *result.Invoke resp *result.Invoke
wif *keys.WIF acc *wallet.Account
) )
endpoint := ctx.String("endpoint") endpoint := ctx.String("endpoint")
@ -394,8 +412,7 @@ func invokeInternal(ctx *cli.Context, withMethod bool, signAndPush bool) error {
if signAndPush { if signAndPush {
gas = util.Fixed8FromFloat(ctx.Float64("gas")) gas = util.Fixed8FromFloat(ctx.Float64("gas"))
acc, err = getAccFromContext(ctx)
wif, err = getWifFromContext(ctx)
if err != nil { if err != nil {
return err return err
} }
@ -421,7 +438,7 @@ func invokeInternal(ctx *cli.Context, withMethod bool, signAndPush bool) error {
if err != nil { if err != nil {
return cli.NewExitError(fmt.Errorf("bad script returned from the RPC node: %v", err), 1) return cli.NewExitError(fmt.Errorf("bad script returned from the RPC node: %v", err), 1)
} }
txHash, err := c.SignAndPushInvocationTx(script, wif, gas) txHash, err := c.SignAndPushInvocationTx(script, acc, 0, gas)
if err != nil { if err != nil {
return cli.NewExitError(fmt.Errorf("failed to push invocation tx: %v", err), 1) return cli.NewExitError(fmt.Errorf("failed to push invocation tx: %v", err), 1)
} }
@ -525,17 +542,41 @@ func inspect(ctx *cli.Context) error {
return nil return nil
} }
func getWifFromContext(ctx *cli.Context) (*keys.WIF, error) { func getAccFromContext(ctx *cli.Context) (*wallet.Account, error) {
wifStr := ctx.String("wif") var addr util.Uint160
if len(wifStr) == 0 {
return nil, cli.NewExitError(errNoWIF, 1) wPath := ctx.String("wallet")
if len(wPath) == 0 {
return nil, cli.NewExitError(errNoWallet, 1)
} }
wif, err := keys.WIFDecode(wifStr, 0) wall, err := wallet.NewWalletFromFile(wPath)
if err != nil { if err != nil {
return nil, cli.NewExitError(fmt.Errorf("bad wif: %v", err), 1) return nil, cli.NewExitError(err, 1)
} }
return wif, nil 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, cli.NewExitError(fmt.Errorf("wallet contains no account for '%s'", address.Uint160ToString(addr)), 1)
}
fmt.Printf("Enter account %s password > ", address.Uint160ToString(addr))
rawPass, err := terminal.ReadPassword(syscall.Stdin)
fmt.Println()
if err != nil {
return nil, cli.NewExitError(err, 1)
}
pass := strings.TrimRight(string(rawPass), "\n")
err = acc.Decrypt(pass)
if err != nil {
return nil, cli.NewExitError(err, 1)
}
return acc, nil
} }
// contractDeploy deploys contract. // contractDeploy deploys contract.
@ -554,7 +595,7 @@ func contractDeploy(ctx *cli.Context) error {
} }
gas := util.Fixed8FromFloat(ctx.Float64("gas")) gas := util.Fixed8FromFloat(ctx.Float64("gas"))
wif, err := getWifFromContext(ctx) acc, err := getAccFromContext(ctx)
if err != nil { if err != nil {
return err return err
} }
@ -583,7 +624,9 @@ func contractDeploy(ctx *cli.Context) error {
return cli.NewExitError(fmt.Errorf("failed to create deployment script: %v", err), 1) return cli.NewExitError(fmt.Errorf("failed to create deployment script: %v", err), 1)
} }
txHash, err := c.SignAndPushInvocationTx(txScript, wif, gas) sysfee := smartcontract.GetDeploymentPrice(request.DetailsToSCProperties(&conf.Contract))
txHash, err := c.SignAndPushInvocationTx(txScript, acc, sysfee, gas)
if err != nil { if err != nil {
return cli.NewExitError(fmt.Errorf("failed to push invocation tx: %v", err), 1) return cli.NewExitError(fmt.Errorf("failed to push invocation tx: %v", err), 1)
} }

View file

@ -93,18 +93,7 @@ func getSyscallPrice(v *vm.VM, id uint32) util.Fixed8 {
arg := estack.Peek(1).BigInt().Int64() arg := estack.Peek(1).BigInt().Int64()
return util.Fixed8FromInt64(arg * 5000) return util.Fixed8FromInt64(arg * 5000)
case neoContractCreate, neoContractMigrate, antSharesContractCreate, antSharesContractMigrate: case neoContractCreate, neoContractMigrate, antSharesContractCreate, antSharesContractMigrate:
fee := int64(100) return smartcontract.GetDeploymentPrice(smartcontract.PropertyState(estack.Peek(3).BigInt().Int64()))
props := smartcontract.PropertyState(estack.Peek(3).BigInt().Int64())
if props&smartcontract.HasStorage != 0 {
fee += 400
}
if props&smartcontract.HasDynamicInvoke != 0 {
fee += 500
}
return util.Fixed8FromInt64(fee)
case systemStoragePut, systemStoragePutEx, neoStoragePut, antSharesStoragePut: case systemStoragePut, systemStoragePutEx, neoStoragePut, antSharesStoragePut:
// price for storage PUT is 1 GAS per 1 KiB // price for storage PUT is 1 GAS per 1 KiB
keySize := len(estack.Peek(1).Bytes()) keySize := len(estack.Peek(1).Bytes())

View file

@ -6,7 +6,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/core" "github.com/nspcc-dev/neo-go/pkg/core"
"github.com/nspcc-dev/neo-go/pkg/core/block" "github.com/nspcc-dev/neo-go/pkg/core/block"
"github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/encoding/address"
"github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/rpc/request" "github.com/nspcc-dev/neo-go/pkg/rpc/request"
"github.com/nspcc-dev/neo-go/pkg/rpc/response/result" "github.com/nspcc-dev/neo-go/pkg/rpc/response/result"
@ -499,24 +499,30 @@ func (c *Client) TransferAsset(asset util.Uint256, address string, amount util.F
// SignAndPushInvocationTx signs and pushes given script as an invocation // SignAndPushInvocationTx signs and pushes given script as an invocation
// transaction using given wif to sign it and spending the amount of gas // transaction using given wif to sign it and spending the amount of gas
// specified. It returns a hash of the invocation transaction and an error. // specified. It returns a hash of the invocation transaction and an error.
func (c *Client) SignAndPushInvocationTx(script []byte, wif *keys.WIF, gas util.Fixed8) (util.Uint256, error) { func (c *Client) SignAndPushInvocationTx(script []byte, acc *wallet.Account, sysfee util.Fixed8, netfee util.Fixed8) (util.Uint256, error) {
var txHash util.Uint256 var txHash util.Uint256
var err error var err error
tx := transaction.NewInvocationTX(script, gas) tx := transaction.NewInvocationTX(script, sysfee)
gas := sysfee + netfee
fromAddress := wif.PrivateKey.Address()
if gas > 0 { if gas > 0 {
if err = request.AddInputsAndUnspentsToTx(tx, fromAddress, core.UtilityTokenID(), gas, c); err != nil { if err = request.AddInputsAndUnspentsToTx(tx, acc.Address, core.UtilityTokenID(), gas, c); err != nil {
return txHash, errors.Wrap(err, "failed to add inputs and unspents to transaction") return txHash, errors.Wrap(err, "failed to add inputs and unspents to transaction")
} }
} else {
addr, err := address.StringToUint160(acc.Address)
if err != nil {
return txHash, errors.Wrap(err, "failed to get address")
}
tx.Attributes = append(tx.Attributes,
transaction.Attribute{
Usage: transaction.Script,
Data: addr.BytesBE(),
})
} }
acc, err := wallet.NewAccountFromWIF(wif.S) if err = acc.SignTx(tx); err != nil {
if err != nil {
return txHash, err
} else if err = acc.SignTx(tx); err != nil {
return txHash, errors.Wrap(err, "failed to sign tx") return txHash, errors.Wrap(err, "failed to sign tx")
} }
txHash = tx.Hash() txHash = tx.Hash()

View file

@ -80,17 +80,10 @@ func AddInputsAndUnspentsToTx(tx *transaction.Transaction, addr string, assetID
return nil return nil
} }
// CreateDeploymentScript returns a script that deploys given smart contract // DetailsToSCProperties extract the fields needed from ContractDetails
// with its metadata. // and converts them to smartcontract.PropertyState.
func CreateDeploymentScript(avm []byte, contract *ContractDetails) ([]byte, error) { func DetailsToSCProperties(contract *ContractDetails) smartcontract.PropertyState {
var props smartcontract.PropertyState var props smartcontract.PropertyState
script := io.NewBufBinWriter()
emit.Bytes(script.BinWriter, []byte(contract.Description))
emit.Bytes(script.BinWriter, []byte(contract.Email))
emit.Bytes(script.BinWriter, []byte(contract.Author))
emit.Bytes(script.BinWriter, []byte(contract.Version))
emit.Bytes(script.BinWriter, []byte(contract.ProjectName))
if contract.HasStorage { if contract.HasStorage {
props |= smartcontract.HasStorage props |= smartcontract.HasStorage
} }
@ -100,7 +93,19 @@ func CreateDeploymentScript(avm []byte, contract *ContractDetails) ([]byte, erro
if contract.IsPayable { if contract.IsPayable {
props |= smartcontract.IsPayable props |= smartcontract.IsPayable
} }
emit.Int(script.BinWriter, int64(props)) return props
}
// CreateDeploymentScript returns a script that deploys given smart contract
// with its metadata.
func CreateDeploymentScript(avm []byte, contract *ContractDetails) ([]byte, error) {
script := io.NewBufBinWriter()
emit.Bytes(script.BinWriter, []byte(contract.Description))
emit.Bytes(script.BinWriter, []byte(contract.Email))
emit.Bytes(script.BinWriter, []byte(contract.Author))
emit.Bytes(script.BinWriter, []byte(contract.Version))
emit.Bytes(script.BinWriter, []byte(contract.ProjectName))
emit.Int(script.BinWriter, int64(DetailsToSCProperties(contract)))
emit.Int(script.BinWriter, int64(contract.ReturnType)) emit.Int(script.BinWriter, int64(contract.ReturnType))
params := make([]byte, len(contract.Parameters)) params := make([]byte, len(contract.Parameters))
for k := range contract.Parameters { for k := range contract.Parameters {

View file

@ -0,0 +1,18 @@
package smartcontract
import "github.com/nspcc-dev/neo-go/pkg/util"
// GetDeploymentPrice returns contract deployment price based on its properties.
func GetDeploymentPrice(props PropertyState) util.Fixed8 {
fee := int64(100)
if props&HasStorage != 0 {
fee += 400
}
if props&HasDynamicInvoke != 0 {
fee += 500
}
return util.Fixed8FromInt64(fee)
}