From dbd460d8836744da96e429cbbd8b3bb2431f0261 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Tue, 28 Jul 2020 16:36:47 +0300 Subject: [PATCH 1/5] core: retrieve contract hash by ID We'll need this ability further to retrieve contracts hashes for Nep5Balances. --- pkg/core/blockchain.go | 5 +++++ pkg/core/blockchainer/blockchainer.go | 1 + pkg/core/dao/dao.go | 29 ++++++++++++++++++++++++++- pkg/core/storage/store.go | 1 + pkg/network/helper_test.go | 3 +++ 5 files changed, 38 insertions(+), 1 deletion(-) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 96a06ea54..2c86d5b33 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -982,6 +982,11 @@ func (bc *Blockchain) GetContractState(hash util.Uint160) *state.Contract { return contract } +// GetContractScriptHash returns contract script hash by its ID. +func (bc *Blockchain) GetContractScriptHash(id int32) (util.Uint160, error) { + return bc.dao.GetContractScriptHash(id) +} + // GetAccountState returns the account state from its script hash. func (bc *Blockchain) GetAccountState(scriptHash util.Uint160) *state.Account { as, err := bc.dao.GetAccountState(scriptHash) diff --git a/pkg/core/blockchainer/blockchainer.go b/pkg/core/blockchainer/blockchainer.go index 9dcac9e33..aebd61ba3 100644 --- a/pkg/core/blockchainer/blockchainer.go +++ b/pkg/core/blockchainer/blockchainer.go @@ -26,6 +26,7 @@ type Blockchainer interface { HeaderHeight() uint32 GetBlock(hash util.Uint256) (*block.Block, error) GetContractState(hash util.Uint160) *state.Contract + GetContractScriptHash(id int32) (util.Uint160, error) GetEnrollments() ([]state.Validator, error) GetGoverningTokenBalance(acc util.Uint160) (*big.Int, uint32) GetHeaderHash(int) util.Uint256 diff --git a/pkg/core/dao/dao.go b/pkg/core/dao/dao.go index f24c7267c..a867fa1eb 100644 --- a/pkg/core/dao/dao.go +++ b/pkg/core/dao/dao.go @@ -27,6 +27,7 @@ type DAO interface { GetBatch() *storage.MemBatch GetBlock(hash util.Uint256) (*block.Block, error) GetContractState(hash util.Uint160) (*state.Contract, error) + GetContractScriptHash(id int32) (util.Uint160, error) GetCurrentBlockHeight() (uint32, error) GetCurrentHeaderHeight() (i uint32, h util.Uint256, err error) GetHeaderHashes() ([]util.Uint256, error) @@ -163,7 +164,10 @@ func (dao *Simple) GetContractState(hash util.Uint160) (*state.Contract, error) // PutContractState puts given contract state into the given store. func (dao *Simple) PutContractState(cs *state.Contract) error { key := storage.AppendPrefix(storage.STContract, cs.ScriptHash().BytesBE()) - return dao.Put(cs, key) + if err := dao.Put(cs, key); err != nil { + return err + } + return dao.putContractScriptHash(cs) } // DeleteContractState deletes given contract state in the given store. @@ -187,6 +191,29 @@ func (dao *Simple) GetAndUpdateNextContractID() (int32, error) { return id, dao.Store.Put(key, data) } +// putContractScriptHash puts given contract script hash into the given store. +// It's a private method because it should be used after PutContractState to keep +// ID-Hash pair always up-to-date. +func (dao *Simple) putContractScriptHash(cs *state.Contract) error { + key := make([]byte, 5) + key[0] = byte(storage.STContractID) + binary.LittleEndian.PutUint32(key[1:], uint32(cs.ID)) + return dao.Store.Put(key, cs.ScriptHash().BytesBE()) +} + +// GetContractScriptHash returns script hash of the contract with the specified ID. +// Contract with the script hash may be destroyed. +func (dao *Simple) GetContractScriptHash(id int32) (util.Uint160, error) { + key := make([]byte, 5) + key[0] = byte(storage.STContractID) + binary.LittleEndian.PutUint32(key[1:], uint32(id)) + data := &util.Uint160{} + if err := dao.GetAndDecode(data, key); err != nil { + return *data, err + } + return *data, nil +} + // -- end contracts. // -- start nep5 balances. diff --git a/pkg/core/storage/store.go b/pkg/core/storage/store.go index 5e70334ed..d1431fce2 100644 --- a/pkg/core/storage/store.go +++ b/pkg/core/storage/store.go @@ -12,6 +12,7 @@ const ( STAccount KeyPrefix = 0x40 STNotification KeyPrefix = 0x4d STContract KeyPrefix = 0x50 + STContractID KeyPrefix = 0x51 STStorage KeyPrefix = 0x70 STNEP5Transfers KeyPrefix = 0x72 STNEP5Balances KeyPrefix = 0x73 diff --git a/pkg/network/helper_test.go b/pkg/network/helper_test.go index 6004c44a4..1d125f264 100644 --- a/pkg/network/helper_test.go +++ b/pkg/network/helper_test.go @@ -67,6 +67,9 @@ func (chain testChain) GetBlock(hash util.Uint256) (*block.Block, error) { func (chain testChain) GetContractState(hash util.Uint160) *state.Contract { panic("TODO") } +func (chain testChain) GetContractScriptHash(id int32) (util.Uint160, error) { + panic("TODO") +} func (chain testChain) GetHeaderHash(int) util.Uint256 { return util.Uint256{} } From 1f5794b316e88075b4f316fb125963a1b3b09d50 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 29 Jul 2020 14:31:16 +0300 Subject: [PATCH 2/5] rpc: make `getDecimals` return a standard error `getDecimals` is an internal method, wo there's no need to return response.Error. --- pkg/rpc/server/server.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/rpc/server/server.go b/pkg/rpc/server/server.go index 9d17c71c5..ca376f692 100644 --- a/pkg/rpc/server/server.go +++ b/pkg/rpc/server/server.go @@ -590,7 +590,7 @@ func amountToString(amount *big.Int, decimals int64) string { return fmt.Sprintf(fs, q, r) } -func (s *Server) getDecimals(h util.Uint160, cache map[util.Uint160]int64) (int64, *response.Error) { +func (s *Server) getDecimals(h util.Uint160, cache map[util.Uint160]int64) (int64, error) { if d, ok := cache[h]; ok { return d, nil } @@ -605,11 +605,11 @@ func (s *Server) getDecimals(h util.Uint160, cache map[util.Uint160]int64) (int6 }, }) if err != nil { - return 0, response.NewInternalServerError("Can't create script", err) + return 0, fmt.Errorf("can't create script: %v", err) } res := s.runScriptInVM(script, nil) if res == nil || res.State != "HALT" || len(res.Stack) == 0 { - return 0, response.NewInternalServerError("execution error", errors.New("no result")) + return 0, errors.New("execution error : no result") } var d int64 @@ -619,10 +619,10 @@ func (s *Server) getDecimals(h util.Uint160, cache map[util.Uint160]int64) (int6 case smartcontract.ByteArrayType: d = bigint.FromBytes(item.Value.([]byte)).Int64() default: - return 0, response.NewInternalServerError("invalid result", errors.New("not an integer")) + return 0, errors.New("invalid result: not an integer") } if d < 0 { - return 0, response.NewInternalServerError("incorrect result", errors.New("negative result")) + return 0, errors.New("incorrect result: negative result") } cache[h] = d return d, nil From b9bdab8ec8f9409988bdaa31e52315c4d572bfd1 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Tue, 28 Jul 2020 12:23:58 +0300 Subject: [PATCH 3/5] core: store nep5balances using contract id Closes #1194 --- pkg/core/blockchain.go | 23 +++++++++---- pkg/core/dao/cacheddao.go | 61 ---------------------------------- pkg/core/interop_neo.go | 1 - pkg/core/native/native_neo.go | 2 +- pkg/core/native/native_nep5.go | 2 +- pkg/core/state/nep5.go | 13 ++++---- pkg/rpc/server/server.go | 6 +++- 7 files changed, 30 insertions(+), 78 deletions(-) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 2c86d5b33..bb9525191 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -715,15 +715,26 @@ func (bc *Blockchain) processNEP5Transfer(cache *dao.Cached, h util.Uint256, b * Timestamp: b.Timestamp, Tx: h, } + var id int32 + nativeContract := bc.contracts.ByHash(sc) + if nativeContract != nil { + id = nativeContract.Metadata().ContractID + } else { + assetContract := bc.GetContractState(sc) + if assetContract == nil { + return + } + id = assetContract.ID + } if !fromAddr.Equals(util.Uint160{}) { balances, err := cache.GetNEP5Balances(fromAddr) if err != nil { return } - bs := balances.Trackers[sc] + bs := balances.Trackers[id] bs.Balance = *new(big.Int).Sub(&bs.Balance, amount) bs.LastUpdatedBlock = b.Index - balances.Trackers[sc] = bs + balances.Trackers[id] = bs transfer.Amount = *new(big.Int).Sub(&transfer.Amount, amount) isBig, err := cache.AppendNEP5Transfer(fromAddr, balances.NextTransferBatch, transfer) if err != nil { @@ -741,10 +752,10 @@ func (bc *Blockchain) processNEP5Transfer(cache *dao.Cached, h util.Uint256, b * if err != nil { return } - bs := balances.Trackers[sc] + bs := balances.Trackers[id] bs.Balance = *new(big.Int).Add(&bs.Balance, amount) bs.LastUpdatedBlock = b.Index - balances.Trackers[sc] = bs + balances.Trackers[id] = bs transfer.Amount = *amount isBig, err := cache.AppendNEP5Transfer(toAddr, balances.NextTransferBatch, transfer) @@ -792,7 +803,7 @@ func (bc *Blockchain) GetUtilityTokenBalance(acc util.Uint160) *big.Int { if err != nil { return big.NewInt(0) } - balance := bs.Trackers[bc.contracts.GAS.Hash].Balance + balance := bs.Trackers[bc.contracts.GAS.ContractID].Balance return &balance } @@ -803,7 +814,7 @@ func (bc *Blockchain) GetGoverningTokenBalance(acc util.Uint160) (*big.Int, uint if err != nil { return big.NewInt(0), 0 } - neo := bs.Trackers[bc.contracts.NEO.Hash] + neo := bs.Trackers[bc.contracts.NEO.ContractID] return &neo.Balance, neo.LastUpdatedBlock } diff --git a/pkg/core/dao/cacheddao.go b/pkg/core/dao/cacheddao.go index 3862ed767..123f8eda8 100644 --- a/pkg/core/dao/cacheddao.go +++ b/pkg/core/dao/cacheddao.go @@ -1,11 +1,9 @@ package dao import ( - "bytes" "errors" "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/io" "github.com/nspcc-dev/neo-go/pkg/util" ) @@ -125,65 +123,6 @@ func (cd *Cached) AppendNEP5Transfer(acc util.Uint160, index uint32, tr *state.N return lg.Size() >= nep5TransferBatchSize, cd.PutNEP5TransferLog(acc, index, lg) } -// MigrateNEP5Balances migrates NEP5 balances from old contract to the new one. -func (cd *Cached) MigrateNEP5Balances(from, to util.Uint160) error { - var ( - simpleDAO *Simple - cachedDAO = cd - ok bool - w = io.NewBufBinWriter() - ) - for simpleDAO == nil { - simpleDAO, ok = cachedDAO.DAO.(*Simple) - if !ok { - cachedDAO, ok = cachedDAO.DAO.(*Cached) - if !ok { - panic("uknown DAO") - } - } - } - for acc, bs := range cd.balances { - err := simpleDAO.putNEP5Balances(acc, bs, w) - if err != nil { - return err - } - w.Reset() - } - cd.dropNEP5Cache = true - var store = simpleDAO.Store - // Create another layer of cache because we can't change original storage - // while seeking. - var upStore = storage.NewMemCachedStore(store) - store.Seek([]byte{byte(storage.STNEP5Balances)}, func(k, v []byte) { - if !bytes.Contains(v, from[:]) { - return - } - bs := state.NewNEP5Balances() - reader := io.NewBinReaderFromBuf(v) - bs.DecodeBinary(reader) - if reader.Err != nil { - panic("bad nep5 balances") - } - tr, ok := bs.Trackers[from] - if !ok { - return - } - delete(bs.Trackers, from) - bs.Trackers[to] = tr - w.Reset() - bs.EncodeBinary(w.BinWriter) - if w.Err != nil { - panic("error on nep5 balance encoding") - } - err := upStore.Put(k, w.Bytes()) - if err != nil { - panic("can't put value in the DB") - } - }) - _, err := upStore.Persist() - return err -} - // Persist flushes all the changes made into the (supposedly) persistent // underlying store. func (cd *Cached) Persist() (int, error) { diff --git a/pkg/core/interop_neo.go b/pkg/core/interop_neo.go index 43e46dd6e..5f5ffb943 100644 --- a/pkg/core/interop_neo.go +++ b/pkg/core/interop_neo.go @@ -154,7 +154,6 @@ func contractUpdate(ic *interop.Context, v *vm.VM) error { if err := ic.DAO.DeleteContractState(oldHash); err != nil { return fmt.Errorf("failed to update script: %v", err) } - ic.DAO.MigrateNEP5Balances(oldHash, newHash) } // if manifest was provided, update the old contract manifest and check associated // storage items if needed diff --git a/pkg/core/native/native_neo.go b/pkg/core/native/native_neo.go index 731e4e4f7..04beb261e 100644 --- a/pkg/core/native/native_neo.go +++ b/pkg/core/native/native_neo.go @@ -210,7 +210,7 @@ func (n *NEO) unclaimedGas(ic *interop.Context, args []stackitem.Item) stackitem if err != nil { panic(err) } - tr := bs.Trackers[n.Hash] + tr := bs.Trackers[n.ContractID] gen := ic.Chain.CalculateClaimable(&tr.Balance, tr.LastUpdatedBlock, end) return stackitem.NewBigInteger(gen) diff --git a/pkg/core/native/native_nep5.go b/pkg/core/native/native_nep5.go index af743023c..589b4144d 100644 --- a/pkg/core/native/native_nep5.go +++ b/pkg/core/native/native_nep5.go @@ -210,7 +210,7 @@ func (c *nep5TokenNative) balanceOf(ic *interop.Context, args []stackitem.Item) if err != nil { panic(err) } - balance := bs.Trackers[c.Hash].Balance + balance := bs.Trackers[c.ContractID].Balance return stackitem.NewBigInteger(&balance) } diff --git a/pkg/core/state/nep5.go b/pkg/core/state/nep5.go index ad89ffac9..9a93b2f53 100644 --- a/pkg/core/state/nep5.go +++ b/pkg/core/state/nep5.go @@ -43,10 +43,10 @@ type NEP5Transfer struct { Tx util.Uint256 } -// NEP5Balances is a map of the NEP5 contract hashes +// NEP5Balances is a map of the NEP5 contract IDs // to the corresponding structures. type NEP5Balances struct { - Trackers map[util.Uint160]NEP5Tracker + Trackers map[int32]NEP5Tracker // NextTransferBatch stores an index of the next transfer batch. NextTransferBatch uint32 } @@ -54,7 +54,7 @@ type NEP5Balances struct { // NewNEP5Balances returns new NEP5Balances. func NewNEP5Balances() *NEP5Balances { return &NEP5Balances{ - Trackers: make(map[util.Uint160]NEP5Tracker), + Trackers: make(map[int32]NEP5Tracker), } } @@ -62,11 +62,10 @@ func NewNEP5Balances() *NEP5Balances { func (bs *NEP5Balances) DecodeBinary(r *io.BinReader) { bs.NextTransferBatch = r.ReadU32LE() lenBalances := r.ReadVarUint() - m := make(map[util.Uint160]NEP5Tracker, lenBalances) + m := make(map[int32]NEP5Tracker, lenBalances) for i := 0; i < int(lenBalances); i++ { - var key util.Uint160 + key := int32(r.ReadU32LE()) var tr NEP5Tracker - r.ReadBytes(key[:]) tr.DecodeBinary(r) m[key] = tr } @@ -78,7 +77,7 @@ func (bs *NEP5Balances) EncodeBinary(w *io.BinWriter) { w.WriteU32LE(bs.NextTransferBatch) w.WriteVarUint(uint64(len(bs.Trackers))) for k, v := range bs.Trackers { - w.WriteBytes(k[:]) + w.WriteU32LE(uint32(k)) v.EncodeBinary(w) } } diff --git a/pkg/rpc/server/server.go b/pkg/rpc/server/server.go index ca376f692..3593b9164 100644 --- a/pkg/rpc/server/server.go +++ b/pkg/rpc/server/server.go @@ -515,7 +515,11 @@ func (s *Server) getNEP5Balances(ps request.Params) (interface{}, *response.Erro } if as != nil { cache := make(map[util.Uint160]int64) - for h, bal := range as.Trackers { + for id, bal := range as.Trackers { + h, err := s.chain.GetContractScriptHash(id) + if err != nil { + continue + } dec, err := s.getDecimals(h, cache) if err != nil { continue From 0dd1730632077976574175968873f62701a5b46a Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Tue, 28 Jul 2020 19:05:16 +0300 Subject: [PATCH 4/5] core: store NEP5Transfer asset by ID instead of hash To avoid problems with retrieving decimals for migrated contracts. --- pkg/core/blockchain.go | 16 ++++++++-------- pkg/core/state/nep5.go | 10 +++++----- pkg/core/state/nep5_test.go | 4 ++-- pkg/rpc/server/server.go | 8 ++++++-- 4 files changed, 21 insertions(+), 17 deletions(-) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index bb9525191..8df7dfd37 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -707,14 +707,6 @@ func parseUint160(addr []byte) util.Uint160 { func (bc *Blockchain) processNEP5Transfer(cache *dao.Cached, h util.Uint256, b *block.Block, sc util.Uint160, from, to []byte, amount *big.Int) { toAddr := parseUint160(to) fromAddr := parseUint160(from) - transfer := &state.NEP5Transfer{ - Asset: sc, - From: fromAddr, - To: toAddr, - Block: b.Index, - Timestamp: b.Timestamp, - Tx: h, - } var id int32 nativeContract := bc.contracts.ByHash(sc) if nativeContract != nil { @@ -726,6 +718,14 @@ func (bc *Blockchain) processNEP5Transfer(cache *dao.Cached, h util.Uint256, b * } id = assetContract.ID } + transfer := &state.NEP5Transfer{ + Asset: id, + From: fromAddr, + To: toAddr, + Block: b.Index, + Timestamp: b.Timestamp, + Tx: h, + } if !fromAddr.Equals(util.Uint160{}) { balances, err := cache.GetNEP5Balances(fromAddr) if err != nil { diff --git a/pkg/core/state/nep5.go b/pkg/core/state/nep5.go index 9a93b2f53..36ac13ec3 100644 --- a/pkg/core/state/nep5.go +++ b/pkg/core/state/nep5.go @@ -26,8 +26,8 @@ type NEP5TransferLog struct { // NEP5Transfer represents a single NEP5 Transfer event. type NEP5Transfer struct { - // Asset is a NEP5 contract hash. - Asset util.Uint160 + // Asset is a NEP5 contract ID. + Asset int32 // Address is the address of the sender. From util.Uint160 // To is the address of the receiver. @@ -132,7 +132,7 @@ func (t *NEP5Tracker) DecodeBinary(r *io.BinReader) { // EncodeBinary implements io.Serializable interface. func (t *NEP5Transfer) EncodeBinary(w *io.BinWriter) { - w.WriteBytes(t.Asset[:]) + w.WriteU32LE(uint32(t.Asset)) w.WriteBytes(t.Tx[:]) w.WriteBytes(t.From[:]) w.WriteBytes(t.To[:]) @@ -150,7 +150,7 @@ func (t *NEP5Transfer) DecodeBinary(r *io.BinReader) { // DecodeBinaryReturnCount decodes NEP5Transfer and returns the number of bytes read. func (t *NEP5Transfer) DecodeBinaryReturnCount(r *io.BinReader) int { - r.ReadBytes(t.Asset[:]) + t.Asset = int32(r.ReadU32LE()) r.ReadBytes(t.Tx[:]) r.ReadBytes(t.From[:]) r.ReadBytes(t.To[:]) @@ -160,5 +160,5 @@ func (t *NEP5Transfer) DecodeBinaryReturnCount(r *io.BinReader) int { amountBytes := make([]byte, amountLen) r.ReadBytes(amountBytes) t.Amount = *bigint.FromBytes(amountBytes) - return util.Uint160Size*3 + 8 + 4 + (8 + len(amountBytes)) + +util.Uint256Size + return 4 + util.Uint160Size*2 + 8 + 4 + (8 + len(amountBytes)) + +util.Uint256Size } diff --git a/pkg/core/state/nep5_test.go b/pkg/core/state/nep5_test.go index 9681fe2ec..507e848c0 100644 --- a/pkg/core/state/nep5_test.go +++ b/pkg/core/state/nep5_test.go @@ -50,7 +50,7 @@ func TestNEP5Tracker_EncodeBinary(t *testing.T) { func TestNEP5Transfer_DecodeBinary(t *testing.T) { expected := &NEP5Transfer{ - Asset: util.Uint160{1, 2, 3}, + Asset: 123, From: util.Uint160{5, 6, 7}, To: util.Uint160{8, 9, 10}, Amount: *big.NewInt(42), @@ -78,7 +78,7 @@ func randomTransfer(r *rand.Rand) *NEP5Transfer { return &NEP5Transfer{ Amount: *big.NewInt(int64(r.Uint64())), Block: r.Uint32(), - Asset: random.Uint160(), + Asset: int32(random.Int(10, 10000000)), From: random.Uint160(), To: random.Uint160(), Tx: random.Uint256(), diff --git a/pkg/rpc/server/server.go b/pkg/rpc/server/server.go index 3593b9164..fb0de6b7a 100644 --- a/pkg/rpc/server/server.go +++ b/pkg/rpc/server/server.go @@ -549,13 +549,17 @@ func (s *Server) getNEP5Transfers(ps request.Params) (interface{}, *response.Err lg := s.chain.GetNEP5TransferLog(u) cache := make(map[util.Uint160]int64) err = lg.ForEach(func(tr *state.NEP5Transfer) error { + h, err := s.chain.GetContractScriptHash(tr.Asset) + if err != nil { + return nil + } transfer := result.NEP5Transfer{ Timestamp: tr.Timestamp, - Asset: tr.Asset, + Asset: h, Index: tr.Block, TxHash: tr.Tx, } - d, err := s.getDecimals(tr.Asset, cache) + d, err := s.getDecimals(h, cache) if err != nil { return nil } From 167bd7d9df1ff8b5d4f0de95cd3145f9891e7214 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 29 Jul 2020 15:09:59 +0300 Subject: [PATCH 5/5] rpc: store decimals by contract ID instead of hash And we should also cache contract scripthash to fill Asset field. --- pkg/rpc/server/server.go | 58 +++++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/pkg/rpc/server/server.go b/pkg/rpc/server/server.go index fb0de6b7a..5e661ac71 100644 --- a/pkg/rpc/server/server.go +++ b/pkg/rpc/server/server.go @@ -514,19 +514,15 @@ func (s *Server) getNEP5Balances(ps request.Params) (interface{}, *response.Erro Balances: []result.NEP5Balance{}, } if as != nil { - cache := make(map[util.Uint160]int64) + cache := make(map[int32]decimals) for id, bal := range as.Trackers { - h, err := s.chain.GetContractScriptHash(id) + dec, err := s.getDecimals(id, cache) if err != nil { continue } - dec, err := s.getDecimals(h, cache) - if err != nil { - continue - } - amount := amountToString(&bal.Balance, dec) + amount := amountToString(&bal.Balance, dec.Value) bs.Balances = append(bs.Balances, result.NEP5Balance{ - Asset: h, + Asset: dec.Hash, Amount: amount, LastUpdated: bal.LastUpdatedBlock, }) @@ -547,24 +543,20 @@ func (s *Server) getNEP5Transfers(ps request.Params) (interface{}, *response.Err Sent: []result.NEP5Transfer{}, } lg := s.chain.GetNEP5TransferLog(u) - cache := make(map[util.Uint160]int64) + cache := make(map[int32]decimals) err = lg.ForEach(func(tr *state.NEP5Transfer) error { - h, err := s.chain.GetContractScriptHash(tr.Asset) + d, err := s.getDecimals(tr.Asset, cache) if err != nil { return nil } transfer := result.NEP5Transfer{ Timestamp: tr.Timestamp, - Asset: h, + Asset: d.Hash, Index: tr.Block, TxHash: tr.Tx, } - d, err := s.getDecimals(h, cache) - if err != nil { - return nil - } if tr.Amount.Sign() > 0 { // token was received - transfer.Amount = amountToString(&tr.Amount, d) + transfer.Amount = amountToString(&tr.Amount, d.Value) if !tr.From.Equals(util.Uint160{}) { transfer.Address = address.Uint160ToString(tr.From) } @@ -572,7 +564,7 @@ func (s *Server) getNEP5Transfers(ps request.Params) (interface{}, *response.Err return nil } - transfer.Amount = amountToString(new(big.Int).Neg(&tr.Amount), d) + transfer.Amount = amountToString(new(big.Int).Neg(&tr.Amount), d.Value) if !tr.To.Equals(util.Uint160{}) { transfer.Address = address.Uint160ToString(tr.To) } @@ -598,10 +590,20 @@ func amountToString(amount *big.Int, decimals int64) string { return fmt.Sprintf(fs, q, r) } -func (s *Server) getDecimals(h util.Uint160, cache map[util.Uint160]int64) (int64, error) { - if d, ok := cache[h]; ok { +// decimals represents decimals value for the contract with the specified scripthash. +type decimals struct { + Hash util.Uint160 + Value int64 +} + +func (s *Server) getDecimals(contractID int32, cache map[int32]decimals) (decimals, error) { + if d, ok := cache[contractID]; ok { return d, nil } + h, err := s.chain.GetContractScriptHash(contractID) + if err != nil { + return decimals{}, err + } script, err := request.CreateFunctionInvocationScript(h, request.Params{ { Type: request.StringT, @@ -613,26 +615,26 @@ func (s *Server) getDecimals(h util.Uint160, cache map[util.Uint160]int64) (int6 }, }) if err != nil { - return 0, fmt.Errorf("can't create script: %v", err) + return decimals{}, fmt.Errorf("can't create script: %v", err) } res := s.runScriptInVM(script, nil) if res == nil || res.State != "HALT" || len(res.Stack) == 0 { - return 0, errors.New("execution error : no result") + return decimals{}, errors.New("execution error : no result") } - var d int64 + d := decimals{Hash: h} switch item := res.Stack[len(res.Stack)-1]; item.Type { case smartcontract.IntegerType: - d = item.Value.(int64) + d.Value = item.Value.(int64) case smartcontract.ByteArrayType: - d = bigint.FromBytes(item.Value.([]byte)).Int64() + d.Value = bigint.FromBytes(item.Value.([]byte)).Int64() default: - return 0, errors.New("invalid result: not an integer") + return d, errors.New("invalid result: not an integer") } - if d < 0 { - return 0, errors.New("incorrect result: negative result") + if d.Value < 0 { + return d, errors.New("incorrect result: negative result") } - cache[h] = d + cache[contractID] = d return d, nil }