From d7fcbd3444aac73a36f1cfeb7a1f11fa4dcd9533 Mon Sep 17 00:00:00 2001 From: dlipicar Date: Wed, 7 Aug 2024 19:33:44 -0300 Subject: [PATCH] feat(wallet)_: handle paraswap price impact error (#5622) --- Makefile | 1 + .../wallet/router/pathprocessor/errors.go | 1 + .../pathprocessor/processor_swap_paraswap.go | 4 +- .../processor_swap_paraswap_test.go | 62 +++++++++ .../wallet/thirdparty/paraswap/mock/types.go | 120 ++++++++++++++++++ services/wallet/thirdparty/paraswap/types.go | 22 ++++ 6 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 services/wallet/thirdparty/paraswap/mock/types.go create mode 100644 services/wallet/thirdparty/paraswap/types.go diff --git a/Makefile b/Makefile index 127d5689f..cb8ea1fc4 100644 --- a/Makefile +++ b/Makefile @@ -357,6 +357,7 @@ mock: ##@other Regenerate mocks mockgen -package=mock_collectibles -destination=services/wallet/collectibles/mock/collection_data_db.go -source=services/wallet/collectibles/collection_data_db.go mockgen -package=mock_collectibles -destination=services/wallet/collectibles/mock/collectible_data_db.go -source=services/wallet/collectibles/collectible_data_db.go mockgen -package=mock_thirdparty -destination=services/wallet/thirdparty/mock/collectible_types.go -source=services/wallet/thirdparty/collectible_types.go + mockgen -package=mock_paraswap -destination=services/wallet/thirdparty/paraswap/mock/types.go -source=services/wallet/thirdparty/paraswap/types.go docker-test: ##@tests Run tests in a docker container with golang. docker run --privileged --rm -it -v "$(PWD):$(DOCKER_TEST_WORKDIR)" -w "$(DOCKER_TEST_WORKDIR)" $(DOCKER_TEST_IMAGE) go test ${ARGS} diff --git a/services/wallet/router/pathprocessor/errors.go b/services/wallet/router/pathprocessor/errors.go index 5b635b9d6..8de34d92f 100644 --- a/services/wallet/router/pathprocessor/errors.go +++ b/services/wallet/router/pathprocessor/errors.go @@ -46,6 +46,7 @@ var ( ErrContextDeadlineExceeded = &errors.ErrorResponse{Code: errors.ErrorCode("WPP-036"), Details: "context deadline exceeded"} ErrPriceTimeout = &errors.ErrorResponse{Code: errors.ErrorCode("WPP-037"), Details: "price timeout"} ErrNotEnoughLiquidity = &errors.ErrorResponse{Code: errors.ErrorCode("WPP-038"), Details: "not enough liquidity"} + ErrPriceImpactTooHigh = &errors.ErrorResponse{Code: errors.ErrorCode("WPP-039"), Details: "price impact too high"} ) func createErrorResponse(processorName string, err error) error { diff --git a/services/wallet/router/pathprocessor/processor_swap_paraswap.go b/services/wallet/router/pathprocessor/processor_swap_paraswap.go index 9dcce15da..e6e247a31 100644 --- a/services/wallet/router/pathprocessor/processor_swap_paraswap.go +++ b/services/wallet/router/pathprocessor/processor_swap_paraswap.go @@ -28,7 +28,7 @@ type SwapParaswapTxArgs struct { } type SwapParaswapProcessor struct { - paraswapClient *paraswap.ClientV5 + paraswapClient paraswap.ClientInterface transactor transactions.TransactorIface priceRoute sync.Map // [fromChainName-toChainName-fromTokenSymbol-toTokenSymbol, paraswap.Route] } @@ -73,6 +73,8 @@ func createSwapParaswapErrorResponse(err error) error { return ErrPriceTimeout case "No routes found with enough liquidity": return ErrNotEnoughLiquidity + case "ESTIMATED_LOSS_GREATER_THAN_MAX_IMPACT": + return ErrPriceImpactTooHigh } return createErrorResponse(ProcessorSwapParaswapName, err) } diff --git a/services/wallet/router/pathprocessor/processor_swap_paraswap_test.go b/services/wallet/router/pathprocessor/processor_swap_paraswap_test.go index 5ad1d91c6..19e01ee76 100644 --- a/services/wallet/router/pathprocessor/processor_swap_paraswap_test.go +++ b/services/wallet/router/pathprocessor/processor_swap_paraswap_test.go @@ -1,14 +1,19 @@ package pathprocessor import ( + "errors" "math/big" "testing" "github.com/ethereum/go-ethereum/common" + + gomock "github.com/golang/mock/gomock" + "github.com/status-im/status-go/params" "github.com/status-im/status-go/services/wallet/bigint" walletCommon "github.com/status-im/status-go/services/wallet/common" "github.com/status-im/status-go/services/wallet/thirdparty/paraswap" + mock_paraswap "github.com/status-im/status-go/services/wallet/thirdparty/paraswap/mock" "github.com/status-im/status-go/services/wallet/token" "github.com/stretchr/testify/require" @@ -69,3 +74,60 @@ func TestParaswapWithPartnerFee(t *testing.T) { } } } + +func expectClientFetchPriceRoute(clientMock *mock_paraswap.MockClientInterface, route paraswap.Route, err error) { + clientMock.EXPECT().FetchPriceRoute( + gomock.Any(), + gomock.Any(), + gomock.Any(), + gomock.Any(), + gomock.Any(), + gomock.Any(), + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Return(route, err) +} + +func TestParaswapErrors(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + client := mock_paraswap.NewMockClientInterface(ctrl) + + processor := NewSwapParaswapProcessor(nil, nil, nil) + processor.paraswapClient = client + + fromToken := token.Token{ + Symbol: EthSymbol, + } + toToken := token.Token{ + Symbol: UsdcSymbol, + } + chainID := walletCommon.EthereumMainnet + + testInputParams := ProcessorInputParams{ + FromChain: ¶ms.Network{ChainID: chainID}, + ToChain: ¶ms.Network{ChainID: chainID}, + FromToken: &fromToken, + ToToken: &toToken, + } + + // Test Errors + type testCase struct { + clientError string + processorError error + } + + testCases := []testCase{ + {"Price Timeout", ErrPriceTimeout}, + {"No routes found with enough liquidity", ErrNotEnoughLiquidity}, + {"ESTIMATED_LOSS_GREATER_THAN_MAX_IMPACT", ErrPriceImpactTooHigh}, + } + + for _, tc := range testCases { + expectClientFetchPriceRoute(client, paraswap.Route{}, errors.New(tc.clientError)) + _, err := processor.EstimateGas(testInputParams) + require.Equal(t, tc.processorError.Error(), err.Error()) + } +} diff --git a/services/wallet/thirdparty/paraswap/mock/types.go b/services/wallet/thirdparty/paraswap/mock/types.go new file mode 100644 index 000000000..32d3fc719 --- /dev/null +++ b/services/wallet/thirdparty/paraswap/mock/types.go @@ -0,0 +1,120 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: services/wallet/thirdparty/paraswap/types.go + +// Package mock_paraswap is a generated GoMock package. +package mock_paraswap + +import ( + context "context" + json "encoding/json" + big "math/big" + reflect "reflect" + + common "github.com/ethereum/go-ethereum/common" + gomock "github.com/golang/mock/gomock" + paraswap "github.com/status-im/status-go/services/wallet/thirdparty/paraswap" +) + +// MockClientInterface is a mock of ClientInterface interface. +type MockClientInterface struct { + ctrl *gomock.Controller + recorder *MockClientInterfaceMockRecorder +} + +// MockClientInterfaceMockRecorder is the mock recorder for MockClientInterface. +type MockClientInterfaceMockRecorder struct { + mock *MockClientInterface +} + +// NewMockClientInterface creates a new mock instance. +func NewMockClientInterface(ctrl *gomock.Controller) *MockClientInterface { + mock := &MockClientInterface{ctrl: ctrl} + mock.recorder = &MockClientInterfaceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockClientInterface) EXPECT() *MockClientInterfaceMockRecorder { + return m.recorder +} + +// BuildTransaction mocks base method. +func (m *MockClientInterface) BuildTransaction(ctx context.Context, srcTokenAddress common.Address, srcTokenDecimals uint, srcAmountWei *big.Int, destTokenAddress common.Address, destTokenDecimals uint, destAmountWei *big.Int, slippageBasisPoints uint, addressFrom, addressTo common.Address, priceRoute json.RawMessage, side paraswap.SwapSide) (paraswap.Transaction, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BuildTransaction", ctx, srcTokenAddress, srcTokenDecimals, srcAmountWei, destTokenAddress, destTokenDecimals, destAmountWei, slippageBasisPoints, addressFrom, addressTo, priceRoute, side) + ret0, _ := ret[0].(paraswap.Transaction) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BuildTransaction indicates an expected call of BuildTransaction. +func (mr *MockClientInterfaceMockRecorder) BuildTransaction(ctx, srcTokenAddress, srcTokenDecimals, srcAmountWei, destTokenAddress, destTokenDecimals, destAmountWei, slippageBasisPoints, addressFrom, addressTo, priceRoute, side interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BuildTransaction", reflect.TypeOf((*MockClientInterface)(nil).BuildTransaction), ctx, srcTokenAddress, srcTokenDecimals, srcAmountWei, destTokenAddress, destTokenDecimals, destAmountWei, slippageBasisPoints, addressFrom, addressTo, priceRoute, side) +} + +// FetchPriceRoute mocks base method. +func (m *MockClientInterface) FetchPriceRoute(ctx context.Context, srcTokenAddress common.Address, srcTokenDecimals uint, destTokenAddress common.Address, destTokenDecimals uint, amountWei *big.Int, addressFrom, addressTo common.Address, side paraswap.SwapSide) (paraswap.Route, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FetchPriceRoute", ctx, srcTokenAddress, srcTokenDecimals, destTokenAddress, destTokenDecimals, amountWei, addressFrom, addressTo, side) + ret0, _ := ret[0].(paraswap.Route) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FetchPriceRoute indicates an expected call of FetchPriceRoute. +func (mr *MockClientInterfaceMockRecorder) FetchPriceRoute(ctx, srcTokenAddress, srcTokenDecimals, destTokenAddress, destTokenDecimals, amountWei, addressFrom, addressTo, side interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchPriceRoute", reflect.TypeOf((*MockClientInterface)(nil).FetchPriceRoute), ctx, srcTokenAddress, srcTokenDecimals, destTokenAddress, destTokenDecimals, amountWei, addressFrom, addressTo, side) +} + +// FetchTokensList mocks base method. +func (m *MockClientInterface) FetchTokensList(ctx context.Context) ([]paraswap.Token, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FetchTokensList", ctx) + ret0, _ := ret[0].([]paraswap.Token) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FetchTokensList indicates an expected call of FetchTokensList. +func (mr *MockClientInterfaceMockRecorder) FetchTokensList(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchTokensList", reflect.TypeOf((*MockClientInterface)(nil).FetchTokensList), ctx) +} + +// SetChainID mocks base method. +func (m *MockClientInterface) SetChainID(chainID uint64) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetChainID", chainID) +} + +// SetChainID indicates an expected call of SetChainID. +func (mr *MockClientInterfaceMockRecorder) SetChainID(chainID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetChainID", reflect.TypeOf((*MockClientInterface)(nil).SetChainID), chainID) +} + +// SetPartnerAddress mocks base method. +func (m *MockClientInterface) SetPartnerAddress(partnerAddress common.Address) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetPartnerAddress", partnerAddress) +} + +// SetPartnerAddress indicates an expected call of SetPartnerAddress. +func (mr *MockClientInterfaceMockRecorder) SetPartnerAddress(partnerAddress interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetPartnerAddress", reflect.TypeOf((*MockClientInterface)(nil).SetPartnerAddress), partnerAddress) +} + +// SetPartnerFeePcnt mocks base method. +func (m *MockClientInterface) SetPartnerFeePcnt(partnerFeePcnt float64) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetPartnerFeePcnt", partnerFeePcnt) +} + +// SetPartnerFeePcnt indicates an expected call of SetPartnerFeePcnt. +func (mr *MockClientInterfaceMockRecorder) SetPartnerFeePcnt(partnerFeePcnt interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetPartnerFeePcnt", reflect.TypeOf((*MockClientInterface)(nil).SetPartnerFeePcnt), partnerFeePcnt) +} diff --git a/services/wallet/thirdparty/paraswap/types.go b/services/wallet/thirdparty/paraswap/types.go new file mode 100644 index 000000000..20ba13501 --- /dev/null +++ b/services/wallet/thirdparty/paraswap/types.go @@ -0,0 +1,22 @@ +package paraswap + +import ( + "context" + "encoding/json" + "math/big" + + "github.com/ethereum/go-ethereum/common" +) + +type ClientInterface interface { + SetChainID(chainID uint64) + SetPartnerAddress(partnerAddress common.Address) + SetPartnerFeePcnt(partnerFeePcnt float64) + BuildTransaction(ctx context.Context, srcTokenAddress common.Address, srcTokenDecimals uint, srcAmountWei *big.Int, + destTokenAddress common.Address, destTokenDecimals uint, destAmountWei *big.Int, slippageBasisPoints uint, + addressFrom common.Address, addressTo common.Address, priceRoute json.RawMessage, side SwapSide) (Transaction, error) + FetchPriceRoute(ctx context.Context, srcTokenAddress common.Address, srcTokenDecimals uint, + destTokenAddress common.Address, destTokenDecimals uint, amountWei *big.Int, addressFrom common.Address, + addressTo common.Address, side SwapSide) (Route, error) + FetchTokensList(ctx context.Context) ([]Token, error) +}