diff --git a/CHANGELOG.md b/CHANGELOG.md index b04e04a..32b8ab3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,12 @@ Ref: https://keepachangelog.com/en/1.0.0/ # Changelog +## [v0.2.0](https://github.com/MalteHerrmann/upgrade-local-node-go/releases/tag/v0.2.0) - 2023-08-09 + +### Improvements + +- [#1](https://github.com/MalteHerrmann/upgrade-local-node-go/pull/1) adaptively gets keys and current proposal ID from the local node + ## [v0.1.0](https://github.com/MalteHerrmann/upgrade-local-node-go/releases/tag/v0.1.0) - 2023-08-01 ### Features @@ -42,4 +48,3 @@ Ref: https://keepachangelog.com/en/1.0.0/ - Gets current block height of local node (at `http://localhost:26657`) - Submit a software upgrade proposal to a running local Evmos node for the target version - Vote on the software proposal - \ No newline at end of file diff --git a/go.mod b/go.mod index 87fb877..0be9cf6 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,11 @@ module github.com/MalteHerrmann/upgrade-local-node-go go 1.20 + +require github.com/stretchr/testify v1.8.4 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..fa4b6e6 --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/keys.go b/keys.go new file mode 100644 index 0000000..a3665bf --- /dev/null +++ b/keys.go @@ -0,0 +1,36 @@ +package main + +import ( + "fmt" + "regexp" +) + +// getKeys returns the list of keys from the current running local node +func getKeys() ([]string, error) { + out, err := executeShellCommand([]string{"keys", "list"}, evmosdHome, "", false) + if err != nil { + return nil, err + } + + return parseKeysFromOut(out) +} + +func parseKeysFromOut(out string) ([]string, error) { + // Define the regular expression pattern + pattern := `\s+name:\s*(\w+)` + + // Compile the regular expression + re := regexp.MustCompile(pattern) + + matches := re.FindAllStringSubmatch(out, -1) + if len(matches) == 0 { + return nil, fmt.Errorf("no keys found in output") + } + + var keys []string + for _, match := range matches { + keys = append(keys, match[1]) + } + + return keys, nil +} diff --git a/keys_test.go b/keys_test.go new file mode 100644 index 0000000..dc63beb --- /dev/null +++ b/keys_test.go @@ -0,0 +1,62 @@ +package main + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestParseKeysFromOut(t *testing.T) { + testcases := []struct { + name string + out string + expKeys []string + expError bool + }{ + { + name: "pass", + out: ` - address: evmos19mx9kcksequm4m4xume5h0k9fquwgmea3yvu89 + name: dev0 + pubkey: '{"@type":"/ethermint.crypto.v1.ethsecp256k1.PubKey","key":"AmquZBW+CPcgHKx6D4YRDICzr0MNcRvl9Wm/jJn8wJxs"}' + type: local + - address: evmos18z7xfs864u49jcv6gkgajpteesjl5d7krpple6 + name: dev1 + pubkey: '{"@type":"/ethermint.crypto.v1.ethsecp256k1.PubKey","key":"AtY/rqJrmhKbXrQ02xSxq/t9JGgbP2T7HPGTZJIbuT8I"}' + type: local + - address: evmos12rrt7vcnxvhxad6gzz0vt5psdlnurtldety57n + name: dev2 + pubkey: '{"@type":"/ethermint.crypto.v1.ethsecp256k1.PubKey","key":"A544btlGjv4zB/qpWT8dQqlAHrcmgZEvrFSgJnp7Yjt4"}' + type: local + - address: evmos1dln2gjtsfd2sny6gwdxzyxcsr0uu8sh5nwajun + name: testKey1 + pubkey: '{"@type":"/ethermint.crypto.v1.ethsecp256k1.PubKey","key":"Amja5pRiVw+5vPkozo6Eo20AEbYVVBqOKBi5yP7EbxyJ"}' + type: local + - address: evmos1qdxgxz9g2la8g9eyjdq4srlpxgrmuqd6ty88zm + name: testKey2 + pubkey: '{"@type":"/ethermint.crypto.v1.ethsecp256k1.PubKey","key":"A+ytKfWmkQiW0c6iOCXSL71e4b5njmJVUd1msONsPEnA"}' + type: local + - address: evmos1hduvvhjvu0pqu7m97pajymdsupqx3us3ntey9a + name: testKey3 + pubkey: '{"@type":"/ethermint.crypto.v1.ethsecp256k1.PubKey","key":"AsdAPndEVttzhUz5iSm0/FoFxkzB0oZE7DuKf3NjzXkS"}' + type: local`, + expKeys: []string{"dev0", "dev1", "dev2", "testKey1", "testKey2", "testKey3"}, + }, + { + name: "fail - no keys", + out: "invalid output", + expError: true, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + keys, err := parseKeysFromOut(tc.out) + if tc.expError { + require.Error(t, err, "expected error parsing keys") + } else { + require.NoError(t, err, "unexpected error parsing keys") + require.Equal(t, tc.expKeys, keys) + } + }) + } +} diff --git a/main.go b/main.go index 23a6f03..c3a694a 100644 --- a/main.go +++ b/main.go @@ -17,8 +17,6 @@ const ( defaultFees int = 1e18 // 1 aevmos // The denomination used for the local node. denom = "aevmos" - // proposalID is the ID of the proposal that will be created. - proposalID = 1 ) // evmosdHome is the home directory of the local node. @@ -67,34 +65,24 @@ func upgradeLocalNode(targetVersion string) { } upgradeHeight := currentHeight + deltaHeight - upgradeProposal := buildUpgradeProposalCommand(targetVersion, upgradeHeight) - _, err = executeShellCommand(upgradeProposal, evmosdHome, "dev0", true) + fmt.Println("Submitting upgrade proposal...") + proposalID, err := submitUpgradeProposal(targetVersion, upgradeHeight) if err != nil { log.Fatalf("Error executing upgrade proposal: %v", err) } fmt.Printf("Scheduled upgrade to %s at height %d.\n", targetVersion, upgradeHeight) - wait(2) - if err = voteForProposal(proposalID, "dev0"); err != nil { - log.Fatalf("Error voting for upgrade: %v", err) - } - - wait(2) - if err = voteForProposal(proposalID, "dev1"); err != nil { - log.Fatalf("Error voting for upgrade: %v", err) + availableKeys, err := getKeys() + if err != nil { + log.Fatalf("Error getting available keys: %v", err) } - - wait(2) - if err = voteForProposal(proposalID, "dev2"); err != nil { - log.Fatalf("Error voting for upgrade: %v", err) + wait(1) + for _, key := range availableKeys { + if err = voteForProposal(proposalID, key); err != nil { + log.Fatalf("Error voting for upgrade: %v", err) + } } - fmt.Printf("Cast all votes for proposal %d.\n", proposalID) -} - -// voteForProposal votes for the proposal with the given ID using the given account. -func voteForProposal(proposalID int, sender string) error { - _, err := executeShellCommand([]string{"tx", "gov", "vote", fmt.Sprintf("%d", proposalID), "yes"}, evmosdHome, sender, true) - return err + fmt.Printf("Cast all %d 'yes' votes for proposal %d.\n", len(availableKeys), proposalID) } // wait waits for the specified amount of seconds. diff --git a/proposal.go b/proposal.go index f8d6462..80a2533 100644 --- a/proposal.go +++ b/proposal.go @@ -1,6 +1,37 @@ package main -import "fmt" +import ( + "fmt" + "regexp" + "strconv" +) + +// submitUpgradeProposal submits a software upgrade proposal with the given target version and upgrade height. +func submitUpgradeProposal(targetVersion string, upgradeHeight int) (int, error) { + upgradeProposal := buildUpgradeProposalCommand(targetVersion, upgradeHeight) + out, err := executeShellCommand(upgradeProposal, evmosdHome, "dev0", true) + if err != nil { + return 0, err + } + + return getProposalID(out) +} + +// getProposalID parses the proposal ID from the given output from submitting an upgrade proposal. +func getProposalID(out string) (int, error) { + // Define the regular expression pattern + pattern := `- key:\s*proposal_id\s*\n\s*value:\s*"([^"]+)"` + + // Compile the regular expression + re := regexp.MustCompile(pattern) + + match := re.FindStringSubmatch(out) + if len(match) != 2 { + return 0, fmt.Errorf("proposal ID not found in output") + } + + return strconv.Atoi(match[1]) +} // buildUpgradeProposalCommand builds the command to submit a software upgrade proposal. func buildUpgradeProposalCommand(targetVersion string, upgradeHeight int) []string { @@ -13,3 +44,9 @@ func buildUpgradeProposalCommand(targetVersion string, upgradeHeight int) []stri "--no-validate", } } + +// voteForProposal votes for the proposal with the given ID using the given account. +func voteForProposal(proposalID int, sender string) error { + _, err := executeShellCommand([]string{"tx", "gov", "vote", fmt.Sprintf("%d", proposalID), "yes"}, evmosdHome, sender, true) + return err +} diff --git a/proposal_test.go b/proposal_test.go new file mode 100644 index 0000000..2973859 --- /dev/null +++ b/proposal_test.go @@ -0,0 +1,65 @@ +package main + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGetProposalID(t *testing.T) { + testcases := []struct { + name string + out string + expID int + expError bool + }{ + { + name: "pass", + out: `gas estimate: 850456 + code: 0 + codespace: "" + data: 12330A2D2F636F736D6F732E676F762E763162657461312E4D73675375626D697450726F706F73616C526573706F6E736512020804 + events: + logs: + - events: + - attributes: + - key: amount + value: 1000000000000aevmos + - key: proposal_id + value: "4" + type: proposal_deposit + - attributes: + - key: proposal_id + value: "4" + - key: proposal_messages + value: ',/cosmos.gov.v1.MsgExecLegacyContent' + - key: voting_period_start + value: "4" + type: submit_proposal + type: transfer + log: "" + msg_index: 0 + timestamp: "" + tx: null + txhash: A505158FF9EFB4E939CD4A9A94F731E0E34AEEF50C7E53A723226EEF33A1A89B`, + expID: 4, + }, + { + name: "fail - no proposal ID", + out: "invalid output", + expError: true, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + id, err := getProposalID(tc.out) + if tc.expError { + require.Error(t, err, "expected error parsing proposal ID") + } else { + require.NoError(t, err, "unexpected error parsing proposal ID") + require.Equal(t, tc.expID, id, "expected different proposal ID") + } + }) + } +}