package keeper // keeper.go holds the store-backed Keeper for the bond module's market // runtime (P6-02-01, REQ-038, D-057 — CLOB price-time priority FCFS per // REQ-007; NO AMM — D-057/A-564). // // The Keeper wraps an sdk.KVStore via a storeKey. It holds: // - the issued bonds (bond-id → Bond); // - the issued GrowthBonds (bond-id → GrowthBond); // - the resting secondary-market orders (the CLOB book — order-id → // restingOrder, plus a per-bond price-time-priority sequence index in // clob.go). // // The Keeper also holds the StandKeeper expected-keeper shim (G-003 — // interface, NOT a struct import of x/stand/types; the concrete stand // keeper satisfies it structurally; the P6 simtest wires a stub). // // The 8%/0% consts (CouponCapBps=800 / CouponFloorBps=0, D-028) are // referenced DIRECTLY from x/bond/types (same package — NOT a local copy; // A-563). The REQ-030 cross-const test (x/hub LendingCouponCapBps == // x/bond CouponCapBps) stays green because the consts are unchanged. // // State-machine ordering (vision §7, enforced in every handler): // ValidateBasic → keeper authz → state mutation → ctx.EventManager().EmitEvent // // D-054: simtest-grade — in-memory sdk.Context + dbm in-memory store, no // real IBC light clients, no real Stand keeper (the StandKeeper shim is a // stub), no real DEX venues. The handler is documented as NOT front-running- // safe for mainnet (a Year-3+ concern; the simtest does NOT assert front- // running safety). import ( "encoding/json" "fmt" storetypes "cosmossdk.io/store/types" "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/oy/openyield/x/bond/types" ) // Keeper is the store-backed bond market keeper. type Keeper struct { cdc codec.Codec storeKey storetypes.StoreKey standKeeper types.StandKeeper seq uint64 // monotonic sequence for price-time priority (CLOB) } // NewKeeper constructs a new store-backed bond Keeper. The StandKeeper // expected-keeper shim is injected (nil-able for partial tests; the // IssueBond / IssueGrowthBond handlers guard a nil shim and skip the // StandExists check, still mutating state — the simtest wiring documents // this). func NewKeeper(cdc codec.Codec, storeKey storetypes.StoreKey, sk types.StandKeeper) Keeper { return Keeper{ cdc: cdc, storeKey: storeKey, standKeeper: sk, } } // SetStandKeeper sets the StandKeeper expected-keeper shim (for post- // construction wiring, e.g., app wiring or test setup). func (k *Keeper) SetStandKeeper(sk types.StandKeeper) { k.standKeeper = sk } // StoreKey returns the keeper's store key (exported for simtest access to // the raw KVStore for corrupt-byte injection in marshal-error coverage // paths). func (k Keeper) StoreKey() storetypes.StoreKey { return k.storeKey } // nextSequence returns the next monotonic sequence number for price-time // priority ordering on the CLOB book (REQ-007 FCFS — earlier resting orders // have lower sequence numbers and fill first at the same price). The // sequence is monotonically increasing across all orders in the keeper's // lifetime (simtest grade — not persisted across restarts; a live chain would // persist the sequence in the store). func (k *Keeper) nextSequence() uint64 { k.seq++ return k.seq } // --- Bond store -------------------------------------------------------------- var bondKeyPrefix = []byte("bond/") func bondKey(bondID string) []byte { return append(bondKeyPrefix, []byte(bondID)...) } // GetBond loads an issued Bond by bond-id. Returns the Bond and true if // found, or zero value + false if not. func (k Keeper) GetBond(ctx sdk.Context, bondID string) (types.Bond, bool) { store := ctx.KVStore(k.storeKey) bz := store.Get(bondKey(bondID)) if bz == nil { return types.Bond{}, false } var b types.Bond if err := json.Unmarshal(bz, &b); err != nil { return types.Bond{}, false } return b, true } // SetBond persists an issued Bond by bond-id. func (k Keeper) SetBond(ctx sdk.Context, b types.Bond) { store := ctx.KVStore(k.storeKey) bz, err := json.Marshal(b) if err != nil { panic(fmt.Sprintf("bond: marshal bond %q: %v", b.BondID, err)) } store.Set(bondKey(b.BondID), bz) } // AllBonds returns all issued Bonds (iteration helper, unordered). func (k Keeper) AllBonds(ctx sdk.Context) []types.Bond { store := ctx.KVStore(k.storeKey) iterator := store.Iterator(bondKeyPrefix, prefixEnd(bondKeyPrefix)) defer iterator.Close() out := []types.Bond{} for ; iterator.Valid(); iterator.Next() { var b types.Bond if err := json.Unmarshal(iterator.Value(), &b); err == nil { out = append(out, b) } } return out } // --- GrowthBond store -------------------------------------------------------- var growthBondKeyPrefix = []byte("growth/") func growthBondKey(bondID string) []byte { return append(growthBondKeyPrefix, []byte(bondID)...) } // GetGrowthBond loads an issued GrowthBond by bond-id. Returns the GrowthBond // and true if found, or zero value + false if not. func (k Keeper) GetGrowthBond(ctx sdk.Context, bondID string) (types.GrowthBond, bool) { store := ctx.KVStore(k.storeKey) bz := store.Get(growthBondKey(bondID)) if bz == nil { return types.GrowthBond{}, false } var gb types.GrowthBond if err := json.Unmarshal(bz, &gb); err != nil { return types.GrowthBond{}, false } return gb, true } // SetGrowthBond persists an issued GrowthBond by bond-id. func (k Keeper) SetGrowthBond(ctx sdk.Context, gb types.GrowthBond) { store := ctx.KVStore(k.storeKey) bz, err := json.Marshal(gb) if err != nil { panic(fmt.Sprintf("bond: marshal growth bond %q: %v", gb.BondID, err)) } store.Set(growthBondKey(gb.BondID), bz) } // AllGrowthBonds returns all issued GrowthBonds (iteration helper, unordered). func (k Keeper) AllGrowthBonds(ctx sdk.Context) []types.GrowthBond { store := ctx.KVStore(k.storeKey) iterator := store.Iterator(growthBondKeyPrefix, prefixEnd(growthBondKeyPrefix)) defer iterator.Close() out := []types.GrowthBond{} for ; iterator.Valid(); iterator.Next() { var gb types.GrowthBond if err := json.Unmarshal(iterator.Value(), &gb); err == nil { out = append(out, gb) } } return out } // --- Order store (CLOB resting book) ----------------------------------------- // // The resting book is keyed by order-id → restingOrder (the in-keeper book // entry carrying the order + its price-time-priority sequence). The CLOB // matching engine (clob.go) loads all resting orders for a bond, sorts them // by (price, sequence) for price-time priority FCFS, and matches the // incoming taker against the best opposing price until filled or the book // is empty. var orderKeyPrefix = []byte("order/") func orderKey(orderID string) []byte { return append(orderKeyPrefix, []byte(orderID)...) } // GetRestingOrder loads a resting order by order-id. Returns the order and // true if found, or zero value + false if not. func (k Keeper) GetRestingOrder(ctx sdk.Context, orderID string) (restingOrder, bool) { store := ctx.KVStore(k.storeKey) bz := store.Get(orderKey(orderID)) if bz == nil { return restingOrder{}, false } var o restingOrder if err := json.Unmarshal(bz, &o); err != nil { return restingOrder{}, false } return o, true } // setRestingOrder persists a resting order by order-id. func (k Keeper) setRestingOrder(ctx sdk.Context, o restingOrder) { store := ctx.KVStore(k.storeKey) bz, err := json.Marshal(o) if err != nil { panic(fmt.Sprintf("bond: marshal order %q: %v", o.Order.OrderID, err)) } store.Set(orderKey(o.Order.OrderID), bz) } // deleteRestingOrder removes a resting order by order-id. func (k Keeper) deleteRestingOrder(ctx sdk.Context, orderID string) { store := ctx.KVStore(k.storeKey) store.Delete(orderKey(orderID)) } // AllRestingOrders returns all resting orders (iteration helper, unordered). // Exported for simtest assertion. func (k Keeper) AllRestingOrders(ctx sdk.Context) []restingOrder { store := ctx.KVStore(k.storeKey) iterator := store.Iterator(orderKeyPrefix, prefixEnd(orderKeyPrefix)) defer iterator.Close() out := []restingOrder{} for ; iterator.Valid(); iterator.Next() { var o restingOrder if err := json.Unmarshal(iterator.Value(), &o); err == nil { out = append(out, o) } } return out } // --- prefixEnd helper -------------------------------------------------------- // prefixEnd returns the key that sorts immediately after all keys sharing // the given prefix (the standard prefix-iteration end key: increment the // last byte, drop overflow). Mirrors x/hub/keeper/keeper.go. func prefixEnd(prefix []byte) []byte { if len(prefix) == 0 { return nil } end := make([]byte, len(prefix)) copy(end, prefix) for i := len(end) - 1; i >= 0; i-- { end[i]++ if end[i] != 0 { return end } } // All bytes were 0xFF; return nil (iterate to end of store). return nil }