Skip to content

Commit

Permalink
feat: simulate swap as part of quotes
Browse files Browse the repository at this point in the history
  • Loading branch information
p0mvn committed Nov 3, 2024
1 parent 662aac4 commit 10eca0f
Show file tree
Hide file tree
Showing 8 changed files with 250 additions and 27 deletions.
47 changes: 34 additions & 13 deletions domain/cosmos/tx/msg_simulator.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,17 @@ type MsgSimulator interface {
chainID string,
msgs []sdk.Msg,
) (*txtypes.SimulateResponse, uint64, error)

// PriceMsgs simulates the execution of the given messages and returns the gas used and the fee coin,
// 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)
}

// NewGasCalculator creates a new GasCalculator instance.
Expand Down Expand Up @@ -79,23 +90,13 @@ func (c *txGasCalulator) BuildTx(
return nil, err
}

_, gas, err := c.SimulateMsgs(
encodingConfig.TxConfig,
account,
chainID,
msg,
)
gasAdjusted, feecoin, err := c.PriceMsgs(ctx, txfeesClient, encodingConfig.TxConfig, account, chainID, msg...)
if err != nil {
return nil, err
}
txBuilder.SetGasLimit(gas)

feecoin, err := CalculateFeeCoin(ctx, txfeesClient, gas)
if err != nil {
return nil, err
}

txBuilder.SetFeeAmount(sdk.NewCoins(feecoin))
txBuilder.SetGasLimit(gasAdjusted)
txBuilder.SetFeeAmount(sdk.Coins{feecoin})

sigV2 := BuildSignatures(privKey.PubKey(), nil, account.Sequence)
err = txBuilder.SetSignatures(sigV2)
Expand Down Expand Up @@ -143,6 +144,26 @@ func (c *txGasCalulator) SimulateMsgs(encodingConfig cosmosclient.TxConfig, acco
return gasResult, adjustedGasUsed, nil
}

// 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) {
_, gasAdjusted, err := c.SimulateMsgs(
encodingConfig,
account,
chainID,
msg,
)
if err != nil {
return 0, sdk.Coin{}, err
}

feecoin, err := CalculateFeeCoin(ctx, txfeesClient, gasAdjusted)
if err != nil {
return 0, sdk.Coin{}, err
}

return gasAdjusted, feecoin, nil
}

// CalculateGas calculates the gas required for a transaction using the provided transaction factory and messages.
func CalculateGas(
clientCtx gogogrpc.ClientConn,
Expand Down
21 changes: 21 additions & 0 deletions domain/mocks/msg_simulator_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@ type MsgSimulatorMock struct {
chainID string,
msgs []sdk.Msg,
) (*txtypes.SimulateResponse, uint64, error)

PriceMsgsFn func(
ctx context.Context,
txfeesClient txfeestypes.QueryClient,
encodingConfig client.TxConfig,
account *authtypes.BaseAccount,
chainID string,
msg ...sdk.Msg,
) (uint64, sdk.Coin, error)
}

var _ sqstx.MsgSimulator = &MsgSimulatorMock{}
Expand Down Expand Up @@ -59,3 +68,15 @@ func (m *MsgSimulatorMock) SimulateMsgs(
}
panic("SimulateMsgsFn not implemented")
}

// PriceMsgs implements tx.MsgSimulator.
func (m *MsgSimulatorMock) PriceMsgs(ctx context.Context, txfeesClient txfeestypes.QueryClient, encodingConfig client.TxConfig, account *authtypes.BaseAccount, chainID string, msg ...interface {
ProtoMessage()
Reset()
String() string
}) (uint64, sdk.Coin, error) {
if m.PriceMsgsFn != nil {
return m.PriceMsgsFn(ctx, txfeesClient, encodingConfig, account, chainID, msg...)
}
panic("PriceMsgsFn not implemented")
}
23 changes: 23 additions & 0 deletions domain/quote_simulator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package domain

import (
"context"

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/osmosis-labs/osmosis/osmomath"
)

// QuoteSimulator simulates a quote and returns the gas adjusted amount and the fee coin.
type QuoteSimulator interface {
// SimulateQuote simulates a quote and returns the gas adjusted amount and the fee coin.
// CONTRACT:
// - 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"`
}
3 changes: 3 additions & 0 deletions domain/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ type Quote interface {
// for the tokens. In that case, we invalidate spot price by setting it to zero.
PrepareResult(ctx context.Context, scalingFactor osmomath.Dec, logger log.Logger) ([]SplitRoute, osmomath.Dec, error)

// SetQuotePriceInfo sets the quote price info.
SetQuotePriceInfo(info *QuotePriceInfo)

String() string
}

Expand Down
77 changes: 77 additions & 0 deletions quotesimulator/quote_simulator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package quotesimulator

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"
"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"
)

type quoteSimulator struct {
gasCalculator tx.MsgSimulator
encodingConfig params.EncodingConfig
txFeesClient txfeestypes.QueryClient
accountQueryClient types.QueryClient
chainID string
}

func NewQuoteSimulator(gasCalculator tx.MsgSimulator, bech32Address string, encodingConfig params.EncodingConfig, txFeesClient txfeestypes.QueryClient, accountQueryClient types.QueryClient, chainID string) *quoteSimulator {
return &quoteSimulator{
gasCalculator: gasCalculator,
encodingConfig: encodingConfig,
txFeesClient: txFeesClient,
accountQueryClient: accountQueryClient,
chainID: chainID,
}
}

func (q *quoteSimulator) SimulateQuote(ctx context.Context, quote domain.Quote, slippageToleranceMultiplier osmomath.Dec, simulatorAddress string) (uint64, sdk.Coin, error) {
route := quote.GetRoute()

if len(route) != 1 {
return 0, sdk.Coin{}, fmt.Errorf("route length must be 1, got %d", len(route))
}

poolsInRoute := route[0].GetPools()

poolManagerRoute := make([]poolmanagertypes.SwapAmountInRoute, len(poolsInRoute))
for i, r := range poolsInRoute {
poolManagerRoute[i] = poolmanagertypes.SwapAmountInRoute{
PoolId: r.GetId(),
TokenOutDenom: r.GetTokenOutDenom(),
}
}

tokenIn := quote.GetAmountIn()

slippageBound := tokenIn.Amount.ToLegacyDec().Mul(slippageToleranceMultiplier).TruncateInt()

swapMsg := &poolmanagertypes.MsgSwapExactAmountIn{
Sender: simulatorAddress,
Routes: poolManagerRoute,
TokenIn: tokenIn,
TokenOutMinAmount: slippageBound,
}

baseAccount, err := q.accountQueryClient.GetAccount(ctx, simulatorAddress)
if err != nil {
return 0, sdk.Coin{}, err
}

gasAdjusted, feeCoin, err := q.gasCalculator.PriceMsgs(ctx, q.txFeesClient, q.encodingConfig.TxConfig, baseAccount, q.chainID, swapMsg)
if err != nil {
return 0, sdk.Coin{}, err
}

return gasAdjusted, feeCoin, nil
}

var _ domain.QuoteSimulator = &quoteSimulator{}
35 changes: 28 additions & 7 deletions router/delivery/http/router_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ import (

// RouterHandler represent the httphandler for the router
type RouterHandler struct {
RUsecase mvc.RouterUsecase
TUsecase mvc.TokensUsecase
logger log.Logger
RUsecase mvc.RouterUsecase
TUsecase mvc.TokensUsecase
QuoteSimulator domain.QuoteSimulator
logger log.Logger
}

const routerResource = "/router"
Expand All @@ -35,11 +36,12 @@ func formatRouterResource(resource string) string {
}

// NewRouterHandler will initialize the pools/ resources endpoint
func NewRouterHandler(e *echo.Echo, us mvc.RouterUsecase, tu mvc.TokensUsecase, logger log.Logger) {
func NewRouterHandler(e *echo.Echo, us mvc.RouterUsecase, tu mvc.TokensUsecase, qs domain.QuoteSimulator, logger log.Logger) {
handler := &RouterHandler{
RUsecase: us,
TUsecase: tu,
logger: logger,
RUsecase: us,
TUsecase: tu,
QuoteSimulator: qs,
logger: logger,
}
e.GET(formatRouterResource("/quote"), handler.GetOptimalQuote)
e.GET(formatRouterResource("/routes"), handler.GetCandidateRoutes)
Expand Down Expand Up @@ -143,6 +145,25 @@ func (a *RouterHandler) GetOptimalQuote(c echo.Context) (err error) {
span.SetAttributes(attribute.Stringer("token_out", quote.GetAmountOut()))
span.SetAttributes(attribute.Stringer("price_impact", quote.GetPriceImpact()))

// Simulate quote if applicable.
// Note: only single routes (non-splits) are supported for simulation.
// Additionally, the functionality is triggerred by the user providing a simulator address.
// Only "out given in" swap method is supported for simulation. Thus, we also check for tokenOutDenom being set.
if req.SingleRoute && req.SimulatorAddress != "" && req.SwapMethod() == domain.TokenSwapMethodExactIn {
simulatorAddress := c.Get("simulatorAddress").(string)

Check failure on line 153 in router/delivery/http/router_handler.go

View workflow job for this annotation

GitHub Actions / Run linter

type assertion must be checked (forcetypeassert)

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()})
}

// Set the quote price info.
quote.SetQuotePriceInfo(&domain.QuotePriceInfo{
AdjustedGasUsed: gasUsed,
FeeCoin: feeCoin,
})
}

return c.JSON(http.StatusOK, quote)
}

Expand Down
64 changes: 57 additions & 7 deletions router/types/get_quote_request.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package types

import (
"fmt"

"github.com/osmosis-labs/osmosis/osmomath"
"github.com/osmosis-labs/sqs/domain"

sdk "github.com/cosmos/cosmos-sdk/types"
Expand All @@ -9,13 +12,15 @@ import (

// GetQuoteRequest represents swap quote request for the /router/quote endpoint.
type GetQuoteRequest struct {
TokenIn *sdk.Coin
TokenOutDenom string
TokenOut *sdk.Coin
TokenInDenom string
SingleRoute bool
HumanDenoms bool
ApplyExponents bool
TokenIn *sdk.Coin
TokenOutDenom string
TokenOut *sdk.Coin
TokenInDenom string
SingleRoute bool
SimulatorAddress string
SlippageToleranceMultiplier osmomath.Dec
HumanDenoms bool
ApplyExponents bool
}

// UnmarshalHTTPRequest unmarshals the HTTP request to GetQuoteRequest.
Expand Down Expand Up @@ -51,9 +56,54 @@ func (r *GetQuoteRequest) UnmarshalHTTPRequest(c echo.Context) error {
r.TokenInDenom = c.QueryParam("tokenInDenom")
r.TokenOutDenom = c.QueryParam("tokenOutDenom")

simulatorAddress := c.QueryParam("simulatorAddress")
slippageToleranceStr := c.QueryParam("simulattionSlippageTolerance")
if err != nil {
return err
}

slippageToleranceDec, err := validateSimulationParams(r.SwapMethod(), simulatorAddress, slippageToleranceStr)
if err != nil {
return err
}

r.SimulatorAddress = simulatorAddress
r.SlippageToleranceMultiplier = slippageToleranceDec

return nil
}

func validateSimulationParams(swapMethod domain.TokenSwapMethod, simulatorAddress string, slippageToleranceStr string) (osmomath.Dec, error) {
if simulatorAddress != "" {
_, err := sdk.ValAddressFromBech32(simulatorAddress)
if err != nil {
return osmomath.Dec{}, fmt.Errorf("simulator address is not valid: %w", err)
}

// Validate that simulation is only requested for "out given in" swap method.
if swapMethod == domain.TokenSwapMethodExactIn {
return osmomath.Dec{}, fmt.Errorf("only 'out given in' swap method is supported for simulation")
}

if slippageToleranceStr == "" {
return osmomath.Dec{}, fmt.Errorf("slippage tolerance is required for simulation")
}

slippageTolerance, err := osmomath.NewDecFromStr(slippageToleranceStr)
if err != nil {
return osmomath.Dec{}, fmt.Errorf("slippage tolerance is not valid: %w", err)
}

return slippageTolerance, nil
} else {
if slippageToleranceStr != "" {
return osmomath.Dec{}, fmt.Errorf("slippage tolerance is not supported without simulator address")
}
}

return osmomath.Dec{}, nil
}

// SwapMethod returns the swap method of the request.
// Request may contain data for both swap methods, only one of them should be specified, otherwise it's invalid.
func (r *GetQuoteRequest) SwapMethod() domain.TokenSwapMethod {
Expand Down
7 changes: 7 additions & 0 deletions router/usecase/quote_out_given_in.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ type quoteExactAmountIn struct {
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"`

Check failure on line 46 in router/usecase/quote_out_given_in.go

View workflow job for this annotation

GitHub Actions / Run linter

structtag: struct field priceInfo has json tag but is not exported (govet)
}

// PrepareResult implements domain.Quote.
Expand Down Expand Up @@ -151,3 +153,8 @@ func (q *quoteExactAmountIn) GetPriceImpact() osmomath.Dec {
func (q *quoteExactAmountIn) GetInBaseOutQuoteSpotPrice() osmomath.Dec {
return q.InBaseOutQuoteSpotPrice
}

// SetQuotePriceInfo implements domain.Quote.
func (q *quoteExactAmountIn) SetQuotePriceInfo(info *domain.QuotePriceInfo) {
q.priceInfo = info
}

0 comments on commit 10eca0f

Please sign in to comment.