From 92361d9e20d6ba86aadc6a9af8da30fcd79af814 Mon Sep 17 00:00:00 2001 From: Sale Djenic Date: Thu, 20 Jun 2024 11:03:42 +0200 Subject: [PATCH] feat_: new endpoint added for an async route/s calculation - `GetSuggestedRoutesV2Async` calculates the route/s based on input parameters and sends `wallet.suggested.routes` signal to notify a client. --- services/wallet/api.go | 13 ++++ services/wallet/router/errors.go | 1 + services/wallet/router/fees.go | 31 ++++---- services/wallet/router/router.go | 21 ++++-- services/wallet/router/router_v2.go | 92 ++++++++++++++++-------- services/wallet/router/router_v2_test.go | 6 +- signal/events_wallet.go | 1 + 7 files changed, 113 insertions(+), 52 deletions(-) diff --git a/services/wallet/api.go b/services/wallet/api.go index 3383f3cee..fc384a6e9 100644 --- a/services/wallet/api.go +++ b/services/wallet/api.go @@ -94,6 +94,7 @@ func (api *API) StartWallet(ctx context.Context) error { } func (api *API) StopWallet(ctx context.Context) error { + api.router.Stop() return api.s.Stop() } @@ -509,6 +510,18 @@ func (api *API) GetSuggestedRoutesV2(ctx context.Context, input *router.RouteInp return api.router.SuggestedRoutesV2(ctx, input) } +func (api *API) GetSuggestedRoutesV2Async(ctx context.Context, input *router.RouteInputParams) { + log.Debug("call to GetSuggestedRoutesV2Async") + + api.router.SuggestedRoutesV2Async(input) +} + +func (api *API) StopSuggestedRoutesV2AsyncCalcualtion(ctx context.Context) { + log.Debug("call to StopSuggestedRoutesV2AsyncCalcualtion") + + api.router.StopSuggestedRoutesV2AsyncCalcualtion() +} + // Generates addresses for the provided paths, response doesn't include `HasActivity` value (if you need it check `GetAddressDetails` function) func (api *API) GetDerivedAddresses(ctx context.Context, password string, derivedFrom string, paths []string) ([]*DerivedAddress, error) { info, err := api.s.gethManager.AccountsGenerator().LoadAccount(derivedFrom, password) diff --git a/services/wallet/router/errors.go b/services/wallet/router/errors.go index c4038ad72..9ab7072eb 100644 --- a/services/wallet/router/errors.go +++ b/services/wallet/router/errors.go @@ -24,4 +24,5 @@ var ( ErrNotEnoughTokenBalance = &errors.ErrorResponse{Code: errors.ErrorCode("WR-016"), Details: "not enough token balance"} ErrNotEnoughNativeBalance = &errors.ErrorResponse{Code: errors.ErrorCode("WR-017"), Details: "not enough native balance"} ErrNativeTokenNotFound = &errors.ErrorResponse{Code: errors.ErrorCode("WR-018"), Details: "native token not found"} + ErrDisabledChainFoundAmongLockedNetworks = &errors.ErrorResponse{Code: errors.ErrorCode("WR-019"), Details: "disabled chain found among locked networks"} ) diff --git a/services/wallet/router/fees.go b/services/wallet/router/fees.go index 808e99eb7..615335043 100644 --- a/services/wallet/router/fees.go +++ b/services/wallet/router/fees.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/consensus/misc" "github.com/ethereum/go-ethereum/params" gaspriceoracle "github.com/status-im/status-go/contracts/gas-price-oracle" @@ -25,9 +26,9 @@ const ( ) type MaxFeesLevels struct { - Low *big.Int `json:"low"` - Medium *big.Int `json:"medium"` - High *big.Int `json:"high"` + Low *hexutil.Big `json:"low"` + Medium *hexutil.Big `json:"medium"` + High *hexutil.Big `json:"high"` } type SuggestedFees struct { @@ -59,14 +60,14 @@ func (s *SuggestedFees) feeFor(mode GasFeeMode) *big.Int { } if mode == GasFeeLow { - return s.MaxFeesLevels.Low + return s.MaxFeesLevels.Low.ToInt() } if mode == GasFeeHigh { - return s.MaxFeesLevels.High + return s.MaxFeesLevels.High.ToInt() } - return s.MaxFeesLevels.Medium + return s.MaxFeesLevels.Medium.ToInt() } func (s *SuggestedFeesGwei) feeFor(mode GasFeeMode) *big.Float { @@ -140,9 +141,9 @@ func (f *FeeManager) SuggestedFees(ctx context.Context, chainID uint64) (*Sugges BaseFee: big.NewInt(0), MaxPriorityFeePerGas: big.NewInt(0), MaxFeesLevels: &MaxFeesLevels{ - Low: big.NewInt(0), - Medium: big.NewInt(0), - High: big.NewInt(0), + Low: (*hexutil.Big)(big.NewInt(0)), + Medium: (*hexutil.Big)(big.NewInt(0)), + High: (*hexutil.Big)(big.NewInt(0)), }, EIP1559Enabled: false, }, nil @@ -158,9 +159,9 @@ func (f *FeeManager) SuggestedFees(ctx context.Context, chainID uint64) (*Sugges BaseFee: baseFee, MaxPriorityFeePerGas: maxPriorityFeePerGas, MaxFeesLevels: &MaxFeesLevels{ - Low: new(big.Int).Add(baseFee, maxPriorityFeePerGas), - Medium: new(big.Int).Add(new(big.Int).Mul(baseFee, big.NewInt(2)), maxPriorityFeePerGas), - High: new(big.Int).Add(new(big.Int).Mul(baseFee, big.NewInt(3)), maxPriorityFeePerGas), + Low: (*hexutil.Big)(new(big.Int).Add(baseFee, maxPriorityFeePerGas)), + Medium: (*hexutil.Big)(new(big.Int).Add(new(big.Int).Mul(baseFee, big.NewInt(2)), maxPriorityFeePerGas)), + High: (*hexutil.Big)(new(big.Int).Add(new(big.Int).Mul(baseFee, big.NewInt(3)), maxPriorityFeePerGas)), }, EIP1559Enabled: true, }, nil @@ -175,9 +176,9 @@ func (f *FeeManager) SuggestedFeesGwei(ctx context.Context, chainID uint64) (*Su GasPrice: weiToGwei(fees.GasPrice), BaseFee: weiToGwei(fees.BaseFee), MaxPriorityFeePerGas: weiToGwei(fees.MaxPriorityFeePerGas), - MaxFeePerGasLow: weiToGwei(fees.MaxFeesLevels.Low), - MaxFeePerGasMedium: weiToGwei(fees.MaxFeesLevels.Medium), - MaxFeePerGasHigh: weiToGwei(fees.MaxFeesLevels.High), + MaxFeePerGasLow: weiToGwei(fees.MaxFeesLevels.Low.ToInt()), + MaxFeePerGasMedium: weiToGwei(fees.MaxFeesLevels.Medium.ToInt()), + MaxFeePerGasHigh: weiToGwei(fees.MaxFeesLevels.High.ToInt()), EIP1559Enabled: fees.EIP1559Enabled, }, nil } diff --git a/services/wallet/router/router.go b/services/wallet/router/router.go index a47189e4f..b5a05665e 100644 --- a/services/wallet/router/router.go +++ b/services/wallet/router/router.go @@ -288,6 +288,7 @@ func NewRouter(rpcClient *rpc.Client, transactor *transactions.Transactor, token stickersService: stickersService, feesManager: &FeeManager{rpcClient}, pathProcessors: processors, + scheduler: async.NewScheduler(), } } @@ -295,6 +296,10 @@ func (r *Router) AddPathProcessor(processor pathprocessor.PathProcessor) { r.pathProcessors[processor.Name()] = processor } +func (r *Router) Stop() { + r.scheduler.Stop() +} + func (r *Router) GetFeesManager() *FeeManager { return r.feesManager } @@ -303,9 +308,9 @@ func (r *Router) GetPathProcessors() map[string]pathprocessor.PathProcessor { return r.pathProcessors } -func containsNetworkChainID(network *params.Network, chainIDs []uint64) bool { - for _, chainID := range chainIDs { - if chainID == network.ChainID { +func containsNetworkChainID(chainID uint64, chainIDs []uint64) bool { + for _, cID := range chainIDs { + if cID == chainID { return true } } @@ -323,6 +328,7 @@ type Router struct { stickersService *stickers.Service feesManager *FeeManager pathProcessors map[string]pathprocessor.PathProcessor + scheduler *async.Scheduler } func (r *Router) requireApproval(ctx context.Context, sendType SendType, approvalContractAddress *common.Address, params pathprocessor.ProcessorInputParams) ( @@ -478,7 +484,7 @@ func (r *Router) SuggestedRoutes( continue } - if containsNetworkChainID(network, disabledFromChainIDs) { + if containsNetworkChainID(network.ChainID, disabledFromChainIDs) { continue } @@ -562,10 +568,10 @@ func (r *Router) SuggestedRoutes( continue } - if len(preferedChainIDs) > 0 && !containsNetworkChainID(dest, preferedChainIDs) { + if len(preferedChainIDs) > 0 && !containsNetworkChainID(dest.ChainID, preferedChainIDs) { continue } - if containsNetworkChainID(dest, disabledToChainIDs) { + if containsNetworkChainID(dest.ChainID, disabledToChainIDs) { continue } @@ -710,6 +716,9 @@ func (r *Router) SuggestedRoutes( FromToken: &token.Token{ Symbol: tokenID, }, + ToToken: &token.Token{ + Symbol: toTokenID, + }, } amountOut, err := r.pathProcessors[path.BridgeName].CalculateAmountOut(processorInputParams) diff --git a/services/wallet/router/router_v2.go b/services/wallet/router/router_v2.go index 15faca360..6d5a49841 100644 --- a/services/wallet/router/router_v2.go +++ b/services/wallet/router/router_v2.go @@ -16,6 +16,14 @@ import ( walletCommon "github.com/status-im/status-go/services/wallet/common" "github.com/status-im/status-go/services/wallet/router/pathprocessor" walletToken "github.com/status-im/status-go/services/wallet/token" + "github.com/status-im/status-go/signal" +) + +var ( + routerTask = async.TaskType{ + ID: 1, + Policy: async.ReplacementPolicyCancelOld, + } ) var ( @@ -33,6 +41,7 @@ var ( ) type RouteInputParams struct { + Uuid string `json:"uuid"` SendType SendType `json:"sendType" validate:"required"` AddrFrom common.Address `json:"addrFrom" validate:"required"` AddrTo common.Address `json:"addrTo" validate:"required"` @@ -106,12 +115,18 @@ func (p *PathV2) Equal(o *PathV2) bool { } type SuggestedRoutesV2 struct { + Uuid string Best []*PathV2 Candidates []*PathV2 TokenPrice float64 NativeChainTokenPrice float64 } +type ErrorResponseWithUUID struct { + Uuid string + ErrorResponse error +} + type GraphV2 = []*NodeV2 type NodeV2 struct { @@ -120,6 +135,7 @@ type NodeV2 struct { } func newSuggestedRoutesV2( + uuid string, amountIn *big.Int, candidates []*PathV2, fromLockedAmount map[uint64]*hexutil.Big, @@ -127,6 +143,7 @@ func newSuggestedRoutesV2( nativeChainTokenPrice float64, ) *SuggestedRoutesV2 { suggestedRoutes := &SuggestedRoutesV2{ + Uuid: uuid, Candidates: candidates, Best: candidates, TokenPrice: tokenPrice, @@ -388,6 +405,9 @@ func validateInputData(input *RouteInputParams) error { totalLockedAmount := big.NewInt(0) for chainID, amount := range input.FromLockedAmount { + if containsNetworkChainID(chainID, input.DisabledFromChainIDs) { + return ErrDisabledChainFoundAmongLockedNetworks + } if input.testnetMode { if !supportedTestNetworks[chainID] { return ErrLockedAmountNotSupportedForNetwork @@ -417,7 +437,34 @@ func validateInputData(input *RouteInputParams) error { return nil } +func (r *Router) SuggestedRoutesV2Async(input *RouteInputParams) { + r.scheduler.Enqueue(routerTask, func(ctx context.Context) (interface{}, error) { + return r.SuggestedRoutesV2(ctx, input) + }, func(result interface{}, taskType async.TaskType, err error) { + if err != nil { + errResponse := &ErrorResponseWithUUID{ + Uuid: input.Uuid, + ErrorResponse: errors.CreateErrorResponseFromError(err), + } + signal.SendWalletEvent(signal.SuggestedRoutes, errResponse) + return + } + signal.SendWalletEvent(signal.SuggestedRoutes, result) + }) +} + +func (r *Router) StopSuggestedRoutesV2AsyncCalcualtion() { + r.scheduler.Stop() +} + func (r *Router) SuggestedRoutesV2(ctx context.Context, input *RouteInputParams) (*SuggestedRoutesV2, error) { + testnetMode, err := r.rpcClient.NetworkManager.GetTestNetworksEnabled() + if err != nil { + return nil, errors.CreateErrorResponseFromError(err) + } + + input.testnetMode = testnetMode + // clear all processors for _, processor := range r.pathProcessors { if clearable, ok := processor.(pathprocessor.PathProcessorClearable); ok { @@ -425,18 +472,11 @@ func (r *Router) SuggestedRoutesV2(ctx context.Context, input *RouteInputParams) } } - err := validateInputData(input) + err = validateInputData(input) if err != nil { return nil, errors.CreateErrorResponseFromError(err) } - testnetMode, err := r.rpcClient.NetworkManager.GetTestNetworksEnabled() - if err != nil { - return nil, errors.CreateErrorResponseFromError(err) - } - - input.testnetMode = testnetMode - candidates, err := r.resolveCandidates(ctx, input) if err != nil { return nil, errors.CreateErrorResponseFromError(err) @@ -467,7 +507,7 @@ func (r *Router) resolveCandidates(ctx context.Context, input *RouteInputParams) continue } - if containsNetworkChainID(network, input.DisabledFromChainIDs) { + if containsNetworkChainID(network.ChainID, input.DisabledFromChainIDs) { continue } @@ -507,11 +547,17 @@ func (r *Router) resolveCandidates(ctx context.Context, input *RouteInputParams) } } - group.Add(func(c context.Context) error { + var fees *SuggestedFees + if testsMode { + fees = input.testParams.suggestedFees + } else { + fees, err = r.feesManager.SuggestedFees(ctx, network.ChainID) if err != nil { - return errors.CreateErrorResponseFromError(err) + continue } + } + group.Add(func(c context.Context) error { for _, pProcessor := range r.pathProcessors { // With the condition below we're eliminating `Swap` as potential path that can participate in calculating the best route // once we decide to inlcude `Swap` in the calculation we need to update `canUseProcessor` function. @@ -546,7 +592,7 @@ func (r *Router) resolveCandidates(ctx context.Context, input *RouteInputParams) continue } - if containsNetworkChainID(dest, input.DisabledToChainIDs) { + if containsNetworkChainID(dest.ChainID, input.DisabledToChainIDs) { continue } @@ -558,7 +604,7 @@ func (r *Router) resolveCandidates(ctx context.Context, input *RouteInputParams) ToAddr: input.AddrTo, FromAddr: input.AddrFrom, AmountIn: amountToSend, - AmountOut: amountToSend, + AmountOut: input.AmountOut.ToInt(), Username: input.Username, PublicKey: input.PublicKey, @@ -606,16 +652,6 @@ func (r *Router) resolveCandidates(ctx context.Context, input *RouteInputParams) l1FeeWei, _ = r.feesManager.GetL1Fee(ctx, network.ChainID, txInputData) } - var fees *SuggestedFees - if testsMode { - fees = input.testParams.suggestedFees - } else { - fees, err = r.feesManager.SuggestedFees(ctx, network.ChainID) - if err != nil { - continue - } - } - amountOut, err := pProcessor.CalculateAmountOut(processorInputParams) if err != nil { continue @@ -679,7 +715,7 @@ func (r *Router) resolveRoutes(ctx context.Context, input *RouteInputParams, can } } - suggestedRoutes = newSuggestedRoutesV2(input.AmountIn.ToInt(), candidates, input.FromLockedAmount, prices[input.TokenID], prices[pathprocessor.EthSymbol]) + suggestedRoutes = newSuggestedRoutesV2(input.Uuid, input.AmountIn.ToInt(), candidates, input.FromLockedAmount, prices[input.TokenID], prices[pathprocessor.EthSymbol]) // check the best route for the required balances for _, path := range suggestedRoutes.Best { @@ -693,12 +729,12 @@ func (r *Router) resolveRoutes(ctx context.Context, input *RouteInputParams, can if input.SendType == ERC1155Transfer { tokenBalance, err = r.getERC1155Balance(ctx, path.FromChain, path.FromToken, input.AddrFrom) if err != nil { - return nil, errors.CreateErrorResponseFromError(err) + return suggestedRoutes, errors.CreateErrorResponseFromError(err) } } else if input.SendType != ERC721Transfer { tokenBalance, err = r.getBalance(ctx, path.FromChain, path.FromToken, input.AddrFrom) if err != nil { - return nil, errors.CreateErrorResponseFromError(err) + return suggestedRoutes, errors.CreateErrorResponseFromError(err) } } } @@ -716,12 +752,12 @@ func (r *Router) resolveRoutes(ctx context.Context, input *RouteInputParams, can } else { nativeToken := r.tokenManager.FindToken(path.FromChain, path.FromChain.NativeCurrencySymbol) if nativeToken == nil { - return nil, ErrNativeTokenNotFound + return suggestedRoutes, ErrNativeTokenNotFound } nativeBalance, err = r.getBalance(ctx, path.FromChain, nativeToken, input.AddrFrom) if err != nil { - return nil, errors.CreateErrorResponseFromError(err) + return suggestedRoutes, errors.CreateErrorResponseFromError(err) } } diff --git a/services/wallet/router/router_v2_test.go b/services/wallet/router/router_v2_test.go index 62c7d7e08..1d0d4c962 100644 --- a/services/wallet/router/router_v2_test.go +++ b/services/wallet/router/router_v2_test.go @@ -67,9 +67,9 @@ var ( BaseFee: big.NewInt(testBaseFee), MaxPriorityFeePerGas: big.NewInt(testPriorityFeeLow), MaxFeesLevels: &MaxFeesLevels{ - Low: big.NewInt(testPriorityFeeLow), - Medium: big.NewInt(testPriorityFeeMedium), - High: big.NewInt(testPriorityFeeHigh), + Low: (*hexutil.Big)(big.NewInt(testPriorityFeeLow)), + Medium: (*hexutil.Big)(big.NewInt(testPriorityFeeMedium)), + High: (*hexutil.Big)(big.NewInt(testPriorityFeeHigh)), }, EIP1559Enabled: false, } diff --git a/signal/events_wallet.go b/signal/events_wallet.go index 528652b53..925958958 100644 --- a/signal/events_wallet.go +++ b/signal/events_wallet.go @@ -5,6 +5,7 @@ type SignalType string const ( Wallet = SignalType("wallet") SignTransactions = SignalType("wallet.sign.transactions") + SuggestedRoutes = SignalType("wallet.suggested.routes") ) // SendWalletEvent sends event from services/wallet/events.