diff --git a/.changeset/fresh-rice-learn.md b/.changeset/fresh-rice-learn.md new file mode 100644 index 00000000000..6425cdd4581 --- /dev/null +++ b/.changeset/fresh-rice-learn.md @@ -0,0 +1,5 @@ +--- +"chainlink": minor +--- + +Moved test functions under evm package to support evm extraction #internal diff --git a/core/chains/evm/testutils/client.go b/core/chains/evm/testutils/client.go new file mode 100644 index 00000000000..31ad23eeb81 --- /dev/null +++ b/core/chains/evm/testutils/client.go @@ -0,0 +1,198 @@ +package testutils + +import ( + "fmt" + "math/big" + "net/http" + "net/http/httptest" + "net/url" + "sync" + "testing" + "time" + + "github.com/gorilla/websocket" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + evmclmocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client/mocks" +) + +func NewEthClientMock(t *testing.T) *evmclmocks.Client { + return evmclmocks.NewClient(t) +} + +func NewEthClientMockWithDefaultChain(t *testing.T) *evmclmocks.Client { + c := NewEthClientMock(t) + c.On("ConfiguredChainID").Return(FixtureChainID).Maybe() + //c.On("IsL2").Return(false).Maybe() + return c +} + +// JSONRPCHandler is called with the method and request param(s). +// respResult will be sent immediately. notifyResult is optional, and sent after a short delay. +type JSONRPCHandler func(reqMethod string, reqParams gjson.Result) JSONRPCResponse + +type JSONRPCResponse struct { + Result, Notify string // raw JSON (i.e. quoted strings etc.) + + Error struct { + Code int + Message string + } +} + +type testWSServer struct { + t *testing.T + s *httptest.Server + mu sync.RWMutex + wsconns []*websocket.Conn + wg sync.WaitGroup +} + +// NewWSServer starts a websocket server which invokes callback for each message received. +// If chainID is set, then eth_chainId calls will be automatically handled. +func NewWSServer(t *testing.T, chainID *big.Int, callback JSONRPCHandler) (ts *testWSServer) { + ts = new(testWSServer) + ts.t = t + ts.wsconns = make([]*websocket.Conn, 0) + handler := ts.newWSHandler(chainID, callback) + ts.s = httptest.NewServer(handler) + t.Cleanup(ts.Close) + return +} + +func (ts *testWSServer) Close() { + if func() bool { + ts.mu.Lock() + defer ts.mu.Unlock() + if ts.wsconns == nil { + ts.t.Log("Test WS server already closed") + return false + } + ts.s.CloseClientConnections() + ts.s.Close() + for _, ws := range ts.wsconns { + ws.Close() + } + ts.wsconns = nil // nil indicates server closed + return true + }() { + ts.wg.Wait() + } +} + +func (ts *testWSServer) WSURL() *url.URL { + return WSServerURL(ts.t, ts.s) +} + +// WSServerURL returns a ws:// url for the server +func WSServerURL(t *testing.T, s *httptest.Server) *url.URL { + u, err := url.Parse(s.URL) + require.NoError(t, err, "Failed to parse url") + u.Scheme = "ws" + return u +} + +func (ts *testWSServer) MustWriteBinaryMessageSync(t *testing.T, msg string) { + ts.mu.Lock() + defer ts.mu.Unlock() + conns := ts.wsconns + if len(conns) != 1 { + t.Fatalf("expected 1 conn, got %d", len(conns)) + } + conn := conns[0] + err := conn.WriteMessage(websocket.BinaryMessage, []byte(msg)) + require.NoError(t, err) +} + +func (ts *testWSServer) newWSHandler(chainID *big.Int, callback JSONRPCHandler) (handler http.HandlerFunc) { + if callback == nil { + callback = func(method string, params gjson.Result) (resp JSONRPCResponse) { return } + } + t := ts.t + upgrader := websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { return true }, + } + return func(w http.ResponseWriter, r *http.Request) { + ts.mu.Lock() + if ts.wsconns == nil { // closed + ts.mu.Unlock() + return + } + ts.wg.Add(1) + defer ts.wg.Done() + conn, err := upgrader.Upgrade(w, r, nil) + if !assert.NoError(t, err, "Failed to upgrade WS connection") { + ts.mu.Unlock() + return + } + defer conn.Close() + ts.wsconns = append(ts.wsconns, conn) + ts.mu.Unlock() + + for { + _, data, err := conn.ReadMessage() + if err != nil { + if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseAbnormalClosure) { + ts.t.Log("Websocket closing") + return + } + ts.t.Logf("Failed to read message: %v", err) + return + } + ts.t.Log("Received message", string(data)) + req := gjson.ParseBytes(data) + if !req.IsObject() { + ts.t.Logf("Request must be object: %v", req.Type) + return + } + if e := req.Get("error"); e.Exists() { + ts.t.Logf("Received jsonrpc error: %v", e) + continue + } + m := req.Get("method") + if m.Type != gjson.String { + ts.t.Logf("Method must be string: %v", m.Type) + return + } + + var resp JSONRPCResponse + if chainID != nil && m.String() == "eth_chainId" { + resp.Result = `"0x` + chainID.Text(16) + `"` + } else if m.String() == "eth_syncing" { + resp.Result = "false" + } else { + resp = callback(m.String(), req.Get("params")) + } + id := req.Get("id") + var msg string + if resp.Error.Message != "" { + msg = fmt.Sprintf(`{"jsonrpc":"2.0","id":%s,"error":{"code":%d,"message":"%s"}}`, id, resp.Error.Code, resp.Error.Message) + } else { + msg = fmt.Sprintf(`{"jsonrpc":"2.0","id":%s,"result":%s}`, id, resp.Result) + } + ts.t.Logf("Sending message: %v", msg) + ts.mu.Lock() + err = conn.WriteMessage(websocket.BinaryMessage, []byte(msg)) + ts.mu.Unlock() + if err != nil { + ts.t.Logf("Failed to write message: %v", err) + return + } + + if resp.Notify != "" { + time.Sleep(100 * time.Millisecond) + msg := fmt.Sprintf(`{"jsonrpc":"2.0","method":"eth_subscription","params":{"subscription":"0x00","result":%s}}`, resp.Notify) + ts.t.Log("Sending message", msg) + ts.mu.Lock() + err = conn.WriteMessage(websocket.BinaryMessage, []byte(msg)) + ts.mu.Unlock() + if err != nil { + ts.t.Logf("Failed to write message: %v", err) + return + } + } + } + } +} diff --git a/core/chains/evm/testutils/config.go b/core/chains/evm/testutils/config.go new file mode 100644 index 00000000000..3d1425c5333 --- /dev/null +++ b/core/chains/evm/testutils/config.go @@ -0,0 +1,29 @@ +package testutils + +import ( + "testing" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config/toml" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils/big" +) + +func NewTestChainScopedConfig(t testing.TB, overrideFn func(c *toml.EVMConfig)) config.ChainScopedConfig { + var chainID = (*big.Big)(FixtureChainID) + evmCfg := &toml.EVMConfig{ + ChainID: chainID, + Chain: toml.Defaults(chainID), + } + + if overrideFn != nil { + // We need to get the chainID from the override function first to load the correct chain defaults. + // Then we apply the override values on top + overrideFn(evmCfg) + evmCfg.Chain = toml.Defaults(evmCfg.ChainID) + overrideFn(evmCfg) + } + + return config.NewTOMLChainScopedConfig(evmCfg, logger.Test(t)) +} diff --git a/core/chains/evm/testutils/config_test.go b/core/chains/evm/testutils/config_test.go new file mode 100644 index 00000000000..0cbcc5eb63b --- /dev/null +++ b/core/chains/evm/testutils/config_test.go @@ -0,0 +1,22 @@ +package testutils + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config/toml" +) + +func TestNewTestChainScopedConfigOverride(t *testing.T) { + c := NewTestChainScopedConfig(t, func(c *toml.EVMConfig) { + finalityDepth := uint32(100) + c.FinalityDepth = &finalityDepth + }) + + // Overrides values + assert.Equal(t, uint32(100), c.EVM().FinalityDepth()) + // fallback.toml values + assert.Equal(t, false, c.EVM().GasEstimator().EIP1559DynamicFees()) + +} diff --git a/core/chains/evm/testutils/evmtypes.go b/core/chains/evm/testutils/evmtypes.go new file mode 100644 index 00000000000..5f9ae83d1b7 --- /dev/null +++ b/core/chains/evm/testutils/evmtypes.go @@ -0,0 +1,76 @@ +package testutils + +import ( + "crypto/rand" + "fmt" + "math" + "math/big" + mrand "math/rand" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + + evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" + evmutils "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" + ubig "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils/big" +) + +// FixtureChainID matches the chain always added by fixtures.sql +// It is set to 0 since no real chain ever has this ID and allows a virtual +// "test" chain ID to be used without clashes +var FixtureChainID = big.NewInt(0) + +// SimulatedChainID is the chain ID for the go-ethereum simulated backend +var SimulatedChainID = big.NewInt(1337) + +// NewRandomEVMChainID returns a suitable random chain ID that will not conflict +// with fixtures +func NewRandomEVMChainID() *big.Int { + id := mrand.Int63n(math.MaxInt32) + 10000 + return big.NewInt(id) +} + +// NewAddress return a random new address +func NewAddress() common.Address { + return common.BytesToAddress(randomBytes(20)) +} + +func randomBytes(n int) []byte { + b := make([]byte, n) + _, err := rand.Read(b) + if err != nil { + panic(err) + } + return b +} + +// Head given the value convert it into an Head +func Head(val interface{}) *evmtypes.Head { + var h evmtypes.Head + time := uint64(0) + switch t := val.(type) { + case int: + h = evmtypes.NewHead(big.NewInt(int64(t)), evmutils.NewHash(), evmutils.NewHash(), time, ubig.New(FixtureChainID)) + case uint64: + h = evmtypes.NewHead(big.NewInt(int64(t)), evmutils.NewHash(), evmutils.NewHash(), time, ubig.New(FixtureChainID)) + case int64: + h = evmtypes.NewHead(big.NewInt(t), evmutils.NewHash(), evmutils.NewHash(), time, ubig.New(FixtureChainID)) + case *big.Int: + h = evmtypes.NewHead(t, evmutils.NewHash(), evmutils.NewHash(), time, ubig.New(FixtureChainID)) + default: + panic(fmt.Sprintf("Could not convert %v of type %T to Head", val, val)) + } + return &h +} + +func NewLegacyTransaction(nonce uint64, to common.Address, value *big.Int, gasLimit uint32, gasPrice *big.Int, data []byte) *types.Transaction { + tx := types.LegacyTx{ + Nonce: nonce, + To: &to, + Value: value, + Gas: uint64(gasLimit), + GasPrice: gasPrice, + Data: data, + } + return types.NewTx(&tx) +} diff --git a/core/chains/evm/testutils/timeout.go b/core/chains/evm/testutils/timeout.go new file mode 100644 index 00000000000..4c7864f1191 --- /dev/null +++ b/core/chains/evm/testutils/timeout.go @@ -0,0 +1,41 @@ +package testutils + +import ( + "testing" + "time" +) + +type Awaiter chan struct{} + +func NewAwaiter() Awaiter { return make(Awaiter) } + +func (a Awaiter) ItHappened() { close(a) } + +func (a Awaiter) AssertHappened(t *testing.T, expected bool) { + t.Helper() + select { + case <-a: + if !expected { + t.Fatal("It happened") + } + default: + if expected { + t.Fatal("It didn't happen") + } + } +} + +func (a Awaiter) AwaitOrFail(t testing.TB, durationParams ...time.Duration) { + t.Helper() + + duration := 10 * time.Second + if len(durationParams) > 0 { + duration = durationParams[0] + } + + select { + case <-a: + case <-time.After(duration): + t.Fatal("Timed out waiting for Awaiter to get ItHappened") + } +}