Skip to content

Commit 1e98a85

Browse files
committed
Refactor decode to use ff-signer
Signed-off-by: Dave Crighton <[email protected]>
1 parent 14c6206 commit 1e98a85

File tree

4 files changed

+193
-341
lines changed

4 files changed

+193
-341
lines changed

internal/ethereum/estimate_gas_test.go

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
package ethereum
1818

1919
import (
20-
"context"
2120
"encoding/hex"
2221
"encoding/json"
2322
"testing"
@@ -32,6 +31,12 @@ import (
3231
"github.com/stretchr/testify/mock"
3332
)
3433

34+
var testDefaultError = &abi.Entry{
35+
Type: abi.Error,
36+
Name: "Error",
37+
Inputs: abi.ParameterArray{{Type: "string"}},
38+
}
39+
3540
const sampleGasEstimate = `{
3641
"ffcapi": {
3742
"version": "v1.0.0",
@@ -150,7 +155,7 @@ func TestGasEstimateFailRevertReasonInData(t *testing.T) {
150155
ctx, c, mRPC, done := newTestConnector(t)
151156
defer done()
152157

153-
errData, err := defaultError.EncodeCallDataValues([]string{"this reason"})
158+
errData, err := testDefaultError.EncodeCallDataValues([]string{"this reason"})
154159
assert.NoError(t, err)
155160
mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_estimateGas",
156161
mock.MatchedBy(func(tx *ethsigner.Transaction) bool {
@@ -205,7 +210,7 @@ func TestGasEstimateFailThenRevertDataFromCall(t *testing.T) {
205210
ctx, c, mRPC, done := newTestConnector(t)
206211
defer done()
207212

208-
errData, err := defaultError.EncodeCallDataValues([]string{"this reason"})
213+
errData, err := testDefaultError.EncodeCallDataValues([]string{"this reason"})
209214
assert.NoError(t, err)
210215
mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_estimateGas",
211216
mock.MatchedBy(func(tx *ethsigner.Transaction) bool {
@@ -261,7 +266,7 @@ func TestGasEstimateFailCustomErrorCannotParse(t *testing.T) {
261266
ctx, c, mRPC, done := newTestConnector(t)
262267
defer done()
263268

264-
errData, err := defaultError.EncodeCallDataValues([]string{"this reason"})
269+
errData, err := testDefaultError.EncodeCallDataValues([]string{"this reason"})
265270
assert.NoError(t, err)
266271
mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_estimateGas",
267272
mock.MatchedBy(func(tx *ethsigner.Transaction) bool {
@@ -281,6 +286,3 @@ func TestGasEstimateFailCustomErrorCannotParse(t *testing.T) {
281286

282287
}
283288

284-
func TestFormatErrorComponentBadCV(t *testing.T) {
285-
assert.Equal(t, "?", formatErrorComponent(context.Background(), &abi.ComponentValue{}))
286-
}

internal/ethereum/exec_query.go

Lines changed: 6 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,8 @@
1717
package ethereum
1818

1919
import (
20-
"bytes"
2120
"context"
22-
"encoding/hex"
2321
"encoding/json"
24-
"fmt"
2522

2623
"github.com/hyperledger/firefly-common/pkg/fftypes"
2724
"github.com/hyperledger/firefly-common/pkg/i18n"
@@ -35,21 +32,6 @@ import (
3532
"github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi"
3633
)
3734

38-
var (
39-
// See https://docs.soliditylang.org/en/v0.8.14/control-structures.html#revert
40-
// There default error for `revert("some error")` is a function Error(string)
41-
defaultError = &abi.Entry{
42-
Type: abi.Error,
43-
Name: "Error",
44-
Inputs: abi.ParameterArray{
45-
{
46-
Type: "string",
47-
},
48-
},
49-
}
50-
defaultErrorID = defaultError.FunctionSelectorBytes()
51-
)
52-
5335
func (c *ethConnector) QueryInvoke(ctx context.Context, req *ffcapi.QueryInvokeRequest) (*ffcapi.QueryInvokeResponse, ffcapi.ErrorReason, error) {
5436
// Parse the input JSON data, to build the call data
5537
callData, method, err := c.prepareCallData(ctx, &req.TransactionInput)
@@ -165,126 +147,15 @@ func processRevertReason(ctx context.Context, outputData ethtypes.HexBytes0xPref
165147
// result in a multiple of 32 bytes) and has exactly 4 extra bytes for a function
166148
// signature
167149
if len(outputData)%32 == 4 {
168-
signature := outputData[0:4]
169-
if bytes.Equal(signature, defaultErrorID) {
170-
errorInfo, err := defaultError.DecodeCallDataCtx(ctx, outputData)
171-
if err == nil && len(errorInfo.Children) == 1 {
172-
if strError, ok := errorInfo.Children[0].Value.(string); ok {
173-
return unwrapNestedRevertReasons(ctx, strError, 0, errorAbis)
174-
}
175-
}
176-
log.L(ctx).Warnf("Invalid revert data: %s", outputData)
177-
} else if len(errorAbis) > 0 {
178-
// check if the signature matches any of the declared custom error definitions
179-
for _, e := range errorAbis {
180-
idBytes := e.FunctionSelectorBytes()
181-
if bytes.Equal(signature, idBytes) {
182-
err := formatCustomError(ctx, e, outputData)
183-
if err == "" {
184-
log.L(ctx).Warnf("Invalid revert data: %s", outputData)
185-
break
186-
}
187-
return err
188-
}
189-
}
150+
var errors abi.ABI
151+
for _, e := range errorAbis {
152+
errors = append(errors, e)
153+
}
154+
if result, ok := errors.UnwrapErrorStringCtx(ctx, outputData); ok {
155+
return result
190156
}
191-
// we call this "transient error" because it signals to the caller of the case
192-
// that the raw revert data is returned, then it gets thrown away. so no need to translate
193157
log.L(ctx).Debugf("Directly returning revert reason: %s", outputData)
194158
return outputData.String()
195159
}
196160
return ""
197161
}
198-
199-
const maxNestedRevertDepth = 10
200-
201-
// unwrapNestedRevertReasons handles Solidity contracts that catch a revert's raw bytes
202-
// and re-throw them inside a new Error(string) by doing string(reason). This produces
203-
// an Error(string) whose decoded "string" contains raw ABI-encoded error data
204-
// (including null bytes from ABI padding). We scan for all known error selectors
205-
// (Error(string) plus any custom errors from the ABI), decode the earliest match,
206-
// and recurse for nested Error(string) chains.
207-
func unwrapNestedRevertReasons(ctx context.Context, s string, depth int, errorAbis []*abi.Entry) string {
208-
if depth >= maxNestedRevertDepth {
209-
return sanitizeBinaryString([]byte(s))
210-
}
211-
212-
raw := []byte(s)
213-
214-
// Find the earliest occurrence of any known error selector
215-
bestIdx := -1
216-
var bestEntry *abi.Entry
217-
if idx := bytes.Index(raw, defaultErrorID); idx >= 0 {
218-
bestIdx = idx
219-
bestEntry = defaultError
220-
}
221-
for _, e := range errorAbis {
222-
sel := e.FunctionSelectorBytes()
223-
if idx := bytes.Index(raw, sel); idx >= 0 && (bestIdx < 0 || idx < bestIdx) {
224-
bestIdx = idx
225-
bestEntry = e
226-
}
227-
}
228-
229-
if bestIdx < 0 {
230-
return sanitizeBinaryString(raw)
231-
}
232-
233-
prefix := sanitizeBinaryString(raw[:bestIdx])
234-
embedded := raw[bestIdx:]
235-
236-
if bestEntry == defaultError {
237-
errorInfo, err := defaultError.DecodeCallDataCtx(ctx, embedded)
238-
if err == nil && len(errorInfo.Children) == 1 {
239-
if nested, ok := errorInfo.Children[0].Value.(string); ok {
240-
return prefix + unwrapNestedRevertReasons(ctx, nested, depth+1, errorAbis)
241-
}
242-
}
243-
} else {
244-
formatted := formatCustomError(ctx, bestEntry, embedded)
245-
if formatted != "" {
246-
return prefix + formatted
247-
}
248-
}
249-
250-
log.L(ctx).Debugf("Could not decode nested revert at depth %d, hex-encoding remaining %d bytes", depth, len(embedded))
251-
return prefix + "0x" + hex.EncodeToString(embedded)
252-
}
253-
254-
// sanitizeBinaryString returns the input as a text string if it is entirely
255-
// printable ASCII, or hex-encodes the entire input otherwise. This all-or-nothing
256-
// approach avoids guessing where "readable" ends in an ambiguous binary blob.
257-
func sanitizeBinaryString(raw []byte) string {
258-
for _, b := range raw {
259-
if b < 32 || b >= 127 {
260-
return "0x" + hex.EncodeToString(raw)
261-
}
262-
}
263-
return string(raw)
264-
}
265-
266-
func formatCustomError(ctx context.Context, e *abi.Entry, outputData ethtypes.HexBytes0xPrefix) string {
267-
errorInfo, err := e.DecodeCallDataCtx(ctx, outputData)
268-
if err == nil {
269-
strError := fmt.Sprintf("%s(", e.Name)
270-
for i, child := range errorInfo.Children {
271-
strError += formatErrorComponent(ctx, child)
272-
if i < len(errorInfo.Children)-1 {
273-
strError += ", "
274-
}
275-
}
276-
strError += ")"
277-
return strError
278-
}
279-
return ""
280-
}
281-
282-
func formatErrorComponent(ctx context.Context, child *abi.ComponentValue) string {
283-
value, err := child.JSON()
284-
if err != nil {
285-
// if this part of the error structure failed to parse, simply append "?"
286-
log.L(ctx).Warnf("Failed to parse component value in error: %+v", child)
287-
return "?"
288-
}
289-
return string(value)
290-
}

0 commit comments

Comments
 (0)