diff --git a/Makefile b/Makefile index 2a25a13..df8deba 100644 --- a/Makefile +++ b/Makefile @@ -2,53 +2,33 @@ DOCKER := $(shell which docker) -CURRENT_DIR = $(shell pwd) BUILDDIR ?= $(CURDIR)/build -BUILD_FLAGS := -tags "$(build_tags)" -ldflags '$(ldflags)' -# check for nostrip option -ifeq (,$(findstring nostrip,$(COSMOS_BUILD_OPTIONS))) - BUILD_FLAGS += -trimpath -endif +BRANCH := $(shell git rev-parse --abbrev-ref HEAD) +COMMIT := $(shell git log -1 --format='%H') -# Check for debug option -ifeq (debug,$(findstring debug,$(COSMOS_BUILD_OPTIONS))) - BUILD_FLAGS += -gcflags "all=-N -l" +DIRTY := -dirty +ifeq (,$(shell git status --porcelain)) + DIRTY := endif -############################################################################### -### Protobuf ### -############################################################################### - -protoImageName=proto-genc -protoImage=$(DOCKER) run --rm -v $(CURDIR):/workspace --workdir /workspace/proto $(protoImageName) - -proto-all: proto-build-docker proto-format proto-lint make proto-format proto-update-deps proto-gen - -proto-build-docker: - @echo "Building Docker Container '$(protoImageName)' for Protobuf Compilation" - @docker build -t $(protoImageName) -f ./proto/Dockerfile . - -proto-gen: - @echo "Generating Protobuf Files" - @$(protoImage) sh -c "cd .. && sh ./scripts/protocgen.sh" +VERSION := $(shell git describe --tags --exact-match 2>/dev/null) +# if VERSION is empty, then populate it with branch's name and raw commit hash +ifeq (,$(VERSION)) + VERSION := $(BRANCH)-$(COMMIT) +endif -proto-format: - @echo "Formatting Protobuf Files with Clang" - @$(protoImage) find ./ -name "*.proto" -exec clang-format -i {} \; +VERSION := $(VERSION)$(DIRTY) -proto-lint: - @echo "Linting Protobuf Files With Buf" - @$(protoImage) buf lint +GIT_REVISION := $(shell git rev-parse HEAD)$(DIRTY) -proto-check-breaking: - @$(protoImage) buf breaking --against $(HTTPS_GIT)#branch=main +GO_SYSTEM_VERSION = $(shell go version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f1-2) -proto-update-deps: - @echo "Updating Protobuf dependencies" - @$(protoImage) buf mod update +ldflags= -X github.com/tessellated-io/restake-go/cmd/restake-go/cmd.RestakeVersion=${VERSION} \ + -X github.com/tessellated-io/restake-go/cmd/restake-go/cmd.GitRevision=${GIT_REVISION} \ + -X github.com/tessellated-io/restake-go/cmd/restake-go/cmd.GoVersion=${GO_SYSTEM_VERSION} -.PHONY: proto-all proto-gen proto-format proto-lint proto-check-breaking proto-update-deps +BUILD_FLAGS := -tags "$(build_tags)" -ldflags '$(ldflags)' ############################################################################### ### Build ### @@ -56,19 +36,17 @@ proto-update-deps: BUILD_TARGETS := build -build: BUILD_ARGS= - -build-linux-amd64: - @GOOS=linux GOARCH=amd64 LEDGER_ENABLED=false $(MAKE) build +build: + mkdir -p $(BUILDDIR)/ + go build -mod=readonly -ldflags '$(ldflags)' -trimpath -o $(BUILDDIR) ./...; -build-linux-arm64: - @GOOS=linux GOARCH=arm64 LEDGER_ENABLED=false $(MAKE) build +install: go.sum + go install $(BUILD_FLAGS) ./cmd/restake-go -$(BUILD_TARGETS): go.sum $(BUILDDIR)/ - @cd ${CURRENT_DIR} && go $@ -mod=readonly $(BUILD_FLAGS) $(BUILD_ARGS) ./... +clean: + rm -rf $(BUILDDIR)/* -$(BUILDDIR)/: - @mkdir -p $(BUILDDIR)/ +.PHONY: build ############################################################################### ### Tools & Dependencies ### diff --git a/README.md b/README.md index 0e565b5..0fe53d5 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,54 @@ Restake-Go is an implementation of the Restake Daemon in native golang. Restake- Restake-Go is provided at a beta-level. Tessellated uses this software in production, and you may too, but we make no warranties or guarantees. +## Installation + +Installing `restake-go` is easy. Simply clone the repository and run `make`. You'll need `go`, `git`, `make` and probably some other standard dev tools you already have installed. + +```shell +# Get restake-go +$ git clone https://github.com/tessellated-io/restake-go/ + +# Install restake-go +$ cd restake-go +$ make install + +# Find out what restake-go can do. +$ restake-go --help +``` + +## Quick Start + +// TODO: Sample config +Copy the sample config: ```shell -make install -restake-go --help +$ mkdir ~/.restake +$ cp config.sample.yml ~/.restake/config.yml ``` + +Fill out the config, then run `restake-go`: + +```shell +$ restake-go start +``` + +## Features + +Restake-go offers a number of features over the restake go. + +TODO: Fill this out +- automatic discover +- retry +- parallel execution +- tx inclusion polling +- gas normalization / auto discover +- ignores +- auto incrementing features + +As well as the features you know and love +- healthchecks + + +## Configuration + +TODO \ No newline at end of file diff --git a/TODO b/TODO index 737c5ab..916a621 100644 --- a/TODO +++ b/TODO @@ -1,16 +1,17 @@ -- Periodically reconnect the clients?\ -- Context with timeout +GAS +- health is only off by one -- make makefile work -- default gas price increase -- pull blocks -- what is with too many pings -- consider grant sizes or limits and query them correctly +DOCS +- header + credits (ascii art) +- README + branding +- Add memo for restake go -- print out what the gas fee is +GENERAL +- min restake amount in mine +- Reap panics - configure restake time - Fix default config parsing -- Rip out sleep - healthchecks only on success pings or obvious failures? -- fix package in go.mod \ No newline at end of file +- move towards debug log statements + diff --git a/cmd/restake-go/cmd/root.go b/cmd/restake-go/cmd/root.go new file mode 100644 index 0000000..6dfbbbb --- /dev/null +++ b/cmd/restake-go/cmd/root.go @@ -0,0 +1,31 @@ +/* +Copyright © 2023 Tessellated +*/ +package cmd + +import ( + "os" + + "github.com/spf13/cobra" +) + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "restake", + Short: "Restake-Go implements the Restake protocol.", + Long: `Restake-Go is an alternative implementation of the Restake protocol by Tessellated. + +See also: https://github.com/eco-stake/restake.`, +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } +} + +func init() { +} diff --git a/cmd/restake-go/cmd/start.go b/cmd/restake-go/cmd/start.go new file mode 100644 index 0000000..d4e9d64 --- /dev/null +++ b/cmd/restake-go/cmd/start.go @@ -0,0 +1,176 @@ +/* +Copyright © 2023 Tessellated +*/ +package cmd + +import ( + "context" + "fmt" + "os/user" + "sort" + "strings" + "sync" + "time" + + os2 "github.com/cometbft/cometbft/libs/os" + "github.com/spf13/cobra" + "github.com/tessellated-io/restake-go/codec" + "github.com/tessellated-io/restake-go/config" + "github.com/tessellated-io/restake-go/health" + "github.com/tessellated-io/restake-go/log" + "github.com/tessellated-io/restake-go/restake" + "github.com/tessellated-io/restake-go/rpc" +) + +var ( + configFile string + gasMultiplier float64 +) + +type RestakeResult struct { + network string + txHash string + err error +} + +type RestakeResults []*RestakeResult + +func (rr RestakeResults) Len() int { return len(rr) } +func (rr RestakeResults) Swap(i, j int) { rr[i], rr[j] = rr[j], rr[i] } +func (rr RestakeResults) Less(i, j int) bool { return rr[i].network < rr[j].network } + +// startCmd represents the start command +var startCmd = &cobra.Command{ + Use: "start", + Short: "Start the Restake Service", + Long: `Starts the Restake Service with the given configuration.`, + Run: func(cmd *cobra.Command, args []string) { + ctx := cmd.Context() + + fmt.Println() + fmt.Println("============================================================") + fmt.Println("Go Go... Restake-Go!") + fmt.Println() + fmt.Println("A Product Of Tessellated / tessellated.io") + fmt.Println("============================================================") + fmt.Println("") + + // Configure a logger + log := log.NewLogger() + + // Load config + expandedConfigFile := expandHomeDir(configFile) + configOk := os2.FileExists(expandedConfigFile) + if !configOk { + panic(fmt.Sprintf("Failed to load config file at: %s", configFile)) + } + log.Info().Str("config file", expandedConfigFile).Msg("Loading config from file") + + // Parse config + config, err := config.GetRestakeConfig(ctx, expandedConfigFile, log) + if err != nil { + panic(err) + } + + cdc := codec.GetCodec() + + // Make restake clients + restakeManagers := []*restake.RestakeManager{} + healthClients := []*health.HealthCheckClient{} + for _, chain := range config.Chains { + prefixedLogger := log.ApplyPrefix(fmt.Sprintf(" [%s]", chain.Network)) + + rpcClient, err := rpc.NewRpcClient(chain.NodeGrpcURI, cdc, prefixedLogger) + if err != nil { + panic(err) + } + + healthcheckId := chain.HealthcheckId + if healthcheckId == "" { + panic(fmt.Sprintf("No health check id found for network %s", chain.Network)) + } + healthClient := health.NewHealthCheckClient(chain.Network, healthcheckId, prefixedLogger) + healthClients = append(healthClients, healthClient) + + restakeManager, err := restake.NewRestakeManager(rpcClient, cdc, config.Mnemonic, config.Memo, gasMultiplier, *chain, prefixedLogger) + if err != nil { + panic(err) + } + restakeManagers = append(restakeManagers, restakeManager) + } + + runInterval := time.Duration(config.RunIntervalHours) * time.Hour + for { + var wg sync.WaitGroup + var results RestakeResults = []*RestakeResult{} + + for idx, restakeManager := range restakeManagers { + wg.Add(1) + + go func(ctx context.Context, restakeManager *restake.RestakeManager, healthClient *health.HealthCheckClient) { + defer wg.Done() + + timeoutContext, cancelFunc := context.WithTimeout(ctx, runInterval) + defer cancelFunc() + + _ = healthClient.Start() + txHash, err := restakeManager.Restake(timeoutContext) + if err != nil { + _ = healthClient.Failed(err) + } else { + _ = healthClient.Success("Hooray!") + } + + result := &RestakeResult{ + network: restakeManager.Network(), + txHash: txHash, + err: err, + } + results = append(results, result) + }(ctx, restakeManager, healthClients[idx]) + } + + // Print results whenever they all finish + go func() { + wg.Wait() + printResults(results, log) + }() + + log.Info().Dur("next run in hours", runInterval).Msg("Finished restaking. Sleeping until next round") + time.Sleep(runInterval) + } + }, +} + +func init() { + rootCmd.AddCommand(startCmd) + + startCmd.Flags().StringVarP(&configFile, "config-file", "c", "~/.restake/config.yml", "A path to the configuration file") + startCmd.Flags().Float64VarP(&gasMultiplier, "gas-multiplier", "g", 1.2, "The multiplier to use for gas") +} + +// TODO: Move to pickaxe here +func expandHomeDir(path string) string { + if !strings.HasPrefix(path, "~") { + return path + } + + usr, err := user.Current() + if err != nil { + panic(fmt.Errorf("failed to get user's home directory: %v", err)) + } + return strings.Replace(path, "~", usr.HomeDir, 1) +} + +func printResults(results RestakeResults, log *log.Logger) { + sort.Sort(results) + + log.Info().Msg("Restake Results:") + for _, result := range results { + if result.err == nil { + log.Info().Str("tx_hash", result.txHash).Msg(fmt.Sprintf("✅ %s: Success", result.network)) + } else { + log.Error().Err(result.err).Msg(fmt.Sprintf("❌ %s: Failure", result.network)) + } + } +} diff --git a/cmd/restake-go/cmd/version.go b/cmd/restake-go/cmd/version.go new file mode 100644 index 0000000..e57390b --- /dev/null +++ b/cmd/restake-go/cmd/version.go @@ -0,0 +1,39 @@ +/* +Copyright © 2023 Tessellated +*/ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// Binary name +const ( + binaryName = "restake-go" + binaryIcon = "♻️" +) + +// Version +var ( + RestakeVersion string + GoVersion string + GitRevision string +) + +// versionCmd represents the version command +var versionCmd = &cobra.Command{ + Use: "version", + Short: "Display the current version of Restake-Go", + Run: func(cmd *cobra.Command, args []string) { + fmt.Printf("%s %s:\n", binaryIcon, binaryName) + fmt.Printf(" - Version: %s\n", RestakeVersion) + fmt.Printf(" - Git Revision: %s\n", GitRevision) + fmt.Printf(" - Go Version: %s\n", GoVersion) + }, +} + +func init() { + rootCmd.AddCommand(versionCmd) +} diff --git a/cmd/restake-go/main.go b/cmd/restake-go/main.go new file mode 100644 index 0000000..8c44c0d --- /dev/null +++ b/cmd/restake-go/main.go @@ -0,0 +1,10 @@ +/* +Copyright © 2023 Tessellated +*/ +package main + +import "github.com/tessellated-io/restake-go/cmd/restake-go/cmd" + +func main() { + cmd.Execute() +} diff --git a/cmd/root.go b/cmd/root.go deleted file mode 100644 index 121ef10..0000000 --- a/cmd/root.go +++ /dev/null @@ -1,38 +0,0 @@ -/* -Copyright © 2023 Tessellated -*/ -package cmd - -import ( - "os" - - "github.com/spf13/cobra" -) - -// rootCmd represents the base command when called without any subcommands -var rootCmd = &cobra.Command{ - Use: "restake", - Short: "Restake implements the Restake protocol.", - Long: `Restake is an alternative implementation of the Restake protocol. See also: https://github.com/eco-stake/restake.`, -} - -// Execute adds all child commands to the root command and sets flags appropriately. -// This is called by main.main(). It only needs to happen once to the rootCmd. -func Execute() { - err := rootCmd.Execute() - if err != nil { - os.Exit(1) - } -} - -func init() { - // Here you will define your flags and configuration settings. - // Cobra supports persistent flags, which, if defined here, - // will be global for your application. - - // rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.restake-go.yaml)") - - // Cobra also supports local flags, which will only run - // when this action is called directly. - rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") -} diff --git a/cmd/start.go b/cmd/start.go deleted file mode 100644 index 0296589..0000000 --- a/cmd/start.go +++ /dev/null @@ -1,120 +0,0 @@ -/* -Copyright © 2023 Tessellated -*/ -package cmd - -import ( - "context" - "fmt" - "sync" - "time" - - os2 "github.com/cometbft/cometbft/libs/os" - "github.com/restake-go/codec" - "github.com/restake-go/config" - "github.com/restake-go/health" - "github.com/restake-go/log" - "github.com/restake-go/restake" - "github.com/restake-go/rpc" - "github.com/spf13/cobra" -) - -var ( - configFile string - gasMultiplier float64 -) - -// startCmd represents the start command -var startCmd = &cobra.Command{ - Use: "start", - Short: "Start the Restake Service", - Long: `Starts the Restake Service with the given configuration.`, - Run: func(cmd *cobra.Command, args []string) { - fmt.Println() - fmt.Println("============================================================") - fmt.Println("Go Go... Restake-Go!") - fmt.Println() - fmt.Println("A Product Of Tessellatd / tessellated.io") - fmt.Println("============================================================") - fmt.Println("") - - // Configure a logger - log := log.NewLogger() - - // Load config - configOk := os2.FileExists(configFile) - if !configOk { - panic(fmt.Sprintf("Failed to load config file at: %s", configFile)) - } - log.Info().Str("config file", configFile).Msg("Loading config from file") - - // Parse config - config, err := config.GetRestakeConfig(configFile, log) - if err != nil { - panic(err) - } - - cdc := codec.GetCodec() - - // Make restake clients - restakeManagers := []*restake.RestakeManager{} - healthClients := []*health.HealthCheckClient{} - for _, chain := range config.Chains { - prefixedLogger := log.ApplyPrefix(fmt.Sprintf(" [%s]", chain.Network)) - - rpcClient, err := rpc.NewRpcClient(chain.NodeGrpcURI, cdc, prefixedLogger) - if err != nil { - panic(err) - } - - healthcheckId := chain.HealthcheckId - if healthcheckId == "" { - panic(fmt.Sprintf("No health check id found for network %s", chain.Network)) - } - healthClient := health.NewHealthCheckClient(chain.Network, healthcheckId, prefixedLogger) - healthClients = append(healthClients, healthClient) - - restakeManager, err := restake.NewRestakeManager(rpcClient, cdc, config.Mnemonic, config.Memo, gasMultiplier, *chain, prefixedLogger) - if err != nil { - panic(err) - } - restakeManagers = append(restakeManagers, restakeManager) - } - - // TODO: Use a context with a deadline. - for { - // Wait group entered once by each network - var restakeSyncGroup sync.WaitGroup - - for idx, restakeClient := range restakeManagers { - restakeSyncGroup.Add(1) - - go func(restakeClient *restake.RestakeManager, healthClient *health.HealthCheckClient) { - defer restakeSyncGroup.Done() - - // TODO: better message - healthClient.Start("start") - err := restakeClient.Restake(context.Background()) - if err != nil { - healthClient.Failed(err.Error()) - } else { - healthClient.Success("Hooray!") - } - }(restakeClient, healthClients[idx]) - } - - // Wait for all networks to finish - restakeSyncGroup.Wait() - - log.Info().Int("sleep hours", config.SleepTimeHours).Msg("Finished restaking. Sleeping until next round") - time.Sleep(time.Duration(config.SleepTimeHours) * time.Hour) - } - }, -} - -func init() { - rootCmd.AddCommand(startCmd) - - startCmd.Flags().StringVarP(&configFile, "configFile", "c", "~/.restake/config.yml", "A path to the configuration file") - startCmd.Flags().Float64VarP(&gasMultiplier, "gasMultipler", "g", 1.2, "The multiplier to use for gas") -} diff --git a/config/config.go b/config/config.go index 2082feb..5b48b64 100644 --- a/config/config.go +++ b/config/config.go @@ -1,20 +1,21 @@ package config import ( + "context" "fmt" "math/big" "strings" - "github.com/restake-go/log" - "github.com/restake-go/registry" "github.com/tessellated-io/pickaxe/arrays" + "github.com/tessellated-io/restake-go/log" + "github.com/tessellated-io/restake-go/registry" ) type RestakeConfig struct { - Memo string - Mnemonic string - SleepTimeHours int - Chains []*ChainConfig + Memo string + Mnemonic string + RunIntervalHours int + Chains []*ChainConfig } type ChainConfig struct { @@ -32,7 +33,7 @@ type ChainConfig struct { CoinType int } -func GetRestakeConfig(filename string, log *log.Logger) (*RestakeConfig, error) { +func GetRestakeConfig(ctx context.Context, filename string, log *log.Logger) (*RestakeConfig, error) { // Get data from the file fileConfig, err := parseConfig(filename) if err != nil { @@ -41,7 +42,7 @@ func GetRestakeConfig(filename string, log *log.Logger) (*RestakeConfig, error) // Request network data for the validator registryClient := registry.NewRegistryClient() - restakeChains, err := registryClient.GetRestakeChains(fileConfig.Moniker) + restakeChains, err := registryClient.GetRestakeChains(ctx, fileConfig.Moniker) if err != nil { return nil, err } @@ -70,7 +71,7 @@ func GetRestakeConfig(filename string, log *log.Logger) (*RestakeConfig, error) } // Fetch chain info - registryChainInfo, err := registryClient.GetChainInfo(restakeChain.Name) + registryChainInfo, err := registryClient.GetChainInfo(ctx, restakeChain.Name) if err != nil { return nil, err } @@ -106,10 +107,10 @@ func GetRestakeConfig(filename string, log *log.Logger) (*RestakeConfig, error) } return &RestakeConfig{ - Mnemonic: fileConfig.Mnemonic, - Memo: fileConfig.Memo, - SleepTimeHours: fileConfig.SleepTimeHours, - Chains: configs, + Mnemonic: fileConfig.Mnemonic, + Memo: fileConfig.Memo, + RunIntervalHours: fileConfig.RunIntervalHours, + Chains: configs, }, nil } diff --git a/config/parser.go b/config/parser.go index d6dded5..578b5db 100644 --- a/config/parser.go +++ b/config/parser.go @@ -15,12 +15,12 @@ type RestakeChain struct { } type Config struct { - Moniker string `yaml:"moniker"` - Mnemonic string `yaml:"mnemonic"` - Memo string `yaml:"memo"` - Chains []RestakeChain `yaml:"chains"` - Ignores []string `yaml:"ignores"` - SleepTimeHours int `yaml:"sleepTime"` + Moniker string `yaml:"moniker"` + Mnemonic string `yaml:"mnemonic"` + Memo string `yaml:"memo"` + Chains []RestakeChain `yaml:"chains"` + Ignores []string `yaml:"ignores"` + RunIntervalHours int `yaml:"runIntervalHours"` } func parseConfig(filename string) (*Config, error) { diff --git a/go.mod b/go.mod index 0c76070..e457729 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/restake-go +module github.com/tessellated-io/restake-go go 1.20 @@ -27,7 +27,7 @@ require ( github.com/evmos/evmos/v14 v14.0.0 github.com/rs/zerolog v1.30.0 github.com/spf13/cobra v1.7.0 - github.com/tessellated-io/pickaxe v1.0.0 + github.com/tessellated-io/pickaxe v1.0.1 gopkg.in/yaml.v2 v2.4.0 ) diff --git a/go.sum b/go.sum index fb77377..84b4583 100644 --- a/go.sum +++ b/go.sum @@ -565,8 +565,8 @@ github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70 github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/tendermint/go-amino v0.16.0 h1:GyhmgQKvqF82e2oZeuMSp9JTN0N09emoSZlb2lyGa2E= github.com/tendermint/go-amino v0.16.0/go.mod h1:TQU0M1i/ImAo+tYpZi73AU3V/dKeCoMC9Sphe2ZwGME= -github.com/tessellated-io/pickaxe v1.0.0 h1:PFOF1vcRNlUFKSszktpaW9SrsBytJk3b5yQLkvAhGiI= -github.com/tessellated-io/pickaxe v1.0.0/go.mod h1:BUuDKLC0P9Y098K24k+cOcy4/6rCuiHX8T2vPQGHAQA= +github.com/tessellated-io/pickaxe v1.0.1 h1:3jQwi8tn6V7k+oK+AUx26gj3RRMsqgEq6fHqCdJK6B0= +github.com/tessellated-io/pickaxe v1.0.1/go.mod h1:BUuDKLC0P9Y098K24k+cOcy4/6rCuiHX8T2vPQGHAQA= github.com/tidwall/btree v1.6.0 h1:LDZfKfQIBHGHWSwckhXI0RPSXzlo+KYdjK7FWSqOzzg= github.com/tidwall/btree v1.6.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= diff --git a/health/healthcheck.go b/health/healthcheck.go index 8dda6a2..850a631 100644 --- a/health/healthcheck.go +++ b/health/healthcheck.go @@ -6,7 +6,7 @@ import ( "fmt" "net/http" - "github.com/restake-go/log" + "github.com/tessellated-io/restake-go/log" ) type PingType string @@ -34,23 +34,25 @@ func NewHealthCheckClient(network, uuid string, log *log.Logger) *HealthCheckCli } } -func (hm *HealthCheckClient) Start(message string) bool { - hm.log.Info().Str("network", hm.network).Msg("🩺 Starting health") - return hm.ping(Start, message) +func (hm *HealthCheckClient) Start() error { + hm.log.Info().Str("network", hm.network).Msg("🏥 Starting health") + + pingMessage := fmt.Sprintf("🏥 Starting health on %s", hm.network) + return hm.ping(Start, pingMessage) } -func (hm *HealthCheckClient) Success(message string) bool { +func (hm *HealthCheckClient) Success(message string) error { hm.log.Info().Str("network", hm.network).Msg("❤️ Health success") return hm.ping(Success, message) } -func (hm *HealthCheckClient) Failed(message string) bool { - hm.log.Info().Str("network", hm.network).Msg("\u200d Health failed") +func (hm *HealthCheckClient) Failed(err error) error { + hm.log.Error().Err(err).Str("network", hm.network).Msg("\u200d Health failed") - return hm.ping(Fail, message) + return hm.ping(Fail, err.Error()) } -func (hm *HealthCheckClient) ping(ptype PingType, message string) bool { +func (hm *HealthCheckClient) ping(ptype PingType, message string) error { url := fmt.Sprintf("https://hc-ping.com/%s", hm.uuid) if ptype == Fail || ptype == Start { url = fmt.Sprintf("https://hc-ping.com/%s/%s", hm.uuid, ptype) @@ -62,19 +64,22 @@ func (hm *HealthCheckClient) ping(ptype PingType, message string) bool { jsonData, err := json.Marshal(data) if err != nil { - panic(fmt.Errorf("failed to marshal JSON data: %s", err)) + hm.log.Error().Err(err).Msg("failed to marshal JSON data") + return err } resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonData)) if err != nil { - panic(fmt.Errorf("failed to post data: %s", err)) + hm.log.Error().Err(err).Msg("failed to post data") + return err } defer resp.Body.Close() if resp.StatusCode == 200 { - return true + return nil } else { - hm.log.Error().Str("network", hm.network).Str("ping type", string(ptype)).Int("response code", resp.StatusCode).Msg("\u200d Health failed") - return false + err := fmt.Errorf("non-200 response code from health: %d", resp.StatusCode) + hm.log.Error().Err(err).Str("network", hm.network).Str("ping type", string(ptype)).Int("response code", resp.StatusCode).Msg("\u200d Health failed") + return err } } diff --git a/log/logger.go b/log/logger.go index dc46b33..282f189 100644 --- a/log/logger.go +++ b/log/logger.go @@ -9,6 +9,8 @@ import ( "github.com/rs/zerolog" ) +// TODO: Move to pickaxe + // This package implements a hierarchical logger which allows adding prefixes. type Logger struct { diff --git a/main.go b/main.go index 9147f7a..78e13d9 100644 --- a/main.go +++ b/main.go @@ -1,10 +1,29 @@ -/* -Copyright © 2023 Tessellated -*/ package main -import "github.com/restake-go/cmd" +import ( + "fmt" + "regexp" +) + +// TODO: Reap this file func main() { - cmd.Execute() + str1 := "provided fee < minimum global fee (96826250658418aevmos < 7746100000000000aevmos). Please increase the gas price.: insufficient fee" + extract(str1) + + str2 := "provided fee < minimum global fee (96826250658418aevmos < 7746100000000000aevmos). Please increase the gas price.: insufficient fee" + extract(str2) +} + +func extract(str string) { + // Regular expression to match the desired number + pattern := `(\d+)\w+\)\. Please increase` + re := regexp.MustCompile(pattern) + + matches := re.FindStringSubmatch(str) + if len(matches) > 1 { + fmt.Println(matches[1]) // This will print 7746100000000000 + } else { + fmt.Println("No match found") + } } diff --git a/registry/registry_client.go b/registry/registry_client.go index da40d0d..9144331 100644 --- a/registry/registry_client.go +++ b/registry/registry_client.go @@ -1,8 +1,11 @@ package registry import ( + "context" + "errors" "fmt" "io" + "log" "net/http" "strings" "time" @@ -23,21 +26,24 @@ func NewRegistryClient() *RegistryClient { } } -func (rc *RegistryClient) GetRestakeChains(targetValidator string) ([]Chain, error) { +func (rc *RegistryClient) GetRestakeChains(ctx context.Context, targetValidator string) ([]Chain, error) { var chains []Chain var err error err = retry.Do(func() error { - chains, err = rc.getRestakeChains(targetValidator) + chains, err = rc.getRestakeChains(ctx, targetValidator) return err - }, rc.delay, rc.attempts) + }, rc.delay, rc.attempts, retry.Context(ctx)) + if err != nil { + err = errors.Unwrap(err) + } return chains, err } // Internal method without retries -func (rc *RegistryClient) getRestakeChains(targetValidator string) ([]Chain, error) { - validators, err := rc.getValidatorsWithRetries() +func (rc *RegistryClient) getRestakeChains(ctx context.Context, targetValidator string) ([]Chain, error) { + validators, err := rc.getValidatorsWithRetries(ctx) if err != nil { return nil, err } @@ -54,22 +60,25 @@ func (rc *RegistryClient) getRestakeChains(targetValidator string) ([]Chain, err return validChains, nil } -func (rc *RegistryClient) GetChainInfo(chainName string) (*ChainInfo, error) { +func (rc *RegistryClient) GetChainInfo(ctx context.Context, chainName string) (*ChainInfo, error) { var chainInfo *ChainInfo var err error err = retry.Do(func() error { - chainInfo, err = rc.getChainInfo(chainName) + chainInfo, err = rc.getChainInfo(ctx, chainName) return err - }, rc.delay, rc.attempts) + }, rc.delay, rc.attempts, retry.Context(ctx)) + if err != nil { + err = errors.Unwrap(err) + } return chainInfo, err } // Internal method without retries -func (rc *RegistryClient) getChainInfo(chainName string) (*ChainInfo, error) { +func (rc *RegistryClient) getChainInfo(ctx context.Context, chainName string) (*ChainInfo, error) { url := fmt.Sprintf("https://proxy.atomscan.com/directory/%s/chain.json", chainName) - bytes, err := rc.makeRequest(url) + bytes, err := rc.makeRequest(ctx, url) if err != nil { return nil, err } @@ -90,20 +99,23 @@ func (rc *RegistryClient) extractValidator(targetValidator string, validators [] return nil, fmt.Errorf("unable to find a validator with name \"%s\"", targetValidator) } -func (rc *RegistryClient) getValidatorsWithRetries() ([]Validator, error) { +func (rc *RegistryClient) getValidatorsWithRetries(ctx context.Context) ([]Validator, error) { var validators []Validator var err error err = retry.Do(func() error { - validators, err = rc.getValidators() + validators, err = rc.getValidators(ctx) return err - }, rc.delay, rc.attempts) + }, rc.delay, rc.attempts, retry.Context(ctx)) + if err != nil { + err = errors.Unwrap(err) + } return validators, err } -func (rc *RegistryClient) getValidators() ([]Validator, error) { - bytes, err := rc.makeRequest("https://validators.cosmos.directory/") +func (rc *RegistryClient) getValidators(ctx context.Context) ([]Validator, error) { + bytes, err := rc.makeRequest(ctx, "https://validators.cosmos.directory/") if err != nil { return nil, err } @@ -115,11 +127,17 @@ func (rc *RegistryClient) getValidators() ([]Validator, error) { return response.Validators, nil } -func (rc *RegistryClient) makeRequest(url string) ([]byte, error) { - resp, err := http.Get(url) +func (rc *RegistryClient) makeRequest(ctx context.Context, url string) ([]byte, error) { + request, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, err } + + client := &http.Client{} + resp, err := client.Do(request) + if err != nil { + log.Fatalf("Error making request: %v", err) + } defer resp.Body.Close() if resp.StatusCode == http.StatusOK { diff --git a/restake/restake_manager.go b/restake/restake_manager.go index 512bc6b..4b5bb6d 100644 --- a/restake/restake_manager.go +++ b/restake/restake_manager.go @@ -4,12 +4,12 @@ import ( "context" "fmt" - "github.com/restake-go/config" - "github.com/restake-go/log" - "github.com/restake-go/rpc" - "github.com/restake-go/signer" "github.com/tessellated-io/pickaxe/arrays" "github.com/tessellated-io/pickaxe/crypto" + "github.com/tessellated-io/restake-go/config" + "github.com/tessellated-io/restake-go/log" + "github.com/tessellated-io/restake-go/rpc" + "github.com/tessellated-io/restake-go/signer" "github.com/cosmos/cosmos-sdk/codec" cdctypes "github.com/cosmos/cosmos-sdk/codec/types" @@ -86,19 +86,21 @@ type restakeTarget struct { amount sdk.Dec } -// TODO: Rip out unused rpc methods -func (r *RestakeManager) Restake(ctx context.Context) error { - startMessage := fmt.Sprintf("✨ Starting Restake on %s", r.Network()) +func (r *RestakeManager) Restake(ctx context.Context) (txHash string, err error) { + startMessage := fmt.Sprintf("♻️ Starting Restake on %s", r.Network()) r.log.Info().Msg(startMessage) // Get all delegators and grants to the bot allGrants, err := r.rpcClient.GetGrants(ctx, r.botAddress) if err != nil { - return err + return "", err } validGrants := arrays.Filter(allGrants, isValidGrant) - r.log.Info().Int("valid grants", len(validGrants)).Msg("..Found valid grants") + r.log.Info().Int("valid grants", len(validGrants)).Msg("Found valid grants") + if len(validGrants) == 0 { + return "", fmt.Errorf("no valid grants found") + } // Map to balances and then filter by rewards validDelegators := arrays.Map(validGrants, func(input *authztypes.GrantAuthorization) string { return input.Granter }) @@ -107,7 +109,7 @@ func (r *RestakeManager) Restake(ctx context.Context) error { // Fetch total rewards totalRewards, err := r.rpcClient.GetPendingRewards(ctx, validDelegator, r.validatorAddress, r.stakingToken) if err != nil { - return err + return "", err } r.log.Info().Str("delegator", validDelegator).Str("total rewards", totalRewards.String()).Str("staking token", r.stakingToken).Msg("Fetched delegation rewards") @@ -120,17 +122,17 @@ func (r *RestakeManager) Restake(ctx context.Context) error { targetsAboveMinimum := arrays.Filter(restakeTargets, func(input *restakeTarget) bool { return input.amount.GTE(r.minRewards) }) - r.log.Info().Int("valid grants", len(targetsAboveMinimum)).Msg("fetched grants above minimum") + r.log.Info().Int("grants_above_min", len(targetsAboveMinimum)).Str("minimum", r.minRewards.String()).Msg("fetched grants above minimum") // Restake all delegators if len(targetsAboveMinimum) > 0 { return r.restakeDelegators(ctx, targetsAboveMinimum) } - return fmt.Errorf("no valid grants found") + return "", fmt.Errorf("no grants above minimum found") } -func (r *RestakeManager) restakeDelegators(ctx context.Context, targets []*restakeTarget) error { +func (r *RestakeManager) restakeDelegators(ctx context.Context, targets []*restakeTarget) (txHash string, err error) { delegateMsgs := []sdk.Msg{} for _, target := range targets { // Form our messages diff --git a/rpc/rpc-client-impl.go b/rpc/rpc-client-impl.go index 724d120..357afff 100644 --- a/rpc/rpc-client-impl.go +++ b/rpc/rpc-client-impl.go @@ -2,15 +2,16 @@ package rpc import ( "context" + "errors" "fmt" "math" "strings" "time" retry "github.com/avast/retry-go/v4" - "github.com/restake-go/log" "github.com/tessellated-io/pickaxe/arrays" "github.com/tessellated-io/pickaxe/grpc" + "github.com/tessellated-io/restake-go/log" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/codec" @@ -89,13 +90,18 @@ func (r *rpcClientImpl) GetPendingRewards(ctx context.Context, delegator, valida err = retry.Do(func() error { chainInfo, err = r.getPendingRewards(ctx, delegator, validator, stakingDenom) return err - }, r.delay, r.attempts) + }, r.delay, r.attempts, retry.Context(ctx)) + if err != nil { + err = errors.Unwrap(err) + } return chainInfo, err } // private function with retries func (r *rpcClientImpl) getPendingRewards(ctx context.Context, delegator, validator, stakingDenom string) (sdk.Dec, error) { + r.log.Info().Str("delegator", delegator).Str("validator", validator).Msg("searching for pending rewards") + request := &distributiontypes.QueryDelegationTotalRewardsRequest{ DelegatorAddress: delegator, } @@ -106,20 +112,29 @@ func (r *rpcClientImpl) getPendingRewards(ctx context.Context, delegator, valida } for _, reward := range response.Rewards { + r.log.Info().Str("delegator", delegator).Str("examining_validator", reward.ValidatorAddress).Msg("searching for target denom") if strings.EqualFold(validator, reward.ValidatorAddress) { + r.log.Info().Str("delegator", delegator).Msg("found validator") + for _, coin := range reward.Reward { + r.log.Info().Str("target", delegator).Str("examining_denom", coin.Denom).Str("staking_denom", stakingDenom).Msg("examinging reward denom") + if strings.EqualFold(coin.Denom, stakingDenom) { + r.log.Info().Str("target", delegator).Msg("found denom") return coin.Amount, nil } + r.log.Info().Str("target", delegator).Msg("incorrect denom") + } } } - return sdk.NewDec(0), fmt.Errorf("unable to find staking reward denom %s", stakingDenom) + r.log.Info().Str("delegator", delegator).Str("validator", validator).Msg("unable to find any rewards attributable to validator") + return sdk.NewDec(0), nil } -// BroadcastTxResponse may or may not be populated in the response. -func (r *rpcClientImpl) BroadcastTxAndWait( +// Broadcast may or may not be populated in the response. +func (r *rpcClientImpl) Broadcast( ctx context.Context, txBytes []byte, ) (*txtypes.BroadcastTxResponse, error) { @@ -130,36 +145,14 @@ func (r *rpcClientImpl) BroadcastTxAndWait( } // Send tx - response, err := r.txClient.BroadcastTx( + return r.txClient.BroadcastTx( ctx, query, ) - if err != nil { - return nil, err - } - - // If successful, attempt to poll for deliver - if response.TxResponse.Code == 0 { - r.log.Info().Str("tx hash", response.TxResponse.TxHash).Msg("Transaction sent, waiting for inclusion...") - time.Sleep(30 * time.Second) - - err = retry.Do(func() error { - return r.checkConfirmed(ctx, response.TxResponse.TxHash) - }, retry.Delay(30*time.Second), retry.Attempts(10)) - - if err != nil { - return response, fmt.Errorf("transaction successfully broadcasted but was not confirmed") - } else { - return response, nil - } - - } else { - return response, fmt.Errorf("error sending transaction: %s", response.TxResponse.RawLog) - } } // Returns nil if the transaction is in a block -func (r *rpcClientImpl) checkConfirmed(ctx context.Context, txHash string) error { +func (r *rpcClientImpl) CheckConfirmed(ctx context.Context, txHash string) error { status, err := r.getTxStatus(ctx, txHash) if err != nil { r.log.Error().Err(err).Msg("Error querying tx status") @@ -167,7 +160,7 @@ func (r *rpcClientImpl) checkConfirmed(ctx context.Context, txHash string) error } else { height := status.TxResponse.Height if height != 0 { - r.log.Info().Err(err).Str("tx hash", txHash).Int64("height", height).Msg("Transaction confirmed") + r.log.Info().Err(err).Str("tx_hash", txHash).Int64("height", height).Msg("Transaction confirmed") return nil } else { r.log.Warn().Msg("Transaction still not confirmed, still waiting...") @@ -188,7 +181,10 @@ func (r *rpcClientImpl) GetAccountData(ctx context.Context, address string) (*Ac err = retry.Do(func() error { accountData, err = r.getAccountData(ctx, address) return err - }, r.delay, r.attempts) + }, r.delay, r.attempts, retry.Context(ctx)) + if err != nil { + err = errors.Unwrap(err) + } return accountData, err } @@ -230,7 +226,10 @@ func (r *rpcClientImpl) SimulateTx( err = retry.Do(func() error { simulationResult, err = r.simulateTx(ctx, tx, txConfig, gasFactor) return err - }, r.delay, r.attempts) + }, r.delay, r.attempts, retry.Context(ctx)) + if err != nil { + err = errors.Unwrap(err) + } return simulationResult, err } @@ -269,7 +268,10 @@ func (r *rpcClientImpl) GetGrants(ctx context.Context, botAddress string) ([]*au err = retry.Do(func() error { grants, err = r.getGrants(ctx, botAddress) return err - }, r.delay, r.attempts) + }, r.delay, r.attempts, retry.Context(ctx)) + if err != nil { + err = errors.Unwrap(err) + } return grants, err } @@ -316,7 +318,10 @@ func (r *rpcClientImpl) GetDelegators(ctx context.Context, validatorAddress stri err = retry.Do(func() error { delegators, err = r.getDelegators(ctx, validatorAddress) return err - }, r.delay, r.attempts) + }, r.delay, r.attempts, retry.Context(ctx)) + if err != nil { + err = errors.Unwrap(err) + } return delegators, err } @@ -378,10 +383,13 @@ func retrievePaginatedData[DataType any]( err = retry.Do(func() error { rpcResponse, err = retrievePageFn(ctx, nextKey) if err != nil { - return nil + return err } return nil - }, r.delay, r.attempts) + }, r.delay, r.attempts, retry.Context(ctx)) + if err != nil { + err = errors.Unwrap(err) + } if err != nil { return nil, err diff --git a/rpc/rpc-client.go b/rpc/rpc-client.go index dfa18e8..aed288f 100644 --- a/rpc/rpc-client.go +++ b/rpc/rpc-client.go @@ -12,7 +12,8 @@ import ( // Handles RPCs for Restake type RpcClient interface { - BroadcastTxAndWait(ctx context.Context, txBytes []byte) (*txtypes.BroadcastTxResponse, error) + Broadcast(ctx context.Context, txBytes []byte) (*txtypes.BroadcastTxResponse, error) + CheckConfirmed(ctx context.Context, txHash string) error SimulateTx(ctx context.Context, tx authsigning.Tx, txConfig client.TxConfig, gasFactor float64) (*SimulationResult, error) diff --git a/signer/signer.go b/signer/signer.go index 2ebdf57..770052f 100644 --- a/signer/signer.go +++ b/signer/signer.go @@ -7,9 +7,9 @@ import ( "strconv" "time" - "github.com/restake-go/log" - "github.com/restake-go/rpc" "github.com/tessellated-io/pickaxe/crypto" + "github.com/tessellated-io/restake-go/log" + "github.com/tessellated-io/restake-go/rpc" "github.com/cosmos/cosmos-sdk/client" cosmostx "github.com/cosmos/cosmos-sdk/client/tx" @@ -21,9 +21,6 @@ import ( txauth "github.com/cosmos/cosmos-sdk/x/auth/tx" ) -// TODO: Support increasing fees - -// TODO: Determine what this should be? const feeIncrement float64 = 0.01 type Signer struct { @@ -78,50 +75,118 @@ func NewSigner( func (s *Signer) SendMessages( ctx context.Context, msgs []sdk.Msg, -) error { +) (string, error) { var err error - // Try to send a few times + // Attempt to send a transaction a few times. for i := 0; i < 5; i++ { - // TODO: if the tx broadcast but did not confirm, let's increase gas then try again. otherwise, try again. - // TODO: After fixing above, make sure that we're not throwing retries into the rpc client - // TODO: Clean up this text - // TODO: mess with these return codes from RPC Client + // Attempt to broadcast var result *txtypes.BroadcastTxResponse var gasWanted uint64 result, gasWanted, err = s.sendMessages(ctx, msgs) + + // Compose for logging + gasPrice := fmt.Sprintf("%f%s", s.gasPrice, s.feeDenom) + + // Give up if we get an error attempting to broadcast. if err != nil { - s.log.Error().Err(err).Msg("Error broadcasting transaction") + s.log.Error().Err(err).Str("gas_price", gasPrice).Float64("gas_factor", s.gasFactor).Msg("Error broadcasting transaction") continue } + // Extract codes code := result.TxResponse.Code logs := result.TxResponse.RawLog - if code == 13 { + + if code == 0 { + // Code 0 indicates a successful broadcast. + txHash := result.TxResponse.TxHash + + // Wait for the transaction to hit the chain. + pollDelay := 30 * time.Second + s.log.Info().Str("tx_hash", txHash).Str("gas_price", gasPrice).Float64("gas_factor", s.gasFactor).Msg("Transaction sent, waiting for inclusion...") + time.Sleep(pollDelay) + + // 1. Try to get a confirmation on the first try. If not, increass the gas. + // Check that it confirmed. + // If it failed, that likely means we should use more gas. + // TODO: extract the gas failure function + err := s.rpcClient.CheckConfirmed(ctx, txHash) + if err == nil { + // Hurrah, things worked out! + s.log.Info().Str("tx_hash", txHash).Str("gas_price", gasPrice).Float64("gas_factor", s.gasFactor).Msg("Transaction confirmed. Success.") + return txHash, nil + } + + // 2a. If the tx broadcast did not error, but it hasn't landed, then we can likely affor more in gas. + s.gasPrice += feeIncrement + newGasPrice := fmt.Sprintf("%f%s", s.gasPrice, s.feeDenom) + s.log.Info().Str("new_gas_price", newGasPrice).Float64("gas_factor", s.gasFactor).Msg(fmt.Sprintf("Transaction broadcasted but failed to confirm. Likely need more gas. Increasing gas price. Code: %d, Logs: %s", code, logs)) + + // Failing for gas seems silly, so let's go ahead and retry. + i-- + + // 3. Eventually it might show up though, so to prevent nonce conflicts go ahead and search + maxPollAttempts := 10 + for j := 0; j < maxPollAttempts; j++ { + // Sleep and wait + time.Sleep(pollDelay) + s.log.Info().Str("tx_hash", txHash).Str("gas_price", gasPrice).Float64("gas_factor", s.gasFactor).Int("attempt", j).Int("max_attempts", maxPollAttempts).Msg("still waiting for tx to land") + + // Re-poll + err = s.rpcClient.CheckConfirmed(ctx, txHash) + if err == nil { + // TODO: Dedupe with above + // Hurrah, things worked out! + s.log.Info().Str("tx_hash", txHash).Str("gas_price", gasPrice).Float64("gas_factor", s.gasFactor).Msg("Transaction confirmed. Success.") + return txHash, nil + } + } + } else if code == 13 { + // Code 13 indicates too little gas + + // First, figure out if the network told us the minimum fee maybeNewMinFee, err := s.extractMinGlobalFee(logs) if err == nil { - s.log.Info().Msg("Adjusting gas price due to Evmos/EVM error") + s.gasPrice += feeIncrement s.gasPrice = float64(maybeNewMinFee/int(gasWanted)) + 1 + newGasPrice := fmt.Sprintf("%f%s", s.gasPrice, s.feeDenom) + s.log.Info().Str("new_gas_price", newGasPrice).Float64("gas_factor", s.gasFactor).Msg("Adjusting gas price due to a minimum global fee error") continue } else { - s.log.Info().Msg(fmt.Sprintf("Need more gas, increasing gas price. Code: %d, Logs: %s", code, logs)) + // Otherwise, use normal increment logic. s.gasPrice += feeIncrement - time.Sleep(30 * time.Second) - continue + newGasPrice := fmt.Sprintf("%f%s", s.gasPrice, s.feeDenom) + s.log.Info().Str("new_gas_price", newGasPrice).Float64("gas_factor", s.gasFactor).Msg(fmt.Sprintf("Transaction failed to broadcast with gas error. Likely need more gas. Increasing gas price. Code: %d, Logs: %s", code, logs)) } + + // Failing for gas seems silly, so let's go ahead and retry. + i-- } else if code != 0 { - s.log.Info().Msg(fmt.Sprintf("Failed to apply transaction batch after broadcast. Code %d, Logs: %s", code, logs)) - time.Sleep(30 * time.Second) + s.log.Info().Str("gas_price", gasPrice).Float64("gas_factor", s.gasFactor).Msg(fmt.Sprintf("Failed to apply transaction batch after broadcast. Code %d, Logs: %s", code, logs)) + time.Sleep(5 * time.Second) continue } + } - hash := result.TxResponse.TxHash - s.log.Info().Str("tx hash", hash).Msg("Transaction sent and included in block") - return nil + // Log that we're giving up and what price we gave up at. + gasPrice := fmt.Sprintf("%f%s", s.gasPrice, s.feeDenom) + s.log.Info().Str("gas_price", gasPrice).Float64("gas_factor", s.gasFactor).Msg("failed in all attempts to broadcast transaction") + + if err != nil { + // All tries exhausted, give up and return an error. + return "", fmt.Errorf("error broadcasting tx: %s", err.Error()) + } else { + // Broadcasted but could not confirm + return "", fmt.Errorf("tx broadcasted but was never confirmed. need higher gas prices?") } - return fmt.Errorf("error broadcasting tx: %s", err.Error()) } +// Try to broadcast a transaction containing the given messages. +// Returns: +// - a broadcast tx response if it was broadcasted successfully +// - a gas estimate if one was able to be made +// - an error if broadcasting couldn't occur func (s *Signer) sendMessages( ctx context.Context, msgs []sdk.Msg, @@ -178,8 +243,8 @@ func (s *Signer) sendMessages( panic(err) } - result, err := s.rpcClient.BroadcastTxAndWait(ctx, signedTx) - return result, simulationResult.GasRecommendation, err + response, err := s.rpcClient.Broadcast(ctx, signedTx) + return response, simulationResult.GasRecommendation, err } func (s *Signer) signTx( @@ -229,18 +294,19 @@ func (s *Signer) signTx( // extractMinGlobalFee is useful for evmos, or other EVMs in the Tendermint space func (s *Signer) extractMinGlobalFee(errMsg string) (int, error) { - pattern := `provided fee < minimum global fee \((\d+)aevmos < (\d+)aevmos\). Please increase the gas price.: insufficient fee` + // Regular expression to match the desired number + pattern := `(\d+)\w+\)\. Please increase` re := regexp.MustCompile(pattern) matches := re.FindStringSubmatch(errMsg) - if len(matches) == 0 && len(matches) > 2 { - converted, err := strconv.Atoi(matches[2]) + if len(matches) > 1 { + converted, err := strconv.Atoi(matches[1]) if err != nil { - s.log.Error().Err(err).Msg("Found a matching eth / evmos error, but failed to atoi it") + s.log.Error().Err(err).Msg("Found a matching min global fee error, but failed to atoi it") return 0, nil } return converted, nil - } + } return 0, fmt.Errorf("unrecognized error format") }