mirror of
https://github.com/status-im/status-go.git
synced 2025-01-12 07:35:02 +00:00
9b9a91f654
This commit maps this kind of errors `status-proxy-0.error: failed with 50011064 gas: insufficient funds for gas * price + value: address 0x4eeB09cf0076F840b38511D808464eE48efD4305 have 0 want 10000000000000` to this form: `status-proxy-0.error: failed with 50011064 gas: insufficient funds for gas * price + value: address 0x4eeB09cf0076F840b38511D808464eE48efD4305` which means that we don't want to display to a user details of how much they have and how much is needed for fees, cause those data are very often misleading, referring mostly to "how much user has". New error added in case there is no positive balances across all enabled chains.
1279 lines
44 KiB
Go
1279 lines
44 KiB
Go
package router
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math"
|
|
"math/big"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/ethereum/go-ethereum/common"
|
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
|
"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/services/ens"
|
|
"github.com/status-im/status-go/services/wallet/async"
|
|
walletCommon "github.com/status-im/status-go/services/wallet/common"
|
|
"github.com/status-im/status-go/services/wallet/router/pathprocessor"
|
|
walletToken "github.com/status-im/status-go/services/wallet/token"
|
|
"github.com/status-im/status-go/signal"
|
|
)
|
|
|
|
const (
|
|
hexAddressLength = 42
|
|
)
|
|
|
|
var (
|
|
routerTask = async.TaskType{
|
|
ID: 1,
|
|
Policy: async.ReplacementPolicyCancelOld,
|
|
}
|
|
)
|
|
|
|
var (
|
|
supportedNetworks = map[uint64]bool{
|
|
walletCommon.EthereumMainnet: true,
|
|
walletCommon.OptimismMainnet: true,
|
|
walletCommon.ArbitrumMainnet: true,
|
|
}
|
|
|
|
supportedTestNetworks = map[uint64]bool{
|
|
walletCommon.EthereumSepolia: true,
|
|
walletCommon.OptimismSepolia: true,
|
|
walletCommon.ArbitrumSepolia: true,
|
|
}
|
|
)
|
|
|
|
type RouteInputParams struct {
|
|
Uuid string `json:"uuid"`
|
|
SendType SendType `json:"sendType" validate:"required"`
|
|
AddrFrom common.Address `json:"addrFrom" validate:"required"`
|
|
AddrTo common.Address `json:"addrTo" validate:"required"`
|
|
AmountIn *hexutil.Big `json:"amountIn" validate:"required"`
|
|
AmountOut *hexutil.Big `json:"amountOut"`
|
|
TokenID string `json:"tokenID" validate:"required"`
|
|
ToTokenID string `json:"toTokenID"`
|
|
DisabledFromChainIDs []uint64 `json:"disabledFromChainIDs"`
|
|
DisabledToChainIDs []uint64 `json:"disabledToChainIDs"`
|
|
GasFeeMode GasFeeMode `json:"gasFeeMode" validate:"required"`
|
|
FromLockedAmount map[uint64]*hexutil.Big `json:"fromLockedAmount"`
|
|
testnetMode bool
|
|
|
|
// For send types like EnsRegister, EnsRelease, EnsSetPubKey, StickersBuy
|
|
Username string `json:"username"`
|
|
PublicKey string `json:"publicKey"`
|
|
PackID *hexutil.Big `json:"packID"`
|
|
|
|
// TODO: Remove two fields below once we implement a better solution for tests
|
|
// Currently used for tests only
|
|
testsMode bool
|
|
testParams *routerTestParams
|
|
}
|
|
|
|
type routerTestParams struct {
|
|
tokenFrom *walletToken.Token
|
|
tokenPrices map[string]float64
|
|
estimationMap map[string]pathprocessor.Estimation // [processor-name, estimation]
|
|
bonderFeeMap map[string]*big.Int // [token-symbol, bonder-fee]
|
|
suggestedFees *SuggestedFees
|
|
baseFee *big.Int
|
|
balanceMap map[string]*big.Int // [token-symbol, balance]
|
|
approvalGasEstimation uint64
|
|
approvalL1Fee uint64
|
|
}
|
|
|
|
type amountOption struct {
|
|
amount *big.Int
|
|
locked bool
|
|
subtractFees bool
|
|
}
|
|
|
|
func makeBalanceKey(chainID uint64, symbol string) string {
|
|
return fmt.Sprintf("%d-%s", chainID, symbol)
|
|
}
|
|
|
|
type PathV2 struct {
|
|
ProcessorName string
|
|
FromChain *params.Network // Source chain
|
|
ToChain *params.Network // Destination chain
|
|
FromToken *walletToken.Token // Source token
|
|
ToToken *walletToken.Token // Destination token, set if applicable
|
|
AmountIn *hexutil.Big // Amount that will be sent from the source chain
|
|
AmountInLocked bool // Is the amount locked
|
|
AmountOut *hexutil.Big // Amount that will be received on the destination chain
|
|
|
|
SuggestedLevelsForMaxFeesPerGas *MaxFeesLevels // Suggested max fees for the transaction (in ETH WEI)
|
|
MaxFeesPerGas *hexutil.Big // Max fees per gas (determined by client via GasFeeMode, in ETH WEI)
|
|
|
|
TxBaseFee *hexutil.Big // Base fee for the transaction (in ETH WEI)
|
|
TxPriorityFee *hexutil.Big // Priority fee for the transaction (in ETH WEI)
|
|
TxGasAmount uint64 // Gas used for the transaction
|
|
TxBonderFees *hexutil.Big // Bonder fees for the transaction - used for Hop bridge (in selected token)
|
|
TxTokenFees *hexutil.Big // Token fees for the transaction - used for bridges (represent the difference between the amount in and the amount out, in selected token)
|
|
|
|
TxFee *hexutil.Big // fee for the transaction (includes tx fee only, doesn't include approval fees, l1 fees, l1 approval fees, token fees or bonders fees, in ETH WEI)
|
|
TxL1Fee *hexutil.Big // L1 fee for the transaction - used for for transactions placed on L2 chains (in ETH WEI)
|
|
|
|
ApprovalRequired bool // Is approval required for the transaction
|
|
ApprovalAmountRequired *hexutil.Big // Amount required for the approval transaction
|
|
ApprovalContractAddress *common.Address // Address of the contract that needs to be approved
|
|
ApprovalBaseFee *hexutil.Big // Base fee for the approval transaction (in ETH WEI)
|
|
ApprovalPriorityFee *hexutil.Big // Priority fee for the approval transaction (in ETH WEI)
|
|
ApprovalGasAmount uint64 // Gas used for the approval transaction
|
|
|
|
ApprovalFee *hexutil.Big // Total fee for the approval transaction (includes approval tx fees only, doesn't include approval l1 fees, in ETH WEI)
|
|
ApprovalL1Fee *hexutil.Big // L1 fee for the approval transaction - used for for transactions placed on L2 chains (in ETH WEI)
|
|
|
|
TxTotalFee *hexutil.Big // Total fee for the transaction (includes tx fees, approval fees, l1 fees, l1 approval fees, in ETH WEI)
|
|
|
|
EstimatedTime TransactionEstimation
|
|
|
|
requiredTokenBalance *big.Int // (in selected token)
|
|
requiredNativeBalance *big.Int // (in ETH WEI)
|
|
subtractFees bool
|
|
}
|
|
|
|
type ProcessorError struct {
|
|
ProcessorName string
|
|
Error error
|
|
}
|
|
|
|
func (p *PathV2) Equal(o *PathV2) bool {
|
|
return p.FromChain.ChainID == o.FromChain.ChainID && p.ToChain.ChainID == o.ToChain.ChainID
|
|
}
|
|
|
|
type SuggestedRoutesV2 struct {
|
|
Uuid string
|
|
Best []*PathV2
|
|
Candidates []*PathV2
|
|
TokenPrice float64
|
|
NativeChainTokenPrice float64
|
|
}
|
|
|
|
type SuggestedRoutesV2Response struct {
|
|
Uuid string `json:"Uuid"`
|
|
Best []*PathV2 `json:"Best,omitempty"`
|
|
Candidates []*PathV2 `json:"Candidates,omitempty"`
|
|
TokenPrice *float64 `json:"TokenPrice,omitempty"`
|
|
NativeChainTokenPrice *float64 `json:"NativeChainTokenPrice,omitempty"`
|
|
ErrorResponse *errors.ErrorResponse `json:"ErrorResponse,omitempty"`
|
|
}
|
|
|
|
type GraphV2 []*NodeV2
|
|
|
|
type NodeV2 struct {
|
|
Path *PathV2
|
|
Children GraphV2
|
|
}
|
|
|
|
func newSuggestedRoutesV2(
|
|
uuid string,
|
|
amountIn *big.Int,
|
|
candidates []*PathV2,
|
|
fromLockedAmount map[uint64]*hexutil.Big,
|
|
tokenPrice float64,
|
|
nativeChainTokenPrice float64,
|
|
) (*SuggestedRoutesV2, [][]*PathV2) {
|
|
suggestedRoutes := &SuggestedRoutesV2{
|
|
Uuid: uuid,
|
|
Candidates: candidates,
|
|
TokenPrice: tokenPrice,
|
|
NativeChainTokenPrice: nativeChainTokenPrice,
|
|
}
|
|
if len(candidates) == 0 {
|
|
return suggestedRoutes, nil
|
|
}
|
|
|
|
node := &NodeV2{
|
|
Path: nil,
|
|
Children: buildGraphV2(amountIn, candidates, 0, []uint64{}),
|
|
}
|
|
allRoutes := node.buildAllRoutesV2()
|
|
allRoutes = filterRoutesV2(allRoutes, amountIn, fromLockedAmount)
|
|
|
|
if len(allRoutes) > 0 {
|
|
sort.Slice(allRoutes, func(i, j int) bool {
|
|
iRoute := getRoutePriority(allRoutes[i])
|
|
jRoute := getRoutePriority(allRoutes[j])
|
|
return iRoute <= jRoute
|
|
})
|
|
}
|
|
|
|
return suggestedRoutes, allRoutes
|
|
}
|
|
|
|
func newNodeV2(path *PathV2) *NodeV2 {
|
|
return &NodeV2{Path: path, Children: make(GraphV2, 0)}
|
|
}
|
|
|
|
func buildGraphV2(AmountIn *big.Int, routes []*PathV2, level int, sourceChainIDs []uint64) GraphV2 {
|
|
graph := make(GraphV2, 0)
|
|
for _, route := range routes {
|
|
found := false
|
|
for _, chainID := range sourceChainIDs {
|
|
if chainID == route.FromChain.ChainID {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if found {
|
|
continue
|
|
}
|
|
node := newNodeV2(route)
|
|
|
|
newRoutes := make([]*PathV2, 0)
|
|
for _, r := range routes {
|
|
if route.Equal(r) {
|
|
continue
|
|
}
|
|
newRoutes = append(newRoutes, r)
|
|
}
|
|
|
|
newAmountIn := new(big.Int).Sub(AmountIn, route.AmountIn.ToInt())
|
|
if newAmountIn.Sign() > 0 {
|
|
newSourceChainIDs := make([]uint64, len(sourceChainIDs))
|
|
copy(newSourceChainIDs, sourceChainIDs)
|
|
newSourceChainIDs = append(newSourceChainIDs, route.FromChain.ChainID)
|
|
node.Children = buildGraphV2(newAmountIn, newRoutes, level+1, newSourceChainIDs)
|
|
|
|
if len(node.Children) == 0 {
|
|
continue
|
|
}
|
|
}
|
|
|
|
graph = append(graph, node)
|
|
}
|
|
|
|
return graph
|
|
}
|
|
|
|
func (n NodeV2) buildAllRoutesV2() [][]*PathV2 {
|
|
res := make([][]*PathV2, 0)
|
|
|
|
if len(n.Children) == 0 && n.Path != nil {
|
|
res = append(res, []*PathV2{n.Path})
|
|
}
|
|
|
|
for _, node := range n.Children {
|
|
for _, route := range node.buildAllRoutesV2() {
|
|
extendedRoute := route
|
|
if n.Path != nil {
|
|
extendedRoute = append([]*PathV2{n.Path}, route...)
|
|
}
|
|
res = append(res, extendedRoute)
|
|
}
|
|
}
|
|
|
|
return res
|
|
}
|
|
|
|
func findBestV2(routes [][]*PathV2, tokenPrice float64, nativeTokenPrice float64) []*PathV2 {
|
|
var best []*PathV2
|
|
bestCost := big.NewFloat(math.Inf(1))
|
|
for _, route := range routes {
|
|
currentCost := big.NewFloat(0)
|
|
for _, path := range route {
|
|
tokenDenominator := big.NewFloat(math.Pow(10, float64(path.FromToken.Decimals)))
|
|
|
|
// calculate the cost of the path
|
|
nativeTokenPrice := new(big.Float).SetFloat64(nativeTokenPrice)
|
|
|
|
// tx fee
|
|
txFeeInEth := gweiToEth(weiToGwei(path.TxFee.ToInt()))
|
|
pathCost := new(big.Float).Mul(txFeeInEth, nativeTokenPrice)
|
|
|
|
if path.TxL1Fee.ToInt().Cmp(pathprocessor.ZeroBigIntValue) > 0 {
|
|
txL1FeeInEth := gweiToEth(weiToGwei(path.TxL1Fee.ToInt()))
|
|
pathCost.Add(pathCost, new(big.Float).Mul(txL1FeeInEth, nativeTokenPrice))
|
|
}
|
|
|
|
if path.TxBonderFees != nil && path.TxBonderFees.ToInt().Cmp(pathprocessor.ZeroBigIntValue) > 0 {
|
|
pathCost.Add(pathCost, new(big.Float).Mul(
|
|
new(big.Float).Quo(new(big.Float).SetInt(path.TxBonderFees.ToInt()), tokenDenominator),
|
|
new(big.Float).SetFloat64(tokenPrice)))
|
|
|
|
}
|
|
|
|
if path.TxTokenFees != nil && path.TxTokenFees.ToInt().Cmp(pathprocessor.ZeroBigIntValue) > 0 && path.FromToken != nil {
|
|
pathCost.Add(pathCost, new(big.Float).Mul(
|
|
new(big.Float).Quo(new(big.Float).SetInt(path.TxTokenFees.ToInt()), tokenDenominator),
|
|
new(big.Float).SetFloat64(tokenPrice)))
|
|
}
|
|
|
|
if path.ApprovalRequired {
|
|
// tx approval fee
|
|
approvalFeeInEth := gweiToEth(weiToGwei(path.ApprovalFee.ToInt()))
|
|
pathCost.Add(pathCost, new(big.Float).Mul(approvalFeeInEth, nativeTokenPrice))
|
|
|
|
if path.ApprovalL1Fee.ToInt().Cmp(pathprocessor.ZeroBigIntValue) > 0 {
|
|
approvalL1FeeInEth := gweiToEth(weiToGwei(path.ApprovalL1Fee.ToInt()))
|
|
pathCost.Add(pathCost, new(big.Float).Mul(approvalL1FeeInEth, nativeTokenPrice))
|
|
}
|
|
}
|
|
|
|
currentCost = new(big.Float).Add(currentCost, pathCost)
|
|
}
|
|
|
|
if currentCost.Cmp(bestCost) == -1 {
|
|
best = route
|
|
bestCost = currentCost
|
|
}
|
|
}
|
|
|
|
return best
|
|
}
|
|
|
|
func validateInputData(input *RouteInputParams) error {
|
|
if input.SendType == ENSRegister {
|
|
if input.Username == "" || input.PublicKey == "" {
|
|
return ErrENSRegisterRequiresUsernameAndPubKey
|
|
}
|
|
if input.testnetMode {
|
|
if input.TokenID != pathprocessor.SttSymbol {
|
|
return ErrENSRegisterTestnetSTTOnly
|
|
}
|
|
} else {
|
|
if input.TokenID != pathprocessor.SntSymbol {
|
|
return ErrENSRegisterMainnetSNTOnly
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if input.SendType == ENSRelease {
|
|
if input.Username == "" {
|
|
return ErrENSReleaseRequiresUsername
|
|
}
|
|
}
|
|
|
|
if input.SendType == ENSSetPubKey {
|
|
if input.Username == "" || input.PublicKey == "" {
|
|
return ErrENSSetPubKeyRequiresUsernameAndPubKey
|
|
}
|
|
|
|
if ens.ValidateENSUsername(input.Username) != nil {
|
|
return ErrENSSetPubKeyInvalidUsername
|
|
}
|
|
}
|
|
|
|
if input.SendType == StickersBuy {
|
|
if input.PackID == nil {
|
|
return ErrStickersBuyRequiresPackID
|
|
}
|
|
}
|
|
|
|
if input.SendType == Swap {
|
|
if input.ToTokenID == "" {
|
|
return ErrSwapRequiresToTokenID
|
|
}
|
|
if input.TokenID == input.ToTokenID {
|
|
return ErrSwapTokenIDMustBeDifferent
|
|
}
|
|
|
|
if input.AmountIn != nil &&
|
|
input.AmountOut != nil &&
|
|
input.AmountIn.ToInt().Cmp(pathprocessor.ZeroBigIntValue) > 0 &&
|
|
input.AmountOut.ToInt().Cmp(pathprocessor.ZeroBigIntValue) > 0 {
|
|
return ErrSwapAmountInAmountOutMustBeExclusive
|
|
}
|
|
|
|
if input.AmountIn != nil && input.AmountIn.ToInt().Sign() < 0 {
|
|
return ErrSwapAmountInMustBePositive
|
|
}
|
|
|
|
if input.AmountOut != nil && input.AmountOut.ToInt().Sign() < 0 {
|
|
return ErrSwapAmountOutMustBePositive
|
|
}
|
|
}
|
|
|
|
return validateFromLockedAmount(input)
|
|
}
|
|
|
|
func validateFromLockedAmount(input *RouteInputParams) error {
|
|
if input.FromLockedAmount == nil || len(input.FromLockedAmount) == 0 {
|
|
return nil
|
|
}
|
|
|
|
var suppNetworks map[uint64]bool
|
|
if input.testnetMode {
|
|
suppNetworks = copyMapGeneric(supportedTestNetworks, nil).(map[uint64]bool)
|
|
} else {
|
|
suppNetworks = copyMapGeneric(supportedNetworks, nil).(map[uint64]bool)
|
|
}
|
|
|
|
if suppNetworks == nil {
|
|
return ErrCannotCheckLockedAmounts
|
|
}
|
|
|
|
totalLockedAmount := big.NewInt(0)
|
|
excludedChainCount := 0
|
|
|
|
for chainID, amount := range input.FromLockedAmount {
|
|
if arrayContainsElement(chainID, input.DisabledFromChainIDs) {
|
|
return ErrDisabledChainFoundAmongLockedNetworks
|
|
}
|
|
|
|
if input.testnetMode {
|
|
if !supportedTestNetworks[chainID] {
|
|
return ErrLockedAmountNotSupportedForNetwork
|
|
}
|
|
} else {
|
|
if !supportedNetworks[chainID] {
|
|
return ErrLockedAmountNotSupportedForNetwork
|
|
}
|
|
}
|
|
|
|
if amount == nil || amount.ToInt().Sign() < 0 {
|
|
return ErrLockedAmountNotNegative
|
|
}
|
|
|
|
if !(amount.ToInt().Sign() > 0) {
|
|
excludedChainCount++
|
|
}
|
|
delete(suppNetworks, chainID)
|
|
totalLockedAmount = new(big.Int).Add(totalLockedAmount, amount.ToInt())
|
|
}
|
|
|
|
if (!input.testnetMode && excludedChainCount == len(supportedNetworks)) ||
|
|
(input.testnetMode && excludedChainCount == len(supportedTestNetworks)) {
|
|
return ErrLockedAmountExcludesAllSupported
|
|
}
|
|
|
|
if totalLockedAmount.Cmp(input.AmountIn.ToInt()) > 0 {
|
|
return ErrLockedAmountExceedsTotalSendAmount
|
|
} else if totalLockedAmount.Cmp(input.AmountIn.ToInt()) < 0 && len(suppNetworks) == 0 {
|
|
return ErrLockedAmountLessThanSendAmountAllNetworks
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *Router) SuggestedRoutesV2Async(input *RouteInputParams) {
|
|
r.scheduler.Enqueue(routerTask, func(ctx context.Context) (interface{}, error) {
|
|
return r.SuggestedRoutesV2(ctx, input)
|
|
}, func(result interface{}, taskType async.TaskType, err error) {
|
|
routesResponse := SuggestedRoutesV2Response{
|
|
Uuid: input.Uuid,
|
|
}
|
|
|
|
if err != nil {
|
|
errorResponse := errors.CreateErrorResponseFromError(err)
|
|
routesResponse.ErrorResponse = errorResponse.(*errors.ErrorResponse)
|
|
}
|
|
|
|
if suggestedRoutes, ok := result.(*SuggestedRoutesV2); ok && suggestedRoutes != nil {
|
|
routesResponse.Best = suggestedRoutes.Best
|
|
routesResponse.Candidates = suggestedRoutes.Candidates
|
|
routesResponse.TokenPrice = &suggestedRoutes.TokenPrice
|
|
routesResponse.NativeChainTokenPrice = &suggestedRoutes.NativeChainTokenPrice
|
|
}
|
|
|
|
signal.SendWalletEvent(signal.SuggestedRoutes, routesResponse)
|
|
})
|
|
}
|
|
|
|
func (r *Router) StopSuggestedRoutesV2AsyncCalcualtion() {
|
|
r.scheduler.Stop()
|
|
}
|
|
|
|
func (r *Router) SuggestedRoutesV2(ctx context.Context, input *RouteInputParams) (*SuggestedRoutesV2, error) {
|
|
testnetMode, err := r.rpcClient.NetworkManager.GetTestNetworksEnabled()
|
|
if err != nil {
|
|
return nil, errors.CreateErrorResponseFromError(err)
|
|
}
|
|
|
|
input.testnetMode = testnetMode
|
|
|
|
// clear all processors
|
|
for _, processor := range r.pathProcessors {
|
|
if clearable, ok := processor.(pathprocessor.PathProcessorClearable); ok {
|
|
clearable.Clear()
|
|
}
|
|
}
|
|
|
|
err = validateInputData(input)
|
|
if err != nil {
|
|
return nil, errors.CreateErrorResponseFromError(err)
|
|
}
|
|
|
|
selectedFromChains, selectedToChains, err := r.getSelectedChains(input)
|
|
if err != nil {
|
|
return nil, errors.CreateErrorResponseFromError(err)
|
|
}
|
|
|
|
balanceMap, err := r.getBalanceMapForTokenOnChains(ctx, input, selectedFromChains)
|
|
// return only if there are no balances, otherwise try to resolve the candidates for chains we know the balances for
|
|
if len(balanceMap) == 0 {
|
|
if err != nil {
|
|
return nil, errors.CreateErrorResponseFromError(err)
|
|
}
|
|
} else {
|
|
noBalanceOnAnyChain := true
|
|
for _, value := range balanceMap {
|
|
if value.Cmp(pathprocessor.ZeroBigIntValue) > 0 {
|
|
noBalanceOnAnyChain = false
|
|
break
|
|
}
|
|
}
|
|
if noBalanceOnAnyChain {
|
|
return nil, ErrNoPositiveBalance
|
|
}
|
|
}
|
|
|
|
candidates, processorErrors, err := r.resolveCandidates(ctx, input, selectedFromChains, selectedToChains, balanceMap)
|
|
if err != nil {
|
|
return nil, errors.CreateErrorResponseFromError(err)
|
|
}
|
|
|
|
suggestedRoutes, err := r.resolveRoutes(ctx, input, candidates, balanceMap)
|
|
|
|
if err == nil && (suggestedRoutes == nil || len(suggestedRoutes.Best) == 0) {
|
|
// No best route found, but no error given.
|
|
if len(processorErrors) > 0 {
|
|
// Return one of the path processor errors if present.
|
|
// Give precedence to the custom error message.
|
|
for _, processorError := range processorErrors {
|
|
if processorError.Error != nil && pathprocessor.IsCustomError(processorError.Error) {
|
|
err = processorError.Error
|
|
break
|
|
}
|
|
}
|
|
if err == nil {
|
|
err = errors.CreateErrorResponseFromError(processorErrors[0].Error)
|
|
}
|
|
} else {
|
|
err = ErrNoBestRouteFound
|
|
}
|
|
}
|
|
|
|
mapError := func(err error) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
pattern := "insufficient funds for gas * price + value: address "
|
|
addressIndex := strings.Index(errors.DetailsFromError(err), pattern)
|
|
if addressIndex != -1 {
|
|
addressIndex += len(pattern) + hexAddressLength
|
|
return errors.CreateErrorResponseFromError(&errors.ErrorResponse{
|
|
Code: errors.ErrorCodeFromError(err),
|
|
Details: errors.DetailsFromError(err)[:addressIndex],
|
|
})
|
|
}
|
|
return err
|
|
}
|
|
// map some errors to more user-friendly messages
|
|
return suggestedRoutes, mapError(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
|
|
}
|
|
|
|
balanceMap = make(map[string]*big.Int)
|
|
|
|
chainError := func(chainId uint64, token string, intErr error) {
|
|
if err == nil {
|
|
err = fmt.Errorf("chain %d, token %s: %w", chainId, token, intErr)
|
|
} else {
|
|
err = fmt.Errorf("%s; chain %d, token %s: %w", err.Error(), chainId, token, intErr)
|
|
}
|
|
}
|
|
|
|
for _, chain := range selectedFromChains {
|
|
// check token existence
|
|
token := input.SendType.FindToken(r.tokenManager, r.collectiblesService, input.AddrFrom, chain, input.TokenID)
|
|
if token == nil {
|
|
chainError(chain.ChainID, input.TokenID, ErrTokenNotFound)
|
|
continue
|
|
}
|
|
// check native token existence
|
|
nativeToken := r.tokenManager.FindToken(chain, chain.NativeCurrencySymbol)
|
|
if nativeToken == nil {
|
|
chainError(chain.ChainID, chain.NativeCurrencySymbol, ErrNativeTokenNotFound)
|
|
continue
|
|
}
|
|
|
|
// add token balance for the chain
|
|
var tokenBalance *big.Int
|
|
if input.SendType == ERC721Transfer {
|
|
tokenBalance = big.NewInt(1)
|
|
} else if input.SendType == ERC1155Transfer {
|
|
tokenBalance, err = r.getERC1155Balance(ctx, chain, token, input.AddrFrom)
|
|
if err != nil {
|
|
chainError(chain.ChainID, token.Symbol, errors.CreateErrorResponseFromError(err))
|
|
}
|
|
} else {
|
|
tokenBalance, err = r.getBalance(ctx, chain.ChainID, token, input.AddrFrom)
|
|
if err != nil {
|
|
chainError(chain.ChainID, token.Symbol, errors.CreateErrorResponseFromError(err))
|
|
}
|
|
}
|
|
// add only if balance is not nil
|
|
if tokenBalance != nil {
|
|
balanceMap[makeBalanceKey(chain.ChainID, token.Symbol)] = tokenBalance
|
|
}
|
|
|
|
if token.IsNative() {
|
|
continue
|
|
}
|
|
|
|
// add native token balance for the chain
|
|
nativeBalance, err := r.getBalance(ctx, chain.ChainID, nativeToken, input.AddrFrom)
|
|
if err != nil {
|
|
chainError(chain.ChainID, token.Symbol, errors.CreateErrorResponseFromError(err))
|
|
}
|
|
// add only if balance is not nil
|
|
if nativeBalance != nil {
|
|
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,
|
|
subtractFees: true, // for chains where we're taking the full balance, we want to subtract the fees
|
|
})
|
|
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()
|
|
|
|
if amountToSend.Cmp(pathprocessor.ZeroBigIntValue) == 0 {
|
|
finalCrossChainAmountOptions[selectedFromChain.ChainID] = append(finalCrossChainAmountOptions[selectedFromChain.ChainID], amountOption{
|
|
amount: amountToSend,
|
|
locked: false,
|
|
})
|
|
continue
|
|
}
|
|
|
|
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, selectedToChains []*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 !arrayContainsElement(network.ChainID, input.DisabledFromChainIDs) {
|
|
selectedFromChains = append(selectedFromChains, network)
|
|
}
|
|
|
|
if !arrayContainsElement(network.ChainID, input.DisabledToChainIDs) {
|
|
selectedToChains = append(selectedToChains, network)
|
|
}
|
|
}
|
|
|
|
return selectedFromChains, selectedToChains, nil
|
|
}
|
|
|
|
func (r *Router) resolveCandidates(ctx context.Context, input *RouteInputParams, selectedFromChains []*params.Network,
|
|
selectedToChains []*params.Network, balanceMap map[string]*big.Int) (candidates []*PathV2, processorErrors []*ProcessorError, 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, nil, errors.CreateErrorResponseFromError(err)
|
|
}
|
|
|
|
appendProcessorErrorFn := func(processorName string, sendType SendType, fromChainID uint64, toChainID uint64, amount *big.Int, err error) {
|
|
log.Error("routerv2.resolveCandidates error", "processor", processorName, "sendType", sendType, "fromChainId: ", fromChainID, "toChainId", toChainID, "amount", amount, "err", err)
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
processorErrors = append(processorErrors, &ProcessorError{
|
|
ProcessorName: processorName,
|
|
Error: err,
|
|
})
|
|
}
|
|
|
|
appendPathFn := func(path *PathV2) {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
candidates = append(candidates, path)
|
|
}
|
|
|
|
for networkIdx := range selectedFromChains {
|
|
network := selectedFromChains[networkIdx]
|
|
|
|
if !input.SendType.isAvailableFor(network) {
|
|
continue
|
|
}
|
|
|
|
var (
|
|
token *walletToken.Token
|
|
toToken *walletToken.Token
|
|
)
|
|
|
|
if testsMode {
|
|
token = input.testParams.tokenFrom
|
|
} else {
|
|
token = input.SendType.FindToken(r.tokenManager, r.collectiblesService, input.AddrFrom, network, input.TokenID)
|
|
}
|
|
if token == nil {
|
|
continue
|
|
}
|
|
|
|
if input.SendType == Swap {
|
|
toToken = input.SendType.FindToken(r.tokenManager, r.collectiblesService, common.Address{}, network, input.ToTokenID)
|
|
}
|
|
|
|
var fees *SuggestedFees
|
|
if testsMode {
|
|
fees = input.testParams.suggestedFees
|
|
} else {
|
|
fees, err = r.feesManager.SuggestedFees(ctx, network.ChainID)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
}
|
|
|
|
group.Add(func(c context.Context) error {
|
|
for _, amountOption := range crossChainAmountOptions[network.ChainID] {
|
|
for _, pProcessor := range r.pathProcessors {
|
|
// With the condition below we're eliminating `Swap` as potential path that can participate in calculating the best route
|
|
// once we decide to inlcude `Swap` in the calculation we need to update `canUseProcessor` function.
|
|
// This also applies to including another (Celer) bridge in the calculation.
|
|
// TODO:
|
|
// this algorithm, includeing finding the best route, has to be updated to include more bridges and one (for now) or more swap options
|
|
// it means that candidates should not be treated linearly, but improve the logic to have multiple routes with different processors of the same type.
|
|
// Example:
|
|
// Routes for sending SNT from Ethereum to Optimism can be:
|
|
// 1. Swap SNT(mainnet) to ETH(mainnet); then bridge via Hop ETH(mainnet) to ETH(opt); then Swap ETH(opt) to SNT(opt); then send SNT (opt) to the destination
|
|
// 2. Swap SNT(mainnet) to ETH(mainnet); then bridge via Celer ETH(mainnet) to ETH(opt); then Swap ETH(opt) to SNT(opt); then send SNT (opt) to the destination
|
|
// 3. Swap SNT(mainnet) to USDC(mainnet); then bridge via Hop USDC(mainnet) to USDC(opt); then Swap USDC(opt) to SNT(opt); then send SNT (opt) to the destination
|
|
// 4. Swap SNT(mainnet) to USDC(mainnet); then bridge via Celer USDC(mainnet) to USDC(opt); then Swap USDC(opt) to SNT(opt); then send SNT (opt) to the destination
|
|
// 5. ...
|
|
// 6. ...
|
|
//
|
|
// With the current routing algorithm atm we're not able to generate all possible routes.
|
|
if !input.SendType.canUseProcessor(pProcessor) {
|
|
continue
|
|
}
|
|
|
|
// if just a single from and to chain is selected for transfer, we can skip the bridge as potential path
|
|
if !input.SendType.simpleTransfer(pProcessor) && sameSingleChainTransfer(selectedFromChains, selectedToChains) {
|
|
continue
|
|
}
|
|
|
|
if !input.SendType.processZeroAmountInProcessor(amountOption.amount, input.AmountOut.ToInt(), pProcessor.Name()) {
|
|
continue
|
|
}
|
|
|
|
for _, dest := range selectedToChains {
|
|
|
|
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 {
|
|
appendProcessorErrorFn(pProcessor.Name(), input.SendType, processorInputParams.FromChain.ChainID, processorInputParams.ToChain.ChainID, processorInputParams.AmountIn, err)
|
|
continue
|
|
}
|
|
if !can {
|
|
continue
|
|
}
|
|
|
|
bonderFees, tokenFees, err := pProcessor.CalculateFees(processorInputParams)
|
|
if err != nil {
|
|
appendProcessorErrorFn(pProcessor.Name(), input.SendType, processorInputParams.FromChain.ChainID, processorInputParams.ToChain.ChainID, processorInputParams.AmountIn, err)
|
|
continue
|
|
}
|
|
|
|
gasLimit, err := pProcessor.EstimateGas(processorInputParams)
|
|
if err != nil {
|
|
appendProcessorErrorFn(pProcessor.Name(), input.SendType, processorInputParams.FromChain.ChainID, processorInputParams.ToChain.ChainID, processorInputParams.AmountIn, err)
|
|
continue
|
|
}
|
|
|
|
approvalContractAddress, err := pProcessor.GetContractAddress(processorInputParams)
|
|
if err != nil {
|
|
appendProcessorErrorFn(pProcessor.Name(), input.SendType, processorInputParams.FromChain.ChainID, processorInputParams.ToChain.ChainID, processorInputParams.AmountIn, err)
|
|
continue
|
|
}
|
|
approvalRequired, approvalAmountRequired, approvalGasLimit, l1ApprovalFee, err := r.requireApproval(ctx, input.SendType, &approvalContractAddress, processorInputParams)
|
|
if err != nil {
|
|
appendProcessorErrorFn(pProcessor.Name(), input.SendType, processorInputParams.FromChain.ChainID, processorInputParams.ToChain.ChainID, processorInputParams.AmountIn, err)
|
|
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 {
|
|
appendProcessorErrorFn(pProcessor.Name(), input.SendType, processorInputParams.FromChain.ChainID, processorInputParams.ToChain.ChainID, processorInputParams.AmountIn, err)
|
|
continue
|
|
}
|
|
|
|
maxFeesPerGas := fees.feeFor(input.GasFeeMode)
|
|
|
|
estimatedTime := r.feesManager.TransactionEstimatedTime(ctx, network.ChainID, maxFeesPerGas)
|
|
if approvalRequired && estimatedTime < MoreThanFiveMinutes {
|
|
estimatedTime += 1
|
|
}
|
|
|
|
// calculate ETH fees
|
|
ethTotalFees := big.NewInt(0)
|
|
txFeeInWei := new(big.Int).Mul(maxFeesPerGas, big.NewInt(int64(gasLimit)))
|
|
ethTotalFees.Add(ethTotalFees, txFeeInWei)
|
|
|
|
txL1FeeInWei := big.NewInt(0)
|
|
if l1FeeWei > 0 {
|
|
txL1FeeInWei = big.NewInt(int64(l1FeeWei))
|
|
ethTotalFees.Add(ethTotalFees, txL1FeeInWei)
|
|
}
|
|
|
|
approvalFeeInWei := big.NewInt(0)
|
|
approvalL1FeeInWei := big.NewInt(0)
|
|
if approvalRequired {
|
|
approvalFeeInWei.Mul(maxFeesPerGas, big.NewInt(int64(approvalGasLimit)))
|
|
ethTotalFees.Add(ethTotalFees, approvalFeeInWei)
|
|
|
|
if l1ApprovalFee > 0 {
|
|
approvalL1FeeInWei = big.NewInt(int64(l1ApprovalFee))
|
|
ethTotalFees.Add(ethTotalFees, approvalL1FeeInWei)
|
|
}
|
|
}
|
|
|
|
// calculate required balances (bonder and token fees are already included in the amountIn by Hop bridge (once we include Celar we need to check how they handle the fees))
|
|
requiredNativeBalance := big.NewInt(0)
|
|
requiredTokenBalance := big.NewInt(0)
|
|
|
|
if token.IsNative() {
|
|
requiredNativeBalance.Add(requiredNativeBalance, amountOption.amount)
|
|
if !amountOption.subtractFees {
|
|
requiredNativeBalance.Add(requiredNativeBalance, ethTotalFees)
|
|
}
|
|
} else {
|
|
requiredTokenBalance.Add(requiredTokenBalance, amountOption.amount)
|
|
requiredNativeBalance.Add(requiredNativeBalance, ethTotalFees)
|
|
}
|
|
|
|
appendPathFn(&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,
|
|
MaxFeesPerGas: (*hexutil.Big)(maxFeesPerGas),
|
|
|
|
TxBaseFee: (*hexutil.Big)(fees.BaseFee),
|
|
TxPriorityFee: (*hexutil.Big)(fees.MaxPriorityFeePerGas),
|
|
TxGasAmount: gasLimit,
|
|
TxBonderFees: (*hexutil.Big)(bonderFees),
|
|
TxTokenFees: (*hexutil.Big)(tokenFees),
|
|
|
|
TxFee: (*hexutil.Big)(txFeeInWei),
|
|
TxL1Fee: (*hexutil.Big)(txL1FeeInWei),
|
|
|
|
ApprovalRequired: approvalRequired,
|
|
ApprovalAmountRequired: (*hexutil.Big)(approvalAmountRequired),
|
|
ApprovalContractAddress: &approvalContractAddress,
|
|
ApprovalBaseFee: (*hexutil.Big)(fees.BaseFee),
|
|
ApprovalPriorityFee: (*hexutil.Big)(fees.MaxPriorityFeePerGas),
|
|
ApprovalGasAmount: approvalGasLimit,
|
|
|
|
ApprovalFee: (*hexutil.Big)(approvalFeeInWei),
|
|
ApprovalL1Fee: (*hexutil.Big)(approvalL1FeeInWei),
|
|
|
|
TxTotalFee: (*hexutil.Big)(ethTotalFees),
|
|
|
|
EstimatedTime: estimatedTime,
|
|
|
|
subtractFees: amountOption.subtractFees,
|
|
requiredTokenBalance: requiredTokenBalance,
|
|
requiredNativeBalance: requiredNativeBalance,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
sort.Slice(candidates, func(i, j int) bool {
|
|
iChain := getChainPriority(candidates[i].FromChain.ChainID)
|
|
jChain := getChainPriority(candidates[j].FromChain.ChainID)
|
|
return iChain <= jChain
|
|
})
|
|
|
|
group.Wait()
|
|
return candidates, processorErrors, nil
|
|
}
|
|
|
|
func (r *Router) checkBalancesForTheBestRoute(ctx context.Context, bestRoute []*PathV2, balanceMap map[string]*big.Int) (hasPositiveBalance bool, err error) {
|
|
balanceMapCopy := copyMapGeneric(balanceMap, func(v interface{}) interface{} {
|
|
return new(big.Int).Set(v.(*big.Int))
|
|
}).(map[string]*big.Int)
|
|
if balanceMapCopy == nil {
|
|
return false, ErrCannotCheckBalance
|
|
}
|
|
|
|
// check the best route for the required balances
|
|
for _, path := range bestRoute {
|
|
tokenKey := makeBalanceKey(path.FromChain.ChainID, path.FromToken.Symbol)
|
|
if tokenBalance, ok := balanceMapCopy[tokenKey]; ok {
|
|
if tokenBalance.Cmp(pathprocessor.ZeroBigIntValue) > 0 {
|
|
hasPositiveBalance = true
|
|
}
|
|
}
|
|
|
|
if path.ProcessorName == pathprocessor.ProcessorBridgeHopName {
|
|
if path.TxBonderFees.ToInt().Cmp(path.AmountOut.ToInt()) > 0 {
|
|
return hasPositiveBalance, ErrLowAmountInForHopBridge
|
|
}
|
|
}
|
|
|
|
if path.requiredTokenBalance != nil && path.requiredTokenBalance.Cmp(pathprocessor.ZeroBigIntValue) > 0 {
|
|
if tokenBalance, ok := balanceMapCopy[tokenKey]; ok {
|
|
if tokenBalance.Cmp(path.requiredTokenBalance) == -1 {
|
|
err := &errors.ErrorResponse{
|
|
Code: ErrNotEnoughTokenBalance.Code,
|
|
Details: fmt.Sprintf(ErrNotEnoughTokenBalance.Details, path.FromToken.Symbol, path.FromChain.ChainID),
|
|
}
|
|
return hasPositiveBalance, err
|
|
}
|
|
balanceMapCopy[tokenKey].Sub(tokenBalance, path.requiredTokenBalance)
|
|
} else {
|
|
return hasPositiveBalance, ErrTokenNotFound
|
|
}
|
|
}
|
|
|
|
ethKey := makeBalanceKey(path.FromChain.ChainID, pathprocessor.EthSymbol)
|
|
if nativeBalance, ok := balanceMapCopy[ethKey]; ok {
|
|
if nativeBalance.Cmp(path.requiredNativeBalance) == -1 {
|
|
err := &errors.ErrorResponse{
|
|
Code: ErrNotEnoughNativeBalance.Code,
|
|
Details: fmt.Sprintf(ErrNotEnoughNativeBalance.Details, pathprocessor.EthSymbol, path.FromChain.ChainID),
|
|
}
|
|
return hasPositiveBalance, err
|
|
}
|
|
balanceMapCopy[ethKey].Sub(nativeBalance, path.requiredNativeBalance)
|
|
} else {
|
|
return hasPositiveBalance, ErrNativeTokenNotFound
|
|
}
|
|
}
|
|
|
|
return hasPositiveBalance, nil
|
|
}
|
|
|
|
func removeBestRouteFromAllRouters(allRoutes [][]*PathV2, best []*PathV2) [][]*PathV2 {
|
|
for i := len(allRoutes) - 1; i >= 0; i-- {
|
|
route := allRoutes[i]
|
|
routeFound := true
|
|
for _, p := range route {
|
|
found := false
|
|
for _, b := range best {
|
|
if p.ProcessorName == b.ProcessorName &&
|
|
(p.FromChain == nil && b.FromChain == nil || p.FromChain.ChainID == b.FromChain.ChainID) &&
|
|
(p.ToChain == nil && b.ToChain == nil || p.ToChain.ChainID == b.ToChain.ChainID) &&
|
|
(p.FromToken == nil && b.FromToken == nil || p.FromToken.Symbol == b.FromToken.Symbol) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
routeFound = false
|
|
break
|
|
}
|
|
}
|
|
if routeFound {
|
|
return append(allRoutes[:i], allRoutes[i+1:]...)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func getChainPriority(chainID uint64) int {
|
|
switch chainID {
|
|
case walletCommon.EthereumMainnet, walletCommon.EthereumSepolia:
|
|
return 1
|
|
case walletCommon.OptimismMainnet, walletCommon.OptimismSepolia:
|
|
return 2
|
|
case walletCommon.ArbitrumMainnet, walletCommon.ArbitrumSepolia:
|
|
return 3
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
func getRoutePriority(route []*PathV2) int {
|
|
priority := 0
|
|
for _, path := range route {
|
|
priority += getChainPriority(path.FromChain.ChainID)
|
|
}
|
|
return priority
|
|
}
|
|
|
|
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
|
|
} else {
|
|
prices, err = input.SendType.FetchPrices(r.marketManager, input.TokenID)
|
|
if err != nil {
|
|
return nil, errors.CreateErrorResponseFromError(err)
|
|
}
|
|
}
|
|
|
|
tokenPrice := prices[input.TokenID]
|
|
nativeTokenPrice := prices[pathprocessor.EthSymbol]
|
|
|
|
var allRoutes [][]*PathV2
|
|
suggestedRoutes, allRoutes = newSuggestedRoutesV2(input.Uuid, input.AmountIn.ToInt(), candidates, input.FromLockedAmount, tokenPrice, nativeTokenPrice)
|
|
|
|
defer func() {
|
|
if suggestedRoutes.Best != nil && len(suggestedRoutes.Best) > 0 {
|
|
sort.Slice(suggestedRoutes.Best, func(i, j int) bool {
|
|
iChain := getChainPriority(suggestedRoutes.Best[i].FromChain.ChainID)
|
|
jChain := getChainPriority(suggestedRoutes.Best[j].FromChain.ChainID)
|
|
return iChain <= jChain
|
|
})
|
|
}
|
|
}()
|
|
|
|
var (
|
|
bestRoute []*PathV2
|
|
lastBestRouteWithPositiveBalance []*PathV2
|
|
lastBestRouteErr error
|
|
)
|
|
|
|
for len(allRoutes) > 0 {
|
|
bestRoute = findBestV2(allRoutes, tokenPrice, nativeTokenPrice)
|
|
var hasPositiveBalance bool
|
|
hasPositiveBalance, err = r.checkBalancesForTheBestRoute(ctx, bestRoute, 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
|
|
if input.SendType == Transfer ||
|
|
input.SendType == Bridge {
|
|
if hasPositiveBalance {
|
|
lastBestRouteWithPositiveBalance = bestRoute
|
|
lastBestRouteErr = err
|
|
}
|
|
|
|
if len(allRoutes) > 1 {
|
|
allRoutes = removeBestRouteFromAllRouters(allRoutes, bestRoute)
|
|
continue
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
break
|
|
}
|
|
|
|
// if none of the routes have positive balance, we should return the last best route with positive balance
|
|
if err != nil && lastBestRouteWithPositiveBalance != nil {
|
|
bestRoute = lastBestRouteWithPositiveBalance
|
|
err = lastBestRouteErr
|
|
}
|
|
|
|
if len(bestRoute) > 0 {
|
|
// At this point we have to do the final check and update the amountIn (subtracting fees) if complete balance is going to be sent for native token (ETH)
|
|
for _, path := range bestRoute {
|
|
if path.subtractFees && path.FromToken.IsNative() {
|
|
path.AmountIn.ToInt().Sub(path.AmountIn.ToInt(), path.TxFee.ToInt())
|
|
if path.TxL1Fee.ToInt().Cmp(pathprocessor.ZeroBigIntValue) > 0 {
|
|
path.AmountIn.ToInt().Sub(path.AmountIn.ToInt(), path.TxL1Fee.ToInt())
|
|
}
|
|
if path.ApprovalRequired {
|
|
path.AmountIn.ToInt().Sub(path.AmountIn.ToInt(), path.ApprovalFee.ToInt())
|
|
if path.ApprovalL1Fee.ToInt().Cmp(pathprocessor.ZeroBigIntValue) > 0 {
|
|
path.AmountIn.ToInt().Sub(path.AmountIn.ToInt(), path.ApprovalL1Fee.ToInt())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
suggestedRoutes.Best = bestRoute
|
|
|
|
return suggestedRoutes, err
|
|
}
|