diff --git a/services/wallet/api.go b/services/wallet/api.go index ddaf4239e..76339b114 100644 --- a/services/wallet/api.go +++ b/services/wallet/api.go @@ -488,31 +488,6 @@ func gweiToWei(val *big.Float) *big.Int { return res } -func (api *API) GetSuggestedRoutes( - ctx context.Context, - sendType router.SendType, - addrFrom common.Address, - addrTo common.Address, - amountIn *hexutil.Big, - tokenID string, - toTokenID string, - disabledFromChainIDs, - disabledToChainIDs, - preferedChainIDs []uint64, - gasFeeMode router.GasFeeMode, - fromLockedAmount map[uint64]*hexutil.Big, -) (*router.SuggestedRoutes, error) { - log.Debug("call to GetSuggestedRoutes") - - testnetMode, err := api.s.rpcClient.NetworkManager.GetTestNetworksEnabled() - if err != nil { - return nil, err - } - - return api.router.SuggestedRoutes(ctx, sendType, addrFrom, addrTo, amountIn.ToInt(), tokenID, toTokenID, disabledFromChainIDs, - disabledToChainIDs, preferedChainIDs, gasFeeMode, fromLockedAmount, testnetMode) -} - func (api *API) GetSuggestedRoutesV2(ctx context.Context, input *router.RouteInputParams) (*router.SuggestedRoutesV2, error) { log.Debug("call to GetSuggestedRoutesV2") diff --git a/services/wallet/router/common.go b/services/wallet/router/common.go new file mode 100644 index 000000000..9c1725fd2 --- /dev/null +++ b/services/wallet/router/common.go @@ -0,0 +1,32 @@ +package router + +import ( + "github.com/status-im/status-go/params" +) + +func arrayContainsElement[T comparable](el T, arr []T) bool { + for _, e := range arr { + if e == el { + return true + } + } + return false +} + +func arraysWithSameElements[T comparable](ar1 []T, ar2 []T, isEqual func(T, T) bool) bool { + if len(ar1) != len(ar2) { + return false + } + for _, el := range ar1 { + if !arrayContainsElement(el, ar2) { + return false + } + } + return true +} + +func isSingleChainOperation(fromChains []*params.Network, toChains []*params.Network) bool { + return len(fromChains) == 1 && + len(toChains) == 1 && + fromChains[0].ChainID == toChains[0].ChainID +} diff --git a/services/wallet/router/fees.go b/services/wallet/router/fees.go index 4dd45f457..1b25b7f6a 100644 --- a/services/wallet/router/fees.go +++ b/services/wallet/router/fees.go @@ -70,18 +70,6 @@ func (s *SuggestedFees) feeFor(mode GasFeeMode) *big.Int { return s.MaxFeesLevels.feeFor(mode) } -func (s *SuggestedFeesGwei) feeFor(mode GasFeeMode) *big.Float { - if mode == GasFeeLow { - return s.MaxFeePerGasLow - } - - if mode == GasFeeHigh { - return s.MaxFeePerGasHigh - } - - return s.MaxFeePerGasMedium -} - const inclusionThreshold = 0.95 type TransactionEstimation int @@ -116,11 +104,6 @@ func gweiToEth(val *big.Float) *big.Float { return new(big.Float).Quo(val, big.NewFloat(1000000000)) } -func gweiToWei(val *big.Float) *big.Int { - res, _ := new(big.Float).Mul(val, big.NewFloat(1000000000)).Int(nil) - return res -} - func (f *FeeManager) SuggestedFees(ctx context.Context, chainID uint64) (*SuggestedFees, error) { backend, err := f.RPCClient.EthClient(chainID) if err != nil { diff --git a/services/wallet/router/router.go b/services/wallet/router/router.go deleted file mode 100644 index 28ecd7cf1..000000000 --- a/services/wallet/router/router.go +++ /dev/null @@ -1,737 +0,0 @@ -package router - -import ( - "context" - "errors" - "math" - "math/big" - "sort" - "strings" - "sync" - - "github.com/ethereum/go-ethereum" - "github.com/ethereum/go-ethereum/accounts/abi" - "github.com/ethereum/go-ethereum/accounts/abi/bind" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/status-im/status-go/contracts" - gaspriceoracle "github.com/status-im/status-go/contracts/gas-price-oracle" - "github.com/status-im/status-go/contracts/ierc20" - "github.com/status-im/status-go/params" - "github.com/status-im/status-go/rpc" - "github.com/status-im/status-go/services/ens" - "github.com/status-im/status-go/services/stickers" - "github.com/status-im/status-go/services/wallet/async" - "github.com/status-im/status-go/services/wallet/bigint" - "github.com/status-im/status-go/services/wallet/collectibles" - walletCommon "github.com/status-im/status-go/services/wallet/common" - "github.com/status-im/status-go/services/wallet/market" - "github.com/status-im/status-go/services/wallet/router/pathprocessor" - "github.com/status-im/status-go/services/wallet/token" - walletToken "github.com/status-im/status-go/services/wallet/token" - "github.com/status-im/status-go/transactions" -) - -// ////////////////////////////////////////////////////////////////////////////// -// TODO: once new router is in place, remove this `router.go` file, -// rename and make `router_v2.go` file the main and only file -// ////////////////////////////////////////////////////////////////////////////// - -// TODO: remove the following two consts once we fully move to routerV2 -const EstimateUsername = "RandomUsername" -const EstimatePubKey = "0x04bb2024ce5d72e45d4a4f8589ae657ef9745855006996115a23a1af88d536cf02c0524a585fce7bfa79d6a9669af735eda6205d6c7e5b3cdc2b8ff7b2fa1f0b56" - -type Path struct { - BridgeName string - From *params.Network - To *params.Network - MaxAmountIn *hexutil.Big - AmountIn *hexutil.Big - AmountInLocked bool - AmountOut *hexutil.Big - GasAmount uint64 - GasFees *SuggestedFeesGwei - BonderFees *hexutil.Big - TokenFees *big.Float - Cost *big.Float - EstimatedTime TransactionEstimation - ApprovalRequired bool - ApprovalGasFees *big.Float - ApprovalAmountRequired *hexutil.Big - ApprovalContractAddress *common.Address -} - -func (p *Path) Equal(o *Path) bool { - return p.From.ChainID == o.From.ChainID && p.To.ChainID == o.To.ChainID -} - -type Graph []*Node - -type Node struct { - Path *Path - Children Graph -} - -func newNode(path *Path) *Node { - return &Node{Path: path, Children: make(Graph, 0)} -} - -func buildGraph(AmountIn *big.Int, routes []*Path, level int, sourceChainIDs []uint64) Graph { - graph := make(Graph, 0) - for _, route := range routes { - found := false - for _, chainID := range sourceChainIDs { - if chainID == route.From.ChainID { - found = true - break - } - } - if found { - continue - } - node := newNode(route) - - newRoutes := make([]*Path, 0) - for _, r := range routes { - if route.Equal(r) { - continue - } - newRoutes = append(newRoutes, r) - } - - newAmountIn := new(big.Int).Sub(AmountIn, route.MaxAmountIn.ToInt()) - if newAmountIn.Sign() > 0 { - newSourceChainIDs := make([]uint64, len(sourceChainIDs)) - copy(newSourceChainIDs, sourceChainIDs) - newSourceChainIDs = append(newSourceChainIDs, route.From.ChainID) - node.Children = buildGraph(newAmountIn, newRoutes, level+1, newSourceChainIDs) - - if len(node.Children) == 0 { - continue - } - } - - graph = append(graph, node) - } - - return graph -} - -func (n Node) buildAllRoutes() [][]*Path { - res := make([][]*Path, 0) - - if len(n.Children) == 0 && n.Path != nil { - res = append(res, []*Path{n.Path}) - } - - for _, node := range n.Children { - for _, route := range node.buildAllRoutes() { - extendedRoute := route - if n.Path != nil { - extendedRoute = append([]*Path{n.Path}, route...) - } - res = append(res, extendedRoute) - } - } - - return res -} - -func filterRoutes(routes [][]*Path, amountIn *big.Int, fromLockedAmount map[uint64]*hexutil.Big) [][]*Path { - if len(fromLockedAmount) == 0 { - return routes - } - - filteredRoutesLevel1 := make([][]*Path, 0) - for _, route := range routes { - routeOk := true - fromIncluded := make(map[uint64]bool) - fromExcluded := make(map[uint64]bool) - for chainID, amount := range fromLockedAmount { - if amount.ToInt().Cmp(pathprocessor.ZeroBigIntValue) == 0 { - fromExcluded[chainID] = false - } else { - fromIncluded[chainID] = false - } - - } - for _, path := range route { - if _, ok := fromExcluded[path.From.ChainID]; ok { - routeOk = false - break - } - if _, ok := fromIncluded[path.From.ChainID]; ok { - fromIncluded[path.From.ChainID] = true - } - } - for _, value := range fromIncluded { - if !value { - routeOk = false - break - } - } - - if routeOk { - filteredRoutesLevel1 = append(filteredRoutesLevel1, route) - } - } - - filteredRoutesLevel2 := make([][]*Path, 0) - for _, route := range filteredRoutesLevel1 { - routeOk := true - for _, path := range route { - if amount, ok := fromLockedAmount[path.From.ChainID]; ok { - requiredAmountIn := new(big.Int).Sub(amountIn, amount.ToInt()) - restAmountIn := big.NewInt(0) - - for _, otherPath := range route { - if path.Equal(otherPath) { - continue - } - restAmountIn = new(big.Int).Add(otherPath.MaxAmountIn.ToInt(), restAmountIn) - } - if restAmountIn.Cmp(requiredAmountIn) >= 0 { - path.AmountIn = amount - path.AmountInLocked = true - } else { - routeOk = false - break - } - } - } - if routeOk { - filteredRoutesLevel2 = append(filteredRoutesLevel2, route) - } - } - - return filteredRoutesLevel2 -} - -func findBest(routes [][]*Path) []*Path { - var best []*Path - bestCost := big.NewFloat(math.Inf(1)) - for _, route := range routes { - currentCost := big.NewFloat(0) - for _, path := range route { - currentCost = new(big.Float).Add(currentCost, path.Cost) - } - - if currentCost.Cmp(bestCost) == -1 { - best = route - bestCost = currentCost - } - } - - return best -} - -type SuggestedRoutes struct { - Best []*Path - Candidates []*Path - TokenPrice float64 - NativeChainTokenPrice float64 -} - -func newSuggestedRoutes( - amountIn *big.Int, - candidates []*Path, - fromLockedAmount map[uint64]*hexutil.Big, -) *SuggestedRoutes { - if len(candidates) == 0 { - return &SuggestedRoutes{ - Candidates: candidates, - Best: candidates, - } - } - - node := &Node{ - Path: nil, - Children: buildGraph(amountIn, candidates, 0, []uint64{}), - } - routes := node.buildAllRoutes() - routes = filterRoutes(routes, amountIn, fromLockedAmount) - best := findBest(routes) - - if len(best) > 0 { - sort.Slice(best, func(i, j int) bool { - return best[i].AmountInLocked - }) - rest := new(big.Int).Set(amountIn) - for _, path := range best { - diff := new(big.Int).Sub(rest, path.MaxAmountIn.ToInt()) - if diff.Cmp(pathprocessor.ZeroBigIntValue) >= 0 { - path.AmountIn = (*hexutil.Big)(path.MaxAmountIn.ToInt()) - } else { - path.AmountIn = (*hexutil.Big)(new(big.Int).Set(rest)) - } - rest.Sub(rest, path.AmountIn.ToInt()) - } - } - - return &SuggestedRoutes{ - Candidates: candidates, - Best: best, - } -} - -func NewRouter(rpcClient *rpc.Client, transactor *transactions.Transactor, tokenManager *token.Manager, marketManager *market.Manager, - collectibles *collectibles.Service, collectiblesManager *collectibles.Manager, ensService *ens.Service, stickersService *stickers.Service) *Router { - processors := make(map[string]pathprocessor.PathProcessor) - - return &Router{ - rpcClient: rpcClient, - tokenManager: tokenManager, - marketManager: marketManager, - collectiblesService: collectibles, - collectiblesManager: collectiblesManager, - ensService: ensService, - stickersService: stickersService, - feesManager: &FeeManager{rpcClient}, - pathProcessors: processors, - scheduler: async.NewScheduler(), - } -} - -func (r *Router) AddPathProcessor(processor pathprocessor.PathProcessor) { - r.pathProcessors[processor.Name()] = processor -} - -func (r *Router) Stop() { - r.scheduler.Stop() -} - -func (r *Router) GetFeesManager() *FeeManager { - return r.feesManager -} - -func (r *Router) GetPathProcessors() map[string]pathprocessor.PathProcessor { - return r.pathProcessors -} - -func arrayContainsElement[T comparable](el T, arr []T) bool { - for _, e := range arr { - if e == el { - return true - } - } - return false -} - -func isSingleChainOperation(fromChains []*params.Network, toChains []*params.Network) bool { - return len(fromChains) == 1 && - len(toChains) == 1 && - fromChains[0].ChainID == toChains[0].ChainID -} - -type Router struct { - rpcClient *rpc.Client - tokenManager *token.Manager - marketManager *market.Manager - collectiblesService *collectibles.Service - collectiblesManager *collectibles.Manager - ensService *ens.Service - stickersService *stickers.Service - feesManager *FeeManager - pathProcessors map[string]pathprocessor.PathProcessor - scheduler *async.Scheduler -} - -func (r *Router) requireApproval(ctx context.Context, sendType SendType, approvalContractAddress *common.Address, params pathprocessor.ProcessorInputParams) ( - bool, *big.Int, uint64, uint64, error) { - if sendType.IsCollectiblesTransfer() || sendType.IsEnsTransfer() || sendType.IsStickersTransfer() { - return false, nil, 0, 0, nil - } - - if params.FromToken.IsNative() { - return false, nil, 0, 0, nil - } - - contractMaker, err := contracts.NewContractMaker(r.rpcClient) - if err != nil { - return false, nil, 0, 0, err - } - - contract, err := contractMaker.NewERC20(params.FromChain.ChainID, params.FromToken.Address) - if err != nil { - return false, nil, 0, 0, err - } - - if approvalContractAddress == nil || *approvalContractAddress == pathprocessor.ZeroAddress { - return false, nil, 0, 0, nil - } - - if params.TestsMode { - return true, params.AmountIn, params.TestApprovalGasEstimation, params.TestApprovalL1Fee, nil - } - - allowance, err := contract.Allowance(&bind.CallOpts{ - Context: ctx, - }, params.FromAddr, *approvalContractAddress) - - if err != nil { - return false, nil, 0, 0, err - } - - if allowance.Cmp(params.AmountIn) >= 0 { - return false, nil, 0, 0, nil - } - - ethClient, err := r.rpcClient.EthClient(params.FromChain.ChainID) - if err != nil { - return false, nil, 0, 0, err - } - - erc20ABI, err := abi.JSON(strings.NewReader(ierc20.IERC20ABI)) - if err != nil { - return false, nil, 0, 0, err - } - - data, err := erc20ABI.Pack("approve", approvalContractAddress, params.AmountIn) - if err != nil { - return false, nil, 0, 0, err - } - - estimate, err := ethClient.EstimateGas(context.Background(), ethereum.CallMsg{ - From: params.FromAddr, - To: ¶ms.FromToken.Address, - Value: pathprocessor.ZeroBigIntValue, - Data: data, - }) - if err != nil { - return false, nil, 0, 0, err - } - - // fetching l1 fee - var l1Fee uint64 - oracleContractAddress, err := gaspriceoracle.ContractAddress(params.FromChain.ChainID) - if err == nil { - oracleContract, err := gaspriceoracle.NewGaspriceoracleCaller(oracleContractAddress, ethClient) - if err != nil { - return false, nil, 0, 0, err - } - - callOpt := &bind.CallOpts{} - - l1FeeResult, _ := oracleContract.GetL1Fee(callOpt, data) - l1Fee = l1FeeResult.Uint64() - } - - return true, params.AmountIn, estimate, l1Fee, nil -} - -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 - } - - return r.tokenManager.GetBalance(ctx, client, account, token.Address) -} - -func (r *Router) getERC1155Balance(ctx context.Context, network *params.Network, token *token.Token, account common.Address) (*big.Int, error) { - tokenID, success := new(big.Int).SetString(token.Symbol, 10) - if !success { - return nil, errors.New("failed to convert token symbol to big.Int") - } - - balances, err := r.collectiblesManager.FetchERC1155Balances( - ctx, - account, - walletCommon.ChainID(network.ChainID), - token.Address, - []*bigint.BigInt{&bigint.BigInt{Int: tokenID}}, - ) - if err != nil { - return nil, err - } - - if len(balances) != 1 || balances[0] == nil { - return nil, errors.New("invalid ERC1155 balance fetch response") - } - - return balances[0].Int, nil -} - -func (r *Router) SuggestedRoutes( - ctx context.Context, - sendType SendType, - addrFrom common.Address, - addrTo common.Address, - amountIn *big.Int, - tokenID string, - toTokenID string, - disabledFromChainIDs, - disabledToChainIDs, - preferedChainIDs []uint64, - gasFeeMode GasFeeMode, - fromLockedAmount map[uint64]*hexutil.Big, - testnetMode bool, -) (*SuggestedRoutes, error) { - - networks, err := r.rpcClient.NetworkManager.Get(false) - if err != nil { - return nil, err - } - - prices, err := sendType.FetchPrices(r.marketManager, tokenID) - if err != nil { - return nil, err - } - var ( - group = async.NewAtomicGroup(ctx) - mu sync.Mutex - candidates = make([]*Path, 0) - ) - - for networkIdx := range networks { - network := networks[networkIdx] - if network.IsTest != testnetMode { - continue - } - - if arrayContainsElement(network.ChainID, disabledFromChainIDs) { - continue - } - - if !sendType.isAvailableFor(network) { - continue - } - - token := sendType.FindToken(r.tokenManager, r.collectiblesService, addrFrom, network, tokenID) - if token == nil { - continue - } - - var toToken *walletToken.Token - if sendType == Swap { - toToken = sendType.FindToken(r.tokenManager, r.collectiblesService, common.Address{}, network, toTokenID) - } - - nativeToken := r.tokenManager.FindToken(network, network.NativeCurrencySymbol) - if nativeToken == nil { - continue - } - - group.Add(func(c context.Context) error { - gasFees, err := r.feesManager.SuggestedFeesGwei(ctx, network.ChainID) - if err != nil { - return err - } - - // Default value is 1 as in case of erc721 as we built the token we are sure the account owns it - balance := big.NewInt(1) - if sendType == ERC1155Transfer { - balance, err = r.getERC1155Balance(ctx, network, token, addrFrom) - if err != nil { - return err - } - } else if sendType != ERC721Transfer { - balance, err = r.getBalance(ctx, network.ChainID, token, addrFrom) - if err != nil { - return err - } - } - - maxAmountIn := (*hexutil.Big)(balance) - if amount, ok := fromLockedAmount[network.ChainID]; ok { - if amount.ToInt().Cmp(balance) == 1 { - return errors.New("locked amount cannot be bigger than balance") - } - maxAmountIn = amount - } - - nativeBalance, err := r.getBalance(ctx, network.ChainID, nativeToken, addrFrom) - if err != nil { - return err - } - maxFees := gasFees.feeFor(gasFeeMode) - - estimatedTime := r.feesManager.TransactionEstimatedTime(ctx, network.ChainID, gweiToWei(maxFees)) - for _, pProcessor := range r.pathProcessors { - // Skip processors that are added because of the Router V2, to not break the current functionality - if pProcessor.Name() == pathprocessor.ProcessorENSRegisterName || - pProcessor.Name() == pathprocessor.ProcessorENSReleaseName || - pProcessor.Name() == pathprocessor.ProcessorENSPublicKeyName || - pProcessor.Name() == pathprocessor.ProcessorStickersBuyName { - continue - } - - if !sendType.canUseProcessor(pProcessor) { - continue - } - - for _, dest := range networks { - if dest.IsTest != testnetMode { - continue - } - - if !sendType.isAvailableFor(network) { - continue - } - - if !sendType.isAvailableBetween(network, dest) { - continue - } - - if len(preferedChainIDs) > 0 && !arrayContainsElement(dest.ChainID, preferedChainIDs) { - continue - } - if arrayContainsElement(dest.ChainID, disabledToChainIDs) { - continue - } - - processorInputParams := pathprocessor.ProcessorInputParams{ - FromChain: network, - ToChain: dest, - FromToken: token, - ToToken: toToken, - ToAddr: addrTo, - FromAddr: addrFrom, - AmountIn: amountIn, - } - - can, err := pProcessor.AvailableFor(processorInputParams) - if err != nil || !can { - continue - } - if maxAmountIn.ToInt().Cmp(pathprocessor.ZeroBigIntValue) == 0 { - continue - } - - bonderFees, tokenFees, err := pProcessor.CalculateFees(processorInputParams) - if err != nil { - continue - } - if bonderFees.Cmp(pathprocessor.ZeroBigIntValue) != 0 { - if maxAmountIn.ToInt().Cmp(amountIn) >= 0 { - if bonderFees.Cmp(amountIn) >= 0 { - continue - } - } else { - if bonderFees.Cmp(maxAmountIn.ToInt()) >= 0 { - continue - } - } - } - gasLimit := uint64(0) - if sendType.isTransfer(false) { - gasLimit, err = pProcessor.EstimateGas(processorInputParams) - if err != nil { - continue - } - } else { - gasLimit = sendType.EstimateGas(r.ensService, r.stickersService, network, addrFrom, tokenID) - } - - approvalContractAddress, err := pProcessor.GetContractAddress(processorInputParams) - if err != nil { - continue - } - approvalRequired, approvalAmountRequired, approvalGasLimit, l1ApprovalFee, err := r.requireApproval(ctx, sendType, &approvalContractAddress, processorInputParams) - if err != nil { - continue - } - - var l1GasFeeWei uint64 - if sendType.needL1Fee() { - txInputData, err := pProcessor.PackTxInputData(processorInputParams) - if err != nil { - continue - } - - l1GasFeeWei, _ = r.feesManager.GetL1Fee(ctx, network.ChainID, txInputData) - l1GasFeeWei += l1ApprovalFee - } - - gasFees.L1GasFee = weiToGwei(big.NewInt(int64(l1GasFeeWei))) - - requiredNativeBalance := new(big.Int).Mul(gweiToWei(maxFees), big.NewInt(int64(gasLimit))) - requiredNativeBalance.Add(requiredNativeBalance, new(big.Int).Mul(gweiToWei(maxFees), big.NewInt(int64(approvalGasLimit)))) - requiredNativeBalance.Add(requiredNativeBalance, big.NewInt(int64(l1GasFeeWei))) // add l1Fee to requiredNativeBalance, in case of L1 chain l1Fee is 0 - - if nativeBalance.Cmp(requiredNativeBalance) <= 0 { - continue - } - - // Removed the required fees from maxAMount in case of native token tx - if token.IsNative() { - maxAmountIn = (*hexutil.Big)(new(big.Int).Sub(maxAmountIn.ToInt(), requiredNativeBalance)) - } - - ethPrice := big.NewFloat(prices["ETH"]) - - approvalGasFees := new(big.Float).Mul(gweiToEth(maxFees), big.NewFloat((float64(approvalGasLimit)))) - - approvalGasCost := new(big.Float) - approvalGasCost.Mul(approvalGasFees, ethPrice) - - l1GasCost := new(big.Float) - l1GasCost.Mul(gasFees.L1GasFee, ethPrice) - - gasCost := new(big.Float) - gasCost.Mul(new(big.Float).Mul(gweiToEth(maxFees), big.NewFloat(float64(gasLimit))), ethPrice) - - tokenFeesAsFloat := new(big.Float).Quo( - new(big.Float).SetInt(tokenFees), - big.NewFloat(math.Pow(10, float64(token.Decimals))), - ) - tokenCost := new(big.Float) - tokenCost.Mul(tokenFeesAsFloat, big.NewFloat(prices[tokenID])) - - cost := new(big.Float) - cost.Add(tokenCost, gasCost) - cost.Add(cost, approvalGasCost) - cost.Add(cost, l1GasCost) - mu.Lock() - candidates = append(candidates, &Path{ - BridgeName: pProcessor.Name(), - From: network, - To: dest, - MaxAmountIn: maxAmountIn, - AmountIn: (*hexutil.Big)(pathprocessor.ZeroBigIntValue), - AmountOut: (*hexutil.Big)(pathprocessor.ZeroBigIntValue), - GasAmount: gasLimit, - GasFees: gasFees, - BonderFees: (*hexutil.Big)(bonderFees), - TokenFees: tokenFeesAsFloat, - Cost: cost, - EstimatedTime: estimatedTime, - ApprovalRequired: approvalRequired, - ApprovalGasFees: approvalGasFees, - ApprovalAmountRequired: (*hexutil.Big)(approvalAmountRequired), - ApprovalContractAddress: &approvalContractAddress, - }) - mu.Unlock() - } - } - return nil - }) - } - - group.Wait() - - suggestedRoutes := newSuggestedRoutes(amountIn, candidates, fromLockedAmount) - suggestedRoutes.TokenPrice = prices[tokenID] - suggestedRoutes.NativeChainTokenPrice = prices["ETH"] - for _, path := range suggestedRoutes.Best { - processorInputParams := pathprocessor.ProcessorInputParams{ - FromChain: path.From, - ToChain: path.To, - AmountIn: path.AmountIn.ToInt(), - FromToken: &token.Token{ - Symbol: tokenID, - }, - ToToken: &token.Token{ - Symbol: toTokenID, - }, - } - - amountOut, err := r.pathProcessors[path.BridgeName].CalculateAmountOut(processorInputParams) - if err != nil { - continue - } - path.AmountOut = (*hexutil.Big)(amountOut) - } - - return suggestedRoutes, nil -} diff --git a/services/wallet/router/router_helper.go b/services/wallet/router/router_helper.go new file mode 100644 index 000000000..cbd7c0ddd --- /dev/null +++ b/services/wallet/router/router_helper.go @@ -0,0 +1,137 @@ +package router + +import ( + "context" + "errors" + "math/big" + "strings" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/status-im/status-go/contracts" + gaspriceoracle "github.com/status-im/status-go/contracts/gas-price-oracle" + "github.com/status-im/status-go/contracts/ierc20" + "github.com/status-im/status-go/params" + "github.com/status-im/status-go/services/wallet/bigint" + walletCommon "github.com/status-im/status-go/services/wallet/common" + "github.com/status-im/status-go/services/wallet/router/pathprocessor" + "github.com/status-im/status-go/services/wallet/token" +) + +func (r *Router) requireApproval(ctx context.Context, sendType SendType, approvalContractAddress *common.Address, params pathprocessor.ProcessorInputParams) ( + bool, *big.Int, uint64, uint64, error) { + if sendType.IsCollectiblesTransfer() || sendType.IsEnsTransfer() || sendType.IsStickersTransfer() { + return false, nil, 0, 0, nil + } + + if params.FromToken.IsNative() { + return false, nil, 0, 0, nil + } + + contractMaker, err := contracts.NewContractMaker(r.rpcClient) + if err != nil { + return false, nil, 0, 0, err + } + + contract, err := contractMaker.NewERC20(params.FromChain.ChainID, params.FromToken.Address) + if err != nil { + return false, nil, 0, 0, err + } + + if approvalContractAddress == nil || *approvalContractAddress == pathprocessor.ZeroAddress { + return false, nil, 0, 0, nil + } + + if params.TestsMode { + return true, params.AmountIn, params.TestApprovalGasEstimation, params.TestApprovalL1Fee, nil + } + + allowance, err := contract.Allowance(&bind.CallOpts{ + Context: ctx, + }, params.FromAddr, *approvalContractAddress) + + if err != nil { + return false, nil, 0, 0, err + } + + if allowance.Cmp(params.AmountIn) >= 0 { + return false, nil, 0, 0, nil + } + + ethClient, err := r.rpcClient.EthClient(params.FromChain.ChainID) + if err != nil { + return false, nil, 0, 0, err + } + + erc20ABI, err := abi.JSON(strings.NewReader(ierc20.IERC20ABI)) + if err != nil { + return false, nil, 0, 0, err + } + + data, err := erc20ABI.Pack("approve", approvalContractAddress, params.AmountIn) + if err != nil { + return false, nil, 0, 0, err + } + + estimate, err := ethClient.EstimateGas(context.Background(), ethereum.CallMsg{ + From: params.FromAddr, + To: ¶ms.FromToken.Address, + Value: pathprocessor.ZeroBigIntValue, + Data: data, + }) + if err != nil { + return false, nil, 0, 0, err + } + + // fetching l1 fee + var l1Fee uint64 + oracleContractAddress, err := gaspriceoracle.ContractAddress(params.FromChain.ChainID) + if err == nil { + oracleContract, err := gaspriceoracle.NewGaspriceoracleCaller(oracleContractAddress, ethClient) + if err != nil { + return false, nil, 0, 0, err + } + + callOpt := &bind.CallOpts{} + + l1FeeResult, _ := oracleContract.GetL1Fee(callOpt, data) + l1Fee = l1FeeResult.Uint64() + } + + return true, params.AmountIn, estimate, l1Fee, nil +} + +func (r *Router) getERC1155Balance(ctx context.Context, network *params.Network, token *token.Token, account common.Address) (*big.Int, error) { + tokenID, success := new(big.Int).SetString(token.Symbol, 10) + if !success { + return nil, errors.New("failed to convert token symbol to big.Int") + } + + balances, err := r.collectiblesManager.FetchERC1155Balances( + ctx, + account, + walletCommon.ChainID(network.ChainID), + token.Address, + []*bigint.BigInt{&bigint.BigInt{Int: tokenID}}, + ) + if err != nil { + return nil, err + } + + if len(balances) != 1 || balances[0] == nil { + return nil, errors.New("invalid ERC1155 balance fetch response") + } + + return balances[0].Int, nil +} + +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 + } + + return r.tokenManager.GetBalance(ctx, client, account, token.Address) +} diff --git a/services/wallet/router/router_send_type.go b/services/wallet/router/router_send_type.go index 50ddbd7ac..079af14b8 100644 --- a/services/wallet/router/router_send_type.go +++ b/services/wallet/router/router_send_type.go @@ -1,24 +1,16 @@ package router import ( - "context" - "fmt" "math/big" "strings" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/status-im/status-go/eth-node/types" "github.com/status-im/status-go/params" - "github.com/status-im/status-go/services/ens" - "github.com/status-im/status-go/services/stickers" - "github.com/status-im/status-go/services/wallet/bigint" "github.com/status-im/status-go/services/wallet/collectibles" walletCommon "github.com/status-im/status-go/services/wallet/common" "github.com/status-im/status-go/services/wallet/market" "github.com/status-im/status-go/services/wallet/router/pathprocessor" "github.com/status-im/status-go/services/wallet/token" - "github.com/status-im/status-go/transactions" ) type SendType int @@ -91,18 +83,6 @@ func (s SendType) FindToken(tokenManager *token.Manager, collectibles *collectib } } -// TODO: remove this function once we fully move to routerV2 -func (s SendType) isTransfer(routerV2Logic bool) bool { - return s == Transfer || - s == Bridge && routerV2Logic || - s == Swap || - s.IsCollectiblesTransfer() -} - -func (s SendType) needL1Fee() bool { - return !s.IsEnsTransfer() && !s.IsStickersTransfer() -} - // canUseProcessor is used to check if certain SendType can be used with a given path processor func (s SendType) canUseProcessor(p pathprocessor.PathProcessor) bool { pathProcessorName := p.Name() @@ -195,44 +175,3 @@ func (s SendType) isAvailableFor(network *params.Network) bool { return false } - -// TODO: remove this function once we fully move to routerV2 -func (s SendType) EstimateGas(ensService *ens.Service, stickersService *stickers.Service, network *params.Network, from common.Address, tokenID string) uint64 { - tx := transactions.SendTxArgs{ - From: (types.Address)(from), - Value: (*hexutil.Big)(pathprocessor.ZeroBigIntValue), - } - switch s { - case ENSRegister: - estimate, err := ensService.API().RegisterEstimate(context.Background(), network.ChainID, tx, EstimateUsername, EstimatePubKey) - if err != nil { - return 400000 - } - return estimate - - case ENSRelease: - estimate, err := ensService.API().ReleaseEstimate(context.Background(), network.ChainID, tx, EstimateUsername) - if err != nil { - return 200000 - } - return estimate - - case ENSSetPubKey: - estimate, err := ensService.API().SetPubKeyEstimate(context.Background(), network.ChainID, tx, fmt.Sprint(EstimateUsername, ".stateofus.eth"), EstimatePubKey) - if err != nil { - return 400000 - } - return estimate - - case StickersBuy: - packID := &bigint.BigInt{Int: big.NewInt(2)} - estimate, err := stickersService.API().BuyEstimate(context.Background(), network.ChainID, (types.Address)(from), packID) - if err != nil { - return 400000 - } - return estimate - - default: - return 0 - } -} diff --git a/services/wallet/router/router_v2.go b/services/wallet/router/router_v2.go index 38cee49cb..b0775258b 100644 --- a/services/wallet/router/router_v2.go +++ b/services/wallet/router/router_v2.go @@ -14,12 +14,18 @@ import ( "github.com/ethereum/go-ethereum/log" "github.com/status-im/status-go/errors" "github.com/status-im/status-go/params" + "github.com/status-im/status-go/rpc" "github.com/status-im/status-go/services/ens" + "github.com/status-im/status-go/services/stickers" "github.com/status-im/status-go/services/wallet/async" + "github.com/status-im/status-go/services/wallet/collectibles" walletCommon "github.com/status-im/status-go/services/wallet/common" + "github.com/status-im/status-go/services/wallet/market" "github.com/status-im/status-go/services/wallet/router/pathprocessor" + "github.com/status-im/status-go/services/wallet/token" walletToken "github.com/status-im/status-go/services/wallet/token" "github.com/status-im/status-go/signal" + "github.com/status-im/status-go/transactions" ) const ( @@ -169,6 +175,53 @@ type NodeV2 struct { Children GraphV2 } +type Router struct { + rpcClient *rpc.Client + tokenManager *token.Manager + marketManager *market.Manager + collectiblesService *collectibles.Service + collectiblesManager *collectibles.Manager + ensService *ens.Service + stickersService *stickers.Service + feesManager *FeeManager + pathProcessors map[string]pathprocessor.PathProcessor + scheduler *async.Scheduler +} + +func NewRouter(rpcClient *rpc.Client, transactor *transactions.Transactor, tokenManager *token.Manager, marketManager *market.Manager, + collectibles *collectibles.Service, collectiblesManager *collectibles.Manager, ensService *ens.Service, stickersService *stickers.Service) *Router { + processors := make(map[string]pathprocessor.PathProcessor) + + return &Router{ + rpcClient: rpcClient, + tokenManager: tokenManager, + marketManager: marketManager, + collectiblesService: collectibles, + collectiblesManager: collectiblesManager, + ensService: ensService, + stickersService: stickersService, + feesManager: &FeeManager{rpcClient}, + pathProcessors: processors, + scheduler: async.NewScheduler(), + } +} + +func (r *Router) AddPathProcessor(processor pathprocessor.PathProcessor) { + r.pathProcessors[processor.Name()] = processor +} + +func (r *Router) Stop() { + r.scheduler.Stop() +} + +func (r *Router) GetFeesManager() *FeeManager { + return r.feesManager +} + +func (r *Router) GetPathProcessors() map[string]pathprocessor.PathProcessor { + return r.pathProcessors +} + func newSuggestedRoutesV2( uuid string, amountIn *big.Int,