diff --git a/amount/main.go b/amount/main.go index 717f7248dc..b1b7bc1474 100644 --- a/amount/main.go +++ b/amount/main.go @@ -13,6 +13,7 @@ import ( "math/big" "regexp" "strconv" + "strings" "github.com/stellar/go/support/errors" "github.com/stellar/go/xdr" @@ -32,7 +33,8 @@ var ( // to `big.Rat.SetString` triggering long calculations. // Note: {1,20} because the biggest amount you can use in Stellar is: // len("922337203685.4775807") = 20. - validAmountSimple = regexp.MustCompile("^-?[.0-9]{1,20}$") + validAmountSimple = regexp.MustCompile("^-?[.0-9]{1,20}$") + negativePositiveNumberOnly = regexp.MustCompile("^-?[0-9]+$") ) // MustParse is the panicking version of Parse. @@ -85,18 +87,29 @@ func ParseInt64(v string) (int64, error) { // and returns the string representation of that number. // It is safe to use with values exceeding int64 limits. func IntStringToAmount(v string) (string, error) { - if !validAmountSimple.MatchString(v) { + if !negativePositiveNumberOnly.MatchString(v) { return "", errors.Errorf("invalid amount format: %s", v) } - r := &big.Rat{} - if _, ok := r.SetString(v); !ok { - return "", errors.Errorf("cannot parse amount: %s", v) + negative := false + if v[0] == '-' { + negative = true + v = v[1:] } - r.Quo(r, bigOne) + l := len(v) + var r string + if l <= 7 { + r = "0." + strings.Repeat("0", 7-l) + v + } else { + r = v[0:l-7] + "." + v[l-7:l] + } + + if negative { + r = "-" + r + } - return r.FloatString(7), nil + return r, nil } // String returns an "amount string" from the provided raw xdr.Int64 value `v`. diff --git a/amount/main_test.go b/amount/main_test.go index 8a6e1700f9..736c88f82f 100644 --- a/amount/main_test.go +++ b/amount/main_test.go @@ -87,9 +87,15 @@ func TestIntStringToAmount(t *testing.T) { {"-92233.7203686", "-922337203686", true}, {"1000000000000.0000000", "10000000000000000000", true}, {"0.0000000", "0", true}, + // Expensive inputs when using big.Rat: + {"10000000000000.0000000", "1" + strings.Repeat("0", 20), true}, + {"-10000000000000.0000000", "-1" + strings.Repeat("0", 20), true}, + {"1" + strings.Repeat("0", 1000-7) + ".0000000", "1" + strings.Repeat("0", 1000), true}, + {"1" + strings.Repeat("0", 1000000-7) + ".0000000", "1" + strings.Repeat("0", 1000000), true}, + // Invalid inputs {"", "nan", false}, - // Expensive inputs: - {"", strings.Repeat("1", 1000000), false}, + {"", "", false}, + {"", "-", false}, {"", "1E9223372036854775807", false}, {"", "1e9223372036854775807", false}, {"", "Inf", false}, diff --git a/services/horizon/internal/actions_assets.go b/services/horizon/internal/actions_assets.go index 7aec4ef2f3..383df3e127 100644 --- a/services/horizon/internal/actions_assets.go +++ b/services/horizon/internal/actions_assets.go @@ -3,10 +3,10 @@ package horizon import ( "fmt" + "github.com/stellar/go/protocols/horizon" "github.com/stellar/go/services/horizon/internal/db2" "github.com/stellar/go/services/horizon/internal/db2/assets" "github.com/stellar/go/services/horizon/internal/resourceadapter" - "github.com/stellar/go/protocols/horizon" "github.com/stellar/go/support/render/hal" ) @@ -71,7 +71,11 @@ func (action *AssetsAction) loadRecords() { func (action *AssetsAction) loadPage() { for _, record := range action.Records { var res horizon.AssetStat - resourceadapter.PopulateAssetStat(action.R.Context(), &res, record) + err := resourceadapter.PopulateAssetStat(action.R.Context(), &res, record) + if err != nil { + action.Err = err + return + } action.Page.Add(res) } diff --git a/services/horizon/internal/resourceadapter/asset_stat.go b/services/horizon/internal/resourceadapter/asset_stat.go index 02486463a1..8a5745c456 100644 --- a/services/horizon/internal/resourceadapter/asset_stat.go +++ b/services/horizon/internal/resourceadapter/asset_stat.go @@ -4,10 +4,11 @@ import ( "context" "github.com/stellar/go/amount" - "github.com/stellar/go/services/horizon/internal/db2/assets" - "github.com/stellar/go/xdr" . "github.com/stellar/go/protocols/horizon" + "github.com/stellar/go/services/horizon/internal/db2/assets" + "github.com/stellar/go/support/errors" "github.com/stellar/go/support/render/hal" + "github.com/stellar/go/xdr" ) // PopulateAssetStat fills out the details @@ -17,13 +18,12 @@ func PopulateAssetStat( res *AssetStat, row assets.AssetStatsR, ) (err error) { - res.Asset.Type = row.Type res.Asset.Code = row.Code res.Asset.Issuer = row.Issuer res.Amount, err = amount.IntStringToAmount(row.Amount) if err != nil { - return err + return errors.Wrap(err, "Invalid amount in PopulateAssetStat") } res.NumAccounts = row.NumAccounts res.Flags = AccountFlags{ diff --git a/services/horizon/internal/resourceadapter/asset_stat_test.go b/services/horizon/internal/resourceadapter/asset_stat_test.go new file mode 100644 index 0000000000..087f98d291 --- /dev/null +++ b/services/horizon/internal/resourceadapter/asset_stat_test.go @@ -0,0 +1,33 @@ +package resourceadapter + +import ( + "context" + "testing" + + protocol "github.com/stellar/go/protocols/horizon" + "github.com/stellar/go/services/horizon/internal/db2/assets" + "github.com/stretchr/testify/assert" +) + +func TestLargeAmount(t *testing.T) { + row := assets.AssetStatsR{ + SortKey: "", + Type: "credit_alphanum4", + Code: "XIM", + Issuer: "GBZ35ZJRIKJGYH5PBKLKOZ5L6EXCNTO7BKIL7DAVVDFQ2ODJEEHHJXIM", + Amount: "100000000000000000000", // 10T + NumAccounts: 429, + Flags: 0, + Toml: "https://xim.com/.well-known/stellar.toml", + } + var res protocol.AssetStat + err := PopulateAssetStat(context.Background(), &res, row) + assert.NoError(t, err) + + assert.Equal(t, "credit_alphanum4", res.Type) + assert.Equal(t, "XIM", res.Code) + assert.Equal(t, "GBZ35ZJRIKJGYH5PBKLKOZ5L6EXCNTO7BKIL7DAVVDFQ2ODJEEHHJXIM", res.Issuer) + assert.Equal(t, "10000000000000.0000000", res.Amount) + assert.Equal(t, int32(429), res.NumAccounts) + assert.Equal(t, "https://xim.com/.well-known/stellar.toml", res.Links.Toml.Href) +}