mirror of
https://github.com/nspcc-dev/neo-go.git
synced 2024-12-12 11:10:35 +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
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (c *Client) GetApplicationLog(hash util.Uint256) (*result.ApplicationLog, error) {
|
||||
var (
|
||||
|
|
|
@ -69,3 +69,24 @@ func (b *NEP5Balance) UnmarshalJSON(data []byte) error {
|
|||
b.LastUpdated = s.LastUpdated
|
||||
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){
|
||||
"getaccountstate": (*Server).getAccountState,
|
||||
"getalltransfertx": (*Server).getAllTransferTx,
|
||||
"getapplicationlog": (*Server).getApplicationLog,
|
||||
"getassetstate": (*Server).getAssetState,
|
||||
"getbestblockhash": (*Server).getBestBlockHash,
|
||||
|
@ -828,6 +829,194 @@ func (s *Server) getNEP5Transfers(ps request.Params) (interface{}, *response.Err
|
|||
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) {
|
||||
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))
|
||||
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{}) {
|
||||
|
|
Loading…
Reference in a new issue