github.com/ethersphere/bee/v2@v2.2.0/pkg/settlement/swap/chequebook/init.go (about)

     1  // Copyright 2020 The Swarm Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package chequebook
     6  
     7  import (
     8  	"context"
     9  	"crypto/rand"
    10  	"errors"
    11  	"fmt"
    12  	"math/big"
    13  	"time"
    14  
    15  	"github.com/ethereum/go-ethereum/common"
    16  	chaincfg "github.com/ethersphere/bee/v2/pkg/config"
    17  	"github.com/ethersphere/bee/v2/pkg/log"
    18  	"github.com/ethersphere/bee/v2/pkg/sctx"
    19  	"github.com/ethersphere/bee/v2/pkg/settlement/swap/erc20"
    20  	"github.com/ethersphere/bee/v2/pkg/storage"
    21  	"github.com/ethersphere/bee/v2/pkg/transaction"
    22  )
    23  
    24  const (
    25  	chequebookKey           = "swap_chequebook"
    26  	ChequebookDeploymentKey = "swap_chequebook_transaction_deployment"
    27  
    28  	balanceCheckBackoffDuration = 20 * time.Second
    29  	balanceCheckMaxRetries      = 10
    30  )
    31  
    32  const (
    33  	erc20SmallUnitStr = "10000000000000000"
    34  	ethSmallUnitStr   = "1000000000000000000"
    35  )
    36  
    37  func checkBalance(
    38  	ctx context.Context,
    39  	logger log.Logger,
    40  	swapInitialDeposit *big.Int,
    41  	swapBackend transaction.Backend,
    42  	chainId int64,
    43  	overlayEthAddress common.Address,
    44  	erc20Token erc20.Service,
    45  ) error {
    46  	timeoutCtx, cancel := context.WithTimeout(ctx, balanceCheckBackoffDuration*time.Duration(balanceCheckMaxRetries))
    47  	defer cancel()
    48  	for {
    49  		erc20Balance, err := erc20Token.BalanceOf(timeoutCtx, overlayEthAddress)
    50  		if err != nil {
    51  			return err
    52  		}
    53  
    54  		ethBalance, err := swapBackend.BalanceAt(timeoutCtx, overlayEthAddress, nil)
    55  		if err != nil {
    56  			return err
    57  		}
    58  
    59  		gasPrice := sctx.GetGasPrice(ctx)
    60  		minimumEth := big.NewInt(0)
    61  
    62  		if gasPrice == nil {
    63  			gasPrice, err = swapBackend.SuggestGasPrice(timeoutCtx)
    64  			if err != nil {
    65  				return err
    66  			}
    67  
    68  			minimumEth = gasPrice.Mul(gasPrice, big.NewInt(250000))
    69  		}
    70  
    71  		insufficientERC20 := erc20Balance.Cmp(swapInitialDeposit) < 0
    72  		insufficientETH := ethBalance.Cmp(minimumEth) < 0
    73  
    74  		erc20SmallUnit, ethSmallUnit := new(big.Int), new(big.Float)
    75  		erc20SmallUnit.SetString(erc20SmallUnitStr, 10)
    76  		ethSmallUnit.SetString(ethSmallUnitStr)
    77  
    78  		if insufficientERC20 || insufficientETH {
    79  			neededERC20, mod := new(big.Int).DivMod(swapInitialDeposit, erc20SmallUnit, new(big.Int))
    80  			if mod.Cmp(big.NewInt(0)) > 0 {
    81  				// always round up the division as the bzzaar cannot handle decimals
    82  				neededERC20.Add(neededERC20, big.NewInt(1))
    83  			}
    84  
    85  			neededETH := new(big.Float).Quo(new(big.Float).SetInt(minimumEth), ethSmallUnit)
    86  
    87  			ccfg, _ := chaincfg.GetByChainID(chainId)
    88  			swarmTokenName := ccfg.SwarmTokenSymbol
    89  			nativeTokenName := ccfg.NativeTokenSymbol
    90  
    91  			if insufficientETH && insufficientERC20 {
    92  				msg := fmt.Sprintf("cannot continue until there is at least min %s (for Gas) and at least min %s available on address", nativeTokenName, swarmTokenName)
    93  				logger.Warning(msg, "min_amount", neededETH, "min_bzz_amount", neededERC20, "address", overlayEthAddress)
    94  			} else if insufficientETH {
    95  				msg := fmt.Sprintf("cannot continue until there is at least min %s (for Gas) available on address", nativeTokenName)
    96  				logger.Warning(msg, "min_amount", neededETH, "address", overlayEthAddress)
    97  			} else {
    98  				msg := fmt.Sprintf("cannot continue until there is at least min %s available on address", swarmTokenName)
    99  				logger.Warning(msg, "min_amount", neededERC20, "address", overlayEthAddress)
   100  			}
   101  			if chainId == chaincfg.Testnet.ChainID {
   102  				logger.Warning("learn how to fund your node by visiting our docs at https://docs.ethswarm.org/docs/installation/fund-your-node")
   103  			}
   104  			select {
   105  			case <-time.After(balanceCheckBackoffDuration):
   106  			case <-timeoutCtx.Done():
   107  				if insufficientERC20 {
   108  					return fmt.Errorf("insufficient %s for initial deposit", swarmTokenName)
   109  				} else {
   110  					return fmt.Errorf("insufficient %s for initial deposit", nativeTokenName)
   111  				}
   112  			}
   113  			continue
   114  		}
   115  
   116  		return nil
   117  	}
   118  }
   119  
   120  // Init initialises the chequebook service.
   121  func Init(
   122  	ctx context.Context,
   123  	chequebookFactory Factory,
   124  	stateStore storage.StateStorer,
   125  	logger log.Logger,
   126  	swapInitialDeposit *big.Int,
   127  	transactionService transaction.Service,
   128  	swapBackend transaction.Backend,
   129  	chainId int64,
   130  	overlayEthAddress common.Address,
   131  	chequeSigner ChequeSigner,
   132  	erc20Service erc20.Service,
   133  ) (chequebookService Service, err error) {
   134  	logger = logger.WithName(loggerName).Register()
   135  
   136  	var chequebookAddress common.Address
   137  	err = stateStore.Get(chequebookKey, &chequebookAddress)
   138  	if err != nil {
   139  		if !errors.Is(err, storage.ErrNotFound) {
   140  			return nil, err
   141  		}
   142  
   143  		var txHash common.Hash
   144  		err = stateStore.Get(ChequebookDeploymentKey, &txHash)
   145  		if err != nil && !errors.Is(err, storage.ErrNotFound) {
   146  			return nil, err
   147  		}
   148  		if errors.Is(err, storage.ErrNotFound) {
   149  			logger.Info("no chequebook found, deploying new one.")
   150  			err = checkBalance(ctx, logger, swapInitialDeposit, swapBackend, chainId, overlayEthAddress, erc20Service)
   151  			if err != nil {
   152  				return nil, err
   153  			}
   154  
   155  			nonce := make([]byte, 32)
   156  			_, err = rand.Read(nonce)
   157  			if err != nil {
   158  				return nil, err
   159  			}
   160  
   161  			// if we don't yet have a chequebook, deploy a new one
   162  			txHash, err = chequebookFactory.Deploy(ctx, overlayEthAddress, big.NewInt(0), common.BytesToHash(nonce))
   163  			if err != nil {
   164  				return nil, err
   165  			}
   166  
   167  			logger.Info("deploying new chequebook", "tx", txHash)
   168  
   169  			err = stateStore.Put(ChequebookDeploymentKey, txHash)
   170  			if err != nil {
   171  				return nil, err
   172  			}
   173  		} else {
   174  			logger.Info("waiting for chequebook deployment", "tx", txHash)
   175  		}
   176  
   177  		chequebookAddress, err = chequebookFactory.WaitDeployed(ctx, txHash)
   178  		if err != nil {
   179  			return nil, err
   180  		}
   181  
   182  		logger.Info("chequebook deployed", "chequebook_address", chequebookAddress)
   183  
   184  		// save the address for later use
   185  		err = stateStore.Put(chequebookKey, chequebookAddress)
   186  		if err != nil {
   187  			return nil, err
   188  		}
   189  
   190  		chequebookService, err = New(transactionService, chequebookAddress, overlayEthAddress, stateStore, chequeSigner, erc20Service)
   191  		if err != nil {
   192  			return nil, err
   193  		}
   194  
   195  		if swapInitialDeposit.Cmp(big.NewInt(0)) != 0 {
   196  			logger.Info("depositing token into new chequebook", "amount", swapInitialDeposit)
   197  			depositHash, err := chequebookService.Deposit(ctx, swapInitialDeposit)
   198  			if err != nil {
   199  				return nil, err
   200  			}
   201  
   202  			logger.Info("sent deposit transaction", "tx", depositHash)
   203  			err = chequebookService.WaitForDeposit(ctx, depositHash)
   204  			if err != nil {
   205  				return nil, err
   206  			}
   207  
   208  			logger.Info("successfully deposited to chequebook")
   209  		}
   210  	} else {
   211  		chequebookService, err = New(transactionService, chequebookAddress, overlayEthAddress, stateStore, chequeSigner, erc20Service)
   212  		if err != nil {
   213  			return nil, err
   214  		}
   215  
   216  		logger.Info("using existing chequebook", "chequebook_address", chequebookAddress)
   217  	}
   218  
   219  	// regardless of how the chequebook service was initialised make sure that the chequebook is valid
   220  	err = chequebookFactory.VerifyChequebook(ctx, chequebookService.Address())
   221  	if err != nil {
   222  		return nil, err
   223  	}
   224  
   225  	return chequebookService, nil
   226  }