Skip to content

Commit 56bb894

Browse files
authored
fix(api): XDPoS_getRewardByAccount to be able to parse scientific notation (#2301)
* fix api XDPoS_getRewardByAccount to be able to parse scientific notation in json file added unit tests formatting * apply copilot suggestions
1 parent 00be1f5 commit 56bb894

2 files changed

Lines changed: 128 additions & 12 deletions

File tree

consensus/XDPoS/api.go

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -614,20 +614,43 @@ func getEpochReward(account common.Address, header *types.Header) (AccountEpochR
614614
return epochReward, nil
615615
}
616616

617+
// jsonNumberToBigInt parses a json.Number into a *big.Int, handling both plain
618+
// decimal strings (e.g. "4500000000000000000") and scientific notation
619+
// (e.g. "4.5e+21") that big.Int.SetString cannot parse directly.
620+
func jsonNumberToBigInt(n json.Number) (*big.Int, bool) {
621+
s := n.String()
622+
// Try plain integer first — the common case.
623+
if i, ok := new(big.Int).SetString(s, 10); ok {
624+
return i, true
625+
}
626+
// Fall back to big.Float to handle scientific notation.
627+
f, _, err := new(big.Float).SetPrec(256).Parse(s, 10)
628+
if err != nil {
629+
log.Warn("[jsonNumberToBigInt] Failed to parse json.Number:", "number", s, "err", err)
630+
return nil, false
631+
}
632+
633+
i, acc := f.Int(nil)
634+
if acc != big.Exact {
635+
// The value had a fractional part; truncate is the best we can do
636+
log.Warn("[jsonNumberToBigInt] json.Number is not an exact integer value", "number", s, "truncated", i.String(), "accuracy", acc)
637+
}
638+
639+
return i, true
640+
}
641+
617642
func (rewardObj *AccountEpochReward) getRewardAndStatus(account string, data map[string]interface{}) {
618643
if signersData, exists := data["signers"]; exists {
619644
if accountData, ok := signersData.(map[string]interface{})[account]; ok {
620645
nodeReward := accountData.(map[string]interface{})["reward"]
621646
delegatedReward := data["rewards"].(map[string]interface{})[account]
622647
rewardObj.AccountStatus = statusMasternode
623-
nodeRewardBigInt, ok := new(big.Int).SetString(nodeReward.(json.Number).String(), 10)
624-
if ok {
648+
if nodeRewardBigInt, ok := jsonNumberToBigInt(nodeReward.(json.Number)); ok {
625649
rewardObj.AccountReward = nodeRewardBigInt
626650
}
627651

628652
for k, v := range delegatedReward.(map[string]interface{}) {
629-
delegatedBigInt, ok := new(big.Int).SetString(v.(json.Number).String(), 10)
630-
if ok {
653+
if delegatedBigInt, ok := jsonNumberToBigInt(v.(json.Number)); ok {
631654
rewardObj.DelegatedReward[k] = delegatedBigInt
632655
}
633656
}
@@ -640,14 +663,12 @@ func (rewardObj *AccountEpochReward) getRewardAndStatus(account string, data map
640663
nodeReward := accountData.(map[string]interface{})["reward"]
641664
delegatedReward := data["rewardsProtector"].(map[string]interface{})[account]
642665
rewardObj.AccountStatus = statusProtectornode
643-
nodeRewardBigInt, successSetNodeReward := new(big.Int).SetString(nodeReward.(json.Number).String(), 10)
644-
if successSetNodeReward {
666+
if nodeRewardBigInt, ok := jsonNumberToBigInt(nodeReward.(json.Number)); ok {
645667
rewardObj.AccountReward = nodeRewardBigInt
646668
}
647669

648670
for k, v := range delegatedReward.(map[string]interface{}) {
649-
delegatedBigInt, successSetDelegatedReward := new(big.Int).SetString(v.(json.Number).String(), 10)
650-
if successSetDelegatedReward {
671+
if delegatedBigInt, ok := jsonNumberToBigInt(v.(json.Number)); ok {
651672
rewardObj.DelegatedReward[k] = delegatedBigInt
652673
}
653674
}
@@ -660,14 +681,12 @@ func (rewardObj *AccountEpochReward) getRewardAndStatus(account string, data map
660681
nodeReward := accountData.(map[string]interface{})["reward"]
661682
delegatedReward := data["rewardsObserver"].(map[string]interface{})[account]
662683
rewardObj.AccountStatus = statusObservernode
663-
nodeRewardBigInt, successSetNodeReward := new(big.Int).SetString(nodeReward.(json.Number).String(), 10)
664-
if successSetNodeReward {
684+
if nodeRewardBigInt, ok := jsonNumberToBigInt(nodeReward.(json.Number)); ok {
665685
rewardObj.AccountReward = nodeRewardBigInt
666686
}
667687

668688
for k, v := range delegatedReward.(map[string]interface{}) {
669-
delegatedBigInt, successSetDelegatedReward := new(big.Int).SetString(v.(json.Number).String(), 10)
670-
if successSetDelegatedReward {
689+
if delegatedBigInt, ok := jsonNumberToBigInt(v.(json.Number)); ok {
671690
rewardObj.DelegatedReward[k] = delegatedBigInt
672691
}
673692
}

consensus/XDPoS/api_test.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
package XDPoS
22

33
import (
4+
"encoding/json"
45
"math/big"
56
"testing"
67

78
"github.com/XinFinOrg/XDPoSChain/common"
89
"github.com/XinFinOrg/XDPoSChain/consensus/XDPoS/utils"
910
"github.com/XinFinOrg/XDPoSChain/core/types"
1011
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
1113
)
1214

1315
func TestCalculateSignersVote(t *testing.T) {
@@ -69,3 +71,98 @@ func TestCalculateSignersTimeout(t *testing.T) {
6971
calculateSigners(info, timeouts.Get(), masternodes)
7072
assert.Equal(t, info["10:450"].CurrentNumber, 2)
7173
}
74+
75+
func TestJsonNumberToBigInt(t *testing.T) {
76+
tests := []struct {
77+
name string
78+
input json.Number
79+
want *big.Int
80+
wantOk bool
81+
}{
82+
{
83+
name: "plain decimal integer",
84+
input: json.Number("4500000000000000000000"),
85+
want: new(big.Int).Mul(big.NewInt(45), new(big.Int).Exp(big.NewInt(10), big.NewInt(20), nil)),
86+
wantOk: true,
87+
},
88+
{
89+
name: "scientific notation 4.5e+21",
90+
input: json.Number("4.5e+21"),
91+
want: new(big.Int).Mul(big.NewInt(45), new(big.Int).Exp(big.NewInt(10), big.NewInt(20), nil)),
92+
wantOk: true,
93+
},
94+
{
95+
name: "scientific notation 1e+18",
96+
input: json.Number("1e+18"),
97+
want: new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil),
98+
wantOk: true,
99+
},
100+
{
101+
name: "scientific notation uppercase E",
102+
input: json.Number("4.5E+21"),
103+
want: new(big.Int).Mul(big.NewInt(45), new(big.Int).Exp(big.NewInt(10), big.NewInt(20), nil)),
104+
wantOk: true,
105+
},
106+
{
107+
name: "zero",
108+
input: json.Number("0"),
109+
want: big.NewInt(0),
110+
wantOk: true,
111+
},
112+
{
113+
name: "small integer",
114+
input: json.Number("12345"),
115+
want: big.NewInt(12345),
116+
wantOk: true,
117+
},
118+
{
119+
name: "fractional value truncates",
120+
input: json.Number("1.23e+1"),
121+
want: big.NewInt(12),
122+
wantOk: true,
123+
},
124+
{
125+
name: "decimal without exponent",
126+
input: json.Number("123.456"),
127+
want: big.NewInt(123),
128+
wantOk: true,
129+
},
130+
{
131+
name: "decimal whole number",
132+
input: json.Number("1000.0"),
133+
want: big.NewInt(1000),
134+
wantOk: true,
135+
},
136+
{
137+
name: "negative integer",
138+
input: json.Number("-500"),
139+
want: big.NewInt(-500),
140+
wantOk: true,
141+
},
142+
{
143+
name: "invalid string",
144+
input: json.Number("not_a_number"),
145+
want: nil,
146+
wantOk: false,
147+
},
148+
{
149+
name: "empty string",
150+
input: json.Number(""),
151+
want: nil,
152+
wantOk: false,
153+
},
154+
}
155+
156+
for _, tt := range tests {
157+
t.Run(tt.name, func(t *testing.T) {
158+
got, ok := jsonNumberToBigInt(tt.input)
159+
if tt.wantOk {
160+
require.True(t, ok, "input %q: parse failed, expected %s", tt.input, tt.want)
161+
assert.Equal(t, 0, tt.want.Cmp(got), "input %q: expected %s but got %s", tt.input, tt.want, got)
162+
} else {
163+
assert.False(t, ok, "input %q: expected parse failure but got %v", tt.input, got)
164+
assert.Nil(t, got, "input %q: expected nil but got %v", tt.input, got)
165+
}
166+
})
167+
}
168+
}

0 commit comments

Comments
 (0)