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 }