Merge pull request #789 from nspcc-dev/fix/tx_specific_data_marshalling
rpc: fix marshalling of type-specific tx data
This commit is contained in:
commit
e41853d0a4
10 changed files with 653 additions and 114 deletions
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 <Type>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
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
@ -119,7 +120,7 @@ func (c *Client) getBlockVerbose(params request.RawParams) (*result.Block, error
|
|||
resp = &result.Block{}
|
||||
err error
|
||||
)
|
||||
if err = c.performRequest("getblock", params, &resp); err != nil {
|
||||
if err = c.performRequest("getblock", params, resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
|
@ -168,7 +169,7 @@ func (c *Client) GetBlockHeaderVerbose(hash util.Uint256) (*result.Header, error
|
|||
params = request.NewRawParams(hash.StringLE(), 1)
|
||||
resp = &result.Header{}
|
||||
)
|
||||
if err := c.performRequest("getblockheader", params, &resp); err != nil {
|
||||
if err := c.performRequest("getblockheader", params, resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
|
@ -289,13 +290,14 @@ 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)
|
||||
resp = &result.TransactionOutputRaw{}
|
||||
err error
|
||||
)
|
||||
if err = c.performRequest("getrawtransaction", params, &resp); err != nil {
|
||||
if err = c.performRequest("getrawtransaction", params, resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
|
@ -336,7 +338,7 @@ func (c *Client) GetTxOut(hash util.Uint256, num int) (*result.TransactionOutput
|
|||
params = request.NewRawParams(hash.StringLE(), num)
|
||||
resp = &result.TransactionOutput{}
|
||||
)
|
||||
if err := c.performRequest("gettxout", params, &resp); err != nil {
|
||||
if err := c.performRequest("gettxout", params, resp); err != nil {
|
||||
return resp, err
|
||||
}
|
||||
return resp, nil
|
||||
|
@ -537,7 +539,7 @@ func (c *Client) ValidateAddress(address string) error {
|
|||
resp = &result.ValidateAddress{}
|
||||
)
|
||||
|
||||
if err := c.performRequest("validateaddress", params, &resp); err != nil {
|
||||
if err := c.performRequest("validateaddress", params, resp); err != nil {
|
||||
return err
|
||||
}
|
||||
if !resp.IsValid {
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
|
|
Loading…
Reference in a new issue