Merge pull request #773 from status-im/transactions-and-accountmanager

[#772] txQueueManager to not depend on AccountManager and nodeManager
This commit is contained in:
Adrià Cidre 2018-03-28 11:06:33 +02:00 committed by GitHub
commit e646001578
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 151 additions and 189 deletions

View File

@ -152,12 +152,12 @@ func (api *StatusAPI) SendTransaction(ctx context.Context, args common.SendTxArg
// CompleteTransaction instructs backend to complete sending of a given transaction
func (api *StatusAPI) CompleteTransaction(id common.QueuedTxID, password string) (gethcommon.Hash, error) {
return api.b.txQueueManager.CompleteTransaction(id, password)
return api.b.CompleteTransaction(id, password)
}
// CompleteTransactions instructs backend to complete sending of multiple transactions
func (api *StatusAPI) CompleteTransactions(ids []common.QueuedTxID, password string) map[common.QueuedTxID]common.TransactionResult {
return api.b.txQueueManager.CompleteTransactions(ids, password)
return api.b.CompleteTransactions(ids, password)
}
// DiscardTransaction discards a given transaction from transaction queue

View File

@ -49,7 +49,7 @@ func NewStatusBackend() *StatusBackend {
nodeManager := node.NewNodeManager()
accountManager := account.NewManager(nodeManager)
txQueueManager := transactions.NewManager(nodeManager, accountManager)
txQueueManager := transactions.NewManager(nodeManager)
jailManager := jail.New(nodeManager)
notificationManager := fcm.NewNotification(fcmServerKey)
@ -120,7 +120,7 @@ func (b *StatusBackend) startNode(config *params.NodeConfig) (err error) {
signal.Send(signal.Envelope{Type: signal.EventNodeStarted})
// tx queue manager should be started after node is started, it depends
// on rpc client being created
b.txQueueManager.Start()
b.txQueueManager.Start(config.NetworkID)
if err := b.registerHandlers(); err != nil {
b.log.Error("Handler registration failed", "err", err)
}
@ -208,14 +208,46 @@ func (b *StatusBackend) SendTransaction(ctx context.Context, args common.SendTxA
return rst.Hash, nil
}
func (b *StatusBackend) getVerifiedAccount(password string) (*account.SelectedExtKey, error) {
selectedAccount, err := b.accountManager.SelectedAccount()
if err != nil {
b.log.Error("failed to get a selected account", "err", err)
return nil, err
}
config, err := b.NodeManager().NodeConfig()
if err != nil {
return nil, err
}
_, err = b.accountManager.VerifyAccountPassword(config.KeyStoreDir, selectedAccount.Address.String(), password)
if err != nil {
b.log.Error("failed to verify account", "account", selectedAccount.Address.String(), "error", err)
return nil, err
}
return selectedAccount, nil
}
// CompleteTransaction instructs backend to complete sending of a given transaction
func (b *StatusBackend) CompleteTransaction(id common.QueuedTxID, password string) (gethcommon.Hash, error) {
return b.txQueueManager.CompleteTransaction(id, password)
func (b *StatusBackend) CompleteTransaction(id common.QueuedTxID, password string) (hash gethcommon.Hash, err error) {
selectedAccount, err := b.getVerifiedAccount(password)
if err != nil {
_ = b.txQueueManager.NotifyErrored(id, err)
return hash, err
}
return b.txQueueManager.CompleteTransaction(id, selectedAccount)
}
// CompleteTransactions instructs backend to complete sending of multiple transactions
func (b *StatusBackend) CompleteTransactions(ids []common.QueuedTxID, password string) map[common.QueuedTxID]common.TransactionResult {
return b.txQueueManager.CompleteTransactions(ids, password)
results := make(map[common.QueuedTxID]common.TransactionResult)
for _, txID := range ids {
txHash, txErr := b.CompleteTransaction(txID, password)
results[txID] = common.TransactionResult{
Hash: txHash,
Error: txErr,
}
}
return results
}
// DiscardTransaction discards a given transaction from transaction queue

View File

@ -1,58 +0,0 @@
package transactions
import (
"reflect"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/golang/mock/gomock"
"github.com/status-im/status-go/geth/account"
)
// 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
}
// 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)
}
// SelectedAccount mocks base method
func (m *MockAccountManager) SelectedAccount() (*account.SelectedExtKey, error) {
ret := m.ctrl.Call(m, "SelectedAccount")
ret0, _ := ret[0].(*account.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))
}

View File

@ -1,16 +0,0 @@
package transactions
import (
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/status-im/status-go/geth/account"
)
// Accounter defines expected methods for managing Status accounts.
type Accounter interface {
// SelectedAccount returns currently selected account
SelectedAccount() (*account.SelectedExtKey, error)
// VerifyAccountPassword tries to decrypt a given account key file, with a provided password.
// If no error is returned, then account is considered verified.
VerifyAccountPassword(keyStoreDir, address, password string) (*keystore.Key, error)
}

View File

@ -13,7 +13,7 @@ import (
"github.com/ethereum/go-ethereum/core/types"
"github.com/status-im/status-go/geth/account"
"github.com/status-im/status-go/geth/common"
"github.com/status-im/status-go/geth/params"
"github.com/status-im/status-go/geth/rpc"
"github.com/status-im/status-go/geth/transactions/queue"
)
@ -27,15 +27,21 @@ const (
defaultTimeout = time.Minute
)
// RPCClientProvider is an interface that provides a way
// to obtain an rpc.Client.
type RPCClientProvider interface {
RPCClient() *rpc.Client
}
// Manager provides means to manage internal Status Backend (injected into LES)
type Manager struct {
nodeManager common.NodeManager
accountManager Accounter
rpcClientProvider RPCClientProvider
txQueue *queue.TxQueue
ethTxClient EthTransactor
notify bool
completionTimeout time.Duration
rpcCallTimeout time.Duration
networkID uint64
addrLock *AddrLocker
localNonce sync.Map
@ -43,10 +49,9 @@ type Manager struct {
}
// NewManager returns a new Manager.
func NewManager(nodeManager common.NodeManager, accountManager Accounter) *Manager {
func NewManager(rpcClientProvider RPCClientProvider) *Manager {
return &Manager{
nodeManager: nodeManager,
accountManager: accountManager,
rpcClientProvider: rpcClientProvider,
txQueue: queue.New(),
addrLock: &AddrLocker{},
notify: true,
@ -64,9 +69,10 @@ func (m *Manager) DisableNotificactions() {
}
// Start starts accepting new transactions into the queue.
func (m *Manager) Start() {
func (m *Manager) Start(networkID uint64) {
m.log.Info("start Manager")
m.ethTxClient = NewEthTxClient(m.nodeManager.RPCClient())
m.networkID = networkID
m.ethTxClient = NewEthTxClient(m.rpcClientProvider.RPCClient())
m.txQueue.Start()
}
@ -125,8 +131,23 @@ func (m *Manager) WaitForTransaction(tx *common.QueuedTx) common.TransactionResu
}
}
// NotifyErrored sends a notification for the given transaction
func (m *Manager) NotifyErrored(id common.QueuedTxID, inputError error) error {
tx, err := m.txQueue.Get(id)
if err != nil {
m.log.Warn("error getting a queued transaction", "err", err)
return err
}
if m.notify {
NotifyOnReturn(tx, inputError)
}
return nil
}
// CompleteTransaction instructs backend to complete sending of a given transaction.
func (m *Manager) CompleteTransaction(id common.QueuedTxID, password string) (hash gethcommon.Hash, err error) {
func (m *Manager) CompleteTransaction(id common.QueuedTxID, account *account.SelectedExtKey) (hash gethcommon.Hash, err error) {
m.log.Info("complete transaction", "id", id)
tx, err := m.txQueue.Get(id)
if err != nil {
@ -137,41 +158,33 @@ func (m *Manager) CompleteTransaction(id common.QueuedTxID, password string) (ha
m.log.Warn("can't process transaction", "err", err)
return hash, err
}
config, err := m.nodeManager.NodeConfig()
if err != nil {
return hash, err
}
account, err := m.validateAccount(config, tx, password)
if err != nil {
if err := m.validateAccount(tx, account); err != nil {
m.txDone(tx, hash, err)
return hash, err
}
hash, err = m.completeTransaction(config, account, tx)
hash, err = m.completeTransaction(account, tx)
m.log.Info("finally completed transaction", "id", tx.ID, "hash", hash, "err", err)
m.txDone(tx, hash, err)
return hash, err
}
func (m *Manager) validateAccount(config *params.NodeConfig, tx *common.QueuedTx, password string) (*account.SelectedExtKey, error) {
selectedAccount, err := m.accountManager.SelectedAccount()
if err != nil {
m.log.Warn("failed to get a selected account", "err", err)
return nil, err
// make sure that only account which created the tx can complete it
func (m *Manager) validateAccount(tx *common.QueuedTx, selectedAccount *account.SelectedExtKey) error {
if selectedAccount == nil {
return account.ErrNoAccountSelected
}
// make sure that only account which created the tx can complete it
if tx.Args.From.Hex() != selectedAccount.Address.Hex() {
m.log.Warn("queued transaction does not belong to the selected account", "err", queue.ErrInvalidCompleteTxSender)
return nil, queue.ErrInvalidCompleteTxSender
return queue.ErrInvalidCompleteTxSender
}
_, err = m.accountManager.VerifyAccountPassword(config.KeyStoreDir, selectedAccount.Address.String(), password)
if err != nil {
m.log.Warn("failed to verify account", "account", selectedAccount.Address.String(), "error", err)
return nil, err
}
return selectedAccount, nil
return nil
}
func (m *Manager) completeTransaction(config *params.NodeConfig, selectedAccount *account.SelectedExtKey, queuedTx *common.QueuedTx) (hash gethcommon.Hash, err error) {
func (m *Manager) completeTransaction(selectedAccount *account.SelectedExtKey, queuedTx *common.QueuedTx) (hash gethcommon.Hash, err error) {
m.log.Info("complete transaction", "id", queuedTx.ID)
m.addrLock.LockAddr(queuedTx.Args.From)
var localNonce uint64
@ -210,7 +223,7 @@ func (m *Manager) completeTransaction(config *params.NodeConfig, selectedAccount
}
}
chainID := big.NewInt(int64(config.NetworkID))
chainID := big.NewInt(int64(m.networkID))
value := (*big.Int)(args.Value)
toAddr := gethcommon.Address{}
if args.To != nil {
@ -260,19 +273,6 @@ func (m *Manager) completeTransaction(config *params.NodeConfig, selectedAccount
return signedTx.Hash(), nil
}
// CompleteTransactions instructs backend to complete sending of multiple transactions
func (m *Manager) CompleteTransactions(ids []common.QueuedTxID, password string) map[common.QueuedTxID]common.TransactionResult {
results := make(map[common.QueuedTxID]common.TransactionResult)
for _, txID := range ids {
txHash, txErr := m.CompleteTransaction(txID, password)
results[txID] = common.TransactionResult{
Hash: txHash,
Error: txErr,
}
}
return results
}
// DiscardTransaction discards a given transaction from transaction queue
func (m *Manager) DiscardTransaction(id common.QueuedTxID) error {
tx, err := m.txQueue.Get(id)

View File

@ -33,26 +33,22 @@ func TestTxQueueTestSuite(t *testing.T) {
type TxQueueTestSuite struct {
suite.Suite
nodeManagerMockCtrl *gomock.Controller
nodeManagerMock *common.MockNodeManager
accountManagerMockCtrl *gomock.Controller
accountManagerMock *MockAccountManager
server *gethrpc.Server
client *gethrpc.Client
txServiceMockCtrl *gomock.Controller
txServiceMock *fake.MockPublicTransactionPoolAPI
nodeConfig *params.NodeConfig
nodeManagerMockCtrl *gomock.Controller
nodeManagerMock *common.MockNodeManager
server *gethrpc.Server
client *gethrpc.Client
txServiceMockCtrl *gomock.Controller
txServiceMock *fake.MockPublicTransactionPoolAPI
nodeConfig *params.NodeConfig
manager *Manager
}
func (s *TxQueueTestSuite) SetupTest() {
s.nodeManagerMockCtrl = gomock.NewController(s.T())
s.accountManagerMockCtrl = gomock.NewController(s.T())
s.txServiceMockCtrl = gomock.NewController(s.T())
s.nodeManagerMock = common.NewMockNodeManager(s.nodeManagerMockCtrl)
s.accountManagerMock = NewMockAccountManager(s.accountManagerMockCtrl)
s.server, s.txServiceMock = fake.NewTestServer(s.txServiceMockCtrl)
s.client = gethrpc.DialInProc(s.server)
@ -62,17 +58,16 @@ func (s *TxQueueTestSuite) SetupTest() {
s.Require().NoError(err)
s.nodeConfig = nodeConfig
s.manager = NewManager(s.nodeManagerMock, s.accountManagerMock)
s.manager = NewManager(s.nodeManagerMock)
s.manager.DisableNotificactions()
s.manager.completionTimeout = time.Second
s.manager.rpcCallTimeout = time.Second
s.manager.Start()
s.manager.Start(params.RopstenNetworkID)
}
func (s *TxQueueTestSuite) TearDownTest() {
s.manager.Stop()
s.nodeManagerMockCtrl.Finish()
s.accountManagerMockCtrl.Finish()
s.txServiceMockCtrl.Finish()
s.server.Stop()
s.client.Close()
@ -125,15 +120,7 @@ func (s *TxQueueTestSuite) rlpEncodeTx(tx *common.QueuedTx, config *params.NodeC
return hexutil.Bytes(data)
}
func (s *TxQueueTestSuite) setupStatusBackend(account *account.SelectedExtKey, password string, passwordErr error) {
s.nodeManagerMock.EXPECT().NodeConfig().Return(s.nodeConfig, nil)
s.accountManagerMock.EXPECT().SelectedAccount().Return(account, nil)
s.accountManagerMock.EXPECT().VerifyAccountPassword(s.nodeConfig.KeyStoreDir, account.Address.String(), password).Return(
nil, passwordErr)
}
func (s *TxQueueTestSuite) TestCompleteTransaction() {
password := TestConfig.Account1.Password
key, _ := crypto.GenerateKey()
selectedAccount := &account.SelectedExtKey{
Address: account.FromAddress(TestConfig.Account1.Address),
@ -169,7 +156,6 @@ func (s *TxQueueTestSuite) TestCompleteTransaction() {
for _, testCase := range testCases {
s.T().Run(testCase.name, func(t *testing.T) {
s.SetupTest()
s.setupStatusBackend(selectedAccount, password, nil)
tx := common.CreateTransaction(context.Background(), common.SendTxArgs{
From: account.FromAddress(TestConfig.Account1.Address),
To: account.ToAddress(TestConfig.Account2.Address),
@ -185,7 +171,7 @@ func (s *TxQueueTestSuite) TestCompleteTransaction() {
err error
)
go func() {
hash, err = s.manager.CompleteTransaction(tx.ID, password)
hash, err = s.manager.CompleteTransaction(tx.ID, selectedAccount)
s.NoError(err)
close(w)
}()
@ -202,13 +188,11 @@ func (s *TxQueueTestSuite) TestCompleteTransaction() {
}
func (s *TxQueueTestSuite) TestCompleteTransactionMultipleTimes() {
password := TestConfig.Account1.Password
key, _ := crypto.GenerateKey()
selectedAccount := &account.SelectedExtKey{
Address: account.FromAddress(TestConfig.Account1.Address),
AccountKey: &keystore.Key{PrivateKey: key},
}
s.setupStatusBackend(selectedAccount, password, nil)
tx := common.CreateTransaction(context.Background(), common.SendTxArgs{
From: account.FromAddress(TestConfig.Account1.Address),
@ -231,7 +215,7 @@ func (s *TxQueueTestSuite) TestCompleteTransactionMultipleTimes() {
wg.Add(1)
go func() {
defer wg.Done()
_, err := s.manager.CompleteTransaction(tx.ID, password)
_, err := s.manager.CompleteTransaction(tx.ID, selectedAccount)
mu.Lock()
defer mu.Unlock()
if err == nil {
@ -257,42 +241,19 @@ func (s *TxQueueTestSuite) TestCompleteTransactionMultipleTimes() {
}
func (s *TxQueueTestSuite) TestAccountMismatch() {
s.nodeManagerMock.EXPECT().NodeConfig().Return(s.nodeConfig, nil)
s.accountManagerMock.EXPECT().SelectedAccount().Return(&account.SelectedExtKey{
Address: account.FromAddress(TestConfig.Account2.Address),
}, nil)
tx := common.CreateTransaction(context.Background(), common.SendTxArgs{
From: account.FromAddress(TestConfig.Account1.Address),
To: account.ToAddress(TestConfig.Account2.Address),
})
s.NoError(s.manager.QueueTransaction(tx))
_, err := s.manager.CompleteTransaction(tx.ID, TestConfig.Account1.Password)
s.Equal(err, queue.ErrInvalidCompleteTxSender)
// Transaction should stay in the queue as mismatched accounts
// is a recoverable error.
s.True(s.manager.TransactionQueue().Has(tx.ID))
}
func (s *TxQueueTestSuite) TestInvalidPassword() {
password := "invalid-password"
key, _ := crypto.GenerateKey()
selectedAccount := &account.SelectedExtKey{
Address: account.FromAddress(TestConfig.Account1.Address),
AccountKey: &keystore.Key{PrivateKey: key},
Address: account.FromAddress(TestConfig.Account2.Address),
}
s.setupStatusBackend(selectedAccount, password, keystore.ErrDecrypt)
tx := common.CreateTransaction(context.Background(), common.SendTxArgs{
From: account.FromAddress(TestConfig.Account1.Address),
To: account.ToAddress(TestConfig.Account2.Address),
})
s.NoError(s.manager.QueueTransaction(tx))
_, err := s.manager.CompleteTransaction(tx.ID, password)
s.Equal(err.Error(), keystore.ErrDecrypt.Error())
_, err := s.manager.CompleteTransaction(tx.ID, selectedAccount)
s.Equal(err, queue.ErrInvalidCompleteTxSender)
// Transaction should stay in the queue as mismatched accounts
// is a recoverable error.
@ -319,6 +280,54 @@ func (s *TxQueueTestSuite) TestDiscardTransaction() {
s.NoError(WaitClosed(w, time.Second))
}
func (s *TxQueueTestSuite) TestDiscardTransactions() {
tx := common.CreateTransaction(context.Background(), common.SendTxArgs{
From: account.FromAddress(TestConfig.Account1.Address),
To: account.ToAddress(TestConfig.Account2.Address),
})
var ids []common.QueuedTxID
ids = append(ids, tx.ID)
s.NoError(s.manager.QueueTransaction(tx))
w := make(chan struct{})
go func() {
result := s.manager.DiscardTransactions(ids)
s.Equal(0, len(result))
close(w)
}()
rst := s.manager.WaitForTransaction(tx)
s.Equal(ErrQueuedTxDiscarded, rst.Error)
// Transaction should be already removed from the queue.
s.False(s.manager.TransactionQueue().Has(tx.ID))
s.NoError(WaitClosed(w, time.Second))
}
func (s *TxQueueTestSuite) TestDiscardTransactionsOnError() {
fakeTxID := common.QueuedTxID("7ab94f26-a866-4aba-1234-b4bbe98737a9")
tx := common.CreateTransaction(context.Background(), common.SendTxArgs{
From: account.FromAddress(TestConfig.Account1.Address),
To: account.ToAddress(TestConfig.Account2.Address),
})
var ids []common.QueuedTxID
ids = append(ids, fakeTxID)
s.NoError(s.manager.QueueTransaction(tx))
w := make(chan struct{})
go func() {
result := s.manager.DiscardTransactions(ids)
s.Equal(1, len(result))
s.Equal(queue.ErrQueuedTxIDNotFound, result[fakeTxID].Error)
close(w)
}()
rst := s.manager.WaitForTransaction(tx)
s.Equal(ErrQueuedTxTimedOut, rst.Error)
// Transaction should be already removed from the queue.
s.False(s.manager.TransactionQueue().Has(tx.ID))
s.NoError(WaitClosed(w, time.Second))
}
func (s *TxQueueTestSuite) TestCompletionTimedOut() {
tx := common.CreateTransaction(context.Background(), common.SendTxArgs{
From: account.FromAddress(TestConfig.Account1.Address),
@ -339,16 +348,11 @@ func (s *TxQueueTestSuite) TestCompletionTimedOut() {
// as the last step, we verify that if tx failed nonce is not updated
func (s *TxQueueTestSuite) TestLocalNonce() {
txCount := 3
password := TestConfig.Account1.Password
key, _ := crypto.GenerateKey()
selectedAccount := &account.SelectedExtKey{
Address: account.FromAddress(TestConfig.Account1.Address),
AccountKey: &keystore.Key{PrivateKey: key},
}
// setup call expectations for 5 transactions in total
for i := 0; i < txCount+2; i++ {
s.setupStatusBackend(selectedAccount, password, nil)
}
nonce := hexutil.Uint64(0)
for i := 0; i < txCount; i++ {
tx := common.CreateTransaction(context.Background(), common.SendTxArgs{
@ -357,7 +361,7 @@ func (s *TxQueueTestSuite) TestLocalNonce() {
})
s.setupTransactionPoolAPI(tx, nonce, hexutil.Uint64(i), selectedAccount, nil)
s.NoError(s.manager.QueueTransaction(tx))
hash, err := s.manager.CompleteTransaction(tx.ID, password)
hash, err := s.manager.CompleteTransaction(tx.ID, selectedAccount)
rst := s.manager.WaitForTransaction(tx)
// simple sanity checks
s.NoError(err)
@ -373,7 +377,7 @@ func (s *TxQueueTestSuite) TestLocalNonce() {
})
s.setupTransactionPoolAPI(tx, nonce, nonce, selectedAccount, nil)
s.NoError(s.manager.QueueTransaction(tx))
hash, err := s.manager.CompleteTransaction(tx.ID, password)
hash, err := s.manager.CompleteTransaction(tx.ID, selectedAccount)
rst := s.manager.WaitForTransaction(tx)
s.NoError(err)
s.NoError(rst.Error)
@ -388,7 +392,7 @@ func (s *TxQueueTestSuite) TestLocalNonce() {
To: account.ToAddress(TestConfig.Account2.Address),
})
s.NoError(s.manager.QueueTransaction(tx))
_, err = s.manager.CompleteTransaction(tx.ID, password)
_, err = s.manager.CompleteTransaction(tx.ID, selectedAccount)
rst = s.manager.WaitForTransaction(tx)
s.EqualError(testErr, err.Error())
s.EqualError(testErr, rst.Error.Error())

View File

@ -599,12 +599,12 @@ func (s *TransactionsTestSuite) TestDiscardMultipleQueuedTransactions() {
txIDs = append(txIDs, "invalid-tx-id")
// discard
discardResults := txQueueManager.DiscardTransactions(txIDs)
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 := txQueueManager.CompleteTransactions(txIDs, 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 {