Merge pull request #723 from nspcc-dev/feature/nep5
core,rpc: implement NEP5-related logic
This commit is contained in:
commit
fbdc60b731
21 changed files with 748 additions and 26 deletions
|
@ -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 |
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
106
pkg/core/state/nep5.go
Normal file
106
pkg/core/state/nep5.go
Normal 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())
|
||||
}
|
89
pkg/core/state/nep5_test.go
Normal file
89
pkg/core/state/nep5_test.go
Normal 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)
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 (
|
||||
|
|
34
pkg/rpc/response/result/nep5.go
Normal file
34
pkg/rpc/response/result/nep5.go
Normal 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"`
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
|
|
BIN
pkg/rpc/server/testdata/test_contract.avm
vendored
BIN
pkg/rpc/server/testdata/test_contract.avm
vendored
Binary file not shown.
81
pkg/rpc/server/testdata/test_contract.go
vendored
81
pkg/rpc/server/testdata/test_contract.go
vendored
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
BIN
pkg/rpc/server/testdata/testblocks.acc
vendored
BIN
pkg/rpc/server/testdata/testblocks.acc
vendored
Binary file not shown.
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue