diff --git a/domain/mocks/orderbook_grpc_client_mock.go b/domain/mocks/orderbook_grpc_client_mock.go index e708007d..ac4b1d02 100644 --- a/domain/mocks/orderbook_grpc_client_mock.go +++ b/domain/mocks/orderbook_grpc_client_mock.go @@ -19,6 +19,11 @@ type OrderbookGRPCClientMock struct { FetchTicksCb func(ctx context.Context, chunkSize int, contractAddress string, tickIDs []int64) ([]orderbookdomain.Tick, error) } +func (o *OrderbookGRPCClientMock) WithGetOrdersByTickCb(orders orderbookdomain.Orders, err error) { + o.GetOrdersByTickCb = func(ctx context.Context, contractAddress string, tick int64) (orderbookdomain.Orders, error) { + return orders, err + } +} func (o *OrderbookGRPCClientMock) GetOrdersByTick(ctx context.Context, contractAddress string, tick int64) (orderbookdomain.Orders, error) { if o.GetOrdersByTickCb != nil { return o.GetOrdersByTickCb(ctx, contractAddress, tick) diff --git a/domain/mocks/orderbook_repository_mock.go b/domain/mocks/orderbook_repository_mock.go index ea087913..a72210e9 100644 --- a/domain/mocks/orderbook_repository_mock.go +++ b/domain/mocks/orderbook_repository_mock.go @@ -23,6 +23,12 @@ func (m *OrderbookRepositoryMock) StoreTicks(poolID uint64, ticksMap map[int64]o panic("StoreTicks not implemented") } +func (m *OrderbookRepositoryMock) WithGetAllTicksFunc(ticks map[int64]orderbookdomain.OrderbookTick, ok bool) { + m.GetAllTicksFunc = func(poolID uint64) (map[int64]orderbookdomain.OrderbookTick, bool) { + return ticks, ok + } +} + // GetAllTicks implements OrderBookRepository. func (m *OrderbookRepositoryMock) GetAllTicks(poolID uint64) (map[int64]orderbookdomain.OrderbookTick, bool) { if m.GetAllTicksFunc != nil { diff --git a/domain/mocks/orderbook_usecase_mock.go b/domain/mocks/orderbook_usecase_mock.go index 0f4065a1..0f9e2283 100644 --- a/domain/mocks/orderbook_usecase_mock.go +++ b/domain/mocks/orderbook_usecase_mock.go @@ -47,6 +47,11 @@ func (m *OrderbookUsecaseMock) GetActiveOrdersStream(ctx context.Context, addres } panic("unimplemented") } +func (m *OrderbookUsecaseMock) WithCreateFormattedLimitOrder(order orderbookdomain.LimitOrder, err error) { + m.CreateFormattedLimitOrderFunc = func(domain.CanonicalOrderBooksResult, orderbookdomain.Order) (orderbookdomain.LimitOrder, error) { + return order, err + } +} func (m *OrderbookUsecaseMock) CreateFormattedLimitOrder(orderbook domain.CanonicalOrderBooksResult, order orderbookdomain.Order) (orderbookdomain.LimitOrder, error) { if m.CreateFormattedLimitOrderFunc != nil { diff --git a/ingest/usecase/plugins/orderbook/claimbot/export_test.go b/ingest/usecase/plugins/orderbook/claimbot/export_test.go index 3ddfc269..f59d5eb9 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/export_test.go +++ b/ingest/usecase/plugins/orderbook/claimbot/export_test.go @@ -3,15 +3,37 @@ package claimbot import ( "context" + "github.com/osmosis-labs/osmosis/osmomath" "github.com/osmosis-labs/sqs/delivery/grpc" + "github.com/osmosis-labs/sqs/domain" authtypes "github.com/osmosis-labs/sqs/domain/cosmos/auth/types" sqstx "github.com/osmosis-labs/sqs/domain/cosmos/tx" "github.com/osmosis-labs/sqs/domain/keyring" + "github.com/osmosis-labs/sqs/domain/mvc" orderbookdomain "github.com/osmosis-labs/sqs/domain/orderbook" + orderbookgrpcclientdomain "github.com/osmosis-labs/sqs/domain/orderbook/grpcclient" + "github.com/osmosis-labs/sqs/log" sdk "github.com/cosmos/cosmos-sdk/types" ) +// Order is order alias data structure for testing purposes. +type Order = order + +// ProcessOrderbooksAndGetClaimableOrders is test wrapper for processOrderbooksAndGetClaimableOrders. +// This function is exported for testing purposes. +func ProcessOrderbooksAndGetClaimableOrders( + ctx context.Context, + fillThreshold osmomath.Dec, + orderbooks []domain.CanonicalOrderBooksResult, + orderbookRepository orderbookdomain.OrderBookRepository, + orderBookClient orderbookgrpcclientdomain.OrderBookClient, + orderbookusecase mvc.OrderBookUsecase, + logger log.Logger, +) []Order { + return processOrderbooksAndGetClaimableOrders(ctx, fillThreshold, orderbooks, orderbookRepository, orderBookClient, orderbookusecase, logger) +} + // buildTxFunc is a function signature for buildTx. // This type is used only for testing purposes. type BuildTx = buildTxFunc @@ -32,7 +54,7 @@ func SetSendTx(fn sendTxFunc) { sendTx = fn } -// SendBatchClaimTx prepares and sends a batch claim transaction to the blockchain. +// SendBatchClaimTx a test wrapper for sendBatchClaimTx. // This function is used only for testing purposes. func SendBatchClaimTx( ctx context.Context, @@ -45,13 +67,13 @@ func SendBatchClaimTx( return sendBatchClaimTx(ctx, keyring, grpcClient, accountQueryClient, contractAddress, claims) } -// GetAccount retrieves account information for a given address. +// GetAccount is a test wrapper for getAccount. // This function is exported for testing purposes. func GetAccount(ctx context.Context, client authtypes.QueryClient, address string) (sqstx.Account, error) { return getAccount(ctx, client, address) } -// PrepareBatchClaimMsg prepares a batch claim message for the claimbot. +// PrepareBatchClaimMsg is a test wrapper for prepareBatchClaimMsg. // This function is exported for testing purposes. func PrepareBatchClaimMsg(claims orderbookdomain.Orders) ([]byte, error) { return prepareBatchClaimMsg(claims) diff --git a/ingest/usecase/plugins/orderbook/claimbot/order.go b/ingest/usecase/plugins/orderbook/claimbot/order.go index 8a90cf3f..2a75696b 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/order.go +++ b/ingest/usecase/plugins/orderbook/claimbot/order.go @@ -9,15 +9,15 @@ import ( "github.com/osmosis-labs/sqs/domain/mvc" orderbookdomain "github.com/osmosis-labs/sqs/domain/orderbook" orderbookgrpcclientdomain "github.com/osmosis-labs/sqs/domain/orderbook/grpcclient" - "go.uber.org/zap" - "github.com/osmosis-labs/sqs/log" + + "go.uber.org/zap" ) type order struct { - orderbook domain.CanonicalOrderBooksResult - orders orderbookdomain.Orders - err error + Orderbook domain.CanonicalOrderBooksResult + Orders orderbookdomain.Orders + Err error } // processOrderbooksAndGetClaimableOrders processes a list of orderbooks and returns claimable orders for each. @@ -51,13 +51,13 @@ func processOrderbook( claimable, err := getClaimableOrdersForOrderbook(ctx, fillThreshold, orderbook, orderbookRepository, orderBookClient, orderbookusecase, logger) if err != nil { return order{ - orderbook: orderbook, - err: err, + Orderbook: orderbook, + Err: err, } } return order{ - orderbook: orderbook, - orders: claimable, + Orderbook: orderbook, + Orders: claimable, } } @@ -131,12 +131,16 @@ func getClaimableOrders( if isTickFullyFilled(tickValues) { return orders } + return filterClaimableOrders(orderbook, orders, fillThreshold, orderbookusecase, logger) } // isTickFullyFilled checks if a tick is fully filled by comparing its cumulative total value // to its effective total amount swapped. func isTickFullyFilled(tickValues orderbookdomain.TickValues) bool { + if len(tickValues.CumulativeTotalValue) == 0 || len(tickValues.EffectiveTotalAmountSwapped) == 0 { + return false // empty values, thus not fully filled + } return tickValues.CumulativeTotalValue == tickValues.EffectiveTotalAmountSwapped } diff --git a/ingest/usecase/plugins/orderbook/claimbot/order_test.go b/ingest/usecase/plugins/orderbook/claimbot/order_test.go new file mode 100644 index 00000000..97306a06 --- /dev/null +++ b/ingest/usecase/plugins/orderbook/claimbot/order_test.go @@ -0,0 +1,175 @@ +package claimbot_test + +import ( + "context" + "testing" + + "github.com/osmosis-labs/osmosis/osmomath" + "github.com/osmosis-labs/sqs/domain" + "github.com/osmosis-labs/sqs/domain/mocks" + orderbookdomain "github.com/osmosis-labs/sqs/domain/orderbook" + "github.com/osmosis-labs/sqs/ingest/usecase/plugins/orderbook/claimbot" + "github.com/osmosis-labs/sqs/log" + "github.com/osmosis-labs/sqs/sqsdomain/cosmwasmpool" + + "github.com/stretchr/testify/assert" +) + +func TestProcessOrderbooksAndGetClaimableOrders(t *testing.T) { + newOrderbookTick := func(tickID int64) map[int64]orderbookdomain.OrderbookTick { + return map[int64]orderbookdomain.OrderbookTick{ + tickID: { + Tick: &cosmwasmpool.OrderbookTick{ + TickId: tickID, + }, + }, + } + } + + newOrderbookFullyFilledTick := func(tickID int64, direction string) map[int64]orderbookdomain.OrderbookTick { + tick := orderbookdomain.OrderbookTick{ + Tick: &cosmwasmpool.OrderbookTick{ + TickId: tickID, + }, + TickState: orderbookdomain.TickState{}, + } + + tickValue := orderbookdomain.TickValues{ + CumulativeTotalValue: "100", + EffectiveTotalAmountSwapped: "100", + } + + if direction == "bid" { + tick.TickState.BidValues = tickValue + } else { + tick.TickState.AskValues = tickValue + } + + return map[int64]orderbookdomain.OrderbookTick{ + tickID: tick, + } + } + + newOrder := func(direction string) orderbookdomain.Order { + return orderbookdomain.Order{ + TickId: 1, + OrderId: 1, + OrderDirection: direction, + } + } + + newLimitOrder := func(percentFilled osmomath.Dec) orderbookdomain.LimitOrder { + return orderbookdomain.LimitOrder{ + OrderId: 1, + PercentFilled: percentFilled, + } + } + + newCanonicalOrderBooksResult := func(poolID uint64, contractAddress string) domain.CanonicalOrderBooksResult { + return domain.CanonicalOrderBooksResult{PoolID: poolID, ContractAddress: contractAddress} + } + + tests := []struct { + name string + fillThreshold osmomath.Dec + orderbooks []domain.CanonicalOrderBooksResult + mockSetup func(*mocks.OrderbookRepositoryMock, *mocks.OrderbookGRPCClientMock, *mocks.OrderbookUsecaseMock) + expectedOrders []claimbot.Order + }{ + { + name: "No orderbooks", + fillThreshold: osmomath.NewDec(1), + orderbooks: []domain.CanonicalOrderBooksResult{}, + mockSetup: func(repo *mocks.OrderbookRepositoryMock, client *mocks.OrderbookGRPCClientMock, usecase *mocks.OrderbookUsecaseMock) { + repo.GetAllTicksFunc = func(poolID uint64) (map[int64]orderbookdomain.OrderbookTick, bool) { + return nil, false + } + }, + expectedOrders: nil, + }, + { + name: "Single orderbook with no claimable orders", + fillThreshold: osmomath.NewDecWithPrec(95, 2), // 0.95 + orderbooks: []domain.CanonicalOrderBooksResult{ + newCanonicalOrderBooksResult(10, "contract1"), + }, + mockSetup: func(repository *mocks.OrderbookRepositoryMock, client *mocks.OrderbookGRPCClientMock, usecase *mocks.OrderbookUsecaseMock) { + repository.WithGetAllTicksFunc(newOrderbookTick(1), true) + + client.WithGetOrdersByTickCb(orderbookdomain.Orders{ + newOrder("ask"), + }, nil) + + // Not claimable order, below threshold + usecase.WithCreateFormattedLimitOrder(newLimitOrder(osmomath.NewDecWithPrec(90, 2)), nil) + }, + expectedOrders: []claimbot.Order{ + { + Orderbook: newCanonicalOrderBooksResult(10, "contract1"), // orderbook with + Orders: nil, // no claimable orders + }, + }, + }, + { + name: "Tick fully filled: all orders are claimable", + fillThreshold: osmomath.NewDecWithPrec(99, 2), // 0.99 + orderbooks: []domain.CanonicalOrderBooksResult{ + newCanonicalOrderBooksResult(38, "contract8"), + }, + mockSetup: func(repository *mocks.OrderbookRepositoryMock, client *mocks.OrderbookGRPCClientMock, usecase *mocks.OrderbookUsecaseMock) { + repository.WithGetAllTicksFunc(newOrderbookFullyFilledTick(35, "bid"), true) + + client.WithGetOrdersByTickCb(orderbookdomain.Orders{ + newOrder("bid"), + }, nil) + + usecase.WithCreateFormattedLimitOrder(newLimitOrder(osmomath.NewDecWithPrec(90, 2)), nil) + }, + expectedOrders: []claimbot.Order{ + { + Orderbook: newCanonicalOrderBooksResult(38, "contract8"), + Orders: orderbookdomain.Orders{newOrder("bid")}, + }, + }, + }, + { + name: "Orderbook with claimable orders", + fillThreshold: osmomath.NewDecWithPrec(95, 2), // 0.95 + orderbooks: []domain.CanonicalOrderBooksResult{ + newCanonicalOrderBooksResult(64, "contract58"), + }, + mockSetup: func(repository *mocks.OrderbookRepositoryMock, client *mocks.OrderbookGRPCClientMock, usecase *mocks.OrderbookUsecaseMock) { + repository.WithGetAllTicksFunc(newOrderbookTick(42), true) + + client.WithGetOrdersByTickCb(orderbookdomain.Orders{ + newOrder("ask"), + }, nil) + + // Claimable order, above threshold + usecase.WithCreateFormattedLimitOrder(newLimitOrder(osmomath.NewDecWithPrec(96, 2)), nil) + }, + expectedOrders: []claimbot.Order{ + { + Orderbook: newCanonicalOrderBooksResult(64, "contract58"), + Orders: orderbookdomain.Orders{newOrder("ask")}, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + repository := mocks.OrderbookRepositoryMock{} + client := mocks.OrderbookGRPCClientMock{} + usecase := mocks.OrderbookUsecaseMock{} + logger := log.NoOpLogger{} + + tt.mockSetup(&repository, &client, &usecase) + + result := claimbot.ProcessOrderbooksAndGetClaimableOrders(ctx, tt.fillThreshold, tt.orderbooks, &repository, &client, &usecase, &logger) + + assert.Equal(t, tt.expectedOrders, result) + }) + } +} diff --git a/ingest/usecase/plugins/orderbook/claimbot/plugin.go b/ingest/usecase/plugins/orderbook/claimbot/plugin.go index 3df71afa..e6c5b2d4 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/plugin.go +++ b/ingest/usecase/plugins/orderbook/claimbot/plugin.go @@ -109,15 +109,15 @@ func (o *claimbot) ProcessEndBlock(ctx context.Context, blockHeight uint64, meta ) for _, orderbook := range orders { - if orderbook.err != nil { - fmt.Println("step1 error", orderbook.err) + if orderbook.Err != nil { + fmt.Println("step1 error", orderbook.Err) continue } - if err := o.processBatchClaimOrders(ctx, orderbook.orderbook, orderbook.orders); err != nil { + if err := o.processBatchClaimOrders(ctx, orderbook.Orderbook, orderbook.Orders); err != nil { o.logger.Info( "failed to process orderbook orders", - zap.String("contract_address", orderbook.orderbook.ContractAddress), + zap.String("contract_address", orderbook.Orderbook.ContractAddress), zap.Error(err), ) }