Skip to content
This repository was archived by the owner on Aug 2, 2021. It is now read-only.

Commit 5c9e792

Browse files
committed
swap, swap/chain: use txqueue for cashout
1 parent 2e02111 commit 5c9e792

File tree

7 files changed

+198
-181
lines changed

7 files changed

+198
-181
lines changed

swap/cashout.go

Lines changed: 80 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121

2222
"github.com/ethereum/go-ethereum/accounts/abi/bind"
2323
"github.com/ethereum/go-ethereum/common"
24+
"github.com/ethereum/go-ethereum/core/types"
2425
"github.com/ethereum/go-ethereum/metrics"
2526
contract "github.com/ethersphere/swarm/contracts/swap"
2627
"github.com/ethersphere/swarm/swap/chain"
@@ -30,10 +31,9 @@ import (
3031
// CashChequeBeneficiaryTransactionCost is the expected gas cost of a CashChequeBeneficiary transaction
3132
const CashChequeBeneficiaryTransactionCost = 50000
3233

33-
// CashoutProcessor holds all relevant fields needed for processing cashouts
34-
type CashoutProcessor struct {
35-
backend chain.Backend // ethereum backend to use
36-
privateKey *ecdsa.PrivateKey // private key to use
34+
var CashoutRequestTypeID = chain.TxRequestTypeID{
35+
Handler: "cashout",
36+
RequestType: "CashoutRequest",
3737
}
3838

3939
// CashoutRequest represents a request for a cashout operation
@@ -42,42 +42,94 @@ type CashoutRequest struct {
4242
Destination common.Address // destination for the payout
4343
}
4444

45-
// ActiveCashout stores the necessary information for a cashout in progess
46-
type ActiveCashout struct {
47-
Request CashoutRequest // the request that caused this cashout
48-
TransactionHash common.Hash // the hash of the current transaction for this request
45+
// CashoutProcessor holds all relevant fields needed for processing cashouts
46+
type CashoutProcessor struct {
47+
backend chain.Backend // ethereum backend to use
48+
txScheduler chain.TxScheduler // transaction queue to use
49+
cashoutResultHandler CashoutResultHandler
50+
cashoutDone chan *CashoutRequest
51+
}
52+
53+
type CashoutResultHandler interface {
54+
HandleCashoutResult(request *CashoutRequest, result *contract.CashChequeResult, receipt *types.Receipt) error
4955
}
5056

5157
// newCashoutProcessor creates a new instance of CashoutProcessor
52-
func newCashoutProcessor(backend chain.Backend, privateKey *ecdsa.PrivateKey) *CashoutProcessor {
53-
return &CashoutProcessor{
54-
backend: backend,
55-
privateKey: privateKey,
58+
func newCashoutProcessor(txScheduler chain.TxScheduler, backend chain.Backend, privateKey *ecdsa.PrivateKey, cashoutResultHandler CashoutResultHandler) *CashoutProcessor {
59+
c := &CashoutProcessor{
60+
backend: backend,
61+
txScheduler: txScheduler,
62+
cashoutResultHandler: cashoutResultHandler,
5663
}
57-
}
5864

59-
// cashCheque tries to cash the cheque specified in the request
60-
// after the transaction is sent it waits on its success
61-
func (c *CashoutProcessor) cashCheque(ctx context.Context, request *CashoutRequest) error {
62-
cheque := request.Cheque
63-
opts := bind.NewKeyedTransactor(c.privateKey)
64-
opts.Context = ctx
65+
txScheduler.SetHandlers(CashoutRequestTypeID, &chain.TxRequestHandlers{
66+
Send: func(id uint64, backend chain.Backend, opts *bind.TransactOpts) (common.Hash, error) {
67+
var request CashoutRequest
68+
if err := c.txScheduler.GetRequest(id, &request); err != nil {
69+
return common.Hash{}, err
70+
}
71+
72+
cheque := request.Cheque
73+
74+
otherSwap, err := contract.InstanceAt(cheque.Contract, backend)
75+
if err != nil {
76+
return common.Hash{}, err
77+
}
78+
79+
tx, err := otherSwap.CashChequeBeneficiaryStart(opts, request.Destination, cheque.CumulativePayout, cheque.Signature)
80+
if err != nil {
81+
return common.Hash{}, err
82+
}
83+
return tx.Hash(), nil
84+
},
85+
NotifyReceipt: func(ctx context.Context, id uint64, notification *chain.TxReceiptNotification) error {
86+
var request *CashoutRequest
87+
err := c.txScheduler.GetRequest(id, &request)
88+
if err != nil {
89+
return err
90+
}
91+
92+
otherSwap, err := contract.InstanceAt(request.Cheque.Contract, c.backend)
93+
if err != nil {
94+
return err
95+
}
96+
97+
receipt := &notification.Receipt
98+
if receipt.Status == 0 {
99+
swapLog.Error("cheque cashing transaction reverted", "tx", receipt.TxHash)
100+
return nil
101+
}
102+
103+
result := otherSwap.CashChequeBeneficiaryResult(receipt)
104+
return c.cashoutResultHandler.HandleCashoutResult(request, result, receipt)
105+
},
106+
})
107+
return c
108+
}
65109

66-
otherSwap, err := contract.InstanceAt(cheque.Contract, c.backend)
110+
func (c *CashoutProcessor) submitCheque(ctx context.Context, request *CashoutRequest) {
111+
expectedPayout, transactionCosts, err := c.estimatePayout(ctx, &request.Cheque)
67112
if err != nil {
68-
return err
113+
swapLog.Error("could not estimate payout", "error", err)
114+
return
69115
}
70116

71-
tx, err := otherSwap.CashChequeBeneficiaryStart(opts, request.Destination, cheque.CumulativePayout, cheque.Signature)
117+
costsMultiplier := uint256.FromUint64(2)
118+
costThreshold, err := uint256.New().Mul(transactionCosts, costsMultiplier)
72119
if err != nil {
73-
return err
120+
swapLog.Error("overflow in transaction fee", "error", err)
121+
return
74122
}
75123

76-
// this blocks until the cashout has been successfully processed
77-
return c.waitForAndProcessActiveCashout(&ActiveCashout{
78-
Request: *request,
79-
TransactionHash: tx.Hash(),
80-
})
124+
// do a payout transaction if we get 2 times the gas costs
125+
if expectedPayout.Cmp(costThreshold) == 1 {
126+
swapLog.Info("queueing cashout", "cheque", &request.Cheque)
127+
_, err := c.txScheduler.ScheduleRequest(CashoutRequestTypeID, request)
128+
if err != nil {
129+
metrics.GetOrRegisterCounter("swap.cheques.cashed.errors", nil).Inc(1)
130+
swapLog.Error("cashing cheque:", "error", err)
131+
}
132+
}
81133
}
82134

83135
// 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) (
123175

124176
return expectedPayout, transactionCosts, nil
125177
}
126-
127-
// waitForAndProcessActiveCashout waits for activeCashout to complete
128-
func (c *CashoutProcessor) waitForAndProcessActiveCashout(activeCashout *ActiveCashout) error {
129-
ctx, cancel := context.WithTimeout(context.Background(), DefaultTransactionTimeout)
130-
defer cancel()
131-
132-
receipt, err := chain.WaitMined(ctx, c.backend, activeCashout.TransactionHash)
133-
if err != nil {
134-
return err
135-
}
136-
137-
otherSwap, err := contract.InstanceAt(activeCashout.Request.Cheque.Contract, c.backend)
138-
if err != nil {
139-
return err
140-
}
141-
142-
result := otherSwap.CashChequeBeneficiaryResult(receipt)
143-
144-
metrics.GetOrRegisterCounter("swap.cheques.cashed.honey", nil).Inc(result.TotalPayout.Int64())
145-
146-
if result.Bounced {
147-
metrics.GetOrRegisterCounter("swap.cheques.cashed.bounced", nil).Inc(1)
148-
swapLog.Warn("cheque bounced", "tx", receipt.TxHash)
149-
}
150-
151-
swapLog.Info("cheque cashed", "honey", activeCashout.Request.Cheque.Honey)
152-
return nil
153-
}

swap/cashout_test.go

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@ package swap
1919
import (
2020
"context"
2121
"testing"
22+
"time"
2223

2324
"github.com/ethereum/go-ethereum/accounts/abi/bind"
2425
"github.com/ethereum/go-ethereum/log"
26+
"github.com/ethersphere/swarm/state"
2527
"github.com/ethersphere/swarm/swap/chain"
2628
"github.com/ethersphere/swarm/uint256"
2729
)
@@ -33,8 +35,7 @@ import (
3335
// afterwards it attempts to cash-in a bouncing cheque
3436
func TestContractIntegration(t *testing.T) {
3537
backend := newTestBackend(t)
36-
reset := setupContractTest()
37-
defer reset()
38+
defer backend.Close()
3839

3940
payout := uint256.FromUint64(42)
4041
chequebook, err := testDeployWithPrivateKey(context.Background(), backend, ownerKey, ownerAddress, payout)
@@ -116,11 +117,18 @@ func TestContractIntegration(t *testing.T) {
116117
// TestCashCheque creates a valid cheque and feeds it to cashoutProcessor.cashCheque
117118
func TestCashCheque(t *testing.T) {
118119
backend := newTestBackend(t)
119-
reset := setupContractTest()
120-
defer reset()
120+
defer backend.Close()
121121

122-
cashoutProcessor := newCashoutProcessor(backend, ownerKey)
123-
payout := uint256.FromUint64(42)
122+
store := state.NewInmemoryStore()
123+
defer store.Close()
124+
125+
transactionQueue := chain.NewTxQueue(store, "queue", backend, ownerKey)
126+
transactionQueue.Start()
127+
defer transactionQueue.Stop()
128+
129+
cashoutHandler := newTestCashoutResultHandler(nil)
130+
cashoutProcessor := newCashoutProcessor(transactionQueue, backend, ownerKey, cashoutHandler)
131+
payout := uint256.FromUint64(CashChequeBeneficiaryTransactionCost*2 + 1)
124132

125133
chequebook, err := testDeployWithPrivateKey(context.Background(), backend, ownerKey, ownerAddress, payout)
126134
if err != nil {
@@ -132,12 +140,14 @@ func TestCashCheque(t *testing.T) {
132140
t.Fatal(err)
133141
}
134142

135-
err = cashoutProcessor.cashCheque(context.Background(), &CashoutRequest{
143+
cashoutProcessor.submitCheque(context.Background(), &CashoutRequest{
136144
Cheque: *testCheque,
137145
Destination: ownerAddress,
138146
})
139-
if err != nil {
140-
t.Fatal(err)
147+
148+
select {
149+
case <-cashoutHandler.cashChequeDone:
150+
case <-time.After(5 * time.Second):
141151
}
142152

143153
paidOut, err := chequebook.PaidOut(nil, ownerAddress)
@@ -154,12 +164,18 @@ func TestCashCheque(t *testing.T) {
154164
// TestEstimatePayout creates a valid cheque and feeds it to cashoutProcessor.estimatePayout
155165
func TestEstimatePayout(t *testing.T) {
156166
backend := newTestBackend(t)
157-
reset := setupContractTest()
158-
defer reset()
167+
defer backend.Close()
159168

160-
cashoutProcessor := newCashoutProcessor(backend, ownerKey)
161-
payout := uint256.FromUint64(42)
169+
store := state.NewInmemoryStore()
170+
defer store.Close()
171+
172+
transactionQueue := chain.NewTxQueue(store, "queue", backend, ownerKey)
173+
transactionQueue.Start()
174+
defer transactionQueue.Stop()
162175

176+
cashoutProcessor := newCashoutProcessor(transactionQueue, backend, ownerKey, &testCashoutResultHandler{})
177+
178+
payout := uint256.FromUint64(42)
163179
chequebook, err := testDeployWithPrivateKey(context.Background(), backend, ownerKey, ownerAddress, payout)
164180
if err != nil {
165181
t.Fatal(err)

swap/common_test.go

Lines changed: 44 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@ import (
1515
"github.com/ethereum/go-ethereum/accounts/abi/bind/backends"
1616
"github.com/ethereum/go-ethereum/common"
1717
"github.com/ethereum/go-ethereum/core"
18+
"github.com/ethereum/go-ethereum/core/types"
1819
"github.com/ethereum/go-ethereum/log"
1920
"github.com/ethereum/go-ethereum/p2p"
2021
"github.com/ethereum/go-ethereum/p2p/simulations/adapters"
2122
contractFactory "github.com/ethersphere/go-sw3/contracts-v0-2-0/simpleswapfactory"
23+
contract "github.com/ethersphere/swarm/contracts/swap"
2224
cswap "github.com/ethersphere/swarm/contracts/swap"
2325
"github.com/ethersphere/swarm/network"
2426
"github.com/ethersphere/swarm/p2p/protocols"
@@ -34,8 +36,6 @@ type swapTestBackend struct {
3436
*mock.TestBackend
3537
factoryAddress common.Address // address of the SimpleSwapFactory in the simulated network
3638
tokenAddress common.Address // address of the token in the simulated network
37-
// the async cashing go routine needs synchronization for tests
38-
cashDone chan struct{}
3939
}
4040

4141
var defaultBackend = backends.NewSimulatedBackend(core.GenesisAlloc{
@@ -67,7 +67,6 @@ func newTestBackend(t *testing.T) *swapTestBackend {
6767
TestBackend: backend,
6868
factoryAddress: factoryAddress,
6969
tokenAddress: tokenAddress,
70-
cashDone: make(chan struct{}),
7170
}
7271
}
7372

@@ -105,7 +104,9 @@ func newBaseTestSwapWithParams(t *testing.T, key *ecdsa.PrivateKey, params *Para
105104
if err != nil {
106105
t.Fatal(err)
107106
}
108-
swap := newSwapInstance(stateStore, owner, backend, 10, params, factory)
107+
108+
txqueue := chain.NewTxQueue(stateStore, "chain", backend, owner.privateKey)
109+
swap := newSwapInstance(stateStore, owner, backend, 10, params, factory, txqueue)
109110
return swap, dir
110111
}
111112

@@ -126,6 +127,7 @@ func newTestSwap(t *testing.T, key *ecdsa.PrivateKey, backend *swapTestBackend)
126127
usedBackend = newTestBackend(t)
127128
}
128129
swap, dir := newBaseTestSwap(t, key, usedBackend)
130+
swap.txScheduler.Start()
129131
clean := func() {
130132
swap.Close()
131133
// only close if created by newTestSwap to avoid double close
@@ -206,32 +208,6 @@ func newRandomTestCheque() *Cheque {
206208
return cheque
207209
}
208210

209-
// During tests, because the cashing in of cheques is async, we should wait for the function to be returned
210-
// Otherwise if we call `handleEmitChequeMsg` manually, it will return before the TX has been committed to the `SimulatedBackend`,
211-
// causing subsequent TX to possibly fail due to nonce mismatch
212-
func testCashCheque(s *Swap, cheque *Cheque) {
213-
cashCheque(s, cheque)
214-
// send to the channel, signals to clients that this function actually finished
215-
if stb, ok := s.backend.(*swapTestBackend); ok {
216-
if stb.cashDone != nil {
217-
stb.cashDone <- struct{}{}
218-
}
219-
}
220-
}
221-
222-
// setupContractTest is a helper function for setting up the
223-
// blockchain wait function for testing
224-
func setupContractTest() func() {
225-
// we also need to store the previous cashCheque function in case this is called multiple times
226-
currentCashCheque := defaultCashCheque
227-
defaultCashCheque = testCashCheque
228-
// overwrite only for the duration of the test, so...
229-
return func() {
230-
// ...we need to set it back to original when done
231-
defaultCashCheque = currentCashCheque
232-
}
233-
}
234-
235211
// deploy for testing (needs simulated backend commit)
236212
func testDeployWithPrivateKey(ctx context.Context, backend chain.Backend, privateKey *ecdsa.PrivateKey, ownerAddress common.Address, depositAmount *uint256.Uint256) (cswap.Contract, error) {
237213
opts := bind.NewKeyedTransactor(privateKey)
@@ -248,9 +224,6 @@ func testDeployWithPrivateKey(ctx context.Context, backend chain.Backend, privat
248224
return nil, err
249225
}
250226

251-
// setup the wait for mined transaction function for testing
252-
cleanup := setupContractTest()
253-
defer cleanup()
254227
contract, err := factory.DeploySimpleSwap(opts, ownerAddress, big.NewInt(int64(defaultHarddepositTimeoutDuration)))
255228
if err != nil {
256229
return nil, err
@@ -315,3 +288,41 @@ func (d *dummyMsgRW) ReadMsg() (p2p.Msg, error) {
315288
func (d *dummyMsgRW) WriteMsg(msg p2p.Msg) error {
316289
return nil
317290
}
291+
292+
type cashChequeDoneData struct {
293+
request *CashoutRequest
294+
result *contract.CashChequeResult
295+
receipt *types.Receipt
296+
}
297+
298+
type testCashoutResultHandler struct {
299+
swap *Swap
300+
cashChequeDone chan cashChequeDoneData
301+
}
302+
303+
func newTestCashoutResultHandler(swap *Swap) *testCashoutResultHandler {
304+
return &testCashoutResultHandler{
305+
swap: swap,
306+
cashChequeDone: make(chan cashChequeDoneData),
307+
}
308+
}
309+
310+
func (h *testCashoutResultHandler) HandleCashoutResult(request *CashoutRequest, result *contract.CashChequeResult, receipt *types.Receipt) error {
311+
if h.swap != nil {
312+
if err := h.swap.HandleCashoutResult(request, result, receipt); err != nil {
313+
return err
314+
}
315+
}
316+
h.cashChequeDone <- cashChequeDoneData{
317+
request: request,
318+
result: result,
319+
receipt: receipt,
320+
}
321+
return nil
322+
}
323+
324+
func overrideCashoutResultHandler(swap *Swap) *testCashoutResultHandler {
325+
cashoutResultHandler := newTestCashoutResultHandler(swap)
326+
swap.cashoutProcessor.cashoutResultHandler = cashoutResultHandler
327+
return cashoutResultHandler
328+
}

0 commit comments

Comments
 (0)