Skip to content

Commit af6f589

Browse files
committed
Merge branch 'main' of github.com:smartcontractkit/chainlink-testing-framework into parrotServer
2 parents 26c8b03 + 72387bd commit af6f589

File tree

17 files changed

+1253
-404
lines changed

17 files changed

+1253
-404
lines changed

book/src/libs/seth.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Reliable and debug-friendly Ethereum client
2222
5. [Configuration](#config)
2323
1. [Simplified configuration](#simplified-configuration)
2424
2. [ClientBuilder](#clientbuilder)
25+
1. [Simulated Backend](#simulated-backend)
2526
3. [Supported env vars](#supported-env-vars)
2627
4. [TOML configuration](#toml-configuration)
2728
6. [Automated gas price estimation](#automatic-gas-estimator)
@@ -263,6 +264,67 @@ if err != nil {
263264
```
264265
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.
265266

267+
### Simulated Backend
268+
269+
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
270+
in-memory environment. When using that option bear in mind that:
271+
* passing RPC URL is not allowed and will result in error
272+
* tracing is disabled
273+
274+
> [!NOTE]
275+
> 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.
276+
277+
So how do you use Seth with simulated backend?
278+
```go
279+
var startBackend := func(fundedAddresses []common.Address) (*simulated.Backend, context.CancelFunc) {
280+
toFund := make(map[common.Address]types.Account)
281+
for _, address := range fundedAddresses {
282+
toFund[address] = types.Account{
283+
Balance: big.NewInt(1000000000000000000), // 1 Ether
284+
}
285+
}
286+
backend := simulated.NewBackend(toFund)
287+
288+
ctx, cancelFn := context.WithCancel(context.Background())
289+
290+
// 100ms block time
291+
ticker := time.NewTicker(100 * time.Millisecond)
292+
go func() {
293+
for {
294+
select {
295+
case <-ticker.C:
296+
backend.Commit()
297+
case <-ctx.Done():
298+
backend.Close()
299+
return
300+
}
301+
}
302+
}()
303+
304+
return backend, cancelFn
305+
}
306+
307+
// 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 is the default dev account
308+
backend, cancelFn := startBackend(
309+
[]common.Address{common.HexToAddress("0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266")},
310+
)
311+
defer func() { cancelFn() }()
312+
313+
client, err := builder.
314+
WithNetworkName("simulated").
315+
WithEthClient(backend.Client()).
316+
WithPrivateKeys([]string{"ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"}).
317+
Build()
318+
319+
require.NoError(t, err, "failed to build client")
320+
_ = client
321+
```
322+
323+
> [!WARNING]
324+
> When using `simulated.Backend` do remember that it doesn't automatically mine blocks. You need to call `backend.Commit()` manually
325+
> to mine a new block and have your transactions processed. The best way to do it is having a goroutine running in the background
326+
> that either mines at specific intervals or when it receives a message on channel.
327+
266328
### Supported env vars
267329

268330
Some crucial data is stored in env vars, create `.envrc` and use `source .envrc`, or use `direnv`

lib/.changeset/v1.50.21.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
- Fix `lib` imports and dependencies
2+
- Fix start up of Nethermind 1.30.1+ containers
3+
- Fix docker 8080 port mappings
4+
- Do not change container name, when restarting it
5+
- Automatically forward `SETH_LOG_LEVEL` to k8s

seth/.changeset/v1.50.12.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- improvement: allow to pass your own `ethclient` to Seth (which allows to use it with `go-ethereum`'s Simulated Backend)

seth/client.go

Lines changed: 73 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,14 @@ import (
1313

1414
"github.com/avast/retry-go"
1515
"github.com/ethereum/go-ethereum"
16+
"github.com/ethereum/go-ethereum/ethclient"
17+
"github.com/ethereum/go-ethereum/ethclient/simulated"
1618
"github.com/ethereum/go-ethereum/rpc"
1719

1820
"github.com/ethereum/go-ethereum/accounts/abi"
1921
"github.com/ethereum/go-ethereum/accounts/abi/bind"
2022
"github.com/ethereum/go-ethereum/common"
2123
"github.com/ethereum/go-ethereum/core/types"
22-
"github.com/ethereum/go-ethereum/ethclient"
2324
"github.com/pkg/errors"
2425
"github.com/rs/zerolog"
2526
"golang.org/x/sync/errgroup"
@@ -64,7 +65,7 @@ var (
6465
// Client is a vanilla go-ethereum client with enhanced debug logging
6566
type Client struct {
6667
Cfg *Config
67-
Client *ethclient.Client
68+
Client simulated.Client
6869
Addresses []common.Address
6970
PrivateKeys []*ecdsa.PrivateKey
7071
ChainID int64
@@ -139,23 +140,26 @@ func NewClientWithConfig(cfg *Config) (*Client, error) {
139140
}
140141

141142
abiFinder := NewABIFinder(contractAddressToNameMap, cs)
142-
if len(cfg.Network.URLs) == 0 {
143-
return nil, fmt.Errorf("at least one url should be present in config in 'secret_urls = []'")
144-
}
145-
tr, err := NewTracer(cs, &abiFinder, cfg, contractAddressToNameMap, addrs)
146-
if err != nil {
147-
return nil, errors.Wrap(err, ErrCreateTracer)
143+
144+
var opts []ClientOpt
145+
146+
// even if the ethclient that was passed supports tracing, we still need the RPC URL, because we cannot get from
147+
// the instance of ethclient, since it doesn't expose any such method
148+
if (cfg.ethclient != nil && shouldIntialiseTracer(cfg.ethclient, cfg) && len(cfg.Network.URLs) > 0) || cfg.ethclient == nil {
149+
tr, err := NewTracer(cs, &abiFinder, cfg, contractAddressToNameMap, addrs)
150+
if err != nil {
151+
return nil, errors.Wrap(err, ErrCreateTracer)
152+
}
153+
opts = append(opts, WithTracer(tr))
148154
}
149155

156+
opts = append(opts, WithContractStore(cs), WithNonceManager(nm), WithContractMap(contractAddressToNameMap), WithABIFinder(&abiFinder))
157+
150158
return NewClientRaw(
151159
cfg,
152160
addrs,
153161
pkeys,
154-
WithContractStore(cs),
155-
WithNonceManager(nm),
156-
WithTracer(tr),
157-
WithContractMap(contractAddressToNameMap),
158-
WithABIFinder(&abiFinder),
162+
opts...,
159163
)
160164
}
161165

@@ -175,53 +179,70 @@ func NewClientRaw(
175179
pkeys []*ecdsa.PrivateKey,
176180
opts ...ClientOpt,
177181
) (*Client, error) {
178-
if len(cfg.Network.URLs) == 0 {
179-
return nil, errors.New("no RPC URL provided")
180-
}
181-
if len(cfg.Network.URLs) > 1 {
182-
L.Warn().Msg("Multiple RPC URLs provided, only the first one will be used")
183-
}
184-
185182
if cfg.ReadOnly && (len(addrs) > 0 || len(pkeys) > 0) {
186183
return nil, errors.New(ErrReadOnlyWithPrivateKeys)
187184
}
188185

189-
ctx, cancel := context.WithTimeout(context.Background(), cfg.Network.DialTimeout.Duration())
190-
defer cancel()
191-
rpcClient, err := rpc.DialOptions(ctx,
192-
cfg.FirstNetworkURL(),
193-
rpc.WithHeaders(cfg.RPCHeaders),
194-
rpc.WithHTTPClient(&http.Client{
195-
Transport: NewLoggingTransport(),
196-
}),
197-
)
198-
if err != nil {
199-
return nil, fmt.Errorf("failed to connect RPC client to '%s' due to: %w", cfg.FirstNetworkURL(), err)
200-
}
201-
client := ethclient.NewClient(rpcClient)
186+
var firstUrl string
187+
var client simulated.Client
188+
if cfg.ethclient == nil {
189+
L.Info().Msg("Creating new ethereum client")
190+
if len(cfg.Network.URLs) == 0 {
191+
return nil, errors.New("no RPC URL provided")
192+
}
202193

203-
if cfg.Network.ChainID == 0 {
204-
chainId, err := client.ChainID(context.Background())
194+
if len(cfg.Network.URLs) > 1 {
195+
L.Warn().Msg("Multiple RPC URLs provided, only the first one will be used")
196+
}
197+
198+
ctx, cancel := context.WithTimeout(context.Background(), cfg.Network.DialTimeout.Duration())
199+
defer cancel()
200+
rpcClient, err := rpc.DialOptions(ctx,
201+
cfg.MustFirstNetworkURL(),
202+
rpc.WithHeaders(cfg.RPCHeaders),
203+
rpc.WithHTTPClient(&http.Client{
204+
Transport: NewLoggingTransport(),
205+
}),
206+
)
205207
if err != nil {
206-
return nil, errors.Wrap(err, "failed to get chain ID")
208+
return nil, fmt.Errorf("failed to connect RPC client to '%s' due to: %w", cfg.MustFirstNetworkURL(), err)
207209
}
208-
cfg.Network.ChainID = chainId.Uint64()
210+
client = ethclient.NewClient(rpcClient)
211+
firstUrl = cfg.MustFirstNetworkURL()
212+
} else {
213+
L.Info().
214+
Str("Type", reflect.TypeOf(cfg.ethclient).String()).
215+
Msg("Using provided ethereum client")
216+
client = cfg.ethclient
209217
}
218+
210219
ctx, cancelFunc := context.WithCancel(context.Background())
211220
c := &Client{
212-
Cfg: cfg,
213221
Client: client,
222+
Cfg: cfg,
214223
Addresses: addrs,
215224
PrivateKeys: pkeys,
216-
URL: cfg.FirstNetworkURL(),
225+
URL: firstUrl,
217226
ChainID: mustSafeInt64(cfg.Network.ChainID),
218227
Context: ctx,
219228
CancelFunc: cancelFunc,
220229
}
230+
221231
for _, o := range opts {
222232
o(c)
223233
}
224234

235+
if cfg.Network.ChainID == 0 {
236+
chainId, err := c.Client.ChainID(context.Background())
237+
if err != nil {
238+
return nil, errors.Wrap(err, "failed to get chain ID")
239+
}
240+
cfg.Network.ChainID = chainId.Uint64()
241+
c.ChainID = mustSafeInt64(cfg.Network.ChainID)
242+
}
243+
244+
var err error
245+
225246
if c.ContractAddressToNameMap.addressMap == nil {
226247
c.ContractAddressToNameMap = NewEmptyContractMap()
227248
if !cfg.IsSimulatedNetwork() {
@@ -279,7 +300,7 @@ func NewClientRaw(
279300
L.Info().
280301
Str("NetworkName", cfg.Network.Name).
281302
Interface("Addresses", addrs).
282-
Str("RPC", cfg.FirstNetworkURL()).
303+
Str("RPC", firstUrl).
283304
Uint64("ChainID", cfg.Network.ChainID).
284305
Int64("Ephemeral keys", *cfg.EphemeralAddrs).
285306
Msg("Created new client")
@@ -316,7 +337,9 @@ func NewClientRaw(
316337
}
317338
}
318339

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

410-
chainID, err := m.Client.NetworkID(ctx)
433+
chainID, err := m.Client.ChainID(ctx)
411434
if err != nil {
412435
return errors.Wrap(err, "failed to get network ID")
413436
}
@@ -1385,3 +1408,11 @@ func (m *Client) mergeLogMeta(pe *DecodedTransactionLog, l types.Log) {
13851408
pe.TXIndex = l.TxIndex
13861409
pe.Removed = l.Removed
13871410
}
1411+
1412+
func shouldIntialiseTracer(client simulated.Client, cfg *Config) bool {
1413+
return len(cfg.Network.URLs) > 0 && supportsTracing(client)
1414+
}
1415+
1416+
func supportsTracing(client simulated.Client) bool {
1417+
return strings.Contains(reflect.TypeOf(client).String(), "ethclient.Client")
1418+
}

seth/client_builder.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"time"
66

7+
"github.com/ethereum/go-ethereum/ethclient/simulated"
78
"github.com/pkg/errors"
89
)
910

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

1719
type ClientBuilder struct {
@@ -363,6 +365,14 @@ func (c *ClientBuilder) WithNonceManager(rateLimitSec int, retries uint, timeout
363365
return c
364366
}
365367

368+
// WithEthClient sets the ethclient to use. It means that the URL you pass will be ignored and the client will use the provided ethclient,
369+
// but what it allows you is to use Geth's Simulated Backend or similar implementations for testing.
370+
// Default value is nil.
371+
func (c *ClientBuilder) WithEthClient(ethclient simulated.Client) *ClientBuilder {
372+
c.config.ethclient = ethclient
373+
return c
374+
}
375+
366376
// WithReadOnlyMode sets the client to read-only mode. It removes all private keys from all Networks and disables nonce protection and ephemeral addresses.
367377
func (c *ClientBuilder) WithReadOnlyMode() *ClientBuilder {
368378
c.readonly = true
@@ -423,6 +433,9 @@ func (c *ClientBuilder) validateConfig() {
423433
if len(c.config.Network.PrivateKeys) == 0 && c.config.Network.GasPriceEstimationEnabled {
424434
c.errors = append(c.errors, errors.New(NoPkForGasPriceEstimation))
425435
}
436+
if len(c.config.Network.URLs) > 0 && c.config.ethclient != nil {
437+
c.errors = append(c.errors, errors.New(EthClientAndUrlsSet))
438+
}
426439
}
427440
}
428441

0 commit comments

Comments
 (0)