From 5841d3931ed41e4079ba3ed22cbd507778ec6126 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Fri, 15 Nov 2019 22:04:10 +0300 Subject: [PATCH] rpc: implement getunspents method, fix #473 --- pkg/core/account_state.go | 6 ++-- pkg/rpc/neoScanBalanceGetter.go | 3 +- pkg/rpc/neoScanTypes.go | 6 ---- pkg/rpc/prometheus.go | 9 ++++++ pkg/rpc/server.go | 38 ++++++++++++++++------ pkg/rpc/server_helper_test.go | 20 ++++++++++++ pkg/rpc/server_test.go | 21 +++++++++++++ pkg/rpc/wrappers/unspents.go | 56 +++++++++++++++++++++++++++++++++ 8 files changed, 139 insertions(+), 20 deletions(-) create mode 100644 pkg/rpc/wrappers/unspents.go diff --git a/pkg/core/account_state.go b/pkg/core/account_state.go index 78646c4fc..7c14151d7 100644 --- a/pkg/core/account_state.go +++ b/pkg/core/account_state.go @@ -72,9 +72,9 @@ func (a Accounts) commit(store storage.Store) error { // UnspentBalance contains input/output transactons that sum up into the // account balance for the given asset. type UnspentBalance struct { - Tx util.Uint256 - Index uint16 - Value util.Fixed8 + Tx util.Uint256 `json:"txid"` + Index uint16 `json:"n"` + Value util.Fixed8 `json:"value"` } // AccountState represents the state of a NEO account. diff --git a/pkg/rpc/neoScanBalanceGetter.go b/pkg/rpc/neoScanBalanceGetter.go index 5e7b6150d..c51fba304 100644 --- a/pkg/rpc/neoScanBalanceGetter.go +++ b/pkg/rpc/neoScanBalanceGetter.go @@ -7,6 +7,7 @@ import ( "sort" "github.com/CityOfZion/neo-go/pkg/core/transaction" + "github.com/CityOfZion/neo-go/pkg/rpc/wrappers" "github.com/CityOfZion/neo-go/pkg/util" errs "github.com/pkg/errors" ) @@ -59,7 +60,7 @@ func (s NeoScanServer) CalculateInputs(address string, assetIDUint util.Uint256, selected = util.Fixed8(0) us []*Unspent assetUnspent Unspent - assetID = GlobalAssets[assetIDUint.ReverseString()] + assetID = wrappers.GlobalAssets[assetIDUint.ReverseString()] ) if us, err = s.GetBalance(address); err != nil { return nil, util.Fixed8(0), errs.Wrapf(err, "Cannot get balance for address %v", address) diff --git a/pkg/rpc/neoScanTypes.go b/pkg/rpc/neoScanTypes.go index 58a8b9ce2..9ef6a81b1 100644 --- a/pkg/rpc/neoScanTypes.go +++ b/pkg/rpc/neoScanTypes.go @@ -39,12 +39,6 @@ type ( } ) -// GlobalAssets stores a map of asset IDs to user-friendly strings ("NEO"/"GAS"). -var GlobalAssets = map[string]string{ - "c56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930faebe74a6daff7c9b": "NEO", - "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7": "GAS", -} - // 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 } diff --git a/pkg/rpc/prometheus.go b/pkg/rpc/prometheus.go index 7a58b8f19..51c865003 100644 --- a/pkg/rpc/prometheus.go +++ b/pkg/rpc/prometheus.go @@ -92,6 +92,14 @@ var ( }, ) + getunspentsCalled = prometheus.NewCounter( + prometheus.CounterOpts{ + Help: "Number of calls to getunspents rpc endpoint", + Name: "getunspents_called", + Namespace: "neogo", + }, + ) + sendrawtransactionCalled = prometheus.NewCounter( prometheus.CounterOpts{ Help: "Number of calls to sendrawtransaction rpc endpoint", @@ -113,6 +121,7 @@ func init() { validateaddressCalled, getassetstateCalled, getaccountstateCalled, + getunspentsCalled, getrawtransactionCalled, sendrawtransactionCalled, ) diff --git a/pkg/rpc/server.go b/pkg/rpc/server.go index 474e9103c..8b521ed36 100644 --- a/pkg/rpc/server.go +++ b/pkg/rpc/server.go @@ -233,20 +233,16 @@ Methods: case "getaccountstate": getaccountstateCalled.Inc() - param, err := reqParams.ValueWithType(0, "string") - if err != nil { - resultsErr = err - } else if scriptHash, err := crypto.Uint160DecodeAddress(param.StringVal); err != nil { - resultsErr = errInvalidParams - } else if as := s.chain.GetAccountState(scriptHash); as != nil { - results = wrappers.NewAccountState(as) - } else { - results = "Invalid public account address" - } + results, resultsErr = s.getAccountState(reqParams, false) + case "getrawtransaction": getrawtransactionCalled.Inc() results, resultsErr = s.getrawtransaction(reqParams) + case "getunspents": + getunspentsCalled.Inc() + results, resultsErr = s.getAccountState(reqParams, true) + case "invokescript": results, resultsErr = s.invokescript(reqParams) @@ -304,6 +300,28 @@ func (s *Server) getrawtransaction(reqParams Params) (interface{}, error) { return results, resultsErr } +// getAccountState returns account state either in short or full (unspents included) form. +func (s *Server) getAccountState(reqParams Params, unspents bool) (interface{}, error) { + var resultsErr error + var results interface{} + + param, err := reqParams.ValueWithType(0, "string") + if err != nil { + resultsErr = err + } else if scriptHash, err := crypto.Uint160DecodeAddress(param.StringVal); err != nil { + resultsErr = errInvalidParams + } else if as := s.chain.GetAccountState(scriptHash); as != nil { + if unspents { + results = wrappers.NewUnspents(as, s.chain, param.StringVal) + } else { + results = wrappers.NewAccountState(as) + } + } else { + results = "Invalid public account address" + } + return results, resultsErr +} + // invokescript implements the `invokescript` RPC call. func (s *Server) invokescript(reqParams Params) (interface{}, error) { hexScript, err := reqParams.ValueWithType(0, "string") diff --git a/pkg/rpc/server_helper_test.go b/pkg/rpc/server_helper_test.go index a2de55187..c57584bf5 100644 --- a/pkg/rpc/server_helper_test.go +++ b/pkg/rpc/server_helper_test.go @@ -138,6 +138,26 @@ type GetAccountStateResponse struct { ID int `json:"id"` } +// GetUnspents struct for testing. +type GetUnspents struct { + Jsonrpc string `json:"jsonrpc"` + Result struct { + Balance []struct { + Unspents []struct { + TxID string `json:"txid"` + Index int `json:"n"` + Value string `json:"value"` + } `json:"unspent"` + AssetHash string `json:"asset_hash"` + Asset string `json:"asset"` + AssetSymbol string `json:"asset_symbol"` + Amount string `json:"amount"` + } `json:"balance"` + Address string `json:"address"` + } `json:"result"` + ID int `json:"id"` +} + func initServerWithInMemoryChain(t *testing.T) (*core.Blockchain, http.HandlerFunc) { var nBlocks uint32 diff --git a/pkg/rpc/server_test.go b/pkg/rpc/server_test.go index 05d7eacf5..e9cb66cfe 100644 --- a/pkg/rpc/server_test.go +++ b/pkg/rpc/server_test.go @@ -146,6 +146,17 @@ func TestRPC(t *testing.T) { assert.Equal(t, false, res.Result.Frozen) }) + t.Run("getunspents_positive", func(t *testing.T) { + rpc := `{"jsonrpc": "2.0", "id": 1, "method": "getunspents", "params": ["AZ81H31DMWzbSnFDLFkzh9vHwaDLayV7fU"]}` + body := doRPCCall(rpc, handler, t) + checkErrResponse(t, body, false) + var res GetUnspents + err := json.Unmarshal(bytes.TrimSpace(body), &res) + assert.NoErrorf(t, err, "could not parse response: %s", body) + assert.Equal(t, 1, len(res.Result.Balance)) + assert.Equal(t, 1, len(res.Result.Balance[0].Unspents)) + }) + t.Run("getaccountstate_negative", func(t *testing.T) { rpc := `{"jsonrpc": "2.0", "id": 1, "method": "getaccountstate", "params": ["AK2nJJpJr6o664CWJKi1QRXjqeic2zRp8y"]}` body := doRPCCall(rpc, handler, t) @@ -156,6 +167,16 @@ func TestRPC(t *testing.T) { assert.Equal(t, "Invalid public account address", res.Result) }) + t.Run("getunspents_negative", func(t *testing.T) { + rpc := `{"jsonrpc": "2.0", "id": 1, "method": "getunspents", "params": ["AK2nJJpJr6o664CWJKi1QRXjqeic2zRp8y"]}` + body := doRPCCall(rpc, handler, t) + checkErrResponse(t, body, false) + var res StringResultResponse + err := json.Unmarshal(bytes.TrimSpace(body), &res) + assert.NoErrorf(t, err, "could not parse response: %s", body) + assert.Equal(t, "Invalid public account address", res.Result) + }) + t.Run("getrawtransaction", func(t *testing.T) { block, _ := chain.GetBlock(chain.GetHeaderHash(0)) TXHash := block.Transactions[1].Hash() diff --git a/pkg/rpc/wrappers/unspents.go b/pkg/rpc/wrappers/unspents.go new file mode 100644 index 000000000..b20307f14 --- /dev/null +++ b/pkg/rpc/wrappers/unspents.go @@ -0,0 +1,56 @@ +package wrappers + +import ( + "github.com/CityOfZion/neo-go/pkg/core" + "github.com/CityOfZion/neo-go/pkg/util" +) + +// UnspentBalanceInfo wrapper is used to represent single unspent asset entry +// in `getunspents` output. +type UnspentBalanceInfo struct { + Unspents []core.UnspentBalance `json:"unspent"` + AssetHash util.Uint256 `json:"asset_hash"` + Asset string `json:"asset"` + AssetSymbol string `json:"asset_symbol"` + Amount util.Fixed8 `json:"amount"` +} + +// Unspents wrapper is used to represent getunspents return result. +type Unspents struct { + Balance []UnspentBalanceInfo `json:"balance"` + Address string `json:"address"` +} + +// GlobalAssets stores a map of asset IDs to user-friendly strings ("NEO"/"GAS"). +var GlobalAssets = map[string]string{ + "c56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930faebe74a6daff7c9b": "NEO", + "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7": "GAS", +} + +// NewUnspents creates a new AccountState wrapper using given Blockchainer. +func NewUnspents(a *core.AccountState, chain core.Blockchainer, addr string) Unspents { + res := Unspents{ + Address: addr, + Balance: make([]UnspentBalanceInfo, 0, len(a.Balances)), + } + balanceValues := a.GetBalanceValues() + for k, v := range a.Balances { + name, ok := GlobalAssets[k.ReverseString()] + if !ok { + as := chain.GetAssetState(k) + if as != nil { + name = as.Name + } + } + + res.Balance = append(res.Balance, UnspentBalanceInfo{ + Unspents: v, + AssetHash: k, + Asset: name, + AssetSymbol: name, + Amount: balanceValues[k], + }) + } + + return res +}