diff --git a/core/chains/chain_kv.go b/core/chains/chain_kv.go index 6292ef10197..6224be1c5c3 100644 --- a/core/chains/chain_kv.go +++ b/core/chains/chain_kv.go @@ -5,16 +5,18 @@ import ( "fmt" "golang.org/x/exp/maps" + + "github.com/smartcontractkit/chainlink-relay/pkg/types" ) -type ChainsKV[T ChainService] struct { +type ChainsKV[T types.ChainService] struct { // note: this is read only after construction so no need for mutex chains map[string]T } var ErrNoSuchChainID = errors.New("chain id does not exist") -func NewChainsKV[T ChainService](cs map[string]T) *ChainsKV[T] { +func NewChainsKV[T types.ChainService](cs map[string]T) *ChainsKV[T] { return &ChainsKV[T]{ chains: cs, diff --git a/core/chains/chain_kv_test.go b/core/chains/chain_kv_test.go index c042eea20f1..e8b8f6c0ab4 100644 --- a/core/chains/chain_kv_test.go +++ b/core/chains/chain_kv_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/assert" + "github.com/smartcontractkit/chainlink-relay/pkg/types" "github.com/smartcontractkit/chainlink/v2/core/chains" ) @@ -88,6 +89,19 @@ func (s *testChainService) HealthReport() map[string]error { return map[string]error{} } +// Implement updated [loop.Relay] interface funcs in preparation for BCF-2441 +// TODO update this comment after BCF-2441 is done +func (s *testChainService) GetChainStatus(ctx context.Context) (stat types.ChainStatus, err error) { + return +} +func (s *testChainService) ListNodeStatuses(ctx context.Context, pageSize int32, pageToken string) (stats []types.NodeStatus, nextPageToken string, err error) { + return +} + +func (s *testChainService) Transact(ctx context.Context, from string, to string, amount *big.Int, balanceCheck bool) error { + return nil +} + func (s *testChainService) SendTx(ctx context.Context, from string, to string, amount *big.Int, balanceCheck bool) error { return nil } diff --git a/core/chains/chain_set.go b/core/chains/chain_set.go deleted file mode 100644 index ed5ee918366..00000000000 --- a/core/chains/chain_set.go +++ /dev/null @@ -1,166 +0,0 @@ -package chains - -import ( - "context" - "fmt" - "math/big" - - "github.com/pkg/errors" - "go.uber.org/multierr" - "golang.org/x/exp/maps" - - "github.com/smartcontractkit/chainlink-relay/pkg/logger" - "github.com/smartcontractkit/chainlink-relay/pkg/types" - - "github.com/smartcontractkit/chainlink/v2/core/services" - "github.com/smartcontractkit/chainlink/v2/core/utils" -) - -var ( - // ErrChainIDEmpty is returned when chain is required but was empty. - ErrChainIDEmpty = errors.New("chain id empty") - ErrNotFound = errors.New("not found") -) - -// ChainStatuser is a generic interface for chain configuration. -type ChainStatuser interface { - // must return [ErrNotFound] if the id is not found - ChainStatus(ctx context.Context, id string) (types.ChainStatus, error) - ChainStatuses(ctx context.Context, offset, limit int) ([]types.ChainStatus, int, error) -} - -// NodesStatuser is an interface for node configuration and state. -// TODO BCF2440, BCF-2511 may need Node(ctx,name) to get a node status by name -type NodesStatuser interface { - NodeStatuses(ctx context.Context, offset, limit int, chainIDs ...string) (nodes []types.NodeStatus, count int, err error) -} - -// ChainService is a live, runtime chain instance, with supporting services. -type ChainService interface { - services.ServiceCtx - SendTx(ctx context.Context, from, to string, amount *big.Int, balanceCheck bool) error -} - -// ChainSetOpts holds options for configuring a ChainSet via NewChainSet. -type ChainSetOpts[I ID, N Node] interface { - Validate() error - ConfigsAndLogger() (Configs[I, N], logger.Logger) -} - -type chainSet[N Node, S ChainService] struct { - utils.StartStopOnce - opts ChainSetOpts[string, N] - configs Configs[string, N] - lggr logger.Logger - chains map[string]S -} - -// NewChainSet returns a new immutable ChainSet for the given ChainSetOpts. -func NewChainSet[N Node, S ChainService]( - chains map[string]S, - opts ChainSetOpts[string, N], -) (types.ChainSet[string, S], error) { - if err := opts.Validate(); err != nil { - return nil, err - } - cfgs, lggr := opts.ConfigsAndLogger() - cs := chainSet[N, S]{ - opts: opts, - configs: cfgs, - lggr: logger.Named(lggr, "ChainSet"), - chains: chains, - } - - return &cs, nil -} - -func (c *chainSet[N, S]) Chain(ctx context.Context, id string) (s S, err error) { - if err = c.StartStopOnce.Ready(); err != nil { - return - } - ch, ok := c.chains[id] - if !ok { - err = fmt.Errorf("chain %s: %w", id, ErrNotFound) - return - } - return ch, nil -} - -func (c *chainSet[N, S]) ChainStatus(ctx context.Context, id string) (cfg types.ChainStatus, err error) { - var cs []types.ChainStatus - cs, _, err = c.configs.Chains(0, -1, id) - if err != nil { - return - } - l := len(cs) - if l == 0 { - err = fmt.Errorf("chain %s: %w", id, ErrNotFound) - return - } - if l > 1 { - err = fmt.Errorf("multiple chains found: %d", len(cs)) - return - } - cfg = cs[0] - return -} - -func (c *chainSet[N, S]) ChainStatuses(ctx context.Context, offset, limit int) ([]types.ChainStatus, int, error) { - return c.configs.Chains(offset, limit) -} - -func (c *chainSet[N, S]) NodeStatuses(ctx context.Context, offset, limit int, chainIDs ...string) (nodes []types.NodeStatus, count int, err error) { - return c.configs.NodeStatusesPaged(offset, limit, chainIDs...) -} - -func (c *chainSet[N, S]) SendTx(ctx context.Context, chainID, from, to string, amount *big.Int, balanceCheck bool) error { - chain, err := c.Chain(ctx, chainID) - if err != nil { - return err - } - - return chain.SendTx(ctx, from, to, amount, balanceCheck) -} - -func (c *chainSet[N, S]) Start(ctx context.Context) error { - return c.StartOnce("ChainSet", func() error { - c.lggr.Debug("Starting") - - var ms services.MultiStart - for id, ch := range c.chains { - if err := ms.Start(ctx, ch); err != nil { - return errors.Wrapf(err, "failed to start chain %q", id) - } - } - c.lggr.Info(fmt.Sprintf("Started %d chains", len(c.chains))) - return nil - }) -} - -func (c *chainSet[N, S]) Close() error { - return c.StopOnce("ChainSet", func() error { - c.lggr.Debug("Stopping") - - return services.MultiCloser(maps.Values(c.chains)).Close() - }) -} - -func (c *chainSet[N, S]) Ready() (err error) { - err = c.StartStopOnce.Ready() - for _, c := range c.chains { - err = multierr.Combine(err, c.Ready()) - } - return -} - -func (c *chainSet[N, S]) Name() string { - return c.lggr.Name() -} - -func (c *chainSet[N, S]) HealthReport() map[string]error { - report := map[string]error{c.Name(): c.StartStopOnce.Healthy()} - for _, c := range c.chains { - maps.Copy(report, c.HealthReport()) - } - return report -} diff --git a/core/chains/config.go b/core/chains/config.go index 54f1bb4bf8a..8ce2a63fe0b 100644 --- a/core/chains/config.go +++ b/core/chains/config.go @@ -1,9 +1,19 @@ package chains import ( + "context" + "errors" + + "github.com/smartcontractkit/chainlink-relay/pkg/logger" "github.com/smartcontractkit/chainlink-relay/pkg/types" ) +var ( + // ErrChainIDEmpty is returned when chain is required but was empty. + ErrChainIDEmpty = errors.New("chain id empty") + ErrNotFound = errors.New("not found") +) + type ChainConfigs interface { Chains(offset, limit int, ids ...string) ([]types.ChainStatus, int, error) } @@ -13,11 +23,30 @@ type NodeConfigs[I ID, N Node] interface { Nodes(chainID I) (nodes []N, err error) NodeStatus(name string) (types.NodeStatus, error) - NodeStatusesPaged(offset, limit int, chainIDs ...string) (nodes []types.NodeStatus, count int, err error) } // Configs holds chain and node configurations. +// TODO: BCF-2605 audit the usage of this interface and potentially remove it type Configs[I ID, N Node] interface { ChainConfigs NodeConfigs[I, N] } + +// ChainStatuser is a generic interface for chain configuration. +type ChainStatuser interface { + // must return [ErrNotFound] if the id is not found + ChainStatus(ctx context.Context, id string) (types.ChainStatus, error) + ChainStatuses(ctx context.Context, offset, limit int) ([]types.ChainStatus, int, error) +} + +// NodesStatuser is an interface for node configuration and state. +// TODO BCF-2440, BCF-2511 may need Node(ctx,name) to get a node status by name +type NodesStatuser interface { + NodeStatuses(ctx context.Context, offset, limit int, chainIDs ...string) (nodes []types.NodeStatus, count int, err error) +} + +// ChainOpts holds options for configuring a Chain +type ChainOpts[I ID, N Node] interface { + Validate() error + ConfigsAndLogger() (Configs[I, N], logger.Logger) +} diff --git a/core/chains/cosmos/chain.go b/core/chains/cosmos/chain.go index 82d9a87f4d1..435ede86d25 100644 --- a/core/chains/cosmos/chain.go +++ b/core/chains/cosmos/chain.go @@ -20,10 +20,12 @@ import ( "github.com/smartcontractkit/chainlink-cosmos/pkg/cosmos/db" "github.com/smartcontractkit/chainlink-relay/pkg/logger" + relaytypes "github.com/smartcontractkit/chainlink-relay/pkg/types" "github.com/smartcontractkit/chainlink/v2/core/chains" "github.com/smartcontractkit/chainlink/v2/core/chains/cosmos/cosmostxm" "github.com/smartcontractkit/chainlink/v2/core/chains/cosmos/types" + "github.com/smartcontractkit/chainlink/v2/core/chains/internal" "github.com/smartcontractkit/chainlink/v2/core/services/keystore" "github.com/smartcontractkit/chainlink/v2/core/services/pg" "github.com/smartcontractkit/chainlink/v2/core/utils" @@ -37,18 +39,80 @@ import ( // TODO(BCI-979): Remove this, or make this configurable with the updated client. const DefaultRequestTimeout = 30 * time.Second +var ( + // ErrChainIDEmpty is returned when chain is required but was empty. + ErrChainIDEmpty = errors.New("chain id empty") + // ErrChainIDInvalid is returned when a chain id does not match any configured chains. + ErrChainIDInvalid = errors.New("chain id does not match any local chains") +) + +// Chain is a wrap for easy use in other places in the core node +type Chain = adapters.Chain + +// ChainOpts holds options for configuring a Chain. +type ChainOpts struct { + QueryConfig pg.QConfig + Logger logger.Logger + DB *sqlx.DB + KeyStore keystore.Cosmos + EventBroadcaster pg.EventBroadcaster + Configs types.Configs +} + +func (o *ChainOpts) Validate() (err error) { + required := func(s string) error { + return fmt.Errorf("%s is required", s) + } + if o.QueryConfig == nil { + err = multierr.Append(err, required("Config")) + } + if o.Logger == nil { + err = multierr.Append(err, required("Logger'")) + } + if o.DB == nil { + err = multierr.Append(err, required("DB")) + } + if o.KeyStore == nil { + err = multierr.Append(err, required("KeyStore")) + } + if o.EventBroadcaster == nil { + err = multierr.Append(err, required("EventBroadcaster")) + } + if o.Configs == nil { + err = multierr.Append(err, required("Configs")) + } + return +} + +func (o *ChainOpts) ConfigsAndLogger() (chains.Configs[string, db.Node], logger.Logger) { + return o.Configs, o.Logger +} + +func NewChain(cfg *CosmosConfig, opts ChainOpts) (adapters.Chain, error) { + if !cfg.IsEnabled() { + return nil, fmt.Errorf("cannot create new chain with ID %s, the chain is disabled", *cfg.ChainID) + } + c, err := newChain(*cfg.ChainID, cfg, opts.DB, opts.KeyStore, opts.QueryConfig, opts.EventBroadcaster, opts.Configs, opts.Logger) + if err != nil { + return nil, err + } + return c, nil +} + var _ adapters.Chain = (*chain)(nil) type chain struct { utils.StartStopOnce - id string - cfg coscfg.Config - txm *cosmostxm.Txm + id string + cfg *CosmosConfig + txm *cosmostxm.Txm + // TODO remove this dep after BCF-2441 + // cfs implements the loop.Relayer interface that will be removed cfgs types.Configs lggr logger.Logger } -func newChain(id string, cfg coscfg.Config, db *sqlx.DB, ks keystore.Cosmos, logCfg pg.QConfig, eb pg.EventBroadcaster, cfgs types.Configs, lggr logger.Logger) (*chain, error) { +func newChain(id string, cfg *CosmosConfig, db *sqlx.DB, ks keystore.Cosmos, logCfg pg.QConfig, eb pg.EventBroadcaster, cfgs types.Configs, lggr logger.Logger) (*chain, error) { lggr = logger.With(lggr, "cosmosChainID", id) var ch = chain{ id: id, @@ -155,6 +219,47 @@ func (c *chain) HealthReport() map[string]error { } } -func (c *chain) SendTx(ctx context.Context, from, to string, amount *big.Int, balanceCheck bool) error { +// ChainService interface +func (c *chain) GetChainStatus(ctx context.Context) (relaytypes.ChainStatus, error) { + toml, err := c.cfg.TOMLString() + if err != nil { + return relaytypes.ChainStatus{}, err + } + return relaytypes.ChainStatus{ + ID: c.id, + Enabled: *c.cfg.Enabled, + Config: toml, + }, nil +} +func (c *chain) ListNodeStatuses(ctx context.Context, pageSize int32, pageToken string) (stats []relaytypes.NodeStatus, nextPageToken string, total int, err error) { + return internal.ListNodeStatuses(int(pageSize), pageToken, c.listNodeStatuses) +} + +func (c *chain) Transact(ctx context.Context, from, to string, amount *big.Int, balanceCheck bool) error { return chains.ErrLOOPPUnsupported } + +func (c *chain) SendTx(ctx context.Context, from, to string, amount *big.Int, balanceCheck bool) error { + return c.Transact(ctx, from, to, amount, balanceCheck) +} + +// TODO BCF-2602 statuses are static for non-evm chain and should be dynamic +func (c *chain) listNodeStatuses(start, end int) ([]relaytypes.NodeStatus, int, error) { + stats := make([]relaytypes.NodeStatus, 0) + total := len(c.cfg.Nodes) + if start >= total { + return stats, total, internal.ErrOutOfRange + } + if end > total { + end = total + } + nodes := c.cfg.Nodes[start:end] + for _, node := range nodes { + stat, err := nodeStatus(node, c.id) + if err != nil { + return stats, total, err + } + stats = append(stats, stat) + } + return stats, total, nil +} diff --git a/core/chains/cosmos/chain_set.go b/core/chains/cosmos/chain_set.go deleted file mode 100644 index 008c62c3ffd..00000000000 --- a/core/chains/cosmos/chain_set.go +++ /dev/null @@ -1,179 +0,0 @@ -package cosmos - -import ( - "context" - "errors" - "fmt" - - "go.uber.org/multierr" - - "github.com/smartcontractkit/sqlx" - - "github.com/smartcontractkit/chainlink-cosmos/pkg/cosmos/adapters" - "github.com/smartcontractkit/chainlink-cosmos/pkg/cosmos/db" - - "github.com/smartcontractkit/chainlink-relay/pkg/logger" - "github.com/smartcontractkit/chainlink-relay/pkg/loop" - - pkgcosmos "github.com/smartcontractkit/chainlink-cosmos/pkg/cosmos" - "github.com/smartcontractkit/chainlink/v2/core/chains" - "github.com/smartcontractkit/chainlink/v2/core/chains/cosmos/types" - "github.com/smartcontractkit/chainlink/v2/core/services/keystore" - "github.com/smartcontractkit/chainlink/v2/core/services/pg" - "github.com/smartcontractkit/chainlink/v2/core/services/relay" -) - -var ( - // ErrChainIDEmpty is returned when chain is required but was empty. - ErrChainIDEmpty = errors.New("chain id empty") - // ErrChainIDInvalid is returned when a chain id does not match any configured chains. - ErrChainIDInvalid = errors.New("chain id does not match any local chains") -) - -// Chain is a wrap for easy use in other places in the core node -type Chain = adapters.Chain - -// ChainSetOpts holds options for configuring a ChainSet. -type ChainSetOpts struct { - QueryConfig pg.QConfig - Logger logger.Logger - DB *sqlx.DB - KeyStore keystore.Cosmos - EventBroadcaster pg.EventBroadcaster - Configs types.Configs -} - -func (o *ChainSetOpts) Validate() (err error) { - required := func(s string) error { - return fmt.Errorf("%s is required", s) - } - if o.QueryConfig == nil { - err = multierr.Append(err, required("Config")) - } - if o.Logger == nil { - err = multierr.Append(err, required("Logger'")) - } - if o.DB == nil { - err = multierr.Append(err, required("DB")) - } - if o.KeyStore == nil { - err = multierr.Append(err, required("KeyStore")) - } - if o.EventBroadcaster == nil { - err = multierr.Append(err, required("EventBroadcaster")) - } - if o.Configs == nil { - err = multierr.Append(err, required("Configs")) - } - return -} - -func (o *ChainSetOpts) ConfigsAndLogger() (chains.Configs[string, db.Node], logger.Logger) { - return o.Configs, o.Logger -} - -func (o *ChainSetOpts) NewTOMLChain(cfg *CosmosConfig) (adapters.Chain, error) { - if !cfg.IsEnabled() { - return nil, fmt.Errorf("cannot create new chain with ID %s, the chain is disabled", *cfg.ChainID) - } - c, err := newChain(*cfg.ChainID, cfg, o.DB, o.KeyStore, o.QueryConfig, o.EventBroadcaster, o.Configs, o.Logger) - if err != nil { - return nil, err - } - return c, nil -} - -// LegacyChainContainer is container interface for Cosmos chains -type LegacyChainContainer interface { - Get(id string) (adapters.Chain, error) - Len() int - List(ids ...string) ([]adapters.Chain, error) - Slice() []adapters.Chain -} - -type LegacyChains = chains.ChainsKV[adapters.Chain] - -var _ LegacyChainContainer = &LegacyChains{} - -func NewLegacyChains(m map[string]adapters.Chain) *LegacyChains { - return chains.NewChainsKV[adapters.Chain](m) -} - -type LoopRelayerChainer interface { - loop.Relayer - Chain() adapters.Chain -} - -type LoopRelayerSingleChain struct { - loop.Relayer - singleChain *SingleChainSet -} - -func NewLoopRelayerSingleChain(r *pkgcosmos.Relayer, s *SingleChainSet) *LoopRelayerSingleChain { - ra := relay.NewRelayerAdapter(r, s) - return &LoopRelayerSingleChain{ - Relayer: ra, - singleChain: s, - } -} -func (r *LoopRelayerSingleChain) Chain() adapters.Chain { - return r.singleChain.chain -} - -var _ LoopRelayerChainer = &LoopRelayerSingleChain{} - -func newChainSet(opts ChainSetOpts, cfgs CosmosConfigs) (adapters.ChainSet, map[string]adapters.Chain, error) { - cosmosChains := map[string]adapters.Chain{} - var err error - for _, chain := range cfgs { - if !chain.IsEnabled() { - continue - } - var err2 error - cosmosChains[*chain.ChainID], err2 = opts.NewTOMLChain(chain) - if err2 != nil { - err = multierr.Combine(err, err2) - continue - } - } - if err != nil { - return nil, nil, fmt.Errorf("failed to load some Cosmos chains: %w", err) - } - - cs, err := chains.NewChainSet[db.Node, adapters.Chain](cosmosChains, &opts) - if err != nil { - return nil, nil, err - } - - return cs, cosmosChains, nil -} - -// SingleChainSet is a chainset with 1 chain. TODO remove when relayer interface is updated -type SingleChainSet struct { - adapters.ChainSet - ID string - chain adapters.Chain -} - -func (s *SingleChainSet) Chain(ctx context.Context, id string) (adapters.Chain, error) { - return s.chain, nil -} - -func NewSingleChainSet(opts ChainSetOpts, cfg *CosmosConfig) (*SingleChainSet, error) { - cs, m, err := newChainSet(opts, CosmosConfigs{cfg}) - if err != nil { - return nil, err - } - if len(m) != 1 { - return nil, fmt.Errorf("invalid Single chain: more than one chain %d", len(m)) - } - var chain adapters.Chain - for _, ch := range m { - chain = ch - } - return &SingleChainSet{ - ChainSet: cs, - ID: *cfg.ChainID, - chain: chain, - }, nil -} diff --git a/core/chains/cosmos/config.go b/core/chains/cosmos/config.go index 62a76be44d1..878de2130b6 100644 --- a/core/chains/cosmos/config.go +++ b/core/chains/cosmos/config.go @@ -234,6 +234,7 @@ func legacyNode(n *coscfg.Node, id string) db.Node { type CosmosConfig struct { ChainID *string + // Do not access directly. Use [IsEnabled] Enabled *bool coscfg.Chain Nodes CosmosNodes diff --git a/core/chains/cosmos/relay_extender.go b/core/chains/cosmos/relay_extender.go new file mode 100644 index 00000000000..3f9b9f5ad5b --- /dev/null +++ b/core/chains/cosmos/relay_extender.go @@ -0,0 +1,85 @@ +package cosmos + +import ( + "context" + "fmt" + "math/big" + + "github.com/smartcontractkit/chainlink-cosmos/pkg/cosmos/adapters" + + "github.com/smartcontractkit/chainlink-relay/pkg/loop" + relaytypes "github.com/smartcontractkit/chainlink-relay/pkg/types" + + pkgcosmos "github.com/smartcontractkit/chainlink-cosmos/pkg/cosmos" + "github.com/smartcontractkit/chainlink/v2/core/chains" + "github.com/smartcontractkit/chainlink/v2/core/services/relay" +) + +// LegacyChainContainer is container interface for Cosmos chains +type LegacyChainContainer interface { + Get(id string) (adapters.Chain, error) + Len() int + List(ids ...string) ([]adapters.Chain, error) + Slice() []adapters.Chain +} + +type LegacyChains = chains.ChainsKV[adapters.Chain] + +var _ LegacyChainContainer = &LegacyChains{} + +func NewLegacyChains(m map[string]adapters.Chain) *LegacyChains { + return chains.NewChainsKV[adapters.Chain](m) +} + +type LoopRelayerChainer interface { + loop.Relayer + Chain() adapters.Chain +} + +type LoopRelayerChain struct { + loop.Relayer + chain adapters.Chain +} + +func NewLoopRelayerChain(r *pkgcosmos.Relayer, s *RelayExtender) *LoopRelayerChain { + + ra := relay.NewRelayerAdapter(r, s) + return &LoopRelayerChain{ + Relayer: ra, + chain: s, + } +} +func (r *LoopRelayerChain) Chain() adapters.Chain { + return r.chain +} + +var _ LoopRelayerChainer = &LoopRelayerChain{} + +// TODO remove these wrappers after BCF-2441 +type RelayExtender struct { + adapters.Chain + chainImpl *chain +} + +var _ relay.RelayerExt = &RelayExtender{} + +func NewRelayExtender(cfg *CosmosConfig, opts ChainOpts) (*RelayExtender, error) { + c, err := NewChain(cfg, opts) + if err != nil { + return nil, err + } + chainImpl, ok := (c).(*chain) + if !ok { + return nil, fmt.Errorf("internal error: cosmos relay extender got wrong type %t", c) + } + return &RelayExtender{Chain: chainImpl, chainImpl: chainImpl}, nil +} +func (r *RelayExtender) GetChainStatus(ctx context.Context) (relaytypes.ChainStatus, error) { + return r.chainImpl.GetChainStatus(ctx) +} +func (r *RelayExtender) ListNodeStatuses(ctx context.Context, pageSize int32, pageToken string) (stats []relaytypes.NodeStatus, nextPageToken string, total int, err error) { + return r.chainImpl.ListNodeStatuses(ctx, pageSize, pageToken) +} +func (r *RelayExtender) Transact(ctx context.Context, from, to string, amount *big.Int, balanceCheck bool) error { + return chains.ErrLOOPPUnsupported +} diff --git a/core/chains/errors.go b/core/chains/errors.go index a87c134f7f4..f13317bb14a 100644 --- a/core/chains/errors.go +++ b/core/chains/errors.go @@ -2,4 +2,7 @@ package chains import "errors" -var ErrLOOPPUnsupported = errors.New("LOOPP not yet supported") +var ( + ErrLOOPPUnsupported = errors.New("LOOPP not yet supported") + ErrChainDisabled = errors.New("chain is disabled") +) diff --git a/core/chains/evm/chain.go b/core/chains/evm/chain.go index 619b3e2db77..991e7451186 100644 --- a/core/chains/evm/chain.go +++ b/core/chains/evm/chain.go @@ -14,8 +14,11 @@ import ( "github.com/smartcontractkit/sqlx" + gotoml "github.com/pelletier/go-toml/v2" + "github.com/smartcontractkit/chainlink-relay/pkg/types" + relaytypes "github.com/smartcontractkit/chainlink-relay/pkg/types" "github.com/smartcontractkit/chainlink/v2/core/chains" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" @@ -29,6 +32,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/chains/evm/monitor" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txmgr" evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" + "github.com/smartcontractkit/chainlink/v2/core/chains/internal" "github.com/smartcontractkit/chainlink/v2/core/config" "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services" @@ -52,6 +56,13 @@ type Chain interface { BalanceMonitor() monitor.BalanceMonitor LogPoller() logpoller.LogPoller GasEstimator() gas.EvmFeeEstimator + + // TODO remove after BCF-2441 + // This funcs are implemented now in preparation the interface change, which is expected + // to absorb these definitions into [types.ChainService] + GetChainStatus(ctx context.Context) (relaytypes.ChainStatus, error) + ListNodeStatuses(ctx context.Context, pageSize int32, pageToken string) (stats []relaytypes.NodeStatus, nextPageToken string, total int, err error) + Transact(ctx context.Context, from, to string, amount *big.Int, balanceCheck bool) error } var ( @@ -126,7 +137,7 @@ func (c *LegacyChains) Get(id string) (Chain, error) { type chain struct { utils.StartStopOnce id *big.Int - cfg evmconfig.ChainScopedConfig + cfg *evmconfig.ChainScoped client evmclient.Client txm txmgr.TxManager logger logger.Logger @@ -157,7 +168,7 @@ type ChainRelayExtenderConfig struct { Logger logger.Logger DB *sqlx.DB KeyStore keystore.Eth - RelayerConfig + *RelayerConfig } // options for the relayer factory. @@ -203,7 +214,7 @@ func NewTOMLChain(ctx context.Context, chain *toml.EVMConfig, opts ChainRelayExt return newChain(ctx, cfg, chain.Nodes, opts) } -func newChain(ctx context.Context, cfg evmconfig.ChainScopedConfig, nodes []*toml.Node, opts ChainRelayExtenderConfig) (*chain, error) { +func newChain(ctx context.Context, cfg *evmconfig.ChainScoped, nodes []*toml.Node, opts ChainRelayExtenderConfig) (*chain, error) { chainID, chainType := cfg.EVM().ChainID(), cfg.EVM().ChainType() l := opts.Logger.Named(chainID.String()).With("evmChainID", chainID.String()) var client evmclient.Client @@ -380,10 +391,71 @@ func (c *chain) HealthReport() map[string]error { return report } -func (c *chain) SendTx(ctx context.Context, from, to string, amount *big.Int, balanceCheck bool) error { +func (c *chain) Transact(ctx context.Context, from, to string, amount *big.Int, balanceCheck bool) error { return chains.ErrLOOPPUnsupported } +func (c *chain) SendTx(ctx context.Context, from, to string, amount *big.Int, balanceCheck bool) error { + return c.Transact(ctx, from, to, amount, balanceCheck) +} + +func (c *chain) GetChainStatus(ctx context.Context) (types.ChainStatus, error) { + toml, err := c.cfg.EVM().TOMLString() + if err != nil { + return types.ChainStatus{}, err + } + return types.ChainStatus{ + ID: c.ID().String(), + Enabled: c.cfg.EVM().IsEnabled(), + Config: toml, + }, nil +} + +// TODO BCF-2602 statuses are static for non-evm chain and should be dynamic +func (c *chain) listNodeStatuses(start, end int) ([]types.NodeStatus, int, error) { + nodes := c.cfg.Nodes() + total := len(nodes) + if start >= total { + return nil, total, internal.ErrOutOfRange + } + if end > total { + end = total + } + stats := make([]types.NodeStatus, 0) + + states := c.Client().NodeStates() + for _, n := range nodes[start:end] { + var ( + nodeState string + exists bool + ) + toml, err := gotoml.Marshal(n) + if err != nil { + return nil, -1, err + } + if states == nil { + nodeState = "Unknown" + } else { + nodeState, exists = states[*n.Name] + if !exists { + // The node is in the DB and the chain is enabled but it's not running + nodeState = "NotLoaded" + } + } + stats = append(stats, types.NodeStatus{ + ChainID: c.ID().String(), + Name: *n.Name, + Config: string(toml), + State: nodeState, + }) + } + return stats, total, nil +} + +func (c *chain) ListNodeStatuses(ctx context.Context, pageSize int32, pageToken string) (stats []types.NodeStatus, nextPageToken string, total int, err error) { + return internal.ListNodeStatuses(int(pageSize), pageToken, c.listNodeStatuses) +} + func (c *chain) ID() *big.Int { return c.id } func (c *chain) Client() evmclient.Client { return c.client } func (c *chain) Config() evmconfig.ChainScopedConfig { return c.cfg } @@ -430,6 +502,9 @@ func (opts *ChainRelayExtenderConfig) Check() error { return errors.New("config must be non-nil") } - opts.operationalConfigs = chains.NewConfigs[utils.Big, evmtypes.Node](opts.AppConfig.EVMConfigs()) + opts.init.Do(func() { + opts.operationalConfigs = chains.NewConfigs[utils.Big, evmtypes.Node](opts.AppConfig.EVMConfigs()) + }) + return nil } diff --git a/core/chains/evm/config/chain_scoped.go b/core/chains/evm/config/chain_scoped.go index 260043ba252..b2046559b0d 100644 --- a/core/chains/evm/config/chain_scoped.go +++ b/core/chains/evm/config/chain_scoped.go @@ -34,6 +34,10 @@ func (c *ChainScoped) EVM() EVM { return c.evmConfig } +func (c *ChainScoped) Nodes() toml.EVMNodes { + return c.evmConfig.c.Nodes +} + func (c *ChainScoped) BlockEmissionIdleWarningThreshold() time.Duration { return c.EVM().NodeNoNewHeadsThreshold() } @@ -60,6 +64,14 @@ type evmConfig struct { c *toml.EVMConfig } +func (e *evmConfig) IsEnabled() bool { + return e.c.IsEnabled() +} + +func (e *evmConfig) TOMLString() (string, error) { + return e.c.TOMLString() +} + func (e *evmConfig) BalanceMonitor() BalanceMonitor { return &balanceMonitorConfig{c: e.c.BalanceMonitor} } diff --git a/core/chains/evm/config/config.go b/core/chains/evm/config/config.go index 05c65a8f997..18c075dc24a 100644 --- a/core/chains/evm/config/config.go +++ b/core/chains/evm/config/config.go @@ -38,6 +38,9 @@ type EVM interface { OperatorFactoryAddress() string RPCDefaultBatchSize() uint32 NodeNoNewHeadsThreshold() time.Duration + + IsEnabled() bool + TOMLString() (string, error) } type OCR interface { diff --git a/core/chains/evm/mocks/chain.go b/core/chains/evm/mocks/chain.go index 081d18edc01..92d39a95fb9 100644 --- a/core/chains/evm/mocks/chain.go +++ b/core/chains/evm/mocks/chain.go @@ -8,6 +8,8 @@ import ( common "github.com/ethereum/go-ethereum/common" client "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" + commontypes "github.com/smartcontractkit/chainlink/v2/common/types" + config "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config" context "context" @@ -28,7 +30,7 @@ import ( txmgr "github.com/smartcontractkit/chainlink/v2/common/txmgr" - types "github.com/smartcontractkit/chainlink/v2/common/types" + types "github.com/smartcontractkit/chainlink-relay/pkg/types" ) // Chain is an autogenerated mock type for the Chain type @@ -114,16 +116,40 @@ func (_m *Chain) GasEstimator() gas.EvmFeeEstimator { return r0 } +// GetChainStatus provides a mock function with given fields: ctx +func (_m *Chain) GetChainStatus(ctx context.Context) (types.ChainStatus, error) { + ret := _m.Called(ctx) + + var r0 types.ChainStatus + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (types.ChainStatus, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) types.ChainStatus); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(types.ChainStatus) + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // HeadBroadcaster provides a mock function with given fields: -func (_m *Chain) HeadBroadcaster() types.HeadBroadcaster[*evmtypes.Head, common.Hash] { +func (_m *Chain) HeadBroadcaster() commontypes.HeadBroadcaster[*evmtypes.Head, common.Hash] { ret := _m.Called() - var r0 types.HeadBroadcaster[*evmtypes.Head, common.Hash] - if rf, ok := ret.Get(0).(func() types.HeadBroadcaster[*evmtypes.Head, common.Hash]); ok { + var r0 commontypes.HeadBroadcaster[*evmtypes.Head, common.Hash] + if rf, ok := ret.Get(0).(func() commontypes.HeadBroadcaster[*evmtypes.Head, common.Hash]); ok { r0 = rf() } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(types.HeadBroadcaster[*evmtypes.Head, common.Hash]) + r0 = ret.Get(0).(commontypes.HeadBroadcaster[*evmtypes.Head, common.Hash]) } } @@ -131,15 +157,15 @@ func (_m *Chain) HeadBroadcaster() types.HeadBroadcaster[*evmtypes.Head, common. } // HeadTracker provides a mock function with given fields: -func (_m *Chain) HeadTracker() types.HeadTracker[*evmtypes.Head, common.Hash] { +func (_m *Chain) HeadTracker() commontypes.HeadTracker[*evmtypes.Head, common.Hash] { ret := _m.Called() - var r0 types.HeadTracker[*evmtypes.Head, common.Hash] - if rf, ok := ret.Get(0).(func() types.HeadTracker[*evmtypes.Head, common.Hash]); ok { + var r0 commontypes.HeadTracker[*evmtypes.Head, common.Hash] + if rf, ok := ret.Get(0).(func() commontypes.HeadTracker[*evmtypes.Head, common.Hash]); ok { r0 = rf() } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(types.HeadTracker[*evmtypes.Head, common.Hash]) + r0 = ret.Get(0).(commontypes.HeadTracker[*evmtypes.Head, common.Hash]) } } @@ -178,6 +204,46 @@ func (_m *Chain) ID() *big.Int { return r0 } +// ListNodeStatuses provides a mock function with given fields: ctx, pageSize, pageToken +func (_m *Chain) ListNodeStatuses(ctx context.Context, pageSize int32, pageToken string) ([]types.NodeStatus, string, int, error) { + ret := _m.Called(ctx, pageSize, pageToken) + + var r0 []types.NodeStatus + var r1 string + var r2 int + var r3 error + if rf, ok := ret.Get(0).(func(context.Context, int32, string) ([]types.NodeStatus, string, int, error)); ok { + return rf(ctx, pageSize, pageToken) + } + if rf, ok := ret.Get(0).(func(context.Context, int32, string) []types.NodeStatus); ok { + r0 = rf(ctx, pageSize, pageToken) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]types.NodeStatus) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int32, string) string); ok { + r1 = rf(ctx, pageSize, pageToken) + } else { + r1 = ret.Get(1).(string) + } + + if rf, ok := ret.Get(2).(func(context.Context, int32, string) int); ok { + r2 = rf(ctx, pageSize, pageToken) + } else { + r2 = ret.Get(2).(int) + } + + if rf, ok := ret.Get(3).(func(context.Context, int32, string) error); ok { + r3 = rf(ctx, pageSize, pageToken) + } else { + r3 = ret.Error(3) + } + + return r0, r1, r2, r3 +} + // LogBroadcaster provides a mock function with given fields: func (_m *Chain) LogBroadcaster() log.Broadcaster { ret := _m.Called() @@ -282,6 +348,20 @@ func (_m *Chain) Start(_a0 context.Context) error { return r0 } +// Transact provides a mock function with given fields: ctx, from, to, amount, balanceCheck +func (_m *Chain) Transact(ctx context.Context, from string, to string, amount *big.Int, balanceCheck bool) error { + ret := _m.Called(ctx, from, to, amount, balanceCheck) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, *big.Int, bool) error); ok { + r0 = rf(ctx, from, to, amount, balanceCheck) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // TxManager provides a mock function with given fields: func (_m *Chain) TxManager() txmgr.TxManager[*big.Int, *evmtypes.Head, common.Address, common.Hash, common.Hash, evmtypes.Nonce, gas.EvmFee] { ret := _m.Called() diff --git a/core/chains/internal/utils.go b/core/chains/internal/utils.go new file mode 100644 index 00000000000..0932f12209c --- /dev/null +++ b/core/chains/internal/utils.go @@ -0,0 +1,109 @@ +package internal + +import ( + "encoding/base64" + "errors" + "fmt" + "net/url" + "strconv" + + "github.com/smartcontractkit/chainlink-relay/pkg/types" +) + +// PageToken is simple internal representation for coordination requests and responses in a paginated API +// It is inspired by the Google API Design patterns +// https://cloud.google.com/apis/design/design_patterns#list_pagination +// https://google.aip.dev/158 +type PageToken struct { + Page int + Size int +} + +var ( + ErrInvalidToken = errors.New("invalid page token") + ErrOutOfRange = errors.New("out of range") + defaultSize = 100 +) + +// Encode the token in base64 for transmission for the wire +func (pr *PageToken) Encode() string { + if pr.Size == 0 { + pr.Size = defaultSize + } + // this is a simple minded implementation and may benefit from something fancier + // note that this is a valid url.Query string, which we leverage in decoding + s := fmt.Sprintf("page=%d&size=%d", pr.Page, pr.Size) + return base64.RawStdEncoding.EncodeToString([]byte(s)) +} + +// b64enc must be the base64 encoded token string, corresponding to [PageToken.Encode()] +func NewPageToken(b64enc string) (*PageToken, error) { + // empty is valid + if b64enc == "" { + return &PageToken{Page: 0, Size: defaultSize}, nil + } + + b, err := base64.RawStdEncoding.DecodeString(b64enc) + if err != nil { + return nil, err + } + // here too, this is simple minded and could be fancier + + vals, err := url.ParseQuery(string(b)) + if err != nil { + return nil, err + } + if !(vals.Has("page") && vals.Has("size")) { + return nil, ErrInvalidToken + } + page, err := strconv.Atoi(vals.Get("page")) + if err != nil { + return nil, fmt.Errorf("%w: bad page", ErrInvalidToken) + } + size, err := strconv.Atoi(vals.Get("size")) + if err != nil { + return nil, fmt.Errorf("%w: bad size", ErrInvalidToken) + } + return &PageToken{ + Page: page, + Size: size, + }, err +} + +func ValidatePageToken(pageSize int, token string) (page int, err error) { + + if token == "" { + return 0, nil + } + t, err := NewPageToken(token) + if err != nil { + return -1, err + } + return t.Page, nil +} + +// if start is out of range, must return ErrOutOfRange +type ListNodeStatusFn = func(start, end int) (stats []types.NodeStatus, total int, err error) + +func ListNodeStatuses(pageSize int, pageToken string, listFn ListNodeStatusFn) (stats []types.NodeStatus, nextPageToken string, total int, err error) { + if pageSize == 0 { + pageSize = defaultSize + } + t := &PageToken{Page: 0, Size: pageSize} + if pageToken != "" { + t, err = NewPageToken(pageToken) + if err != nil { + return nil, "", -1, err + } + } + start, end := t.Page*t.Size, (t.Page+1)*t.Size + stats, total, err = listFn(start, end) + if err != nil { + return stats, "", -1, err + } + if total > end { + next_token := &PageToken{Page: t.Page + 1, Size: t.Size} + nextPageToken = next_token.Encode() + } + return stats, nextPageToken, total, nil +} diff --git a/core/chains/internal/utils_test.go b/core/chains/internal/utils_test.go new file mode 100644 index 00000000000..5a47ed3d8ff --- /dev/null +++ b/core/chains/internal/utils_test.go @@ -0,0 +1,166 @@ +package internal + +import ( + "encoding/base64" + "fmt" + "reflect" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/smartcontractkit/chainlink-relay/pkg/types" +) + +func TestNewPageToken(t *testing.T) { + type args struct { + t *PageToken + } + tests := []struct { + name string + args args + want *PageToken + wantErr bool + }{ + { + name: "empty", + args: args{t: &PageToken{}}, + want: &PageToken{Page: 0, Size: defaultSize}, + }, + { + name: "page set, size unset", + args: args{t: &PageToken{Page: 1}}, + want: &PageToken{Page: 1, Size: defaultSize}, + }, + { + name: "page set, size set", + args: args{t: &PageToken{Page: 3, Size: 10}}, + want: &PageToken{Page: 3, Size: 10}, + }, + { + name: "page unset, size set", + args: args{t: &PageToken{Size: 17}}, + want: &PageToken{Page: 0, Size: 17}, + }, + } + for _, tt := range tests { + enc := tt.args.t.Encode() + t.Run(tt.name, func(t *testing.T) { + got, err := NewPageToken(enc) + if (err != nil) != tt.wantErr { + t.Errorf("NewPageToken() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewPageToken() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestListNodeStatuses(t *testing.T) { + testStats := []types.NodeStatus{ + types.NodeStatus{ + ChainID: "chain-1", + Name: "name-1", + }, + types.NodeStatus{ + ChainID: "chain-2", + Name: "name-2", + }, + types.NodeStatus{ + ChainID: "chain-3", + Name: "name-3", + }, + } + + type args struct { + pageSize int + pageToken string + listFn ListNodeStatusFn + } + tests := []struct { + name string + args args + wantStats []types.NodeStatus + wantNext_pageToken string + wantTotal int + wantErr bool + }{ + { + name: "all on first page", + args: args{ + pageSize: 10, // > length of test stats + pageToken: "", + listFn: func(start, end int) ([]types.NodeStatus, int, error) { + return testStats, len(testStats), nil + }, + }, + wantNext_pageToken: "", + wantTotal: len(testStats), + wantStats: testStats, + }, + { + name: "small first page", + args: args{ + pageSize: len(testStats) - 1, + pageToken: "", + listFn: func(start, end int) ([]types.NodeStatus, int, error) { + return testStats[start:end], len(testStats), nil + }, + }, + wantNext_pageToken: base64.RawStdEncoding.EncodeToString([]byte("page=1&size=2")), // hard coded 2 is len(testStats)-1 + wantTotal: len(testStats), + wantStats: testStats[0 : len(testStats)-1], + }, + { + name: "second page", + args: args{ + pageSize: len(testStats) - 1, + pageToken: base64.RawStdEncoding.EncodeToString([]byte("page=1&size=2")), // hard coded 2 is len(testStats)-1 + listFn: func(start, end int) ([]types.NodeStatus, int, error) { + // note list function must do the start, end bound checking. here we are making it simple + if end > len(testStats) { + end = len(testStats) + } + return testStats[start:end], len(testStats), nil + }, + }, + wantNext_pageToken: "", + wantTotal: len(testStats), + wantStats: testStats[len(testStats)-1:], + }, + { + name: "bad list fn", + args: args{ + listFn: func(start, end int) ([]types.NodeStatus, int, error) { + return nil, 0, fmt.Errorf("i'm a bad list fn") + }, + }, + wantTotal: -1, + wantErr: true, + }, + { + name: "invalid token", + args: args{ + pageToken: "invalid token", + listFn: func(start, end int) ([]types.NodeStatus, int, error) { + return testStats[start:end], len(testStats), nil + }, + }, + wantTotal: -1, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotStats, gotNext_pageToken, gotTotal, err := ListNodeStatuses(tt.args.pageSize, tt.args.pageToken, tt.args.listFn) + if (err != nil) != tt.wantErr { + t.Errorf("ListNodeStatuses() error = %v, wantErr %v", err, tt.wantErr) + return + } + assert.Equal(t, tt.wantStats, gotStats) + assert.Equal(t, tt.wantNext_pageToken, gotNext_pageToken) + assert.Equal(t, tt.wantTotal, gotTotal) + }) + } +} diff --git a/core/chains/solana/chain.go b/core/chains/solana/chain.go index a11c0998a1c..2c443082f80 100644 --- a/core/chains/solana/chain.go +++ b/core/chains/solana/chain.go @@ -18,6 +18,7 @@ import ( "github.com/smartcontractkit/chainlink-relay/pkg/logger" "github.com/smartcontractkit/chainlink-relay/pkg/loop" + relaytypes "github.com/smartcontractkit/chainlink-relay/pkg/types" "github.com/smartcontractkit/chainlink-solana/pkg/solana" "github.com/smartcontractkit/chainlink-solana/pkg/solana/client" @@ -25,6 +26,8 @@ import ( "github.com/smartcontractkit/chainlink-solana/pkg/solana/db" "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm" + "github.com/smartcontractkit/chainlink/v2/core/chains" + "github.com/smartcontractkit/chainlink/v2/core/chains/internal" "github.com/smartcontractkit/chainlink/v2/core/chains/solana/monitor" "github.com/smartcontractkit/chainlink/v2/core/services" "github.com/smartcontractkit/chainlink/v2/core/utils" @@ -33,12 +36,50 @@ import ( // DefaultRequestTimeout is the default Solana client timeout. const DefaultRequestTimeout = 30 * time.Second +// ChainOpts holds options for configuring a Chain. +type ChainOpts struct { + Logger logger.Logger + KeyStore loop.Keystore + Configs Configs +} + +func (o *ChainOpts) Validate() (err error) { + required := func(s string) error { + return errors.Errorf("%s is required", s) + } + if o.Logger == nil { + err = multierr.Append(err, required("Logger")) + } + if o.KeyStore == nil { + err = multierr.Append(err, required("KeyStore")) + } + if o.Configs == nil { + err = multierr.Append(err, required("Configs")) + } + return +} + +func (o *ChainOpts) ConfigsAndLogger() (chains.Configs[string, db.Node], logger.Logger) { + return o.Configs, o.Logger +} + +func NewChain(cfg *SolanaConfig, opts ChainOpts) (solana.Chain, error) { + if !cfg.IsEnabled() { + return nil, fmt.Errorf("cannot create new chain with ID %s: %w", *cfg.ChainID, chains.ErrChainDisabled) + } + c, err := newChain(*cfg.ChainID, cfg, opts.KeyStore, opts.Configs, opts.Logger) + if err != nil { + return nil, err + } + return c, nil +} + var _ solana.Chain = (*chain)(nil) type chain struct { utils.StartStopOnce id string - cfg config.Config + cfg *SolanaConfig txm *txm.Txm balanceMonitor services.ServiceCtx nodes func(chainID string) (nodes []db.Node, err error) @@ -172,7 +213,7 @@ func (v *verifiedCachedClient) GetAccountInfoWithOpts(ctx context.Context, addr return v.ReaderWriter.GetAccountInfoWithOpts(ctx, addr, opts) } -func newChain(id string, cfg config.Config, ks loop.Keystore, cfgs Configs, lggr logger.Logger) (*chain, error) { +func newChain(id string, cfg *SolanaConfig, ks loop.Keystore, cfgs Configs, lggr logger.Logger) (*chain, error) { lggr = logger.With(lggr, "chainID", id, "chainSet", "solana") var ch = chain{ id: id, @@ -189,6 +230,47 @@ func newChain(id string, cfg config.Config, ks loop.Keystore, cfgs Configs, lggr return &ch, nil } +// ChainService interface +func (c *chain) GetChainStatus(ctx context.Context) (relaytypes.ChainStatus, error) { + toml, err := c.cfg.TOMLString() + if err != nil { + return relaytypes.ChainStatus{}, err + } + return relaytypes.ChainStatus{ + ID: c.id, + Enabled: c.cfg.IsEnabled(), + Config: toml, + }, nil +} + +func (c *chain) ListNodeStatuses(ctx context.Context, pageSize int32, pageToken string) (stats []relaytypes.NodeStatus, nextPageToken string, total int, err error) { + return internal.ListNodeStatuses(int(pageSize), pageToken, c.listNodeStatuses) +} + +func (c *chain) Transact(ctx context.Context, from, to string, amount *big.Int, balanceCheck bool) error { + panic("unimplmented") +} + +func (c *chain) listNodeStatuses(start, end int) ([]relaytypes.NodeStatus, int, error) { + stats := make([]relaytypes.NodeStatus, 0) + total := len(c.cfg.Nodes) + if start >= total { + return stats, total, internal.ErrOutOfRange + } + if end > total { + end = total + } + nodes := c.cfg.Nodes[start:end] + for _, node := range nodes { + stat, err := nodeStatus(node, c.id) + if err != nil { + return stats, total, err + } + stats = append(stats, stat) + } + return stats, total, nil +} + func (c *chain) Name() string { return c.lggr.Name() } diff --git a/core/chains/solana/chain_set.go b/core/chains/solana/chain_set.go deleted file mode 100644 index 56a8a6e189f..00000000000 --- a/core/chains/solana/chain_set.go +++ /dev/null @@ -1,71 +0,0 @@ -package solana - -import ( - "github.com/pkg/errors" - "go.uber.org/multierr" - - "github.com/smartcontractkit/chainlink-relay/pkg/logger" - "github.com/smartcontractkit/chainlink-relay/pkg/loop" - "github.com/smartcontractkit/chainlink-solana/pkg/solana" - "github.com/smartcontractkit/chainlink-solana/pkg/solana/db" - - "github.com/smartcontractkit/chainlink/v2/core/chains" -) - -// ChainSetOpts holds options for configuring a ChainSet. -type ChainSetOpts struct { - Logger logger.Logger - KeyStore loop.Keystore - Configs Configs -} - -func (o *ChainSetOpts) Validate() (err error) { - required := func(s string) error { - return errors.Errorf("%s is required", s) - } - if o.Logger == nil { - err = multierr.Append(err, required("Logger")) - } - if o.KeyStore == nil { - err = multierr.Append(err, required("KeyStore")) - } - if o.Configs == nil { - err = multierr.Append(err, required("Configs")) - } - return -} - -func (o *ChainSetOpts) ConfigsAndLogger() (chains.Configs[string, db.Node], logger.Logger) { - return o.Configs, o.Logger -} - -func (o *ChainSetOpts) NewTOMLChain(cfg *SolanaConfig) (solana.Chain, error) { - if !cfg.IsEnabled() { - return nil, errors.Errorf("cannot create new chain with ID %s, the chain is disabled", *cfg.ChainID) - } - c, err := newChain(*cfg.ChainID, cfg, o.KeyStore, o.Configs, o.Logger) - if err != nil { - return nil, err - } - return c, nil -} - -func NewChainSet(opts ChainSetOpts, cfgs SolanaConfigs) (solana.ChainSet, error) { - solChains := map[string]solana.Chain{} - var err error - for _, chain := range cfgs { - if !chain.IsEnabled() { - continue - } - var err2 error - solChains[*chain.ChainID], err2 = opts.NewTOMLChain(chain) - if err2 != nil { - err = multierr.Combine(err, err2) - continue - } - } - if err != nil { - return nil, errors.Wrap(err, "failed to load some Solana chains") - } - return chains.NewChainSet[db.Node, solana.Chain](solChains, &opts) -} diff --git a/core/chains/solana/chain_test.go b/core/chains/solana/chain_test.go index 176cc1d15d8..898b70213df 100644 --- a/core/chains/solana/chain_test.go +++ b/core/chains/solana/chain_test.go @@ -15,7 +15,7 @@ import ( "github.com/smartcontractkit/chainlink-relay/pkg/types" "github.com/smartcontractkit/chainlink-solana/pkg/solana/client" - "github.com/smartcontractkit/chainlink-solana/pkg/solana/config" + solcfg "github.com/smartcontractkit/chainlink-solana/pkg/solana/config" "github.com/smartcontractkit/chainlink-solana/pkg/solana/db" "github.com/smartcontractkit/chainlink/v2/core/logger" @@ -45,11 +45,17 @@ func TestSolanaChain_GetClient(t *testing.T) { defer mockServer.Close() solORM := &mockConfigs{} - lggr := logger.TestLogger(t) + + ch := solcfg.Chain{} + ch.SetDefaults() + cfg := &SolanaConfig{ + ChainID: ptr("devnet"), + Chain: ch, + } testChain := chain{ id: "devnet", nodes: solORM.Nodes, - cfg: config.NewConfig(db.ChainCfg{}, lggr), + cfg: cfg, lggr: logger.TestLogger(t), clientCache: map[string]*verifiedCachedClient{}, } @@ -139,9 +145,14 @@ func TestSolanaChain_VerifiedClient(t *testing.T) { })) defer mockServer.Close() - lggr := logger.TestLogger(t) + ch := solcfg.Chain{} + ch.SetDefaults() + cfg := &SolanaConfig{ + ChainID: ptr("devnet"), + Chain: ch, + } testChain := chain{ - cfg: config.NewConfig(db.ChainCfg{}, lggr), + cfg: cfg, lggr: logger.TestLogger(t), clientCache: map[string]*verifiedCachedClient{}, } @@ -177,10 +188,16 @@ func TestSolanaChain_VerifiedClient_ParallelClients(t *testing.T) { })) defer mockServer.Close() - lggr := logger.TestLogger(t) + ch := solcfg.Chain{} + ch.SetDefaults() + cfg := &SolanaConfig{ + ChainID: ptr("devnet"), + Enabled: ptr(true), + Chain: ch, + } testChain := chain{ id: "devnet", - cfg: config.NewConfig(db.ChainCfg{}, lggr), + cfg: cfg, lggr: logger.TestLogger(t), clientCache: map[string]*verifiedCachedClient{}, } @@ -234,3 +251,7 @@ func (m *mockConfigs) NodeStatus(s string) (types.NodeStatus, error) { panic("un func (m *mockConfigs) NodeStatusesPaged(offset, limit int, chainIDs ...string) (nodes []types.NodeStatus, count int, err error) { panic("unimplemented") } + +func ptr[T any](t T) *T { + return &t +} diff --git a/core/chains/solana/config.go b/core/chains/solana/config.go index e654a9029b1..c9d2f19d61c 100644 --- a/core/chains/solana/config.go +++ b/core/chains/solana/config.go @@ -10,7 +10,7 @@ import ( "go.uber.org/multierr" "golang.org/x/exp/slices" - "github.com/smartcontractkit/chainlink-relay/pkg/types" + relaytypes "github.com/smartcontractkit/chainlink-relay/pkg/types" solcfg "github.com/smartcontractkit/chainlink-solana/pkg/solana/config" soldb "github.com/smartcontractkit/chainlink-solana/pkg/solana/db" @@ -75,7 +75,7 @@ func (cs *SolanaConfigs) SetFrom(fs *SolanaConfigs) (err error) { return } -func (cs SolanaConfigs) Chains(ids ...string) (r []types.ChainStatus, err error) { +func (cs SolanaConfigs) Chains(ids ...string) (r []relaytypes.ChainStatus, err error) { for _, ch := range cs { if ch == nil { continue @@ -92,7 +92,7 @@ func (cs SolanaConfigs) Chains(ids ...string) (r []types.ChainStatus, err error) continue } } - ch2 := types.ChainStatus{ + ch2 := relaytypes.ChainStatus{ ID: *ch.ChainID, Enabled: ch.IsEnabled(), } @@ -140,7 +140,7 @@ func (cs SolanaConfigs) Nodes(chainID string) (ns []soldb.Node, err error) { return } -func (cs SolanaConfigs) NodeStatus(name string) (types.NodeStatus, error) { +func (cs SolanaConfigs) NodeStatus(name string) (relaytypes.NodeStatus, error) { for i := range cs { for _, n := range cs[i].Nodes { if n.Name != nil && *n.Name == name { @@ -148,10 +148,10 @@ func (cs SolanaConfigs) NodeStatus(name string) (types.NodeStatus, error) { } } } - return types.NodeStatus{}, fmt.Errorf("node %s: %w", name, chains.ErrNotFound) + return relaytypes.NodeStatus{}, fmt.Errorf("node %s: %w", name, chains.ErrNotFound) } -func (cs SolanaConfigs) NodeStatuses(chainIDs ...string) (ns []types.NodeStatus, err error) { +func (cs SolanaConfigs) NodeStatuses(chainIDs ...string) (ns []relaytypes.NodeStatus, err error) { if len(chainIDs) == 0 { for i := range cs { for _, n := range cs[i].Nodes { @@ -182,13 +182,13 @@ func (cs SolanaConfigs) NodeStatuses(chainIDs ...string) (ns []types.NodeStatus, return } -func nodeStatus(n *solcfg.Node, chainID string) (types.NodeStatus, error) { - var s types.NodeStatus +func nodeStatus(n *solcfg.Node, chainID string) (relaytypes.NodeStatus, error) { + var s relaytypes.NodeStatus s.ChainID = chainID s.Name = *n.Name b, err := toml.Marshal(n) if err != nil { - return types.NodeStatus{}, err + return relaytypes.NodeStatus{}, err } s.Config = string(b) return s, nil @@ -229,6 +229,7 @@ func legacySolNode(n *solcfg.Node, chainID string) soldb.Node { type SolanaConfig struct { ChainID *string + // Do not access directly, use [IsEnabled] Enabled *bool solcfg.Chain Nodes SolanaNodes diff --git a/core/chains/solana/relay_extender.go b/core/chains/solana/relay_extender.go new file mode 100644 index 00000000000..e25cce0b697 --- /dev/null +++ b/core/chains/solana/relay_extender.go @@ -0,0 +1,40 @@ +package solana + +import ( + "context" + "fmt" + "math/big" + + relaytypes "github.com/smartcontractkit/chainlink-relay/pkg/types" + "github.com/smartcontractkit/chainlink-solana/pkg/solana" + "github.com/smartcontractkit/chainlink/v2/core/services/relay" +) + +// TODO remove these wrappers after BCF-2441 +type RelayExtender struct { + solana.Chain + chainImpl *chain +} + +var _ relay.RelayerExt = &RelayExtender{} + +func NewRelayExtender(cfg *SolanaConfig, opts ChainOpts) (*RelayExtender, error) { + c, err := NewChain(cfg, opts) + if err != nil { + return nil, err + } + chainImpl, ok := (c).(*chain) + if !ok { + return nil, fmt.Errorf("internal error: cosmos relay extender got wrong type %t", c) + } + return &RelayExtender{Chain: chainImpl, chainImpl: chainImpl}, nil +} +func (r *RelayExtender) GetChainStatus(ctx context.Context) (relaytypes.ChainStatus, error) { + return r.chainImpl.GetChainStatus(ctx) +} +func (r *RelayExtender) ListNodeStatuses(ctx context.Context, pageSize int32, pageToken string) (stats []relaytypes.NodeStatus, nextPageToken string, total int, err error) { + return r.chainImpl.ListNodeStatuses(ctx, pageSize, pageToken) +} +func (r *RelayExtender) Transact(ctx context.Context, from, to string, amount *big.Int, balanceCheck bool) error { + return r.chainImpl.SendTx(ctx, from, to, amount, balanceCheck) +} diff --git a/core/chains/starknet/chain.go b/core/chains/starknet/chain.go index 9dfed44e57d..c6718c68065 100644 --- a/core/chains/starknet/chain.go +++ b/core/chains/starknet/chain.go @@ -2,37 +2,85 @@ package starknet import ( "context" + "fmt" "math/big" "math/rand" "github.com/pkg/errors" + "go.uber.org/multierr" "golang.org/x/exp/maps" "github.com/smartcontractkit/chainlink-relay/pkg/logger" "github.com/smartcontractkit/chainlink-relay/pkg/loop" + relaytypes "github.com/smartcontractkit/chainlink-relay/pkg/types" + starkChain "github.com/smartcontractkit/chainlink-starknet/relayer/pkg/chainlink/chain" + starkchain "github.com/smartcontractkit/chainlink-starknet/relayer/pkg/chainlink/chain" "github.com/smartcontractkit/chainlink-starknet/relayer/pkg/chainlink/config" "github.com/smartcontractkit/chainlink-starknet/relayer/pkg/chainlink/db" "github.com/smartcontractkit/chainlink-starknet/relayer/pkg/chainlink/txm" "github.com/smartcontractkit/chainlink-starknet/relayer/pkg/starknet" "github.com/smartcontractkit/chainlink/v2/core/chains" + "github.com/smartcontractkit/chainlink/v2/core/chains/internal" "github.com/smartcontractkit/chainlink/v2/core/chains/starknet/types" "github.com/smartcontractkit/chainlink/v2/core/utils" ) +type ChainOpts struct { + Logger logger.Logger + // the implementation used here needs to be co-ordinated with the starknet transaction manager keystore adapter + KeyStore loop.Keystore + Configs types.Configs +} + +func (o *ChainOpts) Name() string { + return o.Logger.Name() +} + +func (o *ChainOpts) Validate() (err error) { + required := func(s string) error { + return errors.Errorf("%s is required", s) + } + if o.Logger == nil { + err = multierr.Append(err, required("Logger'")) + } + if o.KeyStore == nil { + err = multierr.Append(err, required("KeyStore")) + } + if o.Configs == nil { + err = multierr.Append(err, required("Configs")) + } + return +} + +func (o *ChainOpts) ConfigsAndLogger() (chains.Configs[string, db.Node], logger.Logger) { + return o.Configs, o.Logger +} + var _ starkChain.Chain = (*chain)(nil) type chain struct { utils.StartStopOnce id string - cfg config.Config + cfg *StarknetConfig cfgs types.Configs lggr logger.Logger txm txm.StarkTXM } -func newChain(id string, cfg config.Config, loopKs loop.Keystore, cfgs types.Configs, lggr logger.Logger) (*chain, error) { +func NewChain(cfg *StarknetConfig, opts ChainOpts) (starkchain.Chain, error) { + if !cfg.IsEnabled() { + return nil, fmt.Errorf("cannot create new chain with ID %s: %w", *cfg.ChainID, chains.ErrChainDisabled) + } + c, err := newChain(*cfg.ChainID, cfg, opts.KeyStore, opts.Configs, opts.Logger) + if err != nil { + return nil, err + } + return c, nil +} + +func newChain(id string, cfg *StarknetConfig, loopKs loop.Keystore, cfgs types.Configs, lggr logger.Logger) (*chain, error) { lggr = logger.With(lggr, "starknetChainID", id) ch := &chain{ id: id, @@ -126,6 +174,52 @@ func (c *chain) HealthReport() map[string]error { return report } -func (c *chain) SendTx(ctx context.Context, from, to string, amount *big.Int, balanceCheck bool) error { +func (c *chain) ID() string { + return c.id +} + +// ChainService interface +func (c *chain) GetChainStatus(ctx context.Context) (relaytypes.ChainStatus, error) { + toml, err := c.cfg.TOMLString() + if err != nil { + return relaytypes.ChainStatus{}, err + } + return relaytypes.ChainStatus{ + ID: c.id, + Enabled: c.cfg.IsEnabled(), + Config: toml, + }, nil +} + +func (c *chain) ListNodeStatuses(ctx context.Context, pageSize int32, pageToken string) (stats []relaytypes.NodeStatus, nextPageToken string, total int, err error) { + return internal.ListNodeStatuses(int(pageSize), pageToken, c.listNodeStatuses) +} + +func (c *chain) Transact(ctx context.Context, from, to string, amount *big.Int, balanceCheck bool) error { return chains.ErrLOOPPUnsupported } + +func (c *chain) SendTx(ctx context.Context, from, to string, amount *big.Int, balanceCheck bool) error { + return c.Transact(ctx, from, to, amount, balanceCheck) +} + +// TODO BCF-2602 statuses are static for non-evm chain and should be dynamic +func (c *chain) listNodeStatuses(start, end int) ([]relaytypes.NodeStatus, int, error) { + stats := make([]relaytypes.NodeStatus, 0) + total := len(c.cfg.Nodes) + if start >= total { + return stats, total, internal.ErrOutOfRange + } + if end <= 0 || end > total { + end = total + } + nodes := c.cfg.Nodes[start:end] + for _, node := range nodes { + stat, err := nodeStatus(node, c.id) + if err != nil { + return stats, total, err + } + stats = append(stats, stat) + } + return stats, total, nil +} diff --git a/core/chains/starknet/chain_set.go b/core/chains/starknet/chain_set.go deleted file mode 100644 index ab2b5982e9b..00000000000 --- a/core/chains/starknet/chain_set.go +++ /dev/null @@ -1,76 +0,0 @@ -package starknet - -import ( - "github.com/pkg/errors" - "go.uber.org/multierr" - - "github.com/smartcontractkit/chainlink-relay/pkg/logger" - "github.com/smartcontractkit/chainlink-relay/pkg/loop" - starkchain "github.com/smartcontractkit/chainlink-starknet/relayer/pkg/chainlink/chain" - "github.com/smartcontractkit/chainlink-starknet/relayer/pkg/chainlink/db" - - "github.com/smartcontractkit/chainlink/v2/core/chains" - "github.com/smartcontractkit/chainlink/v2/core/chains/starknet/types" -) - -type ChainSetOpts struct { - Logger logger.Logger - // the implementation used here needs to be co-ordinated with the starknet transaction manager keystore adapter - KeyStore loop.Keystore - Configs types.Configs -} - -func (o *ChainSetOpts) Name() string { - return o.Logger.Name() -} - -func (o *ChainSetOpts) Validate() (err error) { - required := func(s string) error { - return errors.Errorf("%s is required", s) - } - if o.Logger == nil { - err = multierr.Append(err, required("Logger'")) - } - if o.KeyStore == nil { - err = multierr.Append(err, required("KeyStore")) - } - if o.Configs == nil { - err = multierr.Append(err, required("Configs")) - } - return -} - -func (o *ChainSetOpts) ConfigsAndLogger() (chains.Configs[string, db.Node], logger.Logger) { - return o.Configs, o.Logger -} - -func (o *ChainSetOpts) NewTOMLChain(cfg *StarknetConfig) (starkchain.Chain, error) { - if !cfg.IsEnabled() { - return nil, errors.Errorf("cannot create new chain with ID %s, the chain is disabled", *cfg.ChainID) - } - c, err := newChain(*cfg.ChainID, cfg, o.KeyStore, o.Configs, o.Logger) - if err != nil { - return nil, err - } - return c, nil -} - -func NewChainSet(opts ChainSetOpts, cfgs StarknetConfigs) (starkchain.ChainSet, error) { - stkChains := map[string]starkchain.Chain{} - var err error - for _, chain := range cfgs { - if !chain.IsEnabled() { - continue - } - var err2 error - stkChains[*chain.ChainID], err2 = opts.NewTOMLChain(chain) - if err2 != nil { - err = multierr.Combine(err, err2) - continue - } - } - if err != nil { - return nil, errors.Wrap(err, "failed to load some Solana chains") - } - return chains.NewChainSet[db.Node, starkchain.Chain](stkChains, &opts) -} diff --git a/core/chains/starknet/config.go b/core/chains/starknet/config.go index c555028c6fb..b28d8e6a487 100644 --- a/core/chains/starknet/config.go +++ b/core/chains/starknet/config.go @@ -197,6 +197,7 @@ func nodeStatus(n *stkcfg.Node, chainID string) (types.NodeStatus, error) { type StarknetConfig struct { ChainID *string + // Do not access directly. Use [IsEnabled] Enabled *bool stkcfg.Chain Nodes StarknetNodes diff --git a/core/chains/starknet/relay_extender.go b/core/chains/starknet/relay_extender.go new file mode 100644 index 00000000000..54d5ab2ab03 --- /dev/null +++ b/core/chains/starknet/relay_extender.go @@ -0,0 +1,43 @@ +package starknet + +import ( + "context" + "fmt" + "math/big" + + relaytypes "github.com/smartcontractkit/chainlink-relay/pkg/types" + starkchain "github.com/smartcontractkit/chainlink-starknet/relayer/pkg/chainlink/chain" + + "github.com/smartcontractkit/chainlink/v2/core/chains" + + "github.com/smartcontractkit/chainlink/v2/core/services/relay" +) + +// TODO remove these wrappers after BCF-2441 +type RelayExtender struct { + starkchain.Chain + chainImpl *chain +} + +var _ relay.RelayerExt = &RelayExtender{} + +func NewRelayExtender(cfg *StarknetConfig, opts ChainOpts) (*RelayExtender, error) { + c, err := NewChain(cfg, opts) + if err != nil { + return nil, err + } + chainImpl, ok := (c).(*chain) + if !ok { + return nil, fmt.Errorf("internal error: starkent relay extender got wrong type %t", c) + } + return &RelayExtender{Chain: chainImpl, chainImpl: chainImpl}, nil +} +func (r *RelayExtender) GetChainStatus(ctx context.Context) (relaytypes.ChainStatus, error) { + return r.chainImpl.GetChainStatus(ctx) +} +func (r *RelayExtender) ListNodeStatuses(ctx context.Context, pageSize int32, pageToken string) (stats []relaytypes.NodeStatus, nextPageToken string, total int, err error) { + return r.chainImpl.ListNodeStatuses(ctx, pageSize, pageToken) +} +func (r *RelayExtender) Transact(ctx context.Context, from, to string, amount *big.Int, balanceCheck bool) error { + return chains.ErrLOOPPUnsupported +} diff --git a/core/cmd/shell.go b/core/cmd/shell.go index 4215e36b496..14ed6ddd248 100644 --- a/core/cmd/shell.go +++ b/core/cmd/shell.go @@ -157,7 +157,7 @@ func (n ChainlinkAppFactory) NewApplication(ctx context.Context, cfg chainlink.G evmFactoryCfg := chainlink.EVMFactoryConfig{ CSAETHKeystore: keyStore, - RelayerConfig: evm.RelayerConfig{AppConfig: cfg, EventBroadcaster: eventBroadcaster, MailMon: mailMon}, + RelayerConfig: &evm.RelayerConfig{AppConfig: cfg, EventBroadcaster: eventBroadcaster, MailMon: mailMon}, } // evm always enabled for backward compatibility // TODO BCF-2510 this needs to change in order to clear the path for EVM extraction diff --git a/core/cmd/shell_local_test.go b/core/cmd/shell_local_test.go index db170d267d5..7681e2a9abe 100644 --- a/core/cmd/shell_local_test.go +++ b/core/cmd/shell_local_test.go @@ -89,7 +89,7 @@ func TestShell_RunNodeWithPasswords(t *testing.T) { Logger: lggr, DB: db, KeyStore: keyStore.Eth(), - RelayerConfig: evm.RelayerConfig{ + RelayerConfig: &evm.RelayerConfig{ AppConfig: cfg, EventBroadcaster: pg.NewNullEventBroadcaster(), MailMon: &utils.MailboxMonitor{}, @@ -196,7 +196,7 @@ func TestShell_RunNodeWithAPICredentialsFile(t *testing.T) { Logger: lggr, DB: db, KeyStore: keyStore.Eth(), - RelayerConfig: evm.RelayerConfig{ + RelayerConfig: &evm.RelayerConfig{ AppConfig: cfg, EventBroadcaster: pg.NewNullEventBroadcaster(), diff --git a/core/cmd/shell_test.go b/core/cmd/shell_test.go index 733c0058f37..45192392f6d 100644 --- a/core/cmd/shell_test.go +++ b/core/cmd/shell_test.go @@ -342,11 +342,26 @@ func TestSetupSolanaRelayer(t *testing.T) { lggr := logger.TestLogger(t) reg := plugins.NewLoopRegistry(lggr) ks := mocks.NewSolana(t) + + // config 3 chains but only enable 2 => should only be 2 relayer + nEnabledChains := 2 tConfig := configtest.NewGeneralConfig(t, func(c *chainlink.Config, s *chainlink.Secrets) { c.Solana = solana.SolanaConfigs{ &solana.SolanaConfig{ ChainID: ptr[string]("solana-id-1"), - Enabled: new(bool), + Enabled: ptr(true), + Chain: solcfg.Chain{}, + Nodes: []*solcfg.Node{}, + }, + &solana.SolanaConfig{ + ChainID: ptr[string]("solana-id-2"), + Enabled: ptr(true), + Chain: solcfg.Chain{}, + Nodes: []*solcfg.Node{}, + }, + &solana.SolanaConfig{ + ChainID: ptr[string]("disabled-solana-id-1"), + Enabled: ptr(false), Chain: solcfg.Chain{}, Nodes: []*solcfg.Node{}, }, @@ -365,7 +380,7 @@ func TestSetupSolanaRelayer(t *testing.T) { relayers, err := rf.NewSolana(ks, tConfig.SolanaConfigs()) require.NoError(t, err) require.NotNil(t, relayers) - require.Len(t, relayers, 1) + require.Len(t, relayers, nEnabledChains) // no using plugin, so registry should be empty require.Len(t, reg.List(), 0) }) @@ -376,22 +391,65 @@ func TestSetupSolanaRelayer(t *testing.T) { relayers, err := rf.NewSolana(ks, tConfig.SolanaConfigs()) require.NoError(t, err) require.NotNil(t, relayers) - require.Len(t, relayers, 1) + require.Len(t, relayers, nEnabledChains) // make sure registry has the plugin - require.Len(t, reg.List(), 1) + require.Len(t, reg.List(), nEnabledChains) + }) + + // test that duplicate enabled chains is an error when + duplicateConfig := configtest.NewGeneralConfig(t, func(c *chainlink.Config, s *chainlink.Secrets) { + c.Solana = solana.SolanaConfigs{ + &solana.SolanaConfig{ + ChainID: ptr[string]("dupe"), + Enabled: ptr(true), + Chain: solcfg.Chain{}, + Nodes: []*solcfg.Node{}, + }, + &solana.SolanaConfig{ + ChainID: ptr[string]("dupe"), + Enabled: ptr(true), + Chain: solcfg.Chain{}, + Nodes: []*solcfg.Node{}, + }, + } + }) + + // not parallel; shared state + t.Run("no plugin, duplicate chains", func(t *testing.T) { + _, err := rf.NewSolana(ks, duplicateConfig.SolanaConfigs()) + require.Error(t, err) }) + t.Run("plugin, duplicate chains", func(t *testing.T) { + t.Setenv("CL_SOLANA_CMD", "phony_solana_cmd") + _, err := rf.NewSolana(ks, duplicateConfig.SolanaConfigs()) + require.Error(t, err) + }) } func TestSetupStarkNetRelayer(t *testing.T) { lggr := logger.TestLogger(t) reg := plugins.NewLoopRegistry(lggr) ks := mocks.NewStarkNet(t) + // config 3 chains but only enable 2 => should only be 2 relayer + nEnabledChains := 2 tConfig := configtest.NewGeneralConfig(t, func(c *chainlink.Config, s *chainlink.Secrets) { c.Starknet = starknet.StarknetConfigs{ &starknet.StarknetConfig{ ChainID: ptr[string]("starknet-id-1"), - Enabled: new(bool), + Enabled: ptr(true), + Chain: stkcfg.Chain{}, + Nodes: []*config.Node{}, + }, + &starknet.StarknetConfig{ + ChainID: ptr[string]("starknet-id-2"), + Enabled: ptr(true), + Chain: stkcfg.Chain{}, + Nodes: []*config.Node{}, + }, + &starknet.StarknetConfig{ + ChainID: ptr[string]("disabled-starknet-id-1"), + Enabled: ptr(false), Chain: stkcfg.Chain{}, Nodes: []*config.Node{}, }, @@ -409,7 +467,7 @@ func TestSetupStarkNetRelayer(t *testing.T) { relayers, err := rf.NewStarkNet(ks, tConfig.StarknetConfigs()) require.NoError(t, err) require.NotNil(t, relayers) - require.Len(t, relayers, 1) + require.Len(t, relayers, nEnabledChains) // no using plugin, so registry should be empty require.Len(t, reg.List(), 0) }) @@ -420,9 +478,38 @@ func TestSetupStarkNetRelayer(t *testing.T) { relayers, err := rf.NewStarkNet(ks, tConfig.StarknetConfigs()) require.NoError(t, err) require.NotNil(t, relayers) - require.Len(t, relayers, 1) + require.Len(t, relayers, nEnabledChains) // make sure registry has the plugin - require.Len(t, reg.List(), 1) + require.Len(t, reg.List(), nEnabledChains) }) + // test that duplicate enabled chains is an error when + duplicateConfig := configtest.NewGeneralConfig(t, func(c *chainlink.Config, s *chainlink.Secrets) { + c.Starknet = starknet.StarknetConfigs{ + &starknet.StarknetConfig{ + ChainID: ptr[string]("dupe"), + Enabled: ptr(true), + Chain: stkcfg.Chain{}, + Nodes: []*config.Node{}, + }, + &starknet.StarknetConfig{ + ChainID: ptr[string]("dupe"), + Enabled: ptr(true), + Chain: stkcfg.Chain{}, + Nodes: []*config.Node{}, + }, + } + }) + + // not parallel; shared state + t.Run("no plugin, duplicate chains", func(t *testing.T) { + _, err := rf.NewStarkNet(ks, duplicateConfig.StarknetConfigs()) + require.Error(t, err) + }) + + t.Run("plugin, duplicate chains", func(t *testing.T) { + t.Setenv("CL_STARKNET_CMD", "phony_starknet_cmd") + _, err := rf.NewStarkNet(ks, duplicateConfig.StarknetConfigs()) + require.Error(t, err) + }) } diff --git a/core/cmd/solana_chains_commands_test.go b/core/cmd/solana_chains_commands_test.go index 7e8c2373be1..ac80b307d0a 100644 --- a/core/cmd/solana_chains_commands_test.go +++ b/core/cmd/solana_chains_commands_test.go @@ -16,8 +16,11 @@ func TestShell_IndexSolanaChains(t *testing.T) { t.Parallel() id := solanatest.RandomChainID() - chain := solana.SolanaConfig{ChainID: &id} - app := solanaStartNewApplication(t, &chain) + cfg := solana.SolanaConfig{ + ChainID: &id, + Enabled: ptr(true), + } + app := solanaStartNewApplication(t, &cfg) client, r := app.NewShellAndRenderer() require.Nil(t, cmd.SolanaChainClient(client).IndexChains(cltest.EmptyCLIContext())) diff --git a/core/cmd/solana_node_commands_test.go b/core/cmd/solana_node_commands_test.go index 4d6d585b260..f2ca6697555 100644 --- a/core/cmd/solana_node_commands_test.go +++ b/core/cmd/solana_node_commands_test.go @@ -29,7 +29,6 @@ func solanaStartNewApplication(t *testing.T, cfgs ...*solana.SolanaConfig) *clte }) } -// TODO fix https://smartcontract-it.atlassian.net/browse/BCF-2114 func TestShell_IndexSolanaNodes(t *testing.T) { t.Parallel() diff --git a/core/cmd/solana_transaction_commands_test.go b/core/cmd/solana_transaction_commands_test.go index 4fcb8ad84e0..a23a3dce5c2 100644 --- a/core/cmd/solana_transaction_commands_test.go +++ b/core/cmd/solana_transaction_commands_test.go @@ -34,6 +34,7 @@ func TestShell_SolanaSendSol(t *testing.T) { cfg := solana.SolanaConfig{ ChainID: &chainID, Nodes: solana.SolanaNodes{&node}, + Enabled: ptr(true), } app := solanaStartNewApplication(t, &cfg) from, err := app.GetKeyStore().Solana().Create() diff --git a/core/cmd/starknet_node_commands_test.go b/core/cmd/starknet_node_commands_test.go index 92490df2d18..e565b3e8138 100644 --- a/core/cmd/starknet_node_commands_test.go +++ b/core/cmd/starknet_node_commands_test.go @@ -6,11 +6,12 @@ import ( "testing" "github.com/pelletier/go-toml/v2" - "github.com/smartcontractkit/chainlink-relay/pkg/utils" - "github.com/smartcontractkit/chainlink-starknet/relayer/pkg/chainlink/config" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/smartcontractkit/chainlink-relay/pkg/utils" + "github.com/smartcontractkit/chainlink-starknet/relayer/pkg/chainlink/config" + "github.com/smartcontractkit/chainlink/v2/core/chains/starknet" "github.com/smartcontractkit/chainlink/v2/core/cmd" "github.com/smartcontractkit/chainlink/v2/core/internal/cltest" diff --git a/core/internal/cltest/cltest.go b/core/internal/cltest/cltest.go index c57e35d46f6..f147950c3d9 100644 --- a/core/internal/cltest/cltest.go +++ b/core/internal/cltest/cltest.go @@ -397,7 +397,7 @@ func NewApplicationWithConfig(t testing.TB, cfg chainlink.GeneralConfig, flagsAn chainId := ethClient.ConfiguredChainID() evmOpts := chainlink.EVMFactoryConfig{ - RelayerConfig: evm.RelayerConfig{ + RelayerConfig: &evm.RelayerConfig{ AppConfig: cfg, EventBroadcaster: eventBroadcaster, MailMon: mailMon, diff --git a/core/internal/testutils/evmtest/evmtest.go b/core/internal/testutils/evmtest/evmtest.go index 0fd391c3a23..43502ea35ec 100644 --- a/core/internal/testutils/evmtest/evmtest.go +++ b/core/internal/testutils/evmtest/evmtest.go @@ -83,7 +83,7 @@ func NewChainRelayExtOpts(t testing.TB, testopts TestChainOpts) evm.ChainRelayEx Logger: logger.TestLogger(t), DB: testopts.DB, KeyStore: testopts.KeyStore, - RelayerConfig: evm.RelayerConfig{ + RelayerConfig: &evm.RelayerConfig{ AppConfig: testopts.GeneralConfig, EventBroadcaster: pg.NewNullEventBroadcaster(), MailMon: testopts.MailMon, diff --git a/core/scripts/go.mod b/core/scripts/go.mod index 0fb3c9aa3df..888eddd8631 100644 --- a/core/scripts/go.mod +++ b/core/scripts/go.mod @@ -297,10 +297,10 @@ require ( github.com/shirou/gopsutil/v3 v3.22.12 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704 // indirect - github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20230824124058-9b063c470048 // indirect + github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20230824145305-c6541b2b0070 // indirect github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230830140037-e12ff6139b5a // indirect - github.com/smartcontractkit/chainlink-solana v1.0.3-0.20230802143301-165000751a85 // indirect - github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20230802150127-d2c95679d61a // indirect + github.com/smartcontractkit/chainlink-solana v1.0.3-0.20230824141217-c3b72b2683b9 // indirect + github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20230824155404-2f61ad01a404 // indirect github.com/smartcontractkit/tdh2/go/ocr2/decryptionplugin v0.0.0-20230829114801-14bf715f805e // indirect github.com/smartcontractkit/tdh2/go/tdh2 v0.0.0-20230829114801-14bf715f805e // indirect github.com/smartcontractkit/wsrpc v0.7.2 // indirect diff --git a/core/scripts/go.sum b/core/scripts/go.sum index a055c9a8411..7a925990856 100644 --- a/core/scripts/go.sum +++ b/core/scripts/go.sum @@ -1369,14 +1369,14 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704 h1:T3lFWumvbfM1u/etVq42Afwq/jtNSBSOA8n5jntnNPo= github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704/go.mod h1:2QuJdEouTWjh5BDy5o/vgGXQtR4Gz8yH1IYB5eT7u4M= -github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20230824124058-9b063c470048 h1:OHj8qzXajBAIT9TBnHN5LVGoCxvso/4JgCeg/l76Tgk= -github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20230824124058-9b063c470048/go.mod h1:xMwqRdj5vqYhCJXgKVqvyAwdcqM6ZAEhnwEQ4Khsop8= +github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20230824145305-c6541b2b0070 h1:goQdJP/27xeXuT85aE+dHc5SVa+A+395S6rkZ6NofF4= +github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20230824145305-c6541b2b0070/go.mod h1:xMwqRdj5vqYhCJXgKVqvyAwdcqM6ZAEhnwEQ4Khsop8= github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230830140037-e12ff6139b5a h1:7uEY4bgrH1dV/Vmpn/35yOO7lq2vWJiA1pQX8YohEOM= github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230830140037-e12ff6139b5a/go.mod h1:gWclxGW7rLkbjXn7FGizYlyKhp/boekto4MEYGyiMG4= -github.com/smartcontractkit/chainlink-solana v1.0.3-0.20230802143301-165000751a85 h1:/fm02hYSUdhbSh7xPn7os9yHj7dnl8aLs2+nFXPiB4g= -github.com/smartcontractkit/chainlink-solana v1.0.3-0.20230802143301-165000751a85/go.mod h1:H3/j2l84FsxYevCLNERdVasI7FVr+t2mkpv+BCJLSVw= -github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20230802150127-d2c95679d61a h1:b3rjvZLpTV45TmCV+ALX+EDDslf91pnDUugP54Lu9FA= -github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20230802150127-d2c95679d61a/go.mod h1:LL+FLf10gOUHrF3aUsRGEZlT/w8DaW5T/eEo/54W68c= +github.com/smartcontractkit/chainlink-solana v1.0.3-0.20230824141217-c3b72b2683b9 h1:Ktcpq5xOPHe90vbiPRfZ5NQDD7xwtwiwO9uTtiHqmLg= +github.com/smartcontractkit/chainlink-solana v1.0.3-0.20230824141217-c3b72b2683b9/go.mod h1:RIUJXn7EVp24TL2p4FW79dYjyno23x5mjt1nKN+5WEk= +github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20230824155404-2f61ad01a404 h1:OH9nyOrp5FYtI/ClAi6kCMHHN8dA7GW7jpOE7PId938= +github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20230824155404-2f61ad01a404/go.mod h1:/yp/sqD8Iz5GU5fcercjrw0ivJF7HDcupYg+Gjr7EPg= github.com/smartcontractkit/go-plugin v0.0.0-20230605132010-0f4d515d1472 h1:x3kNwgFlDmbE/n0gTSRMt9GBDfsfGrs4X9b9arPZtFI= github.com/smartcontractkit/go-plugin v0.0.0-20230605132010-0f4d515d1472/go.mod h1:6/1TEzT0eQznvI/gV2CM29DLSkAK/e58mUWKVsPaph0= github.com/smartcontractkit/grpc-proxy v0.0.0-20230731113816-f1be6620749f h1:hgJif132UCdjo8u43i7iPN1/MFnu49hv7lFGFftCHKU= diff --git a/core/services/chainlink/relayer_chain_interoperators.go b/core/services/chainlink/relayer_chain_interoperators.go index e4de1352925..01c46c691ff 100644 --- a/core/services/chainlink/relayer_chain_interoperators.go +++ b/core/services/chainlink/relayer_chain_interoperators.go @@ -106,6 +106,7 @@ func InitEVM(ctx context.Context, factory RelayerFactory, config EVMFactoryConfi legacyMap := make(map[string]evm.Chain) var defaultChain evm.Chain + for id, a := range adapters { // adapter is a service op.srvs = append(op.srvs, a) @@ -137,6 +138,7 @@ func InitCosmos(ctx context.Context, factory RelayerFactory, config CosmosFactor return fmt.Errorf("failed to setup Cosmos relayer: %w", err2) } legacyMap := make(map[string]cosmos.Chain) + for id, a := range adapters { op.srvs = append(op.srvs, a) op.loopRelayers[id] = a @@ -155,6 +157,7 @@ func InitSolana(ctx context.Context, factory RelayerFactory, config SolanaFactor if err2 != nil { return fmt.Errorf("failed to setup Solana relayer: %w", err2) } + for id, relayer := range solRelayers { op.srvs = append(op.srvs, relayer) op.loopRelayers[id] = relayer @@ -171,10 +174,12 @@ func InitStarknet(ctx context.Context, factory RelayerFactory, config StarkNetFa if err2 != nil { return fmt.Errorf("failed to setup StarkNet relayer: %w", err2) } + for id, relayer := range starkRelayers { op.srvs = append(op.srvs, relayer) op.loopRelayers[id] = relayer } + return nil } } @@ -211,7 +216,7 @@ func (rs *CoreRelayerChainInteroperators) ChainStatus(ctx context.Context, id re lr, err := rs.Get(id) if err != nil { - return types.ChainStatus{}, fmt.Errorf("%w: error getting chainstatus: %w", chains.ErrNotFound, err) + return types.ChainStatus{}, fmt.Errorf("%w: error getting chain status: %w", chains.ErrNotFound, err) } // this call is weird because the [loop.Relayer] interface still requires id // but in this context the `relayer` should only have only id diff --git a/core/services/chainlink/relayer_chain_interoperators_test.go b/core/services/chainlink/relayer_chain_interoperators_test.go index e86900ea0ee..3a0ce0c7c7d 100644 --- a/core/services/chainlink/relayer_chain_interoperators_test.go +++ b/core/services/chainlink/relayer_chain_interoperators_test.go @@ -206,7 +206,7 @@ func TestCoreRelayerChainInteroperators(t *testing.T) { {name: "2 evm chains with 3 nodes", initFuncs: []chainlink.CoreRelayerChainInitFunc{ chainlink.InitEVM(testctx, factory, chainlink.EVMFactoryConfig{ - RelayerConfig: evm.RelayerConfig{ + RelayerConfig: &evm.RelayerConfig{ AppConfig: cfg, EventBroadcaster: pg.NewNullEventBroadcaster(), MailMon: &utils.MailboxMonitor{}, @@ -221,7 +221,9 @@ func TestCoreRelayerChainInteroperators(t *testing.T) { {Network: relay.EVM, ChainID: relay.ChainID(evmChainID2.String())}, }, }, + {name: "2 solana chain with 2 node", + initFuncs: []chainlink.CoreRelayerChainInitFunc{ chainlink.InitSolana(testctx, factory, chainlink.SolanaFactoryConfig{ Keystore: keyStore.Solana(), @@ -234,7 +236,9 @@ func TestCoreRelayerChainInteroperators(t *testing.T) { {Network: relay.Solana, ChainID: relay.ChainID(solanaChainID2)}, }, }, + {name: "2 starknet chain with 4 nodes", + initFuncs: []chainlink.CoreRelayerChainInitFunc{ chainlink.InitStarknet(testctx, factory, chainlink.StarkNetFactoryConfig{ Keystore: keyStore.StarkNet(), @@ -247,6 +251,7 @@ func TestCoreRelayerChainInteroperators(t *testing.T) { {Network: relay.StarkNet, ChainID: relay.ChainID(starknetChainID2)}, }, }, + { name: "2 cosmos chains with 2 nodes", initFuncs: []chainlink.CoreRelayerChainInitFunc{ @@ -264,11 +269,12 @@ func TestCoreRelayerChainInteroperators(t *testing.T) { }, {name: "all chains", + initFuncs: []chainlink.CoreRelayerChainInitFunc{chainlink.InitSolana(testctx, factory, chainlink.SolanaFactoryConfig{ Keystore: keyStore.Solana(), SolanaConfigs: cfg.SolanaConfigs()}), chainlink.InitEVM(testctx, factory, chainlink.EVMFactoryConfig{ - RelayerConfig: evm.RelayerConfig{ + RelayerConfig: &evm.RelayerConfig{ AppConfig: cfg, EventBroadcaster: pg.NewNullEventBroadcaster(), MailMon: &utils.MailboxMonitor{}, diff --git a/core/services/chainlink/relayer_factory.go b/core/services/chainlink/relayer_factory.go index 51f18b7e61b..4480752621f 100644 --- a/core/services/chainlink/relayer_factory.go +++ b/core/services/chainlink/relayer_factory.go @@ -33,7 +33,7 @@ type RelayerFactory struct { } type EVMFactoryConfig struct { - evm.RelayerConfig + *evm.RelayerConfig evmrelay.CSAETHKeystore } @@ -44,7 +44,6 @@ func (r *RelayerFactory) NewEVM(ctx context.Context, config EVMFactoryConfig) (m // override some common opts with the factory values. this seems weird... maybe other signatures should change, or this should take a different type... ccOpts := evm.ChainRelayExtenderConfig{ - Logger: r.Logger, DB: r.DB, KeyStore: config.CSAETHKeystore.Eth(), @@ -84,43 +83,60 @@ func (r *RelayerFactory) NewSolana(ks keystore.Solana, chainCfgs solana.SolanaCo signer = &keystore.SolanaSigner{Solana: ks} ) + unique := make(map[string]struct{}) // create one relayer per chain id for _, chainCfg := range chainCfgs { + relayId := relay.ID{Network: relay.Solana, ChainID: relay.ChainID(*chainCfg.ChainID)} + _, alreadyExists := unique[relayId.Name()] + if alreadyExists { + return nil, fmt.Errorf("duplicate chain definitions for %s", relayId.Name()) + } + unique[relayId.Name()] = struct{}{} + + // skip disabled chains from further processing + if !chainCfg.IsEnabled() { + solLggr.Warnw("Skipping disabled chain", "id", chainCfg.ChainID) + continue + } + // all the lower level APIs expect chainsets. create a single valued set per id singleChainCfg := solana.SolanaConfigs{chainCfg} if cmdName := env.SolanaPluginCmd.Get(); cmdName != "" { // setup the solana relayer to be a LOOP - tomls, err := toml.Marshal(struct { - Solana solana.SolanaConfigs - }{Solana: singleChainCfg}) + cfgTOML, err := toml.Marshal(struct { + Solana solana.SolanaConfig + }{Solana: *chainCfg}) + if err != nil { return nil, fmt.Errorf("failed to marshal Solana configs: %w", err) } solCmdFn, err := plugins.NewCmdFactory(r.Register, plugins.CmdConfig{ - ID: solLggr.Name(), + ID: relayId.Name(), Cmd: cmdName, }) if err != nil { return nil, fmt.Errorf("failed to create Solana LOOP command: %w", err) } - solanaRelayers[relayId] = loop.NewRelayerService(solLggr, r.GRPCOpts, solCmdFn, string(tomls), signer) + + solanaRelayers[relayId] = loop.NewRelayerService(solLggr, r.GRPCOpts, solCmdFn, string(cfgTOML), signer) } else { - // fallback to embedded chainset - opts := solana.ChainSetOpts{ + // fallback to embedded chain + opts := solana.ChainOpts{ Logger: solLggr, KeyStore: signer, Configs: solana.NewConfigs(singleChainCfg), } - chainSet, err := solana.NewChainSet(opts, singleChainCfg) + + relayExt, err := solana.NewRelayExtender(chainCfg, opts) if err != nil { - return nil, fmt.Errorf("failed to load Solana chainset: %w", err) + return nil, err } - solanaRelayers[relayId] = relay.NewRelayerAdapter(pkgsolana.NewRelayer(solLggr, chainSet), chainSet) + solanaRelayers[relayId] = relay.NewRelayerAdapter(pkgsolana.NewRelayer(solLggr, relayExt), relayExt) } } return solanaRelayers, nil @@ -131,6 +147,8 @@ type StarkNetFactoryConfig struct { starknet.StarknetConfigs } +// TODO BCF-2606 consider consolidating the driving logic with that of NewSolana above via generics +// perhaps when we implement a Cosmos LOOP func (r *RelayerFactory) NewStarkNet(ks keystore.StarkNet, chainCfgs starknet.StarknetConfigs) (map[relay.ID]loop.Relayer, error) { starknetRelayers := make(map[relay.ID]loop.Relayer) @@ -139,23 +157,36 @@ func (r *RelayerFactory) NewStarkNet(ks keystore.StarkNet, chainCfgs starknet.St loopKs = &keystore.StarknetLooppSigner{StarkNet: ks} ) + unique := make(map[string]struct{}) // create one relayer per chain id for _, chainCfg := range chainCfgs { relayId := relay.ID{Network: relay.StarkNet, ChainID: relay.ChainID(*chainCfg.ChainID)} + _, alreadyExists := unique[relayId.Name()] + if alreadyExists { + return nil, fmt.Errorf("duplicate chain definitions for %s", relayId.Name()) + } + unique[relayId.Name()] = struct{}{} + + // skip disabled chains from further processing + if !chainCfg.IsEnabled() { + starkLggr.Warnw("Skipping disabled chain", "id", chainCfg.ChainID) + continue + } + // all the lower level APIs expect chainsets. create a single valued set per id singleChainCfg := starknet.StarknetConfigs{chainCfg} if cmdName := env.StarknetPluginCmd.Get(); cmdName != "" { // setup the starknet relayer to be a LOOP - tomls, err := toml.Marshal(struct { - Starknet starknet.StarknetConfigs - }{Starknet: singleChainCfg}) + cfgTOML, err := toml.Marshal(struct { + Starknet starknet.StarknetConfig + }{Starknet: *chainCfg}) if err != nil { return nil, fmt.Errorf("failed to marshal StarkNet configs: %w", err) } starknetCmdFn, err := plugins.NewCmdFactory(r.Register, plugins.CmdConfig{ - ID: starkLggr.Name(), + ID: relayId.Name(), Cmd: cmdName, }) if err != nil { @@ -163,19 +194,21 @@ func (r *RelayerFactory) NewStarkNet(ks keystore.StarkNet, chainCfgs starknet.St } // the starknet relayer service has a delicate keystore dependency. the value that is passed to NewRelayerService must // be compatible with instantiating a starknet transaction manager KeystoreAdapter within the LOOPp executable. - starknetRelayers[relayId] = loop.NewRelayerService(starkLggr, r.GRPCOpts, starknetCmdFn, string(tomls), loopKs) + starknetRelayers[relayId] = loop.NewRelayerService(starkLggr, r.GRPCOpts, starknetCmdFn, string(cfgTOML), loopKs) } else { // fallback to embedded chainset - opts := starknet.ChainSetOpts{ + opts := starknet.ChainOpts{ Logger: starkLggr, KeyStore: loopKs, Configs: starknet.NewConfigs(singleChainCfg), } - chainSet, err := starknet.NewChainSet(opts, singleChainCfg) + + relayExt, err := starknet.NewRelayExtender(chainCfg, opts) if err != nil { - return nil, fmt.Errorf("failed to load StarkNet chainset: %w", err) + return nil, err } - starknetRelayers[relayId] = relay.NewRelayerAdapter(pkgstarknet.NewRelayer(starkLggr, chainSet), chainSet) + + starknetRelayers[relayId] = relay.NewRelayerAdapter(pkgstarknet.NewRelayer(starkLggr, relayExt), relayExt) } } return starknetRelayers, nil @@ -199,7 +232,7 @@ func (r *RelayerFactory) NewCosmos(ctx context.Context, config CosmosFactoryConf // all the lower level APIs expect chainsets. create a single valued set per id // TODO: Cosmos LOOPp impl. For now, use relayer adapter - opts := cosmos.ChainSetOpts{ + opts := cosmos.ChainOpts{ QueryConfig: r.QConfig, Logger: lggr.Named(relayId.ChainID.String()), DB: r.DB, @@ -207,12 +240,13 @@ func (r *RelayerFactory) NewCosmos(ctx context.Context, config CosmosFactoryConf EventBroadcaster: config.EventBroadcaster, } opts.Configs = cosmos.NewConfigs(cosmos.CosmosConfigs{chainCfg}) - singleChainChainSet, err := cosmos.NewSingleChainSet(opts, chainCfg) + relayExt, err := cosmos.NewRelayExtender(chainCfg, opts) + if err != nil { return nil, fmt.Errorf("failed to load Cosmos chain %q: %w", relayId, err) } - relayers[relayId] = cosmos.NewLoopRelayerSingleChain(pkgcosmos.NewRelayer(lggr, singleChainChainSet), singleChainChainSet) + relayers[relayId] = cosmos.NewLoopRelayerChain(pkgcosmos.NewRelayer(lggr, relayExt), relayExt) } return relayers, nil diff --git a/core/services/relay/evm/relayer_extender.go b/core/services/relay/evm/relayer_extender.go index cab804e5883..90d314e105a 100644 --- a/core/services/relay/evm/relayer_extender.go +++ b/core/services/relay/evm/relayer_extender.go @@ -4,49 +4,29 @@ import ( "context" "fmt" "math/big" - "sync" "github.com/pkg/errors" "go.uber.org/multierr" - "golang.org/x/exp/maps" relaytypes "github.com/smartcontractkit/chainlink-relay/pkg/types" - "github.com/smartcontractkit/chainlink/v2/core/chains" "github.com/smartcontractkit/chainlink/v2/core/chains/evm" evmchain "github.com/smartcontractkit/chainlink/v2/core/chains/evm" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config/toml" - evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" - "github.com/smartcontractkit/chainlink/v2/core/logger" - "github.com/smartcontractkit/chainlink/v2/core/services" "github.com/smartcontractkit/chainlink/v2/core/services/relay" ) // ErrNoChains indicates that no EVM chains have been started var ErrNoChains = errors.New("no EVM chains loaded") -var _ legacyChainSet = &chainSet{} - -type legacyChainSet interface { - services.ServiceCtx - chains.ChainStatuser - chains.NodesStatuser - - Get(id *big.Int) (evmchain.Chain, error) - - Default() (evmchain.Chain, error) - Chains() []evmchain.Chain - ChainCount() int - - Configs() evmtypes.Configs - - SendTx(ctx context.Context, chainID, from, to string, amount *big.Int, balanceCheck bool) error -} - type EVMChainRelayerExtender interface { relay.RelayerExt Chain() evmchain.Chain Default() bool + // compatibility remove after BCF-2441 + ChainStatus(ctx context.Context, id string) (relaytypes.ChainStatus, error) + ChainStatuses(ctx context.Context, offset, limit int) ([]relaytypes.ChainStatus, int, error) + NodeStatuses(ctx context.Context, offset, limit int, chainIDs ...string) (nodes []relaytypes.NodeStatus, count int, err error) } type EVMChainRelayerExtenderSlicer interface { @@ -107,14 +87,28 @@ func (c *ChainRelayerExtenders) Len() int { // implements OneChain type ChainRelayerExt struct { - chain evmchain.Chain - // TODO remove this altogether. BFC-2440 - cs *chainSet + chain evmchain.Chain isDefault bool } var _ EVMChainRelayerExtender = &ChainRelayerExt{} +func (s *ChainRelayerExt) GetChainStatus(ctx context.Context) (relaytypes.ChainStatus, error) { + return s.chain.GetChainStatus(ctx) +} + +func (s *ChainRelayerExt) ListNodeStatuses(ctx context.Context, pageSize int32, pageToken string) (stats []relaytypes.NodeStatus, nextPageToken string, total int, err error) { + return s.chain.ListNodeStatuses(ctx, pageSize, pageToken) +} + +func (s *ChainRelayerExt) Transact(ctx context.Context, from, to string, amount *big.Int, balanceCheck bool) error { + return s.chain.Transact(ctx, from, to, amount, balanceCheck) +} + +func (s *ChainRelayerExt) ID() string { + return s.chain.ID().String() +} + func (s *ChainRelayerExt) Chain() evmchain.Chain { return s.chain } @@ -126,44 +120,43 @@ func (s *ChainRelayerExt) Default() bool { var ErrCorruptEVMChain = errors.New("corrupt evm chain") func (s *ChainRelayerExt) Start(ctx context.Context) error { - if len(s.cs.chains) > 1 { - err := fmt.Errorf("%w: internal error more than one chain (%d)", ErrCorruptEVMChain, len(s.cs.chains)) - panic(err) - } - return s.cs.Start(ctx) + return s.chain.Start(ctx) } func (s *ChainRelayerExt) Close() (err error) { - return s.cs.Close() + return s.chain.Close() } func (s *ChainRelayerExt) Name() string { // we set each private chainSet logger to contain the chain id - return s.cs.Name() + return s.chain.Name() } func (s *ChainRelayerExt) HealthReport() map[string]error { - return s.cs.HealthReport() + return s.chain.HealthReport() } func (s *ChainRelayerExt) Ready() (err error) { - return s.cs.Ready() + return s.chain.Ready() } var ErrInconsistentChainRelayerExtender = errors.New("inconsistent evm chain relayer extender") +// Chainset interface remove after BFC-2441 + +func (s *ChainRelayerExt) SendTx(ctx context.Context, from, to string, amount *big.Int, balanceCheck bool) error { + return s.Transact(ctx, from, to, amount, balanceCheck) +} + func (s *ChainRelayerExt) ChainStatus(ctx context.Context, id string) (relaytypes.ChainStatus, error) { - // TODO BCF-2441: update relayer interface - // we need to implement the interface, but passing id doesn't really make sense because there is only - // one chain here. check the id here to provide clear error reporting. if s.chain.ID().String() != id { return relaytypes.ChainStatus{}, fmt.Errorf("%w: given id %q does not match expected id %q", ErrInconsistentChainRelayerExtender, id, s.chain.ID()) } - return s.cs.ChainStatus(ctx, id) + return s.chain.GetChainStatus(ctx) } func (s *ChainRelayerExt) ChainStatuses(ctx context.Context, offset, limit int) ([]relaytypes.ChainStatus, int, error) { - stat, err := s.cs.ChainStatus(ctx, s.chain.ID().String()) + stat, err := s.chain.GetChainStatus(ctx) if err != nil { return nil, -1, err } @@ -171,7 +164,7 @@ func (s *ChainRelayerExt) ChainStatuses(ctx context.Context, offset, limit int) } -func (s *ChainRelayerExt) NodeStatuses(ctx context.Context, offset, limit int, chainIDs ...string) (nodes []relaytypes.NodeStatus, count int, err error) { +func (s *ChainRelayerExt) NodeStatuses(ctx context.Context, offset, limit int, chainIDs ...string) (nodes []relaytypes.NodeStatus, total int, err error) { if len(chainIDs) > 1 { return nil, -1, fmt.Errorf("single chain chain set only support one chain id. got %v", chainIDs) } @@ -179,207 +172,37 @@ func (s *ChainRelayerExt) NodeStatuses(ctx context.Context, offset, limit int, c if cid != s.chain.ID().String() { return nil, -1, fmt.Errorf("unknown chain id %s. expected %s", cid, s.chain.ID()) } - return s.cs.NodeStatuses(ctx, offset, limit, chainIDs...) -} - -func (s *ChainRelayerExt) SendTx(ctx context.Context, chainID, from, to string, amount *big.Int, balanceCheck bool) error { - return s.cs.SendTx(ctx, chainID, from, to, amount, balanceCheck) -} - -type chainSet struct { - defaultID *big.Int - chains map[string]evmchain.Chain - startedChains []evmchain.Chain - chainsMu sync.RWMutex - logger logger.Logger - opts evmchain.ChainRelayExtenderConfig -} - -func (cll *chainSet) Start(ctx context.Context) error { - if !cll.opts.AppConfig.EVMEnabled() { - cll.logger.Warn("EVM is disabled, no EVM-based chains will be started") - return nil - } - if !cll.opts.AppConfig.EVMRPCEnabled() { - cll.logger.Warn("EVM RPC connections are disabled. Chainlink will not connect to any EVM RPC node.") - } - var ms services.MultiStart - for _, c := range cll.Chains() { - if err := ms.Start(ctx, c); err != nil { - return errors.Wrapf(err, "failed to start chain %q", c.ID().String()) - } - cll.startedChains = append(cll.startedChains, c) - } - evmChainIDs := make([]*big.Int, len(cll.startedChains)) - for i, c := range cll.startedChains { - evmChainIDs[i] = c.ID() - } - defChainID := "unspecified" - if cll.defaultID != nil { - defChainID = fmt.Sprintf("%q", cll.defaultID.String()) - } - cll.logger.Infow(fmt.Sprintf("EVM: Started %d/%d chains, default chain ID is %s", len(cll.startedChains), len(cll.Chains()), defChainID), "startedEvmChainIDs", evmChainIDs) - return nil -} - -func (cll *chainSet) Close() (err error) { - cll.logger.Debug("EVM: stopping") - for _, c := range cll.startedChains { - err = multierr.Combine(err, c.Close()) - } - return -} - -func (cll *chainSet) Name() string { - return cll.logger.Name() -} - -func (cll *chainSet) HealthReport() map[string]error { - report := map[string]error{} - for _, c := range cll.Chains() { - maps.Copy(report, c.HealthReport()) - } - return report -} - -func (cll *chainSet) Ready() (err error) { - for _, c := range cll.Chains() { - err = multierr.Combine(err, c.Ready()) - } - return -} - -func (cll *chainSet) Get(id *big.Int) (evmchain.Chain, error) { - if id == nil { - if cll.defaultID == nil { - cll.logger.Debug("Chain ID not specified, and default is nil") - return nil, errors.New("chain ID not specified, and default is nil") - } - cll.logger.Debugf("Chain ID not specified, using default: %s", cll.defaultID.String()) - return cll.Default() - } - return cll.get(id.String()) -} - -func (cll *chainSet) get(id string) (evmchain.Chain, error) { - cll.chainsMu.RLock() - defer cll.chainsMu.RUnlock() - c, exists := cll.chains[id] - if exists { - return c, nil - } - return nil, errors.Wrap(chains.ErrNotFound, fmt.Sprintf("failed to get chain with id %s", id)) -} - -func (cll *chainSet) ChainStatus(ctx context.Context, id string) (cfg relaytypes.ChainStatus, err error) { - var cs []relaytypes.ChainStatus - cs, _, err = cll.opts.EVMConfigs().Chains(0, -1, id) + nodes, _, total, err = s.ListNodeStatuses(ctx, int32(limit), "") if err != nil { - return - } - l := len(cs) - if l == 0 { - err = fmt.Errorf("chain %s: %w", id, chains.ErrNotFound) - return - } - if l > 1 { - err = fmt.Errorf("multiple chains found: %d", len(cs)) - return - } - cfg = cs[0] - return -} - -func (cll *chainSet) ChainStatuses(ctx context.Context, offset, limit int) ([]relaytypes.ChainStatus, int, error) { - return cll.opts.EVMConfigs().Chains(offset, limit) -} - -func (cll *chainSet) Default() (evmchain.Chain, error) { - cll.chainsMu.RLock() - length := len(cll.chains) - cll.chainsMu.RUnlock() - if length == 0 { - return nil, errors.Wrap(ErrNoChains, "cannot get default EVM chain; no EVM chains are available") - } - if cll.defaultID == nil { - // This is an invariant violation; if any chains are available then a - // default should _always_ have been set in the constructor - return nil, errors.New("no default chain ID specified") - } - - return cll.Get(cll.defaultID) -} - -func (cll *chainSet) Chains() (c []evmchain.Chain) { - cll.chainsMu.RLock() - defer cll.chainsMu.RUnlock() - for _, chain := range cll.chains { - c = append(c, chain) - } - return c -} - -func (cll *chainSet) ChainCount() int { - cll.chainsMu.RLock() - defer cll.chainsMu.RUnlock() - return len(cll.chains) -} - -func (cll *chainSet) Configs() evmtypes.Configs { - return cll.opts.EVMConfigs() -} - -func (cll *chainSet) NodeStatuses(ctx context.Context, offset, limit int, chainIDs ...string) (nodes []relaytypes.NodeStatus, count int, err error) { - nodes, count, err = cll.opts.EVMConfigs().NodeStatusesPaged(offset, limit, chainIDs...) - if err != nil { - err = errors.Wrap(err, "GetNodesForChain failed to load nodes from DB") - return - } - for i := range nodes { - cll.addStateToNode(&nodes[i]) - } - return -} - -func (cll *chainSet) addStateToNode(n *relaytypes.NodeStatus) { - cll.chainsMu.RLock() - chain, exists := cll.chains[n.ChainID] - cll.chainsMu.RUnlock() - if !exists { - // The EVM chain is disabled - n.State = "Disabled" - return + return nil, -1, err } - states := chain.Client().NodeStates() - if states == nil { - n.State = "Unknown" - return + if len(nodes) < offset { + return []relaytypes.NodeStatus{}, -1, fmt.Errorf("out of range") } - state, exists := states[n.Name] - if exists { - n.State = state - return + if limit <= 0 { + limit = len(nodes) + } else if len(nodes) < limit { + limit = len(nodes) } - // The node is in the DB and the chain is enabled but it's not running - n.State = "NotLoaded" -} + return nodes[offset:limit], total, nil -func (cll *chainSet) SendTx(ctx context.Context, chainID, from, to string, amount *big.Int, balanceCheck bool) error { - chain, err := cll.get(chainID) - if err != nil { - return err - } - - return chain.SendTx(ctx, from, to, amount, balanceCheck) } func NewChainRelayerExtenders(ctx context.Context, opts evmchain.ChainRelayExtenderConfig) (*ChainRelayerExtenders, error) { if err := opts.Check(); err != nil { return nil, err } + + unique := make(map[string]struct{}) + evmConfigs := opts.AppConfig.EVMConfigs() var enabled []*toml.EVMConfig - for i := range evmConfigs { + for i, cfg := range evmConfigs { + _, alreadyExists := unique[cfg.ChainID.String()] + if alreadyExists { + return nil, fmt.Errorf("duplicate chain definition for evm chain id %s", cfg.ChainID.String()) + } + unique[cfg.ChainID.String()] = struct{}{} if evmConfigs[i].IsEnabled() { enabled = append(enabled, evmConfigs[i]) } @@ -404,34 +227,19 @@ func NewChainRelayerExtenders(ctx context.Context, opts evmchain.ChainRelayExten DB: opts.DB, KeyStore: opts.KeyStore, } - cll := newChainSet(privOpts) - cll.logger.Infow(fmt.Sprintf("Loading chain %s", cid), "evmChainID", cid) + privOpts.Logger.Infow(fmt.Sprintf("Loading chain %s", cid), "evmChainID", cid) chain, err2 := evmchain.NewTOMLChain(ctx, enabled[i], privOpts) if err2 != nil { err = multierr.Combine(err, err2) continue } - if _, exists := cll.chains[cid]; exists { - return nil, errors.Errorf("duplicate chain with ID %s", cid) - } - cll.chains[cid] = chain s := &ChainRelayerExt{ chain: chain, - cs: cll, isDefault: (cid == defaultChainID.String()), } result = append(result, s) } return newChainRelayerExtsFromSlice(result, opts.AppConfig), nil } - -func newChainSet(opts evmchain.ChainRelayExtenderConfig) *chainSet { - return &chainSet{ - chains: make(map[string]evmchain.Chain), - startedChains: make([]evmchain.Chain, 0), - logger: opts.Logger.Named("ChainSet"), - opts: opts, - } -} diff --git a/core/services/relay/relay.go b/core/services/relay/relay.go index c6d3ab6a900..fd89cf8290f 100644 --- a/core/services/relay/relay.go +++ b/core/services/relay/relay.go @@ -104,14 +104,13 @@ func (c ChainID) Int64() (int64, error) { // RelayerExt is a subset of [loop.Relayer] for adapting [types.Relayer], typically with a ChainSet. See [relayerAdapter]. type RelayerExt interface { - services.ServiceCtx - - ChainStatus(ctx context.Context, id string) (types.ChainStatus, error) - ChainStatuses(ctx context.Context, offset, limit int) ([]types.ChainStatus, int, error) - - NodeStatuses(ctx context.Context, offset, limit int, chainIDs ...string) (nodes []types.NodeStatus, count int, err error) - - SendTx(ctx context.Context, chainID, from, to string, amount *big.Int, balanceCheck bool) error + types.ChainService + // TODO remove after BFC-2441 + ID() string + GetChainStatus(ctx context.Context) (types.ChainStatus, error) + ListNodeStatuses(ctx context.Context, pageSize int32, pageToken string) (stats []types.NodeStatus, nextPageToken string, total int, err error) + // choose different name than SendTx to avoid collison during refactor. + Transact(ctx context.Context, from, to string, amount *big.Int, balanceCheck bool) error } var _ loop.Relayer = (*relayerAdapter)(nil) @@ -119,12 +118,14 @@ var _ loop.Relayer = (*relayerAdapter)(nil) // relayerAdapter adapts a [types.Relayer] and [RelayerExt] to implement [loop.Relayer]. type relayerAdapter struct { types.Relayer - RelayerExt + // TODO we can un-embedded `ext` once BFC-2441 is merged. Right now that's not possible + // because this are conflicting definitions of SendTx + ext RelayerExt } // NewRelayerAdapter returns a [loop.Relayer] adapted from a [types.Relayer] and [RelayerExt]. func NewRelayerAdapter(r types.Relayer, e RelayerExt) loop.Relayer { - return &relayerAdapter{Relayer: r, RelayerExt: e} + return &relayerAdapter{Relayer: r, ext: e} } func (r *relayerAdapter) NewConfigProvider(ctx context.Context, rargs types.RelayArgs) (types.ConfigProvider, error) { @@ -145,24 +146,88 @@ func (r *relayerAdapter) NewFunctionsProvider(ctx context.Context, rargs types.R func (r *relayerAdapter) Start(ctx context.Context) error { var ms services.MultiStart - return ms.Start(ctx, r.RelayerExt, r.Relayer) + return ms.Start(ctx, r.ext, r.Relayer) } func (r *relayerAdapter) Close() error { - return services.CloseAll(r.Relayer, r.RelayerExt) + return services.CloseAll(r.Relayer, r.ext) } func (r *relayerAdapter) Name() string { - return fmt.Sprintf("%s-%s", r.Relayer.Name(), r.RelayerExt.Name()) + return fmt.Sprintf("%s-%s", r.Relayer.Name(), r.ext.Name()) } func (r *relayerAdapter) Ready() (err error) { - return errors.Join(r.Relayer.Ready(), r.RelayerExt.Ready()) + return errors.Join(r.Relayer.Ready(), r.ext.Ready()) } func (r *relayerAdapter) HealthReport() map[string]error { hr := make(map[string]error) maps.Copy(r.Relayer.HealthReport(), hr) - maps.Copy(r.RelayerExt.HealthReport(), hr) + maps.Copy(r.ext.HealthReport(), hr) return hr } + +// Implement the existing [loop.Relayer] interface using the underlaying chain service +// TODO Delete this code after BFC-2441 + +func (r *relayerAdapter) ChainStatus(ctx context.Context, id string) (types.ChainStatus, error) { + if id != r.ext.ID() { + return types.ChainStatus{}, fmt.Errorf("unexpected chain id. got %s want %s", id, r.ID()) + } + return r.ext.GetChainStatus(ctx) +} +func (r *relayerAdapter) ChainStatuses(ctx context.Context, offset, limit int) ([]types.ChainStatus, int, error) { + stat, err := r.ext.GetChainStatus(ctx) + if err != nil { + return nil, -1, err + } + return []types.ChainStatus{stat}, 1, nil +} + +func (r *relayerAdapter) NodeStatuses(ctx context.Context, offset, limit int, chainIDs ...string) (nodes []types.NodeStatus, total int, err error) { + if len(chainIDs) > 1 { + return nil, 0, fmt.Errorf("internal error: node statuses expects at most one chain id got %v", chainIDs) + } + if len(chainIDs) == 1 && chainIDs[0] != r.ext.ID() { + return nil, 0, fmt.Errorf("node statuses unexpected chain id got %s want %s", chainIDs[0], r.ID()) + } + + nodes, _, total, err = r.ext.ListNodeStatuses(ctx, int32(limit), "") + if err != nil { + return nil, 0, err + } + if len(nodes) < offset { + return []types.NodeStatus{}, 0, fmt.Errorf("out of range") + } + if limit <= 0 { + limit = len(nodes) + } else if len(nodes) < limit { + limit = len(nodes) + } + return nodes[offset:limit], total, nil +} + +func (r *relayerAdapter) SendTx(ctx context.Context, chainID, from, to string, amount *big.Int, balanceCheck bool) error { + if chainID != r.ext.ID() { + return fmt.Errorf("send tx unexpected chain id. got %s want %s", chainID, r.ext.ID()) + } + return r.ext.Transact(ctx, from, to, amount, balanceCheck) +} + +func (r *relayerAdapter) ID() string { + return r.ext.ID() +} + +func (r *relayerAdapter) GetChainStatus(ctx context.Context) (types.ChainStatus, error) { + return r.ext.GetChainStatus(ctx) +} + +func (r *relayerAdapter) ListNodeStatuses(ctx context.Context, pageSize int32, pageToken string) (stats []types.NodeStatus, nextPageToken string, total int, err error) { + return r.ext.ListNodeStatuses(ctx, pageSize, pageToken) +} + +// choose different name than SendTx to avoid collison during refactor. +func (r *relayerAdapter) Transact(ctx context.Context, from, to string, amount *big.Int, balanceCheck bool) error { + return r.ext.Transact(ctx, from, to, amount, balanceCheck) +} diff --git a/core/web/starknet_nodes_controller.go b/core/web/starknet_nodes_controller.go index 5c6e5c4d575..d630bc102c1 100644 --- a/core/web/starknet_nodes_controller.go +++ b/core/web/starknet_nodes_controller.go @@ -11,5 +11,8 @@ var ErrStarkNetNotEnabled = errChainDisabled{name: "StarkNet", tomlKey: "Starkne func NewStarkNetNodesController(app chainlink.Application) NodesController { return newNodesController[presenters.StarkNetNodeResource]( - app.GetRelayers().List(chainlink.FilterRelayersByType(relay.StarkNet)), ErrStarkNetNotEnabled, presenters.NewStarkNetNodeResource, app.GetAuditLogger()) + app.GetRelayers().List(chainlink.FilterRelayersByType(relay.StarkNet)), + ErrStarkNetNotEnabled, + presenters.NewStarkNetNodeResource, + app.GetAuditLogger()) } diff --git a/go.mod b/go.mod index 6a78448103d..3774a2983d4 100644 --- a/go.mod +++ b/go.mod @@ -65,10 +65,10 @@ require ( github.com/shirou/gopsutil/v3 v3.22.12 github.com/shopspring/decimal v1.3.1 github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704 - github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20230824124058-9b063c470048 // TODO: update when https://github.com/smartcontractkit/chainlink-cosmos/pull/356 is merged + github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20230824145305-c6541b2b0070 github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230830140037-e12ff6139b5a - github.com/smartcontractkit/chainlink-solana v1.0.3-0.20230802143301-165000751a85 - github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20230802150127-d2c95679d61a + github.com/smartcontractkit/chainlink-solana v1.0.3-0.20230824141217-c3b72b2683b9 + github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20230824155404-2f61ad01a404 github.com/smartcontractkit/libocr v0.0.0-20230816220705-665e93233ae5 github.com/smartcontractkit/ocr2keepers v0.7.18 github.com/smartcontractkit/ocr2vrf v0.0.0-20230804151440-2f1eb1e20687 diff --git a/go.sum b/go.sum index 44c762385fb..50aba2a0cdc 100644 --- a/go.sum +++ b/go.sum @@ -1369,14 +1369,14 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704 h1:T3lFWumvbfM1u/etVq42Afwq/jtNSBSOA8n5jntnNPo= github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704/go.mod h1:2QuJdEouTWjh5BDy5o/vgGXQtR4Gz8yH1IYB5eT7u4M= -github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20230824124058-9b063c470048 h1:OHj8qzXajBAIT9TBnHN5LVGoCxvso/4JgCeg/l76Tgk= -github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20230824124058-9b063c470048/go.mod h1:xMwqRdj5vqYhCJXgKVqvyAwdcqM6ZAEhnwEQ4Khsop8= +github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20230824145305-c6541b2b0070 h1:goQdJP/27xeXuT85aE+dHc5SVa+A+395S6rkZ6NofF4= +github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20230824145305-c6541b2b0070/go.mod h1:xMwqRdj5vqYhCJXgKVqvyAwdcqM6ZAEhnwEQ4Khsop8= github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230830140037-e12ff6139b5a h1:7uEY4bgrH1dV/Vmpn/35yOO7lq2vWJiA1pQX8YohEOM= github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230830140037-e12ff6139b5a/go.mod h1:gWclxGW7rLkbjXn7FGizYlyKhp/boekto4MEYGyiMG4= -github.com/smartcontractkit/chainlink-solana v1.0.3-0.20230802143301-165000751a85 h1:/fm02hYSUdhbSh7xPn7os9yHj7dnl8aLs2+nFXPiB4g= -github.com/smartcontractkit/chainlink-solana v1.0.3-0.20230802143301-165000751a85/go.mod h1:H3/j2l84FsxYevCLNERdVasI7FVr+t2mkpv+BCJLSVw= -github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20230802150127-d2c95679d61a h1:b3rjvZLpTV45TmCV+ALX+EDDslf91pnDUugP54Lu9FA= -github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20230802150127-d2c95679d61a/go.mod h1:LL+FLf10gOUHrF3aUsRGEZlT/w8DaW5T/eEo/54W68c= +github.com/smartcontractkit/chainlink-solana v1.0.3-0.20230824141217-c3b72b2683b9 h1:Ktcpq5xOPHe90vbiPRfZ5NQDD7xwtwiwO9uTtiHqmLg= +github.com/smartcontractkit/chainlink-solana v1.0.3-0.20230824141217-c3b72b2683b9/go.mod h1:RIUJXn7EVp24TL2p4FW79dYjyno23x5mjt1nKN+5WEk= +github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20230824155404-2f61ad01a404 h1:OH9nyOrp5FYtI/ClAi6kCMHHN8dA7GW7jpOE7PId938= +github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20230824155404-2f61ad01a404/go.mod h1:/yp/sqD8Iz5GU5fcercjrw0ivJF7HDcupYg+Gjr7EPg= github.com/smartcontractkit/go-plugin v0.0.0-20230605132010-0f4d515d1472 h1:x3kNwgFlDmbE/n0gTSRMt9GBDfsfGrs4X9b9arPZtFI= github.com/smartcontractkit/go-plugin v0.0.0-20230605132010-0f4d515d1472/go.mod h1:6/1TEzT0eQznvI/gV2CM29DLSkAK/e58mUWKVsPaph0= github.com/smartcontractkit/grpc-proxy v0.0.0-20230731113816-f1be6620749f h1:hgJif132UCdjo8u43i7iPN1/MFnu49hv7lFGFftCHKU= diff --git a/integration-tests/go.mod b/integration-tests/go.mod index 71927ea6ef4..5ef7950bb67 100644 --- a/integration-tests/go.mod +++ b/integration-tests/go.mod @@ -380,10 +380,10 @@ require ( github.com/shopspring/decimal v1.3.1 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704 // indirect - github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20230824124058-9b063c470048 // indirect + github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20230824145305-c6541b2b0070 // indirect github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230830140037-e12ff6139b5a // indirect - github.com/smartcontractkit/chainlink-solana v1.0.3-0.20230802143301-165000751a85 // indirect - github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20230802150127-d2c95679d61a // indirect + github.com/smartcontractkit/chainlink-solana v1.0.3-0.20230824141217-c3b72b2683b9 // indirect + github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20230824155404-2f61ad01a404 // indirect github.com/smartcontractkit/sqlx v1.3.5-0.20210805004948-4be295aacbeb // indirect github.com/smartcontractkit/tdh2/go/ocr2/decryptionplugin v0.0.0-20230829114801-14bf715f805e // indirect github.com/smartcontractkit/tdh2/go/tdh2 v0.0.0-20230829114801-14bf715f805e // indirect diff --git a/integration-tests/go.sum b/integration-tests/go.sum index 7eba3198398..ef83f595b98 100644 --- a/integration-tests/go.sum +++ b/integration-tests/go.sum @@ -2237,16 +2237,16 @@ github.com/slack-go/slack v0.12.2 h1:x3OppyMyGIbbiyFhsBmpf9pwkUzMhthJMRNmNlA4LaQ github.com/slack-go/slack v0.12.2/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw= github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704 h1:T3lFWumvbfM1u/etVq42Afwq/jtNSBSOA8n5jntnNPo= github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704/go.mod h1:2QuJdEouTWjh5BDy5o/vgGXQtR4Gz8yH1IYB5eT7u4M= -github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20230824124058-9b063c470048 h1:OHj8qzXajBAIT9TBnHN5LVGoCxvso/4JgCeg/l76Tgk= -github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20230824124058-9b063c470048/go.mod h1:xMwqRdj5vqYhCJXgKVqvyAwdcqM6ZAEhnwEQ4Khsop8= +github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20230824145305-c6541b2b0070 h1:goQdJP/27xeXuT85aE+dHc5SVa+A+395S6rkZ6NofF4= +github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20230824145305-c6541b2b0070/go.mod h1:xMwqRdj5vqYhCJXgKVqvyAwdcqM6ZAEhnwEQ4Khsop8= github.com/smartcontractkit/chainlink-env v0.36.0 h1:CFOjs0c0y3lrHi/fl5qseCH9EQa5W/6CFyOvmhe2VnA= github.com/smartcontractkit/chainlink-env v0.36.0/go.mod h1:NbRExHmJGnKSYXmvNuJx5VErSx26GtE1AEN/CRzYOg8= github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230830140037-e12ff6139b5a h1:7uEY4bgrH1dV/Vmpn/35yOO7lq2vWJiA1pQX8YohEOM= github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230830140037-e12ff6139b5a/go.mod h1:gWclxGW7rLkbjXn7FGizYlyKhp/boekto4MEYGyiMG4= -github.com/smartcontractkit/chainlink-solana v1.0.3-0.20230802143301-165000751a85 h1:/fm02hYSUdhbSh7xPn7os9yHj7dnl8aLs2+nFXPiB4g= -github.com/smartcontractkit/chainlink-solana v1.0.3-0.20230802143301-165000751a85/go.mod h1:H3/j2l84FsxYevCLNERdVasI7FVr+t2mkpv+BCJLSVw= -github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20230802150127-d2c95679d61a h1:b3rjvZLpTV45TmCV+ALX+EDDslf91pnDUugP54Lu9FA= -github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20230802150127-d2c95679d61a/go.mod h1:LL+FLf10gOUHrF3aUsRGEZlT/w8DaW5T/eEo/54W68c= +github.com/smartcontractkit/chainlink-solana v1.0.3-0.20230824141217-c3b72b2683b9 h1:Ktcpq5xOPHe90vbiPRfZ5NQDD7xwtwiwO9uTtiHqmLg= +github.com/smartcontractkit/chainlink-solana v1.0.3-0.20230824141217-c3b72b2683b9/go.mod h1:RIUJXn7EVp24TL2p4FW79dYjyno23x5mjt1nKN+5WEk= +github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20230824155404-2f61ad01a404 h1:OH9nyOrp5FYtI/ClAi6kCMHHN8dA7GW7jpOE7PId938= +github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20230824155404-2f61ad01a404/go.mod h1:/yp/sqD8Iz5GU5fcercjrw0ivJF7HDcupYg+Gjr7EPg= github.com/smartcontractkit/chainlink-testing-framework v1.16.1-0.20230828224428-5b8a157a94d0 h1:DvIwUQc3/yWlvuXD/t30aOyX5BPVomM7OQT8ZO9VFFo= github.com/smartcontractkit/chainlink-testing-framework v1.16.1-0.20230828224428-5b8a157a94d0/go.mod h1:t6FJX3akEfAO31p96ru0ilNPfE9P2UshUlXTIkI58LM= github.com/smartcontractkit/go-plugin v0.0.0-20230605132010-0f4d515d1472 h1:x3kNwgFlDmbE/n0gTSRMt9GBDfsfGrs4X9b9arPZtFI= diff --git a/plugins/cmd/chainlink-solana/main.go b/plugins/cmd/chainlink-solana/main.go index 3e1d7c05cb5..150a9d840dd 100644 --- a/plugins/cmd/chainlink-solana/main.go +++ b/plugins/cmd/chainlink-solana/main.go @@ -56,21 +56,26 @@ func (c *pluginRelayer) NewRelayer(ctx context.Context, config string, keystore d := toml.NewDecoder(strings.NewReader(config)) d.DisallowUnknownFields() var cfg struct { - Solana solana.SolanaConfigs + Solana solana.SolanaConfig } + if err := d.Decode(&cfg); err != nil { - return nil, fmt.Errorf("failed to decode config toml: %w", err) + return nil, fmt.Errorf("failed to decode config toml: %w:\n\t%s", err, config) } - chainSet, err := solana.NewChainSet(solana.ChainSetOpts{ + // TODO BCF-2605 clean this up when the internal details of Solana Chain construction + // doesn't need `Configs` + cfgAdapter := solana.SolanaConfigs{&cfg.Solana} + opts := solana.ChainOpts{ Logger: c.Logger, KeyStore: keystore, - Configs: solana.NewConfigs(cfg.Solana), - }, cfg.Solana) + Configs: solana.NewConfigs(cfgAdapter), + } + relayExt, err := solana.NewRelayExtender(&cfg.Solana, opts) if err != nil { return nil, fmt.Errorf("failed to create chain: %w", err) } - ra := relay.NewRelayerAdapter(pkgsol.NewRelayer(c.Logger, chainSet), chainSet) + ra := relay.NewRelayerAdapter(pkgsol.NewRelayer(c.Logger, relayExt), relayExt) c.SubService(ra) diff --git a/plugins/cmd/chainlink-starknet/main.go b/plugins/cmd/chainlink-starknet/main.go index 835ea9a18d4..15bd0122121 100644 --- a/plugins/cmd/chainlink-starknet/main.go +++ b/plugins/cmd/chainlink-starknet/main.go @@ -60,21 +60,26 @@ func (c *pluginRelayer) NewRelayer(ctx context.Context, config string, loopKs lo d := toml.NewDecoder(strings.NewReader(config)) d.DisallowUnknownFields() var cfg struct { - Starknet starknet.StarknetConfigs + Starknet starknet.StarknetConfig } if err := d.Decode(&cfg); err != nil { - return nil, fmt.Errorf("failed to decode config toml: %w", err) + return nil, fmt.Errorf("failed to decode config toml: %w:\n\t%s", err, config) } - chainSet, err := starknet.NewChainSet(starknet.ChainSetOpts{ + // TODO BCF-2605 clean this up when the internal details of Chain construction + // doesn't need `Configs` + cfgAdapter := starknet.StarknetConfigs{&cfg.Starknet} + opts := starknet.ChainOpts{ Logger: c.Logger, KeyStore: loopKs, - Configs: starknet.NewConfigs(cfg.Starknet), - }, cfg.Starknet) + Configs: starknet.NewConfigs(cfgAdapter), + } + + relayExt, err := starknet.NewRelayExtender(&cfg.Starknet, opts) if err != nil { return nil, fmt.Errorf("failed to create chain: %w", err) } - ra := relay.NewRelayerAdapter(pkgstarknet.NewRelayer(c.Logger, chainSet), chainSet) + ra := relay.NewRelayerAdapter(pkgstarknet.NewRelayer(c.Logger, relayExt), relayExt) c.SubService(ra) diff --git a/plugins/loop_registry.go b/plugins/loop_registry.go index b24c6ee7d98..ec72a3c4c96 100644 --- a/plugins/loop_registry.go +++ b/plugins/loop_registry.go @@ -48,7 +48,7 @@ func (m *LoopRegistry) Register(id string) (*RegisteredLoop, error) { envCfg := NewEnvConfig(nextPort) m.registry[id] = &RegisteredLoop{Name: id, EnvCfg: envCfg} - m.lggr.Debug("Registered loopp %q with config %v, port %d", id, envCfg, envCfg.PrometheusPort()) + m.lggr.Debugf("Registered loopp %q with config %v, port %d", id, envCfg, envCfg.PrometheusPort()) return m.registry[id], nil }