All checks were successful
Vulncheck / Vulncheck (push) Successful in 1m19s
Pre-commit hooks / Pre-commit (push) Successful in 1m41s
Build / Build Components (push) Successful in 1m43s
Tests and linters / Run gofumpt (push) Successful in 3m38s
Tests and linters / gopls check (push) Successful in 3m41s
Tests and linters / Lint (push) Successful in 3m50s
Tests and linters / Staticcheck (push) Successful in 4m6s
Tests and linters / Tests with -race (push) Successful in 4m22s
Tests and linters / Tests (push) Successful in 4m32s
OCI image / Build container images (push) Successful in 5m0s
NNS contract hash is taken from the contract with ID=1. Because morph client is expected to work with the same chain, and because contract hash doesn't change on update, there is no need to fetch it from each new endpoint. Change-Id: Ic6dc18283789da076d6a0b3701139b97037714cc Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
319 lines
8.2 KiB
Go
319 lines
8.2 KiB
Go
package client
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"time"
|
|
|
|
"git.frostfs.info/TrueCloudLab/frostfs-node/internal/logs"
|
|
"git.frostfs.info/TrueCloudLab/frostfs-node/internal/metrics"
|
|
morphmetrics "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/metrics"
|
|
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/util/logger"
|
|
lru "github.com/hashicorp/golang-lru/v2"
|
|
"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/rpcclient"
|
|
"github.com/nspcc-dev/neo-go/pkg/rpcclient/actor"
|
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
|
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// Option is a client configuration change function.
|
|
type Option func(*cfg)
|
|
|
|
// Callback is a function that is going to be called
|
|
// on certain Client's state.
|
|
type Callback func()
|
|
|
|
// groups the configurations with default values.
|
|
type cfg struct {
|
|
dialTimeout time.Duration // client dial timeout
|
|
|
|
logger *logger.Logger // logging component
|
|
|
|
metrics morphmetrics.Register
|
|
|
|
waitInterval time.Duration
|
|
|
|
signer *transaction.Signer
|
|
|
|
endpoints []Endpoint
|
|
|
|
inactiveModeCb Callback
|
|
|
|
switchInterval time.Duration
|
|
|
|
morphCacheMetrics metrics.MorphCacheMetrics
|
|
|
|
dialerSource DialerSource
|
|
}
|
|
|
|
const (
|
|
defaultDialTimeout = 5 * time.Second
|
|
defaultWaitInterval = 500 * time.Millisecond
|
|
)
|
|
|
|
var ErrNoHealthyEndpoint = errors.New("no healthy endpoint")
|
|
|
|
func defaultConfig() *cfg {
|
|
return &cfg{
|
|
dialTimeout: defaultDialTimeout,
|
|
logger: logger.NewLoggerWrapper(zap.L()),
|
|
metrics: morphmetrics.NoopRegister{},
|
|
waitInterval: defaultWaitInterval,
|
|
signer: &transaction.Signer{
|
|
Scopes: transaction.Global,
|
|
},
|
|
morphCacheMetrics: &morphmetrics.NoopMorphCacheMetrics{},
|
|
dialerSource: &noopDialerSource{},
|
|
}
|
|
}
|
|
|
|
// New creates, initializes and returns the Client instance.
|
|
// Notary support should be enabled with EnableNotarySupport client
|
|
// method separately.
|
|
//
|
|
// If private key is nil, it panics.
|
|
//
|
|
// Other values are set according to provided options, or by default:
|
|
// - client context: Background;
|
|
// - dial timeout: 5s;
|
|
// - blockchain network type: netmode.PrivNet;
|
|
// - signer with the global scope;
|
|
// - wait interval: 500ms;
|
|
// - logger: &logger.Logger{Logger: zap.L()}.
|
|
// - metrics: metrics.NoopRegister
|
|
//
|
|
// If desired option satisfies the default value, it can be omitted.
|
|
// If multiple options of the same config value are supplied,
|
|
// the option with the highest index in the arguments will be used.
|
|
// If the list of endpoints provided - uses first alive.
|
|
// If there are no healthy endpoint - returns ErrNoHealthyEndpoint.
|
|
func New(ctx context.Context, key *keys.PrivateKey, opts ...Option) (*Client, error) {
|
|
if key == nil {
|
|
panic("empty private key")
|
|
}
|
|
|
|
acc := wallet.NewAccountFromPrivateKey(key)
|
|
accAddr := key.GetScriptHash()
|
|
|
|
// build default configuration
|
|
cfg := defaultConfig()
|
|
|
|
// apply options
|
|
for _, opt := range opts {
|
|
opt(cfg)
|
|
}
|
|
|
|
if len(cfg.endpoints) == 0 {
|
|
return nil, errors.New("no endpoints were provided")
|
|
}
|
|
|
|
cli := &Client{
|
|
cache: newClientCache(cfg.morphCacheMetrics),
|
|
logger: cfg.logger,
|
|
metrics: cfg.metrics,
|
|
acc: acc,
|
|
accAddr: accAddr,
|
|
cfg: *cfg,
|
|
closeChan: make(chan struct{}),
|
|
}
|
|
|
|
cli.endpoints.init(cfg.endpoints)
|
|
|
|
var err error
|
|
var act *actor.Actor
|
|
var endpoint Endpoint
|
|
for cli.endpoints.curr, endpoint = range cli.endpoints.list {
|
|
cli.client, act, err = cli.newCli(ctx, endpoint)
|
|
if err != nil {
|
|
cli.logger.Warn(ctx, logs.FrostFSIRCouldntCreateRPCClientForEndpoint,
|
|
zap.Error(err), zap.String("endpoint", endpoint.Address))
|
|
} else {
|
|
cli.logger.Info(ctx, logs.FrostFSIRCreatedRPCClientForEndpoint,
|
|
zap.String("endpoint", endpoint.Address))
|
|
if cli.endpoints.curr > 0 && cli.cfg.switchInterval != 0 {
|
|
cli.switchIsActive.Store(true)
|
|
go cli.switchToMostPrioritized(ctx)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
if cli.client == nil {
|
|
return nil, ErrNoHealthyEndpoint
|
|
}
|
|
cs, err := cli.client.GetContractStateByID(nnsContractID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("resolve nns hash: %w", err)
|
|
}
|
|
cli.nnsHash = cs.Hash
|
|
cli.setActor(act)
|
|
|
|
go cli.closeWaiter(ctx)
|
|
|
|
return cli, nil
|
|
}
|
|
|
|
func (c *Client) newCli(ctx context.Context, endpoint Endpoint) (*rpcclient.WSClient, *actor.Actor, error) {
|
|
cfg, err := endpoint.MTLSConfig.parse()
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("read mtls certificates: %w", err)
|
|
}
|
|
cli, err := rpcclient.NewWS(ctx, endpoint.Address, rpcclient.WSOptions{
|
|
Options: rpcclient.Options{
|
|
DialTimeout: c.cfg.dialTimeout,
|
|
TLSClientConfig: cfg,
|
|
NetDialContext: c.cfg.dialerSource.NetContextDialer(),
|
|
},
|
|
})
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("WS client creation: %w", err)
|
|
}
|
|
|
|
defer func() {
|
|
if err != nil {
|
|
cli.Close()
|
|
}
|
|
}()
|
|
|
|
err = cli.Init()
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("WS client initialization: %w", err)
|
|
}
|
|
|
|
act, err := newActor(cli, c.acc, c.cfg)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("RPC actor creation: %w", err)
|
|
}
|
|
|
|
return cli, act, nil
|
|
}
|
|
|
|
func newActor(ws *rpcclient.WSClient, acc *wallet.Account, cfg cfg) (*actor.Actor, error) {
|
|
return actor.New(ws, []actor.SignerAccount{{
|
|
Signer: transaction.Signer{
|
|
Account: acc.PrivateKey().PublicKey().GetScriptHash(),
|
|
Scopes: cfg.signer.Scopes,
|
|
AllowedContracts: cfg.signer.AllowedContracts,
|
|
AllowedGroups: cfg.signer.AllowedGroups,
|
|
},
|
|
Account: acc,
|
|
}})
|
|
}
|
|
|
|
func newClientCache(morphCacheMetrics metrics.MorphCacheMetrics) cache {
|
|
c, _ := lru.New[util.Uint256, uint32](100) // returns error only if size is negative
|
|
return cache{
|
|
txHeights: c,
|
|
metrics: morphCacheMetrics,
|
|
}
|
|
}
|
|
|
|
// WithDialTimeout returns a client constructor option
|
|
// that specifies neo-go client dial timeout duration.
|
|
//
|
|
// Ignores non-positive value. Has no effect if WithSingleClient
|
|
// is provided.
|
|
//
|
|
// If option not provided, 5s timeout is used.
|
|
func WithDialTimeout(dur time.Duration) Option {
|
|
return func(c *cfg) {
|
|
if dur > 0 {
|
|
c.dialTimeout = dur
|
|
}
|
|
}
|
|
}
|
|
|
|
// WithLogger returns a client constructor option
|
|
// that specifies the component for writing log messages.
|
|
//
|
|
// Ignores nil value.
|
|
//
|
|
// If option not provided, &logger.Logger{Logger: zap.L()} is used.
|
|
func WithLogger(logger *logger.Logger) Option {
|
|
return func(c *cfg) {
|
|
if logger != nil {
|
|
c.logger = logger
|
|
}
|
|
}
|
|
}
|
|
|
|
// WithMetrics returns a client constructor option
|
|
// that specifies the component for reporting metrics.
|
|
//
|
|
// Ignores nil value.
|
|
//
|
|
// If option not provided, NoopMetrics is used.
|
|
func WithMetrics(metrics morphmetrics.Register) Option {
|
|
return func(c *cfg) {
|
|
if metrics != nil {
|
|
c.metrics = metrics
|
|
}
|
|
}
|
|
}
|
|
|
|
// WithSigner returns a client constructor option
|
|
// that specifies the signer and the scope of the transaction.
|
|
//
|
|
// Ignores nil value.
|
|
//
|
|
// If option not provided, signer with global scope is used.
|
|
func WithSigner(signer *transaction.Signer) Option {
|
|
return func(c *cfg) {
|
|
if signer != nil {
|
|
c.signer = signer
|
|
}
|
|
}
|
|
}
|
|
|
|
// WithEndpoints returns a client constructor option
|
|
// that specifies additional Neo rpc endpoints.
|
|
func WithEndpoints(endpoints ...Endpoint) Option {
|
|
return func(c *cfg) {
|
|
c.endpoints = append(c.endpoints, endpoints...)
|
|
}
|
|
}
|
|
|
|
// WithConnLostCallback return a client constructor option
|
|
// that specifies a callback that is called when Client
|
|
// unsuccessfully tried to connect to all the specified
|
|
// endpoints.
|
|
func WithConnLostCallback(cb Callback) Option {
|
|
return func(c *cfg) {
|
|
c.inactiveModeCb = cb
|
|
}
|
|
}
|
|
|
|
// WithSwitchInterval returns a client constructor option
|
|
// that specifies a wait interval b/w attempts to reconnect
|
|
// to an RPC node with the highest priority.
|
|
func WithSwitchInterval(i time.Duration) Option {
|
|
return func(c *cfg) {
|
|
c.switchInterval = i
|
|
}
|
|
}
|
|
|
|
func WithMorphCacheMetrics(morphCacheMetrics metrics.MorphCacheMetrics) Option {
|
|
return func(c *cfg) {
|
|
c.morphCacheMetrics = morphCacheMetrics
|
|
}
|
|
}
|
|
|
|
type DialerSource interface {
|
|
NetContextDialer() func(context.Context, string, string) (net.Conn, error)
|
|
}
|
|
|
|
type noopDialerSource struct{}
|
|
|
|
func (ds *noopDialerSource) NetContextDialer() func(context.Context, string, string) (net.Conn, error) {
|
|
return nil
|
|
}
|
|
|
|
func WithDialerSource(ds DialerSource) Option {
|
|
return func(c *cfg) {
|
|
c.dialerSource = ds
|
|
}
|
|
}
|