Merge phase/06 into milestone/v0.5-bearers-runtime (P6 complete → v0.4.6)

---ci---
project: oy
phase: 6
milestone: v0.5
status: complete
requirements:
  covered: [REQ-038]
  partial: []
---/ci---
This commit is contained in:
2026-08-18 01:07:12 +00:00
parent a70d6faa59
commit a8c9489a6a
7 changed files with 2911 additions and 0 deletions
+286
View File
@@ -0,0 +1,286 @@
package keeper
// clob.go holds the CLOB (central-limit order book) matching engine for the
// bond secondary market (P6-02-01, REQ-038, D-057 — price-time priority FCFS
// per REQ-007; NO AMM — D-057/A-564).
//
// The CLOB engine is PER-TX matching (dYdX-v4-shaped, no batch end-of-block
// matching in v0.5 simtest — D-054). The handler loads the resting book for
// the bond, sorts by (price, sequence) for price-time priority, and matches
// the incoming taker against the best opposing price until filled or the
// book is empty.
//
// G-019 BINDING: this file defines the SINGLE ImpliedCoupon(priceBps,
// principal) helper used by BOTH the CLOB match and the per-match clamp
// check (D-063). The "implied coupon" derivation from trade price (fraction
// of principal in bps) is the unstated precondition of the D-063 REJECT
// threshold; a single helper + boundary unit test (800/801/799 bps) closes
// the formula ambiguity.
//
// D-063/A-562: a match whose ImpliedCoupon EXCEEDS 800 bps is REJECTED
// (fails closed — the resting order stays, the incoming order rests or is
// cancelled; no refund path). The 8% cap is a Mission-Lock invariant (D-028),
// not a soft cap. Matches within [0, 800] use Clamp (in-band, no refund
// needed).
//
// 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 stays green.
//
// Lexicon (REQ-012, A-210): the coupon vocabulary is used EXCLUSIVELY. The
// banned coupon-synonyms are NEVER used.
//
// FEATURE PURITY GATE: the v0.3 types.SecondaryOrder struct is FROZEN (it
// has PriceGrain int64, no PriceBps or QuantityGrain). To avoid amending the
// v0.3 types/ contract, the CLOB book uses a keeper-internal restingOrder
// struct carrying the price-bps + remaining quantity (the runtime book
// state). The restingOrder embeds the public SecondaryOrder (the v0.3
// contract is preserved) PLUS the keeper-internal book fields. This is the
// "runtime adds behavior on top, not changes to the contract" pattern.
import (
"sort"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/oy/openyield/x/bond/types"
)
// restingOrder is the in-keeper book entry for a resting secondary-market
// order. It carries the public SecondaryOrder (the v0.3 type — frozen, not
// amended, per the feature purity gate) PLUS the keeper-internal price-bps
// and remaining-quantity and sequence for price-time priority FCFS
// (REQ-007). The price-bps, remaining-quantity, and sequence are keeper-
// internal concerns (NOT types/ contract fields); adding them here keeps
// the v0.3 types/ contract unchanged (feature purity gate — no breaking
// schema changes).
type restingOrder struct {
// Order is the public v0.3 SecondaryOrder (frozen contract). Carries
// OrderID, BondID, Side, PriceGrain, HolderReachID, Status, CreatedAt.
Order types.SecondaryOrder `json:"order" yaml:"order"`
// PriceBps is the order price in basis points (the price as a fraction
// of principal in bps — this is the implied coupon of a match at this
// price; the CLOB matching engine's ImpliedCoupon helper derives the
// per-match implied coupon from the resting order's price-bps, G-019).
// Keeper-internal (the v0.3 SecondaryOrder has PriceGrain int64, not
// PriceBps; the runtime uses PriceBps for the CLOB match).
PriceBps uint32 `json:"price_bps" yaml:"price_bps"`
// Sequence is the price-time-priority ordering key (monotonic; lower
// sequence = earlier resting order = fills first at the same price —
// REQ-007 FCFS).
Sequence uint64 `json:"sequence" yaml:"sequence"`
// RemainingQuantityGrain is the unfilled quantity of the order (a
// resting order may be partially filled by an earlier match; the
// remaining quantity is what later takers can match against).
RemainingQuantityGrain int64 `json:"remaining_quantity_grain" yaml:"remaining_quantity_grain"`
}
// ImpliedCoupon is the G-019 BINDING helper: it derives the implied coupon
// (in basis points) of a trade at the given price-bps against the principal.
// The implied coupon is the fraction of principal the trade price represents,
// expressed in bps: a price of 10000 bps (100% of principal) implies a 0-bps
// coupon (par); a price of 9500 bps (95% of principal, a discount) implies a
// 500-bps coupon (the buyer pays 95% of principal and receives the full
// principal at maturity, earning a 500-bps coupon).
//
// The formula: impliedCouponBps = max(0, 10000 - priceBps).
// - priceBps == 10000 (par) -> impliedCoupon 0 (no discount, no coupon).
// - priceBps < 10000 (discount) -> impliedCoupon = 10000 - priceBps (the
// discount is the implied coupon).
// - priceBps > 10000 (premium) -> the discount is negative; the implied
// coupon is floored at 0 (a premium bond has a 0 implied coupon — the
// buyer pays MORE than principal, so the implied coupon is 0, not
// negative).
//
// The principal argument is accepted for signature compatibility with the
// plan text (G-019: "ImpliedCoupon(priceBps, principal)") but does not
// affect the implied-coupon derivation for a fixed-coupon bond (the coupon
// is the discount-from-par in bps, independent of the principal amount).
// It is retained so a future v0.6+ amortization model can use it.
//
// G-019 boundary: the D-063 REJECT threshold is 800 bps. A match whose
// ImpliedCoupon exceeds 800 (price-bps < 9200 — a discount greater than
// 800 bps) is REJECTED (fails closed). The boundary unit test in
// msg_server_simtest_test.go covers:
// - price-bps 9200 -> ImpliedCoupon 800 (== cap, in-band, clears via Clamp).
// - price-bps 9199 -> ImpliedCoupon 801 (> cap, REJECTED — D-063).
// - price-bps 9201 -> ImpliedCoupon 799 (< cap, in-band, clears).
func ImpliedCoupon(priceBps uint32, principalGrain int64) uint32 {
_ = principalGrain // retained for G-019 signature compatibility; unused
// at v0.5 (fixed-coupon bond — coupon is discount-from-par in bps).
if priceBps >= 10000 {
return 0 // par or premium -> 0 implied coupon (floored at 0)
}
return 10000 - priceBps // discount -> the discount is the implied coupon
}
// --- CLOB matching engine ----------------------------------------------------
//
// matchTaker attempts to match an incoming taker order against the resting
// book for the given bond. Price-time priority FCFS per REQ-007:
// - Buy taker matches against Sell resting orders with price-bps <= the
// taker's price-bps, best (lowest) price first, then earliest sequence.
// - Sell taker matches against Buy resting orders with price-bps >= the
// taker's price-bps, best (highest) price first, then earliest sequence.
//
// Per D-063/A-562: every match's ImpliedCoupon is computed from the resting
// order's price-bps; a match whose ImpliedCoupon EXCEEDS 800 bps is REJECTED
// (fails closed). The rejection is PER-MATCH (not per-taker): if the best
// resting order is above cap, that match is rejected, the resting order
// stays on the book, and the taker does NOT advance to the next resting order
// (fails closed — the taker is rejected; the resting book above cap is
// unreachable). This is the mission-lock-true choice: the 8% cap is a hard
// invariant, not a soft cap.
//
// Returns the total filled quantity, the list of filled order-ids (for
// event emission), and a boolean indicating whether a per-match REJECT
// occurred (D-063 — when true, no match occurred for the offending resting
// order; the resting book is unchanged; the caller reports the reject).
func (k Keeper) matchTaker(
ctx sdk.Context,
bondID string,
takerSide types.OrderSide,
takerPriceBps uint32,
takerQuantityGrain int64,
) (filledQuantityGrain int64, filledOrderIDs []string, rejected bool) {
// Load the resting book for the bond.
resting := k.restingBookForBond(ctx, bondID)
// Sort for price-time priority.
sortRestingBook(resting, takerSide)
remaining := takerQuantityGrain
filledOrderIDs = []string{}
for i := range resting {
if remaining <= 0 {
break
}
ro := &resting[i]
if ro.Order.Status != types.OrderOpen {
continue // skip non-resting (defensive — the book holds Open only)
}
// Price check: does this resting order's price satisfy the taker?
if !priceCrosses(takerSide, takerPriceBps, ro.PriceBps) {
// The book is sorted best-price-first; once the price does not
// cross, no later (worse-price) resting order will cross. Stop.
break
}
// D-063 per-match coupon clamp (G-019 ImpliedCoupon helper). The
// implied coupon is derived from the RESTING order's price-bps
// (the price at which the match executes). A match above 800 bps
// is REJECTED (fails closed — the resting order stays, the taker
// does not advance).
implied := ImpliedCoupon(ro.PriceBps, 0)
if implied > types.CouponCapBps {
// D-063 REJECT: the resting order stays on the book; the taker
// is rejected (fails closed — no refund path, no advance to
// the next resting order).
return filledQuantityGrain, filledOrderIDs, true
}
// In-band match (implied coupon within [0, 800]). Clamp it (the
// 8% cap is the firewall; Clamp is the helper — defense in depth,
// though ImpliedCoupon <= 800 here so Clamp is a no-op).
clampedCoupon := types.Clamp(implied)
// Determine the fill quantity (the smaller of the taker's
// remaining quantity and the resting order's remaining quantity).
fill := remaining
if ro.RemainingQuantityGrain < fill {
fill = ro.RemainingQuantityGrain
}
// Update the resting order's remaining quantity.
ro.RemainingQuantityGrain -= fill
remaining -= fill
filledQuantityGrain += fill
filledOrderIDs = append(filledOrderIDs, ro.Order.OrderID)
// If the resting order is fully filled, mark it Filled and delete
// it from the book; otherwise persist the updated remaining.
if ro.RemainingQuantityGrain <= 0 {
ro.Order.Status = types.OrderFilled
k.deleteRestingOrder(ctx, ro.Order.OrderID)
} else {
k.setRestingOrder(ctx, *ro)
}
// Emit a match event with the clamped coupon for simtest assertion.
emitMatchEvent(ctx, ro.Order.OrderID, bondID, clampedCoupon, fill)
}
return filledQuantityGrain, filledOrderIDs, false
}
// restingBookForBond loads all resting orders for a given bond-id (the CLOB
// book for that bond). The book is unordered here; matchTaker sorts it for
// price-time priority.
func (k Keeper) restingBookForBond(ctx sdk.Context, bondID string) []restingOrder {
all := k.AllRestingOrders(ctx)
out := make([]restingOrder, 0, len(all))
for _, ro := range all {
if ro.Order.BondID == bondID && ro.Order.Status == types.OrderOpen {
out = append(out, ro)
}
}
return out
}
// sortRestingBook sorts the resting book for price-time priority FCFS
// (REQ-007). For a Buy taker (matching against Sell resting orders), the
// best price is the LOWEST Sell price (cheapest to buy); for a Sell taker
// (matching against Buy resting orders), the best price is the HIGHEST Buy
// price (most expensive to sell to). Ties at the same price are broken by
// sequence (earlier sequence fills first — FCFS).
func sortRestingBook(book []restingOrder, takerSide types.OrderSide) {
if takerSide == types.OrderBuy {
// Buy taker: sort Sell resting orders by ascending price, then
// ascending sequence (best price = lowest; FCFS at same price).
sort.SliceStable(book, func(i, j int) bool {
if book[i].PriceBps != book[j].PriceBps {
return book[i].PriceBps < book[j].PriceBps
}
return book[i].Sequence < book[j].Sequence
})
} else {
// Sell taker: sort Buy resting orders by descending price, then
// ascending sequence (best price = highest; FCFS at same price).
sort.SliceStable(book, func(i, j int) bool {
if book[i].PriceBps != book[j].PriceBps {
return book[i].PriceBps > book[j].PriceBps
}
return book[i].Sequence < book[j].Sequence
})
}
}
// priceCrosses reports whether the taker's price satisfies the resting
// order's price (a match can execute). For a Buy taker, the taker's price-
// bps must be >= the resting Sell's price-bps (the buyer will pay up to
// takerPriceBps; the seller asked for restingPriceBps; if taker >= resting,
// the price crosses). For a Sell taker, the taker's price-bps must be <=
// the resting Buy's price-bps (the seller will accept as low as
// takerPriceBps; the buyer bid restingPriceBps; if taker <= resting, the
// price crosses).
func priceCrosses(takerSide types.OrderSide, takerPriceBps, restingPriceBps uint32) bool {
if takerSide == types.OrderBuy {
return takerPriceBps >= restingPriceBps
}
return takerPriceBps <= restingPriceBps
}
// emitMatchEvent emits a per-match event for simtest assertion. The event
// carries the resting order-id, the bond-id, the clamped matched coupon
// (within [0, 800] bps — D-063 in-band), and the fill quantity.
//
// NOTE: emitMatchEvent is called from matchTaker, which is a Keeper method
// (not on msgServer). The ctx is the sdk.Context passed to matchTaker. This
// helper is defined here (not in msg_server.go) so the CLOB engine is
// self-contained.
func emitMatchEvent(ctx sdk.Context, restingOrderID, bondID string, matchedCouponBps uint32, fillQuantityGrain int64) {
// Avoid importing sdk event helpers in clob.go to keep the import list
// lean; delegate to the msg_server.go helper via a function variable.
// (The simtest asserts events via ctx.EventManager().Events().)
if emitMatchEventHook != nil {
emitMatchEventHook(ctx, restingOrderID, bondID, matchedCouponBps, fillQuantityGrain)
}
}
// emitMatchEventHook is set by msg_server.go (which imports sdk event
// helpers). This indirection keeps clob.go's import list minimal (sort +
// types only) and avoids a circular dependency on the sdk event package.
var emitMatchEventHook func(ctx sdk.Context, restingOrderID, bondID string, matchedCouponBps uint32, fillQuantityGrain int64)
+261
View File
@@ -0,0 +1,261 @@
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
}
+428
View File
@@ -0,0 +1,428 @@
package keeper
// msg_server.go implements the bond module's MsgServer (P6-02-01, REQ-038;
// G-023 ownership split: cosmos-engineer scaffolds the file structure +
// method signatures; backend-engineer implements the handler logic bodies;
// security-engineer reviews the CLOB per-match clamp D-063 + the 8%/0%
// const firewall A-563). The MsgServer wraps the Keeper + the StandKeeper
// expected-keeper shim (already on the Keeper).
//
// Each method returns a (*Response, error). Handler state-machine ordering
// is enforced: ValidateBasic → keeper authz → state mutation →
// ctx.EventManager().EmitEvent.
//
// Handler set (REQ-038):
// - IssueBond: invokes v0.3 Clamp on the coupon at issuance (the clamped
// value is recorded, NOT the original). StandKeeper shim validates the
// issuer-stand-id exists (P1-02-01 stand-id-ref edge).
// - IssueGrowthBond: invokes Clamp on the coupon + ClampGrowth on the
// growth-rate (post-growth coupon <= cap, G-012).
// - TickGrowthBond: applies one growth tick (coupon += growth-rate, then
// clamped so post-growth <= cap via ClampGrowth with currentBps = the
// current coupon).
// - PlaceSecondaryOrder: rests a secondary-market order on the CLOB book
// (price-time priority FCFS per REQ-007; NO AMM — D-057).
// - CancelSecondaryOrder: removes a resting order (status -> Cancelled).
// - MatchSecondaryOrder: CLOB match against the resting book (per-tx
// matching, dYdX-v4-shaped); per-match coupon clamp via the G-019
// ImpliedCoupon helper; D-063 REJECT above 800 (fails closed).
//
// Nil-shim behavior (simtest wiring): a nil StandKeeper shim skips the
// StandExists check (the handler still mutates state — the simtest documents
// the wiring contract). The 8%/0% consts are referenced directly from
// x/bond/types (same package — NOT a local copy; A-563); the REQ-030
// cross-const test stays green.
//
// The handler is documented as NOT front-running-safe for mainnet (a
// Year-3+ concern; the simtest does NOT assert front-running safety — D-054).
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/oy/openyield/x/bond/types"
)
// init wires the emitMatchEventHook so the CLOB engine (clob.go) emits
// sdk events via the keeper's ctx without importing the sdk event helpers
// in clob.go (keeps clob.go's import list minimal).
func init() {
emitMatchEventHook = func(ctx sdk.Context, restingOrderID, bondID string, matchedCouponBps uint32, fillQuantityGrain int64) {
ctx.EventManager().EmitEvent(sdk.NewEvent(
"bond.match",
sdk.NewAttribute("resting_order_id", restingOrderID),
sdk.NewAttribute("bond_id", bondID),
sdk.NewAttribute("matched_coupon_bps", fmt.Sprintf("%d", matchedCouponBps)),
sdk.NewAttribute("fill_quantity_grain", fmt.Sprintf("%d", fillQuantityGrain)),
))
}
}
// msgServer is the concrete MsgServer implementation wrapping the Keeper.
type msgServer struct {
Keeper
}
// NewMsgServerImpl returns the bond MsgServer for the provided Keeper.
func NewMsgServerImpl(k Keeper) types.MsgServer {
return &msgServer{Keeper: k}
}
var _ types.MsgServer = msgServer{}
// unwrapCtx extracts the sdk.Context from the interface-typed ctx.
func unwrapCtx(ctx interface{}) sdk.Context {
if c, ok := ctx.(sdk.Context); ok {
return c
}
panic(fmt.Sprintf("bond: expected sdk.Context, got %T", ctx))
}
// --- IssueBond ---------------------------------------------------------------
// IssueBond issues a fixed-coupon Bond (REQ-038). The handler enforces:
// 1. ValidateBasic (stateless).
// 2. Idempotency: bond-id must not already exist.
// 3. StandKeeper shim: the issuer-stand-id must reference an existing
// Stand (P1-02-01 stand-id-ref edge). A nil shim skips this check
// (simtest wiring); a non-nil shim that returns false REJECTS the
// issuance (the bond is not created).
// 4. Coupon clamp: the coupon-bps is CLAMPED to [CouponFloorBps=0,
// CouponCapBps=800] at runtime via the v0.3 Clamp helper (A-563 —
// defense in depth; ValidateBasic already rejected out-of-band, but the
// handler re-clamps to defend against any future cap change).
//
// On success the Bond is persisted with the clamped coupon and an event is
// emitted.
func (s msgServer) IssueBond(ctx interface{}, msg *types.MsgIssueBond) (*types.MsgIssueBondResponse, error) {
if err := msg.ValidateBasic(); err != nil {
return nil, err
}
sdkCtx := unwrapCtx(ctx)
// Idempotency: bond-id must not already exist.
if _, ok := s.Keeper.GetBond(sdkCtx, msg.BondID); ok {
return nil, fmt.Errorf("bond: bond-id %q already exists", msg.BondID)
}
// StandKeeper: issuer-stand-id must reference an existing Stand (P1-02-01
// edge). A nil shim skips the check (simtest wiring); a non-nil shim that
// returns false REJECTS the issuance.
if s.Keeper.standKeeper != nil {
if !s.Keeper.standKeeper.StandExists(msg.IssuerStandID) {
return nil, fmt.Errorf("bond: issuer-stand-id %q does not exist (IssueBond rejected)", msg.IssuerStandID)
}
}
// A-563: coupon clamp at runtime. The clamped value (NOT the original)
// is recorded. ValidateBasic already rejected out-of-band, so Clamp is
// a no-op here; the re-clamp is defense in depth against any future cap
// change.
clamped := types.Clamp(msg.CouponBps)
b := types.Issue(msg.BondID, msg.IssuerStandID, msg.PrincipalGrain, clamped, msg.TermDays, msg.IssuedAt, msg.Maturity)
s.Keeper.SetBond(sdkCtx, b)
if clamped != msg.CouponBps {
sdkCtx.EventManager().EmitEvent(sdk.NewEvent(
"bond.coupon_clamped",
sdk.NewAttribute("bond_id", msg.BondID),
sdk.NewAttribute("original_coupon_bps", fmt.Sprintf("%d", msg.CouponBps)),
sdk.NewAttribute("clamped_coupon_bps", fmt.Sprintf("%d", clamped)),
))
}
sdkCtx.EventManager().EmitEvent(sdk.NewEvent(
"bond.issued",
sdk.NewAttribute("bond_id", msg.BondID),
sdk.NewAttribute("issuer_stand_id", msg.IssuerStandID),
sdk.NewAttribute("coupon_bps", fmt.Sprintf("%d", clamped)),
))
return &types.MsgIssueBondResponse{ClampedCouponBps: clamped}, nil
}
// --- IssueGrowthBond ---------------------------------------------------------
// IssueGrowthBond issues a GrowthBond (REQ-038). The handler enforces:
// 1. ValidateBasic (stateless).
// 2. Idempotency: bond-id must not already exist (as a Bond or GrowthBond).
// 3. StandKeeper shim: the issuer-stand-id must reference an existing
// Stand (P1-02-01 edge). A nil shim skips (simtest wiring).
// 4. Coupon clamp + growth clamp: the coupon is CLAMPED to [0, 800] via
// Clamp, and the growth-rate is CLAMPED via ClampGrowth so post-growth
// coupon <= cap (G-012).
//
// On success the GrowthBond is persisted with the clamped coupon + clamped
// growth-rate and an event is emitted.
func (s msgServer) IssueGrowthBond(ctx interface{}, msg *types.MsgIssueGrowthBond) (*types.MsgIssueGrowthBondResponse, error) {
if err := msg.ValidateBasic(); err != nil {
return nil, err
}
sdkCtx := unwrapCtx(ctx)
// Idempotency: bond-id must not already exist (as Bond or GrowthBond).
if _, ok := s.Keeper.GetBond(sdkCtx, msg.BondID); ok {
return nil, fmt.Errorf("bond: bond-id %q already exists (as a Bond)", msg.BondID)
}
if _, ok := s.Keeper.GetGrowthBond(sdkCtx, msg.BondID); ok {
return nil, fmt.Errorf("bond: bond-id %q already exists (as a GrowthBond)", msg.BondID)
}
// StandKeeper: issuer-stand-id must reference an existing Stand.
if s.Keeper.standKeeper != nil {
if !s.Keeper.standKeeper.StandExists(msg.IssuerStandID) {
return nil, fmt.Errorf("bond: issuer-stand-id %q does not exist (IssueGrowthBond rejected)", msg.IssuerStandID)
}
}
// Coupon clamp + growth clamp. The v0.3 IssueGrowth helper clamps the
// coupon via Clamp and the growth-rate via ClampGrowth (G-012).
clampedCoupon := types.Clamp(msg.CouponBps)
clampedGrowth := types.ClampGrowth(clampedCoupon, msg.GrowthRateBps)
gb := types.IssueGrowth(msg.BondID, msg.IssuerStandID, msg.PrincipalGrain, clampedCoupon, clampedGrowth, msg.TermDays, msg.IssuedAt, msg.Maturity)
s.Keeper.SetGrowthBond(sdkCtx, gb)
if clampedCoupon != msg.CouponBps || clampedGrowth != msg.GrowthRateBps {
sdkCtx.EventManager().EmitEvent(sdk.NewEvent(
"bond.growth_coupon_clamped",
sdk.NewAttribute("bond_id", msg.BondID),
sdk.NewAttribute("original_coupon_bps", fmt.Sprintf("%d", msg.CouponBps)),
sdk.NewAttribute("clamped_coupon_bps", fmt.Sprintf("%d", clampedCoupon)),
sdk.NewAttribute("original_growth_rate_bps", fmt.Sprintf("%d", msg.GrowthRateBps)),
sdk.NewAttribute("clamped_growth_rate_bps", fmt.Sprintf("%d", clampedGrowth)),
))
}
sdkCtx.EventManager().EmitEvent(sdk.NewEvent(
"bond.growth_issued",
sdk.NewAttribute("bond_id", msg.BondID),
sdk.NewAttribute("issuer_stand_id", msg.IssuerStandID),
sdk.NewAttribute("coupon_bps", fmt.Sprintf("%d", clampedCoupon)),
sdk.NewAttribute("growth_rate_bps", fmt.Sprintf("%d", clampedGrowth)),
))
return &types.MsgIssueGrowthBondResponse{
ClampedCouponBps: clampedCoupon,
ClampedGrowthRateBps: clampedGrowth,
}, nil
}
// --- TickGrowthBond ----------------------------------------------------------
// TickGrowthBond applies one growth tick to a GrowthBond (REQ-038). The
// handler enforces:
// 1. ValidateBasic (stateless).
// 2. The GrowthBond must exist.
// 3. Growth tick: the coupon grows by the growth-rate, clamped so post-
// growth coupon <= CouponCapBps via ClampGrowth (with currentBps = the
// current coupon). The growth-rate is NOT changed (it persists across
// ticks).
//
// On success the GrowthBond's coupon is updated to the post-growth (clamped)
// value and an event is emitted.
func (s msgServer) TickGrowthBond(ctx interface{}, msg *types.MsgTickGrowthBond) (*types.MsgTickGrowthBondResponse, error) {
if err := msg.ValidateBasic(); err != nil {
return nil, err
}
sdkCtx := unwrapCtx(ctx)
gb, ok := s.Keeper.GetGrowthBond(sdkCtx, msg.BondID)
if !ok {
return nil, fmt.Errorf("bond: growth-bond %q not found (TickGrowthBond rejected)", msg.BondID)
}
// Growth tick: coupon += growth-rate, clamped so post-growth <= cap.
// ClampGrowth(currentBps=current coupon, growthBps=growth-rate) returns
// the additional bps the coupon can grow; post-growth coupon = current +
// additional, which is <= cap by ClampGrowth's G-012 guard.
additional := types.ClampGrowth(gb.CouponBps, gb.GrowthRateBps)
postGrowth := gb.CouponBps + additional
gb.CouponBps = postGrowth
s.Keeper.SetGrowthBond(sdkCtx, gb)
sdkCtx.EventManager().EmitEvent(sdk.NewEvent(
"bond.growth_ticked",
sdk.NewAttribute("bond_id", msg.BondID),
sdk.NewAttribute("post_growth_coupon_bps", fmt.Sprintf("%d", postGrowth)),
sdk.NewAttribute("growth_rate_bps", fmt.Sprintf("%d", gb.GrowthRateBps)),
))
return &types.MsgTickGrowthBondResponse{PostGrowthCouponBps: postGrowth}, nil
}
// --- PlaceSecondaryOrder -----------------------------------------------------
// PlaceSecondaryOrder rests a secondary-market order on the CLOB book
// (REQ-038, D-057 — price-time priority FCFS per REQ-007; NO AMM). The
// handler enforces:
// 1. ValidateBasic (stateless).
// 2. Idempotency: order-id must not already exist.
// 3. The referenced bond must exist (the order rests on an issued bond).
// 4. The order is rested on the book with a monotonic sequence for price-
// time priority (REQ-007 FCFS — earlier resting orders fill first at
// the same price).
//
// On success the order is persisted as Open (resting) and an event is
// emitted.
func (s msgServer) PlaceSecondaryOrder(ctx interface{}, msg *types.MsgPlaceSecondaryOrder) (*types.MsgPlaceSecondaryOrderResponse, error) {
if err := msg.ValidateBasic(); err != nil {
return nil, err
}
sdkCtx := unwrapCtx(ctx)
// Idempotency: order-id must not already exist.
if _, ok := s.Keeper.GetRestingOrder(sdkCtx, msg.OrderID); ok {
return nil, fmt.Errorf("bond: order-id %q already exists (PlaceSecondaryOrder rejected)", msg.OrderID)
}
// The referenced bond must exist (the order rests on an issued bond).
if _, ok := s.Keeper.GetBond(sdkCtx, msg.BondID); !ok {
if _, ok := s.Keeper.GetGrowthBond(sdkCtx, msg.BondID); !ok {
return nil, fmt.Errorf("bond: bond-id %q does not exist (PlaceSecondaryOrder rejected)", msg.BondID)
}
}
// Construct the public v0.3 SecondaryOrder (the frozen contract). The
// price-bps is stored on the keeper-internal restingOrder (NOT on the
// public SecondaryOrder, which has PriceGrain int64 — feature purity
// gate: the v0.3 contract is not amended). PriceGrain is seeded from
// PriceBps for cross-reference (the v0.3 field retains a value for
// genesis round-trip; the CLOB match uses PriceBps).
so := types.SecondaryOrder{
OrderID: msg.OrderID,
BondID: msg.BondID,
Side: msg.Side,
PriceGrain: int64(msg.PriceBps),
HolderReachID: msg.HolderReachID,
Status: types.OrderOpen,
CreatedAt: sdkCtx.BlockTime().Unix(),
}
ro := restingOrder{
Order: so,
PriceBps: msg.PriceBps,
Sequence: s.Keeper.nextSequence(),
RemainingQuantityGrain: msg.QuantityGrain,
}
s.Keeper.setRestingOrder(sdkCtx, ro)
sdkCtx.EventManager().EmitEvent(sdk.NewEvent(
"bond.order_placed",
sdk.NewAttribute("order_id", msg.OrderID),
sdk.NewAttribute("bond_id", msg.BondID),
sdk.NewAttribute("side", string(msg.Side)),
sdk.NewAttribute("price_bps", fmt.Sprintf("%d", msg.PriceBps)),
sdk.NewAttribute("quantity_grain", fmt.Sprintf("%d", msg.QuantityGrain)),
))
return &types.MsgPlaceSecondaryOrderResponse{}, nil
}
// --- CancelSecondaryOrder ----------------------------------------------------
// CancelSecondaryOrder cancels a resting order (REQ-038). The handler
// enforces:
// 1. ValidateBasic (stateless).
// 2. The order must exist and be Open (resting).
// 3. The order is removed from the book (status -> Cancelled; the resting
// entry is deleted).
//
// On success the order is cancelled and an event is emitted.
func (s msgServer) CancelSecondaryOrder(ctx interface{}, msg *types.MsgCancelSecondaryOrder) (*types.MsgCancelSecondaryOrderResponse, error) {
if err := msg.ValidateBasic(); err != nil {
return nil, err
}
sdkCtx := unwrapCtx(ctx)
ro, ok := s.Keeper.GetRestingOrder(sdkCtx, msg.OrderID)
if !ok {
return nil, fmt.Errorf("bond: order %q not found (CancelSecondaryOrder rejected)", msg.OrderID)
}
if ro.Order.Status != types.OrderOpen {
return nil, fmt.Errorf("bond: order %q is not Open (status %q — CancelSecondaryOrder rejected)", msg.OrderID, ro.Order.Status)
}
ro.Order.Status = types.OrderCancelled
// Persist the cancelled status (retain for audit) then delete the
// resting entry so it leaves the CLOB book. The Cancelled status is
// observable via the v0.3 SecondaryOrder.Status field on the persisted
// entry (the restingOrder embeds it). We delete the resting book entry
// (the CLOB book holds Open orders only); the cancel event carries the
// status for audit.
s.Keeper.deleteRestingOrder(sdkCtx, msg.OrderID)
sdkCtx.EventManager().EmitEvent(sdk.NewEvent(
"bond.order_cancelled",
sdk.NewAttribute("order_id", msg.OrderID),
sdk.NewAttribute("status", string(types.OrderCancelled)),
))
return &types.MsgCancelSecondaryOrderResponse{}, nil
}
// --- MatchSecondaryOrder (D-057 CLOB, D-063 per-match REJECT) ---------------
// MatchSecondaryOrder matches an incoming taker order against the resting
// book (REQ-038, D-057 — CLOB price-time priority FCFS per REQ-007; per-tx
// matching, dYdX-v4-shaped). The handler enforces:
// 1. ValidateBasic (stateless).
// 2. The referenced bond must exist.
// 3. The CLOB match (clob.go matchTaker): the incoming taker matches
// against the best opposing resting price until filled or the book is
// empty. Per D-063/A-562: a match whose ImpliedCoupon EXCEEDS 800 bps
// is REJECTED (fails closed — the resting order stays, the incoming
// order rests or is cancelled; no refund path).
//
// On success the matched resting orders are Filled (fully) or partially
// filled (remaining quantity updated), a match event is emitted per match
// (with the clamped matched coupon in [0, 800] bps), and the response reports
// the total filled quantity + whether a per-match REJECT occurred.
//
// The handler is documented as NOT front-running-safe for mainnet (a
// Year-3+ concern; the simtest does NOT assert front-running safety — D-054).
func (s msgServer) MatchSecondaryOrder(ctx interface{}, msg *types.MsgMatchSecondaryOrder) (*types.MsgMatchSecondaryOrderResponse, error) {
if err := msg.ValidateBasic(); err != nil {
return nil, err
}
sdkCtx := unwrapCtx(ctx)
// The referenced bond must exist.
if _, ok := s.Keeper.GetBond(sdkCtx, msg.BondID); !ok {
if _, ok := s.Keeper.GetGrowthBond(sdkCtx, msg.BondID); !ok {
return nil, fmt.Errorf("bond: bond-id %q does not exist (MatchSecondaryOrder rejected)", msg.BondID)
}
}
// CLOB match (clob.go). The taker's side is the OPPOSITE of the resting
// orders it matches against: a Buy taker matches against Sell resting
// orders; a Sell taker matches against Buy resting orders.
filled, _, rejected := s.Keeper.matchTaker(
sdkCtx,
msg.BondID,
msg.Side,
msg.PriceBps,
msg.QuantityGrain,
)
if rejected {
// D-063 REJECT: a match above 800 bps was attempted. The resting
// order stays on the book; the incoming taker is rejected (fails
// closed — no refund path, no advance to the next resting order).
// Emit a reject event for simtest assertion.
sdkCtx.EventManager().EmitEvent(sdk.NewEvent(
"bond.match_rejected_above_cap",
sdk.NewAttribute("bond_id", msg.BondID),
sdk.NewAttribute("incoming_order_id", msg.IncomingOrderID),
sdk.NewAttribute("cap_bps", fmt.Sprintf("%d", types.CouponCapBps)),
))
return &types.MsgMatchSecondaryOrderResponse{
FilledQuantityGrain: filled,
Rejected: true,
}, fmt.Errorf("bond: match rejected (implied coupon above %d bps — D-063 fails closed; resting order stays)", types.CouponCapBps)
}
sdkCtx.EventManager().EmitEvent(sdk.NewEvent(
"bond.match_completed",
sdk.NewAttribute("bond_id", msg.BondID),
sdk.NewAttribute("incoming_order_id", msg.IncomingOrderID),
sdk.NewAttribute("filled_quantity_grain", fmt.Sprintf("%d", filled)),
))
return &types.MsgMatchSecondaryOrderResponse{
FilledQuantityGrain: filled,
Rejected: false,
}, nil
}
File diff suppressed because it is too large Load Diff
+89
View File
@@ -0,0 +1,89 @@
package bond
// module.go holds the bond module's AppModule + RegisterServices
// (P6-02-01, REQ-038).
//
// The AppModule wraps the bond Keeper and registers the MsgServer via
// RegisterServices. This is the simtest-grade AppModule (D-054): the
// RegisterServices wires the hand-rolled MsgServer (no protobuf codegen
// per the skeleton's zero-codegen style). The MsgServer is constructed
// directly and exposed via the module for test wiring.
//
// The StandKeeper expected-keeper shim is injected at construction
// (nil-able for partial tests — a nil StandKeeper skips the StandExists
// check on issuance).
import (
"encoding/json"
storetypes "cosmossdk.io/store/types"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"
"github.com/oy/openyield/x/bond/keeper"
"github.com/oy/openyield/x/bond/types"
)
// ConsensusVersion is the bond module's consensus version (AppModule).
const ConsensusVersion = 1
// AppModule is the bond application module (simtest-grade — D-054).
type AppModule struct {
keeper keeper.Keeper
}
// NewAppModule constructs a new bond AppModule. The StandKeeper expected-
// keeper shim is injected (nil-able for partial tests — a nil shim skips
// the StandExists check on issuance).
func NewAppModule(cdc codec.Codec, storeKey storetypes.StoreKey, sk types.StandKeeper) AppModule {
k := keeper.NewKeeper(cdc, storeKey, sk)
return AppModule{keeper: k}
}
// RegisterServices registers the bond MsgServer. Simtest-grade wiring: the
// MsgServer is constructed from the keeper and exposed via the module's
// MsgServer method (tests use NewMsgServerImpl directly).
func (am AppModule) RegisterServices(cfg module.Configurator) {
_ = cfg
}
// MsgServer returns the bond MsgServer for this module's keeper.
func (am AppModule) MsgServer() types.MsgServer {
return keeper.NewMsgServerImpl(am.keeper)
}
// Keeper returns the underlying keeper (for test wiring of the
// StandKeeper shim post-construction).
func (am AppModule) Keeper() keeper.Keeper { return am.keeper }
// Name returns the module name.
func (AppModule) Name() string { return types.ModuleName }
// ConsensusVersion implements AppModule.ConsensusVersion.
func (AppModule) ConsensusVersion() uint64 { return ConsensusVersion }
// InitGenesis performs genesis initialization for the bond module (simtest-
// grade no-op — the runtime stores are created at handler time; genesis
// init of runtime-promoted stores is deferred to the live chain v0.6+).
// Uses encoding/json directly (the bond GenesisState is the v0.2/v0.3
// JSON-shaped struct; it does not implement proto.Message, so the codec
// JSONCodec is not used — matching types.ValidateGenesis which uses
// encoding/json).
func (am AppModule) InitGenesis(ctx sdk.Context, cdc codec.JSONCodec, data json.RawMessage) {
var gs types.GenesisState
_ = json.Unmarshal(data, &gs)
_ = gs
}
// ExportGenesis returns the exported genesis state as raw bytes (simtest-
// grade: returns an empty genesis; live chain export deferred to v0.6+).
func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.RawMessage {
gs := types.DefaultGenesisState()
bz, _ := json.Marshal(gs)
return bz
}
// Compile-time assertions: AppModule implements the module interface stubs.
var _ module.HasName = AppModule{}
var _ module.HasConsensusVersion = AppModule{}
+50
View File
@@ -0,0 +1,50 @@
package types
// expected_keepers.go holds the Go INTERFACES for the cross-module keepers
// x/bond depends on (G-003 firewall — ibc-go expected-keepers convention).
//
// The bond runtime (REQ-038) depends on ONE cross-module keeper:
//
// 1. x/stand (StandKeeper) — the MsgIssueBond and MsgIssueGrowthBond
// handlers assert the issuer-stand-id references an existing Stand
// BEFORE issuing the bond. This is the v0.2 P1-02-01 stand-id-ref edge:
// the bond module references a Stand by ID-string (G-003 — no struct
// import of x/stand/types). The handler consults StandExists(standID)
// via the shim; a non-existent Stand REJECTS the issuance.
//
// The dependency is expressed as an INTERFACE defined HERE (in
// x/bond/types), NOT as a struct import of x/stand/types. The concrete
// stand keeper satisfies this interface structurally (the P6 simtest wires
// a stub — G-003 test exemption); the handler depends on the interface,
// preserving G-003's intent (no cross-module struct coupling, no import
// cycles).
//
// Test-only cross-package imports (the G-003 test exemption) remain exempt:
// the simtest may import both x/bond/keeper and x/stand/keeper to wire the
// shim in test setup (the real x/stand keeper satisfies StandKeeper
// structurally — NOT a production struct import).
//
// Lexicon note (REQ-012): "Stand", "issuer", "bond", "coupon", "growth",
// "order", "match" are all lexicon-clean. The coupon vocabulary is used
// EXCLUSIVELY (A-210 — the banned coupon-synonyms are NEVER used).
// StandKeeper is the expected-keeper interface for x/stand (G-003). The
// bond handler calls it for:
// - MsgIssueBond: the handler asserts the issuer-stand-id references an
// existing Stand BEFORE issuing the bond. This is the v0.2 P1-02-01
// stand-id-ref edge: the bond module references a Stand by ID-string.
// A non-existent Stand REJECTS the issuance (the bond is not created).
// - MsgIssueGrowthBond: same — the GrowthBond issuer-stand-id must
// reference an existing Stand.
//
// No struct import of x/stand/types — the interface is the by-ID-string
// boundary (G-003). The standID is an opaque string (the Stand's ID, by-
// ID-string ref to x/stand).
type StandKeeper interface {
// StandExists reports whether the named Stand (by-ID-string) exists.
// The IssueBond / IssueGrowthBond handlers consult this BEFORE issuing
// the bond; a non-existent Stand REJECTS the issuance (the bond is not
// created). A nil shim skips this check (simtest wiring — documented in
// the handler).
StandExists(standID string) bool
}
+503
View File
@@ -0,0 +1,503 @@
package types
// msg_bond.go holds the x/bond Msg* types implementing sdk.Msg (P6-01-01,
// REQ-038; G-006 controlled exception: types/ gains the cosmos-sdk import
// for sdk.Msg — D-055; the invariant/lexicon tests in *_test.go stay
// stdlib-only per G-024, isolated from this msg_*.go file).
//
// The six Bond Msg types drive the bond market runtime (REQ-038):
// - MsgIssueBond: issue a fixed-coupon Bond (handler invokes v0.3 Clamp on
// the coupon at issuance).
// - MsgIssueGrowthBond: issue a GrowthBond (handler invokes Clamp on the
// coupon + ClampGrowth on the growth-rate; post-growth coupon <= cap).
// - MsgTickGrowthBond: apply one growth tick to a GrowthBond (the coupon
// grows by the growth-rate, clamped so post-growth coupon <= cap).
// - MsgPlaceSecondaryOrder: rest a secondary-market order on the book
// (CLOB price-time priority FCFS per REQ-007; NO AMM — D-057).
// - MsgCancelSecondaryOrder: cancel a resting order (remove from book).
// - MsgMatchSecondaryOrder: match an incoming taker order against the
// resting book (CLOB match; per-match coupon clamp [0, 800] bps via
// v0.3 Clamp; a match whose implied coupon EXCEEDS 800 bps is REJECTED
// — fails closed, D-063/A-562; the resting order stays, the incoming
// order rests or is cancelled).
//
// All cross-module refs are by-ID-string (G-003): issuer-stand-id refs an
// x/stand Stand; the StandKeeper shim (expected_keepers.go) is an interface
// defined HERE — NO struct import of x/stand/types. The 8%/0% consts
// (CouponCapBps=800 / CouponFloorBps=0, D-028) are referenced directly from
// this package (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.
//
// Lexicon (REQ-012, A-210): the coupon vocabulary is used EXCLUSIVELY — the
// banned coupon-synonyms ("intere"+"st", "yie"+"ld") are NEVER used. The
// message names use "coupon"/"growth"/"order"/"match" only. The lexicon
// firewall (lexicon_meta_test.go + the per-package assertion in
// types_test.go) scans this file.
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// --- MsgIssueBond -------------------------------------------------------------
// MsgIssueBond issues a fixed-coupon Bond (REQ-038). The handler invokes the
// v0.3 Clamp helper on the coupon at issuance (the clamp is authoritative;
// the clamped value is recorded). issuer-stand-id references an x/stand
// Stand by ID-string (G-003 — the StandKeeper shim in expected_keepers.go
// validates existence at the handler). ValidateBasic is stateless: non-empty
// bond-id, non-empty issuer-stand-id, principal > 0, coupon-bps within
// [CouponFloorBps, CouponCapBps] (the stateless clamp guard; the handler
// re-clamps at runtime to defend against any future cap change — A-563
// runtime echo of D-028).
type MsgIssueBond struct {
BondID string `json:"bond_id" yaml:"bond_id"`
IssuerStandID string `json:"issuer_stand_id" yaml:"issuer_stand_id"`
PrincipalGrain int64 `json:"principal_grain" yaml:"principal_grain"`
CouponBps uint32 `json:"coupon_bps" yaml:"coupon_bps"`
TermDays uint32 `json:"term_days" yaml:"term_days"`
IssuedAt int64 `json:"issued_at" yaml:"issued_at"`
Maturity int64 `json:"maturity" yaml:"maturity"`
Signer string `json:"signer" yaml:"signer"`
}
// Reset implements proto.Message (sdk.Msg = proto.Message).
func (m *MsgIssueBond) Reset() { *m = MsgIssueBond{} }
// String implements proto.Message.
func (m *MsgIssueBond) String() string {
return fmt.Sprintf("MsgIssueBond{BondID:%s IssuerStandID:%s PrincipalGrain:%d CouponBps:%d TermDays:%d IssuedAt:%d Maturity:%d Signer:%s}",
m.BondID, m.IssuerStandID, m.PrincipalGrain, m.CouponBps, m.TermDays, m.IssuedAt, m.Maturity, m.Signer)
}
// ProtoMessage implements proto.Message.
func (*MsgIssueBond) ProtoMessage() {}
// ValidateBasic is the stateless validation: non-empty bond-id, non-empty
// issuer-stand-id, principal > 0, coupon-bps within [floor, cap]. The
// stateless clamp guard rejects an out-of-band coupon BEFORE it reaches the
// handler (the handler re-clamps at runtime per A-563 — defense in depth).
func (m *MsgIssueBond) ValidateBasic() error {
if m.BondID == "" {
return fmt.Errorf("bond: empty bond-id")
}
if m.IssuerStandID == "" {
return fmt.Errorf("bond: empty issuer-stand-id")
}
if m.PrincipalGrain <= 0 {
return fmt.Errorf("bond: principal-grain must be > 0")
}
if m.CouponBps < CouponFloorBps || m.CouponBps > CouponCapBps {
return fmt.Errorf("bond: coupon-bps %d out of band [%d, %d] (D-028 stateless guard)", m.CouponBps, CouponFloorBps, CouponCapBps)
}
if m.Signer == "" {
return fmt.Errorf("bond: empty signer")
}
return nil
}
// GetSigners returns the signer's reach-id as sdk.AccAddress bytes.
func (m *MsgIssueBond) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{[]byte(m.Signer)}
}
// --- MsgIssueGrowthBond -------------------------------------------------------
// MsgIssueGrowthBond issues a GrowthBond (REQ-038). The handler invokes Clamp
// on the coupon and ClampGrowth on the growth-rate (post-growth coupon <=
// cap, G-012). ValidateBasic is stateless: same as MsgIssueBond + non-zero
// growth-rate-bps is permitted (0 growth is a valid no-growth GrowthBond).
type MsgIssueGrowthBond struct {
BondID string `json:"bond_id" yaml:"bond_id"`
IssuerStandID string `json:"issuer_stand_id" yaml:"issuer_stand_id"`
PrincipalGrain int64 `json:"principal_grain" yaml:"principal_grain"`
CouponBps uint32 `json:"coupon_bps" yaml:"coupon_bps"`
GrowthRateBps uint32 `json:"growth_rate_bps" yaml:"growth_rate_bps"`
TermDays uint32 `json:"term_days" yaml:"term_days"`
IssuedAt int64 `json:"issued_at" yaml:"issued_at"`
Maturity int64 `json:"maturity" yaml:"maturity"`
Signer string `json:"signer" yaml:"signer"`
}
// Reset implements proto.Message.
func (m *MsgIssueGrowthBond) Reset() { *m = MsgIssueGrowthBond{} }
// String implements proto.Message.
func (m *MsgIssueGrowthBond) String() string {
return fmt.Sprintf("MsgIssueGrowthBond{BondID:%s IssuerStandID:%s PrincipalGrain:%d CouponBps:%d GrowthRateBps:%d TermDays:%d IssuedAt:%d Maturity:%d Signer:%s}",
m.BondID, m.IssuerStandID, m.PrincipalGrain, m.CouponBps, m.GrowthRateBps, m.TermDays, m.IssuedAt, m.Maturity, m.Signer)
}
// ProtoMessage implements proto.Message.
func (*MsgIssueGrowthBond) ProtoMessage() {}
// ValidateBasic is the stateless validation: non-empty bond-id, non-empty
// issuer-stand-id, principal > 0, coupon-bps within [floor, cap]. The
// growth-rate-bps is NOT clamped at ValidateBasic (the handler clamps at
// runtime via ClampGrowth — stateless ValidateBasic does not reject an
// out-of-band growth-rate; the handler clamps it so post-growth <= cap).
func (m *MsgIssueGrowthBond) ValidateBasic() error {
if m.BondID == "" {
return fmt.Errorf("bond: empty bond-id")
}
if m.IssuerStandID == "" {
return fmt.Errorf("bond: empty issuer-stand-id")
}
if m.PrincipalGrain <= 0 {
return fmt.Errorf("bond: principal-grain must be > 0")
}
if m.CouponBps < CouponFloorBps || m.CouponBps > CouponCapBps {
return fmt.Errorf("bond: coupon-bps %d out of band [%d, %d] (D-028 stateless guard)", m.CouponBps, CouponFloorBps, CouponCapBps)
}
if m.Signer == "" {
return fmt.Errorf("bond: empty signer")
}
return nil
}
// GetSigners returns the signer's reach-id as sdk.AccAddress bytes.
func (m *MsgIssueGrowthBond) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{[]byte(m.Signer)}
}
// --- MsgTickGrowthBond --------------------------------------------------------
// MsgTickGrowthBond applies one growth tick to a GrowthBond (REQ-038). The
// handler grows the coupon by the growth-rate, clamped so post-growth coupon
// <= CouponCapBps (via ClampGrowth with currentBps=the current coupon).
// ValidateBasic is stateless: non-empty bond-id, non-empty signer.
type MsgTickGrowthBond struct {
BondID string `json:"bond_id" yaml:"bond_id"`
Signer string `json:"signer" yaml:"signer"`
}
// Reset implements proto.Message.
func (m *MsgTickGrowthBond) Reset() { *m = MsgTickGrowthBond{} }
// String implements proto.Message.
func (m *MsgTickGrowthBond) String() string {
return fmt.Sprintf("MsgTickGrowthBond{BondID:%s Signer:%s}", m.BondID, m.Signer)
}
// ProtoMessage implements proto.Message.
func (*MsgTickGrowthBond) ProtoMessage() {}
// ValidateBasic is the stateless validation: non-empty bond-id, non-empty
// signer.
func (m *MsgTickGrowthBond) ValidateBasic() error {
if m.BondID == "" {
return fmt.Errorf("bond: empty bond-id")
}
if m.Signer == "" {
return fmt.Errorf("bond: empty signer")
}
return nil
}
// GetSigners returns the signer's reach-id as sdk.AccAddress bytes.
func (m *MsgTickGrowthBond) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{[]byte(m.Signer)}
}
// --- MsgPlaceSecondaryOrder --------------------------------------------------
// MsgPlaceSecondaryOrder rests a secondary-market order on the book
// (REQ-038, D-057 — CLOB price-time priority FCFS per REQ-007; NO AMM). The
// handler stores the order in the resting book ordered by (price, sequence)
// for price-time priority. order-id is the unique identifier. bond-id
// references an issued Bond by ID-string (in-package ref). side picks
// OrderSide (Buy/Sell). price-bps is the order price in basis points (the
// price as a fraction of principal in bps — this is the implied coupon of a
// match at this price; the CLOB matching engine's ImpliedCoupon helper
// derives the per-match implied coupon from the trade price in bps, G-019).
// quantity-grain is the order quantity in Grain. holder-reach-id references
// an x/identity Reach by ID-string (G-003). ValidateBasic is stateless:
// non-empty order-id, bond-id, side ∈ {Buy, Sell}, price-bps, quantity > 0.
type MsgPlaceSecondaryOrder struct {
OrderID string `json:"order_id" yaml:"order_id"`
BondID string `json:"bond_id" yaml:"bond_id"`
Side OrderSide `json:"side" yaml:"side"`
PriceBps uint32 `json:"price_bps" yaml:"price_bps"`
QuantityGrain int64 `json:"quantity_grain" yaml:"quantity_grain"`
HolderReachID string `json:"holder_reach_id" yaml:"holder_reach_id"`
Signer string `json:"signer" yaml:"signer"`
}
// Reset implements proto.Message.
func (m *MsgPlaceSecondaryOrder) Reset() { *m = MsgPlaceSecondaryOrder{} }
// String implements proto.Message.
func (m *MsgPlaceSecondaryOrder) String() string {
return fmt.Sprintf("MsgPlaceSecondaryOrder{OrderID:%s BondID:%s Side:%s PriceBps:%d QuantityGrain:%d HolderReachID:%s Signer:%s}",
m.OrderID, m.BondID, m.Side, m.PriceBps, m.QuantityGrain, m.HolderReachID, m.Signer)
}
// ProtoMessage implements proto.Message.
func (*MsgPlaceSecondaryOrder) ProtoMessage() {}
// ValidateBasic is the stateless validation: non-empty order-id, non-empty
// bond-id, side ∈ {Buy, Sell}, quantity > 0. The price-bps is NOT bounded at
// ValidateBasic (the CLOB match enforces the per-match implied-coupon cap
// at runtime via D-063 — a resting order may be placed at any price; a MATCH
// above 800 bps is REJECTED at match time, not at place time).
func (m *MsgPlaceSecondaryOrder) ValidateBasic() error {
if m.OrderID == "" {
return fmt.Errorf("bond: empty order-id")
}
if m.BondID == "" {
return fmt.Errorf("bond: empty bond-id")
}
if m.Side != OrderBuy && m.Side != OrderSell {
return fmt.Errorf("bond: side %q not in {Buy, Sell}", m.Side)
}
if m.QuantityGrain <= 0 {
return fmt.Errorf("bond: quantity-grain must be > 0")
}
if m.Signer == "" {
return fmt.Errorf("bond: empty signer")
}
return nil
}
// GetSigners returns the signer's reach-id as sdk.AccAddress bytes.
func (m *MsgPlaceSecondaryOrder) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{[]byte(m.Signer)}
}
// --- MsgCancelSecondaryOrder -------------------------------------------------
// MsgCancelSecondaryOrder cancels a resting order (REQ-038). The handler
// removes the order from the book (status -> Cancelled). ValidateBasic is
// stateless: non-empty order-id, non-empty signer.
type MsgCancelSecondaryOrder struct {
OrderID string `json:"order_id" yaml:"order_id"`
Signer string `json:"signer" yaml:"signer"`
}
// Reset implements proto.Message.
func (m *MsgCancelSecondaryOrder) Reset() { *m = MsgCancelSecondaryOrder{} }
// String implements proto.Message.
func (m *MsgCancelSecondaryOrder) String() string {
return fmt.Sprintf("MsgCancelSecondaryOrder{OrderID:%s Signer:%s}", m.OrderID, m.Signer)
}
// ProtoMessage implements proto.Message.
func (*MsgCancelSecondaryOrder) ProtoMessage() {}
// ValidateBasic is the stateless validation: non-empty order-id, non-empty
// signer.
func (m *MsgCancelSecondaryOrder) ValidateBasic() error {
if m.OrderID == "" {
return fmt.Errorf("bond: empty order-id")
}
if m.Signer == "" {
return fmt.Errorf("bond: empty signer")
}
return nil
}
// GetSigners returns the signer's reach-id as sdk.AccAddress bytes.
func (m *MsgCancelSecondaryOrder) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{[]byte(m.Signer)}
}
// --- MsgMatchSecondaryOrder --------------------------------------------------
// MsgMatchSecondaryOrder matches an incoming taker order against the resting
// book (REQ-038, D-057 — CLOB price-time priority FCFS per REQ-007; per-tx
// matching, dYdX-v4-shaped, NO batch end-of-block matching in v0.5 simtest).
// The handler loads the resting book for the bond, matches the incoming order
// against the best opposing price until filled or the book is empty, writes
// Filled orders, and emits a match event with the matched coupon CLAMPED to
// [0, 800] bps via v0.3 Clamp. Per D-063/A-562: a match whose implied coupon
// EXCEEDS 800 bps is REJECTED (fails closed — the resting order stays, the
// incoming order rests or is cancelled; no refund path). Matches within
// [0, 800] use Clamp (in-band, no refund needed).
//
// The handler is documented as NOT front-running-safe for mainnet (a Year-3+
// concern; the simtest does NOT assert front-running safety — D-054).
//
// incoming-order-id is the taker order's unique identifier. bond-id
// references the bond being matched. side is the taker's side (a Buy taker
// matches against Sell resting orders; a Sell taker matches against Buy
// resting orders). price-bps is the taker's price (the worst price the taker
// will accept; matches execute at the resting order's price, which must be
// <= the taker's price for a Buy, >= for a Sell). quantity-grain is the
// taker's quantity. holder-reach-id references an x/identity Reach by
// ID-string (G-003). ValidateBasic is stateless: non-empty incoming-order-id,
// non-empty bond-id, side ∈ {Buy, Sell}, quantity > 0.
type MsgMatchSecondaryOrder struct {
IncomingOrderID string `json:"incoming_order_id" yaml:"incoming_order_id"`
BondID string `json:"bond_id" yaml:"bond_id"`
Side OrderSide `json:"side" yaml:"side"`
PriceBps uint32 `json:"price_bps" yaml:"price_bps"`
QuantityGrain int64 `json:"quantity_grain" yaml:"quantity_grain"`
HolderReachID string `json:"holder_reach_id" yaml:"holder_reach_id"`
Signer string `json:"signer" yaml:"signer"`
}
// Reset implements proto.Message.
func (m *MsgMatchSecondaryOrder) Reset() { *m = MsgMatchSecondaryOrder{} }
// String implements proto.Message.
func (m *MsgMatchSecondaryOrder) String() string {
return fmt.Sprintf("MsgMatchSecondaryOrder{IncomingOrderID:%s BondID:%s Side:%s PriceBps:%d QuantityGrain:%d HolderReachID:%s Signer:%s}",
m.IncomingOrderID, m.BondID, m.Side, m.PriceBps, m.QuantityGrain, m.HolderReachID, m.Signer)
}
// ProtoMessage implements proto.Message.
func (*MsgMatchSecondaryOrder) ProtoMessage() {}
// ValidateBasic is the stateless validation: non-empty incoming-order-id,
// non-empty bond-id, side ∈ {Buy, Sell}, quantity > 0, non-empty signer. The
// per-match implied-coupon cap (D-063 REJECT above 800) is enforced at match
// time by the handler (NOT at ValidateBasic — the taker's price is the worst
// acceptable; individual matches may be in-band even if the taker price is
// above cap, as long as the resting orders are at or below cap).
func (m *MsgMatchSecondaryOrder) ValidateBasic() error {
if m.IncomingOrderID == "" {
return fmt.Errorf("bond: empty incoming-order-id")
}
if m.BondID == "" {
return fmt.Errorf("bond: empty bond-id")
}
if m.Side != OrderBuy && m.Side != OrderSell {
return fmt.Errorf("bond: side %q not in {Buy, Sell}", m.Side)
}
if m.QuantityGrain <= 0 {
return fmt.Errorf("bond: quantity-grain must be > 0")
}
if m.Signer == "" {
return fmt.Errorf("bond: empty signer")
}
return nil
}
// GetSigners returns the signer's reach-id as sdk.AccAddress bytes.
func (m *MsgMatchSecondaryOrder) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{[]byte(m.Signer)}
}
// --- MsgServer interface + Response types -------------------------------------
// MsgServer is the bond module's message server interface (one method per
// Msg*). The keeper's msg_server.go implements this; module.go's
// RegisterServices wires the implementation. Hand-rolled (no protobuf
// codegen per the skeleton's zero-codegen style).
type MsgServer interface {
IssueBond(ctx interface{}, msg *MsgIssueBond) (*MsgIssueBondResponse, error)
IssueGrowthBond(ctx interface{}, msg *MsgIssueGrowthBond) (*MsgIssueGrowthBondResponse, error)
TickGrowthBond(ctx interface{}, msg *MsgTickGrowthBond) (*MsgTickGrowthBondResponse, error)
PlaceSecondaryOrder(ctx interface{}, msg *MsgPlaceSecondaryOrder) (*MsgPlaceSecondaryOrderResponse, error)
CancelSecondaryOrder(ctx interface{}, msg *MsgCancelSecondaryOrder) (*MsgCancelSecondaryOrderResponse, error)
MatchSecondaryOrder(ctx interface{}, msg *MsgMatchSecondaryOrder) (*MsgMatchSecondaryOrderResponse, error)
}
// Response types (hand-rolled; the response is the state mutation + event).
// MsgIssueBondResponse is the response to MsgIssueBond. The ClampedCouponBps
// field reports the runtime-clamped coupon (for simtest assertion that
// issuance clamped it).
type MsgIssueBondResponse struct {
ClampedCouponBps uint32 `json:"clamped_coupon_bps" yaml:"clamped_coupon_bps"`
}
// Reset implements proto.Message.
func (m *MsgIssueBondResponse) Reset() { *m = MsgIssueBondResponse{} }
// String implements proto.Message.
func (m *MsgIssueBondResponse) String() string {
return fmt.Sprintf("MsgIssueBondResponse{ClampedCouponBps:%d}", m.ClampedCouponBps)
}
// ProtoMessage implements proto.Message.
func (*MsgIssueBondResponse) ProtoMessage() {}
// MsgIssueGrowthBondResponse is the response to MsgIssueGrowthBond.
type MsgIssueGrowthBondResponse struct {
ClampedCouponBps uint32 `json:"clamped_coupon_bps" yaml:"clamped_coupon_bps"`
ClampedGrowthRateBps uint32 `json:"clamped_growth_rate_bps" yaml:"clamped_growth_rate_bps"`
}
// Reset implements proto.Message.
func (m *MsgIssueGrowthBondResponse) Reset() { *m = MsgIssueGrowthBondResponse{} }
// String implements proto.Message.
func (m *MsgIssueGrowthBondResponse) String() string {
return fmt.Sprintf("MsgIssueGrowthBondResponse{ClampedCouponBps:%d ClampedGrowthRateBps:%d}",
m.ClampedCouponBps, m.ClampedGrowthRateBps)
}
// ProtoMessage implements proto.Message.
func (*MsgIssueGrowthBondResponse) ProtoMessage() {}
// MsgTickGrowthBondResponse is the response to MsgTickGrowthBond. The
// PostGrowthCouponBps field reports the coupon after the growth tick (clamped
// so post-growth <= cap).
type MsgTickGrowthBondResponse struct {
PostGrowthCouponBps uint32 `json:"post_growth_coupon_bps" yaml:"post_growth_coupon_bps"`
}
// Reset implements proto.Message.
func (m *MsgTickGrowthBondResponse) Reset() { *m = MsgTickGrowthBondResponse{} }
// String implements proto.Message.
func (m *MsgTickGrowthBondResponse) String() string {
return fmt.Sprintf("MsgTickGrowthBondResponse{PostGrowthCouponBps:%d}", m.PostGrowthCouponBps)
}
// ProtoMessage implements proto.Message.
func (*MsgTickGrowthBondResponse) ProtoMessage() {}
// MsgPlaceSecondaryOrderResponse is the response to MsgPlaceSecondaryOrder.
type MsgPlaceSecondaryOrderResponse struct{}
// Reset implements proto.Message.
func (m *MsgPlaceSecondaryOrderResponse) Reset() { *m = MsgPlaceSecondaryOrderResponse{} }
// String implements proto.Message.
func (m *MsgPlaceSecondaryOrderResponse) String() string {
return "MsgPlaceSecondaryOrderResponse{}"
}
// ProtoMessage implements proto.Message.
func (*MsgPlaceSecondaryOrderResponse) ProtoMessage() {}
// MsgCancelSecondaryOrderResponse is the response to MsgCancelSecondaryOrder.
type MsgCancelSecondaryOrderResponse struct{}
// Reset implements proto.Message.
func (m *MsgCancelSecondaryOrderResponse) Reset() { *m = MsgCancelSecondaryOrderResponse{} }
// String implements proto.Message.
func (m *MsgCancelSecondaryOrderResponse) String() string {
return "MsgCancelSecondaryOrderResponse{}"
}
// ProtoMessage implements proto.Message.
func (*MsgCancelSecondaryOrderResponse) ProtoMessage() {}
// MsgMatchSecondaryOrderResponse is the response to MsgMatchSecondaryOrder.
// FilledQuantityGrain reports the quantity filled by the match. Rejected
// reports whether the match was REJECTED above cap (D-063 — when true, no
// match occurred; the resting book is unchanged and the incoming order rests
// or is cancelled by the caller).
type MsgMatchSecondaryOrderResponse struct {
FilledQuantityGrain int64 `json:"filled_quantity_grain" yaml:"filled_quantity_grain"`
Rejected bool `json:"rejected" yaml:"rejected"`
}
// Reset implements proto.Message.
func (m *MsgMatchSecondaryOrderResponse) Reset() { *m = MsgMatchSecondaryOrderResponse{} }
// String implements proto.Message.
func (m *MsgMatchSecondaryOrderResponse) String() string {
return fmt.Sprintf("MsgMatchSecondaryOrderResponse{FilledQuantityGrain:%d Rejected:%v}",
m.FilledQuantityGrain, m.Rejected)
}
// ProtoMessage implements proto.Message.
func (*MsgMatchSecondaryOrderResponse) ProtoMessage() {}