github.com/status-im/status-go@v1.1.0/services/wallet/router/router.go (about)

     1  package router
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"math/big"
     7  	"sort"
     8  	"strings"
     9  	"sync"
    10  
    11  	"github.com/ethereum/go-ethereum/common"
    12  	"github.com/ethereum/go-ethereum/common/hexutil"
    13  	"github.com/ethereum/go-ethereum/log"
    14  	"github.com/status-im/status-go/errors"
    15  	"github.com/status-im/status-go/params"
    16  	"github.com/status-im/status-go/rpc"
    17  	"github.com/status-im/status-go/services/ens"
    18  	"github.com/status-im/status-go/services/stickers"
    19  	"github.com/status-im/status-go/services/wallet/async"
    20  	"github.com/status-im/status-go/services/wallet/collectibles"
    21  	walletCommon "github.com/status-im/status-go/services/wallet/common"
    22  	"github.com/status-im/status-go/services/wallet/market"
    23  	"github.com/status-im/status-go/services/wallet/requests"
    24  	"github.com/status-im/status-go/services/wallet/responses"
    25  	"github.com/status-im/status-go/services/wallet/router/fees"
    26  	"github.com/status-im/status-go/services/wallet/router/pathprocessor"
    27  	"github.com/status-im/status-go/services/wallet/router/routes"
    28  	"github.com/status-im/status-go/services/wallet/router/sendtype"
    29  	"github.com/status-im/status-go/services/wallet/token"
    30  	walletToken "github.com/status-im/status-go/services/wallet/token"
    31  	"github.com/status-im/status-go/signal"
    32  	"github.com/status-im/status-go/transactions"
    33  )
    34  
    35  var (
    36  	routerTask = async.TaskType{
    37  		ID:     1,
    38  		Policy: async.ReplacementPolicyCancelOld,
    39  	}
    40  )
    41  
    42  type amountOption struct {
    43  	amount       *big.Int
    44  	locked       bool
    45  	subtractFees bool
    46  }
    47  
    48  func makeBalanceKey(chainID uint64, symbol string) string {
    49  	return fmt.Sprintf("%d-%s", chainID, symbol)
    50  }
    51  
    52  type ProcessorError struct {
    53  	ProcessorName string
    54  	Error         error
    55  }
    56  
    57  type SuggestedRoutes struct {
    58  	Uuid          string
    59  	Best          routes.Route
    60  	Candidates    routes.Route
    61  	UpdatedPrices map[string]float64
    62  }
    63  
    64  type Router struct {
    65  	rpcClient           *rpc.Client
    66  	tokenManager        *token.Manager
    67  	marketManager       *market.Manager
    68  	collectiblesService *collectibles.Service
    69  	collectiblesManager *collectibles.Manager
    70  	ensService          *ens.Service
    71  	stickersService     *stickers.Service
    72  	feesManager         *fees.FeeManager
    73  	pathProcessors      map[string]pathprocessor.PathProcessor
    74  	scheduler           *async.Scheduler
    75  
    76  	activeBalanceMap sync.Map // map[string]*big.Int
    77  
    78  	activeRoutesMutex sync.Mutex
    79  	activeRoutes      *SuggestedRoutes
    80  
    81  	lastInputParamsMutex sync.Mutex
    82  	lastInputParams      *requests.RouteInputParams
    83  
    84  	clientsForUpdatesPerChains sync.Map
    85  }
    86  
    87  func NewRouter(rpcClient *rpc.Client, transactor *transactions.Transactor, tokenManager *token.Manager, marketManager *market.Manager,
    88  	collectibles *collectibles.Service, collectiblesManager *collectibles.Manager, ensService *ens.Service, stickersService *stickers.Service) *Router {
    89  	processors := make(map[string]pathprocessor.PathProcessor)
    90  
    91  	return &Router{
    92  		rpcClient:           rpcClient,
    93  		tokenManager:        tokenManager,
    94  		marketManager:       marketManager,
    95  		collectiblesService: collectibles,
    96  		collectiblesManager: collectiblesManager,
    97  		ensService:          ensService,
    98  		stickersService:     stickersService,
    99  		feesManager: &fees.FeeManager{
   100  			RPCClient: rpcClient,
   101  		},
   102  		pathProcessors: processors,
   103  		scheduler:      async.NewScheduler(),
   104  	}
   105  }
   106  
   107  func (r *Router) AddPathProcessor(processor pathprocessor.PathProcessor) {
   108  	r.pathProcessors[processor.Name()] = processor
   109  }
   110  
   111  func (r *Router) Stop() {
   112  	r.scheduler.Stop()
   113  }
   114  
   115  func (r *Router) GetFeesManager() *fees.FeeManager {
   116  	return r.feesManager
   117  }
   118  
   119  func (r *Router) GetPathProcessors() map[string]pathprocessor.PathProcessor {
   120  	return r.pathProcessors
   121  }
   122  
   123  func (r *Router) SetTestBalanceMap(balanceMap map[string]*big.Int) {
   124  	for k, v := range balanceMap {
   125  		r.activeBalanceMap.Store(k, v)
   126  	}
   127  }
   128  
   129  func newSuggestedRoutes(
   130  	input *requests.RouteInputParams,
   131  	candidates routes.Route,
   132  	updatedPrices map[string]float64,
   133  ) (*SuggestedRoutes, []routes.Route) {
   134  	suggestedRoutes := &SuggestedRoutes{
   135  		Uuid:          input.Uuid,
   136  		Candidates:    candidates,
   137  		UpdatedPrices: updatedPrices,
   138  	}
   139  	if len(candidates) == 0 {
   140  		return suggestedRoutes, nil
   141  	}
   142  
   143  	node := &routes.Node{
   144  		Path:     nil,
   145  		Children: routes.BuildGraph(input.AmountIn.ToInt(), candidates, 0, []uint64{}),
   146  	}
   147  	allRoutes := node.BuildAllRoutes()
   148  	allRoutes = filterRoutes(allRoutes, input.AmountIn.ToInt(), input.FromLockedAmount)
   149  
   150  	if len(allRoutes) > 0 {
   151  		sort.Slice(allRoutes, func(i, j int) bool {
   152  			iRoute := getRoutePriority(allRoutes[i])
   153  			jRoute := getRoutePriority(allRoutes[j])
   154  			return iRoute <= jRoute
   155  		})
   156  	}
   157  
   158  	return suggestedRoutes, allRoutes
   159  }
   160  
   161  func sendRouterResult(uuid string, result interface{}, err error) {
   162  	routesResponse := responses.RouterSuggestedRoutes{
   163  		Uuid: uuid,
   164  	}
   165  
   166  	if err != nil {
   167  		errorResponse := errors.CreateErrorResponseFromError(err)
   168  		routesResponse.ErrorResponse = errorResponse.(*errors.ErrorResponse)
   169  	}
   170  
   171  	if suggestedRoutes, ok := result.(*SuggestedRoutes); ok && suggestedRoutes != nil {
   172  		routesResponse.Best = suggestedRoutes.Best
   173  		routesResponse.Candidates = suggestedRoutes.Candidates
   174  		routesResponse.UpdatedPrices = suggestedRoutes.UpdatedPrices
   175  	}
   176  
   177  	signal.SendWalletEvent(signal.SuggestedRoutes, routesResponse)
   178  }
   179  
   180  func (r *Router) SuggestedRoutesAsync(input *requests.RouteInputParams) {
   181  	r.scheduler.Enqueue(routerTask, func(ctx context.Context) (interface{}, error) {
   182  		return r.SuggestedRoutes(ctx, input)
   183  	}, func(result interface{}, taskType async.TaskType, err error) {
   184  		sendRouterResult(input.Uuid, result, err)
   185  	})
   186  }
   187  
   188  func (r *Router) StopSuggestedRoutesAsyncCalculation() {
   189  	r.unsubscribeFeesUpdateAccrossAllChains()
   190  	r.scheduler.Stop()
   191  }
   192  
   193  func (r *Router) StopSuggestedRoutesCalculation() {
   194  	r.unsubscribeFeesUpdateAccrossAllChains()
   195  }
   196  
   197  func (r *Router) SuggestedRoutes(ctx context.Context, input *requests.RouteInputParams) (suggestedRoutes *SuggestedRoutes, err error) {
   198  	// unsubscribe from updates
   199  	r.unsubscribeFeesUpdateAccrossAllChains()
   200  
   201  	// clear all processors
   202  	for _, processor := range r.pathProcessors {
   203  		if clearable, ok := processor.(pathprocessor.PathProcessorClearable); ok {
   204  			clearable.Clear()
   205  		}
   206  	}
   207  
   208  	r.lastInputParamsMutex.Lock()
   209  	r.lastInputParams = input
   210  	r.lastInputParamsMutex.Unlock()
   211  
   212  	defer func() {
   213  		r.activeRoutesMutex.Lock()
   214  		r.activeRoutes = suggestedRoutes
   215  		r.activeRoutesMutex.Unlock()
   216  		if suggestedRoutes != nil && err == nil {
   217  			// subscribe for updates
   218  			for _, path := range suggestedRoutes.Best {
   219  				err = r.subscribeForUdates(path.FromChain.ChainID)
   220  			}
   221  		}
   222  	}()
   223  
   224  	testnetMode, err := r.rpcClient.NetworkManager.GetTestNetworksEnabled()
   225  	if err != nil {
   226  		return nil, errors.CreateErrorResponseFromError(err)
   227  	}
   228  
   229  	input.TestnetMode = testnetMode
   230  
   231  	err = input.Validate()
   232  	if err != nil {
   233  		return nil, errors.CreateErrorResponseFromError(err)
   234  	}
   235  
   236  	selectedFromChains, selectedToChains, err := r.getSelectedChains(input)
   237  	if err != nil {
   238  		return nil, errors.CreateErrorResponseFromError(err)
   239  	}
   240  
   241  	err = r.prepareBalanceMapForTokenOnChains(ctx, input, selectedFromChains)
   242  	// return only if there are no balances, otherwise try to resolve the candidates for chains we know the balances for
   243  	noBalanceOnAnyChain := true
   244  	r.activeBalanceMap.Range(func(key, value interface{}) bool {
   245  		if value.(*big.Int).Cmp(pathprocessor.ZeroBigIntValue) > 0 {
   246  			noBalanceOnAnyChain = false
   247  			return false
   248  		}
   249  		return true
   250  	})
   251  	if noBalanceOnAnyChain {
   252  		if err != nil {
   253  			return nil, errors.CreateErrorResponseFromError(err)
   254  		}
   255  		return nil, ErrNoPositiveBalance
   256  	}
   257  
   258  	candidates, processorErrors, err := r.resolveCandidates(ctx, input, selectedFromChains, selectedToChains)
   259  	if err != nil {
   260  		return nil, errors.CreateErrorResponseFromError(err)
   261  	}
   262  
   263  	suggestedRoutes, err = r.resolveRoutes(ctx, input, candidates)
   264  
   265  	if err == nil && (suggestedRoutes == nil || len(suggestedRoutes.Best) == 0) {
   266  		// No best route found, but no error given.
   267  		if len(processorErrors) > 0 {
   268  			// Return one of the path processor errors if present.
   269  			// Give precedence to the custom error message.
   270  			for _, processorError := range processorErrors {
   271  				if processorError.Error != nil && pathprocessor.IsCustomError(processorError.Error) {
   272  					err = processorError.Error
   273  					break
   274  				}
   275  			}
   276  			if err == nil {
   277  				err = errors.CreateErrorResponseFromError(processorErrors[0].Error)
   278  			}
   279  		} else {
   280  			err = ErrNoBestRouteFound
   281  		}
   282  	}
   283  
   284  	mapError := func(err error) error {
   285  		if err == nil {
   286  			return nil
   287  		}
   288  		pattern := "insufficient funds for gas * price + value: address "
   289  		addressIndex := strings.Index(errors.DetailsFromError(err), pattern)
   290  		if addressIndex != -1 {
   291  			addressIndex += len(pattern) + walletCommon.HexAddressLength
   292  			return errors.CreateErrorResponseFromError(&errors.ErrorResponse{
   293  				Code:    errors.ErrorCodeFromError(err),
   294  				Details: errors.DetailsFromError(err)[:addressIndex],
   295  			})
   296  		}
   297  		return err
   298  	}
   299  	// map some errors to more user-friendly messages
   300  	return suggestedRoutes, mapError(err)
   301  }
   302  
   303  // prepareBalanceMapForTokenOnChains prepares the balance map for passed address, where the key is in format "chainID-tokenSymbol" and
   304  // value is the balance of the token. Native token (EHT) is always added to the balance map.
   305  func (r *Router) prepareBalanceMapForTokenOnChains(ctx context.Context, input *requests.RouteInputParams, selectedFromChains []*params.Network) (err error) {
   306  	// clear the active balance map
   307  	r.activeBalanceMap = sync.Map{}
   308  
   309  	if input.TestsMode {
   310  		for k, v := range input.TestParams.BalanceMap {
   311  			r.activeBalanceMap.Store(k, v)
   312  		}
   313  		return nil
   314  	}
   315  
   316  	chainError := func(chainId uint64, token string, intErr error) {
   317  		if err == nil {
   318  			err = fmt.Errorf("chain %d, token %s: %w", chainId, token, intErr)
   319  		} else {
   320  			err = fmt.Errorf("%s; chain %d, token %s: %w", err.Error(), chainId, token, intErr)
   321  		}
   322  	}
   323  
   324  	for _, chain := range selectedFromChains {
   325  		// check token existence
   326  		token := input.SendType.FindToken(r.tokenManager, r.collectiblesService, input.AddrFrom, chain, input.TokenID)
   327  		if token == nil {
   328  			chainError(chain.ChainID, input.TokenID, ErrTokenNotFound)
   329  			continue
   330  		}
   331  		// check native token existence
   332  		nativeToken := r.tokenManager.FindToken(chain, chain.NativeCurrencySymbol)
   333  		if nativeToken == nil {
   334  			chainError(chain.ChainID, chain.NativeCurrencySymbol, ErrNativeTokenNotFound)
   335  			continue
   336  		}
   337  
   338  		// add token balance for the chain
   339  		var tokenBalance *big.Int
   340  		if input.SendType == sendtype.ERC721Transfer {
   341  			tokenBalance = big.NewInt(1)
   342  		} else if input.SendType == sendtype.ERC1155Transfer {
   343  			tokenBalance, err = r.getERC1155Balance(ctx, chain, token, input.AddrFrom)
   344  			if err != nil {
   345  				chainError(chain.ChainID, token.Symbol, errors.CreateErrorResponseFromError(err))
   346  			}
   347  		} else {
   348  			tokenBalance, err = r.getBalance(ctx, chain.ChainID, token, input.AddrFrom)
   349  			if err != nil {
   350  				chainError(chain.ChainID, token.Symbol, errors.CreateErrorResponseFromError(err))
   351  			}
   352  		}
   353  		// add only if balance is not nil
   354  		if tokenBalance != nil {
   355  			r.activeBalanceMap.Store(makeBalanceKey(chain.ChainID, token.Symbol), tokenBalance)
   356  		}
   357  
   358  		if token.IsNative() {
   359  			continue
   360  		}
   361  
   362  		// add native token balance for the chain
   363  		nativeBalance, err := r.getBalance(ctx, chain.ChainID, nativeToken, input.AddrFrom)
   364  		if err != nil {
   365  			chainError(chain.ChainID, token.Symbol, errors.CreateErrorResponseFromError(err))
   366  		}
   367  		// add only if balance is not nil
   368  		if nativeBalance != nil {
   369  			r.activeBalanceMap.Store(makeBalanceKey(chain.ChainID, nativeToken.Symbol), nativeBalance)
   370  		}
   371  	}
   372  
   373  	return
   374  }
   375  
   376  func (r *Router) getSelectedUnlockedChains(input *requests.RouteInputParams, processingChain *params.Network, selectedFromChains []*params.Network) []*params.Network {
   377  	selectedButNotLockedChains := []*params.Network{processingChain} // always add the processing chain at the beginning
   378  	for _, net := range selectedFromChains {
   379  		if net.ChainID == processingChain.ChainID {
   380  			continue
   381  		}
   382  		if _, ok := input.FromLockedAmount[net.ChainID]; !ok {
   383  			selectedButNotLockedChains = append(selectedButNotLockedChains, net)
   384  		}
   385  	}
   386  	return selectedButNotLockedChains
   387  }
   388  
   389  func (r *Router) getOptionsForAmoutToSplitAccrossChainsForProcessingChain(input *requests.RouteInputParams, amountToSplit *big.Int, processingChain *params.Network,
   390  	selectedFromChains []*params.Network) map[uint64][]amountOption {
   391  	selectedButNotLockedChains := r.getSelectedUnlockedChains(input, processingChain, selectedFromChains)
   392  
   393  	crossChainAmountOptions := make(map[uint64][]amountOption)
   394  	for _, chain := range selectedButNotLockedChains {
   395  		var (
   396  			ok           bool
   397  			tokenBalance *big.Int
   398  		)
   399  
   400  		value, ok := r.activeBalanceMap.Load(makeBalanceKey(chain.ChainID, input.TokenID))
   401  		if !ok {
   402  			continue
   403  		}
   404  		tokenBalance, ok = value.(*big.Int)
   405  		if !ok {
   406  			continue
   407  		}
   408  
   409  		if tokenBalance.Cmp(pathprocessor.ZeroBigIntValue) > 0 {
   410  			if tokenBalance.Cmp(amountToSplit) <= 0 {
   411  				crossChainAmountOptions[chain.ChainID] = append(crossChainAmountOptions[chain.ChainID], amountOption{
   412  					amount:       tokenBalance,
   413  					locked:       false,
   414  					subtractFees: true, // for chains where we're taking the full balance, we want to subtract the fees
   415  				})
   416  				amountToSplit = new(big.Int).Sub(amountToSplit, tokenBalance)
   417  			} else if amountToSplit.Cmp(pathprocessor.ZeroBigIntValue) > 0 {
   418  				crossChainAmountOptions[chain.ChainID] = append(crossChainAmountOptions[chain.ChainID], amountOption{
   419  					amount: amountToSplit,
   420  					locked: false,
   421  				})
   422  				// break since amountToSplit is fully addressed and the rest is 0
   423  				break
   424  			}
   425  		}
   426  	}
   427  
   428  	return crossChainAmountOptions
   429  }
   430  
   431  func (r *Router) getCrossChainsOptionsForSendingAmount(input *requests.RouteInputParams, selectedFromChains []*params.Network) map[uint64][]amountOption {
   432  	// All we do in this block we're free to do, because of the validateInputData function which checks if the locked amount
   433  	// was properly set and if there is something unexpected it will return an error and we will not reach this point
   434  	finalCrossChainAmountOptions := make(map[uint64][]amountOption) // represents all possible amounts that can be sent from the "from" chain
   435  
   436  	for _, selectedFromChain := range selectedFromChains {
   437  
   438  		amountLocked := false
   439  		amountToSend := input.AmountIn.ToInt()
   440  
   441  		if amountToSend.Cmp(pathprocessor.ZeroBigIntValue) == 0 {
   442  			finalCrossChainAmountOptions[selectedFromChain.ChainID] = append(finalCrossChainAmountOptions[selectedFromChain.ChainID], amountOption{
   443  				amount: amountToSend,
   444  				locked: false,
   445  			})
   446  			continue
   447  		}
   448  
   449  		lockedAmount, fromChainLocked := input.FromLockedAmount[selectedFromChain.ChainID]
   450  		if fromChainLocked {
   451  			amountToSend = lockedAmount.ToInt()
   452  			amountLocked = true
   453  		} else if len(input.FromLockedAmount) > 0 {
   454  			for chainID, lockedAmount := range input.FromLockedAmount {
   455  				if chainID == selectedFromChain.ChainID {
   456  					continue
   457  				}
   458  				amountToSend = new(big.Int).Sub(amountToSend, lockedAmount.ToInt())
   459  			}
   460  		}
   461  
   462  		if amountToSend.Cmp(pathprocessor.ZeroBigIntValue) > 0 {
   463  			// add full amount always, cause we want to check for balance errors at the end of the routing algorithm
   464  			// TODO: once we introduce bettwer error handling and start checking for the balance at the beginning of the routing algorithm
   465  			// we can remove this line and optimize the routing algorithm more
   466  			finalCrossChainAmountOptions[selectedFromChain.ChainID] = append(finalCrossChainAmountOptions[selectedFromChain.ChainID], amountOption{
   467  				amount: amountToSend,
   468  				locked: amountLocked,
   469  			})
   470  
   471  			if amountLocked {
   472  				continue
   473  			}
   474  
   475  			// If the amount that need to be send is bigger than the balance on the chain, then we want to check options if that
   476  			// amount can be splitted and sent across multiple chains.
   477  			if input.SendType == sendtype.Transfer && len(selectedFromChains) > 1 {
   478  				// All we do in this block we're free to do, because of the validateInputData function which checks if the locked amount
   479  				// was properly set and if there is something unexpected it will return an error and we will not reach this point
   480  				amountToSplitAccrossChains := new(big.Int).Set(amountToSend)
   481  
   482  				crossChainAmountOptions := r.getOptionsForAmoutToSplitAccrossChainsForProcessingChain(input, amountToSend, selectedFromChain, selectedFromChains)
   483  
   484  				// sum up all the allocated amounts accorss all chains
   485  				allocatedAmount := big.NewInt(0)
   486  				for _, amountOptions := range crossChainAmountOptions {
   487  					for _, amountOption := range amountOptions {
   488  						allocatedAmount = new(big.Int).Add(allocatedAmount, amountOption.amount)
   489  					}
   490  				}
   491  
   492  				// if the allocated amount is the same as the amount that need to be sent, then we can add the options to the finalCrossChainAmountOptions
   493  				if allocatedAmount.Cmp(amountToSplitAccrossChains) == 0 {
   494  					for cID, amountOptions := range crossChainAmountOptions {
   495  						finalCrossChainAmountOptions[cID] = append(finalCrossChainAmountOptions[cID], amountOptions...)
   496  					}
   497  				}
   498  			}
   499  		}
   500  	}
   501  
   502  	return finalCrossChainAmountOptions
   503  }
   504  
   505  func (r *Router) findOptionsForSendingAmount(input *requests.RouteInputParams, selectedFromChains []*params.Network) (map[uint64][]amountOption, error) {
   506  
   507  	crossChainAmountOptions := r.getCrossChainsOptionsForSendingAmount(input, selectedFromChains)
   508  
   509  	// filter out duplicates values for the same chain
   510  	for chainID, amountOptions := range crossChainAmountOptions {
   511  		uniqueAmountOptions := make(map[string]amountOption)
   512  		for _, amountOption := range amountOptions {
   513  			uniqueAmountOptions[amountOption.amount.String()] = amountOption
   514  		}
   515  
   516  		crossChainAmountOptions[chainID] = make([]amountOption, 0)
   517  		for _, amountOption := range uniqueAmountOptions {
   518  			crossChainAmountOptions[chainID] = append(crossChainAmountOptions[chainID], amountOption)
   519  		}
   520  	}
   521  
   522  	return crossChainAmountOptions, nil
   523  }
   524  
   525  func (r *Router) getSelectedChains(input *requests.RouteInputParams) (selectedFromChains []*params.Network, selectedToChains []*params.Network, err error) {
   526  	var networks []*params.Network
   527  	networks, err = r.rpcClient.NetworkManager.Get(false)
   528  	if err != nil {
   529  		return nil, nil, errors.CreateErrorResponseFromError(err)
   530  	}
   531  
   532  	for _, network := range networks {
   533  		if network.IsTest != input.TestnetMode {
   534  			continue
   535  		}
   536  
   537  		if !walletCommon.ArrayContainsElement(network.ChainID, input.DisabledFromChainIDs) {
   538  			selectedFromChains = append(selectedFromChains, network)
   539  		}
   540  
   541  		if !walletCommon.ArrayContainsElement(network.ChainID, input.DisabledToChainIDs) {
   542  			selectedToChains = append(selectedToChains, network)
   543  		}
   544  	}
   545  
   546  	return selectedFromChains, selectedToChains, nil
   547  }
   548  
   549  func (r *Router) resolveCandidates(ctx context.Context, input *requests.RouteInputParams, selectedFromChains []*params.Network,
   550  	selectedToChains []*params.Network) (candidates routes.Route, processorErrors []*ProcessorError, err error) {
   551  	var (
   552  		testsMode = input.TestsMode && input.TestParams != nil
   553  		group     = async.NewAtomicGroup(ctx)
   554  		mu        sync.Mutex
   555  	)
   556  
   557  	crossChainAmountOptions, err := r.findOptionsForSendingAmount(input, selectedFromChains)
   558  	if err != nil {
   559  		return nil, nil, errors.CreateErrorResponseFromError(err)
   560  	}
   561  
   562  	appendProcessorErrorFn := func(processorName string, sendType sendtype.SendType, fromChainID uint64, toChainID uint64, amount *big.Int, err error) {
   563  		log.Error("router.resolveCandidates error", "processor", processorName, "sendType", sendType, "fromChainId: ", fromChainID, "toChainId", toChainID, "amount", amount, "err", err)
   564  		mu.Lock()
   565  		defer mu.Unlock()
   566  		processorErrors = append(processorErrors, &ProcessorError{
   567  			ProcessorName: processorName,
   568  			Error:         err,
   569  		})
   570  	}
   571  
   572  	appendPathFn := func(path *routes.Path) {
   573  		mu.Lock()
   574  		defer mu.Unlock()
   575  		candidates = append(candidates, path)
   576  	}
   577  
   578  	for networkIdx := range selectedFromChains {
   579  		network := selectedFromChains[networkIdx]
   580  
   581  		if !input.SendType.IsAvailableFor(network) {
   582  			continue
   583  		}
   584  
   585  		var (
   586  			token   *walletToken.Token
   587  			toToken *walletToken.Token
   588  		)
   589  
   590  		if testsMode {
   591  			token = input.TestParams.TokenFrom
   592  		} else {
   593  			token = input.SendType.FindToken(r.tokenManager, r.collectiblesService, input.AddrFrom, network, input.TokenID)
   594  		}
   595  		if token == nil {
   596  			continue
   597  		}
   598  
   599  		if input.SendType == sendtype.Swap {
   600  			toToken = input.SendType.FindToken(r.tokenManager, r.collectiblesService, common.Address{}, network, input.ToTokenID)
   601  		}
   602  
   603  		var fetchedFees *fees.SuggestedFees
   604  		if testsMode {
   605  			fetchedFees = input.TestParams.SuggestedFees
   606  		} else {
   607  			fetchedFees, err = r.feesManager.SuggestedFees(ctx, network.ChainID)
   608  			if err != nil {
   609  				continue
   610  			}
   611  		}
   612  
   613  		group.Add(func(c context.Context) error {
   614  			for _, amountOption := range crossChainAmountOptions[network.ChainID] {
   615  				for _, pProcessor := range r.pathProcessors {
   616  					// With the condition below we're eliminating `Swap` as potential path that can participate in calculating the best route
   617  					// once we decide to inlcude `Swap` in the calculation we need to update `canUseProcessor` function.
   618  					// This also applies to including another (Celer) bridge in the calculation.
   619  					// TODO:
   620  					// this algorithm, includeing finding the best route, has to be updated to include more bridges and one (for now) or more swap options
   621  					// it means that candidates should not be treated linearly, but improve the logic to have multiple routes with different processors of the same type.
   622  					// Example:
   623  					// Routes for sending SNT from Ethereum to Optimism can be:
   624  					// 1. Swap SNT(mainnet) to ETH(mainnet); then bridge via Hop ETH(mainnet) to ETH(opt); then Swap ETH(opt) to SNT(opt); then send SNT (opt) to the destination
   625  					// 2. Swap SNT(mainnet) to ETH(mainnet); then bridge via Celer ETH(mainnet) to ETH(opt); then Swap ETH(opt) to SNT(opt); then send SNT (opt) to the destination
   626  					// 3. Swap SNT(mainnet) to USDC(mainnet); then bridge via Hop USDC(mainnet) to USDC(opt); then Swap USDC(opt) to SNT(opt); then send SNT (opt) to the destination
   627  					// 4. Swap SNT(mainnet) to USDC(mainnet); then bridge via Celer USDC(mainnet) to USDC(opt); then Swap USDC(opt) to SNT(opt); then send SNT (opt) to the destination
   628  					// 5. ...
   629  					// 6. ...
   630  					//
   631  					// With the current routing algorithm atm we're not able to generate all possible routes.
   632  					if !input.SendType.CanUseProcessor(pProcessor) {
   633  						continue
   634  					}
   635  
   636  					// if we're doing a single chain operation, we can skip bridge processors
   637  					if walletCommon.IsSingleChainOperation(selectedFromChains, selectedToChains) && pathprocessor.IsProcessorBridge(pProcessor.Name()) {
   638  						continue
   639  					}
   640  
   641  					if !input.SendType.ProcessZeroAmountInProcessor(amountOption.amount, input.AmountOut.ToInt(), pProcessor.Name()) {
   642  						continue
   643  					}
   644  
   645  					for _, dest := range selectedToChains {
   646  
   647  						if !input.SendType.IsAvailableFor(network) {
   648  							continue
   649  						}
   650  
   651  						if !input.SendType.IsAvailableBetween(network, dest) {
   652  							continue
   653  						}
   654  
   655  						processorInputParams := pathprocessor.ProcessorInputParams{
   656  							FromChain: network,
   657  							ToChain:   dest,
   658  							FromToken: token,
   659  							ToToken:   toToken,
   660  							ToAddr:    input.AddrTo,
   661  							FromAddr:  input.AddrFrom,
   662  							AmountIn:  amountOption.amount,
   663  							AmountOut: input.AmountOut.ToInt(),
   664  
   665  							Username:  input.Username,
   666  							PublicKey: input.PublicKey,
   667  							PackID:    input.PackID.ToInt(),
   668  						}
   669  						if input.TestsMode {
   670  							processorInputParams.TestsMode = input.TestsMode
   671  							processorInputParams.TestEstimationMap = input.TestParams.EstimationMap
   672  							processorInputParams.TestBonderFeeMap = input.TestParams.BonderFeeMap
   673  							processorInputParams.TestApprovalGasEstimation = input.TestParams.ApprovalGasEstimation
   674  							processorInputParams.TestApprovalL1Fee = input.TestParams.ApprovalL1Fee
   675  						}
   676  
   677  						can, err := pProcessor.AvailableFor(processorInputParams)
   678  						if err != nil {
   679  							appendProcessorErrorFn(pProcessor.Name(), input.SendType, processorInputParams.FromChain.ChainID, processorInputParams.ToChain.ChainID, processorInputParams.AmountIn, err)
   680  							continue
   681  						}
   682  						if !can {
   683  							continue
   684  						}
   685  
   686  						bonderFees, tokenFees, err := pProcessor.CalculateFees(processorInputParams)
   687  						if err != nil {
   688  							appendProcessorErrorFn(pProcessor.Name(), input.SendType, processorInputParams.FromChain.ChainID, processorInputParams.ToChain.ChainID, processorInputParams.AmountIn, err)
   689  							continue
   690  						}
   691  
   692  						gasLimit, err := pProcessor.EstimateGas(processorInputParams)
   693  						if err != nil {
   694  							appendProcessorErrorFn(pProcessor.Name(), input.SendType, processorInputParams.FromChain.ChainID, processorInputParams.ToChain.ChainID, processorInputParams.AmountIn, err)
   695  							continue
   696  						}
   697  
   698  						approvalContractAddress, err := pProcessor.GetContractAddress(processorInputParams)
   699  						if err != nil {
   700  							appendProcessorErrorFn(pProcessor.Name(), input.SendType, processorInputParams.FromChain.ChainID, processorInputParams.ToChain.ChainID, processorInputParams.AmountIn, err)
   701  							continue
   702  						}
   703  						approvalRequired, approvalAmountRequired, err := r.requireApproval(ctx, input.SendType, &approvalContractAddress, processorInputParams)
   704  						if err != nil {
   705  							appendProcessorErrorFn(pProcessor.Name(), input.SendType, processorInputParams.FromChain.ChainID, processorInputParams.ToChain.ChainID, processorInputParams.AmountIn, err)
   706  							continue
   707  						}
   708  
   709  						var approvalGasLimit uint64
   710  						if approvalRequired {
   711  							if processorInputParams.TestsMode {
   712  								approvalGasLimit = processorInputParams.TestApprovalGasEstimation
   713  							} else {
   714  								approvalGasLimit, err = r.estimateGasForApproval(processorInputParams, &approvalContractAddress)
   715  								if err != nil {
   716  									appendProcessorErrorFn(pProcessor.Name(), input.SendType, processorInputParams.FromChain.ChainID, processorInputParams.ToChain.ChainID, processorInputParams.AmountIn, err)
   717  									continue
   718  								}
   719  							}
   720  						}
   721  
   722  						amountOut, err := pProcessor.CalculateAmountOut(processorInputParams)
   723  						if err != nil {
   724  							appendProcessorErrorFn(pProcessor.Name(), input.SendType, processorInputParams.FromChain.ChainID, processorInputParams.ToChain.ChainID, processorInputParams.AmountIn, err)
   725  							continue
   726  						}
   727  
   728  						maxFeesPerGas := fetchedFees.FeeFor(input.GasFeeMode)
   729  
   730  						estimatedTime := r.feesManager.TransactionEstimatedTime(ctx, network.ChainID, maxFeesPerGas)
   731  						if approvalRequired && estimatedTime < fees.MoreThanFiveMinutes {
   732  							estimatedTime += 1
   733  						}
   734  
   735  						path := &routes.Path{
   736  							ProcessorName:  pProcessor.Name(),
   737  							FromChain:      network,
   738  							ToChain:        dest,
   739  							FromToken:      token,
   740  							ToToken:        toToken,
   741  							AmountIn:       (*hexutil.Big)(amountOption.amount),
   742  							AmountInLocked: amountOption.locked,
   743  							AmountOut:      (*hexutil.Big)(amountOut),
   744  
   745  							// set params that we don't want to be recalculated with every new block creation
   746  							TxGasAmount:  gasLimit,
   747  							TxBonderFees: (*hexutil.Big)(bonderFees),
   748  							TxTokenFees:  (*hexutil.Big)(tokenFees),
   749  
   750  							ApprovalRequired:        approvalRequired,
   751  							ApprovalAmountRequired:  (*hexutil.Big)(approvalAmountRequired),
   752  							ApprovalContractAddress: &approvalContractAddress,
   753  							ApprovalGasAmount:       approvalGasLimit,
   754  
   755  							EstimatedTime: estimatedTime,
   756  
   757  							SubtractFees: amountOption.subtractFees,
   758  						}
   759  
   760  						err = r.cacluateFees(ctx, path, fetchedFees, processorInputParams.TestsMode, processorInputParams.TestApprovalL1Fee)
   761  						if err != nil {
   762  							appendProcessorErrorFn(pProcessor.Name(), input.SendType, processorInputParams.FromChain.ChainID, processorInputParams.ToChain.ChainID, processorInputParams.AmountIn, err)
   763  							continue
   764  						}
   765  
   766  						appendPathFn(path)
   767  					}
   768  				}
   769  			}
   770  			return nil
   771  		})
   772  	}
   773  
   774  	sort.Slice(candidates, func(i, j int) bool {
   775  		iChain := getChainPriority(candidates[i].FromChain.ChainID)
   776  		jChain := getChainPriority(candidates[j].FromChain.ChainID)
   777  		return iChain <= jChain
   778  	})
   779  
   780  	group.Wait()
   781  	return candidates, processorErrors, nil
   782  }
   783  
   784  func (r *Router) checkBalancesForTheBestRoute(ctx context.Context, bestRoute routes.Route) (hasPositiveBalance bool, err error) {
   785  	// make a copy of the active balance map
   786  	balanceMapCopy := make(map[string]*big.Int)
   787  	r.activeBalanceMap.Range(func(k, v interface{}) bool {
   788  		balanceMapCopy[k.(string)] = new(big.Int).Set(v.(*big.Int))
   789  		return true
   790  	})
   791  	if balanceMapCopy == nil {
   792  		return false, ErrCannotCheckBalance
   793  	}
   794  
   795  	// check the best route for the required balances
   796  	for _, path := range bestRoute {
   797  		tokenKey := makeBalanceKey(path.FromChain.ChainID, path.FromToken.Symbol)
   798  		if tokenBalance, ok := balanceMapCopy[tokenKey]; ok {
   799  			if tokenBalance.Cmp(pathprocessor.ZeroBigIntValue) > 0 {
   800  				hasPositiveBalance = true
   801  			}
   802  		}
   803  
   804  		if path.ProcessorName == pathprocessor.ProcessorBridgeHopName {
   805  			if path.TxBonderFees.ToInt().Cmp(path.AmountOut.ToInt()) > 0 {
   806  				return hasPositiveBalance, ErrLowAmountInForHopBridge
   807  			}
   808  		}
   809  
   810  		if path.RequiredTokenBalance != nil && path.RequiredTokenBalance.Cmp(pathprocessor.ZeroBigIntValue) > 0 {
   811  			if tokenBalance, ok := balanceMapCopy[tokenKey]; ok {
   812  				if tokenBalance.Cmp(path.RequiredTokenBalance) == -1 {
   813  					err := &errors.ErrorResponse{
   814  						Code:    ErrNotEnoughTokenBalance.Code,
   815  						Details: fmt.Sprintf(ErrNotEnoughTokenBalance.Details, path.FromToken.Symbol, path.FromChain.ChainID),
   816  					}
   817  					return hasPositiveBalance, err
   818  				}
   819  				balanceMapCopy[tokenKey].Sub(tokenBalance, path.RequiredTokenBalance)
   820  			} else {
   821  				return hasPositiveBalance, ErrTokenNotFound
   822  			}
   823  		}
   824  
   825  		ethKey := makeBalanceKey(path.FromChain.ChainID, pathprocessor.EthSymbol)
   826  		if nativeBalance, ok := balanceMapCopy[ethKey]; ok {
   827  			if nativeBalance.Cmp(path.RequiredNativeBalance) == -1 {
   828  				err := &errors.ErrorResponse{
   829  					Code:    ErrNotEnoughNativeBalance.Code,
   830  					Details: fmt.Sprintf(ErrNotEnoughNativeBalance.Details, pathprocessor.EthSymbol, path.FromChain.ChainID),
   831  				}
   832  				return hasPositiveBalance, err
   833  			}
   834  			balanceMapCopy[ethKey].Sub(nativeBalance, path.RequiredNativeBalance)
   835  		} else {
   836  			return hasPositiveBalance, ErrNativeTokenNotFound
   837  		}
   838  	}
   839  
   840  	return hasPositiveBalance, nil
   841  }
   842  
   843  func (r *Router) resolveRoutes(ctx context.Context, input *requests.RouteInputParams, candidates routes.Route) (suggestedRoutes *SuggestedRoutes, err error) {
   844  	var prices map[string]float64
   845  	if input.TestsMode {
   846  		prices = input.TestParams.TokenPrices
   847  	} else {
   848  		prices, err = input.SendType.FetchPrices(r.marketManager, []string{input.TokenID, input.ToTokenID})
   849  		if err != nil {
   850  			return nil, errors.CreateErrorResponseFromError(err)
   851  		}
   852  	}
   853  
   854  	tokenPrice := prices[input.TokenID]
   855  	nativeTokenPrice := prices[pathprocessor.EthSymbol]
   856  
   857  	var allRoutes []routes.Route
   858  	suggestedRoutes, allRoutes = newSuggestedRoutes(input, candidates, prices)
   859  
   860  	defer func() {
   861  		if suggestedRoutes.Best != nil && len(suggestedRoutes.Best) > 0 {
   862  			sort.Slice(suggestedRoutes.Best, func(i, j int) bool {
   863  				iChain := getChainPriority(suggestedRoutes.Best[i].FromChain.ChainID)
   864  				jChain := getChainPriority(suggestedRoutes.Best[j].FromChain.ChainID)
   865  				return iChain <= jChain
   866  			})
   867  		}
   868  	}()
   869  
   870  	var (
   871  		bestRoute                        routes.Route
   872  		lastBestRouteWithPositiveBalance routes.Route
   873  		lastBestRouteErr                 error
   874  	)
   875  
   876  	for len(allRoutes) > 0 {
   877  		bestRoute = routes.FindBestRoute(allRoutes, tokenPrice, nativeTokenPrice)
   878  		var hasPositiveBalance bool
   879  		hasPositiveBalance, err = r.checkBalancesForTheBestRoute(ctx, bestRoute)
   880  
   881  		if err != nil {
   882  			// If it's about transfer or bridge and there is more routes, but on the best (cheapest) one there is not enugh balance
   883  			// we shold check other routes even though there are not the cheapest ones
   884  			if input.SendType == sendtype.Transfer ||
   885  				input.SendType == sendtype.Bridge {
   886  				if hasPositiveBalance {
   887  					lastBestRouteWithPositiveBalance = bestRoute
   888  					lastBestRouteErr = err
   889  				}
   890  
   891  				if len(allRoutes) > 1 {
   892  					allRoutes = removeBestRouteFromAllRouters(allRoutes, bestRoute)
   893  					continue
   894  				} else {
   895  					break
   896  				}
   897  			}
   898  		}
   899  
   900  		break
   901  	}
   902  
   903  	// if none of the routes have positive balance, we should return the last best route with positive balance
   904  	if err != nil && lastBestRouteWithPositiveBalance != nil {
   905  		bestRoute = lastBestRouteWithPositiveBalance
   906  		err = lastBestRouteErr
   907  	}
   908  
   909  	if len(bestRoute) > 0 {
   910  		// At this point we have to do the final check and update the amountIn (subtracting fees) if complete balance is going to be sent for native token (ETH)
   911  		for _, path := range bestRoute {
   912  			if path.SubtractFees && path.FromToken.IsNative() {
   913  				path.AmountIn.ToInt().Sub(path.AmountIn.ToInt(), path.TxFee.ToInt())
   914  				if path.TxL1Fee.ToInt().Cmp(pathprocessor.ZeroBigIntValue) > 0 {
   915  					path.AmountIn.ToInt().Sub(path.AmountIn.ToInt(), path.TxL1Fee.ToInt())
   916  				}
   917  				if path.ApprovalRequired {
   918  					path.AmountIn.ToInt().Sub(path.AmountIn.ToInt(), path.ApprovalFee.ToInt())
   919  					if path.ApprovalL1Fee.ToInt().Cmp(pathprocessor.ZeroBigIntValue) > 0 {
   920  						path.AmountIn.ToInt().Sub(path.AmountIn.ToInt(), path.ApprovalL1Fee.ToInt())
   921  					}
   922  				}
   923  			}
   924  		}
   925  	}
   926  	suggestedRoutes.Best = bestRoute
   927  
   928  	return suggestedRoutes, err
   929  }