package main import ( "context" "log" "os" "os/signal" "syscall" "time" solana_go "github.com/gagliardetto/solana-go" ipfslog "github.com/ipfs/go-log/v2" "github.com/wormhole-foundation/wormhole-explorer/common/domain" "github.com/wormhole-foundation/wormhole-explorer/common/health" "github.com/wormhole-foundation/wormhole-explorer/contract-watcher/config" "github.com/wormhole-foundation/wormhole-explorer/contract-watcher/http/infrastructure" "github.com/wormhole-foundation/wormhole-explorer/contract-watcher/internal/ankr" "github.com/wormhole-foundation/wormhole-explorer/contract-watcher/internal/db" "github.com/wormhole-foundation/wormhole-explorer/contract-watcher/internal/solana" "github.com/wormhole-foundation/wormhole-explorer/contract-watcher/internal/terra" "github.com/wormhole-foundation/wormhole-explorer/contract-watcher/processor" "github.com/wormhole-foundation/wormhole-explorer/contract-watcher/storage" "github.com/wormhole-foundation/wormhole-explorer/contract-watcher/watcher" "github.com/wormhole-foundation/wormhole/sdk/vaa" "go.mongodb.org/mongo-driver/mongo" "go.uber.org/ratelimit" "go.uber.org/zap" ) type exitCode int func handleExit() { if r := recover(); r != nil { if e, ok := r.(exitCode); ok { os.Exit(int(e)) } panic(r) // not an Exit, bubble up } } func main() { defer handleExit() rootCtx, rootCtxCancel := context.WithCancel(context.Background()) config, err := config.New(rootCtx) if err != nil { log.Fatal("Error creating config", err) } level, err := ipfslog.LevelFromString(config.LogLevel) if err != nil { log.Fatal("Invalid log level", err) } logger := ipfslog.Logger("wormhole-explorer-contract-watcher").Desugar() ipfslog.SetAllLoggers(level) logger.Info("Starting wormhole-explorer-contract-watcher ...") //setup DB connection db, err := db.New(rootCtx, logger, config.MongoURI, config.MongoDatabase) if err != nil { logger.Fatal("failed to connect MongoDB", zap.Error(err)) } // get health check functions. healthChecks, err := newHealthChecks(rootCtx, db.Database) if err != nil { logger.Fatal("failed to create health checks", zap.Error(err)) } // create repositories repo := storage.NewRepository(db.Database, logger) // create watchers watchers := newWatchers(config, repo, logger) //create processor processor := processor.NewProcessor(watchers, logger) processor.Start(rootCtx) // create and start server. server := infrastructure.NewServer(logger, config.Port, config.PprofEnabled, healthChecks...) server.Start() logger.Info("Started wormhole-explorer-contract-watcher") // Waiting for signal sigterm := make(chan os.Signal, 1) signal.Notify(sigterm, syscall.SIGINT, syscall.SIGTERM) select { case <-rootCtx.Done(): logger.Warn("Terminating with root context cancelled.") case signal := <-sigterm: logger.Info("Terminating with signal.", zap.String("signal", signal.String())) } logger.Info("root context cancelled, exiting...") rootCtxCancel() logger.Info("Closing processor ...") processor.Close() logger.Info("Closing database connections ...") db.Close() logger.Info("Closing Http server ...") server.Stop() logger.Info("Finished wormhole-explorer-contract-watcher") } func newHealthChecks(ctx context.Context, db *mongo.Database) ([]health.Check, error) { return []health.Check{health.Mongo(db)}, nil } type watcherBlockchain struct { chainID vaa.ChainID name string address string sizeBlocks uint8 waitSeconds uint16 initialBlock int64 } type watchersConfig struct { evms []watcherBlockchain solana *watcherBlockchain terra *watcherBlockchain rateLimit rateLimitConfig } type rateLimitConfig struct { evm int solana int terra int } func newWatchers(config *config.Configuration, repo *storage.Repository, logger *zap.Logger) []watcher.ContractWatcher { var watchers *watchersConfig switch config.P2pNetwork { case domain.P2pMainNet: watchers = newEVMWatchersForMainnet() case domain.P2pTestNet: watchers = newEVMWatchersForTestnet() default: watchers = &watchersConfig{} } // add evm watchers result := make([]watcher.ContractWatcher, 0) // add evm watchers evmLimiter := ratelimit.New(watchers.rateLimit.evm, ratelimit.Per(time.Second)) ankrClient := ankr.NewAnkrSDK(config.AnkrUrl, evmLimiter) for _, w := range watchers.evms { params := watcher.EVMParams{ChainID: w.chainID, Blockchain: w.name, ContractAddress: w.address, SizeBlocks: w.sizeBlocks, WaitSeconds: w.waitSeconds, InitialBlock: w.initialBlock} result = append(result, watcher.NewEVMWatcher(ankrClient, repo, params, logger)) } // add solana watcher if watchers.solana != nil { contractAddress, err := solana_go.PublicKeyFromBase58(watchers.solana.address) if err != nil { logger.Fatal("failed to parse solana contract address", zap.Error(err)) } solanaLimiter := ratelimit.New(watchers.rateLimit.solana, ratelimit.Per(time.Second)) solanaClient := solana.NewSolanaSDK(config.SolanaUrl, solanaLimiter) params := watcher.SolanaParams{Blockchain: watchers.solana.name, ContractAddress: contractAddress, SizeBlocks: watchers.solana.sizeBlocks, WaitSeconds: watchers.solana.waitSeconds, InitialBlock: watchers.solana.initialBlock} result = append(result, watcher.NewSolanaWatcher(solanaClient, repo, params, logger)) } // add terra watcher if watchers.terra != nil { terraLimiter := ratelimit.New(watchers.rateLimit.terra, ratelimit.Per(time.Second)) terraClient := terra.NewTerraSDK(config.TerraUrl, terraLimiter) params := watcher.TerraParams{ChainID: watchers.terra.chainID, Blockchain: watchers.terra.name, ContractAddress: watchers.terra.address, WaitSeconds: watchers.terra.waitSeconds, InitialBlock: watchers.terra.initialBlock} result = append(result, watcher.NewTerraWatcher(terraClient, params, repo, logger)) } return result } func newEVMWatchersForMainnet() *watchersConfig { return &watchersConfig{ evms: []watcherBlockchain{ {vaa.ChainIDEthereum, "eth", "0x3ee18B2214AFF97000D974cf647E7C347E8fa585", 100, 10, 16820790}, {vaa.ChainIDPolygon, "polygon", "0x5a58505a96D1dbf8dF91cB21B54419FC36e93fdE", 100, 10, 40307020}, {vaa.ChainIDBSC, "bsc", "0xB6F6D86a8f9879A9c87f643768d9efc38c1Da6E7", 100, 10, 26436320}, {vaa.ChainIDFantom, "fantom", "0x7C9Fc5741288cDFdD83CeB07f3ea7e22618D79D2", 100, 10, 57525624}, }, solana: &watcherBlockchain{vaa.ChainIDSolana, "solana", "wormDTUJ6AWPNvk59vGQbDvGJmqbDTdgWgAqcLBCgUb", 100, 10, 183675278}, terra: &watcherBlockchain{vaa.ChainIDTerra, "terra", "terra10nmmwe8r3g99a9newtqa7a75xfgs2e8z87r2sf", 0, 10, 12005338}, rateLimit: rateLimitConfig{ evm: 1000, solana: 3, terra: 10, }, } } func newEVMWatchersForTestnet() *watchersConfig { return &watchersConfig{ evms: []watcherBlockchain{ {vaa.ChainIDEthereum, "eth_goerli", "0xF890982f9310df57d00f659cf4fd87e65adEd8d7", 100, 10, 8660321}, {vaa.ChainIDPolygon, "polygon_mumbai", "0x377D55a7928c046E18eEbb61977e714d2a76472a", 100, 10, 33151522}, {vaa.ChainIDBSC, "bsc_testnet_chapel", "0x9dcF9D205C9De35334D646BeE44b2D2859712A09", 100, 10, 28071327}, {vaa.ChainIDFantom, "fantom_testnet", "0x599CEa2204B4FaECd584Ab1F2b6aCA137a0afbE8", 100, 10, 14524466}, }, solana: &watcherBlockchain{vaa.ChainIDSolana, "solana", "DZnkkTmCiFWfYTfT41X3Rd1kDgozqzxWaHqsw6W4x2oe", 10, 10, 16820790}, rateLimit: rateLimitConfig{ evm: 10, solana: 2, terra: 5, }, } }