-
Notifications
You must be signed in to change notification settings - Fork 3.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
refactor: Move FormatCoins to core
#13306
Changes from 9 commits
a107b87
b2f3103
da90b52
5998eff
0474882
435e83b
0fb8ce3
6576291
00aefd5
802e2d7
7e41cab
4f97362
cb005c0
3e03d62
5dd471b
be1b0af
a1287e8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
package coins | ||
|
||
import ( | ||
"fmt" | ||
"sort" | ||
"strings" | ||
|
||
bankv1beta1 "cosmossdk.io/api/cosmos/bank/v1beta1" | ||
basev1beta1 "cosmossdk.io/api/cosmos/base/v1beta1" | ||
"cosmossdk.io/math" | ||
) | ||
|
||
// 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 | ||
} | ||
|
||
exponentDiff := int64(coinExp) - int64(dispExp) | ||
Check failure Code scanning / gosec Potential integer overflow by integer type conversion
Potential integer overflow by integer type conversion
Check failure Code scanning / gosec Potential integer overflow by integer type conversion
Potential integer overflow by integer type conversion
|
||
|
||
dispAmount, err := math.LegacyNewDecFromStr(coin.Amount) | ||
if err != nil { | ||
return "", err | ||
} | ||
|
||
if exponentDiff > 0 { | ||
dispAmount = dispAmount.Mul(math.LegacyNewDec(10).Power(uint64(exponentDiff))) | ||
Check failure Code scanning / gosec Potential integer overflow by integer type conversion
Potential integer overflow by integer type conversion
|
||
} else { | ||
dispAmount = dispAmount.Quo(math.LegacyNewDec(10).Power(uint64(-exponentDiff))) | ||
Check failure Code scanning / gosec Potential integer overflow by integer type conversion
Potential integer overflow by integer type conversion
|
||
} | ||
|
||
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. autocli actually only needs parsing... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, ok, let's still get this merged IMO. Then we can add parsing directly here. |
||
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 | ||
} | ||
} | ||
|
||
// 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 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
package coins_test | ||
|
||
import ( | ||
"encoding/json" | ||
"os" | ||
"testing" | ||
|
||
bankv1beta1 "cosmossdk.io/api/cosmos/bank/v1beta1" | ||
basev1beta1 "cosmossdk.io/api/cosmos/base/v1beta1" | ||
"cosmossdk.io/core/coins" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestFormatCoin(t *testing.T) { | ||
var testcases []coinJsonTest | ||
raw, err := os.ReadFile("../../tx/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("../../tx/textual/internal/testdata/coins.json") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This We could alternatively:
For now the PR's version is my favorite. Any other opinions? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would say use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What you're doing is also maybe not terrible 🤷 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am going to keep it like this for now |
||
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) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
// 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 | ||
} | ||
amaury1093 marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,22 +5,31 @@ go 1.19 | |
require ( | ||
cosmossdk.io/api v0.2.1 | ||
cosmossdk.io/depinject v1.0.0-alpha.3 | ||
cosmossdk.io/math v1.0.0-beta.3 | ||
github.com/cosmos/cosmos-proto v1.0.0-alpha8 | ||
github.com/stretchr/testify v1.8.0 | ||
google.golang.org/protobuf v1.28.1 | ||
gotest.tools/v3 v3.4.0 | ||
sigs.k8s.io/yaml v1.3.0 | ||
) | ||
|
||
require ( | ||
github.com/cosmos/gogoproto v1.4.1 // indirect | ||
github.com/davecgh/go-spew v1.1.1 // indirect | ||
github.com/golang/protobuf v1.5.2 // indirect | ||
github.com/google/go-cmp v0.5.9 // indirect | ||
github.com/kr/text v0.2.0 // indirect | ||
github.com/pkg/errors v0.9.1 // indirect | ||
github.com/pmezard/go-difflib v1.0.0 // indirect | ||
golang.org/x/exp v0.0.0-20220827204233-334a2380cb91 // indirect | ||
golang.org/x/net v0.0.0-20221004154528-8021a29435af // indirect | ||
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect | ||
golang.org/x/text v0.3.7 // indirect | ||
google.golang.org/genproto v0.0.0-20220930163606-c98284e70a91 // indirect | ||
google.golang.org/grpc v1.50.0 // indirect | ||
gopkg.in/yaml.v2 v2.4.0 // indirect | ||
gopkg.in/yaml.v3 v3.0.1 // indirect | ||
) | ||
|
||
// temporary until we tag a new go module | ||
replace cosmossdk.io/math => ../math | ||
Comment on lines
+34
to
+35
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is kinda annoying...right? make changes, add replace directive, tag, remove replace directive (assuming you don't forget). Can we think of a better way? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed. Maybe we can always keep replaces on main, and use tags in release branches? I don't know. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We shouldn't do separate stuff on release branches. When we have more go.mod's release branches become much less necessary. We should usually be tagging off main. Ideally we'd have a bot that whenever it sees a merged commit with local replace directives, opens a PR to remove them which we can merge after we tag. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are we comparing against
unit.Denom
here? shouldn't it beunit.Display
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's no
unit.Display
, the metadata object looks likeThis piece of code finds the denom unit object (which contains the exponent) from inside the
denom_units
array