github.com/ethersphere/bee/v2@v2.2.0/pkg/settlement/swap/chequebook/cashout.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  	"errors"
    10  	"fmt"
    11  	"math/big"
    12  
    13  	"github.com/ethereum/go-ethereum"
    14  	"github.com/ethereum/go-ethereum/accounts/abi"
    15  	"github.com/ethereum/go-ethereum/common"
    16  	"github.com/ethereum/go-ethereum/core/types"
    17  	"github.com/ethersphere/bee/v2/pkg/sctx"
    18  	"github.com/ethersphere/bee/v2/pkg/storage"
    19  	"github.com/ethersphere/bee/v2/pkg/transaction"
    20  )
    21  
    22  var (
    23  	// ErrNoCashout is the error if there has not been any cashout action for the chequebook
    24  	ErrNoCashout = errors.New("no prior cashout")
    25  )
    26  
    27  // CashoutService is the service responsible for managing cashout actions
    28  type CashoutService interface {
    29  	// CashCheque sends a cashing transaction for the last cheque of the chequebook
    30  	CashCheque(ctx context.Context, chequebook common.Address, recipient common.Address) (common.Hash, error)
    31  	// CashoutStatus gets the status of the latest cashout transaction for the chequebook
    32  	CashoutStatus(ctx context.Context, chequebookAddress common.Address) (*CashoutStatus, error)
    33  }
    34  
    35  type cashoutService struct {
    36  	store              storage.StateStorer
    37  	backend            transaction.Backend
    38  	transactionService transaction.Service
    39  	chequeStore        ChequeStore
    40  }
    41  
    42  // LastCashout contains information about the last cashout
    43  type LastCashout struct {
    44  	TxHash   common.Hash
    45  	Cheque   SignedCheque // the cheque that was used to cashout which may be different from the latest cheque
    46  	Result   *CashChequeResult
    47  	Reverted bool
    48  }
    49  
    50  // CashoutStatus is information about the last cashout and uncashed amounts
    51  type CashoutStatus struct {
    52  	Last           *LastCashout // last cashout for a chequebook
    53  	UncashedAmount *big.Int     // amount not yet cashed out
    54  }
    55  
    56  // CashChequeResult summarizes the result of a CashCheque or CashChequeBeneficiary call
    57  type CashChequeResult struct {
    58  	Beneficiary      common.Address // beneficiary of the cheque
    59  	Recipient        common.Address // address which received the funds
    60  	Caller           common.Address // caller of cashCheque
    61  	TotalPayout      *big.Int       // total amount that was paid out in this call
    62  	CumulativePayout *big.Int       // cumulative payout of the cheque that was cashed
    63  	CallerPayout     *big.Int       // payout for the caller of cashCheque
    64  	Bounced          bool           // indicates whether parts of the cheque bounced
    65  }
    66  
    67  // cashoutAction is the data we store for a cashout
    68  type cashoutAction struct {
    69  	TxHash common.Hash
    70  	Cheque SignedCheque // the cheque that was used to cashout which may be different from the latest cheque
    71  }
    72  type chequeCashedEvent struct {
    73  	Beneficiary      common.Address
    74  	Recipient        common.Address
    75  	Caller           common.Address
    76  	TotalPayout      *big.Int
    77  	CumulativePayout *big.Int
    78  	CallerPayout     *big.Int
    79  }
    80  
    81  // NewCashoutService creates a new CashoutService
    82  func NewCashoutService(
    83  	store storage.StateStorer,
    84  	backend transaction.Backend,
    85  	transactionService transaction.Service,
    86  	chequeStore ChequeStore,
    87  ) CashoutService {
    88  	return &cashoutService{
    89  		store:              store,
    90  		backend:            backend,
    91  		transactionService: transactionService,
    92  		chequeStore:        chequeStore,
    93  	}
    94  }
    95  
    96  // cashoutActionKey computes the store key for the last cashout action for the chequebook
    97  func cashoutActionKey(chequebook common.Address) string {
    98  	return fmt.Sprintf("swap_cashout_%x", chequebook)
    99  }
   100  
   101  func (s *cashoutService) paidOut(ctx context.Context, chequebook, beneficiary common.Address) (*big.Int, error) {
   102  	callData, err := chequebookABI.Pack("paidOut", beneficiary)
   103  	if err != nil {
   104  		return nil, err
   105  	}
   106  
   107  	output, err := s.transactionService.Call(ctx, &transaction.TxRequest{
   108  		To:   &chequebook,
   109  		Data: callData,
   110  	})
   111  	if err != nil {
   112  		return nil, err
   113  	}
   114  
   115  	results, err := chequebookABI.Unpack("paidOut", output)
   116  	if err != nil {
   117  		return nil, err
   118  	}
   119  
   120  	if len(results) != 1 {
   121  		return nil, errDecodeABI
   122  	}
   123  
   124  	paidOut, ok := abi.ConvertType(results[0], new(big.Int)).(*big.Int)
   125  	if !ok || paidOut == nil {
   126  		return nil, errDecodeABI
   127  	}
   128  
   129  	return paidOut, nil
   130  }
   131  
   132  // CashCheque sends a cashout transaction for the last cheque of the chequebook
   133  func (s *cashoutService) CashCheque(ctx context.Context, chequebook, recipient common.Address) (common.Hash, error) {
   134  	cheque, err := s.chequeStore.LastCheque(chequebook)
   135  	if err != nil {
   136  		return common.Hash{}, err
   137  	}
   138  
   139  	callData, err := chequebookABI.Pack("cashChequeBeneficiary", recipient, cheque.CumulativePayout, cheque.Signature)
   140  	if err != nil {
   141  		return common.Hash{}, err
   142  	}
   143  	request := &transaction.TxRequest{
   144  		To:          &chequebook,
   145  		Data:        callData,
   146  		GasPrice:    sctx.GetGasPrice(ctx),
   147  		GasLimit:    sctx.GetGasLimitWithDefault(ctx, 300_000),
   148  		Value:       big.NewInt(0),
   149  		Description: "cheque cashout",
   150  	}
   151  
   152  	txHash, err := s.transactionService.Send(ctx, request, transaction.DefaultTipBoostPercent)
   153  	if err != nil {
   154  		return common.Hash{}, err
   155  	}
   156  
   157  	err = s.store.Put(cashoutActionKey(chequebook), &cashoutAction{
   158  		TxHash: txHash,
   159  		Cheque: *cheque,
   160  	})
   161  	if err != nil {
   162  		return common.Hash{}, err
   163  	}
   164  
   165  	return txHash, nil
   166  }
   167  
   168  // CashoutStatus gets the status of the latest cashout transaction for the chequebook
   169  func (s *cashoutService) CashoutStatus(ctx context.Context, chequebookAddress common.Address) (*CashoutStatus, error) {
   170  	cheque, err := s.chequeStore.LastCheque(chequebookAddress)
   171  	if err != nil {
   172  		return nil, err
   173  	}
   174  
   175  	var action cashoutAction
   176  	err = s.store.Get(cashoutActionKey(chequebookAddress), &action)
   177  	if err != nil {
   178  		if errors.Is(err, storage.ErrNotFound) {
   179  			return &CashoutStatus{
   180  				Last:           nil,
   181  				UncashedAmount: cheque.CumulativePayout, // if we never cashed out, assume everything is uncashed
   182  			}, nil
   183  		}
   184  		return nil, err
   185  	}
   186  
   187  	_, pending, err := s.backend.TransactionByHash(ctx, action.TxHash)
   188  	if err != nil {
   189  		// treat not found as pending
   190  		if !errors.Is(err, ethereum.NotFound) {
   191  			return nil, err
   192  		}
   193  		pending = true
   194  	}
   195  
   196  	if pending {
   197  		return &CashoutStatus{
   198  			Last: &LastCashout{
   199  				TxHash:   action.TxHash,
   200  				Cheque:   action.Cheque,
   201  				Result:   nil,
   202  				Reverted: false,
   203  			},
   204  			// uncashed is the difference since the last sent cashout. we assume that the entire cheque will clear in the pending transaction.
   205  			UncashedAmount: new(big.Int).Sub(cheque.CumulativePayout, action.Cheque.CumulativePayout),
   206  		}, nil
   207  	}
   208  
   209  	receipt, err := s.backend.TransactionReceipt(ctx, action.TxHash)
   210  	if err != nil {
   211  		return nil, err
   212  	}
   213  
   214  	if receipt.Status == types.ReceiptStatusFailed {
   215  		// if a tx failed (should be almost impossible in practice) we no longer have the necessary information to compute uncashed locally
   216  		// assume there are no pending transactions and that the on-chain paidOut is the last cashout action
   217  		paidOut, err := s.paidOut(ctx, chequebookAddress, cheque.Beneficiary)
   218  		if err != nil {
   219  			return nil, err
   220  		}
   221  
   222  		return &CashoutStatus{
   223  			Last: &LastCashout{
   224  				TxHash:   action.TxHash,
   225  				Cheque:   action.Cheque,
   226  				Result:   nil,
   227  				Reverted: true,
   228  			},
   229  			UncashedAmount: new(big.Int).Sub(cheque.CumulativePayout, paidOut),
   230  		}, nil
   231  	}
   232  
   233  	result, err := s.parseCashChequeBeneficiaryReceipt(chequebookAddress, receipt)
   234  	if err != nil {
   235  		return nil, err
   236  	}
   237  
   238  	return &CashoutStatus{
   239  		Last: &LastCashout{
   240  			TxHash:   action.TxHash,
   241  			Cheque:   action.Cheque,
   242  			Result:   result,
   243  			Reverted: false,
   244  		},
   245  		// uncashed is the difference since the last sent (and confirmed) cashout.
   246  		UncashedAmount: new(big.Int).Sub(cheque.CumulativePayout, result.CumulativePayout),
   247  	}, nil
   248  }
   249  
   250  // parseCashChequeBeneficiaryReceipt processes the receipt from a CashChequeBeneficiary transaction
   251  func (s *cashoutService) parseCashChequeBeneficiaryReceipt(chequebookAddress common.Address, receipt *types.Receipt) (*CashChequeResult, error) {
   252  	result := &CashChequeResult{
   253  		Bounced: false,
   254  	}
   255  
   256  	var cashedEvent chequeCashedEvent
   257  	err := transaction.FindSingleEvent(&chequebookABI, receipt, chequebookAddress, chequeCashedEventType, &cashedEvent)
   258  	if err != nil {
   259  		return nil, err
   260  	}
   261  
   262  	result.Beneficiary = cashedEvent.Beneficiary
   263  	result.Caller = cashedEvent.Caller
   264  	result.CallerPayout = cashedEvent.CallerPayout
   265  	result.TotalPayout = cashedEvent.TotalPayout
   266  	result.CumulativePayout = cashedEvent.CumulativePayout
   267  	result.Recipient = cashedEvent.Recipient
   268  
   269  	err = transaction.FindSingleEvent(&chequebookABI, receipt, chequebookAddress, chequeBouncedEventType, nil)
   270  	if err == nil {
   271  		result.Bounced = true
   272  	} else if !errors.Is(err, transaction.ErrEventNotFound) {
   273  		return nil, err
   274  	}
   275  
   276  	return result, nil
   277  }
   278  
   279  // Equal compares to CashChequeResults
   280  func (r *CashChequeResult) Equal(o *CashChequeResult) bool {
   281  	if r.Beneficiary != o.Beneficiary {
   282  		return false
   283  	}
   284  	if r.Bounced != o.Bounced {
   285  		return false
   286  	}
   287  	if r.Caller != o.Caller {
   288  		return false
   289  	}
   290  	if r.CallerPayout.Cmp(o.CallerPayout) != 0 {
   291  		return false
   292  	}
   293  	if r.CumulativePayout.Cmp(o.CumulativePayout) != 0 {
   294  		return false
   295  	}
   296  	if r.Recipient != o.Recipient {
   297  		return false
   298  	}
   299  	if r.TotalPayout.Cmp(o.TotalPayout) != 0 {
   300  		return false
   301  	}
   302  	return true
   303  }