diff --git a/CHANGELOG.md b/CHANGELOG.md index d39794a88..7e58e918c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ ## Unreleased +- #548 - Return base fee in /quote regardless of simulation success. - #547 - Add /quote simulation for "out given in" single routes. - #526 - Refactor gas estimation APIs - #524 - Claimbot diff --git a/app/sidecar_query_server.go b/app/sidecar_query_server.go index b86287c1e..e440a67c9 100644 --- a/app/sidecar_query_server.go +++ b/app/sidecar_query_server.go @@ -25,6 +25,7 @@ import ( "github.com/osmosis-labs/sqs/domain/cosmos/auth/types" 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/basefee" 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" @@ -217,11 +218,10 @@ func NewSideCarQueryServer(appCodec codec.Codec, config domain.Config, logger lo } grpcClient := passthroughGRPCClient.GetChainGRPCClient() - gasCalculator := tx.NewGasCalculator(grpcClient, tx.CalculateGas) + gasCalculator := tx.NewMsgSimulator(grpcClient, tx.CalculateGas, routerRepository) quoteSimulator := quotesimulator.NewQuoteSimulator( gasCalculator, app.GetEncodingConfig(), - txfeestypes.NewQueryClient(grpcClient), types.NewQueryClient(grpcClient), config.ChainID, ) @@ -324,6 +324,10 @@ func NewSideCarQueryServer(appCodec codec.Codec, config domain.Config, logger lo } } + // Unconditionally register the base fee fetcher. + baseFeeFetcherPlugin := basefee.NewEndBlockUpdatePlugin(routerRepository, txfeestypes.NewQueryClient(grpcClient), logger) + ingestUseCase.RegisterEndBlockProcessPlugin(baseFeeFetcherPlugin) + // Register chain info use case as a listener to the pool liquidity compute worker (healthcheck). poolLiquidityComputeWorker.RegisterListener(chainInfoUseCase) diff --git a/docs/docs.go b/docs/docs.go index b0c539691..8be2da856 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -311,6 +311,12 @@ const docTemplate = `{ "description": "Slippage tolerance multiplier for the simulation. If simulatorAddress is provided, this must be provided.", "name": "simulationSlippageTolerance", "in": "query" + }, + { + "type": "boolean", + "description": "Boolean flag indicating whether to append the base fee to the quote. False by default.", + "name": "appendBaseFee", + "in": "query" } ], "responses": { diff --git a/docs/swagger.json b/docs/swagger.json index 00a6aa56b..fbf23dfb7 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -302,6 +302,12 @@ "description": "Slippage tolerance multiplier for the simulation. If simulatorAddress is provided, this must be provided.", "name": "simulationSlippageTolerance", "in": "query" + }, + { + "type": "boolean", + "description": "Boolean flag indicating whether to append the base fee to the quote. False by default.", + "name": "appendBaseFee", + "in": "query" } ], "responses": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 2c581e93a..521c7f5f3 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -417,6 +417,11 @@ paths: in: query name: simulationSlippageTolerance type: string + - description: Boolean flag indicating whether to append the base fee to the + quote. False by default. + in: query + name: appendBaseFee + type: boolean produces: - application/json responses: diff --git a/domain/base_fee.go b/domain/base_fee.go new file mode 100644 index 000000000..5643ea89b --- /dev/null +++ b/domain/base_fee.go @@ -0,0 +1,9 @@ +package domain + +import "github.com/osmosis-labs/osmosis/osmomath" + +// BaseFee holds the denom and current base fee +type BaseFee struct { + Denom string + CurrentFee osmomath.Dec +} diff --git a/domain/cosmos/tx/msg_simulator.go b/domain/cosmos/tx/msg_simulator.go index 9a88df574..a1ed19a70 100644 --- a/domain/cosmos/tx/msg_simulator.go +++ b/domain/cosmos/tx/msg_simulator.go @@ -2,6 +2,7 @@ package tx import ( "context" + "errors" cosmosclient "github.com/cosmos/cosmos-sdk/client" txclient "github.com/cosmos/cosmos-sdk/client/tx" @@ -11,8 +12,10 @@ import ( signingtypes "github.com/cosmos/cosmos-sdk/types/tx/signing" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" "github.com/osmosis-labs/osmosis/v26/app/params" - txfeestypes "github.com/osmosis-labs/osmosis/v26/x/txfees/types" + "github.com/osmosis-labs/sqs/domain" "github.com/osmosis-labs/sqs/domain/keyring" + routerrepo "github.com/osmosis-labs/sqs/router/repository" + "google.golang.org/grpc" gogogrpc "github.com/cosmos/gogoproto/grpc" ) @@ -22,7 +25,6 @@ type MsgSimulator interface { BuildTx( ctx context.Context, keyring keyring.Keyring, - txfeesClient txfeestypes.QueryClient, encodingConfig params.EncodingConfig, account *authtypes.BaseAccount, chainID string, @@ -43,19 +45,19 @@ type MsgSimulator interface { // which is the fee amount in the base denomination. PriceMsgs( ctx context.Context, - txfeesClient txfeestypes.QueryClient, encodingConfig cosmosclient.TxConfig, account *authtypes.BaseAccount, chainID string, msg ...sdk.Msg, - ) (uint64, sdk.Coin, error) + ) domain.TxFeeInfo } -// NewGasCalculator creates a new GasCalculator instance. -func NewGasCalculator(clientCtx gogogrpc.ClientConn, calculateGas CalculateGasFn) MsgSimulator { +// NewMsgSimulator creates a new GasCalculator instance. +func NewMsgSimulator(clientCtx gogogrpc.ClientConn, calculateGas CalculateGasFn, memoryRouterRepository routerrepo.RouterRepository) MsgSimulator { return &txGasCalulator{ - clientCtx: clientCtx, - calculateGas: calculateGas, + clientCtx: clientCtx, + calculateGas: calculateGas, + memoryRouterRepository: memoryRouterRepository, } } @@ -64,8 +66,9 @@ type CalculateGasFn func(clientCtx gogogrpc.ClientConn, txf txclient.Factory, ms // txGasCalulator is a GasCalculator implementation that uses simulated transactions to calculate gas. type txGasCalulator struct { - clientCtx gogogrpc.ClientConn - calculateGas CalculateGasFn + clientCtx grpc.ClientConnInterface + calculateGas CalculateGasFn + memoryRouterRepository routerrepo.BaseFeeRepository } // BuildTx constructs a transaction using the provided parameters and messages. @@ -73,7 +76,6 @@ type txGasCalulator struct { func (c *txGasCalulator) BuildTx( ctx context.Context, keyring keyring.Keyring, - txfeesClient txfeestypes.QueryClient, encodingConfig params.EncodingConfig, account *authtypes.BaseAccount, chainID string, @@ -90,13 +92,13 @@ func (c *txGasCalulator) BuildTx( return nil, err } - gasAdjusted, feecoin, err := c.PriceMsgs(ctx, txfeesClient, encodingConfig.TxConfig, account, chainID, msg...) - if err != nil { - return nil, err + priceInfo := c.PriceMsgs(ctx, encodingConfig.TxConfig, account, chainID, msg...) + if priceInfo.Err != "" { + return nil, errors.New(priceInfo.Err) } - txBuilder.SetGasLimit(gasAdjusted) - txBuilder.SetFeeAmount(sdk.Coins{feecoin}) + txBuilder.SetGasLimit(priceInfo.AdjustedGasUsed) + txBuilder.SetFeeAmount(sdk.Coins{priceInfo.FeeCoin}) sigV2 := BuildSignatures(privKey.PubKey(), nil, account.Sequence) err = txBuilder.SetSignatures(sigV2) @@ -145,7 +147,15 @@ func (c *txGasCalulator) SimulateMsgs(encodingConfig cosmosclient.TxConfig, acco } // PriceMsgs implements MsgSimulator. -func (c *txGasCalulator) PriceMsgs(ctx context.Context, txfeesClient txfeestypes.QueryClient, encodingConfig cosmosclient.TxConfig, account *authtypes.BaseAccount, chainID string, msg ...sdk.Msg) (uint64, sdk.Coin, error) { +func (c *txGasCalulator) PriceMsgs(ctx context.Context, encodingConfig cosmosclient.TxConfig, account *authtypes.BaseAccount, chainID string, msg ...sdk.Msg) domain.TxFeeInfo { + baseFee := c.memoryRouterRepository.GetBaseFee() + if baseFee.CurrentFee.IsNil() || baseFee.CurrentFee.IsZero() { + return domain.TxFeeInfo{Err: "base fee is zero or nil"} + } + if baseFee.Denom == "" { + return domain.TxFeeInfo{Err: "base fee denom is empty"} + } + _, gasAdjusted, err := c.SimulateMsgs( encodingConfig, account, @@ -153,15 +163,17 @@ func (c *txGasCalulator) PriceMsgs(ctx context.Context, txfeesClient txfeestypes msg, ) if err != nil { - return 0, sdk.Coin{}, err + return domain.TxFeeInfo{Err: err.Error(), BaseFee: baseFee.CurrentFee} } - feeCoin, err := CalculateFeeCoin(ctx, txfeesClient, gasAdjusted) - if err != nil { - return 0, sdk.Coin{}, err - } + feeAmount := CalculateFeeAmount(baseFee.CurrentFee, gasAdjusted) - return gasAdjusted, feeCoin, nil + return domain.TxFeeInfo{ + AdjustedGasUsed: gasAdjusted, + FeeCoin: sdk.Coin{Denom: baseFee.Denom, Amount: feeAmount}, + BaseFee: baseFee.CurrentFee, + Err: "", + } } // CalculateGas calculates the gas required for a transaction using the provided transaction factory and messages. diff --git a/domain/cosmos/tx/msg_simulator_test.go b/domain/cosmos/tx/msg_simulator_test.go index c18ac22af..7d18904b5 100644 --- a/domain/cosmos/tx/msg_simulator_test.go +++ b/domain/cosmos/tx/msg_simulator_test.go @@ -8,8 +8,11 @@ import ( txtypes "github.com/cosmos/cosmos-sdk/types/tx" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" "github.com/osmosis-labs/osmosis/osmomath" + "github.com/osmosis-labs/sqs/domain" "github.com/osmosis-labs/sqs/domain/cosmos/tx" "github.com/osmosis-labs/sqs/domain/mocks" + "github.com/osmosis-labs/sqs/log" + routerrepo "github.com/osmosis-labs/sqs/router/repository" "github.com/stretchr/testify/assert" ) @@ -29,6 +32,8 @@ var ( } testMsg = newMsg("sender", "contract", `{"payload": "hello contract"}`) testTxJSON = []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=="]}`) + + testBaseFeeDec = osmomath.MustNewDecFromStr(testBaseFee) ) func TestSimulateMsgs(t *testing.T) { @@ -71,7 +76,8 @@ func TestSimulateMsgs(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { calculateGasFnMock := tt.setupMocks(mocks.DefaultGetCalculateGasMock) - gasCalculator := tx.NewGasCalculator(nil, calculateGasFnMock) + routerRepository := routerrepo.New(&log.NoOpLogger{}) + gasCalculator := tx.NewMsgSimulator(nil, calculateGasFnMock, routerRepository) result, gas, err := gasCalculator.SimulateMsgs( encodingConfig.TxConfig, @@ -95,7 +101,8 @@ func TestSimulateMsgs(t *testing.T) { func TestBuildTx(t *testing.T) { testCases := []struct { name string - setupMocks func(calculator mocks.GetCalculateGasMock, txFeesClient *mocks.TxFeesQueryClient, keyring *mocks.Keyring) tx.CalculateGasFn + setupMocks func(calculator mocks.GetCalculateGasMock, keyring *mocks.Keyring) tx.CalculateGasFn + preSetBaseFee domain.BaseFee account *authtypes.BaseAccount chainID string msgs []sdk.Msg @@ -104,22 +111,23 @@ func TestBuildTx(t *testing.T) { }{ { name: "Valid transaction", - setupMocks: func(calculator mocks.GetCalculateGasMock, txFeesClient *mocks.TxFeesQueryClient, keyring *mocks.Keyring) tx.CalculateGasFn { + setupMocks: func(calculator mocks.GetCalculateGasMock, keyring *mocks.Keyring) tx.CalculateGasFn { keyring.WithGetKey(testKey) - txFeesClient.WithBaseDenom(testDenom, nil) - txFeesClient.WithGetEipBaseFee(testBaseFee, nil) - return calculator(&txtypes.SimulateResponse{GasInfo: &sdk.GasInfo{GasUsed: 100000}}, testGasUsed, nil) }, - account: testAccount, - chainID: testChainID, - msgs: []sdk.Msg{testMsg}, + account: testAccount, + chainID: testChainID, + msgs: []sdk.Msg{testMsg}, + preSetBaseFee: domain.BaseFee{ + Denom: testDenom, + CurrentFee: testBaseFeeDec, + }, expectedJSON: testTxJSON, expectedError: false, }, { name: "Error building transaction", - setupMocks: func(calculator mocks.GetCalculateGasMock, txFeesClient *mocks.TxFeesQueryClient, keyring *mocks.Keyring) tx.CalculateGasFn { + setupMocks: func(calculator mocks.GetCalculateGasMock, keyring *mocks.Keyring) tx.CalculateGasFn { keyring.WithGetKey(testKey) return calculator(&txtypes.SimulateResponse{}, testGasUsed, assert.AnError) }, @@ -133,16 +141,15 @@ func TestBuildTx(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - txFeesClient := mocks.TxFeesQueryClient{} keyring := mocks.Keyring{} - - calculateGasFnMock := tc.setupMocks(mocks.DefaultGetCalculateGasMock, &txFeesClient, &keyring) - msgSimulator := tx.NewGasCalculator(nil, calculateGasFnMock) + routerRepository := routerrepo.New(&log.NoOpLogger{}) + routerRepository.SetBaseFee(tc.preSetBaseFee) + calculateGasFnMock := tc.setupMocks(mocks.DefaultGetCalculateGasMock, &keyring) + msgSimulator := tx.NewMsgSimulator(nil, calculateGasFnMock, routerRepository) txBuilder, err := msgSimulator.BuildTx( context.Background(), &keyring, - &txFeesClient, encodingConfig, tc.account, tc.chainID, @@ -167,69 +174,82 @@ func TestBuildTx(t *testing.T) { func TestPriceMsgs(t *testing.T) { testCases := []struct { name string - setupMocks func(calculator mocks.GetCalculateGasMock, txFeesClient *mocks.TxFeesQueryClient, keyring *mocks.Keyring) tx.CalculateGasFn + setupMocks func(calculator mocks.GetCalculateGasMock, keyring *mocks.Keyring) tx.CalculateGasFn account *authtypes.BaseAccount chainID string msgs []sdk.Msg + preSetBaseFee domain.BaseFee expectedGas uint64 expectedFeeCoin sdk.Coin + expectedBaseFee osmomath.Dec expectedError bool }{ { name: "Valid transaction", - setupMocks: func(calculator mocks.GetCalculateGasMock, txFeesClient *mocks.TxFeesQueryClient, keyring *mocks.Keyring) tx.CalculateGasFn { + setupMocks: func(calculator mocks.GetCalculateGasMock, keyring *mocks.Keyring) tx.CalculateGasFn { keyring.WithGetKey(testKey) - txFeesClient.WithBaseDenom(testDenom, nil) - txFeesClient.WithGetEipBaseFee(testBaseFee, nil) return calculator(&txtypes.SimulateResponse{GasInfo: &sdk.GasInfo{GasUsed: 100000}}, testGasUsed, nil) }, - account: testAccount, - chainID: testChainID, - msgs: []sdk.Msg{testMsg}, + account: testAccount, + chainID: testChainID, + msgs: []sdk.Msg{testMsg}, + preSetBaseFee: domain.BaseFee{ + Denom: testDenom, + CurrentFee: testBaseFeeDec, + }, expectedGas: testGasUsed, expectedFeeCoin: sdk.Coin{Denom: testDenom, Amount: osmomath.NewInt(testAmount)}, + expectedBaseFee: testBaseFeeDec, expectedError: false, }, { name: "Error building transaction", - setupMocks: func(calculator mocks.GetCalculateGasMock, txFeesClient *mocks.TxFeesQueryClient, keyring *mocks.Keyring) tx.CalculateGasFn { + setupMocks: func(calculator mocks.GetCalculateGasMock, keyring *mocks.Keyring) tx.CalculateGasFn { keyring.WithGetKey(testKey) + return calculator(&txtypes.SimulateResponse{}, testGasUsed, assert.AnError) }, + preSetBaseFee: domain.BaseFee{ + Denom: testDenom, + CurrentFee: testBaseFeeDec, + }, account: &authtypes.BaseAccount{ Sequence: 8, AccountNumber: 51, }, - expectedError: true, + expectedFeeCoin: sdk.Coin{}, + expectedBaseFee: osmomath.Dec{}, + expectedError: true, }, { - name: "Error calculating fee coin", - setupMocks: func(calculator mocks.GetCalculateGasMock, txFeesClient *mocks.TxFeesQueryClient, keyring *mocks.Keyring) tx.CalculateGasFn { + name: "Invalid base fee", + setupMocks: func(calculator mocks.GetCalculateGasMock, keyring *mocks.Keyring) tx.CalculateGasFn { keyring.WithGetKey(testKey) - txFeesClient.WithBaseDenom(testDenom, assert.AnError) return calculator(&txtypes.SimulateResponse{GasInfo: &sdk.GasInfo{GasUsed: 100000}}, testGasUsed, nil) }, - account: testAccount, - chainID: testChainID, - msgs: []sdk.Msg{testMsg}, - expectedGas: testGasUsed, - expectedError: true, + account: testAccount, + chainID: testChainID, + msgs: []sdk.Msg{testMsg}, + expectedGas: testGasUsed, + expectedFeeCoin: sdk.Coin{}, + expectedBaseFee: osmomath.Dec{}, + expectedError: true, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - txFeesClient := mocks.TxFeesQueryClient{} keyring := mocks.Keyring{} + routerRepository := routerrepo.New(&log.NoOpLogger{}) + routerRepository.SetBaseFee(tc.preSetBaseFee) - calculateGasFnMock := tc.setupMocks(mocks.DefaultGetCalculateGasMock, &txFeesClient, &keyring) - msgSimulator := tx.NewGasCalculator(nil, calculateGasFnMock) + calculateGasFnMock := tc.setupMocks(mocks.DefaultGetCalculateGasMock, &keyring) + msgSimulator := tx.NewMsgSimulator(nil, calculateGasFnMock, routerRepository) - gasUsed, feeCoin, err := msgSimulator.PriceMsgs( + priceInfo := msgSimulator.PriceMsgs( context.Background(), - &txFeesClient, encodingConfig.TxConfig, tc.account, tc.chainID, @@ -237,13 +257,14 @@ func TestPriceMsgs(t *testing.T) { ) if tc.expectedError { - assert.Error(t, err) - assert.Equal(t, uint64(0), gasUsed) - assert.Equal(t, sdk.Coin{}, feeCoin) + assert.NotEmpty(t, priceInfo.Err) + assert.Equal(t, priceInfo.AdjustedGasUsed, uint64(0)) + assert.Equal(t, priceInfo.FeeCoin, sdk.Coin{}) } else { - assert.NoError(t, err) - assert.Equal(t, tc.expectedGas, gasUsed) - assert.Equal(t, tc.expectedFeeCoin, feeCoin) + assert.Empty(t, priceInfo.Err) + assert.Equal(t, tc.expectedGas, priceInfo.AdjustedGasUsed) + assert.Equal(t, tc.expectedFeeCoin, priceInfo.FeeCoin) + assert.Equal(t, tc.expectedBaseFee, priceInfo.BaseFee) } }) } diff --git a/domain/cosmos/tx/tx.go b/domain/cosmos/tx/tx.go index aad0f4760..d83d00398 100644 --- a/domain/cosmos/tx/tx.go +++ b/domain/cosmos/tx/tx.go @@ -6,6 +6,7 @@ import ( "github.com/osmosis-labs/osmosis/osmomath" txfeestypes "github.com/osmosis-labs/osmosis/v26/x/txfees/types" + "github.com/osmosis-labs/sqs/domain" cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" sdk "github.com/cosmos/cosmos-sdk/types" @@ -54,22 +55,23 @@ func BuildSignerData(chainID string, accountNumber, sequence uint64) authsigning } } -// CalculateFeeCoin determines the appropriate fee coin for a transaction based on the current base fee +// CalculateFeePrice determines the appropriate fee price 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, client txfeestypes.QueryClient, gas uint64) (sdk.Coin, error) { +func CalculateFeePrice(ctx context.Context, client txfeestypes.QueryClient) (domain.BaseFee, error) { queryBaseDenomResponse, err := client.BaseDenom(ctx, &txfeestypes.QueryBaseDenomRequest{}) if err != nil { - return sdk.Coin{}, err + return domain.BaseFee{}, err } queryEipBaseFeeResponse, err := client.GetEipBaseFee(ctx, &txfeestypes.QueryEipBaseFeeRequest{}) if err != nil { - return sdk.Coin{}, err + return domain.BaseFee{}, err } - feeAmount := CalculateFeeAmount(queryEipBaseFeeResponse.BaseFee, gas) - - return sdk.NewCoin(queryBaseDenomResponse.BaseDenom, feeAmount), nil + return domain.BaseFee{ + Denom: queryBaseDenomResponse.BaseDenom, + CurrentFee: queryEipBaseFeeResponse.BaseFee, + }, nil } // CalculateFeeAmount calculates the fee amount based on the base fee and gas used. diff --git a/domain/cosmos/tx/tx_test.go b/domain/cosmos/tx/tx_test.go index 615b970a6..d3390ed70 100644 --- a/domain/cosmos/tx/tx_test.go +++ b/domain/cosmos/tx/tx_test.go @@ -12,7 +12,6 @@ import ( "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" @@ -189,13 +188,14 @@ func TestBuildSignerData(t *testing.T) { func TestCalculateFeeCoin(t *testing.T) { tests := []struct { - name string - gas uint64 - txFeesClient mocks.TxFeesQueryClient - setupMocks func(*mocks.TxFeesQueryClient) - expectedCoin string - expectedAmount osmomath.Int - expectError bool + name string + gas uint64 + txFeesClient mocks.TxFeesQueryClient + setupMocks func(*mocks.TxFeesQueryClient) + expectedCoin string + expectedAmount osmomath.Int + expectedBaseFee osmomath.Dec + expectError bool }{ { name: "Normal case", @@ -204,9 +204,10 @@ func TestCalculateFeeCoin(t *testing.T) { client.WithBaseDenom("uosmo", nil) client.WithGetEipBaseFee("0.5", nil) }, - expectedCoin: "uosmo", - expectedAmount: osmomath.NewInt(50000), - expectError: false, + expectedCoin: "uosmo", + expectedAmount: osmomath.NewInt(50000), + expectedBaseFee: osmomath.NewDecWithPrec(5, 1), + expectError: false, }, { name: "Error getting base denom", @@ -230,13 +231,14 @@ func TestCalculateFeeCoin(t *testing.T) { t.Run(tt.name, func(t *testing.T) { tt.setupMocks(&tt.txFeesClient) - result, err := sqstx.CalculateFeeCoin(context.TODO(), &tt.txFeesClient, tt.gas) + baseFee, err := sqstx.CalculateFeePrice(context.TODO(), &tt.txFeesClient) if tt.expectError { assert.Error(t, err) } else { assert.NoError(t, err) - assert.Equal(t, types.NewCoin(tt.expectedCoin, tt.expectedAmount), result) + assert.Equal(t, tt.expectedCoin, baseFee.Denom) + assert.Equal(t, tt.expectedBaseFee, baseFee.CurrentFee) } }) } diff --git a/domain/mocks/msg_simulator_mock.go b/domain/mocks/msg_simulator_mock.go index 86c014d5f..80ac1c6de 100644 --- a/domain/mocks/msg_simulator_mock.go +++ b/domain/mocks/msg_simulator_mock.go @@ -8,7 +8,7 @@ import ( txtypes "github.com/cosmos/cosmos-sdk/types/tx" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" "github.com/osmosis-labs/osmosis/v26/app/params" - txfeestypes "github.com/osmosis-labs/osmosis/v26/x/txfees/types" + "github.com/osmosis-labs/sqs/domain" sqstx "github.com/osmosis-labs/sqs/domain/cosmos/tx" "github.com/osmosis-labs/sqs/domain/keyring" ) @@ -17,7 +17,6 @@ type MsgSimulatorMock struct { BuildTxFn func( ctx context.Context, keyring keyring.Keyring, - txfeesClient txfeestypes.QueryClient, encodingConfig params.EncodingConfig, account *authtypes.BaseAccount, chainID string, @@ -33,26 +32,24 @@ type MsgSimulatorMock struct { PriceMsgsFn func( ctx context.Context, - txfeesClient txfeestypes.QueryClient, encodingConfig client.TxConfig, account *authtypes.BaseAccount, chainID string, msg ...sdk.Msg, - ) (uint64, sdk.Coin, error) + ) domain.TxFeeInfo } var _ sqstx.MsgSimulator = &MsgSimulatorMock{} func (m *MsgSimulatorMock) BuildTx(ctx context.Context, keyring keyring.Keyring, - txfeesClient txfeestypes.QueryClient, encodingConfig params.EncodingConfig, account *authtypes.BaseAccount, chainID string, msg ...sdk.Msg, ) (client.TxBuilder, error) { if m.BuildTxFn != nil { - return m.BuildTxFn(ctx, keyring, txfeesClient, encodingConfig, account, chainID, msg...) + return m.BuildTxFn(ctx, keyring, encodingConfig, account, chainID, msg...) } panic("BuildTxFn not implemented") } @@ -70,13 +67,13 @@ func (m *MsgSimulatorMock) SimulateMsgs( } // PriceMsgs implements tx.MsgSimulator. -func (m *MsgSimulatorMock) PriceMsgs(ctx context.Context, txfeesClient txfeestypes.QueryClient, encodingConfig client.TxConfig, account *authtypes.BaseAccount, chainID string, msg ...interface { +func (m *MsgSimulatorMock) PriceMsgs(ctx context.Context, encodingConfig client.TxConfig, account *authtypes.BaseAccount, chainID string, msg ...interface { ProtoMessage() Reset() String() string -}) (uint64, sdk.Coin, error) { +}) domain.TxFeeInfo { if m.PriceMsgsFn != nil { - return m.PriceMsgsFn(ctx, txfeesClient, encodingConfig, account, chainID, msg...) + return m.PriceMsgsFn(ctx, encodingConfig, account, chainID, msg...) } panic("PriceMsgsFn not implemented") } diff --git a/domain/mocks/quote_mock.go b/domain/mocks/quote_mock.go index 4c682d271..f60600198 100644 --- a/domain/mocks/quote_mock.go +++ b/domain/mocks/quote_mock.go @@ -63,7 +63,7 @@ func (m *MockQuote) PrepareResult(ctx context.Context, scalingFactor math.Legacy } // SetQuotePriceInfo implements domain.Quote. -func (m *MockQuote) SetQuotePriceInfo(info *domain.QuotePriceInfo) { +func (m *MockQuote) SetQuotePriceInfo(info *domain.TxFeeInfo) { panic("unimplemented") } diff --git a/domain/mocks/quote_simulator_mock.go b/domain/mocks/quote_simulator_mock.go index 355e0499b..5b6c2bed6 100644 --- a/domain/mocks/quote_simulator_mock.go +++ b/domain/mocks/quote_simulator_mock.go @@ -4,16 +4,15 @@ import ( "context" "cosmossdk.io/math" - "github.com/cosmos/cosmos-sdk/types" "github.com/osmosis-labs/sqs/domain" ) type QuoteSimulatorMock struct { - SimulateQuoteFn func(ctx context.Context, quote domain.Quote, slippageToleranceMultiplier math.LegacyDec, simulatorAddress string) (uint64, types.Coin, error) + SimulateQuoteFn func(ctx context.Context, quote domain.Quote, slippageToleranceMultiplier math.LegacyDec, simulatorAddress string) domain.TxFeeInfo } // SimulateQuote implements domain.QuoteSimulator. -func (q *QuoteSimulatorMock) SimulateQuote(ctx context.Context, quote domain.Quote, slippageToleranceMultiplier math.LegacyDec, simulatorAddress string) (uint64, types.Coin, error) { +func (q *QuoteSimulatorMock) SimulateQuote(ctx context.Context, quote domain.Quote, slippageToleranceMultiplier math.LegacyDec, simulatorAddress string) domain.TxFeeInfo { if q.SimulateQuoteFn != nil { return q.SimulateQuoteFn(ctx, quote, slippageToleranceMultiplier, simulatorAddress) } diff --git a/domain/mocks/router_usecase_mock.go b/domain/mocks/router_usecase_mock.go index 6a5bff635..683b7a412 100644 --- a/domain/mocks/router_usecase_mock.go +++ b/domain/mocks/router_usecase_mock.go @@ -35,6 +35,13 @@ type RouterUsecaseMock struct { ConvertMinTokensPoolLiquidityCapToFilterFunc func(minTokensPoolLiquidityCap uint64) uint64 SetSortedPoolsFunc func(pools []sqsdomain.PoolI) GetMinPoolLiquidityCapFilterFunc func(tokenInDenom string, tokenOutDenom string) (uint64, error) + + BaseFee domain.BaseFee +} + +// GetBaseFee implements mvc.RouterUsecase. +func (m *RouterUsecaseMock) GetBaseFee() domain.BaseFee { + return m.BaseFee } // GetMinPoolLiquidityCapFilter implements mvc.RouterUsecase. diff --git a/domain/mvc/router.go b/domain/mvc/router.go index d826fdac3..fcafec388 100644 --- a/domain/mvc/router.go +++ b/domain/mvc/router.go @@ -40,6 +40,8 @@ type RouterRepository interface { SetTakerFee(denom0, denom1 string, takerFee osmomath.Dec) // SetTakerFees sets taker fees on router repository SetTakerFees(takerFees sqsdomain.TakerFeeMap) + + GetBaseFee() domain.BaseFee } // SimpleRouterUsecase represent the simple router's usecases @@ -75,6 +77,9 @@ type RouterUsecase interface { GetCustomDirectQuoteMultiPoolInGivenOut(ctx context.Context, tokenOut sdk.Coin, tokenInDenom []string, poolIDs []uint64) (domain.Quote, error) // GetCandidateRoutes returns the candidate routes for the given tokenIn and tokenOutDenom. GetCandidateRoutes(ctx context.Context, tokenIn sdk.Coin, tokenOutDenom string) (sqsdomain.CandidateRoutes, error) + + GetBaseFee() domain.BaseFee + // GetTakerFee returns the taker fee for all token pairs in a pool. GetTakerFee(poolID uint64) ([]sqsdomain.TakerFeeForPair, error) // SetTakerFees sets the taker fees for all token pairs in all pools. diff --git a/domain/quote_simulator.go b/domain/quote_simulator.go index ce116cb34..a79f4fba6 100644 --- a/domain/quote_simulator.go +++ b/domain/quote_simulator.go @@ -3,7 +3,6 @@ package domain import ( "context" - sdk "github.com/cosmos/cosmos-sdk/types" "github.com/osmosis-labs/osmosis/osmomath" ) @@ -14,10 +13,5 @@ type QuoteSimulator interface { // - Only direct (non-split) quotes are supported. // Retursn error if: // - Simulator address does not have enough funds to pay for the quote. - SimulateQuote(ctx context.Context, quote Quote, slippageToleranceMultiplier osmomath.Dec, simulatorAddress string) (uint64, sdk.Coin, error) -} - -type QuotePriceInfo struct { - AdjustedGasUsed uint64 `json:"adjusted_gas_used"` - FeeCoin sdk.Coin `json:"fee_coin"` + SimulateQuote(ctx context.Context, quote Quote, slippageToleranceMultiplier osmomath.Dec, simulatorAddress string) TxFeeInfo } diff --git a/domain/router.go b/domain/router.go index 748718cf9..a4780372d 100644 --- a/domain/router.go +++ b/domain/router.go @@ -73,7 +73,7 @@ type Quote interface { PrepareResult(ctx context.Context, scalingFactor osmomath.Dec, logger log.Logger) ([]SplitRoute, osmomath.Dec, error) // SetQuotePriceInfo sets the quote price info. - SetQuotePriceInfo(info *QuotePriceInfo) + SetQuotePriceInfo(info *TxFeeInfo) String() string } diff --git a/domain/tx_fee_info.go b/domain/tx_fee_info.go new file mode 100644 index 000000000..aa62d50d8 --- /dev/null +++ b/domain/tx_fee_info.go @@ -0,0 +1,14 @@ +package domain + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/osmosis-labs/osmosis/osmomath" +) + +// TxFeeInfo represents the fee information for a transaction +type TxFeeInfo struct { + AdjustedGasUsed uint64 `json:"adjusted_gas_used,omitempty"` + FeeCoin sdk.Coin `json:"fee_coin,omitempty"` + BaseFee osmomath.Dec `json:"base_fee"` + Err string `json:"error,omitempty"` +} diff --git a/ingest/usecase/plugins/basefee/base_fee_end_block_update_plugin.go b/ingest/usecase/plugins/basefee/base_fee_end_block_update_plugin.go new file mode 100644 index 000000000..1c6556661 --- /dev/null +++ b/ingest/usecase/plugins/basefee/base_fee_end_block_update_plugin.go @@ -0,0 +1,40 @@ +package basefee + +import ( + "context" + + txfeestypes "github.com/osmosis-labs/osmosis/v26/x/txfees/types" + "github.com/osmosis-labs/sqs/domain" + "github.com/osmosis-labs/sqs/domain/cosmos/tx" + "github.com/osmosis-labs/sqs/log" + routerrepo "github.com/osmosis-labs/sqs/router/repository" + "go.uber.org/zap" +) + +type baseFeeEndBlockUpdatePlugin struct { + routerRepository routerrepo.RouterRepository + txfeesClient txfeestypes.QueryClient + logger log.Logger +} + +func NewEndBlockUpdatePlugin(routerRepository routerrepo.RouterRepository, txfeesClient txfeestypes.QueryClient, logger log.Logger) *baseFeeEndBlockUpdatePlugin { + return &baseFeeEndBlockUpdatePlugin{ + routerRepository: routerRepository, + txfeesClient: txfeesClient, + logger: logger, + } +} + +// ProcessEndBlock calculates the base fee for the current block and updates the router repository with the new base fee. +func (p *baseFeeEndBlockUpdatePlugin) ProcessEndBlock(ctx context.Context, blockHeight uint64, metadata domain.BlockPoolMetadata) error { + baseFee, err := tx.CalculateFeePrice(ctx, p.txfeesClient) + if err != nil { + p.logger.Error("failed to calculate fee price", zap.Error(err)) + } + + p.routerRepository.SetBaseFee(baseFee) + + return nil +} + +var _ domain.EndBlockProcessPlugin = &baseFeeEndBlockUpdatePlugin{} diff --git a/ingest/usecase/plugins/orderbook/claimbot/config.go b/ingest/usecase/plugins/orderbook/claimbot/config.go index 3046cbc0a..b2c1f1358 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/config.go +++ b/ingest/usecase/plugins/orderbook/claimbot/config.go @@ -45,8 +45,7 @@ func NewConfig( PoolsUseCase: poolsUseCase, OrderbookUsecase: orderbookusecase, AccountQueryClient: authtypes.NewQueryClient(grpcClient), - TxfeesClient: txfeestypes.NewQueryClient(grpcClient), - MsgSimulator: sqstx.NewGasCalculator(grpcClient, sqstx.CalculateGas), + MsgSimulator: sqstx.NewMsgSimulator(grpcClient, sqstx.CalculateGas, nil), TxServiceClient: txtypes.NewServiceClient(grpcClient), Logger: logger.Named("claimbot"), ChainID: chainID, diff --git a/ingest/usecase/plugins/orderbook/claimbot/export_test.go b/ingest/usecase/plugins/orderbook/claimbot/export_test.go index 3a117ac59..af52a2dac 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/export_test.go +++ b/ingest/usecase/plugins/orderbook/claimbot/export_test.go @@ -53,7 +53,7 @@ func SendBatchClaimTxInternal( claims orderbookdomain.Orders, getEncodingConfig func() params.EncodingConfig, ) (*sdk.TxResponse, error) { - return sendBatchClaimTxInternal(ctx, keyring, txfeesClient, msgSimulator, txServiceClient, chainID, account, contractAddress, claims, getEncodingConfig) + return sendBatchClaimTxInternal(ctx, keyring, msgSimulator, txServiceClient, chainID, account, contractAddress, claims, getEncodingConfig) } // 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 625cbad99..581b5ee60 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/plugin.go +++ b/ingest/usecase/plugins/orderbook/claimbot/plugin.go @@ -172,7 +172,6 @@ func (o *claimbot) processOrderbookOrders(ctx context.Context, account *authtype txres, err := sendBatchClaimTx( ctx, o.config.Keyring, - o.config.TxfeesClient, o.config.MsgSimulator, o.config.TxServiceClient, o.config.ChainID, diff --git a/ingest/usecase/plugins/orderbook/claimbot/tx.go b/ingest/usecase/plugins/orderbook/claimbot/tx.go index 2ab6eb494..543ec903f 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/tx.go +++ b/ingest/usecase/plugins/orderbook/claimbot/tx.go @@ -15,7 +15,6 @@ import ( authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" "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" ) var ( @@ -32,7 +31,6 @@ var ( func sendBatchClaimTx( ctx context.Context, keyring keyring.Keyring, - txfeesClient txfeestypes.QueryClient, msgSimulator sqstx.MsgSimulator, txServiceClient txtypes.ServiceClient, chainID string, @@ -40,7 +38,7 @@ func sendBatchClaimTx( contractAddress string, claims orderbookdomain.Orders, ) (*sdk.TxResponse, error) { - return sendBatchClaimTxInternal(ctx, keyring, txfeesClient, msgSimulator, txServiceClient, chainID, account, contractAddress, claims, defaultEncodingConfigFn) + return sendBatchClaimTxInternal(ctx, keyring, msgSimulator, txServiceClient, chainID, account, contractAddress, claims, defaultEncodingConfigFn) } // sendBatchClaimTxInternal is a helper function that prepares and sends a batch claim transaction to the blockchain. @@ -48,7 +46,6 @@ func sendBatchClaimTx( func sendBatchClaimTxInternal( ctx context.Context, keyring keyring.Keyring, - txfeesClient txfeestypes.QueryClient, msgSimulator sqstx.MsgSimulator, txServiceClient txtypes.ServiceClient, chainID string, @@ -68,7 +65,7 @@ func sendBatchClaimTxInternal( msg := buildExecuteContractMsg(address, contractAddress, msgBytes) - tx, err := msgSimulator.BuildTx(ctx, keyring, txfeesClient, encodingConfig, account, chainID, msg) + tx, err := msgSimulator.BuildTx(ctx, keyring, encodingConfig, account, chainID, msg) if err != nil { return nil, fmt.Errorf("failed to build transaction: %w", err) } diff --git a/ingest/usecase/plugins/orderbook/claimbot/tx_test.go b/ingest/usecase/plugins/orderbook/claimbot/tx_test.go index 75031f928..6929d9c40 100644 --- a/ingest/usecase/plugins/orderbook/claimbot/tx_test.go +++ b/ingest/usecase/plugins/orderbook/claimbot/tx_test.go @@ -12,7 +12,6 @@ import ( authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" "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" "github.com/osmosis-labs/sqs/domain/keyring" "github.com/osmosis-labs/sqs/domain/mocks" orderbookdomain "github.com/osmosis-labs/sqs/domain/orderbook" @@ -54,7 +53,6 @@ func TestSendBatchClaimTx(t *testing.T) { msgSimulator.BuildTxFn = func( ctx context.Context, keyring keyring.Keyring, - txfeesClient txfeestypes.QueryClient, encodingConfig params.EncodingConfig, account *authtypes.BaseAccount, chainID string, @@ -79,7 +77,6 @@ func TestSendBatchClaimTx(t *testing.T) { msgSimulator.BuildTxFn = func( ctx context.Context, keyring keyring.Keyring, - txfeesClient txfeestypes.QueryClient, encodingConfig params.EncodingConfig, account *authtypes.BaseAccount, chainID string, @@ -117,7 +114,6 @@ func TestSendBatchClaimTx(t *testing.T) { msgSimulator.BuildTxFn = func( ctx context.Context, keyring keyring.Keyring, - txfeesClient txfeestypes.QueryClient, encodingConfig params.EncodingConfig, account *authtypes.BaseAccount, chainID string, diff --git a/quotesimulator/quote_simulator.go b/quotesimulator/quote_simulator.go index c36c33087..5141f5d7f 100644 --- a/quotesimulator/quote_simulator.go +++ b/quotesimulator/quote_simulator.go @@ -4,41 +4,36 @@ import ( "context" "fmt" - sdk "github.com/cosmos/cosmos-sdk/types" "github.com/osmosis-labs/osmosis/osmomath" "github.com/osmosis-labs/osmosis/v26/app/params" - txfeestypes "github.com/osmosis-labs/osmosis/v26/x/txfees/types" + poolmanagertypes "github.com/osmosis-labs/osmosis/v26/x/poolmanager/types" "github.com/osmosis-labs/sqs/domain" "github.com/osmosis-labs/sqs/domain/cosmos/auth/types" "github.com/osmosis-labs/sqs/domain/cosmos/tx" - - poolmanagertypes "github.com/osmosis-labs/osmosis/v26/x/poolmanager/types" ) // quoteSimulator simulates a quote and returns the gas adjusted amount and the fee coin. type quoteSimulator struct { msgSimulator tx.MsgSimulator encodingConfig params.EncodingConfig - txFeesClient txfeestypes.QueryClient accountQueryClient types.QueryClient chainID string } -func NewQuoteSimulator(msgSimulator tx.MsgSimulator, encodingConfig params.EncodingConfig, txFeesClient txfeestypes.QueryClient, accountQueryClient types.QueryClient, chainID string) *quoteSimulator { +func NewQuoteSimulator(msgSimulator tx.MsgSimulator, encodingConfig params.EncodingConfig, accountQueryClient types.QueryClient, chainID string) *quoteSimulator { return "eSimulator{ msgSimulator: msgSimulator, encodingConfig: encodingConfig, - txFeesClient: txFeesClient, accountQueryClient: accountQueryClient, chainID: chainID, } } // SimulateQuote implements domain.QuoteSimulator -func (q *quoteSimulator) SimulateQuote(ctx context.Context, quote domain.Quote, slippageToleranceMultiplier osmomath.Dec, simulatorAddress string) (uint64, sdk.Coin, error) { +func (q *quoteSimulator) SimulateQuote(ctx context.Context, quote domain.Quote, slippageToleranceMultiplier osmomath.Dec, simulatorAddress string) domain.TxFeeInfo { route := quote.GetRoute() if len(route) != 1 { - return 0, sdk.Coin{}, fmt.Errorf("route length must be 1, got %d", len(route)) + return domain.TxFeeInfo{Err: fmt.Sprintf("route length must be 1, got %d", len(route))} } poolsInRoute := route[0].GetPools() @@ -67,16 +62,10 @@ func (q *quoteSimulator) SimulateQuote(ctx context.Context, quote domain.Quote, // Get the account for the simulator address baseAccount, err := q.accountQueryClient.GetAccount(ctx, simulatorAddress) if err != nil { - return 0, sdk.Coin{}, err - } - - // Price the message - gasAdjusted, feeCoin, err := q.msgSimulator.PriceMsgs(ctx, q.txFeesClient, q.encodingConfig.TxConfig, baseAccount, q.chainID, swapMsg) - if err != nil { - return 0, sdk.Coin{}, err + return domain.TxFeeInfo{Err: err.Error()} } - return gasAdjusted, feeCoin, nil + return q.msgSimulator.PriceMsgs(ctx, q.encodingConfig.TxConfig, baseAccount, q.chainID, swapMsg) } var _ domain.QuoteSimulator = "eSimulator{} diff --git a/quotesimulator/quote_simulator_test.go b/quotesimulator/quote_simulator_test.go index 88935bd67..96ce9a07d 100644 --- a/quotesimulator/quote_simulator_test.go +++ b/quotesimulator/quote_simulator_test.go @@ -12,7 +12,6 @@ import ( authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" "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/osmosis-labs/sqs/domain" "github.com/osmosis-labs/sqs/domain/mocks" ) @@ -32,6 +31,7 @@ func TestSimulateQuote(t *testing.T) { simulatorAddress string expectedGasAdjusted uint64 expectedFeeCoin sdk.Coin + expectedBaseFee osmomath.Dec expectError bool expectedErrorMsg string }{ @@ -41,6 +41,7 @@ func TestSimulateQuote(t *testing.T) { simulatorAddress: "osmo13t8prr8hu7hkuksnfrd25vpvvnrfxr223k59ph", expectedGasAdjusted: 100000, expectedFeeCoin: sdk.NewCoin("uosmo", osmomath.NewInt(10000)), + expectedBaseFee: osmomath.NewDecWithPrec(5, 1), expectError: false, }, } @@ -82,16 +83,18 @@ func TestSimulateQuote(t *testing.T) { msgSimulator := &mocks.MsgSimulatorMock{ PriceMsgsFn: func( ctx context.Context, - txfeesClient txfeestypes.QueryClient, encodingConfig client.TxConfig, account *authtypes.BaseAccount, chainID string, msg ...sdk.Msg, - ) (uint64, sdk.Coin, error) { - return tt.expectedGasAdjusted, tt.expectedFeeCoin, nil + ) domain.TxFeeInfo { + return domain.TxFeeInfo{ + AdjustedGasUsed: tt.expectedGasAdjusted, + FeeCoin: tt.expectedFeeCoin, + BaseFee: osmomath.NewDecWithPrec(5, 1), + } }, } - txFeesClient := &mocks.TxFeesQueryClient{} accountQueryClient := &mocks.AuthQueryClientMock{ GetAccountFunc: func(ctx context.Context, address string) (*authtypes.BaseAccount, error) { return &authtypes.BaseAccount{ @@ -104,13 +107,12 @@ func TestSimulateQuote(t *testing.T) { simulator := NewQuoteSimulator( msgSimulator, params.EncodingConfig{}, - txFeesClient, accountQueryClient, "osmosis-1", ) // System under test - gasAdjusted, feeCoin, err := simulator.SimulateQuote( + priceInfo := simulator.SimulateQuote( context.Background(), mockQuote, tt.slippageToleranceMultiplier, @@ -119,12 +121,13 @@ func TestSimulateQuote(t *testing.T) { // Assert results if tt.expectError { - assert.Error(t, err) - assert.Contains(t, err.Error(), tt.expectedErrorMsg) + assert.NotEmpty(t, priceInfo.Err) + assert.Contains(t, priceInfo.Err, tt.expectedErrorMsg) } else { - assert.NoError(t, err) - assert.Equal(t, tt.expectedGasAdjusted, gasAdjusted) - assert.Equal(t, tt.expectedFeeCoin, feeCoin) + assert.Empty(t, priceInfo.Err) + assert.Equal(t, tt.expectedGasAdjusted, priceInfo.AdjustedGasUsed) + assert.Equal(t, tt.expectedFeeCoin, priceInfo.FeeCoin) + assert.Equal(t, tt.expectedBaseFee, priceInfo.BaseFee) } }) } diff --git a/router/delivery/http/router_handler.go b/router/delivery/http/router_handler.go index 702936b28..9ef2dbc33 100644 --- a/router/delivery/http/router_handler.go +++ b/router/delivery/http/router_handler.go @@ -73,6 +73,7 @@ func NewRouterHandler(e *echo.Echo, us mvc.RouterUsecase, tu mvc.TokensUsecase, // @Param applyExponents query bool false "Boolean flag indicating whether to apply exponents to the spot price. False by default." // @Param simulatorAddress query string false "Address of the simulator to simulate the quote. If provided, the quote will be simulated." // @Param simulationSlippageTolerance query string false "Slippage tolerance multiplier for the simulation. If simulatorAddress is provided, this must be provided." +// @Param appendBaseFee query bool false "Boolean flag indicating whether to append the base fee to the quote. False by default." // @Success 200 {object} domain.Quote "The computed best route quote" // @Router /router/quote [get] func (a *RouterHandler) GetOptimalQuote(c echo.Context) (err error) { @@ -159,15 +160,15 @@ func (a *RouterHandler) GetOptimalQuote(c echo.Context) (err error) { // Only "out given in" swap method is supported for simulation. Thus, we also check for tokenOutDenom being set. simulatorAddress := req.SimulatorAddress if req.SingleRoute && simulatorAddress != "" && req.SwapMethod() == domain.TokenSwapMethodExactIn { - gasUsed, feeCoin, err := a.QuoteSimulator.SimulateQuote(ctx, quote, req.SlippageToleranceMultiplier, simulatorAddress) - if err != nil { - return c.JSON(domain.GetStatusCode(err), domain.ResponseError{Message: err.Error()}) - } + priceInfo := a.QuoteSimulator.SimulateQuote(ctx, quote, req.SlippageToleranceMultiplier, simulatorAddress) // Set the quote price info. - quote.SetQuotePriceInfo(&domain.QuotePriceInfo{ - AdjustedGasUsed: gasUsed, - FeeCoin: feeCoin, + quote.SetQuotePriceInfo(&priceInfo) + } + + if req.AppendBaseFee { + quote.SetQuotePriceInfo(&domain.TxFeeInfo{ + BaseFee: a.RUsecase.GetBaseFee().CurrentFee, }) } diff --git a/router/delivery/http/router_handler_test.go b/router/delivery/http/router_handler_test.go index 578d977fd..23d2784ca 100644 --- a/router/delivery/http/router_handler_test.go +++ b/router/delivery/http/router_handler_test.go @@ -10,6 +10,7 @@ import ( "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/labstack/echo/v4" + "github.com/osmosis-labs/osmosis/osmomath" "github.com/osmosis-labs/sqs/domain" "github.com/osmosis-labs/sqs/domain/mocks" routerdelivery "github.com/osmosis-labs/sqs/router/delivery/http" @@ -91,14 +92,47 @@ func (s *RouterHandlerSuite) TestGetOptimalQuote() { }, }, QuoteSimulator: &mocks.QuoteSimulatorMock{ - SimulateQuoteFn: func(ctx context.Context, quote domain.Quote, slippageToleranceMultiplier math.LegacyDec, simulatorAddress string) (uint64, sdk.Coin, error) { - return 1_000_000, sdk.NewCoin("uosmo", math.NewInt(1000)), nil + SimulateQuoteFn: func(ctx context.Context, quote domain.Quote, slippageToleranceMultiplier math.LegacyDec, simulatorAddress string) domain.TxFeeInfo { + return domain.TxFeeInfo{ + AdjustedGasUsed: 1_000_000, + FeeCoin: sdk.NewCoin("uosmo", math.NewInt(1000)), + BaseFee: osmomath.NewDecWithPrec(5, 1), + } }, }, }, expectedStatusCode: http.StatusOK, expectedResponse: s.MustReadFile("../../usecase/routertesting/parsing/quote_amount_in_response_simulated.json"), }, + { + name: "valid exact in request with base fee", + queryParams: map[string]string{ + "tokenIn": "1000ibc/EA1D43981D5C9A1C4AAEA9C23BB1D4FA126BA9BC7020A25E0AE4AA841EA25DC5", + "tokenOutDenom": "ibc/498A0751C798A0D9A389AA3691123DADA57DAA4FE165D5C75894505B876BA6E4", + "singleRoute": "true", + "applyExponents": "true", + "appendBaseFee": "true", + }, + handler: &routerdelivery.RouterHandler{ + TUsecase: &mocks.TokensUsecaseMock{ + IsValidChainDenomFunc: func(chainDenom string) bool { + return true + }, + }, + RUsecase: &mocks.RouterUsecaseMock{ + GetOptimalQuoteFunc: func(ctx context.Context, tokenIn sdk.Coin, tokenOutDenom string, opts ...domain.RouterOption) (domain.Quote, error) { + return s.NewExactAmountInQuote(poolOne, poolTwo, poolThree), nil + }, + + BaseFee: domain.BaseFee{ + Denom: "uosmo", + CurrentFee: osmomath.NewDecWithPrec(5, 1), + }, + }, + }, + expectedStatusCode: http.StatusOK, + expectedResponse: s.MustReadFile("../../usecase/routertesting/parsing/quote_amount_in_response_base_fee.json"), + }, { name: "valid exact out request", queryParams: map[string]string{ diff --git a/router/repository/memory_router_repository.go b/router/repository/memory_router_repository.go index 3b520f60b..0422c4b4e 100644 --- a/router/repository/memory_router_repository.go +++ b/router/repository/memory_router_repository.go @@ -13,8 +13,15 @@ import ( "github.com/osmosis-labs/osmosis/osmomath" ) +// BaseFeeRepository represents the contract for a repository handling base fee information +type BaseFeeRepository interface { + SetBaseFee(baseFee domain.BaseFee) + GetBaseFee() domain.BaseFee +} + // RouterRepository represents the contract for a repository handling router information type RouterRepository interface { + BaseFeeRepository mvc.CandidateRouteSearchDataHolder // GetTakerFee returns the taker fee for a given pair of denominations @@ -38,19 +45,37 @@ type routerRepo struct { takerFeeMap sync.Map candidateRouteSearchData sync.Map + baseFeeMx sync.RWMutex + baseFee domain.BaseFee + logger log.Logger } -// New creates a new repository for the router. func New(logger log.Logger) RouterRepository { return &routerRepo{ takerFeeMap: sync.Map{}, candidateRouteSearchData: sync.Map{}, + baseFeeMx: sync.RWMutex{}, + baseFee: domain.BaseFee{}, logger: logger, } } +// GetBaseFee implements RouterRepository. +func (r *routerRepo) GetBaseFee() domain.BaseFee { + r.baseFeeMx.RLock() + defer r.baseFeeMx.RUnlock() + return r.baseFee +} + +// SetBaseFee implements RouterRepository. +func (r *routerRepo) SetBaseFee(baseFee domain.BaseFee) { + r.baseFeeMx.Lock() + defer r.baseFeeMx.Unlock() + r.baseFee = baseFee +} + // GetAllTakerFees implements RouterRepository. func (r *routerRepo) GetAllTakerFees() sqsdomain.TakerFeeMap { takerFeeMap := sqsdomain.TakerFeeMap{} diff --git a/router/types/get_quote_request.go b/router/types/get_quote_request.go index 2894ce0e7..11bd32c6e 100644 --- a/router/types/get_quote_request.go +++ b/router/types/get_quote_request.go @@ -19,6 +19,7 @@ type GetQuoteRequest struct { SingleRoute bool SimulatorAddress string SlippageToleranceMultiplier osmomath.Dec + AppendBaseFee bool HumanDenoms bool ApplyExponents bool } @@ -67,6 +68,11 @@ func (r *GetQuoteRequest) UnmarshalHTTPRequest(c echo.Context) error { r.SimulatorAddress = simulatorAddress r.SlippageToleranceMultiplier = slippageToleranceDec + r.AppendBaseFee, err = domain.ParseBooleanQueryParam(c, "appendBaseFee") + if err != nil { + return err + } + return nil } diff --git a/router/usecase/quote_out_given_in.go b/router/usecase/quote_out_given_in.go index a3411c602..b4a89c6bb 100644 --- a/router/usecase/quote_out_given_in.go +++ b/router/usecase/quote_out_given_in.go @@ -36,13 +36,13 @@ func NewQuoteExactAmountOut(q *QuoteExactAmountIn) *quoteExactAmountOut { // quoteExactAmountIn is a quote implementation for token swap method exact in. type quoteExactAmountIn struct { - AmountIn sdk.Coin "json:\"amount_in\"" - AmountOut osmomath.Int "json:\"amount_out\"" - Route []domain.SplitRoute "json:\"route\"" - EffectiveFee osmomath.Dec "json:\"effective_fee\"" - PriceImpact osmomath.Dec "json:\"price_impact\"" - InBaseOutQuoteSpotPrice osmomath.Dec "json:\"in_base_out_quote_spot_price\"" - PriceInfo *domain.QuotePriceInfo `json:"price_info,omitempty"` + AmountIn sdk.Coin "json:\"amount_in\"" + AmountOut osmomath.Int "json:\"amount_out\"" + Route []domain.SplitRoute "json:\"route\"" + EffectiveFee osmomath.Dec "json:\"effective_fee\"" + PriceImpact osmomath.Dec "json:\"price_impact\"" + InBaseOutQuoteSpotPrice osmomath.Dec "json:\"in_base_out_quote_spot_price\"" + PriceInfo *domain.TxFeeInfo `json:"price_info,omitempty"` } // PrepareResult implements domain.Quote. @@ -154,6 +154,6 @@ func (q *quoteExactAmountIn) GetInBaseOutQuoteSpotPrice() osmomath.Dec { } // SetQuotePriceInfo implements domain.Quote. -func (q *quoteExactAmountIn) SetQuotePriceInfo(info *domain.QuotePriceInfo) { +func (q *quoteExactAmountIn) SetQuotePriceInfo(info *domain.TxFeeInfo) { q.PriceInfo = info } diff --git a/router/usecase/router_usecase.go b/router/usecase/router_usecase.go index 62315abfc..55db924fb 100644 --- a/router/usecase/router_usecase.go +++ b/router/usecase/router_usecase.go @@ -885,6 +885,11 @@ func (r *routerUseCaseImpl) GetPoolSpotPrice(ctx context.Context, poolID uint64, return spotPrice, nil } +// GetBaseFee implements mvc.RouterUsecase. +func (r *routerUseCaseImpl) GetBaseFee() domain.BaseFee { + return r.routerRepository.GetBaseFee() +} + // SetSortedPools implements mvc.RouterUsecase. func (r *routerUseCaseImpl) SetSortedPools(pools []sqsdomain.PoolI) { r.sortedPoolsMu.Lock() diff --git a/router/usecase/routertesting/parsing/quote_amount_in_response_base_fee.json b/router/usecase/routertesting/parsing/quote_amount_in_response_base_fee.json new file mode 100644 index 000000000..d0e944600 --- /dev/null +++ b/router/usecase/routertesting/parsing/quote_amount_in_response_base_fee.json @@ -0,0 +1,57 @@ +{ + "amount_in": { + "denom": "ibc/EA1D43981D5C9A1C4AAEA9C23BB1D4FA126BA9BC7020A25E0AE4AA841EA25DC5", + "amount": "10000000" + }, + "amount_out": "40000000", + "route": [ + { + "pools": [ + { + "id": 1, + "type": 0, + "balances": [], + "spread_factor": "0.010000000000000000", + "token_out_denom": "ibc/4ABBEF4C8926DDDB320AE5188CFD63267ABBCEFC0583E4AE05D6E5AA2401DDAB", + "taker_fee": "0.020000000000000000" + }, + { + "id": 2, + "type": 0, + "balances": [], + "spread_factor": "0.030000000000000000", + "token_out_denom": "ibc/498A0751C798A0D9A389AA3691123DADA57DAA4FE165D5C75894505B876BA6E4", + "taker_fee": "0.000400000000000000" + } + ], + "has-cw-pool": false, + "out_amount": "20000000", + "in_amount": "5000000" + }, + { + "pools": [ + { + "id": 3, + "type": 0, + "balances": [], + "spread_factor": "0.005000000000000000", + "token_out_denom": "ibc/498A0751C798A0D9A389AA3691123DADA57DAA4FE165D5C75894505B876BA6E4", + "taker_fee": "0.003000000000000000" + } + ], + "has-cw-pool": false, + "out_amount": "20000000", + "in_amount": "5000000" + } + ], + "effective_fee": "0.011696000000000000", + "price_impact": "-0.565353638051463862", + "in_base_out_quote_spot_price": "4.500000000000000000", + "price_info": { + "fee_coin": { + "amount": "0" + }, + "base_fee": "0.500000000000000000" + } + } + \ No newline at end of file diff --git a/router/usecase/routertesting/parsing/quote_amount_in_response_simulated.json b/router/usecase/routertesting/parsing/quote_amount_in_response_simulated.json index 43eae6696..e034fadc6 100644 --- a/router/usecase/routertesting/parsing/quote_amount_in_response_simulated.json +++ b/router/usecase/routertesting/parsing/quote_amount_in_response_simulated.json @@ -52,6 +52,7 @@ "fee_coin": { "denom": "uosmo", "amount": "1000" - } + }, + "base_fee": "0.500000000000000000" } }