diff --git a/docs/rpc.md b/docs/rpc.md index 81bb75073..9efe72589 100644 --- a/docs/rpc.md +++ b/docs/rpc.md @@ -47,8 +47,8 @@ which would yield the response: | `getclaimable` | Yes | | `getconnectioncount` | Yes | | `getcontractstate` | Yes | -| `getnep5balances` | No (#498) | -| `getnep5transfers` | No (#498) | +| `getnep5balances` | Yes | +| `getnep5transfers` | Yes | | `getpeers` | Yes | | `getrawmempool` | Yes | | `getrawtransaction` | Yes | diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index f9e930737..6c6118284 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -29,7 +29,7 @@ import ( // Tuning parameters. const ( headerBatchCount = 2000 - version = "0.0.5" + version = "0.0.6" // This one comes from C# code and it's different from the constant used // when creating an asset with Neo.Asset.Create interop call. It looks @@ -708,10 +708,13 @@ func (bc *Blockchain) storeBlock(block *block.Block) error { } amount, ok := arr[3].Value().(*big.Int) if !ok { - continue + bs, ok := arr[3].Value().([]byte) + if !ok { + continue + } + amount = emit.BytesToInt(bs) } - // TODO: #498 - _, _, _, _ = op, from, to, amount + bc.processNEP5Transfer(cache, tx, block, note.ScriptHash, from, to, amount.Int64()) } } else { bc.log.Warn("contract invocation failed", @@ -751,6 +754,77 @@ func (bc *Blockchain) storeBlock(block *block.Block) error { return nil } +func parseUint160(addr []byte) util.Uint160 { + if u, err := util.Uint160DecodeBytesBE(addr); err == nil { + return u + } + return util.Uint160{} +} + +func (bc *Blockchain) processNEP5Transfer(cache *cachedDao, tx *transaction.Transaction, b *block.Block, sc util.Uint160, from, to []byte, amount int64) { + toAddr := parseUint160(to) + fromAddr := parseUint160(from) + transfer := &state.NEP5Transfer{ + Asset: sc, + From: fromAddr, + To: toAddr, + Block: b.Index, + Timestamp: b.Timestamp, + Tx: tx.Hash(), + } + if !fromAddr.Equals(util.Uint160{}) { + acc, err := cache.GetAccountStateOrNew(fromAddr) + if err != nil { + return + } + bs := acc.NEP5Balances[sc] + if bs == nil { + bs = new(state.NEP5Tracker) + acc.NEP5Balances[sc] = bs + } + bs.Balance -= amount + bs.LastUpdatedBlock = b.Index + if err := cache.PutAccountState(acc); err != nil { + return + } + + transfer.Amount = -amount + if err := cache.AppendNEP5Transfer(fromAddr, transfer); err != nil { + return + } + } + if !toAddr.Equals(util.Uint160{}) { + acc, err := cache.GetAccountStateOrNew(toAddr) + if err != nil { + return + } + bs := acc.NEP5Balances[sc] + if bs == nil { + bs = new(state.NEP5Tracker) + acc.NEP5Balances[sc] = bs + } + bs.Balance += amount + bs.LastUpdatedBlock = b.Index + if err := cache.PutAccountState(acc); err != nil { + return + } + + transfer.Amount = amount + if err := cache.AppendNEP5Transfer(toAddr, transfer); err != nil { + return + } + } +} + +// GetNEP5TransferLog returns NEP5 transfer log for the acc. +func (bc *Blockchain) GetNEP5TransferLog(acc util.Uint160) *state.NEP5TransferLog { + lg, err := bc.dao.GetNEP5TransferLog(acc) + if err != nil { + return nil + } + return lg +} + // LastBatch returns last persisted storage batch. func (bc *Blockchain) LastBatch() *storage.MemBatch { return bc.lastBatch diff --git a/pkg/core/blockchainer.go b/pkg/core/blockchainer.go index 968ab66a0..0c2aa0065 100644 --- a/pkg/core/blockchainer.go +++ b/pkg/core/blockchainer.go @@ -34,6 +34,7 @@ type Blockchainer interface { GetAssetState(util.Uint256) *state.Asset GetAccountState(util.Uint160) *state.Account GetAppExecResult(util.Uint256) (*state.AppExecResult, error) + GetNEP5TransferLog(util.Uint160) *state.NEP5TransferLog GetValidators(txes ...*transaction.Transaction) ([]*keys.PublicKey, error) GetScriptHashesForVerifying(*transaction.Transaction) ([]util.Uint160, error) GetStorageItem(scripthash util.Uint160, key []byte) *state.StorageItem diff --git a/pkg/core/dao.go b/pkg/core/dao.go index 34e4dab1e..d64b1426c 100644 --- a/pkg/core/dao.go +++ b/pkg/core/dao.go @@ -135,6 +135,44 @@ func (dao *dao) DeleteContractState(hash util.Uint160) error { // -- end contracts. +// -- start transfer log. + +// GetNEP5TransferLog retrieves transfer log from the cache. +func (dao *dao) GetNEP5TransferLog(acc util.Uint160) (*state.NEP5TransferLog, error) { + key := storage.AppendPrefix(storage.STNEP5Transfers, acc.BytesBE()) + value, err := dao.store.Get(key) + if err != nil { + if err == storage.ErrKeyNotFound { + return new(state.NEP5TransferLog), nil + } + return nil, err + } + return &state.NEP5TransferLog{Raw: value}, nil +} + +// PutNEP5TransferLog saves given transfer log in the cache. +func (dao *dao) PutNEP5TransferLog(acc util.Uint160, lg *state.NEP5TransferLog) error { + key := storage.AppendPrefix(storage.STNEP5Transfers, acc.BytesBE()) + return dao.store.Put(key, lg.Raw) +} + +// AppendNEP5Transfer appends a single NEP5 transfer to a log. +func (dao *dao) AppendNEP5Transfer(acc util.Uint160, tr *state.NEP5Transfer) error { + lg, err := dao.GetNEP5TransferLog(acc) + if err != nil { + if err != storage.ErrKeyNotFound { + return err + } + lg = new(state.NEP5TransferLog) + } + if err := lg.Append(tr); err != nil { + return err + } + return dao.PutNEP5TransferLog(acc, lg) +} + +// -- end transfer log. + // -- start unspent coins. // GetUnspentCoinStateOrNew gets UnspentCoinState from temporary or persistent Store diff --git a/pkg/core/helper_test.go b/pkg/core/helper_test.go index fc82226cb..f8798f164 100644 --- a/pkg/core/helper_test.go +++ b/pkg/core/helper_test.go @@ -279,6 +279,7 @@ func TestCreateBasicChain(t *testing.T) { // Push some contract into the chain. avm, err := ioutil.ReadFile(prefix + "test_contract.avm") require.NoError(t, err) + t.Logf("contractHash: %s", hash.Hash160(avm).StringLE()) var props smartcontract.PropertyState script := io.NewBufBinWriter() @@ -352,6 +353,22 @@ func TestCreateBasicChain(t *testing.T) { b = bc.newBlock(newMinerTX(), txNeo0to1) require.NoError(t, bc.AddBlock(b)) + sh := hash.Hash160(avm) + w := io.NewBufBinWriter() + emit.Int(w.BinWriter, 0) + emit.Opcode(w.BinWriter, opcode.NEWARRAY) + emit.String(w.BinWriter, "init") + emit.AppCall(w.BinWriter, sh, true) + initTx := transaction.NewInvocationTX(w.Bytes(), 0) + transferTx := newNEP5Transfer(sh, sh, priv0.GetScriptHash(), 1000) + + b = bc.newBlock(newMinerTX(), initTx, transferTx) + require.NoError(t, bc.AddBlock(b)) + + transferTx = newNEP5Transfer(sh, priv0.GetScriptHash(), priv1.GetScriptHash(), 123) + b = bc.newBlock(newMinerTX(), transferTx) + require.NoError(t, bc.AddBlock(b)) + if saveChain { outStream, err := os.Create(prefix + "testblocks.acc") require.NoError(t, err) @@ -375,3 +392,18 @@ func TestCreateBasicChain(t *testing.T) { } } } + +func newNEP5Transfer(sc, from, to util.Uint160, amount int64) *transaction.Transaction { + w := io.NewBufBinWriter() + emit.Int(w.BinWriter, amount) + emit.Bytes(w.BinWriter, to.BytesBE()) + emit.Bytes(w.BinWriter, from.BytesBE()) + emit.Int(w.BinWriter, 3) + emit.Opcode(w.BinWriter, opcode.PACK) + emit.String(w.BinWriter, "transfer") + emit.AppCall(w.BinWriter, sc, false) + emit.Opcode(w.BinWriter, opcode.THROWIFNOT) + + script := w.Bytes() + return transaction.NewInvocationTX(script, 0) +} diff --git a/pkg/core/state/account.go b/pkg/core/state/account.go index 828ce5bcc..a857e0cae 100644 --- a/pkg/core/state/account.go +++ b/pkg/core/state/account.go @@ -35,6 +35,9 @@ type Account struct { Votes []*keys.PublicKey Balances map[util.Uint256][]UnspentBalance Unclaimed []UnclaimedBalance + // NEP5Balances is a map of the NEP5 contract hashes + // to the corresponding structures. + NEP5Balances map[util.Uint160]*NEP5Tracker } // NewAccount returns a new Account object. @@ -46,6 +49,8 @@ func NewAccount(scriptHash util.Uint160) *Account { Votes: []*keys.PublicKey{}, Balances: make(map[util.Uint256][]UnspentBalance), Unclaimed: []UnclaimedBalance{}, + + NEP5Balances: make(map[util.Uint160]*NEP5Tracker), } } @@ -70,6 +75,16 @@ func (s *Account) DecodeBinary(br *io.BinReader) { } br.ReadArray(&s.Unclaimed) + + lenBalances = br.ReadVarUint() + s.NEP5Balances = make(map[util.Uint160]*NEP5Tracker, lenBalances) + for i := 0; i < int(lenBalances); i++ { + var key util.Uint160 + var tr NEP5Tracker + br.ReadBytes(key[:]) + tr.DecodeBinary(br) + s.NEP5Balances[key] = &tr + } } // EncodeBinary encodes Account to the given BinWriter. @@ -89,6 +104,12 @@ func (s *Account) EncodeBinary(bw *io.BinWriter) { } bw.WriteArray(s.Unclaimed) + + bw.WriteVarUint(uint64(len(s.NEP5Balances))) + for k, v := range s.NEP5Balances { + bw.WriteBytes(k[:]) + v.EncodeBinary(bw) + } } // DecodeBinary implements io.Serializable interface. diff --git a/pkg/core/state/nep5.go b/pkg/core/state/nep5.go new file mode 100644 index 000000000..589a3d89c --- /dev/null +++ b/pkg/core/state/nep5.go @@ -0,0 +1,106 @@ +package state + +import ( + "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/util" +) + +// NEP5Tracker contains info about a single account in a NEP5 contract. +type NEP5Tracker struct { + // Balance is the current balance of the account. + Balance int64 + // LastUpdatedBlock is a number of block when last `transfer` to or from the + // account occured. + LastUpdatedBlock uint32 +} + +// NEP5TransferLog is a log of NEP5 token transfers for the specific command. +type NEP5TransferLog struct { + Raw []byte +} + +// NEP5TransferSize is a size of a marshaled NEP5Transfer struct in bytes. +const NEP5TransferSize = util.Uint160Size*3 + 8 + 4 + 4 + util.Uint256Size + +// NEP5Transfer represents a single NEP5 Transfer event. +type NEP5Transfer struct { + // Asset is a NEP5 contract hash. + Asset util.Uint160 + // Address is the address of the sender. + From util.Uint160 + // To is the address of the receiver. + To util.Uint160 + // Amount is the amount of tokens transferred. + // It is negative when tokens are sent and positive if they are received. + Amount int64 + // Block is a number of block when the event occured. + Block uint32 + // Timestamp is the timestamp of the block where transfer occured. + Timestamp uint32 + // Tx is a hash the transaction. + Tx util.Uint256 +} + +// Append appends single transfer to a log. +func (lg *NEP5TransferLog) Append(tr *NEP5Transfer) error { + w := io.NewBufBinWriter() + tr.EncodeBinary(w.BinWriter) + if w.Err != nil { + return w.Err + } + lg.Raw = append(lg.Raw, w.Bytes()...) + return nil +} + +// ForEach iterates over transfer log returning on first error. +func (lg *NEP5TransferLog) ForEach(f func(*NEP5Transfer) error) error { + if lg == nil { + return nil + } + tr := new(NEP5Transfer) + for i := 0; i < len(lg.Raw); i += NEP5TransferSize { + r := io.NewBinReaderFromBuf(lg.Raw[i : i+NEP5TransferSize]) + tr.DecodeBinary(r) + if r.Err != nil { + return r.Err + } else if err := f(tr); err != nil { + return nil + } + } + return nil +} + +// EncodeBinary implements io.Serializable interface. +func (t *NEP5Tracker) EncodeBinary(w *io.BinWriter) { + w.WriteU64LE(uint64(t.Balance)) + w.WriteU32LE(t.LastUpdatedBlock) +} + +// DecodeBinary implements io.Serializable interface. +func (t *NEP5Tracker) DecodeBinary(r *io.BinReader) { + t.Balance = int64(r.ReadU64LE()) + t.LastUpdatedBlock = r.ReadU32LE() +} + +// EncodeBinary implements io.Serializable interface. +// Note: change NEP5TransferSize constant when changing this function. +func (t *NEP5Transfer) EncodeBinary(w *io.BinWriter) { + w.WriteBytes(t.Asset[:]) + w.WriteBytes(t.Tx[:]) + w.WriteBytes(t.From[:]) + w.WriteBytes(t.To[:]) + w.WriteU32LE(t.Block) + w.WriteU32LE(t.Timestamp) + w.WriteU64LE(uint64(t.Amount)) +} + +// DecodeBinary implements io.Serializable interface. +func (t *NEP5Transfer) DecodeBinary(r *io.BinReader) { + r.ReadBytes(t.Asset[:]) + r.ReadBytes(t.Tx[:]) + r.ReadBytes(t.From[:]) + r.ReadBytes(t.To[:]) + t.Block = r.ReadU32LE() + t.Timestamp = r.ReadU32LE() + t.Amount = int64(r.ReadU64LE()) +} diff --git a/pkg/core/state/nep5_test.go b/pkg/core/state/nep5_test.go new file mode 100644 index 000000000..03cf14e57 --- /dev/null +++ b/pkg/core/state/nep5_test.go @@ -0,0 +1,89 @@ +package state + +import ( + gio "io" + "math/rand" + "testing" + "time" + + "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/stretchr/testify/require" +) + +func TestNEP5TransferLog_Append(t *testing.T) { + r := rand.New(rand.NewSource(time.Now().UnixNano())) + expected := []*NEP5Transfer{ + randomTransfer(t, r), + randomTransfer(t, r), + randomTransfer(t, r), + randomTransfer(t, r), + } + + lg := new(NEP5TransferLog) + for _, tr := range expected { + require.NoError(t, lg.Append(tr)) + } + + i := 0 + err := lg.ForEach(func(tr *NEP5Transfer) error { + require.Equal(t, expected[i], tr) + i++ + return nil + }) + require.NoError(t, err) + +} + +func TestNEP5Tracker_EncodeBinary(t *testing.T) { + expected := &NEP5Tracker{ + Balance: int64(rand.Uint64()), + LastUpdatedBlock: rand.Uint32(), + } + + testEncodeDecode(t, expected, new(NEP5Tracker)) +} + +func TestNEP5Transfer_DecodeBinary(t *testing.T) { + expected := &NEP5Transfer{ + Asset: util.Uint160{1, 2, 3}, + From: util.Uint160{5, 6, 7}, + To: util.Uint160{8, 9, 10}, + Amount: 42, + Block: 12345, + Timestamp: 54321, + Tx: util.Uint256{8, 5, 3}, + } + + testEncodeDecode(t, expected, new(NEP5Transfer)) +} + +func randomTransfer(t *testing.T, r *rand.Rand) *NEP5Transfer { + tr := &NEP5Transfer{ + Amount: int64(r.Uint64()), + Block: r.Uint32(), + } + + var err error + _, err = gio.ReadFull(r, tr.Asset[:]) + require.NoError(t, err) + _, err = gio.ReadFull(r, tr.From[:]) + require.NoError(t, err) + _, err = gio.ReadFull(r, tr.To[:]) + require.NoError(t, err) + _, err = gio.ReadFull(r, tr.Tx[:]) + require.NoError(t, err) + + return tr +} + +func testEncodeDecode(t *testing.T, expected, actual io.Serializable) { + w := io.NewBufBinWriter() + expected.EncodeBinary(w.BinWriter) + require.NoError(t, w.Err) + + r := io.NewBinReaderFromBuf(w.Bytes()) + actual.DecodeBinary(r) + require.NoError(t, r.Err) + require.Equal(t, expected, actual) +} diff --git a/pkg/core/storage/store.go b/pkg/core/storage/store.go index 5ec0f9d67..a36523f4d 100644 --- a/pkg/core/storage/store.go +++ b/pkg/core/storage/store.go @@ -17,6 +17,7 @@ const ( STNotification KeyPrefix = 0x4d STContract KeyPrefix = 0x50 STStorage KeyPrefix = 0x70 + STNEP5Transfers KeyPrefix = 0x72 IXHeaderHashList KeyPrefix = 0x80 IXValidatorsCount KeyPrefix = 0x90 SYSCurrentBlock KeyPrefix = 0xc0 diff --git a/pkg/network/helper_test.go b/pkg/network/helper_test.go index 21161dc0c..ff2edbc4b 100644 --- a/pkg/network/helper_test.go +++ b/pkg/network/helper_test.go @@ -92,6 +92,9 @@ func (chain testChain) GetAssetState(util.Uint256) *state.Asset { func (chain testChain) GetAccountState(util.Uint160) *state.Account { panic("TODO") } +func (chain testChain) GetNEP5TransferLog(util.Uint160) *state.NEP5TransferLog { + panic("TODO") +} func (chain testChain) GetValidators(...*transaction.Transaction) ([]*keys.PublicKey, error) { panic("TODO") } diff --git a/pkg/rpc/client/doc.go b/pkg/rpc/client/doc.go index 1df4f0682..888ef0a54 100644 --- a/pkg/rpc/client/doc.go +++ b/pkg/rpc/client/doc.go @@ -21,6 +21,8 @@ Supported methods getaccountstate getblock getclaimable + getnep5balances + getnep5transfers getrawtransaction getunspents invoke @@ -43,8 +45,6 @@ Unsupported methods getconnectioncount getcontractstate getmetricblocktimestamp - getnep5balances - getnep5transfers getnewaddress getpeers getrawmempool diff --git a/pkg/rpc/client/rpc.go b/pkg/rpc/client/rpc.go index beeef25cf..d30148ee4 100644 --- a/pkg/rpc/client/rpc.go +++ b/pkg/rpc/client/rpc.go @@ -93,6 +93,26 @@ func (c *Client) GetClaimable(address string) (*result.ClaimableInfo, error) { return resp, nil } +// GetNEP5Balances is a wrapper for getnep5balances RPC. +func (c *Client) GetNEP5Balances(address util.Uint160) (*result.NEP5Balances, error) { + params := request.NewRawParams(address.StringLE()) + resp := new(result.NEP5Balances) + if err := c.performRequest("getnep5balances", params, resp); err != nil { + return nil, err + } + return resp, nil +} + +// GetNEP5Transfers is a wrapper for getnep5transfers RPC. +func (c *Client) GetNEP5Transfers(address string) (*result.NEP5Transfers, error) { + params := request.NewRawParams(address) + resp := new(result.NEP5Transfers) + if err := c.performRequest("getnep5transfers", params, resp); err != nil { + return nil, err + } + return resp, nil +} + // GetRawTransaction returns a transaction by hash. func (c *Client) GetRawTransaction(hash util.Uint256) (*transaction.Transaction, error) { var ( diff --git a/pkg/rpc/response/result/nep5.go b/pkg/rpc/response/result/nep5.go new file mode 100644 index 000000000..29e0bc9e9 --- /dev/null +++ b/pkg/rpc/response/result/nep5.go @@ -0,0 +1,34 @@ +package result + +import "github.com/nspcc-dev/neo-go/pkg/util" + +// NEP5Balances is a result for the getnep5balances RPC call. +type NEP5Balances struct { + Balances []NEP5Balance `json:"balances"` + Address string `json:"address"` +} + +// NEP5Balance represents balance for the single token contract. +type NEP5Balance struct { + Asset util.Uint160 `json:"asset_hash"` + Amount string `json:"amount"` + LastUpdated uint32 `json:"last_updated_block"` +} + +// NEP5Transfers is a result for the getnep5transfers RPC. +type NEP5Transfers struct { + Sent []NEP5Transfer `json:"sent"` + Received []NEP5Transfer `json:"received"` + Address string `json:"address"` +} + +// NEP5Transfer represents single NEP5 transfer event. +type NEP5Transfer struct { + Timestamp uint32 `json:"timestamp"` + Asset util.Uint160 `json:"asset_hash"` + Address string `json:"transfer_address,omitempty"` + Amount string `json:"amount"` + Index uint32 `json:"block_index"` + NotifyIndex uint32 `json:"transfer_notify_index"` + TxHash util.Uint256 `json:"tx_hash"` +} diff --git a/pkg/rpc/server/prometheus.go b/pkg/rpc/server/prometheus.go index 8e1b8c950..f8fea1207 100644 --- a/pkg/rpc/server/prometheus.go +++ b/pkg/rpc/server/prometheus.go @@ -83,6 +83,22 @@ var ( }, ) + getnep5balancesCalled = prometheus.NewCounter( + prometheus.CounterOpts{ + Help: "Number of calls to getnep5balances rpc endpoint", + Name: "getnep5balances_called", + Namespace: "neogo", + }, + ) + + getnep5transfersCalled = prometheus.NewCounter( + prometheus.CounterOpts{ + Help: "Number of calls to getnep5transfers rpc endpoint", + Name: "getnep5transfers_called", + Namespace: "neogo", + }, + ) + getversionCalled = prometheus.NewCounter( prometheus.CounterOpts{ Help: "Number of calls to getversion rpc endpoint", diff --git a/pkg/rpc/server/server.go b/pkg/rpc/server/server.go index 7e02f0d7d..93084fc39 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" ) @@ -202,6 +206,14 @@ Methods: getconnectioncountCalled.Inc() results = s.coreServer.PeerCount() + case "getnep5balances": + getnep5balancesCalled.Inc() + results, resultsErr = s.getNEP5Balances(reqParams) + + case "getnep5transfers": + getnep5transfersCalled.Inc() + results, resultsErr = s.getNEP5Transfers(reqParams) + case "getversion": getversionCalled.Inc() results = result.Version{ @@ -385,6 +397,130 @@ func (s *Server) getClaimable(ps request.Params) (interface{}, error) { }, nil } +func (s *Server) getNEP5Balances(ps request.Params) (interface{}, error) { + p, ok := ps.ValueWithType(0, request.StringT) + if !ok { + return nil, response.ErrInvalidParams + } + u, err := p.GetUint160FromHex() + if err != nil { + return nil, response.ErrInvalidParams + } + + 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 { + 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, + LastUpdated: bal.LastUpdatedBlock, + }) + } + } + return bs, nil +} + +func (s *Server) getNEP5Transfers(ps request.Params) (interface{}, error) { + p, ok := ps.ValueWithType(0, request.StringT) + if !ok { + return nil, response.ErrInvalidParams + } + u, err := p.GetUint160FromAddress() + if err != nil { + return nil, response.ErrInvalidParams + } + + 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, + Asset: tr.Asset, + 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 = amountToString(tr.Amount, d) + if !tr.From.Equals(util.Uint160{}) { + transfer.Address = address.Uint160ToString(tr.From) + } + bs.Received = append(bs.Received, transfer) + return nil + } + + transfer.Amount = amountToString(-tr.Amount, d) + if !tr.From.Equals(util.Uint160{}) { + transfer.Address = address.Uint160ToString(tr.To) + } + bs.Sent = append(bs.Sent, transfer) + return nil + }) + if err != nil { + return nil, response.NewInternalServerError("invalid NEP5 transfer log", err) + } + 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 edae9c932..a56b92091 100644 --- a/pkg/rpc/server/server_test.go +++ b/pkg/rpc/server/server_test.go @@ -15,6 +15,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core" "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/internal/random" "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/rpc/response" @@ -42,18 +43,20 @@ type rpcTestCase struct { check func(t *testing.T, e *executor, result interface{}) } +const testContractHash = "d864728bdbc88da799bc43862ae6aaa62adc3a87" + var rpcTestCases = map[string][]rpcTestCase{ "getapplicationlog": { { name: "positive", - params: `["2441c2776cbab65bf81d38a839cf3a85689421631d4ba091be64703f02867315"]`, + params: `["440b84d1580e36e84379416b58d9a3ad978cc557e54fd7ec6a2648329975b333"]`, result: func(e *executor) interface{} { return &result.ApplicationLog{} }, check: func(t *testing.T, e *executor, acc interface{}) { res, ok := acc.(*result.ApplicationLog) require.True(t, ok) - expectedTxHash, err := util.Uint256DecodeStringLE("2441c2776cbab65bf81d38a839cf3a85689421631d4ba091be64703f02867315") + expectedTxHash, err := util.Uint256DecodeStringLE("440b84d1580e36e84379416b58d9a3ad978cc557e54fd7ec6a2648329975b333") require.NoError(t, err) assert.Equal(t, expectedTxHash, res.TxHash) assert.Equal(t, 1, len(res.Executions)) @@ -119,13 +122,13 @@ var rpcTestCases = map[string][]rpcTestCase{ "getcontractstate": { { name: "positive", - params: `["1a696b32e239dd5eace3f025cac0a193a5746a27"]`, + params: fmt.Sprintf(`["%s"]`, testContractHash), result: func(e *executor) interface{} { return &result.ContractState{} }, check: func(t *testing.T, e *executor, cs interface{}) { res, ok := cs.(*result.ContractState) require.True(t, ok) assert.Equal(t, byte(0), res.Version) - assert.Equal(t, util.Uint160{0x1a, 0x69, 0x6b, 0x32, 0xe2, 0x39, 0xdd, 0x5e, 0xac, 0xe3, 0xf0, 0x25, 0xca, 0xc0, 0xa1, 0x93, 0xa5, 0x74, 0x6a, 0x27}, res.ScriptHash) + assert.Equal(t, testContractHash, res.ScriptHash.StringBE()) assert.Equal(t, "0.99", res.CodeVersion) }, }, @@ -145,10 +148,72 @@ var rpcTestCases = map[string][]rpcTestCase{ fail: true, }, }, + + "getnep5balances": { + { + name: "no params", + params: `[]`, + fail: true, + }, + { + name: "invalid address", + params: `["notahex"]`, + fail: true, + }, + { + name: "positive", + params: `["a90f00d94349a320376b7cb86c884b53ad76aa2b"]`, + result: func(e *executor) interface{} { return &result.NEP5Balances{} }, + check: func(t *testing.T, e *executor, acc interface{}) { + res, ok := acc.(*result.NEP5Balances) + require.True(t, ok) + require.Equal(t, "AKkkumHbBipZ46UMZJoFynJMXzSRnBvKcs", res.Address) + require.Equal(t, 1, len(res.Balances)) + require.Equal(t, "8.77", res.Balances[0].Amount) + require.Equal(t, testContractHash, res.Balances[0].Asset.StringLE()) + require.Equal(t, uint32(208), res.Balances[0].LastUpdated) + }, + }, + }, + "getnep5transfers": { + { + name: "no params", + params: `[]`, + fail: true, + }, + { + name: "invalid address", + params: `["notahex"]`, + fail: true, + }, + { + name: "positive", + params: `["AKkkumHbBipZ46UMZJoFynJMXzSRnBvKcs"]`, + result: func(e *executor) interface{} { return &result.NEP5Transfers{} }, + check: func(t *testing.T, e *executor, acc interface{}) { + res, ok := acc.(*result.NEP5Transfers) + require.True(t, ok) + require.Equal(t, "AKkkumHbBipZ46UMZJoFynJMXzSRnBvKcs", res.Address) + + assetHash, err := util.Uint160DecodeStringLE(testContractHash) + require.NoError(t, err) + + require.Equal(t, 1, len(res.Received)) + 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, "1.23", res.Sent[0].Amount) + require.Equal(t, assetHash, res.Sent[0].Asset) + require.Equal(t, "AWLYWXB8C9Lt1nHdDZJnC5cpYJjgRDLk17", res.Sent[0].Address) + }, + }, + }, "getstorage": { { name: "positive", - params: `["1a696b32e239dd5eace3f025cac0a193a5746a27", "746573746b6579"]`, + params: fmt.Sprintf(`["%s", "746573746b6579"]`, testContractHash), result: func(e *executor) interface{} { v := hex.EncodeToString([]byte("testvalue")) return &v @@ -156,7 +221,7 @@ var rpcTestCases = map[string][]rpcTestCase{ }, { name: "missing key", - params: `["1a696b32e239dd5eace3f025cac0a193a5746a27", "7465"]`, + params: fmt.Sprintf(`["%s", "7465"]`, testContractHash), result: func(e *executor) interface{} { v := "" return &v @@ -169,7 +234,7 @@ var rpcTestCases = map[string][]rpcTestCase{ }, { name: "no second parameter", - params: `["1a696b32e239dd5eace3f025cac0a193a5746a27"]`, + params: fmt.Sprintf(`["%s"]`, testContractHash), fail: true, }, { @@ -179,7 +244,7 @@ var rpcTestCases = map[string][]rpcTestCase{ }, { name: "invalid key", - params: `["1a696b32e239dd5eace3f025cac0a193a5746a27", "notahex"]`, + params: fmt.Sprintf(`["%s", "notahex"]`, testContractHash), fail: true, }, }, diff --git a/pkg/rpc/server/testdata/test_contract.avm b/pkg/rpc/server/testdata/test_contract.avm index 15e36929d..7d3417c3c 100755 Binary files a/pkg/rpc/server/testdata/test_contract.avm and b/pkg/rpc/server/testdata/test_contract.avm differ diff --git a/pkg/rpc/server/testdata/test_contract.go b/pkg/rpc/server/testdata/test_contract.go index 33b4d5bd3..a8da7cf8a 100644 --- a/pkg/rpc/server/testdata/test_contract.go +++ b/pkg/rpc/server/testdata/test_contract.go @@ -1,9 +1,82 @@ package testdata -import "github.com/nspcc-dev/neo-go/pkg/interop/storage" +import ( + "github.com/nspcc-dev/neo-go/pkg/interop/engine" + "github.com/nspcc-dev/neo-go/pkg/interop/runtime" + "github.com/nspcc-dev/neo-go/pkg/interop/storage" +) + +const ( + totalSupply = 1000000 + decimals = 2 +) func Main(operation string, args []interface{}) interface{} { - ctx := storage.GetContext() - storage.Put(ctx, args[0].([]byte), args[1].([]byte)) - return true + runtime.Notify([]interface{}{"contract call", operation, args}) + switch operation { + case "Put": + ctx := storage.GetContext() + storage.Put(ctx, args[0].([]byte), args[1].([]byte)) + return true + case "totalSupply": + return totalSupply + case "decimals": + return decimals + case "name": + return "Rubl" + case "symbol": + return "RUB" + case "balanceOf": + ctx := storage.GetContext() + addr := args[0].([]byte) + if len(addr) != 20 { + runtime.Log("invalid address") + return false + } + amount := storage.Get(ctx, addr).(int) + runtime.Notify([]interface{}{"balanceOf", addr, amount}) + return amount + case "transfer": + ctx := storage.GetContext() + from := args[0].([]byte) + if len(from) != 20 { + runtime.Log("invalid 'from' address") + return false + } + to := args[1].([]byte) + if len(to) != 20 { + runtime.Log("invalid 'to' address") + return false + } + amount := args[2].(int) + if amount < 0 { + runtime.Log("invalid amount") + return false + } + + fromBalance := storage.Get(ctx, from).(int) + if fromBalance < amount { + runtime.Log("insufficient funds") + return false + } + fromBalance -= amount + storage.Put(ctx, from, fromBalance) + + toBalance := storage.Get(ctx, to).(int) + toBalance += amount + storage.Put(ctx, to, toBalance) + + runtime.Notify([]interface{}{"transfer", from, to, amount}) + + return true + case "init": + ctx := storage.GetContext() + h := engine.GetExecutingScriptHash() + amount := totalSupply + storage.Put(ctx, h, amount) + runtime.Notify([]interface{}{"transfer", []byte{}, h, amount}) + return true + default: + panic("invalid operation") + } } diff --git a/pkg/rpc/server/testdata/testblocks.acc b/pkg/rpc/server/testdata/testblocks.acc index 5507af4ea..af3c153a0 100644 Binary files a/pkg/rpc/server/testdata/testblocks.acc and b/pkg/rpc/server/testdata/testblocks.acc differ diff --git a/pkg/smartcontract/parameter.go b/pkg/smartcontract/parameter.go index 498e4dc8c..fd306709d 100644 --- a/pkg/smartcontract/parameter.go +++ b/pkg/smartcontract/parameter.go @@ -72,16 +72,13 @@ func (p *Parameter) MarshalJSON() ([]byte, error) { resultRawValue, resultErr = json.Marshal(hex.EncodeToString(p.Value.([]byte))) } case ArrayType: - var value = make([]rawParameter, 0) + var value = make([]json.RawMessage, 0) for _, parameter := range p.Value.([]Parameter) { - rawValue, err := json.Marshal(parameter.Value) + rawValue, err := json.Marshal(¶meter) if err != nil { return nil, err } - value = append(value, rawParameter{ - Type: parameter.Type, - Value: rawValue, - }) + value = append(value, rawValue) } resultRawValue, resultErr = json.Marshal(value) case MapType: diff --git a/pkg/smartcontract/parameter_test.go b/pkg/smartcontract/parameter_test.go index 565ca95d6..3135ddd08 100644 --- a/pkg/smartcontract/parameter_test.go +++ b/pkg/smartcontract/parameter_test.go @@ -51,6 +51,22 @@ var marshalJSONTestCases = []struct { }, result: `{"type":"Array","value":[{"type":"String","value":"str 1"},{"type":"Integer","value":2}]}`, }, + { + input: Parameter{ + Type: ArrayType, + Value: []Parameter{ + {Type: ByteArrayType, Value: []byte{1, 2}}, + { + Type: ArrayType, + Value: []Parameter{ + {Type: ByteArrayType, Value: []byte{3, 2, 1}}, + {Type: ByteArrayType, Value: []byte{7, 8, 9}}, + }}, + }, + }, + result: `{"type":"Array","value":[{"type":"ByteArray","value":"0102"},{"type":"Array","value":[` + + `{"type":"ByteArray","value":"030201"},{"type":"ByteArray","value":"070809"}]}]}`, + }, { input: Parameter{ Type: MapType,