Restructured project for V2 refactor: **Structure Changes:** - Moved all V1 code to orig/ folder (preserved with git mv) - Created docs/planning/ directory - Added orig/README_V1.md explaining V1 preservation **Planning Documents:** - 00_V2_MASTER_PLAN.md: Complete architecture overview - Executive summary of critical V1 issues - High-level component architecture diagrams - 5-phase implementation roadmap - Success metrics and risk mitigation - 07_TASK_BREAKDOWN.md: Atomic task breakdown - 99+ hours of detailed tasks - Every task < 2 hours (atomic) - Clear dependencies and success criteria - Organized by implementation phase **V2 Key Improvements:** - Per-exchange parsers (factory pattern) - Multi-layer strict validation - Multi-index pool cache - Background validation pipeline - Comprehensive observability **Critical Issues Addressed:** - Zero address tokens (strict validation + cache enrichment) - Parsing accuracy (protocol-specific parsers) - No audit trail (background validation channel) - Inefficient lookups (multi-index cache) - Stats disconnection (event-driven metrics) Next Steps: 1. Review planning documents 2. Begin Phase 1: Foundation (P1-001 through P1-010) 3. Implement parsers in Phase 2 4. Build cache system in Phase 3 5. Add validation pipeline in Phase 4 6. Migrate and test in Phase 5 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
176 lines
5.1 KiB
Go
176 lines
5.1 KiB
Go
package calldata
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
type payloadFixture struct {
|
|
Protocol string `json:"protocol"`
|
|
Function string `json:"function"`
|
|
FunctionSig string `json:"function_sig"`
|
|
InputData string `json:"input_data"`
|
|
}
|
|
|
|
func loadPayload(t *testing.T, name string) payloadFixture {
|
|
t.Helper()
|
|
fullPath := filepath.Join("testdata", "payloads", name)
|
|
raw, err := os.ReadFile(fullPath)
|
|
if err != nil {
|
|
t.Fatalf("failed to read payload fixture %s: %v", name, err)
|
|
}
|
|
var pf payloadFixture
|
|
if err := json.Unmarshal(raw, &pf); err != nil {
|
|
t.Fatalf("failed to unmarshal payload %s: %v", name, err)
|
|
}
|
|
return pf
|
|
}
|
|
|
|
func decodeDirect(raw []byte, ctx *MulticallContext) []*SwapCall {
|
|
validator := getAddressValidator()
|
|
return decodeSwapCallRecursive(raw, ctx, validator, 0)
|
|
}
|
|
|
|
func TestCapturedPayloadDecoding(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
fixture string
|
|
expectSwaps int
|
|
expectSelector string
|
|
tokenIn string
|
|
tokenOut string
|
|
verifyProtocol string
|
|
isMulticall bool
|
|
}{
|
|
{
|
|
name: "uniswap_v3_exact_output_single",
|
|
fixture: "uniswapv3_exact_output_single.json",
|
|
expectSwaps: 1,
|
|
expectSelector: "db3e2198",
|
|
tokenIn: "0xff970a61a04b1ca14834a43f5de4533ebddb5cc8",
|
|
tokenOut: "0x912ce59144191c1204e64559fe8253a0e49e6548",
|
|
verifyProtocol: "UniswapV3",
|
|
},
|
|
{
|
|
name: "uniswap_v3_exact_input_single",
|
|
fixture: "uniswapv3_exact_input_single.json",
|
|
expectSwaps: 1,
|
|
expectSelector: "414bf389",
|
|
tokenIn: "0x82af49447d8a07e3bd95bd0d56f35241523fbab1",
|
|
tokenOut: "0x440017a1b021006d556d7fc06a54c32e42eb745b",
|
|
verifyProtocol: "UniswapV3",
|
|
},
|
|
{
|
|
name: "uniswap_v2_swap_exact_tokens",
|
|
fixture: "uniswapv2_exact_tokens.json",
|
|
expectSwaps: 1,
|
|
expectSelector: "38ed1739",
|
|
tokenIn: "0x03f6921f6e948016631ce796331294d5a863a9ee",
|
|
tokenOut: "0xdcc9691793633176acf5cfdc1a658cb3b982e2fb",
|
|
verifyProtocol: "UniswapV2",
|
|
},
|
|
{
|
|
name: "multicall_without_recognised_swaps",
|
|
fixture: "multicall_uniswap.json",
|
|
expectSwaps: 0,
|
|
isMulticall: true,
|
|
},
|
|
{
|
|
name: "uniswap_v3_decrease_liquidity",
|
|
fixture: "uniswapv3_decrease_liquidity.json",
|
|
expectSwaps: 0,
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
payload := loadPayload(t, tc.fixture)
|
|
raw, err := hex.DecodeString(strings.TrimPrefix(payload.InputData, "0x"))
|
|
if err != nil {
|
|
t.Fatalf("failed to decode input data: %v", err)
|
|
}
|
|
if len(raw) < 4 {
|
|
t.Fatalf("payload too short: %d bytes", len(raw))
|
|
}
|
|
|
|
ctx := &MulticallContext{Protocol: payload.Protocol}
|
|
var swaps []*SwapCall
|
|
if tc.isMulticall {
|
|
swaps, err = DecodeSwapCallsFromMulticall(raw[4:], ctx)
|
|
if err != nil {
|
|
t.Fatalf("multicall decode failed: %v", err)
|
|
}
|
|
} else {
|
|
swaps = decodeDirect(raw, ctx)
|
|
}
|
|
|
|
if len(swaps) != tc.expectSwaps {
|
|
selector := strings.ToLower(hex.EncodeToString(raw[:4]))
|
|
if !tc.isMulticall {
|
|
// attempt direct decode based on selector for diagnostics
|
|
var diag *SwapCall
|
|
switch selector {
|
|
case "414bf389":
|
|
diag = decodeExactInputSingle(raw[4:], ctx)
|
|
case "db3e2198":
|
|
diag = decodeExactInput(raw[4:], ctx)
|
|
case "38ed1739":
|
|
diag = decodeUniswapV2Swap(selector, raw[4:], ctx)
|
|
}
|
|
if diag != nil {
|
|
t.Fatalf("expected %d swaps, got %d (selector=%s, diag=%s)", tc.expectSwaps, len(swaps), selector, diag.String())
|
|
}
|
|
}
|
|
t.Fatalf("expected %d swaps, got %d (selector=%s)", tc.expectSwaps, len(swaps), selector)
|
|
}
|
|
|
|
if tc.expectSwaps == 0 {
|
|
return
|
|
}
|
|
|
|
swap := swaps[0]
|
|
if !selectorsMatch(swap.Selector, tc.expectSelector) {
|
|
t.Fatalf("expected selector %s, got %s", tc.expectSelector, swap.Selector)
|
|
}
|
|
if !strings.EqualFold(swap.TokenIn.Hex(), tc.tokenIn) {
|
|
t.Fatalf("expected tokenIn %s, got %s", tc.tokenIn, swap.TokenIn.Hex())
|
|
}
|
|
if !strings.EqualFold(swap.TokenOut.Hex(), tc.tokenOut) {
|
|
t.Fatalf("expected tokenOut %s, got %s", tc.tokenOut, swap.TokenOut.Hex())
|
|
}
|
|
if tc.verifyProtocol != "" && !strings.EqualFold(swap.Protocol, tc.verifyProtocol) {
|
|
t.Fatalf("expected protocol %s, got %s", tc.verifyProtocol, swap.Protocol)
|
|
}
|
|
|
|
if selectorEquals(tc.expectSelector, "db3e2198") {
|
|
if swap.AmountOut == nil || swap.AmountOut.Sign() == 0 {
|
|
t.Fatalf("expected non-zero amountOut for exactOutputSingle")
|
|
}
|
|
}
|
|
|
|
if selectorEquals(tc.expectSelector, "414bf389") || selectorEquals(tc.expectSelector, "38ed1739") {
|
|
if swap.AmountIn == nil || swap.AmountIn.Sign() == 0 {
|
|
t.Fatalf("expected non-zero amountIn for selector %s", tc.expectSelector)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func normalizeSelectorHex(sel string) string {
|
|
s := strings.TrimSpace(strings.ToLower(sel))
|
|
return strings.TrimPrefix(s, "0x")
|
|
}
|
|
|
|
func selectorEquals(a, b string) bool {
|
|
return normalizeSelectorHex(a) == normalizeSelectorHex(b)
|
|
}
|
|
|
|
func selectorsMatch(actual, expected string) bool {
|
|
return selectorEquals(actual, expected)
|
|
}
|