mirror of
https://github.com/luxfi/mcp.git
synced 2026-09-24 00:25:04 +00:00
Modules declare go 1.27.1 and the build images follow. go fix applied the modernizers: any, max and range over int. omitzero is not applied, since it changes what the JSON says. Co-authored-by: Hanzo Dev <[email protected]>
120 lines
4.6 KiB
Go
120 lines
4.6 KiB
Go
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package evmread
|
|
|
|
import (
|
|
"context"
|
|
"math/big"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
|
|
gethereum "github.com/luxfi/geth"
|
|
"github.com/luxfi/geth/common"
|
|
"github.com/luxfi/geth/core/types"
|
|
)
|
|
|
|
// countingCaller records CallContract invocations and returns a fixed payload. It
|
|
// implements the four read methods of Caller and nothing else — the read-only surface.
|
|
type countingCaller struct {
|
|
calls atomic.Int64
|
|
ret []byte
|
|
}
|
|
|
|
func (c *countingCaller) CallContract(_ context.Context, _ gethereum.CallMsg, _ *big.Int) ([]byte, error) {
|
|
c.calls.Add(1)
|
|
return c.ret, nil
|
|
}
|
|
func (c *countingCaller) ChainID(context.Context) (*big.Int, error) { return big.NewInt(1), nil }
|
|
func (c *countingCaller) BlockNumber(context.Context) (uint64, error) { return 1, nil }
|
|
func (c *countingCaller) HeaderByNumber(context.Context, *big.Int) (*types.Header, error) {
|
|
return &types.Header{Number: big.NewInt(1)}, nil
|
|
}
|
|
|
|
// TestNewContractRejectsBadABI proves NewContract surfaces a parse error for malformed
|
|
// ABI JSON rather than returning a half-built contract.
|
|
func TestNewContractRejectsBadABI(t *testing.T) {
|
|
if _, err := NewContract("this is not json", common.Address{}); err == nil {
|
|
t.Fatal("NewContract accepted malformed ABI JSON")
|
|
}
|
|
// A well-formed ABI binds without error.
|
|
const goodABI = `[{"type":"function","stateMutability":"view","name":"x","inputs":[],"outputs":[{"name":"","type":"uint256"}]}]`
|
|
if _, err := NewContract(goodABI, common.Address{}); err != nil {
|
|
t.Fatalf("NewContract rejected valid ABI: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestBoundedEnforcesCeiling proves Bounded allows exactly `max` CallContract calls then
|
|
// fails the (max+1)th with a ceiling error, while passing the non-amplifying reads
|
|
// through unchanged.
|
|
func TestBoundedEnforcesCeiling(t *testing.T) {
|
|
const max = 4
|
|
cc := &countingCaller{ret: common.LeftPadBytes(big.NewInt(7).Bytes(), 32)}
|
|
b := NewBounded(cc, max)
|
|
|
|
for i := range max {
|
|
if _, err := b.CallContract(context.Background(), gethereum.CallMsg{To: &common.Address{}}, nil); err != nil {
|
|
t.Fatalf("call %d under the ceiling errored: %v", i, err)
|
|
}
|
|
}
|
|
// The (max+1)th call must fail with a ceiling error.
|
|
_, err := b.CallContract(context.Background(), gethereum.CallMsg{To: &common.Address{}}, nil)
|
|
if err == nil {
|
|
t.Fatal("expected a ceiling error on the (max+1)th call, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), "ceiling") {
|
|
t.Fatalf("error %q is not the ceiling error", err.Error())
|
|
}
|
|
// The underlying caller was hit exactly `max` times (the over-limit call short-circuits).
|
|
if got := cc.calls.Load(); got != int64(max) {
|
|
t.Fatalf("underlying caller hit %d times, want %d (over-limit call must not reach it)", got, max)
|
|
}
|
|
if got := b.Calls(); got != int64(max+1) {
|
|
t.Fatalf("Bounded.Calls()=%d, want %d (counts the rejected attempt)", got, max+1)
|
|
}
|
|
|
|
// Non-amplifying reads are forwarded regardless of the ceiling.
|
|
if _, err := b.ChainID(context.Background()); err != nil {
|
|
t.Fatalf("ChainID passthrough errored: %v", err)
|
|
}
|
|
if _, err := b.BlockNumber(context.Background()); err != nil {
|
|
t.Fatalf("BlockNumber passthrough errored: %v", err)
|
|
}
|
|
if _, err := b.HeaderByNumber(context.Background(), nil); err != nil {
|
|
t.Fatalf("HeaderByNumber passthrough errored: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestBoundedZeroMaxDisablesCeiling proves max <= 0 is unbounded passthrough.
|
|
func TestBoundedZeroMaxDisablesCeiling(t *testing.T) {
|
|
cc := &countingCaller{ret: common.LeftPadBytes(big.NewInt(1).Bytes(), 32)}
|
|
b := NewBounded(cc, 0)
|
|
for i := range 1000 {
|
|
if _, err := b.CallContract(context.Background(), gethereum.CallMsg{To: &common.Address{}}, nil); err != nil {
|
|
t.Fatalf("unbounded call %d errored: %v", i, err)
|
|
}
|
|
}
|
|
if got := cc.calls.Load(); got != 1000 {
|
|
t.Fatalf("unbounded caller hit %d times, want 1000", got)
|
|
}
|
|
}
|
|
|
|
// TestCallStructUnpacksSingleReturn proves CallStruct decodes a single uint256 return
|
|
// into a one-field path via the wrapper-struct mechanism geth requires.
|
|
func TestCallStructUnpacksSingleReturn(t *testing.T) {
|
|
const oneUint = `[{"type":"function","stateMutability":"view","name":"v","inputs":[],"outputs":[{"name":"","type":"uint256"}]}]`
|
|
c, err := NewContract(oneUint, common.Address{})
|
|
if err != nil {
|
|
t.Fatalf("NewContract: %v", err)
|
|
}
|
|
cc := &countingCaller{ret: common.LeftPadBytes(big.NewInt(42).Bytes(), 32)}
|
|
got, err := CallStruct[*big.Int](context.Background(), c, cc, "v")
|
|
if err != nil {
|
|
t.Fatalf("CallStruct: %v", err)
|
|
}
|
|
if got.Cmp(big.NewInt(42)) != 0 {
|
|
t.Fatalf("CallStruct returned %s, want 42", got)
|
|
}
|
|
}
|