rpc: implement getutxotransfers RPC

This commit is contained in:
Evgenii Stratonikov 2020-08-04 18:16:32 +03:00
parent 407e348cd5
commit 022fb04077
6 changed files with 276 additions and 0 deletions

View file

@ -1067,6 +1067,25 @@ func (bc *Blockchain) processNEP5Transfer(cache *dao.Cached, tx *transaction.Tra
} }
} }
// ForEachTransfer executes f for each transfer in log.
func (bc *Blockchain) ForEachTransfer(acc util.Uint160, tr *state.Transfer, f func() error) error {
nb, err := bc.dao.GetNextTransferBatch(acc)
if err != nil {
return nil
}
for i := uint32(0); i <= nb; i++ {
lg, err := bc.dao.GetTransferLog(acc, i)
if err != nil {
return nil
}
err = lg.ForEach(state.TransferSize, tr, f)
if err != nil {
return err
}
}
return nil
}
// GetNEP5TransferLog returns NEP5 transfer log for the acc. // GetNEP5TransferLog returns NEP5 transfer log for the acc.
func (bc *Blockchain) GetNEP5TransferLog(acc util.Uint160) *state.TransferLog { func (bc *Blockchain) GetNEP5TransferLog(acc util.Uint160) *state.TransferLog {
balances, err := bc.dao.GetNEP5Balances(acc) balances, err := bc.dao.GetNEP5Balances(acc)

View file

@ -26,6 +26,7 @@ type Blockchainer interface {
GetBlock(hash util.Uint256) (*block.Block, error) GetBlock(hash util.Uint256) (*block.Block, error)
GetContractState(hash util.Uint160) *state.Contract GetContractState(hash util.Uint160) *state.Contract
GetEnrollments() ([]*state.Validator, error) GetEnrollments() ([]*state.Validator, error)
ForEachTransfer(util.Uint160, *state.Transfer, func() error) error
GetHeaderHash(int) util.Uint256 GetHeaderHash(int) util.Uint256
GetHeader(hash util.Uint256) (*block.Header, error) GetHeader(hash util.Uint256) (*block.Header, error)
CurrentHeaderHash() util.Uint256 CurrentHeaderHash() util.Uint256

View file

@ -108,6 +108,9 @@ func (chain testChain) GetValidators(...*transaction.Transaction) ([]*keys.Publi
func (chain testChain) GetEnrollments() ([]*state.Validator, error) { func (chain testChain) GetEnrollments() ([]*state.Validator, error) {
panic("TODO") panic("TODO")
} }
func (chain testChain) ForEachTransfer(util.Uint160, *state.Transfer, func() error) error {
panic("TODO")
}
func (chain testChain) GetScriptHashesForVerifying(*transaction.Transaction) ([]util.Uint160, error) { func (chain testChain) GetScriptHashesForVerifying(*transaction.Transaction) ([]util.Uint160, error) {
panic("TODO") panic("TODO")
} }

View file

@ -0,0 +1,27 @@
package result
import "github.com/nspcc-dev/neo-go/pkg/util"
// UTXO represents single output for a single asset.
type UTXO struct {
Index uint32 `json:"block_index"`
Timestamp uint32 `json:"timestamp"`
TxHash util.Uint256 `json:"txid"`
Address util.Uint160 `json:"transfer_address"`
Amount int64 `json:"amount,string"`
}
// AssetUTXO represents UTXO for a specific asset.
type AssetUTXO struct {
AssetHash util.Uint256 `json:"asset_hash"`
AssetName string `json:"asset"`
TotalAmount int64 `json:"total_amount,string"`
Transactions []UTXO `json:"transactions"`
}
// GetUTXO is a result of the `getutxotransfers` RPC.
type GetUTXO struct {
Address string `json:"address"`
Sent []AssetUTXO `json:"sent"`
Received []AssetUTXO `json:"received"`
}

View file

@ -8,6 +8,7 @@ import (
"net" "net"
"net/http" "net/http"
"strconv" "strconv"
"strings"
"sync" "sync"
"time" "time"
@ -103,6 +104,7 @@ var rpcHandlers = map[string]func(*Server, request.Params) (interface{}, *respon
"getunspents": (*Server).getUnspents, "getunspents": (*Server).getUnspents,
"getvalidators": (*Server).getValidators, "getvalidators": (*Server).getValidators,
"getversion": (*Server).getVersion, "getversion": (*Server).getVersion,
"getutxotransfers": (*Server).getUTXOTransfers,
"invoke": (*Server).invoke, "invoke": (*Server).invoke,
"invokefunction": (*Server).invokeFunction, "invokefunction": (*Server).invokeFunction,
"invokescript": (*Server).invokescript, "invokescript": (*Server).invokescript,
@ -447,6 +449,133 @@ func (s *Server) getVersion(_ request.Params) (interface{}, *response.Error) {
}, nil }, nil
} }
func getTimestamps(p1, p2 *request.Param) (uint32, uint32, error) {
var start, end uint32
if p1 != nil {
val, err := p1.GetInt()
if err != nil {
return 0, 0, err
}
start = uint32(val)
}
if p2 != nil {
val, err := p2.GetInt()
if err != nil {
return 0, 0, err
}
end = uint32(val)
}
return start, end, nil
}
func getAssetMaps(name string) (map[util.Uint256]*result.AssetUTXO, map[util.Uint256]*result.AssetUTXO, error) {
sent := make(map[util.Uint256]*result.AssetUTXO)
recv := make(map[util.Uint256]*result.AssetUTXO)
name = strings.ToLower(name)
switch name {
case "neo", "gas", "":
default:
return nil, nil, errors.New("invalid asset")
}
if name == "neo" || name == "" {
sent[core.GoverningTokenID()] = &result.AssetUTXO{
AssetHash: core.GoverningTokenID(),
AssetName: "NEO",
Transactions: []result.UTXO{},
}
recv[core.GoverningTokenID()] = &result.AssetUTXO{
AssetHash: core.GoverningTokenID(),
AssetName: "NEO",
Transactions: []result.UTXO{},
}
}
if name == "gas" || name == "" {
sent[core.UtilityTokenID()] = &result.AssetUTXO{
AssetHash: core.UtilityTokenID(),
AssetName: "GAS",
Transactions: []result.UTXO{},
}
recv[core.UtilityTokenID()] = &result.AssetUTXO{
AssetHash: core.UtilityTokenID(),
AssetName: "GAS",
Transactions: []result.UTXO{},
}
}
return sent, recv, nil
}
func (s *Server) getUTXOTransfers(ps request.Params) (interface{}, *response.Error) {
addr, err := ps.Value(0).GetUint160FromAddressOrHex()
if err != nil {
return nil, response.NewInvalidParamsError("", err)
}
index := 1
assetName, err := ps.Value(index).GetString()
if err == nil {
index++
}
start, end, err := getTimestamps(ps.Value(index), ps.Value(index+1))
if err != nil {
return nil, response.NewInvalidParamsError("", err)
}
sent, recv, err := getAssetMaps(assetName)
if err != nil {
return nil, response.NewInvalidParamsError("", err)
}
tr := new(state.Transfer)
err = s.chain.ForEachTransfer(addr, tr, func() error {
if tr.Timestamp < start || end != 0 && tr.Timestamp > end {
return nil
}
assetID := core.GoverningTokenID()
if !tr.IsGoverning {
assetID = core.UtilityTokenID()
}
a, ok := sent[assetID]
if ok && tr.From.Equals(addr) && !tr.To.Equals(addr) {
a.Transactions = append(a.Transactions, result.UTXO{
Index: tr.Block,
Timestamp: tr.Timestamp,
TxHash: tr.Tx,
Address: tr.To,
Amount: tr.Amount,
})
a.TotalAmount += tr.Amount
}
a, ok = recv[assetID]
if ok && tr.To.Equals(addr) && !tr.From.Equals(addr) {
a.Transactions = append(a.Transactions, result.UTXO{
Index: tr.Block,
Timestamp: tr.Timestamp,
TxHash: tr.Tx,
Address: tr.From,
Amount: tr.Amount,
})
a.TotalAmount += tr.Amount
}
return nil
})
if err != nil {
return nil, response.NewInternalServerError("", err)
}
res := &result.GetUTXO{
Address: address.Uint160ToString(addr),
Sent: []result.AssetUTXO{},
Received: []result.AssetUTXO{},
}
for _, a := range sent {
res.Sent = append(res.Sent, *a)
}
for _, a := range recv {
res.Received = append(res.Received, *a)
}
return res, nil
}
func (s *Server) getPeers(_ request.Params) (interface{}, *response.Error) { func (s *Server) getPeers(_ request.Params) (interface{}, *response.Error) {
peers := result.NewGetPeers() peers := result.NewGetPeers()
peers.AddUnconnected(s.coreServer.UnconnectedPeers()) peers.AddUnconnected(s.coreServer.UnconnectedPeers())

View file

@ -290,6 +290,28 @@ var rpcTestCases = map[string][]rpcTestCase{
fail: true, fail: true,
}, },
}, },
"getutxotransfers": {
{
name: "invalid address",
params: `["notanaddress"]`,
fail: true,
},
{
name: "invalid asset",
params: `["AKkkumHbBipZ46UMZJoFynJMXzSRnBvKcs", "notanasset"]`,
fail: true,
},
{
name: "invalid start timestamp",
params: `["AKkkumHbBipZ46UMZJoFynJMXzSRnBvKcs", "neo", "notanumber"]`,
fail: true,
},
{
name: "invalid end timestamp",
params: `["AKkkumHbBipZ46UMZJoFynJMXzSRnBvKcs", "neo", 123, "notanumber"]`,
fail: true,
},
},
"getassetstate": { "getassetstate": {
{ {
name: "positive", name: "positive",
@ -1104,6 +1126,39 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) []
assert.ElementsMatch(t, expected, actual) assert.ElementsMatch(t, expected, actual)
}) })
t.Run("getutxotransfers", func(t *testing.T) {
testGetUTXO := func(t *testing.T, asset string, start, stop int) {
ps := []string{`"AKkkumHbBipZ46UMZJoFynJMXzSRnBvKcs"`}
if asset != "" {
ps = append(ps, fmt.Sprintf("%q", asset))
}
if start != 0 {
if start > int(e.chain.HeaderHeight()) {
ps = append(ps, strconv.Itoa(int(time.Now().Unix())))
} else {
b, err := e.chain.GetHeader(e.chain.GetHeaderHash(start))
require.NoError(t, err)
ps = append(ps, strconv.Itoa(int(b.Timestamp)))
}
if stop != 0 {
b, err := e.chain.GetHeader(e.chain.GetHeaderHash(stop))
require.NoError(t, err)
ps = append(ps, strconv.Itoa(int(b.Timestamp)))
}
}
p := strings.Join(ps, ", ")
rpc := fmt.Sprintf(`{"jsonrpc": "2.0", "id": 1, "method": "getutxotransfers", "params": [%s]}`, p)
body := doRPCCall(rpc, httpSrv.URL, t)
res := checkErrGetResult(t, body, false)
actual := new(result.GetUTXO)
require.NoError(t, json.Unmarshal(res, actual))
checkTransfers(t, e, actual, asset, start, stop)
}
t.Run("RestrictByAsset", func(t *testing.T) { testGetUTXO(t, "neo", 0, 0) })
t.Run("TooBigStart", func(t *testing.T) { testGetUTXO(t, "", 300, 0) })
t.Run("RestrictAll", func(t *testing.T) { testGetUTXO(t, "", 202, 203) })
})
} }
func (tc rpcTestCase) getResultPair(e *executor) (expected interface{}, res interface{}) { func (tc rpcTestCase) getResultPair(e *executor) (expected interface{}, res interface{}) {
@ -1190,3 +1245,45 @@ func checkNep5Transfers(t *testing.T, e *executor, acc interface{}) {
require.Equal(t, "AWLYWXB8C9Lt1nHdDZJnC5cpYJjgRDLk17", res.Sent[0].Address) require.Equal(t, "AWLYWXB8C9Lt1nHdDZJnC5cpYJjgRDLk17", res.Sent[0].Address)
require.Equal(t, uint32(0), res.Sent[0].NotifyIndex) require.Equal(t, uint32(0), res.Sent[0].NotifyIndex)
} }
func checkTransfers(t *testing.T, e *executor, acc interface{}, asset string, start, stop int) {
res := acc.(*result.GetUTXO)
require.Equal(t, res.Address, "AKkkumHbBipZ46UMZJoFynJMXzSRnBvKcs")
// transfer from multisig address to us
u := getUTXOForBlock(res, false, asset, 1)
if start <= 1 && (stop == 0 || stop >= 1) && (asset == "neo" || asset == "") {
require.NotNil(t, u)
require.Equal(t, "be48d3a3f5d10013ab9ffee489706078714f1ea2", u.Address.StringBE())
require.EqualValues(t, int64(util.Fixed8FromInt64(99999000)), u.Amount)
} else {
require.Nil(t, u)
}
// transfer from us to another validator
u = getUTXOForBlock(res, true, asset, 206)
if start <= 206 && (stop == 0 || stop >= 206) && (asset == "neo" || asset == "") {
require.NotNil(t, u)
require.Equal(t, "9fbf833320ef6bc52ddee1fe6f5793b42e9b307e", u.Address.StringBE())
require.EqualValues(t, int64(util.Fixed8FromInt64(1000)), u.Amount)
} else {
require.Nil(t, u)
}
}
func getUTXOForBlock(res *result.GetUTXO, sent bool, asset string, b uint32) *result.UTXO {
arr := res.Received
if sent {
arr = res.Sent
}
for i := range arr {
if arr[i].AssetName == strings.ToUpper(asset) {
for j := range arr[i].Transactions {
if b == arr[i].Transactions[j].Index {
return &arr[i].Transactions[j]
}
}
}
}
return nil
}