diff --git a/services/wallet/router/errors.go b/services/wallet/router/errors.go index d5cb845de..d62e403ec 100644 --- a/services/wallet/router/errors.go +++ b/services/wallet/router/errors.go @@ -27,4 +27,5 @@ var ( ErrDisabledChainFoundAmongLockedNetworks = &errors.ErrorResponse{Code: errors.ErrorCode("WR-019"), Details: "disabled chain found among locked networks"} ErrENSSetPubKeyInvalidUsername = &errors.ErrorResponse{Code: errors.ErrorCode("WR-020"), Details: "a valid username, ending in '.eth', is required for ENSSetPubKey"} ErrLockedAmountExcludesAllSupported = &errors.ErrorResponse{Code: errors.ErrorCode("WR-021"), Details: "all supported chains are excluded, routing impossible"} + ErrTokenNotFound = &errors.ErrorResponse{Code: errors.ErrorCode("WR-022"), Details: "token not found"} ) diff --git a/services/wallet/router/router.go b/services/wallet/router/router.go index b7ab0494a..e0693d55e 100644 --- a/services/wallet/router/router.go +++ b/services/wallet/router/router.go @@ -414,8 +414,8 @@ func (r *Router) requireApproval(ctx context.Context, sendType SendType, approva return true, params.AmountIn, estimate, l1Fee, nil } -func (r *Router) getBalance(ctx context.Context, network *params.Network, token *token.Token, account common.Address) (*big.Int, error) { - client, err := r.rpcClient.EthClient(network.ChainID) +func (r *Router) getBalance(ctx context.Context, chainID uint64, token *token.Token, account common.Address) (*big.Int, error) { + client, err := r.rpcClient.EthClient(chainID) if err != nil { return nil, err } @@ -521,7 +521,7 @@ func (r *Router) SuggestedRoutes( return err } } else if sendType != ERC721Transfer { - balance, err = r.getBalance(ctx, network, token, addrFrom) + balance, err = r.getBalance(ctx, network.ChainID, token, addrFrom) if err != nil { return err } @@ -535,7 +535,7 @@ func (r *Router) SuggestedRoutes( maxAmountIn = amount } - nativeBalance, err := r.getBalance(ctx, network, nativeToken, addrFrom) + nativeBalance, err := r.getBalance(ctx, network.ChainID, nativeToken, addrFrom) if err != nil { return err } diff --git a/services/wallet/router/router_v2.go b/services/wallet/router/router_v2.go index 181ffb1b4..6aa37f60f 100644 --- a/services/wallet/router/router_v2.go +++ b/services/wallet/router/router_v2.go @@ -79,15 +79,13 @@ type routerTestParams struct { approvalL1Fee uint64 } -func makeTestBalanceKey(chainID uint64, symbol string) string { - return fmt.Sprintf("%d-%s", chainID, symbol) +type amountOption struct { + amount *big.Int + locked bool } -func (rt routerTestParams) getTestBalance(chainID uint64, symbol string) *big.Int { - if val, ok := rt.balanceMap[makeTestBalanceKey(chainID, symbol)]; ok { - return val - } - return big.NewInt(0) +func makeBalanceKey(chainID uint64, symbol string) string { + return fmt.Sprintf("%d-%s", chainID, symbol) } type PathV2 struct { @@ -487,40 +485,246 @@ func (r *Router) SuggestedRoutesV2(ctx context.Context, input *RouteInputParams) return nil, errors.CreateErrorResponseFromError(err) } - candidates, err := r.resolveCandidates(ctx, input) + selectedFromChains, selectedTohains, err := r.getSelectedChains(input) if err != nil { return nil, errors.CreateErrorResponseFromError(err) } - return r.resolveRoutes(ctx, input, candidates) + balanceMap, err := r.getBalanceMapForTokenOnChains(ctx, input, selectedFromChains) + if err != nil { + return nil, errors.CreateErrorResponseFromError(err) + } + + candidates, err := r.resolveCandidates(ctx, input, selectedFromChains, selectedTohains, balanceMap) + if err != nil { + return nil, errors.CreateErrorResponseFromError(err) + } + + return r.resolveRoutes(ctx, input, candidates, balanceMap) } -func (r *Router) resolveCandidates(ctx context.Context, input *RouteInputParams) (candidates []*PathV2, err error) { - var ( - testsMode = input.testsMode && input.testParams != nil - networks []*params.Network - ) - - networks, err = r.rpcClient.NetworkManager.Get(false) - if err != nil { - return nil, errors.CreateErrorResponseFromError(err) +// 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 } - var ( - group = async.NewAtomicGroup(ctx) - mu sync.Mutex - ) + balanceMap = make(map[string]*big.Int) - for networkIdx := range networks { - network := networks[networkIdx] + for _, chain := range selectedFromChains { + token := input.SendType.FindToken(r.tokenManager, r.collectiblesService, input.AddrFrom, chain, input.TokenID) + if token == nil { + continue + } + + // 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) + } + } + balanceMap[makeBalanceKey(chain.ChainID, token.Symbol)] = tokenBalance + + // 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 { + continue + } + 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) + + crossChainAmountOptions := make(map[uint64][]amountOption) + for _, chain := range selectedButNotLockedChains { + var ( + ok bool + tokenBalance *big.Int + ) + + if tokenBalance, ok = balanceMap[makeBalanceKey(chain.ChainID, input.TokenID)]; !ok { + continue + } + + 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 + } + } + } + + 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 { + + amountLocked := false + amountToSend := input.AmountIn.ToInt() + lockedAmount, fromChainLocked := input.FromLockedAmount[selectedFromChain.ChainID] + if fromChainLocked { + amountToSend = lockedAmount.ToInt() + amountLocked = true + } else if len(input.FromLockedAmount) > 0 { + for chainID, lockedAmount := range input.FromLockedAmount { + if chainID == selectedFromChain.ChainID { + continue + } + amountToSend = new(big.Int).Sub(amountToSend, lockedAmount.ToInt()) + } + } + + 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 { + continue + } + + // 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) + + crossChainAmountOptions := r.getOptionsForAmoutToSplitAccrossChainsForProcessingChain(input, amountToSend, selectedFromChain, selectedFromChains, balanceMap) + + // 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) + } + } + + // 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...) + } + } + } + } + } + + return finalCrossChainAmountOptions +} + +func (r *Router) findOptionsForSendingAmount(input *RouteInputParams, selectedFromChains []*params.Network, + balanceMap map[string]*big.Int) (map[uint64][]amountOption, error) { + + crossChainAmountOptions := r.getCrossChainsOptionsForSendingAmount(input, selectedFromChains, balanceMap) + + // 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 + } + + crossChainAmountOptions[chainID] = make([]amountOption, 0) + for _, amountOption := range uniqueAmountOptions { + crossChainAmountOptions[chainID] = append(crossChainAmountOptions[chainID], amountOption) + } + } + + return crossChainAmountOptions, nil +} + +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) + } + + for _, network := range networks { if network.IsTest != input.testnetMode { continue } - if containsNetworkChainID(network.ChainID, input.DisabledFromChainIDs) { - continue + if !containsNetworkChainID(network.ChainID, input.DisabledFromChainIDs) { + selectedFromChains = append(selectedFromChains, network) } + if !containsNetworkChainID(network.ChainID, input.DisabledToChainIDs) { + selectedTohains = append(selectedTohains, network) + } + } + + return selectedFromChains, selectedTohains, nil +} + +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 } @@ -543,20 +747,6 @@ func (r *Router) resolveCandidates(ctx context.Context, input *RouteInputParams) toToken = input.SendType.FindToken(r.tokenManager, r.collectiblesService, common.Address{}, network, input.ToTokenID) } - amountLocked := false - amountToSend := input.AmountIn.ToInt() - if lockedAmount, ok := input.FromLockedAmount[network.ChainID]; ok { - amountToSend = lockedAmount.ToInt() - amountLocked = true - } else if len(input.FromLockedAmount) > 0 { - for chainID, lockedAmount := range input.FromLockedAmount { - if chainID == network.ChainID { - continue - } - amountToSend = new(big.Int).Sub(amountToSend, lockedAmount.ToInt()) - } - } - var fees *SuggestedFees if testsMode { fees = input.testParams.suggestedFees @@ -568,144 +758,139 @@ func (r *Router) resolveCandidates(ctx context.Context, input *RouteInputParams) } group.Add(func(c context.Context) error { - for _, pProcessor := range r.pathProcessors { - // With the condition below we're eliminating `Swap` as potential path that can participate in calculating the best route - // once we decide to inlcude `Swap` in the calculation we need to update `canUseProcessor` function. - // 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 - } - - for _, dest := range networks { - if dest.IsTest != input.testnetMode { + 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 } - if !input.SendType.isAvailableFor(network) { - continue + 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() } - - if !input.SendType.isAvailableBetween(network, dest) { - continue - } - - if containsNetworkChainID(dest.ChainID, input.DisabledToChainIDs) { - continue - } - - processorInputParams := pathprocessor.ProcessorInputParams{ - FromChain: network, - ToChain: dest, - FromToken: token, - ToToken: toToken, - ToAddr: input.AddrTo, - FromAddr: input.AddrFrom, - AmountIn: amountToSend, - 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)(amountToSend), - AmountInLocked: amountLocked, - 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() } } return nil @@ -716,49 +901,25 @@ func (r *Router) resolveCandidates(ctx context.Context, input *RouteInputParams) return candidates, nil } -func (r *Router) checkBalancesForTheBestRoute(ctx context.Context, bestRoute []*PathV2, input *RouteInputParams) (err error) { +func (r *Router) checkBalancesForTheBestRoute(ctx context.Context, bestRoute []*PathV2, input *RouteInputParams, balanceMap map[string]*big.Int) (err error) { // check the best route for the required balances for _, path := range bestRoute { if path.requiredTokenBalance != nil && path.requiredTokenBalance.Cmp(pathprocessor.ZeroBigIntValue) > 0 { - tokenBalance := big.NewInt(1) - if input.testsMode { - tokenBalance = input.testParams.getTestBalance(path.FromChain.ChainID, path.FromToken.Symbol) - } else { - if input.SendType == ERC1155Transfer { - tokenBalance, err = r.getERC1155Balance(ctx, path.FromChain, path.FromToken, input.AddrFrom) - if err != nil { - return errors.CreateErrorResponseFromError(err) - } - } else if input.SendType != ERC721Transfer { - tokenBalance, err = r.getBalance(ctx, path.FromChain, path.FromToken, input.AddrFrom) - if err != nil { - return errors.CreateErrorResponseFromError(err) - } + if tokenBalance, ok := balanceMap[makeBalanceKey(path.FromChain.ChainID, path.FromToken.Symbol)]; ok { + if tokenBalance.Cmp(path.requiredTokenBalance) == -1 { + return ErrNotEnoughTokenBalance } - } - - if tokenBalance.Cmp(path.requiredTokenBalance) == -1 { - return ErrNotEnoughTokenBalance + } else { + return ErrTokenNotFound } } - var nativeBalance *big.Int - if input.testsMode { - nativeBalance = input.testParams.getTestBalance(path.FromChain.ChainID, pathprocessor.EthSymbol) + if nativeBalance, ok := balanceMap[makeBalanceKey(path.FromChain.ChainID, pathprocessor.EthSymbol)]; ok { + if nativeBalance.Cmp(path.requiredNativeBalance) == -1 { + return ErrNotEnoughNativeBalance + } } else { - nativeToken := r.tokenManager.FindToken(path.FromChain, path.FromChain.NativeCurrencySymbol) - if nativeToken == nil { - return ErrNativeTokenNotFound - } - - nativeBalance, err = r.getBalance(ctx, path.FromChain, nativeToken, input.AddrFrom) - if err != nil { - return errors.CreateErrorResponseFromError(err) - } - } - - if nativeBalance.Cmp(path.requiredNativeBalance) == -1 { - return ErrNotEnoughNativeBalance + return ErrNativeTokenNotFound } } @@ -792,7 +953,7 @@ func removeBestRouteFromAllRouters(allRoutes [][]*PathV2, best []*PathV2) [][]*P return nil } -func (r *Router) resolveRoutes(ctx context.Context, input *RouteInputParams, candidates []*PathV2) (suggestedRoutes *SuggestedRoutesV2, err error) { +func (r *Router) resolveRoutes(ctx context.Context, input *RouteInputParams, candidates []*PathV2, balanceMap map[string]*big.Int) (suggestedRoutes *SuggestedRoutesV2, err error) { var prices map[string]float64 if input.testsMode { prices = input.testParams.tokenPrices @@ -812,7 +973,7 @@ func (r *Router) resolveRoutes(ctx context.Context, input *RouteInputParams, can for len(allRoutes) > 0 { best := findBestV2(allRoutes, tokenPrice, nativeChainTokenPrice) - err := r.checkBalancesForTheBestRoute(ctx, best, input) + err := r.checkBalancesForTheBestRoute(ctx, best, input, balanceMap) 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 diff --git a/services/wallet/router/router_v2_test.go b/services/wallet/router/router_v2_test.go index e32f2343e..fde18c6d5 100644 --- a/services/wallet/router/router_v2_test.go +++ b/services/wallet/router/router_v2_test.go @@ -30,6 +30,7 @@ const ( testBonderFeeETH = 150000000000000 testBonderFeeUSDC = 10000 + testAmount0Point1ETHInWei = 100000000000000000 testAmount0Point2ETHInWei = 200000000000000000 testAmount0Point3ETHInWei = 300000000000000000 testAmount0Point4ETHInWei = 400000000000000000 @@ -38,6 +39,8 @@ const ( testAmount0Point8ETHInWei = 800000000000000000 testAmount1ETHInWei = 1000000000000000000 testAmount2ETHInWei = 2000000000000000000 + testAmount3ETHInWei = 3000000000000000000 + testAmount5ETHInWei = 5000000000000000000 testAmount1USDC = 1000000 testAmount100USDC = 100000000 @@ -75,12 +78,12 @@ var ( } testBalanceMapPerChain = map[string]*big.Int{ - makeTestBalanceKey(walletCommon.EthereumMainnet, pathprocessor.EthSymbol): big.NewInt(testAmount2ETHInWei), - makeTestBalanceKey(walletCommon.EthereumMainnet, pathprocessor.UsdcSymbol): big.NewInt(testAmount100USDC), - makeTestBalanceKey(walletCommon.OptimismMainnet, pathprocessor.EthSymbol): big.NewInt(testAmount2ETHInWei), - makeTestBalanceKey(walletCommon.OptimismMainnet, pathprocessor.UsdcSymbol): big.NewInt(testAmount100USDC), - makeTestBalanceKey(walletCommon.ArbitrumMainnet, pathprocessor.EthSymbol): big.NewInt(testAmount2ETHInWei), - makeTestBalanceKey(walletCommon.ArbitrumMainnet, pathprocessor.UsdcSymbol): big.NewInt(testAmount100USDC), + makeBalanceKey(walletCommon.EthereumMainnet, pathprocessor.EthSymbol): big.NewInt(testAmount2ETHInWei), + makeBalanceKey(walletCommon.EthereumMainnet, pathprocessor.UsdcSymbol): big.NewInt(testAmount100USDC), + makeBalanceKey(walletCommon.OptimismMainnet, pathprocessor.EthSymbol): big.NewInt(testAmount2ETHInWei), + makeBalanceKey(walletCommon.OptimismMainnet, pathprocessor.UsdcSymbol): big.NewInt(testAmount100USDC), + makeBalanceKey(walletCommon.ArbitrumMainnet, pathprocessor.EthSymbol): big.NewInt(testAmount2ETHInWei), + makeBalanceKey(walletCommon.ArbitrumMainnet, pathprocessor.UsdcSymbol): big.NewInt(testAmount100USDC), } ) @@ -201,6 +204,46 @@ var defaultNetworks = []params.Network{ arbitrumSepolia, } +func amountOptionEqual(a, b amountOption) bool { + return a.amount.Cmp(b.amount) == 0 && a.locked == b.locked +} + +func contains(slice []amountOption, val amountOption) bool { + for _, item := range slice { + if amountOptionEqual(item, val) { + return true + } + } + return false +} + +func amountOptionsMapsEqual(map1, map2 map[uint64][]amountOption) bool { + if len(map1) != len(map2) { + return false + } + + for key, slice1 := range map1 { + slice2, ok := map2[key] + if !ok || len(slice1) != len(slice2) { + return false + } + + for _, val1 := range slice1 { + if !contains(slice2, val1) { + return false + } + } + + for _, val2 := range slice2 { + if !contains(slice1, val2) { + return false + } + } + } + + return true +} + func setupTestNetworkDB(t *testing.T) (*sql.DB, func()) { db, cleanup, err := helpers.SetupTestSQLDB(appdatabase.DbInitializer{}, "wallet-router-tests") require.NoError(t, err) @@ -1781,6 +1824,204 @@ func TestRouterV2(t *testing.T) { }, expectedCandidates: []*PathV2{}, }, + { + name: "ERC20 transfer - All FromChains - No Locked Amount - Enough Token Balance Across All Chains", + input: &RouteInputParams{ + testnetMode: false, + SendType: Transfer, + AddrFrom: common.HexToAddress("0x1"), + AddrTo: common.HexToAddress("0x2"), + AmountIn: (*hexutil.Big)(big.NewInt(2.5 * testAmount100USDC)), + TokenID: pathprocessor.UsdcSymbol, + + testsMode: true, + testParams: &routerTestParams{ + tokenFrom: &token.Token{ + ChainID: 1, + Symbol: pathprocessor.UsdcSymbol, + Decimals: 6, + }, + tokenPrices: testTokenPrices, + baseFee: big.NewInt(testBaseFee), + suggestedFees: testSuggestedFees, + balanceMap: testBalanceMapPerChain, + estimationMap: testEstimationMap, + bonderFeeMap: testBbonderFeeMap, + approvalGasEstimation: testApprovalGasEstimation, + approvalL1Fee: testApprovalL1Fee, + }, + }, + expectedCandidates: []*PathV2{ + { + ProcessorName: pathprocessor.ProcessorTransferName, + FromChain: &mainnet, + ToChain: &mainnet, + AmountOut: (*hexutil.Big)(big.NewInt(2.5 * testAmount100USDC)), + ApprovalRequired: false, + }, + { + ProcessorName: pathprocessor.ProcessorTransferName, + FromChain: &mainnet, + ToChain: &mainnet, + AmountOut: (*hexutil.Big)(big.NewInt(testAmount100USDC)), + ApprovalRequired: false, + }, + { + ProcessorName: pathprocessor.ProcessorBridgeHopName, + FromChain: &mainnet, + ToChain: &optimism, + AmountOut: (*hexutil.Big)(big.NewInt(2.5*testAmount100USDC - testBonderFeeUSDC)), + ApprovalRequired: true, + }, + { + ProcessorName: pathprocessor.ProcessorBridgeHopName, + FromChain: &mainnet, + ToChain: &optimism, + AmountOut: (*hexutil.Big)(big.NewInt(testAmount100USDC - testBonderFeeUSDC)), + ApprovalRequired: true, + }, + { + ProcessorName: pathprocessor.ProcessorBridgeHopName, + FromChain: &mainnet, + ToChain: &arbitrum, + AmountOut: (*hexutil.Big)(big.NewInt(2.5*testAmount100USDC - testBonderFeeUSDC)), + ApprovalRequired: true, + }, + { + ProcessorName: pathprocessor.ProcessorBridgeHopName, + FromChain: &mainnet, + ToChain: &arbitrum, + AmountOut: (*hexutil.Big)(big.NewInt(testAmount100USDC - testBonderFeeUSDC)), + ApprovalRequired: true, + }, + { + ProcessorName: pathprocessor.ProcessorTransferName, + FromChain: &optimism, + ToChain: &optimism, + AmountOut: (*hexutil.Big)(big.NewInt(0.5 * testAmount100USDC)), + ApprovalRequired: false, + }, + { + ProcessorName: pathprocessor.ProcessorTransferName, + FromChain: &optimism, + ToChain: &optimism, + AmountOut: (*hexutil.Big)(big.NewInt(testAmount100USDC)), + ApprovalRequired: false, + }, + { + ProcessorName: pathprocessor.ProcessorTransferName, + FromChain: &optimism, + ToChain: &optimism, + AmountOut: (*hexutil.Big)(big.NewInt(2.5 * testAmount100USDC)), + ApprovalRequired: false, + }, + { + ProcessorName: pathprocessor.ProcessorBridgeHopName, + FromChain: &optimism, + ToChain: &mainnet, + AmountOut: (*hexutil.Big)(big.NewInt(0.5*testAmount100USDC - testBonderFeeUSDC)), + ApprovalRequired: true, + }, + { + ProcessorName: pathprocessor.ProcessorBridgeHopName, + FromChain: &optimism, + ToChain: &mainnet, + AmountOut: (*hexutil.Big)(big.NewInt(testAmount100USDC - testBonderFeeUSDC)), + ApprovalRequired: true, + }, + { + ProcessorName: pathprocessor.ProcessorBridgeHopName, + FromChain: &optimism, + ToChain: &mainnet, + AmountOut: (*hexutil.Big)(big.NewInt(2.5*testAmount100USDC - testBonderFeeUSDC)), + ApprovalRequired: true, + }, + { + ProcessorName: pathprocessor.ProcessorBridgeHopName, + FromChain: &optimism, + ToChain: &arbitrum, + AmountOut: (*hexutil.Big)(big.NewInt(0.5*testAmount100USDC - testBonderFeeUSDC)), + ApprovalRequired: true, + }, + { + ProcessorName: pathprocessor.ProcessorBridgeHopName, + FromChain: &optimism, + ToChain: &arbitrum, + AmountOut: (*hexutil.Big)(big.NewInt(testAmount100USDC - testBonderFeeUSDC)), + ApprovalRequired: true, + }, + { + ProcessorName: pathprocessor.ProcessorBridgeHopName, + FromChain: &optimism, + ToChain: &arbitrum, + AmountOut: (*hexutil.Big)(big.NewInt(2.5*testAmount100USDC - testBonderFeeUSDC)), + ApprovalRequired: true, + }, + { + ProcessorName: pathprocessor.ProcessorTransferName, + FromChain: &arbitrum, + ToChain: &arbitrum, + AmountOut: (*hexutil.Big)(big.NewInt(0.5 * testAmount100USDC)), + ApprovalRequired: false, + }, + { + ProcessorName: pathprocessor.ProcessorTransferName, + FromChain: &arbitrum, + ToChain: &arbitrum, + AmountOut: (*hexutil.Big)(big.NewInt(testAmount100USDC)), + ApprovalRequired: false, + }, + { + ProcessorName: pathprocessor.ProcessorTransferName, + FromChain: &arbitrum, + ToChain: &arbitrum, + AmountOut: (*hexutil.Big)(big.NewInt(2.5 * testAmount100USDC)), + ApprovalRequired: false, + }, + { + ProcessorName: pathprocessor.ProcessorBridgeHopName, + FromChain: &arbitrum, + ToChain: &mainnet, + AmountOut: (*hexutil.Big)(big.NewInt(0.5*testAmount100USDC - testBonderFeeUSDC)), + ApprovalRequired: true, + }, + { + ProcessorName: pathprocessor.ProcessorBridgeHopName, + FromChain: &arbitrum, + ToChain: &mainnet, + AmountOut: (*hexutil.Big)(big.NewInt(testAmount100USDC - testBonderFeeUSDC)), + ApprovalRequired: true, + }, + { + ProcessorName: pathprocessor.ProcessorBridgeHopName, + FromChain: &arbitrum, + ToChain: &mainnet, + AmountOut: (*hexutil.Big)(big.NewInt(2.5*testAmount100USDC - testBonderFeeUSDC)), + ApprovalRequired: true, + }, + { + ProcessorName: pathprocessor.ProcessorBridgeHopName, + FromChain: &arbitrum, + ToChain: &optimism, + AmountOut: (*hexutil.Big)(big.NewInt(0.5*testAmount100USDC - testBonderFeeUSDC)), + ApprovalRequired: true, + }, + { + ProcessorName: pathprocessor.ProcessorBridgeHopName, + FromChain: &arbitrum, + ToChain: &optimism, + AmountOut: (*hexutil.Big)(big.NewInt(testAmount100USDC - testBonderFeeUSDC)), + ApprovalRequired: true, + }, + { + ProcessorName: pathprocessor.ProcessorBridgeHopName, + FromChain: &arbitrum, + ToChain: &optimism, + AmountOut: (*hexutil.Big)(big.NewInt(2.5*testAmount100USDC - testBonderFeeUSDC)), + ApprovalRequired: true, + }, + }, + }, { name: "Bridge - No Specific FromChain - No Specific ToChain", input: &RouteInputParams{ @@ -2321,9 +2562,11 @@ func TestNoBalanceForTheBestRouteRouterV2(t *testing.T) { Symbol: pathprocessor.UsdcSymbol, Decimals: 6, }, - tokenPrices: testTokenPrices, - suggestedFees: testSuggestedFees, - balanceMap: map[string]*big.Int{}, + tokenPrices: testTokenPrices, + suggestedFees: testSuggestedFees, + balanceMap: map[string]*big.Int{ + makeBalanceKey(walletCommon.OptimismMainnet, pathprocessor.UsdcSymbol): big.NewInt(0), + }, estimationMap: testEstimationMap, bonderFeeMap: testBbonderFeeMap, approvalGasEstimation: testApprovalGasEstimation, @@ -2364,7 +2607,8 @@ func TestNoBalanceForTheBestRouteRouterV2(t *testing.T) { tokenPrices: testTokenPrices, suggestedFees: testSuggestedFees, balanceMap: map[string]*big.Int{ - makeTestBalanceKey(walletCommon.OptimismMainnet, pathprocessor.UsdcSymbol): big.NewInt(testAmount100USDC), + makeBalanceKey(walletCommon.OptimismMainnet, pathprocessor.UsdcSymbol): big.NewInt(testAmount100USDC), + makeBalanceKey(walletCommon.OptimismMainnet, pathprocessor.EthSymbol): big.NewInt(0), }, estimationMap: testEstimationMap, bonderFeeMap: testBbonderFeeMap, @@ -2402,9 +2646,16 @@ func TestNoBalanceForTheBestRouteRouterV2(t *testing.T) { Symbol: pathprocessor.UsdcSymbol, Decimals: 6, }, - tokenPrices: testTokenPrices, - suggestedFees: testSuggestedFees, - balanceMap: map[string]*big.Int{}, + tokenPrices: testTokenPrices, + suggestedFees: testSuggestedFees, + balanceMap: map[string]*big.Int{ + makeBalanceKey(walletCommon.EthereumMainnet, pathprocessor.UsdcSymbol): big.NewInt(0), + makeBalanceKey(walletCommon.EthereumMainnet, pathprocessor.EthSymbol): big.NewInt(0), + makeBalanceKey(walletCommon.OptimismMainnet, pathprocessor.UsdcSymbol): big.NewInt(0), + makeBalanceKey(walletCommon.OptimismMainnet, pathprocessor.EthSymbol): big.NewInt(0), + makeBalanceKey(walletCommon.ArbitrumMainnet, pathprocessor.UsdcSymbol): big.NewInt(0), + makeBalanceKey(walletCommon.ArbitrumMainnet, pathprocessor.EthSymbol): big.NewInt(0), + }, estimationMap: testEstimationMap, bonderFeeMap: testBbonderFeeMap, approvalGasEstimation: testApprovalGasEstimation, @@ -2454,9 +2705,12 @@ func TestNoBalanceForTheBestRouteRouterV2(t *testing.T) { tokenPrices: testTokenPrices, suggestedFees: testSuggestedFees, balanceMap: map[string]*big.Int{ - makeTestBalanceKey(walletCommon.ArbitrumMainnet, pathprocessor.UsdcSymbol): big.NewInt(testAmount100USDC + testAmount100USDC), - makeTestBalanceKey(walletCommon.EthereumMainnet, pathprocessor.UsdcSymbol): big.NewInt(testAmount100USDC + testAmount100USDC), - makeTestBalanceKey(walletCommon.OptimismMainnet, pathprocessor.UsdcSymbol): big.NewInt(testAmount100USDC + testAmount100USDC), + makeBalanceKey(walletCommon.ArbitrumMainnet, pathprocessor.UsdcSymbol): big.NewInt(testAmount100USDC + testAmount100USDC), + makeBalanceKey(walletCommon.EthereumMainnet, pathprocessor.UsdcSymbol): big.NewInt(testAmount100USDC + testAmount100USDC), + makeBalanceKey(walletCommon.OptimismMainnet, pathprocessor.UsdcSymbol): big.NewInt(testAmount100USDC + testAmount100USDC), + makeBalanceKey(walletCommon.ArbitrumMainnet, pathprocessor.EthSymbol): big.NewInt(0), + makeBalanceKey(walletCommon.EthereumMainnet, pathprocessor.EthSymbol): big.NewInt(0), + makeBalanceKey(walletCommon.OptimismMainnet, pathprocessor.EthSymbol): big.NewInt(0), }, estimationMap: testEstimationMap, bonderFeeMap: testBbonderFeeMap, @@ -2509,8 +2763,8 @@ func TestNoBalanceForTheBestRouteRouterV2(t *testing.T) { tokenPrices: testTokenPrices, suggestedFees: testSuggestedFees, balanceMap: map[string]*big.Int{ - makeTestBalanceKey(walletCommon.ArbitrumMainnet, pathprocessor.UsdcSymbol): big.NewInt(testAmount100USDC + testAmount100USDC), - makeTestBalanceKey(walletCommon.ArbitrumMainnet, pathprocessor.EthSymbol): big.NewInt(testAmount1ETHInWei), + makeBalanceKey(walletCommon.ArbitrumMainnet, pathprocessor.UsdcSymbol): big.NewInt(testAmount100USDC + testAmount100USDC), + makeBalanceKey(walletCommon.ArbitrumMainnet, pathprocessor.EthSymbol): big.NewInt(testAmount1ETHInWei), }, estimationMap: testEstimationMap, bonderFeeMap: testBbonderFeeMap, @@ -2602,420 +2856,441 @@ func TestNoBalanceForTheBestRouteRouterV2(t *testing.T) { } } -func TestValidateInputData(t *testing.T) { - testCases := []struct { - name string - input *RouteInputParams - expectedError error +func TestAmountOptions(t *testing.T) { + router, cleanTmpDb := setupRouter(t) + defer cleanTmpDb() + + tests := []struct { + name string + input *RouteInputParams + expectedAmountOptions map[uint64][]amountOption }{ { - name: "ENSRegister valid data on testnet", + name: "Transfer - Single From Chain - No Locked Amount", input: &RouteInputParams{ - SendType: ENSRegister, - Username: "validusername.eth", - PublicKey: "validpublickey", - TokenID: pathprocessor.SttSymbol, - testnetMode: true, - }, - expectedError: nil, - }, - { - name: "ENSRegister valid data on mainnet", - input: &RouteInputParams{ - SendType: ENSRegister, - Username: "validusername.eth", - PublicKey: "validpublickey", - TokenID: pathprocessor.SntSymbol, - }, - expectedError: nil, - }, - { - name: "ENSRegister missing username", - input: &RouteInputParams{ - SendType: ENSRegister, - PublicKey: "validpublickey", - TokenID: pathprocessor.SttSymbol, - testnetMode: true, - }, - expectedError: ErrENSRegisterRequiresUsernameAndPubKey, - }, - { - name: "ENSRegister missing public key", - input: &RouteInputParams{ - SendType: ENSRegister, - Username: "validusername.eth", - TokenID: pathprocessor.SttSymbol, - testnetMode: true, - }, - expectedError: ErrENSRegisterRequiresUsernameAndPubKey, - }, - { - name: "ENSRegister invalid token on testnet", - input: &RouteInputParams{ - SendType: ENSRegister, - Username: "validusername.eth", - PublicKey: "validpublickey", - TokenID: "invalidtoken", - testnetMode: true, - }, - expectedError: ErrENSRegisterTestnetSTTOnly, - }, - { - name: "ENSRegister invalid token on mainnet", - input: &RouteInputParams{ - SendType: ENSRegister, - Username: "validusername.eth", - PublicKey: "validpublickey", - TokenID: "invalidtoken", - }, - expectedError: ErrENSRegisterMainnetSNTOnly, - }, - { - name: "ENSRegister valid data with mixed case username on mainnet", - input: &RouteInputParams{ - SendType: ENSRegister, - Username: "ValidUsername.eth", - PublicKey: "validpublickey", - TokenID: pathprocessor.SntSymbol, - }, - expectedError: nil, - }, - /* - TODO we should introduce proper ENS validation - { - name: "ENSRegister with special characters in username", - input: &RouteInputParams{ - SendType: ENSRegister, - Username: "validuser!@#name.eth", - PublicKey: "validpublickey", - TokenID: pathprocessor.SntSymbol, + testnetMode: false, + SendType: Transfer, + AmountIn: (*hexutil.Big)(big.NewInt(testAmount1ETHInWei)), + TokenID: pathprocessor.EthSymbol, + DisabledFromChainIDs: []uint64{walletCommon.EthereumMainnet, walletCommon.ArbitrumMainnet}, + + testsMode: true, + testParams: &routerTestParams{ + tokenFrom: &token.Token{ + ChainID: 1, + Symbol: pathprocessor.EthSymbol, + Decimals: 18, + }, + balanceMap: map[string]*big.Int{}, }, - expectedError: ErrENSSetPubKeyInvalidUsername, }, - { - name: "ENSRegister with leading and trailing spaces in username", - input: &RouteInputParams{ - SendType: ENSRegister, - Username: " validusername.eth ", - PublicKey: "validpublickey", - TokenID: pathprocessor.SntSymbol, + expectedAmountOptions: map[uint64][]amountOption{ + walletCommon.OptimismMainnet: { + { + amount: big.NewInt(testAmount1ETHInWei), + locked: false, + }, }, - expectedError: ErrENSSetPubKeyInvalidUsername, }, - */ - { - name: "ENSRelease valid data", - input: &RouteInputParams{ - SendType: ENSRelease, - Username: "validusername.eth", - }, - expectedError: nil, }, { - name: "ENSRelease missing username", - input: &RouteInputParams{ - SendType: ENSRelease, - }, - expectedError: ErrENSReleaseRequiresUsername, - }, - { - name: "ENSSetPubKey valid data", - input: &RouteInputParams{ - SendType: ENSSetPubKey, - Username: "validusername.eth", - PublicKey: "validpublickey", - }, - expectedError: nil, - }, - { - name: "ENSSetPubKey missing username", - input: &RouteInputParams{ - SendType: ENSSetPubKey, - PublicKey: "validpublickey", - }, - expectedError: ErrENSSetPubKeyRequiresUsernameAndPubKey, - }, - { - name: "ENSSetPubKey missing public key", - input: &RouteInputParams{ - SendType: ENSSetPubKey, - Username: "validusername", - }, - expectedError: ErrENSSetPubKeyRequiresUsernameAndPubKey, - }, - { - name: "ENSSetPubKey invalid ENS username", - input: &RouteInputParams{ - SendType: ENSSetPubKey, - Username: "invalidusername", - PublicKey: "validpublickey", - }, - expectedError: ErrENSSetPubKeyInvalidUsername, - }, - { - name: "StickersBuy missing packID", - input: &RouteInputParams{ - SendType: StickersBuy, - }, - expectedError: ErrStickersBuyRequiresPackID, - }, - { - name: "Swap missing toTokenID", - input: &RouteInputParams{ - SendType: Swap, - }, - expectedError: ErrSwapRequiresToTokenID, - }, - { - name: "Swap tokenID equal to toTokenID", - input: &RouteInputParams{ - SendType: Swap, - TokenID: "token", - ToTokenID: "token", - }, - expectedError: ErrSwapTokenIDMustBeDifferent, - }, - { - name: "Swap both amountIn and amountOut set", - input: &RouteInputParams{ - SendType: Swap, - TokenID: "token1", - ToTokenID: "token2", - AmountIn: (*hexutil.Big)(big.NewInt(100)), - AmountOut: (*hexutil.Big)(big.NewInt(100)), - }, - expectedError: ErrSwapAmountInAmountOutMustBeExclusive, - }, - { - name: "Swap negative amountIn", - input: &RouteInputParams{ - SendType: Swap, - TokenID: "token1", - ToTokenID: "token2", - AmountIn: (*hexutil.Big)(big.NewInt(-100)), - }, - expectedError: ErrSwapAmountInMustBePositive, - }, - { - name: "Swap negative amountOut", - input: &RouteInputParams{ - SendType: Swap, - TokenID: "token1", - ToTokenID: "token2", - AmountOut: (*hexutil.Big)(big.NewInt(-100)), - }, - expectedError: ErrSwapAmountOutMustBePositive, - }, - { - name: "Swap with very large amountIn", - input: &RouteInputParams{ - SendType: Swap, - TokenID: "token1", - ToTokenID: "token2", - AmountIn: (*hexutil.Big)(big.NewInt(1e18)), - }, - expectedError: nil, - }, - { - name: "Swap with very large amountOut", - input: &RouteInputParams{ - SendType: Swap, - TokenID: "token1", - ToTokenID: "token2", - AmountOut: (*hexutil.Big)(big.NewInt(1e18)), - }, - expectedError: nil, - }, - { - name: "Swap with very small amountIn", - input: &RouteInputParams{ - SendType: Swap, - TokenID: "token1", - ToTokenID: "token2", - AmountIn: (*hexutil.Big)(big.NewInt(1)), - }, - expectedError: nil, - }, - { - name: "Swap with very small amountOut", - input: &RouteInputParams{ - SendType: Swap, - TokenID: "token1", - ToTokenID: "token2", - AmountOut: (*hexutil.Big)(big.NewInt(1)), - }, - expectedError: nil, - }, - { - name: "fromLockedAmount with supported network on testnet", + name: "Transfer - Single From Chain - Locked Amount To Single Chain Equal Total Amount", input: &RouteInputParams{ + testnetMode: false, + SendType: Transfer, + AmountIn: (*hexutil.Big)(big.NewInt(testAmount1ETHInWei)), + TokenID: pathprocessor.EthSymbol, + DisabledFromChainIDs: []uint64{walletCommon.EthereumMainnet, walletCommon.ArbitrumMainnet}, FromLockedAmount: map[uint64]*hexutil.Big{ - walletCommon.EthereumSepolia: (*hexutil.Big)(big.NewInt(10)), + walletCommon.OptimismMainnet: (*hexutil.Big)(big.NewInt(testAmount1ETHInWei)), + }, + + testsMode: true, + testParams: &routerTestParams{ + tokenFrom: &token.Token{ + ChainID: 1, + Symbol: pathprocessor.EthSymbol, + Decimals: 18, + }, + balanceMap: map[string]*big.Int{}, + }, + }, + expectedAmountOptions: map[uint64][]amountOption{ + walletCommon.OptimismMainnet: { + { + amount: big.NewInt(testAmount1ETHInWei), + locked: true, + }, }, - testnetMode: true, - AmountIn: (*hexutil.Big)(big.NewInt(20)), }, - expectedError: nil, }, { - name: "fromLockedAmount with supported network on mainnet", + name: "Transfer - Multiple From Chains - Locked Amount To Single Chain Is Less Than Total Amount", input: &RouteInputParams{ + testnetMode: false, + SendType: Transfer, + AmountIn: (*hexutil.Big)(big.NewInt(testAmount2ETHInWei)), + TokenID: pathprocessor.EthSymbol, + DisabledFromChainIDs: []uint64{walletCommon.EthereumMainnet}, FromLockedAmount: map[uint64]*hexutil.Big{ - walletCommon.EthereumMainnet: (*hexutil.Big)(big.NewInt(10)), + walletCommon.OptimismMainnet: (*hexutil.Big)(big.NewInt(testAmount1ETHInWei)), + }, + + testsMode: true, + testParams: &routerTestParams{ + tokenFrom: &token.Token{ + ChainID: 1, + Symbol: pathprocessor.EthSymbol, + Decimals: 18, + }, + balanceMap: map[string]*big.Int{}, + }, + }, + expectedAmountOptions: map[uint64][]amountOption{ + walletCommon.OptimismMainnet: { + { + amount: big.NewInt(testAmount1ETHInWei), + locked: true, + }, + }, + walletCommon.ArbitrumMainnet: { + { + amount: big.NewInt(testAmount1ETHInWei), + locked: false, + }, }, - AmountIn: (*hexutil.Big)(big.NewInt(20)), }, - expectedError: nil, }, { - name: "fromLockedAmount with supported mainnet network while in test mode", + name: "Transfer - Multiple From Chains - Locked Amount To Multiple Chains", input: &RouteInputParams{ + testnetMode: false, + SendType: Transfer, + AmountIn: (*hexutil.Big)(big.NewInt(testAmount2ETHInWei)), + TokenID: pathprocessor.EthSymbol, + DisabledFromChainIDs: []uint64{walletCommon.EthereumMainnet}, FromLockedAmount: map[uint64]*hexutil.Big{ - walletCommon.EthereumMainnet: (*hexutil.Big)(big.NewInt(10)), + walletCommon.OptimismMainnet: (*hexutil.Big)(big.NewInt(testAmount1ETHInWei)), + walletCommon.ArbitrumMainnet: (*hexutil.Big)(big.NewInt(testAmount1ETHInWei)), + }, + + testsMode: true, + testParams: &routerTestParams{ + tokenFrom: &token.Token{ + ChainID: 1, + Symbol: pathprocessor.EthSymbol, + Decimals: 18, + }, + balanceMap: map[string]*big.Int{}, + }, + }, + expectedAmountOptions: map[uint64][]amountOption{ + walletCommon.OptimismMainnet: { + { + amount: big.NewInt(testAmount1ETHInWei), + locked: true, + }, + }, + walletCommon.ArbitrumMainnet: { + { + amount: big.NewInt(testAmount1ETHInWei), + locked: true, + }, }, - testnetMode: true, }, - expectedError: ErrLockedAmountNotSupportedForNetwork, }, { - name: "fromLockedAmount with unsupported network on testnet", + name: "Transfer - All From Chains - Locked Amount To Multiple Chains Equal Total Amount", input: &RouteInputParams{ + testnetMode: false, + SendType: Transfer, + AmountIn: (*hexutil.Big)(big.NewInt(testAmount2ETHInWei)), + TokenID: pathprocessor.EthSymbol, FromLockedAmount: map[uint64]*hexutil.Big{ - 999: (*hexutil.Big)(big.NewInt(10)), + walletCommon.OptimismMainnet: (*hexutil.Big)(big.NewInt(testAmount1ETHInWei)), + walletCommon.ArbitrumMainnet: (*hexutil.Big)(big.NewInt(testAmount1ETHInWei)), + }, + + testsMode: true, + testParams: &routerTestParams{ + tokenFrom: &token.Token{ + ChainID: 1, + Symbol: pathprocessor.EthSymbol, + Decimals: 18, + }, + balanceMap: map[string]*big.Int{}, + }, + }, + expectedAmountOptions: map[uint64][]amountOption{ + walletCommon.OptimismMainnet: { + { + amount: big.NewInt(testAmount1ETHInWei), + locked: true, + }, + }, + walletCommon.ArbitrumMainnet: { + { + amount: big.NewInt(testAmount1ETHInWei), + locked: true, + }, }, - testnetMode: true, }, - expectedError: ErrLockedAmountNotSupportedForNetwork, }, { - name: "fromLockedAmount with unsupported network on mainnet", + name: "Transfer - All From Chains - Locked Amount To Multiple Chains Is Less Than Total Amount", input: &RouteInputParams{ + testnetMode: false, + SendType: Transfer, + AmountIn: (*hexutil.Big)(big.NewInt(testAmount5ETHInWei)), + TokenID: pathprocessor.EthSymbol, FromLockedAmount: map[uint64]*hexutil.Big{ - 999: (*hexutil.Big)(big.NewInt(10)), + walletCommon.OptimismMainnet: (*hexutil.Big)(big.NewInt(testAmount1ETHInWei)), + walletCommon.ArbitrumMainnet: (*hexutil.Big)(big.NewInt(testAmount1ETHInWei)), + }, + + testsMode: true, + testParams: &routerTestParams{ + tokenFrom: &token.Token{ + ChainID: 1, + Symbol: pathprocessor.EthSymbol, + Decimals: 18, + }, + balanceMap: map[string]*big.Int{}, + }, + }, + expectedAmountOptions: map[uint64][]amountOption{ + walletCommon.OptimismMainnet: { + { + amount: big.NewInt(testAmount1ETHInWei), + locked: true, + }, + }, + walletCommon.ArbitrumMainnet: { + { + amount: big.NewInt(testAmount1ETHInWei), + locked: true, + }, + }, + walletCommon.EthereumMainnet: { + { + amount: big.NewInt(testAmount3ETHInWei), + locked: false, + }, }, }, - expectedError: ErrLockedAmountNotSupportedForNetwork, }, { - name: "fromLockedAmount with negative amount", + name: "Transfer - All From Chain - No Locked Amount - Enough Token Balance If All Chains Are Used", input: &RouteInputParams{ + testnetMode: false, + SendType: Transfer, + AmountIn: (*hexutil.Big)(big.NewInt(testAmount3ETHInWei)), + TokenID: pathprocessor.EthSymbol, + + testsMode: true, + testParams: &routerTestParams{ + tokenFrom: &token.Token{ + ChainID: 1, + Symbol: pathprocessor.EthSymbol, + Decimals: 18, + }, + balanceMap: map[string]*big.Int{ + makeBalanceKey(walletCommon.EthereumMainnet, pathprocessor.EthSymbol): big.NewInt(testAmount1ETHInWei), + makeBalanceKey(walletCommon.OptimismMainnet, pathprocessor.EthSymbol): big.NewInt(testAmount1ETHInWei), + makeBalanceKey(walletCommon.ArbitrumMainnet, pathprocessor.EthSymbol): big.NewInt(testAmount1ETHInWei), + }, + }, + }, + expectedAmountOptions: map[uint64][]amountOption{ + walletCommon.OptimismMainnet: { + { + amount: big.NewInt(testAmount3ETHInWei), + locked: false, + }, + { + amount: big.NewInt(testAmount1ETHInWei), + locked: false, + }, + }, + walletCommon.ArbitrumMainnet: { + { + amount: big.NewInt(testAmount3ETHInWei), + locked: false, + }, + { + amount: big.NewInt(testAmount1ETHInWei), + locked: false, + }, + }, + walletCommon.EthereumMainnet: { + { + amount: big.NewInt(testAmount3ETHInWei), + locked: false, + }, + { + amount: big.NewInt(testAmount1ETHInWei), + locked: false, + }, + }, + }, + }, + { + name: "Transfer - All From Chain - Locked Amount To Single Chain - Enough Token Balance If All Chains Are Used", + input: &RouteInputParams{ + testnetMode: false, + SendType: Transfer, + AmountIn: (*hexutil.Big)(big.NewInt(testAmount3ETHInWei)), + TokenID: pathprocessor.EthSymbol, FromLockedAmount: map[uint64]*hexutil.Big{ - walletCommon.EthereumMainnet: (*hexutil.Big)(big.NewInt(-10)), + walletCommon.OptimismMainnet: (*hexutil.Big)(big.NewInt(testAmount0Point5ETHInWei)), + }, + + testsMode: true, + testParams: &routerTestParams{ + tokenFrom: &token.Token{ + ChainID: 1, + Symbol: pathprocessor.EthSymbol, + Decimals: 18, + }, + balanceMap: map[string]*big.Int{ + makeBalanceKey(walletCommon.EthereumMainnet, pathprocessor.EthSymbol): big.NewInt(testAmount2ETHInWei), + makeBalanceKey(walletCommon.OptimismMainnet, pathprocessor.EthSymbol): big.NewInt(testAmount1ETHInWei), + makeBalanceKey(walletCommon.ArbitrumMainnet, pathprocessor.EthSymbol): big.NewInt(testAmount3ETHInWei), + }, + }, + }, + expectedAmountOptions: map[uint64][]amountOption{ + walletCommon.OptimismMainnet: { + { + amount: big.NewInt(testAmount0Point5ETHInWei), + locked: true, + }, + }, + walletCommon.ArbitrumMainnet: { + { + amount: big.NewInt(testAmount2ETHInWei + testAmount0Point5ETHInWei), + locked: false, + }, + { + amount: big.NewInt(testAmount0Point5ETHInWei), + locked: false, + }, + }, + walletCommon.EthereumMainnet: { + { + amount: big.NewInt(testAmount2ETHInWei + testAmount0Point5ETHInWei), + locked: false, + }, + { + amount: big.NewInt(testAmount2ETHInWei), + locked: false, + }, }, }, - expectedError: ErrLockedAmountNotNegative, }, { - name: "fromLockedAmount with zero amount", + name: "Transfer - All From Chain - Locked Amount To Multiple Chains - Enough Token Balance If All Chains Are Used", input: &RouteInputParams{ + testnetMode: false, + SendType: Transfer, + AmountIn: (*hexutil.Big)(big.NewInt(testAmount3ETHInWei)), + TokenID: pathprocessor.EthSymbol, FromLockedAmount: map[uint64]*hexutil.Big{ - walletCommon.EthereumMainnet: (*hexutil.Big)(big.NewInt(0)), + walletCommon.OptimismMainnet: (*hexutil.Big)(big.NewInt(testAmount0Point5ETHInWei)), + walletCommon.EthereumMainnet: (*hexutil.Big)(big.NewInt(testAmount1ETHInWei)), }, - AmountIn: (*hexutil.Big)(big.NewInt(20)), - }, - expectedError: nil, - }, - { - name: "fromLockedAmount with zero amounts", - input: &RouteInputParams{ - FromLockedAmount: map[uint64]*hexutil.Big{ - walletCommon.EthereumMainnet: (*hexutil.Big)(big.NewInt(0)), - walletCommon.OptimismMainnet: (*hexutil.Big)(big.NewInt(0)), - }, - AmountIn: (*hexutil.Big)(big.NewInt(20)), - }, - expectedError: nil, - }, - { - name: "fromLockedAmount with all supported networks with zero amount", - input: &RouteInputParams{ - FromLockedAmount: map[uint64]*hexutil.Big{ - walletCommon.EthereumMainnet: (*hexutil.Big)(big.NewInt(0)), - walletCommon.OptimismMainnet: (*hexutil.Big)(big.NewInt(0)), - walletCommon.ArbitrumMainnet: (*hexutil.Big)(big.NewInt(0)), - }, - AmountIn: (*hexutil.Big)(big.NewInt(20)), - }, - expectedError: ErrLockedAmountExcludesAllSupported, - }, - { - name: "fromLockedAmount with all supported test networks with zero amount", - input: &RouteInputParams{ - FromLockedAmount: map[uint64]*hexutil.Big{ - walletCommon.EthereumSepolia: (*hexutil.Big)(big.NewInt(0)), - walletCommon.OptimismSepolia: (*hexutil.Big)(big.NewInt(0)), - walletCommon.ArbitrumSepolia: (*hexutil.Big)(big.NewInt(0)), - }, - testnetMode: true, - AmountIn: (*hexutil.Big)(big.NewInt(20)), - }, - expectedError: ErrLockedAmountExcludesAllSupported, - }, - { - name: "fromLockedAmount with mixed supported and unsupported networks on testnet", - input: &RouteInputParams{ - FromLockedAmount: map[uint64]*hexutil.Big{ - walletCommon.EthereumSepolia: (*hexutil.Big)(big.NewInt(10)), - 999: (*hexutil.Big)(big.NewInt(10)), - }, - testnetMode: true, - }, - expectedError: ErrLockedAmountNotSupportedForNetwork, - }, - { - name: "fromLockedAmount with mixed supported and unsupported networks on mainnet", - input: &RouteInputParams{ - FromLockedAmount: map[uint64]*hexutil.Big{ - walletCommon.EthereumMainnet: (*hexutil.Big)(big.NewInt(10)), - 999: (*hexutil.Big)(big.NewInt(10)), + + testsMode: true, + testParams: &routerTestParams{ + tokenFrom: &token.Token{ + ChainID: 1, + Symbol: pathprocessor.EthSymbol, + Decimals: 18, + }, + balanceMap: map[string]*big.Int{ + makeBalanceKey(walletCommon.EthereumMainnet, pathprocessor.EthSymbol): big.NewInt(testAmount2ETHInWei), + makeBalanceKey(walletCommon.OptimismMainnet, pathprocessor.EthSymbol): big.NewInt(testAmount1ETHInWei), + makeBalanceKey(walletCommon.ArbitrumMainnet, pathprocessor.EthSymbol): big.NewInt(testAmount3ETHInWei), + }, + }, + }, + expectedAmountOptions: map[uint64][]amountOption{ + walletCommon.OptimismMainnet: { + { + amount: big.NewInt(testAmount0Point5ETHInWei), + locked: true, + }, + }, + walletCommon.ArbitrumMainnet: { + { + amount: big.NewInt(testAmount1ETHInWei + testAmount0Point5ETHInWei), + locked: false, + }, + }, + walletCommon.EthereumMainnet: { + { + amount: big.NewInt(testAmount1ETHInWei), + locked: true, + }, }, }, - expectedError: ErrLockedAmountNotSupportedForNetwork, }, { - name: "fromLockedAmount with null map on testnet", + name: "Transfer - All From Chain - No Locked Amount - Not Enough Token Balance", input: &RouteInputParams{ - FromLockedAmount: nil, - testnetMode: true, - AmountIn: (*hexutil.Big)(big.NewInt(20)), + testnetMode: false, + SendType: Transfer, + AmountIn: (*hexutil.Big)(big.NewInt(testAmount5ETHInWei)), + TokenID: pathprocessor.EthSymbol, + + testsMode: true, + testParams: &routerTestParams{ + tokenFrom: &token.Token{ + ChainID: 1, + Symbol: pathprocessor.EthSymbol, + Decimals: 18, + }, + balanceMap: map[string]*big.Int{ + makeBalanceKey(walletCommon.EthereumMainnet, pathprocessor.EthSymbol): big.NewInt(testAmount1ETHInWei), + makeBalanceKey(walletCommon.OptimismMainnet, pathprocessor.EthSymbol): big.NewInt(testAmount1ETHInWei), + makeBalanceKey(walletCommon.ArbitrumMainnet, pathprocessor.EthSymbol): big.NewInt(testAmount1ETHInWei), + }, + }, }, - expectedError: nil, - }, - { - name: "fromLockedAmount with null map on mainnet", - input: &RouteInputParams{ - FromLockedAmount: nil, - AmountIn: (*hexutil.Big)(big.NewInt(20)), + expectedAmountOptions: map[uint64][]amountOption{ + walletCommon.OptimismMainnet: { + { + amount: big.NewInt(testAmount5ETHInWei), + locked: false, + }, + }, + walletCommon.ArbitrumMainnet: { + { + amount: big.NewInt(testAmount5ETHInWei), + locked: false, + }, + }, + walletCommon.EthereumMainnet: { + { + amount: big.NewInt(testAmount5ETHInWei), + locked: false, + }, + }, }, - expectedError: nil, - }, - { - name: "fromLockedAmount with empty map on testnet", - input: &RouteInputParams{ - FromLockedAmount: map[uint64]*hexutil.Big{}, - testnetMode: true, - AmountIn: (*hexutil.Big)(big.NewInt(20)), - }, - expectedError: nil, - }, - { - name: "fromLockedAmount with empty map on mainnet", - input: &RouteInputParams{ - FromLockedAmount: map[uint64]*hexutil.Big{}, - AmountIn: (*hexutil.Big)(big.NewInt(20)), - }, - expectedError: nil, }, } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - err := validateInputData(tc.input) - if tc.expectedError == nil { - assert.NoError(t, err) - } else { - assert.EqualError(t, err, tc.expectedError.Error()) - } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + selectedFromChains, _, err := router.getSelectedChains(tt.input) + assert.NoError(t, err) + + amountOptions, err := router.findOptionsForSendingAmount(tt.input, selectedFromChains, tt.input.testParams.balanceMap) + assert.NoError(t, err) + + assert.Equal(t, len(tt.expectedAmountOptions), len(amountOptions)) + assert.True(t, amountOptionsMapsEqual(tt.expectedAmountOptions, amountOptions)) }) } }