diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 469a69534..2d415ffbc 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -548,8 +548,8 @@ func (bc *Blockchain) HeaderHeight() uint32 { return uint32(bc.headerListLen() - 1) } +// GetAssetState returns asset state from its assetID func (bc *Blockchain) GetAssetState(assetID util.Uint256) *AssetState { - var as *AssetState bc.Store.Seek(storage.STAsset.Bytes(), func(k, v []byte) { var a AssetState @@ -562,6 +562,19 @@ func (bc *Blockchain) GetAssetState(assetID util.Uint256) *AssetState { 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 { buf := make([]byte, 4) binary.LittleEndian.PutUint32(buf, index) diff --git a/pkg/core/blockchainer.go b/pkg/core/blockchainer.go index d965547d1..a0cde74e2 100644 --- a/pkg/core/blockchainer.go +++ b/pkg/core/blockchainer.go @@ -18,4 +18,5 @@ type Blockchainer interface { HasBlock(util.Uint256) bool HasTransaction(util.Uint256) bool GetAssetState(util.Uint256) *AssetState + GetAccountState(util.Uint160) *AccountState } diff --git a/pkg/network/helper_test.go b/pkg/network/helper_test.go index 39de8211b..6e626ffbb 100644 --- a/pkg/network/helper_test.go +++ b/pkg/network/helper_test.go @@ -32,6 +32,9 @@ func (chain testChain) GetHeaderHash(int) util.Uint256 { func (chain testChain) GetAssetState(util.Uint256) *core.AssetState { return nil } +func (chain testChain) GetAccountState(util.Uint160) *core.AccountState { + return nil +} func (chain testChain) CurrentHeaderHash() util.Uint256 { return util.Uint256{} } diff --git a/pkg/rpc/server.go b/pkg/rpc/server.go index daa14d349..677993be8 100644 --- a/pkg/rpc/server.go +++ b/pkg/rpc/server.go @@ -2,16 +2,17 @@ package rpc import ( "context" - "errors" "fmt" "net/http" "strconv" "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/rpc/result" "github.com/CityOfZion/neo-go/pkg/rpc/wrappers" "github.com/CityOfZion/neo-go/pkg/util" + "github.com/pkg/errors" log "github.com/sirupsen/logrus" ) @@ -26,7 +27,7 @@ type ( var ( 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 - 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" case "getassetstate": var err error - - param, exists := reqParams.ValueAt(0) + param, exists := reqParams.ValueAtAndType(0, "string") if !exists { - err = errors.New("Param at index at 0 doesn't exist") - resultsErr = NewInvalidParamsError(err.Error(), err) - break - } - - if param.Type != "string" { - err = errors.New("Param need to be a string") + err = errors.New("expected param at index 0 to be a valid string assetID parameter") resultsErr = NewInvalidParamsError(err.Error(), err) break } 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) - if as != nil { results = wrappers.NewAssetState(as) } else { 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: resultsErr = NewMethodNotFoundError(fmt.Sprintf("Method '%s' not supported", req.Method), nil) } diff --git a/pkg/rpc/server_test.go b/pkg/rpc/server_test.go index dae97795c..520e97ea1 100644 --- a/pkg/rpc/server_test.go +++ b/pkg/rpc/server_test.go @@ -16,12 +16,9 @@ import ( ) func TestHandler(t *testing.T) { - // setup rpcServer server net := config.ModeUnitTestNet - configPath := "../../config" - cfg, err := config.Load(configPath, net) if err != nil { t.Fatal("could not create levelDB chain", err) @@ -34,7 +31,6 @@ func TestHandler(t *testing.T) { serverConfig := network.NewServerConfig(cfg) server := network.NewServer(serverConfig, chain) - rpcServer := NewServer(chain, cfg.ApplicationConfiguration.RPCPort, server) // setup handler @@ -55,11 +51,11 @@ func TestHandler(t *testing.T) { {`{"jsonrpc": "2.0", "id": 1, "method": "getassetstate", "params": ["62c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7"] }`, "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] }`, "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] }`, "getblockhash_1", @@ -92,10 +88,24 @@ func TestHandler(t *testing.T) { {`{"jsonrpc": "2.0", "id": 1, "method": "getpeers", "params": [] }`, "getpeers", `{"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 { - t.Run(fmt.Sprintf("method: %s, rpc call: %s", tc.method, tc.rpcCall), func(t *testing.T) { jsonStr := []byte(tc.rpcCall) @@ -105,9 +115,7 @@ func TestHandler(t *testing.T) { w := httptest.NewRecorder() handler(w, req) - resp := w.Result() - body, err := ioutil.ReadAll(resp.Body) if err != nil { t.Errorf("could not read response from the request: %s", tc.rpcCall) diff --git a/pkg/rpc/wrappers/account_state.go b/pkg/rpc/wrappers/account_state.go new file mode 100644 index 000000000..98f7ebda3 --- /dev/null +++ b/pkg/rpc/wrappers/account_state.go @@ -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, + } +}