chore_: map insufficient funds error

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.
This commit is contained in:
Sale Djenic 2024-08-23 16:01:49 +02:00 committed by saledjenic
parent 85fba77b7d
commit 9b9a91f654
19 changed files with 190 additions and 88 deletions

View File

@ -27,6 +27,28 @@ func IsErrorResponse(err error) bool {
return ok
}
// ErrorCodeFromError returns the ErrorCode from an error.
func ErrorCodeFromError(err error) ErrorCode {
if err == nil {
return GenericErrorCode
}
if errResp, ok := err.(*ErrorResponse); ok {
return errResp.Code
}
return GenericErrorCode
}
// DetailsFromError returns the details from an error.
func DetailsFromError(err error) string {
if err == nil {
return ""
}
if errResp, ok := err.(*ErrorResponse); ok {
return errResp.Details
}
return err.Error()
}
// CreateErrorResponseFromError creates an ErrorResponse from a generic error.
func CreateErrorResponseFromError(err error) error {
if err == nil {

View File

@ -32,4 +32,5 @@ var (
ErrCannotCheckBalance = &errors.ErrorResponse{Code: errors.ErrorCode("WR-024"), Details: "cannot check balance"}
ErrCannotCheckLockedAmounts = &errors.ErrorResponse{Code: errors.ErrorCode("WR-025"), Details: "cannot check locked amounts"}
ErrLowAmountInForHopBridge = &errors.ErrorResponse{Code: errors.ErrorCode("WR-026"), Details: "bonder fee greater than estimated received, a higher amount is needed to cover fees"}
ErrNoPositiveBalance = &errors.ErrorResponse{Code: errors.ErrorCode("WR-027"), Details: "no positive balance"}
)

View File

@ -56,8 +56,13 @@ type ProcessorInputParams struct {
// for testing purposes
TestsMode bool
TestEstimationMap map[string]uint64 // [brifge-name, estimated-value]
TestBonderFeeMap map[string]*big.Int // [token-symbol, bonder-fee]
TestEstimationMap map[string]Estimation // [bridge-name, estimation]
TestBonderFeeMap map[string]*big.Int // [token-symbol, bonder-fee]
TestApprovalGasEstimation uint64
TestApprovalL1Fee uint64
}
type Estimation struct {
Value uint64
Err error
}

View File

@ -236,7 +236,7 @@ func (s *CelerBridgeProcessor) EstimateGas(params ProcessorInputParams) (uint64,
if params.TestsMode {
if params.TestEstimationMap != nil {
if val, ok := params.TestEstimationMap[s.Name()]; ok {
return val, nil
return val.Value, val.Err
}
}
return 0, ErrNoEstimationFound

View File

@ -228,7 +228,7 @@ func (h *HopBridgeProcessor) EstimateGas(params ProcessorInputParams) (uint64, e
if params.TestsMode {
if params.TestEstimationMap != nil {
if val, ok := params.TestEstimationMap[h.Name()]; ok {
return val, nil
return val.Value, val.Err
}
}
return 0, ErrNoEstimationFound

View File

@ -65,7 +65,7 @@ func (s *ENSPublicKeyProcessor) EstimateGas(params ProcessorInputParams) (uint64
if params.TestsMode {
if params.TestEstimationMap != nil {
if val, ok := params.TestEstimationMap[s.Name()]; ok {
return val, nil
return val.Value, val.Err
}
}
return 0, ErrNoEstimationFound

View File

@ -101,7 +101,7 @@ func (s *ENSRegisterProcessor) EstimateGas(params ProcessorInputParams) (uint64,
if params.TestsMode {
if params.TestEstimationMap != nil {
if val, ok := params.TestEstimationMap[s.Name()]; ok {
return val, nil
return val.Value, val.Err
}
}
return 0, ErrNoEstimationFound

View File

@ -64,7 +64,7 @@ func (s *ENSReleaseProcessor) EstimateGas(params ProcessorInputParams) (uint64,
if params.TestsMode {
if params.TestEstimationMap != nil {
if val, ok := params.TestEstimationMap[s.Name()]; ok {
return val, nil
return val.Value, val.Err
}
}
return 0, ErrNoEstimationFound

View File

@ -75,7 +75,7 @@ func (s *ERC1155Processor) EstimateGas(params ProcessorInputParams) (uint64, err
if params.TestsMode {
if params.TestEstimationMap != nil {
if val, ok := params.TestEstimationMap[s.Name()]; ok {
return val, nil
return val.Value, val.Err
}
}
return 0, ErrNoEstimationFound

View File

@ -112,7 +112,7 @@ func (s *ERC721Processor) EstimateGas(params ProcessorInputParams) (uint64, erro
if params.TestsMode {
if params.TestEstimationMap != nil {
if val, ok := params.TestEstimationMap[s.Name()]; ok {
return val, nil
return val.Value, val.Err
}
}
return 0, ErrNoEstimationFound

View File

@ -93,7 +93,7 @@ func (s *StickersBuyProcessor) EstimateGas(params ProcessorInputParams) (uint64,
if params.TestsMode {
if params.TestEstimationMap != nil {
if val, ok := params.TestEstimationMap[s.Name()]; ok {
return val, nil
return val.Value, val.Err
}
}
return 0, ErrNoEstimationFound

View File

@ -173,7 +173,7 @@ func (s *SwapParaswapProcessor) EstimateGas(params ProcessorInputParams) (uint64
if params.TestsMode {
if params.TestEstimationMap != nil {
if val, ok := params.TestEstimationMap[s.Name()]; ok {
return val, nil
return val.Value, val.Err
}
}
return 0, ErrNoEstimationFound

View File

@ -47,10 +47,10 @@ var optimism = params.Network{
RelatedChainID: walletCommon.OptimismMainnet,
}
var testEstimationMap = map[string]uint64{
ProcessorTransferName: uint64(1000),
ProcessorBridgeHopName: uint64(5000),
ProcessorSwapParaswapName: uint64(2000),
var testEstimationMap = map[string]Estimation{
ProcessorTransferName: {uint64(1000), nil},
ProcessorBridgeHopName: {uint64(5000), nil},
ProcessorSwapParaswapName: {uint64(2000), nil},
}
type expectedResult struct {
@ -329,17 +329,17 @@ func TestPathProcessors(t *testing.T) {
assert.Greater(t, estimatedGas, uint64(0))
input := tt.input
input.TestEstimationMap = map[string]uint64{
"randomName": 10000,
input.TestEstimationMap = map[string]Estimation{
"randomName": {10000, nil},
}
estimatedGas, err = processor.EstimateGas(input)
assert.Error(t, err)
assert.Equal(t, err, ErrNoEstimationFound)
assert.Equal(t, ErrNoEstimationFound, err)
assert.Equal(t, uint64(0), estimatedGas)
} else {
estimatedGas, err := processor.EstimateGas(tt.input)
assert.Error(t, err)
assert.Equal(t, err, ErrNoEstimationFound)
assert.Equal(t, ErrNoEstimationFound, err)
assert.Equal(t, uint64(0), estimatedGas)
}
})

View File

@ -69,7 +69,7 @@ func (s *TransferProcessor) EstimateGas(params ProcessorInputParams) (uint64, er
if params.TestsMode {
if params.TestEstimationMap != nil {
if val, ok := params.TestEstimationMap[s.Name()]; ok {
return val, nil
return val.Value, val.Err
}
}
return 0, ErrNoEstimationFound

View File

@ -308,16 +308,35 @@ func (r *Router) GetPathProcessors() map[string]pathprocessor.PathProcessor {
return r.pathProcessors
}
func containsNetworkChainID(chainID uint64, chainIDs []uint64) bool {
for _, cID := range chainIDs {
if cID == chainID {
func arrayContainsElement[T comparable](el T, arr []T) bool {
for _, e := range arr {
if e == el {
return true
}
}
return false
}
func arraysWithSameElements[T comparable](ar1 []T, ar2 []T, isEqual func(T, T) bool) bool {
if len(ar1) != len(ar2) {
return false
}
for _, el := range ar1 {
if !arrayContainsElement(el, ar2) {
return false
}
}
return true
}
func sameSingleChainTransfer(fromChains []*params.Network, toChains []*params.Network) bool {
return len(fromChains) == 1 &&
len(toChains) == 1 &&
arraysWithSameElements(fromChains, toChains, func(a, b *params.Network) bool {
return a.ChainID == b.ChainID
})
}
type Router struct {
rpcClient *rpc.Client
tokenManager *token.Manager
@ -484,7 +503,7 @@ func (r *Router) SuggestedRoutes(
continue
}
if containsNetworkChainID(network.ChainID, disabledFromChainIDs) {
if arrayContainsElement(network.ChainID, disabledFromChainIDs) {
continue
}
@ -568,10 +587,10 @@ func (r *Router) SuggestedRoutes(
continue
}
if len(preferedChainIDs) > 0 && !containsNetworkChainID(dest.ChainID, preferedChainIDs) {
if len(preferedChainIDs) > 0 && !arrayContainsElement(dest.ChainID, preferedChainIDs) {
continue
}
if containsNetworkChainID(dest.ChainID, disabledToChainIDs) {
if arrayContainsElement(dest.ChainID, disabledToChainIDs) {
continue
}

View File

@ -133,6 +133,10 @@ func (s SendType) canUseProcessor(p pathprocessor.PathProcessor) bool {
}
}
func (s SendType) simpleTransfer(p pathprocessor.PathProcessor) bool {
return s == Transfer && p.Name() == pathprocessor.ProcessorTransferName
}
func (s SendType) processZeroAmountInProcessor(amountIn *big.Int, amountOut *big.Int, processorName string) bool {
if amountIn.Cmp(pathprocessor.ZeroBigIntValue) == 0 {
if s == Transfer {

View File

@ -6,6 +6,7 @@ import (
"math"
"math/big"
"sort"
"strings"
"sync"
"github.com/ethereum/go-ethereum/common"
@ -21,6 +22,10 @@ import (
"github.com/status-im/status-go/signal"
)
const (
hexAddressLength = 42
)
var (
routerTask = async.TaskType{
ID: 1,
@ -71,8 +76,8 @@ type RouteInputParams struct {
type routerTestParams struct {
tokenFrom *walletToken.Token
tokenPrices map[string]float64
estimationMap map[string]uint64 // [processor-name, estimated-value]
bonderFeeMap map[string]*big.Int // [token-symbol, bonder-fee]
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]
@ -407,7 +412,7 @@ func validateFromLockedAmount(input *RouteInputParams) error {
excludedChainCount := 0
for chainID, amount := range input.FromLockedAmount {
if containsNetworkChainID(chainID, input.DisabledFromChainIDs) {
if arrayContainsElement(chainID, input.DisabledFromChainIDs) {
return ErrDisabledChainFoundAmongLockedNetworks
}
@ -500,8 +505,21 @@ func (r *Router) SuggestedRoutesV2(ctx context.Context, input *RouteInputParams)
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 && err != nil {
return nil, errors.CreateErrorResponseFromError(err)
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)
@ -530,7 +548,23 @@ func (r *Router) SuggestedRoutesV2(ctx context.Context, input *RouteInputParams)
}
}
return suggestedRoutes, err
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
@ -584,6 +618,10 @@ func (r *Router) getBalanceMapForTokenOnChains(ctx context.Context, input *Route
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 {
@ -756,11 +794,11 @@ func (r *Router) getSelectedChains(input *RouteInputParams) (selectedFromChains
continue
}
if !containsNetworkChainID(network.ChainID, input.DisabledFromChainIDs) {
if !arrayContainsElement(network.ChainID, input.DisabledFromChainIDs) {
selectedFromChains = append(selectedFromChains, network)
}
if !containsNetworkChainID(network.ChainID, input.DisabledToChainIDs) {
if !arrayContainsElement(network.ChainID, input.DisabledToChainIDs) {
selectedToChains = append(selectedToChains, network)
}
}
@ -781,8 +819,8 @@ func (r *Router) resolveCandidates(ctx context.Context, input *RouteInputParams,
return nil, nil, errors.CreateErrorResponseFromError(err)
}
appendProcessorErrorFn := func(processorName string, err error) {
log.Error("routerv2.resolveCandidates error", "processor", processorName, "err", 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{
@ -855,6 +893,11 @@ func (r *Router) resolveCandidates(ctx context.Context, input *RouteInputParams,
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
}
@ -893,7 +936,7 @@ func (r *Router) resolveCandidates(ctx context.Context, input *RouteInputParams,
can, err := pProcessor.AvailableFor(processorInputParams)
if err != nil {
appendProcessorErrorFn(pProcessor.Name(), err)
appendProcessorErrorFn(pProcessor.Name(), input.SendType, processorInputParams.FromChain.ChainID, processorInputParams.ToChain.ChainID, processorInputParams.AmountIn, err)
continue
}
if !can {
@ -902,24 +945,24 @@ func (r *Router) resolveCandidates(ctx context.Context, input *RouteInputParams,
bonderFees, tokenFees, err := pProcessor.CalculateFees(processorInputParams)
if err != nil {
appendProcessorErrorFn(pProcessor.Name(), err)
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(), err)
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(), err)
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(), err)
appendProcessorErrorFn(pProcessor.Name(), input.SendType, processorInputParams.FromChain.ChainID, processorInputParams.ToChain.ChainID, processorInputParams.AmountIn, err)
continue
}
@ -936,7 +979,7 @@ func (r *Router) resolveCandidates(ctx context.Context, input *RouteInputParams,
amountOut, err := pProcessor.CalculateAmountOut(processorInputParams)
if err != nil {
appendProcessorErrorFn(pProcessor.Name(), err)
appendProcessorErrorFn(pProcessor.Name(), input.SendType, processorInputParams.FromChain.ChainID, processorInputParams.ToChain.ChainID, processorInputParams.AmountIn, err)
continue
}
@ -1041,7 +1084,7 @@ func (r *Router) resolveCandidates(ctx context.Context, input *RouteInputParams,
return candidates, processorErrors, nil
}
func (r *Router) checkBalancesForTheBestRoute(ctx context.Context, bestRoute []*PathV2, input *RouteInputParams, balanceMap map[string]*big.Int) (hasPositiveBalance bool, err error) {
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)
@ -1182,7 +1225,7 @@ func (r *Router) resolveRoutes(ctx context.Context, input *RouteInputParams, can
for len(allRoutes) > 0 {
bestRoute = findBestV2(allRoutes, tokenPrice, nativeTokenPrice)
var hasPositiveBalance bool
hasPositiveBalance, err = r.checkBalancesForTheBestRoute(ctx, bestRoute, input, balanceMap)
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

View File

@ -216,8 +216,12 @@ func TestNoBalanceForTheBestRouteRouterV2(t *testing.T) {
if tt.expectedError != nil {
assert.Error(t, err)
assert.Equal(t, tt.expectedError.Error(), err.Error())
assert.NotNil(t, routes)
assertPathsEqual(t, tt.expectedCandidates, routes.Candidates)
if tt.expectedError == ErrNoPositiveBalance {
assert.Nil(t, routes)
} else {
assert.NotNil(t, routes)
assertPathsEqual(t, tt.expectedCandidates, routes.Candidates)
}
} else {
assert.NoError(t, err)
assert.Equal(t, len(tt.expectedCandidates), len(routes.Candidates))

View File

@ -45,9 +45,9 @@ const (
)
var (
testEstimationMap = map[string]uint64{
pathprocessor.ProcessorTransferName: uint64(1000),
pathprocessor.ProcessorBridgeHopName: uint64(5000),
testEstimationMap = map[string]pathprocessor.Estimation{
pathprocessor.ProcessorTransferName: {Value: uint64(1000), Err: nil},
pathprocessor.ProcessorBridgeHopName: {Value: uint64(5000), Err: nil},
}
testBbonderFeeMap = map[string]*big.Int{
@ -208,6 +208,46 @@ type normalTestParams struct {
func getNormalTestParamsList() []normalTestParams {
return []normalTestParams{
{
name: "ETH transfer - Insufficient Funds",
input: &RouteInputParams{
testnetMode: false,
Uuid: uuid.NewString(),
SendType: Transfer,
AddrFrom: common.HexToAddress("0x1"),
AddrTo: common.HexToAddress("0x2"),
AmountIn: (*hexutil.Big)(big.NewInt(testAmount1ETHInWei)),
TokenID: pathprocessor.EthSymbol,
DisabledFromChainIDs: []uint64{walletCommon.OptimismMainnet, walletCommon.ArbitrumMainnet},
DisabledToChainIDs: []uint64{walletCommon.OptimismMainnet, walletCommon.ArbitrumMainnet},
testsMode: true,
testParams: &routerTestParams{
tokenFrom: &token.Token{
ChainID: 1,
Symbol: pathprocessor.EthSymbol,
Decimals: 18,
},
tokenPrices: testTokenPrices,
suggestedFees: testSuggestedFees,
balanceMap: testBalanceMapPerChain,
estimationMap: map[string]pathprocessor.Estimation{
pathprocessor.ProcessorTransferName: {
Value: uint64(0),
Err: fmt.Errorf("failed with 50000000 gas: insufficient funds for gas * price + value: address %s have 68251537427723 want 100000000000000", common.HexToAddress("0x1")),
},
},
bonderFeeMap: testBbonderFeeMap,
approvalGasEstimation: testApprovalGasEstimation,
approvalL1Fee: testApprovalL1Fee,
},
},
expectedError: &errors.ErrorResponse{
Code: errors.GenericErrorCode,
Details: fmt.Sprintf("failed with 50000000 gas: insufficient funds for gas * price + value: address %s", common.HexToAddress("0x1")),
},
expectedCandidates: []*PathV2{},
},
{
name: "ETH transfer - No Specific FromChain - No Specific ToChain - 0 AmountIn",
input: &RouteInputParams{
@ -2669,20 +2709,7 @@ func getNoBalanceTestParamsList() []noBalanceTestParams {
approvalL1Fee: testApprovalL1Fee,
},
},
expectedError: &errors.ErrorResponse{
Code: ErrNotEnoughTokenBalance.Code,
Details: fmt.Sprintf(ErrNotEnoughTokenBalance.Details, pathprocessor.UsdcSymbol, walletCommon.OptimismMainnet),
},
expectedCandidates: []*PathV2{
{
ProcessorName: pathprocessor.ProcessorTransferName,
FromChain: &optimism,
ToChain: &optimism,
ApprovalRequired: false,
requiredTokenBalance: big.NewInt(testAmount100USDC),
requiredNativeBalance: big.NewInt((testBaseFee + testPriorityFeeLow) * testApprovalGasEstimation),
},
},
expectedError: ErrNoPositiveBalance,
},
{
name: "ERC20 transfer - Specific FromChain - Specific ToChain - Not Enough Native Balance",
@ -2766,30 +2793,7 @@ func getNoBalanceTestParamsList() []noBalanceTestParams {
approvalL1Fee: testApprovalL1Fee,
},
},
expectedError: &errors.ErrorResponse{
Code: ErrNotEnoughTokenBalance.Code,
Details: fmt.Sprintf(ErrNotEnoughTokenBalance.Details, pathprocessor.UsdcSymbol, walletCommon.ArbitrumMainnet),
},
expectedCandidates: []*PathV2{
{
ProcessorName: pathprocessor.ProcessorBridgeHopName,
FromChain: &mainnet,
ToChain: &optimism,
ApprovalRequired: true,
},
{
ProcessorName: pathprocessor.ProcessorTransferName,
FromChain: &optimism,
ToChain: &optimism,
ApprovalRequired: false,
},
{
ProcessorName: pathprocessor.ProcessorBridgeHopName,
FromChain: &arbitrum,
ToChain: &optimism,
ApprovalRequired: true,
},
},
expectedError: ErrNoPositiveBalance,
},
{
name: "ERC20 transfer - No Specific FromChain - Specific ToChain - Enough Token Balance On Arbitrum Chain But Not Enough Native Balance",