Skip to content

Commit

Permalink
Consolidate configs for the EVM chain client for easier external use (#…
Browse files Browse the repository at this point in the history
…12294)

* Consolidated configs for easier external use and moved the evm client out of legacyevm

* Reused existing config types and created centralized models for client types

* Removed deprecated client constructor

* Renamed the client config builder file

* Added evm client and config builder tests

* Fixed linting

* Reverted client type aliases
  • Loading branch information
amit-momin authored Mar 13, 2024
1 parent 608ea0a commit 594a7f0
Show file tree
Hide file tree
Showing 9 changed files with 380 additions and 59 deletions.
15 changes: 1 addition & 14 deletions core/chains/evm/client/chain_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,20 +51,7 @@ func NewChainClient(
chainID *big.Int,
chainType config.ChainType,
) Client {
multiNode := commonclient.NewMultiNode[
*big.Int,
evmtypes.Nonce,
common.Address,
common.Hash,
*types.Transaction,
common.Hash,
types.Log,
ethereum.FilterQuery,
*evmtypes.Receipt,
*assets.Wei,
*evmtypes.Head,
RPCClient,
](
multiNode := commonclient.NewMultiNode(
lggr,
selectionMode,
leaseDuration,
Expand Down
100 changes: 100 additions & 0 deletions core/chains/evm/client/config_builder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package client

import (
"fmt"
"net/url"
"time"

"go.uber.org/multierr"

commonconfig "github.com/smartcontractkit/chainlink-common/pkg/config"
"github.com/smartcontractkit/chainlink/v2/common/config"
evmconfig "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config"
"github.com/smartcontractkit/chainlink/v2/core/chains/evm/config/toml"
)

type nodeConfig struct {
Name *string
WSURL *string
HTTPURL *string
SendOnly *bool
Order *int32
}

// Build the configs needed to initialize the chain client
// Parameters should only be basic go types to make it accessible for external users
// Configs can be stored in a variety of ways
func NewClientConfigs(
selectionMode *string,
leaseDuration time.Duration,
chainType string,
nodeCfgs []nodeConfig,
pollFailureThreshold *uint32,
pollInterval time.Duration,
syncThreshold *uint32,
nodeIsSyncingEnabled *bool,
) (evmconfig.NodePool, []*toml.Node, config.ChainType, error) {
nodes, err := parseNodeConfigs(nodeCfgs)
if err != nil {
return nil, nil, "", err
}
nodePool := toml.NodePool{
SelectionMode: selectionMode,
LeaseDuration: commonconfig.MustNewDuration(leaseDuration),
PollFailureThreshold: pollFailureThreshold,
PollInterval: commonconfig.MustNewDuration(pollInterval),
SyncThreshold: syncThreshold,
NodeIsSyncingEnabled: nodeIsSyncingEnabled,
}
nodePoolCfg := &evmconfig.NodePoolConfig{C: nodePool}
return nodePoolCfg, nodes, config.ChainType(chainType), nil
}

func parseNodeConfigs(nodeCfgs []nodeConfig) ([]*toml.Node, error) {
nodes := make([]*toml.Node, len(nodeCfgs))
for i, nodeCfg := range nodeCfgs {
if nodeCfg.WSURL == nil || nodeCfg.HTTPURL == nil {
return nil, fmt.Errorf("node config [%d]: missing WS or HTTP URL", i)
}
wsUrl := commonconfig.MustParseURL(*nodeCfg.WSURL)
httpUrl := commonconfig.MustParseURL(*nodeCfg.HTTPURL)
node := &toml.Node{
Name: nodeCfg.Name,
WSURL: wsUrl,
HTTPURL: httpUrl,
SendOnly: nodeCfg.SendOnly,
Order: nodeCfg.Order,
}
nodes[i] = node
}

if err := validateNodeConfigs(nodes); err != nil {
return nil, err
}

return nodes, nil
}

func validateNodeConfigs(nodes []*toml.Node) (err error) {
names := commonconfig.UniqueStrings{}
wsURLs := commonconfig.UniqueStrings{}
httpURLs := commonconfig.UniqueStrings{}
for i, node := range nodes {
if nodeErr := node.ValidateConfig(); nodeErr != nil {
err = multierr.Append(err, nodeErr)
}
if names.IsDupe(node.Name) {
err = multierr.Append(err, commonconfig.NewErrDuplicate(fmt.Sprintf("Nodes.%d.Name", i), *node.Name))
}
u := (*url.URL)(node.WSURL)
if wsURLs.IsDupeFmt(u) {
err = multierr.Append(err, commonconfig.NewErrDuplicate(fmt.Sprintf("Nodes.%d.WSURL", i), u.String()))
}
u = (*url.URL)(node.HTTPURL)
if httpURLs.IsDupeFmt(u) {
err = multierr.Append(err, commonconfig.NewErrDuplicate(fmt.Sprintf("Nodes.%d.HTTPURL", i), u.String()))
}
}

return err
}
181 changes: 181 additions & 0 deletions core/chains/evm/client/config_builder_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package client_test

import (
"testing"
"time"

"github.com/stretchr/testify/require"

"github.com/smartcontractkit/chainlink/v2/core/chains/evm/client"
)

func TestClientConfigBuilder(t *testing.T) {
t.Parallel()

selectionMode := ptr("HighestHead")
leaseDuration := 0 * time.Second
pollFailureThreshold := ptr(uint32(5))
pollInterval := 10 * time.Second
syncThreshold := ptr(uint32(5))
nodeIsSyncingEnabled := ptr(false)
chainTypeStr := ""
nodeConfigs := []client.TestNodeConfig{
{
Name: ptr("foo"),
WSURL: ptr("ws://foo.test"),
HTTPURL: ptr("http://foo.test"),
},
}
nodePool, nodes, chainType, err := client.NewClientConfigs(selectionMode, leaseDuration, chainTypeStr, nodeConfigs, pollFailureThreshold, pollInterval, syncThreshold, nodeIsSyncingEnabled)
require.NoError(t, err)

// Validate node pool configs
require.Equal(t, *selectionMode, nodePool.SelectionMode())
require.Equal(t, leaseDuration, nodePool.LeaseDuration())
require.Equal(t, *pollFailureThreshold, nodePool.PollFailureThreshold())
require.Equal(t, pollInterval, nodePool.PollInterval())
require.Equal(t, *syncThreshold, nodePool.SyncThreshold())
require.Equal(t, *nodeIsSyncingEnabled, nodePool.NodeIsSyncingEnabled())

// Validate node configs
require.Equal(t, *nodeConfigs[0].Name, *nodes[0].Name)
require.Equal(t, *nodeConfigs[0].WSURL, (*nodes[0].WSURL).String())
require.Equal(t, *nodeConfigs[0].HTTPURL, (*nodes[0].HTTPURL).String())

// Validate chain type
require.Equal(t, chainTypeStr, string(chainType))
}

func TestNodeConfigs(t *testing.T) {
t.Parallel()

t.Run("parsing unique node configs succeeds", func(t *testing.T) {
nodeConfigs := []client.TestNodeConfig{
{
Name: ptr("foo1"),
WSURL: ptr("ws://foo1.test"),
HTTPURL: ptr("http://foo1.test"),
},
{
Name: ptr("foo2"),
WSURL: ptr("ws://foo2.test"),
HTTPURL: ptr("http://foo2.test"),
},
}
tomlNodes, err := client.ParseTestNodeConfigs(nodeConfigs)
require.NoError(t, err)
require.Len(t, tomlNodes, len(nodeConfigs))
})

t.Run("parsing missing ws url fails", func(t *testing.T) {
nodeConfigs := []client.TestNodeConfig{
{
Name: ptr("foo1"),
HTTPURL: ptr("http://foo1.test"),
},
}
_, err := client.ParseTestNodeConfigs(nodeConfigs)
require.Error(t, err)
})

t.Run("parsing missing http url fails", func(t *testing.T) {
nodeConfigs := []client.TestNodeConfig{
{
Name: ptr("foo1"),
WSURL: ptr("ws://foo1.test"),
},
}
_, err := client.ParseTestNodeConfigs(nodeConfigs)
require.Error(t, err)
})

t.Run("parsing invalid ws url fails", func(t *testing.T) {
nodeConfigs := []client.TestNodeConfig{
{
Name: ptr("foo1"),
WSURL: ptr("http://foo1.test"),
HTTPURL: ptr("http://foo1.test"),
},
}
_, err := client.ParseTestNodeConfigs(nodeConfigs)
require.Error(t, err)
})

t.Run("parsing duplicate http url fails", func(t *testing.T) {
nodeConfigs := []client.TestNodeConfig{
{
Name: ptr("foo1"),
WSURL: ptr("ws://foo1.test"),
HTTPURL: ptr("ws://foo1.test"),
},
}
_, err := client.ParseTestNodeConfigs(nodeConfigs)
require.Error(t, err)
})

t.Run("parsing duplicate node names fails", func(t *testing.T) {
nodeConfigs := []client.TestNodeConfig{
{
Name: ptr("foo1"),
WSURL: ptr("ws://foo1.test"),
HTTPURL: ptr("http://foo1.test"),
},
{
Name: ptr("foo1"),
WSURL: ptr("ws://foo2.test"),
HTTPURL: ptr("http://foo2.test"),
},
}
_, err := client.ParseTestNodeConfigs(nodeConfigs)
require.Error(t, err)
})

t.Run("parsing duplicate node ws urls fails", func(t *testing.T) {
nodeConfigs := []client.TestNodeConfig{
{
Name: ptr("foo1"),
WSURL: ptr("ws://foo1.test"),
HTTPURL: ptr("http://foo1.test"),
},
{
Name: ptr("foo2"),
WSURL: ptr("ws://foo2.test"),
HTTPURL: ptr("http://foo1.test"),
},
}
_, err := client.ParseTestNodeConfigs(nodeConfigs)
require.Error(t, err)
})

t.Run("parsing duplicate node http urls fails", func(t *testing.T) {
nodeConfigs := []client.TestNodeConfig{
{
Name: ptr("foo1"),
WSURL: ptr("ws://foo1.test"),
HTTPURL: ptr("http://foo1.test"),
},
{
Name: ptr("foo2"),
WSURL: ptr("ws://foo1.test"),
HTTPURL: ptr("http://foo2.test"),
},
}
_, err := client.ParseTestNodeConfigs(nodeConfigs)
require.Error(t, err)
})

t.Run("parsing order too large fails", func(t *testing.T) {
nodeConfigs := []client.TestNodeConfig{
{
Name: ptr("foo1"),
WSURL: ptr("ws://foo1.test"),
HTTPURL: ptr("http://foo1.test"),
Order: ptr(int32(101)),
},
}
_, err := client.ParseTestNodeConfigs(nodeConfigs)
require.Error(t, err)
})
}

func ptr[T any](t T) *T { return &t }
37 changes: 37 additions & 0 deletions core/chains/evm/client/evm_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package client

import (
"math/big"
"net/url"
"time"

"github.com/smartcontractkit/chainlink-common/pkg/logger"
commonclient "github.com/smartcontractkit/chainlink/v2/common/client"
"github.com/smartcontractkit/chainlink/v2/common/config"
evmconfig "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config"
"github.com/smartcontractkit/chainlink/v2/core/chains/evm/config/toml"
evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types"
)

func NewEvmClient(cfg evmconfig.NodePool, noNewHeadsThreshold time.Duration, lggr logger.Logger, chainID *big.Int, chainType config.ChainType, nodes []*toml.Node) Client {
var empty url.URL
var primaries []commonclient.Node[*big.Int, *evmtypes.Head, RPCClient]
var sendonlys []commonclient.SendOnlyNode[*big.Int, RPCClient]
for i, node := range nodes {
if node.SendOnly != nil && *node.SendOnly {
rpc := NewRPCClient(lggr, empty, (*url.URL)(node.HTTPURL), *node.Name, int32(i), chainID,
commonclient.Secondary)
sendonly := commonclient.NewSendOnlyNode(lggr, (url.URL)(*node.HTTPURL),
*node.Name, chainID, rpc)
sendonlys = append(sendonlys, sendonly)
} else {
rpc := NewRPCClient(lggr, (url.URL)(*node.WSURL), (*url.URL)(node.HTTPURL), *node.Name, int32(i),
chainID, commonclient.Primary)
primaryNode := commonclient.NewNode(cfg, noNewHeadsThreshold,
lggr, (url.URL)(*node.WSURL), (*url.URL)(node.HTTPURL), *node.Name, int32(i), chainID, *node.Order,
rpc, "EVM")
primaries = append(primaries, primaryNode)
}
}
return NewChainClient(lggr, cfg.SelectionMode(), cfg.LeaseDuration(), noNewHeadsThreshold, primaries, sendonlys, chainID, chainType)
}
37 changes: 37 additions & 0 deletions core/chains/evm/client/evm_client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package client_test

import (
"testing"
"time"

"github.com/stretchr/testify/require"

"github.com/smartcontractkit/chainlink/v2/core/chains/evm/client"
"github.com/smartcontractkit/chainlink/v2/core/internal/testutils"
"github.com/smartcontractkit/chainlink/v2/core/logger"
)

func TestNewEvmClient(t *testing.T) {
t.Parallel()

noNewHeadsThreshold := 3 * time.Minute
selectionMode := ptr("HighestHead")
leaseDuration := 0 * time.Second
pollFailureThreshold := ptr(uint32(5))
pollInterval := 10 * time.Second
syncThreshold := ptr(uint32(5))
nodeIsSyncingEnabled := ptr(false)
chainTypeStr := ""
nodeConfigs := []client.TestNodeConfig{
{
Name: ptr("foo"),
WSURL: ptr("ws://foo.test"),
HTTPURL: ptr("http://foo.test"),
},
}
nodePool, nodes, chainType, err := client.NewClientConfigs(selectionMode, leaseDuration, chainTypeStr, nodeConfigs, pollFailureThreshold, pollInterval, syncThreshold, nodeIsSyncingEnabled)
require.NoError(t, err)

client := client.NewEvmClient(nodePool, noNewHeadsThreshold, logger.TestLogger(t), testutils.FixtureChainID, chainType, nodes)
require.NotNil(t, client)
}
7 changes: 7 additions & 0 deletions core/chains/evm/client/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
commonclient "github.com/smartcontractkit/chainlink/v2/common/client"
commonconfig "github.com/smartcontractkit/chainlink/v2/common/config"
"github.com/smartcontractkit/chainlink/v2/core/chains/evm/config"
"github.com/smartcontractkit/chainlink/v2/core/chains/evm/config/toml"
evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types"
)

Expand Down Expand Up @@ -185,3 +186,9 @@ func (mes *mockSubscription) Unsubscribe() {
mes.unsubscribed = true
close(mes.Errors)
}

type TestNodeConfig = nodeConfig

func ParseTestNodeConfigs(nodes []TestNodeConfig) ([]*toml.Node, error) {
return parseNodeConfigs(nodes)
}
Loading

0 comments on commit 594a7f0

Please sign in to comment.