diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f92e75..0c7f3c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,12 @@ Ref: https://keepachangelog.com/en/1.0.0/ # Changelog +## Unreleased + +### Features + +- [#26](https://github.com/MalteHerrmann/upgrade-local-node-go/pull/26) Enable just depositing with the binary + ## [v0.3.0](https://github.com/MalteHerrmann/upgrade-local-node-go/releases/tag/v0.3.0) - 2023-08-30 ### Features diff --git a/go.mod b/go.mod index f0fa956..7d201fb 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module github.com/MalteHerrmann/upgrade-local-node-go go 1.21 require ( - github.com/cometbft/cometbft v0.37.2 github.com/cosmos/cosmos-sdk v0.47.4 github.com/evmos/evmos/v14 v14.0.0-rc3 github.com/pkg/errors v0.9.1 @@ -43,6 +42,7 @@ require ( github.com/chzyer/readline v1.5.1 // indirect github.com/cockroachdb/apd/v2 v2.0.2 // indirect github.com/coinbase/rosetta-sdk-go/types v1.0.0 // indirect + github.com/cometbft/cometbft v0.37.2 // indirect github.com/cometbft/cometbft-db v0.8.0 // indirect github.com/confio/ics23/go v0.9.0 // indirect github.com/cosmos/btcutil v1.0.5 // indirect diff --git a/gov/deposit.go b/gov/deposit.go new file mode 100644 index 0000000..1a519ea --- /dev/null +++ b/gov/deposit.go @@ -0,0 +1,68 @@ +package gov + +import ( + "fmt" + "regexp" + "strconv" + + "github.com/MalteHerrmann/upgrade-local-node-go/utils" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/pkg/errors" +) + +// DepositForProposal deposits the given amount for the proposal with the given proposalID +// from the given account. +func DepositForProposal(bin *utils.Binary, proposalID int, sender, deposit string) (string, error) { + out, err := utils.ExecuteBinaryCmd(bin, utils.BinaryCmdArgs{ + Subcommand: []string{ + "tx", "gov", "deposit", strconv.Itoa(proposalID), deposit, + }, + From: sender, + UseDefaults: true, + Quiet: true, + }) + if err != nil { + return out, errors.Wrap(err, fmt.Sprintf("failed to deposit for proposal %d", proposalID)) + } + + return out, nil +} + +// GetMinDeposit returns the minimum deposit necessary for a proposal from the governance parameters of +// the running chain. +func GetMinDeposit(bin *utils.Binary) (sdk.Coins, error) { + out, err := utils.ExecuteBinaryCmd(bin, utils.BinaryCmdArgs{ + Subcommand: []string{"q", "gov", "param", "deposit", "--output=json"}, + Quiet: true, + }) + if err != nil { + return sdk.Coins{}, errors.Wrap(err, "failed to query governance parameters") + } + + return ParseMinDepositFromResponse(out) +} + +// ParseMinDepositFromResponse parses the minimum deposit from the given output of the governance +// parameters query. +// +// FIXME: It wasn't possible to unmarshal the JSON output of the query because of a missing unit in the max_deposit_period +// parameter. This should rather be done using GRPC. +func ParseMinDepositFromResponse(out string) (sdk.Coins, error) { + // FIXME: This is a workaround for the missing unit in the max_deposit_period parameter. Should be done with gRPC. + depositPatternRaw := `min_deposit":\[{"denom":"(\w+)","amount":"(\d+)` + depositPattern := regexp.MustCompile(depositPatternRaw) + + minDepositMatch := depositPattern.FindStringSubmatch(out) + if len(minDepositMatch) == 0 { + return sdk.Coins{}, fmt.Errorf("failed to find min deposit in params output: %q", out) + } + + minDepositDenom := minDepositMatch[1] + + minDepositAmount, err := strconv.Atoi(minDepositMatch[2]) + if err != nil { + return sdk.Coins{}, fmt.Errorf("failed to find min deposit in params output: %q", out) + } + + return sdk.Coins{sdk.NewInt64Coin(minDepositDenom, int64(minDepositAmount))}, nil +} diff --git a/gov/deposit_test.go b/gov/deposit_test.go new file mode 100644 index 0000000..88ecd3d --- /dev/null +++ b/gov/deposit_test.go @@ -0,0 +1,49 @@ +package gov_test + +import ( + "testing" + + "github.com/MalteHerrmann/upgrade-local-node-go/gov" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" +) + +func TestParseMinDepositFromResponse(t *testing.T) { + t.Parallel() + + testcases := []struct { + name string + out string + expMinDeposit sdk.Coins + expError bool + errContains string + }{ + { + name: "pass", + out: `{"min_deposit":[{"denom":"aevmos","amount":"10000000"}],"max_deposit_period":"30000000000"}`, + expMinDeposit: sdk.Coins{sdk.NewInt64Coin("aevmos", 10000000)}, + }, + { + name: "fail - no min deposit", + out: "invalid output", + expError: true, + errContains: "failed to find min deposit in params output", + }, + } + + for _, tc := range testcases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + minDeposit, err := gov.ParseMinDepositFromResponse(tc.out) + if tc.expError { + require.Error(t, err, "expected error parsing min deposit") + require.ErrorContains(t, err, tc.errContains, "expected different error") + } else { + require.NoError(t, err, "unexpected error parsing min deposit") + require.Equal(t, tc.expMinDeposit, minDeposit, "expected different min deposit") + } + }) + } +} diff --git a/gov/proposal.go b/gov/proposal.go index afe3307..5dfbcf2 100644 --- a/gov/proposal.go +++ b/gov/proposal.go @@ -2,7 +2,6 @@ package gov import ( "fmt" - "log" "strconv" "strings" @@ -18,7 +17,7 @@ func buildUpgradeProposalCommand(targetVersion string, upgradeHeight int) []stri "tx", "gov", "submit-legacy-proposal", "software-upgrade", targetVersion, "--title", fmt.Sprintf("'Upgrade to %s'", targetVersion), "--description", fmt.Sprintf("'Upgrade to %s'", targetVersion), - "--upgrade-height", fmt.Sprintf("%d", upgradeHeight), + "--upgrade-height", strconv.Itoa(upgradeHeight), "--deposit", "100000000000000000000aevmos", "--output", "json", "--no-validate", @@ -78,42 +77,6 @@ func QueryLatestProposalID(bin *utils.Binary) (int, error) { return int(res.Proposals[len(res.Proposals)-1].Id), nil } -// SubmitAllVotesForProposal submits a vote for the given proposal ID using all testing accounts. -func SubmitAllVotesForProposal(bin *utils.Binary, proposalID int) error { - accsWithDelegations, err := utils.FilterAccountsWithDelegations(bin) - if err != nil { - return errors.Wrap(err, "Error filtering accounts") - } - - if len(accsWithDelegations) == 0 { - return errors.New("No accounts with delegations found") - } - - utils.Wait(1) - log.Printf("Voting for proposal %d...\n", proposalID) - - var out string - - for _, acc := range accsWithDelegations { - out, err = VoteForProposal(bin, proposalID, acc.Name) - if err != nil { - if strings.Contains(out, fmt.Sprintf("%d: unknown proposal", proposalID)) { - return fmt.Errorf("no proposal with ID %d found", proposalID) - } - - if strings.Contains(out, fmt.Sprintf("%d: inactive proposal", proposalID)) { - return fmt.Errorf("proposal with ID %d is inactive", proposalID) - } - - log.Printf(" - could NOT vote using key: %s\n", acc.Name) - } else { - log.Printf(" - voted using key: %s\n", acc.Name) - } - } - - return nil -} - // SubmitUpgradeProposal submits a software upgrade proposal with the given target version and upgrade height. func SubmitUpgradeProposal(bin *utils.Binary, targetVersion string, upgradeHeight int) (int, error) { upgradeProposal := buildUpgradeProposalCommand(targetVersion, upgradeHeight) @@ -145,18 +108,3 @@ func SubmitUpgradeProposal(bin *utils.Binary, targetVersion string, upgradeHeigh return GetProposalIDFromSubmitEvents(events) } - -// VoteForProposal votes for the proposal with the given ID using the given account. -func VoteForProposal(bin *utils.Binary, proposalID int, sender string) (string, error) { - out, err := utils.ExecuteBinaryCmd(bin, utils.BinaryCmdArgs{ - Subcommand: []string{"tx", "gov", "vote", fmt.Sprintf("%d", proposalID), "yes"}, - From: sender, - UseDefaults: true, - Quiet: true, - }) - if err != nil { - return out, errors.Wrap(err, fmt.Sprintf("failed to vote for proposal %d", proposalID)) - } - - return out, nil -} diff --git a/gov/vote.go b/gov/vote.go new file mode 100644 index 0000000..b7e5310 --- /dev/null +++ b/gov/vote.go @@ -0,0 +1,62 @@ +package gov + +import ( + "fmt" + "log" + "strconv" + "strings" + + "github.com/MalteHerrmann/upgrade-local-node-go/utils" + "github.com/pkg/errors" +) + +// SubmitAllVotesForProposal submits a vote for the given proposal ID using all testing accounts. +func SubmitAllVotesForProposal(bin *utils.Binary, proposalID int) error { + accsWithDelegations, err := utils.FilterAccountsWithDelegations(bin) + if err != nil { + return errors.Wrap(err, "error filtering accounts") + } + + if len(accsWithDelegations) == 0 { + return errors.New("no accounts with delegations found") + } + + utils.Wait(1) + log.Printf("Voting for proposal %d...\n", proposalID) + + var out string + + for _, acc := range accsWithDelegations { + out, err = VoteForProposal(bin, proposalID, acc.Name) + if err != nil { + if strings.Contains(out, fmt.Sprintf("%d: unknown proposal", proposalID)) { + return fmt.Errorf("no proposal with ID %d found", proposalID) + } + + if strings.Contains(out, fmt.Sprintf("%d: inactive proposal", proposalID)) { + return fmt.Errorf("proposal with ID %d is inactive", proposalID) + } + + log.Printf(" - could NOT vote using key: %s\n", acc.Name) + } else { + log.Printf(" - voted using key: %s\n", acc.Name) + } + } + + return nil +} + +// VoteForProposal votes for the proposal with the given ID using the given account. +func VoteForProposal(bin *utils.Binary, proposalID int, sender string) (string, error) { + out, err := utils.ExecuteBinaryCmd(bin, utils.BinaryCmdArgs{ + Subcommand: []string{"tx", "gov", "vote", strconv.Itoa(proposalID), "yes"}, + From: sender, + UseDefaults: true, + Quiet: true, + }) + if err != nil { + return out, errors.Wrap(err, fmt.Sprintf("failed to vote for proposal %d", proposalID)) + } + + return out, nil +} diff --git a/main.go b/main.go index d4103a3..b5ba5da 100644 --- a/main.go +++ b/main.go @@ -19,47 +19,62 @@ func main() { log.Printf( "Possible usages:\n" + " upgrade-local-node-go \n" + - " upgrade-local-node-go vote [proposal-id]\n", + " upgrade-local-node-go vote [proposal-id]\n" + + " upgrade-local-node-go deposit [proposal-id]\n", ) os.Exit(1) } bin, err := utils.NewEvmosTestingBinary() if err != nil { - log.Fatalf("Error creating binary: %v", err) + log.Fatalf("error creating binary: %v", err) } - err = bin.GetAccounts() - if err != nil { - log.Fatalf("Error getting accounts: %v", err) + if err = bin.GetAccounts(); err != nil { + log.Fatalf("error getting accounts: %v", err) } - //nolint:nestif // nesting complexity is fine here, will be reworked with Cobra commands anyway - if os.Args[1] == "vote" { - proposalID, err := getProposalIDForVoting(bin, os.Args) + // TODO: use with Cobra CLI + switch os.Args[1] { + case "vote": + proposalID, err := getProposalIDFromInput(bin, os.Args) + if err != nil { + log.Fatalf("error getting proposal ID: %v", err) + } + + if err = gov.SubmitAllVotesForProposal(bin, proposalID); err != nil { + log.Fatalf("error submitting votes for proposal %d: %v", proposalID, err) + } + + case "deposit": + deposit, err := gov.GetMinDeposit(bin) if err != nil { - log.Fatalf("Error getting proposal ID: %v", err) + log.Fatalf("error getting minimum deposit: %v", err) } - err = gov.SubmitAllVotesForProposal(bin, proposalID) + proposalID, err := getProposalIDFromInput(bin, os.Args) if err != nil { - log.Fatalf("Error submitting votes for proposal %d: %v", proposalID, err) + log.Fatalf("error getting proposal ID: %v", err) } - } else { + + if _, err = gov.DepositForProposal(bin, proposalID, bin.Accounts[0].Name, deposit.String()); err != nil { + log.Fatalf("error depositing for proposal %d: %v", proposalID, err) + } + + default: targetVersion := os.Args[1] if matched, _ := regexp.MatchString(`v\d+\.\d+\.\d(-rc\d+)?`, targetVersion); !matched { - log.Fatalf("Invalid target version: %s. Please use the format vX.Y.Z(-rc*).\n", targetVersion) + log.Fatalf("invalid target version: %s; please use the format vX.Y.Z(-rc*).\n", targetVersion) } - err := upgradeLocalNode(bin, targetVersion) - if err != nil { - log.Fatalf("Error upgrading local node: %v", err) + if err := upgradeLocalNode(bin, targetVersion); err != nil { + log.Fatalf("error upgrading local node: %v", err) } } } -// getProposalIDForVoting gets the proposal ID from the command line arguments. -func getProposalIDForVoting(bin *utils.Binary, args []string) (int, error) { +// getProposalIDFromInput gets the proposal ID from the command line arguments. +func getProposalIDFromInput(bin *utils.Binary, args []string) (int, error) { var ( err error proposalID int @@ -69,15 +84,15 @@ func getProposalIDForVoting(bin *utils.Binary, args []string) (int, error) { case 2: proposalID, err = gov.QueryLatestProposalID(bin) if err != nil { - return 0, errors.Wrap(err, "Error querying latest proposal ID") + return 0, errors.Wrap(err, "error querying latest proposal ID") } case 3: proposalID, err = strconv.Atoi(args[2]) if err != nil { - return 0, errors.Wrapf(err, "Error converting proposal ID %s to integer", args[2]) + return 0, errors.Wrapf(err, "error converting proposal ID %s to integer", args[2]) } default: - return 0, errors.New("Invalid number of arguments") + return 0, errors.New("invalid number of arguments") } return proposalID, nil @@ -88,7 +103,7 @@ func getProposalIDForVoting(bin *utils.Binary, args []string) (int, error) { func upgradeLocalNode(bin *utils.Binary, targetVersion string) error { currentHeight, err := utils.GetCurrentHeight(bin) if err != nil { - return errors.Wrap(err, "Error getting current height") + return errors.Wrap(err, "error getting current height") } upgradeHeight := currentHeight + deltaHeight @@ -97,14 +112,13 @@ func upgradeLocalNode(bin *utils.Binary, targetVersion string) error { proposalID, err := gov.SubmitUpgradeProposal(bin, targetVersion, upgradeHeight) if err != nil { - return errors.Wrap(err, "Error executing upgrade proposal") + return errors.Wrap(err, "error executing upgrade proposal") } log.Printf("Scheduled upgrade to %s at height %d.\n", targetVersion, upgradeHeight) - err = gov.SubmitAllVotesForProposal(bin, proposalID) - if err != nil { - return errors.Wrapf(err, "Error submitting votes for proposal %d", proposalID) + if err = gov.SubmitAllVotesForProposal(bin, proposalID); err != nil { + return errors.Wrapf(err, "error submitting votes for proposal %d", proposalID) } return nil diff --git a/utils/delegations.go b/utils/delegations.go new file mode 100644 index 0000000..8c21fd9 --- /dev/null +++ b/utils/delegations.go @@ -0,0 +1,25 @@ +package utils + +import ( + "fmt" + + "github.com/cosmos/cosmos-sdk/codec" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" +) + +// ParseDelegationsFromResponse parses the delegations from the given response. +func ParseDelegationsFromResponse(cdc *codec.ProtoCodec, out string) ([]stakingtypes.Delegation, error) { + var res stakingtypes.QueryDelegatorDelegationsResponse + + err := cdc.UnmarshalJSON([]byte(out), &res) + if err != nil { + return nil, fmt.Errorf("error unmarshalling delegations: %w", err) + } + + delegations := make([]stakingtypes.Delegation, len(res.DelegationResponses)) + for i, delegation := range res.DelegationResponses { + delegations[i] = delegation.Delegation + } + + return delegations, nil +} diff --git a/utils/delegations_test.go b/utils/delegations_test.go new file mode 100644 index 0000000..c379e7e --- /dev/null +++ b/utils/delegations_test.go @@ -0,0 +1,56 @@ +package utils_test + +import ( + "testing" + + "github.com/MalteHerrmann/upgrade-local-node-go/utils" + "github.com/stretchr/testify/require" +) + +func TestParseDelegationsFromResponse(t *testing.T) { + t.Parallel() + + cdc, ok := utils.GetCodec() + require.True(t, ok, "unexpected error getting codec") + + testcases := []struct { + name string + out string + expVals []string + expError bool + errContains string + }{ + { + name: "pass", + //nolint:lll // line length is okay here + out: `{"delegation_responses":[{"delegation":{"delegator_address":"evmos1v6jyld5mcu37d3dfe7kjrw0htkc4wu2mxn9y25","validator_address":"evmosvaloper1v6jyld5mcu37d3dfe7kjrw0htkc4wu2mta25tf","shares":"1000000000000000000000.000000000000000000"},"balance":{"denom":"aevmos","amount":"1000000000000000000000"}}],"pagination":{"next_key":null,"total":"0"}}`, + expVals: []string{"evmosvaloper1v6jyld5mcu37d3dfe7kjrw0htkc4wu2mta25tf"}, + }, + { + name: "fail - no keys", + out: "invalid output", + expError: true, + errContains: "error unmarshalling delegations", + }, + } + + for _, tc := range testcases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + delegations, err := utils.ParseDelegationsFromResponse(cdc, tc.out) + if tc.expError { + require.Error(t, err, "expected error parsing delegations") + require.ErrorContains(t, err, tc.errContains, "expected different error") + } else { + require.NoError(t, err, "unexpected error parsing delegations") + + var vals []string + for _, delegation := range delegations { + vals = append(vals, delegation.ValidatorAddress) + } + require.Equal(t, tc.expVals, vals, "expected different validators") + } + }) + } +} diff --git a/utils/keys.go b/utils/keys.go index e403a1a..61614e4 100644 --- a/utils/keys.go +++ b/utils/keys.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" - "github.com/cosmos/cosmos-sdk/codec" cryptokeyring "github.com/cosmos/cosmos-sdk/crypto/keyring" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" ) @@ -68,23 +67,6 @@ func FilterAccountsWithDelegations(bin *Binary) ([]Account, error) { return stakingAccs, nil } -// ParseDelegationsFromResponse parses the delegations from the given response. -func ParseDelegationsFromResponse(cdc *codec.ProtoCodec, out string) ([]stakingtypes.Delegation, error) { - var res stakingtypes.QueryDelegatorDelegationsResponse - - err := cdc.UnmarshalJSON([]byte(out), &res) - if err != nil { - return nil, fmt.Errorf("error unmarshalling delegations: %w", err) - } - - delegations := make([]stakingtypes.Delegation, len(res.DelegationResponses)) - for i, delegation := range res.DelegationResponses { - delegations[i] = delegation.Delegation - } - - return delegations, nil -} - // ParseAccountsFromOut parses the keys from the given output from the keys list command. func ParseAccountsFromOut(out string) ([]Account, error) { var ( diff --git a/utils/keys_test.go b/utils/keys_test.go index e4f2ebc..51f4c03 100644 --- a/utils/keys_test.go +++ b/utils/keys_test.go @@ -51,51 +51,3 @@ func TestParseKeysFromOut(t *testing.T) { }) } } - -func TestParseDelegationsFromResponse(t *testing.T) { - t.Parallel() - - cdc, ok := utils.GetCodec() - require.True(t, ok, "unexpected error getting codec") - - testcases := []struct { - name string - out string - expVals []string - expError bool - errContains string - }{ - { - name: "pass", - //nolint:lll // line length is okay here - out: `{"delegation_responses":[{"delegation":{"delegator_address":"evmos1v6jyld5mcu37d3dfe7kjrw0htkc4wu2mxn9y25","validator_address":"evmosvaloper1v6jyld5mcu37d3dfe7kjrw0htkc4wu2mta25tf","shares":"1000000000000000000000.000000000000000000"},"balance":{"denom":"aevmos","amount":"1000000000000000000000"}}],"pagination":{"next_key":null,"total":"0"}}`, - expVals: []string{"evmosvaloper1v6jyld5mcu37d3dfe7kjrw0htkc4wu2mta25tf"}, - }, - { - name: "fail - no keys", - out: "invalid output", - expError: true, - errContains: "error unmarshalling delegations", - }, - } - - for _, tc := range testcases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - delegations, err := utils.ParseDelegationsFromResponse(cdc, tc.out) - if tc.expError { - require.Error(t, err, "expected error parsing delegations") - require.ErrorContains(t, err, tc.errContains, "expected different error") - } else { - require.NoError(t, err, "unexpected error parsing delegations") - - var vals []string - for _, delegation := range delegations { - vals = append(vals, delegation.ValidatorAddress) - } - require.Equal(t, tc.expVals, vals, "expected different validators") - } - }) - } -}