Skip to content

Commit

Permalink
Merge branch 'main' of github.com:smartcontractkit/chainlink-testing-…
Browse files Browse the repository at this point in the history
…framework into parrotServer
  • Loading branch information
kalverra committed Jan 23, 2025
2 parents 26c8b03 + 72387bd commit af6f589
Show file tree
Hide file tree
Showing 17 changed files with 1,253 additions and 404 deletions.
62 changes: 62 additions & 0 deletions book/src/libs/seth.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Reliable and debug-friendly Ethereum client
5. [Configuration](#config)
1. [Simplified configuration](#simplified-configuration)
2. [ClientBuilder](#clientbuilder)
1. [Simulated Backend](#simulated-backend)
3. [Supported env vars](#supported-env-vars)
4. [TOML configuration](#toml-configuration)
6. [Automated gas price estimation](#automatic-gas-estimator)
Expand Down Expand Up @@ -263,6 +264,67 @@ if err != nil {
```
This can be useful if you already have a config, but want to modify it slightly. It can also be useful if you read TOML config with multiple `Networks` and you want to specify which one you want to use.

### Simulated Backend

Last, but not least, `ClientBuilder` allows you to pass custom implementation of `simulated.Client` interface, which include Geth's [Simulated Backend](https://github.com/ethereum/go-ethereum/blob/master/ethclient/simulated/backend.go), which might be very useful for rapid testing against
in-memory environment. When using that option bear in mind that:
* passing RPC URL is not allowed and will result in error
* tracing is disabled

> [!NOTE]
> Simulated Backend doesn't support tracing, because it doesn't expose the JSON-RPC `Call(result interface{}, method string, args ...interface{})` method, which we use to fetch debug information.
So how do you use Seth with simulated backend?
```go
var startBackend := func(fundedAddresses []common.Address) (*simulated.Backend, context.CancelFunc) {
toFund := make(map[common.Address]types.Account)
for _, address := range fundedAddresses {
toFund[address] = types.Account{
Balance: big.NewInt(1000000000000000000), // 1 Ether
}
}
backend := simulated.NewBackend(toFund)

ctx, cancelFn := context.WithCancel(context.Background())

// 100ms block time
ticker := time.NewTicker(100 * time.Millisecond)
go func() {
for {
select {
case <-ticker.C:
backend.Commit()
case <-ctx.Done():
backend.Close()
return
}
}
}()

return backend, cancelFn
}

// 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 is the default dev account
backend, cancelFn := startBackend(
[]common.Address{common.HexToAddress("0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266")},
)
defer func() { cancelFn() }()

client, err := builder.
WithNetworkName("simulated").
WithEthClient(backend.Client()).
WithPrivateKeys([]string{"ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"}).
Build()

require.NoError(t, err, "failed to build client")
_ = client
```

> [!WARNING]
> When using `simulated.Backend` do remember that it doesn't automatically mine blocks. You need to call `backend.Commit()` manually
> to mine a new block and have your transactions processed. The best way to do it is having a goroutine running in the background
> that either mines at specific intervals or when it receives a message on channel.
### Supported env vars

Some crucial data is stored in env vars, create `.envrc` and use `source .envrc`, or use `direnv`
Expand Down
5 changes: 5 additions & 0 deletions lib/.changeset/v1.50.21.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
- Fix `lib` imports and dependencies
- Fix start up of Nethermind 1.30.1+ containers
- Fix docker 8080 port mappings
- Do not change container name, when restarting it
- Automatically forward `SETH_LOG_LEVEL` to k8s
1 change: 1 addition & 0 deletions seth/.changeset/v1.50.12.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- improvement: allow to pass your own `ethclient` to Seth (which allows to use it with `go-ethereum`'s Simulated Backend)
115 changes: 73 additions & 42 deletions seth/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ import (

"github.com/avast/retry-go"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/ethclient/simulated"
"github.com/ethereum/go-ethereum/rpc"

"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"golang.org/x/sync/errgroup"
Expand Down Expand Up @@ -64,7 +65,7 @@ var (
// Client is a vanilla go-ethereum client with enhanced debug logging
type Client struct {
Cfg *Config
Client *ethclient.Client
Client simulated.Client
Addresses []common.Address
PrivateKeys []*ecdsa.PrivateKey
ChainID int64
Expand Down Expand Up @@ -139,23 +140,26 @@ func NewClientWithConfig(cfg *Config) (*Client, error) {
}

abiFinder := NewABIFinder(contractAddressToNameMap, cs)
if len(cfg.Network.URLs) == 0 {
return nil, fmt.Errorf("at least one url should be present in config in 'secret_urls = []'")
}
tr, err := NewTracer(cs, &abiFinder, cfg, contractAddressToNameMap, addrs)
if err != nil {
return nil, errors.Wrap(err, ErrCreateTracer)

var opts []ClientOpt

// even if the ethclient that was passed supports tracing, we still need the RPC URL, because we cannot get from
// the instance of ethclient, since it doesn't expose any such method
if (cfg.ethclient != nil && shouldIntialiseTracer(cfg.ethclient, cfg) && len(cfg.Network.URLs) > 0) || cfg.ethclient == nil {
tr, err := NewTracer(cs, &abiFinder, cfg, contractAddressToNameMap, addrs)
if err != nil {
return nil, errors.Wrap(err, ErrCreateTracer)
}
opts = append(opts, WithTracer(tr))
}

opts = append(opts, WithContractStore(cs), WithNonceManager(nm), WithContractMap(contractAddressToNameMap), WithABIFinder(&abiFinder))

return NewClientRaw(
cfg,
addrs,
pkeys,
WithContractStore(cs),
WithNonceManager(nm),
WithTracer(tr),
WithContractMap(contractAddressToNameMap),
WithABIFinder(&abiFinder),
opts...,
)
}

Expand All @@ -175,53 +179,70 @@ func NewClientRaw(
pkeys []*ecdsa.PrivateKey,
opts ...ClientOpt,
) (*Client, error) {
if len(cfg.Network.URLs) == 0 {
return nil, errors.New("no RPC URL provided")
}
if len(cfg.Network.URLs) > 1 {
L.Warn().Msg("Multiple RPC URLs provided, only the first one will be used")
}

if cfg.ReadOnly && (len(addrs) > 0 || len(pkeys) > 0) {
return nil, errors.New(ErrReadOnlyWithPrivateKeys)
}

ctx, cancel := context.WithTimeout(context.Background(), cfg.Network.DialTimeout.Duration())
defer cancel()
rpcClient, err := rpc.DialOptions(ctx,
cfg.FirstNetworkURL(),
rpc.WithHeaders(cfg.RPCHeaders),
rpc.WithHTTPClient(&http.Client{
Transport: NewLoggingTransport(),
}),
)
if err != nil {
return nil, fmt.Errorf("failed to connect RPC client to '%s' due to: %w", cfg.FirstNetworkURL(), err)
}
client := ethclient.NewClient(rpcClient)
var firstUrl string
var client simulated.Client
if cfg.ethclient == nil {
L.Info().Msg("Creating new ethereum client")
if len(cfg.Network.URLs) == 0 {
return nil, errors.New("no RPC URL provided")
}

if cfg.Network.ChainID == 0 {
chainId, err := client.ChainID(context.Background())
if len(cfg.Network.URLs) > 1 {
L.Warn().Msg("Multiple RPC URLs provided, only the first one will be used")
}

ctx, cancel := context.WithTimeout(context.Background(), cfg.Network.DialTimeout.Duration())
defer cancel()
rpcClient, err := rpc.DialOptions(ctx,
cfg.MustFirstNetworkURL(),
rpc.WithHeaders(cfg.RPCHeaders),
rpc.WithHTTPClient(&http.Client{
Transport: NewLoggingTransport(),
}),
)
if err != nil {
return nil, errors.Wrap(err, "failed to get chain ID")
return nil, fmt.Errorf("failed to connect RPC client to '%s' due to: %w", cfg.MustFirstNetworkURL(), err)
}
cfg.Network.ChainID = chainId.Uint64()
client = ethclient.NewClient(rpcClient)
firstUrl = cfg.MustFirstNetworkURL()
} else {
L.Info().
Str("Type", reflect.TypeOf(cfg.ethclient).String()).
Msg("Using provided ethereum client")
client = cfg.ethclient
}

ctx, cancelFunc := context.WithCancel(context.Background())
c := &Client{
Cfg: cfg,
Client: client,
Cfg: cfg,
Addresses: addrs,
PrivateKeys: pkeys,
URL: cfg.FirstNetworkURL(),
URL: firstUrl,
ChainID: mustSafeInt64(cfg.Network.ChainID),
Context: ctx,
CancelFunc: cancelFunc,
}

for _, o := range opts {
o(c)
}

if cfg.Network.ChainID == 0 {
chainId, err := c.Client.ChainID(context.Background())
if err != nil {
return nil, errors.Wrap(err, "failed to get chain ID")
}
cfg.Network.ChainID = chainId.Uint64()
c.ChainID = mustSafeInt64(cfg.Network.ChainID)
}

var err error

if c.ContractAddressToNameMap.addressMap == nil {
c.ContractAddressToNameMap = NewEmptyContractMap()
if !cfg.IsSimulatedNetwork() {
Expand Down Expand Up @@ -279,7 +300,7 @@ func NewClientRaw(
L.Info().
Str("NetworkName", cfg.Network.Name).
Interface("Addresses", addrs).
Str("RPC", cfg.FirstNetworkURL()).
Str("RPC", firstUrl).
Uint64("ChainID", cfg.Network.ChainID).
Int64("Ephemeral keys", *cfg.EphemeralAddrs).
Msg("Created new client")
Expand Down Expand Up @@ -316,7 +337,9 @@ func NewClientRaw(
}
}

if c.Cfg.TracingLevel != TracingLevel_None && c.Tracer == nil {
// we cannot use the tracer with simulated backend, because it doesn't expose a method to get rpcClient (even though it has one)
// and Tracer needs rpcClient to call debug_traceTransaction
if shouldIntialiseTracer(c.Client, cfg) && c.Cfg.TracingLevel != TracingLevel_None && c.Tracer == nil {
if c.ContractStore == nil {
cs, err := NewContractStore(filepath.Join(cfg.ConfigDir, cfg.ABIDir), filepath.Join(cfg.ConfigDir, cfg.BINDir), cfg.GethWrappersDirs)
if err != nil {
Expand Down Expand Up @@ -407,7 +430,7 @@ func (m *Client) TransferETHFromKey(ctx context.Context, fromKeyNum int, to stri
ctx, chainCancel := context.WithTimeout(ctx, m.Cfg.Network.TxnTimeout.Duration())
defer chainCancel()

chainID, err := m.Client.NetworkID(ctx)
chainID, err := m.Client.ChainID(ctx)
if err != nil {
return errors.Wrap(err, "failed to get network ID")
}
Expand Down Expand Up @@ -1385,3 +1408,11 @@ func (m *Client) mergeLogMeta(pe *DecodedTransactionLog, l types.Log) {
pe.TXIndex = l.TxIndex
pe.Removed = l.Removed
}

func shouldIntialiseTracer(client simulated.Client, cfg *Config) bool {
return len(cfg.Network.URLs) > 0 && supportsTracing(client)
}

func supportsTracing(client simulated.Client) bool {
return strings.Contains(reflect.TypeOf(client).String(), "ethclient.Client")
}
13 changes: 13 additions & 0 deletions seth/client_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"time"

"github.com/ethereum/go-ethereum/ethclient/simulated"
"github.com/pkg/errors"
)

Expand All @@ -12,6 +13,7 @@ const (
NoPkForNonceProtection = "you need to provide at least one private key to enable nonce protection"
NoPkForEphemeralKeys = "you need to provide at least one private key to generate and fund ephemeral addresses"
NoPkForGasPriceEstimation = "you need to provide at least one private key to enable gas price estimations"
EthClientAndUrlsSet = "you cannot set both EthClient and RPC URLs"
)

type ClientBuilder struct {
Expand Down Expand Up @@ -363,6 +365,14 @@ func (c *ClientBuilder) WithNonceManager(rateLimitSec int, retries uint, timeout
return c
}

// WithEthClient sets the ethclient to use. It means that the URL you pass will be ignored and the client will use the provided ethclient,
// but what it allows you is to use Geth's Simulated Backend or similar implementations for testing.
// Default value is nil.
func (c *ClientBuilder) WithEthClient(ethclient simulated.Client) *ClientBuilder {
c.config.ethclient = ethclient
return c
}

// WithReadOnlyMode sets the client to read-only mode. It removes all private keys from all Networks and disables nonce protection and ephemeral addresses.
func (c *ClientBuilder) WithReadOnlyMode() *ClientBuilder {
c.readonly = true
Expand Down Expand Up @@ -423,6 +433,9 @@ func (c *ClientBuilder) validateConfig() {
if len(c.config.Network.PrivateKeys) == 0 && c.config.Network.GasPriceEstimationEnabled {
c.errors = append(c.errors, errors.New(NoPkForGasPriceEstimation))
}
if len(c.config.Network.URLs) > 0 && c.config.ethclient != nil {
c.errors = append(c.errors, errors.New(EthClientAndUrlsSet))
}
}
}

Expand Down
Loading

0 comments on commit af6f589

Please sign in to comment.