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 }