feat_: the router - add candidates, as potential paths, by taking the max amount on enabled chains

This commit is contained in:
Sale Djenic 2024-07-04 12:48:14 +02:00 committed by saledjenic
parent 4b19845592
commit 378e5741b9
4 changed files with 1026 additions and 589 deletions

View File

@ -27,4 +27,5 @@ var (
ErrDisabledChainFoundAmongLockedNetworks = &errors.ErrorResponse{Code: errors.ErrorCode("WR-019"), Details: "disabled chain found among locked networks"}
ErrENSSetPubKeyInvalidUsername = &errors.ErrorResponse{Code: errors.ErrorCode("WR-020"), Details: "a valid username, ending in '.eth', is required for ENSSetPubKey"}
ErrLockedAmountExcludesAllSupported = &errors.ErrorResponse{Code: errors.ErrorCode("WR-021"), Details: "all supported chains are excluded, routing impossible"}
ErrTokenNotFound = &errors.ErrorResponse{Code: errors.ErrorCode("WR-022"), Details: "token not found"}
)

View File

@ -414,8 +414,8 @@ func (r *Router) requireApproval(ctx context.Context, sendType SendType, approva
return true, params.AmountIn, estimate, l1Fee, nil
}
func (r *Router) getBalance(ctx context.Context, network *params.Network, token *token.Token, account common.Address) (*big.Int, error) {
client, err := r.rpcClient.EthClient(network.ChainID)
func (r *Router) getBalance(ctx context.Context, chainID uint64, token *token.Token, account common.Address) (*big.Int, error) {
client, err := r.rpcClient.EthClient(chainID)
if err != nil {
return nil, err
}
@ -521,7 +521,7 @@ func (r *Router) SuggestedRoutes(
return err
}
} else if sendType != ERC721Transfer {
balance, err = r.getBalance(ctx, network, token, addrFrom)
balance, err = r.getBalance(ctx, network.ChainID, token, addrFrom)
if err != nil {
return err
}
@ -535,7 +535,7 @@ func (r *Router) SuggestedRoutes(
maxAmountIn = amount
}
nativeBalance, err := r.getBalance(ctx, network, nativeToken, addrFrom)
nativeBalance, err := r.getBalance(ctx, network.ChainID, nativeToken, addrFrom)
if err != nil {
return err
}

View File

@ -79,15 +79,13 @@ type routerTestParams struct {
approvalL1Fee uint64
}
func makeTestBalanceKey(chainID uint64, symbol string) string {
return fmt.Sprintf("%d-%s", chainID, symbol)
type amountOption struct {
amount *big.Int
locked bool
}
func (rt routerTestParams) getTestBalance(chainID uint64, symbol string) *big.Int {
if val, ok := rt.balanceMap[makeTestBalanceKey(chainID, symbol)]; ok {
return val
}
return big.NewInt(0)
func makeBalanceKey(chainID uint64, symbol string) string {
return fmt.Sprintf("%d-%s", chainID, symbol)
}
type PathV2 struct {
@ -487,40 +485,246 @@ func (r *Router) SuggestedRoutesV2(ctx context.Context, input *RouteInputParams)
return nil, errors.CreateErrorResponseFromError(err)
}
candidates, err := r.resolveCandidates(ctx, input)
selectedFromChains, selectedTohains, err := r.getSelectedChains(input)
if err != nil {
return nil, errors.CreateErrorResponseFromError(err)
}
return r.resolveRoutes(ctx, input, candidates)
balanceMap, err := r.getBalanceMapForTokenOnChains(ctx, input, selectedFromChains)
if err != nil {
return nil, errors.CreateErrorResponseFromError(err)
}
func (r *Router) resolveCandidates(ctx context.Context, input *RouteInputParams) (candidates []*PathV2, err error) {
candidates, err := r.resolveCandidates(ctx, input, selectedFromChains, selectedTohains, balanceMap)
if err != nil {
return nil, errors.CreateErrorResponseFromError(err)
}
return r.resolveRoutes(ctx, input, candidates, balanceMap)
}
// 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)
for _, chain := range selectedFromChains {
token := input.SendType.FindToken(r.tokenManager, r.collectiblesService, input.AddrFrom, chain, input.TokenID)
if token == nil {
continue
}
// add token balance for the chain
tokenBalance := big.NewInt(1)
if input.SendType == ERC1155Transfer {
tokenBalance, err = r.getERC1155Balance(ctx, chain, token, input.AddrFrom)
if err != nil {
return nil, errors.CreateErrorResponseFromError(err)
}
} else if input.SendType != ERC721Transfer {
tokenBalance, err = r.getBalance(ctx, chain.ChainID, token, input.AddrFrom)
if err != nil {
return nil, errors.CreateErrorResponseFromError(err)
}
}
balanceMap[makeBalanceKey(chain.ChainID, token.Symbol)] = tokenBalance
// add native token balance for the chain
nativeToken := r.tokenManager.FindToken(chain, chain.NativeCurrencySymbol)
if nativeToken == nil {
return nil, ErrNativeTokenNotFound
}
nativeBalance, err := r.getBalance(ctx, chain.ChainID, nativeToken, input.AddrFrom)
if err != nil {
return nil, errors.CreateErrorResponseFromError(err)
}
balanceMap[makeBalanceKey(chain.ChainID, nativeToken.Symbol)] = nativeBalance
}
return
}
func (r *Router) getSelectedUnlockedChains(input *RouteInputParams, processingChain *params.Network, selectedFromChains []*params.Network) []*params.Network {
selectedButNotLockedChains := []*params.Network{processingChain} // always add the processing chain at the beginning
for _, net := range selectedFromChains {
if net.ChainID == processingChain.ChainID {
continue
}
if _, ok := input.FromLockedAmount[net.ChainID]; !ok {
selectedButNotLockedChains = append(selectedButNotLockedChains, net)
}
}
return selectedButNotLockedChains
}
func (r *Router) getOptionsForAmoutToSplitAccrossChainsForProcessingChain(input *RouteInputParams, amountToSplit *big.Int, processingChain *params.Network,
selectedFromChains []*params.Network, balanceMap map[string]*big.Int) map[uint64][]amountOption {
selectedButNotLockedChains := r.getSelectedUnlockedChains(input, processingChain, selectedFromChains)
crossChainAmountOptions := make(map[uint64][]amountOption)
for _, chain := range selectedButNotLockedChains {
var (
testsMode = input.testsMode && input.testParams != nil
networks []*params.Network
ok bool
tokenBalance *big.Int
)
if tokenBalance, ok = balanceMap[makeBalanceKey(chain.ChainID, input.TokenID)]; !ok {
continue
}
if tokenBalance.Cmp(pathprocessor.ZeroBigIntValue) > 0 {
if tokenBalance.Cmp(amountToSplit) <= 0 {
crossChainAmountOptions[chain.ChainID] = append(crossChainAmountOptions[chain.ChainID], amountOption{
amount: tokenBalance,
locked: false,
})
amountToSplit = new(big.Int).Sub(amountToSplit, tokenBalance)
} else if amountToSplit.Cmp(pathprocessor.ZeroBigIntValue) > 0 {
crossChainAmountOptions[chain.ChainID] = append(crossChainAmountOptions[chain.ChainID], amountOption{
amount: amountToSplit,
locked: false,
})
// break since amountToSplit is fully addressed and the rest is 0
break
}
}
}
return crossChainAmountOptions
}
func (r *Router) getCrossChainsOptionsForSendingAmount(input *RouteInputParams, selectedFromChains []*params.Network,
balanceMap map[string]*big.Int) map[uint64][]amountOption {
// All we do in this block we're free to do, because of the validateInputData function which checks if the locked amount
// was properly set and if there is something unexpected it will return an error and we will not reach this point
finalCrossChainAmountOptions := make(map[uint64][]amountOption) // represents all possible amounts that can be sent from the "from" chain
for _, selectedFromChain := range selectedFromChains {
amountLocked := false
amountToSend := input.AmountIn.ToInt()
lockedAmount, fromChainLocked := input.FromLockedAmount[selectedFromChain.ChainID]
if fromChainLocked {
amountToSend = lockedAmount.ToInt()
amountLocked = true
} else if len(input.FromLockedAmount) > 0 {
for chainID, lockedAmount := range input.FromLockedAmount {
if chainID == selectedFromChain.ChainID {
continue
}
amountToSend = new(big.Int).Sub(amountToSend, lockedAmount.ToInt())
}
}
if amountToSend.Cmp(pathprocessor.ZeroBigIntValue) > 0 {
// add full amount always, cause we want to check for balance errors at the end of the routing algorithm
// TODO: once we introduce bettwer error handling and start checking for the balance at the beginning of the routing algorithm
// we can remove this line and optimize the routing algorithm more
finalCrossChainAmountOptions[selectedFromChain.ChainID] = append(finalCrossChainAmountOptions[selectedFromChain.ChainID], amountOption{
amount: amountToSend,
locked: amountLocked,
})
if amountLocked {
continue
}
// If the amount that need to be send is bigger than the balance on the chain, then we want to check options if that
// amount can be splitted and sent across multiple chains.
if input.SendType == Transfer && len(selectedFromChains) > 1 {
// All we do in this block we're free to do, because of the validateInputData function which checks if the locked amount
// was properly set and if there is something unexpected it will return an error and we will not reach this point
amountToSplitAccrossChains := new(big.Int).Set(amountToSend)
crossChainAmountOptions := r.getOptionsForAmoutToSplitAccrossChainsForProcessingChain(input, amountToSend, selectedFromChain, selectedFromChains, balanceMap)
// sum up all the allocated amounts accorss all chains
allocatedAmount := big.NewInt(0)
for _, amountOptions := range crossChainAmountOptions {
for _, amountOption := range amountOptions {
allocatedAmount = new(big.Int).Add(allocatedAmount, amountOption.amount)
}
}
// if the allocated amount is the same as the amount that need to be sent, then we can add the options to the finalCrossChainAmountOptions
if allocatedAmount.Cmp(amountToSplitAccrossChains) == 0 {
for cID, amountOptions := range crossChainAmountOptions {
finalCrossChainAmountOptions[cID] = append(finalCrossChainAmountOptions[cID], amountOptions...)
}
}
}
}
}
return finalCrossChainAmountOptions
}
func (r *Router) findOptionsForSendingAmount(input *RouteInputParams, selectedFromChains []*params.Network,
balanceMap map[string]*big.Int) (map[uint64][]amountOption, error) {
crossChainAmountOptions := r.getCrossChainsOptionsForSendingAmount(input, selectedFromChains, balanceMap)
// filter out duplicates values for the same chain
for chainID, amountOptions := range crossChainAmountOptions {
uniqueAmountOptions := make(map[string]amountOption)
for _, amountOption := range amountOptions {
uniqueAmountOptions[amountOption.amount.String()] = amountOption
}
crossChainAmountOptions[chainID] = make([]amountOption, 0)
for _, amountOption := range uniqueAmountOptions {
crossChainAmountOptions[chainID] = append(crossChainAmountOptions[chainID], amountOption)
}
}
return crossChainAmountOptions, nil
}
func (r *Router) getSelectedChains(input *RouteInputParams) (selectedFromChains []*params.Network, selectedTohains []*params.Network, err error) {
var networks []*params.Network
networks, err = r.rpcClient.NetworkManager.Get(false)
if err != nil {
return nil, errors.CreateErrorResponseFromError(err)
return nil, nil, errors.CreateErrorResponseFromError(err)
}
var (
group = async.NewAtomicGroup(ctx)
mu sync.Mutex
)
for networkIdx := range networks {
network := networks[networkIdx]
for _, network := range networks {
if network.IsTest != input.testnetMode {
continue
}
if containsNetworkChainID(network.ChainID, input.DisabledFromChainIDs) {
continue
if !containsNetworkChainID(network.ChainID, input.DisabledFromChainIDs) {
selectedFromChains = append(selectedFromChains, network)
}
if !containsNetworkChainID(network.ChainID, input.DisabledToChainIDs) {
selectedTohains = append(selectedTohains, network)
}
}
return selectedFromChains, selectedTohains, nil
}
func (r *Router) resolveCandidates(ctx context.Context, input *RouteInputParams, selectedFromChains []*params.Network,
selectedTohains []*params.Network, balanceMap map[string]*big.Int) (candidates []*PathV2, err error) {
var (
testsMode = input.testsMode && input.testParams != nil
group = async.NewAtomicGroup(ctx)
mu sync.Mutex
)
crossChainAmountOptions, err := r.findOptionsForSendingAmount(input, selectedFromChains, balanceMap)
if err != nil {
return nil, errors.CreateErrorResponseFromError(err)
}
for networkIdx := range selectedFromChains {
network := selectedFromChains[networkIdx]
if !input.SendType.isAvailableFor(network) {
continue
}
@ -543,20 +747,6 @@ func (r *Router) resolveCandidates(ctx context.Context, input *RouteInputParams)
toToken = input.SendType.FindToken(r.tokenManager, r.collectiblesService, common.Address{}, network, input.ToTokenID)
}
amountLocked := false
amountToSend := input.AmountIn.ToInt()
if lockedAmount, ok := input.FromLockedAmount[network.ChainID]; ok {
amountToSend = lockedAmount.ToInt()
amountLocked = true
} else if len(input.FromLockedAmount) > 0 {
for chainID, lockedAmount := range input.FromLockedAmount {
if chainID == network.ChainID {
continue
}
amountToSend = new(big.Int).Sub(amountToSend, lockedAmount.ToInt())
}
}
var fees *SuggestedFees
if testsMode {
fees = input.testParams.suggestedFees
@ -568,6 +758,7 @@ func (r *Router) resolveCandidates(ctx context.Context, input *RouteInputParams)
}
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.
@ -589,10 +780,7 @@ func (r *Router) resolveCandidates(ctx context.Context, input *RouteInputParams)
continue
}
for _, dest := range networks {
if dest.IsTest != input.testnetMode {
continue
}
for _, dest := range selectedTohains {
if !input.SendType.isAvailableFor(network) {
continue
@ -602,10 +790,6 @@ func (r *Router) resolveCandidates(ctx context.Context, input *RouteInputParams)
continue
}
if containsNetworkChainID(dest.ChainID, input.DisabledToChainIDs) {
continue
}
processorInputParams := pathprocessor.ProcessorInputParams{
FromChain: network,
ToChain: dest,
@ -613,7 +797,7 @@ func (r *Router) resolveCandidates(ctx context.Context, input *RouteInputParams)
ToToken: toToken,
ToAddr: input.AddrTo,
FromAddr: input.AddrFrom,
AmountIn: amountToSend,
AmountIn: amountOption.amount,
AmountOut: input.AmountOut.ToInt(),
Username: input.Username,
@ -682,8 +866,8 @@ func (r *Router) resolveCandidates(ctx context.Context, input *RouteInputParams)
ToChain: dest,
FromToken: token,
ToToken: toToken,
AmountIn: (*hexutil.Big)(amountToSend),
AmountInLocked: amountLocked,
AmountIn: (*hexutil.Big)(amountOption.amount),
AmountInLocked: amountOption.locked,
AmountOut: (*hexutil.Big)(amountOut),
SuggestedLevelsForMaxFeesPerGas: fees.MaxFeesLevels,
@ -708,6 +892,7 @@ func (r *Router) resolveCandidates(ctx context.Context, input *RouteInputParams)
mu.Unlock()
}
}
}
return nil
})
}
@ -716,50 +901,26 @@ func (r *Router) resolveCandidates(ctx context.Context, input *RouteInputParams)
return candidates, nil
}
func (r *Router) checkBalancesForTheBestRoute(ctx context.Context, bestRoute []*PathV2, input *RouteInputParams) (err error) {
func (r *Router) checkBalancesForTheBestRoute(ctx context.Context, bestRoute []*PathV2, input *RouteInputParams, balanceMap map[string]*big.Int) (err error) {
// check the best route for the required balances
for _, path := range bestRoute {
if path.requiredTokenBalance != nil && path.requiredTokenBalance.Cmp(pathprocessor.ZeroBigIntValue) > 0 {
tokenBalance := big.NewInt(1)
if input.testsMode {
tokenBalance = input.testParams.getTestBalance(path.FromChain.ChainID, path.FromToken.Symbol)
} else {
if input.SendType == ERC1155Transfer {
tokenBalance, err = r.getERC1155Balance(ctx, path.FromChain, path.FromToken, input.AddrFrom)
if err != nil {
return errors.CreateErrorResponseFromError(err)
}
} else if input.SendType != ERC721Transfer {
tokenBalance, err = r.getBalance(ctx, path.FromChain, path.FromToken, input.AddrFrom)
if err != nil {
return errors.CreateErrorResponseFromError(err)
}
}
}
if tokenBalance, ok := balanceMap[makeBalanceKey(path.FromChain.ChainID, path.FromToken.Symbol)]; ok {
if tokenBalance.Cmp(path.requiredTokenBalance) == -1 {
return ErrNotEnoughTokenBalance
}
}
var nativeBalance *big.Int
if input.testsMode {
nativeBalance = input.testParams.getTestBalance(path.FromChain.ChainID, pathprocessor.EthSymbol)
} else {
nativeToken := r.tokenManager.FindToken(path.FromChain, path.FromChain.NativeCurrencySymbol)
if nativeToken == nil {
return ErrNativeTokenNotFound
}
nativeBalance, err = r.getBalance(ctx, path.FromChain, nativeToken, input.AddrFrom)
if err != nil {
return errors.CreateErrorResponseFromError(err)
return ErrTokenNotFound
}
}
if nativeBalance, ok := balanceMap[makeBalanceKey(path.FromChain.ChainID, pathprocessor.EthSymbol)]; ok {
if nativeBalance.Cmp(path.requiredNativeBalance) == -1 {
return ErrNotEnoughNativeBalance
}
} else {
return ErrNativeTokenNotFound
}
}
return nil
@ -792,7 +953,7 @@ func removeBestRouteFromAllRouters(allRoutes [][]*PathV2, best []*PathV2) [][]*P
return nil
}
func (r *Router) resolveRoutes(ctx context.Context, input *RouteInputParams, candidates []*PathV2) (suggestedRoutes *SuggestedRoutesV2, err error) {
func (r *Router) resolveRoutes(ctx context.Context, input *RouteInputParams, candidates []*PathV2, balanceMap map[string]*big.Int) (suggestedRoutes *SuggestedRoutesV2, err error) {
var prices map[string]float64
if input.testsMode {
prices = input.testParams.tokenPrices
@ -812,7 +973,7 @@ func (r *Router) resolveRoutes(ctx context.Context, input *RouteInputParams, can
for len(allRoutes) > 0 {
best := findBestV2(allRoutes, tokenPrice, nativeChainTokenPrice)
err := r.checkBalancesForTheBestRoute(ctx, best, input)
err := r.checkBalancesForTheBestRoute(ctx, best, input, balanceMap)
if err != nil {
// If it's about transfer or bridge and there is more routes, but on the best (cheapest) one there is not enugh balance
// we shold check other routes even though there are not the cheapest ones

File diff suppressed because it is too large Load Diff