// Package indexerconfig is the config loader for the indexer package indexerconfig import ( "fmt" "os" "path/filepath" "github.com/richardwilkes/toolbox/collection" "github.com/synapsecns/sanguine/services/explorer/config" "github.com/ethereum/go-ethereum/common" "github.com/jftuga/ellipsis" "gopkg.in/yaml.v2" ) // ContractType is the type of contract specified by the config and used for selecting the correct parser. type ContractType int const ( // BridgeContractType is the ContractType for the bridge contract. BridgeContractType ContractType = iota // SwapContractType is the ContractType for the swap contract. SwapContractType // MessageBusContractType is the ContractType for the message bus contract. MessageBusContractType // MetaSwapContractType is the ContractType for the meta swap contract. MetaSwapContractType // CCTPContractType is the ContractType for the cctp contract. CCTPContractType // RFQContractType is the ContractType for the rfq contract. RFQContractType ) func (c ContractType) String() string { return [...]string{"bridge", "swap", "messagebus", "metaswap", "cctp", "rfq"}[c] } // ContractTypeFromString converts a string (intended to be from parsed config) into the ContractType type. func ContractTypeFromString(s string) (ContractType, error) { switch s { case "bridge": return BridgeContractType, nil case "swap": return SwapContractType, nil case "messagebus": return MessageBusContractType, nil case "metaswap": return MetaSwapContractType, nil case "cctp": return CCTPContractType, nil case "rfq": return RFQContractType, nil default: return -1, fmt.Errorf("unknown contract type: %s", s) } } // Config is used to configure the explorer's data consumption. type Config struct { // DefaultRefreshRate is the default rate at which data is refreshed. DefaultRefreshRate int `yaml:"default_refresh_rate"` // ScribeURL is the URL of the Scribe server. ScribeURL string `yaml:"scribe_url"` // RPCURL is the URL of the RPC server. RPCURL string `yaml:"rpc_url"` // BridgeConfigAddress is the address of BridgeConfig contract. BridgeConfigAddress string `yaml:"bridge_config_address"` // BridgeConfigChainID is the ChainID of BridgeConfig contract. BridgeConfigChainID uint32 `yaml:"bridge_config_chain_id"` // Chains stores the chain configurations. Chains []ChainConfig `yaml:"chains"` } // ChainConfig is the configuration for a chain. type ChainConfig struct { // ChainID is the ID of the chain. ChainID uint32 `yaml:"chain_id"` // RPCURL is the RPC of the chain. RPCURL string `yaml:"rpc_url"` // FetchBlockIncrement is the number of blocks to fetch at a time. FetchBlockIncrement uint64 `yaml:"fetch_block_increment"` // MaxGoroutines is the maximum number of goroutines that can be spawned. MaxGoroutines int `yaml:"max_goroutines"` // Contracts are the contracts. Contracts []ContractConfig `yaml:"contracts"` } // ContractConfig is the configuration for a contract. type ContractConfig struct { // ContractType is the type of contract. ContractType string `yaml:"contract_type"` // Addresses are the addresses of the contracts Address string `yaml:"address"` // StartBlock is where to start backfilling this address from. StartBlock int64 `yaml:"start_block"` } // IsValid makes sure the config is valid. This is done by calling IsValid() on each // submodule. If any method returns an error that is returned here and the entirety // of IsValid returns false. Any warnings are logged by the submodules respective loggers. func (c *Config) IsValid() error { switch { case c.BridgeConfigAddress == "": return fmt.Errorf("bridge_config_address, %w", config.ErrRequiredGlobalField) case c.ScribeURL == "": return fmt.Errorf("scribe_url, %w", config.ErrRequiredGlobalField) case c.RPCURL == "": return fmt.Errorf("rpc_url, %w", config.ErrRequiredGlobalField) case c.BridgeConfigChainID == 0: return fmt.Errorf("chain_id cannot be 0") } if len(c.BridgeConfigAddress) != (common.AddressLength*2)+2 { return fmt.Errorf("field Address: %w", config.ErrAddressLength) } if len(c.Chains) == 0 { return fmt.Errorf("no chains specified for indexing") } for _, chain := range c.Chains { err := chain.IsValid() if err != nil { return fmt.Errorf("chain with ID %d is invalid: %w", chain.ChainID, err) } } return nil } // IsValid validates the chain config. func (c ChainConfig) IsValid() error { switch { case c.ChainID == 0: return fmt.Errorf("chain_id, %w", config.ErrRequiredGlobalField) case c.MaxGoroutines == 0: return fmt.Errorf("max_goroutines, %w", config.ErrRequiredGlobalField) } intSet := collection.Set[string]{} for _, contract := range c.Contracts { err := contract.IsValid() if err != nil { return fmt.Errorf("contract with address %s is invalid: %w", contract.Address, err) } if intSet.Contains(contract.Address) { return fmt.Errorf("address %s appears twice", contract.Address) } intSet.Add(contract.Address) } return nil } // IsValid validates the chain config. func (c ContractConfig) IsValid() error { _, err := ContractTypeFromString(c.ContractType) if err != nil { return fmt.Errorf("contract_type %s invalid for address %s", c.ContractType, c.Address) } switch { case c.StartBlock == 0: return fmt.Errorf("start_block, %w", config.ErrRequiredContractField) case c.Address == "": return fmt.Errorf("address, %w", config.ErrRequiredContractField) } return nil } // DecodeConfig parses in a config from a file. func DecodeConfig(filePath string) (cfg Config, err error) { if _, err := os.Stat(filePath); os.IsNotExist(err) { return Config{}, fmt.Errorf("config file does not exist: %w", err) } input, err := os.ReadFile(filepath.Clean(filePath)) if err != nil { return Config{}, fmt.Errorf("failed to read file: %w", err) } err = yaml.Unmarshal(input, &cfg) if err != nil { return Config{}, fmt.Errorf("could not unmarshall config %s: %w", ellipsis.Shorten(string(input), 30), err) } err = cfg.IsValid() if err != nil { return cfg, err } return cfg, nil }