diff --git a/client/v2/autocli/flag/coin.go b/client/v2/autocli/flag/coin.go index 3cf5afc581..222f4805fc 100644 --- a/client/v2/autocli/flag/coin.go +++ b/client/v2/autocli/flag/coin.go @@ -22,8 +22,7 @@ func (c coinType) NewValue(context.Context, *Builder) Value { } func (c coinType) DefaultValue() string { - stringCoin, _ := coins.FormatCoins([]*basev1beta1.Coin{}, nil) - return stringCoin + return "zero" } func (c *coinValue) Get(protoreflect.Value) (protoreflect.Value, error) { diff --git a/core/CHANGELOG.md b/core/CHANGELOG.md index 803225487c..a01b3ca464 100644 --- a/core/CHANGELOG.md +++ b/core/CHANGELOG.md @@ -36,9 +36,15 @@ Ref: https://keepachangelog.com/en/1.0.0/ ## [Unreleased] +### Features + * [#18379](https://github.com/cosmos/cosmos-sdk/pull/18379) Add branch service. * [#18457](https://github.com/cosmos/cosmos-sdk/pull/18457) Add branch.ExecuteWithGasLimit. +### API Breaking + +* [#18857](https://github.com/cosmos/cosmos-sdk/pull/18857) Moved `FormatCoins` to `x/tx`. + ## [v0.12.0](https://github.com/cosmos/cosmos-sdk/releases/tag/core%2Fv0.11.0) :::note diff --git a/core/coins/format.go b/core/coins/format.go index 315c96d78f..1c84fe9bea 100644 --- a/core/coins/format.go +++ b/core/coins/format.go @@ -3,104 +3,16 @@ package coins import ( "fmt" "regexp" - "sort" "strings" - bankv1beta1 "cosmossdk.io/api/cosmos/bank/v1beta1" basev1beta1 "cosmossdk.io/api/cosmos/base/v1beta1" - "cosmossdk.io/math" ) -const emptyCoins = "zero" - // Amount can be a whole number or a decimal number. Denominations can be 3 ~ 128 // characters long and support letters, followed by either a letter, a number or // a separator ('/', ':', '.', '_' or '-'). var coinRegex = regexp.MustCompile(`^(\d+(\.\d+)?)([a-zA-Z][a-zA-Z0-9\/\:\._\-]{2,127})$`) -// formatCoin formats a sdk.Coin into a value-rendered string, using the -// given metadata about the denom. It returns the formatted coin string, the -// display denom, and an optional error. -func formatCoin(coin *basev1beta1.Coin, metadata *bankv1beta1.Metadata) (string, error) { - coinDenom := coin.Denom - - // Return early if no display denom or display denom is the current coin denom. - if metadata == nil || metadata.Display == "" || coinDenom == metadata.Display { - vr, err := math.FormatDec(coin.Amount) - return vr + " " + coin.Denom, err - } - - dispDenom := metadata.Display - - // Find exponents of both denoms. - var coinExp, dispExp uint32 - foundCoinExp, foundDispExp := false, false - for _, unit := range metadata.DenomUnits { - if coinDenom == unit.Denom { - coinExp = unit.Exponent - foundCoinExp = true - } - if dispDenom == unit.Denom { - dispExp = unit.Exponent - foundDispExp = true - } - } - - // If we didn't find either exponent, then we return early. - if !foundCoinExp || !foundDispExp { - vr, err := math.FormatInt(coin.Amount) - return vr + " " + coin.Denom, err - } - - dispAmount, err := math.LegacyNewDecFromStr(coin.Amount) - if err != nil { - return "", err - } - - if coinExp > dispExp { - dispAmount = dispAmount.Mul(math.LegacyNewDec(10).Power(uint64(coinExp - dispExp))) - } else { - dispAmount = dispAmount.Quo(math.LegacyNewDec(10).Power(uint64(dispExp - coinExp))) - } - - vr, err := math.FormatDec(dispAmount.String()) - return vr + " " + dispDenom, err -} - -// FormatCoins formats Coins into a value-rendered string, which uses -// `formatCoin` separated by ", " (a comma and a space), and sorted -// alphabetically by value-rendered denoms. It expects an array of metadata -// (optionally nil), where each metadata at index `i` MUST match the coin denom -// at the same index. -func FormatCoins(coins []*basev1beta1.Coin, metadata []*bankv1beta1.Metadata) (string, error) { - if len(coins) != len(metadata) { - return "", fmt.Errorf("formatCoins expect one metadata for each coin; expected %d, got %d", len(coins), len(metadata)) - } - - formatted := make([]string, len(coins)) - for i, coin := range coins { - var err error - formatted[i], err = formatCoin(coin, metadata[i]) - if err != nil { - return "", err - } - } - - if len(coins) == 0 { - return emptyCoins, nil - } - - // Sort the formatted coins by display denom. - sort.SliceStable(formatted, func(i, j int) bool { - denomI := strings.Split(formatted[i], " ")[1] - denomJ := strings.Split(formatted[j], " ")[1] - - return denomI < denomJ - }) - - return strings.Join(formatted, ", "), nil -} - // ParseCoin parses a coin from a string. The string must be in the format // , where is a number and is a valid denom. func ParseCoin(input string) (*basev1beta1.Coin, error) { diff --git a/core/coins/format_test.go b/core/coins/format_test.go index 994be1a829..c9804c202e 100644 --- a/core/coins/format_test.go +++ b/core/coins/format_test.go @@ -1,86 +1,13 @@ package coins_test import ( - "encoding/json" - "os" "testing" "github.com/stretchr/testify/require" - bankv1beta1 "cosmossdk.io/api/cosmos/bank/v1beta1" - basev1beta1 "cosmossdk.io/api/cosmos/base/v1beta1" "cosmossdk.io/core/coins" ) -// coinsJsonTest is the type of test cases in the coin.json file. -type coinJSONTest struct { - Proto *basev1beta1.Coin - Metadata *bankv1beta1.Metadata - Text string - Error bool -} - -// coinsJSONTest is the type of test cases in the coins.json file. -type coinsJSONTest struct { - Proto []*basev1beta1.Coin - Metadata map[string]*bankv1beta1.Metadata - Text string - Error bool -} - -func TestFormatCoin(t *testing.T) { - var testcases []coinJSONTest - raw, err := os.ReadFile("../../x/tx/signing/textual/internal/testdata/coin.json") - require.NoError(t, err) - err = json.Unmarshal(raw, &testcases) - require.NoError(t, err) - - for _, tc := range testcases { - t.Run(tc.Text, func(t *testing.T) { - if tc.Proto != nil { - out, err := coins.FormatCoins([]*basev1beta1.Coin{tc.Proto}, []*bankv1beta1.Metadata{tc.Metadata}) - - if tc.Error { - require.Error(t, err) - return - } - - require.NoError(t, err) - require.Equal(t, tc.Text, out) - } - }) - } -} - -func TestFormatCoins(t *testing.T) { - var testcases []coinsJSONTest - raw, err := os.ReadFile("../../x/tx/signing/textual/internal/testdata/coins.json") - require.NoError(t, err) - err = json.Unmarshal(raw, &testcases) - require.NoError(t, err) - - for _, tc := range testcases { - t.Run(tc.Text, func(t *testing.T) { - if tc.Proto != nil { - metadata := make([]*bankv1beta1.Metadata, len(tc.Proto)) - for i, coin := range tc.Proto { - metadata[i] = tc.Metadata[coin.Denom] - } - - out, err := coins.FormatCoins(tc.Proto, metadata) - - if tc.Error { - require.Error(t, err) - return - } - - require.NoError(t, err) - require.Equal(t, tc.Text, out) - } - }) - } -} - func TestDecodeCoin(t *testing.T) { encodedCoin := "1000000000foo" coin, err := coins.ParseCoin(encodedCoin) diff --git a/x/tx/CHANGELOG.md b/x/tx/CHANGELOG.md index ccfd28117c..a001f957cc 100644 --- a/x/tx/CHANGELOG.md +++ b/x/tx/CHANGELOG.md @@ -29,6 +29,12 @@ Ref: https://keepachangelog.com/en/1.0.0/ # Changelog +## [Unreleased] + +### Improvements + +* [#18857](https://github.com/cosmos/cosmos-sdk/pull/18857) Moved `FormatCoins` from `core/coins` to this package under `signing/textual`. + ## v0.13.0 ### Improvements diff --git a/x/tx/signing/textual/coins.go b/x/tx/signing/textual/coins.go index 049b134214..3e3232ded6 100644 --- a/x/tx/signing/textual/coins.go +++ b/x/tx/signing/textual/coins.go @@ -3,13 +3,13 @@ package textual import ( "context" "fmt" + "sort" "strings" "google.golang.org/protobuf/reflect/protoreflect" bankv1beta1 "cosmossdk.io/api/cosmos/bank/v1beta1" basev1beta1 "cosmossdk.io/api/cosmos/base/v1beta1" - corecoins "cosmossdk.io/core/coins" "cosmossdk.io/math" ) @@ -48,7 +48,7 @@ func (vr coinsValueRenderer) Format(ctx context.Context, v protoreflect.Value) ( return nil, err } - formatted, err := corecoins.FormatCoins([]*basev1beta1.Coin{coin}, []*bankv1beta1.Metadata{metadata}) + formatted, err := FormatCoins([]*basev1beta1.Coin{coin}, []*bankv1beta1.Metadata{metadata}) if err != nil { return nil, err } @@ -76,7 +76,7 @@ func (vr coinsValueRenderer) FormatRepeated(ctx context.Context, v protoreflect. } } - formatted, err := corecoins.FormatCoins(coins, metadatas) + formatted, err := FormatCoins(coins, metadatas) if err != nil { return nil, err } @@ -217,3 +217,86 @@ func parseCoin(coinStr string, metadata *bankv1beta1.Metadata) (*basev1beta1.Coi Denom: baseDenom, }, nil } + +// formatCoin formats a sdk.Coin into a value-rendered string, using the +// given metadata about the denom. It returns the formatted coin string, the +// display denom, and an optional error. +func formatCoin(coin *basev1beta1.Coin, metadata *bankv1beta1.Metadata) (string, error) { + coinDenom := coin.Denom + + // Return early if no display denom or display denom is the current coin denom. + if metadata == nil || metadata.Display == "" || coinDenom == metadata.Display { + vr, err := math.FormatDec(coin.Amount) + return vr + " " + coin.Denom, err + } + + dispDenom := metadata.Display + + // Find exponents of both denoms. + var coinExp, dispExp uint32 + foundCoinExp, foundDispExp := false, false + for _, unit := range metadata.DenomUnits { + if coinDenom == unit.Denom { + coinExp = unit.Exponent + foundCoinExp = true + } + if dispDenom == unit.Denom { + dispExp = unit.Exponent + foundDispExp = true + } + } + + // If we didn't find either exponent, then we return early. + if !foundCoinExp || !foundDispExp { + vr, err := math.FormatInt(coin.Amount) + return vr + " " + coin.Denom, err + } + + dispAmount, err := math.LegacyNewDecFromStr(coin.Amount) + if err != nil { + return "", err + } + + if coinExp > dispExp { + dispAmount = dispAmount.Mul(math.LegacyNewDec(10).Power(uint64(coinExp - dispExp))) + } else { + dispAmount = dispAmount.Quo(math.LegacyNewDec(10).Power(uint64(dispExp - coinExp))) + } + + vr, err := math.FormatDec(dispAmount.String()) + return vr + " " + dispDenom, err +} + +// FormatCoins formats Coins into a value-rendered string, which uses +// `formatCoin` separated by ", " (a comma and a space), and sorted +// alphabetically by value-rendered denoms. It expects an array of metadata +// (optionally nil), where each metadata at index `i` MUST match the coin denom +// at the same index. +func FormatCoins(coins []*basev1beta1.Coin, metadata []*bankv1beta1.Metadata) (string, error) { + if len(coins) != len(metadata) { + return "", fmt.Errorf("formatCoins expect one metadata for each coin; expected %d, got %d", len(coins), len(metadata)) + } + + formatted := make([]string, len(coins)) + for i, coin := range coins { + var err error + formatted[i], err = formatCoin(coin, metadata[i]) + if err != nil { + return "", err + } + } + + if len(coins) == 0 { + return emptyCoins, nil + } + + // Sort the formatted coins by display denom. + sort.SliceStable(formatted, func(i, j int) bool { + denomI := strings.Split(formatted[i], " ")[1] + denomJ := strings.Split(formatted[j], " ")[1] + + return denomI < denomJ + }) + + return strings.Join(formatted, ", "), nil +} diff --git a/x/tx/signing/textual/coins_test.go b/x/tx/signing/textual/coins_test.go index ea139bd451..18b8439899 100644 --- a/x/tx/signing/textual/coins_test.go +++ b/x/tx/signing/textual/coins_test.go @@ -11,6 +11,7 @@ import ( bankv1beta1 "cosmossdk.io/api/cosmos/bank/v1beta1" basev1beta1 "cosmossdk.io/api/cosmos/base/v1beta1" + "cosmossdk.io/core/coins" "cosmossdk.io/math" "cosmossdk.io/x/tx/signing/textual" ) @@ -106,3 +107,64 @@ type coinsJSONTest struct { Text string Error bool } + +// formatCoinJSONTest is the type of test cases in the coin.json file. +type formatCoinJSONTest struct { + Proto *basev1beta1.Coin + Metadata *bankv1beta1.Metadata + Text string + Error bool +} + +func TestFormatCoin(t *testing.T) { + var testcases []formatCoinJSONTest + raw, err := os.ReadFile("./internal/testdata/coin.json") + require.NoError(t, err) + err = json.Unmarshal(raw, &testcases) + require.NoError(t, err) + + for _, tc := range testcases { + t.Run(tc.Text, func(t *testing.T) { + if tc.Proto != nil { + out, err := coins.FormatCoins([]*basev1beta1.Coin{tc.Proto}, []*bankv1beta1.Metadata{tc.Metadata}) + + if tc.Error { + require.Error(t, err) + return + } + + require.NoError(t, err) + require.Equal(t, tc.Text, out) + } + }) + } +} + +func TestFormatCoins(t *testing.T) { + var testcases []coinsJSONTest + raw, err := os.ReadFile("./internal/testdata/coins.json") + require.NoError(t, err) + err = json.Unmarshal(raw, &testcases) + require.NoError(t, err) + + for _, tc := range testcases { + t.Run(tc.Text, func(t *testing.T) { + if tc.Proto != nil { + metadata := make([]*bankv1beta1.Metadata, len(tc.Proto)) + for i, coin := range tc.Proto { + metadata[i] = tc.Metadata[coin.Denom] + } + + out, err := coins.FormatCoins(tc.Proto, metadata) + + if tc.Error { + require.Error(t, err) + return + } + + require.NoError(t, err) + require.Equal(t, tc.Text, out) + } + }) + } +}