diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..30cf57e
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,10 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Ignored default folder with query files
+/queries/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/copilot.data.migration.ask2agent.xml b/.idea/copilot.data.migration.ask2agent.xml
new file mode 100644
index 0000000..1f2ea11
--- /dev/null
+++ b/.idea/copilot.data.migration.ask2agent.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/go-multicall.iml b/.idea/go-multicall.iml
new file mode 100644
index 0000000..5e764c4
--- /dev/null
+++ b/.idea/go-multicall.iml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/go.imports.xml b/.idea/go.imports.xml
new file mode 100644
index 0000000..644cdf0
--- /dev/null
+++ b/.idea/go.imports.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/golinter.xml b/.idea/golinter.xml
new file mode 100644
index 0000000..1ccf3ec
--- /dev/null
+++ b/.idea/golinter.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..c4b021f
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/call_test.go b/call_test.go
index c490245..a83900b 100644
--- a/call_test.go
+++ b/call_test.go
@@ -35,3 +35,19 @@ func TestCall_BadABI(t *testing.T) {
r.Error(err)
r.ErrorContains(err, "unexpected EOF")
}
+
+// TestUnpackResult_SafeTypeAssertion verifies UnpackResult never panics regardless of
+// the concrete type stored in Outputs.
+func TestUnpackResult_SafeTypeAssertion(t *testing.T) {
+ r := require.New(t)
+
+ // nil Outputs → nil
+ r.Nil((&Call{}).UnpackResult())
+
+ // struct pointer (common case) → nil, no panic
+ r.Nil((&Call{Outputs: new(struct{ Val bool })}).UnpackResult())
+
+ // []interface{} → returns the slice as-is
+ out := []interface{}{true, "hello"}
+ r.Equal(out, (&Call{Outputs: out}).UnpackResult())
+}
diff --git a/caller_test.go b/caller_test.go
index 5a7ef33..0c084e3 100644
--- a/caller_test.go
+++ b/caller_test.go
@@ -2,6 +2,7 @@ package multicall
import (
"context"
+ "fmt"
"math/big"
"testing"
@@ -102,6 +103,15 @@ func (ms *multicallStub) Aggregate3(opts *bind.CallOpts, calls []contract_multic
return
}
+// multicallResultStub allows full control over per-call Success flags and errors.
+type multicallResultStub struct {
+ results func(calls []contract_multicall.Multicall3Call3) ([]contract_multicall.Multicall3Result, error)
+}
+
+func (ms *multicallResultStub) Aggregate3(opts *bind.CallOpts, calls []contract_multicall.Multicall3Call3) ([]contract_multicall.Multicall3Result, error) {
+ return ms.results(calls)
+}
+
func TestCaller_TwoCalls(t *testing.T) {
r := require.New(t)
@@ -326,6 +336,136 @@ func TestDial(t *testing.T) {
r.NotNil(caller)
}
+// boolOut is a helper output struct used across CanFail tests.
+type boolOut struct {
+ Val1 bool
+}
+
+// validBoolData packs and strips the 4-byte selector, returning raw ABI-encoded output for true.
+func validBoolData(t *testing.T, c *Contract) []byte {
+ t.Helper()
+ packed, err := c.ABI.Pack("testFunc", true)
+ if err != nil {
+ t.Fatalf("Pack: %v", err)
+ }
+ return packed[4:]
+}
+
+// TestCaller_CanFail_BadReturnData verifies that when a CanFail call receives malformed
+// return data, only that call is marked Failed; other calls are unaffected and no error
+// is returned to the caller.
+func TestCaller_CanFail_BadReturnData(t *testing.T) {
+ r := require.New(t)
+
+ c1, err := NewContract(oneValueABI, testAddr1)
+ r.NoError(err)
+ c2, err := NewContract(oneValueABI, testAddr2)
+ r.NoError(err)
+
+ valid := validBoolData(t, c1)
+
+ call1 := c1.NewCall(new(boolOut), "testFunc", true)
+ call2 := c2.NewCall(new(boolOut), "testFunc", true).AllowFailure() // bad data below
+ call3 := c1.NewCall(new(boolOut), "testFunc", true)
+
+ caller := &Caller{
+ contract: &multicallResultStub{
+ results: func(calls []contract_multicall.Multicall3Call3) ([]contract_multicall.Multicall3Result, error) {
+ return []contract_multicall.Multicall3Result{
+ {Success: true, ReturnData: valid},
+ {Success: true, ReturnData: []byte{0xde, 0xad}}, // garbage — ABI decode fails
+ {Success: true, ReturnData: valid},
+ }, nil
+ },
+ },
+ }
+
+ calls, err := caller.Call(nil, call1, call2, call3)
+ r.NoError(err)
+ r.Len(calls, 3)
+
+ r.False(calls[0].Failed)
+ r.Equal(true, calls[0].Outputs.(*boolOut).Val1)
+
+ r.True(calls[1].Failed) // ABI decode failed, CanFail=true → marked Failed, no error
+
+ r.False(calls[2].Failed)
+ r.Equal(true, calls[2].Outputs.(*boolOut).Val1)
+}
+
+// TestCaller_CanFail_OnChainFailure verifies that when a CanFail call reverts on-chain
+// (Success=false), it is marked Failed and does not affect other calls.
+func TestCaller_CanFail_OnChainFailure(t *testing.T) {
+ r := require.New(t)
+
+ c, err := NewContract(oneValueABI, testAddr1)
+ r.NoError(err)
+
+ valid := validBoolData(t, c)
+
+ call1 := c.NewCall(new(boolOut), "testFunc", true)
+ call2 := c.NewCall(new(boolOut), "testFunc", true).AllowFailure()
+
+ caller := &Caller{
+ contract: &multicallResultStub{
+ results: func(calls []contract_multicall.Multicall3Call3) ([]contract_multicall.Multicall3Result, error) {
+ return []contract_multicall.Multicall3Result{
+ {Success: true, ReturnData: valid},
+ {Success: false, ReturnData: []byte{}}, // on-chain revert
+ }, nil
+ },
+ },
+ }
+
+ calls, err := caller.Call(nil, call1, call2)
+ r.NoError(err)
+ r.Len(calls, 2)
+
+ r.False(calls[0].Failed)
+ r.Equal(true, calls[0].Outputs.(*boolOut).Val1)
+
+ r.True(calls[1].Failed)
+}
+
+// TestCallChunked_ReturnsPartialOnError verifies that when a chunk fails, the results
+// from previously successful chunks are returned alongside the error.
+func TestCallChunked_ReturnsPartialOnError(t *testing.T) {
+ r := require.New(t)
+
+ c, err := NewContract(oneValueABI, testAddr1)
+ r.NoError(err)
+
+ valid := validBoolData(t, c)
+
+ call1 := c.NewCall(new(boolOut), "testFunc", true)
+ call2 := c.NewCall(new(boolOut), "testFunc", true)
+
+ callCount := 0
+ caller := &Caller{
+ contract: &multicallResultStub{
+ results: func(calls []contract_multicall.Multicall3Call3) ([]contract_multicall.Multicall3Result, error) {
+ callCount++
+ if callCount == 2 {
+ return nil, fmt.Errorf("rpc error on second chunk")
+ }
+ return []contract_multicall.Multicall3Result{
+ {Success: true, ReturnData: valid},
+ }, nil
+ },
+ },
+ }
+
+ // chunkSize=1: call1 → chunk 0 (succeeds), call2 → chunk 1 (fails)
+ result, err := caller.CallChunked(nil, 1, 0, call1, call2)
+ r.Error(err)
+ r.ErrorContains(err, "chunk [1]")
+
+ // partial results: only the successful first chunk is returned
+ r.Len(result, 1)
+ r.False(result[0].Failed)
+ r.Equal(true, result[0].Outputs.(*boolOut).Val1)
+}
+
func TestChunkInputs(t *testing.T) {
testCases := []struct {
name string