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 |
|
| `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 |
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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
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
|
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
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
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(
|
getversionCalled = prometheus.NewCounter(
|
||||||
prometheus.CounterOpts{
|
prometheus.CounterOpts{
|
||||||
Help: "Number of calls to getversion rpc endpoint",
|
Help: "Number of calls to getversion rpc endpoint",
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
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
|
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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
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)))
|
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(¶meter)
|
||||||
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:
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in a new issue