6c34650a0d
v0.5 Bearers Runtime — 7 runtime REQs (REQ-033..039) shipped as feature. 8 modules promoted to runtime (MsgServer + simtest). cosmos-sdk v0.50.8 + ibc-go v8.2.1 added (G-006 controlled exception). G-003 + locked-const firewalls intact. 8 keeper packages ≥80% coverage. 5 GRILL decisions ratified; 8 binding fixes landed; 5 P1+ flagged for v0.6+. ---ci--- project: oy phase: 8 milestone: v0.5 status: complete requirements: covered: [REQ-033, REQ-034, REQ-035, REQ-036, REQ-037, REQ-038, REQ-039] partial: [] ---/ci---
516 lines
18 KiB
Go
516 lines
18 KiB
Go
package keeper_test
|
|
|
|
// msg_server_simtest_test.go is the x/exit 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 shim
|
|
// (BridgeKeeper) to an in-test stub (G-003 test exemption: the test imports
|
|
// x/exit/keeper + defines a stub BridgeKeeper that satisfies the interface;
|
|
// no production struct imports across x/<module>/types).
|
|
//
|
|
// Coverage (A-513, G-021):
|
|
// - ExitStatus lifecycle: Proposed → InProgress → Settled; Failed → Refunded.
|
|
// - Cross-chain exit via BridgeKeeper shim (G-003 test exemption — wired to
|
|
// a stub that returns Active status; the simtest asserts the shim is called).
|
|
// - Fee Covenant clamp event (exit-fee-bps clamped to [1, 10] bps).
|
|
// - Replay rejection (duplicate MsgExecuteDEXSwap on a Settled route is an
|
|
// error — the route is terminal).
|
|
|
|
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"
|
|
|
|
"github.com/oy/openyield/x/exit/keeper"
|
|
exittypes "github.com/oy/openyield/x/exit/types"
|
|
)
|
|
|
|
// --- Stub expected-keeper (G-003 test exemption) -----------------------------
|
|
|
|
// stubBridgeKeeper satisfies exittypes.BridgeKeeper for the simtest. It
|
|
// records GetBridgeRoute calls and returns the configured status/bridge-type.
|
|
type stubBridgeKeeper struct {
|
|
// routes maps bridge-id → (status, bridgeType).
|
|
routes map[string]stubBridgeRoute
|
|
calls int
|
|
}
|
|
|
|
type stubBridgeRoute struct {
|
|
status string
|
|
bridgeType string
|
|
}
|
|
|
|
func (s *stubBridgeKeeper) GetBridgeRoute(routeID string) (status string, bridgeType string, err error) {
|
|
s.calls++
|
|
r, ok := s.routes[routeID]
|
|
if !ok {
|
|
return "", "", nil // not found: status "" → handler fails the exit
|
|
}
|
|
return r.status, r.bridgeType, nil
|
|
}
|
|
|
|
// --- Simtest context helper --------------------------------------------------
|
|
|
|
// newSimtestContext constructs an in-memory sdk.Context with a KVStore mounted
|
|
// at the exit store key. D-054: in-memory, no real IBC light clients.
|
|
func newSimtestContext(t *testing.T) (sdk.Context, *stubBridgeKeeper, keeper.Keeper) {
|
|
t.Helper()
|
|
db := dbm.NewMemDB()
|
|
cdc := newTestCodec()
|
|
storeKey := storetypes.NewKVStoreKey(exittypes.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())
|
|
|
|
bk := &stubBridgeKeeper{routes: map[string]stubBridgeRoute{}}
|
|
k := keeper.NewKeeper(cdc, storeKey, bk)
|
|
return ctx, bk, k
|
|
}
|
|
|
|
// newTestCodec constructs a minimal codec for the simtest.
|
|
func newTestCodec() codec.Codec {
|
|
registry := codectypes.NewInterfaceRegistry()
|
|
return codec.NewProtoCodec(registry)
|
|
}
|
|
|
|
// hasEvent reports whether ctx emitted an event of the given type.
|
|
func hasEvent(ctx sdk.Context, eventType string) bool {
|
|
for _, ev := range ctx.EventManager().Events() {
|
|
if ev.Type == eventType {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// eventAttr returns the value of an attribute on the last event of the given
|
|
// type, or "" if not found.
|
|
func eventAttr(ctx sdk.Context, eventType, attrKey string) string {
|
|
for _, ev := range ctx.EventManager().Events() {
|
|
if ev.Type == eventType {
|
|
for _, a := range ev.Attributes {
|
|
if string(a.Key) == attrKey {
|
|
return string(a.Value)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// --- ExitStatus lifecycle: Proposed → InProgress → Settled -------------------
|
|
|
|
// TestExitStatusLifecycleProposedToSettled asserts the full success lifecycle:
|
|
// SubmitExitRoute (Proposed) → ExecuteDEXSwap (InProgress → Settled). The
|
|
// DEXSwap record is produced. The Fee Covenant clamp event is emitted.
|
|
func TestExitStatusLifecycleProposedToSettled(t *testing.T) {
|
|
ctx, _, k := newSimtestContext(t)
|
|
srv := keeper.NewMsgServerImpl(k)
|
|
|
|
// SubmitExitRoute → Proposed.
|
|
if _, err := srv.SubmitExitRoute(ctx, &exittypes.MsgSubmitExitRoute{
|
|
RouteID: "route-1", HolderReachID: "holder-1",
|
|
SourceAsset: "ubread", DestAsset: "uatom", Amount: 500, Signer: "holder-1",
|
|
}); err != nil {
|
|
t.Fatalf("SubmitExitRoute: %v", err)
|
|
}
|
|
r, ok := k.GetExitRoute(ctx, "route-1")
|
|
if !ok {
|
|
t.Fatal("route not found after submit")
|
|
}
|
|
if r.Status != exittypes.ExitProposed {
|
|
t.Errorf("status = %q, want Proposed", r.Status)
|
|
}
|
|
if !hasEvent(ctx, "exit.submit_route") {
|
|
t.Error("submit_route event not emitted")
|
|
}
|
|
|
|
// ExecuteDEXSwap → InProgress → Settled (same-chain exit, no bridge-route-id).
|
|
if _, err := srv.ExecuteDEXSwap(ctx, &exittypes.MsgExecuteDEXSwap{
|
|
RouteID: "route-1", Venue: "uniswap-v3:5", Signer: "holder-1",
|
|
}); err != nil {
|
|
t.Fatalf("ExecuteDEXSwap: %v", err)
|
|
}
|
|
r, _ = k.GetExitRoute(ctx, "route-1")
|
|
if r.Status != exittypes.ExitSettled {
|
|
t.Errorf("status = %q, want Settled", r.Status)
|
|
}
|
|
|
|
// DEXSwap record produced.
|
|
swap, ok := k.GetDEXSwap(ctx, "route-1-swap")
|
|
if !ok {
|
|
t.Fatal("DEXSwap record not produced")
|
|
}
|
|
if swap.Status != exittypes.ExitSettled {
|
|
t.Errorf("swap status = %q, want Settled", swap.Status)
|
|
}
|
|
|
|
// Fee Covenant clamp event emitted (5 bps → within [1,10], no clamp).
|
|
if !hasEvent(ctx, "exit.fee_covenant_clamp") {
|
|
t.Error("fee_covenant_clamp event not emitted")
|
|
}
|
|
if !hasEvent(ctx, "exit.settled") {
|
|
t.Error("settled event not emitted")
|
|
}
|
|
}
|
|
|
|
// TestFeeCovenantClampHighFee asserts a fee above the ceiling (10 bps) is
|
|
// clamped to the ceiling (10 bps) — the Fee Covenant auto-decline-only rule.
|
|
func TestFeeCovenantClampHighFee(t *testing.T) {
|
|
ctx, _, k := newSimtestContext(t)
|
|
srv := keeper.NewMsgServerImpl(k)
|
|
|
|
srv.SubmitExitRoute(ctx, &exittypes.MsgSubmitExitRoute{
|
|
RouteID: "route-clamp-hi", HolderReachID: "h",
|
|
SourceAsset: "ubread", DestAsset: "uatom", Amount: 100, Signer: "h",
|
|
})
|
|
srv.ExecuteDEXSwap(ctx, &exittypes.MsgExecuteDEXSwap{
|
|
RouteID: "route-clamp-hi", Venue: "venue:99", Signer: "h", // 99 bps → clamped to 10
|
|
})
|
|
|
|
clamped := eventAttr(ctx, "exit.fee_covenant_clamp", "fee_bps_clamped")
|
|
if clamped != "10" {
|
|
t.Errorf("fee should be clamped to 10 (ceiling); got %q", clamped)
|
|
}
|
|
requested := eventAttr(ctx, "exit.fee_covenant_clamp", "fee_bps_requested")
|
|
if requested != "99" {
|
|
t.Errorf("fee requested = %q, want 99", requested)
|
|
}
|
|
}
|
|
|
|
// TestFeeCovenantClampLowFee asserts a fee below the floor (1 bps) is clamped
|
|
// up to the floor (1 bps) — the Fee Covenant never-below-floor rule.
|
|
func TestFeeCovenantClampLowFee(t *testing.T) {
|
|
ctx, _, k := newSimtestContext(t)
|
|
srv := keeper.NewMsgServerImpl(k)
|
|
|
|
srv.SubmitExitRoute(ctx, &exittypes.MsgSubmitExitRoute{
|
|
RouteID: "route-clamp-lo", HolderReachID: "h",
|
|
SourceAsset: "ubread", DestAsset: "uatom", Amount: 100, Signer: "h",
|
|
})
|
|
srv.ExecuteDEXSwap(ctx, &exittypes.MsgExecuteDEXSwap{
|
|
RouteID: "route-clamp-lo", Venue: "venue:0", Signer: "h", // 0 bps → clamped to 1
|
|
})
|
|
|
|
clamped := eventAttr(ctx, "exit.fee_covenant_clamp", "fee_bps_clamped")
|
|
if clamped != "1" {
|
|
t.Errorf("fee should be clamped to 1 (floor); got %q", clamped)
|
|
}
|
|
}
|
|
|
|
// TestFeeCovenantClampInBand asserts a fee within [1, 10] bps is unchanged.
|
|
func TestFeeCovenantClampInBand(t *testing.T) {
|
|
ctx, _, k := newSimtestContext(t)
|
|
srv := keeper.NewMsgServerImpl(k)
|
|
|
|
srv.SubmitExitRoute(ctx, &exittypes.MsgSubmitExitRoute{
|
|
RouteID: "route-band", HolderReachID: "h",
|
|
SourceAsset: "ubread", DestAsset: "uatom", Amount: 100, Signer: "h",
|
|
})
|
|
srv.ExecuteDEXSwap(ctx, &exittypes.MsgExecuteDEXSwap{
|
|
RouteID: "route-band", Venue: "venue:5", Signer: "h", // 5 bps → in-band, unchanged
|
|
})
|
|
|
|
clamped := eventAttr(ctx, "exit.fee_covenant_clamp", "fee_bps_clamped")
|
|
if clamped != "5" {
|
|
t.Errorf("fee in-band should be unchanged at 5; got %q", clamped)
|
|
}
|
|
}
|
|
|
|
// --- ExitStatus lifecycle: Failed → Refunded ---------------------------------
|
|
|
|
// TestExitStatusLifecycleFailedToRefunded asserts the failure/refund path:
|
|
// SubmitExitRoute (Proposed) → cross-chain ExecuteDEXSwap with a non-Active
|
|
// bridge route → Failed → RefundExit → Refunded.
|
|
func TestExitStatusLifecycleFailedToRefunded(t *testing.T) {
|
|
ctx, bk, k := newSimtestContext(t)
|
|
srv := keeper.NewMsgServerImpl(k)
|
|
|
|
// Submit a cross-chain exit route (with a bridge-route-id).
|
|
srv.SubmitExitRoute(ctx, &exittypes.MsgSubmitExitRoute{
|
|
RouteID: "route-fail", HolderReachID: "h",
|
|
SourceAsset: "ubread", DestAsset: "uatom", Amount: 200, Signer: "h",
|
|
})
|
|
// Set the bridge-route-id on the route (simtest sets it directly; the real
|
|
// handler sets it at submit time from the route params).
|
|
r, _ := k.GetExitRoute(ctx, "route-fail")
|
|
r.BridgeRouteID = "bridge-fail-1"
|
|
k.SetExitRoute(ctx, r)
|
|
|
|
// Stub bridge returns a non-Active status (Closed) → exit fails.
|
|
bk.routes["bridge-fail-1"] = stubBridgeRoute{status: "Closed", bridgeType: "evm-ibc"}
|
|
|
|
srv.ExecuteDEXSwap(ctx, &exittypes.MsgExecuteDEXSwap{
|
|
RouteID: "route-fail", Venue: "venue:3", Signer: "h",
|
|
})
|
|
r, _ = k.GetExitRoute(ctx, "route-fail")
|
|
if r.Status != exittypes.ExitFailed {
|
|
t.Errorf("status = %q, want Failed", r.Status)
|
|
}
|
|
if !hasEvent(ctx, "exit.failed") {
|
|
t.Error("failed event not emitted")
|
|
}
|
|
|
|
// RefundExit → Refunded.
|
|
if _, err := srv.RefundExit(ctx, &exittypes.MsgRefundExit{
|
|
RouteID: "route-fail", Signer: "h",
|
|
}); err != nil {
|
|
t.Fatalf("RefundExit: %v", err)
|
|
}
|
|
r, _ = k.GetExitRoute(ctx, "route-fail")
|
|
if r.Status != exittypes.ExitRefunded {
|
|
t.Errorf("status = %q, want Refunded", r.Status)
|
|
}
|
|
if !hasEvent(ctx, "exit.refunded") {
|
|
t.Error("refunded event not emitted")
|
|
}
|
|
}
|
|
|
|
// TestCrossChainExitActiveBridge asserts a cross-chain exit with an Active
|
|
// bridge route succeeds (Settled), invoking the BridgeKeeper shim.
|
|
func TestCrossChainExitActiveBridge(t *testing.T) {
|
|
ctx, bk, k := newSimtestContext(t)
|
|
srv := keeper.NewMsgServerImpl(k)
|
|
|
|
srv.SubmitExitRoute(ctx, &exittypes.MsgSubmitExitRoute{
|
|
RouteID: "route-xchain", HolderReachID: "h",
|
|
SourceAsset: "ubread", DestAsset: "uatom", Amount: 300, Signer: "h",
|
|
})
|
|
r, _ := k.GetExitRoute(ctx, "route-xchain")
|
|
r.BridgeRouteID = "bridge-active-1"
|
|
k.SetExitRoute(ctx, r)
|
|
bk.routes["bridge-active-1"] = stubBridgeRoute{status: "Active", bridgeType: "evm-ibc"}
|
|
|
|
srv.ExecuteDEXSwap(ctx, &exittypes.MsgExecuteDEXSwap{
|
|
RouteID: "route-xchain", Venue: "venue:5", Signer: "h",
|
|
})
|
|
r, _ = k.GetExitRoute(ctx, "route-xchain")
|
|
if r.Status != exittypes.ExitSettled {
|
|
t.Errorf("cross-chain exit with Active bridge should Settle; got %q", r.Status)
|
|
}
|
|
if bk.calls == 0 {
|
|
t.Error("BridgeKeeper.GetBridgeRoute was not called (G-003 shim not invoked)")
|
|
}
|
|
}
|
|
|
|
// --- Replay rejection --------------------------------------------------------
|
|
|
|
// TestReplayRejectedOnSettledRoute asserts a duplicate ExecuteDEXSwap on a
|
|
// Settled route returns an error (the route is terminal — replay rejection).
|
|
func TestReplayRejectedOnSettledRoute(t *testing.T) {
|
|
ctx, _, k := newSimtestContext(t)
|
|
srv := keeper.NewMsgServerImpl(k)
|
|
|
|
srv.SubmitExitRoute(ctx, &exittypes.MsgSubmitExitRoute{
|
|
RouteID: "route-replay", HolderReachID: "h",
|
|
SourceAsset: "ubread", DestAsset: "uatom", Amount: 100, Signer: "h",
|
|
})
|
|
srv.ExecuteDEXSwap(ctx, &exittypes.MsgExecuteDEXSwap{
|
|
RouteID: "route-replay", Venue: "venue:5", Signer: "h",
|
|
})
|
|
// Second ExecuteDEXSwap on Settled route → error (replay rejection).
|
|
_, err := srv.ExecuteDEXSwap(ctx, &exittypes.MsgExecuteDEXSwap{
|
|
RouteID: "route-replay", Venue: "venue:5", Signer: "h",
|
|
})
|
|
if err == nil {
|
|
t.Error("duplicate ExecuteDEXSwap on Settled route should return error (replay rejection)")
|
|
}
|
|
}
|
|
|
|
// TestRefundExitRejectsNonFailed asserts RefundExit rejects a route that is
|
|
// not Failed.
|
|
func TestRefundExitRejectsNonFailed(t *testing.T) {
|
|
ctx, _, k := newSimtestContext(t)
|
|
srv := keeper.NewMsgServerImpl(k)
|
|
|
|
srv.SubmitExitRoute(ctx, &exittypes.MsgSubmitExitRoute{
|
|
RouteID: "route-refund-bad", HolderReachID: "h",
|
|
SourceAsset: "ubread", DestAsset: "uatom", Amount: 100, Signer: "h",
|
|
})
|
|
_, err := srv.RefundExit(ctx, &exittypes.MsgRefundExit{
|
|
RouteID: "route-refund-bad", Signer: "h",
|
|
})
|
|
if err == nil {
|
|
t.Error("RefundExit should reject a Proposed route (must be Failed)")
|
|
}
|
|
}
|
|
|
|
// --- SubmitExitRoute validation ----------------------------------------------
|
|
|
|
func TestSubmitExitRouteRejectsDuplicate(t *testing.T) {
|
|
ctx, _, k := newSimtestContext(t)
|
|
srv := keeper.NewMsgServerImpl(k)
|
|
srv.SubmitExitRoute(ctx, &exittypes.MsgSubmitExitRoute{
|
|
RouteID: "dup", HolderReachID: "h", SourceAsset: "a", DestAsset: "b", Amount: 1, Signer: "h",
|
|
})
|
|
_, err := srv.SubmitExitRoute(ctx, &exittypes.MsgSubmitExitRoute{
|
|
RouteID: "dup", HolderReachID: "h", SourceAsset: "a", DestAsset: "b", Amount: 1, Signer: "h",
|
|
})
|
|
if err == nil {
|
|
t.Error("SubmitExitRoute should reject a duplicate route-id")
|
|
}
|
|
}
|
|
|
|
// --- ValidateBasic (Msg types) -----------------------------------------------
|
|
|
|
func TestMsgSubmitExitRouteValidateBasic(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
msg exittypes.MsgSubmitExitRoute
|
|
ok bool
|
|
}{
|
|
{"valid", exittypes.MsgSubmitExitRoute{"r1", "h", "a", "b", 100, "s"}, true},
|
|
{"empty holder", exittypes.MsgSubmitExitRoute{"r1", "", "a", "b", 100, "s"}, false},
|
|
{"empty source", exittypes.MsgSubmitExitRoute{"r1", "h", "", "b", 100, "s"}, false},
|
|
{"empty dest", exittypes.MsgSubmitExitRoute{"r1", "h", "a", "", 100, "s"}, false},
|
|
{"zero amount", exittypes.MsgSubmitExitRoute{"r1", "h", "a", "b", 0, "s"}, false},
|
|
{"neg amount", exittypes.MsgSubmitExitRoute{"r1", "h", "a", "b", -1, "s"}, false},
|
|
{"empty signer", exittypes.MsgSubmitExitRoute{"r1", "h", "a", "b", 100, ""}, 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 TestMsgExecuteDEXSwapValidateBasic(t *testing.T) {
|
|
if err := (&exittypes.MsgExecuteDEXSwap{RouteID: "r1", Signer: "s"}).ValidateBasic(); err != nil {
|
|
t.Errorf("valid: %v", err)
|
|
}
|
|
if err := (&exittypes.MsgExecuteDEXSwap{RouteID: "", Signer: "s"}).ValidateBasic(); err == nil {
|
|
t.Error("empty route-id should fail")
|
|
}
|
|
if err := (&exittypes.MsgExecuteDEXSwap{RouteID: "r1", Signer: ""}).ValidateBasic(); err == nil {
|
|
t.Error("empty signer should fail")
|
|
}
|
|
}
|
|
|
|
func TestMsgRefundExitValidateBasic(t *testing.T) {
|
|
if err := (&exittypes.MsgRefundExit{RouteID: "r1", Signer: "s"}).ValidateBasic(); err != nil {
|
|
t.Errorf("valid: %v", err)
|
|
}
|
|
if err := (&exittypes.MsgRefundExit{RouteID: "", Signer: "s"}).ValidateBasic(); err == nil {
|
|
t.Error("empty route-id should fail")
|
|
}
|
|
}
|
|
|
|
func TestExitMsgGetSigners(t *testing.T) {
|
|
m := &exittypes.MsgSubmitExitRoute{Signer: "holder-reach"}
|
|
addrs := m.GetSigners()
|
|
if len(addrs) != 1 || string(addrs[0]) != "holder-reach" {
|
|
t.Errorf("GetSigners = %v, want [holder-reach]", addrs)
|
|
}
|
|
}
|
|
|
|
// --- Keeper store helpers ----------------------------------------------------
|
|
|
|
func TestSetGetExitRoute(t *testing.T) {
|
|
ctx, _, k := newSimtestContext(t)
|
|
r := exittypes.ExitRoute{RouteID: "r9", Status: exittypes.ExitProposed}
|
|
k.SetExitRoute(ctx, r)
|
|
got, ok := k.GetExitRoute(ctx, "r9")
|
|
if !ok {
|
|
t.Fatal("GetExitRoute: not found")
|
|
}
|
|
if got.Status != exittypes.ExitProposed {
|
|
t.Errorf("status = %q", got.Status)
|
|
}
|
|
if _, ok := k.GetExitRoute(ctx, "missing"); ok {
|
|
t.Error("GetExitRoute should return false for missing route")
|
|
}
|
|
}
|
|
|
|
func TestSetGetDEXSwap(t *testing.T) {
|
|
ctx, _, k := newSimtestContext(t)
|
|
s := exittypes.DEXSwap{SwapID: "s9", Venue: "oy-dex", Status: exittypes.ExitSettled}
|
|
k.SetDEXSwap(ctx, s)
|
|
got, ok := k.GetDEXSwap(ctx, "s9")
|
|
if !ok {
|
|
t.Fatal("GetDEXSwap: not found")
|
|
}
|
|
if got.Venue != "oy-dex" {
|
|
t.Errorf("venue = %q", got.Venue)
|
|
}
|
|
}
|
|
|
|
func TestAllExitRoutesAndSwaps(t *testing.T) {
|
|
ctx, _, k := newSimtestContext(t)
|
|
k.SetExitRoute(ctx, exittypes.ExitRoute{RouteID: "r1", Status: exittypes.ExitProposed})
|
|
k.SetExitRoute(ctx, exittypes.ExitRoute{RouteID: "r2", Status: exittypes.ExitSettled})
|
|
k.SetDEXSwap(ctx, exittypes.DEXSwap{SwapID: "s1", Venue: "v"})
|
|
if len(k.AllExitRoutes(ctx)) != 2 {
|
|
t.Errorf("expected 2 routes")
|
|
}
|
|
if len(k.AllDEXSwaps(ctx)) != 1 {
|
|
t.Errorf("expected 1 swap")
|
|
}
|
|
}
|
|
|
|
// --- Cross-chain exit: nil shim handling -------------------------------------
|
|
|
|
// TestCrossChainExitNilBridgeShimFails asserts a cross-chain exit with a nil
|
|
// BridgeKeeper shim fails the route (not a panic).
|
|
func TestCrossChainExitNilBridgeShimFails(t *testing.T) {
|
|
ctx, _, k := newSimtestContext(t)
|
|
srv := keeper.NewMsgServerImpl(k)
|
|
// Clear the bridge shim to simulate unwired.
|
|
k.SetBridgeKeeper(nil)
|
|
|
|
srv.SubmitExitRoute(ctx, &exittypes.MsgSubmitExitRoute{
|
|
RouteID: "route-noshim", HolderReachID: "h",
|
|
SourceAsset: "ubread", DestAsset: "uatom", Amount: 100, Signer: "h",
|
|
})
|
|
r, _ := k.GetExitRoute(ctx, "route-noshim")
|
|
r.BridgeRouteID = "bridge-x"
|
|
k.SetExitRoute(ctx, r)
|
|
|
|
_, err := srv.ExecuteDEXSwap(ctx, &exittypes.MsgExecuteDEXSwap{
|
|
RouteID: "route-noshim", Venue: "venue:5", Signer: "h",
|
|
})
|
|
if err != nil {
|
|
t.Errorf("ExecuteDEXSwap with nil shim should not return error (route fails to Failed); got %v", err)
|
|
}
|
|
r, _ = k.GetExitRoute(ctx, "route-noshim")
|
|
if r.Status != exittypes.ExitFailed {
|
|
t.Errorf("cross-chain exit with nil shim should fail; got %q", r.Status)
|
|
}
|
|
}
|
|
|
|
// --- JSON marshal/unmarshal for the InflightPacket (bridge) sanity -----------
|
|
|
|
// TestInflightPacketJSON asserts the InflightPacket JSON round-trips (the
|
|
// keeper uses json.Marshal/Unmarshal).
|
|
func TestInflightPacketJSON(t *testing.T) {
|
|
p := struct {
|
|
SourcePort string
|
|
Amount int64
|
|
}{"transfer", 100}
|
|
bz, _ := json.Marshal(p)
|
|
var got struct {
|
|
SourcePort string
|
|
Amount int64
|
|
}
|
|
if err := json.Unmarshal(bz, &got); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
if got.SourcePort != "transfer" || got.Amount != 100 {
|
|
t.Errorf("round-trip mismatch: %+v", got)
|
|
}
|
|
}
|