mirror of
https://github.com/nspcc-dev/neo-go.git
synced 2024-12-04 19:19:44 +00:00
rpc: implement getutxotransfers
RPC
This commit is contained in:
parent
407e348cd5
commit
022fb04077
6 changed files with 276 additions and 0 deletions
|
@ -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.
|
||||
func (bc *Blockchain) GetNEP5TransferLog(acc util.Uint160) *state.TransferLog {
|
||||
balances, err := bc.dao.GetNEP5Balances(acc)
|
||||
|
|
|
@ -26,6 +26,7 @@ type Blockchainer interface {
|
|||
GetBlock(hash util.Uint256) (*block.Block, error)
|
||||
GetContractState(hash util.Uint160) *state.Contract
|
||||
GetEnrollments() ([]*state.Validator, error)
|
||||
ForEachTransfer(util.Uint160, *state.Transfer, func() error) error
|
||||
GetHeaderHash(int) util.Uint256
|
||||
GetHeader(hash util.Uint256) (*block.Header, error)
|
||||
CurrentHeaderHash() util.Uint256
|
||||
|
|
|
@ -108,6 +108,9 @@ func (chain testChain) GetValidators(...*transaction.Transaction) ([]*keys.Publi
|
|||
func (chain testChain) GetEnrollments() ([]*state.Validator, error) {
|
||||
panic("TODO")
|
||||
}
|
||||
func (chain testChain) ForEachTransfer(util.Uint160, *state.Transfer, func() error) error {
|
||||
panic("TODO")
|
||||
}
|
||||
func (chain testChain) GetScriptHashesForVerifying(*transaction.Transaction) ([]util.Uint160, error) {
|
||||
panic("TODO")
|
||||
}
|
||||
|
|
27
pkg/rpc/response/result/utxo.go
Normal file
27
pkg/rpc/response/result/utxo.go
Normal 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"`
|
||||
}
|
|
@ -8,6 +8,7 @@ import (
|
|||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
|
@ -103,6 +104,7 @@ var rpcHandlers = map[string]func(*Server, request.Params) (interface{}, *respon
|
|||
"getunspents": (*Server).getUnspents,
|
||||
"getvalidators": (*Server).getValidators,
|
||||
"getversion": (*Server).getVersion,
|
||||
"getutxotransfers": (*Server).getUTXOTransfers,
|
||||
"invoke": (*Server).invoke,
|
||||
"invokefunction": (*Server).invokeFunction,
|
||||
"invokescript": (*Server).invokescript,
|
||||
|
@ -447,6 +449,133 @@ func (s *Server) getVersion(_ request.Params) (interface{}, *response.Error) {
|
|||
}, 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) {
|
||||
peers := result.NewGetPeers()
|
||||
peers.AddUnconnected(s.coreServer.UnconnectedPeers())
|
||||
|
|
|
@ -290,6 +290,28 @@ var rpcTestCases = map[string][]rpcTestCase{
|
|||
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": {
|
||||
{
|
||||
name: "positive",
|
||||
|
@ -1104,6 +1126,39 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) []
|
|||
|
||||
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{}) {
|
||||
|
@ -1190,3 +1245,45 @@ func checkNep5Transfers(t *testing.T, e *executor, acc interface{}) {
|
|||
require.Equal(t, "AWLYWXB8C9Lt1nHdDZJnC5cpYJjgRDLk17", res.Sent[0].Address)
|
||||
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
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue