2024-05-16 08:55:46 +00:00
package router
2024-05-14 19:11:16 +00:00
import (
"context"
2024-06-30 20:44:21 +00:00
"fmt"
2024-05-14 19:11:16 +00:00
"math"
"math/big"
"sort"
"sync"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
2024-07-03 09:30:17 +00:00
"github.com/status-im/status-go/errors"
2024-05-14 19:11:16 +00:00
"github.com/status-im/status-go/params"
2024-06-06 20:08:25 +00:00
"github.com/status-im/status-go/services/ens"
2024-05-14 19:11:16 +00:00
"github.com/status-im/status-go/services/wallet/async"
2024-06-05 07:56:02 +00:00
walletCommon "github.com/status-im/status-go/services/wallet/common"
2024-06-06 20:08:25 +00:00
"github.com/status-im/status-go/services/wallet/router/pathprocessor"
2024-05-14 19:11:16 +00:00
walletToken "github.com/status-im/status-go/services/wallet/token"
2024-06-20 09:03:42 +00:00
"github.com/status-im/status-go/signal"
)
var (
routerTask = async . TaskType {
ID : 1 ,
Policy : async . ReplacementPolicyCancelOld ,
}
2024-05-14 19:11:16 +00:00
)
2024-06-05 07:56:02 +00:00
var (
supportedNetworks = map [ uint64 ] bool {
walletCommon . EthereumMainnet : true ,
walletCommon . OptimismMainnet : true ,
walletCommon . ArbitrumMainnet : true ,
}
supportedTestNetworks = map [ uint64 ] bool {
walletCommon . EthereumSepolia : true ,
walletCommon . OptimismSepolia : true ,
walletCommon . ArbitrumSepolia : true ,
}
)
2024-05-16 07:37:36 +00:00
type RouteInputParams struct {
2024-06-20 09:03:42 +00:00
Uuid string ` json:"uuid" `
2024-05-16 08:55:46 +00:00
SendType SendType ` json:"sendType" validate:"required" `
AddrFrom common . Address ` json:"addrFrom" validate:"required" `
AddrTo common . Address ` json:"addrTo" validate:"required" `
AmountIn * hexutil . Big ` json:"amountIn" validate:"required" `
2024-06-12 20:14:30 +00:00
AmountOut * hexutil . Big ` json:"amountOut" `
2024-05-16 08:55:46 +00:00
TokenID string ` json:"tokenID" validate:"required" `
2024-05-16 07:37:36 +00:00
ToTokenID string ` json:"toTokenID" `
DisabledFromChainIDs [ ] uint64 ` json:"disabledFromChainIDs" `
2024-06-14 12:14:14 +00:00
DisabledToChainIDs [ ] uint64 ` json:"disabledToChainIDs" `
2024-05-16 08:55:46 +00:00
GasFeeMode GasFeeMode ` json:"gasFeeMode" validate:"required" `
2024-05-16 07:37:36 +00:00
FromLockedAmount map [ uint64 ] * hexutil . Big ` json:"fromLockedAmount" `
2024-06-18 10:31:23 +00:00
testnetMode bool
2024-06-05 07:56:02 +00:00
// For send types like EnsRegister, EnsRelease, EnsSetPubKey, StickersBuy
2024-06-13 13:38:08 +00:00
Username string ` json:"username" `
PublicKey string ` json:"publicKey" `
PackID * hexutil . Big ` json:"packID" `
2024-06-18 10:31:23 +00:00
// TODO: Remove two fields below once we implement a better solution for tests
// Currently used for tests only
testsMode bool
testParams * routerTestParams
}
type routerTestParams struct {
tokenFrom * walletToken . Token
tokenPrices map [ string ] float64
estimationMap map [ string ] uint64 // [processor-name, estimated-value]
bonderFeeMap map [ string ] * big . Int // [token-symbol, bonder-fee]
suggestedFees * SuggestedFees
baseFee * big . Int
balanceMap map [ string ] * big . Int // [token-symbol, balance]
approvalGasEstimation uint64
approvalL1Fee uint64
2024-05-16 07:37:36 +00:00
}
2024-07-04 10:48:14 +00:00
type amountOption struct {
amount * big . Int
locked bool
2024-06-30 20:44:21 +00:00
}
2024-07-04 10:48:14 +00:00
func makeBalanceKey ( chainID uint64 , symbol string ) string {
return fmt . Sprintf ( "%d-%s" , chainID , symbol )
2024-06-30 20:44:21 +00:00
}
2024-05-14 19:11:16 +00:00
type PathV2 struct {
2024-06-06 20:08:25 +00:00
ProcessorName string
2024-06-05 08:36:27 +00:00
FromChain * params . Network // Source chain
ToChain * params . Network // Destination chain
2024-06-30 21:38:32 +00:00
FromToken * walletToken . Token // Source token
ToToken * walletToken . Token // Destination token, set if applicable
2024-05-14 19:11:16 +00:00
AmountIn * hexutil . Big // Amount that will be sent from the source chain
AmountInLocked bool // Is the amount locked
AmountOut * hexutil . Big // Amount that will be received on the destination chain
2024-06-19 09:20:41 +00:00
SuggestedLevelsForMaxFeesPerGas * MaxFeesLevels // Suggested max fees for the transaction
2024-05-14 19:11:16 +00:00
TxBaseFee * hexutil . Big // Base fee for the transaction
2024-06-19 09:20:41 +00:00
TxPriorityFee * hexutil . Big // Priority fee for the transaction
2024-05-14 19:11:16 +00:00
TxGasAmount uint64 // Gas used for the transaction
TxBonderFees * hexutil . Big // Bonder fees for the transaction - used for Hop bridge
TxTokenFees * hexutil . Big // Token fees for the transaction - used for bridges (represent the difference between the amount in and the amount out)
TxL1Fee * hexutil . Big // L1 fee for the transaction - used for for transactions placed on L2 chains
ApprovalRequired bool // Is approval required for the transaction
ApprovalAmountRequired * hexutil . Big // Amount required for the approval transaction
ApprovalContractAddress * common . Address // Address of the contract that needs to be approved
ApprovalBaseFee * hexutil . Big // Base fee for the approval transaction
ApprovalPriorityFee * hexutil . Big // Priority fee for the approval transaction
ApprovalGasAmount uint64 // Gas used for the approval transaction
ApprovalL1Fee * hexutil . Big // L1 fee for the approval transaction - used for for transactions placed on L2 chains
EstimatedTime TransactionEstimation
requiredTokenBalance * big . Int
requiredNativeBalance * big . Int
}
func ( p * PathV2 ) Equal ( o * PathV2 ) bool {
2024-06-05 08:36:27 +00:00
return p . FromChain . ChainID == o . FromChain . ChainID && p . ToChain . ChainID == o . ToChain . ChainID
2024-05-14 19:11:16 +00:00
}
type SuggestedRoutesV2 struct {
2024-06-20 09:03:42 +00:00
Uuid string
2024-05-14 19:11:16 +00:00
Best [ ] * PathV2
Candidates [ ] * PathV2
TokenPrice float64
NativeChainTokenPrice float64
}
2024-06-20 09:03:42 +00:00
type ErrorResponseWithUUID struct {
Uuid string
ErrorResponse error
}
2024-07-02 14:59:11 +00:00
type GraphV2 [ ] * NodeV2
2024-05-14 19:11:16 +00:00
type NodeV2 struct {
Path * PathV2
Children GraphV2
}
func newSuggestedRoutesV2 (
2024-06-20 09:03:42 +00:00
uuid string ,
2024-05-14 19:11:16 +00:00
amountIn * big . Int ,
candidates [ ] * PathV2 ,
fromLockedAmount map [ uint64 ] * hexutil . Big ,
tokenPrice float64 ,
nativeChainTokenPrice float64 ,
2024-06-30 20:44:21 +00:00
) ( * SuggestedRoutesV2 , [ ] [ ] * PathV2 ) {
2024-05-14 19:11:16 +00:00
suggestedRoutes := & SuggestedRoutesV2 {
2024-06-20 09:03:42 +00:00
Uuid : uuid ,
2024-05-14 19:11:16 +00:00
Candidates : candidates ,
Best : candidates ,
TokenPrice : tokenPrice ,
NativeChainTokenPrice : nativeChainTokenPrice ,
}
if len ( candidates ) == 0 {
2024-06-30 20:44:21 +00:00
return suggestedRoutes , nil
2024-05-14 19:11:16 +00:00
}
node := & NodeV2 {
Path : nil ,
Children : buildGraphV2 ( amountIn , candidates , 0 , [ ] uint64 { } ) ,
}
2024-06-30 20:44:21 +00:00
allRoutes := node . buildAllRoutesV2 ( )
allRoutes = filterRoutesV2 ( allRoutes , amountIn , fromLockedAmount )
2024-05-14 19:11:16 +00:00
2024-06-30 20:44:21 +00:00
return suggestedRoutes , allRoutes
2024-05-14 19:11:16 +00:00
}
func newNodeV2 ( path * PathV2 ) * NodeV2 {
return & NodeV2 { Path : path , Children : make ( GraphV2 , 0 ) }
}
func buildGraphV2 ( AmountIn * big . Int , routes [ ] * PathV2 , level int , sourceChainIDs [ ] uint64 ) GraphV2 {
graph := make ( GraphV2 , 0 )
for _ , route := range routes {
found := false
for _ , chainID := range sourceChainIDs {
2024-06-05 08:36:27 +00:00
if chainID == route . FromChain . ChainID {
2024-05-14 19:11:16 +00:00
found = true
break
}
}
if found {
continue
}
node := newNodeV2 ( route )
newRoutes := make ( [ ] * PathV2 , 0 )
for _ , r := range routes {
if route . Equal ( r ) {
continue
}
newRoutes = append ( newRoutes , r )
}
newAmountIn := new ( big . Int ) . Sub ( AmountIn , route . AmountIn . ToInt ( ) )
if newAmountIn . Sign ( ) > 0 {
newSourceChainIDs := make ( [ ] uint64 , len ( sourceChainIDs ) )
copy ( newSourceChainIDs , sourceChainIDs )
2024-06-05 08:36:27 +00:00
newSourceChainIDs = append ( newSourceChainIDs , route . FromChain . ChainID )
2024-05-14 19:11:16 +00:00
node . Children = buildGraphV2 ( newAmountIn , newRoutes , level + 1 , newSourceChainIDs )
if len ( node . Children ) == 0 {
continue
}
}
graph = append ( graph , node )
}
return graph
}
func ( n NodeV2 ) buildAllRoutesV2 ( ) [ ] [ ] * PathV2 {
res := make ( [ ] [ ] * PathV2 , 0 )
if len ( n . Children ) == 0 && n . Path != nil {
res = append ( res , [ ] * PathV2 { n . Path } )
}
for _ , node := range n . Children {
for _ , route := range node . buildAllRoutesV2 ( ) {
extendedRoute := route
if n . Path != nil {
extendedRoute = append ( [ ] * PathV2 { n . Path } , route ... )
}
res = append ( res , extendedRoute )
}
}
return res
}
func findBestV2 ( routes [ ] [ ] * PathV2 , tokenPrice float64 , nativeChainTokenPrice float64 ) [ ] * PathV2 {
var best [ ] * PathV2
bestCost := big . NewFloat ( math . Inf ( 1 ) )
for _ , route := range routes {
currentCost := big . NewFloat ( 0 )
for _ , path := range route {
2024-05-23 12:38:39 +00:00
tokenDenominator := big . NewFloat ( math . Pow ( 10 , float64 ( path . FromToken . Decimals ) ) )
2024-06-14 11:27:53 +00:00
path . requiredTokenBalance = big . NewInt ( 0 )
2024-05-21 14:33:36 +00:00
path . requiredNativeBalance = big . NewInt ( 0 )
2024-06-14 11:27:53 +00:00
if path . FromToken . IsNative ( ) {
path . requiredNativeBalance . Add ( path . requiredNativeBalance , path . AmountIn . ToInt ( ) )
} else {
path . requiredTokenBalance . Add ( path . requiredTokenBalance , path . AmountIn . ToInt ( ) )
}
2024-05-14 19:11:16 +00:00
// ecaluate the cost of the path
pathCost := big . NewFloat ( 0 )
nativeTokenPrice := new ( big . Float ) . SetFloat64 ( nativeChainTokenPrice )
if path . TxBaseFee != nil && path . TxPriorityFee != nil {
feePerGas := new ( big . Int ) . Add ( path . TxBaseFee . ToInt ( ) , path . TxPriorityFee . ToInt ( ) )
txFeeInWei := new ( big . Int ) . Mul ( feePerGas , big . NewInt ( int64 ( path . TxGasAmount ) ) )
txFeeInEth := gweiToEth ( weiToGwei ( txFeeInWei ) )
path . requiredNativeBalance . Add ( path . requiredNativeBalance , txFeeInWei )
pathCost = new ( big . Float ) . Mul ( txFeeInEth , nativeTokenPrice )
}
2024-06-06 20:08:25 +00:00
if path . TxBonderFees != nil && path . TxBonderFees . ToInt ( ) . Cmp ( pathprocessor . ZeroBigIntValue ) > 0 {
2024-06-14 11:27:53 +00:00
if path . FromToken . IsNative ( ) {
path . requiredNativeBalance . Add ( path . requiredNativeBalance , path . TxBonderFees . ToInt ( ) )
} else {
path . requiredTokenBalance . Add ( path . requiredTokenBalance , path . TxBonderFees . ToInt ( ) )
}
2024-05-23 12:38:39 +00:00
pathCost . Add ( pathCost , new ( big . Float ) . Mul (
new ( big . Float ) . Quo ( new ( big . Float ) . SetInt ( path . TxBonderFees . ToInt ( ) ) , tokenDenominator ) ,
new ( big . Float ) . SetFloat64 ( tokenPrice ) ) )
2024-05-14 19:11:16 +00:00
}
2024-06-06 20:08:25 +00:00
if path . TxL1Fee != nil && path . TxL1Fee . ToInt ( ) . Cmp ( pathprocessor . ZeroBigIntValue ) > 0 {
2024-05-14 19:11:16 +00:00
l1FeeInWei := path . TxL1Fee . ToInt ( )
l1FeeInEth := gweiToEth ( weiToGwei ( l1FeeInWei ) )
path . requiredNativeBalance . Add ( path . requiredNativeBalance , l1FeeInWei )
pathCost . Add ( pathCost , new ( big . Float ) . Mul ( l1FeeInEth , nativeTokenPrice ) )
}
2024-06-06 20:08:25 +00:00
if path . TxTokenFees != nil && path . TxTokenFees . ToInt ( ) . Cmp ( pathprocessor . ZeroBigIntValue ) > 0 && path . FromToken != nil {
2024-06-14 11:27:53 +00:00
if path . FromToken . IsNative ( ) {
path . requiredNativeBalance . Add ( path . requiredNativeBalance , path . TxTokenFees . ToInt ( ) )
} else {
path . requiredTokenBalance . Add ( path . requiredTokenBalance , path . TxTokenFees . ToInt ( ) )
}
2024-05-14 19:11:16 +00:00
pathCost . Add ( pathCost , new ( big . Float ) . Mul (
2024-05-23 12:38:39 +00:00
new ( big . Float ) . Quo ( new ( big . Float ) . SetInt ( path . TxTokenFees . ToInt ( ) ) , tokenDenominator ) ,
2024-05-14 19:11:16 +00:00
new ( big . Float ) . SetFloat64 ( tokenPrice ) ) )
}
if path . ApprovalRequired {
if path . ApprovalBaseFee != nil && path . ApprovalPriorityFee != nil {
feePerGas := new ( big . Int ) . Add ( path . ApprovalBaseFee . ToInt ( ) , path . ApprovalPriorityFee . ToInt ( ) )
txFeeInWei := new ( big . Int ) . Mul ( feePerGas , big . NewInt ( int64 ( path . ApprovalGasAmount ) ) )
txFeeInEth := gweiToEth ( weiToGwei ( txFeeInWei ) )
path . requiredNativeBalance . Add ( path . requiredNativeBalance , txFeeInWei )
pathCost . Add ( pathCost , new ( big . Float ) . Mul ( txFeeInEth , nativeTokenPrice ) )
}
if path . ApprovalL1Fee != nil {
l1FeeInWei := path . ApprovalL1Fee . ToInt ( )
l1FeeInEth := gweiToEth ( weiToGwei ( l1FeeInWei ) )
path . requiredNativeBalance . Add ( path . requiredNativeBalance , l1FeeInWei )
pathCost . Add ( pathCost , new ( big . Float ) . Mul ( l1FeeInEth , nativeTokenPrice ) )
}
}
currentCost = new ( big . Float ) . Add ( currentCost , pathCost )
}
if currentCost . Cmp ( bestCost ) == - 1 {
best = route
bestCost = currentCost
}
}
return best
}
2024-06-05 07:56:02 +00:00
func validateInputData ( input * RouteInputParams ) error {
if input . SendType == ENSRegister {
if input . Username == "" || input . PublicKey == "" {
2024-07-02 10:25:30 +00:00
return ErrENSRegisterRequiresUsernameAndPubKey
2024-06-05 07:56:02 +00:00
}
2024-06-18 10:31:23 +00:00
if input . testnetMode {
2024-06-06 20:08:25 +00:00
if input . TokenID != pathprocessor . SttSymbol {
2024-07-02 10:25:30 +00:00
return ErrENSRegisterTestnetSTTOnly
2024-06-05 07:56:02 +00:00
}
} else {
2024-06-06 20:08:25 +00:00
if input . TokenID != pathprocessor . SntSymbol {
2024-07-02 10:25:30 +00:00
return ErrENSRegisterMainnetSNTOnly
2024-06-05 07:56:02 +00:00
}
}
return nil
}
2024-06-05 12:46:10 +00:00
if input . SendType == ENSRelease {
if input . Username == "" {
2024-07-02 10:25:30 +00:00
return ErrENSReleaseRequiresUsername
2024-06-05 12:46:10 +00:00
}
}
2024-06-06 20:08:25 +00:00
if input . SendType == ENSSetPubKey {
2024-07-02 10:25:30 +00:00
if input . Username == "" || input . PublicKey == "" {
return ErrENSSetPubKeyRequiresUsernameAndPubKey
}
if ens . ValidateENSUsername ( input . Username ) != nil {
return ErrENSSetPubKeyInvalidUsername
2024-06-06 20:08:25 +00:00
}
}
2024-06-11 08:00:17 +00:00
if input . SendType == StickersBuy {
if input . PackID == nil {
2024-07-02 10:25:30 +00:00
return ErrStickersBuyRequiresPackID
2024-06-11 08:00:17 +00:00
}
}
2024-06-12 20:14:30 +00:00
if input . SendType == Swap {
if input . ToTokenID == "" {
2024-07-02 10:25:30 +00:00
return ErrSwapRequiresToTokenID
2024-06-12 20:14:30 +00:00
}
if input . TokenID == input . ToTokenID {
2024-07-02 10:25:30 +00:00
return ErrSwapTokenIDMustBeDifferent
2024-06-12 20:14:30 +00:00
}
2024-07-02 14:59:11 +00:00
if input . AmountIn != nil &&
2024-06-12 20:14:30 +00:00
input . AmountOut != nil &&
2024-07-02 14:59:11 +00:00
input . AmountIn . ToInt ( ) . Cmp ( pathprocessor . ZeroBigIntValue ) > 0 &&
2024-06-12 20:14:30 +00:00
input . AmountOut . ToInt ( ) . Cmp ( pathprocessor . ZeroBigIntValue ) > 0 {
2024-07-02 10:25:30 +00:00
return ErrSwapAmountInAmountOutMustBeExclusive
2024-06-12 20:14:30 +00:00
}
2024-07-02 14:59:11 +00:00
if input . AmountIn != nil && input . AmountIn . ToInt ( ) . Sign ( ) < 0 {
2024-07-02 10:25:30 +00:00
return ErrSwapAmountInMustBePositive
2024-06-12 20:14:30 +00:00
}
if input . AmountOut != nil && input . AmountOut . ToInt ( ) . Sign ( ) < 0 {
2024-07-02 10:25:30 +00:00
return ErrSwapAmountOutMustBePositive
2024-06-12 20:14:30 +00:00
}
}
2024-07-03 09:30:17 +00:00
return validateFromLockedAmount ( input )
2024-06-10 15:15:41 +00:00
}
2024-07-03 09:30:17 +00:00
func validateFromLockedAmount ( input * RouteInputParams ) error {
if input . FromLockedAmount == nil || len ( input . FromLockedAmount ) == 0 {
2024-06-10 15:15:41 +00:00
return nil
}
2024-07-03 09:30:17 +00:00
var suppNetworks map [ uint64 ] bool
if input . testnetMode {
suppNetworks = copyMap ( supportedTestNetworks )
} else {
suppNetworks = copyMap ( supportedNetworks )
}
totalLockedAmount := big . NewInt ( 0 )
2024-06-10 15:15:41 +00:00
excludedChainCount := 0
2024-07-03 09:30:17 +00:00
for chainID , amount := range input . FromLockedAmount {
if containsNetworkChainID ( chainID , input . DisabledFromChainIDs ) {
return ErrDisabledChainFoundAmongLockedNetworks
}
if input . testnetMode {
2024-06-10 15:15:41 +00:00
if ! supportedTestNetworks [ chainID ] {
2024-07-03 09:30:17 +00:00
return ErrLockedAmountNotSupportedForNetwork
2024-06-10 15:15:41 +00:00
}
} else {
if ! supportedNetworks [ chainID ] {
2024-07-03 09:30:17 +00:00
return ErrLockedAmountNotSupportedForNetwork
2024-06-10 15:15:41 +00:00
}
}
if amount == nil || amount . ToInt ( ) . Sign ( ) < 0 {
2024-07-03 09:30:17 +00:00
return ErrLockedAmountNotNegative
2024-06-10 15:15:41 +00:00
}
2024-07-03 09:30:17 +00:00
if ! ( amount . ToInt ( ) . Sign ( ) > 0 ) {
2024-06-10 15:15:41 +00:00
excludedChainCount ++
}
2024-07-03 09:30:17 +00:00
delete ( suppNetworks , chainID )
totalLockedAmount = new ( big . Int ) . Add ( totalLockedAmount , amount . ToInt ( ) )
2024-06-10 15:15:41 +00:00
}
2024-07-03 09:30:17 +00:00
if ( ! input . testnetMode && excludedChainCount == len ( supportedNetworks ) ) ||
( input . testnetMode && excludedChainCount == len ( supportedTestNetworks ) ) {
return ErrLockedAmountExcludesAllSupported
}
if totalLockedAmount . Cmp ( input . AmountIn . ToInt ( ) ) > 0 {
return ErrLockedAmountExceedsTotalSendAmount
} else if totalLockedAmount . Cmp ( input . AmountIn . ToInt ( ) ) < 0 && len ( suppNetworks ) == 0 {
return ErrLockedAmountLessThanSendAmountAllNetworks
2024-06-10 15:15:41 +00:00
}
2024-06-05 07:56:02 +00:00
return nil
}
2024-06-20 09:03:42 +00:00
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 ,
2024-07-03 09:30:17 +00:00
ErrorResponse : errors . CreateErrorResponseFromError ( err ) ,
2024-06-20 09:03:42 +00:00
}
signal . SendWalletEvent ( signal . SuggestedRoutes , errResponse )
return
}
signal . SendWalletEvent ( signal . SuggestedRoutes , result )
} )
}
func ( r * Router ) StopSuggestedRoutesV2AsyncCalcualtion ( ) {
r . scheduler . Stop ( )
}
2024-05-16 08:55:46 +00:00
func ( r * Router ) SuggestedRoutesV2 ( ctx context . Context , input * RouteInputParams ) ( * SuggestedRoutesV2 , error ) {
2024-06-20 09:03:42 +00:00
testnetMode , err := r . rpcClient . NetworkManager . GetTestNetworksEnabled ( )
if err != nil {
2024-07-03 09:30:17 +00:00
return nil , errors . CreateErrorResponseFromError ( err )
2024-06-20 09:03:42 +00:00
}
input . testnetMode = testnetMode
2024-06-14 11:27:53 +00:00
// clear all processors
for _ , processor := range r . pathProcessors {
if clearable , ok := processor . ( pathprocessor . PathProcessorClearable ) ; ok {
clearable . Clear ( )
}
}
2024-06-20 09:03:42 +00:00
err = validateInputData ( input )
2024-06-18 10:31:23 +00:00
if err != nil {
2024-07-03 09:30:17 +00:00
return nil , errors . CreateErrorResponseFromError ( err )
2024-06-18 10:31:23 +00:00
}
2024-07-04 10:48:14 +00:00
selectedFromChains , selectedTohains , err := r . getSelectedChains ( input )
2024-06-14 11:27:53 +00:00
if err != nil {
2024-07-03 09:30:17 +00:00
return nil , errors . CreateErrorResponseFromError ( err )
2024-06-14 11:27:53 +00:00
}
2024-07-04 10:48:14 +00:00
balanceMap , err := r . getBalanceMapForTokenOnChains ( ctx , input , selectedFromChains )
if err != nil {
return nil , errors . CreateErrorResponseFromError ( err )
}
2024-06-18 10:31:23 +00:00
2024-07-04 10:48:14 +00:00
candidates , err := r . resolveCandidates ( ctx , input , selectedFromChains , selectedTohains , balanceMap )
2024-05-14 19:11:16 +00:00
if err != nil {
2024-07-03 09:30:17 +00:00
return nil , errors . CreateErrorResponseFromError ( err )
2024-05-14 19:11:16 +00:00
}
2024-07-04 10:48:14 +00:00
return r . resolveRoutes ( ctx , input , candidates , balanceMap )
}
2024-05-14 19:11:16 +00:00
2024-07-04 10:48:14 +00:00
// getBalanceMapForTokenOnChains returns the balance map for passed address, where the key is in format "chainID-tokenSymbol" and
// value is the balance of the token. Native token (EHT) is always added to the balance map.
func ( r * Router ) getBalanceMapForTokenOnChains ( ctx context . Context , input * RouteInputParams , selectedFromChains [ ] * params . Network ) ( balanceMap map [ string ] * big . Int , err error ) {
if input . testsMode {
return input . testParams . balanceMap , nil
}
balanceMap = make ( map [ string ] * big . Int )
for _ , chain := range selectedFromChains {
token := input . SendType . FindToken ( r . tokenManager , r . collectiblesService , input . AddrFrom , chain , input . TokenID )
if token == nil {
2024-05-14 19:11:16 +00:00
continue
}
2024-07-04 10:48:14 +00:00
// add token balance for the chain
tokenBalance := big . NewInt ( 1 )
if input . SendType == ERC1155Transfer {
tokenBalance , err = r . getERC1155Balance ( ctx , chain , token , input . AddrFrom )
if err != nil {
return nil , errors . CreateErrorResponseFromError ( err )
}
} else if input . SendType != ERC721Transfer {
tokenBalance , err = r . getBalance ( ctx , chain . ChainID , token , input . AddrFrom )
if err != nil {
return nil , errors . CreateErrorResponseFromError ( err )
}
2024-05-14 19:11:16 +00:00
}
2024-07-04 10:48:14 +00:00
balanceMap [ makeBalanceKey ( chain . ChainID , token . Symbol ) ] = tokenBalance
2024-05-14 19:11:16 +00:00
2024-07-04 10:48:14 +00:00
// add native token balance for the chain
nativeToken := r . tokenManager . FindToken ( chain , chain . NativeCurrencySymbol )
if nativeToken == nil {
return nil , ErrNativeTokenNotFound
}
nativeBalance , err := r . getBalance ( ctx , chain . ChainID , nativeToken , input . AddrFrom )
if err != nil {
return nil , errors . CreateErrorResponseFromError ( err )
}
balanceMap [ makeBalanceKey ( chain . ChainID , nativeToken . Symbol ) ] = nativeBalance
}
return
}
func ( r * Router ) getSelectedUnlockedChains ( input * RouteInputParams , processingChain * params . Network , selectedFromChains [ ] * params . Network ) [ ] * params . Network {
selectedButNotLockedChains := [ ] * params . Network { processingChain } // always add the processing chain at the beginning
for _ , net := range selectedFromChains {
if net . ChainID == processingChain . ChainID {
2024-05-14 19:11:16 +00:00
continue
}
2024-07-04 10:48:14 +00:00
if _ , ok := input . FromLockedAmount [ net . ChainID ] ; ! ok {
selectedButNotLockedChains = append ( selectedButNotLockedChains , net )
}
}
return selectedButNotLockedChains
}
func ( r * Router ) getOptionsForAmoutToSplitAccrossChainsForProcessingChain ( input * RouteInputParams , amountToSplit * big . Int , processingChain * params . Network ,
selectedFromChains [ ] * params . Network , balanceMap map [ string ] * big . Int ) map [ uint64 ] [ ] amountOption {
selectedButNotLockedChains := r . getSelectedUnlockedChains ( input , processingChain , selectedFromChains )
2024-05-14 19:11:16 +00:00
2024-07-04 10:48:14 +00:00
crossChainAmountOptions := make ( map [ uint64 ] [ ] amountOption )
for _ , chain := range selectedButNotLockedChains {
2024-06-05 07:56:02 +00:00
var (
2024-07-04 10:48:14 +00:00
ok bool
tokenBalance * big . Int
2024-06-05 07:56:02 +00:00
)
2024-07-04 10:48:14 +00:00
if tokenBalance , ok = balanceMap [ makeBalanceKey ( chain . ChainID , input . TokenID ) ] ; ! ok {
2024-05-14 19:11:16 +00:00
continue
}
2024-07-04 10:48:14 +00:00
if tokenBalance . Cmp ( pathprocessor . ZeroBigIntValue ) > 0 {
if tokenBalance . Cmp ( amountToSplit ) <= 0 {
crossChainAmountOptions [ chain . ChainID ] = append ( crossChainAmountOptions [ chain . ChainID ] , amountOption {
amount : tokenBalance ,
locked : false ,
} )
amountToSplit = new ( big . Int ) . Sub ( amountToSplit , tokenBalance )
} else if amountToSplit . Cmp ( pathprocessor . ZeroBigIntValue ) > 0 {
crossChainAmountOptions [ chain . ChainID ] = append ( crossChainAmountOptions [ chain . ChainID ] , amountOption {
amount : amountToSplit ,
locked : false ,
} )
// break since amountToSplit is fully addressed and the rest is 0
break
}
2024-05-14 19:11:16 +00:00
}
2024-07-04 10:48:14 +00:00
}
return crossChainAmountOptions
}
func ( r * Router ) getCrossChainsOptionsForSendingAmount ( input * RouteInputParams , selectedFromChains [ ] * params . Network ,
balanceMap map [ string ] * big . Int ) map [ uint64 ] [ ] amountOption {
// All we do in this block we're free to do, because of the validateInputData function which checks if the locked amount
// was properly set and if there is something unexpected it will return an error and we will not reach this point
finalCrossChainAmountOptions := make ( map [ uint64 ] [ ] amountOption ) // represents all possible amounts that can be sent from the "from" chain
for _ , selectedFromChain := range selectedFromChains {
2024-05-14 19:11:16 +00:00
amountLocked := false
2024-05-16 07:37:36 +00:00
amountToSend := input . AmountIn . ToInt ( )
2024-07-04 10:48:14 +00:00
lockedAmount , fromChainLocked := input . FromLockedAmount [ selectedFromChain . ChainID ]
if fromChainLocked {
2024-05-14 19:11:16 +00:00
amountToSend = lockedAmount . ToInt ( )
amountLocked = true
2024-06-14 11:27:53 +00:00
} else if len ( input . FromLockedAmount ) > 0 {
2024-05-16 07:37:36 +00:00
for chainID , lockedAmount := range input . FromLockedAmount {
2024-07-04 10:48:14 +00:00
if chainID == selectedFromChain . ChainID {
2024-05-14 19:11:16 +00:00
continue
}
amountToSend = new ( big . Int ) . Sub ( amountToSend , lockedAmount . ToInt ( ) )
}
}
2024-07-04 10:48:14 +00:00
if amountToSend . Cmp ( pathprocessor . ZeroBigIntValue ) > 0 {
// add full amount always, cause we want to check for balance errors at the end of the routing algorithm
// TODO: once we introduce bettwer error handling and start checking for the balance at the beginning of the routing algorithm
// we can remove this line and optimize the routing algorithm more
finalCrossChainAmountOptions [ selectedFromChain . ChainID ] = append ( finalCrossChainAmountOptions [ selectedFromChain . ChainID ] , amountOption {
amount : amountToSend ,
locked : amountLocked ,
} )
if amountLocked {
2024-06-20 09:03:42 +00:00
continue
2024-05-14 19:11:16 +00:00
}
2024-07-04 10:48:14 +00:00
// If the amount that need to be send is bigger than the balance on the chain, then we want to check options if that
// amount can be splitted and sent across multiple chains.
if input . SendType == Transfer && len ( selectedFromChains ) > 1 {
// All we do in this block we're free to do, because of the validateInputData function which checks if the locked amount
// was properly set and if there is something unexpected it will return an error and we will not reach this point
amountToSplitAccrossChains := new ( big . Int ) . Set ( amountToSend )
2024-05-14 19:11:16 +00:00
2024-07-04 10:48:14 +00:00
crossChainAmountOptions := r . getOptionsForAmoutToSplitAccrossChainsForProcessingChain ( input , amountToSend , selectedFromChain , selectedFromChains , balanceMap )
2024-05-14 19:11:16 +00:00
2024-07-04 10:48:14 +00:00
// sum up all the allocated amounts accorss all chains
allocatedAmount := big . NewInt ( 0 )
for _ , amountOptions := range crossChainAmountOptions {
for _ , amountOption := range amountOptions {
allocatedAmount = new ( big . Int ) . Add ( allocatedAmount , amountOption . amount )
2024-05-16 10:22:32 +00:00
}
2024-07-04 10:48:14 +00:00
}
2024-05-16 10:22:32 +00:00
2024-07-04 10:48:14 +00:00
// if the allocated amount is the same as the amount that need to be sent, then we can add the options to the finalCrossChainAmountOptions
if allocatedAmount . Cmp ( amountToSplitAccrossChains ) == 0 {
for cID , amountOptions := range crossChainAmountOptions {
finalCrossChainAmountOptions [ cID ] = append ( finalCrossChainAmountOptions [ cID ] , amountOptions ... )
2024-05-16 10:22:32 +00:00
}
2024-07-04 10:48:14 +00:00
}
}
}
}
2024-05-16 10:22:32 +00:00
2024-07-04 10:48:14 +00:00
return finalCrossChainAmountOptions
}
2024-05-14 19:11:16 +00:00
2024-07-04 10:48:14 +00:00
func ( r * Router ) findOptionsForSendingAmount ( input * RouteInputParams , selectedFromChains [ ] * params . Network ,
balanceMap map [ string ] * big . Int ) ( map [ uint64 ] [ ] amountOption , error ) {
2024-06-05 07:56:02 +00:00
2024-07-04 10:48:14 +00:00
crossChainAmountOptions := r . getCrossChainsOptionsForSendingAmount ( input , selectedFromChains , balanceMap )
2024-05-14 19:11:16 +00:00
2024-07-04 10:48:14 +00:00
// filter out duplicates values for the same chain
for chainID , amountOptions := range crossChainAmountOptions {
uniqueAmountOptions := make ( map [ string ] amountOption )
for _ , amountOption := range amountOptions {
uniqueAmountOptions [ amountOption . amount . String ( ) ] = amountOption
}
2024-05-14 19:11:16 +00:00
2024-07-04 10:48:14 +00:00
crossChainAmountOptions [ chainID ] = make ( [ ] amountOption , 0 )
for _ , amountOption := range uniqueAmountOptions {
crossChainAmountOptions [ chainID ] = append ( crossChainAmountOptions [ chainID ] , amountOption )
}
}
2024-05-14 19:11:16 +00:00
2024-07-04 10:48:14 +00:00
return crossChainAmountOptions , nil
}
2024-05-14 19:11:16 +00:00
2024-07-04 10:48:14 +00:00
func ( r * Router ) getSelectedChains ( input * RouteInputParams ) ( selectedFromChains [ ] * params . Network , selectedTohains [ ] * params . Network , err error ) {
var networks [ ] * params . Network
networks , err = r . rpcClient . NetworkManager . Get ( false )
if err != nil {
return nil , nil , errors . CreateErrorResponseFromError ( err )
}
2024-05-14 19:11:16 +00:00
2024-07-04 10:48:14 +00:00
for _ , network := range networks {
if network . IsTest != input . testnetMode {
continue
}
2024-05-14 19:11:16 +00:00
2024-07-04 10:48:14 +00:00
if ! containsNetworkChainID ( network . ChainID , input . DisabledFromChainIDs ) {
selectedFromChains = append ( selectedFromChains , network )
}
if ! containsNetworkChainID ( network . ChainID , input . DisabledToChainIDs ) {
selectedTohains = append ( selectedTohains , network )
}
}
2024-05-14 19:11:16 +00:00
2024-07-04 10:48:14 +00:00
return selectedFromChains , selectedTohains , nil
}
2024-06-19 09:20:41 +00:00
2024-07-04 10:48:14 +00:00
func ( r * Router ) resolveCandidates ( ctx context . Context , input * RouteInputParams , selectedFromChains [ ] * params . Network ,
selectedTohains [ ] * params . Network , balanceMap map [ string ] * big . Int ) ( candidates [ ] * PathV2 , err error ) {
var (
testsMode = input . testsMode && input . testParams != nil
group = async . NewAtomicGroup ( ctx )
mu sync . Mutex
)
crossChainAmountOptions , err := r . findOptionsForSendingAmount ( input , selectedFromChains , balanceMap )
if err != nil {
return nil , errors . CreateErrorResponseFromError ( err )
}
for networkIdx := range selectedFromChains {
network := selectedFromChains [ networkIdx ]
if ! input . SendType . isAvailableFor ( network ) {
continue
}
var (
token * walletToken . Token
toToken * walletToken . Token
)
if testsMode {
token = input . testParams . tokenFrom
} else {
token = input . SendType . FindToken ( r . tokenManager , r . collectiblesService , input . AddrFrom , network , input . TokenID )
}
if token == nil {
continue
}
if input . SendType == Swap {
toToken = input . SendType . FindToken ( r . tokenManager , r . collectiblesService , common . Address { } , network , input . ToTokenID )
}
var fees * SuggestedFees
if testsMode {
fees = input . testParams . suggestedFees
} else {
fees , err = r . feesManager . SuggestedFees ( ctx , network . ChainID )
if err != nil {
continue
}
}
group . Add ( func ( c context . Context ) error {
for _ , amountOption := range crossChainAmountOptions [ network . ChainID ] {
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.
// This also applies to including another (Celer) bridge in the calculation.
// TODO:
// this algorithm, includeing finding the best route, has to be updated to include more bridges and one (for now) or more swap options
// it means that candidates should not be treated linearly, but improve the logic to have multiple routes with different processors of the same type.
// Example:
// Routes for sending SNT from Ethereum to Optimism can be:
// 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
// 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
// 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
// 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
// 5. ...
// 6. ...
//
// With the current routing algorithm atm we're not able to generate all possible routes.
if ! input . SendType . canUseProcessor ( pProcessor ) {
continue
2024-05-14 19:11:16 +00:00
}
2024-07-04 10:48:14 +00:00
for _ , dest := range selectedTohains {
if ! input . SendType . isAvailableFor ( network ) {
continue
}
if ! input . SendType . isAvailableBetween ( network , dest ) {
continue
}
processorInputParams := pathprocessor . ProcessorInputParams {
FromChain : network ,
ToChain : dest ,
FromToken : token ,
ToToken : toToken ,
ToAddr : input . AddrTo ,
FromAddr : input . AddrFrom ,
AmountIn : amountOption . amount ,
AmountOut : input . AmountOut . ToInt ( ) ,
Username : input . Username ,
PublicKey : input . PublicKey ,
PackID : input . PackID . ToInt ( ) ,
}
if input . testsMode {
processorInputParams . TestsMode = input . testsMode
processorInputParams . TestEstimationMap = input . testParams . estimationMap
processorInputParams . TestBonderFeeMap = input . testParams . bonderFeeMap
processorInputParams . TestApprovalGasEstimation = input . testParams . approvalGasEstimation
processorInputParams . TestApprovalL1Fee = input . testParams . approvalL1Fee
}
can , err := pProcessor . AvailableFor ( processorInputParams )
if err != nil || ! can {
continue
}
bonderFees , tokenFees , err := pProcessor . CalculateFees ( processorInputParams )
if err != nil {
continue
}
gasLimit , err := pProcessor . EstimateGas ( processorInputParams )
if err != nil {
continue
}
approvalContractAddress , err := pProcessor . GetContractAddress ( processorInputParams )
if err != nil {
continue
}
approvalRequired , approvalAmountRequired , approvalGasLimit , l1ApprovalFee , err := r . requireApproval ( ctx , input . SendType , & approvalContractAddress , processorInputParams )
if err != nil {
continue
}
// TODO: keep l1 fees at 0 until we have the correct algorithm, as we do base fee x 2 that should cover the l1 fees
var l1FeeWei uint64 = 0
// if input.SendType.needL1Fee() {
// txInputData, err := pProcessor.PackTxInputData(processorInputParams)
// if err != nil {
// continue
// }
// l1FeeWei, _ = r.feesManager.GetL1Fee(ctx, network.ChainID, txInputData)
// }
amountOut , err := pProcessor . CalculateAmountOut ( processorInputParams )
if err != nil {
continue
}
maxFeesPerGas := fees . feeFor ( input . GasFeeMode )
estimatedTime := r . feesManager . TransactionEstimatedTime ( ctx , network . ChainID , maxFeesPerGas )
if approvalRequired && estimatedTime < MoreThanFiveMinutes {
estimatedTime += 1
}
mu . Lock ( )
candidates = append ( candidates , & PathV2 {
ProcessorName : pProcessor . Name ( ) ,
FromChain : network ,
ToChain : dest ,
FromToken : token ,
ToToken : toToken ,
AmountIn : ( * hexutil . Big ) ( amountOption . amount ) ,
AmountInLocked : amountOption . locked ,
AmountOut : ( * hexutil . Big ) ( amountOut ) ,
SuggestedLevelsForMaxFeesPerGas : fees . MaxFeesLevels ,
TxBaseFee : ( * hexutil . Big ) ( fees . BaseFee ) ,
TxPriorityFee : ( * hexutil . Big ) ( fees . MaxPriorityFeePerGas ) ,
TxGasAmount : gasLimit ,
TxBonderFees : ( * hexutil . Big ) ( bonderFees ) ,
TxTokenFees : ( * hexutil . Big ) ( tokenFees ) ,
TxL1Fee : ( * hexutil . Big ) ( big . NewInt ( int64 ( l1FeeWei ) ) ) ,
ApprovalRequired : approvalRequired ,
ApprovalAmountRequired : ( * hexutil . Big ) ( approvalAmountRequired ) ,
ApprovalContractAddress : & approvalContractAddress ,
ApprovalBaseFee : ( * hexutil . Big ) ( fees . BaseFee ) ,
ApprovalPriorityFee : ( * hexutil . Big ) ( fees . MaxPriorityFeePerGas ) ,
ApprovalGasAmount : approvalGasLimit ,
ApprovalL1Fee : ( * hexutil . Big ) ( big . NewInt ( int64 ( l1ApprovalFee ) ) ) ,
EstimatedTime : estimatedTime ,
} )
mu . Unlock ( )
}
2024-05-14 19:11:16 +00:00
}
}
return nil
} )
}
group . Wait ( )
2024-06-14 11:27:53 +00:00
return candidates , nil
}
2024-05-14 19:11:16 +00:00
2024-07-04 10:48:14 +00:00
func ( r * Router ) checkBalancesForTheBestRoute ( ctx context . Context , bestRoute [ ] * PathV2 , input * RouteInputParams , balanceMap map [ string ] * big . Int ) ( err error ) {
2024-05-14 19:11:16 +00:00
// check the best route for the required balances
2024-06-30 20:44:21 +00:00
for _ , path := range bestRoute {
2024-06-06 20:08:25 +00:00
if path . requiredTokenBalance != nil && path . requiredTokenBalance . Cmp ( pathprocessor . ZeroBigIntValue ) > 0 {
2024-07-04 10:48:14 +00:00
if tokenBalance , ok := balanceMap [ makeBalanceKey ( path . FromChain . ChainID , path . FromToken . Symbol ) ] ; ok {
if tokenBalance . Cmp ( path . requiredTokenBalance ) == - 1 {
return ErrNotEnoughTokenBalance
2024-05-14 19:11:16 +00:00
}
2024-07-04 10:48:14 +00:00
} else {
return ErrTokenNotFound
2024-05-14 19:11:16 +00:00
}
}
2024-07-04 10:48:14 +00:00
if nativeBalance , ok := balanceMap [ makeBalanceKey ( path . FromChain . ChainID , pathprocessor . EthSymbol ) ] ; ok {
if nativeBalance . Cmp ( path . requiredNativeBalance ) == - 1 {
return ErrNotEnoughNativeBalance
2024-06-18 10:31:23 +00:00
}
2024-07-04 10:48:14 +00:00
} else {
return ErrNativeTokenNotFound
2024-06-30 20:44:21 +00:00
}
}
return nil
}
func removeBestRouteFromAllRouters ( allRoutes [ ] [ ] * PathV2 , best [ ] * PathV2 ) [ ] [ ] * PathV2 {
for i , route := range allRoutes {
routeFound := true
for _ , p := range route {
found := false
for _ , b := range best {
if p . ProcessorName == b . ProcessorName &&
( p . FromChain == nil && b . FromChain == nil || p . FromChain . ChainID == b . FromChain . ChainID ) &&
( p . ToChain == nil && b . ToChain == nil || p . ToChain . ChainID == b . ToChain . ChainID ) &&
( p . FromToken == nil && b . FromToken == nil || p . FromToken . Symbol == b . FromToken . Symbol ) {
found = true
break
}
}
if ! found {
routeFound = false
break
}
}
if routeFound {
return append ( allRoutes [ : i ] , allRoutes [ i + 1 : ] ... )
}
}
return nil
}
2024-07-04 10:48:14 +00:00
func ( r * Router ) resolveRoutes ( ctx context . Context , input * RouteInputParams , candidates [ ] * PathV2 , balanceMap map [ string ] * big . Int ) ( suggestedRoutes * SuggestedRoutesV2 , err error ) {
2024-06-30 20:44:21 +00:00
var prices map [ string ] float64
if input . testsMode {
prices = input . testParams . tokenPrices
} else {
prices , err = input . SendType . FetchPrices ( r . marketManager , input . TokenID )
if err != nil {
2024-07-03 09:30:17 +00:00
return nil , errors . CreateErrorResponseFromError ( err )
2024-06-30 20:44:21 +00:00
}
}
tokenPrice := prices [ input . TokenID ]
nativeChainTokenPrice := prices [ pathprocessor . EthSymbol ]
var allRoutes [ ] [ ] * PathV2
suggestedRoutes , allRoutes = newSuggestedRoutesV2 ( input . Uuid , input . AmountIn . ToInt ( ) , candidates , input . FromLockedAmount , tokenPrice , nativeChainTokenPrice )
for len ( allRoutes ) > 0 {
best := findBestV2 ( allRoutes , tokenPrice , nativeChainTokenPrice )
2024-07-04 10:48:14 +00:00
err := r . checkBalancesForTheBestRoute ( ctx , best , input , balanceMap )
2024-06-30 20:44:21 +00:00
if err != nil {
// If it's about transfer or bridge and there is more routes, but on the best (cheapest) one there is not enugh balance
// we shold check other routes even though there are not the cheapest ones
if ( input . SendType == Transfer ||
input . SendType == Bridge ) &&
len ( allRoutes ) > 1 {
allRoutes = removeBestRouteFromAllRouters ( allRoutes , best )
continue
} else {
2024-07-03 09:30:17 +00:00
return suggestedRoutes , errors . CreateErrorResponseFromError ( err )
2024-06-30 20:44:21 +00:00
}
}
if len ( best ) > 0 {
sort . Slice ( best , func ( i , j int ) bool {
return best [ i ] . AmountInLocked
} )
2024-05-14 19:11:16 +00:00
}
2024-06-30 20:44:21 +00:00
suggestedRoutes . Best = best
break
2024-05-14 19:11:16 +00:00
}
return suggestedRoutes , nil
}