package keeper_test // msg_server_simtest_test.go is the x/bridge keeper simtest (P1-06-01). // // D-054: simtest-grade — in-memory sdk.Context + dbm in-memory store, no // real IBC light clients. The simtest wires the expected-keeper shims // (WatcherKeeper + BreadKeeper) to in-test stubs (G-003 test exemption: // the test imports x/bridge/keeper + defines stub keepers that satisfy the // interfaces; no production struct imports across x//types). // // Coverage (A-513, G-021): // - OnRecvPacket: mints wrapped Bread (assert BreadKeeper.MintWrappedBread // called); ICS-20 v1 denom trace parse; Solana guardian sig set (2-of-N // stub). // - OnAcknowledgementPacket: deletes the in-flight record (first ack) and // rejects the second (REPLAY PROTECTION — G-021, A-513 CVE-class pitfall). // - OnTimeoutPacket: refunds the escrow exactly once (second timeout is a // no-op — the Refunded flag guards). // - BridgeStatus lifecycle: Pending → Attested (MsgAttestBridgeRoute) → // Active (MsgActivateBridge) → Closed (MsgCloseBridge). // - Solana stub guardian sig set (2-of-N). import ( "encoding/json" "testing" "cosmossdk.io/log" "cosmossdk.io/store" storetypes "cosmossdk.io/store/types" cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" dbm "github.com/cosmos/cosmos-db" "github.com/cosmos/cosmos-sdk/codec" codectypes "github.com/cosmos/cosmos-sdk/codec/types" sdk "github.com/cosmos/cosmos-sdk/types" channeltypes "github.com/cosmos/ibc-go/v8/modules/core/04-channel/types" "github.com/oy/openyield/x/bridge/keeper" bridgetypes "github.com/oy/openyield/x/bridge/types" ) // --- Stub expected-keepers (G-003 test exemption) ---------------------------- // stubWatcherKeeper satisfies bridgetypes.WatcherKeeper for the simtest. The // IsQuorumSigned returns true for the configured quorum-id (the simtest // stubs the Watcher 6-of-9 quorum + the Solana guardian 2-of-N quorum). type stubWatcherKeeper struct { // signedQuorums maps quorum-id → true if the quorum reached threshold. signedQuorums map[string]bool // solanaCalls tracks IsQuorumSigned invocations for the Solana branch. solanaCalls int } func (s *stubWatcherKeeper) IsQuorumSigned(quorumID string, payload []byte) bool { if quorumID == "solana-guardians" { s.solanaCalls++ } return s.signedQuorums[quorumID] } // stubBreadKeeper satisfies bridgetypes.BreadKeeper for the simtest. It // records mint/release calls for assertion. type stubBreadKeeper struct { mints []mintCall releases []releaseCall } type mintCall struct { denom string amount int64 reachID string } type releaseCall struct { denom string amount int64 reachID string } func (s *stubBreadKeeper) MintWrappedBread(ctx interface{}, denom string, amount int64, holderReach string) error { s.mints = append(s.mints, mintCall{denom, amount, holderReach}) return nil } func (s *stubBreadKeeper) ReleaseWrappedBread(ctx interface{}, denom string, amount int64, holderReach string) error { s.releases = append(s.releases, releaseCall{denom, amount, holderReach}) return nil } // --- Simtest context helper -------------------------------------------------- // newSimtestContext constructs an in-memory sdk.Context with a KVStore mounted // at the bridge store key. D-054: in-memory, no real IBC light clients. func newSimtestContext(t *testing.T) (sdk.Context, *stubWatcherKeeper, *stubBreadKeeper, keeper.Keeper) { t.Helper() db := dbm.NewMemDB() cdc := newTestCodec() storeKey := storetypes.NewKVStoreKey(bridgetypes.StoreKey) cms := store.NewCommitMultiStore(db, log.NewNopLogger(), nil) cms.MountStoreWithDB(storeKey, storetypes.StoreTypeDB, nil) if err := cms.LoadLatestVersion(); err != nil { t.Fatalf("load latest version: %v", err) } ctx := sdk.NewContext(cms, cmtproto.Header{}, false, log.NewNopLogger()) wk := &stubWatcherKeeper{signedQuorums: map[string]bool{}} bk := &stubBreadKeeper{} k := keeper.NewKeeper(cdc, storeKey, wk, bk) return ctx, wk, bk, k } // newTestCodec constructs a minimal codec for the simtest (the keeper uses // JSON marshaling, so a bare proto codec suffices). func newTestCodec() codec.Codec { registry := codectypes.NewInterfaceRegistry() return codec.NewProtoCodec(registry) } // --- ICS-20 v1 packet helpers ------------------------------------------------ // ics20PacketData returns the ICS-20 v1 packet payload (matches // keeper.ICS20PacketData). func ics20PacketData(denom, amount, sender, receiver string) []byte { bz, _ := json.Marshal(map[string]string{ "denom": denom, "amount": amount, "sender": sender, "receiver": receiver, }) return bz } // newPacket constructs a real channeltypes.Packet for the simtest. func newPacket(sourcePort, sourceChannel string, sequence uint64, data []byte) channeltypes.Packet { return channeltypes.Packet{ SourcePort: sourcePort, SourceChannel: sourceChannel, Sequence: sequence, Data: data, } } // --- OnRecvPacket: mint wrapped Bread + denom trace + Solana ---------------- // TestOnRecvPacketMintsWrappedBread asserts OnRecvPacket mints wrapped Bread // for a valid ICS-20 v1 packet (EVM chain). func TestOnRecvPacketMintsWrappedBread(t *testing.T) { ctx, _, bk, k := newSimtestContext(t) im := keeper.NewIBCModule(k) packet := newPacket("transfer.Polygon", "channel-0", 1, ics20PacketData( "transfer/channel-0/uatom", "1000", "sender-reach", "receiver-reach")) ack := im.OnRecvPacket(ctx, packet, sdk.AccAddress([]byte("relayer"))) if !ack.Success() { t.Fatalf("OnRecvPacket should succeed; got error ack") } if len(bk.mints) != 1 { t.Fatalf("expected 1 mint call, got %d", len(bk.mints)) } if bk.mints[0].denom != "transfer/channel-0/uatom" { t.Errorf("mint denom = %q, want transfer/channel-0/uatom", bk.mints[0].denom) } if bk.mints[0].amount != 1000 { t.Errorf("mint amount = %d, want 1000", bk.mints[0].amount) } if bk.mints[0].reachID != "receiver-reach" { t.Errorf("mint reach = %q, want receiver-reach", bk.mints[0].reachID) } // In-flight record written. if _, ok := k.GetInflight(ctx, packet.SourcePort, packet.SourceChannel, packet.Sequence); !ok { t.Error("in-flight record not written after OnRecvPacket") } } // TestOnRecvPacketRejectsBadDenomTrace asserts OnRecvPacket rejects a packet // whose denom trace lacks the `transfer/channel-N/` hop prefix. func TestOnRecvPacketRejectsBadDenomTrace(t *testing.T) { ctx, _, bk, k := newSimtestContext(t) im := keeper.NewIBCModule(k) packet := newPacket("transfer.Polygon", "channel-0", 1, ics20PacketData( "uatom", "1000", "sender", "receiver")) // no hop prefix ack := im.OnRecvPacket(ctx, packet, sdk.AccAddress{}) if ack.Success() { t.Error("OnRecvPacket should fail on bad denom trace") } if len(bk.mints) != 0 { t.Errorf("no mint should happen on bad denom trace; got %d", len(bk.mints)) } } // TestOnRecvPacketRejectsBadICS20 asserts a malformed ICS-20 payload is rejected. func TestOnRecvPacketRejectsBadICS20(t *testing.T) { ctx, _, bk, k := newSimtestContext(t) im := keeper.NewIBCModule(k) packet := newPacket("transfer.Polygon", "channel-0", 1, []byte("not-json")) ack := im.OnRecvPacket(ctx, packet, sdk.AccAddress{}) if ack.Success() { t.Error("OnRecvPacket should fail on malformed ICS-20") } if len(bk.mints) != 0 { t.Errorf("no mint on bad ICS-20; got %d", len(bk.mints)) } } // TestOnRecvPacketSolanaGuardianSigSet asserts the Solana branch verifies the // wormhole guardian sig set (2-of-N stub) from state before minting. func TestOnRecvPacketSolanaGuardianSigSet(t *testing.T) { ctx, wk, bk, k := newSimtestContext(t) im := keeper.NewIBCModule(k) // Configure the frozen stub guardian set (D-054 — frozen in simtest). k.SetGuardianSet(ctx, keeper.GuardianSet{ Guardians: []string{"guardian-1", "guardian-2", "guardian-3"}, Threshold: 2, }) wk.signedQuorums["solana-guardians"] = true packet := newPacket("transfer.Solana", "channel-1", 1, ics20PacketData( "transfer/channel-1/wsol", "500", "sol-sender", "sol-receiver")) ack := im.OnRecvPacket(ctx, packet, sdk.AccAddress{}) if !ack.Success() { t.Fatalf("OnRecvPacket Solana should succeed with guardian quorum; got error") } if len(bk.mints) != 1 { t.Fatalf("expected 1 mint for Solana, got %d", len(bk.mints)) } if bk.mints[0].denom != "transfer/channel-1/wsol" { t.Errorf("mint denom = %q", bk.mints[0].denom) } if wk.solanaCalls != 1 { t.Errorf("expected 1 Solana guardian sig check, got %d", wk.solanaCalls) } } // TestOnRecvPacketSolanaRejectsNoGuardianSet asserts the Solana branch rejects // when the guardian set is not configured. func TestOnRecvPacketSolanaRejectsNoGuardianSet(t *testing.T) { ctx, _, bk, k := newSimtestContext(t) im := keeper.NewIBCModule(k) // No guardian set configured. packet := newPacket("transfer.Solana", "channel-1", 1, ics20PacketData( "transfer/channel-1/wsol", "500", "sender", "receiver")) ack := im.OnRecvPacket(ctx, packet, sdk.AccAddress{}) if ack.Success() { t.Error("OnRecvPacket Solana should fail without guardian set") } if len(bk.mints) != 0 { t.Errorf("no mint should happen; got %d", len(bk.mints)) } } // TestOnRecvPacketSolanaRejectsNoQuorum asserts the Solana branch rejects when // the guardian sig set did not reach the 2-of-N quorum. func TestOnRecvPacketSolanaRejectsNoQuorum(t *testing.T) { ctx, wk, bk, k := newSimtestContext(t) im := keeper.NewIBCModule(k) k.SetGuardianSet(ctx, keeper.GuardianSet{ Guardians: []string{"guardian-1", "guardian-2", "guardian-3"}, Threshold: 2, }) wk.signedQuorums["solana-guardians"] = false // quorum NOT reached packet := newPacket("transfer.Solana", "channel-1", 1, ics20PacketData( "transfer/channel-1/wsol", "500", "sender", "receiver")) ack := im.OnRecvPacket(ctx, packet, sdk.AccAddress{}) if ack.Success() { t.Error("OnRecvPacket Solana should fail without quorum") } if len(bk.mints) != 0 { t.Errorf("no mint on Solana quorum failure; got %d", len(bk.mints)) } } // TestOnRecvPacketRejectsZeroAmount asserts a zero/negative amount is rejected. func TestOnRecvPacketRejectsZeroAmount(t *testing.T) { ctx, _, bk, k := newSimtestContext(t) im := keeper.NewIBCModule(k) packet := newPacket("transfer.Polygon", "channel-0", 1, ics20PacketData( "transfer/channel-0/uatom", "0", "sender", "receiver")) ack := im.OnRecvPacket(ctx, packet, sdk.AccAddress{}) if ack.Success() { t.Error("OnRecvPacket should reject zero amount") } if len(bk.mints) != 0 { t.Errorf("no mint on zero amount; got %d", len(bk.mints)) } } // --- OnAcknowledgementPacket: delete-on-first-ack + ERROR-on-second (G-021) -- // TestOnAckPacketDeletesInflightRecord asserts OnAcknowledgementPacket deletes // the in-flight record on the first ack (replay protection mirroring ibc-go). func TestOnAckPacketDeletesInflightRecord(t *testing.T) { ctx, _, _, k := newSimtestContext(t) im := keeper.NewIBCModule(k) k.SetInflight(ctx, keeper.InflightPacket{ SourcePort: "transfer.Polygon", SourceChannel: "channel-0", Sequence: 7, Denom: "transfer/channel-0/uatom", Amount: 1000, Sender: "s", Receiver: "r", }) packet := newPacket("transfer.Polygon", "channel-0", 7, ics20PacketData( "transfer/channel-0/uatom", "1000", "s", "r")) if err := im.OnAcknowledgementPacket(ctx, packet, []byte(`{}`), sdk.AccAddress{}); err != nil { t.Fatalf("first ack should succeed, got: %v", err) } if _, ok := k.GetInflight(ctx, packet.SourcePort, packet.SourceChannel, packet.Sequence); ok { t.Error("in-flight record should be deleted after first ack") } } // TestOnAckPacketRejectsSecondAck asserts the SECOND OnAcknowledgementPacket // returns ERROR (G-021 — NOT a silent no-op; the A-513 CVE-class replay pitfall // is closed by failing loudly). func TestOnAckPacketRejectsSecondAck(t *testing.T) { ctx, _, _, k := newSimtestContext(t) im := keeper.NewIBCModule(k) k.SetInflight(ctx, keeper.InflightPacket{ SourcePort: "transfer.Polygon", SourceChannel: "channel-0", Sequence: 9, }) packet := newPacket("transfer.Polygon", "channel-0", 9, ics20PacketData( "transfer/channel-0/uatom", "1000", "s", "r")) _ = im.OnAcknowledgementPacket(ctx, packet, []byte(`{}`), sdk.AccAddress{}) // Second ack: record is gone → ERROR (G-021). err := im.OnAcknowledgementPacket(ctx, packet, []byte(`{}`), sdk.AccAddress{}) if err == nil { t.Fatal("G-021: second OnAcknowledgementPacket must return ERROR, not nil (A-513 replay pitfall)") } } // TestOnAckPacketNoInflightRecordReturnsError asserts an ack with no prior // in-flight record returns ERROR (the replay signal — G-021). func TestOnAckPacketNoInflightRecordReturnsError(t *testing.T) { ctx, _, _, k := newSimtestContext(t) im := keeper.NewIBCModule(k) packet := newPacket("transfer.Polygon", "channel-0", 42, ics20PacketData( "transfer/channel-0/uatom", "1000", "s", "r")) err := im.OnAcknowledgementPacket(ctx, packet, []byte(`{}`), sdk.AccAddress{}) if err == nil { t.Error("ack with no in-flight record should return ERROR (G-021 replay signal)") } } // --- OnTimeoutPacket: refund exactly once ------------------------------------ // TestOnTimeoutPacketRefundsOnce asserts OnTimeoutPacket refunds the // source-chain escrow via the BreadKeeper shim exactly once. func TestOnTimeoutPacketRefundsOnce(t *testing.T) { ctx, _, bk, k := newSimtestContext(t) im := keeper.NewIBCModule(k) k.SetInflight(ctx, keeper.InflightPacket{ SourcePort: "transfer.Polygon", SourceChannel: "channel-0", Sequence: 3, Denom: "transfer/channel-0/uatom", Amount: 750, Sender: "timeout-sender", Receiver: "r", Refunded: false, }) packet := newPacket("transfer.Polygon", "channel-0", 3, ics20PacketData( "transfer/channel-0/uatom", "750", "timeout-sender", "r")) if err := im.OnTimeoutPacket(ctx, packet, sdk.AccAddress{}); err != nil { t.Fatalf("first timeout should succeed: %v", err) } if len(bk.releases) != 1 { t.Fatalf("expected 1 release on first timeout, got %d", len(bk.releases)) } if bk.releases[0].amount != 750 { t.Errorf("release amount = %d, want 750", bk.releases[0].amount) } if bk.releases[0].reachID != "timeout-sender" { t.Errorf("release reach = %q, want timeout-sender", bk.releases[0].reachID) } // Second timeout: no-op (Refunded flag guards exactly-once). if err := im.OnTimeoutPacket(ctx, packet, sdk.AccAddress{}); err != nil { t.Fatalf("second timeout should be a no-op (nil), got: %v", err) } if len(bk.releases) != 1 { t.Errorf("second timeout should NOT refund again; got %d releases total", len(bk.releases)) } } // TestOnTimeoutPacketNoInflightRecordIsNoop asserts a timeout with no // in-flight record is a benign no-op (not an error). func TestOnTimeoutPacketNoInflightRecordIsNoop(t *testing.T) { ctx, _, bk, k := newSimtestContext(t) im := keeper.NewIBCModule(k) packet := newPacket("transfer.Polygon", "channel-0", 99, ics20PacketData( "transfer/channel-0/uatom", "1000", "s", "r")) err := im.OnTimeoutPacket(ctx, packet, sdk.AccAddress{}) if err != nil { t.Errorf("timeout with no in-flight record should be a no-op (nil); got %v", err) } if len(bk.releases) != 0 { t.Errorf("no release should happen; got %d", len(bk.releases)) } } // --- BridgeStatus lifecycle (MsgServer) -------------------------------------- // TestBridgeStatusLifecycle asserts the full BridgeStatus lifecycle: // Pending → Attested → Active → Closed. func TestBridgeStatusLifecycle(t *testing.T) { ctx, wk, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) k.SetBridgeRoute(ctx, bridgetypes.BridgeRoute{ BridgeID: "bridge-1", L2Chain: "Polygon", Status: bridgetypes.BridgePending, }) wk.signedQuorums["quorum-1"] = true // Pending → Attested. if _, err := srv.AttestBridgeRoute(ctx, &bridgetypes.MsgAttestBridgeRoute{ BridgeID: "bridge-1", WatcherQuorumID: "quorum-1", Signer: "watcher-reach", }); err != nil { t.Fatalf("AttestBridgeRoute: %v", err) } r, _ := k.GetBridgeRoute(ctx, "bridge-1") if r.Status != bridgetypes.BridgeAttested { t.Errorf("after attest, status = %q, want Attested", r.Status) } if r.WatcherQuorumID != "quorum-1" { t.Errorf("watcher quorum id = %q, want quorum-1", r.WatcherQuorumID) } // Attested → Active. if _, err := srv.ActivateBridge(ctx, &bridgetypes.MsgActivateBridge{ BridgeID: "bridge-1", Signer: "watcher-reach", }); err != nil { t.Fatalf("ActivateBridge: %v", err) } r, _ = k.GetBridgeRoute(ctx, "bridge-1") if r.Status != bridgetypes.BridgeActive { t.Errorf("after activate, status = %q, want Active", r.Status) } // Active → Closed. if _, err := srv.CloseBridge(ctx, &bridgetypes.MsgCloseBridge{ BridgeID: "bridge-1", Signer: "watcher-reach", }); err != nil { t.Fatalf("CloseBridge: %v", err) } r, _ = k.GetBridgeRoute(ctx, "bridge-1") if r.Status != bridgetypes.BridgeClosed { t.Errorf("after close, status = %q, want Closed", r.Status) } } // TestAttestBridgeRouteRejectsBadStatus asserts AttestBridgeRoute rejects a // route that is not Pending. func TestAttestBridgeRouteRejectsBadStatus(t *testing.T) { ctx, wk, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) wk.signedQuorums["quorum-1"] = true k.SetBridgeRoute(ctx, bridgetypes.BridgeRoute{ BridgeID: "bridge-2", L2Chain: "Base", Status: bridgetypes.BridgeActive, }) _, err := srv.AttestBridgeRoute(ctx, &bridgetypes.MsgAttestBridgeRoute{ BridgeID: "bridge-2", WatcherQuorumID: "quorum-1", Signer: "watcher-reach", }) if err == nil { t.Error("AttestBridgeRoute should reject an Active route (must be Pending)") } } // TestAttestBridgeRouteRejectsNoQuorum asserts AttestBridgeRoute rejects when // the Watcher quorum did not reach threshold. func TestAttestBridgeRouteRejectsNoQuorum(t *testing.T) { ctx, wk, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) wk.signedQuorums["quorum-1"] = false k.SetBridgeRoute(ctx, bridgetypes.BridgeRoute{ BridgeID: "bridge-3", L2Chain: "Polygon", Status: bridgetypes.BridgePending, }) _, err := srv.AttestBridgeRoute(ctx, &bridgetypes.MsgAttestBridgeRoute{ BridgeID: "bridge-3", WatcherQuorumID: "quorum-1", Signer: "watcher-reach", }) if err == nil { t.Error("AttestBridgeRoute should reject when Watcher quorum not signed") } } // TestAttestBridgeRouteRejectsNotFound asserts AttestBridgeRoute rejects a // missing route. func TestAttestBridgeRouteRejectsNotFound(t *testing.T) { ctx, wk, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) wk.signedQuorums["quorum-1"] = true _, err := srv.AttestBridgeRoute(ctx, &bridgetypes.MsgAttestBridgeRoute{ BridgeID: "missing", WatcherQuorumID: "quorum-1", Signer: "watcher-reach", }) if err == nil { t.Error("AttestBridgeRoute should reject a missing route") } } // TestActivateBridgeRejectsBadStatus asserts ActivateBridge rejects a route // that is not Attested. func TestActivateBridgeRejectsBadStatus(t *testing.T) { ctx, _, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) k.SetBridgeRoute(ctx, bridgetypes.BridgeRoute{ BridgeID: "bridge-4", L2Chain: "Polygon", Status: bridgetypes.BridgePending, }) _, err := srv.ActivateBridge(ctx, &bridgetypes.MsgActivateBridge{ BridgeID: "bridge-4", Signer: "watcher-reach", }) if err == nil { t.Error("ActivateBridge should reject a Pending route (must be Attested)") } } // TestCloseBridgeRejectsBadStatus asserts CloseBridge rejects a route that is // not Active. func TestCloseBridgeRejectsBadStatus(t *testing.T) { ctx, _, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) k.SetBridgeRoute(ctx, bridgetypes.BridgeRoute{ BridgeID: "bridge-5", L2Chain: "Polygon", Status: bridgetypes.BridgeAttested, }) _, err := srv.CloseBridge(ctx, &bridgetypes.MsgCloseBridge{ BridgeID: "bridge-5", Signer: "watcher-reach", }) if err == nil { t.Error("CloseBridge should reject an Attested route (must be Active)") } } // --- ValidateBasic (Msg types) ----------------------------------------------- func TestMsgAttestBridgeRouteValidateBasic(t *testing.T) { cases := []struct { name string msg bridgetypes.MsgAttestBridgeRoute ok bool }{ {"valid", bridgetypes.MsgAttestBridgeRoute{"b1", "q1", "s"}, true}, {"empty bridge-id", bridgetypes.MsgAttestBridgeRoute{"", "q1", "s"}, false}, {"empty quorum-id", bridgetypes.MsgAttestBridgeRoute{"b1", "", "s"}, false}, {"empty signer", bridgetypes.MsgAttestBridgeRoute{"b1", "q1", ""}, false}, } for _, c := range cases { err := c.msg.ValidateBasic() if c.ok && err != nil { t.Errorf("%s: expected ok, got %v", c.name, err) } if !c.ok && err == nil { t.Errorf("%s: expected error, got nil", c.name) } } } func TestMsgActivateBridgeValidateBasic(t *testing.T) { if err := (&bridgetypes.MsgActivateBridge{BridgeID: "b1", Signer: "s"}).ValidateBasic(); err != nil { t.Errorf("valid: %v", err) } if err := (&bridgetypes.MsgActivateBridge{BridgeID: "", Signer: "s"}).ValidateBasic(); err == nil { t.Error("empty bridge-id should fail") } } func TestMsgCloseBridgeValidateBasic(t *testing.T) { if err := (&bridgetypes.MsgCloseBridge{BridgeID: "b1", Signer: "s"}).ValidateBasic(); err != nil { t.Errorf("valid: %v", err) } if err := (&bridgetypes.MsgCloseBridge{BridgeID: "b1", Signer: ""}).ValidateBasic(); err == nil { t.Error("empty signer should fail") } } // TestMsgGetSigners asserts GetSigners returns the signer reach-id as bytes. func TestMsgGetSigners(t *testing.T) { m := &bridgetypes.MsgAttestBridgeRoute{Signer: "watcher-reach"} addrs := m.GetSigners() if len(addrs) != 1 { t.Fatalf("expected 1 signer, got %d", len(addrs)) } if string(addrs[0]) != "watcher-reach" { t.Errorf("signer = %q, want watcher-reach", string(addrs[0])) } } // --- Denom trace parser ------------------------------------------------------ func TestParseDenomTrace(t *testing.T) { cases := []struct { denom string wantPrefix string wantBase string }{ {"transfer/channel-0/uatom", "transfer/channel-0", "uatom"}, {"transfer/channel-1/wsol", "transfer/channel-1", "wsol"}, {"uatom", "", "uatom"}, {"", "", ""}, } for _, c := range cases { p, b := keeper.ParseDenomTrace(c.denom) if p != c.wantPrefix || b != c.wantBase { t.Errorf("ParseDenomTrace(%q) = (%q,%q), want (%q,%q)", c.denom, p, b, c.wantPrefix, c.wantBase) } } } func TestValidateDenomTrace(t *testing.T) { if err := keeper.ValidateDenomTrace("transfer/channel-0/uatom"); err != nil { t.Errorf("valid denom trace: %v", err) } if err := keeper.ValidateDenomTrace("uatom"); err == nil { t.Error("bare denom (no hop prefix) should fail") } if err := keeper.ValidateDenomTrace(""); err == nil { t.Error("empty denom should fail") } } // --- Keeper store helpers ---------------------------------------------------- func TestSetGetBridgeRoute(t *testing.T) { ctx, _, _, k := newSimtestContext(t) r := bridgetypes.BridgeRoute{BridgeID: "b9", L2Chain: "Polygon", Status: bridgetypes.BridgePending} k.SetBridgeRoute(ctx, r) got, ok := k.GetBridgeRoute(ctx, "b9") if !ok { t.Fatal("GetBridgeRoute: not found") } if got.L2Chain != "Polygon" { t.Errorf("L2Chain = %q", got.L2Chain) } if _, ok := k.GetBridgeRoute(ctx, "missing"); ok { t.Error("GetBridgeRoute should return false for missing route") } } func TestAllBridgeRoutes(t *testing.T) { ctx, _, _, k := newSimtestContext(t) k.SetBridgeRoute(ctx, bridgetypes.BridgeRoute{BridgeID: "b1", Status: bridgetypes.BridgePending}) k.SetBridgeRoute(ctx, bridgetypes.BridgeRoute{BridgeID: "b2", Status: bridgetypes.BridgeActive}) all := k.AllBridgeRoutes(ctx) if len(all) != 2 { t.Errorf("expected 2 routes, got %d", len(all)) } } func TestGuardianSetStore(t *testing.T) { ctx, _, _, k := newSimtestContext(t) gs := keeper.GuardianSet{ Guardians: []string{"g1", "g2", "g3"}, Threshold: 2, } k.SetGuardianSet(ctx, gs) got, ok := k.GetGuardianSet(ctx) if !ok { t.Fatal("GetGuardianSet: not found") } if got.Threshold != 2 { t.Errorf("threshold = %d, want 2", got.Threshold) } if len(got.Guardians) != 3 { t.Errorf("guardians = %d, want 3", len(got.Guardians)) } }