Merge pull request #2093 from nspcc-dev/states-exchange/drop-nep17-balance-state

core: implement dynamic NEP17 balances tracking
This commit is contained in:
Roman Khimov 2021-07-29 19:08:42 +03:00 committed by GitHub
commit 06c3dda5d1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 303 additions and 144 deletions

View file

@ -97,6 +97,25 @@ it only works for native contracts.
This method doesn't work for the Ledger contract, you can get data via regular
`getblock` and `getrawtransaction` calls.
#### `getnep17balances`
neo-go's implementation of `getnep17balances` does not perform tracking of NEP17
balances for each account as it is done in the C# node. Instead, neo-go node
maintains the list of NEP17-compliant contracts, i.e. those contracts that have
`NEP-17` declared in the supported standards section of the manifest. Each time
`getnep17balances` is queried, neo-go node asks every NEP17 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 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 matches the C# node's one.
### Unsupported methods
Methods listed down below are not going to be supported for various reasons

View file

@ -254,13 +254,18 @@ func (chain *FakeChain) GetNextBlockValidators() ([]*keys.PublicKey, error) {
panic("TODO")
}
// ForEachNEP17Transfer implements Blockchainer interface.
func (chain *FakeChain) ForEachNEP17Transfer(util.Uint160, func(*state.NEP17Transfer) (bool, error)) error {
// GetNEP17Contracts implements Blockchainer interface.
func (chain *FakeChain) GetNEP17Contracts() []util.Uint160 {
panic("TODO")
}
// GetNEP17Balances implements Blockchainer interface.
func (chain *FakeChain) GetNEP17Balances(util.Uint160) *state.NEP17Balances {
// GetNEP17LastUpdated implements Blockchainer interface.
func (chain *FakeChain) GetNEP17LastUpdated(acc util.Uint160) (map[int32]uint32, error) {
panic("TODO")
}
// ForEachNEP17Transfer implements Blockchainer interface.
func (chain *FakeChain) ForEachNEP17Transfer(util.Uint160, func(*state.NEP17Transfer) (bool, error)) error {
panic("TODO")
}

View file

@ -43,7 +43,7 @@ import (
// Tuning parameters.
const (
headerBatchCount = 2000
version = "0.1.0"
version = "0.1.1"
defaultInitialGAS = 52000000_00000000
defaultMemPoolSize = 50000
@ -52,7 +52,8 @@ const (
defaultMaxBlockSystemFee = 900000000000
defaultMaxTraceableBlocks = 2102400 // 1 year of 15s blocks
defaultMaxTransactionsPerBlock = 512
headerVerificationGasLimit = 3_00000000 // 3 GAS
// HeaderVerificationGasLimit is the maximum amount of GAS for block header verification.
HeaderVerificationGasLimit = 3_00000000 // 3 GAS
)
var (
@ -989,14 +990,11 @@ func (bc *Blockchain) processNEP17Transfer(cache *dao.Cached, h util.Uint256, b
Tx: h,
}
if !fromAddr.Equals(util.Uint160{}) {
balances, err := cache.GetNEP17Balances(fromAddr)
balances, err := cache.GetNEP17TransferInfo(fromAddr)
if err != nil {
return
}
bs := balances.Trackers[id]
bs.Balance = *new(big.Int).Sub(&bs.Balance, amount)
bs.LastUpdatedBlock = b.Index
balances.Trackers[id] = bs
balances.LastUpdated[id] = b.Index
transfer.Amount = *new(big.Int).Sub(&transfer.Amount, amount)
balances.NewBatch, err = cache.AppendNEP17Transfer(fromAddr,
balances.NextTransferBatch, balances.NewBatch, transfer)
@ -1006,19 +1004,16 @@ func (bc *Blockchain) processNEP17Transfer(cache *dao.Cached, h util.Uint256, b
if balances.NewBatch {
balances.NextTransferBatch++
}
if err := cache.PutNEP17Balances(fromAddr, balances); err != nil {
if err := cache.PutNEP17TransferInfo(fromAddr, balances); err != nil {
return
}
}
if !toAddr.Equals(util.Uint160{}) {
balances, err := cache.GetNEP17Balances(toAddr)
balances, err := cache.GetNEP17TransferInfo(toAddr)
if err != nil {
return
}
bs := balances.Trackers[id]
bs.Balance = *new(big.Int).Add(&bs.Balance, amount)
bs.LastUpdatedBlock = b.Index
balances.Trackers[id] = bs
balances.LastUpdated[id] = b.Index
transfer.Amount = *amount
balances.NewBatch, err = cache.AppendNEP17Transfer(toAddr,
@ -1029,7 +1024,7 @@ func (bc *Blockchain) processNEP17Transfer(cache *dao.Cached, h util.Uint256, b
if balances.NewBatch {
balances.NextTransferBatch++
}
if err := cache.PutNEP17Balances(toAddr, balances); err != nil {
if err := cache.PutNEP17TransferInfo(toAddr, balances); err != nil {
return
}
}
@ -1037,7 +1032,7 @@ func (bc *Blockchain) processNEP17Transfer(cache *dao.Cached, h util.Uint256, b
// ForEachNEP17Transfer executes f for each nep17 transfer in log.
func (bc *Blockchain) ForEachNEP17Transfer(acc util.Uint160, f func(*state.NEP17Transfer) (bool, error)) error {
balances, err := bc.dao.GetNEP17Balances(acc)
balances, err := bc.dao.GetNEP17TransferInfo(acc)
if err != nil {
return nil
}
@ -1057,34 +1052,34 @@ func (bc *Blockchain) ForEachNEP17Transfer(acc util.Uint160, f func(*state.NEP17
return nil
}
// GetNEP17Balances returns NEP17 balances for the acc.
func (bc *Blockchain) GetNEP17Balances(acc util.Uint160) *state.NEP17Balances {
bs, err := bc.dao.GetNEP17Balances(acc)
// GetNEP17Contracts returns the list of deployed NEP17 contracts.
func (bc *Blockchain) GetNEP17Contracts() []util.Uint160 {
return bc.contracts.Management.GetNEP17Contracts()
}
// GetNEP17LastUpdated returns a set of contract ids with the corresponding last updated
// block indexes.
func (bc *Blockchain) GetNEP17LastUpdated(acc util.Uint160) (map[int32]uint32, error) {
info, err := bc.dao.GetNEP17TransferInfo(acc)
if err != nil {
return nil
return nil, err
}
return bs
return info.LastUpdated, nil
}
// GetUtilityTokenBalance returns utility token (GAS) balance for the acc.
func (bc *Blockchain) GetUtilityTokenBalance(acc util.Uint160) *big.Int {
bs, err := bc.dao.GetNEP17Balances(acc)
if err != nil {
bs := bc.contracts.GAS.BalanceOf(bc.dao, acc)
if bs == nil {
return big.NewInt(0)
}
balance := bs.Trackers[bc.contracts.GAS.ID].Balance
return &balance
return bs
}
// GetGoverningTokenBalance returns governing token (NEO) balance and the height
// of the last balance change for the account.
func (bc *Blockchain) GetGoverningTokenBalance(acc util.Uint160) (*big.Int, uint32) {
bs, err := bc.dao.GetNEP17Balances(acc)
if err != nil {
return big.NewInt(0), 0
}
neo := bs.Trackers[bc.contracts.NEO.ID]
return &neo.Balance, neo.LastUpdatedBlock
return bc.contracts.NEO.BalanceOf(bc.dao, acc)
}
// GetNotaryBalance returns Notary deposit amount for the specified account.
@ -1873,7 +1868,7 @@ func (bc *Blockchain) verifyHeaderWitnesses(currHeader, prevHeader *block.Header
} else {
hash = prevHeader.NextConsensus
}
return bc.VerifyWitness(hash, currHeader, &currHeader.Script, headerVerificationGasLimit)
return bc.VerifyWitness(hash, currHeader, &currHeader.Script, HeaderVerificationGasLimit)
}
// GoverningTokenHash returns the governing token (NEO) native contract hash.

View file

@ -47,7 +47,8 @@ type Blockchainer interface {
GetNativeContractScriptHash(string) (util.Uint160, error)
GetNatives() []state.NativeContract
GetNextBlockValidators() ([]*keys.PublicKey, error)
GetNEP17Balances(util.Uint160) *state.NEP17Balances
GetNEP17Contracts() []util.Uint160
GetNEP17LastUpdated(acc util.Uint160) (map[int32]uint32, error)
GetNotaryContractScriptHash() util.Uint160
GetNotaryBalance(acc util.Uint160) *big.Int
GetPolicer() Policer

View file

@ -13,7 +13,7 @@ import (
// objects in the storeBlock().
type Cached struct {
DAO
balances map[util.Uint160]*state.NEP17Balances
balances map[util.Uint160]*state.NEP17TransferInfo
transfers map[util.Uint160]map[uint32]*state.NEP17TransferLog
dropNEP17Cache bool
@ -21,21 +21,21 @@ type Cached struct {
// NewCached returns new Cached wrapping around given backing store.
func NewCached(d DAO) *Cached {
balances := make(map[util.Uint160]*state.NEP17Balances)
balances := make(map[util.Uint160]*state.NEP17TransferInfo)
transfers := make(map[util.Uint160]map[uint32]*state.NEP17TransferLog)
return &Cached{d.GetWrapped(), balances, transfers, false}
}
// GetNEP17Balances retrieves NEP17Balances for the acc.
func (cd *Cached) GetNEP17Balances(acc util.Uint160) (*state.NEP17Balances, error) {
// GetNEP17TransferInfo retrieves NEP17TransferInfo for the acc.
func (cd *Cached) GetNEP17TransferInfo(acc util.Uint160) (*state.NEP17TransferInfo, error) {
if bs := cd.balances[acc]; bs != nil {
return bs, nil
}
return cd.DAO.GetNEP17Balances(acc)
return cd.DAO.GetNEP17TransferInfo(acc)
}
// PutNEP17Balances saves NEP17Balances for the acc.
func (cd *Cached) PutNEP17Balances(acc util.Uint160, bs *state.NEP17Balances) error {
// PutNEP17TransferInfo saves NEP17TransferInfo for the acc.
func (cd *Cached) PutNEP17TransferInfo(acc util.Uint160, bs *state.NEP17TransferInfo) error {
cd.balances[acc] = bs
return nil
}
@ -88,7 +88,7 @@ func (cd *Cached) Persist() (int, error) {
// caches (accounts/transfer data) in any way.
if ok {
if cd.dropNEP17Cache {
lowerCache.balances = make(map[util.Uint160]*state.NEP17Balances)
lowerCache.balances = make(map[util.Uint160]*state.NEP17TransferInfo)
}
var simpleCache *Simple
for simpleCache == nil {
@ -105,7 +105,7 @@ func (cd *Cached) Persist() (int, error) {
buf := io.NewBufBinWriter()
for acc, bs := range cd.balances {
err := cd.DAO.putNEP17Balances(acc, bs, buf)
err := cd.DAO.putNEP17TransferInfo(acc, bs, buf)
if err != nil {
return 0, err
}

View file

@ -42,7 +42,7 @@ type DAO interface {
GetCurrentBlockHeight() (uint32, error)
GetCurrentHeaderHeight() (i uint32, h util.Uint256, err error)
GetHeaderHashes() ([]util.Uint256, error)
GetNEP17Balances(acc util.Uint160) (*state.NEP17Balances, error)
GetNEP17TransferInfo(acc util.Uint160) (*state.NEP17TransferInfo, error)
GetNEP17TransferLog(acc util.Uint160, index uint32) (*state.NEP17TransferLog, error)
GetStorageItem(id int32, key []byte) state.StorageItem
GetStorageItems(id int32) (map[string]state.StorageItem, error)
@ -55,7 +55,7 @@ type DAO interface {
PutAppExecResult(aer *state.AppExecResult, buf *io.BufBinWriter) error
PutContractID(id int32, hash util.Uint160) error
PutCurrentHeader(hashAndIndex []byte) error
PutNEP17Balances(acc util.Uint160, bs *state.NEP17Balances) error
PutNEP17TransferInfo(acc util.Uint160, bs *state.NEP17TransferInfo) error
PutNEP17TransferLog(acc util.Uint160, index uint32, lg *state.NEP17TransferLog) error
PutStorageItem(id int32, key []byte, si state.StorageItem) error
PutVersion(v string) error
@ -63,7 +63,7 @@ type DAO interface {
StoreAsBlock(block *block.Block, buf *io.BufBinWriter) error
StoreAsCurrentBlock(block *block.Block, buf *io.BufBinWriter) error
StoreAsTransaction(tx *transaction.Transaction, index uint32, buf *io.BufBinWriter) error
putNEP17Balances(acc util.Uint160, bs *state.NEP17Balances, buf *io.BufBinWriter) error
putNEP17TransferInfo(acc util.Uint160, bs *state.NEP17TransferInfo, buf *io.BufBinWriter) error
}
// Simple is memCached wrapper around DB, simple DAO implementation.
@ -142,12 +142,12 @@ func (dao *Simple) GetContractScriptHash(id int32) (util.Uint160, error) {
return *data, nil
}
// -- start nep17 balances.
// -- start nep17 transfer info.
// GetNEP17Balances retrieves nep17 balances from the cache.
func (dao *Simple) GetNEP17Balances(acc util.Uint160) (*state.NEP17Balances, error) {
key := storage.AppendPrefix(storage.STNEP17Balances, acc.BytesBE())
bs := state.NewNEP17Balances()
// GetNEP17TransferInfo retrieves nep17 transfer info from the cache.
func (dao *Simple) GetNEP17TransferInfo(acc util.Uint160) (*state.NEP17TransferInfo, error) {
key := storage.AppendPrefix(storage.STNEP17TransferInfo, acc.BytesBE())
bs := state.NewNEP17TransferInfo()
err := dao.GetAndDecode(bs, key)
if err != nil && err != storage.ErrKeyNotFound {
return nil, err
@ -155,17 +155,17 @@ func (dao *Simple) GetNEP17Balances(acc util.Uint160) (*state.NEP17Balances, err
return bs, nil
}
// PutNEP17Balances saves nep17 balances from the cache.
func (dao *Simple) PutNEP17Balances(acc util.Uint160, bs *state.NEP17Balances) error {
return dao.putNEP17Balances(acc, bs, io.NewBufBinWriter())
// PutNEP17TransferInfo saves nep17 transfer info in the cache.
func (dao *Simple) PutNEP17TransferInfo(acc util.Uint160, bs *state.NEP17TransferInfo) error {
return dao.putNEP17TransferInfo(acc, bs, io.NewBufBinWriter())
}
func (dao *Simple) putNEP17Balances(acc util.Uint160, bs *state.NEP17Balances, buf *io.BufBinWriter) error {
key := storage.AppendPrefix(storage.STNEP17Balances, acc.BytesBE())
func (dao *Simple) putNEP17TransferInfo(acc util.Uint160, bs *state.NEP17TransferInfo, buf *io.BufBinWriter) error {
key := storage.AppendPrefix(storage.STNEP17TransferInfo, acc.BytesBE())
return dao.putWithBuffer(bs, key, buf)
}
// -- end nep17 balances.
// -- end nep17 transfer info.
// -- start transfer log.

View file

@ -713,8 +713,8 @@ func checkFAULTState(t *testing.T, result *state.AppExecResult) {
}
func checkBalanceOf(t *testing.T, chain *Blockchain, addr util.Uint160, expected int) {
balance := chain.GetNEP17Balances(addr).Trackers[chain.contracts.GAS.ID]
require.Equal(t, int64(expected), balance.Balance.Int64())
balance := chain.GetUtilityTokenBalance(addr)
require.Equal(t, int64(expected), balance.Int64())
}
type NotaryFeerStub struct {

View file

@ -33,6 +33,8 @@ type Management struct {
mtx sync.RWMutex
contracts map[util.Uint160]*state.Contract
// nep17 is a map of NEP17-compliant contracts which is updated with every PostPersist.
nep17 map[util.Uint160]struct{}
}
const (
@ -63,6 +65,7 @@ func newManagement() *Management {
var m = &Management{
ContractMD: *interop.NewContractMD(nativenames.Management, managementContractID),
contracts: make(map[util.Uint160]*state.Contract),
nep17: make(map[util.Uint160]struct{}),
}
defer m.UpdateHash()
@ -471,6 +474,9 @@ func (m *Management) OnPersist(ic *interop.Context) error {
}
m.mtx.Lock()
m.contracts[md.Hash] = cs
if md.Manifest.IsStandardSupported(manifest.NEP17StandardName) {
m.nep17[md.Hash] = struct{}{}
}
m.mtx.Unlock()
}
@ -492,6 +498,9 @@ func (m *Management) InitializeCache(d dao.DAO) error {
return
}
m.contracts[cs.Hash] = cs
if cs.Manifest.IsStandardSupported(manifest.NEP17StandardName) {
m.nep17[cs.Hash] = struct{}{}
}
})
return initErr
}
@ -507,14 +516,33 @@ func (m *Management) PostPersist(ic *interop.Context) error {
if err != nil {
// Contract was destroyed.
delete(m.contracts, h)
delete(m.nep17, h)
continue
}
m.contracts[h] = newCs
if newCs.Manifest.IsStandardSupported(manifest.NEP17StandardName) {
m.nep17[h] = struct{}{}
} else {
delete(m.nep17, h)
}
}
m.mtx.Unlock()
return nil
}
// GetNEP17Contracts returns hashes of all deployed contracts that support NEP17 standard. The list
// is updated every PostPersist, so until PostPersist is called, the result for the previous block
// is returned.
func (m *Management) GetNEP17Contracts() []util.Uint160 {
m.mtx.RLock()
result := make([]util.Uint160, 0, len(m.nep17))
for h := range m.nep17 {
result = append(result, h)
}
m.mtx.RUnlock()
return result
}
// Initialize implements Contract interface.
func (m *Management) Initialize(ic *interop.Context) error {
if err := setIntWithKey(m.ID, ic.DAO, keyMinimumDeploymentFee, defaultMinimumDeploymentFee); err != nil {

View file

@ -83,3 +83,50 @@ func TestManagement_Initialize(t *testing.T) {
require.Error(t, mgmt.InitializeCache(d))
})
}
func TestManagement_GetNEP17Contracts(t *testing.T) {
mgmt := newManagement()
d := dao.NewCached(dao.NewSimple(storage.NewMemoryStore(), false))
err := mgmt.Initialize(&interop.Context{DAO: d})
require.NoError(t, err)
require.Empty(t, mgmt.GetNEP17Contracts())
// Deploy NEP17 contract
script := []byte{byte(opcode.RET)}
sender := util.Uint160{1, 2, 3}
ne, err := nef.NewFile(script)
require.NoError(t, err)
manif := manifest.NewManifest("Test")
manif.ABI.Methods = append(manif.ABI.Methods, manifest.Method{
Name: "dummy",
ReturnType: smartcontract.VoidType,
Parameters: []manifest.Parameter{},
})
manif.SupportedStandards = []string{manifest.NEP17StandardName}
c1, err := mgmt.Deploy(d, sender, ne, manif)
require.NoError(t, err)
// PostPersist is not yet called, thus no NEP17 contracts are expected
require.Empty(t, mgmt.GetNEP17Contracts())
// Call PostPersist, check c1 contract hash is returned
require.NoError(t, mgmt.PostPersist(&interop.Context{DAO: d}))
require.Equal(t, []util.Uint160{c1.Hash}, mgmt.GetNEP17Contracts())
// Update contract
manif.ABI.Methods = append(manif.ABI.Methods, manifest.Method{
Name: "dummy2",
ReturnType: smartcontract.VoidType,
Parameters: []manifest.Parameter{},
})
c2, err := mgmt.Update(d, c1.Hash, ne, manif)
require.NoError(t, err)
// No changes expected before PostPersist call.
require.Equal(t, []util.Uint160{c1.Hash}, mgmt.GetNEP17Contracts())
// Call PostPersist, check c2 contract hash is returned
require.NoError(t, mgmt.PostPersist(&interop.Context{DAO: d}))
require.Equal(t, []util.Uint160{c2.Hash}, mgmt.GetNEP17Contracts())
}

View file

@ -5,6 +5,7 @@ import (
"fmt"
"math/big"
"github.com/nspcc-dev/neo-go/pkg/core/dao"
"github.com/nspcc-dev/neo-go/pkg/core/interop"
"github.com/nspcc-dev/neo-go/pkg/core/interop/runtime"
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
@ -138,6 +139,11 @@ func (g *GAS) PostPersist(ic *interop.Context) error {
return nil
}
// BalanceOf returns native GAS token balance for the acc.
func (g *GAS) BalanceOf(d dao.DAO, acc util.Uint160) *big.Int {
return g.balanceOfInternal(d, acc)
}
func getStandbyValidatorsHash(ic *interop.Context) (util.Uint160, error) {
s, err := smartcontract.CreateDefaultMultiSigRedeemScript(ic.Chain.GetStandByValidators())
if err != nil {

View file

@ -1020,6 +1020,20 @@ func (n *NEO) GetNextBlockValidatorsInternal() keys.PublicKeys {
return n.nextValidators.Load().(keys.PublicKeys).Copy()
}
// BalanceOf returns native NEO token balance for the acc.
func (n *NEO) BalanceOf(d dao.DAO, acc util.Uint160) (*big.Int, uint32) {
key := makeAccountKey(acc)
si := d.GetStorageItem(n.ID, key)
if si == nil {
return big.NewInt(0), 0
}
st, err := state.NEOBalanceFromBytes(si)
if err != nil {
panic(fmt.Errorf("failed to decode NEO balance state: %w", err))
}
return &st.Balance, st.BalanceHeight
}
func pubsToArray(pubs keys.PublicKeys) stackitem.Item {
arr := make([]stackitem.Item, len(pubs))
for i := range pubs {

View file

@ -246,16 +246,20 @@ func (c *nep17TokenNative) TransferInternal(ic *interop.Context, from, to util.U
func (c *nep17TokenNative) balanceOf(ic *interop.Context, args []stackitem.Item) stackitem.Item {
h := toUint160(args[0])
return stackitem.NewBigInteger(c.balanceOfInternal(ic.DAO, h))
}
func (c *nep17TokenNative) balanceOfInternal(d dao.DAO, h util.Uint160) *big.Int {
key := makeAccountKey(h)
si := ic.DAO.GetStorageItem(c.ID, key)
si := d.GetStorageItem(c.ID, key)
if si == nil {
return stackitem.NewBigInteger(big.NewInt(0))
return big.NewInt(0)
}
balance, err := c.balFromBytes(&si)
if err != nil {
panic(fmt.Errorf("can not deserialize balance state: %w", err))
}
return stackitem.NewBigInteger(balance)
return balance
}
func (c *nep17TokenNative) mint(ic *interop.Context, h util.Uint160, amount *big.Int, callOnPayment bool) {

View file

@ -76,12 +76,9 @@ func TestGAS_Roundtrip(t *testing.T) {
bc := newTestChain(t)
getUtilityTokenBalance := func(bc *Blockchain, acc util.Uint160) (*big.Int, uint32) {
bs, err := bc.dao.GetNEP17Balances(acc)
if err != nil {
return big.NewInt(0), 0
}
balance := bs.Trackers[bc.contracts.GAS.ID]
return &balance.Balance, balance.LastUpdatedBlock
lub, err := bc.GetNEP17LastUpdated(acc)
require.NoError(t, err)
return bc.GetUtilityTokenBalance(acc), lub[bc.contracts.GAS.ID]
}
initialBalance, _ := getUtilityTokenBalance(bc, neoOwner)

View file

@ -607,3 +607,18 @@ func TestMinimumDeploymentFee(t *testing.T) {
testGetSet(t, chain, chain.contracts.Management.Hash, "MinimumDeploymentFee", 10_00000000, 0, 0)
}
func TestManagement_GetNEP17Contracts(t *testing.T) {
t.Run("empty chain", func(t *testing.T) {
chain := newTestChain(t)
require.ElementsMatch(t, []util.Uint160{chain.contracts.NEO.Hash, chain.contracts.GAS.Hash}, chain.contracts.Management.GetNEP17Contracts())
})
t.Run("test chain", func(t *testing.T) {
chain := newTestChain(t)
initBasicChain(t, chain)
rublesHash, err := chain.GetContractScriptHash(1)
require.NoError(t, err)
require.ElementsMatch(t, []util.Uint160{chain.contracts.NEO.Hash, chain.contracts.GAS.Hash, rublesHash}, chain.contracts.Management.GetNEP17Contracts())
})
}

View file

@ -11,15 +11,6 @@ import (
// NEP17TransferBatchSize is the maximum number of entries for NEP17TransferLog.
const NEP17TransferBatchSize = 128
// NEP17Tracker contains info about a single account in a NEP17 contract.
type NEP17Tracker struct {
// Balance is the current balance of the account.
Balance big.Int
// LastUpdatedBlock is a number of block when last `transfer` to or from the
// account occurred.
LastUpdatedBlock uint32
}
// NEP17TransferLog is a log of NEP17 token transfers for the specific command.
type NEP17TransferLog struct {
Raw []byte
@ -44,46 +35,44 @@ type NEP17Transfer struct {
Tx util.Uint256
}
// NEP17Balances is a map of the NEP17 contract IDs
// to the corresponding structures.
type NEP17Balances struct {
Trackers map[int32]NEP17Tracker
// NEP17TransferInfo stores map of the NEP17 contract IDs to the balance's last updated
// block trackers along with information about NEP17 transfer batch.
type NEP17TransferInfo struct {
LastUpdated map[int32]uint32
// NextTransferBatch stores an index of the next transfer batch.
NextTransferBatch uint32
// NewBatch is true if batch with the `NextTransferBatch` index should be created.
NewBatch bool
}
// NewNEP17Balances returns new NEP17Balances.
func NewNEP17Balances() *NEP17Balances {
return &NEP17Balances{
Trackers: make(map[int32]NEP17Tracker),
// NewNEP17TransferInfo returns new NEP17TransferInfo.
func NewNEP17TransferInfo() *NEP17TransferInfo {
return &NEP17TransferInfo{
LastUpdated: make(map[int32]uint32),
}
}
// DecodeBinary implements io.Serializable interface.
func (bs *NEP17Balances) DecodeBinary(r *io.BinReader) {
func (bs *NEP17TransferInfo) DecodeBinary(r *io.BinReader) {
bs.NextTransferBatch = r.ReadU32LE()
bs.NewBatch = r.ReadBool()
lenBalances := r.ReadVarUint()
m := make(map[int32]NEP17Tracker, lenBalances)
m := make(map[int32]uint32, lenBalances)
for i := 0; i < int(lenBalances); i++ {
key := int32(r.ReadU32LE())
var tr NEP17Tracker
tr.DecodeBinary(r)
m[key] = tr
m[key] = r.ReadU32LE()
}
bs.Trackers = m
bs.LastUpdated = m
}
// EncodeBinary implements io.Serializable interface.
func (bs *NEP17Balances) EncodeBinary(w *io.BinWriter) {
func (bs *NEP17TransferInfo) EncodeBinary(w *io.BinWriter) {
w.WriteU32LE(bs.NextTransferBatch)
w.WriteBool(bs.NewBatch)
w.WriteVarUint(uint64(len(bs.Trackers)))
for k, v := range bs.Trackers {
w.WriteVarUint(uint64(len(bs.LastUpdated)))
for k, v := range bs.LastUpdated {
w.WriteU32LE(uint32(k))
v.EncodeBinary(w)
w.WriteU32LE(v)
}
}
@ -138,18 +127,6 @@ func (lg *NEP17TransferLog) Size() int {
return int(lg.Raw[0])
}
// EncodeBinary implements io.Serializable interface.
func (t *NEP17Tracker) EncodeBinary(w *io.BinWriter) {
w.WriteVarBytes(bigint.ToBytes(&t.Balance))
w.WriteU32LE(t.LastUpdatedBlock)
}
// DecodeBinary implements io.Serializable interface.
func (t *NEP17Tracker) DecodeBinary(r *io.BinReader) {
t.Balance = *bigint.FromBytes(r.ReadVarBytes())
t.LastUpdatedBlock = r.ReadU32LE()
}
// EncodeBinary implements io.Serializable interface.
func (t *NEP17Transfer) EncodeBinary(w *io.BinWriter) {
w.WriteU32LE(uint32(t.Asset))

View file

@ -38,15 +38,6 @@ func TestNEP17TransferLog_Append(t *testing.T) {
require.True(t, cont)
}
func TestNEP17Tracker_EncodeBinary(t *testing.T) {
expected := &NEP17Tracker{
Balance: *big.NewInt(int64(rand.Uint64())),
LastUpdatedBlock: rand.Uint32(),
}
testserdes.EncodeDecodeBinary(t, expected, new(NEP17Tracker))
}
func TestNEP17Transfer_DecodeBinary(t *testing.T) {
expected := &NEP17Transfer{
Asset: 123,

View file

@ -8,19 +8,19 @@ import (
// KeyPrefix constants.
const (
DataBlock KeyPrefix = 0x01
DataTransaction KeyPrefix = 0x02
DataMPT KeyPrefix = 0x03
STAccount KeyPrefix = 0x40
STNotification KeyPrefix = 0x4d
STContractID KeyPrefix = 0x51
STStorage KeyPrefix = 0x70
STNEP17Transfers KeyPrefix = 0x72
STNEP17Balances KeyPrefix = 0x73
IXHeaderHashList KeyPrefix = 0x80
SYSCurrentBlock KeyPrefix = 0xc0
SYSCurrentHeader KeyPrefix = 0xc1
SYSVersion KeyPrefix = 0xf0
DataBlock KeyPrefix = 0x01
DataTransaction KeyPrefix = 0x02
DataMPT KeyPrefix = 0x03
STAccount KeyPrefix = 0x40
STNotification KeyPrefix = 0x4d
STContractID KeyPrefix = 0x51
STStorage KeyPrefix = 0x70
STNEP17Transfers KeyPrefix = 0x72
STNEP17TransferInfo KeyPrefix = 0x73
IXHeaderHashList KeyPrefix = 0x80
SYSCurrentBlock KeyPrefix = 0xc0
SYSCurrentHeader KeyPrefix = 0xc1
SYSVersion KeyPrefix = 0xf0
)
const (

View file

@ -40,6 +40,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
"go.uber.org/zap"
)
@ -669,28 +670,65 @@ func (s *Server) getNEP17Balances(ps request.Params) (interface{}, *response.Err
return nil, response.ErrInvalidParams
}
as := s.chain.GetNEP17Balances(u)
bs := &result.NEP17Balances{
Address: address.Uint160ToString(u),
Balances: []result.NEP17Balance{},
}
if as != nil {
cache := make(map[int32]util.Uint160)
for id, bal := range as.Trackers {
h, err := s.getHash(id, cache)
if err != nil {
continue
}
bs.Balances = append(bs.Balances, result.NEP17Balance{
Asset: h,
Amount: bal.Balance.String(),
LastUpdated: bal.LastUpdatedBlock,
})
lastUpdated, err := s.chain.GetNEP17LastUpdated(u)
if err != nil {
return nil, response.NewRPCError("Failed to get NEP17 last updated block", err.Error(), err)
}
bw := io.NewBufBinWriter()
for _, h := range s.chain.GetNEP17Contracts() {
balance, err := s.getNEP17Balance(h, u, bw)
if err != nil {
continue
}
if balance.Sign() == 0 {
continue
}
cs := s.chain.GetContractState(h)
if cs == nil {
continue
}
bs.Balances = append(bs.Balances, result.NEP17Balance{
Asset: h,
Amount: balance.String(),
LastUpdated: lastUpdated[cs.ID],
})
}
return bs, nil
}
func (s *Server) getNEP17Balance(h util.Uint160, acc util.Uint160, bw *io.BufBinWriter) (*big.Int, error) {
if bw == nil {
bw = io.NewBufBinWriter()
} else {
bw.Reset()
}
emit.AppCall(bw.BinWriter, h, "balanceOf", callflag.ReadStates, acc)
if bw.Err != nil {
return nil, fmt.Errorf("failed to create `balanceOf` invocation script: %w", bw.Err)
}
script := bw.Bytes()
tx := &transaction.Transaction{Script: script}
v := s.chain.GetTestVM(trigger.Application, tx, nil)
v.GasLimit = core.HeaderVerificationGasLimit
v.LoadScriptWithFlags(script, callflag.All)
err := v.Run()
if err != nil {
return nil, fmt.Errorf("failed to run `balanceOf` for %s: %w", h.StringLE(), err)
}
if v.Estack().Len() != 1 {
return nil, fmt.Errorf("invalid `balanceOf` return values count: expected 1, got %d", v.Estack().Len())
}
res, err := v.Estack().Pop().Item().TryInteger()
if err != nil {
return nil, fmt.Errorf("unexpected `balanceOf` result type: %w", err)
}
return res, nil
}
func getTimestampsAndLimit(ps request.Params, index int) (uint64, uint64, int, int, error) {
var start, end uint64
var limit, page int

View file

@ -119,6 +119,16 @@ func (m *Manifest) IsValid(hash util.Uint160) error {
return Permissions(m.Permissions).AreValid()
}
// IsStandardSupported denotes whether the specified standard supported by the contract.
func (m *Manifest) IsStandardSupported(standard string) bool {
for _, st := range m.SupportedStandards {
if st == standard {
return true
}
}
return false
}
// ToStackItem converts Manifest to stackitem.Item.
func (m *Manifest) ToStackItem() (stackitem.Item, error) {
groups := make([]stackitem.Item, len(m.Groups))

View file

@ -429,3 +429,15 @@ func TestExtraToStackItem(t *testing.T) {
require.Equal(t, tc.expected, string(actual))
}
}
func TestManifest_IsStandardSupported(t *testing.T) {
m := &Manifest{
SupportedStandards: []string{NEP17StandardName, NEP17Payable, NEP11Payable},
}
for _, st := range m.SupportedStandards {
require.True(t, m.IsStandardSupported(st))
}
require.False(t, m.IsStandardSupported(NEP11StandardName))
require.False(t, m.IsStandardSupported(""))
require.False(t, m.IsStandardSupported("unknown standard"))
}