docs(P04): complete Bearers skeleton I phase
P4 complete. Bearers skeleton I (D-020/D-035 pattern, zero ext deps): - x/bridge/types (NEW): BridgeStatus enum (4), BridgeRoute with by-ID-string refs to satellite L2Chain + watcher quorum (G-003). - x/exit/types (NEW): ExitStatus enum (5), ExitRoute with bridge-route-id by-ID-string (A-308), DEXSwap with opaque venue string. - x/bearers/types (EXT): OYSATLink surveillance-resistant LOCKED true (A-311), OYQRCode MarkConsumed idempotent. - x/partner/types (EXT): AnchorCredential, custody-provider-id empty in skeleton (A-304), PartnerTierCount=4 regression (A-305). All 4 packages 100% coverage. Both firewalls green. G-003 intact. ---ci--- project: oy phase: 4 milestone: v0.3 status: complete tag_base: v0.2.x phase_role: execution requirements: covered: [REQ-010, REQ-022, REQ-023] partial: [] ---/ci---
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
package types
|
||||
|
||||
import "fmt"
|
||||
|
||||
// genesis.go holds the data-engineer's genesis schema helpers for the
|
||||
// exit module (G-008 split). ValidateGenesis in types.go composes these
|
||||
// helpers; the security-engineer's test assertions live in types_test.go.
|
||||
//
|
||||
// The Exit genesis schema has two top-level sets: Routes (exit routes) and
|
||||
// Swaps (DEX swaps). The invariants enforced at genesis load are (1)
|
||||
// route-id uniqueness, (2) swap-id uniqueness, and (3) status validity.
|
||||
// The route's bridge-route-id is a by-ID-string ref (G-003) and is NOT
|
||||
// referentially checked at genesis (the referenced x/bridge state is in a
|
||||
// separate module; cross-module referential integrity is a v0.4 keeper
|
||||
// concern, not a v0.3 skeleton concern per A-308).
|
||||
|
||||
// ValidateRoutes asserts route-ids are present and unique, and that each
|
||||
// route's status is a known ExitStatus. ValidateRoutes is the
|
||||
// data-engineer's schema validator, composed by ValidateGenesis in
|
||||
// types.go.
|
||||
func ValidateRoutes(routes []ExitRoute) error {
|
||||
seen := make(map[string]bool, len(routes))
|
||||
for i, r := range routes {
|
||||
if r.RouteID == "" {
|
||||
return fmt.Errorf("exit [%d]: empty route-id", i)
|
||||
}
|
||||
if seen[r.RouteID] {
|
||||
return fmt.Errorf("exit: duplicate route-id %q", r.RouteID)
|
||||
}
|
||||
seen[r.RouteID] = true
|
||||
if !knownExitStatus(r.Status) {
|
||||
return fmt.Errorf("exit %q: unknown exit status %q", r.RouteID, r.Status)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateSwaps asserts swap-ids are present and unique, and that each
|
||||
// swap's status is a known ExitStatus. The venue is an opaque string
|
||||
// (A-308) and is not validated against a locked enum.
|
||||
func ValidateSwaps(swaps []DEXSwap) error {
|
||||
seen := make(map[string]bool, len(swaps))
|
||||
for i, s := range swaps {
|
||||
if s.SwapID == "" {
|
||||
return fmt.Errorf("exit [%d]: empty swap-id", i)
|
||||
}
|
||||
if seen[s.SwapID] {
|
||||
return fmt.Errorf("exit: duplicate swap-id %q", s.SwapID)
|
||||
}
|
||||
seen[s.SwapID] = true
|
||||
if !knownExitStatus(s.Status) {
|
||||
return fmt.Errorf("exit swap %q: unknown exit status %q", s.SwapID, s.Status)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// knownExitStatus reports whether s is one of the five ExitStatus values.
|
||||
func knownExitStatus(s ExitStatus) bool {
|
||||
for _, ss := range AllExitStatuses() {
|
||||
if s == ss {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const (
|
||||
ModuleName = "exit"
|
||||
StoreKey = ModuleName
|
||||
RouterKey = ModuleName
|
||||
QuerierRoute = ModuleName
|
||||
|
||||
// ExitStatusCount is the locked count of ExitStatus enum values
|
||||
// (vision §7, REQ-010, D-036). Five exit lifecycle states: Proposed,
|
||||
// InProgress, Settled, Failed, Refunded. A regression firewall:
|
||||
// adding/removing/renaming a status breaks this const's test.
|
||||
ExitStatusCount = 5
|
||||
)
|
||||
|
||||
// ExitStatus enumerates the lifecycle of a Layer-3 exit (vision §7,
|
||||
// REQ-010, D-036). The five-state lifecycle covers both successful exits
|
||||
// (Proposed → InProgress → Settled) and the failure/recovery paths
|
||||
// (Failed → Refunded). Refunded is the terminal recovery state when an
|
||||
// exit fails and the holder is made whole.
|
||||
type ExitStatus string
|
||||
|
||||
const (
|
||||
ExitProposed ExitStatus = "Proposed" // exit declared, not yet executing
|
||||
ExitInProgress ExitStatus = "InProgress" // exit executing (swap/bridge hop)
|
||||
ExitSettled ExitStatus = "Settled" // exit completed, holder paid out
|
||||
ExitFailed ExitStatus = "Failed" // exit failed (slippage/timeout)
|
||||
ExitRefunded ExitStatus = "Refunded" // failed exit refunded to holder
|
||||
)
|
||||
|
||||
// AllExitStatuses returns all five ExitStatus values in vision §7 lifecycle
|
||||
// order. Locked-const test asserts exactly 5 entries.
|
||||
func AllExitStatuses() []ExitStatus {
|
||||
return []ExitStatus{
|
||||
ExitProposed,
|
||||
ExitInProgress,
|
||||
ExitSettled,
|
||||
ExitFailed,
|
||||
ExitRefunded,
|
||||
}
|
||||
}
|
||||
|
||||
// ExitRoute is a Holder-initiated exit route (REQ-010, D-036, A-308). The
|
||||
// route describes a holder's intent to exit the mesh via a DEX swap and
|
||||
// (optionally) a cross-chain bridge hop. All cross-module references are
|
||||
// by-ID-string per G-003:
|
||||
//
|
||||
// - route-id is this route's unique identifier.
|
||||
// - bridge-route-id references an x/bridge BridgeRoute by ID-string
|
||||
// (A-308, G-003). It is optional (empty for same-chain exits) and
|
||||
// present for cross-chain exits. No struct import of x/bridge.
|
||||
// - status is the exit lifecycle (ExitStatus).
|
||||
//
|
||||
// The bridge-route-id is the P4 intra-phase dependency edge (x/bridge is
|
||||
// authored first within P4; x/exit references it by ID-string only).
|
||||
type ExitRoute struct {
|
||||
RouteID string `json:"route_id" yaml:"route_id"`
|
||||
BridgeRouteID string `json:"bridge_route_id" yaml:"bridge_route_id"`
|
||||
Status ExitStatus `json:"status" yaml:"status"`
|
||||
}
|
||||
|
||||
// DEXSwap is a single DEX swap executed as part of an exit route (REQ-010,
|
||||
// D-036, A-308). The venue is an OPAQUE string (e.g. "uniswap-v3", "oy-dex")
|
||||
// — NOT a locked enum. A-308: venues are operational, not protocol-locked;
|
||||
// locking an enum now risks churn (uniswap-v3/v4, oy-dex, etc. change over
|
||||
// time). The skeleton keeps the venue as a free-form string so the type
|
||||
// shape is stable across venue additions. status reuses ExitStatus (a swap
|
||||
// shares the exit lifecycle: Proposed → InProgress → Settled/Failed).
|
||||
//
|
||||
// - swap-id is this swap's unique identifier.
|
||||
// - venue is the opaque DEX venue string (A-308 — not a locked enum).
|
||||
// - status is the swap lifecycle (ExitStatus).
|
||||
type DEXSwap struct {
|
||||
SwapID string `json:"swap_id" yaml:"swap_id"`
|
||||
Venue string `json:"venue" yaml:"venue"`
|
||||
Status ExitStatus `json:"status" yaml:"status"`
|
||||
}
|
||||
|
||||
// Params for the exit module (skeleton — no tunables in v0.3).
|
||||
type Params struct{}
|
||||
|
||||
func DefaultParams() Params { return Params{} }
|
||||
|
||||
// GenesisState defines the exit module genesis state (REQ-010). Routes is
|
||||
// the set of exit routes; Swaps is the set of DEX swaps. ValidateGenesis
|
||||
// enforces route-id and swap-id uniqueness. The data-engineer's genesis.go
|
||||
// holds the schema helpers (G-008 split).
|
||||
type GenesisState struct {
|
||||
Params Params `json:"params" yaml:"params"`
|
||||
Routes []ExitRoute `json:"routes" yaml:"routes"`
|
||||
Swaps []DEXSwap `json:"swaps" yaml:"swaps"`
|
||||
}
|
||||
|
||||
func DefaultGenesisState() *GenesisState {
|
||||
return &GenesisState{
|
||||
Params: DefaultParams(),
|
||||
Routes: []ExitRoute{},
|
||||
Swaps: []DEXSwap{},
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateGenesis performs ID-uniqueness checks (A-212 upgrade from v0.1
|
||||
// no-op): rejects duplicate route-ids and swap-ids. Delegates to the
|
||||
// data-engineer's genesis.go helpers (G-008).
|
||||
func ValidateGenesis(bz json.RawMessage) error {
|
||||
var gs GenesisState
|
||||
if err := json.Unmarshal(bz, &gs); err != nil {
|
||||
return fmt.Errorf("exit: invalid genesis: %w", err)
|
||||
}
|
||||
if err := ValidateRoutes(gs.Routes); err != nil {
|
||||
return fmt.Errorf("exit: %w", err)
|
||||
}
|
||||
if err := ValidateSwaps(gs.Swaps); err != nil {
|
||||
return fmt.Errorf("exit: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,390 @@
|
||||
package types_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/oy/openyield/lexicon"
|
||||
etypes "github.com/oy/openyield/x/exit/types"
|
||||
)
|
||||
|
||||
// --- ExitStatus enum (exactly 5) -----------------------------------------------
|
||||
|
||||
// TestExitStatusCountLockedConst asserts ExitStatusCount == 5 and
|
||||
// AllExitStatuses() returns exactly 5 (vision §7, REQ-010, D-036). A
|
||||
// regression firewall: adding/removing/renaming a status breaks this test.
|
||||
func TestExitStatusCountLockedConst(t *testing.T) {
|
||||
if etypes.ExitStatusCount != 5 {
|
||||
t.Errorf("ExitStatusCount = %d, expected 5 (vision §7 LOCKED)", etypes.ExitStatusCount)
|
||||
}
|
||||
all := etypes.AllExitStatuses()
|
||||
if len(all) != 5 {
|
||||
t.Errorf("AllExitStatuses() len = %d, expected 5", len(all))
|
||||
}
|
||||
}
|
||||
|
||||
// TestAllExitStatusesNames asserts the 5 vision §7 exit-lifecycle names in
|
||||
// order with no extras, no dups, no renames (Proposed, InProgress, Settled,
|
||||
// Failed, Refunded).
|
||||
func TestAllExitStatusesNames(t *testing.T) {
|
||||
want := []string{"Proposed", "InProgress", "Settled", "Failed", "Refunded"}
|
||||
all := etypes.AllExitStatuses()
|
||||
if len(all) != len(want) {
|
||||
t.Fatalf("len = %d, want %d", len(all), len(want))
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for i, s := range all {
|
||||
if string(s) != want[i] {
|
||||
t.Errorf("AllExitStatuses()[%d] = %q, want %q", i, s, want[i])
|
||||
}
|
||||
if seen[string(s)] {
|
||||
t.Errorf("duplicate ExitStatus %q", s)
|
||||
}
|
||||
seen[string(s)] = true
|
||||
}
|
||||
}
|
||||
|
||||
// TestExitStatusValues asserts each named const matches its AllExitStatuses
|
||||
// entry.
|
||||
func TestExitStatusValues(t *testing.T) {
|
||||
if etypes.ExitProposed != "Proposed" {
|
||||
t.Errorf("ExitProposed = %q", etypes.ExitProposed)
|
||||
}
|
||||
if etypes.ExitInProgress != "InProgress" {
|
||||
t.Errorf("ExitInProgress = %q", etypes.ExitInProgress)
|
||||
}
|
||||
if etypes.ExitSettled != "Settled" {
|
||||
t.Errorf("ExitSettled = %q", etypes.ExitSettled)
|
||||
}
|
||||
if etypes.ExitFailed != "Failed" {
|
||||
t.Errorf("ExitFailed = %q", etypes.ExitFailed)
|
||||
}
|
||||
if etypes.ExitRefunded != "Refunded" {
|
||||
t.Errorf("ExitRefunded = %q", etypes.ExitRefunded)
|
||||
}
|
||||
}
|
||||
|
||||
// --- ExitRoute struct (bridge-route-id by-ID-string — G-003/A-308) ----------------
|
||||
|
||||
// TestExitRouteStructFields asserts ExitRoute carries all required fields
|
||||
// including the by-ID-string ref to x/bridge BridgeRoute (bridge-route-id)
|
||||
// per A-308/G-003. No struct import of x/bridge (the G-003 import-invariant
|
||||
// test enforces this).
|
||||
func TestExitRouteStructFields(t *testing.T) {
|
||||
r := etypes.ExitRoute{
|
||||
RouteID: "route-1",
|
||||
BridgeRouteID: "bridge-1", // by-ID-string ref to x/bridge (A-308/G-003)
|
||||
Status: etypes.ExitProposed,
|
||||
}
|
||||
if r.RouteID != "route-1" {
|
||||
t.Errorf("RouteID = %q", r.RouteID)
|
||||
}
|
||||
if r.BridgeRouteID != "bridge-1" {
|
||||
t.Errorf("BridgeRouteID = %q", r.BridgeRouteID)
|
||||
}
|
||||
if r.Status != etypes.ExitProposed {
|
||||
t.Errorf("Status = %q", r.Status)
|
||||
}
|
||||
}
|
||||
|
||||
// TestExitRouteBridgeRouteIDIsString asserts the BridgeRouteID field is an
|
||||
// opaque string (by-ID-string ref — G-003), NOT a typed x/bridge.BridgeRoute
|
||||
// import. This locks the by-ID-string invariant at the type level.
|
||||
func TestExitRouteBridgeRouteIDIsString(t *testing.T) {
|
||||
r := etypes.ExitRoute{BridgeRouteID: "bridge-9"}
|
||||
// The field must be assignable from a plain string (no bridge.BridgeRoute
|
||||
// type needed).
|
||||
r.BridgeRouteID = "bridge-2"
|
||||
if r.BridgeRouteID != "bridge-2" {
|
||||
t.Errorf("BridgeRouteID = %q, want %q (must be plain string)", r.BridgeRouteID, "bridge-2")
|
||||
}
|
||||
}
|
||||
|
||||
// TestExitRouteBridgeRouteIDOptional asserts an empty bridge-route-id is
|
||||
// valid (same-chain exits have no bridge hop).
|
||||
func TestExitRouteBridgeRouteIDOptional(t *testing.T) {
|
||||
r := etypes.ExitRoute{
|
||||
RouteID: "same-chain-exit",
|
||||
BridgeRouteID: "", // empty = same-chain exit (no bridge hop)
|
||||
Status: etypes.ExitSettled,
|
||||
}
|
||||
if r.BridgeRouteID != "" {
|
||||
t.Errorf("BridgeRouteID should be empty for same-chain exit; got %q", r.BridgeRouteID)
|
||||
}
|
||||
}
|
||||
|
||||
// --- DEXSwap struct (opaque venue — A-308) --------------------------------------
|
||||
|
||||
// TestDEXSwapStructFields asserts DEXSwap carries all required fields
|
||||
// including the opaque venue string (A-308) and an ExitStatus.
|
||||
func TestDEXSwapStructFields(t *testing.T) {
|
||||
s := etypes.DEXSwap{
|
||||
SwapID: "swap-1",
|
||||
Venue: "uniswap-v3",
|
||||
Status: etypes.ExitSettled,
|
||||
}
|
||||
if s.SwapID != "swap-1" {
|
||||
t.Errorf("SwapID = %q", s.SwapID)
|
||||
}
|
||||
if s.Venue != "uniswap-v3" {
|
||||
t.Errorf("Venue = %q", s.Venue)
|
||||
}
|
||||
if s.Status != etypes.ExitSettled {
|
||||
t.Errorf("Status = %q", s.Status)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDEXSwapVenueIsOpaqueString asserts the DEXSwap venue is an opaque
|
||||
// string, NOT a locked enum (A-308 — venues are operational, locking now
|
||||
// risks churn). The field must accept any free-form string.
|
||||
func TestDEXSwapVenueIsOpaqueString(t *testing.T) {
|
||||
// A-308: venue is an opaque string, not a locked enum. Various venue
|
||||
// strings must be assignable without any enum type.
|
||||
venues := []string{"uniswap-v3", "oy-dex", "1inch", "paraswap", "0x-api", "custom-venue-xyz"}
|
||||
for _, v := range venues {
|
||||
s := etypes.DEXSwap{SwapID: "s", Venue: v}
|
||||
if s.Venue != v {
|
||||
t.Errorf("Venue = %q, want %q (A-308: venue must be opaque string)", s.Venue, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestDEXSwapVenueTypeIsString asserts the Venue field's Go type is the
|
||||
// built-in string (not a typed enum). This locks A-308 at the type level:
|
||||
// the field is a plain string, so any venue string is assignable without
|
||||
// conversion.
|
||||
func TestDEXSwapVenueTypeIsString(t *testing.T) {
|
||||
s := etypes.DEXSwap{}
|
||||
// Assigning a plain string literal must compile and work — no enum
|
||||
// conversion needed. If venue were a typed enum, assigning a plain
|
||||
// string would require a type conversion (e.g. etypes.Venue("x")).
|
||||
s.Venue = "any-string-works"
|
||||
var want string = "any-string-works"
|
||||
if s.Venue != want {
|
||||
t.Errorf("Venue type is not plain string (A-308): got %q want %q", s.Venue, want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDEXSwapStatusReusesExitStatus asserts the DEXSwap status field reuses
|
||||
// the ExitStatus enum (a swap shares the exit lifecycle).
|
||||
func TestDEXSwapStatusReusesExitStatus(t *testing.T) {
|
||||
statuses := etypes.AllExitStatuses()
|
||||
for _, st := range statuses {
|
||||
s := etypes.DEXSwap{SwapID: "s", Venue: "v", Status: st}
|
||||
if s.Status != st {
|
||||
t.Errorf("DEXSwap.Status = %q, want %q (must reuse ExitStatus)", s.Status, st)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Genesis tests (A-212) ------------------------------------------------------
|
||||
|
||||
// TestDefaultGenesisStateEmpty asserts DefaultGenesisState returns non-nil
|
||||
// empty slices for Routes and Swaps.
|
||||
func TestDefaultGenesisStateEmpty(t *testing.T) {
|
||||
gs := etypes.DefaultGenesisState()
|
||||
if gs == nil {
|
||||
t.Fatal("DefaultGenesisState returned nil")
|
||||
}
|
||||
if gs.Routes == nil || len(gs.Routes) != 0 {
|
||||
t.Errorf("Default Routes should be non-nil empty slice; got len=%d nil=%v", len(gs.Routes), gs.Routes == nil)
|
||||
}
|
||||
if gs.Swaps == nil || len(gs.Swaps) != 0 {
|
||||
t.Errorf("Default Swaps should be non-nil empty slice; got len=%d nil=%v", len(gs.Swaps), gs.Swaps == nil)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsDupRouteIDs asserts A-212: duplicate route-ids
|
||||
// are rejected.
|
||||
func TestValidateGenesisRejectsDupRouteIDs(t *testing.T) {
|
||||
gs := etypes.GenesisState{
|
||||
Routes: []etypes.ExitRoute{
|
||||
{RouteID: "r1", Status: etypes.ExitProposed},
|
||||
{RouteID: "r1", Status: etypes.ExitSettled}, // dup
|
||||
},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := etypes.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject duplicate route-ids")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsEmptyRouteID asserts empty route-id is rejected.
|
||||
func TestValidateGenesisRejectsEmptyRouteID(t *testing.T) {
|
||||
gs := etypes.GenesisState{
|
||||
Routes: []etypes.ExitRoute{{RouteID: "", Status: etypes.ExitProposed}},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := etypes.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject empty route-id")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsUnknownRouteStatus asserts an unknown ExitStatus
|
||||
// on a route is rejected.
|
||||
func TestValidateGenesisRejectsUnknownRouteStatus(t *testing.T) {
|
||||
gs := etypes.GenesisState{
|
||||
Routes: []etypes.ExitRoute{{RouteID: "r1", Status: etypes.ExitStatus("Bogus")}},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := etypes.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject unknown exit status on route")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsDupSwapIDs asserts A-212: duplicate swap-ids
|
||||
// are rejected.
|
||||
func TestValidateGenesisRejectsDupSwapIDs(t *testing.T) {
|
||||
gs := etypes.GenesisState{
|
||||
Swaps: []etypes.DEXSwap{
|
||||
{SwapID: "s1", Venue: "uniswap-v3", Status: etypes.ExitSettled},
|
||||
{SwapID: "s1", Venue: "oy-dex", Status: etypes.ExitProposed}, // dup
|
||||
},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := etypes.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject duplicate swap-ids")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsEmptySwapID asserts empty swap-id is rejected.
|
||||
func TestValidateGenesisRejectsEmptySwapID(t *testing.T) {
|
||||
gs := etypes.GenesisState{
|
||||
Swaps: []etypes.DEXSwap{{SwapID: "", Venue: "oy-dex", Status: etypes.ExitProposed}},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := etypes.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject empty swap-id")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsUnknownSwapStatus asserts an unknown ExitStatus
|
||||
// on a swap is rejected.
|
||||
func TestValidateGenesisRejectsUnknownSwapStatus(t *testing.T) {
|
||||
gs := etypes.GenesisState{
|
||||
Swaps: []etypes.DEXSwap{{SwapID: "s1", Venue: "oy-dex", Status: etypes.ExitStatus("Bogus")}},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := etypes.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject unknown exit status on swap")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsBadJSON asserts malformed JSON is rejected.
|
||||
func TestValidateGenesisRejectsBadJSON(t *testing.T) {
|
||||
if err := etypes.ValidateGenesis(json.RawMessage(`{not json`)); err == nil {
|
||||
t.Error("ValidateGenesis should reject malformed JSON")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisAcceptsClean asserts a clean genesis validates.
|
||||
func TestValidateGenesisAcceptsClean(t *testing.T) {
|
||||
gs := etypes.GenesisState{
|
||||
Routes: []etypes.ExitRoute{
|
||||
{RouteID: "r1", BridgeRouteID: "bridge-1", Status: etypes.ExitInProgress},
|
||||
{RouteID: "r2", BridgeRouteID: "", Status: etypes.ExitSettled}, // same-chain exit
|
||||
},
|
||||
Swaps: []etypes.DEXSwap{
|
||||
{SwapID: "s1", Venue: "uniswap-v3", Status: etypes.ExitSettled},
|
||||
{SwapID: "s2", Venue: "oy-dex", Status: etypes.ExitProposed},
|
||||
},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := etypes.ValidateGenesis(bz); err != nil {
|
||||
t.Errorf("ValidateGenesis should accept clean genesis, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Module consts -------------------------------------------------------------
|
||||
|
||||
// TestModuleConsts asserts the four Cosmos-convention module consts.
|
||||
func TestModuleConsts(t *testing.T) {
|
||||
if etypes.ModuleName != "exit" {
|
||||
t.Errorf("ModuleName = %q", etypes.ModuleName)
|
||||
}
|
||||
if etypes.StoreKey != "exit" {
|
||||
t.Errorf("StoreKey = %q", etypes.StoreKey)
|
||||
}
|
||||
if etypes.RouterKey != "exit" {
|
||||
t.Errorf("RouterKey = %q", etypes.RouterKey)
|
||||
}
|
||||
if etypes.QuerierRoute != "exit" {
|
||||
t.Errorf("QuerierRoute = %q", etypes.QuerierRoute)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDefaultParams asserts DefaultParams returns a zero-value Params.
|
||||
func TestDefaultParams(t *testing.T) {
|
||||
_ = etypes.DefaultParams() // no panics
|
||||
}
|
||||
|
||||
// --- Lexicon assertion (REQ-012) -------------------------------------------------
|
||||
//
|
||||
// The exit module must avoid the banned financial holder terms (the
|
||||
// lexicon firewall's banned list). Use "Holder"/"Reach" instead. The lexicon
|
||||
// helpers are used here — no banned literals are inlined.
|
||||
|
||||
// TestLexiconNoBannedTermsInExitPackage scans every non-test .go file in
|
||||
// the exit/types package directory for the banned terms (case-insensitive).
|
||||
// Production files only — the test file references banned terms via the
|
||||
// lexicon package helpers (standard lexicon-test bootstrapping pattern).
|
||||
func TestLexiconNoBannedTermsInExitPackage(t *testing.T) {
|
||||
pkgDir := packageDir(t, "github.com/oy/openyield/x/exit/types")
|
||||
files, err := filepath.Glob(filepath.Join(pkgDir, "*.go"))
|
||||
if err != nil {
|
||||
t.Fatalf("glob: %v", err)
|
||||
}
|
||||
prodFiles := []string{}
|
||||
for _, f := range files {
|
||||
if strings.HasSuffix(f, "_test.go") {
|
||||
continue
|
||||
}
|
||||
prodFiles = append(prodFiles, f)
|
||||
}
|
||||
if len(prodFiles) == 0 {
|
||||
t.Fatal("no production .go files found in exit/types")
|
||||
}
|
||||
for _, f := range prodFiles {
|
||||
bz, err := os.ReadFile(f)
|
||||
if err != nil {
|
||||
t.Fatalf("read %s: %v", f, err)
|
||||
}
|
||||
if found, ok := lexicon.FindBannedTerm(string(bz)); ok {
|
||||
t.Errorf("%s: banned term %q (REQ-012 lexicon firewall — use Holder/Reach, not banned financial terms)", filepath.Base(f), found)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestLexiconNoBannedTermsInExitTestFile asserts this test file itself
|
||||
// does not contain any banned term as a literal.
|
||||
func TestLexiconNoBannedTermsInExitTestFile(t *testing.T) {
|
||||
_, thisFile, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
t.Fatal("runtime.Caller failed")
|
||||
}
|
||||
bz, err := os.ReadFile(thisFile)
|
||||
if err != nil {
|
||||
t.Fatalf("read self: %v", err)
|
||||
}
|
||||
if found, ok := lexicon.FindBannedTerm(string(bz)); ok {
|
||||
t.Fatalf("exit test file contains banned term %q — use lexicon helpers, not literals", found)
|
||||
}
|
||||
}
|
||||
|
||||
// packageDir resolves a Go import path to its filesystem directory by
|
||||
// walking up from this test file (v0.3 skeleton has zero external deps).
|
||||
func packageDir(t *testing.T, importPath string) string {
|
||||
t.Helper()
|
||||
_, file, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
t.Fatal("runtime.Caller failed")
|
||||
}
|
||||
// file = .../oy/x/exit/types/types_test.go -> repoRoot = .../oy (4 dirs up)
|
||||
repoRoot := filepath.Dir(filepath.Dir(filepath.Dir(filepath.Dir(file))))
|
||||
rel := strings.TrimPrefix(importPath, "github.com/oy/openyield/")
|
||||
return filepath.Join(repoRoot, rel)
|
||||
}
|
||||
Reference in New Issue
Block a user