status-go/services/wallet/router/router_v2.go

576 lines
18 KiB
Go

package router
import (
"context"
"errors"
"math"
"math/big"
"sort"
"sync"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/status-im/status-go/params"
"github.com/status-im/status-go/services/wallet/async"
walletToken "github.com/status-im/status-go/services/wallet/token"
)
type RouteInputParams struct {
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"`
TokenID string `json:"tokenID" validate:"required"`
ToTokenID string `json:"toTokenID"`
DisabledFromChainIDs []uint64 `json:"disabledFromChainIDs"`
DisabledToChaindIDs []uint64 `json:"disabledToChaindIDs"`
PreferedChainIDs []uint64 `json:"preferedChainIDs"`
GasFeeMode GasFeeMode `json:"gasFeeMode" validate:"required"`
FromLockedAmount map[uint64]*hexutil.Big `json:"fromLockedAmount"`
TestnetMode bool `json:"testnetMode"`
}
type PathV2 struct {
BridgeName string
From *params.Network // Source chain
To *params.Network // Destination chain
FromToken *walletToken.Token // Token on the source chain
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
SuggestedPriorityFees *PriorityFees // Suggested priority fees for the transaction
TxBaseFee *hexutil.Big // Base fee for the transaction
TxPriorityFee *hexutil.Big // Priority fee for the transaction, by default we're using the Medium priority fee
TxGasAmount uint64 // Gas used for the transaction
TxBonderFees *hexutil.Big // Bonder fees for the transaction - used for Hop bridge
TxTokenFees *hexutil.Big // Token fees for the transaction - used for bridges (represent the difference between the amount in and the amount out)
TxL1Fee *hexutil.Big // L1 fee for the transaction - used for for transactions placed on L2 chains
ApprovalRequired bool // Is approval required for the transaction
ApprovalAmountRequired *hexutil.Big // Amount required for the approval transaction
ApprovalContractAddress *common.Address // Address of the contract that needs to be approved
ApprovalBaseFee *hexutil.Big // Base fee for the approval transaction
ApprovalPriorityFee *hexutil.Big // Priority fee for the approval transaction
ApprovalGasAmount uint64 // Gas used for the approval transaction
ApprovalL1Fee *hexutil.Big // L1 fee for the approval transaction - used for for transactions placed on L2 chains
EstimatedTime TransactionEstimation
requiredTokenBalance *big.Int
requiredNativeBalance *big.Int
}
func (p *PathV2) Equal(o *PathV2) bool {
return p.From.ChainID == o.From.ChainID && p.To.ChainID == o.To.ChainID
}
type SuggestedRoutesV2 struct {
Best []*PathV2
Candidates []*PathV2
TokenPrice float64
NativeChainTokenPrice float64
}
type GraphV2 = []*NodeV2
type NodeV2 struct {
Path *PathV2
Children GraphV2
}
func newSuggestedRoutesV2(
amountIn *big.Int,
candidates []*PathV2,
fromLockedAmount map[uint64]*hexutil.Big,
tokenPrice float64,
nativeChainTokenPrice float64,
) *SuggestedRoutesV2 {
suggestedRoutes := &SuggestedRoutesV2{
Candidates: candidates,
Best: candidates,
TokenPrice: tokenPrice,
NativeChainTokenPrice: nativeChainTokenPrice,
}
if len(candidates) == 0 {
return suggestedRoutes
}
node := &NodeV2{
Path: nil,
Children: buildGraphV2(amountIn, candidates, 0, []uint64{}),
}
routes := node.buildAllRoutesV2()
routes = filterRoutesV2(routes, amountIn, fromLockedAmount)
best := findBestV2(routes, tokenPrice, nativeChainTokenPrice)
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.AmountIn.ToInt())
if diff.Cmp(zero) >= 0 {
path.AmountIn = (*hexutil.Big)(path.AmountIn.ToInt())
} else {
path.AmountIn = (*hexutil.Big)(new(big.Int).Set(rest))
}
rest.Sub(rest, path.AmountIn.ToInt())
}
}
suggestedRoutes.Best = best
return suggestedRoutes
}
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.From.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.From.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 filterRoutesV2(routes [][]*PathV2, amountIn *big.Int, fromLockedAmount map[uint64]*hexutil.Big) [][]*PathV2 {
if len(fromLockedAmount) == 0 {
return routes
}
filteredRoutesLevel1 := make([][]*PathV2, 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(zero) == 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([][]*PathV2, 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.AmountIn.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 findBestV2(routes [][]*PathV2, tokenPrice float64, nativeChainTokenPrice float64) []*PathV2 {
var best []*PathV2
bestCost := big.NewFloat(math.Inf(1))
for _, route := range routes {
currentCost := big.NewFloat(0)
for _, path := range route {
if path.FromToken.IsNative() {
path.requiredNativeBalance = new(big.Int).Set(path.AmountIn.ToInt())
} else {
path.requiredTokenBalance = new(big.Int).Set(path.AmountIn.ToInt())
path.requiredNativeBalance = big.NewInt(0)
}
// ecaluate the cost of the path
pathCost := big.NewFloat(0)
nativeTokenPrice := new(big.Float).SetFloat64(nativeChainTokenPrice)
if path.TxBaseFee != nil && path.TxPriorityFee != nil {
feePerGas := new(big.Int).Add(path.TxBaseFee.ToInt(), path.TxPriorityFee.ToInt())
txFeeInWei := new(big.Int).Mul(feePerGas, big.NewInt(int64(path.TxGasAmount)))
txFeeInEth := gweiToEth(weiToGwei(txFeeInWei))
path.requiredNativeBalance.Add(path.requiredNativeBalance, txFeeInWei)
pathCost = new(big.Float).Mul(txFeeInEth, nativeTokenPrice)
}
if path.TxBonderFees != nil && path.TxBonderFees.ToInt().Cmp(zero) > 0 {
bonderFeeInWei := path.TxBonderFees.ToInt()
bonderFeeInEth := gweiToEth(weiToGwei(bonderFeeInWei))
path.requiredNativeBalance.Add(path.requiredNativeBalance, bonderFeeInWei)
pathCost.Add(pathCost, new(big.Float).Mul(bonderFeeInEth, nativeTokenPrice))
}
if path.TxL1Fee != nil && path.TxL1Fee.ToInt().Cmp(zero) > 0 {
l1FeeInWei := path.TxL1Fee.ToInt()
l1FeeInEth := gweiToEth(weiToGwei(l1FeeInWei))
path.requiredNativeBalance.Add(path.requiredNativeBalance, l1FeeInWei)
pathCost.Add(pathCost, new(big.Float).Mul(l1FeeInEth, nativeTokenPrice))
}
if path.TxTokenFees != nil && path.TxTokenFees.ToInt().Cmp(zero) > 0 && path.FromToken != nil {
path.requiredTokenBalance.Add(path.requiredTokenBalance, path.TxTokenFees.ToInt())
pathCost.Add(pathCost, new(big.Float).Mul(
new(big.Float).Quo(new(big.Float).SetInt(path.TxTokenFees.ToInt()), big.NewFloat(math.Pow(10, float64(path.FromToken.Decimals)))),
new(big.Float).SetFloat64(tokenPrice)))
}
if path.ApprovalRequired {
if path.ApprovalBaseFee != nil && path.ApprovalPriorityFee != nil {
feePerGas := new(big.Int).Add(path.ApprovalBaseFee.ToInt(), path.ApprovalPriorityFee.ToInt())
txFeeInWei := new(big.Int).Mul(feePerGas, big.NewInt(int64(path.ApprovalGasAmount)))
txFeeInEth := gweiToEth(weiToGwei(txFeeInWei))
path.requiredNativeBalance.Add(path.requiredNativeBalance, txFeeInWei)
pathCost.Add(pathCost, new(big.Float).Mul(txFeeInEth, nativeTokenPrice))
}
if path.ApprovalL1Fee != nil {
l1FeeInWei := path.ApprovalL1Fee.ToInt()
l1FeeInEth := gweiToEth(weiToGwei(l1FeeInWei))
path.requiredNativeBalance.Add(path.requiredNativeBalance, l1FeeInWei)
pathCost.Add(pathCost, new(big.Float).Mul(l1FeeInEth, nativeTokenPrice))
}
}
currentCost = new(big.Float).Add(currentCost, pathCost)
}
if currentCost.Cmp(bestCost) == -1 {
best = route
bestCost = currentCost
}
}
return best
}
func (r *Router) SuggestedRoutesV2(ctx context.Context, input *RouteInputParams) (*SuggestedRoutesV2, error) {
networks, err := r.rpcClient.NetworkManager.Get(false)
if err != nil {
return nil, err
}
var (
group = async.NewAtomicGroup(ctx)
mu sync.Mutex
candidates = make([]*PathV2, 0)
)
for networkIdx := range networks {
network := networks[networkIdx]
if network.IsTest != input.TestnetMode {
continue
}
if containsNetworkChainID(network, input.DisabledFromChainIDs) {
continue
}
if !input.SendType.isAvailableFor(network) {
continue
}
token := input.SendType.FindToken(r.tokenManager, r.collectiblesService, input.AddrFrom, network, input.TokenID)
if token == nil {
continue
}
var toToken *walletToken.Token
if input.SendType == Swap {
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
}
if len(input.FromLockedAmount) > 0 {
for chainID, lockedAmount := range input.FromLockedAmount {
if chainID == network.ChainID {
continue
}
amountToSend = new(big.Int).Sub(amountToSend, lockedAmount.ToInt())
}
}
group.Add(func(c context.Context) error {
client, err := r.rpcClient.EthClient(network.ChainID)
if err != nil {
return err
}
for _, bridge := range r.bridges {
if !input.SendType.canUseBridge(bridge) {
continue
}
for _, dest := range networks {
if dest.IsTest != input.TestnetMode {
continue
}
if !input.SendType.isAvailableFor(network) {
continue
}
if !input.SendType.isAvailableBetween(network, dest) {
continue
}
if len(input.PreferedChainIDs) > 0 && !containsNetworkChainID(dest, input.PreferedChainIDs) {
continue
}
if containsNetworkChainID(dest, input.DisabledToChaindIDs) {
continue
}
can, err := bridge.AvailableFor(network, dest, token, toToken)
if err != nil || !can {
continue
}
bonderFees, tokenFees, err := bridge.CalculateFees(network, dest, token, amountToSend)
if err != nil {
continue
}
gasLimit := uint64(0)
if input.SendType.isTransfer() {
gasLimit, err = bridge.EstimateGas(network, dest, input.AddrFrom, input.AddrTo, token, toToken, amountToSend)
if err != nil {
continue
}
} else {
gasLimit = input.SendType.EstimateGas(r.ensService, r.stickersService, network, input.AddrFrom, input.TokenID)
}
approvalContractAddress := bridge.GetContractAddress(network, token)
approvalRequired, approvalAmountRequired, approvalGasLimit, l1ApprovalFee, err := r.requireApproval(ctx, input.SendType, approvalContractAddress, input.AddrFrom, network, token, amountToSend)
if err != nil {
continue
}
var l1FeeWei uint64
if input.SendType.needL1Fee() {
tx, err := bridge.BuildTx(network, dest, input.AddrFrom, input.AddrTo, token, amountToSend, bonderFees)
if err != nil {
continue
}
l1FeeWei, _ = r.feesManager.GetL1Fee(ctx, network.ChainID, tx)
}
baseFee, err := r.feesManager.getBaseFee(ctx, client)
if err != nil {
continue
}
priorityFees, err := r.feesManager.getPriorityFees(ctx, client, baseFee)
if err != nil {
continue
}
selctedPriorityFee := priorityFees.Medium
if input.GasFeeMode == GasFeeHigh {
selctedPriorityFee = priorityFees.High
} else if input.GasFeeMode == GasFeeLow {
selctedPriorityFee = priorityFees.Low
}
amountOut, err := bridge.CalculateAmountOut(network, dest, amountToSend, token.Symbol)
if err != nil {
continue
}
maxFeesPerGas := new(big.Float)
maxFeesPerGas.Add(new(big.Float).SetInt(baseFee), new(big.Float).SetInt(selctedPriorityFee))
estimatedTime := r.feesManager.TransactionEstimatedTime(ctx, network.ChainID, maxFeesPerGas)
if approvalRequired && estimatedTime < MoreThanFiveMinutes {
estimatedTime += 1
}
mu.Lock()
candidates = append(candidates, &PathV2{
BridgeName: bridge.Name(),
From: network,
To: network,
FromToken: token,
AmountIn: (*hexutil.Big)(amountToSend),
AmountInLocked: amountLocked,
AmountOut: (*hexutil.Big)(amountOut),
SuggestedPriorityFees: &priorityFees,
TxBaseFee: (*hexutil.Big)(baseFee),
TxPriorityFee: (*hexutil.Big)(selctedPriorityFee),
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)(baseFee),
ApprovalPriorityFee: (*hexutil.Big)(selctedPriorityFee),
ApprovalGasAmount: approvalGasLimit,
ApprovalL1Fee: (*hexutil.Big)(big.NewInt(int64(l1ApprovalFee))),
EstimatedTime: estimatedTime,
},
)
mu.Unlock()
}
}
return nil
})
}
group.Wait()
prices, err := input.SendType.FetchPrices(r.marketManager, input.TokenID)
if err != nil {
return nil, err
}
suggestedRoutes := newSuggestedRoutesV2(input.AmountIn.ToInt(), candidates, input.FromLockedAmount, prices[input.TokenID], prices["ETH"])
// check the best route for the required balances
for _, path := range suggestedRoutes.Best {
if path.requiredTokenBalance != nil && path.requiredTokenBalance.Cmp(big.NewInt(0)) > 0 {
tokenBalance := big.NewInt(1)
if input.SendType == ERC1155Transfer {
tokenBalance, err = r.getERC1155Balance(ctx, path.From, path.FromToken, input.AddrFrom)
if err != nil {
return nil, err
}
} else if input.SendType != ERC721Transfer {
tokenBalance, err = r.getBalance(ctx, path.From, path.FromToken, input.AddrFrom)
if err != nil {
return nil, err
}
}
if tokenBalance.Cmp(path.requiredTokenBalance) == -1 {
return suggestedRoutes, errors.New("not enough token balance")
}
}
nativeToken := r.tokenManager.FindToken(path.From, path.From.NativeCurrencySymbol)
if nativeToken == nil {
return nil, errors.New("native token not found")
}
nativeBalance, err := r.getBalance(ctx, path.From, nativeToken, input.AddrFrom)
if err != nil {
return nil, err
}
if nativeBalance.Cmp(path.requiredNativeBalance) == -1 {
return suggestedRoutes, errors.New("not enough native balance")
}
}
return suggestedRoutes, nil
}