Skip to content

Commit 7d9b3e1

Browse files
authored
Format utility functions for consistency (#802)
This pull request introduces improvements to the representation and formatting of decoded parameters and field values in proposal analysis, with a focus on providing richer type information and enhanced formatting for numeric values. https://smartcontract-it.atlassian.net/browse/OPT-443
1 parent 30b67b6 commit 7d9b3e1

File tree

9 files changed

+218
-101
lines changed

9 files changed

+218
-101
lines changed

engine/cld/mcms/proposalanalysis/decoder/convert.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,14 +90,20 @@ func adaptNamedFields(fields []experimentalanalyzer.NamedField) DecodedParameter
9090

9191
params := make(DecodedParameters, len(fields))
9292
for i, field := range fields {
93-
ptype := ""
94-
if field.Value != nil {
93+
ptype := field.TypeName
94+
if ptype == "" && field.Value != nil {
9595
ptype = field.Value.GetType()
9696
}
97+
98+
value := any(field.Value)
99+
if value == nil {
100+
value = field.RawValue
101+
}
102+
97103
params[i] = &decodedParameter{
98104
name: field.Name,
99105
ptype: ptype,
100-
value: field.Value,
106+
value: value,
101107
rawValue: field.RawValue,
102108
}
103109
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package format
2+
3+
import (
4+
"math/big"
5+
"strings"
6+
)
7+
8+
// CommaGroupBigInt adds comma separators to a big.Int for readability.
9+
// E.g: 1000000 -> "1,000,000".
10+
func CommaGroupBigInt(n *big.Int) string {
11+
if n == nil {
12+
return "0"
13+
}
14+
15+
s := n.String()
16+
sign := ""
17+
if strings.HasPrefix(s, "-") {
18+
sign = "-"
19+
s = s[1:]
20+
}
21+
22+
if len(s) <= 3 {
23+
return sign + s
24+
}
25+
26+
var b strings.Builder
27+
b.WriteString(sign)
28+
for i, ch := range s {
29+
if i > 0 && (len(s)-i)%3 == 0 {
30+
b.WriteRune(',')
31+
}
32+
b.WriteRune(ch)
33+
}
34+
35+
return b.String()
36+
}
37+
38+
// FormatTokenAmount converts a raw token amount to a
39+
// human-readable decimal string using the token's decimals.
40+
func FormatTokenAmount(amount *big.Int, decimals uint8) string {
41+
if amount == nil || amount.Sign() == 0 {
42+
return "0"
43+
}
44+
45+
divisor := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(decimals)), nil)
46+
whole := new(big.Int).Div(amount, divisor)
47+
remainder := new(big.Int).Mod(amount, divisor)
48+
49+
if remainder.Sign() == 0 {
50+
return whole.String()
51+
}
52+
53+
fracStr := remainder.String()
54+
if len(fracStr) < int(decimals) {
55+
fracStr = strings.Repeat("0", int(decimals)-len(fracStr)) + fracStr
56+
}
57+
fracStr = strings.TrimRight(fracStr, "0")
58+
59+
return whole.String() + "." + fracStr
60+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package format
2+
3+
import (
4+
"math/big"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func mustBigInt(s string) *big.Int {
11+
n, ok := new(big.Int).SetString(s, 10)
12+
if !ok {
13+
panic("invalid big.Int: " + s)
14+
}
15+
16+
return n
17+
}
18+
19+
func TestCommaGroupBigInt(t *testing.T) {
20+
t.Parallel()
21+
22+
tests := []struct {
23+
name string
24+
input *big.Int
25+
expected string
26+
}{
27+
{"nil", nil, "0"},
28+
{"zero", big.NewInt(0), "0"},
29+
{"small", big.NewInt(42), "42"},
30+
{"hundreds", big.NewInt(999), "999"},
31+
{"thousands", big.NewInt(1000), "1,000"},
32+
{"millions", big.NewInt(1_000_000), "1,000,000"},
33+
{"wei", new(big.Int).Mul(big.NewInt(25), new(big.Int).Exp(big.NewInt(10), big.NewInt(17), nil)), "2,500,000,000,000,000,000"},
34+
{"negative", big.NewInt(-1234567), "-1,234,567"},
35+
}
36+
37+
for _, tt := range tests {
38+
t.Run(tt.name, func(t *testing.T) {
39+
t.Parallel()
40+
assert.Equal(t, tt.expected, CommaGroupBigInt(tt.input))
41+
})
42+
}
43+
}
44+
45+
func TestFormatTokenAmount(t *testing.T) {
46+
t.Parallel()
47+
48+
tests := []struct {
49+
name string
50+
amount *big.Int
51+
decimals uint8
52+
expected string
53+
}{
54+
{"nil", nil, 18, "0"},
55+
{"zero", big.NewInt(0), 18, "0"},
56+
{"6 decimals whole", big.NewInt(1_000_000), 6, "1"},
57+
{"6 decimals fraction", big.NewInt(1_500_000), 6, "1.5"},
58+
{"18 decimals large", new(big.Int).Mul(big.NewInt(1000), new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil)), 18, "1000"},
59+
{"18 decimals exact fraction", mustBigInt("2500000000000000000"), 18, "2.5"},
60+
{
61+
"exact precision beyond float64",
62+
mustBigInt("123456789012345678"),
63+
18,
64+
"0.123456789012345678",
65+
},
66+
{
67+
"small remainder with leading zeros",
68+
big.NewInt(1_000_001),
69+
6,
70+
"1.000001",
71+
},
72+
}
73+
74+
for _, tt := range tests {
75+
t.Run(tt.name, func(t *testing.T) {
76+
t.Parallel()
77+
assert.Equal(t, tt.expected, FormatTokenAmount(tt.amount, tt.decimals))
78+
})
79+
}
80+
}

engine/cld/mcms/proposalanalysis/renderer/funcmap.go

Lines changed: 35 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ import (
55
"encoding/json"
66
"fmt"
77
"math/big"
8+
"reflect"
89
"strconv"
910
"strings"
1011
"text/template"
1112

1213
chainutils "github.com/smartcontractkit/chainlink-deployments-framework/chain/utils"
1314
"github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/mcms/proposalanalysis/analyzer"
1415
"github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/mcms/proposalanalysis/analyzer/annotation"
16+
"github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/mcms/proposalanalysis/format"
1517
experimentalanalyzer "github.com/smartcontractkit/chainlink-deployments-framework/experimental/analyzer"
1618
)
1719

@@ -198,50 +200,55 @@ func formatStructField(sf experimentalanalyzer.StructField) string {
198200
return "{ " + strings.Join(parts, ", ") + " }"
199201
}
200202

201-
// commaGrouped adds comma separators to a numeric string for readability.
203+
// commaGrouped adds comma separators to a numeric value for readability.
202204
func commaGrouped(v any) string {
203-
var num *big.Int
205+
if n, ok := v.(json.Number); ok {
206+
v = string(n)
207+
}
208+
204209
switch val := v.(type) {
205210
case *big.Int:
206211
if val == nil {
207212
return nilValue
208213
}
209-
num = val
214+
215+
return format.CommaGroupBigInt(val)
210216
case string:
211-
var ok bool
212-
num, ok = new(big.Int).SetString(val, 10)
217+
num, ok := new(big.Int).SetString(val, 10)
213218
if !ok {
214219
return val
215220
}
221+
222+
return format.CommaGroupBigInt(num)
216223
default:
217-
num = new(big.Int)
218-
if _, err := fmt.Sscan(fmt.Sprintf("%v", v), num); err != nil {
219-
return fmt.Sprintf("%v", v)
224+
rv := reflect.ValueOf(v)
225+
if rv.CanInt() {
226+
return format.CommaGroupBigInt(big.NewInt(rv.Int()))
220227
}
221-
}
222228

223-
s := num.String()
224-
sign := ""
225-
if strings.HasPrefix(s, "-") {
226-
sign = "-"
227-
s = strings.TrimPrefix(s, "-")
228-
}
229-
if len(s) <= 3 {
230-
return sign + s
231-
}
229+
if rv.CanUint() {
230+
return format.CommaGroupBigInt(new(big.Int).SetUint64(rv.Uint()))
231+
}
232232

233-
var b strings.Builder
234-
if sign != "" {
235-
b.WriteString(sign)
236-
}
237-
for i, ch := range s {
238-
if i > 0 && (len(s)-i)%3 == 0 {
239-
b.WriteRune(',')
233+
if rv.CanFloat() {
234+
s := strconv.FormatFloat(rv.Float(), 'f', -1, 64)
235+
parts := strings.Split(s, ".")
236+
237+
num, ok := new(big.Int).SetString(parts[0], 10)
238+
if !ok {
239+
return s
240+
}
241+
242+
intPart := format.CommaGroupBigInt(num)
243+
if len(parts) == 2 {
244+
return intPart + "." + parts[1]
245+
}
246+
247+
return intPart
240248
}
241-
b.WriteRune(ch)
242-
}
243249

244-
return b.String()
250+
return formatValue(v)
251+
}
245252
}
246253

247254
func severitySymbol(severity any) string {

engine/cld/mcms/proposalanalysis/renderer/templates/markdown/annotations.tmpl

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ _Annotations:_
44
{{ range . -}}
55
{{- if not (isFrameworkAnnotation .Name) -}}
66
- {{ if .Name }}{{ .Name }}: {{ end }}{{ .Value }}
7-
{{- end -}}
8-
{{ end -}}
7+
{{ end }}{{- end -}}
98

109
{{- end -}}

engine/cld/mcms/proposalanalysis/renderer/testdata/golden_markdown.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
_Annotations:_
99
- batch.note: first batch
1010

11+
1112
#### Call 1
1213

1314
- [ ] **OnRamp v1.5.0** `setRateLimiterConfig`**warning** 🔴 risk: **high**
@@ -30,6 +31,7 @@ _Annotations:_
3031
- ccip.lane: ethereum -> arbitrum
3132

3233

34+
3335
#### Call 2
3436

3537
- [ ] **ERC20** `transfer`

experimental/analyzer/evm_tx_decoder.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ func (p *EVMTxCallDecoder) decodeMethodCall(address string, method *abi.Method,
6161
}
6262
inputs[i] = NamedField{
6363
Name: input.Name,
64+
TypeName: input.Type.String(),
6465
Value: p.decodeArg(input.Name, &input.Type, arg),
6566
RawValue: arg,
6667
}
@@ -73,6 +74,7 @@ func (p *EVMTxCallDecoder) decodeMethodCall(address string, method *abi.Method,
7374
}
7475
outputs[i] = NamedField{
7576
Name: output.Name,
77+
TypeName: output.Type.String(),
7678
Value: p.decodeArg(output.Name, &output.Type, out),
7779
RawValue: out,
7880
}
@@ -121,8 +123,9 @@ func (p *EVMTxCallDecoder) decodeStruct(argAbi *abi.Type, argVal any) StructFiel
121123
argFieldTyp := reflect.ValueOf(argVal).FieldByName(argFieldName)
122124
argument := p.decodeArg(argFieldName, argFieldAbi, argFieldTyp.Interface())
123125
fields[i] = NamedField{
124-
Name: argFieldName,
125-
Value: argument,
126+
Name: argFieldName,
127+
TypeName: argFieldAbi.String(),
128+
Value: argument,
126129
}
127130
}
128131

0 commit comments

Comments
 (0)