rpc: add server-side NEP-11 tracking API

This commit is contained in:
Roman Khimov 2021-11-17 23:04:50 +03:00
parent b622c1934d
commit 7f48653e66
9 changed files with 483 additions and 93 deletions

View file

@ -134,6 +134,7 @@ RPC:
MaxGasInvoke: 50 MaxGasInvoke: 50
MaxIteratorResultItems: 100 MaxIteratorResultItems: 100
MaxFindResultItems: 100 MaxFindResultItems: 100
MaxNEP11Tokens: 100
Port: 10332 Port: 10332
TLSConfig: TLSConfig:
Address: "" Address: ""
@ -154,6 +155,8 @@ where:
`n`, only `n` iterations are returned and truncated is true, indicating that `n`, only `n` iterations are returned and truncated is true, indicating that
there is still data to be returned. there is still data to be returned.
- `MaxFindResultItems` - the maximum number of elements for `findstates` response. - `MaxFindResultItems` - the maximum number of elements for `findstates` response.
- `MaxNEP11Tokens` - limit for the number of tokens returned from
`getnep11balances` call.
- `Port` is an RPC server port it should be bound to. - `Port` is an RPC server port it should be bound to.
- `TLS` section configures TLS protocol. - `TLS` section configures TLS protocol.

View file

@ -48,6 +48,9 @@ which would yield the response:
| `getconnectioncount` | | `getconnectioncount` |
| `getcontractstate` | | `getcontractstate` |
| `getnativecontracts` | | `getnativecontracts` |
| `getnep11balances` |
| `getnep11properties` |
| `getnep11transfers` |
| `getnep17balances` | | `getnep17balances` |
| `getnep17transfers` | | `getnep17transfers` |
| `getnextblockvalidators` | | `getnextblockvalidators` |
@ -107,28 +110,32 @@ This method doesn't work for the Ledger contract, you can get data via regular
the native contract by its name (case-insensitive), unlike the C# node where the native contract by its name (case-insensitive), unlike the C# node where
it only possible for index or hash. it only possible for index or hash.
#### `getnep17balances` #### `getnep11balances` and `getnep17balances`
neo-go's implementation of `getnep11balances` and `getnep17balances` does not
perform tracking of NEP-11 and NEP-17 balances for each account as it is done
in the C# node. Instead, neo-go node maintains the list of standard-compliant
contracts, i.e. those contracts that have `NEP-11` or `NEP-17` declared in the
supported standards section of the manifest. Each time balances are queried,
neo-go node asks every NEP-11/NEP-17 contract for the account balance by
invoking `balanceOf` method with the corresponding args. Invocation GAS limit
is set to be 3 GAS. All non-zero balances are included in the RPC call result.
neo-go's implementation of `getnep17balances` does not perform tracking of NEP17 Thus, if token contract doesn't have proper standard declared in the list of
balances for each account as it is done in the C# node. Instead, neo-go node supported standards but emits compliant NEP-11/NEP-17 `Transfer`
maintains the list of NEP17-compliant contracts, i.e. those contracts that have notifications, the token balance won't be shown in the list of balances
`NEP-17` declared in the supported standards section of the manifest. Each time returned by the neo-go node (unlike the C# node behavior). However, transfer
`getnep17balances` is queried, neo-go node asks every NEP17 contract for the logs of such tokens are still available via respective `getnepXXtransfers` RPC
account balance by invoking `balanceOf` method with the corresponding args. calls.
Invocation GAS limit is set to be 3 GAS. All non-zero NEP17 balances are included
in the RPC call result.
Thus, if NEP17 token contract doesn't have `NEP-17` standard declared in the list
of supported standards but emits proper NEP17 `Transfer` notifications, the token
balance won't be shown in the list of NEP17 balances returned by the neo-go node
(unlike the C# node behavior). However, transfer logs of such token are still
available via `getnep17transfers` RPC call.
The behaviour of the `LastUpdatedBlock` tracking for archival nodes as far as for The behaviour of the `LastUpdatedBlock` tracking for archival nodes as far as for
governing token balances matches the C# node's one. For non-archival nodes and governing token balances matches the C# node's one. For non-archival nodes and
other NEP17-compliant tokens if transfer's `LastUpdatedBlock` is lower than the other NEP-11/NEP-17 tokens if transfer's `LastUpdatedBlock` is lower than the
latest state synchronization point P the node working against, then latest state synchronization point P the node working against, then
`LastUpdatedBlock` equals P. `LastUpdatedBlock` equals P. For NEP-11 NFTs `LastUpdatedBlock` is equal for
all tokens of the same asset.
#### `getnep11transfers` and `getnep17transfers`
`transfernotifyindex` is not tracked by NeoGo, thus this field is always zero.
### Unsupported methods ### Unsupported methods
@ -166,12 +173,12 @@ burned).
This method can be used on P2P Notary enabled networks to submit new notary This method can be used on P2P Notary enabled networks to submit new notary
payloads to be relayed from RPC to P2P. payloads to be relayed from RPC to P2P.
#### Limits and paging for getnep17transfers #### Limits and paging for getnep11transfers and getnep17transfers
`getnep17transfers` RPC call never returns more than 1000 results for one `getnep11transfers` and `getnep17transfers` RPC calls never return more than
request (within specified time frame). You can pass your own limit via an 1000 results for one request (within specified time frame). You can pass your
additional parameter and then use paging to request the next batch of own limit via an additional parameter and then use paging to request the next
transfers. batch of transfers.
Example requesting 10 events for address NbTiM6h8r99kpRtb428XcsUk1TzKed2gTc Example requesting 10 events for address NbTiM6h8r99kpRtb428XcsUk1TzKed2gTc
within 0-1600094189000 timestamps: within 0-1600094189000 timestamps:

View file

@ -53,6 +53,7 @@ func LoadFile(configPath string) (Config, error) {
RPC: rpc.Config{ RPC: rpc.Config{
MaxIteratorResultItems: 100, MaxIteratorResultItems: 100,
MaxFindResultItems: 100, MaxFindResultItems: 100,
MaxNEP11Tokens: 100,
}, },
}, },
} }

View file

@ -1,36 +0,0 @@
package result
import (
"github.com/nspcc-dev/neo-go/pkg/util"
)
// NEP17Balances is a result for the getnep17balances RPC call.
type NEP17Balances struct {
Balances []NEP17Balance `json:"balance"`
Address string `json:"address"`
}
// NEP17Balance represents balance for the single token contract.
type NEP17Balance struct {
Asset util.Uint160 `json:"assethash"`
Amount string `json:"amount"`
LastUpdated uint32 `json:"lastupdatedblock"`
}
// NEP17Transfers is a result for the getnep17transfers RPC.
type NEP17Transfers struct {
Sent []NEP17Transfer `json:"sent"`
Received []NEP17Transfer `json:"received"`
Address string `json:"address"`
}
// NEP17Transfer represents single NEP17 transfer event.
type NEP17Transfer struct {
Timestamp uint64 `json:"timestamp"`
Asset util.Uint160 `json:"assethash"`
Address string `json:"transferaddress,omitempty"`
Amount string `json:"amount"`
Index uint32 `json:"blockindex"`
NotifyIndex uint32 `json:"transfernotifyindex"`
TxHash util.Uint256 `json:"txhash"`
}

View file

@ -0,0 +1,74 @@
package result
import (
"github.com/nspcc-dev/neo-go/pkg/util"
)
// NEP11Balances is a result for the getnep11balances RPC call.
type NEP11Balances struct {
Balances []NEP11AssetBalance `json:"balance"`
Address string `json:"address"`
}
// NEP11Balance is a structure holding balance of a NEP-11 asset.
type NEP11AssetBalance struct {
Asset util.Uint160 `json:"assethash"`
Tokens []NEP11TokenBalance `json:"tokens"`
}
// NEP11TokenBalance represents balance of a single NFT.
type NEP11TokenBalance struct {
ID string `json:"tokenid"`
Amount string `json:"amount"`
LastUpdated uint32 `json:"lastupdatedblock"`
}
// NEP17Balances is a result for the getnep17balances RPC call.
type NEP17Balances struct {
Balances []NEP17Balance `json:"balance"`
Address string `json:"address"`
}
// NEP17Balance represents balance for the single token contract.
type NEP17Balance struct {
Asset util.Uint160 `json:"assethash"`
Amount string `json:"amount"`
LastUpdated uint32 `json:"lastupdatedblock"`
}
// NEP11Transfers is a result for the getnep11transfers RPC.
type NEP11Transfers struct {
Sent []NEP11Transfer `json:"sent"`
Received []NEP11Transfer `json:"received"`
Address string `json:"address"`
}
// NEP11Transfer represents single NEP-11 transfer event.
type NEP11Transfer struct {
Timestamp uint64 `json:"timestamp"`
Asset util.Uint160 `json:"assethash"`
Address string `json:"transferaddress,omitempty"`
ID string `json:"tokenid"`
Amount string `json:"amount"`
Index uint32 `json:"blockindex"`
NotifyIndex uint32 `json:"transfernotifyindex"`
TxHash util.Uint256 `json:"txhash"`
}
// NEP17Transfers is a result for the getnep17transfers RPC.
type NEP17Transfers struct {
Sent []NEP17Transfer `json:"sent"`
Received []NEP17Transfer `json:"received"`
Address string `json:"address"`
}
// NEP17Transfer represents single NEP17 transfer event.
type NEP17Transfer struct {
Timestamp uint64 `json:"timestamp"`
Asset util.Uint160 `json:"assethash"`
Address string `json:"transferaddress,omitempty"`
Amount string `json:"amount"`
Index uint32 `json:"blockindex"`
NotifyIndex uint32 `json:"transfernotifyindex"`
TxHash util.Uint256 `json:"txhash"`
}

View file

@ -15,6 +15,7 @@ type (
MaxGasInvoke fixedn.Fixed8 `yaml:"MaxGasInvoke"` MaxGasInvoke fixedn.Fixed8 `yaml:"MaxGasInvoke"`
MaxIteratorResultItems int `yaml:"MaxIteratorResultItems"` MaxIteratorResultItems int `yaml:"MaxIteratorResultItems"`
MaxFindResultItems int `yaml:"MaxFindResultItems"` MaxFindResultItems int `yaml:"MaxFindResultItems"`
MaxNEP11Tokens int `yaml:"MaxNEP11Tokens"`
Port uint16 `yaml:"Port"` Port uint16 `yaml:"Port"`
TLSConfig TLSConfig `yaml:"TLSConfig"` TLSConfig TLSConfig `yaml:"TLSConfig"`
} }

View file

@ -5,6 +5,7 @@ import (
"context" "context"
"crypto/elliptic" "crypto/elliptic"
"encoding/binary" "encoding/binary"
"encoding/hex"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@ -22,10 +23,12 @@ import (
"github.com/nspcc-dev/neo-go/pkg/core/block" "github.com/nspcc-dev/neo-go/pkg/core/block"
"github.com/nspcc-dev/neo-go/pkg/core/blockchainer" "github.com/nspcc-dev/neo-go/pkg/core/blockchainer"
"github.com/nspcc-dev/neo-go/pkg/core/fee" "github.com/nspcc-dev/neo-go/pkg/core/fee"
"github.com/nspcc-dev/neo-go/pkg/core/interop/iterator"
"github.com/nspcc-dev/neo-go/pkg/core/mempoolevent" "github.com/nspcc-dev/neo-go/pkg/core/mempoolevent"
"github.com/nspcc-dev/neo-go/pkg/core/mpt" "github.com/nspcc-dev/neo-go/pkg/core/mpt"
"github.com/nspcc-dev/neo-go/pkg/core/native" "github.com/nspcc-dev/neo-go/pkg/core/native"
"github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/core/storage"
"github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/crypto/hash" "github.com/nspcc-dev/neo-go/pkg/crypto/hash"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/crypto/keys"
@ -115,6 +118,9 @@ var rpcHandlers = map[string]func(*Server, request.Params) (interface{}, *respon
"getconnectioncount": (*Server).getConnectionCount, "getconnectioncount": (*Server).getConnectionCount,
"getcontractstate": (*Server).getContractState, "getcontractstate": (*Server).getContractState,
"getnativecontracts": (*Server).getNativeContracts, "getnativecontracts": (*Server).getNativeContracts,
"getnep11balances": (*Server).getNEP11Balances,
"getnep11properties": (*Server).getNEP11Properties,
"getnep11transfers": (*Server).getNEP11Transfers,
"getnep17balances": (*Server).getNEP17Balances, "getnep17balances": (*Server).getNEP17Balances,
"getnep17transfers": (*Server).getNEP17Transfers, "getnep17transfers": (*Server).getNEP17Transfers,
"getpeers": (*Server).getPeers, "getpeers": (*Server).getPeers,
@ -153,6 +159,13 @@ var invalidBlockHeightError = func(index int, height int) *response.Error {
// doesn't set any Error function. // doesn't set any Error function.
var upgrader = websocket.Upgrader{} var upgrader = websocket.Upgrader{}
var knownNEP11Properties = map[string]bool{
"description": true,
"image": true,
"name": true,
"tokenURI": true,
}
// New creates a new Server struct. // New creates a new Server struct.
func New(chain blockchainer.Blockchainer, conf rpc.Config, coreServer *network.Server, func New(chain blockchainer.Blockchainer, conf rpc.Config, coreServer *network.Server,
orc *oracle.Oracle, log *zap.Logger) Server { orc *oracle.Oracle, log *zap.Logger) Server {
@ -651,6 +664,141 @@ func (s *Server) getApplicationLog(reqParams request.Params) (interface{}, *resp
return result.NewApplicationLog(hash, appExecResults, trig), nil return result.NewApplicationLog(hash, appExecResults, trig), nil
} }
func (s *Server) getNEP11Tokens(h util.Uint160, acc util.Uint160, bw *io.BufBinWriter) ([]stackitem.Item, error) {
item, finalize, err := s.invokeReadOnly(bw, h, "tokensOf", acc)
if err != nil {
return nil, err
}
defer finalize()
if (item.Type() == stackitem.InteropT) && iterator.IsIterator(item) {
vals, _ := iterator.Values(item, s.config.MaxNEP11Tokens)
return vals, nil
}
return nil, fmt.Errorf("invalid `tokensOf` result type %s", item.String())
}
func (s *Server) getNEP11Balances(ps request.Params) (interface{}, *response.Error) {
u, err := ps.Value(0).GetUint160FromAddressOrHex()
if err != nil {
return nil, response.ErrInvalidParams
}
bs := &result.NEP11Balances{
Address: address.Uint160ToString(u),
Balances: []result.NEP11AssetBalance{},
}
lastUpdated, err := s.chain.GetTokenLastUpdated(u)
if err != nil {
return nil, response.NewRPCError("Failed to get NEP11 last updated block", err.Error(), err)
}
var count int
stateSyncPoint := lastUpdated[math.MinInt32]
bw := io.NewBufBinWriter()
contract_loop:
for _, h := range s.chain.GetNEP11Contracts() {
toks, err := s.getNEP11Tokens(h, u, bw)
if err != nil {
continue
}
if len(toks) == 0 {
continue
}
cs := s.chain.GetContractState(h)
if cs == nil {
continue
}
isDivisible := (cs.Manifest.ABI.GetMethod("balanceOf", 2) != nil)
lub, ok := lastUpdated[cs.ID]
if !ok {
cfg := s.chain.GetConfig()
if !cfg.P2PStateExchangeExtensions && cfg.RemoveUntraceableBlocks {
return nil, response.NewInternalServerError(fmt.Sprintf("failed to get LastUpdatedBlock for balance of %s token", cs.Hash.StringLE()), nil)
}
lub = stateSyncPoint
}
bs.Balances = append(bs.Balances, result.NEP11AssetBalance{
Asset: h,
Tokens: make([]result.NEP11TokenBalance, 0, len(toks)),
})
curAsset := &bs.Balances[len(bs.Balances)-1]
for i := range toks {
id, err := toks[i].TryBytes()
if err != nil || len(id) > storage.MaxStorageKeyLen {
continue
}
var amount = "1"
if isDivisible {
balance, err := s.getTokenBalance(h, u, id, bw)
if err != nil {
continue
}
if balance.Sign() == 0 {
continue
}
amount = balance.String()
}
count++
curAsset.Tokens = append(curAsset.Tokens, result.NEP11TokenBalance{
ID: hex.EncodeToString(id),
Amount: amount,
LastUpdated: lub,
})
if count >= s.config.MaxNEP11Tokens {
break contract_loop
}
}
}
return bs, nil
}
func (s *Server) invokeNEP11Properties(h util.Uint160, id []byte, bw *io.BufBinWriter) ([]stackitem.MapElement, error) {
item, finalize, err := s.invokeReadOnly(bw, h, "properties", id)
if err != nil {
return nil, err
}
defer finalize()
if item.Type() != stackitem.MapT {
return nil, fmt.Errorf("invalid `properties` result type %s", item.String())
}
return item.Value().([]stackitem.MapElement), nil
}
func (s *Server) getNEP11Properties(ps request.Params) (interface{}, *response.Error) {
asset, err := ps.Value(0).GetUint160FromAddressOrHex()
if err != nil {
return nil, response.ErrInvalidParams
}
token, err := ps.Value(1).GetBytesHex()
if err != nil {
return nil, response.ErrInvalidParams
}
props, err := s.invokeNEP11Properties(asset, token, nil)
if err != nil {
return nil, response.NewRPCError("failed to get NEP-11 properties", err.Error(), err)
}
res := make(map[string]interface{})
for _, kv := range props {
key, err := kv.Key.TryBytes()
if err != nil {
continue
}
var val interface{}
if knownNEP11Properties[string(key)] || kv.Value.Type() != stackitem.AnyT {
v, err := kv.Value.TryBytes()
if err != nil {
continue
}
if knownNEP11Properties[string(key)] {
val = string(v)
} else {
val = v
}
}
res[string(key)] = val
}
return res, nil
}
func (s *Server) getNEP17Balances(ps request.Params) (interface{}, *response.Error) { func (s *Server) getNEP17Balances(ps request.Params) (interface{}, *response.Error) {
u, err := ps.Value(0).GetUint160FromAddressOrHex() u, err := ps.Value(0).GetUint160FromAddressOrHex()
if err != nil { if err != nil {
@ -668,7 +816,7 @@ func (s *Server) getNEP17Balances(ps request.Params) (interface{}, *response.Err
stateSyncPoint := lastUpdated[math.MinInt32] stateSyncPoint := lastUpdated[math.MinInt32]
bw := io.NewBufBinWriter() bw := io.NewBufBinWriter()
for _, h := range s.chain.GetNEP17Contracts() { for _, h := range s.chain.GetNEP17Contracts() {
balance, err := s.getNEP17Balance(h, u, bw) balance, err := s.getTokenBalance(h, u, nil, bw)
if err != nil { if err != nil {
continue continue
} }
@ -696,30 +844,53 @@ func (s *Server) getNEP17Balances(ps request.Params) (interface{}, *response.Err
return bs, nil return bs, nil
} }
func (s *Server) getNEP17Balance(h util.Uint160, acc util.Uint160, bw *io.BufBinWriter) (*big.Int, error) { func (s *Server) invokeReadOnly(bw *io.BufBinWriter, h util.Uint160, method string, params ...interface{}) (stackitem.Item, func(), error) {
if bw == nil { if bw == nil {
bw = io.NewBufBinWriter() bw = io.NewBufBinWriter()
} else { } else {
bw.Reset() bw.Reset()
} }
emit.AppCall(bw.BinWriter, h, "balanceOf", callflag.ReadStates, acc) emit.AppCall(bw.BinWriter, h, method, callflag.ReadStates|callflag.AllowCall, params...)
if bw.Err != nil { if bw.Err != nil {
return nil, fmt.Errorf("failed to create `balanceOf` invocation script: %w", bw.Err) return nil, nil, fmt.Errorf("failed to create `%s` invocation script: %w", method, bw.Err)
} }
script := bw.Bytes() script := bw.Bytes()
tx := &transaction.Transaction{Script: script} tx := &transaction.Transaction{Script: script}
v, finalize := s.chain.GetTestVM(trigger.Application, tx, nil) b, err := s.getFakeNextBlock()
defer finalize() if err != nil {
return nil, nil, err
}
v, finalize := s.chain.GetTestVM(trigger.Application, tx, b)
v.GasLimit = core.HeaderVerificationGasLimit v.GasLimit = core.HeaderVerificationGasLimit
v.LoadScriptWithFlags(script, callflag.All) v.LoadScriptWithFlags(script, callflag.All)
err := v.Run() err = v.Run()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to run `balanceOf` for %s: %w", h.StringLE(), err) finalize()
return nil, nil, fmt.Errorf("failed to run `%s` for %s: %w", method, h.StringLE(), err)
} }
if v.Estack().Len() != 1 { if v.Estack().Len() != 1 {
return nil, fmt.Errorf("invalid `balanceOf` return values count: expected 1, got %d", v.Estack().Len()) finalize()
return nil, nil, fmt.Errorf("invalid `%s` return values count: expected 1, got %d", method, v.Estack().Len())
} }
res, err := v.Estack().Pop().Item().TryInteger() return v.Estack().Pop().Item(), finalize, nil
}
func (s *Server) getTokenBalance(h util.Uint160, acc util.Uint160, id []byte, bw *io.BufBinWriter) (*big.Int, error) {
var (
item stackitem.Item
finalize func()
err error
)
if id == nil { // NEP-17 and NEP-11 generic.
item, finalize, err = s.invokeReadOnly(bw, h, "balanceOf", acc)
} else { // NEP-11 divisible.
item, finalize, err = s.invokeReadOnly(bw, h, "balanceOf", acc, id)
}
if err != nil {
return nil, err
}
finalize()
res, err := item.TryInteger()
if err != nil { if err != nil {
return nil, fmt.Errorf("unexpected `balanceOf` result type: %w", err) return nil, fmt.Errorf("unexpected `balanceOf` result type: %w", err)
} }
@ -776,7 +947,15 @@ func getTimestampsAndLimit(ps request.Params, index int) (uint64, uint64, int, i
return start, end, limit, page, nil return start, end, limit, page, nil
} }
func (s *Server) getNEP11Transfers(ps request.Params) (interface{}, *response.Error) {
return s.getTokenTransfers(ps, true)
}
func (s *Server) getNEP17Transfers(ps request.Params) (interface{}, *response.Error) { func (s *Server) getNEP17Transfers(ps request.Params) (interface{}, *response.Error) {
return s.getTokenTransfers(ps, false)
}
func (s *Server) getTokenTransfers(ps request.Params, isNEP11 bool) (interface{}, *response.Error) {
u, err := ps.Value(0).GetUint160FromAddressOrHex() u, err := ps.Value(0).GetUint160FromAddressOrHex()
if err != nil { if err != nil {
return nil, response.ErrInvalidParams return nil, response.ErrInvalidParams
@ -787,33 +966,37 @@ func (s *Server) getNEP17Transfers(ps request.Params) (interface{}, *response.Er
return nil, response.NewInvalidParamsError(err.Error(), err) return nil, response.NewInvalidParamsError(err.Error(), err)
} }
bs := &result.NEP17Transfers{ bs := &tokenTransfers{
Address: address.Uint160ToString(u), Address: address.Uint160ToString(u),
Received: []result.NEP17Transfer{}, Received: []interface{}{},
Sent: []result.NEP17Transfer{}, Sent: []interface{}{},
} }
cache := make(map[int32]util.Uint160) cache := make(map[int32]util.Uint160)
var resCount, frameCount int var resCount, frameCount int
err = s.chain.ForEachNEP17Transfer(u, func(tr *state.NEP17Transfer) (bool, error) { // handleTransfer returns items to be added into received and sent arrays
// along with a continue flag and error.
var handleTransfer = func(tr *state.NEP17Transfer) (*result.NEP17Transfer, *result.NEP17Transfer, bool, error) {
var received, sent *result.NEP17Transfer
// Iterating from newest to oldest, not yet reached required // Iterating from newest to oldest, not yet reached required
// time frame, continue looping. // time frame, continue looping.
if tr.Timestamp > end { if tr.Timestamp > end {
return true, nil return nil, nil, true, nil
} }
// Iterating from newest to oldest, moved past required // Iterating from newest to oldest, moved past required
// time frame, stop looping. // time frame, stop looping.
if tr.Timestamp < start { if tr.Timestamp < start {
return false, nil return nil, nil, false, nil
} }
frameCount++ frameCount++
// Using limits, not yet reached required page. // Using limits, not yet reached required page.
if limit != 0 && page*limit >= frameCount { if limit != 0 && page*limit >= frameCount {
return true, nil return nil, nil, true, nil
} }
h, err := s.getHash(tr.Asset, cache) h, err := s.getHash(tr.Asset, cache)
if err != nil { if err != nil {
return false, err return nil, nil, false, err
} }
transfer := result.NEP17Transfer{ transfer := result.NEP17Transfer{
@ -827,24 +1010,51 @@ func (s *Server) getNEP17Transfers(ps request.Params) (interface{}, *response.Er
if !tr.From.Equals(util.Uint160{}) { if !tr.From.Equals(util.Uint160{}) {
transfer.Address = address.Uint160ToString(tr.From) transfer.Address = address.Uint160ToString(tr.From)
} }
bs.Received = append(bs.Received, transfer) received = &result.NEP17Transfer{}
*received = transfer // Make a copy, transfer is to be modified below.
} else { } else {
transfer.Amount = new(big.Int).Neg(&tr.Amount).String() transfer.Amount = new(big.Int).Neg(&tr.Amount).String()
if !tr.To.Equals(util.Uint160{}) { if !tr.To.Equals(util.Uint160{}) {
transfer.Address = address.Uint160ToString(tr.To) transfer.Address = address.Uint160ToString(tr.To)
} }
bs.Sent = append(bs.Sent, transfer) sent = &result.NEP17Transfer{}
*sent = transfer
} }
resCount++ resCount++
// Using limits, reached limit. // Check limits for continue flag.
if limit != 0 && resCount >= limit { return received, sent, !(limit != 0 && resCount >= limit), nil
return false, nil }
} if !isNEP11 {
return true, nil err = s.chain.ForEachNEP17Transfer(u, func(tr *state.NEP17Transfer) (bool, error) {
}) r, s, res, err := handleTransfer(tr)
if err == nil {
if r != nil {
bs.Received = append(bs.Received, r)
}
if s != nil {
bs.Sent = append(bs.Sent, s)
}
}
return res, err
})
} else {
err = s.chain.ForEachNEP11Transfer(u, func(tr *state.NEP11Transfer) (bool, error) {
r, s, res, err := handleTransfer(&tr.NEP17Transfer)
if err == nil {
id := hex.EncodeToString(tr.ID)
if r != nil {
bs.Received = append(bs.Received, nep17TransferToNEP11(r, id))
}
if s != nil {
bs.Sent = append(bs.Sent, nep17TransferToNEP11(s, id))
}
}
return res, err
})
}
if err != nil { if err != nil {
return nil, response.NewInternalServerError("invalid NEP17 transfer log", err) return nil, response.NewInternalServerError("invalid transfer log", err)
} }
return bs, nil return bs, nil
} }
@ -1444,12 +1654,7 @@ func (s *Server) invokeContractVerify(reqParams request.Params) (interface{}, *r
return s.runScriptInVM(trigger.Verification, invocationScript, scriptHash, tx) return s.runScriptInVM(trigger.Verification, invocationScript, scriptHash, tx)
} }
// runScriptInVM runs given script in a new test VM and returns the invocation func (s *Server) getFakeNextBlock() (*block.Block, error) {
// result. The script is either a simple script in case of `application` trigger
// witness invocation script in case of `verification` trigger (it pushes `verify`
// arguments on stack before verification). In case of contract verification
// contractScriptHash should be specified.
func (s *Server) runScriptInVM(t trigger.Type, script []byte, contractScriptHash util.Uint160, tx *transaction.Transaction) (*result.Invoke, *response.Error) {
// When transferring funds, script execution does no auto GAS claim, // When transferring funds, script execution does no auto GAS claim,
// because it depends on persisting tx height. // because it depends on persisting tx height.
// This is why we provide block here. // This is why we provide block here.
@ -1457,10 +1662,22 @@ func (s *Server) runScriptInVM(t trigger.Type, script []byte, contractScriptHash
b.Index = s.chain.BlockHeight() + 1 b.Index = s.chain.BlockHeight() + 1
hdr, err := s.chain.GetHeader(s.chain.GetHeaderHash(int(s.chain.BlockHeight()))) hdr, err := s.chain.GetHeader(s.chain.GetHeaderHash(int(s.chain.BlockHeight())))
if err != nil { if err != nil {
return nil, response.NewInternalServerError("can't get last block", err) return nil, err
} }
b.Timestamp = hdr.Timestamp + uint64(s.chain.GetConfig().SecondsPerBlock*int(time.Second/time.Millisecond)) b.Timestamp = hdr.Timestamp + uint64(s.chain.GetConfig().SecondsPerBlock*int(time.Second/time.Millisecond))
return b, nil
}
// runScriptInVM runs given script in a new test VM and returns the invocation
// result. The script is either a simple script in case of `application` trigger
// witness invocation script in case of `verification` trigger (it pushes `verify`
// arguments on stack before verification). In case of contract verification
// contractScriptHash should be specified.
func (s *Server) runScriptInVM(t trigger.Type, script []byte, contractScriptHash util.Uint160, tx *transaction.Transaction) (*result.Invoke, *response.Error) {
b, err := s.getFakeNextBlock()
if err != nil {
return nil, response.NewInternalServerError("can't create fake block", err)
}
vm, finalize := s.chain.GetTestVM(t, tx, b) vm, finalize := s.chain.GetTestVM(t, tx, b)
vm.GasLimit = int64(s.config.MaxGasInvoke) vm.GasLimit = int64(s.config.MaxGasInvoke)
if t == trigger.Verification { if t == trigger.Verification {

View file

@ -68,6 +68,20 @@ const invokescriptContractAVM = "VwIADBQBDAMOBQYMDQIODw0DDgcJAAAAANswcGhB+CfsjCG
const nameServiceContractHash = "3a602b3e7cfd760850bfac44f4a9bb0ebad3e2dc" const nameServiceContractHash = "3a602b3e7cfd760850bfac44f4a9bb0ebad3e2dc"
var NNSHash = util.Uint160{0xdc, 0xe2, 0xd3, 0xba, 0x0e, 0xbb, 0xa9, 0xf4, 0x44, 0xac, 0xbf, 0x50, 0x08, 0x76, 0xfd, 0x7c, 0x3e, 0x2b, 0x60, 0x3a}
var nep11Reg = &result.NEP11Balances{
Address: "Nhfg3TbpwogLvDGVvAvqyThbsHgoSUKwtn",
Balances: []result.NEP11AssetBalance{{
Asset: NNSHash,
Tokens: []result.NEP11TokenBalance{{
ID: "6e656f2e636f6d",
Amount: "1",
LastUpdated: 14,
}},
}},
}
var rpcTestCases = map[string][]rpcTestCase{ var rpcTestCases = map[string][]rpcTestCase{
"getapplicationlog": { "getapplicationlog": {
{ {
@ -213,7 +227,89 @@ var rpcTestCases = map[string][]rpcTestCase{
fail: true, fail: true,
}, },
}, },
"getnep11balances": {
{
name: "no params",
params: `[]`,
fail: true,
},
{
name: "invalid address",
params: `["notahex"]`,
fail: true,
},
{
name: "positive",
params: `["` + testchain.PrivateKeyByID(0).GetScriptHash().StringLE() + `"]`,
result: func(e *executor) interface{} { return nep11Reg },
},
{
name: "positive_address",
params: `["` + address.Uint160ToString(testchain.PrivateKeyByID(0).GetScriptHash()) + `"]`,
result: func(e *executor) interface{} { return nep11Reg },
},
},
"getnep11properties": {
{
name: "no params",
params: `[]`,
fail: true,
},
{
name: "invalid address",
params: `["notahex"]`,
fail: true,
},
{
name: "no token",
params: `["` + NNSHash.StringLE() + `"]`,
fail: true,
},
{
name: "bad token",
params: `["` + NNSHash.StringLE() + `", "abcdef"]`,
fail: true,
},
{
name: "positive",
params: `["` + NNSHash.StringLE() + `", "6e656f2e636f6d"]`,
result: func(e *executor) interface{} {
return &map[string]interface{}{
"name": "neo.com",
"expiration": "bhORxoMB",
}
},
},
},
"getnep11transfers": {
{
name: "no params",
params: `[]`,
fail: true,
},
{
name: "invalid address",
params: `["notahex"]`,
fail: true,
},
{
name: "invalid timestamp",
params: `["` + testchain.PrivateKeyByID(0).Address() + `", "notanumber"]`,
fail: true,
},
{
name: "invalid stop timestamp",
params: `["` + testchain.PrivateKeyByID(0).Address() + `", "1", "blah"]`,
fail: true,
},
{
name: "positive",
params: `["` + testchain.PrivateKeyByID(0).Address() + `", 0]`,
result: func(e *executor) interface{} {
return &result.NEP11Transfers{Sent: []result.NEP11Transfer{}, Received: []result.NEP11Transfer{{Timestamp: 0x17c6edfe76e, Asset: util.Uint160{0xdc, 0xe2, 0xd3, 0xba, 0xe, 0xbb, 0xa9, 0xf4, 0x44, 0xac, 0xbf, 0x50, 0x8, 0x76, 0xfd, 0x7c, 0x3e, 0x2b, 0x60, 0x3a}, Address: "", ID: "6e656f2e636f6d", Amount: "1", Index: 0xe, NotifyIndex: 0x0, TxHash: util.Uint256{0x5b, 0x5a, 0x5b, 0xae, 0xf2, 0xc5, 0x63, 0x8a, 0x2e, 0xcc, 0x77, 0x27, 0xd9, 0x6b, 0xb9, 0xda, 0x3a, 0x7f, 0x30, 0xaa, 0xcf, 0xda, 0x7f, 0x8a, 0x10, 0xd3, 0x23, 0xbf, 0xd, 0x1f, 0x28, 0x69}}}, Address: "Nhfg3TbpwogLvDGVvAvqyThbsHgoSUKwtn"}
},
},
},
"getnep17balances": { "getnep17balances": {
{ {
name: "no params", name: "no params",

27
pkg/rpc/server/tokens.go Normal file
View file

@ -0,0 +1,27 @@
package server
import (
"github.com/nspcc-dev/neo-go/pkg/rpc/response/result"
)
// tokenTransfers is a generic type used to represent NEP-11 and NEP-17 transfers.
type tokenTransfers struct {
Sent []interface{} `json:"sent"`
Received []interface{} `json:"received"`
Address string `json:"address"`
}
// nep17TransferToNEP11 adds an ID to provided NEP-17 transfer and returns a new
// NEP-11 structure.
func nep17TransferToNEP11(t17 *result.NEP17Transfer, id string) result.NEP11Transfer {
return result.NEP11Transfer{
Timestamp: t17.Timestamp,
Asset: t17.Asset,
Address: t17.Address,
ID: id,
Amount: t17.Amount,
Index: t17.Index,
NotifyIndex: t17.NotifyIndex,
TxHash: t17.TxHash,
}
}