github.com/ethersphere/bee/v2@v2.2.0/pkg/api/chequebook.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 api
     6  
     7  import (
     8  	"errors"
     9  	"math/big"
    10  	"net/http"
    11  
    12  	"github.com/ethereum/go-ethereum/common"
    13  	"github.com/ethersphere/bee/v2/pkg/bigint"
    14  	"github.com/ethersphere/bee/v2/pkg/jsonhttp"
    15  	"github.com/ethersphere/bee/v2/pkg/postage/postagecontract"
    16  	"github.com/ethersphere/bee/v2/pkg/settlement/swap"
    17  	"github.com/ethersphere/bee/v2/pkg/settlement/swap/chequebook"
    18  
    19  	"github.com/ethersphere/bee/v2/pkg/swarm"
    20  	"github.com/gorilla/mux"
    21  )
    22  
    23  const (
    24  	errChequebookBalance           = "cannot get chequebook balance"
    25  	errChequebookNoAmount          = "did not specify amount"
    26  	errChequebookNoWithdraw        = "cannot withdraw"
    27  	errChequebookNoDeposit         = "cannot deposit"
    28  	errChequebookInsufficientFunds = "insufficient funds"
    29  	errCantLastChequePeer          = "cannot get last cheque for peer"
    30  	errCantLastCheque              = "cannot get last cheque for all peers"
    31  	errCannotCash                  = "cannot cash cheque"
    32  	errCannotCashStatus            = "cannot get cashout status"
    33  	errNoCashout                   = "no prior cashout"
    34  	errNoCheque                    = "no prior cheque"
    35  )
    36  
    37  type chequebookBalanceResponse struct {
    38  	TotalBalance     *bigint.BigInt `json:"totalBalance"`
    39  	AvailableBalance *bigint.BigInt `json:"availableBalance"`
    40  }
    41  
    42  type chequebookAddressResponse struct {
    43  	Address string `json:"chequebookAddress"`
    44  }
    45  
    46  type chequebookLastChequePeerResponse struct {
    47  	Beneficiary string         `json:"beneficiary"`
    48  	Chequebook  string         `json:"chequebook"`
    49  	Payout      *bigint.BigInt `json:"payout"`
    50  }
    51  
    52  type chequebookLastChequesPeerResponse struct {
    53  	Peer         string                            `json:"peer"`
    54  	LastReceived *chequebookLastChequePeerResponse `json:"lastreceived"`
    55  	LastSent     *chequebookLastChequePeerResponse `json:"lastsent"`
    56  }
    57  
    58  type chequebookLastChequesResponse struct {
    59  	LastCheques []chequebookLastChequesPeerResponse `json:"lastcheques"`
    60  }
    61  
    62  func (s *Service) chequebookBalanceHandler(w http.ResponseWriter, r *http.Request) {
    63  	logger := s.logger.WithName("get_chequebook_balance").Build()
    64  
    65  	balance, err := s.chequebook.Balance(r.Context())
    66  	if errors.Is(err, postagecontract.ErrChainDisabled) {
    67  		logger.Debug("get balance failed", "error", err)
    68  		logger.Error(nil, "get balance failed")
    69  		jsonhttp.MethodNotAllowed(w, err)
    70  		return
    71  	}
    72  	if err != nil {
    73  		jsonhttp.InternalServerError(w, errChequebookBalance)
    74  		logger.Debug("get balance failed", "error", err)
    75  		logger.Error(nil, "get balance failed")
    76  		return
    77  	}
    78  
    79  	availableBalance, err := s.chequebook.AvailableBalance(r.Context())
    80  	if err != nil {
    81  		jsonhttp.InternalServerError(w, errChequebookBalance)
    82  		logger.Debug("get available balance failed", "error", err)
    83  		logger.Error(nil, "get available balance failed")
    84  		return
    85  	}
    86  
    87  	jsonhttp.OK(w, chequebookBalanceResponse{TotalBalance: bigint.Wrap(balance), AvailableBalance: bigint.Wrap(availableBalance)})
    88  }
    89  
    90  func (s *Service) chequebookAddressHandler(w http.ResponseWriter, _ *http.Request) {
    91  	jsonhttp.OK(w, chequebookAddressResponse{Address: s.chequebook.Address().String()})
    92  }
    93  
    94  func (s *Service) chequebookLastPeerHandler(w http.ResponseWriter, r *http.Request) {
    95  	logger := s.logger.WithName("get_chequebook_cheque_by_peer").Build()
    96  
    97  	paths := struct {
    98  		Peer swarm.Address `map:"peer" validate:"required"`
    99  	}{}
   100  	if response := s.mapStructure(mux.Vars(r), &paths); response != nil {
   101  		response("invalid path params", logger, w)
   102  		return
   103  	}
   104  
   105  	var lastSentResponse *chequebookLastChequePeerResponse
   106  	lastSent, err := s.swap.LastSentCheque(paths.Peer)
   107  	if errors.Is(err, postagecontract.ErrChainDisabled) {
   108  		logger.Debug("get last sent cheque failed", "peer_address", paths.Peer, "error", err)
   109  		logger.Error(nil, "get last sent cheque failed", "peer_address", paths.Peer)
   110  		jsonhttp.MethodNotAllowed(w, err)
   111  		return
   112  	}
   113  	if err != nil && !errors.Is(err, chequebook.ErrNoCheque) && !errors.Is(err, swap.ErrNoChequebook) {
   114  		logger.Debug("get last sent cheque failed", "peer_address", paths.Peer, "error", err)
   115  		logger.Error(nil, "get last sent cheque failed", "peer_address", paths.Peer)
   116  		jsonhttp.InternalServerError(w, errCantLastChequePeer)
   117  		return
   118  	}
   119  	if err == nil {
   120  		lastSentResponse = &chequebookLastChequePeerResponse{
   121  			Beneficiary: lastSent.Cheque.Beneficiary.String(),
   122  			Chequebook:  lastSent.Cheque.Chequebook.String(),
   123  			Payout:      bigint.Wrap(lastSent.Cheque.CumulativePayout),
   124  		}
   125  	}
   126  
   127  	var lastReceivedResponse *chequebookLastChequePeerResponse
   128  	lastReceived, err := s.swap.LastReceivedCheque(paths.Peer)
   129  	if err != nil && !errors.Is(err, chequebook.ErrNoCheque) {
   130  		logger.Debug("get last received cheque failed", "peer_address", paths.Peer, "error", err)
   131  		logger.Error(nil, "get last received cheque failed", "peer_address", paths.Peer)
   132  		jsonhttp.InternalServerError(w, errCantLastChequePeer)
   133  		return
   134  	}
   135  	if err == nil {
   136  		lastReceivedResponse = &chequebookLastChequePeerResponse{
   137  			Beneficiary: lastReceived.Cheque.Beneficiary.String(),
   138  			Chequebook:  lastReceived.Cheque.Chequebook.String(),
   139  			Payout:      bigint.Wrap(lastReceived.Cheque.CumulativePayout),
   140  		}
   141  	}
   142  
   143  	jsonhttp.OK(w, chequebookLastChequesPeerResponse{
   144  		Peer:         paths.Peer.String(),
   145  		LastReceived: lastReceivedResponse,
   146  		LastSent:     lastSentResponse,
   147  	})
   148  }
   149  
   150  func (s *Service) chequebookAllLastHandler(w http.ResponseWriter, _ *http.Request) {
   151  	logger := s.logger.WithName("get_chequebook_cheques").Build()
   152  
   153  	lastchequessent, err := s.swap.LastSentCheques()
   154  	if errors.Is(err, postagecontract.ErrChainDisabled) {
   155  		logger.Debug("get all last sent cheque failed", "error", err)
   156  		logger.Error(nil, "get all last sent cheque failed")
   157  		jsonhttp.MethodNotAllowed(w, err)
   158  		return
   159  	}
   160  	if err != nil {
   161  		if !errors.Is(err, swap.ErrNoChequebook) {
   162  			logger.Debug("get all last sent cheque failed", "error", err)
   163  			logger.Error(nil, "get all last sent cheque failed")
   164  			jsonhttp.InternalServerError(w, errCantLastCheque)
   165  			return
   166  		}
   167  		lastchequessent = map[string]*chequebook.SignedCheque{}
   168  	}
   169  	lastchequesreceived, err := s.swap.LastReceivedCheques()
   170  	if err != nil {
   171  		logger.Debug("get all last received cheque failed", "error", err)
   172  		logger.Error(nil, "get all last received cheque failed")
   173  		jsonhttp.InternalServerError(w, errCantLastCheque)
   174  		return
   175  	}
   176  
   177  	lcr := make(map[string]chequebookLastChequesPeerResponse)
   178  	for i, j := range lastchequessent {
   179  		lcr[i] = chequebookLastChequesPeerResponse{
   180  			Peer: i,
   181  			LastSent: &chequebookLastChequePeerResponse{
   182  				Beneficiary: j.Cheque.Beneficiary.String(),
   183  				Chequebook:  j.Cheque.Chequebook.String(),
   184  				Payout:      bigint.Wrap(j.Cheque.CumulativePayout),
   185  			},
   186  			LastReceived: nil,
   187  		}
   188  	}
   189  	for i, j := range lastchequesreceived {
   190  		if _, ok := lcr[i]; ok {
   191  			t := lcr[i]
   192  			t.LastReceived = &chequebookLastChequePeerResponse{
   193  				Beneficiary: j.Cheque.Beneficiary.String(),
   194  				Chequebook:  j.Cheque.Chequebook.String(),
   195  				Payout:      bigint.Wrap(j.Cheque.CumulativePayout),
   196  			}
   197  			lcr[i] = t
   198  		} else {
   199  			lcr[i] = chequebookLastChequesPeerResponse{
   200  				Peer:     i,
   201  				LastSent: nil,
   202  				LastReceived: &chequebookLastChequePeerResponse{
   203  					Beneficiary: j.Cheque.Beneficiary.String(),
   204  					Chequebook:  j.Cheque.Chequebook.String(),
   205  					Payout:      bigint.Wrap(j.Cheque.CumulativePayout),
   206  				},
   207  			}
   208  		}
   209  	}
   210  
   211  	lcresponses := make([]chequebookLastChequesPeerResponse, len(lcr))
   212  	i := 0
   213  	for k := range lcr {
   214  		lcresponses[i] = lcr[k]
   215  		i++
   216  	}
   217  
   218  	jsonhttp.OK(w, chequebookLastChequesResponse{LastCheques: lcresponses})
   219  }
   220  
   221  type swapCashoutResponse struct {
   222  	TransactionHash string `json:"transactionHash"`
   223  }
   224  
   225  func (s *Service) swapCashoutHandler(w http.ResponseWriter, r *http.Request) {
   226  	logger := s.logger.WithName("post_chequebook_cashout").Build()
   227  
   228  	paths := struct {
   229  		Peer swarm.Address `map:"peer" validate:"required"`
   230  	}{}
   231  	if response := s.mapStructure(mux.Vars(r), &paths); response != nil {
   232  		response("invalid path params", logger, w)
   233  		return
   234  	}
   235  
   236  	if !s.cashOutChequeSem.TryAcquire(1) {
   237  		logger.Debug("simultaneous on-chain operations not supported")
   238  		logger.Error(nil, "simultaneous on-chain operations not supported")
   239  		jsonhttp.TooManyRequests(w, "simultaneous on-chain operations not supported")
   240  		return
   241  	}
   242  	defer s.cashOutChequeSem.Release(1)
   243  
   244  	txHash, err := s.swap.CashCheque(r.Context(), paths.Peer)
   245  	if errors.Is(err, postagecontract.ErrChainDisabled) {
   246  		logger.Debug("cash cheque failed", "peer_address", paths.Peer, "error", err)
   247  		logger.Error(nil, "cash cheque failed", "peer_address", paths.Peer)
   248  		jsonhttp.MethodNotAllowed(w, err)
   249  		return
   250  	}
   251  	if err != nil {
   252  		logger.Debug("cash cheque failed", "peer_address", paths.Peer, "error", err)
   253  		logger.Error(nil, "cash cheque failed", "peer_address", paths.Peer)
   254  		jsonhttp.InternalServerError(w, errCannotCash)
   255  		return
   256  	}
   257  
   258  	jsonhttp.OK(w, swapCashoutResponse{TransactionHash: txHash.String()})
   259  }
   260  
   261  type swapCashoutStatusResult struct {
   262  	Recipient  common.Address `json:"recipient"`
   263  	LastPayout *bigint.BigInt `json:"lastPayout"`
   264  	Bounced    bool           `json:"bounced"`
   265  }
   266  
   267  type swapCashoutStatusResponse struct {
   268  	Peer            swarm.Address                     `json:"peer"`
   269  	Cheque          *chequebookLastChequePeerResponse `json:"lastCashedCheque"`
   270  	TransactionHash *common.Hash                      `json:"transactionHash"`
   271  	Result          *swapCashoutStatusResult          `json:"result"`
   272  	UncashedAmount  *bigint.BigInt                    `json:"uncashedAmount"`
   273  }
   274  
   275  func (s *Service) swapCashoutStatusHandler(w http.ResponseWriter, r *http.Request) {
   276  	logger := s.logger.WithName("get_chequebook_cashout").Build()
   277  
   278  	paths := struct {
   279  		Peer swarm.Address `map:"peer" validate:"required"`
   280  	}{}
   281  	if response := s.mapStructure(mux.Vars(r), &paths); response != nil {
   282  		response("invalid path params", logger, w)
   283  		return
   284  	}
   285  
   286  	status, err := s.swap.CashoutStatus(r.Context(), paths.Peer)
   287  	if errors.Is(err, postagecontract.ErrChainDisabled) {
   288  		logger.Debug("get cashout status failed", "peer_address", paths.Peer, "error", err)
   289  		logger.Error(nil, "get cashout status failed", "peer_address", paths.Peer)
   290  		jsonhttp.MethodNotAllowed(w, err)
   291  		return
   292  	}
   293  	if err != nil {
   294  		if errors.Is(err, chequebook.ErrNoCheque) {
   295  			logger.Debug("get cashout status failed", "peer_address", paths.Peer, "error", err)
   296  			logger.Error(nil, "get cashout status failed", "peer_address", paths.Peer)
   297  			jsonhttp.NotFound(w, errNoCheque)
   298  			return
   299  		}
   300  		if errors.Is(err, chequebook.ErrNoCashout) {
   301  			logger.Debug("get cashout status failed", "peer_address", paths.Peer, "error", err)
   302  			logger.Error(nil, "get cashout status failed", "peer_address", paths.Peer)
   303  			jsonhttp.NotFound(w, errNoCashout)
   304  			return
   305  		}
   306  		logger.Debug("get cashout status failed", "peer_address", paths.Peer, "error", err)
   307  		logger.Error(nil, "get cashout status failed", "peer_address", paths.Peer)
   308  		jsonhttp.InternalServerError(w, errCannotCashStatus)
   309  		return
   310  	}
   311  
   312  	var result *swapCashoutStatusResult
   313  	var txHash *common.Hash
   314  	var chequeResponse *chequebookLastChequePeerResponse
   315  	if status.Last != nil {
   316  		if status.Last.Result != nil {
   317  			result = &swapCashoutStatusResult{
   318  				Recipient:  status.Last.Result.Recipient,
   319  				LastPayout: bigint.Wrap(status.Last.Result.TotalPayout),
   320  				Bounced:    status.Last.Result.Bounced,
   321  			}
   322  		}
   323  		chequeResponse = &chequebookLastChequePeerResponse{
   324  			Chequebook:  status.Last.Cheque.Chequebook.String(),
   325  			Payout:      bigint.Wrap(status.Last.Cheque.CumulativePayout),
   326  			Beneficiary: status.Last.Cheque.Beneficiary.String(),
   327  		}
   328  		txHash = &status.Last.TxHash
   329  	}
   330  
   331  	jsonhttp.OK(w, swapCashoutStatusResponse{
   332  		Peer:            paths.Peer,
   333  		TransactionHash: txHash,
   334  		Cheque:          chequeResponse,
   335  		Result:          result,
   336  		UncashedAmount:  bigint.Wrap(status.UncashedAmount),
   337  	})
   338  }
   339  
   340  type chequebookTxResponse struct {
   341  	TransactionHash common.Hash `json:"transactionHash"`
   342  }
   343  
   344  func (s *Service) chequebookWithdrawHandler(w http.ResponseWriter, r *http.Request) {
   345  	logger := s.logger.WithName("post_chequebook_withdraw").Build()
   346  
   347  	queries := struct {
   348  		Amount *big.Int `map:"amount" validate:"required"`
   349  	}{}
   350  	if response := s.mapStructure(r.URL.Query(), &queries); response != nil {
   351  		response("invalid query params", logger, w)
   352  		return
   353  	}
   354  
   355  	txHash, err := s.chequebook.Withdraw(r.Context(), queries.Amount)
   356  	if errors.Is(err, chequebook.ErrInsufficientFunds) {
   357  		logger.Debug("withdraw failed", "error", err)
   358  		logger.Error(nil, "withdraw failed")
   359  		jsonhttp.BadRequest(w, errChequebookInsufficientFunds)
   360  		return
   361  	}
   362  	if err != nil {
   363  		logger.Debug("withdraw failed", "error", err)
   364  		logger.Error(nil, "withdraw failed")
   365  		jsonhttp.InternalServerError(w, errChequebookNoWithdraw)
   366  		return
   367  	}
   368  
   369  	jsonhttp.OK(w, chequebookTxResponse{TransactionHash: txHash})
   370  }
   371  
   372  func (s *Service) chequebookDepositHandler(w http.ResponseWriter, r *http.Request) {
   373  	logger := s.logger.WithName("post_chequebook_deposit").Build()
   374  
   375  	queries := struct {
   376  		Amount *big.Int `map:"amount" validate:"required"`
   377  	}{}
   378  	if response := s.mapStructure(r.URL.Query(), &queries); response != nil {
   379  		response("invalid query params", logger, w)
   380  		return
   381  	}
   382  
   383  	txHash, err := s.chequebook.Deposit(r.Context(), queries.Amount)
   384  	if errors.Is(err, chequebook.ErrInsufficientFunds) {
   385  		logger.Debug("chequebook deposit: deposit failed", "error", err)
   386  		logger.Error(nil, "chequebook deposit: deposit failed")
   387  		jsonhttp.BadRequest(w, errChequebookInsufficientFunds)
   388  		return
   389  	}
   390  	if err != nil {
   391  		logger.Debug("chequebook deposit: deposit failed", "error", err)
   392  		logger.Error(nil, "chequebook deposit: deposit failed")
   393  		jsonhttp.InternalServerError(w, errChequebookNoDeposit)
   394  		return
   395  	}
   396  
   397  	jsonhttp.OK(w, chequebookTxResponse{TransactionHash: txHash})
   398  }