mirror of
https://github.com/nspcc-dev/neo-go.git
synced 2024-12-13 15:45:01 +00:00
rpc/server: add new getalltransfertx API
It unifies UTXO and NEP5 transfers for a given address and presents it with transaction-level grouping (and additional metadata).
This commit is contained in:
parent
010c22e2b5
commit
23719f7e72
4 changed files with 326 additions and 0 deletions
|
@ -28,6 +28,20 @@ func (c *Client) GetAccountState(address string) (*result.AccountState, error) {
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAllTransferTx returns all transfer transactions for a given account within
|
||||||
|
// specified timestamps (by block time) with specified output limits and page. It
|
||||||
|
// only works with neo-go 0.78.0+ servers.
|
||||||
|
func (c *Client) GetAllTransferTx(acc util.Uint160, start, end uint32, limit, page int) ([]result.TransferTx, error) {
|
||||||
|
var (
|
||||||
|
params = request.NewRawParams(acc.StringLE(), start, end, limit, page)
|
||||||
|
resp = new([]result.TransferTx)
|
||||||
|
)
|
||||||
|
if err := c.performRequest("getalltransfertx", params, resp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return *resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetApplicationLog returns the contract log based on the specified txid.
|
// GetApplicationLog returns the contract log based on the specified txid.
|
||||||
func (c *Client) GetApplicationLog(hash util.Uint256) (*result.ApplicationLog, error) {
|
func (c *Client) GetApplicationLog(hash util.Uint256) (*result.ApplicationLog, error) {
|
||||||
var (
|
var (
|
||||||
|
|
|
@ -69,3 +69,24 @@ func (b *NEP5Balance) UnmarshalJSON(data []byte) error {
|
||||||
b.LastUpdated = s.LastUpdated
|
b.LastUpdated = s.LastUpdated
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TransferTx is a type used to represent and element of `getalltransfertx`
|
||||||
|
// result. It combines transaction's inputs/outputs with NEP5 events.
|
||||||
|
type TransferTx struct {
|
||||||
|
TxID util.Uint256 `json:"txid"`
|
||||||
|
Timestamp uint32 `json:"timestamp"`
|
||||||
|
Index uint32 `json:"block_index"`
|
||||||
|
SystemFee int64 `json:"sys_fee"`
|
||||||
|
NetworkFee int64 `json:"net_fee"`
|
||||||
|
Elements []TransferTxEvent `json:"elements,omitempty"`
|
||||||
|
Events []TransferTxEvent `json:"events,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransferTxEvent is an event used for elements or events of TransferTx, it's
|
||||||
|
// either a single input/output, or a nep5 transfer.
|
||||||
|
type TransferTxEvent struct {
|
||||||
|
Address string `json:"address"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
Asset string `json:"asset"`
|
||||||
|
}
|
||||||
|
|
|
@ -81,6 +81,7 @@ const (
|
||||||
|
|
||||||
var rpcHandlers = map[string]func(*Server, request.Params) (interface{}, *response.Error){
|
var rpcHandlers = map[string]func(*Server, request.Params) (interface{}, *response.Error){
|
||||||
"getaccountstate": (*Server).getAccountState,
|
"getaccountstate": (*Server).getAccountState,
|
||||||
|
"getalltransfertx": (*Server).getAllTransferTx,
|
||||||
"getapplicationlog": (*Server).getApplicationLog,
|
"getapplicationlog": (*Server).getApplicationLog,
|
||||||
"getassetstate": (*Server).getAssetState,
|
"getassetstate": (*Server).getAssetState,
|
||||||
"getbestblockhash": (*Server).getBestBlockHash,
|
"getbestblockhash": (*Server).getBestBlockHash,
|
||||||
|
@ -828,6 +829,194 @@ func (s *Server) getNEP5Transfers(ps request.Params) (interface{}, *response.Err
|
||||||
return bs, nil
|
return bs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) getAllTransferTx(ps request.Params) (interface{}, *response.Error) {
|
||||||
|
var respErr *response.Error
|
||||||
|
|
||||||
|
u, err := ps.Value(0).GetUint160FromAddressOrHex()
|
||||||
|
if err != nil {
|
||||||
|
return nil, response.ErrInvalidParams
|
||||||
|
}
|
||||||
|
|
||||||
|
start, end, limit, page, err := getTimestampsAndLimit(ps, 1)
|
||||||
|
if err != nil {
|
||||||
|
return nil, response.NewInvalidParamsError("", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
utxoCont = make(chan bool)
|
||||||
|
nep5Cont = make(chan bool)
|
||||||
|
utxoTrs = make(chan state.Transfer)
|
||||||
|
nep5Trs = make(chan state.NEP5Transfer)
|
||||||
|
)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
tr := new(state.Transfer)
|
||||||
|
_ = s.chain.ForEachTransfer(u, tr, func() (bool, error) {
|
||||||
|
var cont bool
|
||||||
|
|
||||||
|
// Iterating from newest to oldest, not yet reached required
|
||||||
|
// time frame, continue looping.
|
||||||
|
if tr.Timestamp > end {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
// Iterating from newest to oldest, moved past required
|
||||||
|
// time frame, stop looping.
|
||||||
|
if tr.Timestamp < start {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
utxoTrs <- *tr
|
||||||
|
cont = <-utxoCont
|
||||||
|
return cont, nil
|
||||||
|
})
|
||||||
|
close(utxoTrs)
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
tr := new(state.NEP5Transfer)
|
||||||
|
_ = s.chain.ForEachNEP5Transfer(u, tr, func() (bool, error) {
|
||||||
|
var cont bool
|
||||||
|
|
||||||
|
// Iterating from newest to oldest, not yet reached required
|
||||||
|
// time frame, continue looping.
|
||||||
|
if tr.Timestamp > end {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
// Iterating from newest to oldest, moved past required
|
||||||
|
// time frame, stop looping.
|
||||||
|
if tr.Timestamp < start {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
nep5Trs <- *tr
|
||||||
|
cont = <-nep5Cont
|
||||||
|
return cont, nil
|
||||||
|
})
|
||||||
|
close(nep5Trs)
|
||||||
|
}()
|
||||||
|
|
||||||
|
var (
|
||||||
|
res = make([]result.TransferTx, 0, limit)
|
||||||
|
frameCount int
|
||||||
|
utxoLast state.Transfer
|
||||||
|
nep5Last state.NEP5Transfer
|
||||||
|
haveUtxo, haveNep5 bool
|
||||||
|
)
|
||||||
|
|
||||||
|
utxoLast, haveUtxo = <-utxoTrs
|
||||||
|
if haveUtxo {
|
||||||
|
utxoCont <- true
|
||||||
|
}
|
||||||
|
nep5Last, haveNep5 = <-nep5Trs
|
||||||
|
if haveNep5 {
|
||||||
|
nep5Cont <- true
|
||||||
|
}
|
||||||
|
for len(res) < limit {
|
||||||
|
if !haveUtxo && !haveNep5 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
var isNep5 = haveNep5 && (!haveUtxo || (nep5Last.Timestamp > utxoLast.Timestamp))
|
||||||
|
var transfer result.TransferTx
|
||||||
|
if isNep5 {
|
||||||
|
transfer.TxID = nep5Last.Tx
|
||||||
|
transfer.Timestamp = nep5Last.Timestamp
|
||||||
|
transfer.Index = nep5Last.Block
|
||||||
|
} else {
|
||||||
|
transfer.TxID = utxoLast.Tx
|
||||||
|
transfer.Timestamp = utxoLast.Timestamp
|
||||||
|
transfer.Index = utxoLast.Block
|
||||||
|
}
|
||||||
|
frameCount++
|
||||||
|
// Using limits, not yet reached required page. But still need
|
||||||
|
// to drain inputs for this tx.
|
||||||
|
skipTx := page*limit >= frameCount
|
||||||
|
|
||||||
|
if !skipTx {
|
||||||
|
tx, _, err := s.chain.GetTransaction(transfer.TxID)
|
||||||
|
if err != nil {
|
||||||
|
respErr = response.NewInternalServerError("invalid NEP5 transfer log", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
transfer.NetworkFee = int64(s.chain.NetworkFee(tx))
|
||||||
|
transfer.SystemFee = int64(s.chain.SystemFee(tx))
|
||||||
|
|
||||||
|
inouts, err := s.chain.References(tx)
|
||||||
|
if err != nil {
|
||||||
|
respErr = response.NewInternalServerError("invalid tx", err)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
nep5Last, haveNep5 = <-nep5Trs
|
||||||
|
if haveNep5 {
|
||||||
|
nep5Cont <- true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip UTXO events, we've already got them from inputs and outputs.
|
||||||
|
for haveUtxo && utxoLast.Tx.Equals(transfer.TxID) {
|
||||||
|
utxoLast, haveUtxo = <-utxoTrs
|
||||||
|
if haveUtxo {
|
||||||
|
utxoCont <- true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !skipTx {
|
||||||
|
res = append(res, transfer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if haveUtxo {
|
||||||
|
_, ok := <-utxoTrs
|
||||||
|
if ok {
|
||||||
|
utxoCont <- false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if haveNep5 {
|
||||||
|
_, ok := <-nep5Trs
|
||||||
|
if ok {
|
||||||
|
nep5Cont <- false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if respErr != nil {
|
||||||
|
return nil, respErr
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) getMinimumNetworkFee(ps request.Params) (interface{}, *response.Error) {
|
func (s *Server) getMinimumNetworkFee(ps request.Params) (interface{}, *response.Error) {
|
||||||
return s.chain.GetConfig().MinimumNetworkFee, nil
|
return s.chain.GetConfig().MinimumNetworkFee, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1223,6 +1223,108 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) []
|
||||||
require.NoError(t, json.Unmarshal(res, actual))
|
require.NoError(t, json.Unmarshal(res, actual))
|
||||||
checkNep5TransfersAux(t, e, actual, true)
|
checkNep5TransfersAux(t, e, actual, true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("getalltransfertx", func(t *testing.T) {
|
||||||
|
testGetTxs := func(t *testing.T, asset string, start, stop, limit, page int, present []util.Uint256) {
|
||||||
|
ps := []string{`"AKkkumHbBipZ46UMZJoFynJMXzSRnBvKcs"`}
|
||||||
|
ps = append(ps, strconv.Itoa(start))
|
||||||
|
ps = append(ps, strconv.Itoa(stop))
|
||||||
|
if limit != 0 {
|
||||||
|
ps = append(ps, strconv.Itoa(limit))
|
||||||
|
}
|
||||||
|
if page != 0 {
|
||||||
|
ps = append(ps, strconv.Itoa(page))
|
||||||
|
}
|
||||||
|
p := strings.Join(ps, ", ")
|
||||||
|
rpc := fmt.Sprintf(`{"jsonrpc": "2.0", "id": 1, "method": "getalltransfertx", "params": [%s]}`, p)
|
||||||
|
body := doRPCCall(rpc, httpSrv.URL, t)
|
||||||
|
res := checkErrGetResult(t, body, false)
|
||||||
|
actualp := new([]result.TransferTx)
|
||||||
|
require.NoError(t, json.Unmarshal(res, actualp))
|
||||||
|
actual := *actualp
|
||||||
|
require.Equal(t, len(present), len(actual))
|
||||||
|
for _, id := range present {
|
||||||
|
var isThere bool
|
||||||
|
var ttx result.TransferTx
|
||||||
|
for i := range actual {
|
||||||
|
if id.Equals(actual[i].TxID) {
|
||||||
|
ttx = actual[i]
|
||||||
|
isThere = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require.True(t, isThere)
|
||||||
|
tx, h, err := e.chain.GetTransaction(id)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, h, ttx.Index)
|
||||||
|
require.Equal(t, int64(e.chain.SystemFee(tx)), ttx.SystemFee)
|
||||||
|
require.Equal(t, int64(e.chain.NetworkFee(tx)), ttx.NetworkFee)
|
||||||
|
require.Equal(t, len(tx.Inputs)+len(tx.Outputs), len(ttx.Elements))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b, err := e.chain.GetBlock(e.chain.GetHeaderHash(1))
|
||||||
|
require.NoError(t, err)
|
||||||
|
txMoveNeo := b.Transactions[1].Hash()
|
||||||
|
b, err = e.chain.GetBlock(e.chain.GetHeaderHash(202))
|
||||||
|
require.NoError(t, err)
|
||||||
|
txNeoRT := b.Transactions[1].Hash()
|
||||||
|
b, err = e.chain.GetBlock(e.chain.GetHeaderHash(203))
|
||||||
|
require.NoError(t, err)
|
||||||
|
txGasClaim := b.Transactions[1].Hash()
|
||||||
|
b, err = e.chain.GetBlock(e.chain.GetHeaderHash(204))
|
||||||
|
require.NoError(t, err)
|
||||||
|
txDeploy := b.Transactions[1].Hash()
|
||||||
|
ts204 := int(b.Timestamp)
|
||||||
|
b, err = e.chain.GetBlock(e.chain.GetHeaderHash(206))
|
||||||
|
require.NoError(t, err)
|
||||||
|
txNeoTo1 := b.Transactions[1].Hash()
|
||||||
|
b, err = e.chain.GetBlock(e.chain.GetHeaderHash(207))
|
||||||
|
require.NoError(t, err)
|
||||||
|
txNep5Tr := b.Transactions[2].Hash()
|
||||||
|
b, err = e.chain.GetBlock(e.chain.GetHeaderHash(208))
|
||||||
|
require.NoError(t, err)
|
||||||
|
txNep5To1 := b.Transactions[1].Hash()
|
||||||
|
ts208 := int(b.Timestamp)
|
||||||
|
b, err = e.chain.GetBlock(e.chain.GetHeaderHash(209))
|
||||||
|
require.NoError(t, err)
|
||||||
|
txMigrate := b.Transactions[1].Hash()
|
||||||
|
b, err = e.chain.GetBlock(e.chain.GetHeaderHash(210))
|
||||||
|
require.NoError(t, err)
|
||||||
|
txNep5To0 := b.Transactions[1].Hash()
|
||||||
|
lastTs := int(b.Timestamp)
|
||||||
|
t.Run("All", func(t *testing.T) {
|
||||||
|
testGetTxs(t, "", 0, lastTs, 0, 0, []util.Uint256{
|
||||||
|
txMoveNeo, txNeoRT, txGasClaim, txDeploy, txNeoTo1, txNep5Tr,
|
||||||
|
txNep5To1, txMigrate, txNep5To0,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
t.Run("last 3", func(t *testing.T) {
|
||||||
|
testGetTxs(t, "", 0, lastTs, 3, 0, []util.Uint256{
|
||||||
|
txNep5To1, txMigrate, txNep5To0,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
t.Run("3, page 1", func(t *testing.T) {
|
||||||
|
testGetTxs(t, "", 0, lastTs, 3, 1, []util.Uint256{
|
||||||
|
txDeploy, txNeoTo1, txNep5Tr,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
t.Run("3, page 2", func(t *testing.T) {
|
||||||
|
testGetTxs(t, "", 0, lastTs, 3, 2, []util.Uint256{
|
||||||
|
txMoveNeo, txNeoRT, txGasClaim,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
t.Run("3, page 3", func(t *testing.T) {
|
||||||
|
testGetTxs(t, "", 0, lastTs, 3, 3, []util.Uint256{})
|
||||||
|
})
|
||||||
|
t.Run("no dates", func(t *testing.T) {
|
||||||
|
testGetTxs(t, "", 0, 1000000, 0, 0, []util.Uint256{})
|
||||||
|
})
|
||||||
|
t.Run("204-208", func(t *testing.T) {
|
||||||
|
testGetTxs(t, "", ts204, ts208, 0, 0, []util.Uint256{
|
||||||
|
txDeploy, txNeoTo1, txNep5Tr, txNep5To1,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tc rpcTestCase) getResultPair(e *executor) (expected interface{}, res interface{}) {
|
func (tc rpcTestCase) getResultPair(e *executor) (expected interface{}, res interface{}) {
|
||||||
|
|
Loading…
Reference in a new issue