diff --git a/x/exchange/keeper/orders.go b/x/exchange/keeper/orders.go new file mode 100644 index 0000000000..a85ed3b7e6 --- /dev/null +++ b/x/exchange/keeper/orders.go @@ -0,0 +1,711 @@ +package keeper + +import ( + "errors" + "fmt" + "strings" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + db "github.com/tendermint/tm-db" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/query" + + "github.com/provenance-io/provenance/x/exchange" +) + +// getLastOrderID gets the id of the last order created. +func getLastOrderID(store sdk.KVStore) uint64 { + key := MakeKeyLastOrderID() + value := store.Get(key) + rv, _ := uint64FromBz(value) + return rv +} + +// setLastOrderID sets the id of the last order created. +func setLastOrderID(store sdk.KVStore, orderID uint64) { + key := MakeKeyLastOrderID() + value := uint64Bz(orderID) + store.Set(key, value) +} + +// nextOrderID finds the next available order id, updates the last order id +// store entry, and returns the unused id it found. +func nextOrderID(store sdk.KVStore) uint64 { + orderID := getLastOrderID(store) + 1 + setLastOrderID(store, orderID) + return orderID +} + +// getOrderStoreKeyValue creates the store key and value representing the provided order. +func (k Keeper) getOrderStoreKeyValue(order exchange.Order) ([]byte, []byte, error) { + // 200 chosen to hopefully be more than what's needed for 99% of orders. + // The largest one I could make was 753 bytes for a bid order with all coins having 128 + // character denoms and 31 digits in the amounts, a 32 byte address, and max market id. + // But the more realistic ones were 130 to 160, so 200 seems like a nice size to start with. + key := MakeKeyOrder(order.OrderId) + value := make([]byte, 1, 200) + value[0] = order.GetOrderTypeByte() + switch value[0] { + case OrderKeyTypeAsk: + ask := order.GetAskOrder() + data, err := k.cdc.Marshal(ask) + if err != nil { + return nil, nil, fmt.Errorf("error marshaling ask order: %w", err) + } + value = append(value, data...) + return key, value, nil + case OrderKeyTypeBid: + bid := order.GetBidOrder() + data, err := k.cdc.Marshal(bid) + if err != nil { + return nil, nil, fmt.Errorf("error marshaling bid order: %w", err) + } + value = append(value, data...) + return key, value, nil + default: + // GetOrderTypeByte panics if it's an unknown order type. + // If we get here, it knew of a new order type that doesn't have a case in here. + // So we panic here instead of returning an error because it's a programming error to address. + panic(fmt.Sprintf("SetOrder missing case for order type byte %#x, order type %T", value[0], order.GetOrder())) + } +} + +// parseOrderStoreValue converts an order store value back into an order. +func (k Keeper) parseOrderStoreValue(orderID uint64, value []byte) (*exchange.Order, error) { + if len(value) == 0 { + return nil, nil + } + typeByte, data := value[0], value[1:] + switch typeByte { + case OrderKeyTypeAsk: + var ask exchange.AskOrder + err := k.cdc.Unmarshal(data, &ask) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal ask order: %w", err) + } + return exchange.NewOrder(orderID).WithAsk(&ask), nil + case OrderKeyTypeBid: + var bid exchange.BidOrder + err := k.cdc.Unmarshal(data, &bid) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal bid order: %w", err) + } + return exchange.NewOrder(orderID).WithBid(&bid), nil + default: + // Returning an error here instead of panicking because, at this point, we don't really + // have a trail of what happened to get this unknown entry here. And if we panic, it makes + // it harder to identify and clean up bad entries once we figure that out. + return nil, fmt.Errorf("unknown type byte %#x", typeByte) + } +} + +// parseOrderStoreKeyValue parses the provided key and value into an exchange.Order. +// The key's leading type byte is optional, only the last 8 bytes of it are used. +func (k Keeper) parseOrderStoreKeyValue(key, value []byte) (*exchange.Order, error) { + if len(key) < 8 { + return nil, fmt.Errorf("invalid order store key %v: length expected to be at least 8", key) + } + orderID, _ := uint64FromBz(key[len(key)-8:]) + return k.parseOrderStoreValue(orderID, value) +} + +// createIndexEntries creates all the key/value index entries for an order. +func createIndexEntries(order exchange.Order) []sdk.KVPair { + marketID := order.GetMarketID() + orderID := order.GetOrderID() + orderTypeByte := order.GetOrderTypeByte() + owner := order.GetOwner() + addr := sdk.MustAccAddressFromBech32(owner) + assets := order.GetAssets() + + return []sdk.KVPair{ + { + Key: MakeIndexKeyMarketToOrder(marketID, orderID), + Value: []byte{orderTypeByte}, + }, + { + Key: MakeIndexKeyAddressToOrder(addr, orderID), + Value: []byte{orderTypeByte}, + }, + { + Key: MakeIndexKeyAssetToOrder(assets.Denom, orderID), + Value: []byte{orderTypeByte}, + }, + } +} + +// getOrderFromStore looks up an order from the store. Returns nil, nil if the order does not exist. +func (k Keeper) getOrderFromStore(store sdk.KVStore, orderID uint64) (*exchange.Order, error) { + key := MakeKeyOrder(orderID) + value := store.Get(key) + if len(value) == 0 { + return nil, nil + } + rv, err := k.parseOrderStoreValue(orderID, value) + if err != nil { + return nil, fmt.Errorf("failed to read order %d: %w", orderID, err) + } + return rv, nil +} + +// setOrderInStore writes an order to the store (along with all its indexes). +func (k Keeper) setOrderInStore(store sdk.KVStore, order exchange.Order) error { + key, value, err := k.getOrderStoreKeyValue(order) + if err != nil { + return fmt.Errorf("failed to create order %d store key/value: %w", order.GetOrderID(), err) + } + isUpdate := store.Has(key) + store.Set(key, value) + if !isUpdate { + indexEntries := createIndexEntries(order) + for _, entry := range indexEntries { + store.Set(entry.Key, entry.Value) + } + // It is assumed that these index entries cannot change over the life of an order. + // The only change that is allowed to an order is the assets (due to partial fulfillment). + // Partial fulfillment is only allowed when there's a single asset type (denom), though. + // That's why no attempt is made in here to update index entries when the order already exists. + } + return nil +} + +// deleteAndDeIndexOrder deletes an order from the store along with its indexes. +func deleteAndDeIndexOrder(store sdk.KVStore, order exchange.Order) { + key := MakeKeyOrder(order.OrderId) + store.Delete(key) + indexEntries := createIndexEntries(order) + for _, entry := range indexEntries { + store.Delete(entry.Key) + } +} + +// iterateOrderIndex iterates over a -to-order index with keys that have the provided prefixBz. +// The callback takes in the order id and order type byte and should return whether to stop iterating. +func (k Keeper) iterateOrderIndex(ctx sdk.Context, prefixBz []byte, cb func(orderID uint64, orderTypeByte byte) bool) { + k.iterate(ctx, prefixBz, func(key, value []byte) bool { + if len(value) == 0 { + return false + } + orderID, ok := ParseIndexKeySuffixOrderID(key) + return ok && cb(orderID, value[0]) + }) +} + +// getPageOfOrdersFromIndex gets a page of orders using a -to-order index. +func (k Keeper) getPageOfOrdersFromIndex( + prefixStore sdk.KVStore, + pageReq *query.PageRequest, + orderType string, + afterOrderID uint64, +) (*query.PageResponse, []*exchange.Order, error) { + var orderTypeByte byte + filterByType := false + if len(orderType) > 0 { + ot := strings.ToLower(orderType) + // only look at the first 3 chars to handle stuff like "asks" or "bidOrders" too. + if len(ot) > 3 { + ot = ot[:3] + } + switch ot { + case exchange.OrderTypeAsk: + orderTypeByte = OrderKeyTypeAsk + case exchange.OrderTypeBid: + orderTypeByte = OrderKeyTypeBid + default: + return nil, nil, status.Errorf(codes.InvalidArgument, "unknown order type %q", orderType) + } + filterByType = true + } + + var orders []*exchange.Order + accumulator := func(key []byte, value []byte, accumulate bool) (bool, error) { + // If filtering by type, but the order type isn't known, or is something else, this entry doesn't count, move on. + if filterByType && (len(value) == 0 || value[0] != orderTypeByte) { + return false, nil + } + // If we can't get the order id from the key, just pretend like it doesn't exist. + orderID, ok := ParseIndexKeySuffixOrderID(key) + if !ok { + return false, nil + } + if accumulate { + // Only add it to the result if we can read it. This might result in fewer results than the limit, + // but at least one bad entry won't block others by causing the whole thing to return an error. + order, err := k.parseOrderStoreValue(orderID, value) + if err == nil && order != nil { + orders = append(orders, order) + } + } + return true, nil + } + pageResp, err := filteredPaginateAfterOrder(prefixStore, pageReq, afterOrderID, accumulator) + + return pageResp, orders, err +} + +// filteredPaginateAfterOrder is similar to query.FilteredPaginate except +// allows limiting the iterator to only entries after a certain order id. +// afterOrderID is exclusive, i.e. if it's 2, this will go over all order ids that are 3 or greater. +// +// FilteredPaginate does pagination of all the results in the PrefixStore based on the +// provided PageRequest. onResult should be used to do actual unmarshaling and filter the results. +// If key is provided, the pagination uses the optimized querying. +// If offset is used, the pagination uses lazy filtering i.e., searches through all the records. +// The accumulate parameter represents if the response is valid based on the offset given. +// It will be false for the results (filtered) < offset and true for `offset > accumulate <= end`. +// When accumulate is set to true the current result should be appended to the result set returned +// to the client. +func filteredPaginateAfterOrder( + prefixStore sdk.KVStore, + pageRequest *query.PageRequest, + afterOrderID uint64, + onResult func(key []byte, value []byte, accumulate bool) (bool, error), +) (*query.PageResponse, error) { + // if the PageRequest is nil, use default PageRequest + if pageRequest == nil { + pageRequest = &query.PageRequest{} + } + + offset := pageRequest.Offset + key := pageRequest.Key + limit := pageRequest.Limit + countTotal := pageRequest.CountTotal + reverse := pageRequest.Reverse + + if offset > 0 && key != nil { + return nil, fmt.Errorf("invalid request, either offset or key is expected, got both") + } + + if limit == 0 { + limit = query.DefaultLimit + + // count total results when the limit is zero/not supplied + countTotal = true + } + + if len(key) != 0 { + // This line is changed from the query.FilteredPaginate version. + iterator := getOrderIterator(prefixStore, key, reverse, afterOrderID) + defer iterator.Close() + + var ( + numHits uint64 + nextKey []byte + ) + + for ; iterator.Valid(); iterator.Next() { + if numHits == limit { + nextKey = iterator.Key() + break + } + + if iterator.Error() != nil { + return nil, iterator.Error() + } + + hit, err := onResult(iterator.Key(), iterator.Value(), true) + if err != nil { + return nil, err + } + + if hit { + numHits++ + } + } + + return &query.PageResponse{ + NextKey: nextKey, + }, nil + } + + // This line is changed from the query.FilteredPaginate version. + iterator := getOrderIterator(prefixStore, nil, reverse, afterOrderID) + defer iterator.Close() + + end := offset + limit + + var ( + numHits uint64 + nextKey []byte + ) + + for ; iterator.Valid(); iterator.Next() { + if iterator.Error() != nil { + return nil, iterator.Error() + } + + accumulate := numHits >= offset && numHits < end + hit, err := onResult(iterator.Key(), iterator.Value(), accumulate) + if err != nil { + return nil, err + } + + if hit { + numHits++ + } + + if numHits == end+1 { + if nextKey == nil { + nextKey = iterator.Key() + } + + if !countTotal { + break + } + } + } + + res := &query.PageResponse{NextKey: nextKey} + if countTotal { + res.Total = numHits + } + + return res, nil +} + +// getOrderIterator is similar to query.getIterator but allows limiting it to only entries after a certain order id. +func getOrderIterator(prefixStore sdk.KVStore, start []byte, reverse bool, afterOrderID uint64) db.Iterator { + if reverse { + var end []byte + if start != nil { + itr := prefixStore.Iterator(start, nil) + defer itr.Close() + if itr.Valid() { + itr.Next() + end = itr.Key() + } + } + // If an afterOrderID was given, use the key of it as the first key. + // This orderIDKey is a change from the query.getIterator version. + var orderIDKey []byte + if afterOrderID != 0 { + // TODO[1658]: Write a unit test that hits this in order to make sure I don't need to afterOrderID++. + orderIDKey = uint64Bz(afterOrderID) + } + return prefixStore.ReverseIterator(orderIDKey, end) + } + + // If a start ("next key") was given, use that as the first key. + // Otherwise, if an afterOrderID was given, use the key of the next order id as the first key. + // This if block is a change from the query.getIterator version. + if len(start) == 0 && afterOrderID != 0 { + if afterOrderID != 18_446_744_073_709_551_615 { + afterOrderID++ + } + start = uint64Bz(afterOrderID) + } + return prefixStore.Iterator(start, nil) +} + +// validateMarketIsAcceptingOrders makes sure the market exists and is accepting orders. +func validateMarketIsAcceptingOrders(store sdk.KVStore, marketID uint32) error { + if err := validateMarketExists(store, marketID); err != nil { + return err + } + if !isMarketActive(store, marketID) { + return fmt.Errorf("market %d is not accepting orders", marketID) + } + return nil +} + +// validateUserCanCreateAsk makes sure the user can create an ask order in the given market. +func (k Keeper) validateUserCanCreateAsk(ctx sdk.Context, marketID uint32, seller sdk.AccAddress) error { + if !k.CanCreateAsk(ctx, marketID, seller) { + return fmt.Errorf("account %s is not allowed to create ask orders in market %d", seller, marketID) + } + return nil +} + +// validateUserCanCreateBid makes sure the user can create a bid order in the given market. +func (k Keeper) validateUserCanCreateBid(ctx sdk.Context, marketID uint32, buyer sdk.AccAddress) error { + if !k.CanCreateBid(ctx, marketID, buyer) { + return fmt.Errorf("account %s is not allowed to create bid orders in market %d", buyer, marketID) + } + return nil +} + +// validateCreateAskFees makes sure the fees are okay for creating an ask order. +func validateCreateAskFees(store sdk.KVStore, marketID uint32, creationFee *sdk.Coin, settlementFlatFee *sdk.Coin) error { + if err := validateCreateAskFlatFee(store, marketID, creationFee); err != nil { + return err + } + return validateSellerSettlementFlatFee(store, marketID, settlementFlatFee) +} + +// validateCreateBidFees makes sure the fees are okay for creating a bid order. +func validateCreateBidFees(store sdk.KVStore, marketID uint32, creationFee *sdk.Coin, price sdk.Coin, settlementFees sdk.Coins) error { + if err := validateCreateBidFlatFee(store, marketID, creationFee); err != nil { + return err + } + return validateBuyerSettlementFee(store, marketID, price, settlementFees) +} + +// getAskOrders gets orders from the store, making sure they're ask orders in the given market +// and do not have the same seller as the provided buyer. If the buyer isn't yet known, just provide "" for it. +func (k Keeper) getAskOrders(store sdk.KVStore, marketID uint32, orderIDs []uint64, buyer string) ([]*exchange.Order, error) { + var errs []error + orders := make([]*exchange.Order, 0, len(orderIDs)) + + for _, orderID := range orderIDs { + order, oerr := k.getOrderFromStore(store, orderID) + if oerr != nil { + errs = append(errs, oerr) + continue + } + if order == nil { + errs = append(errs, fmt.Errorf("order %d not found", orderID)) + continue + } + if !order.IsAskOrder() { + errs = append(errs, fmt.Errorf("order %d is type %s: expected ask", orderID, order.GetOrderType())) + continue + } + + askOrder := order.GetAskOrder() + orderMarketID := askOrder.MarketId + seller := askOrder.Seller + + if orderMarketID != marketID { + errs = append(errs, fmt.Errorf("order %d market id %d does not equal requested market id %d", orderID, orderMarketID, marketID)) + continue + } + if seller == buyer { + errs = append(errs, fmt.Errorf("order %d has the same seller %s as the requested buyer", orderID, seller)) + continue + } + + orders = append(orders, order) + } + + return orders, errors.Join(errs...) +} + +// getBidOrders gets orders from the store, making sure they're bid orders in the given market +// and do not have the same buyer as the provided seller. If the seller isn't yet known, just provide "" for it. +func (k Keeper) getBidOrders(store sdk.KVStore, marketID uint32, orderIDs []uint64, seller string) ([]*exchange.Order, error) { + var errs []error + orders := make([]*exchange.Order, 0, len(orderIDs)) + + for _, orderID := range orderIDs { + order, oerr := k.getOrderFromStore(store, orderID) + if oerr != nil { + errs = append(errs, oerr) + continue + } + if order == nil { + errs = append(errs, fmt.Errorf("order %d not found", orderID)) + continue + } + if !order.IsBidOrder() { + errs = append(errs, fmt.Errorf("order %d is type %s: expected bid", orderID, order.GetOrderType())) + continue + } + + bidOrder := order.GetBidOrder() + orderMarketID := bidOrder.MarketId + buyer := bidOrder.Buyer + + if orderMarketID != marketID { + errs = append(errs, fmt.Errorf("order %d market id %d does not equal requested market id %d", orderID, orderMarketID, marketID)) + continue + } + if buyer == seller { + errs = append(errs, fmt.Errorf("order %d has the same buyer %s as the requested seller", orderID, buyer)) + continue + } + + orders = append(orders, order) + } + + return orders, errors.Join(errs...) +} + +// placeHoldOnOrder places a hold on an order's funds in the owner's account. +func (k Keeper) placeHoldOnOrder(ctx sdk.Context, order exchange.OrderI) error { + orderID := order.GetOrderID() + orderType := order.GetOrderType() + owner := order.GetOwner() + ownerAddr, err := sdk.AccAddressFromBech32(owner) + if err != nil { + return fmt.Errorf("invalid %s order %d owner %q: %w", orderType, orderID, owner, err) + } + toHold := order.GetHoldAmount() + err = k.holdKeeper.AddHold(ctx, ownerAddr, toHold, fmt.Sprintf("x/exchange: order %d", orderID)) + if err != nil { + return fmt.Errorf("error placing hold for %s order %d: %w", orderType, orderID, err) + } + return nil +} + +// releaseHoldOnOrder releases a hold that was placed on an order's funds in the owner's account. +func (k Keeper) releaseHoldOnOrder(ctx sdk.Context, order exchange.OrderI) error { + orderID := order.GetOrderID() + orderType := order.GetOrderType() + owner := order.GetOwner() + ownerAddr, err := sdk.AccAddressFromBech32(owner) + if err != nil { + return fmt.Errorf("invalid %s order %d owner %q: %w", orderType, orderID, owner, err) + } + held := order.GetHoldAmount() + err = k.holdKeeper.ReleaseHold(ctx, ownerAddr, held) + if err != nil { + return fmt.Errorf("error releasing hold for %s order %d: %w", orderType, orderID, err) + } + return nil +} + +// GetOrder gets an order. Returns nil, nil if the order does not exist. +func (k Keeper) GetOrder(ctx sdk.Context, orderID uint64) (*exchange.Order, error) { + return k.getOrderFromStore(k.getStore(ctx), orderID) +} + +// CreateAskOrder creates an ask order, collects the creation fee, and places all needed holds. +func (k Keeper) CreateAskOrder(ctx sdk.Context, askOrder exchange.AskOrder, creationFee *sdk.Coin) (uint64, error) { + if err := askOrder.Validate(); err != nil { + return 0, err + } + + store := k.getStore(ctx) + marketID := askOrder.MarketId + + if err := validateMarketIsAcceptingOrders(store, marketID); err != nil { + return 0, err + } + seller := sdk.MustAccAddressFromBech32(askOrder.Seller) + if err := k.validateUserCanCreateAsk(ctx, marketID, seller); err != nil { + return 0, err + } + if err := validateCreateAskFees(store, marketID, creationFee, askOrder.SellerSettlementFlatFee); err != nil { + return 0, err + } + if err := validateAskPrice(store, marketID, askOrder.Price, askOrder.SellerSettlementFlatFee); err != nil { + return 0, err + } + + if creationFee != nil { + err := k.CollectFee(ctx, marketID, seller, sdk.Coins{*creationFee}) + if err != nil { + return 0, fmt.Errorf("error collecting ask order creation fee: %w", err) + } + } + + orderID := nextOrderID(store) + order := exchange.NewOrder(orderID).WithAsk(&askOrder) + if err := k.setOrderInStore(store, *order); err != nil { + return 0, fmt.Errorf("error storing ask order: %w", err) + } + + if err := k.placeHoldOnOrder(ctx, order); err != nil { + return 0, err + } + + k.emitEvent(ctx, exchange.NewEventOrderCreated(order)) + return orderID, nil +} + +// CreateBidOrder creates a bid order, collects the creation fee, and places all needed holds. +func (k Keeper) CreateBidOrder(ctx sdk.Context, bidOrder exchange.BidOrder, creationFee *sdk.Coin) (uint64, error) { + if err := bidOrder.Validate(); err != nil { + return 0, err + } + + store := k.getStore(ctx) + marketID := bidOrder.MarketId + + if err := validateMarketIsAcceptingOrders(store, marketID); err != nil { + return 0, err + } + buyer := sdk.MustAccAddressFromBech32(bidOrder.Buyer) + if err := k.validateUserCanCreateBid(ctx, marketID, buyer); err != nil { + return 0, err + } + if err := validateCreateBidFees(store, marketID, creationFee, bidOrder.Price, bidOrder.BuyerSettlementFees); err != nil { + return 0, err + } + + if creationFee != nil { + err := k.CollectFee(ctx, marketID, buyer, sdk.Coins{*creationFee}) + if err != nil { + return 0, fmt.Errorf("error collecting bid order creation fee: %w", err) + } + } + + orderID := nextOrderID(store) + order := exchange.NewOrder(orderID).WithBid(&bidOrder) + if err := k.setOrderInStore(store, *order); err != nil { + return 0, fmt.Errorf("error storing bid order: %w", err) + } + + if err := k.placeHoldOnOrder(ctx, order); err != nil { + return 0, err + } + + k.emitEvent(ctx, exchange.NewEventOrderCreated(order)) + return orderID, nil +} + +// CancelOrder releases an order's held funds and deletes it. +func (k Keeper) CancelOrder(ctx sdk.Context, orderID uint64, signer string) error { + order, err := k.GetOrder(ctx, orderID) + if err != nil { + return err + } + if order == nil { + return fmt.Errorf("order %d does not exist", orderID) + } + + orderOwner := order.GetOwner() + if signer != orderOwner && !k.CanCancelOrdersForMarket(ctx, order.GetMarketID(), signer) { + return fmt.Errorf("account %s does not have permission to cancel order %d", signer, orderID) + } + + signerAddr := sdk.MustAccAddressFromBech32(signer) + orderOwnerAddr := sdk.MustAccAddressFromBech32(orderOwner) + heldAmount := order.GetHoldAmount() + err = k.holdKeeper.ReleaseHold(ctx, orderOwnerAddr, heldAmount) + if err != nil { + return fmt.Errorf("unable to release hold on order %d funds: %w", orderID, err) + } + + deleteAndDeIndexOrder(k.getStore(ctx), *order) + k.emitEvent(ctx, exchange.NewEventOrderCancelled(orderID, signerAddr)) + + return nil +} + +// IterateOrders iterates over all orders. An error is returned if there was a problem +// reading an entry along the way. Such a problem does not interrupt iteration. +// The callback takes in the order and should return whether to stop iterating. +func (k Keeper) IterateOrders(ctx sdk.Context, cb func(order *exchange.Order) bool) error { + var errs []error + k.iterate(ctx, GetKeyPrefixOrder(), func(key, value []byte) bool { + order, err := k.parseOrderStoreKeyValue(key, value) + if err != nil { + errs = append(errs, err) + return false + } + return cb(order) + }) + return errors.Join(errs...) +} + +// IterateMarketOrders iterates over all orders for a market. +// The callback takes in the order id and order type byte and should return whether to stop iterating. +func (k Keeper) IterateMarketOrders(ctx sdk.Context, marketID uint32, cb func(orderID uint64, orderTypeByte byte) bool) { + k.iterateOrderIndex(ctx, GetIndexKeyPrefixMarketToOrder(marketID), cb) +} + +// IterateAddressOrders iterates over all orders for an address. +// The callback takes in the order id and order type byte and should return whether to stop iterating. +func (k Keeper) IterateAddressOrders(ctx sdk.Context, addr sdk.AccAddress, cb func(orderID uint64, orderTypeByte byte) bool) { + k.iterateOrderIndex(ctx, GetIndexKeyPrefixAddressToOrder(addr), cb) +} + +// IterateAssetOrders iterates over all orders for a given asset denom. +// The callback takes in the order id and order type byte and should return whether to stop iterating. +func (k Keeper) IterateAssetOrders(ctx sdk.Context, assetDenom string, cb func(orderID uint64, orderTypeByte byte) bool) { + k.iterateOrderIndex(ctx, GetIndexKeyPrefixAssetToOrder(assetDenom), cb) +} diff --git a/x/ibchooks/marker_hooks.go b/x/ibchooks/marker_hooks.go index d01aa14c38..b3a868144d 100644 --- a/x/ibchooks/marker_hooks.go +++ b/x/ibchooks/marker_hooks.go @@ -96,6 +96,7 @@ func (h MarkerHooks) createNewIbcMarker(ctx sdktypes.Context, data transfertypes allowForceTransfer, []string{}, ) + h.adjustSupply(ctx, marker) if err = ResetMarkerAccessGrants(transferAuthAddrs, marker); err != nil { return err } @@ -105,6 +106,12 @@ func (h MarkerHooks) createNewIbcMarker(ctx sdktypes.Context, data transfertypes return h.addDenomMetaData(ctx, packet, ibcKeeper, ibcDenom, data) } +// adjustSupply updates marker account's supply, this ensures the supply is correct for ibc coins that may exist before a marker is created +func (h MarkerHooks) adjustSupply(ctx sdktypes.Context, marker *markertypes.MarkerAccount) { + currentSupply := h.MarkerKeeper.CurrentCirculation(ctx, marker) + marker.SetSupply(marker.GetSupply().Add(sdktypes.NewCoin(marker.Denom, currentSupply))) +} + // addDenomMetaData adds denom metadata for ibc token func (h MarkerHooks) addDenomMetaData(ctx sdktypes.Context, packet exported.PacketI, ibcKeeper *ibckeeper.Keeper, ibcDenom string, data transfertypes.FungibleTokenPacketData) error { chainID := h.GetChainID(ctx, packet, ibcKeeper) diff --git a/x/ibchooks/marker_hooks_test.go b/x/ibchooks/marker_hooks_test.go index bfc1f88916..f991f353cb 100644 --- a/x/ibchooks/marker_hooks_test.go +++ b/x/ibchooks/marker_hooks_test.go @@ -82,6 +82,7 @@ func (suite *MarkerHooksTestSuite) makeMockPacket(denom, receiver, memo string, } func (suite *MarkerHooksTestSuite) TestAddUpdateMarker() { + suite.chainA.GetProvenanceApp().BankKeeper.MintCoins(suite.chainA.GetContext(), markertypes.CoinPoolName, sdk.NewCoins(sdk.NewInt64Coin("ibc/F7466BCD642C14163B3E67D5B4401FD2B77271C3225FDE0C57ADD61B8046253D", 100))) address1 := sdk.AccAddress("address1") address2 := sdk.AccAddress("address2") markerHooks := ibchooks.NewMarkerHooks(&suite.chainA.GetProvenanceApp().MarkerKeeper) @@ -92,6 +93,7 @@ func (suite *MarkerHooksTestSuite) TestAddUpdateMarker() { expErr string expIbcDenom string expTransAuths []sdk.AccAddress + expSupply sdk.Coin }{ { name: "successfully process with empty memo", @@ -99,6 +101,7 @@ func (suite *MarkerHooksTestSuite) TestAddUpdateMarker() { memo: "", expErr: "", expIbcDenom: "ibc/F3F4565153F3DD64470F075D6D6B1CB183F06EB55B287CCD0D3506277A03DE8E", + expSupply: sdk.NewInt64Coin("ibc/F3F4565153F3DD64470F075D6D6B1CB183F06EB55B287CCD0D3506277A03DE8E", 1), }, { name: "successfully process with non json memo", @@ -106,6 +109,7 @@ func (suite *MarkerHooksTestSuite) TestAddUpdateMarker() { memo: "55 burger 55 fries...", expErr: "", expIbcDenom: "ibc/F3F4565153F3DD64470F075D6D6B1CB183F06EB55B287CCD0D3506277A03DE8E", + expSupply: sdk.NewInt64Coin("ibc/F3F4565153F3DD64470F075D6D6B1CB183F06EB55B287CCD0D3506277A03DE8E", 1), }, { name: "successfully process with non json marker part memo", @@ -113,6 +117,7 @@ func (suite *MarkerHooksTestSuite) TestAddUpdateMarker() { memo: `{"marker":{random},"wasm":{"contract":"%1234","msg":{"echo":{"msg":"test"}}}}`, expErr: "", expIbcDenom: "ibc/F3F4565153F3DD64470F075D6D6B1CB183F06EB55B287CCD0D3506277A03DE8E", + expSupply: sdk.NewInt64Coin("ibc/F3F4565153F3DD64470F075D6D6B1CB183F06EB55B287CCD0D3506277A03DE8E", 1), }, { name: "successfully process with transfer auths", @@ -121,6 +126,7 @@ func (suite *MarkerHooksTestSuite) TestAddUpdateMarker() { expErr: "", expIbcDenom: "ibc/1B3A5773661E8A6B9F6BB407979B5933C2FA792DF24ED2A40B028C90277B0C22", expTransAuths: []sdk.AccAddress{address1, address2}, + expSupply: sdk.NewInt64Coin("ibc/1B3A5773661E8A6B9F6BB407979B5933C2FA792DF24ED2A40B028C90277B0C22", 1), }, { name: "fail invalid json", @@ -128,6 +134,14 @@ func (suite *MarkerHooksTestSuite) TestAddUpdateMarker() { memo: fmt.Sprintf(`{"marker":{"transfer-auths":"%s"}}`, address1.String()), expErr: "json: cannot unmarshal string into Go struct field MarkerPayload.transfer-auths of type []string", }, + { + name: "successfully process with ibc denom that existed before marker correctly adjust supply", + denom: "ibcdenombeforemiddleware", + memo: "", + expErr: "", + expIbcDenom: "ibc/F7466BCD642C14163B3E67D5B4401FD2B77271C3225FDE0C57ADD61B8046253D", + expSupply: sdk.NewInt64Coin("ibc/F7466BCD642C14163B3E67D5B4401FD2B77271C3225FDE0C57ADD61B8046253D", 101), + }, } for _, tc := range testCases { suite.T().Run(tc.name, func(t *testing.T) { @@ -140,6 +154,7 @@ func (suite *MarkerHooksTestSuite) TestAddUpdateMarker() { marker, err := suite.chainA.GetProvenanceApp().MarkerKeeper.GetMarkerByDenom(suite.chainA.GetContext(), tc.expIbcDenom) require.NoError(t, err, "GetMarkerByDenom should find "+tc.expErr) assert.Equal(t, tc.expIbcDenom, marker.GetDenom(), "Marker Denom should be ibc denom") + assert.Equal(t, tc.expSupply, marker.GetSupply(), "Marker Supply should match expected") metadata, found := suite.chainA.GetProvenanceApp().BankKeeper.GetDenomMetaData(suite.chainA.GetContext(), tc.expIbcDenom) require.True(t, found, "GetDenomMetaData() not found for "+tc.expErr) assert.Equal(t, marker.GetDenom(), metadata.Base, "Metadata Base should equal marker denom") diff --git a/x/marker/keeper/keeper.go b/x/marker/keeper/keeper.go index bfdd3738dd..d96692bd96 100644 --- a/x/marker/keeper/keeper.go +++ b/x/marker/keeper/keeper.go @@ -324,3 +324,7 @@ func (k Keeper) GetReqAttrBypassAddrs() []sdk.AccAddress { func (k Keeper) IsReqAttrBypassAddr(addr sdk.AccAddress) bool { return k.reqAttrBypassAddrs.Has(addr) } + +func (k Keeper) HasBankSupply(ctx sdk.Context, denom string) { + k.bankKeeper.GetSupply(ctx, denom) +}