Implemented rcp method GetAccountState (#124)

* Implemented rcp method GetAccountState

* code clean up

* Removed empty line

* Used consistently github.com/pkg/errors package. Amended error message

* Get rid of fmt.Sprintf and use either errors.Errorf or errors.Wrapf

* cosmetic changes
This commit is contained in:
dauTT 2019-02-08 09:04:38 +01:00 committed by fabwa
parent a5e2df6942
commit 7e43717657
6 changed files with 105 additions and 23 deletions

View file

@ -548,8 +548,8 @@ func (bc *Blockchain) HeaderHeight() uint32 {
return uint32(bc.headerListLen() - 1) return uint32(bc.headerListLen() - 1)
} }
// GetAssetState returns asset state from its assetID
func (bc *Blockchain) GetAssetState(assetID util.Uint256) *AssetState { func (bc *Blockchain) GetAssetState(assetID util.Uint256) *AssetState {
var as *AssetState var as *AssetState
bc.Store.Seek(storage.STAsset.Bytes(), func(k, v []byte) { bc.Store.Seek(storage.STAsset.Bytes(), func(k, v []byte) {
var a AssetState var a AssetState
@ -562,6 +562,19 @@ func (bc *Blockchain) GetAssetState(assetID util.Uint256) *AssetState {
return as return as
} }
// GetAccountState returns the account state from its script hash
func (bc *Blockchain) GetAccountState(scriptHash util.Uint160) *AccountState {
var as *AccountState
bc.Store.Seek(storage.STAccount.Bytes(), func(k, v []byte) {
var a AccountState
if err := a.DecodeBinary(bytes.NewReader(v)); err == nil && a.ScriptHash == scriptHash {
as = &a
}
})
return as
}
func hashAndIndexToBytes(h util.Uint256, index uint32) []byte { func hashAndIndexToBytes(h util.Uint256, index uint32) []byte {
buf := make([]byte, 4) buf := make([]byte, 4)
binary.LittleEndian.PutUint32(buf, index) binary.LittleEndian.PutUint32(buf, index)

View file

@ -18,4 +18,5 @@ type Blockchainer interface {
HasBlock(util.Uint256) bool HasBlock(util.Uint256) bool
HasTransaction(util.Uint256) bool HasTransaction(util.Uint256) bool
GetAssetState(util.Uint256) *AssetState GetAssetState(util.Uint256) *AssetState
GetAccountState(util.Uint160) *AccountState
} }

View file

@ -32,6 +32,9 @@ func (chain testChain) GetHeaderHash(int) util.Uint256 {
func (chain testChain) GetAssetState(util.Uint256) *core.AssetState { func (chain testChain) GetAssetState(util.Uint256) *core.AssetState {
return nil return nil
} }
func (chain testChain) GetAccountState(util.Uint160) *core.AccountState {
return nil
}
func (chain testChain) CurrentHeaderHash() util.Uint256 { func (chain testChain) CurrentHeaderHash() util.Uint256 {
return util.Uint256{} return util.Uint256{}
} }

View file

@ -2,16 +2,17 @@ package rpc
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"net/http" "net/http"
"strconv" "strconv"
"github.com/CityOfZion/neo-go/pkg/core" "github.com/CityOfZion/neo-go/pkg/core"
"github.com/CityOfZion/neo-go/pkg/crypto"
"github.com/CityOfZion/neo-go/pkg/network" "github.com/CityOfZion/neo-go/pkg/network"
"github.com/CityOfZion/neo-go/pkg/rpc/result" "github.com/CityOfZion/neo-go/pkg/rpc/result"
"github.com/CityOfZion/neo-go/pkg/rpc/wrappers" "github.com/CityOfZion/neo-go/pkg/rpc/wrappers"
"github.com/CityOfZion/neo-go/pkg/util" "github.com/CityOfZion/neo-go/pkg/util"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -26,7 +27,7 @@ type (
var ( var (
invalidBlockHeightError = func(index int, height int) error { invalidBlockHeightError = func(index int, height int) error {
return fmt.Errorf("Param at index %d should be greater than or equal to 0 and less then or equal to current block height, got: %d", index, height) return errors.Errorf("Param at index %d should be greater than or equal to 0 and less then or equal to current block height, got: %d", index, height)
} }
) )
@ -180,35 +181,48 @@ Methods:
results = peers results = peers
case "validateaddress", "getblocksysfee", "getcontractstate", "getrawmempool", "getrawtransaction", "getstorage", "submitblock", "gettxout", "invoke", "invokefunction", "invokescript", "sendrawtransaction", "getaccountstate": case "validateaddress", "getblocksysfee", "getcontractstate", "getrawmempool", "getrawtransaction", "getstorage", "submitblock", "gettxout", "invoke", "invokefunction", "invokescript", "sendrawtransaction":
results = "TODO" results = "TODO"
case "getassetstate": case "getassetstate":
var err error var err error
param, exists := reqParams.ValueAtAndType(0, "string")
param, exists := reqParams.ValueAt(0)
if !exists { if !exists {
err = errors.New("Param at index at 0 doesn't exist") err = errors.New("expected param at index 0 to be a valid string assetID parameter")
resultsErr = NewInvalidParamsError(err.Error(), err)
break
}
if param.Type != "string" {
err = errors.New("Param need to be a string")
resultsErr = NewInvalidParamsError(err.Error(), err) resultsErr = NewInvalidParamsError(err.Error(), err)
break break
} }
paramAssetID, err := util.Uint256DecodeString(param.StringVal) paramAssetID, err := util.Uint256DecodeString(param.StringVal)
if err != nil {
err = errors.Wrapf(err, "unable to decode %s to Uint256", param.StringVal)
resultsErr = NewInvalidParamsError(err.Error(), err)
break
}
as := s.chain.GetAssetState(paramAssetID) as := s.chain.GetAssetState(paramAssetID)
if as != nil { if as != nil {
results = wrappers.NewAssetState(as) results = wrappers.NewAssetState(as)
} else { } else {
results = "Invalid assetid" results = "Invalid assetid"
} }
case "getaccountstate":
var err error
param, exists := reqParams.ValueAtAndType(0, "string")
if !exists {
err = errors.New("expected param at index 0 to be a valid string account address parameter")
resultsErr = NewInvalidParamsError(err.Error(), err)
} else if scriptHash, err := crypto.Uint160DecodeAddress(param.StringVal); err != nil {
err = errors.Wrapf(err, "unable to decode %s to Uint160", param.StringVal)
resultsErr = NewInvalidParamsError(err.Error(), err)
} else if as := s.chain.GetAccountState(scriptHash); as != nil {
results = wrappers.NewAccountState(as)
} else {
results = "Invalid public account address"
}
default: default:
resultsErr = NewMethodNotFoundError(fmt.Sprintf("Method '%s' not supported", req.Method), nil) resultsErr = NewMethodNotFoundError(fmt.Sprintf("Method '%s' not supported", req.Method), nil)
} }

View file

@ -16,12 +16,9 @@ import (
) )
func TestHandler(t *testing.T) { func TestHandler(t *testing.T) {
// setup rpcServer server // setup rpcServer server
net := config.ModeUnitTestNet net := config.ModeUnitTestNet
configPath := "../../config" configPath := "../../config"
cfg, err := config.Load(configPath, net) cfg, err := config.Load(configPath, net)
if err != nil { if err != nil {
t.Fatal("could not create levelDB chain", err) t.Fatal("could not create levelDB chain", err)
@ -34,7 +31,6 @@ func TestHandler(t *testing.T) {
serverConfig := network.NewServerConfig(cfg) serverConfig := network.NewServerConfig(cfg)
server := network.NewServer(serverConfig, chain) server := network.NewServer(serverConfig, chain)
rpcServer := NewServer(chain, cfg.ApplicationConfiguration.RPCPort, server) rpcServer := NewServer(chain, cfg.ApplicationConfiguration.RPCPort, server)
// setup handler // setup handler
@ -55,11 +51,11 @@ func TestHandler(t *testing.T) {
{`{"jsonrpc": "2.0", "id": 1, "method": "getassetstate", "params": ["62c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7"] }`, {`{"jsonrpc": "2.0", "id": 1, "method": "getassetstate", "params": ["62c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7"] }`,
"getassetstate_3", "getassetstate_3",
`{"jsonrpc":"2.0","result":"Invalid assetid","id":1}`}, `{"jsonrpc":"2.0","error":{"code":-32602,"message":"Invalid Params","data":"unable to decode 62c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7 to Uint256: expected string size of 64 got 63"},"id":1}`},
{`{"jsonrpc": "2.0", "id": 1, "method": "getassetstate", "params": [123] }`, {`{"jsonrpc": "2.0", "id": 1, "method": "getassetstate", "params": [123] }`,
"getassetstate_4", "getassetstate_4",
`{"jsonrpc":"2.0","error":{"code":-32602,"message":"Invalid Params","data":"Param need to be a string"},"id":1}`}, `{"jsonrpc":"2.0","error":{"code":-32602,"message":"Invalid Params","data":"expected param at index 0 to be a valid string assetID parameter"},"id":1}`},
{`{"jsonrpc": "2.0", "id": 1, "method": "getblockhash", "params": [10] }`, {`{"jsonrpc": "2.0", "id": 1, "method": "getblockhash", "params": [10] }`,
"getblockhash_1", "getblockhash_1",
@ -92,10 +88,24 @@ func TestHandler(t *testing.T) {
{`{"jsonrpc": "2.0", "id": 1, "method": "getpeers", "params": [] }`, {`{"jsonrpc": "2.0", "id": 1, "method": "getpeers", "params": [] }`,
"getpeers", "getpeers",
`{"jsonrpc":"2.0","result":{"unconnected":[],"connected":[],"bad":[]},"id":1}`}, `{"jsonrpc":"2.0","result":{"unconnected":[],"connected":[],"bad":[]},"id":1}`},
{`{ "jsonrpc": "2.0", "id": 1, "method": "getaccountstate", "params": ["AK2nJJpJr6o664CWJKi1QRXjqeic2zRp8y"] }`,
"getaccountstate_1",
`{"jsonrpc":"2.0","result":{"version":0,"address":"AK2nJJpJr6o664CWJKi1QRXjqeic2zRp8y","script_hash":"0xe9eed8dc39332032dc22e5d6e86332c50327ba23","frozen":false,"votes":[],"balances":{"602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7":"72099.99960000","c56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930faebe74a6daff7c9b":"99989900"}},"id":1}`,
},
{`{ "jsonrpc": "2.0", "id": 1, "method": "getaccountstate", "params": ["AK2nJJpJr6o664CWJKi1QRXjqeic2zR"] }`,
"getaccountstate_2",
`{"jsonrpc":"2.0","error":{"code":-32602,"message":"Invalid Params","data":"unable to decode AK2nJJpJr6o664CWJKi1QRXjqeic2zR to Uint160: invalid base-58 check string: invalid checksum."},"id":1}`,
},
{`{ "jsonrpc": "2.0", "id": 1, "method": "getaccountstate", "params": [123] }`,
"getaccountstate_3",
`{"jsonrpc":"2.0","error":{"code":-32602,"message":"Invalid Params","data":"expected param at index 0 to be a valid string account address parameter"},"id":1}`,
},
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(fmt.Sprintf("method: %s, rpc call: %s", tc.method, tc.rpcCall), func(t *testing.T) { t.Run(fmt.Sprintf("method: %s, rpc call: %s", tc.method, tc.rpcCall), func(t *testing.T) {
jsonStr := []byte(tc.rpcCall) jsonStr := []byte(tc.rpcCall)
@ -105,9 +115,7 @@ func TestHandler(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
handler(w, req) handler(w, req)
resp := w.Result() resp := w.Result()
body, err := ioutil.ReadAll(resp.Body) body, err := ioutil.ReadAll(resp.Body)
if err != nil { if err != nil {
t.Errorf("could not read response from the request: %s", tc.rpcCall) t.Errorf("could not read response from the request: %s", tc.rpcCall)

View file

@ -0,0 +1,43 @@
package wrappers
import (
"github.com/CityOfZion/neo-go/pkg/core"
"github.com/CityOfZion/neo-go/pkg/crypto"
"github.com/CityOfZion/neo-go/pkg/util"
)
// AccountState wrapper used for the representation of
// core.AccountState on the RPC Server.
type AccountState struct {
Version uint8 `json:"version"`
Address string `json:"address"`
ScriptHash util.Uint160 `json:"script_hash"`
IsFrozen bool `json:"frozen"`
Votes []*crypto.PublicKey `json:"votes"`
Balances map[string]util.Fixed8 `json:"balances"`
}
// NewAccountState creates a new AccountState wrapper.
func NewAccountState(a *core.AccountState) AccountState {
balances := make(map[string]util.Fixed8)
address := crypto.AddressFromUint160(a.ScriptHash)
for k, v := range a.Balances {
balances[k.String()] = v
}
// reverse scriptHash to be consistent with other client
scriptHash, err := util.Uint160DecodeBytes(a.ScriptHash.BytesReverse())
if err != nil {
scriptHash = a.ScriptHash
}
return AccountState{
Version: a.Version,
ScriptHash: scriptHash,
IsFrozen: a.IsFrozen,
Votes: a.Votes,
Balances: balances,
Address: address,
}
}