From 7775383b6e8f11ea1121f15c67cc669460bac825 Mon Sep 17 00:00:00 2001 From: Pablo Date: Thu, 19 Feb 2026 19:15:54 -0400 Subject: [PATCH 1/3] feat: new buyers filter index for orders --- cmd/rpc/README.md | 16 ++++++++- cmd/rpc/query.go | 15 +++++++- cmd/rpc/types.go | 1 + fsm/key.go | 11 ++++++ fsm/swap.go | 91 +++++++++++++++++++++++++++++++++++++++++++---- 5 files changed, 126 insertions(+), 8 deletions(-) diff --git a/cmd/rpc/README.md b/cmd/rpc/README.md index bf0fc1e4..81cc639e 100644 --- a/cmd/rpc/README.md +++ b/cmd/rpc/README.md @@ -2562,10 +2562,13 @@ $ curl -X POST localhost:50002/v1/query/order \ - **height**: `uint64` – the block height to read data from (optional: use 0 to read from the latest block) - **committee**: `uint64` – the unique identifier of the committee to filter by (optional: use 0 to get all committees) -- **sellersSendAddress**: `hex-string` – the seller address to filter orders by (optional: when provided, uses indexed lookup for efficient querying) +- **sellersSendAddress**: `hex-string` – the seller address to filter orders by (optional: uses indexed lookup for efficient querying) +- **buyerSendAddress**: `hex-string` – the buyer address to filter locked orders by (optional: uses indexed lookup for efficient querying) - **pageNumber**: `int` – the page number to retrieve (optional: starts at 1) - **perPage**: `int` – the number of orders per page (optional: defaults to system default) +**Note**: `sellersSendAddress` and `buyerSendAddress` are mutually exclusive filters. You cannot use both in the same request. + **Response**: - **pageNumber**: `int` - the current page number - **perPage**: `int` - the number of items per page @@ -2657,6 +2660,17 @@ $ curl -X POST localhost:50002/v1/query/orders \ }' ``` +**Example 5: Filter by buyerSendAddress with pagination (locked orders only)** +``` +$ curl -X POST localhost:50002/v1/query/orders \ + -H "Content-Type: application/json" \ + -d '{ + "buyerSendAddress": "aaac0b3d64c12c6f164545545b2ba2ab4d80deff", + "pageNumber": 1, + "perPage": 10 + }' +``` + ## Dex Batch **Route:** `/v1/query/dex-batch` **Description**: view the locked dex batch for a committee or all dex batches diff --git a/cmd/rpc/query.go b/cmd/rpc/query.go index 4fdb22b4..5875f2de 100644 --- a/cmd/rpc/query.go +++ b/cmd/rpc/query.go @@ -285,6 +285,10 @@ func (s *Server) Order(w http.ResponseWriter, r *http.Request, _ httprouter.Para func (s *Server) Orders(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { // Invoke helper with the HTTP request, response writer and an inline callback s.ordersParams(w, r, func(s *fsm.StateMachine, req *ordersRequest) (any, lib.ErrorI) { + // validate mutual exclusion: cannot filter by both seller and buyer address + if req.SellersSendAddress != "" && req.BuyerSendAddress != "" { + return nil, lib.NewError(lib.CodeInvalidArgument, lib.RPCModule, "cannot filter by both sellersSendAddress and buyerSendAddress") + } // convert seller address if provided var sellerAddr []byte if req.SellersSendAddress != "" { @@ -294,8 +298,17 @@ func (s *Server) Orders(w http.ResponseWriter, r *http.Request, _ httprouter.Par return nil, err } } + // convert buyer address if provided + var buyerAddr []byte + if req.BuyerSendAddress != "" { + var err lib.ErrorI + buyerAddr, err = lib.StringToBytes(req.BuyerSendAddress) + if err != nil { + return nil, err + } + } // use paginated query - return s.GetOrdersPaginated(sellerAddr, req.Committee, req.PageParams) + return s.GetOrdersPaginated(sellerAddr, buyerAddr, req.Committee, req.PageParams) }) } diff --git a/cmd/rpc/types.go b/cmd/rpc/types.go index de8f1531..91dae4dc 100644 --- a/cmd/rpc/types.go +++ b/cmd/rpc/types.go @@ -34,6 +34,7 @@ type orderRequest struct { type ordersRequest struct { Committee uint64 `json:"committee"` SellersSendAddress string `json:"sellersSendAddress"` + BuyerSendAddress string `json:"buyerSendAddress"` heightRequest lib.PageParams } diff --git a/fsm/key.go b/fsm/key.go index 5222e7a4..f5cad053 100644 --- a/fsm/key.go +++ b/fsm/key.go @@ -44,6 +44,7 @@ var ( retiredCommitteePrefix = []byte{14} // store key prefix for 'retired' (dead) committees dexPrefix = []byte{15} // store key prefix for 'dex' functionality orderBySellerPrefix = []byte{16} // store key prefix for 'sell orders' indexed by seller address + orderByBuyerPrefix = []byte{17} // store key prefix for 'sell orders' indexed by buyer address lockedBatchSegment = []byte{1} nextBatchSement = []byte{2} @@ -89,6 +90,16 @@ func OrderBySellerAndChainPrefix(seller []byte, chainId uint64) []byte { func KeyForOrderBySeller(seller []byte, chainId uint64, orderId []byte) []byte { return append(OrderBySellerAndChainPrefix(seller, chainId), lib.JoinLenPrefix(orderId)...) } + +func OrderByBuyerPrefix(buyer []byte) []byte { + return lib.JoinLenPrefix(orderByBuyerPrefix, buyer) +} +func OrderByBuyerAndChainPrefix(buyer []byte, chainId uint64) []byte { + return append(OrderByBuyerPrefix(buyer), lib.JoinLenPrefix(formatUint64(chainId))...) +} +func KeyForOrderByBuyer(buyer []byte, chainId uint64, orderId []byte) []byte { + return append(OrderByBuyerAndChainPrefix(buyer, chainId), lib.JoinLenPrefix(orderId)...) +} func KeyForUnstaking(height uint64, address crypto.AddressI) []byte { return append(UnstakingPrefix(height), lib.JoinLenPrefix(address.Bytes())...) } diff --git a/fsm/swap.go b/fsm/swap.go index 86d12a92..666d22e6 100644 --- a/fsm/swap.go +++ b/fsm/swap.go @@ -4,9 +4,10 @@ import ( "bytes" "encoding/binary" "encoding/json" + "sort" + "github.com/canopy-network/canopy/lib" "github.com/canopy-network/canopy/lib/crypto" - "sort" ) /* This file contains state machine changes related to 'token swapping' */ @@ -262,6 +263,21 @@ func (s *StateMachine) CloseOrder(orderId []byte, chainId uint64) (err lib.Error // SetOrder() sets the sell order in state func (s *StateMachine) SetOrder(order *lib.SellOrder, chainId uint64) (err lib.ErrorI) { + var existingOrder *lib.SellOrder + // check if order already exists to handle buyer index cleanup + existingOrder, err = s.GetOrder(order.Id, chainId) + if err != nil { + return + } + // if existing order has a buyer and it differs from the new order's buyer, clean up the old index + if existingOrder != nil && len(existingOrder.BuyerSendAddress) > 0 { + // remove old buyer index if buyer changed or was removed + if !bytes.Equal(existingOrder.BuyerSendAddress, order.BuyerSendAddress) { + if err = s.Delete(KeyForOrderByBuyer(existingOrder.BuyerSendAddress, chainId, order.Id)); err != nil { + return + } + } + } // convert the order into proto bytes protoBytes, err := s.marshalOrder(order) if err != nil { @@ -272,12 +288,21 @@ func (s *StateMachine) SetOrder(order *lib.SellOrder, chainId uint64) (err lib.E return } // set the secondary index for seller address lookup (value is empty, key is sufficient) - return s.Set(KeyForOrderBySeller(order.SellersSendAddress, chainId, order.Id), []byte{}) + if err = s.Set(KeyForOrderBySeller(order.SellersSendAddress, chainId, order.Id), []byte{}); err != nil { + return + } + // set the secondary index for buyer address lookup if buyer exists (locked order) + if len(order.BuyerSendAddress) > 0 { + if err = s.Set(KeyForOrderByBuyer(order.BuyerSendAddress, chainId, order.Id), []byte{}); err != nil { + return + } + } + return } // DeleteOrder() deletes an existing order in the order book for a committee in the state db func (s *StateMachine) DeleteOrder(orderId []byte, chainId uint64) (err lib.ErrorI) { - // get the order first to retrieve the seller address for index cleanup + // get the order first to retrieve addresses for index cleanup order, err := s.GetOrder(orderId, chainId) if err != nil { return @@ -286,8 +311,17 @@ func (s *StateMachine) DeleteOrder(orderId []byte, chainId uint64) (err lib.Erro if err = s.Delete(KeyForOrder(chainId, orderId)); err != nil { return } - // delete the secondary index entry - return s.Delete(KeyForOrderBySeller(order.SellersSendAddress, chainId, orderId)) + // delete the seller secondary index entry + if err = s.Delete(KeyForOrderBySeller(order.SellersSendAddress, chainId, orderId)); err != nil { + return + } + // delete the buyer secondary index entry if buyer exists + if len(order.BuyerSendAddress) > 0 { + if err = s.Delete(KeyForOrderByBuyer(order.BuyerSendAddress, chainId, orderId)); err != nil { + return + } + } + return } // GetOrder() gets the sell order from state @@ -472,8 +506,22 @@ func (s *StateMachine) parseOrderBySellerKey(key []byte) (chainId uint64, orderI return } +// parseOrderByBuyerKey() extracts chainId and orderId from an order-by-buyer index key +func (s *StateMachine) parseOrderByBuyerKey(key []byte) (chainId uint64, orderId []byte, err lib.ErrorI) { + // key format: orderByBuyerPrefix + buyer + chainId + orderId (all length-prefixed) + segments := lib.DecodeLengthPrefixed(key) + if len(segments) < 4 { + return 0, nil, ErrInvalidKey(key) + } + // segments[0] = prefix, segments[1] = buyer, segments[2] = chainId, segments[3] = orderId + chainId = binary.BigEndian.Uint64(segments[2]) + orderId = segments[3] + return +} + // GetOrdersPaginated() retrieves orders with optional filters and pagination -func (s *StateMachine) GetOrdersPaginated(seller []byte, chainId uint64, p lib.PageParams) (*lib.Page, lib.ErrorI) { +// Note: seller and buyer filters are mutually exclusive +func (s *StateMachine) GetOrdersPaginated(seller, buyer []byte, chainId uint64, p lib.PageParams) (*lib.Page, lib.ErrorI) { // create the page object page := lib.NewPage(p, "orders") results := &lib.SellOrders{} @@ -482,6 +530,10 @@ func (s *StateMachine) GetOrdersPaginated(seller []byte, chainId uint64, p lib.P // use seller index for efficient lookup return s.getOrdersBySellerPaginated(seller, chainId, page, results) } + if len(buyer) > 0 { + // use buyer index for efficient lookup + return s.getOrdersByBuyerPaginated(buyer, chainId, page, results) + } if chainId != 0 { // filter by chainId only - iterate orders for that chain return s.getOrdersByChainPaginated(chainId, page, results) @@ -517,6 +569,33 @@ func (s *StateMachine) getOrdersBySellerPaginated(seller []byte, chainId uint64, return page, err } +// getOrdersByBuyerPaginated() retrieves paginated orders using the buyer index +func (s *StateMachine) getOrdersByBuyerPaginated(buyer []byte, chainId uint64, page *lib.Page, results *lib.SellOrders) (*lib.Page, lib.ErrorI) { + // determine the prefix based on whether chainId filter is provided + var prefix []byte + if chainId == 0 { + prefix = OrderByBuyerPrefix(buyer) + } else { + prefix = OrderByBuyerAndChainPrefix(buyer, chainId) + } + // use the page Load function with the index prefix + err := page.Load(prefix, false, results, s, func(k, v []byte) lib.ErrorI { + // extract chainId and orderId from the index key + cId, orderId, e := s.parseOrderByBuyerKey(k) + if e != nil { + return e + } + // get the actual order from the primary store + order, e := s.GetOrder(orderId, cId) + if e != nil { + return e + } + *results = append(*results, order) + return nil + }) + return page, err +} + // getOrdersByChainPaginated() retrieves paginated orders for a specific chain func (s *StateMachine) getOrdersByChainPaginated(chainId uint64, page *lib.Page, results *lib.SellOrders) (*lib.Page, lib.ErrorI) { // use the page Load function with the order book prefix From 9d4195a0bc4b88b1123256f9613dee629e3fcfae Mon Sep 17 00:00:00 2001 From: Pablo Date: Thu, 19 Feb 2026 19:27:13 -0400 Subject: [PATCH 2/3] chore: ignore not found --- fsm/swap.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/fsm/swap.go b/fsm/swap.go index 666d22e6..62cdcbf8 100644 --- a/fsm/swap.go +++ b/fsm/swap.go @@ -263,11 +263,11 @@ func (s *StateMachine) CloseOrder(orderId []byte, chainId uint64) (err lib.Error // SetOrder() sets the sell order in state func (s *StateMachine) SetOrder(order *lib.SellOrder, chainId uint64) (err lib.ErrorI) { - var existingOrder *lib.SellOrder // check if order already exists to handle buyer index cleanup - existingOrder, err = s.GetOrder(order.Id, chainId) - if err != nil { - return + existingOrder, getErr := s.GetOrder(order.Id, chainId) + // if error is anything other than "not found", return it + if getErr != nil && getErr.Code() != lib.CodeOrderNotFound { + return getErr } // if existing order has a buyer and it differs from the new order's buyer, clean up the old index if existingOrder != nil && len(existingOrder.BuyerSendAddress) > 0 { From 514849b5feb11253bd8ffd7320792b441b5e1246 Mon Sep 17 00:00:00 2001 From: Pablo Date: Thu, 19 Feb 2026 19:44:31 -0400 Subject: [PATCH 3/3] chore: move cleanup and get better the readme --- cmd/rpc/README.md | 4 ++-- fsm/swap.go | 38 ++++++++++++++++++++++++-------------- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/cmd/rpc/README.md b/cmd/rpc/README.md index 81cc639e..8a4ec267 100644 --- a/cmd/rpc/README.md +++ b/cmd/rpc/README.md @@ -2562,8 +2562,8 @@ $ curl -X POST localhost:50002/v1/query/order \ - **height**: `uint64` – the block height to read data from (optional: use 0 to read from the latest block) - **committee**: `uint64` – the unique identifier of the committee to filter by (optional: use 0 to get all committees) -- **sellersSendAddress**: `hex-string` – the seller address to filter orders by (optional: uses indexed lookup for efficient querying) -- **buyerSendAddress**: `hex-string` – the buyer address to filter locked orders by (optional: uses indexed lookup for efficient querying) +- **sellersSendAddress**: `hex-string` – the seller address to filter orders by (optional: use "" to get all seller addresses) +- **buyerSendAddress**: `hex-string` – the buyer address to filter locked orders by (optional: use "" to get all buyer addresses) - **pageNumber**: `int` – the page number to retrieve (optional: starts at 1) - **perPage**: `int` – the number of orders per page (optional: defaults to system default) diff --git a/fsm/swap.go b/fsm/swap.go index 62cdcbf8..f97dbdd8 100644 --- a/fsm/swap.go +++ b/fsm/swap.go @@ -263,20 +263,9 @@ func (s *StateMachine) CloseOrder(orderId []byte, chainId uint64) (err lib.Error // SetOrder() sets the sell order in state func (s *StateMachine) SetOrder(order *lib.SellOrder, chainId uint64) (err lib.ErrorI) { - // check if order already exists to handle buyer index cleanup - existingOrder, getErr := s.GetOrder(order.Id, chainId) - // if error is anything other than "not found", return it - if getErr != nil && getErr.Code() != lib.CodeOrderNotFound { - return getErr - } - // if existing order has a buyer and it differs from the new order's buyer, clean up the old index - if existingOrder != nil && len(existingOrder.BuyerSendAddress) > 0 { - // remove old buyer index if buyer changed or was removed - if !bytes.Equal(existingOrder.BuyerSendAddress, order.BuyerSendAddress) { - if err = s.Delete(KeyForOrderByBuyer(existingOrder.BuyerSendAddress, chainId, order.Id)); err != nil { - return - } - } + // clean up stale buyer index if buyer changed or was removed + if err = s.cleanupStaleBuyerIndex(order, chainId); err != nil { + return } // convert the order into proto bytes protoBytes, err := s.marshalOrder(order) @@ -300,6 +289,27 @@ func (s *StateMachine) SetOrder(order *lib.SellOrder, chainId uint64) (err lib.E return } +// cleanupStaleBuyerIndex() removes the old buyer index entry if the buyer changed or was removed +func (s *StateMachine) cleanupStaleBuyerIndex(order *lib.SellOrder, chainId uint64) lib.ErrorI { + // check if order already exists + existingOrder, err := s.GetOrder(order.Id, chainId) + // if order not found, nothing to clean up + if err != nil && err.Code() == lib.CodeOrderNotFound { + return nil + } + // if other error, return it + if err != nil { + return err + } + // if existing order has a buyer and it differs from the new order's buyer, clean up the old index + if existingOrder != nil && len(existingOrder.BuyerSendAddress) > 0 { + if !bytes.Equal(existingOrder.BuyerSendAddress, order.BuyerSendAddress) { + return s.Delete(KeyForOrderByBuyer(existingOrder.BuyerSendAddress, chainId, order.Id)) + } + } + return nil +} + // DeleteOrder() deletes an existing order in the order book for a committee in the state db func (s *StateMachine) DeleteOrder(orderId []byte, chainId uint64) (err lib.ErrorI) { // get the order first to retrieve addresses for index cleanup