chore: move FormatCoins to x/tx (#18857)
This commit is contained in:
parent
6649bb7d8f
commit
f4e9a1ecd5
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user