diff --git a/swap/cashout.go b/swap/cashout.go index 9d62250c64..08379d40b3 100644 --- a/swap/cashout.go +++ b/swap/cashout.go @@ -21,6 +21,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/metrics" contract "github.com/ethersphere/swarm/contracts/swap" "github.com/ethersphere/swarm/swap/chain" @@ -30,10 +31,9 @@ import ( // CashChequeBeneficiaryTransactionCost is the expected gas cost of a CashChequeBeneficiary transaction const CashChequeBeneficiaryTransactionCost = 50000 -// CashoutProcessor holds all relevant fields needed for processing cashouts -type CashoutProcessor struct { - backend chain.Backend // ethereum backend to use - privateKey *ecdsa.PrivateKey // private key to use +var CashoutRequestTypeID = chain.TxRequestTypeID{ + Handler: "cashout", + RequestType: "CashoutRequest", } // CashoutRequest represents a request for a cashout operation @@ -42,42 +42,94 @@ type CashoutRequest struct { Destination common.Address // destination for the payout } -// ActiveCashout stores the necessary information for a cashout in progess -type ActiveCashout struct { - Request CashoutRequest // the request that caused this cashout - TransactionHash common.Hash // the hash of the current transaction for this request +// CashoutProcessor holds all relevant fields needed for processing cashouts +type CashoutProcessor struct { + backend chain.Backend // ethereum backend to use + txScheduler chain.TxScheduler // transaction queue to use + cashoutResultHandler CashoutResultHandler + cashoutDone chan *CashoutRequest +} + +type CashoutResultHandler interface { + HandleCashoutResult(request *CashoutRequest, result *contract.CashChequeResult, receipt *types.Receipt) error } // newCashoutProcessor creates a new instance of CashoutProcessor -func newCashoutProcessor(backend chain.Backend, privateKey *ecdsa.PrivateKey) *CashoutProcessor { - return &CashoutProcessor{ - backend: backend, - privateKey: privateKey, +func newCashoutProcessor(txScheduler chain.TxScheduler, backend chain.Backend, privateKey *ecdsa.PrivateKey, cashoutResultHandler CashoutResultHandler) *CashoutProcessor { + c := &CashoutProcessor{ + backend: backend, + txScheduler: txScheduler, + cashoutResultHandler: cashoutResultHandler, } -} -// cashCheque tries to cash the cheque specified in the request -// after the transaction is sent it waits on its success -func (c *CashoutProcessor) cashCheque(ctx context.Context, request *CashoutRequest) error { - cheque := request.Cheque - opts := bind.NewKeyedTransactor(c.privateKey) - opts.Context = ctx + txScheduler.SetHandlers(CashoutRequestTypeID, &chain.TxRequestHandlers{ + Send: func(id uint64, backend chain.Backend, opts *bind.TransactOpts) (common.Hash, error) { + var request CashoutRequest + if err := c.txScheduler.GetRequest(id, &request); err != nil { + return common.Hash{}, err + } + + cheque := request.Cheque + + otherSwap, err := contract.InstanceAt(cheque.Contract, backend) + if err != nil { + return common.Hash{}, err + } + + tx, err := otherSwap.CashChequeBeneficiaryStart(opts, request.Destination, cheque.CumulativePayout, cheque.Signature) + if err != nil { + return common.Hash{}, err + } + return tx.Hash(), nil + }, + NotifyReceipt: func(ctx context.Context, id uint64, notification *chain.TxReceiptNotification) error { + var request *CashoutRequest + err := c.txScheduler.GetRequest(id, &request) + if err != nil { + return err + } + + otherSwap, err := contract.InstanceAt(request.Cheque.Contract, c.backend) + if err != nil { + return err + } + + receipt := ¬ification.Receipt + if receipt.Status == 0 { + swapLog.Error("cheque cashing transaction reverted", "tx", receipt.TxHash) + return nil + } + + result := otherSwap.CashChequeBeneficiaryResult(receipt) + return c.cashoutResultHandler.HandleCashoutResult(request, result, receipt) + }, + }) + return c +} - otherSwap, err := contract.InstanceAt(cheque.Contract, c.backend) +func (c *CashoutProcessor) submitCheque(ctx context.Context, request *CashoutRequest) { + expectedPayout, transactionCosts, err := c.estimatePayout(ctx, &request.Cheque) if err != nil { - return err + swapLog.Error("could not estimate payout", "error", err) + return } - tx, err := otherSwap.CashChequeBeneficiaryStart(opts, request.Destination, cheque.CumulativePayout, cheque.Signature) + costsMultiplier := uint256.FromUint64(2) + costThreshold, err := uint256.New().Mul(transactionCosts, costsMultiplier) if err != nil { - return err + swapLog.Error("overflow in transaction fee", "error", err) + return } - // this blocks until the cashout has been successfully processed - return c.waitForAndProcessActiveCashout(&ActiveCashout{ - Request: *request, - TransactionHash: tx.Hash(), - }) + // do a payout transaction if we get 2 times the gas costs + if expectedPayout.Cmp(costThreshold) == 1 { + swapLog.Info("queueing cashout", "cheque", &request.Cheque) + _, err := c.txScheduler.ScheduleRequest(CashoutRequestTypeID, request) + if err != nil { + metrics.GetOrRegisterCounter("swap.cheques.cashed.errors", nil).Inc(1) + swapLog.Error("cashing cheque:", "error", err) + } + } } // estimatePayout estimates the payout for a given cheque as well as the transaction cost @@ -123,31 +175,3 @@ func (c *CashoutProcessor) estimatePayout(ctx context.Context, cheque *Cheque) ( return expectedPayout, transactionCosts, nil } - -// waitForAndProcessActiveCashout waits for activeCashout to complete -func (c *CashoutProcessor) waitForAndProcessActiveCashout(activeCashout *ActiveCashout) error { - ctx, cancel := context.WithTimeout(context.Background(), DefaultTransactionTimeout) - defer cancel() - - receipt, err := chain.WaitMined(ctx, c.backend, activeCashout.TransactionHash) - if err != nil { - return err - } - - otherSwap, err := contract.InstanceAt(activeCashout.Request.Cheque.Contract, c.backend) - if err != nil { - return err - } - - result := otherSwap.CashChequeBeneficiaryResult(receipt) - - metrics.GetOrRegisterCounter("swap.cheques.cashed.honey", nil).Inc(result.TotalPayout.Int64()) - - if result.Bounced { - metrics.GetOrRegisterCounter("swap.cheques.cashed.bounced", nil).Inc(1) - swapLog.Warn("cheque bounced", "tx", receipt.TxHash) - } - - swapLog.Info("cheque cashed", "honey", activeCashout.Request.Cheque.Honey) - return nil -} diff --git a/swap/cashout_test.go b/swap/cashout_test.go index 18d56da4ce..085f984a12 100644 --- a/swap/cashout_test.go +++ b/swap/cashout_test.go @@ -19,9 +19,11 @@ package swap import ( "context" "testing" + "time" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/log" + "github.com/ethersphere/swarm/state" "github.com/ethersphere/swarm/swap/chain" "github.com/ethersphere/swarm/uint256" ) @@ -33,8 +35,7 @@ import ( // afterwards it attempts to cash-in a bouncing cheque func TestContractIntegration(t *testing.T) { backend := newTestBackend(t) - reset := setupContractTest() - defer reset() + defer backend.Close() payout := uint256.FromUint64(42) chequebook, err := testDeployWithPrivateKey(context.Background(), backend, ownerKey, ownerAddress, payout) @@ -116,11 +117,18 @@ func TestContractIntegration(t *testing.T) { // TestCashCheque creates a valid cheque and feeds it to cashoutProcessor.cashCheque func TestCashCheque(t *testing.T) { backend := newTestBackend(t) - reset := setupContractTest() - defer reset() + defer backend.Close() - cashoutProcessor := newCashoutProcessor(backend, ownerKey) - payout := uint256.FromUint64(42) + store := state.NewInmemoryStore() + defer store.Close() + + transactionQueue := chain.NewTxQueue(store, "queue", backend, ownerKey) + transactionQueue.Start() + defer transactionQueue.Stop() + + cashoutHandler := newTestCashoutResultHandler(nil) + cashoutProcessor := newCashoutProcessor(transactionQueue, backend, ownerKey, cashoutHandler) + payout := uint256.FromUint64(CashChequeBeneficiaryTransactionCost*2 + 1) chequebook, err := testDeployWithPrivateKey(context.Background(), backend, ownerKey, ownerAddress, payout) if err != nil { @@ -132,12 +140,14 @@ func TestCashCheque(t *testing.T) { t.Fatal(err) } - err = cashoutProcessor.cashCheque(context.Background(), &CashoutRequest{ + cashoutProcessor.submitCheque(context.Background(), &CashoutRequest{ Cheque: *testCheque, Destination: ownerAddress, }) - if err != nil { - t.Fatal(err) + + select { + case <-cashoutHandler.cashChequeDone: + case <-time.After(5 * time.Second): } paidOut, err := chequebook.PaidOut(nil, ownerAddress) @@ -154,12 +164,18 @@ func TestCashCheque(t *testing.T) { // TestEstimatePayout creates a valid cheque and feeds it to cashoutProcessor.estimatePayout func TestEstimatePayout(t *testing.T) { backend := newTestBackend(t) - reset := setupContractTest() - defer reset() + defer backend.Close() - cashoutProcessor := newCashoutProcessor(backend, ownerKey) - payout := uint256.FromUint64(42) + store := state.NewInmemoryStore() + defer store.Close() + + transactionQueue := chain.NewTxQueue(store, "queue", backend, ownerKey) + transactionQueue.Start() + defer transactionQueue.Stop() + cashoutProcessor := newCashoutProcessor(transactionQueue, backend, ownerKey, &testCashoutResultHandler{}) + + payout := uint256.FromUint64(42) chequebook, err := testDeployWithPrivateKey(context.Background(), backend, ownerKey, ownerAddress, payout) if err != nil { t.Fatal(err) diff --git a/swap/chain/txqueue.go b/swap/chain/txqueue.go index f293aee90f..b240b8d87d 100644 --- a/swap/chain/txqueue.go +++ b/swap/chain/txqueue.go @@ -231,6 +231,8 @@ func (txq *TxQueue) Start() { return } + log.Info("starting transaction queue", "queue", txq.prefix) + txq.running = true txq.wg.Add(1) go func() { diff --git a/swap/common_test.go b/swap/common_test.go index f7b7fbcce6..de0aeca62f 100644 --- a/swap/common_test.go +++ b/swap/common_test.go @@ -15,10 +15,12 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind/backends" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/p2p/simulations/adapters" contractFactory "github.com/ethersphere/go-sw3/contracts-v0-2-0/simpleswapfactory" + contract "github.com/ethersphere/swarm/contracts/swap" cswap "github.com/ethersphere/swarm/contracts/swap" "github.com/ethersphere/swarm/network" "github.com/ethersphere/swarm/p2p/protocols" @@ -34,8 +36,6 @@ type swapTestBackend struct { *mock.TestBackend factoryAddress common.Address // address of the SimpleSwapFactory in the simulated network tokenAddress common.Address // address of the token in the simulated network - // the async cashing go routine needs synchronization for tests - cashDone chan struct{} } var defaultBackend = backends.NewSimulatedBackend(core.GenesisAlloc{ @@ -67,7 +67,6 @@ func newTestBackend(t *testing.T) *swapTestBackend { TestBackend: backend, factoryAddress: factoryAddress, tokenAddress: tokenAddress, - cashDone: make(chan struct{}), } } @@ -105,7 +104,9 @@ func newBaseTestSwapWithParams(t *testing.T, key *ecdsa.PrivateKey, params *Para if err != nil { t.Fatal(err) } - swap := newSwapInstance(stateStore, owner, backend, 10, params, factory) + + txqueue := chain.NewTxQueue(stateStore, "chain", backend, owner.privateKey) + swap := newSwapInstance(stateStore, owner, backend, 10, params, factory, txqueue) return swap, dir } @@ -126,6 +127,7 @@ func newTestSwap(t *testing.T, key *ecdsa.PrivateKey, backend *swapTestBackend) usedBackend = newTestBackend(t) } swap, dir := newBaseTestSwap(t, key, usedBackend) + swap.txScheduler.Start() clean := func() { swap.Close() // only close if created by newTestSwap to avoid double close @@ -206,32 +208,6 @@ func newRandomTestCheque() *Cheque { return cheque } -// During tests, because the cashing in of cheques is async, we should wait for the function to be returned -// Otherwise if we call `handleEmitChequeMsg` manually, it will return before the TX has been committed to the `SimulatedBackend`, -// causing subsequent TX to possibly fail due to nonce mismatch -func testCashCheque(s *Swap, cheque *Cheque) { - cashCheque(s, cheque) - // send to the channel, signals to clients that this function actually finished - if stb, ok := s.backend.(*swapTestBackend); ok { - if stb.cashDone != nil { - stb.cashDone <- struct{}{} - } - } -} - -// setupContractTest is a helper function for setting up the -// blockchain wait function for testing -func setupContractTest() func() { - // we also need to store the previous cashCheque function in case this is called multiple times - currentCashCheque := defaultCashCheque - defaultCashCheque = testCashCheque - // overwrite only for the duration of the test, so... - return func() { - // ...we need to set it back to original when done - defaultCashCheque = currentCashCheque - } -} - // deploy for testing (needs simulated backend commit) func testDeployWithPrivateKey(ctx context.Context, backend chain.Backend, privateKey *ecdsa.PrivateKey, ownerAddress common.Address, depositAmount *uint256.Uint256) (cswap.Contract, error) { opts := bind.NewKeyedTransactor(privateKey) @@ -248,9 +224,6 @@ func testDeployWithPrivateKey(ctx context.Context, backend chain.Backend, privat return nil, err } - // setup the wait for mined transaction function for testing - cleanup := setupContractTest() - defer cleanup() contract, err := factory.DeploySimpleSwap(opts, ownerAddress, big.NewInt(int64(defaultHarddepositTimeoutDuration))) if err != nil { return nil, err @@ -315,3 +288,41 @@ func (d *dummyMsgRW) ReadMsg() (p2p.Msg, error) { func (d *dummyMsgRW) WriteMsg(msg p2p.Msg) error { return nil } + +type cashChequeDoneData struct { + request *CashoutRequest + result *contract.CashChequeResult + receipt *types.Receipt +} + +type testCashoutResultHandler struct { + swap *Swap + cashChequeDone chan cashChequeDoneData +} + +func newTestCashoutResultHandler(swap *Swap) *testCashoutResultHandler { + return &testCashoutResultHandler{ + swap: swap, + cashChequeDone: make(chan cashChequeDoneData), + } +} + +func (h *testCashoutResultHandler) HandleCashoutResult(request *CashoutRequest, result *contract.CashChequeResult, receipt *types.Receipt) error { + if h.swap != nil { + if err := h.swap.HandleCashoutResult(request, result, receipt); err != nil { + return err + } + } + h.cashChequeDone <- cashChequeDoneData{ + request: request, + result: result, + receipt: receipt, + } + return nil +} + +func overrideCashoutResultHandler(swap *Swap) *testCashoutResultHandler { + cashoutResultHandler := newTestCashoutResultHandler(swap) + swap.cashoutProcessor.cashoutResultHandler = cashoutResultHandler + return cashoutResultHandler +} diff --git a/swap/protocol_test.go b/swap/protocol_test.go index ca120ae9d6..56f2a59b99 100644 --- a/swap/protocol_test.go +++ b/swap/protocol_test.go @@ -49,7 +49,6 @@ type swapTester struct { swap *Swap } -// creates a new protocol tester for swap with a deployed chequebook func newSwapTester(t *testing.T, backend *swapTestBackend, depositAmount *uint256.Uint256) (*swapTester, func(), error) { swap, clean := newTestSwap(t, ownerKey, backend) @@ -229,14 +228,11 @@ func TestEmitCheque(t *testing.T) { t.Fatal(err) } creditorSwap := protocolTester.swap + cashoutHandler := overrideCashoutResultHandler(creditorSwap) debitorSwap, cleanDebitorSwap := newTestSwap(t, beneficiaryKey, testBackend) defer cleanDebitorSwap() - // setup the wait for mined transaction function for testing - cleanup := setupContractTest() - defer cleanup() - log.Debug("deploy to simulated backend") // cashCheque cashes a cheque when the reward of doing so is twice the transaction costs. @@ -317,7 +313,7 @@ func TestEmitCheque(t *testing.T) { // we wait until the cashCheque is actually terminated (ensures proper nonce count) select { - case <-testBackend.cashDone: + case <-cashoutHandler.cashChequeDone: log.Debug("cash transaction completed and committed") case <-time.After(4 * time.Second): t.Fatalf("Timeout waiting for cash transaction to complete") @@ -340,9 +336,6 @@ func TestTriggerPaymentThreshold(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - // setup the wait for mined transaction function for testing - cleanup := setupContractTest() - defer cleanup() if err = protocolTester.testHandshake( correctSwapHandshakeMsg(debitorSwap), diff --git a/swap/simulations_test.go b/swap/simulations_test.go index e2222da8b7..38fdb3ef29 100644 --- a/swap/simulations_test.go +++ b/swap/simulations_test.go @@ -47,6 +47,7 @@ import ( "github.com/ethersphere/swarm/network/simulation" "github.com/ethersphere/swarm/p2p/protocols" "github.com/ethersphere/swarm/state" + "github.com/ethersphere/swarm/swap/chain" mock "github.com/ethersphere/swarm/swap/chain/mock" "github.com/ethersphere/swarm/uint256" ) @@ -62,6 +63,7 @@ For integration tests, run test cluster deployments with all integration moduele (blockchains, oracles, etc.) */ // swapSimulationParams allows to avoid global variables for the test + type swapSimulationParams struct { swaps map[int]*Swap dirs map[int]string @@ -165,9 +167,10 @@ func newSimServiceMap(params *swapSimulationParams) map[string]simulation.Servic if err != nil { return nil, nil, err } + ts.swap.cashoutProcessor.txScheduler.Start() cleanup = func() { - ts.swap.store.Close() + ts.swap.Close() os.RemoveAll(dir) } @@ -238,7 +241,6 @@ func newSharedBackendSwaps(t *testing.T, nodeCount int) (*swapSimulationParams, TestBackend: mock.NewTestBackend(defaultBackend), factoryAddress: factoryAddress, tokenAddress: tokenAddress, - cashDone: make(chan struct{}), } // finally, create all Swap instances for each node, which share the same backend var owner *Owner @@ -249,7 +251,8 @@ func newSharedBackendSwaps(t *testing.T, nodeCount int) (*swapSimulationParams, if err != nil { t.Fatal(err) } - params.swaps[i] = newSwapInstance(stores[i], owner, testBackend, 10, defParams, factory) + txqueue := chain.NewTxQueue(stores[i], "chain", testBackend, owner.privateKey) + params.swaps[i] = newSwapInstance(stores[i], owner, testBackend, 10, defParams, factory, txqueue) } params.backend = testBackend @@ -271,10 +274,6 @@ func TestMultiChequeSimulation(t *testing.T) { // cleanup backend defer params.backend.Close() - // setup the wait for mined transaction function for testing - cleanup := setupContractTest() - defer cleanup() - // initialize the simulation sim := simulation.NewBzzInProc(newSimServiceMap(params), false) defer sim.Close() @@ -306,6 +305,8 @@ func TestMultiChequeSimulation(t *testing.T) { // get the testService for the creditor creditorSvc := sim.Service("swap", creditor).(*testService) + cashoutHandler := overrideCashoutResultHandler(creditorSvc.swap) + var debLen, credLen, debSwapLen, credSwapLen int timeout := time.After(10 * time.Second) for { @@ -384,7 +385,7 @@ func TestMultiChequeSimulation(t *testing.T) { balanceAfterMessage := debitorBalance - int64(msgPrice) if balanceAfterMessage <= -paymentThreshold { // we need to wait a bit in order to give time for the cheque to be processed - if err := waitForChequeProcessed(t, params.backend, counter, lastCount, debitorSvc.swap.peers[creditor], expectedPayout); err != nil { + if err := waitForChequeProcessed(t, params.backend, counter, lastCount, debitorSvc.swap.peers[creditor], expectedPayout, cashoutHandler.cashChequeDone); err != nil { t.Fatal(err) } expectedPayout += uint64(-balanceAfterMessage) @@ -620,7 +621,7 @@ func TestBasicSwapSimulation(t *testing.T) { log.Info("Simulation ended") } -func waitForChequeProcessed(t *testing.T, backend *swapTestBackend, counter metrics.Counter, lastCount int64, p *Peer, expectedLastPayout uint64) error { +func waitForChequeProcessed(t *testing.T, backend *swapTestBackend, counter metrics.Counter, lastCount int64, p *Peer, expectedLastPayout uint64, cashChequeDone chan cashChequeDoneData) error { t.Helper() ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel() @@ -643,7 +644,7 @@ func waitForChequeProcessed(t *testing.T, backend *swapTestBackend, counter metr lock.Unlock() wg.Done() return - case <-backend.cashDone: + case <-cashChequeDone: wg.Done() return } diff --git a/swap/swap.go b/swap/swap.go index 274c3a29cd..0d505b4b5d 100644 --- a/swap/swap.go +++ b/swap/swap.go @@ -29,6 +29,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/console" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/log" @@ -68,6 +69,7 @@ type Swap struct { chequebookFactory contract.SimpleSwapFactory // the chequebook factory used honeyPriceOracle HoneyOracle // oracle which resolves the price of honey (in Wei) cashoutProcessor *CashoutProcessor // processor for cashing out + txScheduler chain.TxScheduler // transaction scheduler to use } // Owner encapsulates information related to accessing the contract @@ -136,8 +138,8 @@ func swapRotatingFileHandler(logdir string) (log.Handler, error) { } // newSwapInstance is a swap constructor function without integrity checks -func newSwapInstance(stateStore state.Store, owner *Owner, backend chain.Backend, chainID uint64, params *Params, chequebookFactory contract.SimpleSwapFactory) *Swap { - return &Swap{ +func newSwapInstance(stateStore state.Store, owner *Owner, backend chain.Backend, chainID uint64, params *Params, chequebookFactory contract.SimpleSwapFactory, txScheduler chain.TxScheduler) *Swap { + s := &Swap{ store: stateStore, peers: make(map[enode.ID]*Peer), backend: backend, @@ -146,8 +148,10 @@ func newSwapInstance(stateStore state.Store, owner *Owner, backend chain.Backend chequebookFactory: chequebookFactory, honeyPriceOracle: NewHoneyPriceOracle(), chainID: chainID, - cashoutProcessor: newCashoutProcessor(backend, owner.privateKey), + txScheduler: txScheduler, } + s.cashoutProcessor = newCashoutProcessor(txScheduler, backend, owner.privateKey, s) + return s } // New prepares and creates all fields to create a swap instance: @@ -209,12 +213,15 @@ func New(dbPath string, prvkey *ecdsa.PrivateKey, backendURL string, params *Par chainID.Uint64(), params, factory, + chain.NewTxQueue(stateStore, "chain", backend, owner.privateKey), ) // start the chequebook if swap.contract, err = swap.StartChequebook(chequebookAddressFlag); err != nil { return nil, err } + swap.txScheduler.Start() + // deposit money in the chequebook if desired if !skipDepositFlag { // prompt the user for a depositAmount @@ -391,8 +398,6 @@ func (s *Swap) handleMsg(p *Peer) func(ctx context.Context, msg interface{}) err } } -var defaultCashCheque = cashCheque - // handleEmitChequeMsg should be handled by the creditor when it receives // a cheque from a debitor func (s *Swap) handleEmitChequeMsg(ctx context.Context, p *Peer, msg *EmitChequeMsg) error { @@ -435,21 +440,10 @@ func (s *Swap) handleEmitChequeMsg(ctx context.Context, p *Peer, msg *EmitCheque return protocols.Break(err) } - expectedPayout, transactionCosts, err := s.cashoutProcessor.estimatePayout(context.TODO(), cheque) - if err != nil { - return protocols.Break(err) - } - - costsMultiplier := uint256.FromUint64(2) - costThreshold, err := uint256.New().Mul(transactionCosts, costsMultiplier) - if err != nil { - return err - } - - // do a payout transaction if we get 2 times the gas costs - if expectedPayout.Cmp(costThreshold) == 1 { - go defaultCashCheque(s, cheque) - } + s.cashoutProcessor.submitCheque(ctx, &CashoutRequest{ + Cheque: *cheque, + Destination: s.GetParams().ContractAddress, + }) return nil } @@ -489,20 +483,6 @@ func (s *Swap) handleConfirmChequeMsg(ctx context.Context, p *Peer, msg *Confirm return nil } -// cashCheque should be called async as it blocks until the transaction(s) are mined -// The function cashes the cheque by sending it to the blockchain -func cashCheque(s *Swap, cheque *Cheque) { - err := s.cashoutProcessor.cashCheque(context.Background(), &CashoutRequest{ - Cheque: *cheque, - Destination: s.GetParams().ContractAddress, - }) - - if err != nil { - metrics.GetOrRegisterCounter("swap.cheques.cashed.errors", nil).Inc(1) - swapLog.Error("cashing cheque:", "error", err) - } -} - // processAndVerifyCheque verifies the cheque and compares it with the last received cheque // if the cheque is valid it will also be saved as the new last cheque // the caller is expected to hold p.lock @@ -613,6 +593,7 @@ func (s *Swap) saveBalance(p enode.ID, balance int64) error { // Close cleans up swap func (s *Swap) Close() error { + s.txScheduler.Stop() return s.store.Close() } @@ -737,3 +718,15 @@ func (s *Swap) loadChequebook() (common.Address, error) { func (s *Swap) saveChequebook(chequebook common.Address) error { return s.store.Put(connectedChequebookKey, chequebook) } + +func (s *Swap) HandleCashoutResult(request *CashoutRequest, result *contract.CashChequeResult, receipt *types.Receipt) error { + metrics.GetOrRegisterCounter("swap.cheques.cashed.honey", nil).Inc(result.TotalPayout.Int64()) + + if result.Bounced { + metrics.GetOrRegisterCounter("swap.cheques.cashed.bounced", nil).Inc(1) + swapLog.Warn("cheque bounced", "tx", receipt.TxHash) + } + + swapLog.Info("cheque cashed", "cheque", &request.Cheque) + return nil +} diff --git a/swap/swap_test.go b/swap/swap_test.go index 01bf17d142..6f5fa94104 100644 --- a/swap/swap_test.go +++ b/swap/swap_test.go @@ -603,6 +603,7 @@ func TestResetBalance(t *testing.T) { defer testBackend.Close() // create both test swap accounts creditorSwap, clean1 := newTestSwap(t, beneficiaryKey, testBackend) + cashoutHandler := overrideCashoutResultHandler(creditorSwap) debitorSwap, clean2 := newTestSwap(t, ownerKey, testBackend) defer clean1() defer clean2() @@ -641,10 +642,6 @@ func TestResetBalance(t *testing.T) { t.Fatal(err) } - // setup the wait for mined transaction function for testing - cleanup := setupContractTest() - defer cleanup() - // now simulate sending the cheque to the creditor from the debitor if err = creditor.sendCheque(); err != nil { t.Fatal(err) @@ -663,20 +660,18 @@ func TestResetBalance(t *testing.T) { if cheque == nil { t.Fatal("expected to find a cheque, but it was empty") } - // ...create a message... - msg := &EmitChequeMsg{ - Cheque: cheque, - } // ...and trigger message handling on the receiver side (creditor) // remember that debitor is the model of the remote node for the creditor... - err = creditorSwap.handleEmitChequeMsg(ctx, debitor, msg) + err = creditorSwap.handleEmitChequeMsg(ctx, debitor, &EmitChequeMsg{ + Cheque: cheque, + }) if err != nil { t.Fatal(err) } // ...on which we wait until the cashCheque is actually terminated (ensures proper nonce count) select { - case <-testBackend.cashDone: + case <-cashoutHandler.cashChequeDone: log.Debug("cash transaction completed and committed") case <-time.After(4 * time.Second): t.Fatalf("Timeout waiting for cash transactions to complete") @@ -692,8 +687,6 @@ func TestResetBalance(t *testing.T) { func TestDebtCheques(t *testing.T) { testBackend := newTestBackend(t) defer testBackend.Close() - cleanup := setupContractTest() - defer cleanup() creditorSwap, cleanup := newTestSwap(t, beneficiaryKey, testBackend) defer cleanup() @@ -745,14 +738,6 @@ func TestDebtCheques(t *testing.T) { if err != nil { t.Fatal(err) } - - // ...on which we wait until the cashCheque is actually terminated (ensures proper nonce count) - select { - case <-testBackend.cashDone: - log.Debug("cash transaction completed and committed") - case <-time.After(4 * time.Second): - t.Fatalf("Timeout waiting for cash transactions to complete") - } } // generate bookings based on parameters, apply them to a Swap struct and verify the result @@ -1303,10 +1288,6 @@ func TestSwapLogToFile(t *testing.T) { t.Fatal(err) } - // setup the wait for mined transaction function for testing - cleanup := setupContractTest() - defer cleanup() - // now simulate sending the cheque to the creditor from the debitor if err = creditor.sendCheque(); err != nil { t.Fatal(err) @@ -1360,8 +1341,6 @@ func TestAvailableBalance(t *testing.T) { defer testBackend.Close() swap, clean := newTestSwap(t, ownerKey, testBackend) defer clean() - cleanup := setupContractTest() - defer cleanup() depositAmount := uint256.FromUint64(9000 * RetrieveRequestPrice)