Added improvements in resolving the best route (#5569)

* fix_: filtering out routes which do not match the amount in

* fix_: finding the best route logic updated

* fix_: "to" chains being used in sending bridge tx via hop are more explicit

Using `ChainIDTo` depicts better an intention which chain should be used.

* chore_: checking for required balance improved

An error contains now more details, for which token on which chain there is not enough balance for
executing a tx. Also check for required balance now calculates in fees for all tx that possibly can be
sent from the same chain.
This commit is contained in:
saledjenic 2024-07-25 14:15:30 +02:00 committed by GitHub
parent 8bb0bb0b3c
commit 2bbdb35f6c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 495 additions and 222 deletions

View File

@ -21,12 +21,14 @@ var (
ErrLockedAmountNotNegative = &errors.ErrorResponse{Code: errors.ErrorCode("WR-013"), Details: "locked amount must not be negative"}
ErrLockedAmountExceedsTotalSendAmount = &errors.ErrorResponse{Code: errors.ErrorCode("WR-014"), Details: "locked amount exceeds the total amount to send"}
ErrLockedAmountLessThanSendAmountAllNetworks = &errors.ErrorResponse{Code: errors.ErrorCode("WR-015"), Details: "locked amount is less than the total amount to send, but all networks are locked"}
ErrNotEnoughTokenBalance = &errors.ErrorResponse{Code: errors.ErrorCode("WR-016"), Details: "not enough token balance"}
ErrNotEnoughNativeBalance = &errors.ErrorResponse{Code: errors.ErrorCode("WR-017"), Details: "not enough native balance"}
ErrNotEnoughTokenBalance = &errors.ErrorResponse{Code: errors.ErrorCode("WR-016"), Details: "{\"token\": %s, \"chainId\": %d}"}
ErrNotEnoughNativeBalance = &errors.ErrorResponse{Code: errors.ErrorCode("WR-017"), Details: "{\"token\": %s, \"chainId\": %d}"}
ErrNativeTokenNotFound = &errors.ErrorResponse{Code: errors.ErrorCode("WR-018"), Details: "native token not found"}
ErrDisabledChainFoundAmongLockedNetworks = &errors.ErrorResponse{Code: errors.ErrorCode("WR-019"), Details: "disabled chain found among locked networks"}
ErrENSSetPubKeyInvalidUsername = &errors.ErrorResponse{Code: errors.ErrorCode("WR-020"), Details: "a valid username, ending in '.eth', is required for ENSSetPubKey"}
ErrLockedAmountExcludesAllSupported = &errors.ErrorResponse{Code: errors.ErrorCode("WR-021"), Details: "all supported chains are excluded, routing impossible"}
ErrTokenNotFound = &errors.ErrorResponse{Code: errors.ErrorCode("WR-022"), Details: "token not found"}
ErrNoBestRouteFound = &errors.ErrorResponse{Code: errors.ErrorCode("WR-023"), Details: "no best route found"}
ErrCannotCheckReceiverBalance = &errors.ErrorResponse{Code: errors.ErrorCode("WR-024"), Details: "cannot check receiver balance"}
ErrCannotCheckLockedAmounts = &errors.ErrorResponse{Code: errors.ErrorCode("WR-025"), Details: "cannot check locked amounts"}
)

View File

@ -54,27 +54,23 @@ type SuggestedFeesGwei struct {
EIP1559Enabled bool `json:"eip1559Enabled"`
}
func (s *SuggestedFees) feeFor(mode GasFeeMode) *big.Int {
if !s.EIP1559Enabled {
return s.GasPrice
}
func (m *MaxFeesLevels) feeFor(mode GasFeeMode) *big.Int {
if mode == GasFeeLow {
return s.MaxFeesLevels.Low.ToInt()
return m.Low.ToInt()
}
if mode == GasFeeHigh {
return s.MaxFeesLevels.High.ToInt()
return m.High.ToInt()
}
return s.MaxFeesLevels.Medium.ToInt()
return m.Medium.ToInt()
}
func (s *SuggestedFees) feeFor(mode GasFeeMode) *big.Int {
return s.MaxFeesLevels.feeFor(mode)
}
func (s *SuggestedFeesGwei) feeFor(mode GasFeeMode) *big.Float {
if !s.EIP1559Enabled {
return s.GasPrice
}
if mode == GasFeeLow {
return s.MaxFeePerGasLow
}
@ -141,9 +137,9 @@ func (f *FeeManager) SuggestedFees(ctx context.Context, chainID uint64) (*Sugges
BaseFee: big.NewInt(0),
MaxPriorityFeePerGas: big.NewInt(0),
MaxFeesLevels: &MaxFeesLevels{
Low: (*hexutil.Big)(big.NewInt(0)),
Medium: (*hexutil.Big)(big.NewInt(0)),
High: (*hexutil.Big)(big.NewInt(0)),
Low: (*hexutil.Big)(gasPrice),
Medium: (*hexutil.Big)(gasPrice),
High: (*hexutil.Big)(gasPrice),
},
EIP1559Enabled: false,
}, nil

View File

@ -2,6 +2,7 @@ package router
import (
"math/big"
"reflect"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/status-im/status-go/services/wallet/router/pathprocessor"
@ -20,6 +21,19 @@ func init() {
}
func filterRoutesV2(routes [][]*PathV2, amountIn *big.Int, fromLockedAmount map[uint64]*hexutil.Big) [][]*PathV2 {
for i := len(routes) - 1; i >= 0; i-- {
routeAmount := big.NewInt(0)
for _, p := range routes[i] {
routeAmount.Add(routeAmount, p.AmountIn.ToInt())
}
if routeAmount.Cmp(amountIn) == 0 {
continue
}
routes = append(routes[:i], routes[i+1:]...)
}
if len(fromLockedAmount) == 0 {
return routes
}
@ -43,7 +57,7 @@ func filterNetworkComplianceV2(routes [][]*PathV2, fromLockedAmount map[uint64]*
}
// Create fresh copies of the maps for each route check, because they are manipulated
if isValidForNetworkComplianceV2(route, copyMap(fromIncluded), copyMap(fromExcluded)) {
if isValidForNetworkComplianceV2(route, copyMapGeneric(fromIncluded, nil).(map[uint64]bool), copyMapGeneric(fromExcluded, nil).(map[uint64]bool)) {
filteredRoutes = append(filteredRoutes, route)
}
}
@ -57,6 +71,10 @@ func isValidForNetworkComplianceV2(route []*PathV2, fromIncluded, fromExcluded m
zap.Any("fromExcluded", fromExcluded),
)
if fromIncluded == nil || fromExcluded == nil {
return false
}
for _, path := range route {
if path == nil || path.FromChain == nil {
logger.Debug("Invalid path", zap.Any("path", path))
@ -114,6 +132,10 @@ func filterCapacityValidationV2(routes [][]*PathV2, amountIn *big.Int, fromLocke
func hasSufficientCapacityV2(route []*PathV2, amountIn *big.Int, fromLockedAmount map[uint64]*hexutil.Big) bool {
for _, path := range route {
if amount, ok := fromLockedAmount[path.FromChain.ChainID]; ok {
if path.AmountIn.ToInt().Cmp(amount.ToInt()) != 0 {
logger.Debug("Amount in does not match locked amount", zap.Any("path", path))
return false
}
requiredAmountIn := new(big.Int).Sub(amountIn, amount.ToInt())
restAmountIn := calculateRestAmountInV2(route, path)
@ -121,11 +143,7 @@ func hasSufficientCapacityV2(route []*PathV2, amountIn *big.Int, fromLockedAmoun
logger.Debug("Required amount in", zap.String("requiredAmountIn", requiredAmountIn.String()))
logger.Debug("Rest amount in", zap.String("restAmountIn", restAmountIn.String()))
if restAmountIn.Cmp(requiredAmountIn) >= 0 {
path.AmountIn = amount
path.AmountInLocked = true
logger.Debug("Path has sufficient capacity", zap.Any("path", path))
} else {
if restAmountIn.Cmp(requiredAmountIn) < 0 {
logger.Debug("Path does not have sufficient capacity", zap.Any("path", path))
return false
}
@ -145,11 +163,21 @@ func calculateRestAmountInV2(route []*PathV2, excludePath *PathV2) *big.Int {
return restAmountIn
}
// copyMap creates a copy of the given map[uint64]bool
func copyMap(original map[uint64]bool) map[uint64]bool {
c := make(map[uint64]bool)
for k, v := range original {
c[k] = v
// copyMapGeneric creates a copy of any map, if the deepCopyValue function is provided, it will be used to copy values.
func copyMapGeneric(original interface{}, deepCopyValueFn func(interface{}) interface{}) interface{} {
originalVal := reflect.ValueOf(original)
if originalVal.Kind() != reflect.Map {
return nil
}
return c
newMap := reflect.MakeMap(originalVal.Type())
for iter := originalVal.MapRange(); iter.Next(); {
if deepCopyValueFn != nil {
newMap.SetMapIndex(iter.Key(), reflect.ValueOf(deepCopyValueFn(iter.Value().Interface())))
} else {
newMap.SetMapIndex(iter.Key(), iter.Value())
}
}
return newMap.Interface()
}

View File

@ -27,11 +27,20 @@ var (
amount5 = hexutil.Big(*big.NewInt(500))
path0 = &PathV2{FromChain: network4, AmountIn: &amount0}
path1 = &PathV2{FromChain: network1, AmountIn: &amount1}
path2 = &PathV2{FromChain: network2, AmountIn: &amount2}
path3 = &PathV2{FromChain: network3, AmountIn: &amount3}
path4 = &PathV2{FromChain: network4, AmountIn: &amount4}
path5 = &PathV2{FromChain: network5, AmountIn: &amount5}
pathC1A1 = &PathV2{FromChain: network1, AmountIn: &amount1}
pathC2A1 = &PathV2{FromChain: network2, AmountIn: &amount1}
pathC2A2 = &PathV2{FromChain: network2, AmountIn: &amount2}
pathC3A1 = &PathV2{FromChain: network3, AmountIn: &amount1}
pathC3A2 = &PathV2{FromChain: network3, AmountIn: &amount2}
pathC3A3 = &PathV2{FromChain: network3, AmountIn: &amount3}
pathC4A1 = &PathV2{FromChain: network4, AmountIn: &amount1}
pathC4A4 = &PathV2{FromChain: network4, AmountIn: &amount4}
pathC5A5 = &PathV2{FromChain: network5, AmountIn: &amount5}
)
func routesEqual(t *testing.T, expected, actual [][]*PathV2) bool {
@ -167,33 +176,33 @@ func TestCalculateRestAmountInV2(t *testing.T) {
expected *big.Int
}{
{
name: "Exclude path1",
route: []*PathV2{path1, path2, path3},
excludePath: path1,
name: "Exclude pathC1A1",
route: []*PathV2{pathC1A1, pathC2A2, pathC3A3},
excludePath: pathC1A1,
expected: big.NewInt(500), // 200 + 300
},
{
name: "Exclude path2",
route: []*PathV2{path1, path2, path3},
excludePath: path2,
name: "Exclude pathC2A2",
route: []*PathV2{pathC1A1, pathC2A2, pathC3A3},
excludePath: pathC2A2,
expected: big.NewInt(400), // 100 + 300
},
{
name: "Exclude path3",
route: []*PathV2{path1, path2, path3},
excludePath: path3,
name: "Exclude pathC3A3",
route: []*PathV2{pathC1A1, pathC2A2, pathC3A3},
excludePath: pathC3A3,
expected: big.NewInt(300), // 100 + 200
},
{
name: "Single path, exclude that path",
route: []*PathV2{path1},
excludePath: path1,
route: []*PathV2{pathC1A1},
excludePath: pathC1A1,
expected: big.NewInt(0), // No other paths
},
{
name: "Empty route",
route: []*PathV2{},
excludePath: path1,
excludePath: pathC1A1,
expected: big.NewInt(0), // No paths
},
{
@ -222,42 +231,42 @@ func TestIsValidForNetworkComplianceV2(t *testing.T) {
}{
{
name: "Route with all included chain IDs",
route: []*PathV2{path1, path2},
route: []*PathV2{pathC1A1, pathC2A2},
fromIncluded: map[uint64]bool{1: true, 2: true},
fromExcluded: map[uint64]bool{},
expectedResult: true,
},
{
name: "Route with fromExcluded only",
route: []*PathV2{path1, path2},
route: []*PathV2{pathC1A1, pathC2A2},
fromIncluded: map[uint64]bool{},
fromExcluded: map[uint64]bool{3: false, 4: false},
expectedResult: true,
},
{
name: "Route without excluded chain IDs",
route: []*PathV2{path1, path2},
route: []*PathV2{pathC1A1, pathC2A2},
fromIncluded: map[uint64]bool{1: false, 2: false},
fromExcluded: map[uint64]bool{3: false, 4: false},
expectedResult: true,
},
{
name: "Route with an excluded chain ID",
route: []*PathV2{path1, path3},
route: []*PathV2{pathC1A1, pathC3A3},
fromIncluded: map[uint64]bool{1: false, 2: false},
fromExcluded: map[uint64]bool{3: false, 4: false},
expectedResult: false,
},
{
name: "Route missing one included chain ID",
route: []*PathV2{path1},
route: []*PathV2{pathC1A1},
fromIncluded: map[uint64]bool{1: false, 2: false},
fromExcluded: map[uint64]bool{},
expectedResult: false,
},
{
name: "Route with no fromIncluded or fromExcluded",
route: []*PathV2{path1, path2},
route: []*PathV2{pathC1A1, pathC2A2},
fromIncluded: map[uint64]bool{},
fromExcluded: map[uint64]bool{},
expectedResult: true,
@ -289,7 +298,7 @@ func TestHasSufficientCapacityV2(t *testing.T) {
}{
{
name: "All paths meet required amount",
route: []*PathV2{path1, path2, path3},
route: []*PathV2{pathC1A1, pathC2A2, pathC3A3},
amountIn: big.NewInt(600),
fromLockedAmount: map[uint64]*hexutil.Big{1: &amount1, 2: &amount2, 3: &amount3},
expected: true,
@ -299,7 +308,7 @@ func TestHasSufficientCapacityV2(t *testing.T) {
/*
{
name: "A path does not meet required amount",
route: []*PathV2{path1, path2, path3},
route: []*PathV2{pathC1A1, pathC2A2, pathC3A3},
amountIn: big.NewInt(600),
fromLockedAmount: map[uint64]*hexutil.Big{1: &amount1, 2: &amount2, 4: &amount4},
expected: false,
@ -307,35 +316,35 @@ func TestHasSufficientCapacityV2(t *testing.T) {
*/
{
name: "No fromLockedAmount",
route: []*PathV2{path1, path2, path3},
route: []*PathV2{pathC1A1, pathC2A2, pathC3A3},
amountIn: big.NewInt(600),
fromLockedAmount: map[uint64]*hexutil.Big{},
expected: true,
},
{
name: "Single path meets required amount",
route: []*PathV2{path1},
route: []*PathV2{pathC1A1},
amountIn: big.NewInt(100),
fromLockedAmount: map[uint64]*hexutil.Big{1: &amount1},
expected: true,
},
{
name: "Single path does not meet required amount",
route: []*PathV2{path1},
route: []*PathV2{pathC1A1},
amountIn: big.NewInt(200),
fromLockedAmount: map[uint64]*hexutil.Big{1: &amount1},
expected: false,
},
{
name: "Path meets required amount with excess",
route: []*PathV2{path1, path2},
route: []*PathV2{pathC1A1, pathC2A2},
amountIn: big.NewInt(250),
fromLockedAmount: map[uint64]*hexutil.Big{1: &amount1, 2: &amount2},
expected: true,
},
{
name: "Path does not meet required amount due to insufficient rest",
route: []*PathV2{path1, path2, path4},
route: []*PathV2{pathC1A1, pathC2A2, pathC4A4},
amountIn: big.NewInt(800),
fromLockedAmount: map[uint64]*hexutil.Big{1: &amount1, 4: &amount4},
expected: false,
@ -596,40 +605,40 @@ func TestFilterNetworkComplianceV2(t *testing.T) {
{
name: "Routes without excluded chain IDs, missing included path",
routes: [][]*PathV2{
{path1, path2},
{path2, path3},
{pathC1A1, pathC2A2},
{pathC2A2, pathC3A3},
},
fromLockedAmount: map[uint64]*hexutil.Big{1: &amount1, 2: &amount2},
expected: [][]*PathV2{
{path1, path2},
{pathC1A1, pathC2A2},
},
},
{
name: "Routes with an excluded chain ID",
routes: [][]*PathV2{
{path1, path2},
{path2, path3, path0},
{pathC1A1, pathC2A2},
{pathC2A2, pathC3A3, path0},
},
fromLockedAmount: map[uint64]*hexutil.Big{1: &amount1, 2: &amount2, 4: &amount0},
expected: [][]*PathV2{
{path1, path2},
{pathC1A1, pathC2A2},
},
},
{
name: "Routes with all included chain IDs",
routes: [][]*PathV2{
{path1, path2, path3},
{pathC1A1, pathC2A2, pathC3A3},
},
fromLockedAmount: map[uint64]*hexutil.Big{1: &amount1, 2: &amount2, 3: &amount3},
expected: [][]*PathV2{
{path1, path2, path3},
{pathC1A1, pathC2A2, pathC3A3},
},
},
{
name: "Routes missing one included chain ID",
routes: [][]*PathV2{
{path1, path2},
{path1},
{pathC1A1, pathC2A2},
{pathC1A1},
},
fromLockedAmount: map[uint64]*hexutil.Big{1: &amount1, 2: &amount2, 3: &amount3},
expected: [][]*PathV2{},
@ -637,32 +646,32 @@ func TestFilterNetworkComplianceV2(t *testing.T) {
{
name: "Routes with no fromLockedAmount",
routes: [][]*PathV2{
{path1, path2},
{path2, path3},
{pathC1A1, pathC2A2},
{pathC2A2, pathC3A3},
},
fromLockedAmount: map[uint64]*hexutil.Big{},
expected: [][]*PathV2{
{path1, path2},
{path2, path3},
{pathC1A1, pathC2A2},
{pathC2A2, pathC3A3},
},
},
{
name: "Routes with fromExcluded only",
routes: [][]*PathV2{
{path1, path2},
{path2, path3},
{pathC1A1, pathC2A2},
{pathC2A2, pathC3A3},
},
fromLockedAmount: map[uint64]*hexutil.Big{4: &amount0},
expected: [][]*PathV2{
{path1, path2},
{path2, path3},
{pathC1A1, pathC2A2},
{pathC2A2, pathC3A3},
},
},
{
name: "Routes with all excluded chain IDs",
routes: [][]*PathV2{
{path0, path1},
{path0, path2},
{path0, pathC1A1},
{path0, pathC2A2},
},
fromLockedAmount: map[uint64]*hexutil.Big{1: &amount1, 2: &amount2, 3: &amount3, 4: &amount0},
expected: [][]*PathV2{},
@ -691,27 +700,32 @@ func TestFilterCapacityValidationV2(t *testing.T) {
name: "Sufficient capacity with multiple paths",
routes: [][]*PathV2{
{
{FromChain: network1, AmountIn: (*hexutil.Big)(big.NewInt(100))},
{FromChain: network2, AmountIn: (*hexutil.Big)(big.NewInt(200))},
{FromChain: network1, AmountIn: (*hexutil.Big)(big.NewInt(50))},
{FromChain: network2, AmountIn: (*hexutil.Big)(big.NewInt(100))},
{FromChain: network3, AmountIn: (*hexutil.Big)(big.NewInt(100))},
},
{
{FromChain: network1, AmountIn: (*hexutil.Big)(big.NewInt(50))},
{FromChain: network2, AmountIn: (*hexutil.Big)(big.NewInt(100))},
{FromChain: network2, AmountIn: (*hexutil.Big)(big.NewInt(200))},
},
{
{FromChain: network1, AmountIn: (*hexutil.Big)(big.NewInt(100))},
{FromChain: network2, AmountIn: (*hexutil.Big)(big.NewInt(200))},
},
},
amountIn: big.NewInt(150),
amountIn: big.NewInt(250),
fromLockedAmount: map[uint64]*hexutil.Big{
1: (*hexutil.Big)(big.NewInt(50)),
2: (*hexutil.Big)(big.NewInt(100)),
},
expectedRoutes: [][]*PathV2{
{
{FromChain: network1, AmountIn: (*hexutil.Big)(big.NewInt(50)), AmountInLocked: true},
{FromChain: network2, AmountIn: (*hexutil.Big)(big.NewInt(100)), AmountInLocked: true},
{FromChain: network1, AmountIn: (*hexutil.Big)(big.NewInt(50))},
{FromChain: network2, AmountIn: (*hexutil.Big)(big.NewInt(100))},
{FromChain: network3, AmountIn: (*hexutil.Big)(big.NewInt(100))},
},
{
{FromChain: network1, AmountIn: (*hexutil.Big)(big.NewInt(50)), AmountInLocked: true},
{FromChain: network2, AmountIn: (*hexutil.Big)(big.NewInt(100)), AmountInLocked: true},
{FromChain: network1, AmountIn: (*hexutil.Big)(big.NewInt(50))},
{FromChain: network2, AmountIn: (*hexutil.Big)(big.NewInt(200))},
},
},
},
@ -745,8 +759,8 @@ func TestFilterCapacityValidationV2(t *testing.T) {
},
expectedRoutes: [][]*PathV2{
{
{FromChain: network1, AmountIn: (*hexutil.Big)(big.NewInt(100)), AmountInLocked: true},
{FromChain: network2, AmountIn: (*hexutil.Big)(big.NewInt(50)), AmountInLocked: true},
{FromChain: network1, AmountIn: (*hexutil.Big)(big.NewInt(100))},
{FromChain: network2, AmountIn: (*hexutil.Big)(big.NewInt(50))},
},
},
},
@ -762,17 +776,35 @@ func TestFilterCapacityValidationV2(t *testing.T) {
fromLockedAmount: map[uint64]*hexutil.Big{},
expectedRoutes: [][]*PathV2{
{
{FromChain: network1, AmountIn: (*hexutil.Big)(big.NewInt(100)), AmountInLocked: false},
{FromChain: network2, AmountIn: (*hexutil.Big)(big.NewInt(50)), AmountInLocked: false},
{FromChain: network1, AmountIn: (*hexutil.Big)(big.NewInt(100))},
{FromChain: network2, AmountIn: (*hexutil.Big)(big.NewInt(50))},
},
},
},
{
// TODO Is the behaviour of this test correct? It looks wrong
name: "Single route with sufficient capacity",
routes: [][]*PathV2{
{
{FromChain: network1, AmountIn: (*hexutil.Big)(big.NewInt(200))},
{FromChain: network1, AmountIn: (*hexutil.Big)(big.NewInt(50))},
{FromChain: network2, AmountIn: (*hexutil.Big)(big.NewInt(100))},
},
},
amountIn: big.NewInt(150),
fromLockedAmount: map[uint64]*hexutil.Big{
1: (*hexutil.Big)(big.NewInt(50)),
},
expectedRoutes: [][]*PathV2{
{
{FromChain: network1, AmountIn: (*hexutil.Big)(big.NewInt(50))},
{FromChain: network2, AmountIn: (*hexutil.Big)(big.NewInt(100))},
},
},
},
{
name: "Single route with inappropriately locked amount",
routes: [][]*PathV2{
{
{FromChain: network1, AmountIn: (*hexutil.Big)(big.NewInt(100))},
},
},
amountIn: big.NewInt(150),
@ -785,7 +817,7 @@ func TestFilterCapacityValidationV2(t *testing.T) {
name: "Single route with insufficient capacity",
routes: [][]*PathV2{
{
{FromChain: network1, AmountIn: (*hexutil.Big)(big.NewInt(100))},
{FromChain: network1, AmountIn: (*hexutil.Big)(big.NewInt(50))},
},
},
amountIn: big.NewInt(150),
@ -803,28 +835,13 @@ func TestFilterCapacityValidationV2(t *testing.T) {
},
expectedRoutes: [][]*PathV2{},
},
{
// TODO this seems wrong also. Should this test case work?
name: "Routes with duplicate chain IDs",
routes: [][]*PathV2{
{
{FromChain: network1, AmountIn: (*hexutil.Big)(big.NewInt(100))},
{FromChain: network1, AmountIn: (*hexutil.Big)(big.NewInt(100))},
},
},
amountIn: big.NewInt(150),
fromLockedAmount: map[uint64]*hexutil.Big{
1: (*hexutil.Big)(big.NewInt(50)),
},
expectedRoutes: [][]*PathV2{},
},
{
name: "Partial locked amounts",
routes: [][]*PathV2{
{
{FromChain: network1, AmountIn: (*hexutil.Big)(big.NewInt(100))},
{FromChain: network2, AmountIn: (*hexutil.Big)(big.NewInt(100))},
{FromChain: network3, AmountIn: (*hexutil.Big)(big.NewInt(200))},
{FromChain: network1, AmountIn: (*hexutil.Big)(big.NewInt(50))},
{FromChain: network3, AmountIn: (*hexutil.Big)(big.NewInt(100))},
{FromChain: network4, AmountIn: (*hexutil.Big)(big.NewInt(100))},
},
},
amountIn: big.NewInt(250),
@ -833,7 +850,13 @@ func TestFilterCapacityValidationV2(t *testing.T) {
2: (*hexutil.Big)(big.NewInt(0)), // Excluded path
3: (*hexutil.Big)(big.NewInt(100)),
},
expectedRoutes: [][]*PathV2{},
expectedRoutes: [][]*PathV2{
{
{FromChain: network1, AmountIn: (*hexutil.Big)(big.NewInt(50))},
{FromChain: network3, AmountIn: (*hexutil.Big)(big.NewInt(100))},
{FromChain: network4, AmountIn: (*hexutil.Big)(big.NewInt(100))},
},
},
},
{
name: "Mixed networks with sufficient capacity",
@ -843,15 +866,15 @@ func TestFilterCapacityValidationV2(t *testing.T) {
{FromChain: network3, AmountIn: (*hexutil.Big)(big.NewInt(200))},
},
},
amountIn: big.NewInt(250),
amountIn: big.NewInt(300),
fromLockedAmount: map[uint64]*hexutil.Big{
1: (*hexutil.Big)(big.NewInt(100)),
3: (*hexutil.Big)(big.NewInt(200)),
},
expectedRoutes: [][]*PathV2{
{
{FromChain: network1, AmountIn: (*hexutil.Big)(big.NewInt(100)), AmountInLocked: true},
{FromChain: network3, AmountIn: (*hexutil.Big)(big.NewInt(200)), AmountInLocked: true},
{FromChain: network1, AmountIn: (*hexutil.Big)(big.NewInt(100))},
{FromChain: network3, AmountIn: (*hexutil.Big)(big.NewInt(200))},
},
},
},
@ -891,23 +914,46 @@ func TestFilterRoutesV2(t *testing.T) {
expectedRoutes [][]*PathV2
}{
{
name: "Empty fromLockedAmount",
name: "Empty fromLockedAmount and routes don't match amountIn",
routes: [][]*PathV2{
{path1, path2},
{path3, path4},
{pathC1A1, pathC2A2},
{pathC3A3, pathC4A4},
},
amountIn: big.NewInt(150),
fromLockedAmount: map[uint64]*hexutil.Big{},
expectedRoutes: [][]*PathV2{},
},
{
name: "Empty fromLockedAmount and sigle route match amountIn",
routes: [][]*PathV2{
{pathC1A1, pathC2A2},
{pathC3A3, pathC4A4},
},
amountIn: big.NewInt(300),
fromLockedAmount: map[uint64]*hexutil.Big{},
expectedRoutes: [][]*PathV2{
{path1, path2},
{path3, path4},
{pathC1A1, pathC2A2},
},
},
{
name: "Empty fromLockedAmount and more routes match amountIn",
routes: [][]*PathV2{
{pathC1A1, pathC2A2},
{pathC3A3, pathC4A4},
{pathC1A1, pathC2A1, pathC3A1},
},
amountIn: big.NewInt(300),
fromLockedAmount: map[uint64]*hexutil.Big{},
expectedRoutes: [][]*PathV2{
{pathC1A1, pathC2A2},
{pathC1A1, pathC2A1, pathC3A1},
},
},
{
name: "All paths appear in fromLockedAmount but not within a single route",
routes: [][]*PathV2{
{path1, path3},
{path2, path4},
{pathC1A1, pathC3A3},
{pathC2A2, pathC4A4},
},
amountIn: big.NewInt(500),
fromLockedAmount: map[uint64]*hexutil.Big{
@ -919,11 +965,12 @@ func TestFilterRoutesV2(t *testing.T) {
expectedRoutes: [][]*PathV2{},
},
{
name: "Mixed valid and invalid routes",
name: "Mixed valid and invalid routes I",
routes: [][]*PathV2{
{path1, path2},
{path2, path3},
{path1, path4},
{pathC1A1, pathC2A2},
{pathC2A2, pathC3A3},
{pathC1A1, pathC4A4},
{pathC1A1, pathC2A1, pathC3A1},
},
amountIn: big.NewInt(300),
fromLockedAmount: map[uint64]*hexutil.Big{
@ -931,14 +978,31 @@ func TestFilterRoutesV2(t *testing.T) {
2: &amount2,
},
expectedRoutes: [][]*PathV2{
{path1, path2},
{pathC1A1, pathC2A2},
},
},
{
name: "Mixed valid and invalid routes II",
routes: [][]*PathV2{
{pathC1A1, pathC2A2},
{pathC2A2, pathC3A3},
{pathC1A1, pathC4A4},
{pathC1A1, pathC2A1, pathC3A1},
},
amountIn: big.NewInt(300),
fromLockedAmount: map[uint64]*hexutil.Big{
1: &amount1,
},
expectedRoutes: [][]*PathV2{
{pathC1A1, pathC2A2},
{pathC1A1, pathC2A1, pathC3A1},
},
},
{
name: "All invalid routes",
routes: [][]*PathV2{
{path2, path3},
{path4, path5},
{pathC2A2, pathC3A3},
{pathC4A4, pathC5A5},
},
amountIn: big.NewInt(300),
fromLockedAmount: map[uint64]*hexutil.Big{
@ -949,22 +1013,22 @@ func TestFilterRoutesV2(t *testing.T) {
{
name: "Single valid route",
routes: [][]*PathV2{
{path1, path3},
{path2, path3},
{pathC1A1, pathC3A3},
{pathC2A2, pathC3A3},
},
amountIn: big.NewInt(150),
amountIn: big.NewInt(400),
fromLockedAmount: map[uint64]*hexutil.Big{
1: &amount1,
3: &amount3,
},
expectedRoutes: [][]*PathV2{
{path1, path3},
{pathC1A1, pathC3A3},
},
},
{
name: "Route with mixed valid and invalid paths",
name: "Route with mixed valid and invalid paths I",
routes: [][]*PathV2{
{path1, path2, path3},
{pathC1A1, pathC2A2, pathC3A3},
},
amountIn: big.NewInt(300),
fromLockedAmount: map[uint64]*hexutil.Big{
@ -973,6 +1037,36 @@ func TestFilterRoutesV2(t *testing.T) {
},
expectedRoutes: [][]*PathV2{},
},
{
name: "Route with mixed valid and invalid paths II",
routes: [][]*PathV2{
{pathC1A1, pathC3A3},
},
amountIn: big.NewInt(400),
fromLockedAmount: map[uint64]*hexutil.Big{
1: &amount1,
2: &amount0, // This path should be filtered out due to being excluded via a zero amount, 0 value locked means this chain is disabled
},
expectedRoutes: [][]*PathV2{
{pathC1A1, pathC3A3},
},
},
{
name: "Route with mixed valid and invalid paths III",
routes: [][]*PathV2{
{pathC1A1, pathC3A3},
{pathC1A1, pathC3A2, pathC4A1},
},
amountIn: big.NewInt(400),
fromLockedAmount: map[uint64]*hexutil.Big{
1: &amount1,
2: &amount0, // This path should be filtered out due to being excluded via a zero amount, 0 value locked means this chain is disabled
},
expectedRoutes: [][]*PathV2{
{pathC1A1, pathC3A3},
{pathC1A1, pathC3A2, pathC4A1},
},
},
}
for _, tt := range tests {

View File

@ -305,9 +305,9 @@ func (h *HopBridgeProcessor) GetContractAddress(params ProcessorInputParams) (co
}
func (h *HopBridgeProcessor) sendOrBuild(sendArgs *MultipathProcessorTxArgs, signerFn bind.SignerFn) (tx *ethTypes.Transaction, err error) {
fromChain := h.networkManager.Find(sendArgs.ChainID)
fromChain := h.networkManager.Find(sendArgs.HopTx.ChainID)
if fromChain == nil {
return tx, fmt.Errorf("ChainID not supported %d", sendArgs.ChainID)
return tx, fmt.Errorf("ChainID not supported %d", sendArgs.HopTx.ChainID)
}
token := h.tokenManager.FindToken(fromChain, sendArgs.HopTx.Symbol)
@ -344,22 +344,22 @@ func (h *HopBridgeProcessor) sendOrBuild(sendArgs *MultipathProcessorTxArgs, sig
switch contractType {
case hop.CctpL1Bridge:
tx, err = h.sendCctpL1BridgeTx(contractAddress, ethClient, sendArgs.HopTx.ChainID, sendArgs.HopTx.Recipient, txOpts, bonderFee)
tx, err = h.sendCctpL1BridgeTx(contractAddress, ethClient, sendArgs.HopTx.ChainIDTo, sendArgs.HopTx.Recipient, txOpts, bonderFee)
case hop.L1Bridge:
tx, err = h.sendL1BridgeTx(contractAddress, ethClient, sendArgs.HopTx.ChainID, sendArgs.HopTx.Recipient, txOpts, token, bonderFee)
tx, err = h.sendL1BridgeTx(contractAddress, ethClient, sendArgs.HopTx.ChainIDTo, sendArgs.HopTx.Recipient, txOpts, token, bonderFee)
case hop.L2AmmWrapper:
tx, err = h.sendL2AmmWrapperTx(contractAddress, ethClient, sendArgs.HopTx.ChainID, sendArgs.HopTx.Recipient, txOpts, bonderFee)
tx, err = h.sendL2AmmWrapperTx(contractAddress, ethClient, sendArgs.HopTx.ChainIDTo, sendArgs.HopTx.Recipient, txOpts, bonderFee)
case hop.CctpL2Bridge:
tx, err = h.sendCctpL2BridgeTx(contractAddress, ethClient, sendArgs.HopTx.ChainID, sendArgs.HopTx.Recipient, txOpts, bonderFee)
tx, err = h.sendCctpL2BridgeTx(contractAddress, ethClient, sendArgs.HopTx.ChainIDTo, sendArgs.HopTx.Recipient, txOpts, bonderFee)
case hop.L2Bridge:
tx, err = h.sendL2BridgeTx(contractAddress, ethClient, sendArgs.HopTx.ChainID, sendArgs.HopTx.Recipient, txOpts, bonderFee)
tx, err = h.sendL2BridgeTx(contractAddress, ethClient, sendArgs.HopTx.ChainIDTo, sendArgs.HopTx.Recipient, txOpts, bonderFee)
default:
return tx, ErrContractTypeNotSupported
}
if err != nil {
return tx, createBridgeHopErrorResponse(err)
}
err = h.transactor.StoreAndTrackPendingTx(txOpts.From, sendArgs.HopTx.Symbol, sendArgs.ChainID, sendArgs.HopTx.MultiTransactionID, tx)
err = h.transactor.StoreAndTrackPendingTx(txOpts.From, sendArgs.HopTx.Symbol, sendArgs.HopTx.ChainID, sendArgs.HopTx.MultiTransactionID, tx)
if err != nil {
return tx, createBridgeHopErrorResponse(err)
}
@ -367,7 +367,7 @@ func (h *HopBridgeProcessor) sendOrBuild(sendArgs *MultipathProcessorTxArgs, sig
}
func (h *HopBridgeProcessor) Send(sendArgs *MultipathProcessorTxArgs, verifiedAccount *account.SelectedExtKey) (hash types.Hash, err error) {
tx, err := h.sendOrBuild(sendArgs, getSigner(sendArgs.ChainID, sendArgs.HopTx.From, verifiedAccount))
tx, err := h.sendOrBuild(sendArgs, getSigner(sendArgs.HopTx.ChainID, sendArgs.HopTx.From, verifiedAccount))
if err != nil {
return types.Hash{}, createBridgeHopErrorResponse(err)
}
@ -450,7 +450,7 @@ func (h *HopBridgeProcessor) CalculateAmountOut(params ProcessorInputParams) (*b
return nil, ErrNoBonderFeeFound
}
bonderFee := bonderFeeIns.(*BonderFee)
return bonderFee.EstimatedRecieved.Int, nil
return bonderFee.AmountOutMin.Int, nil
}
func (h *HopBridgeProcessor) packCctpL1BridgeTx(abi abi.ABI, toChainID uint64, to common.Address, bonderFee *BonderFee) ([]byte, error) {

View File

@ -81,8 +81,9 @@ type routerTestParams struct {
}
type amountOption struct {
amount *big.Int
locked bool
amount *big.Int
locked bool
subtractFees bool
}
func makeBalanceKey(chainID uint64, symbol string) string {
@ -99,27 +100,35 @@ type PathV2 struct {
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
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
TxPriorityFee *hexutil.Big // Priority fee for the transaction
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
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
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
ApprovalPriorityFee *hexutil.Big // Priority fee for the approval transaction
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
ApprovalL1Fee *hexutil.Big // L1 fee for the approval transaction - used for for transactions placed on L2 chains
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
requiredNativeBalance *big.Int
requiredTokenBalance *big.Int // (in selected token)
requiredNativeBalance *big.Int // (in ETH WEI)
subtractFees bool
}
type ProcessorError struct {
@ -249,7 +258,7 @@ func (n NodeV2) buildAllRoutesV2() [][]*PathV2 {
return res
}
func findBestV2(routes [][]*PathV2, tokenPrice float64, nativeChainTokenPrice float64) []*PathV2 {
func findBestV2(routes [][]*PathV2, tokenPrice float64, nativeTokenPrice float64) []*PathV2 {
var best []*PathV2
bestCost := big.NewFloat(math.Inf(1))
for _, route := range routes {
@ -257,69 +266,39 @@ func findBestV2(routes [][]*PathV2, tokenPrice float64, nativeChainTokenPrice fl
for _, path := range route {
tokenDenominator := big.NewFloat(math.Pow(10, float64(path.FromToken.Decimals)))
path.requiredTokenBalance = big.NewInt(0)
path.requiredNativeBalance = big.NewInt(0)
if path.FromToken.IsNative() {
path.requiredNativeBalance.Add(path.requiredNativeBalance, path.AmountIn.ToInt())
} else {
path.requiredTokenBalance.Add(path.requiredTokenBalance, path.AmountIn.ToInt())
// 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))
}
// 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(pathprocessor.ZeroBigIntValue) > 0 {
if path.FromToken.IsNative() {
path.requiredNativeBalance.Add(path.requiredNativeBalance, path.TxBonderFees.ToInt())
} else {
path.requiredTokenBalance.Add(path.requiredTokenBalance, path.TxBonderFees.ToInt())
}
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.TxL1Fee != nil && path.TxL1Fee.ToInt().Cmp(pathprocessor.ZeroBigIntValue) > 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(pathprocessor.ZeroBigIntValue) > 0 && path.FromToken != nil {
if path.FromToken.IsNative() {
path.requiredNativeBalance.Add(path.requiredNativeBalance, path.TxTokenFees.ToInt())
} else {
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()), tokenDenominator),
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))
// tx approval fee
approvalFeeInEth := gweiToEth(weiToGwei(path.ApprovalFee.ToInt()))
pathCost.Add(pathCost, new(big.Float).Mul(approvalFeeInEth, nativeTokenPrice))
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))
if path.ApprovalL1Fee.ToInt().Cmp(pathprocessor.ZeroBigIntValue) > 0 {
approvalL1FeeInEth := gweiToEth(weiToGwei(path.ApprovalL1Fee.ToInt()))
pathCost.Add(pathCost, new(big.Float).Mul(approvalL1FeeInEth, nativeTokenPrice))
}
}
@ -408,9 +387,13 @@ func validateFromLockedAmount(input *RouteInputParams) error {
var suppNetworks map[uint64]bool
if input.testnetMode {
suppNetworks = copyMap(supportedTestNetworks)
suppNetworks = copyMapGeneric(supportedTestNetworks, nil).(map[uint64]bool)
} else {
suppNetworks = copyMap(supportedNetworks)
suppNetworks = copyMapGeneric(supportedNetworks, nil).(map[uint64]bool)
}
if suppNetworks == nil {
return ErrCannotCheckLockedAmounts
}
totalLockedAmount := big.NewInt(0)
@ -610,8 +593,9 @@ func (r *Router) getOptionsForAmoutToSplitAccrossChainsForProcessingChain(input
if tokenBalance.Cmp(pathprocessor.ZeroBigIntValue) > 0 {
if tokenBalance.Cmp(amountToSplit) <= 0 {
crossChainAmountOptions[chain.ChainID] = append(crossChainAmountOptions[chain.ChainID], amountOption{
amount: tokenBalance,
locked: false,
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 {
@ -927,6 +911,43 @@ func (r *Router) resolveCandidates(ctx context.Context, input *RouteInputParams,
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,
@ -938,13 +959,16 @@ func (r *Router) resolveCandidates(ctx context.Context, input *RouteInputParams,
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),
TxL1Fee: (*hexutil.Big)(big.NewInt(int64(l1FeeWei))),
TxFee: (*hexutil.Big)(txFeeInWei),
TxL1Fee: (*hexutil.Big)(txL1FeeInWei),
ApprovalRequired: approvalRequired,
ApprovalAmountRequired: (*hexutil.Big)(approvalAmountRequired),
@ -952,9 +976,17 @@ func (r *Router) resolveCandidates(ctx context.Context, input *RouteInputParams,
ApprovalBaseFee: (*hexutil.Big)(fees.BaseFee),
ApprovalPriorityFee: (*hexutil.Big)(fees.MaxPriorityFeePerGas),
ApprovalGasAmount: approvalGasLimit,
ApprovalL1Fee: (*hexutil.Big)(big.NewInt(int64(l1ApprovalFee))),
ApprovalFee: (*hexutil.Big)(approvalFeeInWei),
ApprovalL1Fee: (*hexutil.Big)(approvalL1FeeInWei),
TxTotalFee: (*hexutil.Big)(ethTotalFees),
EstimatedTime: estimatedTime,
subtractFees: amountOption.subtractFees,
requiredTokenBalance: requiredTokenBalance,
requiredNativeBalance: requiredNativeBalance,
})
}
}
@ -968,22 +1000,41 @@ func (r *Router) resolveCandidates(ctx context.Context, input *RouteInputParams,
}
func (r *Router) checkBalancesForTheBestRoute(ctx context.Context, bestRoute []*PathV2, input *RouteInputParams, balanceMap map[string]*big.Int) (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 ErrCannotCheckReceiverBalance
}
// check the best route for the required balances
for _, path := range bestRoute {
if path.requiredTokenBalance != nil && path.requiredTokenBalance.Cmp(pathprocessor.ZeroBigIntValue) > 0 {
if tokenBalance, ok := balanceMap[makeBalanceKey(path.FromChain.ChainID, path.FromToken.Symbol)]; ok {
key := makeBalanceKey(path.FromChain.ChainID, path.FromToken.Symbol)
if tokenBalance, ok := balanceMapCopy[key]; ok {
if tokenBalance.Cmp(path.requiredTokenBalance) == -1 {
return ErrNotEnoughTokenBalance
err := &errors.ErrorResponse{
Code: ErrNotEnoughTokenBalance.Code,
Details: fmt.Sprintf(ErrNotEnoughTokenBalance.Details, path.FromToken.Symbol, path.FromChain.ChainID),
}
return err
}
balanceMapCopy[key].Sub(tokenBalance, path.requiredTokenBalance)
} else {
return ErrTokenNotFound
}
}
if nativeBalance, ok := balanceMap[makeBalanceKey(path.FromChain.ChainID, pathprocessor.EthSymbol)]; ok {
key := makeBalanceKey(path.FromChain.ChainID, pathprocessor.EthSymbol)
if nativeBalance, ok := balanceMapCopy[key]; ok {
if nativeBalance.Cmp(path.requiredNativeBalance) == -1 {
return ErrNotEnoughNativeBalance
err := &errors.ErrorResponse{
Code: ErrNotEnoughNativeBalance.Code,
Details: fmt.Sprintf(ErrNotEnoughNativeBalance.Details, pathprocessor.EthSymbol, path.FromChain.ChainID),
}
return err
}
balanceMapCopy[key].Sub(nativeBalance, path.requiredNativeBalance)
} else {
return ErrNativeTokenNotFound
}
@ -993,7 +1044,8 @@ func (r *Router) checkBalancesForTheBestRoute(ctx context.Context, bestRoute []*
}
func removeBestRouteFromAllRouters(allRoutes [][]*PathV2, best []*PathV2) [][]*PathV2 {
for i, route := range allRoutes {
for i := len(allRoutes) - 1; i >= 0; i-- {
route := allRoutes[i]
routeFound := true
for _, p := range route {
found := false
@ -1031,13 +1083,13 @@ func (r *Router) resolveRoutes(ctx context.Context, input *RouteInputParams, can
}
tokenPrice := prices[input.TokenID]
nativeChainTokenPrice := prices[pathprocessor.EthSymbol]
nativeTokenPrice := prices[pathprocessor.EthSymbol]
var allRoutes [][]*PathV2
suggestedRoutes, allRoutes = newSuggestedRoutesV2(input.Uuid, input.AmountIn.ToInt(), candidates, input.FromLockedAmount, tokenPrice, nativeChainTokenPrice)
suggestedRoutes, allRoutes = newSuggestedRoutesV2(input.Uuid, input.AmountIn.ToInt(), candidates, input.FromLockedAmount, tokenPrice, nativeTokenPrice)
for len(allRoutes) > 0 {
best := findBestV2(allRoutes, tokenPrice, nativeChainTokenPrice)
best := findBestV2(allRoutes, tokenPrice, nativeTokenPrice)
err := r.checkBalancesForTheBestRoute(ctx, best, input, balanceMap)
if err != nil {
@ -1057,6 +1109,22 @@ func (r *Router) resolveRoutes(ctx context.Context, input *RouteInputParams, can
sort.Slice(best, func(i, j int) bool {
return best[i].AmountInLocked
})
// 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 best {
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 = best
break

View File

@ -1,6 +1,7 @@
package router
import (
"fmt"
"math/big"
"github.com/ethereum/go-ethereum/common"
@ -2500,6 +2501,90 @@ func getNormalTestParamsList() []normalTestParams {
expectedError: ErrNoBestRouteFound,
expectedCandidates: []*PathV2{},
},
{
name: "ETH transfer - Not Enough Native Balance",
input: &RouteInputParams{
testnetMode: false,
Uuid: uuid.NewString(),
SendType: Transfer,
AddrFrom: common.HexToAddress("0x1"),
AddrTo: common.HexToAddress("0x2"),
AmountIn: (*hexutil.Big)(big.NewInt(testAmount3ETHInWei)),
TokenID: pathprocessor.EthSymbol,
DisabledFromChainIDs: []uint64{walletCommon.OptimismMainnet, walletCommon.ArbitrumMainnet},
DisabledToChainIDs: []uint64{walletCommon.EthereumMainnet, walletCommon.ArbitrumMainnet},
testsMode: true,
testParams: &routerTestParams{
tokenFrom: &token.Token{
ChainID: 1,
Symbol: pathprocessor.EthSymbol,
Decimals: 18,
},
tokenPrices: testTokenPrices,
suggestedFees: testSuggestedFees,
balanceMap: testBalanceMapPerChain,
estimationMap: testEstimationMap,
bonderFeeMap: testBbonderFeeMap,
approvalGasEstimation: testApprovalGasEstimation,
approvalL1Fee: testApprovalL1Fee,
},
},
expectedError: &errors.ErrorResponse{
Code: ErrNotEnoughNativeBalance.Code,
Details: fmt.Sprintf(ErrNotEnoughNativeBalance.Details, pathprocessor.EthSymbol, walletCommon.EthereumMainnet),
},
expectedCandidates: []*PathV2{
{
ProcessorName: pathprocessor.ProcessorBridgeHopName,
FromChain: &mainnet,
ToChain: &optimism,
ApprovalRequired: false,
},
},
},
{
name: "ETH transfer - Not Enough Native Balance",
input: &RouteInputParams{
testnetMode: false,
Uuid: uuid.NewString(),
SendType: Transfer,
AddrFrom: common.HexToAddress("0x1"),
AddrTo: common.HexToAddress("0x2"),
AmountIn: (*hexutil.Big)(big.NewInt(5 * testAmount100USDC)),
TokenID: pathprocessor.UsdcSymbol,
DisabledFromChainIDs: []uint64{walletCommon.OptimismMainnet, walletCommon.ArbitrumMainnet},
DisabledToChainIDs: []uint64{walletCommon.EthereumMainnet, walletCommon.ArbitrumMainnet},
testsMode: true,
testParams: &routerTestParams{
tokenFrom: &token.Token{
ChainID: 1,
Symbol: pathprocessor.UsdcSymbol,
Decimals: 6,
},
tokenPrices: testTokenPrices,
suggestedFees: testSuggestedFees,
balanceMap: testBalanceMapPerChain,
estimationMap: testEstimationMap,
bonderFeeMap: testBbonderFeeMap,
approvalGasEstimation: testApprovalGasEstimation,
approvalL1Fee: testApprovalL1Fee,
},
},
expectedError: &errors.ErrorResponse{
Code: ErrNotEnoughTokenBalance.Code,
Details: fmt.Sprintf(ErrNotEnoughTokenBalance.Details, pathprocessor.UsdcSymbol, walletCommon.EthereumMainnet),
},
expectedCandidates: []*PathV2{
{
ProcessorName: pathprocessor.ProcessorBridgeHopName,
FromChain: &mainnet,
ToChain: &optimism,
ApprovalRequired: true,
},
},
},
}
}