diff --git a/core/coins/format.go b/core/coins/format.go index b363ecb2e2..5c05ad4309 100644 --- a/core/coins/format.go +++ b/core/coins/format.go @@ -10,6 +10,8 @@ import ( "cosmossdk.io/math" ) +const emptyCoins = "zero" + // 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. @@ -78,6 +80,10 @@ func FormatCoins(coins []*basev1beta1.Coin, metadata []*bankv1beta1.Metadata) (s } } + 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] diff --git a/docs/architecture/adr-050-sign-mode-textual-annex1.md b/docs/architecture/adr-050-sign-mode-textual-annex1.md index ff5c71c60c..d947757d12 100644 --- a/docs/architecture/adr-050-sign-mode-textual-annex1.md +++ b/docs/architecture/adr-050-sign-mode-textual-annex1.md @@ -50,11 +50,13 @@ Value Renderers describe how values of different Protobuf types should be encode * an array of `coin` is display as the concatenation of each `coin` encoded as the specification above, the joined together with the delimiter `", "` (a comma and a space, no quotes around). * the list of coins is ordered by unicode code point of the display denom: `A-Z` < `a-z`. For example, the string `aAbBcC` would be sorted `ABCabc`. +- if the coins list had 0 items in it then it'll be rendered as `zero` ### Example * `["3cosm", "2000000uatom"]` -> `2 atom, 3 COSM` (assuming the display denoms are `atom` and `COSM`) * `["10atom", "20Acoin"]` -> `20 Acoin, 10 atom` (assuming the display denoms are `atom` and `Acoin`) +- `[]` -> `zero` ### `repeated` diff --git a/tx/textual/internal/testdata/coin.json b/tx/textual/internal/testdata/coin.json index 68a5fc37b9..ea4de172db 100644 --- a/tx/textual/internal/testdata/coin.json +++ b/tx/textual/internal/testdata/coin.json @@ -1,137 +1,137 @@ [ { "proto": {"amount": "0", "denom": "ucosm"}, - "metadata": {"display": "COSM", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}]}, + "metadata": {"display": "COSM", "base":"ucosm", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}]}, "text": "0 COSM" }, { "proto": {"amount": "1", "denom": "ucosm"}, - "metadata": {"display": "COSM", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}]}, + "metadata": {"display": "COSM", "base":"ucosm", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}]}, "text": "0.000001 COSM" }, { "proto": {"amount": "10", "denom": "ucosm"}, - "metadata": {"display": "COSM", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}]}, + "metadata": {"display": "COSM", "base":"ucosm", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}]}, "text": "0.00001 COSM" }, { "proto": {"amount": "100", "denom": "ucosm"}, - "metadata": {"display": "COSM", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}]}, + "metadata": {"display": "COSM", "base":"ucosm", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}]}, "text": "0.0001 COSM" }, { "proto": {"amount": "1000", "denom": "ucosm"}, - "metadata": {"display": "COSM", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}]}, + "metadata": {"display": "COSM", "base":"ucosm", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}]}, "text": "0.001 COSM" }, { "proto": {"amount": "10000", "denom": "ucosm"}, - "metadata": {"display": "COSM", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}]}, + "metadata": {"display": "COSM", "base":"ucosm", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}]}, "text": "0.01 COSM" }, { "proto": {"amount": "100000", "denom": "ucosm"}, - "metadata": {"display": "COSM", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}]}, + "metadata": {"display": "COSM", "base":"ucosm", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}]}, "text": "0.1 COSM" }, { "proto": {"amount": "1000000", "denom": "ucosm"}, - "metadata": {"display": "COSM", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}]}, + "metadata": {"display": "COSM", "base":"ucosm", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}]}, "text": "1 COSM" }, { "proto": {"amount": "10000000", "denom": "ucosm"}, - "metadata": {"display": "COSM", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}]}, + "metadata": {"display": "COSM", "base":"ucosm", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}]}, "text": "10 COSM" }, { "proto": {"amount": "0", "denom": "ucosm"}, - "metadata": {"display": "COSM", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}, {"denom":"stake", "exponent": 0}]}, + "metadata": {"display": "COSM", "base":"ucosm", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}, {"denom":"stake", "exponent": 0}]}, "text": "0 COSM" }, { "proto": {"amount": "1", "denom": "ucosm"}, - "metadata": {"display": "COSM", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}, {"denom":"stake", "exponent": 0}]}, + "metadata": {"display": "COSM", "base":"ucosm", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}, {"denom":"stake", "exponent": 0}]}, "text": "0.000001 COSM" }, { "proto": {"amount": "10", "denom": "ucosm"}, - "metadata": {"display": "COSM", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}, {"denom":"stake", "exponent": 0}]}, + "metadata": {"display": "COSM", "base":"ucosm", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}, {"denom":"stake", "exponent": 0}]}, "text": "0.00001 COSM" }, { "proto": {"amount": "100", "denom": "ucosm"}, - "metadata": {"display": "COSM", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}, {"denom":"stake", "exponent": 0}]}, + "metadata": {"display": "COSM", "base":"ucosm", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}, {"denom":"stake", "exponent": 0}]}, "text": "0.0001 COSM" }, { "proto": {"amount": "1000", "denom": "ucosm"}, - "metadata": {"display": "COSM", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}, {"denom":"stake", "exponent": 0}]}, + "metadata": {"display": "COSM", "base":"ucosm", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}, {"denom":"stake", "exponent": 0}]}, "text": "0.001 COSM" }, { "proto": {"amount": "10000", "denom": "ucosm"}, - "metadata": {"display": "COSM", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}, {"denom":"stake", "exponent": 0}]}, + "metadata": {"display": "COSM", "base":"ucosm", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}, {"denom":"stake", "exponent": 0}]}, "text": "0.01 COSM" }, { "proto": {"amount": "100000", "denom": "ucosm"}, - "metadata": {"display": "COSM", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}, {"denom":"stake", "exponent": 0}]}, + "metadata": {"display": "COSM", "base":"ucosm", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}, {"denom":"stake", "exponent": 0}]}, "text": "0.1 COSM" }, { "proto": {"amount": "1000000", "denom": "ucosm"}, - "metadata": {"display": "COSM", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}, {"denom":"stake", "exponent": 0}]}, + "metadata": {"display": "COSM", "base":"ucosm", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}, {"denom":"stake", "exponent": 0}]}, "text": "1 COSM" }, { "proto": {"amount": "10000000", "denom": "ucosm"}, - "metadata": {"display": "COSM", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}, {"denom":"stake", "exponent": 0}]}, + "metadata": {"display": "COSM", "base":"ucosm", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}, {"denom":"stake", "exponent": 0}]}, "text": "10 COSM" }, { "proto": {"amount": "0", "denom": "COSM"}, - "metadata": {"display": "ucosm", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}]}, + "metadata": {"display": "ucosm", "base":"COSM", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}]}, "text": "0 ucosm" }, { "proto": {"amount": "0.000001", "denom": "COSM"}, - "metadata": {"display": "ucosm", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}]}, + "metadata": {"display": "ucosm", "base":"COSM", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}]}, "text": "1 ucosm" }, { "proto": {"amount": "0.00001", "denom": "COSM"}, - "metadata": {"display": "ucosm", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}]}, + "metadata": {"display": "ucosm", "base":"COSM", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}]}, "text": "10 ucosm" }, { "proto": {"amount": "0.0001", "denom": "COSM"}, - "metadata": {"display": "ucosm", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}]}, + "metadata": {"display": "ucosm", "base":"COSM", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}]}, "text": "100 ucosm" }, { "proto": {"amount": "0.001", "denom": "COSM"}, - "metadata": {"display": "ucosm", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}]}, + "metadata": {"display": "ucosm", "base":"COSM", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}]}, "text": "1'000 ucosm" }, { "proto": {"amount": "0.01", "denom": "COSM"}, - "metadata": {"display": "ucosm", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}]}, + "metadata": {"display": "ucosm", "base":"COSM", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}]}, "text": "10'000 ucosm" }, { "proto": {"amount": "0.1", "denom": "COSM"}, - "metadata": {"display": "ucosm", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}]}, + "metadata": {"display": "ucosm", "base":"COSM", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}]}, "text": "100'000 ucosm" }, { "proto": {"amount": "1", "denom": "COSM"}, - "metadata": {"display": "ucosm", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}]}, + "metadata": {"display": "ucosm", "base":"COSM", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}]}, "text": "1'000'000 ucosm" }, { "proto": {"amount": "10", "denom": "COSM"}, - "metadata": {"display": "ucosm", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}]}, + "metadata": {"display": "ucosm", "base":"COSM", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}]}, "text": "10'000'000 ucosm" }, { @@ -217,137 +217,137 @@ }, { "proto": {"amount": "0", "denom": "ucosm"}, - "metadata": {"display": "COSM", "denom_units": [{"denom": "COSM", "exponent": 2}]}, + "metadata": {"display": "COSM", "base":"ucosm", "denom_units": [{"denom": "COSM", "exponent": 2}]}, "text": "0 ucosm" }, { "proto": {"amount": "1", "denom": "ucosm"}, - "metadata": {"display": "COSM", "denom_units": [{"denom": "COSM", "exponent": 2}]}, + "metadata": {"display": "COSM", "base":"ucosm", "denom_units": [{"denom": "COSM", "exponent": 2}]}, "text": "1 ucosm" }, { "proto": {"amount": "10", "denom": "ucosm"}, - "metadata": {"display": "COSM", "denom_units": [{"denom": "COSM", "exponent": 2}]}, + "metadata": {"display": "COSM", "base":"ucosm", "denom_units": [{"denom": "COSM", "exponent": 2}]}, "text": "10 ucosm" }, { "proto": {"amount": "100", "denom": "ucosm"}, - "metadata": {"display": "COSM", "denom_units": [{"denom": "COSM", "exponent": 2}]}, + "metadata": {"display": "COSM", "base":"ucosm", "denom_units": [{"denom": "COSM", "exponent": 2}]}, "text": "100 ucosm" }, { "proto": {"amount": "1000", "denom": "ucosm"}, - "metadata": {"display": "COSM", "denom_units": [{"denom": "COSM", "exponent": 2}]}, + "metadata": {"display": "COSM", "base":"ucosm", "denom_units": [{"denom": "COSM", "exponent": 2}]}, "text": "1'000 ucosm" }, { "proto": {"amount": "10000", "denom": "ucosm"}, - "metadata": {"display": "COSM", "denom_units": [{"denom": "COSM", "exponent": 2}]}, + "metadata": {"display": "COSM", "base":"ucosm", "denom_units": [{"denom": "COSM", "exponent": 2}]}, "text": "10'000 ucosm" }, { "proto": {"amount": "100000", "denom": "ucosm"}, - "metadata": {"display": "COSM", "denom_units": [{"denom": "COSM", "exponent": 2}]}, + "metadata": {"display": "COSM", "base":"ucosm", "denom_units": [{"denom": "COSM", "exponent": 2}]}, "text": "100'000 ucosm" }, { "proto": {"amount": "1000000", "denom": "ucosm"}, - "metadata": {"display": "COSM", "denom_units": [{"denom": "COSM", "exponent": 2}]}, + "metadata": {"display": "COSM", "base":"ucosm", "denom_units": [{"denom": "COSM", "exponent": 2}]}, "text": "1'000'000 ucosm" }, { "proto": {"amount": "10000000", "denom": "ucosm"}, - "metadata": {"display": "COSM", "denom_units": [{"denom": "COSM", "exponent": 2}]}, + "metadata": {"display": "COSM", "base":"ucosm", "denom_units": [{"denom": "COSM", "exponent": 2}]}, "text": "10'000'000 ucosm" }, { "proto": {"amount": "0", "denom": "ucosm"}, - "metadata": {"display": "COSM", "denom_units": [{"denom": "COSM", "exponent": 2}, {"denom": "ucosm", "exponent": 0}]}, + "metadata": {"display": "COSM", "base":"ucosm", "denom_units": [{"denom": "COSM", "exponent": 2}, {"denom": "ucosm", "exponent": 0}]}, "text": "0 COSM" }, { "proto": {"amount": "1", "denom": "ucosm"}, - "metadata": {"display": "COSM", "denom_units": [{"denom": "COSM", "exponent": 2}, {"denom": "ucosm", "exponent": 0}]}, + "metadata": {"display": "COSM", "base":"ucosm", "denom_units": [{"denom": "COSM", "exponent": 2}, {"denom": "ucosm", "exponent": 0}]}, "text": "0.01 COSM" }, { "proto": {"amount": "10", "denom": "ucosm"}, - "metadata": {"display": "COSM", "denom_units": [{"denom": "COSM", "exponent": 2}, {"denom": "ucosm", "exponent": 0}]}, + "metadata": {"display": "COSM", "base":"ucosm", "denom_units": [{"denom": "COSM", "exponent": 2}, {"denom": "ucosm", "exponent": 0}]}, "text": "0.1 COSM" }, { "proto": {"amount": "100", "denom": "ucosm"}, - "metadata": {"display": "COSM", "denom_units": [{"denom": "COSM", "exponent": 2}, {"denom": "ucosm", "exponent": 0}]}, + "metadata": {"display": "COSM", "base":"ucosm", "denom_units": [{"denom": "COSM", "exponent": 2}, {"denom": "ucosm", "exponent": 0}]}, "text": "1 COSM" }, { "proto": {"amount": "1000", "denom": "ucosm"}, - "metadata": {"display": "COSM", "denom_units": [{"denom": "COSM", "exponent": 2}, {"denom": "ucosm", "exponent": 0}]}, + "metadata": {"display": "COSM", "base":"ucosm", "denom_units": [{"denom": "COSM", "exponent": 2}, {"denom": "ucosm", "exponent": 0}]}, "text": "10 COSM" }, { "proto": {"amount": "10000", "denom": "ucosm"}, - "metadata": {"display": "COSM", "denom_units": [{"denom": "COSM", "exponent": 2}, {"denom": "ucosm", "exponent": 0}]}, + "metadata": {"display": "COSM", "base":"ucosm", "denom_units": [{"denom": "COSM", "exponent": 2}, {"denom": "ucosm", "exponent": 0}]}, "text": "100 COSM" }, { "proto": {"amount": "100000", "denom": "ucosm"}, - "metadata": {"display": "COSM", "denom_units": [{"denom": "COSM", "exponent": 2}, {"denom": "ucosm", "exponent": 0}]}, + "metadata": {"display": "COSM", "base":"ucosm", "denom_units": [{"denom": "COSM", "exponent": 2}, {"denom": "ucosm", "exponent": 0}]}, "text": "1'000 COSM" }, { "proto": {"amount": "1000000", "denom": "ucosm"}, - "metadata": {"display": "COSM", "denom_units": [{"denom": "COSM", "exponent": 2}, {"denom": "ucosm", "exponent": 0}]}, + "metadata": {"display": "COSM", "base":"ucosm", "denom_units": [{"denom": "COSM", "exponent": 2}, {"denom": "ucosm", "exponent": 0}]}, "text": "10'000 COSM" }, { "proto": {"amount": "10000000", "denom": "ucosm"}, - "metadata": {"display": "COSM", "denom_units": [{"denom": "COSM", "exponent": 2}, {"denom": "ucosm", "exponent": 0}]}, + "metadata": {"display": "COSM", "base":"ucosm", "denom_units": [{"denom": "COSM", "exponent": 2}, {"denom": "ucosm", "exponent": 0}]}, "text": "100'000 COSM" }, { "proto": {"amount": "0", "denom": "point"}, - "metadata": {"display": "POINT", "denom_units": [{"denom": "point", "exponent": 0}, {"denom": "POINT", "exponent": 0}]}, + "metadata": {"display": "POINT", "base": "point", "denom_units": [{"denom": "point", "exponent": 0}, {"denom": "POINT", "exponent": 0}]}, "text": "0 POINT" }, { "proto": {"amount": "1", "denom": "point"}, - "metadata": {"display": "POINT", "denom_units": [{"denom": "point", "exponent": 0}, {"denom": "POINT", "exponent": 0}]}, + "metadata": {"display": "POINT", "base": "point", "denom_units": [{"denom": "point", "exponent": 0}, {"denom": "POINT", "exponent": 0}]}, "text": "1 POINT" }, { "proto": {"amount": "10", "denom": "point"}, - "metadata": {"display": "POINT", "denom_units": [{"denom": "point", "exponent": 0}, {"denom": "POINT", "exponent": 0}]}, + "metadata": {"display": "POINT", "base": "point", "denom_units": [{"denom": "point", "exponent": 0}, {"denom": "POINT", "exponent": 0}]}, "text": "10 POINT" }, { "proto": {"amount": "100", "denom": "point"}, - "metadata": {"display": "POINT", "denom_units": [{"denom": "point", "exponent": 0}, {"denom": "POINT", "exponent": 0}]}, + "metadata": {"display": "POINT", "base": "point", "denom_units": [{"denom": "point", "exponent": 0}, {"denom": "POINT", "exponent": 0}]}, "text": "100 POINT" }, { "proto": {"amount": "1000", "denom": "point"}, - "metadata": {"display": "POINT", "denom_units": [{"denom": "point", "exponent": 0}, {"denom": "POINT", "exponent": 0}]}, + "metadata": {"display": "POINT", "base": "point", "denom_units": [{"denom": "point", "exponent": 0}, {"denom": "POINT", "exponent": 0}]}, "text": "1'000 POINT" }, { "proto": {"amount": "10000", "denom": "point"}, - "metadata": {"display": "POINT", "denom_units": [{"denom": "point", "exponent": 0}, {"denom": "POINT", "exponent": 0}]}, + "metadata": {"display": "POINT", "base": "point", "denom_units": [{"denom": "point", "exponent": 0}, {"denom": "POINT", "exponent": 0}]}, "text": "10'000 POINT" }, { "proto": {"amount": "100000", "denom": "point"}, - "metadata": {"display": "POINT", "denom_units": [{"denom": "point", "exponent": 0}, {"denom": "POINT", "exponent": 0}]}, + "metadata": {"display": "POINT", "base": "point", "denom_units": [{"denom": "point", "exponent": 0}, {"denom": "POINT", "exponent": 0}]}, "text": "100'000 POINT" }, { "proto": {"amount": "1000000", "denom": "point"}, - "metadata": {"display": "POINT", "denom_units": [{"denom": "point", "exponent": 0}, {"denom": "POINT", "exponent": 0}]}, + "metadata": {"display": "POINT", "base": "point", "denom_units": [{"denom": "point", "exponent": 0}, {"denom": "POINT", "exponent": 0}]}, "text": "1'000'000 POINT" }, { "proto": {"amount": "10000000", "denom": "point"}, - "metadata": {"display": "POINT", "denom_units": [{"denom": "point", "exponent": 0}, {"denom": "POINT", "exponent": 0}]}, + "metadata": {"display": "POINT", "base": "point", "denom_units": [{"denom": "point", "exponent": 0}, {"denom": "POINT", "exponent": 0}]}, "text": "10'000'000 POINT" }, {"text":"", "error": true}, diff --git a/tx/textual/internal/testdata/coins.json b/tx/textual/internal/testdata/coins.json index ccac405411..4370ce550e 100644 --- a/tx/textual/internal/testdata/coins.json +++ b/tx/textual/internal/testdata/coins.json @@ -2,15 +2,15 @@ { "proto": [], "metadata":{}, - "text": "" + "text": "zero" }, { "proto": [ { "amount": "1", "denom": "ucosm" } ], "metadata":{ - "ucosm": {"display": "COSM", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}]}, - "ustake": {"display": "STAKE", "denom_units": [{"denom": "STAKE", "exponent": 6}, {"denom": "ustake", "exponent": 0}]} + "ucosm": {"display": "COSM", "base": "ucosm", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}]}, + "ustake": {"display": "STAKE", "base": "ustake", "denom_units": [{"denom": "STAKE", "exponent": 6}, {"denom": "ustake", "exponent": 0}]} }, "text": "0.000001 COSM" }, @@ -20,8 +20,8 @@ { "amount": "3", "denom": "ustake" } ], "metadata":{ - "ucosm": {"display": "COSM", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}]}, - "ustake": {"display": "STAKE", "denom_units": [{"denom": "STAKE", "exponent": 6}, {"denom": "ustake", "exponent": 0}]} + "ucosm": {"display": "COSM", "base":"ucosm", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}]}, + "ustake": {"display": "STAKE", "base":"ustake", "denom_units": [{"denom": "STAKE", "exponent": 6}, {"denom": "ustake", "exponent": 0}]} }, "text": "0.000001 COSM, 0.000003 STAKE" }, @@ -31,8 +31,8 @@ { "amount": "1", "denom": "ucosm" } ], "metadata": { - "ucosm": {"display": "COSM", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}]}, - "ustake": {"display": "STAKE", "denom_units": [{"denom": "STAKE", "exponent": 6}, {"denom": "ustake", "exponent": 0}]} + "ucosm": {"display": "COSM", "base":"ucosm", "denom_units": [{"denom": "COSM", "exponent": 6}, {"denom": "ucosm", "exponent": 0}]}, + "ustake": {"display": "STAKE", "base":"ustake", "denom_units": [{"denom": "STAKE", "exponent": 6}, {"denom": "ustake", "exponent": 0}]} }, "text": "0.000001 COSM, 0.000003 STAKE" }, @@ -43,9 +43,9 @@ { "amount": "3", "denom": "uatom" } ], "metadata": { - "uaa": {"display": "AA", "denom_units": [{"denom": "AA", "exponent": 6}, {"denom": "uaa", "exponent": 0}]}, - "ubb": {"display": "BB", "denom_units": [{"denom": "BB", "exponent": 6}, {"denom": "ubb", "exponent": 0}]}, - "uatom": {"display": "atom", "denom_units": [{"denom": "atom", "exponent": 6}, {"denom": "uatom", "exponent": 0}]} + "uaa": {"display": "AA", "base":"uaa", "denom_units": [{"denom": "AA", "exponent": 6}, {"denom": "uaa", "exponent": 0}]}, + "ubb": {"display": "BB", "base":"ubb", "denom_units": [{"denom": "BB", "exponent": 6}, {"denom": "ubb", "exponent": 0}]}, + "uatom": {"display": "atom", "base":"uatom", "denom_units": [{"denom": "atom", "exponent": 6}, {"denom": "uatom", "exponent": 0}]} }, "text": "0.000001 AA, 0.000002 BB, 0.000003 atom" }, @@ -57,10 +57,10 @@ { "amount": "1", "denom": "uxa" } ], "metadata": { - "uxa": {"display": "xA", "denom_units": [{"denom": "xA", "exponent": 6}, {"denom": "uxa", "exponent": 0}]}, - "uxb": {"display": "xB", "denom_units": [{"denom": "xB", "exponent": 6}, {"denom": "uxb", "exponent": 0}]}, - "uxc": {"display": "xC", "denom_units": [{"denom": "xC", "exponent": 6}, {"denom": "uxc", "exponent": 0}]}, - "uxc1": {"display": "xC1", "denom_units": [{"denom": "xC1", "exponent": 6}, {"denom": "uxc1", "exponent": 0}]} + "uxa": {"display": "xA", "base":"uxa", "denom_units": [{"denom": "xA", "exponent": 6}, {"denom": "uxa", "exponent": 0}]}, + "uxb": {"display": "xB", "base":"uxb", "denom_units": [{"denom": "xB", "exponent": 6}, {"denom": "uxb", "exponent": 0}]}, + "uxc": {"display": "xC", "base":"uxc", "denom_units": [{"denom": "xC", "exponent": 6}, {"denom": "uxc", "exponent": 0}]}, + "uxc1": {"display": "xC1", "base":"uxc1", "denom_units": [{"denom": "xC1", "exponent": 6}, {"denom": "uxc1", "exponent": 0}]} }, "text": "0.000001 xA, 0.000002 xB, 0.000003 xC, 0.000004 xC1" }, diff --git a/tx/textual/valuerenderer/coin_test.go b/tx/textual/valuerenderer/coin_test.go index abce7f9ea1..8023f9f4f8 100644 --- a/tx/textual/valuerenderer/coin_test.go +++ b/tx/textual/valuerenderer/coin_test.go @@ -48,7 +48,7 @@ func TestMetadataQuerier(t *testing.T) { require.NoError(t, err) _, err = vr.Format(context.Background(), protoreflect.ValueOf((&basev1beta1.Coin{}).ProtoReflect())) require.ErrorIs(t, err, expErr) - _, err = vr.Format(context.Background(), protoreflect.ValueOf(NewGenericList([]*basev1beta1.Coin{{}}))) + _, err = vr.(valuerenderer.RepeatedValueRenderer).FormatRepeated(context.Background(), protoreflect.ValueOf(NewGenericList([]*basev1beta1.Coin{{}}))) require.ErrorIs(t, err, expErr) } @@ -67,6 +67,10 @@ func TestCoinJsonTestcases(t *testing.T) { t.Run(tc.Text, func(t *testing.T) { if tc.Proto != nil { ctx := context.WithValue(context.Background(), mockCoinMetadataKey(tc.Proto.Denom), tc.Metadata) + if tc.Metadata != nil { + ctx = context.WithValue(ctx, mockCoinMetadataKey(tc.Metadata.Display), tc.Metadata) + } + screens, err := vr.Format(ctx, protoreflect.ValueOf(tc.Proto.ProtoReflect())) if tc.Error { @@ -77,10 +81,20 @@ func TestCoinJsonTestcases(t *testing.T) { require.NoError(t, err) require.Equal(t, 1, len(screens)) require.Equal(t, tc.Text, screens[0].Text) - } - // TODO Add parsing tests - // https://github.com/cosmos/cosmos-sdk/issues/13153 + // Round trip. + value, err := vr.Parse(ctx, screens) + if tc.Error { + require.Error(t, err) + return + } + + require.NoError(t, err) + coin, ok := value.Message().Interface().(*basev1beta1.Coin) + require.True(t, ok) + + checkCoinEqual(t, coin, tc.Proto) + } }) } } diff --git a/tx/textual/valuerenderer/coins.go b/tx/textual/valuerenderer/coins.go index 9bbb434941..899fdc259c 100644 --- a/tx/textual/valuerenderer/coins.go +++ b/tx/textual/valuerenderer/coins.go @@ -3,14 +3,18 @@ package valuerenderer import ( "context" "fmt" + "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" ) +const emptyCoins = "zero" + // NewCoinsValueRenderer returns a ValueRenderer for SDK Coin and Coins. func NewCoinsValueRenderer(q CoinMetadataQueryFn) ValueRenderer { return coinsValueRenderer{q} @@ -31,61 +35,187 @@ func (vr coinsValueRenderer) Format(ctx context.Context, v protoreflect.Value) ( return nil, fmt.Errorf("expected non-nil coin metadata querier") } - // Check whether we have a Coin or some Coins. - switch protoCoins := v.Interface().(type) { - // If it's a repeated Coin: - case protoreflect.List: - { - coins, metadatas := make([]*basev1beta1.Coin, protoCoins.Len()), make([]*bankv1beta1.Metadata, protoCoins.Len()) - var err error - for i := 0; i < protoCoins.Len(); i++ { - coin := protoCoins.Get(i).Interface().(protoreflect.Message).Interface().(*basev1beta1.Coin) - coins[i] = coin - metadatas[i], err = vr.coinMetadataQuerier(ctx, coin.Denom) - if err != nil { - return nil, err - } - } + // Since this value renderer has a FormatRepeated method, the Format one + // here only handles single coin. + coin := v.Interface().(protoreflect.Message).Interface().(*basev1beta1.Coin) - formatted, err := corecoins.FormatCoins(coins, metadatas) - if err != nil { - return nil, err - } - - return []Screen{{Text: formatted}}, nil - } - // If it's a single Coin: - case protoreflect.Message: - { - coin := v.Interface().(protoreflect.Message).Interface().(*basev1beta1.Coin) - - metadata, err := vr.coinMetadataQuerier(ctx, coin.Denom) - if err != nil { - return nil, err - } - - formatted, err := corecoins.FormatCoins([]*basev1beta1.Coin{coin}, []*bankv1beta1.Metadata{metadata}) - if err != nil { - return nil, err - } - - return []Screen{{Text: formatted}}, nil - } - default: - return nil, fmt.Errorf("got invalid type %t for coins", v.Interface()) + metadata, err := vr.coinMetadataQuerier(ctx, coin.Denom) + if err != nil { + return nil, err } -} -func (vr coinsValueRenderer) Parse(_ context.Context, screens []Screen) (protoreflect.Value, error) { - // ref: https://github.com/cosmos/cosmos-sdk/issues/13153 - panic("implement me, see #13153") + formatted, err := corecoins.FormatCoins([]*basev1beta1.Coin{coin}, []*bankv1beta1.Metadata{metadata}) + if err != nil { + return nil, err + } + + return []Screen{{Text: formatted}}, nil } func (vr coinsValueRenderer) FormatRepeated(ctx context.Context, v protoreflect.Value) ([]Screen, error) { - return vr.Format(ctx, v) + if vr.coinMetadataQuerier == nil { + return nil, fmt.Errorf("expected non-nil coin metadata querier") + } + + protoCoins := v.List() + coins, metadatas := make([]*basev1beta1.Coin, protoCoins.Len()), make([]*bankv1beta1.Metadata, protoCoins.Len()) + var err error + for i := 0; i < protoCoins.Len(); i++ { + coin := protoCoins.Get(i).Interface().(protoreflect.Message).Interface().(*basev1beta1.Coin) + coins[i] = coin + metadatas[i], err = vr.coinMetadataQuerier(ctx, coin.Denom) + if err != nil { + return nil, err + } + } + + formatted, err := corecoins.FormatCoins(coins, metadatas) + if err != nil { + return nil, err + } + + return []Screen{{Text: formatted}}, nil +} + +func (vr coinsValueRenderer) Parse(ctx context.Context, screens []Screen) (protoreflect.Value, error) { + if len(screens) != 1 { + return nilValue, fmt.Errorf("expected single screen: %v", screens) + } + + if screens[0].Text == emptyCoins { + return protoreflect.ValueOfMessage((&basev1beta1.Coin{}).ProtoReflect()), nil + } + + parsed, err := vr.parseCoins(ctx, screens[0].Text) + if err != nil { + return nilValue, err + } + + return protoreflect.ValueOfMessage(parsed[0].ProtoReflect()), err } func (vr coinsValueRenderer) ParseRepeated(ctx context.Context, screens []Screen, l protoreflect.List) error { - // ref: https://github.com/cosmos/cosmos-sdk/issues/13153 - panic("implement me, see #13153") + if len(screens) != 1 { + return fmt.Errorf("expected single screen: %v", screens) + } + + if screens[0].Text == emptyCoins { + return nil + } + + parsed, err := vr.parseCoins(ctx, screens[0].Text) + if err != nil { + return err + } + + for _, c := range parsed { + l.Append(protoreflect.ValueOf(c.ProtoReflect())) + } + + return nil +} + +func (vr coinsValueRenderer) parseCoins(ctx context.Context, coinsStr string) ([]*basev1beta1.Coin, error) { + coins := strings.Split(coinsStr, ", ") + metadatas := make([]*bankv1beta1.Metadata, len(coins)) + + var err error + for i, coin := range coins { + coinArr := strings.Split(coin, " ") + if len(coinArr) != 2 { + return nil, fmt.Errorf("invalid coin %s", coin) + } + metadatas[i], err = vr.coinMetadataQuerier(ctx, coinArr[1]) + if err != nil { + return nil, err + } + } + + if len(coins) != len(metadatas) { + return []*basev1beta1.Coin{}, fmt.Errorf("formatCoins expect one metadata for each coin; expected %d, got %d", len(coins), len(metadatas)) + } + + parsedCoins := make([]*basev1beta1.Coin, len(coins)) + for i, coinStr := range coins { + coin, err := parseCoin(coinStr, metadatas[i]) + if err != nil { + return nil, err + } + parsedCoins[i] = coin + } + + return parsedCoins, nil +} + +// parseCoin parses a single value-rendered coin into the Coin struct. +// It shares a lot of code with `cosmos-sdk.io/core/coins.Format`, +// so this code might be refactored once we have +// a core Parse function for coins. +func parseCoin(coinStr string, metadata *bankv1beta1.Metadata) (*basev1beta1.Coin, error) { + coinArr := strings.Split(coinStr, " ") + amt1 := coinArr[0] + coinDenom := coinArr[1] + + if metadata == nil || metadata.Base == "" || coinArr[1] == metadata.Base { + dec, err := parseDec(amt1) + if err != nil { + return nil, err + } + + return &basev1beta1.Coin{ + Amount: dec, + Denom: coinDenom, + }, nil + } + baseDenom := metadata.Base + + // Find exponents of both denoms. + foundCoinExp, foundBaseExp := false, false + var coinExp, baseExp uint32 + for _, unit := range metadata.DenomUnits { + if coinDenom == unit.Denom { + coinExp = unit.Exponent + foundCoinExp = true + } + if baseDenom == unit.Denom { + baseExp = unit.Exponent + foundBaseExp = true + } + } + + // If we didn't find either exponent, then we return early. + if !foundCoinExp || !foundBaseExp { + amt, err := parseDec(amt1) + if err != nil { + return nil, err + } + + return &basev1beta1.Coin{ + Amount: amt, + Denom: baseDenom, + }, nil + } + + // remove 1000 separators, (ex: 1'000'000 -> 1000000) + amt1 = strings.ReplaceAll(amt1, "'", "") + amt, err := math.LegacyNewDecFromStr(amt1) + if err != nil { + return nil, err + } + + if coinExp > baseExp { + amt = amt.Mul(math.LegacyNewDec(10).Power(uint64(coinExp - baseExp))) + } else { + amt = amt.Quo(math.LegacyNewDec(10).Power(uint64(baseExp - coinExp))) + } + + amtStr, err := parseDec(amt.String()) + if err != nil { + return nil, err + } + + return &basev1beta1.Coin{ + Amount: amtStr, + Denom: baseDenom, + }, nil } diff --git a/tx/textual/valuerenderer/coins_test.go b/tx/textual/valuerenderer/coins_test.go index 6bb64e6deb..264e5a27d8 100644 --- a/tx/textual/valuerenderer/coins_test.go +++ b/tx/textual/valuerenderer/coins_test.go @@ -8,6 +8,7 @@ import ( bankv1beta1 "cosmossdk.io/api/cosmos/bank/v1beta1" basev1beta1 "cosmossdk.io/api/cosmos/base/v1beta1" + "cosmossdk.io/math" "cosmossdk.io/tx/textual/valuerenderer" "github.com/stretchr/testify/require" "google.golang.org/protobuf/reflect/protoreflect" @@ -22,6 +23,7 @@ func TestCoinsJsonTestcases(t *testing.T) { textual := valuerenderer.NewTextual(mockCoinMetadataQuerier) vr, err := textual.GetFieldValueRenderer(fieldDescriptorFromName("COINS")) + vrr := vr.(valuerenderer.RepeatedValueRenderer) require.NoError(t, err) for _, tc := range testcases { @@ -30,29 +32,67 @@ func TestCoinsJsonTestcases(t *testing.T) { // Create a context.Context containing all coins metadata, to simulate // that they are in state. ctx := context.Background() - for _, coin := range tc.Proto { - ctx = context.WithValue(ctx, mockCoinMetadataKey(coin.Denom), tc.Metadata[coin.Denom]) + + for _, v := range tc.Metadata { + ctx = context.WithValue(ctx, mockCoinMetadataKey(v.Base), v) + ctx = context.WithValue(ctx, mockCoinMetadataKey(v.Display), v) } listValue := NewGenericList(tc.Proto) - screens, err := vr.Format(ctx, protoreflect.ValueOf(listValue)) + screens, err := vrr.FormatRepeated(ctx, protoreflect.ValueOf(listValue)) + require.NoError(t, err) + require.Equal(t, 1, len(screens)) + require.Equal(t, tc.Text, screens[0].Text) + + // Round trip. + parsedValue := NewGenericList([]*basev1beta1.Coin{}) + err = vrr.ParseRepeated(ctx, screens, parsedValue) if tc.Error { require.Error(t, err) return } require.NoError(t, err) - require.Equal(t, 1, len(screens)) - require.Equal(t, tc.Text, screens[0].Text) + checkCoinsEqual(t, listValue, parsedValue) } - - // TODO Add parsing tests - // https://github.com/cosmos/cosmos-sdk/issues/13153 }) } } +// checkCoinsEqual checks that the 2 lists of Coins contain the same +// **set** of coins. It does not check that the order of coins are +// equal, because in Textual, we sort the coins alphabetically after +// rendering, so we lose initial Coins ordering. Instead, we just check +// set equality using a map. +func checkCoinsEqual(t *testing.T, l1, l2 protoreflect.List) { + require.Equal(t, l1.Len(), l2.Len()) + var coinsMap = make(map[string]*basev1beta1.Coin, l1.Len()) + + for i := 0; i < l1.Len(); i++ { + coin, ok := l1.Get(i).Message().Interface().(*basev1beta1.Coin) + require.True(t, ok) + coinsMap[coin.Denom] = coin + } + + for i := 0; i < l2.Len(); i++ { + coin, ok := l2.Get(i).Message().Interface().(*basev1beta1.Coin) + require.True(t, ok) + + coin1 := coinsMap[coin.Denom] + checkCoinEqual(t, coin, coin1) + } +} + +func checkCoinEqual(t *testing.T, coin, coin1 *basev1beta1.Coin) { + require.Equal(t, coin1.Denom, coin.Denom) + v, err := math.LegacyNewDecFromStr(coin.Amount) + require.NoError(t, err) + v1, err := math.LegacyNewDecFromStr(coin1.Amount) + require.NoError(t, err) + require.True(t, v.Equal(v1)) +} + // coinsJsonTest is the type of test cases in the testdata file. // If the test case has a Proto, try to Format() it. If Error is set, expect // an error, otherwise match Text, then Parse() the text and expect it to diff --git a/tx/textual/valuerenderer/dec.go b/tx/textual/valuerenderer/dec.go index 4aa9178d1d..fafbbf9132 100644 --- a/tx/textual/valuerenderer/dec.go +++ b/tx/textual/valuerenderer/dec.go @@ -2,6 +2,8 @@ package valuerenderer import ( "context" + "fmt" + "strings" "google.golang.org/protobuf/reflect/protoreflect" @@ -27,5 +29,32 @@ func (vr decValueRenderer) Format(_ context.Context, v protoreflect.Value) ([]Sc } func (vr decValueRenderer) Parse(_ context.Context, screens []Screen) (protoreflect.Value, error) { - panic("implement me") + if n := len(screens); n != 1 { + return nilValue, fmt.Errorf("expected 1 screen, got: %d", n) + } + + parsed, err := parseDec(screens[0].Text) + if err != nil { + return nilValue, err + } + + return protoreflect.ValueOfString(parsed), nil +} + +func parseDec(v string) (string, error) { + parts := strings.Split(v, ".") + if len(parts) > 2 { + return "", fmt.Errorf("invalid decimal: too many points in %s", v) + } + + intPart, err := parseInt(parts[0]) + if err != nil { + return "", err + } + + if len(parts) == 1 { + return intPart, nil + } + + return intPart + "." + parts[1], nil } diff --git a/tx/textual/valuerenderer/int.go b/tx/textual/valuerenderer/int.go index 857ed14adc..51f8d09428 100644 --- a/tx/textual/valuerenderer/int.go +++ b/tx/textual/valuerenderer/int.go @@ -2,6 +2,9 @@ package valuerenderer import ( "context" + "fmt" + "strconv" + "strings" "cosmossdk.io/math" "google.golang.org/protobuf/reflect/protoreflect" @@ -9,11 +12,13 @@ import ( // NewIntValueRenderer returns a ValueRenderer for uint32, uint64, int32 and // int64, and sdk.Int scalars. -func NewIntValueRenderer() ValueRenderer { - return intValueRenderer{} +func NewIntValueRenderer(fd protoreflect.FieldDescriptor) ValueRenderer { + return intValueRenderer{fd} } -type intValueRenderer struct{} +type intValueRenderer struct { + fd protoreflect.FieldDescriptor +} var _ ValueRenderer = intValueRenderer{} @@ -26,5 +31,62 @@ func (vr intValueRenderer) Format(_ context.Context, v protoreflect.Value) ([]Sc } func (vr intValueRenderer) Parse(_ context.Context, screens []Screen) (protoreflect.Value, error) { - panic("implement me") + if n := len(screens); n != 1 { + return nilValue, fmt.Errorf("expected 1 screen, got: %d", n) + } + + parsedInt, err := parseInt(screens[0].Text) + if err != nil { + return nilValue, err + } + + switch vr.fd.Kind() { + case protoreflect.Uint32Kind: + value, err := strconv.ParseUint(parsedInt, 10, 32) + if err != nil { + return nilValue, err + } + return protoreflect.ValueOfUint32(uint32(value)), nil //nolint:gosec + + case protoreflect.Uint64Kind: + value, err := strconv.ParseUint(parsedInt, 10, 64) + if err != nil { + return nilValue, err + } + return protoreflect.ValueOfUint64(value), nil + + case protoreflect.Int32Kind: + value, err := strconv.ParseInt(parsedInt, 10, 32) + if err != nil { + return nilValue, err + } + return protoreflect.ValueOfInt32(int32(value)), nil //nolint:gosec + + case protoreflect.Int64Kind: + value, err := strconv.ParseInt(parsedInt, 10, 64) + if err != nil { + return nilValue, err + } + return protoreflect.ValueOfInt64(value), nil + + case protoreflect.StringKind: + return protoreflect.ValueOfString(parsedInt), nil + + default: + return nilValue, fmt.Errorf("parsing integers into a %s field is not supported", vr.fd.Kind()) + } +} + +// parseInt parses a value-rendered string into an integer +func parseInt(v string) (string, error) { + sign := "" + if v[0] == '-' { + sign = "-" + v = v[1:] + } + + // remove the 1000 separators (ex: 1'000'000 -> 1000000) + v = strings.ReplaceAll(v, "'", "") + + return sign + v, nil } diff --git a/tx/textual/valuerenderer/int_test.go b/tx/textual/valuerenderer/int_test.go index 0b9b69016e..0b047661b7 100644 --- a/tx/textual/valuerenderer/int_test.go +++ b/tx/textual/valuerenderer/int_test.go @@ -25,32 +25,34 @@ func TestIntJsonTestcases(t *testing.T) { textual := valuerenderer.NewTextual(nil) for _, tc := range testcases { - // Parse test case strings as protobuf uint64 - i, err := strconv.ParseUint(tc[0], 10, 64) - if err == nil { - r, err := textual.GetFieldValueRenderer(fieldDescriptorFromName("UINT64")) - require.NoError(t, err) + t.Run(tc[0], func(t *testing.T) { + // Parse test case strings as protobuf uint64 + i, err := strconv.ParseUint(tc[0], 10, 64) + if err == nil { + r, err := textual.GetFieldValueRenderer(fieldDescriptorFromName("UINT64")) + require.NoError(t, err) - checkNumberTest(t, r, protoreflect.ValueOf(i), tc[1]) - } + checkNumberTest(t, r, protoreflect.ValueOf(i), tc[1]) + } - // Parse test case strings as protobuf uint32 - i, err = strconv.ParseUint(tc[0], 10, 32) - if err == nil { - r, err := textual.GetFieldValueRenderer(fieldDescriptorFromName("UINT32")) - require.NoError(t, err) + // Parse test case strings as protobuf uint32 + i, err = strconv.ParseUint(tc[0], 10, 32) + if err == nil { + r, err := textual.GetFieldValueRenderer(fieldDescriptorFromName("UINT32")) + require.NoError(t, err) - checkNumberTest(t, r, protoreflect.ValueOf(i), tc[1]) - } + checkNumberTest(t, r, protoreflect.ValueOf(i), tc[1]) + } - // Parse test case strings as sdk.Ints - _, ok := math.NewIntFromString(tc[0]) - if ok { - r, err := textual.GetFieldValueRenderer(fieldDescriptorFromName("SDKINT")) - require.NoError(t, err) + // Parse test case strings as sdk.Ints + _, ok := math.NewIntFromString(tc[0]) + if ok { + r, err := textual.GetFieldValueRenderer(fieldDescriptorFromName("SDKINT")) + require.NoError(t, err) - checkNumberTest(t, r, protoreflect.ValueOf(tc[0]), tc[1]) - } + checkNumberTest(t, r, protoreflect.ValueOf(tc[0]), tc[1]) + } + }) } } @@ -64,4 +66,16 @@ func checkNumberTest(t *testing.T, r valuerenderer.ValueRenderer, pv protoreflec require.Equal(t, false, screens[0].Expert) require.Equal(t, expected, screens[0].Text) + + // Round trip. + value, err := r.Parse(context.Background(), screens) + require.NoError(t, err) + + v, err := math.LegacyNewDecFromStr(pv.String()) + require.NoError(t, err) + + v1, err := math.LegacyNewDecFromStr(value.String()) + require.NoError(t, err) + + require.True(t, v.Equal(v1)) } diff --git a/tx/textual/valuerenderer/valuerenderer.go b/tx/textual/valuerenderer/valuerenderer.go index 5365747986..6cbfab2934 100644 --- a/tx/textual/valuerenderer/valuerenderer.go +++ b/tx/textual/valuerenderer/valuerenderer.go @@ -19,6 +19,9 @@ import ( // metadata. It is meant to be passed as an argument into `NewTextual`. type CoinMetadataQueryFn func(ctx context.Context, denom string) (*bankv1beta1.Metadata, error) +// ValueRendererCreator is a function returning a ValueRenderer. +type ValueRendererCreator func(protoreflect.FieldDescriptor) ValueRenderer + // Textual holds the configuration for dispatching // to specific value renderers for SIGN_MODE_TEXTUAL. type Textual struct { @@ -28,9 +31,9 @@ type Textual struct { // server-side code) or a gRPC query client (for client-side code). coinMetadataQuerier CoinMetadataQueryFn // scalars defines a registry for Cosmos scalars. - scalars map[string]ValueRenderer - // messages defines a registry for custom message renderers, as defined in - // point #9 in the spec. Note that we also use this same registry for the + scalars map[string]ValueRendererCreator + // messages defines a registry for custom message renderers. + // Note that we also use this same registry for the // following messages, as they can be thought of custom message rendering: // - SDK coin and coins // - Protobuf timestamp @@ -62,7 +65,7 @@ func (r *Textual) GetFieldValueRenderer(fd protoreflect.FieldDescriptor) (ValueR return nil, fmt.Errorf("got empty value renderer for scalar %s", scalar) } - return vr, nil + return vr(fd), nil } return NewStringValueRenderer(), nil @@ -74,7 +77,7 @@ func (r *Textual) GetFieldValueRenderer(fd protoreflect.FieldDescriptor) (ValueR fd.Kind() == protoreflect.Uint64Kind || fd.Kind() == protoreflect.Int32Kind || fd.Kind() == protoreflect.Int64Kind: - return NewIntValueRenderer(), nil + return NewIntValueRenderer(fd), nil case fd.Kind() == protoreflect.EnumKind: return NewEnumValueRenderer(fd), nil @@ -87,6 +90,7 @@ func (r *Textual) GetFieldValueRenderer(fd protoreflect.FieldDescriptor) (ValueR if found { return vr, nil } + if fd.IsMap() { return nil, fmt.Errorf("value renderers cannot format value of type map") } @@ -111,9 +115,9 @@ func (r *Textual) GetMessageValueRenderer(md protoreflect.MessageDescriptor) (Va func (r *Textual) init() { if r.scalars == nil { - r.scalars = map[string]ValueRenderer{} - r.scalars["cosmos.Int"] = NewIntValueRenderer() - r.scalars["cosmos.Dec"] = NewDecValueRenderer() + r.scalars = map[string]ValueRendererCreator{} + r.scalars["cosmos.Int"] = func(fd protoreflect.FieldDescriptor) ValueRenderer { return NewIntValueRenderer(fd) } + r.scalars["cosmos.Dec"] = func(_ protoreflect.FieldDescriptor) ValueRenderer { return NewDecValueRenderer() } } if r.messages == nil { r.messages = map[protoreflect.FullName]ValueRenderer{} @@ -125,7 +129,7 @@ func (r *Textual) init() { } // DefineScalar adds a value renderer to the given Cosmos scalar. -func (r *Textual) DefineScalar(scalar string, vr ValueRenderer) { +func (r *Textual) DefineScalar(scalar string, vr ValueRendererCreator) { r.init() r.scalars[scalar] = vr } diff --git a/tx/textual/valuerenderer/valuerenderer_test.go b/tx/textual/valuerenderer/valuerenderer_test.go index a499158d53..795cefc152 100644 --- a/tx/textual/valuerenderer/valuerenderer_test.go +++ b/tx/textual/valuerenderer/valuerenderer_test.go @@ -17,9 +17,9 @@ func TestDispatcher(t *testing.T) { expErr bool expValueRenderer valuerenderer.ValueRenderer }{ - {"UINT32", false, valuerenderer.NewIntValueRenderer()}, - {"UINT64", false, valuerenderer.NewIntValueRenderer()}, - {"SDKINT", false, valuerenderer.NewIntValueRenderer()}, + {"UINT32", false, valuerenderer.NewIntValueRenderer(fieldDescriptorFromName("UINT32"))}, + {"UINT64", false, valuerenderer.NewIntValueRenderer(fieldDescriptorFromName("UINT64"))}, + {"SDKINT", false, valuerenderer.NewIntValueRenderer(fieldDescriptorFromName("SDKINT"))}, {"SDKDEC", false, valuerenderer.NewDecValueRenderer()}, {"BYTES", false, valuerenderer.NewBytesValueRenderer()}, {"TIMESTAMP", false, valuerenderer.NewTimestampValueRenderer()},