diff --git a/.pending/features/sdk/Implement-coin-conversion-and-denomination-registration-utilities b/.pending/features/sdk/Implement-coin-conversion-and-denomination-registration-utilities new file mode 100644 index 000000000..4bd7e51e2 --- /dev/null +++ b/.pending/features/sdk/Implement-coin-conversion-and-denomination-registration-utilities @@ -0,0 +1 @@ +Implement coin conversion and denomination registration utilities diff --git a/types/coin.go b/types/coin.go index 5a8292a8f..d2e76d750 100644 --- a/types/coin.go +++ b/types/coin.go @@ -1,7 +1,6 @@ package types import ( - "errors" "fmt" "regexp" "sort" @@ -552,7 +551,7 @@ var ( func validateDenom(denom string) error { if !reDnm.MatchString(denom) { - return errors.New("illegal characters") + return fmt.Errorf("invalid denom: %s", denom) } return nil } diff --git a/types/denom.go b/types/denom.go new file mode 100644 index 000000000..73d396644 --- /dev/null +++ b/types/denom.go @@ -0,0 +1,64 @@ +package types + +import ( + "fmt" +) + +// denomUnits contains a mapping of denomination mapped to their respective unit +// multipliers (e.g. 1atom = 10^-6uatom). +var denomUnits = map[string]Dec{} + +// RegisterDenom registers a denomination with a corresponding unit. If the +// denomination is already registered, an error will be returned. +func RegisterDenom(denom string, unit Dec) error { + if err := validateDenom(denom); err != nil { + return err + } + + if _, ok := denomUnits[denom]; ok { + return fmt.Errorf("denom %s already registered", denom) + } + + denomUnits[denom] = unit + return nil +} + +// GetDenomUnit returns a unit for a given denomination if it exists. A boolean +// is returned if the denomination is registered. +func GetDenomUnit(denom string) (Dec, bool) { + if err := validateDenom(denom); err != nil { + return ZeroDec(), false + } + + unit, ok := denomUnits[denom] + if !ok { + return ZeroDec(), false + } + + return unit, true +} + +// ConvertCoin attempts to convert a coin to a given denomination. If the given +// denomination is invalid or if neither denomination is registered, an error +// is returned. +func ConvertCoin(coin Coin, denom string) (Coin, error) { + if err := validateDenom(denom); err != nil { + return Coin{}, err + } + + srcUnit, ok := GetDenomUnit(coin.Denom) + if !ok { + return Coin{}, fmt.Errorf("source denom not registered: %s", coin.Denom) + } + + dstUnit, ok := GetDenomUnit(denom) + if !ok { + return Coin{}, fmt.Errorf("destination denom not registered: %s", denom) + } + + if srcUnit.Equal(dstUnit) { + return NewCoin(denom, coin.Amount), nil + } + + return NewCoin(denom, coin.Amount.ToDec().Mul(srcUnit.Quo(dstUnit)).TruncateInt()), nil +} diff --git a/types/denom_test.go b/types/denom_test.go new file mode 100644 index 000000000..de6437994 --- /dev/null +++ b/types/denom_test.go @@ -0,0 +1,83 @@ +package types + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +var ( + atom = "atom" // 1 (base denom unit) + matom = "matom" // 10^-3 (milli) + uatom = "uatom" // 10^-6 (micro) + natom = "natom" // 10^-9 (nano) +) + +func TestRegisterDenom(t *testing.T) { + atomUnit := OneDec() // 1 (base denom unit) + + require.NoError(t, RegisterDenom(atom, atomUnit)) + require.Error(t, RegisterDenom(atom, atomUnit)) + + res, ok := GetDenomUnit(atom) + require.True(t, ok) + require.Equal(t, atomUnit, res) + + res, ok = GetDenomUnit(matom) + require.False(t, ok) + require.Equal(t, ZeroDec(), res) + + // reset registration + denomUnits = map[string]Dec{} +} + +func TestConvertCoins(t *testing.T) { + atomUnit := OneDec() // 1 (base denom unit) + require.NoError(t, RegisterDenom(atom, atomUnit)) + + matomUnit := NewDecWithPrec(1, 3) // 10^-3 (milli) + require.NoError(t, RegisterDenom(matom, matomUnit)) + + uatomUnit := NewDecWithPrec(1, 6) // 10^-6 (micro) + require.NoError(t, RegisterDenom(uatom, uatomUnit)) + + natomUnit := NewDecWithPrec(1, 9) // 10^-9 (nano) + require.NoError(t, RegisterDenom(natom, natomUnit)) + + testCases := []struct { + input Coin + denom string + result Coin + expErr bool + }{ + {NewCoin("foo", ZeroInt()), atom, Coin{}, true}, + {NewCoin(atom, ZeroInt()), "foo", Coin{}, true}, + {NewCoin(atom, ZeroInt()), "FOO", Coin{}, true}, + + {NewCoin(atom, NewInt(5)), matom, NewCoin(matom, NewInt(5000)), false}, // atom => matom + {NewCoin(atom, NewInt(5)), uatom, NewCoin(uatom, NewInt(5000000)), false}, // atom => uatom + {NewCoin(atom, NewInt(5)), natom, NewCoin(natom, NewInt(5000000000)), false}, // atom => natom + + {NewCoin(uatom, NewInt(5000000)), matom, NewCoin(matom, NewInt(5000)), false}, // uatom => matom + {NewCoin(uatom, NewInt(5000000)), natom, NewCoin(natom, NewInt(5000000000)), false}, // uatom => natom + {NewCoin(uatom, NewInt(5000000)), atom, NewCoin(atom, NewInt(5)), false}, // uatom => atom + + {NewCoin(matom, NewInt(5000)), natom, NewCoin(natom, NewInt(5000000000)), false}, // matom => natom + {NewCoin(matom, NewInt(5000)), uatom, NewCoin(uatom, NewInt(5000000)), false}, // matom => uatom + } + + for i, tc := range testCases { + res, err := ConvertCoin(tc.input, tc.denom) + require.Equal( + t, tc.expErr, err != nil, + "unexpected error; tc: #%d, input: %s, denom: %s", i+1, tc.input, tc.denom, + ) + require.Equal( + t, tc.result, res, + "invalid result; tc: #%d, input: %s, denom: %s", i+1, tc.input, tc.denom, + ) + } + + // reset registration + denomUnits = map[string]Dec{} +}