Use single codepath for sending transactions to a local and remote nodes (#527)
- new EthereumTransactor that provides higher level API for working with ethereum network, and it is fully conformant with ethclient - new test rpc service that improves flexibility and coverage of txqueue manager tests - run complete transaction sequantially for each address - go-ethereum: Get pending nonce from transaction pool - add a patch with getting nonce from transaction pool
This commit is contained in:
parent
ea55ac0744
commit
0771e7d1b7
|
@ -23,7 +23,7 @@ jobs:
|
||||||
# using fork == false may be preferred as it would allow PRs from the origin
|
# using fork == false may be preferred as it would allow PRs from the origin
|
||||||
# to still run, but currently does not work due to a bug
|
# to still run, but currently does not work due to a bug
|
||||||
if: type != pull_request
|
if: type != pull_request
|
||||||
script: travis_wait 300 make test-e2e networkid=4
|
script: make test-e2e networkid=4
|
||||||
cache:
|
cache:
|
||||||
directories:
|
directories:
|
||||||
- ".ethereumtest/Mainnet"
|
- ".ethereumtest/Mainnet"
|
||||||
|
|
1
Makefile
1
Makefile
|
@ -113,6 +113,7 @@ mock: ##@other Regenerate mocks
|
||||||
mockgen -source=geth/mailservice/mailservice.go -destination=geth/mailservice/mailservice_mock.go -package=mailservice
|
mockgen -source=geth/mailservice/mailservice.go -destination=geth/mailservice/mailservice_mock.go -package=mailservice
|
||||||
mockgen -source=geth/common/notification.go -destination=geth/common/notification_mock.go -package=common -imports fcm=github.com/NaySoftware/go-fcm
|
mockgen -source=geth/common/notification.go -destination=geth/common/notification_mock.go -package=common -imports fcm=github.com/NaySoftware/go-fcm
|
||||||
mockgen -source=geth/notification/fcm/client.go -destination=geth/notification/fcm/client_mock.go -package=fcm -imports fcm=github.com/NaySoftware/go-fcm
|
mockgen -source=geth/notification/fcm/client.go -destination=geth/notification/fcm/client_mock.go -package=fcm -imports fcm=github.com/NaySoftware/go-fcm
|
||||||
|
mockgen -source=geth/txqueue/fake/txservice.go -destination=geth/txqueue/fake/mock.go -package=fake
|
||||||
|
|
||||||
test: test-unit-coverage ##@tests Run basic, short tests during development
|
test: test-unit-coverage ##@tests Run basic, short tests during development
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go
|
||||||
|
index 362379cc..6e12e500 100644
|
||||||
|
--- a/internal/ethapi/api.go
|
||||||
|
+++ b/internal/ethapi/api.go
|
||||||
|
@@ -956,6 +956,14 @@ func (s *PublicTransactionPoolAPI) GetRawTransactionByBlockHashAndIndex(ctx cont
|
||||||
|
|
||||||
|
// GetTransactionCount returns the number of transactions the given address has sent for the given block number
|
||||||
|
func (s *PublicTransactionPoolAPI) GetTransactionCount(ctx context.Context, address common.Address, blockNr rpc.BlockNumber) (*hexutil.Uint64, error) {
|
||||||
|
+ // go-ethereum issue https://github.com/ethereum/go-ethereum/issues/2880
|
||||||
|
+ if blockNr == rpc.PendingBlockNumber {
|
||||||
|
+ nonce, err := s.b.GetPoolNonce(ctx, address)
|
||||||
|
+ if err != nil {
|
||||||
|
+ return nil, err
|
||||||
|
+ }
|
||||||
|
+ return (*hexutil.Uint64)(&nonce), nil
|
||||||
|
+ }
|
||||||
|
state, _, err := s.b.StateAndHeaderByNumber(ctx, blockNr)
|
||||||
|
if state == nil || err != nil {
|
||||||
|
return nil, err
|
|
@ -16,6 +16,7 @@ We try to minimize number and amount of changes in those patches as much as poss
|
||||||
- `0005-whisper-delivery.patch` - adds support for logs/traces of Whisper traffic (questionable, nobody used this functionality so far)
|
- `0005-whisper-delivery.patch` - adds support for logs/traces of Whisper traffic (questionable, nobody used this functionality so far)
|
||||||
- `0006-latest-cht.patch` – updates CHT root hashes, should be updated regularly to keep sync fast, until proper Trusted Checkpoint sync is not implemented as part of LES/2 protocol.
|
- `0006-latest-cht.patch` – updates CHT root hashes, should be updated regularly to keep sync fast, until proper Trusted Checkpoint sync is not implemented as part of LES/2 protocol.
|
||||||
- `0007-README.patch` — update upstream README.md.
|
- `0007-README.patch` — update upstream README.md.
|
||||||
|
- `0008-tx-pool-nonce.patch` - On GetTransactionCount request with PendingBlockNumber get the nonce from transaction pool
|
||||||
|
|
||||||
# Updating upstream version
|
# Updating upstream version
|
||||||
|
|
||||||
|
|
|
@ -90,8 +90,6 @@ func (m *StatusBackend) StartNode(config *params.NodeConfig) (<-chan struct{}, e
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
m.txQueueManager.Start()
|
|
||||||
|
|
||||||
m.nodeReady = make(chan struct{}, 1)
|
m.nodeReady = make(chan struct{}, 1)
|
||||||
go m.onNodeStart(nodeStarted, m.nodeReady) // waits on nodeStarted, writes to backendReady
|
go m.onNodeStart(nodeStarted, m.nodeReady) // waits on nodeStarted, writes to backendReady
|
||||||
|
|
||||||
|
@ -102,6 +100,10 @@ func (m *StatusBackend) StartNode(config *params.NodeConfig) (<-chan struct{}, e
|
||||||
func (m *StatusBackend) onNodeStart(nodeStarted <-chan struct{}, backendReady chan struct{}) {
|
func (m *StatusBackend) onNodeStart(nodeStarted <-chan struct{}, backendReady chan struct{}) {
|
||||||
<-nodeStarted
|
<-nodeStarted
|
||||||
|
|
||||||
|
// tx queue manager should be started after node is started, it depends
|
||||||
|
// on rpc client being created
|
||||||
|
m.txQueueManager.Start()
|
||||||
|
|
||||||
if err := m.registerHandlers(); err != nil {
|
if err := m.registerHandlers(); err != nil {
|
||||||
log.Error("Handler registration failed", "err", err)
|
log.Error("Handler registration failed", "err", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
// copy of go-ethereum/internal/ethapi/addrlock.go
|
||||||
|
|
||||||
|
package txqueue
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AddrLocker struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
locks map[common.Address]*sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// lock returns the lock of the given address.
|
||||||
|
func (l *AddrLocker) lock(address common.Address) *sync.Mutex {
|
||||||
|
l.mu.Lock()
|
||||||
|
defer l.mu.Unlock()
|
||||||
|
if l.locks == nil {
|
||||||
|
l.locks = make(map[common.Address]*sync.Mutex)
|
||||||
|
}
|
||||||
|
if _, ok := l.locks[address]; !ok {
|
||||||
|
l.locks[address] = new(sync.Mutex)
|
||||||
|
}
|
||||||
|
return l.locks[address]
|
||||||
|
}
|
||||||
|
|
||||||
|
// LockAddr locks an account's mutex. This is used to prevent another tx getting the
|
||||||
|
// same nonce until the lock is released. The mutex prevents the (an identical nonce) from
|
||||||
|
// being read again during the time that the first transaction is being signed.
|
||||||
|
func (l *AddrLocker) LockAddr(address common.Address) {
|
||||||
|
l.lock(address).Lock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnlockAddr unlocks the mutex of the given account.
|
||||||
|
func (l *AddrLocker) UnlockAddr(address common.Address) {
|
||||||
|
l.lock(address).Unlock()
|
||||||
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
package txqueue
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"math/big"
|
||||||
|
|
||||||
|
ethereum "github.com/ethereum/go-ethereum"
|
||||||
|
"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/status-im/status-go/geth/rpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EthTransactor provides methods to create transactions for ethereum network.
|
||||||
|
type EthTransactor interface {
|
||||||
|
PendingNonceAt(ctx context.Context, account common.Address) (uint64, error)
|
||||||
|
ethereum.GasEstimator
|
||||||
|
ethereum.GasPricer
|
||||||
|
ethereum.TransactionSender
|
||||||
|
}
|
||||||
|
|
||||||
|
// EthTxClient wraps common API methods that are used to send transaction.
|
||||||
|
type EthTxClient struct {
|
||||||
|
c *rpc.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEthTxClient(client *rpc.Client) *EthTxClient {
|
||||||
|
return &EthTxClient{c: client}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PendingNonceAt returns the account nonce of the given account in the pending state.
|
||||||
|
// This is the nonce that should be used for the next transaction.
|
||||||
|
func (ec *EthTxClient) PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) {
|
||||||
|
var result hexutil.Uint64
|
||||||
|
err := ec.c.CallContext(ctx, &result, "eth_getTransactionCount", account, "pending")
|
||||||
|
return uint64(result), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SuggestGasPrice retrieves the currently suggested gas price to allow a timely
|
||||||
|
// execution of a transaction.
|
||||||
|
func (ec *EthTxClient) SuggestGasPrice(ctx context.Context) (*big.Int, error) {
|
||||||
|
var hex hexutil.Big
|
||||||
|
if err := ec.c.CallContext(ctx, &hex, "eth_gasPrice"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return (*big.Int)(&hex), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EstimateGas tries to estimate the gas needed to execute a specific transaction based on
|
||||||
|
// the current pending state of the backend blockchain. There is no guarantee that this is
|
||||||
|
// the true gas limit requirement as other transactions may be added or removed by miners,
|
||||||
|
// but it should provide a basis for setting a reasonable default.
|
||||||
|
func (ec *EthTxClient) EstimateGas(ctx context.Context, msg ethereum.CallMsg) (*big.Int, error) {
|
||||||
|
var hex hexutil.Big
|
||||||
|
err := ec.c.CallContext(ctx, &hex, "eth_estimateGas", toCallArg(msg))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return (*big.Int)(&hex), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendTransaction injects a signed transaction into the pending pool for execution.
|
||||||
|
//
|
||||||
|
// If the transaction was a contract creation use the TransactionReceipt method to get the
|
||||||
|
// contract address after the transaction has been mined.
|
||||||
|
func (ec *EthTxClient) SendTransaction(ctx context.Context, tx *types.Transaction) error {
|
||||||
|
data, err := rlp.EncodeToBytes(tx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return ec.c.CallContext(ctx, nil, "eth_sendRawTransaction", common.ToHex(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func toCallArg(msg ethereum.CallMsg) interface{} {
|
||||||
|
arg := map[string]interface{}{
|
||||||
|
"from": msg.From,
|
||||||
|
"to": msg.To,
|
||||||
|
}
|
||||||
|
if len(msg.Data) > 0 {
|
||||||
|
arg["data"] = hexutil.Bytes(msg.Data)
|
||||||
|
}
|
||||||
|
if msg.Value != nil {
|
||||||
|
arg["value"] = (*hexutil.Big)(msg.Value)
|
||||||
|
}
|
||||||
|
if msg.Gas != nil {
|
||||||
|
arg["gas"] = (*hexutil.Big)(msg.Gas)
|
||||||
|
}
|
||||||
|
if msg.GasPrice != nil {
|
||||||
|
arg["gasPrice"] = (*hexutil.Big)(msg.GasPrice)
|
||||||
|
}
|
||||||
|
return arg
|
||||||
|
}
|
|
@ -0,0 +1,90 @@
|
||||||
|
// Code generated by MockGen. DO NOT EDIT.
|
||||||
|
// Source: geth/txqueue/fake/txservice.go
|
||||||
|
|
||||||
|
// Package fake is a generated GoMock package.
|
||||||
|
package fake
|
||||||
|
|
||||||
|
import (
|
||||||
|
context "context"
|
||||||
|
common "github.com/ethereum/go-ethereum/common"
|
||||||
|
hexutil "github.com/ethereum/go-ethereum/common/hexutil"
|
||||||
|
rpc "github.com/ethereum/go-ethereum/rpc"
|
||||||
|
gomock "github.com/golang/mock/gomock"
|
||||||
|
big "math/big"
|
||||||
|
reflect "reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockFakePublicTransactionPoolAPI is a mock of FakePublicTransactionPoolAPI interface
|
||||||
|
type MockFakePublicTransactionPoolAPI struct {
|
||||||
|
ctrl *gomock.Controller
|
||||||
|
recorder *MockFakePublicTransactionPoolAPIMockRecorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockFakePublicTransactionPoolAPIMockRecorder is the mock recorder for MockFakePublicTransactionPoolAPI
|
||||||
|
type MockFakePublicTransactionPoolAPIMockRecorder struct {
|
||||||
|
mock *MockFakePublicTransactionPoolAPI
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockFakePublicTransactionPoolAPI creates a new mock instance
|
||||||
|
func NewMockFakePublicTransactionPoolAPI(ctrl *gomock.Controller) *MockFakePublicTransactionPoolAPI {
|
||||||
|
mock := &MockFakePublicTransactionPoolAPI{ctrl: ctrl}
|
||||||
|
mock.recorder = &MockFakePublicTransactionPoolAPIMockRecorder{mock}
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXPECT returns an object that allows the caller to indicate expected use
|
||||||
|
func (m *MockFakePublicTransactionPoolAPI) EXPECT() *MockFakePublicTransactionPoolAPIMockRecorder {
|
||||||
|
return m.recorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// GasPrice mocks base method
|
||||||
|
func (m *MockFakePublicTransactionPoolAPI) GasPrice(ctx context.Context) (*big.Int, error) {
|
||||||
|
ret := m.ctrl.Call(m, "GasPrice", ctx)
|
||||||
|
ret0, _ := ret[0].(*big.Int)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GasPrice indicates an expected call of GasPrice
|
||||||
|
func (mr *MockFakePublicTransactionPoolAPIMockRecorder) GasPrice(ctx interface{}) *gomock.Call {
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GasPrice", reflect.TypeOf((*MockFakePublicTransactionPoolAPI)(nil).GasPrice), ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EstimateGas mocks base method
|
||||||
|
func (m *MockFakePublicTransactionPoolAPI) EstimateGas(ctx context.Context, args CallArgs) (*hexutil.Big, error) {
|
||||||
|
ret := m.ctrl.Call(m, "EstimateGas", ctx, args)
|
||||||
|
ret0, _ := ret[0].(*hexutil.Big)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// EstimateGas indicates an expected call of EstimateGas
|
||||||
|
func (mr *MockFakePublicTransactionPoolAPIMockRecorder) EstimateGas(ctx, args interface{}) *gomock.Call {
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EstimateGas", reflect.TypeOf((*MockFakePublicTransactionPoolAPI)(nil).EstimateGas), ctx, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTransactionCount mocks base method
|
||||||
|
func (m *MockFakePublicTransactionPoolAPI) GetTransactionCount(ctx context.Context, address common.Address, blockNr rpc.BlockNumber) (*hexutil.Uint64, error) {
|
||||||
|
ret := m.ctrl.Call(m, "GetTransactionCount", ctx, address, blockNr)
|
||||||
|
ret0, _ := ret[0].(*hexutil.Uint64)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTransactionCount indicates an expected call of GetTransactionCount
|
||||||
|
func (mr *MockFakePublicTransactionPoolAPIMockRecorder) GetTransactionCount(ctx, address, blockNr interface{}) *gomock.Call {
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTransactionCount", reflect.TypeOf((*MockFakePublicTransactionPoolAPI)(nil).GetTransactionCount), ctx, address, blockNr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendRawTransaction mocks base method
|
||||||
|
func (m *MockFakePublicTransactionPoolAPI) SendRawTransaction(ctx context.Context, encodedTx hexutil.Bytes) (common.Hash, error) {
|
||||||
|
ret := m.ctrl.Call(m, "SendRawTransaction", ctx, encodedTx)
|
||||||
|
ret0, _ := ret[0].(common.Hash)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendRawTransaction indicates an expected call of SendRawTransaction
|
||||||
|
func (mr *MockFakePublicTransactionPoolAPIMockRecorder) SendRawTransaction(ctx, encodedTx interface{}) *gomock.Call {
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendRawTransaction", reflect.TypeOf((*MockFakePublicTransactionPoolAPI)(nil).SendRawTransaction), ctx, encodedTx)
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package fake
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"math/big"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||||
|
"github.com/ethereum/go-ethereum/rpc"
|
||||||
|
"github.com/golang/mock/gomock"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewTestServer(ctrl *gomock.Controller) (*rpc.Server, *MockFakePublicTransactionPoolAPI) {
|
||||||
|
srv := rpc.NewServer()
|
||||||
|
svc := NewMockFakePublicTransactionPoolAPI(ctrl)
|
||||||
|
if err := srv.RegisterName("eth", svc); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return srv, svc
|
||||||
|
}
|
||||||
|
|
||||||
|
// CallArgs copied from module go-ethereum/internal/ethapi
|
||||||
|
type CallArgs 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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FakePublicTransactionPoolAPI used to generate mock by mockgen util.
|
||||||
|
// This was done because PublicTransactionPoolAPI is located in internal/ethapi module
|
||||||
|
// and there is no easy way to generate mocks from internal modules.
|
||||||
|
type FakePublicTransactionPoolAPI interface {
|
||||||
|
GasPrice(ctx context.Context) (*big.Int, error)
|
||||||
|
EstimateGas(ctx context.Context, args CallArgs) (*hexutil.Big, error)
|
||||||
|
GetTransactionCount(ctx context.Context, address common.Address, blockNr rpc.BlockNumber) (*hexutil.Uint64, error)
|
||||||
|
SendRawTransaction(ctx context.Context, encodedTx hexutil.Bytes) (common.Hash, error)
|
||||||
|
}
|
|
@ -5,12 +5,10 @@ import (
|
||||||
"math/big"
|
"math/big"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
ethereum "github.com/ethereum/go-ethereum"
|
||||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||||
gethcommon "github.com/ethereum/go-ethereum/common"
|
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/core/types"
|
||||||
"github.com/ethereum/go-ethereum/les/status"
|
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
"github.com/pborman/uuid"
|
"github.com/pborman/uuid"
|
||||||
"github.com/status-im/status-go/geth/common"
|
"github.com/status-im/status-go/geth/common"
|
||||||
"github.com/status-im/status-go/geth/log"
|
"github.com/status-im/status-go/geth/log"
|
||||||
|
@ -26,6 +24,10 @@ const (
|
||||||
|
|
||||||
// SendTxDefaultErrorCode is sent by default, when error is not nil, but type is unknown/unexpected.
|
// SendTxDefaultErrorCode is sent by default, when error is not nil, but type is unknown/unexpected.
|
||||||
SendTxDefaultErrorCode = SendTransactionDefaultErrorCode
|
SendTxDefaultErrorCode = SendTransactionDefaultErrorCode
|
||||||
|
|
||||||
|
defaultGas = 90000
|
||||||
|
|
||||||
|
defaultTimeout = time.Minute
|
||||||
)
|
)
|
||||||
|
|
||||||
// Send transaction response codes
|
// Send transaction response codes
|
||||||
|
@ -49,6 +51,8 @@ type Manager struct {
|
||||||
nodeManager common.NodeManager
|
nodeManager common.NodeManager
|
||||||
accountManager common.AccountManager
|
accountManager common.AccountManager
|
||||||
txQueue *TxQueue
|
txQueue *TxQueue
|
||||||
|
ethTxClient EthTransactor
|
||||||
|
addrLock *AddrLocker
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewManager returns a new Manager.
|
// NewManager returns a new Manager.
|
||||||
|
@ -57,12 +61,14 @@ func NewManager(nodeManager common.NodeManager, accountManager common.AccountMan
|
||||||
nodeManager: nodeManager,
|
nodeManager: nodeManager,
|
||||||
accountManager: accountManager,
|
accountManager: accountManager,
|
||||||
txQueue: NewTransactionQueue(),
|
txQueue: NewTransactionQueue(),
|
||||||
|
addrLock: &AddrLocker{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start starts accepting new transactions into the queue.
|
// Start starts accepting new transactions into the queue.
|
||||||
func (m *Manager) Start() {
|
func (m *Manager) Start() {
|
||||||
log.Info("start Manager")
|
log.Info("start Manager")
|
||||||
|
m.ethTxClient = NewEthTxClient(m.nodeManager.RPCClient())
|
||||||
m.txQueue.Start()
|
m.txQueue.Start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -157,21 +163,8 @@ func (m *Manager) CompleteTransaction(id common.QueuedTxID, password string) (ge
|
||||||
m.NotifyOnQueuedTxReturn(queuedTx, ErrInvalidCompleteTxSender)
|
m.NotifyOnQueuedTxReturn(queuedTx, ErrInvalidCompleteTxSender)
|
||||||
return gethcommon.Hash{}, 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.
|
// Send the transaction finally.
|
||||||
var hash gethcommon.Hash
|
hash, err := m.completeTransaction(queuedTx, selectedAccount, password)
|
||||||
|
|
||||||
if config.UpstreamConfig.Enabled {
|
|
||||||
hash, err = m.completeRemoteTransaction(queuedTx, password)
|
|
||||||
} else {
|
|
||||||
hash, err = m.completeLocalTransaction(queuedTx, password)
|
|
||||||
}
|
|
||||||
|
|
||||||
// when incorrect sender tries to complete the account,
|
// when incorrect sender tries to complete the account,
|
||||||
// notify and keep tx in queue (so that correct sender can complete)
|
// notify and keep tx in queue (so that correct sender can complete)
|
||||||
|
@ -190,78 +183,64 @@ func (m *Manager) CompleteTransaction(id common.QueuedTxID, password string) (ge
|
||||||
return hash, err
|
return hash, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const cancelTimeout = time.Minute
|
func (m *Manager) completeTransaction(queuedTx *common.QueuedTx, selectedAccount *common.SelectedExtKey, password string) (gethcommon.Hash, error) {
|
||||||
|
log.Info("complete transaction", "id", queuedTx.ID)
|
||||||
func (m *Manager) 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
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), cancelTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
return les.StatusBackend.SendTransaction(ctx, status.SendTxArgs(queuedTx.Args), password)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) completeRemoteTransaction(queuedTx *common.QueuedTx, password string) (gethcommon.Hash, error) {
|
|
||||||
log.Info("complete transaction using upstream node", "id", queuedTx.ID)
|
|
||||||
|
|
||||||
var emptyHash gethcommon.Hash
|
var emptyHash gethcommon.Hash
|
||||||
|
|
||||||
config, err := m.nodeManager.NodeConfig()
|
config, err := m.nodeManager.NodeConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return emptyHash, err
|
return emptyHash, err
|
||||||
}
|
}
|
||||||
|
_, err = m.accountManager.VerifyAccountPassword(config.KeyStoreDir, selectedAccount.Address.String(), password)
|
||||||
selectedAcct, err := m.accountManager.SelectedAccount()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Warn("failed to verify account", "account", selectedAccount.Address.String(), "error", err.Error())
|
||||||
return emptyHash, err
|
return emptyHash, err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = m.accountManager.VerifyAccountPassword(config.KeyStoreDir, selectedAcct.Address.String(), password)
|
// update transaction with nonce, gas price and gas estimates
|
||||||
if err != nil {
|
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
|
||||||
log.Warn("failed to verify account", "account", selectedAcct.Address.String(), "error", err.Error())
|
|
||||||
return emptyHash, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// We need to request a new transaction nounce from upstream node.
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), cancelTimeout)
|
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
m.addrLock.LockAddr(queuedTx.Args.From)
|
||||||
var txCount hexutil.Uint
|
defer m.addrLock.UnlockAddr(queuedTx.Args.From)
|
||||||
client := m.nodeManager.RPCClient()
|
nonce, err := m.ethTxClient.PendingNonceAt(ctx, queuedTx.Args.From)
|
||||||
err = client.CallContext(ctx, &txCount, "eth_getTransactionCount", queuedTx.Args.From, "pending")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return emptyHash, err
|
return emptyHash, err
|
||||||
}
|
}
|
||||||
|
|
||||||
args := queuedTx.Args
|
args := queuedTx.Args
|
||||||
|
gasPrice := (*big.Int)(args.GasPrice)
|
||||||
if args.GasPrice == nil {
|
if args.GasPrice == nil {
|
||||||
value, gasPriceErr := m.gasPrice()
|
ctx, cancel = context.WithTimeout(context.Background(), defaultTimeout)
|
||||||
if gasPriceErr != nil {
|
defer cancel()
|
||||||
return emptyHash, gasPriceErr
|
gasPrice, err = m.ethTxClient.SuggestGasPrice(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return emptyHash, err
|
||||||
}
|
}
|
||||||
|
|
||||||
args.GasPrice = value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
chainID := big.NewInt(int64(config.NetworkID))
|
chainID := big.NewInt(int64(config.NetworkID))
|
||||||
nonce := uint64(txCount)
|
|
||||||
gasPrice := (*big.Int)(args.GasPrice)
|
|
||||||
data := []byte(args.Data)
|
data := []byte(args.Data)
|
||||||
value := (*big.Int)(args.Value)
|
value := (*big.Int)(args.Value)
|
||||||
toAddr := gethcommon.Address{}
|
toAddr := gethcommon.Address{}
|
||||||
if args.To != nil {
|
if args.To != nil {
|
||||||
toAddr = *args.To
|
toAddr = *args.To
|
||||||
}
|
}
|
||||||
|
gas := (*big.Int)(args.Gas)
|
||||||
gas, err := m.estimateGas(args)
|
if args.Gas == nil {
|
||||||
if err != nil {
|
ctx, cancel = context.WithTimeout(context.Background(), defaultTimeout)
|
||||||
return emptyHash, err
|
defer cancel()
|
||||||
|
gas, err = m.ethTxClient.EstimateGas(ctx, ethereum.CallMsg{
|
||||||
|
From: args.From,
|
||||||
|
To: args.To,
|
||||||
|
GasPrice: gasPrice,
|
||||||
|
Value: value,
|
||||||
|
Data: data,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return emptyHash, err
|
||||||
|
}
|
||||||
|
if gas.Cmp(big.NewInt(defaultGas)) == -1 {
|
||||||
|
log.Info("default gas will be used. estimated gas", gas, "is lower than", defaultGas)
|
||||||
|
gas = big.NewInt(defaultGas)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info(
|
log.Info(
|
||||||
|
@ -272,90 +251,19 @@ func (m *Manager) completeRemoteTransaction(queuedTx *common.QueuedTx, password
|
||||||
"gasPrice", gasPrice,
|
"gasPrice", gasPrice,
|
||||||
"value", value,
|
"value", value,
|
||||||
)
|
)
|
||||||
|
tx := types.NewTransaction(nonce, toAddr, value, gas, gasPrice, data)
|
||||||
tx := types.NewTransaction(nonce, toAddr, value, (*big.Int)(gas), gasPrice, data)
|
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), selectedAccount.AccountKey.PrivateKey)
|
||||||
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), selectedAcct.AccountKey.PrivateKey)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return emptyHash, err
|
return emptyHash, err
|
||||||
}
|
}
|
||||||
|
ctx, cancel = context.WithTimeout(context.Background(), defaultTimeout)
|
||||||
txBytes, err := rlp.EncodeToBytes(signedTx)
|
defer cancel()
|
||||||
if err != nil {
|
if err := m.ethTxClient.SendTransaction(ctx, signedTx); err != nil {
|
||||||
return emptyHash, err
|
return emptyHash, err
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx2, cancel2 := context.WithTimeout(context.Background(), cancelTimeout)
|
|
||||||
defer cancel2()
|
|
||||||
|
|
||||||
if err := client.CallContext(ctx2, nil, "eth_sendRawTransaction", gethcommon.ToHex(txBytes)); err != nil {
|
|
||||||
return emptyHash, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return signedTx.Hash(), nil
|
return signedTx.Hash(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) estimateGas(args common.SendTxArgs) (*hexutil.Big, error) {
|
|
||||||
if args.Gas != nil {
|
|
||||||
return args.Gas, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
client := m.nodeManager.RPCClient()
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), cancelTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
var gasPrice hexutil.Big
|
|
||||||
if args.GasPrice != nil {
|
|
||||||
gasPrice = *args.GasPrice
|
|
||||||
}
|
|
||||||
|
|
||||||
var value hexutil.Big
|
|
||||||
if args.Value != nil {
|
|
||||||
value = *args.Value
|
|
||||||
}
|
|
||||||
|
|
||||||
params := struct {
|
|
||||||
From gethcommon.Address `json:"from"`
|
|
||||||
To *gethcommon.Address `json:"to"`
|
|
||||||
Gas hexutil.Big `json:"gas"`
|
|
||||||
GasPrice hexutil.Big `json:"gasPrice"`
|
|
||||||
Value hexutil.Big `json:"value"`
|
|
||||||
Data hexutil.Bytes `json:"data"`
|
|
||||||
}{
|
|
||||||
From: args.From,
|
|
||||||
To: args.To,
|
|
||||||
GasPrice: gasPrice,
|
|
||||||
Value: value,
|
|
||||||
Data: []byte(args.Data),
|
|
||||||
}
|
|
||||||
|
|
||||||
var estimatedGas hexutil.Big
|
|
||||||
if err := client.CallContext(
|
|
||||||
ctx,
|
|
||||||
&estimatedGas,
|
|
||||||
"eth_estimateGas",
|
|
||||||
params,
|
|
||||||
); err != nil {
|
|
||||||
log.Warn("failed to estimate gas", "err", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &estimatedGas, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) gasPrice() (*hexutil.Big, error) {
|
|
||||||
client := m.nodeManager.RPCClient()
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), cancelTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
var gasPrice hexutil.Big
|
|
||||||
if err := client.CallContext(ctx, &gasPrice, "eth_gasPrice"); err != nil {
|
|
||||||
log.Warn("failed to get gas price", "err", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &gasPrice, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CompleteTransactions instructs backend to complete sending of multiple transactions
|
// CompleteTransactions instructs backend to complete sending of multiple transactions
|
||||||
func (m *Manager) CompleteTransactions(ids []common.QueuedTxID, password string) map[common.QueuedTxID]common.RawCompleteTransactionResult {
|
func (m *Manager) CompleteTransactions(ids []common.QueuedTxID, password string) map[common.QueuedTxID]common.RawCompleteTransactionResult {
|
||||||
results := make(map[common.QueuedTxID]common.RawCompleteTransactionResult)
|
results := make(map[common.QueuedTxID]common.RawCompleteTransactionResult)
|
||||||
|
|
|
@ -2,22 +2,26 @@ package txqueue
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"math/big"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||||
"github.com/stretchr/testify/suite"
|
gethcommon "github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||||
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
|
gethrpc "github.com/ethereum/go-ethereum/rpc"
|
||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
"github.com/status-im/status-go/geth/common"
|
"github.com/status-im/status-go/geth/common"
|
||||||
"github.com/status-im/status-go/geth/params"
|
"github.com/status-im/status-go/geth/params"
|
||||||
|
"github.com/status-im/status-go/geth/rpc"
|
||||||
|
"github.com/status-im/status-go/geth/txqueue/fake"
|
||||||
. "github.com/status-im/status-go/testing"
|
. "github.com/status-im/status-go/testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
var errTxAssumedSent = errors.New("assume tx is done")
|
|
||||||
|
|
||||||
func TestTxQueueTestSuite(t *testing.T) {
|
func TestTxQueueTestSuite(t *testing.T) {
|
||||||
suite.Run(t, new(TxQueueTestSuite))
|
suite.Run(t, new(TxQueueTestSuite))
|
||||||
}
|
}
|
||||||
|
@ -28,34 +32,61 @@ type TxQueueTestSuite struct {
|
||||||
nodeManagerMock *common.MockNodeManager
|
nodeManagerMock *common.MockNodeManager
|
||||||
accountManagerMockCtrl *gomock.Controller
|
accountManagerMockCtrl *gomock.Controller
|
||||||
accountManagerMock *common.MockAccountManager
|
accountManagerMock *common.MockAccountManager
|
||||||
|
server *gethrpc.Server
|
||||||
|
client *gethrpc.Client
|
||||||
|
txServiceMockCtrl *gomock.Controller
|
||||||
|
txServiceMock *fake.MockFakePublicTransactionPoolAPI
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *TxQueueTestSuite) SetupTest() {
|
func (s *TxQueueTestSuite) SetupTest() {
|
||||||
s.nodeManagerMockCtrl = gomock.NewController(s.T())
|
s.nodeManagerMockCtrl = gomock.NewController(s.T())
|
||||||
s.accountManagerMockCtrl = gomock.NewController(s.T())
|
s.accountManagerMockCtrl = gomock.NewController(s.T())
|
||||||
|
s.txServiceMockCtrl = gomock.NewController(s.T())
|
||||||
|
|
||||||
s.nodeManagerMock = common.NewMockNodeManager(s.nodeManagerMockCtrl)
|
s.nodeManagerMock = common.NewMockNodeManager(s.nodeManagerMockCtrl)
|
||||||
s.accountManagerMock = common.NewMockAccountManager(s.accountManagerMockCtrl)
|
s.accountManagerMock = common.NewMockAccountManager(s.accountManagerMockCtrl)
|
||||||
|
|
||||||
|
s.server, s.txServiceMock = fake.NewTestServer(s.txServiceMockCtrl)
|
||||||
|
s.client = gethrpc.DialInProc(s.server)
|
||||||
|
rpclient, _ := rpc.NewClient(s.client, params.UpstreamRPCConfig{})
|
||||||
|
s.nodeManagerMock.EXPECT().RPCClient().Return(rpclient)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *TxQueueTestSuite) TearDownTest() {
|
func (s *TxQueueTestSuite) TearDownTest() {
|
||||||
s.nodeManagerMockCtrl.Finish()
|
s.nodeManagerMockCtrl.Finish()
|
||||||
s.accountManagerMockCtrl.Finish()
|
s.accountManagerMockCtrl.Finish()
|
||||||
|
s.txServiceMockCtrl.Finish()
|
||||||
|
s.server.Stop()
|
||||||
|
s.client.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TxQueueTestSuite) setupTransactionPoolAPI(account *common.SelectedExtKey, nonce hexutil.Uint64, gas hexutil.Big, txErr error) {
|
||||||
|
s.txServiceMock.EXPECT().GetTransactionCount(gomock.Any(), account.Address, gethrpc.PendingBlockNumber).Return(&nonce, nil)
|
||||||
|
s.txServiceMock.EXPECT().GasPrice(gomock.Any()).Return(big.NewInt(10), nil)
|
||||||
|
s.txServiceMock.EXPECT().EstimateGas(gomock.Any(), gomock.Any()).Return(&gas, nil)
|
||||||
|
s.txServiceMock.EXPECT().SendRawTransaction(gomock.Any(), gomock.Any()).Return(gethcommon.Hash{}, txErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TxQueueTestSuite) setupStatusBackend(account *common.SelectedExtKey, password string, passwordErr error) {
|
||||||
|
nodeConfig, nodeErr := params.NewNodeConfig("/tmp", params.RopstenNetworkID, true)
|
||||||
|
s.nodeManagerMock.EXPECT().NodeConfig().Return(nodeConfig, nodeErr)
|
||||||
|
s.accountManagerMock.EXPECT().SelectedAccount().Return(account, nil)
|
||||||
|
s.accountManagerMock.EXPECT().VerifyAccountPassword(nodeConfig.KeyStoreDir, account.Address.String(), password).Return(
|
||||||
|
nil, passwordErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *TxQueueTestSuite) TestCompleteTransaction() {
|
func (s *TxQueueTestSuite) TestCompleteTransaction() {
|
||||||
s.accountManagerMock.EXPECT().SelectedAccount().Return(&common.SelectedExtKey{
|
password := TestConfig.Account1.Password
|
||||||
Address: common.FromAddress(TestConfig.Account1.Address),
|
key, _ := crypto.GenerateKey()
|
||||||
}, nil)
|
account := &common.SelectedExtKey{
|
||||||
|
Address: common.FromAddress(TestConfig.Account1.Address),
|
||||||
|
AccountKey: &keystore.Key{PrivateKey: key},
|
||||||
|
}
|
||||||
|
s.setupStatusBackend(account, password, nil)
|
||||||
|
|
||||||
s.nodeManagerMock.EXPECT().NodeConfig().Return(
|
nonce := hexutil.Uint64(10)
|
||||||
params.NewNodeConfig("/tmp", params.RopstenNetworkID, true),
|
gas := hexutil.Big(*big.NewInt(defaultGas + 1))
|
||||||
)
|
s.setupTransactionPoolAPI(account, nonce, gas, nil)
|
||||||
|
|
||||||
// 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 := NewManager(s.nodeManagerMock, s.accountManagerMock)
|
txQueueManager := NewManager(s.nodeManagerMock, s.accountManagerMock)
|
||||||
|
|
||||||
|
@ -74,38 +105,41 @@ func (s *TxQueueTestSuite) TestCompleteTransaction() {
|
||||||
|
|
||||||
txQueueManager.SetTransactionReturnHandler(func(queuedTx *common.QueuedTx, err error) {
|
txQueueManager.SetTransactionReturnHandler(func(queuedTx *common.QueuedTx, err error) {
|
||||||
s.Equal(tx.ID, queuedTx.ID)
|
s.Equal(tx.ID, queuedTx.ID)
|
||||||
s.Equal(errTxAssumedSent, err)
|
s.NoError(err)
|
||||||
})
|
})
|
||||||
|
|
||||||
err := txQueueManager.QueueTransaction(tx)
|
err := txQueueManager.QueueTransaction(tx)
|
||||||
s.NoError(err)
|
s.NoError(err)
|
||||||
|
|
||||||
|
w := make(chan struct{})
|
||||||
go func() {
|
go func() {
|
||||||
_, errCompleteTransaction := txQueueManager.CompleteTransaction(tx.ID, TestConfig.Account1.Password)
|
hash, err := txQueueManager.CompleteTransaction(tx.ID, password)
|
||||||
s.Equal(errTxAssumedSent, errCompleteTransaction)
|
s.NoError(err)
|
||||||
|
s.Equal(tx.Hash, hash)
|
||||||
|
close(w)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
err = txQueueManager.WaitForTransaction(tx)
|
err = txQueueManager.WaitForTransaction(tx)
|
||||||
s.Equal(errTxAssumedSent, err)
|
s.NoError(err)
|
||||||
// Check that error is assigned to the transaction.
|
// Check that error is assigned to the transaction.
|
||||||
s.Equal(errTxAssumedSent, tx.Err)
|
s.NoError(tx.Err)
|
||||||
// Transaction should be already removed from the queue.
|
// Transaction should be already removed from the queue.
|
||||||
s.False(txQueueManager.TransactionQueue().Has(tx.ID))
|
s.False(txQueueManager.TransactionQueue().Has(tx.ID))
|
||||||
|
s.NoError(WaitClosed(w, time.Second))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *TxQueueTestSuite) TestCompleteTransactionMultipleTimes() {
|
func (s *TxQueueTestSuite) TestCompleteTransactionMultipleTimes() {
|
||||||
s.accountManagerMock.EXPECT().SelectedAccount().Return(&common.SelectedExtKey{
|
password := TestConfig.Account1.Password
|
||||||
Address: common.FromAddress(TestConfig.Account1.Address),
|
key, _ := crypto.GenerateKey()
|
||||||
}, nil)
|
account := &common.SelectedExtKey{
|
||||||
|
Address: common.FromAddress(TestConfig.Account1.Address),
|
||||||
|
AccountKey: &keystore.Key{PrivateKey: key},
|
||||||
|
}
|
||||||
|
s.setupStatusBackend(account, password, nil)
|
||||||
|
|
||||||
s.nodeManagerMock.EXPECT().NodeConfig().Return(
|
nonce := hexutil.Uint64(10)
|
||||||
params.NewNodeConfig("/tmp", params.RopstenNetworkID, true),
|
gas := hexutil.Big(*big.NewInt(defaultGas + 1))
|
||||||
)
|
s.setupTransactionPoolAPI(account, nonce, gas, nil)
|
||||||
|
|
||||||
// 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 := NewManager(s.nodeManagerMock, s.accountManagerMock)
|
txQueueManager := NewManager(s.nodeManagerMock, s.accountManagerMock)
|
||||||
|
|
||||||
|
@ -124,36 +158,47 @@ func (s *TxQueueTestSuite) TestCompleteTransactionMultipleTimes() {
|
||||||
|
|
||||||
txQueueManager.SetTransactionReturnHandler(func(queuedTx *common.QueuedTx, err error) {
|
txQueueManager.SetTransactionReturnHandler(func(queuedTx *common.QueuedTx, err error) {
|
||||||
s.Equal(tx.ID, queuedTx.ID)
|
s.Equal(tx.ID, queuedTx.ID)
|
||||||
s.Equal(errTxAssumedSent, err)
|
s.NoError(err)
|
||||||
})
|
})
|
||||||
|
|
||||||
err := txQueueManager.QueueTransaction(tx)
|
err := txQueueManager.QueueTransaction(tx)
|
||||||
s.NoError(err)
|
s.NoError(err)
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
var (
|
||||||
var mu sync.Mutex
|
wg sync.WaitGroup
|
||||||
completeTxErrors := make(map[error]int)
|
mu sync.Mutex
|
||||||
for i := 0; i < 3; i++ {
|
completedTx int
|
||||||
|
inprogressTx int
|
||||||
|
txCount = 3
|
||||||
|
)
|
||||||
|
for i := 0; i < txCount; i++ {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
_, errCompleteTransaction := txQueueManager.CompleteTransaction(tx.ID, TestConfig.Account1.Password)
|
_, err := txQueueManager.CompleteTransaction(tx.ID, password)
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
completeTxErrors[errCompleteTransaction]++
|
if err == nil {
|
||||||
|
completedTx++
|
||||||
|
} else if err == ErrQueuedTxInProgress {
|
||||||
|
inprogressTx++
|
||||||
|
} else {
|
||||||
|
s.Fail("tx failed with unexpected error: ", err.Error())
|
||||||
|
}
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
err = txQueueManager.WaitForTransaction(tx)
|
err = txQueueManager.WaitForTransaction(tx)
|
||||||
s.Equal(errTxAssumedSent, err)
|
s.NoError(err)
|
||||||
// Check that error is assigned to the transaction.
|
// Check that error is assigned to the transaction.
|
||||||
s.Equal(errTxAssumedSent, tx.Err)
|
s.NoError(tx.Err)
|
||||||
// Transaction should be already removed from the queue.
|
// Transaction should be already removed from the queue.
|
||||||
s.False(txQueueManager.TransactionQueue().Has(tx.ID))
|
s.False(txQueueManager.TransactionQueue().Has(tx.ID))
|
||||||
|
|
||||||
// Wait for all CompleteTransaction calls.
|
// Wait for all CompleteTransaction calls.
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
s.Equal(completeTxErrors[errTxAssumedSent], 1)
|
s.Equal(1, completedTx, "only 1 tx expected to be completed")
|
||||||
|
s.Equal(txCount-1, inprogressTx, "txs expected to be reported as inprogress")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *TxQueueTestSuite) TestAccountMismatch() {
|
func (s *TxQueueTestSuite) TestAccountMismatch() {
|
||||||
|
@ -196,16 +241,13 @@ func (s *TxQueueTestSuite) TestAccountMismatch() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *TxQueueTestSuite) TestInvalidPassword() {
|
func (s *TxQueueTestSuite) TestInvalidPassword() {
|
||||||
s.accountManagerMock.EXPECT().SelectedAccount().Return(&common.SelectedExtKey{
|
password := "invalid-password"
|
||||||
Address: common.FromAddress(TestConfig.Account1.Address),
|
key, _ := crypto.GenerateKey()
|
||||||
}, nil)
|
account := &common.SelectedExtKey{
|
||||||
|
Address: common.FromAddress(TestConfig.Account1.Address),
|
||||||
s.nodeManagerMock.EXPECT().NodeConfig().Return(
|
AccountKey: &keystore.Key{PrivateKey: key},
|
||||||
params.NewNodeConfig("/tmp", params.RopstenNetworkID, true),
|
}
|
||||||
)
|
s.setupStatusBackend(account, password, keystore.ErrDecrypt)
|
||||||
|
|
||||||
// Set ErrDecrypt error response as expected with a wrong password.
|
|
||||||
s.nodeManagerMock.EXPECT().LightEthereumService().Return(nil, keystore.ErrDecrypt)
|
|
||||||
|
|
||||||
txQueueManager := NewManager(s.nodeManagerMock, s.accountManagerMock)
|
txQueueManager := NewManager(s.nodeManagerMock, s.accountManagerMock)
|
||||||
|
|
||||||
|
@ -233,8 +275,8 @@ func (s *TxQueueTestSuite) TestInvalidPassword() {
|
||||||
err := txQueueManager.QueueTransaction(tx)
|
err := txQueueManager.QueueTransaction(tx)
|
||||||
s.NoError(err)
|
s.NoError(err)
|
||||||
|
|
||||||
_, err = txQueueManager.CompleteTransaction(tx.ID, "invalid-password")
|
_, err = txQueueManager.CompleteTransaction(tx.ID, password)
|
||||||
s.Equal(err, keystore.ErrDecrypt)
|
s.Equal(err.Error(), keystore.ErrDecrypt.Error())
|
||||||
|
|
||||||
// Transaction should stay in the queue as mismatched accounts
|
// Transaction should stay in the queue as mismatched accounts
|
||||||
// is a recoverable error.
|
// is a recoverable error.
|
||||||
|
@ -265,9 +307,11 @@ func (s *TxQueueTestSuite) TestDiscardTransaction() {
|
||||||
err := txQueueManager.QueueTransaction(tx)
|
err := txQueueManager.QueueTransaction(tx)
|
||||||
s.NoError(err)
|
s.NoError(err)
|
||||||
|
|
||||||
|
w := make(chan struct{})
|
||||||
go func() {
|
go func() {
|
||||||
discardErr := txQueueManager.DiscardTransaction(tx.ID)
|
err := txQueueManager.DiscardTransaction(tx.ID)
|
||||||
s.NoError(discardErr)
|
s.NoError(err)
|
||||||
|
close(w)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
err = txQueueManager.WaitForTransaction(tx)
|
err = txQueueManager.WaitForTransaction(tx)
|
||||||
|
@ -276,4 +320,5 @@ func (s *TxQueueTestSuite) TestDiscardTransaction() {
|
||||||
s.Equal(ErrQueuedTxDiscarded, tx.Err)
|
s.Equal(ErrQueuedTxDiscarded, tx.Err)
|
||||||
// Transaction should be already removed from the queue.
|
// Transaction should be already removed from the queue.
|
||||||
s.False(txQueueManager.TransactionQueue().Has(tx.ID))
|
s.False(txQueueManager.TransactionQueue().Has(tx.ID))
|
||||||
|
s.NoError(WaitClosed(w, time.Second))
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,9 @@ var (
|
||||||
// ErrNoRemoteURL is returned when network id has no associated url.
|
// ErrNoRemoteURL is returned when network id has no associated url.
|
||||||
ErrNoRemoteURL = errors.New("network id requires a remote URL")
|
ErrNoRemoteURL = errors.New("network id requires a remote URL")
|
||||||
|
|
||||||
|
// ErrTimeout is returned when test times out
|
||||||
|
ErrTimeout = errors.New("timeout")
|
||||||
|
|
||||||
// TestConfig defines the default config usable at package-level.
|
// TestConfig defines the default config usable at package-level.
|
||||||
TestConfig *common.TestConfig
|
TestConfig *common.TestConfig
|
||||||
|
|
||||||
|
@ -206,3 +209,16 @@ func GetAccount2PKFile() string {
|
||||||
return "test-account2.pk"
|
return "test-account2.pk"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WaitClosed used to wait on a channel in tests
|
||||||
|
func WaitClosed(c chan struct{}, d time.Duration) error {
|
||||||
|
timer := time.NewTimer(d)
|
||||||
|
defer timer.Stop()
|
||||||
|
select {
|
||||||
|
case <-c:
|
||||||
|
return nil
|
||||||
|
case <-timer.C:
|
||||||
|
return ErrTimeout
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -976,6 +976,14 @@ func (s *PublicTransactionPoolAPI) GetRawTransactionByBlockHashAndIndex(ctx cont
|
||||||
|
|
||||||
// GetTransactionCount returns the number of transactions the given address has sent for the given block number
|
// GetTransactionCount returns the number of transactions the given address has sent for the given block number
|
||||||
func (s *PublicTransactionPoolAPI) GetTransactionCount(ctx context.Context, address common.Address, blockNr rpc.BlockNumber) (*hexutil.Uint64, error) {
|
func (s *PublicTransactionPoolAPI) GetTransactionCount(ctx context.Context, address common.Address, blockNr rpc.BlockNumber) (*hexutil.Uint64, error) {
|
||||||
|
// go-ethereum issue https://github.com/ethereum/go-ethereum/issues/2880
|
||||||
|
if blockNr == rpc.PendingBlockNumber {
|
||||||
|
nonce, err := s.b.GetPoolNonce(ctx, address)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return (*hexutil.Uint64)(&nonce), nil
|
||||||
|
}
|
||||||
state, _, err := s.b.StateAndHeaderByNumber(ctx, blockNr)
|
state, _, err := s.b.StateAndHeaderByNumber(ctx, blockNr)
|
||||||
if state == nil || err != nil {
|
if state == nil || err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -1161,7 +1169,6 @@ func (s *PublicTransactionPoolAPI) SendTransactionWithPassphrase(ctx context.Con
|
||||||
}
|
}
|
||||||
// Assemble the transaction and sign with the wallet
|
// Assemble the transaction and sign with the wallet
|
||||||
tx := args.toTransaction()
|
tx := args.toTransaction()
|
||||||
|
|
||||||
var chainID *big.Int
|
var chainID *big.Int
|
||||||
if config := s.b.ChainConfig(); config.IsEIP155(s.b.CurrentBlock().Number()) {
|
if config := s.b.ChainConfig(); config.IsEIP155(s.b.CurrentBlock().Number()) {
|
||||||
chainID = config.ChainId
|
chainID = config.ChainId
|
||||||
|
|
Loading…
Reference in New Issue