package ante import ( "math/big" "testing" "time" ) // ============================== // OFAC Tests // ============================== func TestOFAC_BlocksSanctionedSender(t *testing.T) { h := NewComplianceAnteHandler() h.AddToOFACList("0xSanctioned") result := h.AnteHandle("0xSanctioned", "0xNormal", big.NewInt(1000)) if result.Allowed { t.Fatal("should block sanctioned sender") } if result.Reason == "" { t.Fatal("should provide reason") } } func TestOFAC_BlocksSanctionedReceiver(t *testing.T) { h := NewComplianceAnteHandler() h.AddToOFACList("0xSanctioned") result := h.AnteHandle("0xNormal", "0xSanctioned", big.NewInt(1000)) if result.Allowed { t.Fatal("should block sanctioned receiver") } } func TestOFAC_AllowsCleanAddresses(t *testing.T) { h := NewComplianceAnteHandler() h.AddToOFACList("0xSanctioned") result := h.AnteHandle("0xAlice", "0xBob", big.NewInt(1000)) if !result.Allowed { t.Fatalf("should allow clean addresses, got reason: %s", result.Reason) } } func TestOFAC_BulkUpdate(t *testing.T) { h := NewComplianceAnteHandler() addresses := []string{"0xA", "0xB", "0xC"} h.UpdateOFACList(addresses) if h.GetOFACListSize() != 3 { t.Fatalf("expected 3, got %d", h.GetOFACListSize()) } for _, addr := range addresses { if !h.IsOFACSanctioned(addr) { t.Fatalf("%s should be sanctioned", addr) } } if h.IsOFACSanctioned("0xClean") { t.Fatal("0xClean should not be sanctioned") } } func TestOFAC_RemoveFromList(t *testing.T) { h := NewComplianceAnteHandler() h.AddToOFACList("0xAddr") if !h.IsOFACSanctioned("0xAddr") { t.Fatal("should be sanctioned") } h.RemoveFromOFACList("0xAddr") if h.IsOFACSanctioned("0xAddr") { t.Fatal("should no longer be sanctioned") } } func TestOFAC_UpdateReplacesOldList(t *testing.T) { h := NewComplianceAnteHandler() h.AddToOFACList("0xOld") h.UpdateOFACList([]string{"0xNew"}) if h.IsOFACSanctioned("0xOld") { t.Fatal("0xOld should be removed after full update") } if !h.IsOFACSanctioned("0xNew") { t.Fatal("0xNew should be in the new list") } } // ============================== // Travel Rule Tests // ============================== func TestTravelRule_BlocksLargeTransferWithoutRecord(t *testing.T) { h := NewComplianceAnteHandler() // $3,000 USDC = 3000 * 1e6 = 3,000,000,000 amount := new(big.Int).Mul(big.NewInt(3000), big.NewInt(1e6)) result := h.AnteHandle("0xAlice", "0xBob", amount) if result.Allowed { t.Fatal("should block large transfer without Travel Rule record") } } func TestTravelRule_AllowsLargeTransferWithRecord(t *testing.T) { h := NewComplianceAnteHandler() h.RecordTravelRule("0xAlice", "0xBob") amount := new(big.Int).Mul(big.NewInt(5000), big.NewInt(1e6)) result := h.AnteHandle("0xAlice", "0xBob", amount) if !result.Allowed { t.Fatalf("should allow large transfer with Travel Rule record, reason: %s", result.Reason) } } func TestTravelRule_AllowsSmallTransferWithoutRecord(t *testing.T) { h := NewComplianceAnteHandler() // $2,999 — below threshold amount := new(big.Int).Mul(big.NewInt(2999), big.NewInt(1e6)) result := h.AnteHandle("0xAlice", "0xBob", amount) if !result.Allowed { t.Fatal("should allow transfer below $3,000 without Travel Rule record") } } func TestTravelRule_ExactThreshold(t *testing.T) { h := NewComplianceAnteHandler() // Exactly $3,000 — at threshold, should require record amount := new(big.Int).Mul(big.NewInt(3000), big.NewInt(1e6)) result := h.AnteHandle("0xAlice", "0xBob", amount) if result.Allowed { t.Fatal("$3,000 exactly should require Travel Rule record") } } func TestTravelRule_HasRecord(t *testing.T) { h := NewComplianceAnteHandler() if h.HasTravelRuleRecord("0xA", "0xB") { t.Fatal("should not have record initially") } h.RecordTravelRule("0xA", "0xB") if !h.HasTravelRuleRecord("0xA", "0xB") { t.Fatal("should have record after recording") } // Direction matters if h.HasTravelRuleRecord("0xB", "0xA") { t.Fatal("reverse direction should not have record") } } // ============================== // Structuring Detection Tests // ============================== func TestStructuring_NoFlagForSingleSmallTransfer(t *testing.T) { h := NewComplianceAnteHandler() amount := new(big.Int).Mul(big.NewInt(500), big.NewInt(1e6)) // $500 result := h.AnteHandle("0xAlice", "0xBob", amount) if result.Suspicious { t.Fatal("single small transfer should not be flagged") } } func TestStructuring_FlagsMultipleSmallTransfersExceedingThreshold(t *testing.T) { h := NewComplianceAnteHandler() // Record several small transfers totaling > $3,000 for i := 0; i < 5; i++ { h.RecordTransfer("0xAlice", new(big.Int).Mul(big.NewInt(500), big.NewInt(1e6))) // $500 each } // 6th transfer: cumulative = $3,000 amount := new(big.Int).Mul(big.NewInt(500), big.NewInt(1e6)) result := h.AnteHandle("0xAlice", "0xBob", amount) if !result.Suspicious { t.Fatal("cumulative small transfers exceeding $3,000 should be flagged as suspicious") } // But still allowed (not blocked) if !result.Allowed { t.Fatal("structuring should flag but not block") } } func TestStructuring_DoesNotFlagIfSingleLargeTransfer(t *testing.T) { h := NewComplianceAnteHandler() // Record one large transfer > threshold h.RecordTransfer("0xAlice", new(big.Int).Mul(big.NewInt(4000), big.NewInt(1e6))) // Another small transfer amount := new(big.Int).Mul(big.NewInt(100), big.NewInt(1e6)) result := h.AnteHandle("0xAlice", "0xBob", amount) // Not structuring because previous transfers include one >= threshold if result.Suspicious { t.Fatal("should not flag as structuring if previous transfers include large ones") } } func TestStructuring_SlidingWindowExpiry(t *testing.T) { h := NewComplianceAnteHandler() // Override window to 1ms for testing h.structuringWindow = 1 * time.Millisecond h.RecordTransfer("0xAlice", new(big.Int).Mul(big.NewInt(2000), big.NewInt(1e6))) // Wait for window to expire time.Sleep(5 * time.Millisecond) amount := new(big.Int).Mul(big.NewInt(1500), big.NewInt(1e6)) result := h.AnteHandle("0xAlice", "0xBob", amount) if result.Suspicious { t.Fatal("expired window records should not contribute to structuring detection") } } // ============================== // Combined Scenario Tests // ============================== func TestCombined_OFACTakesPrecedenceOverTravelRule(t *testing.T) { h := NewComplianceAnteHandler() h.AddToOFACList("0xBad") h.RecordTravelRule("0xBad", "0xGood") amount := new(big.Int).Mul(big.NewInt(5000), big.NewInt(1e6)) result := h.AnteHandle("0xBad", "0xGood", amount) if result.Allowed { t.Fatal("OFAC should block even if Travel Rule record exists") } } func TestCombined_StructuringPlusTravelRule(t *testing.T) { h := NewComplianceAnteHandler() // Record small transfers approaching threshold for i := 0; i < 3; i++ { h.RecordTransfer("0xAlice", new(big.Int).Mul(big.NewInt(900), big.NewInt(1e6))) } // Small transfer that pushes cumulative over threshold // But this single transfer is below threshold, so no Travel Rule needed amount := new(big.Int).Mul(big.NewInt(500), big.NewInt(1e6)) result := h.AnteHandle("0xAlice", "0xBob", amount) if !result.Allowed { t.Fatal("below-threshold transfer should still be allowed") } if !result.Suspicious { t.Fatal("should be flagged as suspicious structuring") } } func TestConcurrency_SafeAccess(t *testing.T) { h := NewComplianceAnteHandler() done := make(chan struct{}) // Writer goroutine go func() { for i := 0; i < 100; i++ { h.AddToOFACList("0xAddr") h.RemoveFromOFACList("0xAddr") h.RecordTravelRule("0xA", "0xB") h.RecordTransfer("0xC", big.NewInt(1000)) } done <- struct{}{} }() // Reader goroutine go func() { for i := 0; i < 100; i++ { h.AnteHandle("0xAlice", "0xBob", big.NewInt(1000)) h.IsOFACSanctioned("0xAddr") h.HasTravelRuleRecord("0xA", "0xB") h.GetOFACListSize() } done <- struct{}{} }() <-done <-done }