Merge pull request #723 from nspcc-dev/feature/nep5

core,rpc: implement NEP5-related logic
This commit is contained in:
Roman Khimov 2020-03-05 18:33:58 +03:00 committed by GitHub
commit fbdc60b731
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 748 additions and 26 deletions

View file

@ -47,8 +47,8 @@ which would yield the response:
| `getclaimable` | Yes | | `getclaimable` | Yes |
| `getconnectioncount` | Yes | | `getconnectioncount` | Yes |
| `getcontractstate` | Yes | | `getcontractstate` | Yes |
| `getnep5balances` | No (#498) | | `getnep5balances` | Yes |
| `getnep5transfers` | No (#498) | | `getnep5transfers` | Yes |
| `getpeers` | Yes | | `getpeers` | Yes |
| `getrawmempool` | Yes | | `getrawmempool` | Yes |
| `getrawtransaction` | Yes | | `getrawtransaction` | Yes |

View file

@ -29,7 +29,7 @@ import (
// Tuning parameters. // Tuning parameters.
const ( const (
headerBatchCount = 2000 headerBatchCount = 2000
version = "0.0.5" version = "0.0.6"
// This one comes from C# code and it's different from the constant used // 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 // 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) amount, ok := arr[3].Value().(*big.Int)
if !ok { if !ok {
continue bs, ok := arr[3].Value().([]byte)
if !ok {
continue
}
amount = emit.BytesToInt(bs)
} }
// TODO: #498 bc.processNEP5Transfer(cache, tx, block, note.ScriptHash, from, to, amount.Int64())
_, _, _, _ = op, from, to, amount
} }
} else { } else {
bc.log.Warn("contract invocation failed", bc.log.Warn("contract invocation failed",
@ -751,6 +754,77 @@ func (bc *Blockchain) storeBlock(block *block.Block) error {
return nil 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. // LastBatch returns last persisted storage batch.
func (bc *Blockchain) LastBatch() *storage.MemBatch { func (bc *Blockchain) LastBatch() *storage.MemBatch {
return bc.lastBatch return bc.lastBatch

View file

@ -34,6 +34,7 @@ type Blockchainer interface {
GetAssetState(util.Uint256) *state.Asset GetAssetState(util.Uint256) *state.Asset
GetAccountState(util.Uint160) *state.Account GetAccountState(util.Uint160) *state.Account
GetAppExecResult(util.Uint256) (*state.AppExecResult, error) GetAppExecResult(util.Uint256) (*state.AppExecResult, error)
GetNEP5TransferLog(util.Uint160) *state.NEP5TransferLog
GetValidators(txes ...*transaction.Transaction) ([]*keys.PublicKey, error) GetValidators(txes ...*transaction.Transaction) ([]*keys.PublicKey, error)
GetScriptHashesForVerifying(*transaction.Transaction) ([]util.Uint160, error) GetScriptHashesForVerifying(*transaction.Transaction) ([]util.Uint160, error)
GetStorageItem(scripthash util.Uint160, key []byte) *state.StorageItem GetStorageItem(scripthash util.Uint160, key []byte) *state.StorageItem

View file

@ -135,6 +135,44 @@ func (dao *dao) DeleteContractState(hash util.Uint160) error {
// -- end contracts. // -- 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. // -- start unspent coins.
// GetUnspentCoinStateOrNew gets UnspentCoinState from temporary or persistent Store // GetUnspentCoinStateOrNew gets UnspentCoinState from temporary or persistent Store

View file

@ -279,6 +279,7 @@ func TestCreateBasicChain(t *testing.T) {
// Push some contract into the chain. // Push some contract into the chain.
avm, err := ioutil.ReadFile(prefix + "test_contract.avm") avm, err := ioutil.ReadFile(prefix + "test_contract.avm")
require.NoError(t, err) require.NoError(t, err)
t.Logf("contractHash: %s", hash.Hash160(avm).StringLE())
var props smartcontract.PropertyState var props smartcontract.PropertyState
script := io.NewBufBinWriter() script := io.NewBufBinWriter()
@ -352,6 +353,22 @@ func TestCreateBasicChain(t *testing.T) {
b = bc.newBlock(newMinerTX(), txNeo0to1) b = bc.newBlock(newMinerTX(), txNeo0to1)
require.NoError(t, bc.AddBlock(b)) 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 { if saveChain {
outStream, err := os.Create(prefix + "testblocks.acc") outStream, err := os.Create(prefix + "testblocks.acc")
require.NoError(t, err) 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)
}

View file

@ -35,6 +35,9 @@ type Account struct {
Votes []*keys.PublicKey Votes []*keys.PublicKey
Balances map[util.Uint256][]UnspentBalance Balances map[util.Uint256][]UnspentBalance
Unclaimed []UnclaimedBalance 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. // NewAccount returns a new Account object.
@ -46,6 +49,8 @@ func NewAccount(scriptHash util.Uint160) *Account {
Votes: []*keys.PublicKey{}, Votes: []*keys.PublicKey{},
Balances: make(map[util.Uint256][]UnspentBalance), Balances: make(map[util.Uint256][]UnspentBalance),
Unclaimed: []UnclaimedBalance{}, Unclaimed: []UnclaimedBalance{},
NEP5Balances: make(map[util.Uint160]*NEP5Tracker),
} }
} }
@ -70,6 +75,16 @@ func (s *Account) DecodeBinary(br *io.BinReader) {
} }
br.ReadArray(&s.Unclaimed) 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. // EncodeBinary encodes Account to the given BinWriter.
@ -89,6 +104,12 @@ func (s *Account) EncodeBinary(bw *io.BinWriter) {
} }
bw.WriteArray(s.Unclaimed) 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. // DecodeBinary implements io.Serializable interface.

106
pkg/core/state/nep5.go Normal file
View file

@ -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())
}

View file

@ -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)
}

View file

@ -17,6 +17,7 @@ const (
STNotification KeyPrefix = 0x4d STNotification KeyPrefix = 0x4d
STContract KeyPrefix = 0x50 STContract KeyPrefix = 0x50
STStorage KeyPrefix = 0x70 STStorage KeyPrefix = 0x70
STNEP5Transfers KeyPrefix = 0x72
IXHeaderHashList KeyPrefix = 0x80 IXHeaderHashList KeyPrefix = 0x80
IXValidatorsCount KeyPrefix = 0x90 IXValidatorsCount KeyPrefix = 0x90
SYSCurrentBlock KeyPrefix = 0xc0 SYSCurrentBlock KeyPrefix = 0xc0

View file

@ -92,6 +92,9 @@ func (chain testChain) GetAssetState(util.Uint256) *state.Asset {
func (chain testChain) GetAccountState(util.Uint160) *state.Account { func (chain testChain) GetAccountState(util.Uint160) *state.Account {
panic("TODO") panic("TODO")
} }
func (chain testChain) GetNEP5TransferLog(util.Uint160) *state.NEP5TransferLog {
panic("TODO")
}
func (chain testChain) GetValidators(...*transaction.Transaction) ([]*keys.PublicKey, error) { func (chain testChain) GetValidators(...*transaction.Transaction) ([]*keys.PublicKey, error) {
panic("TODO") panic("TODO")
} }

View file

@ -21,6 +21,8 @@ Supported methods
getaccountstate getaccountstate
getblock getblock
getclaimable getclaimable
getnep5balances
getnep5transfers
getrawtransaction getrawtransaction
getunspents getunspents
invoke invoke
@ -43,8 +45,6 @@ Unsupported methods
getconnectioncount getconnectioncount
getcontractstate getcontractstate
getmetricblocktimestamp getmetricblocktimestamp
getnep5balances
getnep5transfers
getnewaddress getnewaddress
getpeers getpeers
getrawmempool getrawmempool

View file

@ -93,6 +93,26 @@ func (c *Client) GetClaimable(address string) (*result.ClaimableInfo, error) {
return resp, nil 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. // GetRawTransaction returns a transaction by hash.
func (c *Client) GetRawTransaction(hash util.Uint256) (*transaction.Transaction, error) { func (c *Client) GetRawTransaction(hash util.Uint256) (*transaction.Transaction, error) {
var ( var (

View file

@ -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"`
}

View file

@ -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( getversionCalled = prometheus.NewCounter(
prometheus.CounterOpts{ prometheus.CounterOpts{
Help: "Number of calls to getversion rpc endpoint", Help: "Number of calls to getversion rpc endpoint",

View file

@ -5,6 +5,8 @@ import (
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
"math"
"math/big"
"net/http" "net/http"
"strconv" "strconv"
@ -20,6 +22,8 @@ import (
"github.com/nspcc-dev/neo-go/pkg/rpc/response" "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/rpc/response/result"
"github.com/nspcc-dev/neo-go/pkg/util" "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" "github.com/pkg/errors"
"go.uber.org/zap" "go.uber.org/zap"
) )
@ -202,6 +206,14 @@ Methods:
getconnectioncountCalled.Inc() getconnectioncountCalled.Inc()
results = s.coreServer.PeerCount() results = s.coreServer.PeerCount()
case "getnep5balances":
getnep5balancesCalled.Inc()
results, resultsErr = s.getNEP5Balances(reqParams)
case "getnep5transfers":
getnep5transfersCalled.Inc()
results, resultsErr = s.getNEP5Transfers(reqParams)
case "getversion": case "getversion":
getversionCalled.Inc() getversionCalled.Inc()
results = result.Version{ results = result.Version{
@ -385,6 +397,130 @@ func (s *Server) getClaimable(ps request.Params) (interface{}, error) {
}, nil }, 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) { func (s *Server) getStorage(ps request.Params) (interface{}, error) {
param, ok := ps.Value(0) param, ok := ps.Value(0)
if !ok { if !ok {

View file

@ -15,6 +15,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/core" "github.com/nspcc-dev/neo-go/pkg/core"
"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/encoding/address"
"github.com/nspcc-dev/neo-go/pkg/internal/random" "github.com/nspcc-dev/neo-go/pkg/internal/random"
"github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/rpc/response" "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{}) check func(t *testing.T, e *executor, result interface{})
} }
const testContractHash = "d864728bdbc88da799bc43862ae6aaa62adc3a87"
var rpcTestCases = map[string][]rpcTestCase{ var rpcTestCases = map[string][]rpcTestCase{
"getapplicationlog": { "getapplicationlog": {
{ {
name: "positive", name: "positive",
params: `["2441c2776cbab65bf81d38a839cf3a85689421631d4ba091be64703f02867315"]`, params: `["440b84d1580e36e84379416b58d9a3ad978cc557e54fd7ec6a2648329975b333"]`,
result: func(e *executor) interface{} { return &result.ApplicationLog{} }, result: func(e *executor) interface{} { return &result.ApplicationLog{} },
check: func(t *testing.T, e *executor, acc interface{}) { check: func(t *testing.T, e *executor, acc interface{}) {
res, ok := acc.(*result.ApplicationLog) res, ok := acc.(*result.ApplicationLog)
require.True(t, ok) require.True(t, ok)
expectedTxHash, err := util.Uint256DecodeStringLE("2441c2776cbab65bf81d38a839cf3a85689421631d4ba091be64703f02867315") expectedTxHash, err := util.Uint256DecodeStringLE("440b84d1580e36e84379416b58d9a3ad978cc557e54fd7ec6a2648329975b333")
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, expectedTxHash, res.TxHash) assert.Equal(t, expectedTxHash, res.TxHash)
assert.Equal(t, 1, len(res.Executions)) assert.Equal(t, 1, len(res.Executions))
@ -119,13 +122,13 @@ var rpcTestCases = map[string][]rpcTestCase{
"getcontractstate": { "getcontractstate": {
{ {
name: "positive", name: "positive",
params: `["1a696b32e239dd5eace3f025cac0a193a5746a27"]`, params: fmt.Sprintf(`["%s"]`, testContractHash),
result: func(e *executor) interface{} { return &result.ContractState{} }, result: func(e *executor) interface{} { return &result.ContractState{} },
check: func(t *testing.T, e *executor, cs interface{}) { check: func(t *testing.T, e *executor, cs interface{}) {
res, ok := cs.(*result.ContractState) res, ok := cs.(*result.ContractState)
require.True(t, ok) require.True(t, ok)
assert.Equal(t, byte(0), res.Version) 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) assert.Equal(t, "0.99", res.CodeVersion)
}, },
}, },
@ -145,10 +148,72 @@ var rpcTestCases = map[string][]rpcTestCase{
fail: true, 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": { "getstorage": {
{ {
name: "positive", name: "positive",
params: `["1a696b32e239dd5eace3f025cac0a193a5746a27", "746573746b6579"]`, params: fmt.Sprintf(`["%s", "746573746b6579"]`, testContractHash),
result: func(e *executor) interface{} { result: func(e *executor) interface{} {
v := hex.EncodeToString([]byte("testvalue")) v := hex.EncodeToString([]byte("testvalue"))
return &v return &v
@ -156,7 +221,7 @@ var rpcTestCases = map[string][]rpcTestCase{
}, },
{ {
name: "missing key", name: "missing key",
params: `["1a696b32e239dd5eace3f025cac0a193a5746a27", "7465"]`, params: fmt.Sprintf(`["%s", "7465"]`, testContractHash),
result: func(e *executor) interface{} { result: func(e *executor) interface{} {
v := "" v := ""
return &v return &v
@ -169,7 +234,7 @@ var rpcTestCases = map[string][]rpcTestCase{
}, },
{ {
name: "no second parameter", name: "no second parameter",
params: `["1a696b32e239dd5eace3f025cac0a193a5746a27"]`, params: fmt.Sprintf(`["%s"]`, testContractHash),
fail: true, fail: true,
}, },
{ {
@ -179,7 +244,7 @@ var rpcTestCases = map[string][]rpcTestCase{
}, },
{ {
name: "invalid key", name: "invalid key",
params: `["1a696b32e239dd5eace3f025cac0a193a5746a27", "notahex"]`, params: fmt.Sprintf(`["%s", "notahex"]`, testContractHash),
fail: true, fail: true,
}, },
}, },

Binary file not shown.

View file

@ -1,9 +1,82 @@
package testdata 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{} { func Main(operation string, args []interface{}) interface{} {
ctx := storage.GetContext() runtime.Notify([]interface{}{"contract call", operation, args})
storage.Put(ctx, args[0].([]byte), args[1].([]byte)) switch operation {
return true 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")
}
} }

Binary file not shown.

View file

@ -72,16 +72,13 @@ func (p *Parameter) MarshalJSON() ([]byte, error) {
resultRawValue, resultErr = json.Marshal(hex.EncodeToString(p.Value.([]byte))) resultRawValue, resultErr = json.Marshal(hex.EncodeToString(p.Value.([]byte)))
} }
case ArrayType: case ArrayType:
var value = make([]rawParameter, 0) var value = make([]json.RawMessage, 0)
for _, parameter := range p.Value.([]Parameter) { for _, parameter := range p.Value.([]Parameter) {
rawValue, err := json.Marshal(parameter.Value) rawValue, err := json.Marshal(&parameter)
if err != nil { if err != nil {
return nil, err return nil, err
} }
value = append(value, rawParameter{ value = append(value, rawValue)
Type: parameter.Type,
Value: rawValue,
})
} }
resultRawValue, resultErr = json.Marshal(value) resultRawValue, resultErr = json.Marshal(value)
case MapType: case MapType:

View file

@ -51,6 +51,22 @@ var marshalJSONTestCases = []struct {
}, },
result: `{"type":"Array","value":[{"type":"String","value":"str 1"},{"type":"Integer","value":2}]}`, 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{ input: Parameter{
Type: MapType, Type: MapType,