From 68fc8168ec62a66b0919aec9fab251cb180be29d Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Fri, 30 Oct 2020 15:36:34 +0300 Subject: [PATCH] rpc: add getblocktransfertx RPC call It uses a bit different output format than getalltransfertx, so TransferTxEvent type was adjusted accordingly. --- docs/rpc.md | 77 ++++++++++++++ pkg/rpc/client/rpc.go | 24 +++++ pkg/rpc/response/result/nep5.go | 10 +- pkg/rpc/server/server.go | 180 ++++++++++++++++++++++++-------- pkg/rpc/server/server_test.go | 82 +++++++++++++++ 5 files changed, 324 insertions(+), 49 deletions(-) diff --git a/docs/rpc.md b/docs/rpc.md index e8f5baff9..57c5513d7 100644 --- a/docs/rpc.md +++ b/docs/rpc.md @@ -260,6 +260,83 @@ Reply: } ``` +#### getblocktransfertx call + +`getblocktransfertx` provides a list of transactions that did some asset +transfers in a block (either UTXO or NEP5). It gets a block number or hash as +a single parameter and its output format is similar to `getalltransfertx` +except for `events` where it doesn't use `address` and `type` fields, but +rather provides `from` and `to` (meaning that the asset was moved from `from` +to `to` address). + +Example request: + +```json +{ "jsonrpc": "2.0", "id": 5, "method": "getblocktransfertx", "params": [6000003]} + +``` + +Reply: +```json +{ + "id" : 5, + "result" : [ + { + "txid" : "0xaec0994211e5d7fd459a4445b113db0102ac79cb90a08b3211b9a9190a6feaa3", + "elements" : [ + { + "asset" : "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7", + "type" : "output", + "value" : "0.19479178", + "address" : "AHwyehUHV8ujVJBN6Tz3jBDuPAHQ1wKU5R" + } + ], + "block_index" : 6000003, + "timestamp" : 1597295221, + "sys_fee" : "0", + "net_fee" : "0" + }, + { + "sys_fee" : "0", + "net_fee" : "0", + "elements" : [ + { + "value" : "971", + "address" : "AHFvPbmMbxnD6EQQWcope8VWKEMDtG1qTQ", + "asset" : "c56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930faebe74a6daff7c9b", + "type" : "input" + }, + { + "address" : "AP18zgg58bK6vZ7MX51XfD63eEEuqKCgJt", + "value" : "971", + "asset" : "c56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930faebe74a6daff7c9b", + "type" : "output" + } + ], + "block_index" : 6000003, + "txid" : "0x6b0888b10b1150d301f749d56b7365b307d814cfd843bd064e68313bb30c9351", + "timestamp" : 1597295221 + }, + { + "sys_fee" : "0", + "net_fee" : "0", + "block_index" : 6000003, + "txid" : "0x6b2220834059710aecfe4b2cbdb56311bbb27ac5d94795c041b5a2e6fb76f96e", + "timestamp" : 1597295221, + "events" : [ + { + "from" : "AeNAPrVp7ZWtYLaAWvZ3gkKQsJBZUJJz3r", + "asset" : "b951ecbbc5fe37a9c280a76cb0ce0014827294cf", + "to" : "AVkhaHaxLaboUVFD1Rke5abTJuKAqziCkY", + "value" : "69061428" + } + ] + } + ], + "jsonrpc" : "2.0" +} +``` + #### Websocket server This server accepts websocket connections on `ws://$BASE_URL/ws` address. You diff --git a/pkg/rpc/client/rpc.go b/pkg/rpc/client/rpc.go index 075abcae9..70cf8bd20 100644 --- a/pkg/rpc/client/rpc.go +++ b/pkg/rpc/client/rpc.go @@ -201,6 +201,30 @@ func (c *Client) GetBlockSysFee(index uint32) (util.Fixed8, error) { return resp, nil } +// getBlockTransferTx is an internal version of GetBlockTransferTxByIndex/GetBlockTransferTxByHash. +func (c *Client) getBlockTransferTx(param interface{}) ([]result.TransferTx, error) { + var ( + params = request.NewRawParams(param) + resp = new([]result.TransferTx) + ) + if err := c.performRequest("getblocktransfertx", params, resp); err != nil { + return nil, err + } + return *resp, nil +} + +// GetBlockTransferTxByIndex returns all transfer transactions from a block. +// It only works with neo-go 0.79.0+ servers. +func (c *Client) GetBlockTransferTxByIndex(index uint32) ([]result.TransferTx, error) { + return c.getBlockTransferTx(index) +} + +// GetBlockTransferTxByHash returns all transfer transactions from a block. +// It only works with neo-go 0.79.0+ servers. +func (c *Client) GetBlockTransferTxByHash(hash util.Uint256) ([]result.TransferTx, error) { + return c.getBlockTransferTx(hash) +} + // GetClaimable returns tx outputs which can be claimed. func (c *Client) GetClaimable(address string) (*result.ClaimableInfo, error) { params := request.NewRawParams(address) diff --git a/pkg/rpc/response/result/nep5.go b/pkg/rpc/response/result/nep5.go index 139f4f22a..0cf586f22 100644 --- a/pkg/rpc/response/result/nep5.go +++ b/pkg/rpc/response/result/nep5.go @@ -83,10 +83,14 @@ type TransferTx struct { } // TransferTxEvent is an event used for elements or events of TransferTx, it's -// either a single input/output, or a nep5 transfer. +// either a single input/output, or a nep5 transfer. The former always has +// Address and Type fields set with no From/To, the latter can either have +// From and To or Address and Type depending on particular RPC API function. type TransferTxEvent struct { - Address string `json:"address"` - Type string `json:"type"` + Address string `json:"address,omitempty"` + From string `json:"from,omitempty"` + To string `json:"to,omitempty"` + Type string `json:"type,omitempty"` Value string `json:"value"` Asset string `json:"asset"` } diff --git a/pkg/rpc/server/server.go b/pkg/rpc/server/server.go index 9019e90dd..6073d36bc 100644 --- a/pkg/rpc/server/server.go +++ b/pkg/rpc/server/server.go @@ -90,6 +90,7 @@ var rpcHandlers = map[string]func(*Server, request.Params) (interface{}, *respon "getblockhash": (*Server).getBlockHash, "getblockheader": (*Server).getBlockHeader, "getblocksysfee": (*Server).getBlockSysFee, + "getblocktransfertx": (*Server).getBlockTransferTx, "getclaimable": (*Server).getClaimable, "getconnectioncount": (*Server).getConnectionCount, "getcontractstate": (*Server).getContractState, @@ -395,29 +396,34 @@ func (s *Server) getConnectionCount(_ request.Params) (interface{}, *response.Er return s.coreServer.PeerCount(), nil } -func (s *Server) getBlock(reqParams request.Params) (interface{}, *response.Error) { +func (s *Server) getBlockHashFromParam(param *request.Param) (util.Uint256, *response.Error) { var hash util.Uint256 - - param := reqParams.Value(0) if param == nil { - return nil, response.ErrInvalidParams + return hash, response.ErrInvalidParams } - switch param.Type { case request.StringT: var err error hash, err = param.GetUint256() if err != nil { - return nil, response.ErrInvalidParams + return hash, response.ErrInvalidParams } case request.NumberT: num, err := s.blockHeightFromParam(param) if err != nil { - return nil, response.ErrInvalidParams + return hash, response.ErrInvalidParams } hash = s.chain.GetHeaderHash(num) default: - return nil, response.ErrInvalidParams + return hash, response.ErrInvalidParams + } + return hash, nil +} + +func (s *Server) getBlock(reqParams request.Params) (interface{}, *response.Error) { + hash, respErr := s.getBlockHashFromParam(reqParams.Value(0)) + if respErr != nil { + return nil, respErr } block, err := s.chain.GetBlock(hash) @@ -829,6 +835,56 @@ func (s *Server) getNEP5Transfers(ps request.Params) (interface{}, *response.Err return bs, nil } +func appendUTXOToTransferTx(transfer *result.TransferTx, tx *transaction.Transaction, chain core.Blockchainer) *response.Error { + inouts, err := chain.References(tx) + if err != nil { + return response.NewInternalServerError("invalid tx", err) + } + for _, inout := range inouts { + var event result.TransferTxEvent + + event.Address = address.Uint160ToString(inout.Out.ScriptHash) + event.Type = "input" + event.Value = inout.Out.Amount.String() + event.Asset = inout.Out.AssetID.StringLE() + transfer.Elements = append(transfer.Elements, event) + } + for _, out := range tx.Outputs { + var event result.TransferTxEvent + + event.Address = address.Uint160ToString(out.ScriptHash) + event.Type = "output" + event.Value = out.Amount.String() + event.Asset = out.AssetID.StringLE() + transfer.Elements = append(transfer.Elements, event) + } + return nil +} + +// uint160ToString converts given hash to address, unless it's zero and an empty +// string is returned then. +func uint160ToString(u util.Uint160) string { + if u.Equals(util.Uint160{}) { + return "" + } + return address.Uint160ToString(u) +} + +func appendNEP5ToTransferTx(transfer *result.TransferTx, nepTr *state.NEP5Transfer) { + var event result.TransferTxEvent + event.Asset = nepTr.Asset.StringLE() + if nepTr.Amount > 0 { // token was received + event.Value = strconv.FormatInt(nepTr.Amount, 10) + event.Type = "receive" + event.Address = uint160ToString(nepTr.From) + } else { + event.Value = strconv.FormatInt(-nepTr.Amount, 10) + event.Type = "send" + event.Address = uint160ToString(nepTr.To) + } + transfer.Events = append(transfer.Events, event) +} + func (s *Server) getAllTransferTx(ps request.Params) (interface{}, *response.Error) { var respErr *response.Error @@ -937,50 +993,15 @@ func (s *Server) getAllTransferTx(ps request.Params) (interface{}, *response.Err } transfer.NetworkFee = s.chain.NetworkFee(tx).String() transfer.SystemFee = s.chain.SystemFee(tx).String() - - inouts, err := s.chain.References(tx) - if err != nil { - respErr = response.NewInternalServerError("invalid tx", err) + respErr = appendUTXOToTransferTx(&transfer, tx, s.chain) + if respErr != nil { break } - for _, inout := range inouts { - var event result.TransferTxEvent - - event.Address = address.Uint160ToString(inout.Out.ScriptHash) - event.Type = "input" - event.Value = inout.Out.Amount.String() - event.Asset = inout.Out.AssetID.StringLE() - transfer.Elements = append(transfer.Elements, event) - } - for _, out := range tx.Outputs { - var event result.TransferTxEvent - - event.Address = address.Uint160ToString(out.ScriptHash) - event.Type = "output" - event.Value = out.Amount.String() - event.Asset = out.AssetID.StringLE() - transfer.Elements = append(transfer.Elements, event) - } } // Pick all NEP5 events for this transaction, if there are any. for haveNep5 && nep5Last.Tx.Equals(transfer.TxID) { if !skipTx { - var event result.TransferTxEvent - event.Asset = nep5Last.Asset.StringLE() - if nep5Last.Amount > 0 { // token was received - event.Value = strconv.FormatInt(nep5Last.Amount, 10) - event.Type = "receive" - if !nep5Last.From.Equals(util.Uint160{}) { - event.Address = address.Uint160ToString(nep5Last.From) - } - } else { - event.Value = strconv.FormatInt(-nep5Last.Amount, 10) - event.Type = "send" - if !nep5Last.To.Equals(util.Uint160{}) { - event.Address = address.Uint160ToString(nep5Last.To) - } - } - transfer.Events = append(transfer.Events, event) + appendNEP5ToTransferTx(&transfer, &nep5Last) } nep5Last, haveNep5 = <-nep5Trs if haveNep5 { @@ -1236,6 +1257,73 @@ func (s *Server) getAccountStateAux(reqParams request.Params, unspents bool) (in return results, resultsErr } +func (s *Server) getBlockTransferTx(ps request.Params) (interface{}, *response.Error) { + var ( + res = make([]result.TransferTx, 0) + respErr *response.Error + ) + + hash, respErr := s.getBlockHashFromParam(ps.Value(0)) + if respErr != nil { + return nil, respErr + } + + block, err := s.chain.GetBlock(hash) + if err != nil { + return nil, response.NewInternalServerError(fmt.Sprintf("Problem locating block with hash: %s", hash), err) + } + + for _, tx := range block.Transactions { + var transfer = result.TransferTx{ + TxID: tx.Hash(), + Timestamp: block.Timestamp, + Index: block.Index, + NetworkFee: s.chain.NetworkFee(tx).String(), + SystemFee: s.chain.SystemFee(tx).String(), + } + + respErr = appendUTXOToTransferTx(&transfer, tx, s.chain) + if respErr != nil { + break + } + if tx.Type == transaction.InvocationType { + execRes, err := s.chain.GetAppExecResult(tx.Hash()) + if err != nil { + respErr = response.NewInternalServerError(fmt.Sprintf("no application log for invocation tx %s", tx.Hash()), err) + break + } + + if execRes.VMState != "HALT" { + continue + } + + var index uint32 + for _, note := range execRes.Events { + nepTr, err := state.NEP5TransferFromNotification(note, tx.Hash(), block.Index, block.Timestamp, index) + // It's OK for event to be something different from NEP5 transfer. + if err != nil { + continue + } + transfer.Events = append(transfer.Events, result.TransferTxEvent{ + Asset: nepTr.Asset.StringLE(), + From: uint160ToString(nepTr.From), + To: uint160ToString(nepTr.To), + Value: strconv.FormatInt(nepTr.Amount, 10), + }) + index++ + } + } + + if len(transfer.Elements) != 0 || len(transfer.Events) != 0 { + res = append(res, transfer) + } + } + if respErr != nil { + return nil, respErr + } + return res, nil +} + // getBlockSysFee returns the system fees of the block, based on the specified index. func (s *Server) getBlockSysFee(reqParams request.Params) (interface{}, *response.Error) { param := reqParams.ValueWithType(0, request.NumberT) diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go index 1168ecd22..721c65b35 100644 --- a/pkg/rpc/server/server_test.go +++ b/pkg/rpc/server/server_test.go @@ -1325,6 +1325,88 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) [] }) }) }) + t.Run("getblocktransfertx", func(t *testing.T) { + bNeo, err := e.chain.GetBlock(e.chain.GetHeaderHash(206)) + require.NoError(t, err) + txNeoTo1 := bNeo.Transactions[1].Hash() + + body := doRPCCall(`{"jsonrpc": "2.0", "id": 1, "method": "getblocktransfertx", "params": [206]}`, httpSrv.URL, t) + res := checkErrGetResult(t, body, false) + actualp := new([]result.TransferTx) + require.NoError(t, json.Unmarshal(res, actualp)) + expected := []result.TransferTx{ + result.TransferTx{ + TxID: txNeoTo1, + Timestamp: bNeo.Timestamp, + Index: bNeo.Index, + SystemFee: "0", + NetworkFee: "0", + Elements: []result.TransferTxEvent{ + result.TransferTxEvent{ + Address: "AKkkumHbBipZ46UMZJoFynJMXzSRnBvKcs", + Type: "input", + Value: "99999000", + Asset: "c56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930faebe74a6daff7c9b", + }, + result.TransferTxEvent{ + Address: "AWLYWXB8C9Lt1nHdDZJnC5cpYJjgRDLk17", + Type: "output", + Value: "1000", + Asset: "c56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930faebe74a6daff7c9b", + }, + result.TransferTxEvent{ + Address: "AKkkumHbBipZ46UMZJoFynJMXzSRnBvKcs", + Type: "output", + Value: "99998000", + Asset: "c56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930faebe74a6daff7c9b", + }, + }, + }, + } + require.Equal(t, expected, *actualp) + + bNep5, err := e.chain.GetBlock(e.chain.GetHeaderHash(207)) + require.NoError(t, err) + txNep5Init := bNep5.Transactions[1].Hash() + txNep5Transfer := bNep5.Transactions[2].Hash() + + body = doRPCCall(`{"jsonrpc": "2.0", "id": 1, "method": "getblocktransfertx", "params": [207]}`, httpSrv.URL, t) + res = checkErrGetResult(t, body, false) + actualp = new([]result.TransferTx) + require.NoError(t, json.Unmarshal(res, actualp)) + expected = []result.TransferTx{ + result.TransferTx{ + TxID: txNep5Init, + Timestamp: bNep5.Timestamp, + Index: bNep5.Index, + SystemFee: "0", + NetworkFee: "0", + Events: []result.TransferTxEvent{ + result.TransferTxEvent{ + To: "AeEc6DNaiVZSNJfTJ72rAFFqVKAMR5B7i3", + Value: "1000000", + Asset: testContractHashOld, + }, + }, + }, + result.TransferTx{ + TxID: txNep5Transfer, + Timestamp: bNep5.Timestamp, + Index: bNep5.Index, + SystemFee: "0", + NetworkFee: "0", + Events: []result.TransferTxEvent{ + result.TransferTxEvent{ + From: "AeEc6DNaiVZSNJfTJ72rAFFqVKAMR5B7i3", + To: "AKkkumHbBipZ46UMZJoFynJMXzSRnBvKcs", + Value: "1000", + Asset: testContractHashOld, + }, + }, + }, + } + require.Equal(t, expected, *actualp) + }) } func (tc rpcTestCase) getResultPair(e *executor) (expected interface{}, res interface{}) {