Skip to content

Commit

Permalink
Feature/get swaps (#161)
Browse files Browse the repository at this point in the history
* support open and resolved GetSwap

* parse startAt endAt query params with default vals

* getSwapFills

* add fill struct hybrid of order & fragment

* support func for getSwapFills

* code clean

* Reduce log level (#162)

---------

Co-authored-by: Luke Rogerson <[email protected]>
  • Loading branch information
uv-orbs and Luke-Rogerson authored Mar 25, 2024
1 parent 1d395b5 commit cd20569
Show file tree
Hide file tree
Showing 17 changed files with 232 additions and 16 deletions.
2 changes: 1 addition & 1 deletion data/redisrepo/get_open_swaps.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func (r *redisRepository) GetOpenSwaps(ctx context.Context) ([]models.Swap, erro
for _, key := range keys {
id := Key2UUID(ctx, key)
if id != nil {
swap, err := r.GetSwap(ctx, *id)
swap, err := r.GetSwap(ctx, *id, true)
if err != nil {
logctx.Error(ctx, "failed to get swap", logger.String("swapId", id.String()))
} else {
Expand Down
14 changes: 14 additions & 0 deletions data/redisrepo/resolve_swap.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/orbs-network/order-book/models"
"github.com/orbs-network/order-book/utils/logger"
"github.com/orbs-network/order-book/utils/logger/logctx"
"github.com/redis/go-redis/v9"
)

// ProcessCompletedSwapOrders stores the updated swap orders and removes the swap from Redis. It should be called after a swap is completed.
Expand Down Expand Up @@ -39,3 +40,16 @@ func (r *redisRepository) StoreUserResolvedSwap(ctx context.Context, userId uuid
key := CreateUserResolvedSwapsKey(userId)
return AddVal2Set(ctx, r.client, key, swap.Id.String())
}

func (r *redisRepository) GetUserResolvedSwapIds(ctx context.Context, userId uuid.UUID) ([]string, error) {
key := CreateUserResolvedSwapsKey(userId)
res, err := r.client.SMembers(ctx, key).Result()
if err != nil {
if err == redis.Nil {
return nil, models.ErrNotFound
}
logctx.Error(ctx, "could not get user swaps", logger.Error(err), logger.String("user_id", userId.String()))
return nil, err
}
return res, nil
}
2 changes: 1 addition & 1 deletion data/redisrepo/store_new_pending_swap.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
// StoreNewPendingSwap stores a new pending swap in order for its status (pending/complete) to be checked later
func (r *redisRepository) StoreNewPendingSwap(ctx context.Context, p models.SwapTx) error {
// confirm swapID is valid
swap, err := r.GetSwap(ctx, p.SwapId)
swap, err := r.GetSwap(ctx, p.SwapId, true)
if err != nil {
if err == models.ErrNotFound {
logctx.Warn(ctx, "no swap found by that ID", logger.Error(err), logger.String("swapId", p.SwapId.String()), logger.String("txHash", p.TxHash))
Expand Down
5 changes: 4 additions & 1 deletion data/redisrepo/taker.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,11 @@ func (r *redisRepository) StoreSwap(ctx context.Context, swapId uuid.UUID, frags
return nil
}

func (r *redisRepository) GetSwap(ctx context.Context, swapId uuid.UUID) (*models.Swap, error) {
func (r *redisRepository) GetSwap(ctx context.Context, swapId uuid.UUID, open bool) (*models.Swap, error) {
swapKey := CreateOpenSwapKey(swapId)
if !open {
swapKey = CreateResolvedSwapKey(swapId)
}

swapJson, err := r.client.Get(ctx, swapKey).Result()
// Error
Expand Down
4 changes: 2 additions & 2 deletions data/redisrepo/taker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func TestRedisRepository_GetSwap(t *testing.T) {

mock.ExpectGet(CreateOpenSwapKey(swapId)).SetVal(swapJson[0])

swap, err := repo.GetSwap(ctx, swapId)
swap, err := repo.GetSwap(ctx, swapId, true)
assert.NoError(t, err)
assert.Len(t, swap.Frags, 2, "Should have 2 orders in the swap")
})
Expand All @@ -40,7 +40,7 @@ func TestRedisRepository_GetSwap(t *testing.T) {

mock.ExpectSMembers(CreateOpenSwapKey(swapId)).SetErr(assert.AnError)

_, err := repo.GetSwap(ctx, swapId)
_, err := repo.GetSwap(ctx, swapId, true)
assert.Equal(t, models.ErrUnexpectedError, err)
})
}
Expand Down
4 changes: 3 additions & 1 deletion data/store/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ type OrderBookStore interface {
GetMinAsk(ctx context.Context, symbol models.Symbol) models.OrderIter
GetMaxBid(ctx context.Context, symbol models.Symbol) models.OrderIter
// taker side
GetSwap(ctx context.Context, swapId uuid.UUID) (*models.Swap, error)
GetSwap(ctx context.Context, swapId uuid.UUID, open bool) (*models.Swap, error)
StoreSwap(ctx context.Context, swapId uuid.UUID, frags []models.OrderFrag) error
RemoveSwap(ctx context.Context, swapId uuid.UUID) error
GetOpenSwaps(ctx context.Context) ([]models.Swap, error)
Expand All @@ -70,6 +70,8 @@ type OrderBookStore interface {
ResolveSwap(ctx context.Context, swap models.Swap) error
// save swapId in a set of the userId:resolvedSwap key
StoreUserResolvedSwap(ctx context.Context, userId uuid.UUID, swap models.Swap) error
GetUserResolvedSwapIds(ctx context.Context, userId uuid.UUID) ([]string, error)

// utils
EnumSubKeysOf(ctx context.Context, key string) ([]string, error)

Expand Down
6 changes: 5 additions & 1 deletion mocks/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ func (m *MockOrderBookStore) StoreUserByPublicKey(ctx context.Context, user mode
return m.Error
}

func (m *MockOrderBookStore) GetSwap(ctx context.Context, swapId uuid.UUID) (*models.Swap, error) {
func (m *MockOrderBookStore) GetSwap(ctx context.Context, swapId uuid.UUID, open bool) (*models.Swap, error) {
if m.Error != nil {
return nil, m.Error
}
Expand Down Expand Up @@ -183,6 +183,10 @@ func (m *MockOrderBookStore) StoreUserResolvedSwap(ctx context.Context, userId u
return m.Error
}

func (m *MockOrderBookStore) GetUserResolvedSwapIds(ctx context.Context, userId uuid.UUID) ([]string, error) {
return []string{"111", "222"}, m.Error
}

func (m *MockOrderBookStore) EnumSubKeysOf(tx context.Context, key string) ([]string, error) {
return []string{key + "111", key + "222"}, m.Error
}
Expand Down
5 changes: 5 additions & 0 deletions mocks/transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package mocks

import (
"context"
"time"

"github.com/google/uuid"
"github.com/orbs-network/order-book/models"
Expand Down Expand Up @@ -66,6 +67,10 @@ func (m *MockOrderBookService) GetFilledOrdersForUser(ctx context.Context, userI
return m.Orders, len(m.Orders), m.Error
}

func (m *MockOrderBookService) GetSwapFills(ctx context.Context, userId uuid.UUID, symbol models.Symbol, startAt, endAt time.Time) ([]models.Fill, error) {
return nil, m.Error
}

func (m *MockOrderBookService) CancelOrdersForUser(ctx context.Context, userId uuid.UUID, symbol models.Symbol) (orderIds []uuid.UUID, err error) {
var ids []uuid.UUID
for _, order := range m.Orders {
Expand Down
1 change: 1 addition & 0 deletions models/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ var ErrUnexpectedSizePending = errors.New("unexpected sizePending")
var ErrIterFail = errors.New("failed to get bid/ask iterator from store")
var ErrTokenNotsupported = errors.New("token is not supported")
var ErrMinOutAmount = errors.New("OutAmount is less than MinOutAmount")
var ErrMaxRecExceeded = errors.New("max number of records exceeded, narrow down the range")

// store generic errors
var ErrValAlreadyInSet = errors.New("the value is already a member of the set")
16 changes: 16 additions & 0 deletions models/swap.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,22 @@ type OrderFrag struct {
InSize decimal.Decimal
}

// Individual Fill
// get SwapFills for user
type Fill struct {
OrderId uuid.UUID `json:"orderId"`
ClientOId uuid.UUID `json:"clientOrderId"`
SwapId uuid.UUID `json:"swapId"`
Side Side `json:"side"`
Symbol Symbol `json:"symbol"`
Timestamp time.Time `json:"timestamp"`
Price decimal.Decimal `json:"price"`
Size decimal.Decimal `json:"size"`
OrderSize decimal.Decimal `json:"orderSize"`

Cancelled bool `json:"cancelled"`
}

func (f *OrderFrag) ToMap() map[string]string {
return map[string]string{
"inSize": f.InSize.String(),
Expand Down
2 changes: 1 addition & 1 deletion scripts/redis-repo/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ func createSwaps() {
log.Fatalf("error storing swap: %v", err)
}

swap, err := repository.GetSwap(ctx, swapId)
swap, err := repository.GetSwap(ctx, swapId, true)
if err != nil {
log.Fatalf("error getting swap: %v", err)
}
Expand Down
74 changes: 74 additions & 0 deletions service/get_orders_for_user.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package service
import (
"context"
"fmt"
"time"

"github.com/google/uuid"
"github.com/orbs-network/order-book/models"
Expand Down Expand Up @@ -37,3 +38,76 @@ func (s *Service) GetFilledOrdersForUser(ctx context.Context, userId uuid.UUID)

return orders, totalOrders, nil
}

const MAX_FILLS = 256

// get all fills from all swaps of the user in a given time range
func (s *Service) GetSwapFills(ctx context.Context, userId uuid.UUID, symbol models.Symbol, startAt, endAt time.Time) ([]models.Fill, error) {
logctx.Debug(ctx, "getting open orders for user", logger.String("user_id", userId.String()))

swapIds, err := s.orderBookStore.GetUserResolvedSwapIds(ctx, userId)
if err != nil {
logctx.Error(ctx, "error getting user resolve swapIds", logger.Error(err), logger.String("user_id", userId.String()))
return nil, err
}

if len(swapIds) == 0 {
logctx.Warn(ctx, "user has no resolved swaps", logger.Error(err), logger.String("user_id", userId.String()))
return []models.Fill{}, models.ErrNotFound
}

fills := []models.Fill{}

// fetch swaps
for _, id := range swapIds {
uid, err := uuid.Parse(id)
if err != nil {
logctx.Error(ctx, "failed to parse swapID", logger.Error(err), logger.String("user_id", userId.String()), logger.String("swap_id", id))
return nil, err
}
// get resolved swaps
swap, err := s.orderBookStore.GetSwap(ctx, uid, false)
if err != nil {
logctx.Error(ctx, "error getting a swap", logger.Error(err), logger.String("user_id", userId.String()), logger.String("swap_id", id))
return nil, err
}
// check if swap is in time range
if swap.Resolved.After(startAt) && swap.Resolved.Before(endAt) {
// iterate through fragments
for _, frag := range swap.Frags {
// create fill res
fill := models.Fill{
OrderId: frag.OrderId,
SwapId: swap.Id,
Symbol: symbol,
Timestamp: swap.Resolved,
}

// get order
order, err := s.orderBookStore.FindOrderById(ctx, frag.OrderId, false)
if err != nil {
logctx.Warn(ctx, "error getting a order", logger.Error(err), logger.String("user_id", userId.String()), logger.String("order_id", id))
} else {
// fill size is dependent on the side of the order
fill.Size = frag.InSize
if order.Side == models.SELL {
fill.Size = frag.OutSize
}
// enrich fill data
fill.Side = order.Side
fill.ClientOId = order.ClientOId
fill.Price = order.Price
fill.OrderSize = order.Size
}

fills = append(fills, fill)

if len(fills) >= MAX_FILLS {
return nil, models.ErrMaxRecExceeded
}
}
}
}

return fills, nil
}
2 changes: 2 additions & 0 deletions service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package service
import (
"context"
"errors"
"time"

"github.com/google/uuid"
"github.com/orbs-network/order-book/data/store"
Expand All @@ -22,6 +23,7 @@ type OrderBookService interface {
GetSymbols(ctx context.Context) ([]models.Symbol, error)
GetOpenOrdersForUser(ctx context.Context, userId uuid.UUID) (orders []models.Order, totalOrders int, err error)
GetFilledOrdersForUser(ctx context.Context, userId uuid.UUID) (orders []models.Order, totalOrders int, err error)
GetSwapFills(ctx context.Context, userId uuid.UUID, symbol models.Symbol, startAt, endAt time.Time) ([]models.Fill, error)
// Subscribe to order updates for a specific user
SubscribeUserOrders(ctx context.Context, userId uuid.UUID) (chan []byte, error)

Expand Down
4 changes: 2 additions & 2 deletions service/taker.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ func (s *Service) SwapStarted(ctx context.Context, swapId uuid.UUID, txHash stri
func (s *Service) AbortSwap(ctx context.Context, swapId uuid.UUID) error {
logctx.Debug(ctx, "AbortSwap", logger.String("swapId", swapId.String()))
// get swap from store
swap, err := s.orderBookStore.GetSwap(ctx, swapId)
swap, err := s.orderBookStore.GetSwap(ctx, swapId, true)
if err != nil {
logctx.Warn(ctx, "GetSwap Failed", logger.Error(err))
return err
Expand Down Expand Up @@ -187,7 +187,7 @@ func (s *Service) FillSwap(ctx context.Context, swapId uuid.UUID) error {
logctx.Debug(ctx, "FillSwap", logger.String("swapId", swapId.String()))

// get swap from store
swap, err := s.orderBookStore.GetSwap(ctx, swapId)
swap, err := s.orderBookStore.GetSwap(ctx, swapId, true)
if err != nil {
logctx.Warn(ctx, "GetSwap Failed", logger.Error(err))
return err
Expand Down
39 changes: 37 additions & 2 deletions transport/rest/get_orders_for_user.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,16 +72,51 @@ func (h *Handler) GetFilledOrdersForUser(w http.ResponseWriter, r *http.Request)

jsonData, err := json.Marshal(res)
if err != nil {
logctx.Error(r.Context(), "failed to marshal response", logger.Error(err), logger.String("orderId", user.Id.String()))
logctx.Error(r.Context(), "failed to marshal response", logger.Error(err), logger.String("userId", user.Id.String()))
restutils.WriteJSONError(ctx, w, http.StatusInternalServerError, "Error getting orders. Try again later")
return
}

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if _, err := w.Write(jsonData); err != nil {
logctx.Error(r.Context(), "failed to write response", logger.Error(err), logger.String("orderId", user.Id.String()))
logctx.Error(r.Context(), "failed to write response", logger.Error(err), logger.String("userId", user.Id.String()))
restutils.WriteJSONError(ctx, w, http.StatusInternalServerError, "Error getting orders. Try again later")
}

}

func (h *Handler) GetSwapFills(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
user := utils.GetUserCtx(ctx)
if user == nil {
logctx.Error(ctx, "user should be in context")
restutils.WriteJSONError(ctx, w, http.StatusUnauthorized, "User not found")
return
}

logctx.Debug(r.Context(), "user trying to get their filled orders", logger.String("userId", user.Id.String()))

symbol := models.Symbol("MATIC-USDC")
startAt, endAt := getStartEndTime(r)
fills, err := h.svc.GetSwapFills(r.Context(), user.Id, symbol, startAt, endAt)
if err != nil {
logctx.Error(r.Context(), "failed GetSwapFills", logger.Error(err), logger.String("userId", user.Id.String()))
if err == models.ErrMaxRecExceeded {
// narrow down the time range, 256 exceeded
restutils.WriteJSONError(ctx, w, http.StatusRequestEntityTooLarge, err.Error())
} else {
restutils.WriteJSONError(ctx, w, http.StatusInternalServerError, "Error getting swaps. Try again later")
}
}
jsonData, err := json.Marshal(fills)
if err != nil {
logctx.Error(r.Context(), "failed to Marshal orders", logger.Error(err), logger.String("userId", user.Id.String()))
restutils.WriteJSONError(ctx, w, http.StatusInternalServerError, "Error Marshalling swap orders.")
}

if _, err := w.Write(jsonData); err != nil {
logctx.Error(r.Context(), "failed to write response", logger.Error(err), logger.String("orderId", user.Id.String()))
restutils.WriteJSONError(ctx, w, http.StatusInternalServerError, "Error write response.")
}
}
12 changes: 8 additions & 4 deletions transport/rest/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,10 @@ func (h *Handler) initMakerRoutes(getUserByApiKey middleware.GetUserByApiKeyFunc
mmApi.HandleFunc("/order/{orderId}", h.GetOrderById).Methods("GET")
// Get all open orders for a user
mmApi.HandleFunc("/orders", middleware.PaginationMiddleware(h.GetOpenOrdersForUser)).Methods("GET")
// Get all filled orders for a user
mmApi.HandleFunc("/fills", middleware.PaginationMiddleware(h.GetFilledOrdersForUser)).Methods("GET")
// Legacy way to Get all filled orders for a user
mmApi.HandleFunc("/fills-page", middleware.PaginationMiddleware(h.GetFilledOrdersForUser)).Methods("GET")
// correct way to Get fills using data in swaps
mmApi.HandleFunc("/fills", h.GetSwapFills).Methods("GET")
// Get all symbols
mmApi.HandleFunc("/symbols", h.GetSymbols).Methods("GET")
// Get market depth
Expand All @@ -118,6 +120,8 @@ func (h *Handler) initMakerRoutes(getUserByApiKey middleware.GetUserByApiKeyFunc
mmApi.HandleFunc("/order/{orderId}", h.CancelOrderByOrderId).Methods("DELETE")
// Cancel all orders for a user
mmApi.HandleFunc("/orders", h.CancelOrdersForUser).Methods("DELETE")

// ------- WEBSOCKET -------
// Subscribe to order events (websocket)
mmApi.HandleFunc("/ws/orders", websocket.WebSocketOrderHandler(h.svc, getUserByApiKey)).Methods("GET")
}
Expand All @@ -129,8 +133,8 @@ func (h *Handler) initTakerRoutes(getUserByApiKey middleware.GetUserByApiKeyFunc
takerApi := h.Router.PathPrefix("/taker/v1").Subrouter()

// disabled!
//middlewareValidUser := middleware.ValidateUserMiddleware(getUserByApiKey)
//takerApi.Use(middlewareValidUser) disable for now
middlewareValidUser := middleware.ValidateUserMiddleware(getUserByApiKey)
takerApi.Use(middlewareValidUser)

// IN: InAmount, InToken, OutToken or InTokenAddress, OutTokenAddress
// OUT: CURRENT potential outAmount
Expand Down
Loading

0 comments on commit cd20569

Please sign in to comment.