diff --git a/Makefile b/Makefile index 7a25e666e..5ee3e5e78 100644 --- a/Makefile +++ b/Makefile @@ -46,7 +46,7 @@ statusgo-ios-simulator-mainnet: xgo build/env.sh $(GOBIN)/xgo --image farazdagi/xgo-ios-simulator --go=$(GO) -out statusgo --dest=$(GOBIN) --targets=ios-9.3/framework -v $(shell build/mainnet-flags.sh) ./cmd/statusd @echo "iOS framework cross compilation done (mainnet)." -ci: +ci: mock build/env.sh go test -timeout 40m -v ./geth/api build/env.sh go test -timeout 40m -v ./geth/common build/env.sh go test -timeout 40m -v ./geth/jail @@ -111,6 +111,12 @@ lint: @echo "Linter: gosimple\n--------------------" @gometalinter --disable-all --deadline 45s --enable=gosimple extkeys cmd/... geth/... | grep -v -f ./static/config/linter_exclude_list.txt || echo "OK!" +mock-install: + go get -u github.com/golang/mock/mockgen + +mock: mock-install + mockgen -source=geth/common/types.go -destination=geth/common/types_mock.go -package=common + test: @build/env.sh echo "mode: set" > coverage-all.out build/env.sh go test -coverprofile=coverage.out -covermode=set ./geth/api diff --git a/cmd/statusd/library.go b/cmd/statusd/library.go index b745e6ea5..108ab21f9 100644 --- a/cmd/statusd/library.go +++ b/cmd/statusd/library.go @@ -9,6 +9,7 @@ import ( "gopkg.in/go-playground/validator.v9" "github.com/status-im/status-go/geth/common" + "github.com/status-im/status-go/geth/log" "github.com/status-im/status-go/geth/params" "github.com/status-im/status-go/helpers/profiling" ) @@ -213,7 +214,7 @@ func Logout() *C.char { //export CompleteTransaction func CompleteTransaction(id, password *C.char) *C.char { - txHash, err := statusAPI.CompleteTransaction(C.GoString(id), C.GoString(password)) + txHash, err := statusAPI.CompleteTransaction(common.QueuedTxID(C.GoString(id)), C.GoString(password)) errString := "" if err != nil { @@ -226,7 +227,11 @@ func CompleteTransaction(id, password *C.char) *C.char { Hash: txHash.Hex(), Error: errString, } - outBytes, _ := json.Marshal(&out) + outBytes, err := json.Marshal(&out) + if err != nil { + log.Error("failed to marshal CompleteTransaction output", "error", err.Error()) + return makeJSONResponse(err) + } return C.CString(string(outBytes)) } @@ -236,25 +241,42 @@ func CompleteTransactions(ids, password *C.char) *C.char { out := common.CompleteTransactionsResult{} out.Results = make(map[string]common.CompleteTransactionResult) - results := statusAPI.CompleteTransactions(C.GoString(ids), C.GoString(password)) - for txID, result := range results { - txResult := common.CompleteTransactionResult{ - ID: txID, - Hash: result.Hash.Hex(), + parsedIDs, err := common.ParseJSONArray(C.GoString(ids)) + if err != nil { + out.Results["none"] = common.CompleteTransactionResult{ + Error: err.Error(), } - if result.Error != nil { - txResult.Error = result.Error.Error() + } else { + txIDs := make([]common.QueuedTxID, len(parsedIDs)) + for i, id := range parsedIDs { + txIDs[i] = common.QueuedTxID(id) + } + + results := statusAPI.CompleteTransactions(txIDs, C.GoString(password)) + for txID, result := range results { + txResult := common.CompleteTransactionResult{ + ID: string(txID), + Hash: result.Hash.Hex(), + } + if result.Error != nil { + txResult.Error = result.Error.Error() + } + out.Results[string(txID)] = txResult } - out.Results[txID] = txResult } - outBytes, _ := json.Marshal(&out) + + outBytes, err := json.Marshal(&out) + if err != nil { + log.Error("failed to marshal CompleteTransactions output", "error", err.Error()) + return makeJSONResponse(err) + } return C.CString(string(outBytes)) } //export DiscardTransaction func DiscardTransaction(id *C.char) *C.char { - err := statusAPI.DiscardTransaction(C.GoString(id)) + err := statusAPI.DiscardTransaction(common.QueuedTxID(C.GoString(id))) errString := "" if err != nil { @@ -266,7 +288,11 @@ func DiscardTransaction(id *C.char) *C.char { ID: C.GoString(id), Error: errString, } - outBytes, _ := json.Marshal(&out) + outBytes, err := json.Marshal(&out) + if err != nil { + log.Error("failed to marshal DiscardTransaction output", "error", err.Error()) + return makeJSONResponse(err) + } return C.CString(string(outBytes)) } @@ -276,17 +302,34 @@ func DiscardTransactions(ids *C.char) *C.char { out := common.DiscardTransactionsResult{} out.Results = make(map[string]common.DiscardTransactionResult) - results := statusAPI.DiscardTransactions(C.GoString(ids)) - for txID, result := range results { - txResult := common.DiscardTransactionResult{ - ID: txID, + parsedIDs, err := common.ParseJSONArray(C.GoString(ids)) + if err != nil { + out.Results["none"] = common.DiscardTransactionResult{ + Error: err.Error(), } - if result.Error != nil { - txResult.Error = result.Error.Error() + } else { + txIDs := make([]common.QueuedTxID, len(parsedIDs)) + for i, id := range parsedIDs { + txIDs[i] = common.QueuedTxID(id) + } + + results := statusAPI.DiscardTransactions(txIDs) + for txID, result := range results { + txResult := common.DiscardTransactionResult{ + ID: string(txID), + } + if result.Error != nil { + txResult.Error = result.Error.Error() + } + out.Results[string(txID)] = txResult } - out.Results[txID] = txResult } - outBytes, _ := json.Marshal(&out) + + outBytes, err := json.Marshal(&out) + if err != nil { + log.Error("failed to marshal DiscardTransactions output", "error", err.Error()) + return makeJSONResponse(err) + } return C.CString(string(outBytes)) } diff --git a/cmd/statusd/utils.go b/cmd/statusd/utils.go index cd3e31d3b..d13b50424 100644 --- a/cmd/statusd/utils.go +++ b/cmd/statusd/utils.go @@ -15,7 +15,6 @@ import ( gethcommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/les/status" gethparams "github.com/ethereum/go-ethereum/params" "github.com/status-im/status-go/geth/common" @@ -783,21 +782,15 @@ func testAccountLogout(t *testing.T) bool { } func testCompleteTransaction(t *testing.T) bool { - // obtain reference to status backend - lightEthereum, err := statusAPI.NodeManager().LightEthereumService() - if err != nil { - t.Errorf("Test failed: LES service is not running: %v", err) - return false - } - backend := lightEthereum.StatusBackend + txQueueManager := statusAPI.TxQueueManager() + txQueue := txQueueManager.TransactionQueue() - // reset queue - backend.TransactionQueue().Reset() + txQueue.Reset() time.Sleep(5 * time.Second) // allow to sync // log into account from which transactions will be sent - if err = statusAPI.SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password); err != nil { + if err := statusAPI.SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password); err != nil { t.Errorf("cannot select account: %v", TestConfig.Account1.Address) return false } @@ -811,7 +804,7 @@ func testCompleteTransaction(t *testing.T) bool { var txHash = "" node.SetDefaultNodeNotificationHandler(func(jsonEvent string) { var envelope node.SignalEnvelope - if err = json.Unmarshal([]byte(jsonEvent), &envelope); err != nil { + if err := json.Unmarshal([]byte(jsonEvent), &envelope); err != nil { t.Errorf("cannot unmarshal event's JSON: %s", jsonEvent) return } @@ -822,7 +815,7 @@ func testCompleteTransaction(t *testing.T) bool { completeTxResponse := common.CompleteTransactionResult{} rawResponse := CompleteTransaction(C.CString(event["id"].(string)), C.CString(TestConfig.Account1.Password)) - if err = json.Unmarshal([]byte(C.GoString(rawResponse)), &completeTxResponse); err != nil { + if err := json.Unmarshal([]byte(C.GoString(rawResponse)), &completeTxResponse); err != nil { t.Errorf("cannot decode RecoverAccount response (%s): %v", C.GoString(rawResponse), err) } @@ -838,29 +831,31 @@ func testCompleteTransaction(t *testing.T) bool { } }) - // this call blocks, up until Complete Transaction is called - txHashCheck, err := backend.SendTransaction(nil, status.SendTxArgs{ + // this call blocks, up until Complete Transaction is called + txCheckHash, err := statusAPI.SendTransaction(nil, common.SendTxArgs{ From: common.FromAddress(TestConfig.Account1.Address), To: common.ToAddress(TestConfig.Account2.Address), Value: (*hexutil.Big)(big.NewInt(1000000000000)), }) if err != nil { - t.Errorf("Test failed: cannot send transaction: %v", err) + t.Errorf("Failed to SendTransaction: %s", err) + return false } <-queuedTxCompleted // make sure that complete transaction handler completes its magic, before we proceed - if txHash != txHashCheck.Hex() { - t.Errorf("Transaction hash returned from SendTransaction is invalid: expected %s, got %s", txHashCheck.Hex(), txHash) + if txHash != txCheckHash.Hex() { + t.Errorf("Transaction hash returned from SendTransaction is invalid: expected %s, got %s", + txCheckHash.Hex(), txHash) return false } - if reflect.DeepEqual(txHashCheck, gethcommon.Hash{}) { + if reflect.DeepEqual(txCheckHash, gethcommon.Hash{}) { t.Error("Test failed: transaction was never queued or completed") return false } - if backend.TransactionQueue().Count() != 0 { + if txQueue.Count() != 0 { t.Error("tx queue must be empty at this point") return false } @@ -869,16 +864,8 @@ func testCompleteTransaction(t *testing.T) bool { } func testCompleteMultipleQueuedTransactions(t *testing.T) bool { - // obtain reference to status backend - lightEthereum, err := statusAPI.NodeManager().LightEthereumService() - if err != nil { - t.Errorf("Test failed: LES service is not running: %v", err) - return false - } - backend := lightEthereum.StatusBackend - - // reset queue - backend.TransactionQueue().Reset() + txQueue := statusAPI.TxQueueManager().TransactionQueue() + txQueue.Reset() // log into account from which transactions will be sent if err := statusAPI.SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password); err != nil { @@ -910,7 +897,7 @@ func testCompleteMultipleQueuedTransactions(t *testing.T) bool { // this call blocks, and should return when DiscardQueuedTransaction() for a given tx id is called sendTx := func() { - txHashCheck, err := backend.SendTransaction(nil, status.SendTxArgs{ + txHashCheck, err := statusAPI.SendTransaction(nil, common.SendTxArgs{ From: common.FromAddress(TestConfig.Account1.Address), To: common.ToAddress(TestConfig.Account2.Address), Value: (*hexutil.Big)(big.NewInt(1000000000000)), @@ -946,7 +933,7 @@ func testCompleteMultipleQueuedTransactions(t *testing.T) bool { } results := resultsStruct.Results - if len(results) != (testTxCount+1) || results["invalid-tx-id"].Error != status.ErrQueuedTxIDNotFound.Error() { + if len(results) != (testTxCount+1) || results["invalid-tx-id"].Error != node.ErrQueuedTxIDNotFound.Error() { t.Errorf("cannot complete txs: %v", results) return } @@ -971,7 +958,7 @@ func testCompleteMultipleQueuedTransactions(t *testing.T) bool { time.Sleep(1 * time.Second) // make sure that tx complete signal propagates for _, txID := range parsedIDs { - if backend.TransactionQueue().Has(status.QueuedTxID(txID)) { + if txQueue.Has(common.QueuedTxID(txID)) { t.Errorf("txqueue should not have test tx at this point (it should be completed): %s", txID) return } @@ -1001,7 +988,7 @@ func testCompleteMultipleQueuedTransactions(t *testing.T) bool { return false } - if backend.TransactionQueue().Count() != 0 { + if txQueue.Count() != 0 { t.Error("tx queue must be empty at this point") return false } @@ -1010,19 +997,11 @@ func testCompleteMultipleQueuedTransactions(t *testing.T) bool { } func testDiscardTransaction(t *testing.T) bool { - // obtain reference to status backend - lightEthereum, err := statusAPI.NodeManager().LightEthereumService() - if err != nil { - t.Errorf("Test failed: LES service is not running: %v", err) - return false - } - backend := lightEthereum.StatusBackend - - // reset queue - backend.TransactionQueue().Reset() + txQueue := statusAPI.TxQueueManager().TransactionQueue() + txQueue.Reset() // log into account from which transactions will be sent - if err = statusAPI.SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password); err != nil { + if err := statusAPI.SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password); err != nil { t.Errorf("cannot select account: %v", TestConfig.Account1.Address) return false } @@ -1036,7 +1015,7 @@ func testDiscardTransaction(t *testing.T) bool { txFailedEventCalled := false node.SetDefaultNodeNotificationHandler(func(jsonEvent string) { var envelope node.SignalEnvelope - if err = json.Unmarshal([]byte(jsonEvent), &envelope); err != nil { + if err := json.Unmarshal([]byte(jsonEvent), &envelope); err != nil { t.Errorf("cannot unmarshal event's JSON: %s", jsonEvent) return } @@ -1045,7 +1024,7 @@ func testDiscardTransaction(t *testing.T) bool { txID = event["id"].(string) t.Logf("transaction queued (will be discarded soon): {id: %s}\n", txID) - if !backend.TransactionQueue().Has(status.QueuedTxID(txID)) { + if !txQueue.Has(common.QueuedTxID(txID)) { t.Errorf("txqueue should still have test tx: %s", txID) return } @@ -1054,7 +1033,7 @@ func testDiscardTransaction(t *testing.T) bool { discardResponse := common.DiscardTransactionResult{} rawResponse := DiscardTransaction(C.CString(txID)) - if err = json.Unmarshal([]byte(C.GoString(rawResponse)), &discardResponse); err != nil { + if err := json.Unmarshal([]byte(C.GoString(rawResponse)), &discardResponse); err != nil { t.Errorf("cannot decode RecoverAccount response (%s): %v", C.GoString(rawResponse), err) } @@ -1064,14 +1043,14 @@ func testDiscardTransaction(t *testing.T) bool { } // try completing discarded transaction - _, err = statusAPI.CompleteTransaction(txID, TestConfig.Account1.Password) - if err != status.ErrQueuedTxIDNotFound { + _, err := statusAPI.CompleteTransaction(common.QueuedTxID(txID), TestConfig.Account1.Password) + if err != node.ErrQueuedTxIDNotFound { t.Error("expects tx not found, but call to CompleteTransaction succeeded") return } time.Sleep(1 * time.Second) // make sure that tx complete signal propagates - if backend.TransactionQueue().Has(status.QueuedTxID(txID)) { + if txQueue.Has(common.QueuedTxID(txID)) { t.Errorf("txqueue should not have test tx at this point (it should be discarded): %s", txID) return } @@ -1084,7 +1063,7 @@ func testDiscardTransaction(t *testing.T) bool { t.Logf("transaction return event received: {id: %s}\n", event["id"].(string)) receivedErrMessage := event["error_message"].(string) - expectedErrMessage := status.ErrQueuedTxDiscarded.Error() + expectedErrMessage := node.ErrQueuedTxDiscarded.Error() if receivedErrMessage != expectedErrMessage { t.Errorf("unexpected error message received: got %v", receivedErrMessage) return @@ -1100,13 +1079,13 @@ func testDiscardTransaction(t *testing.T) bool { } }) - // this call blocks, and should return when DiscardQueuedTransaction() is called - txHashCheck, err := backend.SendTransaction(nil, status.SendTxArgs{ + // this call blocks, and should return when DiscardQueuedTransaction() is called + txHashCheck, err := statusAPI.SendTransaction(nil, common.SendTxArgs{ From: common.FromAddress(TestConfig.Account1.Address), To: common.ToAddress(TestConfig.Account2.Address), Value: (*hexutil.Big)(big.NewInt(1000000000000)), }) - if err != status.ErrQueuedTxDiscarded { + if err != node.ErrQueuedTxDiscarded { t.Errorf("expected error not thrown: %v", err) return false } @@ -1116,7 +1095,7 @@ func testDiscardTransaction(t *testing.T) bool { return false } - if backend.TransactionQueue().Count() != 0 { + if txQueue.Count() != 0 { t.Error("tx queue must be empty at this point") return false } @@ -1130,16 +1109,8 @@ func testDiscardTransaction(t *testing.T) bool { } func testDiscardMultipleQueuedTransactions(t *testing.T) bool { - // obtain reference to status backend - lightEthereum, err := statusAPI.NodeManager().LightEthereumService() - if err != nil { - t.Errorf("Test failed: LES service is not running: %v", err) - return false - } - backend := lightEthereum.StatusBackend - - // reset queue - backend.TransactionQueue().Reset() + txQueue := statusAPI.TxQueueManager().TransactionQueue() + txQueue.Reset() // log into account from which transactions will be sent if err := statusAPI.SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password); err != nil { @@ -1166,7 +1137,7 @@ func testDiscardMultipleQueuedTransactions(t *testing.T) bool { txID = event["id"].(string) t.Logf("transaction queued (will be discarded soon): {id: %s}\n", txID) - if !backend.TransactionQueue().Has(status.QueuedTxID(txID)) { + if !txQueue.Has(common.QueuedTxID(txID)) { t.Errorf("txqueue should still have test tx: %s", txID) return } @@ -1179,7 +1150,7 @@ func testDiscardMultipleQueuedTransactions(t *testing.T) bool { t.Logf("transaction return event received: {id: %s}\n", event["id"].(string)) receivedErrMessage := event["error_message"].(string) - expectedErrMessage := status.ErrQueuedTxDiscarded.Error() + expectedErrMessage := node.ErrQueuedTxDiscarded.Error() if receivedErrMessage != expectedErrMessage { t.Errorf("unexpected error message received: got %v", receivedErrMessage) return @@ -1198,14 +1169,14 @@ func testDiscardMultipleQueuedTransactions(t *testing.T) bool { } }) - // this call blocks, and should return when DiscardQueuedTransaction() for a given tx id is called + // this call blocks, and should return when DiscardQueuedTransaction() for a given tx id is called sendTx := func() { - txHashCheck, err := backend.SendTransaction(nil, status.SendTxArgs{ + txHashCheck, err := statusAPI.SendTransaction(nil, common.SendTxArgs{ From: common.FromAddress(TestConfig.Account1.Address), To: common.ToAddress(TestConfig.Account2.Address), Value: (*hexutil.Big)(big.NewInt(1000000000000)), }) - if err != status.ErrQueuedTxDiscarded { + if err != node.ErrQueuedTxDiscarded { t.Errorf("expected error not thrown: %v", err) return } @@ -1236,7 +1207,7 @@ func testDiscardMultipleQueuedTransactions(t *testing.T) bool { } discardResults := discardResultsStruct.Results - if len(discardResults) != 1 || discardResults["invalid-tx-id"].Error != status.ErrQueuedTxIDNotFound.Error() { + if len(discardResults) != 1 || discardResults["invalid-tx-id"].Error != node.ErrQueuedTxIDNotFound.Error() { t.Errorf("cannot discard txs: %v", discardResults) return } @@ -1258,7 +1229,7 @@ func testDiscardMultipleQueuedTransactions(t *testing.T) bool { t.Errorf("tx id not set in result: expected id is %s", txID) return } - if txResult.Error != status.ErrQueuedTxIDNotFound.Error() { + if txResult.Error != node.ErrQueuedTxIDNotFound.Error() { t.Errorf("invalid error for %s", txResult.Hash) return } @@ -1270,7 +1241,7 @@ func testDiscardMultipleQueuedTransactions(t *testing.T) bool { time.Sleep(1 * time.Second) // make sure that tx complete signal propagates for _, txID := range parsedIDs { - if backend.TransactionQueue().Has(status.QueuedTxID(txID)) { + if txQueue.Has(common.QueuedTxID(txID)) { t.Errorf("txqueue should not have test tx at this point (it should be discarded): %s", txID) return } @@ -1299,7 +1270,7 @@ func testDiscardMultipleQueuedTransactions(t *testing.T) bool { return false } - if backend.TransactionQueue().Count() != 0 { + if txQueue.Count() != 0 { t.Error("tx queue must be empty at this point") return false } diff --git a/geth/api/api.go b/geth/api/api.go index 865cbb142..2ac1adf78 100644 --- a/geth/api/api.go +++ b/geth/api/api.go @@ -1,6 +1,8 @@ package api import ( + "context" + "github.com/ethereum/go-ethereum/accounts/keystore" gethcommon "github.com/ethereum/go-ethereum/common" "github.com/status-im/status-go/geth/common" @@ -34,6 +36,11 @@ func (api *StatusAPI) JailManager() common.JailManager { return api.b.JailManager() } +// TxQueueManager returns reference to account manager +func (api *StatusAPI) TxQueueManager() common.TxQueueManager { + return api.b.TxQueueManager() +} + // StartNode start Status node, fails if node is already started func (api *StatusAPI) StartNode(config *params.NodeConfig) error { nodeStarted, err := api.b.StartNode(config) @@ -141,24 +148,29 @@ func (api *StatusAPI) Logout() error { return api.b.AccountManager().Logout() } +// SendTransaction creates a new transaction and waits until it's complete. +func (api *StatusAPI) SendTransaction(ctx context.Context, args common.SendTxArgs) (gethcommon.Hash, error) { + return api.b.SendTransaction(ctx, args) +} + // CompleteTransaction instructs backend to complete sending of a given transaction -func (api *StatusAPI) CompleteTransaction(id, password string) (gethcommon.Hash, error) { - return api.b.CompleteTransaction(id, password) +func (api *StatusAPI) CompleteTransaction(id common.QueuedTxID, password string) (gethcommon.Hash, error) { + return api.b.txQueueManager.CompleteTransaction(id, password) } // CompleteTransactions instructs backend to complete sending of multiple transactions -func (api *StatusAPI) CompleteTransactions(ids, password string) map[string]common.RawCompleteTransactionResult { - return api.b.CompleteTransactions(ids, password) +func (api *StatusAPI) CompleteTransactions(ids []common.QueuedTxID, password string) map[common.QueuedTxID]common.RawCompleteTransactionResult { + return api.b.txQueueManager.CompleteTransactions(ids, password) } // DiscardTransaction discards a given transaction from transaction queue -func (api *StatusAPI) DiscardTransaction(id string) error { - return api.b.DiscardTransaction(id) +func (api *StatusAPI) DiscardTransaction(id common.QueuedTxID) error { + return api.b.txQueueManager.DiscardTransaction(id) } // DiscardTransactions discards given multiple transactions from transaction queue -func (api *StatusAPI) DiscardTransactions(ids string) map[string]common.RawDiscardTransactionResult { - return api.b.DiscardTransactions(ids) +func (api *StatusAPI) DiscardTransactions(ids []common.QueuedTxID) map[common.QueuedTxID]common.RawDiscardTransactionResult { + return api.b.txQueueManager.DiscardTransactions(ids) } // JailParse creates a new jail cell context, with the given chatID as identifier. diff --git a/geth/api/backend.go b/geth/api/backend.go index 10a417564..1e56d0d8c 100644 --- a/geth/api/backend.go +++ b/geth/api/backend.go @@ -1,6 +1,7 @@ package api import ( + "context" "sync" gethcommon "github.com/ethereum/go-ethereum/common" @@ -29,13 +30,14 @@ func NewStatusBackend() *StatusBackend { nodeManager := node.NewNodeManager() accountManager := node.NewAccountManager(nodeManager) + txQueueManager := node.NewTxQueueManager(nodeManager, accountManager) return &StatusBackend{ nodeManager: nodeManager, accountManager: accountManager, - jailManager: jail.New(nodeManager, accountManager), + jailManager: jail.New(nodeManager, accountManager, txQueueManager), rpcManager: node.NewRPCManager(nodeManager), - txQueueManager: node.NewTxQueueManager(nodeManager, accountManager), + txQueueManager: txQueueManager, } } @@ -54,6 +56,11 @@ func (m *StatusBackend) JailManager() common.JailManager { return m.jailManager } +// TxQueueManager returns reference to jail +func (m *StatusBackend) TxQueueManager() common.TxQueueManager { + return m.txQueueManager +} + // IsNodeRunning confirm that node is running func (m *StatusBackend) IsNodeRunning() bool { return m.nodeManager.IsNodeRunning() @@ -73,6 +80,8 @@ func (m *StatusBackend) StartNode(config *params.NodeConfig) (<-chan struct{}, e return nil, err } + m.txQueueManager.Start() + m.nodeReady = make(chan struct{}, 1) go m.onNodeStart(nodeStarted, m.nodeReady) // waits on nodeStarted, writes to backendReady @@ -112,6 +121,8 @@ func (m *StatusBackend) StopNode() (<-chan struct{}, error) { return nil, err } + m.txQueueManager.Stop() + backendStopped := make(chan struct{}, 1) go func() { <-nodeStopped @@ -172,23 +183,42 @@ func (m *StatusBackend) CallRPC(inputJSON string) string { return m.rpcManager.Call(inputJSON) } +// SendTransaction creates a new transaction and waits until it's complete. +func (m *StatusBackend) SendTransaction(ctx context.Context, args common.SendTxArgs) (gethcommon.Hash, error) { + if ctx == nil { + ctx = context.Background() + } + + tx := m.txQueueManager.CreateTransaction(ctx, args) + + if err := m.txQueueManager.QueueTransaction(tx); err != nil { + return gethcommon.Hash{}, err + } + + if err := m.txQueueManager.WaitForTransaction(tx); err != nil { + return gethcommon.Hash{}, err + } + + return tx.Hash, nil +} + // CompleteTransaction instructs backend to complete sending of a given transaction -func (m *StatusBackend) CompleteTransaction(id, password string) (gethcommon.Hash, error) { +func (m *StatusBackend) CompleteTransaction(id common.QueuedTxID, password string) (gethcommon.Hash, error) { return m.txQueueManager.CompleteTransaction(id, password) } // CompleteTransactions instructs backend to complete sending of multiple transactions -func (m *StatusBackend) CompleteTransactions(ids, password string) map[string]common.RawCompleteTransactionResult { +func (m *StatusBackend) CompleteTransactions(ids []common.QueuedTxID, password string) map[common.QueuedTxID]common.RawCompleteTransactionResult { return m.txQueueManager.CompleteTransactions(ids, password) } // DiscardTransaction discards a given transaction from transaction queue -func (m *StatusBackend) DiscardTransaction(id string) error { +func (m *StatusBackend) DiscardTransaction(id common.QueuedTxID) error { return m.txQueueManager.DiscardTransaction(id) } // DiscardTransactions discards given multiple transactions from transaction queue -func (m *StatusBackend) DiscardTransactions(ids string) map[string]common.RawDiscardTransactionResult { +func (m *StatusBackend) DiscardTransactions(ids []common.QueuedTxID) map[common.QueuedTxID]common.RawDiscardTransactionResult { return m.txQueueManager.DiscardTransactions(ids) } @@ -208,10 +238,10 @@ func (m *StatusBackend) registerHandlers() error { lightEthereum.StatusBackend.SetAccountsFilterHandler(m.accountManager.AccountsListRequestHandler()) log.Info("Registered handler", "fn", "AccountsFilterHandler") - lightEthereum.StatusBackend.SetTransactionQueueHandler(m.txQueueManager.TransactionQueueHandler()) + m.txQueueManager.SetTransactionQueueHandler(m.txQueueManager.TransactionQueueHandler()) log.Info("Registered handler", "fn", "TransactionQueueHandler") - lightEthereum.StatusBackend.SetTransactionReturnHandler(m.txQueueManager.TransactionReturnHandler()) + m.txQueueManager.SetTransactionReturnHandler(m.txQueueManager.TransactionReturnHandler()) log.Info("Registered handler", "fn", "TransactionReturnHandler") return nil diff --git a/geth/api/backend_jail_test.go b/geth/api/backend_jail_test.go index 5424659b7..f3818cf88 100644 --- a/geth/api/backend_jail_test.go +++ b/geth/api/backend_jail_test.go @@ -49,7 +49,7 @@ func (s *BackendTestSuite) TestJailSendQueuedTransaction() { }` txCompletedSuccessfully := make(chan struct{}) - txHashes := make(chan gethcommon.Hash) + txHashes := make(chan gethcommon.Hash, 1) // replace transaction notification handler requireMessageId := false @@ -67,9 +67,9 @@ func (s *BackendTestSuite) TestJailSendQueuedTransaction() { } else { require.Empty(messageId, "Message id is not required, but provided") } - log.Info("Transaction queued (will be completed shortly)", "id", event["id"].(string)) - txHash, err := s.backend.CompleteTransaction(event["id"].(string), TestConfig.Account1.Password) + txID := event["id"].(string) + txHash, err := s.backend.CompleteTransaction(common.QueuedTxID(txID), TestConfig.Account1.Password) require.NoError(err, "cannot complete queued transaction[%v]", event["id"]) log.Info("Transaction complete", "URL", "https://ropsten.etherscan.io/tx/%s"+txHash.Hex()) @@ -211,7 +211,8 @@ func (s *BackendTestSuite) TestContractDeployment() { var txHash gethcommon.Hash node.SetDefaultNodeNotificationHandler(func(jsonEvent string) { var envelope node.SignalEnvelope - err := json.Unmarshal([]byte(jsonEvent), &envelope) + var err error + err = json.Unmarshal([]byte(jsonEvent), &envelope) require.NoError(err, fmt.Sprintf("cannot unmarshal JSON: %s", jsonEvent)) if envelope.Type == node.EventTransactionQueued { @@ -222,8 +223,8 @@ func (s *BackendTestSuite) TestContractDeployment() { s.NoError(s.backend.AccountManager().SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password)) - var err error - txHash, err = s.backend.CompleteTransaction(event["id"].(string), TestConfig.Account1.Password) + txID := event["id"].(string) + txHash, err = s.backend.CompleteTransaction(common.QueuedTxID(txID), TestConfig.Account1.Password) if s.NoError(err, event["id"]) { s.T().Logf("contract transaction complete, URL: %s", "https://ropsten.etherscan.io/tx/"+txHash.Hex()) } @@ -240,9 +241,13 @@ func (s *BackendTestSuite) TestContractDeployment() { from: '` + TestConfig.Account1.Address + `', data: '0x6060604052341561000c57fe5b5b60a58061001b6000396000f30060606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680636ffa1caa14603a575bfe5b3415604157fe5b60556004808035906020019091905050606b565b6040518082815260200191505060405180910390f35b60008160020290505b9190505600a165627a7a72305820ccdadd737e4ac7039963b54cee5e5afb25fa859a275252bdcf06f653155228210029', gas: '` + strconv.Itoa(params.DefaultGas) + `' - }, function (e, contract){ + }, function (e, contract) { + // NOTE: The callback will fire twice! + // Once the contract has the transactionHash property set and once its deployed on an address. if (!e) { - responseValue = contract.transactionHash + if (!contract.address) { + responseValue = contract.transactionHash; + } } }) `) @@ -757,7 +762,8 @@ func (s *BackendTestSuite) TestJailVMPersistence() { //} //var txHash common.Hash - txHash, err := s.backend.CompleteTransaction(event["id"].(string), TestConfig.Account1.Password) + txID := event["id"].(string) + txHash, err := s.backend.CompleteTransaction(common.QueuedTxID(txID), TestConfig.Account1.Password) require.NoError(err, "cannot complete queued transaction[%v]: %v", event["id"], err) s.T().Logf("Transaction complete: https://ropsten.etherscan.io/tx/%s", txHash.Hex()) diff --git a/geth/api/backend_test.go b/geth/api/backend_test.go index 0381e47c7..ee5333684 100644 --- a/geth/api/backend_test.go +++ b/geth/api/backend_test.go @@ -102,6 +102,10 @@ func (s *BackendTestSuite) LightEthereumService() *les.LightEthereum { return lightEthereum } +func (s *BackendTestSuite) TxQueueManager() common.TxQueueManager { + return s.backend.TxQueueManager() +} + func (s *BackendTestSuite) RestartTestNode() { require := s.Require() require.NotNil(s.backend) @@ -365,12 +369,14 @@ func (s *BackendTestSuite) TestRaceConditions() { }, func(config *params.NodeConfig) { log.Info("CompleteTransactions()") - s.T().Logf("CompleteTransactions(), result: %v", s.backend.CompleteTransactions(`["id1","id2"]`, "password")) + ids := []common.QueuedTxID{"id1", "id2"} + s.T().Logf("CompleteTransactions(), result: %v", s.backend.CompleteTransactions(ids, "password")) progress <- struct{}{} }, func(config *params.NodeConfig) { log.Info("DiscardTransactions()") - s.T().Logf("DiscardTransactions(), result: %v", s.backend.DiscardTransactions(`["id1","id2"]`)) + ids := []common.QueuedTxID{"id1", "id2"} + s.T().Logf("DiscardTransactions(), result: %v", s.backend.DiscardTransactions(ids)) progress <- struct{}{} }, } diff --git a/geth/api/backend_txqueue_test.go b/geth/api/backend_txqueue_test.go index 008c7c6b6..f1757177c 100644 --- a/geth/api/backend_txqueue_test.go +++ b/geth/api/backend_txqueue_test.go @@ -10,7 +10,6 @@ import ( "github.com/ethereum/go-ethereum/accounts/keystore" gethcommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/les/status" "github.com/status-im/status-go/geth/common" "github.com/status-im/status-go/geth/log" "github.com/status-im/status-go/geth/node" @@ -28,20 +27,16 @@ func (s *BackendTestSuite) TestSendContractTx() { time.Sleep(TestConfig.Node.SyncSeconds * time.Second) // allow to sync - backend := s.LightEthereumService().StatusBackend - require.NotNil(backend) - // create an account sampleAddress, _, _, err := s.backend.AccountManager().CreateAccount(TestConfig.Account1.Password) require.NoError(err) // make sure you panic if transaction complete doesn't return completeQueuedTransaction := make(chan struct{}, 10) - common.PanicAfter(1*time.Minute, completeQueuedTransaction, s.T().Name()) + common.PanicAfter(2*time.Minute, completeQueuedTransaction, s.T().Name()) // replace transaction notification handler - var txHash = gethcommon.Hash{} - var txHashCheck = gethcommon.Hash{} + var txHash gethcommon.Hash node.SetDefaultNodeNotificationHandler(func(jsonEvent string) { // nolint :dupl var envelope node.SignalEnvelope err := json.Unmarshal([]byte(jsonEvent), &envelope) @@ -53,22 +48,37 @@ func (s *BackendTestSuite) TestSendContractTx() { // the first call will fail (we are not logged in, but trying to complete tx) log.Info("trying to complete with no user logged in") - txHash, err = s.backend.CompleteTransaction(event["id"].(string), TestConfig.Account1.Password) - s.EqualError(err, node.ErrNoAccountSelected.Error(), - fmt.Sprintf("expected error on queued transaction[%v] not thrown", event["id"])) + txHash, err = s.backend.CompleteTransaction( + common.QueuedTxID(event["id"].(string)), + TestConfig.Account1.Password, + ) + s.EqualError( + err, + node.ErrNoAccountSelected.Error(), + fmt.Sprintf("expected error on queued transaction[%v] not thrown", event["id"]), + ) // the second call will also fail (we are logged in as different user) log.Info("trying to complete with invalid user") err = s.backend.AccountManager().SelectAccount(sampleAddress, TestConfig.Account1.Password) s.NoError(err) - txHash, err = s.backend.CompleteTransaction(event["id"].(string), TestConfig.Account1.Password) - s.EqualError(err, status.ErrInvalidCompleteTxSender.Error(), - fmt.Sprintf("expected error on queued transaction[%v] not thrown", event["id"])) + txHash, err = s.backend.CompleteTransaction( + common.QueuedTxID(event["id"].(string)), + TestConfig.Account1.Password, + ) + s.EqualError( + err, + node.ErrInvalidCompleteTxSender.Error(), + fmt.Sprintf("expected error on queued transaction[%v] not thrown", event["id"]), + ) // the third call will work as expected (as we are logged in with correct credentials) - log.Info("trying to complete with correct user, this should suceed") + log.Info("trying to complete with correct user, this should succeed") s.NoError(s.backend.AccountManager().SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password)) - txHash, err = s.backend.CompleteTransaction(event["id"].(string), TestConfig.Account1.Password) + txHash, err = s.backend.CompleteTransaction( + common.QueuedTxID(event["id"].(string)), + TestConfig.Account1.Password, + ) s.NoError(err, fmt.Sprintf("cannot complete queued transaction[%v]", event["id"])) log.Info("contract transaction complete", "URL", "https://rinkeby.etherscan.io/tx/"+txHash.Hex()) @@ -81,8 +91,7 @@ func (s *BackendTestSuite) TestSendContractTx() { byteCode, err := hexutil.Decode(`0x6060604052341561000c57fe5b5b60a58061001b6000396000f30060606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680636ffa1caa14603a575bfe5b3415604157fe5b60556004808035906020019091905050606b565b6040518082815260200191505060405180910390f35b60008160020290505b9190505600a165627a7a72305820ccdadd737e4ac7039963b54cee5e5afb25fa859a275252bdcf06f653155228210029`) require.NoError(err) - // send transaction - txHashCheck, err = backend.SendTransaction(nil, status.SendTxArgs{ + txHashCheck, err := s.backend.SendTransaction(nil, common.SendTxArgs{ From: common.FromAddress(TestConfig.Account1.Address), To: nil, // marker, contract creation is expected //Value: (*hexutil.Big)(new(big.Int).Mul(big.NewInt(1), gethcommon.Ether)), @@ -94,7 +103,7 @@ func (s *BackendTestSuite) TestSendContractTx() { <-completeQueuedTransaction s.Equal(txHashCheck.Hex(), txHash.Hex(), "transaction hash returned from SendTransaction is invalid") s.False(reflect.DeepEqual(txHashCheck, gethcommon.Hash{}), "transaction was never queued or completed") - s.Zero(backend.TransactionQueue().Count(), "tx queue must be empty at this point") + s.Zero(s.TxQueueManager().TransactionQueue().Count(), "tx queue must be empty at this point") } func (s *BackendTestSuite) TestSendEtherTx() { @@ -119,7 +128,6 @@ func (s *BackendTestSuite) TestSendEtherTx() { // replace transaction notification handler var txHash = gethcommon.Hash{} - var txHashCheck = gethcommon.Hash{} node.SetDefaultNodeNotificationHandler(func(jsonEvent string) { // nolint: dupl var envelope node.SignalEnvelope err := json.Unmarshal([]byte(jsonEvent), &envelope) @@ -131,22 +139,35 @@ func (s *BackendTestSuite) TestSendEtherTx() { // the first call will fail (we are not logged in, but trying to complete tx) log.Info("trying to complete with no user logged in") - txHash, err = s.backend.CompleteTransaction(event["id"].(string), TestConfig.Account1.Password) - s.EqualError(err, node.ErrNoAccountSelected.Error(), - fmt.Sprintf("expected error on queued transaction[%v] not thrown", event["id"])) + txHash, err = s.backend.CompleteTransaction( + common.QueuedTxID(event["id"].(string)), + TestConfig.Account1.Password, + ) + s.EqualError( + err, + node.ErrNoAccountSelected.Error(), + fmt.Sprintf("expected error on queued transaction[%v] not thrown", event["id"]), + ) // the second call will also fail (we are logged in as different user) log.Info("trying to complete with invalid user") err = s.backend.AccountManager().SelectAccount(sampleAddress, TestConfig.Account1.Password) s.NoError(err) - txHash, err = s.backend.CompleteTransaction(event["id"].(string), TestConfig.Account1.Password) - s.EqualError(err, status.ErrInvalidCompleteTxSender.Error(), - fmt.Sprintf("expected error on queued transaction[%v] not thrown", event["id"])) + txHash, err = s.backend.CompleteTransaction( + common.QueuedTxID(event["id"].(string)), TestConfig.Account1.Password) + s.EqualError( + err, + node.ErrInvalidCompleteTxSender.Error(), + fmt.Sprintf("expected error on queued transaction[%v] not thrown", event["id"]), + ) // the third call will work as expected (as we are logged in with correct credentials) - log.Info("trying to complete with correct user, this should suceed") + log.Info("trying to complete with correct user, this should succeed") s.NoError(s.backend.AccountManager().SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password)) - txHash, err = s.backend.CompleteTransaction(event["id"].(string), TestConfig.Account1.Password) + txHash, err = s.backend.CompleteTransaction( + common.QueuedTxID(event["id"].(string)), + TestConfig.Account1.Password, + ) s.NoError(err, fmt.Sprintf("cannot complete queued transaction[%v]", event["id"])) log.Info("contract transaction complete", "URL", "https://rinkeby.etherscan.io/tx/"+txHash.Hex()) @@ -155,8 +176,8 @@ func (s *BackendTestSuite) TestSendEtherTx() { } }) - // this call blocks, up until Complete Transaction is called - txHashCheck, err = backend.SendTransaction(nil, status.SendTxArgs{ + // this call blocks, up until Complete Transaction is called + txHashCheck, err := s.backend.SendTransaction(nil, common.SendTxArgs{ From: common.FromAddress(TestConfig.Account1.Address), To: common.ToAddress(TestConfig.Account2.Address), Value: (*hexutil.Big)(big.NewInt(1000000000000)), @@ -166,7 +187,7 @@ func (s *BackendTestSuite) TestSendEtherTx() { <-completeQueuedTransaction s.Equal(txHashCheck.Hex(), txHash.Hex(), "transaction hash returned from SendTransaction is invalid") s.False(reflect.DeepEqual(txHashCheck, gethcommon.Hash{}), "transaction was never queued or completed") - s.Zero(backend.TransactionQueue().Count(), "tx queue must be empty at this point") + s.Zero(s.backend.TxQueueManager().TransactionQueue().Count(), "tx queue must be empty at this point") } func (s *BackendTestSuite) TestDoubleCompleteQueuedTransactions() { @@ -189,7 +210,6 @@ func (s *BackendTestSuite) TestDoubleCompleteQueuedTransactions() { common.PanicAfter(1*time.Minute, completeQueuedTransaction, s.T().Name()) // replace transaction notification handler - var txID string txFailedEventCalled := false txHash := gethcommon.Hash{} node.SetDefaultNodeNotificationHandler(func(jsonEvent string) { @@ -199,7 +219,7 @@ func (s *BackendTestSuite) TestDoubleCompleteQueuedTransactions() { if envelope.Type == node.EventTransactionQueued { event := envelope.Event.(map[string]interface{}) - txID = event["id"].(string) + txID := common.QueuedTxID(event["id"].(string)) log.Info("transaction queued (will be failed and completed on the second call)", "id", txID) // try with wrong password @@ -207,15 +227,12 @@ func (s *BackendTestSuite) TestDoubleCompleteQueuedTransactions() { _, err = s.backend.CompleteTransaction(txID, TestConfig.Account1.Password+"wrong") s.EqualError(err, keystore.ErrDecrypt.Error()) - s.Equal(1, backend.TransactionQueue().Count(), "txqueue cannot be empty, as tx has failed") + s.Equal(1, s.TxQueueManager().TransactionQueue().Count(), "txqueue cannot be empty, as tx has failed") // now try to complete transaction, but with the correct password - txHash, err = s.backend.CompleteTransaction(event["id"].(string), TestConfig.Account1.Password) + txHash, err = s.backend.CompleteTransaction(txID, TestConfig.Account1.Password) s.NoError(err) - time.Sleep(1 * time.Second) // make sure that tx complete signal propagates - s.Equal(0, backend.TransactionQueue().Count(), "txqueue must be empty, as tx has completed") - log.Info("transaction complete", "URL", "https://rinkeby.etherscan.io/tx/"+txHash.Hex()) close(completeQueuedTransaction) } @@ -235,8 +252,8 @@ func (s *BackendTestSuite) TestDoubleCompleteQueuedTransactions() { } }) - // this call blocks, and should return on *second* attempt to CompleteTransaction (w/ the correct password) - txHashCheck, err := backend.SendTransaction(nil, status.SendTxArgs{ + // this call blocks, and should return on *second* attempt to CompleteTransaction (w/ the correct password) + txHashCheck, err := s.backend.SendTransaction(nil, common.SendTxArgs{ From: common.FromAddress(TestConfig.Account1.Address), To: common.ToAddress(TestConfig.Account2.Address), Value: (*hexutil.Big)(big.NewInt(1000000000000)), @@ -246,7 +263,7 @@ func (s *BackendTestSuite) TestDoubleCompleteQueuedTransactions() { <-completeQueuedTransaction s.Equal(txHashCheck.Hex(), txHash.Hex(), "transaction hash returned from SendTransaction is invalid") s.False(reflect.DeepEqual(txHashCheck, gethcommon.Hash{}), "transaction was never queued or completed") - s.Zero(backend.TransactionQueue().Count(), "tx queue must be empty at this point") + s.Zero(s.backend.TxQueueManager().TransactionQueue().Count(), "tx queue must be empty at this point") s.True(txFailedEventCalled, "expected tx failure signal is not received") } @@ -263,7 +280,7 @@ func (s *BackendTestSuite) TestDiscardQueuedTransaction() { require.NotNil(backend) // reset queue - backend.TransactionQueue().Reset() + s.backend.TxQueueManager().TransactionQueue().Reset() // log into account from which transactions will be sent require.NoError(s.backend.AccountManager().SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password)) @@ -273,7 +290,6 @@ func (s *BackendTestSuite) TestDiscardQueuedTransaction() { common.PanicAfter(1*time.Minute, completeQueuedTransaction, s.T().Name()) // replace transaction notification handler - var txID string txFailedEventCalled := false node.SetDefaultNodeNotificationHandler(func(jsonEvent string) { var envelope node.SignalEnvelope @@ -282,10 +298,10 @@ func (s *BackendTestSuite) TestDiscardQueuedTransaction() { if envelope.Type == node.EventTransactionQueued { event := envelope.Event.(map[string]interface{}) - txID = event["id"].(string) + txID := common.QueuedTxID(event["id"].(string)) log.Info("transaction queued (will be discarded soon)", "id", txID) - s.True(backend.TransactionQueue().Has(status.QueuedTxID(txID)), "txqueue should still have test tx") + s.True(s.backend.TxQueueManager().TransactionQueue().Has(txID), "txqueue should still have test tx") // discard err := s.backend.DiscardTransaction(txID) @@ -296,7 +312,7 @@ func (s *BackendTestSuite) TestDiscardQueuedTransaction() { s.EqualError(err, "transaction hash not found", "expects tx not found, but call to CompleteTransaction succeeded") time.Sleep(1 * time.Second) // make sure that tx complete signal propagates - s.False(backend.TransactionQueue().Has(status.QueuedTxID(txID)), + s.False(s.backend.TxQueueManager().TransactionQueue().Has(txID), fmt.Sprintf("txqueue should not have test tx at this point (it should be discarded): %s", txID)) close(completeQueuedTransaction) @@ -307,7 +323,7 @@ func (s *BackendTestSuite) TestDiscardQueuedTransaction() { log.Info("transaction return event received", "id", event["id"].(string)) receivedErrMessage := event["error_message"].(string) - expectedErrMessage := status.ErrQueuedTxDiscarded.Error() + expectedErrMessage := node.ErrQueuedTxDiscarded.Error() s.Equal(receivedErrMessage, expectedErrMessage) receivedErrCode := event["error_code"].(string) @@ -317,17 +333,17 @@ func (s *BackendTestSuite) TestDiscardQueuedTransaction() { } }) - // this call blocks, and should return when DiscardQueuedTransaction() is called - txHashCheck, err := backend.SendTransaction(nil, status.SendTxArgs{ + // this call blocks, and should return when DiscardQueuedTransaction() is called + txHashCheck, err := s.backend.SendTransaction(nil, common.SendTxArgs{ From: common.FromAddress(TestConfig.Account1.Address), To: common.ToAddress(TestConfig.Account2.Address), Value: (*hexutil.Big)(big.NewInt(1000000000000)), }) - s.EqualError(err, status.ErrQueuedTxDiscarded.Error(), "transaction is expected to be discarded") + s.EqualError(err, node.ErrQueuedTxDiscarded.Error(), "transaction is expected to be discarded") <-completeQueuedTransaction s.True(reflect.DeepEqual(txHashCheck, gethcommon.Hash{}), "transaction returned hash, while it shouldn't") - s.Zero(backend.TransactionQueue().Count(), "tx queue must be empty at this point") + s.Zero(s.backend.TxQueueManager().TransactionQueue().Count(), "tx queue must be empty at this point") s.True(txFailedEventCalled, "expected tx failure signal is not received") } @@ -338,31 +354,29 @@ func (s *BackendTestSuite) TestCompleteMultipleQueuedTransactions() { s.StartTestBackend(params.RopstenNetworkID) defer s.StopTestBackend() - time.Sleep(TestConfig.Node.SyncSeconds * time.Second) // allow to sync + // allow to sync + time.Sleep(TestConfig.Node.SyncSeconds * time.Second) - backend := s.LightEthereumService().StatusBackend - require.NotNil(backend) - - // reset queue - backend.TransactionQueue().Reset() + s.TxQueueManager().TransactionQueue().Reset() // log into account from which transactions will be sent - require.NoError(s.backend.AccountManager().SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password)) + err := s.backend.AccountManager().SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password) + require.NoError(err) // make sure you panic if transaction complete doesn't return testTxCount := 3 - txIDs := make(chan string, testTxCount) + txIDs := make(chan common.QueuedTxID, testTxCount) allTestTxCompleted := make(chan struct{}, 1) // replace transaction notification handler node.SetDefaultNodeNotificationHandler(func(jsonEvent string) { - var txID string var envelope node.SignalEnvelope err := json.Unmarshal([]byte(jsonEvent), &envelope) s.NoError(err, fmt.Sprintf("cannot unmarshal JSON: %s", jsonEvent)) + if envelope.Type == node.EventTransactionQueued { event := envelope.Event.(map[string]interface{}) - txID = event["id"].(string) + txID := common.QueuedTxID(event["id"].(string)) log.Info("transaction queued (will be completed in a single call, once aggregated)", "id", txID) txIDs <- txID @@ -371,7 +385,7 @@ func (s *BackendTestSuite) TestCompleteMultipleQueuedTransactions() { // this call blocks, and should return when DiscardQueuedTransaction() for a given tx id is called sendTx := func() { - txHashCheck, err := backend.SendTransaction(nil, status.SendTxArgs{ + txHashCheck, err := s.backend.SendTransaction(nil, common.SendTxArgs{ From: common.FromAddress(TestConfig.Account1.Address), To: common.ToAddress(TestConfig.Account2.Address), Value: (*hexutil.Big)(big.NewInt(1000000000000)), @@ -381,16 +395,9 @@ func (s *BackendTestSuite) TestCompleteMultipleQueuedTransactions() { } // wait for transactions, and complete them in a single call - completeTxs := func(txIDStrings string) { - var parsedIDs []string - err := json.Unmarshal([]byte(txIDStrings), &parsedIDs) - s.NoError(err) - - parsedIDs = append(parsedIDs, "invalid-tx-id") - updatedTxIDStrings, _ := json.Marshal(parsedIDs) - - // complete - results := s.backend.CompleteTransactions(string(updatedTxIDStrings), TestConfig.Account1.Password) + completeTxs := func(txIDs []common.QueuedTxID) { + txIDs = append(txIDs, "invalid-tx-id") + results := s.backend.CompleteTransactions(txIDs, TestConfig.Account1.Password) require.Len(results, testTxCount+1) require.EqualError(results["invalid-tx-id"].Error, "transaction hash not found") @@ -399,29 +406,29 @@ func (s *BackendTestSuite) TestCompleteMultipleQueuedTransactions() { txResult.Error != nil && txID != "invalid-tx-id", "invalid error for %s", txID, ) - require.False( - txResult.Hash.Hex() == "0x0000000000000000000000000000000000000000000000000000000000000000" && txID != "invalid-tx-id", + txResult.Hash == (gethcommon.Hash{}) && txID != "invalid-tx-id", "invalid hash (expected non empty hash): %s", txID, ) + log.Info("transaction complete", "URL", "https://rinkeby.etherscan.io/tx/"+txResult.Hash.Hex()) } time.Sleep(1 * time.Second) // make sure that tx complete signal propagates - for _, txID := range parsedIDs { + + for _, txID := range txIDs { require.False( - backend.TransactionQueue().Has(status.QueuedTxID(txID)), + s.backend.TxQueueManager().TransactionQueue().Has(txID), "txqueue should not have test tx at this point (it should be completed)", ) } } go func() { - var txIDStrings []string + ids := make([]common.QueuedTxID, testTxCount) for i := 0; i < testTxCount; i++ { - txIDStrings = append(txIDStrings, <-txIDs) + ids[i] = <-txIDs } - txIDJSON, _ := json.Marshal(txIDStrings) - completeTxs(string(txIDJSON)) + completeTxs(ids) allTestTxCompleted <- struct{}{} }() @@ -437,7 +444,7 @@ func (s *BackendTestSuite) TestCompleteMultipleQueuedTransactions() { require.Fail("test timed out") } - require.Empty(backend.TransactionQueue().Count()) + require.Zero(s.TxQueueManager().TransactionQueue().Count(), "queue should be empty") } func (s *BackendTestSuite) TestDiscardMultipleQueuedTransactions() { @@ -453,29 +460,29 @@ func (s *BackendTestSuite) TestDiscardMultipleQueuedTransactions() { require.NotNil(backend) // reset queue - backend.TransactionQueue().Reset() + s.backend.TxQueueManager().TransactionQueue().Reset() // log into account from which transactions will be sent require.NoError(s.backend.AccountManager().SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password)) // make sure you panic if transaction complete doesn't return testTxCount := 3 - txIDs := make(chan string, testTxCount) + txIDs := make(chan common.QueuedTxID, testTxCount) allTestTxDiscarded := make(chan struct{}, 1) // replace transaction notification handler txFailedEventCallCount := 0 node.SetDefaultNodeNotificationHandler(func(jsonEvent string) { - var txID string var envelope node.SignalEnvelope err := json.Unmarshal([]byte(jsonEvent), &envelope) s.NoError(err) if envelope.Type == node.EventTransactionQueued { event := envelope.Event.(map[string]interface{}) - txID = event["id"].(string) + txID := common.QueuedTxID(event["id"].(string)) log.Info("transaction queued (will be discarded soon)", "id", txID) - s.True(backend.TransactionQueue().Has(status.QueuedTxID(txID)), "txqueue should still have test tx") + s.True(s.backend.TxQueueManager().TransactionQueue().Has(txID), + "txqueue should still have test tx") txIDs <- txID } @@ -484,7 +491,7 @@ func (s *BackendTestSuite) TestDiscardMultipleQueuedTransactions() { log.Info("transaction return event received", "id", event["id"].(string)) receivedErrMessage := event["error_message"].(string) - expectedErrMessage := status.ErrQueuedTxDiscarded.Error() + expectedErrMessage := node.ErrQueuedTxDiscarded.Error() s.Equal(receivedErrMessage, expectedErrMessage) receivedErrCode := event["error_code"].(string) @@ -499,32 +506,27 @@ func (s *BackendTestSuite) TestDiscardMultipleQueuedTransactions() { // this call blocks, and should return when DiscardQueuedTransaction() for a given tx id is called sendTx := func() { - txHashCheck, err := backend.SendTransaction(nil, status.SendTxArgs{ + txHashCheck, err := s.backend.SendTransaction(nil, common.SendTxArgs{ From: common.FromAddress(TestConfig.Account1.Address), To: common.ToAddress(TestConfig.Account2.Address), Value: (*hexutil.Big)(big.NewInt(1000000000000)), }) - s.EqualError(err, status.ErrQueuedTxDiscarded.Error()) + s.EqualError(err, node.ErrQueuedTxDiscarded.Error()) s.True(reflect.DeepEqual(txHashCheck, gethcommon.Hash{}), "transaction returned hash, while it shouldn't") } // wait for transactions, and discard immediately - discardTxs := func(txIDStrings string) { - var parsedIDs []string - err := json.Unmarshal([]byte(txIDStrings), &parsedIDs) - s.NoError(err) - - parsedIDs = append(parsedIDs, "invalid-tx-id") - updatedTxIDStrings, _ := json.Marshal(parsedIDs) + discardTxs := func(txIDs []common.QueuedTxID) { + txIDs = append(txIDs, "invalid-tx-id") // discard - discardResults := s.backend.DiscardTransactions(string(updatedTxIDStrings)) + discardResults := s.backend.DiscardTransactions(txIDs) require.Len(discardResults, 1, "cannot discard txs: %v", discardResults) require.Error(discardResults["invalid-tx-id"].Error, "transaction hash not found", "cannot discard txs: %v", discardResults) // try completing discarded transaction - completeResults := s.backend.CompleteTransactions(string(updatedTxIDStrings), TestConfig.Account1.Password) + completeResults := s.backend.CompleteTransactions(txIDs, TestConfig.Account1.Password) require.Len(completeResults, testTxCount+1, "unexpected number of errors (call to CompleteTransaction should not succeed)") for _, txResult := range completeResults { @@ -533,18 +535,22 @@ func (s *BackendTestSuite) TestDiscardMultipleQueuedTransactions() { } time.Sleep(1 * time.Second) // make sure that tx complete signal propagates - for _, txID := range parsedIDs { - require.False(backend.TransactionQueue().Has(status.QueuedTxID(txID)), "txqueue should not have test txs at this point (it should be discarded): %s", txID) + + for _, txID := range txIDs { + require.False( + s.backend.TxQueueManager().TransactionQueue().Has(txID), + "txqueue should not have test tx at this point (it should be discarded): %s", + txID, + ) } } go func() { - var txIDStrings []string + ids := make([]common.QueuedTxID, testTxCount) for i := 0; i < testTxCount; i++ { - txIDStrings = append(txIDStrings, <-txIDs) + ids[i] = <-txIDs } - txIDJSON, _ := json.Marshal(txIDStrings) - discardTxs(string(txIDJSON)) + discardTxs(ids) }() // send multiple transactions @@ -559,7 +565,7 @@ func (s *BackendTestSuite) TestDiscardMultipleQueuedTransactions() { require.Fail("test timed out") } - require.Empty(backend.TransactionQueue().Count(), "tx queue must be empty at this point") + require.Zero(s.backend.TxQueueManager().TransactionQueue().Count(), "tx queue must be empty at this point") } func (s *BackendTestSuite) TestNonExistentQueuedTransactions() { @@ -581,7 +587,7 @@ func (s *BackendTestSuite) TestNonExistentQueuedTransactions() { // try completing non-existing transaction _, err := s.backend.CompleteTransaction("some-bad-transaction-id", TestConfig.Account1.Password) s.Error(err, "error expected and not received") - s.EqualError(err, status.ErrQueuedTxIDNotFound.Error()) + s.EqualError(err, node.ErrQueuedTxIDNotFound.Error()) } func (s *BackendTestSuite) TestEvictionOfQueuedTransactions() { @@ -595,15 +601,15 @@ func (s *BackendTestSuite) TestEvictionOfQueuedTransactions() { require.NotNil(backend) // reset queue - backend.TransactionQueue().Reset() + s.backend.TxQueueManager().TransactionQueue().Reset() // log into account from which transactions will be sent require.NoError(s.backend.AccountManager().SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password)) - txQueue := backend.TransactionQueue() + txQueue := s.backend.TxQueueManager().TransactionQueue() var i = 0 - txIDs := [status.DefaultTxQueueCap + 5 + 10]status.QueuedTxID{} - backend.SetTransactionQueueHandler(func(queuedTx status.QueuedTx) { + txIDs := [node.DefaultTxQueueCap + 5 + 10]common.QueuedTxID{} + s.backend.TxQueueManager().SetTransactionQueueHandler(func(queuedTx *common.QueuedTx) { log.Info("tx enqueued", "i", i+1, "queue size", txQueue.Count(), "id", queuedTx.ID) txIDs[i] = queuedTx.ID i++ @@ -612,25 +618,25 @@ func (s *BackendTestSuite) TestEvictionOfQueuedTransactions() { s.Zero(txQueue.Count(), "transaction count should be zero") for i := 0; i < 10; i++ { - go backend.SendTransaction(nil, status.SendTxArgs{}) // nolint: errcheck + go s.backend.SendTransaction(nil, common.SendTxArgs{}) // nolint: errcheck } time.Sleep(1 * time.Second) log.Info(fmt.Sprintf("Number of transactions queued: %d. Queue size (shouldn't be more than %d): %d", - i, status.DefaultTxQueueCap, txQueue.Count())) + i, node.DefaultTxQueueCap, txQueue.Count())) s.Equal(10, txQueue.Count(), "transaction count should be 10") - for i := 0; i < status.DefaultTxQueueCap+5; i++ { // stress test by hitting with lots of goroutines - go backend.SendTransaction(nil, status.SendTxArgs{}) // nolint: errcheck + for i := 0; i < node.DefaultTxQueueCap+5; i++ { // stress test by hitting with lots of goroutines + go s.backend.SendTransaction(nil, common.SendTxArgs{}) // nolint: errcheck } time.Sleep(3 * time.Second) - require.True(txQueue.Count() <= status.DefaultTxQueueCap, "transaction count should be %d (or %d): got %d", status.DefaultTxQueueCap, status.DefaultTxQueueCap-1, txQueue.Count()) + require.True(txQueue.Count() <= node.DefaultTxQueueCap, "transaction count should be %d (or %d): got %d", node.DefaultTxQueueCap, node.DefaultTxQueueCap-1, txQueue.Count()) for _, txID := range txIDs { txQueue.Remove(txID) } - require.Empty(txQueue.Count(), "transaction count should be zero: %d", txQueue.Count()) + require.Zero(txQueue.Count(), "transaction count should be zero: %d", txQueue.Count()) } diff --git a/geth/common/types.go b/geth/common/types.go index f8c972a4b..9d20aba75 100644 --- a/geth/common/types.go +++ b/geth/common/types.go @@ -2,6 +2,7 @@ package common import ( "bytes" + "context" "encoding/json" "errors" "fmt" @@ -11,8 +12,8 @@ import ( "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/les" - "github.com/ethereum/go-ethereum/les/status" "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/rpc" whisper "github.com/ethereum/go-ethereum/whisper/whisperv5" @@ -153,25 +154,98 @@ type RawDiscardTransactionResult struct { Error error } +// QueuedTxID queued transaction identifier +type QueuedTxID string + +// QueuedTx holds enough information to complete the queued transaction. +type QueuedTx struct { + ID QueuedTxID + Hash common.Hash + Context context.Context + Args SendTxArgs + Done chan struct{} + Discard chan struct{} + Err error +} + +// SendTxArgs represents the arguments to submit a new transaction into the transaction pool. +type SendTxArgs struct { + From common.Address `json:"from"` + To *common.Address `json:"to"` + Gas *hexutil.Big `json:"gas"` + GasPrice *hexutil.Big `json:"gasPrice"` + Value *hexutil.Big `json:"value"` + Data hexutil.Bytes `json:"data"` + Nonce *hexutil.Uint64 `json:"nonce"` +} + +// EnqueuedTxHandler is a function that receives queued/pending transactions, when they get queued +type EnqueuedTxHandler func(*QueuedTx) + +// EnqueuedTxReturnHandler is a function that receives response when tx is complete (both on success and error) +type EnqueuedTxReturnHandler func(*QueuedTx, error) + +// TxQueue is a queue of transactions. +type TxQueue interface { + // Remove removes a transaction from the queue. + Remove(id QueuedTxID) + + // Reset resets the state of the queue. + Reset() + + // Count returns a number of transactions in the queue. + Count() int + + // Has returns true if a transaction is in the queue. + Has(id QueuedTxID) bool +} + // TxQueueManager defines expected methods for managing transaction queue type TxQueueManager interface { + // Start starts accepting new transaction in the queue. + Start() + + // Stop stops accepting new transactions in the queue. + Stop() + + // TransactionQueue returns a transaction queue. + TransactionQueue() TxQueue + + // CreateTransactoin creates a new transaction. + CreateTransaction(ctx context.Context, args SendTxArgs) *QueuedTx + + // QueueTransaction adds a new transaction to the queue. + QueueTransaction(tx *QueuedTx) error + + // WaitForTransactions blocks until transaction is completed, discarded or timed out. + WaitForTransaction(tx *QueuedTx) error + + // NotifyOnQueuedTxReturn notifies a handler when a transaction returns. + NotifyOnQueuedTxReturn(queuedTx *QueuedTx, err error) + // TransactionQueueHandler returns handler that processes incoming tx queue requests - TransactionQueueHandler() func(queuedTx status.QueuedTx) + TransactionQueueHandler() func(queuedTx *QueuedTx) + + // TODO(adam): might be not needed + SetTransactionQueueHandler(fn EnqueuedTxHandler) + + // TODO(adam): might be not needed + SetTransactionReturnHandler(fn EnqueuedTxReturnHandler) // TransactionReturnHandler returns handler that processes responses from internal tx manager - TransactionReturnHandler() func(queuedTx *status.QueuedTx, err error) + TransactionReturnHandler() func(queuedTx *QueuedTx, err error) // CompleteTransaction instructs backend to complete sending of a given transaction - CompleteTransaction(id, password string) (common.Hash, error) + CompleteTransaction(id QueuedTxID, password string) (common.Hash, error) // CompleteTransactions instructs backend to complete sending of multiple transactions - CompleteTransactions(ids, password string) map[string]RawCompleteTransactionResult + CompleteTransactions(ids []QueuedTxID, password string) map[QueuedTxID]RawCompleteTransactionResult // DiscardTransaction discards a given transaction from transaction queue - DiscardTransaction(id string) error + DiscardTransaction(id QueuedTxID) error // DiscardTransactions discards given multiple transactions from transaction queue - DiscardTransactions(ids string) map[string]RawDiscardTransactionResult + DiscardTransactions(ids []QueuedTxID) map[QueuedTxID]RawDiscardTransactionResult } // JailCell represents single jail cell, which is basically a JavaScript VM. diff --git a/geth/common/types_mock.go b/geth/common/types_mock.go new file mode 100644 index 000000000..6f6501ba7 --- /dev/null +++ b/geth/common/types_mock.go @@ -0,0 +1,842 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: geth/common/types.go + +// Package common is a generated GoMock package. +package common + +import ( + context "context" + accounts "github.com/ethereum/go-ethereum/accounts" + keystore "github.com/ethereum/go-ethereum/accounts/keystore" + common "github.com/ethereum/go-ethereum/common" + les "github.com/ethereum/go-ethereum/les" + node "github.com/ethereum/go-ethereum/node" + rpc "github.com/ethereum/go-ethereum/rpc" + whisperv5 "github.com/ethereum/go-ethereum/whisper/whisperv5" + gomock "github.com/golang/mock/gomock" + otto "github.com/robertkrimen/otto" + params "github.com/status-im/status-go/geth/params" + reflect "reflect" +) + +// MockNodeManager is a mock of NodeManager interface +type MockNodeManager struct { + ctrl *gomock.Controller + recorder *MockNodeManagerMockRecorder +} + +// MockNodeManagerMockRecorder is the mock recorder for MockNodeManager +type MockNodeManagerMockRecorder struct { + mock *MockNodeManager +} + +// NewMockNodeManager creates a new mock instance +func NewMockNodeManager(ctrl *gomock.Controller) *MockNodeManager { + mock := &MockNodeManager{ctrl: ctrl} + mock.recorder = &MockNodeManagerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockNodeManager) EXPECT() *MockNodeManagerMockRecorder { + return m.recorder +} + +// StartNode mocks base method +func (m *MockNodeManager) StartNode(config *params.NodeConfig) (<-chan struct{}, error) { + ret := m.ctrl.Call(m, "StartNode", config) + ret0, _ := ret[0].(<-chan struct{}) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// StartNode indicates an expected call of StartNode +func (mr *MockNodeManagerMockRecorder) StartNode(config interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartNode", reflect.TypeOf((*MockNodeManager)(nil).StartNode), config) +} + +// StopNode mocks base method +func (m *MockNodeManager) StopNode() (<-chan struct{}, error) { + ret := m.ctrl.Call(m, "StopNode") + ret0, _ := ret[0].(<-chan struct{}) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// StopNode indicates an expected call of StopNode +func (mr *MockNodeManagerMockRecorder) StopNode() *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StopNode", reflect.TypeOf((*MockNodeManager)(nil).StopNode)) +} + +// RestartNode mocks base method +func (m *MockNodeManager) RestartNode() (<-chan struct{}, error) { + ret := m.ctrl.Call(m, "RestartNode") + ret0, _ := ret[0].(<-chan struct{}) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RestartNode indicates an expected call of RestartNode +func (mr *MockNodeManagerMockRecorder) RestartNode() *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RestartNode", reflect.TypeOf((*MockNodeManager)(nil).RestartNode)) +} + +// ResetChainData mocks base method +func (m *MockNodeManager) ResetChainData() (<-chan struct{}, error) { + ret := m.ctrl.Call(m, "ResetChainData") + ret0, _ := ret[0].(<-chan struct{}) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ResetChainData indicates an expected call of ResetChainData +func (mr *MockNodeManagerMockRecorder) ResetChainData() *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResetChainData", reflect.TypeOf((*MockNodeManager)(nil).ResetChainData)) +} + +// IsNodeRunning mocks base method +func (m *MockNodeManager) IsNodeRunning() bool { + ret := m.ctrl.Call(m, "IsNodeRunning") + ret0, _ := ret[0].(bool) + return ret0 +} + +// IsNodeRunning indicates an expected call of IsNodeRunning +func (mr *MockNodeManagerMockRecorder) IsNodeRunning() *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsNodeRunning", reflect.TypeOf((*MockNodeManager)(nil).IsNodeRunning)) +} + +// NodeConfig mocks base method +func (m *MockNodeManager) NodeConfig() (*params.NodeConfig, error) { + ret := m.ctrl.Call(m, "NodeConfig") + ret0, _ := ret[0].(*params.NodeConfig) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// NodeConfig indicates an expected call of NodeConfig +func (mr *MockNodeManagerMockRecorder) NodeConfig() *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeConfig", reflect.TypeOf((*MockNodeManager)(nil).NodeConfig)) +} + +// Node mocks base method +func (m *MockNodeManager) Node() (*node.Node, error) { + ret := m.ctrl.Call(m, "Node") + ret0, _ := ret[0].(*node.Node) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Node indicates an expected call of Node +func (mr *MockNodeManagerMockRecorder) Node() *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Node", reflect.TypeOf((*MockNodeManager)(nil).Node)) +} + +// PopulateStaticPeers mocks base method +func (m *MockNodeManager) PopulateStaticPeers() error { + ret := m.ctrl.Call(m, "PopulateStaticPeers") + ret0, _ := ret[0].(error) + return ret0 +} + +// PopulateStaticPeers indicates an expected call of PopulateStaticPeers +func (mr *MockNodeManagerMockRecorder) PopulateStaticPeers() *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PopulateStaticPeers", reflect.TypeOf((*MockNodeManager)(nil).PopulateStaticPeers)) +} + +// AddPeer mocks base method +func (m *MockNodeManager) AddPeer(url string) error { + ret := m.ctrl.Call(m, "AddPeer", url) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddPeer indicates an expected call of AddPeer +func (mr *MockNodeManagerMockRecorder) AddPeer(url interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddPeer", reflect.TypeOf((*MockNodeManager)(nil).AddPeer), url) +} + +// LightEthereumService mocks base method +func (m *MockNodeManager) LightEthereumService() (*les.LightEthereum, error) { + ret := m.ctrl.Call(m, "LightEthereumService") + ret0, _ := ret[0].(*les.LightEthereum) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// LightEthereumService indicates an expected call of LightEthereumService +func (mr *MockNodeManagerMockRecorder) LightEthereumService() *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LightEthereumService", reflect.TypeOf((*MockNodeManager)(nil).LightEthereumService)) +} + +// WhisperService mocks base method +func (m *MockNodeManager) WhisperService() (*whisperv5.Whisper, error) { + ret := m.ctrl.Call(m, "WhisperService") + ret0, _ := ret[0].(*whisperv5.Whisper) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// WhisperService indicates an expected call of WhisperService +func (mr *MockNodeManagerMockRecorder) WhisperService() *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WhisperService", reflect.TypeOf((*MockNodeManager)(nil).WhisperService)) +} + +// AccountManager mocks base method +func (m *MockNodeManager) AccountManager() (*accounts.Manager, error) { + ret := m.ctrl.Call(m, "AccountManager") + ret0, _ := ret[0].(*accounts.Manager) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AccountManager indicates an expected call of AccountManager +func (mr *MockNodeManagerMockRecorder) AccountManager() *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AccountManager", reflect.TypeOf((*MockNodeManager)(nil).AccountManager)) +} + +// AccountKeyStore mocks base method +func (m *MockNodeManager) AccountKeyStore() (*keystore.KeyStore, error) { + ret := m.ctrl.Call(m, "AccountKeyStore") + ret0, _ := ret[0].(*keystore.KeyStore) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AccountKeyStore indicates an expected call of AccountKeyStore +func (mr *MockNodeManagerMockRecorder) AccountKeyStore() *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AccountKeyStore", reflect.TypeOf((*MockNodeManager)(nil).AccountKeyStore)) +} + +// RPCClient mocks base method +func (m *MockNodeManager) RPCClient() (*rpc.Client, error) { + ret := m.ctrl.Call(m, "RPCClient") + ret0, _ := ret[0].(*rpc.Client) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RPCClient indicates an expected call of RPCClient +func (mr *MockNodeManagerMockRecorder) RPCClient() *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RPCClient", reflect.TypeOf((*MockNodeManager)(nil).RPCClient)) +} + +// RPCServer mocks base method +func (m *MockNodeManager) RPCServer() (*rpc.Server, error) { + ret := m.ctrl.Call(m, "RPCServer") + ret0, _ := ret[0].(*rpc.Server) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RPCServer indicates an expected call of RPCServer +func (mr *MockNodeManagerMockRecorder) RPCServer() *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RPCServer", reflect.TypeOf((*MockNodeManager)(nil).RPCServer)) +} + +// MockAccountManager is a mock of AccountManager interface +type MockAccountManager struct { + ctrl *gomock.Controller + recorder *MockAccountManagerMockRecorder +} + +// MockAccountManagerMockRecorder is the mock recorder for MockAccountManager +type MockAccountManagerMockRecorder struct { + mock *MockAccountManager +} + +// NewMockAccountManager creates a new mock instance +func NewMockAccountManager(ctrl *gomock.Controller) *MockAccountManager { + mock := &MockAccountManager{ctrl: ctrl} + mock.recorder = &MockAccountManagerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockAccountManager) EXPECT() *MockAccountManagerMockRecorder { + return m.recorder +} + +// CreateAccount mocks base method +func (m *MockAccountManager) CreateAccount(password string) (string, string, string, error) { + ret := m.ctrl.Call(m, "CreateAccount", password) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(string) + ret2, _ := ret[2].(string) + ret3, _ := ret[3].(error) + return ret0, ret1, ret2, ret3 +} + +// CreateAccount indicates an expected call of CreateAccount +func (mr *MockAccountManagerMockRecorder) CreateAccount(password interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateAccount", reflect.TypeOf((*MockAccountManager)(nil).CreateAccount), password) +} + +// CreateChildAccount mocks base method +func (m *MockAccountManager) CreateChildAccount(parentAddress, password string) (string, string, error) { + ret := m.ctrl.Call(m, "CreateChildAccount", parentAddress, password) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(string) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// CreateChildAccount indicates an expected call of CreateChildAccount +func (mr *MockAccountManagerMockRecorder) CreateChildAccount(parentAddress, password interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateChildAccount", reflect.TypeOf((*MockAccountManager)(nil).CreateChildAccount), parentAddress, password) +} + +// RecoverAccount mocks base method +func (m *MockAccountManager) RecoverAccount(password, mnemonic string) (string, string, error) { + ret := m.ctrl.Call(m, "RecoverAccount", password, mnemonic) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(string) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// RecoverAccount indicates an expected call of RecoverAccount +func (mr *MockAccountManagerMockRecorder) RecoverAccount(password, mnemonic interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RecoverAccount", reflect.TypeOf((*MockAccountManager)(nil).RecoverAccount), password, mnemonic) +} + +// VerifyAccountPassword mocks base method +func (m *MockAccountManager) VerifyAccountPassword(keyStoreDir, address, password string) (*keystore.Key, error) { + ret := m.ctrl.Call(m, "VerifyAccountPassword", keyStoreDir, address, password) + ret0, _ := ret[0].(*keystore.Key) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// VerifyAccountPassword indicates an expected call of VerifyAccountPassword +func (mr *MockAccountManagerMockRecorder) VerifyAccountPassword(keyStoreDir, address, password interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VerifyAccountPassword", reflect.TypeOf((*MockAccountManager)(nil).VerifyAccountPassword), keyStoreDir, address, password) +} + +// SelectAccount mocks base method +func (m *MockAccountManager) SelectAccount(address, password string) error { + ret := m.ctrl.Call(m, "SelectAccount", address, password) + ret0, _ := ret[0].(error) + return ret0 +} + +// SelectAccount indicates an expected call of SelectAccount +func (mr *MockAccountManagerMockRecorder) SelectAccount(address, password interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SelectAccount", reflect.TypeOf((*MockAccountManager)(nil).SelectAccount), address, password) +} + +// ReSelectAccount mocks base method +func (m *MockAccountManager) ReSelectAccount() error { + ret := m.ctrl.Call(m, "ReSelectAccount") + ret0, _ := ret[0].(error) + return ret0 +} + +// ReSelectAccount indicates an expected call of ReSelectAccount +func (mr *MockAccountManagerMockRecorder) ReSelectAccount() *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReSelectAccount", reflect.TypeOf((*MockAccountManager)(nil).ReSelectAccount)) +} + +// SelectedAccount mocks base method +func (m *MockAccountManager) SelectedAccount() (*SelectedExtKey, error) { + ret := m.ctrl.Call(m, "SelectedAccount") + ret0, _ := ret[0].(*SelectedExtKey) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SelectedAccount indicates an expected call of SelectedAccount +func (mr *MockAccountManagerMockRecorder) SelectedAccount() *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SelectedAccount", reflect.TypeOf((*MockAccountManager)(nil).SelectedAccount)) +} + +// Logout mocks base method +func (m *MockAccountManager) Logout() error { + ret := m.ctrl.Call(m, "Logout") + ret0, _ := ret[0].(error) + return ret0 +} + +// Logout indicates an expected call of Logout +func (mr *MockAccountManagerMockRecorder) Logout() *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logout", reflect.TypeOf((*MockAccountManager)(nil).Logout)) +} + +// AccountsListRequestHandler mocks base method +func (m *MockAccountManager) AccountsListRequestHandler() func([]common.Address) []common.Address { + ret := m.ctrl.Call(m, "AccountsListRequestHandler") + ret0, _ := ret[0].(func([]common.Address) []common.Address) + return ret0 +} + +// AccountsListRequestHandler indicates an expected call of AccountsListRequestHandler +func (mr *MockAccountManagerMockRecorder) AccountsListRequestHandler() *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AccountsListRequestHandler", reflect.TypeOf((*MockAccountManager)(nil).AccountsListRequestHandler)) +} + +// AddressToDecryptedAccount mocks base method +func (m *MockAccountManager) AddressToDecryptedAccount(address, password string) (accounts.Account, *keystore.Key, error) { + ret := m.ctrl.Call(m, "AddressToDecryptedAccount", address, password) + ret0, _ := ret[0].(accounts.Account) + ret1, _ := ret[1].(*keystore.Key) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// AddressToDecryptedAccount indicates an expected call of AddressToDecryptedAccount +func (mr *MockAccountManagerMockRecorder) AddressToDecryptedAccount(address, password interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddressToDecryptedAccount", reflect.TypeOf((*MockAccountManager)(nil).AddressToDecryptedAccount), address, password) +} + +// MockRPCManager is a mock of RPCManager interface +type MockRPCManager struct { + ctrl *gomock.Controller + recorder *MockRPCManagerMockRecorder +} + +// MockRPCManagerMockRecorder is the mock recorder for MockRPCManager +type MockRPCManagerMockRecorder struct { + mock *MockRPCManager +} + +// NewMockRPCManager creates a new mock instance +func NewMockRPCManager(ctrl *gomock.Controller) *MockRPCManager { + mock := &MockRPCManager{ctrl: ctrl} + mock.recorder = &MockRPCManagerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockRPCManager) EXPECT() *MockRPCManagerMockRecorder { + return m.recorder +} + +// Call mocks base method +func (m *MockRPCManager) Call(inputJSON string) string { + ret := m.ctrl.Call(m, "Call", inputJSON) + ret0, _ := ret[0].(string) + return ret0 +} + +// Call indicates an expected call of Call +func (mr *MockRPCManagerMockRecorder) Call(inputJSON interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Call", reflect.TypeOf((*MockRPCManager)(nil).Call), inputJSON) +} + +// MockTxQueue is a mock of TxQueue interface +type MockTxQueue struct { + ctrl *gomock.Controller + recorder *MockTxQueueMockRecorder +} + +// MockTxQueueMockRecorder is the mock recorder for MockTxQueue +type MockTxQueueMockRecorder struct { + mock *MockTxQueue +} + +// NewMockTxQueue creates a new mock instance +func NewMockTxQueue(ctrl *gomock.Controller) *MockTxQueue { + mock := &MockTxQueue{ctrl: ctrl} + mock.recorder = &MockTxQueueMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockTxQueue) EXPECT() *MockTxQueueMockRecorder { + return m.recorder +} + +// Remove mocks base method +func (m *MockTxQueue) Remove(id QueuedTxID) { + m.ctrl.Call(m, "Remove", id) +} + +// Remove indicates an expected call of Remove +func (mr *MockTxQueueMockRecorder) Remove(id interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Remove", reflect.TypeOf((*MockTxQueue)(nil).Remove), id) +} + +// Reset mocks base method +func (m *MockTxQueue) Reset() { + m.ctrl.Call(m, "Reset") +} + +// Reset indicates an expected call of Reset +func (mr *MockTxQueueMockRecorder) Reset() *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Reset", reflect.TypeOf((*MockTxQueue)(nil).Reset)) +} + +// Count mocks base method +func (m *MockTxQueue) Count() int { + ret := m.ctrl.Call(m, "Count") + ret0, _ := ret[0].(int) + return ret0 +} + +// Count indicates an expected call of Count +func (mr *MockTxQueueMockRecorder) Count() *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Count", reflect.TypeOf((*MockTxQueue)(nil).Count)) +} + +// Has mocks base method +func (m *MockTxQueue) Has(id QueuedTxID) bool { + ret := m.ctrl.Call(m, "Has", id) + ret0, _ := ret[0].(bool) + return ret0 +} + +// Has indicates an expected call of Has +func (mr *MockTxQueueMockRecorder) Has(id interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Has", reflect.TypeOf((*MockTxQueue)(nil).Has), id) +} + +// MockTxQueueManager is a mock of TxQueueManager interface +type MockTxQueueManager struct { + ctrl *gomock.Controller + recorder *MockTxQueueManagerMockRecorder +} + +// MockTxQueueManagerMockRecorder is the mock recorder for MockTxQueueManager +type MockTxQueueManagerMockRecorder struct { + mock *MockTxQueueManager +} + +// NewMockTxQueueManager creates a new mock instance +func NewMockTxQueueManager(ctrl *gomock.Controller) *MockTxQueueManager { + mock := &MockTxQueueManager{ctrl: ctrl} + mock.recorder = &MockTxQueueManagerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockTxQueueManager) EXPECT() *MockTxQueueManagerMockRecorder { + return m.recorder +} + +// Start mocks base method +func (m *MockTxQueueManager) Start() { + m.ctrl.Call(m, "Start") +} + +// Start indicates an expected call of Start +func (mr *MockTxQueueManagerMockRecorder) Start() *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockTxQueueManager)(nil).Start)) +} + +// Stop mocks base method +func (m *MockTxQueueManager) Stop() { + m.ctrl.Call(m, "Stop") +} + +// Stop indicates an expected call of Stop +func (mr *MockTxQueueManagerMockRecorder) Stop() *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stop", reflect.TypeOf((*MockTxQueueManager)(nil).Stop)) +} + +// TransactionQueue mocks base method +func (m *MockTxQueueManager) TransactionQueue() TxQueue { + ret := m.ctrl.Call(m, "TransactionQueue") + ret0, _ := ret[0].(TxQueue) + return ret0 +} + +// TransactionQueue indicates an expected call of TransactionQueue +func (mr *MockTxQueueManagerMockRecorder) TransactionQueue() *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TransactionQueue", reflect.TypeOf((*MockTxQueueManager)(nil).TransactionQueue)) +} + +// CreateTransaction mocks base method +func (m *MockTxQueueManager) CreateTransaction(ctx context.Context, args SendTxArgs) *QueuedTx { + ret := m.ctrl.Call(m, "CreateTransaction", ctx, args) + ret0, _ := ret[0].(*QueuedTx) + return ret0 +} + +// CreateTransaction indicates an expected call of CreateTransaction +func (mr *MockTxQueueManagerMockRecorder) CreateTransaction(ctx, args interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateTransaction", reflect.TypeOf((*MockTxQueueManager)(nil).CreateTransaction), ctx, args) +} + +// QueueTransaction mocks base method +func (m *MockTxQueueManager) QueueTransaction(tx *QueuedTx) error { + ret := m.ctrl.Call(m, "QueueTransaction", tx) + ret0, _ := ret[0].(error) + return ret0 +} + +// QueueTransaction indicates an expected call of QueueTransaction +func (mr *MockTxQueueManagerMockRecorder) QueueTransaction(tx interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueueTransaction", reflect.TypeOf((*MockTxQueueManager)(nil).QueueTransaction), tx) +} + +// WaitForTransaction mocks base method +func (m *MockTxQueueManager) WaitForTransaction(tx *QueuedTx) error { + ret := m.ctrl.Call(m, "WaitForTransaction", tx) + ret0, _ := ret[0].(error) + return ret0 +} + +// WaitForTransaction indicates an expected call of WaitForTransaction +func (mr *MockTxQueueManagerMockRecorder) WaitForTransaction(tx interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitForTransaction", reflect.TypeOf((*MockTxQueueManager)(nil).WaitForTransaction), tx) +} + +// NotifyOnQueuedTxReturn mocks base method +func (m *MockTxQueueManager) NotifyOnQueuedTxReturn(queuedTx *QueuedTx, err error) { + m.ctrl.Call(m, "NotifyOnQueuedTxReturn", queuedTx, err) +} + +// NotifyOnQueuedTxReturn indicates an expected call of NotifyOnQueuedTxReturn +func (mr *MockTxQueueManagerMockRecorder) NotifyOnQueuedTxReturn(queuedTx, err interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NotifyOnQueuedTxReturn", reflect.TypeOf((*MockTxQueueManager)(nil).NotifyOnQueuedTxReturn), queuedTx, err) +} + +// TransactionQueueHandler mocks base method +func (m *MockTxQueueManager) TransactionQueueHandler() func(*QueuedTx) { + ret := m.ctrl.Call(m, "TransactionQueueHandler") + ret0, _ := ret[0].(func(*QueuedTx)) + return ret0 +} + +// TransactionQueueHandler indicates an expected call of TransactionQueueHandler +func (mr *MockTxQueueManagerMockRecorder) TransactionQueueHandler() *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TransactionQueueHandler", reflect.TypeOf((*MockTxQueueManager)(nil).TransactionQueueHandler)) +} + +// SetTransactionQueueHandler mocks base method +func (m *MockTxQueueManager) SetTransactionQueueHandler(fn EnqueuedTxHandler) { + m.ctrl.Call(m, "SetTransactionQueueHandler", fn) +} + +// SetTransactionQueueHandler indicates an expected call of SetTransactionQueueHandler +func (mr *MockTxQueueManagerMockRecorder) SetTransactionQueueHandler(fn interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetTransactionQueueHandler", reflect.TypeOf((*MockTxQueueManager)(nil).SetTransactionQueueHandler), fn) +} + +// SetTransactionReturnHandler mocks base method +func (m *MockTxQueueManager) SetTransactionReturnHandler(fn EnqueuedTxReturnHandler) { + m.ctrl.Call(m, "SetTransactionReturnHandler", fn) +} + +// SetTransactionReturnHandler indicates an expected call of SetTransactionReturnHandler +func (mr *MockTxQueueManagerMockRecorder) SetTransactionReturnHandler(fn interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetTransactionReturnHandler", reflect.TypeOf((*MockTxQueueManager)(nil).SetTransactionReturnHandler), fn) +} + +// TransactionReturnHandler mocks base method +func (m *MockTxQueueManager) TransactionReturnHandler() func(*QueuedTx, error) { + ret := m.ctrl.Call(m, "TransactionReturnHandler") + ret0, _ := ret[0].(func(*QueuedTx, error)) + return ret0 +} + +// TransactionReturnHandler indicates an expected call of TransactionReturnHandler +func (mr *MockTxQueueManagerMockRecorder) TransactionReturnHandler() *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TransactionReturnHandler", reflect.TypeOf((*MockTxQueueManager)(nil).TransactionReturnHandler)) +} + +// CompleteTransaction mocks base method +func (m *MockTxQueueManager) CompleteTransaction(id QueuedTxID, password string) (common.Hash, error) { + ret := m.ctrl.Call(m, "CompleteTransaction", id, password) + ret0, _ := ret[0].(common.Hash) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CompleteTransaction indicates an expected call of CompleteTransaction +func (mr *MockTxQueueManagerMockRecorder) CompleteTransaction(id, password interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CompleteTransaction", reflect.TypeOf((*MockTxQueueManager)(nil).CompleteTransaction), id, password) +} + +// CompleteTransactions mocks base method +func (m *MockTxQueueManager) CompleteTransactions(ids []QueuedTxID, password string) map[QueuedTxID]RawCompleteTransactionResult { + ret := m.ctrl.Call(m, "CompleteTransactions", ids, password) + ret0, _ := ret[0].(map[QueuedTxID]RawCompleteTransactionResult) + return ret0 +} + +// CompleteTransactions indicates an expected call of CompleteTransactions +func (mr *MockTxQueueManagerMockRecorder) CompleteTransactions(ids, password interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CompleteTransactions", reflect.TypeOf((*MockTxQueueManager)(nil).CompleteTransactions), ids, password) +} + +// DiscardTransaction mocks base method +func (m *MockTxQueueManager) DiscardTransaction(id QueuedTxID) error { + ret := m.ctrl.Call(m, "DiscardTransaction", id) + ret0, _ := ret[0].(error) + return ret0 +} + +// DiscardTransaction indicates an expected call of DiscardTransaction +func (mr *MockTxQueueManagerMockRecorder) DiscardTransaction(id interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DiscardTransaction", reflect.TypeOf((*MockTxQueueManager)(nil).DiscardTransaction), id) +} + +// DiscardTransactions mocks base method +func (m *MockTxQueueManager) DiscardTransactions(ids []QueuedTxID) map[QueuedTxID]RawDiscardTransactionResult { + ret := m.ctrl.Call(m, "DiscardTransactions", ids) + ret0, _ := ret[0].(map[QueuedTxID]RawDiscardTransactionResult) + return ret0 +} + +// DiscardTransactions indicates an expected call of DiscardTransactions +func (mr *MockTxQueueManagerMockRecorder) DiscardTransactions(ids interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DiscardTransactions", reflect.TypeOf((*MockTxQueueManager)(nil).DiscardTransactions), ids) +} + +// MockJailCell is a mock of JailCell interface +type MockJailCell struct { + ctrl *gomock.Controller + recorder *MockJailCellMockRecorder +} + +// MockJailCellMockRecorder is the mock recorder for MockJailCell +type MockJailCellMockRecorder struct { + mock *MockJailCell +} + +// NewMockJailCell creates a new mock instance +func NewMockJailCell(ctrl *gomock.Controller) *MockJailCell { + mock := &MockJailCell{ctrl: ctrl} + mock.recorder = &MockJailCellMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockJailCell) EXPECT() *MockJailCellMockRecorder { + return m.recorder +} + +// Set mocks base method +func (m *MockJailCell) Set(arg0 string, arg1 interface{}) error { + ret := m.ctrl.Call(m, "Set", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// Set indicates an expected call of Set +func (mr *MockJailCellMockRecorder) Set(arg0, arg1 interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Set", reflect.TypeOf((*MockJailCell)(nil).Set), arg0, arg1) +} + +// Get mocks base method +func (m *MockJailCell) Get(arg0 string) (otto.Value, error) { + ret := m.ctrl.Call(m, "Get", arg0) + ret0, _ := ret[0].(otto.Value) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get +func (mr *MockJailCellMockRecorder) Get(arg0 interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockJailCell)(nil).Get), arg0) +} + +// Run mocks base method +func (m *MockJailCell) Run(arg0 string) (otto.Value, error) { + ret := m.ctrl.Call(m, "Run", arg0) + ret0, _ := ret[0].(otto.Value) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Run indicates an expected call of Run +func (mr *MockJailCellMockRecorder) Run(arg0 interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Run", reflect.TypeOf((*MockJailCell)(nil).Run), arg0) +} + +// RunOnLoop mocks base method +func (m *MockJailCell) RunOnLoop(arg0 string) (otto.Value, error) { + ret := m.ctrl.Call(m, "RunOnLoop", arg0) + ret0, _ := ret[0].(otto.Value) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RunOnLoop indicates an expected call of RunOnLoop +func (mr *MockJailCellMockRecorder) RunOnLoop(arg0 interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RunOnLoop", reflect.TypeOf((*MockJailCell)(nil).RunOnLoop), arg0) +} + +// MockJailManager is a mock of JailManager interface +type MockJailManager struct { + ctrl *gomock.Controller + recorder *MockJailManagerMockRecorder +} + +// MockJailManagerMockRecorder is the mock recorder for MockJailManager +type MockJailManagerMockRecorder struct { + mock *MockJailManager +} + +// NewMockJailManager creates a new mock instance +func NewMockJailManager(ctrl *gomock.Controller) *MockJailManager { + mock := &MockJailManager{ctrl: ctrl} + mock.recorder = &MockJailManagerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockJailManager) EXPECT() *MockJailManagerMockRecorder { + return m.recorder +} + +// Parse mocks base method +func (m *MockJailManager) Parse(chatID, js string) string { + ret := m.ctrl.Call(m, "Parse", chatID, js) + ret0, _ := ret[0].(string) + return ret0 +} + +// Parse indicates an expected call of Parse +func (mr *MockJailManagerMockRecorder) Parse(chatID, js interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Parse", reflect.TypeOf((*MockJailManager)(nil).Parse), chatID, js) +} + +// Call mocks base method +func (m *MockJailManager) Call(chatID, path, args string) string { + ret := m.ctrl.Call(m, "Call", chatID, path, args) + ret0, _ := ret[0].(string) + return ret0 +} + +// Call indicates an expected call of Call +func (mr *MockJailManagerMockRecorder) Call(chatID, path, args interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Call", reflect.TypeOf((*MockJailManager)(nil).Call), chatID, path, args) +} + +// NewJailCell mocks base method +func (m *MockJailManager) NewJailCell(id string) (JailCell, error) { + ret := m.ctrl.Call(m, "NewJailCell", id) + ret0, _ := ret[0].(JailCell) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// NewJailCell indicates an expected call of NewJailCell +func (mr *MockJailManagerMockRecorder) NewJailCell(id interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewJailCell", reflect.TypeOf((*MockJailManager)(nil).NewJailCell), id) +} + +// GetJailCell mocks base method +func (m *MockJailManager) GetJailCell(chatID string) (JailCell, error) { + ret := m.ctrl.Call(m, "GetJailCell", chatID) + ret0, _ := ret[0].(JailCell) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetJailCell indicates an expected call of GetJailCell +func (mr *MockJailManagerMockRecorder) GetJailCell(chatID interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetJailCell", reflect.TypeOf((*MockJailManager)(nil).GetJailCell), chatID) +} + +// BaseJS mocks base method +func (m *MockJailManager) BaseJS(js string) { + m.ctrl.Call(m, "BaseJS", js) +} + +// BaseJS indicates an expected call of BaseJS +func (mr *MockJailManagerMockRecorder) BaseJS(js interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BaseJS", reflect.TypeOf((*MockJailManager)(nil).BaseJS), js) +} diff --git a/geth/jail/execution_policy.go b/geth/jail/execution_policy.go index cc568f6b6..6b99fff61 100644 --- a/geth/jail/execution_policy.go +++ b/geth/jail/execution_policy.go @@ -3,16 +3,12 @@ package jail import ( "context" "encoding/json" - "math/big" - "time" gethcommon "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rpc" "github.com/robertkrimen/otto" "github.com/status-im/status-go/geth/common" + "github.com/status-im/status-go/geth/params" ) // ExecutionPolicy provides a central container for the executions of RPCCall requests for both @@ -20,131 +16,70 @@ import ( type ExecutionPolicy struct { nodeManager common.NodeManager accountManager common.AccountManager + txQueueManager common.TxQueueManager } // NewExecutionPolicy returns a new instance of ExecutionPolicy. -func NewExecutionPolicy(nodeManager common.NodeManager, accountManager common.AccountManager) *ExecutionPolicy { +func NewExecutionPolicy( + nodeManager common.NodeManager, accountManager common.AccountManager, txQueueManager common.TxQueueManager, +) *ExecutionPolicy { return &ExecutionPolicy{ nodeManager: nodeManager, accountManager: accountManager, + txQueueManager: txQueueManager, + } +} + +// Execute handles a received RPC call. +func (ep *ExecutionPolicy) Execute(req common.RPCCall, call otto.FunctionCall) (*otto.Object, error) { + switch req.Method { + case params.SendTransactionMethodName: + return ep.executeSendTransaction(req, call) + default: + return ep.executeOtherTransaction(req, call) } } // ExecuteSendTransaction defines a function to execute RPC requests for eth_sendTransaction method only. -func (ep *ExecutionPolicy) ExecuteSendTransaction(req common.RPCCall, call otto.FunctionCall) (*otto.Object, error) { - config, err := ep.nodeManager.NodeConfig() +func (ep *ExecutionPolicy) executeSendTransaction(req common.RPCCall, call otto.FunctionCall) (*otto.Object, error) { + res, err := call.Otto.Object(`({"jsonrpc":"2.0"})`) if err != nil { return nil, err } - if config.UpstreamConfig.Enabled { - return ep.executeRemoteSendTransaction(req, call) - } + res.Set("id", req.ID) - return ep.executeLocalSendTransaction(req, call) -} - -// executeRemoteSendTransaction defines a function to execute RPC method eth_sendTransaction over the upstream server. -func (ep *ExecutionPolicy) executeRemoteSendTransaction(req common.RPCCall, call otto.FunctionCall) (*otto.Object, error) { - config, err := ep.nodeManager.NodeConfig() + messageID, err := preProcessRequest(call.Otto, req) if err != nil { return nil, err } - selectedAcct, err := ep.accountManager.SelectedAccount() - if err != nil { + // TODO(adam): check if context is used + ctx := context.WithValue(context.Background(), common.MessageIDKey, messageID) + args := sendTxArgsFromRPCCall(req) + tx := ep.txQueueManager.CreateTransaction(ctx, args) + + if err := ep.txQueueManager.QueueTransaction(tx); err != nil { return nil, err } - client, err := ep.nodeManager.RPCClient() - if err != nil { + if err := ep.txQueueManager.WaitForTransaction(tx); err != nil { return nil, err } - fromAddr, err := req.ParseFromAddress() - if err != nil { - return nil, err - } + // invoke post processing + postProcessRequest(call.Otto, req, messageID) - toAddr, err := req.ParseToAddress() - if err != nil { - return nil, err - } + // @TODO(adam): which one is actually used? + res.Set("result", tx.Hash.Hex()) + res.Set("hash", tx.Hash.Hex()) - // We need to request a new transaction nounce from upstream node. - ctx, canceller := context.WithTimeout(context.Background(), time.Minute) - defer canceller() - - var num hexutil.Uint - if err := client.CallContext(ctx, &num, "eth_getTransactionCount", fromAddr, "pending"); err != nil { - return nil, err - } - - nonce := uint64(num) - gas := (*big.Int)(req.ParseGas()) - dataVal := []byte(req.ParseData()) - priceVal := (*big.Int)(req.ParseValue()) - gasPrice := (*big.Int)(req.ParseGasPrice()) - chainID := big.NewInt(int64(config.NetworkID)) - - tx := types.NewTransaction(nonce, toAddr, priceVal, gas, gasPrice, dataVal) - txs, err := types.SignTx(tx, types.NewEIP155Signer(chainID), selectedAcct.AccountKey.PrivateKey) - if err != nil { - return nil, err - } - - // Attempt to get the hex version of the transaction. - txBytes, err := rlp.EncodeToBytes(txs) - if err != nil { - return nil, err - } - - //TODO(influx6): Should we use a single context with a higher timeout, say 3-5 minutes - // for calls to rpcClient? - ctx2, canceler2 := context.WithTimeout(context.Background(), time.Minute) - defer canceler2() - - var result json.RawMessage - if err := client.CallContext(ctx2, &result, "eth_sendRawTransaction", gethcommon.ToHex(txBytes)); err != nil { - return nil, err - } - - resp, err := call.Otto.Object(`({"jsonrpc":"2.0"})`) - if err != nil { - return nil, err - } - - resp.Set("id", req.ID) - resp.Set("result", result) - resp.Set("hash", txs.Hash().String()) - - return resp, nil -} - -// executeLocalSendTransaction defines a function which handles execution of RPC method over the internal rpc server -// from the eth.LightClient. It specifically caters to process eth_sendTransaction. -func (ep *ExecutionPolicy) executeLocalSendTransaction(req common.RPCCall, call otto.FunctionCall) (*otto.Object, error) { - resp, err := call.Otto.Object(`({"jsonrpc":"2.0"})`) - if err != nil { - return nil, err - } - - resp.Set("id", req.ID) - - txHash, err := processRPCCall(ep.nodeManager, req, call) - resp.Set("result", txHash.Hex()) - - if err != nil { - resp = newErrorResponse(call.Otto, -32603, err.Error(), &req.ID).Object() - return resp, nil - } - - return resp, nil + return res, nil } // ExecuteOtherTransaction defines a function which handles the processing of non `eth_sendTransaction` // rpc request to the internal node server. -func (ep *ExecutionPolicy) ExecuteOtherTransaction(req common.RPCCall, call otto.FunctionCall) (*otto.Object, error) { +func (ep *ExecutionPolicy) executeOtherTransaction(req common.RPCCall, call otto.FunctionCall) (*otto.Object, error) { client, err := ep.nodeManager.RPCClient() if err != nil { return nil, common.StopRPCCallError{Err: err} @@ -207,3 +142,66 @@ func (ep *ExecutionPolicy) ExecuteOtherTransaction(req common.RPCCall, call otto return resp, nil } + +// preProcessRequest pre-processes a given RPC call to a given Otto VM +func preProcessRequest(vm *otto.Otto, req common.RPCCall) (string, error) { + messageID := currentMessageID(vm.Context()) + + return messageID, nil +} + +// postProcessRequest post-processes a given RPC call to a given Otto VM +func postProcessRequest(vm *otto.Otto, req common.RPCCall, messageID string) { + if len(messageID) > 0 { + vm.Call("addContext", nil, messageID, common.MessageIDKey, messageID) // nolint: errcheck + } + + // set extra markers for queued transaction requests + if req.Method == params.SendTransactionMethodName { + vm.Call("addContext", nil, messageID, params.SendTransactionMethodName, true) // nolint: errcheck + } +} + +// currentMessageID looks for `status.message_id` variable in current JS context +func currentMessageID(ctx otto.Context) string { + if statusObj, ok := ctx.Symbols["status"]; ok { + messageID, err := statusObj.Object().Get("message_id") + if err != nil { + return "" + } + if messageID, err := messageID.ToString(); err == nil { + return messageID + } + } + + return "" +} + +func sendTxArgsFromRPCCall(req common.RPCCall) common.SendTxArgs { + // no need to persist extra state for other requests + if req.Method != params.SendTransactionMethodName { + return common.SendTxArgs{} + } + + var err error + var fromAddr, toAddr gethcommon.Address + + fromAddr, err = req.ParseFromAddress() + if err != nil { + fromAddr = gethcommon.HexToAddress("0x0") + } + + toAddr, err = req.ParseToAddress() + if err != nil { + toAddr = gethcommon.HexToAddress("0x0") + } + + return common.SendTxArgs{ + To: &toAddr, + From: fromAddr, + Value: req.ParseValue(), + Data: req.ParseData(), + Gas: req.ParseGas(), + GasPrice: req.ParseGasPrice(), + } +} diff --git a/geth/jail/jail.go b/geth/jail/jail.go index 684f5bf51..618c4c521 100644 --- a/geth/jail/jail.go +++ b/geth/jail/jail.go @@ -1,18 +1,14 @@ package jail import ( - "context" "encoding/json" "errors" "fmt" "sync" - gethcommon "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/les/status" "github.com/robertkrimen/otto" "github.com/status-im/status-go/geth/common" "github.com/status-im/status-go/geth/log" - "github.com/status-im/status-go/geth/params" "github.com/status-im/status-go/static" ) @@ -31,6 +27,7 @@ type Jail struct { sync.RWMutex nodeManager common.NodeManager accountManager common.AccountManager + txQueueManager common.TxQueueManager policy *ExecutionPolicy cells map[string]*Cell // jail supports running many isolated instances of jailed runtime baseJSCode string // JavaScript used to initialize all new cells with @@ -38,12 +35,15 @@ type Jail struct { // New returns new Jail environment with the associated NodeManager and // AccountManager. -func New(nodeManager common.NodeManager, accountManager common.AccountManager) *Jail { +func New( + nodeManager common.NodeManager, accountManager common.AccountManager, txQueueManager common.TxQueueManager, +) *Jail { return &Jail{ nodeManager: nodeManager, accountManager: accountManager, + txQueueManager: txQueueManager, cells: make(map[string]*Cell), - policy: NewExecutionPolicy(nodeManager, accountManager), + policy: NewExecutionPolicy(nodeManager, accountManager, txQueueManager), } } @@ -176,18 +176,12 @@ func (jail *Jail) Send(call otto.FunctionCall) (response otto.Value) { // Execute the requests. for _, req := range reqs { - var resErr error - var res *otto.Object + log.Info("executing a request via jail.policy", "method", req.Method, "params", req.Params) + res, err := jail.policy.Execute(req, call) + log.Info("response from the request", "err", err) - switch req.Method { - case params.SendTransactionMethodName: - res, resErr = jail.policy.ExecuteSendTransaction(req, call) - default: - res, resErr = jail.policy.ExecuteOtherTransaction(req, call) - } - - if resErr != nil { - switch resErr.(type) { + if err != nil { + switch err.(type) { case common.StopRPCCallError: return newErrorResponse(call.Otto, -32603, err.Error(), nil) default: @@ -214,99 +208,6 @@ func (jail *Jail) Send(call otto.FunctionCall) (response otto.Value) { return response } -//================================================================================================================================== - -func processRPCCall(manager common.NodeManager, req common.RPCCall, call otto.FunctionCall) (gethcommon.Hash, error) { - lightEthereum, err := manager.LightEthereumService() - if err != nil { - return gethcommon.Hash{}, err - } - - backend := lightEthereum.StatusBackend - - messageID, err := preProcessRequest(call.Otto, req) - if err != nil { - return gethcommon.Hash{}, err - } - - // onSendTransactionRequest() will use context to obtain and release ticket - ctx := context.Background() - ctx = context.WithValue(ctx, common.MessageIDKey, messageID) - - // this call blocks, up until Complete Transaction is called - txHash, err := backend.SendTransaction(ctx, sendTxArgsFromRPCCall(req)) - if err != nil { - return gethcommon.Hash{}, err - } - - // invoke post processing - postProcessRequest(call.Otto, req, messageID) - - return txHash, nil -} - -// preProcessRequest pre-processes a given RPC call to a given Otto VM -func preProcessRequest(vm *otto.Otto, req common.RPCCall) (string, error) { - messageID := currentMessageID(vm.Context()) - - return messageID, nil -} - -// postProcessRequest post-processes a given RPC call to a given Otto VM -func postProcessRequest(vm *otto.Otto, req common.RPCCall, messageID string) { - if len(messageID) > 0 { - vm.Call("addContext", nil, messageID, common.MessageIDKey, messageID) // nolint: errcheck - } - - // set extra markers for queued transaction requests - if req.Method == params.SendTransactionMethodName { - vm.Call("addContext", nil, messageID, params.SendTransactionMethodName, true) // nolint: errcheck - } -} - -func sendTxArgsFromRPCCall(req common.RPCCall) status.SendTxArgs { - if req.Method != params.SendTransactionMethodName { // no need to persist extra state for other requests - return status.SendTxArgs{} - } - - var err error - var fromAddr, toAddr gethcommon.Address - - fromAddr, err = req.ParseFromAddress() - if err != nil { - fromAddr = gethcommon.HexToAddress("0x0") - } - - toAddr, err = req.ParseToAddress() - if err != nil { - toAddr = gethcommon.HexToAddress("0x0") - } - - return status.SendTxArgs{ - To: &toAddr, - From: fromAddr, - Value: req.ParseValue(), - Data: req.ParseData(), - Gas: req.ParseGas(), - GasPrice: req.ParseGasPrice(), - } -} - -// currentMessageID looks for `status.message_id` variable in current JS context -func currentMessageID(ctx otto.Context) string { - if statusObj, ok := ctx.Symbols["status"]; ok { - messageID, err := statusObj.Object().Get("message_id") - if err != nil { - return "" - } - if messageID, err := messageID.ToString(); err == nil { - return messageID - } - } - - return "" -} - //========================================================================================================== func newErrorResponse(vm *otto.Otto, code int, msg string, id interface{}) otto.Value { diff --git a/geth/jail/jail_rpc_test.go b/geth/jail/jail_rpc_test.go index a5a0615f9..ff43bcb9e 100644 --- a/geth/jail/jail_rpc_test.go +++ b/geth/jail/jail_rpc_test.go @@ -55,7 +55,9 @@ func (s *JailRPCTestSuite) SetupTest() { acctman := node.NewAccountManager(nodeman) require.NotNil(acctman) - policy := jail.NewExecutionPolicy(nodeman, acctman) + txQueueManager := node.NewTxQueueManager(nodeman, acctman) + + policy := jail.NewExecutionPolicy(nodeman, acctman, txQueueManager) require.NotNil(policy) s.Policy = policy @@ -145,7 +147,7 @@ func (s *JailRPCTestSuite) TestSendTransaction() { selectErr := s.Account.SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password) require.NoError(selectErr) - res, err := s.Policy.ExecuteSendTransaction(request, odFunc) + res, err := s.Policy.Execute(request, odFunc) require.NoError(err) result, err := res.Get("result") diff --git a/geth/jail/jail_test.go b/geth/jail/jail_test.go index b451f0f22..7fc6c6093 100644 --- a/geth/jail/jail_test.go +++ b/geth/jail/jail_test.go @@ -38,7 +38,7 @@ func (s *JailTestSuite) SetupTest() { accountManager := node.NewAccountManager(nodeManager) require.NotNil(accountManager) - jail := jail.New(nodeManager, accountManager) + jail := jail.New(nodeManager, accountManager, nil) require.NotNil(jail) s.jail = jail diff --git a/geth/node/rpc.go b/geth/node/rpc.go index 7023c04f6..ac5b81a29 100644 --- a/geth/node/rpc.go +++ b/geth/node/rpc.go @@ -13,7 +13,6 @@ import ( "net/url" - "github.com/ethereum/go-ethereum/les/status" "github.com/status-im/status-go/geth/common" "github.com/status-im/status-go/geth/log" "github.com/status-im/status-go/geth/params" @@ -100,7 +99,7 @@ func (c *RPCManager) Call(inputJSON string) string { select { case out := <-outputJSON: return out - case <-time.After((status.DefaultTxSendCompletionTimeout + 10) * time.Minute): // give up eventually + case <-time.After((DefaultTxSendCompletionTimeout + 10) * time.Minute): // give up eventually // pass } diff --git a/geth/node/txqueue.go b/geth/node/txqueue.go index a091d335e..3948d2f99 100644 --- a/geth/node/txqueue.go +++ b/geth/node/txqueue.go @@ -1,194 +1,256 @@ package node import ( - "context" + "errors" + "fmt" + "sync" + "time" "github.com/ethereum/go-ethereum/accounts/keystore" - gethcommon "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/les/status" "github.com/status-im/status-go/geth/common" + "github.com/status-im/status-go/geth/log" ) const ( - // EventTransactionQueued is triggered when send transaction request is queued - EventTransactionQueued = "transaction.queued" - - // EventTransactionFailed is triggered when send transaction request fails - EventTransactionFailed = "transaction.failed" - - // SendTxDefaultErrorCode is sent by default, when error is not nil, but type is unknown/unexpected. - SendTxDefaultErrorCode = SendTransactionDefaultErrorCode + // DefaultTxQueueCap defines how many items can be queued. + DefaultTxQueueCap = int(35) + // DefaultTxSendQueueCap defines how many items can be passed to sendTransaction() w/o blocking. + DefaultTxSendQueueCap = int(70) + // DefaultTxSendCompletionTimeout defines how many seconds to wait before returning result in sentTransaction(). + DefaultTxSendCompletionTimeout = 300 ) -// Send transaction response codes -const ( - SendTransactionNoErrorCode = "0" - SendTransactionDefaultErrorCode = "1" - SendTransactionPasswordErrorCode = "2" - SendTransactionTimeoutErrorCode = "3" - SendTransactionDiscardedErrorCode = "4" +var ( + ErrQueuedTxIDNotFound = errors.New("transaction hash not found") + ErrQueuedTxTimedOut = errors.New("transaction sending timed out") + ErrQueuedTxDiscarded = errors.New("transaction has been discarded") + ErrInvalidCompleteTxSender = errors.New("transaction can only be completed by the same account which created it") ) -var txReturnCodes = map[error]string{ // deliberately strings, in case more meaningful codes are to be returned - nil: SendTransactionNoErrorCode, - keystore.ErrDecrypt: SendTransactionPasswordErrorCode, - status.ErrQueuedTxTimedOut: SendTransactionTimeoutErrorCode, - status.ErrQueuedTxDiscarded: SendTransactionDiscardedErrorCode, +// TxQueue is capped container that holds pending transactions +type TxQueue struct { + transactions map[common.QueuedTxID]*common.QueuedTx + mu sync.RWMutex // to guard transactions map + evictableIDs chan common.QueuedTxID + enqueueTicker chan struct{} + incomingPool chan *common.QueuedTx + + // when this channel is closed, all queue channels processing must cease (incoming queue, processing queued items etc) + stopped chan struct{} + stoppedGroup sync.WaitGroup // to make sure that all routines are stopped + + // when items are enqueued notify subscriber + txEnqueueHandler common.EnqueuedTxHandler + + // when tx is returned (either successfully or with error) notify subscriber + txReturnHandler common.EnqueuedTxReturnHandler } -// TxQueueManager provides means to manage internal Status Backend (injected into LES) -type TxQueueManager struct { - nodeManager common.NodeManager - accountManager common.AccountManager -} - -func NewTxQueueManager(nodeManager common.NodeManager, accountManager common.AccountManager) *TxQueueManager { - return &TxQueueManager{ - nodeManager: nodeManager, - accountManager: accountManager, +// NewTransactionQueue make new transaction queue +func NewTransactionQueue() *TxQueue { + log.Info("initializing transaction queue") + return &TxQueue{ + transactions: make(map[common.QueuedTxID]*common.QueuedTx), + evictableIDs: make(chan common.QueuedTxID, DefaultTxQueueCap), // will be used to evict in FIFO + enqueueTicker: make(chan struct{}), + incomingPool: make(chan *common.QueuedTx, DefaultTxSendQueueCap), } } -// CompleteTransaction instructs backend to complete sending of a given transaction -func (m *TxQueueManager) CompleteTransaction(id, password string) (gethcommon.Hash, error) { - lightEthereum, err := m.nodeManager.LightEthereumService() - if err != nil { - return gethcommon.Hash{}, err +// Start starts enqueue and eviction loops +func (q *TxQueue) Start() { + log.Info("starting transaction queue") + + if q.stopped != nil { + return } - backend := lightEthereum.StatusBackend + q.stopped = make(chan struct{}) + q.stoppedGroup.Add(2) - selectedAccount, err := m.accountManager.SelectedAccount() - if err != nil { - return gethcommon.Hash{}, err - } - - ctx := context.Background() - ctx = context.WithValue(ctx, status.SelectedAccountKey, selectedAccount.Hex()) - - return backend.CompleteQueuedTransaction(ctx, status.QueuedTxID(id), password) + go q.evictionLoop() + go q.enqueueLoop() } -// CompleteTransactions instructs backend to complete sending of multiple transactions -func (m *TxQueueManager) CompleteTransactions(ids, password string) map[string]common.RawCompleteTransactionResult { - results := make(map[string]common.RawCompleteTransactionResult) +// Stop stops transaction enqueue and eviction loops +func (q *TxQueue) Stop() { + log.Info("stopping transaction queue") - parsedIDs, err := common.ParseJSONArray(ids) - if err != nil { - results["none"] = common.RawCompleteTransactionResult{ - Error: err, - } - return results + if q.stopped == nil { + return } - for _, txID := range parsedIDs { - txHash, txErr := m.CompleteTransaction(txID, password) - results[txID] = common.RawCompleteTransactionResult{ - Hash: txHash, - Error: txErr, + close(q.stopped) // stops all processing loops (enqueue, eviction etc) + q.stoppedGroup.Wait() + q.stopped = nil + + log.Info("finally stopped transaction queue") +} + +// evictionLoop frees up queue to accommodate another transaction item +func (q *TxQueue) evictionLoop() { + defer HaltOnPanic() + evict := func() { + if len(q.transactions) >= DefaultTxQueueCap { // eviction is required to accommodate another/last item + q.Remove(<-q.evictableIDs) } } - return results -} - -// DiscardTransaction discards a given transaction from transaction queue -func (m *TxQueueManager) DiscardTransaction(id string) error { - lightEthereum, err := m.nodeManager.LightEthereumService() - if err != nil { - return err - } - - backend := lightEthereum.StatusBackend - - return backend.DiscardQueuedTransaction(status.QueuedTxID(id)) -} - -// DiscardTransactions discards given multiple transactions from transaction queue -func (m *TxQueueManager) DiscardTransactions(ids string) map[string]common.RawDiscardTransactionResult { - var parsedIDs []string - results := make(map[string]common.RawDiscardTransactionResult) - - parsedIDs, err := common.ParseJSONArray(ids) - if err != nil { - results["none"] = common.RawDiscardTransactionResult{ - Error: err, - } - return results - } - - for _, txID := range parsedIDs { - err := m.DiscardTransaction(txID) - if err != nil { - results[txID] = common.RawDiscardTransactionResult{ - Error: err, - } - } - } - - return results -} - -// SendTransactionEvent is a signal sent on a send transaction request -type SendTransactionEvent struct { - ID string `json:"id"` - Args status.SendTxArgs `json:"args"` - MessageID string `json:"message_id"` -} - -// TransactionQueueHandler returns handler that processes incoming tx queue requests -func (m *TxQueueManager) TransactionQueueHandler() func(queuedTx status.QueuedTx) { - return func(queuedTx status.QueuedTx) { - SendSignal(SignalEnvelope{ - Type: EventTransactionQueued, - Event: SendTransactionEvent{ - ID: string(queuedTx.ID), - Args: queuedTx.Args, - MessageID: common.MessageIDFromContext(queuedTx.Context), - }, - }) - } -} - -// ReturnSendTransactionEvent is a JSON returned whenever transaction send is returned -type ReturnSendTransactionEvent struct { - ID string `json:"id"` - Args status.SendTxArgs `json:"args"` - MessageID string `json:"message_id"` - ErrorMessage string `json:"error_message"` - ErrorCode string `json:"error_code"` -} - -// TransactionReturnHandler returns handler that processes responses from internal tx manager -func (m *TxQueueManager) TransactionReturnHandler() func(queuedTx *status.QueuedTx, err error) { - return func(queuedTx *status.QueuedTx, err error) { - if err == nil { + for { + select { + case <-time.After(250 * time.Millisecond): // do not wait for manual ticks, check queue regularly + evict() + case <-q.enqueueTicker: // when manually requested + evict() + case <-q.stopped: + log.Info("transaction queue's eviction loop stopped") + q.stoppedGroup.Done() return } + } +} - // discard notifications with empty tx - if queuedTx == nil { +// enqueueLoop process incoming enqueue requests +func (q *TxQueue) enqueueLoop() { + defer HaltOnPanic() + + // enqueue incoming transactions + for { + select { + case queuedTx := <-q.incomingPool: + log.Info("transaction enqueued requested", "tx", queuedTx.ID) + q.Enqueue(queuedTx) + log.Info("transaction enqueued", "tx", queuedTx.ID) + case <-q.stopped: + log.Info("transaction queue's enqueue loop stopped") + q.stoppedGroup.Done() return } - - // error occurred, signal up to application - SendSignal(SignalEnvelope{ - Type: EventTransactionFailed, - Event: ReturnSendTransactionEvent{ - ID: string(queuedTx.ID), - Args: queuedTx.Args, - MessageID: common.MessageIDFromContext(queuedTx.Context), - ErrorMessage: err.Error(), - ErrorCode: m.sendTransactionErrorCode(err), - }, - }) } } -func (m *TxQueueManager) sendTransactionErrorCode(err error) string { - if code, ok := txReturnCodes[err]; ok { - return code +// Reset is to be used in tests only, as it simply creates new transaction map, w/o any cleanup of the previous one +func (q *TxQueue) Reset() { + q.mu.Lock() + defer q.mu.Unlock() + + q.transactions = make(map[common.QueuedTxID]*common.QueuedTx) + q.evictableIDs = make(chan common.QueuedTxID, DefaultTxQueueCap) +} + +// EnqueueAsync enqueues incoming transaction in async manner, returns as soon as possible +func (q *TxQueue) EnqueueAsync(tx *common.QueuedTx) error { + q.incomingPool <- tx + + return nil +} + +// Enqueue enqueues incoming transaction +func (q *TxQueue) Enqueue(tx *common.QueuedTx) error { + log.Info(fmt.Sprintf("enqueue transaction: %s", tx.ID)) + + if q.txEnqueueHandler == nil { //discard, until handler is provided + log.Info("there is no txEnqueueHandler") + return nil } - return SendTxDefaultErrorCode + log.Info("before enqueueTicker") + q.enqueueTicker <- struct{}{} // notify eviction loop that we are trying to insert new item + log.Info("before evictableIDs") + q.evictableIDs <- tx.ID // this will block when we hit DefaultTxQueueCap + log.Info("after evictableIDs") + + q.mu.Lock() + q.transactions[tx.ID] = tx + q.mu.Unlock() + + // notify handler + log.Info("calling txEnqueueHandler") + q.txEnqueueHandler(tx) + + return nil +} + +// Get returns transaction by transaction identifier +func (q *TxQueue) Get(id common.QueuedTxID) (*common.QueuedTx, error) { + q.mu.RLock() + defer q.mu.RUnlock() + + if tx, ok := q.transactions[id]; ok { + return tx, nil + } + + return nil, ErrQueuedTxIDNotFound +} + +// Remove removes transaction by transaction identifier +func (q *TxQueue) Remove(id common.QueuedTxID) { + q.mu.Lock() + defer q.mu.Unlock() + + delete(q.transactions, id) +} + +// Count returns number of currently queued transactions +func (q *TxQueue) Count() int { + q.mu.RLock() + defer q.mu.RUnlock() + + return len(q.transactions) +} + +// Has checks whether transaction with a given identifier exists in queue +func (q *TxQueue) Has(id common.QueuedTxID) bool { + q.mu.RLock() + defer q.mu.RUnlock() + + _, ok := q.transactions[id] + + return ok +} + +// SetEnqueueHandler sets callback handler, that is triggered on enqueue operation +func (q *TxQueue) SetEnqueueHandler(fn common.EnqueuedTxHandler) { + q.txEnqueueHandler = fn +} + +// SetTxReturnHandler sets callback handler, that is triggered when transaction is finished executing +func (q *TxQueue) SetTxReturnHandler(fn common.EnqueuedTxReturnHandler) { + q.txReturnHandler = fn +} + +// NotifyOnQueuedTxReturn is invoked when transaction is ready to return +// Transaction can be in error state, or executed successfully at this point. +func (q *TxQueue) NotifyOnQueuedTxReturn(queuedTx *common.QueuedTx, err error) { + if q == nil { + return + } + + // discard, if transaction is not found + if queuedTx == nil { + return + } + + // on success, remove item from the queue and stop propagating + if err == nil { + q.Remove(queuedTx.ID) + return + } + + // error occurred, send upward notification + if q.txReturnHandler == nil { // discard, until handler is provided + return + } + + // remove from queue on any error (except for transient ones) and propagate + transientErrs := map[error]bool{ + keystore.ErrDecrypt: true, // wrong password + ErrInvalidCompleteTxSender: true, // completing tx create from another account + } + if !transientErrs[err] { // remove only on unrecoverable errors + q.Remove(queuedTx.ID) + } + + // notify handler + q.txReturnHandler(queuedTx, err) } diff --git a/geth/node/txqueue_manager.go b/geth/node/txqueue_manager.go new file mode 100644 index 000000000..371debcab --- /dev/null +++ b/geth/node/txqueue_manager.go @@ -0,0 +1,379 @@ +package node + +import ( + "context" + "math/big" + "time" + + "github.com/ethereum/go-ethereum/accounts/keystore" + gethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/les/status" + "github.com/ethereum/go-ethereum/rlp" + "github.com/pborman/uuid" + "github.com/status-im/status-go/geth/common" + "github.com/status-im/status-go/geth/log" +) + +const ( + // EventTransactionQueued is triggered when send transaction request is queued + EventTransactionQueued = "transaction.queued" + + // EventTransactionFailed is triggered when send transaction request fails + EventTransactionFailed = "transaction.failed" + + // SendTxDefaultErrorCode is sent by default, when error is not nil, but type is unknown/unexpected. + SendTxDefaultErrorCode = SendTransactionDefaultErrorCode +) + +// Send transaction response codes +const ( + SendTransactionNoErrorCode = "0" + SendTransactionDefaultErrorCode = "1" + SendTransactionPasswordErrorCode = "2" + SendTransactionTimeoutErrorCode = "3" + SendTransactionDiscardedErrorCode = "4" +) + +var txReturnCodes = map[error]string{ // deliberately strings, in case more meaningful codes are to be returned + nil: SendTransactionNoErrorCode, + keystore.ErrDecrypt: SendTransactionPasswordErrorCode, + ErrQueuedTxTimedOut: SendTransactionTimeoutErrorCode, + ErrQueuedTxDiscarded: SendTransactionDiscardedErrorCode, +} + +// TxQueueManager provides means to manage internal Status Backend (injected into LES) +type TxQueueManager struct { + nodeManager common.NodeManager + accountManager common.AccountManager + txQueue *TxQueue +} + +// NewTxQueueManager returns a new TxQueueManager. +func NewTxQueueManager(nodeManager common.NodeManager, accountManager common.AccountManager) *TxQueueManager { + return &TxQueueManager{ + nodeManager: nodeManager, + accountManager: accountManager, + txQueue: NewTransactionQueue(), + } +} + +// Start starts accepting new transactions into the queue. +func (m *TxQueueManager) Start() { + log.Info("start TxQueueManager") + m.txQueue.Start() +} + +// Stop stops accepting new transactions into the queue. +func (m *TxQueueManager) Stop() { + log.Info("stop TxQueueManager") + m.txQueue.Stop() +} + +// TransactionQueue returns a reference to the queue. +func (m *TxQueueManager) TransactionQueue() common.TxQueue { + return m.txQueue +} + +// CreateTransaction returns a transaction object. +func (m *TxQueueManager) CreateTransaction(ctx context.Context, args common.SendTxArgs) *common.QueuedTx { + return &common.QueuedTx{ + ID: common.QueuedTxID(uuid.New()), + Hash: gethcommon.Hash{}, + Context: ctx, + Args: args, + Done: make(chan struct{}, 1), + Discard: make(chan struct{}, 1), + } +} + +// QueueTransaction puts a transaction into the queue. +func (m *TxQueueManager) QueueTransaction(tx *common.QueuedTx) error { + to := "" + if tx.Args.To != nil { + to = tx.Args.To.Hex() + } + log.Info("queue a new transaction", "id", tx.ID, "from", tx.Args.From.Hex(), "to", to) + + return m.txQueue.Enqueue(tx) +} + +// WaitForTransaction adds a transaction to the queue and blocks +// until it's completed, discarded or times out. +func (m *TxQueueManager) WaitForTransaction(tx *common.QueuedTx) error { + log.Info("wait for transaction", "id", tx.ID) + + // now wait up until transaction is: + // - completed (via CompleteQueuedTransaction), + // - discarded (via DiscardQueuedTransaction) + // - or times out + select { + case <-tx.Done: + m.NotifyOnQueuedTxReturn(tx, tx.Err) + return tx.Err + case <-tx.Discard: + m.NotifyOnQueuedTxReturn(tx, ErrQueuedTxDiscarded) + return ErrQueuedTxDiscarded + case <-time.After(DefaultTxSendCompletionTimeout * time.Second): + m.NotifyOnQueuedTxReturn(tx, ErrQueuedTxTimedOut) + return ErrQueuedTxTimedOut + } +} + +// NotifyOnQueuedTxReturn calls a handler when a transaction resolves. +func (m *TxQueueManager) NotifyOnQueuedTxReturn(queuedTx *common.QueuedTx, err error) { + m.txQueue.NotifyOnQueuedTxReturn(queuedTx, err) +} + +// CompleteTransaction instructs backend to complete sending of a given transaction. +// TODO(adam): investigate a possible bug that calling this method multiple times with the same Transaction ID +// results in sending multiple transactions. +func (m *TxQueueManager) CompleteTransaction(id common.QueuedTxID, password string) (gethcommon.Hash, error) { + log.Info("complete transaction", "id", id) + + queuedTx, err := m.txQueue.Get(id) + if err != nil { + log.Warn("could not get a queued transaction", "err", err) + return gethcommon.Hash{}, err + } + + selectedAccount, err := m.accountManager.SelectedAccount() + if err != nil { + log.Warn("failed to get a selected account", "err", err) + return gethcommon.Hash{}, err + } + + // make sure that only account which created the tx can complete it + if queuedTx.Args.From.Hex() != selectedAccount.Address.Hex() { + log.Warn("queued transaction does not belong to the selected account", "err", ErrInvalidCompleteTxSender) + m.NotifyOnQueuedTxReturn(queuedTx, ErrInvalidCompleteTxSender) + return gethcommon.Hash{}, ErrInvalidCompleteTxSender + } + + config, err := m.nodeManager.NodeConfig() + if err != nil { + log.Warn("could not get a node config", "err", err) + return gethcommon.Hash{}, err + } + + // Send the transaction finally. + var hash gethcommon.Hash + var txErr error + + if config.UpstreamConfig.Enabled { + hash, txErr = m.completeRemoteTransaction(queuedTx, password) + } else { + hash, txErr = m.completeLocalTransaction(queuedTx, password) + } + + // when incorrect sender tries to complete the account, + // notify and keep tx in queue (so that correct sender can complete) + if txErr == keystore.ErrDecrypt { + log.Warn("failed to complete transaction", "err", txErr) + m.NotifyOnQueuedTxReturn(queuedTx, txErr) + return hash, txErr + } + + log.Info("finally completed transaction", "id", queuedTx.ID, "hash", hash, "err", txErr) + + queuedTx.Hash = hash + queuedTx.Err = txErr + queuedTx.Done <- struct{}{} // sendTransaction() waits on this, notify so that it can return + + return hash, txErr +} + +func (m *TxQueueManager) completeLocalTransaction(queuedTx *common.QueuedTx, password string) (gethcommon.Hash, error) { + log.Info("complete transaction using local node", "id", queuedTx.ID) + + les, err := m.nodeManager.LightEthereumService() + if err != nil { + return gethcommon.Hash{}, err + } + + return les.StatusBackend.SendTransaction(context.Background(), status.SendTxArgs(queuedTx.Args), password) +} + +func (m *TxQueueManager) completeRemoteTransaction(queuedTx *common.QueuedTx, password string) (gethcommon.Hash, error) { + log.Info("complete transaction using upstream node", "id", queuedTx.ID) + + var emptyHash gethcommon.Hash + + config, err := m.nodeManager.NodeConfig() + if err != nil { + return emptyHash, err + } + + selectedAcct, err := m.accountManager.SelectedAccount() + if err != nil { + return emptyHash, err + } + + client, err := m.nodeManager.RPCClient() + if err != nil { + return emptyHash, err + } + + // We need to request a new transaction nounce from upstream node. + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + var txCount hexutil.Uint + if callErr := client.CallContext(ctx, &txCount, "eth_getTransactionCount", queuedTx.Args.From, "pending"); callErr != nil { + return emptyHash, callErr + } + + chainID := big.NewInt(int64(config.NetworkID)) + nonce := uint64(txCount) + gas := (*big.Int)(queuedTx.Args.Gas) + gasPrice := (*big.Int)(queuedTx.Args.GasPrice) + dataVal := []byte(queuedTx.Args.Data) + priceVal := (*big.Int)(queuedTx.Args.Value) + + tx := types.NewTransaction(nonce, *queuedTx.Args.To, priceVal, gas, gasPrice, dataVal) + signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), selectedAcct.AccountKey.PrivateKey) + if err != nil { + return emptyHash, err + } + + txBytes, err := rlp.EncodeToBytes(signedTx) + if err != nil { + return emptyHash, err + } + + ctx2, cancel2 := context.WithTimeout(context.Background(), time.Minute) + defer cancel2() + + if err := client.CallContext(ctx2, nil, "eth_sendRawTransaction", gethcommon.ToHex(txBytes)); err != nil { + return emptyHash, err + } + + return signedTx.Hash(), nil +} + +// CompleteTransactions instructs backend to complete sending of multiple transactions +func (m *TxQueueManager) CompleteTransactions(ids []common.QueuedTxID, password string) map[common.QueuedTxID]common.RawCompleteTransactionResult { + results := make(map[common.QueuedTxID]common.RawCompleteTransactionResult) + + for _, txID := range ids { + txHash, txErr := m.CompleteTransaction(txID, password) + results[txID] = common.RawCompleteTransactionResult{ + Hash: txHash, + Error: txErr, + } + } + + return results +} + +// DiscardTransaction discards a given transaction from transaction queue +func (m *TxQueueManager) DiscardTransaction(id common.QueuedTxID) error { + queuedTx, err := m.txQueue.Get(id) + if err != nil { + return err + } + + // remove from queue, before notifying SendTransaction + m.txQueue.Remove(queuedTx.ID) + + // allow SendTransaction to return + queuedTx.Err = ErrQueuedTxDiscarded + queuedTx.Discard <- struct{}{} // sendTransaction() waits on this, notify so that it can return + + return nil +} + +// DiscardTransactions discards given multiple transactions from transaction queue +func (m *TxQueueManager) DiscardTransactions(ids []common.QueuedTxID) map[common.QueuedTxID]common.RawDiscardTransactionResult { + results := make(map[common.QueuedTxID]common.RawDiscardTransactionResult) + + for _, txID := range ids { + err := m.DiscardTransaction(txID) + if err != nil { + results[txID] = common.RawDiscardTransactionResult{ + Error: err, + } + } + } + + return results +} + +// SendTransactionEvent is a signal sent on a send transaction request +type SendTransactionEvent struct { + ID string `json:"id"` + Args common.SendTxArgs `json:"args"` + MessageID string `json:"message_id"` +} + +// TransactionQueueHandler returns handler that processes incoming tx queue requests +func (m *TxQueueManager) TransactionQueueHandler() func(queuedTx *common.QueuedTx) { + return func(queuedTx *common.QueuedTx) { + log.Info("calling TransactionQueueHandler") + SendSignal(SignalEnvelope{ + Type: EventTransactionQueued, + Event: SendTransactionEvent{ + ID: string(queuedTx.ID), + Args: queuedTx.Args, + MessageID: common.MessageIDFromContext(queuedTx.Context), + }, + }) + } +} + +// SetTransactionQueueHandler sets a handler that will be called +// when a new transaction is enqueued. +func (m *TxQueueManager) SetTransactionQueueHandler(fn common.EnqueuedTxHandler) { + m.txQueue.SetEnqueueHandler(fn) +} + +// ReturnSendTransactionEvent is a JSON returned whenever transaction send is returned +type ReturnSendTransactionEvent struct { + ID string `json:"id"` + Args common.SendTxArgs `json:"args"` + MessageID string `json:"message_id"` + ErrorMessage string `json:"error_message"` + ErrorCode string `json:"error_code"` +} + +// TransactionReturnHandler returns handler that processes responses from internal tx manager +func (m *TxQueueManager) TransactionReturnHandler() func(queuedTx *common.QueuedTx, err error) { + return func(queuedTx *common.QueuedTx, err error) { + if err == nil { + return + } + + // discard notifications with empty tx + if queuedTx == nil { + return + } + + // error occurred, signal up to application + SendSignal(SignalEnvelope{ + Type: EventTransactionFailed, + Event: ReturnSendTransactionEvent{ + ID: string(queuedTx.ID), + Args: queuedTx.Args, + MessageID: common.MessageIDFromContext(queuedTx.Context), + ErrorMessage: err.Error(), + ErrorCode: m.sendTransactionErrorCode(err), + }, + }) + } +} + +func (m *TxQueueManager) sendTransactionErrorCode(err error) string { + if code, ok := txReturnCodes[err]; ok { + return code + } + + return SendTxDefaultErrorCode +} + +// SetTransactionReturnHandler sets a handler that will be called +// when a transaction is about to return or when a recoverable error occured. +// Recoverable error is, for instance, wrong password. +func (m *TxQueueManager) SetTransactionReturnHandler(fn common.EnqueuedTxReturnHandler) { + m.txQueue.SetTxReturnHandler(fn) +} diff --git a/geth/node/txqueue_manager_test.go b/geth/node/txqueue_manager_test.go new file mode 100644 index 000000000..5b911c148 --- /dev/null +++ b/geth/node/txqueue_manager_test.go @@ -0,0 +1,215 @@ +package node + +import ( + "context" + "errors" + "testing" + + "github.com/ethereum/go-ethereum/accounts/keystore" + "github.com/stretchr/testify/suite" + + "github.com/golang/mock/gomock" + + "github.com/status-im/status-go/geth/common" + "github.com/status-im/status-go/geth/params" + . "github.com/status-im/status-go/geth/testing" +) + +var errTxAssumedSent = errors.New("assume tx is done") + +func TestTxQueueTestSuite(t *testing.T) { + suite.Run(t, new(TxQueueTestSuite)) +} + +type TxQueueTestSuite struct { + suite.Suite + nodeManagerMockCtrl *gomock.Controller + nodeManagerMock *common.MockNodeManager + accountManagerMockCtrl *gomock.Controller + accountManagerMock *common.MockAccountManager +} + +func (s *TxQueueTestSuite) SetupTest() { + s.nodeManagerMockCtrl = gomock.NewController(s.T()) + s.accountManagerMockCtrl = gomock.NewController(s.T()) + + s.nodeManagerMock = common.NewMockNodeManager(s.nodeManagerMockCtrl) + s.accountManagerMock = common.NewMockAccountManager(s.accountManagerMockCtrl) +} + +func (s *TxQueueTestSuite) TearDownTest() { + s.nodeManagerMockCtrl.Finish() + s.accountManagerMockCtrl.Finish() +} + +func (s *TxQueueTestSuite) TestCompleteTransaction() { + s.accountManagerMock.EXPECT().SelectedAccount().Return(&common.SelectedExtKey{ + Address: common.FromAddress(TestConfig.Account1.Address), + }, nil) + + s.nodeManagerMock.EXPECT().NodeConfig().Return( + params.NewNodeConfig("/tmp", params.RopstenNetworkID, true), + ) + + // TODO(adam): StatusBackend as an interface would allow a better solution. + // As we want to avoid network connection, we mock LES with a known error + // and treat as success. + s.nodeManagerMock.EXPECT().LightEthereumService().Return(nil, errTxAssumedSent) + + txQueueManager := NewTxQueueManager(s.nodeManagerMock, s.accountManagerMock) + + txQueueManager.Start() + defer txQueueManager.Stop() + + tx := txQueueManager.CreateTransaction(context.Background(), common.SendTxArgs{ + From: common.FromAddress(TestConfig.Account1.Address), + To: common.ToAddress(TestConfig.Account2.Address), + }) + + // TransactionQueueHandler is required to enqueue a transaction. + txQueueManager.SetTransactionQueueHandler(func(queuedTx *common.QueuedTx) { + s.Equal(tx.ID, queuedTx.ID) + }) + + txQueueManager.SetTransactionReturnHandler(func(queuedTx *common.QueuedTx, err error) { + s.Equal(tx.ID, queuedTx.ID) + s.Equal(errTxAssumedSent, err) + }) + + err := txQueueManager.QueueTransaction(tx) + s.NoError(err) + + go func() { + _, err := txQueueManager.CompleteTransaction(tx.ID, TestConfig.Account1.Password) + s.Equal(errTxAssumedSent, err) + }() + + err = txQueueManager.WaitForTransaction(tx) + s.Equal(errTxAssumedSent, err) + // Check that error is assigned to the transaction. + s.Equal(errTxAssumedSent, tx.Err) + // Transaction should be already removed from the queue. + s.False(txQueueManager.TransactionQueue().Has(tx.ID)) +} + +func (s *TxQueueTestSuite) TestAccountMismatch() { + s.accountManagerMock.EXPECT().SelectedAccount().Return(&common.SelectedExtKey{ + Address: common.FromAddress(TestConfig.Account2.Address), + }, nil) + + txQueueManager := NewTxQueueManager(s.nodeManagerMock, s.accountManagerMock) + + txQueueManager.Start() + defer txQueueManager.Stop() + + tx := txQueueManager.CreateTransaction(context.Background(), common.SendTxArgs{ + From: common.FromAddress(TestConfig.Account1.Address), + To: common.ToAddress(TestConfig.Account2.Address), + }) + + // TransactionQueueHandler is required to enqueue a transaction. + txQueueManager.SetTransactionQueueHandler(func(queuedTx *common.QueuedTx) { + s.Equal(tx.ID, queuedTx.ID) + }) + + // Missmatched address is a recoverable error, that's why + // the return handler is called. + txQueueManager.SetTransactionReturnHandler(func(queuedTx *common.QueuedTx, err error) { + s.Equal(tx.ID, queuedTx.ID) + s.Equal(ErrInvalidCompleteTxSender, err) + s.Nil(tx.Err) + }) + + err := txQueueManager.QueueTransaction(tx) + s.NoError(err) + + _, err = txQueueManager.CompleteTransaction(tx.ID, TestConfig.Account1.Password) + s.Equal(err, ErrInvalidCompleteTxSender) + + // Transaction should stay in the queue as mismatched accounts + // is a recoverable error. + s.True(txQueueManager.TransactionQueue().Has(tx.ID)) +} + +func (s *TxQueueTestSuite) TestInvalidPassword() { + s.accountManagerMock.EXPECT().SelectedAccount().Return(&common.SelectedExtKey{ + Address: common.FromAddress(TestConfig.Account1.Address), + }, nil) + + s.nodeManagerMock.EXPECT().NodeConfig().Return( + params.NewNodeConfig("/tmp", params.RopstenNetworkID, true), + ) + + // Set ErrDecrypt error response as expected with a wrong password. + s.nodeManagerMock.EXPECT().LightEthereumService().Return(nil, keystore.ErrDecrypt) + + txQueueManager := NewTxQueueManager(s.nodeManagerMock, s.accountManagerMock) + + txQueueManager.Start() + defer txQueueManager.Stop() + + tx := txQueueManager.CreateTransaction(context.Background(), common.SendTxArgs{ + From: common.FromAddress(TestConfig.Account1.Address), + To: common.ToAddress(TestConfig.Account2.Address), + }) + + // TransactionQueueHandler is required to enqueue a transaction. + txQueueManager.SetTransactionQueueHandler(func(queuedTx *common.QueuedTx) { + s.Equal(tx.ID, queuedTx.ID) + }) + + // Missmatched address is a revocable error, that's why + // the return handler is called. + txQueueManager.SetTransactionReturnHandler(func(queuedTx *common.QueuedTx, err error) { + s.Equal(tx.ID, queuedTx.ID) + s.Equal(keystore.ErrDecrypt, err) + s.Nil(tx.Err) + }) + + err := txQueueManager.QueueTransaction(tx) + s.NoError(err) + + _, err = txQueueManager.CompleteTransaction(tx.ID, "invalid-password") + s.Equal(err, keystore.ErrDecrypt) + + // Transaction should stay in the queue as mismatched accounts + // is a recoverable error. + s.True(txQueueManager.TransactionQueue().Has(tx.ID)) +} + +func (s *TxQueueTestSuite) TestDiscardTransaction() { + txQueueManager := NewTxQueueManager(s.nodeManagerMock, s.accountManagerMock) + + txQueueManager.Start() + defer txQueueManager.Stop() + + tx := txQueueManager.CreateTransaction(context.Background(), common.SendTxArgs{ + From: common.FromAddress(TestConfig.Account1.Address), + To: common.ToAddress(TestConfig.Account2.Address), + }) + + // TransactionQueueHandler is required to enqueue a transaction. + txQueueManager.SetTransactionQueueHandler(func(queuedTx *common.QueuedTx) { + s.Equal(tx.ID, queuedTx.ID) + }) + + txQueueManager.SetTransactionReturnHandler(func(queuedTx *common.QueuedTx, err error) { + s.Equal(tx.ID, queuedTx.ID) + s.Equal(ErrQueuedTxDiscarded, err) + }) + + err := txQueueManager.QueueTransaction(tx) + s.NoError(err) + + go func() { + err := txQueueManager.DiscardTransaction(tx.ID) + s.NoError(err) + }() + + err = txQueueManager.WaitForTransaction(tx) + s.Equal(ErrQueuedTxDiscarded, err) + // Check that error is assigned to the transaction. + s.Equal(ErrQueuedTxDiscarded, tx.Err) + // Transaction should be already removed from the queue. + s.False(txQueueManager.TransactionQueue().Has(tx.ID)) +} diff --git a/vendor/github.com/ethereum/go-ethereum/internal/ethapi/api.go b/vendor/github.com/ethereum/go-ethereum/internal/ethapi/api.go index 1c2273655..85836f3f6 100644 --- a/vendor/github.com/ethereum/go-ethereum/internal/ethapi/api.go +++ b/vendor/github.com/ethereum/go-ethereum/internal/ethapi/api.go @@ -36,7 +36,6 @@ import ( "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethdb" - "github.com/ethereum/go-ethereum/les/status" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/params" @@ -1181,34 +1180,14 @@ func submitTransaction(ctx context.Context, b Backend, tx *types.Transaction) (c return tx.Hash(), nil } -// SendTransaction queues transactions, to be fulfilled by CompleteQueuedTransaction() -func (s *PublicTransactionPoolAPI) SendTransaction(ctx context.Context, args SendTxArgs) (common.Hash, error) { - backend := s.b.GetStatusBackend() - if backend != nil { - return backend.SendTransaction(ctx, status.SendTxArgs(args)) - } - - return common.Hash{}, ErrStatusBackendNotInited -} - // CompleteQueuedTransaction creates a transaction by unpacking queued transaction, signs it and submits to the // transaction pool. -func (s *PublicTransactionPoolAPI) CompleteQueuedTransaction(ctx context.Context, args SendTxArgs, passphrase string) (common.Hash, error) { +func (s *PublicTransactionPoolAPI) SendTransaction(ctx context.Context, args SendTxArgs, passphrase string) (common.Hash, error) { // Set some sanity defaults and terminate on failure if err := args.setDefaults(ctx, s.b); err != nil { return common.Hash{}, err } - // make sure that only account which created the tx can complete it - selectedAccountAddress := "0x0" - if address, ok := ctx.Value(status.SelectedAccountKey).(string); ok { - selectedAccountAddress = address - } - if args.From.Hex() != selectedAccountAddress { - log.Info("Failed to complete tx", "from", args.From.Hex(), "selected account", selectedAccountAddress) - return common.Hash{}, status.ErrInvalidCompleteTxSender - } - // Look up the wallet containing the requested signer account := accounts.Account{Address: args.From} diff --git a/vendor/github.com/ethereum/go-ethereum/internal/ethapi/status_backend.go b/vendor/github.com/ethereum/go-ethereum/internal/ethapi/status_backend.go index 663251df1..763d0e27e 100644 --- a/vendor/github.com/ethereum/go-ethereum/internal/ethapi/status_backend.go +++ b/vendor/github.com/ethereum/go-ethereum/internal/ethapi/status_backend.go @@ -3,14 +3,11 @@ package ethapi import ( "errors" "math/big" - "time" - "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/les/status" "github.com/ethereum/go-ethereum/log" - "github.com/pborman/uuid" "golang.org/x/net/context" ) @@ -20,8 +17,7 @@ type StatusBackend struct { bcapi *PublicBlockChainAPI // Wrapper around the blockchain to access chain data txapi *PublicTransactionPoolAPI // Wrapper around the transaction pool to access transaction data - txQueue *status.TxQueue - am *status.AccountManager + am *status.AccountManager } var ( @@ -32,50 +28,13 @@ var ( func NewStatusBackend(apiBackend Backend) *StatusBackend { log.Info("StatusIM: backend service inited") return &StatusBackend{ - eapi: NewPublicEthereumAPI(apiBackend), - bcapi: NewPublicBlockChainAPI(apiBackend), - txapi: NewPublicTransactionPoolAPI(apiBackend, new(AddrLocker)), - txQueue: status.NewTransactionQueue(), - am: status.NewAccountManager(apiBackend.AccountManager()), + eapi: NewPublicEthereumAPI(apiBackend), + bcapi: NewPublicBlockChainAPI(apiBackend), + txapi: NewPublicTransactionPoolAPI(apiBackend, new(AddrLocker)), + am: status.NewAccountManager(apiBackend.AccountManager()), } } -// Start starts status backend -func (b *StatusBackend) Start() { - log.Info("StatusIM: started as LES sub-protocol") - b.txQueue.Start() -} - -// Stop stops status backend -func (b *StatusBackend) Stop() { - log.Info("StatusIM: stopped as LES sub-protocol") - b.txQueue.Stop() -} - -// NotifyOnQueuedTxReturn notifies any registered handlers that transaction is ready to return -func (b *StatusBackend) NotifyOnQueuedTxReturn(queuedTx *status.QueuedTx, err error) { - if b == nil { - return - } - - b.txQueue.NotifyOnQueuedTxReturn(queuedTx, err) -} - -// SetTransactionReturnHandler sets a callback that is triggered when transaction is ready to return -func (b *StatusBackend) SetTransactionReturnHandler(fn status.EnqueuedTxReturnHandler) { - b.txQueue.SetTxReturnHandler(fn) -} - -// SetTransactionQueueHandler sets a callback that is triggered when transaction is enqueued -func (b *StatusBackend) SetTransactionQueueHandler(fn status.EnqueuedTxHandler) { - b.txQueue.SetEnqueueHandler(fn) -} - -// TransactionQueue returns reference to transaction queue -func (b *StatusBackend) TransactionQueue() *status.TxQueue { - return b.txQueue -} - // SetAccountsFilterHandler sets a callback that is triggered when account list is requested func (b *StatusBackend) SetAccountsFilterHandler(fn status.AccountsFilterHandler) { b.am.SetAccountsFilterHandler(fn) @@ -87,7 +46,7 @@ func (b *StatusBackend) AccountManager() *status.AccountManager { } // SendTransaction wraps call to PublicTransactionPoolAPI.SendTransaction -func (b *StatusBackend) SendTransaction(ctx context.Context, args status.SendTxArgs) (common.Hash, error) { +func (b *StatusBackend) SendTransaction(ctx context.Context, args status.SendTxArgs, passphrase string) (common.Hash, error) { if ctx == nil { ctx = context.Background() } @@ -98,81 +57,7 @@ func (b *StatusBackend) SendTransaction(ctx context.Context, args status.SendTxA } } - queuedTx := &status.QueuedTx{ - ID: status.QueuedTxID(uuid.New()), - Hash: common.Hash{}, - Context: ctx, - Args: status.SendTxArgs(args), - Done: make(chan struct{}, 1), - Discard: make(chan struct{}, 1), - } - - // send transaction to pending pool, w/o blocking - b.txQueue.EnqueueAsync(queuedTx) - - // now wait up until transaction is: - // - completed (via CompleteQueuedTransaction), - // - discarded (via DiscardQueuedTransaction) - // - or times out - select { - case <-queuedTx.Done: - b.NotifyOnQueuedTxReturn(queuedTx, queuedTx.Err) - return queuedTx.Hash, queuedTx.Err - case <-queuedTx.Discard: - b.NotifyOnQueuedTxReturn(queuedTx, status.ErrQueuedTxDiscarded) - return queuedTx.Hash, queuedTx.Err - case <-time.After(status.DefaultTxSendCompletionTimeout * time.Second): - b.NotifyOnQueuedTxReturn(queuedTx, status.ErrQueuedTxTimedOut) - return common.Hash{}, status.ErrQueuedTxTimedOut - } - - return queuedTx.Hash, nil -} - -// CompleteQueuedTransaction wraps call to PublicTransactionPoolAPI.CompleteQueuedTransaction -func (b *StatusBackend) CompleteQueuedTransaction(ctx context.Context, id status.QueuedTxID, passphrase string) (common.Hash, error) { - queuedTx, err := b.txQueue.Get(id) - if err != nil { - return common.Hash{}, err - } - - hash, err := b.txapi.CompleteQueuedTransaction(ctx, SendTxArgs(queuedTx.Args), passphrase) - - // on password error, notify the app, and keep tx in queue (so that CompleteQueuedTransaction() can be resent) - if err == keystore.ErrDecrypt { - b.NotifyOnQueuedTxReturn(queuedTx, err) - return hash, err // SendTransaction is still blocked - } - - // when incorrect sender tries to complete the account, notify and keep tx in queue (so that correct sender can complete) - if err == status.ErrInvalidCompleteTxSender { - b.NotifyOnQueuedTxReturn(queuedTx, err) - return hash, err // SendTransaction is still blocked - } - - // allow SendTransaction to return - queuedTx.Hash = hash - queuedTx.Err = err - queuedTx.Done <- struct{}{} // sendTransaction() waits on this, notify so that it can return - - return hash, err -} - -// DiscardQueuedTransaction discards queued transaction forcing SendTransaction to return -func (b *StatusBackend) DiscardQueuedTransaction(id status.QueuedTxID) error { - queuedTx, err := b.txQueue.Get(id) - if err != nil { - return err - } - - // remove from queue, before notifying SendTransaction - b.TransactionQueue().Remove(queuedTx.ID) - - // allow SendTransaction to return - queuedTx.Err = status.ErrQueuedTxDiscarded - queuedTx.Discard <- struct{}{} // sendTransaction() waits on this, notify so that it can return - - return nil + return b.txapi.SendTransaction(ctx, SendTxArgs(args), passphrase) } // EstimateGas uses underlying blockchain API to obtain gas for a given tx arguments diff --git a/vendor/github.com/ethereum/go-ethereum/les/backend.go b/vendor/github.com/ethereum/go-ethereum/les/backend.go index 47a337543..c142afd43 100644 --- a/vendor/github.com/ethereum/go-ethereum/les/backend.go +++ b/vendor/github.com/ethereum/go-ethereum/les/backend.go @@ -213,7 +213,6 @@ func (s *LightEthereum) Start(srvr *p2p.Server) error { s.netRPCService = ethapi.NewPublicNetAPI(srvr, s.networkId) s.serverPool.start(srvr, lesTopic(s.blockchain.Genesis().Hash())) s.protocolManager.Start() - s.StatusBackend.Start() return nil } @@ -231,8 +230,6 @@ func (s *LightEthereum) Stop() error { s.chainDb.Close() close(s.shutdownChan) - s.StatusBackend.Stop() - return nil } diff --git a/vendor/github.com/ethereum/go-ethereum/les/status/accounts.go b/vendor/github.com/ethereum/go-ethereum/les/status/accounts.go index 78bd2ad92..ef60d9fcc 100644 --- a/vendor/github.com/ethereum/go-ethereum/les/status/accounts.go +++ b/vendor/github.com/ethereum/go-ethereum/les/status/accounts.go @@ -5,6 +5,8 @@ import ( "github.com/ethereum/go-ethereum/common" ) +const SelectedAccountKey = "selected_account" + // AccountManager abstracts both internal account manager and extra filter status backend requires type AccountManager struct { am *accounts.Manager diff --git a/vendor/github.com/ethereum/go-ethereum/les/status/txqueue.go b/vendor/github.com/ethereum/go-ethereum/les/status/txqueue.go deleted file mode 100644 index 1ca48ad0a..000000000 --- a/vendor/github.com/ethereum/go-ethereum/les/status/txqueue.go +++ /dev/null @@ -1,278 +0,0 @@ -package status - -import ( - "errors" - "os" - "runtime/debug" - "sync" - "time" - - "github.com/ethereum/go-ethereum/accounts/keystore" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/log" - "golang.org/x/net/context" -) - -const ( - DefaultTxQueueCap = int(35) // how many items can be queued - DefaultTxSendQueueCap = int(70) // how many items can be passed to sendTransaction() w/o blocking - DefaultTxSendCompletionTimeout = 300 // how many seconds to wait before returning result in sentTransaction() - SelectedAccountKey = "selected_account" -) - -var ( - ErrQueuedTxIDNotFound = errors.New("transaction hash not found") - ErrQueuedTxTimedOut = errors.New("transaction sending timed out") - ErrQueuedTxDiscarded = errors.New("transaction has been discarded") - ErrInvalidCompleteTxSender = errors.New("transaction can only be completed by the same account which created it") -) - -// TxQueue is capped container that holds pending transactions -type TxQueue struct { - transactions map[QueuedTxID]*QueuedTx - mu sync.RWMutex // to guard transactions map - evictableIDs chan QueuedTxID - enqueueTicker chan struct{} - incomingPool chan *QueuedTx - - // when this channel is closed, all queue channels processing must cease (incoming queue, processing queued items etc) - stopped chan struct{} - stoppedGroup sync.WaitGroup // to make sure that all routines are stopped - - // when items are enqueued notify subscriber - txEnqueueHandler EnqueuedTxHandler - - // when tx is returned (either successfully or with error) notify subscriber - txReturnHandler EnqueuedTxReturnHandler -} - -// QueuedTx holds enough information to complete the queued transaction. -type QueuedTx struct { - ID QueuedTxID - Hash common.Hash - Context context.Context - Args SendTxArgs - Done chan struct{} - Discard chan struct{} - Err error -} - -// QueuedTxID queued transaction identifier -type QueuedTxID string - -// EnqueuedTxHandler is a function that receives queued/pending transactions, when they get queued -type EnqueuedTxHandler func(QueuedTx) - -// EnqueuedTxReturnHandler is a function that receives response when tx is complete (both on success and error) -type EnqueuedTxReturnHandler func(queuedTx *QueuedTx, err error) - -// SendTxArgs represents the arguments to submit a new transaction into the transaction pool. -type SendTxArgs struct { - From common.Address `json:"from"` - To *common.Address `json:"to"` - Gas *hexutil.Big `json:"gas"` - GasPrice *hexutil.Big `json:"gasPrice"` - Value *hexutil.Big `json:"value"` - Data hexutil.Bytes `json:"data"` - Nonce *hexutil.Uint64 `json:"nonce"` -} - -// NewTransactionQueue make new transaction queue -func NewTransactionQueue() *TxQueue { - log.Info("StatusIM: initializing transaction queue") - return &TxQueue{ - transactions: make(map[QueuedTxID]*QueuedTx), - evictableIDs: make(chan QueuedTxID, DefaultTxQueueCap), // will be used to evict in FIFO - enqueueTicker: make(chan struct{}), - incomingPool: make(chan *QueuedTx, DefaultTxSendQueueCap), - } -} - -// Start starts enqueue and eviction loops -func (q *TxQueue) Start() { - log.Info("StatusIM: starting transaction queue") - - q.stopped = make(chan struct{}) - q.stoppedGroup.Add(2) - - go q.evictionLoop() - go q.enqueueLoop() -} - -// Stop stops transaction enqueue and eviction loops -func (q *TxQueue) Stop() { - log.Info("StatusIM: stopping transaction queue") - close(q.stopped) // stops all processing loops (enqueue, eviction etc) - q.stoppedGroup.Wait() -} - -// HaltOnPanic recovers from panic, logs issue, and exits -func HaltOnPanic() { - if r := recover(); r != nil { - log.Error("Runtime PANIC!!!", "error", r, "stack", string(debug.Stack())) - time.Sleep(5 * time.Second) // allow logger to flush logs - os.Exit(1) - } -} - -// evictionLoop frees up queue to accommodate another transaction item -func (q *TxQueue) evictionLoop() { - defer HaltOnPanic() - evict := func() { - if len(q.transactions) >= DefaultTxQueueCap { // eviction is required to accommodate another/last item - q.Remove(<-q.evictableIDs) - } - } - - for { - select { - case <-time.After(250 * time.Millisecond): // do not wait for manual ticks, check queue regularly - evict() - case <-q.enqueueTicker: // when manually requested - evict() - case <-q.stopped: - log.Info("StatusIM: transaction queue's eviction loop stopped") - q.stoppedGroup.Done() - return - } - } -} - -// enqueueLoop process incoming enqueue requests -func (q *TxQueue) enqueueLoop() { - defer HaltOnPanic() - - // enqueue incoming transactions - for { - select { - case queuedTx := <-q.incomingPool: - log.Info("StatusIM: transaction enqueued requested", "tx", queuedTx.ID) - q.Enqueue(queuedTx) - log.Info("StatusIM: transaction enqueued", "tx", queuedTx.ID) - case <-q.stopped: - log.Info("StatusIM: transaction queue's enqueue loop stopped") - q.stoppedGroup.Done() - return - } - } -} - -// Reset is to be used in tests only, as it simply creates new transaction map, w/o any cleanup of the previous one -func (q *TxQueue) Reset() { - q.mu.Lock() - defer q.mu.Unlock() - - q.transactions = make(map[QueuedTxID]*QueuedTx) - q.evictableIDs = make(chan QueuedTxID, DefaultTxQueueCap) -} - -// EnqueueAsync enqueues incoming transaction in async manner, returns as soon as possible -func (q *TxQueue) EnqueueAsync(tx *QueuedTx) error { - q.incomingPool <- tx - - return nil -} - -// Enqueue enqueues incoming transaction -func (q *TxQueue) Enqueue(tx *QueuedTx) error { - if q.txEnqueueHandler == nil { //discard, until handler is provided - return nil - } - - q.enqueueTicker <- struct{}{} // notify eviction loop that we are trying to insert new item - q.evictableIDs <- tx.ID // this will block when we hit DefaultTxQueueCap - - q.mu.Lock() - q.transactions[tx.ID] = tx - q.mu.Unlock() - - // notify handler - q.txEnqueueHandler(*tx) - - return nil -} - -// Get returns transaction by transaction identifier -func (q *TxQueue) Get(id QueuedTxID) (*QueuedTx, error) { - q.mu.RLock() - defer q.mu.RUnlock() - - if tx, ok := q.transactions[id]; ok { - return tx, nil - } - - return nil, ErrQueuedTxIDNotFound -} - -// Remove removes transaction by transaction identifier -func (q *TxQueue) Remove(id QueuedTxID) { - q.mu.Lock() - defer q.mu.Unlock() - - delete(q.transactions, id) -} - -// Count returns number of currently queued transactions -func (q *TxQueue) Count() int { - q.mu.RLock() - defer q.mu.RUnlock() - - return len(q.transactions) -} - -// Has checks whether transaction with a given identifier exists in queue -func (q *TxQueue) Has(id QueuedTxID) bool { - q.mu.RLock() - defer q.mu.RUnlock() - - _, ok := q.transactions[id] - - return ok -} - -// SetEnqueueHandler sets callback handler, that is triggered on enqueue operation -func (q *TxQueue) SetEnqueueHandler(fn EnqueuedTxHandler) { - q.txEnqueueHandler = fn -} - -// SetTxReturnHandler sets callback handler, that is triggered when transaction is finished executing -func (q *TxQueue) SetTxReturnHandler(fn EnqueuedTxReturnHandler) { - q.txReturnHandler = fn -} - -// NotifyOnQueuedTxReturn is invoked when transaction is ready to return -// Transaction can be in error state, or executed successfully at this point. -func (q *TxQueue) NotifyOnQueuedTxReturn(queuedTx *QueuedTx, err error) { - if q == nil { - return - } - - // discard, if transaction is not found - if queuedTx == nil { - return - } - - // on success, remove item from the queue and stop propagating - if err == nil { - q.Remove(queuedTx.ID) - return - } - - // error occurred, send upward notification - if q.txReturnHandler == nil { // discard, until handler is provided - return - } - - // remove from queue on any error (except for transient ones) and propagate - transientErrs := map[error]bool{ - keystore.ErrDecrypt: true, // wrong password - ErrInvalidCompleteTxSender: true, // completing tx create from another account - } - if !transientErrs[err] { // remove only on unrecoverable errors - q.Remove(queuedTx.ID) - } - - // notify handler - q.txReturnHandler(queuedTx, err) -} diff --git a/vendor/github.com/ethereum/go-ethereum/les/status/types.go b/vendor/github.com/ethereum/go-ethereum/les/status/types.go new file mode 100644 index 000000000..04437bdb6 --- /dev/null +++ b/vendor/github.com/ethereum/go-ethereum/les/status/types.go @@ -0,0 +1,17 @@ +package status + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" +) + +// SendTxArgs represents the arguments to submit a new transaction into the transaction pool. +type SendTxArgs struct { + From common.Address `json:"from"` + To *common.Address `json:"to"` + Gas *hexutil.Big `json:"gas"` + GasPrice *hexutil.Big `json:"gasPrice"` + Value *hexutil.Big `json:"value"` + Data hexutil.Bytes `json:"data"` + Nonce *hexutil.Uint64 `json:"nonce"` +} diff --git a/vendor/github.com/golang/mock/gomock/call.go b/vendor/github.com/golang/mock/gomock/call.go new file mode 100644 index 000000000..dc2a479d1 --- /dev/null +++ b/vendor/github.com/golang/mock/gomock/call.go @@ -0,0 +1,282 @@ +// Copyright 2010 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gomock + +import ( + "fmt" + "reflect" + "strconv" + "strings" +) + +// Call represents an expected call to a mock. +type Call struct { + t TestReporter // for triggering test failures on invalid call setup + + receiver interface{} // the receiver of the method call + method string // the name of the method + methodType reflect.Type // the type of the method + args []Matcher // the args + rets []interface{} // the return values (if any) + origin string // file and line number of call setup + + preReqs []*Call // prerequisite calls + + // Expectations + minCalls, maxCalls int + + numCalls int // actual number made + + // Actions + doFunc reflect.Value + setArgs map[int]reflect.Value +} + +// AnyTimes allows the expectation to be called 0 or more times +func (c *Call) AnyTimes() *Call { + c.minCalls, c.maxCalls = 0, 1e8 // close enough to infinity + return c +} + +// MinTimes requires the call to occur at least n times. If AnyTimes or MaxTimes have not been called, MinTimes also +// sets the maximum number of calls to infinity. +func (c *Call) MinTimes(n int) *Call { + c.minCalls = n + if c.maxCalls == 1 { + c.maxCalls = 1e8 + } + return c +} + +// MaxTimes limits the number of calls to n times. If AnyTimes or MinTimes have not been called, MaxTimes also +// sets the minimum number of calls to 0. +func (c *Call) MaxTimes(n int) *Call { + c.maxCalls = n + if c.minCalls == 1 { + c.minCalls = 0 + } + return c +} + +// Do declares the action to run when the call is matched. +// It takes an interface{} argument to support n-arity functions. +func (c *Call) Do(f interface{}) *Call { + // TODO: Check arity and types here, rather than dying badly elsewhere. + c.doFunc = reflect.ValueOf(f) + return c +} + +func (c *Call) Return(rets ...interface{}) *Call { + mt := c.methodType + if len(rets) != mt.NumOut() { + c.t.Fatalf("wrong number of arguments to Return for %T.%v: got %d, want %d [%s]", + c.receiver, c.method, len(rets), mt.NumOut(), c.origin) + } + for i, ret := range rets { + if got, want := reflect.TypeOf(ret), mt.Out(i); got == want { + // Identical types; nothing to do. + } else if got == nil { + // Nil needs special handling. + switch want.Kind() { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: + // ok + default: + c.t.Fatalf("argument %d to Return for %T.%v is nil, but %v is not nillable [%s]", + i, c.receiver, c.method, want, c.origin) + } + } else if got.AssignableTo(want) { + // Assignable type relation. Make the assignment now so that the generated code + // can return the values with a type assertion. + v := reflect.New(want).Elem() + v.Set(reflect.ValueOf(ret)) + rets[i] = v.Interface() + } else { + c.t.Fatalf("wrong type of argument %d to Return for %T.%v: %v is not assignable to %v [%s]", + i, c.receiver, c.method, got, want, c.origin) + } + } + + c.rets = rets + return c +} + +func (c *Call) Times(n int) *Call { + c.minCalls, c.maxCalls = n, n + return c +} + +// SetArg declares an action that will set the nth argument's value, +// indirected through a pointer. Or, in the case of a slice, SetArg +// will copy value's elements into the nth argument. +func (c *Call) SetArg(n int, value interface{}) *Call { + if c.setArgs == nil { + c.setArgs = make(map[int]reflect.Value) + } + mt := c.methodType + // TODO: This will break on variadic methods. + // We will need to check those at invocation time. + if n < 0 || n >= mt.NumIn() { + c.t.Fatalf("SetArg(%d, ...) called for a method with %d args [%s]", + n, mt.NumIn(), c.origin) + } + // Permit setting argument through an interface. + // In the interface case, we don't (nay, can't) check the type here. + at := mt.In(n) + switch at.Kind() { + case reflect.Ptr: + dt := at.Elem() + if vt := reflect.TypeOf(value); !vt.AssignableTo(dt) { + c.t.Fatalf("SetArg(%d, ...) argument is a %v, not assignable to %v [%s]", + n, vt, dt, c.origin) + } + case reflect.Interface: + // nothing to do + case reflect.Slice: + // nothing to do + default: + c.t.Fatalf("SetArg(%d, ...) referring to argument of non-pointer non-interface non-slice type %v", + n, at, c.origin) + } + c.setArgs[n] = reflect.ValueOf(value) + return c +} + +// isPreReq returns true if other is a direct or indirect prerequisite to c. +func (c *Call) isPreReq(other *Call) bool { + for _, preReq := range c.preReqs { + if other == preReq || preReq.isPreReq(other) { + return true + } + } + return false +} + +// After declares that the call may only match after preReq has been exhausted. +func (c *Call) After(preReq *Call) *Call { + if c == preReq { + c.t.Fatalf("A call isn't allowed to be its own prerequisite") + } + if preReq.isPreReq(c) { + c.t.Fatalf("Loop in call order: %v is a prerequisite to %v (possibly indirectly).", c, preReq) + } + + c.preReqs = append(c.preReqs, preReq) + return c +} + +// Returns true if the minimum number of calls have been made. +func (c *Call) satisfied() bool { + return c.numCalls >= c.minCalls +} + +// Returns true iff the maximum number of calls have been made. +func (c *Call) exhausted() bool { + return c.numCalls >= c.maxCalls +} + +func (c *Call) String() string { + args := make([]string, len(c.args)) + for i, arg := range c.args { + args[i] = arg.String() + } + arguments := strings.Join(args, ", ") + return fmt.Sprintf("%T.%v(%s) %s", c.receiver, c.method, arguments, c.origin) +} + +// Tests if the given call matches the expected call. +// If yes, returns nil. If no, returns error with message explaining why it does not match. +func (c *Call) matches(args []interface{}) error { + if len(args) != len(c.args) { + return fmt.Errorf("Expected call at %s has the wrong number of arguments. Got: %s, want: %s", + c.origin, strconv.Itoa(len(args)), strconv.Itoa(len(c.args))) + } + for i, m := range c.args { + if !m.Matches(args[i]) { + return fmt.Errorf("Expected call at %s doesn't match the argument at index %s.\nGot: %v\nWant: %v\n", + c.origin, strconv.Itoa(i), args[i], m) + } + } + + // Check that all prerequisite calls have been satisfied. + for _, preReqCall := range c.preReqs { + if !preReqCall.satisfied() { + return fmt.Errorf("Expected call at %s doesn't have a prerequisite call satisfied:\n%v\nshould be called before:\n%v", + c.origin, preReqCall, c) + } + } + + return nil +} + +// dropPrereqs tells the expected Call to not re-check prerequisite calls any +// longer, and to return its current set. +func (c *Call) dropPrereqs() (preReqs []*Call) { + preReqs = c.preReqs + c.preReqs = nil + return +} + +func (c *Call) call(args []interface{}) (rets []interface{}, action func()) { + c.numCalls++ + + // Actions + if c.doFunc.IsValid() { + doArgs := make([]reflect.Value, len(args)) + ft := c.doFunc.Type() + for i := 0; i < len(args); i++ { + if args[i] != nil { + doArgs[i] = reflect.ValueOf(args[i]) + } else { + // Use the zero value for the arg. + doArgs[i] = reflect.Zero(ft.In(i)) + } + } + action = func() { c.doFunc.Call(doArgs) } + } + for n, v := range c.setArgs { + switch reflect.TypeOf(args[n]).Kind() { + case reflect.Slice: + setSlice(args[n], v) + default: + reflect.ValueOf(args[n]).Elem().Set(v) + } + } + + rets = c.rets + if rets == nil { + // Synthesize the zero value for each of the return args' types. + mt := c.methodType + rets = make([]interface{}, mt.NumOut()) + for i := 0; i < mt.NumOut(); i++ { + rets[i] = reflect.Zero(mt.Out(i)).Interface() + } + } + + return +} + +// InOrder declares that the given calls should occur in order. +func InOrder(calls ...*Call) { + for i := 1; i < len(calls); i++ { + calls[i].After(calls[i-1]) + } +} + +func setSlice(arg interface{}, v reflect.Value) { + va := reflect.ValueOf(arg) + for i := 0; i < v.Len(); i++ { + va.Index(i).Set(v.Index(i)) + } +} diff --git a/vendor/github.com/golang/mock/gomock/callset.go b/vendor/github.com/golang/mock/gomock/callset.go new file mode 100644 index 000000000..a88a172f8 --- /dev/null +++ b/vendor/github.com/golang/mock/gomock/callset.go @@ -0,0 +1,86 @@ +// Copyright 2011 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gomock + +import ( + "errors" + "fmt" +) + +// callSet represents a set of expected calls, indexed by receiver and method +// name. +type callSet map[interface{}]map[string][]*Call + +// Add adds a new expected call. +func (cs callSet) Add(call *Call) { + methodMap, ok := cs[call.receiver] + if !ok { + methodMap = make(map[string][]*Call) + cs[call.receiver] = methodMap + } + methodMap[call.method] = append(methodMap[call.method], call) +} + +// Remove removes an expected call. +func (cs callSet) Remove(call *Call) { + methodMap, ok := cs[call.receiver] + if !ok { + return + } + sl := methodMap[call.method] + for i, c := range sl { + if c == call { + // quick removal; we don't need to maintain call order + if len(sl) > 1 { + sl[i] = sl[len(sl)-1] + } + methodMap[call.method] = sl[:len(sl)-1] + break + } + } +} + +// FindMatch searches for a matching call. Returns error with explanation message if no call matched. +func (cs callSet) FindMatch(receiver interface{}, method string, args []interface{}) (*Call, error) { + methodMap, ok := cs[receiver] + if !ok { + return nil, errors.New("there are no expected method calls for that receiver") + } + calls, ok := methodMap[method] + if !ok { + return nil, fmt.Errorf("there are no expected calls of the method: %s for that receiver", method) + } + + // Search through the unordered set of calls expected on a method on a + // receiver. + callsErrors := "" + for _, call := range calls { + // A call should not normally still be here if exhausted, + // but it can happen if, for instance, .Times(0) was used. + // Pretend the call doesn't match. + if call.exhausted() { + callsErrors += "\nThe call was exhausted." + continue + } + err := call.matches(args) + if err != nil { + callsErrors += "\n" + err.Error() + } else { + return call, nil + } + } + + return nil, fmt.Errorf(callsErrors) +} diff --git a/vendor/github.com/golang/mock/gomock/controller.go b/vendor/github.com/golang/mock/gomock/controller.go new file mode 100644 index 000000000..d06dfbbe8 --- /dev/null +++ b/vendor/github.com/golang/mock/gomock/controller.go @@ -0,0 +1,193 @@ +// Copyright 2010 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// GoMock - a mock framework for Go. +// +// Standard usage: +// (1) Define an interface that you wish to mock. +// type MyInterface interface { +// SomeMethod(x int64, y string) +// } +// (2) Use mockgen to generate a mock from the interface. +// (3) Use the mock in a test: +// func TestMyThing(t *testing.T) { +// mockCtrl := gomock.NewController(t) +// defer mockCtrl.Finish() +// +// mockObj := something.NewMockMyInterface(mockCtrl) +// mockObj.EXPECT().SomeMethod(4, "blah") +// // pass mockObj to a real object and play with it. +// } +// +// By default, expected calls are not enforced to run in any particular order. +// Call order dependency can be enforced by use of InOrder and/or Call.After. +// Call.After can create more varied call order dependencies, but InOrder is +// often more convenient. +// +// The following examples create equivalent call order dependencies. +// +// Example of using Call.After to chain expected call order: +// +// firstCall := mockObj.EXPECT().SomeMethod(1, "first") +// secondCall := mockObj.EXPECT().SomeMethod(2, "second").After(firstCall) +// mockObj.EXPECT().SomeMethod(3, "third").After(secondCall) +// +// Example of using InOrder to declare expected call order: +// +// gomock.InOrder( +// mockObj.EXPECT().SomeMethod(1, "first"), +// mockObj.EXPECT().SomeMethod(2, "second"), +// mockObj.EXPECT().SomeMethod(3, "third"), +// ) +// +// TODO: +// - Handle different argument/return types (e.g. ..., chan, map, interface). +package gomock + +import ( + "fmt" + "reflect" + "runtime" + "sync" +) + +// A TestReporter is something that can be used to report test failures. +// It is satisfied by the standard library's *testing.T. +type TestReporter interface { + Errorf(format string, args ...interface{}) + Fatalf(format string, args ...interface{}) +} + +// A Controller represents the top-level control of a mock ecosystem. +// It defines the scope and lifetime of mock objects, as well as their expectations. +// It is safe to call Controller's methods from multiple goroutines. +type Controller struct { + mu sync.Mutex + t TestReporter + expectedCalls callSet +} + +func NewController(t TestReporter) *Controller { + return &Controller{ + t: t, + expectedCalls: make(callSet), + } +} + +func (ctrl *Controller) RecordCall(receiver interface{}, method string, args ...interface{}) *Call { + recv := reflect.ValueOf(receiver) + for i := 0; i < recv.Type().NumMethod(); i++ { + if recv.Type().Method(i).Name == method { + return ctrl.RecordCallWithMethodType(receiver, method, recv.Method(i).Type(), args...) + } + } + ctrl.t.Fatalf("gomock: failed finding method %s on %T", method, receiver) + // In case t.Fatalf does not panic. + panic(fmt.Sprintf("gomock: failed finding method %s on %T", method, receiver)) +} + +func (ctrl *Controller) RecordCallWithMethodType(receiver interface{}, method string, methodType reflect.Type, args ...interface{}) *Call { + // TODO: check arity, types. + margs := make([]Matcher, len(args)) + for i, arg := range args { + if m, ok := arg.(Matcher); ok { + margs[i] = m + } else if arg == nil { + // Handle nil specially so that passing a nil interface value + // will match the typed nils of concrete args. + margs[i] = Nil() + } else { + margs[i] = Eq(arg) + } + } + + ctrl.mu.Lock() + defer ctrl.mu.Unlock() + + origin := callerInfo(2) + call := &Call{t: ctrl.t, receiver: receiver, method: method, methodType: methodType, args: margs, origin: origin, minCalls: 1, maxCalls: 1} + + ctrl.expectedCalls.Add(call) + return call +} + +func (ctrl *Controller) Call(receiver interface{}, method string, args ...interface{}) []interface{} { + ctrl.mu.Lock() + defer ctrl.mu.Unlock() + + expected, err := ctrl.expectedCalls.FindMatch(receiver, method, args) + if err != nil { + origin := callerInfo(2) + ctrl.t.Fatalf("Unexpected call to %T.%v(%v) at %s because: %s", receiver, method, args, origin, err) + } + + // Two things happen here: + // * the matching call no longer needs to check prerequite calls, + // * and the prerequite calls are no longer expected, so remove them. + preReqCalls := expected.dropPrereqs() + for _, preReqCall := range preReqCalls { + ctrl.expectedCalls.Remove(preReqCall) + } + + rets, action := expected.call(args) + if expected.exhausted() { + ctrl.expectedCalls.Remove(expected) + } + + // Don't hold the lock while doing the call's action (if any) + // so that actions may execute concurrently. + // We use the deferred Unlock to capture any panics that happen above; + // here we add a deferred Lock to balance it. + ctrl.mu.Unlock() + defer ctrl.mu.Lock() + if action != nil { + action() + } + + return rets +} + +func (ctrl *Controller) Finish() { + ctrl.mu.Lock() + defer ctrl.mu.Unlock() + + // If we're currently panicking, probably because this is a deferred call, + // pass through the panic. + if err := recover(); err != nil { + panic(err) + } + + // Check that all remaining expected calls are satisfied. + failures := false + for _, methodMap := range ctrl.expectedCalls { + for _, calls := range methodMap { + for _, call := range calls { + if !call.satisfied() { + ctrl.t.Errorf("missing call(s) to %v", call) + failures = true + } + } + } + } + if failures { + ctrl.t.Fatalf("aborting test due to missing call(s)") + } +} + +func callerInfo(skip int) string { + if _, file, line, ok := runtime.Caller(skip + 1); ok { + return fmt.Sprintf("%s:%d", file, line) + } + return "unknown file" +} diff --git a/vendor/github.com/golang/mock/gomock/matchers.go b/vendor/github.com/golang/mock/gomock/matchers.go new file mode 100644 index 000000000..e8b1ddccf --- /dev/null +++ b/vendor/github.com/golang/mock/gomock/matchers.go @@ -0,0 +1,99 @@ +//go:generate mockgen -destination mock_matcher/mock_matcher.go github.com/golang/mock/gomock Matcher + +// Copyright 2010 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gomock + +import ( + "fmt" + "reflect" +) + +// A Matcher is a representation of a class of values. +// It is used to represent the valid or expected arguments to a mocked method. +type Matcher interface { + // Matches returns whether x is a match. + Matches(x interface{}) bool + + // String describes what the matcher matches. + String() string +} + +type anyMatcher struct{} + +func (anyMatcher) Matches(x interface{}) bool { + return true +} + +func (anyMatcher) String() string { + return "is anything" +} + +type eqMatcher struct { + x interface{} +} + +func (e eqMatcher) Matches(x interface{}) bool { + return reflect.DeepEqual(e.x, x) +} + +func (e eqMatcher) String() string { + return fmt.Sprintf("is equal to %v", e.x) +} + +type nilMatcher struct{} + +func (nilMatcher) Matches(x interface{}) bool { + if x == nil { + return true + } + + v := reflect.ValueOf(x) + switch v.Kind() { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, + reflect.Ptr, reflect.Slice: + return v.IsNil() + } + + return false +} + +func (nilMatcher) String() string { + return "is nil" +} + +type notMatcher struct { + m Matcher +} + +func (n notMatcher) Matches(x interface{}) bool { + return !n.m.Matches(x) +} + +func (n notMatcher) String() string { + // TODO: Improve this if we add a NotString method to the Matcher interface. + return "not(" + n.m.String() + ")" +} + +// Constructors +func Any() Matcher { return anyMatcher{} } +func Eq(x interface{}) Matcher { return eqMatcher{x} } +func Nil() Matcher { return nilMatcher{} } +func Not(x interface{}) Matcher { + if m, ok := x.(Matcher); ok { + return notMatcher{m} + } + return notMatcher{Eq(x)} +} diff --git a/vendor/github.com/golang/mock/gomock/mock_matcher/mock_matcher.go b/vendor/github.com/golang/mock/gomock/mock_matcher/mock_matcher.go new file mode 100644 index 000000000..3328ab7a0 --- /dev/null +++ b/vendor/github.com/golang/mock/gomock/mock_matcher/mock_matcher.go @@ -0,0 +1,56 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/golang/mock/gomock (interfaces: Matcher) + +package mock_gomock + +import ( + gomock "github.com/golang/mock/gomock" + reflect "reflect" +) + +// MockMatcher is a mock of Matcher interface +type MockMatcher struct { + ctrl *gomock.Controller + recorder *MockMatcherMockRecorder +} + +// MockMatcherMockRecorder is the mock recorder for MockMatcher +type MockMatcherMockRecorder struct { + mock *MockMatcher +} + +// NewMockMatcher creates a new mock instance +func NewMockMatcher(ctrl *gomock.Controller) *MockMatcher { + mock := &MockMatcher{ctrl: ctrl} + mock.recorder = &MockMatcherMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockMatcher) EXPECT() *MockMatcherMockRecorder { + return m.recorder +} + +// Matches mocks base method +func (m *MockMatcher) Matches(arg0 interface{}) bool { + ret := m.ctrl.Call(m, "Matches", arg0) + ret0, _ := ret[0].(bool) + return ret0 +} + +// Matches indicates an expected call of Matches +func (mr *MockMatcherMockRecorder) Matches(arg0 interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Matches", reflect.TypeOf((*MockMatcher)(nil).Matches), arg0) +} + +// String mocks base method +func (m *MockMatcher) String() string { + ret := m.ctrl.Call(m, "String") + ret0, _ := ret[0].(string) + return ret0 +} + +// String indicates an expected call of String +func (mr *MockMatcherMockRecorder) String() *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "String", reflect.TypeOf((*MockMatcher)(nil).String)) +}