rpc/cli: implement contract deployment, fix #474

It's used like this:
./bin/neo-go contract deploy -i 1-print.avm -c neo-go.yml -e http://localhost:20331 -w KxDgvEKzgSBPPfuVfw67oPQBSjidEiqTHURKSDL1R7yGaGYAeYnr -g 100
This commit is contained in:
Roman Khimov 2019-11-20 16:07:43 +03:00
parent 310104667d
commit aae3e217a8
4 changed files with 199 additions and 28 deletions

View file

@ -11,7 +11,10 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"github.com/CityOfZion/neo-go/pkg/crypto/hash"
"github.com/CityOfZion/neo-go/pkg/crypto/keys"
"github.com/CityOfZion/neo-go/pkg/rpc" "github.com/CityOfZion/neo-go/pkg/rpc"
"github.com/CityOfZion/neo-go/pkg/util"
"github.com/CityOfZion/neo-go/pkg/vm" "github.com/CityOfZion/neo-go/pkg/vm"
"github.com/CityOfZion/neo-go/pkg/vm/compiler" "github.com/CityOfZion/neo-go/pkg/vm/compiler"
"github.com/go-yaml/yaml" "github.com/go-yaml/yaml"
@ -22,6 +25,8 @@ import (
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")
errNoWIF = errors.New("no WIF parameter found, specify it with the '--wif or -w' flag")
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")
) )
@ -63,6 +68,33 @@ func NewCommands() []cli.Command {
}, },
}, },
}, },
{
Name: "deploy",
Usage: "deploy a smart contract (.avm with description)",
Action: contractDeploy,
Flags: []cli.Flag{
cli.StringFlag{
Name: "in, i",
Usage: "Input file for the smart contract (*.avm)",
},
cli.StringFlag{
Name: "config, c",
Usage: "configuration input file (*.yml)",
},
cli.StringFlag{
Name: "endpoint, e",
Usage: "RPC endpoint address (like 'http://seed4.ngd.network:20332')",
},
cli.StringFlag{
Name: "wif, w",
Usage: "key to sign deployed transaction (in wif format)",
},
cli.IntFlag{
Name: "gas, g",
Usage: "gas to pay for contract deployment",
},
},
},
{ {
Name: "testinvoke", Name: "testinvoke",
Usage: "Test an invocation of a smart contract on the blockchain", Usage: "Test an invocation of a smart contract on the blockchain",
@ -218,25 +250,11 @@ func testInvoke(ctx *cli.Context) error {
// ProjectConfig contains project metadata. // ProjectConfig contains project metadata.
type ProjectConfig struct { type ProjectConfig struct {
Version uint Version uint
Contract ContractDetails `yaml:"project"` Contract rpc.ContractDetails `yaml:"project"`
} }
// ContractDetails contains contract metadata. func parseContractDetails() rpc.ContractDetails {
type ContractDetails struct { details := rpc.ContractDetails{}
Author string
Email string
Version string
ProjectName string `yaml:"name"`
Description string
HasStorage bool
HasDynamicInvocation bool
IsPayable bool
ReturnType rpc.StackParamType
Parameters []rpc.StackParamType
}
func parseContractDetails() ContractDetails {
details := ContractDetails{}
reader := bufio.NewReader(os.Stdin) reader := bufio.NewReader(os.Stdin)
fmt.Print("Author: ") fmt.Print("Author: ")
@ -279,3 +297,55 @@ func inspect(ctx *cli.Context) error {
return nil return nil
} }
// contractDeploy deploys contract.
func contractDeploy(ctx *cli.Context) error {
in := ctx.String("in")
if len(in) == 0 {
return cli.NewExitError(errNoInput, 1)
}
confFile := ctx.String("config")
if len(confFile) == 0 {
return cli.NewExitError(errNoConfFile, 1)
}
endpoint := ctx.String("endpoint")
if len(endpoint) == 0 {
return cli.NewExitError(errNoEndpoint, 1)
}
wifStr := ctx.String("wif")
if len(wifStr) == 0 {
return cli.NewExitError(errNoWIF, 1)
}
gas := util.Fixed8FromInt64(int64(ctx.Int("gas")))
wif, err := keys.WIFDecode(wifStr, 0)
if err != nil {
return cli.NewExitError(fmt.Errorf("bad wif: %v", err), 1)
}
avm, err := ioutil.ReadFile(in)
if err != nil {
return cli.NewExitError(err, 1)
}
confBytes, err := ioutil.ReadFile(confFile)
if err != nil {
return cli.NewExitError(err, 1)
}
conf := ProjectConfig{}
err = yaml.Unmarshal(confBytes, &conf)
if err != nil {
return cli.NewExitError(fmt.Errorf("bad config: %v", err), 1)
}
client, err := rpc.NewClient(context.TODO(), endpoint, rpc.ClientOptions{})
if err != nil {
return cli.NewExitError(err, 1)
}
txHash, err := client.DeployContract(avm, &conf.Contract, wif, gas)
if err != nil {
return cli.NewExitError(fmt.Errorf("failed to deploy: %v", err), 1)
}
fmt.Printf("Sent deployment transaction %s for contract %s\n", txHash.ReverseString(), hash.Hash160(avm).ReverseString())
return nil
}

View file

@ -2,9 +2,10 @@ package rpc
import ( import (
"encoding/hex" "encoding/hex"
"fmt"
"github.com/CityOfZion/neo-go/pkg/core/transaction" "github.com/CityOfZion/neo-go/pkg/core/transaction"
"github.com/CityOfZion/neo-go/pkg/io" "github.com/CityOfZion/neo-go/pkg/crypto/keys"
"github.com/CityOfZion/neo-go/pkg/smartcontract" "github.com/CityOfZion/neo-go/pkg/smartcontract"
"github.com/CityOfZion/neo-go/pkg/util" "github.com/CityOfZion/neo-go/pkg/util"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -108,9 +109,9 @@ func (c *Client) Invoke(script string, params []smartcontract.Parameter) (*Invok
// The given hex string needs to be signed with a keypair. // The given hex string needs to be signed with a keypair.
// When the result of the response object is true, the TX has successfully // When the result of the response object is true, the TX has successfully
// been broadcasted to the network. // been broadcasted to the network.
func (c *Client) sendRawTransaction(rawTX string) (*response, error) { func (c *Client) sendRawTransaction(rawTX *transaction.Transaction) (*response, error) {
var ( var (
params = newParams(rawTX) params = newParams(hex.EncodeToString(rawTX.Bytes()))
resp = &response{} resp = &response{}
) )
if err := c.performRequest("sendrawtransaction", params, resp); err != nil { if err := c.performRequest("sendrawtransaction", params, resp); err != nil {
@ -125,9 +126,7 @@ func (c *Client) sendRawTransaction(rawTX string) (*response, error) {
func (c *Client) SendToAddress(asset util.Uint256, address string, amount util.Fixed8) (*SendToAddressResponse, error) { func (c *Client) SendToAddress(asset util.Uint256, address string, amount util.Fixed8) (*SendToAddressResponse, error) {
var ( var (
err error err error
buf = io.NewBufBinWriter()
rawTx *transaction.Transaction rawTx *transaction.Transaction
rawTxStr string
txParams = ContractTxParams{ txParams = ContractTxParams{
assetID: asset, assetID: asset,
address: address, address: address,
@ -142,12 +141,7 @@ func (c *Client) SendToAddress(asset util.Uint256, address string, amount util.F
if rawTx, err = CreateRawContractTransaction(txParams); err != nil { if rawTx, err = CreateRawContractTransaction(txParams); err != nil {
return nil, errors.Wrap(err, "failed to create raw transaction for `sendtoaddress`") return nil, errors.Wrap(err, "failed to create raw transaction for `sendtoaddress`")
} }
rawTx.EncodeBinary(buf.BinWriter) if resp, err = c.sendRawTransaction(rawTx); err != nil {
if buf.Err != nil {
return nil, errors.Wrap(buf.Err, "failed to encode raw transaction to binary for `sendtoaddress`")
}
rawTxStr = hex.EncodeToString(buf.Bytes())
if resp, err = c.sendRawTransaction(rawTxStr); err != nil {
return nil, errors.Wrap(err, "failed to send raw transaction") return nil, errors.Wrap(err, "failed to send raw transaction")
} }
response.Error = resp.Error response.Error = resp.Error
@ -158,3 +152,39 @@ func (c *Client) SendToAddress(asset util.Uint256, address string, amount util.F
} }
return response, nil return response, nil
} }
// DeployContract deploys given contract to the blockchain using given wif to
// sign the transaction and spending the amount of gas specified. It returns
// a hash of the deployment transaction and an error.
func (c *Client) DeployContract(avm []byte, contract *ContractDetails, wif *keys.WIF, gas util.Fixed8) (util.Uint256, error) {
var txHash util.Uint256
gasIDB, _ := hex.DecodeString("602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7")
gasID, _ := util.Uint256DecodeReverseBytes(gasIDB)
txScript, err := CreateDeploymentScript(avm, contract)
if err != nil {
return txHash, errors.Wrap(err, "failed creating deployment script")
}
tx := transaction.NewInvocationTX(txScript, gas)
fromAddress := wif.PrivateKey.Address()
if err = AddInputsAndUnspentsToTx(tx, fromAddress, gasID, gas, c); err != nil {
return txHash, errors.Wrap(err, "failed to add inputs and unspents to transaction")
}
if err = SignTx(tx, wif); err != nil {
return txHash, errors.Wrap(err, "failed to sign tx")
}
txHash = tx.Hash()
resp, err := c.sendRawTransaction(tx)
if err != nil {
return txHash, errors.Wrap(err, "failed sendning tx")
}
if resp.Error != nil {
return txHash, fmt.Errorf("remote returned %d: %s", resp.Error.Code, resp.Error.Message)
}
return txHash, nil
}

15
pkg/rpc/scdetails.go Normal file
View file

@ -0,0 +1,15 @@
package rpc
// ContractDetails contains contract metadata.
type ContractDetails struct {
Author string
Email string
Version string
ProjectName string `yaml:"name"`
Description string
HasStorage bool
HasDynamicInvocation bool
IsPayable bool
ReturnType StackParamType
Parameters []StackParamType
}

View file

@ -1,11 +1,15 @@
package rpc package rpc
import ( import (
"bytes"
"github.com/CityOfZion/neo-go/pkg/core/transaction" "github.com/CityOfZion/neo-go/pkg/core/transaction"
"github.com/CityOfZion/neo-go/pkg/crypto" "github.com/CityOfZion/neo-go/pkg/crypto"
"github.com/CityOfZion/neo-go/pkg/crypto/keys" "github.com/CityOfZion/neo-go/pkg/crypto/keys"
"github.com/CityOfZion/neo-go/pkg/io" "github.com/CityOfZion/neo-go/pkg/io"
"github.com/CityOfZion/neo-go/pkg/smartcontract"
"github.com/CityOfZion/neo-go/pkg/util" "github.com/CityOfZion/neo-go/pkg/util"
"github.com/CityOfZion/neo-go/pkg/vm"
errs "github.com/pkg/errors" errs "github.com/pkg/errors"
) )
@ -106,3 +110,55 @@ func GetInvocationScript(tx *transaction.Transaction, wif *keys.WIF) ([]byte, er
} }
return append([]byte{pushbytes64}, signature...), nil return append([]byte{pushbytes64}, signature...), nil
} }
// CreateDeploymentScript returns a script that deploys given smart contract
// with its metadata.
func CreateDeploymentScript(avm []byte, contract *ContractDetails) ([]byte, error) {
var props smartcontract.PropertyState
script := new(bytes.Buffer)
if err := vm.EmitBytes(script, []byte(contract.Description)); err != nil {
return nil, err
}
if err := vm.EmitBytes(script, []byte(contract.Email)); err != nil {
return nil, err
}
if err := vm.EmitBytes(script, []byte(contract.Author)); err != nil {
return nil, err
}
if err := vm.EmitBytes(script, []byte(contract.Version)); err != nil {
return nil, err
}
if err := vm.EmitBytes(script, []byte(contract.ProjectName)); err != nil {
return nil, err
}
if contract.HasStorage {
props |= smartcontract.HasStorage
}
if contract.HasDynamicInvocation {
props |= smartcontract.HasDynamicInvoke
}
if contract.IsPayable {
props |= smartcontract.IsPayable
}
if err := vm.EmitInt(script, int64(props)); err != nil {
return nil, err
}
if err := vm.EmitInt(script, int64(contract.ReturnType)); err != nil {
return nil, err
}
params := make([]byte, len(contract.Parameters))
for k := range contract.Parameters {
params[k] = byte(contract.Parameters[k])
}
if err := vm.EmitBytes(script, params); err != nil {
return nil, err
}
if err := vm.EmitBytes(script, avm); err != nil {
return nil, err
}
if err := vm.EmitSyscall(script, "Neo.Contract.Create"); err != nil {
return nil, err
}
return script.Bytes(), nil
}