From 29882b076cfa3040c0760438eae482fdc7c0ad97 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Tue, 19 Nov 2019 17:35:04 +0300 Subject: [PATCH 1/9] rpc: remove duplicating definition of UTXO Port sorting methods to core. --- pkg/core/account_state.go | 12 ++++++++++++ pkg/rpc/neoScanBalanceGetter.go | 5 ++--- pkg/rpc/neoScanTypes.go | 22 +++++----------------- 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/pkg/core/account_state.go b/pkg/core/account_state.go index 7c14151d7..f2779340c 100644 --- a/pkg/core/account_state.go +++ b/pkg/core/account_state.go @@ -77,6 +77,9 @@ type UnspentBalance struct { 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. type AccountState struct { Version uint8 @@ -156,3 +159,12 @@ func (s *AccountState) GetBalanceValues() map[util.Uint256]util.Fixed8 { } 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] } diff --git a/pkg/rpc/neoScanBalanceGetter.go b/pkg/rpc/neoScanBalanceGetter.go index c51fba304..50d6fb289 100644 --- a/pkg/rpc/neoScanBalanceGetter.go +++ b/pkg/rpc/neoScanBalanceGetter.go @@ -36,7 +36,6 @@ func (s NeoScanServer) GetBalance(address string) ([]*Unspent, error) { if err = json.NewDecoder(res.Body).Decode(&balance); err != nil { return nil, errs.Wrap(err, "Failed to decode HTTP response") } - return balance.Balance, nil } @@ -82,8 +81,8 @@ func (s NeoScanServer) CalculateInputs(address string, assetIDUint util.Uint256, inputs := make([]transaction.Input, 0, num) for i = 0; i < num; i++ { inputs = append(inputs, transaction.Input{ - PrevHash: assetUnspent.Unspent[i].TxID, - PrevIndex: assetUnspent.Unspent[i].N, + PrevHash: assetUnspent.Unspent[i].Tx, + PrevIndex: assetUnspent.Unspent[i].Index, }) } diff --git a/pkg/rpc/neoScanTypes.go b/pkg/rpc/neoScanTypes.go index 9ef6a81b1..bafd0b648 100644 --- a/pkg/rpc/neoScanTypes.go +++ b/pkg/rpc/neoScanTypes.go @@ -1,6 +1,9 @@ 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 @@ -15,19 +18,9 @@ type ( 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 struct { - Unspent Unspents + Unspent core.UnspentBalances Asset string // "NEO" / "GAS" Amount util.Fixed8 // total unspent of this asset } @@ -38,8 +31,3 @@ type ( 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] } From 826a29cc988f847476d119bd000bbeada11d81b4 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Tue, 19 Nov 2019 20:15:32 +0300 Subject: [PATCH 2/9] rpc: implement client-side getunspents --- pkg/rpc/doc.go | 1 + pkg/rpc/rpc.go | 12 ++++++++++++ pkg/rpc/types.go | 8 ++++++++ 3 files changed, 21 insertions(+) diff --git a/pkg/rpc/doc.go b/pkg/rpc/doc.go index 57246e7ca..5ce0884ef 100644 --- a/pkg/rpc/doc.go +++ b/pkg/rpc/doc.go @@ -41,6 +41,7 @@ Supported methods getblock getaccountstate + getunspents invokescript invokefunction sendrawtransaction diff --git a/pkg/rpc/rpc.go b/pkg/rpc/rpc.go index fc5ba9c73..b14e6db55 100644 --- a/pkg/rpc/rpc.go +++ b/pkg/rpc/rpc.go @@ -39,6 +39,18 @@ func (c *Client) GetAccountState(address string) (*AccountStateResponse, error) 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. // NOTE: This is a test invoke and will not affect the blockchain. func (c *Client) InvokeScript(script string) (*InvokeScriptResponse, error) { diff --git a/pkg/rpc/types.go b/pkg/rpc/types.go index f87f712f8..058bc9046 100644 --- a/pkg/rpc/types.go +++ b/pkg/rpc/types.go @@ -2,6 +2,7 @@ package rpc import ( "github.com/CityOfZion/neo-go/pkg/core/transaction" + "github.com/CityOfZion/neo-go/pkg/rpc/wrappers" "github.com/CityOfZion/neo-go/pkg/vm" ) @@ -27,6 +28,13 @@ type AccountStateResponse struct { 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. type Account struct { Version int `json:"version"` From d93499cc6f7f64c050cc3536efe7bf0d5b36dd64 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Tue, 19 Nov 2019 20:16:15 +0300 Subject: [PATCH 3/9] rpc: implement CalculateInputs for RPC client Using getunspents RPC call. --- pkg/rpc/client.go | 26 ++++++++++++++++++++++++++ pkg/rpc/neoScanBalanceGetter.go | 22 +++++++++++++++------- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/pkg/rpc/client.go b/pkg/rpc/client.go index eae5d30ec..30c19ac9d 100644 --- a/pkg/rpc/client.go +++ b/pkg/rpc/client.go @@ -11,7 +11,10 @@ import ( "sync" "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/util" "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 { var ( r = request{ diff --git a/pkg/rpc/neoScanBalanceGetter.go b/pkg/rpc/neoScanBalanceGetter.go index 50d6fb289..543ca12cb 100644 --- a/pkg/rpc/neoScanBalanceGetter.go +++ b/pkg/rpc/neoScanBalanceGetter.go @@ -6,6 +6,7 @@ import ( "net/http" "sort" + "github.com/CityOfZion/neo-go/pkg/core" "github.com/CityOfZion/neo-go/pkg/core/transaction" "github.com/CityOfZion/neo-go/pkg/rpc/wrappers" "github.com/CityOfZion/neo-go/pkg/util" @@ -54,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) { var ( err error - num, i uint16 - required = cost - selected = util.Fixed8(0) us []*Unspent assetUnspent Unspent assetID = wrappers.GlobalAssets[assetIDUint.ReverseString()] @@ -65,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) } 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 { break } @@ -81,8 +89,8 @@ func (s NeoScanServer) CalculateInputs(address string, assetIDUint util.Uint256, inputs := make([]transaction.Input, 0, num) for i = 0; i < num; i++ { inputs = append(inputs, transaction.Input{ - PrevHash: assetUnspent.Unspent[i].Tx, - PrevIndex: assetUnspent.Unspent[i].Index, + PrevHash: utxos[i].Tx, + PrevIndex: utxos[i].Index, }) } From 27a57e1a2d3cef683081d8abc3cc231da59ee531 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Tue, 19 Nov 2019 20:23:14 +0300 Subject: [PATCH 4/9] transaction: allow system fee specification in NewInvocationTX It's not possible to create any deployment TX without it. --- pkg/core/transaction/invocation.go | 3 ++- pkg/core/transaction/transaction_test.go | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/core/transaction/invocation.go b/pkg/core/transaction/invocation.go index a0d82cb58..f303ddf16 100644 --- a/pkg/core/transaction/invocation.go +++ b/pkg/core/transaction/invocation.go @@ -17,12 +17,13 @@ type InvocationTX struct { } // NewInvocationTX returns a new invocation transaction. -func NewInvocationTX(script []byte) *Transaction { +func NewInvocationTX(script []byte, gas util.Fixed8) *Transaction { return &Transaction{ Type: InvocationType, Version: 1, Data: &InvocationTX{ Script: script, + Gas: gas, Version: 1, }, Attributes: []*Attribute{}, diff --git a/pkg/core/transaction/transaction_test.go b/pkg/core/transaction/transaction_test.go index 4d5ef35ac..45fc7bb0a 100644 --- a/pkg/core/transaction/transaction_test.go +++ b/pkg/core/transaction/transaction_test.go @@ -98,7 +98,7 @@ func TestDecodeEncodeInvocationTX(t *testing.T) { func TestNewInvocationTX(t *testing.T) { script := []byte{0x51} - tx := NewInvocationTX(script) + tx := NewInvocationTX(script, 1) txData := tx.Data.(*InvocationTX) assert.Equal(t, InvocationType, tx.Type) assert.Equal(t, tx.Version, txData.Version) From 34e2122e580c698d1ea3f1608d56fd8770f7e157 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Tue, 19 Nov 2019 20:37:27 +0300 Subject: [PATCH 5/9] core: only check tx against mempool if it's not in the block Fixes failure to process transaction from the block when it was relayed initially: WARN[0788] blockQueue: failed adding block into the blockchain blockHeight=7270 error="transaction 35088916403e5cf2152e16c3bc6e0fba20c955fba38543b9fa5c50a3d3a4ace5 failed to verify: invalid transaction due to conflicts with the memory pool" nextIndex=7271 WARN[0790] blockQueue: failed adding block into the blockchain blockHeight=7270 error="transaction 35088916403e5cf2152e16c3bc6e0fba20c955fba38543b9fa5c50a3d3a4ace5 failed to verify: invalid transaction due to conflicts with the memory pool" nextIndex=7271 WARN[0790] blockQueue: failed adding block into the blockchain blockHeight=7270 error="transaction 35088916403e5cf2152e16c3bc6e0fba20c955fba38543b9fa5c50a3d3a4ace5 failed to verify: invalid transaction due to conflicts with the memory pool" nextIndex=7271 --- pkg/core/blockchain.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index dd861c90e..22bf454a9 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -943,8 +943,10 @@ func (bc *Blockchain) VerifyTx(t *transaction.Transaction, block *Block) error { if ok := bc.verifyInputs(t); !ok { return errors.New("invalid transaction's inputs") } - if ok := bc.memPool.Verify(t); !ok { - return errors.New("invalid transaction due to conflicts with the memory pool") + if block == nil { + if ok := bc.memPool.Verify(t); !ok { + return errors.New("invalid transaction due to conflicts with the memory pool") + } } if IsDoubleSpend(bc.store, t) { return errors.New("invalid transaction caused by double spending") From 7d89ccdb6f11f945317ad8892e1e110a1e0216b4 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 20 Nov 2019 11:08:48 +0300 Subject: [PATCH 6/9] rpc: implement YAML marshaling/unmarshaling for StackParamType --- pkg/rpc/stack_param.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/pkg/rpc/stack_param.go b/pkg/rpc/stack_param.go index 343a64f9a..907e3e885 100644 --- a/pkg/rpc/stack_param.go +++ b/pkg/rpc/stack_param.go @@ -103,6 +103,23 @@ func (t *StackParamType) UnmarshalJSON(data []byte) (err error) { 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. type StackParam struct { Type StackParamType `json:"type"` From 3adc9150d3d60b78bdf774b0f29a88cd26814386 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 20 Nov 2019 12:29:37 +0300 Subject: [PATCH 7/9] cli: rework smart contract configs Use plain yaml structure. --- cli/smartcontract/smart_contract.go | 59 ++++++++++++++--------------- 1 file changed, 28 insertions(+), 31 deletions(-) diff --git a/cli/smartcontract/smart_contract.go b/cli/smartcontract/smart_contract.go index 05b3a6004..1573be2c7 100644 --- a/cli/smartcontract/smart_contract.go +++ b/cli/smartcontract/smart_contract.go @@ -14,6 +14,7 @@ import ( "github.com/CityOfZion/neo-go/pkg/rpc" "github.com/CityOfZion/neo-go/pkg/vm" "github.com/CityOfZion/neo-go/pkg/vm/compiler" + "github.com/go-yaml/yaml" "github.com/pkg/errors" "github.com/urfave/cli" ) @@ -135,7 +136,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. if !ctx.Bool("skip-details") { 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) } } @@ -204,38 +215,24 @@ func testInvoke(ctx *cli.Context) error { return nil } -// ContractDetails contains contract metadata. -type ContractDetails struct { - Author string - Email string - Version string - ProjectName string - Description string +// ProjectConfig contains project metadata. +type ProjectConfig struct { + Version uint + Contract ContractDetails `yaml:"project"` } -func (d ContractDetails) toStormFile() []byte { - buf := new(bytes.Buffer) - - 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() +// 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 rpc.StackParamType + Parameters []rpc.StackParamType } func parseContractDetails() ContractDetails { From 310104667dac8fb7e77d4eb4dca46488c805bc00 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 20 Nov 2019 13:14:01 +0300 Subject: [PATCH 8/9] rpc: refactor out reusable parts of CreateRawContractTransaction() Signing and inputs/outputs management is common for different transactions, so make separate functions for them. --- pkg/rpc/txBuilder.go | 46 ++++++++++++++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/pkg/rpc/txBuilder.go b/pkg/rpc/txBuilder.go index 72a2298af..abe1c581f 100644 --- a/pkg/rpc/txBuilder.go +++ b/pkg/rpc/txBuilder.go @@ -16,10 +16,7 @@ func CreateRawContractTransaction(params ContractTxParams) (*transaction.Transac tx = transaction.NewContractTX() toAddressHash, fromAddressHash util.Uint160 fromAddress string - senderOutput, receiverOutput *transaction.Output - inputs []transaction.Input - spent util.Fixed8 - witness transaction.Witness + receiverOutput *transaction.Output wif, assetID, address, amount, balancer = params.wif, params.assetID, params.address, params.value, params.balancer ) @@ -39,32 +36,57 @@ func CreateRawContractTransaction(params ContractTxParams) (*transaction.Transac Data: fromAddressHash.Bytes(), }) - if inputs, spent, err = balancer.CalculateInputs(fromAddress, assetID, amount); err != nil { - return nil, errs.Wrap(err, "Failed to get inputs for transaction") + if err = AddInputsAndUnspentsToTx(tx, fromAddress, assetID, amount, balancer); err != nil { + 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 { tx.AddInput(&input) } if senderUnspent := spent - amount; senderUnspent > 0 { - senderOutput = transaction.NewOutput(assetID, senderUnspent, fromAddressHash) + senderOutput := transaction.NewOutput(assetID, senderUnspent, scriptHash) tx.AddOutput(senderOutput) } - receiverOutput = transaction.NewOutput(assetID, amount, toAddressHash) - tx.AddOutput(receiverOutput) + return nil +} + +// 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 { - return nil, errs.Wrap(err, "Failed to create invocation script") + return errs.Wrap(err, "failed to create invocation script") } witness.VerificationScript = wif.GetVerificationScript() tx.Scripts = append(tx.Scripts, &witness) tx.Hash() - return tx, nil + return nil } // 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 ( pushbytes64 = 0x40 ) From aae3e217a82a988dce09973d5938017919d50895 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 20 Nov 2019 16:07:43 +0300 Subject: [PATCH 9/9] 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 --- cli/smartcontract/smart_contract.go | 104 +++++++++++++++++++++++----- pkg/rpc/rpc.go | 52 +++++++++++--- pkg/rpc/scdetails.go | 15 ++++ pkg/rpc/txBuilder.go | 56 +++++++++++++++ 4 files changed, 199 insertions(+), 28 deletions(-) create mode 100644 pkg/rpc/scdetails.go diff --git a/cli/smartcontract/smart_contract.go b/cli/smartcontract/smart_contract.go index 1573be2c7..d3f6796f5 100644 --- a/cli/smartcontract/smart_contract.go +++ b/cli/smartcontract/smart_contract.go @@ -11,7 +11,10 @@ import ( "os" "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/util" "github.com/CityOfZion/neo-go/pkg/vm" "github.com/CityOfZion/neo-go/pkg/vm/compiler" "github.com/go-yaml/yaml" @@ -22,6 +25,8 @@ import ( var ( 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") + 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") 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", 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. type ProjectConfig struct { Version uint - Contract ContractDetails `yaml:"project"` + Contract rpc.ContractDetails `yaml:"project"` } -// 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 rpc.StackParamType - Parameters []rpc.StackParamType -} - -func parseContractDetails() ContractDetails { - details := ContractDetails{} +func parseContractDetails() rpc.ContractDetails { + details := rpc.ContractDetails{} reader := bufio.NewReader(os.Stdin) fmt.Print("Author: ") @@ -279,3 +297,55 @@ func inspect(ctx *cli.Context) error { 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 +} diff --git a/pkg/rpc/rpc.go b/pkg/rpc/rpc.go index b14e6db55..6995c74d7 100644 --- a/pkg/rpc/rpc.go +++ b/pkg/rpc/rpc.go @@ -2,9 +2,10 @@ package rpc import ( "encoding/hex" + "fmt" "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/util" "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. // When the result of the response object is true, the TX has successfully // been broadcasted to the network. -func (c *Client) sendRawTransaction(rawTX string) (*response, error) { +func (c *Client) sendRawTransaction(rawTX *transaction.Transaction) (*response, error) { var ( - params = newParams(rawTX) + params = newParams(hex.EncodeToString(rawTX.Bytes())) resp = &response{} ) 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) { var ( err error - buf = io.NewBufBinWriter() rawTx *transaction.Transaction - rawTxStr string txParams = ContractTxParams{ assetID: asset, 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 { return nil, errors.Wrap(err, "failed to create raw transaction for `sendtoaddress`") } - rawTx.EncodeBinary(buf.BinWriter) - 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 { + if resp, err = c.sendRawTransaction(rawTx); err != nil { return nil, errors.Wrap(err, "failed to send raw transaction") } response.Error = resp.Error @@ -158,3 +152,39 @@ func (c *Client) SendToAddress(asset util.Uint256, address string, amount util.F } 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 +} diff --git a/pkg/rpc/scdetails.go b/pkg/rpc/scdetails.go new file mode 100644 index 000000000..5988e3436 --- /dev/null +++ b/pkg/rpc/scdetails.go @@ -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 +} diff --git a/pkg/rpc/txBuilder.go b/pkg/rpc/txBuilder.go index abe1c581f..b13dfd972 100644 --- a/pkg/rpc/txBuilder.go +++ b/pkg/rpc/txBuilder.go @@ -1,11 +1,15 @@ package rpc import ( + "bytes" + "github.com/CityOfZion/neo-go/pkg/core/transaction" "github.com/CityOfZion/neo-go/pkg/crypto" "github.com/CityOfZion/neo-go/pkg/crypto/keys" "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/vm" 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 } + +// 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 +}