diff --git a/docs/rpc.md b/docs/rpc.md index ffbaa3c4d..117e957fa 100644 --- a/docs/rpc.md +++ b/docs/rpc.md @@ -51,7 +51,7 @@ which would yield the response: | `getrawmempool` | No (#175) | | `getrawtransaction` | Yes | | `getstorage` | No (#343) | -| `gettxout` | No (#345) | +| `gettxout` | Yes | | `getunspents` | Yes | | `getversion` | Yes | | `invoke` | Yes | diff --git a/pkg/rpc/prometheus.go b/pkg/rpc/prometheus.go index 51c865003..686d09853 100644 --- a/pkg/rpc/prometheus.go +++ b/pkg/rpc/prometheus.go @@ -84,6 +84,14 @@ var ( }, ) + gettxoutCalled = prometheus.NewCounter( + prometheus.CounterOpts{ + Help: "Number of calls to gettxout rpc endpoint", + Name: "gettxout_called", + Namespace: "neogo", + }, + ) + getrawtransactionCalled = prometheus.NewCounter( prometheus.CounterOpts{ Help: "Number of calls to getrawtransaction rpc endpoint", @@ -122,6 +130,7 @@ func init() { getassetstateCalled, getaccountstateCalled, getunspentsCalled, + gettxoutCalled, getrawtransactionCalled, sendrawtransactionCalled, ) diff --git a/pkg/rpc/server.go b/pkg/rpc/server.go index 641aa90eb..3805da144 100644 --- a/pkg/rpc/server.go +++ b/pkg/rpc/server.go @@ -245,6 +245,10 @@ Methods: getrawtransactionCalled.Inc() results, resultsErr = s.getrawtransaction(reqParams) + case "gettxout": + gettxoutCalled.Inc() + results, resultsErr = s.getTxOut(reqParams) + case "getunspents": getunspentsCalled.Inc() results, resultsErr = s.getAccountState(reqParams, true) @@ -311,6 +315,40 @@ func (s *Server) getrawtransaction(reqParams Params) (interface{}, error) { return results, resultsErr } +func (s *Server) getTxOut(ps Params) (interface{}, error) { + p, ok := ps.Value(0) + if !ok { + return nil, errInvalidParams + } + + h, err := p.GetUint256() + if err != nil { + return nil, errInvalidParams + } + + p, ok = ps.ValueWithType(1, numberT) + if !ok { + return nil, errInvalidParams + } + + num, err := p.GetInt() + if err != nil || num < 0 { + return nil, errInvalidParams + } + + tx, _, err := s.chain.GetTransaction(h) + if err != nil { + return nil, NewInvalidParamsError(err.Error(), err) + } + + if num >= len(tx.Outputs) { + return nil, NewInvalidParamsError("invalid index", errors.New("too big index")) + } + + out := tx.Outputs[num] + return wrappers.NewTxOutput(&out), nil +} + // 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 diff --git a/pkg/rpc/server_test.go b/pkg/rpc/server_test.go index dcf9bdc7b..0c03ffb4d 100644 --- a/pkg/rpc/server_test.go +++ b/pkg/rpc/server_test.go @@ -112,6 +112,38 @@ var rpcTestCases = map[string][]rpcTestCase{ fail: true, }, }, + "gettxout": { + { + name: "no params", + params: `[]`, + fail: true, + }, + { + name: "invalid hash", + params: `["notahex"]`, + fail: true, + }, + { + name: "missing hash", + params: `["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 0]`, + fail: true, + }, + { + name: "invalid index", + params: `["7aadf91ca8ac1e2c323c025a7e492bee2dd90c783b86ebfc3b18db66b530a76d", "string"]`, + fail: true, + }, + { + name: "negative index", + params: `["7aadf91ca8ac1e2c323c025a7e492bee2dd90c783b86ebfc3b18db66b530a76d", -1]`, + fail: true, + }, + { + name: "too big index", + params: `["7aadf91ca8ac1e2c323c025a7e492bee2dd90c783b86ebfc3b18db66b530a76d", 100]`, + fail: true, + }, + }, "getblock": { { name: "positive", @@ -489,6 +521,23 @@ func TestRPC(t *testing.T) { require.NoErrorf(t, err, "could not parse response: %s", body) assert.Equal(t, "400000455b7b226c616e67223a227a682d434e222c226e616d65223a22e5b08fe89a81e882a1227d2c7b226c616e67223a22656e222c226e616d65223a22416e745368617265227d5d0000c16ff28623000000da1745e9b549bd0bfa1a569971c77eba30cd5a4b00000000", res.Result) }) + + t.Run("gettxout", func(t *testing.T) { + block, _ := chain.GetBlock(chain.GetHeaderHash(0)) + tx := block.Transactions[3] + rpc := fmt.Sprintf(`{"jsonrpc": "2.0", "id": 1, "method": "gettxout", "params": [%s, %d]}"`, + `"`+tx.Hash().StringLE()+`"`, 0) + body := doRPCCall(rpc, handler, t) + checkErrResponse(t, body, false) + + var result GetTxOutResponse + err := json.Unmarshal(body, &result) + require.NoErrorf(t, err, "could not parse response: %s", body) + assert.Equal(t, 0, result.Result.N) + assert.Equal(t, "0x9b7cffdaa674beae0f930ebe6085af9093e5fe56b34a5c220ccdcf6efc336fc5", result.Result.Asset) + assert.Equal(t, util.Fixed8FromInt64(100000000), result.Result.Value) + assert.Equal(t, "AZ81H31DMWzbSnFDLFkzh9vHwaDLayV7fU", result.Result.Address) + }) } func (tc rpcTestCase) getResultPair(e *executor) (expected interface{}, res interface{}) { diff --git a/pkg/rpc/types.go b/pkg/rpc/types.go index 058bc9046..50beee7d5 100644 --- a/pkg/rpc/types.go +++ b/pkg/rpc/types.go @@ -96,6 +96,13 @@ type GetRawTxResponse struct { Result *RawTxResponse `json:"result"` } +// GetTxOutResponse represents result of `gettxout` RPC call. +type GetTxOutResponse struct { + responseHeader + Error *Error + Result *wrappers.TransactionOutput +} + // RawTxResponse stores transaction with blockchain metadata to be sent as a response. type RawTxResponse struct { TxResponse diff --git a/pkg/rpc/wrappers/tx_output.go b/pkg/rpc/wrappers/tx_output.go new file mode 100644 index 000000000..e624dff8e --- /dev/null +++ b/pkg/rpc/wrappers/tx_output.go @@ -0,0 +1,27 @@ +package wrappers + +import ( + "github.com/CityOfZion/neo-go/pkg/core/transaction" + "github.com/CityOfZion/neo-go/pkg/encoding/address" + "github.com/CityOfZion/neo-go/pkg/util" +) + +// TransactionOutput is a wrapper to represent transaction's output. +type TransactionOutput struct { + N int `json:"n"` + Asset string `json:"asset"` + Value util.Fixed8 `json:"value"` + Address string `json:"address"` +} + +// NewTxOutput converts out to a TransactionOutput. +func NewTxOutput(out *transaction.Output) *TransactionOutput { + addr := address.Uint160ToString(out.ScriptHash) + + return &TransactionOutput{ + N: out.Position, + Asset: "0x" + out.AssetID.String(), + Value: out.Amount, + Address: addr, + } +}