package keeper import ( "bytes" "fmt" "sort" "time" "cosmossdk.io/collections" "cosmossdk.io/collections/indexes" storetypes "cosmossdk.io/core/store" errorsmod "cosmossdk.io/errors" "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/codec/legacy" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" auth "github.com/cosmos/cosmos-sdk/x/auth/keeper" bank "github.com/cosmos/cosmos-sdk/x/bank/keeper" "github.com/gibson042/canonicaljson-go" "github.com/ipld/go-ipld-prime" "github.com/ipld/go-ipld-prime/codec/dagjson" "github.com/ipld/go-ipld-prime/node/basicnode" auctionkeeper "git.vdb.to/cerc-io/laconic2d/x/auction/keeper" bondkeeper "git.vdb.to/cerc-io/laconic2d/x/bond/keeper" registrytypes "git.vdb.to/cerc-io/laconic2d/x/registry" "git.vdb.to/cerc-io/laconic2d/x/registry/helpers" ) // TODO: Add required methods type RecordsIndexes struct { BondId *indexes.Multi[string, string, registrytypes.Record] } func (b RecordsIndexes) IndexesList() []collections.Index[string, registrytypes.Record] { return []collections.Index[string, registrytypes.Record]{b.BondId} } func newRecordIndexes(sb *collections.SchemaBuilder) RecordsIndexes { return RecordsIndexes{ BondId: indexes.NewMulti( sb, registrytypes.BondIdIndexPrefix, "records_by_bond_id", collections.StringKey, collections.StringKey, func(_ string, v registrytypes.Record) (string, error) { return v.BondId, nil }, ), } } type Keeper struct { cdc codec.BinaryCodec accountKeeper auth.AccountKeeper bankKeeper bank.Keeper recordKeeper RecordKeeper bondKeeper bondkeeper.Keeper auctionKeeper auctionkeeper.Keeper // state management Schema collections.Schema Params collections.Item[registrytypes.Params] Records *collections.IndexedMap[string, registrytypes.Record, RecordsIndexes] } // NewKeeper creates a new Keeper instance func NewKeeper( cdc codec.BinaryCodec, storeService storetypes.KVStoreService, accountKeeper auth.AccountKeeper, bankKeeper bank.Keeper, recordKeeper RecordKeeper, bondKeeper bondkeeper.Keeper, auctionKeeper auctionkeeper.Keeper, ) Keeper { sb := collections.NewSchemaBuilder(storeService) k := Keeper{ cdc: cdc, accountKeeper: accountKeeper, bankKeeper: bankKeeper, recordKeeper: recordKeeper, bondKeeper: bondKeeper, auctionKeeper: auctionKeeper, Params: collections.NewItem(sb, registrytypes.ParamsPrefix, "params", codec.CollValue[registrytypes.Params](cdc)), Records: collections.NewIndexedMap(sb, registrytypes.RecordsPrefix, "records", collections.StringKey, codec.CollValue[registrytypes.Record](cdc), newRecordIndexes(sb)), } schema, err := sb.Build() if err != nil { panic(err) } k.Schema = schema return k } // HasRecord - checks if a record by the given id exists. func (k Keeper) HasRecord(ctx sdk.Context, id string) (bool, error) { has, err := k.Records.Has(ctx, id) if err != nil { return false, err } return has, nil } // GetRecord - gets a record from the store. func (k Keeper) GetRecord(ctx sdk.Context, id string) (registrytypes.Record, error) { record, err := k.Records.Get(ctx, id) if err != nil { return registrytypes.Record{}, err } return record, nil } // ListRecords - get all records. func (k Keeper) ListRecords(ctx sdk.Context) ([]registrytypes.Record, error) { iter, err := k.Records.Iterate(ctx, nil) if err != nil { return nil, err } // TODO: Check if required // decodeRecordNames(store, &record) return iter.Values() } // RecordsFromAttributes gets a list of records whose attributes match all provided values func (k Keeper) RecordsFromAttributes(ctx sdk.Context, attributes []*registrytypes.QueryRecordsRequest_KeyValueInput, all bool) ([]registrytypes.Record, error) { panic("unimplemented") } func (k Keeper) GetRecordExpiryQueue(ctx sdk.Context) []*registrytypes.ExpiryQueueRecord { panic("unimplemented") } // PutRecord - saves a record to the store. func (k Keeper) SaveRecord(ctx sdk.Context, record registrytypes.Record) error { return k.Records.Set(ctx, record.Id, record) // TODO // k.updateBlockChangeSetForRecord(ctx, record.Id) } // ProcessSetRecord creates a record. func (k Keeper) SetRecord(ctx sdk.Context, msg registrytypes.MsgSetRecord) (*registrytypes.ReadableRecord, error) { payload := msg.Payload.ToReadablePayload() record := registrytypes.ReadableRecord{Attributes: payload.RecordAttributes, BondId: msg.BondId} // Check signatures. resourceSignBytes, _ := record.GetSignBytes() cid, err := record.GetCid() if err != nil { return nil, errorsmod.Wrap(sdkerrors.ErrInvalidRequest, "Invalid record JSON") } record.Id = cid has, err := k.HasRecord(ctx, record.Id) if err != nil { return nil, err } if has { // Immutable record already exists. No-op. return &record, nil } record.Owners = []string{} for _, sig := range payload.Signatures { pubKey, err := legacy.PubKeyFromBytes(helpers.BytesFromBase64(sig.PubKey)) if err != nil { return nil, errorsmod.Wrap(sdkerrors.ErrUnauthorized, fmt.Sprint("Error decoding pubKey from bytes: ", err)) } sigOK := pubKey.VerifySignature(resourceSignBytes, helpers.BytesFromBase64(sig.Sig)) if !sigOK { return nil, errorsmod.Wrap(sdkerrors.ErrUnauthorized, fmt.Sprint("Signature mismatch: ", sig.PubKey)) } record.Owners = append(record.Owners, pubKey.Address().String()) } // Sort owners list. sort.Strings(record.Owners) sdkErr := k.processRecord(ctx, &record, false) if sdkErr != nil { return nil, sdkErr } return &record, nil } func (k Keeper) processRecord(ctx sdk.Context, record *registrytypes.ReadableRecord, isRenewal bool) error { params, err := k.GetParams(ctx) if err != nil { return err } rent := params.RecordRent if err = k.bondKeeper.TransferCoinsToModuleAccount( ctx, record.BondId, registrytypes.RecordRentModuleAccountName, sdk.NewCoins(rent), ); err != nil { return err } record.CreateTime = ctx.BlockHeader().Time.Format(time.RFC3339) record.ExpiryTime = ctx.BlockHeader().Time.Add(params.RecordRentDuration).Format(time.RFC3339) record.Deleted = false recordObj, err := record.ToRecordObj() if err != nil { return err } // Save record in store. if err = k.SaveRecord(ctx, recordObj); err != nil { return err } // TODO look up/validate record type here if err := k.processAttributes(ctx, record.Attributes, record.Id, ""); err != nil { return err } // TODO // expiryTimeKey := GetAttributesIndexKey(ExpiryTimeAttributeName, []byte(record.ExpiryTime)) // if err := k.SetAttributeMapping(ctx, expiryTimeKey, record.ID); err != nil { // return err // } // k.InsertRecordExpiryQueue(ctx, recordObj) return nil } func (k Keeper) processAttributes(ctx sdk.Context, attrs registrytypes.AttributeMap, id string, prefix string) error { np := basicnode.Prototype.Map nb := np.NewBuilder() encAttrs, err := canonicaljson.Marshal(attrs) if err != nil { return err } if len(attrs) == 0 { encAttrs = []byte("{}") } err = dagjson.Decode(nb, bytes.NewReader(encAttrs)) if err != nil { return fmt.Errorf("failed to decode attributes: %w", err) } n := nb.Build() if n.Kind() != ipld.Kind_Map { return fmt.Errorf("record attributes must be a map, not %T", n.Kind()) } return k.processAttributeMap(ctx, n, id, prefix) } func (k Keeper) processAttributeMap(ctx sdk.Context, n ipld.Node, id string, prefix string) error { for it := n.MapIterator(); !it.Done(); { //nolint:misspell keynode, valuenode, err := it.Next() if err != nil { return err } key, err := keynode.AsString() if err != nil { return err } if valuenode.Kind() == ipld.Kind_Map { err := k.processAttributeMap(ctx, valuenode, id, key) if err != nil { return err } } else { var buf bytes.Buffer if err := dagjson.Encode(valuenode, &buf); err != nil { return err } // TODO // value := buf.Bytes() // indexKey := GetAttributesIndexKey(prefix+key, value) // if err := k.SetAttributeMapping(ctx, indexKey, id); err != nil { // return err // } } } return nil } // GetModuleBalances gets the registry module account(s) balances. func (k Keeper) GetModuleBalances(ctx sdk.Context) []*registrytypes.AccountBalance { panic("unimplemented") }