From 9c09ad9c899f1fb87b48ab815d3d55b89016ea6b Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Mon, 23 Mar 2020 17:31:28 +0300 Subject: [PATCH] rpc: fix marshalling of type-specific tx data closes #585 --- pkg/core/transaction/publish.go | 20 +++ pkg/core/transaction/register.go | 12 ++ pkg/core/transaction/state_descriptor.go | 42 +++++ pkg/core/transaction/transaction.go | 177 ++++++++++++++++++++- pkg/core/transaction/transaction_test.go | 194 ++++++++++++++++++++++- pkg/rpc/client/rpc.go | 2 + pkg/rpc/client/rpc_test.go | 106 +++++++------ pkg/rpc/response/result/block.go | 94 +++++++---- pkg/rpc/response/result/tx_raw_output.go | 78 +++++++-- pkg/rpc/server/server_test.go | 32 +++- 10 files changed, 648 insertions(+), 109 deletions(-) diff --git a/pkg/core/transaction/publish.go b/pkg/core/transaction/publish.go index 2acf80cbd..1cd89fa96 100644 --- a/pkg/core/transaction/publish.go +++ b/pkg/core/transaction/publish.go @@ -3,6 +3,7 @@ package transaction import ( "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/util" ) // PublishTX represents a publish transaction. @@ -62,3 +63,22 @@ func (tx *PublishTX) EncodeBinary(bw *io.BinWriter) { bw.WriteString(tx.Email) bw.WriteString(tx.Description) } + +// publishedContract is a JSON wrapper for PublishTransaction +type publishedContract struct { + Code publishedCode `json:"code"` + NeedStorage bool `json:"needstorage,omitempty"` + Name string `json:"name,omitempty"` + CodeVersion string `json:"version,omitempty"` + Author string `json:"author,omitempty"` + Email string `json:"email,omitempty"` + Description string `json:"description,omitempty"` +} + +// publishedCode is a JSON wrapper for PublishTransaction Code +type publishedCode struct { + Hash util.Uint160 `json:"hash,omitempty"` + Script string `json:"script,omitempty"` + ParamList []smartcontract.ParamType `json:"parameters,omitempty"` + ReturnType smartcontract.ParamType `json:"returntype,omitempty"` +} diff --git a/pkg/core/transaction/register.go b/pkg/core/transaction/register.go index 321c5652b..3ce5be61f 100644 --- a/pkg/core/transaction/register.go +++ b/pkg/core/transaction/register.go @@ -1,6 +1,8 @@ package transaction import ( + "encoding/json" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/util" @@ -51,3 +53,13 @@ func (tx *RegisterTX) EncodeBinary(bw *io.BinWriter) { bw.WriteBytes(tx.Owner.Bytes()) bw.WriteBytes(tx.Admin[:]) } + +// registeredAsset is a wrapper for RegisterTransaction +type registeredAsset struct { + AssetType AssetType `json:"type,omitempty"` + Name json.RawMessage `json:"name,omitempty"` + Amount util.Fixed8 `json:"amount,omitempty"` + Precision uint8 `json:"precision,omitempty"` + Owner keys.PublicKey `json:"owner,omitempty"` + Admin string `json:"admin,omitempty"` +} diff --git a/pkg/core/transaction/state_descriptor.go b/pkg/core/transaction/state_descriptor.go index 85f41a529..81d4295fe 100644 --- a/pkg/core/transaction/state_descriptor.go +++ b/pkg/core/transaction/state_descriptor.go @@ -1,6 +1,9 @@ package transaction import ( + "encoding/hex" + "encoding/json" + "github.com/nspcc-dev/neo-go/pkg/io" ) @@ -37,3 +40,42 @@ func (s *StateDescriptor) EncodeBinary(w *io.BinWriter) { w.WriteString(s.Field) w.WriteVarBytes(s.Value) } + +// stateDescriptor is a wrapper for StateDescriptor +type stateDescriptor struct { + Type DescStateType `json:"type"` + Key string `json:"key"` + Value string `json:"value"` + Field string `json:"field"` +} + +// MarshalJSON implements json.Marshaler interface. +func (s *StateDescriptor) MarshalJSON() ([]byte, error) { + return json.Marshal(&stateDescriptor{ + Type: s.Type, + Key: hex.EncodeToString(s.Key), + Value: hex.EncodeToString(s.Value), + Field: s.Field, + }) +} + +// UnmarshalJSON implements json.Unmarshaler interface. +func (s *StateDescriptor) UnmarshalJSON(data []byte) error { + t := new(stateDescriptor) + if err := json.Unmarshal(data, t); err != nil { + return err + } + key, err := hex.DecodeString(t.Key) + if err != nil { + return err + } + value, err := hex.DecodeString(t.Value) + if err != nil { + return err + } + s.Key = key + s.Value = value + s.Field = t.Field + s.Type = t.Type + return nil +} diff --git a/pkg/core/transaction/transaction.go b/pkg/core/transaction/transaction.go index 2bf7108ee..f26726034 100644 --- a/pkg/core/transaction/transaction.go +++ b/pkg/core/transaction/transaction.go @@ -1,10 +1,14 @@ package transaction import ( + "encoding/hex" + "encoding/json" "errors" "fmt" "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/io" "github.com/nspcc-dev/neo-go/pkg/util" ) @@ -18,28 +22,28 @@ const ( // Transaction is a process recorded in the NEO blockchain. type Transaction struct { // The type of the transaction. - Type TXType `json:"type"` + Type TXType // The trading version which is currently 0. - Version uint8 `json:"version"` + Version uint8 // Data specific to the type of the transaction. // This is always a pointer to a Transaction. - Data TXer `json:"-"` + Data TXer // Transaction attributes. - Attributes []Attribute `json:"attributes"` + Attributes []Attribute // The inputs of the transaction. - Inputs []Input `json:"vin"` + Inputs []Input // The outputs of the transaction. - Outputs []Output `json:"vout"` + Outputs []Output // The scripts that comes with this transaction. // Scripts exist out of the verification script // and invocation script. - Scripts []Witness `json:"scripts"` + Scripts []Witness // Hash of the transaction (double SHA256). hash util.Uint256 @@ -49,7 +53,7 @@ type Transaction struct { // Trimmed indicates this is a transaction from trimmed // data. - Trimmed bool `json:"-"` + Trimmed bool } // NewTrimmedTX returns a trimmed transaction with only its hash @@ -233,3 +237,160 @@ func (t *Transaction) Bytes() []byte { } return buf.Bytes() } + +// transactionJSON is a wrapper for Transaction and +// used for correct marhalling of transaction.Data +type transactionJSON struct { + TxID util.Uint256 `json:"txid"` + Size int `json:"size"` + Type TXType `json:"type"` + Version uint8 `json:"version"` + Attributes []Attribute `json:"attributes"` + Inputs []Input `json:"vin"` + Outputs []Output `json:"vout"` + Scripts []Witness `json:"scripts"` + + Claims []Input `json:"claims,omitempty"` + PublicKey *keys.PublicKey `json:"pubkey,omitempty"` + Script string `json:"script,omitempty"` + Gas util.Fixed8 `json:"gas,omitempty"` + Nonce uint32 `json:"nonce,omitempty"` + Contract *publishedContract `json:"contract,omitempty"` + Asset *registeredAsset `json:"asset,omitempty"` + Descriptors []*StateDescriptor `json:"descriptors,omitempty"` +} + +// MarshalJSON implements json.Marshaler interface. +func (t *Transaction) MarshalJSON() ([]byte, error) { + tx := transactionJSON{ + TxID: t.Hash(), + Size: io.GetVarSize(t), + Type: t.Type, + Version: t.Version, + Attributes: t.Attributes, + Inputs: t.Inputs, + Outputs: t.Outputs, + Scripts: t.Scripts, + } + switch t.Type { + case MinerType: + tx.Nonce = t.Data.(*MinerTX).Nonce + case ClaimType: + tx.Claims = t.Data.(*ClaimTX).Claims + case EnrollmentType: + tx.PublicKey = &t.Data.(*EnrollmentTX).PublicKey + case InvocationType: + tx.Script = hex.EncodeToString(t.Data.(*InvocationTX).Script) + tx.Gas = t.Data.(*InvocationTX).Gas + case PublishType: + transaction := t.Data.(*PublishTX) + tx.Contract = &publishedContract{ + Code: publishedCode{ + Hash: hash.Hash160(transaction.Script), + Script: hex.EncodeToString(transaction.Script), + ParamList: transaction.ParamList, + ReturnType: transaction.ReturnType, + }, + NeedStorage: transaction.NeedStorage, + Name: transaction.Name, + CodeVersion: transaction.CodeVersion, + Author: transaction.Author, + Email: transaction.Email, + Description: transaction.Description, + } + case RegisterType: + transaction := *t.Data.(*RegisterTX) + tx.Asset = ®isteredAsset{ + AssetType: transaction.AssetType, + Name: json.RawMessage(transaction.Name), + Amount: transaction.Amount, + Precision: transaction.Precision, + Owner: transaction.Owner, + Admin: address.Uint160ToString(transaction.Admin), + } + case StateType: + tx.Descriptors = t.Data.(*StateTX).Descriptors + } + return json.Marshal(tx) +} + +// UnmarshalJSON implements json.Unmarshaler interface. +func (t *Transaction) UnmarshalJSON(data []byte) error { + tx := new(transactionJSON) + if err := json.Unmarshal(data, tx); err != nil { + return err + } + t.Type = tx.Type + t.Version = tx.Version + t.Attributes = tx.Attributes + t.Inputs = tx.Inputs + t.Outputs = tx.Outputs + t.Scripts = tx.Scripts + switch tx.Type { + case MinerType: + t.Data = &MinerTX{ + Nonce: tx.Nonce, + } + case ClaimType: + t.Data = &ClaimTX{ + Claims: tx.Claims, + } + case EnrollmentType: + t.Data = &EnrollmentTX{ + PublicKey: *tx.PublicKey, + } + case InvocationType: + bytes, err := hex.DecodeString(tx.Script) + if err != nil { + return err + } + t.Data = &InvocationTX{ + Script: bytes, + Gas: tx.Gas, + Version: tx.Version, + } + case PublishType: + bytes, err := hex.DecodeString(tx.Contract.Code.Script) + if err != nil { + return err + } + t.Data = &PublishTX{ + Script: bytes, + ParamList: tx.Contract.Code.ParamList, + ReturnType: tx.Contract.Code.ReturnType, + NeedStorage: tx.Contract.NeedStorage, + Name: tx.Contract.Name, + CodeVersion: tx.Contract.CodeVersion, + Author: tx.Contract.Author, + Email: tx.Contract.Email, + Description: tx.Contract.Description, + Version: tx.Version, + } + case RegisterType: + admin, err := address.StringToUint160(tx.Asset.Admin) + if err != nil { + return err + } + t.Data = &RegisterTX{ + AssetType: tx.Asset.AssetType, + Name: string(tx.Asset.Name), + Amount: tx.Asset.Amount, + Precision: tx.Asset.Precision, + Owner: tx.Asset.Owner, + Admin: admin, + } + case StateType: + t.Data = &StateTX{ + Descriptors: tx.Descriptors, + } + case ContractType: + t.Data = &ContractTX{} + case IssueType: + t.Data = &IssueTX{} + } + if t.Hash() != tx.TxID { + return errors.New("txid doesn't match transaction hash") + } + + return nil +} diff --git a/pkg/core/transaction/transaction_test.go b/pkg/core/transaction/transaction_test.go index 34c3090db..aad72f0f0 100644 --- a/pkg/core/transaction/transaction_test.go +++ b/pkg/core/transaction/transaction_test.go @@ -4,6 +4,7 @@ import ( "encoding/hex" "testing" + "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/internal/testserdes" "github.com/nspcc-dev/neo-go/pkg/smartcontract" @@ -155,7 +156,7 @@ func TestEncodingTXWithNoData(t *testing.T) { require.Error(t, err) } -func TestMarshalUnmarshalJSON(t *testing.T) { +func TestMarshalUnmarshalJSONContractTX(t *testing.T) { tx := NewContractTX() tx.Outputs = []Output{{ AssetID: util.Uint256{1, 2, 3, 4}, @@ -167,5 +168,196 @@ func TestMarshalUnmarshalJSON(t *testing.T) { InvocationScript: []byte{5, 3, 1}, VerificationScript: []byte{2, 4, 6}, }} + tx.Data = &ContractTX{} + testserdes.MarshalUnmarshalJSON(t, tx, new(Transaction)) +} + +func TestMarshalUnmarshalJSONMinerTX(t *testing.T) { + tx := &Transaction{ + Type: MinerType, + Version: 0, + Data: &MinerTX{Nonce: 12345}, + Attributes: []Attribute{}, + Inputs: []Input{}, + Outputs: []Output{}, + Scripts: []Witness{}, + Trimmed: false, + } + + testserdes.MarshalUnmarshalJSON(t, tx, new(Transaction)) +} + +func TestMarshalUnmarshalJSONClaimTX(t *testing.T) { + tx := &Transaction{ + Type: ClaimType, + Version: 0, + Data: &ClaimTX{Claims: []Input{ + { + PrevHash: util.Uint256{1, 2, 3, 4}, + PrevIndex: uint16(56), + }, + }}, + Attributes: []Attribute{}, + Inputs: []Input{{ + PrevHash: util.Uint256{5, 6, 7, 8}, + PrevIndex: uint16(12), + }}, + Outputs: []Output{{ + AssetID: util.Uint256{1, 2, 3}, + Amount: util.Fixed8FromInt64(1), + ScriptHash: util.Uint160{1, 2, 3}, + Position: 0, + }}, + Scripts: []Witness{}, + Trimmed: false, + } + + testserdes.MarshalUnmarshalJSON(t, tx, new(Transaction)) +} + +func TestMarshalUnmarshalJSONEnrollmentTX(t *testing.T) { + str := "03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c" + pubKey, err := keys.NewPublicKeyFromString(str) + require.NoError(t, err) + tx := &Transaction{ + Type: EnrollmentType, + Version: 5, + Data: &EnrollmentTX{PublicKey: *pubKey}, + Attributes: []Attribute{}, + Inputs: []Input{{ + PrevHash: util.Uint256{5, 6, 7, 8}, + PrevIndex: uint16(12), + }}, + Outputs: []Output{{ + AssetID: util.Uint256{1, 2, 3}, + Amount: util.Fixed8FromInt64(1), + ScriptHash: util.Uint160{1, 2, 3}, + Position: 0, + }}, + Scripts: []Witness{}, + Trimmed: false, + } + + testserdes.MarshalUnmarshalJSON(t, tx, new(Transaction)) +} + +func TestMarshalUnmarshalJSONInvocationTX(t *testing.T) { + tx := &Transaction{ + Type: InvocationType, + Version: 3, + Data: &InvocationTX{ + Script: []byte{1, 2, 3, 4}, + Gas: util.Fixed8FromFloat(100), + Version: 3, + }, + Attributes: []Attribute{}, + Inputs: []Input{{ + PrevHash: util.Uint256{5, 6, 7, 8}, + PrevIndex: uint16(12), + }}, + Outputs: []Output{{ + AssetID: util.Uint256{1, 2, 3}, + Amount: util.Fixed8FromInt64(1), + ScriptHash: util.Uint160{1, 2, 3}, + Position: 0, + }}, + Scripts: []Witness{}, + Trimmed: false, + } + + testserdes.MarshalUnmarshalJSON(t, tx, new(Transaction)) +} + +func TestMarshalUnmarshalJSONPublishTX(t *testing.T) { + tx := &Transaction{ + Type: PublishType, + Version: 5, + Data: &PublishTX{ + Script: []byte{1, 2, 3, 4}, + ParamList: []smartcontract.ParamType{smartcontract.IntegerType, smartcontract.Hash160Type}, + ReturnType: smartcontract.BoolType, + NeedStorage: true, + Name: "Name", + CodeVersion: "1.0", + Author: "Author", + Email: "Email", + Description: "Description", + Version: 5, + }, + Attributes: []Attribute{}, + Inputs: []Input{{ + PrevHash: util.Uint256{5, 6, 7, 8}, + PrevIndex: uint16(12), + }}, + Outputs: []Output{{ + AssetID: util.Uint256{1, 2, 3}, + Amount: util.Fixed8FromInt64(1), + ScriptHash: util.Uint160{1, 2, 3}, + Position: 0, + }}, + Scripts: []Witness{}, + Trimmed: false, + } + + testserdes.MarshalUnmarshalJSON(t, tx, new(Transaction)) +} + +func TestMarshalUnmarshalJSONRegisterTX(t *testing.T) { + tx := &Transaction{ + Type: RegisterType, + Version: 5, + Data: &RegisterTX{ + AssetType: 0, + Name: `[{"lang":"zh-CN","name":"小蚁股"},{"lang":"en","name":"AntShare"}]`, + Amount: 1000000, + Precision: 0, + Owner: keys.PublicKey{}, + Admin: util.Uint160{}, + }, + Attributes: []Attribute{}, + Inputs: []Input{{ + PrevHash: util.Uint256{5, 6, 7, 8}, + PrevIndex: uint16(12), + }}, + Outputs: []Output{{ + AssetID: util.Uint256{1, 2, 3}, + Amount: util.Fixed8FromInt64(1), + ScriptHash: util.Uint160{1, 2, 3}, + Position: 0, + }}, + Scripts: []Witness{}, + Trimmed: false, + } + + testserdes.MarshalUnmarshalJSON(t, tx, new(Transaction)) +} + +func TestMarshalUnmarshalJSONStateTX(t *testing.T) { + tx := &Transaction{ + Type: StateType, + Version: 5, + Data: &StateTX{ + Descriptors: []*StateDescriptor{&StateDescriptor{ + Type: Validator, + Key: []byte{1, 2, 3}, + Value: []byte{4, 5, 6}, + Field: "Field", + }}, + }, + Attributes: []Attribute{}, + Inputs: []Input{{ + PrevHash: util.Uint256{5, 6, 7, 8}, + PrevIndex: uint16(12), + }}, + Outputs: []Output{{ + AssetID: util.Uint256{1, 2, 3}, + Amount: util.Fixed8FromInt64(1), + ScriptHash: util.Uint160{1, 2, 3}, + Position: 0, + }}, + Scripts: []Witness{}, + Trimmed: false, + } + testserdes.MarshalUnmarshalJSON(t, tx, new(Transaction)) } diff --git a/pkg/rpc/client/rpc.go b/pkg/rpc/client/rpc.go index c19e05989..bb0144582 100644 --- a/pkg/rpc/client/rpc.go +++ b/pkg/rpc/client/rpc.go @@ -104,6 +104,7 @@ func (c *Client) getBlock(params request.RawParams) (*block.Block, error) { // GetBlockByIndexVerbose returns a block wrapper with additional metadata by // its height. +// NOTE: to get transaction.ID and transaction.Size, use t.Hash() and io.GetVarSize(t) respectively. func (c *Client) GetBlockByIndexVerbose(index uint32) (*result.Block, error) { return c.getBlockVerbose(request.NewRawParams(index, 1)) } @@ -289,6 +290,7 @@ func (c *Client) GetRawTransaction(hash util.Uint256) (*transaction.Transaction, // GetRawTransactionVerbose returns a transaction wrapper with additional // metadata by transaction's hash. +// NOTE: to get transaction.ID and transaction.Size, use t.Hash() and io.GetVarSize(t) respectively. func (c *Client) GetRawTransactionVerbose(hash util.Uint256) (*result.TransactionOutputRaw, error) { var ( params = request.NewRawParams(hash.StringLE(), 1) diff --git a/pkg/rpc/client/rpc_test.go b/pkg/rpc/client/rpc_test.go index 249fd2668..6cef59d25 100644 --- a/pkg/rpc/client/rpc_test.go +++ b/pkg/rpc/client/rpc_test.go @@ -172,10 +172,6 @@ var rpcClientTestCases = map[string][]rpcClientTestCase{ if err != nil { panic(err) } - txID, err := util.Uint256DecodeStringLE("cb6ddb5f99d6af4c94a6c396d5294472f2eebc91a2c933e0f527422296fa9fb2") - if err != nil { - panic(err) - } invScript, err := hex.DecodeString("40356a91d94e398170e47447d6a0f60aa5470e209782a5452403115a49166db3e1c4a3898122db19f779c30f8ccd0b7d401acdf71eda340655e4ae5237a64961bf4034dd47955e5a71627dafc39dd92999140e9eaeec6b11dbb2b313efa3f1093ed915b4455e199c69ec53778f94ffc236b92f8b97fff97a1f6bbb3770c0c0b3844a40fbe743bd5c90b2f5255e0b073281d7aeb2fb516572f36bec8446bcc37ac755cbf10d08b16c95644db1b2dddc2df5daa377880b20198fc7b967ac6e76474b22df") if err != nil { panic(err) @@ -184,6 +180,18 @@ var rpcClientTestCases = map[string][]rpcClientTestCase{ if err != nil { panic(err) } + tx := &transaction.Transaction{ + Type: transaction.MinerType, + Version: 0, + Data: &transaction.MinerTX{Nonce: 4266257741}, + Attributes: []transaction.Attribute{}, + Inputs: []transaction.Input{}, + Outputs: []transaction.Output{}, + Scripts: []transaction.Witness{}, + Trimmed: false, + } + // Update hashes for correct result comparison. + _ = tx.Hash() return &result.Block{ Hash: hash, Size: 452, @@ -201,17 +209,11 @@ var rpcClientTestCases = map[string][]rpcClientTestCase{ VerificationScript: verifScript, }, Tx: []result.Tx{{ - TxID: txID, - Size: 10, - Type: transaction.MinerType, - Version: 0, - Attributes: []transaction.Attribute{}, - VIn: []transaction.Input{}, - VOut: []transaction.Output{}, - Scripts: []transaction.Witness{}, - SysFee: 0, - NetFee: 0, - Nonce: 4266257741, + Transaction: tx, + Fees: result.Fees{ + SysFee: 0, + NetFee: 0, + }, }}, } }, @@ -265,10 +267,6 @@ var rpcClientTestCases = map[string][]rpcClientTestCase{ if err != nil { panic(err) } - txID, err := util.Uint256DecodeStringLE("cb6ddb5f99d6af4c94a6c396d5294472f2eebc91a2c933e0f527422296fa9fb2") - if err != nil { - panic(err) - } invScript, err := hex.DecodeString("40356a91d94e398170e47447d6a0f60aa5470e209782a5452403115a49166db3e1c4a3898122db19f779c30f8ccd0b7d401acdf71eda340655e4ae5237a64961bf4034dd47955e5a71627dafc39dd92999140e9eaeec6b11dbb2b313efa3f1093ed915b4455e199c69ec53778f94ffc236b92f8b97fff97a1f6bbb3770c0c0b3844a40fbe743bd5c90b2f5255e0b073281d7aeb2fb516572f36bec8446bcc37ac755cbf10d08b16c95644db1b2dddc2df5daa377880b20198fc7b967ac6e76474b22df") if err != nil { panic(err) @@ -277,6 +275,18 @@ var rpcClientTestCases = map[string][]rpcClientTestCase{ if err != nil { panic(err) } + tx := &transaction.Transaction{ + Type: transaction.MinerType, + Version: 0, + Data: &transaction.MinerTX{Nonce: 4266257741}, + Attributes: []transaction.Attribute{}, + Inputs: []transaction.Input{}, + Outputs: []transaction.Output{}, + Scripts: []transaction.Witness{}, + Trimmed: false, + } + // Update hashes for correct result comparison. + _ = tx.Hash() return &result.Block{ Hash: hash, Size: 452, @@ -294,17 +304,11 @@ var rpcClientTestCases = map[string][]rpcClientTestCase{ VerificationScript: verifScript, }, Tx: []result.Tx{{ - TxID: txID, - Size: 10, - Type: transaction.MinerType, - Version: 0, - Attributes: []transaction.Attribute{}, - VIn: []transaction.Input{}, - VOut: []transaction.Output{}, - Scripts: []transaction.Witness{}, - SysFee: 0, - NetFee: 0, - Nonce: 4266257741, + Transaction: tx, + Fees: result.Fees{ + SysFee: 0, + NetFee: 0, + }, }}, } }, @@ -646,34 +650,34 @@ var rpcClientTestCases = map[string][]rpcClientTestCase{ } return c.GetRawTransactionVerbose(hash) }, - serverResponse: `{"id":1,"jsonrpc":"2.0","result":{"type":"MinerTransaction","version":0,"attributes":[],"vin":[],"vout":[],"scripts":[],"txid":"0xcb6ddb5f99d6af4c94a6c396d5294472f2eebc91a2c933e0f527422296fa9fb2","size":10,"sys_fee":"0","net_fee":"0","blockhash":"0xe93d17a52967f9e69314385482bf86f85260e811b46bf4d4b261a7f4135a623c","confirmations":20875,"blocktime":1541215200}}`, + serverResponse: `{"id":1,"jsonrpc":"2.0","result":{"nonce":4266257741,"type":"MinerTransaction","version":0,"attributes":[],"vin":[],"vout":[],"scripts":[],"txid":"0xcb6ddb5f99d6af4c94a6c396d5294472f2eebc91a2c933e0f527422296fa9fb2","size":10,"sys_fee":"0","net_fee":"0","blockhash":"0xe93d17a52967f9e69314385482bf86f85260e811b46bf4d4b261a7f4135a623c","confirmations":20875,"blocktime":1541215200}}`, result: func(c *Client) interface{} { - txHash, err := util.Uint256DecodeStringLE("cb6ddb5f99d6af4c94a6c396d5294472f2eebc91a2c933e0f527422296fa9fb2") - if err != nil { - panic(err) - } blockHash, err := util.Uint256DecodeStringLE("e93d17a52967f9e69314385482bf86f85260e811b46bf4d4b261a7f4135a623c") if err != nil { panic(err) } + tx := &transaction.Transaction{ + Type: transaction.MinerType, + Version: 0, + Data: &transaction.MinerTX{Nonce: 4266257741}, + Attributes: []transaction.Attribute{}, + Inputs: []transaction.Input{}, + Outputs: []transaction.Output{}, + Scripts: []transaction.Witness{}, + Trimmed: false, + } + // Update hashes for correct result comparison. + _ = tx.Hash() + return &result.TransactionOutputRaw{ - Transaction: &transaction.Transaction{ - Type: transaction.MinerType, - Version: 0, - Data: nil, - Attributes: []transaction.Attribute{}, - Inputs: []transaction.Input{}, - Outputs: []transaction.Output{}, - Scripts: []transaction.Witness{}, - Trimmed: false, + Transaction: tx, + TransactionMetadata: result.TransactionMetadata{ + SysFee: 0, + NetFee: 0, + Blockhash: blockHash, + Confirmations: 20875, + Timestamp: uint32(1541215200), }, - TxHash: txHash, - Size: 10, - SysFee: 0, - NetFee: 0, - Blockhash: blockHash, - Confirmations: 20875, - Timestamp: 1541215200, } }, }, diff --git a/pkg/rpc/response/result/block.go b/pkg/rpc/response/result/block.go index 080224563..e021cc64a 100644 --- a/pkg/rpc/response/result/block.go +++ b/pkg/rpc/response/result/block.go @@ -1,6 +1,8 @@ package result import ( + "encoding/json" + "errors" "fmt" "github.com/nspcc-dev/neo-go/pkg/core" @@ -15,19 +17,14 @@ type ( // Tx wrapper used for the representation of // transaction on the RPC Server. Tx struct { - TxID util.Uint256 `json:"txid"` - Size int `json:"size"` - Type transaction.TXType `json:"type"` - Version uint8 `json:"version"` - Attributes []transaction.Attribute `json:"attributes"` - VIn []transaction.Input `json:"vin"` - VOut []transaction.Output `json:"vout"` - Scripts []transaction.Witness `json:"scripts"` + *transaction.Transaction + Fees + } + // Fees is an auxilliary struct for proper Tx marshaling. + Fees struct { SysFee util.Fixed8 `json:"sys_fee"` NetFee util.Fixed8 `json:"net_fee"` - - Nonce uint32 `json:"nonce,omitempty"` } // Block wrapper used for the representation of @@ -77,32 +74,59 @@ func NewBlock(b *block.Block, chain core.Blockchainer) Block { } for i := range b.Transactions { - tx := Tx{ - TxID: b.Transactions[i].Hash(), - Size: io.GetVarSize(b.Transactions[i]), - Type: b.Transactions[i].Type, - Version: b.Transactions[i].Version, - Attributes: make([]transaction.Attribute, 0, len(b.Transactions[i].Attributes)), - VIn: make([]transaction.Input, 0, len(b.Transactions[i].Inputs)), - VOut: make([]transaction.Output, 0, len(b.Transactions[i].Outputs)), - Scripts: make([]transaction.Witness, 0, len(b.Transactions[i].Scripts)), - } - - copy(tx.Attributes, b.Transactions[i].Attributes) - copy(tx.VIn, b.Transactions[i].Inputs) - copy(tx.VOut, b.Transactions[i].Outputs) - copy(tx.Scripts, b.Transactions[i].Scripts) - - tx.SysFee = chain.SystemFee(b.Transactions[i]) - tx.NetFee = chain.NetworkFee(b.Transactions[i]) - - // set nonce only for MinerTransaction - if miner, ok := b.Transactions[i].Data.(*transaction.MinerTX); ok { - tx.Nonce = miner.Nonce - } - - res.Tx = append(res.Tx, tx) + res.Tx = append(res.Tx, Tx{ + Transaction: b.Transactions[i], + Fees: Fees{ + SysFee: chain.SystemFee(b.Transactions[i]), + NetFee: chain.NetworkFee(b.Transactions[i]), + }, + }) } return res } + +// MarshalJSON implements json.Marshaler interface. +func (t Tx) MarshalJSON() ([]byte, error) { + output, err := json.Marshal(&Fees{ + SysFee: t.SysFee, + NetFee: t.NetFee, + }) + if err != nil { + return nil, err + } + txBytes, err := json.Marshal(t.Transaction) + if err != nil { + return nil, err + } + + // We have to keep both transaction.Transaction and tx at the same level in json in order to match C# API, + // so there's no way to marshall Tx correctly with standard json.Marshaller tool. + if output[len(output)-1] != '}' || txBytes[0] != '{' { + return nil, errors.New("can't merge internal jsons") + } + output[len(output)-1] = ',' + output = append(output, txBytes[1:]...) + return output, nil +} + +// UnmarshalJSON implements json.Marshaler interface. +func (t *Tx) UnmarshalJSON(data []byte) error { + // As transaction.Transaction and tx are at the same level in json, do unmarshalling + // separately for both structs. + output := new(Fees) + err := json.Unmarshal(data, output) + if err != nil { + return err + } + t.SysFee = output.SysFee + t.NetFee = output.NetFee + + transaction := new(transaction.Transaction) + err = json.Unmarshal(data, transaction) + if err != nil { + return err + } + t.Transaction = transaction + return nil +} diff --git a/pkg/rpc/response/result/tx_raw_output.go b/pkg/rpc/response/result/tx_raw_output.go index 288311422..d039e2eb6 100644 --- a/pkg/rpc/response/result/tx_raw_output.go +++ b/pkg/rpc/response/result/tx_raw_output.go @@ -1,10 +1,12 @@ package result import ( + "encoding/json" + "errors" + "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/transaction" - "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/util" ) @@ -12,8 +14,11 @@ import ( // a Transaction. type TransactionOutputRaw struct { *transaction.Transaction - TxHash util.Uint256 `json:"txid"` - Size int `json:"size"` + TransactionMetadata +} + +// TransactionMetadata is an auxilliary struct for proper TransactionOutputRaw marshaling. +type TransactionMetadata struct { SysFee util.Fixed8 `json:"sys_fee"` NetFee util.Fixed8 `json:"net_fee"` Blockhash util.Uint256 `json:"blockhash,omitempty"` @@ -30,13 +35,64 @@ func NewTransactionOutputRaw(tx *transaction.Transaction, header *block.Header, o.Position = i } return TransactionOutputRaw{ - Transaction: tx, - TxHash: tx.Hash(), - Size: io.GetVarSize(tx), - SysFee: chain.SystemFee(tx), - NetFee: chain.NetworkFee(tx), - Blockhash: header.Hash(), - Confirmations: confirmations, - Timestamp: header.Timestamp, + Transaction: tx, + TransactionMetadata: TransactionMetadata{ + SysFee: chain.SystemFee(tx), + NetFee: chain.NetworkFee(tx), + Blockhash: header.Hash(), + Confirmations: confirmations, + Timestamp: header.Timestamp, + }, } } + +// MarshalJSON implements json.Marshaler interface. +func (t TransactionOutputRaw) MarshalJSON() ([]byte, error) { + output, err := json.Marshal(TransactionMetadata{ + SysFee: t.SysFee, + NetFee: t.NetFee, + Blockhash: t.Blockhash, + Confirmations: t.Confirmations, + Timestamp: t.Timestamp, + }) + if err != nil { + return nil, err + } + txBytes, err := json.Marshal(t.Transaction) + if err != nil { + return nil, err + } + + // We have to keep both transaction.Transaction and tranactionOutputRaw at the same level in json + // in order to match C# API, so there's no way to marshall Tx correctly with standard json.Marshaller tool. + if output[len(output)-1] != '}' || txBytes[0] != '{' { + return nil, errors.New("can't merge internal jsons") + } + output[len(output)-1] = ',' + output = append(output, txBytes[1:]...) + return output, nil +} + +// UnmarshalJSON implements json.Marshaler interface. +func (t *TransactionOutputRaw) UnmarshalJSON(data []byte) error { + // As transaction.Transaction and tranactionOutputRaw are at the same level in json, + // do unmarshalling separately for both structs. + output := new(TransactionMetadata) + err := json.Unmarshal(data, output) + if err != nil { + return err + } + t.SysFee = output.SysFee + t.NetFee = output.NetFee + t.Blockhash = output.Blockhash + t.Confirmations = output.Confirmations + t.Timestamp = output.Timestamp + + transaction := new(transaction.Transaction) + err = json.Unmarshal(data, transaction) + if err != nil { + return err + } + t.Transaction = transaction + return nil +} diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go index 42dccac03..62448388a 100644 --- a/pkg/rpc/server/server_test.go +++ b/pkg/rpc/server/server_test.go @@ -15,6 +15,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core" "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/internal/random" "github.com/nspcc-dev/neo-go/pkg/io" @@ -336,12 +337,12 @@ var rpcTestCases = map[string][]rpcTestCase{ assert.Equal(t, block.Hash(), res.Hash) for i := range res.Tx { tx := res.Tx[i] - require.Equal(t, transaction.MinerType, tx.Type) + require.Equal(t, transaction.MinerType, tx.Transaction.Type) miner, ok := block.Transactions[i].Data.(*transaction.MinerTX) require.True(t, ok) - require.Equal(t, miner.Nonce, tx.Nonce) - require.Equal(t, block.Transactions[i].Hash(), tx.TxID) + require.Equal(t, miner.Nonce, tx.Transaction.Data.(*transaction.MinerTX).Nonce) + require.Equal(t, block.Transactions[i].Hash(), tx.Transaction.Hash()) } }, }, @@ -935,6 +936,31 @@ func TestRPC(t *testing.T) { assert.Equal(t, "400000455b7b226c616e67223a227a682d434e222c226e616d65223a22e5b08fe89a81e882a1227d2c7b226c616e67223a22656e222c226e616d65223a22416e745368617265227d5d0000c16ff28623000000da1745e9b549bd0bfa1a569971c77eba30cd5a4b00000000", res) }) + t.Run("getrawtransaction 2 arguments, verbose", func(t *testing.T) { + block, _ := chain.GetBlock(chain.GetHeaderHash(0)) + TXHash := block.Transactions[1].Hash() + rpc := fmt.Sprintf(`{"jsonrpc": "2.0", "id": 1, "method": "getrawtransaction", "params": ["%s", 1]}"`, TXHash.StringLE()) + body := doRPCCall(rpc, handler, t) + txOut := checkErrGetResult(t, body, false) + actual := result.TransactionOutputRaw{} + err := json.Unmarshal(txOut, &actual) + require.NoErrorf(t, err, "could not parse response: %s", txOut) + admin, err := util.Uint160DecodeStringBE("da1745e9b549bd0bfa1a569971c77eba30cd5a4b") + require.NoError(t, err) + + assert.Equal(t, transaction.RegisterType, actual.Transaction.Type) + assert.Equal(t, &transaction.RegisterTX{ + AssetType: 0, + Name: `[{"lang":"zh-CN","name":"小蚁股"},{"lang":"en","name":"AntShare"}]`, + Amount: util.Fixed8FromInt64(100000000), + Precision: 0, + Owner: keys.PublicKey{}, + Admin: admin, + }, actual.Transaction.Data.(*transaction.RegisterTX)) + assert.Equal(t, 210, actual.Confirmations) + assert.Equal(t, TXHash, actual.Transaction.Hash()) + }) + t.Run("gettxout", func(t *testing.T) { block, _ := chain.GetBlock(chain.GetHeaderHash(0)) tx := block.Transactions[3]