From 8d17118882f50003654e5d582f0b4049f913998f Mon Sep 17 00:00:00 2001 From: Charlie Chen <34498985+ws4charlie@users.noreply.github.com> Date: Tue, 17 Sep 2024 22:38:11 -0500 Subject: [PATCH] feat: support multiple btc chain config (#2870) * support multiple btc chain config * keep BitcoinConfig to be backward compatible * clean accidentially included changelog entry during merge; improve unit test * use go-mask to mask sensitive information in zetaclient config file * fix e2etest failure by renaming btc node user: smoketest --> e2etest * change e2etest --> smoketest; change Signet chain id as 18333 --- changelog.md | 1 + cmd/zetaclientd/debug.go | 14 ++- cmd/zetaclientd/start.go | 4 +- cmd/zetaclientd/start_utils.go | 41 ------ go.mod | 1 + go.sum | 2 + pkg/chains/chains.go | 2 +- zetaclient/config/config_chain.go | 13 +- zetaclient/config/types.go | 59 ++++++--- zetaclient/config/types_test.go | 125 +++++++++++++++++++ zetaclient/context/app_test.go | 6 +- zetaclient/context/chain.go | 2 +- zetaclient/context/chain_test.go | 2 +- zetaclient/orchestrator/bootstap_test.go | 4 +- zetaclient/orchestrator/bootstrap.go | 14 +-- zetaclient/orchestrator/orchestrator.go | 2 +- zetaclient/orchestrator/orchestrator_test.go | 14 ++- 17 files changed, 218 insertions(+), 88 deletions(-) diff --git a/changelog.md b/changelog.md index 6a8e026a4e..a94409e383 100644 --- a/changelog.md +++ b/changelog.md @@ -9,6 +9,7 @@ * [2784](https://github.com/zeta-chain/node/pull/2784) - staking precompiled contract * [2795](https://github.com/zeta-chain/node/pull/2795) - support restricted address in Solana * [2861](https://github.com/zeta-chain/node/pull/2861) - emit events from staking precompile +* [2870](https://github.com/zeta-chain/node/pull/2870) - support for multiple Bitcoin chains in the zetaclient * [2883](https://github.com/zeta-chain/node/pull/2883) - add chain static information for btc signet testnet ### Refactor diff --git a/cmd/zetaclientd/debug.go b/cmd/zetaclientd/debug.go index 2e5c1bbd49..a897fdea65 100644 --- a/cmd/zetaclientd/debug.go +++ b/cmd/zetaclientd/debug.go @@ -169,17 +169,21 @@ func debugCmd(_ *cobra.Command, args []string) error { fmt.Println("CoinType not detected") } fmt.Println("CoinType : ", coinType) - } else if chain.IsUTXO() { + } else if chain.IsBitcoin() { btcObserver := btcobserver.Observer{} btcObserver.WithZetacoreClient(client) btcObserver.WithChain(*chainProto) + btcConfig, found := cfg.GetBTCConfig(chainID) + if !found { + return fmt.Errorf("unable to find config for BTC chain %d", chainID) + } connCfg := &rpcclient.ConnConfig{ - Host: cfg.BitcoinConfig.RPCHost, - User: cfg.BitcoinConfig.RPCUsername, - Pass: cfg.BitcoinConfig.RPCPassword, + Host: btcConfig.RPCHost, + User: btcConfig.RPCUsername, + Pass: btcConfig.RPCPassword, HTTPPostMode: true, DisableTLS: true, - Params: cfg.BitcoinConfig.RPCParams, + Params: btcConfig.RPCParams, } btcClient, err := rpcclient.New(connCfg, nil) diff --git a/cmd/zetaclientd/start.go b/cmd/zetaclientd/start.go index 2dfc388d05..c46cfc4f3a 100644 --- a/cmd/zetaclientd/start.go +++ b/cmd/zetaclientd/start.go @@ -154,7 +154,7 @@ func start(_ *cobra.Command, _ []string) error { return err } - startLogger.Info().Msgf("Config is updated from zetacore %s", maskCfg(cfg)) + startLogger.Info().Msgf("Config is updated from zetacore\n %s", cfg.StringMasked()) go zetacoreClient.UpdateAppContextWorker(ctx, appContext) @@ -230,7 +230,7 @@ func start(_ *cobra.Command, _ []string) error { return err } - btcChains := appContext.FilterChains(zctx.Chain.IsUTXO) + btcChains := appContext.FilterChains(zctx.Chain.IsBitcoin) switch { case len(btcChains) == 0: return errors.New("no BTC chains found") diff --git a/cmd/zetaclientd/start_utils.go b/cmd/zetaclientd/start_utils.go index a0b97e59ca..df80814bb8 100644 --- a/cmd/zetaclientd/start_utils.go +++ b/cmd/zetaclientd/start_utils.go @@ -3,7 +3,6 @@ package main import ( "fmt" "net" - "net/url" "strings" "time" @@ -52,43 +51,3 @@ func validatePeer(seedPeer string) error { return nil } - -// maskCfg sensitive fields are masked, currently only the EVM endpoints and bitcoin credentials, -// -// other fields can be added. -func maskCfg(cfg config.Config) string { - maskedCfg := cfg - - maskedCfg.BitcoinConfig = config.BTCConfig{ - RPCUsername: cfg.BitcoinConfig.RPCUsername, - RPCPassword: cfg.BitcoinConfig.RPCPassword, - RPCHost: cfg.BitcoinConfig.RPCHost, - RPCParams: cfg.BitcoinConfig.RPCParams, - } - maskedCfg.EVMChainConfigs = map[int64]config.EVMConfig{} - for key, val := range cfg.EVMChainConfigs { - maskedCfg.EVMChainConfigs[key] = config.EVMConfig{ - Chain: val.Chain, - Endpoint: val.Endpoint, - } - } - - // Mask Sensitive data - for _, chain := range maskedCfg.EVMChainConfigs { - if chain.Endpoint == "" { - continue - } - endpointURL, err := url.Parse(chain.Endpoint) - if err != nil { - continue - } - chain.Endpoint = endpointURL.Hostname() - } - - // mask endpoints - maskedCfg.BitcoinConfig.RPCUsername = "" - maskedCfg.BitcoinConfig.RPCPassword = "" - maskedCfg.SolanaConfig.Endpoint = "" - - return maskedCfg.String() -} diff --git a/go.mod b/go.mod index d237f56929..7dd5a40f3d 100644 --- a/go.mod +++ b/go.mod @@ -337,6 +337,7 @@ require ( require ( github.com/oasisprotocol/curve25519-voi v0.0.0-20220328075252-7dd334e3daae // indirect + github.com/showa-93/go-mask v0.6.2 // indirect github.com/snksoft/crc v1.1.0 // indirect github.com/tonkeeper/tongo v1.9.3 // indirect ) diff --git a/go.sum b/go.sum index 441cdb09db..b31bb3d573 100644 --- a/go.sum +++ b/go.sum @@ -1433,6 +1433,8 @@ github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1 github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/showa-93/go-mask v0.6.2 h1:sJEUQRpbxUoMTfBKey5K9hCg+eSx5KIAZFT7pa1LXbM= +github.com/showa-93/go-mask v0.6.2/go.mod h1:aswIj007gm0EPAzOGES9ACy1jDm3QT08/LPSClMp410= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= diff --git a/pkg/chains/chains.go b/pkg/chains/chains.go index bc2cea1ccf..38c9252291 100644 --- a/pkg/chains/chains.go +++ b/pkg/chains/chains.go @@ -171,7 +171,7 @@ var ( BitcoinSignetTestnet = Chain{ ChainName: ChainName_btc_signet_testnet, - ChainId: 18334, + ChainId: 18333, Network: Network_btc, NetworkType: NetworkType_testnet, Vm: Vm_no_vm, diff --git a/zetaclient/config/config_chain.go b/zetaclient/config/config_chain.go index 54d01baaf7..ca0234c126 100644 --- a/zetaclient/config/config_chain.go +++ b/zetaclient/config/config_chain.go @@ -14,15 +14,15 @@ const ( func New(setDefaults bool) Config { cfg := Config{ EVMChainConfigs: make(map[int64]EVMConfig), - BitcoinConfig: BTCConfig{}, + BTCChainConfigs: make(map[int64]BTCConfig), mu: &sync.RWMutex{}, } if setDefaults { - cfg.BitcoinConfig = bitcoinConfigRegnet() - cfg.SolanaConfig = solanaConfigLocalnet() cfg.EVMChainConfigs = evmChainsConfigs() + cfg.BTCChainConfigs = btcChainsConfigs() + cfg.SolanaConfig = solanaConfigLocalnet() } return cfg @@ -80,3 +80,10 @@ func evmChainsConfigs() map[int64]EVMConfig { }, } } + +// btcChainsConfigs contains BTC chain configs +func btcChainsConfigs() map[int64]BTCConfig { + return map[int64]BTCConfig{ + chains.BitcoinRegtest.ChainId: bitcoinConfigRegnet(), + } +} diff --git a/zetaclient/config/types.go b/zetaclient/config/types.go index fdda9f0526..b9a889272a 100644 --- a/zetaclient/config/types.go +++ b/zetaclient/config/types.go @@ -5,6 +5,8 @@ import ( "strings" "sync" + "github.com/showa-93/go-mask" + "github.com/zeta-chain/node/pkg/chains" ) @@ -38,23 +40,23 @@ type ClientConfiguration struct { // EVMConfig is the config for EVM chain type EVMConfig struct { Chain chains.Chain - Endpoint string + Endpoint string `mask:"filled"` RPCAlertLatency int64 } // BTCConfig is the config for Bitcoin chain type BTCConfig struct { // the following are rpcclient ConnConfig fields - RPCUsername string - RPCPassword string - RPCHost string + RPCUsername string `mask:"filled"` + RPCPassword string `mask:"filled"` + RPCHost string `mask:"filled"` RPCParams string // "regtest", "mainnet", "testnet3" , "signet" RPCAlertLatency int64 } // SolanaConfig is the config for Solana chain type SolanaConfig struct { - Endpoint string + Endpoint string `mask:"filled"` RPCAlertLatency int64 } @@ -91,8 +93,10 @@ type Config struct { // chain configs EVMChainConfigs map[int64]EVMConfig `json:"EVMChainConfigs"` - BitcoinConfig BTCConfig `json:"BitcoinConfig"` - SolanaConfig SolanaConfig `json:"SolanaConfig"` + BTCChainConfigs map[int64]BTCConfig `json:"BTCChainConfigs"` + // Deprecated: the 'BitcoinConfig' will be removed once the 'BTCChainConfigs' is fully adopted + BitcoinConfig BTCConfig `json:"BitcoinConfig"` + SolanaConfig SolanaConfig `json:"SolanaConfig"` // compliance config ComplianceConfig ComplianceConfig `json:"ComplianceConfig"` @@ -104,8 +108,9 @@ type Config struct { func (c Config) GetEVMConfig(chainID int64) (EVMConfig, bool) { c.mu.RLock() defer c.mu.RUnlock() - evmCfg, found := c.EVMChainConfigs[chainID] - return evmCfg, found + + evmCfg := c.EVMChainConfigs[chainID] + return evmCfg, !evmCfg.Empty() } // GetAllEVMConfigs returns a map of all EVM configs @@ -121,12 +126,19 @@ func (c Config) GetAllEVMConfigs() map[int64]EVMConfig { return copied } -// GetBTCConfig returns the BTC config -func (c Config) GetBTCConfig() (BTCConfig, bool) { +// GetBTCConfig returns the BTC config for the given chain ID +func (c Config) GetBTCConfig(chainID int64) (BTCConfig, bool) { c.mu.RLock() defer c.mu.RUnlock() - return c.BitcoinConfig, c.BitcoinConfig != (BTCConfig{}) + // we prefer 'BTCChainConfigs' over 'BitcoinConfig' but still fallback to be backward compatible + // this will allow new 'zetaclientd' binary to work with old config file + btcCfg, found := c.BTCChainConfigs[chainID] + if !found || btcCfg.Empty() { + btcCfg = c.BitcoinConfig + } + + return btcCfg, !btcCfg.Empty() } // GetSolanaConfig returns the Solana config @@ -137,9 +149,20 @@ func (c Config) GetSolanaConfig() (SolanaConfig, bool) { return c.SolanaConfig, c.SolanaConfig != (SolanaConfig{}) } -// String returns the string representation of the config -func (c Config) String() string { - s, err := json.MarshalIndent(c, "", "\t") +// StringMasked returns the string representation of the config with sensitive fields masked. +// Currently only the endpoints and bitcoin credentials are masked. +func (c Config) StringMasked() string { + // create a masker + masker := mask.NewMasker() + masker.RegisterMaskStringFunc(mask.MaskTypeFilled, masker.MaskFilledString) + + // mask the config + masked, err := masker.Mask(c) + if err != nil { + return "" + } + + s, err := json.MarshalIndent(masked, "", "\t") if err != nil { return "" } @@ -178,5 +201,9 @@ func (c Config) GetRelayerKeyPath() string { } func (c EVMConfig) Empty() bool { - return c.Endpoint == "" && c.Chain.IsEmpty() + return c.Endpoint == "" || c.Chain.IsEmpty() +} + +func (c BTCConfig) Empty() bool { + return c.RPCHost == "" } diff --git a/zetaclient/config/types_test.go b/zetaclient/config/types_test.go index e63684db28..c57fd002e0 100644 --- a/zetaclient/config/types_test.go +++ b/zetaclient/config/types_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/stretchr/testify/require" + "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/zetaclient/config" ) @@ -14,3 +15,127 @@ func Test_GetRelayerKeyPath(t *testing.T) { // should return default relayer key path require.Equal(t, config.DefaultRelayerKeyPath, cfg.GetRelayerKeyPath()) } + +func Test_GetEVMConfig(t *testing.T) { + chain := chains.Sepolia + chainID := chains.Sepolia.ChainId + + t.Run("should find non-empty evm config", func(t *testing.T) { + // create config with defaults + cfg := config.New(true) + + // set valid evm endpoint + cfg.EVMChainConfigs[chainID] = config.EVMConfig{ + Chain: chain, + Endpoint: "localhost", + } + + // should return non-empty evm config + evmCfg, found := cfg.GetEVMConfig(chainID) + require.True(t, found) + require.False(t, evmCfg.Empty()) + }) + + t.Run("should not find evm config if endpoint is empty", func(t *testing.T) { + // create config with defaults + cfg := config.New(true) + + // should not find evm config because endpoint is empty + _, found := cfg.GetEVMConfig(chainID) + require.False(t, found) + }) + + t.Run("should not find evm config if chain is empty", func(t *testing.T) { + // create config with defaults + cfg := config.New(true) + + // set empty chain + cfg.EVMChainConfigs[chainID] = config.EVMConfig{ + Chain: chains.Chain{}, + Endpoint: "localhost", + } + + // should not find evm config because chain is empty + _, found := cfg.GetEVMConfig(chainID) + require.False(t, found) + }) +} + +func Test_GetBTCConfig(t *testing.T) { + tests := []struct { + name string + chainID int64 + oldCfg config.BTCConfig + btcCfg *config.BTCConfig + want bool + }{ + { + name: "should find non-empty btc config", + chainID: chains.BitcoinRegtest.ChainId, + btcCfg: &config.BTCConfig{ + RPCHost: "localhost", + }, + want: true, + }, + { + name: "should fallback to old 'BitcoinConfig' if new config is not set", + chainID: chains.BitcoinRegtest.ChainId, + oldCfg: config.BTCConfig{ + RPCHost: "old_host", + }, + btcCfg: nil, // new config is not set + want: true, + }, + { + name: "should fallback to old config but still can't find btc config as it's empty", + chainID: chains.BitcoinRegtest.ChainId, + oldCfg: config.BTCConfig{ + RPCUsername: "user", + RPCPassword: "pass", + RPCHost: "", // empty config + RPCParams: "regtest", + }, + btcCfg: &config.BTCConfig{ + RPCUsername: "user", + RPCPassword: "pass", + RPCHost: "", // empty config + RPCParams: "regtest", + }, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // create config with defaults + cfg := config.New(true) + + // set both new and old btc config + cfg.BitcoinConfig = tt.oldCfg + if tt.btcCfg != nil { + cfg.BTCChainConfigs[tt.chainID] = *tt.btcCfg + } + + // should return btc config + btcCfg, found := cfg.GetBTCConfig(tt.chainID) + require.Equal(t, tt.want, found) + require.Equal(t, tt.want, !btcCfg.Empty()) + }) + } +} + +func Test_StringMasked(t *testing.T) { + // create config with defaults + cfg := config.New(true) + + // mask the config JSON string + masked := cfg.StringMasked() + require.NotEmpty(t, masked) + + // should contain necessary fields + require.Contains(t, masked, "EVMChainConfigs") + require.Contains(t, masked, "BTCChainConfigs") + + // should not contain endpoint + require.NotContains(t, masked, "http") +} diff --git a/zetaclient/context/app_test.go b/zetaclient/context/app_test.go index c0b2fd7126..7439aebc44 100644 --- a/zetaclient/context/app_test.go +++ b/zetaclient/context/app_test.go @@ -30,7 +30,7 @@ func TestAppContext(t *testing.T) { ttsPubKey = "tssPubKeyTest" ) - testCfg.BitcoinConfig.RPCUsername = "abc" + testCfg.BTCChainConfigs[111] = config.BTCConfig{RPCUsername: "satoshi"} ethParams := types.GetDefaultEthMainnetChainParams() ethParams.IsSupported = true @@ -106,7 +106,7 @@ func TestAppContext(t *testing.T) { ethChain, err := appContext.GetChain(1) assert.NoError(t, err) assert.True(t, ethChain.IsEVM()) - assert.False(t, ethChain.IsUTXO()) + assert.False(t, ethChain.IsBitcoin()) assert.False(t, ethChain.IsSolana()) assert.Equal(t, ethParams, ethChain.Params()) @@ -121,7 +121,7 @@ func TestAppContext(t *testing.T) { assert.ElementsMatch(t, expectedIDs, appContext.ListChainIDs()) // Check config - assert.Equal(t, "abc", appContext.Config().BitcoinConfig.RPCUsername) + assert.Equal(t, "satoshi", appContext.Config().BTCChainConfigs[111].RPCUsername) t.Run("edge-cases", func(t *testing.T) { for _, tt := range []struct { diff --git a/zetaclient/context/chain.go b/zetaclient/context/chain.go index 946dbae598..117b921168 100644 --- a/zetaclient/context/chain.go +++ b/zetaclient/context/chain.go @@ -157,7 +157,7 @@ func (c Chain) IsZeta() bool { return chains.IsZetaChain(c.ID(), c.registry.additionalChains) } -func (c Chain) IsUTXO() bool { +func (c Chain) IsBitcoin() bool { return chains.IsBitcoinChain(c.ID(), c.registry.additionalChains) } diff --git a/zetaclient/context/chain_test.go b/zetaclient/context/chain_test.go index 8151b8e5eb..31e74b39d7 100644 --- a/zetaclient/context/chain_test.go +++ b/zetaclient/context/chain_test.go @@ -73,7 +73,7 @@ func TestChainRegistry(t *testing.T) { ethChain, err := r.Get(eth.ChainId) require.NoError(t, err) require.True(t, ethChain.IsEVM()) - require.False(t, ethChain.IsUTXO()) + require.False(t, ethChain.IsBitcoin()) require.False(t, ethChain.IsSolana()) require.Equal(t, ethParams, ethChain.Params()) }) diff --git a/zetaclient/orchestrator/bootstap_test.go b/zetaclient/orchestrator/bootstap_test.go index 6153961183..73f47d21cf 100644 --- a/zetaclient/orchestrator/bootstap_test.go +++ b/zetaclient/orchestrator/bootstap_test.go @@ -49,7 +49,7 @@ func TestCreateSignerMap(t *testing.T) { Endpoint: testutils.MockEVMRPCEndpoint, } - cfg.BitcoinConfig = btcConfig + cfg.BTCChainConfigs[chains.BitcoinMainnet.ChainId] = btcConfig // Given AppContext app := zctx.New(cfg, nil, log) @@ -227,7 +227,7 @@ func TestCreateChainObserverMap(t *testing.T) { Endpoint: evmServer.Endpoint, } - cfg.BitcoinConfig = btcConfig + cfg.BTCChainConfigs[chains.BitcoinMainnet.ChainId] = btcConfig cfg.SolanaConfig = solConfig // Given AppContext diff --git a/zetaclient/orchestrator/bootstrap.go b/zetaclient/orchestrator/bootstrap.go index f7be6ad504..08d625548f 100644 --- a/zetaclient/orchestrator/bootstrap.go +++ b/zetaclient/orchestrator/bootstrap.go @@ -136,16 +136,16 @@ func syncSignerMap( } addSigner(chainID, signer) - case chain.IsUTXO(): - cfg, found := app.Config().GetBTCConfig() + case chain.IsBitcoin(): + cfg, found := app.Config().GetBTCConfig(chainID) if !found { - logger.Std.Warn().Msgf("Unable to find UTXO config for chain %d", chainID) + logger.Std.Warn().Msgf("Unable to find BTC config for chain %d signer", chainID) continue } signer, err := btcsigner.NewSigner(*rawChain, tss, ts, logger, cfg) if err != nil { - logger.Std.Error().Err(err).Msgf("Unable to construct signer for UTXO chain %d", chainID) + logger.Std.Error().Err(err).Msgf("Unable to construct signer for BTC chain %d", chainID) continue } @@ -321,10 +321,10 @@ func syncObserverMap( } addObserver(chainID, observer) - case chain.IsUTXO(): - cfg, found := app.Config().GetBTCConfig() + case chain.IsBitcoin(): + cfg, found := app.Config().GetBTCConfig(chainID) if !found { - logger.Std.Warn().Msgf("Unable to find chain params for BTC chain %d", chainID) + logger.Std.Warn().Msgf("Unable to find BTC config for chain %d observer", chainID) continue } diff --git a/zetaclient/orchestrator/orchestrator.go b/zetaclient/orchestrator/orchestrator.go index 6b2b673f4e..dd0ef1eaab 100644 --- a/zetaclient/orchestrator/orchestrator.go +++ b/zetaclient/orchestrator/orchestrator.go @@ -410,7 +410,7 @@ func (oc *Orchestrator) runScheduler(ctx context.Context) error { switch { case chain.IsEVM(): oc.ScheduleCctxEVM(ctx, zetaHeight, chainID, cctxList, ob, signer) - case chain.IsUTXO(): + case chain.IsBitcoin(): oc.ScheduleCctxBTC(ctx, zetaHeight, chainID, cctxList, ob, signer) case chain.IsSolana(): oc.ScheduleCctxSolana(ctx, zetaHeight, chainID, cctxList, ob, signer) diff --git a/zetaclient/orchestrator/orchestrator_test.go b/zetaclient/orchestrator/orchestrator_test.go index b15b93bb54..969dbbb393 100644 --- a/zetaclient/orchestrator/orchestrator_test.go +++ b/zetaclient/orchestrator/orchestrator_test.go @@ -518,16 +518,20 @@ func createAppContext(t *testing.T, chainsOrParams ...any) *zctx.AppContext { cfg := config.New(false) // Mock config - cfg.BitcoinConfig = config.BTCConfig{ - RPCHost: "localhost", - } - for _, c := range supportedChains { - if chains.IsEVMChain(c.ChainId, nil) { + switch { + case chains.IsEVMChain(c.ChainId, nil): cfg.EVMChainConfigs[c.ChainId] = config.EVMConfig{Chain: c} + case chains.IsBitcoinChain(c.ChainId, nil): + cfg.BTCChainConfigs[c.ChainId] = config.BTCConfig{RPCHost: "localhost"} + case chains.IsSolanaChain(c.ChainId, nil): + cfg.SolanaConfig = config.SolanaConfig{Endpoint: "localhost"} + default: + t.Fatalf("create app context: unsupported chain %d", c.ChainId) } } + // chain params params := map[int64]*observertypes.ChainParams{} for i := range obsParams { cp := obsParams[i]