From 547bd3bde30380607cac3f84a43a846a8661665a Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Thu, 5 Mar 2020 15:39:53 +0300 Subject: [PATCH] rpc: display for NEP5 token amount properly Every NEP5 contract MUST have `decimals` method which is used to properly display token amount. --- pkg/rpc/server/server.go | 68 +++++++++++++++++++++++++++++++++-- pkg/rpc/server/server_test.go | 6 ++-- 2 files changed, 68 insertions(+), 6 deletions(-) diff --git a/pkg/rpc/server/server.go b/pkg/rpc/server/server.go index 9d13d33f0..d5430025b 100644 --- a/pkg/rpc/server/server.go +++ b/pkg/rpc/server/server.go @@ -5,6 +5,8 @@ import ( "encoding/hex" "encoding/json" "fmt" + "math" + "math/big" "net/http" "strconv" @@ -20,6 +22,8 @@ import ( "github.com/nspcc-dev/neo-go/pkg/rpc/response" "github.com/nspcc-dev/neo-go/pkg/rpc/response/result" "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" "github.com/pkg/errors" "go.uber.org/zap" ) @@ -402,8 +406,13 @@ func (s *Server) getNEP5Balances(ps request.Params) (interface{}, error) { as := s.chain.GetAccountState(u) bs := &result.NEP5Balances{Address: address.Uint160ToString(u)} if as != nil { + cache := make(map[util.Uint160]int64) for h, bal := range as.NEP5Balances { - amount := strconv.FormatInt(bal.Balance, 10) + dec, err := s.getDecimals(h, cache) + if err != nil { + continue + } + amount := amountToString(bal.Balance, dec) bs.Balances = append(bs.Balances, result.NEP5Balance{ Asset: h, Amount: amount, @@ -426,6 +435,7 @@ func (s *Server) getNEP5Transfers(ps request.Params) (interface{}, error) { bs := &result.NEP5Transfers{Address: address.Uint160ToString(u)} lg := s.chain.GetNEP5TransferLog(u) + cache := make(map[util.Uint160]int64) err = lg.ForEach(func(tr *state.NEP5Transfer) error { transfer := result.NEP5Transfer{ Timestamp: tr.Timestamp, @@ -433,8 +443,12 @@ func (s *Server) getNEP5Transfers(ps request.Params) (interface{}, error) { Index: tr.Block, TxHash: tr.Tx, } + d, err := s.getDecimals(tr.Asset, cache) + if err != nil { + return nil + } if tr.Amount > 0 { // token was received - transfer.Amount = strconv.FormatInt(tr.Amount, 10) + transfer.Amount = amountToString(tr.Amount, d) if !tr.From.Equals(util.Uint160{}) { transfer.Address = address.Uint160ToString(tr.From) } @@ -442,7 +456,7 @@ func (s *Server) getNEP5Transfers(ps request.Params) (interface{}, error) { return nil } - transfer.Amount = strconv.FormatInt(-tr.Amount, 10) + transfer.Amount = amountToString(-tr.Amount, d) if !tr.From.Equals(util.Uint160{}) { transfer.Address = address.Uint160ToString(tr.To) } @@ -455,6 +469,54 @@ func (s *Server) getNEP5Transfers(ps request.Params) (interface{}, error) { return bs, nil } +func amountToString(amount int64, decimals int64) string { + if decimals == 0 { + return strconv.FormatInt(amount, 10) + } + pow := int64(math.Pow10(int(decimals))) + q := amount / pow + r := amount % pow + if r == 0 { + return strconv.FormatInt(q, 10) + } + fs := fmt.Sprintf("%%d.%%0%dd", decimals) + 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 { + return d, nil + } + w := io.NewBufBinWriter() + emit.Int(w.BinWriter, 0) + emit.Opcode(w.BinWriter, opcode.NEWARRAY) + emit.String(w.BinWriter, "decimals") + emit.AppCall(w.BinWriter, h, true) + v, _ := s.chain.GetTestVM() + v.LoadScript(w.Bytes()) + if err := v.Run(); err != nil { + return 0, err + } + res := v.PopResult() + if res == nil { + return 0, errors.New("invalid result") + } + bi, ok := res.(*big.Int) + if !ok { + bs, ok := res.([]byte) + if !ok { + return 0, errors.New("invalid result") + } + bi = emit.BytesToInt(bs) + } + d := bi.Int64() + if d < 0 { + return 0, errors.New("negative decimals") + } + cache[h] = d + return d, nil +} + func (s *Server) getStorage(ps request.Params) (interface{}, error) { param, ok := ps.Value(0) if !ok { diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go index c26c98332..ca9772a1a 100644 --- a/pkg/rpc/server/server_test.go +++ b/pkg/rpc/server/server_test.go @@ -165,7 +165,7 @@ var rpcTestCases = map[string][]rpcTestCase{ require.True(t, ok) require.Equal(t, "AKkkumHbBipZ46UMZJoFynJMXzSRnBvKcs", res.Address) require.Equal(t, 1, len(res.Balances)) - require.Equal(t, "877", res.Balances[0].Amount) + require.Equal(t, "8.77", res.Balances[0].Amount) require.Equal(t, "d864728bdbc88da799bc43862ae6aaa62adc3a87", res.Balances[0].Asset.StringLE()) require.Equal(t, uint32(208), res.Balances[0].LastUpdated) }, @@ -195,12 +195,12 @@ var rpcTestCases = map[string][]rpcTestCase{ require.NoError(t, err) require.Equal(t, 1, len(res.Received)) - require.Equal(t, "1000", res.Received[0].Amount) + require.Equal(t, "10", res.Received[0].Amount) require.Equal(t, assetHash, res.Received[0].Asset) require.Equal(t, address.Uint160ToString(assetHash), res.Received[0].Address) require.Equal(t, 1, len(res.Sent)) - require.Equal(t, "123", res.Sent[0].Amount) + require.Equal(t, "1.23", res.Sent[0].Amount) require.Equal(t, assetHash, res.Sent[0].Asset) require.Equal(t, "AWLYWXB8C9Lt1nHdDZJnC5cpYJjgRDLk17", res.Sent[0].Address) },