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/core/blockchain.go b/pkg/core/blockchain.go index 2ae8116e6..180b80da4 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -3,7 +3,6 @@ package core import ( "fmt" "math" - "math/big" "sort" "sync" "sync/atomic" @@ -801,31 +800,11 @@ func (bc *Blockchain) storeBlock(block *block.Block) error { } var index uint32 for _, note := range systemInterop.notifications { - arr, ok := note.Item.Value().([]vm.StackItem) - if !ok || len(arr) != 4 { + transfer, err := state.NEP5TransferFromNotification(note, tx.Hash(), block.Index, block.Timestamp, index) + if err != nil { continue } - op, ok := arr[0].Value().([]byte) - if !ok || string(op) != "transfer" { - continue - } - from, ok := arr[1].Value().([]byte) - if !ok { - continue - } - to, ok := arr[2].Value().([]byte) - if !ok { - continue - } - amount, ok := arr[3].Value().(*big.Int) - if !ok { - bs, ok := arr[3].Value().([]byte) - if !ok { - continue - } - amount = emit.BytesToInt(bs) - } - bc.processNEP5Transfer(cache, tx, block, note.ScriptHash, from, to, amount.Int64(), index) + bc.processNEP5Transfer(cache, transfer) index++ } } else { @@ -957,66 +936,48 @@ func processTransfer(cache *dao.Cached, tx *transaction.Transaction, b *block.Bl return nil } -func parseUint160(addr []byte) util.Uint160 { - if u, err := util.Uint160DecodeBytesBE(addr); err == nil { - return u - } - return util.Uint160{} -} - -func (bc *Blockchain) processNEP5Transfer(cache *dao.Cached, tx *transaction.Transaction, b *block.Block, sc util.Uint160, from, to []byte, amount int64, index uint32) { - toAddr := parseUint160(to) - fromAddr := parseUint160(from) - transfer := &state.NEP5Transfer{ - Asset: sc, - From: fromAddr, - To: toAddr, - Block: b.Index, - Timestamp: b.Timestamp, - Tx: tx.Hash(), - Index: index, - } - if !fromAddr.Equals(util.Uint160{}) { - balances, err := cache.GetNEP5Balances(fromAddr) +func (bc *Blockchain) processNEP5Transfer(cache *dao.Cached, transfer *state.NEP5Transfer) { + if !transfer.From.Equals(util.Uint160{}) { + balances, err := cache.GetNEP5Balances(transfer.From) if err != nil { return } - bs := balances.Trackers[sc] - bs.Balance -= amount - bs.LastUpdatedBlock = b.Index - balances.Trackers[sc] = bs + bs := balances.Trackers[transfer.Asset] + bs.Balance -= transfer.Amount + bs.LastUpdatedBlock = transfer.Block + balances.Trackers[transfer.Asset] = bs - transfer.Amount = -amount - isBig, err := cache.AppendNEP5Transfer(fromAddr, balances.NextTransferBatch, transfer) + transfer.Amount = -transfer.Amount + isBig, err := cache.AppendNEP5Transfer(transfer.From, balances.NextTransferBatch, transfer) + if err != nil { + return + } + transfer.Amount = -transfer.Amount + if isBig { + balances.NextTransferBatch++ + } + if err := cache.PutNEP5Balances(transfer.From, balances); err != nil { + return + } + } + if !transfer.To.Equals(util.Uint160{}) { + balances, err := cache.GetNEP5Balances(transfer.To) + if err != nil { + return + } + bs := balances.Trackers[transfer.Asset] + bs.Balance += transfer.Amount + bs.LastUpdatedBlock = transfer.Block + balances.Trackers[transfer.Asset] = bs + + isBig, err := cache.AppendNEP5Transfer(transfer.To, balances.NextTransferBatch, transfer) if err != nil { return } if isBig { balances.NextTransferBatch++ } - if err := cache.PutNEP5Balances(fromAddr, balances); err != nil { - return - } - } - if !toAddr.Equals(util.Uint160{}) { - balances, err := cache.GetNEP5Balances(toAddr) - if err != nil { - return - } - bs := balances.Trackers[sc] - bs.Balance += amount - bs.LastUpdatedBlock = b.Index - balances.Trackers[sc] = bs - - transfer.Amount = amount - isBig, err := cache.AppendNEP5Transfer(toAddr, balances.NextTransferBatch, transfer) - if err != nil { - return - } - if isBig { - balances.NextTransferBatch++ - } - if err := cache.PutNEP5Balances(toAddr, balances); err != nil { + if err := cache.PutNEP5Balances(transfer.To, balances); err != nil { return } } diff --git a/pkg/core/state/nep5.go b/pkg/core/state/nep5.go index 41b7a661d..d289ec00e 100644 --- a/pkg/core/state/nep5.go +++ b/pkg/core/state/nep5.go @@ -1,8 +1,13 @@ package state import ( + "errors" + "math/big" + "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm" + "github.com/nspcc-dev/neo-go/pkg/vm/emit" ) // NEP5Tracker contains info about a single account in a NEP5 contract. @@ -148,6 +153,56 @@ func (t *NEP5Tracker) DecodeBinary(r *io.BinReader) { t.LastUpdatedBlock = r.ReadU32LE() } +func parseUint160(addr []byte) util.Uint160 { + if u, err := util.Uint160DecodeBytesBE(addr); err == nil { + return u + } + return util.Uint160{} +} + +// NEP5TransferFromNotification creates NEP5Transfer structure from the given +// notification (and using given context) if it's possible to parse it as +// NEP5 transfer. +func NEP5TransferFromNotification(ne NotificationEvent, txHash util.Uint256, height uint32, time uint32, index uint32) (*NEP5Transfer, error) { + arr, ok := ne.Item.Value().([]vm.StackItem) + if !ok || len(arr) != 4 { + return nil, errors.New("no array or wrong element count") + } + op, ok := arr[0].Value().([]byte) + if !ok || string(op) != "transfer" { + return nil, errors.New("not a 'transfer' event") + } + from, ok := arr[1].Value().([]byte) + if !ok { + return nil, errors.New("wrong 'from' type") + } + to, ok := arr[2].Value().([]byte) + if !ok { + return nil, errors.New("wrong 'to' type") + } + amount, ok := arr[3].Value().(*big.Int) + if !ok { + bs, ok := arr[3].Value().([]byte) + if !ok { + return nil, errors.New("wrong amount type") + } + amount = emit.BytesToInt(bs) + } + toAddr := parseUint160(to) + fromAddr := parseUint160(from) + transfer := &NEP5Transfer{ + Asset: ne.ScriptHash, + From: fromAddr, + To: toAddr, + Amount: amount.Int64(), + Block: height, + Timestamp: time, + Tx: txHash, + Index: index, + } + return transfer, nil +} + // EncodeBinary implements io.Serializable interface. // Note: change NEP5TransferSize constant when changing this function. func (t *NEP5Transfer) EncodeBinary(w *io.BinWriter) { 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{}) {