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.
|
// 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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
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"
|
||||||
"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())
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue