From cd919d6707f3a04bd23c5897632078cb1e12f1eb Mon Sep 17 00:00:00 2001 From: Deividas Petraitis Date: Fri, 4 Oct 2024 18:14:03 +0300 Subject: [PATCH 01/33] BE-586 | Claimbot prototype Init --- app/sidecar_query_server.go | 13 +++ domain/config.go | 4 +- domain/mvc/orderbook.go | 4 + .../grpcclient/orderbook_grpc_client.go | 5 +- .../grpcclient/orderbook_query_payloads.go | 3 +- domain/orderbook/order.go | 11 +++ domain/orderbook/orderbook_repository.go | 1 + domain/orderbook/orderbook_tick.go | 28 ++++++- domain/orderbook/plugin/config.go | 3 +- .../orderbook/plugin/orderbook_grpc_client.go | 4 +- domain/orderbook/plugin/orderbook_order.go | 21 ++--- .../orderbookfiller/fillable_orders.go | 5 +- .../plugins/orderbookfiller/tick_fetcher.go | 5 +- orderbook/usecase/export_test.go | 11 --- orderbook/usecase/orderbook_usecase.go | 79 +++++++++---------- 15 files changed, 112 insertions(+), 85 deletions(-) diff --git a/app/sidecar_query_server.go b/app/sidecar_query_server.go index f97fbc85a..a35a9b4bd 100644 --- a/app/sidecar_query_server.go +++ b/app/sidecar_query_server.go @@ -22,6 +22,7 @@ import ( ingestrpcdelivry "github.com/osmosis-labs/sqs/ingest/delivery/grpc" ingestusecase "github.com/osmosis-labs/sqs/ingest/usecase" + "github.com/osmosis-labs/sqs/ingest/usecase/plugins/orderbookclaimer" "github.com/osmosis-labs/sqs/ingest/usecase/plugins/orderbookfiller" orderbookrepository "github.com/osmosis-labs/sqs/orderbook/repository" orderbookusecase "github.com/osmosis-labs/sqs/orderbook/usecase" @@ -282,6 +283,18 @@ func NewSideCarQueryServer(appCodec codec.Codec, config domain.Config, logger lo currentPlugin = orderbookfiller.New(poolsUseCase, routerUsecase, tokensUseCase, passthroughGRPCClient, orderBookAPIClient, keyring, defaultQuoteDenom, logger) } + if plugin.GetName() == orderbookplugindomain.OrderBookClaimerPluginName { + currentPlugin = orderbookclaimer.New( + orderBookUseCase, + poolsUseCase, + orderBookRepository, + orderBookAPIClient, + passthroughGRPCClient, + orderBookAPIClient, + logger, + ) + } + // Register the plugin with the ingest use case ingestUseCase.RegisterEndBlockProcessPlugin(currentPlugin) } diff --git a/domain/config.go b/domain/config.go index a29c8718a..d48359f49 100644 --- a/domain/config.go +++ b/domain/config.go @@ -156,8 +156,8 @@ var ( ServerConnectionTimeoutSeconds: 10, Plugins: []Plugin{ &OrderBookPluginConfig{ - Enabled: false, - Name: orderbookplugindomain.OrderBookPluginName, + Enabled: true, + Name: orderbookplugindomain.OrderBookClaimerPluginName, }, }, }, diff --git a/domain/mvc/orderbook.go b/domain/mvc/orderbook.go index 4e6cabf4c..1f3a8b0e0 100644 --- a/domain/mvc/orderbook.go +++ b/domain/mvc/orderbook.go @@ -3,6 +3,7 @@ package mvc import ( "context" + "github.com/osmosis-labs/sqs/domain" orderbookdomain "github.com/osmosis-labs/sqs/domain/orderbook" "github.com/osmosis-labs/sqs/sqsdomain" ) @@ -21,4 +22,7 @@ type OrderBookUsecase interface { // The caller should range over the channel, but note that channel is never closed since there may be multiple // sender goroutines. GetActiveOrdersStream(ctx context.Context, address string) <-chan orderbookdomain.OrderbookResult + + // TODO + CreateFormattedLimitOrder(orderbook domain.CanonicalOrderBooksResult, order orderbookdomain.Order) (orderbookdomain.LimitOrder, error) } diff --git a/domain/orderbook/grpcclient/orderbook_grpc_client.go b/domain/orderbook/grpcclient/orderbook_grpc_client.go index ba0336c3d..33a3cbfc3 100644 --- a/domain/orderbook/grpcclient/orderbook_grpc_client.go +++ b/domain/orderbook/grpcclient/orderbook_grpc_client.go @@ -6,7 +6,6 @@ import ( cosmwasmdomain "github.com/osmosis-labs/sqs/domain/cosmwasm" orderbookdomain "github.com/osmosis-labs/sqs/domain/orderbook" - orderbookplugindomain "github.com/osmosis-labs/sqs/domain/orderbook/plugin" wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" ) @@ -14,7 +13,7 @@ import ( // OrderBookClient is an interface for fetching orders by tick from the orderbook contract. type OrderBookClient interface { // GetOrdersByTick fetches orders by tick from the orderbook contract. - GetOrdersByTick(ctx context.Context, contractAddress string, tick int64) ([]orderbookplugindomain.Order, error) + GetOrdersByTick(ctx context.Context, contractAddress string, tick int64) (orderbookdomain.Orders, error) // GetActiveOrders fetches active orders by owner from the orderbook contract. GetActiveOrders(ctx context.Context, contractAddress string, ownerAddress string) (orderbookdomain.Orders, uint64, error) @@ -55,7 +54,7 @@ func New(wasmClient wasmtypes.QueryClient) *orderbookClientImpl { } // GetOrdersByTick implements OrderBookClient. -func (o *orderbookClientImpl) GetOrdersByTick(ctx context.Context, contractAddress string, tick int64) ([]orderbookplugindomain.Order, error) { +func (o *orderbookClientImpl) GetOrdersByTick(ctx context.Context, contractAddress string, tick int64) (orderbookdomain.Orders, error) { ordersByTick := ordersByTick{Tick: tick} var orders ordersByTickResponse diff --git a/domain/orderbook/grpcclient/orderbook_query_payloads.go b/domain/orderbook/grpcclient/orderbook_query_payloads.go index 6763c1810..cc73db1c0 100644 --- a/domain/orderbook/grpcclient/orderbook_query_payloads.go +++ b/domain/orderbook/grpcclient/orderbook_query_payloads.go @@ -2,7 +2,6 @@ package orderbookgrpcclientdomain import ( orderbookdomain "github.com/osmosis-labs/sqs/domain/orderbook" - orderbookplugindomain "github.com/osmosis-labs/sqs/domain/orderbook/plugin" ) // ordersByTick is a struct that represents the request payload for the orders_by_tick query. @@ -17,7 +16,7 @@ type ordersByTickRequest struct { // ordersByTickResponse is a struct that represents the response payload for the orders_by_tick query. type ordersByTickResponse struct { - Orders []orderbookplugindomain.Order `json:"orders"` + Orders []orderbookdomain.Order `json:"orders"` } // unrealizedCancelsRequestPayload is a struct that represents the payload for the get_unrealized_cancels query. diff --git a/domain/orderbook/order.go b/domain/orderbook/order.go index cd2a5a19c..91601e310 100644 --- a/domain/orderbook/order.go +++ b/domain/orderbook/order.go @@ -65,6 +65,17 @@ func (o Orders) TickID() []int64 { return tickIDs } +// TODO +func (o Orders) OrderByDirection(direction string) Orders { + var result Orders + for _, v := range o { + if v.OrderDirection == direction { + result = append(result, v) + } + } + return result +} + // Asset represents orderbook asset returned by the orderbook contract. type Asset struct { Symbol string `json:"symbol"` diff --git a/domain/orderbook/orderbook_repository.go b/domain/orderbook/orderbook_repository.go index 582bd08d0..1cf57542d 100644 --- a/domain/orderbook/orderbook_repository.go +++ b/domain/orderbook/orderbook_repository.go @@ -5,6 +5,7 @@ type OrderBookRepository interface { StoreTicks(poolID uint64, ticksMap map[int64]OrderbookTick) // GetTicks returns the orderbook ticks for a given orderbook pool id. + // Bool indicates whether ticks were found for the given orderbook pool id. GetAllTicks(poolID uint64) (map[int64]OrderbookTick, bool) // GetTicks returns specific orderbook ticks for a given orderbook pool id. diff --git a/domain/orderbook/orderbook_tick.go b/domain/orderbook/orderbook_tick.go index c2130d7b4..0a6bb9e60 100644 --- a/domain/orderbook/orderbook_tick.go +++ b/domain/orderbook/orderbook_tick.go @@ -27,9 +27,29 @@ type TickState struct { } type TickValues struct { - TotalAmountOfLiquidity string `json:"total_amount_of_liquidity"` - CumulativeTotalValue string `json:"cumulative_total_value"` + // Total Amount of Liquidity at tick (TAL) + // - Every limit order placement increments this value. + // - Every swap at this tick decrements this value. + // - Every cancellation decrements this value. + TotalAmountOfLiquidity string `json:"total_amount_of_liquidity"` + + // Cumulative Total Limits at tick (CTT) + // - Every limit order placement increments this value. + // - There might be an edge-case optimization to lower this value. + CumulativeTotalValue string `json:"cumulative_total_value"` + + // Effective Total Amount Swapped at tick (ETAS) + // - Every swap increments ETAS by the swap amount. + // - There will be other ways to update ETAS as described below. EffectiveTotalAmountSwapped string `json:"effective_total_amount_swapped"` - CumulativeRealizedCancels string `json:"cumulative_realized_cancels"` - LastTickSyncEtas string `json:"last_tick_sync_etas"` + + // Cumulative Realized Cancellations at tick + // - Increases as cancellations are checkpointed in batches on the sumtree + // - Equivalent to the prefix sum at the tick's current ETAS after being synced + CumulativeRealizedCancels string `json:"cumulative_realized_cancels"` + + // last_tick_sync_etas is the ETAS value after the most recent tick sync. + // It is used to skip tick syncs if ETAS has not changed since the previous + // sync. + LastTickSyncEtas string `json:"last_tick_sync_etas"` } diff --git a/domain/orderbook/plugin/config.go b/domain/orderbook/plugin/config.go index 37139f10d..7075e65bf 100644 --- a/domain/orderbook/plugin/config.go +++ b/domain/orderbook/plugin/config.go @@ -2,5 +2,6 @@ package orderbookplugindomain const ( // OrderBookPluginName is the name of the orderbook plugin. - OrderBookPluginName = "orderbook" + OrderBookPluginName = "orderbook" + OrderBookClaimerPluginName = "orderbookclaimer" ) diff --git a/domain/orderbook/plugin/orderbook_grpc_client.go b/domain/orderbook/plugin/orderbook_grpc_client.go index f997e23ca..816df149e 100644 --- a/domain/orderbook/plugin/orderbook_grpc_client.go +++ b/domain/orderbook/plugin/orderbook_grpc_client.go @@ -2,10 +2,12 @@ package orderbookplugindomain import ( "context" + + orderbookdomain "github.com/osmosis-labs/sqs/domain/orderbook" ) // OrderbookCWAPIClient is an interface for fetching orders by tick from the orderbook contract. type OrderbookCWAPIClient interface { // GetOrdersByTick fetches orders by tick from the orderbook contract. - GetOrdersByTick(ctx context.Context, contractAddress string, tick int64) ([]Order, error) + GetOrdersByTick(ctx context.Context, contractAddress string, tick int64) (orderbookdomain.Orders, error) } diff --git a/domain/orderbook/plugin/orderbook_order.go b/domain/orderbook/plugin/orderbook_order.go index 738ac2c01..68aa0ab48 100644 --- a/domain/orderbook/plugin/orderbook_order.go +++ b/domain/orderbook/plugin/orderbook_order.go @@ -1,21 +1,12 @@ package orderbookplugindomain -// Order represents an order in the orderbook returned by the orderbook contract. -type Order struct { - TickId int64 `json:"tick_id"` - OrderId int64 `json:"order_id"` - OrderDirection string `json:"order_direction"` - Owner string `json:"owner"` - Quantity string `json:"quantity"` - Etas string `json:"etas"` - ClaimBounty string `json:"claim_bounty"` - PlacedQuantity string `json:"placed_quantity"` - PlacedAt string `json:"placed_at"` -} +import ( + orderbookdomain "github.com/osmosis-labs/sqs/domain/orderbook" +) // OrdersResponse represents the response from the orderbook contract containing the orders for a given tick. type OrdersResponse struct { - Address string `json:"address"` - BidOrders []Order `json:"bid_orders"` - AskOrders []Order `json:"ask_orders"` + Address string `json:"address"` + BidOrders []orderbookdomain.Order `json:"bid_orders"` + AskOrders []orderbookdomain.Order `json:"ask_orders"` } diff --git a/ingest/usecase/plugins/orderbookfiller/fillable_orders.go b/ingest/usecase/plugins/orderbookfiller/fillable_orders.go index f4cbb6b7e..1eb706d5d 100644 --- a/ingest/usecase/plugins/orderbookfiller/fillable_orders.go +++ b/ingest/usecase/plugins/orderbookfiller/fillable_orders.go @@ -9,6 +9,7 @@ import ( "go.uber.org/zap" clmath "github.com/osmosis-labs/osmosis/v26/x/concentrated-liquidity/math" + orderbookdomain "github.com/osmosis-labs/sqs/domain/orderbook" orderbookplugindomain "github.com/osmosis-labs/sqs/domain/orderbook/plugin" "github.com/osmosis-labs/sqs/sqsdomain/cosmwasmpool" @@ -132,7 +133,7 @@ func applyFee(amount osmomath.Int, fee osmomath.Dec) osmomath.Int { // Iterates over all ask orders, identifying their ticks. // Compares the order tick to the current tick. If ask order is below the market tick, it is fillable. // In that case, we determine the remaining ask liquidity on that tick, applying the price to get its value in the quote denom. -func (o *orderbookFillerIngestPlugin) getFillableAskAmountInQuoteDenom(askOrders []orderbookplugindomain.Order, currentTick int64, tickRemainingLiqMap map[int64]cosmwasmpool.OrderbookTickLiquidity) (osmomath.Int, error) { +func (o *orderbookFillerIngestPlugin) getFillableAskAmountInQuoteDenom(askOrders []orderbookdomain.Order, currentTick int64, tickRemainingLiqMap map[int64]cosmwasmpool.OrderbookTickLiquidity) (osmomath.Int, error) { fillableAskAmountInQuoteDenom := osmomath.ZeroBigDec() // Multiple orders may be placed on the same tick, so we need to keep track of which ticks we have processed @@ -178,7 +179,7 @@ func (o *orderbookFillerIngestPlugin) getFillableAskAmountInQuoteDenom(askOrders // Iterates over all bid orders, identifying their ticks. // Compares the order tick to the current tick. If bid order is above the market tick, it is fillable. // In that case, we determine the remaining bi liquidity on that tick, applying the price to get its value in the base denom. -func (o *orderbookFillerIngestPlugin) getFillableBidAmountInBaseDenom(bidOrders []orderbookplugindomain.Order, currentTick int64, tickRemainingLiqMap map[int64]cosmwasmpool.OrderbookTickLiquidity) (osmomath.Int, error) { +func (o *orderbookFillerIngestPlugin) getFillableBidAmountInBaseDenom(bidOrders []orderbookdomain.Order, currentTick int64, tickRemainingLiqMap map[int64]cosmwasmpool.OrderbookTickLiquidity) (osmomath.Int, error) { fillableBidAmountInBaseDenom := osmomath.ZeroBigDec() // Multiple orders may be placed on the same tick, so we need to keep track of which ticks we have processed diff --git a/ingest/usecase/plugins/orderbookfiller/tick_fetcher.go b/ingest/usecase/plugins/orderbookfiller/tick_fetcher.go index ee06ae06c..2f245dc49 100644 --- a/ingest/usecase/plugins/orderbookfiller/tick_fetcher.go +++ b/ingest/usecase/plugins/orderbookfiller/tick_fetcher.go @@ -4,6 +4,7 @@ import ( "context" "github.com/osmosis-labs/sqs/domain" + orderbookdomain "github.com/osmosis-labs/sqs/domain/orderbook" orderbookplugindomain "github.com/osmosis-labs/sqs/domain/orderbook/plugin" ) @@ -16,8 +17,8 @@ func (o *orderbookFillerIngestPlugin) fetchTicksForOrderbook(ctx context.Context ticks := orderBookPool.GetSQSPoolModel().CosmWasmPoolModel.Data.Orderbook.Ticks orderResult := orderbookplugindomain.OrdersResponse{ - AskOrders: []orderbookplugindomain.Order{}, - BidOrders: []orderbookplugindomain.Order{}, + AskOrders: []orderbookdomain.Order{}, + BidOrders: []orderbookdomain.Order{}, } for _, tick := range ticks { orders, err := o.orderbookCWAAPIClient.GetOrdersByTick(ctx, orderbook.ContractAddress, tick.TickId) diff --git a/orderbook/usecase/export_test.go b/orderbook/usecase/export_test.go index 01d015805..46e910432 100644 --- a/orderbook/usecase/export_test.go +++ b/orderbook/usecase/export_test.go @@ -13,17 +13,6 @@ func (o *OrderbookUseCaseImpl) SetFetchActiveOrdersEveryDuration(duration time.D fetchActiveOrdersDuration = duration } -// CreateFormattedLimitOrder is an alias of createFormattedLimitOrder for testing purposes -func (o *OrderbookUseCaseImpl) CreateFormattedLimitOrder( - poolID uint64, - order orderbookdomain.Order, - quoteAsset orderbookdomain.Asset, - baseAsset orderbookdomain.Asset, - orderbookAddress string, -) (orderbookdomain.LimitOrder, error) { - return o.createFormattedLimitOrder(poolID, order, quoteAsset, baseAsset, orderbookAddress) -} - // ProcessOrderBookActiveOrders is an alias of processOrderBookActiveOrders for testing purposes func (o *OrderbookUseCaseImpl) ProcessOrderBookActiveOrders(ctx context.Context, orderBook domain.CanonicalOrderBooksResult, ownerAddress string) ([]orderbookdomain.LimitOrder, bool, error) { return o.processOrderBookActiveOrders(ctx, orderBook, ownerAddress) diff --git a/orderbook/usecase/orderbook_usecase.go b/orderbook/usecase/orderbook_usecase.go index 8bce75e2a..78c6bc31d 100644 --- a/orderbook/usecase/orderbook_usecase.go +++ b/orderbook/usecase/orderbook_usecase.go @@ -262,15 +262,15 @@ func (o *OrderbookUseCaseImpl) GetActiveOrders(ctx context.Context, address stri // // For every order, if an error occurs processing the order, it is skipped rather than failing the entire process. // This is a best-effort process. -func (o *OrderbookUseCaseImpl) processOrderBookActiveOrders(ctx context.Context, orderBook domain.CanonicalOrderBooksResult, ownerAddress string) ([]orderbookdomain.LimitOrder, bool, error) { - if err := orderBook.Validate(); err != nil { +func (o *OrderbookUseCaseImpl) processOrderBookActiveOrders(ctx context.Context, orderbook domain.CanonicalOrderBooksResult, ownerAddress string) ([]orderbookdomain.LimitOrder, bool, error) { + if err := orderbook.Validate(); err != nil { return nil, false, err } - orders, count, err := o.orderBookClient.GetActiveOrders(ctx, orderBook.ContractAddress, ownerAddress) + orders, count, err := o.orderBookClient.GetActiveOrders(ctx, orderbook.ContractAddress, ownerAddress) if err != nil { return nil, false, types.FailedToGetActiveOrdersError{ - ContractAddress: orderBook.ContractAddress, + ContractAddress: orderbook.ContractAddress, OwnerAddress: ownerAddress, Err: err, } @@ -281,22 +281,6 @@ func (o *OrderbookUseCaseImpl) processOrderBookActiveOrders(ctx context.Context, return nil, false, nil } - quoteToken, err := o.tokensUsecease.GetMetadataByChainDenom(orderBook.Quote) - if err != nil { - return nil, false, types.FailedToGetMetadataError{ - TokenDenom: orderBook.Quote, - Err: err, - } - } - - baseToken, err := o.tokensUsecease.GetMetadataByChainDenom(orderBook.Base) - if err != nil { - return nil, false, types.FailedToGetMetadataError{ - TokenDenom: orderBook.Base, - Err: err, - } - } - // Create a slice to store the results results := make([]orderbookdomain.LimitOrder, 0, len(orders)) @@ -306,18 +290,9 @@ func (o *OrderbookUseCaseImpl) processOrderBookActiveOrders(ctx context.Context, // For each order, create a formatted limit order for _, order := range orders { // create limit order - result, err := o.createFormattedLimitOrder( - orderBook.PoolID, + result, err := o.CreateFormattedLimitOrder( + orderbook, order, - orderbookdomain.Asset{ - Symbol: quoteToken.CoinMinimalDenom, - Decimals: quoteToken.Precision, - }, - orderbookdomain.Asset{ - Symbol: baseToken.CoinMinimalDenom, - Decimals: baseToken.Precision, - }, - orderBook.ContractAddress, ) if err != nil { telemetry.CreateLimitOrderErrorCounter.Inc() @@ -338,19 +313,39 @@ func (o *OrderbookUseCaseImpl) processOrderBookActiveOrders(ctx context.Context, // It is defined in a global space to avoid creating a new instance every time. var zeroDec = osmomath.ZeroDec() -// createFormattedLimitOrder creates a limit order from the orderbook order. -func (o *OrderbookUseCaseImpl) createFormattedLimitOrder( - poolID uint64, - order orderbookdomain.Order, - quoteAsset orderbookdomain.Asset, - baseAsset orderbookdomain.Asset, - orderbookAddress string, -) (orderbookdomain.LimitOrder, error) { - tickForOrder, ok := o.orderbookRepository.GetTickByID(poolID, order.TickId) +// CreateFormattedLimitOrder creates a limit order from the orderbook order. +func (o *OrderbookUseCaseImpl) CreateFormattedLimitOrder(orderbook domain.CanonicalOrderBooksResult, order orderbookdomain.Order) (orderbookdomain.LimitOrder, error) { + quoteToken, err := o.tokensUsecease.GetMetadataByChainDenom(orderbook.Quote) + if err != nil { + return orderbookdomain.LimitOrder{}, types.FailedToGetMetadataError{ + TokenDenom: orderbook.Quote, + Err: err, + } + } + + quoteAsset := orderbookdomain.Asset{ + Symbol: quoteToken.CoinMinimalDenom, + Decimals: quoteToken.Precision, + } + + baseToken, err := o.tokensUsecease.GetMetadataByChainDenom(orderbook.Base) + if err != nil { + return orderbookdomain.LimitOrder{}, types.FailedToGetMetadataError{ + TokenDenom: orderbook.Base, + Err: err, + } + } + + baseAsset := orderbookdomain.Asset{ + Symbol: baseToken.CoinMinimalDenom, + Decimals: baseToken.Precision, + } + + tickForOrder, ok := o.orderbookRepository.GetTickByID(orderbook.PoolID, order.TickId) if !ok { telemetry.GetTickByIDNotFoundCounter.Inc() return orderbookdomain.LimitOrder{}, types.TickForOrderbookNotFoundError{ - OrderbookAddress: orderbookAddress, + OrderbookAddress: orderbook.ContractAddress, TickID: order.TickId, } } @@ -497,7 +492,7 @@ func (o *OrderbookUseCaseImpl) createFormattedLimitOrder( PercentClaimed: percentClaimed, TotalFilled: totalFilled, PercentFilled: percentFilled, - OrderbookAddress: orderbookAddress, + OrderbookAddress: orderbook.ContractAddress, Price: normalizedPrice, Status: status, Output: output, From 25746a1031465c56cddf33751d4cdf2a851843fe Mon Sep 17 00:00:00 2001 From: Deividas Petraitis Date: Mon, 7 Oct 2024 19:58:02 +0300 Subject: [PATCH 02/33] BE-586 | WIP --- app/sidecar_query_server.go | 8 + domain/keyring/osmosis_keyring.go | 8 +- ingest/usecase/plugins/orderbookclaimer/.env | 4 + .../plugins/orderbookclaimer/README.md | 1 + .../orderbookclaimer/docker-compose.yml | 147 ++++++++ .../ordebook_filler_ingest_plugin.go | 164 +++++++++ ingest/usecase/plugins/orderbookclaimer/tx.go | 316 ++++++++++++++++++ .../usecase/plugins/orderbookfiller/README.md | 2 +- .../plugins/orderbookfiller/osmosis_swap.go | 29 +- 9 files changed, 651 insertions(+), 28 deletions(-) create mode 100644 ingest/usecase/plugins/orderbookclaimer/.env create mode 100644 ingest/usecase/plugins/orderbookclaimer/README.md create mode 100644 ingest/usecase/plugins/orderbookclaimer/docker-compose.yml create mode 100644 ingest/usecase/plugins/orderbookclaimer/ordebook_filler_ingest_plugin.go create mode 100644 ingest/usecase/plugins/orderbookclaimer/tx.go diff --git a/app/sidecar_query_server.go b/app/sidecar_query_server.go index a35a9b4bd..d634c82c4 100644 --- a/app/sidecar_query_server.go +++ b/app/sidecar_query_server.go @@ -284,7 +284,15 @@ func NewSideCarQueryServer(appCodec codec.Codec, config domain.Config, logger lo } if plugin.GetName() == orderbookplugindomain.OrderBookClaimerPluginName { + // Create keyring + keyring, err := keyring.New() + if err != nil { + return nil, err + } + + logger.Info("Using keyring with address", zap.Stringer("address", keyring.GetAddress())) currentPlugin = orderbookclaimer.New( + keyring, orderBookUseCase, poolsUseCase, orderBookRepository, diff --git a/domain/keyring/osmosis_keyring.go b/domain/keyring/osmosis_keyring.go index c1237ed73..32f931f96 100644 --- a/domain/keyring/osmosis_keyring.go +++ b/domain/keyring/osmosis_keyring.go @@ -56,6 +56,9 @@ func New() (*keyringImpl, error) { } keyringConfig := keyring.Config{ + AllowedBackends: []keyring.BackendType{ + keyring.FileBackend, + }, ServiceName: keyringServiceName, FileDir: keyringPathEnv, KeychainTrustApplication: true, @@ -64,16 +67,15 @@ func New() (*keyringImpl, error) { }, } - // Open the keyring openKeyring, err := keyring.Open(keyringConfig) if err != nil { - return nil, err + return nil, fmt.Errorf("Unable to open keyring [ %s ]: %w", keyringPathEnv, err) } // Get the keyring record openRecord, err := openKeyring.Get(keyringKeyName) if err != nil { - return nil, err + return nil, fmt.Errorf("Unable to get keyring record [ %s ]: %w", keyringKeyName, err) } // Unmarshal the keyring record diff --git a/ingest/usecase/plugins/orderbookclaimer/.env b/ingest/usecase/plugins/orderbookclaimer/.env new file mode 100644 index 000000000..ceb560988 --- /dev/null +++ b/ingest/usecase/plugins/orderbookclaimer/.env @@ -0,0 +1,4 @@ +DD_API_KEY=YOUR_API_KEY +OSMOSIS_KEYRING_PATH=/root/.osmosisd/keyring-test +OSMOSIS_KEYRING_PASSWORD=test +OSMOSIS_KEYRING_KEY_NAME=local.info \ No newline at end of file diff --git a/ingest/usecase/plugins/orderbookclaimer/README.md b/ingest/usecase/plugins/orderbookclaimer/README.md new file mode 100644 index 000000000..ec982141f --- /dev/null +++ b/ingest/usecase/plugins/orderbookclaimer/README.md @@ -0,0 +1 @@ +# Order Book Claimer Plugin diff --git a/ingest/usecase/plugins/orderbookclaimer/docker-compose.yml b/ingest/usecase/plugins/orderbookclaimer/docker-compose.yml new file mode 100644 index 000000000..e84922af3 --- /dev/null +++ b/ingest/usecase/plugins/orderbookclaimer/docker-compose.yml @@ -0,0 +1,147 @@ +version: "3" + +services: + osmosis: + labels: + com.datadoghq.ad.logs: >- + [{ + "source": "osmosis", + "service": "osmosis", + "log_processing_rules": [{ + "type": "exclude_at_match", + "name": "exclude_p2p_module", + "pattern": "\"module\":\\s*\".*p2p.*\"" + }] + }] + environment: + - DD_AGENT_HOST=dd-agent + - OTEL_EXPORTER_OTLP_ENDPOINT=http://dd-agent:4317 + - DD_SERVICE=osmosis + - DD_ENV=prod + - DD_VERSION=25.0.0 + command: + - start + - --home=/osmosis/.osmosisd + image: osmolabs/osmosis-dev:v25.x-c775cee7-1722825184 + container_name: osmosis + restart: always + ports: + - 26657:26657 + - 1317:1317 + - 9191:9090 + - 9091:9091 + - 26660:26660 + - 6060:6060 + volumes: + - ${HOME}/.osmosisd/:/osmosis/.osmosisd/ + logging: + driver: "json-file" + options: + max-size: "2048m" + max-file: "3" + tag: "{{.ImageName}}|{{.Name}}" + + osmosis-sqs: + environment: + - DD_AGENT_HOST=dd-agent + - OTEL_EXPORTER_OTLP_ENDPOINT=http://dd-agent:4317 + - DD_SERVICE=sqs + - DD_ENV=prod + - DD_VERSION=25.0.0 + - OSMOSIS_KEYRING_PATH=${OSMOSIS_KEYRING_PATH} + - OSMOSIS_KEYRING_PASSWORD=${OSMOSIS_KEYRING_PASSWORD} + - OSMOSIS_KEYRING_KEY_NAME=${OSMOSIS_KEYRING_KEY_NAME} + - OSMOSIS_RPC_ENDPOINT=http://osmosis:26657 + - OSMOSIS_LCD_ENDPOINT=http://osmosis:1317 + - SQS_GRPC_TENDERMINT_RPC_ENDPOINT=http://osmosis:26657 + - SQS_GRPC_GATEWAY_ENDPOINT=osmosis:9090 + - SQS_OTEL_ENVIRONMENT=sqs-fill-bot + - SQS_GRPC_INGESTER_PLUGINS_ORDERBOOK_ENABLED=true + command: + - --host + - sqs-fill-bot + build: + context: ../../../../ + dockerfile: Dockerfile + depends_on: + - osmosis + container_name: osmosis-sqs + restart: always + ports: + - 9092:9092 + volumes: + - ${OSMOSIS_KEYRING_PATH}:${OSMOSIS_KEYRING_PATH} + logging: + driver: "json-file" + options: + max-size: "2048m" + max-file: "3" + tag: "{{.ImageName}}|{{.Name}}" + + dd-agent: + image: gcr.io/datadoghq/agent:7 + container_name: dd-agent + labels: + com.datadoghq.ad.checks: | + { + "openmetrics": { + "init_configs": [{}], + "instances": [ + { + "openmetrics_endpoint": "http://droid:8080/metrics", + "namespace": "osmosisd", + "metrics": + [ + {"osmosisd_info": "info"}, + {"osmosisd_cur_eip_base_fee": "cur_eip_base_fee"} + ] + }#, + # { + # "openmetrics_endpoint": "http://nginx/metrics", + # "namespace": "sqs", + # "metrics": [".*"] + # } + ] + } + } + environment: + - DD_API_KEY=${DD_API_KEY} + - DD_SITE=us5.datadoghq.com + - DD_ENV=prod + - DD_OTLP_CONFIG_RECEIVER_PROTOCOLS_GRPC_ENDPOINT=0.0.0.0:4317 + - DD_APM_ENABLED=true + - DD_LOGS_ENABLED=true + - DD_LOGS_CONFIG_DOCKER_CONTAINER_FORCE_USE_FILE=true + - DD_LOGS_CONFIG_CONTAINER_COLLECT_ALL=true + - DD_CONTAINER_EXCLUDE_LOGS=image:.*agent.* image:.*droid.* + - DD_OTLP_CONFIG_LOGS_ENABLED=true + - DD_APM_PROBABILISTIC_SAMPLER_ENABLED=true + - DD_APM_PROBABILISTIC_SAMPLER_SAMPLING_PERCENTAGE=1 + + volumes: + - /var/run/docker.sock:/var/run/docker.sock:rw + - /proc/:/host/proc/:rw + - /sys/fs/cgroup/:/host/sys/fs/cgroup:rw + - /var/lib/docker/containers:/var/lib/docker/containers:rw + - /opt/datadog/apm:/opt/datadog/apm + ports: + - 4317:4317 + - 4318:4318 + - 8126:8126 + + droid: + image: osmolabs/droid:0.0.3 + container_name: droid + restart: unless-stopped + depends_on: + - osmosis + ports: + - "8080:8080" + environment: + RPC_ENDPOINT: "http://osmosis:26657" + LCD_ENDPOINT: "http://osmosis:1317" + EIP1559_ENABLED: "true" + logging: + driver: "json-file" + options: + max-size: "512m" \ No newline at end of file diff --git a/ingest/usecase/plugins/orderbookclaimer/ordebook_filler_ingest_plugin.go b/ingest/usecase/plugins/orderbookclaimer/ordebook_filler_ingest_plugin.go new file mode 100644 index 000000000..823c2c89d --- /dev/null +++ b/ingest/usecase/plugins/orderbookclaimer/ordebook_filler_ingest_plugin.go @@ -0,0 +1,164 @@ +package orderbookclaimer + +import ( + "context" + "fmt" + "sync/atomic" + + "github.com/osmosis-labs/osmosis/osmomath" + "github.com/osmosis-labs/sqs/domain" + "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" + orderbookplugindomain "github.com/osmosis-labs/sqs/domain/orderbook/plugin" + passthroughdomain "github.com/osmosis-labs/sqs/domain/passthrough" + "github.com/osmosis-labs/sqs/log" + "go.opentelemetry.io/otel" + "go.uber.org/zap" +) + +// orderbookClaimerIngestPlugin is a plugin that fills the orderbook orders at the end of the block. +type orderbookClaimerIngestPlugin struct { + keyring keyring.Keyring + poolsUseCase mvc.PoolsUsecase + orderbookusecase mvc.OrderBookUsecase + orderbookRepository orderbookdomain.OrderBookRepository + orderBookClient orderbookgrpcclientdomain.OrderBookClient + + atomicBool atomic.Bool + + logger log.Logger +} + +var _ domain.EndBlockProcessPlugin = &orderbookClaimerIngestPlugin{} + +const ( + tracerName = "sqs-orderbook-claimer" +) + +var ( + tracer = otel.Tracer(tracerName) +) + +func New( + keyring keyring.Keyring, + orderbookusecase mvc.OrderBookUsecase, + poolsUseCase mvc.PoolsUsecase, + orderbookRepository orderbookdomain.OrderBookRepository, + orderBookClient orderbookgrpcclientdomain.OrderBookClient, + passthroughGRPCClient passthroughdomain.PassthroughGRPCClient, + orderBookCWAPIClient orderbookplugindomain.OrderbookCWAPIClient, + logger log.Logger, +) *orderbookClaimerIngestPlugin { + return &orderbookClaimerIngestPlugin{ + keyring: keyring, + orderbookusecase: orderbookusecase, + orderbookRepository: orderbookRepository, + orderBookClient: orderBookClient, + poolsUseCase: poolsUseCase, + + atomicBool: atomic.Bool{}, + + logger: logger, + } +} + +// ProcessEndBlock implements domain.EndBlockProcessPlugin. +func (o *orderbookClaimerIngestPlugin) ProcessEndBlock(ctx context.Context, blockHeight uint64, metadata domain.BlockPoolMetadata) error { + ctx, span := tracer.Start(ctx, "orderbooktFillerIngestPlugin.ProcessEndBlock") + defer span.End() + + orderbooks, err := o.poolsUseCase.GetAllCanonicalOrderbookPoolIDs() + if err != nil { + o.logger.Error("failed to get all canonical orderbook pool IDs", zap.Error(err)) + return err + } + + // For simplicity, we allow only one block to be processed at a time. + // This may be relaxed in the future. + if !o.atomicBool.CompareAndSwap(false, true) { + o.logger.Info("orderbook claimer is already in progress", zap.Uint64("block_height", blockHeight)) + return nil + } + defer o.atomicBool.Store(false) + + for _, orderbook := range orderbooks { + // TODO: get ticks + ticks, ok := o.orderbookRepository.GetAllTicks(orderbook.PoolID) + if !ok { + // TODO: report an error, this should not happen + fmt.Printf("no ticks for orderbook %s\n", orderbook.ContractAddress) + continue + } + + for _, t := range ticks { + // TODO: Do we wont to store all orders inside memory? + orders, err := o.orderBookClient.GetOrdersByTick(ctx, orderbook.ContractAddress, t.Tick.TickId) + if err != nil { + fmt.Printf("no unable to fetch orderbook orders by tick ID %d\n", t.Tick.TickId) + continue + } + + if len(orders) == 0 { + continue // nothing to do + } + + var claimable orderbookdomain.Orders + claimable = append(claimable, o.getClaimableOrders(orderbook, orders.OrderByDirection("ask"), t.TickState.AskValues)...) + claimable = append(claimable, o.getClaimableOrders(orderbook, orders.OrderByDirection("bid"), t.TickState.BidValues)...) + + if len(claimable) == 0 { + continue // nothing to do + } + + var claims []Claim + for _, order := range claimable { + claims = append(claims, Claim{ + TickID: order.TickId, + OrderID: order.OrderId, + }) + + break + } + + err = o.sendBatchClaimTx(orderbook.ContractAddress, claims) + fmt.Println("claims", claims, err) + + break + } + } + + o.logger.Info("processed end block in orderbook claimer ingest plugin", zap.Uint64("block_height", blockHeight)) + + return nil +} + +func (q *orderbookClaimerIngestPlugin) getClaimableOrders(orderbook domain.CanonicalOrderBooksResult, orders orderbookdomain.Orders, tickValues orderbookdomain.TickValues) orderbookdomain.Orders { + // When cumulative total value of the tick is equal to its effective total amount swapped + // that would mean that all orders for this tick is already filled and we attempt to claim all orders + if tickValues.CumulativeTotalValue == tickValues.EffectiveTotalAmountSwapped { + return orders + } + + // Calculate claimable orders for the tick by iterating over each active order + // and checking each orders percent filled + var claimable orderbookdomain.Orders + for _, order := range orders { + result, err := q.orderbookusecase.CreateFormattedLimitOrder( + orderbook, + order, + ) + + // TODO: to config? + claimable_threshold, err := osmomath.NewDecFromStr("0.98") + if err != nil { + } + + if result.PercentFilled.GT(claimable_threshold) && result.PercentFilled.LTE(osmomath.OneDec()) { + claimable = append(claimable, order) + } + } + + return claimable +} diff --git a/ingest/usecase/plugins/orderbookclaimer/tx.go b/ingest/usecase/plugins/orderbookclaimer/tx.go new file mode 100644 index 000000000..ebe92c94d --- /dev/null +++ b/ingest/usecase/plugins/orderbookclaimer/tx.go @@ -0,0 +1,316 @@ +package orderbookclaimer + +import ( + "context" + "encoding/json" + "fmt" + "io" + "log" + "net" + "net/http" + "strconv" + "time" + + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + + wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" + // "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/tx" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/tx/signing" + authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + + txtypes "github.com/cosmos/cosmos-sdk/types/tx" + "github.com/osmosis-labs/osmosis/osmomath" + "github.com/osmosis-labs/osmosis/v26/app" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +var ( + chainID = "osmosis-1" + + RPC = "localhost:9090" + LCD = "http://127.0.0.1:1317" + Denom = "uosmo" + NobleUSDC = "ibc/498A0751C798A0D9A389AA3691123DADA57DAA4FE165D5C75894505B876BA6E4" + + encodingConfig = app.MakeEncodingConfig() +) + +func (o *orderbookClaimerIngestPlugin) sendBatchClaimTx(contractAddress string, claims []Claim) error { + key := o.keyring.GetKey() + keyBytes := key.Bytes() + privKey := &secp256k1.PrivKey{Key: keyBytes} + + account := o.keyring + + txConfig := encodingConfig.TxConfig + + // Prepare the message + orders := make([][]int64, len(claims)) + for i, claim := range claims { + orders[i] = []int64{claim.TickID, claim.OrderID} + } + + batchClaim := struct { + BatchClaim struct { + Orders [][]int64 `json:"orders"` + } `json:"batch_claim"` + }{ + BatchClaim: struct { + Orders [][]int64 `json:"orders"` + }{ + Orders: orders, + }, + } + + msgBytes, err := json.Marshal(batchClaim) + if err != nil { + return fmt.Errorf("failed to marshal message: %w", err) + } + + // Create and sign the transaction + txBuilder := txConfig.NewTxBuilder() + + msg := wasmtypes.MsgExecuteContract{ + Sender: account.GetAddress().String(), + Contract: contractAddress, + Msg: msgBytes, + Funds: sdk.NewCoins(), + } + + err = txBuilder.SetMsgs(&msg) + if err != nil { + return fmt.Errorf("failed to set messages: %w", err) + } + + // Query gas price from chain + gasPrice, err := osmomath.NewDecFromStr("0.025") // Example gas price, adjust as necessary + if err != nil { + return err + } + + _, gas, err := o.simulateMsgs(context.TODO(), []sdk.Msg{&msg}) + + // Calculate the fee based on gas and gas price + feeAmount := gasPrice.MulInt64(int64(gas)).Ceil().TruncateInt64() + + fmt.Println("fee amount", feeAmount) + // Create the final fee structure + feecoin := sdk.NewCoin("uosmo", osmomath.NewInt(feeAmount)) + + accountSequence, accountNumber := getInitialSequence(context.TODO(), o.keyring.GetAddress().String()) + + txBuilder.SetGasLimit(gas) + txBuilder.SetFeeAmount(sdk.NewCoins(feecoin)) + + signMode := encodingConfig.TxConfig.SignModeHandler().DefaultMode() + protoSignMode, _ := authsigning.APISignModeToInternal(signMode) + + sigV2 := signing.SignatureV2{ + PubKey: privKey.PubKey(), + Data: &signing.SingleSignatureData{ + SignMode: protoSignMode, + Signature: nil, + }, + Sequence: accountSequence, + } + + err = txBuilder.SetSignatures(sigV2) + if err != nil { + return fmt.Errorf("failed to set signatures: %w", err) + } + + signerData := authsigning.SignerData{ + ChainID: chainID, + AccountNumber: accountNumber, + Sequence: accountSequence, + } + + signed, err := tx.SignWithPrivKey( + context.TODO(), + protoSignMode, signerData, + txBuilder, privKey, encodingConfig.TxConfig, accountSequence) + if err != nil { + return fmt.Errorf("failed to sing transaction: %w", err) + } + + err = txBuilder.SetSignatures(signed) + if err != nil { + return fmt.Errorf("failed to set signatures: %w", err) + } + + // Broadcast the transaction + txBytes, err := txConfig.TxEncoder()(txBuilder.GetTx()) + if err != nil { + return fmt.Errorf("failed to encode transaction: %w", err) + } + + // Generate a JSON string. + txJSONBytes, err := encodingConfig.TxConfig.TxJSONEncoder()(txBuilder.GetTx()) + if err != nil { + return err + } + + err = sendTx(context.TODO(), txBytes) + + log.Println("txJSON", string(txJSONBytes), err) + + return nil +} + +// You'll need to define the Claim struct and Config variables +type Claim struct { + TickID int64 + OrderID int64 +} + +// Config should contain all the necessary configuration variables +var Config struct { + CHAIN_ID string + RPC_ENDPOINT string + TX_KEY string + TX_GAS int64 + TX_FEE_DENOM string + TX_FEE_AMOUNT int64 +} + +type AccountInfo struct { + Sequence string `json:"sequence"` + AccountNumber string `json:"account_number"` +} + +type AccountResult struct { + Account AccountInfo `json:"account"` +} + +func getInitialSequence(ctx context.Context, address string) (uint64, uint64) { + resp, err := httpGet(ctx, LCD+"/cosmos/auth/v1beta1/accounts/"+address) + if err != nil { + log.Printf("Failed to get initial sequence: %v", err) + return 0, 0 + } + + var accountRes AccountResult + err = json.Unmarshal(resp, &accountRes) + if err != nil { + log.Printf("Failed to unmarshal account result: %v", err) + return 0, 0 + } + + seqint, err := strconv.ParseUint(accountRes.Account.Sequence, 10, 64) + if err != nil { + log.Printf("Failed to convert sequence to int: %v", err) + return 0, 0 + } + + accnum, err := strconv.ParseUint(accountRes.Account.AccountNumber, 10, 64) + if err != nil { + log.Printf("Failed to convert account number to int: %v", err) + return 0, 0 + } + + return seqint, accnum +} + +var httpClient = &http.Client{ + Timeout: 10 * time.Second, // Adjusted timeout to 10 seconds + Transport: otelhttp.NewTransport(http.DefaultTransport), +} + +func httpGet(ctx context.Context, url string) ([]byte, error) { + ctx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, err + } + + resp, err := httpClient.Do(req) + if err != nil { + netErr, ok := err.(net.Error) + if ok && netErr.Timeout() { + log.Printf("Request to %s timed out, continuing...", url) + return nil, nil + } + return nil, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + return body, nil +} + +// broadcastTransaction broadcasts a transaction to the chain. +// Returning the result and error. +func sendTx(ctx context.Context, txBytes []byte) error { + // Create a connection to the gRPC server. + grpcConn, err := grpc.NewClient( + RPC, // Or your gRPC server address. + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + if err != nil { + return err + } + + defer grpcConn.Close() + + // Broadcast the tx via gRPC. We create a new client for the Protobuf Tx + // service. + txClient := txtypes.NewServiceClient(grpcConn) + // We then call the BroadcastTx method on this client. + grpcRes, err := txClient.BroadcastTx( + ctx, + &txtypes.BroadcastTxRequest{ + Mode: txtypes.BroadcastMode_BROADCAST_MODE_SYNC, + TxBytes: txBytes, // Proto-binary of the signed transaction, see previous step. + }, + ) + if err != nil { + return err + } + + fmt.Printf("claim TxResponse: %#v\n", grpcRes.TxResponse) // Should be `0` if the tx is successful + + return nil +} + +func (o *orderbookClaimerIngestPlugin) simulateMsgs(ctx context.Context, msgs []sdk.Msg) (*txtypes.SimulateResponse, uint64, error) { + grpcConn, err := grpc.NewClient( + RPC, // Or your gRPC server address. + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + if err != nil { + return nil, 0, err + } + + defer grpcConn.Close() + + accSeq, accNum := getInitialSequence(ctx, o.keyring.GetAddress().String()) + + txFactory := tx.Factory{} + txFactory = txFactory.WithTxConfig(encodingConfig.TxConfig) + txFactory = txFactory.WithAccountNumber(accNum) + txFactory = txFactory.WithSequence(accSeq) + txFactory = txFactory.WithChainID(chainID) + txFactory = txFactory.WithGasAdjustment(1.02) + + // Estimate transaction + gasResult, adjustedGasUsed, err := tx.CalculateGas( + grpcConn, + txFactory, + msgs..., + ) + if err != nil { + return nil, adjustedGasUsed, err + } + + return gasResult, adjustedGasUsed, nil +} diff --git a/ingest/usecase/plugins/orderbookfiller/README.md b/ingest/usecase/plugins/orderbookfiller/README.md index 95cd2b342..4bb23a666 100644 --- a/ingest/usecase/plugins/orderbookfiller/README.md +++ b/ingest/usecase/plugins/orderbookfiller/README.md @@ -62,7 +62,7 @@ OSMOSIS_KEYRING_KEY_NAME=local.info To create your key: ```bash -osmosisd keys add local --kerying-backend test --recover +osmosisd keys add local --keyring-backend test --recover # Enter your mnemonic diff --git a/ingest/usecase/plugins/orderbookfiller/osmosis_swap.go b/ingest/usecase/plugins/orderbookfiller/osmosis_swap.go index f94436aa1..b918aedac 100644 --- a/ingest/usecase/plugins/orderbookfiller/osmosis_swap.go +++ b/ingest/usecase/plugins/orderbookfiller/osmosis_swap.go @@ -13,7 +13,6 @@ import ( "time" "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" - gogogrpc "github.com/cosmos/gogoproto/grpc" "go.uber.org/zap" cometrpc "github.com/cometbft/cometbft/rpc/client/http" @@ -324,7 +323,11 @@ func (o *orderbookFillerIngestPlugin) simulateMsgs(ctx context.Context, msgs []s txFactory = txFactory.WithGasAdjustment(1.02) // Estimate transaction - gasResult, adjustedGasUsed, err := CalculateGas(ctx, o.passthroughGRPCClient.GetChainGRPCClient(), txFactory, msgs...) + gasResult, adjustedGasUsed, err := tx.CalculateGas( + o.passthroughGRPCClient.GetChainGRPCClient(), + txFactory, + msgs..., + ) if err != nil { return nil, adjustedGasUsed, err } @@ -332,28 +335,6 @@ func (o *orderbookFillerIngestPlugin) simulateMsgs(ctx context.Context, msgs []s return gasResult, adjustedGasUsed, nil } -// CalculateGas simulates the execution of a transaction and returns the -// simulation response obtained by the query and the adjusted gas amount. -func CalculateGas( - ctx context.Context, - clientCtx gogogrpc.ClientConn, txf tx.Factory, msgs ...sdk.Msg, -) (*txtypes.SimulateResponse, uint64, error) { - txBytes, err := txf.BuildSimTx(msgs...) - if err != nil { - return nil, 0, err - } - - txSvcClient := txtypes.NewServiceClient(clientCtx) - simRes, err := txSvcClient.Simulate(ctx, &txtypes.SimulateRequest{ - TxBytes: txBytes, - }) - if err != nil { - return nil, 0, err - } - - return simRes, uint64(txf.GasAdjustment() * float64(simRes.GasInfo.GasUsed)), nil -} - // broadcastTransaction broadcasts a transaction to the chain. // Returning the result and error. func broadcastTransaction(ctx context.Context, txBytes []byte, rpcEndpoint string) (*coretypes.ResultBroadcastTx, error) { From 801f9f41c6b499e7f98dadba9c8f72f9a8968bd2 Mon Sep 17 00:00:00 2001 From: Deividas Petraitis Date: Tue, 8 Oct 2024 17:32:30 +0300 Subject: [PATCH 03/33] BE-586 | WIP --- .../grpcclient/orderbook_grpc_client.go | 1 + grpc/grpc.go | 58 +++++++++++++++ .../ordebook_filler_ingest_plugin.go | 24 ++++--- ingest/usecase/plugins/orderbookclaimer/tx.go | 70 ++++++++++++------- slices/slices.go | 16 +++++ 5 files changed, 131 insertions(+), 38 deletions(-) create mode 100644 grpc/grpc.go create mode 100644 slices/slices.go diff --git a/domain/orderbook/grpcclient/orderbook_grpc_client.go b/domain/orderbook/grpcclient/orderbook_grpc_client.go index 33a3cbfc3..04e28332c 100644 --- a/domain/orderbook/grpcclient/orderbook_grpc_client.go +++ b/domain/orderbook/grpcclient/orderbook_grpc_client.go @@ -88,6 +88,7 @@ func (o *orderbookClientImpl) GetTickUnrealizedCancels(ctx context.Context, cont func (o *orderbookClientImpl) FetchTickUnrealizedCancels(ctx context.Context, chunkSize int, contractAddress string, tickIDs []int64) ([]UnrealizedTickCancels, error) { allUnrealizedCancels := make([]UnrealizedTickCancels, 0, len(tickIDs)) + // TODO: use slices.Split package for i := 0; i < len(tickIDs); i += chunkSize { end := i + chunkSize if end > len(tickIDs) { diff --git a/grpc/grpc.go b/grpc/grpc.go new file mode 100644 index 000000000..5141bb440 --- /dev/null +++ b/grpc/grpc.go @@ -0,0 +1,58 @@ +package grpc + +import ( + "fmt" + + proto "github.com/cosmos/gogoproto/proto" + + "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/encoding" +) + +type customCodec struct { + parentCodec encoding.Codec +} + +func (c customCodec) Marshal(v interface{}) ([]byte, error) { + protoMsg, ok := v.(proto.Message) + if !ok { + return nil, fmt.Errorf("failed to assert proto.Message") + } + return proto.Marshal(protoMsg) +} + +func (c customCodec) Unmarshal(data []byte, v interface{}) error { + protoMsg, ok := v.(proto.Message) + if !ok { + return fmt.Errorf("failed to assert proto.Message") + } + return proto.Unmarshal(data, protoMsg) +} + +func (c customCodec) Name() string { + return "gogoproto" +} + +// connectGRPC dials up our grpc connection endpoint. +// See: https://github.com/cosmos/cosmos-sdk/issues/18430 +func NewClient(grpcEndpoint string) (*grpc.ClientConn, error) { + customCodec := &customCodec{parentCodec: encoding.GetCodec("proto")} + + grpcOpts := []grpc.DialOption{ + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithStatsHandler(otelgrpc.NewClientHandler()), + grpc.WithDefaultCallOptions(grpc.ForceCodec(customCodec)), + } + + grpcConn, err := grpc.NewClient( + grpcEndpoint, + grpcOpts..., + ) + if err != nil { + return nil, fmt.Errorf("failed to dial Cosmos gRPC service: %w", err) + } + + return grpcConn, nil +} diff --git a/ingest/usecase/plugins/orderbookclaimer/ordebook_filler_ingest_plugin.go b/ingest/usecase/plugins/orderbookclaimer/ordebook_filler_ingest_plugin.go index 823c2c89d..cdb7c8419 100644 --- a/ingest/usecase/plugins/orderbookclaimer/ordebook_filler_ingest_plugin.go +++ b/ingest/usecase/plugins/orderbookclaimer/ordebook_filler_ingest_plugin.go @@ -14,6 +14,7 @@ import ( orderbookplugindomain "github.com/osmosis-labs/sqs/domain/orderbook/plugin" passthroughdomain "github.com/osmosis-labs/sqs/domain/passthrough" "github.com/osmosis-labs/sqs/log" + "github.com/osmosis-labs/sqs/slices" "go.opentelemetry.io/otel" "go.uber.org/zap" ) @@ -107,23 +108,24 @@ func (o *orderbookClaimerIngestPlugin) ProcessEndBlock(ctx context.Context, bloc var claimable orderbookdomain.Orders claimable = append(claimable, o.getClaimableOrders(orderbook, orders.OrderByDirection("ask"), t.TickState.AskValues)...) claimable = append(claimable, o.getClaimableOrders(orderbook, orders.OrderByDirection("bid"), t.TickState.BidValues)...) - if len(claimable) == 0 { continue // nothing to do } - var claims []Claim - for _, order := range claimable { - claims = append(claims, Claim{ - TickID: order.TickId, - OrderID: order.OrderId, - }) + // Chunk claimable orders of size 100 + for _, chunk := range slices.Split(claimable, 100) { + var claims []Claim + for _, order := range chunk { + claims = append(claims, Claim{ + TickID: order.TickId, + OrderID: order.OrderId, + }) + } - break - } + err = o.sendBatchClaimTx(orderbook.ContractAddress, claims) - err = o.sendBatchClaimTx(orderbook.ContractAddress, claims) - fmt.Println("claims", claims, err) + fmt.Println("claims", orderbook.ContractAddress, claims, err) + } break } diff --git a/ingest/usecase/plugins/orderbookclaimer/tx.go b/ingest/usecase/plugins/orderbookclaimer/tx.go index ebe92c94d..c98273d80 100644 --- a/ingest/usecase/plugins/orderbookclaimer/tx.go +++ b/ingest/usecase/plugins/orderbookclaimer/tx.go @@ -14,17 +14,19 @@ import ( "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" - // "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/tx" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/tx/signing" authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + txfeestypes "github.com/osmosis-labs/osmosis/v26/x/txfees/types" + txtypes "github.com/cosmos/cosmos-sdk/types/tx" - "github.com/osmosis-labs/osmosis/osmomath" "github.com/osmosis-labs/osmosis/v26/app" + sqsgcp "github.com/osmosis-labs/sqs/grpc" + "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ) @@ -32,15 +34,21 @@ import ( var ( chainID = "osmosis-1" - RPC = "localhost:9090" - LCD = "http://127.0.0.1:1317" - Denom = "uosmo" - NobleUSDC = "ibc/498A0751C798A0D9A389AA3691123DADA57DAA4FE165D5C75894505B876BA6E4" + RPC = "localhost:9090" + LCD = "http://127.0.0.1:1317" + Denom = "uosmo" encodingConfig = app.MakeEncodingConfig() ) func (o *orderbookClaimerIngestPlugin) sendBatchClaimTx(contractAddress string, claims []Claim) error { + // Create a connection to the gRPC server. + grpcConn, err := sqsgcp.NewClient(RPC) + if err != nil { + return err + } + + defer grpcConn.Close() key := o.keyring.GetKey() keyBytes := key.Bytes() privKey := &secp256k1.PrivKey{Key: keyBytes} @@ -87,22 +95,24 @@ func (o *orderbookClaimerIngestPlugin) sendBatchClaimTx(contractAddress string, return fmt.Errorf("failed to set messages: %w", err) } - // Query gas price from chain - gasPrice, err := osmomath.NewDecFromStr("0.025") // Example gas price, adjust as necessary - if err != nil { - return err - } + accountSequence, accountNumber := getInitialSequence(context.TODO(), o.keyring.GetAddress().String()) _, gas, err := o.simulateMsgs(context.TODO(), []sdk.Msg{&msg}) + fmt.Println("gas", gas, err) + + txFeeClient := txfeestypes.NewQueryClient(grpcConn) + resp, err := txFeeClient.BaseDenom(context.Background(), &txfeestypes.QueryBaseDenomRequest{}) + + fmt.Println("base denom", resp.BaseDenom, err) + + resp0, err := txFeeClient.GetEipBaseFee(context.TODO(), &txfeestypes.QueryEipBaseFeeRequest{}) + fmt.Printf("fee amount %#v : %s\n", resp0, err) // Calculate the fee based on gas and gas price - feeAmount := gasPrice.MulInt64(int64(gas)).Ceil().TruncateInt64() + feeAmount := resp0.BaseFee.MulInt64(int64(gas)).Ceil().TruncateInt() - fmt.Println("fee amount", feeAmount) // Create the final fee structure - feecoin := sdk.NewCoin("uosmo", osmomath.NewInt(feeAmount)) - - accountSequence, accountNumber := getInitialSequence(context.TODO(), o.keyring.GetAddress().String()) + feecoin := sdk.NewCoin(resp.BaseDenom, feeAmount) txBuilder.SetGasLimit(gas) txBuilder.SetFeeAmount(sdk.NewCoins(feecoin)) @@ -155,6 +165,11 @@ func (o *orderbookClaimerIngestPlugin) sendBatchClaimTx(contractAddress string, return err } + defer func() { + // Wait for block inclusion with buffer to avoid sequence mismatch + time.Sleep(5 * time.Second) + }() + err = sendTx(context.TODO(), txBytes) log.Println("txJSON", string(txJSONBytes), err) @@ -168,16 +183,6 @@ type Claim struct { OrderID int64 } -// Config should contain all the necessary configuration variables -var Config struct { - CHAIN_ID string - RPC_ENDPOINT string - TX_KEY string - TX_GAS int64 - TX_FEE_DENOM string - TX_FEE_AMOUNT int64 -} - type AccountInfo struct { Sequence string `json:"sequence"` AccountNumber string `json:"account_number"` @@ -188,6 +193,17 @@ type AccountResult struct { } func getInitialSequence(ctx context.Context, address string) (uint64, uint64) { + // Create a connection to the gRPC server. + grpcConn, err := grpc.NewClient( + RPC, // Or your gRPC server address. + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + if err != nil { + // return err + } + + defer grpcConn.Close() + resp, err := httpGet(ctx, LCD+"/cosmos/auth/v1beta1/accounts/"+address) if err != nil { log.Printf("Failed to get initial sequence: %v", err) @@ -300,7 +316,7 @@ func (o *orderbookClaimerIngestPlugin) simulateMsgs(ctx context.Context, msgs [] txFactory = txFactory.WithAccountNumber(accNum) txFactory = txFactory.WithSequence(accSeq) txFactory = txFactory.WithChainID(chainID) - txFactory = txFactory.WithGasAdjustment(1.02) + txFactory = txFactory.WithGasAdjustment(1.05) // Estimate transaction gasResult, adjustedGasUsed, err := tx.CalculateGas( diff --git a/slices/slices.go b/slices/slices.go new file mode 100644 index 000000000..ca5ca4675 --- /dev/null +++ b/slices/slices.go @@ -0,0 +1,16 @@ +package slices + +// Split splits slice into chunks of specified size. +func Split[T any](s []T, size int) [][]T { + var result [][]T + + for l := 0; l < len(s); l += size { + h := l + size + if h > len(s) { + h = len(s) + } + result = append(result, s[l:h]) + } + + return result +} From 53dc4b8c0d5557345ef072ad40ca6e92bfb6d087 Mon Sep 17 00:00:00 2001 From: Deividas Petraitis Date: Wed, 9 Oct 2024 17:12:26 +0300 Subject: [PATCH 04/33] BE-595 | Clean up --- delivery/http/http.go | 11 + domain/cosmos/account/types/types.go | 70 +++++ domain/cosmos/tx/tx.go | 171 ++++++++++++ {slices => domain/slices}/slices.go | 0 grpc/grpc.go | 58 ---- .../ordebook_filler_ingest_plugin.go | 16 +- ingest/usecase/plugins/orderbookclaimer/tx.go | 263 ++---------------- .../plugins/orderbookfiller/osmosis_swap.go | 1 + 8 files changed, 283 insertions(+), 307 deletions(-) create mode 100644 domain/cosmos/account/types/types.go create mode 100644 domain/cosmos/tx/tx.go rename {slices => domain/slices}/slices.go (100%) delete mode 100644 grpc/grpc.go diff --git a/delivery/http/http.go b/delivery/http/http.go index 82073dc33..5beeb6fab 100644 --- a/delivery/http/http.go +++ b/delivery/http/http.go @@ -1,10 +1,21 @@ package http import ( + "net/http" + "time" + "github.com/labstack/echo/v4" "github.com/osmosis-labs/sqs/validator" + + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" ) +// defaultClient represents default HTTP client for issuing outgoing HTTP requests. +var defaultClient = &http.Client{ + Timeout: 10 * time.Second, // Adjusted timeout to 10 seconds + Transport: otelhttp.NewTransport(http.DefaultTransport), +} + // RequestUnmarshaler is any type capable to unmarshal data from HTTP request to itself. type RequestUnmarshaler interface { UnmarshalHTTPRequest(c echo.Context) error diff --git a/domain/cosmos/account/types/types.go b/domain/cosmos/account/types/types.go new file mode 100644 index 000000000..b1fffab2a --- /dev/null +++ b/domain/cosmos/account/types/types.go @@ -0,0 +1,70 @@ +package types + +import ( + "context" + "encoding/json" + "strconv" + + "github.com/osmosis-labs/sqs/delivery/http" +) + +// QueryClient is the client API for Query service. +type QueryClient interface { + GetAccount(ctx context.Context, address string) (*QueryAccountResponse, error) +} + +func NewQueryClient(lcd string) QueryClient { + return &queryClient{lcd} +} + + +var _ QueryClient = &queryClient{} + +type queryClient struct { + lcd string +} + +func (c *queryClient) GetAccount(ctx context.Context, address string) (*QueryAccountResponse, error) { + resp, err := http.Get(ctx, c.lcd+"/cosmos/auth/v1beta1/accounts/"+address) + if err != nil { + return nil, err + } + + type queryAccountResponse struct { + Account struct { + Sequence string `json:"sequence"` + AccountNumber string `json:"account_number"` + } `json:"account"` + } + + var accountRes queryAccountResponse + err = json.Unmarshal(resp, &accountRes) + if err != nil { + return nil, err + } + + sequence, err := strconv.ParseUint(accountRes.Account.Sequence, 10, 64) + if err != nil { + return nil, err + } + + accountNumber, err := strconv.ParseUint(accountRes.Account.AccountNumber, 10, 64) + if err != nil { + return nil, err + } + + return &QueryAccountResponse{ + Account: Account{ + Sequence: sequence, + AccountNumber: accountNumber, + }, + }, nil +} + +type Account struct { + Sequence uint64 `json:"sequence"` + AccountNumber uint64 `json:"account_number"` +} +type QueryAccountResponse struct { + Account Account `json:"account"` +} diff --git a/domain/cosmos/tx/tx.go b/domain/cosmos/tx/tx.go new file mode 100644 index 000000000..5e432eb2a --- /dev/null +++ b/domain/cosmos/tx/tx.go @@ -0,0 +1,171 @@ +package tx + +import ( + "context" + + cosmosClient "github.com/cosmos/cosmos-sdk/client" + + "github.com/cosmos/cosmos-sdk/client/tx" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdk "github.com/cosmos/cosmos-sdk/types" + txtypes "github.com/cosmos/cosmos-sdk/types/tx" + signingtypes "github.com/cosmos/cosmos-sdk/types/tx/signing" + authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" + "github.com/osmosis-labs/osmosis/osmomath" + txfeestypes "github.com/osmosis-labs/osmosis/v26/x/txfees/types" + "github.com/osmosis-labs/sqs/delivery/grpc" + "github.com/osmosis-labs/sqs/domain/keyring" + + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + + "github.com/osmosis-labs/osmosis/v26/app/params" +) + +type Account struct { + Sequence uint64 + AccountNumber uint64 +} + +// TODO: +// SimulateMsgs +func SimulateMsgs( + grpcClient *grpc.Client, + encodingConfig params.EncodingConfig, + account Account, + chainID string, + msgs []sdk.Msg, +) (*txtypes.SimulateResponse, uint64, error) { + txFactory := tx.Factory{} + txFactory = txFactory.WithTxConfig(encodingConfig.TxConfig) + txFactory = txFactory.WithAccountNumber(account.AccountNumber) + txFactory = txFactory.WithSequence(account.Sequence) + txFactory = txFactory.WithChainID(chainID) + txFactory = txFactory.WithGasAdjustment(1.05) + + // Estimate transaction + gasResult, adjustedGasUsed, err := tx.CalculateGas( + grpcClient, + txFactory, + msgs..., + ) + if err != nil { + return nil, adjustedGasUsed, err + } + + return gasResult, adjustedGasUsed, nil +} + +func BuildTx(ctx context.Context,grpcClient *grpc.Client, keyring keyring.Keyring, encodingConfig params.EncodingConfig, account Account, chainID string, msg ...sdk.Msg) (cosmosClient.TxBuilder, error) { + key := keyring.GetKey() + privKey := &secp256k1.PrivKey{Key: key.Bytes()} + + // Create and sign the transaction + txBuilder := encodingConfig.TxConfig.NewTxBuilder() + + err := txBuilder.SetMsgs(msg...) + if err != nil { + return nil, err + } + + _, gas, err := SimulateMsgs( + grpcClient, + encodingConfig, + account, + chainID, + msg, + ) + if err != nil { + return nil, err + } + txBuilder.SetGasLimit(gas) + + feecoin, err := CalculateFeeCoin(ctx, grpcClient, gas) + if err != nil { + return nil, err + } + + txBuilder.SetFeeAmount(sdk.NewCoins(feecoin)) + + sigV2 := BuildSignatures(privKey.PubKey(), nil, account.Sequence) + err = txBuilder.SetSignatures(sigV2) + if err != nil { + return nil, err + } + + signerData := BuildSignerData(chainID, account.AccountNumber, account.Sequence) + + signed, err := tx.SignWithPrivKey( + ctx, + signingtypes.SignMode_SIGN_MODE_DIRECT, signerData, + txBuilder, privKey, encodingConfig.TxConfig, account.Sequence) + if err != nil { + return nil, err + } + + err = txBuilder.SetSignatures(signed) + if err != nil { + return nil, err + } + + return txBuilder, nil +} + +// SendTx broadcasts a transaction to the chain, returning the result and error. +func SendTx(ctx context.Context, grpcConn *grpc.Client, txBytes []byte) (*sdk.TxResponse, error) { + // Broadcast the tx via gRPC. We create a new client for the Protobuf Tx service. + txClient := txtypes.NewServiceClient(grpcConn) + + // We then call the BroadcastTx method on this client. + resp, err := txClient.BroadcastTx( + ctx, + &txtypes.BroadcastTxRequest{ + Mode: txtypes.BroadcastMode_BROADCAST_MODE_SYNC, + TxBytes: txBytes, // Proto-binary of the signed transaction + }, + ) + if err != nil { + return nil, err + } + + return resp.TxResponse, nil +} + +func BuildSignatures(publicKey cryptotypes.PubKey, signature []byte, sequence uint64) signingtypes.SignatureV2 { + return signingtypes.SignatureV2{ + PubKey: publicKey, + Data: &signingtypes.SingleSignatureData{ + SignMode: signingtypes.SignMode_SIGN_MODE_DIRECT, + Signature: signature, + }, + Sequence: sequence, + } +} +func BuildSignerData(chainID string, accountNumber, sequence uint64) authsigning.SignerData { + return authsigning.SignerData{ + ChainID: chainID, + AccountNumber: accountNumber, + Sequence: sequence, + } +} +func CalculateFeeCoin(ctx context.Context, grpcConn *grpc.Client, gas uint64) (sdk.Coin, error) { + client := txfeestypes.NewQueryClient(grpcConn) + + queryBaseDenomResponse, err := client.BaseDenom(ctx, &txfeestypes.QueryBaseDenomRequest{}) + if err != nil { + return sdk.Coin{}, err + } + + queryEipBaseFeeResponse, err := client.GetEipBaseFee(ctx, &txfeestypes.QueryEipBaseFeeRequest{}) + if err != nil { + return sdk.Coin{}, err + } + + feeAmount := CalculateFeeAmount(queryEipBaseFeeResponse.BaseFee, gas) + + return sdk.NewCoin(queryBaseDenomResponse.BaseDenom, feeAmount), nil +} + +// CalculateFeeAmount calculates the fee based on gas and gas price +func CalculateFeeAmount(baseFee osmomath.Dec, gas uint64) osmomath.Int { + return baseFee.MulInt64(int64(gas)).Ceil().TruncateInt() +} diff --git a/slices/slices.go b/domain/slices/slices.go similarity index 100% rename from slices/slices.go rename to domain/slices/slices.go diff --git a/grpc/grpc.go b/grpc/grpc.go deleted file mode 100644 index 5141bb440..000000000 --- a/grpc/grpc.go +++ /dev/null @@ -1,58 +0,0 @@ -package grpc - -import ( - "fmt" - - proto "github.com/cosmos/gogoproto/proto" - - "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" - "google.golang.org/grpc/encoding" -) - -type customCodec struct { - parentCodec encoding.Codec -} - -func (c customCodec) Marshal(v interface{}) ([]byte, error) { - protoMsg, ok := v.(proto.Message) - if !ok { - return nil, fmt.Errorf("failed to assert proto.Message") - } - return proto.Marshal(protoMsg) -} - -func (c customCodec) Unmarshal(data []byte, v interface{}) error { - protoMsg, ok := v.(proto.Message) - if !ok { - return fmt.Errorf("failed to assert proto.Message") - } - return proto.Unmarshal(data, protoMsg) -} - -func (c customCodec) Name() string { - return "gogoproto" -} - -// connectGRPC dials up our grpc connection endpoint. -// See: https://github.com/cosmos/cosmos-sdk/issues/18430 -func NewClient(grpcEndpoint string) (*grpc.ClientConn, error) { - customCodec := &customCodec{parentCodec: encoding.GetCodec("proto")} - - grpcOpts := []grpc.DialOption{ - grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithStatsHandler(otelgrpc.NewClientHandler()), - grpc.WithDefaultCallOptions(grpc.ForceCodec(customCodec)), - } - - grpcConn, err := grpc.NewClient( - grpcEndpoint, - grpcOpts..., - ) - if err != nil { - return nil, fmt.Errorf("failed to dial Cosmos gRPC service: %w", err) - } - - return grpcConn, nil -} diff --git a/ingest/usecase/plugins/orderbookclaimer/ordebook_filler_ingest_plugin.go b/ingest/usecase/plugins/orderbookclaimer/ordebook_filler_ingest_plugin.go index cdb7c8419..128f1c8fe 100644 --- a/ingest/usecase/plugins/orderbookclaimer/ordebook_filler_ingest_plugin.go +++ b/ingest/usecase/plugins/orderbookclaimer/ordebook_filler_ingest_plugin.go @@ -6,15 +6,17 @@ import ( "sync/atomic" "github.com/osmosis-labs/osmosis/osmomath" + "github.com/osmosis-labs/sqs/delivery/grpc" "github.com/osmosis-labs/sqs/domain" + accounttypes "github.com/osmosis-labs/sqs/domain/cosmos/account/types" "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" orderbookplugindomain "github.com/osmosis-labs/sqs/domain/orderbook/plugin" passthroughdomain "github.com/osmosis-labs/sqs/domain/passthrough" + "github.com/osmosis-labs/sqs/domain/slices" "github.com/osmosis-labs/sqs/log" - "github.com/osmosis-labs/sqs/slices" "go.opentelemetry.io/otel" "go.uber.org/zap" ) @@ -27,7 +29,9 @@ type orderbookClaimerIngestPlugin struct { orderbookRepository orderbookdomain.OrderBookRepository orderBookClient orderbookgrpcclientdomain.OrderBookClient - atomicBool atomic.Bool + accountQueryClient accounttypes.QueryClient + grpcClient *grpc.Client + atomicBool atomic.Bool logger log.Logger } @@ -52,7 +56,15 @@ func New( orderBookCWAPIClient orderbookplugindomain.OrderbookCWAPIClient, logger log.Logger, ) *orderbookClaimerIngestPlugin { + // Create a connection to the gRPC server. + grpcClient, err := grpc.NewClient(RPC) + if err != nil { + logger.Error("err", zap.Error(err)) // TODO + } + return &orderbookClaimerIngestPlugin{ + accountQueryClient: accounttypes.NewQueryClient(LCD), // TODO: as param + grpcClient: grpcClient, keyring: keyring, orderbookusecase: orderbookusecase, orderbookRepository: orderbookRepository, diff --git a/ingest/usecase/plugins/orderbookclaimer/tx.go b/ingest/usecase/plugins/orderbookclaimer/tx.go index c98273d80..800fb16db 100644 --- a/ingest/usecase/plugins/orderbookclaimer/tx.go +++ b/ingest/usecase/plugins/orderbookclaimer/tx.go @@ -4,31 +4,15 @@ import ( "context" "encoding/json" "fmt" - "io" "log" - "net" - "net/http" - "strconv" "time" - "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" - wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" - "github.com/cosmos/cosmos-sdk/client/tx" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/types/tx/signing" - authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" - "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" - - txfeestypes "github.com/osmosis-labs/osmosis/v26/x/txfees/types" - txtypes "github.com/cosmos/cosmos-sdk/types/tx" "github.com/osmosis-labs/osmosis/v26/app" - sqsgcp "github.com/osmosis-labs/sqs/grpc" - - "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" + sqstx "github.com/osmosis-labs/sqs/domain/cosmos/tx" ) var ( @@ -42,21 +26,11 @@ var ( ) func (o *orderbookClaimerIngestPlugin) sendBatchClaimTx(contractAddress string, claims []Claim) error { - // Create a connection to the gRPC server. - grpcConn, err := sqsgcp.NewClient(RPC) + account, err := o.accountQueryClient.GetAccount(context.TODO(), o.keyring.GetAddress().String()) if err != nil { - return err + // TODO } - defer grpcConn.Close() - key := o.keyring.GetKey() - keyBytes := key.Bytes() - privKey := &secp256k1.PrivKey{Key: keyBytes} - - account := o.keyring - - txConfig := encodingConfig.TxConfig - // Prepare the message orders := make([][]int64, len(claims)) for i, claim := range claims { @@ -80,101 +54,44 @@ func (o *orderbookClaimerIngestPlugin) sendBatchClaimTx(contractAddress string, return fmt.Errorf("failed to marshal message: %w", err) } - // Create and sign the transaction - txBuilder := txConfig.NewTxBuilder() - msg := wasmtypes.MsgExecuteContract{ - Sender: account.GetAddress().String(), + Sender: o.keyring.GetAddress().String(), Contract: contractAddress, Msg: msgBytes, Funds: sdk.NewCoins(), } - err = txBuilder.SetMsgs(&msg) - if err != nil { - return fmt.Errorf("failed to set messages: %w", err) - } - - accountSequence, accountNumber := getInitialSequence(context.TODO(), o.keyring.GetAddress().String()) - - _, gas, err := o.simulateMsgs(context.TODO(), []sdk.Msg{&msg}) - fmt.Println("gas", gas, err) - - txFeeClient := txfeestypes.NewQueryClient(grpcConn) - resp, err := txFeeClient.BaseDenom(context.Background(), &txfeestypes.QueryBaseDenomRequest{}) - - fmt.Println("base denom", resp.BaseDenom, err) - - resp0, err := txFeeClient.GetEipBaseFee(context.TODO(), &txfeestypes.QueryEipBaseFeeRequest{}) - fmt.Printf("fee amount %#v : %s\n", resp0, err) - - // Calculate the fee based on gas and gas price - feeAmount := resp0.BaseFee.MulInt64(int64(gas)).Ceil().TruncateInt() - - // Create the final fee structure - feecoin := sdk.NewCoin(resp.BaseDenom, feeAmount) - - txBuilder.SetGasLimit(gas) - txBuilder.SetFeeAmount(sdk.NewCoins(feecoin)) - - signMode := encodingConfig.TxConfig.SignModeHandler().DefaultMode() - protoSignMode, _ := authsigning.APISignModeToInternal(signMode) - - sigV2 := signing.SignatureV2{ - PubKey: privKey.PubKey(), - Data: &signing.SingleSignatureData{ - SignMode: protoSignMode, - Signature: nil, - }, - Sequence: accountSequence, - } - - err = txBuilder.SetSignatures(sigV2) - if err != nil { - return fmt.Errorf("failed to set signatures: %w", err) - } - - signerData := authsigning.SignerData{ - ChainID: chainID, - AccountNumber: accountNumber, - Sequence: accountSequence, - } - - signed, err := tx.SignWithPrivKey( - context.TODO(), - protoSignMode, signerData, - txBuilder, privKey, encodingConfig.TxConfig, accountSequence) - if err != nil { - return fmt.Errorf("failed to sing transaction: %w", err) - } + tx, err := sqstx.BuildTx(context.TODO(), o.grpcClient, o.keyring, encodingConfig, sqstx.Account{ + Sequence: account.Account.Sequence, + AccountNumber: account.Account.AccountNumber, + }, chainID, &msg) - err = txBuilder.SetSignatures(signed) - if err != nil { - return fmt.Errorf("failed to set signatures: %w", err) - } // Broadcast the transaction - txBytes, err := txConfig.TxEncoder()(txBuilder.GetTx()) + txBytes, err := encodingConfig.TxConfig.TxEncoder()(tx.GetTx()) if err != nil { return fmt.Errorf("failed to encode transaction: %w", err) } // Generate a JSON string. - txJSONBytes, err := encodingConfig.TxConfig.TxJSONEncoder()(txBuilder.GetTx()) + txJSONBytes, err := encodingConfig.TxConfig.TxJSONEncoder()(tx.GetTx()) if err != nil { return err } + log.Println("txJSON", string(txJSONBytes), err) + defer func() { // Wait for block inclusion with buffer to avoid sequence mismatch time.Sleep(5 * time.Second) }() - err = sendTx(context.TODO(), txBytes) + txresp, err := sqstx.SendTx(context.TODO(), o.grpcClient, txBytes) + + log.Printf("txres %#v : %s", txresp, err) - log.Println("txJSON", string(txJSONBytes), err) - return nil + return nil // TODO } // You'll need to define the Claim struct and Config variables @@ -182,151 +99,3 @@ type Claim struct { TickID int64 OrderID int64 } - -type AccountInfo struct { - Sequence string `json:"sequence"` - AccountNumber string `json:"account_number"` -} - -type AccountResult struct { - Account AccountInfo `json:"account"` -} - -func getInitialSequence(ctx context.Context, address string) (uint64, uint64) { - // Create a connection to the gRPC server. - grpcConn, err := grpc.NewClient( - RPC, // Or your gRPC server address. - grpc.WithTransportCredentials(insecure.NewCredentials()), - ) - if err != nil { - // return err - } - - defer grpcConn.Close() - - resp, err := httpGet(ctx, LCD+"/cosmos/auth/v1beta1/accounts/"+address) - if err != nil { - log.Printf("Failed to get initial sequence: %v", err) - return 0, 0 - } - - var accountRes AccountResult - err = json.Unmarshal(resp, &accountRes) - if err != nil { - log.Printf("Failed to unmarshal account result: %v", err) - return 0, 0 - } - - seqint, err := strconv.ParseUint(accountRes.Account.Sequence, 10, 64) - if err != nil { - log.Printf("Failed to convert sequence to int: %v", err) - return 0, 0 - } - - accnum, err := strconv.ParseUint(accountRes.Account.AccountNumber, 10, 64) - if err != nil { - log.Printf("Failed to convert account number to int: %v", err) - return 0, 0 - } - - return seqint, accnum -} - -var httpClient = &http.Client{ - Timeout: 10 * time.Second, // Adjusted timeout to 10 seconds - Transport: otelhttp.NewTransport(http.DefaultTransport), -} - -func httpGet(ctx context.Context, url string) ([]byte, error) { - ctx, cancel := context.WithTimeout(ctx, 10*time.Second) - defer cancel() - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) - if err != nil { - return nil, err - } - - resp, err := httpClient.Do(req) - if err != nil { - netErr, ok := err.(net.Error) - if ok && netErr.Timeout() { - log.Printf("Request to %s timed out, continuing...", url) - return nil, nil - } - return nil, err - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - return body, nil -} - -// broadcastTransaction broadcasts a transaction to the chain. -// Returning the result and error. -func sendTx(ctx context.Context, txBytes []byte) error { - // Create a connection to the gRPC server. - grpcConn, err := grpc.NewClient( - RPC, // Or your gRPC server address. - grpc.WithTransportCredentials(insecure.NewCredentials()), - ) - if err != nil { - return err - } - - defer grpcConn.Close() - - // Broadcast the tx via gRPC. We create a new client for the Protobuf Tx - // service. - txClient := txtypes.NewServiceClient(grpcConn) - // We then call the BroadcastTx method on this client. - grpcRes, err := txClient.BroadcastTx( - ctx, - &txtypes.BroadcastTxRequest{ - Mode: txtypes.BroadcastMode_BROADCAST_MODE_SYNC, - TxBytes: txBytes, // Proto-binary of the signed transaction, see previous step. - }, - ) - if err != nil { - return err - } - - fmt.Printf("claim TxResponse: %#v\n", grpcRes.TxResponse) // Should be `0` if the tx is successful - - return nil -} - -func (o *orderbookClaimerIngestPlugin) simulateMsgs(ctx context.Context, msgs []sdk.Msg) (*txtypes.SimulateResponse, uint64, error) { - grpcConn, err := grpc.NewClient( - RPC, // Or your gRPC server address. - grpc.WithTransportCredentials(insecure.NewCredentials()), - ) - if err != nil { - return nil, 0, err - } - - defer grpcConn.Close() - - accSeq, accNum := getInitialSequence(ctx, o.keyring.GetAddress().String()) - - txFactory := tx.Factory{} - txFactory = txFactory.WithTxConfig(encodingConfig.TxConfig) - txFactory = txFactory.WithAccountNumber(accNum) - txFactory = txFactory.WithSequence(accSeq) - txFactory = txFactory.WithChainID(chainID) - txFactory = txFactory.WithGasAdjustment(1.05) - - // Estimate transaction - gasResult, adjustedGasUsed, err := tx.CalculateGas( - grpcConn, - txFactory, - msgs..., - ) - if err != nil { - return nil, adjustedGasUsed, err - } - - return gasResult, adjustedGasUsed, nil -} diff --git a/ingest/usecase/plugins/orderbookfiller/osmosis_swap.go b/ingest/usecase/plugins/orderbookfiller/osmosis_swap.go index b918aedac..cb7012734 100644 --- a/ingest/usecase/plugins/orderbookfiller/osmosis_swap.go +++ b/ingest/usecase/plugins/orderbookfiller/osmosis_swap.go @@ -312,6 +312,7 @@ func (o *orderbookFillerIngestPlugin) simulateSwapExactAmountIn(ctx blockctx.Blo return msgCtx, nil } +// TODO: func (o *orderbookFillerIngestPlugin) simulateMsgs(ctx context.Context, msgs []sdk.Msg) (*txtypes.SimulateResponse, uint64, error) { accSeq, accNum := getInitialSequence(ctx, o.keyring.GetAddress().String()) From d349b3f3d061c7ab8bb6a3f20bfc3fb2db890c13 Mon Sep 17 00:00:00 2001 From: Deividas Petraitis Date: Thu, 10 Oct 2024 08:54:24 +0300 Subject: [PATCH 05/33] BE-586 | Add docs --- domain/cosmos/account/types/types.go | 70 ------------------- domain/cosmos/tx/tx.go | 26 +++++-- domain/orderbook/order.go | 3 +- domain/slices/slices.go | 5 +- .../ordebook_filler_ingest_plugin.go | 6 +- ingest/usecase/plugins/orderbookclaimer/tx.go | 3 +- 6 files changed, 31 insertions(+), 82 deletions(-) delete mode 100644 domain/cosmos/account/types/types.go diff --git a/domain/cosmos/account/types/types.go b/domain/cosmos/account/types/types.go deleted file mode 100644 index b1fffab2a..000000000 --- a/domain/cosmos/account/types/types.go +++ /dev/null @@ -1,70 +0,0 @@ -package types - -import ( - "context" - "encoding/json" - "strconv" - - "github.com/osmosis-labs/sqs/delivery/http" -) - -// QueryClient is the client API for Query service. -type QueryClient interface { - GetAccount(ctx context.Context, address string) (*QueryAccountResponse, error) -} - -func NewQueryClient(lcd string) QueryClient { - return &queryClient{lcd} -} - - -var _ QueryClient = &queryClient{} - -type queryClient struct { - lcd string -} - -func (c *queryClient) GetAccount(ctx context.Context, address string) (*QueryAccountResponse, error) { - resp, err := http.Get(ctx, c.lcd+"/cosmos/auth/v1beta1/accounts/"+address) - if err != nil { - return nil, err - } - - type queryAccountResponse struct { - Account struct { - Sequence string `json:"sequence"` - AccountNumber string `json:"account_number"` - } `json:"account"` - } - - var accountRes queryAccountResponse - err = json.Unmarshal(resp, &accountRes) - if err != nil { - return nil, err - } - - sequence, err := strconv.ParseUint(accountRes.Account.Sequence, 10, 64) - if err != nil { - return nil, err - } - - accountNumber, err := strconv.ParseUint(accountRes.Account.AccountNumber, 10, 64) - if err != nil { - return nil, err - } - - return &QueryAccountResponse{ - Account: Account{ - Sequence: sequence, - AccountNumber: accountNumber, - }, - }, nil -} - -type Account struct { - Sequence uint64 `json:"sequence"` - AccountNumber uint64 `json:"account_number"` -} -type QueryAccountResponse struct { - Account Account `json:"account"` -} diff --git a/domain/cosmos/tx/tx.go b/domain/cosmos/tx/tx.go index 5e432eb2a..c370054f7 100644 --- a/domain/cosmos/tx/tx.go +++ b/domain/cosmos/tx/tx.go @@ -1,3 +1,4 @@ +// Package tx provides functionality for building, simulating, and sending Cosmos SDK transactions. package tx import ( @@ -21,13 +22,15 @@ import ( "github.com/osmosis-labs/osmosis/v26/app/params" ) +// Account represents the account information required for transaction building and signing. type Account struct { - Sequence uint64 - AccountNumber uint64 + Sequence uint64 // Current sequence (nonce) of the account, used to prevent replay attacks. + AccountNumber uint64 // Unique identifier of the account on the blockchain. } -// TODO: -// SimulateMsgs +// SimulateMsgs simulates the execution of the given messages and returns the simulation response, +// adjusted gas used, and any error encountered. It uses the provided gRPC client, encoding config, +// account details, and chain ID to create a transaction factory for the simulation. func SimulateMsgs( grpcClient *grpc.Client, encodingConfig params.EncodingConfig, @@ -55,7 +58,9 @@ func SimulateMsgs( return gasResult, adjustedGasUsed, nil } -func BuildTx(ctx context.Context,grpcClient *grpc.Client, keyring keyring.Keyring, encodingConfig params.EncodingConfig, account Account, chainID string, msg ...sdk.Msg) (cosmosClient.TxBuilder, error) { +// BuildTx constructs a transaction using the provided parameters and messages. +// Returns a TxBuilder and any error encountered. +func BuildTx(ctx context.Context, grpcClient *grpc.Client, keyring keyring.Keyring, encodingConfig params.EncodingConfig, account Account, chainID string, msg ...sdk.Msg) (cosmosClient.TxBuilder, error) { key := keyring.GetKey() privKey := &secp256k1.PrivKey{Key: key.Bytes()} @@ -130,6 +135,8 @@ func SendTx(ctx context.Context, grpcConn *grpc.Client, txBytes []byte) (*sdk.Tx return resp.TxResponse, nil } +// BuildSignatures creates a SignatureV2 object using the provided public key, signature, and sequence number. +// This is used in the process of building and signing transactions. func BuildSignatures(publicKey cryptotypes.PubKey, signature []byte, sequence uint64) signingtypes.SignatureV2 { return signingtypes.SignatureV2{ PubKey: publicKey, @@ -140,6 +147,9 @@ func BuildSignatures(publicKey cryptotypes.PubKey, signature []byte, sequence ui Sequence: sequence, } } + +// BuildSignerData creates a SignerData object with the given chain ID, account number, and sequence. +// This data is used in the process of signing transactions. func BuildSignerData(chainID string, accountNumber, sequence uint64) authsigning.SignerData { return authsigning.SignerData{ ChainID: chainID, @@ -147,6 +157,9 @@ func BuildSignerData(chainID string, accountNumber, sequence uint64) authsigning Sequence: sequence, } } + +// CalculateFeeCoin determines the appropriate fee coin for a transaction based on the current base fee +// and the amount of gas used. It queries the base denomination and EIP base fee using the provided gRPC connection. func CalculateFeeCoin(ctx context.Context, grpcConn *grpc.Client, gas uint64) (sdk.Coin, error) { client := txfeestypes.NewQueryClient(grpcConn) @@ -165,7 +178,8 @@ func CalculateFeeCoin(ctx context.Context, grpcConn *grpc.Client, gas uint64) (s return sdk.NewCoin(queryBaseDenomResponse.BaseDenom, feeAmount), nil } -// CalculateFeeAmount calculates the fee based on gas and gas price +// CalculateFeeAmount calculates the fee amount based on the base fee and gas used. +// It multiplies the base fee by the gas amount, rounds up to the nearest integer, and returns the result. func CalculateFeeAmount(baseFee osmomath.Dec, gas uint64) osmomath.Int { return baseFee.MulInt64(int64(gas)).Ceil().TruncateInt() } diff --git a/domain/orderbook/order.go b/domain/orderbook/order.go index 91601e310..fdc5862db 100644 --- a/domain/orderbook/order.go +++ b/domain/orderbook/order.go @@ -65,7 +65,8 @@ func (o Orders) TickID() []int64 { return tickIDs } -// TODO +// OrderByDirection filters orders by given direction and returns resulting slice. +// Original slice is not mutated. func (o Orders) OrderByDirection(direction string) Orders { var result Orders for _, v := range o { diff --git a/domain/slices/slices.go b/domain/slices/slices.go index ca5ca4675..e2e9364d7 100644 --- a/domain/slices/slices.go +++ b/domain/slices/slices.go @@ -1,6 +1,9 @@ +// Package slices provides utility functions for working with slices. package slices -// Split splits slice into chunks of specified size. +// Split splits given slice into chunks of specified size. +// Returns a slice of slices, where each inner slice is a chunk of the original slice of given size. +// The last chunk may be smaller than the specified size if the original slice length is not evenly divisible by the chunk size. func Split[T any](s []T, size int) [][]T { var result [][]T diff --git a/ingest/usecase/plugins/orderbookclaimer/ordebook_filler_ingest_plugin.go b/ingest/usecase/plugins/orderbookclaimer/ordebook_filler_ingest_plugin.go index 128f1c8fe..288dbff1e 100644 --- a/ingest/usecase/plugins/orderbookclaimer/ordebook_filler_ingest_plugin.go +++ b/ingest/usecase/plugins/orderbookclaimer/ordebook_filler_ingest_plugin.go @@ -8,7 +8,7 @@ import ( "github.com/osmosis-labs/osmosis/osmomath" "github.com/osmosis-labs/sqs/delivery/grpc" "github.com/osmosis-labs/sqs/domain" - accounttypes "github.com/osmosis-labs/sqs/domain/cosmos/account/types" + authtypes "github.com/osmosis-labs/sqs/domain/cosmos/auth/types" "github.com/osmosis-labs/sqs/domain/keyring" "github.com/osmosis-labs/sqs/domain/mvc" orderbookdomain "github.com/osmosis-labs/sqs/domain/orderbook" @@ -29,7 +29,7 @@ type orderbookClaimerIngestPlugin struct { orderbookRepository orderbookdomain.OrderBookRepository orderBookClient orderbookgrpcclientdomain.OrderBookClient - accountQueryClient accounttypes.QueryClient + accountQueryClient authtypes.QueryClient grpcClient *grpc.Client atomicBool atomic.Bool @@ -63,7 +63,7 @@ func New( } return &orderbookClaimerIngestPlugin{ - accountQueryClient: accounttypes.NewQueryClient(LCD), // TODO: as param + accountQueryClient: authtypes.NewQueryClient(LCD), // TODO: as param grpcClient: grpcClient, keyring: keyring, orderbookusecase: orderbookusecase, diff --git a/ingest/usecase/plugins/orderbookclaimer/tx.go b/ingest/usecase/plugins/orderbookclaimer/tx.go index 800fb16db..116dda126 100644 --- a/ingest/usecase/plugins/orderbookclaimer/tx.go +++ b/ingest/usecase/plugins/orderbookclaimer/tx.go @@ -91,9 +91,10 @@ func (o *orderbookClaimerIngestPlugin) sendBatchClaimTx(contractAddress string, log.Printf("txres %#v : %s", txresp, err) - return nil // TODO + return nil } +// TODO // You'll need to define the Claim struct and Config variables type Claim struct { TickID int64 From dbc37ec97c22ed13bf18b2f0cb8f02c8a9a3be24 Mon Sep 17 00:00:00 2001 From: Deividas Petraitis Date: Thu, 10 Oct 2024 17:09:31 +0300 Subject: [PATCH 06/33] BE-586 | Clean up --- Makefile | 8 +- app/sidecar_query_server.go | 13 +- delivery/grpc/grpc.go | 64 +++++++ delivery/http/get.go | 38 ++++ domain/cosmos/auth/types/types.go | 77 ++++++++ domain/mocks/orderbook_grpc_client_mock.go | 5 +- domain/mocks/orderbook_usecase_mock.go | 17 +- domain/mvc/orderbook.go | 2 +- domain/orderbook/order.go | 6 + .../claimbot}/.env | 0 .../claimbot}/README.md | 0 .../claimbot}/docker-compose.yml | 0 .../plugins/orderbook/claimbot/order.go | 179 ++++++++++++++++++ .../plugins/orderbook/claimbot/plugin.go | 167 ++++++++++++++++ .../usecase/plugins/orderbook/claimbot/tx.go | 125 ++++++++++++ .../fillbot}/README.md | 0 .../fillbot}/context/block/block_context.go | 2 +- .../fillbot}/context/msg/msg_context.go | 0 .../fillbot}/context/tx/tx_context.go | 2 +- .../fillbot}/create_copy_config.sh | 0 .../fillbot}/cyclic_arb.go | 6 +- .../fillbot}/docker-compose.yml | 0 .../fillbot}/fillable_orders.go | 4 +- .../fillbot}/ordebook_filler_ingest_plugin.go | 8 +- .../fillbot}/osmosis_swap.go | 10 +- .../fillbot}/tick_fetcher.go | 2 +- .../fillbot}/utils.go | 4 +- .../ordebook_filler_ingest_plugin.go | 178 ----------------- ingest/usecase/plugins/orderbookclaimer/tx.go | 102 ---------- ingest/usecase/plugins/orderbookfiller/.env | 4 - orderbook/usecase/orderbook_usecase_test.go | 89 ++++----- 31 files changed, 748 insertions(+), 364 deletions(-) create mode 100644 delivery/grpc/grpc.go create mode 100644 delivery/http/get.go create mode 100644 domain/cosmos/auth/types/types.go rename ingest/usecase/plugins/{orderbookclaimer => orderbook/claimbot}/.env (100%) rename ingest/usecase/plugins/{orderbookclaimer => orderbook/claimbot}/README.md (100%) rename ingest/usecase/plugins/{orderbookclaimer => orderbook/claimbot}/docker-compose.yml (100%) create mode 100644 ingest/usecase/plugins/orderbook/claimbot/order.go create mode 100644 ingest/usecase/plugins/orderbook/claimbot/plugin.go create mode 100644 ingest/usecase/plugins/orderbook/claimbot/tx.go rename ingest/usecase/plugins/{orderbookfiller => orderbook/fillbot}/README.md (100%) rename ingest/usecase/plugins/{orderbookfiller => orderbook/fillbot}/context/block/block_context.go (99%) rename ingest/usecase/plugins/{orderbookfiller => orderbook/fillbot}/context/msg/msg_context.go (100%) rename ingest/usecase/plugins/{orderbookfiller => orderbook/fillbot}/context/tx/tx_context.go (99%) rename ingest/usecase/plugins/{orderbookfiller => orderbook/fillbot}/create_copy_config.sh (100%) rename ingest/usecase/plugins/{orderbookfiller => orderbook/fillbot}/cyclic_arb.go (97%) rename ingest/usecase/plugins/{orderbookfiller => orderbook/fillbot}/docker-compose.yml (100%) rename ingest/usecase/plugins/{orderbookfiller => orderbook/fillbot}/fillable_orders.go (99%) rename ingest/usecase/plugins/{orderbookfiller => orderbook/fillbot}/ordebook_filler_ingest_plugin.go (99%) rename ingest/usecase/plugins/{orderbookfiller => orderbook/fillbot}/osmosis_swap.go (98%) rename ingest/usecase/plugins/{orderbookfiller => orderbook/fillbot}/tick_fetcher.go (97%) rename ingest/usecase/plugins/{orderbookfiller => orderbook/fillbot}/utils.go (98%) delete mode 100644 ingest/usecase/plugins/orderbookclaimer/ordebook_filler_ingest_plugin.go delete mode 100644 ingest/usecase/plugins/orderbookclaimer/tx.go delete mode 100644 ingest/usecase/plugins/orderbookfiller/.env diff --git a/Makefile b/Makefile index f1a24ac05..acf39ce2c 100644 --- a/Makefile +++ b/Makefile @@ -235,16 +235,16 @@ datadog-agent-start: # order fill bot configuration. # Starts node and SQS in the background. # - Starts DataDog service -# Use ./ingest/usecase/plugins/orderbookfiller/.env to configure the keyring. +# Use ./ingest/usecase/plugins/orderbook/fillbot/.env to configure the keyring. orderbook-filler-start: - ./ingest/usecase/plugins/orderbookfiller/create_copy_config.sh - cd ./ingest/usecase/plugins/orderbookfiller && docker compose up -d + ./ingest/usecase/plugins/orderbook/fillbot/create_copy_config.sh + cd ./ingest/usecase/plugins/orderbook/fillbot && docker compose up -d cd ../../../../ echo "Order Book Filler Bot Started" sleep 10 && osmosisd status sleep 10 && docker logs -f osmosis-sqs orderbook-filler-stop: - cd ./ingest/usecase/plugins/orderbookfiller && docker compose down + cd ./ingest/usecase/plugins/orderbook/fillbot && docker compose down cd ../../../../ echo "Order Book Filler Bot Stopped" diff --git a/app/sidecar_query_server.go b/app/sidecar_query_server.go index d634c82c4..95a62ec01 100644 --- a/app/sidecar_query_server.go +++ b/app/sidecar_query_server.go @@ -22,8 +22,8 @@ import ( ingestrpcdelivry "github.com/osmosis-labs/sqs/ingest/delivery/grpc" ingestusecase "github.com/osmosis-labs/sqs/ingest/usecase" - "github.com/osmosis-labs/sqs/ingest/usecase/plugins/orderbookclaimer" - "github.com/osmosis-labs/sqs/ingest/usecase/plugins/orderbookfiller" + orderbookclaimbot "github.com/osmosis-labs/sqs/ingest/usecase/plugins/orderbook/claimbot" + orderbookfillbot "github.com/osmosis-labs/sqs/ingest/usecase/plugins/orderbook/fillbot" orderbookrepository "github.com/osmosis-labs/sqs/orderbook/repository" orderbookusecase "github.com/osmosis-labs/sqs/orderbook/usecase" "github.com/osmosis-labs/sqs/sqsutil/datafetchers" @@ -280,7 +280,7 @@ func NewSideCarQueryServer(appCodec codec.Codec, config domain.Config, logger lo } logger.Info("Using keyring with address", zap.Stringer("address", keyring.GetAddress())) - currentPlugin = orderbookfiller.New(poolsUseCase, routerUsecase, tokensUseCase, passthroughGRPCClient, orderBookAPIClient, keyring, defaultQuoteDenom, logger) + currentPlugin = orderbookfillbot.New(poolsUseCase, routerUsecase, tokensUseCase, passthroughGRPCClient, orderBookAPIClient, keyring, defaultQuoteDenom, logger) } if plugin.GetName() == orderbookplugindomain.OrderBookClaimerPluginName { @@ -291,16 +291,17 @@ func NewSideCarQueryServer(appCodec codec.Codec, config domain.Config, logger lo } logger.Info("Using keyring with address", zap.Stringer("address", keyring.GetAddress())) - currentPlugin = orderbookclaimer.New( + currentPlugin, err = orderbookclaimbot.New( keyring, orderBookUseCase, poolsUseCase, orderBookRepository, orderBookAPIClient, - passthroughGRPCClient, - orderBookAPIClient, logger, ) + if err != nil { + return nil, err + } } // Register the plugin with the ingest use case diff --git a/delivery/grpc/grpc.go b/delivery/grpc/grpc.go new file mode 100644 index 000000000..d85e90373 --- /dev/null +++ b/delivery/grpc/grpc.go @@ -0,0 +1,64 @@ +package grpc + +import ( + "fmt" + + proto "github.com/cosmos/gogoproto/proto" + + "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/encoding" +) + +type customCodec struct { + parentCodec encoding.Codec +} + +func (c customCodec) Marshal(v interface{}) ([]byte, error) { + protoMsg, ok := v.(proto.Message) + if !ok { + return nil, fmt.Errorf("failed to assert proto.Message") + } + return proto.Marshal(protoMsg) +} + +func (c customCodec) Unmarshal(data []byte, v interface{}) error { + protoMsg, ok := v.(proto.Message) + if !ok { + return fmt.Errorf("failed to assert proto.Message") + } + return proto.Unmarshal(data, protoMsg) +} + +func (c customCodec) Name() string { + return "gogoproto" +} + +type Client struct { + *grpc.ClientConn +} + +// connectGRPC dials up our grpc connection endpoint. +// See: https://github.com/cosmos/cosmos-sdk/issues/18430 +func NewClient(grpcEndpoint string) (*Client, error) { + customCodec := &customCodec{parentCodec: encoding.GetCodec("proto")} + + grpcOpts := []grpc.DialOption{ + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithStatsHandler(otelgrpc.NewClientHandler()), + grpc.WithDefaultCallOptions(grpc.ForceCodec(customCodec)), + } + + grpcConn, err := grpc.NewClient( + grpcEndpoint, + grpcOpts..., + ) + if err != nil { + return nil, fmt.Errorf("failed to dial Cosmos gRPC service: %w", err) + } + + return &Client{ + ClientConn: grpcConn, + }, nil +} diff --git a/delivery/http/get.go b/delivery/http/get.go new file mode 100644 index 000000000..7c0b7f5a9 --- /dev/null +++ b/delivery/http/get.go @@ -0,0 +1,38 @@ +package http + +import ( + "context" + "io" + "log" + "net" + "net/http" + "time" +) + +// Get issues GET request to given URL using default httpClient. +func Get(ctx context.Context, url string) ([]byte, error) { + ctx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, err + } + + resp, err := defaultClient.Do(req) + if err != nil { + netErr, ok := err.(net.Error) + if ok && netErr.Timeout() { + log.Printf("Request to %s timed out, continuing...", url) + return nil, nil + } + return nil, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + return body, nil +} diff --git a/domain/cosmos/auth/types/types.go b/domain/cosmos/auth/types/types.go new file mode 100644 index 000000000..cbb402149 --- /dev/null +++ b/domain/cosmos/auth/types/types.go @@ -0,0 +1,77 @@ +// Package types provides types and client implementations for interacting with the Cosmos Auth module. +package types + +import ( + "context" + "encoding/json" + "strconv" + + "github.com/osmosis-labs/sqs/delivery/http" +) + +// QueryClient is the client API for Query service. +type QueryClient interface { + // GetAccount retrieves account information for a given address. + GetAccount(ctx context.Context, address string) (*QueryAccountResponse, error) +} + +// NewQueryClient creates a new QueryClient instance with the provided LCD (Light Client Daemon) endpoint. +func NewQueryClient(lcd string) QueryClient { + return &queryClient{lcd} +} + +var _ QueryClient = &queryClient{} + +// queryClient is an implementation of the QueryClient interface. +type queryClient struct { + lcd string +} + +// Account represents the basic account information. +type Account struct { + Sequence uint64 `json:"sequence"` // Current sequence (nonce) of the account, used to prevent replay attacks. + AccountNumber uint64 `json:"account_number"` // Unique identifier of the account on the blockchain. +} + +// QueryAccountResponse encapsulates the response for an account query. +type QueryAccountResponse struct { + Account Account `json:"account"` +} + +// GetAccount retrieves account information for a given address. +func (c *queryClient) GetAccount(ctx context.Context, address string) (*QueryAccountResponse, error) { + resp, err := http.Get(ctx, c.lcd+"/cosmos/auth/v1beta1/accounts/"+address) + if err != nil { + return nil, err + } + + type queryAccountResponse struct { + Account struct { + Sequence string `json:"sequence"` + AccountNumber string `json:"account_number"` + } `json:"account"` + } + + var accountRes queryAccountResponse + err = json.Unmarshal(resp, &accountRes) + if err != nil { + return nil, err + } + + sequence, err := strconv.ParseUint(accountRes.Account.Sequence, 10, 64) + if err != nil { + return nil, err + } + + accountNumber, err := strconv.ParseUint(accountRes.Account.AccountNumber, 10, 64) + if err != nil { + return nil, err + } + + return &QueryAccountResponse{ + Account: Account{ + Sequence: sequence, + AccountNumber: accountNumber, + }, + }, nil +} diff --git a/domain/mocks/orderbook_grpc_client_mock.go b/domain/mocks/orderbook_grpc_client_mock.go index ba66d489d..e708007da 100644 --- a/domain/mocks/orderbook_grpc_client_mock.go +++ b/domain/mocks/orderbook_grpc_client_mock.go @@ -5,14 +5,13 @@ import ( orderbookdomain "github.com/osmosis-labs/sqs/domain/orderbook" orderbookgrpcclientdomain "github.com/osmosis-labs/sqs/domain/orderbook/grpcclient" - orderbookplugindomain "github.com/osmosis-labs/sqs/domain/orderbook/plugin" ) var _ orderbookgrpcclientdomain.OrderBookClient = (*OrderbookGRPCClientMock)(nil) // OrderbookGRPCClientMock is a mock struct that implements orderbookplugindomain.OrderbookGRPCClient. type OrderbookGRPCClientMock struct { - GetOrdersByTickCb func(ctx context.Context, contractAddress string, tick int64) ([]orderbookplugindomain.Order, error) + GetOrdersByTickCb func(ctx context.Context, contractAddress string, tick int64) (orderbookdomain.Orders, error) GetActiveOrdersCb func(ctx context.Context, contractAddress string, ownerAddress string) (orderbookdomain.Orders, uint64, error) GetTickUnrealizedCancelsCb func(ctx context.Context, contractAddress string, tickIDs []int64) ([]orderbookgrpcclientdomain.UnrealizedTickCancels, error) FetchTickUnrealizedCancelsCb func(ctx context.Context, chunkSize int, contractAddress string, tickIDs []int64) ([]orderbookgrpcclientdomain.UnrealizedTickCancels, error) @@ -20,7 +19,7 @@ type OrderbookGRPCClientMock struct { FetchTicksCb func(ctx context.Context, chunkSize int, contractAddress string, tickIDs []int64) ([]orderbookdomain.Tick, error) } -func (o *OrderbookGRPCClientMock) GetOrdersByTick(ctx context.Context, contractAddress string, tick int64) ([]orderbookplugindomain.Order, error) { +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_usecase_mock.go b/domain/mocks/orderbook_usecase_mock.go index 65065d595..0f4065a1f 100644 --- a/domain/mocks/orderbook_usecase_mock.go +++ b/domain/mocks/orderbook_usecase_mock.go @@ -3,6 +3,7 @@ package mocks import ( "context" + "github.com/osmosis-labs/sqs/domain" "github.com/osmosis-labs/sqs/domain/mvc" orderbookdomain "github.com/osmosis-labs/sqs/domain/orderbook" "github.com/osmosis-labs/sqs/sqsdomain" @@ -12,10 +13,11 @@ var _ mvc.OrderBookUsecase = &OrderbookUsecaseMock{} // OrderbookUsecaseMock is a mock implementation of the RouterUsecase interface type OrderbookUsecaseMock struct { - ProcessPoolFunc func(ctx context.Context, pool sqsdomain.PoolI) error - GetAllTicksFunc func(poolID uint64) (map[int64]orderbookdomain.OrderbookTick, bool) - GetActiveOrdersFunc func(ctx context.Context, address string) ([]orderbookdomain.LimitOrder, bool, error) - GetActiveOrdersStreamFunc func(ctx context.Context, address string) <-chan orderbookdomain.OrderbookResult + ProcessPoolFunc func(ctx context.Context, pool sqsdomain.PoolI) error + GetAllTicksFunc func(poolID uint64) (map[int64]orderbookdomain.OrderbookTick, bool) + GetActiveOrdersFunc func(ctx context.Context, address string) ([]orderbookdomain.LimitOrder, bool, error) + GetActiveOrdersStreamFunc func(ctx context.Context, address string) <-chan orderbookdomain.OrderbookResult + CreateFormattedLimitOrderFunc func(orderbook domain.CanonicalOrderBooksResult, order orderbookdomain.Order) (orderbookdomain.LimitOrder, error) } func (m *OrderbookUsecaseMock) ProcessPool(ctx context.Context, pool sqsdomain.PoolI) error { @@ -45,3 +47,10 @@ func (m *OrderbookUsecaseMock) GetActiveOrdersStream(ctx context.Context, addres } panic("unimplemented") } + +func (m *OrderbookUsecaseMock) CreateFormattedLimitOrder(orderbook domain.CanonicalOrderBooksResult, order orderbookdomain.Order) (orderbookdomain.LimitOrder, error) { + if m.CreateFormattedLimitOrderFunc != nil { + return m.CreateFormattedLimitOrderFunc(orderbook, order) + } + panic("unimplemented") +} diff --git a/domain/mvc/orderbook.go b/domain/mvc/orderbook.go index 1f3a8b0e0..b565dded9 100644 --- a/domain/mvc/orderbook.go +++ b/domain/mvc/orderbook.go @@ -23,6 +23,6 @@ type OrderBookUsecase interface { // sender goroutines. GetActiveOrdersStream(ctx context.Context, address string) <-chan orderbookdomain.OrderbookResult - // TODO + // CreateFormattedLimitOrder creates a formatted limit order from the given orderbook and order. CreateFormattedLimitOrder(orderbook domain.CanonicalOrderBooksResult, order orderbookdomain.Order) (orderbookdomain.LimitOrder, error) } diff --git a/domain/orderbook/order.go b/domain/orderbook/order.go index fdc5862db..ceff8f763 100644 --- a/domain/orderbook/order.go +++ b/domain/orderbook/order.go @@ -106,6 +106,12 @@ type LimitOrder struct { PlacedTx *string `json:"placed_tx,omitempty"` } +// IsClaimable reports whether the limit order is filled above the given +// threshold to be considered as claimable. +func (o LimitOrder) IsClaimable(threshold osmomath.Dec) bool { + return o.PercentFilled.GT(threshold) && o.PercentFilled.LTE(osmomath.OneDec()) +} + // OrderbookResult represents orderbook orders result. type OrderbookResult struct { LimitOrders []LimitOrder // The channel on which the orders are delivered. diff --git a/ingest/usecase/plugins/orderbookclaimer/.env b/ingest/usecase/plugins/orderbook/claimbot/.env similarity index 100% rename from ingest/usecase/plugins/orderbookclaimer/.env rename to ingest/usecase/plugins/orderbook/claimbot/.env diff --git a/ingest/usecase/plugins/orderbookclaimer/README.md b/ingest/usecase/plugins/orderbook/claimbot/README.md similarity index 100% rename from ingest/usecase/plugins/orderbookclaimer/README.md rename to ingest/usecase/plugins/orderbook/claimbot/README.md diff --git a/ingest/usecase/plugins/orderbookclaimer/docker-compose.yml b/ingest/usecase/plugins/orderbook/claimbot/docker-compose.yml similarity index 100% rename from ingest/usecase/plugins/orderbookclaimer/docker-compose.yml rename to ingest/usecase/plugins/orderbook/claimbot/docker-compose.yml diff --git a/ingest/usecase/plugins/orderbook/claimbot/order.go b/ingest/usecase/plugins/orderbook/claimbot/order.go new file mode 100644 index 000000000..8a90cf3ff --- /dev/null +++ b/ingest/usecase/plugins/orderbook/claimbot/order.go @@ -0,0 +1,179 @@ +package claimbot + +import ( + "context" + "fmt" + + "github.com/osmosis-labs/osmosis/osmomath" + "github.com/osmosis-labs/sqs/domain" + "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" +) + +type order struct { + orderbook domain.CanonicalOrderBooksResult + orders orderbookdomain.Orders + err error +} + +// processOrderbooksAndGetClaimableOrders processes a list of orderbooks and returns claimable orders for each. +func processOrderbooksAndGetClaimableOrders( + ctx context.Context, + fillThreshold osmomath.Dec, + orderbooks []domain.CanonicalOrderBooksResult, + orderbookRepository orderbookdomain.OrderBookRepository, + orderBookClient orderbookgrpcclientdomain.OrderBookClient, + orderbookusecase mvc.OrderBookUsecase, + logger log.Logger, +) []order { + var result []order + for _, orderbook := range orderbooks { + processedOrder := processOrderbook(ctx, fillThreshold, orderbook, orderbookRepository, orderBookClient, orderbookusecase, logger) + result = append(result, processedOrder) + } + return result +} + +// processOrderbook processes a single orderbook and returns an order struct containing the processed orderbook and its claimable orders. +func processOrderbook( + ctx context.Context, + fillThreshold osmomath.Dec, + orderbook domain.CanonicalOrderBooksResult, + orderbookRepository orderbookdomain.OrderBookRepository, + orderBookClient orderbookgrpcclientdomain.OrderBookClient, + orderbookusecase mvc.OrderBookUsecase, + logger log.Logger, +) order { + claimable, err := getClaimableOrdersForOrderbook(ctx, fillThreshold, orderbook, orderbookRepository, orderBookClient, orderbookusecase, logger) + if err != nil { + return order{ + orderbook: orderbook, + err: err, + } + } + return order{ + orderbook: orderbook, + orders: claimable, + } +} + +// getClaimableOrdersForOrderbook retrieves all claimable orders for a given orderbook. +// It fetches all ticks for the orderbook, processes each tick to find claimable orders, +// and returns a combined list of all claimable orders across all ticks. +func getClaimableOrdersForOrderbook( + ctx context.Context, + fillThreshold osmomath.Dec, + orderbook domain.CanonicalOrderBooksResult, + orderbookRepository orderbookdomain.OrderBookRepository, + orderBookClient orderbookgrpcclientdomain.OrderBookClient, + orderbookusecase mvc.OrderBookUsecase, + logger log.Logger, +) (orderbookdomain.Orders, error) { + ticks, ok := orderbookRepository.GetAllTicks(orderbook.PoolID) + if !ok { + return nil, fmt.Errorf("no ticks for orderbook") + } + + var claimable orderbookdomain.Orders + for _, t := range ticks { + tickClaimable, err := getClaimableOrdersForTick(ctx, fillThreshold, orderbook, t, orderBookClient, orderbookusecase, logger) + if err != nil { + logger.Error("error processing tick", zap.String("orderbook", orderbook.ContractAddress), zap.Int64("tick", t.Tick.TickId), zap.Error(err)) + continue + } + claimable = append(claimable, tickClaimable...) + } + + return claimable, nil +} + +// getClaimableOrdersForTick retrieves claimable orders for a specific tick in an orderbook +// It processes all ask/bid direction orders and filters the orders that are claimable. +func getClaimableOrdersForTick( + ctx context.Context, + fillThreshold osmomath.Dec, + orderbook domain.CanonicalOrderBooksResult, + tick orderbookdomain.OrderbookTick, + orderBookClient orderbookgrpcclientdomain.OrderBookClient, + orderbookusecase mvc.OrderBookUsecase, + logger log.Logger, +) (orderbookdomain.Orders, error) { + orders, err := orderBookClient.GetOrdersByTick(ctx, orderbook.ContractAddress, tick.Tick.TickId) + if err != nil { + return nil, fmt.Errorf("unable to fetch orderbook orders by tick ID: %w", err) + } + + if len(orders) == 0 { + return nil, nil + } + + askClaimable := getClaimableOrders(orderbook, orders.OrderByDirection("ask"), tick.TickState.AskValues, fillThreshold, orderbookusecase, logger) + bidClaimable := getClaimableOrders(orderbook, orders.OrderByDirection("bid"), tick.TickState.BidValues, fillThreshold, orderbookusecase, logger) + + return append(askClaimable, bidClaimable...), nil +} + +// getClaimableOrders determines which orders are claimable for a given direction (ask or bid) in a tick. +// If the tick is fully filled, all orders are considered claimable. Otherwise, it filters the orders +// based on the fill threshold. +func getClaimableOrders( + orderbook domain.CanonicalOrderBooksResult, + orders orderbookdomain.Orders, + tickValues orderbookdomain.TickValues, + fillThreshold osmomath.Dec, + orderbookusecase mvc.OrderBookUsecase, + logger log.Logger, +) orderbookdomain.Orders { + 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 { + return tickValues.CumulativeTotalValue == tickValues.EffectiveTotalAmountSwapped +} + +// filterClaimableOrders processes a list of orders and returns only those that are considered claimable. +func filterClaimableOrders( + orderbook domain.CanonicalOrderBooksResult, + orders orderbookdomain.Orders, + fillThreshold osmomath.Dec, + orderbookusecase mvc.OrderBookUsecase, + logger log.Logger, +) orderbookdomain.Orders { + var claimable orderbookdomain.Orders + for _, order := range orders { + if isOrderClaimable(orderbook, order, fillThreshold, orderbookusecase, logger) { + claimable = append(claimable, order) + } + } + return claimable +} + +// isOrderClaimable determines if a single order is claimable based on the fill threshold. +func isOrderClaimable( + orderbook domain.CanonicalOrderBooksResult, + order orderbookdomain.Order, + fillThreshold osmomath.Dec, + orderbookusecase mvc.OrderBookUsecase, + logger log.Logger, +) bool { + result, err := orderbookusecase.CreateFormattedLimitOrder(orderbook, order) + if err != nil { + logger.Info( + "unable to create orderbook limit order; marking as not claimable", + zap.String("orderbook", orderbook.ContractAddress), + zap.Int64("order", order.OrderId), + zap.Error(err), + ) + return false + } + return result.IsClaimable(fillThreshold) +} diff --git a/ingest/usecase/plugins/orderbook/claimbot/plugin.go b/ingest/usecase/plugins/orderbook/claimbot/plugin.go new file mode 100644 index 000000000..3df71afa6 --- /dev/null +++ b/ingest/usecase/plugins/orderbook/claimbot/plugin.go @@ -0,0 +1,167 @@ +package claimbot + +import ( + "context" + "fmt" + "sync/atomic" + "time" + + "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" + "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/domain/slices" + "github.com/osmosis-labs/sqs/log" + "go.opentelemetry.io/otel" + "go.uber.org/zap" +) + +// claimbot is a claim bot that processes and claims eligible orderbook orders at the end of each block. +// Claimable orders are determined based on order filled percentage that is handled with fillThreshold package level variable. +type claimbot struct { + keyring keyring.Keyring + poolsUseCase mvc.PoolsUsecase + orderbookusecase mvc.OrderBookUsecase + orderbookRepository orderbookdomain.OrderBookRepository + orderBookClient orderbookgrpcclientdomain.OrderBookClient + + accountQueryClient authtypes.QueryClient + grpcClient *grpc.Client + atomicBool atomic.Bool + + logger log.Logger +} + +var _ domain.EndBlockProcessPlugin = &claimbot{} + +const ( + tracerName = "sqs-orderbook-claimer" +) + +var ( + tracer = otel.Tracer(tracerName) + fillThreshold = osmomath.MustNewDecFromStr("0.98") +) + +// New creates and returns a new claimbot instance. +func New( + keyring keyring.Keyring, + orderbookusecase mvc.OrderBookUsecase, + poolsUseCase mvc.PoolsUsecase, + orderbookRepository orderbookdomain.OrderBookRepository, + orderBookClient orderbookgrpcclientdomain.OrderBookClient, + logger log.Logger, +) (*claimbot, error) { + // Create a connection to the gRPC server. + grpcClient, err := grpc.NewClient(RPC) + if err != nil { + return nil, err + } + + return &claimbot{ + accountQueryClient: authtypes.NewQueryClient(LCD), + grpcClient: grpcClient, + keyring: keyring, + orderbookusecase: orderbookusecase, + orderbookRepository: orderbookRepository, + orderBookClient: orderBookClient, + poolsUseCase: poolsUseCase, + + atomicBool: atomic.Bool{}, + + logger: logger, + }, nil +} + +// ProcessEndBlock implements domain.EndBlockProcessPlugin. +// This method is called at the end of each block to process and claim eligible orderbook orders. +// ProcessEndBlock implements domain.EndBlockProcessPlugin. +func (o *claimbot) ProcessEndBlock(ctx context.Context, blockHeight uint64, metadata domain.BlockPoolMetadata) error { + ctx, span := tracer.Start(ctx, "orderbooktFillerIngestPlugin.ProcessEndBlock") + defer span.End() + + // For simplicity, we allow only one block to be processed at a time. + // This may be relaxed in the future. + if !o.atomicBool.CompareAndSwap(false, true) { + o.logger.Info("orderbook claimer is already in progress", zap.Uint64("block_height", blockHeight)) + return nil + } + defer o.atomicBool.Store(false) + + orderbooks, err := o.getOrderbooks(ctx, blockHeight, metadata) + if err != nil { + return err + } + + // retrieve claimable orders for the orderbooks + orders := processOrderbooksAndGetClaimableOrders( + ctx, + fillThreshold, + orderbooks, + o.orderbookRepository, + o.orderBookClient, + o.orderbookusecase, + o.logger, + ) + + for _, orderbook := range orders { + if orderbook.err != nil { + fmt.Println("step1 error", orderbook.err) + continue + } + + 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.Error(err), + ) + } + } + + o.logger.Info("processed end block in orderbook claimer ingest plugin", zap.Uint64("block_height", blockHeight)) + + return nil +} + +func (o *claimbot) processBatchClaimOrders(ctx context.Context, orderbook domain.CanonicalOrderBooksResult, orders orderbookdomain.Orders) error { + for _, chunk := range slices.Split(orders, 100) { + txres, err := sendBatchClaimTx( + ctx, + o.keyring, + o.grpcClient, + o.accountQueryClient, + orderbook.ContractAddress, + chunk, + ) + + if err != nil { + o.logger.Info( + "failed to sent batch claim tx", + zap.String("contract_address", orderbook.ContractAddress), + zap.Any("tx_result", txres), + zap.Error(err), + ) + } + + fmt.Println("claims", orderbook.ContractAddress, txres, chunk, err) + + // Wait for block inclusion with buffer to avoid sequence mismatch + time.Sleep(5 * time.Second) + } + + return nil +} + +// TODO: process only block orderbooks +func (o *claimbot) getOrderbooks(ctx context.Context, blockHeight uint64, metadata domain.BlockPoolMetadata) ([]domain.CanonicalOrderBooksResult, error) { + orderbooks, err := o.poolsUseCase.GetAllCanonicalOrderbookPoolIDs() + if err != nil { + return nil, fmt.Errorf("failed to get all canonical orderbook pool IDs : %w", err) + } + return orderbooks, nil +} diff --git a/ingest/usecase/plugins/orderbook/claimbot/tx.go b/ingest/usecase/plugins/orderbook/claimbot/tx.go new file mode 100644 index 000000000..180b0d5b5 --- /dev/null +++ b/ingest/usecase/plugins/orderbook/claimbot/tx.go @@ -0,0 +1,125 @@ +package claimbot + +import ( + "context" + "encoding/json" + "fmt" + "os" + + "github.com/osmosis-labs/sqs/delivery/grpc" + 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" + orderbookdomain "github.com/osmosis-labs/sqs/domain/orderbook" + + "github.com/osmosis-labs/osmosis/v26/app" + + wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +var ( + chainID = "osmosis-1" + + RPC = "localhost:9090" + LCD = "http://127.0.0.1:1317" + + encodingConfig = app.MakeEncodingConfig() +) + +// init overrides LCD and RPC endpoints from environment variables if those are set. +func init() { + if rpc := os.Getenv("OSMOSIS_RPC_ENDPOINT"); len(rpc) > 0 { + RPC = rpc + } + + if lcd := os.Getenv("OSMOSIS_LCD_ENDPOINT"); len(lcd) > 0 { + LCD = lcd + } +} + +// sendBatchClaimTx prepares and sends a batch claim transaction to the blockchain. +// It builds the transaction, signs it, and broadcasts it to the network. +func sendBatchClaimTx( + ctx context.Context, + keyring keyring.Keyring, + grpcClient *grpc.Client, + accountQueryClient authtypes.QueryClient, + contractAddress string, + claims orderbookdomain.Orders, +) (*sdk.TxResponse, error) { + address := keyring.GetAddress().String() + + account, err := getAccount(ctx, accountQueryClient, address) + if err != nil { + return nil, err + } + + msgBytes, err := prepareBatchClaimMsg(claims) + if err != nil { + return nil, err + } + + msg := buildExecuteContractMsg(address, contractAddress, msgBytes) + + tx, err := sqstx.BuildTx(ctx, grpcClient, keyring, encodingConfig, account, chainID, msg) + if err != nil { + return nil, fmt.Errorf("failed to build transaction: %w", err) + } + + txBytes, err := encodingConfig.TxConfig.TxEncoder()(tx.GetTx()) + if err != nil { + return nil, fmt.Errorf("failed to encode transaction: %w", err) + } + + // Broadcast the transaction + return sqstx.SendTx(ctx, grpcClient, txBytes) +} + +// getAccount retrieves account information for a given address. +func getAccount(ctx context.Context, accountQueryClient authtypes.QueryClient, address string) (sqstx.Account, error) { + account, err := accountQueryClient.GetAccount(ctx, address) + if err != nil { + return sqstx.Account{}, fmt.Errorf("failed to get account: %w", err) + } + return sqstx.Account{ + Sequence: account.Account.Sequence, + AccountNumber: account.Account.AccountNumber, + }, nil +} + +// prepareBatchClaimMsg creates a JSON-encoded batch claim message from the provided orders. +func prepareBatchClaimMsg(claims orderbookdomain.Orders) ([]byte, error) { + orders := make([][]int64, len(claims)) + for i, claim := range claims { + orders[i] = []int64{claim.TickId, claim.OrderId} + } + + batchClaim := struct { + BatchClaim struct { + Orders [][]int64 `json:"orders"` + } `json:"batch_claim"` + }{ + BatchClaim: struct { + Orders [][]int64 `json:"orders"` + }{ + Orders: orders, + }, + } + + msgBytes, err := json.Marshal(batchClaim) + if err != nil { + return nil, fmt.Errorf("failed to marshal message: %w", err) + } + return msgBytes, nil +} + +// buildExecuteContractMsg constructs a message for executing a smart contract. +func buildExecuteContractMsg(address, contractAddress string, msgBytes []byte) *wasmtypes.MsgExecuteContract { + return &wasmtypes.MsgExecuteContract{ + Sender: address, + Contract: contractAddress, + Msg: msgBytes, + Funds: sdk.NewCoins(), + } +} diff --git a/ingest/usecase/plugins/orderbookfiller/README.md b/ingest/usecase/plugins/orderbook/fillbot/README.md similarity index 100% rename from ingest/usecase/plugins/orderbookfiller/README.md rename to ingest/usecase/plugins/orderbook/fillbot/README.md diff --git a/ingest/usecase/plugins/orderbookfiller/context/block/block_context.go b/ingest/usecase/plugins/orderbook/fillbot/context/block/block_context.go similarity index 99% rename from ingest/usecase/plugins/orderbookfiller/context/block/block_context.go rename to ingest/usecase/plugins/orderbook/fillbot/context/block/block_context.go index 71ad7f773..0c90a4692 100644 --- a/ingest/usecase/plugins/orderbookfiller/context/block/block_context.go +++ b/ingest/usecase/plugins/orderbook/fillbot/context/block/block_context.go @@ -7,7 +7,7 @@ import ( "github.com/osmosis-labs/osmosis/osmomath" "github.com/osmosis-labs/sqs/domain" orderbookplugindomain "github.com/osmosis-labs/sqs/domain/orderbook/plugin" - txctx "github.com/osmosis-labs/sqs/ingest/usecase/plugins/orderbookfiller/context/tx" + txctx "github.com/osmosis-labs/sqs/ingest/usecase/plugins/orderbook/fillbot/context/tx" "google.golang.org/grpc" ) diff --git a/ingest/usecase/plugins/orderbookfiller/context/msg/msg_context.go b/ingest/usecase/plugins/orderbook/fillbot/context/msg/msg_context.go similarity index 100% rename from ingest/usecase/plugins/orderbookfiller/context/msg/msg_context.go rename to ingest/usecase/plugins/orderbook/fillbot/context/msg/msg_context.go diff --git a/ingest/usecase/plugins/orderbookfiller/context/tx/tx_context.go b/ingest/usecase/plugins/orderbook/fillbot/context/tx/tx_context.go similarity index 99% rename from ingest/usecase/plugins/orderbookfiller/context/tx/tx_context.go rename to ingest/usecase/plugins/orderbook/fillbot/context/tx/tx_context.go index 78c93a2ff..a31f00344 100644 --- a/ingest/usecase/plugins/orderbookfiller/context/tx/tx_context.go +++ b/ingest/usecase/plugins/orderbook/fillbot/context/tx/tx_context.go @@ -7,7 +7,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/osmosis-labs/osmosis/osmomath" poolmanagertypes "github.com/osmosis-labs/osmosis/v26/x/poolmanager/types" - msgctx "github.com/osmosis-labs/sqs/ingest/usecase/plugins/orderbookfiller/context/msg" + msgctx "github.com/osmosis-labs/sqs/ingest/usecase/plugins/orderbook/fillbot/context/msg" ) // TxContextI is an interface responsible for abstracting transaction context diff --git a/ingest/usecase/plugins/orderbookfiller/create_copy_config.sh b/ingest/usecase/plugins/orderbook/fillbot/create_copy_config.sh similarity index 100% rename from ingest/usecase/plugins/orderbookfiller/create_copy_config.sh rename to ingest/usecase/plugins/orderbook/fillbot/create_copy_config.sh diff --git a/ingest/usecase/plugins/orderbookfiller/cyclic_arb.go b/ingest/usecase/plugins/orderbook/fillbot/cyclic_arb.go similarity index 97% rename from ingest/usecase/plugins/orderbookfiller/cyclic_arb.go rename to ingest/usecase/plugins/orderbook/fillbot/cyclic_arb.go index 8f4332177..7e25db8dc 100644 --- a/ingest/usecase/plugins/orderbookfiller/cyclic_arb.go +++ b/ingest/usecase/plugins/orderbook/fillbot/cyclic_arb.go @@ -1,4 +1,4 @@ -package orderbookfiller +package fillbot import ( "fmt" @@ -6,8 +6,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/osmosis-labs/osmosis/osmomath" "github.com/osmosis-labs/sqs/domain" - blockctx "github.com/osmosis-labs/sqs/ingest/usecase/plugins/orderbookfiller/context/block" - msgctx "github.com/osmosis-labs/sqs/ingest/usecase/plugins/orderbookfiller/context/msg" + blockctx "github.com/osmosis-labs/sqs/ingest/usecase/plugins/orderbook/fillbot/context/block" + msgctx "github.com/osmosis-labs/sqs/ingest/usecase/plugins/orderbook/fillbot/context/msg" "go.uber.org/zap" ) diff --git a/ingest/usecase/plugins/orderbookfiller/docker-compose.yml b/ingest/usecase/plugins/orderbook/fillbot/docker-compose.yml similarity index 100% rename from ingest/usecase/plugins/orderbookfiller/docker-compose.yml rename to ingest/usecase/plugins/orderbook/fillbot/docker-compose.yml diff --git a/ingest/usecase/plugins/orderbookfiller/fillable_orders.go b/ingest/usecase/plugins/orderbook/fillbot/fillable_orders.go similarity index 99% rename from ingest/usecase/plugins/orderbookfiller/fillable_orders.go rename to ingest/usecase/plugins/orderbook/fillbot/fillable_orders.go index 1eb706d5d..e29978789 100644 --- a/ingest/usecase/plugins/orderbookfiller/fillable_orders.go +++ b/ingest/usecase/plugins/orderbook/fillbot/fillable_orders.go @@ -1,11 +1,11 @@ -package orderbookfiller +package fillbot import ( "fmt" "github.com/osmosis-labs/osmosis/osmomath" "github.com/osmosis-labs/sqs/domain" - blockctx "github.com/osmosis-labs/sqs/ingest/usecase/plugins/orderbookfiller/context/block" + blockctx "github.com/osmosis-labs/sqs/ingest/usecase/plugins/orderbook/fillbot/context/block" "go.uber.org/zap" clmath "github.com/osmosis-labs/osmosis/v26/x/concentrated-liquidity/math" diff --git a/ingest/usecase/plugins/orderbookfiller/ordebook_filler_ingest_plugin.go b/ingest/usecase/plugins/orderbook/fillbot/ordebook_filler_ingest_plugin.go similarity index 99% rename from ingest/usecase/plugins/orderbookfiller/ordebook_filler_ingest_plugin.go rename to ingest/usecase/plugins/orderbook/fillbot/ordebook_filler_ingest_plugin.go index 31e087a18..1483bf7d3 100644 --- a/ingest/usecase/plugins/orderbookfiller/ordebook_filler_ingest_plugin.go +++ b/ingest/usecase/plugins/orderbook/fillbot/ordebook_filler_ingest_plugin.go @@ -1,4 +1,4 @@ -package orderbookfiller +package fillbot import ( "context" @@ -12,9 +12,9 @@ import ( "github.com/osmosis-labs/sqs/domain/mvc" orderbookplugindomain "github.com/osmosis-labs/sqs/domain/orderbook/plugin" passthroughdomain "github.com/osmosis-labs/sqs/domain/passthrough" - blockctx "github.com/osmosis-labs/sqs/ingest/usecase/plugins/orderbookfiller/context/block" - msgctx "github.com/osmosis-labs/sqs/ingest/usecase/plugins/orderbookfiller/context/msg" - txctx "github.com/osmosis-labs/sqs/ingest/usecase/plugins/orderbookfiller/context/tx" + blockctx "github.com/osmosis-labs/sqs/ingest/usecase/plugins/orderbook/fillbot/context/block" + msgctx "github.com/osmosis-labs/sqs/ingest/usecase/plugins/orderbook/fillbot/context/msg" + txctx "github.com/osmosis-labs/sqs/ingest/usecase/plugins/orderbook/fillbot/context/tx" "github.com/osmosis-labs/sqs/log" "github.com/osmosis-labs/sqs/tokens/usecase/pricing/worker" "go.opentelemetry.io/otel" diff --git a/ingest/usecase/plugins/orderbookfiller/osmosis_swap.go b/ingest/usecase/plugins/orderbook/fillbot/osmosis_swap.go similarity index 98% rename from ingest/usecase/plugins/orderbookfiller/osmosis_swap.go rename to ingest/usecase/plugins/orderbook/fillbot/osmosis_swap.go index cb7012734..1ea4c7f04 100644 --- a/ingest/usecase/plugins/orderbookfiller/osmosis_swap.go +++ b/ingest/usecase/plugins/orderbook/fillbot/osmosis_swap.go @@ -1,4 +1,4 @@ -package orderbookfiller +package fillbot import ( "context" @@ -28,8 +28,8 @@ import ( poolmanagertypes "github.com/osmosis-labs/osmosis/v26/x/poolmanager/types" "github.com/osmosis-labs/sqs/domain" orderbookplugindomain "github.com/osmosis-labs/sqs/domain/orderbook/plugin" - blockctx "github.com/osmosis-labs/sqs/ingest/usecase/plugins/orderbookfiller/context/block" - msgctx "github.com/osmosis-labs/sqs/ingest/usecase/plugins/orderbookfiller/context/msg" + blockctx "github.com/osmosis-labs/sqs/ingest/usecase/plugins/orderbook/fillbot/context/block" + msgctx "github.com/osmosis-labs/sqs/ingest/usecase/plugins/orderbook/fillbot/context/msg" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" ) @@ -325,8 +325,8 @@ func (o *orderbookFillerIngestPlugin) simulateMsgs(ctx context.Context, msgs []s // Estimate transaction gasResult, adjustedGasUsed, err := tx.CalculateGas( - o.passthroughGRPCClient.GetChainGRPCClient(), - txFactory, + o.passthroughGRPCClient.GetChainGRPCClient(), + txFactory, msgs..., ) if err != nil { diff --git a/ingest/usecase/plugins/orderbookfiller/tick_fetcher.go b/ingest/usecase/plugins/orderbook/fillbot/tick_fetcher.go similarity index 97% rename from ingest/usecase/plugins/orderbookfiller/tick_fetcher.go rename to ingest/usecase/plugins/orderbook/fillbot/tick_fetcher.go index 2f245dc49..55e18ee1b 100644 --- a/ingest/usecase/plugins/orderbookfiller/tick_fetcher.go +++ b/ingest/usecase/plugins/orderbook/fillbot/tick_fetcher.go @@ -1,4 +1,4 @@ -package orderbookfiller +package fillbot import ( "context" diff --git a/ingest/usecase/plugins/orderbookfiller/utils.go b/ingest/usecase/plugins/orderbook/fillbot/utils.go similarity index 98% rename from ingest/usecase/plugins/orderbookfiller/utils.go rename to ingest/usecase/plugins/orderbook/fillbot/utils.go index 61c21a505..a5b7cf3f1 100644 --- a/ingest/usecase/plugins/orderbookfiller/utils.go +++ b/ingest/usecase/plugins/orderbook/fillbot/utils.go @@ -1,4 +1,4 @@ -package orderbookfiller +package fillbot import ( "fmt" @@ -6,7 +6,7 @@ import ( "github.com/osmosis-labs/osmosis/osmomath" "github.com/osmosis-labs/sqs/domain" orderbookplugindomain "github.com/osmosis-labs/sqs/domain/orderbook/plugin" - blockctx "github.com/osmosis-labs/sqs/ingest/usecase/plugins/orderbookfiller/context/block" + blockctx "github.com/osmosis-labs/sqs/ingest/usecase/plugins/orderbook/fillbot/context/block" "go.uber.org/zap" ) diff --git a/ingest/usecase/plugins/orderbookclaimer/ordebook_filler_ingest_plugin.go b/ingest/usecase/plugins/orderbookclaimer/ordebook_filler_ingest_plugin.go deleted file mode 100644 index 288dbff1e..000000000 --- a/ingest/usecase/plugins/orderbookclaimer/ordebook_filler_ingest_plugin.go +++ /dev/null @@ -1,178 +0,0 @@ -package orderbookclaimer - -import ( - "context" - "fmt" - "sync/atomic" - - "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" - "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" - orderbookplugindomain "github.com/osmosis-labs/sqs/domain/orderbook/plugin" - passthroughdomain "github.com/osmosis-labs/sqs/domain/passthrough" - "github.com/osmosis-labs/sqs/domain/slices" - "github.com/osmosis-labs/sqs/log" - "go.opentelemetry.io/otel" - "go.uber.org/zap" -) - -// orderbookClaimerIngestPlugin is a plugin that fills the orderbook orders at the end of the block. -type orderbookClaimerIngestPlugin struct { - keyring keyring.Keyring - poolsUseCase mvc.PoolsUsecase - orderbookusecase mvc.OrderBookUsecase - orderbookRepository orderbookdomain.OrderBookRepository - orderBookClient orderbookgrpcclientdomain.OrderBookClient - - accountQueryClient authtypes.QueryClient - grpcClient *grpc.Client - atomicBool atomic.Bool - - logger log.Logger -} - -var _ domain.EndBlockProcessPlugin = &orderbookClaimerIngestPlugin{} - -const ( - tracerName = "sqs-orderbook-claimer" -) - -var ( - tracer = otel.Tracer(tracerName) -) - -func New( - keyring keyring.Keyring, - orderbookusecase mvc.OrderBookUsecase, - poolsUseCase mvc.PoolsUsecase, - orderbookRepository orderbookdomain.OrderBookRepository, - orderBookClient orderbookgrpcclientdomain.OrderBookClient, - passthroughGRPCClient passthroughdomain.PassthroughGRPCClient, - orderBookCWAPIClient orderbookplugindomain.OrderbookCWAPIClient, - logger log.Logger, -) *orderbookClaimerIngestPlugin { - // Create a connection to the gRPC server. - grpcClient, err := grpc.NewClient(RPC) - if err != nil { - logger.Error("err", zap.Error(err)) // TODO - } - - return &orderbookClaimerIngestPlugin{ - accountQueryClient: authtypes.NewQueryClient(LCD), // TODO: as param - grpcClient: grpcClient, - keyring: keyring, - orderbookusecase: orderbookusecase, - orderbookRepository: orderbookRepository, - orderBookClient: orderBookClient, - poolsUseCase: poolsUseCase, - - atomicBool: atomic.Bool{}, - - logger: logger, - } -} - -// ProcessEndBlock implements domain.EndBlockProcessPlugin. -func (o *orderbookClaimerIngestPlugin) ProcessEndBlock(ctx context.Context, blockHeight uint64, metadata domain.BlockPoolMetadata) error { - ctx, span := tracer.Start(ctx, "orderbooktFillerIngestPlugin.ProcessEndBlock") - defer span.End() - - orderbooks, err := o.poolsUseCase.GetAllCanonicalOrderbookPoolIDs() - if err != nil { - o.logger.Error("failed to get all canonical orderbook pool IDs", zap.Error(err)) - return err - } - - // For simplicity, we allow only one block to be processed at a time. - // This may be relaxed in the future. - if !o.atomicBool.CompareAndSwap(false, true) { - o.logger.Info("orderbook claimer is already in progress", zap.Uint64("block_height", blockHeight)) - return nil - } - defer o.atomicBool.Store(false) - - for _, orderbook := range orderbooks { - // TODO: get ticks - ticks, ok := o.orderbookRepository.GetAllTicks(orderbook.PoolID) - if !ok { - // TODO: report an error, this should not happen - fmt.Printf("no ticks for orderbook %s\n", orderbook.ContractAddress) - continue - } - - for _, t := range ticks { - // TODO: Do we wont to store all orders inside memory? - orders, err := o.orderBookClient.GetOrdersByTick(ctx, orderbook.ContractAddress, t.Tick.TickId) - if err != nil { - fmt.Printf("no unable to fetch orderbook orders by tick ID %d\n", t.Tick.TickId) - continue - } - - if len(orders) == 0 { - continue // nothing to do - } - - var claimable orderbookdomain.Orders - claimable = append(claimable, o.getClaimableOrders(orderbook, orders.OrderByDirection("ask"), t.TickState.AskValues)...) - claimable = append(claimable, o.getClaimableOrders(orderbook, orders.OrderByDirection("bid"), t.TickState.BidValues)...) - if len(claimable) == 0 { - continue // nothing to do - } - - // Chunk claimable orders of size 100 - for _, chunk := range slices.Split(claimable, 100) { - var claims []Claim - for _, order := range chunk { - claims = append(claims, Claim{ - TickID: order.TickId, - OrderID: order.OrderId, - }) - } - - err = o.sendBatchClaimTx(orderbook.ContractAddress, claims) - - fmt.Println("claims", orderbook.ContractAddress, claims, err) - } - - break - } - } - - o.logger.Info("processed end block in orderbook claimer ingest plugin", zap.Uint64("block_height", blockHeight)) - - return nil -} - -func (q *orderbookClaimerIngestPlugin) getClaimableOrders(orderbook domain.CanonicalOrderBooksResult, orders orderbookdomain.Orders, tickValues orderbookdomain.TickValues) orderbookdomain.Orders { - // When cumulative total value of the tick is equal to its effective total amount swapped - // that would mean that all orders for this tick is already filled and we attempt to claim all orders - if tickValues.CumulativeTotalValue == tickValues.EffectiveTotalAmountSwapped { - return orders - } - - // Calculate claimable orders for the tick by iterating over each active order - // and checking each orders percent filled - var claimable orderbookdomain.Orders - for _, order := range orders { - result, err := q.orderbookusecase.CreateFormattedLimitOrder( - orderbook, - order, - ) - - // TODO: to config? - claimable_threshold, err := osmomath.NewDecFromStr("0.98") - if err != nil { - } - - if result.PercentFilled.GT(claimable_threshold) && result.PercentFilled.LTE(osmomath.OneDec()) { - claimable = append(claimable, order) - } - } - - return claimable -} diff --git a/ingest/usecase/plugins/orderbookclaimer/tx.go b/ingest/usecase/plugins/orderbookclaimer/tx.go deleted file mode 100644 index 116dda126..000000000 --- a/ingest/usecase/plugins/orderbookclaimer/tx.go +++ /dev/null @@ -1,102 +0,0 @@ -package orderbookclaimer - -import ( - "context" - "encoding/json" - "fmt" - "log" - "time" - - wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" - sdk "github.com/cosmos/cosmos-sdk/types" - - "github.com/osmosis-labs/osmosis/v26/app" - - sqstx "github.com/osmosis-labs/sqs/domain/cosmos/tx" -) - -var ( - chainID = "osmosis-1" - - RPC = "localhost:9090" - LCD = "http://127.0.0.1:1317" - Denom = "uosmo" - - encodingConfig = app.MakeEncodingConfig() -) - -func (o *orderbookClaimerIngestPlugin) sendBatchClaimTx(contractAddress string, claims []Claim) error { - account, err := o.accountQueryClient.GetAccount(context.TODO(), o.keyring.GetAddress().String()) - if err != nil { - // TODO - } - - // Prepare the message - orders := make([][]int64, len(claims)) - for i, claim := range claims { - orders[i] = []int64{claim.TickID, claim.OrderID} - } - - batchClaim := struct { - BatchClaim struct { - Orders [][]int64 `json:"orders"` - } `json:"batch_claim"` - }{ - BatchClaim: struct { - Orders [][]int64 `json:"orders"` - }{ - Orders: orders, - }, - } - - msgBytes, err := json.Marshal(batchClaim) - if err != nil { - return fmt.Errorf("failed to marshal message: %w", err) - } - - msg := wasmtypes.MsgExecuteContract{ - Sender: o.keyring.GetAddress().String(), - Contract: contractAddress, - Msg: msgBytes, - Funds: sdk.NewCoins(), - } - - tx, err := sqstx.BuildTx(context.TODO(), o.grpcClient, o.keyring, encodingConfig, sqstx.Account{ - Sequence: account.Account.Sequence, - AccountNumber: account.Account.AccountNumber, - }, chainID, &msg) - - - // Broadcast the transaction - txBytes, err := encodingConfig.TxConfig.TxEncoder()(tx.GetTx()) - if err != nil { - return fmt.Errorf("failed to encode transaction: %w", err) - } - - // Generate a JSON string. - txJSONBytes, err := encodingConfig.TxConfig.TxJSONEncoder()(tx.GetTx()) - if err != nil { - return err - } - - log.Println("txJSON", string(txJSONBytes), err) - - defer func() { - // Wait for block inclusion with buffer to avoid sequence mismatch - time.Sleep(5 * time.Second) - }() - - txresp, err := sqstx.SendTx(context.TODO(), o.grpcClient, txBytes) - - log.Printf("txres %#v : %s", txresp, err) - - - return nil -} - -// TODO -// You'll need to define the Claim struct and Config variables -type Claim struct { - TickID int64 - OrderID int64 -} diff --git a/ingest/usecase/plugins/orderbookfiller/.env b/ingest/usecase/plugins/orderbookfiller/.env deleted file mode 100644 index ceb560988..000000000 --- a/ingest/usecase/plugins/orderbookfiller/.env +++ /dev/null @@ -1,4 +0,0 @@ -DD_API_KEY=YOUR_API_KEY -OSMOSIS_KEYRING_PATH=/root/.osmosisd/keyring-test -OSMOSIS_KEYRING_PASSWORD=test -OSMOSIS_KEYRING_KEY_NAME=local.info \ No newline at end of file diff --git a/orderbook/usecase/orderbook_usecase_test.go b/orderbook/usecase/orderbook_usecase_test.go index 1009d41cf..65d6b1eed 100644 --- a/orderbook/usecase/orderbook_usecase_test.go +++ b/orderbook/usecase/orderbook_usecase_test.go @@ -819,24 +819,27 @@ func (s *OrderbookUsecaseTestSuite) TestCreateFormattedLimitOrder() { return "9223372036854775808" + strings.Repeat("0", 100000) } + newOrderbook := func(addr string) domain.CanonicalOrderBooksResult { + return domain.CanonicalOrderBooksResult{ + ContractAddress: addr, + } + } + testCases := []struct { - name string - poolID uint64 - order orderbookdomain.Order - quoteAsset orderbookdomain.Asset - baseAsset orderbookdomain.Asset - orderbookAddress string - setupMocks func(orderbookrepository *mocks.OrderbookRepositoryMock, tokensusecase *mocks.TokensUsecaseMock) - expectedError error - expectedOrder orderbookdomain.LimitOrder + name string + order orderbookdomain.Order + orderbook domain.CanonicalOrderBooksResult + setupMocks func(orderbookrepository *mocks.OrderbookRepositoryMock, tokensusecase *mocks.TokensUsecaseMock) + expectedError error + expectedOrder orderbookdomain.LimitOrder }{ { name: "tick not found", order: orderbookdomain.Order{ TickId: 99, // Non-existent tick ID }, - orderbookAddress: "osmo10dl92ghwn3v44pd8w24c3htqn2mj29549zcsn06usr56ng9ppp0qe6wd0r", - expectedError: &types.TickForOrderbookNotFoundError{}, + orderbook: newOrderbook("osmo10dl92ghwn3v44pd8w24c3htqn2mj29549zcsn06usr56ng9ppp0qe6wd0r"), + expectedError: &types.TickForOrderbookNotFoundError{}, setupMocks: func(orderbookrepository *mocks.OrderbookRepositoryMock, tokensusecase *mocks.TokensUsecaseMock) { orderbookrepository.GetTickByIDFunc = s.GetTickByIDFunc(orderbookdomain.OrderbookTick{}, false) }, @@ -846,8 +849,8 @@ func (s *OrderbookUsecaseTestSuite) TestCreateFormattedLimitOrder() { order: orderbookdomain.Order{ Quantity: "invalid", // Invalid quantity }, - orderbookAddress: "osmo1xvmtylht48gyvwe2s5rf3w6kn5g9rc4s0da0v0md82t9ldx447gsk07thg", - expectedError: &types.ParsingQuantityError{}, + orderbook: newOrderbook("osmo1xvmtylht48gyvwe2s5rf3w6kn5g9rc4s0da0v0md82t9ldx447gsk07thg"), + expectedError: &types.ParsingQuantityError{}, setupMocks: func(orderbookrepository *mocks.OrderbookRepositoryMock, tokensusecase *mocks.TokensUsecaseMock) { orderbookrepository.GetTickByIDFunc = s.GetTickByIDFunc(s.NewTick("6431", 935, "ask"), true) }, @@ -860,8 +863,8 @@ func (s *OrderbookUsecaseTestSuite) TestCreateFormattedLimitOrder() { Etas: "500", ClaimBounty: "10", }, - orderbookAddress: "osmo1rummy6vy4pfm82ctzmz4rr6fxgk0y4jf8h5s7zsadr2znwtuvq7slvl7p4", - expectedError: &types.ParsingQuantityError{}, + orderbook: newOrderbook("osmo1rummy6vy4pfm82ctzmz4rr6fxgk0y4jf8h5s7zsadr2znwtuvq7slvl7p4"), + expectedError: &types.ParsingQuantityError{}, setupMocks: func(orderbookrepository *mocks.OrderbookRepositoryMock, tokensusecase *mocks.TokensUsecaseMock) { orderbookrepository.GetTickByIDFunc = s.GetTickByIDFunc(s.NewTick("500", 100, "bid"), true) }, @@ -872,8 +875,8 @@ func (s *OrderbookUsecaseTestSuite) TestCreateFormattedLimitOrder() { Quantity: "1000", PlacedQuantity: "invalid", // Invalid placed quantity }, - orderbookAddress: "osmo1pwnxmmynz4esx79qv60cshhxkuu0glmzltsaykhccnq7jmj7tvsqdumey8", - expectedError: &types.ParsingPlacedQuantityError{}, + orderbook: newOrderbook("osmo1pwnxmmynz4esx79qv60cshhxkuu0glmzltsaykhccnq7jmj7tvsqdumey8"), + expectedError: &types.ParsingPlacedQuantityError{}, setupMocks: func(orderbookrepository *mocks.OrderbookRepositoryMock, tokensusecase *mocks.TokensUsecaseMock) { orderbookrepository.GetTickByIDFunc = s.GetTickByIDFunc(s.NewTick("813", 1331, "bid"), true) }, @@ -886,8 +889,8 @@ func (s *OrderbookUsecaseTestSuite) TestCreateFormattedLimitOrder() { Etas: "500", ClaimBounty: "10", }, - orderbookAddress: "osmo1z6h6etav6mfljq66vej7eqwsu4kummg9dfkvs969syw09fm0592s3fwgcs", - expectedError: &types.ParsingPlacedQuantityError{}, + orderbook: newOrderbook("osmo1z6h6etav6mfljq66vej7eqwsu4kummg9dfkvs969syw09fm0592s3fwgcs"), + expectedError: &types.ParsingPlacedQuantityError{}, setupMocks: func(orderbookrepository *mocks.OrderbookRepositoryMock, tokensusecase *mocks.TokensUsecaseMock) { orderbookrepository.GetTickByIDFunc = s.GetTickByIDFunc(s.NewTick("500", 100, "bid"), true) }, @@ -898,8 +901,8 @@ func (s *OrderbookUsecaseTestSuite) TestCreateFormattedLimitOrder() { Quantity: "1000", PlacedQuantity: "0", // division by zero }, - orderbookAddress: "osmo1w8jm03vws7h448yvh83utd8p43j02npydy2jll0r0k7f6w7hjspsvw2u42", - expectedError: &types.InvalidPlacedQuantityError{}, + orderbook: newOrderbook("osmo1w8jm03vws7h448yvh83utd8p43j02npydy2jll0r0k7f6w7hjspsvw2u42"), + expectedError: &types.InvalidPlacedQuantityError{}, setupMocks: func(orderbookrepository *mocks.OrderbookRepositoryMock, tokensusecase *mocks.TokensUsecaseMock) { orderbookrepository.GetTickByIDFunc = s.GetTickByIDFunc(s.NewTick("813", 1331, "bid"), true) }, @@ -910,8 +913,8 @@ func (s *OrderbookUsecaseTestSuite) TestCreateFormattedLimitOrder() { Quantity: "931", PlacedQuantity: "183", }, - orderbookAddress: "osmo197hxw89l3gqn5ake3l5as0zh2ls6e52ata2sgq80lep0854dwe5sstljsp", - expectedError: &types.GettingSpotPriceScalingFactorError{}, + orderbook: newOrderbook("osmo197hxw89l3gqn5ake3l5as0zh2ls6e52ata2sgq80lep0854dwe5sstljsp"), + expectedError: &types.GettingSpotPriceScalingFactorError{}, setupMocks: func(orderbookrepository *mocks.OrderbookRepositoryMock, tokensusecase *mocks.TokensUsecaseMock) { orderbookrepository.GetTickByIDFunc = s.GetTickByIDFunc(s.NewTick("130", 13, "ask"), true) tokensusecase.GetSpotPriceScalingFactorByDenomFunc = s.GetSpotPriceScalingFactorByDenomFunc(1, assert.AnError) @@ -924,8 +927,8 @@ func (s *OrderbookUsecaseTestSuite) TestCreateFormattedLimitOrder() { PlacedQuantity: "131", OrderDirection: "bid", }, - orderbookAddress: "osmo1s552kx03vsr7ha5ck0k9tmg74gn4w72fmmjcqgr4ky3wf96wwpcqlg7vn9", - expectedError: &types.ParsingTickValuesError{}, + orderbook: newOrderbook("osmo1s552kx03vsr7ha5ck0k9tmg74gn4w72fmmjcqgr4ky3wf96wwpcqlg7vn9"), + expectedError: &types.ParsingTickValuesError{}, setupMocks: func(orderbookrepository *mocks.OrderbookRepositoryMock, tokensusecase *mocks.TokensUsecaseMock) { orderbookrepository.GetTickByIDFunc = s.GetTickByIDFunc(s.NewTick("invalid", 13, "bid"), true) }, @@ -937,8 +940,8 @@ func (s *OrderbookUsecaseTestSuite) TestCreateFormattedLimitOrder() { PlacedQuantity: "131", OrderDirection: "ask", }, - orderbookAddress: "osmo1yuz6952hrcx0hadq4mgg6fq3t04d4kxhzwsfezlvvsvhq053qyys5udd8z", - expectedError: &types.ParsingTickValuesError{}, + orderbook: newOrderbook("osmo1yuz6952hrcx0hadq4mgg6fq3t04d4kxhzwsfezlvvsvhq053qyys5udd8z"), + expectedError: &types.ParsingTickValuesError{}, setupMocks: func(orderbookrepository *mocks.OrderbookRepositoryMock, tokensusecase *mocks.TokensUsecaseMock) { orderbookrepository.GetTickByIDFunc = s.GetTickByIDFunc(s.NewTick("invalid", 1, "ask"), true) }, @@ -950,8 +953,8 @@ func (s *OrderbookUsecaseTestSuite) TestCreateFormattedLimitOrder() { PlacedQuantity: "153", OrderDirection: "bid", }, - orderbookAddress: "osmo1apmfjhycfh4cyvc7e6px4vtfwhnl5k4l0ssjq9el4rqx8kxzh2mq5gm3j9", - expectedError: &types.ParsingUnrealizedCancelsError{}, + orderbook: newOrderbook("osmo1apmfjhycfh4cyvc7e6px4vtfwhnl5k4l0ssjq9el4rqx8kxzh2mq5gm3j9"), + expectedError: &types.ParsingUnrealizedCancelsError{}, setupMocks: func(orderbookrepository *mocks.OrderbookRepositoryMock, tokensusecase *mocks.TokensUsecaseMock) { orderbookrepository.GetTickByIDFunc = s.GetTickByIDFunc(s.NewTick("15", 0, "bid"), true) }, @@ -963,8 +966,8 @@ func (s *OrderbookUsecaseTestSuite) TestCreateFormattedLimitOrder() { PlacedQuantity: "313", OrderDirection: "ask", }, - orderbookAddress: "osmo17qvca7z822w5hy6jxzvaut46k44tlyk4fshx9aklkzq6prze4s9q73u4wz", - expectedError: &types.ParsingUnrealizedCancelsError{}, + orderbook: newOrderbook("osmo17qvca7z822w5hy6jxzvaut46k44tlyk4fshx9aklkzq6prze4s9q73u4wz"), + expectedError: &types.ParsingUnrealizedCancelsError{}, setupMocks: func(orderbookrepository *mocks.OrderbookRepositoryMock, tokensusecase *mocks.TokensUsecaseMock) { orderbookrepository.GetTickByIDFunc = s.GetTickByIDFunc(s.NewTick("13", 0, "ask"), true) }, @@ -977,8 +980,8 @@ func (s *OrderbookUsecaseTestSuite) TestCreateFormattedLimitOrder() { OrderDirection: "bid", Etas: "invalid", // Invalid ETAs }, - orderbookAddress: "osmo1dkqnzv7r5wgq08yaj7cxpqy766mwneec2z2agke2l59x7qxff5sqzd2y5l", - expectedError: &types.ParsingEtasError{}, + orderbook: newOrderbook("osmo1dkqnzv7r5wgq08yaj7cxpqy766mwneec2z2agke2l59x7qxff5sqzd2y5l"), + expectedError: &types.ParsingEtasError{}, setupMocks: func(orderbookrepository *mocks.OrderbookRepositoryMock, tokensusecase *mocks.TokensUsecaseMock) { orderbookrepository.GetTickByIDFunc = s.GetTickByIDFunc(s.NewTick("386", 830, "bid"), true) }, @@ -992,8 +995,8 @@ func (s *OrderbookUsecaseTestSuite) TestCreateFormattedLimitOrder() { Etas: overflowDecStr(), // overflow value for ETAs ClaimBounty: "10", }, - orderbookAddress: "osmo1nkt9lwky3l3gnrdjw075u557fhzxn9ke085uxnxvtkpj6kz2asrqkd65ra", - expectedError: &types.ParsingEtasError{}, + orderbook: newOrderbook("osmo1nkt9lwky3l3gnrdjw075u557fhzxn9ke085uxnxvtkpj6kz2asrqkd65ra"), + expectedError: &types.ParsingEtasError{}, setupMocks: func(orderbookrepository *mocks.OrderbookRepositoryMock, tokensusecase *mocks.TokensUsecaseMock) { orderbookrepository.GetTickByIDFunc = s.GetTickByIDFunc(s.NewTick("500", 100, "bid"), true) }, @@ -1007,8 +1010,8 @@ func (s *OrderbookUsecaseTestSuite) TestCreateFormattedLimitOrder() { OrderDirection: "ask", Etas: "100", }, - orderbookAddress: "osmo1nzpy57uftd877avsgfqjnqtsg5jhnzt8uv8mmytnku7lt76qa4lqds80nn", - expectedError: &types.ConvertingTickToPriceError{}, + orderbook: newOrderbook("osmo1nzpy57uftd877avsgfqjnqtsg5jhnzt8uv8mmytnku7lt76qa4lqds80nn"), + expectedError: &types.ConvertingTickToPriceError{}, setupMocks: func(orderbookrepository *mocks.OrderbookRepositoryMock, tokensusecase *mocks.TokensUsecaseMock) { orderbookrepository.GetTickByIDFunc = s.GetTickByIDFunc(s.NewTick("190", 150, "ask"), true) }, @@ -1023,8 +1026,8 @@ func (s *OrderbookUsecaseTestSuite) TestCreateFormattedLimitOrder() { Etas: "100", PlacedAt: "invalid", // Invalid timestamp }, - orderbookAddress: "osmo1ewuvnvtvh5jrcve9v8txr9eqnnq9x9vq82ujct53yzt2jpc8usjsyx72sr", - expectedError: &types.ParsingPlacedAtError{}, + orderbook: newOrderbook("osmo1ewuvnvtvh5jrcve9v8txr9eqnnq9x9vq82ujct53yzt2jpc8usjsyx72sr"), + expectedError: &types.ParsingPlacedAtError{}, setupMocks: func(orderbookrepository *mocks.OrderbookRepositoryMock, tokensusecase *mocks.TokensUsecaseMock) { orderbookrepository.GetTickByIDFunc = s.GetTickByIDFunc(s.NewTick("100", 100, "ask"), true) tokensusecase.GetSpotPriceScalingFactorByDenomFunc = s.GetSpotPriceScalingFactorByDenomFunc(10, nil) @@ -1037,9 +1040,9 @@ func (s *OrderbookUsecaseTestSuite) TestCreateFormattedLimitOrder() { orderbookrepository.GetTickByIDFunc = s.GetTickByIDFunc(s.NewTick("500", 100, "bid"), true) tokensusecase.GetSpotPriceScalingFactorByDenomFunc = s.GetSpotPriceScalingFactorByDenomFunc(1, nil) }, - orderbookAddress: "osmo1kfct7fcu3qqc9jlxeku873p7t5vucfzw5ujn0dh97hypg24t2w6qe9q5zs", - expectedError: nil, - expectedOrder: s.NewLimitOrder().WithOrderbookAddress("osmo1kfct7fcu3qqc9jlxeku873p7t5vucfzw5ujn0dh97hypg24t2w6qe9q5zs").LimitOrder, + orderbook: newOrderbook("osmo1kfct7fcu3qqc9jlxeku873p7t5vucfzw5ujn0dh97hypg24t2w6qe9q5zs"), + expectedError: nil, + expectedOrder: s.NewLimitOrder().WithOrderbookAddress("osmo1kfct7fcu3qqc9jlxeku873p7t5vucfzw5ujn0dh97hypg24t2w6qe9q5zs").LimitOrder, }, } @@ -1062,7 +1065,7 @@ func (s *OrderbookUsecaseTestSuite) TestCreateFormattedLimitOrder() { ) // Call the method under test - result, err := usecase.CreateFormattedLimitOrder(tc.poolID, tc.order, tc.quoteAsset, tc.baseAsset, tc.orderbookAddress) + result, err := usecase.CreateFormattedLimitOrder(tc.orderbook, tc.order) // Assert the results if tc.expectedError != nil { From a629788a50a61d920dd68f4e19a5ced4c5036aaa Mon Sep 17 00:00:00 2001 From: Deividas Petraitis Date: Thu, 10 Oct 2024 18:39:06 +0300 Subject: [PATCH 07/33] BE-586 | Add docs --- delivery/grpc/grpc.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/delivery/grpc/grpc.go b/delivery/grpc/grpc.go index d85e90373..2112ea926 100644 --- a/delivery/grpc/grpc.go +++ b/delivery/grpc/grpc.go @@ -1,3 +1,4 @@ +// Package grpc provides a custom gRPC client implementation for Cosmos SDK-based applications. package grpc import ( @@ -35,12 +36,15 @@ func (c customCodec) Name() string { return "gogoproto" } +// Client wraps a gRPC ClientConn, providing a custom connection. +// Connection is set up with custom options, including the use of a custom codec +// for gogoproto and OpenTelemetry instrumentation. +// Client addresses issue mentioned here: https://github.com/cosmos/cosmos-sdk/issues/18430 type Client struct { *grpc.ClientConn } -// connectGRPC dials up our grpc connection endpoint. -// See: https://github.com/cosmos/cosmos-sdk/issues/18430 +// NewClient creates a new gRPC client connection to the specified endpoint. func NewClient(grpcEndpoint string) (*Client, error) { customCodec := &customCodec{parentCodec: encoding.GetCodec("proto")} From 3d1c9b61bf7229840d02c41cf4f17598fa4591ef Mon Sep 17 00:00:00 2001 From: Deividas Petraitis Date: Fri, 11 Oct 2024 18:28:21 +0300 Subject: [PATCH 08/33] BE-596 | Tests init --- delivery/grpc/grpc.go | 12 +- delivery/grpc/grpc_test.go | 38 ++ delivery/http/get.go | 2 +- delivery/http/get_test.go | 71 +++ delivery/http/http.go | 6 +- domain/cosmos/auth/types/types_test.go | 80 ++++ domain/cosmos/tx/export_test.go | 35 ++ domain/cosmos/tx/tx.go | 112 +++-- domain/cosmos/tx/tx_test.go | 472 ++++++++++++++++++++ domain/mocks/keyring.go | 38 ++ domain/mocks/txfees_client.go | 55 +++ domain/mocks/txservice_client.go | 86 ++++ orderbook/usecase/orderbook_usecase_test.go | 40 +- 13 files changed, 971 insertions(+), 76 deletions(-) create mode 100644 delivery/grpc/grpc_test.go create mode 100644 delivery/http/get_test.go create mode 100644 domain/cosmos/auth/types/types_test.go create mode 100644 domain/cosmos/tx/export_test.go create mode 100644 domain/cosmos/tx/tx_test.go create mode 100644 domain/mocks/keyring.go create mode 100644 domain/mocks/txfees_client.go create mode 100644 domain/mocks/txservice_client.go diff --git a/delivery/grpc/grpc.go b/delivery/grpc/grpc.go index 2112ea926..ac79442d9 100644 --- a/delivery/grpc/grpc.go +++ b/delivery/grpc/grpc.go @@ -12,11 +12,11 @@ import ( "google.golang.org/grpc/encoding" ) -type customCodec struct { +type OsmomathCodec struct { parentCodec encoding.Codec } -func (c customCodec) Marshal(v interface{}) ([]byte, error) { +func (c OsmomathCodec) Marshal(v interface{}) ([]byte, error) { protoMsg, ok := v.(proto.Message) if !ok { return nil, fmt.Errorf("failed to assert proto.Message") @@ -24,7 +24,7 @@ func (c customCodec) Marshal(v interface{}) ([]byte, error) { return proto.Marshal(protoMsg) } -func (c customCodec) Unmarshal(data []byte, v interface{}) error { +func (c OsmomathCodec) Unmarshal(data []byte, v interface{}) error { protoMsg, ok := v.(proto.Message) if !ok { return fmt.Errorf("failed to assert proto.Message") @@ -32,21 +32,21 @@ func (c customCodec) Unmarshal(data []byte, v interface{}) error { return proto.Unmarshal(data, protoMsg) } -func (c customCodec) Name() string { +func (c OsmomathCodec) Name() string { return "gogoproto" } // Client wraps a gRPC ClientConn, providing a custom connection. // Connection is set up with custom options, including the use of a custom codec // for gogoproto and OpenTelemetry instrumentation. -// Client addresses issue mentioned here: https://github.com/cosmos/cosmos-sdk/issues/18430 +// Client addresses marshaling math.LegacyDec issue: https://github.com/cosmos/cosmos-sdk/issues/18430 type Client struct { *grpc.ClientConn } // NewClient creates a new gRPC client connection to the specified endpoint. func NewClient(grpcEndpoint string) (*Client, error) { - customCodec := &customCodec{parentCodec: encoding.GetCodec("proto")} + customCodec := &OsmomathCodec{parentCodec: encoding.GetCodec("proto")} grpcOpts := []grpc.DialOption{ grpc.WithTransportCredentials(insecure.NewCredentials()), diff --git a/delivery/grpc/grpc_test.go b/delivery/grpc/grpc_test.go new file mode 100644 index 000000000..a3f3432ba --- /dev/null +++ b/delivery/grpc/grpc_test.go @@ -0,0 +1,38 @@ +package grpc_test + +import ( + "testing" + + "github.com/osmosis-labs/sqs/delivery/grpc" + + "github.com/stretchr/testify/assert" +) + +// TestNewClient tests the NewClient function +func TestNewClient(t *testing.T) { + tests := []struct { + name string + endpoint string + wantErr bool + }{ + { + name: "Valid endpoint", + endpoint: "localhost:9090", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client, err := grpc.NewClient(tt.endpoint) + if tt.wantErr { + assert.Error(t, err) + assert.Nil(t, client) + } else { + assert.NoError(t, err) + assert.NotNil(t, client) + assert.NotNil(t, client.ClientConn) + } + }) + } +} diff --git a/delivery/http/get.go b/delivery/http/get.go index 7c0b7f5a9..5d0408a8a 100644 --- a/delivery/http/get.go +++ b/delivery/http/get.go @@ -19,7 +19,7 @@ func Get(ctx context.Context, url string) ([]byte, error) { return nil, err } - resp, err := defaultClient.Do(req) + resp, err := DefaultClient.Do(req) if err != nil { netErr, ok := err.(net.Error) if ok && netErr.Timeout() { diff --git a/delivery/http/get_test.go b/delivery/http/get_test.go new file mode 100644 index 000000000..e7f64dd56 --- /dev/null +++ b/delivery/http/get_test.go @@ -0,0 +1,71 @@ +package http_test + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + "time" + + sqshttp "github.com/osmosis-labs/sqs/delivery/http" + + "github.com/alecthomas/assert/v2" +) + +func TestGet(t *testing.T) { + tests := []struct { + name string + url string + expectedBody string + timeout time.Duration + serverResponse func(w http.ResponseWriter, r *http.Request) + }{ + { + name: "Success", + url: "/success", + expectedBody: "Hello, World!", + serverResponse: func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("Hello, World!")) + }, + }, + { + name: "Timeout", + url: "/timeout", + expectedBody: "", + timeout: 10 * time.Millisecond, + serverResponse: func(w http.ResponseWriter, r *http.Request) { + time.Sleep(20 * time.Millisecond) + w.Write([]byte("Too late")) + }, + }, + { + name: "Server Error", + url: "/error", + expectedBody: "Internal Server Error\n", + serverResponse: func(w http.ResponseWriter, r *http.Request) { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + }, + }, + } + + defaultTimeout := sqshttp.DefaultClient.Timeout + resetClient := func() { + sqshttp.DefaultClient.Timeout = defaultTimeout + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(tt.serverResponse)) + defer server.Close() + + sqshttp.DefaultClient.Timeout = tt.timeout + defer resetClient() + + ctx := context.Background() + body, err := sqshttp.Get(ctx, server.URL+tt.url) + assert.NoError(t, err) + assert.Equal(t, string(body), tt.expectedBody) + + }) + } +} diff --git a/delivery/http/http.go b/delivery/http/http.go index 5beeb6fab..a2901954f 100644 --- a/delivery/http/http.go +++ b/delivery/http/http.go @@ -10,9 +10,9 @@ import ( "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" ) -// defaultClient represents default HTTP client for issuing outgoing HTTP requests. -var defaultClient = &http.Client{ - Timeout: 10 * time.Second, // Adjusted timeout to 10 seconds +// DefaultClient represents default HTTP client for issuing outgoing HTTP requests. +var DefaultClient = &http.Client{ + Timeout: 5 * time.Second, // Adjusted timeout to 5 seconds Transport: otelhttp.NewTransport(http.DefaultTransport), } diff --git a/domain/cosmos/auth/types/types_test.go b/domain/cosmos/auth/types/types_test.go new file mode 100644 index 000000000..b02807589 --- /dev/null +++ b/domain/cosmos/auth/types/types_test.go @@ -0,0 +1,80 @@ +package types_test + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + authtypes "github.com/osmosis-labs/sqs/domain/cosmos/auth/types" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetAccount(t *testing.T) { + tests := []struct { + name string + address string + mockResponse string + expectedResult *authtypes.QueryAccountResponse + expectedError bool + }{ + { + name: "Valid account", + address: "cosmos1abcde", + mockResponse: `{ + "account": { + "sequence": "10", + "account_number": "100" + } + }`, + expectedResult: &authtypes.QueryAccountResponse{ + Account: authtypes.Account{ + Sequence: 10, + AccountNumber: 100, + }, + }, + expectedError: false, + }, + { + name: "Invalid JSON response", + address: "cosmos1fghij", + mockResponse: `{ + "account": { + "sequence": "invalid", + "account_number": "100" + } + }`, + expectedResult: nil, + expectedError: true, + }, + { + name: "Empty response", + address: "cosmos1klmno", + mockResponse: `{}`, + expectedResult: nil, + expectedError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte(tt.mockResponse)) + })) + defer server.Close() + + client := authtypes.NewQueryClient(server.URL) + result, err := client.GetAccount(context.Background(), tt.address) + + if tt.expectedError { + assert.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.expectedResult, result) + } + }) + } +} diff --git a/domain/cosmos/tx/export_test.go b/domain/cosmos/tx/export_test.go new file mode 100644 index 000000000..f4573ee9b --- /dev/null +++ b/domain/cosmos/tx/export_test.go @@ -0,0 +1,35 @@ +package tx + +import ( + txfeestypes "github.com/osmosis-labs/osmosis/v26/x/txfees/types" + + txtypes "github.com/cosmos/cosmos-sdk/types/tx" + + gogogrpc "github.com/cosmos/gogoproto/grpc" +) + +// CalculateGasFunc defines the function signature for calculating gas for a transaction. +// It is used only for testing. +type CalculateGasFunc = calculateGasFunc + +// SetCalculateGasFunc sets the function used to calculate gas for a transaction. +// This is only used for testing. +func SetCalculateGasFunc(fn CalculateGasFunc) { + calculateGas = fn +} + +// SetTxServiceClient sets the tx service client used by the tx package. +// This is only used for testing. +func SetTxServiceClient(client txtypes.ServiceClient) { + newTxServiceClient = func(gogogrpc.ClientConn) txtypes.ServiceClient { + return client + } +} + +// SetTxFeesClient sets the txfees client used by the tx package. +// This is only used for testing. +func SetTxFeesClient(client txfeestypes.QueryClient) { + newTxFeesClient = func(gogogrpc.ClientConn) txfeestypes.QueryClient { + return client + } +} diff --git a/domain/cosmos/tx/tx.go b/domain/cosmos/tx/tx.go index c370054f7..73e76cd1c 100644 --- a/domain/cosmos/tx/tx.go +++ b/domain/cosmos/tx/tx.go @@ -4,22 +4,23 @@ package tx import ( "context" - cosmosClient "github.com/cosmos/cosmos-sdk/client" + "github.com/osmosis-labs/sqs/delivery/grpc" + "github.com/osmosis-labs/sqs/domain/keyring" + + "github.com/osmosis-labs/osmosis/osmomath" + "github.com/osmosis-labs/osmosis/v26/app/params" + txfeestypes "github.com/osmosis-labs/osmosis/v26/x/txfees/types" - "github.com/cosmos/cosmos-sdk/client/tx" + cosmosClient "github.com/cosmos/cosmos-sdk/client" + txclient "github.com/cosmos/cosmos-sdk/client/tx" cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" sdk "github.com/cosmos/cosmos-sdk/types" txtypes "github.com/cosmos/cosmos-sdk/types/tx" signingtypes "github.com/cosmos/cosmos-sdk/types/tx/signing" authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" - "github.com/osmosis-labs/osmosis/osmomath" - txfeestypes "github.com/osmosis-labs/osmosis/v26/x/txfees/types" - "github.com/osmosis-labs/sqs/delivery/grpc" - "github.com/osmosis-labs/sqs/domain/keyring" - "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" - "github.com/osmosis-labs/osmosis/v26/app/params" + gogogrpc "github.com/cosmos/gogoproto/grpc" ) // Account represents the account information required for transaction building and signing. @@ -28,39 +29,17 @@ type Account struct { AccountNumber uint64 // Unique identifier of the account on the blockchain. } -// SimulateMsgs simulates the execution of the given messages and returns the simulation response, -// adjusted gas used, and any error encountered. It uses the provided gRPC client, encoding config, -// account details, and chain ID to create a transaction factory for the simulation. -func SimulateMsgs( +// BuildTx constructs a transaction using the provided parameters and messages. +// Returns a TxBuilder and any error encountered. +func BuildTx( + ctx context.Context, grpcClient *grpc.Client, + keyring keyring.Keyring, encodingConfig params.EncodingConfig, account Account, chainID string, - msgs []sdk.Msg, -) (*txtypes.SimulateResponse, uint64, error) { - txFactory := tx.Factory{} - txFactory = txFactory.WithTxConfig(encodingConfig.TxConfig) - txFactory = txFactory.WithAccountNumber(account.AccountNumber) - txFactory = txFactory.WithSequence(account.Sequence) - txFactory = txFactory.WithChainID(chainID) - txFactory = txFactory.WithGasAdjustment(1.05) - - // Estimate transaction - gasResult, adjustedGasUsed, err := tx.CalculateGas( - grpcClient, - txFactory, - msgs..., - ) - if err != nil { - return nil, adjustedGasUsed, err - } - - return gasResult, adjustedGasUsed, nil -} - -// BuildTx constructs a transaction using the provided parameters and messages. -// Returns a TxBuilder and any error encountered. -func BuildTx(ctx context.Context, grpcClient *grpc.Client, keyring keyring.Keyring, encodingConfig params.EncodingConfig, account Account, chainID string, msg ...sdk.Msg) (cosmosClient.TxBuilder, error) { + msg ...sdk.Msg, +) (cosmosClient.TxBuilder, error) { key := keyring.GetKey() privKey := &secp256k1.PrivKey{Key: key.Bytes()} @@ -99,7 +78,7 @@ func BuildTx(ctx context.Context, grpcClient *grpc.Client, keyring keyring.Keyri signerData := BuildSignerData(chainID, account.AccountNumber, account.Sequence) - signed, err := tx.SignWithPrivKey( + signed, err := txclient.SignWithPrivKey( ctx, signingtypes.SignMode_SIGN_MODE_DIRECT, signerData, txBuilder, privKey, encodingConfig.TxConfig, account.Sequence) @@ -115,13 +94,17 @@ func BuildTx(ctx context.Context, grpcClient *grpc.Client, keyring keyring.Keyri return txBuilder, nil } +// newTxServiceClient creates a new tx NewServiceClient instance with the provided gRPC connection. +// In testing, this function is replaced with a mock implementation. +var newTxServiceClient = txtypes.NewServiceClient + // SendTx broadcasts a transaction to the chain, returning the result and error. -func SendTx(ctx context.Context, grpcConn *grpc.Client, txBytes []byte) (*sdk.TxResponse, error) { +func SendTx(ctx context.Context, grpcClient *grpc.Client, txBytes []byte) (*sdk.TxResponse, error) { // Broadcast the tx via gRPC. We create a new client for the Protobuf Tx service. - txClient := txtypes.NewServiceClient(grpcConn) + txServiceClient := newTxServiceClient(grpcClient) // We then call the BroadcastTx method on this client. - resp, err := txClient.BroadcastTx( + resp, err := txServiceClient.BroadcastTx( ctx, &txtypes.BroadcastTxRequest{ Mode: txtypes.BroadcastMode_BROADCAST_MODE_SYNC, @@ -135,6 +118,44 @@ func SendTx(ctx context.Context, grpcConn *grpc.Client, txBytes []byte) (*sdk.Tx return resp.TxResponse, nil } +// nolint +// calculateGasFunc defines the function signature for calculating gas for a transaction. +type calculateGasFunc func(clientCtx gogogrpc.ClientConn, txf txclient.Factory, msgs ...sdk.Msg) (*txtypes.SimulateResponse, uint64, error) + +// calculateGas is a function that calculates the gas used for a transaction. +// In testing, this function is replaced with a mock implementation. +var calculateGas = txclient.CalculateGas + +// SimulateMsgs simulates the execution of the given messages and returns the simulation response, +// adjusted gas used, and any error encountered. It uses the provided gRPC client, encoding config, +// account details, and chain ID to create a transaction factory for the simulation. +func SimulateMsgs( + grpcClient *grpc.Client, + encodingConfig params.EncodingConfig, + account Account, + chainID string, + msgs []sdk.Msg, +) (*txtypes.SimulateResponse, uint64, error) { + txFactory := txclient.Factory{} + txFactory = txFactory.WithTxConfig(encodingConfig.TxConfig) + txFactory = txFactory.WithAccountNumber(account.AccountNumber) + txFactory = txFactory.WithSequence(account.Sequence) + txFactory = txFactory.WithChainID(chainID) + txFactory = txFactory.WithGasAdjustment(1.05) + + // Estimate transaction + gasResult, adjustedGasUsed, err := calculateGas( + grpcClient, + txFactory, + msgs..., + ) + if err != nil { + return nil, adjustedGasUsed, err + } + + return gasResult, adjustedGasUsed, nil +} + // BuildSignatures creates a SignatureV2 object using the provided public key, signature, and sequence number. // This is used in the process of building and signing transactions. func BuildSignatures(publicKey cryptotypes.PubKey, signature []byte, sequence uint64) signingtypes.SignatureV2 { @@ -158,11 +179,14 @@ func BuildSignerData(chainID string, accountNumber, sequence uint64) authsigning } } +// newTxFeesClient creates a new tx fees NewQueryClient instance with the provided gRPC connection. +// In testing, this function is replaced with a mock implementation. +var newTxFeesClient = txfeestypes.NewQueryClient + // CalculateFeeCoin determines the appropriate fee coin for a transaction based on the current base fee // and the amount of gas used. It queries the base denomination and EIP base fee using the provided gRPC connection. -func CalculateFeeCoin(ctx context.Context, grpcConn *grpc.Client, gas uint64) (sdk.Coin, error) { - client := txfeestypes.NewQueryClient(grpcConn) - +func CalculateFeeCoin(ctx context.Context, grpcClient *grpc.Client, gas uint64) (sdk.Coin, error) { + client := newTxFeesClient(grpcClient) queryBaseDenomResponse, err := client.BaseDenom(ctx, &txfeestypes.QueryBaseDenomRequest{}) if err != nil { return sdk.Coin{}, err diff --git a/domain/cosmos/tx/tx_test.go b/domain/cosmos/tx/tx_test.go new file mode 100644 index 000000000..cc777411d --- /dev/null +++ b/domain/cosmos/tx/tx_test.go @@ -0,0 +1,472 @@ +package tx_test + +import ( + "context" + "encoding/hex" + "testing" + + txfeestypes "github.com/osmosis-labs/osmosis/v26/x/txfees/types" + sqstx "github.com/osmosis-labs/sqs/domain/cosmos/tx" + "github.com/osmosis-labs/sqs/domain/keyring" + "github.com/osmosis-labs/sqs/domain/mocks" + + "github.com/osmosis-labs/osmosis/osmomath" + "github.com/osmosis-labs/osmosis/v26/app" + + txclient "github.com/cosmos/cosmos-sdk/client/tx" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + "github.com/cosmos/cosmos-sdk/types" + sdk "github.com/cosmos/cosmos-sdk/types" + txtypes "github.com/cosmos/cosmos-sdk/types/tx" + signingtypes "github.com/cosmos/cosmos-sdk/types/tx/signing" + authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" + + wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" + + gogogrpc "github.com/cosmos/gogoproto/grpc" + "github.com/stretchr/testify/assert" + "google.golang.org/grpc" +) + +var ( + encodingConfig = app.MakeEncodingConfig() + + newQueryBaseDenomResponse = func(denom string) *txfeestypes.QueryBaseDenomResponse { + return &txfeestypes.QueryBaseDenomResponse{BaseDenom: denom} + } + + newQueryEipBaseFeeResponse = func(baseFee string) *txfeestypes.QueryEipBaseFeeResponse { + return &txfeestypes.QueryEipBaseFeeResponse{ + BaseFee: osmomath.MustNewDecFromStr(baseFee), + } + } + + calculateGasFunc = func(response *txtypes.SimulateResponse, n uint64, err error) sqstx.CalculateGasFunc { + return func(clientCtx gogogrpc.ClientConn, txf txclient.Factory, msgs ...sdk.Msg) (*txtypes.SimulateResponse, uint64, error) { + return response, n, err + } + } + + genPrivKey = func(key string) secp256k1.PrivKey { + bz, _ := hex.DecodeString(key) + return secp256k1.PrivKey{Key: bz} + } + + newMsg = func(sender, contract, msg string) sdk.Msg { + return &wasmtypes.MsgExecuteContract{ + Sender: sender, + Contract: contract, + Msg: []byte(msg), + Funds: sdk.NewCoins(), + } + } +) + +func TestBuildTx(t *testing.T) { + testCases := []struct { + name string + keyring keyring.Keyring + calculateGas sqstx.CalculateGasFunc + txFeesClient mocks.TxFeesQueryClient + account sqstx.Account + chainID string + msgs []sdk.Msg + expectedJSON []byte + expectedError bool + }{ + { + name: "Valid transaction", + keyring: &mocks.Keyring{ + GetKeyFunc: func() secp256k1.PrivKey { + return genPrivKey("6cf5103c60c939a5f38e383b52239c5296c968579eec1c68a47d70fbf1d19159") + }, + }, + calculateGas: calculateGasFunc(nil, 50, nil), + txFeesClient: mocks.TxFeesQueryClient{ + BaseDenomFunc: func(ctx context.Context, in *txfeestypes.QueryBaseDenomRequest, opts ...grpc.CallOption) (*txfeestypes.QueryBaseDenomResponse, error) { + return newQueryBaseDenomResponse("eth"), nil + }, + GetEipBaseFeeFunc: func(ctx context.Context, in *txfeestypes.QueryEipBaseFeeRequest, opts ...grpc.CallOption) (*txfeestypes.QueryEipBaseFeeResponse, error) { + return newQueryEipBaseFeeResponse("0.1"), nil + }, + }, + account: sqstx.Account{ + Sequence: 13, + AccountNumber: 1, + }, + chainID: "test-chain", + msgs: []sdk.Msg{newMsg("sender", "contract", `{"payload": "hello contract"}`)}, + expectedJSON: []byte(`{"body":{"messages":[{"@type":"/cosmwasm.wasm.v1.MsgExecuteContract","sender":"sender","contract":"contract","msg":{"payload":"hello contract"},"funds":[]}],"memo":"","timeout_height":"0","extension_options":[],"non_critical_extension_options":[]},"auth_info":{"signer_infos":[{"public_key":{"@type":"/cosmos.crypto.secp256k1.PubKey","key":"A+9dbfKKCHgfmiV2XUWelqidYzZhHR+KtNMvcSzWjdPQ"},"mode_info":{"single":{"mode":"SIGN_MODE_DIRECT"}},"sequence":"13"}],"fee":{"amount":[{"denom":"eth","amount":"5"}],"gas_limit":"50","payer":"","granter":""},"tip":null},"signatures":["aRlC8F2MnDA50tNNTJUk7zPvH/xc5c3Av+yaGQEiU0l0AXJxUdzOUxWHiC74D9ltvbsk0HzWbb+2uetCjdQdfA=="]}`), + expectedError: false, + }, + { + name: "Error building transaction", + keyring: &mocks.Keyring{ + GetKeyFunc: func() secp256k1.PrivKey { + return genPrivKey("6cf5103c60c939a5f38e383b52239c5296c968579eec1c68a47d70fbf1d19159") + }, + }, + calculateGas: calculateGasFunc(nil, 50, assert.AnError), + expectedError: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + sqstx.SetCalculateGasFunc(tc.calculateGas) + sqstx.SetTxFeesClient(&tc.txFeesClient) + + txBuilder, err := sqstx.BuildTx( + context.Background(), + nil, + tc.keyring, + encodingConfig, + tc.account, + tc.chainID, + tc.msgs..., + ) + + if tc.expectedError { + assert.Error(t, err) + assert.Nil(t, txBuilder) + } else { + assert.NoError(t, err) + assert.NotNil(t, txBuilder) + + txJSONBytes, err := encodingConfig.TxConfig.TxJSONEncoder()(txBuilder.GetTx()) + assert.NoError(t, err) + + // Add more specific assertions here based on the expected output + assert.Equal(t, string(tc.expectedJSON), string(txJSONBytes)) + } + }) + } +} + +func TestSendTx(t *testing.T) { + newBroadcastTxFunc := func(txResponse *txtypes.BroadcastTxResponse, err error) func(ctx context.Context, in *txtypes.BroadcastTxRequest, opts ...grpc.CallOption) (*txtypes.BroadcastTxResponse, error) { + return func(ctx context.Context, in *txtypes.BroadcastTxRequest, opts ...grpc.CallOption) (*txtypes.BroadcastTxResponse, error) { + return txResponse, err + } + } + tests := []struct { + name string + txBytes []byte + txServiceClient mocks.ServiceClient + expectedResult *sdk.TxResponse + expectedError error + }{ + { + name: "Successful transaction", + txBytes: []byte("txbytes"), + txServiceClient: mocks.ServiceClient{ + BroadcastTxFunc: newBroadcastTxFunc(&txtypes.BroadcastTxResponse{ + TxResponse: &sdk.TxResponse{ + Code: 0, + TxHash: "test_hash", + }, + }, nil), + }, + expectedResult: &sdk.TxResponse{Code: 0, TxHash: "test_hash"}, + expectedError: nil, + }, + { + name: "Error in BroadcastTx", + txBytes: []byte("failtxbytes"), + txServiceClient: mocks.ServiceClient{ + BroadcastTxFunc: newBroadcastTxFunc(nil, assert.AnError), + }, + expectedResult: nil, + expectedError: assert.AnError, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sqstx.SetTxServiceClient(&tt.txServiceClient) + + result, err := sqstx.SendTx(context.Background(), nil, tt.txBytes) + + assert.Equal(t, tt.expectedResult, result) + assert.Equal(t, tt.expectedError, err) + }) + } +} + +func TestSimulateMsgs(t *testing.T) { + tests := []struct { + name string + account sqstx.Account + chainID string + msgs []sdk.Msg + calculateGas sqstx.CalculateGasFunc + expectedSimulateResponse *txtypes.SimulateResponse + expectedGas uint64 + expectedError error + }{ + { + name: "Successful simulation", + account: sqstx.Account{AccountNumber: 1, Sequence: 1}, + chainID: "test-chain", + msgs: []sdk.Msg{newMsg("sender", "contract", `{}`)}, + calculateGas: calculateGasFunc( + &txtypes.SimulateResponse{GasInfo: &sdk.GasInfo{GasUsed: 100000}}, + 50, + nil, + ), + expectedSimulateResponse: &txtypes.SimulateResponse{GasInfo: &sdk.GasInfo{GasUsed: 100000}}, + expectedGas: 50, + expectedError: nil, + }, + { + name: "Simulation error", + account: sqstx.Account{AccountNumber: 2, Sequence: 2}, + chainID: "test-chain", + msgs: []sdk.Msg{}, + calculateGas: calculateGasFunc(nil, 3, assert.AnError), + expectedSimulateResponse: nil, + expectedGas: 3, + expectedError: assert.AnError, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sqstx.SetCalculateGasFunc(tt.calculateGas) + + // Call the function + result, gas, err := sqstx.SimulateMsgs( + nil, + encodingConfig, + tt.account, + tt.chainID, + tt.msgs, + ) + + // Assert the results + assert.Equal(t, tt.expectedSimulateResponse, result) + assert.Equal(t, tt.expectedGas, gas) + if tt.expectedError != nil { + assert.Error(t, err) + assert.Equal(t, tt.expectedError, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestBuildSignatures(t *testing.T) { + tests := []struct { + name string + publicKey cryptotypes.PubKey + signature []byte + sequence uint64 + expectedSig signingtypes.SignatureV2 + }{ + { + name: "Valid signature", + publicKey: secp256k1.GenPrivKey().PubKey(), + signature: []byte("test signature"), + sequence: 10, + expectedSig: signingtypes.SignatureV2{ + PubKey: secp256k1.GenPrivKey().PubKey(), + Data: &signingtypes.SingleSignatureData{ + SignMode: signingtypes.SignMode_SIGN_MODE_DIRECT, + Signature: []byte("test signature"), + }, + Sequence: 10, + }, + }, + { + name: "Empty signature", + publicKey: secp256k1.GenPrivKey().PubKey(), + signature: []byte{}, + sequence: 5, + expectedSig: signingtypes.SignatureV2{ + PubKey: secp256k1.GenPrivKey().PubKey(), + Data: &signingtypes.SingleSignatureData{ + SignMode: signingtypes.SignMode_SIGN_MODE_DIRECT, + Signature: []byte{}, + }, + Sequence: 5, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := sqstx.BuildSignatures(tt.publicKey, tt.signature, tt.sequence) + + assert.Equal(t, tt.expectedSig.Sequence, result.Sequence) + assert.Equal(t, tt.expectedSig.Data.(*signingtypes.SingleSignatureData).SignMode, result.Data.(*signingtypes.SingleSignatureData).SignMode) + assert.Equal(t, tt.expectedSig.Data.(*signingtypes.SingleSignatureData).Signature, result.Data.(*signingtypes.SingleSignatureData).Signature) + + assert.Equal(t, tt.publicKey.Bytes(), result.PubKey.Bytes()) + }) + } +} + +func TestBuildSignerData(t *testing.T) { + tests := []struct { + name string + chainID string + accountNumber uint64 + sequence uint64 + expected authsigning.SignerData + }{ + { + name: "Basic test", + chainID: "test-chain", + accountNumber: 1, + sequence: 5, + expected: authsigning.SignerData{ + ChainID: "test-chain", + AccountNumber: 1, + Sequence: 5, + }, + }, + { + name: "Zero values", + chainID: "", + accountNumber: 0, + sequence: 0, + expected: authsigning.SignerData{ + ChainID: "", + AccountNumber: 0, + Sequence: 0, + }, + }, + { + name: "Large values", + chainID: "long-chain-id-123456789", + accountNumber: 9999999, + sequence: 9999999, + expected: authsigning.SignerData{ + ChainID: "long-chain-id-123456789", + AccountNumber: 9999999, + Sequence: 9999999, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := sqstx.BuildSignerData(tt.chainID, tt.accountNumber, tt.sequence) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestCalculateFeeCoin(t *testing.T) { + tests := []struct { + name string + gas uint64 + txFeesClient mocks.TxFeesQueryClient + expectedCoin string + expectedAmount osmomath.Int + expectError bool + }{ + { + name: "Normal case", + gas: 100000, + txFeesClient: mocks.TxFeesQueryClient{ + BaseDenomFunc: func(ctx context.Context, in *txfeestypes.QueryBaseDenomRequest, opts ...grpc.CallOption) (*txfeestypes.QueryBaseDenomResponse, error) { + return newQueryBaseDenomResponse("uosmo"), nil + }, + GetEipBaseFeeFunc: func(ctx context.Context, in *txfeestypes.QueryEipBaseFeeRequest, opts ...grpc.CallOption) (*txfeestypes.QueryEipBaseFeeResponse, error) { + return newQueryEipBaseFeeResponse("0.5"), nil + }, + }, + expectedCoin: "uosmo", + expectedAmount: osmomath.NewInt(50000), + expectError: false, + }, + { + name: "Error getting base denom", + txFeesClient: mocks.TxFeesQueryClient{ + BaseDenomFunc: func(ctx context.Context, in *txfeestypes.QueryBaseDenomRequest, opts ...grpc.CallOption) (*txfeestypes.QueryBaseDenomResponse, error) { + return nil, assert.AnError + }, + GetEipBaseFeeFunc: func(ctx context.Context, in *txfeestypes.QueryEipBaseFeeRequest, opts ...grpc.CallOption) (*txfeestypes.QueryEipBaseFeeResponse, error) { + return nil, nil + }, + }, + expectError: true, + }, + { + name: "Error getting EIP base fee", + txFeesClient: mocks.TxFeesQueryClient{ + BaseDenomFunc: func(ctx context.Context, in *txfeestypes.QueryBaseDenomRequest, opts ...grpc.CallOption) (*txfeestypes.QueryBaseDenomResponse, error) { + return newQueryBaseDenomResponse("wbtc"), nil + }, + GetEipBaseFeeFunc: func(ctx context.Context, in *txfeestypes.QueryEipBaseFeeRequest, opts ...grpc.CallOption) (*txfeestypes.QueryEipBaseFeeResponse, error) { + return nil, assert.AnError + }, + }, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sqstx.SetTxFeesClient(&tt.txFeesClient) + result, err := sqstx.CalculateFeeCoin(context.TODO(), nil, tt.gas) + + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, types.NewCoin(tt.expectedCoin, tt.expectedAmount), result) + } + }) + } +} + +func TestCalculateFeeAmount(t *testing.T) { + tests := []struct { + name string + baseFee osmomath.Dec + gas uint64 + expected osmomath.Int + }{ + { + name: "Zero base fee", + baseFee: osmomath.NewDec(0), + gas: 1000, + expected: osmomath.NewInt(0), + }, + { + name: "Zero gas", + baseFee: osmomath.NewDec(100), + gas: 0, + expected: osmomath.NewInt(0), + }, + { + name: "Normal case", + baseFee: osmomath.NewDecWithPrec(5, 1), // 0.5 + gas: 100000, + expected: osmomath.NewInt(50000), + }, + { + name: "Large numbers", + baseFee: osmomath.NewDec(1000), + gas: 1000000, + expected: osmomath.NewInt(1000000000), + }, + { + name: "Fractional result", + baseFee: osmomath.NewDecWithPrec(33, 2), // 0.33 + gas: 10000, + expected: osmomath.NewInt(3300), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := sqstx.CalculateFeeAmount(tt.baseFee, tt.gas) + assert.True(t, tt.expected.Equal(result), "Expected %s, but got %s", tt.expected, result) + }) + } +} diff --git a/domain/mocks/keyring.go b/domain/mocks/keyring.go new file mode 100644 index 000000000..3680d2548 --- /dev/null +++ b/domain/mocks/keyring.go @@ -0,0 +1,38 @@ +package mocks + +import ( + "github.com/osmosis-labs/sqs/domain/keyring" + + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +var _ keyring.Keyring = &Keyring{} + +type Keyring struct { + GetKeyFunc func() secp256k1.PrivKey + GetAddressFunc func() sdk.AccAddress + GetPubKeyFunc func() cryptotypes.PubKey +} + +func (m *Keyring) GetKey() secp256k1.PrivKey { + if m.GetKeyFunc != nil { + return m.GetKeyFunc() + } + panic("unimplemented") +} + +func (m *Keyring) GetAddress() sdk.AccAddress { + if m.GetAddressFunc != nil { + return m.GetAddressFunc() + } + panic("unimplemented") +} + +func (m *Keyring) GetPubKey() cryptotypes.PubKey { + if m.GetPubKeyFunc != nil { + return m.GetPubKeyFunc() + } + panic("unimplemented") +} diff --git a/domain/mocks/txfees_client.go b/domain/mocks/txfees_client.go new file mode 100644 index 000000000..f9545fd06 --- /dev/null +++ b/domain/mocks/txfees_client.go @@ -0,0 +1,55 @@ +package mocks + +import ( + "context" + + txfeestypes "github.com/osmosis-labs/osmosis/v26/x/txfees/types" + + "google.golang.org/grpc" +) + +var _ txfeestypes.QueryClient = &TxFeesQueryClient{} + +type TxFeesQueryClient struct { + FeeTokensFunc func(ctx context.Context, in *txfeestypes.QueryFeeTokensRequest, opts ...grpc.CallOption) (*txfeestypes.QueryFeeTokensResponse, error) + DenomSpotPriceFunc func(ctx context.Context, in *txfeestypes.QueryDenomSpotPriceRequest, opts ...grpc.CallOption) (*txfeestypes.QueryDenomSpotPriceResponse, error) + DenomPoolIdFunc func(ctx context.Context, in *txfeestypes.QueryDenomPoolIdRequest, opts ...grpc.CallOption) (*txfeestypes.QueryDenomPoolIdResponse, error) + BaseDenomFunc func(ctx context.Context, in *txfeestypes.QueryBaseDenomRequest, opts ...grpc.CallOption) (*txfeestypes.QueryBaseDenomResponse, error) + + GetEipBaseFeeFunc func(ctx context.Context, in *txfeestypes.QueryEipBaseFeeRequest, opts ...grpc.CallOption) (*txfeestypes.QueryEipBaseFeeResponse, error) +} + +func (m *TxFeesQueryClient) FeeTokens(ctx context.Context, in *txfeestypes.QueryFeeTokensRequest, opts ...grpc.CallOption) (*txfeestypes.QueryFeeTokensResponse, error) { + if m.FeeTokensFunc != nil { + return m.FeeTokensFunc(ctx, in, opts...) + } + panic("unimplemented") +} + +func (m *TxFeesQueryClient) DenomSpotPrice(ctx context.Context, in *txfeestypes.QueryDenomSpotPriceRequest, opts ...grpc.CallOption) (*txfeestypes.QueryDenomSpotPriceResponse, error) { + if m.DenomSpotPriceFunc != nil { + return m.DenomSpotPriceFunc(ctx, in, opts...) + } + panic("unimplemented") +} + +func (m *TxFeesQueryClient) DenomPoolId(ctx context.Context, in *txfeestypes.QueryDenomPoolIdRequest, opts ...grpc.CallOption) (*txfeestypes.QueryDenomPoolIdResponse, error) { + if m.DenomPoolIdFunc != nil { + return m.DenomPoolIdFunc(ctx, in, opts...) + } + panic("unimplemented") +} + +func (m *TxFeesQueryClient) BaseDenom(ctx context.Context, in *txfeestypes.QueryBaseDenomRequest, opts ...grpc.CallOption) (*txfeestypes.QueryBaseDenomResponse, error) { + if m.BaseDenomFunc != nil { + return m.BaseDenomFunc(ctx, in, opts...) + } + panic("unimplemented") +} + +func (m *TxFeesQueryClient) GetEipBaseFee(ctx context.Context, in *txfeestypes.QueryEipBaseFeeRequest, opts ...grpc.CallOption) (*txfeestypes.QueryEipBaseFeeResponse, error) { + if m.GetEipBaseFeeFunc != nil { + return m.GetEipBaseFeeFunc(ctx, in, opts...) + } + panic("unimplemented") +} diff --git a/domain/mocks/txservice_client.go b/domain/mocks/txservice_client.go new file mode 100644 index 000000000..7d2feae7d --- /dev/null +++ b/domain/mocks/txservice_client.go @@ -0,0 +1,86 @@ +package mocks + +import ( + "context" + + txtypes "github.com/cosmos/cosmos-sdk/types/tx" + + "google.golang.org/grpc" +) + +var _ txtypes.ServiceClient = &ServiceClient{} + +type ServiceClient struct { + SimulateFunc func(ctx context.Context, in *txtypes.SimulateRequest, opts ...grpc.CallOption) (*txtypes.SimulateResponse, error) + GetTxFunc func(ctx context.Context, in *txtypes.GetTxRequest, opts ...grpc.CallOption) (*txtypes.GetTxResponse, error) + BroadcastTxFunc func(ctx context.Context, in *txtypes.BroadcastTxRequest, opts ...grpc.CallOption) (*txtypes.BroadcastTxResponse, error) + GetTxsEventFunc func(ctx context.Context, in *txtypes.GetTxsEventRequest, opts ...grpc.CallOption) (*txtypes.GetTxsEventResponse, error) + GetBlockWithTxsFunc func(ctx context.Context, in *txtypes.GetBlockWithTxsRequest, opts ...grpc.CallOption) (*txtypes.GetBlockWithTxsResponse, error) + TxDecodeFunc func(ctx context.Context, in *txtypes.TxDecodeRequest, opts ...grpc.CallOption) (*txtypes.TxDecodeResponse, error) + TxEncodeFunc func(ctx context.Context, in *txtypes.TxEncodeRequest, opts ...grpc.CallOption) (*txtypes.TxEncodeResponse, error) + TxEncodeAminoFunc func(ctx context.Context, in *txtypes.TxEncodeAminoRequest, opts ...grpc.CallOption) (*txtypes.TxEncodeAminoResponse, error) + TxDecodeAminoFunc func(ctx context.Context, in *txtypes.TxDecodeAminoRequest, opts ...grpc.CallOption) (*txtypes.TxDecodeAminoResponse, error) +} + +func (m *ServiceClient) Simulate(ctx context.Context, in *txtypes.SimulateRequest, opts ...grpc.CallOption) (*txtypes.SimulateResponse, error) { + if m.SimulateFunc != nil { + return m.SimulateFunc(ctx, in, opts...) + } + panic("unimplemented") +} + +func (m *ServiceClient) GetTx(ctx context.Context, in *txtypes.GetTxRequest, opts ...grpc.CallOption) (*txtypes.GetTxResponse, error) { + if m.GetTxFunc != nil { + return m.GetTxFunc(ctx, in, opts...) + } + panic("unimplemented") +} + +func (m *ServiceClient) BroadcastTx(ctx context.Context, in *txtypes.BroadcastTxRequest, opts ...grpc.CallOption) (*txtypes.BroadcastTxResponse, error) { + if m.BroadcastTxFunc != nil { + return m.BroadcastTxFunc(ctx, in, opts...) + } + panic("unimplemented") +} + +func (m *ServiceClient) GetTxsEvent(ctx context.Context, in *txtypes.GetTxsEventRequest, opts ...grpc.CallOption) (*txtypes.GetTxsEventResponse, error) { + if m.GetTxsEventFunc != nil { + return m.GetTxsEventFunc(ctx, in, opts...) + } + panic("unimplemented") +} + +func (m *ServiceClient) GetBlockWithTxs(ctx context.Context, in *txtypes.GetBlockWithTxsRequest, opts ...grpc.CallOption) (*txtypes.GetBlockWithTxsResponse, error) { + if m.GetBlockWithTxsFunc != nil { + return m.GetBlockWithTxsFunc(ctx, in, opts...) + } + panic("unimplemented") +} + +func (m *ServiceClient) TxDecode(ctx context.Context, in *txtypes.TxDecodeRequest, opts ...grpc.CallOption) (*txtypes.TxDecodeResponse, error) { + if m.TxDecodeFunc != nil { + return m.TxDecodeFunc(ctx, in, opts...) + } + panic("unimplemented") +} + +func (m *ServiceClient) TxEncode(ctx context.Context, in *txtypes.TxEncodeRequest, opts ...grpc.CallOption) (*txtypes.TxEncodeResponse, error) { + if m.TxEncodeFunc != nil { + return m.TxEncodeFunc(ctx, in, opts...) + } + panic("unimplemented") +} + +func (m *ServiceClient) TxEncodeAmino(ctx context.Context, in *txtypes.TxEncodeAminoRequest, opts ...grpc.CallOption) (*txtypes.TxEncodeAminoResponse, error) { + if m.TxEncodeAminoFunc != nil { + return m.TxEncodeAminoFunc(ctx, in, opts...) + } + panic("unimplemented") +} + +func (m *ServiceClient) TxDecodeAmino(ctx context.Context, in *txtypes.TxDecodeAminoRequest, opts ...grpc.CallOption) (*txtypes.TxDecodeAminoResponse, error) { + if m.TxDecodeAminoFunc != nil { + return m.TxDecodeAminoFunc(ctx, in, opts...) + } + panic("unimplemented") +} diff --git a/orderbook/usecase/orderbook_usecase_test.go b/orderbook/usecase/orderbook_usecase_test.go index 65d6b1eed..fd063605b 100644 --- a/orderbook/usecase/orderbook_usecase_test.go +++ b/orderbook/usecase/orderbook_usecase_test.go @@ -686,28 +686,6 @@ func (s *OrderbookUsecaseTestSuite) TestProcessOrderBookActiveOrders() { expectedOrders: nil, expectedIsBestEffort: false, }, - { - name: "failed to get quote token metadata", - setupMocks: func(usecase *orderbookusecase.OrderbookUseCaseImpl, orderbookrepository *mocks.OrderbookRepositoryMock, client *mocks.OrderbookGRPCClientMock, tokensusecase *mocks.TokensUsecaseMock) { - client.GetActiveOrdersCb = s.GetActiveOrdersFunc(orderbookdomain.Orders{s.NewOrder().Order}, 1, nil) - tokensusecase.GetMetadataByChainDenomFunc = s.GetMetadataByChainDenomFunc(newLimitOrder(), "quoteToken") - }, - poolID: 11, - order: newLimitOrder().WithOrderID(1), - ownerAddress: "osmo103l28g7r3q90d20vta2p2mz0x7qvdr3xgfwnas", - expectedError: &types.FailedToGetMetadataError{}, - }, - { - name: "failed to get base token metadata", - setupMocks: func(usecase *orderbookusecase.OrderbookUseCaseImpl, orderbookrepository *mocks.OrderbookRepositoryMock, client *mocks.OrderbookGRPCClientMock, tokensusecase *mocks.TokensUsecaseMock) { - client.GetActiveOrdersCb = s.GetActiveOrdersFunc(orderbookdomain.Orders{s.NewOrder().Order}, 1, nil) - tokensusecase.GetMetadataByChainDenomFunc = s.GetMetadataByChainDenomFunc(newLimitOrder(), "quoteToken") - }, - poolID: 35, - order: newLimitOrder().WithOrderbookAddress("D"), - ownerAddress: "osmo1rlj2g3etczywhawuk7zh3tv8sp9edavvntn7jr", - expectedError: &types.FailedToGetMetadataError{}, - }, { name: "error on creating formatted limit order ( no error - best effort )", setupMocks: func(usecase *orderbookusecase.OrderbookUseCaseImpl, orderbookrepository *mocks.OrderbookRepositoryMock, client *mocks.OrderbookGRPCClientMock, tokensusecase *mocks.TokensUsecaseMock) { @@ -920,6 +898,24 @@ func (s *OrderbookUsecaseTestSuite) TestCreateFormattedLimitOrder() { tokensusecase.GetSpotPriceScalingFactorByDenomFunc = s.GetSpotPriceScalingFactorByDenomFunc(1, assert.AnError) }, }, + { + name: "failed to get quote token metadata", + order: s.NewOrder().Order, + orderbook: newOrderbook("osmo197hxw89l3gqn5ake3l5as0zh2ls6e52ata2sgq80lep0854dwe5sstljsp"), + expectedError: &types.FailedToGetMetadataError{}, + setupMocks: func(orderbookrepository *mocks.OrderbookRepositoryMock, tokensusecase *mocks.TokensUsecaseMock) { + tokensusecase.GetMetadataByChainDenomFunc = s.GetMetadataByChainDenomFunc(s.NewLimitOrder(), "quoteToken") + }, + }, + { + name: "failed to get base token metadata", + order: s.NewOrder().Order, + orderbook: newOrderbook("osmo197hxw89l3gqn5ake3l5as0zh2ls6e52ata2sgq80lep0854dwe5sstljsp"), + expectedError: &types.FailedToGetMetadataError{}, + setupMocks: func(orderbookrepository *mocks.OrderbookRepositoryMock, tokensusecase *mocks.TokensUsecaseMock) { + tokensusecase.GetMetadataByChainDenomFunc = s.GetMetadataByChainDenomFunc(s.NewLimitOrder(), "baseToken") + }, + }, { name: "error parsing bid effective total amount swapped", order: orderbookdomain.Order{ From c548460c100cb64777c16ed9e4c60fff2b17c96f Mon Sep 17 00:00:00 2001 From: Deividas Petraitis Date: Mon, 14 Oct 2024 09:59:28 +0300 Subject: [PATCH 09/33] BE-586 | Add tests for slices, orderbook packages --- domain/cosmos/tx/tx.go | 2 +- domain/orderbook/order_test.go | 110 +++++++++++++++++++++++++++++++++ domain/slices/slices_test.go | 63 +++++++++++++++++++ 3 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 domain/slices/slices_test.go diff --git a/domain/cosmos/tx/tx.go b/domain/cosmos/tx/tx.go index 73e76cd1c..c6a8b1d00 100644 --- a/domain/cosmos/tx/tx.go +++ b/domain/cosmos/tx/tx.go @@ -13,12 +13,12 @@ import ( cosmosClient "github.com/cosmos/cosmos-sdk/client" txclient "github.com/cosmos/cosmos-sdk/client/tx" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" sdk "github.com/cosmos/cosmos-sdk/types" txtypes "github.com/cosmos/cosmos-sdk/types/tx" signingtypes "github.com/cosmos/cosmos-sdk/types/tx/signing" authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" - "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" gogogrpc "github.com/cosmos/gogoproto/grpc" ) diff --git a/domain/orderbook/order_test.go b/domain/orderbook/order_test.go index 8a0e69afe..4659836fe 100644 --- a/domain/orderbook/order_test.go +++ b/domain/orderbook/order_test.go @@ -5,6 +5,8 @@ import ( orderbookdomain "github.com/osmosis-labs/sqs/domain/orderbook" + "github.com/osmosis-labs/osmosis/osmomath" + "github.com/stretchr/testify/assert" ) @@ -75,3 +77,111 @@ func TestOrderStatus(t *testing.T) { }) } } + +func TestOrdersByDirection(t *testing.T) { + testCases := []struct { + name string + orders orderbookdomain.Orders + direction string + expectedOrders orderbookdomain.Orders + }{ + { + name: "Filter buy orders", + orders: orderbookdomain.Orders{ + {OrderDirection: "buy", OrderId: 1}, + {OrderDirection: "sell", OrderId: 2}, + {OrderDirection: "buy", OrderId: 3}, + }, + direction: "buy", + expectedOrders: orderbookdomain.Orders{ + {OrderDirection: "buy", OrderId: 1}, + {OrderDirection: "buy", OrderId: 3}, + }, + }, + { + name: "Filter sell orders", + orders: orderbookdomain.Orders{ + {OrderDirection: "buy", OrderId: 1}, + {OrderDirection: "sell", OrderId: 2}, + {OrderDirection: "buy", OrderId: 3}, + {OrderDirection: "sell", OrderId: 4}, + }, + direction: "sell", + expectedOrders: orderbookdomain.Orders{ + {OrderDirection: "sell", OrderId: 2}, + {OrderDirection: "sell", OrderId: 4}, + }, + }, + { + name: "No matching orders", + orders: orderbookdomain.Orders{ + {OrderDirection: "buy", OrderId: 1}, + {OrderDirection: "buy", OrderId: 2}, + }, + direction: "sell", + expectedOrders: nil, + }, + { + name: "Empty orders slice", + orders: orderbookdomain.Orders{}, + direction: "buy", + expectedOrders: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := tc.orders.OrderByDirection(tc.direction) + assert.Equal(t, tc.expectedOrders, result) + }) + } +} + +func TestLimitOrder_IsClaimable(t *testing.T) { + tests := []struct { + name string + order orderbookdomain.LimitOrder + threshold osmomath.Dec + want bool + }{ + { + name: "Fully filled order", + order: orderbookdomain.LimitOrder{ + PercentFilled: osmomath.NewDec(1), + }, + threshold: osmomath.NewDecWithPrec(4, 1), // 0.4 + want: true, + }, + { + name: "Partially filled order above threshold", + order: orderbookdomain.LimitOrder{ + PercentFilled: osmomath.NewDecWithPrec(75, 2), // 0.75 + }, + threshold: osmomath.NewDecWithPrec(6, 1), // 0.6 + want: true, + }, + { + name: "Partially filled order below threshold", + order: orderbookdomain.LimitOrder{ + PercentFilled: osmomath.NewDecWithPrec(85, 2), // 0.85 + }, + threshold: osmomath.NewDecWithPrec(9, 1), // 0.9 + want: false, + }, + { + name: "Unfilled order", + order: orderbookdomain.LimitOrder{ + PercentFilled: osmomath.NewDec(0), + }, + threshold: osmomath.NewDecWithPrec(1, 1), // 0.1 + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.order.IsClaimable(tt.threshold) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/domain/slices/slices_test.go b/domain/slices/slices_test.go new file mode 100644 index 000000000..46e696b14 --- /dev/null +++ b/domain/slices/slices_test.go @@ -0,0 +1,63 @@ +package slices_test + +import ( + "reflect" + "testing" + + "github.com/osmosis-labs/sqs/domain/slices" +) + +func TestSplit(t *testing.T) { + tests := []struct { + name string + input []int + size int + expected [][]int + }{ + { + name: "empty slice", + input: []int{}, + size: 3, + expected: nil, + }, + { + name: "slice smaller than chunk size", + input: []int{1, 2}, + size: 3, + expected: [][]int{{1, 2}}, + }, + { + name: "slice equal to chunk size", + input: []int{1, 2, 3}, + size: 3, + expected: [][]int{{1, 2, 3}}, + }, + { + name: "slice larger than chunk size", + input: []int{1, 2, 3, 4, 5}, + size: 2, + expected: [][]int{{1, 2}, {3, 4}, {5}}, + }, + { + name: "slice multiple of chunk size", + input: []int{1, 2, 3, 4, 5, 6}, + size: 2, + expected: [][]int{{1, 2}, {3, 4}, {5, 6}}, + }, + { + name: "chunk size of 1", + input: []int{1, 2, 3}, + size: 1, + expected: [][]int{{1}, {2}, {3}}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := slices.Split(tt.input, tt.size) + if !reflect.DeepEqual(result, tt.expected) { + t.Errorf("Split(%v, %d) = %v, want %v", tt.input, tt.size, result, tt.expected) + } + }) + } +} From f37ac1d31525b6566821b66746e6e8966afc907b Mon Sep 17 00:00:00 2001 From: Deividas Petraitis Date: Mon, 14 Oct 2024 12:50:12 +0300 Subject: [PATCH 10/33] BE-586 | claimbot/tx tests --- delivery/http/get_test.go | 2 +- domain/cosmos/tx/tx_test.go | 10 +- domain/mocks/auth.go | 20 ++ domain/mocks/keyring.go | 13 +- .../plugins/orderbook/claimbot/export_test.go | 58 ++++ .../usecase/plugins/orderbook/claimbot/tx.go | 30 +- .../plugins/orderbook/claimbot/tx_test.go | 286 ++++++++++++++++++ .../memory_router_repository_test.go | 2 +- 8 files changed, 406 insertions(+), 15 deletions(-) create mode 100644 domain/mocks/auth.go create mode 100644 ingest/usecase/plugins/orderbook/claimbot/export_test.go create mode 100644 ingest/usecase/plugins/orderbook/claimbot/tx_test.go diff --git a/delivery/http/get_test.go b/delivery/http/get_test.go index e7f64dd56..30ece3d0b 100644 --- a/delivery/http/get_test.go +++ b/delivery/http/get_test.go @@ -9,7 +9,7 @@ import ( sqshttp "github.com/osmosis-labs/sqs/delivery/http" - "github.com/alecthomas/assert/v2" + "github.com/stretchr/testify/assert" ) func TestGet(t *testing.T) { diff --git a/domain/cosmos/tx/tx_test.go b/domain/cosmos/tx/tx_test.go index cc777411d..d0f956f8c 100644 --- a/domain/cosmos/tx/tx_test.go +++ b/domain/cosmos/tx/tx_test.go @@ -2,7 +2,6 @@ package tx_test import ( "context" - "encoding/hex" "testing" txfeestypes "github.com/osmosis-labs/osmosis/v26/x/txfees/types" @@ -48,11 +47,6 @@ var ( } } - genPrivKey = func(key string) secp256k1.PrivKey { - bz, _ := hex.DecodeString(key) - return secp256k1.PrivKey{Key: bz} - } - newMsg = func(sender, contract, msg string) sdk.Msg { return &wasmtypes.MsgExecuteContract{ Sender: sender, @@ -79,7 +73,7 @@ func TestBuildTx(t *testing.T) { name: "Valid transaction", keyring: &mocks.Keyring{ GetKeyFunc: func() secp256k1.PrivKey { - return genPrivKey("6cf5103c60c939a5f38e383b52239c5296c968579eec1c68a47d70fbf1d19159") + return (&mocks.Keyring{}).GenPrivKey("6cf5103c60c939a5f38e383b52239c5296c968579eec1c68a47d70fbf1d19159") }, }, calculateGas: calculateGasFunc(nil, 50, nil), @@ -104,7 +98,7 @@ func TestBuildTx(t *testing.T) { name: "Error building transaction", keyring: &mocks.Keyring{ GetKeyFunc: func() secp256k1.PrivKey { - return genPrivKey("6cf5103c60c939a5f38e383b52239c5296c968579eec1c68a47d70fbf1d19159") + return (&mocks.Keyring{}).GenPrivKey("6cf5103c60c939a5f38e383b52239c5296c968579eec1c68a47d70fbf1d19159") }, }, calculateGas: calculateGasFunc(nil, 50, assert.AnError), diff --git a/domain/mocks/auth.go b/domain/mocks/auth.go new file mode 100644 index 000000000..d5a8265b1 --- /dev/null +++ b/domain/mocks/auth.go @@ -0,0 +1,20 @@ +package mocks + +import ( + "context" + + authtypes "github.com/osmosis-labs/sqs/domain/cosmos/auth/types" +) + +var _ authtypes.QueryClient = &AuthQueryClientMock{} + +type AuthQueryClientMock struct { + GetAccountFunc func(ctx context.Context, address string) (*authtypes.QueryAccountResponse, error) +} + +func (m *AuthQueryClientMock) GetAccount(ctx context.Context, address string) (*authtypes.QueryAccountResponse, error) { + if m.GetAccountFunc != nil { + return m.GetAccountFunc(ctx, address) + } + panic("GetAccountFunc has not been mocked") +} diff --git a/domain/mocks/keyring.go b/domain/mocks/keyring.go index 3680d2548..3ea7eddd1 100644 --- a/domain/mocks/keyring.go +++ b/domain/mocks/keyring.go @@ -1,6 +1,8 @@ package mocks import ( + "encoding/hex" + "github.com/osmosis-labs/sqs/domain/keyring" "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" @@ -20,19 +22,24 @@ func (m *Keyring) GetKey() secp256k1.PrivKey { if m.GetKeyFunc != nil { return m.GetKeyFunc() } - panic("unimplemented") + panic("Keyring.GetKey(): unimplemented") } func (m *Keyring) GetAddress() sdk.AccAddress { if m.GetAddressFunc != nil { return m.GetAddressFunc() } - panic("unimplemented") + panic("Keyring.GetAddress(): unimplemented") } func (m *Keyring) GetPubKey() cryptotypes.PubKey { if m.GetPubKeyFunc != nil { return m.GetPubKeyFunc() } - panic("unimplemented") + panic("Keyring.GetPubKey(): unimplemented") +} + +func (m *Keyring) GenPrivKey(key string) secp256k1.PrivKey { + bz, _ := hex.DecodeString(key) + return secp256k1.PrivKey{Key: bz} } diff --git a/ingest/usecase/plugins/orderbook/claimbot/export_test.go b/ingest/usecase/plugins/orderbook/claimbot/export_test.go new file mode 100644 index 000000000..3ddfc269a --- /dev/null +++ b/ingest/usecase/plugins/orderbook/claimbot/export_test.go @@ -0,0 +1,58 @@ +package claimbot + +import ( + "context" + + "github.com/osmosis-labs/sqs/delivery/grpc" + 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" + orderbookdomain "github.com/osmosis-labs/sqs/domain/orderbook" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// buildTxFunc is a function signature for buildTx. +// This type is used only for testing purposes. +type BuildTx = buildTxFunc + +// SetBuildTx is used to override function that constructs a transaction. +// This function is used only for testing purposes. +func SetBuildTx(fn buildTxFunc) { + buildTx = fn +} + +// SendTxFunc is an alias for the sendTxFunc. +// This type is used only for testing purposes. +type SendTxFunc = sendTxFunc + +// SetSendTx is used to override function that sends a transaction to the blockchain. +// This function is used only for testing purposes. +func SetSendTx(fn sendTxFunc) { + sendTx = fn +} + +// SendBatchClaimTx prepares and sends a batch claim transaction to the blockchain. +// This function is used only for testing purposes. +func SendBatchClaimTx( + ctx context.Context, + keyring keyring.Keyring, + grpcClient *grpc.Client, + accountQueryClient authtypes.QueryClient, + contractAddress string, + claims orderbookdomain.Orders, +) (*sdk.TxResponse, error) { + return sendBatchClaimTx(ctx, keyring, grpcClient, accountQueryClient, contractAddress, claims) +} + +// GetAccount retrieves account information for a given address. +// 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. +// 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/tx.go b/ingest/usecase/plugins/orderbook/claimbot/tx.go index 180b0d5b5..176cee4c8 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/tx.go +++ b/ingest/usecase/plugins/orderbook/claimbot/tx.go @@ -13,8 +13,10 @@ import ( orderbookdomain "github.com/osmosis-labs/sqs/domain/orderbook" "github.com/osmosis-labs/osmosis/v26/app" + "github.com/osmosis-labs/osmosis/v26/app/params" wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" + cosmosClient "github.com/cosmos/cosmos-sdk/client" sdk "github.com/cosmos/cosmos-sdk/types" ) @@ -38,6 +40,30 @@ func init() { } } +// buildTxFunc is a function signature for buildTx. +// nolint: unused +type buildTxFunc func( + ctx context.Context, + grpcClient *grpc.Client, + keyring keyring.Keyring, + encodingConfig params.EncodingConfig, + account sqstx.Account, + chainID string, + msg ...sdk.Msg, +) (cosmosClient.TxBuilder, error) + +// buildTx is a function that constructs a transaction. +// In testing, this function is replaced with a mock implementation. +var buildTx = sqstx.BuildTx + +// sendTxFunc is a function signature for sendTx. +// nolint: unused +type sendTxFunc func(ctx context.Context, grpcClient *grpc.Client, txBytes []byte) (*sdk.TxResponse, error) + +// SendTx is a function that sends a transaction to the blockchain. +// In testing, this function is replaced with a mock implementation. +var sendTx = sqstx.SendTx + // sendBatchClaimTx prepares and sends a batch claim transaction to the blockchain. // It builds the transaction, signs it, and broadcasts it to the network. func sendBatchClaimTx( @@ -62,7 +88,7 @@ func sendBatchClaimTx( msg := buildExecuteContractMsg(address, contractAddress, msgBytes) - tx, err := sqstx.BuildTx(ctx, grpcClient, keyring, encodingConfig, account, chainID, msg) + tx, err := buildTx(ctx, grpcClient, keyring, encodingConfig, account, chainID, msg) if err != nil { return nil, fmt.Errorf("failed to build transaction: %w", err) } @@ -73,7 +99,7 @@ func sendBatchClaimTx( } // Broadcast the transaction - return sqstx.SendTx(ctx, grpcClient, txBytes) + return sendTx(ctx, grpcClient, txBytes) } // getAccount retrieves account information for a given address. diff --git a/ingest/usecase/plugins/orderbook/claimbot/tx_test.go b/ingest/usecase/plugins/orderbook/claimbot/tx_test.go new file mode 100644 index 000000000..405ef744a --- /dev/null +++ b/ingest/usecase/plugins/orderbook/claimbot/tx_test.go @@ -0,0 +1,286 @@ +package claimbot_test + +import ( + "context" + "testing" + + "github.com/osmosis-labs/sqs/delivery/grpc" + authtypes "github.com/osmosis-labs/sqs/domain/cosmos/auth/types" + "github.com/osmosis-labs/sqs/domain/cosmos/tx" + sqstx "github.com/osmosis-labs/sqs/domain/cosmos/tx" + "github.com/osmosis-labs/sqs/domain/keyring" + "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/osmosis/v26/app/params" + + cosmosClient "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/stretchr/testify/assert" +) + +func TestSendBatchClaimTx(t *testing.T) { + keyringWithGetAddressFunc := func(mock *mocks.Keyring, address string) { + mock.GetAddressFunc = func() sdk.AccAddress { + return sdk.AccAddress(address) + } + } + + keyringWithGetKeyFunc := func(mock *mocks.Keyring, key string) { + mock.GetKeyFunc = func() secp256k1.PrivKey { + return mock.GenPrivKey(key) + } + } + + authQueryClientWithGetAccountFunc := func(mock *mocks.AuthQueryClientMock, response *authtypes.QueryAccountResponse, err error) { + mock.GetAccountFunc = func(ctx context.Context, address string) (*authtypes.QueryAccountResponse, error) { + return response, err + } + } + + tests := []struct { + name string + contractAddress string + claims orderbookdomain.Orders + setupMocks func(*mocks.Keyring, *mocks.AuthQueryClientMock) + setSendTxFunc func() []byte + expectedResponse *sdk.TxResponse + expectedError bool + }{ + { + name: "AuthQueryClient.GetAccountFunc returns error", + contractAddress: "osmo1contractaddress", + claims: orderbookdomain.Orders{ + {TickId: 13, OrderId: 99}, + }, + setupMocks: func(keyringMock *mocks.Keyring, authQueryClient *mocks.AuthQueryClientMock) { + keyringWithGetAddressFunc(keyringMock, "osmo0address") + keyringWithGetKeyFunc(keyringMock, "6cf5103c60c939a5f38e383b52239c5296c968579eec1c68a47d70fbf1d19159") + authQueryClientWithGetAccountFunc(authQueryClient, nil, assert.AnError) + }, + expectedResponse: &sdk.TxResponse{}, + expectedError: true, + }, + { + name: "SetBuildTx returns error", + contractAddress: "osmo1contractaddress", + claims: orderbookdomain.Orders{ + {TickId: 13, OrderId: 99}, + }, + setupMocks: func(keyringMock *mocks.Keyring, authQueryClient *mocks.AuthQueryClientMock) { + keyringWithGetAddressFunc(keyringMock, "osmo0address") + keyringWithGetKeyFunc(keyringMock, "6cf5103c60c939a5f38e383b52239c5296c968579eec1c68a47d70fbf1d19159") + authQueryClientWithGetAccountFunc(authQueryClient, &authtypes.QueryAccountResponse{ + Account: authtypes.Account{ + AccountNumber: 3, + Sequence: 31, + }, + }, nil) + + claimbot.SetBuildTx(func( + ctx context.Context, + grpcClient *grpc.Client, + keyring keyring.Keyring, + encodingConfig params.EncodingConfig, + account sqstx.Account, + chainID string, + msg ...sdk.Msg, + ) (cosmosClient.TxBuilder, error) { + return nil, assert.AnError + }) + }, + expectedResponse: &sdk.TxResponse{}, + expectedError: true, + }, + { + name: "SetSendTx returns error", + contractAddress: "osmo1contractaddress", + claims: orderbookdomain.Orders{ + {TickId: 13, OrderId: 99}, + }, + setupMocks: func(keyringMock *mocks.Keyring, authQueryClient *mocks.AuthQueryClientMock) { + keyringWithGetAddressFunc(keyringMock, "osmo0address") + keyringWithGetKeyFunc(keyringMock, "6cf5103c60c939a5f38e383b52239c5296c968579eec1c68a47d70fbf1d19159") + authQueryClientWithGetAccountFunc(authQueryClient, &authtypes.QueryAccountResponse{ + Account: authtypes.Account{ + AccountNumber: 3, + Sequence: 31, + }, + }, nil) + + claimbot.SetBuildTx(func( + ctx context.Context, + grpcClient *grpc.Client, + keyring keyring.Keyring, + encodingConfig params.EncodingConfig, + account sqstx.Account, + chainID string, + msg ...sdk.Msg, + ) (cosmosClient.TxBuilder, error) { + builder := encodingConfig.TxConfig.NewTxBuilder() + builder.SetMsgs(msg...) + return builder, nil + }) + + claimbot.SetSendTx(func(ctx context.Context, grpcClient *grpc.Client, txBytes []byte) (*sdk.TxResponse, error) { + return nil, assert.AnError + }) + }, + expectedResponse: &sdk.TxResponse{}, + expectedError: true, + }, + { + name: "Successful transaction", + contractAddress: "osmo1contractaddress", + claims: orderbookdomain.Orders{ + {TickId: 1, OrderId: 100}, + {TickId: 2, OrderId: 200}, + }, + setupMocks: func(keyringMock *mocks.Keyring, authQueryClient *mocks.AuthQueryClientMock) { + keyringWithGetAddressFunc(keyringMock, "osmo1address") + keyringWithGetKeyFunc(keyringMock, "6cf5103c60c939a5f38e383b52239c5296c968579eec1c68a47d70fbf1d19159") + authQueryClientWithGetAccountFunc(authQueryClient, &authtypes.QueryAccountResponse{ + Account: authtypes.Account{ + AccountNumber: 1, + Sequence: 1, + }, + }, nil) + + claimbot.SetBuildTx(func( + ctx context.Context, + grpcClient *grpc.Client, + keyring keyring.Keyring, + encodingConfig params.EncodingConfig, + account sqstx.Account, + chainID string, + msg ...sdk.Msg, + ) (cosmosClient.TxBuilder, error) { + builder := encodingConfig.TxConfig.NewTxBuilder() + builder.SetMsgs(msg...) + return builder, nil + }) + + claimbot.SetSendTx(func(ctx context.Context, grpcClient *grpc.Client, txBytes []byte) (*sdk.TxResponse, error) { + return &sdk.TxResponse{ + Data: string(txBytes), // Assigning the txBytes to response Data to compare it later + }, nil + }) + }, + expectedResponse: &sdk.TxResponse{ + Data: "\n\x90\x01\n\x8d\x01\n$/cosmwasm.wasm.v1.MsgExecuteContract\x12e\n\x1fosmo1daek6me3v9jxgun9wdes7m4n5q\x12\x14osmo1contractaddress\x1a,{\"batch_claim\":{\"orders\":[[1,100],[2,200]]}}\x12\x02\x12\x00", + }, + expectedError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + keyring := mocks.Keyring{} + authQueryClient := mocks.AuthQueryClientMock{} + + tt.setupMocks(&keyring, &authQueryClient) + + response, err := claimbot.SendBatchClaimTx(ctx, &keyring, nil, &authQueryClient, tt.contractAddress, tt.claims) + if tt.expectedError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedResponse, response) + } + }) + } +} + +func TestGetAccount(t *testing.T) { + newQueryClient := func(resp *authtypes.QueryAccountResponse, err error) authtypes.QueryClient { + return &mocks.AuthQueryClientMock{ + GetAccountFunc: func(ctx context.Context, address string) (*authtypes.QueryAccountResponse, error) { + return resp, err + }, + } + } + tests := []struct { + name string + address string + queryClient authtypes.QueryClient + expectedResult sqstx.Account + expectedError bool + }{ + { + name: "Successful account retrieval", + address: "osmo1f4tvsdukfwh6s9swrc24gkuz23tp8pd3e9r5fa", + queryClient: newQueryClient(&authtypes.QueryAccountResponse{ + Account: authtypes.Account{ + Sequence: 123, + AccountNumber: 456, + }, + }, nil), + expectedResult: tx.Account{ + Sequence: 123, + AccountNumber: 456, + }, + expectedError: false, + }, + { + name: "Error retrieving account", + address: "osmo1jllfytsz4dryxhz5tl7u73v29exsf80vz52ucc", + queryClient: newQueryClient(nil, assert.AnError), + expectedResult: tx.Account{}, + expectedError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := claimbot.GetAccount(context.Background(), tt.queryClient, tt.address) + if tt.expectedError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedResult, result) + } + }) + } +} + +func TestPrepareBatchClaimMsg(t *testing.T) { + tests := []struct { + name string + claims orderbookdomain.Orders + want []byte + }{ + { + name: "Single claim", + claims: orderbookdomain.Orders{ + {TickId: 1, OrderId: 100}, + }, + want: []byte(`{"batch_claim":{"orders":[[1,100]]}}`), + }, + { + name: "Multiple claims", + claims: orderbookdomain.Orders{ + {TickId: 1, OrderId: 100}, + {TickId: 2, OrderId: 200}, + {TickId: 3, OrderId: 300}, + }, + want: []byte(`{"batch_claim":{"orders":[[1,100],[2,200],[3,300]]}}`), + }, + { + name: "Empty claims", + claims: orderbookdomain.Orders{}, + want: []byte(`{"batch_claim":{"orders":[]}}`), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := claimbot.PrepareBatchClaimMsg(tt.claims) + assert.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/router/repository/memory_router_repository_test.go b/router/repository/memory_router_repository_test.go index 22b4c6a99..64a13da2c 100644 --- a/router/repository/memory_router_repository_test.go +++ b/router/repository/memory_router_repository_test.go @@ -3,13 +3,13 @@ package routerrepo_test import ( "testing" - "github.com/alecthomas/assert/v2" "github.com/osmosis-labs/osmosis/osmomath" "github.com/osmosis-labs/sqs/domain" "github.com/osmosis-labs/sqs/domain/mocks" "github.com/osmosis-labs/sqs/log" routerrepo "github.com/osmosis-labs/sqs/router/repository" "github.com/osmosis-labs/sqs/sqsdomain" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" ) From ad494d3ec043047a07ba63a20ae80a5d5924a16b Mon Sep 17 00:00:00 2001 From: Deividas Petraitis Date: Mon, 14 Oct 2024 16:49:43 +0300 Subject: [PATCH 11/33] BE-586 | claimbot/order.go tests --- domain/mocks/orderbook_grpc_client_mock.go | 5 + domain/mocks/orderbook_repository_mock.go | 6 + domain/mocks/orderbook_usecase_mock.go | 5 + .../plugins/orderbook/claimbot/export_test.go | 28 ++- .../plugins/orderbook/claimbot/order.go | 22 ++- .../plugins/orderbook/claimbot/order_test.go | 175 ++++++++++++++++++ .../plugins/orderbook/claimbot/plugin.go | 8 +- 7 files changed, 233 insertions(+), 16 deletions(-) create mode 100644 ingest/usecase/plugins/orderbook/claimbot/order_test.go diff --git a/domain/mocks/orderbook_grpc_client_mock.go b/domain/mocks/orderbook_grpc_client_mock.go index e708007da..ac4b1d02b 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 ea087913e..a72210e99 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 0f4065a1f..0f9e2283a 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 3ddfc269a..f59d5eb98 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 8a90cf3ff..2a75696bc 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 000000000..97306a065 --- /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 3df71afa6..e6c5b2d46 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), ) } From ac10250d150f1b326c00859ee37350d6cbfa2b71 Mon Sep 17 00:00:00 2001 From: Deividas Petraitis Date: Tue, 15 Oct 2024 15:09:18 +0300 Subject: [PATCH 12/33] BE-586 | Requested changes --- domain/cosmos/tx/export_test.go | 35 ----- domain/cosmos/tx/gas.go | 43 ++++++ domain/cosmos/tx/tx.go | 39 +---- domain/cosmos/tx/tx_test.go | 136 ++++++----------- domain/mocks/auth.go | 6 + domain/mocks/gas_calculator.go | 25 ++++ domain/mocks/keyring.go | 12 ++ domain/mocks/txfees_client.go | 33 ++++- domain/mocks/txservice_client.go | 46 +++--- .../plugins/orderbook/claimbot/export_test.go | 33 ++--- .../plugins/orderbook/claimbot/plugin.go | 33 +++-- .../usecase/plugins/orderbook/claimbot/tx.go | 38 +---- .../plugins/orderbook/claimbot/tx_test.go | 139 ++++++------------ 13 files changed, 280 insertions(+), 338 deletions(-) delete mode 100644 domain/cosmos/tx/export_test.go create mode 100644 domain/cosmos/tx/gas.go create mode 100644 domain/mocks/gas_calculator.go diff --git a/domain/cosmos/tx/export_test.go b/domain/cosmos/tx/export_test.go deleted file mode 100644 index f4573ee9b..000000000 --- a/domain/cosmos/tx/export_test.go +++ /dev/null @@ -1,35 +0,0 @@ -package tx - -import ( - txfeestypes "github.com/osmosis-labs/osmosis/v26/x/txfees/types" - - txtypes "github.com/cosmos/cosmos-sdk/types/tx" - - gogogrpc "github.com/cosmos/gogoproto/grpc" -) - -// CalculateGasFunc defines the function signature for calculating gas for a transaction. -// It is used only for testing. -type CalculateGasFunc = calculateGasFunc - -// SetCalculateGasFunc sets the function used to calculate gas for a transaction. -// This is only used for testing. -func SetCalculateGasFunc(fn CalculateGasFunc) { - calculateGas = fn -} - -// SetTxServiceClient sets the tx service client used by the tx package. -// This is only used for testing. -func SetTxServiceClient(client txtypes.ServiceClient) { - newTxServiceClient = func(gogogrpc.ClientConn) txtypes.ServiceClient { - return client - } -} - -// SetTxFeesClient sets the txfees client used by the tx package. -// This is only used for testing. -func SetTxFeesClient(client txfeestypes.QueryClient) { - newTxFeesClient = func(gogogrpc.ClientConn) txfeestypes.QueryClient { - return client - } -} diff --git a/domain/cosmos/tx/gas.go b/domain/cosmos/tx/gas.go new file mode 100644 index 000000000..935f392d8 --- /dev/null +++ b/domain/cosmos/tx/gas.go @@ -0,0 +1,43 @@ +package tx + +import ( + txclient "github.com/cosmos/cosmos-sdk/client/tx" + sdk "github.com/cosmos/cosmos-sdk/types" + txtypes "github.com/cosmos/cosmos-sdk/types/tx" + + gogogrpc "github.com/cosmos/gogoproto/grpc" +) + +// GasCalculator is an interface for calculating gas for a transaction. +type GasCalculator interface { + CalculateGas(txf txclient.Factory, msgs ...sdk.Msg) (*txtypes.SimulateResponse, uint64, error) +} + +// NewGasCalculator creates a new GasCalculator instance. +func NewGasCalculator(clientCtx gogogrpc.ClientConn) GasCalculator { + return &TxGasCalulator{ + clientCtx: clientCtx, + } +} + +// TxGasCalulator is a GasCalculator implementation that uses simulated transactions to calculate gas. +type TxGasCalulator struct { + clientCtx gogogrpc.ClientConn +} + +// CalculateGas calculates the gas required for a transaction using the provided transaction factory and messages. +func (c *TxGasCalulator) CalculateGas( + txf txclient.Factory, + msgs ...sdk.Msg, +) (*txtypes.SimulateResponse, uint64, error) { + gasResult, adjustedGasUsed, err := txclient.CalculateGas( + c.clientCtx, + txf, + msgs..., + ) + if err != nil { + return nil, adjustedGasUsed, err + } + + return gasResult, adjustedGasUsed, nil +} diff --git a/domain/cosmos/tx/tx.go b/domain/cosmos/tx/tx.go index c6a8b1d00..2ba12c10d 100644 --- a/domain/cosmos/tx/tx.go +++ b/domain/cosmos/tx/tx.go @@ -4,7 +4,6 @@ package tx import ( "context" - "github.com/osmosis-labs/sqs/delivery/grpc" "github.com/osmosis-labs/sqs/domain/keyring" "github.com/osmosis-labs/osmosis/osmomath" @@ -19,8 +18,6 @@ import ( txtypes "github.com/cosmos/cosmos-sdk/types/tx" signingtypes "github.com/cosmos/cosmos-sdk/types/tx/signing" authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" - - gogogrpc "github.com/cosmos/gogoproto/grpc" ) // Account represents the account information required for transaction building and signing. @@ -33,8 +30,9 @@ type Account struct { // Returns a TxBuilder and any error encountered. func BuildTx( ctx context.Context, - grpcClient *grpc.Client, keyring keyring.Keyring, + txfeesClient txfeestypes.QueryClient, + gasCalculator GasCalculator, encodingConfig params.EncodingConfig, account Account, chainID string, @@ -52,7 +50,7 @@ func BuildTx( } _, gas, err := SimulateMsgs( - grpcClient, + gasCalculator, encodingConfig, account, chainID, @@ -63,7 +61,7 @@ func BuildTx( } txBuilder.SetGasLimit(gas) - feecoin, err := CalculateFeeCoin(ctx, grpcClient, gas) + feecoin, err := CalculateFeeCoin(ctx, txfeesClient, gas) if err != nil { return nil, err } @@ -94,15 +92,8 @@ func BuildTx( return txBuilder, nil } -// newTxServiceClient creates a new tx NewServiceClient instance with the provided gRPC connection. -// In testing, this function is replaced with a mock implementation. -var newTxServiceClient = txtypes.NewServiceClient - // SendTx broadcasts a transaction to the chain, returning the result and error. -func SendTx(ctx context.Context, grpcClient *grpc.Client, txBytes []byte) (*sdk.TxResponse, error) { - // Broadcast the tx via gRPC. We create a new client for the Protobuf Tx service. - txServiceClient := newTxServiceClient(grpcClient) - +func SendTx(ctx context.Context, txServiceClient txtypes.ServiceClient, txBytes []byte) (*sdk.TxResponse, error) { // We then call the BroadcastTx method on this client. resp, err := txServiceClient.BroadcastTx( ctx, @@ -118,19 +109,11 @@ func SendTx(ctx context.Context, grpcClient *grpc.Client, txBytes []byte) (*sdk. return resp.TxResponse, nil } -// nolint -// calculateGasFunc defines the function signature for calculating gas for a transaction. -type calculateGasFunc func(clientCtx gogogrpc.ClientConn, txf txclient.Factory, msgs ...sdk.Msg) (*txtypes.SimulateResponse, uint64, error) - -// calculateGas is a function that calculates the gas used for a transaction. -// In testing, this function is replaced with a mock implementation. -var calculateGas = txclient.CalculateGas - // SimulateMsgs simulates the execution of the given messages and returns the simulation response, // adjusted gas used, and any error encountered. It uses the provided gRPC client, encoding config, // account details, and chain ID to create a transaction factory for the simulation. func SimulateMsgs( - grpcClient *grpc.Client, + gasCalculator GasCalculator, encodingConfig params.EncodingConfig, account Account, chainID string, @@ -144,8 +127,7 @@ func SimulateMsgs( txFactory = txFactory.WithGasAdjustment(1.05) // Estimate transaction - gasResult, adjustedGasUsed, err := calculateGas( - grpcClient, + gasResult, adjustedGasUsed, err := gasCalculator.CalculateGas( txFactory, msgs..., ) @@ -179,14 +161,9 @@ func BuildSignerData(chainID string, accountNumber, sequence uint64) authsigning } } -// newTxFeesClient creates a new tx fees NewQueryClient instance with the provided gRPC connection. -// In testing, this function is replaced with a mock implementation. -var newTxFeesClient = txfeestypes.NewQueryClient - // CalculateFeeCoin determines the appropriate fee coin for a transaction based on the current base fee // and the amount of gas used. It queries the base denomination and EIP base fee using the provided gRPC connection. -func CalculateFeeCoin(ctx context.Context, grpcClient *grpc.Client, gas uint64) (sdk.Coin, error) { - client := newTxFeesClient(grpcClient) +func CalculateFeeCoin(ctx context.Context, client txfeestypes.QueryClient, gas uint64) (sdk.Coin, error) { queryBaseDenomResponse, err := client.BaseDenom(ctx, &txfeestypes.QueryBaseDenomRequest{}) if err != nil { return sdk.Coin{}, err diff --git a/domain/cosmos/tx/tx_test.go b/domain/cosmos/tx/tx_test.go index d0f956f8c..a23205b77 100644 --- a/domain/cosmos/tx/tx_test.go +++ b/domain/cosmos/tx/tx_test.go @@ -4,15 +4,12 @@ import ( "context" "testing" - txfeestypes "github.com/osmosis-labs/osmosis/v26/x/txfees/types" sqstx "github.com/osmosis-labs/sqs/domain/cosmos/tx" - "github.com/osmosis-labs/sqs/domain/keyring" "github.com/osmosis-labs/sqs/domain/mocks" "github.com/osmosis-labs/osmosis/osmomath" "github.com/osmosis-labs/osmosis/v26/app" - txclient "github.com/cosmos/cosmos-sdk/client/tx" "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" "github.com/cosmos/cosmos-sdk/types" @@ -23,7 +20,6 @@ import ( wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" - gogogrpc "github.com/cosmos/gogoproto/grpc" "github.com/stretchr/testify/assert" "google.golang.org/grpc" ) @@ -31,22 +27,6 @@ import ( var ( encodingConfig = app.MakeEncodingConfig() - newQueryBaseDenomResponse = func(denom string) *txfeestypes.QueryBaseDenomResponse { - return &txfeestypes.QueryBaseDenomResponse{BaseDenom: denom} - } - - newQueryEipBaseFeeResponse = func(baseFee string) *txfeestypes.QueryEipBaseFeeResponse { - return &txfeestypes.QueryEipBaseFeeResponse{ - BaseFee: osmomath.MustNewDecFromStr(baseFee), - } - } - - calculateGasFunc = func(response *txtypes.SimulateResponse, n uint64, err error) sqstx.CalculateGasFunc { - return func(clientCtx gogogrpc.ClientConn, txf txclient.Factory, msgs ...sdk.Msg) (*txtypes.SimulateResponse, uint64, error) { - return response, n, err - } - } - newMsg = func(sender, contract, msg string) sdk.Msg { return &wasmtypes.MsgExecuteContract{ Sender: sender, @@ -60,9 +40,7 @@ var ( func TestBuildTx(t *testing.T) { testCases := []struct { name string - keyring keyring.Keyring - calculateGas sqstx.CalculateGasFunc - txFeesClient mocks.TxFeesQueryClient + setupMocks func(calculator *mocks.GasCalculator, txFeesClient *mocks.TxFeesQueryClient, keyring *mocks.Keyring) account sqstx.Account chainID string msgs []sdk.Msg @@ -71,19 +49,11 @@ func TestBuildTx(t *testing.T) { }{ { name: "Valid transaction", - keyring: &mocks.Keyring{ - GetKeyFunc: func() secp256k1.PrivKey { - return (&mocks.Keyring{}).GenPrivKey("6cf5103c60c939a5f38e383b52239c5296c968579eec1c68a47d70fbf1d19159") - }, - }, - calculateGas: calculateGasFunc(nil, 50, nil), - txFeesClient: mocks.TxFeesQueryClient{ - BaseDenomFunc: func(ctx context.Context, in *txfeestypes.QueryBaseDenomRequest, opts ...grpc.CallOption) (*txfeestypes.QueryBaseDenomResponse, error) { - return newQueryBaseDenomResponse("eth"), nil - }, - GetEipBaseFeeFunc: func(ctx context.Context, in *txfeestypes.QueryEipBaseFeeRequest, opts ...grpc.CallOption) (*txfeestypes.QueryEipBaseFeeResponse, error) { - return newQueryEipBaseFeeResponse("0.1"), nil - }, + setupMocks: func(calculator *mocks.GasCalculator, txFeesClient *mocks.TxFeesQueryClient, keyring *mocks.Keyring) { + calculator.WithCalculateGas(nil, 50, nil) + keyring.WithGetKey("6cf5103c60c939a5f38e383b52239c5296c968579eec1c68a47d70fbf1d19159") + txFeesClient.WithBaseDenom("eth", nil) + txFeesClient.WithGetEipBaseFee("0.1", nil) }, account: sqstx.Account{ Sequence: 13, @@ -96,25 +66,27 @@ func TestBuildTx(t *testing.T) { }, { name: "Error building transaction", - keyring: &mocks.Keyring{ - GetKeyFunc: func() secp256k1.PrivKey { - return (&mocks.Keyring{}).GenPrivKey("6cf5103c60c939a5f38e383b52239c5296c968579eec1c68a47d70fbf1d19159") - }, + setupMocks: func(calculator *mocks.GasCalculator, txFeesClient *mocks.TxFeesQueryClient, keyring *mocks.Keyring) { + calculator.WithCalculateGas(nil, 50, assert.AnError) + keyring.WithGetKey("6cf5103c60c939a5f38e383b52239c5296c968579eec1c68a47d70fbf1d19159") }, - calculateGas: calculateGasFunc(nil, 50, assert.AnError), expectedError: true, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - sqstx.SetCalculateGasFunc(tc.calculateGas) - sqstx.SetTxFeesClient(&tc.txFeesClient) + gasCalculator := mocks.GasCalculator{} + txFeesClient := mocks.TxFeesQueryClient{} + keyring := mocks.Keyring{} + + tc.setupMocks(&gasCalculator, &txFeesClient, &keyring) txBuilder, err := sqstx.BuildTx( context.Background(), - nil, - tc.keyring, + &keyring, + &txFeesClient, + &gasCalculator, encodingConfig, tc.account, tc.chainID, @@ -147,14 +119,14 @@ func TestSendTx(t *testing.T) { tests := []struct { name string txBytes []byte - txServiceClient mocks.ServiceClient + txServiceClient mocks.TxServiceClient expectedResult *sdk.TxResponse expectedError error }{ { name: "Successful transaction", txBytes: []byte("txbytes"), - txServiceClient: mocks.ServiceClient{ + txServiceClient: mocks.TxServiceClient{ BroadcastTxFunc: newBroadcastTxFunc(&txtypes.BroadcastTxResponse{ TxResponse: &sdk.TxResponse{ Code: 0, @@ -168,7 +140,7 @@ func TestSendTx(t *testing.T) { { name: "Error in BroadcastTx", txBytes: []byte("failtxbytes"), - txServiceClient: mocks.ServiceClient{ + txServiceClient: mocks.TxServiceClient{ BroadcastTxFunc: newBroadcastTxFunc(nil, assert.AnError), }, expectedResult: nil, @@ -178,9 +150,7 @@ func TestSendTx(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - sqstx.SetTxServiceClient(&tt.txServiceClient) - - result, err := sqstx.SendTx(context.Background(), nil, tt.txBytes) + result, err := sqstx.SendTx(context.Background(), &tt.txServiceClient, tt.txBytes) assert.Equal(t, tt.expectedResult, result) assert.Equal(t, tt.expectedError, err) @@ -194,7 +164,7 @@ func TestSimulateMsgs(t *testing.T) { account sqstx.Account chainID string msgs []sdk.Msg - calculateGas sqstx.CalculateGasFunc + setupMocks func(calculator *mocks.GasCalculator) expectedSimulateResponse *txtypes.SimulateResponse expectedGas uint64 expectedError error @@ -204,21 +174,21 @@ func TestSimulateMsgs(t *testing.T) { account: sqstx.Account{AccountNumber: 1, Sequence: 1}, chainID: "test-chain", msgs: []sdk.Msg{newMsg("sender", "contract", `{}`)}, - calculateGas: calculateGasFunc( - &txtypes.SimulateResponse{GasInfo: &sdk.GasInfo{GasUsed: 100000}}, - 50, - nil, - ), + setupMocks: func(calculator *mocks.GasCalculator) { + calculator.WithCalculateGas(&txtypes.SimulateResponse{GasInfo: &sdk.GasInfo{GasUsed: 100000}}, 50, nil) + }, expectedSimulateResponse: &txtypes.SimulateResponse{GasInfo: &sdk.GasInfo{GasUsed: 100000}}, expectedGas: 50, expectedError: nil, }, { - name: "Simulation error", - account: sqstx.Account{AccountNumber: 2, Sequence: 2}, - chainID: "test-chain", - msgs: []sdk.Msg{}, - calculateGas: calculateGasFunc(nil, 3, assert.AnError), + name: "Simulation error", + account: sqstx.Account{AccountNumber: 2, Sequence: 2}, + chainID: "test-chain", + msgs: []sdk.Msg{}, + setupMocks: func(calculator *mocks.GasCalculator) { + calculator.WithCalculateGas(nil, 3, assert.AnError) + }, expectedSimulateResponse: nil, expectedGas: 3, expectedError: assert.AnError, @@ -227,11 +197,13 @@ func TestSimulateMsgs(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - sqstx.SetCalculateGasFunc(tt.calculateGas) + calculator := mocks.GasCalculator{} + + tt.setupMocks(&calculator) // Call the function result, gas, err := sqstx.SimulateMsgs( - nil, + &calculator, encodingConfig, tt.account, tt.chainID, @@ -358,6 +330,7 @@ func TestCalculateFeeCoin(t *testing.T) { name string gas uint64 txFeesClient mocks.TxFeesQueryClient + setupMocks func(*mocks.TxFeesQueryClient) expectedCoin string expectedAmount osmomath.Int expectError bool @@ -365,13 +338,9 @@ func TestCalculateFeeCoin(t *testing.T) { { name: "Normal case", gas: 100000, - txFeesClient: mocks.TxFeesQueryClient{ - BaseDenomFunc: func(ctx context.Context, in *txfeestypes.QueryBaseDenomRequest, opts ...grpc.CallOption) (*txfeestypes.QueryBaseDenomResponse, error) { - return newQueryBaseDenomResponse("uosmo"), nil - }, - GetEipBaseFeeFunc: func(ctx context.Context, in *txfeestypes.QueryEipBaseFeeRequest, opts ...grpc.CallOption) (*txfeestypes.QueryEipBaseFeeResponse, error) { - return newQueryEipBaseFeeResponse("0.5"), nil - }, + setupMocks: func(client *mocks.TxFeesQueryClient) { + client.WithBaseDenom("uosmo", nil) + client.WithGetEipBaseFee("0.5", nil) }, expectedCoin: "uosmo", expectedAmount: osmomath.NewInt(50000), @@ -379,25 +348,17 @@ func TestCalculateFeeCoin(t *testing.T) { }, { name: "Error getting base denom", - txFeesClient: mocks.TxFeesQueryClient{ - BaseDenomFunc: func(ctx context.Context, in *txfeestypes.QueryBaseDenomRequest, opts ...grpc.CallOption) (*txfeestypes.QueryBaseDenomResponse, error) { - return nil, assert.AnError - }, - GetEipBaseFeeFunc: func(ctx context.Context, in *txfeestypes.QueryEipBaseFeeRequest, opts ...grpc.CallOption) (*txfeestypes.QueryEipBaseFeeResponse, error) { - return nil, nil - }, + setupMocks: func(client *mocks.TxFeesQueryClient) { + client.WithBaseDenom("", assert.AnError) + client.WithGetEipBaseFee("", nil) }, expectError: true, }, { name: "Error getting EIP base fee", - txFeesClient: mocks.TxFeesQueryClient{ - BaseDenomFunc: func(ctx context.Context, in *txfeestypes.QueryBaseDenomRequest, opts ...grpc.CallOption) (*txfeestypes.QueryBaseDenomResponse, error) { - return newQueryBaseDenomResponse("wbtc"), nil - }, - GetEipBaseFeeFunc: func(ctx context.Context, in *txfeestypes.QueryEipBaseFeeRequest, opts ...grpc.CallOption) (*txfeestypes.QueryEipBaseFeeResponse, error) { - return nil, assert.AnError - }, + setupMocks: func(client *mocks.TxFeesQueryClient) { + client.WithBaseDenom("wbtc", nil) + client.WithGetEipBaseFee("", assert.AnError) }, expectError: true, }, @@ -405,8 +366,9 @@ func TestCalculateFeeCoin(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - sqstx.SetTxFeesClient(&tt.txFeesClient) - result, err := sqstx.CalculateFeeCoin(context.TODO(), nil, tt.gas) + tt.setupMocks(&tt.txFeesClient) + + result, err := sqstx.CalculateFeeCoin(context.TODO(), &tt.txFeesClient, tt.gas) if tt.expectError { assert.Error(t, err) diff --git a/domain/mocks/auth.go b/domain/mocks/auth.go index d5a8265b1..06bfab137 100644 --- a/domain/mocks/auth.go +++ b/domain/mocks/auth.go @@ -18,3 +18,9 @@ func (m *AuthQueryClientMock) GetAccount(ctx context.Context, address string) (* } panic("GetAccountFunc has not been mocked") } + +func (m *AuthQueryClientMock) WithGetAccount(response *authtypes.QueryAccountResponse, err error) { + m.GetAccountFunc = func(ctx context.Context, address string) (*authtypes.QueryAccountResponse, error) { + return response, err + } +} diff --git a/domain/mocks/gas_calculator.go b/domain/mocks/gas_calculator.go new file mode 100644 index 000000000..ef9d40369 --- /dev/null +++ b/domain/mocks/gas_calculator.go @@ -0,0 +1,25 @@ +package mocks + +import ( + txclient "github.com/cosmos/cosmos-sdk/client/tx" + sdk "github.com/cosmos/cosmos-sdk/types" + txtypes "github.com/cosmos/cosmos-sdk/types/tx" +) + +type GasCalculator struct { + CalculateGasFunc func(txf txclient.Factory, msgs ...sdk.Msg) (*txtypes.SimulateResponse, uint64, error) +} + +func (m *GasCalculator) CalculateGas(txf txclient.Factory, msgs ...sdk.Msg) (*txtypes.SimulateResponse, uint64, error) { + if m.CalculateGasFunc != nil { + return m.CalculateGasFunc(txf, msgs...) + } + + panic("GasCalculator.CalculateGasFunc not implemented") +} + +func (m *GasCalculator) WithCalculateGas(response *txtypes.SimulateResponse, n uint64, err error) { + m.CalculateGasFunc = func(txf txclient.Factory, msgs ...sdk.Msg) (*txtypes.SimulateResponse, uint64, error) { + return response, n, err + } +} diff --git a/domain/mocks/keyring.go b/domain/mocks/keyring.go index 3ea7eddd1..2159a4362 100644 --- a/domain/mocks/keyring.go +++ b/domain/mocks/keyring.go @@ -25,6 +25,12 @@ func (m *Keyring) GetKey() secp256k1.PrivKey { panic("Keyring.GetKey(): unimplemented") } +func (m *Keyring) WithGetKey(key string) { + m.GetKeyFunc = func() secp256k1.PrivKey { + return m.GenPrivKey(key) + } +} + func (m *Keyring) GetAddress() sdk.AccAddress { if m.GetAddressFunc != nil { return m.GetAddressFunc() @@ -32,6 +38,12 @@ func (m *Keyring) GetAddress() sdk.AccAddress { panic("Keyring.GetAddress(): unimplemented") } +func (m *Keyring) WithGetAddress(address string) { + m.GetAddressFunc = func() sdk.AccAddress { + return sdk.AccAddress(address) + } +} + func (m *Keyring) GetPubKey() cryptotypes.PubKey { if m.GetPubKeyFunc != nil { return m.GetPubKeyFunc() diff --git a/domain/mocks/txfees_client.go b/domain/mocks/txfees_client.go index f9545fd06..3e176bd8a 100644 --- a/domain/mocks/txfees_client.go +++ b/domain/mocks/txfees_client.go @@ -5,6 +5,8 @@ import ( txfeestypes "github.com/osmosis-labs/osmosis/v26/x/txfees/types" + "github.com/osmosis-labs/osmosis/osmomath" + "google.golang.org/grpc" ) @@ -23,33 +25,54 @@ func (m *TxFeesQueryClient) FeeTokens(ctx context.Context, in *txfeestypes.Query if m.FeeTokensFunc != nil { return m.FeeTokensFunc(ctx, in, opts...) } - panic("unimplemented") + panic("TxFeesQueryClient.FeeTokens unimplemented") } func (m *TxFeesQueryClient) DenomSpotPrice(ctx context.Context, in *txfeestypes.QueryDenomSpotPriceRequest, opts ...grpc.CallOption) (*txfeestypes.QueryDenomSpotPriceResponse, error) { if m.DenomSpotPriceFunc != nil { return m.DenomSpotPriceFunc(ctx, in, opts...) } - panic("unimplemented") + panic("TxFeesQueryClient.DenomSpotPrice unimplemented") } func (m *TxFeesQueryClient) DenomPoolId(ctx context.Context, in *txfeestypes.QueryDenomPoolIdRequest, opts ...grpc.CallOption) (*txfeestypes.QueryDenomPoolIdResponse, error) { if m.DenomPoolIdFunc != nil { return m.DenomPoolIdFunc(ctx, in, opts...) } - panic("unimplemented") + panic("TxFeesQueryClient.DenomPoolId unimplemented") } func (m *TxFeesQueryClient) BaseDenom(ctx context.Context, in *txfeestypes.QueryBaseDenomRequest, opts ...grpc.CallOption) (*txfeestypes.QueryBaseDenomResponse, error) { if m.BaseDenomFunc != nil { return m.BaseDenomFunc(ctx, in, opts...) } - panic("unimplemented") + panic("TxFeesQueryClient.BaseDenom unimplemented") } +func (m *TxFeesQueryClient) WithBaseDenom(denom string, err error) { + m.BaseDenomFunc = func(ctx context.Context, in *txfeestypes.QueryBaseDenomRequest, opts ...grpc.CallOption) (*txfeestypes.QueryBaseDenomResponse, error) { + if len(denom) > 0 { + return &txfeestypes.QueryBaseDenomResponse{BaseDenom: denom}, err + } + return nil, err + } +} func (m *TxFeesQueryClient) GetEipBaseFee(ctx context.Context, in *txfeestypes.QueryEipBaseFeeRequest, opts ...grpc.CallOption) (*txfeestypes.QueryEipBaseFeeResponse, error) { if m.GetEipBaseFeeFunc != nil { return m.GetEipBaseFeeFunc(ctx, in, opts...) } - panic("unimplemented") + + panic("TxFeesQueryClient.GetEipBaseFee unimplemented") +} + +func (m *TxFeesQueryClient) WithGetEipBaseFee(baseFee string, err error) { + m.GetEipBaseFeeFunc = func(ctx context.Context, in *txfeestypes.QueryEipBaseFeeRequest, opts ...grpc.CallOption) (*txfeestypes.QueryEipBaseFeeResponse, error) { + if baseFee == "" { + return nil, err + } + + return &txfeestypes.QueryEipBaseFeeResponse{ + BaseFee: osmomath.MustNewDecFromStr(baseFee), + }, err + } } diff --git a/domain/mocks/txservice_client.go b/domain/mocks/txservice_client.go index 7d2feae7d..65e496521 100644 --- a/domain/mocks/txservice_client.go +++ b/domain/mocks/txservice_client.go @@ -8,9 +8,9 @@ import ( "google.golang.org/grpc" ) -var _ txtypes.ServiceClient = &ServiceClient{} +var _ txtypes.ServiceClient = &TxServiceClient{} -type ServiceClient struct { +type TxServiceClient struct { SimulateFunc func(ctx context.Context, in *txtypes.SimulateRequest, opts ...grpc.CallOption) (*txtypes.SimulateResponse, error) GetTxFunc func(ctx context.Context, in *txtypes.GetTxRequest, opts ...grpc.CallOption) (*txtypes.GetTxResponse, error) BroadcastTxFunc func(ctx context.Context, in *txtypes.BroadcastTxRequest, opts ...grpc.CallOption) (*txtypes.BroadcastTxResponse, error) @@ -22,65 +22,71 @@ type ServiceClient struct { TxDecodeAminoFunc func(ctx context.Context, in *txtypes.TxDecodeAminoRequest, opts ...grpc.CallOption) (*txtypes.TxDecodeAminoResponse, error) } -func (m *ServiceClient) Simulate(ctx context.Context, in *txtypes.SimulateRequest, opts ...grpc.CallOption) (*txtypes.SimulateResponse, error) { +func (m *TxServiceClient) Simulate(ctx context.Context, in *txtypes.SimulateRequest, opts ...grpc.CallOption) (*txtypes.SimulateResponse, error) { if m.SimulateFunc != nil { return m.SimulateFunc(ctx, in, opts...) } - panic("unimplemented") + panic("TxServiceClient.Simulate unimplemented") } -func (m *ServiceClient) GetTx(ctx context.Context, in *txtypes.GetTxRequest, opts ...grpc.CallOption) (*txtypes.GetTxResponse, error) { +func (m *TxServiceClient) GetTx(ctx context.Context, in *txtypes.GetTxRequest, opts ...grpc.CallOption) (*txtypes.GetTxResponse, error) { if m.GetTxFunc != nil { return m.GetTxFunc(ctx, in, opts...) } - panic("unimplemented") + panic("TxServiceClient.GetTx unimplemented") } -func (m *ServiceClient) BroadcastTx(ctx context.Context, in *txtypes.BroadcastTxRequest, opts ...grpc.CallOption) (*txtypes.BroadcastTxResponse, error) { +func (m *TxServiceClient) BroadcastTx(ctx context.Context, in *txtypes.BroadcastTxRequest, opts ...grpc.CallOption) (*txtypes.BroadcastTxResponse, error) { if m.BroadcastTxFunc != nil { return m.BroadcastTxFunc(ctx, in, opts...) } - panic("unimplemented") + panic("TxServiceClient.BroadcastTx unimplemented") } -func (m *ServiceClient) GetTxsEvent(ctx context.Context, in *txtypes.GetTxsEventRequest, opts ...grpc.CallOption) (*txtypes.GetTxsEventResponse, error) { +func (m *TxServiceClient) WithBroadcastTx(response *txtypes.BroadcastTxResponse, err error) { + m.BroadcastTxFunc = func(ctx context.Context, in *txtypes.BroadcastTxRequest, opts ...grpc.CallOption) (*txtypes.BroadcastTxResponse, error) { + return response, err + } +} + +func (m *TxServiceClient) GetTxsEvent(ctx context.Context, in *txtypes.GetTxsEventRequest, opts ...grpc.CallOption) (*txtypes.GetTxsEventResponse, error) { if m.GetTxsEventFunc != nil { return m.GetTxsEventFunc(ctx, in, opts...) } - panic("unimplemented") + panic("TxServiceClient.GetTxsEvent unimplemented") } -func (m *ServiceClient) GetBlockWithTxs(ctx context.Context, in *txtypes.GetBlockWithTxsRequest, opts ...grpc.CallOption) (*txtypes.GetBlockWithTxsResponse, error) { +func (m *TxServiceClient) GetBlockWithTxs(ctx context.Context, in *txtypes.GetBlockWithTxsRequest, opts ...grpc.CallOption) (*txtypes.GetBlockWithTxsResponse, error) { if m.GetBlockWithTxsFunc != nil { return m.GetBlockWithTxsFunc(ctx, in, opts...) } - panic("unimplemented") + panic("TxServiceClient.GetBlockWithTxs unimplemented") } -func (m *ServiceClient) TxDecode(ctx context.Context, in *txtypes.TxDecodeRequest, opts ...grpc.CallOption) (*txtypes.TxDecodeResponse, error) { +func (m *TxServiceClient) TxDecode(ctx context.Context, in *txtypes.TxDecodeRequest, opts ...grpc.CallOption) (*txtypes.TxDecodeResponse, error) { if m.TxDecodeFunc != nil { return m.TxDecodeFunc(ctx, in, opts...) } - panic("unimplemented") + panic("TxServiceClient.TxDecode unimplemented") } -func (m *ServiceClient) TxEncode(ctx context.Context, in *txtypes.TxEncodeRequest, opts ...grpc.CallOption) (*txtypes.TxEncodeResponse, error) { +func (m *TxServiceClient) TxEncode(ctx context.Context, in *txtypes.TxEncodeRequest, opts ...grpc.CallOption) (*txtypes.TxEncodeResponse, error) { if m.TxEncodeFunc != nil { return m.TxEncodeFunc(ctx, in, opts...) } - panic("unimplemented") + panic("TxServiceClient.TxEncode unimplemented") } -func (m *ServiceClient) TxEncodeAmino(ctx context.Context, in *txtypes.TxEncodeAminoRequest, opts ...grpc.CallOption) (*txtypes.TxEncodeAminoResponse, error) { +func (m *TxServiceClient) TxEncodeAmino(ctx context.Context, in *txtypes.TxEncodeAminoRequest, opts ...grpc.CallOption) (*txtypes.TxEncodeAminoResponse, error) { if m.TxEncodeAminoFunc != nil { return m.TxEncodeAminoFunc(ctx, in, opts...) } - panic("unimplemented") + panic("TxServiceClient.TxEncodeAmino unimplemented") } -func (m *ServiceClient) TxDecodeAmino(ctx context.Context, in *txtypes.TxDecodeAminoRequest, opts ...grpc.CallOption) (*txtypes.TxDecodeAminoResponse, error) { +func (m *TxServiceClient) TxDecodeAmino(ctx context.Context, in *txtypes.TxDecodeAminoRequest, opts ...grpc.CallOption) (*txtypes.TxDecodeAminoResponse, error) { if m.TxDecodeAminoFunc != nil { return m.TxDecodeAminoFunc(ctx, in, opts...) } - panic("unimplemented") + panic("TxServiceClient.TxDecodeAmino unimplemented") } diff --git a/ingest/usecase/plugins/orderbook/claimbot/export_test.go b/ingest/usecase/plugins/orderbook/claimbot/export_test.go index f59d5eb98..1f21fbfea 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/export_test.go +++ b/ingest/usecase/plugins/orderbook/claimbot/export_test.go @@ -3,8 +3,6 @@ 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" @@ -14,7 +12,12 @@ import ( orderbookgrpcclientdomain "github.com/osmosis-labs/sqs/domain/orderbook/grpcclient" "github.com/osmosis-labs/sqs/log" + txfeestypes "github.com/osmosis-labs/osmosis/v26/x/txfees/types" + + "github.com/osmosis-labs/osmosis/osmomath" + sdk "github.com/cosmos/cosmos-sdk/types" + txtypes "github.com/cosmos/cosmos-sdk/types/tx" ) // Order is order alias data structure for testing purposes. @@ -34,37 +37,19 @@ func ProcessOrderbooksAndGetClaimableOrders( 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 - -// SetBuildTx is used to override function that constructs a transaction. -// This function is used only for testing purposes. -func SetBuildTx(fn buildTxFunc) { - buildTx = fn -} - -// SendTxFunc is an alias for the sendTxFunc. -// This type is used only for testing purposes. -type SendTxFunc = sendTxFunc - -// SetSendTx is used to override function that sends a transaction to the blockchain. -// This function is used only for testing purposes. -func SetSendTx(fn sendTxFunc) { - sendTx = fn -} - // SendBatchClaimTx a test wrapper for sendBatchClaimTx. // This function is used only for testing purposes. func SendBatchClaimTx( ctx context.Context, keyring keyring.Keyring, - grpcClient *grpc.Client, accountQueryClient authtypes.QueryClient, + txfeesClient txfeestypes.QueryClient, + gasCalculator sqstx.GasCalculator, + txServiceClient txtypes.ServiceClient, contractAddress string, claims orderbookdomain.Orders, ) (*sdk.TxResponse, error) { - return sendBatchClaimTx(ctx, keyring, grpcClient, accountQueryClient, contractAddress, claims) + return sendBatchClaimTx(ctx, keyring, accountQueryClient, txfeesClient, gasCalculator, txServiceClient, contractAddress, claims) } // GetAccount is a test wrapper for getAccount. diff --git a/ingest/usecase/plugins/orderbook/claimbot/plugin.go b/ingest/usecase/plugins/orderbook/claimbot/plugin.go index e6c5b2d46..d4becf762 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/plugin.go +++ b/ingest/usecase/plugins/orderbook/claimbot/plugin.go @@ -6,16 +6,23 @@ import ( "sync/atomic" "time" - "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/domain/slices" "github.com/osmosis-labs/sqs/log" + + "github.com/osmosis-labs/osmosis/osmomath" + + txfeestypes "github.com/osmosis-labs/osmosis/v26/x/txfees/types" + + txtypes "github.com/cosmos/cosmos-sdk/types/tx" + "go.opentelemetry.io/otel" "go.uber.org/zap" ) @@ -28,12 +35,12 @@ type claimbot struct { orderbookusecase mvc.OrderBookUsecase orderbookRepository orderbookdomain.OrderBookRepository orderBookClient orderbookgrpcclientdomain.OrderBookClient - - accountQueryClient authtypes.QueryClient - grpcClient *grpc.Client - atomicBool atomic.Bool - - logger log.Logger + accountQueryClient authtypes.QueryClient + txfeesClient txfeestypes.QueryClient + gasCalculator sqstx.GasCalculator + txServiceClient txtypes.ServiceClient + atomicBool atomic.Bool + logger log.Logger } var _ domain.EndBlockProcessPlugin = &claimbot{} @@ -64,9 +71,11 @@ func New( return &claimbot{ accountQueryClient: authtypes.NewQueryClient(LCD), - grpcClient: grpcClient, keyring: keyring, orderbookusecase: orderbookusecase, + txfeesClient: txfeestypes.NewQueryClient(grpcClient), + gasCalculator: sqstx.NewGasCalculator(grpcClient), + txServiceClient: txtypes.NewServiceClient(grpcClient), orderbookRepository: orderbookRepository, orderBookClient: orderBookClient, poolsUseCase: poolsUseCase, @@ -130,11 +139,17 @@ func (o *claimbot) ProcessEndBlock(ctx context.Context, blockHeight uint64, meta func (o *claimbot) processBatchClaimOrders(ctx context.Context, orderbook domain.CanonicalOrderBooksResult, orders orderbookdomain.Orders) error { for _, chunk := range slices.Split(orders, 100) { + if len(chunk) == 0 { + continue + } + txres, err := sendBatchClaimTx( ctx, o.keyring, - o.grpcClient, o.accountQueryClient, + o.txfeesClient, + o.gasCalculator, + o.txServiceClient, orderbook.ContractAddress, chunk, ) diff --git a/ingest/usecase/plugins/orderbook/claimbot/tx.go b/ingest/usecase/plugins/orderbook/claimbot/tx.go index 176cee4c8..8d1d85fac 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/tx.go +++ b/ingest/usecase/plugins/orderbook/claimbot/tx.go @@ -6,18 +6,17 @@ import ( "fmt" "os" - "github.com/osmosis-labs/sqs/delivery/grpc" 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" orderbookdomain "github.com/osmosis-labs/sqs/domain/orderbook" "github.com/osmosis-labs/osmosis/v26/app" - "github.com/osmosis-labs/osmosis/v26/app/params" + txfeestypes "github.com/osmosis-labs/osmosis/v26/x/txfees/types" wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" - cosmosClient "github.com/cosmos/cosmos-sdk/client" sdk "github.com/cosmos/cosmos-sdk/types" + txtypes "github.com/cosmos/cosmos-sdk/types/tx" ) var ( @@ -40,37 +39,15 @@ func init() { } } -// buildTxFunc is a function signature for buildTx. -// nolint: unused -type buildTxFunc func( - ctx context.Context, - grpcClient *grpc.Client, - keyring keyring.Keyring, - encodingConfig params.EncodingConfig, - account sqstx.Account, - chainID string, - msg ...sdk.Msg, -) (cosmosClient.TxBuilder, error) - -// buildTx is a function that constructs a transaction. -// In testing, this function is replaced with a mock implementation. -var buildTx = sqstx.BuildTx - -// sendTxFunc is a function signature for sendTx. -// nolint: unused -type sendTxFunc func(ctx context.Context, grpcClient *grpc.Client, txBytes []byte) (*sdk.TxResponse, error) - -// SendTx is a function that sends a transaction to the blockchain. -// In testing, this function is replaced with a mock implementation. -var sendTx = sqstx.SendTx - // sendBatchClaimTx prepares and sends a batch claim transaction to the blockchain. // It builds the transaction, signs it, and broadcasts it to the network. func sendBatchClaimTx( ctx context.Context, keyring keyring.Keyring, - grpcClient *grpc.Client, accountQueryClient authtypes.QueryClient, + txfeesClient txfeestypes.QueryClient, + gasCalculator sqstx.GasCalculator, + txServiceClient txtypes.ServiceClient, contractAddress string, claims orderbookdomain.Orders, ) (*sdk.TxResponse, error) { @@ -88,7 +65,7 @@ func sendBatchClaimTx( msg := buildExecuteContractMsg(address, contractAddress, msgBytes) - tx, err := buildTx(ctx, grpcClient, keyring, encodingConfig, account, chainID, msg) + tx, err := sqstx.BuildTx(ctx, keyring, txfeesClient, gasCalculator, encodingConfig, account, chainID, msg) if err != nil { return nil, fmt.Errorf("failed to build transaction: %w", err) } @@ -98,8 +75,7 @@ func sendBatchClaimTx( return nil, fmt.Errorf("failed to encode transaction: %w", err) } - // Broadcast the transaction - return sendTx(ctx, grpcClient, txBytes) + return sqstx.SendTx(ctx, txServiceClient, txBytes) } // getAccount retrieves account information for a given address. diff --git a/ingest/usecase/plugins/orderbook/claimbot/tx_test.go b/ingest/usecase/plugins/orderbook/claimbot/tx_test.go index 405ef744a..8356f9cfb 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/tx_test.go +++ b/ingest/usecase/plugins/orderbook/claimbot/tx_test.go @@ -4,48 +4,26 @@ import ( "context" "testing" - "github.com/osmosis-labs/sqs/delivery/grpc" authtypes "github.com/osmosis-labs/sqs/domain/cosmos/auth/types" "github.com/osmosis-labs/sqs/domain/cosmos/tx" sqstx "github.com/osmosis-labs/sqs/domain/cosmos/tx" - "github.com/osmosis-labs/sqs/domain/keyring" "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/osmosis/v26/app/params" - - cosmosClient "github.com/cosmos/cosmos-sdk/client" - "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" sdk "github.com/cosmos/cosmos-sdk/types" + txtypes "github.com/cosmos/cosmos-sdk/types/tx" "github.com/stretchr/testify/assert" + "google.golang.org/grpc" ) func TestSendBatchClaimTx(t *testing.T) { - keyringWithGetAddressFunc := func(mock *mocks.Keyring, address string) { - mock.GetAddressFunc = func() sdk.AccAddress { - return sdk.AccAddress(address) - } - } - - keyringWithGetKeyFunc := func(mock *mocks.Keyring, key string) { - mock.GetKeyFunc = func() secp256k1.PrivKey { - return mock.GenPrivKey(key) - } - } - - authQueryClientWithGetAccountFunc := func(mock *mocks.AuthQueryClientMock, response *authtypes.QueryAccountResponse, err error) { - mock.GetAccountFunc = func(ctx context.Context, address string) (*authtypes.QueryAccountResponse, error) { - return response, err - } - } - tests := []struct { name string contractAddress string claims orderbookdomain.Orders - setupMocks func(*mocks.Keyring, *mocks.AuthQueryClientMock) + setupMocks func(*mocks.Keyring, *mocks.AuthQueryClientMock, *mocks.TxFeesQueryClient, *mocks.GasCalculator, *mocks.TxServiceClient) setSendTxFunc func() []byte expectedResponse *sdk.TxResponse expectedError bool @@ -56,78 +34,53 @@ func TestSendBatchClaimTx(t *testing.T) { claims: orderbookdomain.Orders{ {TickId: 13, OrderId: 99}, }, - setupMocks: func(keyringMock *mocks.Keyring, authQueryClient *mocks.AuthQueryClientMock) { - keyringWithGetAddressFunc(keyringMock, "osmo0address") - keyringWithGetKeyFunc(keyringMock, "6cf5103c60c939a5f38e383b52239c5296c968579eec1c68a47d70fbf1d19159") - authQueryClientWithGetAccountFunc(authQueryClient, nil, assert.AnError) + setupMocks: func(keyringMock *mocks.Keyring, authQueryClient *mocks.AuthQueryClientMock, txfeesClient *mocks.TxFeesQueryClient, gasCalculator *mocks.GasCalculator, txServiceClient *mocks.TxServiceClient) { + keyringMock.WithGetAddress("osmo0address") + keyringMock.WithGetKey("6cf5103c60c939a5f38e383b52239c5296c968579eec1c68a47d70fbf1d19159") + authQueryClient.WithGetAccount(nil, assert.AnError) }, expectedResponse: &sdk.TxResponse{}, expectedError: true, }, { - name: "SetBuildTx returns error", + name: "BuildTx returns error", contractAddress: "osmo1contractaddress", claims: orderbookdomain.Orders{ {TickId: 13, OrderId: 99}, }, - setupMocks: func(keyringMock *mocks.Keyring, authQueryClient *mocks.AuthQueryClientMock) { - keyringWithGetAddressFunc(keyringMock, "osmo0address") - keyringWithGetKeyFunc(keyringMock, "6cf5103c60c939a5f38e383b52239c5296c968579eec1c68a47d70fbf1d19159") - authQueryClientWithGetAccountFunc(authQueryClient, &authtypes.QueryAccountResponse{ + setupMocks: func(keyringMock *mocks.Keyring, authQueryClient *mocks.AuthQueryClientMock, txfeesClient *mocks.TxFeesQueryClient, gasCalculator *mocks.GasCalculator, txServiceClient *mocks.TxServiceClient) { + keyringMock.WithGetAddress("osmo0address") + keyringMock.WithGetKey("6cf5103c60c939a5f38e383b52239c5296c968579eec1c68a47d70fbf1d19159") + authQueryClient.WithGetAccount(&authtypes.QueryAccountResponse{ Account: authtypes.Account{ AccountNumber: 3, Sequence: 31, }, }, nil) - - claimbot.SetBuildTx(func( - ctx context.Context, - grpcClient *grpc.Client, - keyring keyring.Keyring, - encodingConfig params.EncodingConfig, - account sqstx.Account, - chainID string, - msg ...sdk.Msg, - ) (cosmosClient.TxBuilder, error) { - return nil, assert.AnError - }) + gasCalculator.WithCalculateGas(nil, 0, assert.AnError) // Fail BuildTx }, expectedResponse: &sdk.TxResponse{}, expectedError: true, }, { - name: "SetSendTx returns error", + name: "SendTx returns error", contractAddress: "osmo1contractaddress", claims: orderbookdomain.Orders{ {TickId: 13, OrderId: 99}, }, - setupMocks: func(keyringMock *mocks.Keyring, authQueryClient *mocks.AuthQueryClientMock) { - keyringWithGetAddressFunc(keyringMock, "osmo0address") - keyringWithGetKeyFunc(keyringMock, "6cf5103c60c939a5f38e383b52239c5296c968579eec1c68a47d70fbf1d19159") - authQueryClientWithGetAccountFunc(authQueryClient, &authtypes.QueryAccountResponse{ + setupMocks: func(keyringMock *mocks.Keyring, authQueryClient *mocks.AuthQueryClientMock, txfeesClient *mocks.TxFeesQueryClient, gasCalculator *mocks.GasCalculator, txServiceClient *mocks.TxServiceClient) { + keyringMock.WithGetAddress("osmo5address") + keyringMock.WithGetKey("6cf5103c60c939a5f38e383b52239c5296c968579eec1c68a47d70fbf1d19159") + gasCalculator.WithCalculateGas(nil, 51, nil) + txfeesClient.WithBaseDenom("uosmo", nil) + txfeesClient.WithGetEipBaseFee("0.2", nil) + authQueryClient.WithGetAccount(&authtypes.QueryAccountResponse{ Account: authtypes.Account{ - AccountNumber: 3, - Sequence: 31, + AccountNumber: 83, + Sequence: 5, }, }, nil) - - claimbot.SetBuildTx(func( - ctx context.Context, - grpcClient *grpc.Client, - keyring keyring.Keyring, - encodingConfig params.EncodingConfig, - account sqstx.Account, - chainID string, - msg ...sdk.Msg, - ) (cosmosClient.TxBuilder, error) { - builder := encodingConfig.TxConfig.NewTxBuilder() - builder.SetMsgs(msg...) - return builder, nil - }) - - claimbot.SetSendTx(func(ctx context.Context, grpcClient *grpc.Client, txBytes []byte) (*sdk.TxResponse, error) { - return nil, assert.AnError - }) + txServiceClient.WithBroadcastTx(nil, assert.AnError) // SendTx returns error }, expectedResponse: &sdk.TxResponse{}, expectedError: true, @@ -139,38 +92,29 @@ func TestSendBatchClaimTx(t *testing.T) { {TickId: 1, OrderId: 100}, {TickId: 2, OrderId: 200}, }, - setupMocks: func(keyringMock *mocks.Keyring, authQueryClient *mocks.AuthQueryClientMock) { - keyringWithGetAddressFunc(keyringMock, "osmo1address") - keyringWithGetKeyFunc(keyringMock, "6cf5103c60c939a5f38e383b52239c5296c968579eec1c68a47d70fbf1d19159") - authQueryClientWithGetAccountFunc(authQueryClient, &authtypes.QueryAccountResponse{ + setupMocks: func(keyringMock *mocks.Keyring, authQueryClient *mocks.AuthQueryClientMock, txfeesClient *mocks.TxFeesQueryClient, gasCalculator *mocks.GasCalculator, txServiceClient *mocks.TxServiceClient) { + keyringMock.WithGetAddress("osmo1address") + keyringMock.WithGetKey("6cf5103c60c939a5f38e383b52239c5296c968579eec1c68a47d70fbf1d19159") + gasCalculator.WithCalculateGas(nil, 51, nil) + txfeesClient.WithBaseDenom("uosmo", nil) + txfeesClient.WithGetEipBaseFee("0.15", nil) + authQueryClient.WithGetAccount(&authtypes.QueryAccountResponse{ Account: authtypes.Account{ AccountNumber: 1, Sequence: 1, }, }, nil) - claimbot.SetBuildTx(func( - ctx context.Context, - grpcClient *grpc.Client, - keyring keyring.Keyring, - encodingConfig params.EncodingConfig, - account sqstx.Account, - chainID string, - msg ...sdk.Msg, - ) (cosmosClient.TxBuilder, error) { - builder := encodingConfig.TxConfig.NewTxBuilder() - builder.SetMsgs(msg...) - return builder, nil - }) - - claimbot.SetSendTx(func(ctx context.Context, grpcClient *grpc.Client, txBytes []byte) (*sdk.TxResponse, error) { - return &sdk.TxResponse{ - Data: string(txBytes), // Assigning the txBytes to response Data to compare it later + txServiceClient.BroadcastTxFunc = func(ctx context.Context, in *txtypes.BroadcastTxRequest, opts ...grpc.CallOption) (*txtypes.BroadcastTxResponse, error) { + return &txtypes.BroadcastTxResponse{ + TxResponse: &sdk.TxResponse{ + Data: string(in.TxBytes), // Assigning the txBytes to response Data to compare it later + }, }, nil - }) + } }, expectedResponse: &sdk.TxResponse{ - Data: "\n\x90\x01\n\x8d\x01\n$/cosmwasm.wasm.v1.MsgExecuteContract\x12e\n\x1fosmo1daek6me3v9jxgun9wdes7m4n5q\x12\x14osmo1contractaddress\x1a,{\"batch_claim\":{\"orders\":[[1,100],[2,200]]}}\x12\x02\x12\x00", + Data: "\n\x90\x01\n\x8d\x01\n$/cosmwasm.wasm.v1.MsgExecuteContract\x12e\n\x1fosmo1daek6me3v9jxgun9wdes7m4n5q\x12\x14osmo1contractaddress\x1a,{\"batch_claim\":{\"orders\":[[1,100],[2,200]]}}\x12b\nP\nF\n\x1f/cosmos.crypto.secp256k1.PubKey\x12#\n!\x03\xef]m\xf2\x8a\bx\x1f\x9a%v]E\x9e\x96\xa8\x9dc6a\x1d\x1f\x8a\xb4\xd3/q,֍\xd3\xd0\x12\x04\n\x02\b\x01\x18\x01\x12\x0e\n\n\n\x05uosmo\x12\x018\x103\x1a@Xߠ&\xea\xb8\x0e\xefؓf\xb3\xe7DMӡW\x99h\u008e\xbdh\xef\\\xd3\xd7\x02\xf1\xdc\xe1&\r\x91\xdd\xcdtu\xee\xdeJ\x90\x1a\x7f\xb2(L\x15\xe0+'\xf5\xe3\fV\t3!\xa2,\x802z", }, expectedError: false, }, @@ -181,10 +125,13 @@ func TestSendBatchClaimTx(t *testing.T) { ctx := context.Background() keyring := mocks.Keyring{} authQueryClient := mocks.AuthQueryClientMock{} + txFeesClient := mocks.TxFeesQueryClient{} + gasCalculator := mocks.GasCalculator{} + txServiceClient := mocks.TxServiceClient{} - tt.setupMocks(&keyring, &authQueryClient) + tt.setupMocks(&keyring, &authQueryClient, &txFeesClient, &gasCalculator, &txServiceClient) - response, err := claimbot.SendBatchClaimTx(ctx, &keyring, nil, &authQueryClient, tt.contractAddress, tt.claims) + response, err := claimbot.SendBatchClaimTx(ctx, &keyring, &authQueryClient, &txFeesClient, &gasCalculator, &txServiceClient, tt.contractAddress, tt.claims) if tt.expectedError { assert.Error(t, err) } else { From 0183cce98aabe3c603a5fb4bbf213fb6c550835b Mon Sep 17 00:00:00 2001 From: Deividas Petraitis Date: Wed, 16 Oct 2024 14:10:11 +0300 Subject: [PATCH 13/33] BE-586 | Process block orderbooks, tests --- domain/mocks/pools_usecase_mock.go | 6 ++ .../plugins/orderbook/claimbot/config.go | 58 ++++++++++++ .../plugins/orderbook/claimbot/export_test.go | 6 ++ .../plugins/orderbook/claimbot/orderbook.go | 24 +++++ .../orderbook/claimbot/orderbook_test.go | 81 +++++++++++++++++ .../plugins/orderbook/claimbot/plugin.go | 88 ++++++------------- 6 files changed, 203 insertions(+), 60 deletions(-) create mode 100644 ingest/usecase/plugins/orderbook/claimbot/config.go create mode 100644 ingest/usecase/plugins/orderbook/claimbot/orderbook.go create mode 100644 ingest/usecase/plugins/orderbook/claimbot/orderbook_test.go diff --git a/domain/mocks/pools_usecase_mock.go b/domain/mocks/pools_usecase_mock.go index dee9b8ee6..dadb517e5 100644 --- a/domain/mocks/pools_usecase_mock.go +++ b/domain/mocks/pools_usecase_mock.go @@ -47,6 +47,12 @@ func (pm *PoolsUsecaseMock) GetAllCanonicalOrderbookPoolIDs() ([]domain.Canonica panic("unimplemented") } +func (pm *PoolsUsecaseMock) WithGetAllCanonicalOrderbookPoolIDs(result []domain.CanonicalOrderBooksResult, err error) { + pm.GetAllCanonicalOrderbookPoolIDsFunc = func() ([]domain.CanonicalOrderBooksResult, error) { + return result, err + } +} + // GetCanonicalOrderbookPool implements mvc.PoolsUsecase. func (pm *PoolsUsecaseMock) GetCanonicalOrderbookPool(baseDenom string, quoteDenom string) (uint64, string, error) { panic("unimplemented") diff --git a/ingest/usecase/plugins/orderbook/claimbot/config.go b/ingest/usecase/plugins/orderbook/claimbot/config.go new file mode 100644 index 000000000..6d24a070a --- /dev/null +++ b/ingest/usecase/plugins/orderbook/claimbot/config.go @@ -0,0 +1,58 @@ +package claimbot + +import ( + "github.com/osmosis-labs/sqs/delivery/grpc" + 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" + + txfeestypes "github.com/osmosis-labs/osmosis/v26/x/txfees/types" + + txtypes "github.com/cosmos/cosmos-sdk/types/tx" +) + +// Config is the configuration for the claimbot plugin +type Config struct { + Keyring keyring.Keyring + PoolsUseCase mvc.PoolsUsecase + OrderbookUsecase mvc.OrderBookUsecase + OrderbookRepository orderbookdomain.OrderBookRepository + OrderBookClient orderbookgrpcclientdomain.OrderBookClient + AccountQueryClient authtypes.QueryClient + TxfeesClient txfeestypes.QueryClient + GasCalculator sqstx.GasCalculator + TxServiceClient txtypes.ServiceClient + Logger log.Logger +} + +// NewConfig creates a new Config instance. +func NewConfig( + keyring keyring.Keyring, + orderbookusecase mvc.OrderBookUsecase, + poolsUseCase mvc.PoolsUsecase, + orderbookRepository orderbookdomain.OrderBookRepository, + orderBookClient orderbookgrpcclientdomain.OrderBookClient, + logger log.Logger, +) (*Config, error) { + grpcClient, err := grpc.NewClient(RPC) + if err != nil { + return nil, err + } + + return &Config{ + Keyring: keyring, + PoolsUseCase: poolsUseCase, + OrderbookUsecase: orderbookusecase, + OrderbookRepository: orderbookRepository, + OrderBookClient: orderBookClient, + AccountQueryClient: authtypes.NewQueryClient(LCD), + TxfeesClient: txfeestypes.NewQueryClient(grpcClient), + GasCalculator: sqstx.NewGasCalculator(grpcClient), + TxServiceClient: txtypes.NewServiceClient(grpcClient), + Logger: logger, + }, nil +} diff --git a/ingest/usecase/plugins/orderbook/claimbot/export_test.go b/ingest/usecase/plugins/orderbook/claimbot/export_test.go index 1f21fbfea..5560aa5c5 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/export_test.go +++ b/ingest/usecase/plugins/orderbook/claimbot/export_test.go @@ -63,3 +63,9 @@ func GetAccount(ctx context.Context, client authtypes.QueryClient, address strin func PrepareBatchClaimMsg(claims orderbookdomain.Orders) ([]byte, error) { return prepareBatchClaimMsg(claims) } + +// GetOrderbooks is a test wrapper for getOrderbooks. +// This function is exported for testing purposes. +func GetOrderbooks(poolsUsecase mvc.PoolsUsecase, blockHeight uint64, metadata domain.BlockPoolMetadata) ([]domain.CanonicalOrderBooksResult, error) { + return getOrderbooks(poolsUsecase, blockHeight, metadata) +} diff --git a/ingest/usecase/plugins/orderbook/claimbot/orderbook.go b/ingest/usecase/plugins/orderbook/claimbot/orderbook.go new file mode 100644 index 000000000..f93c032e2 --- /dev/null +++ b/ingest/usecase/plugins/orderbook/claimbot/orderbook.go @@ -0,0 +1,24 @@ +package claimbot + +import ( + "fmt" + + "github.com/osmosis-labs/sqs/domain" + "github.com/osmosis-labs/sqs/domain/mvc" +) + +// getOrderbooks returns canonical orderbooks that are within the metadata. +func getOrderbooks(poolsUsecase mvc.PoolsUsecase, blockHeight uint64, metadata domain.BlockPoolMetadata) ([]domain.CanonicalOrderBooksResult, error) { + orderbooks, err := poolsUsecase.GetAllCanonicalOrderbookPoolIDs() + if err != nil { + return nil, fmt.Errorf("failed to get all canonical orderbook pool IDs ( block height %d ) : %w", blockHeight, err) + } + + var result []domain.CanonicalOrderBooksResult + for _, orderbook := range orderbooks { + if _, ok := metadata.PoolIDs[orderbook.PoolID]; ok { + result = append(result, orderbook) + } + } + return result, nil +} diff --git a/ingest/usecase/plugins/orderbook/claimbot/orderbook_test.go b/ingest/usecase/plugins/orderbook/claimbot/orderbook_test.go new file mode 100644 index 000000000..9bf788368 --- /dev/null +++ b/ingest/usecase/plugins/orderbook/claimbot/orderbook_test.go @@ -0,0 +1,81 @@ +package claimbot_test + +import ( + "testing" + + "github.com/osmosis-labs/sqs/domain" + "github.com/osmosis-labs/sqs/domain/mocks" + "github.com/osmosis-labs/sqs/ingest/usecase/plugins/orderbook/claimbot" + + "github.com/stretchr/testify/assert" +) + +func TestGetOrderbooks(t *testing.T) { + tests := []struct { + name string + blockHeight uint64 + metadata domain.BlockPoolMetadata + setupMocks func(*mocks.PoolsUsecaseMock) + want []domain.CanonicalOrderBooksResult + err bool + }{ + { + name: "Metadata contains all canonical orderbooks but one", + blockHeight: 1000, + metadata: domain.BlockPoolMetadata{ + PoolIDs: map[uint64]struct{}{1: {}, 2: {}, 3: {}}, + }, + setupMocks: func(poolsUsecase *mocks.PoolsUsecaseMock) { + poolsUsecase.WithGetAllCanonicalOrderbookPoolIDs([]domain.CanonicalOrderBooksResult{ + {PoolID: 1}, {PoolID: 2}, {PoolID: 3}, {PoolID: 4}, + }, nil) + }, + want: []domain.CanonicalOrderBooksResult{ + {PoolID: 1}, {PoolID: 2}, {PoolID: 3}, + }, + err: false, + }, + { + name: "Metadata contains only canonical orderbooks", + blockHeight: 1893, + metadata: domain.BlockPoolMetadata{ + PoolIDs: map[uint64]struct{}{1: {}, 2: {}, 3: {}}, + }, + setupMocks: func(poolsUsecase *mocks.PoolsUsecaseMock) { + poolsUsecase.WithGetAllCanonicalOrderbookPoolIDs([]domain.CanonicalOrderBooksResult{ + {PoolID: 1}, {PoolID: 2}, {PoolID: 3}, + }, nil) + }, + want: []domain.CanonicalOrderBooksResult{ + {PoolID: 1}, {PoolID: 2}, {PoolID: 3}, + }, + err: false, + }, + { + name: "Error getting all canonical orderbook pool IDs", + blockHeight: 2000, + metadata: domain.BlockPoolMetadata{}, + setupMocks: func(poolsUsecase *mocks.PoolsUsecaseMock) { + poolsUsecase.WithGetAllCanonicalOrderbookPoolIDs(nil, assert.AnError) + }, + want: nil, + err: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + poolsUsecase := mocks.PoolsUsecaseMock{} + + tt.setupMocks(&poolsUsecase) + + got, err := claimbot.GetOrderbooks(&poolsUsecase, tt.blockHeight, tt.metadata) + if tt.err { + assert.Error(t, err) + return + } + + assert.Equal(t, got, tt.want) + }) + } +} diff --git a/ingest/usecase/plugins/orderbook/claimbot/plugin.go b/ingest/usecase/plugins/orderbook/claimbot/plugin.go index d4becf762..cbecd82c3 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/plugin.go +++ b/ingest/usecase/plugins/orderbook/claimbot/plugin.go @@ -6,10 +6,7 @@ import ( "sync/atomic" "time" - "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" @@ -19,10 +16,6 @@ import ( "github.com/osmosis-labs/osmosis/osmomath" - txfeestypes "github.com/osmosis-labs/osmosis/v26/x/txfees/types" - - txtypes "github.com/cosmos/cosmos-sdk/types/tx" - "go.opentelemetry.io/otel" "go.uber.org/zap" ) @@ -30,17 +23,8 @@ import ( // claimbot is a claim bot that processes and claims eligible orderbook orders at the end of each block. // Claimable orders are determined based on order filled percentage that is handled with fillThreshold package level variable. type claimbot struct { - keyring keyring.Keyring - poolsUseCase mvc.PoolsUsecase - orderbookusecase mvc.OrderBookUsecase - orderbookRepository orderbookdomain.OrderBookRepository - orderBookClient orderbookgrpcclientdomain.OrderBookClient - accountQueryClient authtypes.QueryClient - txfeesClient txfeestypes.QueryClient - gasCalculator sqstx.GasCalculator - txServiceClient txtypes.ServiceClient - atomicBool atomic.Bool - logger log.Logger + config *Config + atomicBool atomic.Bool } var _ domain.EndBlockProcessPlugin = &claimbot{} @@ -54,35 +38,27 @@ var ( fillThreshold = osmomath.MustNewDecFromStr("0.98") ) +// maxBatchOfClaimableOrders is the maximum number of claimable orders +// that can be processed in a single batch. +const maxBatchOfClaimableOrders = 100 + // New creates and returns a new claimbot instance. func New( keyring keyring.Keyring, orderbookusecase mvc.OrderBookUsecase, - poolsUseCase mvc.PoolsUsecase, + poolsUsecase mvc.PoolsUsecase, orderbookRepository orderbookdomain.OrderBookRepository, orderBookClient orderbookgrpcclientdomain.OrderBookClient, logger log.Logger, ) (*claimbot, error) { - // Create a connection to the gRPC server. - grpcClient, err := grpc.NewClient(RPC) + config, err := NewConfig(keyring, orderbookusecase, poolsUsecase, orderbookRepository, orderBookClient, logger) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to create claimbot config: %w", err) } return &claimbot{ - accountQueryClient: authtypes.NewQueryClient(LCD), - keyring: keyring, - orderbookusecase: orderbookusecase, - txfeesClient: txfeestypes.NewQueryClient(grpcClient), - gasCalculator: sqstx.NewGasCalculator(grpcClient), - txServiceClient: txtypes.NewServiceClient(grpcClient), - orderbookRepository: orderbookRepository, - orderBookClient: orderBookClient, - poolsUseCase: poolsUseCase, - + config: config, atomicBool: atomic.Bool{}, - - logger: logger, }, nil } @@ -96,12 +72,12 @@ func (o *claimbot) ProcessEndBlock(ctx context.Context, blockHeight uint64, meta // For simplicity, we allow only one block to be processed at a time. // This may be relaxed in the future. if !o.atomicBool.CompareAndSwap(false, true) { - o.logger.Info("orderbook claimer is already in progress", zap.Uint64("block_height", blockHeight)) + o.config.Logger.Info("orderbook claimer is already in progress", zap.Uint64("block_height", blockHeight)) return nil } defer o.atomicBool.Store(false) - orderbooks, err := o.getOrderbooks(ctx, blockHeight, metadata) + orderbooks, err := getOrderbooks(o.config.PoolsUseCase, blockHeight, metadata) if err != nil { return err } @@ -111,10 +87,10 @@ func (o *claimbot) ProcessEndBlock(ctx context.Context, blockHeight uint64, meta ctx, fillThreshold, orderbooks, - o.orderbookRepository, - o.orderBookClient, - o.orderbookusecase, - o.logger, + o.config.OrderbookRepository, + o.config.OrderBookClient, + o.config.OrderbookUsecase, + o.config.Logger, ) for _, orderbook := range orders { @@ -123,8 +99,8 @@ func (o *claimbot) ProcessEndBlock(ctx context.Context, blockHeight uint64, meta continue } - if err := o.processBatchClaimOrders(ctx, orderbook.Orderbook, orderbook.Orders); err != nil { - o.logger.Info( + if err := o.processOrderbookOrders(ctx, orderbook.Orderbook, orderbook.Orders); err != nil { + o.config.Logger.Info( "failed to process orderbook orders", zap.String("contract_address", orderbook.Orderbook.ContractAddress), zap.Error(err), @@ -132,30 +108,31 @@ func (o *claimbot) ProcessEndBlock(ctx context.Context, blockHeight uint64, meta } } - o.logger.Info("processed end block in orderbook claimer ingest plugin", zap.Uint64("block_height", blockHeight)) + o.config.Logger.Info("processed end block in orderbook claimer ingest plugin", zap.Uint64("block_height", blockHeight)) return nil } -func (o *claimbot) processBatchClaimOrders(ctx context.Context, orderbook domain.CanonicalOrderBooksResult, orders orderbookdomain.Orders) error { - for _, chunk := range slices.Split(orders, 100) { +// processOrderbookOrders processes a batch of claimable orders. +func (o *claimbot) processOrderbookOrders(ctx context.Context, orderbook domain.CanonicalOrderBooksResult, orders orderbookdomain.Orders) error { + for _, chunk := range slices.Split(orders, maxBatchOfClaimableOrders) { if len(chunk) == 0 { continue } txres, err := sendBatchClaimTx( ctx, - o.keyring, - o.accountQueryClient, - o.txfeesClient, - o.gasCalculator, - o.txServiceClient, + o.config.Keyring, + o.config.AccountQueryClient, + o.config.TxfeesClient, + o.config.GasCalculator, + o.config.TxServiceClient, orderbook.ContractAddress, chunk, ) if err != nil { - o.logger.Info( + o.config.Logger.Info( "failed to sent batch claim tx", zap.String("contract_address", orderbook.ContractAddress), zap.Any("tx_result", txres), @@ -171,12 +148,3 @@ func (o *claimbot) processBatchClaimOrders(ctx context.Context, orderbook domain return nil } - -// TODO: process only block orderbooks -func (o *claimbot) getOrderbooks(ctx context.Context, blockHeight uint64, metadata domain.BlockPoolMetadata) ([]domain.CanonicalOrderBooksResult, error) { - orderbooks, err := o.poolsUseCase.GetAllCanonicalOrderbookPoolIDs() - if err != nil { - return nil, fmt.Errorf("failed to get all canonical orderbook pool IDs : %w", err) - } - return orderbooks, nil -} From 54d41bd3f9f04e4f16e83cf6f34a73b50ce05963 Mon Sep 17 00:00:00 2001 From: Deividas Petraitis Date: Thu, 17 Oct 2024 14:39:08 +0300 Subject: [PATCH 14/33] BE-586 | Requested changes --- domain/cosmos/auth/types/types.go | 71 ++++++---------- domain/cosmos/auth/types/types_test.go | 80 ------------------- domain/cosmos/tx/tx.go | 11 +-- domain/cosmos/tx/tx_test.go | 15 ++-- domain/mocks/auth.go | 16 ++-- .../plugins/orderbook/claimbot/config.go | 2 +- .../plugins/orderbook/claimbot/export_test.go | 6 -- .../usecase/plugins/orderbook/claimbot/tx.go | 19 +---- .../plugins/orderbook/claimbot/tx_test.go | 80 +++---------------- 9 files changed, 60 insertions(+), 240 deletions(-) delete mode 100644 domain/cosmos/auth/types/types_test.go diff --git a/domain/cosmos/auth/types/types.go b/domain/cosmos/auth/types/types.go index cbb402149..5a12920dd 100644 --- a/domain/cosmos/auth/types/types.go +++ b/domain/cosmos/auth/types/types.go @@ -3,75 +3,56 @@ package types import ( "context" - "encoding/json" - "strconv" + "fmt" - "github.com/osmosis-labs/sqs/delivery/http" + "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/cosmos/gogoproto/grpc" + "github.com/osmosis-labs/osmosis/v26/app" +) + +var ( + encodingConfig = app.MakeEncodingConfig() ) // QueryClient is the client API for Query service. type QueryClient interface { // GetAccount retrieves account information for a given address. - GetAccount(ctx context.Context, address string) (*QueryAccountResponse, error) + GetAccount(ctx context.Context, address string) (*authtypes.BaseAccount, error) } // NewQueryClient creates a new QueryClient instance with the provided LCD (Light Client Daemon) endpoint. -func NewQueryClient(lcd string) QueryClient { - return &queryClient{lcd} +func NewQueryClient(conn grpc.ClientConn) QueryClient { + return &queryClient{ + queryClient: authtypes.NewQueryClient(conn), + } } var _ QueryClient = &queryClient{} // queryClient is an implementation of the QueryClient interface. type queryClient struct { - lcd string -} - -// Account represents the basic account information. -type Account struct { - Sequence uint64 `json:"sequence"` // Current sequence (nonce) of the account, used to prevent replay attacks. - AccountNumber uint64 `json:"account_number"` // Unique identifier of the account on the blockchain. -} - -// QueryAccountResponse encapsulates the response for an account query. -type QueryAccountResponse struct { - Account Account `json:"account"` + queryClient authtypes.QueryClient } -// GetAccount retrieves account information for a given address. -func (c *queryClient) GetAccount(ctx context.Context, address string) (*QueryAccountResponse, error) { - resp, err := http.Get(ctx, c.lcd+"/cosmos/auth/v1beta1/accounts/"+address) - if err != nil { - return nil, err - } - - type queryAccountResponse struct { - Account struct { - Sequence string `json:"sequence"` - AccountNumber string `json:"account_number"` - } `json:"account"` - } - - var accountRes queryAccountResponse - err = json.Unmarshal(resp, &accountRes) +// GetAccount returns the account information for the given address. +func (c *queryClient) GetAccount(ctx context.Context, address string) (*authtypes.BaseAccount, error) { + resp, err := c.queryClient.Account(ctx, &authtypes.QueryAccountRequest{ + Address: address, + }) if err != nil { return nil, err } - sequence, err := strconv.ParseUint(accountRes.Account.Sequence, 10, 64) - if err != nil { + var account types.AccountI + if err := encodingConfig.InterfaceRegistry.UnpackAny(resp.Account, &account); err != nil { return nil, err } - accountNumber, err := strconv.ParseUint(accountRes.Account.AccountNumber, 10, 64) - if err != nil { - return nil, err + baseAccount, ok := account.(*authtypes.BaseAccount) + if !ok { + return nil, fmt.Errorf("account is not of the type of BaseAccount") } - return &QueryAccountResponse{ - Account: Account{ - Sequence: sequence, - AccountNumber: accountNumber, - }, - }, nil + return baseAccount, nil } diff --git a/domain/cosmos/auth/types/types_test.go b/domain/cosmos/auth/types/types_test.go deleted file mode 100644 index b02807589..000000000 --- a/domain/cosmos/auth/types/types_test.go +++ /dev/null @@ -1,80 +0,0 @@ -package types_test - -import ( - "context" - "net/http" - "net/http/httptest" - "testing" - - authtypes "github.com/osmosis-labs/sqs/domain/cosmos/auth/types" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestGetAccount(t *testing.T) { - tests := []struct { - name string - address string - mockResponse string - expectedResult *authtypes.QueryAccountResponse - expectedError bool - }{ - { - name: "Valid account", - address: "cosmos1abcde", - mockResponse: `{ - "account": { - "sequence": "10", - "account_number": "100" - } - }`, - expectedResult: &authtypes.QueryAccountResponse{ - Account: authtypes.Account{ - Sequence: 10, - AccountNumber: 100, - }, - }, - expectedError: false, - }, - { - name: "Invalid JSON response", - address: "cosmos1fghij", - mockResponse: `{ - "account": { - "sequence": "invalid", - "account_number": "100" - } - }`, - expectedResult: nil, - expectedError: true, - }, - { - name: "Empty response", - address: "cosmos1klmno", - mockResponse: `{}`, - expectedResult: nil, - expectedError: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - w.Write([]byte(tt.mockResponse)) - })) - defer server.Close() - - client := authtypes.NewQueryClient(server.URL) - result, err := client.GetAccount(context.Background(), tt.address) - - if tt.expectedError { - assert.Error(t, err) - } else { - require.NoError(t, err) - assert.Equal(t, tt.expectedResult, result) - } - }) - } -} diff --git a/domain/cosmos/tx/tx.go b/domain/cosmos/tx/tx.go index 2ba12c10d..2c6673523 100644 --- a/domain/cosmos/tx/tx.go +++ b/domain/cosmos/tx/tx.go @@ -18,14 +18,9 @@ import ( txtypes "github.com/cosmos/cosmos-sdk/types/tx" signingtypes "github.com/cosmos/cosmos-sdk/types/tx/signing" authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" ) -// Account represents the account information required for transaction building and signing. -type Account struct { - Sequence uint64 // Current sequence (nonce) of the account, used to prevent replay attacks. - AccountNumber uint64 // Unique identifier of the account on the blockchain. -} - // BuildTx constructs a transaction using the provided parameters and messages. // Returns a TxBuilder and any error encountered. func BuildTx( @@ -34,7 +29,7 @@ func BuildTx( txfeesClient txfeestypes.QueryClient, gasCalculator GasCalculator, encodingConfig params.EncodingConfig, - account Account, + account *authtypes.BaseAccount, chainID string, msg ...sdk.Msg, ) (cosmosClient.TxBuilder, error) { @@ -115,7 +110,7 @@ func SendTx(ctx context.Context, txServiceClient txtypes.ServiceClient, txBytes func SimulateMsgs( gasCalculator GasCalculator, encodingConfig params.EncodingConfig, - account Account, + account *authtypes.BaseAccount, chainID string, msgs []sdk.Msg, ) (*txtypes.SimulateResponse, uint64, error) { diff --git a/domain/cosmos/tx/tx_test.go b/domain/cosmos/tx/tx_test.go index a23205b77..a48857d5a 100644 --- a/domain/cosmos/tx/tx_test.go +++ b/domain/cosmos/tx/tx_test.go @@ -17,6 +17,7 @@ import ( txtypes "github.com/cosmos/cosmos-sdk/types/tx" signingtypes "github.com/cosmos/cosmos-sdk/types/tx/signing" authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" @@ -41,7 +42,7 @@ func TestBuildTx(t *testing.T) { testCases := []struct { name string setupMocks func(calculator *mocks.GasCalculator, txFeesClient *mocks.TxFeesQueryClient, keyring *mocks.Keyring) - account sqstx.Account + account *authtypes.BaseAccount chainID string msgs []sdk.Msg expectedJSON []byte @@ -55,7 +56,7 @@ func TestBuildTx(t *testing.T) { txFeesClient.WithBaseDenom("eth", nil) txFeesClient.WithGetEipBaseFee("0.1", nil) }, - account: sqstx.Account{ + account: &authtypes.BaseAccount{ Sequence: 13, AccountNumber: 1, }, @@ -70,6 +71,10 @@ func TestBuildTx(t *testing.T) { calculator.WithCalculateGas(nil, 50, assert.AnError) keyring.WithGetKey("6cf5103c60c939a5f38e383b52239c5296c968579eec1c68a47d70fbf1d19159") }, + account: &authtypes.BaseAccount{ + Sequence: 8, + AccountNumber: 51, + }, expectedError: true, }, } @@ -161,7 +166,7 @@ func TestSendTx(t *testing.T) { func TestSimulateMsgs(t *testing.T) { tests := []struct { name string - account sqstx.Account + account *authtypes.BaseAccount chainID string msgs []sdk.Msg setupMocks func(calculator *mocks.GasCalculator) @@ -171,7 +176,7 @@ func TestSimulateMsgs(t *testing.T) { }{ { name: "Successful simulation", - account: sqstx.Account{AccountNumber: 1, Sequence: 1}, + account: &authtypes.BaseAccount{AccountNumber: 1, Sequence: 1}, chainID: "test-chain", msgs: []sdk.Msg{newMsg("sender", "contract", `{}`)}, setupMocks: func(calculator *mocks.GasCalculator) { @@ -183,7 +188,7 @@ func TestSimulateMsgs(t *testing.T) { }, { name: "Simulation error", - account: sqstx.Account{AccountNumber: 2, Sequence: 2}, + account: &authtypes.BaseAccount{AccountNumber: 2, Sequence: 2}, chainID: "test-chain", msgs: []sdk.Msg{}, setupMocks: func(calculator *mocks.GasCalculator) { diff --git a/domain/mocks/auth.go b/domain/mocks/auth.go index 06bfab137..ca9f0dc4f 100644 --- a/domain/mocks/auth.go +++ b/domain/mocks/auth.go @@ -3,24 +3,26 @@ package mocks import ( "context" - authtypes "github.com/osmosis-labs/sqs/domain/cosmos/auth/types" + "github.com/osmosis-labs/sqs/domain/cosmos/auth/types" + + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" ) -var _ authtypes.QueryClient = &AuthQueryClientMock{} +var _ types.QueryClient = &AuthQueryClientMock{} type AuthQueryClientMock struct { - GetAccountFunc func(ctx context.Context, address string) (*authtypes.QueryAccountResponse, error) + GetAccountFunc func(ctx context.Context, address string) (*authtypes.BaseAccount, error) } -func (m *AuthQueryClientMock) GetAccount(ctx context.Context, address string) (*authtypes.QueryAccountResponse, error) { +func (m *AuthQueryClientMock) GetAccount(ctx context.Context, address string) (*authtypes.BaseAccount, error) { if m.GetAccountFunc != nil { return m.GetAccountFunc(ctx, address) } panic("GetAccountFunc has not been mocked") } -func (m *AuthQueryClientMock) WithGetAccount(response *authtypes.QueryAccountResponse, err error) { - m.GetAccountFunc = func(ctx context.Context, address string) (*authtypes.QueryAccountResponse, error) { - return response, err +func (m *AuthQueryClientMock) WithGetAccount(account *authtypes.BaseAccount, err error) { + m.GetAccountFunc = func(ctx context.Context, address string) (*authtypes.BaseAccount, error) { + return account, err } } diff --git a/ingest/usecase/plugins/orderbook/claimbot/config.go b/ingest/usecase/plugins/orderbook/claimbot/config.go index 6d24a070a..b634250fd 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/config.go +++ b/ingest/usecase/plugins/orderbook/claimbot/config.go @@ -49,7 +49,7 @@ func NewConfig( OrderbookUsecase: orderbookusecase, OrderbookRepository: orderbookRepository, OrderBookClient: orderBookClient, - AccountQueryClient: authtypes.NewQueryClient(LCD), + AccountQueryClient: authtypes.NewQueryClient(grpcClient), TxfeesClient: txfeestypes.NewQueryClient(grpcClient), GasCalculator: sqstx.NewGasCalculator(grpcClient), TxServiceClient: txtypes.NewServiceClient(grpcClient), diff --git a/ingest/usecase/plugins/orderbook/claimbot/export_test.go b/ingest/usecase/plugins/orderbook/claimbot/export_test.go index 5560aa5c5..a013e8490 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/export_test.go +++ b/ingest/usecase/plugins/orderbook/claimbot/export_test.go @@ -52,12 +52,6 @@ func SendBatchClaimTx( return sendBatchClaimTx(ctx, keyring, accountQueryClient, txfeesClient, gasCalculator, txServiceClient, contractAddress, claims) } -// 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 is a test wrapper for prepareBatchClaimMsg. // This function is exported for testing purposes. func PrepareBatchClaimMsg(claims orderbookdomain.Orders) ([]byte, error) { diff --git a/ingest/usecase/plugins/orderbook/claimbot/tx.go b/ingest/usecase/plugins/orderbook/claimbot/tx.go index 8d1d85fac..c8be6bb44 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/tx.go +++ b/ingest/usecase/plugins/orderbook/claimbot/tx.go @@ -23,7 +23,6 @@ var ( chainID = "osmosis-1" RPC = "localhost:9090" - LCD = "http://127.0.0.1:1317" encodingConfig = app.MakeEncodingConfig() ) @@ -33,10 +32,6 @@ func init() { if rpc := os.Getenv("OSMOSIS_RPC_ENDPOINT"); len(rpc) > 0 { RPC = rpc } - - if lcd := os.Getenv("OSMOSIS_LCD_ENDPOINT"); len(lcd) > 0 { - LCD = lcd - } } // sendBatchClaimTx prepares and sends a batch claim transaction to the blockchain. @@ -53,7 +48,7 @@ func sendBatchClaimTx( ) (*sdk.TxResponse, error) { address := keyring.GetAddress().String() - account, err := getAccount(ctx, accountQueryClient, address) + account, err := accountQueryClient.GetAccount(ctx, address) if err != nil { return nil, err } @@ -78,18 +73,6 @@ func sendBatchClaimTx( return sqstx.SendTx(ctx, txServiceClient, txBytes) } -// getAccount retrieves account information for a given address. -func getAccount(ctx context.Context, accountQueryClient authtypes.QueryClient, address string) (sqstx.Account, error) { - account, err := accountQueryClient.GetAccount(ctx, address) - if err != nil { - return sqstx.Account{}, fmt.Errorf("failed to get account: %w", err) - } - return sqstx.Account{ - Sequence: account.Account.Sequence, - AccountNumber: account.Account.AccountNumber, - }, nil -} - // prepareBatchClaimMsg creates a JSON-encoded batch claim message from the provided orders. func prepareBatchClaimMsg(claims orderbookdomain.Orders) ([]byte, error) { orders := make([][]int64, len(claims)) diff --git a/ingest/usecase/plugins/orderbook/claimbot/tx_test.go b/ingest/usecase/plugins/orderbook/claimbot/tx_test.go index 8356f9cfb..749e04da6 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/tx_test.go +++ b/ingest/usecase/plugins/orderbook/claimbot/tx_test.go @@ -4,15 +4,13 @@ import ( "context" "testing" - authtypes "github.com/osmosis-labs/sqs/domain/cosmos/auth/types" - "github.com/osmosis-labs/sqs/domain/cosmos/tx" - sqstx "github.com/osmosis-labs/sqs/domain/cosmos/tx" "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" sdk "github.com/cosmos/cosmos-sdk/types" txtypes "github.com/cosmos/cosmos-sdk/types/tx" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" "github.com/stretchr/testify/assert" "google.golang.org/grpc" @@ -51,11 +49,9 @@ func TestSendBatchClaimTx(t *testing.T) { setupMocks: func(keyringMock *mocks.Keyring, authQueryClient *mocks.AuthQueryClientMock, txfeesClient *mocks.TxFeesQueryClient, gasCalculator *mocks.GasCalculator, txServiceClient *mocks.TxServiceClient) { keyringMock.WithGetAddress("osmo0address") keyringMock.WithGetKey("6cf5103c60c939a5f38e383b52239c5296c968579eec1c68a47d70fbf1d19159") - authQueryClient.WithGetAccount(&authtypes.QueryAccountResponse{ - Account: authtypes.Account{ - AccountNumber: 3, - Sequence: 31, - }, + authQueryClient.WithGetAccount(&authtypes.BaseAccount{ + AccountNumber: 3, + Sequence: 31, }, nil) gasCalculator.WithCalculateGas(nil, 0, assert.AnError) // Fail BuildTx }, @@ -74,11 +70,9 @@ func TestSendBatchClaimTx(t *testing.T) { gasCalculator.WithCalculateGas(nil, 51, nil) txfeesClient.WithBaseDenom("uosmo", nil) txfeesClient.WithGetEipBaseFee("0.2", nil) - authQueryClient.WithGetAccount(&authtypes.QueryAccountResponse{ - Account: authtypes.Account{ - AccountNumber: 83, - Sequence: 5, - }, + authQueryClient.WithGetAccount(&authtypes.BaseAccount{ + AccountNumber: 83, + Sequence: 5, }, nil) txServiceClient.WithBroadcastTx(nil, assert.AnError) // SendTx returns error }, @@ -98,11 +92,9 @@ func TestSendBatchClaimTx(t *testing.T) { gasCalculator.WithCalculateGas(nil, 51, nil) txfeesClient.WithBaseDenom("uosmo", nil) txfeesClient.WithGetEipBaseFee("0.15", nil) - authQueryClient.WithGetAccount(&authtypes.QueryAccountResponse{ - Account: authtypes.Account{ - AccountNumber: 1, - Sequence: 1, - }, + authQueryClient.WithGetAccount(&authtypes.BaseAccount{ + AccountNumber: 1, + Sequence: 1, }, nil) txServiceClient.BroadcastTxFunc = func(ctx context.Context, in *txtypes.BroadcastTxRequest, opts ...grpc.CallOption) (*txtypes.BroadcastTxResponse, error) { @@ -142,58 +134,6 @@ func TestSendBatchClaimTx(t *testing.T) { } } -func TestGetAccount(t *testing.T) { - newQueryClient := func(resp *authtypes.QueryAccountResponse, err error) authtypes.QueryClient { - return &mocks.AuthQueryClientMock{ - GetAccountFunc: func(ctx context.Context, address string) (*authtypes.QueryAccountResponse, error) { - return resp, err - }, - } - } - tests := []struct { - name string - address string - queryClient authtypes.QueryClient - expectedResult sqstx.Account - expectedError bool - }{ - { - name: "Successful account retrieval", - address: "osmo1f4tvsdukfwh6s9swrc24gkuz23tp8pd3e9r5fa", - queryClient: newQueryClient(&authtypes.QueryAccountResponse{ - Account: authtypes.Account{ - Sequence: 123, - AccountNumber: 456, - }, - }, nil), - expectedResult: tx.Account{ - Sequence: 123, - AccountNumber: 456, - }, - expectedError: false, - }, - { - name: "Error retrieving account", - address: "osmo1jllfytsz4dryxhz5tl7u73v29exsf80vz52ucc", - queryClient: newQueryClient(nil, assert.AnError), - expectedResult: tx.Account{}, - expectedError: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result, err := claimbot.GetAccount(context.Background(), tt.queryClient, tt.address) - if tt.expectedError { - assert.Error(t, err) - } else { - assert.NoError(t, err) - assert.Equal(t, tt.expectedResult, result) - } - }) - } -} - func TestPrepareBatchClaimMsg(t *testing.T) { tests := []struct { name string From 92ee4b29f583cd39b059d13a6a80946632c51daf Mon Sep 17 00:00:00 2001 From: Deividas Petraitis Date: Thu, 17 Oct 2024 16:22:55 +0300 Subject: [PATCH 15/33] BE-586 | Config update --- app/sidecar_query_server.go | 4 +-- config.json | 32 +++++++++++-------- domain/config.go | 12 +++++-- domain/orderbook/plugin/config.go | 6 ++-- .../plugins/orderbook/fillbot/README.md | 2 +- 5 files changed, 34 insertions(+), 22 deletions(-) diff --git a/app/sidecar_query_server.go b/app/sidecar_query_server.go index 95a62ec01..141714e3f 100644 --- a/app/sidecar_query_server.go +++ b/app/sidecar_query_server.go @@ -272,7 +272,7 @@ func NewSideCarQueryServer(appCodec codec.Codec, config domain.Config, logger lo if plugin.IsEnabled() { var currentPlugin domain.EndBlockProcessPlugin - if plugin.GetName() == orderbookplugindomain.OrderBookPluginName { + if plugin.GetName() == orderbookplugindomain.OrderbookFillbotPlugin { // Create keyring keyring, err := keyring.New() if err != nil { @@ -283,7 +283,7 @@ func NewSideCarQueryServer(appCodec codec.Codec, config domain.Config, logger lo currentPlugin = orderbookfillbot.New(poolsUseCase, routerUsecase, tokensUseCase, passthroughGRPCClient, orderBookAPIClient, keyring, defaultQuoteDenom, logger) } - if plugin.GetName() == orderbookplugindomain.OrderBookClaimerPluginName { + if plugin.GetName() == orderbookplugindomain.OrderbookClaimbotPlugin { // Create keyring keyring, err := keyring.New() if err != nil { diff --git a/config.json b/config.json index 6b88f18b4..0489aaa74 100644 --- a/config.json +++ b/config.json @@ -1,15 +1,21 @@ { - "flight-record": { - "enabled": false - }, - "otel": { - "enabled": false, - "environment": "sqs-dev" - }, - "plugins": [ - { - "name": "orderbook", - "enabled": false - } - ] + "flight-record": { + "enabled": false + }, + "otel": { + "enabled": false, + "environment": "sqs-dev" + }, + "grpc-ingester": { + "plugins": [ + { + "name": "orderbook-fillbot-plugin", + "enabled": false + }, + { + "name": "orderbook-claimbot-plugin", + "enabled": false + } + ] + } } diff --git a/domain/config.go b/domain/config.go index d48359f49..ce13a69e9 100644 --- a/domain/config.go +++ b/domain/config.go @@ -156,8 +156,12 @@ var ( ServerConnectionTimeoutSeconds: 10, Plugins: []Plugin{ &OrderBookPluginConfig{ - Enabled: true, - Name: orderbookplugindomain.OrderBookClaimerPluginName, + Enabled: false, + Name: orderbookplugindomain.OrderbookFillbotPlugin, + }, + &OrderBookPluginConfig{ + Enabled: false, + Name: orderbookplugindomain.OrderbookClaimbotPlugin, }, }, }, @@ -377,7 +381,9 @@ func validateDynamicMinLiquidityCapDesc(values []DynamicMinLiquidityCapFilterEnt // PluginFactory creates a Plugin instance based on the provided name. func PluginFactory(name string) Plugin { switch name { - case orderbookplugindomain.OrderBookPluginName: + case orderbookplugindomain.OrderbookFillbotPlugin: + return &OrderBookPluginConfig{} + case orderbookplugindomain.OrderbookClaimbotPlugin: return &OrderBookPluginConfig{} // Add cases for other plugins as needed default: diff --git a/domain/orderbook/plugin/config.go b/domain/orderbook/plugin/config.go index 7075e65bf..4b8004a95 100644 --- a/domain/orderbook/plugin/config.go +++ b/domain/orderbook/plugin/config.go @@ -1,7 +1,7 @@ package orderbookplugindomain +// Orderbook plugin names const ( - // OrderBookPluginName is the name of the orderbook plugin. - OrderBookPluginName = "orderbook" - OrderBookClaimerPluginName = "orderbookclaimer" + OrderbookFillbotPlugin = "orderbook-fillbot-plugin" + OrderbookClaimbotPlugin = "orderbook-claimbot-plugin" ) diff --git a/ingest/usecase/plugins/orderbook/fillbot/README.md b/ingest/usecase/plugins/orderbook/fillbot/README.md index 4bb23a666..8d3873c35 100644 --- a/ingest/usecase/plugins/orderbook/fillbot/README.md +++ b/ingest/usecase/plugins/orderbook/fillbot/README.md @@ -45,7 +45,7 @@ In `config.json`, set the plugin to enabled: ... "plugins": [ { - "name": "orderbook", + "name": "orderbook-fillbot-plugin", "enabled": true } ] From 811bfac73bec6ffffa30e2d24d0f6abd5b3934c2 Mon Sep 17 00:00:00 2001 From: Deividas Petraitis Date: Tue, 22 Oct 2024 10:07:27 +0300 Subject: [PATCH 16/33] BE-586 | OrderBookClient use slices.Split for pagination Cleans up OrderBookClient by reusing slices.Split instead of duplicating splitting slices into chunks logic in some of the methods. --- .../grpcclient/orderbook_grpc_client.go | 27 +++++-------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/domain/orderbook/grpcclient/orderbook_grpc_client.go b/domain/orderbook/grpcclient/orderbook_grpc_client.go index 04e28332c..f30beab14 100644 --- a/domain/orderbook/grpcclient/orderbook_grpc_client.go +++ b/domain/orderbook/grpcclient/orderbook_grpc_client.go @@ -6,6 +6,7 @@ import ( cosmwasmdomain "github.com/osmosis-labs/sqs/domain/cosmwasm" orderbookdomain "github.com/osmosis-labs/sqs/domain/orderbook" + "github.com/osmosis-labs/sqs/domain/slices" wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" ) @@ -88,20 +89,11 @@ func (o *orderbookClientImpl) GetTickUnrealizedCancels(ctx context.Context, cont func (o *orderbookClientImpl) FetchTickUnrealizedCancels(ctx context.Context, chunkSize int, contractAddress string, tickIDs []int64) ([]UnrealizedTickCancels, error) { allUnrealizedCancels := make([]UnrealizedTickCancels, 0, len(tickIDs)) - // TODO: use slices.Split package - for i := 0; i < len(tickIDs); i += chunkSize { - end := i + chunkSize - if end > len(tickIDs) { - end = len(tickIDs) - } - - currentTickIDs := tickIDs[i:end] - - unrealizedCancels, err := o.GetTickUnrealizedCancels(ctx, contractAddress, currentTickIDs) + for _, chunk := range slices.Split(tickIDs, chunkSize) { + unrealizedCancels, err := o.GetTickUnrealizedCancels(ctx, contractAddress, chunk) if err != nil { - return nil, fmt.Errorf("failed to fetch unrealized cancels for ticks %v: %w", currentTickIDs, err) + return nil, fmt.Errorf("failed to fetch unrealized cancels for ticks %v: %w", chunk, err) } - allUnrealizedCancels = append(allUnrealizedCancels, unrealizedCancels...) } @@ -125,15 +117,8 @@ func (o *orderbookClientImpl) QueryTicks(ctx context.Context, contractAddress st func (o *orderbookClientImpl) FetchTicks(ctx context.Context, chunkSize int, contractAddress string, tickIDs []int64) ([]orderbookdomain.Tick, error) { finalTickStates := make([]orderbookdomain.Tick, 0, len(tickIDs)) - for i := 0; i < len(tickIDs); i += chunkSize { - end := i + chunkSize - if end > len(tickIDs) { - end = len(tickIDs) - } - - currentTickIDs := tickIDs[i:end] - - tickStates, err := o.QueryTicks(ctx, contractAddress, currentTickIDs) + for _, chunk := range slices.Split(tickIDs, chunkSize) { + tickStates, err := o.QueryTicks(ctx, contractAddress, chunk) if err != nil { return nil, fmt.Errorf("failed to fetch ticks for pool %s: %w", contractAddress, err) } From f2084e09978aba1d1afb30177c49c87804ef36a9 Mon Sep 17 00:00:00 2001 From: Deividas Petraitis Date: Tue, 22 Oct 2024 10:53:52 +0300 Subject: [PATCH 17/33] BE-586 | Clean up --- ingest/usecase/plugins/orderbook/fillbot/osmosis_swap.go | 1 - 1 file changed, 1 deletion(-) diff --git a/ingest/usecase/plugins/orderbook/fillbot/osmosis_swap.go b/ingest/usecase/plugins/orderbook/fillbot/osmosis_swap.go index 1ea4c7f04..f74770542 100644 --- a/ingest/usecase/plugins/orderbook/fillbot/osmosis_swap.go +++ b/ingest/usecase/plugins/orderbook/fillbot/osmosis_swap.go @@ -312,7 +312,6 @@ func (o *orderbookFillerIngestPlugin) simulateSwapExactAmountIn(ctx blockctx.Blo return msgCtx, nil } -// TODO: func (o *orderbookFillerIngestPlugin) simulateMsgs(ctx context.Context, msgs []sdk.Msg) (*txtypes.SimulateResponse, uint64, error) { accSeq, accNum := getInitialSequence(ctx, o.keyring.GetAddress().String()) From 6bc41390f388410cba01c6db133c8cc9fd202fdc Mon Sep 17 00:00:00 2001 From: Deividas Petraitis Date: Tue, 22 Oct 2024 16:27:47 +0300 Subject: [PATCH 18/33] BE-586 | Fix fillbot docker-compose Fixes errors running fillbot via docker-compose --- .gitignore | 2 +- Makefile | 9 ++++++--- ingest/usecase/plugins/orderbook/fillbot/README.md | 4 ++-- .../usecase/plugins/orderbook/fillbot/docker-compose.yml | 4 ++-- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 96a540818..e062c5e23 100644 --- a/.gitignore +++ b/.gitignore @@ -31,7 +31,7 @@ __debug_bin* debug.test* # Local env for the order book filler -ingest/usecase/plugins/orderbookfiller/.env +ingest/usecase/plugins/orderbook/fillbot/.env config_orderbook_fill_plugin.json .envrc diff --git a/Makefile b/Makefile index acf39ce2c..4c9c0494c 100644 --- a/Makefile +++ b/Makefile @@ -228,7 +228,10 @@ datadog-agent-start: -e DD_OTLP_CONFIG_RECEIVER_PROTOCOLS_GRPC_ENDPOINT=0.0.0.0:4317 \ gcr.io/datadoghq/agent:latest -#### Order Book Fill Bot Plugin + +#### Orderbook plugins #### +#### +#### Orderbook Fill Bot Plugin # Starts the full-scale order fill bot. # - Creates a copy of the config.json file with the updated @@ -236,7 +239,7 @@ datadog-agent-start: # Starts node and SQS in the background. # - Starts DataDog service # Use ./ingest/usecase/plugins/orderbook/fillbot/.env to configure the keyring. -orderbook-filler-start: +orderbook-fillbot-start: ./ingest/usecase/plugins/orderbook/fillbot/create_copy_config.sh cd ./ingest/usecase/plugins/orderbook/fillbot && docker compose up -d cd ../../../../ @@ -244,7 +247,7 @@ orderbook-filler-start: sleep 10 && osmosisd status sleep 10 && docker logs -f osmosis-sqs -orderbook-filler-stop: +orderbook-fillbot-stop: cd ./ingest/usecase/plugins/orderbook/fillbot && docker compose down cd ../../../../ echo "Order Book Filler Bot Stopped" diff --git a/ingest/usecase/plugins/orderbook/fillbot/README.md b/ingest/usecase/plugins/orderbook/fillbot/README.md index 8d3873c35..fd3973f0f 100644 --- a/ingest/usecase/plugins/orderbook/fillbot/README.md +++ b/ingest/usecase/plugins/orderbook/fillbot/README.md @@ -76,8 +76,8 @@ of POC implementation. In the future, this can be improved to support multiple b ## Starting (via docker compose) 1. Ensure that the "Configuration" section is complete. -2. From project root, `cd` into `ingest/usecase/plugins/orderbookfiller` +2. From project root, `cd` into `ingest/usecase/plugins/orderbook/fillbot` 3. Update `.env` with your environment variables. -4. Run `make orderbook-filler-start` +4. Run `make orderbook-fillbot-start` 5. Run `osmosisd status` to check that the node is running and caught up to tip. 6. Curl `/healthcheck` to check that SQS is running `curl http://localhost:9092/healthcheck` diff --git a/ingest/usecase/plugins/orderbook/fillbot/docker-compose.yml b/ingest/usecase/plugins/orderbook/fillbot/docker-compose.yml index e84922af3..3938d2ff8 100644 --- a/ingest/usecase/plugins/orderbook/fillbot/docker-compose.yml +++ b/ingest/usecase/plugins/orderbook/fillbot/docker-compose.yml @@ -61,7 +61,7 @@ services: - --host - sqs-fill-bot build: - context: ../../../../ + context: ../../../../../ dockerfile: Dockerfile depends_on: - osmosis @@ -144,4 +144,4 @@ services: logging: driver: "json-file" options: - max-size: "512m" \ No newline at end of file + max-size: "512m" From 89df2a6f3bb5445eefa7681058c1167d11c4436c Mon Sep 17 00:00:00 2001 From: Deividas Petraitis Date: Thu, 24 Oct 2024 18:35:03 +0300 Subject: [PATCH 19/33] BE-586 | Docs, docker compose fixes --- .../plugins/orderbook/claimbot/README.md | 70 ++++++++++++++++++- .../orderbook/claimbot/docker-compose.yml | 6 +- 2 files changed, 72 insertions(+), 4 deletions(-) diff --git a/ingest/usecase/plugins/orderbook/claimbot/README.md b/ingest/usecase/plugins/orderbook/claimbot/README.md index ec982141f..240828765 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/README.md +++ b/ingest/usecase/plugins/orderbook/claimbot/README.md @@ -1 +1,69 @@ -# Order Book Claimer Plugin +# Orderbook Claimbot Plugin + +The Orderbook Claimbot plugin is a plugin that claims filled order book orders. + +It scans all active orders for each order book determining which orders have been filled and need to be claimed. At the moment order is said to be claimable if it is filled 98 percent or more. In order for an order book to be processed to claim its active orders it must be canonical as per SQS definition. + + +Such order book scanning and claiming is achieved by listening for new blocks and core logic is triggered at the end of each new block by calling Claimbot `ProcessEndBlock` method. + +## Configuration + +### Node + +1. Initialize a fresh node with the `osmosisd` binary. +```bash +osmosisd init claim-bot --chain-id osmosis-1 +``` + +2. Get latest snapshot from [here](https://snapshots.osmosis.zone/index.html) + +3. Go to `$HOME/.osmosisd/config/app.toml` and set `osmosis-sqs.is-enabled` to true + +4. Optionally, turn off any services from `app.toml` and `config.toml` that you don't need + +### SQS + +In `config.json`, set the plugin to enabled: + +```json +"grpc-ingester":{ + ... + "plugins": [ + { + "name": "orderbook-claimbot-plugin", + "enabled": true + } + ] +}, +``` + +Configure the key on a test keyring, and set the following environment variables: +```bash +OSMOSIS_KEYRING_PATH=/root/.osmosisd/keyring-test +OSMOSIS_KEYRING_PASSWORD=test +OSMOSIS_KEYRING_KEY_NAME=local.info +``` +- Here, the key is named `local` and the keyring path is in the default `osmosisd` home directory. + +To create your key: +```bash +osmosisd keys add local --keyring-backend test --recover + +# Enter your mnemonic + +# Confirm the key is created +osmosisd keys list --keyring-backend test +``` + +Note that the test keyring is not a secure approach but we opted-in for simplicity and speed +of PoC implementation. In the future, this can be improved to support multiple backends. + +## Starting (via docker compose) + +1. Ensure that the "Configuration" section is complete. +2. From project root, `cd` into `ingest/usecase/plugins/orderbook/claimbot` +3. Update `.env` with your environment variables. +4. Run `make orderbook-claimbot-start` +5. Run `osmosisd status` to check that the node is running and caught up to tip. +6. Curl `/healthcheck` to check that SQS is running `curl http://localhost:9092/healthcheck` diff --git a/ingest/usecase/plugins/orderbook/claimbot/docker-compose.yml b/ingest/usecase/plugins/orderbook/claimbot/docker-compose.yml index e84922af3..3a657bf14 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/docker-compose.yml +++ b/ingest/usecase/plugins/orderbook/claimbot/docker-compose.yml @@ -55,11 +55,11 @@ services: - OSMOSIS_LCD_ENDPOINT=http://osmosis:1317 - SQS_GRPC_TENDERMINT_RPC_ENDPOINT=http://osmosis:26657 - SQS_GRPC_GATEWAY_ENDPOINT=osmosis:9090 - - SQS_OTEL_ENVIRONMENT=sqs-fill-bot + - SQS_OTEL_ENVIRONMENT=sqs-claim-bot - SQS_GRPC_INGESTER_PLUGINS_ORDERBOOK_ENABLED=true command: - --host - - sqs-fill-bot + - sqs-claim-bot build: context: ../../../../ dockerfile: Dockerfile @@ -144,4 +144,4 @@ services: logging: driver: "json-file" options: - max-size: "512m" \ No newline at end of file + max-size: "512m" From 3c90eb8db25044366c313f8f2704bff9ab6dbc4f Mon Sep 17 00:00:00 2001 From: Deividas Petraitis Date: Fri, 25 Oct 2024 13:53:58 +0300 Subject: [PATCH 20/33] BE-586 | Run fillbot via docker-compose --- Makefile | 18 ++++++++++++++++-- .../orderbook/claimbot/docker-compose.yml | 4 ++-- .../orderbook/fillbot/create_copy_config.sh | 7 ++++++- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 4c9c0494c..5e9bf8196 100644 --- a/Makefile +++ b/Makefile @@ -243,11 +243,25 @@ orderbook-fillbot-start: ./ingest/usecase/plugins/orderbook/fillbot/create_copy_config.sh cd ./ingest/usecase/plugins/orderbook/fillbot && docker compose up -d cd ../../../../ - echo "Order Book Filler Bot Started" + echo "Orderbook Fill Bot Started" sleep 10 && osmosisd status sleep 10 && docker logs -f osmosis-sqs orderbook-fillbot-stop: cd ./ingest/usecase/plugins/orderbook/fillbot && docker compose down cd ../../../../ - echo "Order Book Filler Bot Stopped" + echo "Orderbook Fill Bot Stopped" + + +orderbook-claimbot-start: + ./ingest/usecase/plugins/orderbook/fillbot/create_copy_config.sh + cd ./ingest/usecase/plugins/orderbook/claimbot && docker compose up -d + cd ../../../../ + echo "Orderbook Claim Bot Started" + sleep 10 && osmosisd status + sleep 10 && docker logs -f osmosis-sqs + +orderbook-claimbot-stop: + cd ./ingest/usecase/plugins/orderbook/claimbot && docker compose down + cd ../../../../ + echo "Orderbook Claim Bot Stopped" diff --git a/ingest/usecase/plugins/orderbook/claimbot/docker-compose.yml b/ingest/usecase/plugins/orderbook/claimbot/docker-compose.yml index 3a657bf14..20355b824 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/docker-compose.yml +++ b/ingest/usecase/plugins/orderbook/claimbot/docker-compose.yml @@ -22,7 +22,7 @@ services: command: - start - --home=/osmosis/.osmosisd - image: osmolabs/osmosis-dev:v25.x-c775cee7-1722825184 + image: osmolabs/osmosis:26.0.2 container_name: osmosis restart: always ports: @@ -61,7 +61,7 @@ services: - --host - sqs-claim-bot build: - context: ../../../../ + context: ../../../../../ dockerfile: Dockerfile depends_on: - osmosis diff --git a/ingest/usecase/plugins/orderbook/fillbot/create_copy_config.sh b/ingest/usecase/plugins/orderbook/fillbot/create_copy_config.sh index 01d6ae4a9..c219306dc 100755 --- a/ingest/usecase/plugins/orderbook/fillbot/create_copy_config.sh +++ b/ingest/usecase/plugins/orderbook/fillbot/create_copy_config.sh @@ -1,9 +1,14 @@ -#!/bin/bash +#!/usr/bin/env bash # Define the input and output file paths ORIGINAL_APP_TOML_NAME="$HOME/.osmosisd/config/app.toml" # Replace with the actual file path BACKUP_APP_TOML_NAME="$HOME/.osmosisd/config/app-backup.toml" +if [ -f $BACKUP_APP_TOML_NAME ]; then + echo "Backup file $BACKUP_APP_TOML_NAME already exist, no modifications will be made." + exit 0 +fi + mv $ORIGINAL_APP_TOML_NAME $BACKUP_APP_TOML_NAME # Use sed to modify the TOML and create a new file From 3e2d513b9cd24c6a1a2e54aa41113f36f2a05495 Mon Sep 17 00:00:00 2001 From: Deividas Petraitis Date: Fri, 25 Oct 2024 15:24:36 +0300 Subject: [PATCH 21/33] BE-586 | Run claimbot via docker-compose, clean up --- app/sidecar_query_server.go | 2 ++ ingest/usecase/plugins/orderbook/claimbot/config.go | 6 +++++- .../plugins/orderbook/claimbot/docker-compose.yml | 6 +++--- .../plugins/orderbook/claimbot/export_test.go | 3 ++- ingest/usecase/plugins/orderbook/claimbot/plugin.go | 5 ++++- ingest/usecase/plugins/orderbook/claimbot/tx.go | 13 +------------ .../usecase/plugins/orderbook/claimbot/tx_test.go | 4 +++- 7 files changed, 20 insertions(+), 19 deletions(-) diff --git a/app/sidecar_query_server.go b/app/sidecar_query_server.go index 141714e3f..e0aa90233 100644 --- a/app/sidecar_query_server.go +++ b/app/sidecar_query_server.go @@ -298,6 +298,8 @@ func NewSideCarQueryServer(appCodec codec.Codec, config domain.Config, logger lo orderBookRepository, orderBookAPIClient, logger, + config.ChainGRPCGatewayEndpoint, + config.ChainID, ) if err != nil { return nil, err diff --git a/ingest/usecase/plugins/orderbook/claimbot/config.go b/ingest/usecase/plugins/orderbook/claimbot/config.go index b634250fd..651d05898 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/config.go +++ b/ingest/usecase/plugins/orderbook/claimbot/config.go @@ -26,6 +26,7 @@ type Config struct { TxfeesClient txfeestypes.QueryClient GasCalculator sqstx.GasCalculator TxServiceClient txtypes.ServiceClient + ChainID string Logger log.Logger } @@ -37,8 +38,10 @@ func NewConfig( orderbookRepository orderbookdomain.OrderBookRepository, orderBookClient orderbookgrpcclientdomain.OrderBookClient, logger log.Logger, + chainGRPCGatewayEndpoint string, + chainID string, ) (*Config, error) { - grpcClient, err := grpc.NewClient(RPC) + grpcClient, err := grpc.NewClient(chainGRPCGatewayEndpoint) if err != nil { return nil, err } @@ -54,5 +57,6 @@ func NewConfig( GasCalculator: sqstx.NewGasCalculator(grpcClient), TxServiceClient: txtypes.NewServiceClient(grpcClient), Logger: logger, + ChainID: chainID, }, nil } diff --git a/ingest/usecase/plugins/orderbook/claimbot/docker-compose.yml b/ingest/usecase/plugins/orderbook/claimbot/docker-compose.yml index 20355b824..2bea623d0 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/docker-compose.yml +++ b/ingest/usecase/plugins/orderbook/claimbot/docker-compose.yml @@ -51,15 +51,14 @@ services: - OSMOSIS_KEYRING_PATH=${OSMOSIS_KEYRING_PATH} - OSMOSIS_KEYRING_PASSWORD=${OSMOSIS_KEYRING_PASSWORD} - OSMOSIS_KEYRING_KEY_NAME=${OSMOSIS_KEYRING_KEY_NAME} - - OSMOSIS_RPC_ENDPOINT=http://osmosis:26657 - - OSMOSIS_LCD_ENDPOINT=http://osmosis:1317 - SQS_GRPC_TENDERMINT_RPC_ENDPOINT=http://osmosis:26657 - SQS_GRPC_GATEWAY_ENDPOINT=osmosis:9090 - SQS_OTEL_ENVIRONMENT=sqs-claim-bot - - SQS_GRPC_INGESTER_PLUGINS_ORDERBOOK_ENABLED=true command: - --host - sqs-claim-bot + - --config + - /etc/config.json build: context: ../../../../../ dockerfile: Dockerfile @@ -71,6 +70,7 @@ services: - 9092:9092 volumes: - ${OSMOSIS_KEYRING_PATH}:${OSMOSIS_KEYRING_PATH} + - ../../../../../config.json:/etc/config.json logging: driver: "json-file" options: diff --git a/ingest/usecase/plugins/orderbook/claimbot/export_test.go b/ingest/usecase/plugins/orderbook/claimbot/export_test.go index a013e8490..303178a13 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/export_test.go +++ b/ingest/usecase/plugins/orderbook/claimbot/export_test.go @@ -46,10 +46,11 @@ func SendBatchClaimTx( txfeesClient txfeestypes.QueryClient, gasCalculator sqstx.GasCalculator, txServiceClient txtypes.ServiceClient, + chainID string, contractAddress string, claims orderbookdomain.Orders, ) (*sdk.TxResponse, error) { - return sendBatchClaimTx(ctx, keyring, accountQueryClient, txfeesClient, gasCalculator, txServiceClient, contractAddress, claims) + return sendBatchClaimTx(ctx, keyring, accountQueryClient, txfeesClient, gasCalculator, txServiceClient, chainID, contractAddress, claims) } // PrepareBatchClaimMsg is a test wrapper for prepareBatchClaimMsg. diff --git a/ingest/usecase/plugins/orderbook/claimbot/plugin.go b/ingest/usecase/plugins/orderbook/claimbot/plugin.go index cbecd82c3..a722b88d4 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/plugin.go +++ b/ingest/usecase/plugins/orderbook/claimbot/plugin.go @@ -50,8 +50,10 @@ func New( orderbookRepository orderbookdomain.OrderBookRepository, orderBookClient orderbookgrpcclientdomain.OrderBookClient, logger log.Logger, + chainGRPCGatewayEndpoint string, + chainID string, ) (*claimbot, error) { - config, err := NewConfig(keyring, orderbookusecase, poolsUsecase, orderbookRepository, orderBookClient, logger) + config, err := NewConfig(keyring, orderbookusecase, poolsUsecase, orderbookRepository, orderBookClient, logger, chainGRPCGatewayEndpoint, chainID) if err != nil { return nil, fmt.Errorf("failed to create claimbot config: %w", err) } @@ -127,6 +129,7 @@ func (o *claimbot) processOrderbookOrders(ctx context.Context, orderbook domain. o.config.TxfeesClient, o.config.GasCalculator, o.config.TxServiceClient, + o.config.ChainID, orderbook.ContractAddress, chunk, ) diff --git a/ingest/usecase/plugins/orderbook/claimbot/tx.go b/ingest/usecase/plugins/orderbook/claimbot/tx.go index c8be6bb44..239357db5 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/tx.go +++ b/ingest/usecase/plugins/orderbook/claimbot/tx.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "fmt" - "os" authtypes "github.com/osmosis-labs/sqs/domain/cosmos/auth/types" sqstx "github.com/osmosis-labs/sqs/domain/cosmos/tx" @@ -20,20 +19,9 @@ import ( ) var ( - chainID = "osmosis-1" - - RPC = "localhost:9090" - encodingConfig = app.MakeEncodingConfig() ) -// init overrides LCD and RPC endpoints from environment variables if those are set. -func init() { - if rpc := os.Getenv("OSMOSIS_RPC_ENDPOINT"); len(rpc) > 0 { - RPC = rpc - } -} - // sendBatchClaimTx prepares and sends a batch claim transaction to the blockchain. // It builds the transaction, signs it, and broadcasts it to the network. func sendBatchClaimTx( @@ -43,6 +31,7 @@ func sendBatchClaimTx( txfeesClient txfeestypes.QueryClient, gasCalculator sqstx.GasCalculator, txServiceClient txtypes.ServiceClient, + chainID string, contractAddress string, claims orderbookdomain.Orders, ) (*sdk.TxResponse, error) { diff --git a/ingest/usecase/plugins/orderbook/claimbot/tx_test.go b/ingest/usecase/plugins/orderbook/claimbot/tx_test.go index 749e04da6..9f046e0ae 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/tx_test.go +++ b/ingest/usecase/plugins/orderbook/claimbot/tx_test.go @@ -19,6 +19,7 @@ import ( func TestSendBatchClaimTx(t *testing.T) { tests := []struct { name string + chainID string contractAddress string claims orderbookdomain.Orders setupMocks func(*mocks.Keyring, *mocks.AuthQueryClientMock, *mocks.TxFeesQueryClient, *mocks.GasCalculator, *mocks.TxServiceClient) @@ -81,6 +82,7 @@ func TestSendBatchClaimTx(t *testing.T) { }, { name: "Successful transaction", + chainID: "osmosis-1", contractAddress: "osmo1contractaddress", claims: orderbookdomain.Orders{ {TickId: 1, OrderId: 100}, @@ -123,7 +125,7 @@ func TestSendBatchClaimTx(t *testing.T) { tt.setupMocks(&keyring, &authQueryClient, &txFeesClient, &gasCalculator, &txServiceClient) - response, err := claimbot.SendBatchClaimTx(ctx, &keyring, &authQueryClient, &txFeesClient, &gasCalculator, &txServiceClient, tt.contractAddress, tt.claims) + response, err := claimbot.SendBatchClaimTx(ctx, &keyring, &authQueryClient, &txFeesClient, &gasCalculator, &txServiceClient, tt.chainID, tt.contractAddress, tt.claims) if tt.expectedError { assert.Error(t, err) } else { From 0ff913b692d8b2bb0048e5f8d5dc73ba477fecb0 Mon Sep 17 00:00:00 2001 From: Deividas Petraitis Date: Fri, 25 Oct 2024 17:14:53 +0300 Subject: [PATCH 22/33] BE-586 | Cleanup --- .../plugins/orderbook/claimbot/plugin.go | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/ingest/usecase/plugins/orderbook/claimbot/plugin.go b/ingest/usecase/plugins/orderbook/claimbot/plugin.go index a722b88d4..89461a8ef 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/plugin.go +++ b/ingest/usecase/plugins/orderbook/claimbot/plugin.go @@ -97,12 +97,16 @@ func (o *claimbot) ProcessEndBlock(ctx context.Context, blockHeight uint64, meta for _, orderbook := range orders { if orderbook.Err != nil { - fmt.Println("step1 error", orderbook.Err) + o.config.Logger.Warn( + "failed to retrieve claimable orders", + zap.String("contract_address", orderbook.Orderbook.ContractAddress), + zap.Error(err), + ) continue } if err := o.processOrderbookOrders(ctx, orderbook.Orderbook, orderbook.Orders); err != nil { - o.config.Logger.Info( + o.config.Logger.Warn( "failed to process orderbook orders", zap.String("contract_address", orderbook.Orderbook.ContractAddress), zap.Error(err), @@ -117,6 +121,10 @@ func (o *claimbot) ProcessEndBlock(ctx context.Context, blockHeight uint64, meta // processOrderbookOrders processes a batch of claimable orders. func (o *claimbot) processOrderbookOrders(ctx context.Context, orderbook domain.CanonicalOrderBooksResult, orders orderbookdomain.Orders) error { + if len(orders) == 0 { + return fmt.Errorf("no claimable orders found for orderbook %s, nothing to process", orderbook.ContractAddress) + } + for _, chunk := range slices.Split(orders, maxBatchOfClaimableOrders) { if len(chunk) == 0 { continue @@ -134,16 +142,12 @@ func (o *claimbot) processOrderbookOrders(ctx context.Context, orderbook domain. chunk, ) - if err != nil { - o.config.Logger.Info( - "failed to sent batch claim tx", - zap.String("contract_address", orderbook.ContractAddress), - zap.Any("tx_result", txres), - zap.Error(err), - ) - } - - fmt.Println("claims", orderbook.ContractAddress, txres, chunk, err) + o.config.Logger.Info("claimed orders", + zap.String("orderbook contract address", orderbook.ContractAddress), + zap.Any("orders", chunk), + zap.Any("tx result", txres), + zap.Error(err), + ) // Wait for block inclusion with buffer to avoid sequence mismatch time.Sleep(5 * time.Second) From fc39dd76e9b4c02e8570390fee9e0faf53286760 Mon Sep 17 00:00:00 2001 From: Deividas Petraitis Date: Mon, 28 Oct 2024 16:51:24 +0200 Subject: [PATCH 23/33] BE-586 | Named logger --- .../usecase/plugins/orderbook/claimbot/config.go | 2 +- .../usecase/plugins/orderbook/claimbot/plugin.go | 8 ++++---- log/logger.go | 16 +++++++++++++++- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/ingest/usecase/plugins/orderbook/claimbot/config.go b/ingest/usecase/plugins/orderbook/claimbot/config.go index 651d05898..e8dc6d981 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/config.go +++ b/ingest/usecase/plugins/orderbook/claimbot/config.go @@ -56,7 +56,7 @@ func NewConfig( TxfeesClient: txfeestypes.NewQueryClient(grpcClient), GasCalculator: sqstx.NewGasCalculator(grpcClient), TxServiceClient: txtypes.NewServiceClient(grpcClient), - Logger: logger, + Logger: logger.Named("claimbot"), ChainID: chainID, }, nil } diff --git a/ingest/usecase/plugins/orderbook/claimbot/plugin.go b/ingest/usecase/plugins/orderbook/claimbot/plugin.go index 89461a8ef..46e828e33 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/plugin.go +++ b/ingest/usecase/plugins/orderbook/claimbot/plugin.go @@ -30,7 +30,7 @@ type claimbot struct { var _ domain.EndBlockProcessPlugin = &claimbot{} const ( - tracerName = "sqs-orderbook-claimer" + tracerName = "sqs-orderbook-claimbot" ) var ( @@ -55,7 +55,7 @@ func New( ) (*claimbot, error) { config, err := NewConfig(keyring, orderbookusecase, poolsUsecase, orderbookRepository, orderBookClient, logger, chainGRPCGatewayEndpoint, chainID) if err != nil { - return nil, fmt.Errorf("failed to create claimbot config: %w", err) + return nil, fmt.Errorf("failed to create config: %w", err) } return &claimbot{ @@ -74,7 +74,7 @@ func (o *claimbot) ProcessEndBlock(ctx context.Context, blockHeight uint64, meta // For simplicity, we allow only one block to be processed at a time. // This may be relaxed in the future. if !o.atomicBool.CompareAndSwap(false, true) { - o.config.Logger.Info("orderbook claimer is already in progress", zap.Uint64("block_height", blockHeight)) + o.config.Logger.Info("already in progress", zap.Uint64("block_height", blockHeight)) return nil } defer o.atomicBool.Store(false) @@ -114,7 +114,7 @@ func (o *claimbot) ProcessEndBlock(ctx context.Context, blockHeight uint64, meta } } - o.config.Logger.Info("processed end block in orderbook claimer ingest plugin", zap.Uint64("block_height", blockHeight)) + o.config.Logger.Info("processed end block", zap.Uint64("block_height", blockHeight)) return nil } diff --git a/log/logger.go b/log/logger.go index 087458635..9a8332ab3 100644 --- a/log/logger.go +++ b/log/logger.go @@ -8,6 +8,7 @@ import ( ) type Logger interface { + Named(s string) Logger Info(msg string, fields ...zap.Field) Warn(msg string, fields ...zap.Field) Error(msg string, fields ...zap.Field) @@ -36,6 +37,11 @@ func (*NoOpLogger) Warn(msg string, fields ...zapcore.Field) { // no-op } +// Warn implements Logger. +func (l *NoOpLogger) Named(s string) Logger { + return l +} + var _ Logger = (*NoOpLogger)(nil) type loggerImpl struct { @@ -64,6 +70,12 @@ func (l *loggerImpl) Warn(msg string, fields ...zapcore.Field) { l.zapLogger.Warn(msg, fields...) } +func (l *loggerImpl) Named(s string) Logger { + return &loggerImpl{ + zapLogger: *l.zapLogger.Named(s), + } +} + // NewLogger creates a new logger. // If fileName is non-empty, it pipes logs to file and stdout. // if filename is empty, it pipes logs only to stdout. @@ -113,5 +125,7 @@ func NewLogger(isProduction bool, fileName string, logLevelStr string) (Logger, logger.Info("log level", zap.Bool("is_debug", isDebugLevel), zap.String("log_level", loggerConfig.Level.String())) - return logger, nil + return &loggerImpl{ + zapLogger: *logger, + }, nil } From 3216bb16d67ef62e440cd9496d43f597368d142c Mon Sep 17 00:00:00 2001 From: Deividas Petraitis Date: Mon, 28 Oct 2024 16:54:32 +0200 Subject: [PATCH 24/33] BE-586 | Requested changes --- ingest/usecase/plugins/orderbook/claimbot/plugin.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ingest/usecase/plugins/orderbook/claimbot/plugin.go b/ingest/usecase/plugins/orderbook/claimbot/plugin.go index 46e828e33..ed4146909 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/plugin.go +++ b/ingest/usecase/plugins/orderbook/claimbot/plugin.go @@ -34,8 +34,9 @@ const ( ) var ( - tracer = otel.Tracer(tracerName) - fillThreshold = osmomath.MustNewDecFromStr("0.98") + tracer = otel.Tracer(tracerName) + fillThreshold = osmomath.MustNewDecFromStr("0.98") + blockInclusionWaitTime = 5 * time.Second ) // maxBatchOfClaimableOrders is the maximum number of claimable orders @@ -150,7 +151,7 @@ func (o *claimbot) processOrderbookOrders(ctx context.Context, orderbook domain. ) // Wait for block inclusion with buffer to avoid sequence mismatch - time.Sleep(5 * time.Second) + time.Sleep(blockInclusionWaitTime) } return nil From b1ecbf46d4f99971935d0d3322e30bcf70bf6e83 Mon Sep 17 00:00:00 2001 From: Deividas Petraitis Date: Mon, 28 Oct 2024 17:21:50 +0200 Subject: [PATCH 25/33] BE-586 | Logging failing tx --- .../usecase/plugins/orderbook/claimbot/plugin.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/ingest/usecase/plugins/orderbook/claimbot/plugin.go b/ingest/usecase/plugins/orderbook/claimbot/plugin.go index ed4146909..0539a8772 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/plugin.go +++ b/ingest/usecase/plugins/orderbook/claimbot/plugin.go @@ -143,12 +143,14 @@ func (o *claimbot) processOrderbookOrders(ctx context.Context, orderbook domain. chunk, ) - o.config.Logger.Info("claimed orders", - zap.String("orderbook contract address", orderbook.ContractAddress), - zap.Any("orders", chunk), - zap.Any("tx result", txres), - zap.Error(err), - ) + if err != nil || (txres != nil && txres.Code != 0) { + o.config.Logger.Info("failed sending tx", + zap.String("orderbook contract address", orderbook.ContractAddress), + zap.Any("orders", chunk), + zap.Any("tx result", txres), + zap.Error(err), + ) + } // Wait for block inclusion with buffer to avoid sequence mismatch time.Sleep(blockInclusionWaitTime) From 8a8ef15903920a9e79b66167d1ce06fa24db2328 Mon Sep 17 00:00:00 2001 From: Deividas Petraitis Date: Mon, 28 Oct 2024 17:28:43 +0200 Subject: [PATCH 26/33] BE-586 | Increase gas adjustment --- domain/cosmos/tx/tx.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain/cosmos/tx/tx.go b/domain/cosmos/tx/tx.go index 2c6673523..5c7353e0e 100644 --- a/domain/cosmos/tx/tx.go +++ b/domain/cosmos/tx/tx.go @@ -119,7 +119,7 @@ func SimulateMsgs( txFactory = txFactory.WithAccountNumber(account.AccountNumber) txFactory = txFactory.WithSequence(account.Sequence) txFactory = txFactory.WithChainID(chainID) - txFactory = txFactory.WithGasAdjustment(1.05) + txFactory = txFactory.WithGasAdjustment(1.15) // Estimate transaction gasResult, adjustedGasUsed, err := gasCalculator.CalculateGas( From adb286c6ad5a8d413b91c7937fe396ed55d902a8 Mon Sep 17 00:00:00 2001 From: Deividas Petraitis Date: Mon, 28 Oct 2024 17:37:54 +0200 Subject: [PATCH 27/33] BE-586 | Error logging fix --- ingest/usecase/plugins/orderbook/claimbot/plugin.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ingest/usecase/plugins/orderbook/claimbot/plugin.go b/ingest/usecase/plugins/orderbook/claimbot/plugin.go index 0539a8772..749107db3 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/plugin.go +++ b/ingest/usecase/plugins/orderbook/claimbot/plugin.go @@ -101,7 +101,7 @@ func (o *claimbot) ProcessEndBlock(ctx context.Context, blockHeight uint64, meta o.config.Logger.Warn( "failed to retrieve claimable orders", zap.String("contract_address", orderbook.Orderbook.ContractAddress), - zap.Error(err), + zap.Error(orderbook.Err), ) continue } From d298179a9014e35c8b2ba744bcd2a8e6dce776b7 Mon Sep 17 00:00:00 2001 From: Deividas Petraitis Date: Tue, 29 Oct 2024 12:14:05 +0200 Subject: [PATCH 28/33] BE-586 | Trace name update --- ingest/usecase/plugins/orderbook/claimbot/plugin.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ingest/usecase/plugins/orderbook/claimbot/plugin.go b/ingest/usecase/plugins/orderbook/claimbot/plugin.go index 749107db3..0fa828afc 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/plugin.go +++ b/ingest/usecase/plugins/orderbook/claimbot/plugin.go @@ -69,7 +69,7 @@ func New( // This method is called at the end of each block to process and claim eligible orderbook orders. // ProcessEndBlock implements domain.EndBlockProcessPlugin. func (o *claimbot) ProcessEndBlock(ctx context.Context, blockHeight uint64, metadata domain.BlockPoolMetadata) error { - ctx, span := tracer.Start(ctx, "orderbooktFillerIngestPlugin.ProcessEndBlock") + ctx, span := tracer.Start(ctx, "orderbookClaimbotIngestPlugin.ProcessEndBlock") defer span.End() // For simplicity, we allow only one block to be processed at a time. From 131ba9e8fbed993a4105e4b0a9e56e14cd8a4bb8 Mon Sep 17 00:00:00 2001 From: Deividas Petraitis Date: Wed, 30 Oct 2024 16:25:09 +0200 Subject: [PATCH 29/33] BE-586 | Requested changes #1 --- .../plugins/orderbook/claimbot/export_test.go | 10 +-- .../plugins/orderbook/claimbot/order.go | 61 +++++++++++++------ .../plugins/orderbook/claimbot/order_test.go | 17 ++++-- .../plugins/orderbook/claimbot/orderbook.go | 6 +- .../orderbook/claimbot/orderbook_test.go | 24 +++----- .../plugins/orderbook/claimbot/plugin.go | 22 +++++-- .../usecase/plugins/orderbook/claimbot/tx.go | 21 ++++--- .../orderbook/fillbot/create_copy_config.sh | 5 ++ 8 files changed, 107 insertions(+), 59 deletions(-) diff --git a/ingest/usecase/plugins/orderbook/claimbot/export_test.go b/ingest/usecase/plugins/orderbook/claimbot/export_test.go index 303178a13..9118f06f9 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/export_test.go +++ b/ingest/usecase/plugins/orderbook/claimbot/export_test.go @@ -20,8 +20,8 @@ import ( txtypes "github.com/cosmos/cosmos-sdk/types/tx" ) -// Order is order alias data structure for testing purposes. -type Order = order +// ProcessedOrderbook is order alias data structure for testing purposes. +type ProcessedOrderbook = processedOrderbook // ProcessOrderbooksAndGetClaimableOrders is test wrapper for processOrderbooksAndGetClaimableOrders. // This function is exported for testing purposes. @@ -33,7 +33,7 @@ func ProcessOrderbooksAndGetClaimableOrders( orderBookClient orderbookgrpcclientdomain.OrderBookClient, orderbookusecase mvc.OrderBookUsecase, logger log.Logger, -) []Order { +) ([]ProcessedOrderbook, error) { return processOrderbooksAndGetClaimableOrders(ctx, fillThreshold, orderbooks, orderbookRepository, orderBookClient, orderbookusecase, logger) } @@ -61,6 +61,6 @@ func PrepareBatchClaimMsg(claims orderbookdomain.Orders) ([]byte, error) { // GetOrderbooks is a test wrapper for getOrderbooks. // This function is exported for testing purposes. -func GetOrderbooks(poolsUsecase mvc.PoolsUsecase, blockHeight uint64, metadata domain.BlockPoolMetadata) ([]domain.CanonicalOrderBooksResult, error) { - return getOrderbooks(poolsUsecase, blockHeight, metadata) +func GetOrderbooks(poolsUsecase mvc.PoolsUsecase, metadata domain.BlockPoolMetadata) ([]domain.CanonicalOrderBooksResult, error) { + return getOrderbooks(poolsUsecase, metadata) } diff --git a/ingest/usecase/plugins/orderbook/claimbot/order.go b/ingest/usecase/plugins/orderbook/claimbot/order.go index 2a75696bc..9b22e0384 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/order.go +++ b/ingest/usecase/plugins/orderbook/claimbot/order.go @@ -14,13 +14,14 @@ import ( "go.uber.org/zap" ) -type order struct { +type processedOrderbook struct { Orderbook domain.CanonicalOrderBooksResult Orders orderbookdomain.Orders Err error } // processOrderbooksAndGetClaimableOrders processes a list of orderbooks and returns claimable orders for each. +// Under the hood processing of each orderbook in done concurrently to speed up the process. func processOrderbooksAndGetClaimableOrders( ctx context.Context, fillThreshold osmomath.Dec, @@ -29,13 +30,27 @@ func processOrderbooksAndGetClaimableOrders( orderBookClient orderbookgrpcclientdomain.OrderBookClient, orderbookusecase mvc.OrderBookUsecase, logger log.Logger, -) []order { - var result []order +) ([]processedOrderbook, error) { + ch := make(chan processedOrderbook, len(orderbooks)) + for _, orderbook := range orderbooks { - processedOrder := processOrderbook(ctx, fillThreshold, orderbook, orderbookRepository, orderBookClient, orderbookusecase, logger) - result = append(result, processedOrder) + go func(orderbook domain.CanonicalOrderBooksResult) { + o := processOrderbook(ctx, fillThreshold, orderbook, orderbookRepository, orderBookClient, orderbookusecase, logger) + ch <- o + }(orderbook) } - return result + + var results []processedOrderbook + for i := 0; i < len(orderbooks); i++ { + select { + case result := <-ch: + results = append(results, result) + case <-ctx.Done(): + return nil, ctx.Err() + } + } + + return results, nil } // processOrderbook processes a single orderbook and returns an order struct containing the processed orderbook and its claimable orders. @@ -47,15 +62,15 @@ func processOrderbook( orderBookClient orderbookgrpcclientdomain.OrderBookClient, orderbookusecase mvc.OrderBookUsecase, logger log.Logger, -) order { +) processedOrderbook { claimable, err := getClaimableOrdersForOrderbook(ctx, fillThreshold, orderbook, orderbookRepository, orderBookClient, orderbookusecase, logger) if err != nil { - return order{ + return processedOrderbook{ Orderbook: orderbook, Err: err, } } - return order{ + return processedOrderbook{ Orderbook: orderbook, Orders: claimable, } @@ -75,14 +90,19 @@ func getClaimableOrdersForOrderbook( ) (orderbookdomain.Orders, error) { ticks, ok := orderbookRepository.GetAllTicks(orderbook.PoolID) if !ok { - return nil, fmt.Errorf("no ticks for orderbook") + return nil, fmt.Errorf("no ticks found for orderbook %s with pool %d", orderbook.ContractAddress, orderbook.PoolID) } var claimable orderbookdomain.Orders - for _, t := range ticks { - tickClaimable, err := getClaimableOrdersForTick(ctx, fillThreshold, orderbook, t, orderBookClient, orderbookusecase, logger) + for _, tick := range ticks { + tickClaimable, err := getClaimableOrdersForTick(ctx, fillThreshold, orderbook, tick, orderBookClient, orderbookusecase, logger) if err != nil { - logger.Error("error processing tick", zap.String("orderbook", orderbook.ContractAddress), zap.Int64("tick", t.Tick.TickId), zap.Error(err)) + logger.Error( + "error processing tick", + zap.String("orderbook", orderbook.ContractAddress), + zap.Int64("tick", tick.Tick.TickId), + zap.Error(err), + ) continue } claimable = append(claimable, tickClaimable...) @@ -104,7 +124,7 @@ func getClaimableOrdersForTick( ) (orderbookdomain.Orders, error) { orders, err := orderBookClient.GetOrdersByTick(ctx, orderbook.ContractAddress, tick.Tick.TickId) if err != nil { - return nil, fmt.Errorf("unable to fetch orderbook orders by tick ID: %w", err) + return nil, err } if len(orders) == 0 { @@ -138,10 +158,17 @@ func getClaimableOrders( // 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 + cumulativeTotalValue, err := osmomath.NewDecFromStr(tickValues.CumulativeTotalValue) + if err != nil { + return false // if the cumulative total value is invalid, we assume the tick is not fully filled } - return tickValues.CumulativeTotalValue == tickValues.EffectiveTotalAmountSwapped + + effectiveTotalAmountSwapped, err := osmomath.NewDecFromStr(tickValues.EffectiveTotalAmountSwapped) + if err != nil { + return false // if the effective total amount swapped is invalid, we assume the tick is not fully filled + } + + return cumulativeTotalValue.Equal(effectiveTotalAmountSwapped) } // filterClaimableOrders processes a list of orders and returns only those that are considered claimable. diff --git a/ingest/usecase/plugins/orderbook/claimbot/order_test.go b/ingest/usecase/plugins/orderbook/claimbot/order_test.go index 97306a065..f67c18a13 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/order_test.go +++ b/ingest/usecase/plugins/orderbook/claimbot/order_test.go @@ -74,7 +74,7 @@ func TestProcessOrderbooksAndGetClaimableOrders(t *testing.T) { fillThreshold osmomath.Dec orderbooks []domain.CanonicalOrderBooksResult mockSetup func(*mocks.OrderbookRepositoryMock, *mocks.OrderbookGRPCClientMock, *mocks.OrderbookUsecaseMock) - expectedOrders []claimbot.Order + expectedOrders []claimbot.ProcessedOrderbook }{ { name: "No orderbooks", @@ -103,7 +103,7 @@ func TestProcessOrderbooksAndGetClaimableOrders(t *testing.T) { // Not claimable order, below threshold usecase.WithCreateFormattedLimitOrder(newLimitOrder(osmomath.NewDecWithPrec(90, 2)), nil) }, - expectedOrders: []claimbot.Order{ + expectedOrders: []claimbot.ProcessedOrderbook{ { Orderbook: newCanonicalOrderBooksResult(10, "contract1"), // orderbook with Orders: nil, // no claimable orders @@ -125,7 +125,7 @@ func TestProcessOrderbooksAndGetClaimableOrders(t *testing.T) { usecase.WithCreateFormattedLimitOrder(newLimitOrder(osmomath.NewDecWithPrec(90, 2)), nil) }, - expectedOrders: []claimbot.Order{ + expectedOrders: []claimbot.ProcessedOrderbook{ { Orderbook: newCanonicalOrderBooksResult(38, "contract8"), Orders: orderbookdomain.Orders{newOrder("bid")}, @@ -143,15 +143,19 @@ func TestProcessOrderbooksAndGetClaimableOrders(t *testing.T) { client.WithGetOrdersByTickCb(orderbookdomain.Orders{ newOrder("ask"), + newOrder("bid"), }, nil) // Claimable order, above threshold usecase.WithCreateFormattedLimitOrder(newLimitOrder(osmomath.NewDecWithPrec(96, 2)), nil) }, - expectedOrders: []claimbot.Order{ + expectedOrders: []claimbot.ProcessedOrderbook{ { Orderbook: newCanonicalOrderBooksResult(64, "contract58"), - Orders: orderbookdomain.Orders{newOrder("ask")}, + Orders: orderbookdomain.Orders{ + newOrder("ask"), + newOrder("bid"), + }, }, }, }, @@ -167,7 +171,8 @@ func TestProcessOrderbooksAndGetClaimableOrders(t *testing.T) { tt.mockSetup(&repository, &client, &usecase) - result := claimbot.ProcessOrderbooksAndGetClaimableOrders(ctx, tt.fillThreshold, tt.orderbooks, &repository, &client, &usecase, &logger) + result, err := claimbot.ProcessOrderbooksAndGetClaimableOrders(ctx, tt.fillThreshold, tt.orderbooks, &repository, &client, &usecase, &logger) + assert.NoError(t, err) assert.Equal(t, tt.expectedOrders, result) }) diff --git a/ingest/usecase/plugins/orderbook/claimbot/orderbook.go b/ingest/usecase/plugins/orderbook/claimbot/orderbook.go index f93c032e2..9711874ba 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/orderbook.go +++ b/ingest/usecase/plugins/orderbook/claimbot/orderbook.go @@ -1,17 +1,15 @@ package claimbot import ( - "fmt" - "github.com/osmosis-labs/sqs/domain" "github.com/osmosis-labs/sqs/domain/mvc" ) // getOrderbooks returns canonical orderbooks that are within the metadata. -func getOrderbooks(poolsUsecase mvc.PoolsUsecase, blockHeight uint64, metadata domain.BlockPoolMetadata) ([]domain.CanonicalOrderBooksResult, error) { +func getOrderbooks(poolsUsecase mvc.PoolsUsecase, metadata domain.BlockPoolMetadata) ([]domain.CanonicalOrderBooksResult, error) { orderbooks, err := poolsUsecase.GetAllCanonicalOrderbookPoolIDs() if err != nil { - return nil, fmt.Errorf("failed to get all canonical orderbook pool IDs ( block height %d ) : %w", blockHeight, err) + return nil, err } var result []domain.CanonicalOrderBooksResult diff --git a/ingest/usecase/plugins/orderbook/claimbot/orderbook_test.go b/ingest/usecase/plugins/orderbook/claimbot/orderbook_test.go index 9bf788368..e8f48ddf6 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/orderbook_test.go +++ b/ingest/usecase/plugins/orderbook/claimbot/orderbook_test.go @@ -12,16 +12,14 @@ import ( func TestGetOrderbooks(t *testing.T) { tests := []struct { - name string - blockHeight uint64 - metadata domain.BlockPoolMetadata - setupMocks func(*mocks.PoolsUsecaseMock) - want []domain.CanonicalOrderBooksResult - err bool + name string + metadata domain.BlockPoolMetadata + setupMocks func(*mocks.PoolsUsecaseMock) + want []domain.CanonicalOrderBooksResult + err bool }{ { - name: "Metadata contains all canonical orderbooks but one", - blockHeight: 1000, + name: "Metadata contains all canonical orderbooks but one", metadata: domain.BlockPoolMetadata{ PoolIDs: map[uint64]struct{}{1: {}, 2: {}, 3: {}}, }, @@ -36,8 +34,7 @@ func TestGetOrderbooks(t *testing.T) { err: false, }, { - name: "Metadata contains only canonical orderbooks", - blockHeight: 1893, + name: "Metadata contains only canonical orderbooks", metadata: domain.BlockPoolMetadata{ PoolIDs: map[uint64]struct{}{1: {}, 2: {}, 3: {}}, }, @@ -52,9 +49,8 @@ func TestGetOrderbooks(t *testing.T) { err: false, }, { - name: "Error getting all canonical orderbook pool IDs", - blockHeight: 2000, - metadata: domain.BlockPoolMetadata{}, + name: "Error getting all canonical orderbook pool IDs", + metadata: domain.BlockPoolMetadata{}, setupMocks: func(poolsUsecase *mocks.PoolsUsecaseMock) { poolsUsecase.WithGetAllCanonicalOrderbookPoolIDs(nil, assert.AnError) }, @@ -69,7 +65,7 @@ func TestGetOrderbooks(t *testing.T) { tt.setupMocks(&poolsUsecase) - got, err := claimbot.GetOrderbooks(&poolsUsecase, tt.blockHeight, tt.metadata) + got, err := claimbot.GetOrderbooks(&poolsUsecase, tt.metadata) if tt.err { assert.Error(t, err) return diff --git a/ingest/usecase/plugins/orderbook/claimbot/plugin.go b/ingest/usecase/plugins/orderbook/claimbot/plugin.go index 0fa828afc..a440f9e11 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/plugin.go +++ b/ingest/usecase/plugins/orderbook/claimbot/plugin.go @@ -79,14 +79,20 @@ func (o *claimbot) ProcessEndBlock(ctx context.Context, blockHeight uint64, meta return nil } defer o.atomicBool.Store(false) + defer o.config.Logger.Info("processed end block", zap.Uint64("block_height", blockHeight)) - orderbooks, err := getOrderbooks(o.config.PoolsUseCase, blockHeight, metadata) + orderbooks, err := getOrderbooks(o.config.PoolsUseCase, metadata) if err != nil { + o.config.Logger.Warn( + "failed to get canonical orderbook pools for block", + zap.Uint64("block_height", blockHeight), + zap.Error(err), + ) return err } // retrieve claimable orders for the orderbooks - orders := processOrderbooksAndGetClaimableOrders( + orders, err := processOrderbooksAndGetClaimableOrders( ctx, fillThreshold, orderbooks, @@ -96,6 +102,14 @@ func (o *claimbot) ProcessEndBlock(ctx context.Context, blockHeight uint64, meta o.config.Logger, ) + if err != nil { + o.config.Logger.Warn( + "failed to process block orderbooks", + zap.Error(err), + ) + return err + } + for _, orderbook := range orders { if orderbook.Err != nil { o.config.Logger.Warn( @@ -115,15 +129,13 @@ func (o *claimbot) ProcessEndBlock(ctx context.Context, blockHeight uint64, meta } } - o.config.Logger.Info("processed end block", zap.Uint64("block_height", blockHeight)) - return nil } // processOrderbookOrders processes a batch of claimable orders. func (o *claimbot) processOrderbookOrders(ctx context.Context, orderbook domain.CanonicalOrderBooksResult, orders orderbookdomain.Orders) error { if len(orders) == 0 { - return fmt.Errorf("no claimable orders found for orderbook %s, nothing to process", orderbook.ContractAddress) + return nil } for _, chunk := range slices.Split(orders, maxBatchOfClaimableOrders) { diff --git a/ingest/usecase/plugins/orderbook/claimbot/tx.go b/ingest/usecase/plugins/orderbook/claimbot/tx.go index 239357db5..7d042674b 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/tx.go +++ b/ingest/usecase/plugins/orderbook/claimbot/tx.go @@ -62,6 +62,17 @@ func sendBatchClaimTx( return sqstx.SendTx(ctx, txServiceClient, txBytes) } +// batchClaim represents batch claim orders message. +type batchClaim struct { + batchClaimOrders `json:"batch_claim"` +} + +// batchClaimOrders represents the orders in the batch claim message. +// Each order is represented by a pair of tick ID and order ID. +type batchClaimOrders struct { + Orders [][]int64 `json:"orders"` +} + // prepareBatchClaimMsg creates a JSON-encoded batch claim message from the provided orders. func prepareBatchClaimMsg(claims orderbookdomain.Orders) ([]byte, error) { orders := make([][]int64, len(claims)) @@ -69,14 +80,8 @@ func prepareBatchClaimMsg(claims orderbookdomain.Orders) ([]byte, error) { orders[i] = []int64{claim.TickId, claim.OrderId} } - batchClaim := struct { - BatchClaim struct { - Orders [][]int64 `json:"orders"` - } `json:"batch_claim"` - }{ - BatchClaim: struct { - Orders [][]int64 `json:"orders"` - }{ + batchClaim := batchClaim{ + batchClaimOrders: batchClaimOrders{ Orders: orders, }, } diff --git a/ingest/usecase/plugins/orderbook/fillbot/create_copy_config.sh b/ingest/usecase/plugins/orderbook/fillbot/create_copy_config.sh index c219306dc..6bc66a52c 100755 --- a/ingest/usecase/plugins/orderbook/fillbot/create_copy_config.sh +++ b/ingest/usecase/plugins/orderbook/fillbot/create_copy_config.sh @@ -4,6 +4,11 @@ ORIGINAL_APP_TOML_NAME="$HOME/.osmosisd/config/app.toml" # Replace with the actual file path BACKUP_APP_TOML_NAME="$HOME/.osmosisd/config/app-backup.toml" +if [ ! -f "$ORIGINAL_APP_TOML_NAME" ]; then + echo "Error: Source file $ORIGINAL_APP_TOML_NAME does not exist." + exit 1 +fi + if [ -f $BACKUP_APP_TOML_NAME ]; then echo "Backup file $BACKUP_APP_TOML_NAME already exist, no modifications will be made." exit 0 From 172b2adfb699bdf346e5aef5dd365e8cabc0c155 Mon Sep 17 00:00:00 2001 From: Deividas Petraitis Date: Wed, 30 Oct 2024 20:54:07 +0200 Subject: [PATCH 30/33] BE-586 | Requested changes #2 --- app/sidecar_query_server.go | 2 - domain/mocks/orderbook_usecase_mock.go | 26 ++- domain/mvc/orderbook.go | 6 + domain/orderbook/orderbook_tick.go | 16 ++ .../plugins/orderbook/claimbot/config.go | 44 ++--- .../plugins/orderbook/claimbot/export_test.go | 15 +- .../plugins/orderbook/claimbot/order.go | 154 +----------------- .../plugins/orderbook/claimbot/order_test.go | 87 ++-------- .../plugins/orderbook/claimbot/plugin.go | 41 +++-- .../usecase/plugins/orderbook/claimbot/tx.go | 9 +- .../plugins/orderbook/claimbot/tx_test.go | 42 ++--- orderbook/usecase/orderbook_usecase.go | 103 ++++++++++++ 12 files changed, 225 insertions(+), 320 deletions(-) diff --git a/app/sidecar_query_server.go b/app/sidecar_query_server.go index e0aa90233..4011d3b3f 100644 --- a/app/sidecar_query_server.go +++ b/app/sidecar_query_server.go @@ -295,8 +295,6 @@ func NewSideCarQueryServer(appCodec codec.Codec, config domain.Config, logger lo keyring, orderBookUseCase, poolsUseCase, - orderBookRepository, - orderBookAPIClient, logger, config.ChainGRPCGatewayEndpoint, config.ChainID, diff --git a/domain/mocks/orderbook_usecase_mock.go b/domain/mocks/orderbook_usecase_mock.go index 0f9e2283a..fe15249d0 100644 --- a/domain/mocks/orderbook_usecase_mock.go +++ b/domain/mocks/orderbook_usecase_mock.go @@ -7,17 +7,20 @@ import ( "github.com/osmosis-labs/sqs/domain/mvc" orderbookdomain "github.com/osmosis-labs/sqs/domain/orderbook" "github.com/osmosis-labs/sqs/sqsdomain" + + "github.com/osmosis-labs/osmosis/osmomath" ) var _ mvc.OrderBookUsecase = &OrderbookUsecaseMock{} // OrderbookUsecaseMock is a mock implementation of the RouterUsecase interface type OrderbookUsecaseMock struct { - ProcessPoolFunc func(ctx context.Context, pool sqsdomain.PoolI) error - GetAllTicksFunc func(poolID uint64) (map[int64]orderbookdomain.OrderbookTick, bool) - GetActiveOrdersFunc func(ctx context.Context, address string) ([]orderbookdomain.LimitOrder, bool, error) - GetActiveOrdersStreamFunc func(ctx context.Context, address string) <-chan orderbookdomain.OrderbookResult - CreateFormattedLimitOrderFunc func(orderbook domain.CanonicalOrderBooksResult, order orderbookdomain.Order) (orderbookdomain.LimitOrder, error) + ProcessPoolFunc func(ctx context.Context, pool sqsdomain.PoolI) error + GetAllTicksFunc func(poolID uint64) (map[int64]orderbookdomain.OrderbookTick, bool) + GetActiveOrdersFunc func(ctx context.Context, address string) ([]orderbookdomain.LimitOrder, bool, error) + GetActiveOrdersStreamFunc func(ctx context.Context, address string) <-chan orderbookdomain.OrderbookResult + CreateFormattedLimitOrderFunc func(orderbook domain.CanonicalOrderBooksResult, order orderbookdomain.Order) (orderbookdomain.LimitOrder, error) + GetClaimableOrdersForOrderbookFunc func(ctx context.Context, fillThreshold osmomath.Dec, orderbook domain.CanonicalOrderBooksResult) (orderbookdomain.Orders, error) } func (m *OrderbookUsecaseMock) ProcessPool(ctx context.Context, pool sqsdomain.PoolI) error { @@ -59,3 +62,16 @@ func (m *OrderbookUsecaseMock) CreateFormattedLimitOrder(orderbook domain.Canoni } panic("unimplemented") } + +func (m *OrderbookUsecaseMock) GetClaimableOrdersForOrderbook(ctx context.Context, fillThreshold osmomath.Dec, orderbook domain.CanonicalOrderBooksResult) (orderbookdomain.Orders, error) { + if m.GetClaimableOrdersForOrderbookFunc != nil { + return m.GetClaimableOrdersForOrderbookFunc(ctx, fillThreshold, orderbook) + } + panic("unimplemented") +} + +func (m *OrderbookUsecaseMock) WithGetClaimableOrdersForOrderbook(orders orderbookdomain.Orders, err error) { + m.GetClaimableOrdersForOrderbookFunc = func(ctx context.Context, fillThreshold osmomath.Dec, orderbook domain.CanonicalOrderBooksResult) (orderbookdomain.Orders, error) { + return orders, err + } +} diff --git a/domain/mvc/orderbook.go b/domain/mvc/orderbook.go index b565dded9..c112d933f 100644 --- a/domain/mvc/orderbook.go +++ b/domain/mvc/orderbook.go @@ -3,6 +3,7 @@ package mvc import ( "context" + "github.com/osmosis-labs/osmosis/osmomath" "github.com/osmosis-labs/sqs/domain" orderbookdomain "github.com/osmosis-labs/sqs/domain/orderbook" "github.com/osmosis-labs/sqs/sqsdomain" @@ -25,4 +26,9 @@ type OrderBookUsecase interface { // CreateFormattedLimitOrder creates a formatted limit order from the given orderbook and order. CreateFormattedLimitOrder(orderbook domain.CanonicalOrderBooksResult, order orderbookdomain.Order) (orderbookdomain.LimitOrder, error) + + // GetClaimableOrdersForOrderbook retrieves all claimable orders for a given orderbook. + // It fetches all ticks for the orderbook, processes each tick to find claimable orders, + // and returns a combined list of all claimable orders across all ticks. + GetClaimableOrdersForOrderbook(ctx context.Context, fillThreshold osmomath.Dec, orderbook domain.CanonicalOrderBooksResult) (orderbookdomain.Orders, error) } diff --git a/domain/orderbook/orderbook_tick.go b/domain/orderbook/orderbook_tick.go index 0a6bb9e60..c7426b3d6 100644 --- a/domain/orderbook/orderbook_tick.go +++ b/domain/orderbook/orderbook_tick.go @@ -53,3 +53,19 @@ type TickValues struct { // sync. LastTickSyncEtas string `json:"last_tick_sync_etas"` } + +// isTickFullyFilled checks if a tick is fully filled by comparing its cumulative total value +// to its effective total amount swapped. +func (tv *TickValues) IsTickFullyFilled() (bool, error) { + cumulativeTotalValue, err := osmomath.NewDecFromStr(tv.CumulativeTotalValue) + if err != nil { + return false, err + } + + effectiveTotalAmountSwapped, err := osmomath.NewDecFromStr(tv.EffectiveTotalAmountSwapped) + if err != nil { + return false, err + } + + return cumulativeTotalValue.Equal(effectiveTotalAmountSwapped), nil +} diff --git a/ingest/usecase/plugins/orderbook/claimbot/config.go b/ingest/usecase/plugins/orderbook/claimbot/config.go index e8dc6d981..cc25934bd 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/config.go +++ b/ingest/usecase/plugins/orderbook/claimbot/config.go @@ -6,8 +6,6 @@ import ( 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" txfeestypes "github.com/osmosis-labs/osmosis/v26/x/txfees/types" @@ -17,17 +15,15 @@ import ( // Config is the configuration for the claimbot plugin type Config struct { - Keyring keyring.Keyring - PoolsUseCase mvc.PoolsUsecase - OrderbookUsecase mvc.OrderBookUsecase - OrderbookRepository orderbookdomain.OrderBookRepository - OrderBookClient orderbookgrpcclientdomain.OrderBookClient - AccountQueryClient authtypes.QueryClient - TxfeesClient txfeestypes.QueryClient - GasCalculator sqstx.GasCalculator - TxServiceClient txtypes.ServiceClient - ChainID string - Logger log.Logger + Keyring keyring.Keyring + PoolsUseCase mvc.PoolsUsecase + OrderbookUsecase mvc.OrderBookUsecase + AccountQueryClient authtypes.QueryClient + TxfeesClient txfeestypes.QueryClient + GasCalculator sqstx.GasCalculator + TxServiceClient txtypes.ServiceClient + ChainID string + Logger log.Logger } // NewConfig creates a new Config instance. @@ -35,8 +31,6 @@ func NewConfig( keyring keyring.Keyring, orderbookusecase mvc.OrderBookUsecase, poolsUseCase mvc.PoolsUsecase, - orderbookRepository orderbookdomain.OrderBookRepository, - orderBookClient orderbookgrpcclientdomain.OrderBookClient, logger log.Logger, chainGRPCGatewayEndpoint string, chainID string, @@ -47,16 +41,14 @@ func NewConfig( } return &Config{ - Keyring: keyring, - PoolsUseCase: poolsUseCase, - OrderbookUsecase: orderbookusecase, - OrderbookRepository: orderbookRepository, - OrderBookClient: orderBookClient, - AccountQueryClient: authtypes.NewQueryClient(grpcClient), - TxfeesClient: txfeestypes.NewQueryClient(grpcClient), - GasCalculator: sqstx.NewGasCalculator(grpcClient), - TxServiceClient: txtypes.NewServiceClient(grpcClient), - Logger: logger.Named("claimbot"), - ChainID: chainID, + Keyring: keyring, + PoolsUseCase: poolsUseCase, + OrderbookUsecase: orderbookusecase, + AccountQueryClient: authtypes.NewQueryClient(grpcClient), + TxfeesClient: txfeestypes.NewQueryClient(grpcClient), + GasCalculator: sqstx.NewGasCalculator(grpcClient), + TxServiceClient: txtypes.NewServiceClient(grpcClient), + Logger: logger.Named("claimbot"), + ChainID: chainID, }, nil } diff --git a/ingest/usecase/plugins/orderbook/claimbot/export_test.go b/ingest/usecase/plugins/orderbook/claimbot/export_test.go index 9118f06f9..ad9ee4597 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/export_test.go +++ b/ingest/usecase/plugins/orderbook/claimbot/export_test.go @@ -4,13 +4,10 @@ import ( "context" "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" txfeestypes "github.com/osmosis-labs/osmosis/v26/x/txfees/types" @@ -18,6 +15,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" txtypes "github.com/cosmos/cosmos-sdk/types/tx" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" ) // ProcessedOrderbook is order alias data structure for testing purposes. @@ -27,14 +25,11 @@ type ProcessedOrderbook = processedOrderbook // This function is exported for testing purposes. func ProcessOrderbooksAndGetClaimableOrders( ctx context.Context, + orderbookusecase mvc.OrderBookUsecase, fillThreshold osmomath.Dec, orderbooks []domain.CanonicalOrderBooksResult, - orderbookRepository orderbookdomain.OrderBookRepository, - orderBookClient orderbookgrpcclientdomain.OrderBookClient, - orderbookusecase mvc.OrderBookUsecase, - logger log.Logger, ) ([]ProcessedOrderbook, error) { - return processOrderbooksAndGetClaimableOrders(ctx, fillThreshold, orderbooks, orderbookRepository, orderBookClient, orderbookusecase, logger) + return processOrderbooksAndGetClaimableOrders(ctx, orderbookusecase, fillThreshold, orderbooks) } // SendBatchClaimTx a test wrapper for sendBatchClaimTx. @@ -42,15 +37,15 @@ func ProcessOrderbooksAndGetClaimableOrders( func SendBatchClaimTx( ctx context.Context, keyring keyring.Keyring, - accountQueryClient authtypes.QueryClient, txfeesClient txfeestypes.QueryClient, gasCalculator sqstx.GasCalculator, txServiceClient txtypes.ServiceClient, chainID string, + account *authtypes.BaseAccount, contractAddress string, claims orderbookdomain.Orders, ) (*sdk.TxResponse, error) { - return sendBatchClaimTx(ctx, keyring, accountQueryClient, txfeesClient, gasCalculator, txServiceClient, chainID, contractAddress, claims) + return sendBatchClaimTx(ctx, keyring, txfeesClient, gasCalculator, txServiceClient, chainID, account, contractAddress, claims) } // PrepareBatchClaimMsg is a test wrapper for prepareBatchClaimMsg. diff --git a/ingest/usecase/plugins/orderbook/claimbot/order.go b/ingest/usecase/plugins/orderbook/claimbot/order.go index 9b22e0384..52ee1a132 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/order.go +++ b/ingest/usecase/plugins/orderbook/claimbot/order.go @@ -2,18 +2,15 @@ package claimbot import ( "context" - "fmt" "github.com/osmosis-labs/osmosis/osmomath" "github.com/osmosis-labs/sqs/domain" "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" - - "go.uber.org/zap" ) +// processedOrderbook is a data structure +// containing the processed orderbook and its claimable orders. type processedOrderbook struct { Orderbook domain.CanonicalOrderBooksResult Orders orderbookdomain.Orders @@ -24,18 +21,15 @@ type processedOrderbook struct { // Under the hood processing of each orderbook in done concurrently to speed up the process. func processOrderbooksAndGetClaimableOrders( ctx context.Context, + orderbookusecase mvc.OrderBookUsecase, fillThreshold osmomath.Dec, orderbooks []domain.CanonicalOrderBooksResult, - orderbookRepository orderbookdomain.OrderBookRepository, - orderBookClient orderbookgrpcclientdomain.OrderBookClient, - orderbookusecase mvc.OrderBookUsecase, - logger log.Logger, ) ([]processedOrderbook, error) { ch := make(chan processedOrderbook, len(orderbooks)) for _, orderbook := range orderbooks { go func(orderbook domain.CanonicalOrderBooksResult) { - o := processOrderbook(ctx, fillThreshold, orderbook, orderbookRepository, orderBookClient, orderbookusecase, logger) + o := processOrderbook(ctx, orderbookusecase, fillThreshold, orderbook) ch <- o }(orderbook) } @@ -56,14 +50,11 @@ func processOrderbooksAndGetClaimableOrders( // processOrderbook processes a single orderbook and returns an order struct containing the processed orderbook and its claimable orders. func processOrderbook( ctx context.Context, + orderbookusecase mvc.OrderBookUsecase, fillThreshold osmomath.Dec, orderbook domain.CanonicalOrderBooksResult, - orderbookRepository orderbookdomain.OrderBookRepository, - orderBookClient orderbookgrpcclientdomain.OrderBookClient, - orderbookusecase mvc.OrderBookUsecase, - logger log.Logger, ) processedOrderbook { - claimable, err := getClaimableOrdersForOrderbook(ctx, fillThreshold, orderbook, orderbookRepository, orderBookClient, orderbookusecase, logger) + claimable, err := orderbookusecase.GetClaimableOrdersForOrderbook(ctx, fillThreshold, orderbook) if err != nil { return processedOrderbook{ Orderbook: orderbook, @@ -75,136 +66,3 @@ func processOrderbook( Orders: claimable, } } - -// getClaimableOrdersForOrderbook retrieves all claimable orders for a given orderbook. -// It fetches all ticks for the orderbook, processes each tick to find claimable orders, -// and returns a combined list of all claimable orders across all ticks. -func getClaimableOrdersForOrderbook( - ctx context.Context, - fillThreshold osmomath.Dec, - orderbook domain.CanonicalOrderBooksResult, - orderbookRepository orderbookdomain.OrderBookRepository, - orderBookClient orderbookgrpcclientdomain.OrderBookClient, - orderbookusecase mvc.OrderBookUsecase, - logger log.Logger, -) (orderbookdomain.Orders, error) { - ticks, ok := orderbookRepository.GetAllTicks(orderbook.PoolID) - if !ok { - return nil, fmt.Errorf("no ticks found for orderbook %s with pool %d", orderbook.ContractAddress, orderbook.PoolID) - } - - var claimable orderbookdomain.Orders - for _, tick := range ticks { - tickClaimable, err := getClaimableOrdersForTick(ctx, fillThreshold, orderbook, tick, orderBookClient, orderbookusecase, logger) - if err != nil { - logger.Error( - "error processing tick", - zap.String("orderbook", orderbook.ContractAddress), - zap.Int64("tick", tick.Tick.TickId), - zap.Error(err), - ) - continue - } - claimable = append(claimable, tickClaimable...) - } - - return claimable, nil -} - -// getClaimableOrdersForTick retrieves claimable orders for a specific tick in an orderbook -// It processes all ask/bid direction orders and filters the orders that are claimable. -func getClaimableOrdersForTick( - ctx context.Context, - fillThreshold osmomath.Dec, - orderbook domain.CanonicalOrderBooksResult, - tick orderbookdomain.OrderbookTick, - orderBookClient orderbookgrpcclientdomain.OrderBookClient, - orderbookusecase mvc.OrderBookUsecase, - logger log.Logger, -) (orderbookdomain.Orders, error) { - orders, err := orderBookClient.GetOrdersByTick(ctx, orderbook.ContractAddress, tick.Tick.TickId) - if err != nil { - return nil, err - } - - if len(orders) == 0 { - return nil, nil - } - - askClaimable := getClaimableOrders(orderbook, orders.OrderByDirection("ask"), tick.TickState.AskValues, fillThreshold, orderbookusecase, logger) - bidClaimable := getClaimableOrders(orderbook, orders.OrderByDirection("bid"), tick.TickState.BidValues, fillThreshold, orderbookusecase, logger) - - return append(askClaimable, bidClaimable...), nil -} - -// getClaimableOrders determines which orders are claimable for a given direction (ask or bid) in a tick. -// If the tick is fully filled, all orders are considered claimable. Otherwise, it filters the orders -// based on the fill threshold. -func getClaimableOrders( - orderbook domain.CanonicalOrderBooksResult, - orders orderbookdomain.Orders, - tickValues orderbookdomain.TickValues, - fillThreshold osmomath.Dec, - orderbookusecase mvc.OrderBookUsecase, - logger log.Logger, -) orderbookdomain.Orders { - 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 { - cumulativeTotalValue, err := osmomath.NewDecFromStr(tickValues.CumulativeTotalValue) - if err != nil { - return false // if the cumulative total value is invalid, we assume the tick is not fully filled - } - - effectiveTotalAmountSwapped, err := osmomath.NewDecFromStr(tickValues.EffectiveTotalAmountSwapped) - if err != nil { - return false // if the effective total amount swapped is invalid, we assume the tick is not fully filled - } - - return cumulativeTotalValue.Equal(effectiveTotalAmountSwapped) -} - -// filterClaimableOrders processes a list of orders and returns only those that are considered claimable. -func filterClaimableOrders( - orderbook domain.CanonicalOrderBooksResult, - orders orderbookdomain.Orders, - fillThreshold osmomath.Dec, - orderbookusecase mvc.OrderBookUsecase, - logger log.Logger, -) orderbookdomain.Orders { - var claimable orderbookdomain.Orders - for _, order := range orders { - if isOrderClaimable(orderbook, order, fillThreshold, orderbookusecase, logger) { - claimable = append(claimable, order) - } - } - return claimable -} - -// isOrderClaimable determines if a single order is claimable based on the fill threshold. -func isOrderClaimable( - orderbook domain.CanonicalOrderBooksResult, - order orderbookdomain.Order, - fillThreshold osmomath.Dec, - orderbookusecase mvc.OrderBookUsecase, - logger log.Logger, -) bool { - result, err := orderbookusecase.CreateFormattedLimitOrder(orderbook, order) - if err != nil { - logger.Info( - "unable to create orderbook limit order; marking as not claimable", - zap.String("orderbook", orderbook.ContractAddress), - zap.Int64("order", order.OrderId), - zap.Error(err), - ) - return false - } - return result.IsClaimable(fillThreshold) -} diff --git a/ingest/usecase/plugins/orderbook/claimbot/order_test.go b/ingest/usecase/plugins/orderbook/claimbot/order_test.go index f67c18a13..d813f269e 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/order_test.go +++ b/ingest/usecase/plugins/orderbook/claimbot/order_test.go @@ -9,47 +9,11 @@ import ( "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, @@ -58,13 +22,6 @@ func TestProcessOrderbooksAndGetClaimableOrders(t *testing.T) { } } - 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} } @@ -73,17 +30,14 @@ func TestProcessOrderbooksAndGetClaimableOrders(t *testing.T) { name string fillThreshold osmomath.Dec orderbooks []domain.CanonicalOrderBooksResult - mockSetup func(*mocks.OrderbookRepositoryMock, *mocks.OrderbookGRPCClientMock, *mocks.OrderbookUsecaseMock) + mockSetup func(*mocks.OrderbookUsecaseMock) expectedOrders []claimbot.ProcessedOrderbook }{ { 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 - } + mockSetup: func(usecase *mocks.OrderbookUsecaseMock) { }, expectedOrders: nil, }, @@ -93,15 +47,8 @@ func TestProcessOrderbooksAndGetClaimableOrders(t *testing.T) { 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) + mockSetup: func(usecase *mocks.OrderbookUsecaseMock) { + usecase.WithGetClaimableOrdersForOrderbook(nil, nil) }, expectedOrders: []claimbot.ProcessedOrderbook{ { @@ -116,14 +63,8 @@ func TestProcessOrderbooksAndGetClaimableOrders(t *testing.T) { 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) + mockSetup: func(usecase *mocks.OrderbookUsecaseMock) { + usecase.WithGetClaimableOrdersForOrderbook(orderbookdomain.Orders{newOrder("bid")}, nil) }, expectedOrders: []claimbot.ProcessedOrderbook{ { @@ -138,16 +79,11 @@ func TestProcessOrderbooksAndGetClaimableOrders(t *testing.T) { 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{ + mockSetup: func(usecase *mocks.OrderbookUsecaseMock) { + usecase.WithGetClaimableOrdersForOrderbook(orderbookdomain.Orders{ newOrder("ask"), newOrder("bid"), }, nil) - - // Claimable order, above threshold - usecase.WithCreateFormattedLimitOrder(newLimitOrder(osmomath.NewDecWithPrec(96, 2)), nil) }, expectedOrders: []claimbot.ProcessedOrderbook{ { @@ -164,14 +100,11 @@ func TestProcessOrderbooksAndGetClaimableOrders(t *testing.T) { 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) + tt.mockSetup(&usecase) - result, err := claimbot.ProcessOrderbooksAndGetClaimableOrders(ctx, tt.fillThreshold, tt.orderbooks, &repository, &client, &usecase, &logger) + result, err := claimbot.ProcessOrderbooksAndGetClaimableOrders(ctx, &usecase, tt.fillThreshold, tt.orderbooks) assert.NoError(t, err) 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 a440f9e11..ee86ab168 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/plugin.go +++ b/ingest/usecase/plugins/orderbook/claimbot/plugin.go @@ -4,18 +4,18 @@ import ( "context" "fmt" "sync/atomic" - "time" "github.com/osmosis-labs/sqs/domain" "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/domain/slices" "github.com/osmosis-labs/sqs/log" "github.com/osmosis-labs/osmosis/osmomath" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "go.opentelemetry.io/otel" "go.uber.org/zap" ) @@ -34,9 +34,8 @@ const ( ) var ( - tracer = otel.Tracer(tracerName) - fillThreshold = osmomath.MustNewDecFromStr("0.98") - blockInclusionWaitTime = 5 * time.Second + tracer = otel.Tracer(tracerName) + fillThreshold = osmomath.MustNewDecFromStr("0.98") ) // maxBatchOfClaimableOrders is the maximum number of claimable orders @@ -48,13 +47,11 @@ func New( keyring keyring.Keyring, orderbookusecase mvc.OrderBookUsecase, poolsUsecase mvc.PoolsUsecase, - orderbookRepository orderbookdomain.OrderBookRepository, - orderBookClient orderbookgrpcclientdomain.OrderBookClient, logger log.Logger, chainGRPCGatewayEndpoint string, chainID string, ) (*claimbot, error) { - config, err := NewConfig(keyring, orderbookusecase, poolsUsecase, orderbookRepository, orderBookClient, logger, chainGRPCGatewayEndpoint, chainID) + config, err := NewConfig(keyring, orderbookusecase, poolsUsecase, logger, chainGRPCGatewayEndpoint, chainID) if err != nil { return nil, fmt.Errorf("failed to create config: %w", err) } @@ -91,15 +88,17 @@ func (o *claimbot) ProcessEndBlock(ctx context.Context, blockHeight uint64, meta return err } + account, err := o.config.AccountQueryClient.GetAccount(ctx, o.config.Keyring.GetAddress().String()) + if err != nil { + return err + } + // retrieve claimable orders for the orderbooks orders, err := processOrderbooksAndGetClaimableOrders( ctx, + o.config.OrderbookUsecase, fillThreshold, orderbooks, - o.config.OrderbookRepository, - o.config.OrderBookClient, - o.config.OrderbookUsecase, - o.config.Logger, ) if err != nil { @@ -120,7 +119,7 @@ func (o *claimbot) ProcessEndBlock(ctx context.Context, blockHeight uint64, meta continue } - if err := o.processOrderbookOrders(ctx, orderbook.Orderbook, orderbook.Orders); err != nil { + if err := o.processOrderbookOrders(ctx, account, orderbook.Orderbook, orderbook.Orders); err != nil { o.config.Logger.Warn( "failed to process orderbook orders", zap.String("contract_address", orderbook.Orderbook.ContractAddress), @@ -133,7 +132,7 @@ func (o *claimbot) ProcessEndBlock(ctx context.Context, blockHeight uint64, meta } // processOrderbookOrders processes a batch of claimable orders. -func (o *claimbot) processOrderbookOrders(ctx context.Context, orderbook domain.CanonicalOrderBooksResult, orders orderbookdomain.Orders) error { +func (o *claimbot) processOrderbookOrders(ctx context.Context, account *authtypes.BaseAccount, orderbook domain.CanonicalOrderBooksResult, orders orderbookdomain.Orders) error { if len(orders) == 0 { return nil } @@ -146,11 +145,11 @@ func (o *claimbot) processOrderbookOrders(ctx context.Context, orderbook domain. txres, err := sendBatchClaimTx( ctx, o.config.Keyring, - o.config.AccountQueryClient, o.config.TxfeesClient, o.config.GasCalculator, o.config.TxServiceClient, o.config.ChainID, + account, orderbook.ContractAddress, chunk, ) @@ -162,10 +161,18 @@ func (o *claimbot) processOrderbookOrders(ctx context.Context, orderbook domain. zap.Any("tx result", txres), zap.Error(err), ) + continue // continue processing the next batch } - // Wait for block inclusion with buffer to avoid sequence mismatch - time.Sleep(blockInclusionWaitTime) + // Since we have a lock on block processing, that is, if block X is being processed, + // block X+1 processing cannot start, instead of waiting for the tx to be included + // in the block we set the sequence number here to avoid sequence number mismatch errors. + if err := account.SetSequence(account.GetSequence() + 1); err != nil { + o.config.Logger.Info("failed incrementing account sequence number", + zap.String("orderbook contract address", orderbook.ContractAddress), + zap.Error(err), + ) + } } return nil diff --git a/ingest/usecase/plugins/orderbook/claimbot/tx.go b/ingest/usecase/plugins/orderbook/claimbot/tx.go index 7d042674b..7456ebde0 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/tx.go +++ b/ingest/usecase/plugins/orderbook/claimbot/tx.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" - 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" orderbookdomain "github.com/osmosis-labs/sqs/domain/orderbook" @@ -16,6 +15,7 @@ import ( wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" sdk "github.com/cosmos/cosmos-sdk/types" txtypes "github.com/cosmos/cosmos-sdk/types/tx" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" ) var ( @@ -27,21 +27,16 @@ var ( func sendBatchClaimTx( ctx context.Context, keyring keyring.Keyring, - accountQueryClient authtypes.QueryClient, txfeesClient txfeestypes.QueryClient, gasCalculator sqstx.GasCalculator, txServiceClient txtypes.ServiceClient, chainID string, + account *authtypes.BaseAccount, contractAddress string, claims orderbookdomain.Orders, ) (*sdk.TxResponse, error) { address := keyring.GetAddress().String() - account, err := accountQueryClient.GetAccount(ctx, address) - if err != nil { - return nil, err - } - msgBytes, err := prepareBatchClaimMsg(claims) if err != nil { return nil, err diff --git a/ingest/usecase/plugins/orderbook/claimbot/tx_test.go b/ingest/usecase/plugins/orderbook/claimbot/tx_test.go index 9f046e0ae..79bc6c652 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/tx_test.go +++ b/ingest/usecase/plugins/orderbook/claimbot/tx_test.go @@ -22,38 +22,24 @@ func TestSendBatchClaimTx(t *testing.T) { chainID string contractAddress string claims orderbookdomain.Orders - setupMocks func(*mocks.Keyring, *mocks.AuthQueryClientMock, *mocks.TxFeesQueryClient, *mocks.GasCalculator, *mocks.TxServiceClient) + setupMocks func(*mocks.Keyring, *authtypes.BaseAccount, *mocks.TxFeesQueryClient, *mocks.GasCalculator, *mocks.TxServiceClient) setSendTxFunc func() []byte expectedResponse *sdk.TxResponse expectedError bool }{ - { - name: "AuthQueryClient.GetAccountFunc returns error", - contractAddress: "osmo1contractaddress", - claims: orderbookdomain.Orders{ - {TickId: 13, OrderId: 99}, - }, - setupMocks: func(keyringMock *mocks.Keyring, authQueryClient *mocks.AuthQueryClientMock, txfeesClient *mocks.TxFeesQueryClient, gasCalculator *mocks.GasCalculator, txServiceClient *mocks.TxServiceClient) { - keyringMock.WithGetAddress("osmo0address") - keyringMock.WithGetKey("6cf5103c60c939a5f38e383b52239c5296c968579eec1c68a47d70fbf1d19159") - authQueryClient.WithGetAccount(nil, assert.AnError) - }, - expectedResponse: &sdk.TxResponse{}, - expectedError: true, - }, { name: "BuildTx returns error", contractAddress: "osmo1contractaddress", claims: orderbookdomain.Orders{ {TickId: 13, OrderId: 99}, }, - setupMocks: func(keyringMock *mocks.Keyring, authQueryClient *mocks.AuthQueryClientMock, txfeesClient *mocks.TxFeesQueryClient, gasCalculator *mocks.GasCalculator, txServiceClient *mocks.TxServiceClient) { + setupMocks: func(keyringMock *mocks.Keyring, account *authtypes.BaseAccount, txfeesClient *mocks.TxFeesQueryClient, gasCalculator *mocks.GasCalculator, txServiceClient *mocks.TxServiceClient) { keyringMock.WithGetAddress("osmo0address") keyringMock.WithGetKey("6cf5103c60c939a5f38e383b52239c5296c968579eec1c68a47d70fbf1d19159") - authQueryClient.WithGetAccount(&authtypes.BaseAccount{ + account = &authtypes.BaseAccount{ AccountNumber: 3, Sequence: 31, - }, nil) + } gasCalculator.WithCalculateGas(nil, 0, assert.AnError) // Fail BuildTx }, expectedResponse: &sdk.TxResponse{}, @@ -65,16 +51,16 @@ func TestSendBatchClaimTx(t *testing.T) { claims: orderbookdomain.Orders{ {TickId: 13, OrderId: 99}, }, - setupMocks: func(keyringMock *mocks.Keyring, authQueryClient *mocks.AuthQueryClientMock, txfeesClient *mocks.TxFeesQueryClient, gasCalculator *mocks.GasCalculator, txServiceClient *mocks.TxServiceClient) { + setupMocks: func(keyringMock *mocks.Keyring, account *authtypes.BaseAccount, txfeesClient *mocks.TxFeesQueryClient, gasCalculator *mocks.GasCalculator, txServiceClient *mocks.TxServiceClient) { keyringMock.WithGetAddress("osmo5address") keyringMock.WithGetKey("6cf5103c60c939a5f38e383b52239c5296c968579eec1c68a47d70fbf1d19159") gasCalculator.WithCalculateGas(nil, 51, nil) txfeesClient.WithBaseDenom("uosmo", nil) txfeesClient.WithGetEipBaseFee("0.2", nil) - authQueryClient.WithGetAccount(&authtypes.BaseAccount{ + account = &authtypes.BaseAccount{ AccountNumber: 83, Sequence: 5, - }, nil) + } txServiceClient.WithBroadcastTx(nil, assert.AnError) // SendTx returns error }, expectedResponse: &sdk.TxResponse{}, @@ -88,16 +74,16 @@ func TestSendBatchClaimTx(t *testing.T) { {TickId: 1, OrderId: 100}, {TickId: 2, OrderId: 200}, }, - setupMocks: func(keyringMock *mocks.Keyring, authQueryClient *mocks.AuthQueryClientMock, txfeesClient *mocks.TxFeesQueryClient, gasCalculator *mocks.GasCalculator, txServiceClient *mocks.TxServiceClient) { + setupMocks: func(keyringMock *mocks.Keyring, account *authtypes.BaseAccount, txfeesClient *mocks.TxFeesQueryClient, gasCalculator *mocks.GasCalculator, txServiceClient *mocks.TxServiceClient) { keyringMock.WithGetAddress("osmo1address") keyringMock.WithGetKey("6cf5103c60c939a5f38e383b52239c5296c968579eec1c68a47d70fbf1d19159") gasCalculator.WithCalculateGas(nil, 51, nil) txfeesClient.WithBaseDenom("uosmo", nil) txfeesClient.WithGetEipBaseFee("0.15", nil) - authQueryClient.WithGetAccount(&authtypes.BaseAccount{ + account = &authtypes.BaseAccount{ AccountNumber: 1, Sequence: 1, - }, nil) + } txServiceClient.BroadcastTxFunc = func(ctx context.Context, in *txtypes.BroadcastTxRequest, opts ...grpc.CallOption) (*txtypes.BroadcastTxResponse, error) { return &txtypes.BroadcastTxResponse{ @@ -108,7 +94,7 @@ func TestSendBatchClaimTx(t *testing.T) { } }, expectedResponse: &sdk.TxResponse{ - Data: "\n\x90\x01\n\x8d\x01\n$/cosmwasm.wasm.v1.MsgExecuteContract\x12e\n\x1fosmo1daek6me3v9jxgun9wdes7m4n5q\x12\x14osmo1contractaddress\x1a,{\"batch_claim\":{\"orders\":[[1,100],[2,200]]}}\x12b\nP\nF\n\x1f/cosmos.crypto.secp256k1.PubKey\x12#\n!\x03\xef]m\xf2\x8a\bx\x1f\x9a%v]E\x9e\x96\xa8\x9dc6a\x1d\x1f\x8a\xb4\xd3/q,֍\xd3\xd0\x12\x04\n\x02\b\x01\x18\x01\x12\x0e\n\n\n\x05uosmo\x12\x018\x103\x1a@Xߠ&\xea\xb8\x0e\xefؓf\xb3\xe7DMӡW\x99h\u008e\xbdh\xef\\\xd3\xd7\x02\xf1\xdc\xe1&\r\x91\xdd\xcdtu\xee\xdeJ\x90\x1a\x7f\xb2(L\x15\xe0+'\xf5\xe3\fV\t3!\xa2,\x802z", + Data: "\n\x90\x01\n\x8d\x01\n$/cosmwasm.wasm.v1.MsgExecuteContract\x12e\n\x1fosmo1daek6me3v9jxgun9wdes7m4n5q\x12\x14osmo1contractaddress\x1a,{\"batch_claim\":{\"orders\":[[1,100],[2,200]]}}\x12`\nN\nF\n\x1f/cosmos.crypto.secp256k1.PubKey\x12#\n!\x03\xef]m\xf2\x8a\bx\x1f\x9a%v]E\x9e\x96\xa8\x9dc6a\x1d\x1f\x8a\xb4\xd3/q,֍\xd3\xd0\x12\x04\n\x02\b\x01\x12\x0e\n\n\n\x05uosmo\x12\x018\x103\x1a@\x1dI\xb5/D\xd0L\v2\xacg\x91\xb3;b+\xdb\xf6\xe0\x1c\x92\xee\xb8d\xc4&%<ڵ\x81\xd6u\xeb-\xf0ੌ\xf5\xa8);\x19\xfc%@\r\xfb2\x05AI\x13\xf3)=\n\xcf~\xb0\"\xf0\xb1", }, expectedError: false, }, @@ -118,14 +104,14 @@ func TestSendBatchClaimTx(t *testing.T) { t.Run(tt.name, func(t *testing.T) { ctx := context.Background() keyring := mocks.Keyring{} - authQueryClient := mocks.AuthQueryClientMock{} + account := authtypes.BaseAccount{} txFeesClient := mocks.TxFeesQueryClient{} gasCalculator := mocks.GasCalculator{} txServiceClient := mocks.TxServiceClient{} - tt.setupMocks(&keyring, &authQueryClient, &txFeesClient, &gasCalculator, &txServiceClient) + tt.setupMocks(&keyring, &account, &txFeesClient, &gasCalculator, &txServiceClient) - response, err := claimbot.SendBatchClaimTx(ctx, &keyring, &authQueryClient, &txFeesClient, &gasCalculator, &txServiceClient, tt.chainID, tt.contractAddress, tt.claims) + response, err := claimbot.SendBatchClaimTx(ctx, &keyring, &txFeesClient, &gasCalculator, &txServiceClient, tt.chainID, &account, tt.contractAddress, tt.claims) if tt.expectedError { assert.Error(t, err) } else { diff --git a/orderbook/usecase/orderbook_usecase.go b/orderbook/usecase/orderbook_usecase.go index 78c6bc31d..32558cd23 100644 --- a/orderbook/usecase/orderbook_usecase.go +++ b/orderbook/usecase/orderbook_usecase.go @@ -501,3 +501,106 @@ func (o *OrderbookUseCaseImpl) CreateFormattedLimitOrder(orderbook domain.Canoni PlacedAt: placedAt, }, nil } + +func (o *OrderbookUseCaseImpl) GetClaimableOrdersForOrderbook(ctx context.Context, fillThreshold osmomath.Dec, orderbook domain.CanonicalOrderBooksResult) (orderbookdomain.Orders, error) { + ticks, ok := o.orderbookRepository.GetAllTicks(orderbook.PoolID) + if !ok { + return nil, fmt.Errorf("no ticks found for orderbook %s with pool %d", orderbook.ContractAddress, orderbook.PoolID) + } + + var claimable orderbookdomain.Orders + for _, tick := range ticks { + tickClaimable, err := o.getClaimableOrdersForTick(ctx, fillThreshold, orderbook, tick) + if err != nil { + o.logger.Error( + "error processing tick", + zap.String("orderbook", orderbook.ContractAddress), + zap.Int64("tick", tick.Tick.TickId), + zap.Error(err), + ) + continue + } + claimable = append(claimable, tickClaimable...) + } + + return claimable, nil +} + +// getClaimableOrdersForTick retrieves claimable orders for a specific tick in an orderbook +// It processes all ask/bid direction orders and filters the orders that are claimable. +func (o *OrderbookUseCaseImpl) getClaimableOrdersForTick( + ctx context.Context, + fillThreshold osmomath.Dec, + orderbook domain.CanonicalOrderBooksResult, + tick orderbookdomain.OrderbookTick, +) (orderbookdomain.Orders, error) { + orders, err := o.orderBookClient.GetOrdersByTick(ctx, orderbook.ContractAddress, tick.Tick.TickId) + if err != nil { + return nil, err + } + + if len(orders) == 0 { + return nil, nil + } + + askClaimable, err := o.getClaimableOrders(orderbook, orders.OrderByDirection("ask"), tick.TickState.AskValues, fillThreshold) + if err != nil { + return nil, err + } + + bidClaimable, err := o.getClaimableOrders(orderbook, orders.OrderByDirection("bid"), tick.TickState.BidValues, fillThreshold) + if err != nil { + return nil, err + } + + return append(askClaimable, bidClaimable...), nil +} + +// getClaimableOrders determines which orders are claimable for a given direction (ask or bid) in a tick. +// If the tick is fully filled, all orders are considered claimable. Otherwise, it filters the orders +// based on the fill threshold. +func (o *OrderbookUseCaseImpl) getClaimableOrders( + orderbook domain.CanonicalOrderBooksResult, + orders orderbookdomain.Orders, + tickValues orderbookdomain.TickValues, + fillThreshold osmomath.Dec, +) (orderbookdomain.Orders, error) { + // if the cumulative total value is invalid, we assume the tick is not fully filled + isFilled, err := tickValues.IsTickFullyFilled() + if err != nil { + return nil, err + } + + if isFilled { + return orders, nil + } + + var result orderbookdomain.Orders + for _, order := range orders { + claimable := o.isOrderClaimable(orderbook, order, fillThreshold) + if claimable { + result = append(result, order) + } + } + + return result, nil +} + +// isOrderClaimable determines if a single order is claimable based on the fill threshold. +func (o *OrderbookUseCaseImpl) isOrderClaimable( + orderbook domain.CanonicalOrderBooksResult, + order orderbookdomain.Order, + fillThreshold osmomath.Dec, +) bool { + result, err := o.CreateFormattedLimitOrder(orderbook, order) + if err != nil { + o.logger.Info( + "unable to create orderbook limit order; marking as not claimable", + zap.String("orderbook", orderbook.ContractAddress), + zap.Int64("order", order.OrderId), + zap.Error(err), + ) + return false + } + return result.IsClaimable(fillThreshold) +} From d11fbf2588bf75415aadad8658a77b82d7f87bb9 Mon Sep 17 00:00:00 2001 From: Deividas Petraitis Date: Wed, 30 Oct 2024 21:15:07 +0200 Subject: [PATCH 31/33] BE-586 | Sequence number update --- ingest/usecase/plugins/orderbook/claimbot/plugin.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ingest/usecase/plugins/orderbook/claimbot/plugin.go b/ingest/usecase/plugins/orderbook/claimbot/plugin.go index ee86ab168..cb2d3e4c8 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/plugin.go +++ b/ingest/usecase/plugins/orderbook/claimbot/plugin.go @@ -161,6 +161,13 @@ func (o *claimbot) processOrderbookOrders(ctx context.Context, account *authtype zap.Any("tx result", txres), zap.Error(err), ) + + // if the tx failed, we need to fetch the account again to get the latest sequence number. + account, err = o.config.AccountQueryClient.GetAccount(ctx, o.config.Keyring.GetAddress().String()) + if err != nil { + return err + } + continue // continue processing the next batch } From beee7692fd7aafc7fe355cc49f7c778a67e2f4c2 Mon Sep 17 00:00:00 2001 From: Deividas Petraitis Date: Thu, 31 Oct 2024 21:19:27 +0200 Subject: [PATCH 32/33] BE-586 | added tests --- domain/mocks/orderbook_usecase_mock.go | 8 +- domain/mvc/orderbook.go | 2 +- domain/orderbook/order.go | 16 +++ .../plugins/orderbook/claimbot/order.go | 32 ++--- .../plugins/orderbook/claimbot/order_test.go | 48 +++++-- .../plugins/orderbook/claimbot/plugin.go | 39 +++++- orderbook/usecase/orderbook_usecase.go | 60 ++++----- orderbook/usecase/orderbook_usecase_test.go | 117 ++++++++++++++++++ orderbook/usecase/orderbooktesting/suite.go | 13 ++ 9 files changed, 257 insertions(+), 78 deletions(-) diff --git a/domain/mocks/orderbook_usecase_mock.go b/domain/mocks/orderbook_usecase_mock.go index fe15249d0..2652fe4bd 100644 --- a/domain/mocks/orderbook_usecase_mock.go +++ b/domain/mocks/orderbook_usecase_mock.go @@ -20,7 +20,7 @@ type OrderbookUsecaseMock struct { GetActiveOrdersFunc func(ctx context.Context, address string) ([]orderbookdomain.LimitOrder, bool, error) GetActiveOrdersStreamFunc func(ctx context.Context, address string) <-chan orderbookdomain.OrderbookResult CreateFormattedLimitOrderFunc func(orderbook domain.CanonicalOrderBooksResult, order orderbookdomain.Order) (orderbookdomain.LimitOrder, error) - GetClaimableOrdersForOrderbookFunc func(ctx context.Context, fillThreshold osmomath.Dec, orderbook domain.CanonicalOrderBooksResult) (orderbookdomain.Orders, error) + GetClaimableOrdersForOrderbookFunc func(ctx context.Context, fillThreshold osmomath.Dec, orderbook domain.CanonicalOrderBooksResult) ([]orderbookdomain.ClaimableOrderbook, error) } func (m *OrderbookUsecaseMock) ProcessPool(ctx context.Context, pool sqsdomain.PoolI) error { @@ -63,15 +63,15 @@ func (m *OrderbookUsecaseMock) CreateFormattedLimitOrder(orderbook domain.Canoni panic("unimplemented") } -func (m *OrderbookUsecaseMock) GetClaimableOrdersForOrderbook(ctx context.Context, fillThreshold osmomath.Dec, orderbook domain.CanonicalOrderBooksResult) (orderbookdomain.Orders, error) { +func (m *OrderbookUsecaseMock) GetClaimableOrdersForOrderbook(ctx context.Context, fillThreshold osmomath.Dec, orderbook domain.CanonicalOrderBooksResult) ([]orderbookdomain.ClaimableOrderbook, error) { if m.GetClaimableOrdersForOrderbookFunc != nil { return m.GetClaimableOrdersForOrderbookFunc(ctx, fillThreshold, orderbook) } panic("unimplemented") } -func (m *OrderbookUsecaseMock) WithGetClaimableOrdersForOrderbook(orders orderbookdomain.Orders, err error) { - m.GetClaimableOrdersForOrderbookFunc = func(ctx context.Context, fillThreshold osmomath.Dec, orderbook domain.CanonicalOrderBooksResult) (orderbookdomain.Orders, error) { +func (m *OrderbookUsecaseMock) WithGetClaimableOrdersForOrderbook(orders []orderbookdomain.ClaimableOrderbook, err error) { + m.GetClaimableOrdersForOrderbookFunc = func(ctx context.Context, fillThreshold osmomath.Dec, orderbook domain.CanonicalOrderBooksResult) ([]orderbookdomain.ClaimableOrderbook, error) { return orders, err } } diff --git a/domain/mvc/orderbook.go b/domain/mvc/orderbook.go index c112d933f..c1cb18206 100644 --- a/domain/mvc/orderbook.go +++ b/domain/mvc/orderbook.go @@ -30,5 +30,5 @@ type OrderBookUsecase interface { // GetClaimableOrdersForOrderbook retrieves all claimable orders for a given orderbook. // It fetches all ticks for the orderbook, processes each tick to find claimable orders, // and returns a combined list of all claimable orders across all ticks. - GetClaimableOrdersForOrderbook(ctx context.Context, fillThreshold osmomath.Dec, orderbook domain.CanonicalOrderBooksResult) (orderbookdomain.Orders, error) + GetClaimableOrdersForOrderbook(ctx context.Context, fillThreshold osmomath.Dec, orderbook domain.CanonicalOrderBooksResult) ([]orderbookdomain.ClaimableOrderbook, error) } diff --git a/domain/orderbook/order.go b/domain/orderbook/order.go index ceff8f763..66d542522 100644 --- a/domain/orderbook/order.go +++ b/domain/orderbook/order.go @@ -119,3 +119,19 @@ type OrderbookResult struct { IsBestEffort bool Error error } + +// ClaimableOrderbook represents a list of claimable orders for an orderbook. +// If an error occurs processing the orders, it is stored in the error field. +type ClaimableOrderbook struct { + Tick OrderbookTick + Orders []ClaimableOrder + Error error +} + +// ClaimableOrder represents an order that is claimable. +// If an error occurs processing the order, it is stored in the error field +// and the order is nil. +type ClaimableOrder struct { + Order Order + Error error +} diff --git a/ingest/usecase/plugins/orderbook/claimbot/order.go b/ingest/usecase/plugins/orderbook/claimbot/order.go index 52ee1a132..f4c9d9c0e 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/order.go +++ b/ingest/usecase/plugins/orderbook/claimbot/order.go @@ -13,8 +13,8 @@ import ( // containing the processed orderbook and its claimable orders. type processedOrderbook struct { Orderbook domain.CanonicalOrderBooksResult - Orders orderbookdomain.Orders - Err error + Orders []orderbookdomain.ClaimableOrderbook + Error error } // processOrderbooksAndGetClaimableOrders processes a list of orderbooks and returns claimable orders for each. @@ -29,8 +29,12 @@ func processOrderbooksAndGetClaimableOrders( for _, orderbook := range orderbooks { go func(orderbook domain.CanonicalOrderBooksResult) { - o := processOrderbook(ctx, orderbookusecase, fillThreshold, orderbook) - ch <- o + orders, err := orderbookusecase.GetClaimableOrdersForOrderbook(ctx, fillThreshold, orderbook) + ch <- processedOrderbook{ + Orderbook: orderbook, + Orders: orders, + Error: err, + } }(orderbook) } @@ -46,23 +50,3 @@ func processOrderbooksAndGetClaimableOrders( return results, nil } - -// processOrderbook processes a single orderbook and returns an order struct containing the processed orderbook and its claimable orders. -func processOrderbook( - ctx context.Context, - orderbookusecase mvc.OrderBookUsecase, - fillThreshold osmomath.Dec, - orderbook domain.CanonicalOrderBooksResult, -) processedOrderbook { - claimable, err := orderbookusecase.GetClaimableOrdersForOrderbook(ctx, fillThreshold, orderbook) - if err != nil { - return processedOrderbook{ - Orderbook: orderbook, - Err: err, - } - } - return processedOrderbook{ - Orderbook: orderbook, - Orders: claimable, - } -} diff --git a/ingest/usecase/plugins/orderbook/claimbot/order_test.go b/ingest/usecase/plugins/orderbook/claimbot/order_test.go index d813f269e..80c8786da 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/order_test.go +++ b/ingest/usecase/plugins/orderbook/claimbot/order_test.go @@ -64,12 +64,29 @@ func TestProcessOrderbooksAndGetClaimableOrders(t *testing.T) { newCanonicalOrderBooksResult(38, "contract8"), }, mockSetup: func(usecase *mocks.OrderbookUsecaseMock) { - usecase.WithGetClaimableOrdersForOrderbook(orderbookdomain.Orders{newOrder("bid")}, nil) + usecase.WithGetClaimableOrdersForOrderbook( + []orderbookdomain.ClaimableOrderbook{ + { + Orders: []orderbookdomain.ClaimableOrder{ + { + Order: newOrder("bid"), + }, + }, + }, + }, nil) }, expectedOrders: []claimbot.ProcessedOrderbook{ { Orderbook: newCanonicalOrderBooksResult(38, "contract8"), - Orders: orderbookdomain.Orders{newOrder("bid")}, + Orders: []orderbookdomain.ClaimableOrderbook{ + { + Orders: []orderbookdomain.ClaimableOrder{ + { + Order: newOrder("bid"), + }, + }, + }, + }, }, }, }, @@ -80,17 +97,30 @@ func TestProcessOrderbooksAndGetClaimableOrders(t *testing.T) { newCanonicalOrderBooksResult(64, "contract58"), }, mockSetup: func(usecase *mocks.OrderbookUsecaseMock) { - usecase.WithGetClaimableOrdersForOrderbook(orderbookdomain.Orders{ - newOrder("ask"), - newOrder("bid"), - }, nil) + usecase.WithGetClaimableOrdersForOrderbook( + []orderbookdomain.ClaimableOrderbook{ + { + Orders: []orderbookdomain.ClaimableOrder{ + { + Order: newOrder("ask"), + }, + { + Order: newOrder("bid"), + }, + }, + }, + }, nil) }, expectedOrders: []claimbot.ProcessedOrderbook{ { Orderbook: newCanonicalOrderBooksResult(64, "contract58"), - Orders: orderbookdomain.Orders{ - newOrder("ask"), - newOrder("bid"), + Orders: []orderbookdomain.ClaimableOrderbook{ + { + Orders: []orderbookdomain.ClaimableOrder{ + {Order: newOrder("ask")}, + {Order: newOrder("bid")}, + }, + }, }, }, }, diff --git a/ingest/usecase/plugins/orderbook/claimbot/plugin.go b/ingest/usecase/plugins/orderbook/claimbot/plugin.go index cb2d3e4c8..c9042c220 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/plugin.go +++ b/ingest/usecase/plugins/orderbook/claimbot/plugin.go @@ -93,8 +93,8 @@ func (o *claimbot) ProcessEndBlock(ctx context.Context, blockHeight uint64, meta return err } - // retrieve claimable orders for the orderbooks - orders, err := processOrderbooksAndGetClaimableOrders( + // retrieve claimable proccessedOrderbooks for the orderbooks + proccessedOrderbooks, err := processOrderbooksAndGetClaimableOrders( ctx, o.config.OrderbookUsecase, fillThreshold, @@ -109,17 +109,44 @@ func (o *claimbot) ProcessEndBlock(ctx context.Context, blockHeight uint64, meta return err } - for _, orderbook := range orders { - if orderbook.Err != nil { + for _, orderbook := range proccessedOrderbooks { + if orderbook.Error != nil { o.config.Logger.Warn( "failed to retrieve claimable orders", zap.String("contract_address", orderbook.Orderbook.ContractAddress), - zap.Error(orderbook.Err), + zap.Error(orderbook.Error), ) continue } - if err := o.processOrderbookOrders(ctx, account, orderbook.Orderbook, orderbook.Orders); err != nil { + var claimable orderbookdomain.Orders + for _, orderbookOrder := range orderbook.Orders { + if orderbookOrder.Error != nil { + o.config.Logger.Warn( + "error processing orderbook", + zap.String("orderbook", orderbook.Orderbook.ContractAddress), + zap.Int64("tick", orderbookOrder.Tick.Tick.TickId), + zap.Error(err), + ) + continue + } + + for _, order := range orderbookOrder.Orders { + if order.Error != nil { + o.config.Logger.Warn( + "unable to create orderbook limit order; marking as not claimable", + zap.String("orderbook", orderbook.Orderbook.ContractAddress), + zap.Int64("tick", orderbookOrder.Tick.Tick.TickId), + zap.Error(err), + ) + continue + } + + claimable = append(claimable, order.Order) + } + } + + if err := o.processOrderbookOrders(ctx, account, orderbook.Orderbook, claimable); err != nil { o.config.Logger.Warn( "failed to process orderbook orders", zap.String("contract_address", orderbook.Orderbook.ContractAddress), diff --git a/orderbook/usecase/orderbook_usecase.go b/orderbook/usecase/orderbook_usecase.go index 32558cd23..d43ef636e 100644 --- a/orderbook/usecase/orderbook_usecase.go +++ b/orderbook/usecase/orderbook_usecase.go @@ -502,28 +502,23 @@ func (o *OrderbookUseCaseImpl) CreateFormattedLimitOrder(orderbook domain.Canoni }, nil } -func (o *OrderbookUseCaseImpl) GetClaimableOrdersForOrderbook(ctx context.Context, fillThreshold osmomath.Dec, orderbook domain.CanonicalOrderBooksResult) (orderbookdomain.Orders, error) { +func (o *OrderbookUseCaseImpl) GetClaimableOrdersForOrderbook(ctx context.Context, fillThreshold osmomath.Dec, orderbook domain.CanonicalOrderBooksResult) ([]orderbookdomain.ClaimableOrderbook, error) { ticks, ok := o.orderbookRepository.GetAllTicks(orderbook.PoolID) if !ok { return nil, fmt.Errorf("no ticks found for orderbook %s with pool %d", orderbook.ContractAddress, orderbook.PoolID) } - var claimable orderbookdomain.Orders + var orders []orderbookdomain.ClaimableOrderbook for _, tick := range ticks { - tickClaimable, err := o.getClaimableOrdersForTick(ctx, fillThreshold, orderbook, tick) - if err != nil { - o.logger.Error( - "error processing tick", - zap.String("orderbook", orderbook.ContractAddress), - zap.Int64("tick", tick.Tick.TickId), - zap.Error(err), - ) - continue - } - claimable = append(claimable, tickClaimable...) + tickOrders, err := o.getClaimableOrdersForTick(ctx, fillThreshold, orderbook, tick) + orders = append(orders, orderbookdomain.ClaimableOrderbook{ + Tick: tick, + Orders: tickOrders, + Error: err, + }) } - return claimable, nil + return orders, nil } // getClaimableOrdersForTick retrieves claimable orders for a specific tick in an orderbook @@ -533,14 +528,14 @@ func (o *OrderbookUseCaseImpl) getClaimableOrdersForTick( fillThreshold osmomath.Dec, orderbook domain.CanonicalOrderBooksResult, tick orderbookdomain.OrderbookTick, -) (orderbookdomain.Orders, error) { +) ([]orderbookdomain.ClaimableOrder, error) { orders, err := o.orderBookClient.GetOrdersByTick(ctx, orderbook.ContractAddress, tick.Tick.TickId) if err != nil { return nil, err } if len(orders) == 0 { - return nil, nil + return nil, nil // nothing to process } askClaimable, err := o.getClaimableOrders(orderbook, orders.OrderByDirection("ask"), tick.TickState.AskValues, fillThreshold) @@ -564,22 +559,25 @@ func (o *OrderbookUseCaseImpl) getClaimableOrders( orders orderbookdomain.Orders, tickValues orderbookdomain.TickValues, fillThreshold osmomath.Dec, -) (orderbookdomain.Orders, error) { - // if the cumulative total value is invalid, we assume the tick is not fully filled +) ([]orderbookdomain.ClaimableOrder, error) { isFilled, err := tickValues.IsTickFullyFilled() if err != nil { return nil, err } - if isFilled { - return orders, nil - } - - var result orderbookdomain.Orders + var result []orderbookdomain.ClaimableOrder for _, order := range orders { - claimable := o.isOrderClaimable(orderbook, order, fillThreshold) + if isFilled { + result = append(result, orderbookdomain.ClaimableOrder{Order: order}) + continue + } + + claimable, err := o.isOrderClaimable(orderbook, order, fillThreshold) if claimable { - result = append(result, order) + result = append(result, orderbookdomain.ClaimableOrder{ + Order: order, + Error: err, + }) } } @@ -591,16 +589,10 @@ func (o *OrderbookUseCaseImpl) isOrderClaimable( orderbook domain.CanonicalOrderBooksResult, order orderbookdomain.Order, fillThreshold osmomath.Dec, -) bool { +) (bool, error) { result, err := o.CreateFormattedLimitOrder(orderbook, order) if err != nil { - o.logger.Info( - "unable to create orderbook limit order; marking as not claimable", - zap.String("orderbook", orderbook.ContractAddress), - zap.Int64("order", order.OrderId), - zap.Error(err), - ) - return false + return false, err } - return result.IsClaimable(fillThreshold) + return result.IsClaimable(fillThreshold), nil } diff --git a/orderbook/usecase/orderbook_usecase_test.go b/orderbook/usecase/orderbook_usecase_test.go index fd063605b..645b84f6a 100644 --- a/orderbook/usecase/orderbook_usecase_test.go +++ b/orderbook/usecase/orderbook_usecase_test.go @@ -1074,3 +1074,120 @@ func (s *OrderbookUsecaseTestSuite) TestCreateFormattedLimitOrder() { }) } } + +func (s *OrderbookUsecaseTestSuite) TestGetClaimableOrdersForOrderbook() { + + newOrder := func(id int64, direction string) orderbookdomain.Order { + order := s.NewOrder() + order.OrderId = id + order.OrderDirection = direction + return order.Order + } + + testCases := []struct { + name string + setupMocks func(orderbookrepository *mocks.OrderbookRepositoryMock, client *mocks.OrderbookGRPCClientMock, tokensusecase *mocks.TokensUsecaseMock) + orderbook domain.CanonicalOrderBooksResult + fillThreshold osmomath.Dec + expectedOrders []orderbookdomain.ClaimableOrderbook + expectedError bool + }{ + { + name: "no ticks found for orderbook", + setupMocks: func(orderbookrepository *mocks.OrderbookRepositoryMock, client *mocks.OrderbookGRPCClientMock, tokensusecase *mocks.TokensUsecaseMock) { + orderbookrepository.GetAllTicksFunc = func(poolID uint64) (map[int64]orderbookdomain.OrderbookTick, bool) { + return nil, false + } + }, + orderbook: domain.CanonicalOrderBooksResult{PoolID: 1, ContractAddress: "osmo1contract"}, + fillThreshold: osmomath.NewDec(80), + expectedError: true, + }, + { + name: "error processing tick", + setupMocks: func(orderbookrepository *mocks.OrderbookRepositoryMock, client *mocks.OrderbookGRPCClientMock, tokensusecase *mocks.TokensUsecaseMock) { + orderbookrepository.GetAllTicksFunc = func(poolID uint64) (map[int64]orderbookdomain.OrderbookTick, bool) { + return map[int64]orderbookdomain.OrderbookTick{ + 1: s.NewTick("500", 100, "bid"), + }, true + } + client.GetOrdersByTickCb = func(ctx context.Context, contractAddress string, tickID int64) (orderbookdomain.Orders, error) { + return nil, assert.AnError + } + }, + orderbook: domain.CanonicalOrderBooksResult{PoolID: 1, ContractAddress: "osmo1contract"}, + fillThreshold: osmomath.NewDec(80), + expectedOrders: []orderbookdomain.ClaimableOrderbook{ + { + Tick: s.NewTick("500", 100, "bid"), + Orders: nil, + Error: assert.AnError, + }, + }, + expectedError: false, + }, + { + name: "successful retrieval of claimable orders", + setupMocks: func(orderbookrepository *mocks.OrderbookRepositoryMock, client *mocks.OrderbookGRPCClientMock, tokensusecase *mocks.TokensUsecaseMock) { + + tokensusecase.GetSpotPriceScalingFactorByDenomFunc = s.GetSpotPriceScalingFactorByDenomFunc(1, nil) + orderbookrepository.GetAllTicksFunc = func(poolID uint64) (map[int64]orderbookdomain.OrderbookTick, bool) { + return map[int64]orderbookdomain.OrderbookTick{ + 1: s.NewTick("500", 100, "all"), + }, true + } + client.GetOrdersByTickCb = func(ctx context.Context, contractAddress string, tickID int64) (orderbookdomain.Orders, error) { + return orderbookdomain.Orders{ + newOrder(1, "bid"), + newOrder(2, "bid"), + }, nil + } + orderbookrepository.GetTickByIDFunc = s.GetTickByIDFunc(s.NewTick("500", 100, "bid"), true) + }, + orderbook: domain.CanonicalOrderBooksResult{PoolID: 1, ContractAddress: "osmo1contract"}, + fillThreshold: osmomath.MustNewDecFromStr("0.3"), + expectedOrders: []orderbookdomain.ClaimableOrderbook{ + { + Tick: s.NewTick("500", 100, "all"), + Orders: []orderbookdomain.ClaimableOrder{ + { + Order: newOrder(1, "bid"), + }, + { + Order: newOrder(2, "bid"), + }, + }, + Error: nil, + }, + }, + expectedError: false, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + // Create instances of the mocks + orderbookrepository := mocks.OrderbookRepositoryMock{} + client := mocks.OrderbookGRPCClientMock{} + tokensusecase := mocks.TokensUsecaseMock{} + + if tc.setupMocks != nil { + tc.setupMocks(&orderbookrepository, &client, &tokensusecase) + } + + // Setup the mocks according to the test case + usecase := orderbookusecase.New(&orderbookrepository, &client, nil, &tokensusecase, &log.NoOpLogger{}) + + // Call the method under test + orders, err := usecase.GetClaimableOrdersForOrderbook(context.Background(), tc.fillThreshold, tc.orderbook) + + // Assert the results + if tc.expectedError { + s.Assert().Error(err) + } else { + s.Assert().NoError(err) + s.Assert().Equal(tc.expectedOrders, orders) + } + }) + } +} diff --git a/orderbook/usecase/orderbooktesting/suite.go b/orderbook/usecase/orderbooktesting/suite.go index c256e5566..c9039c848 100644 --- a/orderbook/usecase/orderbooktesting/suite.go +++ b/orderbook/usecase/orderbooktesting/suite.go @@ -8,6 +8,7 @@ import ( "github.com/osmosis-labs/sqs/domain" orderbookdomain "github.com/osmosis-labs/sqs/domain/orderbook" "github.com/osmosis-labs/sqs/router/usecase/routertesting" + "github.com/osmosis-labs/sqs/sqsdomain/cosmwasmpool" "github.com/osmosis-labs/osmosis/osmomath" ) @@ -114,6 +115,7 @@ func (s *OrderbookTestHelper) NewTick(effectiveTotalAmountSwapped string, unreal tickValues := orderbookdomain.TickValues{ EffectiveTotalAmountSwapped: effectiveTotalAmountSwapped, + CumulativeTotalValue: "1000", } tick := orderbookdomain.OrderbookTick{ @@ -133,6 +135,17 @@ func (s *OrderbookTestHelper) NewTick(effectiveTotalAmountSwapped string, unreal } } + if direction == "all" { + tick.TickState.BidValues = tickValues + if unrealizedCancels != 0 { + tick.UnrealizedCancels.BidUnrealizedCancels = osmomath.NewInt(unrealizedCancels) + } + } + + tick.Tick = &cosmwasmpool.OrderbookTick{ + TickLiquidity: cosmwasmpool.OrderbookTickLiquidity{}, + } + return tick } From 2593bc0f7f5cd8051d148da2628822d2bcb81df9 Mon Sep 17 00:00:00 2001 From: Deividas Petraitis Date: Thu, 31 Oct 2024 22:01:19 +0200 Subject: [PATCH 33/33] BE-586 | Suggested improvements --- orderbook/usecase/orderbook_usecase.go | 10 ++++------ orderbook/usecase/orderbooktesting/suite.go | 14 ++++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/orderbook/usecase/orderbook_usecase.go b/orderbook/usecase/orderbook_usecase.go index d43ef636e..a0227d911 100644 --- a/orderbook/usecase/orderbook_usecase.go +++ b/orderbook/usecase/orderbook_usecase.go @@ -571,13 +571,11 @@ func (o *OrderbookUseCaseImpl) getClaimableOrders( result = append(result, orderbookdomain.ClaimableOrder{Order: order}) continue } - claimable, err := o.isOrderClaimable(orderbook, order, fillThreshold) - if claimable { - result = append(result, orderbookdomain.ClaimableOrder{ - Order: order, - Error: err, - }) + orderToAdd := orderbookdomain.ClaimableOrder{Order: order, Error: err} + + if err != nil || claimable { + result = append(result, orderToAdd) } } diff --git a/orderbook/usecase/orderbooktesting/suite.go b/orderbook/usecase/orderbooktesting/suite.go index c9039c848..3bf0974b9 100644 --- a/orderbook/usecase/orderbooktesting/suite.go +++ b/orderbook/usecase/orderbooktesting/suite.go @@ -122,24 +122,26 @@ func (s *OrderbookTestHelper) NewTick(effectiveTotalAmountSwapped string, unreal TickState: orderbookdomain.TickState{}, UnrealizedCancels: orderbookdomain.UnrealizedCancels{}, } - - if direction == "bid" { + switch direction { + case "bid": tick.TickState.BidValues = tickValues if unrealizedCancels != 0 { tick.UnrealizedCancels.BidUnrealizedCancels = osmomath.NewInt(unrealizedCancels) } - } else { + case "ask": tick.TickState.AskValues = tickValues if unrealizedCancels != 0 { tick.UnrealizedCancels.AskUnrealizedCancels = osmomath.NewInt(unrealizedCancels) } - } - - if direction == "all" { + case "all": + tick.TickState.AskValues = tickValues tick.TickState.BidValues = tickValues if unrealizedCancels != 0 { + tick.UnrealizedCancels.AskUnrealizedCancels = osmomath.NewInt(unrealizedCancels) tick.UnrealizedCancels.BidUnrealizedCancels = osmomath.NewInt(unrealizedCancels) } + default: + s.T().Fatalf("invalid direction: %s", direction) } tick.Tick = &cosmwasmpool.OrderbookTick{