diff --git a/client/eth/client_test.go b/client/eth/client_test.go index ab39233..7ecee93 100644 --- a/client/eth/client_test.go +++ b/client/eth/client_test.go @@ -2,11 +2,11 @@ package eth_test import ( "context" - "os" "testing" "time" "github.com/berachain/offchain-sdk/client/eth" + "github.com/berachain/offchain-sdk/config/env" "github.com/stretchr/testify/assert" "github.com/ethereum/go-ethereum" @@ -29,8 +29,16 @@ const ( TestModeEither ) +// setupClientTest loads environment variables and performs any necessary test setup. +func setupClientTest(t *testing.T) { + t.Helper() + err := env.Load() + assert.NoError(t, err) +} + // NOTE: requires Ethereum chain rpc url at env var `ETH_RPC_URL` or `ETH_RPC_URL_WS`. func setUp(testMode int, t *testing.T) (*eth.ExtendedEthClient, error) { + setupClientTest(t) rpcTimeout := 5 * time.Second ctxWithTimeout, cancel := context.WithTimeout(context.Background(), rpcTimeout) defer cancel() @@ -38,12 +46,12 @@ func setUp(testMode int, t *testing.T) (*eth.ExtendedEthClient, error) { var ethRPC string switch testMode { case TestModeWS: - ethRPC = os.Getenv("ETH_RPC_URL_WS") + ethRPC = env.GetEthWSURL() case TestModeHTTP: - ethRPC = os.Getenv("ETH_RPC_URL") + ethRPC = env.GetEthRPCURL() case TestModeEither: - if ethRPC = os.Getenv("ETH_RPC_URL_WS"); ethRPC == "" { - ethRPC = os.Getenv("ETH_RPC_URL") + if ethRPC = env.GetEthWSURL(); ethRPC == "" { + ethRPC = env.GetEthRPCURL() } default: panic("invalid test mode") @@ -130,7 +138,7 @@ func TestTxPoolContentFrom(t *testing.T) { assert.NoError(t, err) ctx := context.Background() - addrStr := os.Getenv("ETH_ADDR") + addrStr := env.GetAddressToListen() if addrStr == "" { t.Skipf("Skipping test: no eth address provided") } diff --git a/client/eth/connection_pool_test.go b/client/eth/connection_pool_test.go index 1921bea..c64b3c9 100644 --- a/client/eth/connection_pool_test.go +++ b/client/eth/connection_pool_test.go @@ -3,26 +3,26 @@ package eth_test import ( "bytes" "io" - "os" "testing" "github.com/berachain/offchain-sdk/client/eth" + "github.com/berachain/offchain-sdk/config/env" "github.com/berachain/offchain-sdk/log" "github.com/stretchr/testify/require" ) -var ( - HTTPURL = os.Getenv("ETH_HTTP_URL") - WSURL = os.Getenv("ETH_WS_URL") -) - -/******************************* HELPER FUNCTIONS ***************************************/ +// setupTest loads environment variables and performs any necessary test setup. +func setupTest(t *testing.T) { + t.Helper() + err := env.Load() + require.NoError(t, err) +} -// NOTE: requires chain rpc url at env var `ETH_HTTP_URL` and `ETH_WS_URL`. +// NOTE: requires chain rpc url at env var `ETH_RPC_URL` and `ETH_WS_URL`. func checkEnv(t *testing.T) { - ethHTTPRPC := os.Getenv("ETH_HTTP_URL") - ethWSRPC := os.Getenv("ETH_WS_URL") - if ethHTTPRPC == "" || ethWSRPC == "" { + ethRPC := env.GetEthRPCURL() + ethWS := env.GetEthWSURL() + if ethRPC == "" || ethWS == "" { t.Skipf("Skipping test: no eth rpc url provided") } } @@ -58,7 +58,7 @@ func TestNewConnectionPoolImpl_MissingURLs(t *testing.T) { // TestNewConnectionPoolImpl_MissingWSURLs tests the case when the WS URLs are missing. func TestNewConnectionPoolImpl_MissingWSURLs(t *testing.T) { cfg := eth.ConnectionPoolConfig{ - EthHTTPURLs: []string{HTTPURL}, + EthHTTPURLs: []string{env.GetEthRPCURL()}, } var logBuffer bytes.Buffer pool, err := Init(cfg, &logBuffer, t) @@ -71,9 +71,10 @@ func TestNewConnectionPoolImpl_MissingWSURLs(t *testing.T) { // TestNewConnectionPoolImpl tests the case when the URLs are provided. // It should the expected behavior. func TestNewConnectionPoolImpl(t *testing.T) { + setupTest(t) cfg := eth.ConnectionPoolConfig{ - EthHTTPURLs: []string{HTTPURL}, - EthWSURLs: []string{WSURL}, + EthHTTPURLs: []string{env.GetEthRPCURL()}, + EthWSURLs: []string{env.GetEthWSURL()}, } var logBuffer bytes.Buffer pool, err := Init(cfg, &logBuffer, t) @@ -86,8 +87,9 @@ func TestNewConnectionPoolImpl(t *testing.T) { // TestGetHTTP tests the retrieval of the HTTP client when it // has been set and the connection has been established. func TestGetHTTP(t *testing.T) { + setupTest(t) cfg := eth.ConnectionPoolConfig{ - EthHTTPURLs: []string{HTTPURL}, + EthHTTPURLs: []string{env.GetEthRPCURL()}, } var logBuffer bytes.Buffer pool, _ := Init(cfg, &logBuffer, t) @@ -102,9 +104,10 @@ func TestGetHTTP(t *testing.T) { // TestGetWS tests the retrieval of the HTTP client when it // has been set and the connection has been established. func TestGetWS(t *testing.T) { + setupTest(t) cfg := eth.ConnectionPoolConfig{ - EthHTTPURLs: []string{HTTPURL}, - EthWSURLs: []string{WSURL}, + EthHTTPURLs: []string{env.GetEthRPCURL()}, + EthWSURLs: []string{env.GetEthWSURL()}, } var logBuffer bytes.Buffer pool, _ := Init(cfg, &logBuffer, t) @@ -120,8 +123,9 @@ func TestGetWS(t *testing.T) { // TestGetWS_WhenItIsNotSet tests the retrieval of the WS client when // no WS URLs have been provided. func TestGetWS_WhenItIsNotSet(t *testing.T) { + setupTest(t) cfg := eth.ConnectionPoolConfig{ - EthHTTPURLs: []string{HTTPURL}, + EthHTTPURLs: []string{env.GetEthRPCURL()}, } var logBuffer bytes.Buffer pool, _ := Init(cfg, &logBuffer, t) diff --git a/config/env/env.go b/config/env/env.go index a10c605..75a6174 100644 --- a/config/env/env.go +++ b/config/env/env.go @@ -1,3 +1,82 @@ package env -// TODO support reading .env +import ( + "os" + "path/filepath" + + "github.com/joho/godotenv" +) + +const ( + // Ethereum RPC URLs. + EnvEthRPCURL = "ETH_RPC_URL" + EnvEthWSURL = "ETH_WS_URL" + EnvEthRPCURLWS = "ETH_RPC_URL_WS" // Alternative WS URL used in some tests + + // Event listening configuration. + EnvEventName = "EVENT_NAME" + EnvAddressListen = "ADDRESS_TO_LISTEN" +) + +// Loads environment variables from .env file. +func Load() error { + // Try loading from current directory first + err := godotenv.Load() + if err == nil { + return nil + } + + // Then If that fails, try to find .env in + // parent directories. + dir, err := os.Getwd() + if err != nil { + return err + } + + for { + envPath := filepath.Join(dir, ".env") + if _, statErr := os.Stat(envPath); statErr == nil { + return godotenv.Load(envPath) + } + + parent := filepath.Dir(dir) + if parent == dir { + break + } + dir = parent + } + + // If we get here, we couldn't find the .env file + // But we don't return an error because the env vars + // might be actually set in the system in which case + // we don't need the .env file. + return nil +} + +// Loads environment variables from the specified file. +func LoadFile(filename string) error { + return godotenv.Load(filename) +} + +// Returns the Ethereum RPC URL. +func GetEthRPCURL() string { + return os.Getenv(EnvEthRPCURL) +} + +// Returns the Ethereum WebSocket URL. +func GetEthWSURL() string { + if url := os.Getenv(EnvEthRPCURLWS); url != "" { + return url + } + return os.Getenv(EnvEthWSURL) +} + +// Returns the event name to listen for. +func GetEventName() string { + return os.Getenv(EnvEventName) +} + +// Returns the contract address to listen to. +func GetAddressToListen() string { + return os.Getenv(EnvAddressListen) +} diff --git a/config/env/env_test.go b/config/env/env_test.go new file mode 100644 index 0000000..b600484 --- /dev/null +++ b/config/env/env_test.go @@ -0,0 +1,60 @@ +package env + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestEnv(t *testing.T) { + t.Run("test loading from .env file", func(t *testing.T) { + // Creating a temporary .env file for this test + dir := t.TempDir() + envFile := filepath.Join(dir, ".env") + err := os.WriteFile(envFile, []byte(` +ETH_RPC_URL=http://localhost:8545 +ETH_WS_URL=ws://localhost:8546 +ETH_RPC_URL_WS=ws://localhost:8547 +EVENT_NAME=NumberChanged(uint256) +ADDRESS_TO_LISTEN=0x5793a71D3eF074f71dCC21216Dbfd5C0e780132c +`), 0644) + require.NoError(t, err) + + // Loading the env file + err = LoadFile(envFile) + require.NoError(t, err) + + // Testing each getter + require.Equal(t, "http://localhost:8545", GetEthRPCURL()) + require.Equal(t, "ws://localhost:8547", GetEthWSURL(), "should prefer ETH_RPC_URL_WS") + + // Clearing ETH_RPC_URL_WS and verifying fallback to ETH_WS_URL + os.Unsetenv(EnvEthRPCURLWS) + require.Equal(t, "ws://localhost:8546", GetEthWSURL(), "should fallback to ETH_WS_URL") + + require.Equal(t, "NumberChanged(uint256)", GetEventName()) + require.Equal(t, "0x5793a71D3eF074f71dCC21216Dbfd5C0e780132c", GetAddressToListen()) + }) + + t.Run("test loading non-existent file", func(t *testing.T) { + err := LoadFile("non-existent.env") + require.Error(t, err) + }) + + t.Run("test loading with missing values", func(t *testing.T) { + // Clearing all env vars first + os.Unsetenv(EnvEthRPCURL) + os.Unsetenv(EnvEthWSURL) + os.Unsetenv(EnvEthRPCURLWS) + os.Unsetenv(EnvEventName) + os.Unsetenv(EnvAddressListen) + + // Testing empty values + require.Empty(t, GetEthRPCURL()) + require.Empty(t, GetEthWSURL()) + require.Empty(t, GetEventName()) + require.Empty(t, GetAddressToListen()) + }) +} diff --git a/go.mod b/go.mod index 1cbd11d..6160bb6 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/holiman/uint256 v1.2.4 github.com/huandu/skiplist v1.2.0 github.com/jellydator/ttlcache/v2 v2.11.1 + github.com/joho/godotenv v1.5.1 github.com/prometheus/client_golang v1.17.0 github.com/redis/go-redis/v9 v9.5.1 github.com/rs/zerolog v1.31.0 diff --git a/go.sum b/go.sum index b9444b1..788038a 100644 --- a/go.sum +++ b/go.sum @@ -418,6 +418,8 @@ github.com/jjti/go-spancheck v0.6.2 h1:iYtoxqPMzHUPp7St+5yA8+cONdyXD3ug6KK15n7Pk github.com/jjti/go-spancheck v0.6.2/go.mod h1:+X7lvIrR5ZdUTkxFYqzJ0abr8Sb5LOo80uOhWNqIrYA= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/juju/errors v0.0.0-20181118221551-089d3ea4e4d5/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q=