forked from TrueCloudLab/frostfs-node
Initial commit
Initial public review release v0.10.0
This commit is contained in:
commit
dadfd90dcd
276 changed files with 46331 additions and 0 deletions
31
lib/blockchain/event/event.go
Normal file
31
lib/blockchain/event/event.go
Normal file
|
@ -0,0 +1,31 @@
|
|||
package event
|
||||
|
||||
// Type is a notification event enumeration type.
|
||||
type Type string
|
||||
|
||||
// Event is an interface that is
|
||||
// provided by Neo:Morph event structures.
|
||||
type Event interface {
|
||||
MorphEvent()
|
||||
}
|
||||
|
||||
// Equal compares two Type values and
|
||||
// returns true if they are equal.
|
||||
func (t Type) Equal(t2 Type) bool {
|
||||
return string(t) == string(t2)
|
||||
}
|
||||
|
||||
// String returns casted to string Type.
|
||||
func (t Type) String() string {
|
||||
return string(t)
|
||||
}
|
||||
|
||||
// TypeFromBytes converts bytes slice to Type.
|
||||
func TypeFromBytes(data []byte) Type {
|
||||
return Type(data)
|
||||
}
|
||||
|
||||
// TypeFromString converts string to Type.
|
||||
func TypeFromString(str string) Type {
|
||||
return Type(str)
|
||||
}
|
22
lib/blockchain/event/handler.go
Normal file
22
lib/blockchain/event/handler.go
Normal file
|
@ -0,0 +1,22 @@
|
|||
package event
|
||||
|
||||
// Handler is an Event processing function.
|
||||
type Handler func(Event)
|
||||
|
||||
// HandlerInfo is a structure that groups
|
||||
// the parameters of the handler of particular
|
||||
// contract event.
|
||||
type HandlerInfo struct {
|
||||
scriptHashWithType
|
||||
|
||||
h Handler
|
||||
}
|
||||
|
||||
// SetHandler is an event handler setter.
|
||||
func (s *HandlerInfo) SetHandler(v Handler) {
|
||||
s.h = v
|
||||
}
|
||||
|
||||
func (s HandlerInfo) handler() Handler {
|
||||
return s.h
|
||||
}
|
309
lib/blockchain/event/listener.go
Normal file
309
lib/blockchain/event/listener.go
Normal file
|
@ -0,0 +1,309 @@
|
|||
package event
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/nspcc-dev/neo-go/pkg/rpc/response/result"
|
||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||
"github.com/nspcc-dev/neofs-node/internal"
|
||||
"github.com/nspcc-dev/neofs-node/lib/blockchain/goclient"
|
||||
"github.com/nspcc-dev/neofs-node/lib/blockchain/subscriber"
|
||||
"github.com/pkg/errors"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// Listener is an interface of smart contract notification event listener.
|
||||
type Listener interface {
|
||||
// Must start the event listener.
|
||||
//
|
||||
// Must listen to events with the parser installed.
|
||||
//
|
||||
// Must return an error if event listening could not be started.
|
||||
Listen(context.Context)
|
||||
|
||||
// Must set the parser of particular contract event.
|
||||
//
|
||||
// Parser of each event must be set once. All parsers must be set before Listen call.
|
||||
//
|
||||
// Must ignore nil parsers and all calls after listener has been started.
|
||||
SetParser(ParserInfo)
|
||||
|
||||
// Must register the event handler for particular notification event of contract.
|
||||
//
|
||||
// The specified handler must be called after each capture and parsing of the event
|
||||
//
|
||||
// Must ignore nil handlers.
|
||||
RegisterHandler(HandlerInfo)
|
||||
}
|
||||
|
||||
// ListenerParams is a group of parameters
|
||||
// for Listener constructor.
|
||||
type ListenerParams struct {
|
||||
Logger *zap.Logger
|
||||
|
||||
Subscriber subscriber.Subscriber
|
||||
}
|
||||
|
||||
type listener struct {
|
||||
mtx *sync.RWMutex
|
||||
|
||||
once *sync.Once
|
||||
|
||||
started bool
|
||||
|
||||
parsers map[scriptHashWithType]Parser
|
||||
|
||||
handlers map[scriptHashWithType][]Handler
|
||||
|
||||
log *zap.Logger
|
||||
|
||||
subscriber subscriber.Subscriber
|
||||
}
|
||||
|
||||
const (
|
||||
newListenerFailMsg = "could not instantiate Listener"
|
||||
|
||||
errNilLogger = internal.Error("nil logger")
|
||||
|
||||
errNilSubscriber = internal.Error("nil event subscriber")
|
||||
)
|
||||
|
||||
// Listen starts the listening for events with registered handlers.
|
||||
//
|
||||
// Executes once, all subsequent calls do nothing.
|
||||
//
|
||||
// Returns an error if listener was already started.
|
||||
func (s listener) Listen(ctx context.Context) {
|
||||
s.once.Do(func() {
|
||||
if err := s.listen(ctx); err != nil {
|
||||
s.log.Error("could not start listen to events",
|
||||
zap.String("error", err.Error()),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (s listener) listen(ctx context.Context) error {
|
||||
// create the list of listening contract hashes
|
||||
hashes := make([]util.Uint160, 0)
|
||||
|
||||
// fill the list with the contracts with set event parsers.
|
||||
s.mtx.RLock()
|
||||
for hashType := range s.parsers {
|
||||
scHash := hashType.scriptHash()
|
||||
|
||||
// prevent repetitions
|
||||
for _, hash := range hashes {
|
||||
if hash.Equals(scHash) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
hashes = append(hashes, hashType.scriptHash())
|
||||
}
|
||||
|
||||
// mark listener as started
|
||||
s.started = true
|
||||
|
||||
s.mtx.RUnlock()
|
||||
|
||||
chEvent, err := s.subscriber.SubscribeForNotification(hashes...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.listenLoop(ctx, chEvent)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s listener) listenLoop(ctx context.Context, chEvent <-chan *result.NotificationEvent) {
|
||||
loop:
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
s.log.Warn("stop event listener by context",
|
||||
zap.String("error", ctx.Err().Error()),
|
||||
)
|
||||
break loop
|
||||
case notifyEvent, ok := <-chEvent:
|
||||
if !ok {
|
||||
s.log.Warn("stop event listener by channel")
|
||||
break loop
|
||||
} else if notifyEvent == nil {
|
||||
s.log.Warn("nil notification event was caught")
|
||||
continue loop
|
||||
}
|
||||
|
||||
s.parseAndHandle(notifyEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s listener) parseAndHandle(notifyEvent *result.NotificationEvent) {
|
||||
log := s.log.With(
|
||||
zap.String("script hash LE", notifyEvent.Contract.StringLE()),
|
||||
)
|
||||
|
||||
// stack item must be an array of items
|
||||
arr, err := goclient.ArrayFromStackParameter(notifyEvent.Item)
|
||||
if err != nil {
|
||||
log.Warn("stack item is not an array type",
|
||||
zap.String("error", err.Error()),
|
||||
)
|
||||
|
||||
return
|
||||
} else if len(arr) == 0 {
|
||||
log.Warn("stack item array is empty")
|
||||
return
|
||||
}
|
||||
|
||||
// first item must be a byte array
|
||||
typBytes, err := goclient.BytesFromStackParameter(arr[0])
|
||||
if err != nil {
|
||||
log.Warn("first array item is not a byte array",
|
||||
zap.String("error", err.Error()),
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// calculate event type from bytes
|
||||
typEvent := TypeFromBytes(typBytes)
|
||||
|
||||
log = log.With(
|
||||
zap.Stringer("event type", typEvent),
|
||||
)
|
||||
|
||||
// get the event parser
|
||||
keyEvent := scriptHashWithType{}
|
||||
keyEvent.SetScriptHash(notifyEvent.Contract)
|
||||
keyEvent.SetType(typEvent)
|
||||
|
||||
s.mtx.RLock()
|
||||
parser, ok := s.parsers[keyEvent]
|
||||
s.mtx.RUnlock()
|
||||
|
||||
if !ok {
|
||||
log.Warn("event parser not set")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// parse the notification event
|
||||
event, err := parser(arr[1:])
|
||||
if err != nil {
|
||||
log.Warn("could not parse notification event",
|
||||
zap.String("error", err.Error()),
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// handler the event
|
||||
s.mtx.RLock()
|
||||
handlers := s.handlers[keyEvent]
|
||||
s.mtx.RUnlock()
|
||||
|
||||
if len(handlers) == 0 {
|
||||
log.Info("handlers for parsed notification event were not registered",
|
||||
zap.Any("event", event),
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
for _, handler := range handlers {
|
||||
handler(event)
|
||||
}
|
||||
}
|
||||
|
||||
// SetParser sets the parser of particular contract event.
|
||||
//
|
||||
// Ignores nil and already set parsers.
|
||||
// Ignores the parser if listener is started.
|
||||
func (s listener) SetParser(p ParserInfo) {
|
||||
log := s.log.With(
|
||||
zap.String("script hash LE", p.scriptHash().StringLE()),
|
||||
zap.Stringer("event type", p.getType()),
|
||||
)
|
||||
|
||||
parser := p.parser()
|
||||
if parser == nil {
|
||||
log.Info("ignore nil event parser")
|
||||
return
|
||||
}
|
||||
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
|
||||
// check if the listener was started
|
||||
if s.started {
|
||||
log.Warn("listener has been already started, ignore parser")
|
||||
return
|
||||
}
|
||||
|
||||
// add event parser
|
||||
if _, ok := s.parsers[p.scriptHashWithType]; !ok {
|
||||
s.parsers[p.scriptHashWithType] = p.parser()
|
||||
}
|
||||
|
||||
log.Info("registered new event parser")
|
||||
}
|
||||
|
||||
// RegisterHandler registers the handler for particular notification event of contract.
|
||||
//
|
||||
// Ignores nil handlers.
|
||||
// Ignores handlers of event without parser.
|
||||
func (s listener) RegisterHandler(p HandlerInfo) {
|
||||
log := s.log.With(
|
||||
zap.String("script hash LE", p.scriptHash().StringLE()),
|
||||
zap.Stringer("event type", p.getType()),
|
||||
)
|
||||
|
||||
handler := p.handler()
|
||||
if handler == nil {
|
||||
log.Warn("ignore nil event handler")
|
||||
return
|
||||
}
|
||||
|
||||
// check if parser was set
|
||||
s.mtx.RLock()
|
||||
_, ok := s.parsers[p.scriptHashWithType]
|
||||
s.mtx.RUnlock()
|
||||
|
||||
if !ok {
|
||||
log.Warn("ignore handler of event w/o parser")
|
||||
return
|
||||
}
|
||||
|
||||
// add event handler
|
||||
s.mtx.Lock()
|
||||
s.handlers[p.scriptHashWithType] = append(
|
||||
s.handlers[p.scriptHashWithType],
|
||||
p.handler(),
|
||||
)
|
||||
s.mtx.Unlock()
|
||||
|
||||
log.Info("registered new event handler")
|
||||
}
|
||||
|
||||
// NewListener create the notification event listener instance and returns Listener interface.
|
||||
func NewListener(p ListenerParams) (Listener, error) {
|
||||
switch {
|
||||
case p.Logger == nil:
|
||||
return nil, errors.Wrap(errNilLogger, newListenerFailMsg)
|
||||
case p.Subscriber == nil:
|
||||
return nil, errors.Wrap(errNilSubscriber, newListenerFailMsg)
|
||||
}
|
||||
|
||||
return &listener{
|
||||
mtx: new(sync.RWMutex),
|
||||
once: new(sync.Once),
|
||||
parsers: make(map[scriptHashWithType]Parser),
|
||||
handlers: make(map[scriptHashWithType][]Handler),
|
||||
log: p.Logger,
|
||||
subscriber: p.Subscriber,
|
||||
}, nil
|
||||
}
|
39
lib/blockchain/event/netmap/epoch.go
Normal file
39
lib/blockchain/event/netmap/epoch.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
package netmap
|
||||
|
||||
import (
|
||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
||||
"github.com/nspcc-dev/neofs-node/lib/blockchain/event"
|
||||
"github.com/nspcc-dev/neofs-node/lib/blockchain/goclient"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// NewEpoch is a new epoch Neo:Morph event.
|
||||
type NewEpoch struct {
|
||||
num uint64
|
||||
}
|
||||
|
||||
// MorphEvent implements Neo:Morph Event interface.
|
||||
func (NewEpoch) MorphEvent() {}
|
||||
|
||||
// EpochNumber returns new epoch number.
|
||||
func (s NewEpoch) EpochNumber() uint64 {
|
||||
return s.num
|
||||
}
|
||||
|
||||
// ParseNewEpoch is a parser of new epoch notification event.
|
||||
//
|
||||
// Result is type of NewEpoch.
|
||||
func ParseNewEpoch(prms []smartcontract.Parameter) (event.Event, error) {
|
||||
if ln := len(prms); ln != 1 {
|
||||
return nil, event.WrongNumberOfParameters(1, ln)
|
||||
}
|
||||
|
||||
prmEpochNum, err := goclient.IntFromStackParameter(prms[0])
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not get integer epoch number")
|
||||
}
|
||||
|
||||
return NewEpoch{
|
||||
num: uint64(prmEpochNum),
|
||||
}, nil
|
||||
}
|
47
lib/blockchain/event/netmap/epoch_test.go
Normal file
47
lib/blockchain/event/netmap/epoch_test.go
Normal file
|
@ -0,0 +1,47 @@
|
|||
package netmap
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
||||
"github.com/nspcc-dev/neofs-node/lib/blockchain/event"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParseNewEpoch(t *testing.T) {
|
||||
t.Run("wrong number of parameters", func(t *testing.T) {
|
||||
prms := []smartcontract.Parameter{
|
||||
{},
|
||||
{},
|
||||
}
|
||||
|
||||
_, err := ParseNewEpoch(prms)
|
||||
require.EqualError(t, err, event.WrongNumberOfParameters(1, len(prms)).Error())
|
||||
})
|
||||
|
||||
t.Run("wrong first parameter type", func(t *testing.T) {
|
||||
_, err := ParseNewEpoch([]smartcontract.Parameter{
|
||||
{
|
||||
Type: smartcontract.ByteArrayType,
|
||||
},
|
||||
})
|
||||
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("correct behavior", func(t *testing.T) {
|
||||
epochNum := uint64(100)
|
||||
|
||||
ev, err := ParseNewEpoch([]smartcontract.Parameter{
|
||||
{
|
||||
Type: smartcontract.IntegerType,
|
||||
Value: int64(epochNum),
|
||||
},
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, NewEpoch{
|
||||
num: epochNum,
|
||||
}, ev)
|
||||
})
|
||||
}
|
53
lib/blockchain/event/parser.go
Normal file
53
lib/blockchain/event/parser.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
package event
|
||||
|
||||
import (
|
||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Parser is a function that constructs Event
|
||||
// from the StackItem list.
|
||||
type Parser func([]smartcontract.Parameter) (Event, error)
|
||||
|
||||
// ParserInfo is a structure that groups
|
||||
// the parameters of particular contract
|
||||
// notification event parser.
|
||||
type ParserInfo struct {
|
||||
scriptHashWithType
|
||||
|
||||
p Parser
|
||||
}
|
||||
|
||||
type wrongPrmNumber struct {
|
||||
exp, act int
|
||||
}
|
||||
|
||||
// WrongNumberOfParameters returns an error about wrong number of smart contract parameters.
|
||||
func WrongNumberOfParameters(exp, act int) error {
|
||||
return &wrongPrmNumber{
|
||||
exp: exp,
|
||||
act: act,
|
||||
}
|
||||
}
|
||||
|
||||
func (s wrongPrmNumber) Error() string {
|
||||
return errors.Errorf("wrong parameter count: expected %d, has %d", s.exp, s.act).Error()
|
||||
}
|
||||
|
||||
// SetParser is an event parser setter.
|
||||
func (s *ParserInfo) SetParser(v Parser) {
|
||||
s.p = v
|
||||
}
|
||||
|
||||
func (s ParserInfo) parser() Parser {
|
||||
return s.p
|
||||
}
|
||||
|
||||
// SetType is an event type setter.
|
||||
func (s *ParserInfo) SetType(v Type) {
|
||||
s.typ = v
|
||||
}
|
||||
|
||||
func (s ParserInfo) getType() Type {
|
||||
return s.typ
|
||||
}
|
34
lib/blockchain/event/utils.go
Normal file
34
lib/blockchain/event/utils.go
Normal file
|
@ -0,0 +1,34 @@
|
|||
package event
|
||||
|
||||
import "github.com/nspcc-dev/neo-go/pkg/util"
|
||||
|
||||
type scriptHashValue struct {
|
||||
hash util.Uint160
|
||||
}
|
||||
|
||||
type typeValue struct {
|
||||
typ Type
|
||||
}
|
||||
|
||||
type scriptHashWithType struct {
|
||||
scriptHashValue
|
||||
typeValue
|
||||
}
|
||||
|
||||
// SetScriptHash is a script hash setter.
|
||||
func (s *scriptHashValue) SetScriptHash(v util.Uint160) {
|
||||
s.hash = v
|
||||
}
|
||||
|
||||
func (s scriptHashValue) scriptHash() util.Uint160 {
|
||||
return s.hash
|
||||
}
|
||||
|
||||
// SetType is an event type setter.
|
||||
func (s *typeValue) SetType(v Type) {
|
||||
s.typ = v
|
||||
}
|
||||
|
||||
func (s typeValue) getType() Type {
|
||||
return s.typ
|
||||
}
|
190
lib/blockchain/goclient/client.go
Normal file
190
lib/blockchain/goclient/client.go
Normal file
|
@ -0,0 +1,190 @@
|
|||
package goclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"encoding/hex"
|
||||
"time"
|
||||
|
||||
"github.com/nspcc-dev/neo-go/pkg/config/netmode"
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||
"github.com/nspcc-dev/neo-go/pkg/rpc/client"
|
||||
sc "github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
||||
crypto "github.com/nspcc-dev/neofs-crypto"
|
||||
"github.com/nspcc-dev/neofs-node/internal"
|
||||
"github.com/pkg/errors"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type (
|
||||
// Params is a group of Client's constructor parameters.
|
||||
Params struct {
|
||||
Log *zap.Logger
|
||||
Key *ecdsa.PrivateKey
|
||||
Endpoint string
|
||||
Magic netmode.Magic
|
||||
DialTimeout time.Duration
|
||||
}
|
||||
|
||||
// Client is a neo-go wrapper that provides smart-contract invocation interface.
|
||||
Client struct {
|
||||
log *zap.Logger
|
||||
cli *client.Client
|
||||
acc *wallet.Account
|
||||
}
|
||||
)
|
||||
|
||||
// ErrNilClient is returned by functions that expect
|
||||
// a non-nil Client, but received nil.
|
||||
const ErrNilClient = internal.Error("go client is nil")
|
||||
|
||||
// HaltState returned if TestInvoke function processed without panic.
|
||||
const HaltState = "HALT"
|
||||
|
||||
// ErrMissingFee is returned by functions that expect
|
||||
// a positive invocation fee, but received non-positive.
|
||||
const ErrMissingFee = internal.Error("invocation fee must be positive")
|
||||
|
||||
var (
|
||||
errNilParams = errors.New("chain/client: config was not provided to the constructor")
|
||||
|
||||
errNilLogger = errors.New("chain/client: logger was not provided to the constructor")
|
||||
|
||||
errNilKey = errors.New("chain/client: private key was not provided to the constructor")
|
||||
)
|
||||
|
||||
// Invoke invokes contract method by sending transaction into blockchain.
|
||||
// Supported args types: int64, string, util.Uint160, []byte and bool.
|
||||
//
|
||||
// If passed fee is non-positive, ErrMissingFee returns.
|
||||
func (c *Client) Invoke(contract util.Uint160, fee util.Fixed8, method string, args ...interface{}) error {
|
||||
var params []sc.Parameter
|
||||
for i := range args {
|
||||
param, err := toStackParameter(args[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
params = append(params, param)
|
||||
}
|
||||
|
||||
cosigner := []transaction.Cosigner{
|
||||
{
|
||||
Account: c.acc.PrivateKey().PublicKey().GetScriptHash(),
|
||||
Scopes: transaction.Global,
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := c.cli.InvokeFunction(contract, method, params, cosigner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(resp.Script) == 0 {
|
||||
return errors.New("chain/client: got empty invocation script from neo node")
|
||||
}
|
||||
|
||||
script, err := hex.DecodeString(resp.Script)
|
||||
if err != nil {
|
||||
return errors.New("chain/client: can't decode invocation script from neo node")
|
||||
}
|
||||
|
||||
txHash, err := c.cli.SignAndPushInvocationTx(script, c.acc, 0, fee, cosigner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.log.Debug("neo client invoke",
|
||||
zap.String("method", method),
|
||||
zap.Stringer("tx_hash", txHash))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestInvoke invokes contract method locally in neo-go node. This method should
|
||||
// be used to read data from smart-contract.
|
||||
func (c *Client) TestInvoke(contract util.Uint160, method string, args ...interface{}) ([]sc.Parameter, error) {
|
||||
var params = make([]sc.Parameter, 0, len(args))
|
||||
|
||||
for i := range args {
|
||||
p, err := toStackParameter(args[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
params = append(params, p)
|
||||
}
|
||||
|
||||
cosigner := []transaction.Cosigner{
|
||||
{
|
||||
Account: c.acc.PrivateKey().PublicKey().GetScriptHash(),
|
||||
Scopes: transaction.Global,
|
||||
},
|
||||
}
|
||||
|
||||
val, err := c.cli.InvokeFunction(contract, method, params, cosigner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if val.State != HaltState {
|
||||
return nil, errors.Errorf("chain/client: contract execution finished with state %s", val.State)
|
||||
}
|
||||
|
||||
return val.Stack, nil
|
||||
}
|
||||
|
||||
// New is a Client constructor.
|
||||
func New(ctx context.Context, p *Params) (*Client, error) {
|
||||
switch {
|
||||
case p == nil:
|
||||
return nil, errNilParams
|
||||
case p.Log == nil:
|
||||
return nil, errNilLogger
|
||||
case p.Key == nil:
|
||||
return nil, errNilKey
|
||||
}
|
||||
|
||||
privKeyBytes := crypto.MarshalPrivateKey(p.Key)
|
||||
|
||||
wif, err := keys.WIFEncode(privKeyBytes, keys.WIFVersion, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
account, err := wallet.NewAccountFromWIF(wif)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cli, err := client.New(ctx, p.Endpoint, client.Options{
|
||||
DialTimeout: p.DialTimeout,
|
||||
Network: p.Magic,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Client{log: p.Log, cli: cli, acc: account}, nil
|
||||
}
|
||||
|
||||
func toStackParameter(value interface{}) (sc.Parameter, error) {
|
||||
var result = sc.Parameter{
|
||||
Value: value,
|
||||
}
|
||||
|
||||
// todo: add more types
|
||||
switch value.(type) {
|
||||
case []byte:
|
||||
result.Type = sc.ByteArrayType
|
||||
case int64: // TODO: add other numerical types
|
||||
result.Type = sc.IntegerType
|
||||
default:
|
||||
return result, errors.Errorf("chain/client: unsupported parameter %v", value)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
33
lib/blockchain/goclient/client_test.go
Normal file
33
lib/blockchain/goclient/client_test.go
Normal file
|
@ -0,0 +1,33 @@
|
|||
package goclient
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
sc "github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestToStackParameter(t *testing.T) {
|
||||
items := []struct {
|
||||
value interface{}
|
||||
expType sc.ParamType
|
||||
}{
|
||||
{
|
||||
value: []byte{1, 2, 3},
|
||||
expType: sc.ByteArrayType,
|
||||
},
|
||||
{
|
||||
value: int64(100),
|
||||
expType: sc.IntegerType,
|
||||
},
|
||||
}
|
||||
|
||||
for _, item := range items {
|
||||
t.Run(item.expType.String()+" to stack parameter", func(t *testing.T) {
|
||||
res, err := toStackParameter(item.value)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, item.expType, res.Type)
|
||||
require.Equal(t, item.value, res.Value)
|
||||
})
|
||||
}
|
||||
}
|
131
lib/blockchain/goclient/util.go
Normal file
131
lib/blockchain/goclient/util.go
Normal file
|
@ -0,0 +1,131 @@
|
|||
package goclient
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
|
||||
sc "github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
/*
|
||||
Use these function to parse stack parameters obtained from `TestInvoke`
|
||||
function to native go types. You should know upfront return types of invoked
|
||||
method.
|
||||
*/
|
||||
|
||||
// BoolFromStackParameter receives boolean value from the value of a smart contract parameter.
|
||||
func BoolFromStackParameter(param sc.Parameter) (bool, error) {
|
||||
switch param.Type {
|
||||
case sc.BoolType:
|
||||
val, ok := param.Value.(bool)
|
||||
if !ok {
|
||||
return false, errors.Errorf("chain/client: can't convert %T to boolean", param.Value)
|
||||
}
|
||||
|
||||
return val, nil
|
||||
case sc.IntegerType:
|
||||
val, ok := param.Value.(int64)
|
||||
if !ok {
|
||||
return false, errors.Errorf("chain/client: can't convert %T to boolean", param.Value)
|
||||
}
|
||||
|
||||
return val > 0, nil
|
||||
case sc.ByteArrayType:
|
||||
val, ok := param.Value.([]byte)
|
||||
if !ok {
|
||||
return false, errors.Errorf("chain/client: can't convert %T to boolean", param.Value)
|
||||
}
|
||||
|
||||
return len(val) != 0, nil
|
||||
default:
|
||||
return false, errors.Errorf("chain/client: %s is not a bool type", param.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// IntFromStackParameter receives numerical value from the value of a smart contract parameter.
|
||||
func IntFromStackParameter(param sc.Parameter) (int64, error) {
|
||||
switch param.Type {
|
||||
case sc.IntegerType:
|
||||
val, ok := param.Value.(int64)
|
||||
if !ok {
|
||||
return 0, errors.Errorf("chain/client: can't convert %T to integer", param.Value)
|
||||
}
|
||||
|
||||
return val, nil
|
||||
case sc.ByteArrayType:
|
||||
val, ok := param.Value.([]byte)
|
||||
if !ok || len(val) > 8 {
|
||||
return 0, errors.Errorf("chain/client: can't convert %T to integer", param.Value)
|
||||
}
|
||||
|
||||
res := make([]byte, 8)
|
||||
copy(res[:len(val)], val)
|
||||
|
||||
return int64(binary.LittleEndian.Uint64(res)), nil
|
||||
default:
|
||||
return 0, errors.Errorf("chain/client: %s is not an integer type", param.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// BytesFromStackParameter receives binary value from the value of a smart contract parameter.
|
||||
func BytesFromStackParameter(param sc.Parameter) ([]byte, error) {
|
||||
if param.Type != sc.ByteArrayType {
|
||||
return nil, errors.Errorf("chain/client: %s is not a byte array type", param.Type)
|
||||
}
|
||||
|
||||
val, ok := param.Value.([]byte)
|
||||
if !ok {
|
||||
return nil, errors.Errorf("chain/client: can't convert %T to byte slice", param.Value)
|
||||
}
|
||||
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// ArrayFromStackParameter returns the slice contract parameters from passed parameter.
|
||||
//
|
||||
// If passed parameter carries boolean false value, (nil, nil) returns.
|
||||
func ArrayFromStackParameter(param sc.Parameter) ([]sc.Parameter, error) {
|
||||
if param.Type == sc.BoolType && !param.Value.(bool) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if param.Type != sc.ArrayType {
|
||||
return nil, errors.Errorf("chain/client: %s is not an array type", param.Type)
|
||||
}
|
||||
|
||||
val, ok := param.Value.([]sc.Parameter)
|
||||
if !ok {
|
||||
return nil, errors.Errorf("chain/client: can't convert %T to parameter slice", param.Value)
|
||||
}
|
||||
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// StringFromStackParameter receives string value from the value of a smart contract parameter.
|
||||
func StringFromStackParameter(param sc.Parameter) (string, error) {
|
||||
switch param.Type {
|
||||
case sc.StringType:
|
||||
val, ok := param.Value.(string)
|
||||
if !ok {
|
||||
return "", errors.Errorf("chain/client: can't convert %T to string", param.Value)
|
||||
}
|
||||
|
||||
return val, nil
|
||||
case sc.ByteArrayType:
|
||||
val, ok := param.Value.([]byte)
|
||||
if !ok {
|
||||
return "", errors.Errorf("chain/client: can't convert %T to string", param.Value)
|
||||
}
|
||||
|
||||
return string(val), nil
|
||||
default:
|
||||
return "", errors.Errorf("chain/client: %s is not a string type", param.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// ReadStorage of the contract directly. Use it for debug, try to obtain
|
||||
// smart-contract data from contract method with TestInvoke function.
|
||||
func ReadStorage(c *Client, contract util.Uint160, key []byte) ([]byte, error) {
|
||||
return c.cli.GetStorageByHash(contract, key)
|
||||
}
|
145
lib/blockchain/goclient/util_test.go
Normal file
145
lib/blockchain/goclient/util_test.go
Normal file
|
@ -0,0 +1,145 @@
|
|||
package goclient
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
sc "github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var (
|
||||
stringParam = sc.Parameter{
|
||||
Type: sc.StringType,
|
||||
Value: "Hello World",
|
||||
}
|
||||
|
||||
intParam = sc.Parameter{
|
||||
Type: sc.IntegerType,
|
||||
Value: int64(1),
|
||||
}
|
||||
|
||||
byteWithIntParam = sc.Parameter{
|
||||
Type: sc.ByteArrayType,
|
||||
Value: []byte{0x0a},
|
||||
}
|
||||
|
||||
byteArrayParam = sc.Parameter{
|
||||
Type: sc.ByteArrayType,
|
||||
Value: []byte("Hello World"),
|
||||
}
|
||||
|
||||
emptyByteArrayParam = sc.Parameter{
|
||||
Type: sc.ByteArrayType,
|
||||
Value: []byte{},
|
||||
}
|
||||
|
||||
trueBoolParam = sc.Parameter{
|
||||
Type: sc.BoolType,
|
||||
Value: true,
|
||||
}
|
||||
|
||||
falseBoolParam = sc.Parameter{
|
||||
Type: sc.BoolType,
|
||||
Value: false,
|
||||
}
|
||||
|
||||
arrayParam = sc.Parameter{
|
||||
Type: sc.ArrayType,
|
||||
Value: []sc.Parameter{intParam, byteArrayParam},
|
||||
}
|
||||
)
|
||||
|
||||
func TestBoolFromStackParameter(t *testing.T) {
|
||||
t.Run("true assert", func(t *testing.T) {
|
||||
val, err := BoolFromStackParameter(trueBoolParam)
|
||||
require.NoError(t, err)
|
||||
require.True(t, val)
|
||||
|
||||
val, err = BoolFromStackParameter(intParam)
|
||||
require.NoError(t, err)
|
||||
require.True(t, val)
|
||||
})
|
||||
|
||||
t.Run("false assert", func(t *testing.T) {
|
||||
val, err := BoolFromStackParameter(falseBoolParam)
|
||||
require.NoError(t, err)
|
||||
require.False(t, val)
|
||||
|
||||
val, err = BoolFromStackParameter(emptyByteArrayParam)
|
||||
require.NoError(t, err)
|
||||
require.False(t, val)
|
||||
})
|
||||
|
||||
t.Run("incorrect assert", func(t *testing.T) {
|
||||
_, err := BoolFromStackParameter(stringParam)
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestArrayFromStackParameter(t *testing.T) {
|
||||
t.Run("correct assert", func(t *testing.T) {
|
||||
val, err := ArrayFromStackParameter(arrayParam)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, val, len(arrayParam.Value.([]sc.Parameter)))
|
||||
})
|
||||
t.Run("incorrect assert", func(t *testing.T) {
|
||||
_, err := ArrayFromStackParameter(byteArrayParam)
|
||||
require.Error(t, err)
|
||||
})
|
||||
t.Run("boolean false case", func(t *testing.T) {
|
||||
val, err := ArrayFromStackParameter(falseBoolParam)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, val)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBytesFromStackParameter(t *testing.T) {
|
||||
t.Run("correct assert", func(t *testing.T) {
|
||||
val, err := BytesFromStackParameter(byteArrayParam)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, byteArrayParam.Value.([]byte), val)
|
||||
})
|
||||
|
||||
t.Run("incorrect assert", func(t *testing.T) {
|
||||
_, err := BytesFromStackParameter(stringParam)
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntFromStackParameter(t *testing.T) {
|
||||
t.Run("correct assert", func(t *testing.T) {
|
||||
val, err := IntFromStackParameter(intParam)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, intParam.Value.(int64), val)
|
||||
|
||||
val, err = IntFromStackParameter(byteWithIntParam)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(0x0a), val)
|
||||
|
||||
val, err = IntFromStackParameter(emptyByteArrayParam)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(0), val)
|
||||
})
|
||||
|
||||
t.Run("incorrect assert", func(t *testing.T) {
|
||||
_, err := IntFromStackParameter(byteArrayParam)
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestStringFromStackParameter(t *testing.T) {
|
||||
t.Run("correct assert", func(t *testing.T) {
|
||||
val, err := StringFromStackParameter(stringParam)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, stringParam.Value.(string), val)
|
||||
|
||||
val, err = StringFromStackParameter(byteArrayParam)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, string(byteArrayParam.Value.([]byte)), val)
|
||||
})
|
||||
|
||||
t.Run("incorrect assert", func(t *testing.T) {
|
||||
_, err := StringFromStackParameter(intParam)
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
151
lib/blockchain/subscriber/subscriber.go
Normal file
151
lib/blockchain/subscriber/subscriber.go
Normal file
|
@ -0,0 +1,151 @@
|
|||
package subscriber
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/nspcc-dev/neo-go/pkg/rpc/client"
|
||||
"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"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type (
|
||||
// Subscriber is an interface of the NotificationEvent listener.
|
||||
Subscriber interface {
|
||||
SubscribeForNotification(...util.Uint160) (<-chan *result.NotificationEvent, error)
|
||||
UnsubscribeForNotification()
|
||||
}
|
||||
|
||||
subscriber struct {
|
||||
*sync.RWMutex
|
||||
log *zap.Logger
|
||||
client *client.WSClient
|
||||
|
||||
notify chan *result.NotificationEvent
|
||||
notifyIDs map[util.Uint160]string
|
||||
}
|
||||
|
||||
// Params is a group of Subscriber constructor parameters.
|
||||
Params struct {
|
||||
Log *zap.Logger
|
||||
Endpoint string
|
||||
DialTimeout time.Duration
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
errNilParams = errors.New("chain/subscriber: config was not provided to the constructor")
|
||||
|
||||
errNilLogger = errors.New("chain/subscriber: logger was not provided to the constructor")
|
||||
)
|
||||
|
||||
func (s *subscriber) SubscribeForNotification(contracts ...util.Uint160) (<-chan *result.NotificationEvent, error) {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
notifyIDs := make(map[util.Uint160]string, len(contracts))
|
||||
|
||||
for i := range contracts {
|
||||
// do not subscribe to already subscribed contracts
|
||||
if _, ok := s.notifyIDs[contracts[i]]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// subscribe to contract notifications
|
||||
id, err := s.client.SubscribeForExecutionNotifications(&contracts[i])
|
||||
if err != nil {
|
||||
// if there is some error, undo all subscriptions and return error
|
||||
for _, id := range notifyIDs {
|
||||
_ = s.client.Unsubscribe(id)
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// save notification id
|
||||
notifyIDs[contracts[i]] = id
|
||||
}
|
||||
|
||||
// update global map of subscribed contracts
|
||||
for contract, id := range notifyIDs {
|
||||
s.notifyIDs[contract] = id
|
||||
}
|
||||
|
||||
return s.notify, nil
|
||||
}
|
||||
|
||||
func (s *subscriber) UnsubscribeForNotification() {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
for i := range s.notifyIDs {
|
||||
err := s.client.Unsubscribe(s.notifyIDs[i])
|
||||
if err != nil {
|
||||
s.log.Error("unsubscribe for notification",
|
||||
zap.String("event", s.notifyIDs[i]),
|
||||
zap.Error(err))
|
||||
}
|
||||
|
||||
delete(s.notifyIDs, i)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *subscriber) routeNotifications(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case notification := <-s.client.Notifications:
|
||||
switch notification.Type {
|
||||
case response.NotificationEventID:
|
||||
notification, ok := notification.Value.(*result.NotificationEvent)
|
||||
if !ok {
|
||||
s.log.Error("can't cast notify event to the notify struct")
|
||||
continue
|
||||
}
|
||||
|
||||
s.notify <- notification
|
||||
default:
|
||||
s.log.Debug("unsupported notification from the chain",
|
||||
zap.Uint8("type", uint8(notification.Type)),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// New is a constructs Neo:Morph event listener and returns Subscriber interface.
|
||||
func New(ctx context.Context, p *Params) (Subscriber, error) {
|
||||
switch {
|
||||
case p == nil:
|
||||
return nil, errNilParams
|
||||
case p.Log == nil:
|
||||
return nil, errNilLogger
|
||||
}
|
||||
|
||||
wsClient, err := client.NewWS(ctx, p.Endpoint, client.Options{
|
||||
DialTimeout: p.DialTimeout,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sub := &subscriber{
|
||||
RWMutex: new(sync.RWMutex),
|
||||
log: p.Log,
|
||||
client: wsClient,
|
||||
notify: make(chan *result.NotificationEvent),
|
||||
notifyIDs: make(map[util.Uint160]string),
|
||||
}
|
||||
|
||||
// Worker listens all events from neo-go websocket and puts them
|
||||
// into corresponding channel. It may be notifications, transactions,
|
||||
// new blocks. For now only notifications.
|
||||
go sub.routeNotifications(ctx)
|
||||
|
||||
return sub, nil
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue