3c52aa1bb2
---ci--- project: oy phase: 4 milestone: v0.5 status: complete requirements: covered: [REQ-036] partial: [] ---/ci---
264 lines
10 KiB
Go
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
|
|
}
|