docs(milestone): complete OpenYield v0.2 (The Mesh)
---ci--- project: oy phase: 5 milestone: v0.2 status: complete phase_role: final requirements: covered: [REQ-009, REQ-011, REQ-015, REQ-016, REQ-017, REQ-018, REQ-020, REQ-021, REQ-012] partial: [] ---/ci--- Milestone v0.2 (The Mesh) complete. Skeleton+tests layer for 9 new modules + 1 extension: x/window (REQ-015, fullest), x/stand (REQ-016, 9 types), x/guild (REQ-017, HandPass 0%), x/pact (REQ-020, 6 types + Mission Lock), x/partner (REQ-018, 4-tier), x/council (REQ-011, 3 councils + Mission Lock const), x/forex (Forex v1, Bread/Asset pairs), x/bond (REQ-021, 8% cap Clamp), x/satellite (REQ-009, 5-chain L2 + ICS-20 v1), x/bearers EXTENDED (OY-LR/Beacon). 303 tests total (53 v0.1 baseline + 250 new). Coverage >=95.9% on all new/extended packages (8 at 100%). Lexicon firewall (REQ-012) project-wide + per-module. G-003 by-ID-string import invariant. 5 phases: P0 (v0.1.0) + P1-P4 (v0.1.1..v0.1.4) + P5 final (v0.1.5 = this milestone release). Per run.md patch-line model: no separate minor tag. 14 clarification decisions (D-020..D-033), 15 research assumptions (A-201..A-215), 31 tasks across 5 phases, 10 grill binding decisions (G-001..G-010) all applied. Next milestone: v0.3 (The Bearers) per ROADMAP Phase 3.
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
package types
|
||||
|
||||
import "fmt"
|
||||
|
||||
// genesis.go holds the data-engineer's genesis schema helpers for the
|
||||
// satellite module (G-008 split). ValidateGenesis in types.go composes these
|
||||
// helpers; the security-engineer's test assertions live in types_test.go.
|
||||
//
|
||||
// The Satellite genesis schema has two top-level sets: Channels (the IBC
|
||||
// transfer channels between OY Chain and L2 satellites) and Denoms (the
|
||||
// wrapped Bread denoms). The invariants enforced at genesis load are
|
||||
// (1) channel-id uniqueness, (2) denom uniqueness, and (3) each channel's
|
||||
// status is a known ChannelStatus.
|
||||
|
||||
// ValidateChannels asserts channel-ids are present and unique, and that
|
||||
// each channel's status is a known ChannelStatus. ValidateChannels is the
|
||||
// data-engineer's schema validator, composed by ValidateGenesis in types.go.
|
||||
func ValidateChannels(channels []TransferChannel) error {
|
||||
seen := make(map[string]bool, len(channels))
|
||||
for i, c := range channels {
|
||||
if c.ChannelID == "" {
|
||||
return fmt.Errorf("channel [%d]: empty channel-id", i)
|
||||
}
|
||||
if seen[c.ChannelID] {
|
||||
return fmt.Errorf("channel: duplicate channel-id %q", c.ChannelID)
|
||||
}
|
||||
seen[c.ChannelID] = true
|
||||
if !knownChannelStatus(c.Status) {
|
||||
return fmt.Errorf("channel %q: unknown channel status %q", c.ChannelID, c.Status)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateDenoms asserts denoms are present and unique. ValidateDenoms is
|
||||
// the data-engineer's schema validator for the wrapped Bread denom set.
|
||||
func ValidateDenoms(denoms []WrappedBreadDenom) error {
|
||||
seen := make(map[string]bool, len(denoms))
|
||||
for i, d := range denoms {
|
||||
if d.Denom == "" {
|
||||
return fmt.Errorf("denom [%d]: empty denom", i)
|
||||
}
|
||||
if seen[d.Denom] {
|
||||
return fmt.Errorf("denom: duplicate denom %q", d.Denom)
|
||||
}
|
||||
seen[d.Denom] = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// knownChannelStatus reports whether s is one of the four ChannelStatus values.
|
||||
func knownChannelStatus(s ChannelStatus) bool {
|
||||
for _, ss := range AllChannelStatuses() {
|
||||
if s == ss {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const (
|
||||
ModuleName = "satellite"
|
||||
StoreKey = ModuleName
|
||||
RouterKey = ModuleName
|
||||
QuerierRoute = ModuleName
|
||||
|
||||
// L2ChainCount is the locked count of L2Chain enum values (vision §10,
|
||||
// REQ-009, D-021). Five L2 satellite chains: Polygon (the one active
|
||||
// representative in v0.2) plus Base, Arbitrum, Optimism, Solana (four
|
||||
// StatusPending enum placeholders). A regression firewall:
|
||||
// adding/removing/renaming a chain breaks this const's test.
|
||||
L2ChainCount = 5
|
||||
|
||||
// ChannelStatusCount is the locked count of ChannelStatus enum values
|
||||
// (ICS-20 handshake): Init, TryOpen, Open, Closed. A regression firewall
|
||||
// for the ICS-20 handshake shape (A-215).
|
||||
ChannelStatusCount = 4
|
||||
)
|
||||
|
||||
// L2Chain enumerates the L2 satellite chains (vision §10, REQ-009, D-021).
|
||||
// Polygon is the one active representative in v0.2 (D-021 scopes v0.2 to ONE
|
||||
// representative chain). Base, Arbitrum, Optimism, and Solana are
|
||||
// StatusPending enum placeholders (the full 5-chain IBC rollout is Phase 3
|
||||
// per D-021). Solana lacks native IBC (RESEARCH §1.1) and is stubbed as
|
||||
// StatusPending — no Solana light-client logic in v0.2.
|
||||
type L2Chain string
|
||||
|
||||
const (
|
||||
ChainPolygon L2Chain = "Polygon" // active representative (D-021)
|
||||
ChainBase L2Chain = "Base" // StatusPending placeholder
|
||||
ChainArbitrum L2Chain = "Arbitrum" // StatusPending placeholder
|
||||
ChainOptimism L2Chain = "Optimism" // StatusPending placeholder
|
||||
ChainSolana L2Chain = "Solana" // StatusPending placeholder (no native IBC)
|
||||
)
|
||||
|
||||
// ChainActivation is the activation state of an L2 chain (D-021): Active
|
||||
// (Polygon in v0.2) or StatusPending (the four stubs).
|
||||
type ChainActivation string
|
||||
|
||||
const (
|
||||
ChainActive ChainActivation = "Active" // chain is live for IBC transfer
|
||||
ChainStatusPending ChainActivation = "StatusPending" // chain is a placeholder (Phase 3 rollout)
|
||||
)
|
||||
|
||||
// ChainInfo describes an L2 chain's properties (REQ-009, D-021).
|
||||
type ChainInfo struct {
|
||||
Chain L2Chain `json:"chain" yaml:"chain"`
|
||||
Activation ChainActivation `json:"activation" yaml:"activation"`
|
||||
}
|
||||
|
||||
// AllL2Chains returns all five L2Chain values (Polygon + 4 stubs) with their
|
||||
// activation states (D-021). Locked-const test asserts exactly 5 entries.
|
||||
// Polygon is the only ChainActive entry; the other four are StatusPending.
|
||||
func AllL2Chains() []ChainInfo {
|
||||
return []ChainInfo{
|
||||
{ChainPolygon, ChainActive},
|
||||
{ChainBase, ChainStatusPending},
|
||||
{ChainArbitrum, ChainStatusPending},
|
||||
{ChainOptimism, ChainStatusPending},
|
||||
{ChainSolana, ChainStatusPending},
|
||||
}
|
||||
}
|
||||
|
||||
// ChannelStatus enumerates the ICS-20 channel handshake states (A-215):
|
||||
// Init (channel initialized), TryOpen (counterparty trying to open), Open
|
||||
// (channel established), Closed (channel closed). The four-state handshake
|
||||
// mirrors ibc-go ICS-20 v1 channel state (stable, widely implemented).
|
||||
type ChannelStatus string
|
||||
|
||||
const (
|
||||
ChannelInit ChannelStatus = "Init" // channel initialized
|
||||
ChannelTryOpen ChannelStatus = "TryOpen" // counterparty trying to open
|
||||
ChannelOpen ChannelStatus = "Open" // channel established
|
||||
ChannelClosed ChannelStatus = "Closed" // channel closed
|
||||
)
|
||||
|
||||
// AllChannelStatuses returns all four ChannelStatus values in ICS-20
|
||||
// handshake order. Locked-const test asserts exactly 4 entries.
|
||||
func AllChannelStatuses() []ChannelStatus {
|
||||
return []ChannelStatus{
|
||||
ChannelInit,
|
||||
ChannelTryOpen,
|
||||
ChannelOpen,
|
||||
ChannelClosed,
|
||||
}
|
||||
}
|
||||
|
||||
// TransferChannel is an IBC transfer channel between OY Chain (L1) and an L2
|
||||
// satellite (REQ-009, A-215). port-id and channel-id are the ICS-20 port and
|
||||
// channel identifiers (e.g. "transfer" / "channel-0"). counterparty is the
|
||||
// counterparty port+channel on the L2. status is the handshake state.
|
||||
type TransferChannel struct {
|
||||
PortID string `json:"port_id" yaml:"port_id"`
|
||||
ChannelID string `json:"channel_id" yaml:"channel_id"`
|
||||
Counterparty string `json:"counterparty" yaml:"counterparty"`
|
||||
Status ChannelStatus `json:"status" yaml:"status"`
|
||||
}
|
||||
|
||||
// WrappedBreadDenom encodes an IBC-traced wrapped Bread denom (REQ-009,
|
||||
// A-215). When Bread propagates from OY Chain (L1) to an L2 via IBC, the
|
||||
// denom on the L2 is the original denom prefixed with the IBC trace path
|
||||
// (e.g. "transfer/channel-0/bread"). denom is the full traced denom on the
|
||||
// destination chain; trace-path is the IBC trace (the port/channel hops).
|
||||
type WrappedBreadDenom struct {
|
||||
Denom string `json:"denom" yaml:"denom"`
|
||||
TracePath string `json:"trace_path" yaml:"trace_path"`
|
||||
}
|
||||
|
||||
// Packet is the ICS-20 v1 packet shape stub (REQ-009, A-215). Pinned to the
|
||||
// ICS-20 v1 channel packet shape (stable, widely implemented) to minimize
|
||||
// churn if a different ibc-go version is chosen in Phase 3. Fields:
|
||||
// sequence, source-port, source-channel, dest-port, dest-channel, data,
|
||||
// timeout-height, timeout-timestamp. NO ibc-go import — zero external deps
|
||||
// (A-201); the type is a self-contained Go struct.
|
||||
type Packet struct {
|
||||
Sequence uint64 `json:"sequence" yaml:"sequence"`
|
||||
SourcePort string `json:"source_port" yaml:"source_port"`
|
||||
SourceChannel string `json:"source_channel" yaml:"source_channel"`
|
||||
DestPort string `json:"dest_port" yaml:"dest_port"`
|
||||
DestChannel string `json:"dest_channel" yaml:"dest_channel"`
|
||||
Data []byte `json:"data" yaml:"data"`
|
||||
TimeoutHeight uint64 `json:"timeout_height" yaml:"timeout_height"`
|
||||
TimeoutTimestamp uint64 `json:"timeout_timestamp" yaml:"timeout_timestamp"`
|
||||
}
|
||||
|
||||
// Params for the satellite module (skeleton — no tunables in v0.2).
|
||||
type Params struct{}
|
||||
|
||||
func DefaultParams() Params { return Params{} }
|
||||
|
||||
// GenesisState defines the satellite module genesis state (REQ-009).
|
||||
// Channels is the set of IBC transfer channels; Denoms is the set of wrapped
|
||||
// Bread denoms. ValidateGenesis enforces channel-id uniqueness and denom
|
||||
// uniqueness. The data-engineer's genesis.go holds the schema helpers (G-008).
|
||||
type GenesisState struct {
|
||||
Params Params `json:"params" yaml:"params"`
|
||||
Channels []TransferChannel `json:"channels" yaml:"channels"`
|
||||
Denoms []WrappedBreadDenom `json:"denoms" yaml:"denoms"`
|
||||
}
|
||||
|
||||
func DefaultGenesisState() *GenesisState {
|
||||
return &GenesisState{
|
||||
Params: DefaultParams(),
|
||||
Channels: []TransferChannel{},
|
||||
Denoms: []WrappedBreadDenom{},
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateGenesis performs ID-uniqueness checks (A-212 upgrade from v0.1
|
||||
// no-op): rejects duplicate channel-ids and duplicate denoms. 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("satellite: invalid genesis: %w", err)
|
||||
}
|
||||
if err := ValidateChannels(gs.Channels); err != nil {
|
||||
return fmt.Errorf("satellite: %w", err)
|
||||
}
|
||||
if err := ValidateDenoms(gs.Denoms); err != nil {
|
||||
return fmt.Errorf("satellite: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,467 @@
|
||||
package types_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/oy/openyield/lexicon"
|
||||
stypes "github.com/oy/openyield/x/satellite/types"
|
||||
)
|
||||
|
||||
// --- L2Chain enum (exactly 5, Polygon active + 4 stubs) ------------------------
|
||||
|
||||
// TestL2ChainCountLockedConst asserts L2ChainCount == 5 and AllL2Chains()
|
||||
// returns exactly 5 (REQ-009, D-021). A regression firewall.
|
||||
func TestL2ChainCountLockedConst(t *testing.T) {
|
||||
if stypes.L2ChainCount != 5 {
|
||||
t.Errorf("L2ChainCount = %d, expected 5 (REQ-009, D-021 LOCKED)", stypes.L2ChainCount)
|
||||
}
|
||||
all := stypes.AllL2Chains()
|
||||
if len(all) != 5 {
|
||||
t.Errorf("AllL2Chains() len = %d, expected 5", len(all))
|
||||
}
|
||||
}
|
||||
|
||||
// TestAllL2ChainsNames asserts the 5 chain names in order with no extras, no
|
||||
// dups, no renames (D-021: Polygon + Base/Arbitrum/Optimism/Solana).
|
||||
func TestAllL2ChainsNames(t *testing.T) {
|
||||
want := []string{"Polygon", "Base", "Arbitrum", "Optimism", "Solana"}
|
||||
all := stypes.AllL2Chains()
|
||||
if len(all) != len(want) {
|
||||
t.Fatalf("len = %d, want %d", len(all), len(want))
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for i, c := range all {
|
||||
if string(c.Chain) != want[i] {
|
||||
t.Errorf("AllL2Chains()[%d].Chain = %q, want %q", i, c.Chain, want[i])
|
||||
}
|
||||
if seen[string(c.Chain)] {
|
||||
t.Errorf("duplicate L2Chain %q", c.Chain)
|
||||
}
|
||||
seen[string(c.Chain)] = true
|
||||
}
|
||||
}
|
||||
|
||||
// TestL2ChainValues asserts each named const matches its AllL2Chains entry.
|
||||
func TestL2ChainValues(t *testing.T) {
|
||||
if stypes.ChainPolygon != "Polygon" {
|
||||
t.Errorf("ChainPolygon = %q", stypes.ChainPolygon)
|
||||
}
|
||||
if stypes.ChainBase != "Base" {
|
||||
t.Errorf("ChainBase = %q", stypes.ChainBase)
|
||||
}
|
||||
if stypes.ChainArbitrum != "Arbitrum" {
|
||||
t.Errorf("ChainArbitrum = %q", stypes.ChainArbitrum)
|
||||
}
|
||||
if stypes.ChainOptimism != "Optimism" {
|
||||
t.Errorf("ChainOptimism = %q", stypes.ChainOptimism)
|
||||
}
|
||||
if stypes.ChainSolana != "Solana" {
|
||||
t.Errorf("ChainSolana = %q", stypes.ChainSolana)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPolygonOnlyActiveRep asserts Polygon is the only ChainActive entry in
|
||||
// AllL2Chains (D-021: v0.2 scopes to ONE representative chain). The other
|
||||
// four must be StatusPending.
|
||||
func TestPolygonOnlyActiveRep(t *testing.T) {
|
||||
all := stypes.AllL2Chains()
|
||||
activeCount := 0
|
||||
for _, c := range all {
|
||||
if c.Activation == stypes.ChainActive {
|
||||
activeCount++
|
||||
if c.Chain != stypes.ChainPolygon {
|
||||
t.Errorf("chain %q is active, expected only Polygon (D-021)", c.Chain)
|
||||
}
|
||||
}
|
||||
if c.Activation == stypes.ChainStatusPending {
|
||||
if c.Chain == stypes.ChainPolygon {
|
||||
t.Error("Polygon must be active, not StatusPending (D-021)")
|
||||
}
|
||||
}
|
||||
}
|
||||
if activeCount != 1 {
|
||||
t.Errorf("expected exactly 1 active chain (Polygon, D-021), got %d", activeCount)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFourStubsAreStatusPending asserts Base, Arbitrum, Optimism, Solana are
|
||||
// all StatusPending (D-021 — the 4 stubs).
|
||||
func TestFourStubsAreStatusPending(t *testing.T) {
|
||||
stubs := []stypes.L2Chain{stypes.ChainBase, stypes.ChainArbitrum, stypes.ChainOptimism, stypes.ChainSolana}
|
||||
all := stypes.AllL2Chains()
|
||||
activationByChain := map[string]stypes.ChainActivation{}
|
||||
for _, c := range all {
|
||||
activationByChain[string(c.Chain)] = c.Activation
|
||||
}
|
||||
for _, s := range stubs {
|
||||
if activationByChain[string(s)] != stypes.ChainStatusPending {
|
||||
t.Errorf("chain %q activation = %q, expected StatusPending (D-021)", s, activationByChain[string(s)])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- ChannelStatus enum (4 states) ---------------------------------------------
|
||||
|
||||
// TestChannelStatusCountLockedConst asserts ChannelStatusCount == 4 and
|
||||
// AllChannelStatuses() returns exactly 4 (A-215 ICS-20 handshake).
|
||||
func TestChannelStatusCountLockedConst(t *testing.T) {
|
||||
if stypes.ChannelStatusCount != 4 {
|
||||
t.Errorf("ChannelStatusCount = %d, expected 4 (A-215 ICS-20)", stypes.ChannelStatusCount)
|
||||
}
|
||||
all := stypes.AllChannelStatuses()
|
||||
if len(all) != 4 {
|
||||
t.Errorf("AllChannelStatuses() len = %d, expected 4", len(all))
|
||||
}
|
||||
}
|
||||
|
||||
// TestAllChannelStatusesNames asserts the 4 ICS-20 handshake names in order.
|
||||
func TestAllChannelStatusesNames(t *testing.T) {
|
||||
want := []string{"Init", "TryOpen", "Open", "Closed"}
|
||||
all := stypes.AllChannelStatuses()
|
||||
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("AllChannelStatuses()[%d] = %q, want %q", i, s, want[i])
|
||||
}
|
||||
if seen[string(s)] {
|
||||
t.Errorf("duplicate ChannelStatus %q", s)
|
||||
}
|
||||
seen[string(s)] = true
|
||||
}
|
||||
}
|
||||
|
||||
// TestChannelStatusValues asserts each named const.
|
||||
func TestChannelStatusValues(t *testing.T) {
|
||||
if stypes.ChannelInit != "Init" {
|
||||
t.Errorf("ChannelInit = %q", stypes.ChannelInit)
|
||||
}
|
||||
if stypes.ChannelTryOpen != "TryOpen" {
|
||||
t.Errorf("ChannelTryOpen = %q", stypes.ChannelTryOpen)
|
||||
}
|
||||
if stypes.ChannelOpen != "Open" {
|
||||
t.Errorf("ChannelOpen = %q", stypes.ChannelOpen)
|
||||
}
|
||||
if stypes.ChannelClosed != "Closed" {
|
||||
t.Errorf("ChannelClosed = %q", stypes.ChannelClosed)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Packet struct fields (ICS-20 v1 shape — A-215) ---------------------------
|
||||
|
||||
// TestPacketFieldsMatchICS20v1 asserts the Packet struct has exactly the 8
|
||||
// ICS-20 v1 fields with the expected names. A-215 pins the packet shape to
|
||||
// ICS-20 v1 to minimize churn. Cross-check field names via JSON tags.
|
||||
func TestPacketFieldsMatchICS20v1(t *testing.T) {
|
||||
p := stypes.Packet{
|
||||
Sequence: 42,
|
||||
SourcePort: "transfer",
|
||||
SourceChannel: "channel-0",
|
||||
DestPort: "transfer",
|
||||
DestChannel: "channel-1",
|
||||
Data: []byte("payload"),
|
||||
TimeoutHeight: 1000,
|
||||
TimeoutTimestamp: 9999999999,
|
||||
}
|
||||
if p.Sequence != 42 || p.SourcePort != "transfer" || p.SourceChannel != "channel-0" ||
|
||||
p.DestPort != "transfer" || p.DestChannel != "channel-1" ||
|
||||
len(p.Data) != 7 || p.TimeoutHeight != 1000 || p.TimeoutTimestamp != 9999999999 {
|
||||
t.Error("Packet fields not set correctly")
|
||||
}
|
||||
// ICS-20 v1 field-name parity: marshal and check JSON tags.
|
||||
bz, err := json.Marshal(p)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
js := string(bz)
|
||||
wantTags := []string{
|
||||
`"sequence"`, `"source_port"`, `"source_channel"`, `"dest_port"`,
|
||||
`"dest_channel"`, `"data"`, `"timeout_height"`, `"timeout_timestamp"`,
|
||||
}
|
||||
for _, tag := range wantTags {
|
||||
if !strings.Contains(js, tag) {
|
||||
t.Errorf("Packet JSON missing tag %s (ICS-20 v1 shape parity A-215)", tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestPacketICS20v1FieldCount asserts the Packet struct has exactly 8 fields
|
||||
// (the ICS-20 v1 shape). A regression firewall for packet-shape drift.
|
||||
func TestPacketICS20v1FieldCount(t *testing.T) {
|
||||
// The 8 ICS-20 v1 fields: sequence, source_port, source_channel,
|
||||
// dest_port, dest_channel, data, timeout_height, timeout_timestamp.
|
||||
// We verify by constructing a Packet with all 8 fields and asserting
|
||||
// each is independently settable to a non-zero value.
|
||||
p := stypes.Packet{
|
||||
Sequence: 1,
|
||||
SourcePort: "sp",
|
||||
SourceChannel: "sc",
|
||||
DestPort: "dp",
|
||||
DestChannel: "dc",
|
||||
Data: []byte{0x01},
|
||||
TimeoutHeight: 1,
|
||||
TimeoutTimestamp: 1,
|
||||
}
|
||||
if p.Sequence != 1 || p.SourcePort != "sp" || p.SourceChannel != "sc" ||
|
||||
p.DestPort != "dp" || p.DestChannel != "dc" || len(p.Data) != 1 ||
|
||||
p.TimeoutHeight != 1 || p.TimeoutTimestamp != 1 {
|
||||
t.Error("Packet does not have all 8 ICS-20 v1 fields independently settable")
|
||||
}
|
||||
}
|
||||
|
||||
// --- WrappedBreadDenom trace-path encoding ------------------------------------
|
||||
|
||||
// TestWrappedBreadDenomStruct asserts the WrappedBreadDenom struct carries
|
||||
// the denom and trace-path fields.
|
||||
func TestWrappedBreadDenomStruct(t *testing.T) {
|
||||
d := stypes.WrappedBreadDenom{
|
||||
Denom: "transfer/channel-0/bread",
|
||||
TracePath: "transfer/channel-0",
|
||||
}
|
||||
if d.Denom != "transfer/channel-0/bread" {
|
||||
t.Errorf("Denom = %q", d.Denom)
|
||||
}
|
||||
if d.TracePath != "transfer/channel-0" {
|
||||
t.Errorf("TracePath = %q", d.TracePath)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWrappedBreadDenomTracePathEncoding asserts the IBC trace-path encoding
|
||||
// (REQ-009): the denom is the trace-path + "/" + original-denom.
|
||||
func TestWrappedBreadDenomTracePathEncoding(t *testing.T) {
|
||||
cases := []struct {
|
||||
trace string
|
||||
orig string
|
||||
}{
|
||||
{"transfer/channel-0", "bread"},
|
||||
{"transfer/channel-5", "bread"},
|
||||
{"transfer/channel-0/transfer/channel-3", "bread"}, // multi-hop
|
||||
}
|
||||
for _, c := range cases {
|
||||
full := c.trace + "/" + c.orig
|
||||
d := stypes.WrappedBreadDenom{Denom: full, TracePath: c.trace}
|
||||
if !strings.HasPrefix(d.Denom, d.TracePath) {
|
||||
t.Errorf("denom %q must start with trace-path %q", d.Denom, d.TracePath)
|
||||
}
|
||||
if !strings.HasSuffix(d.Denom, c.orig) {
|
||||
t.Errorf("denom %q must end with original denom %q", d.Denom, c.orig)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- TransferChannel ----------------------------------------------------------
|
||||
|
||||
// TestTransferChannelStruct asserts the TransferChannel struct carries all
|
||||
// required fields.
|
||||
func TestTransferChannelStruct(t *testing.T) {
|
||||
ch := stypes.TransferChannel{
|
||||
PortID: "transfer",
|
||||
ChannelID: "channel-0",
|
||||
Counterparty: "transfer/channel-0",
|
||||
Status: stypes.ChannelOpen,
|
||||
}
|
||||
if ch.PortID != "transfer" || ch.ChannelID != "channel-0" ||
|
||||
ch.Counterparty != "transfer/channel-0" || ch.Status != stypes.ChannelOpen {
|
||||
t.Error("TransferChannel fields not set correctly")
|
||||
}
|
||||
}
|
||||
|
||||
// --- Genesis -------------------------------------------------------------------
|
||||
|
||||
// TestDefaultGenesisStateEmpty asserts DefaultGenesisState returns non-nil
|
||||
// empty slices for Channels and Denoms.
|
||||
func TestDefaultGenesisStateEmpty(t *testing.T) {
|
||||
gs := stypes.DefaultGenesisState()
|
||||
if gs == nil {
|
||||
t.Fatal("DefaultGenesisState returned nil")
|
||||
}
|
||||
if gs.Channels == nil || len(gs.Channels) != 0 {
|
||||
t.Errorf("Default Channels should be non-nil empty slice; got len=%d nil=%v", len(gs.Channels), gs.Channels == nil)
|
||||
}
|
||||
if gs.Denoms == nil || len(gs.Denoms) != 0 {
|
||||
t.Errorf("Default Denoms should be non-nil empty slice; got len=%d nil=%v", len(gs.Denoms), gs.Denoms == nil)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsDupChannelIDs asserts A-212: duplicate
|
||||
// channel-ids are rejected.
|
||||
func TestValidateGenesisRejectsDupChannelIDs(t *testing.T) {
|
||||
gs := stypes.GenesisState{
|
||||
Channels: []stypes.TransferChannel{
|
||||
{PortID: "transfer", ChannelID: "channel-0", Status: stypes.ChannelOpen},
|
||||
{PortID: "transfer", ChannelID: "channel-0", Status: stypes.ChannelInit}, // dup
|
||||
},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := stypes.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject duplicate channel-ids")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsEmptyChannelID asserts empty channel-id is rejected.
|
||||
func TestValidateGenesisRejectsEmptyChannelID(t *testing.T) {
|
||||
gs := stypes.GenesisState{
|
||||
Channels: []stypes.TransferChannel{{PortID: "transfer", ChannelID: "", Status: stypes.ChannelInit}},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := stypes.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject empty channel-id")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsUnknownChannelStatus asserts an unknown
|
||||
// ChannelStatus is rejected.
|
||||
func TestValidateGenesisRejectsUnknownChannelStatus(t *testing.T) {
|
||||
gs := stypes.GenesisState{
|
||||
Channels: []stypes.TransferChannel{{PortID: "transfer", ChannelID: "channel-0", Status: stypes.ChannelStatus("Bogus")}},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := stypes.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject unknown channel status")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsDupDenom asserts duplicate denoms are rejected.
|
||||
func TestValidateGenesisRejectsDupDenom(t *testing.T) {
|
||||
gs := stypes.GenesisState{
|
||||
Denoms: []stypes.WrappedBreadDenom{
|
||||
{Denom: "transfer/channel-0/bread", TracePath: "transfer/channel-0"},
|
||||
{Denom: "transfer/channel-0/bread", TracePath: "transfer/channel-0"}, // dup
|
||||
},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := stypes.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject duplicate denoms")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsEmptyDenom asserts empty denom is rejected.
|
||||
func TestValidateGenesisRejectsEmptyDenom(t *testing.T) {
|
||||
gs := stypes.GenesisState{
|
||||
Denoms: []stypes.WrappedBreadDenom{{Denom: "", TracePath: "transfer/channel-0"}},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := stypes.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject empty denom")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsBadJSON asserts malformed JSON is rejected.
|
||||
func TestValidateGenesisRejectsBadJSON(t *testing.T) {
|
||||
if err := stypes.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 := stypes.GenesisState{
|
||||
Channels: []stypes.TransferChannel{
|
||||
{PortID: "transfer", ChannelID: "channel-0", Status: stypes.ChannelOpen},
|
||||
{PortID: "transfer", ChannelID: "channel-1", Status: stypes.ChannelInit},
|
||||
},
|
||||
Denoms: []stypes.WrappedBreadDenom{
|
||||
{Denom: "transfer/channel-0/bread", TracePath: "transfer/channel-0"},
|
||||
},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := stypes.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 stypes.ModuleName != "satellite" {
|
||||
t.Errorf("ModuleName = %q", stypes.ModuleName)
|
||||
}
|
||||
if stypes.StoreKey != "satellite" {
|
||||
t.Errorf("StoreKey = %q", stypes.StoreKey)
|
||||
}
|
||||
if stypes.RouterKey != "satellite" {
|
||||
t.Errorf("RouterKey = %q", stypes.RouterKey)
|
||||
}
|
||||
if stypes.QuerierRoute != "satellite" {
|
||||
t.Errorf("QuerierRoute = %q", stypes.QuerierRoute)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDefaultParams asserts DefaultParams returns a zero-value Params.
|
||||
func TestDefaultParams(t *testing.T) {
|
||||
_ = stypes.DefaultParams() // no panics
|
||||
}
|
||||
|
||||
// --- Lexicon assertion (REQ-012) -------------------------------------------------
|
||||
// The satellite 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.
|
||||
|
||||
// TestLexiconNoBannedTermsInSatellitePackage scans every non-test .go file in
|
||||
// the satellite/types package directory for the banned terms (case-
|
||||
// insensitive). Production files only — the test file references banned
|
||||
// terms via the lexicon package helpers.
|
||||
func TestLexiconNoBannedTermsInSatellitePackage(t *testing.T) {
|
||||
pkgDir := packageDir(t, "github.com/oy/openyield/x/satellite/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 satellite/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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestLexiconNoBannedTermsInSatelliteTestFile asserts this test file itself
|
||||
// does not contain any banned term as a literal.
|
||||
func TestLexiconNoBannedTermsInSatelliteTestFile(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("satellite 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.2 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/satellite/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