Skip to content

Commit

Permalink
chore: move FormatCoins to x/tx (#18857)
Browse files Browse the repository at this point in the history
  • Loading branch information
facundomedica authored Dec 21, 2023
1 parent 6649bb7 commit f4e9a1e
Show file tree
Hide file tree
Showing 7 changed files with 161 additions and 166 deletions.
3 changes: 1 addition & 2 deletions client/v2/autocli/flag/coin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
6 changes: 6 additions & 0 deletions core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
88 changes: 0 additions & 88 deletions core/coins/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
// <amount><denom>, where <amount> is a number and <denom> is a valid denom.
func ParseCoin(input string) (*basev1beta1.Coin, error) {
Expand Down
73 changes: 0 additions & 73 deletions core/coins/format_test.go
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
6 changes: 6 additions & 0 deletions x/tx/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
89 changes: 86 additions & 3 deletions x/tx/signing/textual/coins.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Loading

0 comments on commit f4e9a1e

Please sign in to comment.