forked from TrueCloudLab/neoneo-go
Merge pull request #511 from nspcc-dev/contract-deployment
Contract deployment
This commit is contained in:
commit
080b236275
14 changed files with 351 additions and 87 deletions
|
@ -11,9 +11,13 @@ 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/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
)
|
)
|
||||||
|
@ -21,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")
|
||||||
)
|
)
|
||||||
|
@ -62,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",
|
||||||
|
@ -135,7 +168,17 @@ func initSmartContract(ctx *cli.Context) error {
|
||||||
// TODO: Fix the missing neo-go.yml file with the `init` command when the package manager is in place.
|
// TODO: Fix the missing neo-go.yml file with the `init` command when the package manager is in place.
|
||||||
if !ctx.Bool("skip-details") {
|
if !ctx.Bool("skip-details") {
|
||||||
details := parseContractDetails()
|
details := parseContractDetails()
|
||||||
if err := ioutil.WriteFile(filepath.Join(basePath, "neo-go.yml"), details.toStormFile(), 0644); err != nil {
|
details.ReturnType = rpc.ByteArray
|
||||||
|
details.Parameters = make([]rpc.StackParamType, 2)
|
||||||
|
details.Parameters[0] = rpc.String
|
||||||
|
details.Parameters[1] = rpc.Array
|
||||||
|
|
||||||
|
project := &ProjectConfig{Contract: details}
|
||||||
|
b, err := yaml.Marshal(project)
|
||||||
|
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)
|
return cli.NewExitError(err, 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -204,42 +247,14 @@ func testInvoke(ctx *cli.Context) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ContractDetails contains contract metadata.
|
// ProjectConfig contains project metadata.
|
||||||
type ContractDetails struct {
|
type ProjectConfig struct {
|
||||||
Author string
|
Version uint
|
||||||
Email string
|
Contract rpc.ContractDetails `yaml:"project"`
|
||||||
Version string
|
|
||||||
ProjectName string
|
|
||||||
Description string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d ContractDetails) toStormFile() []byte {
|
func parseContractDetails() rpc.ContractDetails {
|
||||||
buf := new(bytes.Buffer)
|
details := rpc.ContractDetails{}
|
||||||
|
|
||||||
buf.WriteString("# NEO-GO specific configuration. Do not modify this unless you know what you are doing!\n")
|
|
||||||
buf.WriteString("neo-go:\n")
|
|
||||||
buf.WriteString(" version: 1.0\n")
|
|
||||||
|
|
||||||
buf.WriteString("\n")
|
|
||||||
|
|
||||||
buf.WriteString("# Project section contains information about your smart contract\n")
|
|
||||||
buf.WriteString("project:\n")
|
|
||||||
buf.WriteString(" author: " + d.Author)
|
|
||||||
buf.WriteString(" email: " + d.Email)
|
|
||||||
buf.WriteString(" version: " + d.Version)
|
|
||||||
buf.WriteString(" name: " + d.ProjectName)
|
|
||||||
buf.WriteString(" description: " + d.Description)
|
|
||||||
|
|
||||||
buf.WriteString("\n")
|
|
||||||
|
|
||||||
buf.WriteString("# Module section contains a list of imported modules\n")
|
|
||||||
buf.WriteString("# This will be automatically managed by the neo-go package manager\n")
|
|
||||||
buf.WriteString("modules: \n")
|
|
||||||
return buf.Bytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseContractDetails() ContractDetails {
|
|
||||||
details := ContractDetails{}
|
|
||||||
reader := bufio.NewReader(os.Stdin)
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
|
||||||
fmt.Print("Author: ")
|
fmt.Print("Author: ")
|
||||||
|
@ -282,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
|
||||||
|
}
|
||||||
|
|
|
@ -77,6 +77,9 @@ type UnspentBalance struct {
|
||||||
Value util.Fixed8 `json:"value"`
|
Value util.Fixed8 `json:"value"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UnspentBalances is a slice of UnspentBalance (mostly needed to sort them).
|
||||||
|
type UnspentBalances []UnspentBalance
|
||||||
|
|
||||||
// AccountState represents the state of a NEO account.
|
// AccountState represents the state of a NEO account.
|
||||||
type AccountState struct {
|
type AccountState struct {
|
||||||
Version uint8
|
Version uint8
|
||||||
|
@ -156,3 +159,12 @@ func (s *AccountState) GetBalanceValues() map[util.Uint256]util.Fixed8 {
|
||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Len returns the length of UnspentBalances (used to sort things).
|
||||||
|
func (us UnspentBalances) Len() int { return len(us) }
|
||||||
|
|
||||||
|
// Less compares two elements of UnspentBalances (used to sort things).
|
||||||
|
func (us UnspentBalances) Less(i, j int) bool { return us[i].Value < us[j].Value }
|
||||||
|
|
||||||
|
// Swap swaps two elements of UnspentBalances (used to sort things).
|
||||||
|
func (us UnspentBalances) Swap(i, j int) { us[i], us[j] = us[j], us[i] }
|
||||||
|
|
|
@ -943,9 +943,11 @@ func (bc *Blockchain) VerifyTx(t *transaction.Transaction, block *Block) error {
|
||||||
if ok := bc.verifyInputs(t); !ok {
|
if ok := bc.verifyInputs(t); !ok {
|
||||||
return errors.New("invalid transaction's inputs")
|
return errors.New("invalid transaction's inputs")
|
||||||
}
|
}
|
||||||
|
if block == nil {
|
||||||
if ok := bc.memPool.Verify(t); !ok {
|
if ok := bc.memPool.Verify(t); !ok {
|
||||||
return errors.New("invalid transaction due to conflicts with the memory pool")
|
return errors.New("invalid transaction due to conflicts with the memory pool")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if IsDoubleSpend(bc.store, t) {
|
if IsDoubleSpend(bc.store, t) {
|
||||||
return errors.New("invalid transaction caused by double spending")
|
return errors.New("invalid transaction caused by double spending")
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,12 +17,13 @@ type InvocationTX struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewInvocationTX returns a new invocation transaction.
|
// NewInvocationTX returns a new invocation transaction.
|
||||||
func NewInvocationTX(script []byte) *Transaction {
|
func NewInvocationTX(script []byte, gas util.Fixed8) *Transaction {
|
||||||
return &Transaction{
|
return &Transaction{
|
||||||
Type: InvocationType,
|
Type: InvocationType,
|
||||||
Version: 1,
|
Version: 1,
|
||||||
Data: &InvocationTX{
|
Data: &InvocationTX{
|
||||||
Script: script,
|
Script: script,
|
||||||
|
Gas: gas,
|
||||||
Version: 1,
|
Version: 1,
|
||||||
},
|
},
|
||||||
Attributes: []*Attribute{},
|
Attributes: []*Attribute{},
|
||||||
|
|
|
@ -98,7 +98,7 @@ func TestDecodeEncodeInvocationTX(t *testing.T) {
|
||||||
|
|
||||||
func TestNewInvocationTX(t *testing.T) {
|
func TestNewInvocationTX(t *testing.T) {
|
||||||
script := []byte{0x51}
|
script := []byte{0x51}
|
||||||
tx := NewInvocationTX(script)
|
tx := NewInvocationTX(script, 1)
|
||||||
txData := tx.Data.(*InvocationTX)
|
txData := tx.Data.(*InvocationTX)
|
||||||
assert.Equal(t, InvocationType, tx.Type)
|
assert.Equal(t, InvocationType, tx.Type)
|
||||||
assert.Equal(t, tx.Version, txData.Version)
|
assert.Equal(t, tx.Version, txData.Version)
|
||||||
|
|
|
@ -11,7 +11,10 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/CityOfZion/neo-go/pkg/core"
|
||||||
|
"github.com/CityOfZion/neo-go/pkg/core/transaction"
|
||||||
"github.com/CityOfZion/neo-go/pkg/crypto/keys"
|
"github.com/CityOfZion/neo-go/pkg/crypto/keys"
|
||||||
|
"github.com/CityOfZion/neo-go/pkg/util"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -152,6 +155,29 @@ func (c *Client) SetClient(cli *http.Client) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CalculateInputs creates input transactions for the specified amount of given
|
||||||
|
// asset belonging to specified address. This implementation uses GetUnspents
|
||||||
|
// JSON-RPC call internally, so make sure your RPC server suppors that.
|
||||||
|
func (c *Client) CalculateInputs(address string, asset util.Uint256, cost util.Fixed8) ([]transaction.Input, util.Fixed8, error) {
|
||||||
|
var utxos core.UnspentBalances
|
||||||
|
|
||||||
|
resp, err := c.GetUnspents(address)
|
||||||
|
if err != nil || resp.Error != nil {
|
||||||
|
if err == nil {
|
||||||
|
err = fmt.Errorf("remote returned %d: %s", resp.Error.Code, resp.Error.Message)
|
||||||
|
}
|
||||||
|
return nil, util.Fixed8(0), errors.Wrapf(err, "cannot get balance for address %v", address)
|
||||||
|
}
|
||||||
|
for _, ubi := range resp.Result.Balance {
|
||||||
|
if asset.Equals(ubi.AssetHash) {
|
||||||
|
utxos = ubi.Unspents
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return unspentsToInputs(utxos, cost)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) performRequest(method string, p params, v interface{}) error {
|
func (c *Client) performRequest(method string, p params, v interface{}) error {
|
||||||
var (
|
var (
|
||||||
r = request{
|
r = request{
|
||||||
|
|
|
@ -41,6 +41,7 @@ Supported methods
|
||||||
|
|
||||||
getblock
|
getblock
|
||||||
getaccountstate
|
getaccountstate
|
||||||
|
getunspents
|
||||||
invokescript
|
invokescript
|
||||||
invokefunction
|
invokefunction
|
||||||
sendrawtransaction
|
sendrawtransaction
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
|
"github.com/CityOfZion/neo-go/pkg/core"
|
||||||
"github.com/CityOfZion/neo-go/pkg/core/transaction"
|
"github.com/CityOfZion/neo-go/pkg/core/transaction"
|
||||||
"github.com/CityOfZion/neo-go/pkg/rpc/wrappers"
|
"github.com/CityOfZion/neo-go/pkg/rpc/wrappers"
|
||||||
"github.com/CityOfZion/neo-go/pkg/util"
|
"github.com/CityOfZion/neo-go/pkg/util"
|
||||||
|
@ -36,7 +37,6 @@ func (s NeoScanServer) GetBalance(address string) ([]*Unspent, error) {
|
||||||
if err = json.NewDecoder(res.Body).Decode(&balance); err != nil {
|
if err = json.NewDecoder(res.Body).Decode(&balance); err != nil {
|
||||||
return nil, errs.Wrap(err, "Failed to decode HTTP response")
|
return nil, errs.Wrap(err, "Failed to decode HTTP response")
|
||||||
}
|
}
|
||||||
|
|
||||||
return balance.Balance, nil
|
return balance.Balance, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,9 +55,6 @@ func filterSpecificAsset(asset string, balance []*Unspent, assetBalance *Unspent
|
||||||
func (s NeoScanServer) CalculateInputs(address string, assetIDUint util.Uint256, cost util.Fixed8) ([]transaction.Input, util.Fixed8, error) {
|
func (s NeoScanServer) CalculateInputs(address string, assetIDUint util.Uint256, cost util.Fixed8) ([]transaction.Input, util.Fixed8, error) {
|
||||||
var (
|
var (
|
||||||
err error
|
err error
|
||||||
num, i uint16
|
|
||||||
required = cost
|
|
||||||
selected = util.Fixed8(0)
|
|
||||||
us []*Unspent
|
us []*Unspent
|
||||||
assetUnspent Unspent
|
assetUnspent Unspent
|
||||||
assetID = wrappers.GlobalAssets[assetIDUint.ReverseString()]
|
assetID = wrappers.GlobalAssets[assetIDUint.ReverseString()]
|
||||||
|
@ -66,9 +63,19 @@ func (s NeoScanServer) CalculateInputs(address string, assetIDUint util.Uint256,
|
||||||
return nil, util.Fixed8(0), errs.Wrapf(err, "Cannot get balance for address %v", address)
|
return nil, util.Fixed8(0), errs.Wrapf(err, "Cannot get balance for address %v", address)
|
||||||
}
|
}
|
||||||
filterSpecificAsset(assetID, us, &assetUnspent)
|
filterSpecificAsset(assetID, us, &assetUnspent)
|
||||||
sort.Sort(assetUnspent.Unspent)
|
return unspentsToInputs(assetUnspent.Unspent, cost)
|
||||||
|
}
|
||||||
|
|
||||||
for _, us := range assetUnspent.Unspent {
|
// unspentsToInputs uses UnspentBalances to create a slice of inputs for a new
|
||||||
|
// transcation containing the required amount of asset.
|
||||||
|
func unspentsToInputs(utxos core.UnspentBalances, required util.Fixed8) ([]transaction.Input, util.Fixed8, error) {
|
||||||
|
var (
|
||||||
|
num, i uint16
|
||||||
|
selected = util.Fixed8(0)
|
||||||
|
)
|
||||||
|
sort.Sort(utxos)
|
||||||
|
|
||||||
|
for _, us := range utxos {
|
||||||
if selected >= required {
|
if selected >= required {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -82,8 +89,8 @@ func (s NeoScanServer) CalculateInputs(address string, assetIDUint util.Uint256,
|
||||||
inputs := make([]transaction.Input, 0, num)
|
inputs := make([]transaction.Input, 0, num)
|
||||||
for i = 0; i < num; i++ {
|
for i = 0; i < num; i++ {
|
||||||
inputs = append(inputs, transaction.Input{
|
inputs = append(inputs, transaction.Input{
|
||||||
PrevHash: assetUnspent.Unspent[i].TxID,
|
PrevHash: utxos[i].Tx,
|
||||||
PrevIndex: assetUnspent.Unspent[i].N,
|
PrevIndex: utxos[i].Index,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
package rpc
|
package rpc
|
||||||
|
|
||||||
import "github.com/CityOfZion/neo-go/pkg/util"
|
import (
|
||||||
|
"github.com/CityOfZion/neo-go/pkg/core"
|
||||||
|
"github.com/CityOfZion/neo-go/pkg/util"
|
||||||
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Definition of types, helper functions and variables
|
Definition of types, helper functions and variables
|
||||||
|
@ -15,19 +18,9 @@ type (
|
||||||
Path string // path to API endpoint without wallet address
|
Path string // path to API endpoint without wallet address
|
||||||
}
|
}
|
||||||
|
|
||||||
// UTXO stores unspent TX output for some transaction.
|
|
||||||
UTXO struct {
|
|
||||||
Value util.Fixed8
|
|
||||||
TxID util.Uint256
|
|
||||||
N uint16
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unspents is a slice of UTXOs (TODO: drop it?).
|
|
||||||
Unspents []UTXO
|
|
||||||
|
|
||||||
// Unspent stores Unspents per asset
|
// Unspent stores Unspents per asset
|
||||||
Unspent struct {
|
Unspent struct {
|
||||||
Unspent Unspents
|
Unspent core.UnspentBalances
|
||||||
Asset string // "NEO" / "GAS"
|
Asset string // "NEO" / "GAS"
|
||||||
Amount util.Fixed8 // total unspent of this asset
|
Amount util.Fixed8 // total unspent of this asset
|
||||||
}
|
}
|
||||||
|
@ -38,8 +31,3 @@ type (
|
||||||
Address string
|
Address string
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// functions for sorting array of `Unspents`
|
|
||||||
func (us Unspents) Len() int { return len(us) }
|
|
||||||
func (us Unspents) Less(i, j int) bool { return us[i].Value < us[j].Value }
|
|
||||||
func (us Unspents) Swap(i, j int) { us[i], us[j] = us[j], us[i] }
|
|
||||||
|
|
|
@ -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"
|
||||||
|
@ -39,6 +40,18 @@ func (c *Client) GetAccountState(address string) (*AccountStateResponse, error)
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUnspents returns UTXOs for the given NEO account.
|
||||||
|
func (c *Client) GetUnspents(address string) (*UnspentResponse, error) {
|
||||||
|
var (
|
||||||
|
params = newParams(address)
|
||||||
|
resp = &UnspentResponse{}
|
||||||
|
)
|
||||||
|
if err := c.performRequest("getunspents", params, resp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
// InvokeScript returns the result of the given script after running it true the VM.
|
// InvokeScript returns the result of the given script after running it true the VM.
|
||||||
// NOTE: This is a test invoke and will not affect the blockchain.
|
// NOTE: This is a test invoke and will not affect the blockchain.
|
||||||
func (c *Client) InvokeScript(script string) (*InvokeScriptResponse, error) {
|
func (c *Client) InvokeScript(script string) (*InvokeScriptResponse, error) {
|
||||||
|
@ -96,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 {
|
||||||
|
@ -113,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,
|
||||||
|
@ -130,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
|
||||||
|
@ -146,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
15
pkg/rpc/scdetails.go
Normal 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
|
||||||
|
}
|
|
@ -103,6 +103,23 @@ func (t *StackParamType) UnmarshalJSON(data []byte) (err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalYAML implements the YAML Marshaler interface.
|
||||||
|
func (t *StackParamType) MarshalYAML() (interface{}, error) {
|
||||||
|
return t.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalYAML implements the YAML Unmarshaler interface.
|
||||||
|
func (t *StackParamType) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
|
var name string
|
||||||
|
|
||||||
|
err := unmarshal(&name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*t, err = StackParamTypeFromString(name)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// StackParam represent a stack parameter.
|
// StackParam represent a stack parameter.
|
||||||
type StackParam struct {
|
type StackParam struct {
|
||||||
Type StackParamType `json:"type"`
|
Type StackParamType `json:"type"`
|
||||||
|
|
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -16,10 +20,7 @@ func CreateRawContractTransaction(params ContractTxParams) (*transaction.Transac
|
||||||
tx = transaction.NewContractTX()
|
tx = transaction.NewContractTX()
|
||||||
toAddressHash, fromAddressHash util.Uint160
|
toAddressHash, fromAddressHash util.Uint160
|
||||||
fromAddress string
|
fromAddress string
|
||||||
senderOutput, receiverOutput *transaction.Output
|
receiverOutput *transaction.Output
|
||||||
inputs []transaction.Input
|
|
||||||
spent util.Fixed8
|
|
||||||
witness transaction.Witness
|
|
||||||
|
|
||||||
wif, assetID, address, amount, balancer = params.wif, params.assetID, params.address, params.value, params.balancer
|
wif, assetID, address, amount, balancer = params.wif, params.assetID, params.address, params.value, params.balancer
|
||||||
)
|
)
|
||||||
|
@ -39,32 +40,57 @@ func CreateRawContractTransaction(params ContractTxParams) (*transaction.Transac
|
||||||
Data: fromAddressHash.Bytes(),
|
Data: fromAddressHash.Bytes(),
|
||||||
})
|
})
|
||||||
|
|
||||||
if inputs, spent, err = balancer.CalculateInputs(fromAddress, assetID, amount); err != nil {
|
if err = AddInputsAndUnspentsToTx(tx, fromAddress, assetID, amount, balancer); err != nil {
|
||||||
return nil, errs.Wrap(err, "Failed to get inputs for transaction")
|
return nil, errs.Wrap(err, "failed to add inputs and unspents to transaction")
|
||||||
|
}
|
||||||
|
receiverOutput = transaction.NewOutput(assetID, amount, toAddressHash)
|
||||||
|
tx.AddOutput(receiverOutput)
|
||||||
|
if err = SignTx(tx, &wif); err != nil {
|
||||||
|
return nil, errs.Wrap(err, "failed to sign tx")
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddInputsAndUnspentsToTx adds inputs needed to transaction and one output
|
||||||
|
// with change.
|
||||||
|
func AddInputsAndUnspentsToTx(tx *transaction.Transaction, address string, assetID util.Uint256, amount util.Fixed8, balancer BalanceGetter) error {
|
||||||
|
scriptHash, err := crypto.Uint160DecodeAddress(address)
|
||||||
|
if err != nil {
|
||||||
|
return errs.Wrapf(err, "failed to take script hash from address: %v", address)
|
||||||
|
}
|
||||||
|
inputs, spent, err := balancer.CalculateInputs(address, assetID, amount)
|
||||||
|
if err != nil {
|
||||||
|
return errs.Wrap(err, "failed to get inputs")
|
||||||
}
|
}
|
||||||
for _, input := range inputs {
|
for _, input := range inputs {
|
||||||
tx.AddInput(&input)
|
tx.AddInput(&input)
|
||||||
}
|
}
|
||||||
|
|
||||||
if senderUnspent := spent - amount; senderUnspent > 0 {
|
if senderUnspent := spent - amount; senderUnspent > 0 {
|
||||||
senderOutput = transaction.NewOutput(assetID, senderUnspent, fromAddressHash)
|
senderOutput := transaction.NewOutput(assetID, senderUnspent, scriptHash)
|
||||||
tx.AddOutput(senderOutput)
|
tx.AddOutput(senderOutput)
|
||||||
}
|
}
|
||||||
receiverOutput = transaction.NewOutput(assetID, amount, toAddressHash)
|
return nil
|
||||||
tx.AddOutput(receiverOutput)
|
}
|
||||||
|
|
||||||
|
// SignTx signs given transaction in-place using given key.
|
||||||
|
func SignTx(tx *transaction.Transaction, wif *keys.WIF) error {
|
||||||
|
var witness transaction.Witness
|
||||||
|
var err error
|
||||||
|
|
||||||
if witness.InvocationScript, err = GetInvocationScript(tx, wif); err != nil {
|
if witness.InvocationScript, err = GetInvocationScript(tx, wif); err != nil {
|
||||||
return nil, errs.Wrap(err, "Failed to create invocation script")
|
return errs.Wrap(err, "failed to create invocation script")
|
||||||
}
|
}
|
||||||
witness.VerificationScript = wif.GetVerificationScript()
|
witness.VerificationScript = wif.GetVerificationScript()
|
||||||
tx.Scripts = append(tx.Scripts, &witness)
|
tx.Scripts = append(tx.Scripts, &witness)
|
||||||
tx.Hash()
|
tx.Hash()
|
||||||
|
|
||||||
return tx, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetInvocationScript returns NEO VM script containing transaction signature.
|
// GetInvocationScript returns NEO VM script containing transaction signature.
|
||||||
func GetInvocationScript(tx *transaction.Transaction, wif keys.WIF) ([]byte, error) {
|
func GetInvocationScript(tx *transaction.Transaction, wif *keys.WIF) ([]byte, error) {
|
||||||
const (
|
const (
|
||||||
pushbytes64 = 0x40
|
pushbytes64 = 0x40
|
||||||
)
|
)
|
||||||
|
@ -84,3 +110,55 @@ func GetInvocationScript(tx *transaction.Transaction, wif keys.WIF) ([]byte, err
|
||||||
}
|
}
|
||||||
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
|
||||||
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package rpc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/CityOfZion/neo-go/pkg/core/transaction"
|
"github.com/CityOfZion/neo-go/pkg/core/transaction"
|
||||||
|
"github.com/CityOfZion/neo-go/pkg/rpc/wrappers"
|
||||||
"github.com/CityOfZion/neo-go/pkg/vm"
|
"github.com/CityOfZion/neo-go/pkg/vm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -27,6 +28,13 @@ type AccountStateResponse struct {
|
||||||
Result *Account `json:"result"`
|
Result *Account `json:"result"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UnspentResponse represents server response to the `getunspents` command.
|
||||||
|
type UnspentResponse struct {
|
||||||
|
responseHeader
|
||||||
|
Error *Error `json:"error,omitempty"`
|
||||||
|
Result *wrappers.Unspents `json:"result,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// Account represents details about a NEO account.
|
// Account represents details about a NEO account.
|
||||||
type Account struct {
|
type Account struct {
|
||||||
Version int `json:"version"`
|
Version int `json:"version"`
|
||||||
|
|
Loading…
Reference in a new issue