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 }