Files
openyield/x/hub/keeper/keeper.go
T
cloudinit-bot 3c52aa1bb2
docs-build / go test ./... (lexicon firewall + all x/* tests) (push) Has been cancelled
docs-build / mkdocs build (docs site artifact) (push) Has been cancelled
Merge phase/04 into milestone/v0.5-bearers-runtime (P4 complete → v0.4.4)
---ci---
project: oy
phase: 4
milestone: v0.5
status: complete
requirements:
  covered: [REQ-036]
  partial: []
---/ci---
2026-08-18 00:49:16 +00:00

264 lines
10 KiB
Go

package keeper
// keeper.go holds the store-backed Keeper for the hub module's custody/
// lending/compliance runtime (P4-04-01, REQ-036).
//
// The Keeper wraps an sdk.KVStore via a storeKey. It holds:
// - the custody asset records (custody_state.go — assetID → CustodyEntry);
// - the registered custody services (service-id → CustodyService);
// - the lending primitive records (loan-id → LendingPrimitive);
// - the compliance attestation records (partner-id → attestation-ref, the
// store the ComplianceKeeper shim's IsCompliant reads — A-544).
//
// The Keeper also holds the two expected-keeper shims (PartnerKeeper for
// IsAnchorOnboarded on RegisterCustodyService; ComplianceKeeper for
// IsCompliant on CustodyReleaseAsset — A-544 compliance-before-custody).
// The shims are interfaces (G-003 — no struct import of x/partner/types);
// the concrete partner keeper satisfies them structurally.
//
// The Keeper holds the CustodyKeyring (D-058) — the custody key-share
// abstraction. v0.5 ships the in-memory test-only memKeyring impl
// (keyring_mem.go); real MPC/HSM backing is deferred (Year 3+). The
// handler consults the keyring per operation (no cross-block caching —
// D-058: a cached pubkey breaks rotation).
//
// State-machine ordering (vision §7, enforced in every handler):
// ValidateBasic → keeper authz → state mutation → ctx.EventManager().EmitEvent
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/hub/types"
)
// Keeper is the store-backed hub custody/lending/compliance keeper.
type Keeper struct {
cdc codec.Codec
storeKey storetypes.StoreKey
partnerKeeper types.PartnerKeeper
keyring types.CustodyKeyring
custody custodyStore
}
// NewKeeper constructs a new store-backed hub Keeper. The PartnerKeeper
// expected-keeper shim is injected (nil-able for partial tests; the
// RegisterCustodyService handler guards a nil shim and skips the
// IsAnchorOnboarded check, still mutating state — the simtest wiring
// documents this). The CustodyKeyring is injected (D-058 — the memKeyring
// for simtest; real MPC/HSM for production, deferred).
//
// The ComplianceKeeper shim is satisfied by the Keeper ITSELF (the
// IsCompliant method reads the attestation store the
// RecordComplianceAttestation handler populates — A-544); the
// CustodyReleaseAsset handler passes the keeper as the ComplianceKeeper.
// This is the by-ID-string boundary (G-003): the hub keeper satisfies
// ComplianceKeeper structurally (same package; no cross-module struct
// import).
func NewKeeper(cdc codec.Codec, storeKey storetypes.StoreKey, pk types.PartnerKeeper, kr types.CustodyKeyring) Keeper {
return Keeper{
cdc: cdc,
storeKey: storeKey,
partnerKeeper: pk,
keyring: kr,
custody: custodyStore{storeKey: storeKey},
}
}
// SetPartnerKeeper sets the PartnerKeeper expected-keeper shim (for
// post-construction wiring, e.g., app wiring or test setup).
func (k *Keeper) SetPartnerKeeper(pk types.PartnerKeeper) { k.partnerKeeper = pk }
// SetKeyring sets the CustodyKeyring (for post-construction wiring).
func (k *Keeper) SetKeyring(kr types.CustodyKeyring) { k.keyring = kr }
// Compile-time assertion: Keeper satisfies types.ComplianceKeeper (the
// CustodyReleaseAsset handler passes the keeper as the ComplianceKeeper
// shim — A-544 compliance-before-custody; the IsCompliant method reads the
// attestation store the RecordComplianceAttestation handler populates).
var _ types.ComplianceKeeper = (*Keeper)(nil)
// --- Custody service store ---------------------------------------------------
var custodyServiceKeyPrefix = []byte("svc/custody/")
func custodyServiceKey(serviceID string) []byte {
return append(custodyServiceKeyPrefix, []byte(serviceID)...)
}
// GetCustodyService loads a registered custody service by service-id.
// Returns the service and true if found, or zero value + false if not.
func (k Keeper) GetCustodyService(ctx sdk.Context, serviceID string) (types.CustodyService, bool) {
store := ctx.KVStore(k.storeKey)
bz := store.Get(custodyServiceKey(serviceID))
if bz == nil {
return types.CustodyService{}, false
}
var s types.CustodyService
if err := json.Unmarshal(bz, &s); err != nil {
return types.CustodyService{}, false
}
return s, true
}
// SetCustodyService persists a registered custody service by service-id.
func (k Keeper) SetCustodyService(ctx sdk.Context, s types.CustodyService) {
store := ctx.KVStore(k.storeKey)
bz, err := json.Marshal(s)
if err != nil {
panic(fmt.Sprintf("hub: marshal custody service %q: %v", s.CustodyID, err))
}
store.Set(custodyServiceKey(s.CustodyID), bz)
}
// AllCustodyServices returns all registered custody services.
func (k Keeper) AllCustodyServices(ctx sdk.Context) []types.CustodyService {
store := ctx.KVStore(k.storeKey)
iterator := store.Iterator(custodyServiceKeyPrefix, prefixEnd(custodyServiceKeyPrefix))
defer iterator.Close()
out := []types.CustodyService{}
for ; iterator.Valid(); iterator.Next() {
var s types.CustodyService
if err := json.Unmarshal(iterator.Value(), &s); err == nil {
out = append(out, s)
}
}
return out
}
// --- Lending primitive store -------------------------------------------------
var lendingKeyPrefix = []byte("lending/")
func lendingKey(loanID string) []byte {
return append(lendingKeyPrefix, []byte(loanID)...)
}
// GetLendingPrimitive loads a recorded lending primitive by loan-id.
func (k Keeper) GetLendingPrimitive(ctx sdk.Context, loanID string) (types.LendingPrimitive, bool) {
store := ctx.KVStore(k.storeKey)
bz := store.Get(lendingKey(loanID))
if bz == nil {
return types.LendingPrimitive{}, false
}
var l types.LendingPrimitive
if err := json.Unmarshal(bz, &l); err != nil {
return types.LendingPrimitive{}, false
}
return l, true
}
// SetLendingPrimitive persists a recorded lending primitive by loan-id.
func (k Keeper) SetLendingPrimitive(ctx sdk.Context, l types.LendingPrimitive) {
store := ctx.KVStore(k.storeKey)
bz, err := json.Marshal(l)
if err != nil {
panic(fmt.Sprintf("hub: marshal lending primitive %q: %v", l.LoanID, err))
}
store.Set(lendingKey(l.LoanID), bz)
}
// AllLendingPrimitives returns all recorded lending primitives.
func (k Keeper) AllLendingPrimitives(ctx sdk.Context) []types.LendingPrimitive {
store := ctx.KVStore(k.storeKey)
iterator := store.Iterator(lendingKeyPrefix, prefixEnd(lendingKeyPrefix))
defer iterator.Close()
out := []types.LendingPrimitive{}
for ; iterator.Valid(); iterator.Next() {
var l types.LendingPrimitive
if err := json.Unmarshal(iterator.Value(), &l); err == nil {
out = append(out, l)
}
}
return out
}
// --- Custody entry exported accessors (for simtest + handler helpers) --------
// GetCustodyEntry loads a CustodyEntry by assetID. Returns the entry and
// true if found, or zero value + false if not. Exported for simtest
// assertion (the custody store's getCustodyEntry is lowercase; this is the
// exported wrapper on the Keeper).
func (k Keeper) GetCustodyEntry(ctx sdk.Context, assetID string) (CustodyEntry, bool) {
return k.custody.getCustodyEntry(ctx, assetID)
}
// AllCustodyEntries returns all persisted CustodyEntry records (iteration
// helper, unordered). Exported for simtest assertion.
func (k Keeper) AllCustodyEntries(ctx sdk.Context) []CustodyEntry {
return k.custody.allCustodyEntries(ctx)
}
// --- Compliance attestation store --------------------------------------------
// The compliance attestation store is keyed by partner-id. The value is
// the latest attestation-ref (the RecordComplianceAttestation handler
// overwrites prior attestations for the same partner-id; the IsCompliant
// method reads this store). A-544 compliance-before-custody: the
// CustodyReleaseAsset handler consults IsCompliant(partnerID) via the
// ComplianceKeeper shim (the Keeper satisfies it) BEFORE the custody debit.
var complianceKeyPrefix = []byte("compliance/")
func complianceKey(partnerID string) []byte {
return append(complianceKeyPrefix, []byte(partnerID)...)
}
// GetComplianceAttestation loads the latest attestation-ref for a partner.
// Returns the attestation-ref and true if found, or "" + false if not.
func (k Keeper) GetComplianceAttestation(ctx sdk.Context, partnerID string) (string, bool) {
store := ctx.KVStore(k.storeKey)
bz := store.Get(complianceKey(partnerID))
if bz == nil {
return "", false
}
return string(bz), true
}
// SetComplianceAttestation persists the latest attestation-ref for a partner.
func (k Keeper) SetComplianceAttestation(ctx sdk.Context, partnerID, attestationRef string) {
store := ctx.KVStore(k.storeKey)
store.Set(complianceKey(partnerID), []byte(attestationRef))
}
// IsCompliant reports whether the named partner has a valid compliance
// attestation on record (i.e., a MsgRecordComplianceAttestation has been
// recorded against it). The CustodyReleaseAsset handler consults this
// BEFORE the custody debit (A-544 compliance-before-custody); a
// non-compliant partner REJECTS the release (the asset stays in custody).
//
// Implements types.ComplianceKeeper (the Keeper satisfies the
// ComplianceKeeper shim structurally — A-544; the handler passes the
// keeper as the ComplianceKeeper to itself).
func (k Keeper) IsCompliant(ctx interface{}, partnerID string) bool {
sdkCtx := unwrapCtx(ctx)
_, ok := k.GetComplianceAttestation(sdkCtx, partnerID)
return ok
}
// --- 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). Used for store.Iterator(start, prefixEnd(start))
// prefix scans. Mirrors x/partner/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
}